From d4aa4c9887eb0296344cbaeac38ac4670e1c1a7a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20L=C3=B3pez?= Date: Thu, 20 Jan 2022 09:21:28 +0100 Subject: [PATCH 001/743] Project version is 4.0.0-SNAPSHOT --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index f26c5c39133..a3ac09058f3 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -projectVersion=3.3.0-SNAPSHOT +projectVersion=4.0.0-SNAPSHOT projectGroupId=io.micronaut title=Micronaut projectDesc=Natively Cloud Native From a133d73b7d6ec9237e8870b03f7c185eed202eae Mon Sep 17 00:00:00 2001 From: Cristian Greco Date: Wed, 9 Mar 2022 10:58:21 +0000 Subject: [PATCH 002/743] build: upgrade H2 for CVE-2022-23221 (#7000) * Upgrade H2 for CVE-2022-23221 Hello, As per [CVE-2022-23221](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2022-23221), we should update H2 to at least `2.1.210`. The dependency cannot be overridden via the build system because Micronaut explicitly queries H2 to get its compatibilities and fails: ``` io.micronaut.context.exceptions.BeanInstantiationException: Bean definition [javax.sql.DataSource] could not be loaded: Unable to determine H2 compatibility mode ----------------------------------------- SQL State : 42001 Error Code : 42001 Message : Syntax error in SQL statement "SELECT [*]VALUE FROM INFORMATION_SCHEMA.SETTINGS WHERE NAME = 'MODE'"; expected "TOP, DISTINCT, ALL, *, INTERSECTS, NOT, EXISTS, UNIQUE, INTERSECTS"; SQL statement: SELECT VALUE FROM INFORMATION_SCHEMA.SETTINGS WHERE NAME = 'MODE' [42001-210] ``` Therefore the fix is needed in Micronaut itself. * Update test --- gradle/libs.versions.toml | 2 +- .../management/endpoint/health/HealthEndpointSpec.groovy | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 30e0f568bfe..7353cebfb95 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -58,7 +58,7 @@ managed-gorm-hibernate = "7.1.0.M3" managed-graal = "21.3.0" managed-graal-sdk = "21.3.0" managed-groovy = "3.0.9" -managed-h2 = "1.4.200" +managed-h2 = "2.1.210" managed-hystrix = "1.5.18" managed-jaeger = "1.6.0" managed-jakarta-annotation-api = "2.0.0" diff --git a/management/src/test/groovy/io/micronaut/management/endpoint/health/HealthEndpointSpec.groovy b/management/src/test/groovy/io/micronaut/management/endpoint/health/HealthEndpointSpec.groovy index f1c7c1783c4..321e65b1cdf 100644 --- a/management/src/test/groovy/io/micronaut/management/endpoint/health/HealthEndpointSpec.groovy +++ b/management/src/test/groovy/io/micronaut/management/endpoint/health/HealthEndpointSpec.groovy @@ -159,10 +159,10 @@ class HealthEndpointSpec extends Specification { result.details.jdbc.status == "UP" result.details.jdbc.details."jdbc:h2:mem:oneDb".status == "UP" result.details.jdbc.details."jdbc:h2:mem:oneDb".details.database == "H2" - result.details.jdbc.details."jdbc:h2:mem:oneDb".details.version == "1.4.200 (2019-10-14)" + result.details.jdbc.details."jdbc:h2:mem:oneDb".details.version == "2.1.210 (2022-01-17)" result.details.jdbc.details."jdbc:h2:mem:twoDb".status == "UP" result.details.jdbc.details."jdbc:h2:mem:twoDb".details.database == "H2" - result.details.jdbc.details."jdbc:h2:mem:twoDb".details.version == "1.4.200 (2019-10-14)" + result.details.jdbc.details."jdbc:h2:mem:twoDb".details.version == "2.1.210 (2022-01-17)" result.details.service.status == "UP" cleanup: From 18accb324ab6cbb7e20dc5960bc2a32ae918e077 Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Thu, 4 Aug 2022 08:43:11 +0200 Subject: [PATCH 003/743] ci: projectVersion to 3.7.0-SNAPSHOT [ci skip] --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 0b5d42d712e..8c8f9d248b0 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -projectVersion=3.6.1-SNAPSHOT +projectVersion=3.7.0-SNAPSHOT projectGroupId=io.micronaut title=Micronaut projectDesc=Natively Cloud Native From de3d18b1d196294c90ef16e11ff37fb3ae359c65 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 16 Aug 2022 08:55:10 +0200 Subject: [PATCH 004/743] fix(deps): update htmlunit to v2.63.0 (#7854) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index bf6c14bb356..ad3869b5615 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -9,7 +9,7 @@ geb = "3.4.1" hibernate = "5.5.9.Final" hibernate-validator = "6.1.6.Final" htmlSanityCheck = "1.1.6" -htmlunit = "2.61.0" +htmlunit = "2.63.0" httpcomponents-client = "4.5.13" jakarta-inject-api = "2.0.1" jakarta-inject-tck = "2.0.1" From 525f6799487aec764542e3b31a9b2849def194af Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 16 Aug 2022 08:56:03 +0200 Subject: [PATCH 005/743] fix(deps): update dependency org.apache.logging.log4j:log4j-core to v2.18.0 (#7851) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index ad3869b5615..948ad98c243 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -22,7 +22,7 @@ jsr107 = "1.1.1" javax-el = "3.0.1-b12" javax-el-impl = "2.2.1-b05" logbook-netty = "2.14.0" -log4j = "2.17.2" +log4j = "2.18.0" selenium = "3.141.59" smallrye = "5.4.0" systemlambda = "1.2.1" From 38943e10689352992e843f4f912ca948992f1a12 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 16 Aug 2022 08:56:52 +0200 Subject: [PATCH 006/743] fix(deps): update dependency io.micronaut.neo4j:micronaut-neo4j-bolt to v5.2.0 (#7847) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 948ad98c243..b32aa62def9 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -95,7 +95,7 @@ managed-micronaut-liquibase = "5.4.1" managed-micronaut-mongo = "4.4.0" managed-micronaut-mqtt = "2.2.0" managed-micronaut-multitenancy = "4.1.0" -managed-micronaut-neo4j = "5.1.0" +managed-micronaut-neo4j = "5.2.0" managed-micronaut-nats = "3.1.0" managed-micronaut-netflix = "2.1.0" managed-micronaut-openapi = "4.4.3" From d3c5788ff6862670cb35f510847b4e19078f8310 Mon Sep 17 00:00:00 2001 From: micronaut-build <65172877+micronaut-build@users.noreply.github.com> Date: Tue, 16 Aug 2022 02:58:13 -0400 Subject: [PATCH 007/743] build: bump micronaut-test to 3.5.0 (#7824) * Bump micronaut-test to 3.5.0 * Fix kotlin version for test runtime configuration Co-authored-by: Tim Yates --- gradle/libs.versions.toml | 2 +- test-suite-kotlin/build.gradle | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index b32aa62def9..8999fc7e599 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -115,7 +115,7 @@ managed-micronaut-serialization = "1.3.0" managed-micronaut-servlet = "3.3.0" managed-micronaut-spring = "4.2.1" managed-micronaut-sql = "4.6.3" -managed-micronaut-test = "3.4.0" +managed-micronaut-test = "3.5.0" managed-micronaut-test-resources = "1.0.1" managed-micronaut-toml = "1.1.1" managed-micronaut-tracing = "4.2.1" diff --git a/test-suite-kotlin/build.gradle b/test-suite-kotlin/build.gradle index 924212cb0b7..a85f16a8d55 100644 --- a/test-suite-kotlin/build.gradle +++ b/test-suite-kotlin/build.gradle @@ -67,6 +67,14 @@ dependencies { testImplementation libs.managed.reactor } +configurations.testRuntimeClasspath { + resolutionStrategy.eachDependency { + if (it.requested.group == 'org.jetbrains.kotlin') { + it.useVersion(libs.versions.managed.kotlin.asProvider().get()) + } + } +} + tasks.named("compileTestKotlin") { kotlinOptions.jvmTarget = "1.8" } From 0b29dd833f5503f1a83af22c4d6fa7f0459a53c5 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 16 Aug 2022 08:59:38 +0200 Subject: [PATCH 008/743] fix(deps): update dependency io.methvin:directory-watcher to v0.16.1 (#7845) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 8999fc7e599..d206f8a6f4e 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -63,7 +63,7 @@ managed-ktor = "1.6.8" managed-logback = "1.2.11" managed-lombok = "1.18.24" managed-maven-native-plugin = "0.9.13" -managed-methvin-directory-watcher = "0.15.1" +managed-methvin-directory-watcher = "0.16.1" managed-micrometer = "1.9.2" managed-micronaut-acme = "3.2.0" managed-micronaut-aot = "1.1.1" From 6748d5b84fb4b86f850a21d8fa33845cbe9e9b72 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 16 Aug 2022 09:00:15 +0200 Subject: [PATCH 009/743] fix(deps): update dependency io.smallrye:smallrye-fault-tolerance to v5.5.0 (#7849) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index d206f8a6f4e..437dc9d4939 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -24,7 +24,7 @@ javax-el-impl = "2.2.1-b05" logbook-netty = "2.14.0" log4j = "2.18.0" selenium = "3.141.59" -smallrye = "5.4.0" +smallrye = "5.5.0" systemlambda = "1.2.1" vertx = "3.9.13" wiremock = "2.33.2" From a5c9c17c4850a3c906f34aa632c81fd45555981f Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 16 Aug 2022 09:00:41 +0200 Subject: [PATCH 010/743] fix(deps): update dependency io.micronaut.discovery:micronaut-discovery-client to v3.2.0 (#7846) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 437dc9d4939..a3d48b7449b 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -73,7 +73,7 @@ managed-micronaut-cache = "3.5.0" managed-micronaut-cassandra = "5.1.1" managed-micronaut-coherence = "3.5.1" managed-micronaut-data = "3.7.2" -managed-micronaut-discovery = "3.1.0" +managed-micronaut-discovery = "3.2.0" managed-micronaut-elasticsearch = "4.3.0" managed-micronaut-email = "1.3.1" managed-micronaut-flyway = "5.4.0" From 5c3fcfa43ec6f1e9e8de2080900b9a1355c86cb0 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 16 Aug 2022 09:01:11 +0200 Subject: [PATCH 011/743] fix(deps): update dependency net.java.dev.jna:jna to v5.12.1 (#7850) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index a3d48b7449b..7d59df4ae78 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -56,7 +56,7 @@ managed-jackson = "2.13.3" managed-jackson-databind = "2.13.3" managed-javax-annotation-api = "1.3.2" managed-jcache = "1.1.1" -managed-jna = "5.11.0" +managed-jna = "5.12.1" managed-jsr305 = "3.0.2" managed-kafka = "2.8.1" managed-ktor = "1.6.8" From 976698a5309c0989735743054279242c63297e37 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 16 Aug 2022 09:05:09 +0200 Subject: [PATCH 012/743] fix(deps): update dependency org.grails:grails-datastore-gorm-hibernate5 to v7.3.0 (#7853) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 7d59df4ae78..f0e4d168484 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -42,7 +42,7 @@ managed-kotlin-coroutines = "1.5.1" managed-google-function-framework = "1.0.4" managed-google-function-invoker = "1.0.0" managed-gorm = "7.3.2" -managed-gorm-hibernate = "7.2.2" +managed-gorm-hibernate = "7.3.0" # be sure to update graal version in gradle.properties as well # Intentionally pin to 22.0.0.2 see https://github.com/micronaut-projects/micronaut-kafka/pull/564 and https://github.com/micronaut-projects/micronaut-core/pull/7663 managed-graal-sdk = "22.0.0.2" From a977b702e6b231595754847b52a442ebbcef01af Mon Sep 17 00:00:00 2001 From: micronaut-build <65172877+micronaut-build@users.noreply.github.com> Date: Thu, 18 Aug 2022 12:41:44 -0400 Subject: [PATCH 013/743] build: Bump micronaut-security to 3.7.0 (#7892) --- gradle.properties | 2 +- gradle/libs.versions.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle.properties b/gradle.properties index 8c8f9d248b0..f545b61b702 100644 --- a/gradle.properties +++ b/gradle.properties @@ -50,7 +50,7 @@ kotlin.stdlib.default.dependency=false # For the docs graalVersion=22.0.0.2 -micronautSecurityVersion=3.6.2 +micronautSecurityVersion=3.7.0 org.gradle.caching=true org.gradle.parallel=true diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index f0e4d168484..fa56d523233 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -110,7 +110,7 @@ managed-micronaut-rss = "3.1.0" managed-micronaut-rxjava1 = "1.0.0" managed-micronaut-rxjava2 = "1.2.2" managed-micronaut-rxjava3 = "2.3.0" -managed-micronaut-security = "3.6.3" +managed-micronaut-security = "3.7.0" managed-micronaut-serialization = "1.3.0" managed-micronaut-servlet = "3.3.0" managed-micronaut-spring = "4.2.1" From 92a66d83f4dda7f838ebdfa2d0fa4b89a2f28a44 Mon Sep 17 00:00:00 2001 From: micronaut-build <65172877+micronaut-build@users.noreply.github.com> Date: Fri, 2 Sep 2022 05:36:15 -0400 Subject: [PATCH 014/743] build: Bump micronaut-multitenancy to 4.2.0 (#7929) --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 78f57305971..1fbbfcaabfd 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -94,7 +94,7 @@ managed-micronaut-microstream = "1.0.0" managed-micronaut-liquibase = "5.4.1" managed-micronaut-mongo = "4.4.0" managed-micronaut-mqtt = "2.2.0" -managed-micronaut-multitenancy = "4.1.0" +managed-micronaut-multitenancy = "4.2.0" managed-micronaut-neo4j = "5.2.0" managed-micronaut-nats = "3.1.0" managed-micronaut-netflix = "2.1.0" From ddbe6c5af17fa9bd036eb438ecbb34e03fe578eb Mon Sep 17 00:00:00 2001 From: micronaut-build <65172877+micronaut-build@users.noreply.github.com> Date: Fri, 2 Sep 2022 12:28:43 -0400 Subject: [PATCH 015/743] Bump micronaut-azure to 3.4.0 (#7915) --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 1fbbfcaabfd..2f9d1c52e5f 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -68,7 +68,7 @@ managed-micrometer = "1.9.2" managed-micronaut-acme = "3.2.0" managed-micronaut-aot = "1.1.1" managed-micronaut-aws = "3.7.0" -managed-micronaut-azure = "3.3.0" +managed-micronaut-azure = "3.4.0" managed-micronaut-cache = "3.5.0" managed-micronaut-cassandra = "5.1.1" managed-micronaut-coherence = "3.5.1" From 711c86d58b2fdad784da63936fdecd96e2bf0d8e Mon Sep 17 00:00:00 2001 From: micronaut-build <65172877+micronaut-build@users.noreply.github.com> Date: Fri, 2 Sep 2022 15:17:18 -0400 Subject: [PATCH 016/743] build: Bump micronaut-aws to 3.8.0 (#7941) --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 2f9d1c52e5f..08890b91abd 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -67,7 +67,7 @@ managed-methvin-directory-watcher = "0.16.1" managed-micrometer = "1.9.2" managed-micronaut-acme = "3.2.0" managed-micronaut-aot = "1.1.1" -managed-micronaut-aws = "3.7.0" +managed-micronaut-aws = "3.8.0" managed-micronaut-azure = "3.4.0" managed-micronaut-cache = "3.5.0" managed-micronaut-cassandra = "5.1.1" From 491d186ff1ec5f6190cccbed8b55cd22be76a1bd Mon Sep 17 00:00:00 2001 From: micronaut-build <65172877+micronaut-build@users.noreply.github.com> Date: Tue, 6 Sep 2022 09:58:49 -0400 Subject: [PATCH 017/743] build: Bump micronaut-tracing to 4.3.0 (#7954) Co-authored-by: Sergio del Amo --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 25c23099345..151c84a74b0 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -118,7 +118,7 @@ managed-micronaut-sql = "4.6.3" managed-micronaut-test = "3.5.0" managed-micronaut-test-resources = "1.0.1" managed-micronaut-toml = "1.1.1" -managed-micronaut-tracing = "4.2.2" +managed-micronaut-tracing = "4.3.0" managed-micronaut-tracing-legacy = "3.2.7" managed-micronaut-views = "3.5.0" managed-micronaut-xml = "3.1.0" From ce2363e9f33bbed098c26524c5ba9a2918a6ffdc Mon Sep 17 00:00:00 2001 From: micronaut-build <65172877+micronaut-build@users.noreply.github.com> Date: Wed, 7 Sep 2022 07:12:25 -0400 Subject: [PATCH 018/743] build: Bump micronaut-security to 3.7.1 (#7957) --- gradle.properties | 2 +- gradle/libs.versions.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle.properties b/gradle.properties index f545b61b702..24867a522b5 100644 --- a/gradle.properties +++ b/gradle.properties @@ -50,7 +50,7 @@ kotlin.stdlib.default.dependency=false # For the docs graalVersion=22.0.0.2 -micronautSecurityVersion=3.7.0 +micronautSecurityVersion=3.7.1 org.gradle.caching=true org.gradle.parallel=true diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 151c84a74b0..9da413fed5d 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -110,7 +110,7 @@ managed-micronaut-rss = "3.1.0" managed-micronaut-rxjava1 = "1.0.0" managed-micronaut-rxjava2 = "1.2.2" managed-micronaut-rxjava3 = "2.3.0" -managed-micronaut-security = "3.7.0" +managed-micronaut-security = "3.7.1" managed-micronaut-serialization = "1.3.2" managed-micronaut-servlet = "3.3.1" managed-micronaut-spring = "4.2.2" From 58d2cb369eacee9e82ad38ec9bbac6f58c89bac7 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 7 Sep 2022 19:58:54 +0200 Subject: [PATCH 019/743] fix(deps): update dependency io.micronaut.gcp:micronaut-gcp-bom to v4.5.0 (#7904) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Sergio del Amo --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 35a0e7cd04e..8180bf6c540 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -77,7 +77,7 @@ managed-micronaut-discovery = "3.2.0" managed-micronaut-elasticsearch = "4.3.0" managed-micronaut-email = "1.3.1" managed-micronaut-flyway = "5.4.1" -managed-micronaut-gcp = "4.4.1" +managed-micronaut-gcp = "4.5.0" managed-micronaut-graphql = "3.1.0" managed-micronaut-groovy = "3.2.0" managed-micronaut-grpc = "3.3.1" From b108ac5b235158e636fe39d714b23422a12c1e9d Mon Sep 17 00:00:00 2001 From: micronaut-build <65172877+micronaut-build@users.noreply.github.com> Date: Wed, 7 Sep 2022 13:59:19 -0400 Subject: [PATCH 020/743] Bump micronaut-micrometer to 4.5.0 (#7895) * Bump micronaut-micrometer to 4.5.0 * Update libs.versions.toml Co-authored-by: Sergio del Amo --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 8180bf6c540..3d446b8abe5 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -89,7 +89,7 @@ managed-micronaut-jmx = "3.1.0" managed-micronaut-kafka = "4.4.0" managed-micronaut-kotlin = "3.2.2" managed-micronaut-kubernetes = "3.4.0" -managed-micronaut-micrometer = "4.4.0" +managed-micronaut-micrometer = "4.5.0" managed-micronaut-microstream = "1.0.0" managed-micronaut-liquibase = "5.4.1" managed-micronaut-mongo = "4.4.0" From 7b2c5718179d58bef65b2960e9e57ef54c1f3c2e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20S=C3=A1nchez-Mariscal?= Date: Thu, 8 Sep 2022 12:30:29 +0200 Subject: [PATCH 021/743] Add Object Storage to the BOM and to the what's new section of the docs. (#7966) --- gradle/libs.versions.toml | 2 ++ .../docs/guide/introduction/whatsNew.adoc | 22 ++++++++++++------- 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 3d446b8abe5..e304293ddfb 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -98,6 +98,7 @@ managed-micronaut-multitenancy = "4.2.0" managed-micronaut-neo4j = "5.2.0" managed-micronaut-nats = "3.1.0" managed-micronaut-netflix = "2.1.0" +managed-micronaut-object-storage = "1.0.0" managed-micronaut-openapi = "4.4.3" managed-micronaut-oraclecloud = "2.1.5" managed-micronaut-picocli = "4.3.0" @@ -162,6 +163,7 @@ boms-micronaut-micrometer = { module = "io.micronaut.micrometer:micronaut-microm boms-micronaut-microstream = { module = "io.micronaut.microstream:micronaut-microstream-bom", version.ref = "managed-micronaut-microstream" } boms-micronaut-mongo = { module = "io.micronaut.mongodb:micronaut-mongo-bom", version.ref = "managed-micronaut-mongo" } boms-micronaut-mqtt = { module = "io.micronaut.mqtt:micronaut-mqtt-bom", version.ref = "managed-micronaut-mqtt" } +boms-micronaut-object-storage = { module = "io.micronaut.objectstorage:micronaut-object-storage-bom", version.ref = "managed-micronaut-object-storage" } boms-micronaut-oraclecloud = { module = "io.micronaut.oraclecloud:micronaut-oraclecloud-bom", version.ref = "managed-micronaut-oraclecloud" } boms-micronaut-openapi = { module = "io.micronaut.openapi:micronaut-openapi-bom", version.ref = "managed-micronaut-openapi" } boms-micronaut-picocli = { module = "io.micronaut.picocli:micronaut-picocli-bom", version.ref = "managed-micronaut-picocli" } diff --git a/src/main/docs/guide/introduction/whatsNew.adoc b/src/main/docs/guide/introduction/whatsNew.adoc index 4c7a7de854c..495864656d9 100644 --- a/src/main/docs/guide/introduction/whatsNew.adoc +++ b/src/main/docs/guide/introduction/whatsNew.adoc @@ -1,5 +1,11 @@ //Micronaut {version} includes the following changes: +== 3.7.0 + +New modules: + +- https://micronaut-projects.github.io/micronaut-object-storage/latest/guide/[Object Storage]. + == 3.6.0 Key features: @@ -184,20 +190,20 @@ https://micronaut-projects.github.io/micronaut-aot/latest/guide/[Micronaut AOT] For more details, check the https://micronaut-projects.github.io/micronaut-maven-plugin/latest/examples/aot.html[Micronaut Maven Plugin documentation]. -=== Micronaut TOML +=== Micronaut TOML -https://micronaut-projects.github.io/micronaut-toml/latest/guide/[Micronaut TOML] allows you to write your application configuration with https://toml.io/en/[TOML] in addition to `Properties`, `YAML`, `Groovy` or `Config4k`. +https://micronaut-projects.github.io/micronaut-toml/latest/guide/[Micronaut TOML] allows you to write your application configuration with https://toml.io/en/[TOML] in addition to `Properties`, `YAML`, `Groovy` or `Config4k`. === Micronaut Security -https://github.com/micronaut-projects/micronaut-security/releases/tag/v3.4.0[Micronaut Security 3.4.1] responds with an error when an authenticated user visits a sensitive endpoint. This forces the developer to define how they want their application to behave in that scenario. Read the https://github.com/micronaut-projects/micronaut-security/releases/tag/v3.4.0[release notes] and the https://micronaut-projects.github.io/micronaut-security/latest/guide/#builtInEndpointsAccess[documentation] to learn more. +https://github.com/micronaut-projects/micronaut-security/releases/tag/v3.4.0[Micronaut Security 3.4.1] responds with an error when an authenticated user visits a sensitive endpoint. This forces the developer to define how they want their application to behave in that scenario. Read the https://github.com/micronaut-projects/micronaut-security/releases/tag/v3.4.0[release notes] and the https://micronaut-projects.github.io/micronaut-security/latest/guide/#builtInEndpointsAccess[documentation] to learn more. === BOM Modules -Several projects include a BOM (Bills of Materials) module: +Several projects include a BOM (Bills of Materials) module: - https://github.com/micronaut-projects/micronaut-azure/releases/tag/v3.1.0[Micronaut Azure 3.1.0] -- https://github.com/micronaut-projects/micronaut-gcp/releases/tag/v4.1.0[Micronaut GCP 4.1.0]. It includes updates to the latest versions of Google Cloud dependencies. +- https://github.com/micronaut-projects/micronaut-gcp/releases/tag/v4.1.0[Micronaut GCP 4.1.0]. It includes updates to the latest versions of Google Cloud dependencies. - https://github.com/micronaut-projects/micronaut-kotlin/releases/tag/v3.2.0[Micronaut Kotlin 3.2.0] - https://github.com/micronaut-projects/micronaut-mongodb/releases/tag/v4.1.0[Micronaut MongoDB 4.1.0] - https://github.com/micronaut-projects/micronaut-mqtt/releases/tag/v2.1.0[Micronaut MQTT 2.1.0] @@ -206,13 +212,13 @@ Several projects include a BOM (Bills of Materials) module: - https://github.com/micronaut-projects/micronaut-rxjava2/releases/tag/v1.2.0[Micronaut RxJava2 1.2.0] - https://github.com/micronaut-projects/micronaut-rxjava3/releases/tag/v2.2.0[Micronaut RxJava3 2.2.0] - https://github.com/micronaut-projects/micronaut-security/releases/tag/v3.4.0[Micronaut Security 3.4.1] -- https://github.com/micronaut-projects/micronaut-servlet/releases/tag/v3.2.0[Micronaut Servlet 3.2.0]. It includes updates to Tomcat and Undertow dependencies. +- https://github.com/micronaut-projects/micronaut-servlet/releases/tag/v3.2.0[Micronaut Servlet 3.2.0]. It includes updates to Tomcat and Undertow dependencies. === Other Module Upgrades -- https://github.com/micronaut-projects/micronaut-aws/releases/tag/v3.2.0[Micronaut AWS 3.2.0] updates to the latest version of AWS SDK, ASK SDK and AWS Serverless Java Container. +- https://github.com/micronaut-projects/micronaut-aws/releases/tag/v3.2.0[Micronaut AWS 3.2.0] updates to the latest version of AWS SDK, ASK SDK and AWS Serverless Java Container. - https://github.com/micronaut-projects/micronaut-email/releases/tag/v1.1.0[Micronaut Email 1.1.0] updates to the Sendgrid 4.8.3 and contains improvements for `javamail` module users. -- https://github.com/micronaut-projects/micronaut-test/releases/tag/v3.1.0[Micronaut Test 3.1.0] updates the underlying testing dependencies. +- https://github.com/micronaut-projects/micronaut-test/releases/tag/v3.1.0[Micronaut Test 3.1.0] updates the underlying testing dependencies. == 3.3.0 From 1d5016b2917e8c3467677c14e543417553be65bc Mon Sep 17 00:00:00 2001 From: Tim Yates Date: Mon, 12 Sep 2022 10:39:16 +0100 Subject: [PATCH 022/743] feature: Allow stopping netty without stopping the application context (#7933) * feature: Allow stopping netty without stopping the application context For CRaC, we need to be able to stop the Netty server, but keep the ApplicationContext alive. This change adds a new method to NettyEmbeddedServer to do this. * Add test * Add @NonNull annotations --- .../server/netty/NettyEmbeddedServer.java | 12 +++++++ .../http/server/netty/NettyHttpServer.java | 21 +++++++++--- .../http/server/netty/NettyStopSpec.groovy | 33 +++++++++++++++++++ .../context/ApplicationContextLifeCycle.java | 4 +++ .../messaging/MessagingApplication.java | 3 ++ 5 files changed, 69 insertions(+), 4 deletions(-) create mode 100644 http-server-netty/src/test/groovy/io/micronaut/http/server/netty/NettyStopSpec.groovy diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/NettyEmbeddedServer.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/NettyEmbeddedServer.java index ab4baeed98d..aa7281ac80f 100644 --- a/http-server-netty/src/main/java/io/micronaut/http/server/netty/NettyEmbeddedServer.java +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/NettyEmbeddedServer.java @@ -46,15 +46,27 @@ default Set getBoundPorts() { } @Override + @NonNull default NettyEmbeddedServer start() { return (NettyEmbeddedServer) EmbeddedServer.super.start(); } @Override + @NonNull default NettyEmbeddedServer stop() { return (NettyEmbeddedServer) EmbeddedServer.super.stop(); } + /** + * Stops the Netty instance, but keeps the ApplicationContext running. + * This for CRaC checkpointing purposes. + * + * @return The stopped NettyEmbeddedServer + */ + @SuppressWarnings("unused") // Used by CRaC + @NonNull + NettyEmbeddedServer stopServerOnly(); + @Override default void register(@NonNull NettyServerCustomizer customizer) { throw new UnsupportedOperationException(); diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/NettyHttpServer.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/NettyHttpServer.java index 4690a2651ea..bbb3cac6807 100644 --- a/http-server-netty/src/main/java/io/micronaut/http/server/netty/NettyHttpServer.java +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/NettyHttpServer.java @@ -259,6 +259,7 @@ public boolean isRunning() { } @Override + @NonNull public synchronized NettyEmbeddedServer start() { if (!isRunning()) { if (isDefault && !applicationContext.isRunning()) { @@ -317,10 +318,22 @@ private EventLoopGroupConfiguration resolveWorkerConfiguration() { } @Override + @NonNull public synchronized NettyEmbeddedServer stop() { + return stop(true); + } + + @Override + @NonNull + public NettyEmbeddedServer stopServerOnly() { + return stop(false); + } + + @NonNull + private NettyEmbeddedServer stop(boolean stopApplicationContext) { if (isRunning() && workerGroup != null) { if (running.compareAndSet(true, false)) { - stopInternal(); + stopInternal(stopApplicationContext); } } return this; @@ -524,7 +537,7 @@ protected void initChannel(@NonNull Channel ch) { LOG.error("Error starting Micronaut server: " + e.getMessage(), e); } } - stopInternal(); + stopInternal(true); throw new ServerStartupException("Unable to start Micronaut server on " + displayAddress(cfg), e); } } @@ -584,7 +597,7 @@ private void logShutdownErrorIfNecessary(Future future) { } } - private void stopInternal() { + private void stopInternal(boolean stopApplicationContext) { try { if (shutdownParent) { EventLoopGroupConfiguration parent = serverConfiguration.getParent(); @@ -608,7 +621,7 @@ private void stopInternal() { applicationContext.getEventPublisher(ServiceStoppedEvent.class) .publishEvent(new ServiceStoppedEvent(serviceInstance)); } - if (isDefault && applicationContext.isRunning()) { + if (isDefault && applicationContext.isRunning() && stopApplicationContext) { applicationContext.stop(); } serverConfiguration.getMultipart().getLocation().ifPresent(dir -> DiskFileUpload.baseDirectory = null); diff --git a/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/NettyStopSpec.groovy b/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/NettyStopSpec.groovy new file mode 100644 index 00000000000..317c06fb8f0 --- /dev/null +++ b/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/NettyStopSpec.groovy @@ -0,0 +1,33 @@ +package io.micronaut.http.server.netty + +import io.micronaut.context.ApplicationContext +import io.micronaut.runtime.server.EmbeddedServer +import spock.lang.Specification + +class NettyStopSpec extends Specification { + + def 'can shutdown netty and application context'() { + NettyEmbeddedServer server = ApplicationContext.run(EmbeddedServer, ['spec.name': 'NettyStopSpec']) + def ctx = server.applicationContext + + when: + server.stop() + + then: + !ctx.running + } + + def 'can shutdown netty and keep application context running'() { + NettyEmbeddedServer server = ApplicationContext.run(EmbeddedServer, ['spec.name': 'NettyStopSpec']) + def ctx = server.applicationContext + + when: + server.stopServerOnly() + + then: + ctx.running + + cleanup: + ctx.stop() + } +} diff --git a/inject/src/main/java/io/micronaut/context/ApplicationContextLifeCycle.java b/inject/src/main/java/io/micronaut/context/ApplicationContextLifeCycle.java index 4ffbaa62d60..c7501c14189 100644 --- a/inject/src/main/java/io/micronaut/context/ApplicationContextLifeCycle.java +++ b/inject/src/main/java/io/micronaut/context/ApplicationContextLifeCycle.java @@ -15,6 +15,8 @@ */ package io.micronaut.context; +import io.micronaut.core.annotation.NonNull; + /** * An interface for classes that manage the {@link ApplicationContext} life cycle and shut it down when the class is shutdown. * @@ -24,12 +26,14 @@ public interface ApplicationContextLifeCycle Date: Tue, 13 Sep 2022 14:26:53 +0300 Subject: [PATCH 023/743] Allow static executable methods, support executing private fields + methods (#7963) --- .../core/reflect/ReflectionUtils.java | 19 ++ .../io/micronaut/core/type/Executable.java | 4 +- .../BeanDefinitionInjectProcessor.java | 219 ++++++++---------- .../aop/itfce/InterfaceTypeLevel.java | 6 + .../aop/itfce/InterfaceTypeLevelSpec.groovy | 2 - .../inject/executable/BookController.java | 87 ++++++- .../inject/executable/ExecutableSpec.groovy | 167 ++++++++++++- .../beanfield/FactoryBeanFieldSpec.groovy | 165 ++++++++++++- .../beanmethod/FactoryBeanMethodSpec.groovy | 156 +++++++++++++ .../micronaut/inject/ast/MemberElement.java | 55 ++++- .../writer/AbstractBeanDefinitionBuilder.java | 5 + .../inject/writer/BeanDefinitionWriter.java | 200 ++++++++++------ .../inject/writer/DispatchWriter.java | 77 +++--- 13 files changed, 911 insertions(+), 251 deletions(-) diff --git a/core/src/main/java/io/micronaut/core/reflect/ReflectionUtils.java b/core/src/main/java/io/micronaut/core/reflect/ReflectionUtils.java index 9d79b5fde5d..e3a3c06f3ba 100644 --- a/core/src/main/java/io/micronaut/core/reflect/ReflectionUtils.java +++ b/core/src/main/java/io/micronaut/core/reflect/ReflectionUtils.java @@ -428,4 +428,23 @@ public static void setField( throw new InvocationException("Exception occurred setting field [" + field + "]: " + e.getMessage(), e); } } + + /** + * Gets the value of the given field reflectively. + * @param clazz The class + * @param fieldName The fieldName + * @param instance The instance + * @since 3.7.0 + */ + @UsedByGeneratedCode + public static Object getField(@NonNull Class clazz, @NonNull String fieldName, @NonNull Object instance) { + try { + ClassUtils.REFLECTION_LOGGER.debug("Reflectively getting field {} of class {} and instance {}", fieldName, clazz, instance); + Field field = getRequiredField(clazz, fieldName); + field.setAccessible(true); + return field.get(instance); + } catch (Throwable e) { + throw new InvocationException("Exception occurred getting a field [" + fieldName + "] of class [" + clazz + "]: " + e.getMessage(), e); + } + } } diff --git a/core/src/main/java/io/micronaut/core/type/Executable.java b/core/src/main/java/io/micronaut/core/type/Executable.java index 53363e1fb0d..43b461ff203 100644 --- a/core/src/main/java/io/micronaut/core/type/Executable.java +++ b/core/src/main/java/io/micronaut/core/type/Executable.java @@ -46,9 +46,9 @@ public interface Executable extends AnnotationMetadataProvider { /** * Invokes the method. * - * @param instance The instance + * @param instance The instance. Nullable only if it's a static method call. * @param arguments The arguments * @return The result */ - @Nullable R invoke(@NonNull T instance, Object... arguments); + @Nullable R invoke(@Nullable T instance, Object... arguments); } diff --git a/inject-java/src/main/java/io/micronaut/annotation/processing/BeanDefinitionInjectProcessor.java b/inject-java/src/main/java/io/micronaut/annotation/processing/BeanDefinitionInjectProcessor.java index 32116cfd3ce..e0a39b54b87 100644 --- a/inject-java/src/main/java/io/micronaut/annotation/processing/BeanDefinitionInjectProcessor.java +++ b/inject-java/src/main/java/io/micronaut/annotation/processing/BeanDefinitionInjectProcessor.java @@ -858,9 +858,8 @@ public Object visitExecutable(ExecutableElement method, Object o) { postponeIfParametersContainErrors(method); - final AnnotationMetadata annotationMetadata = annotationUtils.getAnnotationMetadata(method); - - AnnotationMetadata methodAnnotationMetadata = getMetadataHierarchy(annotationMetadata); + AnnotationMetadata methodAndClassAnnotationMetadata = getMetadataHierarchy(annotationUtils.getAnnotationMetadata(method)); + AnnotationMetadata methodAnnotationMetadata = methodAndClassAnnotationMetadata.getDeclaredMetadata(); TypeKind returnKind = method.getReturnType().getKind(); if ((returnKind == TypeKind.ERROR) && !processingOver) { @@ -868,7 +867,7 @@ public Object visitExecutable(ExecutableElement method, Object o) { } // handle @Bean annotation for @Factory class - JavaMethodElement javaMethodElement = elementFactory.newMethodElement(concreteClassElement, method, methodAnnotationMetadata); + JavaMethodElement javaMethodElement = elementFactory.newMethodElement(concreteClassElement, method, methodAndClassAnnotationMetadata); if (isFactoryType && javaMethodElement.hasDeclaredStereotype(Bean.class.getName(), AnnotationUtil.SCOPE)) { if (!modelUtils.overridingOrHidingMethod(method, concreteClass, true).isPresent()) { visitBeanFactoryElement(method); @@ -876,14 +875,15 @@ public Object visitExecutable(ExecutableElement method, Object o) { return null; } - if (modelUtils.isStatic(method)) { - return null; - } - boolean injected = methodAnnotationMetadata.hasDeclaredStereotype(AnnotationUtil.INJECT); boolean postConstruct = methodAnnotationMetadata.hasDeclaredAnnotation(AnnotationUtil.POST_CONSTRUCT); boolean preDestroy = methodAnnotationMetadata.hasDeclaredAnnotation(AnnotationUtil.PRE_DESTROY); + boolean isStatic = javaMethodElement.isStatic(); + if (injected || postConstruct || preDestroy || methodAnnotationMetadata.hasDeclaredStereotype(ConfigurationInject.class)) { + if (isStatic) { + return null; + } if (isDeclaredBean) { visitAnnotatedMethod(javaMethodElement, method, o); } else if (injected) { @@ -893,53 +893,80 @@ public Object visitExecutable(ExecutableElement method, Object o) { return null; } - final boolean isAbstract = javaMethodElement.isAbstract(); final boolean isPrivate = javaMethodElement.isPrivate(); - final boolean isStatic = javaMethodElement.isStatic(); - final boolean isPublic = javaMethodElement.isPublic(); - final boolean isInternal = methodAnnotationMetadata.hasAnnotation(Internal.class); - boolean hasInvalidModifiers = isAbstract || isStatic || isPrivate || isInternal; + if (javaMethodElement.isAbstract()) { + return null; + } + if (isStatic && !isExecutableDeclaredOnMethod(methodAnnotationMetadata)) { + // Require explicit @Executable on static methods + return null; + } - Set modifiers = method.getModifiers(); - boolean isExecutable = - isExecutableThroughType(method.getEnclosingElement(), methodAnnotationMetadata, annotationMetadata, modifiers, isPublic) || - InterceptedMethodUtil.hasAroundStereotype(annotationMetadata); + boolean validatedMethod = isValidatedMethod(methodAnnotationMetadata, javaMethodElement); - boolean hasConstraints = false; - if (isDeclaredBean && !methodAnnotationMetadata.hasStereotype(ANN_VALIDATED) && - Arrays.stream(javaMethodElement.getParameters()) - .anyMatch(p -> p.hasStereotype(ANN_CONSTRAINT) || p.hasStereotype(ANN_VALID))) { - hasConstraints = true; - methodAnnotationMetadata = javaMethodElement.annotate(ANN_VALIDATED); - } + if (isDeclaredBean) { + if (validatedMethod && !isConfigurationPropertiesType) { + // Configurations are checked using the bean introspection, not requiring the interceptor + methodAnnotationMetadata = javaMethodElement.annotate(ANN_VALIDATED); + } - if (isDeclaredBean && isExecutable) { - if (hasInvalidModifiers) { - if (isPrivate) { - error(method, "Method annotated as executable but is declared private. Change the method to be non-private in order for AOP advice to be applied."); + boolean aroundDeclaredOnMethod = InterceptedMethodUtil.hasAroundStereotype(methodAnnotationMetadata); + if (aroundDeclaredOnMethod || InterceptedMethodUtil.hasAroundStereotype(methodAndClassAnnotationMetadata)) { + // AOP doesn't support private methods or static + if (!isPrivate && !isStatic) { + visitExecutableMethod(javaMethodElement, method, methodAnnotationMetadata); + } else if (aroundDeclaredOnMethod) { + if (isPrivate) { + error(method, "Method annotated as executable but is declared private. Change the method to be non-private in order for AOP advice to be applied."); + } else if (isStatic) { + error(method, "Static methods aren't supported for AOP"); + } + } + // Continue to check if the method is executable + } + if (isExecutableDeclaredOnType(method)) { + // @Executable annotated on the class + // only include own accessible methods or the ones annotated with @ReflectiveAccess + if (javaMethodElement.isAccessible()) { + visitExecutableMethod(javaMethodElement, method, methodAnnotationMetadata); + } + return null; + } else if (isExecutableDeclaredOnMethod(methodAnnotationMetadata)) { + // @Executable annotated on the method + // Throw error if it cannot be accessed without the reflection + if (!javaMethodElement.isAccessible()) { + error(method, "Method annotated as executable but is declared private. To invoke the method using reflection annotate it with @ReflectiveAccess"); + return null; } - } else { visitExecutableMethod(javaMethodElement, method, methodAnnotationMetadata); + return null; + } else if (isExecutableDeclaredOnParentType(method)) { + // @Executable annotated on the parent class + // Only include public methods + if (javaMethodElement.isPublic()) { + visitExecutableMethod(javaMethodElement, method, methodAnnotationMetadata); + } + return null; } - } else if (isConfigurationPropertiesType && !modelUtils.isPrivate(method) && !modelUtils.isStatic(method)) { + } + + if (isConfigurationPropertiesType && !isPrivate && !isStatic) { String methodName = javaMethodElement.getSimpleName(); if (NameUtils.isSetterName(methodName) && javaMethodElement.getParameters().length == 1) { visitConfigurationPropertySetter(method); } else if (NameUtils.isGetterName(methodName)) { BeanDefinitionVisitor writer = getOrCreateBeanDefinitionWriter(concreteClass, concreteClass.getQualifiedName()); if (!writer.isValidated()) { - writer.setValidated(IS_CONSTRAINT.test(methodAnnotationMetadata)); + writer.setValidated(validatedMethod); } } - } else if (hasConstraints) { - if (hasInvalidModifiers) { - if (isPrivate) { - error(method, "Method annotated with constraints but is declared private. Change the method to be non-private in order for AOP advice to be applied."); - } - } else if (isPublic) { - visitExecutableMethod(javaMethodElement, method, methodAnnotationMetadata); + } else if (validatedMethod) { + if (isPrivate) { + error(method, "Method annotated with constraints but is declared private. Change the method to be non-private in order for AOP advice to be applied."); + return null; } + visitExecutableMethod(javaMethodElement, method, methodAnnotationMetadata); } return null; @@ -947,28 +974,35 @@ public Object visitExecutable(ExecutableElement method, Object o) { @NonNull private AnnotationMetadata getMetadataHierarchy(AnnotationMetadata annotationMetadata) { - AnnotationMetadata methodAnnotationMetadata; - + // NOTE: if annotation processor modified the method's annotation + // annotationUtils.getAnnotationMetadata(method) will return AnnotationMetadataHierarchy of both method+class metadata if (annotationMetadata instanceof AnnotationMetadataHierarchy) { - methodAnnotationMetadata = annotationMetadata; - } else { + return annotationMetadata; + } + return new AnnotationMetadataHierarchy(concreteClassMetadata, annotationMetadata); + } - methodAnnotationMetadata = new AnnotationMetadataHierarchy( - concreteClassMetadata, - annotationMetadata - ); + private boolean isExecutableDeclaredOnMethod(AnnotationMetadata annotationMetadata) { + return annotationMetadata.hasStereotype(Executable.class); + } + + private boolean isExecutableDeclaredOnType(ExecutableElement method) { + return isExecutableType && concreteClass.equals(method.getEnclosingElement()); + } + + private boolean isExecutableDeclaredOnParentType(ExecutableElement method) { + return isExecutableType && !concreteClass.equals(method.getEnclosingElement()); + } + + private boolean isValidatedMethod(AnnotationMetadata methodAnnotationMetadata, JavaMethodElement javaMethodElement) { + if (methodAnnotationMetadata.hasStereotype(ANN_VALIDATED)) { + return false; } - return methodAnnotationMetadata; + return requiresValidation(javaMethodElement) || Arrays.stream(javaMethodElement.getParameters()).anyMatch(this::requiresValidation); } - private boolean isExecutableThroughType( - Element enclosingElement, - AnnotationMetadata annotationMetadataHierarchy, - AnnotationMetadata declaredMetadata, Set modifiers, - boolean isPublic) { - return (isExecutableType && (isPublic || (modifiers.isEmpty()) && concreteClass.equals(enclosingElement))) || - annotationMetadataHierarchy.hasDeclaredStereotype(Executable.class) || - declaredMetadata.hasStereotype(Executable.class); + private boolean requiresValidation(io.micronaut.inject.ast.Element p) { + return p.hasStereotype(ANN_CONSTRAINT) || p.hasStereotype(ANN_VALID); } private void visitConfigurationPropertySetter(ExecutableElement method) { @@ -1015,19 +1049,9 @@ private void visitConfigurationPropertySetter(ExecutableElement method) { addPropertyMetadata(javaMethodElement, propertyMetadata); - boolean requiresReflection = true; - if (javaMethodElement.isPublic()) { - requiresReflection = false; - } else if (modelUtils.isPackagePrivate(method) || javaMethodElement.isProtected()) { - String declaringPackage = javaClassElement.getPackageName(); - String concretePackage = concreteClassElement.getPackageName(); - requiresReflection = !declaringPackage.equals(concretePackage); - } - - writer.visitSetterValue( - javaClassElement, + writer.visitSetterValue(javaClassElement, javaMethodElement, - requiresReflection, + javaMethodElement.isReflectionRequired(concreteClassElement), true ); } @@ -1096,7 +1120,6 @@ void visitBeanFactoryElement(Element element) { concreteClassMetadata ); - io.micronaut.inject.ast.Element beanProducingElement; ClassElement producedClassElement; if (element instanceof ExecutableElement) { @@ -1127,7 +1150,6 @@ void visitBeanFactoryElement(Element element) { producedClassElement = fieldElement.getGenericField(); } - BeanDefinitionWriter beanMethodWriter = createFactoryBeanMethodWriterFor(element, producedAnnotationMetadata); Map> allTypeArguments = producedClassElement.getAllTypeArguments(); visitAnnotationMetadata(beanMethodWriter, producedAnnotationMetadata); @@ -1619,7 +1641,7 @@ private AopProxyWriter resolveAopProxyWriter(BeanDefinitionVisitor beanWriter, if (constructorElement != null) { aopProxyWriter.visitBeanDefinitionConstructor( constructorElement, - constructorElement.isPrivate(), + constructorElement.isReflectionRequired(), javaVisitorContext ); } else { @@ -1667,7 +1689,6 @@ void visitAnnotatedMethod(MethodElement javaMethodElement, ExecutableElement met String packageOfDeclaringClass = declaringClass.getPackageName(); boolean isPackagePrivateAndPackagesDiffer = overridden && isPackagePrivate && !packageOfOverridingClass.equals(packageOfDeclaringClass); - boolean requiresReflection = isPrivate || isPackagePrivateAndPackagesDiffer; boolean overriddenInjected = overridden && annotationUtils.getAnnotationMetadata(overridingMethod).hasDeclaredStereotype(AnnotationUtil.INJECT); if (isParent && isPackagePrivate && !isPackagePrivateAndPackagesDiffer && overriddenInjected) { @@ -1679,9 +1700,6 @@ void visitAnnotatedMethod(MethodElement javaMethodElement, ExecutableElement met // and is not annotated with @Inject return; } - if (!requiresReflection && modelUtils.isInheritedAndNotPublic(this.concreteClassElement, declaringClass, javaMethodElement)) { - requiresReflection = true; - } boolean lifecycleMethod = false; if (javaMethodElement.hasDeclaredAnnotation(AnnotationUtil.POST_CONSTRUCT)) { BeanDefinitionVisitor writer = getOrCreateBeanDefinitionWriter(concreteClass, concreteClass.getQualifiedName()); @@ -1689,7 +1707,7 @@ void visitAnnotatedMethod(MethodElement javaMethodElement, ExecutableElement met writer.visitPostConstructMethod( declaringClass, javaMethodElement, - requiresReflection, + javaMethodElement.isReflectionRequired(concreteClassElement), javaVisitorContext ); lifecycleMethod = true; @@ -1700,7 +1718,7 @@ void visitAnnotatedMethod(MethodElement javaMethodElement, ExecutableElement met writer.visitPreDestroyMethod( declaringClass, javaMethodElement, - requiresReflection, + javaMethodElement.isReflectionRequired(concreteClassElement), javaVisitorContext ); lifecycleMethod = true; @@ -1715,7 +1733,7 @@ void visitAnnotatedMethod(MethodElement javaMethodElement, ExecutableElement met writer.visitMethodInjectionPoint( declaringClass, javaMethodElement, - requiresReflection, + javaMethodElement.isReflectionRequired(concreteClassElement), javaVisitorContext ); } else { @@ -1732,9 +1750,10 @@ public Object visitVariable(VariableElement variable, Object o) { AnnotationMetadata fieldAnnotationMetadata = annotationUtils.getAnnotationMetadata(variable); if (isFactoryType && fieldAnnotationMetadata.hasDeclaredStereotype(Bean.class)) { + FieldElement javaFieldElement = elementFactory.newFieldElement(concreteClassElement, variable, fieldAnnotationMetadata); // field factory for bean - if (modelUtils.isPrivate(variable) || modelUtils.isProtected(variable)) { - error(variable, "Beans produced from fields cannot be private or protected"); + if (!javaFieldElement.isAccessible(concreteClassElement)) { + error(variable, "Beans produced from fields cannot be private"); } else { visitBeanFactoryElement(variable); } @@ -1764,10 +1783,6 @@ public Object visitVariable(VariableElement variable, Object o) { FieldElement javaFieldElement = elementFactory.newFieldElement(concreteClassElement, variable, fieldAnnotationMetadata); addOriginatingElementIfNecessary(writer, declaringClass); - boolean isPrivate = javaFieldElement.isPrivate(); - boolean requiresReflection = isPrivate - || modelUtils.isInheritedAndNotPublic(this.concreteClass, declaringClass, variable); - if (!writer.isValidated()) { writer.setValidated(IS_CONSTRAINT.test(fieldAnnotationMetadata)); } @@ -1781,25 +1796,18 @@ public Object visitVariable(VariableElement variable, Object o) { writer.visitFieldValue( declaringClassElement, javaFieldElement, - requiresReflection, + javaFieldElement.isReflectionRequired(concreteClassElement), isConfigurationPropertiesType ); } else { writer.visitFieldInjectionPoint( declaringClassElement, javaFieldElement, - requiresReflection + javaFieldElement.isReflectionRequired(concreteClassElement) ); } } else if (isConfigurationPropertiesType) { visitConfigurationProperty(variable, fieldAnnotationMetadata); - } else if (isFactoryType && fieldAnnotationMetadata.hasDeclaredStereotype(Bean.class)) { - // field factory for bean - if (modelUtils.isPrivate(variable) || modelUtils.isProtected(variable)) { - error(variable, "Beans produced from fields cannot be private or protected"); - } else { - visitBeanFactoryElement(variable); - } } return null; } @@ -1846,17 +1854,8 @@ public Object visitConfigurationProperty(VariableElement field, AnnotationMetada if (fieldAnnotationMetadata.hasStereotype(ConfigurationBuilder.class)) { - boolean accessible = false; - if (modelUtils.isPublic(field)) { - accessible = true; - } else if (modelUtils.isPackagePrivate(field) || modelUtils.isProtected(field)) { - String declaringPackage = declaringClassElement.getPackageName(); - String concretePackage = concreteClassElement.getPackageName(); - accessible = declaringPackage.equals(concretePackage); - } - boolean isInterface = javaFieldElement.getType().isInterface(); - if (accessible) { + if (!javaFieldElement.isReflectionRequired(concreteClassElement)) { writer.visitConfigBuilderField(javaFieldElement.getType(), fieldName, fieldAnnotationMetadata, metadataBuilder, isInterface); } else { // Using the field would throw a IllegalAccessError, use the method instead @@ -1890,7 +1889,6 @@ public Object visitConfigurationProperty(VariableElement field, AnnotationMetada ); } else { boolean isPrivate = javaFieldElement.isPrivate(); - boolean requiresReflection = isInheritedAndNotPublic(modelUtils.classElementFor(field), field.getModifiers()); if (!isPrivate) { String docComment = elementUtils.getDocComment(field); @@ -1907,7 +1905,7 @@ public Object visitConfigurationProperty(VariableElement field, AnnotationMetada writer.visitFieldValue( declaringClassElement, javaFieldElement, - requiresReflection, + javaFieldElement.isReflectionRequired(concreteClassElement), isConfigurationPropertiesType ); } @@ -1933,19 +1931,6 @@ public Object visitUnknown(Element e, Object o) { return o; } - /** - * @param declaringClass The {@link TypeElement} - * @param modifiers The {@link Modifier} - * @return Whether is inherited and not public - */ - protected boolean isInheritedAndNotPublic(TypeElement declaringClass, Set modifiers) { - PackageElement declaringPackage = elementUtils.getPackageOf(declaringClass); - PackageElement concretePackage = elementUtils.getPackageOf(concreteClass); - return !declaringClass.equals(concreteClass) && - !declaringPackage.equals(concretePackage) && - !(modifiers.contains(Modifier.PUBLIC)); - } - private void visitConfigurationBuilder(TypeElement declaringClass, Element builderElement, TypeMirror builderType, BeanDefinitionVisitor writer) { AnnotationMetadata annotationMetadata = annotationUtils.getAnnotationMetadata(builderElement); Boolean allowZeroArgs = annotationMetadata.getValue(ConfigurationBuilder.class, "allowZeroArgs", Boolean.class).orElse(false); diff --git a/inject-java/src/test/groovy/io/micronaut/aop/itfce/InterfaceTypeLevel.java b/inject-java/src/test/groovy/io/micronaut/aop/itfce/InterfaceTypeLevel.java index b5b2547daf1..c03d73d34a1 100644 --- a/inject-java/src/test/groovy/io/micronaut/aop/itfce/InterfaceTypeLevel.java +++ b/inject-java/src/test/groovy/io/micronaut/aop/itfce/InterfaceTypeLevel.java @@ -81,4 +81,10 @@ public interface InterfaceTypeLevel { A testGenericsFromType(A name, int age); + + + static String getFoo() { + return null; + } + } diff --git a/inject-java/src/test/groovy/io/micronaut/aop/itfce/InterfaceTypeLevelSpec.groovy b/inject-java/src/test/groovy/io/micronaut/aop/itfce/InterfaceTypeLevelSpec.groovy index bf88a51230d..f4df7c7cb6f 100644 --- a/inject-java/src/test/groovy/io/micronaut/aop/itfce/InterfaceTypeLevelSpec.groovy +++ b/inject-java/src/test/groovy/io/micronaut/aop/itfce/InterfaceTypeLevelSpec.groovy @@ -18,10 +18,8 @@ package io.micronaut.aop.itfce import io.micronaut.aop.Intercepted import io.micronaut.context.BeanContext import io.micronaut.context.DefaultBeanContext -import io.micronaut.inject.BeanDefinition import spock.lang.Specification import spock.lang.Unroll - /** * @author Graeme Rocher * @since 1.0 diff --git a/inject-java/src/test/groovy/io/micronaut/inject/executable/BookController.java b/inject-java/src/test/groovy/io/micronaut/inject/executable/BookController.java index b0229c81abc..5e3e4a09f20 100644 --- a/inject-java/src/test/groovy/io/micronaut/inject/executable/BookController.java +++ b/inject-java/src/test/groovy/io/micronaut/inject/executable/BookController.java @@ -17,6 +17,7 @@ import io.micronaut.context.annotation.Executable; +import io.micronaut.core.annotation.ReflectiveAccess; import jakarta.inject.Inject; import java.util.List; @@ -25,33 +26,107 @@ public class BookController { @Inject BookService bookService; - @Executable public String show(Long id) { return String.format("%d - The Stand", id); } - @Executable public String showArray(Long[] id) { return String.format("%d - The Stand", id[0]); } - @Executable public String showPrimitive(long id) { return String.format("%d - The Stand", id); } - @Executable public String showPrimitiveArray(long[] id) { return String.format("%d - The Stand", id[0]); } - @Executable public void showVoidReturn(List jobNames) { jobNames.add("test"); } - @Executable public int showPrimitiveReturn(int[] values) { return values[0]; } + + @Executable + public static String showStatic(Long id) { + return String.format("%d - The Stand", id); + } + + @Executable + public static String showArrayStatic(Long[] id) { + return String.format("%d - The Stand", id[0]); + } + + @Executable + public static String showPrimitiveStatic(long id) { + return String.format("%d - The Stand", id); + } + + @Executable + public static String showPrimitiveArrayStatic(long[] id) { + return String.format("%d - The Stand", id[0]); + } + + @Executable + public static void showVoidReturnStatic(List jobNames) { + jobNames.add("test"); + } + + @Executable + public static int showPrimitiveReturnStatic(int[] values) { + return values[0]; + } + + String showPackageProtected(Long id) { + return String.format("%d - The Stand", id); + } + + protected String showProtected(Long id) { + return String.format("%d - The Stand", id); + } + + private String showPrivate(Long id) { + return String.format("%d - The Stand", id); + } + + @ReflectiveAccess + protected String showProtectedReflectiveAccess(Long id) { + return String.format("%d - The Stand", id); + } + + @ReflectiveAccess + private String showPrivateReflectiveAccess(Long id) { + return String.format("%d - The Stand", id); + } + + @Executable + static String showPackageProtectedStatic(Long id) { + return String.format("%d - The Stand", id); + } + + @Executable + static protected String showProtectedStatic(Long id) { + return String.format("%d - The Stand", id); + } + + @Executable + static private String showPrivateStatic(Long id) { + return String.format("%d - The Stand", id); + } + + @ReflectiveAccess + @Executable + static protected String showProtectedReflectiveAccessStatic(Long id) { + return String.format("%d - The Stand", id); + } + + @ReflectiveAccess + @Executable + static private String showPrivateReflectiveAccessStatic(Long id) { + return String.format("%d - The Stand", id); + } + } diff --git a/inject-java/src/test/groovy/io/micronaut/inject/executable/ExecutableSpec.groovy b/inject-java/src/test/groovy/io/micronaut/inject/executable/ExecutableSpec.groovy index 92470def71d..886d2fb8c65 100644 --- a/inject-java/src/test/groovy/io/micronaut/inject/executable/ExecutableSpec.groovy +++ b/inject-java/src/test/groovy/io/micronaut/inject/executable/ExecutableSpec.groovy @@ -39,11 +39,11 @@ class MyBean { public String methodOne(@jakarta.inject.Named("foo") String one) { return "good"; } - + public String methodTwo(String one, String two) { return "good"; } - + public String methodZero() { return "good"; } @@ -57,6 +57,129 @@ class MyBean { beanDefinition.executableMethods[0].getArguments()[0].getAnnotationMetadata().stringValue(AnnotationUtil.NAMED).get() == 'foo' } + void "test static method"() { + given:"A bean that defines no explicit scope" + when: + BeanDefinition beanDefinition = buildBeanDefinition('test.MyBean', ''' +package test; + +import io.micronaut.context.annotation.*; + +@Executable +class MyBean { + + @Executable + public static String methodOne(@jakarta.inject.Named("foo") String one) { + return "good" + one; + } + + @Executable + public static String methodTwo(String one, String two) { + return "good" + one + two; + } + + @Executable + public static String methodZero() { + return "good"; + } +} + +''') + then: + beanDefinition.executableMethods.size() == 3 + beanDefinition.executableMethods[0].methodName == 'methodOne' + beanDefinition.executableMethods[0].getArguments()[0].getAnnotationMetadata().stringValue(AnnotationUtil.NAMED).get() == 'foo' + beanDefinition.executableMethods[0].invoke(null, "abc") == "goodabc" + beanDefinition.executableMethods[1].methodName == 'methodTwo' + beanDefinition.executableMethods[1].getArguments()[0].name == 'one' + beanDefinition.executableMethods[1].getArguments()[1].name == 'two' + beanDefinition.executableMethods[1].invoke(null, "abc", "xyz") == "goodabcxyz" + beanDefinition.executableMethods[2].methodName == 'methodZero' + beanDefinition.executableMethods[2].getArguments().length == 0 + beanDefinition.executableMethods[2].invoke(null) == "good" + } + + void "test static method protected/private require explicit @Executable"() { + given:"A bean that defines no explicit scope" + when: + BeanDefinition beanDefinition = buildBeanDefinition('test.MyBean', ''' +package test; + +import io.micronaut.context.annotation.*; +import io.micronaut.core.annotation.ReflectiveAccess; + +@Executable +class MyBean { + + private static String methodOne(@jakarta.inject.Named("foo") String one) { + return "good" + one; + } + + protected static String methodTwo(String one, String two) { + return "good" + one + two; + } + + static String methodZero() { + return "good"; + } + + public static String methodThree() { + return "good"; + } +} + +''') + then: + beanDefinition.executableMethods.size() == 0 + } + + void "test static method protected/private"() { + given:"A bean that defines no explicit scope" + when: + BeanDefinition beanDefinition = buildBeanDefinition('test.MyBean', ''' +package test; + +import io.micronaut.context.annotation.*; +import io.micronaut.core.annotation.ReflectiveAccess; + +@Executable +class MyBean { + + @ReflectiveAccess + @Executable + private static String methodOne(@jakarta.inject.Named("foo") String one) { + return "good" + one; + } + + @ReflectiveAccess + @Executable + protected static String methodTwo(String one, String two) { + return "good" + one + two; + } + + @ReflectiveAccess + @Executable + static String methodZero() { + return "good"; + + } +} + +''') + then: + beanDefinition.executableMethods.size() == 3 + beanDefinition.executableMethods[0].methodName == 'methodOne' + beanDefinition.executableMethods[0].getArguments()[0].getAnnotationMetadata().stringValue(AnnotationUtil.NAMED).get() == 'foo' + beanDefinition.executableMethods[0].invoke(null, "abc") == "goodabc" + beanDefinition.executableMethods[1].methodName == 'methodTwo' + beanDefinition.executableMethods[1].getArguments()[0].name == 'one' + beanDefinition.executableMethods[1].getArguments()[1].name == 'two' + beanDefinition.executableMethods[1].invoke(null, "abc", "xyz") == "goodabcxyz" + beanDefinition.executableMethods[2].methodName == 'methodZero' + beanDefinition.executableMethods[2].getArguments().length == 0 + beanDefinition.executableMethods[2].invoke(null) == "good" + } + void "test executable metadata"() { given: ApplicationContext applicationContext = new DefaultApplicationContext("test").start() @@ -93,14 +216,40 @@ class MyBean { ExecutionHandle method = applicationContext.findExecutionHandle(BookController, methodName, argTypes as Class[]).get() method.invoke(args as Object[]) == result + where: + methodName | argTypes | args | result + "show" | [Long] | [1L] | "1 - The Stand" + "showArray" | [Long[].class] | [[1L] as Long[]] | "1 - The Stand" + "showPrimitive" | [long.class] | [1L as long] | "1 - The Stand" + "showPrimitiveArray" | [long[].class] | [[1L] as long[]] | "1 - The Stand" + "showVoidReturn" | [Iterable.class] | [['test']] | null + "showPrimitiveReturn" | [int[].class] | [[1] as int[]] | 1 + "showStatic" | [Long] | [1L] | "1 - The Stand" + "showArrayStatic" | [Long[].class] | [[1L] as Long[]] | "1 - The Stand" + "showPrimitiveStatic" | [long.class] | [1L as long] | "1 - The Stand" + "showPrimitiveArrayStatic" | [long[].class] | [[1L] as long[]] | "1 - The Stand" + "showVoidReturnStatic" | [Iterable.class] | [['test']] | null + "showPrimitiveReturnStatic" | [int[].class] | [[1] as int[]] | 1 + "showProtectedReflectiveAccess" | [Long] | [1L] | "1 - The Stand" + "showPrivateReflectiveAccess" | [Long] | [1L] | "1 - The Stand" + "showProtectedReflectiveAccessStatic" | [Long] | [1L] | "1 - The Stand" + "showPrivateReflectiveAccessStatic" | [Long] | [1L] | "1 - The Stand" + "showProtected" | [Long] | [1L] | "1 - The Stand" + "showPackageProtected" | [Long] | [1L] | "1 - The Stand" + "showPackageProtectedStatic" | [Long] | [1L] | "1 - The Stand" + "showProtectedStatic" | [Long] | [1L] | "1 - The Stand" + } + + void "test executable missing methods"() { + given: + ApplicationContext applicationContext = new DefaultApplicationContext("test").start() + + expect: + !applicationContext.findExecutionHandle(BookController, methodName, argTypes as Class[]).isPresent() where: - methodName | argTypes | args | result - "show" | [Long] | [1L] | "1 - The Stand" - "showArray" | [Long[].class] | [[1L] as Long[]] | "1 - The Stand" - "showPrimitive" | [long.class] | [1L as long] | "1 - The Stand" - "showPrimitiveArray" | [long[].class] | [[1L] as long[]] | "1 - The Stand" - "showVoidReturn" | [Iterable.class] | [['test']] | null - "showPrimitiveReturn" | [int[].class] | [[1] as int[]] | 1 + methodName | argTypes | args | result + "showPrivate" | [Long] | [1L] | "1 - The Stand" + "showPrivateStatic" | [Long] | [1L] | "1 - The Stand" } } diff --git a/inject-java/src/test/groovy/io/micronaut/inject/factory/beanfield/FactoryBeanFieldSpec.groovy b/inject-java/src/test/groovy/io/micronaut/inject/factory/beanfield/FactoryBeanFieldSpec.groovy index 282bec10e04..87d2cfd22d6 100644 --- a/inject-java/src/test/groovy/io/micronaut/inject/factory/beanfield/FactoryBeanFieldSpec.groovy +++ b/inject-java/src/test/groovy/io/micronaut/inject/factory/beanfield/FactoryBeanFieldSpec.groovy @@ -381,7 +381,7 @@ class Test {} e.message.contains(modifier) where: - modifier << ['private', 'protected'] + modifier << ['private'] } void "if a factory field bean defines Bean and Prototype scopes and the original bean type scope is Singleton BeanDefinition getScope returns Prototype and BeanDefinition getAnnotationNamesByStereotype for javax.inject.Scope returns Prototype The original qualifier is not present if the factory field bean does not define a Qualifier"() { @@ -954,4 +954,167 @@ public class Implementation2 implements Interface { implementation.closed.intValue() == 1 test.closed.intValue() == 1 } + + void "factory that is also a producer"() { + given: + ApplicationContext context = buildContext('test.TestFactory', '''\ +package test; + +import static java.lang.annotation.RetentionPolicy.RUNTIME; +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; + +import io.micronaut.inject.annotation.*; +import io.micronaut.aop.*; +import io.micronaut.context.annotation.*; +import jakarta.inject.*; +import jakarta.inject.Singleton; + +@Factory +class TestFactory { + @Bean + @Xyz + public static String VAL1 = "val1"; + @Bean + @Xyz + public static Integer VAL2 = 123; + + @Bean + @Xyz + public static Double val3producer() { + return 876d; + } + + public TestFactory(@Xyz String val1, @Xyz Integer val2, @Xyz Double val3) { + if (!val1.equals(VAL1) || !val2.equals(VAL2) || !val3.equals(val3producer())) { + throw new RuntimeException(); + } + } +} + +@Retention(RUNTIME) +@Qualifier +@interface Xyz { +} + +''') + when: + BeanDefinition factoryBeanDefinition = context.getBeanDefinition(context.classLoader.loadClass('test.TestFactory')) + + then: + context.getBeanRegistration(factoryBeanDefinition).bean + + cleanup: + context.close() + } + + void "factory field access rights"() { + given: + ApplicationContext context = buildContext('test.TestFactory', '''\ +package test; + +import static java.lang.annotation.RetentionPolicy.RUNTIME; +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; + +import io.micronaut.core.annotation.ReflectiveAccess; +import io.micronaut.inject.annotation.*; +import io.micronaut.aop.*; +import io.micronaut.context.annotation.*; +import jakarta.inject.*; +import jakarta.inject.Singleton; +import java.math.*; +import java.time.LocalDate; +import java.util.Arrays; + +@Factory +class TestFactory { + @Bean + @Xyz + public static String VAL1 = "val1"; + + @Bean + @Xyz + static Integer VAL2 = 123; + + @Bean + @Xyz + protected static Double VAL3 = 789d; + + @Bean + @Xyz + @ReflectiveAccess + private static Float VAL4 = 744f; + + @Bean + @Xyz + public Boolean VAL5 = Boolean.TRUE; + + @Bean + @Xyz + BigDecimal VAL6 = BigDecimal.TEN; + + @Bean + @Xyz + protected BigInteger VAL7 = BigInteger.ONE; + + @Bean + @Xyz + @ReflectiveAccess + private LocalDate VAL8 = LocalDate.MAX; + + @Bean + @Named + @ReflectiveAccess + private int VAL9 = 999; + + @Bean + @Named + @ReflectiveAccess + private int[] VAL10 = {1, 2, 3}; + + @Bean + @Named + @ReflectiveAccess + private static int VAL11 = 333; + + @Bean + @Named + @ReflectiveAccess + private static int[] VAL12 = {4, 5, 6}; + +} + +@Singleton +class MyBean { + + public MyBean(@Xyz String val1, @Xyz Integer val2, @Xyz Double val3, @Xyz Float val4, + @Xyz Boolean val5, @Xyz BigDecimal val6, @Xyz BigInteger val7, @Xyz LocalDate val8, + @Named int val9, @Named int[] val10, @Named int val11, @Named int[] val12) { + if (!val1.equals("val1") || !val2.equals(123) || !val3.equals(789d) || !val4.equals(744f) + || !val5.equals(Boolean.TRUE) || !val6.equals(BigDecimal.TEN) + || !val7.equals(BigInteger.ONE) || !val8.equals(LocalDate.MAX) + || val9 != 999 || !Arrays.equals(val10, new int[] {1, 2, 3}) + || val11 != 333 || !Arrays.equals(val12, new int[] {4, 5, 6}) + ) { + throw new RuntimeException(); + } + } +} + +@Retention(RUNTIME) +@Qualifier +@interface Xyz { +} + +''') + when: + BeanDefinition beanDefinition = context.getBeanDefinition(context.classLoader.loadClass('test.MyBean')) + + then: + context.getBeanRegistration(beanDefinition).bean + + cleanup: + context.close() + } } diff --git a/inject-java/src/test/groovy/io/micronaut/inject/factory/beanmethod/FactoryBeanMethodSpec.groovy b/inject-java/src/test/groovy/io/micronaut/inject/factory/beanmethod/FactoryBeanMethodSpec.groovy index 1c534c012d7..a96bc22c5e7 100644 --- a/inject-java/src/test/groovy/io/micronaut/inject/factory/beanmethod/FactoryBeanMethodSpec.groovy +++ b/inject-java/src/test/groovy/io/micronaut/inject/factory/beanmethod/FactoryBeanMethodSpec.groovy @@ -4,6 +4,7 @@ import io.micronaut.annotation.processing.test.AbstractTypeElementSpec import io.micronaut.context.ApplicationContext import io.micronaut.context.annotation.Prototype import io.micronaut.core.annotation.AnnotationUtil +import io.micronaut.inject.BeanDefinition class FactoryBeanMethodSpec extends AbstractTypeElementSpec { @@ -257,4 +258,159 @@ class Bar8 { cleanup: context.close() } + + void "factory method access rights"() { + given: + ApplicationContext context = buildContext('test.TestFactory', '''\ +package test; + +import static java.lang.annotation.RetentionPolicy.RUNTIME; +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; + +import io.micronaut.core.annotation.ReflectiveAccess; +import io.micronaut.inject.annotation.*; +import io.micronaut.aop.*; +import io.micronaut.context.annotation.*; +import jakarta.inject.*; +import jakarta.inject.Singleton; +import java.math.*; +import java.time.LocalDate; +import java.time.LocalTime; +import java.util.Arrays; + +@Factory +class TestFactory { + @Bean + @Xyz + public static String val1() { + return "val1"; + } + + @Bean + @Xyz + static Integer val2() { + return 123; + } + + @Bean + @Xyz + protected static Double val3() { + return 789d; + } + + @Bean + @Xyz + @ReflectiveAccess + private static LocalTime val4() { + return LocalTime.MIDNIGHT; + } + + @Bean + @Xyz + public Boolean val5() { + return Boolean.TRUE; + } + + @Bean + @Xyz + BigDecimal val6() { + return BigDecimal.TEN; + } + + @Bean + @Xyz + protected BigInteger val7() { + return BigInteger.ONE; + } + + @Bean + @Xyz + @ReflectiveAccess + private LocalDate val8(@Xyz String val1, @Xyz Integer val2, @Xyz Double val3, @Xyz LocalTime val4, + @Xyz Boolean val5, @Xyz BigDecimal val6, @Xyz BigInteger val7, + @Named int val9, @Named int[] val10, @Named int val11) { + if (!val1.equals("val1") || !val2.equals(123) || !val3.equals(789d) || !val4.equals(LocalTime.MIDNIGHT) + || !val5.equals(Boolean.TRUE) || !val6.equals(BigDecimal.TEN) + || !val7.equals(BigInteger.ONE) + || val9 != 999 || !Arrays.equals(val10, new int[] {1, 2, 3}) + || val11 != 333 + ) { + throw new RuntimeException(); + } + return LocalDate.MAX; + } + + @Bean + @Named + @ReflectiveAccess + private int val9() { + return 999; + } + + @Bean + @Named + @ReflectiveAccess + private int[] val10() { + return new int[] {1, 2, 3}; + } + + @Bean + @Named + @ReflectiveAccess + private static int val11() { + return 333; + } + + @Bean + @Named + @ReflectiveAccess + private static int[] val12(@Xyz String val1, @Xyz Integer val2, @Xyz Double val3, @Xyz LocalTime val4, + @Xyz Boolean val5, @Xyz BigDecimal val6, @Xyz BigInteger val7, @Xyz LocalDate val8, + @Named int val9, @Named int[] val10, @Named int val11) { + if (!val1.equals("val1") || !val2.equals(123) || !val3.equals(789d) || !val4.equals(LocalTime.MIDNIGHT) + || !val5.equals(Boolean.TRUE) || !val6.equals(BigDecimal.TEN) + || !val7.equals(BigInteger.ONE) || !val8.equals(LocalDate.MAX) + || val9 != 999 || !Arrays.equals(val10, new int[] {1, 2, 3}) + || val11 != 333 + ) { + throw new RuntimeException(); + } + return new int[]{4, 5, 6}; + } + +} + +@Singleton +class MyBean { + + public MyBean(@Xyz String val1, @Xyz Integer val2, @Xyz Double val3, @Xyz LocalTime val4, + @Xyz Boolean val5, @Xyz BigDecimal val6, @Xyz BigInteger val7, @Xyz LocalDate val8, + @Named int val9, @Named int[] val10, @Named int val11, @Named int[] val12) { + if (!val1.equals("val1") || !val2.equals(123) || !val3.equals(789d) || !val4.equals(LocalTime.MIDNIGHT) + || !val5.equals(Boolean.TRUE) || !val6.equals(BigDecimal.TEN) + || !val7.equals(BigInteger.ONE) || !val8.equals(LocalDate.MAX) + || val9 != 999 || !Arrays.equals(val10, new int[] {1, 2, 3}) + || val11 != 333 || !Arrays.equals(val12, new int[] {4, 5, 6}) + ) { + throw new RuntimeException(); + } + } +} + +@Retention(RUNTIME) +@Qualifier +@interface Xyz { +} + +''') + when: + BeanDefinition beanDefinition = context.getBeanDefinition(context.classLoader.loadClass('test.MyBean')) + + then: + context.getBeanRegistration(beanDefinition).bean + + cleanup: + context.close() + } } diff --git a/inject/src/main/java/io/micronaut/inject/ast/MemberElement.java b/inject/src/main/java/io/micronaut/inject/ast/MemberElement.java index 3ced9bec30f..5249c38785c 100644 --- a/inject/src/main/java/io/micronaut/inject/ast/MemberElement.java +++ b/inject/src/main/java/io/micronaut/inject/ast/MemberElement.java @@ -15,11 +15,12 @@ */ package io.micronaut.inject.ast; +import io.micronaut.core.annotation.NonNull; +import io.micronaut.core.annotation.ReflectiveAccess; + import java.util.Collections; import java.util.Set; -import io.micronaut.core.annotation.NonNull; - /** * A member element is an element that is contained within a {@link ClassElement}. * The {@link #getDeclaringType()} method returns the class that declares the element. @@ -75,16 +76,46 @@ default boolean isReflectionRequired() { default boolean isReflectionRequired(@NonNull ClassElement callingType) { if (isPublic()) { return false; - } else { - if (isPackagePrivate() || isProtected()) { - // the declaring type might be a super class in which - // case if the super class is in a different package then - // the method or field is not visible and hence reflection is required - final ClassElement declaringType = getDeclaringType(); - return !declaringType.getPackageName().equals(callingType.getPackageName()); - } else { - return true; - } } + if (isPackagePrivate() || isProtected()) { + // the declaring type might be a super class in which + // case if the super class is in a different package then + // the method or field is not visible and hence reflection is required + final ClassElement declaringType = getDeclaringType(); + return !declaringType.getPackageName().equals(callingType.getPackageName()); + } + return isPrivate(); + } + + /** + * Returns whether this member element can be invoked or retrieved at runtime. + * It can be accessible by a simple invocation or a reflection invocation. + * + *

This method uses {@link #isReflectionRequired()} with a checks if the reflection access is allowed. + * By checking for {@link io.micronaut.core.annotation.ReflectiveAccess} annotation. + *

+ * + * @return Will return {@code true} if is accessible. + * @since 3.7.0 + */ + default boolean isAccessible() { + return isAccessible(getOwningType()); + } + + /** + * Returns whether this member element can be invoked or retrieved at runtime. + * It can be accessible by a simple invocation or a reflection invocation. + * + *

This method uses {@link #isReflectionRequired()} with a checks if the reflection access is allowed. + * By checking for {@link io.micronaut.core.annotation.ReflectiveAccess} annotation. + *

+ * + * @param callingType The calling type + * @return Will return {@code true} if is accessible. + * @since 3.7.0 + */ + default boolean isAccessible(@NonNull ClassElement callingType) { + return !isReflectionRequired(callingType) || hasAnnotation(ReflectiveAccess.class); + } } diff --git a/inject/src/main/java/io/micronaut/inject/writer/AbstractBeanDefinitionBuilder.java b/inject/src/main/java/io/micronaut/inject/writer/AbstractBeanDefinitionBuilder.java index c9ac84ebc41..81a62ad0474 100644 --- a/inject/src/main/java/io/micronaut/inject/writer/AbstractBeanDefinitionBuilder.java +++ b/inject/src/main/java/io/micronaut/inject/writer/AbstractBeanDefinitionBuilder.java @@ -1011,6 +1011,11 @@ public boolean isReflectionRequired() { return requiresReflection; } + @Override + public boolean isReflectionRequired(ClassElement callingType) { + return requiresReflection; + } + @Override public boolean isPackagePrivate() { return methodElement.isPackagePrivate(); diff --git a/inject/src/main/java/io/micronaut/inject/writer/BeanDefinitionWriter.java b/inject/src/main/java/io/micronaut/inject/writer/BeanDefinitionWriter.java index 910a6b7dbca..30026a60bcf 100644 --- a/inject/src/main/java/io/micronaut/inject/writer/BeanDefinitionWriter.java +++ b/inject/src/main/java/io/micronaut/inject/writer/BeanDefinitionWriter.java @@ -388,6 +388,14 @@ public class BeanDefinitionWriter extends AbstractClassFileWriter implements Bea ReflectionUtils.getRequiredMethod(AbstractInitializableBeanDefinition.class, "invokeMethodWithReflection", BeanResolutionContext.class, BeanContext.class, int.class, Object.class, Object[].class) ); + private static final Type TYPE_REFLECTION_UTILS = Type.getType(ReflectionUtils.class); + + private static final org.objectweb.asm.commons.Method GET_FIELD_WITH_REFLECTION_METHOD = org.objectweb.asm.commons.Method.getMethod( + ReflectionUtils.getRequiredInternalMethod(ReflectionUtils.class, "getField", Class.class, String.class, Object.class)); + + private static final org.objectweb.asm.commons.Method METHOD_INVOKE_METHOD = org.objectweb.asm.commons.Method.getMethod( + ReflectionUtils.getRequiredInternalMethod(ReflectionUtils.class, "invokeMethod", Object.class, java.lang.reflect.Method.class, Object[].class)); + private static final org.objectweb.asm.commons.Method BEAN_DEFINITION_CLASS_CONSTRUCTOR = new org.objectweb.asm.commons.Method(CONSTRUCTOR_NAME, getConstructorDescriptor( Class.class, // beanType AbstractInitializableBeanDefinition.MethodOrFieldReference.class, // constructor @@ -3089,116 +3097,140 @@ private void invokeSuperInjectMethod(GeneratorAdapter methodVisitor, Method meth private void visitBuildFactoryMethodDefinition( ClassElement factoryClass, - Element factoryMethod, ParameterElement... parameters) { + Element factoryElement, ParameterElement... parameters) { if (buildMethodVisitor == null) { List parameterList = Arrays.asList(parameters); boolean isParametrized = isParametrized(parameters); - boolean isIntercepted = isConstructorIntercepted(factoryMethod); + boolean isIntercepted = isConstructorIntercepted(factoryElement); Type factoryType = JavaModelUtils.getTypeReference(factoryClass); defineBuilderMethod(isParametrized); // load this GeneratorAdapter buildMethodVisitor = this.buildMethodVisitor; - invokeCheckIfShouldLoadIfNecessary(buildMethodVisitor); - // for Factory beans first we need to lookup the the factory bean - // before invoking the method to instantiate - // the below code looks up the factory bean. - - // Load the BeanContext for the method call - buildMethodVisitor.loadArg(1); - pushCastToType(buildMethodVisitor, DefaultBeanContext.class); - // load the first argument of the method (the BeanResolutionContext) to be passed to the method - buildMethodVisitor.loadArg(0); - // second argument is the bean type - buildMethodVisitor.push(factoryType); - // third argument is the qualifier for the factory if any - pushQualifier(buildMethodVisitor, factoryClass, () -> { - buildMethodVisitor.push(factoryType); - buildMethodVisitor.push("factory"); - invokeInterfaceStaticMethod(buildMethodVisitor, Argument.class, METHOD_CREATE_ARGUMENT_SIMPLE); - }); - buildMethodVisitor.invokeVirtual( - Type.getType(DefaultBeanContext.class), - org.objectweb.asm.commons.Method.getMethod(METHOD_GET_BEAN) - ); - - int factoryVar = buildMethodVisitor.newLocal(factoryType); - buildMethodVisitor.storeLocal(factoryVar, factoryType); - - // BeanResolutionContext - buildMethodVisitor.loadArg(0); - // .markDependentAsFactory() - buildMethodVisitor.invokeInterface(TYPE_RESOLUTION_CONTEXT, METHOD_BEAN_RESOLUTION_CONTEXT_MARK_FACTORY); - buildMethodVisitor.loadLocal(factoryVar); - pushCastToType(buildMethodVisitor, factoryClass); + int factoryVar = -1; + // Skip initializing a producer instance for static producers + if (!factoryElement.isStatic()) { + factoryVar = pushGetFactoryBean(factoryClass, factoryType, buildMethodVisitor); + } String methodDescriptor = getMethodDescriptorForReturnType(beanType, parameterList); boolean hasInjectScope = false; if (isIntercepted) { int constructorIndex = initInterceptedConstructorWriter( buildMethodVisitor, parameterList, - new FactoryMethodDef(factoryType, factoryMethod, methodDescriptor, factoryVar) + new FactoryMethodDef(factoryType, factoryElement, methodDescriptor, factoryVar) ); // populate an Object[] of all constructor arguments final int parametersIndex = createParameterArray(parameterList, buildMethodVisitor); invokeConstructorChain(buildMethodVisitor, constructorIndex, parametersIndex, parameterList); } else { - - if (!parameterList.isEmpty()) { - hasInjectScope = pushConstructorArguments(buildMethodVisitor, parameters); - } - if (factoryMethod instanceof MethodElement) { - MethodElement methodElement = (MethodElement) factoryMethod; + if (factoryElement instanceof MethodElement) { + MethodElement methodElement = (MethodElement) factoryElement; + if (!methodElement.isReflectionRequired() && !parameterList.isEmpty()) { + hasInjectScope = pushConstructorArguments(buildMethodVisitor, parameters); + } + if (methodElement.isReflectionRequired()) { + if (methodElement.isStatic()) { + buildMethodVisitor.push((String) null); + } + DispatchWriter.pushTypeUtilsGetRequiredMethod(buildMethodVisitor, factoryType, methodElement); + buildMethodVisitor.dup(); + buildMethodVisitor.push(true); + buildMethodVisitor.invokeVirtual(Type.getType(Method.class), org.objectweb.asm.commons.Method.getMethod( + ReflectionUtils.getRequiredMethod(Method.class, "setAccessible", boolean.class) + )); + hasInjectScope = pushParametersAsArray(buildMethodVisitor, parameters); + buildMethodVisitor.invokeStatic(TYPE_REFLECTION_UTILS, METHOD_INVOKE_METHOD); +// buildMethodVisitor.push((String) null); + if (methodElement.isReflectionRequired() && isPrimitiveBean) { + // Reflection always returns Object, convert it to appropriate primitive + pushCastToType(buildMethodVisitor, beanType); + } + } else if (methodElement.isStatic()) { - buildMethodVisitor.visitMethodInsn(INVOKESTATIC, - factoryType.getInternalName(), - factoryMethod.getName(), - methodDescriptor, - false - ); + buildMethodVisitor.invokeStatic(factoryType, new org.objectweb.asm.commons.Method(factoryElement.getName(), methodDescriptor)); } else { - buildMethodVisitor.visitMethodInsn(INVOKEVIRTUAL, - factoryType.getInternalName(), - factoryMethod.getName(), - methodDescriptor, - false - ); + buildMethodVisitor.invokeVirtual(factoryType, new org.objectweb.asm.commons.Method(factoryElement.getName(), methodDescriptor)); } } else { - if (factoryMethod.isStatic()) { - - buildMethodVisitor.getStatic( - factoryType, - factoryMethod.getName(), - beanType - ); + FieldElement fieldElement = (FieldElement) factoryElement; + if (fieldElement.isReflectionRequired()) { + if (!fieldElement.isStatic()) { + buildMethodVisitor.storeLocal(factoryVar); + } + buildMethodVisitor.push(factoryType); + buildMethodVisitor.push(fieldElement.getName()); + if (fieldElement.isStatic()) { + buildMethodVisitor.push((String) null); + } else { + buildMethodVisitor.loadLocal(factoryVar); + } + buildMethodVisitor.invokeStatic(TYPE_REFLECTION_UTILS, GET_FIELD_WITH_REFLECTION_METHOD); + if (fieldElement.isReflectionRequired() && isPrimitiveBean) { + // Reflection always returns Object, convert it to appropriate primitive + pushCastToType(buildMethodVisitor, beanType); + } + } else if (fieldElement.isStatic()) { + buildMethodVisitor.getStatic(factoryType, factoryElement.getName(), beanType); } else { - buildMethodVisitor.getField( - factoryType, - factoryMethod.getName(), - beanType - ); + buildMethodVisitor.getField(factoryType, factoryElement.getName(), beanType); } } } - this.buildInstanceLocalVarIndex = buildMethodVisitor.newLocal(beanType); - buildMethodVisitor.storeLocal(buildInstanceLocalVarIndex); + buildMethodVisitor.storeLocal(buildInstanceLocalVarIndex, beanType); if (!isPrimitiveBean) { pushBeanDefinitionMethodInvocation(buildMethodVisitor, "injectBean"); pushCastToType(buildMethodVisitor, beanType); buildMethodVisitor.storeLocal(buildInstanceLocalVarIndex); } destroyInjectScopeBeansIfNecessary(buildMethodVisitor, hasInjectScope); - buildMethodVisitor.loadLocal(buildInstanceLocalVarIndex); + buildMethodVisitor.loadLocal(buildInstanceLocalVarIndex, beanType); initLifeCycleMethodsIfNecessary(); } } + private int pushGetFactoryBean(ClassElement factoryClass, Type factoryType, GeneratorAdapter buildMethodVisitor) { + invokeCheckIfShouldLoadIfNecessary(buildMethodVisitor); + // for Factory beans first we need to lookup the factory bean + // before invoking the method to instantiate + // the below code looks up the factory bean. + + // Load the BeanContext for the method call + buildMethodVisitor.loadArg(1); + pushCastToType(buildMethodVisitor, DefaultBeanContext.class); + // load the first argument of the method (the BeanResolutionContext) to be passed to the method + buildMethodVisitor.loadArg(0); + // second argument is the bean type + buildMethodVisitor.push(factoryType); + // third argument is the qualifier for the factory if any + pushQualifier(buildMethodVisitor, factoryClass, () -> { + buildMethodVisitor.push(factoryType); + buildMethodVisitor.push("factory"); + invokeInterfaceStaticMethod(buildMethodVisitor, Argument.class, METHOD_CREATE_ARGUMENT_SIMPLE); + }); + buildMethodVisitor.invokeVirtual( + Type.getType(DefaultBeanContext.class), + org.objectweb.asm.commons.Method.getMethod(METHOD_GET_BEAN) + ); + + int factoryVar = buildMethodVisitor.newLocal(factoryType); + buildMethodVisitor.storeLocal(factoryVar, factoryType); + + // BeanResolutionContext + buildMethodVisitor.loadArg(0); + // .markDependentAsFactory() + buildMethodVisitor.invokeInterface(TYPE_RESOLUTION_CONTEXT, METHOD_BEAN_RESOLUTION_CONTEXT_MARK_FACTORY); + + buildMethodVisitor.loadLocal(factoryVar); + pushCastToType(buildMethodVisitor, factoryClass); + return factoryVar; + } + private void visitBuildMethodDefinition(MethodElement constructor, boolean requiresReflection) { if (buildMethodVisitor == null) { boolean isIntercepted = isConstructorIntercepted(constructor); @@ -3527,7 +3559,8 @@ private int createParameterArray(List parameters, GeneratorAda parameter.getName(), parameter, parameter.getAnnotationMetadata(), - parameterIndex + parameterIndex, + false ) ); } @@ -3603,7 +3636,7 @@ private boolean pushConstructorArguments(GeneratorAdapter buildMethodVisitor, if (size > 0) { for (int i = 0; i < parameters.length; i++) { ParameterElement parameter = parameters[i]; - pushConstructorArgument(buildMethodVisitor, parameter.getName(), parameter, parameter.getAnnotationMetadata(), i); + pushConstructorArgument(buildMethodVisitor, parameter.getName(), parameter, parameter.getAnnotationMetadata(), i, false); if (parameter.hasDeclaredAnnotation(InjectScope.class)) { hasInjectScope = true; } @@ -3612,11 +3645,28 @@ private boolean pushConstructorArguments(GeneratorAdapter buildMethodVisitor, return hasInjectScope; } + private boolean pushParametersAsArray(GeneratorAdapter buildMethodVisitor, ParameterElement[] parameters) { + final int pLen = parameters.length; + boolean hasInjectScope = false; + pushNewArray(buildMethodVisitor, Object.class, pLen); + for (int i = 0; i < pLen; i++) { + final ParameterElement parameter = parameters[i]; + if (parameter.hasDeclaredAnnotation(InjectScope.class)) { + hasInjectScope = true; + } + int finalI = i; + pushStoreInArray(buildMethodVisitor, i, pLen, () -> + pushConstructorArgument(buildMethodVisitor, parameter.getName(), parameter, parameter.getAnnotationMetadata(), finalI, true) + ); + } + return hasInjectScope; + } + private void pushConstructorArgument(GeneratorAdapter buildMethodVisitor, String argumentName, ParameterElement argumentType, AnnotationMetadata annotationMetadata, - int index) { + int index, boolean castToObject) { if (isAnnotatedWithParameter(annotationMetadata) && isParametrized) { // load the args buildMethodVisitor.loadArg(3); @@ -3691,7 +3741,13 @@ private void pushConstructorArgument(GeneratorAdapter buildMethodVisitor, if (isArray && hasGenericType) { convertToArray(argumentType.getGenericType().fromArray(), buildMethodVisitor); } - pushCastToType(buildMethodVisitor, argumentType); + if (castToObject) { + if (argumentType.isPrimitive()) { + pushCastToType(buildMethodVisitor, Object.class); + } + } else { + pushCastToType(buildMethodVisitor, argumentType); + } } } diff --git a/inject/src/main/java/io/micronaut/inject/writer/DispatchWriter.java b/inject/src/main/java/io/micronaut/inject/writer/DispatchWriter.java index 174f5d5078f..301a64ea864 100644 --- a/inject/src/main/java/io/micronaut/inject/writer/DispatchWriter.java +++ b/inject/src/main/java/io/micronaut/inject/writer/DispatchWriter.java @@ -47,7 +47,7 @@ * @since 3.1 */ @Internal -public class DispatchWriter extends AbstractClassFileWriter implements Opcodes { +public final class DispatchWriter extends AbstractClassFileWriter implements Opcodes { private static final Method DISPATCH_METHOD = new Method("dispatch", getMethodDescriptor(Object.class, Arrays.asList(int.class, Object.class, Object[].class))); @@ -268,29 +268,10 @@ public void buildGetTargetMethodByIndex(ClassWriter classWriter) { @Override public void generateCase(int key, Label end) { MethodDispatchTarget method = (MethodDispatchTarget) dispatchTargets.get(key); - Type declaringTypeObject = JavaModelUtils.getTypeReference(method.declaringType); - List argumentTypes = Arrays.asList(method.methodElement.getSuspendParameters()); - - getTargetMethodByIndex.push(declaringTypeObject); - getTargetMethodByIndex.push(method.methodElement.getName()); - if (!argumentTypes.isEmpty()) { - int len = argumentTypes.size(); - Iterator iter = argumentTypes.iterator(); - pushNewArray(getTargetMethodByIndex, Class.class, len); - for (int i = 0; i < len; i++) { - ParameterElement type = iter.next(); - pushStoreInArray( - getTargetMethodByIndex, - i, - len, - () -> getTargetMethodByIndex.push(JavaModelUtils.getTypeReference(type)) - ); - - } - } else { - getTargetMethodByIndex.getStatic(TYPE_REFLECTION_UTILS, "EMPTY_CLASS_ARRAY", Type.getType(Class[].class)); - } - getTargetMethodByIndex.invokeStatic(TYPE_REFLECTION_UTILS, METHOD_GET_REQUIRED_METHOD); + TypedElement declaringType = method.declaringType; + Type declaringTypeObject = JavaModelUtils.getTypeReference(declaringType); + MethodElement methodElement = method.methodElement; + pushTypeUtilsGetRequiredMethod(getTargetMethodByIndex, declaringTypeObject, methodElement); getTargetMethodByIndex.returnValue(); } @@ -306,6 +287,31 @@ public void generateDefault() { getTargetMethodByIndex.visitEnd(); } + public static void pushTypeUtilsGetRequiredMethod(GeneratorAdapter builder, Type declaringTypeObject, MethodElement methodElement) { + List argumentTypes = Arrays.asList(methodElement.getSuspendParameters()); + + builder.push(declaringTypeObject); + builder.push(methodElement.getName()); + if (!argumentTypes.isEmpty()) { + int len = argumentTypes.size(); + Iterator iter = argumentTypes.iterator(); + pushNewArray(builder, Class.class, len); + for (int i = 0; i < len; i++) { + ParameterElement type = iter.next(); + pushStoreInArray( + builder, + i, + len, + () -> builder.push(JavaModelUtils.getTypeReference(type)) + ); + + } + } else { + builder.getStatic(TYPE_REFLECTION_UTILS, "EMPTY_CLASS_ARRAY", Type.getType(Class[].class)); + } + builder.invokeStatic(TYPE_REFLECTION_UTILS, METHOD_GET_REQUIRED_METHOD); + } + @Override public void accept(ClassWriterOutputVisitor classWriterOutputVisitor) throws IOException { throw new IllegalStateException(); @@ -503,9 +509,14 @@ public void writeDispatchMulti(GeneratorAdapter writer, int methodIndex) { boolean hasArgs = !argumentTypes.isEmpty(); // load this - writer.loadArg(1); + if (!methodElement.isStatic()) { + writer.loadArg(1); + } if (reflectionRequired) { + if (methodElement.isStatic()) { + writer.push((String) null); + } writer.loadThis(); writer.push(methodIndex); writer.invokeVirtual(ExecutableMethodsDefinitionWriter.SUPER_TYPE, GET_ACCESSIBLE_TARGET_METHOD); @@ -516,7 +527,9 @@ public void writeDispatchMulti(GeneratorAdapter writer, int methodIndex) { } writer.invokeStatic(TYPE_REFLECTION_UTILS, METHOD_INVOKE_METHOD); } else { - pushCastToType(writer, declaringTypeObject); + if (!methodElement.isStatic()) { + pushCastToType(writer, declaringTypeObject); + } if (hasArgs) { int argCount = argumentTypes.size(); Iterator argIterator = argumentTypes.iterator(); @@ -529,13 +542,17 @@ public void writeDispatchMulti(GeneratorAdapter writer, int methodIndex) { } } String methodDescriptor = getMethodDescriptor(returnType, argumentTypes); - writer.visitMethodInsn(isInterface ? INVOKEINTERFACE : INVOKEVIRTUAL, - declaringTypeObject.getInternalName(), methodName, - methodDescriptor, isInterface); + if (methodElement.isStatic()) { + writer.invokeStatic(declaringTypeObject, new Method(methodName, methodDescriptor)); + } else { + writer.visitMethodInsn(isInterface ? INVOKEINTERFACE : INVOKEVIRTUAL, + declaringTypeObject.getInternalName(), methodName, + methodDescriptor, isInterface); + } } if (returnTypeObject.equals(Type.VOID_TYPE)) { - writer.visitInsn(ACONST_NULL); + writer.push((String) null); } else if (!reflectionRequired) { pushBoxPrimitiveIfNecessary(returnType, writer); } From 85f9247bc858eae20102973dc5528a30e6889c85 Mon Sep 17 00:00:00 2001 From: micronaut-build <65172877+micronaut-build@users.noreply.github.com> Date: Tue, 13 Sep 2022 09:39:38 -0400 Subject: [PATCH 024/743] build: Bump micronaut-spring to 4.3.0 (#7991) --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 647abcc67bd..9c2b3c812e9 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -114,7 +114,7 @@ managed-micronaut-rxjava3 = "2.3.0" managed-micronaut-security = "3.7.1" managed-micronaut-serialization = "1.3.2" managed-micronaut-servlet = "3.3.1" -managed-micronaut-spring = "4.2.2" +managed-micronaut-spring = "4.3.0" managed-micronaut-sql = "4.6.3" managed-micronaut-test = "3.5.0" managed-micronaut-test-resources = "1.0.1" From af3fe017af4f4668ef7f58f0940ad7da9e649052 Mon Sep 17 00:00:00 2001 From: Jonas Konrad Date: Tue, 13 Sep 2022 16:18:28 +0200 Subject: [PATCH 025/743] More java.time support for yaml and converters (#7870) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit YAML has a defined timestamp type that is used when iso8601-like values are encountered, e.g. 2022-08-12. Unfortunately, snakeyaml parses them as java.util.Date by default (at UTC timezone). This patch adds a snakeyaml constructor that parses timestamps as the appropriate java.time type instead – LocalDate, LocalDateTime, or OffsetDateTime, depending on the actual input. To maintain compatibility, I added conversions from these types to Date. For ODT this is straight-forward, but for LD and LDT, it uses UTC as the offset to match the old snakeyaml behavior. This is a bit arbitrary, but required for compatibility. We could remove these conversions for Micronaut 4. I've also altered the existing time conversions to be able to handle ISO 8601 strings as inputs for conversion to the java.time types. This means that when a configuration property is defined with java.time, you can use the same ISO 8601 string for the yaml and e.g. properties or toml definition. Fixes #7863 --- .../time/TimeConverterRegistrar.java | 96 +++++++-------- .../env/yaml/ConstructIsoTimestampString.java | 114 ++++++++++++++++++ .../env/yaml/CustomSafeConstructor.java | 4 + .../ConstructIsoTimestampStringSpec.groovy | 25 ++++ .../yaml/YamlPropertySourceLoaderSpec2.groovy | 93 ++++++++++++++ .../time/TimeConverterRegistrarSpec.groovy | 33 +++++ 6 files changed, 313 insertions(+), 52 deletions(-) create mode 100644 inject/src/main/java/io/micronaut/context/env/yaml/ConstructIsoTimestampString.java create mode 100644 inject/src/test/groovy/io/micronaut/context/env/yaml/ConstructIsoTimestampStringSpec.groovy create mode 100644 inject/src/test/groovy/io/micronaut/context/env/yaml/YamlPropertySourceLoaderSpec2.groovy diff --git a/context/src/main/java/io/micronaut/runtime/converters/time/TimeConverterRegistrar.java b/context/src/main/java/io/micronaut/runtime/converters/time/TimeConverterRegistrar.java index 2ccb149dbe2..3fcb8c5e28a 100644 --- a/context/src/main/java/io/micronaut/runtime/converters/time/TimeConverterRegistrar.java +++ b/context/src/main/java/io/micronaut/runtime/converters/time/TimeConverterRegistrar.java @@ -41,8 +41,11 @@ import java.time.format.DateTimeParseException; import java.time.temporal.TemporalAccessor; import java.time.temporal.TemporalAmount; +import java.time.temporal.TemporalQuery; +import java.util.Date; import java.util.Optional; import java.util.function.BiFunction; +import java.util.function.Function; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -143,23 +146,7 @@ public void register(ConversionService conversionService) { (object, targetType, context) -> durationConverter.apply(object, context).map(TemporalAmount.class::cast) ); - // CharSequence -> LocalDateTime - conversionService.addConverter( - CharSequence.class, - LocalDateTime.class, - (object, targetType, context) -> { - try { - DateTimeFormatter formatter = resolveFormatter(context); - LocalDateTime result = LocalDateTime.parse(object, formatter); - return Optional.of(result); - } catch (DateTimeParseException e) { - context.reject(object, e); - return Optional.empty(); - } - } - ); - - // TemporalAccessor - CharSequence + // TemporalAccessor -> CharSequence final TypeConverter temporalConverter = (object, targetType, context) -> { try { DateTimeFormatter formatter = resolveFormatter(context); @@ -175,54 +162,59 @@ public void register(ConversionService conversionService) { temporalConverter ); - // CharSequence -> LocalDate - conversionService.addConverter( - CharSequence.class, - LocalDate.class, - (object, targetType, context) -> { + addTemporalStringConverter(conversionService, Instant.class, DateTimeFormatter.ISO_INSTANT, Instant::from); + addTemporalStringConverter(conversionService, LocalDate.class, DateTimeFormatter.ISO_LOCAL_DATE, LocalDate::from); + addTemporalStringConverter(conversionService, LocalDateTime.class, DateTimeFormatter.ISO_LOCAL_DATE_TIME, LocalDateTime::from); + addTemporalStringConverter(conversionService, OffsetTime.class, DateTimeFormatter.ISO_OFFSET_TIME, OffsetTime::from); + addTemporalStringConverter(conversionService, OffsetDateTime.class, DateTimeFormatter.ISO_OFFSET_DATE_TIME, OffsetDateTime::from); + addTemporalStringConverter(conversionService, ZonedDateTime.class, DateTimeFormatter.ISO_ZONED_DATE_TIME, ZonedDateTime::from); + + // java.time -> Date + addTemporalToDateConverter(conversionService, Instant.class, Function.identity()); + addTemporalToDateConverter(conversionService, OffsetDateTime.class, OffsetDateTime::toInstant); + addTemporalToDateConverter(conversionService, ZonedDateTime.class, ZonedDateTime::toInstant); + // these two are a bit icky, but required for yaml parsing compatibility + // TODO Micronaut 4 Consider deletion + addTemporalToDateConverter(conversionService, LocalDate.class, ld -> ld.atTime(0, 0).toInstant(ZoneOffset.UTC)); + addTemporalToDateConverter(conversionService, LocalDateTime.class, ldt -> ldt.toInstant(ZoneOffset.UTC)); + } + + private void addTemporalStringConverter(ConversionService conversionService, Class temporalType, DateTimeFormatter isoFormatter, TemporalQuery query) { + conversionService.addConverter(CharSequence.class, temporalType, (CharSequence object, Class targetType, ConversionContext context) -> { + if (StringUtils.isEmpty(object)) { + return Optional.empty(); + } + // try explicit format first + Optional format = context.getAnnotationMetadata().stringValue(Format.class); + if (format.isPresent()) { + DateTimeFormatter formatter = DateTimeFormatter.ofPattern(format.get(), context.getLocale()); try { - DateTimeFormatter formatter = resolveFormatter(context); - LocalDate result = LocalDate.parse(object, formatter); - return Optional.of(result); + T converted = formatter.parse(object, query); + return Optional.of(converted); } catch (DateTimeParseException e) { context.reject(object, e); return Optional.empty(); } - } - ); - - - // CharSequence -> ZonedDateTime - conversionService.addConverter( - CharSequence.class, - ZonedDateTime.class, - (object, targetType, context) -> { + } else { + try { + T converted = isoFormatter.parse(object, query); + return Optional.of(converted); + } catch (DateTimeParseException ignored) { + } + // fall back to RFC 1123 date time for compatibility try { - DateTimeFormatter formatter = resolveFormatter(context); - ZonedDateTime result = ZonedDateTime.parse(object, formatter); + T result = DateTimeFormatter.RFC_1123_DATE_TIME.parse(object, query); return Optional.of(result); } catch (DateTimeParseException e) { context.reject(object, e); return Optional.empty(); } } - ); + }); + } - // CharSequence -> OffsetDateTime - conversionService.addConverter( - CharSequence.class, - OffsetDateTime.class, - (object, targetType, context) -> { - try { - DateTimeFormatter formatter = resolveFormatter(context); - OffsetDateTime result = OffsetDateTime.parse(object, formatter); - return Optional.of(result); - } catch (DateTimeParseException e) { - context.reject(object, e); - return Optional.empty(); - } - } - ); + private void addTemporalToDateConverter(ConversionService conversionService, Class temporalType, Function toInstant) { + conversionService.addConverter(temporalType, Date.class, (T object, Class targetType, ConversionContext context) -> Optional.of(Date.from(toInstant.apply(object)))); } private DateTimeFormatter resolveFormatter(ConversionContext context) { diff --git a/inject/src/main/java/io/micronaut/context/env/yaml/ConstructIsoTimestampString.java b/inject/src/main/java/io/micronaut/context/env/yaml/ConstructIsoTimestampString.java new file mode 100644 index 00000000000..23182e42762 --- /dev/null +++ b/inject/src/main/java/io/micronaut/context/env/yaml/ConstructIsoTimestampString.java @@ -0,0 +1,114 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.context.env.yaml; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.NonNull; +import org.yaml.snakeyaml.constructor.AbstractConstruct; +import org.yaml.snakeyaml.error.YAMLException; +import org.yaml.snakeyaml.nodes.Node; +import org.yaml.snakeyaml.nodes.ScalarNode; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.ZoneOffset; +import java.time.temporal.Temporal; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Adapted from {@link org.yaml.snakeyaml.constructor.SafeConstructor.ConstructYamlTimestamp} but + * with java 8 time. + */ +@Internal +final class ConstructIsoTimestampString extends AbstractConstruct { + private static final Pattern TIMESTAMP_REGEXP = Pattern.compile( + "^([0-9][0-9][0-9][0-9])-([0-9][0-9]?)-([0-9][0-9]?)(?:(?:[Tt]|[ \t]+)([0-9][0-9]?):([0-9][0-9]):([0-9][0-9])(?:\\.([0-9]*))?(?:[ \t]*(?:(Z)|([-+][0-9][0-9]?)(?::([0-9][0-9])?)?))?)?$"); + private static final Pattern YMD_REGEXP = Pattern + .compile("^([0-9][0-9][0-9][0-9])-([0-9][0-9]?)-([0-9][0-9]?)$"); + + @Override + public Object construct(Node node) { + ScalarNode scalar = (ScalarNode) node; + String nodeValue = scalar.getValue(); + return parse(nodeValue); + } + + @NonNull + static Temporal parse(String nodeValue) { + Matcher match = YMD_REGEXP.matcher(nodeValue); + if (match.matches()) { + String yearS = match.group(1); + String monthS = match.group(2); + String dayS = match.group(3); + return LocalDate.of( + Integer.parseInt(yearS), + Integer.parseInt(monthS), + Integer.parseInt(dayS) + ); + } else { + match = TIMESTAMP_REGEXP.matcher(nodeValue); + if (!match.matches()) { + throw new YAMLException("Unexpected timestamp: " + nodeValue); + } + String yearS = match.group(1); + String monthS = match.group(2); + String dayS = match.group(3); + String hourS = match.group(4); + String minS = match.group(5); + // seconds and milliseconds + String seconds = match.group(6); + String millis = match.group(7); + if (millis != null) { + seconds = seconds + "." + millis; + } + double fractions = Double.parseDouble(seconds); + int secS = (int) Math.round(Math.floor(fractions)); + int nsec = (int) Math.round((fractions - secS) * 1_000_000_000); + + LocalDateTime ldt = LocalDateTime.of( + Integer.parseInt(yearS), + Integer.parseInt(monthS), + Integer.parseInt(dayS), + Integer.parseInt(hourS), + Integer.parseInt(minS), + secS, + nsec + ); + + // timezone + String timezonehS = match.group(9); + String timezonemS = match.group(10); + if (timezonehS != null) { + ZoneOffset offset; + if (timezonemS == null) { + offset = ZoneOffset.ofHours(Integer.parseInt(timezonehS)); + } else { + offset = ZoneOffset.ofHoursMinutes(Integer.parseInt(timezonehS), Integer.parseInt(timezonemS)); + } + return ldt.atOffset(offset); + } else { + if (match.group(8) != null) { + // Z + return ldt.atOffset(ZoneOffset.UTC); + } else { + // no time zone provided + return ldt; + } + } + } + } +} diff --git a/inject/src/main/java/io/micronaut/context/env/yaml/CustomSafeConstructor.java b/inject/src/main/java/io/micronaut/context/env/yaml/CustomSafeConstructor.java index 06caafb4260..3c1a2d9cfa5 100644 --- a/inject/src/main/java/io/micronaut/context/env/yaml/CustomSafeConstructor.java +++ b/inject/src/main/java/io/micronaut/context/env/yaml/CustomSafeConstructor.java @@ -19,6 +19,7 @@ import org.yaml.snakeyaml.constructor.SafeConstructor; import org.yaml.snakeyaml.nodes.MappingNode; import org.yaml.snakeyaml.nodes.SequenceNode; +import org.yaml.snakeyaml.nodes.Tag; import java.util.List; import java.util.Map; @@ -32,6 +33,9 @@ */ @Internal class CustomSafeConstructor extends SafeConstructor { + CustomSafeConstructor() { + yamlConstructors.put(Tag.TIMESTAMP, new ConstructIsoTimestampString()); + } @Override protected Map newMap(MappingNode node) { diff --git a/inject/src/test/groovy/io/micronaut/context/env/yaml/ConstructIsoTimestampStringSpec.groovy b/inject/src/test/groovy/io/micronaut/context/env/yaml/ConstructIsoTimestampStringSpec.groovy new file mode 100644 index 00000000000..e4d639a58c7 --- /dev/null +++ b/inject/src/test/groovy/io/micronaut/context/env/yaml/ConstructIsoTimestampStringSpec.groovy @@ -0,0 +1,25 @@ +package io.micronaut.context.env.yaml + +import spock.lang.Specification + +import java.time.LocalDate +import java.time.LocalDateTime +import java.time.ZoneOffset +import java.time.temporal.Temporal + +class ConstructIsoTimestampStringSpec extends Specification { + def parse(String literal, Temporal expected) { + when: + Temporal parsed = ConstructIsoTimestampString.parse(literal) + then: + parsed == expected + + where: + literal | expected + '2022-08-12' | LocalDate.of(2022, 8, 12) + '2022-08-12T10:01:02.345' | LocalDateTime.of(2022, 8, 12, 10, 1, 2, 345_000_000) + '2022-08-12T10:01:02.345Z' | LocalDateTime.of(2022, 8, 12, 10, 1, 2, 345_000_000).atOffset(ZoneOffset.UTC) + '2022-08-12T10:01:02.345+05' | LocalDateTime.of(2022, 8, 12, 10, 1, 2, 345_000_000).atOffset(ZoneOffset.ofHours(5)) + '2022-08-12T10:01:02.345+05:06' | LocalDateTime.of(2022, 8, 12, 10, 1, 2, 345_000_000).atOffset(ZoneOffset.ofHoursMinutes(5, 6)) + } +} diff --git a/inject/src/test/groovy/io/micronaut/context/env/yaml/YamlPropertySourceLoaderSpec2.groovy b/inject/src/test/groovy/io/micronaut/context/env/yaml/YamlPropertySourceLoaderSpec2.groovy new file mode 100644 index 00000000000..0fcf44a6625 --- /dev/null +++ b/inject/src/test/groovy/io/micronaut/context/env/yaml/YamlPropertySourceLoaderSpec2.groovy @@ -0,0 +1,93 @@ +/* + * Copyright 2017-2019 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.context.env.yaml + +import io.micronaut.context.ApplicationContextConfiguration +import io.micronaut.context.env.DefaultEnvironment +import io.micronaut.context.env.Environment +import io.micronaut.context.env.PropertySourceLoader +import io.micronaut.core.io.service.ServiceDefinition +import io.micronaut.core.io.service.SoftServiceLoader +import spock.lang.Specification + +import java.time.LocalDate +import java.time.LocalDateTime +import java.time.OffsetDateTime +import java.time.ZoneOffset + +/** + * not limited by groovy version + */ +class YamlPropertySourceLoaderSpec2 extends Specification { + + void "test yaml value conversion"(String literal, Class type, Object expected) { + given: + def serviceDefinition = Mock(ServiceDefinition) + serviceDefinition.isPresent() >> true + serviceDefinition.load() >> new YamlPropertySourceLoader() + + Environment env = new DefaultEnvironment(new ApplicationContextConfiguration() { + @Override + List getEnvironments() { + return ['test'] + } + }) { + @Override + protected SoftServiceLoader readPropertySourceLoaders() { + GroovyClassLoader gcl = new GroovyClassLoader() + gcl.addClass(YamlPropertySourceLoader) + gcl.addURL(YamlPropertySourceLoader.getResource("/META-INF/services/io.micronaut.context.env.PropertySourceLoader")) + return new SoftServiceLoader(PropertySourceLoader, gcl) + } + + @Override + Optional getResourceAsStream(String path) { + if (path.endsWith("application.yml")) { + return Optional.of(new ByteArrayInputStream("""\ +foo: $literal +""".bytes)) + } + + return Optional.empty() + } + + } + + + when: + env.start() + + then: + env.get("foo", type).get() == expected + + where: + // note: string->Date is not supported + + literal | type | expected + // YMD + '2022-08-12' | LocalDate | LocalDate.of(2022, 8, 12) + '"2022-08-12"' | LocalDate | LocalDate.of(2022, 8, 12) + '2022-08-12' | Date | Date.from(LocalDate.of(2022, 8, 12).atTime(0, 0).atOffset(ZoneOffset.UTC).toInstant()) + // YMD HMS + '2022-08-12T10:12:34' | LocalDateTime | LocalDateTime.of(2022, 8, 12, 10, 12, 34) + '"2022-08-12T10:12:34"' | LocalDateTime | LocalDateTime.of(2022, 8, 12, 10, 12, 34) + '2022-08-12T10:12:34' | Date | Date.from(LocalDateTime.of(2022, 8, 12, 10, 12, 34).atOffset(ZoneOffset.UTC).toInstant()) + // YMD HMS Z + '2022-08-12T10:12:34+05:00' | OffsetDateTime | LocalDateTime.of(2022, 8, 12, 10, 12, 34).atOffset(ZoneOffset.ofHours(5)) + '"2022-08-12T10:12:34+05:00"' | OffsetDateTime | LocalDateTime.of(2022, 8, 12, 10, 12, 34).atOffset(ZoneOffset.ofHours(5)) + '2022-08-12T10:12:34+05:00' | Date | Date.from(LocalDateTime.of(2022, 8, 12, 10, 12, 34).atOffset(ZoneOffset.ofHours(5)).toInstant()) + } +} diff --git a/runtime/src/test/groovy/io/micronaut/runtime/converters/time/TimeConverterRegistrarSpec.groovy b/runtime/src/test/groovy/io/micronaut/runtime/converters/time/TimeConverterRegistrarSpec.groovy index 4057334138a..830825cb871 100644 --- a/runtime/src/test/groovy/io/micronaut/runtime/converters/time/TimeConverterRegistrarSpec.groovy +++ b/runtime/src/test/groovy/io/micronaut/runtime/converters/time/TimeConverterRegistrarSpec.groovy @@ -21,6 +21,14 @@ import spock.lang.Specification import spock.lang.Unroll import java.time.Duration +import java.time.Instant +import java.time.LocalDate +import java.time.LocalDateTime +import java.time.OffsetDateTime +import java.time.OffsetTime +import java.time.ZoneId +import java.time.ZoneOffset +import java.time.ZonedDateTime class TimeConverterRegistrarSpec extends Specification { @@ -44,4 +52,29 @@ class TimeConverterRegistrarSpec extends Specification { '10ns' | Duration.ofNanos(10) } + @Unroll + void "test converts a #sourceObject.class.name to a #targetType.name"() { + given: + ConversionService conversionService = new DefaultConversionService() + new TimeConverterRegistrar().register(conversionService) + + expect: + conversionService.convert(sourceObject, targetType).get() == result + + where: + sourceObject | targetType | result + Instant.ofEpochMilli(123) | Date | new Date(123) + Instant.ofEpochMilli(123).atOffset(ZoneOffset.ofHours(5)) | Date | new Date(123) + Instant.ofEpochMilli(123).atZone(ZoneId.of("Europe/Berlin")) | Date | new Date(123) + Instant.ofEpochMilli(123).atOffset(ZoneOffset.UTC).toLocalDateTime() | Date | new Date(123) + Instant.ofEpochMilli(123).atOffset(ZoneOffset.UTC).toLocalDate() | Date | new Date(0) + + "2022-08-12" | LocalDate | LocalDate.of(2022, 8, 12) + "2022-08-12T12:19:00" | LocalDateTime | LocalDateTime.of(2022, 8, 12, 12, 19) + "12:19:00+05:00" | OffsetTime | OffsetTime.of(12, 19, 0, 0, ZoneOffset.ofHours(5)) + "2022-08-12T12:19:00+05:00" | OffsetDateTime | OffsetDateTime.of(2022, 8, 12, 12, 19, 0, 0, ZoneOffset.ofHours(5)) + "2022-08-12T12:19:00+05:00" | ZonedDateTime | ZonedDateTime.of(2022, 8, 12, 12, 19, 0, 0, ZoneOffset.ofHours(5)) + "2022-08-12T12:19:00+02:00[Europe/Berlin]" | ZonedDateTime | ZonedDateTime.of(2022, 8, 12, 12, 19, 0, 0, ZoneId.of("Europe/Berlin")) + "2022-08-12T12:19:00Z" | Instant | LocalDateTime.of(2022, 8, 12, 12, 19).toInstant(ZoneOffset.UTC) + } } From be6ed9b7a6f6826f6b59998064bde304230c6aba Mon Sep 17 00:00:00 2001 From: Tim Yates Date: Wed, 14 Sep 2022 12:53:56 +0100 Subject: [PATCH 026/743] build: Add CRaC module to BOM (#7997) --- gradle/libs.versions.toml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 9c2b3c812e9..2f1b8215642 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -72,6 +72,7 @@ managed-micronaut-azure = "3.4.0" managed-micronaut-cache = "3.5.0" managed-micronaut-cassandra = "5.1.1" managed-micronaut-coherence = "3.5.1" +managed-micronaut-crac = "1.0.0" managed-micronaut-data = "3.7.3" managed-micronaut-discovery = "3.2.0" managed-micronaut-elasticsearch = "4.3.0" @@ -149,6 +150,7 @@ boms-micronaut-aws = { module = "io.micronaut.aws:micronaut-aws-bom", version.re boms-micronaut-azure = { module = "io.micronaut.azure:micronaut-azure-bom", version.ref = "managed-micronaut-azure" } boms-micronaut-cache = { module = "io.micronaut.cache:micronaut-cache-bom", version.ref = "managed-micronaut-cache" } boms-micronaut-coherence = { module = "io.micronaut.coherence:micronaut-coherence-bom", version.ref = "managed-micronaut-coherence" } +boms-micronaut-crac = { module = "io.micronaut.crac:micronaut-crac-bom", version.ref = "managed-micronaut-crac" } boms-micronaut-email = { module = "io.micronaut.email:micronaut-email-bom", version.ref = "managed-micronaut-email" } boms-micronaut-data = { module = "io.micronaut.data:micronaut-data-bom", version.ref = "managed-micronaut-data" } boms-micronaut-gcp = { module = "io.micronaut.gcp:micronaut-gcp-bom", version.ref = "managed-micronaut-gcp" } From d530a878c5e518e052855a0a5d39d0ca302ef985 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 14 Sep 2022 13:54:11 +0200 Subject: [PATCH 027/743] fix(deps): update dependency org.yaml:snakeyaml to v1.32 (#7996) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 2f1b8215642..b3d28d61b0f 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -141,7 +141,7 @@ managed-springboot = "2.7.0" managed-swagger = "2.2.2" managed-validation = "2.0.1.Final" managed-testcontainers = "1.17.3" -managed-snakeyaml = "1.31" +managed-snakeyaml = "1.32" micronaut-docs = "2.0.0" [libraries] From 1918e255d83c82e585ecfc6f6e5c09a3836f2792 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 14 Sep 2022 13:54:29 +0200 Subject: [PATCH 028/743] fix(deps): update dependency io.micronaut.azure:micronaut-azure-bom to v3.5.0 (#7983) * fix: UriBuilder queryParam and replaceQueryParam should ignore null values (#7681) * fix(deps): update dependency io.micronaut.azure:micronaut-azure-bom to v3.5.0 Co-authored-by: Yohan Siguret Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Sergio del Amo --- gradle/libs.versions.toml | 2 +- .../micronaut/http/uri/DefaultUriBuilder.java | 9 ++++- .../micronaut/http/uri/UriBuilderSpec.groovy | 38 ++++++++++++++++--- 3 files changed, 40 insertions(+), 9 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index b3d28d61b0f..546aadcd68c 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -68,7 +68,7 @@ managed-micrometer = "1.9.3" managed-micronaut-acme = "3.2.0" managed-micronaut-aot = "1.1.1" managed-micronaut-aws = "3.8.0" -managed-micronaut-azure = "3.4.0" +managed-micronaut-azure = "3.5.0" managed-micronaut-cache = "3.5.0" managed-micronaut-cassandra = "5.1.1" managed-micronaut-coherence = "3.5.1" diff --git a/http/src/main/java/io/micronaut/http/uri/DefaultUriBuilder.java b/http/src/main/java/io/micronaut/http/uri/DefaultUriBuilder.java index 263e1a62dd7..20dc8add682 100644 --- a/http/src/main/java/io/micronaut/http/uri/DefaultUriBuilder.java +++ b/http/src/main/java/io/micronaut/http/uri/DefaultUriBuilder.java @@ -18,6 +18,7 @@ import io.micronaut.core.convert.value.MutableConvertibleMultiValues; import io.micronaut.core.convert.value.MutableConvertibleMultiValuesMap; import io.micronaut.core.util.ArrayUtils; +import io.micronaut.core.util.CollectionUtils; import io.micronaut.core.util.StringUtils; import io.micronaut.http.exceptions.UriSyntaxException; @@ -235,7 +236,9 @@ public UriBuilder queryParam(String name, Object... values) { strings.add(value.toString()); } } - queryParams.put(name, strings); + if (CollectionUtils.isNotEmpty(strings)) { + queryParams.put(name, strings); + } } return this; } @@ -250,7 +253,9 @@ public UriBuilder replaceQueryParam(String name, Object... values) { strings.add(value.toString()); } } - queryParams.put(name, strings); + if (CollectionUtils.isNotEmpty(strings)) { + queryParams.put(name, strings); + } } return this; } diff --git a/http/src/test/groovy/io/micronaut/http/uri/UriBuilderSpec.groovy b/http/src/test/groovy/io/micronaut/http/uri/UriBuilderSpec.groovy index 7f2f910169c..d7e65306869 100644 --- a/http/src/test/groovy/io/micronaut/http/uri/UriBuilderSpec.groovy +++ b/http/src/test/groovy/io/micronaut/http/uri/UriBuilderSpec.groovy @@ -138,12 +138,38 @@ class UriBuilderSpec extends Specification { builder.toString() == expected where: - uri | params | expected - '/foo?existing=true' | ['foo': 'bar'] | '/foo?existing=true&foo=bar' - '/foo' | ['foo': 'bar'] | '/foo?foo=bar' - '/foo' | ['foo': 'hello world'] | '/foo?foo=hello+world' - '/foo' | ['foo': ['bar', 'baz']] | '/foo?foo=bar&foo=baz' - '/foo' | ['foo': ['bar', 'baz']] | '/foo?foo=bar&foo=baz' + uri | params | expected + '/foo?existing=true' | ['foo': 'bar'] | '/foo?existing=true&foo=bar' + '/foo' | ['foo': 'bar'] | '/foo?foo=bar' + '/foo' | ['foo': 'hello world'] | '/foo?foo=hello+world' + '/foo' | ['foo': ['bar', 'baz']] | '/foo?foo=bar&foo=baz' + '/foo' | ['foo': null, 'bar': 'baz'] | '/foo?bar=baz' + '/foo' | ['foo': [null, null], 'bar': 'baz'] | '/foo?bar=baz' + } + + @Unroll + void "test replaceQueryParam method for uri #uri"() { + given: + def builder = UriBuilder.of(uri) + for (p in params) { + if (p.value instanceof List) { + builder.replaceQueryParam(p.key, *p.value) + } else { + builder.replaceQueryParam(p.key, p.value) + } + } + + expect: + builder.toString() == expected + + where: + uri | params | expected + '/foo?foo=old' | ['foo': 'bar'] | '/foo?foo=bar' + '/foo?old=keep' | ['foo': 'bar'] | '/foo?old=keep&foo=bar' + '/foo?foo=old' | ['foo': 'hello world'] | '/foo?foo=hello+world' + '/foo?foo=old' | ['foo': ['bar', 'baz']] | '/foo?foo=bar&foo=baz' + '/foo?foo=old' | ['foo': null, 'bar': 'baz'] | '/foo?foo=old&bar=baz' + '/foo?foo=old' | ['foo': [null, null], 'bar': 'baz'] | '/foo?foo=old&bar=baz' } @Issue("https://github.com/micronaut-projects/micronaut-core/issues/2823") From 5aab51f976ba4efa90855dbf74f9410d397725ce Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 14 Sep 2022 23:14:55 +0200 Subject: [PATCH 029/743] fix(deps): update netty monorepo to v4.1.82.final (#7999) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 546aadcd68c..7a9a70c44f0 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -126,7 +126,7 @@ managed-micronaut-views = "3.5.0" managed-micronaut-xml = "3.1.0" managed-neo4j = "3.5.35" managed-neo4j-java-driver = "4.4.9" -managed-netty = "4.1.81.Final" +managed-netty = "4.1.82.Final" managed-reactive-pg-client = "0.11.4" managed-reactive-streams = "1.0.4" # This should be kept aligned with https://github.com/micronaut-projects/micronaut-reactor/blob/master/gradle.properties from the BOM From 4547733a61b5c0d21ad3c8a90aca4e9e356da585 Mon Sep 17 00:00:00 2001 From: micronaut-build <65172877+micronaut-build@users.noreply.github.com> Date: Wed, 14 Sep 2022 17:15:18 -0400 Subject: [PATCH 030/743] Bump micronaut-jms to 2.1.0 (#8002) --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 7a9a70c44f0..d842dc9b49a 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -85,7 +85,7 @@ managed-micronaut-grpc = "3.3.1" managed-micronaut-hibernate-validator = "3.0.1" managed-micronaut-ignite = "1.0.0.RC1" managed-micronaut-jaxrs = "3.4.0" -managed-micronaut-jms = "2.0.0-M1" +managed-micronaut-jms = "2.1.0" managed-micronaut-jmx = "3.1.0" managed-micronaut-kafka = "4.4.0" managed-micronaut-kotlin = "3.2.2" From 6b6a4e0e0c46d2e3db29becf518e6ed94cd61940 Mon Sep 17 00:00:00 2001 From: micronaut-build <65172877+micronaut-build@users.noreply.github.com> Date: Wed, 14 Sep 2022 17:15:30 -0400 Subject: [PATCH 031/743] Bump micronaut-hibernate-validator to 3.1.0 (#8000) --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index d842dc9b49a..f00020f0bbe 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -82,7 +82,7 @@ managed-micronaut-gcp = "4.5.0" managed-micronaut-graphql = "3.1.0" managed-micronaut-groovy = "3.2.0" managed-micronaut-grpc = "3.3.1" -managed-micronaut-hibernate-validator = "3.0.1" +managed-micronaut-hibernate-validator = "3.1.0" managed-micronaut-ignite = "1.0.0.RC1" managed-micronaut-jaxrs = "3.4.0" managed-micronaut-jms = "2.1.0" From bbd8888a27fe2143377dfc5d1082c6476e87f401 Mon Sep 17 00:00:00 2001 From: micronaut-build <65172877+micronaut-build@users.noreply.github.com> Date: Thu, 15 Sep 2022 00:50:33 -0400 Subject: [PATCH 032/743] build: Bump micronaut-groovy to 3.3.0 (#7998) --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index f00020f0bbe..8477dcdc09b 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -80,7 +80,7 @@ managed-micronaut-email = "1.3.2" managed-micronaut-flyway = "5.4.1" managed-micronaut-gcp = "4.5.0" managed-micronaut-graphql = "3.1.0" -managed-micronaut-groovy = "3.2.0" +managed-micronaut-groovy = "3.3.0" managed-micronaut-grpc = "3.3.1" managed-micronaut-hibernate-validator = "3.1.0" managed-micronaut-ignite = "1.0.0.RC1" From 8dab0555f4277425cea2dd73b504a612dfc56d74 Mon Sep 17 00:00:00 2001 From: micronaut-build <65172877+micronaut-build@users.noreply.github.com> Date: Thu, 15 Sep 2022 00:51:05 -0400 Subject: [PATCH 033/743] build: Bump micronaut-azure to 3.5.0 (#7992) From 156453fc741a8e549df058cd682c9c8cbadec1ad Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Thu, 15 Sep 2022 07:32:49 +0200 Subject: [PATCH 034/743] build: Microstream 1.1.0 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 49085fe637d..7bccb854857 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -91,7 +91,7 @@ managed-micronaut-kafka = "4.4.0" managed-micronaut-kotlin = "3.2.2" managed-micronaut-kubernetes = "3.4.0" managed-micronaut-micrometer = "4.5.0" -managed-micronaut-microstream = "1.0.0" +managed-micronaut-microstream = "1.1.0" managed-micronaut-liquibase = "5.4.1" managed-micronaut-mongo = "4.4.0" managed-micronaut-mqtt = "2.2.0" From ae8adcfd6b49f7ba62fa62ec6e0d4f1c0a547598 Mon Sep 17 00:00:00 2001 From: micronaut-build <65172877+micronaut-build@users.noreply.github.com> Date: Thu, 15 Sep 2022 03:57:55 -0400 Subject: [PATCH 035/743] Bump micronaut-hibernate-validator to 3.2.0 (#8006) --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 7bccb854857..6369ea35352 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -82,7 +82,7 @@ managed-micronaut-gcp = "4.5.0" managed-micronaut-graphql = "3.1.0" managed-micronaut-groovy = "3.3.0" managed-micronaut-grpc = "3.3.1" -managed-micronaut-hibernate-validator = "3.1.0" +managed-micronaut-hibernate-validator = "3.2.0" managed-micronaut-ignite = "1.0.0.RC1" managed-micronaut-jaxrs = "3.4.0" managed-micronaut-jms = "2.1.0" From 52efd86ae16d3ef0a4ae4e8837e9817d44794d05 Mon Sep 17 00:00:00 2001 From: micronaut-build <65172877+micronaut-build@users.noreply.github.com> Date: Thu, 15 Sep 2022 03:58:24 -0400 Subject: [PATCH 036/743] build: Bump micronaut-security to 3.8.0 (#8003) --- gradle.properties | 2 +- gradle/libs.versions.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle.properties b/gradle.properties index 04a8b54b889..c797ae746c2 100644 --- a/gradle.properties +++ b/gradle.properties @@ -52,7 +52,7 @@ kotlin.stdlib.default.dependency=false # For the docs graalVersion=22.0.0.2 -micronautSecurityVersion=3.7.1 +micronautSecurityVersion=3.8.0 org.gradle.caching=true org.gradle.parallel=true diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 6369ea35352..4a0b65bfe00 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -112,7 +112,7 @@ managed-micronaut-rss = "3.1.0" managed-micronaut-rxjava1 = "1.0.0" managed-micronaut-rxjava2 = "1.2.2" managed-micronaut-rxjava3 = "2.3.0" -managed-micronaut-security = "3.7.1" +managed-micronaut-security = "3.8.0" managed-micronaut-serialization = "1.3.2" managed-micronaut-servlet = "3.3.1" managed-micronaut-spring = "4.3.0" From b50c4a9a6d38fde90f8f60c342d11f865154d27f Mon Sep 17 00:00:00 2001 From: micronaut-build <65172877+micronaut-build@users.noreply.github.com> Date: Thu, 15 Sep 2022 03:58:58 -0400 Subject: [PATCH 037/743] Bump micronaut-oracle-cloud to 2.2.0 (#7990) --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 4a0b65bfe00..e8714a36a2c 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -101,7 +101,7 @@ managed-micronaut-nats = "3.1.0" managed-micronaut-netflix = "2.1.0" managed-micronaut-object-storage = "1.0.0" managed-micronaut-openapi = "4.4.3" -managed-micronaut-oraclecloud = "2.1.5" +managed-micronaut-oraclecloud = "2.2.0" managed-micronaut-picocli = "4.3.0" managed-micronaut-problem = "2.4.1" managed-micronaut-rabbitmq = "3.3.0" From 69cd05e7fd05dd5dca39bf94455b352436695390 Mon Sep 17 00:00:00 2001 From: Jonas Konrad Date: Thu, 15 Sep 2022 11:31:30 +0200 Subject: [PATCH 038/743] refactor : HTTP client refactor (#7994) * move some fields to ConnectionManager, no big changes * move doConnect, and merge the methods * move ssl context init * move to a PoolHandle api instead of working with the pool directly * move sendRequestThroughChannel logic to flatmap operation * move connectForExchange * move stream connection method * move websocket connection method * move some methods that are only used in ConnectionManager now * move some methods that are only used in ConnectionManager now * move HttpClientInitializer * move connection init methods * move Http2SettingsHandler and UpgradeRequestHandler * delay channel release until http2 settings arrive, instead of waiting further downstream * move configureProxy * move pool shutdown logic * hide ConnectionManager bootstrap * move addInstrumentedListener * duplicate customizeException * remove closeChannelAsync, it did nothing closeFuture() does not close the channel, and it can never fail. * move h2c stream 1 discard logic to a handler * move read timeout handling to ConnectionManager * remove logging for termination failure, this can never happen with NioEventLoopGroup * inline misleading close method * hide sslContext * move event loop init logic to ConnectionManager * move more initializers to ConnectionManager * move some channel attrs to ConnectionManager * move CHANNEL_CUSTOMIZER_KEY * checkstyle * move isAcceptEvents back * save an intermediate promise we don't need anymore * merge some proxy setup methods * docs --- .../http/client/netty/ConnectTTLHandler.java | 2 +- .../http/client/netty/ConnectionManager.java | 1324 ++++++++++++++ .../http/client/netty/DefaultHttpClient.java | 1627 ++--------------- .../NettyWebSocketClientHandler.java | 32 +- .../client/ClientEventLoopGroupSpec.groovy | 2 +- .../http/client/ConnectionTTLSpec.groovy | 5 +- .../http/client/IdleTimeoutSpec.groovy | 6 +- .../http/client/ReadTimeoutSpec.groovy | 2 +- 8 files changed, 1526 insertions(+), 1474 deletions(-) create mode 100644 http-client/src/main/java/io/micronaut/http/client/netty/ConnectionManager.java diff --git a/http-client/src/main/java/io/micronaut/http/client/netty/ConnectTTLHandler.java b/http-client/src/main/java/io/micronaut/http/client/netty/ConnectTTLHandler.java index b9fb9682676..724611fb18a 100644 --- a/http-client/src/main/java/io/micronaut/http/client/netty/ConnectTTLHandler.java +++ b/http-client/src/main/java/io/micronaut/http/client/netty/ConnectTTLHandler.java @@ -78,7 +78,7 @@ private void markChannelExpired(ChannelHandlerContext ctx) { } /** - * Indicates whether the channels connection ttl has expired + * Indicates whether the channels connection ttl has expired. * @param channel The channel to check * @return true if the channels ttl has expired */ diff --git a/http-client/src/main/java/io/micronaut/http/client/netty/ConnectionManager.java b/http-client/src/main/java/io/micronaut/http/client/netty/ConnectionManager.java new file mode 100644 index 00000000000..fe4f9769767 --- /dev/null +++ b/http-client/src/main/java/io/micronaut/http/client/netty/ConnectionManager.java @@ -0,0 +1,1324 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.http.client.netty; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.NonNull; +import io.micronaut.core.annotation.Nullable; +import io.micronaut.core.reflect.InstantiationUtils; +import io.micronaut.core.util.StringUtils; +import io.micronaut.http.HttpVersion; +import io.micronaut.http.client.HttpClientConfiguration; +import io.micronaut.http.client.exceptions.HttpClientException; +import io.micronaut.http.client.netty.ssl.NettyClientSslBuilder; +import io.micronaut.http.netty.channel.ChannelPipelineCustomizer; +import io.micronaut.http.netty.channel.ChannelPipelineListener; +import io.micronaut.http.netty.channel.NettyThreadFactory; +import io.micronaut.http.netty.stream.DefaultHttp2Content; +import io.micronaut.http.netty.stream.Http2Content; +import io.micronaut.http.netty.stream.HttpStreamsClientHandler; +import io.micronaut.http.netty.stream.StreamingInboundHttp2ToHttpAdapter; +import io.micronaut.scheduling.instrument.Instrumentation; +import io.micronaut.scheduling.instrument.InvocationInstrumenter; +import io.micronaut.websocket.exceptions.WebSocketSessionException; +import io.netty.bootstrap.Bootstrap; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.netty.channel.Channel; +import io.netty.channel.ChannelFactory; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelFutureListener; +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.ChannelOption; +import io.netty.channel.ChannelPipeline; +import io.netty.channel.ChannelPromise; +import io.netty.channel.EventLoopGroup; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.pool.AbstractChannelPoolHandler; +import io.netty.channel.pool.AbstractChannelPoolMap; +import io.netty.channel.pool.ChannelHealthChecker; +import io.netty.channel.pool.ChannelPool; +import io.netty.channel.pool.ChannelPoolMap; +import io.netty.channel.pool.FixedChannelPool; +import io.netty.channel.pool.SimpleChannelPool; +import io.netty.channel.socket.SocketChannel; +import io.netty.handler.codec.LineBasedFrameDecoder; +import io.netty.handler.codec.http.DefaultFullHttpRequest; +import io.netty.handler.codec.http.DefaultHttpContent; +import io.netty.handler.codec.http.FullHttpMessage; +import io.netty.handler.codec.http.HttpClientCodec; +import io.netty.handler.codec.http.HttpClientUpgradeHandler; +import io.netty.handler.codec.http.HttpContent; +import io.netty.handler.codec.http.HttpContentDecompressor; +import io.netty.handler.codec.http.HttpHeaderNames; +import io.netty.handler.codec.http.HttpMessage; +import io.netty.handler.codec.http.HttpMethod; +import io.netty.handler.codec.http.HttpObjectAggregator; +import io.netty.handler.codec.http.HttpUtil; +import io.netty.handler.codec.http.LastHttpContent; +import io.netty.handler.codec.http.websocketx.extensions.compression.WebSocketClientCompressionHandler; +import io.netty.handler.codec.http2.DefaultHttp2Connection; +import io.netty.handler.codec.http2.DelegatingDecompressorFrameListener; +import io.netty.handler.codec.http2.Http2ClientUpgradeCodec; +import io.netty.handler.codec.http2.Http2Connection; +import io.netty.handler.codec.http2.Http2FrameListener; +import io.netty.handler.codec.http2.Http2FrameLogger; +import io.netty.handler.codec.http2.Http2Settings; +import io.netty.handler.codec.http2.Http2Stream; +import io.netty.handler.codec.http2.HttpConversionUtil; +import io.netty.handler.codec.http2.HttpToHttp2ConnectionHandler; +import io.netty.handler.codec.http2.HttpToHttp2ConnectionHandlerBuilder; +import io.netty.handler.codec.http2.InboundHttp2ToHttpAdapterBuilder; +import io.netty.handler.logging.LoggingHandler; +import io.netty.handler.proxy.HttpProxyHandler; +import io.netty.handler.proxy.Socks5ProxyHandler; +import io.netty.handler.ssl.ApplicationProtocolNames; +import io.netty.handler.ssl.ApplicationProtocolNegotiationHandler; +import io.netty.handler.ssl.SslContext; +import io.netty.handler.ssl.SslHandler; +import io.netty.handler.timeout.IdleStateEvent; +import io.netty.handler.timeout.IdleStateHandler; +import io.netty.handler.timeout.ReadTimeoutHandler; +import io.netty.resolver.NoopAddressResolverGroup; +import io.netty.util.Attribute; +import io.netty.util.AttributeKey; +import io.netty.util.ReferenceCountUtil; +import io.netty.util.concurrent.Future; +import io.netty.util.concurrent.GenericFutureListener; +import io.netty.util.concurrent.Promise; +import org.reactivestreams.Publisher; +import org.slf4j.Logger; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.core.publisher.Sinks; +import reactor.core.scheduler.Scheduler; +import reactor.core.scheduler.Schedulers; + +import java.net.InetSocketAddress; +import java.net.Proxy; +import java.net.SocketAddress; +import java.time.Duration; +import java.util.Collection; +import java.util.Map; +import java.util.Optional; +import java.util.OptionalInt; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; + +/** + * Connection manager for {@link DefaultHttpClient}. This class manages the lifecycle of netty + * channels (wrapped in {@link PoolHandle}s), including pooling and timeouts. + */ +@Internal +final class ConnectionManager { + private static final AttributeKey CHANNEL_CUSTOMIZER_KEY = + AttributeKey.valueOf("micronaut.http.customizer"); + /** + * Future on a pooled channel that will be completed when the channel has fully connected (e.g. + * TLS handshake has completed). If unset, then no handshake is needed or it has already + * completed. + */ + private static final AttributeKey> STREAM_CHANNEL_INITIALIZED = + AttributeKey.valueOf("micronaut.http.streamChannelInitialized"); + private static final AttributeKey STREAM_KEY = AttributeKey.valueOf("micronaut.http2.stream"); + + final ChannelPoolMap poolMap; + final InvocationInstrumenter instrumenter; + final HttpVersion httpVersion; + + private final Logger log; + private EventLoopGroup group; + private final boolean shutdownGroup; + private final ThreadFactory threadFactory; + private final Bootstrap bootstrap; + private final HttpClientConfiguration configuration; + @Nullable + private final Long readTimeoutMillis; + @Nullable + private final Long connectionTimeAliveMillis; + private final SslContext sslContext; + private final NettyClientCustomizer clientCustomizer; + private final Collection pipelineListeners; + private final String informationalServiceId; + + ConnectionManager( + Logger log, + @Nullable EventLoopGroup eventLoopGroup, + ThreadFactory threadFactory, + HttpClientConfiguration configuration, + HttpVersion httpVersion, + InvocationInstrumenter instrumenter, + ChannelFactory socketChannelFactory, + NettyClientSslBuilder nettyClientSslBuilder, + NettyClientCustomizer clientCustomizer, + Collection pipelineListeners, + String informationalServiceId) { + + if (httpVersion == null) { + httpVersion = configuration.getHttpVersion(); + } + + this.log = log; + this.httpVersion = httpVersion; + this.threadFactory = threadFactory; + this.configuration = configuration; + this.instrumenter = instrumenter; + this.clientCustomizer = clientCustomizer; + this.pipelineListeners = pipelineListeners; + this.informationalServiceId = informationalServiceId; + + this.connectionTimeAliveMillis = configuration.getConnectTtl() + .map(duration -> !duration.isNegative() ? duration.toMillis() : null) + .orElse(null); + this.readTimeoutMillis = configuration.getReadTimeout() + .map(duration -> !duration.isNegative() ? duration.toMillis() : null) + .orElse(null); + this.sslContext = nettyClientSslBuilder.build(configuration.getSslConfiguration(), httpVersion).orElse(null); + + if (eventLoopGroup != null) { + group = eventLoopGroup; + shutdownGroup = false; + } else { + group = createEventLoopGroup(configuration, threadFactory); + shutdownGroup = true; + } + + this.bootstrap = new Bootstrap(); + this.bootstrap.group(group) + .channelFactory(socketChannelFactory) + .option(ChannelOption.SO_KEEPALIVE, true); + + final ChannelHealthChecker channelHealthChecker = channel -> channel.eventLoop().newSucceededFuture(channel.isActive() && !ConnectTTLHandler.isChannelExpired(channel)); + + HttpClientConfiguration.ConnectionPoolConfiguration connectionPoolConfiguration = configuration.getConnectionPoolConfiguration(); + // HTTP/2 defaults to keep alive connections so should we should always use a pool + if (connectionPoolConfiguration.isEnabled() || httpVersion == io.micronaut.http.HttpVersion.HTTP_2_0) { + int maxConnections = connectionPoolConfiguration.getMaxConnections(); + if (maxConnections > -1) { + poolMap = new AbstractChannelPoolMap() { + @Override + protected ChannelPool newPool(DefaultHttpClient.RequestKey key) { + Bootstrap newBootstrap = bootstrap.clone(group); + initBootstrapForProxy(newBootstrap, key.isSecure(), key.getHost(), key.getPort()); + newBootstrap.remoteAddress(key.getRemoteAddress()); + + AbstractChannelPoolHandler channelPoolHandler = newPoolHandler(key); + final long acquireTimeoutMillis = connectionPoolConfiguration.getAcquireTimeout().map(Duration::toMillis).orElse(-1L); + return new FixedChannelPool( + newBootstrap, + channelPoolHandler, + channelHealthChecker, + acquireTimeoutMillis > -1 ? FixedChannelPool.AcquireTimeoutAction.FAIL : null, + acquireTimeoutMillis, + maxConnections, + connectionPoolConfiguration.getMaxPendingAcquires() + + ); + } + }; + } else { + poolMap = new AbstractChannelPoolMap() { + @Override + protected ChannelPool newPool(DefaultHttpClient.RequestKey key) { + Bootstrap newBootstrap = bootstrap.clone(group); + initBootstrapForProxy(newBootstrap, key.isSecure(), key.getHost(), key.getPort()); + newBootstrap.remoteAddress(key.getRemoteAddress()); + + AbstractChannelPoolHandler channelPoolHandler = newPoolHandler(key); + return new SimpleChannelPool( + newBootstrap, + channelPoolHandler, + channelHealthChecker + ); + } + }; + } + } else { + this.poolMap = null; + } + + Optional connectTimeout = configuration.getConnectTimeout(); + connectTimeout.ifPresent(duration -> bootstrap.option( + ChannelOption.CONNECT_TIMEOUT_MILLIS, + (int) duration.toMillis() + )); + + for (Map.Entry entry : configuration.getChannelOptions().entrySet()) { + Object v = entry.getValue(); + if (v != null) { + String channelOption = entry.getKey(); + bootstrap.option(ChannelOption.valueOf(channelOption), v); + } + } + } + + /** + * Creates the {@link NioEventLoopGroup} for this client. + * + * @param configuration The configuration + * @param threadFactory The thread factory + * @return The group + */ + private static NioEventLoopGroup createEventLoopGroup(HttpClientConfiguration configuration, ThreadFactory threadFactory) { + OptionalInt numOfThreads = configuration.getNumOfThreads(); + Optional> threadFactoryType = configuration.getThreadFactory(); + boolean hasThreads = numOfThreads.isPresent(); + boolean hasFactory = threadFactoryType.isPresent(); + NioEventLoopGroup group; + if (hasThreads && hasFactory) { + group = new NioEventLoopGroup(numOfThreads.getAsInt(), InstantiationUtils.instantiate(threadFactoryType.get())); + } else if (hasThreads) { + if (threadFactory != null) { + group = new NioEventLoopGroup(numOfThreads.getAsInt(), threadFactory); + } else { + group = new NioEventLoopGroup(numOfThreads.getAsInt()); + } + } else { + if (threadFactory != null) { + group = new NioEventLoopGroup(NettyThreadFactory.DEFAULT_EVENT_LOOP_THREADS, threadFactory); + } else { + + group = new NioEventLoopGroup(); + } + } + return group; + } + + /** + * @see DefaultHttpClient#start() + */ + public void start() { + group = createEventLoopGroup(configuration, threadFactory); + bootstrap.group(group); + } + + /** + * @see DefaultHttpClient#stop() + */ + public void shutdown() { + if (poolMap instanceof Iterable) { + Iterable> i = (Iterable) poolMap; + for (Map.Entry entry : i) { + ChannelPool cp = entry.getValue(); + try { + if (cp instanceof SimpleChannelPool) { + addInstrumentedListener(((SimpleChannelPool) cp).closeAsync(), future -> { + if (!future.isSuccess()) { + final Throwable cause = future.cause(); + if (cause != null) { + log.error("Error shutting down HTTP client connection pool: " + cause.getMessage(), cause); + } + } + }); + } else { + cp.close(); + } + } catch (Exception cause) { + log.error("Error shutting down HTTP client connection pool: " + cause.getMessage(), cause); + } + } + } + if (shutdownGroup) { + Duration shutdownTimeout = configuration.getShutdownTimeout() + .orElse(Duration.ofMillis(HttpClientConfiguration.DEFAULT_SHUTDOWN_TIMEOUT_MILLISECONDS)); + Duration shutdownQuietPeriod = configuration.getShutdownQuietPeriod() + .orElse(Duration.ofMillis(HttpClientConfiguration.DEFAULT_SHUTDOWN_QUIET_PERIOD_MILLISECONDS)); + + Future future = group.shutdownGracefully( + shutdownQuietPeriod.toMillis(), + shutdownTimeout.toMillis(), + TimeUnit.MILLISECONDS + ); + try { + future.await(shutdownTimeout.toMillis()); + } catch (InterruptedException e) { + // ignore + } + } + } + + /** + * @see DefaultHttpClient#isRunning() + * + * @return Whether this connection manager is still running and can serve requests + */ + public boolean isRunning() { + return !group.isShutdown(); + } + + /** + * Get a reactive scheduler that runs on the event loop group of this connection manager. + * + * @return A scheduler that runs on the event loop + */ + public Scheduler getEventLoopScheduler() { + return Schedulers.fromExecutor(group); + } + + /** + * Creates an initial connection to the given remote host. + * + * @param requestKey The request key to connect to + * @param isStream Is the connection a stream connection + * @param isProxy Is this a streaming proxy + * @param acceptsEvents Whether the connection will accept events + * @param contextConsumer The logic to run once the channel is configured correctly + * @return A ChannelFuture + * @throws HttpClientException If the URI is invalid + */ + private ChannelFuture doConnect( + DefaultHttpClient.RequestKey requestKey, + boolean isStream, + boolean isProxy, + boolean acceptsEvents, + Consumer contextConsumer) throws HttpClientException { + + SslContext sslCtx = buildSslContext(requestKey); + String host = requestKey.getHost(); + int port = requestKey.getPort(); + Bootstrap localBootstrap = bootstrap.clone(); + initBootstrapForProxy(localBootstrap, sslCtx != null, host, port); + localBootstrap.handler(new HttpClientInitializer( + sslCtx, + host, + port, + isStream, + isProxy, + acceptsEvents, + contextConsumer) + ); + return localBootstrap.connect(host, port); + } + + private void initBootstrapForProxy(Bootstrap localBootstrap, boolean sslCtx, String host, int port) { + Proxy proxy = configuration.resolveProxy(sslCtx, host, port); + if (proxy.type() != Proxy.Type.DIRECT) { + localBootstrap.resolver(NoopAddressResolverGroup.INSTANCE); + } + } + + /** + * Builds an {@link SslContext} for the given URI if necessary. + * + * @return The {@link SslContext} instance + */ + private SslContext buildSslContext(DefaultHttpClient.RequestKey requestKey) { + final SslContext sslCtx; + if (requestKey.isSecure()) { + sslCtx = sslContext; + //Allow https requests to be sent if SSL is disabled but a proxy is present + if (sslCtx == null && !configuration.getProxyAddress().isPresent()) { + throw customizeException(new HttpClientException("Cannot send HTTPS request. SSL is disabled")); + } + } else { + sslCtx = null; + } + return sslCtx; + } + + private PoolHandle mockPoolHandle(Channel channel) { + return new PoolHandle(null, channel); + } + + /** + * Get a connection for exchange-like (non-streaming) http client methods. + * + * @param requestKey The remote to connect to + * @param multipart Whether the request should be multipart + * @param acceptEvents Whether the response may be an event stream + * @return A mono that will complete once the channel is ready for transmission + */ + Mono connectForExchange(DefaultHttpClient.RequestKey requestKey, boolean multipart, boolean acceptEvents) { + return Mono.create(emitter -> { + if (poolMap != null && !multipart) { + try { + ChannelPool channelPool = poolMap.get(requestKey); + addInstrumentedListener(channelPool.acquire(), future -> { + if (future.isSuccess()) { + Channel channel = future.get(); + PoolHandle poolHandle = new PoolHandle(channelPool, channel); + Future initFuture = channel.attr(STREAM_CHANNEL_INITIALIZED).get(); + if (initFuture == null) { + emitter.success(poolHandle); + } else { + // we should wait until the handshake completes + addInstrumentedListener(initFuture, f -> { + emitter.success(poolHandle); + }); + } + } else { + Throwable cause = future.cause(); + emitter.error(customizeException(new HttpClientException("Connect Error: " + cause.getMessage(), cause))); + } + }); + } catch (HttpClientException e) { + emitter.error(e); + } + } else { + ChannelFuture connectionFuture = doConnect(requestKey, false, false, acceptEvents, null); + addInstrumentedListener(connectionFuture, future -> { + if (!future.isSuccess()) { + Throwable cause = future.cause(); + emitter.error(customizeException(new HttpClientException("Connect Error: " + cause.getMessage(), cause))); + } else { + emitter.success(mockPoolHandle(connectionFuture.channel())); + } + }); + } + }) + .delayUntil(this::delayUntilHttp2Ready) + .map(poolHandle -> { + addReadTimeoutHandler(poolHandle.channel.pipeline()); + return poolHandle; + }); + } + + private Publisher delayUntilHttp2Ready(PoolHandle poolHandle) { + Http2SettingsHandler settingsHandler = (Http2SettingsHandler) poolHandle.channel.pipeline().get(ChannelPipelineCustomizer.HANDLER_HTTP2_SETTINGS); + if (settingsHandler == null) { + return Flux.empty(); + } + Sinks.Empty empty = Sinks.empty(); + addInstrumentedListener(settingsHandler.promise, future -> { + if (future.isSuccess()) { + empty.tryEmitEmpty(); + } else { + poolHandle.taint(); + poolHandle.release(); + empty.tryEmitError(future.cause()); + } + }); + return empty.asMono(); + } + + /** + * Get a connection for streaming http client methods. + * + * @param requestKey The remote to connect to + * @param isProxy Whether the request is for a {@link io.micronaut.http.client.ProxyHttpClient} call + * @param acceptEvents Whether the response may be an event stream + * @return A mono that will complete once the channel is ready for transmission + */ + Mono connectForStream(DefaultHttpClient.RequestKey requestKey, boolean isProxy, boolean acceptEvents) { + return Mono.create(emitter -> { + ChannelFuture channelFuture; + try { + if (httpVersion == HttpVersion.HTTP_2_0) { + + channelFuture = doConnect(requestKey, true, isProxy, acceptEvents, channelHandlerContext -> { + try { + final Channel channel = channelHandlerContext.channel(); + emitter.success(mockPoolHandle(channel)); + } catch (Exception e) { + emitter.error(e); + } + }); + } else { + channelFuture = doConnect(requestKey, true, isProxy, acceptEvents, null); + addInstrumentedListener(channelFuture, + (ChannelFutureListener) f -> { + if (f.isSuccess()) { + Channel channel = f.channel(); + emitter.success(mockPoolHandle(channel)); + } else { + Throwable cause = f.cause(); + emitter.error(customizeException(new HttpClientException("Connect error:" + cause.getMessage(), cause))); + } + }); + } + } catch (HttpClientException e) { + emitter.error(e); + return; + } + + // todo: on emitter dispose/cancel, close channel + }) + .delayUntil(this::delayUntilHttp2Ready) + .map(poolHandle -> { + addReadTimeoutHandler(poolHandle.channel.pipeline()); + return poolHandle; + }); + } + + /** + * Connect to a remote websocket. The given {@link ChannelHandler} is added to the pipeline + * when the handshakes complete. + * + * @param requestKey The remote to connect to + * @param handler The websocket message handler + * @return A mono that will complete when the handshakes complete + */ + Mono connectForWebsocket(DefaultHttpClient.RequestKey requestKey, ChannelHandler handler) { + Sinks.Empty initial = Sinks.empty(); + + Bootstrap bootstrap = this.bootstrap.clone(); + SslContext sslContext = buildSslContext(requestKey); + + bootstrap.remoteAddress(requestKey.getHost(), requestKey.getPort()); + initBootstrapForProxy(bootstrap, sslContext != null, requestKey.getHost(), requestKey.getPort()); + bootstrap.handler(new HttpClientInitializer( + sslContext, + requestKey.getHost(), + requestKey.getPort(), + false, + false, + false, + null + ) { + @Override + protected void addFinalHandler(ChannelPipeline pipeline) { + pipeline.remove(ChannelPipelineCustomizer.HANDLER_HTTP_DECODER); + ReadTimeoutHandler readTimeoutHandler = pipeline.get(ReadTimeoutHandler.class); + if (readTimeoutHandler != null) { + pipeline.remove(readTimeoutHandler); + } + + Optional readIdleTime = configuration.getReadIdleTimeout(); + if (readIdleTime.isPresent()) { + Duration duration = readIdleTime.get(); + if (!duration.isNegative()) { + pipeline.addLast(ChannelPipelineCustomizer.HANDLER_IDLE_STATE, new IdleStateHandler(duration.toMillis(), duration.toMillis(), duration.toMillis(), TimeUnit.MILLISECONDS)); + } + } + + try { + pipeline.addLast(WebSocketClientCompressionHandler.INSTANCE); + pipeline.addLast(ChannelPipelineCustomizer.HANDLER_MICRONAUT_WEBSOCKET_CLIENT, handler); + initial.tryEmitEmpty(); + } catch (Throwable e) { + initial.tryEmitError(new WebSocketSessionException("Error opening WebSocket client session: " + e.getMessage(), e)); + } + } + }); + + addInstrumentedListener(bootstrap.connect(), future -> { + if (!future.isSuccess()) { + initial.tryEmitError(future.cause()); + } + }); + + return initial.asMono(); + } + + private AbstractChannelPoolHandler newPoolHandler(DefaultHttpClient.RequestKey key) { + return new AbstractChannelPoolHandler() { + @Override + public void channelCreated(Channel ch) { + Promise streamPipelineBuilt = ch.newPromise(); + ch.attr(STREAM_CHANNEL_INITIALIZED).set(streamPipelineBuilt); + + // make sure the future completes eventually + ChannelHandler failureHandler = new ChannelInboundHandlerAdapter() { + @Override + public void handlerRemoved(ChannelHandlerContext ctx) { + streamPipelineBuilt.trySuccess(null); + } + + @Override + public void channelInactive(ChannelHandlerContext ctx) { + streamPipelineBuilt.trySuccess(null); + ctx.fireChannelInactive(); + } + }; + ch.pipeline().addLast(failureHandler); + + ch.pipeline().addLast(ChannelPipelineCustomizer.HANDLER_HTTP_CLIENT_INIT, new HttpClientInitializer( + key.isSecure() ? sslContext : null, + key.getHost(), + key.getPort(), + false, + false, + false, + null + ) { + @Override + protected void addFinalHandler(ChannelPipeline pipeline) { + // no-op, don't add the stream handler which is not supported + // in the connection pooled scenario + } + + @Override + void onStreamPipelineBuilt() { + super.onStreamPipelineBuilt(); + streamPipelineBuilt.trySuccess(null); + ch.pipeline().remove(failureHandler); + ch.attr(STREAM_CHANNEL_INITIALIZED).set(null); + } + }); + + if (connectionTimeAliveMillis != null) { + ch.pipeline() + .addLast( + ChannelPipelineCustomizer.HANDLER_CONNECT_TTL, + new ConnectTTLHandler(connectionTimeAliveMillis) + ); + } + } + + @Override + public void channelReleased(Channel ch) { + Duration idleTimeout = configuration.getConnectionPoolIdleTimeout().orElse(Duration.ofNanos(0)); + ChannelPipeline pipeline = ch.pipeline(); + if (ch.isOpen()) { + ch.config().setAutoRead(true); + pipeline.addLast(IdlingConnectionHandler.INSTANCE); + if (idleTimeout.toNanos() > 0) { + pipeline.addLast(ChannelPipelineCustomizer.HANDLER_IDLE_STATE, new IdleStateHandler(idleTimeout.toNanos(), idleTimeout.toNanos(), 0, TimeUnit.NANOSECONDS)); + pipeline.addLast(IdleTimeoutHandler.INSTANCE); + } + } + + if (ConnectTTLHandler.isChannelExpired(ch) && ch.isOpen() && !ch.eventLoop().isShuttingDown()) { + ch.close(); + } + + removeReadTimeoutHandler(pipeline); + } + + @Override + public void channelAcquired(Channel ch) throws Exception { + ChannelPipeline pipeline = ch.pipeline(); + if (pipeline.context(IdlingConnectionHandler.INSTANCE) != null) { + pipeline.remove(IdlingConnectionHandler.INSTANCE); + } + if (pipeline.context(ChannelPipelineCustomizer.HANDLER_IDLE_STATE) != null) { + pipeline.remove(ChannelPipelineCustomizer.HANDLER_IDLE_STATE); + } + if (pipeline.context(IdleTimeoutHandler.INSTANCE) != null) { + pipeline.remove(IdleTimeoutHandler.INSTANCE); + } + } + }; + } + + /** + * Configures HTTP/2 for the channel when SSL is enabled. + * + * @param httpClientInitializer The client initializer + * @param ch The channel + * @param sslCtx The SSL context + * @param host The host + * @param port The port + * @param connectionHandler The connection handler + */ + private void configureHttp2Ssl( + HttpClientInitializer httpClientInitializer, + @NonNull SocketChannel ch, + @NonNull SslContext sslCtx, + String host, + int port, + HttpToHttp2ConnectionHandler connectionHandler) { + ChannelPipeline pipeline = ch.pipeline(); + // Specify Host in SSLContext New Handler to add TLS SNI Extension + pipeline.addLast(ChannelPipelineCustomizer.HANDLER_SSL, sslCtx.newHandler(ch.alloc(), host, port)); + // We must wait for the handshake to finish and the protocol to be negotiated before configuring + // the HTTP/2 components of the pipeline. + pipeline.addLast( + ChannelPipelineCustomizer.HANDLER_HTTP2_PROTOCOL_NEGOTIATOR, + new ApplicationProtocolNegotiationHandler(ApplicationProtocolNames.HTTP_2) { + + @Override + public void handlerRemoved(ChannelHandlerContext ctx) { + // the logic to send the request should only be executed once the HTTP/2 + // Connection Preface request has been sent. Once the Preface has been sent and + // removed then this handler is removed so we invoke the remaining logic once + // this handler removed + final Consumer contextConsumer = + httpClientInitializer.contextConsumer; + if (contextConsumer != null) { + contextConsumer.accept(ctx); + } + } + + @Override + protected void configurePipeline(ChannelHandlerContext ctx, String protocol) { + if (ApplicationProtocolNames.HTTP_2.equals(protocol)) { + ChannelPipeline p = ctx.pipeline(); + if (httpClientInitializer.stream) { + // stream consumer manages backpressure and reads + ctx.channel().config().setAutoRead(false); + } + p.addLast( + ChannelPipelineCustomizer.HANDLER_HTTP2_SETTINGS, + new Http2SettingsHandler(ch.newPromise()) + ); + httpClientInitializer.addEventStreamHandlerIfNecessary(p); + httpClientInitializer.addFinalHandler(p); + for (ChannelPipelineListener pipelineListener : pipelineListeners) { + pipelineListener.onConnect(p); + } + } else if (ApplicationProtocolNames.HTTP_1_1.equals(protocol)) { + ChannelPipeline p = ctx.pipeline(); + httpClientInitializer.addHttp1Handlers(p); + } else { + ctx.close(); + throw customizeException(new HttpClientException("Unknown Protocol: " + protocol)); + } + httpClientInitializer.onStreamPipelineBuilt(); + } + }); + + pipeline.addLast(ChannelPipelineCustomizer.HANDLER_HTTP2_CONNECTION, connectionHandler); + } + + /** + * Configures HTTP/2 handling for plaintext (non-SSL) connections. + * + * @param httpClientInitializer The client initializer + * @param ch The channel + * @param connectionHandler The connection handler + */ + private void configureHttp2ClearText( + HttpClientInitializer httpClientInitializer, + @NonNull SocketChannel ch, + @NonNull HttpToHttp2ConnectionHandler connectionHandler) { + HttpClientCodec sourceCodec = new HttpClientCodec(); + Http2ClientUpgradeCodec upgradeCodec = new Http2ClientUpgradeCodec(ChannelPipelineCustomizer.HANDLER_HTTP2_CONNECTION, connectionHandler); + HttpClientUpgradeHandler upgradeHandler = new HttpClientUpgradeHandler(sourceCodec, upgradeCodec, 65536); + + final ChannelPipeline pipeline = ch.pipeline(); + pipeline.addLast(ChannelPipelineCustomizer.HANDLER_HTTP_CLIENT_CODEC, sourceCodec); + httpClientInitializer.settingsHandler = new Http2SettingsHandler(ch.newPromise()); + pipeline.addLast(upgradeHandler); + pipeline.addLast(new ChannelInboundHandlerAdapter() { + @Override + public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception { + ctx.fireUserEventTriggered(evt); + if (evt instanceof HttpClientUpgradeHandler.UpgradeEvent) { + httpClientInitializer.onStreamPipelineBuilt(); + ctx.pipeline().remove(this); + } + } + }); + pipeline.addLast(ChannelPipelineCustomizer.HANDLER_HTTP2_UPGRADE_REQUEST, new H2cUpgradeRequestHandler(httpClientInitializer)); + } + + /** + * Creates a new {@link HttpToHttp2ConnectionHandlerBuilder} for the given HTTP/2 connection object and config. + * + * @param connection The connection + * @param configuration The configuration + * @param stream Whether this is a stream request + * @return The {@link HttpToHttp2ConnectionHandlerBuilder} + */ + @NonNull + private static HttpToHttp2ConnectionHandlerBuilder newHttp2ConnectionHandlerBuilder( + @NonNull Http2Connection connection, @NonNull HttpClientConfiguration configuration, boolean stream) { + final HttpToHttp2ConnectionHandlerBuilder builder = new HttpToHttp2ConnectionHandlerBuilder(); + builder.validateHeaders(true); + final Http2FrameListener http2ToHttpAdapter; + + if (!stream) { + http2ToHttpAdapter = new InboundHttp2ToHttpAdapterBuilder(connection) + .maxContentLength(configuration.getMaxContentLength()) + .validateHttpHeaders(true) + .propagateSettings(true) + .build(); + + } else { + http2ToHttpAdapter = new StreamingInboundHttp2ToHttpAdapter( + connection, + configuration.getMaxContentLength() + ); + } + return builder + .connection(connection) + .frameListener(new DelegatingDecompressorFrameListener( + connection, + http2ToHttpAdapter)); + + } + + private void configureProxy(ChannelPipeline pipeline, boolean secure, String host, int port) { + Proxy proxy = configuration.resolveProxy(secure, host, port); + if (Proxy.NO_PROXY.equals(proxy)) { + return; + } + Proxy.Type proxyType = proxy.type(); + SocketAddress proxyAddress = proxy.address(); + String username = configuration.getProxyUsername().orElse(null); + String password = configuration.getProxyPassword().orElse(null); + + if (proxyAddress instanceof InetSocketAddress) { + InetSocketAddress isa = (InetSocketAddress) proxyAddress; + if (isa.isUnresolved()) { + proxyAddress = new InetSocketAddress(isa.getHostString(), isa.getPort()); + } + } + + if (StringUtils.isNotEmpty(username) && StringUtils.isNotEmpty(password)) { + switch (proxyType) { + case HTTP: + pipeline.addLast(ChannelPipelineCustomizer.HANDLER_HTTP_PROXY, new HttpProxyHandler(proxyAddress, username, password)); + break; + case SOCKS: + pipeline.addLast(ChannelPipelineCustomizer.HANDLER_SOCKS_5_PROXY, new Socks5ProxyHandler(proxyAddress, username, password)); + break; + default: + // no-op + } + } else { + switch (proxyType) { + case HTTP: + pipeline.addLast(ChannelPipelineCustomizer.HANDLER_HTTP_PROXY, new HttpProxyHandler(proxyAddress)); + break; + case SOCKS: + pipeline.addLast(ChannelPipelineCustomizer.HANDLER_SOCKS_5_PROXY, new Socks5ProxyHandler(proxyAddress)); + break; + default: + // no-op + } + } + } + + > Future addInstrumentedListener( + Future channelFuture, GenericFutureListener listener) { + return channelFuture.addListener(f -> { + try (Instrumentation ignored = instrumenter.newInstrumentation()) { + listener.operationComplete((C) f); + } + }); + } + + private E customizeException(E exc) { + DefaultHttpClient.customizeException0(configuration, informationalServiceId, exc); + return exc; + } + + private void addReadTimeoutHandler(ChannelPipeline pipeline) { + if (readTimeoutMillis != null) { + if (httpVersion == HttpVersion.HTTP_2_0) { + pipeline.addBefore( + ChannelPipelineCustomizer.HANDLER_HTTP2_CONNECTION, + ChannelPipelineCustomizer.HANDLER_READ_TIMEOUT, + new ReadTimeoutHandler(readTimeoutMillis, TimeUnit.MILLISECONDS) + ); + } else { + pipeline.addBefore( + ChannelPipelineCustomizer.HANDLER_HTTP_CLIENT_CODEC, + ChannelPipelineCustomizer.HANDLER_READ_TIMEOUT, + new ReadTimeoutHandler(readTimeoutMillis, TimeUnit.MILLISECONDS)); + } + } + } + + private void removeReadTimeoutHandler(ChannelPipeline pipeline) { + if (readTimeoutMillis != null && pipeline.context(ChannelPipelineCustomizer.HANDLER_READ_TIMEOUT) != null) { + pipeline.remove(ChannelPipelineCustomizer.HANDLER_READ_TIMEOUT); + } + } + + /** + * A handler that triggers the cleartext upgrade to HTTP/2 by sending an initial HTTP request. + */ + private class H2cUpgradeRequestHandler extends ChannelInboundHandlerAdapter { + + private final HttpClientInitializer initializer; + + /** + * Default constructor. + * + * @param initializer The initializer + */ + public H2cUpgradeRequestHandler(HttpClientInitializer initializer) { + this.initializer = initializer; + } + + @Override + public void channelActive(ChannelHandlerContext ctx) { + // Done with this handler, remove it from the pipeline. + final ChannelPipeline pipeline = ctx.pipeline(); + + pipeline.addLast(ChannelPipelineCustomizer.HANDLER_HTTP2_SETTINGS, initializer.settingsHandler); + DefaultFullHttpRequest upgradeRequest = + new DefaultFullHttpRequest(io.netty.handler.codec.http.HttpVersion.HTTP_1_1, HttpMethod.GET, "/", Unpooled.EMPTY_BUFFER); + + // Set HOST header as the remote peer may require it. + InetSocketAddress remote = (InetSocketAddress) ctx.channel().remoteAddress(); + String hostString = remote.getHostString(); + if (hostString == null) { + hostString = remote.getAddress().getHostAddress(); + } + upgradeRequest.headers().set(HttpHeaderNames.HOST, hostString + ':' + remote.getPort()); + ctx.writeAndFlush(upgradeRequest); + + ctx.fireChannelActive(); + if (initializer.contextConsumer != null) { + initializer.contextConsumer.accept(ctx); + } + initializer.addFinalHandler(pipeline); + } + + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { + if (msg instanceof HttpMessage) { + int streamId = ((HttpMessage) msg).headers().getInt(HttpConversionUtil.ExtensionHeaderNames.STREAM_ID.text(), -1); + if (streamId == 1) { + // ignore this message + if (log.isDebugEnabled()) { + log.debug("Received response on HTTP2 stream 1, the stream used to respond to the initial upgrade request. Ignoring."); + } + ReferenceCountUtil.release(msg); + if (msg instanceof LastHttpContent) { + ctx.pipeline().remove(this); + } + return; + } + } + + super.channelRead(ctx, msg); + } + } + + /** + * Reads the first {@link Http2Settings} object and notifies a {@link io.netty.channel.ChannelPromise}. + */ + private class Http2SettingsHandler extends + SimpleChannelInboundHandlerInstrumented { + final ChannelPromise promise; + + /** + * Create new instance. + * + * @param promise Promise object used to notify when first settings are received + */ + Http2SettingsHandler(ChannelPromise promise) { + super(instrumenter); + this.promise = promise; + } + + @Override + protected void channelReadInstrumented(ChannelHandlerContext ctx, Http2Settings msg) { + promise.setSuccess(); + + // Only care about the first settings message + ctx.pipeline().remove(this); + } + + @Override + public void channelInactive(ChannelHandlerContext ctx) throws Exception { + super.channelInactive(ctx); + if (!promise.isDone()) { + promise.tryFailure(new HttpClientException("Channel became inactive before settings frame was received")); + } + } + + @Override + public void handlerRemoved(ChannelHandlerContext ctx) throws Exception { + super.handlerRemoved(ctx); + if (!promise.isDone()) { + promise.tryFailure(new HttpClientException("Handler was removed before settings frame was received")); + } + } + } + + /** + * Initializes the HTTP client channel. + */ + private class HttpClientInitializer extends ChannelInitializer { + + final SslContext sslContext; + final String host; + final int port; + final boolean stream; + final boolean proxy; + final boolean acceptsEvents; + Http2SettingsHandler settingsHandler; + final Consumer contextConsumer; + private NettyClientCustomizer channelCustomizer; + + /** + * @param sslContext The ssl context + * @param host The host + * @param port The port + * @param stream Whether is stream + * @param proxy Is this a streaming proxy + * @param acceptsEvents Whether an event stream is accepted + * @param contextConsumer The context consumer + */ + protected HttpClientInitializer(SslContext sslContext, + String host, + int port, + boolean stream, + boolean proxy, + boolean acceptsEvents, + Consumer contextConsumer) { + this.sslContext = sslContext; + this.stream = stream; + this.host = host; + this.port = port; + this.proxy = proxy; + this.acceptsEvents = acceptsEvents; + this.contextConsumer = contextConsumer; + } + + /** + * @param ch The channel + */ + @Override + protected void initChannel(SocketChannel ch) { + channelCustomizer = clientCustomizer.specializeForChannel(ch, NettyClientCustomizer.ChannelRole.CONNECTION); + ch.attr(CHANNEL_CUSTOMIZER_KEY).set(channelCustomizer); + + ChannelPipeline p = ch.pipeline(); + + configureProxy(p, sslContext != null, host, port); + + if (httpVersion == HttpVersion.HTTP_2_0) { + final Http2Connection connection = new DefaultHttp2Connection(false); + final HttpToHttp2ConnectionHandlerBuilder builder = + newHttp2ConnectionHandlerBuilder(connection, configuration, stream); + + configuration.getLogLevel().ifPresent(logLevel -> { + try { + final io.netty.handler.logging.LogLevel nettyLevel = io.netty.handler.logging.LogLevel.valueOf( + logLevel.name() + ); + builder.frameLogger(new Http2FrameLogger(nettyLevel, DefaultHttpClient.class)); + } catch (IllegalArgumentException e) { + throw customizeException(new HttpClientException("Unsupported log level: " + logLevel)); + } + }); + HttpToHttp2ConnectionHandler connectionHandler = builder + .build(); + if (sslContext != null) { + configureHttp2Ssl(this, ch, sslContext, host, port, connectionHandler); + } else { + configureHttp2ClearText(this, ch, connectionHandler); + } + channelCustomizer.onInitialPipelineBuilt(); + } else { + if (stream) { + // for streaming responses we disable auto read + // so that the consumer is in charge of back pressure + ch.config().setAutoRead(false); + } + + configuration.getLogLevel().ifPresent(logLevel -> { + try { + final io.netty.handler.logging.LogLevel nettyLevel = io.netty.handler.logging.LogLevel.valueOf( + logLevel.name() + ); + p.addLast(new LoggingHandler(DefaultHttpClient.class, nettyLevel)); + } catch (IllegalArgumentException e) { + throw customizeException(new HttpClientException("Unsupported log level: " + logLevel)); + } + }); + + if (sslContext != null) { + SslHandler sslHandler = sslContext.newHandler(ch.alloc(), host, port); + sslHandler.setHandshakeTimeoutMillis(configuration.getSslConfiguration().getHandshakeTimeout().toMillis()); + p.addLast(ChannelPipelineCustomizer.HANDLER_SSL, sslHandler); + } + + // Pool connections require alternative timeout handling + if (poolMap == null) { + // read timeout settings are not applied to streamed requests. + // instead idle timeout settings are applied. + if (stream) { + Optional readIdleTime = configuration.getReadIdleTimeout(); + if (readIdleTime.isPresent()) { + Duration duration = readIdleTime.get(); + if (!duration.isNegative()) { + p.addLast(ChannelPipelineCustomizer.HANDLER_IDLE_STATE, new IdleStateHandler( + duration.toMillis(), + duration.toMillis(), + duration.toMillis(), + TimeUnit.MILLISECONDS + )); + } + } + } + } + + addHttp1Handlers(p); + channelCustomizer.onInitialPipelineBuilt(); + onStreamPipelineBuilt(); + } + } + + /** + * Called when the stream pipeline is fully set up (all handshakes completed) and we can + * start processing requests. + */ + void onStreamPipelineBuilt() { + channelCustomizer.onStreamPipelineBuilt(); + } + + void addHttp1Handlers(ChannelPipeline p) { + p.addLast(ChannelPipelineCustomizer.HANDLER_HTTP_CLIENT_CODEC, new HttpClientCodec()); + + p.addLast(ChannelPipelineCustomizer.HANDLER_HTTP_DECODER, new HttpContentDecompressor()); + + int maxContentLength = configuration.getMaxContentLength(); + + if (!stream) { + p.addLast(ChannelPipelineCustomizer.HANDLER_HTTP_AGGREGATOR, new HttpObjectAggregator(maxContentLength) { + @Override + protected void finishAggregation(FullHttpMessage aggregated) throws Exception { + if (!HttpUtil.isContentLengthSet(aggregated)) { + if (aggregated.content().readableBytes() > 0) { + super.finishAggregation(aggregated); + } + } + } + }); + } + addEventStreamHandlerIfNecessary(p); + addFinalHandler(p); + for (ChannelPipelineListener pipelineListener : pipelineListeners) { + pipelineListener.onConnect(p); + } + } + + void addEventStreamHandlerIfNecessary(ChannelPipeline p) { + // if the content type is a SSE event stream we add a decoder + // to delimit the content by lines (unless we are proxying the stream) + if (acceptsEventStream() && !proxy) { + p.addLast(ChannelPipelineCustomizer.HANDLER_MICRONAUT_SSE_EVENT_STREAM, new LineBasedFrameDecoder(configuration.getMaxContentLength(), true, true) { + + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { + if (msg instanceof HttpContent) { + if (msg instanceof LastHttpContent) { + super.channelRead(ctx, msg); + } else { + Attribute streamKey = ctx.channel().attr(STREAM_KEY); + if (msg instanceof Http2Content) { + streamKey.set(((Http2Content) msg).stream()); + } + try { + super.channelRead(ctx, ((HttpContent) msg).content()); + } finally { + streamKey.set(null); + } + } + } else { + super.channelRead(ctx, msg); + } + } + }); + + p.addLast(ChannelPipelineCustomizer.HANDLER_MICRONAUT_SSE_CONTENT, new SimpleChannelInboundHandlerInstrumented(instrumenter, false) { + + @Override + public boolean acceptInboundMessage(Object msg) { + return msg instanceof ByteBuf; + } + + @Override + protected void channelReadInstrumented(ChannelHandlerContext ctx, ByteBuf msg) { + try { + Attribute streamKey = ctx.channel().attr(STREAM_KEY); + Http2Stream http2Stream = streamKey.get(); + if (http2Stream != null) { + ctx.fireChannelRead(new DefaultHttp2Content(msg.copy(), http2Stream)); + } else { + ctx.fireChannelRead(new DefaultHttpContent(msg.copy())); + } + } finally { + msg.release(); + } + } + }); + + } + } + + /** + * Allows overriding the final handler added to the pipeline. + * + * @param pipeline The pipeline + */ + protected void addFinalHandler(ChannelPipeline pipeline) { + pipeline.addLast( + ChannelPipelineCustomizer.HANDLER_HTTP_STREAM, + new HttpStreamsClientHandler() { + @Override + public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception { + if (evt instanceof IdleStateEvent) { + // close the connection if it is idle for too long + ctx.close(); + } + super.userEventTriggered(ctx, evt); + } + }); + } + + private boolean acceptsEventStream() { + return this.acceptsEvents; + } + } + + final class PoolHandle { + final Channel channel; + private final ChannelPool channelPool; + private boolean canReturn; + + private PoolHandle(ChannelPool channelPool, Channel channel) { + this.channel = channel; + this.channelPool = channelPool; + this.canReturn = channelPool != null; + } + + /** + * Prevent this connection from being reused. + */ + void taint() { + canReturn = false; + } + + /** + * Close this connection or release it back to the pool. + */ + void release() { + if (channelPool != null) { + removeReadTimeoutHandler(channel.pipeline()); + if (!canReturn) { + channel.closeFuture().addListener((future -> + channelPool.release(channel) + )); + } else { + channelPool.release(channel); + } + } else { + // just close it to prevent any future reads without a handler registered + channel.close(); + } + } + + /** + * Whether this connection may be returned to a connection pool (i.e. should be kept + * keepalive). + * + * @return Whether this connection may be reused + */ + public boolean canReturn() { + return canReturn; + } + + /** + * Notify any {@link NettyClientCustomizer} that the request pipeline has been built. + */ + void notifyRequestPipelineBuilt() { + channel.attr(CHANNEL_CUSTOMIZER_KEY).get().onRequestPipelineBuilt(); + } + } +} diff --git a/http-client/src/main/java/io/micronaut/http/client/netty/DefaultHttpClient.java b/http-client/src/main/java/io/micronaut/http/client/netty/DefaultHttpClient.java index 495e6e3c608..8bf5d2456f7 100644 --- a/http-client/src/main/java/io/micronaut/http/client/netty/DefaultHttpClient.java +++ b/http-client/src/main/java/io/micronaut/http/client/netty/DefaultHttpClient.java @@ -29,7 +29,6 @@ import io.micronaut.core.io.buffer.ByteBufferFactory; import io.micronaut.core.io.buffer.ReferenceCounted; import io.micronaut.core.order.OrderUtil; -import io.micronaut.core.reflect.InstantiationUtils; import io.micronaut.core.type.Argument; import io.micronaut.core.util.ArgumentUtils; import io.micronaut.core.util.ArrayUtils; @@ -82,26 +81,20 @@ import io.micronaut.http.netty.NettyHttpResponseBuilder; import io.micronaut.http.netty.channel.ChannelPipelineCustomizer; import io.micronaut.http.netty.channel.ChannelPipelineListener; -import io.micronaut.http.netty.channel.NettyThreadFactory; -import io.micronaut.http.netty.stream.DefaultHttp2Content; import io.micronaut.http.netty.stream.DefaultStreamedHttpResponse; -import io.micronaut.http.netty.stream.Http2Content; -import io.micronaut.http.netty.stream.HttpStreamsClientHandler; import io.micronaut.http.netty.stream.JsonSubscriber; import io.micronaut.http.netty.stream.StreamedHttpRequest; import io.micronaut.http.netty.stream.StreamedHttpResponse; -import io.micronaut.http.netty.stream.StreamingInboundHttp2ToHttpAdapter; import io.micronaut.http.sse.Event; import io.micronaut.http.uri.UriBuilder; import io.micronaut.http.uri.UriTemplate; import io.micronaut.jackson.databind.JacksonDatabindMapper; import io.micronaut.json.JsonMapper; -import io.micronaut.json.codec.MapperMediaTypeCodec; import io.micronaut.json.codec.JsonMediaTypeCodec; import io.micronaut.json.codec.JsonStreamMediaTypeCodec; +import io.micronaut.json.codec.MapperMediaTypeCodec; import io.micronaut.json.tree.JsonNode; import io.micronaut.runtime.ApplicationConfiguration; -import io.micronaut.scheduling.instrument.Instrumentation; import io.micronaut.scheduling.instrument.InvocationInstrumenter; import io.micronaut.scheduling.instrument.InvocationInstrumenterFactory; import io.micronaut.websocket.WebSocketClient; @@ -109,28 +102,36 @@ import io.micronaut.websocket.annotation.OnMessage; import io.micronaut.websocket.context.WebSocketBean; import io.micronaut.websocket.context.WebSocketBeanRegistry; -import io.micronaut.websocket.exceptions.WebSocketSessionException; -import io.netty.bootstrap.Bootstrap; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; import io.netty.buffer.ByteBufUtil; import io.netty.buffer.CompositeByteBuf; import io.netty.buffer.EmptyByteBuf; import io.netty.buffer.Unpooled; -import io.netty.channel.*; -import io.netty.channel.nio.NioEventLoopGroup; -import io.netty.channel.pool.AbstractChannelPoolHandler; -import io.netty.channel.pool.AbstractChannelPoolMap; -import io.netty.channel.pool.ChannelHealthChecker; -import io.netty.channel.pool.ChannelPool; -import io.netty.channel.pool.ChannelPoolMap; -import io.netty.channel.pool.FixedChannelPool; -import io.netty.channel.pool.SimpleChannelPool; -import io.netty.channel.socket.SocketChannel; +import io.netty.channel.Channel; +import io.netty.channel.ChannelFactory; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelPipeline; +import io.netty.channel.EventLoopGroup; +import io.netty.channel.MultithreadEventLoopGroup; import io.netty.channel.socket.nio.NioSocketChannel; -import io.netty.handler.codec.LineBasedFrameDecoder; import io.netty.handler.codec.TooLongFrameException; -import io.netty.handler.codec.http.*; +import io.netty.handler.codec.http.DefaultFullHttpResponse; +import io.netty.handler.codec.http.DefaultHttpContent; +import io.netty.handler.codec.http.DefaultHttpHeaders; +import io.netty.handler.codec.http.DefaultLastHttpContent; +import io.netty.handler.codec.http.EmptyHttpHeaders; +import io.netty.handler.codec.http.FullHttpRequest; +import io.netty.handler.codec.http.FullHttpResponse; +import io.netty.handler.codec.http.HttpContent; +import io.netty.handler.codec.http.HttpHeaderNames; +import io.netty.handler.codec.http.HttpHeaderValues; +import io.netty.handler.codec.http.HttpHeaders; +import io.netty.handler.codec.http.HttpRequest; +import io.netty.handler.codec.http.HttpResponseStatus; +import io.netty.handler.codec.http.HttpScheme; +import io.netty.handler.codec.http.HttpUtil; import io.netty.handler.codec.http.multipart.DefaultHttpDataFactory; import io.netty.handler.codec.http.multipart.FileUpload; import io.netty.handler.codec.http.multipart.HttpDataFactory; @@ -138,27 +139,11 @@ import io.netty.handler.codec.http.multipart.InterfaceHttpData; import io.netty.handler.codec.http.websocketx.WebSocketClientHandshakerFactory; import io.netty.handler.codec.http.websocketx.WebSocketVersion; -import io.netty.handler.codec.http.websocketx.extensions.compression.WebSocketClientCompressionHandler; -import io.netty.handler.codec.http2.*; -import io.netty.handler.logging.LoggingHandler; -import io.netty.handler.proxy.HttpProxyHandler; -import io.netty.handler.proxy.Socks5ProxyHandler; -import io.netty.handler.ssl.ApplicationProtocolNames; -import io.netty.handler.ssl.ApplicationProtocolNegotiationHandler; -import io.netty.handler.ssl.SslContext; -import io.netty.handler.ssl.SslHandler; import io.netty.handler.stream.ChunkedWriteHandler; -import io.netty.handler.timeout.IdleStateEvent; -import io.netty.handler.timeout.IdleStateHandler; -import io.netty.handler.timeout.ReadTimeoutHandler; -import io.netty.resolver.NoopAddressResolverGroup; -import io.netty.util.Attribute; import io.netty.util.AttributeKey; import io.netty.util.CharsetUtil; import io.netty.util.ReferenceCountUtil; import io.netty.util.concurrent.DefaultThreadFactory; -import io.netty.util.concurrent.Future; -import io.netty.util.concurrent.GenericFutureListener; import io.netty.util.concurrent.Promise; import org.reactivestreams.Processor; import org.reactivestreams.Publisher; @@ -170,8 +155,6 @@ import reactor.core.publisher.Flux; import reactor.core.publisher.FluxSink; import reactor.core.publisher.Mono; -import reactor.core.scheduler.Scheduler; -import reactor.core.scheduler.Schedulers; import java.io.Closeable; import java.io.File; @@ -179,19 +162,20 @@ import java.io.InputStream; import java.net.InetSocketAddress; import java.net.MalformedURLException; -import java.net.Proxy; -import java.net.Proxy.Type; -import java.net.SocketAddress; import java.net.URI; import java.net.URISyntaxException; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.time.Duration; -import java.util.*; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; import java.util.concurrent.ThreadFactory; -import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; -import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; import java.util.function.Consumer; @@ -199,10 +183,6 @@ import java.util.function.Supplier; import java.util.stream.Collectors; -import static io.micronaut.http.client.HttpClientConfiguration.DEFAULT_SHUTDOWN_QUIET_PERIOD_MILLISECONDS; -import static io.micronaut.http.client.HttpClientConfiguration.DEFAULT_SHUTDOWN_TIMEOUT_MILLISECONDS; -import static io.micronaut.http.netty.channel.ChannelPipelineCustomizer.HANDLER_HTTP2_SETTINGS; -import static io.micronaut.http.netty.channel.ChannelPipelineCustomizer.HANDLER_IDLE_STATE; import static io.micronaut.scheduling.instrument.InvocationInstrumenter.NOOP; /** @@ -225,16 +205,6 @@ public class DefaultHttpClient implements * Default logger, use {@link #log} where possible. */ private static final Logger DEFAULT_LOG = LoggerFactory.getLogger(DefaultHttpClient.class); - private static final AttributeKey STREAM_KEY = AttributeKey.valueOf("micronaut.http2.stream"); - private static final AttributeKey CHANNEL_CUSTOMIZER_KEY = - AttributeKey.valueOf("micronaut.http.customizer"); - /** - * Future on a pooled channel that will be completed when the channel has fully connected (e.g. - * TLS handshake has completed). If unset, then no handshake is needed or it has already - * completed. - */ - private static final AttributeKey> STREAM_CHANNEL_INITIALIZED = - AttributeKey.valueOf("micronaut.http.streamChannelInitialized"); private static final int DEFAULT_HTTP_PORT = 80; private static final int DEFAULT_HTTPS_PORT = 443; @@ -259,32 +229,20 @@ public class DefaultHttpClient implements REDIRECT_HEADER_BLOCKLIST.add(HttpHeaderNames.CONNECTION, ""); } - protected final Bootstrap bootstrap; - protected EventLoopGroup group; protected MediaTypeCodecRegistry mediaTypeCodecRegistry; protected ByteBufferFactory byteBufferFactory = new NettyByteBufferFactory(); + final ConnectionManager connectionManager; + private final List> clientFilterEntries; - private final io.micronaut.http.HttpVersion httpVersion; - private final Scheduler scheduler; private final LoadBalancer loadBalancer; private final HttpClientConfiguration configuration; private final String contextPath; - private final SslContext sslContext; - private final ThreadFactory threadFactory; - private final boolean shutdownGroup; private final Charset defaultCharset; - private final ChannelPoolMap poolMap; private final Logger log; - private final @Nullable - Long readTimeoutMillis; - private final @Nullable - Long connectionTimeAliveMillis; private final HttpClientFilterResolver filterResolver; private final WebSocketBeanRegistry webSocketRegistry; private final RequestBinderRegistry requestBinderRegistry; - private final Collection pipelineListeners; - private final NettyClientCustomizer clientCustomizer; private final List invocationInstrumenterFactories; private final String informationalServiceId; @@ -331,7 +289,7 @@ public DefaultHttpClient(@Nullable LoadBalancer loadBalancer, /** * Construct a client for the given arguments. * @param loadBalancer The {@link LoadBalancer} to use for selecting servers - * @param httpVersion The HTTP version to use. Can be null and defaults to {@link io.micronaut.http.HttpVersion#HTTP_1_1} + * @param explicitHttpVersion The HTTP version to use. Can be null and defaults to {@link io.micronaut.http.HttpVersion#HTTP_1_1} * @param configuration The {@link HttpClientConfiguration} object * @param contextPath The base URI to prepend to request uris * @param filterResolver The http client filter resolver @@ -349,7 +307,7 @@ public DefaultHttpClient(@Nullable LoadBalancer loadBalancer, * @param informationalServiceId Optional service ID that will be passed to exceptions created by this client */ public DefaultHttpClient(@Nullable LoadBalancer loadBalancer, - @Nullable io.micronaut.http.HttpVersion httpVersion, + @Nullable io.micronaut.http.HttpVersion explicitHttpVersion, @NonNull HttpClientConfiguration configuration, @Nullable String contextPath, @NonNull HttpClientFilterResolver filterResolver, @@ -374,7 +332,6 @@ public DefaultHttpClient(@Nullable LoadBalancer loadBalancer, ArgumentUtils.requireNonNull("filterResolver", filterResolver); ArgumentUtils.requireNonNull("socketChannelFactory", socketChannelFactory); this.loadBalancer = loadBalancer; - this.httpVersion = httpVersion != null ? httpVersion : configuration.getHttpVersion(); this.defaultCharset = configuration.getDefaultCharset(); if (StringUtils.isNotEmpty(contextPath)) { if (contextPath.charAt(0) != '/') { @@ -384,93 +341,11 @@ public DefaultHttpClient(@Nullable LoadBalancer loadBalancer, } else { this.contextPath = null; } - this.bootstrap = new Bootstrap(); this.configuration = configuration; - this.sslContext = nettyClientSslBuilder.build(configuration.getSslConfiguration(), this.httpVersion).orElse(null); - if (eventLoopGroup != null) { - this.group = eventLoopGroup; - this.shutdownGroup = false; - } else { - this.group = createEventLoopGroup(configuration, threadFactory); - this.shutdownGroup = true; - } - - this.scheduler = Schedulers.fromExecutorService(group); - this.threadFactory = threadFactory; - this.bootstrap.group(group) - .channelFactory(socketChannelFactory) - .option(ChannelOption.SO_KEEPALIVE, true); - - Optional readTimeout = configuration.getReadTimeout(); - this.readTimeoutMillis = readTimeout.map(duration -> !duration.isNegative() ? duration.toMillis() : null).orElse(null); - - Optional connectTtl = configuration.getConnectTtl(); - this.connectionTimeAliveMillis = connectTtl.map(duration -> !duration.isNegative() ? duration.toMillis() : null).orElse(null); - final ChannelHealthChecker channelHealthChecker = channel -> channel.eventLoop().newSucceededFuture(channel.isActive() && !ConnectTTLHandler.isChannelExpired(channel)); this.invocationInstrumenterFactories = invocationInstrumenterFactories == null ? Collections.emptyList() : invocationInstrumenterFactories; - HttpClientConfiguration.ConnectionPoolConfiguration connectionPoolConfiguration = configuration.getConnectionPoolConfiguration(); - // HTTP/2 defaults to keep alive connections so should we should always use a pool - if (connectionPoolConfiguration.isEnabled() || this.httpVersion == io.micronaut.http.HttpVersion.HTTP_2_0) { - int maxConnections = connectionPoolConfiguration.getMaxConnections(); - if (maxConnections > -1) { - poolMap = new AbstractChannelPoolMap() { - @Override - protected ChannelPool newPool(RequestKey key) { - Bootstrap newBootstrap = bootstrap.clone(group); - initBootstrapForProxy(newBootstrap, key.isSecure(), key.getHost(), key.getPort()); - newBootstrap.remoteAddress(key.getRemoteAddress()); - - AbstractChannelPoolHandler channelPoolHandler = newPoolHandler(key); - final long acquireTimeoutMillis = connectionPoolConfiguration.getAcquireTimeout().map(Duration::toMillis).orElse(-1L); - return new FixedChannelPool( - newBootstrap, - channelPoolHandler, - channelHealthChecker, - acquireTimeoutMillis > -1 ? FixedChannelPool.AcquireTimeoutAction.FAIL : null, - acquireTimeoutMillis, - maxConnections, - connectionPoolConfiguration.getMaxPendingAcquires() - - ); - } - }; - } else { - poolMap = new AbstractChannelPoolMap() { - @Override - protected ChannelPool newPool(RequestKey key) { - Bootstrap newBootstrap = bootstrap.clone(group); - initBootstrapForProxy(newBootstrap, key.isSecure(), key.getHost(), key.getPort()); - newBootstrap.remoteAddress(key.getRemoteAddress()); - - AbstractChannelPoolHandler channelPoolHandler = newPoolHandler(key); - return new SimpleChannelPool( - newBootstrap, - channelPoolHandler, - channelHealthChecker - ); - } - }; - } - } else { - this.poolMap = null; - } - - Optional connectTimeout = configuration.getConnectTimeout(); - connectTimeout.ifPresent(duration -> this.bootstrap.option( - ChannelOption.CONNECT_TIMEOUT_MILLIS, - (int) duration.toMillis() - )); - - for (Map.Entry entry : configuration.getChannelOptions().entrySet()) { - Object v = entry.getValue(); - if (v != null) { - String channelOption = entry.getKey(); - bootstrap.option(ChannelOption.valueOf(channelOption), v); - } - } this.mediaTypeCodecRegistry = codecRegistry; this.log = configuration.getLoggerName().map(LoggerFactory::getLogger).orElse(DEFAULT_LOG); this.filterResolver = filterResolver; @@ -483,9 +358,20 @@ protected ChannelPool newPool(RequestKey key) { } this.webSocketRegistry = webSocketBeanRegistry != null ? webSocketBeanRegistry : WebSocketBeanRegistry.EMPTY; this.requestBinderRegistry = requestBinderRegistry; - this.pipelineListeners = pipelineListeners; - this.clientCustomizer = clientCustomizer; this.informationalServiceId = informationalServiceId; + + this.connectionManager = new ConnectionManager( + log, + eventLoopGroup, + threadFactory, + configuration, + explicitHttpVersion, + combineFactories(), + socketChannelFactory, + nettyClientSslBuilder, + clientCustomizer, + pipelineListeners, + informationalServiceId); } /** @@ -529,6 +415,11 @@ configuration, null, new DefaultThreadFactory(MultithreadEventLoopGroup.class), invocationInstrumenterFactories); } + static boolean isAcceptEvents(io.micronaut.http.HttpRequest request) { + String acceptHeader = request.getHeaders().get(io.micronaut.http.HttpHeaders.ACCEPT); + return acceptHeader != null && acceptHeader.equalsIgnoreCase(MediaType.TEXT_EVENT_STREAM); + } + /** * @return The configuration used by this client */ @@ -546,65 +437,20 @@ public Logger getLog() { @Override public HttpClient start() { if (!isRunning()) { - this.group = createEventLoopGroup(configuration, threadFactory); + connectionManager.start(); } return this; } @Override public boolean isRunning() { - return !group.isShutdown(); + return connectionManager.isRunning(); } @Override public HttpClient stop() { if (isRunning()) { - if (poolMap instanceof Iterable) { - Iterable> i = (Iterable) poolMap; - for (Map.Entry entry : i) { - ChannelPool cp = entry.getValue(); - try { - if (cp instanceof SimpleChannelPool) { - addInstrumentedListener(((SimpleChannelPool) cp).closeAsync(), future -> { - if (!future.isSuccess()) { - final Throwable cause = future.cause(); - if (cause != null) { - log.error("Error shutting down HTTP client connection pool: " + cause.getMessage(), cause); - } - } - }); - } else { - cp.close(); - } - } catch (Exception cause) { - log.error("Error shutting down HTTP client connection pool: " + cause.getMessage(), cause); - } - - } - } - if (shutdownGroup) { - Duration shutdownTimeout = configuration.getShutdownTimeout() - .orElse(Duration.ofMillis(DEFAULT_SHUTDOWN_TIMEOUT_MILLISECONDS)); - Duration shutdownQuietPeriod = configuration.getShutdownQuietPeriod() - .orElse(Duration.ofMillis(DEFAULT_SHUTDOWN_QUIET_PERIOD_MILLISECONDS)); - - Future future = this.group.shutdownGracefully( - shutdownQuietPeriod.toMillis(), - shutdownTimeout.toMillis(), - TimeUnit.MILLISECONDS - ); - addInstrumentedListener(future, f -> { - if (!f.isSuccess() && log.isErrorEnabled()) { - Throwable cause = f.cause(); - log.error("Error shutting down HTTP client: " + cause.getMessage(), cause); - } - }); - try { - future.await(shutdownTimeout.toMillis()); - } catch (InterruptedException e) { - // ignore - } - } + connectionManager.shutdown(); } return this; } @@ -954,96 +800,48 @@ public void close() { stop(); } - private Flux connectWebSocket(URI uri, MutableHttpRequest request, Class clientEndpointType, WebSocketBean webSocketBean) { - Bootstrap bootstrap = this.bootstrap.clone(); + private Publisher connectWebSocket(URI uri, MutableHttpRequest request, Class clientEndpointType, WebSocketBean webSocketBean) { + RequestKey requestKey; + try { + requestKey = new RequestKey(this, uri); + } catch (HttpClientException e) { + return Flux.error(e); + } + if (webSocketBean == null) { webSocketBean = webSocketRegistry.getWebSocket(clientEndpointType); } - WebSocketBean finalWebSocketBean = webSocketBean; - return Flux.create(emitter -> { - SslContext sslContext = buildSslContext(uri); - WebSocketVersion protocolVersion = finalWebSocketBean.getBeanDefinition().enumValue(ClientWebSocket.class, "version", WebSocketVersion.class).orElse(WebSocketVersion.V13); - int maxFramePayloadLength = finalWebSocketBean.messageMethod() - .map(m -> m.intValue(OnMessage.class, "maxPayloadLength") - .orElse(65536)).orElse(65536); - String subprotocol = finalWebSocketBean.getBeanDefinition().stringValue(ClientWebSocket.class, "subprotocol").orElse(StringUtils.EMPTY_STRING); - - RequestKey requestKey; - try { - requestKey = new RequestKey(this, uri); - } catch (HttpClientException e) { - emitter.error(e); - return; - } - - bootstrap.remoteAddress(requestKey.getHost(), requestKey.getPort()); - initBootstrapForProxy(bootstrap, sslContext != null, requestKey.getHost(), requestKey.getPort()); - bootstrap.handler(new HttpClientInitializer( - sslContext, - requestKey.getHost(), - requestKey.getPort(), - false, - false, - false, - null - ) { - @Override - protected void addFinalHandler(ChannelPipeline pipeline) { - pipeline.remove(ChannelPipelineCustomizer.HANDLER_HTTP_DECODER); - ReadTimeoutHandler readTimeoutHandler = pipeline.get(ReadTimeoutHandler.class); - if (readTimeoutHandler != null) { - pipeline.remove(readTimeoutHandler); - } - - Optional readIdleTime = configuration.getReadIdleTimeout(); - if (readIdleTime.isPresent()) { - Duration duration = readIdleTime.get(); - if (!duration.isNegative()) { - pipeline.addLast(ChannelPipelineCustomizer.HANDLER_IDLE_STATE, new IdleStateHandler(duration.toMillis(), duration.toMillis(), duration.toMillis(), TimeUnit.MILLISECONDS)); - } - } + WebSocketVersion protocolVersion = webSocketBean.getBeanDefinition().enumValue(ClientWebSocket.class, "version", WebSocketVersion.class).orElse(WebSocketVersion.V13); + int maxFramePayloadLength = webSocketBean.messageMethod() + .map(m -> m.intValue(OnMessage.class, "maxPayloadLength") + .orElse(65536)).orElse(65536); + String subprotocol = webSocketBean.getBeanDefinition().stringValue(ClientWebSocket.class, "subprotocol").orElse(StringUtils.EMPTY_STRING); + URI webSocketURL = UriBuilder.of(uri) + .scheme(!requestKey.isSecure() ? "ws" : "wss") + .host(requestKey.getHost()) + .port(requestKey.getPort()) + .build(); - final NettyWebSocketClientHandler webSocketHandler; - try { - String scheme = (sslContext == null) ? "ws" : "wss"; - URI webSocketURL = UriBuilder.of(uri) - .scheme(scheme) - .host(host) - .port(port) - .build(); - - MutableHttpHeaders headers = request.getHeaders(); - HttpHeaders customHeaders = EmptyHttpHeaders.INSTANCE; - if (headers instanceof NettyHttpHeaders) { - customHeaders = ((NettyHttpHeaders) headers).getNettyHeaders(); - } - if (StringUtils.isNotEmpty(subprotocol)) { - customHeaders.add("Sec-WebSocket-Protocol", subprotocol); - } + MutableHttpHeaders headers = request.getHeaders(); + HttpHeaders customHeaders = EmptyHttpHeaders.INSTANCE; + if (headers instanceof NettyHttpHeaders) { + customHeaders = ((NettyHttpHeaders) headers).getNettyHeaders(); + } + if (StringUtils.isNotEmpty(subprotocol)) { + customHeaders.add("Sec-WebSocket-Protocol", subprotocol); + } - webSocketHandler = new NettyWebSocketClientHandler<>( - request, - finalWebSocketBean, - WebSocketClientHandshakerFactory.newHandshaker( - webSocketURL, protocolVersion, subprotocol, true, customHeaders, maxFramePayloadLength), - requestBinderRegistry, - mediaTypeCodecRegistry, - emitter); - pipeline.addLast(WebSocketClientCompressionHandler.INSTANCE); - pipeline.addLast(ChannelPipelineCustomizer.HANDLER_MICRONAUT_WEBSOCKET_CLIENT, webSocketHandler); - } catch (Throwable e) { - emitter.error(new WebSocketSessionException("Error opening WebSocket client session: " + e.getMessage(), e)); - } - } - }); + NettyWebSocketClientHandler handler = new NettyWebSocketClientHandler<>( + request, + webSocketBean, + WebSocketClientHandshakerFactory.newHandshaker( + webSocketURL, protocolVersion, subprotocol, true, customHeaders, maxFramePayloadLength), + requestBinderRegistry, + mediaTypeCodecRegistry); - addInstrumentedListener(bootstrap.connect(), future -> { - if (!future.isSuccess()) { - emitter.error(future.cause()); - } - }); - }, FluxSink.OverflowStrategy.ERROR); + return connectionManager.connectForWebsocket(requestKey, handler) + .then(handler.getHandshakeCompletedMono()); } private Flux>> exchangeStreamImpl(io.micronaut.http.HttpRequest parentRequest, io.micronaut.http.HttpRequest request, Argument errorType, URI requestURI) { @@ -1161,7 +959,7 @@ private Publisher> buildStreamExchange( @Nullable Argument errorType) { AtomicReference> requestWrapper = new AtomicReference<>(request); - Flux> streamResponsePublisher = connectAndStream(parentRequest, request, requestURI, buildSslContext(requestURI), requestWrapper, false, true); + Flux> streamResponsePublisher = connectAndStream(parentRequest, request, requestURI, requestWrapper, false, true); streamResponsePublisher = readBodyOnError(errorType, streamResponsePublisher); @@ -1170,7 +968,7 @@ private Publisher> buildStreamExchange( applyFilterToResponsePublisher(parentRequest, request, requestURI, requestWrapper, streamResponsePublisher) ); - return streamResponsePublisher.subscribeOn(scheduler); + return streamResponsePublisher.subscribeOn(connectionManager.getEventLoopScheduler()); } @Override @@ -1191,7 +989,7 @@ public Publisher> proxy(@NonNull io.micronaut.http.HttpRe } AtomicReference> requestWrapper = new AtomicReference<>(httpRequest); - Flux> proxyResponsePublisher = connectAndStream(request, request, requestURI, buildSslContext(requestURI), requestWrapper, true, false); + Flux> proxyResponsePublisher = connectAndStream(request, request, requestURI, requestWrapper, true, false); // apply filters //noinspection unchecked proxyResponsePublisher = Flux.from( @@ -1211,58 +1009,26 @@ private Flux> connectAndStream( io.micronaut.http.HttpRequest parentRequest, io.micronaut.http.HttpRequest request, URI requestURI, - SslContext sslContext, AtomicReference> requestWrapper, boolean isProxy, boolean failOnError ) { - return Flux.create(emitter -> { - ChannelFuture channelFuture; - try { - if (httpVersion == io.micronaut.http.HttpVersion.HTTP_2_0) { - - channelFuture = doConnect(request, requestURI, sslContext, true, isProxy, channelHandlerContext -> { - try { - final Channel channel = channelHandlerContext.channel(); - request.setAttribute(NettyClientHttpRequest.CHANNEL, channel); - this.streamRequestThroughChannel( - parentRequest, - requestWrapper.get(), - channel, - failOnError - ).subscribe(new ForwardingSubscriber<>(emitter)); - } catch (Exception e) { - emitter.error(e); - } - }); - } else { - channelFuture = doConnect(request, requestURI, sslContext, true, isProxy, null); - addInstrumentedListener(channelFuture, - (ChannelFutureListener) f -> { - if (f.isSuccess()) { - Channel channel = f.channel(); - request.setAttribute(NettyClientHttpRequest.CHANNEL, channel); - this.streamRequestThroughChannel( - parentRequest, - requestWrapper.get(), - channel, - failOnError - ).subscribe(new ForwardingSubscriber<>(emitter)); - } else { - Throwable cause = f.cause(); - emitter.error(customizeException(new HttpClientException("Connect error:" + cause.getMessage(), cause))); - } - }); - } - } catch (HttpClientException e) { - emitter.error(e); - return; - } - - Disposable disposable = buildDisposableChannel(channelFuture); - emitter.onDispose(disposable); - emitter.onCancel(disposable); - }, FluxSink.OverflowStrategy.BUFFER); + RequestKey requestKey; + try { + requestKey = new RequestKey(this, requestURI); + } catch (Exception e) { + return Flux.error(e); + } + return connectionManager.connectForStream(requestKey, isProxy, isAcceptEvents(request)).flatMapMany(poolHandle -> { + request.setAttribute(NettyClientHttpRequest.CHANNEL, poolHandle.channel); + return this.streamRequestThroughChannel( + parentRequest, + requestWrapper.get(), + poolHandle, + failOnError, + requestKey.isSecure() + ); + }); } /** @@ -1276,87 +1042,32 @@ private Publisher> exchang @NonNull Argument errorType) { AtomicReference> requestWrapper = new AtomicReference<>(request); - Flux> responsePublisher = Flux.create(emitter -> { + RequestKey requestKey; + try { + requestKey = new RequestKey(this, requestURI); + } catch (HttpClientException e) { + return Flux.error(e); + } + + Mono handlePublisher = connectionManager.connectForExchange(requestKey, MediaType.MULTIPART_FORM_DATA_TYPE.equals(request.getContentType().orElse(null)), isAcceptEvents(request)); - boolean multipart = MediaType.MULTIPART_FORM_DATA_TYPE.equals(request.getContentType().orElse(null)); - if (poolMap != null && !multipart) { + Flux> responsePublisher = handlePublisher.flatMapMany(poolHandle -> { + return Flux.create(emitter -> { try { - RequestKey requestKey = new RequestKey(this, requestURI); - ChannelPool channelPool = poolMap.get(requestKey); - Future channelFuture = channelPool.acquire(); - addInstrumentedListener(channelFuture, future -> { - if (future.isSuccess()) { - Channel channel = future.get(); - Future initFuture = channel.attr(STREAM_CHANNEL_INITIALIZED).get(); - if (initFuture == null) { - try { - sendRequestThroughChannel( - requestWrapper.get(), - bodyType, - errorType, - emitter, - channel, - requestKey.isSecure(), - channelPool - ); - } catch (Exception e) { - emitter.error(e); - } - } else { - // we should wait until the handshake completes - addInstrumentedListener(initFuture, f -> { - try { - sendRequestThroughChannel( - requestWrapper.get(), - bodyType, - errorType, - emitter, - channel, - requestKey.isSecure(), - channelPool - ); - } catch (Exception e) { - emitter.error(e); - } - }); - } - } else { - Throwable cause = future.cause(); - emitter.error(customizeException(new HttpClientException("Connect Error: " + cause.getMessage(), cause))); - } - }); - } catch (HttpClientException e) { + sendRequestThroughChannel( + requestWrapper.get(), + bodyType, + errorType, + emitter, + poolHandle.channel, + requestKey.isSecure(), + poolHandle + ); + } catch (Exception e) { emitter.error(e); } - } else { - SslContext sslContext = buildSslContext(requestURI); - ChannelFuture connectionFuture = doConnect(request, requestURI, sslContext, false, null); - addInstrumentedListener(connectionFuture, future -> { - if (!future.isSuccess()) { - Throwable cause = future.cause(); - if (emitter.isCancelled()) { - log.trace("Connection to {} failed, but emitter already cancelled.", requestURI, cause); - } else { - emitter.error(customizeException(new HttpClientException("Connect Error: " + cause.getMessage(), cause))); - } - } else { - try { - sendRequestThroughChannel( - requestWrapper.get(), - bodyType, - errorType, - emitter, - connectionFuture.channel(), - sslContext != null, - null); - } catch (Exception e) { - emitter.error(e); - } - } - }); - } - - }, FluxSink.OverflowStrategy.ERROR); + }); + }); Publisher> finalPublisher = applyFilterToResponsePublisher( parentRequest, @@ -1386,22 +1097,6 @@ private Publisher> exchang return finalReactiveSequence; } - /** - * @param channel The channel to close asynchronously - */ - protected void closeChannelAsync(Channel channel) { - if (channel.isOpen()) { - - ChannelFuture closeFuture = channel.closeFuture(); - closeFuture.addListener(f2 -> { - if (!f2.isSuccess() && log.isErrorEnabled()) { - Throwable cause = f2.cause(); - log.error("Error closing request connection: " + cause.getMessage(), cause); - } - }); - } - } - /** * @param request The request * @param The input type @@ -1475,229 +1170,6 @@ protected Object getLoadBalancerDiscriminator() { return null; } - private void initBootstrapForProxy(Bootstrap bootstrap, boolean ssl, String host, int port) { - Proxy proxy = configuration.resolveProxy(ssl, host, port); - if (proxy.type() != Type.DIRECT) { - bootstrap.resolver(NoopAddressResolverGroup.INSTANCE); - } - } - - /** - * Creates an initial connection to the given remote host. - * - * @param request The request - * @param uri The URI to connect to - * @param sslCtx The SslContext instance - * @param isStream Is the connection a stream connection - * @param contextConsumer The logic to run once the channel is configured correctly - * @return A ChannelFuture - * @throws HttpClientException If the URI is invalid - */ - protected ChannelFuture doConnect( - io.micronaut.http.HttpRequest request, - URI uri, - @Nullable SslContext sslCtx, - boolean isStream, - Consumer contextConsumer) throws HttpClientException { - return doConnect(request, uri, sslCtx, isStream, false, contextConsumer); - } - - /** - * Creates an initial connection to the given remote host. - * - * @param request The request - * @param uri The URI to connect to - * @param sslCtx The SslContext instance - * @param isStream Is the connection a stream connection - * @param isProxy Is this a streaming proxy - * @param contextConsumer The logic to run once the channel is configured correctly - * @return A ChannelFuture - * @throws HttpClientException If the URI is invalid - */ - protected ChannelFuture doConnect( - io.micronaut.http.HttpRequest request, - URI uri, - @Nullable SslContext sslCtx, - boolean isStream, - boolean isProxy, - Consumer contextConsumer) throws HttpClientException { - - RequestKey requestKey = new RequestKey(this, uri); - return doConnect(request, requestKey.getHost(), requestKey.getPort(), sslCtx, isStream, isProxy, contextConsumer); - } - - /** - * Creates an initial connection to the given remote host. - * - * @param request The request - * @param host The host - * @param port The port - * @param sslCtx The SslContext instance - * @param isStream Is the connection a stream connection - * @param contextConsumer The logic to run once the channel is configured correctly - * @return A ChannelFuture - */ - protected ChannelFuture doConnect( - io.micronaut.http.HttpRequest request, - String host, - int port, - @Nullable SslContext sslCtx, - boolean isStream, - Consumer contextConsumer) { - return doConnect(request, host, port, sslCtx, isStream, false, contextConsumer); - } - - /** - * Creates an initial connection to the given remote host. - * - * @param request The request - * @param host The host - * @param port The port - * @param sslCtx The SslContext instance - * @param isStream Is the connection a stream connection - * @param isProxy Is this a streaming proxy - * @param contextConsumer The logic to run once the channel is configured correctly - * @return A ChannelFuture - */ - protected ChannelFuture doConnect( - io.micronaut.http.HttpRequest request, - String host, - int port, - @Nullable SslContext sslCtx, - boolean isStream, - boolean isProxy, - Consumer contextConsumer) { - Bootstrap localBootstrap = this.bootstrap.clone(); - initBootstrapForProxy(localBootstrap, sslCtx != null, host, port); - String acceptHeader = request.getHeaders().get(io.micronaut.http.HttpHeaders.ACCEPT); - localBootstrap.handler(new HttpClientInitializer( - sslCtx, - host, - port, - isStream, - isProxy, - acceptHeader != null && acceptHeader.equalsIgnoreCase(MediaType.TEXT_EVENT_STREAM), contextConsumer) - ); - return doConnect(localBootstrap, host, port); - } - - /** - * Creates the {@link NioEventLoopGroup} for this client. - * - * @param configuration The configuration - * @param threadFactory The thread factory - * @return The group - */ - protected NioEventLoopGroup createEventLoopGroup(HttpClientConfiguration configuration, ThreadFactory threadFactory) { - OptionalInt numOfThreads = configuration.getNumOfThreads(); - Optional> threadFactoryType = configuration.getThreadFactory(); - boolean hasThreads = numOfThreads.isPresent(); - boolean hasFactory = threadFactoryType.isPresent(); - NioEventLoopGroup group; - if (hasThreads && hasFactory) { - group = new NioEventLoopGroup(numOfThreads.getAsInt(), InstantiationUtils.instantiate(threadFactoryType.get())); - } else if (hasThreads) { - if (threadFactory != null) { - group = new NioEventLoopGroup(numOfThreads.getAsInt(), threadFactory); - } else { - group = new NioEventLoopGroup(numOfThreads.getAsInt()); - } - } else { - if (threadFactory != null) { - group = new NioEventLoopGroup(NettyThreadFactory.DEFAULT_EVENT_LOOP_THREADS, threadFactory); - } else { - - group = new NioEventLoopGroup(); - } - } - return group; - } - - /** - * Creates an initial connection with the given bootstrap and remote host. - * - * @param bootstrap The bootstrap instance - * @param host The host - * @param port The port - * @return The ChannelFuture - */ - protected ChannelFuture doConnect(Bootstrap bootstrap, String host, int port) { - return bootstrap.connect(host, port); - } - - /** - * Builds an {@link SslContext} for the given URI if necessary. - * - * @param uriObject The URI - * @return The {@link SslContext} instance - */ - protected SslContext buildSslContext(URI uriObject) { - final SslContext sslCtx; - if (isSecureScheme(uriObject.getScheme())) { - sslCtx = sslContext; - //Allow https requests to be sent if SSL is disabled but a proxy is present - if (sslCtx == null && !configuration.getProxyAddress().isPresent()) { - throw customizeException(new HttpClientException("Cannot send HTTPS request. SSL is disabled")); - } - } else { - sslCtx = null; - } - return sslCtx; - } - - /** - * Configures the HTTP proxy for the pipeline. - * - * @param pipeline The pipeline - * @param proxy The proxy - */ - protected void configureProxy(ChannelPipeline pipeline, Proxy proxy) { - configureProxy(pipeline, proxy.type(), proxy.address()); - } - - /** - * Configures the HTTP proxy for the pipeline. - * - * @param pipeline The pipeline - * @param proxyType The proxy type - * @param proxyAddress The proxy address - */ - protected void configureProxy(ChannelPipeline pipeline, Type proxyType, SocketAddress proxyAddress) { - String username = configuration.getProxyUsername().orElse(null); - String password = configuration.getProxyPassword().orElse(null); - - if (proxyAddress instanceof InetSocketAddress) { - InetSocketAddress isa = (InetSocketAddress) proxyAddress; - if (isa.isUnresolved()) { - proxyAddress = new InetSocketAddress(isa.getHostString(), isa.getPort()); - } - } - - if (StringUtils.isNotEmpty(username) && StringUtils.isNotEmpty(password)) { - switch (proxyType) { - case HTTP: - pipeline.addLast(ChannelPipelineCustomizer.HANDLER_HTTP_PROXY, new HttpProxyHandler(proxyAddress, username, password)); - break; - case SOCKS: - pipeline.addLast(ChannelPipelineCustomizer.HANDLER_SOCKS_5_PROXY, new Socks5ProxyHandler(proxyAddress, username, password)); - break; - default: - // no-op - } - } else { - switch (proxyType) { - case HTTP: - pipeline.addLast(ChannelPipelineCustomizer.HANDLER_HTTP_PROXY, new HttpProxyHandler(proxyAddress)); - break; - case SOCKS: - pipeline.addLast(ChannelPipelineCustomizer.HANDLER_SOCKS_5_PROXY, new Socks5ProxyHandler(proxyAddress)); - break; - default: - // no-op - } - } - } - private > Publisher applyFilterToResponsePublisher( io.micronaut.http.HttpRequest parentRequest, io.micronaut.http.HttpRequest request, @@ -1747,7 +1219,6 @@ private > Publisher applyFi * @param permitsBody Whether permits body * @param bodyType The body type * @param onError Called when the body publisher encounters an error - * @param closeChannelAfterWrite Whether to close the channel. For stream requests we don't close the channel until disposed of. * @return A {@link NettyRequestWriter} * @throws HttpPostRequestEncoder.ErrorDataEncoderException if there is an encoder exception */ @@ -1757,8 +1228,7 @@ protected NettyRequestWriter buildNettyRequest( MediaType requestContentType, boolean permitsBody, @Nullable Argument bodyType, - Consumer onError, - boolean closeChannelAfterWrite) throws HttpPostRequestEncoder.ErrorDataEncoderException { + Consumer onError) throws HttpPostRequestEncoder.ErrorDataEncoderException { io.netty.handler.codec.http.HttpRequest nettyRequest; HttpPostRequestEncoder postRequestEncoder = null; @@ -1853,7 +1323,7 @@ protected NettyRequestWriter buildNettyRequest( } catch (MalformedURLException e) { //should never happen } - return new NettyRequestWriter(requestURI.getScheme(), nettyRequest, null, closeChannelAfterWrite); + return new NettyRequestWriter(nettyRequest, null); } else if (bodyValue instanceof CharSequence) { bodyContent = charSequenceToByteBuf((CharSequence) bodyValue, requestContentType); } else if (mediaTypeCodecRegistry != null) { @@ -1889,153 +1359,7 @@ protected NettyRequestWriter buildNettyRequest( } catch (MalformedURLException e) { //should never happen } - return new NettyRequestWriter(requestURI.getScheme(), nettyRequest, postRequestEncoder, closeChannelAfterWrite); - } - - /** - * Configures HTTP/2 for the channel when SSL is enabled. - * - * @param httpClientInitializer The client initializer - * @param ch The channel - * @param sslCtx The SSL context - * @param host The host - * @param port The port - * @param connectionHandler The connection handler - */ - protected void configureHttp2Ssl( - HttpClientInitializer httpClientInitializer, - @NonNull SocketChannel ch, - @NonNull SslContext sslCtx, - String host, - int port, - HttpToHttp2ConnectionHandler connectionHandler) { - ChannelPipeline pipeline = ch.pipeline(); - // Specify Host in SSLContext New Handler to add TLS SNI Extension - pipeline.addLast(ChannelPipelineCustomizer.HANDLER_SSL, sslCtx.newHandler(ch.alloc(), host, port)); - // We must wait for the handshake to finish and the protocol to be negotiated before configuring - // the HTTP/2 components of the pipeline. - pipeline.addLast( - ChannelPipelineCustomizer.HANDLER_HTTP2_PROTOCOL_NEGOTIATOR, - new ApplicationProtocolNegotiationHandler(ApplicationProtocolNames.HTTP_2) { - - @Override - public void handlerRemoved(ChannelHandlerContext ctx) { - // the logic to send the request should only be executed once the HTTP/2 - // Connection Preface request has been sent. Once the Preface has been sent and - // removed then this handler is removed so we invoke the remaining logic once - // this handler removed - final Consumer contextConsumer = - httpClientInitializer.contextConsumer; - if (contextConsumer != null) { - contextConsumer.accept(ctx); - } - } - - @Override - protected void configurePipeline(ChannelHandlerContext ctx, String protocol) { - if (ApplicationProtocolNames.HTTP_2.equals(protocol)) { - ChannelPipeline p = ctx.pipeline(); - if (httpClientInitializer.stream) { - // stream consumer manages backpressure and reads - ctx.channel().config().setAutoRead(false); - } - p.addLast( - ChannelPipelineCustomizer.HANDLER_HTTP2_SETTINGS, - new Http2SettingsHandler(ch.newPromise()) - ); - httpClientInitializer.addEventStreamHandlerIfNecessary(p); - httpClientInitializer.addFinalHandler(p); - for (ChannelPipelineListener pipelineListener : pipelineListeners) { - pipelineListener.onConnect(p); - } - } else if (ApplicationProtocolNames.HTTP_1_1.equals(protocol)) { - ChannelPipeline p = ctx.pipeline(); - httpClientInitializer.addHttp1Handlers(p); - } else { - ctx.close(); - throw customizeException(new HttpClientException("Unknown Protocol: " + protocol)); - } - httpClientInitializer.onStreamPipelineBuilt(); - } - }); - - pipeline.addLast(ChannelPipelineCustomizer.HANDLER_HTTP2_CONNECTION, connectionHandler); - } - - /** - * Configures HTTP/2 handling for plaintext (non-SSL) connections. - * - * @param httpClientInitializer The client initializer - * @param ch The channel - * @param connectionHandler The connection handler - */ - protected void configureHttp2ClearText( - HttpClientInitializer httpClientInitializer, - @NonNull SocketChannel ch, - @NonNull HttpToHttp2ConnectionHandler connectionHandler) { - HttpClientCodec sourceCodec = new HttpClientCodec(); - Http2ClientUpgradeCodec upgradeCodec = new Http2ClientUpgradeCodec(ChannelPipelineCustomizer.HANDLER_HTTP2_CONNECTION, connectionHandler); - HttpClientUpgradeHandler upgradeHandler = new HttpClientUpgradeHandler(sourceCodec, upgradeCodec, 65536); - - final ChannelPipeline pipeline = ch.pipeline(); - pipeline.addLast(ChannelPipelineCustomizer.HANDLER_HTTP_CLIENT_CODEC, sourceCodec); - httpClientInitializer.settingsHandler = new Http2SettingsHandler(ch.newPromise()); - pipeline.addLast(upgradeHandler); - pipeline.addLast(new ChannelInboundHandlerAdapter() { - @Override - public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception { - ctx.fireUserEventTriggered(evt); - if (evt instanceof HttpClientUpgradeHandler.UpgradeEvent) { - httpClientInitializer.onStreamPipelineBuilt(); - ctx.pipeline().remove(this); - } - } - }); - pipeline.addLast(ChannelPipelineCustomizer.HANDLER_HTTP2_UPGRADE_REQUEST, new UpgradeRequestHandler(httpClientInitializer) { - @Override - public void handlerRemoved(ChannelHandlerContext ctx) { - final Consumer contextConsumer = httpClientInitializer.contextConsumer; - if (contextConsumer != null) { - contextConsumer.accept(ctx); - } - } - }); - } - - /** - * Creates a new {@link HttpToHttp2ConnectionHandlerBuilder} for the given HTTP/2 connection object and config. - * - * @param connection The connection - * @param configuration The configuration - * @param stream Whether this is a stream request - * @return The {@link HttpToHttp2ConnectionHandlerBuilder} - */ - protected @NonNull - HttpToHttp2ConnectionHandlerBuilder newHttp2ConnectionHandlerBuilder( - @NonNull Http2Connection connection, @NonNull HttpClientConfiguration configuration, boolean stream) { - final HttpToHttp2ConnectionHandlerBuilder builder = new HttpToHttp2ConnectionHandlerBuilder(); - builder.validateHeaders(true); - final Http2FrameListener http2ToHttpAdapter; - - if (!stream) { - http2ToHttpAdapter = new InboundHttp2ToHttpAdapterBuilder(connection) - .maxContentLength(configuration.getMaxContentLength()) - .validateHttpHeaders(true) - .propagateSettings(true) - .build(); - - } else { - http2ToHttpAdapter = new StreamingInboundHttp2ToHttpAdapter( - connection, - configuration.getMaxContentLength() - ); - } - return builder - .connection(connection) - .frameListener(new DelegatingDecompressorFrameListener( - connection, - http2ToHttpAdapter)); - + return new NettyRequestWriter(nettyRequest, postRequestEncoder); } private Flux> readBodyOnError(@Nullable Argument errorType, @NonNull Flux> publisher) { @@ -2122,7 +1446,7 @@ private void sendRequestThroughChannel( FluxSink> emitter, Channel channel, boolean secure, - ChannelPool channelPool) throws HttpPostRequestEncoder.ErrorDataEncoderException { + ConnectionManager.PoolHandle poolHandle) throws HttpPostRequestEncoder.ErrorDataEncoderException { URI requestURI = finalRequest.getUri(); MediaType requestContentType = finalRequest .getContentType() @@ -2141,8 +1465,7 @@ private void sendRequestThroughChannel( if (!emitter.isCancelled()) { emitter.error(throwable); } - }, - true + } ); HttpRequest nettyRequest = requestWriter.getNettyRequest(); @@ -2151,7 +1474,7 @@ private void sendRequestThroughChannel( finalRequest, nettyRequest, permitsBody, - poolMap == null + !poolHandle.canReturn() ); if (log.isDebugEnabled()) { @@ -2164,8 +1487,8 @@ private void sendRequestThroughChannel( Promise> responsePromise = channel.eventLoop().newPromise(); channel.pipeline().addLast(ChannelPipelineCustomizer.HANDLER_MICRONAUT_FULL_HTTP_RESPONSE, - new FullHttpResponseHandler<>(responsePromise, channelPool, secure, finalRequest, bodyType, errorType)); - channel.attr(CHANNEL_CUSTOMIZER_KEY).get().onRequestPipelineBuilt(); + new FullHttpResponseHandler<>(responsePromise, poolHandle, secure, finalRequest, bodyType, errorType)); + poolHandle.notifyRequestPipelineBuilt(); Publisher> publisher = new NettyFuturePublisher<>(responsePromise, true); if (bodyType != null && bodyType.isVoid()) { // don't emit response if bodyType is void @@ -2173,17 +1496,18 @@ private void sendRequestThroughChannel( } publisher.subscribe(new ForwardingSubscriber<>(emitter)); - requestWriter.writeAndClose(channel, channelPool, emitter); + requestWriter.write(channel, secure, emitter); } private Flux> streamRequestThroughChannel( io.micronaut.http.HttpRequest parentRequest, io.micronaut.http.HttpRequest request, - Channel channel, - boolean failOnError) { + ConnectionManager.PoolHandle poolHandle, + boolean failOnError, + boolean secure) { return Flux.>create(sink -> { try { - streamRequestThroughChannel0(parentRequest, request, sink, channel); + streamRequestThroughChannel0(parentRequest, request, sink, poolHandle, secure); } catch (HttpPostRequestEncoder.ErrorDataEncoderException e) { sink.error(e); } @@ -2206,19 +1530,19 @@ private void streamRequestThroughChannel0( io.micronaut.http.HttpRequest parentRequest, final io.micronaut.http.HttpRequest finalRequest, FluxSink emitter, - Channel channel) throws HttpPostRequestEncoder.ErrorDataEncoderException { + ConnectionManager.PoolHandle poolHandle, + boolean secure) throws HttpPostRequestEncoder.ErrorDataEncoderException { NettyRequestWriter requestWriter = prepareRequest( finalRequest, finalRequest.getUri(), - emitter, - false + emitter ); HttpRequest nettyRequest = requestWriter.getNettyRequest(); - Promise> responsePromise = channel.eventLoop().newPromise(); - ChannelPipeline pipeline = channel.pipeline(); + Promise> responsePromise = poolHandle.channel.eventLoop().newPromise(); + ChannelPipeline pipeline = poolHandle.channel.pipeline(); pipeline.addLast(ChannelPipelineCustomizer.HANDLER_MICRONAUT_HTTP_RESPONSE_FULL, new StreamFullHttpResponseHandler(responsePromise, parentRequest, finalRequest)); pipeline.addLast(ChannelPipelineCustomizer.HANDLER_MICRONAUT_HTTP_RESPONSE_STREAM, new StreamStreamHttpResponseHandler(responsePromise, parentRequest, finalRequest)); - channel.attr(CHANNEL_CUSTOMIZER_KEY).get().onRequestPipelineBuilt(); + poolHandle.notifyRequestPipelineBuilt(); if (log.isDebugEnabled()) { debugRequest(finalRequest.getUri(), nettyRequest); @@ -2228,7 +1552,7 @@ private void streamRequestThroughChannel0( traceRequest(finalRequest, nettyRequest); } - requestWriter.writeAndClose(channel, null, emitter); + requestWriter.write(poolHandle.channel, secure, emitter); responsePromise.addListener(future -> { if (future.isSuccess()) { emitter.next(future.getNow()); @@ -2271,7 +1595,7 @@ private void prepareHttpHeaders( } // HTTP/2 assumes keep-alive connections - if (httpVersion != io.micronaut.http.HttpVersion.HTTP_2_0) { + if (connectionManager.httpVersion != io.micronaut.http.HttpVersion.HTTP_2_0) { if (closeConnection) { headers.set(HttpHeaderNames.CONNECTION, HttpHeaderValues.CLOSE); } else { @@ -2300,64 +1624,6 @@ private void prepareHttpHeaders( } } - /** - * Note: caller must ensure this is only called for plaintext HTTP, not TLS HTTP2. - */ - private boolean discardH2cStream(HttpMessage message) { - // only applies to h2c - if (httpVersion == io.micronaut.http.HttpVersion.HTTP_2_0) { - int streamId = message.headers().getInt(HttpConversionUtil.ExtensionHeaderNames.STREAM_ID.text(), -1); - if (streamId == 1) { - // ignore this message - if (log.isDebugEnabled()) { - log.debug("Received response on HTTP2 stream 1, the stream used to respond to the initial upgrade request. Ignoring."); - } - return true; - } else { - return false; - } - } else { - return false; - } - } - - private void addReadTimeoutHandler(ChannelPipeline pipeline) { - if (readTimeoutMillis != null) { - if (httpVersion == io.micronaut.http.HttpVersion.HTTP_2_0) { - Http2SettingsHandler settingsHandler = (Http2SettingsHandler) pipeline.get(HANDLER_HTTP2_SETTINGS); - if (settingsHandler != null) { - addInstrumentedListener(settingsHandler.promise, future -> { - if (future.isSuccess()) { - pipeline.addBefore( - ChannelPipelineCustomizer.HANDLER_HTTP2_CONNECTION, - ChannelPipelineCustomizer.HANDLER_READ_TIMEOUT, - new ReadTimeoutHandler(readTimeoutMillis, TimeUnit.MILLISECONDS) - ); - } - - }); - } else { - pipeline.addBefore( - ChannelPipelineCustomizer.HANDLER_HTTP2_CONNECTION, - ChannelPipelineCustomizer.HANDLER_READ_TIMEOUT, - new ReadTimeoutHandler(readTimeoutMillis, TimeUnit.MILLISECONDS) - ); - } - } else { - pipeline.addBefore( - ChannelPipelineCustomizer.HANDLER_HTTP_CLIENT_CODEC, - ChannelPipelineCustomizer.HANDLER_READ_TIMEOUT, - new ReadTimeoutHandler(readTimeoutMillis, TimeUnit.MILLISECONDS)); - } - } - } - - private void removeReadTimeoutHandler(ChannelPipeline pipeline) { - if (readTimeoutMillis != null && pipeline.context(ChannelPipelineCustomizer.HANDLER_READ_TIMEOUT) != null) { - pipeline.remove(ChannelPipelineCustomizer.HANDLER_READ_TIMEOUT); - } - } - private ClientFilterChain buildChain(AtomicReference> requestWrapper, List filters) { AtomicInteger integer = new AtomicInteger(); int len = filters.size(); @@ -2524,8 +1790,7 @@ private static MediaTypeCodecRegistry createDefaultMediaTypeRegistry() { private NettyRequestWriter prepareRequest( io.micronaut.http.HttpRequest request, URI requestURI, - FluxSink> emitter, - boolean closeChannelAfterWrite) throws HttpPostRequestEncoder.ErrorDataEncoderException { + FluxSink> emitter) throws HttpPostRequestEncoder.ErrorDataEncoderException { MediaType requestContentType = request .getContentType() .orElse(MediaType.APPLICATION_JSON_TYPE); @@ -2546,148 +1811,13 @@ private NettyRequestWriter prepareRequest( if (!emitter.isCancelled()) { emitter.error(throwable); } - }, - closeChannelAfterWrite + } ); io.netty.handler.codec.http.HttpRequest nettyRequest = requestWriter.getNettyRequest(); prepareHttpHeaders(requestURI, request, nettyRequest, permitsBody, true); return requestWriter; } - private Disposable buildDisposableChannel(ChannelFuture channelFuture) { - return new Disposable() { - private AtomicBoolean disposed = new AtomicBoolean(false); - - @Override - public void dispose() { - if (disposed.compareAndSet(false, true)) { - Channel channel = channelFuture.channel(); - if (channel.isOpen()) { - closeChannelAsync(channel); - } - } - } - - @Override - public boolean isDisposed() { - return disposed.get(); - } - }; - } - - private AbstractChannelPoolHandler newPoolHandler(RequestKey key) { - return new AbstractChannelPoolHandler() { - @Override - public void channelCreated(Channel ch) { - Promise streamPipelineBuilt = ch.newPromise(); - ch.attr(STREAM_CHANNEL_INITIALIZED).set(streamPipelineBuilt); - - // make sure the future completes eventually - ChannelHandler failureHandler = new ChannelInboundHandlerAdapter() { - @Override - public void handlerRemoved(ChannelHandlerContext ctx) { - streamPipelineBuilt.trySuccess(null); - } - - @Override - public void channelInactive(ChannelHandlerContext ctx) { - streamPipelineBuilt.trySuccess(null); - ctx.fireChannelInactive(); - } - }; - ch.pipeline().addLast(failureHandler); - - ch.pipeline().addLast(ChannelPipelineCustomizer.HANDLER_HTTP_CLIENT_INIT, new HttpClientInitializer( - key.isSecure() ? sslContext : null, - key.getHost(), - key.getPort(), - false, - false, - false, - null - ) { - @Override - protected void addFinalHandler(ChannelPipeline pipeline) { - // no-op, don't add the stream handler which is not supported - // in the connection pooled scenario - } - - @Override - void onStreamPipelineBuilt() { - super.onStreamPipelineBuilt(); - streamPipelineBuilt.trySuccess(null); - ch.pipeline().remove(failureHandler); - ch.attr(STREAM_CHANNEL_INITIALIZED).set(null); - } - }); - - if (connectionTimeAliveMillis != null) { - ch.pipeline() - .addLast( - ChannelPipelineCustomizer.HANDLER_CONNECT_TTL, - new ConnectTTLHandler(connectionTimeAliveMillis) - ); - } - } - - @Override - public void channelReleased(Channel ch) { - Duration idleTimeout = configuration.getConnectionPoolIdleTimeout().orElse(Duration.ofNanos(0)); - ChannelPipeline pipeline = ch.pipeline(); - if (ch.isOpen()) { - ch.config().setAutoRead(true); - pipeline.addLast(IdlingConnectionHandler.INSTANCE); - if (idleTimeout.toNanos() > 0) { - pipeline.addLast(HANDLER_IDLE_STATE, new IdleStateHandler(idleTimeout.toNanos(), idleTimeout.toNanos(), 0, TimeUnit.NANOSECONDS)); - pipeline.addLast(IdleTimeoutHandler.INSTANCE); - } - } - - if (ConnectTTLHandler.isChannelExpired(ch) && ch.isOpen() && !ch.eventLoop().isShuttingDown()) { - ch.close(); - } - - removeReadTimeoutHandler(pipeline); - } - - @Override - public void channelAcquired(Channel ch) throws Exception { - ChannelPipeline pipeline = ch.pipeline(); - if (pipeline.context(IdlingConnectionHandler.INSTANCE) != null) { - pipeline.remove(IdlingConnectionHandler.INSTANCE); - } - if (pipeline.context(HANDLER_IDLE_STATE) != null) { - pipeline.remove(HANDLER_IDLE_STATE); - } - if (pipeline.context(IdleTimeoutHandler.INSTANCE) != null) { - pipeline.remove(IdleTimeoutHandler.INSTANCE); - } - } - }; - } - - /** - * Adds a Netty listener that is instrumented by instrumenters given by managed or provided collection of - * the {@link InvocationInstrumenterFactory}. - * - * @param channelFuture The channel future - * @param listener The listener logic - * @param the type of value returned by the future - * @param the future type - * @return a Netty listener that is instrumented - */ - private > Future addInstrumentedListener( - Future channelFuture, GenericFutureListener listener - ) { - InvocationInstrumenter instrumenter = combineFactories(); - - return channelFuture.addListener(f -> { - try (Instrumentation ignored = instrumenter.newInstrumentation()) { - listener.operationComplete((C) f); - } - }); - } - private @NonNull InvocationInstrumenter combineFactories() { if (CollectionUtils.isEmpty(invocationInstrumenterFactories)) { return NOOP; @@ -2698,17 +1828,21 @@ private > Future addInstrumentedListener( .collect(Collectors.toList())); } - private static boolean isSecureScheme(String scheme) { + static boolean isSecureScheme(String scheme) { return io.micronaut.http.HttpRequest.SCHEME_HTTPS.equalsIgnoreCase(scheme) || SCHEME_WSS.equalsIgnoreCase(scheme); } private E customizeException(E exc) { + customizeException0(configuration, informationalServiceId, exc); + return exc; + } + + static void customizeException0(HttpClientConfiguration configuration, String informationalServiceId, HttpClientException exc) { if (informationalServiceId != null) { exc.setServiceId(informationalServiceId); } else if (configuration instanceof ServiceHttpClientConfiguration) { exc.setServiceId(((ServiceHttpClientConfiguration) configuration).getServiceId()); } - return exc; } @FunctionalInterface @@ -2716,333 +1850,10 @@ interface ThrowingBiConsumer { void accept(T1 t1, T2 t2) throws Exception; } - /** - * Initializes the HTTP client channel. - */ - protected class HttpClientInitializer extends ChannelInitializer { - - final SslContext sslContext; - final String host; - final int port; - final boolean stream; - final boolean proxy; - final boolean acceptsEvents; - Http2SettingsHandler settingsHandler; - private final Consumer contextConsumer; - private NettyClientCustomizer channelCustomizer; - - /** - * @param sslContext The ssl context - * @param host The host - * @param port The port - * @param stream Whether is stream - * @param proxy Is this a streaming proxy - * @param acceptsEvents Whether an event stream is accepted - * @param contextConsumer The context consumer - */ - protected HttpClientInitializer( - SslContext sslContext, - String host, - int port, - boolean stream, - boolean proxy, - boolean acceptsEvents, - Consumer contextConsumer) { - this.sslContext = sslContext; - this.stream = stream; - this.host = host; - this.port = port; - this.proxy = proxy; - this.acceptsEvents = acceptsEvents; - this.contextConsumer = contextConsumer; - } - - /** - * @param ch The channel - */ - @Override - protected void initChannel(SocketChannel ch) { - channelCustomizer = clientCustomizer.specializeForChannel(ch, NettyClientCustomizer.ChannelRole.CONNECTION); - ch.attr(CHANNEL_CUSTOMIZER_KEY).set(channelCustomizer); - - ChannelPipeline p = ch.pipeline(); - - Proxy proxy = configuration.resolveProxy(sslContext != null, host, port); - if (!Proxy.NO_PROXY.equals(proxy)) { - configureProxy(p, proxy); - } - - if (httpVersion == io.micronaut.http.HttpVersion.HTTP_2_0) { - final Http2Connection connection = new DefaultHttp2Connection(false); - final HttpToHttp2ConnectionHandlerBuilder builder = - newHttp2ConnectionHandlerBuilder(connection, configuration, stream); - - configuration.getLogLevel().ifPresent(logLevel -> { - try { - final io.netty.handler.logging.LogLevel nettyLevel = io.netty.handler.logging.LogLevel.valueOf( - logLevel.name() - ); - builder.frameLogger(new Http2FrameLogger(nettyLevel, DefaultHttpClient.class)); - } catch (IllegalArgumentException e) { - throw customizeException(new HttpClientException("Unsupported log level: " + logLevel)); - } - }); - HttpToHttp2ConnectionHandler connectionHandler = builder - .build(); - if (sslContext != null) { - configureHttp2Ssl(this, ch, sslContext, host, port, connectionHandler); - } else { - configureHttp2ClearText(this, ch, connectionHandler); - } - channelCustomizer.onInitialPipelineBuilt(); - } else { - if (stream) { - // for streaming responses we disable auto read - // so that the consumer is in charge of back pressure - ch.config().setAutoRead(false); - } - - configuration.getLogLevel().ifPresent(logLevel -> { - try { - final io.netty.handler.logging.LogLevel nettyLevel = io.netty.handler.logging.LogLevel.valueOf( - logLevel.name() - ); - p.addLast(new LoggingHandler(DefaultHttpClient.class, nettyLevel)); - } catch (IllegalArgumentException e) { - throw customizeException(new HttpClientException("Unsupported log level: " + logLevel)); - } - }); - - if (sslContext != null) { - SslHandler sslHandler = sslContext.newHandler(ch.alloc(), host, port); - sslHandler.setHandshakeTimeoutMillis(configuration.getSslConfiguration().getHandshakeTimeout().toMillis()); - p.addLast(ChannelPipelineCustomizer.HANDLER_SSL, sslHandler); - } - - // Pool connections require alternative timeout handling - if (poolMap == null) { - // read timeout settings are not applied to streamed requests. - // instead idle timeout settings are applied. - if (stream) { - Optional readIdleTime = configuration.getReadIdleTimeout(); - if (readIdleTime.isPresent()) { - Duration duration = readIdleTime.get(); - if (!duration.isNegative()) { - p.addLast(ChannelPipelineCustomizer.HANDLER_IDLE_STATE, new IdleStateHandler( - duration.toMillis(), - duration.toMillis(), - duration.toMillis(), - TimeUnit.MILLISECONDS - )); - } - } - } - } - - addHttp1Handlers(p); - channelCustomizer.onInitialPipelineBuilt(); - onStreamPipelineBuilt(); - } - } - - /** - * Called when the stream pipeline is fully set up (all handshakes completed) and we can - * start processing requests. - */ - void onStreamPipelineBuilt() { - channelCustomizer.onStreamPipelineBuilt(); - } - - private void addHttp1Handlers(ChannelPipeline p) { - p.addLast(ChannelPipelineCustomizer.HANDLER_HTTP_CLIENT_CODEC, new HttpClientCodec()); - - p.addLast(ChannelPipelineCustomizer.HANDLER_HTTP_DECODER, new HttpContentDecompressor()); - - int maxContentLength = configuration.getMaxContentLength(); - - if (!stream) { - p.addLast(ChannelPipelineCustomizer.HANDLER_HTTP_AGGREGATOR, new HttpObjectAggregator(maxContentLength) { - @Override - protected void finishAggregation(FullHttpMessage aggregated) throws Exception { - if (!HttpUtil.isContentLengthSet(aggregated)) { - if (aggregated.content().readableBytes() > 0) { - super.finishAggregation(aggregated); - } - } - } - }); - } - addEventStreamHandlerIfNecessary(p); - addFinalHandler(p); - for (ChannelPipelineListener pipelineListener : pipelineListeners) { - pipelineListener.onConnect(p); - } - } - - private void addEventStreamHandlerIfNecessary(ChannelPipeline p) { - // if the content type is a SSE event stream we add a decoder - // to delimit the content by lines (unless we are proxying the stream) - if (acceptsEventStream() && !proxy) { - p.addLast(ChannelPipelineCustomizer.HANDLER_MICRONAUT_SSE_EVENT_STREAM, new LineBasedFrameDecoder(configuration.getMaxContentLength(), true, true) { - - @Override - public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { - if (msg instanceof HttpContent) { - if (msg instanceof LastHttpContent) { - super.channelRead(ctx, msg); - } else { - Attribute streamKey = ctx.channel().attr(STREAM_KEY); - if (msg instanceof Http2Content) { - streamKey.set(((Http2Content) msg).stream()); - } - try { - super.channelRead(ctx, ((HttpContent) msg).content()); - } finally { - streamKey.set(null); - } - } - } else { - super.channelRead(ctx, msg); - } - } - }); - - p.addLast(ChannelPipelineCustomizer.HANDLER_MICRONAUT_SSE_CONTENT, new SimpleChannelInboundHandlerInstrumented(combineFactories(), false) { - - @Override - public boolean acceptInboundMessage(Object msg) { - return msg instanceof ByteBuf; - } - - @Override - protected void channelReadInstrumented(ChannelHandlerContext ctx, ByteBuf msg) { - try { - Attribute streamKey = ctx.channel().attr(STREAM_KEY); - Http2Stream http2Stream = streamKey.get(); - if (http2Stream != null) { - ctx.fireChannelRead(new DefaultHttp2Content(msg.copy(), http2Stream)); - } else { - ctx.fireChannelRead(new DefaultHttpContent(msg.copy())); - } - } finally { - msg.release(); - } - } - }); - - } - } - - /** - * Allows overriding the final handler added to the pipeline. - * - * @param pipeline The pipeline - */ - protected void addFinalHandler(ChannelPipeline pipeline) { - pipeline.addLast( - ChannelPipelineCustomizer.HANDLER_HTTP_STREAM, - new HttpStreamsClientHandler() { - @Override - public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception { - if (evt instanceof IdleStateEvent) { - // close the connection if it is idle for too long - ctx.close(); - } - super.userEventTriggered(ctx, evt); - } - - @Override - protected boolean isValidInMessage(Object msg) { - // ignore data on stream 1, that is the response to our initial upgrade request - return super.isValidInMessage(msg) && (sslContext != null || !discardH2cStream((HttpMessage) msg)); - } - }); - } - - private boolean acceptsEventStream() { - return this.acceptsEvents; - } - } - - /** - * Reads the first {@link Http2Settings} object and notifies a {@link io.netty.channel.ChannelPromise}. - */ - private final class Http2SettingsHandler extends - SimpleChannelInboundHandlerInstrumented { - private final ChannelPromise promise; - - /** - * Create new instance. - * - * @param promise Promise object used to notify when first settings are received - */ - Http2SettingsHandler(ChannelPromise promise) { - super(combineFactories()); - this.promise = promise; - } - - @Override - protected void channelReadInstrumented(ChannelHandlerContext ctx, Http2Settings msg) { - promise.setSuccess(); - - // Only care about the first settings message - ctx.pipeline().remove(this); - } - } - - /** - * A handler that triggers the cleartext upgrade to HTTP/2 by sending an initial HTTP request. - */ - private class UpgradeRequestHandler extends ChannelInboundHandlerAdapter { - - private final HttpClientInitializer initializer; - private final Http2SettingsHandler settingsHandler; - - /** - * Default constructor. - * - * @param initializer The initializer - */ - public UpgradeRequestHandler(HttpClientInitializer initializer) { - this.initializer = initializer; - this.settingsHandler = initializer.settingsHandler; - } - - /** - * @return The settings handler - */ - public Http2SettingsHandler getSettingsHandler() { - return settingsHandler; - } - - @Override - public void channelActive(ChannelHandlerContext ctx) { - // Done with this handler, remove it from the pipeline. - final ChannelPipeline pipeline = ctx.pipeline(); - - pipeline.addLast(ChannelPipelineCustomizer.HANDLER_HTTP2_SETTINGS, initializer.settingsHandler); - DefaultFullHttpRequest upgradeRequest = - new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, "/", Unpooled.EMPTY_BUFFER); - - // Set HOST header as the remote peer may require it. - InetSocketAddress remote = (InetSocketAddress) ctx.channel().remoteAddress(); - String hostString = remote.getHostString(); - if (hostString == null) { - hostString = remote.getAddress().getHostAddress(); - } - upgradeRequest.headers().set(HttpHeaderNames.HOST, hostString + ':' + remote.getPort()); - ctx.writeAndFlush(upgradeRequest); - - ctx.fireChannelActive(); - pipeline.remove(this); - initializer.addFinalHandler(pipeline); - } - } - /** * Key used for connection pooling and determining host/port. */ - private static final class RequestKey { + static final class RequestKey { private final String host; private final int port; private final boolean secure; @@ -3121,24 +1932,19 @@ public int hashCode() { /** * A Netty request writer. */ - protected class NettyRequestWriter { + private class NettyRequestWriter { private final HttpRequest nettyRequest; private final HttpPostRequestEncoder encoder; - private final String scheme; - private final boolean closeChannelAfterWrite; /** * @param scheme The scheme * @param nettyRequest The Netty request * @param encoder The encoder - * @param closeChannelAfterWrite Whether to close the after write */ - NettyRequestWriter(String scheme, HttpRequest nettyRequest, HttpPostRequestEncoder encoder, boolean closeChannelAfterWrite) { + NettyRequestWriter(HttpRequest nettyRequest, HttpPostRequestEncoder encoder) { this.nettyRequest = nettyRequest; this.encoder = encoder; - this.scheme = scheme; - this.closeChannelAfterWrite = closeChannelAfterWrite; } /** @@ -3146,70 +1952,30 @@ protected class NettyRequestWriter { * @param channelPool The channel pool * @param emitter The emitter */ - protected void writeAndClose(Channel channel, ChannelPool channelPool, FluxSink emitter) { + protected void write(Channel channel, boolean isSecure, FluxSink emitter) { final ChannelPipeline pipeline = channel.pipeline(); - if (httpVersion == io.micronaut.http.HttpVersion.HTTP_2_0) { - final boolean isSecure = sslContext != null && isSecureScheme(scheme); + if (connectionManager.httpVersion == io.micronaut.http.HttpVersion.HTTP_2_0) { if (isSecure) { nettyRequest.headers().add(AbstractNettyHttpRequest.HTTP2_SCHEME, HttpScheme.HTTPS); } else { nettyRequest.headers().add(AbstractNettyHttpRequest.HTTP2_SCHEME, HttpScheme.HTTP); } - - // for HTTP/2 over cleartext we have to wait for the protocol upgrade to complete - // so we get the Http2SettingsHandler and await receiving the Http2Settings object - // which indicates the protocol negotiation has completed successfully - final UpgradeRequestHandler upgradeRequestHandler = - (UpgradeRequestHandler) pipeline.get(ChannelPipelineCustomizer.HANDLER_HTTP2_UPGRADE_REQUEST); - final Http2SettingsHandler settingsHandler; - if (upgradeRequestHandler != null) { - settingsHandler = upgradeRequestHandler.getSettingsHandler(); - } else { - // upgrade request already received to handler must have been removed - // therefore the Http2SettingsHandler is in the pipeline - settingsHandler = (Http2SettingsHandler) pipeline.get(ChannelPipelineCustomizer.HANDLER_HTTP2_SETTINGS); - } - // if the settings handler is null and no longer in the pipeline, fall through - // since this means the HTTP/2 clear text upgrade completed, otherwise - // add a listener to the future that writes once the upgrade completes - if (settingsHandler != null) { - addInstrumentedListener(settingsHandler.promise, future -> { - if (future.isSuccess()) { - processRequestWrite(channel, channelPool, emitter, pipeline); - } else { - throw customizeException(new HttpClientException("HTTP/2 clear text upgrade failed to complete", future.cause())); - } - }); - return; - } } - processRequestWrite(channel, channelPool, emitter, pipeline); + processRequestWrite(channel, emitter, pipeline); } - private void processRequestWrite(Channel channel, ChannelPool channelPool, FluxSink emitter, ChannelPipeline pipeline) { - ChannelFuture channelFuture; + private void processRequestWrite(Channel channel, FluxSink emitter, ChannelPipeline pipeline) { + ChannelFuture writeFuture; if (encoder != null && encoder.isChunked()) { channel.attr(AttributeKey.valueOf(ChannelPipelineCustomizer.HANDLER_HTTP_CHUNK)).set(true); pipeline.addAfter(ChannelPipelineCustomizer.HANDLER_HTTP_STREAM, ChannelPipelineCustomizer.HANDLER_HTTP_CHUNK, new ChunkedWriteHandler()); channel.write(nettyRequest); - channelFuture = channel.writeAndFlush(encoder); + writeFuture = channel.writeAndFlush(encoder); } else { - channelFuture = channel.writeAndFlush(nettyRequest); + writeFuture = channel.writeAndFlush(nettyRequest); } - if (channelPool != null) { - closeChannelIfNecessary(channel, emitter, channelFuture, false); - } else { - closeChannelIfNecessary(channel, emitter, channelFuture, closeChannelAfterWrite); - } - } - - private void closeChannelIfNecessary( - Channel channel, - FluxSink emitter, - ChannelFuture channelFuture, - boolean closeChannelAfterWrite) { - addInstrumentedListener(channelFuture, f -> { + connectionManager.addInstrumentedListener(writeFuture, f -> { try { if (!f.isSuccess()) { if (!emitter.isCancelled()) { @@ -3224,9 +1990,6 @@ private void closeChannelIfNecessary( encoder.cleanFiles(); } channel.attr(AttributeKey.valueOf(ChannelPipelineCustomizer.HANDLER_HTTP_CHUNK)).set(null); - if (closeChannelAfterWrite) { - closeChannelAsync(channel); - } } }); } @@ -3255,7 +2018,7 @@ private abstract class BaseHttpResponseHandler finalRequest; public BaseHttpResponseHandler(Promise responsePromise, io.micronaut.http.HttpRequest parentRequest, io.micronaut.http.HttpRequest finalRequest) { - super(combineFactories()); + super(connectionManager.instrumenter); this.responsePromise = responsePromise; this.parentRequest = parentRequest; this.finalRequest = finalRequest; @@ -3360,13 +2123,11 @@ private class FullHttpResponseHandler extends BaseHttpResponseHandler bodyType; private final Argument errorType; - private final ChannelPool channelPool; - - private boolean keepAlive = true; + private final ConnectionManager.PoolHandle poolHandle; public FullHttpResponseHandler( Promise> responsePromise, - ChannelPool channelPool, + ConnectionManager.PoolHandle poolHandle, boolean secure, io.micronaut.http.HttpRequest request, Argument bodyType, @@ -3375,12 +2136,12 @@ public FullHttpResponseHandler( this.secure = secure; this.bodyType = bodyType; this.errorType = errorType; - this.channelPool = channelPool; + this.poolHandle = poolHandle; } @Override public boolean acceptInboundMessage(Object msg) { - return msg instanceof FullHttpResponse && (secure || !discardH2cStream((HttpMessage) msg)); + return msg instanceof FullHttpResponse; } @Override @@ -3407,7 +2168,7 @@ protected void channelReadInstrumented(ChannelHandlerContext channelHandlerConte } } if (!HttpUtil.isKeepAlive(fullResponse)) { - keepAlive = false; + poolHandle.taint(); } channelHandlerContext.pipeline().remove(this); } @@ -3528,31 +2289,13 @@ public Argument getErrorType(MediaType mediaType) { @Override public void handlerRemoved(ChannelHandlerContext ctx) { - if (channelPool != null) { - removeReadTimeoutHandler(ctx.pipeline()); - final Channel ch = ctx.channel(); - if (!keepAlive) { - ch.closeFuture().addListener((future -> - channelPool.release(ch) - )); - } else { - channelPool.release(ch); - } - } else { - // just close it to prevent any future reads without a handler registered - ctx.close(); - } - } - - @Override - public void handlerAdded(ChannelHandlerContext ctx) { - addReadTimeoutHandler(ctx.pipeline()); + poolHandle.release(); } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { super.exceptionCaught(ctx, cause); - keepAlive = false; + poolHandle.taint(); ctx.pipeline().remove(this); } } @@ -3584,18 +2327,6 @@ protected void buildResponse(Promise> promise, FullHttpR promise.trySuccess(new NettyStreamedHttpResponse<>(nettyResponse, httpStatus)); } - @Override - public void handlerAdded(ChannelHandlerContext ctx) throws Exception { - super.handlerAdded(ctx); - addReadTimeoutHandler(ctx.pipeline()); - } - - @Override - public void handlerRemoved(ChannelHandlerContext ctx) throws Exception { - super.handlerRemoved(ctx); - removeReadTimeoutHandler(ctx.pipeline()); - } - @Override protected Function>> makeRedirectHandler(io.micronaut.http.HttpRequest parentRequest, MutableHttpRequest redirectRequest) { return uri -> buildStreamExchange(parentRequest, redirectRequest, uri, null); diff --git a/http-client/src/main/java/io/micronaut/http/client/netty/websocket/NettyWebSocketClientHandler.java b/http-client/src/main/java/io/micronaut/http/client/netty/websocket/NettyWebSocketClientHandler.java index cb29635b755..5e825b38400 100644 --- a/http-client/src/main/java/io/micronaut/http/client/netty/websocket/NettyWebSocketClientHandler.java +++ b/http-client/src/main/java/io/micronaut/http/client/netty/websocket/NettyWebSocketClientHandler.java @@ -54,7 +54,8 @@ import io.netty.handler.timeout.IdleStateEvent; import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; -import reactor.core.publisher.FluxSink; +import reactor.core.publisher.Mono; +import reactor.core.publisher.Sinks; import java.util.Collections; import java.util.List; @@ -74,7 +75,7 @@ public class NettyWebSocketClientHandler extends AbstractNettyWebSocketHandle * Generic version of {@link #webSocketBean}. */ private final WebSocketBean genericWebSocketBean; - private final FluxSink emitter; + private final Sinks.One completion = Sinks.one(); private final UriMatchInfo matchInfo; private final MediaTypeCodecRegistry codecRegistry; private ChannelPromise handshakeFuture; @@ -91,20 +92,17 @@ public class NettyWebSocketClientHandler extends AbstractNettyWebSocketHandle * @param handshaker The handshaker * @param requestBinderRegistry The request binder registry * @param mediaTypeCodecRegistry The media type codec registry - * @param emitter The socket emitter */ public NettyWebSocketClientHandler( MutableHttpRequest request, WebSocketBean webSocketBean, final WebSocketClientHandshaker handshaker, RequestBinderRegistry requestBinderRegistry, - MediaTypeCodecRegistry mediaTypeCodecRegistry, - FluxSink emitter) { + MediaTypeCodecRegistry mediaTypeCodecRegistry) { super(null, requestBinderRegistry, mediaTypeCodecRegistry, webSocketBean, request, Collections.emptyMap(), handshaker.version(), handshaker.actualSubprotocol(), null); this.codecRegistry = mediaTypeCodecRegistry; this.handshaker = handshaker; this.genericWebSocketBean = webSocketBean; - this.emitter = emitter; this.webSocketStateBinderRegistry = new WebSocketStateBinderRegistry(requestBinderRegistry != null ? requestBinderRegistry : new DefaultRequestBinderRegistry(ConversionService.SHARED)); String clientPath = webSocketBean.getBeanDefinition().stringValue(ClientWebSocket.class).orElse(""); UriMatchTemplate matchTemplate = UriMatchTemplate.of(clientPath); @@ -162,7 +160,7 @@ protected void channelRead0(ChannelHandlerContext ctx, Object msg) { handshaker.finishHandshake(ch, res); } catch (Exception e) { try { - emitter.error(new WebSocketClientException("Error finishing WebSocket handshake: " + e.getMessage(), e)); + completion.tryEmitError(new WebSocketClientException("Error finishing WebSocket handshake: " + e.getMessage(), e)); } finally { // clientSession isn't set yet, so we do the close manually instead of through session.close ch.writeAndFlush(new CloseWebSocketFrame(CloseReason.INTERNAL_ERROR.getCode(), CloseReason.INTERNAL_ERROR.getReason())); @@ -191,7 +189,7 @@ protected void channelRead0(ChannelHandlerContext ctx, Object msg) { this.clientBodyArgument = null; try { - emitter.error(new WebSocketClientException("WebSocket @OnMessage method " + targetBean.getClass().getSimpleName() + "." + messageHandler.getExecutableMethod() + " should define exactly 1 message parameter, but found 2 possible candidates: " + unboundArguments)); + completion.tryEmitError(new WebSocketClientException("WebSocket @OnMessage method " + targetBean.getClass().getSimpleName() + "." + messageHandler.getExecutableMethod() + " should define exactly 1 message parameter, but found 2 possible candidates: " + unboundArguments)); } finally { if (getSession().isOpen()) { getSession().close(CloseReason.INTERNAL_ERROR); @@ -210,7 +208,7 @@ protected void channelRead0(ChannelHandlerContext ctx, Object msg) { this.clientPongArgument = null; try { - emitter.error(new WebSocketClientException("WebSocket @OnMessage pong handler method " + targetBean.getClass().getSimpleName() + "." + messageHandler.getExecutableMethod() + " should define exactly 1 pong message parameter, but found: " + unboundArguments)); + completion.tryEmitError(new WebSocketClientException("WebSocket @OnMessage pong handler method " + targetBean.getClass().getSimpleName() + "." + messageHandler.getExecutableMethod() + " should define exactly 1 pong message parameter, but found: " + unboundArguments)); } finally { if (getSession().isOpen()) { getSession().close(CloseReason.INTERNAL_ERROR); @@ -234,25 +232,22 @@ protected void channelRead0(ChannelHandlerContext ctx, Object msg) { Publisher reactiveSequence = Publishers.convertPublisher(result, Publisher.class); Flux.from(reactiveSequence).subscribe( o -> { }, - error -> emitter.error(new WebSocketSessionException("Error opening WebSocket client session: " + error.getMessage(), error)), + error -> completion.tryEmitError(new WebSocketSessionException("Error opening WebSocket client session: " + error.getMessage(), error)), () -> { - emitter.next(targetBean); - emitter.complete(); + completion.tryEmitValue(targetBean); } ); } else { - emitter.next(targetBean); - emitter.complete(); + completion.tryEmitValue(targetBean); } } catch (Throwable e) { - emitter.error(new WebSocketClientException("Error opening WebSocket client session: " + e.getMessage(), e)); + completion.tryEmitError(new WebSocketClientException("Error opening WebSocket client session: " + e.getMessage(), e)); if (getSession().isOpen()) { getSession().close(CloseReason.INTERNAL_ERROR); } } } else { - emitter.next(targetBean); - emitter.complete(); + completion.tryEmitValue(targetBean); } return; } @@ -298,4 +293,7 @@ public void exceptionCaught(final ChannelHandlerContext ctx, final Throwable cau super.exceptionCaught(ctx, cause); } + public final Mono getHandshakeCompletedMono() { + return completion.asMono(); + } } diff --git a/http-client/src/test/groovy/io/micronaut/http/client/ClientEventLoopGroupSpec.groovy b/http-client/src/test/groovy/io/micronaut/http/client/ClientEventLoopGroupSpec.groovy index 53ebe8790cf..1ccd3bf36f2 100644 --- a/http-client/src/test/groovy/io/micronaut/http/client/ClientEventLoopGroupSpec.groovy +++ b/http-client/src/test/groovy/io/micronaut/http/client/ClientEventLoopGroupSpec.groovy @@ -34,7 +34,7 @@ class ClientEventLoopGroupSpec extends Specification { HttpClient client = context.getBean(HttpClient) then: - client.group == context.getBean(EventLoopGroup, Qualifiers.byName("other")) + client.connectionManager.group == context.getBean(EventLoopGroup, Qualifiers.byName("other")) cleanup: context.close() diff --git a/http-client/src/test/groovy/io/micronaut/http/client/ConnectionTTLSpec.groovy b/http-client/src/test/groovy/io/micronaut/http/client/ConnectionTTLSpec.groovy index 3dc41e641ca..e2660359945 100644 --- a/http-client/src/test/groovy/io/micronaut/http/client/ConnectionTTLSpec.groovy +++ b/http-client/src/test/groovy/io/micronaut/http/client/ConnectionTTLSpec.groovy @@ -1,4 +1,4 @@ -package io.micronaut.http.client; +package io.micronaut.http.client import io.micronaut.context.ApplicationContext import io.micronaut.context.annotation.Requires @@ -9,7 +9,6 @@ import io.micronaut.http.annotation.Get import io.micronaut.runtime.server.EmbeddedServer import io.netty.channel.Channel import io.netty.channel.pool.AbstractChannelPoolMap -import reactor.core.publisher.Flux import spock.lang.AutoCleanup import spock.lang.Retry import spock.lang.Shared @@ -151,7 +150,7 @@ class ConnectionTTLSpec extends Specification { } Deque getQueuedChannels(HttpClient client) { - AbstractChannelPoolMap poolMap = client.poolMap + AbstractChannelPoolMap poolMap = client.connectionManager.poolMap Field mapField = AbstractChannelPoolMap.getDeclaredField("map") mapField.setAccessible(true) Map innerMap = mapField.get(poolMap) diff --git a/http-client/src/test/groovy/io/micronaut/http/client/IdleTimeoutSpec.groovy b/http-client/src/test/groovy/io/micronaut/http/client/IdleTimeoutSpec.groovy index 4bb8718dcdb..a65805a92bf 100644 --- a/http-client/src/test/groovy/io/micronaut/http/client/IdleTimeoutSpec.groovy +++ b/http-client/src/test/groovy/io/micronaut/http/client/IdleTimeoutSpec.groovy @@ -9,10 +9,10 @@ import io.micronaut.http.annotation.Get import io.micronaut.runtime.server.EmbeddedServer import io.netty.channel.Channel import io.netty.channel.pool.AbstractChannelPoolMap -import spock.lang.Retry -import spock.lang.Specification import spock.lang.AutoCleanup +import spock.lang.Retry import spock.lang.Shared +import spock.lang.Specification import spock.util.concurrent.PollingConditions import java.lang.reflect.Field @@ -112,7 +112,7 @@ class IdleTimeoutSpec extends Specification { } Deque getQueuedChannels(HttpClient client) { - AbstractChannelPoolMap poolMap = client.poolMap + AbstractChannelPoolMap poolMap = client.connectionManager.poolMap Field mapField = AbstractChannelPoolMap.getDeclaredField("map") mapField.setAccessible(true) Map innerMap = mapField.get(poolMap) diff --git a/http-client/src/test/groovy/io/micronaut/http/client/ReadTimeoutSpec.groovy b/http-client/src/test/groovy/io/micronaut/http/client/ReadTimeoutSpec.groovy index 09e09c45eb2..a04d8e137c0 100644 --- a/http-client/src/test/groovy/io/micronaut/http/client/ReadTimeoutSpec.groovy +++ b/http-client/src/test/groovy/io/micronaut/http/client/ReadTimeoutSpec.groovy @@ -310,7 +310,7 @@ class ReadTimeoutSpec extends Specification { } FixedChannelPool getPool(HttpClient client) { - AbstractChannelPoolMap poolMap = client.poolMap + AbstractChannelPoolMap poolMap = client.connectionManager.poolMap Field mapField = AbstractChannelPoolMap.getDeclaredField("map") mapField.setAccessible(true) Map innerMap = mapField.get(poolMap) From 416e2e7538fa7a4024767431eb289117228cf722 Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Thu, 15 Sep 2022 13:24:30 +0200 Subject: [PATCH 039/743] build: Update BOM to latest releases (#8008) * build: Micronaut AWS from 3.8.0 to 3.9.0 * build: Micronaut MQTT from 2.2.0 to 2.3.0 * build: Micronaut OpenAPI from 4.4.3 to 4.5.0 * build: Micronaut R2DBC from 3.0.1 to 3.1.0 * build: Micronaut Reactor from 2.3.1 to 2.4.0 * build: Micronaut RSS from 3.1.0 to 3.2.0 * build: Micronaut RxJava2 from 1.2.2 to 1.3.0 * build: Micronaut Test from 3.5.0 to 3.6.0 * build: test-resources from 1.0.1 to 1.1.1 * build: R2DBC from 3.1.0 to 4.0.0 --- gradle/libs.versions.toml | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index e8714a36a2c..4acfcb922bc 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -67,7 +67,7 @@ managed-methvin-directory-watcher = "0.16.1" managed-micrometer = "1.9.3" managed-micronaut-acme = "3.2.0" managed-micronaut-aot = "1.1.1" -managed-micronaut-aws = "3.8.0" +managed-micronaut-aws = "3.9.0" managed-micronaut-azure = "3.5.0" managed-micronaut-cache = "3.5.0" managed-micronaut-cassandra = "5.1.1" @@ -94,31 +94,31 @@ managed-micronaut-micrometer = "4.5.0" managed-micronaut-microstream = "1.1.0" managed-micronaut-liquibase = "5.4.1" managed-micronaut-mongo = "4.4.0" -managed-micronaut-mqtt = "2.2.0" +managed-micronaut-mqtt = "2.3.0" managed-micronaut-multitenancy = "4.2.0" managed-micronaut-neo4j = "5.2.0" managed-micronaut-nats = "3.1.0" managed-micronaut-netflix = "2.1.0" managed-micronaut-object-storage = "1.0.0" -managed-micronaut-openapi = "4.4.3" +managed-micronaut-openapi = "4.5.0" managed-micronaut-oraclecloud = "2.2.0" managed-micronaut-picocli = "4.3.0" managed-micronaut-problem = "2.4.1" managed-micronaut-rabbitmq = "3.3.0" -managed-micronaut-r2dbc = "3.0.1" -managed-micronaut-reactor = "2.3.1" +managed-micronaut-r2dbc = "4.0.0" +managed-micronaut-reactor = "2.4.0" managed-micronaut-redis = "5.3.0" -managed-micronaut-rss = "3.1.0" +managed-micronaut-rss = "3.2.0" managed-micronaut-rxjava1 = "1.0.0" -managed-micronaut-rxjava2 = "1.2.2" +managed-micronaut-rxjava2 = "1.3.0" managed-micronaut-rxjava3 = "2.3.0" managed-micronaut-security = "3.8.0" managed-micronaut-serialization = "1.3.2" managed-micronaut-servlet = "3.3.1" managed-micronaut-spring = "4.3.0" managed-micronaut-sql = "4.6.3" -managed-micronaut-test = "3.5.0" -managed-micronaut-test-resources = "1.0.1" +managed-micronaut-test = "3.6.0" +managed-micronaut-test-resources = "1.1.1" managed-micronaut-toml = "1.1.1" managed-micronaut-tracing = "4.3.0" managed-micronaut-tracing-legacy = "3.2.7" From 3b5a5e8b239c8c3d303fd2f91da1f6ef9ed5eff4 Mon Sep 17 00:00:00 2001 From: micronaut-build <65172877+micronaut-build@users.noreply.github.com> Date: Fri, 16 Sep 2022 02:12:07 -0400 Subject: [PATCH 040/743] build: bump micronaut-openapi to 4.5.1 (#8009) * Bump micronaut-openapi to 4.5.1 * Update libs.versions.toml Co-authored-by: Sergio del Amo --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 4acfcb922bc..459de03ef73 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -100,7 +100,7 @@ managed-micronaut-neo4j = "5.2.0" managed-micronaut-nats = "3.1.0" managed-micronaut-netflix = "2.1.0" managed-micronaut-object-storage = "1.0.0" -managed-micronaut-openapi = "4.5.0" +managed-micronaut-openapi = "4.5.1" managed-micronaut-oraclecloud = "2.2.0" managed-micronaut-picocli = "4.3.0" managed-micronaut-problem = "2.4.1" From e01dcf4e5c8cd47eee1af51180734798cabfe09c Mon Sep 17 00:00:00 2001 From: Jonas Konrad Date: Fri, 16 Sep 2022 12:54:40 +0200 Subject: [PATCH 041/743] Make AttributeKeys in ConnectionManager non-static to avoid native-image initialization (#8013) Fixes #8012 --- .../http/client/netty/ConnectionManager.java | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/http-client/src/main/java/io/micronaut/http/client/netty/ConnectionManager.java b/http-client/src/main/java/io/micronaut/http/client/netty/ConnectionManager.java index fe4f9769767..6024bd86932 100644 --- a/http-client/src/main/java/io/micronaut/http/client/netty/ConnectionManager.java +++ b/http-client/src/main/java/io/micronaut/http/client/netty/ConnectionManager.java @@ -128,20 +128,21 @@ */ @Internal final class ConnectionManager { - private static final AttributeKey CHANNEL_CUSTOMIZER_KEY = + final ChannelPoolMap poolMap; + final InvocationInstrumenter instrumenter; + final HttpVersion httpVersion; + + // not static to avoid build-time initialization by native image + private final AttributeKey CHANNEL_CUSTOMIZER_KEY = AttributeKey.valueOf("micronaut.http.customizer"); /** * Future on a pooled channel that will be completed when the channel has fully connected (e.g. * TLS handshake has completed). If unset, then no handshake is needed or it has already * completed. */ - private static final AttributeKey> STREAM_CHANNEL_INITIALIZED = + private final AttributeKey> STREAM_CHANNEL_INITIALIZED = AttributeKey.valueOf("micronaut.http.streamChannelInitialized"); - private static final AttributeKey STREAM_KEY = AttributeKey.valueOf("micronaut.http2.stream"); - - final ChannelPoolMap poolMap; - final InvocationInstrumenter instrumenter; - final HttpVersion httpVersion; + private final AttributeKey STREAM_KEY = AttributeKey.valueOf("micronaut.http2.stream"); private final Logger log; private EventLoopGroup group; From fc6e1c9b485015e94f0a963c032ae3f582a43e7a Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Fri, 16 Sep 2022 15:12:00 +0200 Subject: [PATCH 042/743] build: bump up Micronaut AWS to 3.9.1 (#8015) --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 459de03ef73..40955e49e7b 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -67,7 +67,7 @@ managed-methvin-directory-watcher = "0.16.1" managed-micrometer = "1.9.3" managed-micronaut-acme = "3.2.0" managed-micronaut-aot = "1.1.1" -managed-micronaut-aws = "3.9.0" +managed-micronaut-aws = "3.9.1" managed-micronaut-azure = "3.5.0" managed-micronaut-cache = "3.5.0" managed-micronaut-cassandra = "5.1.1" From 0ef7724f5fa11c1e335e38fa199b77397535ed41 Mon Sep 17 00:00:00 2001 From: micronaut-build <65172877+micronaut-build@users.noreply.github.com> Date: Fri, 16 Sep 2022 12:27:44 -0400 Subject: [PATCH 043/743] Bump micronaut-aws to 3.9.2 (#8019) --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 40955e49e7b..0ee9f081fd2 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -67,7 +67,7 @@ managed-methvin-directory-watcher = "0.16.1" managed-micrometer = "1.9.3" managed-micronaut-acme = "3.2.0" managed-micronaut-aot = "1.1.1" -managed-micronaut-aws = "3.9.1" +managed-micronaut-aws = "3.9.2" managed-micronaut-azure = "3.5.0" managed-micronaut-cache = "3.5.0" managed-micronaut-cassandra = "5.1.1" From 8e1be35e2b8087a907bc947bae0ca5f249fb3976 Mon Sep 17 00:00:00 2001 From: micronaut-build <65172877+micronaut-build@users.noreply.github.com> Date: Fri, 16 Sep 2022 12:43:56 -0400 Subject: [PATCH 044/743] Bump micronaut-views to 3.6.0 (#8018) --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 0ee9f081fd2..ace3ac37b99 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -122,7 +122,7 @@ managed-micronaut-test-resources = "1.1.1" managed-micronaut-toml = "1.1.1" managed-micronaut-tracing = "4.3.0" managed-micronaut-tracing-legacy = "3.2.7" -managed-micronaut-views = "3.5.0" +managed-micronaut-views = "3.6.0" managed-micronaut-xml = "3.1.0" managed-neo4j = "3.5.35" managed-neo4j-java-driver = "4.4.9" From 52855b4befe044d85067d5ce494ab9a15459a5a9 Mon Sep 17 00:00:00 2001 From: micronaut-build <65172877+micronaut-build@users.noreply.github.com> Date: Sat, 17 Sep 2022 04:21:36 -0400 Subject: [PATCH 045/743] Bump micronaut-test to 3.6.1 (#8024) --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index ace3ac37b99..1ea77eed8ad 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -117,7 +117,7 @@ managed-micronaut-serialization = "1.3.2" managed-micronaut-servlet = "3.3.1" managed-micronaut-spring = "4.3.0" managed-micronaut-sql = "4.6.3" -managed-micronaut-test = "3.6.0" +managed-micronaut-test = "3.6.1" managed-micronaut-test-resources = "1.1.1" managed-micronaut-toml = "1.1.1" managed-micronaut-tracing = "4.3.0" From 4434f45d6149998617d46a5dffed18e615f140b2 Mon Sep 17 00:00:00 2001 From: Graeme Rocher Date: Sun, 18 Sep 2022 07:45:28 +0200 Subject: [PATCH 046/743] fix runtime beans using interfaces or abstract classes (#8020) --- .../beans/RuntimeBeanDefinitionSpec.groovy | 23 +++++++++++++++++++ .../context/DefaultRuntimeBeanDefinition.java | 5 ++++ .../context/RuntimeBeanDefinition.java | 16 +++++++++++++ 3 files changed, 44 insertions(+) diff --git a/inject-java/src/test/groovy/io/micronaut/inject/beans/RuntimeBeanDefinitionSpec.groovy b/inject-java/src/test/groovy/io/micronaut/inject/beans/RuntimeBeanDefinitionSpec.groovy index 6cf467874a4..0d1090eeb93 100644 --- a/inject-java/src/test/groovy/io/micronaut/inject/beans/RuntimeBeanDefinitionSpec.groovy +++ b/inject-java/src/test/groovy/io/micronaut/inject/beans/RuntimeBeanDefinitionSpec.groovy @@ -101,6 +101,7 @@ import io.micronaut.inject.annotation.MutableAnnotationMetadata; @Singleton class Foo { + @Inject @Named("test2") public Bazz bazz; @Inject public Bar bar; @Inject @Named("another") public Bar another; } @@ -164,6 +165,18 @@ class RegistrarC { new Stuff() ) ); + registry.registerBeanDefinition( + RuntimeBeanDefinition.builder( + Bazz.class, + () -> new BazzImpl(1) + ).named("test").build() + ); + registry.registerBeanDefinition( + RuntimeBeanDefinition.builder( + Bazz.class, + () -> new BazzImpl(2) + ).named("test2").build() + ); } } @@ -178,10 +191,20 @@ class Bar { class Baz {} class Stuff {} + +interface Bazz {} +class BazzImpl implements Bazz { + public final int num; + BazzImpl(int num) { + this.num = num; + } +} ''') def foo = getBean(context, 'registerref.Foo') expect: foo.bar != null + foo.bazz != null + foo.bazz.num == 2 foo.bar.name == 'primary' foo.another.name == 'another' diff --git a/inject/src/main/java/io/micronaut/context/DefaultRuntimeBeanDefinition.java b/inject/src/main/java/io/micronaut/context/DefaultRuntimeBeanDefinition.java index 3482d17da9f..1a50274e16f 100644 --- a/inject/src/main/java/io/micronaut/context/DefaultRuntimeBeanDefinition.java +++ b/inject/src/main/java/io/micronaut/context/DefaultRuntimeBeanDefinition.java @@ -75,6 +75,11 @@ final class DefaultRuntimeBeanDefinition extends AbstractBeanContextCondition this.exposedTypes = exposedTypes; } + @Override + public boolean isAbstract() { + return false; + } + @Override @NonNull public Set> getExposedTypes() { diff --git a/inject/src/main/java/io/micronaut/context/RuntimeBeanDefinition.java b/inject/src/main/java/io/micronaut/context/RuntimeBeanDefinition.java index a38acb2b2c1..45f51ba4964 100644 --- a/inject/src/main/java/io/micronaut/context/RuntimeBeanDefinition.java +++ b/inject/src/main/java/io/micronaut/context/RuntimeBeanDefinition.java @@ -25,6 +25,7 @@ import io.micronaut.inject.BeanDefinition; import io.micronaut.inject.BeanDefinitionReference; import io.micronaut.inject.BeanFactory; +import io.micronaut.inject.qualifiers.Qualifiers; import java.lang.annotation.Annotation; import java.util.Objects; @@ -181,6 +182,21 @@ interface Builder { */ Builder qualifier(@Nullable Qualifier qualifier); + /** + * The qualifier to use. + * @param name The named qualifier to use. + * @return This builder + * @since 3.7.0 + */ + default Builder named(@Nullable String name) { + if (name == null) { + qualifier(null); + } else { + qualifier(Qualifiers.byName(name)); + } + return this; + } + /** * The scope to use. * @param scope The scope From 4ecb8966970c873d00fa79ddd5be0ef90df8cc00 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 18 Sep 2022 07:45:44 +0200 Subject: [PATCH 047/743] fix(deps): update managed-micrometer to v1.9.4 (#8026) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 1ea77eed8ad..9332de42841 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -64,7 +64,7 @@ managed-logback = "1.2.11" managed-lombok = "1.18.24" managed-maven-native-plugin = "0.9.13" managed-methvin-directory-watcher = "0.16.1" -managed-micrometer = "1.9.3" +managed-micrometer = "1.9.4" managed-micronaut-acme = "3.2.0" managed-micronaut-aot = "1.1.1" managed-micronaut-aws = "3.9.2" From 5d99c9774bc4ed89759d7323ace750f52f6529c6 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 18 Sep 2022 08:38:59 +0200 Subject: [PATCH 048/743] fix(deps): update dependency io.micronaut.mongodb:micronaut-mongo-bom to v4.5.0 (#8029) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 9332de42841..41fc974aa2e 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -93,7 +93,7 @@ managed-micronaut-kubernetes = "3.4.0" managed-micronaut-micrometer = "4.5.0" managed-micronaut-microstream = "1.1.0" managed-micronaut-liquibase = "5.4.1" -managed-micronaut-mongo = "4.4.0" +managed-micronaut-mongo = "4.5.0" managed-micronaut-mqtt = "2.3.0" managed-micronaut-multitenancy = "4.2.0" managed-micronaut-neo4j = "5.2.0" From c88d7e67a34f0153392e254dd127bc6f57d07733 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 18 Sep 2022 08:39:10 +0200 Subject: [PATCH 049/743] fix(deps): update dependency org.springframework:spring-core to v5.3.23 (#8025) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 41fc974aa2e..4f6226cd1db 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -136,7 +136,7 @@ managed-rxjava1-interop = "0.13.7" managed-slf4j = "1.7.36" managed-spock = "2.0-groovy-3.0" managed-spotbugs = "4.7.1" -managed-spring = "5.3.22" +managed-spring = "5.3.23" managed-springboot = "2.7.0" managed-swagger = "2.2.2" managed-validation = "2.0.1.Final" From 259a11c9964fc3fdf2c4f8d1a2e920c8606c53e4 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 18 Sep 2022 08:39:23 +0200 Subject: [PATCH 050/743] fix(deps): update dependency io.projectreactor:reactor-core to v3.4.23 (#8022) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 4f6226cd1db..db059354c1e 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -130,7 +130,7 @@ managed-netty = "4.1.82.Final" managed-reactive-pg-client = "0.11.4" managed-reactive-streams = "1.0.4" # This should be kept aligned with https://github.com/micronaut-projects/micronaut-reactor/blob/master/gradle.properties from the BOM -managed-reactor = "3.4.22" +managed-reactor = "3.4.23" managed-rxjava1 = "1.3.8" managed-rxjava1-interop = "0.13.7" managed-slf4j = "1.7.36" From daf46ac3ac8ea77e34c88e2d6a501b6659088d7a Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 18 Sep 2022 08:39:33 +0200 Subject: [PATCH 051/743] fix(deps): update dependency io.micronaut.oraclecloud:micronaut-oraclecloud-bom to v2.2.1 (#8021) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index db059354c1e..fc2bc04799b 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -101,7 +101,7 @@ managed-micronaut-nats = "3.1.0" managed-micronaut-netflix = "2.1.0" managed-micronaut-object-storage = "1.0.0" managed-micronaut-openapi = "4.5.1" -managed-micronaut-oraclecloud = "2.2.0" +managed-micronaut-oraclecloud = "2.2.1" managed-micronaut-picocli = "4.3.0" managed-micronaut-problem = "2.4.1" managed-micronaut-rabbitmq = "3.3.0" From c4f93ed97649a86a4da35f8d2b03177b886d8053 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 19 Sep 2022 07:05:59 +0200 Subject: [PATCH 052/743] fix(deps): update dependency org.apache.logging.log4j:log4j-core to v2.19.0 (#8034) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index fc2bc04799b..8f06d010fbe 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -22,7 +22,7 @@ jsr107 = "1.1.1" javax-el = "3.0.1-b12" javax-el-impl = "2.2.1-b05" logbook-netty = "2.14.0" -log4j = "2.18.0" +log4j = "2.19.0" selenium = "3.141.59" smallrye = "5.5.0" systemlambda = "1.2.1" From 83e77f1f9ea1c15a4094c299e0172d78d2a305df Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 19 Sep 2022 07:06:10 +0200 Subject: [PATCH 053/743] fix(deps): update groovy monorepo to v3.0.13 (#8032) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 8f06d010fbe..153d640bb7f 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -48,7 +48,7 @@ managed-gorm-hibernate = "7.3.0" managed-graal-sdk = "22.0.0.2" managed-graal = "22.2.0" managed-graal-svm = "22.0.0.2" -managed-groovy = "3.0.12" +managed-groovy = "3.0.13" managed-h2 = "1.4.200" managed-hystrix = "1.5.18" managed-jakarta-annotation-api = "2.1.1" From 77647ae53fc36c77656925a239aead5f8203475d Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 19 Sep 2022 07:06:20 +0200 Subject: [PATCH 054/743] fix(deps): update dependency io.micronaut.reactor:micronaut-reactor-bom to v2.4.1 (#8031) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 153d640bb7f..748687629eb 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -106,7 +106,7 @@ managed-micronaut-picocli = "4.3.0" managed-micronaut-problem = "2.4.1" managed-micronaut-rabbitmq = "3.3.0" managed-micronaut-r2dbc = "4.0.0" -managed-micronaut-reactor = "2.4.0" +managed-micronaut-reactor = "2.4.1" managed-micronaut-redis = "5.3.0" managed-micronaut-rss = "3.2.0" managed-micronaut-rxjava1 = "1.0.0" From d706c3f0b60181872b179ec24b1dd2b17c35ef46 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 19 Sep 2022 07:06:39 +0200 Subject: [PATCH 055/743] fix(deps): update dependency io.micronaut.problem:micronaut-problem-json-bom to v2.5.1 (#8030) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 748687629eb..b8ffdd7fd39 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -103,7 +103,7 @@ managed-micronaut-object-storage = "1.0.0" managed-micronaut-openapi = "4.5.1" managed-micronaut-oraclecloud = "2.2.1" managed-micronaut-picocli = "4.3.0" -managed-micronaut-problem = "2.4.1" +managed-micronaut-problem = "2.5.1" managed-micronaut-rabbitmq = "3.3.0" managed-micronaut-r2dbc = "4.0.0" managed-micronaut-reactor = "2.4.1" From ef54fda43300b4978f49f511395c5e579698fddb Mon Sep 17 00:00:00 2001 From: Graeme Rocher Date: Mon, 19 Sep 2022 07:07:01 +0200 Subject: [PATCH 056/743] feat: add an option to completely disable default property source loading (#8016) --- .../context/ApplicationContextBuilder.java | 10 ++++++ .../ApplicationContextConfiguration.java | 9 +++++ .../context/DefaultApplicationContext.java | 6 +++- .../DefaultApplicationContextBuilder.java | 13 +++++++ .../micronaut/context/DefaultBeanContext.java | 2 +- .../context/env/DefaultEnvironment.java | 35 +++++++++++-------- .../AnnotationMetadataQualifier.java | 2 +- .../ApplicationContextBuilderSpec.groovy | 17 +++++++++ .../docs/guide/config/propertySource.adoc | 5 ++- 9 files changed, 81 insertions(+), 18 deletions(-) diff --git a/inject/src/main/java/io/micronaut/context/ApplicationContextBuilder.java b/inject/src/main/java/io/micronaut/context/ApplicationContextBuilder.java index 1dae18d19da..01033c7e5fd 100644 --- a/inject/src/main/java/io/micronaut/context/ApplicationContextBuilder.java +++ b/inject/src/main/java/io/micronaut/context/ApplicationContextBuilder.java @@ -60,6 +60,16 @@ public interface ApplicationContextBuilder { return this; } + /** + * Specify whether the default set of property sources should be enabled (default is {@code true}). + * @param areEnabled Whether the default property sources are enabled + * @return This builder + * @since 3.7.0 + */ + default @NonNull ApplicationContextBuilder enableDefaultPropertySources(boolean areEnabled) { + return this; + } + /** * Specifies to eager init the given annotated types. * diff --git a/inject/src/main/java/io/micronaut/context/ApplicationContextConfiguration.java b/inject/src/main/java/io/micronaut/context/ApplicationContextConfiguration.java index f3e014e5464..00ed9676c0e 100644 --- a/inject/src/main/java/io/micronaut/context/ApplicationContextConfiguration.java +++ b/inject/src/main/java/io/micronaut/context/ApplicationContextConfiguration.java @@ -54,6 +54,15 @@ default List getDefaultEnvironments() { return Collections.emptyList(); } + /** + * Whether to load the default set of property sources. + * @return Returns {@code true} if the default set of property sources should be loaded. + * @since 3.7.0 + */ + default boolean isEnableDefaultPropertySources() { + return true; + } + /** * @return True if environment variables should contribute to configuration */ diff --git a/inject/src/main/java/io/micronaut/context/DefaultApplicationContext.java b/inject/src/main/java/io/micronaut/context/DefaultApplicationContext.java index df206109600..3230a4b5658 100644 --- a/inject/src/main/java/io/micronaut/context/DefaultApplicationContext.java +++ b/inject/src/main/java/io/micronaut/context/DefaultApplicationContext.java @@ -134,7 +134,11 @@ ApplicationContext registerSingleton(@NonNull Class type, @NonNull T sing */ protected @NonNull Environment createEnvironment(@NonNull ApplicationContextConfiguration configuration) { - return new RuntimeConfiguredEnvironment(configuration, isBootstrapEnabled(configuration)); + if (configuration.isEnableDefaultPropertySources()) { + return new RuntimeConfiguredEnvironment(configuration, isBootstrapEnabled(configuration)); + } else { + return new DefaultEnvironment(configuration); + } } private boolean isBootstrapEnabled(ApplicationContextConfiguration configuration) { diff --git a/inject/src/main/java/io/micronaut/context/DefaultApplicationContextBuilder.java b/inject/src/main/java/io/micronaut/context/DefaultApplicationContextBuilder.java index 81f4dd0058a..ae86c07eba2 100644 --- a/inject/src/main/java/io/micronaut/context/DefaultApplicationContextBuilder.java +++ b/inject/src/main/java/io/micronaut/context/DefaultApplicationContextBuilder.java @@ -66,6 +66,7 @@ public class DefaultApplicationContextBuilder implements ApplicationContextBuild private ClassPathResourceLoader classPathResourceLoader; private boolean allowEmptyProviders = false; private Boolean bootstrapEnvironment = null; + private boolean enableDefaultPropertySources = true; /** * Default constructor. @@ -87,6 +88,18 @@ public boolean isAllowEmptyProviders() { return allowEmptyProviders; } + @Override + @NonNull + public ApplicationContextBuilder enableDefaultPropertySources(boolean areEnabled) { + this.enableDefaultPropertySources = areEnabled; + return this; + } + + @Override + public boolean isEnableDefaultPropertySources() { + return enableDefaultPropertySources; + } + @NonNull @Override public ApplicationContextBuilder eagerInitAnnotated(Class... annotations) { diff --git a/inject/src/main/java/io/micronaut/context/DefaultBeanContext.java b/inject/src/main/java/io/micronaut/context/DefaultBeanContext.java index 61a138fbf1a..b5066573566 100644 --- a/inject/src/main/java/io/micronaut/context/DefaultBeanContext.java +++ b/inject/src/main/java/io/micronaut/context/DefaultBeanContext.java @@ -368,7 +368,7 @@ public synchronized BeanContext start() { */ @Override public synchronized BeanContext stop() { - if (terminating.compareAndSet(false, true)) { + if (terminating.compareAndSet(false, true) && isRunning()) { if (LOG.isDebugEnabled()) { LOG.debug("Stopping BeanContext"); } diff --git a/inject/src/main/java/io/micronaut/context/env/DefaultEnvironment.java b/inject/src/main/java/io/micronaut/context/env/DefaultEnvironment.java index 8de20873d3b..b83fd78d29f 100644 --- a/inject/src/main/java/io/micronaut/context/env/DefaultEnvironment.java +++ b/inject/src/main/java/io/micronaut/context/env/DefaultEnvironment.java @@ -150,7 +150,7 @@ public DefaultEnvironment(@NonNull ApplicationContextConfiguration configuration packages.add(aPackage); } - environments.removeAll(specifiedNames); + specifiedNames.forEach(environments::remove); environments.addAll(specifiedNames); this.classLoader = configuration.getClassLoader(); this.annotationScanner = createAnnotationScanner(classLoader); @@ -357,7 +357,7 @@ protected boolean shouldDeduceEnvironments() { } return deduceEnvironments; - } else { + } else if (configuration.isEnableDefaultPropertySources()) { String deduceProperty = CachedEnvironment.getProperty(Environment.DEDUCE_ENVIRONMENT_PROPERTY); String deduceEnv = CachedEnvironment.getenv(Environment.DEDUCE_ENVIRONMENT_ENV); @@ -380,6 +380,8 @@ protected boolean shouldDeduceEnvironments() { } return deduceDefault; } + } else { + return false; } } @@ -405,18 +407,23 @@ protected String getPropertySourceRootName() { */ protected void readPropertySources(String name) { refreshablePropertySources.clear(); - List propertySources = readPropertySourceList(name); - addDefaultPropertySources(propertySources); - String propertySourcesSystemProperty = CachedEnvironment.getProperty(Environment.PROPERTY_SOURCES_KEY); - if (propertySourcesSystemProperty != null) { - propertySources.addAll(readPropertySourceListFromFiles(propertySourcesSystemProperty)); - } - String propertySourcesEnv = readPropertySourceListKeyFromEnvironment(); - if (propertySourcesEnv != null) { - propertySources.addAll(readPropertySourceListFromFiles(propertySourcesEnv)); - } - refreshablePropertySources.addAll(propertySources); - readConstantPropertySources(name, propertySources); + List propertySources; + if (configuration.isEnableDefaultPropertySources()) { + propertySources = readPropertySourceList(name); + addDefaultPropertySources(propertySources); + String propertySourcesSystemProperty = CachedEnvironment.getProperty(Environment.PROPERTY_SOURCES_KEY); + if (propertySourcesSystemProperty != null) { + propertySources.addAll(readPropertySourceListFromFiles(propertySourcesSystemProperty)); + } + String propertySourcesEnv = readPropertySourceListKeyFromEnvironment(); + if (propertySourcesEnv != null) { + propertySources.addAll(readPropertySourceListFromFiles(propertySourcesEnv)); + } + refreshablePropertySources.addAll(propertySources); + readConstantPropertySources(name, propertySources); + } else { + propertySources = new ArrayList<>(this.propertySources.size()); + } propertySources.addAll(this.propertySources.values()); OrderUtil.sort(propertySources); diff --git a/inject/src/main/java/io/micronaut/inject/qualifiers/AnnotationMetadataQualifier.java b/inject/src/main/java/io/micronaut/inject/qualifiers/AnnotationMetadataQualifier.java index c175aa9ebea..96ba24c0fbb 100644 --- a/inject/src/main/java/io/micronaut/inject/qualifiers/AnnotationMetadataQualifier.java +++ b/inject/src/main/java/io/micronaut/inject/qualifiers/AnnotationMetadataQualifier.java @@ -48,7 +48,7 @@ * @since 1.0 */ @Internal -class AnnotationMetadataQualifier implements Qualifier { +final class AnnotationMetadataQualifier implements Qualifier { @NonNull final String annotationName; diff --git a/inject/src/test/groovy/io/micronaut/context/ApplicationContextBuilderSpec.groovy b/inject/src/test/groovy/io/micronaut/context/ApplicationContextBuilderSpec.groovy index 77c23429c15..fe777cf7195 100644 --- a/inject/src/test/groovy/io/micronaut/context/ApplicationContextBuilderSpec.groovy +++ b/inject/src/test/groovy/io/micronaut/context/ApplicationContextBuilderSpec.groovy @@ -1,9 +1,26 @@ package io.micronaut.context +import io.micronaut.context.env.PropertySource import spock.lang.Specification class ApplicationContextBuilderSpec extends Specification { + void "test disable default property sources"() { + given: + ApplicationContextBuilder builder = ApplicationContext.builder() + builder.enableDefaultPropertySources(false) + .propertySources(PropertySource.of("custom", [foo:'bar'])) + when: + def ctx = builder.build().start() + + then: + ctx.environment.propertySources.size() == 1 + ctx.environment.propertySources.first().name == 'custom' + + cleanup: + ctx.close() + } + void "test context configuration"() { given: ApplicationContextBuilder builder = ApplicationContext.builder() diff --git a/src/main/docs/guide/config/propertySource.adoc b/src/main/docs/guide/config/propertySource.adoc index 54dc30f0238..91fbba9de2d 100644 --- a/src/main/docs/guide/config/propertySource.adoc +++ b/src/main/docs/guide/config/propertySource.adoc @@ -21,6 +21,9 @@ Micronaut by default contains `PropertySourceLoader` implementations that load p TIP: `.properties`, `.json`, `.yml` are supported out of the box. For Groovy users `.groovy` is supported as well. +Note that if you want full control of where your application loads configuration from you can disable the default `PropertySourceLoader` implementations listed above by calling the `enableDefaultPropertySources(false)` method of the api:context.ApplicationContextBuilder[] interface when starting your application. + +In this case only explicit api:context.env.PropertySource[] instances that you add via the `propertySources(..)` method of the api:context.ApplicationContextBuilder[] interface will be used. === Supplying Configuration via Command Line @@ -54,7 +57,7 @@ public class Application { === Secrets and Sensitive Configuration -It is important to note that it is not recommended to store sensitive configuration such as passwords and tokens within configuration files that can potentially be checked into source control systems. +It is important to note that it is not recommended to store sensitive configuration such as passwords and tokens within configuration files that can potentially be checked into source control systems. It is good practise to instead externalize sensitive configuration completely from the application code using preferably a external secret manager system (there are many options here, many provided by Cloud providers) or environment variables that are set during the deployment of the application. You can also use property placeholders (see the following section), to customize names of the environment variables to use and supply default values: From e0f67a4a169234c13a6d38c436bd3415353b969c Mon Sep 17 00:00:00 2001 From: James Kleeh Date: Mon, 19 Sep 2022 01:07:36 -0400 Subject: [PATCH 057/743] fix: Register a binder for parts that returns unsatisfied (#8004) --- .../bind/DefaultRequestBinderRegistry.java | 3 ++ .../bind/binders/PartAnnotationBinder.java | 47 +++++++++++++++++++ 2 files changed, 50 insertions(+) create mode 100644 http/src/main/java/io/micronaut/http/bind/binders/PartAnnotationBinder.java diff --git a/http/src/main/java/io/micronaut/http/bind/DefaultRequestBinderRegistry.java b/http/src/main/java/io/micronaut/http/bind/DefaultRequestBinderRegistry.java index fc25be32c0f..7c8a6f34fb6 100644 --- a/http/src/main/java/io/micronaut/http/bind/DefaultRequestBinderRegistry.java +++ b/http/src/main/java/io/micronaut/http/bind/DefaultRequestBinderRegistry.java @@ -262,6 +262,9 @@ protected void registerDefaultAnnotationBinders(Map, RequestBeanAnnotationBinder requestBeanAnnotationBinder = new RequestBeanAnnotationBinder<>(this, conversionService); byAnnotation.put(requestBeanAnnotationBinder.getAnnotationType(), requestBeanAnnotationBinder); + PartAnnotationBinder partAnnotationBinder = new PartAnnotationBinder<>(conversionService); + byAnnotation.put(partAnnotationBinder.getAnnotationType(), partAnnotationBinder); + if (KOTLIN_COROUTINES_SUPPORTED) { ContinuationArgumentBinder continuationArgumentBinder = new ContinuationArgumentBinder(); byType.put(continuationArgumentBinder.argumentType().typeHashCode(), continuationArgumentBinder); diff --git a/http/src/main/java/io/micronaut/http/bind/binders/PartAnnotationBinder.java b/http/src/main/java/io/micronaut/http/bind/binders/PartAnnotationBinder.java new file mode 100644 index 00000000000..dc4ef36ac69 --- /dev/null +++ b/http/src/main/java/io/micronaut/http/bind/binders/PartAnnotationBinder.java @@ -0,0 +1,47 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.http.bind.binders; + +import io.micronaut.core.bind.annotation.AbstractAnnotatedArgumentBinder; +import io.micronaut.core.convert.ArgumentConversionContext; +import io.micronaut.core.convert.ConversionService; +import io.micronaut.http.HttpRequest; +import io.micronaut.http.annotation.Part; + +/** + * Skips binding parts because they should be handled by a multipart processor. + * + * @param The part type + * @author James Kleeh + * @since 3.6.4 + */ +public class PartAnnotationBinder extends AbstractAnnotatedArgumentBinder> implements AnnotatedRequestArgumentBinder { + + public PartAnnotationBinder(ConversionService conversionService) { + super(conversionService); + } + + @Override + public BindingResult bind(ArgumentConversionContext context, HttpRequest source) { + //noinspection unchecked + return BindingResult.UNSATISFIED; + } + + @Override + public Class getAnnotationType() { + return Part.class; + } +} From aceecd6372214be8bf99cce96f8d3def4106e69c Mon Sep 17 00:00:00 2001 From: micronaut-build <65172877+micronaut-build@users.noreply.github.com> Date: Mon, 19 Sep 2022 08:02:48 -0400 Subject: [PATCH 058/743] Bump micronaut-gcp to 4.5.1 (#8035) --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index b8ffdd7fd39..4d5113fee3e 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -78,7 +78,7 @@ managed-micronaut-discovery = "3.2.0" managed-micronaut-elasticsearch = "4.3.0" managed-micronaut-email = "1.3.2" managed-micronaut-flyway = "5.4.1" -managed-micronaut-gcp = "4.5.0" +managed-micronaut-gcp = "4.5.1" managed-micronaut-graphql = "3.1.0" managed-micronaut-groovy = "3.3.0" managed-micronaut-grpc = "3.3.1" From ed3cee7edde18dac6d6e40e43613848d4f158c5b Mon Sep 17 00:00:00 2001 From: Jonas Konrad Date: Mon, 19 Sep 2022 16:42:11 +0200 Subject: [PATCH 059/743] Allow matching `@Produces(ALL)` for ambiguous routes (#8039) If the client sends a specific content type, but the controller is annotated `@Produces(ALL)`, the matching logic for ambiguous routes would bail out and match no routes. This patch removes that logic, so that the ambiguity resolution can go on and match e.g. based on path variables. Fixes #7344 --- .../netty/ContentNegotiationSpec.groovy | 53 ++++++++++++++++++- .../micronaut/web/router/DefaultRouter.java | 24 +++++++-- 2 files changed, 71 insertions(+), 6 deletions(-) diff --git a/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/ContentNegotiationSpec.groovy b/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/ContentNegotiationSpec.groovy index 71fbbd6722f..7fa8e5f935a 100644 --- a/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/ContentNegotiationSpec.groovy +++ b/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/ContentNegotiationSpec.groovy @@ -6,7 +6,11 @@ import io.micronaut.http.HttpRequest import io.micronaut.http.HttpResponse import io.micronaut.http.HttpStatus import io.micronaut.http.MediaType -import io.micronaut.http.annotation.* +import io.micronaut.http.annotation.Controller +import io.micronaut.http.annotation.Error +import io.micronaut.http.annotation.Get +import io.micronaut.http.annotation.Post +import io.micronaut.http.annotation.Produces import io.micronaut.http.client.HttpClient import io.micronaut.http.client.annotation.Client import io.micronaut.http.client.exceptions.HttpClientResponseException @@ -18,7 +22,9 @@ import reactor.core.publisher.Flux import spock.lang.Specification import spock.lang.Unroll -import static io.micronaut.http.server.netty.ContentNegotiationSpec.NegotiatingController.* +import static io.micronaut.http.server.netty.ContentNegotiationSpec.NegotiatingController.JSON +import static io.micronaut.http.server.netty.ContentNegotiationSpec.NegotiatingController.TEXT +import static io.micronaut.http.server.netty.ContentNegotiationSpec.NegotiatingController.XML @MicronautTest class ContentNegotiationSpec extends Specification { @@ -137,6 +143,30 @@ class ContentNegotiationSpec extends Specification { MediaType.APPLICATION_JSON_TYPE | HttpStatus.BAD_REQUEST | MediaType.APPLICATION_JSON_TYPE | '{"message":"not a good request"}' } + @Unroll + void 'test produces any accepts #accept'() { + given: + def request = HttpRequest.GET('/negotiate/any/foo') + if (accept != null) { + request = request.accept(accept) + } + + when: + HttpResponse response = client.toBlocking().exchange(request, String) + + then: + response.getContentType().get() == expectedContentType + response.body() == expectedBody + + where: + accept | expectedContentType | expectedBody + null | MediaType.TEXT_PLAIN_TYPE | TEXT + MediaType.APPLICATION_XML_TYPE | MediaType.APPLICATION_XML_TYPE | XML + MediaType.APPLICATION_JSON_TYPE | MediaType.APPLICATION_JSON_TYPE | JSON + MediaType.TEXT_PLAIN_TYPE | MediaType.TEXT_PLAIN_TYPE | TEXT + MediaType.ALL_TYPE | MediaType.TEXT_PLAIN_TYPE | TEXT + } + @Controller("/negotiate") static class NegotiatingController { @@ -168,6 +198,25 @@ class ContentNegotiationSpec extends Specification { return TEXT } + @Get("/any/foo") + @Produces(MediaType.ALL) + HttpResponse any(HttpRequest req) { + def accept = req.accept() + if (accept.contains(MediaType.APPLICATION_JSON_TYPE)) { + return HttpResponse.ok(JSON).contentType(MediaType.APPLICATION_JSON_TYPE) + } else if (accept.contains(MediaType.APPLICATION_XML_TYPE)) { + return HttpResponse.ok(XML).contentType(MediaType.APPLICATION_XML_TYPE) + } else { + return HttpResponse.ok(TEXT).contentType(MediaType.TEXT_PLAIN_TYPE) + } + } + + @Get("/any/{someVariable}") + @Produces(MediaType.ALL) + HttpResponse anyWithVariable() { + throw new UnsupportedOperationException() + } + @Post(value = "/process", processes = [MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML]) Person process(Person person) { diff --git a/router/src/main/java/io/micronaut/web/router/DefaultRouter.java b/router/src/main/java/io/micronaut/web/router/DefaultRouter.java index 181c2990c7c..542c84f09ef 100644 --- a/router/src/main/java/io/micronaut/web/router/DefaultRouter.java +++ b/router/src/main/java/io/micronaut/web/router/DefaultRouter.java @@ -22,7 +22,11 @@ import io.micronaut.core.reflect.ClassUtils; import io.micronaut.core.util.CollectionUtils; import io.micronaut.core.util.SupplierUtil; -import io.micronaut.http.*; +import io.micronaut.http.HttpAttributes; +import io.micronaut.http.HttpMethod; +import io.micronaut.http.HttpRequest; +import io.micronaut.http.HttpStatus; +import io.micronaut.http.MediaType; import io.micronaut.http.annotation.Filter; import io.micronaut.http.annotation.FilterMatcher; import io.micronaut.http.filter.FilterPatternStyle; @@ -34,7 +38,18 @@ import jakarta.inject.Singleton; import java.net.URI; -import java.util.*; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; import java.util.function.Predicate; import java.util.function.Supplier; import java.util.stream.Stream; @@ -66,7 +81,7 @@ public class DefaultRouter implements Router, HttpServerFilterResolver List> findAllClosest(@NonNull HttpRequest r if (routeCount <= 1) { return uriRoutes; } + // if there are multiple routes, try to resolve the ambiguity if (CollectionUtils.isNotEmpty(acceptedProducedTypes)) { // take the highest priority accepted type @@ -213,7 +229,7 @@ public List> findAllClosest(@NonNull HttpRequest r mostSpecific.add(routeMatch); } } - if (!mostSpecific.isEmpty() || !acceptedProducedTypes.contains(MediaType.ALL_TYPE)) { + if (!mostSpecific.isEmpty()) { uriRoutes = mostSpecific; } } From 89e2c4d9e8ebdfb292a7b1291f8c0edd775bc82e Mon Sep 17 00:00:00 2001 From: micronaut-build <65172877+micronaut-build@users.noreply.github.com> Date: Mon, 19 Sep 2022 13:16:04 -0400 Subject: [PATCH 060/743] Bump micronaut-data to 3.8.0 (#8044) --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 4d5113fee3e..15c5a758304 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -73,7 +73,7 @@ managed-micronaut-cache = "3.5.0" managed-micronaut-cassandra = "5.1.1" managed-micronaut-coherence = "3.5.1" managed-micronaut-crac = "1.0.0" -managed-micronaut-data = "3.7.3" +managed-micronaut-data = "3.8.0" managed-micronaut-discovery = "3.2.0" managed-micronaut-elasticsearch = "4.3.0" managed-micronaut-email = "1.3.2" From 385175a79e117ad336759807acdbe72e7f6c578a Mon Sep 17 00:00:00 2001 From: micronaut-build <65172877+micronaut-build@users.noreply.github.com> Date: Mon, 19 Sep 2022 13:17:27 -0400 Subject: [PATCH 061/743] Bump micronaut-micrometer to 4.6.1 (#8043) * Bump micronaut-micrometer to 4.6.1 * Update libs.versions.toml Co-authored-by: Sergio del Amo --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 15c5a758304..c89187258c3 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -90,7 +90,7 @@ managed-micronaut-jmx = "3.1.0" managed-micronaut-kafka = "4.4.0" managed-micronaut-kotlin = "3.2.2" managed-micronaut-kubernetes = "3.4.0" -managed-micronaut-micrometer = "4.5.0" +managed-micronaut-micrometer = "4.6.1" managed-micronaut-microstream = "1.1.0" managed-micronaut-liquibase = "5.4.1" managed-micronaut-mongo = "4.5.0" From 6d9b69ca36444789fcaac896b278e9f771a5cfe8 Mon Sep 17 00:00:00 2001 From: micronaut-build <65172877+micronaut-build@users.noreply.github.com> Date: Mon, 19 Sep 2022 13:17:41 -0400 Subject: [PATCH 062/743] Bump micronaut-tracing to 4.3.1 (#8040) --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index c89187258c3..541bb9827fe 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -120,7 +120,7 @@ managed-micronaut-sql = "4.6.3" managed-micronaut-test = "3.6.1" managed-micronaut-test-resources = "1.1.1" managed-micronaut-toml = "1.1.1" -managed-micronaut-tracing = "4.3.0" +managed-micronaut-tracing = "4.3.1" managed-micronaut-tracing-legacy = "3.2.7" managed-micronaut-views = "3.6.0" managed-micronaut-xml = "3.1.0" From 711f967ff1c354869bb87518cdb3f7ef0da556ab Mon Sep 17 00:00:00 2001 From: micronaut-build <65172877+micronaut-build@users.noreply.github.com> Date: Mon, 19 Sep 2022 13:17:55 -0400 Subject: [PATCH 063/743] Bump micronaut-groovy to 3.3.1 (#8036) * Bump micronaut-groovy to 3.3.1 * Update libs.versions.toml Co-authored-by: Sergio del Amo --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 541bb9827fe..b6dc07a41f5 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -80,7 +80,7 @@ managed-micronaut-email = "1.3.2" managed-micronaut-flyway = "5.4.1" managed-micronaut-gcp = "4.5.1" managed-micronaut-graphql = "3.1.0" -managed-micronaut-groovy = "3.3.0" +managed-micronaut-groovy = "3.3.1" managed-micronaut-grpc = "3.3.1" managed-micronaut-hibernate-validator = "3.2.0" managed-micronaut-ignite = "1.0.0.RC1" From 5253d90482134f6fe4188c71002bfee3ea7f8a5b Mon Sep 17 00:00:00 2001 From: altro3 Date: Tue, 20 Sep 2022 02:11:42 +0700 Subject: [PATCH 064/743] Fix incorrect getPropertiesFromGettersAndSetters groovy visitor for interfaces (#8028) --- .../groovy/visitor/GroovyClassElement.java | 3 +- .../inject/visitor/ClassElementSpec.groovy | 88 ++++++++++++------- 2 files changed, 57 insertions(+), 34 deletions(-) diff --git a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyClassElement.java b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyClassElement.java index 6ea977d09b1..efec5f1f4d7 100644 --- a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyClassElement.java +++ b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyClassElement.java @@ -922,6 +922,7 @@ private List getPropertiesFromGettersAndSetters(List getPropertiesFromGettersAndSetters(List properties = element.getBeanProperties() + expect: + properties + properties.size() == 3 + } + @Unroll void "test throws declarations on method with generics"() { given: @@ -162,24 +184,24 @@ abstract class Test extends SuperType implements AnotherInterface, SomeInt { protected boolean t1; private boolean t2; - + private boolean privateMethod() { return true; } - + boolean packagePrivateMethod() { return true; } - + @java.lang.Override public boolean publicMethod() { return true; } - + static boolean staticMethod() { return true; } - + abstract boolean unimplementedMethod(); } @@ -189,15 +211,15 @@ abstract class SuperType { private boolean privateMethod() { return true; } - + public boolean publicMethod() { return true; } - + public boolean otherSuper() { return true; } - + abstract boolean unimplementedSuperMethod(); } @@ -205,13 +227,13 @@ interface SomeInt { default boolean itfeMethod() { return true; } - + boolean publicMethod(); } interface AnotherInterface { boolean publicMethod(); - + boolean unimplementedItfeMethod(); } ''') @@ -244,19 +266,19 @@ class Test extends SuperType implements AnotherInterface, SomeInt { protected boolean t1; private boolean t2; - + private boolean privateMethod() { return true; } - + boolean packagePrivateMethod() { return true; } - + public boolean publicMethod() { return true; } - + static boolean staticMethod() { return true; } @@ -268,11 +290,11 @@ class SuperType { private boolean privateMethod() { return true; } - + public boolean publicMethod() { return true; } - + public boolean otherSuper() { return true; } @@ -282,7 +304,7 @@ interface SomeInt { default boolean itfeMethod() { return true; } - + boolean publicMethod(); } @@ -344,12 +366,12 @@ import javax.inject.Inject; @Controller("/test") public class TestController implements java.util.function.Supplier { - + @Get("/getMethod") public String get() { return null; } - + } @@ -369,12 +391,12 @@ import javax.inject.Inject; @Controller("/test") public class TestController { - + @Get("/getMethod") public String[] getMethod(int[] argument) { return null; } - + } ''') @@ -397,12 +419,12 @@ import javax.inject.Inject; @Controller("/test") public class TestController { - + @Get("/getMethod") public HttpMethod getMethod(HttpMethod argument) { return null; } - + } ''') @@ -425,12 +447,12 @@ import javax.inject.Inject; @Controller("/test") public class TestController { - + @Get("/getMethod") public int getMethod(long argument) { return 0; } - + } ''') @@ -451,12 +473,12 @@ import javax.inject.Inject; @Controller("/test") public class TestController { - + @Get("/getMethod") public T getMethod(T argument) { return null; } - + } @@ -479,12 +501,12 @@ import javax.inject.Inject; @Controller("/test") public class TestController { - + @Get("/getMethod") public T[] getMethod(T[] argument) { return null; } - + } @@ -509,12 +531,12 @@ import javax.inject.Inject; @Controller("/test") public class TestController { - + @Get("/getMethod") public T getMethod(T argument) { return null; } - + } @@ -537,12 +559,12 @@ import javax.inject.Inject; @Controller("/test") public class TestController { - + @Get("/getMethod") public java.util.List getMethod(java.util.Set argument) { return null; } - + } From 2282474a65ed24b0f1f5b3ab65246bc7633b4b6c Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Mon, 19 Sep 2022 22:37:55 +0200 Subject: [PATCH 065/743] build: bump up Micronaut Spring, SQL and Tracing (#8045) * build: Micronaut Spring 4.3.1 * build: Micronaut SQL 4.7.0 * build: Micronaut Tracing 4.4.0 --- gradle/libs.versions.toml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index b6dc07a41f5..cbc8c6d423e 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -115,12 +115,12 @@ managed-micronaut-rxjava3 = "2.3.0" managed-micronaut-security = "3.8.0" managed-micronaut-serialization = "1.3.2" managed-micronaut-servlet = "3.3.1" -managed-micronaut-spring = "4.3.0" -managed-micronaut-sql = "4.6.3" +managed-micronaut-spring = "4.3.1" +managed-micronaut-sql = "4.7.0" managed-micronaut-test = "3.6.1" managed-micronaut-test-resources = "1.1.1" managed-micronaut-toml = "1.1.1" -managed-micronaut-tracing = "4.3.1" +managed-micronaut-tracing = "4.4.0" managed-micronaut-tracing-legacy = "3.2.7" managed-micronaut-views = "3.6.0" managed-micronaut-xml = "3.1.0" From 70bf89cea1ba1c677af1874d56fe7bab9784837f Mon Sep 17 00:00:00 2001 From: micronaut-build <65172877+micronaut-build@users.noreply.github.com> Date: Tue, 20 Sep 2022 01:52:04 -0400 Subject: [PATCH 066/743] Bump micronaut-mongodb to 4.6.0 (#8046) --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index cbc8c6d423e..41a81e90bd5 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -93,7 +93,7 @@ managed-micronaut-kubernetes = "3.4.0" managed-micronaut-micrometer = "4.6.1" managed-micronaut-microstream = "1.1.0" managed-micronaut-liquibase = "5.4.1" -managed-micronaut-mongo = "4.5.0" +managed-micronaut-mongo = "4.6.0" managed-micronaut-mqtt = "2.3.0" managed-micronaut-multitenancy = "4.2.0" managed-micronaut-neo4j = "5.2.0" From c5d4d3f45bd16dcfc94741bc5c407f1c13df3010 Mon Sep 17 00:00:00 2001 From: micronaut-build <65172877+micronaut-build@users.noreply.github.com> Date: Tue, 20 Sep 2022 02:40:18 -0400 Subject: [PATCH 067/743] Bump micronaut-gcp to 4.6.0 (#8048) --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 41a81e90bd5..74bf2a7a6a8 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -78,7 +78,7 @@ managed-micronaut-discovery = "3.2.0" managed-micronaut-elasticsearch = "4.3.0" managed-micronaut-email = "1.3.2" managed-micronaut-flyway = "5.4.1" -managed-micronaut-gcp = "4.5.1" +managed-micronaut-gcp = "4.6.0" managed-micronaut-graphql = "3.1.0" managed-micronaut-groovy = "3.3.1" managed-micronaut-grpc = "3.3.1" From 78045387ab417544235e3685f2f270dcaf646e54 Mon Sep 17 00:00:00 2001 From: micronaut-build <65172877+micronaut-build@users.noreply.github.com> Date: Tue, 20 Sep 2022 09:21:43 -0400 Subject: [PATCH 068/743] Bump micronaut-test to 3.6.2 (#8053) --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 74bf2a7a6a8..c0c3c752a51 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -117,7 +117,7 @@ managed-micronaut-serialization = "1.3.2" managed-micronaut-servlet = "3.3.1" managed-micronaut-spring = "4.3.1" managed-micronaut-sql = "4.7.0" -managed-micronaut-test = "3.6.1" +managed-micronaut-test = "3.6.2" managed-micronaut-test-resources = "1.1.1" managed-micronaut-toml = "1.1.1" managed-micronaut-tracing = "4.4.0" From 96c2b9b257146a43f36c4f4dbfc3c1b82bd9b476 Mon Sep 17 00:00:00 2001 From: micronaut-build <65172877+micronaut-build@users.noreply.github.com> Date: Tue, 20 Sep 2022 12:26:18 -0400 Subject: [PATCH 069/743] Bump micronaut-openapi to 4.5.2 (#8055) * Bump micronaut-openapi to 4.5.2 * Update libs.versions.toml * microstream 1.2.0 * Update libs.versions.toml Co-authored-by: Sergio del Amo --- gradle/libs.versions.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index c0c3c752a51..8e546ae0be8 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -91,7 +91,7 @@ managed-micronaut-kafka = "4.4.0" managed-micronaut-kotlin = "3.2.2" managed-micronaut-kubernetes = "3.4.0" managed-micronaut-micrometer = "4.6.1" -managed-micronaut-microstream = "1.1.0" +managed-micronaut-microstream = "1.2.0" managed-micronaut-liquibase = "5.4.1" managed-micronaut-mongo = "4.6.0" managed-micronaut-mqtt = "2.3.0" @@ -100,7 +100,7 @@ managed-micronaut-neo4j = "5.2.0" managed-micronaut-nats = "3.1.0" managed-micronaut-netflix = "2.1.0" managed-micronaut-object-storage = "1.0.0" -managed-micronaut-openapi = "4.5.1" +managed-micronaut-openapi = "4.5.2" managed-micronaut-oraclecloud = "2.2.1" managed-micronaut-picocli = "4.3.0" managed-micronaut-problem = "2.5.1" From 6d197d6eaa1b36bfcdfc8379a6626ec92f5edf12 Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Tue, 20 Sep 2022 22:41:23 +0200 Subject: [PATCH 070/743] =?UTF-8?q?doc:=203.7.0=20what=E2=80=99s=20new?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [ci skip] --- .../docs/guide/introduction/whatsNew.adoc | 28 ++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/src/main/docs/guide/introduction/whatsNew.adoc b/src/main/docs/guide/introduction/whatsNew.adoc index 7a3d6857973..442642fb330 100644 --- a/src/main/docs/guide/introduction/whatsNew.adoc +++ b/src/main/docs/guide/introduction/whatsNew.adoc @@ -2,10 +2,36 @@ == 3.7.0 -New modules: +Several improvements: + +- If you want complete control of where your application loads configuration from, for example, due to security restrictions, you can disable the default https://docs.micronaut.io/snapshot/guide/#propertySource[`PropertySourceLoader`] implementations by calling `ApplicationContextBuilder::enableDefaultPropertySources(false)` when starting your application. + +- Better `java.time` conversion for YAML configuration + +- Client SSL inner configuration is https://docs.micronaut.io/latest/guide/#bootstrap[Bootstrap] context compatible. + +- https://docs.micronaut.io/snapshot/api/io/micronaut/http/uri/UriBuilder.html[`UriBuilder`] methods `queryParam` and `replaceQueryParam` ignore null values. + +- It is possible to stop the Netty server without stopping the Application context. + +- You can declare beans at runtime using interfaces. + +- You can mark static methods as `@Executable`. + +- A big HTTP client refactor. + +**Spring integration improvements** + +- https://micronaut-projects.github.io/micronaut-spring/latest/guide/[Micronaut Spring] contains improvements for developers who want to use Micronaut modules with a Spring application or consume Spring libraries from a Micronaut application. + +**New modules**: - https://micronaut-projects.github.io/micronaut-object-storage/latest/guide/[Object Storage]. +- https://micronaut-projects.github.io/micronaut-crac/latest/guide/[Micronaut CRaC]. + +Please read the https://micronaut.io/2022/09/21/micronaut-framework-3-7-0-released/[Micronaut Framework 3.7.0 announcement blog post]. You will find a detailed overview of what’s new in Micronaut 3.7.0. + == 3.6.0 Key features: From 9c4d7cc91121606df6f9878622ddf90f5eeec3f5 Mon Sep 17 00:00:00 2001 From: micronaut-build Date: Wed, 21 Sep 2022 03:07:32 +0000 Subject: [PATCH 071/743] [skip ci] Release v3.7.0 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index c797ae746c2..c7452ad1529 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -projectVersion=3.7.0-SNAPSHOT +projectVersion=3.7.0 projectGroupId=io.micronaut title=Micronaut projectDesc=Natively Cloud Native From 74804e0335c908b2885ddc78adb16f64aea855e8 Mon Sep 17 00:00:00 2001 From: micronaut-build Date: Wed, 21 Sep 2022 03:20:15 +0000 Subject: [PATCH 072/743] Back to 3.7.1-SNAPSHOT --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index c7452ad1529..3c1d365478f 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -projectVersion=3.7.0 +projectVersion=3.7.1-SNAPSHOT projectGroupId=io.micronaut title=Micronaut projectDesc=Natively Cloud Native From 5d521dcda76204b588914efcfb71f510e48d3756 Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Wed, 21 Sep 2022 05:56:17 +0200 Subject: [PATCH 073/743] ci: projectVersion 3.8.0 SNAPSHOT --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 3c1d365478f..f8d4597a167 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -projectVersion=3.7.1-SNAPSHOT +projectVersion=3.8.0-SNAPSHOT projectGroupId=io.micronaut title=Micronaut projectDesc=Natively Cloud Native From 962ec7d517980c5d25cb8ececad45a695cd6e92e Mon Sep 17 00:00:00 2001 From: Graeme Rocher Date: Wed, 21 Sep 2022 07:01:12 +0200 Subject: [PATCH 074/743] Stop repackaging Caffeine (#8054) * Stop repackaging Caffeine * Add ability to exclude packages from binary compatibility checks Co-authored-by: Cedric Champeau --- buildSrc/build.gradle | 1 + ...t.build.internal.convention-library.gradle | 1 + .../internal/japicmp/RemovedPackages.groovy | 30 +++++++++++++++++ core/build.gradle | 14 +++++--- .../core/graal/CacheSubstitutions.java | 33 ------------------- session/build.gradle | 1 + src/main/docs/guide/appendix/breaks.adoc | 9 +++++ 7 files changed, 52 insertions(+), 37 deletions(-) create mode 100644 buildSrc/src/main/groovy/io/micronaut/build/internal/japicmp/RemovedPackages.groovy delete mode 100644 core/src/main/java/io/micronaut/core/graal/CacheSubstitutions.java diff --git a/buildSrc/build.gradle b/buildSrc/build.gradle index ab8cb95e0e1..07d7bd4312b 100644 --- a/buildSrc/build.gradle +++ b/buildSrc/build.gradle @@ -11,4 +11,5 @@ dependencies { implementation "org.aim42:htmlSanityCheck:1.1.6" implementation "io.micronaut.build.internal:micronaut-gradle-plugins:5.3.14" implementation "org.tomlj:tomlj:1.0.0" + implementation "me.champeau.gradle:japicmp-gradle-plugin:0.4.1" } diff --git a/buildSrc/src/main/groovy/io.micronaut.build.internal.convention-library.gradle b/buildSrc/src/main/groovy/io.micronaut.build.internal.convention-library.gradle index 3446fceef91..c570d2aecbc 100644 --- a/buildSrc/src/main/groovy/io.micronaut.build.internal.convention-library.gradle +++ b/buildSrc/src/main/groovy/io.micronaut.build.internal.convention-library.gradle @@ -21,3 +21,4 @@ tasks.named("shadowJar") { relocate "com.github.benmanes.caffeine", "io.micronaut.caffeine" relocate "org.objectweb.asm", "io.micronaut.asm" } + diff --git a/buildSrc/src/main/groovy/io/micronaut/build/internal/japicmp/RemovedPackages.groovy b/buildSrc/src/main/groovy/io/micronaut/build/internal/japicmp/RemovedPackages.groovy new file mode 100644 index 00000000000..991eb4261d4 --- /dev/null +++ b/buildSrc/src/main/groovy/io/micronaut/build/internal/japicmp/RemovedPackages.groovy @@ -0,0 +1,30 @@ +package io.micronaut.build.internal.japicmp + +import me.champeau.gradle.japicmp.report.Violation +import me.champeau.gradle.japicmp.report.ViolationTransformer +import me.champeau.gradle.japicmp.report.Severity +import java.util.Optional +import groovy.transform.CompileStatic + +@CompileStatic +class RemovedPackages implements ViolationTransformer { + private final List excludedPackagesPrefixes + private final List excludedPackages + + public RemovedPackages(Map> params) { + this.excludedPackagesPrefixes = params.prefixes as List + this.excludedPackages = params.exact as List + } + + Optional transform(String type, Violation v) { + String pkg = type.substring(0, type.lastIndexOf('.')) + + if (excludedPackagesPrefixes.any { pkg.startsWith(it) }) { + return Optional.of(v.withSeverity(Severity.accepted)) + } else if (excludedPackages.any { pkg == it }) { + return Optional.of(v.withSeverity(Severity.accepted)) + } + return Optional.of(v) + } +} + diff --git a/core/build.gradle b/core/build.gradle index b9826c19da3..8216747be5c 100644 --- a/core/build.gradle +++ b/core/build.gradle @@ -1,3 +1,6 @@ +import me.champeau.gradle.japicmp.JapicmpTask +import io.micronaut.build.internal.japicmp.RemovedPackages + plugins { id "io.micronaut.build.internal.convention-core-library" } @@ -20,10 +23,6 @@ dependencies { shadowCompile libs.bundles.asm shadowCompile libs.asm.tree - shadowCompile(libs.caffeine) { - exclude group: "com.google.errorprone", module: "error_prone_annotations" - exclude group: "org.checkerframework", module: "checker-qual" - } } spotless { @@ -46,3 +45,10 @@ def versionInfo = tasks.register("micronautVersionInfo", WriteProperties) { tasks.named("processResources") { from(versionInfo) } + +tasks.withType(JapicmpTask).configureEach { + richReport { + addViolationTransformer(RemovedPackages, [prefixes: ['io.micronaut.caffeine'], exact: []]) + } +} + diff --git a/core/src/main/java/io/micronaut/core/graal/CacheSubstitutions.java b/core/src/main/java/io/micronaut/core/graal/CacheSubstitutions.java deleted file mode 100644 index 0e97592e92e..00000000000 --- a/core/src/main/java/io/micronaut/core/graal/CacheSubstitutions.java +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright 2017-2020 original authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.micronaut.core.graal; - -//CHECKSTYLE:OFF - -import com.oracle.svm.core.annotate.Alias; -import com.oracle.svm.core.annotate.RecomputeFieldValue; -import com.oracle.svm.core.annotate.TargetClass; - -/** - * Substitutions for Caffeine UnsafeRefArrayAccess. - */ -@TargetClass(className = "io.micronaut.caffeine.cache.UnsafeRefArrayAccess") -final class Target_io_micronaut_caffeine_cache_UnsafeRefArrayAccess { - @Alias - @RecomputeFieldValue(kind = RecomputeFieldValue.Kind.ArrayIndexShift, declClass = Object[].class) - public static int REF_ELEMENT_SHIFT; -} -//CHECKSTYLE:ON diff --git a/session/build.gradle b/session/build.gradle index bc73ab914a2..8c5cfac7fcd 100644 --- a/session/build.gradle +++ b/session/build.gradle @@ -11,6 +11,7 @@ dependencies { compileOnly project(":http-server-netty") implementation libs.managed.reactor + implementation libs.caffeine testAnnotationProcessor project(":inject-java") testCompileOnly project(":inject-groovy") diff --git a/src/main/docs/guide/appendix/breaks.adoc b/src/main/docs/guide/appendix/breaks.adoc index c3c5792f2cb..58cf97b26de 100644 --- a/src/main/docs/guide/appendix/breaks.adoc +++ b/src/main/docs/guide/appendix/breaks.adoc @@ -1,5 +1,14 @@ This section documents breaking changes between Micronaut versions +== 4.0.0 + +=== Core Changes + +==== Caffeine No Longer Shaded + +https://github.com/ben-manes/caffeine[Caffeine] is no longer shaded into the `io.micronaut.caffeine` package. If you depend on this library you should directly depend on the latest version of Caffeine. + + == 3.3.0 - The <> is now disabled by default. To enable it, you must update your endpoint config: From 5084845c979f38f9424cc0a4c905859e79c3b5a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20S=C3=A1nchez-Mariscal?= Date: Fri, 23 Sep 2022 11:42:01 +0200 Subject: [PATCH 075/743] Exclude signature files from the uber-jar (#8076) --- parent/build.gradle | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/parent/build.gradle b/parent/build.gradle index 9805ad72d7d..0b325db055a 100644 --- a/parent/build.gradle +++ b/parent/build.gradle @@ -253,6 +253,16 @@ ext.extraPomInfo = { } transformer(implementation:'org.apache.maven.plugins.shade.resource.ServicesResourceTransformer') } + filters{ + filter { + artifact '*.*' + excludes { + exclude "META-INF/*.SF" + exclude "META-INF/*.DSA" + exclude "META-INF/*.RSA" + } + } + } } } } From 186bfa30958808bed8d5444ad8b3168521736ef3 Mon Sep 17 00:00:00 2001 From: micronaut-build <65172877+micronaut-build@users.noreply.github.com> Date: Fri, 23 Sep 2022 06:15:38 -0400 Subject: [PATCH 076/743] Bump micronaut-object-storage to 1.1.0 (#8069) --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 8e546ae0be8..d746c52cc32 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -99,7 +99,7 @@ managed-micronaut-multitenancy = "4.2.0" managed-micronaut-neo4j = "5.2.0" managed-micronaut-nats = "3.1.0" managed-micronaut-netflix = "2.1.0" -managed-micronaut-object-storage = "1.0.0" +managed-micronaut-object-storage = "1.1.0" managed-micronaut-openapi = "4.5.2" managed-micronaut-oraclecloud = "2.2.1" managed-micronaut-picocli = "4.3.0" From 945fc1cc60225918f0314b3bcbedb83071350938 Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Fri, 23 Sep 2022 12:16:12 +0200 Subject: [PATCH 077/743] build: Micronaut Crac to 1.0.1 (#8060) --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 8e546ae0be8..a6f91f26892 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -72,7 +72,7 @@ managed-micronaut-azure = "3.5.0" managed-micronaut-cache = "3.5.0" managed-micronaut-cassandra = "5.1.1" managed-micronaut-coherence = "3.5.1" -managed-micronaut-crac = "1.0.0" +managed-micronaut-crac = "1.0.1" managed-micronaut-data = "3.8.0" managed-micronaut-discovery = "3.2.0" managed-micronaut-elasticsearch = "4.3.0" From 9df408771aec84cba96faaf1a821164ecb563f06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Champeau?= Date: Fri, 23 Sep 2022 12:54:51 +0200 Subject: [PATCH 078/743] Bump Micronaut Test Resources version (#8070) * Bump micronaut-aws to 3.7.3 (#8050) * Bump Micronaut Test Resources version Co-authored-by: micronaut-build <65172877+micronaut-build@users.noreply.github.com> Co-authored-by: Sergio del Amo --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index a6f91f26892..f7031cc1244 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -118,7 +118,7 @@ managed-micronaut-servlet = "3.3.1" managed-micronaut-spring = "4.3.1" managed-micronaut-sql = "4.7.0" managed-micronaut-test = "3.6.2" -managed-micronaut-test-resources = "1.1.1" +managed-micronaut-test-resources = "1.1.2" managed-micronaut-toml = "1.1.1" managed-micronaut-tracing = "4.4.0" managed-micronaut-tracing-legacy = "3.2.7" From aa9bfc3016aa91f3381be288611a7f1032d275fa Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 27 Sep 2022 15:15:05 +0200 Subject: [PATCH 079/743] fix(deps): update dependency io.micronaut.crac:micronaut-crac-bom to v1.0.1 (#8081) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index d6184908394..2256022ebf0 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -72,7 +72,7 @@ managed-micronaut-azure = "3.5.0" managed-micronaut-cache = "3.5.0" managed-micronaut-cassandra = "5.1.1" managed-micronaut-coherence = "3.5.1" -managed-micronaut-crac = "1.0.0" +managed-micronaut-crac = "1.0.1" managed-micronaut-data = "3.8.0" managed-micronaut-discovery = "3.2.0" managed-micronaut-elasticsearch = "4.3.0" From 146e4ab26a419c88fa8068c22c81a6fb8310504f Mon Sep 17 00:00:00 2001 From: altro3 Date: Wed, 28 Sep 2022 12:02:46 +0700 Subject: [PATCH 080/743] fix: wrong declaringType in groovy properties when get genericReturnType. (#8084) See https://github.com/micronaut-projects/micronaut-openapi/issues/670 --- .../groovy/visitor/GroovyPropertyElement.java | 34 ++++++-- .../inject/visitor/ClassElementSpec.groovy | 80 +++++++++++++++++++ 2 files changed, 109 insertions(+), 5 deletions(-) diff --git a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyPropertyElement.java b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyPropertyElement.java index de68106200b..aac8f81a075 100644 --- a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyPropertyElement.java +++ b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyPropertyElement.java @@ -15,6 +15,7 @@ */ package io.micronaut.ast.groovy.visitor; +import io.micronaut.ast.groovy.utils.AstAnnotationUtils; import io.micronaut.core.annotation.AnnotationMetadata; import io.micronaut.core.annotation.Internal; import io.micronaut.core.util.CollectionUtils; @@ -22,6 +23,7 @@ import io.micronaut.inject.ast.ElementModifier; import io.micronaut.inject.ast.PropertyElement; import org.codehaus.groovy.ast.AnnotatedNode; +import org.codehaus.groovy.ast.PropertyNode; import java.util.Collections; import java.util.Set; @@ -34,16 +36,18 @@ */ @Internal abstract class GroovyPropertyElement extends AbstractGroovyElement implements PropertyElement { + private final String name; private final boolean readOnly; private final Object nativeType; - private final GroovyClassElement declaringElement; + private final GroovyClassElement declaringClass; + private ClassElement declaringElement; /** * Default constructor. * * @param visitorContext The visitor context - * @param declaringElement The declaring element + * @param declaringClass The declaring class * @param annotatedNode The annotated node * @param annotationMetadata the annotation metadata * @param name the name @@ -52,7 +56,7 @@ abstract class GroovyPropertyElement extends AbstractGroovyElement implements Pr */ GroovyPropertyElement( GroovyVisitorContext visitorContext, - GroovyClassElement declaringElement, + GroovyClassElement declaringClass, AnnotatedNode annotatedNode, AnnotationMetadata annotationMetadata, String name, @@ -62,7 +66,7 @@ abstract class GroovyPropertyElement extends AbstractGroovyElement implements Pr this.name = name; this.readOnly = readOnly; this.nativeType = nativeType; - this.declaringElement = declaringElement; + this.declaringClass = declaringClass; } @Override @@ -106,6 +110,26 @@ public String toString() { @Override public ClassElement getDeclaringType() { - return declaringElement; + if (declaringElement == null && nativeType instanceof PropertyNode) { + PropertyNode propertyNode = (PropertyNode) nativeType; + declaringElement = visitorContext.getElementFactory().newClassElement( + propertyNode.getDeclaringClass(), + AstAnnotationUtils.getAnnotationMetadata( + sourceUnit, + compilationUnit, + propertyNode.getDeclaringClass() + ) + ); + } + if (declaringElement != null) { + return declaringElement; + } + return declaringClass; } + + @Override + public ClassElement getOwningType() { + return declaringClass; + } + } diff --git a/inject-groovy/src/test/groovy/io/micronaut/inject/visitor/ClassElementSpec.groovy b/inject-groovy/src/test/groovy/io/micronaut/inject/visitor/ClassElementSpec.groovy index 2a868b81cde..9cd60337108 100644 --- a/inject-groovy/src/test/groovy/io/micronaut/inject/visitor/ClassElementSpec.groovy +++ b/inject-groovy/src/test/groovy/io/micronaut/inject/visitor/ClassElementSpec.groovy @@ -23,15 +23,18 @@ import io.micronaut.inject.ast.ElementModifier import io.micronaut.inject.ast.ElementQuery import io.micronaut.inject.ast.EnumElement import io.micronaut.inject.ast.FieldElement +import io.micronaut.inject.ast.MemberElement import io.micronaut.inject.ast.MethodElement import io.micronaut.inject.ast.PackageElement import io.micronaut.inject.ast.PropertyElement +import io.micronaut.inject.ast.TypedElement import spock.lang.Issue import spock.lang.Unroll import spock.util.environment.RestoreSystemProperties import java.sql.SQLException import java.util.function.Supplier +import java.util.stream.Collectors @RestoreSystemProperties class ClassElementSpec extends AbstractBeanDefinitionSpec { @@ -44,6 +47,83 @@ class ClassElementSpec extends AbstractBeanDefinitionSpec { AllElementsVisitor.clearVisited() } + @Issue("https://github.com/micronaut-projects/micronaut-openapi/issues/670") + void "test correct properties decaliring class with inheritance"() { + given: + def controller = buildBeanDefinition('test.TestController', ''' +package test + +import groovy.transform.CompileStatic +import io.micronaut.core.annotation.Introspected +import io.micronaut.http.annotation.Controller +import io.micronaut.http.annotation.Get + +import javax.validation.constraints.NotNull + +@Controller +class TestController { + + @Get + PessoaFisicaDto test() { + return null + } +} + +@Introspected +@CompileStatic +abstract class EntidadeDto { + + @NotNull + UUID id + Long tenantId + Integer version + @NotNull + Date criadoEm + @NotNull + Date atualizadoEm +} + +@Introspected +@CompileStatic +class PessoaFisicaDto extends EntidadeDto { + + String nome + String nomeMae + String nomePai + String CPF +} + +''') + expect: + controller + AllElementsVisitor.VISITED_METHOD_ELEMENTS + when: + def method = AllElementsVisitor.VISITED_METHOD_ELEMENTS[0] + def type = method.genericReturnType; + List beanProperties = type.getBeanProperties().stream().filter(p -> !"groovy.lang.MetaClass".equals(p.getType().getName())).collect(Collectors.toList()); + + List childFieldsOwned = new ArrayList<>(); + List childClassFields = new ArrayList<>(); + for (TypedElement publicField : beanProperties) { + if (publicField instanceof MemberElement) { + + MemberElement memberEl = (MemberElement) publicField; + if (memberEl.getDeclaringType().getType().getName() == type.getName()) { + childClassFields.add(publicField) + } + if (memberEl.getOwningType().getType().getName() == type.getName()) { + childFieldsOwned.add(publicField) + } + } + } + + then: + childClassFields + childClassFields.size() == 4 + childFieldsOwned + childFieldsOwned.size() == 9 + } + void "test interface bean properties"() { given: def element = buildClassElement(""" From 36375eed961c60cae2459e8cd8357482d50a46bf Mon Sep 17 00:00:00 2001 From: micronaut-build <65172877+micronaut-build@users.noreply.github.com> Date: Wed, 28 Sep 2022 01:03:49 -0400 Subject: [PATCH 081/743] build: Bump micronaut-sql to 4.7.1 (#8088) --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index f7031cc1244..9926ee4fe90 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -116,7 +116,7 @@ managed-micronaut-security = "3.8.0" managed-micronaut-serialization = "1.3.2" managed-micronaut-servlet = "3.3.1" managed-micronaut-spring = "4.3.1" -managed-micronaut-sql = "4.7.0" +managed-micronaut-sql = "4.7.1" managed-micronaut-test = "3.6.2" managed-micronaut-test-resources = "1.1.2" managed-micronaut-toml = "1.1.1" From f24738288af6b01f8b5bbff9c694ede8b39649e2 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 28 Sep 2022 07:05:24 +0200 Subject: [PATCH 082/743] chore(deps): update plugin me.champeau.jmh to v0.6.8 (#8078) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- benchmarks/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/benchmarks/build.gradle b/benchmarks/build.gradle index 2276fa0a7b3..0a31c534fba 100644 --- a/benchmarks/build.gradle +++ b/benchmarks/build.gradle @@ -1,6 +1,6 @@ plugins { id 'io.micronaut.build.internal.convention-base' - id "me.champeau.jmh" version "0.6.7" + id "me.champeau.jmh" version "0.6.8" } dependencies { From 8d6862edd5da2f9c4718ad1a04823a9fcf74e53e Mon Sep 17 00:00:00 2001 From: Jonas Konrad Date: Wed, 28 Sep 2022 11:06:28 +0200 Subject: [PATCH 083/743] One channel per HTTP2 stream (#6842) This PR refactors the NettyHttpServer to have a more straight-forward pipeline setup in a separate class (HttpPipelineBuilder), and then uses netty's Http2MultiplexHandler to multiplex each HTTP2 stream from one connection to its own Channel. Multiplexing improves compatibility with HTTP 1 handlers downstream. While HTTP 1 handlers do not need to be aware of multiple concurrent request or response streams, HTTP 2 handlers do. With multiplexing, the HTTP 1 handlers only manage one HTTP 2 stream each, so concurrent requests are not an issue anymore. This change prevents a class of bugs related to faulty STREAM_ID handling, such as #6785. It also adds a new feature, concurrent streaming responses (tested in this PR by Http2ConcurrentStreamSpec). While all tests pass, this is a bit of a risky change. We expose a lot of the handler pipeline to downstream users, and this PR changes that pipeline fundamentally. For example, the SslHandler isn't visible on the pipeline anymore, which might lead downstream code to think SSL is disabled (this caused the test failure fixed in c9456e2). It's possible that there are similar downstream dependencies on the structure of the pipeline in other modules. --- .../http/netty/AbstractNettyHttpRequest.java | 2 +- .../http/netty/stream/HttpStreamsHandler.java | 68 +---- .../StreamingInboundHttp2ToHttpAdapter.java | 2 + .../server/netty/HttpPipelineBuilder.java | 250 ++++++++++-------- .../http/server/netty/NettyHttpRequest.java | 100 ++++--- .../http/server/netty/NettyHttpServer.java | 23 +- .../server/netty/NettyServerCustomizer.java | 8 + .../server/netty/RoutingInBoundHandler.java | 12 - .../accesslog/HttpAccessLogHandler.java | 66 ++--- .../netty/types/files/FileTypeHandler.java | 4 - ...ttySystemFileCustomizableResponseType.java | 4 - ...NettyStreamedCustomizableResponseType.java | 5 - .../handler/accesslog/AccessLogSpec.groovy | 21 +- .../http2/Http2ConcurrentStreamSpec.groovy | 89 +++++++ .../LogbookNettyServerCustomizerSpec.groovy | 36 ++- 15 files changed, 395 insertions(+), 295 deletions(-) create mode 100644 http-server-netty/src/test/groovy/io/micronaut/http/server/netty/http2/Http2ConcurrentStreamSpec.groovy diff --git a/http-netty/src/main/java/io/micronaut/http/netty/AbstractNettyHttpRequest.java b/http-netty/src/main/java/io/micronaut/http/netty/AbstractNettyHttpRequest.java index 3dc9935e97f..6d368acb75f 100644 --- a/http-netty/src/main/java/io/micronaut/http/netty/AbstractNettyHttpRequest.java +++ b/http-netty/src/main/java/io/micronaut/http/netty/AbstractNettyHttpRequest.java @@ -148,7 +148,7 @@ public boolean isStream() { @Override public HttpVersion getHttpVersion() { - if (nettyRequest.headers().contains(STREAM_ID)) { + if (nettyRequest.headers().contains(HTTP2_SCHEME)) { return HttpVersion.HTTP_2_0; } return HttpVersion.HTTP_1_1; diff --git a/http-netty/src/main/java/io/micronaut/http/netty/stream/HttpStreamsHandler.java b/http-netty/src/main/java/io/micronaut/http/netty/stream/HttpStreamsHandler.java index 27acf66b930..e3b56b348e6 100644 --- a/http-netty/src/main/java/io/micronaut/http/netty/stream/HttpStreamsHandler.java +++ b/http-netty/src/main/java/io/micronaut/http/netty/stream/HttpStreamsHandler.java @@ -17,7 +17,6 @@ import io.micronaut.core.annotation.Internal; import io.micronaut.http.exceptions.HttpStatusException; -import io.micronaut.http.netty.AbstractNettyHttpRequest; import io.micronaut.http.netty.reactive.HandlerPublisher; import io.micronaut.http.netty.reactive.HandlerSubscriber; import io.netty.channel.ChannelDuplexHandler; @@ -231,49 +230,22 @@ public void channelRead(final ChannelHandlerContext ctx, Object msg) throws Exce currentlyStreamedMessage = inMsg; // It has a body, stream it - int streamId = getStreamId(msg); - HandlerPublisher publisher; - if (streamId > -1) { - publisher = new HandlerPublisher(ctx.executor(), Http2Content.class) { - @Override - protected boolean acceptInboundMessage(Object msg) { - return super.acceptInboundMessage(msg) && ((Http2Content) msg).stream().id() == streamId; - } - - @Override - protected void cancelled() { - if (ctx.executor().inEventLoop()) { - handleCancelled(ctx, inMsg); - } else { - ctx.executor().execute(() -> handleCancelled(ctx, inMsg)); - } - } - - @Override - protected void requestDemand() { - bodyRequested(ctx); - super.requestDemand(); - } - }; - } else { - - publisher = new HandlerPublisher(ctx.executor(), HttpContent.class) { - @Override - protected void cancelled() { - if (ctx.executor().inEventLoop()) { - handleCancelled(ctx, inMsg); - } else { - ctx.executor().execute(() -> handleCancelled(ctx, inMsg)); - } + HandlerPublisher publisher = new HandlerPublisher(ctx.executor(), HttpContent.class) { + @Override + protected void cancelled() { + if (ctx.executor().inEventLoop()) { + handleCancelled(ctx, inMsg); + } else { + ctx.executor().execute(() -> handleCancelled(ctx, inMsg)); } + } - @Override - protected void requestDemand() { - bodyRequested(ctx); - super.requestDemand(); - } - }; - } + @Override + protected void requestDemand() { + bodyRequested(ctx); + super.requestDemand(); + } + }; ctx.channel().pipeline().addAfter(ctx.name(), HANDLER_BODY_PUBLISHER, publisher); ctx.fireChannelRead(createStreamedMessage(inMsg, publisher)); @@ -283,18 +255,6 @@ protected void requestDemand() { } } - /** - * Gets the stream ID from the message. - * @param msg The message - * @return The stream id - */ - protected int getStreamId(Object msg) { - if (msg instanceof io.netty.handler.codec.http.HttpMessage) { - return ((io.netty.handler.codec.http.HttpMessage) msg).headers().getInt(AbstractNettyHttpRequest.STREAM_ID, -1); - } - return -1; - } - private void handleCancelled(ChannelHandlerContext ctx, In msg) { if (currentlyStreamedMessage == msg) { ignoreBodyRead = true; diff --git a/http-netty/src/main/java/io/micronaut/http/netty/stream/StreamingInboundHttp2ToHttpAdapter.java b/http-netty/src/main/java/io/micronaut/http/netty/stream/StreamingInboundHttp2ToHttpAdapter.java index 315e61b47a3..135ef2c6094 100644 --- a/http-netty/src/main/java/io/micronaut/http/netty/stream/StreamingInboundHttp2ToHttpAdapter.java +++ b/http-netty/src/main/java/io/micronaut/http/netty/stream/StreamingInboundHttp2ToHttpAdapter.java @@ -15,6 +15,7 @@ */ package io.micronaut.http.netty.stream; +import io.micronaut.core.annotation.Internal; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; import io.netty.channel.ChannelHandlerContext; @@ -48,6 +49,7 @@ * @author graemerocher * @since 2.0 */ +@Internal public class StreamingInboundHttp2ToHttpAdapter extends Http2EventAdapter { protected final Http2Connection connection; protected final boolean validateHttpHeaders; diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/HttpPipelineBuilder.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/HttpPipelineBuilder.java index 799f8eb7022..f6eeb733f63 100644 --- a/http-server-netty/src/main/java/io/micronaut/http/server/netty/HttpPipelineBuilder.java +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/HttpPipelineBuilder.java @@ -17,11 +17,9 @@ import io.micronaut.core.annotation.NonNull; import io.micronaut.core.naming.Named; -import io.micronaut.http.netty.AbstractNettyHttpRequest; import io.micronaut.http.context.event.HttpRequestReceivedEvent; import io.micronaut.http.netty.channel.ChannelPipelineCustomizer; import io.micronaut.http.netty.stream.HttpStreamsServerHandler; -import io.micronaut.http.netty.stream.StreamingInboundHttp2ToHttpAdapter; import io.micronaut.http.server.netty.configuration.NettyHttpServerConfiguration; import io.micronaut.http.server.netty.decoders.HttpRequestDecoder; import io.micronaut.http.server.netty.encoders.HttpResponseEncoder; @@ -32,27 +30,26 @@ import io.micronaut.http.ssl.ServerSslConfiguration; import io.netty.channel.Channel; import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInitializer; import io.netty.channel.ChannelOutboundHandler; import io.netty.channel.ChannelPipeline; import io.netty.channel.SimpleChannelInboundHandler; import io.netty.handler.codec.http.FullHttpRequest; import io.netty.handler.codec.http.HttpContentDecompressor; import io.netty.handler.codec.http.HttpMessage; -import io.netty.handler.codec.http.HttpRequest; import io.netty.handler.codec.http.HttpServerCodec; import io.netty.handler.codec.http.HttpServerKeepAliveHandler; import io.netty.handler.codec.http.HttpServerUpgradeHandler; import io.netty.handler.codec.http.websocketx.extensions.compression.WebSocketServerCompressionHandler; import io.netty.handler.codec.http2.CleartextHttp2ServerUpgradeHandler; -import io.netty.handler.codec.http2.DefaultHttp2Connection; import io.netty.handler.codec.http2.Http2CodecUtil; -import io.netty.handler.codec.http2.Http2Connection; -import io.netty.handler.codec.http2.Http2FrameListener; +import io.netty.handler.codec.http2.Http2FrameCodec; +import io.netty.handler.codec.http2.Http2FrameCodecBuilder; import io.netty.handler.codec.http2.Http2FrameLogger; +import io.netty.handler.codec.http2.Http2MultiplexHandler; import io.netty.handler.codec.http2.Http2ServerUpgradeCodec; -import io.netty.handler.codec.http2.HttpConversionUtil; -import io.netty.handler.codec.http2.HttpToHttp2ConnectionHandler; -import io.netty.handler.codec.http2.HttpToHttp2ConnectionHandlerBuilder; +import io.netty.handler.codec.http2.Http2StreamChannel; +import io.netty.handler.codec.http2.Http2StreamFrameToHttpObjectCodec; import io.netty.handler.flow.FlowControlHandler; import io.netty.handler.logging.LoggingHandler; import io.netty.handler.pcap.PcapWriteHandler; @@ -64,6 +61,7 @@ import io.netty.handler.stream.ChunkedWriteHandler; import io.netty.handler.timeout.IdleStateHandler; import io.netty.util.AsciiString; +import io.netty.util.AttributeKey; import io.netty.util.ReferenceCountUtil; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -87,6 +85,8 @@ * @author ywkat */ final class HttpPipelineBuilder { + static final AttributeKey STREAM_PIPELINE_ATTRIBUTE = AttributeKey.newInstance("stream-pipeline"); + private static final Logger LOG = LoggerFactory.getLogger(HttpPipelineBuilder.class); private final NettyHttpServer server; @@ -234,7 +234,6 @@ void insertOuterTcpHandlers() { private void onRequestPipelineBuilt() { server.triggerPipelineListeners(pipeline); - connectionCustomizer.onStreamPipelineBuilt(); } /** @@ -251,28 +250,6 @@ private void insertIdleStateHandler() { } } - /** - * Insert the handlers that manage the micronaut message handling, e.g. conversion between micronaut requests - * and netty requests, and routing. - */ - private void insertMicronautHandlers() { - pipeline.addLast(NettyServerWebSocketUpgradeHandler.COMPRESSION_HANDLER, new WebSocketServerCompressionHandler()); - pipeline.addLast(ChannelPipelineCustomizer.HANDLER_HTTP_STREAM, new HttpStreamsServerHandler()); - pipeline.addLast(ChannelPipelineCustomizer.HANDLER_HTTP_CHUNK, new ChunkedWriteHandler()); - pipeline.addLast(HttpRequestDecoder.ID, requestDecoder); - if (server.getServerConfiguration().isDualProtocol() && server.getServerConfiguration().isHttpToHttpsRedirect() && !ssl) { - pipeline.addLast(ChannelPipelineCustomizer.HANDLER_HTTP_TO_HTTPS_REDIRECT, new HttpToHttpsRedirectHandler(sslConfiguration, hostResolver)); - } - if (ssl) { - pipeline.addLast("request-certificate-handler", requestCertificateHandler); - } - pipeline.addLast(HttpResponseEncoder.ID, responseEncoder); - pipeline.addLast(NettyServerWebSocketUpgradeHandler.ID, new NettyServerWebSocketUpgradeHandler( - embeddedServices, - server.getWebSocketSessionRepository())); - pipeline.addLast(ChannelPipelineCustomizer.HANDLER_MICRONAUT_INBOUND, routingInBoundHandler); - } - /** * Configure this pipeline for normal HTTP 1. */ @@ -281,80 +258,40 @@ void configureForHttp1() { pipeline.addLast(ChannelPipelineCustomizer.HANDLER_HTTP_SERVER_CODEC, createServerCodec()); - insertHttp1DownstreamHandlers(); + new StreamPipeline(channel, ssl, connectionCustomizer).insertHttp1DownstreamHandlers(); connectionCustomizer.onInitialPipelineBuilt(); + connectionCustomizer.onStreamPipelineBuilt(); onRequestPipelineBuilt(); } - /** - * Insert the handlers for HTTP 1 that are upstream of the - * {@value ChannelPipelineCustomizer#HANDLER_HTTP_SERVER_CODEC}. Used both for normal HTTP 1 connections, and - * after a H2C negotiation failure. - */ - private void insertHttp1DownstreamHandlers() { - if (accessLogHandler != null) { - pipeline.addLast(ChannelPipelineCustomizer.HANDLER_ACCESS_LOGGER, accessLogHandler); - } - registerMicronautChannelHandlers(); - pipeline.addLast(ChannelPipelineCustomizer.HANDLER_FLOW_CONTROL, new FlowControlHandler()); - pipeline.addLast(ChannelPipelineCustomizer.HANDLER_HTTP_KEEP_ALIVE, new HttpServerKeepAliveHandler()); - pipeline.addLast(ChannelPipelineCustomizer.HANDLER_HTTP_COMPRESSOR, new SmartHttpContentCompressor(embeddedServices.getHttpCompressionStrategy())); - pipeline.addLast(ChannelPipelineCustomizer.HANDLER_HTTP_DECOMPRESSOR, new HttpContentDecompressor()); - - insertMicronautHandlers(); - } - /** * Insert the handlers for normal HTTP 2, after ALPN. */ private void configureForHttp2() { insertIdleStateHandler(); - pipeline.addLast(ChannelPipelineCustomizer.HANDLER_HTTP2_CONNECTION, newHttpToHttp2ConnectionHandler()); - registerMicronautChannelHandlers(); - - insertHttp2DownstreamHandlers(); + pipeline.addLast(ChannelPipelineCustomizer.HANDLER_HTTP2_CONNECTION, createHttp2FrameCodec()); + pipeline.addLast(new Http2MultiplexHandler(new ChannelInitializer() { + @Override + protected void initChannel(@NonNull Channel ch) { + StreamPipeline streamPipeline = new StreamPipeline(ch, ssl, connectionCustomizer.specializeForChannel(ch, NettyServerCustomizer.ChannelRole.REQUEST_STREAM)); + streamPipeline.insertHttp2FrameHandlers(); + streamPipeline.streamCustomizer.onStreamPipelineBuilt(); + } + })); connectionCustomizer.onInitialPipelineBuilt(); onRequestPipelineBuilt(); } - /** - * Insert the handlers downstream of the {@value ChannelPipelineCustomizer#HANDLER_HTTP2_CONNECTION}. Used both - * for ALPN HTTP 2 and h2c. - */ - private void insertHttp2DownstreamHandlers() { - pipeline.addLast(ChannelPipelineCustomizer.HANDLER_FLOW_CONTROL, new FlowControlHandler()); - if (accessLogHandler != null) { - pipeline.addLast(ChannelPipelineCustomizer.HANDLER_ACCESS_LOGGER, accessLogHandler); - } - - insertMicronautHandlers(); - } - - /** - * Create the HTTP 2 <-> HTTP 1 converter, inserted as - * {@value ChannelPipelineCustomizer#HANDLER_HTTP2_CONNECTION}. - */ - private HttpToHttp2ConnectionHandler newHttpToHttp2ConnectionHandler() { - Http2Connection connection = new DefaultHttp2Connection(true); - final Http2FrameListener http2ToHttpAdapter = new StreamingInboundHttp2ToHttpAdapter( - connection, - (int) server.getServerConfiguration().getMaxRequestSize(), - server.getServerConfiguration().isValidateHeaders(), - true - ); - final HttpToHttp2ConnectionHandlerBuilder builder = new HttpToHttp2ConnectionHandlerBuilder() - .frameListener(http2ToHttpAdapter) + private Http2FrameCodec createHttp2FrameCodec() { + Http2FrameCodecBuilder builder = Http2FrameCodecBuilder.forServer() .validateHeaders(server.getServerConfiguration().isValidateHeaders()) .initialSettings(server.getServerConfiguration().getHttp2().http2Settings()); - server.getServerConfiguration().getLogLevel().ifPresent(logLevel -> - builder.frameLogger(new Http2FrameLogger(logLevel, - NettyHttpServer.class)) - ); - return builder.connection(connection).build(); + builder.frameLogger(new Http2FrameLogger(logLevel, NettyHttpServer.class))); + return builder.build(); } /** @@ -389,7 +326,7 @@ public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exc } @Override - protected void configurePipeline(ChannelHandlerContext ctx, String protocol) throws Exception { + protected void configurePipeline(ChannelHandlerContext ctx, String protocol) { switch (protocol) { case ApplicationProtocolNames.HTTP_2: configureForHttp2(); @@ -413,21 +350,24 @@ protected void configurePipeline(ChannelHandlerContext ctx, String protocol) thr void configureForH2cSupport() { insertIdleStateHandler(); - final HttpToHttp2ConnectionHandler connectionHandler = newHttpToHttp2ConnectionHandler(); + final Http2FrameCodec connectionHandler = createHttp2FrameCodec(); final String fallbackHandlerName = "http1-fallback-handler"; HttpServerUpgradeHandler.UpgradeCodecFactory upgradeCodecFactory = protocol -> { if (AsciiString.contentEquals(Http2CodecUtil.HTTP_UPGRADE_PROTOCOL_NAME, protocol)) { - return new Http2ServerUpgradeCodec(ChannelPipelineCustomizer.HANDLER_HTTP2_CONNECTION, connectionHandler) { + return new Http2ServerUpgradeCodec(connectionHandler, new Http2MultiplexHandler(new ChannelInitializer() { + @Override + protected void initChannel(@NonNull Http2StreamChannel ch) { + StreamPipeline streamPipeline = new StreamPipeline(ch, ssl, connectionCustomizer.specializeForChannel(ch, NettyServerCustomizer.ChannelRole.REQUEST_STREAM)); + streamPipeline.insertHttp2FrameHandlers(); + streamPipeline.streamCustomizer.onStreamPipelineBuilt(); + } + })) { @Override public void upgradeTo(ChannelHandlerContext ctx, FullHttpRequest upgradeRequest) { + super.upgradeTo(ctx, upgradeRequest); pipeline.remove(fallbackHandlerName); - insertHttp2DownstreamHandlers(); onRequestPipelineBuilt(); - super.upgradeTo(ctx, upgradeRequest); - // HTTP1 request is on the implicit stream 1 - upgradeRequest.headers().set(HttpConversionUtil.ExtensionHeaderNames.STREAM_ID.text(), 1); - ctx.fireChannelRead(ReferenceCountUtil.retain(upgradeRequest)); } }; } else { @@ -449,15 +389,6 @@ public void upgradeTo(ChannelHandlerContext ctx, FullHttpRequest upgradeRequest) @Override protected void channelRead0(ChannelHandlerContext ctx, HttpMessage msg) { // If this handler is hit then no upgrade has been attempted and the client is just talking HTTP. - if (msg instanceof HttpRequest) { - HttpRequest req = (HttpRequest) msg; - if (req.headers().contains(AbstractNettyHttpRequest.STREAM_ID)) { - ChannelPipeline pipeline = ctx.pipeline(); - pipeline.remove(this); - pipeline.fireChannelRead(ReferenceCountUtil.retain(msg)); - return; - } - } ChannelPipeline pipeline = ctx.pipeline(); // remove the handlers we don't need anymore @@ -466,8 +397,8 @@ protected void channelRead0(ChannelHandlerContext ctx, HttpMessage msg) { // reconfigure for http1 // note: we have to reuse the serverCodec in case it still has some data buffered - insertHttp1DownstreamHandlers(); - + new StreamPipeline(channel, ssl, connectionCustomizer).insertHttp1DownstreamHandlers(); + connectionCustomizer.onStreamPipelineBuilt(); onRequestPipelineBuilt(); pipeline.fireChannelRead(ReferenceCountUtil.retain(msg)); } @@ -475,6 +406,102 @@ protected void channelRead0(ChannelHandlerContext ctx, HttpMessage msg) { connectionCustomizer.onInitialPipelineBuilt(); } + @NonNull + private HttpServerCodec createServerCodec() { + return new HttpServerCodec( + server.getServerConfiguration().getMaxInitialLineLength(), + server.getServerConfiguration().getMaxHeaderSize(), + server.getServerConfiguration().getMaxChunkSize(), + server.getServerConfiguration().isValidateHeaders(), + server.getServerConfiguration().getInitialBufferSize() + ); + } + } + + final class StreamPipeline { + private final Channel channel; + private final ChannelPipeline pipeline; + private final boolean ssl; + + private final NettyServerCustomizer streamCustomizer; + + private StreamPipeline(Channel channel, boolean ssl, NettyServerCustomizer streamCustomizer) { + this.channel = channel; + this.pipeline = channel.pipeline(); + this.ssl = ssl; + this.streamCustomizer = streamCustomizer; + } + + void initializeChildPipelineForPushPromise(Channel childChannel) { + StreamPipeline promisePipeline = new StreamPipeline(childChannel, ssl, streamCustomizer.specializeForChannel(childChannel, NettyServerCustomizer.ChannelRole.PUSH_PROMISE_STREAM)); + promisePipeline.insertHttp2FrameHandlers(); + promisePipeline.streamCustomizer.onStreamPipelineBuilt(); + } + + private void insertHttp2FrameHandlers() { + pipeline.addLast(ChannelPipelineCustomizer.HANDLER_HTTP_DECODER, new Http2StreamFrameToHttpObjectCodec(true, server.getServerConfiguration().isValidateHeaders())); + + insertHttp2DownstreamHandlers(); + } + + /** + * Insert the handlers downstream of the {@value ChannelPipelineCustomizer#HANDLER_HTTP2_CONNECTION}. Used both + * for ALPN HTTP 2 and h2c. + */ + private void insertHttp2DownstreamHandlers() { + pipeline.addLast(ChannelPipelineCustomizer.HANDLER_FLOW_CONTROL, new FlowControlHandler()); + if (accessLogHandler != null) { + pipeline.addLast(ChannelPipelineCustomizer.HANDLER_ACCESS_LOGGER, accessLogHandler); + } + + registerMicronautChannelHandlers(); + + insertMicronautHandlers(); + } + + /** + * Insert the handlers that manage the micronaut message handling, e.g. conversion between micronaut requests + * and netty requests, and routing. + */ + private void insertMicronautHandlers() { + channel.attr(STREAM_PIPELINE_ATTRIBUTE).set(this); + + pipeline.addLast(ChannelPipelineCustomizer.HANDLER_HTTP_COMPRESSOR, new SmartHttpContentCompressor(embeddedServices.getHttpCompressionStrategy())); + pipeline.addLast(ChannelPipelineCustomizer.HANDLER_HTTP_DECOMPRESSOR, new HttpContentDecompressor()); + + pipeline.addLast(NettyServerWebSocketUpgradeHandler.COMPRESSION_HANDLER, new WebSocketServerCompressionHandler()); + pipeline.addLast(ChannelPipelineCustomizer.HANDLER_HTTP_STREAM, new HttpStreamsServerHandler()); + pipeline.addLast(ChannelPipelineCustomizer.HANDLER_HTTP_CHUNK, new ChunkedWriteHandler()); + pipeline.addLast(HttpRequestDecoder.ID, requestDecoder); + if (server.getServerConfiguration().isDualProtocol() && server.getServerConfiguration().isHttpToHttpsRedirect() && !ssl) { + pipeline.addLast(ChannelPipelineCustomizer.HANDLER_HTTP_TO_HTTPS_REDIRECT, new HttpToHttpsRedirectHandler(sslConfiguration, hostResolver)); + } + if (ssl) { + pipeline.addLast("request-certificate-handler", requestCertificateHandler); + } + pipeline.addLast(HttpResponseEncoder.ID, responseEncoder); + pipeline.addLast(NettyServerWebSocketUpgradeHandler.ID, new NettyServerWebSocketUpgradeHandler( + embeddedServices, + server.getWebSocketSessionRepository())); + pipeline.addLast(ChannelPipelineCustomizer.HANDLER_MICRONAUT_INBOUND, routingInBoundHandler); + } + + /** + * Insert the handlers for HTTP 1 that are upstream of the + * {@value ChannelPipelineCustomizer#HANDLER_HTTP_SERVER_CODEC}. Used both for normal HTTP 1 connections, and + * after a H2C negotiation failure. + */ + private void insertHttp1DownstreamHandlers() { + if (accessLogHandler != null) { + pipeline.addLast(ChannelPipelineCustomizer.HANDLER_ACCESS_LOGGER, accessLogHandler); + } + registerMicronautChannelHandlers(); + pipeline.addLast(ChannelPipelineCustomizer.HANDLER_FLOW_CONTROL, new FlowControlHandler()); + pipeline.addLast(ChannelPipelineCustomizer.HANDLER_HTTP_KEEP_ALIVE, new HttpServerKeepAliveHandler()); + + insertMicronautHandlers(); + } + /** * Add handlers registered through {@link NettyEmbeddedServices#getOutboundHandlers()}. */ @@ -490,16 +517,5 @@ private void registerMicronautChannelHandlers() { pipeline.addLast(name, outboundHandlerAdapter); } } - - @NonNull - private HttpServerCodec createServerCodec() { - return new HttpServerCodec( - server.getServerConfiguration().getMaxInitialLineLength(), - server.getServerConfiguration().getMaxHeaderSize(), - server.getServerConfiguration().getMaxChunkSize(), - server.getServerConfiguration().isValidateHeaders(), - server.getServerConfiguration().getInitialBufferSize() - ); - } } } diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/NettyHttpRequest.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/NettyHttpRequest.java index a847fa26167..0d3a99388c3 100644 --- a/http-server-netty/src/main/java/io/micronaut/http/server/netty/NettyHttpRequest.java +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/NettyHttpRequest.java @@ -17,6 +17,7 @@ import io.micronaut.core.annotation.Internal; import io.micronaut.core.annotation.NonNull; +import io.micronaut.core.annotation.Nullable; import io.micronaut.core.async.publisher.Publishers; import io.micronaut.core.convert.ConversionContext; import io.micronaut.core.convert.ConversionService; @@ -38,6 +39,7 @@ import io.micronaut.http.netty.NettyHttpHeaders; import io.micronaut.http.netty.NettyHttpParameters; import io.micronaut.http.netty.NettyHttpRequestBuilder; +import io.micronaut.http.netty.channel.ChannelPipelineCustomizer; import io.micronaut.http.netty.cookies.NettyCookie; import io.micronaut.http.netty.cookies.NettyCookies; import io.micronaut.http.netty.stream.DefaultStreamedHttpRequest; @@ -51,23 +53,31 @@ import io.netty.buffer.Unpooled; import io.netty.channel.Channel; import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInitializer; import io.netty.handler.codec.http.DefaultFullHttpRequest; import io.netty.handler.codec.http.DefaultHttpHeaders; import io.netty.handler.codec.http.DefaultHttpRequest; import io.netty.handler.codec.http.DefaultLastHttpContent; import io.netty.handler.codec.http.EmptyHttpHeaders; import io.netty.handler.codec.http.HttpHeaderNames; -import io.netty.handler.codec.http.HttpResponse; import io.netty.handler.codec.http.QueryStringDecoder; import io.netty.handler.codec.http.cookie.ClientCookieEncoder; import io.netty.handler.codec.http.multipart.AbstractHttpData; import io.netty.handler.codec.http.multipart.HttpData; import io.netty.handler.codec.http.multipart.MixedAttribute; +import io.netty.handler.codec.http2.DefaultHttp2PushPromiseFrame; import io.netty.handler.codec.http2.Http2ConnectionHandler; +import io.netty.handler.codec.http2.Http2FrameCodec; +import io.netty.handler.codec.http2.Http2StreamChannel; +import io.netty.handler.codec.http2.Http2StreamChannelBootstrap; import io.netty.handler.codec.http2.HttpConversionUtil; import io.netty.handler.ssl.SslHandler; import io.netty.util.ReferenceCountUtil; import io.netty.util.ReferenceCounted; +import io.netty.util.concurrent.Future; +import io.netty.util.concurrent.GenericFutureListener; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.io.IOException; import java.net.InetSocketAddress; @@ -95,6 +105,7 @@ */ @Internal public class NettyHttpRequest extends AbstractNettyHttpRequest implements HttpRequest, PushCapableHttpRequest { + private static final Logger LOG = LoggerFactory.getLogger(NettyHttpRequest.class); /** * Headers to exclude from the push promise sent to the client. We use @@ -186,24 +197,6 @@ public NettyHttpRequest(io.netty.handler.codec.http.HttpRequest nettyRequest, }); } - /** - * Prepares a response based on this HTTP/2 request if HTTP/2 is enabled. - * - * @param finalResponse The response to prepare, never {@code null} - */ - @Internal - public final void prepareHttp2ResponseIfNecessary(@NonNull HttpResponse finalResponse) { - final io.micronaut.http.HttpVersion httpVersion = getHttpVersion(); - final boolean isHttp2 = httpVersion == io.micronaut.http.HttpVersion.HTTP_2_0; - if (isHttp2) { - final io.netty.handler.codec.http.HttpHeaders nativeHeaders = nettyRequest.headers(); - final String streamId = nativeHeaders.get(STREAM_ID); - if (streamId != null) { - finalResponse.headers().set(STREAM_ID, streamId); - } - } - } - @Override public MutableHttpRequest mutate() { return new NettyMutableHttpRequest(); @@ -458,15 +451,28 @@ boolean isBodyRequired() { return bodyRequired || HttpMethod.requiresRequestBody(getMethod()); } + @Nullable + private ChannelHandlerContext findConnectionHandler() { + ChannelHandlerContext current = channelHandlerContext.pipeline().context(Http2ConnectionHandler.class); + if (current != null) { + return current; + } + Channel parentChannel = channelHandlerContext.channel().parent(); + if (parentChannel != null) { + return parentChannel.pipeline().context(Http2FrameCodec.class); + } + return null; + } + @Override public boolean isServerPushSupported() { - Http2ConnectionHandler http2ConnectionHandler = channelHandlerContext.pipeline().get(Http2ConnectionHandler.class); - return http2ConnectionHandler != null && http2ConnectionHandler.connection().remote().allowPushTo(); + ChannelHandlerContext http2ConnectionHandlerContext = findConnectionHandler(); + return http2ConnectionHandlerContext != null && ((Http2ConnectionHandler) http2ConnectionHandlerContext.handler()).connection().remote().allowPushTo(); } @Override public PushCapableHttpRequest serverPush(@NonNull HttpRequest request) { - ChannelHandlerContext connectionHandlerContext = channelHandlerContext.pipeline().context(Http2ConnectionHandler.class); + ChannelHandlerContext connectionHandlerContext = findConnectionHandler(); if (connectionHandlerContext != null) { Http2ConnectionHandler connectionHandler = (Http2ConnectionHandler) connectionHandlerContext.handler(); @@ -477,7 +483,7 @@ public PushCapableHttpRequest serverPush(@NonNull HttpRequest request) { URI configuredUri = request.getUri(); String scheme = configuredUri.getScheme(); if (scheme == null) { - scheme = channelHandlerContext.pipeline().get(SslHandler.class) == null ? SCHEME_HTTP : SCHEME_HTTPS; + scheme = channelHandlerContext.channel().parent().pipeline().get(SslHandler.class) == null ? SCHEME_HTTP : SCHEME_HTTPS; } String authority = configuredUri.getAuthority(); if (authority == null) { @@ -520,22 +526,42 @@ public PushCapableHttpRequest serverPush(@NonNull HttpRequest request) { inboundRequest.headers() ); - int ourStream = this.nettyRequest.headers().getInt(HttpConversionUtil.ExtensionHeaderNames.STREAM_ID.text()); - int newStream = connectionHandler.connection().local().incrementAndGetNextStreamId(); + int ourStream = ((Http2StreamChannel) channelHandlerContext.channel()).stream().id(); + HttpPipelineBuilder.StreamPipeline originalStreamPipeline = channelHandlerContext.channel().attr(HttpPipelineBuilder.STREAM_PIPELINE_ATTRIBUTE).get(); - connectionHandler.encoder().frameWriter().writePushPromise( - connectionHandlerContext, - ourStream, - newStream, - HttpConversionUtil.toHttp2Headers(outboundRequest, false), - 0, - connectionHandlerContext.voidPromise() - ); + new Http2StreamChannelBootstrap(channelHandlerContext.channel().parent()) + .handler(new ChannelInitializer() { + @Override + protected void initChannel(@NonNull Http2StreamChannel ch) throws Exception { + int newStream = ch.stream().id(); + + channelHandlerContext.write(new DefaultHttp2PushPromiseFrame(HttpConversionUtil.toHttp2Headers(outboundRequest, false)) + .stream(((Http2StreamChannel) channelHandlerContext.channel()).stream()) + .pushStream(ch.stream())); - inboundRequest.headers().setInt(HttpConversionUtil.ExtensionHeaderNames.STREAM_ID.text(), newStream); - inboundRequest.headers().setInt(HttpConversionUtil.ExtensionHeaderNames.STREAM_PROMISE_ID.text(), ourStream); - // delay until our handling is complete - connectionHandlerContext.executor().execute(() -> connectionHandlerContext.fireChannelRead(inboundRequest)); + originalStreamPipeline.initializeChildPipelineForPushPromise(ch); + + inboundRequest.headers().setInt(HttpConversionUtil.ExtensionHeaderNames.STREAM_ID.text(), newStream); + inboundRequest.headers().setInt(HttpConversionUtil.ExtensionHeaderNames.STREAM_PROMISE_ID.text(), ourStream); + + // delay until our handling is complete + connectionHandlerContext.executor().execute(() -> { + try { + ch.pipeline().context(ChannelPipelineCustomizer.HANDLER_HTTP_DECODER).fireChannelRead(inboundRequest); + } catch (Exception e) { + LOG.warn("Failed to complete push promise", e); + } + }); + } + }) + .open() + .addListener((GenericFutureListener>) future -> { + try { + future.sync(); + } catch (Exception e) { + LOG.warn("Failed to complete push promise", e); + } + }); return this; } else { throw new UnsupportedOperationException("Server push not supported by this client: Not a HTTP2 client"); diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/NettyHttpServer.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/NettyHttpServer.java index bbb3cac6807..21dd006bdf5 100644 --- a/http-server-netty/src/main/java/io/micronaut/http/server/netty/NettyHttpServer.java +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/NettyHttpServer.java @@ -726,14 +726,25 @@ private HttpPipelineBuilder createPipelineBuilder(NettyServerCustomizer customiz /** * Builds Embedded Channel. * - * @param ssl SSL - * @return Embedded Channel + * @param ssl whether to enable SSL + * @return The embedded channel with our server handlers */ @Internal public EmbeddedChannel buildEmbeddedChannel(boolean ssl) { - EmbeddedChannel embeddedChannel = new EmbeddedChannel(); - createPipelineBuilder(rootCustomizer).new ConnectionPipeline(embeddedChannel, ssl).initChannel(); - return embeddedChannel; + EmbeddedChannel channel = new EmbeddedChannel(); + buildEmbeddedChannel(channel, ssl); + return channel; + } + + /** + * Builds Embedded Channel. + * + * @param prototype The embedded channel to add our handlers to + * @param ssl whether to enable SSL + */ + @Internal + public void buildEmbeddedChannel(EmbeddedChannel prototype, boolean ssl) { + createPipelineBuilder(rootCustomizer).new ConnectionPipeline(prototype, ssl).initChannel(); } static Predicate inclusionPredicate(NettyHttpServerConfiguration.AccessLogger config) { @@ -749,7 +760,7 @@ static Predicate inclusionPredicate(NettyHttpServerConfiguration.AccessL private class Listener extends ChannelInitializer { Channel serverChannel; - private NettyServerCustomizer listenerCustomizer; + NettyServerCustomizer listenerCustomizer; NettyHttpServerConfiguration.NettyListenerConfiguration config; private volatile HttpPipelineBuilder httpPipelineBuilder; diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/NettyServerCustomizer.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/NettyServerCustomizer.java index f37d7e4cc9f..e94991f429c 100644 --- a/http-server-netty/src/main/java/io/micronaut/http/server/netty/NettyServerCustomizer.java +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/NettyServerCustomizer.java @@ -113,5 +113,13 @@ enum ChannelRole { * {@link io.netty.channel.socket.SocketChannel}, representing an HTTP connection. */ CONNECTION, + /** + * The channel is a channel representing an individual HTTP2 stream. + */ + REQUEST_STREAM, + /** + * The channel is a channel representing an individual HTTP2 stream, created for a push promise. + */ + PUSH_PROMISE_STREAM, } } diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/RoutingInBoundHandler.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/RoutingInBoundHandler.java index cbf7f9b8d58..44b6da7d8ff 100644 --- a/http-server-netty/src/main/java/io/micronaut/http/server/netty/RoutingInBoundHandler.java +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/RoutingInBoundHandler.java @@ -45,7 +45,6 @@ import io.micronaut.http.context.event.HttpRequestTerminatedEvent; import io.micronaut.http.multipart.PartData; import io.micronaut.http.multipart.StreamingFileUpload; -import io.micronaut.http.netty.AbstractNettyHttpRequest; import io.micronaut.http.netty.NettyHttpResponseBuilder; import io.micronaut.http.netty.NettyMutableHttpResponse; import io.micronaut.http.netty.stream.JsonSubscriber; @@ -1025,7 +1024,6 @@ private void encodeHttpResponse( toNettyResponse(response), mapToHttpContent(nettyRequest, response, body, context) ); - nettyRequest.prepareHttp2ResponseIfNecessary(streamedResponse); context.writeAndFlush(streamedResponse); context.read(); } else { @@ -1268,9 +1266,6 @@ private void writeFinalNettyResponse(MutableHttpResponse message, HttpRequest // close handled by HttpServerKeepAliveHandler final NettyHttpRequest nettyHttpRequest = (NettyHttpRequest) request; - if (isHttp2) { - addHttp2StreamHeader(request, nettyResponse); - } io.netty.handler.codec.http.HttpRequest nativeRequest = nettyHttpRequest.getNativeRequest(); if (nativeRequest instanceof StreamedHttpRequest && !((StreamedHttpRequest) nativeRequest).isConsumed()) { @@ -1327,13 +1322,6 @@ private void syncWriteAndFlushNettyResponse( } } - private void addHttp2StreamHeader(HttpRequest request, io.netty.handler.codec.http.HttpResponse nettyResponse) { - final String streamId = request.getHeaders().get(AbstractNettyHttpRequest.STREAM_ID); - if (streamId != null) { - nettyResponse.headers().set(AbstractNettyHttpRequest.STREAM_ID, streamId); - } - } - @NonNull private io.netty.handler.codec.http.HttpResponse toNettyResponse(HttpResponse message) { if (message instanceof NettyHttpResponseBuilder) { diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/handler/accesslog/HttpAccessLogHandler.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/handler/accesslog/HttpAccessLogHandler.java index 976310f3e22..dc9345f1389 100644 --- a/http-server-netty/src/main/java/io/micronaut/http/server/netty/handler/accesslog/HttpAccessLogHandler.java +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/handler/accesslog/HttpAccessLogHandler.java @@ -20,13 +20,13 @@ import io.micronaut.http.server.netty.handler.accesslog.element.AccessLogFormatParser; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufHolder; +import io.netty.channel.Channel; import io.netty.channel.ChannelDuplexHandler; import io.netty.channel.ChannelHandler.Sharable; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelPromise; import io.netty.channel.socket.SocketChannel; import io.netty.handler.codec.http.HttpHeaders; -import io.netty.handler.codec.http.HttpMessage; import io.netty.handler.codec.http.HttpRequest; import io.netty.handler.codec.http.HttpResponse; import io.netty.handler.codec.http.HttpResponseStatus; @@ -38,8 +38,8 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.util.HashMap; -import java.util.Map; +import java.util.LinkedList; +import java.util.Queue; import java.util.function.Predicate; /** @@ -108,10 +108,21 @@ public HttpAccessLogHandler(Logger logger, String spec, Predicate uriInc this.uriInclusion = uriInclusion; } + private SocketChannel findSocketChannel(Channel channel) { + if (channel instanceof SocketChannel) { + return (SocketChannel) channel; + } + Channel parent = channel.parent(); + if (parent == null) { + throw new IllegalArgumentException("No socket channel available"); + } + return findSocketChannel(parent); + } + @Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Http2Exception { if (logger.isInfoEnabled() && msg instanceof HttpRequest) { - final SocketChannel channel = (SocketChannel) ctx.channel(); + final SocketChannel channel = findSocketChannel(ctx.channel()); final HttpRequest request = (HttpRequest) msg; AccessLogHolder accessLogHolder = getAccessLogHolder(ctx, true); assert accessLogHolder != null; // can only return null when createIfMissing is false @@ -124,9 +135,9 @@ public void channelRead(ChannelHandlerContext ctx, Object msg) throws Http2Excep } else { protocol = request.protocolVersion().text(); } - accessLogHolder.createLogForRequest(request).onRequestHeaders(channel, request.method().name(), request.headers(), request.uri(), protocol); + accessLogHolder.createLogForRequest().onRequestHeaders(channel, request.method().name(), request.headers(), request.uri(), protocol); } else { - accessLogHolder.excludeRequest(request); + accessLogHolder.excludeRequest(); } } ctx.fireChannelRead(msg); @@ -154,7 +165,6 @@ private void processWriteEvent(ChannelHandlerContext ctx, Object msg, ChannelPro if (accessLogHolder != null) { boolean isContinueResponse = msg instanceof HttpResponse && ((HttpResponse) msg).status().equals(HttpResponseStatus.CONTINUE); AccessLog accessLogger = accessLogHolder.getLogForResponse( - msg instanceof HttpMessage ? (HttpMessage) msg : null, msg instanceof LastHttpContent && !isContinueResponse); if (accessLogger != null && !isContinueResponse) { if (msg instanceof HttpResponse) { @@ -193,16 +203,10 @@ private AccessLogHolder getAccessLogHolder(ChannelHandlerContext ctx, boolean cr * class multiplexes access where necessary. */ private final class AccessLogHolder { - private final Map liveAccessLogsByStreamId = new HashMap<>(); - // HTTP1 does not have stream IDs. To emulate them, we have two counters. One counts up on every request, and - // the other counts up on every *completed* response. - private long http1NextRequestStreamId = 0; - private long currentPendingResponseStreamId = 0; - + private final Queue liveLogs = new LinkedList<>(); // ArrayDeque doesn't like null elements :( private AccessLog logForReuse; - AccessLog createLogForRequest(HttpRequest request) { - long streamId = getOrCreateStreamId(request); + AccessLog createLogForRequest() { AccessLog log = logForReuse; logForReuse = null; if (log != null) { @@ -210,42 +214,22 @@ AccessLog createLogForRequest(HttpRequest request) { } else { log = accessLogFormatParser.newAccessLogger(); } - liveAccessLogsByStreamId.put(streamId, log); + liveLogs.add(log); return log; } - void excludeRequest(HttpRequest request) { - getOrCreateStreamId(request); // claim stream id, but no access logger - } - - private long getOrCreateStreamId(HttpRequest request) { - String streamIdHeader = request.headers().get(ExtensionHeaderNames.STREAM_ID.text()); - if (streamIdHeader == null) { - return http1NextRequestStreamId++; - } else { - return Long.parseLong(streamIdHeader); - } + void excludeRequest() { + liveLogs.add(null); } @Nullable - AccessLog getLogForResponse(@Nullable HttpMessage msg, boolean finishResponse) { - String streamIdHeader = msg == null ? null : msg.headers().get(ExtensionHeaderNames.STREAM_ID.text()); - long streamId; - if (streamIdHeader == null) { - streamId = currentPendingResponseStreamId; - if (finishResponse) { - currentPendingResponseStreamId++; - } - } else { - streamId = Long.parseLong(streamIdHeader); - currentPendingResponseStreamId = streamId; // in case future HttpContent objects arrive without a stream_id header - } + AccessLog getLogForResponse(boolean finishResponse) { if (finishResponse) { - AccessLog accessLog = liveAccessLogsByStreamId.remove(streamId); + AccessLog accessLog = liveLogs.poll(); logForReuse = accessLog; return accessLog; } else { - return liveAccessLogsByStreamId.get(streamId); + return liveLogs.peek(); } } } diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/types/files/FileTypeHandler.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/types/files/FileTypeHandler.java index 0be16a28715..3c2c1424c37 100644 --- a/http-server-netty/src/main/java/io/micronaut/http/server/netty/types/files/FileTypeHandler.java +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/types/files/FileTypeHandler.java @@ -22,7 +22,6 @@ import io.micronaut.http.MutableHttpHeaders; import io.micronaut.http.MutableHttpResponse; import io.micronaut.http.netty.NettyMutableHttpResponse; -import io.micronaut.http.server.netty.NettyHttpRequest; import io.micronaut.http.server.netty.configuration.NettyHttpServerConfiguration; import io.micronaut.http.server.netty.types.NettyCustomizableResponseTypeHandler; import io.micronaut.http.server.netty.types.NettyFileCustomizableResponseType; @@ -89,9 +88,6 @@ public ChannelFuture handle(Object obj, HttpRequest request, MutableHttpRespo long fileLastModifiedSeconds = lastModified / 1000; if (ifModifiedSinceDateSeconds == fileLastModifiedSeconds) { FullHttpResponse nettyResponse = notModified(response); - if (request instanceof NettyHttpRequest) { - ((NettyHttpRequest) request).prepareHttp2ResponseIfNecessary(nettyResponse); - } return context.writeAndFlush(nettyResponse); } } diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/types/files/NettySystemFileCustomizableResponseType.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/types/files/NettySystemFileCustomizableResponseType.java index 1f87b9b5255..1eb4944bbe3 100644 --- a/http-server-netty/src/main/java/io/micronaut/http/server/netty/types/files/NettySystemFileCustomizableResponseType.java +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/types/files/NettySystemFileCustomizableResponseType.java @@ -22,7 +22,6 @@ import io.micronaut.http.MediaType; import io.micronaut.http.MutableHttpResponse; import io.micronaut.http.netty.NettyMutableHttpResponse; -import io.micronaut.http.server.netty.NettyHttpRequest; import io.micronaut.http.server.netty.SmartHttpContentCompressor; import io.micronaut.http.server.netty.types.NettyFileCustomizableResponseType; import io.micronaut.http.server.types.CustomizableResponseTypeException; @@ -112,9 +111,6 @@ public ChannelFuture write(HttpRequest request, MutableHttpResponse respon // Write the request data final DefaultHttpResponse finalResponse = new DefaultHttpResponse(nettyResponse.getNettyHttpVersion(), nettyResponse.getNettyHttpStatus(), nettyResponse.getNettyHeaders()); - if (request instanceof NettyHttpRequest) { - ((NettyHttpRequest) request).prepareHttp2ResponseIfNecessary(finalResponse); - } context.write(finalResponse, context.voidPromise()); FileHolder file = new FileHolder(getFile()); diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/types/stream/NettyStreamedCustomizableResponseType.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/types/stream/NettyStreamedCustomizableResponseType.java index 3fed9af5059..2177d826eea 100644 --- a/http-server-netty/src/main/java/io/micronaut/http/server/netty/types/stream/NettyStreamedCustomizableResponseType.java +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/types/stream/NettyStreamedCustomizableResponseType.java @@ -19,7 +19,6 @@ import io.micronaut.http.HttpRequest; import io.micronaut.http.MutableHttpResponse; import io.micronaut.http.netty.NettyMutableHttpResponse; -import io.micronaut.http.server.netty.NettyHttpRequest; import io.micronaut.http.server.netty.types.NettyCustomizableResponseType; import io.netty.channel.ChannelFuture; import io.netty.channel.ChannelFutureListener; @@ -56,10 +55,6 @@ default ChannelFuture write(HttpRequest request, MutableHttpResponse respo // Write the request data final DefaultHttpResponse finalResponse = new DefaultHttpResponse(nettyResponse.getNettyHttpVersion(), nettyResponse.getNettyHttpStatus(), nettyResponse.getNettyHeaders()); - final io.micronaut.http.HttpVersion httpVersion = request.getHttpVersion(); - if (request instanceof NettyHttpRequest) { - ((NettyHttpRequest) request).prepareHttp2ResponseIfNecessary(finalResponse); - } InputStream inputStream = getInputStream(); // can be null if the stream was closed context.write(finalResponse, context.voidPromise()); diff --git a/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/handler/accesslog/AccessLogSpec.groovy b/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/handler/accesslog/AccessLogSpec.groovy index 6cccb271e39..34266f9c18c 100644 --- a/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/handler/accesslog/AccessLogSpec.groovy +++ b/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/handler/accesslog/AccessLogSpec.groovy @@ -274,6 +274,7 @@ class AccessLogSpec extends Specification { 'micronaut.ssl.buildSelfSigned': true, 'micronaut.server.netty.access-logger.enabled': true, 'micronaut.server.netty.access-logger.logger-name': 'http-access-log', + 'micronaut.netty.event-loops.default.num-threads': 1 ]) def server = ctx.getBean(EmbeddedServer) server.start() @@ -358,15 +359,15 @@ class AccessLogSpec extends Specification { responses.size() == 3 } responses[0].content().toString(StandardCharsets.UTF_8) == 'simple' - responses[1].content().toString(StandardCharsets.UTF_8) == 'open' - responses[2].content().toString(StandardCharsets.UTF_8) == 'finish' + responses[1].content().toString(StandardCharsets.UTF_8) == 'finish' + responses[2].content().toString(StandardCharsets.UTF_8) == 'open' new PollingConditions(timeout: 5).eventually { listAppender.list.size() == 3 } listAppender.list[0].message.contains('/interleave/simple') - listAppender.list[1].message.contains('/interleave/open') - listAppender.list[2].message.contains('/interleave/finish') + listAppender.list[1].message.contains('/interleave/finish') + listAppender.list[2].message.contains('/interleave/open') cleanup: responses*.content().forEach(ByteBuf::release) @@ -455,16 +456,16 @@ class AccessLogSpec extends Specification { } responses[0].content().toString(StandardCharsets.UTF_8) == 'simple' responses[1].content().toString(StandardCharsets.UTF_8) == 'simple' - responses[2].content().toString(StandardCharsets.UTF_8) == 'open' - responses[3].content().toString(StandardCharsets.UTF_8) == 'finish' + responses[2].content().toString(StandardCharsets.UTF_8) == 'finish' + responses[3].content().toString(StandardCharsets.UTF_8) == 'open' new PollingConditions(timeout: 5).eventually { listAppender.list.size() == 4 } listAppender.list[0].message.contains('/interleave/simple') listAppender.list[1].message.contains('/interleave/simple') - listAppender.list[2].message.contains('/interleave/open') - listAppender.list[3].message.contains('/interleave/finish') + listAppender.list[2].message.contains('/interleave/finish') + listAppender.list[3].message.contains('/interleave/open') cleanup: responses*.content().forEach(ByteBuf::release) @@ -554,8 +555,8 @@ class AccessLogSpec extends Specification { } responses[0].content().toString(StandardCharsets.UTF_8) == 'simple' responses[1].content().toString(StandardCharsets.UTF_8) == 'simple' - responses[2].content().toString(StandardCharsets.UTF_8) == 'open' - responses[3].content().toString(StandardCharsets.UTF_8) == 'finish' + responses[2].content().toString(StandardCharsets.UTF_8) == 'finish' + responses[3].content().toString(StandardCharsets.UTF_8) == 'open' new PollingConditions(timeout: 5).eventually { listAppender.list.size() == 1 diff --git a/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/http2/Http2ConcurrentStreamSpec.groovy b/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/http2/Http2ConcurrentStreamSpec.groovy new file mode 100644 index 00000000000..f62d3281d02 --- /dev/null +++ b/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/http2/Http2ConcurrentStreamSpec.groovy @@ -0,0 +1,89 @@ +package io.micronaut.http.server.netty.http2 + +import io.micronaut.context.ApplicationContext +import io.micronaut.context.annotation.Requires +import io.micronaut.http.annotation.Controller +import io.micronaut.http.annotation.Get +import io.micronaut.runtime.server.EmbeddedServer +import io.vertx.core.Vertx +import io.vertx.core.buffer.Buffer +import io.vertx.core.http.HttpVersion +import io.vertx.ext.web.client.HttpResponse +import io.vertx.ext.web.client.WebClient +import io.vertx.ext.web.client.WebClientOptions +import jakarta.inject.Singleton +import reactor.core.publisher.Flux +import reactor.core.publisher.FluxSink +import spock.lang.Specification +import spock.util.concurrent.PollingConditions + +class Http2ConcurrentStreamSpec extends Specification { + def 'test concurrent streaming responses'() { + given: + def ctx = ApplicationContext.run([ + 'spec.name' : 'Http2ConcurrentStreamSpec', + 'micronaut.server.http-version': '2.0', + 'micronaut.ssl.enabled' : true, + 'micronaut.ssl.port' : -1, + 'micronaut.ssl.buildSelfSigned': true, + ]) + def server = ctx.getBean(EmbeddedServer) + server.start() + def client = WebClient.create(Vertx.vertx(), new WebClientOptions() + .setSsl(true) + .setUseAlpn(true) + .setProtocolVersion(HttpVersion.HTTP_2) + .setTrustAll(true)) + + when: + HttpResponse r1 + client.get(server.port, server.host, "/http2concurrent/request1").send(ar -> { + r1 = ar.result() + }) + HttpResponse r2 + client.get(server.port, server.host, "/http2concurrent/request2").send(ar -> { + r2 = ar.result() + }) + client.get(server.port, server.host, "/http2concurrent/triggerCompletion").send(ar -> { }) + then: + new PollingConditions(timeout: 5).eventually { + r1 != null + r2 != null + } + r1.bodyAsString() == '["r1: 1","r1: 2"]' + r2.bodyAsString() == '["r2: 1","r2: 2"]' + + cleanup: + server.close() + } + + @Requires(property = 'spec.name', value = 'Http2ConcurrentStreamSpec') + @Controller('/http2concurrent') + @Singleton + static class ConcurrentResponseController { + FluxSink sink1 + FluxSink sink2 + + @Get('/request1') + def request1() { + return Flux. create(s -> + sink1 = s) + } + + @Get('/request2') + def request2() { + return Flux. create(s -> + sink2 = s) + } + + @Get('/triggerCompletion') + def triggerCompletion() { + sink1.next('"r1: 1"') + sink2.next('"r2: 1"') + sink1.next('"r1: 2"') + sink2.next('"r2: 2"') + sink1.complete() + sink2.complete() + } + } +} diff --git a/test-suite/src/test/groovy/io/micronaut/docs/netty/LogbookNettyServerCustomizerSpec.groovy b/test-suite/src/test/groovy/io/micronaut/docs/netty/LogbookNettyServerCustomizerSpec.groovy index 7a2979be5aa..6792d514b7f 100644 --- a/test-suite/src/test/groovy/io/micronaut/docs/netty/LogbookNettyServerCustomizerSpec.groovy +++ b/test-suite/src/test/groovy/io/micronaut/docs/netty/LogbookNettyServerCustomizerSpec.groovy @@ -4,6 +4,7 @@ import io.micronaut.context.ApplicationContext import io.micronaut.context.annotation.Bean import io.micronaut.context.annotation.Factory import io.micronaut.context.annotation.Requires +import io.micronaut.core.annotation.NonNull import io.micronaut.http.MediaType import io.micronaut.http.annotation.Body import io.micronaut.http.annotation.Controller @@ -15,8 +16,10 @@ import io.micronaut.http.server.netty.NettyHttpServer import io.micronaut.runtime.server.EmbeddedServer import io.netty.buffer.Unpooled import io.netty.channel.ChannelHandlerContext +import io.netty.channel.ChannelId import io.netty.channel.ChannelOutboundHandlerAdapter import io.netty.channel.ChannelPromise +import io.netty.channel.ServerChannel import io.netty.channel.embedded.EmbeddedChannel import io.netty.handler.codec.http.DefaultFullHttpRequest import io.netty.handler.codec.http.FullHttpResponse @@ -344,8 +347,13 @@ class LogbookNettyServerCustomizerSpec extends Specification { 'spec.name' : 'LogbookNettyServerCustomizerSpec' ]) - - def serverEmbeddedChannel = ((NettyHttpServer) ctx.getBean(EmbeddedServer)).buildEmbeddedChannel(false) + // Http2MultiplexHandler doesn't work properly without a ServerChannel parent (https://github.com/netty/netty/pull/12546) + def serverEmbeddedChannel = new EmbeddedChannel( + new EmbeddedServerChannel(), + new EmbeddedChannelId(), + true, false + ) + ((NettyHttpServer) ctx.getBean(EmbeddedServer)).buildEmbeddedChannel(serverEmbeddedChannel, false) def clientEmbeddedChannel = connectClientEmbeddedChannel(serverEmbeddedChannel) def http2Connection = new DefaultHttp2Connection(false) @@ -413,8 +421,7 @@ class LogbookNettyServerCustomizerSpec extends Specification { 'POST /logbook/logged', 'bar', '200', - // second response body not included because of logbook bug: https://github.com/zalando/logbook/issues/1216 - //'bar', + 'bar', ] } @@ -474,4 +481,25 @@ class LogbookNettyServerCustomizerSpec extends Specification { return body } } + + private static class EmbeddedChannelId implements ChannelId { + @Override + String asShortText() { + return toString() + } + + @Override + String asLongText() { + return toString() + } + + @Override + int compareTo(@NonNull ChannelId o) { + throw new UnsupportedOperationException() + } + } + + private static class EmbeddedServerChannel extends EmbeddedChannel implements ServerChannel { + + } } From 2ebd2e7a7dc202560f3f3690f8cd30b6a277fb1e Mon Sep 17 00:00:00 2001 From: micronaut-build <65172877+micronaut-build@users.noreply.github.com> Date: Wed, 28 Sep 2022 16:07:36 -0400 Subject: [PATCH 084/743] Bump micronaut-sql to 4.7.2 (#8095) --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 98ccd9f591b..8da75562193 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -116,7 +116,7 @@ managed-micronaut-security = "3.8.0" managed-micronaut-serialization = "1.3.2" managed-micronaut-servlet = "3.3.1" managed-micronaut-spring = "4.3.1" -managed-micronaut-sql = "4.7.1" +managed-micronaut-sql = "4.7.2" managed-micronaut-test = "3.6.2" managed-micronaut-test-resources = "1.1.2" managed-micronaut-toml = "1.1.1" From faf9d64fee09a533b2593e50b9b8fb0ef31e574b Mon Sep 17 00:00:00 2001 From: micronaut-build Date: Wed, 28 Sep 2022 20:56:04 +0000 Subject: [PATCH 085/743] [skip ci] Release v3.7.1 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 3c1d365478f..ee7720f2981 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -projectVersion=3.7.1-SNAPSHOT +projectVersion=3.7.1 projectGroupId=io.micronaut title=Micronaut projectDesc=Natively Cloud Native From e0108388b8aecb21ed0c52556e4be6de8bbdf3ea Mon Sep 17 00:00:00 2001 From: micronaut-build Date: Wed, 28 Sep 2022 21:10:44 +0000 Subject: [PATCH 086/743] Back to 3.7.2-SNAPSHOT --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index ee7720f2981..35cdf627e8a 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -projectVersion=3.7.1 +projectVersion=3.7.2-SNAPSHOT projectGroupId=io.micronaut title=Micronaut projectDesc=Natively Cloud Native From 96ba9f22a0dc67391fc942c4b6704a5bf01095ec Mon Sep 17 00:00:00 2001 From: Denis Stepanov Date: Thu, 29 Sep 2022 15:51:53 +0200 Subject: [PATCH 087/743] Fix: @entity should not become a bean (#8098) Close https://github.com/micronaut-projects/micronaut-core/issues/8096 --- .../BeanDefinitionInjectProcessor.java | 2 +- .../micronaut/inject/validation/Account1.java | 43 +++++++++++++++++++ .../micronaut/inject/validation/Account2.java | 39 +++++++++++++++++ .../micronaut/inject/validation/Account3.java | 41 ++++++++++++++++++ .../validation/BeanWithValidationSpec.groovy | 18 ++++++++ 5 files changed, 142 insertions(+), 1 deletion(-) create mode 100644 inject-java/src/test/groovy/io/micronaut/inject/validation/Account1.java create mode 100644 inject-java/src/test/groovy/io/micronaut/inject/validation/Account2.java create mode 100644 inject-java/src/test/groovy/io/micronaut/inject/validation/Account3.java create mode 100644 inject-java/src/test/groovy/io/micronaut/inject/validation/BeanWithValidationSpec.groovy diff --git a/inject-java/src/main/java/io/micronaut/annotation/processing/BeanDefinitionInjectProcessor.java b/inject-java/src/main/java/io/micronaut/annotation/processing/BeanDefinitionInjectProcessor.java index e0a39b54b87..b32e89358d3 100644 --- a/inject-java/src/main/java/io/micronaut/annotation/processing/BeanDefinitionInjectProcessor.java +++ b/inject-java/src/main/java/io/micronaut/annotation/processing/BeanDefinitionInjectProcessor.java @@ -961,7 +961,7 @@ public Object visitExecutable(ExecutableElement method, Object o) { writer.setValidated(validatedMethod); } } - } else if (validatedMethod) { + } else if (validatedMethod && isDeclaredBean) { if (isPrivate) { error(method, "Method annotated with constraints but is declared private. Change the method to be non-private in order for AOP advice to be applied."); return null; diff --git a/inject-java/src/test/groovy/io/micronaut/inject/validation/Account1.java b/inject-java/src/test/groovy/io/micronaut/inject/validation/Account1.java new file mode 100644 index 00000000000..40d7ed1e007 --- /dev/null +++ b/inject-java/src/test/groovy/io/micronaut/inject/validation/Account1.java @@ -0,0 +1,43 @@ +package io.micronaut.inject.validation; + +import javax.annotation.Nullable; +import javax.persistence.Entity; +import javax.validation.constraints.NotBlank; + +@Entity +public class Account1 { + + private Long id; + + @Nullable + @NotBlank + private String username; + + @Nullable + @NotBlank + private String password; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } +} diff --git a/inject-java/src/test/groovy/io/micronaut/inject/validation/Account2.java b/inject-java/src/test/groovy/io/micronaut/inject/validation/Account2.java new file mode 100644 index 00000000000..b6c8cdb633d --- /dev/null +++ b/inject-java/src/test/groovy/io/micronaut/inject/validation/Account2.java @@ -0,0 +1,39 @@ +package io.micronaut.inject.validation; + +import javax.persistence.Entity; +import javax.validation.constraints.NotBlank; + +@Entity +public class Account2 { + + private Long id; + + private String username; + + private String password; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + @NotBlank + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } +} diff --git a/inject-java/src/test/groovy/io/micronaut/inject/validation/Account3.java b/inject-java/src/test/groovy/io/micronaut/inject/validation/Account3.java new file mode 100644 index 00000000000..07b9fb9db5d --- /dev/null +++ b/inject-java/src/test/groovy/io/micronaut/inject/validation/Account3.java @@ -0,0 +1,41 @@ +package io.micronaut.inject.validation; + +import javax.annotation.Nullable; +import javax.persistence.Entity; +import javax.validation.constraints.NotBlank; + +@Entity +@Nullable +public class Account3 { + + private Long id; + + private String username; + + private String password; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + @NotBlank + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } +} diff --git a/inject-java/src/test/groovy/io/micronaut/inject/validation/BeanWithValidationSpec.groovy b/inject-java/src/test/groovy/io/micronaut/inject/validation/BeanWithValidationSpec.groovy new file mode 100644 index 00000000000..4d6aad66be8 --- /dev/null +++ b/inject-java/src/test/groovy/io/micronaut/inject/validation/BeanWithValidationSpec.groovy @@ -0,0 +1,18 @@ +package io.micronaut.inject.validation + +import io.micronaut.context.ApplicationContext +import spock.lang.AutoCleanup +import spock.lang.Shared +import spock.lang.Specification + +class BeanWithValidationSpec extends Specification { + + @Shared @AutoCleanup ApplicationContext context = ApplicationContext.run() + + void 'test bean definition is not created for a bean with validation'() { + expect: + context.getBeanDefinitions(Account1.class).isEmpty() + context.getBeanDefinitions(Account2.class).isEmpty() + context.getBeanDefinitions(Account3.class).isEmpty() + } +} From d67912bf0267ce788f11c73554280ff9b5ff59d0 Mon Sep 17 00:00:00 2001 From: Dean Wette Date: Thu, 29 Sep 2022 08:53:28 -0500 Subject: [PATCH 088/743] build: kotlin version to 1.7.10 (#8074) --- .../build/internal/ext/DefaultMicronautCoreExtension.java | 2 +- .../build/internal/ext/MicronautCoreExtension.java | 2 +- gradle.properties | 2 +- gradle/libs.versions.toml | 5 +++-- inject-kotlin-test/build.gradle | 2 +- .../annotation/processing/test/KotlinCompileHelper.kt | 8 ++++++-- src/main/docs/guide/appendix/breaks.adoc | 3 +++ src/main/docs/guide/introduction/whatsNew.adoc | 6 ++++++ 8 files changed, 22 insertions(+), 8 deletions(-) diff --git a/buildSrc/src/main/groovy/io/micronaut/build/internal/ext/DefaultMicronautCoreExtension.java b/buildSrc/src/main/groovy/io/micronaut/build/internal/ext/DefaultMicronautCoreExtension.java index f9a5fc06e45..bd5f9f28916 100644 --- a/buildSrc/src/main/groovy/io/micronaut/build/internal/ext/DefaultMicronautCoreExtension.java +++ b/buildSrc/src/main/groovy/io/micronaut/build/internal/ext/DefaultMicronautCoreExtension.java @@ -57,7 +57,7 @@ public void usesMicronautTestJunit() { @Override public void usesMicronautTestKotest() { - addTestImplementationDependency("kotest"); + addTestImplementationDependency("kotest5"); } private void addTestImplementationDependency(String lib) { diff --git a/buildSrc/src/main/groovy/io/micronaut/build/internal/ext/MicronautCoreExtension.java b/buildSrc/src/main/groovy/io/micronaut/build/internal/ext/MicronautCoreExtension.java index 680edf5d113..99af3efc33d 100644 --- a/buildSrc/src/main/groovy/io/micronaut/build/internal/ext/MicronautCoreExtension.java +++ b/buildSrc/src/main/groovy/io/micronaut/build/internal/ext/MicronautCoreExtension.java @@ -18,6 +18,7 @@ import org.gradle.api.provider.Property; public interface MicronautCoreExtension { + Property getDocumented(); void usesMicronautTest(); @@ -27,5 +28,4 @@ public interface MicronautCoreExtension { void usesMicronautTestJunit(); void usesMicronautTestKotest(); - } diff --git a/gradle.properties b/gradle.properties index 30f79a6596a..bd42ee8c7af 100644 --- a/gradle.properties +++ b/gradle.properties @@ -47,7 +47,7 @@ micronautMavenPluginVersion=3.4.0 chromedriverVersion=79.0.3945.36 geckodriverVersion=0.26.0 webdriverBinariesVersion=1.4 -kotlinVersion=1.6.10 +kotlinVersion=1.7.10 kotlin.stdlib.default.dependency=false # For the docs diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index adf6aa72660..87c49556a89 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -37,8 +37,8 @@ managed-dekorate = "1.0.3" managed-elasticsearch = "7.16.3" managed-ignite = "2.13.0" managed-junit5 = "5.9.0" -managed-kotlin = "1.6.21" -managed-kotlin-coroutines = "1.5.1" +managed-kotlin = "1.7.10" +managed-kotlin-coroutines = "1.6.4" managed-google-function-framework = "1.0.4" managed-google-function-invoker = "1.0.0" managed-gorm = "7.3.2" @@ -315,6 +315,7 @@ managed-micronaut-test-bom = { module = "io.micronaut.test:micronaut-test-bom", managed-micronaut-test-core = { module = "io.micronaut.test:micronaut-test-core", version.ref = "managed-micronaut-test" } managed-micronaut-test-junit5 = { module = "io.micronaut.test:micronaut-test-junit5", version.ref = "managed-micronaut-test" } managed-micronaut-test-kotest = { module = "io.micronaut.test:micronaut-test-kotest", version.ref = "managed-micronaut-test" } +managed-micronaut-test-kotest5 = { module = "io.micronaut.test:micronaut-test-kotest5", version.ref = "managed-micronaut-test" } managed-micronaut-test-spock = { module = "io.micronaut.test:micronaut-test-spock", version.ref = "managed-micronaut-test" } managed-micronaut-toml = { module = "io.micronaut.toml:micronaut-toml", version.ref = "managed-micronaut-toml" } managed-micronaut-tracing-legacy = { module = "io.micronaut:micronaut-tracing", version.ref = "managed-micronaut-tracing-legacy" } diff --git a/inject-kotlin-test/build.gradle b/inject-kotlin-test/build.gradle index e1d0445d4b8..537b5280c58 100644 --- a/inject-kotlin-test/build.gradle +++ b/inject-kotlin-test/build.gradle @@ -41,5 +41,5 @@ tasks.named("compileKotlin") { tasks.named("compileGroovy") { // this allows groovy to access kotlin classes. dependsOn tasks.getByPath('compileKotlin') - classpath += files(compileKotlin.destinationDir) + classpath += files(compileKotlin.destinationDirectory) } diff --git a/inject-kotlin-test/src/main/kotlin/io/micronaut/annotation/processing/test/KotlinCompileHelper.kt b/inject-kotlin-test/src/main/kotlin/io/micronaut/annotation/processing/test/KotlinCompileHelper.kt index bdac8827e77..36bdaa7825e 100644 --- a/inject-kotlin-test/src/main/kotlin/io/micronaut/annotation/processing/test/KotlinCompileHelper.kt +++ b/inject-kotlin-test/src/main/kotlin/io/micronaut/annotation/processing/test/KotlinCompileHelper.kt @@ -51,7 +51,10 @@ import java.io.IOException import java.net.URL import java.net.URLClassLoader import java.nio.charset.StandardCharsets -import java.nio.file.* +import java.nio.file.FileVisitResult +import java.nio.file.FileVisitor +import java.nio.file.Files +import java.nio.file.Path import java.nio.file.attribute.BasicFileAttributes import java.util.* import javax.annotation.processing.Processor @@ -125,6 +128,7 @@ object KotlinCompileHelper { configuration.put(CLIConfigurationKeys.MESSAGE_COLLECTOR_KEY, messageCollector) configuration.put(JVMConfigurationKeys.IR, false) configuration.put(JVMConfigurationKeys.OUTPUT_DIRECTORY, outDir.toFile()) + configuration.put(JVMConfigurationKeys.JDK_HOME, File(System.getProperty("java.home"))) val env = KotlinCoreEnvironment.createForTests({ }, configuration, EnvironmentConfigFiles.JVM_CONFIG_FILES) @@ -241,4 +245,4 @@ object KotlinCompileHelper { return defineClass(name, file, 0, file.size) } } -} \ No newline at end of file +} diff --git a/src/main/docs/guide/appendix/breaks.adoc b/src/main/docs/guide/appendix/breaks.adoc index 58cf97b26de..3b24563ac14 100644 --- a/src/main/docs/guide/appendix/breaks.adoc +++ b/src/main/docs/guide/appendix/breaks.adoc @@ -8,6 +8,9 @@ This section documents breaking changes between Micronaut versions https://github.com/ben-manes/caffeine[Caffeine] is no longer shaded into the `io.micronaut.caffeine` package. If you depend on this library you should directly depend on the latest version of Caffeine. +==== Kotlin base version updated to 1.7.10 + +Kotlin has been updated to 1.7.10, which may cause issues when compiling or linking to Kotlin libraries. == 3.3.0 diff --git a/src/main/docs/guide/introduction/whatsNew.adoc b/src/main/docs/guide/introduction/whatsNew.adoc index 442642fb330..f26f3aeb935 100644 --- a/src/main/docs/guide/introduction/whatsNew.adoc +++ b/src/main/docs/guide/introduction/whatsNew.adoc @@ -1,5 +1,11 @@ //Micronaut {version} includes the following changes: +== 4.0.0 + +=== Other Dependency Upgrades + +- Kotlin 1.7.10 + == 3.7.0 Several improvements: From 5b81643dc0726c974a6711201c402cf5d6c5052b Mon Sep 17 00:00:00 2001 From: Graeme Rocher Date: Thu, 29 Sep 2022 17:50:01 +0200 Subject: [PATCH 089/743] Make SnakeYAML and javax.annotation optional (#8061) --- inject-groovy/build.gradle | 2 +- .../aop/compile/LifeCycleWithProxySpec.groovy | 22 +++++++------- inject-java/build.gradle | 3 +- inject/build.gradle | 8 ++--- jackson-databind/build.gradle | 1 + runtime/build.gradle | 4 +-- .../instrument/ReactorInstrumentation.java | 5 ++-- src/main/docs/guide/appendix/breaks.adoc | 8 +++++ src/main/docs/guide/config.adoc | 29 +++++++++++++++++-- test-suite-javax-inject/build.gradle | 1 + 10 files changed, 57 insertions(+), 26 deletions(-) diff --git a/inject-groovy/build.gradle b/inject-groovy/build.gradle index 007fb154a33..a13e1a0ccd1 100644 --- a/inject-groovy/build.gradle +++ b/inject-groovy/build.gradle @@ -33,7 +33,7 @@ dependencies { } testImplementation libs.managed.groovy.json testImplementation libs.blaze.persistence.core - + testImplementation libs.managed.snakeyaml testImplementation libs.managed.reactor functionalTestImplementation(testFixtures(project(":test-suite"))) diff --git a/inject-groovy/src/test/groovy/io/micronaut/aop/compile/LifeCycleWithProxySpec.groovy b/inject-groovy/src/test/groovy/io/micronaut/aop/compile/LifeCycleWithProxySpec.groovy index 21d82c91589..d53e710d431 100644 --- a/inject-groovy/src/test/groovy/io/micronaut/aop/compile/LifeCycleWithProxySpec.groovy +++ b/inject-groovy/src/test/groovy/io/micronaut/aop/compile/LifeCycleWithProxySpec.groovy @@ -22,17 +22,17 @@ class MyBean { @jakarta.inject.Inject public ConversionService conversionService; public int count = 0; - + public String someMethod() { return "good"; } - + @jakarta.annotation.PostConstruct void created() { count++; } - - @javax.annotation.PreDestroy + + @jakarta.annotation.PreDestroy void destroyed() { count--; } @@ -73,7 +73,7 @@ class MyBean { @jakarta.inject.Inject public ConversionService conversionService; public int count = 0; - + @Mutating("someVal") public String someMethod() { return "good"; @@ -83,8 +83,8 @@ class MyBean { void created() { count++; } - - @javax.annotation.PreDestroy + + @jakarta.annotation.PreDestroy void destroyed() { count--; } @@ -124,17 +124,17 @@ class MyBean { @jakarta.inject.Inject public ConversionService conversionService; public int count = 0; - + @jakarta.annotation.PostConstruct void created() { count++; } - - @javax.annotation.PreDestroy + + @jakarta.annotation.PreDestroy void destroyed() { count--; } - + @Mutating("someVal") public String someMethod() { return "good"; diff --git a/inject-java/build.gradle b/inject-java/build.gradle index 8a2f4eb2c54..2c7ffbae470 100644 --- a/inject-java/build.gradle +++ b/inject-java/build.gradle @@ -48,7 +48,8 @@ dependencies { exclude module: 'micronaut-inject' exclude module: 'micronaut-runtime' } - + testImplementation libs.managed.javax.annotation.api + testImplementation libs.managed.snakeyaml testRuntimeOnly libs.javax.el.impl testRuntimeOnly libs.javax.el } diff --git a/inject/build.gradle b/inject/build.gradle index cfafae94867..1245659e695 100644 --- a/inject/build.gradle +++ b/inject/build.gradle @@ -11,14 +11,12 @@ micronautBuild { } dependencies { - api libs.managed.javax.annotation.api compileOnly libs.javax.inject api libs.jakarta.inject.api api libs.managed.jakarta.annotation.api api project(':core') - api libs.managed.snakeyaml - compileOnly libs.javax.persistence + compileOnly libs.managed.snakeyaml compileOnly libs.managed.groovy compileOnly libs.kotlin.stdlib.jdk8 compileOnly libs.managed.validation @@ -28,7 +26,7 @@ dependencies { testImplementation project(":inject-groovy") testImplementation project(":inject-test-utils") testImplementation libs.systemlambda - + testImplementation libs.managed.snakeyaml testRuntimeOnly libs.junit.jupiter.engine } @@ -38,4 +36,4 @@ tasks.withType(Test) { logger.warn("Opening java.util, so SystemLambda can work") jvmArgs += ['--add-opens', 'java.base/java.util=ALL-UNNAMED'] } -} \ No newline at end of file +} diff --git a/jackson-databind/build.gradle b/jackson-databind/build.gradle index bf90e7ab508..3322e95b8db 100644 --- a/jackson-databind/build.gradle +++ b/jackson-databind/build.gradle @@ -27,6 +27,7 @@ dependencies { testImplementation project(":inject-java-test") testImplementation project(":inject-groovy") testImplementation "com.fasterxml.jackson.dataformat:jackson-dataformat-xml" + testImplementation libs.managed.snakeyaml if (!JavaVersion.current().isJava9Compatible()) { testImplementation files(org.gradle.internal.jvm.Jvm.current().toolsJar) } diff --git a/runtime/build.gradle b/runtime/build.gradle index af59880bbba..23cac4f7e94 100644 --- a/runtime/build.gradle +++ b/runtime/build.gradle @@ -25,7 +25,7 @@ dependencies { compileOnly libs.kotlinx.coroutines.reactive compileOnly libs.managed.logback testImplementation libs.managed.logback - + testImplementation libs.managed.snakeyaml testAnnotationProcessor project(":inject-java") testImplementation libs.jsr107 testImplementation libs.managed.jcache @@ -54,4 +54,4 @@ tasks.withType(Test) { logger.warn("Opening java.util, so SystemLambda can work") jvmArgs += ['--add-opens', 'java.base/java.util=ALL-UNNAMED'] } -} \ No newline at end of file +} diff --git a/runtime/src/main/java/io/micronaut/reactive/reactor/instrument/ReactorInstrumentation.java b/runtime/src/main/java/io/micronaut/reactive/reactor/instrument/ReactorInstrumentation.java index e58a51578cf..9587aaae8de 100644 --- a/runtime/src/main/java/io/micronaut/reactive/reactor/instrument/ReactorInstrumentation.java +++ b/runtime/src/main/java/io/micronaut/reactive/reactor/instrument/ReactorInstrumentation.java @@ -24,13 +24,13 @@ import io.micronaut.scheduling.instrument.Instrumentation; import io.micronaut.scheduling.instrument.InvocationInstrumenter; import io.micronaut.scheduling.instrument.ReactiveInvocationInstrumenterFactory; +import jakarta.annotation.PostConstruct; +import jakarta.annotation.PreDestroy; import reactor.core.publisher.Flux; import reactor.core.publisher.Hooks; import reactor.core.publisher.Operators; import reactor.core.scheduler.Schedulers; -import javax.annotation.PostConstruct; -import javax.annotation.PreDestroy; import java.util.ArrayList; import java.util.List; @@ -50,7 +50,6 @@ class ReactorInstrumentation { * * @param instrumenterFactory The instrumenter factory */ - @SuppressWarnings("unchecked") @PostConstruct void init(ReactorInstrumenterFactory instrumenterFactory) { if (instrumenterFactory.hasInstrumenters()) { diff --git a/src/main/docs/guide/appendix/breaks.adoc b/src/main/docs/guide/appendix/breaks.adoc index 3b24563ac14..e6e3e7490fa 100644 --- a/src/main/docs/guide/appendix/breaks.adoc +++ b/src/main/docs/guide/appendix/breaks.adoc @@ -8,6 +8,14 @@ This section documents breaking changes between Micronaut versions https://github.com/ben-manes/caffeine[Caffeine] is no longer shaded into the `io.micronaut.caffeine` package. If you depend on this library you should directly depend on the latest version of Caffeine. +==== SnakeYAML no longer a direct dependency + +SnakeYAML is no longer a direct dependency, if you need YAML configuration you should add SnakeYAML to your classpath explicitly + +==== `javax.annotation` no longer a directory dependency + +The `javax.annotation` library is no longer a directory dependency. Any references to types in the `javax.anotation` package should be changed to `jakarta.annotation` + ==== Kotlin base version updated to 1.7.10 Kotlin has been updated to 1.7.10, which may cause issues when compiling or linking to Kotlin libraries. diff --git a/src/main/docs/guide/config.adoc b/src/main/docs/guide/config.adoc index d4dc8d7e1c4..e413c31e556 100644 --- a/src/main/docs/guide/config.adoc +++ b/src/main/docs/guide/config.adoc @@ -1,7 +1,30 @@ -Configuration in Micronaut takes inspiration from both Spring Boot and Grails, integrating configuration properties from multiple sources directly into the core IoC container. +Micronaut features a flexible configuration mechanism that allows reading configuration from a variety of sources into a unified model that can be bound to Java types annotated with <>. -Configuration can by default be provided in Java properties, YAML, JSON, or Groovy files. The convention is to search for a file named `application.yml`, `application.properties`, `application.json` or `application.groovy`. +Configuration can by default be provided in Java properties files or https://www.json.org/json-en.html[JSON] with the ability to add support for more formats (such as YAML or Groovy configuration) by adding addition third-party libraries to your classpath. The convention is to search for a file named `application.properties` or `application.json` with support for other formats requiring additional dependencies as described by the following table: -In addition, like Spring and Grails, Micronaut allows overriding any property via system properties or environment variables. +.Supported Configuration Formats +|=== +|Format|File|Dependency Required + +| https://yaml.org[YAML] +|`application.yml` +|`org.yaml:snakeyaml` + +| https://micronaut-projects.github.io/micronaut-groovy/latest/guide/#config[Groovy Config] +|`application.groovy` +|`io.micronaut.groovy:micronaut-runtime-groovy` + +|https://github.com/lightbend/config/blob/main/HOCON.md[HOCON] +|`application.conf` +|`io.micronaut.kotlin:micronaut-kotlin-runtime` + +|https://toml.io/en/[TOML] +|`application.toml` +|`io.micronaut.toml:micronaut-toml` + +|=== + + +In addition, Micronaut allows overriding any property via system properties or environment variables. Each source of configuration is modeled with the link:{api}/io/micronaut/context/env/PropertySource.html[PropertySource] interface and the mechanism is extensible, allowing the implementation of additional link:{api}/io/micronaut/context/env/PropertySourceLoader.html[PropertySourceLoader] implementations. diff --git a/test-suite-javax-inject/build.gradle b/test-suite-javax-inject/build.gradle index e39503e43b5..8517f80313e 100644 --- a/test-suite-javax-inject/build.gradle +++ b/test-suite-javax-inject/build.gradle @@ -8,4 +8,5 @@ dependencies { testImplementation project(":context") testImplementation project(":inject") testImplementation libs.javax.inject + testImplementation libs.managed.javax.annotation.api } From 1aba43681050c2eaa4fc31a7f6bf95b7a12af2ea Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 7 Oct 2022 10:09:54 +0200 Subject: [PATCH 090/743] fix(deps): update junit5 monorepo to v5.9.1 (#8103) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 87c49556a89..4cd564825ea 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -36,7 +36,7 @@ wiremock = "2.33.2" managed-dekorate = "1.0.3" managed-elasticsearch = "7.16.3" managed-ignite = "2.13.0" -managed-junit5 = "5.9.0" +managed-junit5 = "5.9.1" managed-kotlin = "1.7.10" managed-kotlin-coroutines = "1.6.4" managed-google-function-framework = "1.0.4" From efb514fbebca62f3a0b4d34a36c0d22bba885de9 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 7 Oct 2022 10:10:05 +0200 Subject: [PATCH 091/743] chore(deps): update mikepenz/action-junit-report action to v3.5.1 (#8101) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/graalvm.yml | 2 +- .github/workflows/gradle.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/graalvm.yml b/.github/workflows/graalvm.yml index 22b17459968..8f3f1c04aaf 100644 --- a/.github/workflows/graalvm.yml +++ b/.github/workflows/graalvm.yml @@ -58,7 +58,7 @@ jobs: PREDICTIVE_TEST_SELECTION: "${{ github.event_name == 'pull_request' && 'true' || 'false' }}" - name: Publish Test Report if: always() - uses: mikepenz/action-junit-report@v3.5.0 + uses: mikepenz/action-junit-report@v3.5.1 with: check_name: GraalVM CE CI / Test Report (Java ${{ matrix.java }}) report_paths: '**/build/test-results/test/TEST-*.xml' diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index 9ba193f0b0f..51d91606360 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -60,7 +60,7 @@ jobs: PREDICTIVE_TEST_SELECTION: "${{ github.event_name == 'pull_request' && 'true' || 'false' }}" - name: Publish Test Report if: always() - uses: mikepenz/action-junit-report@v3.5.0 + uses: mikepenz/action-junit-report@v3.5.1 with: check_name: Java CI / Test Report (${{ matrix.java }}) report_paths: '**/build/test-results/test/TEST-*.xml' From 4d7b685b16af18c75e0e8b358532fc9f77d9e43d Mon Sep 17 00:00:00 2001 From: Dean Wette Date: Fri, 7 Oct 2022 03:11:19 -0500 Subject: [PATCH 092/743] BREAKING: del deprecated code in Micronaut Core for 4.0.x (partial) (#8089) closes #7988 --- .../micronaut/aop/writer/AopProxyWriter.java | 8 +- config/accepted-api-changes.json | 57 +++++++- .../context/scope/refresh/RefreshScope.java | 14 -- .../watch/event/FileWatchRestartListener.java | 12 -- .../core/io/service/SoftServiceLoader.java | 13 -- .../io/service/StreamSoftServiceLoader.java | 100 ------------- .../AnnotationProcessingOutputVisitor.java | 13 +- .../ServiceDescriptionProcessor.java | 138 ------------------ .../visitor/JavaPropertyElement.java | 29 +--- .../processing/visitor/JavaVoidElement.java | 88 ----------- .../visitor/JavaClassElementSpec.groovy | 56 ------- .../io/micronaut/context/BeanContext.java | 29 +--- .../context/annotation/Provided.java | 4 +- .../io/micronaut/inject/ast/ClassElement.java | 25 ++-- .../inject/processing/ProcessedTypes.java | 39 ----- .../InterceptorBindingQualifier.java | 39 ++--- .../inject/qualifiers/Qualifiers.java | 16 -- .../inject/visitor/VisitorContext.java | 16 +- .../writer/AbstractClassFileWriter.java | 11 +- .../writer/ClassWriterOutputVisitor.java | 2 + 20 files changed, 91 insertions(+), 618 deletions(-) delete mode 100644 core/src/main/java/io/micronaut/core/io/service/StreamSoftServiceLoader.java delete mode 100644 inject-java/src/main/java/io/micronaut/annotation/processing/ServiceDescriptionProcessor.java delete mode 100644 inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaVoidElement.java delete mode 100644 inject-java/src/test/groovy/io/micronaut/annotation/processing/visitor/JavaClassElementSpec.groovy delete mode 100644 inject/src/main/java/io/micronaut/inject/processing/ProcessedTypes.java diff --git a/aop/src/main/java/io/micronaut/aop/writer/AopProxyWriter.java b/aop/src/main/java/io/micronaut/aop/writer/AopProxyWriter.java index a242cbc4477..a3e7ad1c8eb 100644 --- a/aop/src/main/java/io/micronaut/aop/writer/AopProxyWriter.java +++ b/aop/src/main/java/io/micronaut/aop/writer/AopProxyWriter.java @@ -208,7 +208,7 @@ public class AopProxyWriter extends AbstractClassFileWriter implements ProxyingB /** *

Constructs a new {@link AopProxyWriter} for the given parent {@link BeanDefinitionWriter} and starting interceptors types.

- * + * *

Additional {@link Interceptor} types can be added downstream with {@link #visitInterceptorBinding(AnnotationValue[])} .

* @param parent The parent {@link BeanDefinitionWriter} * @param settings optional setting @@ -387,12 +387,6 @@ private String[] getImplementedInterfaceInternalNames() { return interfaceTypes.stream().map(o -> JavaModelUtils.getTypeReference(o).getInternalName()).toArray(String[]::new); } - @Override - @Deprecated - public Element getOriginatingElement() { - return proxyBeanDefinitionWriter.getOriginatingElement(); - } - @Override public void visitBeanFactoryMethod(ClassElement factoryClass, MethodElement factoryMethod) { proxyBeanDefinitionWriter.visitBeanFactoryMethod(factoryClass, factoryMethod); diff --git a/config/accepted-api-changes.json b/config/accepted-api-changes.json index 30cff8dc8e7..ea9312644de 100644 --- a/config/accepted-api-changes.json +++ b/config/accepted-api-changes.json @@ -34,7 +34,7 @@ "type": "io.micronaut.inject.visitor.TypeElementVisitor", "member": "Method io.micronaut.inject.visitor.TypeElementVisitor.visitEnumConstant(io.micronaut.inject.ast.EnumConstantElement,io.micronaut.inject.visitor.VisitorContext)", "reason": "Added EnumConstantElement. Added EnumElement.elements() method to collect enum constant elements" - }, + }, { "type": "io.micronaut.scheduling.TaskScheduler", "member": "Method io.micronaut.scheduling.TaskScheduler.schedule(java.lang.String,java.lang.String,java.lang.Runnable)", @@ -279,5 +279,60 @@ "type": "io.micronaut.runtime.converters.time.$TimeConverterRegistrar$Definition$Reference", "member": "Constructor io.micronaut.runtime.converters.time.$TimeConverterRegistrar$Definition$Reference()", "reason": "Not a bean anymore" + }, + { + "type": "io.micronaut.annotation.processing.ServiceDescriptionProcessor", + "member": "Constructor io.micronaut.annotation.processing.ServiceDescriptionProcessor()", + "reason": "Deprecated and removed for Micronaut 4. No longer needed." + }, + { + "type": "io.micronaut.annotation.processing.ServiceDescriptionProcessor", + "member": "Implemented interface javax.annotation.processing.Processor", + "reason": "Deprecated and removed for Micronaut 4. No longer needed" + }, + { + "type": "io.micronaut.runtime.context.scope.refresh.RefreshScope", + "member": "Constructor io.micronaut.runtime.context.scope.refresh.RefreshScope(io.micronaut.context.BeanContext,java.util.concurrent.Executor)", + "reason": "Deprecated and removed for Micronaut 4" + }, + { + "type": "io.micronaut.runtime.server.watch.event.FileWatchRestartListener", + "member": "Constructor io.micronaut.runtime.server.watch.event.FileWatchRestartListener(io.micronaut.runtime.server.EmbeddedServer)", + "reason": "Deprecated and removed for Micronaut 4. Use FileWatchRestartListener(EmbeddedApplication) instead." + }, + { + "type": "io.micronaut.inject.processing.ProcessedTypes", + "member": "Field PRE_DESTROY", + "reason": "Deprecated and removed for Micronaut 4. Use io.micronaut.core.annotation.AnnotationUtil instead." + }, + { + "type": "io.micronaut.inject.processing.ProcessedTypes", + "member": "Field POST_CONSTRUCT", + "reason": "Deprecated and removed for Micronaut 4. Use io.micronaut.core.annotation.AnnotationUtil instead." + }, + { + "type": "io.micronaut.http.filter.OncePerRequestHttpServerFilter", + "member": "Constructor io.micronaut.http.filter.OncePerRequestHttpServerFilter()", + "reason": "Deprecated and removed for Micronaut 4. All filters are executed once per request starting in Micronaut 3.0." + }, + { + "type": "io.micronaut.http.filter.OncePerRequestHttpServerFilter", + "member": "Implemented interface io.micronaut.http.filter.HttpServerFilter", + "reason": "Deprecated and removed for Micronaut 4. All filters are executed once per request starting in Micronaut 3.0." + }, + { + "type": "io.micronaut.http.filter.OncePerRequestHttpServerFilter", + "member": "Implemented interface io.micronaut.http.filter.HttpFilter", + "reason": "Deprecated and removed for Micronaut 4. All filters are executed once per request starting in Micronaut 3.0." + }, + { + "type": "io.micronaut.http.client.loadbalance.FixedLoadBalancer", + "member": "Constructor io.micronaut.http.client.loadbalance.FixedLoadBalancer(java.net.URL)", + "reason": "Deprecated and removed for Micronaut 4. Use FixedLoadBalancer(java.net.URI) instead" + }, + { + "type": "io.micronaut.core.io.service.StreamSoftServiceLoader", + "member": "Constructor io.micronaut.core.io.service.StreamSoftServiceLoader()", + "reason": "Deprecated and removed for Micronaut 4. Use io.micronaut.core.io.service.SoftServiceLoader#collectAll(java.util.Collection)." } ] diff --git a/context/src/main/java/io/micronaut/runtime/context/scope/refresh/RefreshScope.java b/context/src/main/java/io/micronaut/runtime/context/scope/refresh/RefreshScope.java index dab1d2de456..0772c02e242 100644 --- a/context/src/main/java/io/micronaut/runtime/context/scope/refresh/RefreshScope.java +++ b/context/src/main/java/io/micronaut/runtime/context/scope/refresh/RefreshScope.java @@ -33,9 +33,6 @@ import io.micronaut.inject.BeanIdentifier; import io.micronaut.inject.qualifiers.Qualifiers; import io.micronaut.runtime.context.scope.Refreshable; -import io.micronaut.scheduling.TaskExecutors; -import jakarta.inject.Inject; -import jakarta.inject.Named; import jakarta.inject.Singleton; import java.util.Collection; @@ -44,7 +41,6 @@ import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; -import java.util.concurrent.Executor; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock; @@ -69,17 +65,7 @@ public class RefreshScope implements CustomScope, LifeCycle embeddedApplication) { this.embeddedApplication = embeddedApplication; } - /** - * @param embeddedServer The embedded server - * @deprecated Use {@link #FileWatchRestartListener(EmbeddedApplication)} instead. - */ - @Deprecated - public FileWatchRestartListener(EmbeddedServer embeddedServer) { - this.embeddedApplication = embeddedServer; - } - @Override public void onApplicationEvent(FileChangedEvent event) { embeddedApplication.stop(); diff --git a/core/src/main/java/io/micronaut/core/io/service/SoftServiceLoader.java b/core/src/main/java/io/micronaut/core/io/service/SoftServiceLoader.java index d0c86811d69..c2c5ec8ff73 100644 --- a/core/src/main/java/io/micronaut/core/io/service/SoftServiceLoader.java +++ b/core/src/main/java/io/micronaut/core/io/service/SoftServiceLoader.java @@ -262,19 +262,6 @@ public Iterator> iterator() { return servicesForIterator.iterator(); } - /** - * @param name The name - * @param loadedClass The loaded class - * @return The service definition - * @deprecated No longer used - */ - @SuppressWarnings({"unchecked", "ProtectedMemberInFinalClass", "OptionalUsedAsFieldOrParameterType", "java:S3740", - "java:S1133", "rawtypes"}) - @Deprecated - protected ServiceDefinition newService(String name, Optional loadedClass) { - return new DefaultServiceDefinition<>(name, loadedClass.orElse(null)); - } - /** * @param name The name * @param loadedClass The loaded class diff --git a/core/src/main/java/io/micronaut/core/io/service/StreamSoftServiceLoader.java b/core/src/main/java/io/micronaut/core/io/service/StreamSoftServiceLoader.java deleted file mode 100644 index f7d4b4c34f6..00000000000 --- a/core/src/main/java/io/micronaut/core/io/service/StreamSoftServiceLoader.java +++ /dev/null @@ -1,100 +0,0 @@ -/* - * Copyright 2017-2020 original authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.micronaut.core.io.service; - -import io.micronaut.core.reflect.ClassUtils; -import io.micronaut.core.util.CollectionUtils; - -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStreamReader; -import java.net.URL; -import java.util.ArrayList; -import java.util.Enumeration; -import java.util.List; -import java.util.ServiceConfigurationError; -import java.util.Set; -import java.util.stream.Stream; - -/** - * Variation of {@link SoftServiceLoader} that returns a stream instead of an iterable thus allowing parallel loading etc. - * - * @author Graeme Rocher - * @since 1.0 - * @deprecated Use {@link io.micronaut.core.io.service.SoftServiceLoader#collectAll(java.util.Collection)} instead - */ -@Deprecated -@SuppressWarnings({"java:S1118", "java:S1133"}) -public class StreamSoftServiceLoader { - - /** - * @param serviceType The service type - * @param classLoader The class loader - * @param The type - * @return A stream - */ - @SuppressWarnings({"unchecked", "java:S2112"}) - public static Stream> loadParallel(Class serviceType, ClassLoader classLoader) { - Enumeration serviceConfigs; - String name = serviceType.getName(); - try { - serviceConfigs = classLoader.getResources(SoftServiceLoader.META_INF_SERVICES + '/' + name); - } catch (IOException e) { - throw new ServiceConfigurationError("Failed to load resources for service: " + name, e); - } - Set urlSet = CollectionUtils.enumerationToSet(serviceConfigs); - - return urlSet - .stream() - .parallel() - .flatMap(url -> { - List lines = new ArrayList<>(); - try (BufferedReader reader = new BufferedReader(new InputStreamReader(url.openStream()))) { - String line = reader.readLine(); - while (line != null) { - if (line.length() != 0 && line.charAt(0) != '#') { - int i = line.indexOf('#'); - if (i > -1) { - line = line.substring(0, i); - } - lines.add(line); - } - line = reader.readLine(); - } - } catch (IOException e) { - throw new ServiceConfigurationError("Failed to load resources for URL: " + url, e); - } - return lines.stream(); - } - ).map(serviceName -> { - Class loadedClass = ClassUtils.forName(serviceName, classLoader) - .orElse(null); - return new DefaultServiceDefinition<>(name, loadedClass); - }); - } - - /** - * @param serviceType The service type - * @param classLoader The class loader - * @param The type - * @return A stream with services loaded - */ - public static Stream loadPresentParallel(Class serviceType, ClassLoader classLoader) { - return loadParallel(serviceType, classLoader) - .filter(ServiceDefinition::isPresent) - .map(ServiceDefinition::load); - } -} diff --git a/inject-java/src/main/java/io/micronaut/annotation/processing/AnnotationProcessingOutputVisitor.java b/inject-java/src/main/java/io/micronaut/annotation/processing/AnnotationProcessingOutputVisitor.java index f6fa879bbde..3e2ec991ec7 100644 --- a/inject-java/src/main/java/io/micronaut/annotation/processing/AnnotationProcessingOutputVisitor.java +++ b/inject-java/src/main/java/io/micronaut/annotation/processing/AnnotationProcessingOutputVisitor.java @@ -37,7 +37,12 @@ import java.lang.reflect.Field; import java.lang.reflect.Method; import java.net.URI; -import java.util.*; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; /** * An implementation of {@link io.micronaut.inject.writer.ClassWriterOutputVisitor} for annotation processing. @@ -197,12 +202,6 @@ public void close() throws IOException { return os; } - @Override - @Deprecated - public Optional visitMetaInfFile(String path) { - return visitMetaInfFile(path, io.micronaut.inject.ast.Element.EMPTY_ELEMENT_ARRAY); - } - @Override public Optional visitMetaInfFile(String path, io.micronaut.inject.ast.Element... originatingElements) { return metaInfFiles.computeIfAbsent(path, s -> { diff --git a/inject-java/src/main/java/io/micronaut/annotation/processing/ServiceDescriptionProcessor.java b/inject-java/src/main/java/io/micronaut/annotation/processing/ServiceDescriptionProcessor.java deleted file mode 100644 index f4f1bebc1f1..00000000000 --- a/inject-java/src/main/java/io/micronaut/annotation/processing/ServiceDescriptionProcessor.java +++ /dev/null @@ -1,138 +0,0 @@ -/* - * Copyright 2017-2020 original authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.micronaut.annotation.processing; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.Set; - -import javax.annotation.processing.RoundEnvironment; -import javax.annotation.processing.SupportedOptions; -import javax.lang.model.element.Element; -import javax.lang.model.element.TypeElement; -import javax.lang.model.type.DeclaredType; -import javax.lang.model.type.TypeMirror; - -import io.micronaut.annotation.processing.visitor.JavaClassElement; -import io.micronaut.context.ApplicationContextConfigurer; -import io.micronaut.context.annotation.ContextConfigurer; -import io.micronaut.context.visitor.ContextConfigurerVisitor; -import io.micronaut.core.annotation.AnnotationMetadata; -import io.micronaut.core.annotation.AnnotationValue; -import io.micronaut.core.annotation.Generated; -import io.micronaut.core.util.StringUtils; - -/** - * A separate aggregating annotation processor responsible for creating META-INF/services entries. - * - * @author graemerocher - * @since 2.0.0 - * @deprecated No longer needed and will be removed in a future release - */ -@SupportedOptions({ - AbstractInjectAnnotationProcessor.MICRONAUT_PROCESSING_INCREMENTAL, - AbstractInjectAnnotationProcessor.MICRONAUT_PROCESSING_ANNOTATIONS -}) -@Deprecated -public class ServiceDescriptionProcessor extends AbstractInjectAnnotationProcessor { - private static final Set SUPPORTED_ANNOTATIONS = Collections.emptySet(); - private static final Set SUPPORTED_SERVICE_TYPES = Collections.singleton( - ApplicationContextConfigurer.class.getName() - ); - - private final Map> serviceDescriptors = new HashMap<>(); - - @Override - protected String getIncrementalProcessorType() { - return GRADLE_PROCESSING_AGGREGATING; - } - - @Override - public Set getSupportedAnnotationTypes() { - return SUPPORTED_ANNOTATIONS; - } - - @Override - public boolean process(Set annotations, RoundEnvironment roundEnv) { - List originatingElements = new ArrayList<>(); - for (TypeElement annotation : annotations) { - Set elements = roundEnv.getElementsAnnotatedWith(annotation); - for (Element element : elements) { - if (element instanceof TypeElement) { - TypeElement typeElement = (TypeElement) element; - String name = typeElement.getQualifiedName().toString(); - if (!processGeneratedAnnotation(originatingElements, element, typeElement, name)) { - processContextConfigurerAnnotation(originatingElements, element, typeElement); - } - } - } - } - if (roundEnv.processingOver() && !serviceDescriptors.isEmpty()) { - classWriterOutputVisitor.writeServiceEntries( - serviceDescriptors, - originatingElements.toArray(io.micronaut.inject.ast.Element.EMPTY_ELEMENT_ARRAY) - ); - } - return true; - } - - private void processContextConfigurerAnnotation(List originatingElements, - Element element, - TypeElement typeElement) { - AnnotationMetadata annotationMetadata = annotationUtils.getAnnotationMetadata(element); - Optional> ann = annotationMetadata.findAnnotation(ContextConfigurer.class); - if (ann.isPresent()) { - JavaClassElement javaClassElement = javaVisitorContext.getElementFactory() - .newClassElement(typeElement, annotationMetadata); - ContextConfigurerVisitor.assertNoConstructorForContextAnnotation(javaClassElement); - List interfaces = typeElement.getInterfaces(); - for (TypeMirror interfaceType : interfaces) { - if (interfaceType instanceof DeclaredType) { - String serviceName = modelUtils.resolveTypeName(interfaceType); - String serviceImpl = modelUtils.resolveTypeName(element.asType()); - if (SUPPORTED_SERVICE_TYPES.contains(serviceName)) { - serviceDescriptors.computeIfAbsent(serviceName, s1 -> new HashSet<>()) - .add(serviceImpl); - originatingElements.add(new JavaClassElement(typeElement, AnnotationMetadata.EMPTY_METADATA, null)); - } - } - } - } - AnnotationUtils.invalidateCache(); - } - - private boolean processGeneratedAnnotation(List originatingElements, - Element element, - TypeElement typeElement, - String name) { - Generated generated = element.getAnnotation(Generated.class); - if (generated != null) { - String serviceName = generated.service(); - if (StringUtils.isNotEmpty(serviceName)) { - serviceDescriptors.computeIfAbsent(serviceName, s1 -> new HashSet<>()) - .add(name); - originatingElements.add(new JavaClassElement(typeElement, AnnotationMetadata.EMPTY_METADATA, null)); - } - return true; - } - return false; - } -} diff --git a/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaPropertyElement.java b/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaPropertyElement.java index b3836413b97..a60f17760ec 100644 --- a/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaPropertyElement.java +++ b/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaPropertyElement.java @@ -17,13 +17,11 @@ import io.micronaut.core.annotation.AnnotationMetadata; import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.NonNull; import io.micronaut.inject.ast.ClassElement; import io.micronaut.inject.ast.PropertyElement; -import io.micronaut.core.annotation.NonNull; - import javax.lang.model.element.Element; -import javax.lang.model.element.ExecutableElement; import javax.lang.model.element.TypeElement; import javax.lang.model.type.TypeMirror; import java.util.Collections; @@ -71,31 +69,6 @@ class JavaPropertyElement extends AbstractJavaElement implements PropertyElement this.visitorContext = visitorContext; } - /** - * Default constructor. - * - * @param declaringElement The declaring element - * @param rootElement The element - * @param annotationMetadata The annotation metadata - * @param name The name - * @param type The type - * @param readOnly Whether it is read only - * @param visitorContext The java visitor context - * @deprecated Use {@link #JavaPropertyElement(ClassElement, Element, AnnotationMetadata, String, ClassElement, boolean, JavaVisitorContext)} instead - */ - @SuppressWarnings("DeprecatedIsStillUsed") // used by openapi processor - @Deprecated - JavaPropertyElement( - ClassElement declaringElement, - ExecutableElement rootElement, - AnnotationMetadata annotationMetadata, - String name, - ClassElement type, - boolean readOnly, - JavaVisitorContext visitorContext) { - this(declaringElement, (Element) rootElement, annotationMetadata, name, type, readOnly, visitorContext); - } - @Override public ClassElement getGenericType() { Map> declaredGenericInfo; diff --git a/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaVoidElement.java b/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaVoidElement.java deleted file mode 100644 index 8e64f13d62c..00000000000 --- a/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaVoidElement.java +++ /dev/null @@ -1,88 +0,0 @@ -/* - * Copyright 2017-2020 original authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.micronaut.annotation.processing.visitor; - -import io.micronaut.core.annotation.NonNull; -import io.micronaut.core.annotation.AnnotationMetadata; -import io.micronaut.core.annotation.AnnotationMetadataDelegate; -import io.micronaut.core.annotation.Internal; -import io.micronaut.inject.ast.ClassElement; -import io.micronaut.inject.ast.PrimitiveElement; - -/** - * Represents the void type. - * - * @author graemerocher - * @since 1.0 - * @deprecated Use {@link PrimitiveElement#VOID} instead. - */ -@Internal -@Deprecated -final class JavaVoidElement implements ClassElement, AnnotationMetadataDelegate { - - @Override - public boolean isPrimitive() { - return true; - } - - @Override - public boolean isAssignable(String type) { - return "void".equals(type); - } - - @Override - public boolean isAssignable(ClassElement type) { - return "void".equals(type.getName()); - } - - @Override - public ClassElement toArray() { - return PrimitiveElement.VOID.toArray(); - } - - @Override - public ClassElement fromArray() { - return PrimitiveElement.VOID.fromArray(); - } - - @Override - public String getName() { - return "void"; - } - - @Override - public boolean isPublic() { - return true; - } - - @Override - public boolean isProtected() { - return false; - } - - @Override - @NonNull - public Object getNativeType() { - return void.class; - } - - @Override - @NonNull - public AnnotationMetadata getAnnotationMetadata() { - return AnnotationMetadata.EMPTY_METADATA; - } - -} diff --git a/inject-java/src/test/groovy/io/micronaut/annotation/processing/visitor/JavaClassElementSpec.groovy b/inject-java/src/test/groovy/io/micronaut/annotation/processing/visitor/JavaClassElementSpec.groovy deleted file mode 100644 index 50ff3e255eb..00000000000 --- a/inject-java/src/test/groovy/io/micronaut/annotation/processing/visitor/JavaClassElementSpec.groovy +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Copyright 2017-2020 original authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.micronaut.annotation.processing.visitor - -import io.micronaut.inject.ast.AbstractClassElementSpec -import io.micronaut.inject.ast.ClassElement -import io.micronaut.inject.ast.PrimitiveElement -import spock.lang.Issue - -import javax.lang.model.element.TypeElement - -@Issue("https://github.com/micronaut-projects/micronaut-core/issues/4424") -class JavaClassElementSpec extends AbstractClassElementSpec { - @Override - protected List getClassElements() { - return [new JavaClassElement(Mock(TypeElement), null, null, [], [:]), - new JavaEnumElement(Mock(TypeElement), null, null)] - } - - @Deprecated - def "test (deprecated) JavaVoidElement array conversions"() { - given: - final jve = new JavaVoidElement() - - expect: - !jve.isArray() - - when: - final arrayType = jve.toArray() - - then: - arrayType.isArray() - - and: - arrayType.class === PrimitiveElement.class - - when: - jve.fromArray() - - then: - thrown IllegalStateException - } -} diff --git a/inject/src/main/java/io/micronaut/context/BeanContext.java b/inject/src/main/java/io/micronaut/context/BeanContext.java index 6051db87841..f87c18c155c 100644 --- a/inject/src/main/java/io/micronaut/context/BeanContext.java +++ b/inject/src/main/java/io/micronaut/context/BeanContext.java @@ -17,18 +17,16 @@ import io.micronaut.context.event.ApplicationEventPublisher; import io.micronaut.core.annotation.AnnotationMetadataResolver; +import io.micronaut.core.annotation.NonNull; +import io.micronaut.core.annotation.Nullable; import io.micronaut.core.attr.MutableAttributeHolder; import io.micronaut.core.type.Argument; import io.micronaut.inject.BeanIdentifier; import io.micronaut.inject.validation.BeanDefinitionValidator; -import io.micronaut.core.annotation.NonNull; -import io.micronaut.core.annotation.Nullable; - import java.util.Map; import java.util.Objects; import java.util.Optional; -import java.util.concurrent.Future; /** *

The core BeanContext abstraction which allows for dependency injection of classes annotated with @@ -68,29 +66,6 @@ public interface BeanContext extends return getBean(Argument.of(ApplicationEventPublisher.class, eventType)); } - /** - * Publish the given event. The event will be published synchronously and only return once all listeners have consumed the event. - * - * @deprecated Preferred way is to use event typed {@code ApplicationEventPublisher} - * @param event The event to publish - */ - @Override - @Deprecated - void publishEvent(Object event); - - /** - * Publish the given event. The event will be published asynchronously. A future is returned that can be used to check whether the event completed successfully or not. - * - * @deprecated Preferred way is to use event typed {@code ApplicationEventPublisher} - * @param event The event to publish - * @return A future that completes when the event is published - */ - @Override - @Deprecated - default Future publishEventAsync(Object event) { - return ApplicationEventPublisher.super.publishEventAsync(event); - } - /** * Inject an existing instance. * diff --git a/inject/src/main/java/io/micronaut/context/annotation/Provided.java b/inject/src/main/java/io/micronaut/context/annotation/Provided.java index 1cba54e8aa2..82a335bc133 100644 --- a/inject/src/main/java/io/micronaut/context/annotation/Provided.java +++ b/inject/src/main/java/io/micronaut/context/annotation/Provided.java @@ -17,10 +17,10 @@ import jakarta.inject.Scope; -import static java.lang.annotation.RetentionPolicy.RUNTIME; - import java.lang.annotation.Retention; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + /** *

Provided scope is used to define a bean that should not be considered a candidate for dependency injection because * it is provided by another bean. This scope is used when, for example, you have a factory bean that returns a bean diff --git a/inject/src/main/java/io/micronaut/inject/ast/ClassElement.java b/inject/src/main/java/io/micronaut/inject/ast/ClassElement.java index 10490486142..aafbd47c45c 100644 --- a/inject/src/main/java/io/micronaut/inject/ast/ClassElement.java +++ b/inject/src/main/java/io/micronaut/inject/ast/ClassElement.java @@ -18,10 +18,10 @@ import io.micronaut.core.annotation.AnnotationMetadata; import io.micronaut.core.annotation.Experimental; import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.NonNull; import io.micronaut.core.annotation.Nullable; import io.micronaut.core.naming.NameUtils; import io.micronaut.core.util.ArgumentUtils; -import io.micronaut.core.annotation.NonNull; import io.micronaut.inject.ast.beans.BeanElementBuilder; import java.lang.reflect.GenericArrayType; @@ -29,9 +29,15 @@ import java.lang.reflect.Type; import java.lang.reflect.TypeVariable; import java.lang.reflect.WildcardType; -import java.util.*; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; import java.util.function.Function; -import java.util.function.Predicate; import java.util.stream.Collectors; import static io.micronaut.inject.writer.BeanDefinitionVisitor.PROXY_SUFFIX; @@ -245,19 +251,6 @@ default List getFields() { return getEnclosedElements(ElementQuery.ALL_FIELDS); } - /** - * Return fields contained with the given modifiers include / exclude rules. - * - * @param modifierFilter Can be used to filter fields by modifier - * @return The fields - * @deprecated Use {@link #getEnclosedElements(ElementQuery)} instead - */ - @Deprecated - default List getFields(@NonNull Predicate> modifierFilter) { - Objects.requireNonNull(modifierFilter, "The modifier filter cannot be null"); - return getEnclosedElements(ElementQuery.ALL_FIELDS.modifiers(modifierFilter)); - } - /** * Return the elements that match the given query. * diff --git a/inject/src/main/java/io/micronaut/inject/processing/ProcessedTypes.java b/inject/src/main/java/io/micronaut/inject/processing/ProcessedTypes.java deleted file mode 100644 index 7452f524180..00000000000 --- a/inject/src/main/java/io/micronaut/inject/processing/ProcessedTypes.java +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright 2017-2020 original authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.micronaut.inject.processing; - -import io.micronaut.core.annotation.AnnotationUtil; - -/** - * Constants for processed type names. - * - * @author graemerocher - * @since 1.0 - * @deprecated Use {@link io.micronaut.core.annotation.AnnotationUtil} instead - */ -@Deprecated -public interface ProcessedTypes { - - /** - * Constant for {@link jakarta.annotation.PostConstruct} annotation. - */ - String POST_CONSTRUCT = AnnotationUtil.POST_CONSTRUCT; - - /** - * Constant for {@link jakarta.annotation.PreDestroy}} annotation. - */ - String PRE_DESTROY = AnnotationUtil.POST_CONSTRUCT; -} diff --git a/inject/src/main/java/io/micronaut/inject/qualifiers/InterceptorBindingQualifier.java b/inject/src/main/java/io/micronaut/inject/qualifiers/InterceptorBindingQualifier.java index a6fb22c43cb..fb93461b944 100644 --- a/inject/src/main/java/io/micronaut/inject/qualifiers/InterceptorBindingQualifier.java +++ b/inject/src/main/java/io/micronaut/inject/qualifiers/InterceptorBindingQualifier.java @@ -15,6 +15,16 @@ */ package io.micronaut.inject.qualifiers; +import io.micronaut.context.Qualifier; +import io.micronaut.core.annotation.AnnotationMetadata; +import io.micronaut.core.annotation.AnnotationUtil; +import io.micronaut.core.annotation.AnnotationValue; +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.NonNull; +import io.micronaut.core.annotation.Nullable; +import io.micronaut.core.util.CollectionUtils; +import io.micronaut.inject.BeanType; + import java.lang.annotation.Annotation; import java.util.ArrayList; import java.util.Collection; @@ -27,17 +37,6 @@ import java.util.stream.Collectors; import java.util.stream.Stream; -import io.micronaut.context.Qualifier; -import io.micronaut.core.annotation.AnnotationMetadata; -import io.micronaut.core.annotation.AnnotationUtil; -import io.micronaut.core.annotation.AnnotationValue; -import io.micronaut.core.annotation.Internal; -import io.micronaut.core.annotation.NonNull; -import io.micronaut.core.annotation.Nullable; -import io.micronaut.core.util.ArrayUtils; -import io.micronaut.core.util.CollectionUtils; -import io.micronaut.inject.BeanType; - /** * Qualifier used to resolve the interceptor binding when injection method interceptors for AOP. * @@ -105,24 +104,6 @@ public final class InterceptorBindingQualifier implements Qualifier { this.supportedInterceptorTypes = Collections.emptySet(); } - /** - * Interceptor binding qualifiers. - * @param bindingAnnotations The binding annotations - * @deprecated Use {@link #InterceptorBindingQualifier(java.util.Collection)} instead - */ - @Deprecated - InterceptorBindingQualifier(String[] bindingAnnotations) { - if (ArrayUtils.isNotEmpty(bindingAnnotations)) { - this.supportedAnnotationNames = new HashMap<>(bindingAnnotations.length); - for (String bindingAnnotation : bindingAnnotations) { - supportedAnnotationNames.put(bindingAnnotation, null); - } - } else { - this.supportedAnnotationNames = Collections.emptyMap(); - } - this.supportedInterceptorTypes = Collections.emptySet(); - } - @Override public > Stream reduce(Class beanType, Stream candidates) { return candidates.filter(candidate -> { diff --git a/inject/src/main/java/io/micronaut/inject/qualifiers/Qualifiers.java b/inject/src/main/java/io/micronaut/inject/qualifiers/Qualifiers.java index a574bca4ab9..c2ac2991c26 100644 --- a/inject/src/main/java/io/micronaut/inject/qualifiers/Qualifiers.java +++ b/inject/src/main/java/io/micronaut/inject/qualifiers/Qualifiers.java @@ -28,7 +28,6 @@ import io.micronaut.core.annotation.UsedByGeneratedCode; import io.micronaut.core.type.Argument; import io.micronaut.core.util.CollectionUtils; -import io.micronaut.core.util.StringUtils; import jakarta.inject.Named; import java.lang.annotation.Annotation; @@ -309,21 +308,6 @@ Qualifier byInterceptorBinding(@NonNull AnnotationMetadata annotationMeta return new InterceptorBindingQualifier<>(annotationMetadata); } - /** - * Reduces bean definitions by the given interceptor binding. - * - * @param bindingAnnotationNames The binding annotation names - * @param The bean type - * @return The qualifier - * @since 3.0.0 - * @deprecated Use {@link #byInterceptorBindingValues(java.util.Collection)} - */ - @Deprecated - public static @NonNull - Qualifier byInterceptorBinding(@NonNull Collection bindingAnnotationNames) { - return new InterceptorBindingQualifier<>(bindingAnnotationNames.toArray(StringUtils.EMPTY_STRING_ARRAY)); - } - /** * Reduces bean definitions by the given interceptor binding. * diff --git a/inject/src/main/java/io/micronaut/inject/visitor/VisitorContext.java b/inject/src/main/java/io/micronaut/inject/visitor/VisitorContext.java index 0eabafcc547..b0451388361 100644 --- a/inject/src/main/java/io/micronaut/inject/visitor/VisitorContext.java +++ b/inject/src/main/java/io/micronaut/inject/visitor/VisitorContext.java @@ -15,9 +15,9 @@ */ package io.micronaut.inject.visitor; +import io.micronaut.core.annotation.Experimental; import io.micronaut.core.annotation.NonNull; import io.micronaut.core.annotation.Nullable; -import io.micronaut.core.annotation.Experimental; import io.micronaut.core.convert.value.MutableConvertibleValues; import io.micronaut.inject.ast.ClassElement; import io.micronaut.inject.ast.Element; @@ -93,20 +93,6 @@ public interface VisitorContext extends MutableConvertibleValues, ClassW return VisitorConfiguration.DEFAULT; } - /** - * Visit a file within the META-INF directory. - * - * @param path The path to the file - * @return An optional file it was possible to create it - * @deprecated Visiting a file should supply the originating elements. Use {@link #visitMetaInfFile(String, Element...)} instead - */ - @Override - @Experimental - @Deprecated - default Optional visitMetaInfFile(String path) { - return visitMetaInfFile(path, Element.EMPTY_ELEMENT_ARRAY); - } - /** * Visit a file within the META-INF directory. * diff --git a/inject/src/main/java/io/micronaut/inject/writer/AbstractClassFileWriter.java b/inject/src/main/java/io/micronaut/inject/writer/AbstractClassFileWriter.java index 199016bd8e0..4c9a3c890e0 100644 --- a/inject/src/main/java/io/micronaut/inject/writer/AbstractClassFileWriter.java +++ b/inject/src/main/java/io/micronaut/inject/writer/AbstractClassFileWriter.java @@ -31,10 +31,10 @@ import io.micronaut.inject.annotation.DefaultAnnotationMetadata; import io.micronaut.inject.ast.ClassElement; import io.micronaut.inject.ast.Element; +import io.micronaut.inject.ast.GenericPlaceholderElement; import io.micronaut.inject.ast.MethodElement; import io.micronaut.inject.ast.ParameterElement; import io.micronaut.inject.ast.TypedElement; -import io.micronaut.inject.ast.GenericPlaceholderElement; import io.micronaut.inject.processing.JavaModelUtils; import org.objectweb.asm.AnnotationVisitor; import org.objectweb.asm.ClassVisitor; @@ -187,15 +187,6 @@ public abstract class AbstractClassFileWriter implements Opcodes, OriginatingEle protected final OriginatingElements originatingElements; - /** - * @param originatingElement The originating element - * @deprecated Use {@link #AbstractClassFileWriter(Element...)} instead - */ - @Deprecated - protected AbstractClassFileWriter(Element originatingElement) { - this(OriginatingElements.of(originatingElement)); - } - /** * @param originatingElements The originating elements */ diff --git a/inject/src/main/java/io/micronaut/inject/writer/ClassWriterOutputVisitor.java b/inject/src/main/java/io/micronaut/inject/writer/ClassWriterOutputVisitor.java index c0d9d005a78..b75a7dbb182 100644 --- a/inject/src/main/java/io/micronaut/inject/writer/ClassWriterOutputVisitor.java +++ b/inject/src/main/java/io/micronaut/inject/writer/ClassWriterOutputVisitor.java @@ -99,6 +99,7 @@ default OutputStream visitClass(String classname, @Nullable Element originatingE * @return An optional file it was possible to create it * @deprecated Visiting a file should supply the originating elements. Use {@link #visitMetaInfFile(String, Element...)} instead */ + // this is still needed @Deprecated default Optional visitMetaInfFile(String path) { return visitMetaInfFile(path, Element.EMPTY_ELEMENT_ARRAY); @@ -143,6 +144,7 @@ default Map> getServiceEntries() { * @param classname the fully qualified classname * @deprecated Use {@link #visitServiceDescriptor(String, String, io.micronaut.inject.ast.Element)} */ + // this is still used @Deprecated @SuppressWarnings("java:S1133") default void visitServiceDescriptor(Class type, String classname) { From be94dad0ba6e041281121c46afdbb175d1fd0df1 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 7 Oct 2022 10:12:31 +0200 Subject: [PATCH 093/743] fix(deps): update managed-testcontainers to v1.17.5 (#8105) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 4cd564825ea..3caec4bf456 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -140,7 +140,7 @@ managed-spring = "5.3.23" managed-springboot = "2.7.0" managed-swagger = "2.2.2" managed-validation = "2.0.1.Final" -managed-testcontainers = "1.17.3" +managed-testcontainers = "1.17.5" managed-snakeyaml = "1.32" micronaut-docs = "2.0.0" From ca4d779ffc1112327201596142a21753070efde5 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 7 Oct 2022 10:12:43 +0200 Subject: [PATCH 094/743] fix(deps): update managed-swagger to v2.2.3 (#8104) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 3caec4bf456..e380a3ec536 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -138,7 +138,7 @@ managed-spock = "2.0-groovy-3.0" managed-spotbugs = "4.7.1" managed-spring = "5.3.23" managed-springboot = "2.7.0" -managed-swagger = "2.2.2" +managed-swagger = "2.2.3" managed-validation = "2.0.1.Final" managed-testcontainers = "1.17.5" managed-snakeyaml = "1.32" From d369461bf2588c0c0c71aab47a4f8ab774d0598b Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 7 Oct 2022 10:14:19 +0200 Subject: [PATCH 095/743] fix(deps): update dependency io.micronaut.graphql:micronaut-graphql to v3.2.0 (#8106) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index e380a3ec536..f3fd69eba78 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -79,7 +79,7 @@ managed-micronaut-elasticsearch = "4.3.0" managed-micronaut-email = "1.3.2" managed-micronaut-flyway = "5.4.1" managed-micronaut-gcp = "4.6.0" -managed-micronaut-graphql = "3.1.0" +managed-micronaut-graphql = "3.2.0" managed-micronaut-groovy = "3.3.1" managed-micronaut-grpc = "3.3.1" managed-micronaut-hibernate-validator = "3.2.0" From eee65fe95700d5957081a1d4ffff74b4a94edfd0 Mon Sep 17 00:00:00 2001 From: micronaut-build <65172877+micronaut-build@users.noreply.github.com> Date: Fri, 7 Oct 2022 04:36:10 -0400 Subject: [PATCH 096/743] build: Bump micronaut-email to 1.4.0 (#8128) --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 8b099df7fc8..51a345fc367 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -76,7 +76,7 @@ managed-micronaut-crac = "1.0.1" managed-micronaut-data = "3.8.0" managed-micronaut-discovery = "3.2.0" managed-micronaut-elasticsearch = "4.3.0" -managed-micronaut-email = "1.3.2" +managed-micronaut-email = "1.4.0" managed-micronaut-flyway = "5.4.1" managed-micronaut-gcp = "4.6.0" managed-micronaut-graphql = "3.1.0" From ec42b98d4ebe22f0c0c011b5979346a337408187 Mon Sep 17 00:00:00 2001 From: Tim Yates Date: Fri, 7 Oct 2022 10:41:28 +0100 Subject: [PATCH 097/743] BREAKING: update to Groovy 4.0 (#8010) --- buildSrc/settings.gradle | 7 ++ ...naut.build.internal.convention-base.gradle | 9 ++ ....build.internal.convention-geb-base.gradle | 100 ++++++++++++++++++ config/accepted-api-changes.json | 6 +- gradle/geb.gradle | 16 --- gradle/libs.versions.toml | 22 ++-- gradle/webdriverbinaries.gradle | 9 -- .../http/client/SocketAddressSpec.groovy | 6 +- http-server-netty/build.gradle | 1 + .../netty/binding/HttpResponseSpec.groovy | 16 +-- .../netty/jackson/JsonFactorySetupSpec.groovy | 9 +- .../netty/jackson/JsonViewSetupSpec.groovy | 3 +- inject-groovy-test/build.gradle | 1 + .../groovy/visitor/GroovyClassElement.java | 2 +- .../RequiresBeanPropertiesSpec.groovy | 41 ++++++- inject-java-test/build.gradle | 1 + .../inject/beans/AbstractBeanSpec.groovy | 10 +- .../InheritanceSingletonSpec.groovy | 4 +- inject-kotlin-test/build.gradle | 17 +++ .../processing/test/KotlinCompileHelper.kt | 3 +- .../processing/test/KotlinCompilerTest.groovy | 9 +- .../util/VisitorContextUtilsSpec.groovy | 7 +- .../micronaut/jackson/JacksonSetupSpec.groovy | 9 +- ...PropertyNamingStrategyConverterSpec.groovy | 3 +- settings.gradle | 1 + src/main/docs/guide/appendix/breaks.adoc | 6 ++ test-suite-geb/build.gradle | 21 ++++ .../upload/browser/CreatePage.groovy | 18 ++++ .../upload/browser/FileEmptyPage.groovy | 10 ++ .../upload/browser/UploadBrowserSpec.groovy | 15 --- .../browser/UploadImageController.groovy | 18 ---- .../src/test/resources/GebConfig.groovy | 0 test-suite-geb/src/test/resources/logback.xml | 14 +++ test-suite-groovy/build.gradle | 8 +- .../secondary/SecondaryServerTest.groovy | 5 - test-suite/build.gradle | 5 +- .../upload/browser/CreatePage.groovy | 33 ------ .../upload/browser/FileEmptyPage.groovy | 25 ----- validation/build.gradle | 4 +- 39 files changed, 307 insertions(+), 187 deletions(-) create mode 100644 buildSrc/settings.gradle create mode 100644 buildSrc/src/main/groovy/io.micronaut.build.internal.convention-geb-base.gradle delete mode 100644 gradle/geb.gradle delete mode 100644 gradle/webdriverbinaries.gradle create mode 100644 test-suite-geb/build.gradle create mode 100644 test-suite-geb/src/test/groovy/io/micronaut/upload/browser/CreatePage.groovy create mode 100644 test-suite-geb/src/test/groovy/io/micronaut/upload/browser/FileEmptyPage.groovy rename {test-suite => test-suite-geb}/src/test/groovy/io/micronaut/upload/browser/UploadBrowserSpec.groovy (61%) rename {test-suite => test-suite-geb}/src/test/groovy/io/micronaut/upload/browser/UploadImageController.groovy (65%) rename {test-suite => test-suite-geb}/src/test/resources/GebConfig.groovy (100%) create mode 100644 test-suite-geb/src/test/resources/logback.xml delete mode 100644 test-suite/src/test/groovy/io/micronaut/upload/browser/CreatePage.groovy delete mode 100644 test-suite/src/test/groovy/io/micronaut/upload/browser/FileEmptyPage.groovy diff --git a/buildSrc/settings.gradle b/buildSrc/settings.gradle new file mode 100644 index 00000000000..b5a0fabf664 --- /dev/null +++ b/buildSrc/settings.gradle @@ -0,0 +1,7 @@ +dependencyResolutionManagement { + versionCatalogs { + create("libs") { + from(files("../gradle/libs.versions.toml")) + } + } +} diff --git a/buildSrc/src/main/groovy/io.micronaut.build.internal.convention-base.gradle b/buildSrc/src/main/groovy/io.micronaut.build.internal.convention-base.gradle index 3b14a76d3ca..76fde10763c 100644 --- a/buildSrc/src/main/groovy/io.micronaut.build.internal.convention-base.gradle +++ b/buildSrc/src/main/groovy/io.micronaut.build.internal.convention-base.gradle @@ -46,6 +46,15 @@ tasks.withType(Jar).configureEach { preserveFileTimestamps = false } +configurations.all { + resolutionStrategy.eachDependency { DependencyResolveDetails details -> + if (details.requested.group == 'org.codehaus.groovy') { + details.useTarget("org.apache.groovy:${details.requested.name}:${details.requested.version}") + details.because "Plugin 'io.micronaut.build.internal.common' isn't Groovy 4 yet and it's pulling in old versions" + } + } +} + dependencies { annotationProcessor libs.bundles.asm annotationProcessor(libs.micronaut.docs.map { diff --git a/buildSrc/src/main/groovy/io.micronaut.build.internal.convention-geb-base.gradle b/buildSrc/src/main/groovy/io.micronaut.build.internal.convention-geb-base.gradle new file mode 100644 index 00000000000..2c0dc4520fa --- /dev/null +++ b/buildSrc/src/main/groovy/io.micronaut.build.internal.convention-geb-base.gradle @@ -0,0 +1,100 @@ +import io.micronaut.build.internal.ext.MicronautCoreExtension +import io.micronaut.build.internal.ext.DefaultMicronautCoreExtension + +plugins { + id "io.micronaut.build.internal.base" + id "groovy" + id "java-library" +} + +micronautBuild { + enableBom = false + enableProcessing = false +} + +group = projectGroupId + +def micronautBuild = (ExtensionAware) project.extensions.getByName("micronautBuild") +def micronautCore = micronautBuild.extensions.create(MicronautCoreExtension, "core", DefaultMicronautCoreExtension, extensions.findByType(VersionCatalogsExtension)) +micronautCore.documented.convention(true) + +if (System.getProperty('geb.env')) { + apply plugin:"com.energizedwork.webdriver-binaries" + + webdriverBinaries { + chromedriver "${chromedriverVersion}" + geckodriver "${geckodriverVersion}" + } +} + +tasks.withType(Test).configureEach { + useJUnitPlatform() + jvmArgs '-Xmx2048m' + systemProperty "micronaut.cloud.platform", "OTHER" + if (JavaVersion.current().isCompatibleWith(JavaVersion.VERSION_15)) { + jvmArgs "--enable-preview" + } +} + +tasks.named("test") { + systemProperty "geb.env", System.getProperty('geb.env') + systemProperty "webdriver.chrome.driver", System.getProperty('webdriver.chrome.driver') + systemProperty "webdriver.gecko.driver", System.getProperty('webdriver.gecko.driver') +} + +tasks.withType(JavaCompile).configureEach { + options.fork = true + options.compilerArgs.add("-Amicronaut.processing.group=$project.group") + options.compilerArgs.add("-Amicronaut.processing.module=micronaut-$project.name") + options.compilerArgs.add("-Amicronaut.processing.omit.confprop.injectpoints=true") + options.forkOptions.memoryMaximumSize = "2g" +} + +tasks.withType(GroovyCompile).configureEach { + options.fork = true + options.compilerArgs.add("-Amicronaut.processing.group=$project.group") + options.compilerArgs.add("-Amicronaut.processing.module=micronaut-$project.name") + groovyOptions.forkOptions.memoryMaximumSize = "2g" +} + +// This is for reproducible builds +tasks.withType(Jar).configureEach { + reproducibleFileOrder = true + preserveFileTimestamps = false +} + +dependencies { + annotationProcessor libs.bundles.asm + annotationProcessor(libs.micronaut.docs.map { + if (micronautCore.documented.get()) { + it + } else { + null + } + }) { + transitive = false + } + + api libs.managed.slf4j + compileOnly libs.caffeine + compileOnly libs.bundles.asm + + testAnnotationProcessor project(":http-validation") + testAnnotationProcessor libs.bundles.asm + + testImplementation libs.caffeine + testImplementation libs.bundles.asm + + // Geb currently requires Groovy 3, and Spock for Groovy 3 + testImplementation libs.geb.spock + testImplementation libs.spock.for.geb + testImplementation libs.geb.groovy.test + testImplementation libs.selenium.driver.htmlunit + testImplementation libs.selenium.remote.driver + testImplementation libs.selenium.api + testImplementation libs.selenium.support + + testRuntimeOnly libs.htmlunit + testRuntimeOnly libs.selenium.driver.chrome + testRuntimeOnly libs.selenium.driver.firefox +} diff --git a/config/accepted-api-changes.json b/config/accepted-api-changes.json index ea9312644de..b16d69f0b93 100644 --- a/config/accepted-api-changes.json +++ b/config/accepted-api-changes.json @@ -1,5 +1,9 @@ [ - + { + "type": "io.micronaut.http.client.netty.DefaultHttpClient$HttpClientInitializer", + "member": "Constructor io.micronaut.http.client.netty.DefaultHttpClient$HttpClientInitializer(io.micronaut.http.client.netty.DefaultHttpClient,io.netty.handler.ssl.SslContext,java.lang.String,int,boolean,boolean,boolean,java.util.function.Consumer)", + "reason": "4.0.0 Release. All bets are off" + }, { "type": "io.micronaut.ast.groovy.visitor.AbstractGroovyElement", "member": "Class io.micronaut.ast.groovy.visitor.AbstractGroovyElement", diff --git a/gradle/geb.gradle b/gradle/geb.gradle deleted file mode 100644 index f82b46a5b9f..00000000000 --- a/gradle/geb.gradle +++ /dev/null @@ -1,16 +0,0 @@ -dependencies { - testImplementation libs.geb.spock - testImplementation libs.selenium.driver.htmlunit - testRuntimeOnly libs.htmlunit - testImplementation libs.selenium.remote.driver - testImplementation libs.selenium.api - testImplementation libs.selenium.support - testRuntimeOnly libs.selenium.driver.chrome - testRuntimeOnly libs.selenium.driver.firefox -} - -tasks.named("test") { - systemProperty "geb.env", System.getProperty('geb.env') - systemProperty "webdriver.chrome.driver", System.getProperty('webdriver.chrome.driver') - systemProperty "webdriver.gecko.driver", System.getProperty('webdriver.gecko.driver') -} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index f3fd69eba78..1e734b90d43 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -5,7 +5,11 @@ bcpkix = "1.70" blaze = "1.6.7" caffeine = "2.9.3" compile-testing = "0.19" + geb = "3.4.1" +geb-groovy = "3.0.12" +geb-spock = "2.2-groovy-3.0" + hibernate = "5.5.9.Final" hibernate-validator = "6.1.6.Final" htmlSanityCheck = "1.1.6" @@ -48,7 +52,7 @@ managed-gorm-hibernate = "7.3.0" managed-graal-sdk = "22.0.0.2" managed-graal = "22.2.0" managed-graal-svm = "22.0.0.2" -managed-groovy = "3.0.13" +managed-groovy = "4.0.5" managed-h2 = "2.1.210" managed-hystrix = "1.5.18" managed-jakarta-annotation-api = "2.1.1" @@ -134,7 +138,7 @@ managed-reactor = "3.4.23" managed-rxjava1 = "1.3.8" managed-rxjava1-interop = "0.13.7" managed-slf4j = "1.7.36" -managed-spock = "2.0-groovy-3.0" +managed-spock = "2.2-groovy-4.0" managed-spotbugs = "4.7.1" managed-spring = "5.3.23" managed-springboot = "2.7.0" @@ -186,7 +190,7 @@ boms-micronaut-r2dbc = { module = "io.micronaut.r2dbc:micronaut-r2dbc-bom", vers boms-micronaut-flyway = { module = "io.micronaut.flyway:micronaut-flyway-bom", version.ref = "managed-micronaut-flyway" } boms-micronaut-test-resources = { module = "io.micronaut.testresources:micronaut-test-resources-bom", version.ref = "managed-micronaut-test-resources" } -boms-groovy = { module = "org.codehaus.groovy:groovy-bom", version.ref = "managed-groovy" } +boms-groovy = { module = "org.apache.groovy:groovy-bom", version.ref = "managed-groovy" } boms-jackson = { module = "com.fasterxml.jackson:jackson-bom", version.ref = "managed-jackson" } boms-junit5 = { module = "org.junit:junit-bom", version.ref = "managed-junit5" } boms-kotlin = { module = "org.jetbrains.kotlin:kotlin-bom", version.ref = "managed-kotlin" } @@ -227,10 +231,10 @@ managed-gorm-hibernate = { module = "org.grails:grails-datastore-gorm-hibernate5 managed-graal = { module = "org.graalvm.nativeimage:svm", version.ref = "managed-graal-svm" } managed-graal-sdk = { module = "org.graalvm.sdk:graal-sdk", version.ref = "managed-graal-sdk" } -managed-groovy = { module = "org.codehaus.groovy:groovy", version.ref = "managed-groovy" } -managed-groovy-json = { module = "org.codehaus.groovy:groovy-json", version.ref = "managed-groovy" } -managed-groovy-sql = { module = "org.codehaus.groovy:groovy-sql", version.ref = "managed-groovy" } -managed-groovy-templates = { module = "org.codehaus.groovy:groovy-templates", version.ref = "managed-groovy" } +managed-groovy = { module = "org.apache.groovy:groovy", version.ref = "managed-groovy" } +managed-groovy-json = { module = "org.apache.groovy:groovy-json", version.ref = "managed-groovy" } +managed-groovy-sql = { module = "org.apache.groovy:groovy-sql", version.ref = "managed-groovy" } +managed-groovy-templates = { module = "org.apache.groovy:groovy-templates", version.ref = "managed-groovy" } managed-h2 = { module = "com.h2database:h2", version.ref = "managed-h2" } @@ -365,8 +369,10 @@ caffeine = { module = "com.github.ben-manes.caffeine:caffeine", version.ref = "c compile-testing = { module = "com.google.testing.compile:compile-testing", version.ref = "compile-testing" } geb-spock = { module = "org.gebish:geb-spock", version.ref = "geb" } +spock-for-geb = { module = "org.spockframework:spock-core", version.ref = "geb-spock" } +geb-groovy-test = { module = "org.codehaus.groovy:groovy-test", version.ref = "geb-groovy" } -groovy-test-junit5 = { module = "org.codehaus.groovy:groovy-test-junit5", version.ref = "managed-groovy" } +groovy-test-junit5 = { module = "org.apache.groovy:groovy-test-junit5", version.ref = "managed-groovy" } hibernate = { module = "org.hibernate:hibernate-core", version.ref = "hibernate" } hibernate-validator = { module = "org.hibernate:hibernate-validator", version.ref = "hibernate-validator" } diff --git a/gradle/webdriverbinaries.gradle b/gradle/webdriverbinaries.gradle deleted file mode 100644 index 454f66613e4..00000000000 --- a/gradle/webdriverbinaries.gradle +++ /dev/null @@ -1,9 +0,0 @@ -if (System.getProperty('geb.env')) { - apply plugin:"com.energizedwork.webdriver-binaries" - - webdriverBinaries { - chromedriver "${chromedriverVersion}" - geckodriver "${geckodriverVersion}" - } -} - diff --git a/http-client-core/src/test/groovy/io/micronaut/http/client/SocketAddressSpec.groovy b/http-client-core/src/test/groovy/io/micronaut/http/client/SocketAddressSpec.groovy index 082eee79395..8efc2e56cce 100644 --- a/http-client-core/src/test/groovy/io/micronaut/http/client/SocketAddressSpec.groovy +++ b/http-client-core/src/test/groovy/io/micronaut/http/client/SocketAddressSpec.groovy @@ -13,7 +13,7 @@ class SocketAddressSpec extends Specification { ConversionService converter = ctx.getBean(ConversionService) when: - Optional address = converter.convert("1.2.3.4:8080", SocketAddress.class) + Optional address = converter.convert("1.2.3.4:8080", SocketAddress) then: address.isPresent() @@ -22,7 +22,7 @@ class SocketAddressSpec extends Specification { ((InetSocketAddress) address.get()).getPort() == 8080 when: - address = converter.convert("https://foo.bar:8081", SocketAddress.class) + address = converter.convert("https://foo.bar:8081", SocketAddress) then: address.isPresent() @@ -31,7 +31,7 @@ class SocketAddressSpec extends Specification { ((InetSocketAddress) address.get()).getPort() == 8081 when: - ConversionContext conversionContext = ArgumentConversionContext.of(SocketAddress.class) + ConversionContext conversionContext = ConversionContext.of(SocketAddress) address = converter.convert("abc:456456456456", conversionContext) then: diff --git a/http-server-netty/build.gradle b/http-server-netty/build.gradle index bc871ce3e97..a568bdf19f5 100644 --- a/http-server-netty/build.gradle +++ b/http-server-netty/build.gradle @@ -45,6 +45,7 @@ dependencies { testImplementation(libs.managed.micronaut.xml) { exclude module:'micronaut-inject' exclude module:'micronaut-http' + exclude module:'micronaut-bom' } testImplementation libs.managed.jackson.databind diff --git a/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/binding/HttpResponseSpec.groovy b/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/binding/HttpResponseSpec.groovy index df3fe92a1cc..2c2ea5d784e 100644 --- a/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/binding/HttpResponseSpec.groovy +++ b/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/binding/HttpResponseSpec.groovy @@ -162,7 +162,7 @@ class HttpResponseSpec extends AbstractMicronautSpec { void "test server header"() { given: - EmbeddedServer server = ApplicationContext.run(EmbeddedServer, ['micronaut.server.serverHeader': 'Foo!', (SPEC_NAME_PROPERTY):getClass().simpleName]) + EmbeddedServer server = ApplicationContext.run(EmbeddedServer, ['micronaut.server.server-header': 'Foo!', (SPEC_NAME_PROPERTY):getClass().simpleName]) def ctx = server.getApplicationContext() HttpClient client = ctx.createBean(HttpClient, server.getURL()) @@ -216,7 +216,7 @@ class HttpResponseSpec extends AbstractMicronautSpec { void "test date header turned off"() { given: - EmbeddedServer server = ApplicationContext.run(EmbeddedServer, ['micronaut.server.dateHeader': false, (SPEC_NAME_PROPERTY):getClass().simpleName]) + EmbeddedServer server = ApplicationContext.run(EmbeddedServer, ['micronaut.server.date-header': false, (SPEC_NAME_PROPERTY):getClass().simpleName]) ApplicationContext ctx = server.getApplicationContext() HttpClient client = ctx.createBean(HttpClient, server.getURL()) @@ -234,9 +234,9 @@ class HttpResponseSpec extends AbstractMicronautSpec { void "test keep alive connection header is not set by default for > 499 response"() { when: - EmbeddedServer server = applicationContext.run(EmbeddedServer, [(SPEC_NAME_PROPERTY):getClass().simpleName]) + EmbeddedServer server = ApplicationContext.run(EmbeddedServer, ['micronaut.server.date-header': false, (SPEC_NAME_PROPERTY):getClass().simpleName]) ApplicationContext ctx = server.getApplicationContext() - HttpClient client = applicationContext.createBean(HttpClient, embeddedServer.getURL()) + HttpClient client = ctx.createBean(HttpClient, server.getURL()) Flux.from(client.exchange( HttpRequest.GET('/test-header/fail') @@ -256,14 +256,16 @@ class HttpResponseSpec extends AbstractMicronautSpec { void "test connection header is defaulted to keep-alive when configured to true for > 499 response"() { when: DefaultHttpClientConfiguration config = new DefaultHttpClientConfiguration() + // The client will explicitly request "Connection: close" unless using a connection pool, so set it up config.connectionPoolConfiguration.enabled = true - EmbeddedServer server = applicationContext.run(EmbeddedServer, [ + + EmbeddedServer server = ApplicationContext.run(EmbeddedServer, [ (SPEC_NAME_PROPERTY):getClass().simpleName, 'micronaut.server.netty.keepAliveOnServerError':true ]) def ctx = server.getApplicationContext() - HttpClient client = applicationContext.createBean(HttpClient, embeddedServer.getURL(), config) + HttpClient client = ctx.createBean(HttpClient, embeddedServer.getURL(), config) Flux.from(client.exchange( HttpRequest.GET('/test-header/fail') @@ -301,6 +303,6 @@ class HttpResponseSpec extends AbstractMicronautSpec { @Override Map getConfiguration() { - super.getConfiguration() << ['micronaut.server.dateHeader': false] + super.getConfiguration() << ['micronaut.server.date-header': false] } } diff --git a/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/jackson/JsonFactorySetupSpec.groovy b/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/jackson/JsonFactorySetupSpec.groovy index e70567c60c7..ade665a8be6 100644 --- a/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/jackson/JsonFactorySetupSpec.groovy +++ b/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/jackson/JsonFactorySetupSpec.groovy @@ -16,15 +16,11 @@ package io.micronaut.http.server.netty.jackson import com.fasterxml.jackson.core.JsonFactory -import com.fasterxml.jackson.core.util.BufferRecycler import com.fasterxml.jackson.databind.ObjectMapper import io.micronaut.context.ApplicationContext import io.micronaut.context.DefaultApplicationContext import io.micronaut.context.env.MapPropertySource -import io.micronaut.docs.context.annotation.primary.ColorPicker -import io.micronaut.http.HttpRequest -import io.micronaut.http.HttpResponse -import io.micronaut.http.HttpStatus +import io.micronaut.context.env.PropertySource import spock.lang.Specification /** @@ -34,7 +30,6 @@ import spock.lang.Specification class JsonFactorySetupSpec extends Specification { void "verify default jackson setup with JsonFactory bean"() { - given: ApplicationContext applicationContext = new DefaultApplicationContext("test").start() @@ -49,7 +44,7 @@ class JsonFactorySetupSpec extends Specification { void "verify JsonFactory properties are injected into the bean"() { given: ApplicationContext applicationContext = new DefaultApplicationContext("test") - applicationContext.environment.addPropertySource(MapPropertySource.of( + applicationContext.environment.addPropertySource((MapPropertySource) PropertySource.of( 'jackson.factory.use-thread-local-for-buffer-recycling': false )) applicationContext.start() diff --git a/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/jackson/JsonViewSetupSpec.groovy b/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/jackson/JsonViewSetupSpec.groovy index bf1c5425324..d25315fb12f 100644 --- a/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/jackson/JsonViewSetupSpec.groovy +++ b/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/jackson/JsonViewSetupSpec.groovy @@ -19,6 +19,7 @@ package io.micronaut.http.server.netty.jackson import io.micronaut.context.ApplicationContext import io.micronaut.context.DefaultApplicationContext import io.micronaut.context.env.MapPropertySource +import io.micronaut.context.env.PropertySource import io.micronaut.jackson.JacksonConfiguration import spock.lang.Specification @@ -43,7 +44,7 @@ class JsonViewSetupSpec extends Specification { given: ApplicationContext applicationContext = new DefaultApplicationContext("test") - applicationContext.environment.addPropertySource(MapPropertySource.of( + applicationContext.environment.addPropertySource((MapPropertySource) PropertySource.of( 'jackson.json-view.enabled': true )) applicationContext.start() diff --git a/inject-groovy-test/build.gradle b/inject-groovy-test/build.gradle index 34e0b706f7e..29e331e3e82 100644 --- a/inject-groovy-test/build.gradle +++ b/inject-groovy-test/build.gradle @@ -11,6 +11,7 @@ dependencies { exclude module:'groovy-all' } api project(":context") + api libs.jetbrains.annotations } tasks.named("sourcesJar") { diff --git a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyClassElement.java b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyClassElement.java index efec5f1f4d7..b76bd1abdde 100644 --- a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyClassElement.java +++ b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyClassElement.java @@ -906,7 +906,7 @@ public Optional getReadMethod() { private String getGetterName(String propertyName, ClassElement type) { return NameUtils.getterNameFor( propertyName, - type.equals(PrimitiveElement.BOOLEAN) || type.getName().equals(Boolean.class.getName()) + type.equals(PrimitiveElement.BOOLEAN) ); } }; diff --git a/inject-groovy/src/test/groovy/io/micronaut/inject/requires/RequiresBeanPropertiesSpec.groovy b/inject-groovy/src/test/groovy/io/micronaut/inject/requires/RequiresBeanPropertiesSpec.groovy index 0cd639f96af..f4ba769afe2 100644 --- a/inject-groovy/src/test/groovy/io/micronaut/inject/requires/RequiresBeanPropertiesSpec.groovy +++ b/inject-groovy/src/test/groovy/io/micronaut/inject/requires/RequiresBeanPropertiesSpec.groovy @@ -113,6 +113,39 @@ class Config Boolean boolProperty } +@Singleton +@Requires(bean = Config.class, beanProperty = "boolProperty", notEquals = "true") +class DependantBean +{ +} +''') + def type = context.classLoader.loadClass('test.DependantBean') + + when: + context.environment.addPropertySource(PropertySource.of("test", ['test.bool-property': "true"])) + context.getBean(type) + + then: + thrown(NoSuchBeanException.class) + + cleanup: + context.close() + } + + void "test requires not equals property value with primitive value set"() { + given: + ApplicationContext context = buildContext(''' +package test +import io.micronaut.context.annotation.* +import io.micronaut.core.annotation.* +import jakarta.inject.Singleton + +@ConfigurationProperties("test") +class Config +{ + boolean boolProperty +} + @Singleton @Requires(bean = Config.class, beanProperty = "boolProperty", notEquals = "true") class DependantBean @@ -143,11 +176,11 @@ import jakarta.inject.Singleton @ConfigurationProperties("config.properties") class Config { private String property - + public String getProperty() { return property; } - + public void setProperty(String property) { this.property = property; } @@ -561,7 +594,7 @@ class Config { Integer intProperty String stringProperty - + @ConfigurationProperties("inner") static class InnerConfig { String innerProperty = "default value" @@ -615,7 +648,7 @@ interface Configuration extends Toggleable {} class ConfigurationImpl implements Configuration { boolean enabled = false; - + @Override boolean isEnabled() { return enabled; diff --git a/inject-java-test/build.gradle b/inject-java-test/build.gradle index 74d6da36bd9..63a7dbdad7a 100644 --- a/inject-java-test/build.gradle +++ b/inject-java-test/build.gradle @@ -17,6 +17,7 @@ dependencies { if (!JavaVersion.current().isJava9Compatible()) { api files(org.gradle.internal.jvm.Jvm.current().toolsJar) } + api libs.jetbrains.annotations testAnnotationProcessor project(":inject-java") testCompileOnly project(":inject-groovy") diff --git a/inject-java/src/test/groovy/io/micronaut/inject/beans/AbstractBeanSpec.groovy b/inject-java/src/test/groovy/io/micronaut/inject/beans/AbstractBeanSpec.groovy index c2ea3c6793d..b2d80c2ebba 100644 --- a/inject-java/src/test/groovy/io/micronaut/inject/beans/AbstractBeanSpec.groovy +++ b/inject-java/src/test/groovy/io/micronaut/inject/beans/AbstractBeanSpec.groovy @@ -17,7 +17,7 @@ package io.micronaut.inject.beans import io.micronaut.aop.Intercepted import io.micronaut.context.ApplicationContext -import io.micronaut.context.DefaultBeanContext +import io.micronaut.context.BeanContext import io.micronaut.annotation.processing.test.AbstractTypeElementSpec import io.micronaut.inject.BeanDefinition @@ -37,7 +37,7 @@ import jakarta.inject.Inject; import java.util.List; class Test { - + @Inject List list; } @@ -84,7 +84,7 @@ abstract class AbstractBean { void "test getBeansOfType filters proxy targets"() { when: - def ctx = DefaultBeanContext.run() + def ctx = BeanContext.run() def targetBean = ctx.getProxyTargetBean(InterceptedBean, null) def bean = ctx.getBean(InterceptedBean) @@ -102,7 +102,7 @@ abstract class AbstractBean { void "test getBeansOfType filters proxy targets with context scoped beans"() { when: - def ctx = DefaultBeanContext.run() + def ctx = BeanContext.run() def targetBean = ctx.getProxyTargetBean(ContextScopedInterceptedBean, null) def bean = ctx.getBean(ContextScopedInterceptedBean) @@ -120,7 +120,7 @@ abstract class AbstractBean { void "test getBeansOfType filters proxy targets with parallel beans"() { when: - def ctx = DefaultBeanContext.run() + def ctx = BeanContext.run() Thread.sleep(100) def targetBean = ctx.getProxyTargetBean(ParallelBean, null) def bean = ctx.getBean(ParallelBean) diff --git a/inject-java/src/test/groovy/io/micronaut/inject/beans/inheritance/InheritanceSingletonSpec.groovy b/inject-java/src/test/groovy/io/micronaut/inject/beans/inheritance/InheritanceSingletonSpec.groovy index 77b287e2917..7939a3a61a9 100644 --- a/inject-java/src/test/groovy/io/micronaut/inject/beans/inheritance/InheritanceSingletonSpec.groovy +++ b/inject-java/src/test/groovy/io/micronaut/inject/beans/inheritance/InheritanceSingletonSpec.groovy @@ -1,13 +1,13 @@ package io.micronaut.inject.beans.inheritance -import io.micronaut.context.DefaultBeanContext +import io.micronaut.context.BeanContext import io.micronaut.inject.qualifiers.Qualifiers import spock.lang.Specification class InheritanceSingletonSpec extends Specification { void "test getBeansOfType returns the same instance"() { - def ctx = DefaultBeanContext.run() + def ctx = BeanContext.run() def bankService = ctx.getBean(BankService) when: diff --git a/inject-kotlin-test/build.gradle b/inject-kotlin-test/build.gradle index 537b5280c58..842fefc0611 100644 --- a/inject-kotlin-test/build.gradle +++ b/inject-kotlin-test/build.gradle @@ -43,3 +43,20 @@ tasks.named("compileGroovy") { dependsOn tasks.getByPath('compileKotlin') classpath += files(compileKotlin.destinationDirectory) } + +tasks.named("test", Test) { + if(JavaVersion.current().majorVersion.toInteger() >= 17) { + jvmArgs( + '--add-opens', 'jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED', + '--add-opens', 'jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED', + '--add-opens', 'jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED', + '--add-opens', 'jdk.compiler/com.sun.tools.javac.jvm=ALL-UNNAMED', + '--add-opens', 'jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED', + '--add-opens', 'jdk.compiler/com.sun.tools.javac.processing=ALL-UNNAMED', + '--add-opens', 'jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED', + '--add-opens', 'jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED', + '--add-opens', 'jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED', + '--add-opens', 'jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED' + ) + } +} diff --git a/inject-kotlin-test/src/main/kotlin/io/micronaut/annotation/processing/test/KotlinCompileHelper.kt b/inject-kotlin-test/src/main/kotlin/io/micronaut/annotation/processing/test/KotlinCompileHelper.kt index 36bdaa7825e..2735410a962 100644 --- a/inject-kotlin-test/src/main/kotlin/io/micronaut/annotation/processing/test/KotlinCompileHelper.kt +++ b/inject-kotlin-test/src/main/kotlin/io/micronaut/annotation/processing/test/KotlinCompileHelper.kt @@ -120,7 +120,8 @@ object KotlinCompileHelper { message: String, location: CompilerMessageSourceLocation? ) { - if (severity == CompilerMessageSeverity.ERROR) { + // With Java 17 and Groovy 4.x this breaks inject-kotlin-test:KotlinCompilerTest as it throws an AssertionError for the Note: message + if (severity == CompilerMessageSeverity.ERROR && !message.startsWith("Note:")) { throw AssertionError("Error reported in processing: $message") } } diff --git a/inject-kotlin-test/src/test/groovy/io/micronaut/annotation/processing/test/KotlinCompilerTest.groovy b/inject-kotlin-test/src/test/groovy/io/micronaut/annotation/processing/test/KotlinCompilerTest.groovy index b5aa8eec46a..ed249f9cc34 100644 --- a/inject-kotlin-test/src/test/groovy/io/micronaut/annotation/processing/test/KotlinCompilerTest.groovy +++ b/inject-kotlin-test/src/test/groovy/io/micronaut/annotation/processing/test/KotlinCompilerTest.groovy @@ -2,14 +2,11 @@ package io.micronaut.annotation.processing.test import io.micronaut.core.beans.BeanIntrospection import io.micronaut.core.version.SemanticVersion +import spock.lang.Ignore +import spock.lang.IgnoreIf import spock.lang.Requires import spock.util.environment.Jvm -// fails due to https://issues.apache.org/jira/browse/GROOVY-10145 -@Requires({ - SemanticVersion.isAtLeastMajorMinor(GroovySystem.version, 4, 0) || - !Jvm.current.isJava16Compatible() -}) class KotlinCompilerTest extends AbstractKotlinCompilerSpec { void "simple class"() { given: @@ -53,7 +50,7 @@ class Foo { @Singleton class Bar { - + } ''') def bean = getBean(context, "example.Foo") diff --git a/inject/src/test/groovy/io/micronaut/inject/util/VisitorContextUtilsSpec.groovy b/inject/src/test/groovy/io/micronaut/inject/util/VisitorContextUtilsSpec.groovy index 0dcd9e31419..7adc5b4a204 100644 --- a/inject/src/test/groovy/io/micronaut/inject/util/VisitorContextUtilsSpec.groovy +++ b/inject/src/test/groovy/io/micronaut/inject/util/VisitorContextUtilsSpec.groovy @@ -8,6 +8,7 @@ import javax.annotation.processing.ProcessingEnvironment @Stepwise class VisitorContextUtilsSpec extends Specification { + public static final String INVALID_CUSTOM_PROP_1 = "invalid.custom.prop" public static final String VALID_CUSTOM_PROP_1 = "micronaut.custom.prop" public static final String VALID_CUSTOM_PROP_2 = "micronaut.another.custom.prop" @@ -52,9 +53,9 @@ class VisitorContextUtilsSpec extends Specification { with(options) { size() == 2 - containsKey(VALID_CUSTOM_PROP_1) - containsKey(VALID_CUSTOM_PROP_2) - !containsKey(INVALID_CUSTOM_PROP_1) + containsKey(VisitorContextUtilsSpec.VALID_CUSTOM_PROP_1) + containsKey(VisitorContextUtilsSpec.VALID_CUSTOM_PROP_2) + !containsKey(VisitorContextUtilsSpec.INVALID_CUSTOM_PROP_1) } } diff --git a/jackson-databind/src/test/groovy/io/micronaut/jackson/JacksonSetupSpec.groovy b/jackson-databind/src/test/groovy/io/micronaut/jackson/JacksonSetupSpec.groovy index 7ff2a480e05..c591717d28e 100644 --- a/jackson-databind/src/test/groovy/io/micronaut/jackson/JacksonSetupSpec.groovy +++ b/jackson-databind/src/test/groovy/io/micronaut/jackson/JacksonSetupSpec.groovy @@ -21,6 +21,7 @@ import com.fasterxml.jackson.databind.SerializationFeature import io.micronaut.context.ApplicationContext import io.micronaut.context.DefaultApplicationContext import io.micronaut.context.env.MapPropertySource +import io.micronaut.context.env.PropertySource import spock.lang.Specification import spock.lang.Unroll @@ -61,7 +62,7 @@ class JacksonSetupSpec extends Specification { given: ApplicationContext applicationContext = new DefaultApplicationContext("test") - applicationContext.environment.addPropertySource(MapPropertySource.of( + applicationContext.environment.addPropertySource((MapPropertySource) PropertySource.of( 'jackson.dateFormat': 'yyMMdd', 'jackson.serialization.indentOutput': true, 'jackson.json-view.enabled': true @@ -83,7 +84,7 @@ class JacksonSetupSpec extends Specification { void "verify that the defaultTyping configuration option is correctly converted and set on the object mapper"() { given: ApplicationContext applicationContext = new DefaultApplicationContext("test") - applicationContext.environment.addPropertySource(MapPropertySource.of( + applicationContext.environment.addPropertySource((MapPropertySource) PropertySource.of( 'jackson.dateFormat': 'yyMMdd', 'jackson.defaultTyping': 'NON_FINAL' )) @@ -103,7 +104,7 @@ class JacksonSetupSpec extends Specification { @Unroll void 'Configuring #configuredJackonPropertyNamingStrategy sets PropertyNamingStrategy on the Context ObjectMapper.'() { when: - ApplicationContext applicationContext = ApplicationContext.run(MapPropertySource.of( + ApplicationContext applicationContext = ApplicationContext.run((MapPropertySource) PropertySource.of( 'jackson.property-naming-strategy': configuredJackonPropertyNamingStrategy.toString() )) @@ -137,7 +138,7 @@ class JacksonSetupSpec extends Specification { void "verify trim strings with custom property enabled"() { given: ApplicationContext applicationContext = new DefaultApplicationContext("test") - applicationContext.environment.addPropertySource(MapPropertySource.of( + applicationContext.environment.addPropertySource((MapPropertySource) PropertySource.of( 'jackson.trim-strings': true )) applicationContext.start() diff --git a/jackson-databind/src/test/groovy/io/micronaut/jackson/bind/CharSequencePropertyNamingStrategyConverterSpec.groovy b/jackson-databind/src/test/groovy/io/micronaut/jackson/bind/CharSequencePropertyNamingStrategyConverterSpec.groovy index fbacffb6fd7..342be9255b3 100644 --- a/jackson-databind/src/test/groovy/io/micronaut/jackson/bind/CharSequencePropertyNamingStrategyConverterSpec.groovy +++ b/jackson-databind/src/test/groovy/io/micronaut/jackson/bind/CharSequencePropertyNamingStrategyConverterSpec.groovy @@ -18,7 +18,6 @@ package io.micronaut.jackson.bind import com.fasterxml.jackson.databind.PropertyNamingStrategies import com.fasterxml.jackson.databind.PropertyNamingStrategy import io.micronaut.context.ApplicationContext -import io.micronaut.core.convert.ArgumentConversionContext import io.micronaut.core.convert.ConversionContext import io.micronaut.core.convert.ConversionError import io.micronaut.core.convert.ConversionService @@ -61,7 +60,7 @@ class CharSequencePropertyNamingStrategyConverterSpec extends Specification { def converter = ctx.getBean(ConversionService) when: - ConversionContext conversionContext = ArgumentConversionContext.of(CharSequence) + ConversionContext conversionContext = ConversionContext.of(CharSequence) converter.convert(invalidString, PropertyNamingStrategy, conversionContext) ConversionError conversionError = conversionContext.last() diff --git a/settings.gradle b/settings.gradle index 2a4ada517da..277e478b387 100644 --- a/settings.gradle +++ b/settings.gradle @@ -58,6 +58,7 @@ include "websocket" // test suites include "test-suite" +include "test-suite-geb" include "test-suite-helper" include "test-suite-javax-inject" include "test-suite-jakarta-inject-bean-import" diff --git a/src/main/docs/guide/appendix/breaks.adoc b/src/main/docs/guide/appendix/breaks.adoc index e6e3e7490fa..c2b8b0e978b 100644 --- a/src/main/docs/guide/appendix/breaks.adoc +++ b/src/main/docs/guide/appendix/breaks.adoc @@ -8,6 +8,12 @@ This section documents breaking changes between Micronaut versions https://github.com/ben-manes/caffeine[Caffeine] is no longer shaded into the `io.micronaut.caffeine` package. If you depend on this library you should directly depend on the latest version of Caffeine. +==== Update to Groovy 4 + +Micronaut now uses Groovy 4. +This means that Groovy 4 is now the minimum version required to run Groovy Micronaut applications. +There have been several core differences in Groovy parsing and behavior for version 4 which can be found in the breaking changes section of the https://groovy-lang.org/releasenotes/groovy-4.0.html[4.0.0 release notes]. + ==== SnakeYAML no longer a direct dependency SnakeYAML is no longer a direct dependency, if you need YAML configuration you should add SnakeYAML to your classpath explicitly diff --git a/test-suite-geb/build.gradle b/test-suite-geb/build.gradle new file mode 100644 index 00000000000..2ae34854e5f --- /dev/null +++ b/test-suite-geb/build.gradle @@ -0,0 +1,21 @@ +plugins { + id "io.micronaut.build.internal.convention-geb-base" +} + +micronautBuild { + core { + usesMicronautTestJunit() + usesMicronautTestSpock() + } +} + +dependencies { + testImplementation(project(":inject-groovy")) { + exclude module: 'groovy' + } + + testImplementation project(':http') + testImplementation project(':http-server-netty') + + testRuntimeOnly "ch.qos.logback:logback-classic:1.2.3" +} diff --git a/test-suite-geb/src/test/groovy/io/micronaut/upload/browser/CreatePage.groovy b/test-suite-geb/src/test/groovy/io/micronaut/upload/browser/CreatePage.groovy new file mode 100644 index 00000000000..b68bcf4606d --- /dev/null +++ b/test-suite-geb/src/test/groovy/io/micronaut/upload/browser/CreatePage.groovy @@ -0,0 +1,18 @@ +package io.micronaut.upload.browser + +import geb.Page + +class CreatePage extends Page { + + static url = '/image/create' + + static at = { title == 'Create Image' } + + static content = { + uploadButton { $('input', type: 'submit', value: 'Upload') } + } + + void upload() { + uploadButton.click() + } +} diff --git a/test-suite-geb/src/test/groovy/io/micronaut/upload/browser/FileEmptyPage.groovy b/test-suite-geb/src/test/groovy/io/micronaut/upload/browser/FileEmptyPage.groovy new file mode 100644 index 00000000000..7495add2d5a --- /dev/null +++ b/test-suite-geb/src/test/groovy/io/micronaut/upload/browser/FileEmptyPage.groovy @@ -0,0 +1,10 @@ +package io.micronaut.upload.browser + +import geb.Page + +class FileEmptyPage extends Page { + + static url = '/image/save' + + static at = { title == 'File is Empty' } +} diff --git a/test-suite/src/test/groovy/io/micronaut/upload/browser/UploadBrowserSpec.groovy b/test-suite-geb/src/test/groovy/io/micronaut/upload/browser/UploadBrowserSpec.groovy similarity index 61% rename from test-suite/src/test/groovy/io/micronaut/upload/browser/UploadBrowserSpec.groovy rename to test-suite-geb/src/test/groovy/io/micronaut/upload/browser/UploadBrowserSpec.groovy index d9baf8e4630..23163f7a040 100644 --- a/test-suite/src/test/groovy/io/micronaut/upload/browser/UploadBrowserSpec.groovy +++ b/test-suite-geb/src/test/groovy/io/micronaut/upload/browser/UploadBrowserSpec.groovy @@ -1,18 +1,3 @@ -/* - * Copyright 2017-2019 original authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ package io.micronaut.upload.browser import geb.spock.GebSpec diff --git a/test-suite/src/test/groovy/io/micronaut/upload/browser/UploadImageController.groovy b/test-suite-geb/src/test/groovy/io/micronaut/upload/browser/UploadImageController.groovy similarity index 65% rename from test-suite/src/test/groovy/io/micronaut/upload/browser/UploadImageController.groovy rename to test-suite-geb/src/test/groovy/io/micronaut/upload/browser/UploadImageController.groovy index 4692f9851ac..04c9489b6ec 100644 --- a/test-suite/src/test/groovy/io/micronaut/upload/browser/UploadImageController.groovy +++ b/test-suite-geb/src/test/groovy/io/micronaut/upload/browser/UploadImageController.groovy @@ -1,18 +1,3 @@ -/* - * Copyright 2017-2020 original authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ package io.micronaut.upload.browser import io.micronaut.context.annotation.Requires @@ -42,7 +27,4 @@ class UploadImageController { String title = file.getSize() == 0 ? 'File is Empty' : 'File Saved' "${title}" } - - - } diff --git a/test-suite/src/test/resources/GebConfig.groovy b/test-suite-geb/src/test/resources/GebConfig.groovy similarity index 100% rename from test-suite/src/test/resources/GebConfig.groovy rename to test-suite-geb/src/test/resources/GebConfig.groovy diff --git a/test-suite-geb/src/test/resources/logback.xml b/test-suite-geb/src/test/resources/logback.xml new file mode 100644 index 00000000000..44b79c40d49 --- /dev/null +++ b/test-suite-geb/src/test/resources/logback.xml @@ -0,0 +1,14 @@ + + + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + diff --git a/test-suite-groovy/build.gradle b/test-suite-groovy/build.gradle index 9aab800b056..5b8cf4f1a9f 100644 --- a/test-suite-groovy/build.gradle +++ b/test-suite-groovy/build.gradle @@ -28,15 +28,9 @@ dependencies { testImplementation project(":function-web") testRuntimeOnly(platform(libs.boms.micronaut.aws)) testRuntimeOnly libs.aws.java.sdk.lambda + testRuntimeOnly libs.bcpkix testImplementation libs.managed.reactor } -//tasks.withType(Test).configureEach { -// testLogging { -// showStandardStreams = true -// exceptionFormat = 'full' -// } -//} - //compileTestGroovy.groovyOptions.forkOptions.jvmArgs = ['-Xdebug', '-Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=5005'] diff --git a/test-suite-groovy/src/test/groovy/io/micronaut/docs/http/server/secondary/SecondaryServerTest.groovy b/test-suite-groovy/src/test/groovy/io/micronaut/docs/http/server/secondary/SecondaryServerTest.groovy index fe04d6d16ea..3c75829bc89 100644 --- a/test-suite-groovy/src/test/groovy/io/micronaut/docs/http/server/secondary/SecondaryServerTest.groovy +++ b/test-suite-groovy/src/test/groovy/io/micronaut/docs/http/server/secondary/SecondaryServerTest.groovy @@ -19,11 +19,6 @@ import spock.util.environment.Jvm @MicronautTest @Property(name = "secondary.enabled", value = StringUtils.TRUE) @Property(name = "micronaut.http.client.ssl.insecure-trust-all-certificates", value = StringUtils.TRUE) -// fails due to https://issues.apache.org/jira/browse/GROOVY-10145 -@Requires({ - SemanticVersion.isAtLeastMajorMinor(GroovySystem.version, 4, 0) || - !Jvm.current.isJava16Compatible() -}) class SecondaryServerTest extends Specification { // tag::inject[] @Client(path = "/", id = SecondaryNettyServer.SERVER_ID) diff --git a/test-suite/build.gradle b/test-suite/build.gradle index e3375e3fb0f..8a7f190001b 100644 --- a/test-suite/build.gradle +++ b/test-suite/build.gradle @@ -22,9 +22,6 @@ micronautBuild { } } -apply from: "${rootProject.projectDir}/gradle/geb.gradle" -apply from: "${rootProject.projectDir}/gradle/webdriverbinaries.gradle" - dependencies { annotationProcessor project(":inject-java") api project(":inject") @@ -56,6 +53,7 @@ dependencies { exclude module: 'micronaut-runtime' } testImplementation libs.managed.groovy.json + testImplementation libs.managed.groovy.templates // tag::testcontainers-dependencies[] testImplementation libs.testcontainers.spock // end::testcontainers-dependencies[] @@ -91,6 +89,7 @@ dependencies { testFixturesApi libs.managed.spock testFixturesApi libs.managed.groovy + testFixturesApi libs.jetbrains.annotations } //tasks.withType(Test).configureEach { diff --git a/test-suite/src/test/groovy/io/micronaut/upload/browser/CreatePage.groovy b/test-suite/src/test/groovy/io/micronaut/upload/browser/CreatePage.groovy deleted file mode 100644 index 4d5e5d22527..00000000000 --- a/test-suite/src/test/groovy/io/micronaut/upload/browser/CreatePage.groovy +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright 2017-2020 original authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.micronaut.upload.browser - -import geb.Page - -class CreatePage extends Page { - - static url = '/image/create' - - static at = { title == 'Create Image' } - - static content = { - uploadButton { $('input', type: 'submit', value: 'Upload') } - } - - void upload() { - uploadButton.click() - } -} diff --git a/test-suite/src/test/groovy/io/micronaut/upload/browser/FileEmptyPage.groovy b/test-suite/src/test/groovy/io/micronaut/upload/browser/FileEmptyPage.groovy deleted file mode 100644 index f8b6ee02802..00000000000 --- a/test-suite/src/test/groovy/io/micronaut/upload/browser/FileEmptyPage.groovy +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright 2017-2020 original authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.micronaut.upload.browser - -import geb.Page - -class FileEmptyPage extends Page { - - static url = '/image/save' - - static at = { title == 'File is Empty' } -} diff --git a/validation/build.gradle b/validation/build.gradle index 9efa30c8d85..35c41696ca0 100644 --- a/validation/build.gradle +++ b/validation/build.gradle @@ -15,7 +15,9 @@ dependencies { api project(":core-reactive") api libs.managed.validation - compileOnly libs.managed.gorm + compileOnly(libs.managed.gorm) { + exclude(module: 'groovy') + } compileOnly project(":http-server") implementation libs.managed.reactor From 5d5bcbbdddb235d79c26ec5fc0e583c62f533c24 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 10 Oct 2022 11:58:07 +0200 Subject: [PATCH 098/743] fix(deps): update managed-kotlin to v1.7.20 (#7855) * fix(deps): update managed-kotlin to v1.7.20 * managed-kotlin is updated to 1.7.20 * also update kotlin for project gradle.properties Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Dean Wette --- gradle.properties | 2 +- gradle/libs.versions.toml | 2 +- src/main/docs/guide/appendix/breaks.adoc | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/gradle.properties b/gradle.properties index bd42ee8c7af..40a8a0b1c4a 100644 --- a/gradle.properties +++ b/gradle.properties @@ -47,7 +47,7 @@ micronautMavenPluginVersion=3.4.0 chromedriverVersion=79.0.3945.36 geckodriverVersion=0.26.0 webdriverBinariesVersion=1.4 -kotlinVersion=1.7.10 +kotlinVersion=1.7.20 kotlin.stdlib.default.dependency=false # For the docs diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 1e734b90d43..4a9e88f2825 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -41,7 +41,7 @@ managed-dekorate = "1.0.3" managed-elasticsearch = "7.16.3" managed-ignite = "2.13.0" managed-junit5 = "5.9.1" -managed-kotlin = "1.7.10" +managed-kotlin = "1.7.20" managed-kotlin-coroutines = "1.6.4" managed-google-function-framework = "1.0.4" managed-google-function-invoker = "1.0.0" diff --git a/src/main/docs/guide/appendix/breaks.adoc b/src/main/docs/guide/appendix/breaks.adoc index c2b8b0e978b..ae91f9b645b 100644 --- a/src/main/docs/guide/appendix/breaks.adoc +++ b/src/main/docs/guide/appendix/breaks.adoc @@ -22,9 +22,9 @@ SnakeYAML is no longer a direct dependency, if you need YAML configuration you s The `javax.annotation` library is no longer a directory dependency. Any references to types in the `javax.anotation` package should be changed to `jakarta.annotation` -==== Kotlin base version updated to 1.7.10 +==== Kotlin base version updated to 1.7.20 -Kotlin has been updated to 1.7.10, which may cause issues when compiling or linking to Kotlin libraries. +Kotlin has been updated to 1.7.20, which may cause issues when compiling or linking to Kotlin libraries. == 3.3.0 From 01f8480cfaa74ab8bfc33a2294fea3c6875464a8 Mon Sep 17 00:00:00 2001 From: Tim Yates Date: Tue, 11 Oct 2022 06:49:08 +0100 Subject: [PATCH 099/743] fix: lazily create the singleton stream-pipeline attribute to fix native image (#8125) * Memoize the stream-pipeline attribute to remove Graal incomapability We cannot have static AttributeKey fields as Graal doesn't allow them to be generated at build time This PR switches the one in HttpPipelineBuilder to be a memoized supplier Fixes #8115 --- .../server/netty/HttpPipelineBuilder.java | 23 +++++++++++++++++-- .../http/server/netty/NettyHttpRequest.java | 2 +- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/HttpPipelineBuilder.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/HttpPipelineBuilder.java index f6eeb733f63..1d21a583f9a 100644 --- a/http-server-netty/src/main/java/io/micronaut/http/server/netty/HttpPipelineBuilder.java +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/HttpPipelineBuilder.java @@ -74,6 +74,7 @@ import java.time.Duration; import java.time.Instant; import java.util.concurrent.ThreadLocalRandom; +import java.util.concurrent.atomic.AtomicReference; /** * Helper class that manages the {@link ChannelPipeline} of incoming HTTP connections. @@ -85,7 +86,6 @@ * @author ywkat */ final class HttpPipelineBuilder { - static final AttributeKey STREAM_PIPELINE_ATTRIBUTE = AttributeKey.newInstance("stream-pipeline"); private static final Logger LOG = LoggerFactory.getLogger(HttpPipelineBuilder.class); @@ -464,7 +464,7 @@ private void insertHttp2DownstreamHandlers() { * and netty requests, and routing. */ private void insertMicronautHandlers() { - channel.attr(STREAM_PIPELINE_ATTRIBUTE).set(this); + channel.attr(StreamPipelineAttributeKeyHolder.getInstance()).set(this); pipeline.addLast(ChannelPipelineCustomizer.HANDLER_HTTP_COMPRESSOR, new SmartHttpContentCompressor(embeddedServices.getHttpCompressionStrategy())); pipeline.addLast(ChannelPipelineCustomizer.HANDLER_HTTP_DECOMPRESSOR, new HttpContentDecompressor()); @@ -518,4 +518,23 @@ private void registerMicronautChannelHandlers() { } } } + + // We need the AttributeKey to be static, as it's used in NettyHttpRequest, but we can't eagerly initialize it + // as it would fail in Graal + static final class StreamPipelineAttributeKeyHolder { + + private static final AtomicReference> INSTANCE = new AtomicReference<>(); + + private StreamPipelineAttributeKeyHolder() { + } + + static AttributeKey getInstance() { + return INSTANCE.updateAndGet(key -> { + if (key == null) { + return AttributeKey.newInstance("micronaut-stream-pipeline"); + } + return key; + }); + } + } } diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/NettyHttpRequest.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/NettyHttpRequest.java index 0d3a99388c3..2f87d752d6d 100644 --- a/http-server-netty/src/main/java/io/micronaut/http/server/netty/NettyHttpRequest.java +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/NettyHttpRequest.java @@ -527,7 +527,7 @@ public PushCapableHttpRequest serverPush(@NonNull HttpRequest request) { ); int ourStream = ((Http2StreamChannel) channelHandlerContext.channel()).stream().id(); - HttpPipelineBuilder.StreamPipeline originalStreamPipeline = channelHandlerContext.channel().attr(HttpPipelineBuilder.STREAM_PIPELINE_ATTRIBUTE).get(); + HttpPipelineBuilder.StreamPipeline originalStreamPipeline = channelHandlerContext.channel().attr(HttpPipelineBuilder.StreamPipelineAttributeKeyHolder.getInstance()).get(); new Http2StreamChannelBootstrap(channelHandlerContext.channel().parent()) .handler(new ChannelInitializer() { From e930fbda9143a8b574f8868ea2087794b70ca517 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 11 Oct 2022 09:12:14 +0200 Subject: [PATCH 100/743] fix(deps): update dependency io.micronaut.email:micronaut-email-bom to v1.4.0 (#8136) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 4a9e88f2825..1a70e1af934 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -80,7 +80,7 @@ managed-micronaut-crac = "1.0.1" managed-micronaut-data = "3.8.0" managed-micronaut-discovery = "3.2.0" managed-micronaut-elasticsearch = "4.3.0" -managed-micronaut-email = "1.3.2" +managed-micronaut-email = "1.4.0" managed-micronaut-flyway = "5.4.1" managed-micronaut-gcp = "4.6.0" managed-micronaut-graphql = "3.2.0" From 17d1b242ac5836cf1f94704b9d04d5131f33804a Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 11 Oct 2022 09:15:14 +0200 Subject: [PATCH 101/743] fix(deps): update asm to v9.4 (#8133) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 1a70e1af934..2b3f79e02a7 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,5 +1,5 @@ [versions] -asm = "9.3" +asm = "9.4" awaitility = "4.2.0" bcpkix = "1.70" blaze = "1.6.7" From 6027a813c8c630553e774245b795b3d55a0ca4a3 Mon Sep 17 00:00:00 2001 From: micronaut-build <65172877+micronaut-build@users.noreply.github.com> Date: Tue, 11 Oct 2022 06:00:16 -0400 Subject: [PATCH 102/743] build: Bump micronaut-data to 3.8.1 (#8141) --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 8da75562193..a9c9875415a 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -73,7 +73,7 @@ managed-micronaut-cache = "3.5.0" managed-micronaut-cassandra = "5.1.1" managed-micronaut-coherence = "3.5.1" managed-micronaut-crac = "1.0.1" -managed-micronaut-data = "3.8.0" +managed-micronaut-data = "3.8.1" managed-micronaut-discovery = "3.2.0" managed-micronaut-elasticsearch = "4.3.0" managed-micronaut-email = "1.3.2" From 4356c4aacc1877d35406d526433bcd1a331ef78b Mon Sep 17 00:00:00 2001 From: Tim Yates Date: Tue, 11 Oct 2022 11:09:05 +0100 Subject: [PATCH 103/743] fix: Stop relocating caffeine (#8143) I believe this is causing issues downstream in security ``` Caused by: java.lang.NoClassDefFoundError: io/micronaut/caffeine/cache/Expiry at app//io.micronaut.context.DefaultBeanContext.resolveByBeanFactory(DefaultBeanContext.java:2354) ... 86 more Caused by: java.lang.ClassNotFoundException: io.micronaut.caffeine.cache.Expiry ``` --- .../groovy/io.micronaut.build.internal.convention-library.gradle | 1 - 1 file changed, 1 deletion(-) diff --git a/buildSrc/src/main/groovy/io.micronaut.build.internal.convention-library.gradle b/buildSrc/src/main/groovy/io.micronaut.build.internal.convention-library.gradle index c570d2aecbc..3e5b58540a7 100644 --- a/buildSrc/src/main/groovy/io.micronaut.build.internal.convention-library.gradle +++ b/buildSrc/src/main/groovy/io.micronaut.build.internal.convention-library.gradle @@ -18,7 +18,6 @@ configurations { tasks.named("shadowJar") { configurations = [project.configurations.shadowCompile] - relocate "com.github.benmanes.caffeine", "io.micronaut.caffeine" relocate "org.objectweb.asm", "io.micronaut.asm" } From 2a6513b202c4da622c52174cd28f6c1743fee2ed Mon Sep 17 00:00:00 2001 From: micronaut-build <65172877+micronaut-build@users.noreply.github.com> Date: Wed, 12 Oct 2022 05:05:42 -0400 Subject: [PATCH 104/743] Bump micronaut-test to 3.7.0 (#8142) --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 51a345fc367..34426221054 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -117,7 +117,7 @@ managed-micronaut-serialization = "1.3.2" managed-micronaut-servlet = "3.3.1" managed-micronaut-spring = "4.3.1" managed-micronaut-sql = "4.7.2" -managed-micronaut-test = "3.6.2" +managed-micronaut-test = "3.7.0" managed-micronaut-test-resources = "1.1.2" managed-micronaut-toml = "1.1.1" managed-micronaut-tracing = "4.4.0" From 752a22117bf80695899b721257e9436292ee3a77 Mon Sep 17 00:00:00 2001 From: Denis Stepanov Date: Wed, 12 Oct 2022 17:03:28 +0200 Subject: [PATCH 105/743] Rewrite of Bean Definition processing to use it for both Java and Groovy (#8121) --- .../aop/chain/AbstractInterceptorChain.java | 1 + .../micronaut/aop/writer/AopHelperImpl.java | 288 +++ .../micronaut/aop/writer/AopProxyWriter.java | 62 +- ...t.build.internal.convention-library.gradle | 7 + .../core/annotation/AnnotationMetadata.java | 72 +- .../AnnotationMetadataDelegate.java | 42 +- .../AnnotationMetadataProvider.java | 5 + .../core/annotation/AnnotationSource.java | 9 + .../annotation/AnnotationValueResolver.java | 13 + .../annotation/EmptyAnnotationMetadata.java | 21 +- .../core/annotation/Introspected.java | 10 + .../core/annotation/NextMajorVersion.java | 25 +- .../core/reflect/ReflectionUtils.java | 1 + .../io/micronaut/core/util/ArrayUtils.java | 27 +- .../reflect/GraalTypeElementVisitor.java | 29 +- .../GraalTypeElementVisitorSpec.groovy | 47 +- .../test/AbstractBeanDefinitionSpec.groovy | 24 +- .../ast/groovy/InjectTransform.groovy | 102 +- .../micronaut/ast/groovy/InjectVisitor.groovy | 1771 -------------- .../ast/groovy/TypeElementVisitorEnd.groovy | 7 - .../groovy/TypeElementVisitorTransform.groovy | 184 +- .../GroovyAnnotationMetadataBuilder.java | 59 +- ...roovyElementAnnotationMetadataFactory.java | 40 + .../GroovyConfigurationMetadataBuilder.groovy | 178 -- .../groovy/utils/AstAnnotationUtils.groovy | 285 --- .../ast/groovy/utils/AstClassUtils.groovy | 28 +- .../InMemoryByteCodeGroovyClassLoader.java | 11 + .../utils/PublicAbstractMethodVisitor.groovy | 75 - .../groovy/utils/PublicMethodVisitor.groovy | 3 + .../groovy/visitor/AbstractGroovyElement.java | 224 +- .../visitor/GroovyAnnotationElement.java | 7 +- .../visitor/GroovyBeanDefinitionBuilder.java | 125 +- .../groovy/visitor/GroovyClassElement.java | 1103 ++++----- .../visitor/GroovyConstructorElement.java | 26 +- .../groovy/visitor/GroovyElementFactory.java | 246 +- .../visitor/GroovyEnumConstantElement.java | 39 +- .../ast/groovy/visitor/GroovyEnumElement.java | 41 +- .../groovy/visitor/GroovyFieldElement.java | 118 +- .../GroovyGenericPlaceholderElement.java | 17 +- .../groovy/visitor/GroovyMethodElement.java | 151 +- .../groovy/visitor/GroovyPackageElement.java | 22 +- .../visitor/GroovyParameterElement.java | 42 +- .../groovy/visitor/GroovyPropertyElement.java | 320 ++- .../groovy/visitor/GroovyVisitorContext.java | 104 +- .../groovy/visitor/GroovyWildcardElement.java | 30 +- .../ast/groovy/visitor/LoadedVisitor.groovy | 66 +- .../aop/adapter/MethodAdapterSpec.groovy | 22 +- .../AbstractClassIntroductionSpec.groovy | 27 +- .../aop/compile/FinalModifierSpec.groovy | 14 +- .../MyRepoIntroductionSpec.groovy | 10 +- .../io/micronaut/aop/introduction/Stub.groovy | 13 +- ...uctionWithAroundOnConcreteClassSpec.groovy | 4 +- .../visitor/GroovyBeanPropertiesSpec.groovy | 3 +- .../visitor/GroovyEnclosedElementsSpec.groovy | 4 +- .../visitor/GroovyEnumElementSpec.groovy | 2 +- .../AnnotationMetadataWriterSpec.groovy | 14 +- .../annotation/RemoveAnnotationSpec.groovy | 2 +- .../ChildConfigProperties.groovy | 13 + ...eritedConfigurationReaderPrefixSpec.groovy | 70 +- ...nterfaceConfigurationPropertiesSpec.groovy | 18 +- .../configproperties/TestEndpoint1.java | 49 + .../configproperties/TestEndpoint2.java | 49 + .../configproperties/TestEndpoint3.java | 49 + .../configproperties/TestEndpoint4.java | 49 + .../VisibilityIssuesSpec.groovy | 38 +- ...ovyConfigurationMetadataBuilderSpec.groovy | 94 - .../factory/FactoryBeanFieldSpec.groovy | 2 +- .../visitor/BeanIntrospectionSpec.groovy | 114 +- .../inject/visitor/ClassElementSpec.groovy | 92 +- .../inject/visitor/CustomVisitorSpec.groovy | 43 +- .../inject/visitor/ElementAnnotateSpec.groovy | 6 +- .../visitor/IntroductionVisitorSpec.groovy | 31 +- .../inject/visitor/PropertyElementSpec.groovy | 34 +- .../io/micronaut/inject/visitor/Test.groovy | 13 + .../inject/visitor/TestInjectVisitor.java | 1 - .../test/AbstractTypeElementSpec.groovy | 56 +- .../inject/visitor/AllClassesVisitor.java | 1 + .../inject/visitor/ElementAnnotateSpec.groovy | 16 +- .../visitor/InheritanceVisitorSpec.groovy | 13 +- .../beans/BeanIntrospectionSpec.groovy | 248 +- .../processing/AnnotationUtils.java | 156 +- .../BeanDefinitionInjectProcessor.java | 2174 +---------------- .../ConfigurationMetadataProcessor.java | 28 +- .../JavaAnnotationMetadataBuilder.java | 116 +- .../JavaConfigurationMetadataBuilder.java | 267 -- .../JavaElementAnnotationMetadataFactory.java | 41 + .../annotation/processing/ModelUtils.java | 203 +- .../PackageConfigurationInjectProcessor.java | 13 +- .../TypeElementVisitorProcessor.java | 278 ++- .../visitor/AbstractJavaElement.java | 331 +-- .../visitor/JavaAnnotationElement.java | 16 +- .../visitor/JavaBeanDefinitionBuilder.java | 123 +- .../processing/visitor/JavaClassElement.java | 1216 +++++---- .../visitor/JavaConstructorElement.java | 24 +- .../visitor/JavaElementFactory.java | 271 +- .../visitor/JavaEnumConstantElement.java | 39 +- .../processing/visitor/JavaEnumElement.java | 42 +- .../processing/visitor/JavaFieldElement.java | 69 +- .../JavaGenericPlaceholderElement.java | 33 +- .../processing/visitor/JavaMethodElement.java | 174 +- .../visitor/JavaPackageElement.java | 27 +- .../visitor/JavaParameterElement.java | 49 +- .../visitor/JavaPropertyElement.java | 299 ++- .../visitor/JavaVisitorContext.java | 101 +- .../visitor/JavaWildcardElement.java | 26 +- .../processing/visitor/LoadedVisitor.java | 76 +- .../annotation/JavaEnumElementSpec.groovy | 2 +- .../mapping/AnnotationMappingSpec.groovy | 10 +- .../visitor/JavaReconstructionSpec.groovy | 3 +- .../compile/OriginatingElementsSpec.groovy | 9 +- .../AddStereotypesFromVisitorSpec.groovy | 3 +- .../AnnotatedFieldWithSetterSpec.groovy | 51 +- .../AnnotationMetadataHierarchySpec.groovy | 60 +- .../AnnotationMetadataWriterSpec.groovy | 32 +- .../annotation/RemoveAnnotationSpec.groovy | 18 +- .../beanbuilder/ApplyAopToMethodVisitor.java | 1 - ...dElementBuilderProcessedMethodsSpec.groovy | 2 +- .../beanbuilder/TestBeanDefiningVisitor.java | 1 - .../ConfigPropertiesParseSpec.groovy | 75 +- .../ConfigurationMetadataSpec.groovy | 514 ++++ ...eritedConfigurationReaderPrefixSpec.groovy | 131 + .../inject/configproperties/TestEndpoint.java | 0 .../configproperties/TestEndpoint1.java | 49 + .../configproperties/TestEndpoint2.java | 49 + .../configproperties/TestEndpoint3.java | 49 + .../configproperties/TestEndpoint4.java | 49 + ...avaConfigurationMetadataBuilderSpec.groovy | 478 ---- .../inject/executable/BookController.java | 1 - .../replaces/AnnotateReplacesSpec.groovy | 4 +- .../RequiresBeanPropertiesSpec.groovy | 32 +- .../visitors/AllElementsVisitor.java | 8 + .../visitors/PropertyElementSpec.groovy | 23 +- .../context/DefaultApplicationContext.java | 2 +- .../context/annotation/BeanProperties.java | 134 + .../annotation/ConfigurationBuilder.java | 7 +- .../annotation/ConfigurationProperties.java | 4 +- .../annotation/ConfigurationReader.java | 24 +- .../context/annotation/EachProperty.java | 6 +- .../visitor/ConfigurationReaderVisitor.java | 144 ++ .../context/visitor}/ExecutableVisitor.java | 6 +- .../InternalApiTypeElementVisitor.java | 5 +- .../context/visitor/ValidationVisitor.java | 110 + .../io/micronaut/inject/BeanDefinition.java | 2 +- .../AbstractAnnotationMetadataBuilder.java | 1404 +++++------ ...tractElementAnnotationMetadataFactory.java | 378 +++ ...AbstractEnvironmentAnnotationMetadata.java | 16 +- .../AnnotationMetadataHierarchy.java | 209 +- .../AnnotationMetadataReference.java | 6 + .../annotation/AnnotationMetadataSupport.java | 3 + .../annotation/AnnotationMetadataWriter.java | 11 +- .../annotation/DefaultAnnotationMetadata.java | 161 +- .../EnvironmentAnnotationMetadata.java | 8 +- .../annotation/MutableAnnotationMetadata.java | 33 + .../inject/ast/BeanPropertiesQuery.java | 173 ++ .../io/micronaut/inject/ast/ClassElement.java | 245 +- .../inject/ast/DefaultElementQuery.java | 433 ++-- .../java/io/micronaut/inject/ast/Element.java | 165 +- .../inject/ast/ElementAnnotationMetadata.java | 14 +- .../ast/ElementAnnotationMetadataFactory.java | 99 + .../micronaut/inject/ast/ElementFactory.java | 127 +- .../ast/ElementMutableAnnotationMetadata.java | 168 ++ ...mentMutableAnnotationMetadataDelegate.java | 116 + .../io/micronaut/inject/ast/ElementQuery.java | 28 + .../io/micronaut/inject/ast/FieldElement.java | 6 + .../micronaut/inject/ast/MemberElement.java | 5 + .../micronaut/inject/ast/MethodElement.java | 320 ++- .../micronaut/inject/ast/PackageElement.java | 16 +- .../inject/ast/ParameterElement.java | 113 +- .../inject/ast/PrimitiveElement.java | 50 +- .../micronaut/inject/ast/PropertyElement.java | 92 +- .../inject/ast/ReflectParameterElement.java | 4 +- .../inject/ast/beans/BeanElement.java | 4 +- .../ast/utils/AstBeanPropertiesUtils.java | 387 +++ .../visitor/BeanIntrospectionWriter.java | 5 +- .../IntrospectedTypeElementVisitor.java | 77 +- .../ConfigurationMetadataBuilder.java | 180 +- .../ConfigurationMetadataWriter.java | 2 +- .../configuration/ConfigurationUtils.java | 140 ++ .../JsonConfigurationMetadataWriter.java | 2 +- ...gurationBuilderToBeanPropertiesMapper.java | 56 + ...ationPropertiesToBeanPropertiesMapper.java | 52 + .../AbstractBeanElementCreator.java | 159 ++ .../inject/processing/AopHelper.java | 52 + ...ctionProxySupportedBeanElementCreator.java | 91 + .../processing/BeanDefinitionCreator.java | 35 + .../BeanDefinitionCreatorFactory.java | 169 ++ ...ConfigurationReaderBeanElementCreator.java | 313 +++ .../DeclaredBeanElementCreator.java | 508 ++++ .../processing/FactoryBeanElementCreator.java | 301 +++ ...troductionInterfaceBeanElementCreator.java | 91 + .../processing/ProcessingException.java | 48 + .../inject/validation/RequiresValidation.java | 38 + .../inject/visitor/VisitorContext.java | 56 +- .../AbstractAnnotationMetadataWriter.java | 51 +- .../writer/AbstractBeanDefinitionBuilder.java | 95 +- .../writer/AbstractClassFileWriter.java | 11 +- .../writer/BeanDefinitionReferenceWriter.java | 6 +- .../inject/writer/BeanDefinitionVisitor.java | 51 +- .../inject/writer/BeanDefinitionWriter.java | 298 +-- .../inject/writer/ConfigBuilderState.java | 13 +- .../ExecutableMethodsDefinitionWriter.java | 16 +- ...cronaut.inject.annotation.AnnotationMapper | 4 +- ...icronaut.inject.visitor.TypeElementVisitor | 5 + .../endpoint/annotation/Endpoint.java | 8 +- ...GroovyAnnotationMetadataBuilderSpec.groovy | 11 +- .../support/AbstractBeanDefinitionSpec.groovy | 52 - .../DefaultConstraintValidators.java | 5 +- ...icronaut.inject.visitor.TypeElementVisitor | 3 - .../groovy/io/micronaut/validation/Foo.java | 4 +- .../validation/validator/ValidatorSpec.groovy | 19 +- 210 files changed, 12139 insertions(+), 11293 deletions(-) create mode 100644 aop/src/main/java/io/micronaut/aop/writer/AopHelperImpl.java rename validation/src/main/java/io/micronaut/validation/internal/package-info.java => core/src/main/java/io/micronaut/core/annotation/NextMajorVersion.java (55%) delete mode 100644 inject-groovy/src/main/groovy/io/micronaut/ast/groovy/InjectVisitor.groovy create mode 100644 inject-groovy/src/main/groovy/io/micronaut/ast/groovy/annotation/GroovyElementAnnotationMetadataFactory.java delete mode 100644 inject-groovy/src/main/groovy/io/micronaut/ast/groovy/config/GroovyConfigurationMetadataBuilder.groovy delete mode 100644 inject-groovy/src/main/groovy/io/micronaut/ast/groovy/utils/AstAnnotationUtils.groovy delete mode 100644 inject-groovy/src/main/groovy/io/micronaut/ast/groovy/utils/PublicAbstractMethodVisitor.groovy create mode 100644 inject-groovy/src/test/groovy/io/micronaut/inject/configproperties/ChildConfigProperties.groovy create mode 100644 inject-groovy/src/test/groovy/io/micronaut/inject/configproperties/TestEndpoint1.java create mode 100644 inject-groovy/src/test/groovy/io/micronaut/inject/configproperties/TestEndpoint2.java create mode 100644 inject-groovy/src/test/groovy/io/micronaut/inject/configproperties/TestEndpoint3.java create mode 100644 inject-groovy/src/test/groovy/io/micronaut/inject/configproperties/TestEndpoint4.java delete mode 100644 inject-groovy/src/test/groovy/io/micronaut/inject/configuration/GroovyConfigurationMetadataBuilderSpec.groovy create mode 100644 inject-groovy/src/test/groovy/io/micronaut/inject/visitor/Test.groovy delete mode 100644 inject-java/src/main/java/io/micronaut/annotation/processing/JavaConfigurationMetadataBuilder.java create mode 100644 inject-java/src/main/java/io/micronaut/annotation/processing/JavaElementAnnotationMetadataFactory.java create mode 100644 inject-java/src/test/groovy/io/micronaut/inject/configproperties/ConfigurationMetadataSpec.groovy create mode 100644 inject-java/src/test/groovy/io/micronaut/inject/configproperties/InheritedConfigurationReaderPrefixSpec.groovy rename {inject-groovy => inject-java}/src/test/groovy/io/micronaut/inject/configproperties/TestEndpoint.java (100%) create mode 100644 inject-java/src/test/groovy/io/micronaut/inject/configproperties/TestEndpoint1.java create mode 100644 inject-java/src/test/groovy/io/micronaut/inject/configproperties/TestEndpoint2.java create mode 100644 inject-java/src/test/groovy/io/micronaut/inject/configproperties/TestEndpoint3.java create mode 100644 inject-java/src/test/groovy/io/micronaut/inject/configproperties/TestEndpoint4.java delete mode 100644 inject-java/src/test/groovy/io/micronaut/inject/configuration/JavaConfigurationMetadataBuilderSpec.groovy create mode 100644 inject/src/main/java/io/micronaut/context/annotation/BeanProperties.java create mode 100644 inject/src/main/java/io/micronaut/context/visitor/ConfigurationReaderVisitor.java rename {validation/src/main/java/io/micronaut/validation/executable => inject/src/main/java/io/micronaut/context/visitor}/ExecutableVisitor.java (91%) rename {validation/src/main/java/io/micronaut/validation/internal => inject/src/main/java/io/micronaut/context/visitor}/InternalApiTypeElementVisitor.java (95%) create mode 100644 inject/src/main/java/io/micronaut/context/visitor/ValidationVisitor.java create mode 100644 inject/src/main/java/io/micronaut/inject/annotation/AbstractElementAnnotationMetadataFactory.java create mode 100644 inject/src/main/java/io/micronaut/inject/ast/BeanPropertiesQuery.java rename validation/src/main/java/io/micronaut/validation/executable/package-info.java => inject/src/main/java/io/micronaut/inject/ast/ElementAnnotationMetadata.java (64%) create mode 100644 inject/src/main/java/io/micronaut/inject/ast/ElementAnnotationMetadataFactory.java create mode 100644 inject/src/main/java/io/micronaut/inject/ast/ElementMutableAnnotationMetadata.java create mode 100644 inject/src/main/java/io/micronaut/inject/ast/ElementMutableAnnotationMetadataDelegate.java create mode 100644 inject/src/main/java/io/micronaut/inject/ast/utils/AstBeanPropertiesUtils.java create mode 100644 inject/src/main/java/io/micronaut/inject/configuration/ConfigurationUtils.java create mode 100644 inject/src/main/java/io/micronaut/inject/mappers/ConfigurationBuilderToBeanPropertiesMapper.java create mode 100644 inject/src/main/java/io/micronaut/inject/mappers/ConfigurationPropertiesToBeanPropertiesMapper.java create mode 100644 inject/src/main/java/io/micronaut/inject/processing/AbstractBeanElementCreator.java create mode 100644 inject/src/main/java/io/micronaut/inject/processing/AopHelper.java create mode 100644 inject/src/main/java/io/micronaut/inject/processing/AopIntroductionProxySupportedBeanElementCreator.java create mode 100644 inject/src/main/java/io/micronaut/inject/processing/BeanDefinitionCreator.java create mode 100644 inject/src/main/java/io/micronaut/inject/processing/BeanDefinitionCreatorFactory.java create mode 100644 inject/src/main/java/io/micronaut/inject/processing/ConfigurationReaderBeanElementCreator.java create mode 100644 inject/src/main/java/io/micronaut/inject/processing/DeclaredBeanElementCreator.java create mode 100644 inject/src/main/java/io/micronaut/inject/processing/FactoryBeanElementCreator.java create mode 100644 inject/src/main/java/io/micronaut/inject/processing/IntroductionInterfaceBeanElementCreator.java create mode 100644 inject/src/main/java/io/micronaut/inject/processing/ProcessingException.java create mode 100644 inject/src/main/java/io/micronaut/inject/validation/RequiresValidation.java delete mode 100644 validation/src/main/resources/META-INF/services/io.micronaut.inject.visitor.TypeElementVisitor diff --git a/aop/src/main/java/io/micronaut/aop/chain/AbstractInterceptorChain.java b/aop/src/main/java/io/micronaut/aop/chain/AbstractInterceptorChain.java index db856a76e36..c1380f08a0f 100644 --- a/aop/src/main/java/io/micronaut/aop/chain/AbstractInterceptorChain.java +++ b/aop/src/main/java/io/micronaut/aop/chain/AbstractInterceptorChain.java @@ -187,6 +187,7 @@ public R proceed(@NonNull Interceptor from) throws RuntimeException { */ protected static @NonNull Collection> resolveInterceptorValues(@NonNull AnnotationMetadata annotationMetadata, @NonNull InterceptorKind kind) { + annotationMetadata = annotationMetadata.getTargetAnnotationMetadata(); if (annotationMetadata instanceof AnnotationMetadataHierarchy) { final List> declaredValues = annotationMetadata.getDeclaredMetadata().getAnnotationValuesByType(InterceptorBinding.class); diff --git a/aop/src/main/java/io/micronaut/aop/writer/AopHelperImpl.java b/aop/src/main/java/io/micronaut/aop/writer/AopHelperImpl.java new file mode 100644 index 00000000000..e3aaed1cdaa --- /dev/null +++ b/aop/src/main/java/io/micronaut/aop/writer/AopHelperImpl.java @@ -0,0 +1,288 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.aop.writer; + +import io.micronaut.aop.Adapter; +import io.micronaut.aop.Interceptor; +import io.micronaut.aop.InterceptorKind; +import io.micronaut.aop.Introduction; +import io.micronaut.aop.internal.intercepted.InterceptedMethodUtil; +import io.micronaut.core.annotation.AnnotationClassValue; +import io.micronaut.core.annotation.AnnotationMetadata; +import io.micronaut.core.annotation.AnnotationUtil; +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.NextMajorVersion; +import io.micronaut.core.util.ArrayUtils; +import io.micronaut.core.util.StringUtils; +import io.micronaut.core.value.OptionalValues; +import io.micronaut.inject.processing.ProcessingException; +import io.micronaut.inject.annotation.AnnotationMetadataHierarchy; +import io.micronaut.inject.ast.ClassElement; +import io.micronaut.inject.ast.ElementQuery; +import io.micronaut.inject.ast.GenericPlaceholderElement; +import io.micronaut.inject.ast.MethodElement; +import io.micronaut.inject.ast.ParameterElement; +import io.micronaut.inject.ast.TypedElement; +import io.micronaut.inject.processing.AopHelper; +import io.micronaut.inject.processing.JavaModelUtils; +import io.micronaut.inject.visitor.VisitorContext; +import io.micronaut.inject.writer.BeanDefinitionVisitor; +import io.micronaut.inject.writer.BeanDefinitionWriter; + +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * AOP helper to connect Inject module with AOP. + * + * @author Denis Stepanov + * @since 4.0.0 + */ +@Internal +@NextMajorVersion("Correct project dependency so this hack is not needed") +public final class AopHelperImpl implements AopHelper { + + @Override + public BeanDefinitionVisitor visitAdaptedMethod(ClassElement classElement, + MethodElement sourceMethod, + AtomicInteger adaptedMethodIndex, + VisitorContext visitorContext) { + + AnnotationMetadata methodAnnotationMetadata = sourceMethod.getDeclaredMetadata(); + + Optional interfaceToAdaptValue = methodAnnotationMetadata.getValue(Adapter.class, String.class) + .flatMap(clazz -> visitorContext.getClassElement(clazz, visitorContext.getElementAnnotationMetadataFactory().readOnly())); + + if (!interfaceToAdaptValue.isPresent()) { + return null; + } + ClassElement interfaceToAdapt = interfaceToAdaptValue.get(); + if (!interfaceToAdapt.isInterface()) { + throw new ProcessingException(sourceMethod, "Class to adapt [" + interfaceToAdapt.getName() + "] is not an interface"); + } + + String rootName = classElement.getSimpleName() + '$' + interfaceToAdapt.getSimpleName() + '$' + sourceMethod.getSimpleName(); + String beanClassName = rootName + adaptedMethodIndex.incrementAndGet(); + + AopProxyWriter aopProxyWriter = new AopProxyWriter( + classElement.getPackageName(), + beanClassName, + true, + false, + sourceMethod, + new AnnotationMetadataHierarchy(classElement.getAnnotationMetadata(), methodAnnotationMetadata), + new ClassElement[]{interfaceToAdapt}, + visitorContext + ); + + aopProxyWriter.visitDefaultConstructor(methodAnnotationMetadata, visitorContext); + + List methods = interfaceToAdapt.getEnclosedElements(ElementQuery.ALL_METHODS.onlyAbstract()); + if (methods.isEmpty()) { + throw new ProcessingException(sourceMethod, "Interface to adapt [" + interfaceToAdapt.getName() + "] is not a SAM type. No methods found."); + } + if (methods.size() > 1) { + throw new ProcessingException(sourceMethod, "Interface to adapt [" + interfaceToAdapt.getName() + "] is not a SAM type. More than one abstract method declared."); + } + + MethodElement targetMethod = methods.iterator().next(); + + ParameterElement[] sourceParams = sourceMethod.getParameters(); + ParameterElement[] targetParams = targetMethod.getParameters(); + + int paramLen = targetParams.length; + if (paramLen != sourceParams.length) { + throw new ProcessingException(sourceMethod, "Cannot adapt method [" + sourceMethod + "] to target method [" + targetMethod + "]. Argument lengths don't match."); + } + if (sourceMethod.isSuspend()) { + throw new ProcessingException(sourceMethod, "Cannot adapt method [" + sourceMethod + "] to target method [" + targetMethod + "]. Kotlin suspend method not supported here."); + } + + Map typeVariables = interfaceToAdapt.getTypeArguments(); + Map genericTypes = new LinkedHashMap<>(paramLen); + for (int i = 0; i < paramLen; i++) { + ParameterElement targetParam = targetParams[i]; + ParameterElement sourceParam = sourceParams[i]; + + ClassElement targetType = targetParam.getType(); + ClassElement targetGenericType = targetParam.getGenericType(); + ClassElement sourceType = sourceParam.getGenericType(); + + // ??? Java returns generic placeholder for the generic type and Groovy from the ordinary type + if (targetGenericType instanceof GenericPlaceholderElement) { + GenericPlaceholderElement genericPlaceholderElement = (GenericPlaceholderElement) targetGenericType; + String variableName = genericPlaceholderElement.getVariableName(); + if (typeVariables.containsKey(variableName)) { + genericTypes.put(variableName, sourceType); + } + } else if (targetType instanceof GenericPlaceholderElement) { + GenericPlaceholderElement genericPlaceholderElement = (GenericPlaceholderElement) targetType; + String variableName = genericPlaceholderElement.getVariableName(); + if (typeVariables.containsKey(variableName)) { + genericTypes.put(variableName, sourceType); + } + } + + if (!sourceType.isAssignable(targetGenericType.getName())) { + throw new ProcessingException(sourceMethod, "Cannot adapt method [" + sourceMethod + "] to target method [" + targetMethod + "]. Type [" + sourceType.getName() + "] is not a subtype of type [" + targetGenericType.getName() + "] for argument at position " + i); + } + } + + if (!genericTypes.isEmpty()) { + aopProxyWriter.visitTypeArguments(Collections.singletonMap(interfaceToAdapt.getName(), genericTypes)); + } + + AnnotationClassValue[] adaptedArgumentTypes = Arrays.stream(sourceParams) + .map(p -> new AnnotationClassValue<>(JavaModelUtils.getClassname(p.getGenericType()))) + .toArray(AnnotationClassValue[]::new); + + targetMethod = targetMethod.withNewOwningType(classElement); + + targetMethod.annotate(Adapter.class, builder -> { + builder.member(Adapter.InternalAttributes.ADAPTED_BEAN, new AnnotationClassValue<>(JavaModelUtils.getClassname(classElement))); + builder.member(Adapter.InternalAttributes.ADAPTED_METHOD, sourceMethod.getName()); + builder.member(Adapter.InternalAttributes.ADAPTED_ARGUMENT_TYPES, adaptedArgumentTypes); + String qualifier = classElement.stringValue(AnnotationUtil.NAMED).orElse(null); + if (StringUtils.isNotEmpty(qualifier)) { + builder.member(Adapter.InternalAttributes.ADAPTED_QUALIFIER, qualifier); + } + }); + + aopProxyWriter.visitAroundMethod(interfaceToAdapt, targetMethod); + + return aopProxyWriter; + } + + @Override + public boolean visitIntrospectedMethod(BeanDefinitionVisitor visitor, ClassElement typeElement, MethodElement methodElement) { + AopProxyWriter aopProxyWriter = (AopProxyWriter) visitor; + + final AnnotationMetadata resolvedTypeMetadata = typeElement.getAnnotationMetadata(); + final boolean resolvedTypeMetadataIsAopProxyType = InterceptedMethodUtil.hasDeclaredAroundAdvice(resolvedTypeMetadata); + + if (methodElement.isAbstract() + || resolvedTypeMetadataIsAopProxyType + || InterceptedMethodUtil.hasDeclaredAroundAdvice(methodElement.getAnnotationMetadata())) { + addToIntroduction(aopProxyWriter, typeElement, methodElement, false); + return true; + } + return false; + } + + @Override + public AopProxyWriter createIntroductionAopProxyWriter(ClassElement typeElement, + VisitorContext visitorContext) { + AnnotationMetadata annotationMetadata = typeElement.getAnnotationMetadata(); + + String packageName = typeElement.getPackageName(); + String beanClassName = typeElement.getSimpleName(); + io.micronaut.core.annotation.AnnotationValue[] aroundInterceptors = + InterceptedMethodUtil.resolveInterceptorBinding(annotationMetadata, InterceptorKind.AROUND); + io.micronaut.core.annotation.AnnotationValue[] introductionInterceptors = InterceptedMethodUtil.resolveInterceptorBinding(annotationMetadata, InterceptorKind.INTRODUCTION); + + ClassElement[] interfaceTypes = Arrays.stream(annotationMetadata.getValue(Introduction.class, "interfaces", String[].class).orElse(new String[0])) + .map(v -> visitorContext.getClassElement(v, visitorContext.getElementAnnotationMetadataFactory().readOnly()) + .orElseThrow(() -> new ProcessingException(typeElement, "Cannot find interface: " + v)) + ).toArray(ClassElement[]::new); + + io.micronaut.core.annotation.AnnotationValue[] interceptorTypes = ArrayUtils.concat(aroundInterceptors, introductionInterceptors); + boolean isInterface = typeElement.isInterface(); + AopProxyWriter aopProxyWriter = new AopProxyWriter( + packageName, + beanClassName, + isInterface, + typeElement, + annotationMetadata, + interfaceTypes, + visitorContext, + interceptorTypes); + + Arrays.stream(interfaceTypes) + .flatMap(interfaceElement -> interfaceElement.getEnclosedElements(ElementQuery.ALL_METHODS).stream()) + .forEach(methodElement -> addToIntroduction(aopProxyWriter, typeElement, methodElement.withNewOwningType(typeElement), true)); + + return aopProxyWriter; + } + + @Override + public AopProxyWriter createAroundAopProxyWriter(BeanDefinitionVisitor existingWriter, + AnnotationMetadata aopElementAnnotationProcessor, + VisitorContext visitorContext, + boolean forceProxyTarget) { + OptionalValues aroundSettings = aopElementAnnotationProcessor.getValues(AnnotationUtil.ANN_AROUND, Boolean.class); + Map settings = new LinkedHashMap<>(); + for (CharSequence setting : aroundSettings) { + Optional entry = aroundSettings.get(setting); + entry.ifPresent(val -> settings.put(setting, val)); + } + if (forceProxyTarget) { + settings.put(Interceptor.PROXY_TARGET, true); + } + aroundSettings = OptionalValues.of(Boolean.class, settings); + + return new AopProxyWriter( + (BeanDefinitionWriter) existingWriter, + aroundSettings, + visitorContext, + InterceptedMethodUtil.resolveInterceptorBinding(aopElementAnnotationProcessor, InterceptorKind.AROUND) + ); + } + + private static void addToIntroduction(AopProxyWriter aopProxyWriter, + ClassElement classElement, + MethodElement methodElement, + boolean ignoreNotAbstract) { + AnnotationMetadata methodAnnotationMetadata = methodElement.getDeclaredMetadata(); + + if (InterceptedMethodUtil.hasAroundStereotype(methodAnnotationMetadata)) { + aopProxyWriter.visitInterceptorBinding( + InterceptedMethodUtil.resolveInterceptorBinding(methodAnnotationMetadata, InterceptorKind.AROUND) + ); + } + + if (!classElement.getName().equals(methodElement.getDeclaringType().getName())) { + aopProxyWriter.addOriginatingElement(methodElement.getDeclaringType()); + } + + ClassElement declaringType = methodElement.getDeclaringType(); + if (methodElement.isAbstract()) { + aopProxyWriter.visitIntroductionMethod(declaringType, methodElement); + } else if (!ignoreNotAbstract) { + boolean isInterface = methodElement.getDeclaringType().isInterface(); + boolean isDefault = methodElement.isDefault(); + if (isInterface && isDefault) { + // Default methods cannot be "super" accessed on the defined type + declaringType = classElement; + } + // only apply around advise to non-abstract methods of introduction advise + aopProxyWriter.visitAroundMethod(declaringType, methodElement); + } + } + + @Override + public void visitAroundMethod(BeanDefinitionVisitor existingWriter, TypedElement beanType, MethodElement methodElement) { + AopProxyWriter aopProxyWriter = (AopProxyWriter) existingWriter; + aopProxyWriter.visitInterceptorBinding( + InterceptedMethodUtil.resolveInterceptorBinding(methodElement.getAnnotationMetadata(), InterceptorKind.AROUND) + ); + aopProxyWriter.visitAroundMethod(beanType, methodElement); + } +} diff --git a/aop/src/main/java/io/micronaut/aop/writer/AopProxyWriter.java b/aop/src/main/java/io/micronaut/aop/writer/AopProxyWriter.java index a3e7ad1c8eb..d4d51090253 100644 --- a/aop/src/main/java/io/micronaut/aop/writer/AopProxyWriter.java +++ b/aop/src/main/java/io/micronaut/aop/writer/AopProxyWriter.java @@ -31,7 +31,6 @@ import io.micronaut.context.DefaultBeanContext; import io.micronaut.context.ExecutionHandleLocator; import io.micronaut.context.Qualifier; -import io.micronaut.context.annotation.ConfigurationReader; import io.micronaut.core.annotation.AnnotationMetadata; import io.micronaut.core.annotation.AnnotationUtil; import io.micronaut.core.annotation.AnnotationValue; @@ -39,14 +38,12 @@ import io.micronaut.core.reflect.ReflectionUtils; import io.micronaut.core.type.Argument; import io.micronaut.core.util.ArrayUtils; -import io.micronaut.core.util.StringUtils; import io.micronaut.core.util.Toggleable; import io.micronaut.core.value.OptionalValues; import io.micronaut.inject.BeanDefinition; import io.micronaut.inject.ExecutableMethod; import io.micronaut.inject.ProxyBeanDefinition; import io.micronaut.inject.annotation.AnnotationMetadataReference; -import io.micronaut.inject.annotation.DefaultAnnotationMetadata; import io.micronaut.inject.ast.ClassElement; import io.micronaut.inject.ast.Element; import io.micronaut.inject.ast.ElementQuery; @@ -54,7 +51,6 @@ import io.micronaut.inject.ast.MethodElement; import io.micronaut.inject.ast.ParameterElement; import io.micronaut.inject.ast.TypedElement; -import io.micronaut.inject.configuration.ConfigurationMetadata; import io.micronaut.inject.configuration.ConfigurationMetadataBuilder; import io.micronaut.inject.processing.JavaModelUtils; import io.micronaut.inject.visitor.VisitorContext; @@ -120,12 +116,6 @@ public class AopProxyWriter extends AbstractClassFileWriter implements ProxyingB Argument.class, Qualifier.class )); - public static final Method METHOD_GET_PROXY_TARGET_BEAN = Method.getMethod(ReflectionUtils.getRequiredInternalMethod( - BeanLocator.class, - "getProxyTargetBean", - Argument.class, - Qualifier.class - )); public static final Method METHOD_HAS_CACHED_INTERCEPTED_METHOD = Method.getMethod(ReflectionUtils.getRequiredInternalMethod( InterceptedProxy.class, @@ -212,13 +202,11 @@ public class AopProxyWriter extends AbstractClassFileWriter implements ProxyingB *

Additional {@link Interceptor} types can be added downstream with {@link #visitInterceptorBinding(AnnotationValue[])} .

* @param parent The parent {@link BeanDefinitionWriter} * @param settings optional setting - * @param metadataBuilder The configuration metadata builder * @param visitorContext The visitor context * @param interceptorBinding The interceptor binding of the {@link Interceptor} instances to be injected */ public AopProxyWriter(BeanDefinitionWriter parent, OptionalValues settings, - ConfigurationMetadataBuilder metadataBuilder, VisitorContext visitorContext, AnnotationValue... interceptorBinding) { super(parent.getOriginatingElements()); @@ -244,7 +232,7 @@ public AopProxyWriter(BeanDefinitionWriter parent, this.proxyBeanDefinitionWriter = new BeanDefinitionWriter( aopElement, parent, - metadataBuilder, visitorContext + visitorContext ); startClass(classWriter, getInternalName(proxyFullName), getTypeReferenceForName(targetClassFullName)); proxyBeanDefinitionWriter.setInterceptedType(targetClassFullName); @@ -259,8 +247,6 @@ public AopProxyWriter(BeanDefinitionWriter parent, * @param annotationMetadata The annotation metadata * @param interfaceTypes The additional interfaces to implement * @param visitorContext The visitor context - * @param metadataBuilder The configuration metadata builder - * @param configurationMetadata The configuration metadata for the class * @param interceptorBinding The interceptor types */ public AopProxyWriter(String packageName, @@ -270,10 +256,8 @@ public AopProxyWriter(String packageName, AnnotationMetadata annotationMetadata, ClassElement[] interfaceTypes, VisitorContext visitorContext, - ConfigurationMetadataBuilder metadataBuilder, - ConfigurationMetadata configurationMetadata, AnnotationValue... interceptorBinding) { - this(packageName, className, isInterface, true, originatingElement, annotationMetadata, interfaceTypes, visitorContext, metadataBuilder, configurationMetadata, interceptorBinding); + this(packageName, className, isInterface, true, originatingElement, annotationMetadata, interfaceTypes, visitorContext, interceptorBinding); } /** @@ -286,8 +270,6 @@ public AopProxyWriter(String packageName, * @param annotationMetadata The annotation metadata * @param interfaceTypes The additional interfaces to implement * @param visitorContext The visitor context - * @param metadataBuilder The configuration metadata builder - * @param configurationMetadata The configuration metadata for the class * @param interceptorBinding The interceptor binding */ public AopProxyWriter(String packageName, @@ -298,8 +280,6 @@ public AopProxyWriter(String packageName, AnnotationMetadata annotationMetadata, ClassElement[] interfaceTypes, VisitorContext visitorContext, - ConfigurationMetadataBuilder metadataBuilder, - ConfigurationMetadata configurationMetadata, AnnotationValue... interceptorBinding) { super(OriginatingElements.of(originatingElement)); this.isIntroduction = true; @@ -323,21 +303,6 @@ public AopProxyWriter(String packageName, this.interceptorBinding = toInterceptorBindingMap(interceptorBinding); this.interfaceTypes = interfaceTypes != null ? new LinkedHashSet<>(Arrays.asList(interfaceTypes)) : Collections.emptySet(); this.classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS | ClassWriter.COMPUTE_FRAMES); - if (configurationMetadata != null) { - // unfortunate we have to do this - String existingPrefix = annotationMetadata.stringValue( - ConfigurationReader.class, - "prefix") - .orElse(""); - - String computedPrefix = StringUtils.isNotEmpty(existingPrefix) ? existingPrefix + "." + configurationMetadata.getName() : configurationMetadata.getName(); - annotationMetadata = DefaultAnnotationMetadata.mutateMember( - annotationMetadata, - ConfigurationReader.class.getName(), - "prefix", - computedPrefix - ); - } ClassElement aopElement = ClassElement.of( proxyFullName, isInterface, @@ -346,7 +311,7 @@ public AopProxyWriter(String packageName, this.proxyBeanDefinitionWriter = new BeanDefinitionWriter( aopElement, this, - metadataBuilder, visitorContext + visitorContext ); if (isInterface) { if (implementInterface) { @@ -1199,12 +1164,14 @@ public void visitSuperBeanDefinitionFactory(String beanName) { public void visitSetterValue( TypedElement declaringType, MethodElement methodElement, + AnnotationMetadata annotationMetadata, boolean requiresReflection, boolean isOptional) { deferredInjectionPoints.add(() -> proxyBeanDefinitionWriter.visitSetterValue( declaringType, methodElement, + annotationMetadata, requiresReflection, isOptional ) @@ -1298,16 +1265,13 @@ public void visitAnnotationMemberPropertyInjectionPoint(TypedElement annotationM @Override public void visitFieldValue( - TypedElement declaringType, - FieldElement fieldType, - boolean requiresReflection, - boolean isOptional) { + TypedElement declaringType, + FieldElement fieldType, + boolean requiresReflection, boolean isOptional) { deferredInjectionPoints.add(() -> proxyBeanDefinitionWriter.visitFieldValue( declaringType, - fieldType, - requiresReflection, - isOptional + fieldType, requiresReflection, isOptional ) ); } @@ -1338,13 +1302,13 @@ public void visitConfigBuilderMethod(ClassElement type, String methodName, Annot } @Override - public void visitConfigBuilderMethod(String prefix, ClassElement returnType, String methodName, ClassElement paramType, Map generics, String propertyPath) { - proxyBeanDefinitionWriter.visitConfigBuilderMethod(prefix, returnType, methodName, paramType, generics, propertyPath); + public void visitConfigBuilderMethod(String propertyName, ClassElement returnType, String methodName, ClassElement paramType, Map generics, String propertyPath) { + proxyBeanDefinitionWriter.visitConfigBuilderMethod(propertyName, returnType, methodName, paramType, generics, propertyPath); } @Override - public void visitConfigBuilderDurationMethod(String prefix, ClassElement returnType, String methodName, String propertyPath) { - proxyBeanDefinitionWriter.visitConfigBuilderDurationMethod(prefix, returnType, methodName, propertyPath); + public void visitConfigBuilderDurationMethod(String propertyName, ClassElement returnType, String methodName, String propertyPath) { + proxyBeanDefinitionWriter.visitConfigBuilderDurationMethod(propertyName, returnType, methodName, propertyPath); } @Override diff --git a/buildSrc/src/main/groovy/io.micronaut.build.internal.convention-library.gradle b/buildSrc/src/main/groovy/io.micronaut.build.internal.convention-library.gradle index 3e5b58540a7..bab8bdd7022 100644 --- a/buildSrc/src/main/groovy/io.micronaut.build.internal.convention-library.gradle +++ b/buildSrc/src/main/groovy/io.micronaut.build.internal.convention-library.gradle @@ -21,3 +21,10 @@ tasks.named("shadowJar") { relocate "org.objectweb.asm", "io.micronaut.asm" } +micronautBuild { + binaryCompatibility { + // Enable before Micronaut 4 release + enabled.set(false) + } +} + diff --git a/core/src/main/java/io/micronaut/core/annotation/AnnotationMetadata.java b/core/src/main/java/io/micronaut/core/annotation/AnnotationMetadata.java index 3a446c4798b..5d6eb3cd272 100644 --- a/core/src/main/java/io/micronaut/core/annotation/AnnotationMetadata.java +++ b/core/src/main/java/io/micronaut/core/annotation/AnnotationMetadata.java @@ -28,7 +28,9 @@ import java.lang.annotation.Repeatable; import java.lang.reflect.Array; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; +import java.util.EnumSet; import java.util.List; import java.util.Map; import java.util.Optional; @@ -807,7 +809,7 @@ default OptionalLong longValue(@NonNull Class annotation, * @param The enum type * @return An {@link Optional} enum value */ - default Optional enumValue(@NonNull String annotation, Class enumType) { + default > Optional enumValue(@NonNull String annotation, Class enumType) { ArgumentUtils.requireNonNull("annotation", annotation); return enumValue(annotation, VALUE_MEMBER, enumType); } @@ -821,7 +823,7 @@ default Optional enumValue(@NonNull String annotation, Class * @param The enum type * @return An {@link Optional} class */ - default Optional enumValue(@NonNull String annotation, @NonNull String member, Class enumType) { + default > Optional enumValue(@NonNull String annotation, @NonNull String member, Class enumType) { ArgumentUtils.requireNonNull("annotation", annotation); ArgumentUtils.requireNonNull("member", member); @@ -836,7 +838,7 @@ default Optional enumValue(@NonNull String annotation, @NonN * @param The enum type * @return An {@link Optional} class */ - default Optional enumValue(@NonNull Class annotation, Class enumType) { + default > Optional enumValue(@NonNull Class annotation, Class enumType) { ArgumentUtils.requireNonNull("annotation", annotation); return enumValue(annotation, VALUE_MEMBER, enumType); @@ -851,7 +853,7 @@ default Optional enumValue(@NonNull Class The enum type * @return An {@link Optional} class */ - default Optional enumValue(@NonNull Class annotation, @NonNull String member, Class enumType) { + default > Optional enumValue(@NonNull Class annotation, @NonNull String member, Class enumType) { ArgumentUtils.requireNonNull("annotation", annotation); ArgumentUtils.requireNonNull("member", member); @@ -866,7 +868,7 @@ default Optional enumValue(@NonNull Class The enum type * @return An array of enum values */ - default E[] enumValues(@NonNull String annotation, Class enumType) { + default > E[] enumValues(@NonNull String annotation, Class enumType) { ArgumentUtils.requireNonNull("annotation", annotation); return enumValues(annotation, VALUE_MEMBER, enumType); } @@ -880,13 +882,28 @@ default E[] enumValues(@NonNull String annotation, Class enu * @param The enum type * @return An array of enum values */ - default E[] enumValues(@NonNull String annotation, @NonNull String member, Class enumType) { + default > E[] enumValues(@NonNull String annotation, @NonNull String member, Class enumType) { ArgumentUtils.requireNonNull("annotation", annotation); ArgumentUtils.requireNonNull("member", member); return (E[]) Array.newInstance(enumType, 0); } + /** + * The enum values for the given annotation. + * + * @param annotation The annotation + * @param member The annotation member + * @param enumType The enum type + * @param The enum type + * @return An enum set of enum values + * @since 4.0.0 + */ + default > EnumSet enumValuesSet(@NonNull String annotation, @NonNull String member, Class enumType) { + E[] values = enumValues(annotation, member, enumType); + return values.length == 0 ? EnumSet.noneOf(enumType) : EnumSet.copyOf(Arrays.asList(values)); + } + /** * The enum values for the given annotation. * @@ -895,7 +912,7 @@ default E[] enumValues(@NonNull String annotation, @NonNull Str * @param The enum type * @return An array of enum values */ - default E[] enumValues(@NonNull Class annotation, Class enumType) { + default > E[] enumValues(@NonNull Class annotation, Class enumType) { ArgumentUtils.requireNonNull("annotation", annotation); return enumValues(annotation, VALUE_MEMBER, enumType); @@ -910,13 +927,27 @@ default E[] enumValues(@NonNull Class ann * @param The enum type * @return An array of enum values */ - default E[] enumValues(@NonNull Class annotation, @NonNull String member, Class enumType) { + default > E[] enumValues(@NonNull Class annotation, @NonNull String member, Class enumType) { ArgumentUtils.requireNonNull("annotation", annotation); ArgumentUtils.requireNonNull("member", member); return enumValues(annotation.getName(), member, enumType); } + /** + * The enum values for the given annotation. + * + * @param annotation The annotation + * @param member The annotation member + * @param enumType The enum type + * @param The enum type + * @return An enum set of enum values + * @since 4.0.0 + */ + default > EnumSet enumValuesSet(@NonNull Class annotation, @NonNull String member, Class enumType) { + return enumValuesSet(annotation.getName(), member, enumType); + } + /** * The value of the annotation as a Class. * @@ -1595,4 +1626,29 @@ default boolean isEmpty() { return this == AnnotationMetadata.EMPTY_METADATA; } + /** + * Makes a copy of the annotation or returns this. + * + * @return the copy + * @since 4.0.0 + */ + @NonNull + default AnnotationMetadata copyAnnotationMetadata() { + return this; + } + + /** + * Unwraps possible a possible delegate or a provider, returns this otherwise. + * + * @return unwrapped + * @see AnnotationMetadataDelegate + * @see AnnotationMetadataProvider + * @since 4.0.0 + */ + @Override + @NonNull + default AnnotationMetadata getTargetAnnotationMetadata() { + return this; + } + } diff --git a/core/src/main/java/io/micronaut/core/annotation/AnnotationMetadataDelegate.java b/core/src/main/java/io/micronaut/core/annotation/AnnotationMetadataDelegate.java index 8bc561ae1e0..1b15c9f4731 100644 --- a/core/src/main/java/io/micronaut/core/annotation/AnnotationMetadataDelegate.java +++ b/core/src/main/java/io/micronaut/core/annotation/AnnotationMetadataDelegate.java @@ -29,6 +29,7 @@ * @since 1.0 */ public interface AnnotationMetadataDelegate extends AnnotationMetadataProvider, AnnotationMetadata { + @Override default Set getStereotypeAnnotationNames() { return getAnnotationMetadata().getStereotypeAnnotationNames(); @@ -39,6 +40,21 @@ default Set getDeclaredStereotypeAnnotationNames() { return getAnnotationMetadata().getDeclaredStereotypeAnnotationNames(); } + @Override + default List> getDeclaredAnnotationValuesByName(String annotationType) { + return getAnnotationMetadata().getDeclaredAnnotationValuesByName(annotationType); + } + + @Override + default List> getAnnotationValuesByName(String annotationType) { + return getAnnotationMetadata().getAnnotationValuesByName(annotationType); + } + + @Override + default List> getAnnotationValuesByStereotype(String stereotype) { + return getAnnotationMetadata().getAnnotationValuesByStereotype(stereotype); + } + @NonNull @Override default AnnotationMetadata getDeclaredMetadata() { @@ -61,22 +77,22 @@ default boolean hasSimpleDeclaredAnnotation(@Nullable String annotation) { } @Override - default E[] enumValues(@NonNull String annotation, Class enumType) { + default > E[] enumValues(@NonNull String annotation, Class enumType) { return getAnnotationMetadata().enumValues(annotation, enumType); } @Override - default E[] enumValues(@NonNull String annotation, @NonNull String member, Class enumType) { + default > E[] enumValues(@NonNull String annotation, @NonNull String member, Class enumType) { return getAnnotationMetadata().enumValues(annotation, member, enumType); } @Override - default E[] enumValues(@NonNull Class annotation, Class enumType) { + default > E[] enumValues(@NonNull Class annotation, Class enumType) { return getAnnotationMetadata().enumValues(annotation, enumType); } @Override - default E[] enumValues(@NonNull Class annotation, @NonNull String member, Class enumType) { + default > E[] enumValues(@NonNull Class annotation, @NonNull String member, Class enumType) { return getAnnotationMetadata().enumValues(annotation, member, enumType); } @@ -101,22 +117,22 @@ default Class[] classValues(@NonNull Class annotati } @Override - default Optional enumValue(@NonNull String annotation, Class enumType) { + default > Optional enumValue(@NonNull String annotation, Class enumType) { return getAnnotationMetadata().enumValue(annotation, enumType); } @Override - default Optional enumValue(@NonNull String annotation, @NonNull String member, Class enumType) { + default > Optional enumValue(@NonNull String annotation, @NonNull String member, Class enumType) { return getAnnotationMetadata().enumValue(annotation, member, enumType); } @Override - default Optional enumValue(@NonNull Class annotation, Class enumType) { + default > Optional enumValue(@NonNull Class annotation, Class enumType) { return getAnnotationMetadata().enumValue(annotation, enumType); } @Override - default Optional enumValue(@NonNull Class annotation, @NonNull String member, Class enumType) { + default > Optional enumValue(@NonNull Class annotation, @NonNull String member, Class enumType) { return getAnnotationMetadata().enumValue(annotation, member, enumType); } @@ -649,4 +665,14 @@ default Optional findRepeatableAnnotation(Class an default Optional findRepeatableAnnotation(String annotation) { return getAnnotationMetadata().findRepeatableAnnotation(annotation); } + + @Override + default AnnotationMetadata copyAnnotationMetadata() { + return getAnnotationMetadata().copyAnnotationMetadata(); + } + + @Override + default AnnotationMetadata getTargetAnnotationMetadata() { + return getAnnotationMetadata().getTargetAnnotationMetadata(); + } } diff --git a/core/src/main/java/io/micronaut/core/annotation/AnnotationMetadataProvider.java b/core/src/main/java/io/micronaut/core/annotation/AnnotationMetadataProvider.java index 5e31ad49601..a2c5a80b7dc 100644 --- a/core/src/main/java/io/micronaut/core/annotation/AnnotationMetadataProvider.java +++ b/core/src/main/java/io/micronaut/core/annotation/AnnotationMetadataProvider.java @@ -95,4 +95,9 @@ default Optional> findDeclaredAnnotati default Optional> findDeclaredAnnotation(Class annotationClass) { return getAnnotationMetadata().findDeclaredAnnotation(annotationClass); } + + @Override + default AnnotationSource getTargetAnnotationMetadata() { + return getAnnotationMetadata().getTargetAnnotationMetadata(); + } } diff --git a/core/src/main/java/io/micronaut/core/annotation/AnnotationSource.java b/core/src/main/java/io/micronaut/core/annotation/AnnotationSource.java index b521df5da5b..b94cba58445 100644 --- a/core/src/main/java/io/micronaut/core/annotation/AnnotationSource.java +++ b/core/src/main/java/io/micronaut/core/annotation/AnnotationSource.java @@ -299,4 +299,13 @@ default boolean isDeclaredAnnotationPresent(@NonNull String annotationName) { ArgumentUtils.requireNonNull("annotationClass", annotationName); return false; } + + /** + * Unwraps possible delegate or provider. + * @return unwrapped + * @since 4.0.0 + */ + default AnnotationSource getTargetAnnotationMetadata() { + return this; + } } diff --git a/core/src/main/java/io/micronaut/core/annotation/AnnotationValueResolver.java b/core/src/main/java/io/micronaut/core/annotation/AnnotationValueResolver.java index 14aceccfc90..a458659de88 100644 --- a/core/src/main/java/io/micronaut/core/annotation/AnnotationValueResolver.java +++ b/core/src/main/java/io/micronaut/core/annotation/AnnotationValueResolver.java @@ -56,6 +56,19 @@ default Optional enumValue(@NonNull Class enumType) { */ E[] enumValues(@NonNull String member, @NonNull Class enumType); + /** + * Return the enum value of the given member of the given enum type. + * + * @param member The annotation member + * @param enumType The required type + * @return An {@link Optional} of the enum value + * @param The enum type + */ + default > EnumSet enumValuesSet(@NonNull String member, @NonNull Class enumType) { + E[] values = enumValues(member, enumType); + return values.length == 0 ? EnumSet.noneOf(enumType) : EnumSet.copyOf(Arrays.asList(values)); + } + /** * Return the enum value of the given member of the given enum type. * diff --git a/core/src/main/java/io/micronaut/core/annotation/EmptyAnnotationMetadata.java b/core/src/main/java/io/micronaut/core/annotation/EmptyAnnotationMetadata.java index fa93891624a..72ac519c520 100644 --- a/core/src/main/java/io/micronaut/core/annotation/EmptyAnnotationMetadata.java +++ b/core/src/main/java/io/micronaut/core/annotation/EmptyAnnotationMetadata.java @@ -38,22 +38,22 @@ public boolean hasPropertyExpressions() { } @Override - public E[] enumValues(@NonNull String annotation, Class enumType) { + public > E[] enumValues(@NonNull String annotation, Class enumType) { return (E[]) Array.newInstance(enumType, 0); } @Override - public E[] enumValues(@NonNull String annotation, @NonNull String member, Class enumType) { + public > E[] enumValues(@NonNull String annotation, @NonNull String member, Class enumType) { return (E[]) Array.newInstance(enumType, 0); } @Override - public E[] enumValues(@NonNull Class annotation, Class enumType) { + public > E[] enumValues(@NonNull Class annotation, Class enumType) { return (E[]) Array.newInstance(enumType, 0); } @Override - public E[] enumValues(@NonNull Class annotation, @NonNull String member, Class enumType) { + public > E[] enumValues(@NonNull Class annotation, @NonNull String member, Class enumType) { return (E[]) Array.newInstance(enumType, 0); } @@ -277,22 +277,22 @@ public OptionalLong longValue(@NonNull Class annotation, @ } @Override - public Optional enumValue(@NonNull String annotation, Class enumType) { + public > Optional enumValue(@NonNull String annotation, Class enumType) { return Optional.empty(); } @Override - public Optional enumValue(@NonNull String annotation, @NonNull String member, Class enumType) { + public > Optional enumValue(@NonNull String annotation, @NonNull String member, Class enumType) { return Optional.empty(); } @Override - public Optional enumValue(@NonNull Class annotation, Class enumType) { + public > Optional enumValue(@NonNull Class annotation, Class enumType) { return Optional.empty(); } @Override - public Optional enumValue(@NonNull Class annotation, @NonNull String member, Class enumType) { + public > Optional enumValue(@NonNull Class annotation, @NonNull String member, Class enumType) { return Optional.empty(); } @@ -558,4 +558,9 @@ public boolean hasDeclaredStereotype(@Nullable Class... an public boolean isEmpty() { return true; } + + @Override + public AnnotationMetadata copyAnnotationMetadata() { + return this; + } } diff --git a/core/src/main/java/io/micronaut/core/annotation/Introspected.java b/core/src/main/java/io/micronaut/core/annotation/Introspected.java index c3b59a74da7..cb8034244eb 100644 --- a/core/src/main/java/io/micronaut/core/annotation/Introspected.java +++ b/core/src/main/java/io/micronaut/core/annotation/Introspected.java @@ -51,6 +51,16 @@ @Inherited public @interface Introspected { + /** + * The default values for the access kind attribute. + */ + Introspected.AccessKind[] DEFAULT_ACCESS_KIND = {Introspected.AccessKind.METHOD}; + + /** + * The default values for the visibility attribute. + */ + Introspected.Visibility[] DEFAULT_VISIBILITY = {Introspected.Visibility.DEFAULT}; + /** * By default {@link Introspected} applies to the class it is applied on. However if classes are specified * introspections will instead be generated for each class specified. This is useful in cases where you cannot diff --git a/validation/src/main/java/io/micronaut/validation/internal/package-info.java b/core/src/main/java/io/micronaut/core/annotation/NextMajorVersion.java similarity index 55% rename from validation/src/main/java/io/micronaut/validation/internal/package-info.java rename to core/src/main/java/io/micronaut/core/annotation/NextMajorVersion.java index d3ab93cf00b..7a3f5ef0dae 100644 --- a/validation/src/main/java/io/micronaut/validation/internal/package-info.java +++ b/core/src/main/java/io/micronaut/core/annotation/NextMajorVersion.java @@ -13,8 +13,27 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +package io.micronaut.core.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + /** - * Package to organize classes responsible for validating - * extensions of internal APIs. + * Documents the code change that needs to be done in the next major version. + * + * @author Denis Stepanov + * @since 4.0.0 */ -package io.micronaut.validation.internal; +@Retention(RetentionPolicy.SOURCE) +@Documented +@Inherited +public @interface NextMajorVersion { + + /** + * @return the message + */ + String value(); + +} diff --git a/core/src/main/java/io/micronaut/core/reflect/ReflectionUtils.java b/core/src/main/java/io/micronaut/core/reflect/ReflectionUtils.java index e3a3c06f3ba..c5c4e9ea193 100644 --- a/core/src/main/java/io/micronaut/core/reflect/ReflectionUtils.java +++ b/core/src/main/java/io/micronaut/core/reflect/ReflectionUtils.java @@ -434,6 +434,7 @@ public static void setField( * @param clazz The class * @param fieldName The fieldName * @param instance The instance + * @return the field value * @since 3.7.0 */ @UsedByGeneratedCode diff --git a/core/src/main/java/io/micronaut/core/util/ArrayUtils.java b/core/src/main/java/io/micronaut/core/util/ArrayUtils.java index 3d9fe0e7c7b..4b2a0cf2ac0 100644 --- a/core/src/main/java/io/micronaut/core/util/ArrayUtils.java +++ b/core/src/main/java/io/micronaut/core/util/ArrayUtils.java @@ -20,7 +20,13 @@ import io.micronaut.core.reflect.ReflectionUtils; import java.lang.reflect.Array; -import java.util.*; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.NoSuchElementException; +import java.util.Objects; import java.util.function.IntFunction; /** @@ -279,6 +285,25 @@ public static Object toPrimitiveArray(final Object[] wrapperArray) { } } + /** + * Mutates the passed array by reversing the order of the items in it. + * + * @param input The array + * @param The array type + * @since 4.0.0 + */ + public static void reverse(T[] input) { + final int len = input.length; + if (len > 1) { + for (int i = 0; i < len / 2; i++) { + T temp = input[i]; + final int pos = len - i - 1; + input[i] = input[pos]; + input[pos] = temp; + } + } + } + /** * Iterator implementation used to efficiently expose contents of an * Array as read-only iterator. diff --git a/graal/src/main/java/io/micronaut/graal/reflect/GraalTypeElementVisitor.java b/graal/src/main/java/io/micronaut/graal/reflect/GraalTypeElementVisitor.java index 385599c72da..01cc7b8bf9b 100644 --- a/graal/src/main/java/io/micronaut/graal/reflect/GraalTypeElementVisitor.java +++ b/graal/src/main/java/io/micronaut/graal/reflect/GraalTypeElementVisitor.java @@ -15,18 +15,6 @@ */ package io.micronaut.graal.reflect; -import java.io.IOException; -import java.lang.annotation.RetentionPolicy; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashSet; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.function.Predicate; -import java.util.stream.Collectors; - import io.micronaut.context.annotation.Bean; import io.micronaut.context.annotation.Executable; import io.micronaut.context.annotation.Import; @@ -53,6 +41,18 @@ import io.micronaut.inject.writer.ClassGenerationException; import jakarta.inject.Inject; +import java.io.IOException; +import java.lang.annotation.RetentionPolicy; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.Predicate; +import java.util.stream.Collectors; + /** * Generates the GraalVM reflect.json file at compilation time. * @@ -141,6 +141,11 @@ public void visitClass(ClassElement element, VisitorContext context) { .forEach(m -> processMethodElement(m, reflectiveClasses)); element.getEnclosedElements(ElementQuery.ALL_FIELDS.annotated(ann -> ann.hasAnnotation(ReflectiveAccess.class))) .forEach(m -> processFieldElement(m, reflectiveClasses)); + if (!element.isInner()) { + // Inner classes aren't processed if there is no annotation + // We might trigger the visitor twice but the originatingElements check should avoid it + element.getEnclosedElements(ElementQuery.ALL_INNER_CLASSES).forEach(c -> visitClass(c, context)); + } if (element.hasAnnotation(TypeHint.class)) { final String[] introspectedClasses = element.stringValues(TypeHint.class); diff --git a/graal/src/test/groovy/io/micronaut/graal/reflect/GraalTypeElementVisitorSpec.groovy b/graal/src/test/groovy/io/micronaut/graal/reflect/GraalTypeElementVisitorSpec.groovy index 555dda9e17e..659b9fdf7b2 100644 --- a/graal/src/test/groovy/io/micronaut/graal/reflect/GraalTypeElementVisitorSpec.groovy +++ b/graal/src/test/groovy/io/micronaut/graal/reflect/GraalTypeElementVisitorSpec.groovy @@ -17,7 +17,7 @@ import io.micronaut.core.annotation.Introspected; @Introspected class Test { - + } ''') @@ -36,7 +36,7 @@ import io.micronaut.core.annotation.*; @TypeHint(Bar.class) class Test { - + } class Bar {} @@ -68,7 +68,7 @@ import io.micronaut.core.annotation.*; ) ) class Test { - + } class Bar {} @@ -96,7 +96,7 @@ import io.micronaut.core.annotation.*; @TypeHint(value = Bar.class, typeNames = "java.lang.String") class Test { - + } class Bar {} @@ -136,7 +136,7 @@ import io.micronaut.core.annotation.*; @TypeHint(value = {Bar.class, String[].class}) class Test { - + } class Bar {} @@ -177,7 +177,7 @@ import io.micronaut.core.annotation.*; @TypeHint(value=Bar.class, accessType = TypeHint.AccessType.ALL_PUBLIC_METHODS) class Test { - + } class Bar {} @@ -202,10 +202,10 @@ package test; import io.micronaut.core.annotation.*; class Test { - + @ReflectiveAccess private String name; - + @ReflectiveAccess public String getFoo() { return name; @@ -238,10 +238,10 @@ import io.micronaut.core.annotation.*; @jakarta.inject.Singleton class Test { - + @jakarta.inject.Inject private String name; - + @jakarta.inject.Inject private void setFoo(Other other) { } @@ -282,9 +282,9 @@ class HTTPCheck extends NewCheck { } } abstract class NewCheck { - + private String status; - + @ReflectiveAccess protected void setStatus(String status) { this.status = status; @@ -295,15 +295,22 @@ abstract class NewCheck { ''') when: - AnnotationValue config = configurer.getAnnotationMetadata().getAnnotationValuesByType(ReflectionConfig).first() + // New check is returned first because the methods from the subtype are processed first + AnnotationValue httpCheck = configurer.getAnnotationMetadata().getAnnotationValuesByType(ReflectionConfig).get(1) + AnnotationValue newCheck = configurer.getAnnotationMetadata().getAnnotationValuesByType(ReflectionConfig).get(0) then: - config - config.stringValue("type").get() == 'test.HTTPCheck' - config.enumValues("accessType", TypeHint.AccessType) == [] as TypeHint.AccessType[] - config.getAnnotations("methods").size() == 1 - config.getAnnotations("methods").first().stringValue("name").get() == 'setInterval' - config.getAnnotations("methods").first().classValues("parameterTypes") == [String] as Class[] + httpCheck + httpCheck.stringValue("type").get() == 'test.HTTPCheck' + httpCheck.enumValues("accessType", TypeHint.AccessType) == [] as TypeHint.AccessType[] + httpCheck.getAnnotations("methods").size() == 1 + httpCheck.getAnnotations("methods").first().stringValue("name").get() == 'setInterval' + httpCheck.getAnnotations("methods").first().classValues("parameterTypes") == [String] as Class[] + newCheck.stringValue("type").get() == 'test.NewCheck' + newCheck.enumValues("accessType", TypeHint.AccessType) == [] as TypeHint.AccessType[] + newCheck.getAnnotations("methods").size() == 1 + newCheck.getAnnotations("methods").first().stringValue("name").get() == 'setStatus' + newCheck.getAnnotations("methods").first().classValues("parameterTypes") == [String] as Class[] } void "test write reflect.json for @ReflectiveAccess with classes"() { @@ -399,7 +406,7 @@ enum Test { config config.stringValue("type").get() == 'test.Test' config.enumValues("accessType", TypeHint.AccessType) == [TypeHint.AccessType.ALL_PUBLIC_METHODS, TypeHint.AccessType.ALL_DECLARED_CONSTRUCTORS, TypeHint.AccessType.ALL_DECLARED_FIELDS] as TypeHint.AccessType[] - config.getAnnotations("methods").size() == 0 + config.getAnnotations("methods").size() == 2 // Two methods from Enum } } diff --git a/inject-groovy-test/src/main/groovy/io/micronaut/ast/transform/test/AbstractBeanDefinitionSpec.groovy b/inject-groovy-test/src/main/groovy/io/micronaut/ast/transform/test/AbstractBeanDefinitionSpec.groovy index 065bf139d55..07cae25f985 100644 --- a/inject-groovy-test/src/main/groovy/io/micronaut/ast/transform/test/AbstractBeanDefinitionSpec.groovy +++ b/inject-groovy-test/src/main/groovy/io/micronaut/ast/transform/test/AbstractBeanDefinitionSpec.groovy @@ -18,8 +18,6 @@ package io.micronaut.ast.transform.test import groovy.transform.CompileStatic import io.micronaut.aop.internal.InterceptorRegistryBean import io.micronaut.ast.groovy.annotation.GroovyAnnotationMetadataBuilder -import io.micronaut.ast.groovy.utils.AstAnnotationUtils -import io.micronaut.ast.groovy.utils.ExtendedParameter import io.micronaut.ast.groovy.utils.InMemoryByteCodeGroovyClassLoader import io.micronaut.ast.groovy.visitor.GroovyElementFactory import io.micronaut.ast.groovy.visitor.GroovyVisitorContext @@ -71,9 +69,9 @@ abstract class AbstractBeanDefinitionSpec extends Specification { def cc = new CompilerConfiguration() def sourceUnit = new SourceUnit("test", source, cc, new GroovyClassLoader(), new ErrorCollector(cc)) def compilationUnit = new CompilationUnit() - def metadata = AstAnnotationUtils.getAnnotationMetadata(sourceUnit, compilationUnit, cn) - def elementFactory = new GroovyElementFactory(new GroovyVisitorContext(sourceUnit, compilationUnit)) - return elementFactory.newClassElement(cn, metadata) + def visitorContext = new GroovyVisitorContext(sourceUnit, compilationUnit) + def elementFactory = new GroovyElementFactory(visitorContext) + return elementFactory.newClassElement(cn, visitorContext.getElementAnnotationMetadataFactory()) } else { throw new IllegalArgumentException("No class found in passed source code") } @@ -87,9 +85,9 @@ abstract class AbstractBeanDefinitionSpec extends Specification { def cc = new CompilerConfiguration() def sourceUnit = new SourceUnit("test", source, cc, new GroovyClassLoader(), new ErrorCollector(cc)) def compilationUnit = new CompilationUnit() - def metadata = AstAnnotationUtils.getAnnotationMetadata(sourceUnit, compilationUnit, node) - def elementFactory = new GroovyElementFactory(new GroovyVisitorContext(sourceUnit, compilationUnit)) - return elementFactory.newClassElement(node, metadata) + def visitorContext = new GroovyVisitorContext(sourceUnit, compilationUnit) + def elementFactory = new GroovyElementFactory(visitorContext) + return elementFactory.newClassElement(node, visitorContext.getElementAnnotationMetadataFactory()) } } } @@ -133,7 +131,7 @@ abstract class AbstractBeanDefinitionSpec extends Specification { def beanDefName= (className.startsWith('$') ? '' : '$') + className + BeanDefinitionWriter.CLASS_SUFFIX String beanFullName = "${packageName}.${beanDefName}" - def classLoader = new InMemoryByteCodeGroovyClassLoader() + def classLoader = new InMemoryByteCodeGroovyClassLoader() {} classLoader.parseClass(classStr) try { return (BeanDefinition) classLoader.loadClass(beanFullName).newInstance() @@ -187,7 +185,7 @@ abstract class AbstractBeanDefinitionSpec extends Specification { def sourceUnit = Mock(SourceUnit) sourceUnit.getErrorCollector() >> new ErrorCollector(new CompilerConfiguration()) GroovyAnnotationMetadataBuilder builder = new GroovyAnnotationMetadataBuilder(sourceUnit, null) - AnnotationMetadata metadata = element != null ? builder.build(element) : null + AnnotationMetadata metadata = element != null ? builder.lookupOrBuildForType(element) : null AbstractAnnotationMetadataBuilder.copyToRuntime() return metadata } @@ -198,17 +196,17 @@ abstract class AbstractBeanDefinitionSpec extends Specification { GroovyAnnotationMetadataBuilder builder = new GroovyAnnotationMetadataBuilder(Stub(SourceUnit) { getErrorCollector() >> null }, null) - AnnotationMetadata metadata = method != null ? builder.build(method) : null + AnnotationMetadata metadata = method != null ? builder.lookupOrBuildForMethod(element, method) : null AbstractAnnotationMetadataBuilder.copyToRuntime() return metadata } - AnnotationMetadata buildFieldAnnotationMetadata(String cls, @Language("groovy") String source, String methodName, String fieldName) { + AnnotationMetadata buildParameterAnnotationMetadata(String cls, @Language("groovy") String source, String methodName, String fieldName) { ClassNode element = buildClassNode(source, cls) MethodNode method = element.getMethods(methodName)[0] Parameter parameter = Arrays.asList(method.getParameters()).find { it.name == fieldName } GroovyAnnotationMetadataBuilder builder = new GroovyAnnotationMetadataBuilder(null, null) - AnnotationMetadata metadata = method != null ? builder.build(new ExtendedParameter(method, parameter)) : null + AnnotationMetadata metadata = method != null ? builder.lookupOrBuildForParameter(element, method, parameter) : null AbstractAnnotationMetadataBuilder.copyToRuntime() return metadata } diff --git a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/InjectTransform.groovy b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/InjectTransform.groovy index d7fafa80445..00ace8b874c 100644 --- a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/InjectTransform.groovy +++ b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/InjectTransform.groovy @@ -17,21 +17,28 @@ package io.micronaut.ast.groovy import groovy.transform.CompilationUnitAware import groovy.transform.CompileStatic -import io.micronaut.ast.groovy.config.GroovyConfigurationMetadataBuilder -import io.micronaut.ast.groovy.utils.AstAnnotationUtils import io.micronaut.ast.groovy.utils.AstMessageUtils import io.micronaut.ast.groovy.utils.InMemoryByteCodeGroovyClassLoader import io.micronaut.ast.groovy.utils.InMemoryClassWriterOutputVisitor import io.micronaut.ast.groovy.visitor.GroovyPackageElement import io.micronaut.ast.groovy.visitor.GroovyVisitorContext import io.micronaut.context.annotation.Configuration -import io.micronaut.context.annotation.ConfigurationReader import io.micronaut.context.annotation.Context -import io.micronaut.core.annotation.AnnotationMetadata - -import io.micronaut.inject.configuration.ConfigurationMetadataBuilder -import io.micronaut.inject.writer.* -import org.codehaus.groovy.ast.* +import io.micronaut.inject.processing.ProcessingException +import io.micronaut.inject.processing.BeanDefinitionCreator +import io.micronaut.inject.processing.BeanDefinitionCreatorFactory +import io.micronaut.inject.visitor.VisitorConfiguration +import io.micronaut.inject.writer.BeanConfigurationWriter +import io.micronaut.inject.writer.BeanDefinitionReferenceWriter +import io.micronaut.inject.writer.BeanDefinitionVisitor +import io.micronaut.inject.writer.ClassWriterOutputVisitor +import io.micronaut.inject.writer.DirectoryClassWriterOutputVisitor +import org.codehaus.groovy.ast.ASTNode +import org.codehaus.groovy.ast.AnnotatedNode +import org.codehaus.groovy.ast.ClassNode +import org.codehaus.groovy.ast.InnerClassNode +import org.codehaus.groovy.ast.ModuleNode +import org.codehaus.groovy.ast.PackageNode import org.codehaus.groovy.control.CompilationUnit import org.codehaus.groovy.control.CompilePhase import org.codehaus.groovy.control.SourceUnit @@ -40,7 +47,6 @@ import org.codehaus.groovy.transform.ASTTransformation import org.codehaus.groovy.transform.GroovyASTTransformation import java.lang.reflect.Modifier -import java.util.function.Predicate /** * An AST transformation that produces metadata for use by the injection container * @@ -52,46 +58,33 @@ import java.util.function.Predicate @GroovyASTTransformation(phase = CompilePhase.CANONICALIZATION) class InjectTransform implements ASTTransformation, CompilationUnitAware { - public static final String ANN_VALID = "javax.validation.Valid" - public static final String ANN_CONSTRAINT = "javax.validation.Constraint" - public static final String ANN_CONFIGURATION_ADVICE = "io.micronaut.runtime.context.env.ConfigurationAdvice" - public static final String ANN_VALIDATED = "io.micronaut.validation.Validated" - public static final Predicate IS_CONSTRAINT = (Predicate) { AnnotationMetadata am -> - am.hasStereotype(InjectTransform.ANN_CONSTRAINT) || am.hasStereotype(InjectTransform.ANN_VALID) - } CompilationUnit unit - ConfigurationMetadataBuilder configurationMetadataBuilder @Override void visit(ASTNode[] nodes, SourceUnit source) { - configurationMetadataBuilder = new GroovyConfigurationMetadataBuilder(source, unit) ModuleNode moduleNode = source.getAST() Map beanDefinitionWriters = [:] File classesDir = source.configuration.targetDirectory boolean defineClassesInMemory = source.classLoader instanceof InMemoryByteCodeGroovyClassLoader ClassWriterOutputVisitor outputVisitor if (defineClassesInMemory) { - outputVisitor = new InMemoryClassWriterOutputVisitor( - source.classLoader as InMemoryByteCodeGroovyClassLoader - ) + outputVisitor = new InMemoryClassWriterOutputVisitor(source.classLoader as InMemoryByteCodeGroovyClassLoader) } else { - outputVisitor = new DirectoryClassWriterOutputVisitor( - classesDir - ) + outputVisitor = new DirectoryClassWriterOutputVisitor(classesDir) } List classes = moduleNode.getClasses() if (classes.size() == 1) { ClassNode classNode = classes[0] if (classNode.nameWithoutPackage == 'package-info') { PackageNode packageNode = classNode.getPackage() - if (AstAnnotationUtils.hasStereotype(source, unit, packageNode, Configuration)) { - def annotationMetadata = AstAnnotationUtils.getAnnotationMetadata(source, unit, packageNode) - GroovyVisitorContext visitorContext = new GroovyVisitorContext(source, unit) + GroovyVisitorContext visitorContext = new GroovyVisitorContext(source, unit) + GroovyPackageElement groovyPackageElement = new GroovyPackageElement(visitorContext, packageNode, visitorContext.getElementAnnotationMetadataFactory()) + if (groovyPackageElement.hasStereotype(Configuration)) { BeanConfigurationWriter writer = new BeanConfigurationWriter( classNode.packageName, - new GroovyPackageElement(visitorContext, packageNode, annotationMetadata), - annotationMetadata + groovyPackageElement, + groovyPackageElement.getAnnotationMetadata() ) try { writer.accept(outputVisitor) @@ -100,27 +93,40 @@ class InjectTransform implements ASTTransformation, CompilationUnitAware { AstMessageUtils.error(source, classNode, "Error generating bean configuration for package-info class [${classNode.name}]: $e.message") } } - return } } + GroovyVisitorContext groovyVisitorContext = new GroovyVisitorContext(source, unit) { + @Override + VisitorConfiguration getConfiguration() { + new VisitorConfiguration() { + @Override + boolean includeTypeLevelAnnotationsInGenericArguments() { + return false + } + } + } + } + def elementAnnotationMetadataFactory = groovyVisitorContext + .getElementAnnotationMetadataFactory() + .readOnly() for (ClassNode classNode in classes) { if ((classNode instanceof InnerClassNode && !Modifier.isStatic(classNode.getModifiers()))) { continue - } else { - if (classNode.isInterface()) { - if (AstAnnotationUtils.hasStereotype(source, unit, classNode, InjectVisitor.INTRODUCTION_TYPE) || - AstAnnotationUtils.hasStereotype(source, unit, classNode, ConfigurationReader.class)) { - InjectVisitor injectVisitor = new InjectVisitor(source, unit, classNode, configurationMetadataBuilder) - injectVisitor.visitClass(classNode) - beanDefinitionWriters.putAll(injectVisitor.beanDefinitionWriters) + } + try { + def classElement = groovyVisitorContext.getElementFactory().newClassElement(classNode, elementAnnotationMetadataFactory) + BeanDefinitionCreator beanProcessor = BeanDefinitionCreatorFactory.produce(classElement, groovyVisitorContext); + beanProcessor.build().forEach(writer -> { + if (writer.getBeanTypeName() == classNode.getName()) { + beanDefinitionWriters.put(classNode, writer) + } else { + beanDefinitionWriters.put(new AnnotatedNode(), writer) } - } else { - InjectVisitor injectVisitor = new InjectVisitor(source, unit, classNode, configurationMetadataBuilder) - injectVisitor.visitClass(classNode) - beanDefinitionWriters.putAll(injectVisitor.beanDefinitionWriters) - } + }) + } catch (ProcessingException ex) { + groovyVisitorContext.fail(ex.getMessage(), ex.getOriginatingElement() as ASTNode) } } @@ -129,10 +135,7 @@ class InjectTransform implements ASTTransformation, CompilationUnitAware { String beanTypeName = beanDefWriter.beanTypeName AnnotatedNode beanClassNode = entry.key try { - BeanDefinitionReferenceWriter beanReferenceWriter = new BeanDefinitionReferenceWriter( - beanDefWriter - ) - + BeanDefinitionReferenceWriter beanReferenceWriter = new BeanDefinitionReferenceWriter(beanDefWriter) beanReferenceWriter.setRequiresMethodProcessing(beanDefWriter.requiresMethodProcessing()) beanReferenceWriter.setContextScope(beanDefWriter.getAnnotationMetadata().hasDeclaredAnnotation(Context)) beanDefWriter.visitBeanDefinitionEnd() @@ -142,20 +145,15 @@ class InjectTransform implements ASTTransformation, CompilationUnitAware { } else if (source.source instanceof StringReaderSource && defineClassesInMemory) { beanReferenceWriter.accept(outputVisitor) beanDefWriter.accept(outputVisitor) - } - - } catch (Throwable e) { AstMessageUtils.error(source, beanClassNode, "Error generating bean definition class for dependency injection of class [${beanTypeName}]: $e.message") e.printStackTrace(System.err) } } if (!beanDefinitionWriters.isEmpty()) { - try { outputVisitor.finish() - } catch (Throwable e) { AstMessageUtils.error(source, moduleNode, "Error generating META-INF/services files: $e.message") if (e.message == null) { @@ -163,8 +161,6 @@ class InjectTransform implements ASTTransformation, CompilationUnitAware { } } } - - AstAnnotationUtils.invalidateCache() } @Override diff --git a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/InjectVisitor.groovy b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/InjectVisitor.groovy deleted file mode 100644 index ab2c898f0bd..00000000000 --- a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/InjectVisitor.groovy +++ /dev/null @@ -1,1771 +0,0 @@ -/* - * Copyright 2017-2021 original authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.micronaut.ast.groovy - -import groovy.transform.CompileDynamic -import groovy.transform.CompileStatic -import groovy.transform.PackageScope -import io.micronaut.aop.Adapter -import io.micronaut.aop.Around -import io.micronaut.aop.Interceptor -import io.micronaut.aop.InterceptorBinding -import io.micronaut.aop.InterceptorKind -import io.micronaut.aop.Introduction -import io.micronaut.aop.internal.intercepted.InterceptedMethodUtil -import io.micronaut.aop.writer.AopProxyWriter -import io.micronaut.ast.groovy.annotation.GroovyAnnotationMetadataBuilder -import io.micronaut.ast.groovy.utils.AstAnnotationUtils -import io.micronaut.ast.groovy.utils.AstGenericUtils -import io.micronaut.ast.groovy.utils.AstMessageUtils -import io.micronaut.ast.groovy.utils.PublicAbstractMethodVisitor -import io.micronaut.ast.groovy.utils.PublicMethodVisitor -import io.micronaut.ast.groovy.visitor.GroovyElementFactory -import io.micronaut.ast.groovy.visitor.GroovyVisitorContext -import io.micronaut.context.RequiresCondition -import io.micronaut.context.annotation.Bean -import io.micronaut.context.annotation.ConfigurationBuilder -import io.micronaut.context.annotation.ConfigurationInject -import io.micronaut.context.annotation.ConfigurationReader -import io.micronaut.context.annotation.DefaultScope -import io.micronaut.context.annotation.Executable -import io.micronaut.context.annotation.Factory -import io.micronaut.context.annotation.Property -import io.micronaut.context.annotation.Requires -import io.micronaut.context.annotation.Value -import io.micronaut.core.annotation.AccessorsStyle -import io.micronaut.core.annotation.AnnotationClassValue -import io.micronaut.core.annotation.AnnotationMetadata -import io.micronaut.core.annotation.AnnotationUtil -import io.micronaut.core.annotation.AnnotationValue -import io.micronaut.core.annotation.Internal -import io.micronaut.core.bind.annotation.Bindable -import io.micronaut.core.naming.NameUtils -import io.micronaut.core.reflect.ClassUtils -import io.micronaut.core.util.ArrayUtils -import io.micronaut.core.util.StringUtils -import io.micronaut.core.value.OptionalValues -import io.micronaut.inject.annotation.AnnotationMetadataHierarchy -import io.micronaut.inject.annotation.AnnotationMetadataReference -import io.micronaut.inject.annotation.DefaultAnnotationMetadata -import io.micronaut.inject.ast.ClassElement -import io.micronaut.inject.ast.Element -import io.micronaut.inject.ast.FieldElement -import io.micronaut.inject.ast.MethodElement -import io.micronaut.inject.ast.ParameterElement -import io.micronaut.inject.ast.PrimitiveElement -import io.micronaut.inject.configuration.ConfigurationMetadata -import io.micronaut.inject.configuration.ConfigurationMetadataBuilder -import io.micronaut.inject.configuration.PropertyMetadata -import io.micronaut.inject.visitor.VisitorConfiguration -import io.micronaut.inject.writer.BeanDefinitionReferenceWriter -import io.micronaut.inject.writer.BeanDefinitionVisitor -import io.micronaut.inject.writer.BeanDefinitionWriter -import io.micronaut.inject.writer.OriginatingElements -import org.codehaus.groovy.ast.ASTNode -import org.codehaus.groovy.ast.AnnotatedNode -import org.codehaus.groovy.ast.AnnotationNode -import org.codehaus.groovy.ast.ClassCodeVisitorSupport -import org.codehaus.groovy.ast.ClassHelper -import org.codehaus.groovy.ast.ClassNode -import org.codehaus.groovy.ast.FieldNode -import org.codehaus.groovy.ast.GenericsType -import org.codehaus.groovy.ast.MethodNode -import org.codehaus.groovy.ast.Parameter -import org.codehaus.groovy.ast.PropertyNode -import org.codehaus.groovy.ast.expr.ClassExpression -import org.codehaus.groovy.ast.expr.Expression -import org.codehaus.groovy.ast.expr.ListExpression -import org.codehaus.groovy.ast.tools.GeneralUtils -import org.codehaus.groovy.control.CompilationUnit -import org.codehaus.groovy.control.SourceUnit -import org.codehaus.groovy.control.messages.SyntaxErrorMessage -import org.codehaus.groovy.syntax.SyntaxException - -import java.lang.reflect.Modifier -import java.time.Duration -import java.util.concurrent.TimeUnit -import java.util.concurrent.atomic.AtomicInteger -import java.util.function.Function - -import static org.codehaus.groovy.ast.ClassHelper.makeCached -import static org.codehaus.groovy.ast.tools.GeneralUtils.getGetterName -import static org.codehaus.groovy.ast.tools.GeneralUtils.getSetterName - -@CompileStatic -final class InjectVisitor extends ClassCodeVisitorSupport { - public static final String AROUND_TYPE = AnnotationUtil.ANN_AROUND - public static final String INTRODUCTION_TYPE = AnnotationUtil.ANN_INTRODUCTION - final SourceUnit sourceUnit - final ClassNode concreteClass - final ClassElement concreteClassElement - AnnotationMetadata concreteClassAnnotationMetadata - final ClassElement originatingElement - final boolean isConfigurationProperties - final boolean isFactoryClass - final boolean isExecutableType - final boolean isAopProxyType - final boolean isDeclaredBean - final ConfigurationMetadataBuilder configurationMetadataBuilder - ConfigurationMetadata configurationMetadata - - final Map beanDefinitionWriters = [:] - private BeanDefinitionVisitor beanWriter - BeanDefinitionVisitor aopProxyWriter - final AtomicInteger adaptedMethodIndex = new AtomicInteger(0) - final AtomicInteger factoryMethodIndex = new AtomicInteger(0) - private final CompilationUnit compilationUnit - private final GroovyElementFactory elementFactory - GroovyVisitorContext groovyVisitorContext - - InjectVisitor(SourceUnit sourceUnit, CompilationUnit compilationUnit, ClassNode targetClassNode, ConfigurationMetadataBuilder configurationMetadataBuilder) { - this(sourceUnit, compilationUnit, targetClassNode, null, configurationMetadataBuilder) - } - - InjectVisitor(SourceUnit sourceUnit, CompilationUnit compilationUnit, ClassNode targetClassNode, Boolean configurationProperties, ConfigurationMetadataBuilder configurationMetadataBuilder) { - this.compilationUnit = compilationUnit - this.sourceUnit = sourceUnit - groovyVisitorContext = new GroovyVisitorContext(sourceUnit, compilationUnit) { - @Override - VisitorConfiguration getConfiguration() { - new VisitorConfiguration() { - @Override - boolean includeTypeLevelAnnotationsInGenericArguments() { - return false - } - } - } - } - this.elementFactory = groovyVisitorContext.getElementFactory() - this.configurationMetadataBuilder = configurationMetadataBuilder - this.concreteClass = targetClassNode - def annotationMetadata = AstAnnotationUtils.getAnnotationMetadata(sourceUnit, compilationUnit, targetClassNode) - this.concreteClassAnnotationMetadata = annotationMetadata - this.originatingElement = elementFactory.newClassElement(concreteClass, annotationMetadata) - this.concreteClassElement = originatingElement - this.isFactoryClass = annotationMetadata.hasStereotype(Factory) - this.isAopProxyType = hasAroundStereotype(annotationMetadata) && !targetClassNode.isAbstract() && !concreteClassElement.isAssignable(Interceptor.class) - this.isExecutableType = isAopProxyType || annotationMetadata.hasStereotype(Executable) - this.isConfigurationProperties = configurationProperties != null ? configurationProperties : annotationMetadata.hasDeclaredStereotype(ConfigurationReader) - if (isConfigurationProperties) { - this.configurationMetadata = configurationMetadataBuilder.visitProperties( - concreteClass, - null - ) - } - - if (isAopProxyType && Modifier.isFinal(targetClassNode.modifiers)) { - addError("Cannot apply AOP advice to final class. Class must be made non-final to support proxying: " + targetClassNode.name, targetClassNode) - } - this.isDeclaredBean = isExecutableType || isConfigurationProperties || isFactoryClass || annotationMetadata.hasStereotype(AnnotationUtil.SCOPE) || annotationMetadata.hasStereotype(DefaultScope) || annotationMetadata.hasDeclaredStereotype(Bean) || concreteClass.declaredConstructors.any { - AnnotationMetadata constructorMetadata = AstAnnotationUtils.getAnnotationMetadata(sourceUnit, compilationUnit, it) - constructorMetadata.hasStereotype(AnnotationUtil.INJECT) - } - if (isDeclaredBean) { - defineBeanDefinition(concreteClass) - } - } - - static boolean hasAroundStereotype(AnnotationMetadata annotationMetadata) { - if (annotationMetadata.hasStereotype(AROUND_TYPE)) { - return true - } else { - if (annotationMetadata.hasStereotype(AnnotationUtil.ANN_INTERCEPTOR_BINDINGS)) { - return annotationMetadata.getAnnotationValuesByType(InterceptorBinding) - .stream().anyMatch{ av -> - av.enumValue("kind", InterceptorKind).orElse(InterceptorKind.AROUND) == InterceptorKind.AROUND - } - } - } - return false - } - - BeanDefinitionVisitor getBeanWriter() { - if (this.beanWriter == null) { - defineBeanDefinition(concreteClass) - } - return beanWriter - } - - @Override - void addError(String msg, ASTNode expr) { - SourceUnit source = getSourceUnit() - source.getErrorCollector().addError( - new SyntaxErrorMessage(new SyntaxException(msg + '\n', expr.getLineNumber(), expr.getColumnNumber(), expr.getLastLineNumber(), expr.getLastColumnNumber()), source) - ) - } - - @Override - void visitClass(ClassNode node) { - AnnotationMetadata annotationMetadata - if (concreteClass == node) { - annotationMetadata = concreteClassAnnotationMetadata - } else { - annotationMetadata = AstAnnotationUtils.getAnnotationMetadata(sourceUnit, compilationUnit, node) - } - boolean isInterface = node.isInterface() - if (isConfigurationProperties && isInterface) { - String adviceType = InjectTransform.ANN_CONFIGURATION_ADVICE - ((Element)concreteClassElement).annotate(adviceType) // hack to make Groovy compile - concreteClassAnnotationMetadata = concreteClassElement.annotationMetadata - annotationMetadata = concreteClassAnnotationMetadata - } - if (annotationMetadata.hasStereotype(INTRODUCTION_TYPE)) { - String packageName = node.packageName - String beanClassName = node.nameWithoutPackage - - AnnotationValue[] aroundInterceptors = InterceptedMethodUtil - .resolveInterceptorBinding(annotationMetadata, InterceptorKind.AROUND) - - AnnotationValue[] introductionInterceptors = InterceptedMethodUtil - .resolveInterceptorBinding(annotationMetadata, InterceptorKind.INTRODUCTION) - - - AnnotationValue[] interceptorTypes = (AnnotationValue[]) ArrayUtils.concat(aroundInterceptors, introductionInterceptors) - ClassElement[] interfaceTypes = annotationMetadata.getValue(Introduction.class, "interfaces", String[].class).orElse(new String[0]) - .collect { ClassElement.of(it) } - - AopProxyWriter aopProxyWriter = new AopProxyWriter( - packageName, - beanClassName, - isInterface, - originatingElement, - annotationMetadata, - interfaceTypes, - groovyVisitorContext, - configurationMetadataBuilder, - configurationMetadata, - interceptorTypes - ) - ClassElement groovyClassElement = elementFactory.newClassElement( - node, - annotationMetadata - ) - aopProxyWriter.visitTypeArguments(groovyClassElement.getAllTypeArguments()) - populateProxyWriterConstructor(groovyClassElement, aopProxyWriter, groovyClassElement.getPrimaryConstructor().orElse(null)) - beanDefinitionWriters.put(node, aopProxyWriter) - this.aopProxyWriter = aopProxyWriter - visitAnnotationMetadata(aopProxyWriter, annotationMetadata) - visitIntroductionTypePublicMethods(aopProxyWriter, node) - if (ArrayUtils.isNotEmpty(interfaceTypes)) { - List annotationNodes = node.annotations - Set interfacesToVisit = [] - - populateIntroducedInterfaces(annotationNodes, interfacesToVisit) - - if (!interfacesToVisit.isEmpty()) { - for (ClassNode itce in interfacesToVisit as Set) { - visitIntroductionTypePublicMethods(aopProxyWriter, itce) - } - } - } - - if (!isInterface) { - node.visitContents(this) - } - } else { - boolean isOwningClass = node == concreteClass - if (isOwningClass && concreteClass.abstract && !isDeclaredBean) { - return - } - - if (annotationMetadata.hasStereotype(AROUND_TYPE)) { - AnnotationValue[] interceptorTypeReferences = InterceptedMethodUtil - .resolveInterceptorBinding(annotationMetadata, InterceptorKind.AROUND) - resolveProxyWriter(annotationMetadata.getValues(AROUND_TYPE, Boolean.class), false, interceptorTypeReferences) - } - - ClassNode superClass = node.getSuperClass() - List superClasses = [] - while (superClass != null) { - superClasses.add(superClass) - superClass = superClass.getSuperClass() - } - superClasses = superClasses.reverse() - for (classNode in superClasses) { - if (classNode.name != ClassHelper.OBJECT_TYPE.name && classNode.name != GroovyObjectSupport.name && classNode.name != Script.name) { - classNode.visitContents(this) - } - } - super.visitClass(node) - } - } - - private void visitAnnotationMetadata(BeanDefinitionVisitor writer, AnnotationMetadata annotationMetadata) { - for (AnnotationValue annotation: annotationMetadata.getAnnotationValuesByType(Requires.class)) { - annotation.stringValue(RequiresCondition.MEMBER_BEAN_PROPERTY) - .ifPresent((String beanProperty) -> { - annotation.stringValue(RequiresCondition.MEMBER_BEAN) - .map{ String s -> compilationUnit.getAST().classes.find {ClassNode cn -> cn.name == s }} - .map{elementFactory.newClassElement(it, AstAnnotationUtils.getAnnotationMetadata(sourceUnit, compilationUnit, it))} - .ifPresent((ClassElement classElement) -> { - String requiredValue = annotation.stringValue().orElse(null); - String notEqualsValue = annotation.stringValue(RequiresCondition.MEMBER_NOT_EQUALS).orElse(null); - writer.visitAnnotationMemberPropertyInjectionPoint(classElement, beanProperty, requiredValue, notEqualsValue) - }) - }) - } - } - - private void populateIntroducedInterfaces(List annotationNodes, Set interfacesToVisit) { - for (ann in annotationNodes) { - if (ann.classNode.name == Introduction.class.getName()) { - Expression expression = ann.getMember("interfaces") - if (expression instanceof ClassExpression) { - interfacesToVisit.add(((ClassExpression) expression).type) - } else if (expression instanceof ListExpression) { - ListExpression list = (ListExpression) expression - for (expr in list.expressions) { - if (expr instanceof ClassExpression) { - interfacesToVisit.add(((ClassExpression) expr).type) - } - } - } - } else if (AstAnnotationUtils.hasStereotype(sourceUnit, compilationUnit, ann.classNode, Introduction)) { - populateIntroducedInterfaces(ann.classNode.annotations, interfacesToVisit) - } - } - } - - @CompileStatic - protected void visitIntroductionTypePublicMethods(AopProxyWriter aopProxyWriter, ClassNode node) { - AnnotationMetadata typeAnnotationMetadata = aopProxyWriter.getAnnotationMetadata() - SourceUnit source = this.sourceUnit - CompilationUnit unit = this.compilationUnit - ClassElement concreteClassElement = this.concreteClassElement - AnnotationMetadata concreteClassAnnotationMetadata = this.concreteClassAnnotationMetadata - PublicMethodVisitor publicMethodVisitor = new PublicAbstractMethodVisitor(source, unit) { - - @Override - protected boolean isAcceptableMethod(MethodNode methodNode) { - return super.isAcceptableMethod(methodNode) || hasDeclaredAroundStereotype(AstAnnotationUtils.getAnnotationMetadata(source, unit, methodNode)) - } - - @Override - void accept(ClassNode classNode, MethodNode methodNode) { - AnnotationMetadata annotationMetadata - if (AstAnnotationUtils.isAnnotated(node.name, methodNode) || AstAnnotationUtils.hasAnnotation(methodNode, Override)) { - // Class annotations are referenced by concreteClassAnnotationMetadata - annotationMetadata = AstAnnotationUtils.newBuilder(source, unit).buildForParent(node.name, null, methodNode) - annotationMetadata = new AnnotationMetadataHierarchy(concreteClassAnnotationMetadata, annotationMetadata) - } else { - annotationMetadata = new AnnotationMetadataReference( - aopProxyWriter.getBeanDefinitionName() + BeanDefinitionReferenceWriter.REF_SUFFIX, - typeAnnotationMetadata - ) - } - MethodElement groovyMethodElement = elementFactory.newMethodElement( - concreteClassElement, - methodNode, - annotationMetadata - ) - - ClassNode owningType = AstGenericUtils.resolveTypeReference(methodNode.declaringClass) - ClassElement owningClassElement = elementFactory.newClassElement( - owningType, - concreteClassAnnotationMetadata - ) - - - if (!annotationMetadata.hasStereotype("io.micronaut.validation.Validated") && - isDeclaredBean) { - boolean hasConstraint - for (ParameterElement p: groovyMethodElement.getParameters()) { - AnnotationMetadata parameterMetadata = p.annotationMetadata - if (InjectTransform.IS_CONSTRAINT.test(parameterMetadata)) { - hasConstraint = true - break - } - } - if (hasConstraint) { - if (annotationMetadata instanceof AnnotationMetadataReference) { - annotationMetadata = AstAnnotationUtils.newBuilder(source, unit).buildForParent(node.name, node, methodNode) - groovyMethodElement = elementFactory.newMethodElement( - concreteClassElement, - methodNode, - annotationMetadata - ) - } - - annotationMetadata = addValidated(groovyMethodElement) - } - } - - final String[] readPrefixes = annotationMetadata.getValue(AccessorsStyle.class, "readPrefixes", String[].class) - .orElse(new String[]{AccessorsStyle.DEFAULT_READ_PREFIX}) - - if (isConfigurationProperties && methodNode.isAbstract()) { - if (!aopProxyWriter.isValidated()) { - aopProxyWriter.setValidated(InjectTransform.IS_CONSTRAINT.test(annotationMetadata)) - } - - if (!NameUtils.isReaderName(methodNode.name, readPrefixes)) { - error("Only getter methods are allowed on @ConfigurationProperties interfaces: " + methodNode.name + ". You can change the accessors using @AccessorsStyle annotation)", classNode) - return - } - - if (groovyMethodElement.hasParameters()) { - error("Only zero argument getter methods are allowed on @ConfigurationProperties interfaces: " + methodNode.name, classNode) - return - } - String propertyName = NameUtils.getPropertyNameForGetter(methodNode.name, readPrefixes) - String propertyType = methodNode.returnType.name - - if ("void".equals(propertyType)) { - error("Getter methods must return a value @ConfigurationProperties interfaces: " + methodNode.name, classNode) - return - } - - final PropertyMetadata propertyMetadata = configurationMetadataBuilder.visitProperty( - current.isInterface() ? current : classNode, - classNode, - propertyType, - propertyName, - null, - annotationMetadata.stringValue(Bindable.class, "defaultValue").orElse(null) - ) - - annotationMetadata = addPropertyMetadata( - groovyMethodElement, - propertyMetadata - ) - - final ClassNode typeElement = !ClassUtils.isJavaBasicType(propertyType) ? methodNode.returnType : null - if (typeElement != null && AstAnnotationUtils.hasStereotype(source, unit, typeElement, AnnotationUtil.SCOPE)) { - annotationMetadata = addBeanConfigAdvise(annotationMetadata) - } else { - annotationMetadata = addAnnotation(groovyMethodElement, InjectTransform.ANN_CONFIGURATION_ADVICE) - } - - } - - if (hasAroundStereotype(AstAnnotationUtils.getAnnotationMetadata(source, unit, methodNode))) { - AnnotationValue[] interceptorTypeReferences = InterceptedMethodUtil - .resolveInterceptorBinding(annotationMetadata, InterceptorKind.AROUND) - aopProxyWriter.visitInterceptorBinding(interceptorTypeReferences) - } - - if (methodNode.isAbstract()) { - aopProxyWriter.visitIntroductionMethod( - owningClassElement, - groovyMethodElement - ) - } else { - aopProxyWriter.visitAroundMethod( - owningClassElement, - groovyMethodElement - ) - } - } - - @CompileDynamic - private void error(String msg, ClassNode classNode) { - addError(msg, (ASTNode) classNode) - } - - @CompileDynamic - private AnnotationMetadata addBeanConfigAdvise(AnnotationMetadata annotationMetadata) { - new GroovyAnnotationMetadataBuilder(source, compilationUnit).annotate( - annotationMetadata, - AnnotationValue.builder(InjectTransform.ANN_CONFIGURATION_ADVICE).member("bean", true).build() - ) - } - - } - publicMethodVisitor.accept(node) - } - - @Override - protected void visitConstructorOrMethod(MethodNode methodNode, boolean isConstructor) { - if (methodNode.isSynthetic() || methodNode.name.contains('$')) return - - String methodName = methodNode.name - ClassNode declaringClass = methodNode.declaringClass - AnnotationMetadata methodAnnotationMetadata = getAnnotationMetadataHierarchy( - AstAnnotationUtils.getMethodAnnotationMetadata(sourceUnit, compilationUnit, methodNode) - ) - def declaringElement = elementFactory.newClassElement( - declaringClass, - AnnotationMetadata.EMPTY_METADATA - ) - - final boolean isStatic = methodNode.isStatic() - final boolean isAbstract = methodNode.isAbstract() - final boolean isPrivate = methodNode.isPrivate() - final boolean isPublic = methodNode.isPublic() - - if (isFactoryClass && !isConstructor && methodAnnotationMetadata.hasDeclaredStereotype(Bean.getName(), AnnotationUtil.SCOPE)) { - boolean isParent = declaringClass != concreteClass - MethodNode overriddenMethod = isParent ? concreteClass.getMethod(methodName, methodNode.parameters) : methodNode - boolean overridden = isParent && overriddenMethod.declaringClass != declaringClass - if (!overridden) { - methodAnnotationMetadata = new GroovyAnnotationMetadataBuilder(sourceUnit, compilationUnit).buildForParent(methodNode.returnType, methodNode, true) - - visitBeanFactoryElement(declaringClass, methodNode, methodAnnotationMetadata, methodName) - } - } else if (methodAnnotationMetadata.hasStereotype(AnnotationUtil.INJECT) || - methodAnnotationMetadata.hasDeclaredAnnotation(AnnotationUtil.POST_CONSTRUCT) || - methodAnnotationMetadata.hasDeclaredAnnotation(AnnotationUtil.PRE_DESTROY)) { - if (isConstructor && methodAnnotationMetadata.hasStereotype(AnnotationUtil.INJECT)) { - // constructor with explicit @Inject - defineBeanDefinition(concreteClass) - } else if (!isConstructor) { - if (!isStatic && !isAbstract) { - boolean isParent = declaringClass != concreteClass - MethodNode overriddenMethod = isParent ? concreteClass.getMethod(methodName, methodNode.parameters) : methodNode - boolean overridden = isParent && overriddenMethod.declaringClass != declaringClass - - boolean isPackagePrivate = isPackagePrivate(methodNode, methodNode.modifiers) - - if (isParent && !isPrivate && !isPackagePrivate) { - if (overridden) { - // bail out if the method has been overridden, since it will have already been handled - return - } - } - boolean packagesDiffer = overriddenMethod.declaringClass.packageName != declaringClass.packageName - boolean isPackagePrivateAndPackagesDiffer = overridden && packagesDiffer && isPackagePrivate - boolean requiresReflection = isPrivate || isPackagePrivateAndPackagesDiffer - boolean overriddenInjected = overridden && AstAnnotationUtils.hasStereotype(sourceUnit, compilationUnit, overriddenMethod, AnnotationUtil.INJECT) - - if (isParent && isPackagePrivate && !isPackagePrivateAndPackagesDiffer && overriddenInjected) { - // bail out if the method has been overridden by another method annotated with @INject - return - } - if (isParent && overridden && !overriddenInjected && !isPackagePrivateAndPackagesDiffer && !isPrivate) { - // bail out if the overridden method is package private and in the same package - // and is not annotated with @Inject - return - } - if (!requiresReflection && isInheritedAndNotPublic(methodNode, declaringClass, methodNode.modifiers)) { - requiresReflection = true - } - - MethodElement groovyMethodElement = elementFactory.newMethodElement( - declaringElement, - methodNode, - methodAnnotationMetadata - ) - - if (isDeclaredBean && methodAnnotationMetadata.hasDeclaredAnnotation(AnnotationUtil.POST_CONSTRUCT)) { - defineBeanDefinition(concreteClass) - getBeanWriter().visitPostConstructMethod( - declaringElement, - groovyMethodElement, - requiresReflection, - groovyVisitorContext - ) - } else if (isDeclaredBean && methodAnnotationMetadata.hasDeclaredAnnotation(AnnotationUtil.PRE_DESTROY)) { - defineBeanDefinition(concreteClass) - beanWriter.visitPreDestroyMethod( - declaringElement, - groovyMethodElement, - requiresReflection, - groovyVisitorContext - ) - if (aopProxyWriter instanceof AopProxyWriter && !((AopProxyWriter)aopProxyWriter).isProxyTarget()) { - aopProxyWriter.visitPreDestroyMethod( - declaringElement, - groovyMethodElement, - requiresReflection, - groovyVisitorContext - ) - } - } else if (methodAnnotationMetadata.hasStereotype(AnnotationUtil.INJECT)) { - defineBeanDefinition(concreteClass) - getBeanWriter().visitMethodInjectionPoint( - declaringElement, - groovyMethodElement, - requiresReflection, - groovyVisitorContext - ) - } - } - } - } else if (!isConstructor) { - boolean hasInvalidModifiers = isStatic || isAbstract || methodNode.isSynthetic() || methodAnnotationMetadata.hasAnnotation(Internal) || isPrivate - boolean isExecutable = ((isExecutableType && isPublic) || methodAnnotationMetadata.hasStereotype(Executable) || hasAroundStereotype(methodAnnotationMetadata)) - - if (isDeclaredBean && isExecutable) { - if (hasInvalidModifiers) { - if (isPrivate && (methodAnnotationMetadata.hasDeclaredStereotype(Executable) || hasDeclaredAroundStereotype(methodAnnotationMetadata))) { - addError("Method annotated as executable but is declared private. Change the method to be non-private in order for AOP advice to be applied.", methodNode) - } - } else { - visitExecutableMethod( - declaringClass, - methodNode, - methodAnnotationMetadata, - methodName, - isPublic - ) - } - } else if (isConfigurationProperties && isPublic) { - methodAnnotationMetadata = AstAnnotationUtils.newBuilder(sourceUnit, compilationUnit).buildDeclared(methodNode) - - final String[] readPrefixes = declaringElement.getValue(AccessorsStyle.class, "readPrefixes", String[].class) - .orElse(new String[]{AccessorsStyle.DEFAULT_READ_PREFIX}) - final String[] writePrefixes = declaringElement.getValue(AccessorsStyle.class, "writePrefixes", String[].class) - .orElse(new String[]{AccessorsStyle.DEFAULT_WRITE_PREFIX}) - - if (NameUtils.isWriterName(methodNode.name, writePrefixes) && methodNode.parameters.length == 1) { - String propertyName = NameUtils.getPropertyNameForSetter(methodNode.name, writePrefixes) - MethodElement groovyMethodElement = elementFactory.newMethodElement( - declaringElement, - methodNode, - methodAnnotationMetadata - ) - ParameterElement parameterElement = groovyMethodElement.parameters[0] - - if (methodAnnotationMetadata.hasStereotype(ConfigurationBuilder.class)) { - getBeanWriter().visitConfigBuilderMethod( - parameterElement.type, - NameUtils.getterNameFor(propertyName, readPrefixes), - methodAnnotationMetadata, - configurationMetadataBuilder, - parameterElement.type.interface - ) - try { - visitConfigurationBuilder( - declaringElement, - methodAnnotationMetadata, - parameterElement.type, - getBeanWriter() - ) - } finally { - getBeanWriter().visitConfigBuilderEnd() - } - } else if (declaringClass.getField(propertyName) == null) { - if (shouldExclude(configurationMetadata, propertyName)) { - return - } - PropertyMetadata propertyMetadata = configurationMetadataBuilder.visitProperty( - concreteClass, - declaringClass, - parameterElement.type.name, - propertyName, - null, - null - ) - - methodAnnotationMetadata = addPropertyMetadata(parameterElement, propertyMetadata) - - getBeanWriter().visitSetterValue( - groovyMethodElement.declaringType, - groovyMethodElement, - false, - true - ) - } - } else if (NameUtils.isReaderName(methodNode.name, readPrefixes)) { - if (!getBeanWriter().isValidated()) { - getBeanWriter().setValidated(InjectTransform.IS_CONSTRAINT.test(methodAnnotationMetadata)) - } - } - } else { - def sourceUnit = sourceUnit - def compilationUnit = this.compilationUnit - final boolean isConstrained = isDeclaredBean && - methodNode.getParameters() - .any { Parameter p -> - AnnotationMetadata annotationMetadata = AstAnnotationUtils.getAnnotationMetadata(sourceUnit, compilationUnit, p) - InjectTransform.IS_CONSTRAINT.test(annotationMetadata) - } - if (isConstrained) { - if (hasInvalidModifiers) { - if (isPrivate) { - addError("Method annotated with constraints but is declared private. Change the method to be non-private in order for AOP advice to be applied.", methodNode) - } - } else if (isPublic) { - visitExecutableMethod(declaringClass, methodNode, methodAnnotationMetadata, methodName, isPublic) - } - } - } - } - } - - private AnnotationMetadata getAnnotationMetadataHierarchy(AnnotationMetadata methodAnnotationMetadata) { - return methodAnnotationMetadata instanceof AnnotationMetadataHierarchy ? methodAnnotationMetadata : new AnnotationMetadataHierarchy(concreteClassAnnotationMetadata, methodAnnotationMetadata) - } - - @CompileStatic - private void visitBeanFactoryElement( - ClassNode declaringClass, - AnnotatedNode annotatedNode, - AnnotationMetadata methodAnnotationMetadata, - String elementName) { - if (annotatedNode instanceof MethodNode && concreteClassAnnotationMetadata.hasDeclaredStereotype(Around)) { - visitExecutableMethod(declaringClass, annotatedNode, methodAnnotationMetadata, elementName, annotatedNode.isPublic()) - } - - ClassElement producedClassElement - ClassNode returnType - Map> allTypeArguments - BeanDefinitionWriter beanMethodWriter - AnnotationMetadata beanFactoryMetadata = new AnnotationMetadataHierarchy( - concreteClassAnnotationMetadata, - methodAnnotationMetadata - ); - if (annotatedNode instanceof MethodNode) { - - def methodNode = (MethodNode) annotatedNode - MethodElement factoryMethodElement = elementFactory.newMethodElement( - concreteClassElement, - methodNode, - beanFactoryMetadata - ) - producedClassElement = factoryMethodElement.genericReturnType - beanMethodWriter = new BeanDefinitionWriter( - factoryMethodElement, - OriginatingElements.of(originatingElement), - configurationMetadataBuilder, - groovyVisitorContext, - factoryMethodIndex.getAndIncrement() - ) - - returnType = methodNode.getReturnType() - allTypeArguments = factoryMethodElement.returnType.allTypeArguments - visitAnnotationMetadata(beanMethodWriter, beanFactoryMetadata) - beanMethodWriter.visitTypeArguments(allTypeArguments) - beanMethodWriter.visitBeanFactoryMethod( - originatingElement, - factoryMethodElement - ) - } else { - FieldNode fieldNode - if (annotatedNode instanceof PropertyNode) { - fieldNode = ((PropertyNode) annotatedNode).field - } else { - fieldNode = annotatedNode as FieldNode - } - FieldElement factoryField = elementFactory.newFieldElement( - concreteClassElement, - fieldNode, - beanFactoryMetadata - ) - producedClassElement = factoryField.genericField - beanMethodWriter = new BeanDefinitionWriter( - factoryField, - OriginatingElements.of(originatingElement), - configurationMetadataBuilder, - groovyVisitorContext, - factoryMethodIndex.getAndIncrement() - ) - - returnType = factoryField.type.nativeType as ClassNode - allTypeArguments = factoryField.type.allTypeArguments - visitAnnotationMetadata(beanMethodWriter, beanFactoryMetadata) - beanMethodWriter.visitTypeArguments(allTypeArguments) - beanMethodWriter.visitBeanFactoryField( - originatingElement, - factoryField - ) - } - - if (hasAroundStereotype(methodAnnotationMetadata) && !producedClassElement.isAssignable(Interceptor.class)) { - - if (Modifier.isFinal(returnType.modifiers)) { - addError( - "Cannot apply AOP advice to final class. Class must be made non-final to support proxying: $annotatedNode", - annotatedNode - ) - return - } - MethodElement constructor = producedClassElement.getPrimaryConstructor().orElse(null) - if (!producedClassElement.isInterface() && constructor != null && constructor.getParameters().length > 0) { - final String proxyTargetMode = methodAnnotationMetadata.stringValue(AROUND_TYPE, "proxyTargetMode") - .orElseGet(() -> { - // temporary workaround until micronaut-test can be upgraded to 3.0 - if (methodAnnotationMetadata.hasAnnotation("io.micronaut.test.annotation.MockBean")) { - return "WARN"; - } else { - return "ERROR"; - } - }); - switch (proxyTargetMode) { - case "ALLOW": - allowProxyConstruction(constructor) - break - case "WARN": - allowProxyConstruction(constructor) - AstMessageUtils.warning(sourceUnit, annotatedNode, "The produced type of a @Factory method has constructor arguments and is proxied. This can lead to unexpected behaviour. See the javadoc for Around.ProxyTargetConstructorMode for more information.") - break - default: - addError("The produced type from a factory which has AOP proxy advice specified must define an accessible no arguments constructor. Proxying types with constructor arguments can lead to unexpected behaviour. See the javadoc for for Around.ProxyTargetConstructorMode for more information and possible solutions.", annotatedNode) - return - } - } - - AnnotationValue[] interceptorTypeReferences = InterceptedMethodUtil - .resolveInterceptorBinding(methodAnnotationMetadata, InterceptorKind.AROUND) - OptionalValues aopSettings = methodAnnotationMetadata.getValues(AROUND_TYPE, Boolean) - Map finalSettings = [:] - for (key in aopSettings) { - finalSettings.put(key, aopSettings.get(key).get()) - } - finalSettings.put(Interceptor.PROXY_TARGET, true) - - AopProxyWriter proxyWriter = new AopProxyWriter( - beanMethodWriter, - OptionalValues.of(Boolean.class, finalSettings), - configurationMetadataBuilder, - groovyVisitorContext, - interceptorTypeReferences - ) - proxyWriter.visitTypeArguments(allTypeArguments) - if (producedClassElement.isInterface()) { - proxyWriter.visitDefaultConstructor(AnnotationMetadata.EMPTY_METADATA, groovyVisitorContext) - } else { - populateProxyWriterConstructor(producedClassElement, proxyWriter, constructor) - } - SourceUnit source = this.sourceUnit - CompilationUnit unit = this.compilationUnit - ClassElement finalConcreteClassElement = this.concreteClassElement - new PublicMethodVisitor(source) { - @Override - void accept(ClassNode classNode, MethodNode targetBeanMethodNode) { - AnnotationMetadata annotationMetadata - if (AstAnnotationUtils.isAnnotated(producedClassElement.name, annotatedNode)) { - annotationMetadata = AstAnnotationUtils.newBuilder(source, unit) - .buildForParent(producedClassElement.name, annotatedNode, targetBeanMethodNode) - } else { - annotationMetadata = new AnnotationMetadataReference( - beanMethodWriter.getBeanDefinitionName() + BeanDefinitionReferenceWriter.REF_SUFFIX, - methodAnnotationMetadata - ) - } - MethodElement targetMethodElement = elementFactory.newMethodElement( - finalConcreteClassElement, - targetBeanMethodNode, - annotationMetadata - ) - - proxyWriter.visitAroundMethod( - targetMethodElement.declaringType, - targetMethodElement - ) - } - }.accept(returnType) - beanDefinitionWriters.put(new AnnotatedNode(), proxyWriter) - - } - Optional preDestroy = methodAnnotationMetadata.getValue(Bean, "preDestroy", String.class) - if (preDestroy.isPresent()) { - String destroyMethodName = preDestroy.get() - MethodNode destroyMethod - ClassNode producedClassNode = (ClassNode) producedClassElement.nativeType - SourceUnit source = this.sourceUnit - new PublicMethodVisitor(source) { - @Override - void accept(ClassNode classNode, MethodNode methodNode) { - destroyMethod = methodNode - } - @Override - protected boolean isAcceptable(MethodNode node) { - return node.name == destroyMethodName && node.parameters.length == 0 && node.isPublic() - } - }.accept(producedClassNode) - - if (destroyMethod != null) { - def destroyMethodElement = elementFactory.newMethodElement( - producedClassElement, - destroyMethod, - AnnotationMetadata.EMPTY_METADATA - ) - beanMethodWriter.visitPreDestroyMethod( - producedClassElement, - destroyMethodElement, - false, - groovyVisitorContext - ) - } else { - addError("@Bean method defines a preDestroy method that does not exist or is not public: $destroyMethodName", annotatedNode) - } - } - beanDefinitionWriters.put(annotatedNode, beanMethodWriter) - } - - private static void allowProxyConstruction(MethodElement constructor) { - final ParameterElement[] parameters = constructor.getParameters() - for (ParameterElement parameter : parameters) { - if (parameter.primitive && !parameter.array) { - final String name = parameter.getType().getName() - if ("boolean" == name) { - parameter.annotate(Value.class, (builder) -> builder.value(false)) - } else { - parameter.annotate(Value.class, (builder) -> builder.value(0)) - } - } else { - // allow null - parameter.annotate(AnnotationUtil.NULLABLE) - parameter.removeAnnotation(AnnotationUtil.NON_NULL) - } - } - } - - private static AnnotationMetadata addPropertyMetadata(Element element, PropertyMetadata propertyMetadata) { - element.annotate( - Property.class.getName(), - { builder -> - builder.member("name", propertyMetadata.path) - } - - ) - return element.annotationMetadata - } - - private void visitExecutableMethod( - ClassNode declaringClass, - MethodNode methodNode, - AnnotationMetadata methodAnnotationMetadata, - String methodName, boolean isPublic) { - if (declaringClass != ClassHelper.OBJECT_TYPE) { - - boolean isOwningClass = declaringClass == concreteClass - boolean isParent = declaringClass != concreteClass - - ClassElement declaringElement = elementFactory.newClassElement(declaringClass, concreteClassAnnotationMetadata) - def methodElement = elementFactory.newMethodElement(concreteClassElement, methodNode, methodAnnotationMetadata) - Parameter[] resolvedParameters = methodElement.parameters.collect { ParameterElement pe -> - if (pe.type.isPrimitive()) { - return (Parameter) pe.nativeType - } else { - return new Parameter((ClassNode) pe.genericType.nativeType, pe.name) - } - } as Parameter[] - - MethodNode overriddenMethod = isParent ? concreteClass.getMethod(methodName, resolvedParameters) : methodNode - if (!isOwningClass && overriddenMethod != null && overriddenMethod.declaringClass != declaringClass) { - return - } - - defineBeanDefinition(concreteClass) - - boolean preprocess = methodAnnotationMetadata.booleanValue(Executable.class, "processOnStartup").orElse(false) - if (preprocess) { - getBeanWriter().setRequiresMethodProcessing(true) - } - final boolean hasConstraints = methodElement.parameters.any { am -> - InjectTransform.IS_CONSTRAINT.test(am.annotationMetadata) - } - - if (hasConstraints) { - if (!methodAnnotationMetadata.hasStereotype("io.micronaut.validation.Validated")) { - methodAnnotationMetadata = addValidated(methodElement) - } - } - - boolean executorMethodAdded = false - - if (methodAnnotationMetadata.hasStereotype(Adapter.class)) { - visitAdaptedMethod(methodNode, methodAnnotationMetadata) - } - - boolean hasAround = hasConstraints || hasAroundStereotype(methodAnnotationMetadata) - if ((isAopProxyType && isPublic) || (hasAround && !concreteClass.isAbstract() && !concreteClassElement.isAssignable(Interceptor.class))) { - - boolean hasExplicitAround = hasDeclaredAroundStereotype(methodAnnotationMetadata) - - if (methodNode.isFinal()) { - if (hasExplicitAround) { - addError("Method defines AOP advice but is declared final. Change the method to be non-final in order for AOP advice to be applied.", methodNode) - } else { - addError("Public method inherits AOP advice but is declared final. Change the method to be non-final in order for AOP advice to be applied.", methodNode) - } - } else { - AnnotationValue[] interceptorTypeReferences = InterceptedMethodUtil - .resolveInterceptorBinding(methodAnnotationMetadata, InterceptorKind.AROUND) - OptionalValues aopSettings = methodAnnotationMetadata.getValues(AROUND_TYPE, Boolean) - AopProxyWriter proxyWriter = resolveProxyWriter( - aopSettings, - false, - interceptorTypeReferences - ) - - if (proxyWriter != null && !methodNode.isFinal()) { - - proxyWriter.visitInterceptorBinding(interceptorTypeReferences) - - proxyWriter.visitAroundMethod( - declaringElement, - methodElement - ) - - executorMethodAdded = true - } - } - } - - if (!executorMethodAdded) { - getBeanWriter().visitExecutableMethod( - declaringElement, - methodElement, - groovyVisitorContext - ) - } - } - } - - static boolean hasDeclaredAroundStereotype(AnnotationMetadata methodAnnotationMetadata) { - if (methodAnnotationMetadata.hasDeclaredStereotype(AROUND_TYPE)) { - return true - } else if (methodAnnotationMetadata.hasDeclaredStereotype(AnnotationUtil.ANN_INTERCEPTOR_BINDINGS)) { - methodAnnotationMetadata.getDeclaredAnnotationValuesByType(InterceptorBinding) - .stream().anyMatch { av -> - InterceptorKind.AROUND == av.enumValue("kind", InterceptorKind).orElse(null) - } - } - } - - @CompileDynamic - private AnnotationMetadata addValidated(Element element) { - return addAnnotation(element, "io.micronaut.validation.Validated") - } - - @CompileDynamic - private AnnotationMetadata addAnnotation(Element element, String annotationName) { - element.annotate(annotationName) - return element.annotationMetadata - } - - private AopProxyWriter resolveProxyWriter( - OptionalValues aopSettings, - boolean isFactoryType, - AnnotationValue[] interceptorTypeReferences) { - AopProxyWriter proxyWriter = (AopProxyWriter) aopProxyWriter - if (proxyWriter == null) { - if (getBeanWriter() instanceof BeanDefinitionWriter) { - proxyWriter = new AopProxyWriter( - (BeanDefinitionWriter) getBeanWriter(), - aopSettings, - configurationMetadataBuilder, - groovyVisitorContext, - interceptorTypeReferences - ) - } else { - // Unexpected: should be unreachable - throw new IllegalStateException("Internal Error: bean writer not an instance of BeanDefinitionWriter") - } - - populateProxyWriterConstructor(concreteClassElement, proxyWriter, concreteClassElement.primaryConstructor.orElse(null)) - String beanDefinitionName = getBeanWriter().getBeanDefinitionName() - if (isFactoryType) { - proxyWriter.visitSuperBeanDefinitionFactory(beanDefinitionName) - } else { - proxyWriter.visitSuperBeanDefinition(beanDefinitionName) - } - - this.aopProxyWriter = proxyWriter - - beanDefinitionWriters.put(new AnnotatedNode(), proxyWriter) - } - proxyWriter - } - - protected void populateProxyWriterConstructor(ClassElement targetClass, AopProxyWriter proxyWriter, MethodElement constructor) { - if (constructor != null) { - if (constructor.parameters.length == 0) { - proxyWriter.visitDefaultConstructor( - AnnotationMetadata.EMPTY_METADATA, - groovyVisitorContext - ) - } else { - proxyWriter.visitBeanDefinitionConstructor( - constructor, - constructor.isPrivate(), - groovyVisitorContext - ) - } - } else { - ClassNode cn = targetClass.nativeType as ClassNode - if (cn.declaredConstructors.isEmpty()) { - proxyWriter.visitDefaultConstructor(AnnotationMetadata.EMPTY_METADATA, groovyVisitorContext) - } else { - addError("Class must have at least one non private constructor in order to be a candidate for dependency injection", (ASTNode) targetClass.nativeType) - } - } - } - - protected static boolean isPackagePrivate(AnnotatedNode annotatedNode, int modifiers) { - return ((!Modifier.isProtected(modifiers) && !Modifier.isPublic(modifiers) && !Modifier.isPrivate(modifiers)) || !annotatedNode.getAnnotations(makeCached(PackageScope)).isEmpty()) - } - - @Override - void visitField(FieldNode fieldNode) { - if (fieldNode.name == 'metaClass') return - int modifiers = fieldNode.modifiers - if (Modifier.isStatic(modifiers)) { - return - } - if (fieldNode.isSynthetic() && !isPackagePrivate(fieldNode, fieldNode.modifiers)) { - return - } - ClassNode declaringClass = fieldNode.declaringClass - AnnotationMetadata fieldAnnotationMetadata = AstAnnotationUtils.getAnnotationMetadata(sourceUnit, compilationUnit, fieldNode) - if (Modifier.isFinal(modifiers) && !fieldAnnotationMetadata.hasStereotype(ConfigurationBuilder)) { - if (isFactoryClass && fieldAnnotationMetadata.hasDeclaredStereotype(Bean.class)) { - // field factory for bean - if (fieldNode.isPrivate() || fieldNode.isProtected()) { - AstMessageUtils.error(sourceUnit, fieldNode, "Beans produced from fields cannot be private or protected visibility") - } else { - visitBeanFactoryElement( - concreteClass, - fieldNode, - fieldAnnotationMetadata, - fieldNode.name - ) - } - } - return - } else if (isFactoryClass && fieldAnnotationMetadata.hasDeclaredStereotype(Bean.class)) { - // field factory for bean - if (fieldNode.isPrivate() || fieldNode.isProtected()) { - AstMessageUtils.error(sourceUnit, fieldNode, "Beans produced from fields cannot be private or protected visibility") - } - return - } - boolean isInject = isFieldInjected(fieldNode, fieldAnnotationMetadata) - boolean isValue = isValueInjection(fieldNode, fieldAnnotationMetadata) - FieldElement fieldElement = elementFactory.newFieldElement(fieldNode, fieldAnnotationMetadata) - - if ((isInject || isValue) && declaringClass.getProperty(fieldNode.getName()) == null) { - defineBeanDefinition(concreteClass) - if (!fieldNode.isStatic()) { - - boolean isPrivate = Modifier.isPrivate(modifiers) - boolean requiresReflection = isPrivate || isInheritedAndNotPublic(fieldNode, fieldNode.declaringClass, modifiers) - if (!getBeanWriter().isValidated()) { - getBeanWriter().setValidated(InjectTransform.IS_CONSTRAINT.test(fieldAnnotationMetadata)) - } - String fieldName = fieldNode.name - ClassElement fieldType = fieldElement.type - if (isValue) { - if (isConfigurationProperties && fieldAnnotationMetadata.hasStereotype(ConfigurationBuilder.class)) { - if(requiresReflection) { - // Using the field would throw a IllegalAccessError, use the method instead - String fieldGetterName = NameUtils.getterNameFor(fieldName) - MethodNode getterMethod = declaringClass.methods?.find { it.name == fieldGetterName} - if(getterMethod != null) { - getBeanWriter().visitConfigBuilderMethod( - fieldType, - getterMethod.name, - fieldAnnotationMetadata, - configurationMetadataBuilder, - fieldType.interface - ) - } else { - addError("ConfigurationBuilder applied to a non accessible (private or package-private/protected in a different package) field must have a corresponding non-private getter method.", fieldNode) - } - } else { - getBeanWriter().visitConfigBuilderField(fieldType, fieldName, fieldAnnotationMetadata, configurationMetadataBuilder, fieldNode.type.interface) - } - try { - visitConfigurationBuilder( - fieldElement.declaringType, - fieldAnnotationMetadata, - fieldElement.type, getBeanWriter() - ) - } finally { - getBeanWriter().visitConfigBuilderEnd() - } - } else { - if (isConfigurationProperties) { - if (shouldExclude(configurationMetadata, fieldName)) { - return - } - PropertyMetadata propertyMetadata = configurationMetadataBuilder.visitProperty( - concreteClass, - declaringClass, - fieldNode.type.name, - fieldName, - null, // TODO: fix groovy doc support - null - ) - fieldElement.annotate(Property.class.getName(), {builder -> - builder.member("name", propertyMetadata.path) - }) - } - getBeanWriter().visitFieldValue( - fieldElement.declaringType, - fieldElement, - requiresReflection, - isConfigurationProperties - ) - } - } else { - getBeanWriter().visitFieldInjectionPoint( - fieldElement.declaringType, - fieldElement, - requiresReflection - ) - } - } - } - } - - @Override - @CompileDynamic - void visitProperty(PropertyNode propertyNode) { - FieldNode fieldNode = propertyNode.field - if (fieldNode.name == 'metaClass') return - def modifiers = propertyNode.getModifiers() - AnnotationMetadata fieldAnnotationMetadata = AstAnnotationUtils.getAnnotationMetadata(sourceUnit, compilationUnit, fieldNode) - if (Modifier.isFinal(modifiers) && !fieldAnnotationMetadata.hasStereotype(ConfigurationBuilder)) { - if (isFactoryClass && fieldAnnotationMetadata.hasDeclaredStereotype(Bean.class)) { - // field factory for bean - if (propertyNode.isPrivate()) { - AstMessageUtils.error(sourceUnit, propertyNode, "Beans produced from fields cannot be private") - } else { - visitFactoryProperty(propertyNode, fieldNode, fieldAnnotationMetadata) - - } - } - return - } - boolean isInject = isFieldInjected(fieldNode, fieldAnnotationMetadata) - boolean isValue = isValueInjection(fieldNode, fieldAnnotationMetadata) - - String propertyName = propertyNode.name - if (!propertyNode.isStatic() && (isInject || isValue)) { - defineBeanDefinition(concreteClass) - FieldElement fieldElement = elementFactory.newFieldElement( - fieldNode, - fieldAnnotationMetadata - ) - - if (!getBeanWriter().isValidated()) { - getBeanWriter().setValidated(InjectTransform.IS_CONSTRAINT.test(fieldAnnotationMetadata)) - } - - if (isInject) { - ParameterElement parameterElement = elementFactory.newParameterElement(fieldElement, fieldAnnotationMetadata) - MethodElement methodElement = MethodElement.of( - fieldElement.declaringType, - fieldElement, - PrimitiveElement.VOID, - PrimitiveElement.VOID, - getSetterName(propertyName), - parameterElement - ) - getBeanWriter().visitMethodInjectionPoint( - fieldElement.declaringType, - methodElement, - false, - groovyVisitorContext - ) - } else if (isValue) { - if (isConfigurationProperties && fieldAnnotationMetadata.hasStereotype(ConfigurationBuilder.class)) { - getBeanWriter().visitConfigBuilderMethod( - fieldElement.type, - getGetterName(propertyNode), - fieldAnnotationMetadata, - configurationMetadataBuilder, - fieldNode.type.interface) - try { - visitConfigurationBuilder( - fieldElement.declaringType, - fieldAnnotationMetadata, - fieldElement.type, - getBeanWriter() - ) - } finally { - getBeanWriter().visitConfigBuilderEnd() - } - } else { - if (isConfigurationProperties) { - if (shouldExclude(configurationMetadata, propertyName)) { - return - } - PropertyMetadata propertyMetadata = configurationMetadataBuilder.visitProperty( - concreteClass, - fieldNode.declaringClass, - propertyNode.type.name, - propertyNode.name, - null, // TODO: fix groovy doc support - null - ) - fieldElement.annotate(Property.class.getName(), { builder -> - builder.member("name", propertyMetadata.path) - }) - fieldAnnotationMetadata = fieldElement.annotationMetadata - } - def setterName = GeneralUtils.getSetterName(propertyName) - - ParameterElement parameterElement = elementFactory.newParameterElement(fieldElement, fieldAnnotationMetadata) - def methodElement = MethodElement.of( - fieldElement.declaringType, - fieldAnnotationMetadata, - PrimitiveElement.VOID, - PrimitiveElement.VOID, - setterName, - parameterElement - ) - getBeanWriter().visitSetterValue( - fieldElement.declaringType, - methodElement, - false, - isConfigurationProperties - ) - } - } - } else if (isAopProxyType && !propertyNode.isStatic()) { - AopProxyWriter aopWriter = (AopProxyWriter) aopProxyWriter - if (aopProxyWriter != null) { - AnnotationMetadata fieldMetadata = AstAnnotationUtils.getAnnotationMetadata(sourceUnit, compilationUnit, propertyNode.field) - FieldElement fieldElement = elementFactory.newFieldElement( - fieldNode, - fieldMetadata - ) - ParameterElement parameterElement = elementFactory.newParameterElement(fieldElement, fieldAnnotationMetadata) - def methodAnnotationMetadata = new AnnotationMetadataHierarchy( - concreteClassAnnotationMetadata, - fieldAnnotationMetadata - ) - MethodElement setterElement = MethodElement.of( - fieldElement.declaringType, - methodAnnotationMetadata, - PrimitiveElement.VOID, - PrimitiveElement.VOID, - getSetterName(propertyName), - parameterElement - ) - aopWriter.visitAroundMethod( - fieldElement.declaringType, - setterElement - ) - - // also visit getter to ensure proxying - MethodElement getterElement = MethodElement.of( - fieldElement.declaringType, - methodAnnotationMetadata, - fieldElement.type, - fieldElement.genericType, - getGetterName(propertyNode) - ) - aopWriter.visitAroundMethod( - fieldElement.declaringType, - getterElement - ) - } - } else if (isFactoryClass && fieldAnnotationMetadata.hasDeclaredStereotype(Bean.class)) { - // field factory for bean - if (propertyNode.isPrivate()) { - AstMessageUtils.error(sourceUnit, propertyNode, "Beans produced from fields cannot be private"); - } else { - visitFactoryProperty(propertyNode, fieldNode, fieldAnnotationMetadata) - } - } - } - - private boolean isFieldInjected(FieldNode fieldNode, AnnotationMetadata fieldAnnotationMetadata) { - fieldNode != null && (fieldAnnotationMetadata.hasStereotype(AnnotationUtil.INJECT) || (fieldAnnotationMetadata.hasDeclaredStereotype(AnnotationUtil.QUALIFIER)) && !fieldAnnotationMetadata.hasDeclaredAnnotation(Bean)) - } - - private void visitFactoryProperty(PropertyNode propertyNode, FieldNode fieldNode, AnnotationMetadata fieldAnnotationMetadata) { - - def modifiers = propertyNode.isStatic() ? Modifier.STATIC | Modifier.PUBLIC : Modifier.PUBLIC - def getterNode = new MethodNode( - getGetterName(propertyNode), - modifiers, - fieldNode.type, - new Parameter[0], - null, - null - ) - getterNode.declaringClass = concreteClass - visitBeanFactoryElement( - concreteClass, - getterNode, - fieldAnnotationMetadata, - getterNode.name - ) - } - - private boolean isValueInjection(FieldNode fieldNode, AnnotationMetadata fieldAnnotationMetadata) { - fieldNode != null && ( - fieldAnnotationMetadata.hasStereotype(Value) || - fieldAnnotationMetadata.hasStereotype(Property) || - isConfigurationProperties - ) - } - - protected boolean isInheritedAndNotPublic(AnnotatedNode annotatedNode, ClassNode declaringClass, int modifiers) { - return declaringClass != concreteClass && - declaringClass.packageName != concreteClass.packageName && - ((Modifier.isProtected(modifiers) || !Modifier.isPublic(modifiers)) || !annotatedNode.getAnnotations(makeCached(PackageScope)).isEmpty()) - } - - @Override - protected SourceUnit getSourceUnit() { - return sourceUnit - } - - private void defineBeanDefinition(ClassNode classNode) { - if (!beanDefinitionWriters.containsKey(classNode)) { - if (classNode.packageName == null) { - addError("Micronaut beans cannot be in the default package", classNode) - return - } - AnnotationMetadata annotationMetadata = AstAnnotationUtils.getAnnotationMetadata(sourceUnit, compilationUnit, classNode) - if (configurationMetadata != null) { - String existingPrefix = annotationMetadata.getValue( - ConfigurationReader.class, - "prefix", String.class) - .orElse("") - - def computedPrefix = StringUtils.isNotEmpty(existingPrefix) ? existingPrefix + "." + configurationMetadata.getName() : configurationMetadata.getName() - annotationMetadata = DefaultAnnotationMetadata.mutateMember( - annotationMetadata, - ConfigurationReader.class.getName(), - "prefix", - computedPrefix - ) - } - - ClassElement groovyClassElement = elementFactory.newClassElement( - classNode, - annotationMetadata - ) - - if (annotationMetadata.hasStereotype(Singleton)) { - addError("Class annotated with groovy.lang.Singleton instead of jakarta.inject.Singleton. Import jakarta.inject.Singleton to use Micronaut Dependency Injection.", classNode) - } - - beanWriter = new BeanDefinitionWriter(groovyClassElement, configurationMetadataBuilder, groovyVisitorContext) - beanWriter.visitTypeArguments(groovyClassElement.allTypeArguments) - beanDefinitionWriters.put(classNode, beanWriter) - visitAnnotationMetadata(beanWriter, annotationMetadata) - - MethodElement constructor = groovyClassElement.getPrimaryConstructor().orElse(null) - - if (constructor != null) { - if (constructor.parameters.length == 0) { - - beanWriter.visitDefaultConstructor(AnnotationMetadata.EMPTY_METADATA, groovyVisitorContext) - } else { - def constructorMetadata = constructor.annotationMetadata - final boolean isConstructBinding = constructorMetadata.hasDeclaredStereotype(ConfigurationInject.class) - if (isConstructBinding) { - this.configurationMetadata = configurationMetadataBuilder.visitProperties( - concreteClass, - null) - } - beanWriter.visitBeanDefinitionConstructor(constructor, constructor.isPrivate(), groovyVisitorContext) - } - - } else { - ClassNode cn = groovyClassElement.nativeType as ClassNode - if (cn.declaredConstructors.isEmpty()) { - beanWriter.visitDefaultConstructor(AnnotationMetadata.EMPTY_METADATA, groovyVisitorContext) - } else { - addError("Class must have at least one non private constructor in order to be a candidate for dependency injection", classNode) - } - } - } else { - beanWriter = beanDefinitionWriters.get(classNode) - } - } - - @CompileDynamic - private void visitAdaptedMethod(MethodNode method, AnnotationMetadata methodAnnotationMetadata) { - if (methodAnnotationMetadata instanceof AnnotationMetadataHierarchy) { - methodAnnotationMetadata = ((AnnotationMetadataHierarchy) methodAnnotationMetadata).getDeclaredMetadata(); - } - Optional adaptedType = methodAnnotationMetadata.getValue(Adapter.class, String.class).flatMap({ String s -> - ClassNode cn = sourceUnit.AST.classes.find { ClassNode cn -> cn.name == s } - if (cn != null) { - return Optional.of(cn) - } - def type = ClassUtils.forName(s, InjectTransform.classLoader).orElse(null) - if (type != null) { - return Optional.of(ClassHelper.make(type)) - } - return Optional.empty() - } as Function>) - - if (adaptedType.isPresent()) { - ClassNode typeToImplement = adaptedType.get() - boolean isInterface = typeToImplement.isInterface() - if (isInterface) { - - String packageName = concreteClass.packageName - String declaringClassSimpleName = concreteClass.nameWithoutPackage - String beanClassName = generateAdaptedMethodClassName(declaringClassSimpleName, typeToImplement, method) - - AopProxyWriter aopProxyWriter = new AopProxyWriter( - packageName, - beanClassName, - true, - false, - originatingElement, - new AnnotationMetadataHierarchy(concreteClassAnnotationMetadata, methodAnnotationMetadata), - [elementFactory.newClassElement(typeToImplement, AnnotationMetadata.EMPTY_METADATA)] as ClassElement[], - groovyVisitorContext, - configurationMetadataBuilder, - null - ) - - aopProxyWriter.visitDefaultConstructor(methodAnnotationMetadata, groovyVisitorContext) - - beanDefinitionWriters.put(ClassHelper.make(packageName + '.' + beanClassName), aopProxyWriter) - - ClassElement typeToImplementElement = elementFactory.newClassElement( - typeToImplement, - methodAnnotationMetadata - ) - Map typeVariables = typeToImplementElement.getTypeArguments(); - - InjectVisitor thisVisitor = this - SourceUnit source = this.sourceUnit - CompilationUnit unit = this.compilationUnit - MethodElement sourceMethod = elementFactory.newMethodElement( - concreteClassElement, - method, - methodAnnotationMetadata - ) - PublicAbstractMethodVisitor visitor = new PublicAbstractMethodVisitor(source, unit) { - boolean first = true - - @Override - void accept(ClassNode classNode, MethodNode targetMethod) { - if (!first) { - thisVisitor.addError("Interface to adapt [" + typeToImplement + "] is not a SAM type. More than one abstract method declared.", (MethodNode)method) - return - } - first = false - MethodElement targetMethodElement = elementFactory.newMethodElement( - typeToImplementElement, - targetMethod, - AstAnnotationUtils.getAnnotationMetadata(sourceUnit, compilationUnit, targetMethod) - ) - ParameterElement[] sourceParams = sourceMethod.getParameters(); - ParameterElement[] targetParams = targetMethodElement.getParameters(); - Parameter[] targetParameters = targetMethod.getParameters() - if (targetParameters.size() == sourceParams.size()) { - - int i = 0 - Map genericTypes = [:] - for (Parameter targetElement in targetParameters) { - - ParameterElement sourceElement = sourceParams[i] - - ClassElement targetType = targetParams[i].getType() - ClassElement sourceType = sourceElement.getType() - - if (targetElement.type.isGenericsPlaceHolder()) { - GenericsType[] targetGenerics = targetElement.type.genericsTypes - - if (targetGenerics) { - String variableName = targetGenerics[0].name - if (typeVariables.containsKey(variableName)) { - targetType = typeVariables.get(variableName) - - genericTypes.put(variableName, sourceType) - } - } - } - - if (!sourceType.isAssignable(targetType.getName())) { - thisVisitor.addError("Cannot adapt method [${method.declaringClass.name}.$method.name(..)] to target method [${targetMethod.declaringClass.name}.$targetMethod.name(..)]. Argument type [" + sourceType.name + "] is not a subtype of type [$targetType.name] at position $i.", (MethodNode)method) - return - } - - i++ - } - - if (!genericTypes.isEmpty()) { - Map> typeData = Collections.>singletonMap( - typeToImplement.name, - genericTypes - ) - aopProxyWriter.visitTypeArguments( - typeData - ) - } - - String qualifier = concreteClassAnnotationMetadata.getValue(AnnotationUtil.NAMED, String.class).orElse(null) - MethodElement groovyMethodElement = elementFactory.newMethodElement( - concreteClassElement, - targetMethod, - methodAnnotationMetadata - ) - - AnnotationClassValue[] adaptedArgumentTypes = new AnnotationClassValue[sourceParams.length] - int j = 0 - for (ParameterElement ve in sourceMethod.parameters) { - adaptedArgumentTypes[j] = new AnnotationClassValue(ve.type.name) - j++ - } - groovyMethodElement.annotate(Adapter.class, { builder -> - builder.member(Adapter.InternalAttributes.ADAPTED_BEAN, new AnnotationClassValue<>(concreteClass.name)) - builder.member(Adapter.InternalAttributes.ADAPTED_METHOD, method.name) - builder.member(Adapter.InternalAttributes.ADAPTED_ARGUMENT_TYPES, adaptedArgumentTypes) - if (StringUtils.isNotEmpty(qualifier)) { - builder.member(Adapter.InternalAttributes.ADAPTED_QUALIFIER, qualifier) - } - }) - - ClassElement declaringElement = elementFactory.newClassElement( - targetMethod.declaringClass, - AnnotationMetadata.EMPTY_METADATA - ) - aopProxyWriter.visitAroundMethod( - declaringElement, - groovyMethodElement - ) - - - } else { - thisVisitor.addError( - "Cannot adapt method [${method.declaringClass.name}.$method.name(..)] to target method [${targetMethod.declaringClass.name}.$targetMethod.name(..)]. Argument lengths don't match.", - (MethodNode) method - ) - } - } - } - - visitor.accept(typeToImplement) - } - - } - } - - private String generateAdaptedMethodClassName(String declaringClassSimpleName, ClassNode typeToImplement, MethodNode method) { - String rootName = declaringClassSimpleName + '$' + typeToImplement.nameWithoutPackage + '$' + method.getName() - return rootName + adaptedMethodIndex.incrementAndGet() - } - - private void visitConfigurationBuilder(ClassElement declaringClass, - AnnotationMetadata annotationMetadata, - ClassElement classNode, - BeanDefinitionVisitor writer) { - Boolean allowZeroArgs = annotationMetadata.getValue(ConfigurationBuilder.class, "allowZeroArgs", Boolean.class).orElse(false) - List prefixes = Arrays.asList(annotationMetadata.getValue(AccessorsStyle.class, "writePrefixes", String[].class).orElse(["set"] as String[])) - String configurationPrefix = annotationMetadata.getValue(ConfigurationBuilder.class, String.class) - .map({ value -> value + "."}).orElse("") - Set includes = annotationMetadata.getValue(ConfigurationBuilder.class, "includes", Set.class).orElse(Collections.emptySet()) - Set excludes = annotationMetadata.getValue(ConfigurationBuilder.class, "excludes", Set.class).orElse(Collections.emptySet()) - - SourceUnit source = this.sourceUnit - CompilationUnit compilationUnit = this.compilationUnit - ClassElement concreteClassElement = this.concreteClassElement - PublicMethodVisitor visitor = new PublicMethodVisitor(source) { - @Override - void accept(ClassNode cn, MethodNode method) { - String name = method.getName() - String prefix = getMethodPrefix(name) - String propertyName = NameUtils.decapitalize(name.substring(prefix.length())) - if (shouldExclude(includes, excludes, propertyName)) { - return - } - MethodElement groovyMethodElement = elementFactory.newMethodElement( - concreteClassElement, - method, - AstAnnotationUtils.getAnnotationMetadata(source, compilationUnit, method) - ) - ParameterElement[] params = groovyMethodElement.parameters - int paramCount = params.size() - if (paramCount < 2) { - ParameterElement paramType = params.size() == 1 ? params[0] : null - - PropertyMetadata metadata = configurationMetadataBuilder.visitProperty( - concreteClassElement.nativeType as ClassNode, - declaringClass.nativeType as ClassNode, - paramType?.type?.name, - configurationPrefix + propertyName, - null, - null - ) - - writer.visitConfigBuilderMethod( - prefix, - groovyMethodElement.returnType, - name, - paramType?.type, - paramType?.type?.typeArguments, - metadata.path - ) - - } else if (paramCount == 2) { - // check the params are a long and a TimeUnit - ParameterElement first = params[0] - ParameterElement second = params[1] - - PropertyMetadata metadata = configurationMetadataBuilder.visitProperty( - concreteClassElement.nativeType as ClassNode, - declaringClass.nativeType as ClassNode, - Duration.class.name, - configurationPrefix + propertyName, - null, - null - ) - - if (second.type.name == TimeUnit.class.name && first.type.name == "long") { - writer.visitConfigBuilderDurationMethod( - prefix, - groovyMethodElement.returnType, - name, - metadata.path - ) - } - } - } - - @Override - protected boolean isAcceptable(MethodNode node) { - // ignore deprecated methods - if (AstAnnotationUtils.hasStereotype(source, compilationUnit, node, Deprecated.class)) { - return false - } - int paramCount = node.getParameters().size() - ((paramCount > 0 && paramCount < 3) || (allowZeroArgs && paramCount == 0)) && - super.isAcceptable(node) && - isPrefixedWith(node.getName()) - } - - private boolean isPrefixedWith(String name) { - for (String prefix : prefixes) { - if (name.startsWith(prefix)) return true - } - return false - } - - private String getMethodPrefix(String methodName) { - for (String prefix : prefixes) { - if (methodName.startsWith(prefix)) { - return prefix - } - } - return methodName - } - } - - visitor.accept(classNode.nativeType as ClassNode) - } - - private boolean shouldExclude(Set includes, Set excludes, String propertyName) { - if (!includes.isEmpty() && !includes.contains(propertyName)) { - return true - } - if (!excludes.isEmpty() && excludes.contains(propertyName)) { - return true - } - return false - } - - private boolean shouldExclude(ConfigurationMetadata configurationMetadata, String propertyName) { - return shouldExclude(configurationMetadata.getIncludes(), configurationMetadata.getExcludes(), propertyName) - } -} diff --git a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/TypeElementVisitorEnd.groovy b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/TypeElementVisitorEnd.groovy index cca3f6957b6..25953bdc5a9 100644 --- a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/TypeElementVisitorEnd.groovy +++ b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/TypeElementVisitorEnd.groovy @@ -17,19 +17,14 @@ package io.micronaut.ast.groovy import groovy.transform.CompilationUnitAware import groovy.transform.CompileStatic -import io.micronaut.ast.groovy.utils.AstAnnotationUtils import io.micronaut.ast.groovy.utils.AstMessageUtils import io.micronaut.ast.groovy.utils.InMemoryByteCodeGroovyClassLoader import io.micronaut.ast.groovy.utils.InMemoryClassWriterOutputVisitor import io.micronaut.ast.groovy.visitor.GroovyVisitorContext import io.micronaut.ast.groovy.visitor.LoadedVisitor import io.micronaut.core.order.OrderUtil -import io.micronaut.core.util.CollectionUtils import io.micronaut.inject.annotation.AbstractAnnotationMetadataBuilder -import io.micronaut.inject.visitor.VisitorContext import io.micronaut.inject.writer.AbstractBeanDefinitionBuilder -import io.micronaut.inject.writer.BeanDefinitionReferenceWriter -import io.micronaut.inject.writer.BeanDefinitionWriter import io.micronaut.inject.writer.ClassWriterOutputVisitor import io.micronaut.inject.writer.DirectoryClassWriterOutputVisitor import org.codehaus.groovy.ast.ASTNode @@ -38,7 +33,6 @@ import org.codehaus.groovy.control.CompilePhase import org.codehaus.groovy.control.SourceUnit import org.codehaus.groovy.transform.ASTTransformation import org.codehaus.groovy.transform.GroovyASTTransformation - /** * Finishes the type element visitors. * @@ -104,7 +98,6 @@ class TypeElementVisitorEnd implements ASTTransformation, CompilationUnitAware { TypeElementVisitorTransform.loadedVisitors.remove() TypeElementVisitorTransform.beanDefinitionBuilders.remove() - AstAnnotationUtils.invalidateCache() AbstractAnnotationMetadataBuilder.clearMutated() } diff --git a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/TypeElementVisitorTransform.groovy b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/TypeElementVisitorTransform.groovy index fa07271a7e8..6dbfad35078 100644 --- a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/TypeElementVisitorTransform.groovy +++ b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/TypeElementVisitorTransform.groovy @@ -18,33 +18,24 @@ package io.micronaut.ast.groovy import groovy.transform.CompilationUnitAware import groovy.transform.CompileStatic import groovy.transform.PackageScope -import io.micronaut.aop.Introduction -import io.micronaut.ast.groovy.utils.AstAnnotationUtils -import io.micronaut.ast.groovy.utils.AstMessageUtils -import io.micronaut.ast.groovy.utils.PublicAbstractMethodVisitor -import io.micronaut.ast.groovy.utils.PublicMethodVisitor +import io.micronaut.ast.groovy.visitor.GroovyClassElement import io.micronaut.ast.groovy.visitor.GroovyVisitorContext import io.micronaut.ast.groovy.visitor.LoadedVisitor -import io.micronaut.core.annotation.AnnotationMetadata import io.micronaut.core.annotation.Generated -import io.micronaut.core.annotation.Introspected -import io.micronaut.core.io.service.ServiceDefinition -import io.micronaut.core.io.service.SoftServiceLoader import io.micronaut.core.order.OrderUtil -import io.micronaut.inject.annotation.AnnotationMetadataHierarchy -import io.micronaut.inject.ast.Element -import io.micronaut.inject.visitor.TypeElementVisitor +import io.micronaut.inject.processing.ProcessingException +import io.micronaut.inject.ast.ClassElement +import io.micronaut.inject.ast.ElementQuery +import io.micronaut.inject.ast.MethodElement +import io.micronaut.inject.ast.PropertyElement import io.micronaut.inject.writer.AbstractBeanDefinitionBuilder import org.codehaus.groovy.ast.ASTNode import org.codehaus.groovy.ast.AnnotatedNode -import org.codehaus.groovy.ast.ClassCodeVisitorSupport -import org.codehaus.groovy.ast.ClassHelper import org.codehaus.groovy.ast.ClassNode +import org.codehaus.groovy.ast.ConstructorNode import org.codehaus.groovy.ast.FieldNode import org.codehaus.groovy.ast.InnerClassNode -import org.codehaus.groovy.ast.MethodNode import org.codehaus.groovy.ast.ModuleNode -import org.codehaus.groovy.ast.PropertyNode import org.codehaus.groovy.control.CompilationUnit import org.codehaus.groovy.control.CompilePhase import org.codehaus.groovy.control.SourceUnit @@ -54,7 +45,6 @@ import org.codehaus.groovy.transform.GroovyASTTransformation import java.lang.reflect.Modifier import static org.codehaus.groovy.ast.ClassHelper.makeCached - /** * Executes type element visitors. * @@ -80,31 +70,34 @@ class TypeElementVisitorTransform implements ASTTransformation, CompilationUnitA if (visitors == null) return GroovyVisitorContext visitorContext = new GroovyVisitorContext(source, compilationUnit) - for (ClassNode classNode in classes) { - if (!(classNode instanceof InnerClassNode && !Modifier.isStatic(classNode.getModifiers())) && classNode.getAnnotations(generatedNode).empty) { - Collection matchedVisitors = visitors.values().findAll { v -> - v.matches(classNode) - } - List values = new ArrayList<>(matchedVisitors) - OrderUtil.reverseSort(values) - def annotationMetadata = AstAnnotationUtils.getAnnotationMetadata(source, compilationUnit, classNode) - def isIntroduction = annotationMetadata.hasStereotype(Introduction.class) - def visitor = new ElementVisitor(source, compilationUnit, classNode, values, visitorContext, !isIntroduction) - if (isIntroduction || (annotationMetadata.hasStereotype(Introspected.class) && classNode.isAbstract())) { - visitor.visitClass(classNode) - new PublicMethodVisitor(source) { - @Override - void accept(ClassNode cn, MethodNode methodNode) { - visitor.doVisitMethod(methodNode) - } - }.accept(classNode) - } else { - visitor.visitClass(classNode) + List sortedVisitors = new ArrayList<>(visitors.values()) + OrderUtil.reverseSort(sortedVisitors) + + // The visitor X with a higher priority should process elements of A before + // the visitor Y which is processing elements of B but also using elements A + + // Micronaut Data use-case: EntityMapper with a higher priority needs to process entities first + // before RepositoryMapper is going to process repositories and read entities + + for (LoadedVisitor loadedVisitor : sortedVisitors) { + for (ClassNode classNode in classes) { + if (!(classNode instanceof InnerClassNode && !Modifier.isStatic(classNode.getModifiers())) && classNode.getAnnotations(generatedNode).empty) { + ClassElement targetClassElement = visitorContext.getElementFactory().newSourceClassElement(classNode, visitorContext.getElementAnnotationMetadataFactory()) + if (!loadedVisitor.matchesClass(targetClassElement)) { + continue + } + try { + def visitor = new ElementVisitor(source, compilationUnit, classNode, [loadedVisitor], visitorContext, targetClassElement) + visitor.visitClass(classNode) + } catch (ProcessingException ex) { + visitorContext.fail(ex.getMessage(), ex.getOriginatingElement() as ASTNode) + } } } } + loadedVisitors.set(visitors) beanDefinitionBuilders.get().addAll(visitorContext.getBeanElementBuilders()) } @@ -113,85 +106,82 @@ class TypeElementVisitorTransform implements ASTTransformation, CompilationUnitA this.compilationUnit = unit } - private static class ElementVisitor extends ClassCodeVisitorSupport { + @CompileStatic + private static class ElementVisitor { final SourceUnit sourceUnit final CompilationUnit compilationUnit - final AnnotationMetadata annotationMetadata final GroovyVisitorContext visitorContext - final boolean visitMethods private final ClassNode concreteClass private final Collection typeElementVisitors + private ClassElement targetClassElement + ElementVisitor( SourceUnit sourceUnit, CompilationUnit compilationUnit, ClassNode targetClassNode, Collection typeElementVisitors, GroovyVisitorContext visitorContext, - boolean visitMethods = true) { + ClassElement targetClassElement) { + this.targetClassElement = targetClassElement this.compilationUnit = compilationUnit this.typeElementVisitors = typeElementVisitors this.concreteClass = targetClassNode this.sourceUnit = sourceUnit - this.annotationMetadata = AstAnnotationUtils.getAnnotationMetadata(sourceUnit, compilationUnit, targetClassNode) this.visitorContext = visitorContext - this.visitMethods = visitMethods } protected boolean isPackagePrivate(AnnotatedNode annotatedNode, int modifiers) { return ((!Modifier.isProtected(modifiers) && !Modifier.isPublic(modifiers) && !Modifier.isPrivate(modifiers)) || !annotatedNode.getAnnotations(makeCached(PackageScope)).isEmpty()) } - @Override void visitClass(ClassNode node) { - AnnotationMetadata annotationMetadata = AstAnnotationUtils.getAnnotationMetadata(sourceUnit, compilationUnit, node) - typeElementVisitors.each { - def element = it.visit(node, annotationMetadata, visitorContext) - if (element != null) { - annotationMetadata = element.annotationMetadata + if (targetClassElement.getNativeType() != node) { + targetClassElement = visitorContext.getElementFactory().newSourceClassElement(node, visitorContext.getElementAnnotationMetadataFactory()) + } + for (LoadedVisitor it : typeElementVisitors) { + if (it.matchesClass(targetClassElement)) { + it.getVisitor().visitClass(targetClassElement, visitorContext) } } + GroovyClassElement classElement = targetClassElement as GroovyClassElement + // Pre cache methods because of their source flag + def methods = classElement.getSourceEnclosedElements(ElementQuery.ALL_METHODS) - ClassNode superClass = node.getSuperClass() - List superClasses = [] - while (superClass != null) { - superClasses.add(superClass) - superClass = superClass.getSuperClass() + def properties = classElement.getSyntheticBeanProperties() + for (PropertyElement pn : (properties)) { + visitNativeProperty(pn) } - superClasses = superClasses.reverse() - for (classNode in superClasses) { - if (classNode.name != ClassHelper.OBJECT_TYPE.name && classNode.name != GroovyObjectSupport.name && classNode.name != Script.name) { - classNode.visitContents(this) - } + for (FieldNode fn : node.getFields()) { + visitField(fn) + } + for (ConstructorNode cn : node.getDeclaredConstructors()) { + visitConstructor(cn) + } + for (MethodElement methodElement : methods) { + visitMethod(methodElement) } - super.visitClass(node) } - @Override - protected void visitConstructorOrMethod(MethodNode methodNode, boolean isConstructor) { - if (visitMethods) { - doVisitMethod(methodNode) + void visitConstructor(ConstructorNode node) { + def e = visitorContext.getElementFactory() + .newConstructorElement(targetClassElement, node, visitorContext.getElementAnnotationMetadataFactory()) + for (LoadedVisitor it : typeElementVisitors) { + if (it.matchesElement(e)) { + it.getVisitor().visitConstructor(e, visitorContext) + } } } - void doVisitMethod(MethodNode methodNode) { - AnnotationMetadata methodAnnotationMetadata = AstAnnotationUtils.getMethodAnnotationMetadata(sourceUnit, compilationUnit, methodNode) - if (!(methodAnnotationMetadata instanceof AnnotationMetadataHierarchy)) { - methodAnnotationMetadata = new AnnotationMetadataHierarchy( - AstAnnotationUtils.getAnnotationMetadata(sourceUnit, compilationUnit, methodNode.declaringClass), - methodAnnotationMetadata - ); - } - typeElementVisitors.findAll { it.matches(methodAnnotationMetadata) }.each { - def element = it.visit(methodNode, methodAnnotationMetadata, visitorContext) - if (element != null) { - methodAnnotationMetadata = element.annotationMetadata + void visitMethod(MethodElement e) { + for (LoadedVisitor it : typeElementVisitors) { + if (it.matchesElement(e)) { + it.getVisitor().visitMethod(e, visitorContext) } } } - @Override void visitField(FieldNode fieldNode) { if (fieldNode.name == 'metaClass') return int modifiers = fieldNode.modifiers @@ -201,28 +191,32 @@ class TypeElementVisitorTransform implements ASTTransformation, CompilationUnitA if (fieldNode.isSynthetic() && !isPackagePrivate(fieldNode, fieldNode.modifiers)) { return } - AnnotationMetadata fieldAnnotationMetadata = AstAnnotationUtils.getAnnotationMetadata(sourceUnit, compilationUnit, fieldNode) - typeElementVisitors.findAll { it.matches(fieldAnnotationMetadata) }.each { - def element = it.visit(fieldNode, fieldAnnotationMetadata, visitorContext) - if (element != null) { - fieldAnnotationMetadata = element.annotationMetadata + if (fieldNode.enum) { + def e = visitorContext.getElementFactory() + .newEnumConstantElement(targetClassElement, fieldNode, visitorContext.getElementAnnotationMetadataFactory()) + for (LoadedVisitor it : typeElementVisitors) { + if (it.matchesElement(e)) { + it.getVisitor().visitEnumConstant(e, visitorContext) + } + } + } else { + def e = visitorContext.getElementFactory() + .newFieldElement(targetClassElement, fieldNode, visitorContext.getElementAnnotationMetadataFactory()) + for (LoadedVisitor it : typeElementVisitors) { + if (it.matchesElement(e)) { + it.getVisitor().visitField(e, visitorContext) + } } } } - @Override - void visitProperty(PropertyNode propertyNode) { - FieldNode fieldNode = propertyNode.field - if (fieldNode.name == 'metaClass') return - def modifiers = propertyNode.getModifiers() - if (Modifier.isFinal(modifiers) || Modifier.isStatic(modifiers)) { - return - } - AnnotationMetadata fieldAnnotationMetadata = AstAnnotationUtils.getAnnotationMetadata(sourceUnit, compilationUnit, fieldNode) - typeElementVisitors.findAll { it.matches(fieldAnnotationMetadata) }.each { - def element = it.visit(fieldNode, fieldAnnotationMetadata, visitorContext) - if (element != null) { - fieldAnnotationMetadata = element.annotationMetadata + void visitNativeProperty(PropertyElement propertyNode) { + for (LoadedVisitor it : typeElementVisitors) { + if (it.matchesElement(propertyNode)) { + propertyNode.field.ifPresent(f -> it.getVisitor().visitField(f, visitorContext)) + // visit synthetic getter/setter methods + propertyNode.writeMethod.ifPresent(m -> it.getVisitor().visitMethod(m, visitorContext)) + propertyNode.readMethod.ifPresent(m -> it.getVisitor().visitMethod(m, visitorContext)) } } } diff --git a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/annotation/GroovyAnnotationMetadataBuilder.java b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/annotation/GroovyAnnotationMetadataBuilder.java index 0074b375562..1cfc1f83842 100644 --- a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/annotation/GroovyAnnotationMetadataBuilder.java +++ b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/annotation/GroovyAnnotationMetadataBuilder.java @@ -19,12 +19,12 @@ import groovy.lang.GroovyObjectSupport; import groovy.lang.Script; import io.micronaut.ast.groovy.utils.AstGenericUtils; -import io.micronaut.core.annotation.NonNull; import io.micronaut.ast.groovy.utils.AstMessageUtils; import io.micronaut.ast.groovy.utils.ExtendedParameter; import io.micronaut.ast.groovy.visitor.GroovyVisitorContext; import io.micronaut.core.annotation.AnnotationClassValue; import io.micronaut.core.annotation.AnnotationValue; +import io.micronaut.core.annotation.NonNull; import io.micronaut.core.convert.ConversionService; import io.micronaut.core.io.service.SoftServiceLoader; import io.micronaut.core.reflect.ClassUtils; @@ -108,7 +108,11 @@ public GroovyAnnotationMetadataBuilder(SourceUnit sourceUnit, CompilationUnit co } else { this.elementValidator = null; } + } + @Override + public CachedAnnotationMetadata lookupOrBuildForParameter(AnnotatedNode owningType, AnnotatedNode methodElement, AnnotatedNode parameterElement) { + return super.lookupOrBuildForParameter(owningType, methodElement, new ExtendedParameter((MethodNode) methodElement, (Parameter) parameterElement)); } @Override @@ -180,23 +184,6 @@ protected void addWarning(@NonNull AnnotatedNode originatingElement, @NonNull St AstMessageUtils.warning(sourceUnit, originatingElement, warning); } - @Override - protected boolean isMethodOrClassElement(AnnotatedNode element) { - return element instanceof ClassNode || element instanceof MethodNode; - } - - @Override - protected String getDeclaringType(@NonNull AnnotatedNode element) { - if (element instanceof ClassNode) { - return ((ClassNode) element).getName(); - } - final ClassNode declaringClass = element.getDeclaringClass(); - if (declaringClass != null) { - return declaringClass.getName(); - } - return null; - } - @Override protected boolean hasAnnotation(AnnotatedNode element, Class annotation) { return !element.getAnnotations(ClassHelper.makeCached(annotation)).isEmpty(); @@ -204,7 +191,7 @@ protected boolean hasAnnotation(AnnotatedNode element, Class getAnnotationMirror(String annotationName) { ClassNode cn = ClassUtils.forName(annotationName, GroovyAnnotationMetadataBuilder.class.getClassLoader()) - .map(ClassHelper::make) - .orElseGet(() -> ClassHelper.make(annotationName)); + .map(ClassHelper::make) + .orElseGet(() -> ClassHelper.make(annotationName)); if (!cn.getName().equals(ClassHelper.OBJECT)) { return Optional.of(cn); } else { @@ -281,11 +268,11 @@ protected String getElementName(AnnotatedNode element) { protected List getAnnotationsForType(AnnotatedNode element) { List annotations = element.getAnnotations(); List expanded = new ArrayList<>(annotations.size()); - for (AnnotationNode node: annotations) { + for (AnnotationNode node : annotations) { Expression value = node.getMember("value"); boolean repeatable = false; if (value instanceof ListExpression) { - for (Expression expression: ((ListExpression) value).getExpressions()) { + for (Expression expression : ((ListExpression) value).getExpressions()) { if (expression instanceof AnnotationConstantExpression) { String name = getRepeatableNameForType(expression.getType()); if (name != null && name.equals(node.getClassNode().getName())) { @@ -305,7 +292,7 @@ protected List getAnnotationsForType(AnnotatedNode ele @Override protected List buildHierarchy(AnnotatedNode element, boolean inheritTypeAnnotations, boolean declaredOnly) { if (declaredOnly) { - return Collections.singletonList(element); + return new ArrayList<>(Collections.singletonList(element)); } else if (element instanceof ClassNode) { List hierarchy = new ArrayList<>(); ClassNode cn = (ClassNode) element; @@ -345,19 +332,19 @@ protected List buildHierarchy(AnnotatedNode element, boolean inhe if (element == null) { return new ArrayList<>(); } else { - return Collections.singletonList(element); + return new ArrayList<>(Collections.singletonList(element)); } } } @Override protected void readAnnotationRawValues( - AnnotatedNode originatingElement, - String annotationName, - AnnotatedNode member, - String memberName, - Object annotationValue, - Map annotationValues) { + AnnotatedNode originatingElement, + String annotationName, + AnnotatedNode member, + String memberName, + Object annotationValue, + Map annotationValues) { if (!annotationValues.containsKey(memberName)) { final Object v = readAnnotationValue(originatingElement, member, memberName, annotationValue); if (v != null) { @@ -443,7 +430,7 @@ protected boolean isInheritedAnnotationType(@NonNull AnnotatedNode annotationTyp final List annotations = annotationType.getAnnotations(); if (CollectionUtils.isNotEmpty(annotations)) { return annotations.stream().anyMatch((ann) -> - ann.getClassNode().getName().equals(Inherited.class.getName()) + ann.getClassNode().getName().equals(Inherited.class.getName()) ); } return false; @@ -489,7 +476,7 @@ protected Object readAnnotationValue(AnnotatedNode originatingElement, Annotated AnnotationNode value = (AnnotationNode) ann.getValue(); final AnnotationValue av = readNestedAnnotationValue(originatingElement, value); if (member instanceof MethodNode && ((MethodNode) member).getReturnType().isArray()) { - return new AnnotationValue[] { av }; + return new AnnotationValue[]{av}; } else { return av; } @@ -572,7 +559,7 @@ protected Object readAnnotationValue(AnnotatedNode originatingElement, Annotated } // for some reason this is necessary to produce correct array type in Groovy return ConversionService.SHARED.convert(converted, Array.newInstance(arrayType, 0).getClass()) - .orElse(null); + .orElse(null); } else if (annotationValue instanceof VariableExpression) { VariableExpression ve = (VariableExpression) annotationValue; Variable variable = ve.getAccessedVariable(); @@ -605,7 +592,7 @@ protected OptionalValues getAnnotationValues(AnnotatedNode originatingElement AnnotationNode ann = anns.get(0); Map converted = new LinkedHashMap<>(); ann.getMembers().forEach((key, value) -> - readAnnotationRawValues(originatingElement, annotationType.getName(), member, key, value, converted) + readAnnotationRawValues(originatingElement, annotationType.getName(), member, key, value, converted) ); return OptionalValues.of(Object.class, converted); } @@ -659,7 +646,7 @@ private List findOverriddenMethods(MethodNode methodNode) { classNode = classNode.getSuperClass(); if (classNode != null && !classNode.getName().equals(Object.class.getName())) { - for (MethodNode parent: classNode.getMethods(methodName)) { + for (MethodNode parent : classNode.getMethods(methodName)) { if (methodOverrides(methodNode, parent, genericsInfo.get(classNode.getName()))) { if (!parent.isPrivate()) { overriddenMethods.add(parent); diff --git a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/annotation/GroovyElementAnnotationMetadataFactory.java b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/annotation/GroovyElementAnnotationMetadataFactory.java new file mode 100644 index 00000000000..067a455eb84 --- /dev/null +++ b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/annotation/GroovyElementAnnotationMetadataFactory.java @@ -0,0 +1,40 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.ast.groovy.annotation; + +import io.micronaut.inject.annotation.AbstractElementAnnotationMetadataFactory; +import io.micronaut.inject.ast.ElementAnnotationMetadataFactory; +import org.codehaus.groovy.ast.AnnotatedNode; +import org.codehaus.groovy.ast.AnnotationNode; + +/** + * Groovy element annotation metadata factory. + * + * @author Denis Stepanov + * @since 4.0.0 + */ +public final class GroovyElementAnnotationMetadataFactory extends AbstractElementAnnotationMetadataFactory { + + public GroovyElementAnnotationMetadataFactory(boolean isReadOnly, GroovyAnnotationMetadataBuilder metadataBuilder) { + super(isReadOnly, metadataBuilder); + } + + @Override + public ElementAnnotationMetadataFactory readOnly() { + return new GroovyElementAnnotationMetadataFactory(true, (GroovyAnnotationMetadataBuilder) metadataBuilder); + } + +} diff --git a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/config/GroovyConfigurationMetadataBuilder.groovy b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/config/GroovyConfigurationMetadataBuilder.groovy deleted file mode 100644 index 7a3d4df430d..00000000000 --- a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/config/GroovyConfigurationMetadataBuilder.groovy +++ /dev/null @@ -1,178 +0,0 @@ -/* - * Copyright 2017-2020 original authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.micronaut.ast.groovy.config - -import groovy.transform.CompileStatic -import io.micronaut.ast.groovy.utils.AstAnnotationUtils -import io.micronaut.ast.groovy.utils.AstGenericUtils -import io.micronaut.context.annotation.ConfigurationReader -import io.micronaut.context.annotation.EachProperty -import io.micronaut.core.annotation.AnnotationMetadata -import io.micronaut.core.util.StringUtils -import io.micronaut.inject.ast.Element -import io.micronaut.inject.configuration.ConfigurationMetadataBuilder -import org.codehaus.groovy.ast.ClassHelper -import org.codehaus.groovy.ast.ClassNode -import org.codehaus.groovy.ast.InnerClassNode -import org.codehaus.groovy.control.CompilationUnit -import org.codehaus.groovy.control.SourceUnit - -import java.util.function.Function - -/** - * Implementation of ConfigurationMetadataBuilder for Groovy - * - * @author graemerocher - * @since 1.0 - */ -@CompileStatic -class GroovyConfigurationMetadataBuilder extends ConfigurationMetadataBuilder { - - final SourceUnit sourceUnit - final CompilationUnit compilationUnit - - GroovyConfigurationMetadataBuilder(SourceUnit sourceUnit, CompilationUnit compilationUnit) { - this.compilationUnit = compilationUnit - this.sourceUnit = sourceUnit - } - - @Override - Element[] getOriginatingElements() { - return Element.EMPTY_ELEMENT_ARRAY - } - - @Override - protected String buildPropertyPath(ClassNode owningType, ClassNode declaringType, String propertyName) { - String value = buildTypePath(owningType, declaringType) - return value + '.' + propertyName - } - - @Override - protected String buildTypePath(ClassNode owningType, ClassNode declaringType) { - return buildTypePath(owningType, declaringType, getAnnotationMetadata(owningType.isInterface() ? owningType : declaringType)) - } - - @Override - protected String buildTypePath(ClassNode owningType, ClassNode declaringType, AnnotationMetadata annotationMetadata) { - StringBuilder path = new StringBuilder(calculateInitialPath(owningType, annotationMetadata)) - - prependSuperclasses(owningType.isInterface() ? owningType : declaringType, path) - while (declaringType != null && declaringType instanceof InnerClassNode) { - // we have an inner class, so prepend inner class - declaringType = ((InnerClassNode) declaringType).getOuterClass() - if (declaringType != null) { - - AnnotationMetadata parentMetadata = getAnnotationMetadata(declaringType) - Optional parentConfig = parentMetadata.getValue(ConfigurationReader.class, String.class) - if (parentConfig.isPresent()) { - String parentPath = parentConfig.get() - if (parentMetadata.hasDeclaredAnnotation(EachProperty)) { - if (parentMetadata.booleanValue(EachProperty.class, "list").orElse(false)) { - path.insert(0, parentPath + "[*].") - } else { - path.insert(0, parentPath + ".*.") - } - } else { - path.insert(0, parentPath + '.') - } - prependSuperclasses(declaringType, path) - } else { - break - } - - } - - } - return path.toString() - } - - private String calculateInitialPath(ClassNode owningType, AnnotationMetadata annotationMetadata) { - return annotationMetadata.stringValue(ConfigurationReader.class) - .map(pathEvaluationFunction(annotationMetadata)).orElseGet( {-> - AnnotationMetadata ownerMetadata = getAnnotationMetadata(owningType) - return ownerMetadata.stringValue(ConfigurationReader.class) - .map(pathEvaluationFunction(ownerMetadata)).orElseGet({ -> - pathEvaluationFunction(annotationMetadata).apply("") - }) - }) - } - - private Function pathEvaluationFunction(AnnotationMetadata annotationMetadata) { - return { String path -> - if (annotationMetadata.hasDeclaredAnnotation(EachProperty.class)) { - if (annotationMetadata.booleanValue(EachProperty.class, "list").orElse(false)) { - return path + "[*]" - } else { - return path + ".*" - } - } - String prefix = annotationMetadata.getValue(ConfigurationReader.class, "prefix", String.class) - .orElse(null) - if (StringUtils.isNotEmpty(prefix)) { - if (StringUtils.isEmpty(path)) { - return prefix - } else { - return prefix + "." + path - } - } else { - return path - } - } as Function - } - - @Override - protected String getTypeString(ClassNode type) { - return AstGenericUtils.resolveTypeReference(type) - } - - @Override - protected AnnotationMetadata getAnnotationMetadata(ClassNode type) { - return AstAnnotationUtils.getAnnotationMetadata(sourceUnit, compilationUnit, type) - } - - private void prependSuperclasses(ClassNode declaringType, StringBuilder path) { - if (declaringType.isInterface()) { - ClassNode superInterface = resolveSuperInterface(declaringType) - while (superInterface != null) { - AnnotationMetadata annotationMetadata = getAnnotationMetadata(superInterface) - Optional parentConfig = annotationMetadata.stringValue(ConfigurationReader.class) - if (parentConfig.isPresent()) { - path.insert(0, parentConfig.get() + '.') - superInterface = resolveSuperInterface(superInterface) - } else { - break; - } - } - } else { - ClassNode superclass = declaringType.getSuperClass() - while (superclass != ClassHelper.OBJECT_TYPE) { - Optional parentConfig = getAnnotationMetadata(superclass).stringValue(ConfigurationReader.class) - if (parentConfig.isPresent()) { - path.insert(0, parentConfig.get() + '.') - superclass = superclass.getSuperClass() - } else { - break - } - } - } - } - private ClassNode resolveSuperInterface(ClassNode declaringType) { - def interfaces = declaringType.getInterfaces() - if (interfaces) { - return interfaces.find { getAnnotationMetadata(it).hasStereotype(ConfigurationReader) } - } - } -} diff --git a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/utils/AstAnnotationUtils.groovy b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/utils/AstAnnotationUtils.groovy deleted file mode 100644 index 1d5adad0d54..00000000000 --- a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/utils/AstAnnotationUtils.groovy +++ /dev/null @@ -1,285 +0,0 @@ -/* - * Copyright 2017-2020 original authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.micronaut.ast.groovy.utils - -import groovy.transform.CompileStatic -import io.micronaut.ast.groovy.annotation.GroovyAnnotationMetadataBuilder -import io.micronaut.core.annotation.AnnotationMetadata -import io.micronaut.core.annotation.AnnotationUtil -import io.micronaut.core.annotation.Internal -import io.micronaut.core.util.clhm.ConcurrentLinkedHashMap -import io.micronaut.inject.annotation.AbstractAnnotationMetadataBuilder -import io.micronaut.inject.annotation.AnnotationMetadataSupport -import org.codehaus.groovy.ast.AnnotatedNode -import org.codehaus.groovy.ast.AnnotationNode -import org.codehaus.groovy.ast.ClassNode -import org.codehaus.groovy.ast.MethodNode -import org.codehaus.groovy.ast.expr.Expression -import org.codehaus.groovy.control.CompilationUnit -import org.codehaus.groovy.control.SourceUnit - -import java.lang.annotation.Annotation - -/** - * Utility methods for dealing with annotations within the context of AST - * - * @author Graeme Rocher - * @since 1.0 - */ -@CompileStatic -class AstAnnotationUtils { - - private static final Map annotationMetadataCache = new ConcurrentLinkedHashMap.Builder().maximumWeightedCapacity(100).build() - - /** - * Get the {@link AnnotationMetadata} for the given annotated node - * - * @param annotatedNode The node - * @return The metadata - */ - static AnnotationMetadata getAnnotationMetadata(SourceUnit sourceUnit, CompilationUnit compilationUnit, AnnotatedNode annotatedNode) { - return annotationMetadataCache.computeIfAbsent(annotatedNode, { AnnotatedNode annotatedNode1 -> - new GroovyAnnotationMetadataBuilder(sourceUnit, compilationUnit).build(annotatedNode1) - }) - } - - /** - * Get the {@link AnnotationMetadata} for the given method node node - * - * @param annotatedNode The node - * @return The metadata - */ - static AnnotationMetadata getMethodAnnotationMetadata(SourceUnit sourceUnit, CompilationUnit compilationUnit, MethodNode annotatedNode) { - return new GroovyAnnotationMetadataBuilder(sourceUnit, compilationUnit).buildOverridden(annotatedNode) - } - - /** - * Get the {@link AnnotationMetadata} for the given annotated node - * - * @param sourceUnit the source unit - * @param parents the parents - * @param annotatedNode The node - * @return The metadata - */ - static AnnotationMetadata getAnnotationMetadata(SourceUnit sourceUnit, CompilationUnit compilationUnit, List parents, AnnotatedNode annotatedNode) { - new GroovyAnnotationMetadataBuilder(sourceUnit, compilationUnit).buildForParents(parents, annotatedNode) - } - - - /** - * Get the {@link AnnotationMetadata} for the given annotated node - * - * @param sourceUnit the source unit - * @param parent the parent - * @param annotatedNode The node - * @return The metadata - */ - static AnnotationMetadata getAnnotationMetadata(SourceUnit sourceUnit, CompilationUnit compilationUnit, AnnotatedNode parent, AnnotatedNode annotatedNode, boolean inheritTypeAnnotations) { - newBuilder(sourceUnit, compilationUnit).buildForParent(parent, annotatedNode, inheritTypeAnnotations) - } - - /** - * Creates a new annotation builder for the given source unit - * @param sourceUnit The unit - * @return the builder - */ - static GroovyAnnotationMetadataBuilder newBuilder(SourceUnit sourceUnit, CompilationUnit compilationUnit) { - new GroovyAnnotationMetadataBuilder(sourceUnit, compilationUnit) - } - - /** - * Invalidates any cached metadata - */ - @Internal - static void invalidateCache() { - annotationMetadataCache.clear() - AbstractAnnotationMetadataBuilder.clearCaches() - } - - /** - * Invalidates any cached metadata - */ - @Internal - static void invalidateCache(AnnotatedNode node) { - if (node) - annotationMetadataCache.remove(node) - } - - /** - * Return whether the given annotated node has the given stereotype - * - * @param annotatedNode The annotated node - * @param stereotype The stereotype - * @return True if it does - */ - static boolean hasStereotype(SourceUnit sourceUnit, CompilationUnit compilationUnit, AnnotatedNode annotatedNode, String stereotype) { - return getAnnotationMetadata(sourceUnit, compilationUnit, annotatedNode).hasStereotype(stereotype) - } - /** - * Return whether the given annotated node has the given stereotype - * - * @param annotatedNode The annotated node - * @param stereotype The stereotype - * @return True if it does - */ - static boolean hasStereotype(SourceUnit sourceUnit, CompilationUnit compilationUnit, AnnotatedNode annotatedNode, Class stereotype) { - return hasStereotype(sourceUnit, compilationUnit, annotatedNode, stereotype.getName()) - } - - /** - * Return whether the given element is annotated with any of the given annotation stereotypes. - * - * @param element The element - * @param stereotypes The stereotypes - * @return True if it is - */ - static boolean hasStereotype(SourceUnit sourceUnit, CompilationUnit compilationUnit, AnnotatedNode annotatedNode, List stereotypes) { - if (annotatedNode == null) { - return false - } - AnnotationMetadata annotationMetadata = getAnnotationMetadata(sourceUnit, compilationUnit, annotatedNode) - for (String stereotype : stereotypes) { - if (annotationMetadata.hasStereotype(stereotype)) { - return true - } - } - return false - } - - /** - * Whether the node is annotated with any non internal annotations - * - * @param declaringType The declaring type - * @param annotatedNode The annotated node - * @return True if it is - */ - static boolean isAnnotated(String declaringType, AnnotatedNode annotatedNode) { - if (AbstractAnnotationMetadataBuilder.isMetadataMutated(declaringType, annotatedNode)) { - return true - } - for (ann in annotatedNode.annotations) { - if (!AnnotationUtil.INTERNAL_ANNOTATION_NAMES.contains(ann.classNode.name)) { - return true - } - } - return false - } - /** - * Finds an annotation for the given annotated node and type - * - * @param annotatedNode The annotated node - * @param annotationName The annotation type - * @return The annotation or null - */ - static AnnotationNode findAnnotation(AnnotatedNode annotatedNode, Class type) { - String annotationName = type.name - return findAnnotation(annotatedNode, annotationName) - } - - /** - * Finds an annotation for the given annotated node and name - * - * @param annotatedNode The annotated node - * @param annotationName The annotation name - * @return The annotation or null - */ - static AnnotationNode findAnnotation(AnnotatedNode annotatedNode, String annotationName) { - if (annotatedNode != null) { - List annotations = annotatedNode.getAnnotations() - for (ann in annotations) { - ClassNode annotationClassNode = ann.classNode - if (annotationClassNode.name == annotationName) { - return ann - } else if (!(annotationClassNode.name in AnnotationUtil.INTERNAL_ANNOTATION_NAMES)) { - - ann = findAnnotation(annotationClassNode, annotationName) - if (ann != null) { - return ann - } - } - } - } - return null - } - - /** - * Returns true if MethodNode is marked with annotationClass - * - * @param methodNode A MethodNode to inspect - * @param annotationClass an annotation to look for - * @return true if classNode is marked with annotationClass, otherwise false - */ - static boolean hasAnnotation(final MethodNode methodNode, final Class annotationClass) { - def classNode = new ClassNode(annotationClass) - return hasAnnotation(methodNode, classNode) - } - - /** - * Returns true if MethodNode is marked with annotationClassNode - * - * @param methodNode A MethodNode to inspect - * @param annotationClassNode An annotation class to look for - * @return true if classNode is marked with annotationClass, otherwise false - */ - static boolean hasAnnotation(MethodNode methodNode, ClassNode annotationClassNode) { - return !methodNode.getAnnotations(annotationClassNode).isEmpty() - } - /** - * Copy the annotations from one annotated node to another - * - * @param from The source annotated node - * @param to The target annotated node - */ - static void copyAnnotations(final AnnotatedNode from, final AnnotatedNode to) { - copyAnnotations(from, to, null, null) - } - - /** - * Copy the annotations from one annotated node to another - * - * @param from The source annotated node - * @param to The target annotated node - * @param included The includes annotations - * @param excluded The excluded annotations - */ - static void copyAnnotations( - final AnnotatedNode from, final AnnotatedNode to, final Set included, final Set excluded) { - final List annotationsToCopy = from.getAnnotations() - for (final AnnotationNode node : annotationsToCopy) { - String annotationClassName = node.getClassNode().getName() - if ((excluded == null || !excluded.contains(annotationClassName)) && - (included == null || included.contains(annotationClassName))) { - final AnnotationNode copyOfAnnotationNode = cloneAnnotation(node) - to.addAnnotation(copyOfAnnotationNode) - } - } - } - - /** - * Clones the given annotation node returning a new one - * - * @param node The annotation node - * @return The cloned annotation node - */ - static AnnotationNode cloneAnnotation(final AnnotationNode node) { - final AnnotationNode copyOfAnnotationNode = new AnnotationNode(node.getClassNode()) - final Map members = node.getMembers() - for (final Map.Entry entry : members.entrySet()) { - copyOfAnnotationNode.addMember(entry.getKey(), entry.getValue()) - } - return copyOfAnnotationNode - } -} diff --git a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/utils/AstClassUtils.groovy b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/utils/AstClassUtils.groovy index cb495c12f1f..eefc7717185 100644 --- a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/utils/AstClassUtils.groovy +++ b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/utils/AstClassUtils.groovy @@ -15,13 +15,11 @@ */ package io.micronaut.ast.groovy.utils -import static org.codehaus.groovy.ast.ClassHelper.make -import static org.codehaus.groovy.ast.ClassHelper.makeCached - import groovy.transform.CompileStatic import org.codehaus.groovy.ast.ClassHelper import org.codehaus.groovy.ast.ClassNode +import static org.codehaus.groovy.ast.ClassHelper.make /** * Utility methods for working with classes * @@ -84,17 +82,6 @@ class AstClassUtils { return implementsInterface(classNode, interfaceNode) } - /** - * Whether the given class node implements the given interface node - * - * @param classNode The class node - * @param itfc The interface - * @return True if it does - */ - static boolean implementsInterface(ClassNode classNode, Class itfc) { - return classNode.getAllInterfaces().contains(makeCached(itfc)) - } - /** * Whether the given class node implements the given interface node * @@ -103,6 +90,17 @@ class AstClassUtils { * @return True if it does */ static boolean implementsInterface(ClassNode classNode, ClassNode interfaceNode) { - return classNode.getAllInterfaces().contains(interfaceNode) + if (classNode.getAllInterfaces().contains(interfaceNode)) { + return true + } + ClassNode superClass = classNode.getSuperClass() + while (superClass != null) { + if (superClass.getAllInterfaces().contains(interfaceNode)) { + return true + } + superClass = superClass.getSuperClass() + } + return false } + } diff --git a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/utils/InMemoryByteCodeGroovyClassLoader.java b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/utils/InMemoryByteCodeGroovyClassLoader.java index 64d97114f59..3896bce5766 100644 --- a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/utils/InMemoryByteCodeGroovyClassLoader.java +++ b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/utils/InMemoryByteCodeGroovyClassLoader.java @@ -17,6 +17,7 @@ import groovy.lang.GroovyClassLoader; import io.micronaut.core.annotation.Internal; +import io.micronaut.inject.annotation.AbstractAnnotationMetadataBuilder; import org.codehaus.groovy.control.CompilerConfiguration; import java.io.ByteArrayInputStream; @@ -54,6 +55,8 @@ public class InMemoryByteCodeGroovyClassLoader extends GroovyClassLoader { */ public InMemoryByteCodeGroovyClassLoader() { clearCache(); + AbstractAnnotationMetadataBuilder.clearCaches(); + AbstractAnnotationMetadataBuilder.clearMutated(); } /** @@ -62,6 +65,8 @@ public InMemoryByteCodeGroovyClassLoader() { public InMemoryByteCodeGroovyClassLoader(ClassLoader loader) { super(loader); clearCache(); + AbstractAnnotationMetadataBuilder.clearCaches(); + AbstractAnnotationMetadataBuilder.clearMutated(); } /** @@ -70,6 +75,8 @@ public InMemoryByteCodeGroovyClassLoader(ClassLoader loader) { public InMemoryByteCodeGroovyClassLoader(GroovyClassLoader parent) { super(parent); clearCache(); + AbstractAnnotationMetadataBuilder.clearCaches(); + AbstractAnnotationMetadataBuilder.clearMutated(); } /** @@ -149,4 +156,8 @@ public Enumeration findResources(String name) throws IOException { return super.findResources(name); } } + + public final Map getGeneratedClasses() { + return generatedClasses; + } } diff --git a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/utils/PublicAbstractMethodVisitor.groovy b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/utils/PublicAbstractMethodVisitor.groovy deleted file mode 100644 index b815388ca94..00000000000 --- a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/utils/PublicAbstractMethodVisitor.groovy +++ /dev/null @@ -1,75 +0,0 @@ -/* - * Copyright 2017-2020 original authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.micronaut.ast.groovy.utils - -import groovy.transform.CompilationUnitAware -import groovy.transform.CompileStatic -import org.codehaus.groovy.ast.ClassNode -import org.codehaus.groovy.ast.MethodNode -import org.codehaus.groovy.control.CompilationUnit -import org.codehaus.groovy.control.SourceUnit - -/** - * A visitor that visits only abstract methods - * - * @author graemerocher - * @since 1.0 - */ -@CompileStatic -abstract class PublicAbstractMethodVisitor extends PublicMethodVisitor { - - ClassNode current - private final CompilationUnit compilationUnit - - PublicAbstractMethodVisitor(SourceUnit sourceUnit, CompilationUnit compilationUnit) { - super(sourceUnit) - this.compilationUnit = compilationUnit - } - - CompilationUnit getCompilationUnit() { - compilationUnit - } - - @Override - void accept(ClassNode classNode) { - this.current = classNode - super.accept(classNode) - } - - @Override - protected boolean isAcceptable(MethodNode node) { - if (!isAcceptableMethod(node)) { - return false - } - if (current != null) { - // ignore overridden methods - def existing = current.getMethod(node.name, node.parameters) - if (existing != null && existing != node) { - return false - } - } - return super.isAcceptable(node) - } - - /** - * Return whether the given executable element is acceptable. By default just checks if the method is abstract. - * @param executableElement The method - * @return True if it is - */ - protected boolean isAcceptableMethod(MethodNode node) { - return node.isAbstract() - } -} diff --git a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/utils/PublicMethodVisitor.groovy b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/utils/PublicMethodVisitor.groovy index 72757d7e695..345d0570594 100644 --- a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/utils/PublicMethodVisitor.groovy +++ b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/utils/PublicMethodVisitor.groovy @@ -54,6 +54,9 @@ abstract class PublicMethodVisitor extends ClassCodeVisitorSupport { this.current = classNode classNode.visitContents(this) for (i in classNode.getAllInterfaces()) { + if (classNode == i) { + continue + } if (i.name != GroovyObject.class.name) { this.current = i i.visitContents(this) diff --git a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/AbstractGroovyElement.java b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/AbstractGroovyElement.java index e8b3e6ee42f..1efb17053e2 100644 --- a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/AbstractGroovyElement.java +++ b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/AbstractGroovyElement.java @@ -15,38 +15,38 @@ */ package io.micronaut.ast.groovy.visitor; -import io.micronaut.core.annotation.Nullable; -import groovy.transform.CompileStatic; import groovy.transform.PackageScope; -import io.micronaut.ast.groovy.annotation.GroovyAnnotationMetadataBuilder; -import io.micronaut.ast.groovy.utils.AstAnnotationUtils; import io.micronaut.core.annotation.AnnotationMetadata; -import io.micronaut.core.annotation.AnnotationMetadataDelegate; -import io.micronaut.core.annotation.AnnotationValue; -import io.micronaut.core.annotation.AnnotationValueBuilder; -import io.micronaut.core.util.ArgumentUtils; +import io.micronaut.core.annotation.NonNull; +import io.micronaut.core.annotation.Nullable; import io.micronaut.core.util.ArrayUtils; import io.micronaut.core.util.CollectionUtils; import io.micronaut.core.util.StringUtils; -import io.micronaut.inject.annotation.AbstractAnnotationMetadataBuilder; import io.micronaut.inject.ast.ClassElement; import io.micronaut.inject.ast.Element; +import io.micronaut.inject.ast.ElementAnnotationMetadata; +import io.micronaut.inject.ast.ElementAnnotationMetadataFactory; import io.micronaut.inject.ast.ElementModifier; -import io.micronaut.inject.ast.MemberElement; +import io.micronaut.inject.ast.ElementMutableAnnotationMetadata; +import io.micronaut.inject.ast.ElementMutableAnnotationMetadataDelegate; import org.codehaus.groovy.ast.AnnotatedNode; import org.codehaus.groovy.ast.ClassNode; -import org.codehaus.groovy.ast.MethodNode; import org.codehaus.groovy.ast.FieldNode; import org.codehaus.groovy.ast.GenericsType; +import org.codehaus.groovy.ast.MethodNode; import org.codehaus.groovy.control.CompilationUnit; import org.codehaus.groovy.control.SourceUnit; -import io.micronaut.core.annotation.NonNull; -import java.lang.annotation.Annotation; import java.lang.reflect.Modifier; -import java.util.*; -import java.util.function.Consumer; -import java.util.function.Predicate; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; import java.util.regex.Pattern; /** @@ -56,122 +56,110 @@ * @since 1.1 */ -public abstract class AbstractGroovyElement implements AnnotationMetadataDelegate, Element { +public abstract class AbstractGroovyElement implements Element, ElementMutableAnnotationMetadataDelegate { private static final Pattern JAVADOC_PATTERN = Pattern.compile("(/\\s*\\*\\*)|\\s*\\*|(\\s*[*/])"); protected final SourceUnit sourceUnit; protected final CompilationUnit compilationUnit; protected final GroovyVisitorContext visitorContext; + protected final ElementAnnotationMetadataFactory elementAnnotationMetadataFactory; + protected AnnotationMetadata presetAnnotationMetadata; + private ElementAnnotationMetadata elementAnnotationMetadata; private final AnnotatedNode annotatedNode; - private AnnotationMetadata annotationMetadata; /** * Default constructor. - * @param visitorContext The groovy visitor context - * @param annotatedNode The annotated node - * @param annotationMetadata The annotation metadata + * + * @param visitorContext The groovy visitor context + * @param annotatedNode The annotated node + * @param annotationMetadataFactory The annotation metadata factory */ - protected AbstractGroovyElement(GroovyVisitorContext visitorContext, AnnotatedNode annotatedNode, AnnotationMetadata annotationMetadata) { + protected AbstractGroovyElement(GroovyVisitorContext visitorContext, + AnnotatedNode annotatedNode, + ElementAnnotationMetadataFactory annotationMetadataFactory) { this.visitorContext = visitorContext; this.compilationUnit = visitorContext.getCompilationUnit(); this.annotatedNode = annotatedNode; - this.annotationMetadata = annotationMetadata; + this.elementAnnotationMetadataFactory = annotationMetadataFactory; this.sourceUnit = visitorContext.getSourceUnit(); } @Override - public AnnotationMetadata getAnnotationMetadata() { - return annotationMetadata; - } - - @Override - public boolean isPackagePrivate() { - return hasDeclaredAnnotation(PackageScope.class); + public Element getReturnInstance() { + return this; } - @CompileStatic @Override - public Element annotate(@NonNull String annotationType, @NonNull Consumer> consumer) { - ArgumentUtils.requireNonNull("annotationType", annotationType); - AnnotationValueBuilder builder = AnnotationValue.builder(annotationType); - //noinspection ConstantConditions - if (consumer != null) { - - consumer.accept(builder); - AnnotationValue av = builder.build(); - this.annotationMetadata = new GroovyAnnotationMetadataBuilder(sourceUnit, compilationUnit).annotate( - this.annotationMetadata, - av - ); - updateAnnotationCaches(); + public ElementMutableAnnotationMetadata getAnnotationMetadata() { + if (elementAnnotationMetadata == null) { + if (presetAnnotationMetadata == null) { + elementAnnotationMetadata = elementAnnotationMetadataFactory.build(this); + } else { + elementAnnotationMetadata = elementAnnotationMetadataFactory.build(this, presetAnnotationMetadata); + } } - return this; + return elementAnnotationMetadata; } - @Override - public Element annotate(AnnotationValue annotationValue) { - ArgumentUtils.requireNonNull("annotationValue", annotationValue); - this.annotationMetadata = new GroovyAnnotationMetadataBuilder(sourceUnit, compilationUnit).annotate( - this.annotationMetadata, - annotationValue - ); - updateAnnotationCaches(); - return this; + /** + * Constructs this element by invoking the constructor. + * + * @return the copy + */ + @NonNull + protected abstract AbstractGroovyElement copyConstructor(); + + /** + * Copies additional values after the element was constructed by {@link #copyConstructor()}. + * + * @param element the values to be copied to + */ + protected void copyValues(@NonNull AbstractGroovyElement element) { + element.presetAnnotationMetadata = presetAnnotationMetadata; } - @Override - public Element removeAnnotation(@NonNull String annotationType) { - ArgumentUtils.requireNonNull("annotationType", annotationType); - this.annotationMetadata = new GroovyAnnotationMetadataBuilder(sourceUnit, compilationUnit).removeAnnotation( - this.annotationMetadata, annotationType - ); - updateAnnotationCaches(); - return this; + /** + * Makes a copy of this element. + * + * @return a copy + */ + @NonNull + protected final AbstractGroovyElement copy() { + AbstractGroovyElement element = copyConstructor(); + copyValues(element); + return element; } @Override - public Element removeAnnotationIf(@NonNull Predicate> predicate) { - ArgumentUtils.requireNonNull("predicate", predicate); - this.annotationMetadata = new GroovyAnnotationMetadataBuilder(sourceUnit, compilationUnit).removeAnnotationIf( - this.annotationMetadata, predicate - ); - updateAnnotationCaches(); - return this; - + public io.micronaut.inject.ast.Element withAnnotationMetadata(AnnotationMetadata annotationMetadata) { + AbstractGroovyElement element = copy(); + element.presetAnnotationMetadata = annotationMetadata; + return element; } @Override - public Element removeStereotype(@NonNull String annotationType) { - ArgumentUtils.requireNonNull("annotationType", annotationType); - this.annotationMetadata = new GroovyAnnotationMetadataBuilder(sourceUnit, compilationUnit).removeStereotype( - this.annotationMetadata, annotationType - ); - updateAnnotationCaches(); - return this; + public AnnotatedNode getNativeType() { + return annotatedNode; } - private void updateAnnotationCaches() { - String declaringTypeName = this instanceof MemberElement ? ((MemberElement) this).getOwningType().getName() : getName(); - AbstractAnnotationMetadataBuilder.addMutatedMetadata( - declaringTypeName, - annotatedNode, - this.annotationMetadata - ); - AstAnnotationUtils.invalidateCache(annotatedNode); + @Override + public boolean isPackagePrivate() { + return hasDeclaredAnnotation(PackageScope.class); } /** * Align the given generic types. + * * @param genericsTypes The generic types * @param redirectTypes The redirect types - * @param genericsSpec The current generics spec + * @param genericsSpec The current generics spec * @return The new generic spec */ protected Map alignNewGenericsInfo( - @NonNull GenericsType[] genericsTypes, - @NonNull GenericsType[] redirectTypes, - @NonNull Map genericsSpec) { + @NonNull GenericsType[] genericsTypes, + @NonNull GenericsType[] redirectTypes, + @NonNull Map genericsSpec) { if (redirectTypes == null || redirectTypes.length != genericsTypes.length) { return Collections.emptyMap(); } else { @@ -273,18 +261,19 @@ private void toNewGenericSpec(Map genericsSpec, Map genericsSpec) { + @NonNull SourceUnit sourceUnit, + @NonNull ClassNode type, + @NonNull ClassElement rawElement, + @NonNull Map genericsSpec) { if (CollectionUtils.isNotEmpty(genericsSpec)) { ClassElement classNode = resolveGenericType(genericsSpec, type); if (classNode != null) { @@ -294,9 +283,9 @@ protected ClassElement getGenericElement( GenericsType[] redirectTypes = type.redirect().getGenericsTypes(); if (genericsTypes != null && redirectTypes != null) { genericsSpec = alignNewGenericsInfo(genericsTypes, redirectTypes, genericsSpec); - return new GroovyClassElement(visitorContext, type, annotationMetadata, Collections.singletonMap( - type.getName(), - genericsSpec + return new GroovyClassElement(visitorContext, type, elementAnnotationMetadataFactory, Collections.singletonMap( + type.getName(), + genericsSpec ), 0); } } @@ -312,10 +301,9 @@ private ClassElement resolveGenericType(Map typeGenericInfo, if (classNode.isGenericsPlaceHolder() && classNode != returnType) { return resolveGenericType(typeGenericInfo, classNode); } else { - AnnotationMetadata annotationMetadata = resolveAnnotationMetadata(classNode); - return this.visitorContext.getElementFactory().newClassElement( - classNode, annotationMetadata - ); + return adjustTypeAnnotationMetadata(visitorContext.getElementFactory().newClassElement( + classNode, elementAnnotationMetadataFactory + )); } } } else if (returnType.isArray()) { @@ -328,10 +316,9 @@ private ClassElement resolveGenericType(Map typeGenericInfo, return resolveGenericType(typeGenericInfo, classNode); } else { ClassNode cn = classNode.makeArray(); - AnnotationMetadata annotationMetadata = resolveAnnotationMetadata(cn); - return this.visitorContext.getElementFactory().newClassElement( - cn, annotationMetadata - ); + return adjustTypeAnnotationMetadata(visitorContext.getElementFactory().newClassElement( + cn, elementAnnotationMetadataFactory + )); } } } @@ -348,18 +335,18 @@ public Optional getDocumentation() { } /** - * Resolves the annotation metadata for the given type. - * @param type The type - * @return The annotation metadata + * The method will replace the annotation metadata with empty value if {@link io.micronaut.inject.visitor.VisitorConfiguration#includeTypeLevelAnnotationsInGenericArguments()} + * is false. + * + * @param classElement The class element to adjust annotation metadata + * @return the adjusted element or the same value */ - protected @NonNull AnnotationMetadata resolveAnnotationMetadata(@NonNull ClassNode type) { - AnnotationMetadata annotationMetadata; - if (visitorContext.getConfiguration().includeTypeLevelAnnotationsInGenericArguments()) { - annotationMetadata = AstAnnotationUtils.getAnnotationMetadata(sourceUnit, compilationUnit, type); - } else { - annotationMetadata = AnnotationMetadata.EMPTY_METADATA; + @NonNull + protected final ClassElement adjustTypeAnnotationMetadata(@NonNull ClassElement classElement) { + if (classElement.isPrimitive() || visitorContext.getConfiguration().includeTypeLevelAnnotationsInGenericArguments()) { + return classElement; } - return annotationMetadata; + return classElement.withAnnotationMetadata(AnnotationMetadata.EMPTY_METADATA); } @Override @@ -381,6 +368,7 @@ public int hashCode() { /** * Resolve modifiers for a method node. + * * @param methodNode The method node * @return The modifiers */ @@ -390,6 +378,7 @@ protected Set resolveModifiers(MethodNode methodNode) { /** * Resolve modifiers for a field node. + * * @param fieldNode The field node * @return The modifiers */ @@ -399,6 +388,7 @@ protected Set resolveModifiers(FieldNode fieldNode) { /** * Resolve modifiers for a class node. + * * @param classNode The class node * @return The modifiers */ diff --git a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyAnnotationElement.java b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyAnnotationElement.java index c1cb7490d53..e97e7ceb14d 100644 --- a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyAnnotationElement.java +++ b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyAnnotationElement.java @@ -15,8 +15,8 @@ */ package io.micronaut.ast.groovy.visitor; -import io.micronaut.core.annotation.AnnotationMetadata; import io.micronaut.inject.ast.AnnotationElement; +import io.micronaut.inject.ast.ElementAnnotationMetadataFactory; import org.codehaus.groovy.ast.ClassNode; /** @@ -26,9 +26,10 @@ * @author graemerocher */ final class GroovyAnnotationElement extends GroovyClassElement implements AnnotationElement { + public GroovyAnnotationElement(GroovyVisitorContext visitorContext, ClassNode classNode, - AnnotationMetadata annotationMetadata) { - super(visitorContext, classNode, annotationMetadata); + ElementAnnotationMetadataFactory annotationMetadataFactory) { + super(visitorContext, classNode, annotationMetadataFactory); } } diff --git a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyBeanDefinitionBuilder.java b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyBeanDefinitionBuilder.java index d2ad3537fb8..97ee15ed790 100644 --- a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyBeanDefinitionBuilder.java +++ b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyBeanDefinitionBuilder.java @@ -28,6 +28,7 @@ import io.micronaut.inject.annotation.AnnotationMetadataHierarchy; import io.micronaut.inject.ast.ClassElement; import io.micronaut.inject.ast.Element; +import io.micronaut.inject.ast.ElementAnnotationMetadataFactory; import io.micronaut.inject.ast.FieldElement; import io.micronaut.inject.ast.MethodElement; import io.micronaut.inject.ast.TypedElement; @@ -37,8 +38,6 @@ import io.micronaut.inject.writer.BeanDefinitionVisitor; import io.micronaut.inject.writer.BeanDefinitionWriter; import org.codehaus.groovy.ast.ClassNode; -import org.codehaus.groovy.ast.FieldNode; -import org.codehaus.groovy.ast.MethodNode; import java.lang.annotation.Annotation; import java.util.function.BiConsumer; @@ -58,17 +57,19 @@ class GroovyBeanDefinitionBuilder extends AbstractBeanDefinitionBuilder { /** * Default constructor. * - * @param originatingElement The originating element - * @param beanType The bean type - * @param metadataBuilder the metadata builder - * @param visitorContext the visitor context + * @param originatingElement The originating element + * @param beanType The bean type + * @param metadataBuilder the metadata builder + * @param elementAnnotationMetadataFactory The element annotation metadata factory + * @param visitorContext the visitor context */ GroovyBeanDefinitionBuilder( - Element originatingElement, - ClassElement beanType, - ConfigurationMetadataBuilder metadataBuilder, - GroovyVisitorContext visitorContext) { - super(originatingElement, beanType, metadataBuilder, visitorContext); + Element originatingElement, + ClassElement beanType, + ConfigurationMetadataBuilder metadataBuilder, + ElementAnnotationMetadataFactory elementAnnotationMetadataFactory, + GroovyVisitorContext visitorContext) { + super(originatingElement, beanType, metadataBuilder, visitorContext, elementAnnotationMetadataFactory); if (getClass() == GroovyBeanDefinitionBuilder.class) { visitorContext.addBeanDefinitionBuilder(this); } @@ -79,10 +80,11 @@ class GroovyBeanDefinitionBuilder extends AbstractBeanDefinitionBuilder { protected AbstractBeanDefinitionBuilder createChildBean(FieldElement producerField) { final ClassElement parentType = getBeanType(); return new GroovyBeanDefinitionBuilder( - GroovyBeanDefinitionBuilder.this.getOriginatingElement(), - producerField.getGenericField().getType(), - GroovyBeanDefinitionBuilder.this.metadataBuilder, - GroovyBeanDefinitionBuilder.this.visitorContext + GroovyBeanDefinitionBuilder.this.getOriginatingElement(), + producerField.getGenericField().getType(), + GroovyBeanDefinitionBuilder.this.metadataBuilder, + elementAnnotationMetadataFactory, + GroovyBeanDefinitionBuilder.this.visitorContext ) { @Override public Element getProducingElement() { @@ -97,16 +99,12 @@ public ClassElement getDeclaringElement() { @Override protected BeanDefinitionVisitor createBeanDefinitionWriter() { final BeanDefinitionVisitor writer = super.createBeanDefinitionWriter(); - final GroovyElementFactory elementFactory = ((GroovyVisitorContext) visitorContext).getElementFactory(); - final FieldNode fieldNode = (FieldNode) producerField.getNativeType(); - ClassElement resolvedParent = resolveParent(parentType, elementFactory); + ClassElement newParent = parentType.withAnnotationMetadata(parentType.copyAnnotationMetadata()); // Just a copy writer.visitBeanFactoryField( - resolvedParent, - elementFactory.newFieldElement( - resolvedParent, - fieldNode, - new AnnotationMetadataHierarchy(resolvedParent.getDeclaredMetadata(), producerField.getDeclaredMetadata()) - ) + newParent, + producerField.withAnnotationMetadata( + new AnnotationMetadataHierarchy(newParent.getDeclaredMetadata(), producerField.getDeclaredMetadata()) + ) ); return writer; } @@ -118,10 +116,11 @@ protected BeanDefinitionVisitor createBeanDefinitionWriter() { protected AbstractBeanDefinitionBuilder createChildBean(MethodElement producerMethod) { final ClassElement parentType = getBeanType(); return new GroovyBeanDefinitionBuilder( - GroovyBeanDefinitionBuilder.this.getOriginatingElement(), - producerMethod.getGenericReturnType().getType(), - GroovyBeanDefinitionBuilder.this.metadataBuilder, - GroovyBeanDefinitionBuilder.this.visitorContext + GroovyBeanDefinitionBuilder.this.getOriginatingElement(), + producerMethod.getGenericReturnType().getType(), + GroovyBeanDefinitionBuilder.this.metadataBuilder, + elementAnnotationMetadataFactory, + GroovyBeanDefinitionBuilder.this.visitorContext ) { BeanParameterElement[] parameters; @@ -148,15 +147,12 @@ protected BeanDefinitionVisitor createBeanDefinitionWriter() { final BeanDefinitionVisitor writer = super.createBeanDefinitionWriter(); final GroovyElementFactory elementFactory = ((GroovyVisitorContext) visitorContext).getElementFactory(); ClassElement resolvedParent = resolveParent(parentType, elementFactory); - final MethodNode methodNode = (MethodNode) producerMethod.getNativeType(); writer.visitBeanFactoryMethod( - resolvedParent, - elementFactory.newMethodElement( - resolvedParent, - methodNode, - new AnnotationMetadataHierarchy(resolvedParent.getDeclaredMetadata(), producerMethod.getDeclaredMetadata()) - ), - getParameters() + resolvedParent, + producerMethod.withAnnotationMetadata( + new AnnotationMetadataHierarchy(resolvedParent.getDeclaredMetadata(), producerMethod.getDeclaredMetadata()) + ), + getParameters() ); return writer; } @@ -170,11 +166,11 @@ protected void annotate(AnnotationMetadata annotationMeta consumer.accept(builder); AnnotationValue av = builder.build(); final GroovyAnnotationMetadataBuilder annotationBuilder = new GroovyAnnotationMetadataBuilder( - visitorContext.getSourceUnit(), - visitorContext.getCompilationUnit()); + visitorContext.getSourceUnit(), + visitorContext.getCompilationUnit()); annotationBuilder.annotate( - annotationMetadata, - av + annotationMetadata, + av ); } } @@ -185,11 +181,11 @@ protected void annotate(AnnotationMetadata annotationMeta ArgumentUtils.requireNonNull("annotationValue", annotationValue); final GroovyAnnotationMetadataBuilder annotationBuilder = new GroovyAnnotationMetadataBuilder( - visitorContext.getSourceUnit(), - visitorContext.getCompilationUnit()); + visitorContext.getSourceUnit(), + visitorContext.getCompilationUnit()); annotationBuilder.annotate( - annotationMetadata, - annotationValue + annotationMetadata, + annotationValue ); } @@ -197,11 +193,11 @@ protected void annotate(AnnotationMetadata annotationMeta protected void removeStereotype(AnnotationMetadata annotationMetadata, String annotationType) { if (annotationMetadata != null && annotationType != null) { final GroovyAnnotationMetadataBuilder annotationBuilder = new GroovyAnnotationMetadataBuilder( - visitorContext.getSourceUnit(), - visitorContext.getCompilationUnit()); + visitorContext.getSourceUnit(), + visitorContext.getCompilationUnit()); annotationBuilder.removeStereotype( - annotationMetadata, - annotationType + annotationMetadata, + annotationType ); } } @@ -210,11 +206,11 @@ protected void removeStereotype(AnnotationMetadata annotationMetadata, String an protected void removeAnnotationIf(AnnotationMetadata annotationMetadata, Predicate> predicate) { if (annotationMetadata != null && predicate != null) { final GroovyAnnotationMetadataBuilder annotationBuilder = new GroovyAnnotationMetadataBuilder( - visitorContext.getSourceUnit(), - visitorContext.getCompilationUnit()); + visitorContext.getSourceUnit(), + visitorContext.getCompilationUnit()); annotationBuilder.removeAnnotationIf( - annotationMetadata, - predicate + annotationMetadata, + predicate ); } } @@ -223,11 +219,11 @@ protected void removeAnnotationIf(AnnotationMetadata anno protected void removeAnnotation(AnnotationMetadata annotationMetadata, String annotationType) { if (annotationMetadata != null && annotationType != null) { final GroovyAnnotationMetadataBuilder annotationBuilder = new GroovyAnnotationMetadataBuilder( - visitorContext.getSourceUnit(), - visitorContext.getCompilationUnit()); + visitorContext.getSourceUnit(), + visitorContext.getCompilationUnit()); annotationBuilder.removeAnnotation( - annotationMetadata, - annotationType + annotationMetadata, + annotationType ); } } @@ -236,7 +232,7 @@ private ClassElement resolveParent(ClassElement parentType, GroovyElementFactory Object nativeType = parentType.getNativeType(); ClassElement resolvedParent = parentType; if (nativeType instanceof ClassNode) { - resolvedParent = elementFactory.newClassElement((ClassNode) nativeType, this.getAnnotationMetadata()); + resolvedParent = elementFactory.newClassElement((ClassNode) nativeType, elementAnnotationMetadataFactory); } return resolvedParent; } @@ -244,13 +240,12 @@ private ClassElement resolveParent(ClassElement parentType, GroovyElementFactory @Override protected BeanDefinitionVisitor createAopWriter(BeanDefinitionWriter beanDefinitionWriter, AnnotationMetadata annotationMetadata) { AnnotationValue[] interceptorTypes = - InterceptedMethodUtil.resolveInterceptorBinding(annotationMetadata, InterceptorKind.AROUND); + InterceptedMethodUtil.resolveInterceptorBinding(annotationMetadata, InterceptorKind.AROUND); return new AopProxyWriter( - beanDefinitionWriter, - annotationMetadata.getValues(Around.class, Boolean.class), - ConfigurationMetadataBuilder.getConfigurationMetadataBuilder().orElse(null), - visitorContext, - interceptorTypes + beanDefinitionWriter, + annotationMetadata.getValues(Around.class, Boolean.class), + visitorContext, + interceptorTypes ); } @@ -259,10 +254,10 @@ protected BiConsumer createAroundMethodVisitor(Bean AopProxyWriter aopProxyWriter = (AopProxyWriter) aopWriter; return (bean, method) -> { AnnotationValue[] newTypes = - InterceptedMethodUtil.resolveInterceptorBinding(method.getAnnotationMetadata(), InterceptorKind.AROUND); + InterceptedMethodUtil.resolveInterceptorBinding(method.getAnnotationMetadata(), InterceptorKind.AROUND); aopProxyWriter.visitInterceptorBinding(newTypes); aopProxyWriter.visitAroundMethod( - bean, method + bean, method ); }; } diff --git a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyClassElement.java b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyClassElement.java index b76bd1abdde..f62f672569e 100644 --- a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyClassElement.java +++ b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyClassElement.java @@ -15,31 +15,74 @@ */ package io.micronaut.ast.groovy.visitor; -import io.micronaut.ast.groovy.annotation.GroovyAnnotationMetadataBuilder; -import io.micronaut.ast.groovy.utils.AstAnnotationUtils; +import groovy.lang.GroovyObject; +import groovy.lang.GroovyObjectSupport; +import groovy.lang.Script; import io.micronaut.ast.groovy.utils.AstClassUtils; import io.micronaut.ast.groovy.utils.AstGenericUtils; -import io.micronaut.ast.groovy.utils.PublicMethodVisitor; -import io.micronaut.core.annotation.*; +import io.micronaut.context.annotation.BeanProperties; +import io.micronaut.core.annotation.AnnotationMetadata; +import io.micronaut.core.annotation.AnnotationMetadataProvider; +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.NonNull; +import io.micronaut.core.annotation.Nullable; import io.micronaut.core.naming.NameUtils; import io.micronaut.core.reflect.ClassUtils; import io.micronaut.core.util.ArrayUtils; import io.micronaut.core.util.CollectionUtils; -import io.micronaut.inject.ast.*; -import org.apache.groovy.ast.tools.ClassNodeUtils; -import org.apache.groovy.util.concurrent.LazyInitializable; -import org.codehaus.groovy.ast.*; +import io.micronaut.inject.annotation.AnnotationMetadataHierarchy; +import io.micronaut.inject.ast.ArrayableClassElement; +import io.micronaut.inject.ast.BeanPropertiesQuery; +import io.micronaut.inject.ast.ClassElement; +import io.micronaut.inject.ast.ConstructorElement; +import io.micronaut.inject.ast.Element; +import io.micronaut.inject.ast.ElementAnnotationMetadataFactory; +import io.micronaut.inject.ast.ElementModifier; +import io.micronaut.inject.ast.ElementQuery; +import io.micronaut.inject.ast.FieldElement; +import io.micronaut.inject.ast.GenericPlaceholderElement; +import io.micronaut.inject.ast.MethodElement; +import io.micronaut.inject.ast.PackageElement; +import io.micronaut.inject.ast.ParameterElement; +import io.micronaut.inject.ast.PrimitiveElement; +import io.micronaut.inject.ast.PropertyElement; +import io.micronaut.inject.ast.utils.AstBeanPropertiesUtils; +import org.codehaus.groovy.ast.AnnotatedNode; +import org.codehaus.groovy.ast.ClassHelper; +import org.codehaus.groovy.ast.ClassNode; +import org.codehaus.groovy.ast.ConstructorNode; +import org.codehaus.groovy.ast.FieldNode; +import org.codehaus.groovy.ast.GenericsType; +import org.codehaus.groovy.ast.InnerClassNode; +import org.codehaus.groovy.ast.MethodNode; +import org.codehaus.groovy.ast.PackageNode; +import org.codehaus.groovy.ast.PropertyNode; import org.codehaus.groovy.ast.stmt.BlockStatement; +import org.objectweb.asm.Opcodes; -import java.lang.reflect.Field; import java.lang.reflect.Modifier; -import java.util.*; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.atomic.AtomicReference; import java.util.function.Predicate; import java.util.stream.Collectors; import java.util.stream.Stream; -import static groovyjarjarasm.asm.Opcodes.*; -import static org.codehaus.groovy.ast.ClassHelper.makeCached; +import static groovyjarjarasm.asm.Opcodes.ACC_PRIVATE; +import static groovyjarjarasm.asm.Opcodes.ACC_PROTECTED; +import static groovyjarjarasm.asm.Opcodes.ACC_PUBLIC; /** * A class element returning data from a {@link ClassNode}. @@ -54,71 +97,77 @@ public class GroovyClassElement extends AbstractGroovyElement implements Arrayab String methodName = m.getName(); return m.isStaticConstructor() || - methodName.startsWith("$") || - methodName.contains("trait$") || - methodName.startsWith("super$") || - methodName.equals("setMetaClass") || - m.getReturnType().getNameWithoutPackage().equals("MetaClass") || - m.getDeclaringClass().equals(ClassHelper.GROOVY_OBJECT_TYPE) || - m.getDeclaringClass().equals(ClassHelper.OBJECT_TYPE); + methodName.startsWith("$") || + methodName.contains("trait$") || + methodName.startsWith("super$") || + methodName.equals("setMetaClass") || + m.getReturnType().getNameWithoutPackage().equals("MetaClass") || + m.getDeclaringClass().equals(ClassHelper.GROOVY_OBJECT_TYPE) || + m.getDeclaringClass().equals(ClassHelper.OBJECT_TYPE); }; private static final Predicate JUNK_FIELD_FILTER = m -> { String fieldName = m.getName(); - return fieldName.startsWith("$") || - fieldName.startsWith("__$") || - fieldName.contains("trait$") || - fieldName.equals("metaClass") || - m.getDeclaringClass().equals(ClassHelper.GROOVY_OBJECT_TYPE) || - m.getDeclaringClass().equals(ClassHelper.OBJECT_TYPE); + return fieldName.startsWith("$") || + fieldName.startsWith("__$") || + fieldName.contains("trait$") || + fieldName.equals("metaClass") || + m.getDeclaringClass().equals(ClassHelper.GROOVY_OBJECT_TYPE) || + m.getDeclaringClass().equals(ClassHelper.OBJECT_TYPE); }; protected final ClassNode classNode; private final int arrayDimensions; private final boolean isTypeVar; private List overrideBoundGenericTypes; private Map> genericInfo; + private List properties; + private List nativeProperties; + private Map elementsMap = new HashMap<>(); + private Map resolvedTypeArguments; /** - * @param visitorContext The visitor context - * @param classNode The {@link ClassNode} - * @param annotationMetadata The annotation metadata + * @param visitorContext The visitor context + * @param classNode The {@link ClassNode} + * @param annotationMetadataFactory The annotation metadata */ - public GroovyClassElement(GroovyVisitorContext visitorContext, ClassNode classNode, AnnotationMetadata annotationMetadata) { - this(visitorContext, classNode, annotationMetadata, null, 0); + public GroovyClassElement(GroovyVisitorContext visitorContext, + ClassNode classNode, + ElementAnnotationMetadataFactory annotationMetadataFactory) { + this(visitorContext, classNode, annotationMetadataFactory, null, 0); } /** - * @param visitorContext The visitor context - * @param classNode The {@link ClassNode} - * @param annotationMetadata The annotation metadata - * @param genericInfo The generic info - * @param arrayDimensions The number of array dimensions + * @param visitorContext The visitor context + * @param classNode The {@link ClassNode} + * @param annotationMetadataFactory The annotation metadata factory + * @param genericInfo The generic info + * @param arrayDimensions The number of array dimensions */ GroovyClassElement( - GroovyVisitorContext visitorContext, - ClassNode classNode, - AnnotationMetadata annotationMetadata, - Map> genericInfo, - int arrayDimensions) { - this(visitorContext, classNode, annotationMetadata, genericInfo, arrayDimensions, false); + GroovyVisitorContext visitorContext, + ClassNode classNode, + ElementAnnotationMetadataFactory annotationMetadataFactory, + Map> genericInfo, + int arrayDimensions) { + this(visitorContext, classNode, annotationMetadataFactory, genericInfo, arrayDimensions, false); } /** - * @param visitorContext The visitor context - * @param classNode The {@link ClassNode} - * @param annotationMetadata The annotation metadata - * @param genericInfo The generic info - * @param arrayDimensions The number of array dimensions - * @param isTypeVar Is the element a type variable + * @param visitorContext The visitor context + * @param classNode The {@link ClassNode} + * @param annotationMetadataFactory The annotation metadata factory + * @param genericInfo The generic info + * @param arrayDimensions The number of array dimensions + * @param isTypeVar Is the element a type variable */ GroovyClassElement( - GroovyVisitorContext visitorContext, - ClassNode classNode, - AnnotationMetadata annotationMetadata, - Map> genericInfo, - int arrayDimensions, - boolean isTypeVar) { - super(visitorContext, classNode, annotationMetadata); + GroovyVisitorContext visitorContext, + ClassNode classNode, + ElementAnnotationMetadataFactory annotationMetadataFactory, + Map> genericInfo, + int arrayDimensions, + boolean isTypeVar) { + super(visitorContext, classNode, annotationMetadataFactory); this.classNode = classNode; this.genericInfo = genericInfo; this.arrayDimensions = arrayDimensions; @@ -128,6 +177,29 @@ public GroovyClassElement(GroovyVisitorContext visitorContext, ClassNode classNo this.isTypeVar = isTypeVar; } + @Override + protected GroovyClassElement copyConstructor() { + return new GroovyClassElement(visitorContext, classNode, elementAnnotationMetadataFactory, genericInfo, arrayDimensions, isTypeVar); + } + + @Override + protected void copyValues(AbstractGroovyElement element) { + super.copyValues(element); + ((GroovyClassElement) element).resolvedTypeArguments = resolvedTypeArguments; + } + + @Override + public ClassElement withAnnotationMetadata(AnnotationMetadata annotationMetadata) { + return (ClassElement) super.withAnnotationMetadata(annotationMetadata); + } + + @Override + public ClassElement withTypeArguments(Map typeArguments) { + GroovyClassElement groovyClassElement = new GroovyClassElement(visitorContext, classNode, elementAnnotationMetadataFactory, genericInfo, arrayDimensions); + groovyClassElement.resolvedTypeArguments = typeArguments; + return groovyClassElement; + } + @Override public boolean isTypeVariable() { return isTypeVar; @@ -135,6 +207,23 @@ public boolean isTypeVariable() { @Override public List getEnclosedElements(@NonNull ElementQuery query) { + return getEnclosedElements(query, false); + } + + /** + * This method will produce th elements just like {@link #getEnclosedElements(ElementQuery)} + * but the elements are constructed as the source ones. + * {@link io.micronaut.inject.ast.ElementFactory#newSourceMethodElement(ClassElement, Object, ElementAnnotationMetadataFactory)}. + * + * @param query The query + * @param The element type + * @return The list of elements + */ + public final List getSourceEnclosedElements(@NonNull ElementQuery query) { + return getEnclosedElements(query, true); + } + + private List getEnclosedElements(@NonNull ElementQuery query, boolean isSource) { Objects.requireNonNull(query, "Query cannot be null"); ElementQuery.Result result = query.result(); boolean onlyDeclared = result.isOnlyDeclared(); @@ -142,6 +231,27 @@ public List getEnclosedElements(@NonNull ElementQuery boolean onlyAbstract = result.isOnlyAbstract(); boolean onlyConcrete = result.isOnlyConcrete(); boolean onlyInstance = result.isOnlyInstance(); + boolean onlyStatic = result.isOnlyStatic(); + boolean excludePropertyElements = result.isExcludePropertyElements(); + Set excludeMethodNodes; + Set excludeFieldNodes; + if (excludePropertyElements) { + excludeMethodNodes = new HashSet<>(); + excludeFieldNodes = new HashSet<>(); + for (PropertyElement excludePropertyElement : getBeanProperties()) { + excludePropertyElement.getReadMethod() + .filter(m -> !m.isSynthetic()) + .ifPresent(methodElement -> excludeMethodNodes.add((AnnotatedNode) methodElement.getNativeType())); + excludePropertyElement.getWriteMethod() + .filter(m -> !m.isSynthetic()) + .ifPresent(methodElement -> excludeMethodNodes.add((AnnotatedNode) methodElement.getNativeType())); + excludePropertyElement.getField().ifPresent(fieldElement -> excludeFieldNodes.add((AnnotatedNode) fieldElement.getNativeType())); + } + } else { + excludeMethodNodes = Collections.emptySet(); + excludeFieldNodes = Collections.emptySet(); + } + List> namePredicates = result.getNamePredicates(); List> typePredicates = result.getTypePredicates(); List> annotationPredicates = result.getAnnotationPredicates(); @@ -150,74 +260,62 @@ public List getEnclosedElements(@NonNull ElementQuery List elements; Class elementType = result.getElementType(); if (elementType == MethodElement.class) { - - List methods; - Map declaredMethodsMap = classNode.getDeclaredMethodsMap(); - ClassNodeUtils.addDeclaredMethodsFromInterfaces(classNode, declaredMethodsMap); + Predicate methodNodePredicate = methodNode -> { + for (Predicate predicate : namePredicates) { + if (!predicate.test(methodNode.getName())) { + return false; + } + } + return !JUNK_METHOD_FILTER.test(methodNode); + }; + List methods; if (onlyDeclared) { - methods = new ArrayList<>(classNode.getMethods()); + methods = classNode.getMethods().stream().filter(methodNodePredicate).map(mn -> toMethodElement(mn, isSource)).collect(Collectors.toList()); } else { - methods = new ArrayList<>(classNode.getAllDeclaredMethods()); + methods = new ArrayList<>(getAllMethods(classNode, methodNodePredicate, result.isIncludeOverriddenMethods(), isSource)); } - Iterator i = methods.iterator(); + Iterator i = methods.iterator(); while (i.hasNext()) { - MethodNode methodNode = i.next(); - if (JUNK_METHOD_FILTER.test(methodNode)) { + MethodElement method = i.next(); + if (onlyAbstract && !method.isAbstract()) { i.remove(); continue; } - if (onlyAbstract && !methodNode.isAbstract()) { + if (onlyConcrete && method.isAbstract()) { i.remove(); continue; } - if (onlyConcrete && methodNode.isAbstract()) { + if (onlyInstance && method.isStatic()) { i.remove(); continue; } - if (onlyInstance && methodNode.isStatic()) { + if (onlyStatic && !method.isStatic()) { i.remove(); continue; } if (onlyAccessible) { final ClassElement accessibleFromType = result.getOnlyAccessibleFromType().orElse(this); - if (methodNode.isPrivate()) { + if (!method.isAccessible(accessibleFromType)) { i.remove(); continue; - } else if (!methodNode.getDeclaringClass().getName().equals(accessibleFromType.getName())) { - // inaccessible through package scope - if (methodNode.isPackageScope() && !methodNode.getDeclaringClass().getPackageName().equals(accessibleFromType.getPackageName())) { - i.remove(); - continue; - } } } if (!modifierPredicates.isEmpty()) { - Set elementModifiers = resolveModifiers(methodNode); + Set elementModifiers = method.getModifiers(); if (!modifierPredicates.stream().allMatch(p -> p.test(elementModifiers))) { i.remove(); continue; } } - - if (!namePredicates.isEmpty()) { - if (!namePredicates.stream().allMatch(p -> p.test(methodNode.getName()))) { - i.remove(); - } + if (excludeMethodNodes.contains(method.getNativeType())) { + i.remove(); } } - - //noinspection unchecked - elements = methods.stream().map(methodNode -> (T) new GroovyMethodElement( - this, - visitorContext, - methodNode, - AstAnnotationUtils.getAnnotationMetadata(sourceUnit, compilationUnit, methodNode) - )).collect(Collectors.toList()); - if (!typePredicates.isEmpty()) { - elements.removeIf(e -> !typePredicates.stream().allMatch(p -> p.test(((MethodElement) e).getGenericReturnType()))); + methods.removeIf(e -> !typePredicates.stream().allMatch(p -> p.test(e.getGenericReturnType()))); } + elements = (List) methods; } else if (elementType == ConstructorElement.class) { List constructors = new ArrayList<>(classNode.getDeclaredConstructors()); if (!onlyDeclared) { @@ -260,12 +358,9 @@ public List getEnclosedElements(@NonNull ElementQuery } //noinspection unchecked - elements = constructors.stream().map(constructorNode -> (T) new GroovyConstructorElement( - this, - visitorContext, - constructorNode, - AstAnnotationUtils.getAnnotationMetadata(sourceUnit, compilationUnit, constructorNode) - )).collect(Collectors.toList()); + elements = constructors.stream() + .map(constructorNode -> (T) asConstructor(constructorNode)) + .collect(Collectors.toList()); } else if (elementType == FieldElement.class) { List fields; if (onlyDeclared) { @@ -280,18 +375,15 @@ public List getEnclosedElements(@NonNull ElementQuery } fields = findRelevantFields(onlyAccessible, result.getOnlyAccessibleFromType().orElse(this), fields, namePredicates, modifierPredicates); } - //noinspection unchecked - Stream fieldStream = fields.stream(); + Stream fieldStream = fields.stream().filter(f -> !excludeFieldNodes.contains(f)); if (onlyInstance) { fieldStream = fieldStream.filter((fn) -> !fn.isStatic()); + } else if (onlyStatic) { + fieldStream = fieldStream.filter(FieldNode::isStatic); } - elements = fieldStream.map(fieldNode -> (T) new GroovyFieldElement( - visitorContext, - fieldNode, - fieldNode, - AstAnnotationUtils.getAnnotationMetadata(sourceUnit, compilationUnit, fieldNode) - )).collect(Collectors.toList()); - + elements = fieldStream + .map(fieldNode -> (T) elementsMap.computeIfAbsent(fieldNode, annotatedNode -> visitorContext.getElementFactory().newFieldElement(this, fieldNode, elementAnnotationMetadataFactory))) + .collect(Collectors.toList()); if (!typePredicates.isEmpty()) { elements.removeIf(e -> !typePredicates.stream().allMatch(p -> p.test(((FieldElement) e).getGenericField()))); } @@ -317,15 +409,12 @@ public List getEnclosedElements(@NonNull ElementQuery continue; } } - if (!namePredicates.isEmpty()) { if (!namePredicates.stream().allMatch(p -> p.test(innerClassNode.getName()))) { continue; } } - ClassElement classElement = visitorContext.getElementFactory() - .newClassElement(innerClassNode, AstAnnotationUtils.getAnnotationMetadata(sourceUnit, compilationUnit, innerClassNode)); - + ClassElement classElement = visitorContext.getElementFactory().newClassElement(innerClassNode, elementAnnotationMetadataFactory); if (!typePredicates.isEmpty()) { if (!typePredicates.stream().allMatch(p -> p.test(classElement))) { continue; @@ -342,7 +431,6 @@ public List getEnclosedElements(@NonNull ElementQuery if (!annotationPredicates.isEmpty()) { elements.removeIf(e -> !annotationPredicates.stream().allMatch(p -> p.test(e.getAnnotationMetadata()))); } - if (!elements.isEmpty() && !elementPredicates.isEmpty()) { elements.removeIf(e -> !elementPredicates.stream().allMatch(p -> p.test(e))); } @@ -351,15 +439,15 @@ public List getEnclosedElements(@NonNull ElementQuery } private List findRelevantFields( - boolean onlyAccessible, - ClassElement onlyAccessibleType, - List initialFields, - List> namePredicates, - List>> modifierPredicates) { + boolean onlyAccessible, + ClassElement onlyAccessibleType, + List initialFields, + List> namePredicates, + List>> modifierPredicates) { List filteredFields = new ArrayList<>(initialFields.size()); elementLoop: - for (FieldNode fn: initialFields) { + for (FieldNode fn : initialFields) { if (JUNK_FIELD_FILTER.test(fn)) { continue; } @@ -378,7 +466,6 @@ private List findRelevantFields( } } } - if (!namePredicates.isEmpty()) { String name = fn.getName(); for (Predicate namePredicate : namePredicates) { @@ -387,12 +474,73 @@ private List findRelevantFields( } } } - filteredFields.add(fn); } return filteredFields; } + private Collection getAllMethods(ClassNode classNode, + Predicate methodNodePredicate, + boolean includeOverriddenMethods, + boolean isSource) { + // This method will return private/package private methods that + // cannot be overridden by defining a method with the same signature + Set methods = new LinkedHashSet<>(); + Map methodElements = new HashMap<>(); + List> hierarchy = new ArrayList<>(); + collectHierarchyMethods(classNode, methodNodePredicate, hierarchy); + for (List classMethods : hierarchy) { + Set addedFromClassMethods = new LinkedHashSet<>(); + for (MethodNode methodNode : classMethods) { + MethodElement newMethod = methodElements.computeIfAbsent(methodNode, mn -> toMethodElement(mn, isSource)); + for (Iterator iterator = methods.iterator(); iterator.hasNext(); ) { + MethodElement existingMethod = iterator.next(); + if (!includeOverriddenMethods && newMethod.overrides(existingMethod)) { + iterator.remove(); + addedFromClassMethods.add(newMethod); + } + } + addedFromClassMethods.add(newMethod); + } + methods.addAll(addedFromClassMethods); + } + return methods; + } + + private static void collectHierarchyMethods(ClassNode classNode, + Predicate methodNodePredicate, + List> hierarchy) { + if (Object.class.getName().equals(classNode.getName()) + || Enum.class.getName().equals(classNode.getName()) + || GroovyObjectSupport.class.getName().equals(classNode.getName()) + || Script.class.getName().equals(classNode.getName())) { + return; + } + ClassNode parent = classNode.getSuperClass(); + if (parent != null) { + collectHierarchyMethods(parent, methodNodePredicate, hierarchy); + } + for (ClassNode iface : classNode.getInterfaces()) { + if (iface.getName().equals(GroovyObject.class.getName())) { + continue; + } + List> interfaceMethods = new ArrayList<>(); + collectHierarchyMethods(iface, methodNodePredicate, interfaceMethods); + interfaceMethods.forEach(methodNodes -> methodNodes.removeIf(methodNode -> (methodNode.getModifiers() & Opcodes.ACC_SYNTHETIC) != 0)); + hierarchy.addAll(interfaceMethods); + } + hierarchy.add(classNode.getMethods().stream().filter(methodNodePredicate).collect(Collectors.toList())); + } + + private GroovyMethodElement toMethodElement(MethodNode methodNode, boolean isSource) { + return (GroovyMethodElement) elementsMap.computeIfAbsent(methodNode, annotatedNode -> { + if (isSource) { + return visitorContext.getElementFactory().newSourceMethodElement(this, methodNode, elementAnnotationMetadataFactory); + } + return visitorContext.getElementFactory().newMethodElement(this, methodNode, elementAnnotationMetadataFactory); + }); + } + private boolean isPackageScope(FieldNode fn) { return (fn.getModifiers() & (ACC_PUBLIC | ACC_PRIVATE | ACC_PROTECTED)) == 0; } @@ -410,20 +558,7 @@ public boolean isInner() { @Override public Optional getEnclosingType() { if (isInner()) { - ClassNode outerClass = classNode.getOuterClass(); - if (outerClass != null) { - return Optional.of( - visitorContext.getElementFactory() - .newClassElement( - outerClass, - AstAnnotationUtils.getAnnotationMetadata( - sourceUnit, - compilationUnit, - outerClass - ) - ) - ); - } + return Optional.ofNullable(classNode.getOuterClass()).map(this::toGroovyClassElement); } return Optional.empty(); } @@ -442,10 +577,7 @@ public boolean isPrimitive() { public Collection getInterfaces() { final ClassNode[] interfaces = classNode.getInterfaces(); if (ArrayUtils.isNotEmpty(interfaces)) { - return Arrays.stream(interfaces).map((cn) -> visitorContext.getElementFactory().newClassElement( - cn, - AstAnnotationUtils.getAnnotationMetadata(sourceUnit, compilationUnit, cn) - )).collect(Collectors.toList()); + return Arrays.stream(interfaces).map(this::toGroovyClassElement).collect(Collectors.toList()); } return Collections.emptyList(); } @@ -455,44 +587,50 @@ public Optional getSuperType() { final ClassNode superClass = classNode.getUnresolvedSuperClass(false); if (superClass != null && !superClass.equals(ClassHelper.OBJECT_TYPE)) { return Optional.of( - visitorContext.getElementFactory().newClassElement( - superClass, - AstAnnotationUtils.getAnnotationMetadata(sourceUnit, compilationUnit, superClass) - ) + toGroovyClassElement(superClass) ); } return Optional.empty(); } + private ClassElement toGroovyClassElement(ClassNode superClass) { + return visitorContext.getElementFactory().newClassElement(superClass, elementAnnotationMetadataFactory); + } + @NonNull @Override public Optional getPrimaryConstructor() { - MethodNode method = findStaticCreator(); - if (method == null) { - method = findConcreteConstructor(); + Optional primaryConstructor = ArrayableClassElement.super.getPrimaryConstructor(); + if (primaryConstructor.isPresent()) { + return primaryConstructor; } - - return createMethodElement(method); + return possibleDefaultEmptyConstructor(); } - @NonNull @Override public Optional getDefaultConstructor() { - MethodNode method = findDefaultStaticCreator(); - if (method == null) { - method = findDefaultConstructor(); + Optional defaultConstructor = ArrayableClassElement.super.getDefaultConstructor(); + if (defaultConstructor.isPresent()) { + return defaultConstructor; } - return createMethodElement(method); + return possibleDefaultEmptyConstructor(); + } + + private Optional possibleDefaultEmptyConstructor() { + List constructors = classNode.getDeclaredConstructors(); + if (CollectionUtils.isEmpty(constructors) && !classNode.isAbstract() && !classNode.isEnum()) { + // empty default constructor + return createMethodElement(new ConstructorNode(Modifier.PUBLIC, new BlockStatement())); + } + return Optional.empty(); } private Optional createMethodElement(MethodNode method) { return Optional.ofNullable(method).map(executableElement -> { - - final AnnotationMetadata annotationMetadata = AstAnnotationUtils.getAnnotationMetadata(sourceUnit, compilationUnit, executableElement); if (executableElement instanceof ConstructorNode) { - return new GroovyConstructorElement(this, visitorContext, (ConstructorNode) executableElement, annotationMetadata); + return visitorContext.getElementFactory().newConstructorElement(this, executableElement, elementAnnotationMetadataFactory); } else { - return new GroovyMethodElement(this, visitorContext, executableElement, annotationMetadata); + return visitorContext.getElementFactory().newMethodElement(this, executableElement, elementAnnotationMetadataFactory); } }); } @@ -513,16 +651,14 @@ public Map> getGenericTypeInfo() { @Override public Map getTypeArguments(@NonNull String type) { Map> allData = getGenericTypeInfo(); - Map thisSpec = allData.get(getName()); Map forType = allData.get(type); if (forType != null) { Map typeArgs = new LinkedHashMap<>(forType.size()); for (Map.Entry entry : forType.entrySet()) { ClassNode classNode = entry.getValue(); - - AnnotationMetadata annotationMetadata = resolveAnnotationMetadata(classNode); - ClassElement rawElement = visitorContext.getElementFactory().newClassElement(classNode, annotationMetadata); + ClassElement rawElement = new GroovyClassElement(visitorContext, classNode, elementAnnotationMetadataFactory); + rawElement = adjustTypeAnnotationMetadata(rawElement); if (thisSpec != null) { rawElement = getGenericElement(sourceUnit, classNode, rawElement, thisSpec); } @@ -530,7 +666,6 @@ public Map getTypeArguments(@NonNull String type) { } return Collections.unmodifiableMap(typeArgs); } - return Collections.emptyMap(); } @@ -538,14 +673,15 @@ public Map getTypeArguments(@NonNull String type) { @Override public Map> getAllTypeArguments() { Map> genericInfo = - AstGenericUtils.buildAllGenericElementInfo(classNode, new GroovyVisitorContext(sourceUnit, compilationUnit)); + AstGenericUtils.buildAllGenericElementInfo(classNode, new GroovyVisitorContext(sourceUnit, compilationUnit)); Map> results = new LinkedHashMap<>(genericInfo.size()); genericInfo.forEach((name, generics) -> { Map resolved = new LinkedHashMap<>(generics.size()); generics.forEach((variable, type) -> { - AnnotationMetadata annotationMetadata = resolveAnnotationMetadata(type); - resolved.put(variable, new GroovyClassElement(visitorContext, type, annotationMetadata)); + ClassElement classElement = new GroovyClassElement(visitorContext, type, elementAnnotationMetadataFactory); + classElement = adjustTypeAnnotationMetadata(classElement); + resolved.put(variable, classElement); }); results.put(name, resolved); }); @@ -554,11 +690,14 @@ public Map> getAllTypeArguments() { } @Override - public @NonNull - Map getTypeArguments() { - Map> genericInfo = getGenericTypeInfo(); - Map info = genericInfo.get(classNode.getName()); - return resolveGenericMap(info); + @NonNull + public Map getTypeArguments() { + if (resolvedTypeArguments == null) { + Map> genericInfo = getGenericTypeInfo(); + Map info = genericInfo.get(classNode.getName()); + resolvedTypeArguments = resolveGenericMap(info); + } + return resolvedTypeArguments; } @NonNull @@ -575,15 +714,14 @@ private Map resolveGenericMap(Map info) ClassNode cn = resolveTypeArgument(info, redirectType.getName()); if (cn != null) { Map newInfo = alignNewGenericsInfo(genericsTypes, redirectTypes, info); - AnnotationMetadata annotationMetadata = resolveAnnotationMetadata(cn); - typeArgumentMap.put(redirectType.getName(), new GroovyClassElement( - visitorContext, - cn, - annotationMetadata, - Collections.singletonMap(cn.getName(), newInfo), - cn.isArray() ? computeDimensions(cn) : 0, - true - )); + typeArgumentMap.put(redirectType.getName(), adjustTypeAnnotationMetadata(new GroovyClassElement( + visitorContext, + cn, + elementAnnotationMetadataFactory, + Collections.singletonMap(cn.getName(), newInfo), + cn.isArray() ? computeDimensions(cn) : 0, + true + ))); } } else { ClassNode type; @@ -594,18 +732,16 @@ private Map resolveGenericMap(Map info) } else { type = gt.getType(); } - AnnotationMetadata annotationMetadata = resolveAnnotationMetadata(type); - typeArgumentMap.put(redirectType.getName(), new GroovyClassElement( - visitorContext, - type, - annotationMetadata, - Collections.emptyMap(), - type.isArray() ? computeDimensions(type) : 0 - )); + typeArgumentMap.put(redirectType.getName(), adjustTypeAnnotationMetadata(new GroovyClassElement( + visitorContext, + type, + elementAnnotationMetadataFactory, + Collections.emptyMap(), + type.isArray() ? computeDimensions(type) : 0 + ))); } } } else if (redirectTypes != null) { - for (GenericsType gt : redirectTypes) { String name = gt.getName(); ClassNode cn = resolveTypeArgument(info, name); @@ -614,14 +750,13 @@ private Map resolveGenericMap(Map info) if (genericsTypes != null) { newInfo = alignNewGenericsInfo(genericsTypes, redirectTypes, info); } - AnnotationMetadata annotationMetadata = resolveAnnotationMetadata(cn); - typeArgumentMap.put(gt.getName(), new GroovyClassElement( - visitorContext, - cn, - annotationMetadata, - Collections.singletonMap(cn.getName(), newInfo), - cn.isArray() ? computeDimensions(cn) : 0 - )); + typeArgumentMap.put(gt.getName(), adjustTypeAnnotationMetadata(new GroovyClassElement( + visitorContext, + cn, + elementAnnotationMetadataFactory, + Collections.singletonMap(cn.getName(), newInfo), + cn.isArray() ? computeDimensions(cn) : 0 + ))); } } } @@ -634,8 +769,8 @@ private Map resolveGenericMap(Map info) Map map = new LinkedHashMap<>(spec.size()); for (Map.Entry entry : spec.entrySet()) { ClassNode cn = entry.getValue(); - AnnotationMetadata annotationMetadata = resolveAnnotationMetadata(cn); - ClassElement classElement = visitorContext.getElementFactory().newClassElement(cn, annotationMetadata); + ClassElement classElement = visitorContext.getElementFactory().newClassElement(cn, elementAnnotationMetadataFactory); + classElement = adjustTypeAnnotationMetadata(classElement); map.put(entry.getKey(), classElement); } return Collections.unmodifiableMap(map); @@ -667,13 +802,127 @@ private int computeDimensions(ClassNode cn) { return i; } + @Override + public List getSyntheticBeanProperties() { + // Native properties should be composed of field + synthetic getter/setter + if (nativeProperties == null) { + BeanPropertiesQuery configuration = new BeanPropertiesQuery(); + configuration.setAllowStaticProperties(true); + Set nativeProps = getPropertyNodes().stream().map(PropertyNode::getName).collect(Collectors.toCollection(LinkedHashSet::new)); + nativeProperties = AstBeanPropertiesUtils.resolveBeanProperties(configuration, + this, + () -> AstBeanPropertiesUtils.getSubtypeFirstMethods(this), + () -> AstBeanPropertiesUtils.getSubtypeFirstFields(this), + true, + nativeProps, + methodElement -> Optional.empty(), + methodElement -> Optional.empty(), + value -> mapPropertyElement(nativeProps, value, configuration, true)); + } + return nativeProperties; + } + + @Override + public List getBeanProperties(BeanPropertiesQuery beanPropertiesQuery) { + Set nativeProps = getPropertyNodes().stream().map(PropertyNode::getName).collect(Collectors.toCollection(LinkedHashSet::new)); + return AstBeanPropertiesUtils.resolveBeanProperties(beanPropertiesQuery, + this, + () -> AstBeanPropertiesUtils.getSubtypeFirstMethods(this), + () -> AstBeanPropertiesUtils.getSubtypeFirstFields(this), + true, + nativeProps, + methodElement -> Optional.empty(), + methodElement -> Optional.empty(), + value -> mapPropertyElement(nativeProps, value, beanPropertiesQuery, false)); + } + @Override public List getBeanProperties() { - List allProperties = new ArrayList<>(); - List propertyElements = getPropertyNodes(); - allProperties.addAll(propertyElements); - allProperties.addAll(getPropertiesFromGettersAndSetters(propertyElements)); - return Collections.unmodifiableList(allProperties); + if (properties == null) { + properties = getBeanProperties(BeanPropertiesQuery.of(this)); + } + return properties; + } + + private GroovyPropertyElement mapPropertyElement(Set nativeProps, + AstBeanPropertiesUtils.BeanPropertyData value, + BeanPropertiesQuery conf, + boolean nativePropertiesOnly) { + if (value.type == null) { + // withSomething() builder setter + value.type = PrimitiveElement.VOID; + } + AtomicReference ref = new AtomicReference<>(); + if (conf.getAccessKinds().contains(BeanProperties.AccessKind.METHOD) && nativeProps.remove(value.propertyName)) { + AnnotationMetadataProvider annotationMetadataProvider = new AnnotationMetadataProvider() { + @Override + public AnnotationMetadata getAnnotationMetadata() { + return new AnnotationMetadataHierarchy(GroovyClassElement.this, ref.get().getAnnotationMetadata()); + } + }; + if (value.readAccessKind != BeanProperties.AccessKind.METHOD) { + String getterName = NameUtils.getterNameFor( + value.propertyName, + value.type.equals(PrimitiveElement.BOOLEAN) + ); + value.getter = MethodElement.of( + this, + value.field.getDeclaringType(), + annotationMetadataProvider, + visitorContext.getAnnotationMetadataBuilder(), + value.field.getGenericType(), + value.field.getGenericType(), + getterName, + value.field.isStatic(), + value.field.isFinal() + ); + value.readAccessKind = BeanProperties.AccessKind.METHOD; + } else if (nativePropertiesOnly) { + value.getter = null; + value.readAccessKind = null; + } + if (!value.field.isFinal() && value.writeAccessKind != BeanProperties.AccessKind.METHOD) { + value.setter = MethodElement.of( + this, + value.field.getDeclaringType(), + annotationMetadataProvider, + visitorContext.getAnnotationMetadataBuilder(), + PrimitiveElement.VOID, + PrimitiveElement.VOID, + NameUtils.setterNameFor(value.propertyName), + value.field.isStatic(), + value.field.isFinal(), + ParameterElement.of(value.field.getGenericType(), value.propertyName, annotationMetadataProvider, visitorContext.getAnnotationMetadataBuilder()) + ); + value.writeAccessKind = BeanProperties.AccessKind.METHOD; + } else if (nativePropertiesOnly) { + value.setter = null; + value.writeAccessKind = null; + } + } else if (nativePropertiesOnly) { + return null; + } + // Skip not accessible setters / getters + if (value.writeAccessKind != BeanProperties.AccessKind.METHOD) { + value.setter = null; + } + if (value.readAccessKind != BeanProperties.AccessKind.METHOD) { + value.getter = null; + } + GroovyPropertyElement propertyElement = new GroovyPropertyElement( + visitorContext, + this, + value.type, + value.getter, + value.setter, + value.field, + elementAnnotationMetadataFactory, + value.propertyName, + value.readAccessKind == null ? PropertyElement.AccessKind.METHOD : PropertyElement.AccessKind.valueOf(value.readAccessKind.name()), + value.writeAccessKind == null ? PropertyElement.AccessKind.METHOD : PropertyElement.AccessKind.valueOf(value.writeAccessKind.name()), + value.isExcluded); + ref.set(propertyElement); + return propertyElement; } @Override @@ -683,7 +932,7 @@ public boolean isArray() { @Override public ClassElement withArrayDimensions(int arrayDimensions) { - return new GroovyClassElement(visitorContext, classNode, getAnnotationMetadata(), getGenericTypeInfo(), arrayDimensions); + return new GroovyClassElement(visitorContext, classNode, elementAnnotationMetadataFactory, getGenericTypeInfo(), arrayDimensions); } @Override @@ -715,15 +964,10 @@ public String getPackageName() { public PackageElement getPackage() { final PackageNode aPackage = classNode.getPackage(); if (aPackage != null) { - return new GroovyPackageElement( - visitorContext, - aPackage, - AstAnnotationUtils.getAnnotationMetadata( - sourceUnit, - compilationUnit, - aPackage - ) + visitorContext, + aPackage, + elementAnnotationMetadataFactory ); } else { return PackageElement.DEFAULT_PACKAGE; @@ -737,7 +981,9 @@ public boolean isAbstract() { @Override public boolean isStatic() { - return classNode.isStaticClass(); + // I assume Groovy can decide not to make the class static internally + // and isStaticClass will be false even if the class has static modifier + return classNode.isStaticClass() || Modifier.isStatic(classNode.getModifiers()); } @Override @@ -761,7 +1007,7 @@ public boolean isProtected() { } @Override - public Object getNativeType() { + public ClassNode getNativeType() { return classNode; } @@ -791,31 +1037,32 @@ private List getBoundGenericTypes(ClassNode classNode) { return Collections.emptyList(); } else { return Arrays.stream(genericsTypes) - .map(cn -> { - if (cn.isWildcard()) { - List upperBounds; - if (cn.getUpperBounds() != null && cn.getUpperBounds().length > 0) { - upperBounds = Arrays.stream(cn.getUpperBounds()) - .map(bound -> (GroovyClassElement) toClassElement(bound)) - .collect(Collectors.toList()); - } else { - upperBounds = Collections.singletonList((GroovyClassElement) visitorContext.getClassElement(Object.class).get()); - } - List lowerBounds; - if (cn.getLowerBound() == null) { - lowerBounds = Collections.emptyList(); - } else { - lowerBounds = Collections.singletonList((GroovyClassElement) toClassElement(cn.getLowerBound())); - } - return new GroovyWildcardElement( - upperBounds, - lowerBounds - ); + .map(cn -> { + if (cn.isWildcard()) { + List upperBounds; + if (cn.getUpperBounds() != null && cn.getUpperBounds().length > 0) { + upperBounds = Arrays.stream(cn.getUpperBounds()) + .map(bound -> (GroovyClassElement) toClassElement(bound)) + .collect(Collectors.toList()); } else { - return toClassElement(cn.getType()); + upperBounds = Collections.singletonList((GroovyClassElement) visitorContext.getClassElement(Object.class).get()); } - }) - .collect(Collectors.toList()); + List lowerBounds; + if (cn.getLowerBound() == null) { + lowerBounds = Collections.emptyList(); + } else { + lowerBounds = Collections.singletonList((GroovyClassElement) toClassElement(cn.getLowerBound())); + } + return new GroovyWildcardElement( + upperBounds, + lowerBounds, + elementAnnotationMetadataFactory + ); + } else { + return toClassElement(cn.getType()); + } + }) + .collect(Collectors.toList()); } } @@ -827,406 +1074,38 @@ public List getDeclaredGenericPlaceholders( } protected final ClassElement toClassElement(ClassNode classNode) { - return visitorContext.getElementFactory().newClassElement(classNode, AnnotationMetadata.EMPTY_METADATA); + return visitorContext.getElementFactory().newClassElement(classNode, elementAnnotationMetadataFactory) + .withAnnotationMetadata(AnnotationMetadata.EMPTY_METADATA); } @NonNull @Override - public ClassElement withBoundGenericTypes(@NonNull List typeArguments) { + public ClassElement withBoundGenericTypes(@NonNull List typeArguments) { // we can't create a new ClassNode, so we have to go this route. - GroovyClassElement copy = (GroovyClassElement) visitorContext.getElementFactory().newClassElement(classNode, getAnnotationMetadata()); + GroovyClassElement copy = new GroovyClassElement(visitorContext, classNode, elementAnnotationMetadataFactory); copy.overrideBoundGenericTypes = typeArguments; return copy; } - private List getPropertyNodes() { + private List getPropertyNodes() { List propertyNodes = new ArrayList<>(); ClassNode classNode = this.classNode; while (classNode != null && !classNode.equals(ClassHelper.OBJECT_TYPE) && !classNode.equals(ClassHelper.Enum_Type)) { propertyNodes.addAll(classNode.getProperties()); classNode = classNode.getSuperClass(); } - List propertyElements = new ArrayList<>(); + List propertyElements = new ArrayList<>(); for (PropertyNode propertyNode : propertyNodes) { - if (propertyNode.isPublic() && !propertyNode.isStatic()) { - final String propertyName = propertyNode.getName(); - boolean readOnly = propertyNode.getField().isFinal(); - final AnnotationMetadata annotationMetadata = - AstAnnotationUtils.getAnnotationMetadata(sourceUnit, compilationUnit, propertyNode.getField()); - GroovyPropertyElement groovyPropertyElement = new GroovyPropertyElement( - visitorContext, - this, - propertyNode.getField(), - annotationMetadata, - propertyName, - readOnly, - propertyNode - ) { - - final String[] readPrefixes = GroovyClassElement.this.getValue(AccessorsStyle.class, "readPrefixes", String[].class) - .orElse(new String[]{AccessorsStyle.DEFAULT_READ_PREFIX}); - final String[] writePrefixes = GroovyClassElement.this.getValue(AccessorsStyle.class, "writePrefixes", String[].class) - .orElse(new String[]{AccessorsStyle.DEFAULT_WRITE_PREFIX}); - - @NonNull - @Override - public ClassElement getType() { - ClassNode type = propertyNode.getType(); - return visitorContext.getElementFactory().newClassElement(type, - AstAnnotationUtils.getAnnotationMetadata(sourceUnit, compilationUnit, type)); - } - - @Override - public Optional getWriteMethod() { - if (!readOnly) { - return Optional.of(MethodElement.of( - GroovyClassElement.this, - annotationMetadata, - PrimitiveElement.VOID, - PrimitiveElement.VOID, - NameUtils.setterNameFor(propertyName, writePrefixes), - ParameterElement.of(getType(), propertyName) - - )); - } - return Optional.empty(); - } - - @Override - public Optional getReadMethod() { - return Optional.of(MethodElement.of( - GroovyClassElement.this, - annotationMetadata, - getType(), - getGenericType(), - getGetterName(propertyName, getType()) - )); - } - - private String getGetterName(String propertyName, ClassElement type) { - return NameUtils.getterNameFor( - propertyName, - type.equals(PrimitiveElement.BOOLEAN) - ); - } - }; - propertyElements.add(groovyPropertyElement); + if (propertyNode.isPublic()) { + propertyElements.add(propertyNode); } } return propertyElements; } - private List getPropertiesFromGettersAndSetters(List propertyNodes) { - Set groovyProps = propertyNodes.stream().map(PropertyElement::getName).collect(Collectors.toSet()); - Map props = new LinkedHashMap<>(); - ClassNode classNode = this.classNode; - while (classNode != null && !classNode.equals(ClassHelper.OBJECT_TYPE) && !classNode.equals(ClassHelper.Enum_Type)) { - - ClassNode finalClassNode = classNode; - classNode.visitContents( - new PublicMethodVisitor(null) { - - final String[] readPrefixes = getValue(AccessorsStyle.class, "readPrefixes", String[].class) - .orElse(new String[]{AccessorsStyle.DEFAULT_READ_PREFIX}); - final String[] writePrefixes = getValue(AccessorsStyle.class, "writePrefixes", String[].class) - .orElse(new String[]{AccessorsStyle.DEFAULT_WRITE_PREFIX}); - - @Override - protected boolean isAcceptable(MethodNode node) { - boolean validModifiers = node.isPublic() && !node.isStatic() && !node.isSynthetic() && (!node.isAbstract() || finalClassNode.isInterface()); - if (validModifiers) { - String methodName = node.getName(); - if (methodName.contains("$") || methodName.equals("getMetaClass")) { - return false; - } - - if (NameUtils.isReaderName(methodName, readPrefixes) && node.getParameters().length == 0) { - return true; - } else { - return NameUtils.isWriterName(methodName, writePrefixes) && node.getParameters().length == 1; - } - } - return validModifiers; - } - - @Override - public void accept(ClassNode classNode, MethodNode node) { - String methodName = node.getName(); - final ClassNode declaringTypeElement = node.getDeclaringClass(); - if (NameUtils.isReaderName(methodName, readPrefixes) && node.getParameters().length == 0) { - String propertyName = NameUtils.getPropertyNameForGetter(methodName, readPrefixes); - if (groovyProps.contains(propertyName)) { - return; - } - ClassNode returnTypeNode = node.getReturnType(); - ClassElement getterReturnType = null; - if (returnTypeNode.isGenericsPlaceHolder()) { - final String placeHolderName = returnTypeNode.getUnresolvedName(); - final ClassElement classElement = getTypeArguments().get(placeHolderName); - if (classElement != null) { - getterReturnType = classElement; - } - } - if (getterReturnType == null) { - getterReturnType = visitorContext.getElementFactory().newClassElement(returnTypeNode, AstAnnotationUtils.getAnnotationMetadata(sourceUnit, compilationUnit, returnTypeNode)); - } - - GetterAndSetter getterAndSetter = props.computeIfAbsent(propertyName, GetterAndSetter::new); - configureDeclaringType(declaringTypeElement, getterAndSetter); - getterAndSetter.type = getterReturnType; - getterAndSetter.getter = node; - if (getterAndSetter.setter != null) { - ClassNode typeMirror = getterAndSetter.setter.getParameters()[0].getType(); - ClassElement setterParameterType = visitorContext.getElementFactory().newClassElement(typeMirror, AnnotationMetadata.EMPTY_METADATA); - if (!setterParameterType.getName().equals(getterReturnType.getName())) { - getterAndSetter.setter = null; // not a compatible setter - } - } - } else if (NameUtils.isWriterName(methodName, writePrefixes) && node.getParameters().length == 1) { - String propertyName = NameUtils.getPropertyNameForSetter(methodName, writePrefixes); - if (groovyProps.contains(propertyName)) { - return; - } - ClassNode typeMirror = node.getParameters()[0].getType(); - ClassElement setterParameterType = visitorContext.getElementFactory().newClassElement(typeMirror, AnnotationMetadata.EMPTY_METADATA); - - GetterAndSetter getterAndSetter = props.computeIfAbsent(propertyName, GetterAndSetter::new); - configureDeclaringType(declaringTypeElement, getterAndSetter); - ClassElement propertyType = getterAndSetter.type; - if (propertyType != null) { - if (propertyType.getName().equals(setterParameterType.getName())) { - getterAndSetter.setter = node; - } - } else { - getterAndSetter.setter = node; - } - } - } - - private void configureDeclaringType(ClassNode declaringTypeElement, GetterAndSetter beanPropertyData) { - if (beanPropertyData.declaringType == null) { - if (GroovyClassElement.this.classNode.equals(declaringTypeElement)) { - beanPropertyData.declaringType = GroovyClassElement.this; - } else { - beanPropertyData.declaringType = new GroovyClassElement( - visitorContext, - declaringTypeElement, - AstAnnotationUtils.getAnnotationMetadata(sourceUnit, compilationUnit, declaringTypeElement) - ); - } - } - } - }); - classNode = classNode.getSuperClass(); - } - List propertyElements = new ArrayList<>(props.size()); - if (!props.isEmpty()) { - GroovyClassElement thisElement = this; - for (Map.Entry entry : props.entrySet()) { - String propertyName = entry.getKey(); - GetterAndSetter value = entry.getValue(); - if (value.getter != null) { - - final AnnotationMetadata annotationMetadata; - final GroovyAnnotationMetadataBuilder groovyAnnotationMetadataBuilder = new GroovyAnnotationMetadataBuilder(sourceUnit, compilationUnit); - FieldNode field = value.declaringType.classNode.getField(propertyName); - if (field instanceof LazyInitializable) { - //this nonsense is to work around https://issues.apache.org/jira/browse/GROOVY-10398 - ((LazyInitializable) field).lazyInit(); - try { - Field delegate = field.getClass().getDeclaredField("delegate"); - delegate.setAccessible(true); - field = (FieldNode) delegate.get(field); - } catch (NoSuchFieldException | IllegalAccessException e) { - // no op - } - } - final List parents = new ArrayList<>(); - if (field != null) { - parents.add(field); - } - if (value.setter != null) { - parents.add(value.setter); - } - if (!parents.isEmpty()) { - annotationMetadata = AstAnnotationUtils.getAnnotationMetadata(sourceUnit, compilationUnit, parents, value.getter); - } else { - annotationMetadata = groovyAnnotationMetadataBuilder.buildForMethod(value.getter); - } - GroovyPropertyElement propertyElement = new GroovyPropertyElement( - visitorContext, - value.declaringType, - value.getter, - annotationMetadata, - propertyName, - value.setter == null, - value.getter) { - @Override - public Optional getWriteMethod() { - if (value.setter != null) { - return Optional.of(new GroovyMethodElement( - thisElement, - visitorContext, - value.setter, - groovyAnnotationMetadataBuilder.buildForMethod(value.setter) - )); - } - return Optional.empty(); - } - - @NonNull - @Override - public ClassElement getType() { - return value.type; - } - - @Override - public Optional getReadMethod() { - return Optional.of(new GroovyMethodElement(thisElement, visitorContext, value.getter, annotationMetadata)); - } - }; - propertyElements.add(propertyElement); - } - } - } - return propertyElements; + private MethodElement asConstructor(ConstructorNode cn) { + return visitorContext.getElementFactory().newConstructorElement(this, cn, elementAnnotationMetadataFactory); } - private MethodNode findConcreteConstructor() { - List constructors = classNode.getDeclaredConstructors(); - if (CollectionUtils.isEmpty(constructors) && !classNode.isAbstract() && !classNode.isEnum()) { - return new ConstructorNode(Modifier.PUBLIC, new BlockStatement()); // empty default constructor - } - - List nonPrivateConstructors = findNonPrivateMethods(constructors); - - MethodNode methodNode; - if (nonPrivateConstructors.size() == 1) { - methodNode = nonPrivateConstructors.get(0); - } else { - methodNode = nonPrivateConstructors.stream() - .filter(cn -> { - AnnotationMetadata annotationMetadata = AstAnnotationUtils.getAnnotationMetadata(sourceUnit, compilationUnit, cn); - return annotationMetadata.hasAnnotation(AnnotationUtil.INJECT) || - annotationMetadata.hasAnnotation(Creator.class); - }) - .findFirst().orElse(null); - if (methodNode == null) { - methodNode = nonPrivateConstructors.stream().filter(cn -> Modifier.isPublic(cn.getModifiers())).findFirst().orElse(null); - } - } - return methodNode; - } - - private MethodNode findDefaultConstructor() { - List constructors = classNode.getDeclaredConstructors(); - if (CollectionUtils.isEmpty(constructors) && !classNode.isEnum()) { - return new ConstructorNode(Modifier.PUBLIC, new BlockStatement()); // empty default constructor - } - - constructors = findNonPrivateMethods(constructors).stream() - .filter(ctor -> ctor.getParameters().length == 0) - .collect(Collectors.toList()); - - if (constructors.isEmpty()) { - return null; - } - - if (constructors.size() == 1) { - return constructors.get(0); - } - - return constructors.stream() - .filter(method -> Modifier.isPublic(method.getModifiers())) - .findFirst().orElse(null); - } - - private MethodNode findStaticCreator() { - List creators = findNonPrivateStaticCreators(); - - if (creators.isEmpty()) { - return null; - } - if (creators.size() == 1) { - return creators.get(0); - } - - //Can be multiple static @Creator methods. Prefer one with args here. The no arg method (if present) will - //be picked up by staticDefaultCreatorFor - List withArgs = creators.stream() - .filter(method -> method.getParameters().length > 0) - .collect(Collectors.toList()); - - if (withArgs.size() == 1) { - return withArgs.get(0); - } else { - creators = withArgs; - } - - return creators.stream() - .filter(method -> Modifier.isPublic(method.getModifiers())) - .findFirst().orElse(null); - } - - private MethodNode findDefaultStaticCreator() { - List creators = findNonPrivateStaticCreators().stream() - .filter(ctor -> ctor.getParameters().length == 0) - .collect(Collectors.toList()); - - if (creators.isEmpty()) { - return null; - } - - if (creators.size() == 1) { - return creators.get(0); - } - - return creators.stream() - .filter(method -> Modifier.isPublic(method.getModifiers())) - .findFirst().orElse(null); - } - - private List findNonPrivateMethods(List methodNodes) { - List nonPrivateMethods = new ArrayList<>(2); - for (MethodNode node : methodNodes) { - if (!Modifier.isPrivate(node.getModifiers())) { - nonPrivateMethods.add((T) node); - } - } - return nonPrivateMethods; - } - - private List findNonPrivateStaticCreators() { - List creators = classNode.getAllDeclaredMethods().stream() - .filter(method -> Modifier.isStatic(method.getModifiers())) - .filter(method -> !Modifier.isPrivate(method.getModifiers())) - .filter(method -> method.getReturnType().equals(classNode)) - .filter(method -> method.getAnnotations(makeCached(Creator.class)).size() > 0) - .collect(Collectors.toList()); - - if (creators.isEmpty() && classNode.isEnum()) { - creators = classNode.getAllDeclaredMethods().stream() - .filter(method -> Modifier.isStatic(method.getModifiers())) - .filter(method -> !Modifier.isPrivate(method.getModifiers())) - .filter(method -> method.getReturnType().equals(classNode)) - .filter(method -> method.getName().equals("valueOf")) - .collect(Collectors.toList()); - } - return creators; - } - - /** - * Internal holder class for getters and setters. - */ - private class GetterAndSetter { - ClassElement type; - GroovyClassElement declaringType; - MethodNode getter; - MethodNode setter; - final String propertyName; - - GetterAndSetter(String propertyName) { - this.propertyName = propertyName; - } - } } diff --git a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyConstructorElement.java b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyConstructorElement.java index e4191cfbf0b..161421f85a7 100644 --- a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyConstructorElement.java +++ b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyConstructorElement.java @@ -17,6 +17,7 @@ import io.micronaut.core.annotation.AnnotationMetadata; import io.micronaut.inject.ast.ConstructorElement; +import io.micronaut.inject.ast.ElementAnnotationMetadataFactory; import org.codehaus.groovy.ast.ConstructorNode; /** @@ -27,12 +28,25 @@ */ public class GroovyConstructorElement extends GroovyMethodElement implements ConstructorElement { /** - * @param declaringClass The declaring class - * @param visitorContext The visitor context - * @param methodNode The {@link ConstructorNode} - * @param annotationMetadata The annotation metadata + * @param owningType The owning class + * @param visitorContext The visitor context + * @param methodNode The {@link ConstructorNode} + * @param annotationMetadataFactory The annotation metadata */ - GroovyConstructorElement(GroovyClassElement declaringClass, GroovyVisitorContext visitorContext, ConstructorNode methodNode, AnnotationMetadata annotationMetadata) { - super(declaringClass, visitorContext, methodNode, annotationMetadata); + GroovyConstructorElement(GroovyClassElement owningType, + GroovyVisitorContext visitorContext, + ConstructorNode methodNode, + ElementAnnotationMetadataFactory annotationMetadataFactory) { + super(owningType, visitorContext, methodNode, annotationMetadataFactory); + } + + @Override + protected AbstractGroovyElement copyConstructor() { + return new GroovyConstructorElement(getOwningType(), visitorContext, (ConstructorNode) getNativeType(), elementAnnotationMetadataFactory); + } + + @Override + public ConstructorElement withAnnotationMetadata(AnnotationMetadata annotationMetadata) { + return (ConstructorElement) super.withAnnotationMetadata(annotationMetadata); } } diff --git a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyElementFactory.java b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyElementFactory.java index cc32a69531a..28b50a8f14b 100644 --- a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyElementFactory.java +++ b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyElementFactory.java @@ -15,15 +15,21 @@ */ package io.micronaut.ast.groovy.visitor; -import io.micronaut.ast.groovy.config.GroovyConfigurationMetadataBuilder; import io.micronaut.core.annotation.NonNull; -import io.micronaut.core.annotation.Nullable; -import io.micronaut.core.annotation.AnnotationMetadata; -import io.micronaut.inject.ast.*; +import io.micronaut.inject.ast.ClassElement; +import io.micronaut.inject.ast.ConstructorElement; +import io.micronaut.inject.ast.ElementAnnotationMetadataFactory; +import io.micronaut.inject.ast.ElementFactory; +import io.micronaut.inject.ast.EnumConstantElement; +import io.micronaut.inject.ast.PrimitiveElement; import io.micronaut.inject.ast.beans.BeanElementBuilder; -import org.codehaus.groovy.ast.*; -import org.codehaus.groovy.control.CompilationUnit; -import org.codehaus.groovy.control.SourceUnit; +import io.micronaut.inject.configuration.ConfigurationMetadataBuilder; +import org.codehaus.groovy.ast.AnnotatedNode; +import org.codehaus.groovy.ast.ClassHelper; +import org.codehaus.groovy.ast.ClassNode; +import org.codehaus.groovy.ast.ConstructorNode; +import org.codehaus.groovy.ast.FieldNode; +import org.codehaus.groovy.ast.MethodNode; import java.util.Map; @@ -34,60 +40,50 @@ * @since 2.3.0 */ public class GroovyElementFactory implements ElementFactory { - private final SourceUnit sourceUnit; - private final CompilationUnit compilationUnit; private final GroovyVisitorContext visitorContext; public GroovyElementFactory(GroovyVisitorContext groovyVisitorContext) { - this.visitorContext = groovyVisitorContext; - this.sourceUnit = groovyVisitorContext.getSourceUnit(); - this.compilationUnit = groovyVisitorContext.getCompilationUnit(); + this.visitorContext = groovyVisitorContext; } - @NonNull @Override - public ClassElement newClassElement(@NonNull ClassNode classNode, @NonNull AnnotationMetadata annotationMetadata) { + public ClassElement newClassElement(ClassNode classNode, ElementAnnotationMetadataFactory annotationMetadataFactory) { if (classNode.isArray()) { ClassNode componentType = classNode.getComponentType(); - ClassElement componentElement = newClassElement(componentType, annotationMetadata); + ClassElement componentElement = newClassElement(componentType, annotationMetadataFactory); return componentElement.toArray(); - } else if (ClassHelper.isPrimitiveType(classNode)) { + } + if (ClassHelper.isPrimitiveType(classNode)) { return PrimitiveElement.valueOf(classNode.getName()); - } else if (classNode.isEnum()) { - return new GroovyEnumElement(visitorContext, classNode, annotationMetadata); - } else if (classNode.isAnnotationDefinition()) { - return new GroovyAnnotationElement(visitorContext, classNode, annotationMetadata); - } else if (classNode.isGenericsPlaceHolder()) { - return new GroovyGenericPlaceholderElement(visitorContext, classNode, annotationMetadata, 0); + } + if (classNode.isEnum()) { + return new GroovyEnumElement(visitorContext, classNode, annotationMetadataFactory); + } + if (classNode.isAnnotationDefinition()) { + return new GroovyAnnotationElement(visitorContext, classNode, annotationMetadataFactory); + } + if (classNode.isGenericsPlaceHolder()) { + return new GroovyGenericPlaceholderElement(visitorContext, classNode, annotationMetadataFactory, 0); } else { - return new GroovyClassElement(visitorContext, classNode, annotationMetadata); + return new GroovyClassElement(visitorContext, classNode, annotationMetadataFactory); } } @NonNull @Override - public ClassElement newClassElement(@NonNull ClassNode classNode, @NonNull AnnotationMetadata annotationMetadata, @NonNull Map resolvedGenerics) { + public ClassElement newClassElement(ClassNode classNode, + ElementAnnotationMetadataFactory annotationMetadataFactory, + Map resolvedGenerics) { if (classNode.isArray()) { ClassNode componentType = classNode.getComponentType(); - ClassElement componentElement = newClassElement(componentType, annotationMetadata); + ClassElement componentElement = newClassElement(componentType, annotationMetadataFactory); return componentElement.toArray(); - } else if (ClassHelper.isPrimitiveType(classNode)) { + } + if (ClassHelper.isPrimitiveType(classNode)) { return PrimitiveElement.valueOf(classNode.getName()); - } else if (classNode.isEnum()) { - return new GroovyEnumElement(visitorContext, classNode, annotationMetadata) { - @NonNull - @Override - public Map getTypeArguments() { - if (resolvedGenerics != null) { - return resolvedGenerics; - } - return super.getTypeArguments(); - } - }; - } else if (classNode.isAnnotationDefinition()) { - return new GroovyAnnotationElement(visitorContext, classNode, annotationMetadata); - } else { - return new GroovyClassElement(visitorContext, classNode, annotationMetadata) { + } + if (classNode.isEnum()) { + return new GroovyEnumElement(visitorContext, classNode, annotationMetadataFactory) { @NonNull @Override public Map getTypeArguments() { @@ -98,80 +94,99 @@ public Map getTypeArguments() { } }; } + if (classNode.isAnnotationDefinition()) { + return new GroovyAnnotationElement(visitorContext, classNode, annotationMetadataFactory); + } + return new GroovyClassElement(visitorContext, classNode, annotationMetadataFactory) { + @NonNull + @Override + public Map getTypeArguments() { + if (resolvedGenerics != null) { + return resolvedGenerics; + } + return super.getTypeArguments(); + } + }; } @NonNull @Override - public MethodElement newMethodElement(ClassElement declaringClass, @NonNull MethodNode method, @NonNull AnnotationMetadata annotationMetadata) { - if (!(declaringClass instanceof GroovyClassElement)) { + public GroovyMethodElement newMethodElement(ClassElement owningType, + MethodNode method, + ElementAnnotationMetadataFactory elementAnnotationMetadataFactory) { + if (!(owningType instanceof GroovyClassElement)) { throw new IllegalArgumentException("Declaring class must be a GroovyClassElement"); } return new GroovyMethodElement( - (GroovyClassElement) declaringClass, - visitorContext, - method, - annotationMetadata + (GroovyClassElement) owningType, + visitorContext, + method, + elementAnnotationMetadataFactory ); } @NonNull @Override - public ClassElement newSourceClassElement(@NonNull ClassNode classNode, @NonNull AnnotationMetadata annotationMetadata) { + public ClassElement newSourceClassElement(ClassNode classNode, ElementAnnotationMetadataFactory annotationMetadataFactory) { if (classNode.isArray()) { ClassNode componentType = classNode.getComponentType(); - ClassElement componentElement = newSourceClassElement(componentType, annotationMetadata); + ClassElement componentElement = newSourceClassElement(componentType, annotationMetadataFactory); return componentElement.toArray(); } else if (ClassHelper.isPrimitiveType(classNode)) { return PrimitiveElement.valueOf(classNode.getName()); } else if (classNode.isEnum()) { - return new GroovyEnumElement(visitorContext, classNode, annotationMetadata) { + return new GroovyEnumElement(visitorContext, classNode, annotationMetadataFactory) { @NonNull @Override public BeanElementBuilder addAssociatedBean(@NonNull ClassElement type) { return new GroovyBeanDefinitionBuilder( - this, - type, - new GroovyConfigurationMetadataBuilder(sourceUnit, compilationUnit), - visitorContext + this, + type, + ConfigurationMetadataBuilder.INSTANCE, + annotationMetadataFactory, + visitorContext ); } }; } else { - return new GroovyClassElement(visitorContext, classNode, annotationMetadata) { + return new GroovyClassElement(visitorContext, classNode, annotationMetadataFactory) { @NonNull @Override public BeanElementBuilder addAssociatedBean(@NonNull ClassElement type) { return new GroovyBeanDefinitionBuilder( - this, - type, - new GroovyConfigurationMetadataBuilder(sourceUnit, compilationUnit), - visitorContext + this, + type, + ConfigurationMetadataBuilder.INSTANCE, + annotationMetadataFactory, + visitorContext ); } }; } } - @NonNull @Override - public MethodElement newSourceMethodElement(ClassElement declaringClass, @NonNull MethodNode method, @NonNull AnnotationMetadata annotationMetadata) { - if (!(declaringClass instanceof GroovyClassElement)) { + public GroovyMethodElement newSourceMethodElement(ClassElement owningType, + MethodNode method, + ElementAnnotationMetadataFactory elementAnnotationMetadataFactory) { + if (!(owningType instanceof GroovyClassElement)) { throw new IllegalArgumentException("Declaring class must be a GroovyClassElement"); } return new GroovyMethodElement( - (GroovyClassElement) declaringClass, - visitorContext, - method, - annotationMetadata + (GroovyClassElement) owningType, + visitorContext, + method, + elementAnnotationMetadataFactory ) { @NonNull @Override public BeanElementBuilder addAssociatedBean(@NonNull ClassElement type) { return new GroovyBeanDefinitionBuilder( - this, - type, - new GroovyConfigurationMetadataBuilder(sourceUnit, compilationUnit), - visitorContext + this, + type, + ConfigurationMetadataBuilder.INSTANCE, + elementAnnotationMetadataFactory, + visitorContext ); } }; @@ -179,7 +194,9 @@ public BeanElementBuilder addAssociatedBean(@NonNull ClassElement type) { @NonNull @Override - public ConstructorElement newConstructorElement(ClassElement declaringClass, @NonNull MethodNode constructor, @NonNull AnnotationMetadata annotationMetadata) { + public ConstructorElement newConstructorElement(ClassElement declaringClass, + MethodNode constructor, + ElementAnnotationMetadataFactory annotationMetadataFactory) { if (!(declaringClass instanceof GroovyClassElement)) { throw new IllegalArgumentException("Declaring class must be a GroovyClassElement"); } @@ -187,91 +204,38 @@ public ConstructorElement newConstructorElement(ClassElement declaringClass, @No throw new IllegalArgumentException("Constructor must be a ConstructorNode"); } return new GroovyConstructorElement( - (GroovyClassElement) declaringClass, - visitorContext, - (ConstructorNode) constructor, - annotationMetadata + (GroovyClassElement) declaringClass, + visitorContext, + (ConstructorNode) constructor, + annotationMetadataFactory ); } @Override - public EnumConstantElement newEnumConstantElement(ClassElement declaringClass, FieldNode enumConstant, AnnotationMetadata annotationMetadata) { + public EnumConstantElement newEnumConstantElement(ClassElement declaringClass, + FieldNode enumConstant, + ElementAnnotationMetadataFactory annotationMetadataFactory) { if (!(declaringClass instanceof GroovyClassElement)) { throw new IllegalArgumentException("Declaring class must be a GroovyEnumElement"); } return new GroovyEnumConstantElement( - (GroovyClassElement) declaringClass, - visitorContext, - enumConstant, - enumConstant, - annotationMetadata + (GroovyClassElement) declaringClass, + visitorContext, + enumConstant, + enumConstant, + annotationMetadataFactory ); } @NonNull @Override - public FieldElement newFieldElement(ClassElement declaringClass, @NonNull FieldNode field, @NonNull AnnotationMetadata annotationMetadata) { - if (!(declaringClass instanceof GroovyClassElement)) { + public GroovyFieldElement newFieldElement(ClassElement owningType, + FieldNode field, + ElementAnnotationMetadataFactory annotationMetadataFactory) { + if (!(owningType instanceof GroovyClassElement)) { throw new IllegalArgumentException("Declaring class must be a GroovyClassElement"); } - return new GroovyFieldElement( - visitorContext, - field, - field, - annotationMetadata - ); - } - - @NonNull - @Override - public FieldElement newFieldElement(@NonNull FieldNode field, @NonNull AnnotationMetadata annotationMetadata) { - return new GroovyFieldElement( - visitorContext, - field, - field, - annotationMetadata - ); - } - - /** - * Builds a new field element for the given property. - * - * @param property The property - * @param annotationMetadata The resolved annotation metadata - * @return The field element - */ - public FieldElement newFieldElement(@NonNull PropertyNode property, @NonNull AnnotationMetadata annotationMetadata) { - return new GroovyFieldElement( - visitorContext, - property, - property, - annotationMetadata - ); - } - - /** - * Constructs a new {@link ParameterElement} for the given field element and metadata. - * @param field The field - * @param annotationMetadata The metadata - * @return The parameter element - */ - public ParameterElement newParameterElement(@NonNull FieldElement field, @NonNull AnnotationMetadata annotationMetadata) { - if (!(field instanceof GroovyFieldElement)) { - throw new IllegalArgumentException("Field must be a GroovyFieldElement"); - } - FieldNode fieldNode = (FieldNode) field.getNativeType(); - return new GroovyParameterElement( - null, - visitorContext, - new Parameter(fieldNode.getType(), fieldNode.getName()), - annotationMetadata - ) { - @Nullable - @Override - public ClassElement getGenericType() { - return field.getGenericType(); - } - }; + return new GroovyFieldElement(visitorContext, (GroovyClassElement) owningType, field, annotationMetadataFactory); } } diff --git a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyEnumConstantElement.java b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyEnumConstantElement.java index 2a3e2078bdd..26e6d426af8 100644 --- a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyEnumConstantElement.java +++ b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyEnumConstantElement.java @@ -15,18 +15,19 @@ */ package io.micronaut.ast.groovy.visitor; -import java.util.Set; - import io.micronaut.core.annotation.AnnotationMetadata; import io.micronaut.core.annotation.Internal; import io.micronaut.core.annotation.NonNull; import io.micronaut.inject.ast.ClassElement; +import io.micronaut.inject.ast.ElementAnnotationMetadataFactory; import io.micronaut.inject.ast.ElementModifier; import io.micronaut.inject.ast.EnumConstantElement; - +import io.micronaut.inject.ast.MemberElement; import org.codehaus.groovy.ast.AnnotatedNode; import org.codehaus.groovy.ast.FieldNode; +import java.util.Set; + /** * A enum constant element returning data from a {@link org.codehaus.groovy.ast.Variable}. * @@ -39,21 +40,31 @@ public final class GroovyEnumConstantElement extends AbstractGroovyElement imple private final FieldNode variable; /** - * @param declaringEnum The declaring enum - * @param visitorContext The visitor context - * @param variable The {@link org.codehaus.groovy.ast.Variable} - * @param annotatedNode The annotated node - * @param annotationMetadata The annotation medatada + * @param declaringEnum The declaring enum + * @param visitorContext The visitor context + * @param variable The {@link org.codehaus.groovy.ast.Variable} + * @param annotatedNode The annotated node + * @param annotationMetadataFactory The annotation medatada */ - GroovyEnumConstantElement( - GroovyClassElement declaringEnum, - GroovyVisitorContext visitorContext, - FieldNode variable, AnnotatedNode annotatedNode, AnnotationMetadata annotationMetadata) { - super(visitorContext, annotatedNode, annotationMetadata); + GroovyEnumConstantElement(GroovyClassElement declaringEnum, + GroovyVisitorContext visitorContext, + FieldNode variable, AnnotatedNode annotatedNode, + ElementAnnotationMetadataFactory annotationMetadataFactory) { + super(visitorContext, annotatedNode, annotationMetadataFactory); this.declaringEnum = declaringEnum; this.variable = variable; } + @Override + protected AbstractGroovyElement copyConstructor() { + return new GroovyEnumConstantElement(declaringEnum, visitorContext, variable, getNativeType(), elementAnnotationMetadataFactory); + } + + @Override + public MemberElement withAnnotationMetadata(AnnotationMetadata annotationMetadata) { + return (MemberElement) super.withAnnotationMetadata(annotationMetadata); + } + @Override public ClassElement getDeclaringType() { return declaringEnum; @@ -121,7 +132,7 @@ public String getName() { } @Override - public Object getNativeType() { + public FieldNode getNativeType() { return variable; } diff --git a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyEnumElement.java b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyEnumElement.java index 164681f15df..89856b9eda6 100644 --- a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyEnumElement.java +++ b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyEnumElement.java @@ -15,19 +15,17 @@ */ package io.micronaut.ast.groovy.visitor; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; - -import io.micronaut.ast.groovy.utils.AstAnnotationUtils; -import io.micronaut.core.annotation.AnnotationMetadata; import io.micronaut.inject.ast.ClassElement; +import io.micronaut.inject.ast.ElementAnnotationMetadataFactory; import io.micronaut.inject.ast.EnumConstantElement; import io.micronaut.inject.ast.EnumElement; - import org.codehaus.groovy.ast.ClassNode; import org.codehaus.groovy.ast.FieldNode; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + /** * Implementation of {@link EnumElement} for Groovy. * @@ -42,20 +40,25 @@ class GroovyEnumElement extends GroovyClassElement implements EnumElement { /** * @param visitorContext The visitor context * @param classNode The {@link ClassNode} - * @param annotationMetadata The annotation metadata + * @param annotationMetadataFactory The annotation metadata factory */ - GroovyEnumElement(GroovyVisitorContext visitorContext, ClassNode classNode, AnnotationMetadata annotationMetadata) { - this(visitorContext, classNode, annotationMetadata, 0); + GroovyEnumElement(GroovyVisitorContext visitorContext, + ClassNode classNode, + ElementAnnotationMetadataFactory annotationMetadataFactory) { + this(visitorContext, classNode, annotationMetadataFactory, 0); } /** * @param visitorContext The visitor context * @param classNode The {@link ClassNode} - * @param annotationMetadata The annotation metadata - * @param arrayDimensions The number of array dimensions + * @param annotationMetadataFactory The annotation metadata + * @param arrayDimensions The number of array dimensions factory */ - GroovyEnumElement(GroovyVisitorContext visitorContext, ClassNode classNode, AnnotationMetadata annotationMetadata, int arrayDimensions) { - super(visitorContext, classNode, annotationMetadata, null, arrayDimensions); + GroovyEnumElement(GroovyVisitorContext visitorContext, + ClassNode classNode, + ElementAnnotationMetadataFactory annotationMetadataFactory, + int arrayDimensions) { + super(visitorContext, classNode, annotationMetadataFactory, null, arrayDimensions); } @Override @@ -79,18 +82,14 @@ public List elements() { private void initEnum() { values = new ArrayList<>(); enumConstants = new ArrayList<>(); - ClassNode nativeType = (ClassNode) getNativeType(); + ClassNode nativeType = getNativeType(); for (FieldNode field : nativeType.getFields()) { if (field.getName().equals("MAX_VALUE") || field.getName().equals("MIN_VALUE")) { continue; } if (field.isEnum()) { values.add(field.getName()); - enumConstants.add(new GroovyEnumConstantElement(this, visitorContext, field, field, AstAnnotationUtils.getAnnotationMetadata( - sourceUnit, - compilationUnit, - field - ))); + enumConstants.add(new GroovyEnumConstantElement(this, visitorContext, field, field, elementAnnotationMetadataFactory)); } } @@ -100,7 +99,7 @@ private void initEnum() { @Override public ClassElement withArrayDimensions(int arrayDimensions) { - return new GroovyEnumElement(visitorContext, classNode, getAnnotationMetadata(), arrayDimensions); + return new GroovyEnumElement(visitorContext, classNode, elementAnnotationMetadataFactory, arrayDimensions); } } diff --git a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyFieldElement.java b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyFieldElement.java index 02440aa1443..d46b6a0f5bc 100644 --- a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyFieldElement.java +++ b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyFieldElement.java @@ -15,22 +15,22 @@ */ package io.micronaut.ast.groovy.visitor; -import io.micronaut.ast.groovy.utils.AstAnnotationUtils; import io.micronaut.core.annotation.AnnotationMetadata; +import io.micronaut.core.annotation.NonNull; import io.micronaut.core.reflect.ClassUtils; import io.micronaut.inject.ast.ClassElement; +import io.micronaut.inject.ast.ElementAnnotationMetadataFactory; import io.micronaut.inject.ast.ElementModifier; import io.micronaut.inject.ast.FieldElement; -import org.codehaus.groovy.ast.*; - -import io.micronaut.core.annotation.NonNull; +import org.codehaus.groovy.ast.ClassHelper; +import org.codehaus.groovy.ast.ClassNode; +import org.codehaus.groovy.ast.FieldNode; import java.lang.reflect.Modifier; -import java.util.Collections; import java.util.Set; /** - * A field element returning data from a {@link Variable}. The + * A field element returning data from a {@link FieldNode}. The * variable could be a field or property node. * * @author James Kleeh @@ -38,33 +38,52 @@ */ public class GroovyFieldElement extends AbstractGroovyElement implements FieldElement { - private final Variable variable; + private final GroovyClassElement owningType; + private final FieldNode fieldNode; /** - * @param visitorContext The visitor context - * @param variable The {@link Variable} - * @param annotatedNode The annotated ndoe - * @param annotationMetadata The annotation medatada + * @param visitorContext The visitor context + * @param owningType The owningType + * @param fieldNode The {@link FieldNode} + * @param annotationMetadataFactory The annotation metadata */ - GroovyFieldElement( - GroovyVisitorContext visitorContext, - Variable variable, AnnotatedNode annotatedNode, AnnotationMetadata annotationMetadata) { - super(visitorContext, annotatedNode, annotationMetadata); - this.variable = variable; + GroovyFieldElement(GroovyVisitorContext visitorContext, + GroovyClassElement owningType, + FieldNode fieldNode, + ElementAnnotationMetadataFactory annotationMetadataFactory) { + super(visitorContext, fieldNode, annotationMetadataFactory); + this.owningType = owningType; + this.fieldNode = fieldNode; + } + + @Override + protected AbstractGroovyElement copyConstructor() { + return new GroovyFieldElement(visitorContext, owningType, fieldNode, elementAnnotationMetadataFactory); + } + + @Override + public FieldElement withAnnotationMetadata(AnnotationMetadata annotationMetadata) { + return (FieldElement) super.withAnnotationMetadata(annotationMetadata); + } + + @Override + public FieldNode getNativeType() { + return fieldNode; + } + + @Override + public GroovyClassElement getOwningType() { + return owningType; } @Override public Set getModifiers() { - if (variable instanceof FieldNode) { - return super.resolveModifiers(((FieldNode) variable)); - } else { - return Collections.emptySet(); - } + return super.resolveModifiers(fieldNode); } @Override public String toString() { - return variable.getName(); + return fieldNode.getName(); } @Override @@ -72,12 +91,8 @@ public ClassElement getGenericField() { if (isPrimitive()) { ClassNode cn = ClassHelper.make(ClassUtils.getPrimitiveType(getType().getName()).orElse(null)); if (cn != null) { + return new GroovyClassElement(visitorContext, cn, elementAnnotationMetadataFactory) { - return new GroovyClassElement( - visitorContext, - cn, - getAnnotationMetadata() - ) { @Override public boolean isPrimitive() { return true; @@ -86,13 +101,8 @@ public boolean isPrimitive() { } else { return getGenericType(); } - } else { - return new GroovyClassElement( - visitorContext, - (ClassNode) getGenericType().getNativeType(), - getAnnotationMetadata() - ); } + return new GroovyClassElement(visitorContext, (ClassNode) getGenericType().getNativeType(), elementAnnotationMetadataFactory); } @Override @@ -112,72 +122,56 @@ public int getArrayDimensions() { @Override public String getName() { - return variable.getName(); + return fieldNode.getName(); } @Override public boolean isAbstract() { - return Modifier.isAbstract(variable.getModifiers()); + return Modifier.isAbstract(fieldNode.getModifiers()); } @Override public boolean isStatic() { - return Modifier.isStatic(variable.getModifiers()); + return Modifier.isStatic(fieldNode.getModifiers()); } @Override public boolean isPublic() { - return Modifier.isPublic(variable.getModifiers()); + return Modifier.isPublic(fieldNode.getModifiers()); } @Override public boolean isPrivate() { - return Modifier.isPrivate(variable.getModifiers()); + return Modifier.isPrivate(fieldNode.getModifiers()); } @Override public boolean isFinal() { - return Modifier.isFinal(variable.getModifiers()); + return Modifier.isFinal(fieldNode.getModifiers()); } @Override public boolean isProtected() { - return Modifier.isProtected(variable.getModifiers()); + return Modifier.isProtected(fieldNode.getModifiers()); } @Override - public Object getNativeType() { - return variable; + public boolean isPackagePrivate() { + return !Modifier.isPublic(fieldNode.getModifiers()) && !Modifier.isProtected(fieldNode.getModifiers()) && !Modifier.isPrivate(fieldNode.getModifiers()); } @NonNull @Override public ClassElement getType() { - return visitorContext.getElementFactory().newClassElement(variable.getType(), AstAnnotationUtils.getAnnotationMetadata(sourceUnit, compilationUnit, variable.getType())); + return visitorContext.getElementFactory().newClassElement(fieldNode.getType(), elementAnnotationMetadataFactory); } @Override - public ClassElement getDeclaringType() { - ClassNode declaringClass = null; - if (variable instanceof FieldNode) { - FieldNode fn = (FieldNode) variable; - declaringClass = fn.getDeclaringClass(); - } else if (variable instanceof PropertyNode) { - PropertyNode pn = (PropertyNode) variable; - declaringClass = pn.getDeclaringClass(); - } - + public GroovyClassElement getDeclaringType() { + ClassNode declaringClass = fieldNode.getDeclaringClass(); if (declaringClass == null) { throw new IllegalStateException("Declaring class could not be established"); } - - return visitorContext.getElementFactory().newClassElement( - declaringClass, - AstAnnotationUtils.getAnnotationMetadata( - sourceUnit, - compilationUnit, - declaringClass - ) - ); + return (GroovyClassElement) visitorContext.getElementFactory().newClassElement(declaringClass, elementAnnotationMetadataFactory); } } diff --git a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyGenericPlaceholderElement.java b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyGenericPlaceholderElement.java index a3e14600b07..1c139edcf01 100644 --- a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyGenericPlaceholderElement.java +++ b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyGenericPlaceholderElement.java @@ -15,11 +15,11 @@ */ package io.micronaut.ast.groovy.visitor; -import io.micronaut.core.annotation.AnnotationMetadata; import io.micronaut.core.annotation.Internal; import io.micronaut.core.annotation.NonNull; import io.micronaut.inject.ast.ClassElement; import io.micronaut.inject.ast.Element; +import io.micronaut.inject.ast.ElementAnnotationMetadataFactory; import io.micronaut.inject.ast.GenericPlaceholderElement; import org.codehaus.groovy.ast.ClassNode; @@ -37,8 +37,17 @@ */ @Internal final class GroovyGenericPlaceholderElement extends GroovyClassElement implements GenericPlaceholderElement { - GroovyGenericPlaceholderElement(GroovyVisitorContext visitorContext, ClassNode classNode, AnnotationMetadata annotationMetadata, int arrayDimensions) { - super(visitorContext, classNode, annotationMetadata, null, arrayDimensions); + + GroovyGenericPlaceholderElement(GroovyVisitorContext visitorContext, + ClassNode classNode, + ElementAnnotationMetadataFactory annotationMetadataFactory, + int arrayDimensions) { + super(visitorContext, classNode, annotationMetadataFactory, null, arrayDimensions); + } + + @Override + protected GroovyClassElement copyConstructor() { + return new GroovyGenericPlaceholderElement(visitorContext, classNode, elementAnnotationMetadataFactory, getArrayDimensions()); } @NonNull @@ -63,7 +72,7 @@ public Optional getDeclaringElement() { @Override public ClassElement withArrayDimensions(int arrayDimensions) { - return new GroovyGenericPlaceholderElement(visitorContext, classNode, getAnnotationMetadata(), arrayDimensions); + return new GroovyGenericPlaceholderElement(visitorContext, classNode, elementAnnotationMetadataFactory, arrayDimensions); } @Override diff --git a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyMethodElement.java b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyMethodElement.java index 0e1afb979db..c6fe7978a08 100644 --- a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyMethodElement.java +++ b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyMethodElement.java @@ -15,13 +15,13 @@ */ package io.micronaut.ast.groovy.visitor; -import io.micronaut.ast.groovy.utils.AstAnnotationUtils; import io.micronaut.ast.groovy.utils.AstGenericUtils; -import io.micronaut.ast.groovy.utils.ExtendedParameter; import io.micronaut.core.annotation.AnnotationMetadata; +import io.micronaut.core.annotation.NonNull; import io.micronaut.core.util.ArrayUtils; import io.micronaut.core.util.CollectionUtils; import io.micronaut.inject.ast.ClassElement; +import io.micronaut.inject.ast.ElementAnnotationMetadataFactory; import io.micronaut.inject.ast.ElementModifier; import io.micronaut.inject.ast.GenericPlaceholderElement; import io.micronaut.inject.ast.MethodElement; @@ -31,10 +31,12 @@ import org.codehaus.groovy.ast.MethodNode; import org.codehaus.groovy.ast.Parameter; -import io.micronaut.core.annotation.NonNull; - -import java.util.*; -import java.util.function.Function; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; import java.util.stream.Collectors; /** @@ -45,22 +47,54 @@ */ public class GroovyMethodElement extends AbstractGroovyElement implements MethodElement { + protected ParameterElement[] parameters; private final MethodNode methodNode; - private final GroovyClassElement declaringClass; - private Map genericsSpec = null; + private final GroovyClassElement owningType; + private Map genericsSpec; private ClassElement declaringElement; - private ParameterElement[] parameters; /** - * @param declaringClass The declaring class + * @param owningType The owning type * @param visitorContext The visitor context * @param methodNode The {@link MethodNode} * @param annotationMetadata The annotation metadata */ - GroovyMethodElement(GroovyClassElement declaringClass, GroovyVisitorContext visitorContext, MethodNode methodNode, AnnotationMetadata annotationMetadata) { + GroovyMethodElement(GroovyClassElement owningType, + GroovyVisitorContext visitorContext, + MethodNode methodNode, + ElementAnnotationMetadataFactory annotationMetadata) { super(visitorContext, methodNode, annotationMetadata); this.methodNode = methodNode; - this.declaringClass = declaringClass; + this.owningType = owningType; + } + + @Override + protected AbstractGroovyElement copyConstructor() { + return new GroovyMethodElement(owningType, visitorContext, methodNode, elementAnnotationMetadataFactory); + } + + @Override + protected void copyValues(AbstractGroovyElement element) { + ((GroovyMethodElement) element).parameters = parameters; + } + + @Override + public MethodElement withAnnotationMetadata(AnnotationMetadata annotationMetadata) { + return (MethodElement) super.withAnnotationMetadata(annotationMetadata); + } + + @Override + public MethodElement withParameters(ParameterElement... newParameters) { + GroovyMethodElement methodElement = (GroovyMethodElement) copy(); + methodElement.parameters = newParameters; + return methodElement; + } + + @Override + public MethodElement withNewOwningType(ClassElement owningType) { + GroovyMethodElement groovyMethodElement = new GroovyMethodElement((GroovyClassElement) owningType, visitorContext, methodNode, elementAnnotationMetadataFactory); + copyValues(groovyMethodElement); + return groovyMethodElement; } @Override @@ -68,11 +102,11 @@ public ClassElement[] getThrownTypes() { final ClassNode[] exceptions = methodNode.getExceptions(); if (ArrayUtils.isNotEmpty(exceptions)) { return Arrays.stream(exceptions) - .map(cn -> getGenericElement(cn, visitorContext.getElementFactory().newClassElement( - cn, - AnnotationMetadata.EMPTY_METADATA, - Collections.emptyMap() - ))).toArray(ClassElement[]::new); + .map(cn -> getGenericElement(cn, visitorContext.getElementFactory().newClassElement( + cn, + elementAnnotationMetadataFactory, + Collections.emptyMap() + ))).toArray(ClassElement[]::new); } return ClassElement.ZERO_CLASS_ELEMENTS; } @@ -84,7 +118,11 @@ public Set getModifiers() { @Override public String toString() { - return methodNode.toString(); + ClassNode declaringClass = methodNode.getDeclaringClass(); + if (declaringClass == null) { + declaringClass = owningType.getNativeType(); + } + return declaringClass.getName() + "." + methodNode.getName() + "(..)"; } @Override @@ -104,7 +142,7 @@ public boolean isStatic() { @Override public boolean isPublic() { - return methodNode.isPublic() || methodNode.isSyntheticPublic(); + return methodNode.isPublic() || (methodNode.isSyntheticPublic() && !isPackagePrivate()); } @Override @@ -112,6 +150,11 @@ public boolean isPrivate() { return methodNode.isPrivate(); } + @Override + public boolean isPackagePrivate() { + return methodNode.isPackageScope(); + } + @Override public boolean isFinal() { return methodNode.isFinal(); @@ -123,7 +166,12 @@ public boolean isProtected() { } @Override - public Object getNativeType() { + public boolean isDefault() { + return !isAbstract() && getDeclaringType().isInterface(); + } + + @Override + public MethodNode getNativeType() { return methodNode; } @@ -157,15 +205,17 @@ ClassElement getGenericElement(@NonNull ClassNode type, @NonNull ClassElement ra @NonNull Map getGenericsSpec() { if (genericsSpec == null) { - Map> info = declaringClass.getGenericTypeInfo(); + Map> info = owningType.getGenericTypeInfo(); if (CollectionUtils.isNotEmpty(info)) { - Map typeGenericInfo = info.get(methodNode.getDeclaringClass().getName()); + ClassNode declaringClazz = methodNode.getDeclaringClass(); + if (declaringClazz == null) { + declaringClazz = owningType.getNativeType(); + } + Map typeGenericInfo = info.get(declaringClazz.getName()); if (CollectionUtils.isNotEmpty(typeGenericInfo)) { - genericsSpec = AstGenericUtils.createGenericsSpec(methodNode, new HashMap<>(typeGenericInfo)); } } - if (genericsSpec == null) { genericsSpec = Collections.emptyMap(); } @@ -176,64 +226,51 @@ Map getGenericsSpec() { @Override @NonNull public ClassElement getReturnType() { - return visitorContext.getElementFactory().newClassElement(methodNode.getReturnType(), AstAnnotationUtils.getAnnotationMetadata(sourceUnit, compilationUnit, methodNode.getReturnType())); + return visitorContext.getElementFactory().newClassElement(methodNode.getReturnType(), elementAnnotationMetadataFactory); } @Override public ParameterElement[] getParameters() { Parameter[] parameters = methodNode.getParameters(); if (this.parameters == null) { - this.parameters = Arrays.stream(parameters).map((Function) parameter -> - new GroovyParameterElement( - this, - visitorContext, - parameter, - AstAnnotationUtils.getAnnotationMetadata(sourceUnit, compilationUnit, new ExtendedParameter(methodNode, parameter)) - ) + this.parameters = Arrays.stream(parameters).map(parameter -> + new GroovyParameterElement( + this, + visitorContext, + parameter, + elementAnnotationMetadataFactory + ) ).toArray(ParameterElement[]::new); } return this.parameters; } - @Override - public MethodElement withNewParameters(ParameterElement... newParameters) { - final ParameterElement[] existing = getParameters(); - return new GroovyMethodElement(declaringClass, visitorContext, methodNode, getAnnotationMetadata()) { - @Override - public ParameterElement[] getParameters() { - return ArrayUtils.concat(existing, newParameters); - } - }; - } - @Override public ClassElement getDeclaringType() { if (this.declaringElement == null) { - this.declaringElement = visitorContext.getElementFactory().newClassElement( - methodNode.getDeclaringClass(), - AstAnnotationUtils.getAnnotationMetadata( - sourceUnit, - compilationUnit, - methodNode.getDeclaringClass() - ) - ); + ClassNode methodDeclaringClass = methodNode.getDeclaringClass(); + if (methodDeclaringClass == null) { + return owningType; + } + this.declaringElement = visitorContext.getElementFactory().newClassElement(methodDeclaringClass, elementAnnotationMetadataFactory); } return this.declaringElement; } @Override - public ClassElement getOwningType() { - return declaringClass; + public GroovyClassElement getOwningType() { + return owningType; } @Override public List getDeclaredTypeVariables() { GenericsType[] genericsTypes = methodNode.getGenericsTypes(); return genericsTypes == null ? - Collections.emptyList() : - Arrays.stream(genericsTypes) - .map(gt -> (GenericPlaceholderElement) visitorContext.getElementFactory().newClassElement(gt.getType(), AnnotationMetadata.EMPTY_METADATA)) - .collect(Collectors.toList()); + Collections.emptyList() : + Arrays.stream(genericsTypes) + .map(gt -> (GenericPlaceholderElement) visitorContext.getElementFactory().newClassElement(gt.getType(), elementAnnotationMetadataFactory)) + .collect(Collectors.toList()); } + } diff --git a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyPackageElement.java b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyPackageElement.java index a7633047c0e..bd9d9880b3e 100644 --- a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyPackageElement.java +++ b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyPackageElement.java @@ -15,9 +15,9 @@ */ package io.micronaut.ast.groovy.visitor; -import io.micronaut.core.annotation.AnnotationMetadata; import io.micronaut.core.annotation.Internal; import io.micronaut.core.annotation.NonNull; +import io.micronaut.inject.ast.ElementAnnotationMetadataFactory; import io.micronaut.inject.ast.PackageElement; import org.codehaus.groovy.ast.PackageNode; @@ -34,15 +34,22 @@ public class GroovyPackageElement extends AbstractGroovyElement implements Packa /** * Default constructor. * - * @param visitorContext The visitor context - * @param packageNode The annotated node - * @param annotationMetadata The annotation metadata + * @param visitorContext The visitor context + * @param packageNode The annotated node + * @param annotationMetadataFactory The annotation metadata */ - public GroovyPackageElement(GroovyVisitorContext visitorContext, PackageNode packageNode, AnnotationMetadata annotationMetadata) { - super(visitorContext, packageNode, annotationMetadata); + public GroovyPackageElement(GroovyVisitorContext visitorContext, + PackageNode packageNode, + ElementAnnotationMetadataFactory annotationMetadataFactory) { + super(visitorContext, packageNode, annotationMetadataFactory); this.packageNode = packageNode; } + @Override + protected AbstractGroovyElement copyConstructor() { + return new GroovyPackageElement(visitorContext, packageNode, elementAnnotationMetadataFactory); + } + @NonNull @Override public String getName() { @@ -75,7 +82,8 @@ public boolean isPublic() { @NonNull @Override - public Object getNativeType() { + public PackageNode getNativeType() { return packageNode; } + } diff --git a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyParameterElement.java b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyParameterElement.java index 8a9c03b91d6..90f14e9e1e8 100644 --- a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyParameterElement.java +++ b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyParameterElement.java @@ -15,16 +15,15 @@ */ package io.micronaut.ast.groovy.visitor; -import io.micronaut.ast.groovy.utils.AstAnnotationUtils; import io.micronaut.core.annotation.AnnotationMetadata; import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.NonNull; +import io.micronaut.core.annotation.Nullable; import io.micronaut.inject.ast.ClassElement; +import io.micronaut.inject.ast.ElementAnnotationMetadataFactory; import io.micronaut.inject.ast.ParameterElement; import org.codehaus.groovy.ast.Parameter; -import io.micronaut.core.annotation.NonNull; -import io.micronaut.core.annotation.Nullable; - /** * Implementation of {@link ParameterElement} for Groovy. * @@ -42,17 +41,30 @@ public class GroovyParameterElement extends AbstractGroovyElement implements Par /** * Default constructor. * - * @param methodElement The parent method element - * @param visitorContext The visitor context - * @param parameter The parameter - * @param annotationMetadata The annotation metadata + * @param methodElement The parent method element + * @param visitorContext The visitor context + * @param parameter The parameter + * @param elementAnnotationMetadata The annotation metadata */ - GroovyParameterElement(GroovyMethodElement methodElement, GroovyVisitorContext visitorContext, Parameter parameter, AnnotationMetadata annotationMetadata) { - super(visitorContext, parameter, annotationMetadata); + GroovyParameterElement(GroovyMethodElement methodElement, + GroovyVisitorContext visitorContext, + Parameter parameter, + ElementAnnotationMetadataFactory elementAnnotationMetadata) { + super(visitorContext, parameter, elementAnnotationMetadata); this.parameter = parameter; this.methodElement = methodElement; } + @Override + protected AbstractGroovyElement copyConstructor() { + return new GroovyParameterElement(methodElement, visitorContext, parameter, elementAnnotationMetadataFactory); + } + + @Override + public ParameterElement withAnnotationMetadata(AnnotationMetadata annotationMetadata) { + return (ParameterElement) super.withAnnotationMetadata(annotationMetadata); + } + @Override public boolean isPrimitive() { return getType().isPrimitive(); @@ -94,16 +106,22 @@ public boolean isPublic() { } @Override - public Object getNativeType() { + public Parameter getNativeType() { return parameter; } + @Override + public GroovyMethodElement getMethodElement() { + return methodElement; + } + @NonNull @Override public ClassElement getType() { if (this.typeElement == null) { - this.typeElement = visitorContext.getElementFactory().newClassElement(parameter.getType(), AstAnnotationUtils.getAnnotationMetadata(sourceUnit, compilationUnit, parameter.getType())); + this.typeElement = visitorContext.getElementFactory().newClassElement(parameter.getType(), elementAnnotationMetadataFactory); } return this.typeElement; } + } diff --git a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyPropertyElement.java b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyPropertyElement.java index aac8f81a075..f14a6caaf40 100644 --- a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyPropertyElement.java +++ b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyPropertyElement.java @@ -15,72 +15,246 @@ */ package io.micronaut.ast.groovy.visitor; -import io.micronaut.ast.groovy.utils.AstAnnotationUtils; import io.micronaut.core.annotation.AnnotationMetadata; +import io.micronaut.core.annotation.AnnotationMetadataDelegate; +import io.micronaut.core.annotation.AnnotationValue; +import io.micronaut.core.annotation.AnnotationValueBuilder; import io.micronaut.core.annotation.Internal; -import io.micronaut.core.util.CollectionUtils; +import io.micronaut.core.annotation.Nullable; +import io.micronaut.inject.annotation.AnnotationMetadataHierarchy; import io.micronaut.inject.ast.ClassElement; -import io.micronaut.inject.ast.ElementModifier; +import io.micronaut.inject.ast.Element; +import io.micronaut.inject.ast.ElementAnnotationMetadataFactory; +import io.micronaut.inject.ast.FieldElement; +import io.micronaut.inject.ast.MemberElement; +import io.micronaut.inject.ast.MethodElement; +import io.micronaut.inject.ast.ElementMutableAnnotationMetadata; import io.micronaut.inject.ast.PropertyElement; import org.codehaus.groovy.ast.AnnotatedNode; -import org.codehaus.groovy.ast.PropertyNode; -import java.util.Collections; -import java.util.Set; +import java.lang.annotation.Annotation; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.function.Consumer; +import java.util.function.Predicate; /** * Implementation of {@link PropertyElement} for Groovy. * * @author graemerocher + * @author Denis Stepanov * @since 1.0 */ @Internal -abstract class GroovyPropertyElement extends AbstractGroovyElement implements PropertyElement { - +final class GroovyPropertyElement extends AbstractGroovyElement implements PropertyElement { + private final ClassElement type; private final String name; - private final boolean readOnly; - private final Object nativeType; - private final GroovyClassElement declaringClass; - private ClassElement declaringElement; - - /** - * Default constructor. - * - * @param visitorContext The visitor context - * @param declaringClass The declaring class - * @param annotatedNode The annotated node - * @param annotationMetadata the annotation metadata - * @param name the name - * @param readOnly Whether it is read only - * @param nativeType the native underlying type - */ - GroovyPropertyElement( - GroovyVisitorContext visitorContext, - GroovyClassElement declaringClass, - AnnotatedNode annotatedNode, - AnnotationMetadata annotationMetadata, - String name, - boolean readOnly, - Object nativeType) { - super(visitorContext, annotatedNode, annotationMetadata); + private final AccessKind readAccessKind; + private final AccessKind writeAccessKind; + private final ClassElement owningElement; + @Nullable + private final MethodElement getter; + @Nullable + private final MethodElement setter; + @Nullable + private final FieldElement field; + private final boolean excluded; + private final ElementMutableAnnotationMetadata annotationMetadata; + + GroovyPropertyElement(GroovyVisitorContext visitorContext, + ClassElement owningElement, + ClassElement type, + MethodElement getter, + MethodElement setter, + FieldElement field, + ElementAnnotationMetadataFactory annotationMetadataFactory, + String name, + AccessKind readAccessKind, + AccessKind writeAccessKind, + boolean excluded) { + super(visitorContext, selectNativeType(getter, setter, field), annotationMetadataFactory); + this.type = type; + this.getter = getter; + this.setter = setter; + this.field = field; this.name = name; - this.readOnly = readOnly; - this.nativeType = nativeType; - this.declaringClass = declaringClass; + this.readAccessKind = readAccessKind; + this.writeAccessKind = writeAccessKind; + this.owningElement = owningElement; + this.excluded = excluded; + List elements = new ArrayList<>(3); + if (getter instanceof AbstractGroovyElement) { + elements.add(getter); + } + if (setter instanceof AbstractGroovyElement) { + elements.add(setter); + } + if (field instanceof AbstractGroovyElement) { + elements.add(field); + } + // The instance AnnotationMetadata of each element can change after a modification + // Set annotation metadata as actual elements so the changes are reflected + AnnotationMetadata propertyAnnotationMetadata; + if (elements.size() == 1) { + propertyAnnotationMetadata = elements.iterator().next(); + } else { + propertyAnnotationMetadata = new AnnotationMetadataHierarchy( + true, + elements.stream().map(e -> { + if (e instanceof MethodElement) { + return new AnnotationMetadataDelegate() { + @Override + public AnnotationMetadata getAnnotationMetadata() { + // Exclude type metadata + return e.getAnnotationMetadata().getDeclaredMetadata(); + } + }; + } + return e; + }).toArray(AnnotationMetadata[]::new) + ); + } + annotationMetadata = new ElementMutableAnnotationMetadata() { + + @Override + public io.micronaut.inject.ast.Element annotate(AnnotationValue annotationValue) { + for (MemberElement memberElement : elements) { + memberElement.annotate(annotationValue); + } + return GroovyPropertyElement.this; + } + + @Override + public io.micronaut.inject.ast.Element annotate(String annotationType, Consumer> consumer) { + for (MemberElement memberElement : elements) { + memberElement.annotate(annotationType, consumer); + } + return GroovyPropertyElement.this; + } + + @Override + public io.micronaut.inject.ast.Element annotate(Class annotationType) { + for (MemberElement memberElement : elements) { + memberElement.annotate(annotationType); + } + return GroovyPropertyElement.this; + } + + @Override + public io.micronaut.inject.ast.Element annotate(String annotationType) { + for (MemberElement memberElement : elements) { + memberElement.annotate(annotationType); + } + return GroovyPropertyElement.this; + } + + @Override + public io.micronaut.inject.ast.Element annotate(Class annotationType, Consumer> consumer) { + for (MemberElement memberElement : elements) { + memberElement.annotate(annotationType, consumer); + } + return GroovyPropertyElement.this; + } + + @Override + public io.micronaut.inject.ast.Element removeAnnotation(String annotationType) { + for (MemberElement memberElement : elements) { + memberElement.removeAnnotation(annotationType); + } + return GroovyPropertyElement.this; + } + + @Override + public io.micronaut.inject.ast.Element removeAnnotationIf(Predicate> predicate) { + for (MemberElement memberElement : elements) { + memberElement.removeAnnotationIf(predicate); + } + return GroovyPropertyElement.this; + } + + @Override + public AnnotationMetadata getAnnotationMetadata() { + return propertyAnnotationMetadata; + } + + }; } @Override - public Set getModifiers() { - if (isReadOnly()) { - return CollectionUtils.setOf(ElementModifier.FINAL, ElementModifier.PUBLIC); - } else { - return Collections.singleton(ElementModifier.PUBLIC); + protected AbstractGroovyElement copyConstructor() { + return new GroovyPropertyElement(visitorContext, owningElement, type, getter, setter, field, + elementAnnotationMetadataFactory, name, readAccessKind, writeAccessKind, excluded); + } + + @Override + public MemberElement withAnnotationMetadata(AnnotationMetadata annotationMetadata) { + return (MemberElement) super.withAnnotationMetadata(annotationMetadata); + } + + private static AnnotatedNode selectNativeType(MethodElement getter, + MethodElement setter, + FieldElement field) { + if (getter instanceof AbstractGroovyElement) { + return (AnnotatedNode) getter.getNativeType(); } + if (setter instanceof AbstractGroovyElement) { + return (AnnotatedNode) setter.getNativeType(); + } + if (field instanceof AbstractGroovyElement) { + return (AnnotatedNode) field.getNativeType(); + } + throw new IllegalStateException(); } @Override - public boolean isReadOnly() { - return readOnly; + public boolean isExcluded() { + return excluded; + } + + @Override + public ElementMutableAnnotationMetadata getAnnotationMetadata() { + return annotationMetadata; + } + + @Override + public ClassElement getType() { + return type; + } + + @Override + public ClassElement getGenericType() { + return type; // Already generic + } + + @Override + public Optional getField() { + return Optional.ofNullable(field); + } + + @Override + public Optional getWriteMethod() { + return Optional.ofNullable(setter); + } + + @Override + public Optional getReadMethod() { + return Optional.ofNullable(getter); + } + + @Override + public boolean isPrimitive() { + return getType().isPrimitive(); + } + + @Override + public boolean isArray() { + return getType().isArray(); + } + + @Override + public int getArrayDimensions() { + return getType().getArrayDimensions(); } @Override @@ -99,37 +273,61 @@ public boolean isPublic() { } @Override - public Object getNativeType() { - return nativeType; + public String toString() { + return getDeclaringType().getName() + "." + name; } @Override - public String toString() { - return getName(); + public AccessKind getReadAccessKind() { + return readAccessKind; + } + + @Override + public AccessKind getWriteAccessKind() { + return writeAccessKind; + } + + @Override + public boolean isReadOnly() { + switch (readAccessKind) { + case METHOD: + return setter == null; + case FIELD: + return field == null || field.isFinal(); + default: + throw new IllegalStateException(); + } + } + + @Override + public boolean isWriteOnly() { + switch (writeAccessKind) { + case METHOD: + return getter == null; + case FIELD: + return field == null; + default: + throw new IllegalStateException(); + } } @Override public ClassElement getDeclaringType() { - if (declaringElement == null && nativeType instanceof PropertyNode) { - PropertyNode propertyNode = (PropertyNode) nativeType; - declaringElement = visitorContext.getElementFactory().newClassElement( - propertyNode.getDeclaringClass(), - AstAnnotationUtils.getAnnotationMetadata( - sourceUnit, - compilationUnit, - propertyNode.getDeclaringClass() - ) - ); + if (field != null) { + return field.getDeclaringType(); + } + if (getter != null) { + return getter.getDeclaringType(); } - if (declaringElement != null) { - return declaringElement; + if (setter != null) { + return setter.getDeclaringType(); } - return declaringClass; + throw new IllegalStateException(); } @Override public ClassElement getOwningType() { - return declaringClass; + return owningElement; } } diff --git a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyVisitorContext.java b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyVisitorContext.java index 23722e8e51b..abeb5caa3a9 100644 --- a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyVisitorContext.java +++ b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyVisitorContext.java @@ -15,21 +15,24 @@ */ package io.micronaut.ast.groovy.visitor; +import groovy.lang.GroovyClassLoader; +import io.micronaut.ast.groovy.annotation.GroovyAnnotationMetadataBuilder; +import io.micronaut.ast.groovy.annotation.GroovyElementAnnotationMetadataFactory; +import io.micronaut.ast.groovy.scan.ClassPathAnnotationScanner; +import io.micronaut.ast.groovy.utils.AstMessageUtils; import io.micronaut.core.annotation.Internal; import io.micronaut.core.annotation.NonNull; import io.micronaut.core.annotation.Nullable; -import groovy.lang.GroovyClassLoader; -import io.micronaut.ast.groovy.utils.AstAnnotationUtils; -import io.micronaut.core.annotation.AnnotationMetadata; import io.micronaut.core.convert.ArgumentConversionContext; import io.micronaut.core.convert.value.MutableConvertibleValues; import io.micronaut.core.convert.value.MutableConvertibleValuesMap; -import io.micronaut.ast.groovy.scan.ClassPathAnnotationScanner; import io.micronaut.core.reflect.ClassUtils; import io.micronaut.core.util.ArgumentUtils; import io.micronaut.core.util.CollectionUtils; +import io.micronaut.inject.annotation.AbstractAnnotationMetadataBuilder; import io.micronaut.inject.ast.ClassElement; import io.micronaut.inject.ast.Element; +import io.micronaut.inject.ast.ElementAnnotationMetadataFactory; import io.micronaut.inject.util.VisitorContextUtils; import io.micronaut.inject.visitor.VisitorContext; import io.micronaut.inject.writer.AbstractBeanDefinitionBuilder; @@ -39,18 +42,20 @@ import org.codehaus.groovy.ast.ClassHelper; import org.codehaus.groovy.ast.ClassNode; import org.codehaus.groovy.control.CompilationUnit; -import org.codehaus.groovy.control.ErrorCollector; import org.codehaus.groovy.control.Janitor; import org.codehaus.groovy.control.SourceUnit; -import org.codehaus.groovy.control.messages.Message; -import org.codehaus.groovy.control.messages.SimpleMessage; -import org.codehaus.groovy.control.messages.SyntaxErrorMessage; -import org.codehaus.groovy.syntax.SyntaxException; import java.io.IOException; import java.io.OutputStream; import java.net.URL; -import java.util.*; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Enumeration; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; /** * The visitor context when visiting Groovy code. @@ -61,7 +66,6 @@ */ public class GroovyVisitorContext implements VisitorContext { private static final MutableConvertibleValues VISITOR_ATTRIBUTES = new MutableConvertibleValuesMap<>(); - private final ErrorCollector errorCollector; private final CompilationUnit compilationUnit; private final ClassWriterOutputVisitor outputVisitor; private final SourceUnit sourceUnit; @@ -69,6 +73,7 @@ public class GroovyVisitorContext implements VisitorContext { private final List generatedResources = new ArrayList<>(); private final GroovyElementFactory groovyElementFactory; private final List beanDefinitionBuilders = new ArrayList<>(); + private final GroovyElementAnnotationMetadataFactory elementAnnotationMetadataFactory; /** * @param sourceUnit The source unit @@ -85,11 +90,11 @@ public GroovyVisitorContext(SourceUnit sourceUnit, @Nullable CompilationUnit com */ public GroovyVisitorContext(SourceUnit sourceUnit, @Nullable CompilationUnit compilationUnit, ClassWriterOutputVisitor outputVisitor) { this.sourceUnit = sourceUnit; - this.errorCollector = sourceUnit != null ? sourceUnit.getErrorCollector() : null; this.compilationUnit = compilationUnit; this.outputVisitor = outputVisitor; this.attributes = VISITOR_ATTRIBUTES; this.groovyElementFactory = new GroovyElementFactory(this); + this.elementAnnotationMetadataFactory = new GroovyElementAnnotationMetadataFactory(false, new GroovyAnnotationMetadataBuilder(sourceUnit, compilationUnit)); } @NonNull @@ -105,34 +110,32 @@ public Iterable getClasspathResources(@NonNull String path) { @Override public Optional getClassElement(String name) { + return getClassElement(name, getElementAnnotationMetadataFactory()); + } + + @Override + public Optional getClassElement(String name, ElementAnnotationMetadataFactory annotationMetadataFactory) { if (name == null || compilationUnit == null) { return Optional.empty(); } - ClassNode classNode = Optional.ofNullable(compilationUnit.getClassNode(name)) - .orElseGet(() -> { - if (sourceUnit != null) { - GroovyClassLoader classLoader = sourceUnit.getClassLoader(); - if (classLoader != null) { - return ClassUtils.forName(name, classLoader).map(ClassHelper::make).orElse(null); - } + .orElseGet(() -> { + if (sourceUnit != null) { + GroovyClassLoader classLoader = sourceUnit.getClassLoader(); + if (classLoader != null) { + return ClassUtils.forName(name, classLoader).map(ClassHelper::make).orElse(null); } - return null; - }); + } + return null; + }); - return Optional.ofNullable(classNode) - .map(cn -> groovyElementFactory.newClassElement(cn, AstAnnotationUtils.getAnnotationMetadata(sourceUnit, compilationUnit, cn))); + return Optional.ofNullable(classNode).map(cn -> groovyElementFactory.newClassElement(cn, annotationMetadataFactory)); } @Override public Optional getClassElement(Class type) { final ClassNode classNode = ClassHelper.makeCached(type); - final AnnotationMetadata annotationMetadata = AstAnnotationUtils - .getAnnotationMetadata(sourceUnit, compilationUnit, classNode); - final ClassElement classElement = groovyElementFactory.newClassElement(classNode, annotationMetadata); - return Optional.of( - classElement - ); + return Optional.of(groovyElementFactory.newClassElement(classNode, getElementAnnotationMetadataFactory())); } @NonNull @@ -150,7 +153,7 @@ public ClassElement[] getClassElements(@NonNull String aPackage, @NonNull String for (String s : stereotypes) { scanner.scan(s, aPackage).forEach(aClass -> { final ClassNode classNode = ClassHelper.make(aClass); - classElements.add(groovyElementFactory.newClassElement(classNode, AstAnnotationUtils.getAnnotationMetadata(sourceUnit, compilationUnit, classNode))); + classElements.add(groovyElementFactory.newClassElement(classNode, getElementAnnotationMetadataFactory())); }); } return classElements.toArray(new ClassElement[0]); @@ -162,6 +165,17 @@ public GroovyElementFactory getElementFactory() { return groovyElementFactory; } + @NonNull + @Override + public GroovyElementAnnotationMetadataFactory getElementAnnotationMetadataFactory() { + return elementAnnotationMetadataFactory; + } + + @Override + public AbstractAnnotationMetadataBuilder getAnnotationMetadataBuilder() { + return new GroovyAnnotationMetadataBuilder(sourceUnit, compilationUnit); + } + @Override public void info(String message, @Nullable Element element) { StringBuilder msg = new StringBuilder("Note: ").append(message); @@ -180,27 +194,24 @@ public void info(String message) { @Override public void fail(String message, @Nullable Element element) { - Message msg; if (element instanceof AbstractGroovyElement) { - msg = buildErrorMessage(message, element); + AstMessageUtils.error(sourceUnit, ((AbstractGroovyElement) element).getNativeType(), message); } else { - msg = new SimpleMessage(message, sourceUnit); - } - if (errorCollector != null) { - errorCollector.addError(msg); + AstMessageUtils.error(sourceUnit, null, message); } } + public final void fail(String message, ASTNode expr) { + AstMessageUtils.error(sourceUnit, expr, message); + } + @Override public void warn(String message, @Nullable Element element) { - StringBuilder msg = new StringBuilder("WARNING: ").append(message); - if (element != null) { - ASTNode expr = (ASTNode) element.getNativeType(); - final String sample = sourceUnit.getSample(expr.getLineNumber(), expr.getColumnNumber(), new Janitor()); - msg.append("\n\n").append(sample); + if (element instanceof AbstractGroovyElement) { + AstMessageUtils.warning(sourceUnit, ((AbstractGroovyElement) element).getNativeType(), message); + } else { + AstMessageUtils.warning(sourceUnit, null, message); } - System.out.println(msg); - } @Override @@ -263,13 +274,6 @@ public Map getOptions() { return VisitorContextUtils.getSystemOptions(); } - private SyntaxErrorMessage buildErrorMessage(String message, Element element) { - ASTNode expr = (ASTNode) element.getNativeType(); - return new SyntaxErrorMessage( - new SyntaxException(message + '\n', expr.getLineNumber(), expr.getColumnNumber(), - expr.getLastLineNumber(), expr.getLastColumnNumber()), sourceUnit); - } - @Override public MutableConvertibleValues put(CharSequence key, @Nullable Object value) { return attributes.put(key, value); diff --git a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyWildcardElement.java b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyWildcardElement.java index 4d1e3390312..8aadf358330 100644 --- a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyWildcardElement.java +++ b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyWildcardElement.java @@ -19,6 +19,7 @@ import io.micronaut.core.annotation.NonNull; import io.micronaut.inject.ast.ArrayableClassElement; import io.micronaut.inject.ast.ClassElement; +import io.micronaut.inject.ast.ElementAnnotationMetadataFactory; import io.micronaut.inject.ast.WildcardElement; import java.util.List; @@ -36,18 +37,25 @@ final class GroovyWildcardElement extends GroovyClassElement implements Wildcard private final List upperBounds; private final List lowerBounds; - GroovyWildcardElement(@NonNull List upperBounds, @NonNull List lowerBounds) { + GroovyWildcardElement(@NonNull List upperBounds, + @NonNull List lowerBounds, + ElementAnnotationMetadataFactory annotationMetadataFactory) { super( - upperBounds.get(0).visitorContext, - upperBounds.get(0).classNode, - upperBounds.get(0).getAnnotationMetadata(), - upperBounds.get(0).getGenericTypeInfo(), - 0 + upperBounds.get(0).visitorContext, + upperBounds.get(0).classNode, + annotationMetadataFactory, + upperBounds.get(0).getGenericTypeInfo(), + 0 ); this.upperBounds = upperBounds; this.lowerBounds = lowerBounds; } + @Override + protected GroovyClassElement copyConstructor() { + return new GroovyWildcardElement(upperBounds, lowerBounds, elementAnnotationMetadataFactory); + } + @NonNull @Override public List getUpperBounds() { @@ -72,7 +80,7 @@ public ClassElement withArrayDimensions(int arrayDimensions) { public ClassElement foldBoundGenericTypes(@NonNull Function fold) { List upperBounds = this.upperBounds.stream().map(ele -> toGroovyClassElement(ele.foldBoundGenericTypes(fold))).collect(Collectors.toList()); List lowerBounds = this.lowerBounds.stream().map(ele -> toGroovyClassElement(ele.foldBoundGenericTypes(fold))).collect(Collectors.toList()); - return fold.apply(upperBounds.contains(null) || lowerBounds.contains(null) ? null : new GroovyWildcardElement(upperBounds, lowerBounds)); + return fold.apply(upperBounds.contains(null) || lowerBounds.contains(null) ? null : new GroovyWildcardElement(upperBounds, lowerBounds, elementAnnotationMetadataFactory)); } private GroovyClassElement toGroovyClassElement(ClassElement element) { @@ -82,10 +90,10 @@ private GroovyClassElement toGroovyClassElement(ClassElement element) { if (element.isWildcard() || element.isGenericPlaceholder()) { throw new UnsupportedOperationException("Cannot convert wildcard / free type variable to GroovyClassElement"); } else { - return (GroovyClassElement) ((ArrayableClassElement) visitorContext.getClassElement(element.getName()) - .orElseThrow(() -> new UnsupportedOperationException("Cannot convert ClassElement to GroovyClassElement, class was not found on the visitor context"))) - .withArrayDimensions(element.getArrayDimensions()) - .withBoundGenericTypes(element.getBoundGenericTypes()); + return (GroovyClassElement) ((ArrayableClassElement) visitorContext.getClassElement(element.getName(), elementAnnotationMetadataFactory) + .orElseThrow(() -> new UnsupportedOperationException("Cannot convert ClassElement to GroovyClassElement, class was not found on the visitor context"))) + .withArrayDimensions(element.getArrayDimensions()) + .withBoundGenericTypes(element.getBoundGenericTypes()); } } } diff --git a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/LoadedVisitor.groovy b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/LoadedVisitor.groovy index 3cf51898837..07c373cfd5b 100644 --- a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/LoadedVisitor.groovy +++ b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/LoadedVisitor.groovy @@ -15,26 +15,16 @@ */ package io.micronaut.ast.groovy.visitor -import io.micronaut.core.annotation.Nullable import groovy.transform.CompileStatic -import io.micronaut.ast.groovy.utils.AstAnnotationUtils import io.micronaut.core.annotation.AnnotationMetadata import io.micronaut.core.annotation.Internal import io.micronaut.core.order.Ordered -import io.micronaut.inject.ast.ClassElement -import io.micronaut.inject.ast.Element import io.micronaut.inject.visitor.TypeElementVisitor -import org.codehaus.groovy.ast.AnnotatedNode import org.codehaus.groovy.ast.ClassHelper import org.codehaus.groovy.ast.ClassNode -import org.codehaus.groovy.ast.ConstructorNode -import org.codehaus.groovy.ast.FieldNode import org.codehaus.groovy.ast.GenericsType -import org.codehaus.groovy.ast.MethodNode -import org.codehaus.groovy.ast.PropertyNode import org.codehaus.groovy.control.CompilationUnit import org.codehaus.groovy.control.SourceUnit - /** * Used to store a reference to an underlying {@link TypeElementVisitor} and * optionally invoke the visit methods on the visitor if it matches the @@ -51,7 +41,6 @@ class LoadedVisitor implements Ordered { private final TypeElementVisitor visitor private final String classAnnotation private final String elementAnnotation - private ClassElement currentClassElement private final CompilationUnit compilationUnit private static final String OBJECT_CLASS = Object.class.getName() @@ -114,15 +103,15 @@ class LoadedVisitor implements Ordered { String toString() { visitor.toString() } + /** - * @param classNode The class node - * @return True if the class node should be visited + * @param annotationMetadata The annotation data + * @return True if the class should be visited */ - boolean matches(ClassNode classNode) { + boolean matchesClass(AnnotationMetadata annotationMetadata) { if (classAnnotation == ClassHelper.OBJECT) { return true } - AnnotationMetadata annotationMetadata = AstAnnotationUtils.getAnnotationMetadata(sourceUnit, compilationUnit, classNode) return annotationMetadata.hasStereotype(classAnnotation) } @@ -130,58 +119,13 @@ class LoadedVisitor implements Ordered { * @param annotationMetadata The annotation data * @return True if the element should be visited */ - boolean matches(AnnotationMetadata annotationMetadata) { + boolean matchesElement(AnnotationMetadata annotationMetadata) { if (elementAnnotation == ClassHelper.OBJECT) { return true } return annotationMetadata.hasStereotype(elementAnnotation) } - /** - * Invoke the underlying visitor for the given node. - * - * @param annotatedNode The node to visit - * @param annotationMetadata The annotation data for the node - * @param visitorContext the Groovy visitor context - */ - @Nullable Element visit(AnnotatedNode annotatedNode, AnnotationMetadata annotationMetadata, GroovyVisitorContext visitorContext) { - switch (annotatedNode.getClass()) { - case PropertyNode: - def e = visitorContext.getElementFactory().newFieldElement((PropertyNode) annotatedNode, annotationMetadata) - visitor.visitField(e, visitorContext) - return e - case FieldNode: - def field = (FieldNode) annotatedNode; - if (field.enum) { - def e = visitorContext.getElementFactory().newEnumConstantElement(currentClassElement, (FieldNode) annotatedNode, annotationMetadata) - visitor.visitEnumConstant(e, visitorContext) - return e - } else { - def e = visitorContext.getElementFactory().newFieldElement(currentClassElement, (FieldNode) annotatedNode, annotationMetadata) - visitor.visitField(e, visitorContext) - return e - } - case ConstructorNode: - def e = visitorContext.getElementFactory().newConstructorElement(currentClassElement, (ConstructorNode) annotatedNode, annotationMetadata) - visitor.visitConstructor(e, visitorContext) - return e - case MethodNode: - if (currentClassElement != null) { - def e = visitorContext.getElementFactory().newSourceMethodElement(currentClassElement, (MethodNode) annotatedNode, annotationMetadata) - visitor.visitMethod(e, visitorContext) - return e - } - break - case ClassNode: - ClassNode cn = (ClassNode) annotatedNode - currentClassElement = visitorContext.getElementFactory().newSourceClassElement(cn, annotationMetadata) - visitor.visitClass(currentClassElement, visitorContext) - return currentClassElement - } - - return null - } - void start(GroovyVisitorContext visitorContext) { visitor.start(visitorContext) } diff --git a/inject-groovy/src/test/groovy/io/micronaut/aop/adapter/MethodAdapterSpec.groovy b/inject-groovy/src/test/groovy/io/micronaut/aop/adapter/MethodAdapterSpec.groovy index 7f39c54a112..2b495e9ec47 100644 --- a/inject-groovy/src/test/groovy/io/micronaut/aop/adapter/MethodAdapterSpec.groovy +++ b/inject-groovy/src/test/groovy/io/micronaut/aop/adapter/MethodAdapterSpec.groovy @@ -75,23 +75,23 @@ import io.micronaut.runtime.event.annotation.*; public class Test { boolean invoked = false; boolean shutdown = false; - + public boolean getInvoked() { return invoked; - } + } public boolean isShutdown() { return shutdown; } - + @EventListener void receive(StartupEvent event) { invoked = true; } - + @EventListener void receive(ShutdownEvent event) { shutdown = true; - } + } } ''') @@ -124,7 +124,7 @@ class MethodAdapterTest { @Adapter(ApplicationEventListener.class) void onStartup(StartupEvent event) { - + } } @@ -151,7 +151,7 @@ class MethodAdapterTest implements Contract { @Override void onStartup(StartupEvent event) { - + } } @@ -182,7 +182,7 @@ class MethodAdapterTest2 { @Adapter(Foo.class) void myMethod(String blah) { - + } } @@ -211,7 +211,7 @@ class MethodAdapterTest3 { @Adapter(Foo.class) void myMethod(Integer blah) { - + } } @@ -219,7 +219,7 @@ interface Foo extends java.util.function.Consumer {} ''') then:"An error occurs" def e = thrown(RuntimeException) - e.message.contains('Cannot adapt method [test.MethodAdapterTest3.myMethod(..)] to target method [java.util.function.Consumer.accept(..)]. Argument type [java.lang.Integer] is not a subtype of type [java.lang.CharSequence] at position 0.') + e.message.contains('Cannot adapt method [test.MethodAdapterTest3.myMethod(..)] to target method [java.util.function.Consumer.accept(..)]. Type [java.lang.Integer] is not a subtype of type [java.lang.CharSequence] for argument at position 0') } void "test method adapter wrong argument count"() { @@ -237,7 +237,7 @@ class MethodAdapterTest4 { @Adapter(ApplicationEventListener.class) void onStartup(StartupEvent event, boolean stuff) { - + } } diff --git a/inject-groovy/src/test/groovy/io/micronaut/aop/compile/AbstractClassIntroductionSpec.groovy b/inject-groovy/src/test/groovy/io/micronaut/aop/compile/AbstractClassIntroductionSpec.groovy index a5a55a1b5c7..f3255ef1d84 100644 --- a/inject-groovy/src/test/groovy/io/micronaut/aop/compile/AbstractClassIntroductionSpec.groovy +++ b/inject-groovy/src/test/groovy/io/micronaut/aop/compile/AbstractClassIntroductionSpec.groovy @@ -20,13 +20,10 @@ import io.micronaut.context.DefaultBeanContext import io.micronaut.inject.BeanDefinition import io.micronaut.inject.BeanFactory import io.micronaut.inject.writer.BeanDefinitionVisitor -import spock.lang.Ignore - /** * @author graemerocher * @since 1.0 */ -@Ignore // for some reason fails when run with Gradle.. probably due to order of compilation class AbstractClassIntroductionSpec extends AbstractBeanDefinitionSpec { void "test that a non-abstract method defined in class is not overridden by the introduction advise"() { @@ -40,8 +37,8 @@ import io.micronaut.context.annotation.*; @Stub @jakarta.inject.Singleton abstract class AbstractBean { - public abstract String isAbstract(); - + public abstract String isAbstract(); + public String nonAbstract() { return "good"; } @@ -78,8 +75,8 @@ interface Foo { @Stub @jakarta.inject.Singleton abstract class AbstractBean implements Foo { - public abstract String isAbstract(); - + public abstract String isAbstract(); + @Override public String nonAbstract() { return "good"; @@ -118,8 +115,8 @@ interface Foo { @Stub @jakarta.inject.Singleton abstract class AbstractBean implements Foo { - public abstract String isAbstract(); - + public abstract String isAbstract(); + @Override public String nonAbstract() { return "good"; @@ -154,7 +151,7 @@ import io.micronaut.context.annotation.*; interface Bar { @Stub String nonAbstract(); - + String another(); } @@ -165,13 +162,13 @@ interface Foo extends Bar { @Stub @jakarta.inject.Singleton abstract class AbstractBean implements Foo { - public abstract String isAbstract(); - + public abstract String isAbstract(); + @Override public String nonAbstract() { return "good"; } - + @Override public String another() { return "good"; @@ -211,8 +208,8 @@ interface Foo { @Stub @jakarta.inject.Singleton abstract class AbstractBean implements Foo { - public abstract String isAbstract(); - + public abstract String isAbstract(); + @Override public String nonAbstract() { return "good"; diff --git a/inject-groovy/src/test/groovy/io/micronaut/aop/compile/FinalModifierSpec.groovy b/inject-groovy/src/test/groovy/io/micronaut/aop/compile/FinalModifierSpec.groovy index 10720dfe051..84f178b3709 100644 --- a/inject-groovy/src/test/groovy/io/micronaut/aop/compile/FinalModifierSpec.groovy +++ b/inject-groovy/src/test/groovy/io/micronaut/aop/compile/FinalModifierSpec.groovy @@ -32,11 +32,11 @@ import io.micronaut.context.annotation.*; final class FinalModifierMyBean1 { private String myValue; - + FinalModifierMyBean1(String val) { this.myValue = val; } - + public String someMethod() { return myValue; } @@ -61,11 +61,11 @@ import io.micronaut.context.annotation.*; class FinalModifierMyBean2 { private String myValue; - + FinalModifierMyBean2(String val) { this.myValue = val; } - + public final String someMethod() { return myValue; } @@ -74,7 +74,7 @@ class FinalModifierMyBean2 { ''') then: def e = thrown(RuntimeException) - e.message.contains 'Public method inherits AOP advice but is declared final. Change the method to be non-final in order for AOP advice to be applied.' + e.message.contains 'Public method inherits AOP advice but is declared final. Either make the method non-public or apply AOP advice only to public methods declared on the class.' } void "test final modifier on method with explicit AOP advice doesn't compile"() { @@ -89,11 +89,11 @@ import io.micronaut.context.annotation.*; class FinalModifierMyBean2 { private String myValue; - + FinalModifierMyBean2(String val) { this.myValue = val; } - + @Mutating("someVal") public final String someMethod() { return myValue; diff --git a/inject-groovy/src/test/groovy/io/micronaut/aop/introduction/MyRepoIntroductionSpec.groovy b/inject-groovy/src/test/groovy/io/micronaut/aop/introduction/MyRepoIntroductionSpec.groovy index 34380722bb8..3b177e5f5f5 100644 --- a/inject-groovy/src/test/groovy/io/micronaut/aop/introduction/MyRepoIntroductionSpec.groovy +++ b/inject-groovy/src/test/groovy/io/micronaut/aop/introduction/MyRepoIntroductionSpec.groovy @@ -34,8 +34,8 @@ class MyRepoIntroductionSpec extends Specification { def interceptorDeclaredMethods = Arrays.stream(bean.getClass().getMethods()).filter(m -> m.getDeclaringClass() == bean.getClass()).collect(Collectors.toList()) def repoDeclaredMethods = Arrays.stream(MyRepo.class.getMethods()).filter(m -> m.getDeclaringClass() == MyRepo.class).collect(Collectors.toList()) then: - repoDeclaredMethods.size() == 3 - interceptorDeclaredMethods.size() == 3 + repoDeclaredMethods.size() == 3 // Groovy will exclude overridden methods (Java would have 4) + interceptorDeclaredMethods.size() == 4 // We need to intercept overridden methods bean.getClass().getName().contains("Intercepted") MyRepoIntroducer.EXECUTED_METHODS.isEmpty() when: @@ -57,7 +57,7 @@ class MyRepoIntroductionSpec extends Specification { def findByIdMethods = beanDef.getExecutableMethods().findAll(m -> m.getName() == "findById") then: MyRepoIntroducer.EXECUTED_METHODS.size() == 0 - findByIdMethods.size() == 2 + findByIdMethods.size() == 1 findByIdMethods[0].hasAnnotation(Marker) when: Object id = 111 @@ -80,7 +80,7 @@ class MyRepoIntroductionSpec extends Specification { def findByIdMethods = beanDef.getExecutableMethods().findAll(m -> m.getName() == "findById") then: MyRepoIntroducer.EXECUTED_METHODS.size() == 0 - findByIdMethods.size() == 2 + findByIdMethods.size() == 1 findByIdMethods[0].hasAnnotation(Marker) when: Object id = 111 @@ -103,7 +103,7 @@ class MyRepoIntroductionSpec extends Specification { def findByIdMethods = beanDef.getExecutableMethods().findAll(m -> m.getName() == "findById") then: MyRepoIntroducer.EXECUTED_METHODS.size() == 0 - findByIdMethods.size() == 2 + findByIdMethods.size() == 1 findByIdMethods[0].hasAnnotation(Marker) when: def id = 111 diff --git a/inject-groovy/src/test/groovy/io/micronaut/aop/introduction/Stub.groovy b/inject-groovy/src/test/groovy/io/micronaut/aop/introduction/Stub.groovy index 0cf1f9e1efe..e2edb76f2f5 100644 --- a/inject-groovy/src/test/groovy/io/micronaut/aop/introduction/Stub.groovy +++ b/inject-groovy/src/test/groovy/io/micronaut/aop/introduction/Stub.groovy @@ -16,14 +16,13 @@ package io.micronaut.aop.introduction import io.micronaut.context.annotation.Executable -import io.micronaut.context.annotation.Type; -import io.micronaut.aop.Introduction; -import io.micronaut.context.annotation.Type; +import io.micronaut.aop.Introduction +import io.micronaut.context.annotation.Type -import java.lang.annotation.Documented; -import java.lang.annotation.Retention; +import java.lang.annotation.Documented +import java.lang.annotation.Retention -import static java.lang.annotation.RetentionPolicy.RUNTIME; +import static java.lang.annotation.RetentionPolicy.RUNTIME /** * @author Graeme Rocher @@ -34,5 +33,5 @@ import static java.lang.annotation.RetentionPolicy.RUNTIME; @Documented @Retention(RUNTIME) @Executable -public @interface Stub { +@interface Stub { } diff --git a/inject-groovy/src/test/groovy/io/micronaut/aop/introduction/with_around/IntroductionWithAroundOnConcreteClassSpec.groovy b/inject-groovy/src/test/groovy/io/micronaut/aop/introduction/with_around/IntroductionWithAroundOnConcreteClassSpec.groovy index ed069ae0ff0..6298f078218 100644 --- a/inject-groovy/src/test/groovy/io/micronaut/aop/introduction/with_around/IntroductionWithAroundOnConcreteClassSpec.groovy +++ b/inject-groovy/src/test/groovy/io/micronaut/aop/introduction/with_around/IntroductionWithAroundOnConcreteClassSpec.groovy @@ -78,8 +78,8 @@ class Test {} def proxyTargetBeanDefinition = applicationContext.getProxyTargetBeanDefinition(clazz, null) def beanDefinition = applicationContext.getBeanDefinition(clazz, null) then: - proxyTargetBeanDefinition.getExecutableMethods().size() == 1 - beanDefinition.getExecutableMethods().size() == 1 + proxyTargetBeanDefinition.getExecutableMethods().size() == 5 + beanDefinition.getExecutableMethods().size() == 5 } void "test executable methods count for around with executable"() { diff --git a/inject-groovy/src/test/groovy/io/micronaut/ast/groovy/visitor/GroovyBeanPropertiesSpec.groovy b/inject-groovy/src/test/groovy/io/micronaut/ast/groovy/visitor/GroovyBeanPropertiesSpec.groovy index f2b4f4c7c06..7254d23bcf1 100644 --- a/inject-groovy/src/test/groovy/io/micronaut/ast/groovy/visitor/GroovyBeanPropertiesSpec.groovy +++ b/inject-groovy/src/test/groovy/io/micronaut/ast/groovy/visitor/GroovyBeanPropertiesSpec.groovy @@ -1,7 +1,6 @@ package io.micronaut.ast.groovy.visitor import io.micronaut.ast.transform.test.AbstractBeanDefinitionSpec -import io.micronaut.inject.ast.ClassElement import javax.validation.constraints.NotBlank @@ -19,7 +18,7 @@ class Test extends SuperClass { @NotBlank @NotNull private String tenant - + String getTenant() { return tenant } diff --git a/inject-groovy/src/test/groovy/io/micronaut/ast/groovy/visitor/GroovyEnclosedElementsSpec.groovy b/inject-groovy/src/test/groovy/io/micronaut/ast/groovy/visitor/GroovyEnclosedElementsSpec.groovy index 3765686a116..ce451e8f1a7 100644 --- a/inject-groovy/src/test/groovy/io/micronaut/ast/groovy/visitor/GroovyEnclosedElementsSpec.groovy +++ b/inject-groovy/src/test/groovy/io/micronaut/ast/groovy/visitor/GroovyEnclosedElementsSpec.groovy @@ -15,7 +15,7 @@ class Test extends SuperType { static {} Test() {} - + Test(int i) {} } @@ -23,7 +23,7 @@ class SuperType { static {} SuperType() {} - + SuperType(String s) {} } ''') diff --git a/inject-groovy/src/test/groovy/io/micronaut/ast/groovy/visitor/GroovyEnumElementSpec.groovy b/inject-groovy/src/test/groovy/io/micronaut/ast/groovy/visitor/GroovyEnumElementSpec.groovy index 279cf79deaa..e3d8ed3ec41 100644 --- a/inject-groovy/src/test/groovy/io/micronaut/ast/groovy/visitor/GroovyEnumElementSpec.groovy +++ b/inject-groovy/src/test/groovy/io/micronaut/ast/groovy/visitor/GroovyEnumElementSpec.groovy @@ -60,7 +60,7 @@ package test enum MyEnum { @io.micronaut.ast.groovy.visitor.EnumConstantAnn("C") - A, + A, B } """) diff --git a/inject-groovy/src/test/groovy/io/micronaut/inject/annotation/AnnotationMetadataWriterSpec.groovy b/inject-groovy/src/test/groovy/io/micronaut/inject/annotation/AnnotationMetadataWriterSpec.groovy index 8b1bc9275cc..2831515afe3 100644 --- a/inject-groovy/src/test/groovy/io/micronaut/inject/annotation/AnnotationMetadataWriterSpec.groovy +++ b/inject-groovy/src/test/groovy/io/micronaut/inject/annotation/AnnotationMetadataWriterSpec.groovy @@ -46,7 +46,7 @@ import io.micronaut.inject.annotation.Outer; @Outer.Inner class Test { - + } """) @@ -255,7 +255,7 @@ public @interface EnumAnn { enum MyEnum { ONE, TWO; - + @Override public String toString() { return this == ONE ? "1" : "2"; @@ -391,14 +391,14 @@ class Test { void "test basic argument metadata"() { given: - AnnotationMetadata metadata = buildFieldAnnotationMetadata("annmetawriter12.Test", ''' + AnnotationMetadata metadata = buildParameterAnnotationMetadata("annmetawriter12.Test", ''' package annmetawriter12; @javax.inject.Singleton class Test { void test(@javax.inject.Named("foo") String id) { - + } } ''', 'test', 'id') @@ -412,7 +412,7 @@ class Test { void "test argument metadata inheritance"() { given: - AnnotationMetadata metadata = buildFieldAnnotationMetadata("annmetawriter13.Test", ''' + AnnotationMetadata metadata = buildParameterAnnotationMetadata("annmetawriter13.Test", ''' package annmetawriter13; import java.lang.annotation.*; @@ -423,7 +423,7 @@ class Test implements TestApi { @jakarta.annotation.PostConstruct @java.lang.Override public void test(String id) { - + } } @@ -435,7 +435,7 @@ interface TestApi { @Inherited @Retention(RetentionPolicy.RUNTIME) -@jakarta.inject.Named("foo") +@jakarta.inject.Named("foo") @interface MyAnn {} ''', 'test', 'id') diff --git a/inject-groovy/src/test/groovy/io/micronaut/inject/annotation/RemoveAnnotationSpec.groovy b/inject-groovy/src/test/groovy/io/micronaut/inject/annotation/RemoveAnnotationSpec.groovy index 5e72252c202..bb39f5495e6 100644 --- a/inject-groovy/src/test/groovy/io/micronaut/inject/annotation/RemoveAnnotationSpec.groovy +++ b/inject-groovy/src/test/groovy/io/micronaut/inject/annotation/RemoveAnnotationSpec.groovy @@ -31,7 +31,7 @@ import io.micronaut.context.annotation.Bean; @Bean class Test { -} +} ''') expect: definition diff --git a/inject-groovy/src/test/groovy/io/micronaut/inject/configproperties/ChildConfigProperties.groovy b/inject-groovy/src/test/groovy/io/micronaut/inject/configproperties/ChildConfigProperties.groovy new file mode 100644 index 00000000000..91290f1ced9 --- /dev/null +++ b/inject-groovy/src/test/groovy/io/micronaut/inject/configproperties/ChildConfigProperties.groovy @@ -0,0 +1,13 @@ +package io.micronaut.inject.configproperties + +import io.micronaut.context.annotation.ConfigurationProperties +import io.micronaut.inject.configproperties.other.ParentConfigProperties + +@ConfigurationProperties("child") +class ChildConfigProperties extends ParentConfigProperties { + + protected void setName(String name) { + super.setName(name) + } + +} diff --git a/inject-groovy/src/test/groovy/io/micronaut/inject/configproperties/InheritedConfigurationReaderPrefixSpec.groovy b/inject-groovy/src/test/groovy/io/micronaut/inject/configproperties/InheritedConfigurationReaderPrefixSpec.groovy index 69c91039020..e2b7209837e 100644 --- a/inject-groovy/src/test/groovy/io/micronaut/inject/configproperties/InheritedConfigurationReaderPrefixSpec.groovy +++ b/inject-groovy/src/test/groovy/io/micronaut/inject/configproperties/InheritedConfigurationReaderPrefixSpec.groovy @@ -21,23 +21,79 @@ import io.micronaut.inject.BeanDefinition class InheritedConfigurationReaderPrefixSpec extends AbstractBeanDefinitionSpec { - void "test property paths are correct"() { + void "property path is broken because alias is pointing to another alias"() { given: - BeanDefinition beanDefinition = buildBeanDefinition('io.micronaut.inject.configproperties.MyBean', ''' -package io.micronaut.inject.configproperties -; + BeanDefinition beanDefinition = buildBeanDefinition('io.micronaut.inject.configproperties.MyBean', """ +package io.micronaut.inject.configproperties; -@TestEndpoint("simple") +@TestEndpoint1("simple") class MyBean { String myValue } -''') +""") expect: beanDefinition.getInjectedMethods()[0].name == 'setMyValue' def metadata = beanDefinition.getInjectedMethods()[0].getAnnotationMetadata() metadata.hasAnnotation(Property) - metadata.getValue(Property, "name", String).get() == 'endpoints.simple.my-value' + metadata.getValue(Property, "name", String).get() == 'endpoints.my-value' + } + + void "property path is overriding the existing one without base prefix"() { + given: + BeanDefinition beanDefinition = buildBeanDefinition('io.micronaut.inject.configproperties.MyBean', """ +package io.micronaut.inject.configproperties; + +@TestEndpoint2("simple") +class MyBean { + String myValue +} + +""") + + expect: + beanDefinition.getInjectedMethods()[0].name == 'setMyValue' + def metadata = beanDefinition.getInjectedMethods()[0].getAnnotationMetadata() + metadata.hasAnnotation(Property) + metadata.getValue(Property, "name", String).get() == 'simple.my-value' + } + + void "property path is broken because alias is pointing to another alias 2"() { + given: + BeanDefinition beanDefinition = buildBeanDefinition('io.micronaut.inject.configproperties.MyBean', """ +package io.micronaut.inject.configproperties; + +@TestEndpoint3("simple") +class MyBean { + String myValue +} + +""") + + expect: + beanDefinition.getInjectedMethods()[0].name == 'setMyValue' + def metadata = beanDefinition.getInjectedMethods()[0].getAnnotationMetadata() + metadata.hasAnnotation(Property) + metadata.getValue(Property, "name", String).get() == 'endpoints.my-value' + } + + void "property path is overriding the existing one"() { + given: + BeanDefinition beanDefinition = buildBeanDefinition('io.micronaut.inject.configproperties.MyBean', """ +package io.micronaut.inject.configproperties; + +@TestEndpoint4("simple") +class MyBean { + String myValue +} + +""") + + expect: + beanDefinition.getInjectedMethods()[0].name == 'setMyValue' + def metadata = beanDefinition.getInjectedMethods()[0].getAnnotationMetadata() + metadata.hasAnnotation(Property) + metadata.getValue(Property, "name", String).get() == 'endpoints.simple.my-value' } } diff --git a/inject-groovy/src/test/groovy/io/micronaut/inject/configproperties/InterfaceConfigurationPropertiesSpec.groovy b/inject-groovy/src/test/groovy/io/micronaut/inject/configproperties/InterfaceConfigurationPropertiesSpec.groovy index 2e90492bc30..61871b5c4b4 100644 --- a/inject-groovy/src/test/groovy/io/micronaut/inject/configproperties/InterfaceConfigurationPropertiesSpec.groovy +++ b/inject-groovy/src/test/groovy/io/micronaut/inject/configproperties/InterfaceConfigurationPropertiesSpec.groovy @@ -25,7 +25,7 @@ import java.time.Duration; interface MyConfig { @javax.validation.constraints.NotBlank String getHost(); - + @javax.validation.constraints.Min(10L) int getServerPort(); } @@ -121,7 +121,7 @@ import java.time.Duration; @ConfigurationProperties("bar") @Executable interface MyConfig extends ParentConfig { - + @javax.validation.constraints.Min(10L) int getServerPort(); } @@ -169,11 +169,11 @@ import java.net.URL; interface MyConfig { @javax.validation.constraints.NotBlank String getHost(); - + @javax.validation.constraints.Min(10L) int getServerPort(); - @ConfigurationProperties("child") + @ConfigurationProperties("child") static interface ChildConfig { URL getURL(); } @@ -206,14 +206,14 @@ import java.net.URL; interface MyConfig { @javax.validation.constraints.NotBlank String getHost(); - + @javax.validation.constraints.Min(10L) int getServerPort(); - + @Executable ChildConfig getChild(); - @ConfigurationProperties("child") + @ConfigurationProperties("child") static interface ChildConfig { @Executable URL getURL(); @@ -253,7 +253,7 @@ import java.time.Duration; interface MyConfig { @javax.validation.constraints.NotBlank String junk(String s); - + @javax.validation.constraints.Min(10L) int getServerPort(); } @@ -261,7 +261,7 @@ interface MyConfig { ''') then: def e = thrown(RuntimeException) - e.message.contains('Only getter methods are allowed on @ConfigurationProperties interfaces: junk. You can change the accessors using @AccessorsStyle annotation') + e.message.contains('Only getter methods are allowed on @ConfigurationProperties interfaces: test.MyConfig.junk(..). You can change the accessors using @AccessorsStyle annotation') } void "test getter that returns void method"() { diff --git a/inject-groovy/src/test/groovy/io/micronaut/inject/configproperties/TestEndpoint1.java b/inject-groovy/src/test/groovy/io/micronaut/inject/configproperties/TestEndpoint1.java new file mode 100644 index 00000000000..a7a18b3eaea --- /dev/null +++ b/inject-groovy/src/test/groovy/io/micronaut/inject/configproperties/TestEndpoint1.java @@ -0,0 +1,49 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.inject.configproperties; + +import io.micronaut.context.annotation.AliasFor; +import io.micronaut.context.annotation.ConfigurationReader; +import jakarta.inject.Singleton; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +@Documented +@Retention(RUNTIME) +@Target(ElementType.TYPE) +@Singleton +@ConfigurationReader(prefix = "endpoints") +public @interface TestEndpoint1 { + /** + * @return The ID of the endpoint + */ + @AliasFor(annotation = ConfigurationReader.class, member = "value") + @AliasFor(member = "id") + String value() default ""; + + /** + * @return The ID of the endpoint + */ + @AliasFor(member = "value") + @AliasFor(annotation = ConfigurationReader.class, member = "value") + String id() default ""; + +} diff --git a/inject-groovy/src/test/groovy/io/micronaut/inject/configproperties/TestEndpoint2.java b/inject-groovy/src/test/groovy/io/micronaut/inject/configproperties/TestEndpoint2.java new file mode 100644 index 00000000000..da2c8fd672c --- /dev/null +++ b/inject-groovy/src/test/groovy/io/micronaut/inject/configproperties/TestEndpoint2.java @@ -0,0 +1,49 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.inject.configproperties; + +import io.micronaut.context.annotation.AliasFor; +import io.micronaut.context.annotation.ConfigurationReader; +import jakarta.inject.Singleton; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +@Documented +@Retention(RUNTIME) +@Target(ElementType.TYPE) +@Singleton +@ConfigurationReader(prefix = "endpoints") +public @interface TestEndpoint2 { + /** + * @return The ID of the endpoint + */ + @AliasFor(annotation = ConfigurationReader.class, member = ConfigurationReader.PREFIX) + @AliasFor(member = "id") + String value() default ""; + + /** + * @return The ID of the endpoint + */ + @AliasFor(member = "value") + @AliasFor(annotation = ConfigurationReader.class, member = ConfigurationReader.PREFIX) + String id() default ""; + +} diff --git a/inject-groovy/src/test/groovy/io/micronaut/inject/configproperties/TestEndpoint3.java b/inject-groovy/src/test/groovy/io/micronaut/inject/configproperties/TestEndpoint3.java new file mode 100644 index 00000000000..6bec93a75a9 --- /dev/null +++ b/inject-groovy/src/test/groovy/io/micronaut/inject/configproperties/TestEndpoint3.java @@ -0,0 +1,49 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.inject.configproperties; + +import io.micronaut.context.annotation.AliasFor; +import io.micronaut.context.annotation.ConfigurationReader; +import jakarta.inject.Singleton; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +@Documented +@Retention(RUNTIME) +@Target(ElementType.TYPE) +@Singleton +@ConfigurationReader(basePrefix = "endpoints") +public @interface TestEndpoint3 { + /** + * @return The ID of the endpoint + */ + @AliasFor(annotation = ConfigurationReader.class, member = "value") + @AliasFor(member = "id") + String value() default ""; + + /** + * @return The ID of the endpoint + */ + @AliasFor(member = "value") + @AliasFor(annotation = ConfigurationReader.class, member = "value") + String id() default ""; + +} diff --git a/inject-groovy/src/test/groovy/io/micronaut/inject/configproperties/TestEndpoint4.java b/inject-groovy/src/test/groovy/io/micronaut/inject/configproperties/TestEndpoint4.java new file mode 100644 index 00000000000..11d9a85df9e --- /dev/null +++ b/inject-groovy/src/test/groovy/io/micronaut/inject/configproperties/TestEndpoint4.java @@ -0,0 +1,49 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.inject.configproperties; + +import io.micronaut.context.annotation.AliasFor; +import io.micronaut.context.annotation.ConfigurationReader; +import jakarta.inject.Singleton; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +@Documented +@Retention(RUNTIME) +@Target(ElementType.TYPE) +@Singleton +@ConfigurationReader(basePrefix = "endpoints") +public @interface TestEndpoint4 { + /** + * @return The ID of the endpoint + */ + @AliasFor(annotation = ConfigurationReader.class, member = ConfigurationReader.PREFIX) + @AliasFor(member = "id") + String value() default ""; + + /** + * @return The ID of the endpoint + */ + @AliasFor(member = "value") + @AliasFor(annotation = ConfigurationReader.class, member = ConfigurationReader.PREFIX) + String id() default ""; + +} diff --git a/inject-groovy/src/test/groovy/io/micronaut/inject/configproperties/VisibilityIssuesSpec.groovy b/inject-groovy/src/test/groovy/io/micronaut/inject/configproperties/VisibilityIssuesSpec.groovy index 6455d232cce..d0154cf7fbe 100644 --- a/inject-groovy/src/test/groovy/io/micronaut/inject/configproperties/VisibilityIssuesSpec.groovy +++ b/inject-groovy/src/test/groovy/io/micronaut/inject/configproperties/VisibilityIssuesSpec.groovy @@ -22,17 +22,17 @@ import io.micronaut.inject.BeanFactory class VisibilityIssuesSpec extends AbstractBeanDefinitionSpec { - void "test extending a class with protected method in a different package fails compilation"() { + void "test extending a class with protected method in a different package"() { given: BeanDefinition beanDefinition = buildBeanDefinition("io.micronaut.inject.configproperties.ChildConfigProperties", """ package io.micronaut.inject.configproperties; - + import io.micronaut.context.annotation.ConfigurationProperties; import io.micronaut.inject.configproperties.other.ParentConfigProperties; - + @ConfigurationProperties("child") class ChildConfigProperties extends ParentConfigProperties { - + Integer age } """) @@ -45,7 +45,7 @@ class VisibilityIssuesSpec extends AbstractBeanDefinitionSpec { def instance = ((BeanFactory)beanDefinition).build(context, beanDefinition) then: - instance.getName() == "Sally" + instance.getName() == null //methods that require reflection are not injected instance.getAge() == 22 instance.getBuilder().build().getManufacturer() == 'Chevy' @@ -53,31 +53,15 @@ class VisibilityIssuesSpec extends AbstractBeanDefinitionSpec { context.close() } - void "test extending a class with protected field in a different package fails compilation"() { - given: - BeanDefinition beanDefinition = buildBeanDefinition("io.micronaut.inject.configproperties.ChildConfigProperties", """ - package io.micronaut.inject.configproperties; - - import io.micronaut.context.annotation.ConfigurationProperties; - import io.micronaut.inject.configproperties.other.ParentConfigProperties; - - @ConfigurationProperties("child") - class ChildConfigProperties extends ParentConfigProperties { - - protected void setName(String name) { - super.setName(name) - } - - } - """) - + void "test extending a class with protected field in a different package"() { when: - //not configured with parent.child.name because non public methods are ignored - def context = ApplicationContext.run('parent.nationality': 'Italian', 'parent.name': 'Sally') - def instance = ((BeanFactory)beanDefinition).build(context, beanDefinition) + // Micronaut 3: not configured with parent.child.name because non public methods are ignored + // Micronaut 4: correctly using parent.child.name + def context = ApplicationContext.run('parent.nationality': 'Italian', 'parent.child.name': 'Sally') + def instance = context.getBean(ChildConfigProperties) then: - instance.nationality == "Italian" + instance.nationality == "Italian" //fields that require reflection are injected instance.getName() == 'Sally' cleanup: diff --git a/inject-groovy/src/test/groovy/io/micronaut/inject/configuration/GroovyConfigurationMetadataBuilderSpec.groovy b/inject-groovy/src/test/groovy/io/micronaut/inject/configuration/GroovyConfigurationMetadataBuilderSpec.groovy deleted file mode 100644 index a67093a95fb..00000000000 --- a/inject-groovy/src/test/groovy/io/micronaut/inject/configuration/GroovyConfigurationMetadataBuilderSpec.groovy +++ /dev/null @@ -1,94 +0,0 @@ -/* - * Copyright 2017-2019 original authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.micronaut.inject.configuration - -import groovy.json.JsonSlurper -import io.micronaut.ast.transform.test.AbstractBeanDefinitionSpec -import io.micronaut.ast.groovy.config.GroovyConfigurationMetadataBuilder -import org.codehaus.groovy.ast.ClassNode -/** - * @author graemerocher - * @since 1.0 - */ -class GroovyConfigurationMetadataBuilderSpec extends AbstractBeanDefinitionSpec { - - void "test build configuration metadata with annotation aliases"() { - given: - ClassNode element = buildClassNode(''' -package test; - -import io.micronaut.context.annotation.*; -import io.micronaut.inject.annotation.*; - -@MultipleAlias("foo") -class MyProperties { - protected String fieldTest = "unconfigured"; - private String internalField = "unconfigured"; - - public void setSetterTest(String s) { - this.internalField = s; - } - - public String getSetter() { return this.internalField; } -} -''', "test.MyProperties") - - when: - GroovyConfigurationMetadataBuilder builder = new GroovyConfigurationMetadataBuilder(null, null) - def configurationMetadata = builder.visitProperties(element, "some description") - def propertyMetadata = builder.visitProperty(element, element, "java.lang.String", "setterTest", "some description", null) - - then: - builder.configurations.size() == 1 - configurationMetadata.name == 'foo' - configurationMetadata.description == 'some description' - configurationMetadata.type == 'test.MyProperties' - - builder.properties.size() == 1 - propertyMetadata.name == 'setterTest' - propertyMetadata.path == 'foo.setter-test' - propertyMetadata.type == 'java.lang.String' - propertyMetadata.declaringType == 'test.MyProperties' - propertyMetadata.description == 'some description' - - - when:"the config metadata is converted to JSON" - def sw = new StringWriter() - configurationMetadata.writeTo(sw) - def text = sw.toString() - def json = new JsonSlurper().parseText(text) - - then:"the json is correct" - json.type == configurationMetadata.type - json.name == configurationMetadata.name - json.description == configurationMetadata.description - - - when:"the property metadata is converted to JSON " - - sw = new StringWriter() - propertyMetadata.writeTo(sw) - text = sw.toString() - println "text = $text" - json = new JsonSlurper().parseText(text) - - then:"the json is correct" - json.type == propertyMetadata.type - json.name == propertyMetadata.path - json.sourceType == propertyMetadata.declaringType - json.description == propertyMetadata.description - } -} diff --git a/inject-groovy/src/test/groovy/io/micronaut/inject/factory/FactoryBeanFieldSpec.groovy b/inject-groovy/src/test/groovy/io/micronaut/inject/factory/FactoryBeanFieldSpec.groovy index c74780a11f6..df77096a7f9 100644 --- a/inject-groovy/src/test/groovy/io/micronaut/inject/factory/FactoryBeanFieldSpec.groovy +++ b/inject-groovy/src/test/groovy/io/micronaut/inject/factory/FactoryBeanFieldSpec.groovy @@ -206,6 +206,6 @@ class Test {} e.message.contains(modifier) where: - modifier << ['private', 'protected'] + modifier << ['private'] } } diff --git a/inject-groovy/src/test/groovy/io/micronaut/inject/visitor/BeanIntrospectionSpec.groovy b/inject-groovy/src/test/groovy/io/micronaut/inject/visitor/BeanIntrospectionSpec.groovy index ae0fc2503da..1c69849e4e1 100644 --- a/inject-groovy/src/test/groovy/io/micronaut/inject/visitor/BeanIntrospectionSpec.groovy +++ b/inject-groovy/src/test/groovy/io/micronaut/inject/visitor/BeanIntrospectionSpec.groovy @@ -40,7 +40,7 @@ class Test { public String getOne() { invoked = true one - } + } } '''); when: @@ -121,7 +121,7 @@ class Test { public String getOne() { invoked = true one - } + } } '''); when: @@ -168,7 +168,7 @@ class Test { @groovy.transform.PackageScope String three // package protected protected String four // not included since protected private String five // not included since private - + Test(int two) { this.two = two } @@ -217,28 +217,28 @@ import java.net.URL @io.micronaut.core.annotation.Introspected class CopyMe { - //@groovy.transform.PackageScope + //@groovy.transform.PackageScope URL url - //@groovy.transform.PackageScope + //@groovy.transform.PackageScope boolean enabled = false private final String name private final String another - + CopyMe(String name, String another) { this.name = name; this.another = another; } - + //@groovy.transform.PackageScope String getName() { return name } - + //@groovy.transform.PackageScope String getAnother() { return another } - + CopyMe withAnother(String a) { return this.another.is(a) ? this : new CopyMe(this.name, a.toUpperCase()) } @@ -398,12 +398,12 @@ public class MethodTest extends SuperType implements SomeInt { public boolean nonAnnotated() { return true; } - + @Executable public String invokeMe(String str) { return str; } - + @Executable int invokePrim(int i) { return i; @@ -415,7 +415,7 @@ class SuperType { String superMethod(String str) { return str; } - + @Executable public String invokeMe(String str) { return str; @@ -427,7 +427,7 @@ interface SomeInt { default boolean ok() { return true; } - + default String getName() { return "ok"; } @@ -438,18 +438,20 @@ interface SomeInt { then: // bizarrely Groovy doesn't support resolving default interface methods - beanMethods*.name as Set == ['invokeMe', 'invokePrim', 'superMethod'] as Set + beanMethods*.name as Set == ['invokeMe', 'invokePrim', 'superMethod', 'ok'] as Set beanMethods.every({it.annotationMetadata.hasAnnotation(Executable)}) beanMethods.every { it.declaringBean == introspection} when: def invokeMe = beanMethods.find { it.name == 'invokeMe' } def invokePrim = beanMethods.find { it.name == 'invokePrim' } + def itfeMethod = beanMethods.find { it.name == 'ok' } def bean = introspection.instantiate() then: invokeMe.invoke(bean, "test") == 'test' invokePrim.invoke(bean, 10) == 10 + itfeMethod.invoke(bean) == true } void "test generate bean introspection for interface"() { @@ -544,23 +546,23 @@ import com.fasterxml.jackson.annotation.*; class Test { private String name; private int age; - + @JsonCreator Test(@JsonProperty("name") String name) { this.name = name; } - + Test(int age) { this.age = age; } - + public int getAge() { return age; } public void setAge(int age) { this.age = age; } - + public String getName() { return this.name; } @@ -789,11 +791,11 @@ import java.util.*; @Introspected class Test { private Status status; - + public void setStatus(Status status) { this.status = status; } - + public Status getStatus() { return this.status; } @@ -891,7 +893,7 @@ class Test { @Size(max=100) private int age; private int[] primitiveArray; - + public Test(String name, int age, int[] primitiveArray) { this.name = name; this.age = age; @@ -908,19 +910,19 @@ class Test { public void setAge(int age) { this.age = age; } - + public void setId(Long id) { this.id = id; } - + public Long getId() { return this.id; } - + public void setVersion(Long version) { this.version = version; } - + public Long getVersion() { return this.version; } @@ -969,28 +971,28 @@ class Test extends ParentBean { private String name; @Size(max=100) private int age; - + private List list; private String[] stringArray; private int[] primitiveArray; private boolean flag; private TypeConverter genericsTest; - + public TypeConverter getGenericsTest() { return genericsTest; } - + public String getReadOnly() { return readOnly; } public boolean isFlag() { return flag; } - + public void setFlag(boolean flag) { this.flag = flag; } - + public String getName() { return this.name; } @@ -1003,11 +1005,11 @@ class Test extends ParentBean { public void setAge(int age) { this.age = age; } - + public List getList() { return this.list; } - + public void setList(List l) { this.list = l; } @@ -1031,11 +1033,11 @@ class Test extends ParentBean { class ParentBean { private List listOfBytes; - + public List getListOfBytes() { return this.listOfBytes; } - + public void setListOfBytes(List list) { this.listOfBytes = list; } @@ -1290,7 +1292,7 @@ import io.micronaut.core.annotation.*; @Introspected class Test { - + final String name } @@ -1321,16 +1323,16 @@ import io.micronaut.core.annotation.* @Introspected class Test { private String name - + private Test(String name) { this.name = name } - + @Creator static Test forName(String name) { new Test(name) } - + String getName() { name } @@ -1368,16 +1370,16 @@ import io.micronaut.core.annotation.* @Introspected class Test { private String name - + private Test(String name) { this.name = name } - + @Creator static Test forName() { new Test("default") } - + String getName() { name } @@ -1416,21 +1418,21 @@ import io.micronaut.core.annotation.* class Test { private String name - + private Test(String name) { this.name = name } - + @Creator static Test forName() { new Test("default") } - + @Creator static Test forName(String name) { new Test(name) } - + String getName() { name } @@ -1529,7 +1531,7 @@ enum Test { Test(int number) { this.number = number } - + int getNumber() { number } @@ -1726,11 +1728,11 @@ class ValidatedConfig { @NotNull URL url - + static class Inner { - + } - + } ''') expect: @@ -1774,17 +1776,17 @@ class MyConfig { private String host private int serverPort - + @ConfigurationInject MyConfig(@javax.validation.constraints.NotBlank String host, int serverPort) { this.host = host this.serverPort = serverPort } - + String getHost() { host } - + int getServerPort() { serverPort } @@ -1849,13 +1851,13 @@ class ValidatedConfig { @NotNull URL url - + public static class Inner { } - + @ConfigurationProperties("another") static class Another { - + @NotNull URL url } @@ -2096,7 +2098,7 @@ import io.micronaut.core.annotation.Introspected abstract class Test { String name String author - + int getAge() { 0 } diff --git a/inject-groovy/src/test/groovy/io/micronaut/inject/visitor/ClassElementSpec.groovy b/inject-groovy/src/test/groovy/io/micronaut/inject/visitor/ClassElementSpec.groovy index 9cd60337108..7d5563448b1 100644 --- a/inject-groovy/src/test/groovy/io/micronaut/inject/visitor/ClassElementSpec.groovy +++ b/inject-groovy/src/test/groovy/io/micronaut/inject/visitor/ClassElementSpec.groovy @@ -321,8 +321,7 @@ interface AnotherInterface { def allMethods = classElement.getEnclosedElements(ElementQuery.ALL_METHODS) then:"All methods, including non-accessible are returned but not overridden" - // slightly different result to java since groovy hides private methods - allMethods.size() == 9 + allMethods.size() == 10 when:"only abstract methods are requested" def abstractMethods = classElement.getEnclosedElements(ElementQuery.ALL_METHODS.onlyAbstract()) @@ -396,7 +395,7 @@ interface AnotherInterface { def allMethods = classElement.getEnclosedElements(ElementQuery.ALL_METHODS) then:"All methods, including non-accessible are returned but not overridden" - allMethods.size() == 6 // slightly different result to java since groovy hides private methods + allMethods.size() == 7 allMethods.find { it.name == 'publicMethod'}.declaringType.simpleName == 'Test' allMethods.find { it.name == 'otherSuper'}.declaringType.simpleName == 'SuperType' @@ -754,4 +753,91 @@ enum Test { } allFields.size() == expected.size() } + + void "test unrecognized default method"() { + given: + ClassElement classElement = buildClassElement('elementquery.MyBean', ''' +package elementquery; + +class Generic { +} +class Specific extends Generic { +} +interface GenericInterface { + Generic getObject() +} +interface SpecificInterface { + Specific getObject() +} + +interface MyBean extends GenericInterface, SpecificInterface { + + default Specific getObject() { + return null + } + +} + +''') + when: + def allMethods = classElement.getEnclosedElements(ElementQuery.ALL_METHODS) + then: + allMethods.size() == 1 + when: + def declaredMethods = classElement.getEnclosedElements(ElementQuery.ALL_METHODS.onlyDeclared()) + then: + declaredMethods.size() == 1 + declaredMethods.get(0).isAbstract() == true + declaredMethods.get(0).isDefault() == false + } + + // Groovy bug? + void "test unrecognized default method 2"() { + given: + ClassElement classElement = buildClassElement('elementquery.MyBean', ''' +package elementquery; + +interface MyBean { + + default String getObject() { + return null + } + +} + +''') + when: + def allMethods = classElement.getEnclosedElements(ElementQuery.ALL_METHODS) + then: + allMethods.size() == 1 + allMethods.get(0).isAbstract() == true + allMethods.get(0).isDefault() == false + } + + // Groovy bug? + void "test default method"() { + given: + ClassElement classElement = buildClassElement('elementquery.MyBean', ''' +package elementquery; + +interface MyInt { + + default String getObject() { + return null + } + +} + +class MyBean implements MyInt { +} + +''') + when: + def allMethods = classElement.getEnclosedElements(ElementQuery.ALL_METHODS) + then: + // In this case the default method is not abstract but still not default + allMethods.size() == 1 + allMethods.get(0).isAbstract() == false + allMethods.get(0).isDefault() == false + } } diff --git a/inject-groovy/src/test/groovy/io/micronaut/inject/visitor/CustomVisitorSpec.groovy b/inject-groovy/src/test/groovy/io/micronaut/inject/visitor/CustomVisitorSpec.groovy index 39617e402f9..2b49ac3539a 100644 --- a/inject-groovy/src/test/groovy/io/micronaut/inject/visitor/CustomVisitorSpec.groovy +++ b/inject-groovy/src/test/groovy/io/micronaut/inject/visitor/CustomVisitorSpec.groovy @@ -16,7 +16,6 @@ package io.micronaut.inject.visitor import io.micronaut.ast.transform.test.AbstractBeanDefinitionSpec -import io.micronaut.ast.groovy.utils.AstAnnotationUtils class CustomVisitorSpec extends AbstractBeanDefinitionSpec { @@ -27,10 +26,6 @@ class CustomVisitorSpec extends AbstractBeanDefinitionSpec { TestInjectVisitor.clearVisited() } - void cleanup() { - AstAnnotationUtils.invalidateCache() - } - void "test class is visited by custom visitor"() { buildBeanDefinition('test.TestController', ''' package test @@ -42,21 +37,21 @@ import jakarta.inject.Inject class TestController { @Inject private String privateField - protected String protectedField + protected String protectedField public String publicField @groovy.transform.PackageScope String packagePrivateField String property - + TestController(String constructorArg) {} - + @Inject void setterMethod(String method) {} - + @Get("/getMethod") String getMethod(String argument) { "" } - + @Post("/postMethod") String postMethod() { "" @@ -66,7 +61,13 @@ class TestController { ''') expect: ControllerGetVisitor.getVisited() == ["test.TestController", "getMethod"] - AllElementsVisitor.getVisited().toSet() == ["test.TestController","privateField", "protectedField", "publicField", "packagePrivateField", "property", "setterMethod", "getMethod", "postMethod"].toSet() + AllElementsVisitor.getVisited().toSet() == ["test.TestController", + "privateField", + "protectedField", + "publicField", "setProperty", "getProperty", + "packagePrivateField", "setPackagePrivateField", "getPackagePrivateField", + "property", + "setterMethod", "getMethod", "postMethod"].toSet() AllClassesVisitor.getVisited() == ["test.TestController", "getMethod"] } @@ -80,21 +81,21 @@ import jakarta.inject.Inject public class TestController { @Inject private String privateField - protected String protectedField + protected String protectedField public String publicField @groovy.transform.PackageScope String packagePrivateField String property - - + + TestController(String constructorArg) {} - + void setterMethod(String method) {} - + @Get("/getMethod") String getMethod(String argument) { "" } - + @Post("/postMethod") String postMethod() { "" @@ -120,14 +121,14 @@ import jakarta.inject.Inject public class TestGenerated { @Inject private String privateField - protected String protectedField + protected String protectedField public String publicField @groovy.transform.PackageScope String packagePrivateField String property - - + + TestGenerated(String constructorArg) {} - + void setterMethod(String method) {} } diff --git a/inject-groovy/src/test/groovy/io/micronaut/inject/visitor/ElementAnnotateSpec.groovy b/inject-groovy/src/test/groovy/io/micronaut/inject/visitor/ElementAnnotateSpec.groovy index 32667ad0f5e..5a59ac8697e 100644 --- a/inject-groovy/src/test/groovy/io/micronaut/inject/visitor/ElementAnnotateSpec.groovy +++ b/inject-groovy/src/test/groovy/io/micronaut/inject/visitor/ElementAnnotateSpec.groovy @@ -81,11 +81,11 @@ import io.micronaut.core.annotation.Introspected; @Introspected class Test { private String name; - - public String getName() { + + public String getName() { return name; } - + public void setName(String name) { this.name = name; } diff --git a/inject-groovy/src/test/groovy/io/micronaut/inject/visitor/IntroductionVisitorSpec.groovy b/inject-groovy/src/test/groovy/io/micronaut/inject/visitor/IntroductionVisitorSpec.groovy index b0c1cfcc887..4fe8d05fb55 100644 --- a/inject-groovy/src/test/groovy/io/micronaut/inject/visitor/IntroductionVisitorSpec.groovy +++ b/inject-groovy/src/test/groovy/io/micronaut/inject/visitor/IntroductionVisitorSpec.groovy @@ -36,13 +36,15 @@ class Foo {} visitedElements.find { it.name == 'deleteAll'}.parameters[0].genericType.getFirstTypeArgument().get().name == 'introv1.Foo' IntroductionVisitor.VISITED_CLASS_ELEMENTS.size() == 1 visitedElements.size() == 5 - visitedElements[1].name == 'save' - visitedElements[1].genericReturnType.name == 'introv1.Foo' - visitedElements[1].parameters[0].genericType.name == 'introv1.Foo' - visitedElements[2].parameters[0].genericType.name == Iterable.name - visitedElements[2].parameters[0].genericType.getFirstTypeArgument().isPresent() - visitedElements[2].parameters[0].genericType.getFirstTypeArgument().get().name == 'introv1.Foo' - visitedElements[2].genericReturnType.getFirstTypeArgument().get().name == 'introv1.Foo' + def saveElement = visitedElements.find { it.name == 'save'} + saveElement.name == 'save' + saveElement.genericReturnType.name == 'introv1.Foo' + saveElement.parameters[0].genericType.name == 'introv1.Foo' + def saveAllElement = visitedElements.find { it.name == 'saveAll'} + saveAllElement.parameters[0].genericType.name == Iterable.name + saveAllElement.parameters[0].genericType.getFirstTypeArgument().isPresent() + saveAllElement.parameters[0].genericType.getFirstTypeArgument().get().name == 'introv1.Foo' + saveAllElement.genericReturnType.getFirstTypeArgument().get().name == 'introv1.Foo' and: ClassElement classElement = IntroductionVisitor.VISITED_CLASS_ELEMENTS[0] @@ -88,13 +90,14 @@ class Foo {} expect: IntroductionVisitor.VISITED_CLASS_ELEMENTS.size() == 1 IntroductionVisitor.VISITED_METHOD_ELEMENTS.size() == 5 - IntroductionVisitor.VISITED_METHOD_ELEMENTS[1].name == 'save' - IntroductionVisitor.VISITED_METHOD_ELEMENTS[1].genericReturnType.name == 'introv2.Foo' - IntroductionVisitor.VISITED_METHOD_ELEMENTS[1].parameters[0].genericType.name == 'introv2.Foo' - IntroductionVisitor.VISITED_METHOD_ELEMENTS[2].parameters[0].genericType.name == Iterable.name - IntroductionVisitor.VISITED_METHOD_ELEMENTS[2].parameters[0].genericType.getFirstTypeArgument().isPresent() - IntroductionVisitor.VISITED_METHOD_ELEMENTS[2].parameters[0].genericType.getFirstTypeArgument().get().name == 'introv2.Foo' - IntroductionVisitor.VISITED_METHOD_ELEMENTS[2].genericReturnType.getFirstTypeArgument().get().name == 'introv2.Foo' + def saveElement = IntroductionVisitor.VISITED_METHOD_ELEMENTS.find { it.name == 'save'} + saveElement.genericReturnType.name == 'introv2.Foo' + saveElement.parameters[0].genericType.name == 'introv2.Foo' + def saveElementAll = IntroductionVisitor.VISITED_METHOD_ELEMENTS.find { it.name == 'saveAll'} + saveElementAll.parameters[0].genericType.name == Iterable.name + saveElementAll.parameters[0].genericType.getFirstTypeArgument().isPresent() + saveElementAll.parameters[0].genericType.getFirstTypeArgument().get().name == 'introv2.Foo' + saveElementAll.genericReturnType.getFirstTypeArgument().get().name == 'introv2.Foo' and: ClassElement classElement = IntroductionVisitor.VISITED_CLASS_ELEMENTS[0] diff --git a/inject-groovy/src/test/groovy/io/micronaut/inject/visitor/PropertyElementSpec.groovy b/inject-groovy/src/test/groovy/io/micronaut/inject/visitor/PropertyElementSpec.groovy index 6f3658d7aa9..f5335f79c3d 100644 --- a/inject-groovy/src/test/groovy/io/micronaut/inject/visitor/PropertyElementSpec.groovy +++ b/inject-groovy/src/test/groovy/io/micronaut/inject/visitor/PropertyElementSpec.groovy @@ -38,12 +38,12 @@ import jakarta.inject.Inject; @Controller("/test") public class TestController { - + private int age; private String name; String groovyProp - - + + /** * The age */ @@ -55,7 +55,7 @@ public class TestController { public String getName() { return name; } - + public void setName(String n) { name = n; } @@ -64,14 +64,14 @@ public class TestController { expect: AllElementsVisitor.VISITED_CLASS_ELEMENTS.size() == 1 AllElementsVisitor.VISITED_CLASS_ELEMENTS[0].beanProperties.size() == 3 - AllElementsVisitor.VISITED_CLASS_ELEMENTS[0].beanProperties[1].name == 'age' - AllElementsVisitor.VISITED_CLASS_ELEMENTS[0].beanProperties[1].isAnnotationPresent(Get) - AllElementsVisitor.VISITED_CLASS_ELEMENTS[0].beanProperties[1].type.name == 'int' - AllElementsVisitor.VISITED_CLASS_ELEMENTS[0].beanProperties[1].isReadOnly() - AllElementsVisitor.VISITED_CLASS_ELEMENTS[0].beanProperties[2].name == 'name' - AllElementsVisitor.VISITED_CLASS_ELEMENTS[0].beanProperties[2].type.name == 'java.lang.String' - !AllElementsVisitor.VISITED_CLASS_ELEMENTS[0].beanProperties[2].isReadOnly() - AllElementsVisitor.VISITED_CLASS_ELEMENTS[0].beanProperties[0].name == 'groovyProp' + AllElementsVisitor.VISITED_CLASS_ELEMENTS[0].beanProperties[0].name == 'age' + AllElementsVisitor.VISITED_CLASS_ELEMENTS[0].beanProperties[0].isAnnotationPresent(Get) + AllElementsVisitor.VISITED_CLASS_ELEMENTS[0].beanProperties[0].type.name == 'int' + AllElementsVisitor.VISITED_CLASS_ELEMENTS[0].beanProperties[0].isReadOnly() + AllElementsVisitor.VISITED_CLASS_ELEMENTS[0].beanProperties[1].name == 'name' + AllElementsVisitor.VISITED_CLASS_ELEMENTS[0].beanProperties[1].type.name == 'java.lang.String' + !AllElementsVisitor.VISITED_CLASS_ELEMENTS[0].beanProperties[1].isReadOnly() + AllElementsVisitor.VISITED_CLASS_ELEMENTS[0].beanProperties[2].name == 'groovyProp' } void "test simple bean properties with generics"() { @@ -83,10 +83,10 @@ import jakarta.inject.Inject; @Controller("/test") public class TestController { - + private int age; private T name; - + public int getAge() { return age; } @@ -94,7 +94,7 @@ public class TestController { public T getName() { return name; } - + public void setName(T n) { name = n; } @@ -121,9 +121,9 @@ import jakarta.inject.Inject; @Controller("/test") public class TestController { - + private Response age; - + public Response getAge() { return age; } diff --git a/inject-groovy/src/test/groovy/io/micronaut/inject/visitor/Test.groovy b/inject-groovy/src/test/groovy/io/micronaut/inject/visitor/Test.groovy new file mode 100644 index 00000000000..d05ee337763 --- /dev/null +++ b/inject-groovy/src/test/groovy/io/micronaut/inject/visitor/Test.groovy @@ -0,0 +1,13 @@ +package io.micronaut.inject.visitor; + +import io.micronaut.core.annotation.* + +@Introspected(accessKind=[Introspected.AccessKind.FIELD, Introspected.AccessKind.METHOD]) +class Test { + public String one + public boolean invoked = false + public String getOne() { + invoked = true + one + } +} diff --git a/inject-groovy/src/test/groovy/io/micronaut/inject/visitor/TestInjectVisitor.java b/inject-groovy/src/test/groovy/io/micronaut/inject/visitor/TestInjectVisitor.java index 4c14fa35f6f..3e95f073db2 100644 --- a/inject-groovy/src/test/groovy/io/micronaut/inject/visitor/TestInjectVisitor.java +++ b/inject-groovy/src/test/groovy/io/micronaut/inject/visitor/TestInjectVisitor.java @@ -18,7 +18,6 @@ import io.micronaut.core.annotation.AnnotationUtil; import io.micronaut.inject.ast.ClassElement; import io.micronaut.inject.ast.Element; -import io.micronaut.inject.ast.EnumConstantElement; import io.micronaut.inject.ast.FieldElement; import io.micronaut.inject.ast.MethodElement; diff --git a/inject-java-test/src/main/groovy/io/micronaut/annotation/processing/test/AbstractTypeElementSpec.groovy b/inject-java-test/src/main/groovy/io/micronaut/annotation/processing/test/AbstractTypeElementSpec.groovy index 0430afc2963..d12043e04a3 100644 --- a/inject-java-test/src/main/groovy/io/micronaut/annotation/processing/test/AbstractTypeElementSpec.groovy +++ b/inject-java-test/src/main/groovy/io/micronaut/annotation/processing/test/AbstractTypeElementSpec.groovy @@ -17,11 +17,20 @@ package io.micronaut.annotation.processing.test import com.sun.source.util.JavacTask import groovy.transform.CompileStatic -import io.micronaut.annotation.processing.* +import io.micronaut.annotation.processing.AggregatingTypeElementVisitorProcessor +import io.micronaut.annotation.processing.AnnotationUtils +import io.micronaut.annotation.processing.GenericUtils +import io.micronaut.annotation.processing.JavaAnnotationMetadataBuilder +import io.micronaut.annotation.processing.ModelUtils +import io.micronaut.annotation.processing.TypeElementVisitorProcessor import io.micronaut.annotation.processing.visitor.JavaElementFactory import io.micronaut.annotation.processing.visitor.JavaVisitorContext import io.micronaut.aop.internal.InterceptorRegistryBean -import io.micronaut.context.* +import io.micronaut.context.ApplicationContext +import io.micronaut.context.ApplicationContextBuilder +import io.micronaut.context.ApplicationContextConfiguration +import io.micronaut.context.DefaultApplicationContext +import io.micronaut.context.Qualifier import io.micronaut.context.event.ApplicationEventPublisherFactory import io.micronaut.core.annotation.AnnotationMetadata import io.micronaut.core.annotation.Experimental @@ -30,6 +39,7 @@ import io.micronaut.core.annotation.Nullable import io.micronaut.core.beans.BeanIntrospection import io.micronaut.core.convert.value.MutableConvertibleValuesMap import io.micronaut.core.graal.GraalReflectionConfigurer +import io.micronaut.core.io.IOUtils import io.micronaut.core.naming.NameUtils import io.micronaut.inject.BeanConfiguration import io.micronaut.inject.BeanDefinition @@ -85,7 +95,6 @@ abstract class AbstractTypeElementSpec extends Specification { GenericUtils genericUtils = new GenericUtils(elements, types, modelUtils) {} AnnotationUtils annotationUtils = new AnnotationUtils(processingEnv, elements, messager, types, modelUtils, genericUtils, processingEnv.filer) { } - AnnotationMetadata annotationMetadata = annotationUtils.getAnnotationMetadata(typeElement) JavaVisitorContext visitorContext = new JavaVisitorContext( processingEnv, @@ -100,7 +109,7 @@ abstract class AbstractTypeElementSpec extends Specification { TypeElementVisitor.VisitorKind.ISOLATING ) - return new JavaElementFactory(visitorContext).newClassElement(typeElement, annotationMetadata) + return new JavaElementFactory(visitorContext).newClassElement(typeElement, visitorContext.getElementAnnotationMetadataFactory()) } /** @@ -109,28 +118,21 @@ abstract class AbstractTypeElementSpec extends Specification { */ @CompileStatic AnnotationMetadata buildTypeAnnotationMetadata(@Language("java") String cls) { + AbstractAnnotationMetadataBuilder.clearMutated() Element element = buildTypeElement(cls) JavaAnnotationMetadataBuilder builder = newJavaAnnotationBuilder() - AnnotationMetadata metadata = element != null ? builder.build(element) : null - AbstractAnnotationMetadataBuilder.copyToRuntime() - return metadata - } - - AnnotationMetadata buildDeclaredMethodAnnotationMetadata(@Language("java") String cls, String methodName) { - TypeElement element = buildTypeElement(cls) - Element method = element.getEnclosedElements().find() { it.simpleName.toString() == methodName } - JavaAnnotationMetadataBuilder builder = newJavaAnnotationBuilder() - AnnotationMetadata metadata = method != null ? builder.buildDeclared(method) : null + AnnotationMetadata metadata = element != null ? builder.lookupOrBuildForType(element) : null AbstractAnnotationMetadataBuilder.copyToRuntime() return metadata } AnnotationMetadata buildMethodArgumentAnnotationMetadata(@Language("java") String cls, String methodName, String argumentName) { + AbstractAnnotationMetadataBuilder.clearMutated() TypeElement element = buildTypeElement(cls) ExecutableElement method = (ExecutableElement)element.getEnclosedElements().find() { it.simpleName.toString() == methodName } VariableElement argument = method.parameters.find() { it.simpleName.toString() == argumentName } JavaAnnotationMetadataBuilder builder = newJavaAnnotationBuilder() - AnnotationMetadata metadata = argument != null ? builder.build(argument) : null + AnnotationMetadata metadata = argument != null ? builder.lookupOrBuildForMethod(element, argument) : null AbstractAnnotationMetadataBuilder.copyToRuntime() return metadata } @@ -318,23 +320,7 @@ class Test { TypeElement element = buildTypeElement(cls) Element method = element.getEnclosedElements().find() { it.simpleName.toString() == methodName } JavaAnnotationMetadataBuilder builder = newJavaAnnotationBuilder() - AnnotationMetadata metadata = method != null ? builder.build(method) : null - return metadata - } - - /** - * @param cls The class string - * @param methodName The method name - * @param fieldName The field name - * @return The annotation metadata for the field - */ - @CompileStatic - AnnotationMetadata buildFieldAnnotationMetadata(@Language("java") String cls, String methodName, String fieldName) { - TypeElement element = buildTypeElement(cls) - ExecutableElement method = (ExecutableElement)element.getEnclosedElements().find() { it.simpleName.toString() == methodName } - VariableElement argument = method.parameters.find() { it.simpleName.toString() == fieldName } - JavaAnnotationMetadataBuilder builder = newJavaAnnotationBuilder() - AnnotationMetadata metadata = argument != null ? builder.build(argument) : null + AnnotationMetadata metadata = method != null ? builder.lookupOrBuildForMethod(element, method) : null return metadata } @@ -365,6 +351,11 @@ class Test { ) } + protected String buildAndReadResourceAsString(String resourceName, @Language("java") String cls) { + ClassLoader classLoader = buildClassLoader("test.Test", cls) + return IOUtils.readText(new BufferedReader(new InputStreamReader(classLoader.getResources(resourceName).toList().last().openStream()))) + } + protected BeanDefinition buildBeanDefinition(String className, @Language("java") String cls) { def classSimpleName = NameUtils.getSimpleName(className) def beanDefName = (classSimpleName.startsWith('$') ? '' : '$') + classSimpleName + BeanDefinitionWriter.CLASS_SUFFIX @@ -466,6 +457,7 @@ class Test { @CompileStatic protected ClassLoader buildClassLoader(String className, @Language("java") String cls) { + AbstractAnnotationMetadataBuilder.clearMutated() Iterable files = newJavaParser().generate(className, cls) return new JavaFileObjectClassLoader(files) } diff --git a/inject-java-test/src/test/groovy/io/micronaut/inject/visitor/AllClassesVisitor.java b/inject-java-test/src/test/groovy/io/micronaut/inject/visitor/AllClassesVisitor.java index 131b42d1907..255c9fa208f 100644 --- a/inject-java-test/src/test/groovy/io/micronaut/inject/visitor/AllClassesVisitor.java +++ b/inject-java-test/src/test/groovy/io/micronaut/inject/visitor/AllClassesVisitor.java @@ -53,6 +53,7 @@ public List getVisitedClassElements() { @Override public void visitClass(ClassElement element, VisitorContext context) { + element.getBeanProperties(); visitedClassElements.add(element); } diff --git a/inject-java-test/src/test/groovy/io/micronaut/inject/visitor/ElementAnnotateSpec.groovy b/inject-java-test/src/test/groovy/io/micronaut/inject/visitor/ElementAnnotateSpec.groovy index 48e0418751c..66858cd1ddf 100644 --- a/inject-java-test/src/test/groovy/io/micronaut/inject/visitor/ElementAnnotateSpec.groovy +++ b/inject-java-test/src/test/groovy/io/micronaut/inject/visitor/ElementAnnotateSpec.groovy @@ -60,12 +60,12 @@ class TestListener { @Executable void receive(String v) { } - + @Executable int[] receiveArray(int[] v) { return v; } - + @Executable int receiveInt(int v) { return v; @@ -96,11 +96,11 @@ import io.micronaut.core.annotation.Introspected; @Introspected class Test { private String name; - - public String getName() { + + public String getName() { return name; } - + public void setName(String name) { this.name = name; } @@ -123,11 +123,11 @@ class Outer { @Introspected static class Test { private String name; - - public String getName() { + + public String getName() { return name; } - + public void setName(String name) { this.name = name; } diff --git a/inject-java-test/src/test/groovy/io/micronaut/inject/visitor/InheritanceVisitorSpec.groovy b/inject-java-test/src/test/groovy/io/micronaut/inject/visitor/InheritanceVisitorSpec.groovy index 8ef4b6345c4..c5a5f512e6a 100644 --- a/inject-java-test/src/test/groovy/io/micronaut/inject/visitor/InheritanceVisitorSpec.groovy +++ b/inject-java-test/src/test/groovy/io/micronaut/inject/visitor/InheritanceVisitorSpec.groovy @@ -50,11 +50,14 @@ class Parent { expect: introspection != null - allClassesVisitor.visitedClassElements[0].beanProperties.size() == 2 - allClassesVisitor.visitedClassElements[0].beanProperties[0].name == 'name' - allClassesVisitor.visitedClassElements[0].beanProperties[0].declaringType.name == 'test.Test' - allClassesVisitor.visitedClassElements[0].beanProperties[1].name == 'bar' - allClassesVisitor.visitedClassElements[0].beanProperties[1].declaringType.name == 'test.Parent' + def properties = allClassesVisitor.visitedClassElements[0].beanProperties + properties.size() == 2 + def nameProp = properties.find { it.name == "name"} + nameProp.name == 'name' + nameProp.declaringType.name == 'test.Test' + def barProp = properties.find { it.name == "bar"} + barProp.name == 'bar' + barProp.declaringType.name == 'test.Parent' // result is 6 because the Parent is visited through Test and by itself allClassesVisitor.visitedMethodElements.size() == 6 } diff --git a/inject-java-test/src/test/groovy/io/micronaut/inject/visitor/beans/BeanIntrospectionSpec.groovy b/inject-java-test/src/test/groovy/io/micronaut/inject/visitor/beans/BeanIntrospectionSpec.groovy index 0468e72d21f..d78c9b84c54 100644 --- a/inject-java-test/src/test/groovy/io/micronaut/inject/visitor/beans/BeanIntrospectionSpec.groovy +++ b/inject-java-test/src/test/groovy/io/micronaut/inject/visitor/beans/BeanIntrospectionSpec.groovy @@ -11,6 +11,8 @@ import io.micronaut.annotation.processing.test.JavaParser import io.micronaut.context.ApplicationContext import io.micronaut.context.annotation.Executable import io.micronaut.context.annotation.Replaces +import io.micronaut.context.visitor.ConfigurationReaderVisitor +import io.micronaut.context.visitor.ValidationVisitor import io.micronaut.core.annotation.Introspected import io.micronaut.core.beans.BeanIntrospection import io.micronaut.core.beans.BeanIntrospectionReference @@ -62,7 +64,7 @@ class Test { public void setArray(T[] array) { this.array = array; } - + @Executable T[] myMethod() { return array; @@ -90,7 +92,7 @@ class Test { public String getOne() { this.invoked = true; return one; - } + } } '''); @@ -158,7 +160,7 @@ class Test { public String getOne() { this.invoked = true; return one; - } + } } '''); @@ -227,7 +229,7 @@ class Test { String three; // package protected protected String four; // not included since protected private String five; // not included since private - + Test(int two) { this.two = two; } @@ -280,7 +282,7 @@ class Test { String three; // package protected protected String four; // not included since protected private String five; // not included since private - + Test(int two) { this.two = two; } @@ -309,7 +311,7 @@ package beanctor; public class Test { private final String another; - + @com.fasterxml.jackson.annotation.JsonCreator Test(String another) { this.another = another; @@ -356,7 +358,7 @@ public class MethodTest extends SuperType implements SomeInt { public String invokeMe(String str) { return str; } - + @Executable int invokePrim(int i) { return i; @@ -368,7 +370,7 @@ class SuperType { String superMethod(String str) { return str; } - + @Executable public String invokeMe(String str) { return str; @@ -380,7 +382,7 @@ interface SomeInt { default boolean ok() { return true; } - + default String getName() { return "ok"; } @@ -494,7 +496,7 @@ import java.net.URL; public class CopyMe { private final String another; - + CopyMe(String another) { this.another = another; } @@ -502,7 +504,7 @@ public class CopyMe { public String getAnother() { return another; } - + public CopyMe alterAnother(String a) { return this.another == a ? this : new CopyMe(a.toUpperCase()); } @@ -538,7 +540,7 @@ public class CopyMe { private URL url; private final String name; private final String another; - + CopyMe(String name, String another) { this.name = name; this.another = another; @@ -551,15 +553,15 @@ public class CopyMe { public void setUrl(URL url) { this.url = url; } - + public String getName() { return name; } - + public String getAnother() { return another; } - + public CopyMe withAnother(String a) { return this.another == a ? this : new CopyMe(this.name, a.toUpperCase()); } @@ -729,7 +731,7 @@ public class Foo { public List getValue() { return value; } - + public void setValue(List value) { this.value = value; } @@ -850,7 +852,7 @@ public interface MyInterface { @Executable default String name() { return getName(); - } + } } class MyImpl implements MyInterface { @@ -884,7 +886,7 @@ class Test {} public interface MyInterface { @Executable - String name(); + String name(); } class MyImpl implements MyInterface { @@ -1000,7 +1002,7 @@ class Foo extends GenBase { abstract class GenBase { abstract T getName(); - + public T getOther() { return (T) "other"; } @@ -1032,7 +1034,7 @@ class Foo implements GenBase { public Long getValue() { return value; } - + public void setValue(Long value) { this.value = value; } @@ -1040,7 +1042,7 @@ class Foo implements GenBase { interface GenBase { T getValue(); - + void setValue(T t); } ''') @@ -1070,7 +1072,7 @@ import io.micronaut.core.annotation.Creator; @io.micronaut.core.annotation.Introspected interface Foo { String getName(); - + @Creator static Foo create(String name) { return () -> name; @@ -1097,7 +1099,7 @@ import io.micronaut.core.annotation.Creator; @io.micronaut.core.annotation.Introspected interface Foo { String getName(); - + @Creator static Foo create(String name) { return () -> name; @@ -1388,11 +1390,11 @@ public class ValidatedConfig { public void setUrl(URL url) { this.url = url; } - + public static class Inner { - + } - + } ''') expect: @@ -1455,21 +1457,21 @@ class ValidatedConfig { public void setUrl(URL url) { this.url = url; } - + public static class Inner { - + } - + @ConfigurationProperties("another") static class Another { - + private URL url; @NotNull public URL getUrl() { return url; } - + public void setUrl(URL url) { this.url = url; } @@ -1633,17 +1635,17 @@ import java.time.Duration; class MyConfig { private String host; private int serverPort; - + @ConfigurationInject MyConfig(@javax.validation.constraints.NotBlank String host, int serverPort) { this.host = host; this.serverPort = serverPort; } - + public String getHost() { return host; } - + public int getServerPort() { return serverPort; } @@ -1705,7 +1707,7 @@ public class Test { public > Test(int num, String str, Class enumClass) { this(num, str + enumClass.getName()); } - + @Creator public > Test(int num, String str) { this.num = num; @@ -1740,7 +1742,7 @@ class Test { } public String getTest() { return test; - } + } } @@ -1760,7 +1762,7 @@ class Test { private T child; public T getChild() { return child; - } + } } class B {} @@ -1800,7 +1802,7 @@ class Address { @NotBlank(groups = GroupThree.class, message = "different message") @Size(min = 5, max = 20, groups = GroupTwo.class) private String street; - + public String getStreet() { return this.street; } @@ -1834,7 +1836,7 @@ class Book { this.title = title; this.author = author; } - + @io.micronaut.core.annotation.Creator public Book(String title) { this.title = title; @@ -1843,7 +1845,7 @@ class Book { public String getTitle() { return title; } - + void setTitle(String title) { this.title = title; } @@ -1869,7 +1871,7 @@ class Book { then: introspection != null introspection.hasAnnotation(Introspected) - introspection.propertyNames.length == 1 + introspection.propertyNames.length == 2 // Author also passes the rules to be a property, but only write only when: introspection.instantiate() @@ -1908,12 +1910,12 @@ class Book { private String title; public Book() { - } + } public String getTitle() { return title; } - + public void setTitle(String title) { this.title = title; } @@ -1967,23 +1969,23 @@ import com.fasterxml.jackson.annotation.*; class Test { private String name; private int age; - + @JsonCreator Test(@JsonProperty("name") String name) { this.name = name; } - + Test(int age) { this.age = age; } - + public int getAge() { return age; } public void setAge(int age) { this.age = age; } - + public String getName() { return this.name; } @@ -2140,11 +2142,11 @@ import java.util.*; @Introspected class Test { private Status status; - + public void setStatus(Status status) { this.status = status; } - + public Status getStatus() { return this.status; } @@ -2200,9 +2202,9 @@ class Test { @Size(max=100) private int age; private int[] primitiveArray; - + private long v; - + public Test(String name, @Size(max=100) int age, int[] primitiveArray) { this.name = name; this.age = age; @@ -2219,28 +2221,28 @@ class Test { public void setAge(int age) { this.age = age; } - + public void setId(Long id) { this.id = id; } - + public Long getId() { return this.id; } - + public void setVersion(Long version) { this.version = version; } - + public Long getVersion() { return this.version; } - + @Version public long getAnotherVersion() { return v; } - + public void setAnotherVersion(long v) { this.v = v; } @@ -2314,8 +2316,8 @@ class Test { private String name; @Size(max=100) private int age; - - + + public String getName() { return this.name; } @@ -2328,19 +2330,19 @@ class Test { public void setAge(int age) { this.age = age; } - + public void setId(Long id) { this.id = id; } - + public Long getId() { return this.id; } - + public void setVersion(Long version) { this.version = version; } - + public Long getVersion() { return this.version; } @@ -2491,33 +2493,33 @@ class Test extends ParentBean { private String name; @Size(max=100) private int age; - + private List list; private String[] stringArray; private int[] primitiveArray; private boolean flag; private TypeConverter genericsTest; private TypeConverter genericsArrayTest; - + public TypeConverter getGenericsTest() { return genericsTest; } - + public TypeConverter getGenericsArrayTest() { return genericsArrayTest; } - + public String getReadOnly() { return readOnly; } public boolean isFlag() { return flag; } - + public void setFlag(boolean flag) { this.flag = flag; } - + public String getName() { return this.name; } @@ -2530,11 +2532,11 @@ class Test extends ParentBean { public void setAge(int age) { this.age = age; } - + public List getList() { return this.list; } - + public void setList(List l) { this.list = l; } @@ -2558,11 +2560,11 @@ class Test extends ParentBean { class ParentBean { private List listOfBytes; - + public List getListOfBytes() { return this.listOfBytes; } - + public void setListOfBytes(List list) { this.listOfBytes = list; } @@ -2867,12 +2869,12 @@ import com.fasterxml.jackson.annotation.*; @Introspected class Test { private Map properties; - + @Creator Test(Map properties) { this.properties = properties; } - + public Map getProperties() { return properties; } @@ -2895,16 +2897,16 @@ import io.micronaut.core.annotation.*; @Introspected class Test { private String name; - + private Test(String name) { this.name = name; } - + @Creator public static Test forName(String name) { return new Test(name); } - + public String getName() { return name; } @@ -2990,16 +2992,16 @@ import io.micronaut.core.annotation.*; @Introspected class Test { private String name; - + private Test(String name) { this.name = name; } - + @Creator public static Test forName() { return new Test("default"); } - + public String getName() { return name; } @@ -3037,21 +3039,21 @@ import io.micronaut.core.annotation.*; @Introspected class Test { private String name; - + private Test(String name) { this.name = name; } - + @Creator public static Test forName() { return new Test("default"); } - + @Creator public static Test forName(String name) { return new Test(name); } - + public String getName() { return name; } @@ -3091,22 +3093,22 @@ class Test { private final String name; public static final Companion Companion = new Companion(); - + public final String getName() { return name; } - + private Test(String name) { this.name = name; } - + public static final class Companion { - + @Creator public final Test forName(String name) { return new Test(name); } - + private Companion() { } } @@ -3165,7 +3167,7 @@ public enum Test { Test(int number) { this.number = number; } - + public int getNumber() { return number; } @@ -3286,7 +3288,7 @@ import java.util.Map; class Test { public Test(Map> map) { - + } } @@ -3320,7 +3322,7 @@ class Test { public Test() { } - + public int[] getOneDimension() { return oneDimension; } @@ -3336,7 +3338,7 @@ class Test { public void setTwoDimensions(int[][] twoDimensions) { this.twoDimensions = twoDimensions; } - + public int[][][] getThreeDimensions() { return threeDimensions; } @@ -3394,13 +3396,13 @@ class Test { public Test() { } - + public String[] getOneDimension() { return oneDimension; } public void setOneDimension(String[] oneDimension) { - this.oneDimension = oneDimension; + this.oneDimension = oneDimension; } public String[][] getTwoDimensions() { @@ -3410,7 +3412,7 @@ class Test { public void setTwoDimensions(String[][] twoDimensions) { this.twoDimensions = twoDimensions; } - + public String[][][] getThreeDimensions() { return threeDimensions; } @@ -3469,13 +3471,13 @@ class Test { public Test() { } - + public SomeEnum[] getOneDimension() { return oneDimension; } public void setOneDimension(SomeEnum[] oneDimension) { - this.oneDimension = oneDimension; + this.oneDimension = oneDimension; } public SomeEnum[][] getTwoDimensions() { @@ -3485,7 +3487,7 @@ class Test { public void setTwoDimensions(SomeEnum[][] twoDimensions) { this.twoDimensions = twoDimensions; } - + public SomeEnum[][][] getThreeDimensions() { return threeDimensions; } @@ -3600,11 +3602,11 @@ import io.micronaut.core.annotation.Introspected; class Test { private final String xForwardedFor; - + Test(String xForwardedFor) { this.xForwardedFor = xForwardedFor; } - + public String getXForwardedFor() { return xForwardedFor; } @@ -3665,19 +3667,19 @@ import io.micronaut.core.annotation.Introspected; abstract class Test { private String name; private String author; - + public String getName() { return name; } - + public void setName(String name) { this.name = name; } - + public String getAuthor() { return author; } - + public void setAuthor(String author) { this.author = author; } @@ -3771,23 +3773,23 @@ import io.micronaut.core.annotation.Introspected; abstract class Test { private String name; private String author; - + public String getName() { return name; } - + public void setName(String name) { this.name = name; } - + public String getAuthor() { return author; } - + public void setAuthor(String author) { this.author = author; } - + public int getAge() { return 0; } @@ -3851,7 +3853,7 @@ import java.util.Set; public class Test { private Set authors; - + public Set getAuthors() { return authors; } @@ -3893,7 +3895,7 @@ public class Test { public String getFoo() { return "bar"; } - + @JsonProperty public void setFoo(String s) { } @@ -3947,11 +3949,11 @@ import io.micronaut.core.annotation.Introspected; public class Test { @JsonProperty String foo; - + public String getFoo() { return foo; } - + public void setFoo(String s) { this.foo = s; } @@ -4008,12 +4010,12 @@ import io.micronaut.core.annotation.Introspected; public class Test { @JsonProperty("field") String foo; - + @JsonProperty("getter") public String getFoo() { return foo; } - + @JsonProperty("setter") public void setFoo(String s) { this.foo = s; @@ -4073,11 +4075,11 @@ import io.micronaut.core.annotation.Introspected; public class Test { @JsonProperty("field") String foo; - + public String getFoo() { return foo; } - + @JsonProperty("setter") public void setFoo(String s) { this.foo = s; @@ -4102,15 +4104,15 @@ import io.micronaut.core.annotation.Introspected; @Introspected public class Test { private final java.util.ArrayList foo; - + public Test(java.util.List foo) { this.foo = null; } - + public java.util.List getFoo() { return null; } - + } ''') @@ -4238,7 +4240,7 @@ public record Foo(String name, String isSurname, boolean contains, Boolean purge static class MyTypeElementVisitorProcessor extends TypeElementVisitorProcessor { @Override protected Collection findTypeElementVisitors() { - return [new IntrospectedTypeElementVisitor()] + return [new ValidationVisitor(), new ConfigurationReaderVisitor(), new IntrospectedTypeElementVisitor()] } } diff --git a/inject-java/src/main/java/io/micronaut/annotation/processing/AnnotationUtils.java b/inject-java/src/main/java/io/micronaut/annotation/processing/AnnotationUtils.java index b6c63efd505..90d8df38613 100644 --- a/inject-java/src/main/java/io/micronaut/annotation/processing/AnnotationUtils.java +++ b/inject-java/src/main/java/io/micronaut/annotation/processing/AnnotationUtils.java @@ -16,29 +16,19 @@ package io.micronaut.annotation.processing; import io.micronaut.annotation.processing.visitor.JavaVisitorContext; -import io.micronaut.core.annotation.AnnotationMetadata; -import io.micronaut.core.annotation.AnnotationUtil; import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.Nullable; import io.micronaut.core.convert.value.MutableConvertibleValues; import io.micronaut.core.convert.value.MutableConvertibleValuesMap; import io.micronaut.core.io.service.SoftServiceLoader; -import io.micronaut.core.util.clhm.ConcurrentLinkedHashMap; import io.micronaut.inject.annotation.AnnotatedElementValidator; -import io.micronaut.inject.annotation.AbstractAnnotationMetadataBuilder; - -import io.micronaut.core.annotation.Nullable; import io.micronaut.inject.visitor.TypeElementVisitor; import javax.annotation.processing.Filer; import javax.annotation.processing.Messager; import javax.annotation.processing.ProcessingEnvironment; -import javax.lang.model.element.AnnotationMirror; -import javax.lang.model.element.Element; -import javax.lang.model.element.ExecutableElement; import javax.lang.model.util.Elements; import javax.lang.model.util.Types; -import java.lang.annotation.Annotation; -import java.util.*; /** * Utility methods for annotations. @@ -50,10 +40,6 @@ @Internal public class AnnotationUtils { - private static final int CACHE_SIZE = 100; - private static final Map annotationMetadataCache - = new ConcurrentLinkedHashMap.Builder().maximumWeightedCapacity(CACHE_SIZE).build(); - private final Elements elementUtils; private final Messager messager; private final Types types; @@ -62,7 +48,6 @@ public class AnnotationUtils { private final MutableConvertibleValues visitorAttributes; private final ProcessingEnvironment processingEnv; private final AnnotatedElementValidator elementValidator; - private JavaAnnotationMetadataBuilder javaAnnotationMetadataBuilder; private final GenericUtils genericUtils; /** @@ -95,7 +80,6 @@ protected AnnotationUtils( this.visitorAttributes = visitorAttributes; this.processingEnv = processingEnv; this.elementValidator = SoftServiceLoader.load(AnnotatedElementValidator.class).firstAvailable().orElse(null); - this.javaAnnotationMetadataBuilder = newAnnotationBuilder(); } /** @@ -128,125 +112,6 @@ protected AnnotationUtils( return elementValidator; } - /** - * Return whether the given element is annotated with the given annotation stereotype. - * - * @param element The element - * @param stereotype The stereotype - * @return True if it is - */ - protected boolean hasStereotype(Element element, Class stereotype) { - return hasStereotype(element, stereotype.getName()); - } - - /** - * Return whether the given element is annotated with the given annotation stereotypes. - * - * @param element The element - * @param stereotypes The stereotypes - * @return True if it is - */ - protected boolean hasStereotype(Element element, String... stereotypes) { - return hasStereotype(element, Arrays.asList(stereotypes)); - } - - /** - * Return whether the given element is annotated with any of the given annotation stereotypes. - * - * @param element The element - * @param stereotypes The stereotypes - * @return True if it is - */ - protected boolean hasStereotype(Element element, List stereotypes) { - if (element == null) { - return false; - } - if (stereotypes.contains(element.toString())) { - return true; - } - AnnotationMetadata annotationMetadata = getAnnotationMetadata(element); - for (String stereotype : stereotypes) { - if (annotationMetadata.hasStereotype(stereotype)) { - return true; - } - } - return false; - } - - /** - * Get the annotation metadata for the given element. - * - * @param element The element - * @return The {@link AnnotationMetadata} - */ - public AnnotationMetadata getAnnotationMetadata(Element element) { - AnnotationMetadata metadata = annotationMetadataCache.get(element); - if (metadata == null) { - metadata = javaAnnotationMetadataBuilder.buildOverridden(element); - annotationMetadataCache.put(element, metadata); - } - return metadata; - } - - /** - * Get the declared annotation metadata for the given element. - * - * @param element The element - * @return The {@link AnnotationMetadata} - */ - public AnnotationMetadata getDeclaredAnnotationMetadata(Element element) { - return javaAnnotationMetadataBuilder.buildDeclared(element); - } - - /** - * Get the annotation metadata for the given element and the given parent. - * This method is used for cases when you need to combine annotation metadata for - * two elements, for example a JavaBean property where the field and the method metadata - * need to be combined. - * - * @param parent The parent - * @param element The element - * @return The {@link AnnotationMetadata} - */ - public AnnotationMetadata getAnnotationMetadata(Element parent, Element element) { - return newAnnotationBuilder().buildForParent(parent, element); - } - - /** - * Get the annotation metadata for the given element and the given parents. - * This method is used for cases when you need to combine annotation metadata for - * two elements, for example a JavaBean property where the field and the method metadata - * need to be combined. - * - * @param parents The parents - * @param element The element - * @return The {@link AnnotationMetadata} - */ - public AnnotationMetadata getAnnotationMetadata(List parents, Element element) { - return newAnnotationBuilder().buildForParents(parents, element); - } - - /** - * Check whether the method is annotated. - * - * @param declaringType The declaring type - * @param method The method - * @return True if it is annotated with non internal annotations - */ - public boolean isAnnotated(String declaringType, ExecutableElement method) { - if (AbstractAnnotationMetadataBuilder.isMetadataMutated(declaringType, method)) { - return true; - } - List annotationMirrors = method.getAnnotationMirrors(); - for (AnnotationMirror annotationMirror : annotationMirrors) { - String typeName = annotationMirror.getAnnotationType().toString(); - if (!AnnotationUtil.INTERNAL_ANNOTATION_NAMES.contains(typeName)) { - return true; - } - } - return false; - } - /** * Creates a new annotation builder. * @@ -281,23 +146,4 @@ public JavaVisitorContext newVisitorContext() { ); } - /** - * Invalidates any cached metadata. - */ - @Internal - static void invalidateCache() { - annotationMetadataCache.clear(); - } - - /** - * Invalidates any cached metadata. - * - * @param element The element - */ - @Internal - public static void invalidateMetadata(Element element) { - if (element != null) { - annotationMetadataCache.remove(element); - } - } } diff --git a/inject-java/src/main/java/io/micronaut/annotation/processing/BeanDefinitionInjectProcessor.java b/inject-java/src/main/java/io/micronaut/annotation/processing/BeanDefinitionInjectProcessor.java index e0a39b54b87..d308dddba16 100644 --- a/inject-java/src/main/java/io/micronaut/annotation/processing/BeanDefinitionInjectProcessor.java +++ b/inject-java/src/main/java/io/micronaut/annotation/processing/BeanDefinitionInjectProcessor.java @@ -15,54 +15,21 @@ */ package io.micronaut.annotation.processing; -import io.micronaut.annotation.processing.visitor.JavaElementFactory; -import io.micronaut.annotation.processing.visitor.JavaMethodElement; +import io.micronaut.annotation.processing.visitor.JavaClassElement; import io.micronaut.annotation.processing.visitor.JavaVisitorContext; -import io.micronaut.aop.Adapter; -import io.micronaut.aop.Interceptor; -import io.micronaut.aop.InterceptorKind; -import io.micronaut.aop.Introduction; -import io.micronaut.aop.internal.intercepted.InterceptedMethodUtil; -import io.micronaut.aop.writer.AopProxyWriter; -import io.micronaut.context.RequiresCondition; -import io.micronaut.context.annotation.Bean; -import io.micronaut.context.annotation.ConfigurationBuilder; -import io.micronaut.context.annotation.ConfigurationInject; import io.micronaut.context.annotation.ConfigurationReader; import io.micronaut.context.annotation.Context; -import io.micronaut.context.annotation.DefaultScope; -import io.micronaut.context.annotation.EachProperty; -import io.micronaut.context.annotation.Executable; -import io.micronaut.context.annotation.Factory; -import io.micronaut.context.annotation.Property; -import io.micronaut.context.annotation.Requires; -import io.micronaut.context.annotation.Value; -import io.micronaut.core.annotation.AccessorsStyle; -import io.micronaut.core.annotation.AnnotationClassValue; import io.micronaut.core.annotation.AnnotationMetadata; import io.micronaut.core.annotation.AnnotationUtil; import io.micronaut.core.annotation.Internal; import io.micronaut.core.annotation.NonNull; -import io.micronaut.core.annotation.Nullable; -import io.micronaut.core.bind.annotation.Bindable; import io.micronaut.core.naming.NameUtils; -import io.micronaut.core.util.ArrayUtils; import io.micronaut.core.util.CollectionUtils; -import io.micronaut.core.util.StringUtils; -import io.micronaut.core.value.OptionalValues; +import io.micronaut.inject.processing.ProcessingException; import io.micronaut.inject.annotation.AbstractAnnotationMetadataBuilder; -import io.micronaut.inject.annotation.AnnotationMetadataHierarchy; -import io.micronaut.inject.annotation.AnnotationMetadataReference; -import io.micronaut.inject.annotation.MutableAnnotationMetadata; -import io.micronaut.inject.ast.ClassElement; -import io.micronaut.inject.ast.ElementQuery; -import io.micronaut.inject.ast.FieldElement; -import io.micronaut.inject.ast.MethodElement; -import io.micronaut.inject.ast.ParameterElement; -import io.micronaut.inject.ast.TypedElement; -import io.micronaut.inject.configuration.ConfigurationMetadata; -import io.micronaut.inject.configuration.ConfigurationMetadataBuilder; -import io.micronaut.inject.configuration.PropertyMetadata; +import io.micronaut.inject.ast.ElementAnnotationMetadataFactory; +import io.micronaut.inject.processing.BeanDefinitionCreator; +import io.micronaut.inject.processing.BeanDefinitionCreatorFactory; import io.micronaut.inject.processing.JavaModelUtils; import io.micronaut.inject.visitor.BeanElementVisitor; import io.micronaut.inject.visitor.VisitorConfiguration; @@ -70,50 +37,24 @@ import io.micronaut.inject.writer.BeanDefinitionReferenceWriter; import io.micronaut.inject.writer.BeanDefinitionVisitor; import io.micronaut.inject.writer.BeanDefinitionWriter; -import io.micronaut.inject.writer.OriginatingElements; import javax.annotation.processing.ProcessingEnvironment; import javax.annotation.processing.RoundEnvironment; import javax.annotation.processing.SupportedOptions; -import javax.lang.model.element.AnnotationMirror; -import javax.lang.model.element.AnnotationValue; import javax.lang.model.element.Element; import javax.lang.model.element.ElementKind; -import javax.lang.model.element.ExecutableElement; -import javax.lang.model.element.Modifier; import javax.lang.model.element.Name; -import javax.lang.model.element.PackageElement; import javax.lang.model.element.TypeElement; -import javax.lang.model.element.TypeParameterElement; -import javax.lang.model.element.VariableElement; -import javax.lang.model.type.DeclaredType; -import javax.lang.model.type.PrimitiveType; -import javax.lang.model.type.TypeKind; import javax.lang.model.type.TypeMirror; -import javax.lang.model.type.TypeVariable; -import javax.lang.model.util.ElementFilter; -import javax.lang.model.util.ElementScanner8; import java.io.IOException; -import java.time.Duration; -import java.util.Arrays; -import java.util.Collections; import java.util.HashSet; -import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Optional; import java.util.Set; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.function.Predicate; import java.util.stream.Collectors; import static javax.lang.model.element.ElementKind.ANNOTATION_TYPE; -import static javax.lang.model.element.ElementKind.CONSTRUCTOR; import static javax.lang.model.element.ElementKind.ENUM; -import static javax.lang.model.element.ElementKind.FIELD; /** *

The core annotation processor used to generate bean definitions and power AOP for Micronaut.

@@ -132,34 +73,27 @@ public class BeanDefinitionInjectProcessor extends AbstractInjectAnnotationProce private static final String AROUND_TYPE = AnnotationUtil.ANN_AROUND; private static final String INTRODUCTION_TYPE = AnnotationUtil.ANN_INTRODUCTION; private static final String[] ANNOTATION_STEREOTYPES = new String[]{ - AnnotationUtil.POST_CONSTRUCT, - AnnotationUtil.PRE_DESTROY, - "jakarta.annotation.PreDestroy", - "jakarta.annotation.PostConstruct", - "javax.inject.Inject", - "javax.inject.Qualifier", - "javax.inject.Singleton", - "jakarta.inject.Inject", - "jakarta.inject.Qualifier", - "jakarta.inject.Singleton", - "io.micronaut.context.annotation.Bean", - "io.micronaut.context.annotation.Replaces", - "io.micronaut.context.annotation.Value", - "io.micronaut.context.annotation.Property", - "io.micronaut.context.annotation.Executable", - AROUND_TYPE, - AnnotationUtil.ANN_INTERCEPTOR_BINDINGS, - AnnotationUtil.ANN_INTERCEPTOR_BINDING, - INTRODUCTION_TYPE + AnnotationUtil.POST_CONSTRUCT, + AnnotationUtil.PRE_DESTROY, + "jakarta.annotation.PreDestroy", + "jakarta.annotation.PostConstruct", + "javax.inject.Inject", + "javax.inject.Qualifier", + "javax.inject.Singleton", + "jakarta.inject.Inject", + "jakarta.inject.Qualifier", + "jakarta.inject.Singleton", + "io.micronaut.context.annotation.Bean", + "io.micronaut.context.annotation.Replaces", + "io.micronaut.context.annotation.Value", + "io.micronaut.context.annotation.Property", + "io.micronaut.context.annotation.Executable", + AROUND_TYPE, + AnnotationUtil.ANN_INTERCEPTOR_BINDINGS, + AnnotationUtil.ANN_INTERCEPTOR_BINDING, + INTRODUCTION_TYPE }; - private static final String ANN_CONSTRAINT = "javax.validation.Constraint"; - private static final String ANN_VALID = "javax.validation.Valid"; - private static final Predicate IS_CONSTRAINT = am -> - am.hasStereotype(ANN_CONSTRAINT) || am.hasStereotype(ANN_VALID); - private static final String ANN_CONFIGURATION_ADVICE = "io.micronaut.runtime.context.env.ConfigurationAdvice"; - private static final String ANN_VALIDATED = "io.micronaut.validation.Validated"; - private JavaConfigurationMetadataBuilder metadataBuilder; private Set beanDefinitions; private boolean processingOver; private final Set processed = new HashSet<>(); @@ -167,8 +101,6 @@ public class BeanDefinitionInjectProcessor extends AbstractInjectAnnotationProce @Override public final synchronized void init(ProcessingEnvironment processingEnv) { super.init(processingEnv); - this.metadataBuilder = new JavaConfigurationMetadataBuilder(elementUtils, typeUtils, annotationUtils); - ConfigurationMetadataBuilder.setConfigurationMetadataBuilder(metadataBuilder); this.beanDefinitions = new LinkedHashSet<>(); for (BeanElementVisitor visitor : BeanElementVisitor.VISITORS) { @@ -186,16 +118,16 @@ public final synchronized void init(ProcessingEnvironment processingEnv) { @Override protected JavaVisitorContext newVisitorContext(@NonNull ProcessingEnvironment processingEnv) { return new JavaVisitorContext( - processingEnv, - messager, - elementUtils, - annotationUtils, - typeUtils, - modelUtils, - genericUtils, - filer, - visitorAttributes, - getVisitorKind() + processingEnv, + messager, + elementUtils, + annotationUtils, + typeUtils, + modelUtils, + genericUtils, + filer, + visitorAttributes, + getVisitorKind() ) { @NonNull @Override @@ -215,54 +147,56 @@ public final boolean process(Set annotations, RoundEnviro processingOver = roundEnv.processingOver(); if (!processingOver) { - + JavaAnnotationMetadataBuilder annotationMetadataBuilder = javaVisitorContext.getAnnotationMetadataBuilder(); annotations = annotations - .stream() - .filter(ann -> { - final String name = ann.getQualifiedName().toString(); - String packageName = NameUtils.getPackageName(name); - return !name.equals(AnnotationUtil.KOTLIN_METADATA) && !AnnotationUtil.STEREOTYPE_EXCLUDES.contains(packageName); - }) - .filter(ann -> annotationUtils.hasStereotype(ann, ANNOTATION_STEREOTYPES) || isProcessedAnnotation(ann.getQualifiedName().toString())) - .collect(Collectors.toSet()); + .stream() + .filter(ann -> { + final String name = ann.getQualifiedName().toString(); + String packageName = NameUtils.getPackageName(name); + return !name.equals(AnnotationUtil.KOTLIN_METADATA) && !AnnotationUtil.STEREOTYPE_EXCLUDES.contains(packageName); + }) + .filter(ann -> annotationMetadataBuilder.lookupOrBuildForType(ann).hasStereotype(ANNOTATION_STEREOTYPES) + || isProcessedAnnotation(ann.getQualifiedName().toString())) + .collect(Collectors.toSet()); if (!annotations.isEmpty()) { TypeElement groovyObjectTypeElement = elementUtils.getTypeElement("groovy.lang.GroovyObject"); TypeMirror groovyObjectType = groovyObjectTypeElement != null ? groovyObjectTypeElement.asType() : null; // accumulate all the class elements for all annotated elements annotations.forEach(annotation -> roundEnv.getElementsAnnotatedWith(annotation) - .stream() - // filtering annotation definitions, which are not processed - .filter(element -> element.getKind() != ANNOTATION_TYPE) - .forEach(element -> { - TypeElement typeElement = modelUtils.classElementFor(element); - if (typeElement == null) { - return; - } - if (element.getKind() == ENUM) { - final AnnotationMetadata am = annotationUtils.getAnnotationMetadata(element); - if (isDeclaredBeanInMetadata(am)) { - error(element, "Enum types cannot be defined as beans"); - } - return; - } - // skip Groovy code, handled by InjectTransform. Required for GroovyEclipse compiler - if ((groovyObjectType != null && typeUtils.isAssignable(typeElement.asType(), groovyObjectType))) { - return; + .stream() + // filtering annotation definitions, which are not processed + .filter(element -> element.getKind() != ANNOTATION_TYPE) + .forEach(element -> { + TypeElement typeElement = modelUtils.classElementFor(element); + if (typeElement == null) { + return; + } + if (element.getKind() == ENUM) { + final AnnotationMetadata am = annotationMetadataBuilder.lookupOrBuildForType(element); + if (BeanDefinitionCreatorFactory.isDeclaredBeanInMetadata(am)) { + error(element, "Enum types cannot be defined as beans"); } + return; + } + // skip Groovy code, handled by InjectTransform. Required for GroovyEclipse compiler + if ((groovyObjectType != null && typeUtils.isAssignable(typeElement.asType(), groovyObjectType))) { + return; + } - String name = typeElement.getQualifiedName().toString(); - if (!beanDefinitions.contains(name) && !processed.contains(name) && !name.endsWith(BeanDefinitionVisitor.PROXY_SUFFIX)) { - boolean isInterface = JavaModelUtils.resolveKind(typeElement, ElementKind.INTERFACE).isPresent(); - if (!isInterface) { + String name = typeElement.getQualifiedName().toString(); + if (!beanDefinitions.contains(name) && !processed.contains(name) && !name.endsWith(BeanDefinitionVisitor.PROXY_SUFFIX)) { + boolean isInterface = JavaModelUtils.resolveKind(typeElement, ElementKind.INTERFACE).isPresent(); + if (!isInterface) { + beanDefinitions.add(name); + } else { + AnnotationMetadata annotationMetadata = annotationMetadataBuilder.lookupOrBuildForType(typeElement); + if (annotationMetadata.hasStereotype(INTRODUCTION_TYPE) || annotationMetadata.hasStereotype(ConfigurationReader.class)) { beanDefinitions.add(name); - } else { - if (annotationUtils.hasStereotype(typeElement, INTRODUCTION_TYPE) || annotationUtils.hasStereotype(typeElement, ConfigurationReader.class)) { - beanDefinitions.add(name); - } } } - })); + } + })); } // remove already processed in previous round @@ -274,25 +208,32 @@ public final boolean process(Set annotations, RoundEnviro int count = beanDefinitions.size(); if (count > 0) { note("Creating bean classes for %s type elements", count); - beanDefinitions.forEach(className -> { + ElementAnnotationMetadataFactory annotationMetadataFactory = javaVisitorContext.getElementAnnotationMetadataFactory().readOnly(); + for (String className : beanDefinitions) { if (processed.add(className)) { - final TypeElement refreshedClassElement = elementUtils.getTypeElement(className); + final TypeElement typeElement = elementUtils.getTypeElement(className); try { - final AnnBeanElementVisitor visitor = new AnnBeanElementVisitor(refreshedClassElement); - refreshedClassElement.accept(visitor, className); - visitor.getBeanDefinitionWriters().forEach((name, writer) -> { - String beanDefinitionName = writer.getBeanDefinitionName(); - if (processed.add(beanDefinitionName)) { + Name classElementQualifiedName = typeElement.getQualifiedName(); + if ("java.lang.Record".equals(classElementQualifiedName.toString())) { + continue; + } + JavaClassElement classElement = javaVisitorContext.getElementFactory() + .newClassElement(typeElement, annotationMetadataFactory); + BeanDefinitionCreator beanDefinitionCreator = BeanDefinitionCreatorFactory.produce(classElement, javaVisitorContext); + for (BeanDefinitionVisitor writer : beanDefinitionCreator.build()) { + if (processed.add(writer.getBeanDefinitionName())) { processBeanDefinitions(writer); + } else { + throw new IllegalStateException("Already processed: " + writer.getBeanDefinitionName()); } - }); - AnnotationUtils.invalidateMetadata(refreshedClassElement); + } + } catch (ProcessingException ex) { + error((Element) ex.getOriginatingElement(), ex.getMessage()); } catch (PostponeToNextRoundException e) { processed.remove(className); } } - }); - AnnotationUtils.invalidateCache(); + } } } @@ -323,7 +264,6 @@ public final boolean process(Set annotations, RoundEnviro } } } finally { - AnnotationUtils.invalidateCache(); AbstractAnnotationMetadataBuilder.clearMutated(); JavaAnnotationMetadataBuilder.clearCaches(); } @@ -350,13 +290,13 @@ private void processBeanDefinitions(BeanDefinitionVisitor beanDefinitionWriter) if (beanDefinitionWriter.isEnabled()) { beanDefinitionWriter.accept(classWriterOutputVisitor); BeanDefinitionReferenceWriter beanDefinitionReferenceWriter = - new BeanDefinitionReferenceWriter(beanDefinitionWriter); + new BeanDefinitionReferenceWriter(beanDefinitionWriter); beanDefinitionReferenceWriter.setRequiresMethodProcessing(beanDefinitionWriter.requiresMethodProcessing()); String className = beanDefinitionReferenceWriter.getBeanDefinitionQualifiedClassName(); processed.add(className); beanDefinitionReferenceWriter.setContextScope( - beanDefinitionWriter.getAnnotationMetadata().hasDeclaredAnnotation(Context.class)); + beanDefinitionWriter.getAnnotationMetadata().hasDeclaredAnnotation(Context.class)); beanDefinitionReferenceWriter.accept(classWriterOutputVisitor); } @@ -367,1934 +307,4 @@ private void processBeanDefinitions(BeanDefinitionVisitor beanDefinitionWriter) } } - private String getPropertyMetadataTypeReference(TypeMirror valueType) { - if (modelUtils.isOptional(valueType)) { - return genericUtils.getFirstTypeArgument(valueType) - .map(typeMirror -> modelUtils.resolveTypeName(typeMirror)) - .orElseGet(() -> modelUtils.resolveTypeName(valueType)); - } else { - return modelUtils.resolveTypeName(valueType); - } - } - - private AnnotationMetadata addPropertyMetadata(io.micronaut.inject.ast.Element targetElement, PropertyMetadata propertyMetadata) { - return targetElement.annotate(Property.class, (builder) -> builder.member("name", propertyMetadata.getPath())).getAnnotationMetadata(); - } - - private boolean isDeclaredBeanInMetadata(AnnotationMetadata concreteClassMetadata) { - return concreteClassMetadata.hasDeclaredStereotype(Bean.class) || - concreteClassMetadata.hasStereotype(AnnotationUtil.SCOPE) || - concreteClassMetadata.hasStereotype(DefaultScope.class); - } - - /** - * Annotation Bean element visitor. - */ - class AnnBeanElementVisitor extends ElementScanner8 { - private final TypeElement concreteClass; - private final AnnotationMetadata concreteClassMetadata; - private final Map beanDefinitionWriters; - private final boolean isConfigurationPropertiesType; - private final boolean isFactoryType; - private final boolean isExecutableType; - private final boolean isAopProxyType; - private final OptionalValues aopSettings; - private final boolean isDeclaredBean; - private final JavaElementFactory elementFactory; - private final ClassElement concreteClassElement; - private final MethodElement constructorElement; - private final AnnotationMetadata constructorAnnotationMetadata; - private ConfigurationMetadata configurationMetadata; - private final AtomicInteger adaptedMethodIndex = new AtomicInteger(0); - private final AtomicInteger factoryMethodIndex = new AtomicInteger(0); - private AnnotationMetadata currentClassMetadata; - private final Set visitedTypes = new HashSet<>(); - - /** - * @param concreteClass The {@link TypeElement} - */ - AnnBeanElementVisitor(TypeElement concreteClass) { - this.concreteClass = concreteClass; - this.concreteClassMetadata = annotationUtils.getAnnotationMetadata(concreteClass); - this.currentClassMetadata = concreteClassMetadata; - this.elementFactory = javaVisitorContext.getElementFactory(); - this.concreteClassElement = elementFactory.newClassElement(concreteClass, concreteClassMetadata); - this.beanDefinitionWriters = new LinkedHashMap<>(); - this.isFactoryType = concreteClassMetadata.hasStereotype(Factory.class); - this.isConfigurationPropertiesType = concreteClassMetadata.hasDeclaredStereotype(ConfigurationReader.class) || concreteClassMetadata.hasDeclaredStereotype(EachProperty.class); - this.isAopProxyType = !concreteClassElement.isAbstract() && !concreteClassElement.isAssignable(Interceptor.class) && InterceptedMethodUtil.hasAroundStereotype(concreteClassMetadata); - this.aopSettings = isAopProxyType ? concreteClassMetadata.getValues(AROUND_TYPE, Boolean.class) : OptionalValues.empty(); - this.constructorElement = concreteClassElement.getPrimaryConstructor().orElse(null); - MethodElement constructorElement = this.constructorElement; - if (constructorElement != null) { - postponeIfParametersContainErrors((ExecutableElement) constructorElement.getNativeType()); - } - this.constructorAnnotationMetadata = constructorElement != null ? constructorElement.getAnnotationMetadata() : AnnotationMetadata.EMPTY_METADATA; - this.isExecutableType = isAopProxyType || concreteClassMetadata.hasStereotype(Executable.class); - boolean hasQualifier = concreteClassMetadata.hasStereotype(AnnotationUtil.QUALIFIER) && !concreteClassElement.isAbstract(); - this.isDeclaredBean = isDeclaredBean(constructorElement, hasQualifier); - } - - private void postponeIfParametersContainErrors(ExecutableElement executableElement) { - if (executableElement != null && !processingOver) { - List parameters = executableElement.getParameters(); - for (VariableElement parameter : parameters) { - TypeMirror typeMirror = parameter.asType(); - if ((typeMirror.getKind() == TypeKind.ERROR)) { - throw new PostponeToNextRoundException(); - } - } - } - } - - private boolean isDeclaredBean(@Nullable MethodElement constructor, boolean hasQualifier) { - final AnnotationMetadata concreteClassMetadata = this.concreteClassMetadata; - return isExecutableType || - isDeclaredBeanInMetadata(concreteClassMetadata) || - (constructor != null && constructor.hasStereotype(AnnotationUtil.INJECT)) || hasQualifier; - } - - /** - * @return The bean definition writers - */ - Map getBeanDefinitionWriters() { - return beanDefinitionWriters; - } - - @Override - public Object visitType(TypeElement classElement, Object o) { - Name classElementQualifiedName = classElement.getQualifiedName(); - if ("java.lang.Record".equals(classElementQualifiedName.toString())) { - return o; - } - if (visitedTypes.contains(classElementQualifiedName)) { - // bail out if already visited - return o; - } - boolean isInterface = concreteClassElement.isInterface(); - visitedTypes.add(classElementQualifiedName); - AnnotationMetadata typeAnnotationMetadata = annotationUtils.getAnnotationMetadata(classElement); - this.currentClassMetadata = typeAnnotationMetadata; - - if (isConfigurationPropertiesType) { - this.configurationMetadata = metadataBuilder.visitProperties( - concreteClass, - concreteClassElement.getDocumentation().orElse(null) - ); - if (isInterface) { - typeAnnotationMetadata = concreteClassElement.annotate( - ANN_CONFIGURATION_ADVICE - ).getAnnotationMetadata(); - this.currentClassMetadata = typeAnnotationMetadata; - } - } - // don't process inner class unless this is the visitor for it - final Name qualifiedName = concreteClass.getQualifiedName(); - boolean isTypeConcreteClass = qualifiedName.equals(classElementQualifiedName); - - if (typeAnnotationMetadata.hasStereotype(INTRODUCTION_TYPE) && isTypeConcreteClass) { - AopProxyWriter aopProxyWriter = createIntroductionAdviceWriter(concreteClassElement); - - if (constructorElement != null) { - aopProxyWriter.visitBeanDefinitionConstructor( - constructorElement, - concreteClassElement.isPrivate(), - javaVisitorContext - ); - } else { - aopProxyWriter.visitDefaultConstructor(concreteClassMetadata, javaVisitorContext); - } - beanDefinitionWriters.put(classElementQualifiedName, aopProxyWriter); - visitAnnotationMetadata(aopProxyWriter, this.currentClassMetadata); - visitIntroductionAdviceInterface(classElement, typeAnnotationMetadata, aopProxyWriter); - - if (!isInterface) { - - List elements = classElement.getEnclosedElements().stream() - // already handled the public ctor - .filter(element -> element.getKind() != CONSTRUCTOR) - .collect(Collectors.toList()); - return scan(elements, o); - } else { - return null; - } - - } else { - Element enclosingElement = classElement.getEnclosingElement(); - if (!JavaModelUtils.isClass(enclosingElement) || isTypeConcreteClass) { - if (isTypeConcreteClass) { - if (isDeclaredBean) { - // we know this class has supported annotations so we need a beandef writer for it - PackageElement packageElement = elementUtils.getPackageOf(classElement); - if (packageElement.isUnnamed()) { - error(classElement, "Micronaut beans cannot be in the default package"); - return null; - } - BeanDefinitionVisitor beanDefinitionWriter = getOrCreateBeanDefinitionWriter(classElement, qualifiedName); - visitAnnotationMetadata(beanDefinitionWriter, this.currentClassMetadata); - - if (isAopProxyType) { - - if (modelUtils.isFinal(classElement)) { - error(classElement, "Cannot apply AOP advice to final class. Class must be made non-final to support proxying: " + classElement); - return null; - } - io.micronaut.core.annotation.AnnotationValue[] interceptorTypes = - InterceptedMethodUtil.resolveInterceptorBinding(concreteClassMetadata, InterceptorKind.AROUND); - resolveAopProxyWriter( - beanDefinitionWriter, - aopSettings, - false, - this.constructorElement, - interceptorTypes - ); - } - } else { - if (modelUtils.isAbstract(classElement)) { - return null; - } - } - } - - List elements = classElement - .getEnclosedElements() - .stream() - // already handled the public ctor - .filter(element -> element.getKind() != CONSTRUCTOR) - .collect(Collectors.toList()); - - if (isConfigurationPropertiesType) { - // handle non @Inject, @Value fields as config properties - List members = elementUtils.getAllMembers(classElement); - ElementFilter.fieldsIn(members).forEach( - field -> { - if (classElement != field.getEnclosingElement()) { - - AnnotationMetadata fieldAnnotationMetadata = annotationUtils.getDeclaredAnnotationMetadata(field); - boolean isConfigBuilder = fieldAnnotationMetadata.hasStereotype(ConfigurationBuilder.class); - if (modelUtils.isStatic(field)) { - return; - } - // its common for builders to be initialized, so allow final - if (!modelUtils.isFinal(field) || isConfigBuilder) { - visitConfigurationProperty(field, fieldAnnotationMetadata); - } - } - } - ); - - AnnotationMetadataHierarchy annotationMetadata = new AnnotationMetadataHierarchy(concreteClassMetadata, constructorAnnotationMetadata); - final String[] readPrefixes = annotationMetadata.getValue(AccessorsStyle.class, "readPrefixes", String[].class) - .orElse(new String[]{AccessorsStyle.DEFAULT_READ_PREFIX}); - final String[] writePrefixes = annotationMetadata.getValue(AccessorsStyle.class, "writePrefixes", String[].class) - .orElse(new String[]{AccessorsStyle.DEFAULT_WRITE_PREFIX}); - - ElementFilter.methodsIn(members).forEach(method -> { - boolean isCandidateMethod = !modelUtils.isStatic(method) && - !modelUtils.isPrivate(method) && - !modelUtils.isAbstract(method); - if (isCandidateMethod) { - Element e = method.getEnclosingElement(); - if (e instanceof TypeElement && !e.equals(classElement)) { - String methodName = method.getSimpleName().toString(); - if (NameUtils.isWriterName(methodName, writePrefixes) && method.getParameters().size() == 1) { - visitConfigurationPropertySetter(method); - } else if (NameUtils.isReaderName(methodName, readPrefixes)) { - BeanDefinitionVisitor writer = getOrCreateBeanDefinitionWriter(concreteClass, concreteClass.getQualifiedName()); - if (!writer.isValidated()) { - writer.setValidated(IS_CONSTRAINT.test(annotationUtils.getAnnotationMetadata(method))); - } - } - } - } - }); - } else { - TypeElement superClass = modelUtils.superClassFor(classElement); - if (superClass != null && !modelUtils.isObjectClass(superClass)) { - superClass.accept(this, o); - } - } - - return scan(elements, o); - } else { - return null; - } - } - } - - /** - * Gets or creates a bean definition writer. - * - * @param classElement The class element - * @param qualifiedName The name - * @return The writer - */ - public BeanDefinitionVisitor getOrCreateBeanDefinitionWriter(TypeElement classElement, Name qualifiedName) { - BeanDefinitionVisitor beanDefinitionWriter = beanDefinitionWriters.get(qualifiedName); - if (beanDefinitionWriter == null) { - - beanDefinitionWriter = createBeanDefinitionWriterFor(classElement); - Name proxyKey = createProxyKey(beanDefinitionWriter.getBeanDefinitionName()); - beanDefinitionWriters.put(qualifiedName, beanDefinitionWriter); - - - BeanDefinitionVisitor proxyWriter = beanDefinitionWriters.get(proxyKey); - final AnnotationMetadata annotationMetadata = new AnnotationMetadataHierarchy( - concreteClassMetadata, - constructorAnnotationMetadata - ); - if (proxyWriter != null) { - if (constructorElement != null) { - proxyWriter.visitBeanDefinitionConstructor( - constructorElement, - constructorElement.isPrivate(), - javaVisitorContext - ); - } else { - proxyWriter.visitDefaultConstructor( - annotationMetadata, - javaVisitorContext - ); - } - } - - if (constructorElement != null) { - beanDefinitionWriter.visitBeanDefinitionConstructor( - constructorElement, - constructorElement.isPrivate(), - javaVisitorContext - ); - } else { - beanDefinitionWriter.visitDefaultConstructor(annotationMetadata, javaVisitorContext); - } - } - return beanDefinitionWriter; - } - - private void visitAnnotationMetadata(BeanDefinitionVisitor writer, AnnotationMetadata annotationMetadata) { - for (io.micronaut.core.annotation.AnnotationValue annotation: annotationMetadata.getAnnotationValuesByType(Requires.class)) { - annotation.stringValue(RequiresCondition.MEMBER_BEAN_PROPERTY) - .ifPresent(beanProperty -> { - annotation.stringValue(RequiresCondition.MEMBER_BEAN) - .map(className -> elementUtils.getTypeElement(className.replace('$', '.'))) - .map(element -> elementFactory.newClassElement(element, annotationUtils.getAnnotationMetadata(element))) - .ifPresent(classElement -> { - String requiredValue = annotation.stringValue().orElse(null); - String notEqualsValue = annotation.stringValue(RequiresCondition.MEMBER_NOT_EQUALS).orElse(null); - writer.visitAnnotationMemberPropertyInjectionPoint(classElement, beanProperty, requiredValue, notEqualsValue); - }); - }); - } - } - - private void visitIntroductionAdviceInterface(TypeElement classElement, AnnotationMetadata typeAnnotationMetadata, AopProxyWriter aopProxyWriter) { - ClassElement introductionType = elementFactory.newClassElement(classElement, typeAnnotationMetadata); - final AnnotationMetadata resolvedTypeMetadata = annotationUtils.getAnnotationMetadata(classElement); - final boolean resolvedTypeMetadataIsAopProxyType = InterceptedMethodUtil.hasDeclaredAroundAdvice(resolvedTypeMetadata); - final boolean isConfigProps = typeAnnotationMetadata.hasAnnotation(ANN_CONFIGURATION_ADVICE); - if (isConfigProps) { - metadataBuilder.visitProperties( - classElement, - null - ); - } - classElement.asType().accept(new PublicAbstractMethodVisitor(classElement, javaVisitorContext) { - - @Override - protected boolean isAcceptableMethod(ExecutableElement executableElement) { - return super.isAcceptableMethod(executableElement) - || resolvedTypeMetadataIsAopProxyType - || hasMethodLevelAdvice(executableElement); - } - - private boolean hasMethodLevelAdvice(ExecutableElement executableElement) { - final AnnotationMetadata annotationMetadata = annotationUtils.getAnnotationMetadata(executableElement); - return InterceptedMethodUtil.hasDeclaredAroundAdvice(annotationMetadata); - } - - @Override - protected void accept(DeclaredType type, Element element, AopProxyWriter aopProxyWriter) { - ExecutableElement method = (ExecutableElement) element; - Element declaredTypeElement = type.asElement(); - if (declaredTypeElement instanceof TypeElement) { - ClassElement declaringClassElement = elementFactory.newClassElement( - (TypeElement) declaredTypeElement, - concreteClassMetadata - ); - if (!classElement.equals(declaredTypeElement)) { - aopProxyWriter.addOriginatingElement( - declaringClassElement - ); - } - - TypeElement owningType = modelUtils.classElementFor(method); - if (owningType == null) { - throw new IllegalStateException("Owning type cannot be null"); - } - ClassElement owningTypeElement = elementFactory.newClassElement(owningType, typeAnnotationMetadata); - - AnnotationMetadata annotationMetadata; - - if (annotationUtils.isAnnotated(introductionType.getName(), method) || JavaAnnotationMetadataBuilder.hasAnnotation(method, Override.class)) { - // Class annotations are referenced by typeAnnotationMetadata - annotationMetadata = annotationUtils.newAnnotationBuilder().buildForParent(introductionType.getName(), null, method); - annotationMetadata = new AnnotationMetadataHierarchy(typeAnnotationMetadata, annotationMetadata); - } else { - annotationMetadata = new AnnotationMetadataReference( - aopProxyWriter.getBeanDefinitionName() + BeanDefinitionReferenceWriter.REF_SUFFIX, - typeAnnotationMetadata - ); - } - - MethodElement javaMethodElement = elementFactory.newMethodElement( - introductionType, - method, - annotationMetadata - ); - String methodName = javaMethodElement.getName(); - - if (!annotationMetadata.hasStereotype(ANN_VALIDATED) && - isDeclaredBean && - Arrays.stream(javaMethodElement.getParameters()).anyMatch(IS_CONSTRAINT)) { - annotationMetadata = javaMethodElement.annotate(ANN_VALIDATED).getAnnotationMetadata(); - } - - if (isConfigProps) { - if (javaMethodElement.isAbstract()) { - - if (!aopProxyWriter.isValidated()) { - aopProxyWriter.setValidated(IS_CONSTRAINT.test(annotationMetadata)); - } - - final String[] readPrefixes = declaringClassElement.getValue(AccessorsStyle.class, "readPrefixes", String[].class) - .orElse(new String[]{AccessorsStyle.DEFAULT_READ_PREFIX}); - - if (!NameUtils.isReaderName(methodName, readPrefixes)) { - error(classElement, "Only getter methods are allowed on @ConfigurationProperties interfaces: " + method + ". You can change the accessors using @AccessorsStyle annotation"); - return; - } - - if (javaMethodElement.hasParameters()) { - error(classElement, "Only zero argument getter methods are allowed on @ConfigurationProperties interfaces: " + method); - return; - } - - String docComment = elementUtils.getDocComment(method); - final String propertyName = NameUtils.getPropertyNameForGetter(methodName, readPrefixes); - final String propertyType = javaMethodElement.getReturnType().getName(); - - if ("void".equals(propertyType)) { - error(classElement, "Getter methods must return a value @ConfigurationProperties interfaces: " + method); - return; - } - final PropertyMetadata propertyMetadata = metadataBuilder.visitProperty( - classElement, - classElement, - propertyType, - propertyName, - docComment, - annotationMetadata.stringValue(Bindable.class, "defaultValue").orElse(null) - ); - addPropertyMetadata( - javaMethodElement, - propertyMetadata - ); - - annotationMetadata = javaMethodElement.annotate(ANN_CONFIGURATION_ADVICE, (annBuilder) -> { - if (!javaMethodElement.getReturnType().isPrimitive() && javaMethodElement.getReturnType().hasStereotype(AnnotationUtil.SCOPE)) { - annBuilder.member("bean", true); - } - if (typeAnnotationMetadata.hasStereotype(EachProperty.class)) { - annBuilder.member("iterable", true); - } - - }).getAnnotationMetadata(); - } - } - - if (annotationMetadata.hasStereotype(AROUND_TYPE) || annotationMetadata.hasStereotype(AnnotationUtil.ANN_INTERCEPTOR_BINDINGS)) { - io.micronaut.core.annotation.AnnotationValue[] interceptorTypes = InterceptedMethodUtil.resolveInterceptorBinding(annotationMetadata, InterceptorKind.AROUND); - aopProxyWriter.visitInterceptorBinding(interceptorTypes); - } - - if (javaMethodElement.isAbstract()) { - aopProxyWriter.visitIntroductionMethod( - owningTypeElement, - javaMethodElement - ); - } else { - boolean isInterface = declaringClassElement.isInterface(); - boolean isDefault = method.isDefault(); - if (isInterface && isDefault) { - // Default methods cannot be "super" accessed on the defined type - owningTypeElement = introductionType; - } - - // only apply around advise to non-abstract methods of introduction advise - aopProxyWriter.visitAroundMethod( - owningTypeElement, - javaMethodElement - ); - } - } - - - } - }, aopProxyWriter); - } - - @Override - public Object visitExecutable(ExecutableElement method, Object o) { - if (method.getKind() == ElementKind.CONSTRUCTOR) { - // ctor is handled by visitType - error("Unexpected call to visitExecutable for ctor %s of %s", - method.getSimpleName(), o); - return null; - } - - if (modelUtils.isAbstract(method)) { - return null; - } - - postponeIfParametersContainErrors(method); - - AnnotationMetadata methodAndClassAnnotationMetadata = getMetadataHierarchy(annotationUtils.getAnnotationMetadata(method)); - AnnotationMetadata methodAnnotationMetadata = methodAndClassAnnotationMetadata.getDeclaredMetadata(); - - TypeKind returnKind = method.getReturnType().getKind(); - if ((returnKind == TypeKind.ERROR) && !processingOver) { - throw new PostponeToNextRoundException(); - } - - // handle @Bean annotation for @Factory class - JavaMethodElement javaMethodElement = elementFactory.newMethodElement(concreteClassElement, method, methodAndClassAnnotationMetadata); - if (isFactoryType && javaMethodElement.hasDeclaredStereotype(Bean.class.getName(), AnnotationUtil.SCOPE)) { - if (!modelUtils.overridingOrHidingMethod(method, concreteClass, true).isPresent()) { - visitBeanFactoryElement(method); - } - return null; - } - - boolean injected = methodAnnotationMetadata.hasDeclaredStereotype(AnnotationUtil.INJECT); - boolean postConstruct = methodAnnotationMetadata.hasDeclaredAnnotation(AnnotationUtil.POST_CONSTRUCT); - boolean preDestroy = methodAnnotationMetadata.hasDeclaredAnnotation(AnnotationUtil.PRE_DESTROY); - boolean isStatic = javaMethodElement.isStatic(); - - if (injected || postConstruct || preDestroy || methodAnnotationMetadata.hasDeclaredStereotype(ConfigurationInject.class)) { - if (isStatic) { - return null; - } - if (isDeclaredBean) { - visitAnnotatedMethod(javaMethodElement, method, o); - } else if (injected) { - // DEPRECATE: This behaviour should be deprecated in 2.0 - visitAnnotatedMethod(javaMethodElement, method, o); - } - return null; - } - - final boolean isPrivate = javaMethodElement.isPrivate(); - - if (javaMethodElement.isAbstract()) { - return null; - } - if (isStatic && !isExecutableDeclaredOnMethod(methodAnnotationMetadata)) { - // Require explicit @Executable on static methods - return null; - } - - boolean validatedMethod = isValidatedMethod(methodAnnotationMetadata, javaMethodElement); - - if (isDeclaredBean) { - if (validatedMethod && !isConfigurationPropertiesType) { - // Configurations are checked using the bean introspection, not requiring the interceptor - methodAnnotationMetadata = javaMethodElement.annotate(ANN_VALIDATED); - } - - boolean aroundDeclaredOnMethod = InterceptedMethodUtil.hasAroundStereotype(methodAnnotationMetadata); - if (aroundDeclaredOnMethod || InterceptedMethodUtil.hasAroundStereotype(methodAndClassAnnotationMetadata)) { - // AOP doesn't support private methods or static - if (!isPrivate && !isStatic) { - visitExecutableMethod(javaMethodElement, method, methodAnnotationMetadata); - } else if (aroundDeclaredOnMethod) { - if (isPrivate) { - error(method, "Method annotated as executable but is declared private. Change the method to be non-private in order for AOP advice to be applied."); - } else if (isStatic) { - error(method, "Static methods aren't supported for AOP"); - } - } - // Continue to check if the method is executable - } - if (isExecutableDeclaredOnType(method)) { - // @Executable annotated on the class - // only include own accessible methods or the ones annotated with @ReflectiveAccess - if (javaMethodElement.isAccessible()) { - visitExecutableMethod(javaMethodElement, method, methodAnnotationMetadata); - } - return null; - } else if (isExecutableDeclaredOnMethod(methodAnnotationMetadata)) { - // @Executable annotated on the method - // Throw error if it cannot be accessed without the reflection - if (!javaMethodElement.isAccessible()) { - error(method, "Method annotated as executable but is declared private. To invoke the method using reflection annotate it with @ReflectiveAccess"); - return null; - } - visitExecutableMethod(javaMethodElement, method, methodAnnotationMetadata); - return null; - } else if (isExecutableDeclaredOnParentType(method)) { - // @Executable annotated on the parent class - // Only include public methods - if (javaMethodElement.isPublic()) { - visitExecutableMethod(javaMethodElement, method, methodAnnotationMetadata); - } - return null; - } - } - - if (isConfigurationPropertiesType && !isPrivate && !isStatic) { - String methodName = javaMethodElement.getSimpleName(); - if (NameUtils.isSetterName(methodName) && javaMethodElement.getParameters().length == 1) { - visitConfigurationPropertySetter(method); - } else if (NameUtils.isGetterName(methodName)) { - BeanDefinitionVisitor writer = getOrCreateBeanDefinitionWriter(concreteClass, concreteClass.getQualifiedName()); - if (!writer.isValidated()) { - writer.setValidated(validatedMethod); - } - } - } else if (validatedMethod) { - if (isPrivate) { - error(method, "Method annotated with constraints but is declared private. Change the method to be non-private in order for AOP advice to be applied."); - return null; - } - visitExecutableMethod(javaMethodElement, method, methodAnnotationMetadata); - } - - return null; - } - - @NonNull - private AnnotationMetadata getMetadataHierarchy(AnnotationMetadata annotationMetadata) { - // NOTE: if annotation processor modified the method's annotation - // annotationUtils.getAnnotationMetadata(method) will return AnnotationMetadataHierarchy of both method+class metadata - if (annotationMetadata instanceof AnnotationMetadataHierarchy) { - return annotationMetadata; - } - return new AnnotationMetadataHierarchy(concreteClassMetadata, annotationMetadata); - } - - private boolean isExecutableDeclaredOnMethod(AnnotationMetadata annotationMetadata) { - return annotationMetadata.hasStereotype(Executable.class); - } - - private boolean isExecutableDeclaredOnType(ExecutableElement method) { - return isExecutableType && concreteClass.equals(method.getEnclosingElement()); - } - - private boolean isExecutableDeclaredOnParentType(ExecutableElement method) { - return isExecutableType && !concreteClass.equals(method.getEnclosingElement()); - } - - private boolean isValidatedMethod(AnnotationMetadata methodAnnotationMetadata, JavaMethodElement javaMethodElement) { - if (methodAnnotationMetadata.hasStereotype(ANN_VALIDATED)) { - return false; - } - return requiresValidation(javaMethodElement) || Arrays.stream(javaMethodElement.getParameters()).anyMatch(this::requiresValidation); - } - - private boolean requiresValidation(io.micronaut.inject.ast.Element p) { - return p.hasStereotype(ANN_CONSTRAINT) || p.hasStereotype(ANN_VALID); - } - - private void visitConfigurationPropertySetter(ExecutableElement method) { - BeanDefinitionVisitor writer = getOrCreateBeanDefinitionWriter(concreteClass, concreteClass.getQualifiedName()); - VariableElement parameter = method.getParameters().get(0); - - TypeElement declaringClass = modelUtils.classElementFor(method); - - if (declaringClass != null) { - AnnotationMetadata methodAnnotationMetadata = annotationUtils.getDeclaredAnnotationMetadata(method); - ClassElement javaClassElement = elementFactory.newClassElement(declaringClass, methodAnnotationMetadata); - MethodElement javaMethodElement = elementFactory.newMethodElement(javaClassElement, method, methodAnnotationMetadata); - ParameterElement parameterElement = javaMethodElement.getParameters()[0]; - - String propertyName = NameUtils.getPropertyNameForSetter(method.getSimpleName().toString()); - boolean isInterface = parameterElement.getType().isInterface(); - - if (methodAnnotationMetadata.hasStereotype(ConfigurationBuilder.class)) { - writer.visitConfigBuilderMethod( - parameterElement.getType(), - NameUtils.getterNameFor(propertyName), - methodAnnotationMetadata, - metadataBuilder, - isInterface - ); - try { - visitConfigurationBuilder(declaringClass, method, parameter.asType(), writer); - } finally { - writer.visitConfigBuilderEnd(); - } - } else { - if (shouldExclude(configurationMetadata, propertyName)) { - return; - } - String docComment = elementUtils.getDocComment(method); - PropertyMetadata propertyMetadata = metadataBuilder.visitProperty( - concreteClass, - declaringClass, - parameterElement.getGenericType().getCanonicalName(), - propertyName, - docComment, - null - ); - - addPropertyMetadata(javaMethodElement, propertyMetadata); - - writer.visitSetterValue(javaClassElement, - javaMethodElement, - javaMethodElement.isReflectionRequired(concreteClassElement), - true - ); - } - } - - } - - /** - * @param element The element - */ - void visitBeanFactoryElement(Element element) { - final TypeMirror producedType; - if (element instanceof ExecutableElement) { - producedType = ((ExecutableElement) element).getReturnType(); - } else { - producedType = element.asType(); - } - - final TypeKind producedTypeKind = producedType.getKind(); - final boolean isPrimitive = producedTypeKind.isPrimitive(); - final boolean isArray = producedTypeKind == TypeKind.ARRAY; - final Element producedElement = typeUtils.asElement(producedType); - TypeElement producedTypeElement = modelUtils.classElementFor(producedElement); - TypeElement factoryTypeElement = modelUtils.classElementFor(element); - AnnotationMetadata producedAnnotationMetadata; - String producedTypeName; - - if (factoryTypeElement == null) { - return; - } - - if (isPrimitive) { - PrimitiveType pt = (PrimitiveType) producedType; - producedTypeName = pt.toString(); - producedAnnotationMetadata = annotationUtils.newAnnotationBuilder().build(element); - } else { - - if (producedTypeElement == null) { - if (isArray) { - producedAnnotationMetadata = annotationUtils.newAnnotationBuilder().build(element); - producedTypeName = null; - } else { - error("Cannot produce bean for unsupported return type: " + producedType, element); - return; - } - } else { - producedAnnotationMetadata = annotationUtils.newAnnotationBuilder().buildForParent( - producedTypeElement, - element - ); - AnnotationMetadata producedTypeAnnotationMetadata = annotationUtils.getAnnotationMetadata(producedTypeElement); - AnnotationMetadata elementAnnotationMetadata = annotationUtils.getAnnotationMetadata(element); - if (elementAnnotationMetadata instanceof AnnotationMetadataHierarchy) { - // If the element has added annotation from a type visitor, annotation metadata is a hierarchy - // We only need actual element metadata - elementAnnotationMetadata = elementAnnotationMetadata.getDeclaredMetadata(); - } - cleanupScopeAndQualifierAnnotations((MutableAnnotationMetadata) producedAnnotationMetadata, producedTypeAnnotationMetadata, elementAnnotationMetadata); - producedTypeName = producedTypeElement.getQualifiedName().toString(); - } - - } - - ClassElement declaringClassElement = elementFactory.newClassElement( - factoryTypeElement, - concreteClassMetadata - ); - - io.micronaut.inject.ast.Element beanProducingElement; - ClassElement producedClassElement; - if (element instanceof ExecutableElement) { - final ExecutableElement executableElement = (ExecutableElement) element; - final JavaMethodElement methodElement = elementFactory.newMethodElement( - declaringClassElement, - executableElement, - producedAnnotationMetadata - ); - if (isFactoryType && annotationUtils.hasStereotype(concreteClass, AROUND_TYPE)) { - final JavaMethodElement aopMethod = elementFactory.newMethodElement( - declaringClassElement, - executableElement, - getMetadataHierarchy(producedAnnotationMetadata) - ); - visitExecutableMethod(aopMethod, executableElement, producedAnnotationMetadata); - } - - beanProducingElement = methodElement; - producedClassElement = methodElement.getGenericReturnType(); - } else { - final FieldElement fieldElement = elementFactory.newFieldElement( - declaringClassElement, - (VariableElement) element, - producedAnnotationMetadata - ); - beanProducingElement = fieldElement; - producedClassElement = fieldElement.getGenericField(); - } - - BeanDefinitionWriter beanMethodWriter = createFactoryBeanMethodWriterFor(element, producedAnnotationMetadata); - Map> allTypeArguments = producedClassElement.getAllTypeArguments(); - visitAnnotationMetadata(beanMethodWriter, producedAnnotationMetadata); - beanMethodWriter.visitTypeArguments(allTypeArguments); - - beanDefinitionWriters.put(new DynamicName(beanProducingElement.getDescription(false)), beanMethodWriter); - if (beanProducingElement instanceof MethodElement) { - beanMethodWriter.visitBeanFactoryMethod( - concreteClassElement, - (MethodElement) beanProducingElement - ); - } else { - beanMethodWriter.visitBeanFactoryField( - concreteClassElement, - (FieldElement) beanProducingElement - ); - } - - if (producedAnnotationMetadata.hasStereotype(AROUND_TYPE) && !modelUtils.isAbstract(concreteClass)) { - if (isPrimitive) { - error(element, "Cannot apply AOP advice to primitive beans"); - return; - } else if (isArray) { - error(element, "Cannot apply AOP advice to arrays"); - return; - } - - io.micronaut.core.annotation.AnnotationValue[] interceptorTypes = InterceptedMethodUtil.resolveInterceptorBinding(producedAnnotationMetadata, InterceptorKind.AROUND); - - if (producedClassElement.isFinal()) { - final Element nativeElement = (Element) producedClassElement.getNativeType(); - error(nativeElement, "Cannot apply AOP advice to final class. Class must be made non-final to support proxying: " + nativeElement); - return; - } - - MethodElement constructor = producedClassElement.getPrimaryConstructor().orElse(null); - if (!producedClassElement.isInterface() && constructor != null && constructor.getParameters().length > 0) { - final String proxyTargetMode = producedAnnotationMetadata.stringValue(AROUND_TYPE, "proxyTargetMode") - .orElseGet(() -> { - // temporary workaround until micronaut-test can be upgraded to 3.0 - if (producedAnnotationMetadata.hasAnnotation("io.micronaut.test.annotation.MockBean")) { - return "WARN"; - } else { - return "ERROR"; - } - }); - switch (proxyTargetMode) { - case "ALLOW": - allowProxyConstruction(constructor); - break; - case "WARN": - allowProxyConstruction(constructor); - warning(element, "The produced type of a @Factory method has constructor arguments and is proxied. This can lead to unexpected behaviour. See the javadoc for Around.ProxyTargetConstructorMode for more information: " + element); - break; - case "ERROR": - default: - error(element, "The produced type from a factory which has AOP proxy advice specified must define an accessible no arguments constructor. Proxying types with constructor arguments can lead to unexpected behaviour. See the javadoc for for Around.ProxyTargetConstructorMode for more information and possible solutions: " + element); - return; - } - - } - OptionalValues aroundSettings = producedAnnotationMetadata.getValues(AROUND_TYPE, Boolean.class); - Map finalSettings = new LinkedHashMap<>(); - for (CharSequence setting : aroundSettings) { - Optional entry = aroundSettings.get(setting); - entry.ifPresent(val -> - finalSettings.put(setting, val) - ); - } - finalSettings.put(Interceptor.PROXY_TARGET, true); - AopProxyWriter proxyWriter = resolveAopProxyWriter( - beanMethodWriter, - OptionalValues.of(Boolean.class, finalSettings), - true, - constructor, - interceptorTypes - ); - proxyWriter.visitTypeArguments(allTypeArguments); - - producedType.accept(new PublicMethodVisitor(javaVisitorContext) { - @Override - protected void accept(DeclaredType type, Element element, AopProxyWriter aopProxyWriter) { - ExecutableElement method = (ExecutableElement) element; - TypeElement owningType = modelUtils.classElementFor(method); - if (owningType != null) { - - ClassElement declaringClassElement = elementFactory.newClassElement( - owningType, - concreteClassMetadata - ); - AnnotationMetadata annotationMetadata; - // if the method is annotated we build metadata for the method - if (producedTypeName != null && annotationUtils.isAnnotated(producedTypeName, method)) { - annotationMetadata = annotationUtils.getAnnotationMetadata(element, method); - } else { - // otherwise we setup a reference to the parent metadata (essentially the annotations declared on the bean factory method) - annotationMetadata = new AnnotationMetadataReference( - beanMethodWriter.getBeanDefinitionName() + BeanDefinitionReferenceWriter.REF_SUFFIX, - producedAnnotationMetadata - ); - } - - MethodElement advisedMethodElement = elementFactory.newMethodElement( - declaringClassElement, - method, - annotationMetadata - ); - - aopProxyWriter.visitAroundMethod( - declaringClassElement, - advisedMethodElement - ); - } - } - }, proxyWriter); - } else if (producedAnnotationMetadata.hasStereotype(Executable.class)) { - if (isPrimitive) { - error("Using '@Executable' is not allowed on primitive type beans"); - return; - } - if (isArray) { - error("Using '@Executable' is not allowed on array type beans"); - return; - } - DeclaredType dt = (DeclaredType) producedType; - Map> finalBeanTypeArgumentsMirrors = genericUtils.buildGenericTypeArgumentElementInfo(dt.asElement(), dt, Collections.emptyMap()); - producedType.accept(new PublicMethodVisitor(javaVisitorContext) { - @Override - protected void accept(DeclaredType type, Element element, BeanDefinitionWriter beanWriter) { - ExecutableElement method = (ExecutableElement) element; - TypeElement owningType = modelUtils.classElementFor(method); - if (owningType == null) { - throw new IllegalStateException("Owning type cannot be null"); - } - AnnotationMetadata annotationMetadata = new AnnotationMetadataReference( - beanMethodWriter.getBeanDefinitionName() + BeanDefinitionReferenceWriter.REF_SUFFIX, - producedAnnotationMetadata - ); - - ClassElement declaringClassElement = elementFactory.newClassElement( - producedTypeElement, - concreteClassMetadata - ); - MethodElement executableMethod = elementFactory.newMethodElement( - declaringClassElement, - method, - annotationMetadata, - finalBeanTypeArgumentsMirrors - ); - - beanMethodWriter.visitExecutableMethod( - declaringClassElement, - executableMethod, - javaVisitorContext - ); - - } - }, beanMethodWriter); - } - - if (producedAnnotationMetadata.isPresent(Bean.class, "preDestroy")) { - if (isPrimitive) { - error("Using 'preDestroy' is not allowed on primitive type beans"); - return; - } - if (isArray) { - error("Using 'preDestroy' is not allowed on array type beans"); - return; - } - - Optional preDestroyMethod = producedAnnotationMetadata.getValue(Bean.class, "preDestroy", String.class); - preDestroyMethod - .ifPresent(destroyMethodName -> { - if (StringUtils.isNotEmpty(destroyMethodName)) { - TypeElement destroyMethodDeclaringClass = (TypeElement) producedElement; - ClassElement destroyMethodDeclaringElement = elementFactory.newClassElement(destroyMethodDeclaringClass, AnnotationMetadata.EMPTY_METADATA); - final Optional destroyMethod = destroyMethodDeclaringElement.getEnclosedElement( - ElementQuery.ALL_METHODS - .onlyAccessible(concreteClassElement) - .onlyInstance() - .named((name) -> name.equals(destroyMethodName)) - .filter((e) -> !e.hasParameters()) - ); - if (destroyMethod.isPresent()) { - MethodElement destroyMethodElement = destroyMethod.get(); - beanMethodWriter.visitPreDestroyMethod( - destroyMethodDeclaringElement, - destroyMethodElement, - false, - javaVisitorContext - ); - } else { - error(element, "@Bean method defines a preDestroy method that does not exist or is not public: " + destroyMethodName); - } - - } - }); - } - } - - private void allowProxyConstruction(MethodElement constructor) { - final ParameterElement[] parameters = constructor.getParameters(); - for (ParameterElement parameter : parameters) { - if (parameter.isPrimitive() && !parameter.isArray()) { - final String name = parameter.getType().getName(); - if ("boolean".equals(name)) { - parameter.annotate(Value.class, (builder) -> builder.value(false)); - } else { - parameter.annotate(Value.class, (builder) -> builder.value(0)); - } - } else { - // allow null - parameter.annotate(AnnotationUtil.NULLABLE); - parameter.removeAnnotation(AnnotationUtil.NON_NULL); - } - } - } - - /** - * @param javaMethodElement The method element - * @param method The {@link ExecutableElement} - * @param methodAnnotationMetadata The {@link AnnotationMetadata} - */ - void visitExecutableMethod(MethodElement javaMethodElement, ExecutableElement method, AnnotationMetadata methodAnnotationMetadata) { - TypeElement declaringClass = modelUtils.classElementFor(method); - if (declaringClass == null || modelUtils.isObjectClass(declaringClass)) { - return; - } - - boolean isOwningClass = declaringClass.getQualifiedName().equals(concreteClass.getQualifiedName()); - - if (isOwningClass && modelUtils.isAbstract(concreteClass) && !concreteClassMetadata.hasStereotype(INTRODUCTION_TYPE)) { - return; - } - - if (!isOwningClass && modelUtils.overridingOrHidingMethod(method, concreteClass, true).isPresent()) { - return; - } - - ClassElement declaringClassElement = elementFactory.newClassElement(declaringClass, concreteClassMetadata); - BeanDefinitionVisitor beanWriter = getOrCreateBeanDefinitionWriter(concreteClass, concreteClass.getQualifiedName()); - - // This method requires pre-processing. See Executable#processOnStartup() - boolean preprocess = methodAnnotationMetadata.isTrue(Executable.class, "processOnStartup"); - if (preprocess) { - beanWriter.setRequiresMethodProcessing(true); - } - - if (methodAnnotationMetadata.hasStereotype(Adapter.class)) { - visitAdaptedMethod(method, methodAnnotationMetadata); - } - - boolean executableMethodVisited = false; - - // shouldn't visit around advice on an introduction advice instance - if (!(beanWriter instanceof AopProxyWriter)) { - final boolean isConcrete = !concreteClassElement.isAbstract(); - final boolean isPublic = javaMethodElement.isPublic() || javaMethodElement.isPackagePrivate(); - if ((isAopProxyType && isPublic) || - (!isAopProxyType && methodAnnotationMetadata.hasStereotype(AROUND_TYPE)) || - (InterceptedMethodUtil.hasDeclaredAroundAdvice(methodAnnotationMetadata) && isConcrete)) { - - io.micronaut.core.annotation.AnnotationValue[] interceptorTypes = InterceptedMethodUtil.resolveInterceptorBinding(methodAnnotationMetadata, InterceptorKind.AROUND); - - OptionalValues settings = methodAnnotationMetadata.getValues(AROUND_TYPE, Boolean.class); - AopProxyWriter aopProxyWriter = resolveAopProxyWriter( - beanWriter, - settings, - false, - this.constructorElement, - interceptorTypes - ); - - aopProxyWriter.visitInterceptorBinding(interceptorTypes); - - if (javaMethodElement.isFinal()) { - if (InterceptedMethodUtil.hasDeclaredAroundAdvice(methodAnnotationMetadata)) { - error(method, "Method defines AOP advice but is declared final. Change the method to be non-final in order for AOP advice to be applied."); - } else { - if (isAopProxyType && isPublic && !declaringClass.equals(concreteClass)) { - addOriginatingElementIfNecessary(beanWriter, declaringClass); - beanWriter.visitExecutableMethod( - declaringClassElement, - javaMethodElement, - javaVisitorContext - ); - executableMethodVisited = true; - } else { - error(method, "Public method inherits AOP advice but is declared final. Either make the method non-public or apply AOP advice only to public methods declared on the class."); - } - } - } else { - addOriginatingElementIfNecessary(beanWriter, declaringClass); - aopProxyWriter.visitAroundMethod( - declaringClassElement, - javaMethodElement - ); - executableMethodVisited = true; - } - - } - } - - if (!executableMethodVisited) { - addOriginatingElementIfNecessary(beanWriter, declaringClass); - beanWriter.visitExecutableMethod( - declaringClassElement, - javaMethodElement, - javaVisitorContext - ); - } - - } - - private void visitAdaptedMethod(ExecutableElement sourceMethod, AnnotationMetadata methodAnnotationMetadata) { - if (methodAnnotationMetadata instanceof AnnotationMetadataHierarchy) { - methodAnnotationMetadata = methodAnnotationMetadata.getDeclaredMetadata(); - } - Optional targetType = methodAnnotationMetadata.getValue(Adapter.class, String.class).flatMap(s -> - Optional.ofNullable(elementUtils.getTypeElement(s)) - ); - - if (targetType.isPresent()) { - TypeElement typeElement = targetType.get(); - boolean isInterface = JavaModelUtils.isInterface(typeElement); - if (isInterface) { - ClassElement typeToImplementElement = elementFactory.newClassElement( - typeElement, - annotationUtils.getAnnotationMetadata(typeElement) - ); - DeclaredType typeToImplement = (DeclaredType) typeElement.asType(); - String packageName = concreteClassElement.getPackageName(); - String declaringClassSimpleName = concreteClassElement.getSimpleName(); - String beanClassName = generateAdaptedMethodClassName(sourceMethod, typeElement, declaringClassSimpleName); - - MethodElement sourceMethodElement = elementFactory.newMethodElement( - concreteClassElement, - sourceMethod, - methodAnnotationMetadata - ); - - AopProxyWriter aopProxyWriter = new AopProxyWriter( - packageName, - beanClassName, - true, - false, - sourceMethodElement, - new AnnotationMetadataHierarchy(concreteClassMetadata, methodAnnotationMetadata), - new ClassElement[]{typeToImplementElement}, - javaVisitorContext, - metadataBuilder, - null - ); - - aopProxyWriter.visitDefaultConstructor(methodAnnotationMetadata, javaVisitorContext); - beanDefinitionWriters.put(elementUtils.getName(packageName + '.' + beanClassName), aopProxyWriter); - Map typeVariables = typeToImplementElement.getTypeArguments(); - - AnnotationMetadata finalMethodAnnotationMetadata = methodAnnotationMetadata; - typeToImplement.accept(new PublicAbstractMethodVisitor(typeElement, javaVisitorContext) { - boolean first = true; - - @Override - protected void accept(DeclaredType type, Element element, AopProxyWriter aopProxyWriter) { - if (!first) { - error(sourceMethod, "Interface to adapt [" + typeToImplement + "] is not a SAM type. More than one abstract method declared."); - return; - } - first = false; - ExecutableElement targetMethod = (ExecutableElement) element; - MethodElement targetMethodElement = elementFactory.newMethodElement( - typeToImplementElement, - targetMethod, - annotationUtils.getAnnotationMetadata(targetMethod) - ); - - ParameterElement[] sourceParams = sourceMethodElement.getParameters(); - ParameterElement[] targetParams = targetMethodElement.getParameters(); - List targetParameters = targetMethod.getParameters(); - - int paramLen = targetParameters.size(); - if (paramLen == sourceParams.length) { - - if (sourceMethodElement.isSuspend()) { - error(sourceMethod, "Cannot adapt method [" + sourceMethod + "] to target method [" + targetMethod + "]. Kotlin suspend method not supported here."); - return; - } - - Map genericTypes = new LinkedHashMap<>(paramLen); - for (int i = 0; i < paramLen; i++) { - TypeMirror targetMirror = targetParameters.get(i).asType(); - ClassElement targetType = targetParams[i].getGenericType(); - ClassElement sourceType = sourceParams[i].getGenericType(); - - if (targetMirror.getKind() == TypeKind.TYPEVAR) { - TypeVariable tv = (TypeVariable) targetMirror; - String variableName = tv.toString(); - - if (typeVariables.containsKey(variableName)) { - genericTypes.put(variableName, sourceType); - } - } - - if (!sourceType.isAssignable(targetType.getName())) { - error(sourceMethod, "Cannot adapt method [" + sourceMethod + "] to target method [" + targetMethod + "]. Type [" + sourceType.getName() + "] is not a subtype of type [" + targetType.getName() + "] for argument at position " + i); - return; - } - } - - if (!genericTypes.isEmpty()) { - Map> typeData = Collections.singletonMap( - typeToImplementElement.getName(), - genericTypes - ); - - aopProxyWriter.visitTypeArguments( - typeData - ); - } - - ClassElement declaringClassElement = elementFactory.newClassElement( - typeElement, - finalMethodAnnotationMetadata - ); - - AnnotationClassValue[] adaptedArgumentTypes = new AnnotationClassValue[paramLen]; - for (int i = 0; i < adaptedArgumentTypes.length; i++) { - ParameterElement parameterElement = sourceParams[i]; - final ClassElement genericType = parameterElement.getGenericType(); - adaptedArgumentTypes[i] = new AnnotationClassValue<>(JavaModelUtils.getClassname(genericType)); - } - - MethodElement javaMethodElement = elementFactory.newMethodElement( - concreteClassElement, - targetMethod, - finalMethodAnnotationMetadata - ); - - javaMethodElement.annotate(Adapter.class, (builder) -> { - AnnotationClassValue acv = new AnnotationClassValue<>(concreteClassElement.getName()); - builder.member(Adapter.InternalAttributes.ADAPTED_BEAN, acv); - builder.member(Adapter.InternalAttributes.ADAPTED_METHOD, sourceMethodElement.getName()); - builder.member(Adapter.InternalAttributes.ADAPTED_ARGUMENT_TYPES, adaptedArgumentTypes); - String qualifier = concreteClassMetadata.stringValue(AnnotationUtil.NAMED).orElse(null); - if (StringUtils.isNotEmpty(qualifier)) { - builder.member(Adapter.InternalAttributes.ADAPTED_QUALIFIER, qualifier); - } - }); - - aopProxyWriter.visitAroundMethod( - declaringClassElement, - javaMethodElement - ); - - - } else { - error(sourceMethod, "Cannot adapt method [" + sourceMethod + "] to target method [" + targetMethod + "]. Argument lengths don't match."); - } - } - }, aopProxyWriter); - } - } - } - - private String generateAdaptedMethodClassName(ExecutableElement method, TypeElement typeElement, String declaringClassSimpleName) { - String rootName = declaringClassSimpleName + '$' + typeElement.getSimpleName().toString() + '$' + method.getSimpleName().toString(); - return rootName + adaptedMethodIndex.incrementAndGet(); - } - - private AopProxyWriter resolveAopProxyWriter(BeanDefinitionVisitor beanWriter, - OptionalValues aopSettings, - boolean isFactoryType, - MethodElement constructorElement, - io.micronaut.core.annotation.AnnotationValue... interceptorBinding) { - String beanName = beanWriter.getBeanDefinitionName(); - Name proxyKey = createProxyKey(beanName); - BeanDefinitionVisitor aopWriter = beanWriter instanceof AopProxyWriter ? beanWriter : beanDefinitionWriters.get(proxyKey); - - AopProxyWriter aopProxyWriter; - if (aopWriter == null) { - aopProxyWriter - = new AopProxyWriter( - (BeanDefinitionWriter) beanWriter, - aopSettings, - metadataBuilder, - javaVisitorContext, - interceptorBinding); - - if (constructorElement != null) { - aopProxyWriter.visitBeanDefinitionConstructor( - constructorElement, - constructorElement.isReflectionRequired(), - javaVisitorContext - ); - } else { - aopProxyWriter.visitDefaultConstructor(AnnotationMetadata.EMPTY_METADATA, javaVisitorContext); - } - - if (isFactoryType) { - aopProxyWriter - .visitSuperBeanDefinitionFactory(beanName); - } else { - aopProxyWriter - .visitSuperBeanDefinition(beanName); - } - aopWriter = aopProxyWriter; - beanDefinitionWriters.put( - proxyKey, - aopWriter - ); - } else { - aopProxyWriter = (AopProxyWriter) aopWriter; - } - return aopProxyWriter; - } - - /** - * @param javaMethodElement The java method element - * @param method The {@link ExecutableElement} - * @param o An object - */ - void visitAnnotatedMethod(MethodElement javaMethodElement, ExecutableElement method, Object o) { - ClassElement declaringClass = javaMethodElement.getDeclaringType(); - boolean isParent = !declaringClass.getName().equals(this.concreteClassElement.getName()); - ExecutableElement overridingMethod = modelUtils.overridingOrHidingMethod(method, this.concreteClass, false).orElse(method); - TypeElement overridingClass = modelUtils.classElementFor(overridingMethod); - boolean overridden = isParent && overridingClass != null && !overridingClass.getQualifiedName().toString().equals(declaringClass.getName()); - - boolean isPackagePrivate = javaMethodElement.isPackagePrivate(); - boolean isPrivate = javaMethodElement.isPrivate(); - if (overridden && !(isPrivate || isPackagePrivate)) { - // bail out if the method has been overridden, since it will have already been handled - return; - } - - String packageOfOverridingClass = elementUtils.getPackageOf(overridingMethod).getQualifiedName().toString(); - String packageOfDeclaringClass = declaringClass.getPackageName(); - boolean isPackagePrivateAndPackagesDiffer = overridden && isPackagePrivate && - !packageOfOverridingClass.equals(packageOfDeclaringClass); - boolean overriddenInjected = overridden && annotationUtils.getAnnotationMetadata(overridingMethod).hasDeclaredStereotype(AnnotationUtil.INJECT); - - if (isParent && isPackagePrivate && !isPackagePrivateAndPackagesDiffer && overriddenInjected) { - // bail out if the method has been overridden by another method annotated with @Inject - return; - } - if (isParent && overridden && !overriddenInjected && !isPackagePrivateAndPackagesDiffer && !isPrivate) { - // bail out if the overridden method is package private and in the same package - // and is not annotated with @Inject - return; - } - boolean lifecycleMethod = false; - if (javaMethodElement.hasDeclaredAnnotation(AnnotationUtil.POST_CONSTRUCT)) { - BeanDefinitionVisitor writer = getOrCreateBeanDefinitionWriter(concreteClass, concreteClass.getQualifiedName()); - addOriginatingElementIfNecessary(writer, declaringClass); - writer.visitPostConstructMethod( - declaringClass, - javaMethodElement, - javaMethodElement.isReflectionRequired(concreteClassElement), - javaVisitorContext - ); - lifecycleMethod = true; - } - if (javaMethodElement.hasDeclaredAnnotation(AnnotationUtil.PRE_DESTROY)) { - BeanDefinitionVisitor writer = getOrCreateBeanDefinitionWriter(concreteClass, concreteClass.getQualifiedName()); - addOriginatingElementIfNecessary(writer, declaringClass); - writer.visitPreDestroyMethod( - declaringClass, - javaMethodElement, - javaMethodElement.isReflectionRequired(concreteClassElement), - javaVisitorContext - ); - lifecycleMethod = true; - } - if (lifecycleMethod) { - return; - } - if (javaMethodElement.hasDeclaredStereotype(AnnotationUtil.INJECT) || - javaMethodElement.hasDeclaredStereotype(ConfigurationInject.class)) { - BeanDefinitionVisitor writer = getOrCreateBeanDefinitionWriter(concreteClass, concreteClass.getQualifiedName()); - addOriginatingElementIfNecessary(writer, declaringClass); - writer.visitMethodInjectionPoint( - declaringClass, - javaMethodElement, - javaMethodElement.isReflectionRequired(concreteClassElement), - javaVisitorContext - ); - } else { - error("Unexpected call to visitAnnotatedMethod(%s)", method); - } - } - - @Override - public Object visitVariable(VariableElement variable, Object o) { - // assuming just fields, visitExecutable should be handling params for method calls - if (variable.getKind() != FIELD) { - return null; - } - - AnnotationMetadata fieldAnnotationMetadata = annotationUtils.getAnnotationMetadata(variable); - if (isFactoryType && fieldAnnotationMetadata.hasDeclaredStereotype(Bean.class)) { - FieldElement javaFieldElement = elementFactory.newFieldElement(concreteClassElement, variable, fieldAnnotationMetadata); - // field factory for bean - if (!javaFieldElement.isAccessible(concreteClassElement)) { - error(variable, "Beans produced from fields cannot be private"); - } else { - visitBeanFactoryElement(variable); - } - return null; - } else if (fieldAnnotationMetadata.hasStereotype(ConfigurationBuilder.class)) { - visitConfigurationProperty(variable, fieldAnnotationMetadata); - return null; - } - - if (modelUtils.isStatic(variable) || modelUtils.isFinal(variable)) { - // static and final injection not allowed at this stage - return null; - } - - boolean isInjected = fieldAnnotationMetadata.hasStereotype(AnnotationUtil.INJECT) || (fieldAnnotationMetadata.hasDeclaredStereotype(AnnotationUtil.QUALIFIER) && !fieldAnnotationMetadata.hasDeclaredAnnotation(Bean.class)); - boolean isValue = (fieldAnnotationMetadata.hasStereotype(Value.class) || fieldAnnotationMetadata.hasStereotype(Property.class)); - - if (isInjected || isValue) { - BeanDefinitionVisitor writer = getOrCreateBeanDefinitionWriter(concreteClass, concreteClass.getQualifiedName()); - TypeElement declaringClass = modelUtils.classElementFor(variable); - - if (declaringClass == null) { - return null; - } - - ClassElement declaringClassElement = elementFactory.newClassElement(declaringClass, concreteClassMetadata); - FieldElement javaFieldElement = elementFactory.newFieldElement(concreteClassElement, variable, fieldAnnotationMetadata); - addOriginatingElementIfNecessary(writer, declaringClass); - - if (!writer.isValidated()) { - writer.setValidated(IS_CONSTRAINT.test(fieldAnnotationMetadata)); - } - - TypeMirror type = variable.asType(); - if ((type.getKind() == TypeKind.ERROR) && !processingOver) { - throw new PostponeToNextRoundException(); - } - - if (isValue) { - writer.visitFieldValue( - declaringClassElement, - javaFieldElement, - javaFieldElement.isReflectionRequired(concreteClassElement), - isConfigurationPropertiesType - ); - } else { - writer.visitFieldInjectionPoint( - declaringClassElement, - javaFieldElement, - javaFieldElement.isReflectionRequired(concreteClassElement) - ); - } - } else if (isConfigurationPropertiesType) { - visitConfigurationProperty(variable, fieldAnnotationMetadata); - } - return null; - } - - private void addOriginatingElementIfNecessary(BeanDefinitionVisitor writer, TypeElement declaringClass) { - if (!concreteClass.equals(declaringClass)) { - writer.addOriginatingElement(elementFactory.newClassElement(declaringClass, currentClassMetadata)); - } - } - - private void addOriginatingElementIfNecessary(BeanDefinitionVisitor writer, ClassElement declaringClass) { - if (!concreteClassElement.getName().equals(declaringClass.getName())) { - writer.addOriginatingElement(declaringClass); - } - } - - /** - * @param field The {@link VariableElement} - * @param fieldAnnotationMetadata The annotation metadata for the field - * @return Returns null after visiting the configuration properties - */ - public Object visitConfigurationProperty(VariableElement field, AnnotationMetadata fieldAnnotationMetadata) { - Optional setterMethod = modelUtils.findSetterMethodFor(field); - boolean isInjected = fieldAnnotationMetadata.hasStereotype(AnnotationUtil.INJECT); - boolean isValue = fieldAnnotationMetadata.hasStereotype(Value.class) || fieldAnnotationMetadata.hasStereotype(Property.class); - - boolean isMethodInjected = isInjected || (setterMethod.isPresent() && annotationUtils.hasStereotype(setterMethod.get(), AnnotationUtil.INJECT)); - if (!(isMethodInjected || isValue)) { - // visitVariable didn't handle it - BeanDefinitionVisitor writer = getOrCreateBeanDefinitionWriter(concreteClass, concreteClass.getQualifiedName()); - if (!writer.isValidated()) { - writer.setValidated(IS_CONSTRAINT.test(fieldAnnotationMetadata)); - } - - - TypeElement declaringClass = modelUtils.classElementFor(field); - - if (declaringClass == null) { - return null; - } - ClassElement declaringClassElement = elementFactory.newClassElement(declaringClass, concreteClassMetadata); - FieldElement javaFieldElement = elementFactory.newFieldElement(declaringClassElement, field, fieldAnnotationMetadata); - String fieldName = javaFieldElement.getName(); - - if (fieldAnnotationMetadata.hasStereotype(ConfigurationBuilder.class)) { - - boolean isInterface = javaFieldElement.getType().isInterface(); - if (!javaFieldElement.isReflectionRequired(concreteClassElement)) { - writer.visitConfigBuilderField(javaFieldElement.getType(), fieldName, fieldAnnotationMetadata, metadataBuilder, isInterface); - } else { - // Using the field would throw a IllegalAccessError, use the method instead - Optional getterMethod = modelUtils.findGetterMethodFor(field); - if (getterMethod.isPresent()) { - writer.visitConfigBuilderMethod(javaFieldElement.getType(), getterMethod.get().getSimpleName().toString(), fieldAnnotationMetadata, metadataBuilder, isInterface); - } else { - error(field, "ConfigurationBuilder applied to a non accessible (private or package-private/protected in a different package) field must have a corresponding non-private getter method."); - } - } - try { - visitConfigurationBuilder(declaringClass, field, field.asType(), writer); - } finally { - writer.visitConfigBuilderEnd(); - } - } else { - if (shouldExclude(configurationMetadata, fieldName)) { - return null; - } - if (setterMethod.isPresent()) { - ExecutableElement method = setterMethod.get(); - // Just visit the field metadata, the setter will be processed - String docComment = elementUtils.getDocComment(method); - metadataBuilder.visitProperty( - concreteClass, - declaringClass, - getPropertyMetadataTypeReference(field.asType()), - fieldName, - docComment, - null - ); - } else { - boolean isPrivate = javaFieldElement.isPrivate(); - - if (!isPrivate) { - String docComment = elementUtils.getDocComment(field); - PropertyMetadata propertyMetadata = metadataBuilder.visitProperty( - concreteClass, - declaringClass, - getPropertyMetadataTypeReference(field.asType()), - fieldName, - docComment, - null - ); - - addPropertyMetadata(javaFieldElement, propertyMetadata); - writer.visitFieldValue( - declaringClassElement, - javaFieldElement, - javaFieldElement.isReflectionRequired(concreteClassElement), - isConfigurationPropertiesType - ); - } - } - } - } - - return null; - } - - @Override - public Object visitTypeParameter(TypeParameterElement e, Object o) { - note("Visit param %s for %s", e.getSimpleName(), o); - return super.visitTypeParameter(e, o); - } - - @Override - public Object visitUnknown(Element e, Object o) { - if (!JavaModelUtils.isRecordOrRecordComponent(e)) { - note("Visit unknown %s for %s", e.getSimpleName(), o); - return super.visitUnknown(e, o); - } - return o; - } - - private void visitConfigurationBuilder(TypeElement declaringClass, Element builderElement, TypeMirror builderType, BeanDefinitionVisitor writer) { - AnnotationMetadata annotationMetadata = annotationUtils.getAnnotationMetadata(builderElement); - Boolean allowZeroArgs = annotationMetadata.getValue(ConfigurationBuilder.class, "allowZeroArgs", Boolean.class).orElse(false); - List prefixes = Arrays.asList(annotationMetadata.getValue(AccessorsStyle.class, "writePrefixes", String[].class).orElse(new String[]{"set"})); - String configurationPrefix = annotationMetadata.getValue(ConfigurationBuilder.class, String.class) - .map(v -> v + ".").orElse(""); - Set includes = annotationMetadata.getValue(ConfigurationBuilder.class, "includes", Set.class).orElse(Collections.emptySet()); - Set excludes = annotationMetadata.getValue(ConfigurationBuilder.class, "excludes", Set.class).orElse(Collections.emptySet()); - - PublicMethodVisitor visitor = new PublicMethodVisitor(javaVisitorContext) { - @Override - protected void accept(DeclaredType type, Element element, Object o) { - ExecutableElement method = (ExecutableElement) element; - List params = method.getParameters(); - String methodName = method.getSimpleName().toString(); - String prefix = getMethodPrefix(prefixes, methodName); - String propertyName = NameUtils.decapitalize(methodName.substring(prefix.length())); - if (shouldExclude(includes, excludes, propertyName)) { - return; - } - - int paramCount = params.size(); - MethodElement javaMethodElement = elementFactory.newMethodElement( - concreteClassElement, - method, - AnnotationMetadata.EMPTY_METADATA - ); - if (paramCount < 2) { - VariableElement paramType = paramCount == 1 ? params.get(0) : null; - - ClassElement parameterElement = null; - if (paramType != null) { - parameterElement = ((TypedElement) elementFactory.newParameterElement(concreteClassElement, paramType, AnnotationMetadata.EMPTY_METADATA)).getGenericType(); - } - PropertyMetadata metadata = metadataBuilder.visitProperty( - concreteClass, - declaringClass, - parameterElement != null ? parameterElement.getName() : null, - configurationPrefix + propertyName, - null, - null - ); - - - writer.visitConfigBuilderMethod( - prefix, - javaMethodElement.getReturnType(), - methodName, - parameterElement, - parameterElement != null ? parameterElement.getTypeArguments() : null, - metadata.getPath() - ); - } else if (paramCount == 2) { - // check the params are a long and a TimeUnit - VariableElement first = params.get(0); - VariableElement second = params.get(1); - TypeMirror tu = elementUtils.getTypeElement(TimeUnit.class.getName()).asType(); - TypeMirror typeMirror = first.asType(); - if (typeMirror.toString().equals("long") && typeUtils.isAssignable(second.asType(), tu)) { - - PropertyMetadata metadata = metadataBuilder.visitProperty( - concreteClass, - declaringClass, - Duration.class.getName(), - configurationPrefix + propertyName, - null, - null - ); - - writer.visitConfigBuilderDurationMethod( - prefix, - javaMethodElement.getReturnType(), - methodName, - metadata.getPath() - ); - } - } - } - - @SuppressWarnings("MagicNumber") - @Override - protected boolean isAcceptable(Element element) { - // ignore deprecated methods - if (annotationUtils.hasStereotype(element, Deprecated.class)) { - return false; - } - Set modifiers = element.getModifiers(); - if (element.getKind() == ElementKind.METHOD) { - ExecutableElement method = (ExecutableElement) element; - int paramCount = method.getParameters().size(); - return modifiers.contains(Modifier.PUBLIC) && ((paramCount > 0 && paramCount < 3) || allowZeroArgs && paramCount == 0) && isPrefixedWith(method, prefixes); - } else { - return false; - } - } - - private boolean isPrefixedWith(Element enclosedElement, List prefixes) { - String name = enclosedElement.getSimpleName().toString(); - for (String prefix : prefixes) { - if (name.startsWith(prefix)) { - return true; - } - } - return false; - } - - private String getMethodPrefix(List prefixes, String methodName) { - for (String prefix : prefixes) { - if (methodName.startsWith(prefix)) { - return prefix; - } - } - return methodName; - } - }; - - builderType.accept(visitor, null); - } - - private BeanDefinitionWriter createBeanDefinitionWriterFor(TypeElement typeElement) { - ClassElement classElement; - AnnotationMetadata annotationMetadata; - if (typeElement == concreteClass) { - classElement = concreteClassElement; - annotationMetadata = classElement.getAnnotationMetadata(); - } else { - annotationMetadata = annotationUtils.getAnnotationMetadata(typeElement); - classElement = elementFactory.newClassElement(typeElement, annotationMetadata); - } - if (configurationMetadata != null) { - // unfortunate we have to do this - String existingPrefix = annotationMetadata.stringValue( - ConfigurationReader.class, - "prefix") - .orElse(""); - - String computedPrefix = StringUtils.isNotEmpty(existingPrefix) ? existingPrefix + "." + configurationMetadata.getName() : configurationMetadata.getName(); - classElement.annotate(ConfigurationReader.class, (builder) -> - builder.member("prefix", computedPrefix) - ); - } - - - BeanDefinitionWriter beanDefinitionWriter = new BeanDefinitionWriter(classElement, metadataBuilder, javaVisitorContext); - beanDefinitionWriter.visitTypeArguments(classElement.getAllTypeArguments()); - return beanDefinitionWriter; - } - - private DynamicName createProxyKey(String beanName) { - return new DynamicName(beanName + "$Proxy"); - } - - @SuppressWarnings("MagicNumber") - private AopProxyWriter createIntroductionAdviceWriter(ClassElement typeElement) { - AnnotationMetadata annotationMetadata = typeElement.getAnnotationMetadata(); - - String packageName = typeElement.getPackageName(); - String beanClassName = typeElement.getSimpleName(); - io.micronaut.core.annotation.AnnotationValue[] aroundInterceptors = - InterceptedMethodUtil.resolveInterceptorBinding(annotationMetadata, InterceptorKind.AROUND); - io.micronaut.core.annotation.AnnotationValue[] introductionInterceptors = InterceptedMethodUtil.resolveInterceptorBinding(annotationMetadata, InterceptorKind.INTRODUCTION); - - ClassElement[] interfaceTypes = Arrays.stream(annotationMetadata.getValue(Introduction.class, "interfaces", String[].class).orElse(new String[0])) - .map(ClassElement::of).toArray(ClassElement[]::new); - - io.micronaut.core.annotation.AnnotationValue[] interceptorTypes = ArrayUtils.concat(aroundInterceptors, introductionInterceptors); - boolean isInterface = typeElement.isInterface(); - AopProxyWriter aopProxyWriter = new AopProxyWriter( - packageName, - beanClassName, - isInterface, - typeElement, - annotationMetadata, - interfaceTypes, - javaVisitorContext, - metadataBuilder, - configurationMetadata, - interceptorTypes); - - aopProxyWriter.visitTypeArguments(typeElement.getAllTypeArguments()); - - Set additionalInterfaces = Arrays.stream(interfaceTypes) - .map(ce -> elementUtils.getTypeElement(ce.getName())) - .filter(Objects::nonNull).collect(Collectors.toCollection(LinkedHashSet::new)); - - if (ArrayUtils.isNotEmpty(interfaceTypes)) { - TypeElement te = (TypeElement) typeElement.getNativeType(); - List annotationMirrors = te.getAnnotationMirrors(); - populateIntroductionInterfaces(annotationMirrors, additionalInterfaces); - if (!additionalInterfaces.isEmpty()) { - for (TypeElement additionalInterface : additionalInterfaces) { - visitIntroductionAdviceInterface(additionalInterface, annotationMetadata, aopProxyWriter); - } - } - } - return aopProxyWriter; - } - - private void populateIntroductionInterfaces(List annotationMirrors, Set additionalInterfaces) { - for (AnnotationMirror annotationMirror : annotationMirrors) { - DeclaredType annotationType = annotationMirror.getAnnotationType(); - if (annotationType.toString().equals(Introduction.class.getName())) { - Map values = annotationMirror.getElementValues(); - for (Map.Entry entry : values.entrySet()) { - ExecutableElement key = entry.getKey(); - if (key.toString().equalsIgnoreCase("interfaces()")) { - Object value = entry.getValue().getValue(); - if (value instanceof List) { - for (Object v : ((List) value)) { - if (v instanceof AnnotationValue) { - tryAddAnnotationValue(additionalInterfaces, (AnnotationValue) v); - } - } - } else if (value instanceof AnnotationValue) { - tryAddAnnotationValue(additionalInterfaces, (AnnotationValue) value); - } - } - } - } else { - Element element = annotationType.asElement(); - if (annotationUtils.hasStereotype(element, Introduction.class)) { - populateIntroductionInterfaces(element.getAnnotationMirrors(), additionalInterfaces); - } - } - } - } - - private void tryAddAnnotationValue(Set additionalInterfaces, AnnotationValue v) { - Object v2 = v.getValue(); - if (v2 instanceof TypeMirror) { - TypeMirror tm = (TypeMirror) v2; - if (tm.getKind() == TypeKind.DECLARED) { - DeclaredType dt = (DeclaredType) tm; - additionalInterfaces.add((TypeElement) dt.asElement()); - } - } - } - - private BeanDefinitionWriter createFactoryBeanMethodWriterFor(Element method, - AnnotationMetadata elementAnnotationMetadata) { - - MutableAnnotationMetadata factoryClassAnnotationMetadata = ((MutableAnnotationMetadata) concreteClassMetadata).clone(); - - AnnotationMetadata producerAnnotationMetadata = new AnnotationMetadataHierarchy( - factoryClassAnnotationMetadata, - elementAnnotationMetadata - ); - - if (concreteClassMetadata.hasStereotype(AnnotationUtil.QUALIFIER)) { - // Don't propagate any qualifiers to the factories - for (String scope : concreteClassMetadata.getAnnotationNamesByStereotype(AnnotationUtil.QUALIFIER)) { - if (!elementAnnotationMetadata.hasStereotype(scope)) { - factoryClassAnnotationMetadata.removeAnnotation(scope); - } - } - } - - io.micronaut.inject.ast.Element factoryElement; - if (method instanceof ExecutableElement) { - factoryElement = elementFactory.newMethodElement( - concreteClassElement, - (ExecutableElement) method, - producerAnnotationMetadata - ); - - } else { - factoryElement = elementFactory.newFieldElement( - concreteClassElement, - (VariableElement) method, - producerAnnotationMetadata - ); - } - return new BeanDefinitionWriter( - factoryElement, - OriginatingElements.of(factoryElement), - metadataBuilder, - javaVisitorContext, - factoryMethodIndex.getAndIncrement() - ); - } - - private boolean shouldExclude(Set includes, Set excludes, String propertyName) { - if (!includes.isEmpty() && !includes.contains(propertyName)) { - return true; - } - return !excludes.isEmpty() && excludes.contains(propertyName); - } - - private boolean shouldExclude(ConfigurationMetadata configurationMetadata, String propertyName) { - return shouldExclude(configurationMetadata.getIncludes(), configurationMetadata.getExcludes(), propertyName); - } - - private void cleanupScopeAndQualifierAnnotations(MutableAnnotationMetadata producedAnnotationMetadata, - AnnotationMetadata producedTypeAnnotationMetadata, - AnnotationMetadata producingElementAnnotationMetadata) { - // If the producing element defines a scope don't inherit it from the type - if (producingElementAnnotationMetadata.hasStereotype(AnnotationUtil.SCOPE) || producingElementAnnotationMetadata.hasStereotype(AnnotationUtil.QUALIFIER)) { - // The producing element is declaring the scope then we should remove the scope defined by the type - for (String scope : producedTypeAnnotationMetadata.getAnnotationNamesByStereotype(AnnotationUtil.SCOPE)) { - if (!producingElementAnnotationMetadata.hasStereotype(scope)) { - producedAnnotationMetadata.removeAnnotation(scope); - } - } - // Remove any qualifier coming from the type - for (String qualifier : producedTypeAnnotationMetadata.getAnnotationNamesByStereotype(AnnotationUtil.QUALIFIER)) { - if (!producingElementAnnotationMetadata.hasStereotype(qualifier)) { - producedAnnotationMetadata.removeAnnotation(qualifier); - } - } - } - } - } - - /** - * A dynamic name. - */ - static class DynamicName implements Name { - private final CharSequence name; - - /** - * @param name The name - */ - public DynamicName(CharSequence name) { - this.name = name; - } - - @Override - public boolean contentEquals(CharSequence cs) { - return name.equals(cs); - } - - @Override - public int length() { - return name.length(); - } - - @Override - public char charAt(int index) { - return name.charAt(index); - } - - @Override - public CharSequence subSequence(int start, int end) { - return name.subSequence(start, end); - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - - DynamicName that = (DynamicName) o; - - return name.equals(that.name); - } - - @Override - public int hashCode() { - return name.hashCode(); - } - } - } diff --git a/inject-java/src/main/java/io/micronaut/annotation/processing/ConfigurationMetadataProcessor.java b/inject-java/src/main/java/io/micronaut/annotation/processing/ConfigurationMetadataProcessor.java index d2797e73cc8..37a4fbaf395 100644 --- a/inject-java/src/main/java/io/micronaut/annotation/processing/ConfigurationMetadataProcessor.java +++ b/inject-java/src/main/java/io/micronaut/annotation/processing/ConfigurationMetadataProcessor.java @@ -66,27 +66,25 @@ public boolean process(Set annotations, RoundEnvironment } private void writeConfigurationMetadata() { - ConfigurationMetadataBuilder.getConfigurationMetadataBuilder().ifPresent(builder -> { - try { - if (builder.hasMetadata()) { - ServiceLoader writers = ServiceLoader.load(ConfigurationMetadataWriter.class, getClass().getClassLoader()); - - try { - for (ConfigurationMetadataWriter writer : writers) { - writeConfigurationMetadata(builder, writer); - } - } catch (ServiceConfigurationError e) { - warning("Unable to load ConfigurationMetadataWriter due to : %s", e.getMessage()); + try { + ConfigurationMetadataBuilder builder = ConfigurationMetadataBuilder.INSTANCE; + if (builder.hasMetadata()) { + ServiceLoader writers = ServiceLoader.load(ConfigurationMetadataWriter.class, getClass().getClassLoader()); + try { + for (ConfigurationMetadataWriter writer : writers) { + writeConfigurationMetadata(builder, writer); } + } catch (ServiceConfigurationError e) { + warning("Unable to load ConfigurationMetadataWriter due to : %s", e.getMessage()); } - } finally { - ConfigurationMetadataBuilder.setConfigurationMetadataBuilder(null); } - }); + } finally { + ConfigurationMetadataBuilder.INSTANCE = new ConfigurationMetadataBuilder(); + } } - private void writeConfigurationMetadata(ConfigurationMetadataBuilder metadataBuilder, ConfigurationMetadataWriter writer) { + private void writeConfigurationMetadata(ConfigurationMetadataBuilder metadataBuilder, ConfigurationMetadataWriter writer) { try { writer.write(metadataBuilder, classWriterOutputVisitor); } catch (IOException e) { diff --git a/inject-java/src/main/java/io/micronaut/annotation/processing/JavaAnnotationMetadataBuilder.java b/inject-java/src/main/java/io/micronaut/annotation/processing/JavaAnnotationMetadataBuilder.java index 45fc9462b11..361ef5de9d1 100644 --- a/inject-java/src/main/java/io/micronaut/annotation/processing/JavaAnnotationMetadataBuilder.java +++ b/inject-java/src/main/java/io/micronaut/annotation/processing/JavaAnnotationMetadataBuilder.java @@ -17,6 +17,8 @@ import io.micronaut.core.annotation.AnnotationClassValue; import io.micronaut.core.annotation.AnnotationUtil; +import io.micronaut.core.annotation.NonNull; +import io.micronaut.core.annotation.Nullable; import io.micronaut.core.reflect.ClassUtils; import io.micronaut.core.util.ArrayUtils; import io.micronaut.core.util.CollectionUtils; @@ -28,10 +30,14 @@ import io.micronaut.inject.processing.JavaModelUtils; import io.micronaut.inject.visitor.VisitorContext; -import io.micronaut.core.annotation.NonNull; -import io.micronaut.core.annotation.Nullable; import javax.annotation.processing.Messager; -import javax.lang.model.element.*; +import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.element.AnnotationValue; +import javax.lang.model.element.Element; +import javax.lang.model.element.ElementKind; +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.element.TypeElement; +import javax.lang.model.element.VariableElement; import javax.lang.model.type.ArrayType; import javax.lang.model.type.DeclaredType; import javax.lang.model.type.TypeMirror; @@ -46,7 +52,14 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.reflect.Array; -import java.util.*; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; import java.util.stream.Collectors; /** @@ -67,16 +80,16 @@ public class JavaAnnotationMetadataBuilder extends AbstractAnnotationMetadataBui /** * Default constructor. * - * @param elements The elementUtils - * @param messager The messager + * @param elements The elementUtils + * @param messager The messager * @param annotationUtils The annotation utils - * @param modelUtils The model utils + * @param modelUtils The model utils */ public JavaAnnotationMetadataBuilder( - Elements elements, - Messager messager, - AnnotationUtils annotationUtils, - ModelUtils modelUtils) { + Elements elements, + Messager messager, + AnnotationUtils annotationUtils, + ModelUtils modelUtils) { this.elementUtils = elements; this.messager = messager; this.annotationUtils = annotationUtils; @@ -119,7 +132,7 @@ protected String getRepeatableNameForType(Element annotationType) { String name = mirror.getAnnotationType().toString(); if (Repeatable.class.getName().equals(name)) { Map elementValues = mirror.getElementValues(); - for (Map.Entry entry: elementValues.entrySet()) { + for (Map.Entry entry : elementValues.entrySet()) { if (entry.getKey().getSimpleName().toString().equals("value")) { javax.lang.model.element.AnnotationValue av = entry.getValue(); Object value = av.getValue(); @@ -153,7 +166,7 @@ protected RetentionPolicy getRetentionPolicy(@NonNull Element annotation) { if (Retention.class.getName().equals(annotationTypeName)) { final Iterator i = annotationMirror - .getElementValues().values().iterator(); + .getElementValues().values().iterator(); if (i.hasNext()) { final AnnotationValue av = i.next(); final String v = av.getValue().toString(); @@ -211,8 +224,8 @@ protected boolean hasSimpleAnnotation(Element element, String simpleName) { final List mirrors = element.getAnnotationMirrors(); for (AnnotationMirror mirror : mirrors) { final String s = mirror.getAnnotationType() - .asElement() - .getSimpleName().toString(); + .asElement() + .getSimpleName().toString(); if (s.equalsIgnoreCase(simpleName)) { return true; } @@ -221,21 +234,6 @@ protected boolean hasSimpleAnnotation(Element element, String simpleName) { return false; } - @Override - protected boolean isMethodOrClassElement(Element element) { - return element instanceof TypeElement || element instanceof ExecutableElement; - } - - @NonNull - @Override - protected String getDeclaringType(@NonNull Element element) { - TypeElement typeElement = modelUtils.classElementFor(element); - if (typeElement != null) { - return typeElement.getQualifiedName().toString(); - } - return element.getSimpleName().toString(); - } - @Override protected Element getTypeForAnnotation(AnnotationMirror annotationMirror) { return annotationMirror.getAnnotationType().asElement(); @@ -246,15 +244,15 @@ protected List getAnnotationsForType(Element element List annotationMirrors = new ArrayList<>(element.getAnnotationMirrors()); annotationMirrors.removeIf(mirror -> getAnnotationTypeName(mirror).equals(AnnotationUtil.KOTLIN_METADATA)); List expanded = new ArrayList<>(annotationMirrors.size()); - for (AnnotationMirror annotation: annotationMirrors) { + for (AnnotationMirror annotation : annotationMirrors) { boolean repeatable = false; boolean hasOtherMembers = false; - for (Map.Entry entry: annotation.getElementValues().entrySet()) { + for (Map.Entry entry : annotation.getElementValues().entrySet()) { if (entry.getKey().getSimpleName().toString().equals("value")) { Object value = entry.getValue().getValue(); if (value instanceof List) { String parentAnnotationName = getAnnotationTypeName(annotation); - for (Object val: (List) value) { + for (Object val : (List) value) { if (val instanceof AnnotationMirror) { String name = getRepeatableName((AnnotationMirror) val); if (name != null && name.equals(parentAnnotationName)) { @@ -323,7 +321,7 @@ protected List buildHierarchy(Element element, boolean inheritTypeAnnot if (enclosingElement instanceof ExecutableElement) { ExecutableElement executableElement = (ExecutableElement) enclosingElement; int variableIdx = executableElement.getParameters().indexOf(variable); - for (ExecutableElement overridden: findOverriddenMethods(executableElement)) { + for (ExecutableElement overridden : findOverriddenMethods(executableElement)) { hierarchy.add(overridden.getParameters().get(variableIdx)); } } @@ -376,11 +374,11 @@ protected OptionalValues getAnnotationValues(Element originatingElement, Elem @Override protected void readAnnotationRawValues( - Element originatingElement, - String annotationName, Element member, - String memberName, - Object annotationValue, - Map annotationValues) { + Element originatingElement, + String annotationName, Element member, + String memberName, + Object annotationValue, + Map annotationValues) { if (memberName != null && annotationValue instanceof javax.lang.model.element.AnnotationValue && !annotationValues.containsKey(memberName)) { final MetadataAnnotationValueVisitor resolver = new MetadataAnnotationValueVisitor(originatingElement); ((javax.lang.model.element.AnnotationValue) annotationValue).accept(resolver, this); @@ -407,9 +405,9 @@ private boolean isValidationRequired(List annotation final Element element = getAnnotationMirror(annotationName).orElse(null); if (element != null) { final List childMirrors = element.getAnnotationMirrors() - .stream() - .filter(ann -> !getAnnotationTypeName(ann).equals(annotationName)) - .collect(Collectors.toList()); + .stream() + .filter(ann -> !getAnnotationTypeName(ann).equals(annotationName)) + .collect(Collectors.toList()); if (isValidationRequired(childMirrors)) { return true; } @@ -447,16 +445,16 @@ protected Object readAnnotationValue(Element originatingElement, Element member, TypeElement annotationElement = (TypeElement) element; final List allMembers = elementUtils.getAllMembers(annotationElement); allMembers - .stream() - .filter(member -> member.getEnclosingElement().equals(annotationElement)) - .filter(ExecutableElement.class::isInstance) - .map(ExecutableElement.class::cast) - .filter(this::isValidDefaultValue) - .forEach(executableElement -> { - final AnnotationValue defaultValue = executableElement.getDefaultValue(); - defaultValues.put(executableElement, defaultValue); - } - ); + .stream() + .filter(member -> member.getEnclosingElement().equals(annotationElement)) + .filter(ExecutableElement.class::isInstance) + .map(ExecutableElement.class::cast) + .filter(this::isValidDefaultValue) + .forEach(executableElement -> { + final AnnotationValue defaultValue = executableElement.getDefaultValue(); + defaultValues.put(executableElement, defaultValue); + } + ); } return defaultValues; } @@ -502,7 +500,7 @@ private void populateTypeHierarchy(Element element, List hierarchy) { populateTypeHierarchy(e, hierarchy); } } - } else { + } else { while (JavaModelUtils.resolveKind(element, ElementKind.CLASS).isPresent()) { TypeElement typeElement = (TypeElement) element; @@ -554,11 +552,11 @@ private List findOverriddenMethods(ExecutableElement sourceMe } private void addOverriddenMethodIfNecessary(ExecutableElement executableElement, - List overridden, - TypeElement declaringElement, - TypeElement supertype) { + List overridden, + TypeElement declaringElement, + TypeElement supertype) { final List possibleMethods = - ElementFilter.methodsIn(supertype.getEnclosedElements()); + ElementFilter.methodsIn(supertype.getEnclosedElements()); for (ExecutableElement possibleMethod : possibleMethods) { if (elementUtils.overrides(executableElement, possibleMethod, declaringElement)) { overridden.add(possibleMethod); @@ -580,7 +578,7 @@ private TypeElement toTypeElement(TypeMirror mirror, Types typeUtils) { * Checks if a method has an annotation. * * @param element The method - * @param ann The annotation to look for + * @param ann The annotation to look for * @return Whether if the method has the annotation */ @Override @@ -592,7 +590,7 @@ public boolean hasAnnotation(Element element, Class ann) { * Checks if a method has an annotation. * * @param element The method - * @param ann The annotation to look for + * @param ann The annotation to look for * @return Whether if the method has the annotation */ @Override diff --git a/inject-java/src/main/java/io/micronaut/annotation/processing/JavaConfigurationMetadataBuilder.java b/inject-java/src/main/java/io/micronaut/annotation/processing/JavaConfigurationMetadataBuilder.java deleted file mode 100644 index 39f51bf52ef..00000000000 --- a/inject-java/src/main/java/io/micronaut/annotation/processing/JavaConfigurationMetadataBuilder.java +++ /dev/null @@ -1,267 +0,0 @@ -/* - * Copyright 2017-2020 original authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.micronaut.annotation.processing; - -import io.micronaut.core.annotation.NonNull; -import io.micronaut.annotation.processing.visitor.JavaClassElement; -import io.micronaut.context.annotation.ConfigurationReader; -import io.micronaut.context.annotation.EachProperty; -import io.micronaut.core.annotation.AnnotationMetadata; -import io.micronaut.core.util.StringUtils; -import io.micronaut.inject.ast.ClassElement; -import io.micronaut.inject.configuration.ConfigurationMetadataBuilder; - -import javax.lang.model.element.Element; -import javax.lang.model.element.ElementKind; -import javax.lang.model.element.NestingKind; -import javax.lang.model.element.TypeElement; -import javax.lang.model.type.DeclaredType; -import javax.lang.model.type.TypeMirror; -import javax.lang.model.util.Elements; -import javax.lang.model.util.Types; -import java.util.LinkedHashMap; -import java.util.Map; -import java.util.Optional; -import java.util.function.Function; - -/** - * Implementation of {@link ConfigurationMetadataBuilder} for Java. - * - * @author Graeme Rocher - * @see ConfigurationMetadataBuilder - * @since 1.0 - */ -public class JavaConfigurationMetadataBuilder extends ConfigurationMetadataBuilder { - - private final AnnotationUtils annotationUtils; - private final ModelUtils modelUtils; - private final Elements elements; - private final Map originatingElements = new LinkedHashMap<>(); - - /** - * @param elements The {@link Elements} - * @param types The {@link Types} - * @param annotationUtils The annotation utils - */ - public JavaConfigurationMetadataBuilder(Elements elements, Types types, AnnotationUtils annotationUtils) { - this.elements = elements; - this.annotationUtils = annotationUtils; - this.modelUtils = new ModelUtils(elements, types); - // ensure initialization - final TypeElement crte = elements.getTypeElement(ConfigurationReader.class.getName()); - if (crte != null) { - getAnnotationMetadata(crte); - } - final TypeElement epte = elements.getTypeElement(EachProperty.class.getName()); - if (epte != null) { - getAnnotationMetadata(epte); - } - } - - /** - * @return The {@link Elements} - */ - public Elements getElements() { - return elements; - } - - @NonNull - @Override - public io.micronaut.inject.ast.Element[] getOriginatingElements() { - return originatingElements.values().toArray(io.micronaut.inject.ast.Element.EMPTY_ELEMENT_ARRAY); - } - - @Override - protected String buildPropertyPath(TypeElement owningType, TypeElement declaringType, String propertyName) { - addOriginatingElements(owningType); - String value = buildTypePath(owningType, declaringType); - return value + '.' + propertyName; - } - - @Override - protected String buildTypePath(TypeElement owningType, TypeElement declaringType, AnnotationMetadata annotationMetadata) { - addOriginatingElements(owningType, declaringType); - String initialPath = calculateInitialPath(owningType, annotationMetadata); - StringBuilder path = new StringBuilder(initialPath); - - prependSuperclasses(declaringType, path); - if (owningType.getNestingKind() == NestingKind.MEMBER) { - // we have an inner class, so prepend inner class - Element enclosingElement = owningType.getEnclosingElement(); - if (enclosingElement instanceof TypeElement) { - TypeElement enclosingType = (TypeElement) enclosingElement; - while (true) { - AnnotationMetadata enclosingTypeMetadata = getAnnotationMetadata(enclosingType); - Optional parentConfig = enclosingTypeMetadata.getValue(ConfigurationReader.class, String.class); - if (parentConfig.isPresent()) { - String parentPath = pathEvaluationFunctionForMetadata(enclosingTypeMetadata).apply(parentConfig.get()); - if (StringUtils.isNotEmpty(parentPath)) { - path.insert(0, parentPath + '.'); - } - prependSuperclasses(enclosingType, path); - if (enclosingType.getNestingKind() == NestingKind.MEMBER) { - Element el = enclosingType.getEnclosingElement(); - if (el instanceof TypeElement) { - enclosingType = (TypeElement) el; - } else { - break; - } - } else { - break; - } - } else { - String parentPath = pathEvaluationFunctionForMetadata(enclosingTypeMetadata).apply(""); - if (StringUtils.isNotEmpty(parentPath)) { - path.insert(0, parentPath + '.'); - } - prependSuperclasses(enclosingType, path); - if (enclosingType.getNestingKind() == NestingKind.MEMBER) { - Element el = enclosingType.getEnclosingElement(); - if (el instanceof TypeElement) { - enclosingType = (TypeElement) el; - } else { - break; - } - } else { - break; - } - } - } - } - } - return path.toString(); - } - - @Override - protected String buildTypePath(TypeElement owningType, TypeElement declaringType) { - addOriginatingElements(owningType, declaringType); - AnnotationMetadata annotationMetadata = getAnnotationMetadata(declaringType); - return buildTypePath(owningType, declaringType, annotationMetadata); - } - - private void addOriginatingElements(TypeElement... types) { - for (TypeElement type : types) { - String className = type.getQualifiedName().toString(); - if (!originatingElements.containsKey(className)) { - originatingElements.put(className, new JavaClassElement( - type, - AnnotationMetadata.EMPTY_METADATA, - null - )); - } - } - } - - private String calculateInitialPath(TypeElement owningType, AnnotationMetadata annotationMetadata) { - - Function evaluatePathFunction = pathEvaluationFunctionForMetadata(annotationMetadata); - return annotationMetadata.getValue(ConfigurationReader.class, String.class) - .map(evaluatePathFunction) - .orElseGet(() -> { - AnnotationMetadata ownerMetadata = getAnnotationMetadata(owningType); - return ownerMetadata - .getValue(ConfigurationReader.class, String.class) - .map(pathEvaluationFunctionForMetadata(ownerMetadata)) - .orElseGet(() -> - pathEvaluationFunctionForMetadata(annotationMetadata).apply("") - ); - } - - ); - } - - private Function pathEvaluationFunctionForMetadata(AnnotationMetadata annotationMetadata) { - return path -> { - if (annotationMetadata.hasDeclaredAnnotation(EachProperty.class)) { - if (annotationMetadata.booleanValue(EachProperty.class, "list").orElse(false)) { - return path + "[*]"; - } else { - return path + ".*"; - } - } - String prefix = annotationMetadata.getValue(ConfigurationReader.class, "prefix", String.class) - .orElse(null); - if (StringUtils.isNotEmpty(prefix)) { - if (StringUtils.isEmpty(path)) { - return prefix; - } else { - return prefix + "." + path; - } - } else { - return path; - } - }; - } - - @Override - protected String getTypeString(TypeElement type) { - return modelUtils.resolveTypeReference(type.asType()).toString(); - } - - @Override - protected AnnotationMetadata getAnnotationMetadata(TypeElement type) { - return annotationUtils.getDeclaredAnnotationMetadata(type); - } - - private void prependSuperclasses(TypeElement declaringType, StringBuilder path) { - if (declaringType.getKind() == ElementKind.INTERFACE) { - DeclaredType superInterface = resolveSuperInterface(declaringType); - while (superInterface != null) { - final TypeElement element = (TypeElement) superInterface.asElement(); - AnnotationMetadata annotationMetadata = annotationUtils.getDeclaredAnnotationMetadata(element); - String parentConfig = annotationMetadata.getValue(ConfigurationReader.class, String.class).orElse(""); - String parentPath = pathEvaluationFunctionForMetadata(annotationMetadata).apply(parentConfig); - if (StringUtils.isNotEmpty(parentPath)) { - path.insert(0, parentPath + '.'); - } - superInterface = resolveSuperInterface(element); - } - } else { - TypeMirror superclass = declaringType.getSuperclass(); - while (superclass instanceof DeclaredType) { - DeclaredType declaredType = (DeclaredType) superclass; - Element element = declaredType.asElement(); - AnnotationMetadata annotationMetadata = annotationUtils.getDeclaredAnnotationMetadata(element); - Optional parentConfig = annotationMetadata.getValue(ConfigurationReader.class, String.class); - if (parentConfig.isPresent()) { - String parentPath = pathEvaluationFunctionForMetadata(annotationMetadata).apply(parentConfig.get()); - if (StringUtils.isNotEmpty(parentPath)) { - path.insert(0, parentPath + '.'); - } - superclass = ((TypeElement) element).getSuperclass(); - } else { - if (annotationMetadata.isPresent(ConfigurationReader.class, "prefix")) { - String parentPath = pathEvaluationFunctionForMetadata(annotationMetadata).apply(""); - if (StringUtils.isNotEmpty(parentPath)) { - path.insert(0, parentPath + '.'); - } - superclass = ((TypeElement) element).getSuperclass(); - } else { - break; - } - } - } - } - } - - private DeclaredType resolveSuperInterface(TypeElement declaringType) { - return declaringType.getInterfaces().stream().filter(tm -> - tm instanceof DeclaredType && - annotationUtils.getAnnotationMetadata(((DeclaredType) tm).asElement()).hasStereotype(ConfigurationReader.class) - ).map(dt -> (DeclaredType) dt).findFirst().orElse(null); - } - -} diff --git a/inject-java/src/main/java/io/micronaut/annotation/processing/JavaElementAnnotationMetadataFactory.java b/inject-java/src/main/java/io/micronaut/annotation/processing/JavaElementAnnotationMetadataFactory.java new file mode 100644 index 00000000000..4c5957bd3f1 --- /dev/null +++ b/inject-java/src/main/java/io/micronaut/annotation/processing/JavaElementAnnotationMetadataFactory.java @@ -0,0 +1,41 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.annotation.processing; + +import io.micronaut.inject.annotation.AbstractElementAnnotationMetadataFactory; +import io.micronaut.inject.ast.ElementAnnotationMetadataFactory; + +import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.element.Element; + +/** + * Java element annotation metadata factory. + * + * @author Denis Stepanov + * @since 4.0.0 + */ +public final class JavaElementAnnotationMetadataFactory extends AbstractElementAnnotationMetadataFactory { + + public JavaElementAnnotationMetadataFactory(boolean isReadOnly, JavaAnnotationMetadataBuilder metadataBuilder) { + super(isReadOnly, metadataBuilder); + } + + @Override + public ElementAnnotationMetadataFactory readOnly() { + return new JavaElementAnnotationMetadataFactory(true, (JavaAnnotationMetadataBuilder) metadataBuilder); + } + +} diff --git a/inject-java/src/main/java/io/micronaut/annotation/processing/ModelUtils.java b/inject-java/src/main/java/io/micronaut/annotation/processing/ModelUtils.java index 8b1ee457771..d067bb0bab4 100644 --- a/inject-java/src/main/java/io/micronaut/annotation/processing/ModelUtils.java +++ b/inject-java/src/main/java/io/micronaut/annotation/processing/ModelUtils.java @@ -15,26 +15,37 @@ */ package io.micronaut.annotation.processing; -import io.micronaut.core.annotation.AnnotationUtil; -import io.micronaut.core.annotation.Nullable; -import io.micronaut.core.annotation.AnnotationMetadata; -import io.micronaut.core.annotation.Creator; import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.Nullable; import io.micronaut.core.naming.NameUtils; import io.micronaut.inject.ast.ClassElement; import io.micronaut.inject.processing.JavaModelUtils; -import javax.lang.model.element.*; +import javax.lang.model.element.Element; +import javax.lang.model.element.ElementKind; +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.element.Modifier; +import javax.lang.model.element.Name; +import javax.lang.model.element.PackageElement; +import javax.lang.model.element.TypeElement; import javax.lang.model.type.DeclaredType; import javax.lang.model.type.TypeKind; import javax.lang.model.type.TypeMirror; import javax.lang.model.util.ElementFilter; import javax.lang.model.util.Elements; import javax.lang.model.util.Types; -import java.util.*; -import java.util.stream.Collectors; - -import static javax.lang.model.element.Modifier.*; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.SortedSet; +import java.util.TreeSet; + +import static javax.lang.model.element.Modifier.ABSTRACT; +import static javax.lang.model.element.Modifier.FINAL; +import static javax.lang.model.element.Modifier.PRIVATE; +import static javax.lang.model.element.Modifier.PROTECTED; +import static javax.lang.model.element.Modifier.PUBLIC; +import static javax.lang.model.element.Modifier.STATIC; import static javax.lang.model.type.TypeKind.NONE; /** @@ -189,124 +200,6 @@ String setterNameFor(String fieldName) { return "set" + NameUtils.capitalize(fieldName); } - /** - * The constructor inject for the given class element. - * - * @param classElement The class element - * @param annotationUtils The annotation utilities - * @return The constructor - */ - @Nullable - public ExecutableElement concreteConstructorFor(TypeElement classElement, AnnotationUtils annotationUtils) { - if (JavaModelUtils.isRecord(classElement)) { - final List constructors = ElementFilter - .constructorsIn(classElement.getEnclosedElements()); - Optional element = findAnnotatedConstructor(annotationUtils, constructors); - if (element.isPresent()) { - return element.get(); - } else { - - // with records the record constructor is always the last constructor - return constructors.get(constructors.size() - 1); - } - } else { - List constructors = findNonPrivateConstructors(classElement); - if (constructors.isEmpty()) { - return null; - } - if (constructors.size() == 1) { - return constructors.get(0); - } - - Optional element = findAnnotatedConstructor(annotationUtils, constructors); - if (!element.isPresent()) { - element = constructors.stream().filter(ctor -> - ctor.getModifiers().contains(PUBLIC) - ).findFirst(); - } - return element.orElse(null); - } - } - - private Optional findAnnotatedConstructor(AnnotationUtils annotationUtils, List constructors) { - return constructors.stream().filter(ctor -> { - final AnnotationMetadata annotationMetadata = annotationUtils.getAnnotationMetadata(ctor); - return annotationMetadata.hasStereotype(AnnotationUtil.INJECT) || annotationMetadata.hasStereotype(Creator.class); - } - ).findFirst(); - } - - /** - * The static method or Kotlin companion method to execute to - * construct the given class element. - * - * @param classElement The class element - * @param annotationUtils The annotation utilities - * @return The creator method - */ - @Nullable - public ExecutableElement staticCreatorFor(TypeElement classElement, AnnotationUtils annotationUtils) { - List creators = findNonPrivateStaticCreators(classElement, annotationUtils); - - if (creators.isEmpty()) { - return null; - } - if (creators.size() == 1) { - return creators.get(0); - } - - //Can be multiple static @Creator methods. Prefer one with args here. The no arg method (if present) will - //be picked up by staticDefaultCreatorFor - List withArgs = creators.stream().filter(method -> !method.getParameters().isEmpty()).collect(Collectors.toList()); - - if (withArgs.size() == 1) { - return withArgs.get(0); - } else { - creators = withArgs; - } - - return creators.stream().filter(method -> method.getModifiers().contains(PUBLIC)).findFirst().orElse(null); - } - - /** - * @param classElement The class element - * @return True if the element has a non private 0 arg constructor - */ - public ExecutableElement defaultConstructorFor(TypeElement classElement) { - List constructors = findNonPrivateConstructors(classElement) - .stream().filter(ctor -> ctor.getParameters().isEmpty()).collect(Collectors.toList()); - - if (constructors.isEmpty()) { - return null; - } - - if (constructors.size() == 1) { - return constructors.get(0); - } - - return constructors.stream().filter(method -> method.getModifiers().contains(PUBLIC)).findFirst().orElse(null); - } - - /** - * @param classElement The class element - * @param annotationUtils The annotation utils - * @return A static creator with no args, or null - */ - public ExecutableElement defaultStaticCreatorFor(TypeElement classElement, AnnotationUtils annotationUtils) { - List creators = findNonPrivateStaticCreators(classElement, annotationUtils) - .stream().filter(ctor -> ctor.getParameters().isEmpty()).collect(Collectors.toList()); - - if (creators.isEmpty()) { - return null; - } - - if (creators.size() == 1) { - return creators.get(0); - } - - return creators.stream().filter(method -> method.getModifiers().contains(PUBLIC)).findFirst().orElse(null); - } - /** * Return whether the given element is the java.lang.Object class. * @@ -317,62 +210,6 @@ public boolean isObjectClass(TypeElement element) { return element.getSuperclass().getKind() == NONE; } - /** - * @param classElement The {@link TypeElement} - * @return A list of {@link ExecutableElement} - */ - private List findNonPrivateConstructors(TypeElement classElement) { - List ctors = - ElementFilter.constructorsIn(classElement.getEnclosedElements()); - return ctors.stream() - .filter(ctor -> !ctor.getModifiers().contains(PRIVATE)) - .collect(Collectors.toList()); - } - - private List findNonPrivateStaticCreators(TypeElement classElement, AnnotationUtils annotationUtils) { - List enclosedElements = classElement.getEnclosedElements(); - List staticCreators = ElementFilter.methodsIn(enclosedElements) - .stream() - .filter(method -> method.getModifiers().contains(STATIC)) - .filter(method -> !method.getModifiers().contains(PRIVATE)) - .filter(method -> typeUtils.isAssignable(typeUtils.erasure(method.getReturnType()), classElement.asType())) - .filter(method -> { - final AnnotationMetadata annotationMetadata = annotationUtils.getAnnotationMetadata(method); - return annotationMetadata.hasStereotype(Creator.class); - }) - .collect(Collectors.toList()); - - if (staticCreators.isEmpty()) { - TypeElement companionClass = ElementFilter.typesIn(enclosedElements) - .stream() - .filter(type -> type.getSimpleName().toString().equals("Companion")) - .filter(type -> type.getModifiers().contains(STATIC)) - .findFirst().orElse(null); - - if (companionClass != null) { - staticCreators = ElementFilter.methodsIn(companionClass.getEnclosedElements()) - .stream() - .filter(method -> !method.getModifiers().contains(PRIVATE)) - .filter(method -> method.getReturnType().equals(classElement.asType())) - .filter(method -> { - final AnnotationMetadata annotationMetadata = annotationUtils.getAnnotationMetadata(method); - return annotationMetadata.hasStereotype(Creator.class); - }) - .collect(Collectors.toList()); - } else if (classElement.getKind() == ElementKind.ENUM) { - staticCreators = ElementFilter.methodsIn(classElement.getEnclosedElements()) - .stream() - .filter(method -> method.getModifiers().contains(STATIC)) - .filter(method -> !method.getModifiers().contains(PRIVATE)) - .filter(method -> method.getReturnType().equals(classElement.asType())) - .filter(method -> method.getSimpleName().toString().equals("valueOf")) - .collect(Collectors.toList()); - } - } - - return staticCreators; - } - /** * Obtains the super type element for a given type element. * diff --git a/inject-java/src/main/java/io/micronaut/annotation/processing/PackageConfigurationInjectProcessor.java b/inject-java/src/main/java/io/micronaut/annotation/processing/PackageConfigurationInjectProcessor.java index 1a25c0d2356..e8f0a283f03 100644 --- a/inject-java/src/main/java/io/micronaut/annotation/processing/PackageConfigurationInjectProcessor.java +++ b/inject-java/src/main/java/io/micronaut/annotation/processing/PackageConfigurationInjectProcessor.java @@ -17,7 +17,6 @@ import io.micronaut.annotation.processing.visitor.JavaPackageElement; import io.micronaut.context.annotation.Configuration; -import io.micronaut.core.annotation.AnnotationMetadata; import io.micronaut.core.annotation.Internal; import io.micronaut.inject.writer.BeanConfigurationWriter; @@ -82,13 +81,17 @@ class AnnotationElementScanner extends SimpleElementVisitor8 { @Override public Object visitPackage(PackageElement packageElement, Object p) { Object aPackage = super.visitPackage(packageElement, p); - if (annotationUtils.hasStereotype(packageElement, Configuration.class)) { + JavaPackageElement javaPackageElement = new JavaPackageElement( + packageElement, + javaVisitorContext.getElementAnnotationMetadataFactory(), + javaVisitorContext + ); + if (javaPackageElement.hasStereotype(Configuration.class)) { String packageName = packageElement.getQualifiedName().toString(); - AnnotationMetadata annotationMetadata = annotationUtils.getAnnotationMetadata(packageElement); BeanConfigurationWriter writer = new BeanConfigurationWriter( packageName, - new JavaPackageElement(packageElement, annotationMetadata, javaVisitorContext), - annotationMetadata + javaPackageElement, + javaPackageElement.getAnnotationMetadata() ); try { writer.accept(classWriterOutputVisitor); diff --git a/inject-java/src/main/java/io/micronaut/annotation/processing/TypeElementVisitorProcessor.java b/inject-java/src/main/java/io/micronaut/annotation/processing/TypeElementVisitorProcessor.java index e96504f6cbd..fce6d3726ac 100644 --- a/inject-java/src/main/java/io/micronaut/annotation/processing/TypeElementVisitorProcessor.java +++ b/inject-java/src/main/java/io/micronaut/annotation/processing/TypeElementVisitorProcessor.java @@ -15,38 +15,51 @@ */ package io.micronaut.annotation.processing; +import io.micronaut.annotation.processing.visitor.JavaClassElement; +import io.micronaut.annotation.processing.visitor.JavaElementFactory; import io.micronaut.annotation.processing.visitor.LoadedVisitor; import io.micronaut.aop.Introduction; import io.micronaut.context.annotation.Requires; -import io.micronaut.core.annotation.AnnotationMetadata; import io.micronaut.core.annotation.Generated; import io.micronaut.core.annotation.Introspected; +import io.micronaut.core.annotation.NonNull; import io.micronaut.core.annotation.Nullable; import io.micronaut.core.io.service.SoftServiceLoader; import io.micronaut.core.order.OrderUtil; import io.micronaut.core.util.CollectionUtils; import io.micronaut.core.util.StringUtils; import io.micronaut.core.version.VersionUtils; -import io.micronaut.inject.annotation.AnnotationMetadataHierarchy; +import io.micronaut.inject.processing.ProcessingException; +import io.micronaut.inject.ast.ConstructorElement; +import io.micronaut.inject.ast.EnumConstantElement; +import io.micronaut.inject.ast.FieldElement; +import io.micronaut.inject.ast.MethodElement; import io.micronaut.inject.processing.JavaModelUtils; import io.micronaut.inject.visitor.TypeElementVisitor; import io.micronaut.inject.visitor.VisitorContext; -import io.micronaut.core.annotation.NonNull; import io.micronaut.inject.writer.AbstractBeanDefinitionBuilder; import javax.annotation.processing.ProcessingEnvironment; import javax.annotation.processing.RoundEnvironment; import javax.annotation.processing.SupportedOptions; import javax.lang.model.element.Element; +import javax.lang.model.element.ElementKind; import javax.lang.model.element.ExecutableElement; import javax.lang.model.element.TypeElement; import javax.lang.model.element.VariableElement; import javax.lang.model.type.DeclaredType; import javax.lang.model.type.TypeMirror; import javax.lang.model.util.ElementScanner8; - import java.io.IOException; -import java.util.*; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Objects; +import java.util.Set; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -60,11 +73,11 @@ * @since 1.0 */ @SupportedOptions({ - AbstractInjectAnnotationProcessor.MICRONAUT_PROCESSING_INCREMENTAL, - AbstractInjectAnnotationProcessor.MICRONAUT_PROCESSING_ANNOTATIONS, - VisitorContext.MICRONAUT_PROCESSING_PROJECT_DIR, - VisitorContext.MICRONAUT_PROCESSING_GROUP, - VisitorContext.MICRONAUT_PROCESSING_MODULE + AbstractInjectAnnotationProcessor.MICRONAUT_PROCESSING_INCREMENTAL, + AbstractInjectAnnotationProcessor.MICRONAUT_PROCESSING_ANNOTATIONS, + VisitorContext.MICRONAUT_PROCESSING_PROJECT_DIR, + VisitorContext.MICRONAUT_PROCESSING_GROUP, + VisitorContext.MICRONAUT_PROCESSING_MODULE }) public class TypeElementVisitorProcessor extends AbstractInjectAnnotationProcessor { private static final SoftServiceLoader SERVICE_LOADER = SoftServiceLoader.load(TypeElementVisitor.class, TypeElementVisitorProcessor.class.getClassLoader()).disableFork(); @@ -95,11 +108,13 @@ public class TypeElementVisitorProcessor extends AbstractInjectAnnotationProcess VISITOR_WARNINGS = Collections.unmodifiableSet(warnings); } } + private List loadedVisitors; private Collection typeElementVisitors; /** * The visited annotation names. + * * @return The names of all the visited annotations. */ static Set getVisitedAnnotationNames() { @@ -108,7 +123,6 @@ static Set getVisitedAnnotationNames() { @Override public synchronized void init(ProcessingEnvironment processingEnv) { - super.init(processingEnv); this.typeElementVisitors = findTypeElementVisitors(); @@ -127,17 +141,11 @@ public synchronized void init(ProcessingEnvironment processingEnv) { if (incrementalProcessorKind == visitorKind) { try { - loadedVisitors.add(new LoadedVisitor( - visitor, - javaVisitorContext, - genericUtils, - processingEnv - )); + loadedVisitors.add(new LoadedVisitor(visitor, genericUtils, processingEnv)); } catch (TypeNotPresentException | NoClassDefFoundError e) { // ignored, means annotations referenced are not on the classpath } } - } OrderUtil.reverseSort(loadedVisitors); @@ -149,11 +157,11 @@ public synchronized void init(ProcessingEnvironment processingEnv) { error("Error initializing type visitor [%s]: %s", loadedVisitor.getVisitor(), e.getMessage()); } } - } /** * Does this process have any visitors. + * * @return True if visitors are present. */ protected boolean hasVisitors() { @@ -173,7 +181,6 @@ protected List getLoadedVisitors() { } /** - * * @return The incremental processor type. * @see #GRADLE_PROCESSING_AGGREGATING * @see #GRADLE_PROCESSING_ISOLATING @@ -199,25 +206,24 @@ public Set getSupportedAnnotationTypes() { public Set getSupportedOptions() { Stream baseOption = super.getSupportedOptions().stream(); Stream visitorsOptions = typeElementVisitors - .stream() - .map(TypeElementVisitor::getSupportedOptions) - .flatMap(Collection::stream); + .stream() + .map(TypeElementVisitor::getSupportedOptions) + .flatMap(Collection::stream); Stream visitorsAnnotationsOptions = typeElementVisitors - .stream() - .filter(tev -> tev.getClass().isAnnotationPresent(SupportedOptions.class)) - .map(TypeElementVisitor::getClass) - .map(cls -> (SupportedOptions) cls.getAnnotation(SupportedOptions.class)) - .flatMap((SupportedOptions supportedOptions) -> Arrays.stream(supportedOptions.value())); + .stream() + .filter(tev -> tev.getClass().isAnnotationPresent(SupportedOptions.class)) + .map(TypeElementVisitor::getClass) + .map(cls -> (SupportedOptions) cls.getAnnotation(SupportedOptions.class)) + .flatMap((SupportedOptions supportedOptions) -> Arrays.stream(supportedOptions.value())); return Stream.of(baseOption, visitorsAnnotationsOptions, visitorsOptions) - .flatMap(Stream::sequential) - .collect(Collectors.toSet()); + .flatMap(Stream::sequential) + .collect(Collectors.toSet()); } @Override public boolean process(Set annotations, RoundEnvironment roundEnv) { - if (!loadedVisitors.isEmpty() && !(annotations.size() == 1 - && Generated.class.getName().equals(annotations.iterator().next().getQualifiedName().toString()))) { + && Generated.class.getName().equals(annotations.iterator().next().getQualifiedName().toString()))) { TypeElement groovyObjectTypeElement = elementUtils.getTypeElement("groovy.lang.GroovyObject"); TypeMirror groovyObjectType = groovyObjectTypeElement != null ? groovyObjectTypeElement.asType() : null; @@ -236,6 +242,9 @@ public boolean process(Set annotations, RoundEnvironment if (!elements.isEmpty()) { + JavaElementFactory elementFactory = javaVisitorContext.getElementFactory(); + JavaElementAnnotationMetadataFactory elementAnnotationMetadataFactory = javaVisitorContext.getElementAnnotationMetadataFactory(); + // The visitor X with a higher priority should process elements of A before // the visitor Y which is processing elements of B but also using elements A @@ -244,11 +253,22 @@ public boolean process(Set annotations, RoundEnvironment for (LoadedVisitor loadedVisitor : loadedVisitors) { for (TypeElement typeElement : elements) { - if (!loadedVisitor.matches(typeElement)) { + try { + JavaClassElement javaClassElement = elementFactory.newSourceClassElement( + typeElement, + elementAnnotationMetadataFactory + ); + if (!loadedVisitor.matchesClass(javaClassElement)) { + continue; + } + String className = typeElement.getQualifiedName().toString(); + typeElement.accept(new ElementVisitor(javaClassElement, typeElement, Collections.singletonList(loadedVisitor)), className); + } catch (ProcessingException e) { + error((Element) e.getOriginatingElement(), e.getMessage()); + } catch (PostponeToNextRoundException e) { + // Ignore continue; } - String className = typeElement.getQualifiedName().toString(); - typeElement.accept(new ElementVisitor(typeElement, Collections.singletonList(loadedVisitor)), className); } } } @@ -283,14 +303,13 @@ public boolean process(Set annotations, RoundEnvironment private void includeElements(Set target, Set annotatedElements, TypeMirror groovyObjectType) { annotatedElements - .stream() - .filter(element -> JavaModelUtils.isClassOrInterface(element) || JavaModelUtils.isEnum(element) || JavaModelUtils.isRecord(element)) - .map(modelUtils::classElementFor) - .filter(Objects::nonNull) - .filter(element -> element.getAnnotation(Generated.class) == null) - .filter(typeElement -> groovyObjectType == null || !typeUtils.isAssignable(typeElement.asType(), - groovyObjectType)) - .forEach(target::add); + .stream() + .filter(element -> JavaModelUtils.isClassOrInterface(element) || JavaModelUtils.isEnum(element) || JavaModelUtils.isRecord(element)) + .map(modelUtils::classElementFor) + .filter(Objects::nonNull) + .filter(element -> element.getAnnotation(Generated.class) == null) + .filter(typeElement -> groovyObjectType == null || !typeUtils.isAssignable(typeElement.asType(), groovyObjectType)) + .forEach(target::add); } /** @@ -320,31 +339,31 @@ private void writeBeanDefinitionsToMetaInf() { private static @NonNull Collection findCoreTypeElementVisitors( - @Nullable Set warnings) { + @Nullable Set warnings) { return SERVICE_LOADER.collectAll(visitor -> { - if (!visitor.isEnabled()) { - return false; - } + if (!visitor.isEnabled()) { + return false; + } - final Requires requires = visitor.getClass().getAnnotation(Requires.class); - if (requires != null) { - final Requires.Sdk sdk = requires.sdk(); - if (sdk == Requires.Sdk.MICRONAUT) { - final String version = requires.version(); - if (StringUtils.isNotEmpty(version) && !VersionUtils.isAtLeastMicronautVersion(version)) { - try { - if (warnings != null) { - warnings.add("TypeElementVisitor [" + visitor.getClass().getName() + "] will be ignored because Micronaut version [" + VersionUtils.MICRONAUT_VERSION + "] must be at least " + version); + final Requires requires = visitor.getClass().getAnnotation(Requires.class); + if (requires != null) { + final Requires.Sdk sdk = requires.sdk(); + if (sdk == Requires.Sdk.MICRONAUT) { + final String version = requires.version(); + if (StringUtils.isNotEmpty(version) && !VersionUtils.isAtLeastMicronautVersion(version)) { + try { + if (warnings != null) { + warnings.add("TypeElementVisitor [" + visitor.getClass().getName() + "] will be ignored because Micronaut version [" + VersionUtils.MICRONAUT_VERSION + "] must be at least " + version); + } + return false; + } catch (IllegalArgumentException e) { + // shouldn't happen, thrown when invalid version encountered } - return false; - } catch (IllegalArgumentException e) { - // shouldn't happen, thrown when invalid version encountered } } } - } - return true; - }).stream() + return true; + }).stream() // remove duplicate classes .collect(Collectors.toMap(v -> v.getClass(), v -> v, (a, b) -> a)).values(); } @@ -356,8 +375,10 @@ private class ElementVisitor extends ElementScanner8 { private final TypeElement concreteClass; private final List visitors; + private JavaClassElement javaClassElement; - ElementVisitor(TypeElement concreteClass, List visitors) { + ElementVisitor(JavaClassElement javaClassElement, TypeElement concreteClass, List visitors) { + this.javaClassElement = javaClassElement; this.concreteClass = concreteClass; this.visitors = visitors; } @@ -370,28 +391,29 @@ public Object visitUnknown(Element e, Object o) { @Override public Object visitType(TypeElement classElement, Object o) { - - AnnotationMetadata typeAnnotationMetadata = annotationUtils.getAnnotationMetadata(classElement); + if (!classElement.equals(javaClassElement.getNativeTypeElement())) { + javaClassElement = javaVisitorContext.getElementFactory().newSourceClassElement( + classElement, + javaVisitorContext.getElementAnnotationMetadataFactory() + ); + } for (LoadedVisitor visitor : visitors) { - final io.micronaut.inject.ast.Element resultingElement = visitor.visit(classElement, typeAnnotationMetadata); - if (resultingElement != null) { - typeAnnotationMetadata = resultingElement.getAnnotationMetadata(); - } + visitor.getVisitor().visitClass(javaClassElement, javaVisitorContext); } Element enclosingElement = classElement.getEnclosingElement(); // don't process inner class unless this is the visitor for it boolean shouldVisit = !JavaModelUtils.isClass(enclosingElement) || - concreteClass.getQualifiedName().equals(classElement.getQualifiedName()); + concreteClass.getQualifiedName().equals(classElement.getQualifiedName()); if (shouldVisit) { - if (typeAnnotationMetadata.hasStereotype(Introduction.class) || (typeAnnotationMetadata.hasStereotype(Introspected.class) && modelUtils.isAbstract(classElement))) { + if (javaClassElement.hasStereotype(Introduction.class) || (javaClassElement.hasStereotype(Introspected.class) && javaClassElement.isAbstract())) { classElement.asType().accept(new PublicAbstractMethodVisitor(classElement, javaVisitorContext) { @Override protected void accept(DeclaredType type, Element element, Object o) { if (element instanceof ExecutableElement) { ElementVisitor.this.visitExecutable( - (ExecutableElement) element, - o + (ExecutableElement) element, + o ); } } @@ -400,25 +422,16 @@ protected void accept(DeclaredType type, Element element, Object o) { } else if (JavaModelUtils.isEnum(classElement)) { return scan(classElement.getEnclosedElements(), o); } else { - List classes = new ArrayList<>(); List elements = enclosedElements(classElement); Object value = null; for (Element element : elements) { if (element instanceof TypeElement) { - classes.add((TypeElement) element); + // Ignore any inner classes, annotation processor will process them if needed + continue; } else { value = scan(element, o); } } - // TypeElementVisitor needs to process type's methods first and then all inner classes - for (TypeElement typeElement : classes) { - value = scan(typeElement, o); - for (LoadedVisitor visitor : visitors) { - if (visitor.matches(typeElement)) { - value = scan(enclosedElements(typeElement), o); - } - } - } return value; } } else { @@ -432,7 +445,7 @@ private List enclosedElements(TypeElement classElement) { // collect fields and methods, skip overrides while (superClass != null && !modelUtils.isObjectClass(superClass)) { List elements = superClass.getEnclosedElements(); - for (Element elt1: elements) { + for (Element elt1 : elements) { if (elt1 instanceof ExecutableElement) { checkMethodOverride(enclosedElements, elt1); } else if (elt1 instanceof VariableElement) { @@ -446,8 +459,8 @@ private List enclosedElements(TypeElement classElement) { private void checkFieldHide(List enclosedElements, Element elt1) { boolean hides = false; - for (Element elt2: enclosedElements) { - if (elt1.equals(elt2) || ! (elt2 instanceof VariableElement)) { + for (Element elt2 : enclosedElements) { + if (elt1.equals(elt2) || !(elt2 instanceof VariableElement)) { continue; } if (elementUtils.hides(elt2, elt1)) { @@ -455,62 +468,59 @@ private void checkFieldHide(List enclosedElements, Element elt1) { break; } } - if (! hides) { + if (!hides) { enclosedElements.add(elt1); } } private void checkMethodOverride(List enclosedElements, Element elt1) { boolean overrides = false; - for (Element elt2: enclosedElements) { - if (elt1.equals(elt2) || ! (elt2 instanceof ExecutableElement)) { + for (Element elt2 : enclosedElements) { + if (elt1.equals(elt2) || !(elt2 instanceof ExecutableElement)) { continue; } - if (elementUtils.overrides((ExecutableElement) elt2, (ExecutableElement) elt1, modelUtils.classElementFor(elt2))) { + if (elementUtils.overrides((ExecutableElement) elt2, (ExecutableElement) elt1, modelUtils.classElementFor(elt2))) { overrides = true; break; } } - if (! overrides) { + if (!overrides) { enclosedElements.add(elt1); } } @Override public Object visitExecutable(ExecutableElement executableElement, Object o) { - final AnnotationMetadata resolvedMethodMetadata = annotationUtils.getAnnotationMetadata(executableElement); - - AnnotationMetadata methodAnnotationMetadata; - - if (resolvedMethodMetadata instanceof AnnotationMetadataHierarchy) { - methodAnnotationMetadata = resolvedMethodMetadata; - } else { - methodAnnotationMetadata = new AnnotationMetadataHierarchy( - annotationUtils.getAnnotationMetadata(executableElement.getEnclosingElement()), - resolvedMethodMetadata - ); + if (javaClassElement == null) { + return null; } if (executableElement.getSimpleName().toString().equals("")) { + ConstructorElement constructorElement = javaVisitorContext.getElementFactory().newConstructorElement( + javaClassElement, + executableElement, + javaVisitorContext.getElementAnnotationMetadataFactory() + ); for (LoadedVisitor visitor : visitors) { - final io.micronaut.inject.ast.Element resultingElement = visitor.visit(executableElement, methodAnnotationMetadata); - if (resultingElement != null) { - methodAnnotationMetadata = resultingElement.getAnnotationMetadata(); + if (visitor.matchesElement(constructorElement)) { + visitor.getVisitor().visitConstructor(constructorElement, javaVisitorContext); } } - } else { - - for (LoadedVisitor visitor : visitors) { - if (visitor.matches(methodAnnotationMetadata)) { - final io.micronaut.inject.ast.Element resultingElement = visitor.visit(executableElement, methodAnnotationMetadata); - if (resultingElement != null) { - methodAnnotationMetadata = resultingElement.getAnnotationMetadata(); - } - } + return constructorElement; + } + MethodElement methodElement = javaVisitorContext.getElementFactory().newSourceMethodElement( + javaClassElement, + executableElement, + javaVisitorContext.getElementAnnotationMetadataFactory() + ); + if (methodElement.getDeclaringType().isAssignable(Enum.class)) { + return null; + } + for (LoadedVisitor visitor : visitors) { + if (visitor.matchesElement(methodElement)) { + visitor.getVisitor().visitMethod(methodElement, javaVisitorContext); } } - - - return null; + return methodElement; } @Override @@ -519,19 +529,31 @@ public Object visitVariable(VariableElement variable, Object o) { if (variable.getKind() != FIELD) { return null; } - - AnnotationMetadata fieldAnnotationMetadata = annotationUtils.getAnnotationMetadata(variable); - - for (LoadedVisitor visitor : visitors) { - if (visitor.matches(fieldAnnotationMetadata)) { - final io.micronaut.inject.ast.Element resultingElement = visitor.visit(variable, fieldAnnotationMetadata); - if (resultingElement != null) { - fieldAnnotationMetadata = resultingElement.getAnnotationMetadata(); + if (variable.getKind() == ElementKind.ENUM_CONSTANT) { + EnumConstantElement constantElement = javaVisitorContext.getElementFactory().newEnumConstantElement( + javaClassElement, + variable, + javaVisitorContext.getElementAnnotationMetadataFactory() + ); + for (LoadedVisitor visitor : visitors) { + if (visitor.matchesElement(constantElement)) { + visitor.getVisitor().visitEnumConstant(constantElement, javaVisitorContext); } } + return constantElement; } - - return null; + FieldElement fieldElement = javaVisitorContext.getElementFactory() + .newFieldElement( + javaClassElement, + variable, + javaVisitorContext.getElementAnnotationMetadataFactory() + ); + for (LoadedVisitor visitor : visitors) { + if (visitor.matchesElement(fieldElement)) { + visitor.getVisitor().visitField(fieldElement, javaVisitorContext); + } + } + return fieldElement; } } } diff --git a/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/AbstractJavaElement.java b/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/AbstractJavaElement.java index 54282c3b9b3..ab77fba80ba 100644 --- a/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/AbstractJavaElement.java +++ b/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/AbstractJavaElement.java @@ -17,15 +17,14 @@ import io.micronaut.annotation.processing.AnnotationUtils; import io.micronaut.core.annotation.AnnotationMetadata; -import io.micronaut.core.annotation.AnnotationMetadataDelegate; -import io.micronaut.core.annotation.AnnotationValue; -import io.micronaut.core.annotation.AnnotationValueBuilder; import io.micronaut.core.annotation.NonNull; -import io.micronaut.core.util.ArgumentUtils; -import io.micronaut.inject.annotation.AbstractAnnotationMetadataBuilder; +import io.micronaut.core.annotation.Nullable; import io.micronaut.inject.ast.ClassElement; +import io.micronaut.inject.ast.ElementAnnotationMetadata; +import io.micronaut.inject.ast.ElementAnnotationMetadataFactory; import io.micronaut.inject.ast.ElementModifier; -import io.micronaut.inject.ast.MemberElement; +import io.micronaut.inject.ast.ElementMutableAnnotationMetadata; +import io.micronaut.inject.ast.ElementMutableAnnotationMetadataDelegate; import io.micronaut.inject.ast.PrimitiveElement; import javax.lang.model.element.AnnotationMirror; @@ -43,15 +42,12 @@ import javax.lang.model.type.TypeVariable; import javax.lang.model.type.UnionType; import javax.lang.model.type.WildcardType; -import java.lang.annotation.Annotation; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.Set; -import java.util.function.Consumer; -import java.util.function.Predicate; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -66,146 +62,75 @@ * @author graemerocher * @since 1.0 */ -public abstract class AbstractJavaElement implements io.micronaut.inject.ast.Element { +public abstract class AbstractJavaElement implements io.micronaut.inject.ast.Element, ElementMutableAnnotationMetadataDelegate { + protected final JavaVisitorContext visitorContext; + protected final ElementAnnotationMetadataFactory elementAnnotationMetadataFactory; + @Nullable + protected AnnotationMetadata presetAnnotationMetadata; private final Element element; - private final JavaVisitorContext visitorContext; - private AnnotationMetadata annotationMetadata; + @Nullable + private ElementAnnotationMetadata elementAnnotationMetadata; /** - * @param element The {@link Element} - * @param annotationMetadata The Annotation metadata - * @param visitorContext The Java visitor context + * @param element The {@link Element} + * @param annotationMetadataFactory The annotation metadata factory + * @param visitorContext The Java visitor context */ - AbstractJavaElement(Element element, AnnotationMetadata annotationMetadata, JavaVisitorContext visitorContext) { + AbstractJavaElement(Element element, ElementAnnotationMetadataFactory annotationMetadataFactory, JavaVisitorContext visitorContext) { this.element = element; - this.annotationMetadata = annotationMetadata; + this.elementAnnotationMetadataFactory = annotationMetadataFactory; this.visitorContext = visitorContext; } - @NonNull @Override - public io.micronaut.inject.ast.Element annotate(@NonNull String annotationType, @NonNull Consumer> consumer) { - ArgumentUtils.requireNonNull("annotationType", annotationType); - ArgumentUtils.requireNonNull("consumer", consumer); - - final AnnotationValueBuilder builder = AnnotationValue.builder(annotationType); - consumer.accept(builder); - final AnnotationValue av = builder.build(); - AnnotationUtils annotationUtils = visitorContext - .getAnnotationUtils(); - annotationMetadata = annotationUtils - .newAnnotationBuilder() - .annotate(annotationMetadata, av); - - updateMetadataCaches(); + public io.micronaut.inject.ast.Element getReturnInstance() { return this; } @Override - public io.micronaut.inject.ast.Element annotate(AnnotationValue annotationValue) { - ArgumentUtils.requireNonNull("annotationValue", annotationValue); - - AnnotationUtils annotationUtils = visitorContext - .getAnnotationUtils(); - annotationMetadata = annotationUtils - .newAnnotationBuilder() - .annotate(annotationMetadata, annotationValue); - - updateMetadataCaches(); - return this; - } - - @Override - public io.micronaut.inject.ast.Element removeAnnotation(@NonNull String annotationType) { - ArgumentUtils.requireNonNull("annotationType", annotationType); - try { - AnnotationUtils annotationUtils = visitorContext - .getAnnotationUtils(); - annotationMetadata = annotationUtils - .newAnnotationBuilder() - .removeAnnotation(annotationMetadata, annotationType); - return this; - } finally { - updateMetadataCaches(); - } - } - - @Override - public io.micronaut.inject.ast.Element removeAnnotationIf(@NonNull Predicate> predicate) { - //noinspection ConstantConditions - if (predicate != null) { - try { - AnnotationUtils annotationUtils = visitorContext - .getAnnotationUtils(); - annotationMetadata = annotationUtils - .newAnnotationBuilder() - .removeAnnotationIf(annotationMetadata, predicate); - return this; - } finally { - updateMetadataCaches(); + public ElementMutableAnnotationMetadata getAnnotationMetadata() { + if (elementAnnotationMetadata == null) { + if (presetAnnotationMetadata == null) { + elementAnnotationMetadata = elementAnnotationMetadataFactory.build(this); + } else { + elementAnnotationMetadata = elementAnnotationMetadataFactory.build(this, presetAnnotationMetadata); } } - return this; + return elementAnnotationMetadata; } - @Override - public io.micronaut.inject.ast.Element removeStereotype(@NonNull String annotationType) { - ArgumentUtils.requireNonNull("annotationType", annotationType); - try { - AnnotationUtils annotationUtils = visitorContext - .getAnnotationUtils(); - annotationMetadata = annotationUtils - .newAnnotationBuilder() - .removeStereotype(annotationMetadata, annotationType); - return this; - } finally { - updateMetadataCaches(); - } - } + /** + * @return copy of this element + */ + protected abstract AbstractJavaElement copyThis(); - private void updateMetadataCaches() { - String declaringTypeName = resolveDeclaringTypeName(); - AbstractAnnotationMetadataBuilder.addMutatedMetadata(declaringTypeName, element, annotationMetadata); - AnnotationUtils.invalidateMetadata(element); + /** + * @param element the values to be copied to + */ + protected void copyValues(AbstractJavaElement element) { + element.presetAnnotationMetadata = presetAnnotationMetadata; } - private String resolveDeclaringTypeName() { - String declaringTypeName; - if (this instanceof MemberElement) { - final ClassElement owningType = ((MemberElement) this).getOwningType(); - final Element nativeType = (Element) owningType.getNativeType(); - declaringTypeName = resolveCanonicalName(nativeType); - } else { - final Object nativeType = getNativeType(); - if (nativeType instanceof TypeVariable) { - declaringTypeName = resolveCanonicalName(((TypeVariable) nativeType).asElement()); - } else if (nativeType instanceof Element) { - declaringTypeName = resolveCanonicalName((Element) nativeType); - } else { - throw new IllegalStateException("Cannot determine type name from: " + nativeType); - } - } - return declaringTypeName; + protected final AbstractJavaElement makeCopy() { + AbstractJavaElement element = copyThis(); + copyValues(element); + return element; } - private String resolveCanonicalName(Element nativeType) { - String declaringTypeName; - TypeElement typeElement = visitorContext.getModelUtils().classElementFor(nativeType); - if (typeElement == null) { - declaringTypeName = getName(); - } else { - declaringTypeName = typeElement.getQualifiedName().toString(); - } - return declaringTypeName; + @Override + public io.micronaut.inject.ast.Element withAnnotationMetadata(AnnotationMetadata annotationMetadata) { + AbstractJavaElement abstractJavaElement = makeCopy(); + abstractJavaElement.presetAnnotationMetadata = annotationMetadata; + return abstractJavaElement; } @Override public boolean isPackagePrivate() { Set modifiers = element.getModifiers(); return !(modifiers.contains(PUBLIC) - || modifiers.contains(PROTECTED) - || modifiers.contains(PRIVATE)); + || modifiers.contains(PROTECTED) + || modifiers.contains(PRIVATE)); } @Override @@ -216,9 +141,9 @@ public String getName() { @Override public Set getModifiers() { return element - .getModifiers().stream() - .map(m -> ElementModifier.valueOf(m.name())) - .collect(Collectors.toSet()); + .getModifiers().stream() + .map(m -> ElementModifier.valueOf(m.name())) + .collect(Collectors.toSet()); } @Override @@ -262,11 +187,6 @@ public Object getNativeType() { return element; } - @Override - public AnnotationMetadata getAnnotationMetadata() { - return annotationMetadata; - } - @Override public String toString() { return element.toString(); @@ -274,26 +194,27 @@ public String toString() { /** * Returns a class element with aligned generic information. - * @param typeMirror The type mirror - * @param visitorContext The visitor context + * + * @param typeMirror The type mirror + * @param visitorContext The visitor context * @param declaredGenericInfo The declared generic info * @return The class element */ protected @NonNull ClassElement parameterizedClassElement( - TypeMirror typeMirror, - JavaVisitorContext visitorContext, - Map> declaredGenericInfo) { + TypeMirror typeMirror, + JavaVisitorContext visitorContext, + Map> declaredGenericInfo) { return mirrorToClassElement( - typeMirror, - visitorContext, - declaredGenericInfo, - true); + typeMirror, + visitorContext, + declaredGenericInfo, + true); } /** * Obtain the ClassElement for the given mirror. * - * @param returnType The return type + * @param returnType The return type * @param visitorContext The visitor context * @return The class element */ @@ -304,9 +225,9 @@ public String toString() { /** * Obtain the ClassElement for the given mirror. * - * @param returnType The return type + * @param returnType The return type * @param visitorContext The visitor context - * @param genericsInfo The generic information. + * @param genericsInfo The generic information. * @return The class element */ protected @NonNull ClassElement mirrorToClassElement(TypeMirror returnType, JavaVisitorContext visitorContext, Map> genericsInfo) { @@ -316,9 +237,9 @@ public String toString() { /** * Obtain the ClassElement for the given mirror. * - * @param returnType The return type - * @param visitorContext The visitor context - * @param genericsInfo The generic information. + * @param returnType The return type + * @param visitorContext The visitor context + * @param genericsInfo The generic information. * @param includeTypeAnnotations Whether to include type level annotations in the metadata for the element * @return The class element */ @@ -329,19 +250,19 @@ public String toString() { /** * Obtain the ClassElement for the given mirror. * - * @param returnType The return type - * @param visitorContext The visitor context - * @param genericsInfo The generic information. + * @param returnType The return type + * @param visitorContext The visitor context + * @param genericsInfo The generic information. * @param includeTypeAnnotations Whether to include type level annotations in the metadata for the element - * @param isTypeVariable is the type a type variable + * @param isTypeVariable is the type a type variable * @return The class element */ protected @NonNull ClassElement mirrorToClassElement( - TypeMirror returnType, - JavaVisitorContext visitorContext, - Map> genericsInfo, - boolean includeTypeAnnotations, - boolean isTypeVariable) { + TypeMirror returnType, + JavaVisitorContext visitorContext, + Map> genericsInfo, + boolean includeTypeAnnotations, + boolean isTypeVariable) { if (genericsInfo == null) { genericsInfo = Collections.emptyMap(); } @@ -356,35 +277,25 @@ public String toString() { if (e instanceof TypeElement) { TypeElement typeElement = (TypeElement) e; Map boundGenerics = resolveBoundGenerics(visitorContext, genericsInfo); - AnnotationUtils annotationUtils = visitorContext - .getAnnotationUtils(); - AnnotationMetadata newAnnotationMetadata; - List annotationMirrors = dt.getAnnotationMirrors(); - if (!annotationMirrors.isEmpty()) { - newAnnotationMetadata = annotationUtils.newAnnotationBuilder().buildDeclared(typeElement, annotationMirrors, includeTypeAnnotations); - } else { - newAnnotationMetadata = includeTypeAnnotations ? annotationUtils.getAnnotationMetadata(typeElement) : AnnotationMetadata.EMPTY_METADATA; - } if (visitorContext.getModelUtils().resolveKind(typeElement, ElementKind.ENUM).isPresent()) { return new JavaEnumElement( - typeElement, - newAnnotationMetadata, - visitorContext + typeElement, + resolveElementAnnotationMetadataFactory(typeElement, dt, includeTypeAnnotations), + visitorContext ); } else { - genericsInfo = visitorContext.getGenericUtils().alignNewGenericsInfo( - typeElement, - typeArguments, - boundGenerics + typeElement, + typeArguments, + boundGenerics ); return new JavaClassElement( - typeElement, - newAnnotationMetadata, - visitorContext, - typeArguments, - genericsInfo, - isTypeVariable + typeElement, + resolveElementAnnotationMetadataFactory(typeElement, dt, includeTypeAnnotations), + visitorContext, + typeArguments, + genericsInfo, + isTypeVariable ); } } @@ -394,11 +305,11 @@ public String toString() { } else if (returnType instanceof TypeVariable) { TypeVariable tv = (TypeVariable) returnType; return resolveTypeVariable( - visitorContext, - genericsInfo, - includeTypeAnnotations, - tv, - tv + visitorContext, + genericsInfo, + includeTypeAnnotations, + tv, + tv ); } else if (returnType instanceof ArrayType) { @@ -437,23 +348,40 @@ public String toString() { upperBounds = Stream.of(extendsBound); } return new JavaWildcardElement( - wt, - upperBounds - .map(tm -> (JavaClassElement) mirrorToClassElement(tm, visitorContext, finalGenericsInfo, includeTypeAnnotations)) - .collect(Collectors.toList()), - lowerBounds - .map(tm -> (JavaClassElement) mirrorToClassElement(tm, visitorContext, finalGenericsInfo, includeTypeAnnotations)) - .collect(Collectors.toList()) + elementAnnotationMetadataFactory, + wt, + upperBounds + .map(tm -> (JavaClassElement) mirrorToClassElement(tm, visitorContext, finalGenericsInfo, includeTypeAnnotations)) + .collect(Collectors.toList()), + lowerBounds + .map(tm -> (JavaClassElement) mirrorToClassElement(tm, visitorContext, finalGenericsInfo, includeTypeAnnotations)) + .collect(Collectors.toList()) ); } return PrimitiveElement.VOID; } + @NonNull + private ElementAnnotationMetadataFactory resolveElementAnnotationMetadataFactory(TypeElement typeElement, DeclaredType dt, boolean includeTypeAnnotations) { + return elementAnnotationMetadataFactory.overrideForNativeType(typeElement, element -> { + AnnotationUtils annotationUtils = visitorContext + .getAnnotationUtils(); + AnnotationMetadata newAnnotationMetadata; + List annotationMirrors = dt.getAnnotationMirrors(); + if (!annotationMirrors.isEmpty()) { + newAnnotationMetadata = annotationUtils.newAnnotationBuilder().buildDeclared(typeElement, annotationMirrors, includeTypeAnnotations); + } else { + newAnnotationMetadata = includeTypeAnnotations ? annotationUtils.newAnnotationBuilder().lookupOrBuildForType(typeElement).copyAnnotationMetadata() : AnnotationMetadata.EMPTY_METADATA; + } + return elementAnnotationMetadataFactory.build(element, newAnnotationMetadata); + }); + } + private ClassElement resolveTypeVariable(JavaVisitorContext visitorContext, - Map> genericsInfo, - boolean includeTypeAnnotations, - TypeVariable tv, - TypeMirror declaration) { + Map> genericsInfo, + boolean includeTypeAnnotations, + TypeVariable tv, + TypeMirror declaration) { TypeMirror upperBound = tv.getUpperBound(); Map boundGenerics = resolveBoundGenerics(visitorContext, genericsInfo); @@ -463,15 +391,15 @@ private ClassElement resolveTypeVariable(JavaVisitorContext visitorContext, } else { // type variable is still free. List boundsUnresolved = upperBound instanceof IntersectionType ? - ((IntersectionType) upperBound).getBounds() : - Collections.singletonList(upperBound); + ((IntersectionType) upperBound).getBounds() : + Collections.singletonList(upperBound); List bounds = boundsUnresolved.stream() - .map(tm -> (JavaClassElement) mirrorToClassElement(tm, - visitorContext, - genericsInfo, - includeTypeAnnotations)) - .collect(Collectors.toList()); - return new JavaGenericPlaceholderElement(tv, bounds, 0); + .map(tm -> (JavaClassElement) mirrorToClassElement(tm, + visitorContext, + genericsInfo, + includeTypeAnnotations)) + .collect(Collectors.toList()); + return new JavaGenericPlaceholderElement(tv, bounds, elementAnnotationMetadataFactory, 0); } } @@ -497,11 +425,12 @@ public boolean equals(Object o) { if (this == o) { return true; } - if (o == null || getClass() != o.getClass()) { + // Do not check if classes match, sometimes it's an anonymous one + if (o == null) { return false; } - AbstractJavaElement that = (AbstractJavaElement) o; - return element.equals(that.element); + io.micronaut.inject.ast.Element that = (io.micronaut.inject.ast.Element) o; + return element.equals(that.getNativeType()); } @Override diff --git a/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaAnnotationElement.java b/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaAnnotationElement.java index d843a8573c7..c4e883041d3 100644 --- a/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaAnnotationElement.java +++ b/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaAnnotationElement.java @@ -15,10 +15,10 @@ */ package io.micronaut.annotation.processing.visitor; -import javax.lang.model.element.TypeElement; - -import io.micronaut.core.annotation.AnnotationMetadata; import io.micronaut.inject.ast.AnnotationElement; +import io.micronaut.inject.ast.ElementAnnotationMetadataFactory; + +import javax.lang.model.element.TypeElement; /** * Represents an annotation in the AST for Java. @@ -28,11 +28,11 @@ */ final class JavaAnnotationElement extends JavaClassElement implements AnnotationElement { /** - * @param classElement The {@link javax.lang.model.element.TypeElement} - * @param annotationMetadata The annotation metadata - * @param visitorContext The visitor context + * @param classElement The {@link javax.lang.model.element.TypeElement} + * @param annotationMetadataFactory The annotation metadata factory + * @param visitorContext The visitor context */ - JavaAnnotationElement(TypeElement classElement, AnnotationMetadata annotationMetadata, JavaVisitorContext visitorContext) { - super(classElement, annotationMetadata, visitorContext); + JavaAnnotationElement(TypeElement classElement, ElementAnnotationMetadataFactory annotationMetadataFactory, JavaVisitorContext visitorContext) { + super(classElement, annotationMetadataFactory, visitorContext); } } diff --git a/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaBeanDefinitionBuilder.java b/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaBeanDefinitionBuilder.java index 99e54b8c1ef..dbbcdc04a71 100644 --- a/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaBeanDefinitionBuilder.java +++ b/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaBeanDefinitionBuilder.java @@ -27,6 +27,7 @@ import io.micronaut.inject.annotation.AnnotationMetadataHierarchy; import io.micronaut.inject.ast.ClassElement; import io.micronaut.inject.ast.Element; +import io.micronaut.inject.ast.ElementAnnotationMetadataFactory; import io.micronaut.inject.ast.FieldElement; import io.micronaut.inject.ast.MethodElement; import io.micronaut.inject.ast.TypedElement; @@ -37,9 +38,6 @@ import io.micronaut.inject.writer.BeanDefinitionVisitor; import io.micronaut.inject.writer.BeanDefinitionWriter; -import javax.lang.model.element.ExecutableElement; -import javax.lang.model.element.TypeElement; -import javax.lang.model.element.VariableElement; import java.lang.annotation.Annotation; import java.util.function.BiConsumer; import java.util.function.Consumer; @@ -57,13 +55,18 @@ class JavaBeanDefinitionBuilder extends AbstractBeanDefinitionBuilder { /** * Default constructor. * - * @param originatingElement The originating element - * @param beanType The bean type - * @param metadataBuilder the metadata builder - * @param visitorContext the visitor context + * @param originatingElement The originating element + * @param beanType The bean type + * @param metadataBuilder the metadata builder + * @param elementAnnotationMetadataFactory The element annotation metadata factory + * @param visitorContext the visitor context */ - JavaBeanDefinitionBuilder(Element originatingElement, ClassElement beanType, ConfigurationMetadataBuilder metadataBuilder, JavaVisitorContext visitorContext) { - super(originatingElement, beanType, metadataBuilder, visitorContext); + JavaBeanDefinitionBuilder(Element originatingElement, + ClassElement beanType, + ConfigurationMetadataBuilder metadataBuilder, + ElementAnnotationMetadataFactory elementAnnotationMetadataFactory, + JavaVisitorContext visitorContext) { + super(originatingElement, beanType, metadataBuilder, visitorContext, elementAnnotationMetadataFactory); this.javaVisitorContext = visitorContext; if (visitorContext.getVisitorKind() == TypeElementVisitor.VisitorKind.ISOLATING) { if (getClass() == JavaBeanDefinitionBuilder.class) { @@ -78,10 +81,11 @@ class JavaBeanDefinitionBuilder extends AbstractBeanDefinitionBuilder { protected AbstractBeanDefinitionBuilder createChildBean(FieldElement producerField) { final ClassElement parentType = getBeanType(); return new JavaBeanDefinitionBuilder( - JavaBeanDefinitionBuilder.this.getOriginatingElement(), - producerField.getGenericField().getType(), - JavaBeanDefinitionBuilder.this.metadataBuilder, - (JavaVisitorContext) JavaBeanDefinitionBuilder.this.visitorContext + JavaBeanDefinitionBuilder.this.getOriginatingElement(), + producerField.getGenericField().getType(), + JavaBeanDefinitionBuilder.this.metadataBuilder, + elementAnnotationMetadataFactory, + (JavaVisitorContext) JavaBeanDefinitionBuilder.this.visitorContext ) { @Override public Element getProducingElement() { @@ -96,16 +100,12 @@ public ClassElement getDeclaringElement() { @Override protected BeanDefinitionVisitor createBeanDefinitionWriter() { final BeanDefinitionVisitor writer = super.createBeanDefinitionWriter(); - final JavaElementFactory elementFactory = ((JavaVisitorContext) visitorContext).getElementFactory(); - final VariableElement variableElement = (VariableElement) producerField.getNativeType(); - ClassElement resolvedParent = resolveParentType(parentType, elementFactory); + ClassElement newParent = parentType.withAnnotationMetadata(parentType.copyAnnotationMetadata()); // Just a copy writer.visitBeanFactoryField( - resolvedParent, - elementFactory.newFieldElement( - resolvedParent, - variableElement, - new AnnotationMetadataHierarchy(resolvedParent.getDeclaredMetadata(), producerField.getDeclaredMetadata()) - ) + newParent, + producerField.withAnnotationMetadata( + new AnnotationMetadataHierarchy(newParent.getDeclaredMetadata(), producerField.getDeclaredMetadata()) + ) ); return writer; } @@ -115,13 +115,12 @@ protected BeanDefinitionVisitor createBeanDefinitionWriter() { @Override protected BeanDefinitionVisitor createAopWriter(BeanDefinitionWriter beanDefinitionWriter, AnnotationMetadata annotationMetadata) { AnnotationValue[] interceptorTypes = - InterceptedMethodUtil.resolveInterceptorBinding(annotationMetadata, InterceptorKind.AROUND); + InterceptedMethodUtil.resolveInterceptorBinding(annotationMetadata, InterceptorKind.AROUND); return new AopProxyWriter( - beanDefinitionWriter, - annotationMetadata.getValues(Around.class, Boolean.class), - ConfigurationMetadataBuilder.getConfigurationMetadataBuilder().orElse(null), - visitorContext, - interceptorTypes + beanDefinitionWriter, + annotationMetadata.getValues(Around.class, Boolean.class), + visitorContext, + interceptorTypes ); } @@ -130,10 +129,10 @@ protected BiConsumer createAroundMethodVisitor(Bean AopProxyWriter aopProxyWriter = (AopProxyWriter) aopWriter; return (bean, method) -> { AnnotationValue[] newTypes = - InterceptedMethodUtil.resolveInterceptorBinding(method.getAnnotationMetadata(), InterceptorKind.AROUND); + InterceptedMethodUtil.resolveInterceptorBinding(method.getAnnotationMetadata(), InterceptorKind.AROUND); aopProxyWriter.visitInterceptorBinding(newTypes); aopProxyWriter.visitAroundMethod( - bean, method + bean, method ); }; } @@ -142,12 +141,14 @@ protected BiConsumer createAroundMethodVisitor(Bean protected AbstractBeanDefinitionBuilder createChildBean(MethodElement producerMethod) { final ClassElement parentType = getBeanType(); return new JavaBeanDefinitionBuilder( - JavaBeanDefinitionBuilder.this.getOriginatingElement(), - producerMethod.getGenericReturnType(), - JavaBeanDefinitionBuilder.this.metadataBuilder, - (JavaVisitorContext) JavaBeanDefinitionBuilder.this.visitorContext + JavaBeanDefinitionBuilder.this.getOriginatingElement(), + producerMethod.getGenericReturnType(), + JavaBeanDefinitionBuilder.this.metadataBuilder, + elementAnnotationMetadataFactory, + (JavaVisitorContext) JavaBeanDefinitionBuilder.this.visitorContext ) { BeanParameterElement[] parameters; + @Override public Element getProducingElement() { return producerMethod; @@ -169,17 +170,13 @@ protected BeanParameterElement[] getParameters() { @Override protected BeanDefinitionVisitor createBeanDefinitionWriter() { final BeanDefinitionVisitor writer = super.createBeanDefinitionWriter(); - final JavaElementFactory elementFactory = ((JavaVisitorContext) visitorContext).getElementFactory(); - final ExecutableElement variableElement = (ExecutableElement) producerMethod.getNativeType(); - ClassElement resolvedParent = resolveParentType(parentType, elementFactory); + ClassElement newParent = parentType.withAnnotationMetadata(parentType.copyAnnotationMetadata()); // Just a copy writer.visitBeanFactoryMethod( - resolvedParent, - elementFactory.newMethodElement( - resolvedParent, - variableElement, - new AnnotationMetadataHierarchy(resolvedParent.getDeclaredMetadata(), producerMethod.getDeclaredMetadata()) - ), - getParameters() + newParent, + producerMethod.withAnnotationMetadata( + new AnnotationMetadataHierarchy(newParent.getDeclaredMetadata(), producerMethod.getDeclaredMetadata()) + ), + getParameters() ); return writer; } @@ -193,10 +190,10 @@ protected void annotate(AnnotationMetadata annotationMeta ArgumentUtils.requireNonNull("annotationValue", annotationValue); AnnotationUtils annotationUtils = javaVisitorContext - .getAnnotationUtils(); + .getAnnotationUtils(); annotationUtils - .newAnnotationBuilder() - .annotate(annotationMetadata, annotationValue); + .newAnnotationBuilder() + .annotate(annotationMetadata, annotationValue); } @Override @@ -208,48 +205,40 @@ protected void annotate(AnnotationMetadata annotationMeta consumer.accept(builder); final AnnotationValue av = builder.build(); AnnotationUtils annotationUtils = javaVisitorContext - .getAnnotationUtils(); + .getAnnotationUtils(); annotationUtils - .newAnnotationBuilder() - .annotate(annotationMetadata, av); + .newAnnotationBuilder() + .annotate(annotationMetadata, av); } @Override protected void removeStereotype(AnnotationMetadata annotationMetadata, String annotationType) { ArgumentUtils.requireNonNull("annotationType", annotationType); AnnotationUtils annotationUtils = javaVisitorContext - .getAnnotationUtils(); + .getAnnotationUtils(); annotationUtils - .newAnnotationBuilder() - .removeStereotype(annotationMetadata, annotationType); + .newAnnotationBuilder() + .removeStereotype(annotationMetadata, annotationType); } @Override protected void removeAnnotationIf(AnnotationMetadata annotationMetadata, Predicate> predicate) { ArgumentUtils.requireNonNull("predicate", predicate); AnnotationUtils annotationUtils = javaVisitorContext - .getAnnotationUtils(); + .getAnnotationUtils(); annotationUtils - .newAnnotationBuilder() - .removeAnnotationIf(annotationMetadata, predicate); + .newAnnotationBuilder() + .removeAnnotationIf(annotationMetadata, predicate); } @Override protected void removeAnnotation(AnnotationMetadata annotationMetadata, String annotationType) { ArgumentUtils.requireNonNull("annotationType", annotationType); AnnotationUtils annotationUtils = javaVisitorContext - .getAnnotationUtils(); + .getAnnotationUtils(); annotationUtils - .newAnnotationBuilder() - .removeAnnotation(annotationMetadata, annotationType); + .newAnnotationBuilder() + .removeAnnotation(annotationMetadata, annotationType); } - private ClassElement resolveParentType(ClassElement parentType, JavaElementFactory elementFactory) { - Object nativeType = parentType.getNativeType(); - ClassElement resolvedParent = parentType; - if (nativeType instanceof TypeElement) { - resolvedParent = elementFactory.newClassElement((TypeElement) nativeType, this.getAnnotationMetadata()); - } - return resolvedParent; - } } diff --git a/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaClassElement.java b/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaClassElement.java index 8637fcc33bb..04227a7a96a 100644 --- a/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaClassElement.java +++ b/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaClassElement.java @@ -15,36 +15,36 @@ */ package io.micronaut.annotation.processing.visitor; -import io.micronaut.annotation.processing.AnnotationUtils; -import io.micronaut.annotation.processing.ModelUtils; -import io.micronaut.annotation.processing.PublicMethodVisitor; import io.micronaut.annotation.processing.SuperclassAwareTypeVisitor; -import io.micronaut.core.annotation.AccessorsStyle; import io.micronaut.core.annotation.AnnotationMetadata; import io.micronaut.core.annotation.AnnotationUtil; +import io.micronaut.core.annotation.Creator; import io.micronaut.core.annotation.Internal; import io.micronaut.core.annotation.NonNull; import io.micronaut.core.naming.NameUtils; import io.micronaut.core.reflect.ClassUtils; import io.micronaut.core.util.StringUtils; import io.micronaut.inject.ast.ArrayableClassElement; +import io.micronaut.inject.ast.BeanPropertiesQuery; import io.micronaut.inject.ast.ClassElement; import io.micronaut.inject.ast.ConstructorElement; +import io.micronaut.inject.ast.ElementAnnotationMetadataFactory; import io.micronaut.inject.ast.ElementModifier; import io.micronaut.inject.ast.ElementQuery; import io.micronaut.inject.ast.FieldElement; import io.micronaut.inject.ast.GenericPlaceholderElement; +import io.micronaut.inject.ast.MemberElement; import io.micronaut.inject.ast.MethodElement; import io.micronaut.inject.ast.PackageElement; import io.micronaut.inject.ast.PropertyElement; import io.micronaut.inject.ast.WildcardElement; +import io.micronaut.inject.ast.utils.AstBeanPropertiesUtils; import io.micronaut.inject.processing.JavaModelUtils; import javax.lang.model.element.Element; import javax.lang.model.element.ElementKind; import javax.lang.model.element.ExecutableElement; import javax.lang.model.element.Modifier; -import javax.lang.model.element.Name; import javax.lang.model.element.TypeElement; import javax.lang.model.element.TypeParameterElement; import javax.lang.model.element.VariableElement; @@ -56,13 +56,18 @@ import java.util.ArrayList; import java.util.Collection; import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; import java.util.Iterator; import java.util.LinkedHashMap; +import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.Set; +import java.util.function.BiPredicate; +import java.util.function.Function; import java.util.function.Predicate; import java.util.stream.Collectors; @@ -78,7 +83,6 @@ public class JavaClassElement extends AbstractJavaElement implements ArrayableCl private static final String KOTLIN_METADATA = "kotlin.Metadata"; private static final String PREFIX_IS = "is"; protected final TypeElement classElement; - protected final JavaVisitorContext visitorContext; final List typeArguments; private final int arrayDimensions; private final boolean isTypeVariable; @@ -88,95 +92,119 @@ public class JavaClassElement extends AbstractJavaElement implements ArrayableCl private String simpleName; private String name; private String packageName; + private final Map elementsCache = new HashMap<>(); + private Map resolvedTypeArguments; /** - * @param classElement The {@link TypeElement} - * @param annotationMetadata The annotation metadata - * @param visitorContext The visitor context + * @param classElement The {@link TypeElement} + * @param annotationMetadataFactory The annotation metadata factory + * @param visitorContext The visitor context */ @Internal - public JavaClassElement(TypeElement classElement, AnnotationMetadata annotationMetadata, JavaVisitorContext visitorContext) { - this(classElement, annotationMetadata, visitorContext, Collections.emptyList(), null, 0, false); + public JavaClassElement(TypeElement classElement, ElementAnnotationMetadataFactory annotationMetadataFactory, JavaVisitorContext visitorContext) { + this(classElement, annotationMetadataFactory, visitorContext, Collections.emptyList(), null, 0, false); } /** - * @param classElement The {@link TypeElement} - * @param annotationMetadata The annotation metadata - * @param visitorContext The visitor context - * @param typeArguments The declared type arguments - * @param genericsInfo The generic type info + * @param classElement The {@link TypeElement} + * @param annotationMetadataFactory The annotation metadata factory + * @param visitorContext The visitor context + * @param typeArguments The declared type arguments + * @param genericsInfo The generic type info */ JavaClassElement( - TypeElement classElement, - AnnotationMetadata annotationMetadata, - JavaVisitorContext visitorContext, - List typeArguments, - Map> genericsInfo) { - this(classElement, annotationMetadata, visitorContext, typeArguments, genericsInfo, 0, false); + TypeElement classElement, + ElementAnnotationMetadataFactory annotationMetadataFactory, + JavaVisitorContext visitorContext, + List typeArguments, + Map> genericsInfo) { + this(classElement, annotationMetadataFactory, visitorContext, typeArguments, genericsInfo, 0, false); } /** - * @param classElement The {@link TypeElement} - * @param annotationMetadata The annotation metadata - * @param visitorContext The visitor context - * @param typeArguments The declared type arguments - * @param genericsInfo The generic type info - * @param arrayDimensions The number of array dimensions + * @param classElement The {@link TypeElement} + * @param annotationMetadataFactory The annotation metadata factory + * @param visitorContext The visitor context + * @param typeArguments The declared type arguments + * @param genericsInfo The generic type info + * @param arrayDimensions The number of array dimensions */ JavaClassElement( - TypeElement classElement, - AnnotationMetadata annotationMetadata, - JavaVisitorContext visitorContext, - List typeArguments, - Map> genericsInfo, - int arrayDimensions) { - this(classElement, annotationMetadata, visitorContext, typeArguments, genericsInfo, arrayDimensions, false); + TypeElement classElement, + ElementAnnotationMetadataFactory annotationMetadataFactory, + JavaVisitorContext visitorContext, + List typeArguments, + Map> genericsInfo, + int arrayDimensions) { + this(classElement, annotationMetadataFactory, visitorContext, typeArguments, genericsInfo, arrayDimensions, false); } /** - * @param classElement The {@link TypeElement} - * @param annotationMetadata The annotation metadata - * @param visitorContext The visitor context - * @param typeArguments The declared type arguments - * @param genericsInfo The generic type info - * @param isTypeVariable Is the class element a type variable + * @param classElement The {@link TypeElement} + * @param annotationMetadataFactory The annotation metadata factory + * @param visitorContext The visitor context + * @param typeArguments The declared type arguments + * @param genericsInfo The generic type info + * @param isTypeVariable Is the class element a type variable */ JavaClassElement( - TypeElement classElement, - AnnotationMetadata annotationMetadata, - JavaVisitorContext visitorContext, - List typeArguments, - Map> genericsInfo, - boolean isTypeVariable) { - this(classElement, annotationMetadata, visitorContext, typeArguments, genericsInfo, 0, isTypeVariable); + TypeElement classElement, + ElementAnnotationMetadataFactory annotationMetadataFactory, + JavaVisitorContext visitorContext, + List typeArguments, + Map> genericsInfo, + boolean isTypeVariable) { + this(classElement, annotationMetadataFactory, visitorContext, typeArguments, genericsInfo, 0, isTypeVariable); } /** - * @param classElement The {@link TypeElement} - * @param annotationMetadata The annotation metadata - * @param visitorContext The visitor context - * @param typeArguments The declared type arguments - * @param genericsInfo The generic type info - * @param arrayDimensions The number of array dimensions - * @param isTypeVariable Is the type a type variable + * @param classElement The {@link TypeElement} + * @param annotationMetadataFactory The annotation metadata factory + * @param visitorContext The visitor context + * @param typeArguments The declared type arguments + * @param genericsInfo The generic type info + * @param arrayDimensions The number of array dimensions + * @param isTypeVariable Is the type a type variable */ JavaClassElement( - TypeElement classElement, - AnnotationMetadata annotationMetadata, - JavaVisitorContext visitorContext, - List typeArguments, - Map> genericsInfo, - int arrayDimensions, - boolean isTypeVariable) { - super(classElement, annotationMetadata, visitorContext); + TypeElement classElement, + ElementAnnotationMetadataFactory annotationMetadataFactory, + JavaVisitorContext visitorContext, + List typeArguments, + Map> genericsInfo, + int arrayDimensions, + boolean isTypeVariable) { + super(classElement, annotationMetadataFactory, visitorContext); this.classElement = classElement; - this.visitorContext = visitorContext; this.typeArguments = typeArguments; this.genericTypeInfo = genericsInfo; this.arrayDimensions = arrayDimensions; this.isTypeVariable = isTypeVariable; } + @Override + protected JavaClassElement copyThis() { + return new JavaClassElement(classElement, elementAnnotationMetadataFactory, visitorContext, typeArguments, genericTypeInfo); + } + + @Override + protected void copyValues(AbstractJavaElement element) { + super.copyValues(element); + ((JavaClassElement) element).resolvedTypeArguments = resolvedTypeArguments; + } + + @Override + public ClassElement withTypeArguments(Map newTypeArguments) { + JavaClassElement javaClassElement = (JavaClassElement) makeCopy(); + javaClassElement.resolvedTypeArguments = newTypeArguments; + return javaClassElement; + } + + @Override + public ClassElement withAnnotationMetadata(AnnotationMetadata annotationMetadata) { + return (ClassElement) super.withAnnotationMetadata(annotationMetadata); + } + @Override public boolean isTypeVariable() { return isTypeVariable; @@ -197,6 +225,10 @@ public boolean isRecord() { return JavaModelUtils.isRecord(classElement); } + public final TypeElement getNativeTypeElement() { + return classElement; + } + @NonNull @Override public Map getTypeArguments(@NonNull String type) { @@ -248,17 +280,17 @@ public Optional getSuperType() { // if super type has type arguments, then build a parameterized ClassElement if (superclass instanceof DeclaredType && !((DeclaredType) superclass).getTypeArguments().isEmpty()) { return Optional.of( - parameterizedClassElement( - superclass, - visitorContext, - visitorContext.getGenericUtils().buildGenericTypeArgumentElementInfo(classElement, null, getBoundTypeMirrors()))); + parameterizedClassElement( + superclass, + visitorContext, + visitorContext.getGenericUtils().buildGenericTypeArgumentElementInfo(classElement, null, getBoundTypeMirrors()))); } return Optional.of( - new JavaClassElement( - superElement, - visitorContext.getAnnotationUtils().getAnnotationMetadata(superElement), - visitorContext - ) + new JavaClassElement( + superElement, + elementAnnotationMetadataFactory, + visitorContext + ) ); } } @@ -278,257 +310,158 @@ public boolean isInterface() { @Override public List getBeanProperties() { - if (this.beanProperties == null) { - - Map props = new LinkedHashMap<>(); - Map fields = new LinkedHashMap<>(); - - if (isRecord()) { - classElement.asType().accept(new SuperclassAwareTypeVisitor(visitorContext) { + if (beanProperties == null) { + beanProperties = getBeanProperties(BeanPropertiesQuery.of(this)); + } + return Collections.unmodifiableList(beanProperties); + } - @Override - protected boolean isAcceptable(Element element) { - return JavaModelUtils.isRecord(element); + @Override + public List getBeanProperties(BeanPropertiesQuery beanPropertiesQuery) { + if (isRecord()) { + return AstBeanPropertiesUtils.resolveBeanProperties(beanPropertiesQuery, + this, + this::getRecordMethods, + this::getRecordFields, + true, + Collections.emptySet(), + methodElement -> Optional.empty(), + methodElement -> Optional.empty(), + this::mapToPropertyElement); + } + Function> customReaderPropertyNameResolver = methodElement -> Optional.empty(); + Function> customWriterPropertyNameResolver = methodElement -> Optional.empty(); + if (isKotlinClass(getNativeTypeElement())) { + Set isProperties = getEnclosedElements(ElementQuery.ALL_METHODS) + .stream() + .map(io.micronaut.inject.ast.Element::getName) + .filter(method -> method.startsWith(PREFIX_IS)) + .collect(Collectors.toSet()); + if (!isProperties.isEmpty()) { + customReaderPropertyNameResolver = methodElement -> { + String methodName = methodElement.getSimpleName(); + if (methodName.startsWith(PREFIX_IS)) { + return Optional.of(methodName); } - - @Override - public Object visitDeclared(DeclaredType type, Object o) { - Element element = type.asElement(); - if (isAcceptable(element)) { - List enclosedElements = element.getEnclosedElements(); - for (Element enclosedElement : enclosedElements) { - if (JavaModelUtils.isRecordComponent(enclosedElement) || enclosedElement instanceof ExecutableElement) { - if (enclosedElement.getKind() != ElementKind.CONSTRUCTOR) { - accept(type, enclosedElement, o); - } - } - } - } - return o; + return Optional.empty(); + }; + customWriterPropertyNameResolver = methodElement -> { + String methodName = methodElement.getSimpleName(); + String propertyName = NameUtils.getPropertyNameForSetter(methodName); + String isPropertyName = PREFIX_IS + NameUtils.capitalize(propertyName); + if (isProperties.contains(isPropertyName)) { + return Optional.of(isPropertyName); } + return Optional.empty(); + }; + } + } + return AstBeanPropertiesUtils.resolveBeanProperties(beanPropertiesQuery, + this, + () -> getEnclosedElements(ElementQuery.ALL_METHODS), + () -> getEnclosedElements(ElementQuery.ALL_FIELDS), + false, + Collections.emptySet(), + customReaderPropertyNameResolver, + customWriterPropertyNameResolver, + this::mapToPropertyElement); + } - @Override - protected void accept(DeclaredType type, Element element, Object o) { - String name = element.getSimpleName().toString(); - if (element instanceof ExecutableElement) { - BeanPropertyData beanPropertyData = props.get(name); - if (beanPropertyData != null) { - beanPropertyData.getter = (ExecutableElement) element; - } - } else { - - props.computeIfAbsent(name, propertyName -> { - - BeanPropertyData beanPropertyData = new BeanPropertyData(propertyName); - beanPropertyData.declaringType = JavaClassElement.this; - beanPropertyData.type = mirrorToClassElement(element.asType(), visitorContext, genericTypeInfo, true); - return beanPropertyData; - }); - } - } + private JavaPropertyElement mapToPropertyElement(AstBeanPropertiesUtils.BeanPropertyData value) { + return new JavaPropertyElement( + JavaClassElement.this, + value.type, + value.readAccessKind == null ? null : value.getter, + value.writeAccessKind == null ? null : value.setter, + value.field, + elementAnnotationMetadataFactory, + value.propertyName, + value.readAccessKind == null ? PropertyElement.AccessKind.METHOD : PropertyElement.AccessKind.valueOf(value.readAccessKind.name()), + value.writeAccessKind == null ? PropertyElement.AccessKind.METHOD : PropertyElement.AccessKind.valueOf(value.writeAccessKind.name()), + value.isExcluded, + visitorContext); + } - }, null); - } else { + private List getRecordMethods() { + List methodElements = new ArrayList<>(); + classElement.asType().accept(new SuperclassAwareTypeVisitor(visitorContext) { - classElement.asType().accept(new PublicMethodVisitor(visitorContext) { + private final Set recordComponents = new HashSet<>(); - final String[] readPrefixes = getValue(AccessorsStyle.class, "readPrefixes", String[].class) - .orElse(new String[]{AccessorsStyle.DEFAULT_READ_PREFIX}); - final String[] writePrefixes = getValue(AccessorsStyle.class, "writePrefixes", String[].class) - .orElse(new String[]{AccessorsStyle.DEFAULT_WRITE_PREFIX}); + @Override + protected boolean isAcceptable(Element element) { + return JavaModelUtils.isRecord(element); + } - @Override - protected boolean isAcceptable(javax.lang.model.element.Element element) { - if (element.getKind() == ElementKind.FIELD) { - return true; - } - if (element.getKind() == ElementKind.METHOD && element instanceof ExecutableElement) { - Set modifiers = element.getModifiers(); - if (modifiers.contains(Modifier.PUBLIC) && !modifiers.contains(Modifier.STATIC)) { - ExecutableElement executableElement = (ExecutableElement) element; - String methodName = executableElement.getSimpleName().toString(); - if (methodName.contains("$")) { - return false; - } - - if (NameUtils.isReaderName(methodName, readPrefixes) && executableElement.getParameters().isEmpty()) { - return true; - } else { - return NameUtils.isWriterName(methodName, writePrefixes) && executableElement.getParameters().size() == 1; - } + @Override + public Object visitDeclared(DeclaredType type, Object o) { + Element element = type.asElement(); + if (isAcceptable(element)) { + for (Element enclosedElement : element.getEnclosedElements()) { + if (JavaModelUtils.isRecordComponent(enclosedElement) || enclosedElement instanceof ExecutableElement) { + if (enclosedElement.getKind() != ElementKind.CONSTRUCTOR) { + accept(type, enclosedElement, o); } } - return false; } + } + return o; + } - @Override - protected void accept(DeclaredType declaringType, javax.lang.model.element.Element element, Object o) { + @Override + protected void accept(DeclaredType type, Element element, Object o) { + String name = element.getSimpleName().toString(); + if (element instanceof ExecutableElement) { + if (recordComponents.contains(name)) { + methodElements.add( + new JavaMethodElement(JavaClassElement.this, (ExecutableElement) element, elementAnnotationMetadataFactory, visitorContext) + ); + } + } else if (element instanceof VariableElement) { + recordComponents.add(name); + } + } - if (element instanceof VariableElement) { - fields.put(element.getSimpleName().toString(), (VariableElement) element); - return; - } + }, null); + return methodElements; + } - ExecutableElement executableElement = (ExecutableElement) element; - String methodName = executableElement.getSimpleName().toString(); - final TypeElement declaringTypeElement = (TypeElement) executableElement.getEnclosingElement(); - - if (NameUtils.isReaderName(methodName, readPrefixes) && executableElement.getParameters().isEmpty()) { - String propertyName = isKotlinClass(element.getEnclosingElement()) && methodName.startsWith(PREFIX_IS) ? - methodName : NameUtils.getPropertyNameForGetter(methodName, readPrefixes); - TypeMirror returnType = executableElement.getReturnType(); - ClassElement getterReturnType; - if (returnType instanceof TypeVariable) { - TypeVariable tv = (TypeVariable) returnType; - final String tvn = tv.toString(); - final ClassElement classElement = getTypeArguments().get(tvn); - if (classElement != null) { - getterReturnType = classElement; - } else { - getterReturnType = mirrorToClassElement(returnType, visitorContext, JavaClassElement.this.genericTypeInfo, true); - } - } else { - getterReturnType = mirrorToClassElement(returnType, visitorContext, JavaClassElement.this.genericTypeInfo, true); - } + private List getRecordFields() { + List fieldElements = new ArrayList<>(); + classElement.asType().accept(new SuperclassAwareTypeVisitor(visitorContext) { - BeanPropertyData beanPropertyData = props.computeIfAbsent(propertyName, BeanPropertyData::new); - configureDeclaringType(declaringTypeElement, beanPropertyData); - beanPropertyData.type = getterReturnType; - beanPropertyData.getter = executableElement; - if (beanPropertyData.setter != null) { - TypeMirror typeMirror = beanPropertyData.setter.getParameters().get(0).asType(); - ClassElement setterParameterType = mirrorToClassElement(typeMirror, visitorContext, JavaClassElement.this.genericTypeInfo, true); - if (!setterParameterType.isAssignable(getterReturnType)) { - beanPropertyData.setter = null; // not a compatible setter - } - } - } else if (NameUtils.isWriterName(methodName, writePrefixes) && executableElement.getParameters().size() == 1) { - String propertyName = NameUtils.getPropertyNameForSetter(methodName, writePrefixes); - TypeMirror typeMirror = executableElement.getParameters().get(0).asType(); - ClassElement setterParameterType = mirrorToClassElement(typeMirror, visitorContext, JavaClassElement.this.genericTypeInfo, true); - - BeanPropertyData beanPropertyData = props.computeIfAbsent(propertyName, BeanPropertyData::new); - configureDeclaringType(declaringTypeElement, beanPropertyData); - ClassElement propertyType = beanPropertyData.type; - if (propertyType != null) { - if (propertyType.getName().equals(setterParameterType.getName())) { - beanPropertyData.setter = executableElement; - } - } else { - beanPropertyData.setter = executableElement; - } - } - } + @Override + protected boolean isAcceptable(Element element) { + return JavaModelUtils.isRecord(element); + } - private void configureDeclaringType(TypeElement declaringTypeElement, BeanPropertyData beanPropertyData) { - if (beanPropertyData.declaringType == null && !classElement.equals(declaringTypeElement)) { - beanPropertyData.declaringType = mirrorToClassElement( - declaringTypeElement.asType(), - visitorContext, - genericTypeInfo, - true); - } else if (beanPropertyData.declaringType == null) { - beanPropertyData.declaringType = mirrorToClassElement( - declaringTypeElement.asType(), - visitorContext, - genericTypeInfo, - false); + @Override + public Object visitDeclared(DeclaredType type, Object o) { + Element element = type.asElement(); + if (isAcceptable(element)) { + List enclosedElements = element.getEnclosedElements(); + for (Element enclosedElement : enclosedElements) { + if ((JavaModelUtils.isRecordComponent(enclosedElement) + || enclosedElement instanceof ExecutableElement) + && enclosedElement.getKind() != ElementKind.CONSTRUCTOR) { + accept(type, enclosedElement, o); } } - }, null); + } + return o; } - if (!props.isEmpty()) { - this.beanProperties = new ArrayList<>(props.size()); - for (Map.Entry entry : props.entrySet()) { - String propertyName = entry.getKey(); - BeanPropertyData value = entry.getValue(); - final VariableElement fieldElement = fields.get(propertyName); - - if (value.getter != null) { - final AnnotationMetadata annotationMetadata; - List parents = new ArrayList<>(); - if (fieldElement != null) { - parents.add(fieldElement); - } - if (value.setter != null) { - parents.add(value.setter); - } - if (!parents.isEmpty()) { - annotationMetadata = visitorContext.getAnnotationUtils().getAnnotationMetadata(parents, value.getter); - } else { - annotationMetadata = visitorContext - .getAnnotationUtils() - .newAnnotationBuilder().buildForMethod(value.getter); - } - - JavaPropertyElement propertyElement = new JavaPropertyElement( - value.declaringType == null ? this : value.declaringType, - value.getter, - annotationMetadata, - propertyName, - value.type, - value.setter == null, - visitorContext) { - - @Override - public ClassElement getGenericType() { - TypeMirror propertyType = value.getter.getReturnType(); - Map> declaredGenericInfo = getGenericTypeInfo(); - ClassElement typeElement = parameterizedClassElement(propertyType, visitorContext, declaredGenericInfo); - if (typeElement instanceof JavaClassElement && fieldElement != null) { - TypeMirror fieldType = fieldElement.asType(); - if (visitorContext.getTypes().isAssignable(fieldType, propertyType)) { - ClassElement fieldElement = parameterizedClassElement(fieldType, visitorContext, declaredGenericInfo); - int typeGenericsSize = typeElement.getBoundGenericTypes().size(); - if (fieldElement instanceof JavaClassElement - && typeGenericsSize > 0 && typeGenericsSize == fieldElement.getBoundGenericTypes().size()) { - return ((JavaClassElement) typeElement).withBoundGenericTypeMirrors(((JavaClassElement) fieldElement).typeArguments); - } - } - } - return typeElement; - } - - @Override - public Optional getDocumentation() { - Elements elements = visitorContext.getElements(); - String docComment = elements.getDocComment(value.getter); - return Optional.ofNullable(docComment); - } - - @Override - public Optional getWriteMethod() { - if (value.setter != null) { - return Optional.of(new JavaMethodElement( - JavaClassElement.this, - value.setter, - visitorContext.getAnnotationUtils().newAnnotationBuilder().buildForMethod(value.setter), - visitorContext - )); - } - return Optional.empty(); - } - - @Override - public Optional getReadMethod() { - return Optional.of(new JavaMethodElement( - JavaClassElement.this, - value.getter, - annotationMetadata, - visitorContext - )); - } - }; - beanProperties.add(propertyElement); - } + @Override + protected void accept(DeclaredType type, Element element, Object o) { + if (element instanceof VariableElement) { + fieldElements.add( + new JavaFieldElement(JavaClassElement.this, (VariableElement) element, elementAnnotationMetadataFactory, visitorContext) + ); } - this.beanProperties = Collections.unmodifiableList(beanProperties); - } else { - this.beanProperties = Collections.emptyList(); } - } - return Collections.unmodifiableList(beanProperties); + + }, null); + return fieldElements; } private boolean isKotlinClass(Element element) { @@ -539,316 +472,262 @@ private boolean isKotlinClass(Element element) { public List getEnclosedElements(@NonNull ElementQuery query) { Objects.requireNonNull(query, "Query cannot be null"); ElementQuery.Result result = query.result(); + Set excludeElements; + if (result.isExcludePropertyElements()) { + excludeElements = new HashSet<>(); + for (PropertyElement excludePropertyElement : getBeanProperties()) { + excludePropertyElement.getReadMethod().ifPresent(methodElement -> excludeElements.add((Element) methodElement.getNativeType())); + excludePropertyElement.getWriteMethod().ifPresent(methodElement -> excludeElements.add((Element) methodElement.getNativeType())); + excludePropertyElement.getField().ifPresent(fieldElement -> excludeElements.add((Element) fieldElement.getNativeType())); + } + } else { + excludeElements = Collections.emptySet(); + } + Elements elements = visitorContext.getElements(); ElementKind kind = getElementKind(result.getElementType()); - List resultingElements = new ArrayList<>(); - List enclosedElements = new ArrayList<>(getDeclaredEnclosedElements()); - - boolean onlyDeclared = result.isOnlyDeclared(); - boolean onlyAbstract = result.isOnlyAbstract(); - boolean onlyConcrete = result.isOnlyConcrete(); - boolean onlyInstance = result.isOnlyInstance(); - boolean includeEnumConstants = result.isIncludeEnumConstants(); - boolean includeOverriddenMethods = result.isIncludeOverriddenMethods(); - boolean includeHiddenElements = result.isIncludeHiddenElements(); - - if (!onlyDeclared) { - Elements elements = visitorContext.getElements(); - - TypeMirror superclass = classElement.getSuperclass(); - // traverse the super class true and add elements that are not overridden - while (superclass instanceof DeclaredType) { - DeclaredType dt = (DeclaredType) superclass; - TypeElement element = (TypeElement) dt.asElement(); - // reached non-accessible class like Object, Enum, Record etc. - if (element.getQualifiedName().toString().startsWith("java.lang.")) { - break; - } - List superElements = element.getEnclosedElements(); - - List elementsToAdd = new ArrayList<>(superElements.size()); - superElements: - for (Element superElement : superElements) { - ElementKind superKind = superElement.getKind(); - if (superKind == kind) { - for (Element enclosedElement : enclosedElements) { - if (!includeHiddenElements && elements.hides(enclosedElement, superElement)) { - continue superElements; - } else if (enclosedElement.getKind() == ElementKind.METHOD && superElement.getKind() == ElementKind.METHOD) { - final ExecutableElement methodCandidate = (ExecutableElement) superElement; - if (!includeOverriddenMethods && elements.overrides((ExecutableElement) enclosedElement, methodCandidate, this.classElement)) { - continue superElements; - } - } - } - // dependency injection method resolution requires extended overrides checks - if (result.isOnlyInjected() && superElement.getKind() == ElementKind.METHOD) { - final ExecutableElement methodCandidate = (ExecutableElement) superElement; - // check for extended override - final String thisClassName = this.classElement.getQualifiedName().toString(); - final String declaringClassName = element.getQualifiedName().toString(); - boolean isParent = !declaringClassName.equals(thisClassName); - final ModelUtils javaModelUtils = visitorContext.getModelUtils(); - final ExecutableElement overridingMethod = javaModelUtils - .overridingOrHidingMethod(methodCandidate, this.classElement, false) - .orElse(methodCandidate); - TypeElement overridingClass = javaModelUtils.classElementFor(overridingMethod); - boolean overridden = isParent && overridingClass != null && - !overridingClass.getQualifiedName().toString().equals(declaringClassName); - - boolean isPackagePrivate = javaModelUtils.isPackagePrivate(methodCandidate); - boolean isPrivate = methodCandidate.getModifiers().contains(Modifier.PRIVATE); - if (overridden && !(isPrivate || isPackagePrivate)) { - // bail out if the method has been overridden, since it will have already been handled - continue; - } - if (isParent && overridden) { - - boolean overriddenInjected = overridden && visitorContext.getAnnotationUtils() - .getAnnotationMetadata(overridingMethod).hasDeclaredAnnotation( - AnnotationUtil.INJECT); - String packageOfOverridingClass = visitorContext.getElements().getPackageOf(overridingMethod).getQualifiedName().toString(); - String packageOfDeclaringClass = visitorContext.getElements().getPackageOf(element).getQualifiedName().toString(); - boolean isPackagePrivateAndPackagesDiffer = overridden && isPackagePrivate && - !packageOfOverridingClass.equals(packageOfDeclaringClass); - if (!overriddenInjected && !isPackagePrivateAndPackagesDiffer && !isPrivate) { - // bail out if the overridden method is package private and in the same package - // and is not annotated with @Inject - continue; - } - } - } - - if (onlyAbstract && !superElement.getModifiers().contains(Modifier.ABSTRACT)) { - continue; - } else if (onlyConcrete && superElement.getModifiers().contains(Modifier.ABSTRACT)) { - continue; - } else if (onlyInstance && superElement.getModifiers().contains(Modifier.STATIC)) { - continue; - } - elementsToAdd.add(superElement); + Predicate predicate = element -> { + Element enclosingElement = element.getEnclosingElement(); + if (enclosingElement instanceof TypeElement + && ((TypeElement) enclosingElement).getQualifiedName().toString().equals(Enum.class.getName()) + && element.getKind() == ElementKind.FIELD) { + // Skip any fields on Enum but allow to query methods + return false; + } + ElementKind enclosedElementKind = element.getKind(); + return enclosedElementKind == kind + || result.isIncludeEnumConstants() && kind == ElementKind.FIELD && enclosedElementKind == ElementKind.ENUM_CONSTANT + || (enclosedElementKind == ElementKind.ENUM && kind == ElementKind.CLASS); + }; + + Predicate filter = element -> { + if (excludeElements.contains(element.getNativeType())) { + return false; + } + List> elementPredicates = result.getElementPredicates(); + if (!elementPredicates.isEmpty()) { + for (Predicate elementPredicate : elementPredicates) { + if (!elementPredicate.test((T) element)) { + return false; } } - enclosedElements.addAll(elementsToAdd); - superclass = element.getSuperclass(); } - - if (kind == ElementKind.METHOD || kind == ElementKind.FIELD) { - // if the element kind is interfaces then we need to go through interfaces as well - Set allInterfaces = visitorContext.getModelUtils().getAllInterfaces(this.classElement); - Collection interfacesToProcess = new ArrayList<>(allInterfaces.size()); - // Remove duplicates - outer: - for (TypeElement el : allInterfaces) { - for (TypeElement existingEl : interfacesToProcess) { - Name qualifiedName = existingEl.getQualifiedName(); - if (qualifiedName.equals(el.getQualifiedName())) { - continue outer; - } + if (element instanceof MethodElement) { + MethodElement methodElement = (MethodElement) element; + if (result.isOnlyAbstract()) { + if (methodElement.getDeclaringType().isInterface() && methodElement.isDefault()) { + return false; + } else if (!element.isAbstract()) { + return false; } - interfacesToProcess.add(el); - } - List elementsToAdd = new ArrayList<>(allInterfaces.size()); - for (TypeElement itfe : interfacesToProcess) { - List interfaceElements = itfe.getEnclosedElements(); - interfaceElements: - for (Element interfaceElement : interfaceElements) { - if (interfaceElement.getKind() == ElementKind.METHOD) { - ExecutableElement ee = (ExecutableElement) interfaceElement; - if (onlyAbstract && ee.getModifiers().contains(Modifier.DEFAULT)) { - continue; - } else if (onlyConcrete && !ee.getModifiers().contains(Modifier.DEFAULT)) { - continue; - } - - for (Element enclosedElement : enclosedElements) { - if (enclosedElement.getKind() == ElementKind.METHOD) { - if (!includeOverriddenMethods && elements.overrides((ExecutableElement) enclosedElement, ee, this.classElement)) { - continue interfaceElements; - } - } - } - } - elementsToAdd.add(interfaceElement); + } else if (result.isOnlyConcrete()) { + if (methodElement.getDeclaringType().isInterface() && !methodElement.isDefault()) { + return false; + } else if (element.isAbstract()) { + return false; } } - enclosedElements.addAll(elementsToAdd); - elementsToAdd.clear(); } - if (onlyAbstract) { - if (isInterface()) { - enclosedElements.removeIf((e) -> e.getModifiers().contains(Modifier.DEFAULT)); - } else { - enclosedElements.removeIf((e) -> !e.getModifiers().contains(Modifier.ABSTRACT)); + if (result.isOnlyInstance() && element.isStatic()) { + return false; + } else if (result.isOnlyStatic() && !element.isStatic()) { + return false; + } + if (result.isOnlyAccessible()) { + // exclude private members + // exclude synthetic members or bridge methods that start with $ + if (element.isPrivate() || element.getName().startsWith("$")) { + return false; } - } else if (onlyConcrete) { - if (isInterface()) { - enclosedElements.removeIf((e) -> !e.getModifiers().contains(Modifier.DEFAULT)); - } else { - enclosedElements.removeIf((e) -> e.getModifiers().contains(Modifier.ABSTRACT)); + if (element instanceof MemberElement && !((MemberElement) element).isAccessible()) { + return false; } } - } - if (onlyInstance) { - enclosedElements.removeIf((e) -> e.getModifiers().contains(Modifier.STATIC)); - } - List>> modifierPredicates = result.getModifierPredicates(); - List> namePredicates = result.getNamePredicates(); - List> annotationPredicates = result.getAnnotationPredicates(); - final List> typePredicates = result.getTypePredicates(); - boolean hasNamePredicates = !namePredicates.isEmpty(); - boolean hasModifierPredicates = !modifierPredicates.isEmpty(); - boolean hasAnnotationPredicates = !annotationPredicates.isEmpty(); - boolean hasTypePredicates = !typePredicates.isEmpty(); - boolean onlyAccessible = result.isOnlyAccessible(); - final JavaElementFactory elementFactory = visitorContext.getElementFactory(); - - elementLoop: - for (Element enclosedElement : enclosedElements) { - ElementKind enclosedElementKind = enclosedElement.getKind(); - if (enclosedElementKind == kind - || includeEnumConstants && kind == ElementKind.FIELD && enclosedElementKind == ElementKind.ENUM_CONSTANT - || (enclosedElementKind == ElementKind.ENUM && kind == ElementKind.CLASS)) { - String elementName = enclosedElement.getSimpleName().toString(); - if (onlyAccessible) { - // exclude private members - if (enclosedElement.getModifiers().contains(Modifier.PRIVATE)) { - continue; - } else if (elementName.startsWith("$")) { - // exclude synthetic members or bridge methods that start with $ - continue; - } else { - Element enclosingElement = enclosedElement.getEnclosingElement(); - final ClassElement onlyAccessibleFrom = result.getOnlyAccessibleFromType().orElse(this); - Object accessibleFrom = onlyAccessibleFrom.getNativeType(); - // if the outer element of the enclosed element is not the current class - // we need to check if it package private and within a different package so it can be excluded - if (enclosingElement != accessibleFrom && visitorContext.getModelUtils().isPackagePrivate(enclosedElement)) { - if (enclosingElement instanceof TypeElement) { - Name qualifiedName = ((TypeElement) enclosingElement).getQualifiedName(); - String packageName = NameUtils.getPackageName(qualifiedName.toString()); - if (!packageName.equals(onlyAccessibleFrom.getPackageName())) { - continue; - } - } - } + if (!result.getModifierPredicates().isEmpty()) { + Set modifiers = element.getModifiers(); + for (Predicate> modifierPredicate : result.getModifierPredicates()) { + if (!modifierPredicate.test(modifiers)) { + return false; } } - - if (hasModifierPredicates) { - Set modifiers = enclosedElement - .getModifiers().stream().map(m -> ElementModifier.valueOf(m.name())).collect(Collectors.toSet()); - for (Predicate> modifierPredicate : modifierPredicates) { - if (!modifierPredicate.test(modifiers)) { - continue elementLoop; - } + } + if (!result.getNamePredicates().isEmpty()) { + for (Predicate namePredicate : result.getNamePredicates()) { + if (!namePredicate.test(element.getName())) { + return false; } } - - if (hasNamePredicates) { - for (Predicate namePredicate : namePredicates) { - if (!namePredicate.test(elementName)) { - continue elementLoop; - } + } + if (!result.getAnnotationPredicates().isEmpty()) { + for (Predicate annotationPredicate : result.getAnnotationPredicates()) { + if (!annotationPredicate.test(element)) { + return false; } } - - final AnnotationMetadata metadata = visitorContext.getAnnotationUtils().getAnnotationMetadata(enclosedElement); - if (hasAnnotationPredicates) { - for (Predicate annotationPredicate : annotationPredicates) { - if (!annotationPredicate.test(metadata)) { - continue elementLoop; + } + if (!result.getTypePredicates().isEmpty()) { + for (Predicate typePredicate : result.getTypePredicates()) { + ClassElement classElement; + if (element instanceof ConstructorElement) { + classElement = JavaClassElement.this; + } else if (element instanceof MethodElement) { + classElement = ((MethodElement) element).getGenericReturnType(); + } else if (element instanceof ClassElement) { + classElement = (ClassElement) element; + } else { + classElement = ((FieldElement) element).getGenericField(); + } + if (!typePredicate.test(classElement)) { + return false; + } + } + } +// TODO: FIX only injected +// if (result.isOnlyInjected() && !element.hasDeclaredAnnotation(AnnotationUtil.INJECT)) { +// return false; +// } + return true; + }; + + BiPredicate reduce; + if (result.isIncludeHiddenElements() && result.isIncludeOverriddenMethods()) { + reduce = (t1, t2) -> false; + } else { + reduce = (newElement, existingElement) -> { + if (!result.isIncludeHiddenElements() && hidden(elements, newElement, existingElement)) { + return true; + } + if (!result.isIncludeOverriddenMethods()) { + if (kind == ElementKind.METHOD) { + if (newElement instanceof MethodElement && existingElement instanceof MethodElement) { + return ((MethodElement) newElement).overrides((MethodElement) existingElement); } } } + return false; + }; + } + return (List) Collections.unmodifiableList( + getAllElements(classElement, result.isOnlyDeclared(), predicate, reduce) + .stream() + .filter(filter) + .collect(Collectors.toList()) + ); + } - T element; - - switch (enclosedElementKind) { - case METHOD: - - final ExecutableElement executableElement = (ExecutableElement) enclosedElement; - //noinspection unchecked - element = (T) elementFactory.newMethodElement( - this, - executableElement, - metadata, - genericTypeInfo - ); - break; - case FIELD: - //noinspection unchecked - element = (T) elementFactory.newFieldElement( - this, - (VariableElement) enclosedElement, - metadata - ); - break; - case ENUM_CONSTANT: - //noinspection unchecked - element = (T) elementFactory.newEnumConstantElement( - this, - (VariableElement) enclosedElement, - metadata - ); - break; - case CONSTRUCTOR: - //noinspection unchecked - element = (T) elementFactory.newConstructorElement( - this, - (ExecutableElement) enclosedElement, - metadata - ); - break; - case CLASS: - case ENUM: - //noinspection unchecked - element = (T) elementFactory.newClassElement( - (TypeElement) enclosedElement, - metadata - ); - break; - default: - element = null; - } + private static boolean hidden(Elements elements, io.micronaut.inject.ast.Element newElement, io.micronaut.inject.ast.Element existingElement) { + if (newElement instanceof MemberElement) { + if (newElement.isStatic() && ((MemberElement) newElement).getDeclaringType().isInterface()) { + return false; + } + } + return elements.hides((Element) newElement.getNativeType(), (Element) existingElement.getNativeType()); + } - if (element != null) { - if (hasTypePredicates) { - for (Predicate typePredicate : typePredicates) { - ClassElement classElement; - if (element instanceof ConstructorElement) { - classElement = this; - } else if (element instanceof MethodElement) { - classElement = ((MethodElement) element).getGenericReturnType(); - } else if (element instanceof ClassElement) { - classElement = (ClassElement) element; - } else { - classElement = ((FieldElement) element).getGenericField(); - } - if (!typePredicate.test(classElement)) { - continue elementLoop; - } - } + private Collection getAllElements(TypeElement classNode, + boolean onlyDeclared, + Predicate predicate, + BiPredicate reduce) { + Set elements = new LinkedHashSet<>(); + List> hierarchy = new ArrayList<>(); + collectHierarchy(classNode, onlyDeclared, predicate, hierarchy); + for (List classElements : hierarchy) { + Set addedFromClassElements = new LinkedHashSet<>(); + classElements: + for (Element element : classElements) { + io.micronaut.inject.ast.Element newElement = elementsCache.computeIfAbsent(element, this::toAstElement); + for (Iterator iterator = elements.iterator(); iterator.hasNext(); ) { + io.micronaut.inject.ast.Element existingElement = iterator.next(); + if (newElement.equals(existingElement)) { + continue; } - List> elementPredicates = result.getElementPredicates(); - if (!elementPredicates.isEmpty()) { - for (Predicate elementPredicate : elementPredicates) { - if (!elementPredicate.test(element)) { - continue elementLoop; - } - } + if (reduce.test(newElement, existingElement)) { + iterator.remove(); + addedFromClassElements.add(newElement); + } else if (reduce.test(existingElement, newElement)) { + continue classElements; } - resultingElements.add(element); } + addedFromClassElements.add(newElement); + } + elements.addAll(addedFromClassElements); + } + return elements; + } + + private void collectHierarchy(TypeElement classNode, + boolean onlyDeclared, + Predicate predicate, + List> hierarchy) { + if (classNode.getQualifiedName().toString().equals(Object.class.getName()) + || classNode.getQualifiedName().toString().equals(Enum.class.getName())) { + return; + } + if (!onlyDeclared) { + TypeMirror superclass = classNode.getSuperclass(); + if (superclass instanceof DeclaredType) { + DeclaredType dt = (DeclaredType) superclass; + TypeElement element = (TypeElement) dt.asElement(); + collectHierarchy(element, false, predicate, hierarchy); + } + for (TypeMirror ifaceMirror : classNode.getInterfaces()) { + final Element ifaceEl = visitorContext.getTypes().asElement(ifaceMirror); + if (ifaceEl instanceof TypeElement) { + TypeElement iface = (TypeElement) ifaceEl; + List> interfaceElements = new ArrayList<>(); + collectHierarchy(iface, false, predicate, interfaceElements); + hierarchy.addAll(interfaceElements); + } + } + } + List enclosedElements; + if (classNode == classElement) { + if (this.enclosedElements == null) { + this.enclosedElements = classElement.getEnclosedElements(); } + enclosedElements = this.enclosedElements; + } else { + enclosedElements = classNode.getEnclosedElements(); } - return Collections.unmodifiableList(resultingElements); + hierarchy.add(enclosedElements.stream().filter(predicate).collect(Collectors.toList())); } - private List getDeclaredEnclosedElements() { - if (this.enclosedElements == null) { - this.enclosedElements = classElement.getEnclosedElements(); + private io.micronaut.inject.ast.Element toAstElement(Element enclosedElement) { + final JavaElementFactory elementFactory = visitorContext.getElementFactory(); + switch (enclosedElement.getKind()) { + case METHOD: + return elementFactory.newMethodElement( + JavaClassElement.this, + (ExecutableElement) enclosedElement, + elementAnnotationMetadataFactory, + genericTypeInfo + ); + case FIELD: + return elementFactory.newFieldElement( + JavaClassElement.this, + (VariableElement) enclosedElement, + elementAnnotationMetadataFactory + ); + case ENUM_CONSTANT: + return elementFactory.newEnumConstantElement( + JavaClassElement.this, + (VariableElement) enclosedElement, + elementAnnotationMetadataFactory + ); + case CONSTRUCTOR: + return elementFactory.newConstructorElement( + JavaClassElement.this, + (ExecutableElement) enclosedElement, + elementAnnotationMetadataFactory + ); + case CLASS: + case ENUM: + return elementFactory.newClassElement( + (TypeElement) enclosedElement, + elementAnnotationMetadataFactory + ); + default: + return null; } - return this.enclosedElements; } private ElementKind getElementKind(Class elementType) { @@ -879,7 +758,7 @@ public ClassElement withArrayDimensions(int arrayDimensions) { if (arrayDimensions == this.arrayDimensions) { return this; } - return new JavaClassElement(classElement, getAnnotationMetadata(), visitorContext, typeArguments, getGenericTypeInfo(), arrayDimensions, false); + return new JavaClassElement(classElement, elementAnnotationMetadataFactory, visitorContext, typeArguments, getGenericTypeInfo(), arrayDimensions, false); } @Override @@ -913,10 +792,11 @@ public PackageElement getPackage() { enclosingElement = enclosingElement.getEnclosingElement(); } if (enclosingElement instanceof javax.lang.model.element.PackageElement) { + javax.lang.model.element.PackageElement packageElement = (javax.lang.model.element.PackageElement) enclosingElement; return new JavaPackageElement( - ((javax.lang.model.element.PackageElement) enclosingElement), - visitorContext.getAnnotationUtils().getAnnotationMetadata(enclosingElement), - visitorContext + packageElement, + elementAnnotationMetadataFactory, + visitorContext ); } else { return PackageElement.DEFAULT_PACKAGE; @@ -928,10 +808,7 @@ public PackageElement getPackage() { public boolean isAssignable(String type) { TypeElement otherElement = visitorContext.getElements().getTypeElement(type); if (otherElement != null) { - Types types = visitorContext.getTypes(); - TypeMirror thisType = types.erasure(classElement.asType()); - TypeMirror thatType = types.erasure(otherElement.asType()); - return types.isAssignable(thisType, thatType); + return isAssignable(otherElement); } return false; } @@ -940,48 +817,59 @@ public boolean isAssignable(String type) { public boolean isAssignable(ClassElement type) { if (type.isPrimitive()) { return isAssignable(type.getName()); - } else { - Object nativeType = type.getNativeType(); - if (nativeType instanceof TypeElement) { - Types types = visitorContext.getTypes(); - TypeMirror thisType = types.erasure(classElement.asType()); - TypeMirror thatType = types.erasure(((TypeElement) nativeType).asType()); - return types.isAssignable(thisType, thatType); - } } - return false; + Object nativeType = type.getNativeType(); + if (nativeType instanceof TypeElement) { + return isAssignable((TypeElement) nativeType); + } + return isAssignable(type.getName()); + } + + private boolean isAssignable(TypeElement otherElement) { + Types types = visitorContext.getTypes(); + TypeMirror thisType = types.erasure(classElement.asType()); + TypeMirror thatType = types.erasure(otherElement.asType()); + return types.isAssignable(thisType, thatType); } @NonNull @Override public Optional getPrimaryConstructor() { - final AnnotationUtils annotationUtils = visitorContext.getAnnotationUtils(); - final ModelUtils modelUtils = visitorContext.getModelUtils(); - ExecutableElement method = modelUtils.staticCreatorFor(classElement, annotationUtils); - if (method == null) { + if (JavaModelUtils.isRecord(classElement)) { + Optional staticCreator = findStaticCreator(); + if (staticCreator.isPresent()) { + return staticCreator; + } if (isInner() && !isStatic()) { // only static inner classes can be constructed return Optional.empty(); } - method = modelUtils.concreteConstructorFor(classElement, annotationUtils); + List constructors = getAccessibleConstructors(); + Optional annotatedConstructor = constructors.stream() + .filter(c -> c.hasStereotype(AnnotationUtil.INJECT) || c.hasStereotype(Creator.class)) + .findFirst(); + if (annotatedConstructor.isPresent()) { + return annotatedConstructor.map(c -> c); + } + // with records the record constructor is always the last constructor + return Optional.of(constructors.get(constructors.size() - 1)); } - - return createMethodElement(annotationUtils, method); + return ArrayableClassElement.super.getPrimaryConstructor(); } @Override - public Optional getDefaultConstructor() { - final AnnotationUtils annotationUtils = visitorContext.getAnnotationUtils(); - final ModelUtils modelUtils = visitorContext.getModelUtils(); - ExecutableElement method = modelUtils.defaultStaticCreatorFor(classElement, annotationUtils); - if (method == null) { - if (isInner() && !isStatic()) { - // only static inner classes can be constructed - return Optional.empty(); - } - method = modelUtils.defaultConstructorFor(classElement); + public List getAccessibleStaticCreators() { + List staticCreators = new ArrayList<>(ArrayableClassElement.super.getAccessibleStaticCreators()); + if (!staticCreators.isEmpty()) { + return staticCreators; } - return createMethodElement(annotationUtils, method); + return visitorContext.getClassElement(getName() + "$Companion", elementAnnotationMetadataFactory) + .filter(io.micronaut.inject.ast.Element::isStatic) + .flatMap(typeElement -> typeElement.getEnclosedElements(ElementQuery.ALL_METHODS + .annotated(annotationMetadata -> annotationMetadata.hasStereotype(Creator.class))).stream().findFirst() + ) + .filter(method -> !method.isPrivate() && method.getReturnType().equals(this)) + .map(Collections::singletonList).orElse(Collections.emptyList()); } @Override @@ -989,52 +877,42 @@ public Optional getEnclosingType() { if (isInner()) { Element enclosingElement = this.classElement.getEnclosingElement(); if (enclosingElement instanceof TypeElement) { + TypeElement typeElement = (TypeElement) enclosingElement; return Optional.of(visitorContext.getElementFactory().newClassElement( - ((TypeElement) enclosingElement), - visitorContext.getAnnotationUtils().getAnnotationMetadata(enclosingElement) + typeElement, + elementAnnotationMetadataFactory )); } } return Optional.empty(); } - private Optional createMethodElement(AnnotationUtils annotationUtils, ExecutableElement method) { - return Optional.ofNullable(method).map(executableElement -> { - final AnnotationMetadata annotationMetadata = annotationUtils.getAnnotationMetadata(executableElement); - if (executableElement.getKind() == ElementKind.CONSTRUCTOR) { - return new JavaConstructorElement(this, executableElement, annotationMetadata, visitorContext); - } else { - return new JavaMethodElement(this, executableElement, annotationMetadata, visitorContext); - } - }); - } - @NonNull @Override public List getBoundGenericTypes() { return typeArguments.stream() - //return getGenericTypeInfo().getOrDefault(classElement.getQualifiedName().toString(), Collections.emptyMap()).values().stream() - .map(tm -> mirrorToClassElement(tm, visitorContext, getGenericTypeInfo())) - .collect(Collectors.toList()); + //return getGenericTypeInfo().getOrDefault(classElement.getQualifiedName().toString(), Collections.emptyMap()).values().stream() + .map(tm -> mirrorToClassElement(tm, visitorContext, getGenericTypeInfo())) + .collect(Collectors.toList()); } @NonNull @Override public List getDeclaredGenericPlaceholders() { return classElement.getTypeParameters().stream() - // we want the *declared* variables, so we don't pass in our genericsInfo. - .map(tpe -> (GenericPlaceholderElement) mirrorToClassElement(tpe.asType(), visitorContext)) - .collect(Collectors.toList()); + // we want the *declared* variables, so we don't pass in our genericsInfo. + .map(tpe -> (GenericPlaceholderElement) mirrorToClassElement(tpe.asType(), visitorContext)) + .collect(Collectors.toList()); } @NonNull @Override public ClassElement getRawClassElement() { - return visitorContext.getElementFactory().newClassElement(classElement, visitorContext.getAnnotationUtils().getAnnotationMetadata(classElement)) - .withArrayDimensions(getArrayDimensions()); + return visitorContext.getElementFactory().newClassElement(classElement, elementAnnotationMetadataFactory) + .withArrayDimensions(getArrayDimensions()); } - private static TypeMirror toTypeMirror(JavaVisitorContext visitorContext, ClassElement element) { + private TypeMirror toTypeMirror(JavaVisitorContext visitorContext, ClassElement element) { if (element.isArray()) { return visitorContext.getTypes().getArrayType(toTypeMirror(visitorContext, element.fromArray())); } else if (element.isWildcard()) { @@ -1061,12 +939,13 @@ private static TypeMirror toTypeMirror(JavaVisitorContext visitorContext, ClassE } else { if (element instanceof JavaClassElement) { return visitorContext.getTypes().getDeclaredType( - ((JavaClassElement) element).classElement, - ((JavaClassElement) element).typeArguments.toArray(new TypeMirror[0])); + ((JavaClassElement) element).classElement, + ((JavaClassElement) element).typeArguments.toArray(new TypeMirror[0])); } else { + ClassElement classElement1 = visitorContext.getRequiredClassElement(element.getName(), elementAnnotationMetadataFactory); return visitorContext.getTypes().getDeclaredType( - ((JavaClassElement) visitorContext.getClassElement(element.getName()).get()).classElement, - element.getBoundGenericTypes().stream().map(ce -> toTypeMirror(visitorContext, ce)).toArray(TypeMirror[]::new)); + ((JavaClassElement) classElement1).classElement, + element.getBoundGenericTypes().stream().map(ce -> toTypeMirror(visitorContext, ce)).toArray(TypeMirror[]::new)); } } } @@ -1079,8 +958,8 @@ public ClassElement withBoundGenericTypes(@NonNull List } List typeMirrors = typeArguments.stream() - .map(ce -> toTypeMirror(visitorContext, ce)) - .collect(Collectors.toList()); + .map(ce -> toTypeMirror(visitorContext, ce)) + .collect(Collectors.toList()); return withBoundGenericTypeMirrors(typeMirrors); } @@ -1097,12 +976,19 @@ private ClassElement withBoundGenericTypeMirrors(@NonNull List> genericsInfo = visitorContext.getGenericUtils().buildGenericTypeArgumentElementInfo(classElement, null, boundByName); - return new JavaClassElement(classElement, getAnnotationMetadata(), visitorContext, typeMirrors, genericsInfo, arrayDimensions); + return new JavaClassElement(classElement, elementAnnotationMetadataFactory, visitorContext, typeMirrors, genericsInfo, arrayDimensions); } @Override - public @NonNull - Map getTypeArguments() { + @NonNull + public Map getTypeArguments() { + if (resolvedTypeArguments == null) { + resolvedTypeArguments = resolveTypeArguments(); + } + return resolvedTypeArguments; + } + + private Map resolveTypeArguments() { List typeParameters = classElement.getTypeParameters(); Iterator tpi = typeParameters.iterator(); @@ -1135,11 +1021,11 @@ private Map getBoundTypeMirrors() { public Map> getAllTypeArguments() { Map typeArguments = getBoundTypeMirrors(); Map> info = visitorContext.getGenericUtils() - .buildGenericTypeArgumentElementInfo( - classElement, - null, - typeArguments - ); + .buildGenericTypeArgumentElementInfo( + classElement, + null, + typeArguments + ); Map> result = new LinkedHashMap<>(info.size()); info.forEach((name, generics) -> { Map resolved = new LinkedHashMap<>(generics.size()); @@ -1153,11 +1039,11 @@ public Map> getAllTypeArguments() { } } ClassElement classElement = mirrorToClassElement( - resolvedType, - visitorContext, - info, - visitorContext.getConfiguration().includeTypeLevelAnnotationsInGenericArguments(), - mirror instanceof TypeVariable + resolvedType, + visitorContext, + info, + visitorContext.getConfiguration().includeTypeLevelAnnotationsInGenericArguments(), + mirror instanceof TypeVariable ); resolved.put(variable, classElement); }); @@ -1180,18 +1066,4 @@ Map> getGenericTypeInfo() { return genericTypeInfo; } - /** - * Internal holder class for getters and setters. - */ - private static class BeanPropertyData { - ClassElement type; - ClassElement declaringType; - ExecutableElement getter; - ExecutableElement setter; - final String propertyName; - - public BeanPropertyData(String propertyName) { - this.propertyName = propertyName; - } - } } diff --git a/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaConstructorElement.java b/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaConstructorElement.java index 7353b22919f..81d8450e3c4 100644 --- a/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaConstructorElement.java +++ b/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaConstructorElement.java @@ -15,10 +15,9 @@ */ package io.micronaut.annotation.processing.visitor; -import io.micronaut.core.annotation.AnnotationMetadata; import io.micronaut.core.annotation.Internal; -import io.micronaut.core.util.ArrayUtils; import io.micronaut.inject.ast.ConstructorElement; +import io.micronaut.inject.ast.ElementAnnotationMetadataFactory; import io.micronaut.inject.ast.MethodElement; import io.micronaut.inject.ast.ParameterElement; @@ -34,21 +33,24 @@ class JavaConstructorElement extends JavaMethodElement implements ConstructorElement { /** - * @param declaringClass The declaring class - * @param executableElement The {@link ExecutableElement} - * @param annotationMetadata The annotation metadata - * @param visitorContext The visitor context + * @param declaringClass The declaring class + * @param executableElement The {@link ExecutableElement} + * @param annotationMetadataFactory The annotation metadata factory + * @param visitorContext The visitor context */ - JavaConstructorElement(JavaClassElement declaringClass, ExecutableElement executableElement, AnnotationMetadata annotationMetadata, JavaVisitorContext visitorContext) { - super(declaringClass, executableElement, annotationMetadata, visitorContext); + JavaConstructorElement(JavaClassElement declaringClass, + ExecutableElement executableElement, + ElementAnnotationMetadataFactory annotationMetadataFactory, + JavaVisitorContext visitorContext) { + super(declaringClass, executableElement, annotationMetadataFactory, visitorContext); } @Override - public MethodElement withNewParameters(ParameterElement... newParameters) { - return new JavaConstructorElement(declaringClass, executableElement, getAnnotationMetadata(), visitorContext) { + public MethodElement withParameters(ParameterElement... newParameters) { + return new JavaConstructorElement(owningType, executableElement, elementAnnotationMetadataFactory, visitorContext) { @Override public ParameterElement[] getParameters() { - return ArrayUtils.concat(super.getParameters(), newParameters); + return newParameters; } }; } diff --git a/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaElementFactory.java b/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaElementFactory.java index 2667096c40b..c7ad6bb9839 100644 --- a/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaElementFactory.java +++ b/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaElementFactory.java @@ -15,16 +15,26 @@ */ package io.micronaut.annotation.processing.visitor; -import io.micronaut.annotation.processing.JavaConfigurationMetadataBuilder; +import io.micronaut.annotation.processing.JavaElementAnnotationMetadataFactory; +import io.micronaut.annotation.processing.PostponeToNextRoundException; +import io.micronaut.core.annotation.AnnotationMetadata; import io.micronaut.core.annotation.NonNull; import io.micronaut.core.annotation.Nullable; -import io.micronaut.core.annotation.AnnotationMetadata; -import io.micronaut.inject.ast.*; +import io.micronaut.inject.ast.ClassElement; +import io.micronaut.inject.ast.ElementAnnotationMetadataFactory; +import io.micronaut.inject.ast.ElementFactory; +import io.micronaut.inject.ast.MethodElement; import io.micronaut.inject.ast.beans.BeanElementBuilder; +import io.micronaut.inject.configuration.ConfigurationMetadataBuilder; -import javax.lang.model.element.*; import javax.lang.model.element.Element; +import javax.lang.model.element.ElementKind; +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.element.TypeElement; +import javax.lang.model.element.VariableElement; +import javax.lang.model.type.TypeKind; import javax.lang.model.type.TypeMirror; +import java.util.List; import java.util.Map; import java.util.Objects; @@ -42,44 +52,51 @@ public JavaElementFactory(JavaVisitorContext visitorContext) { this.visitorContext = Objects.requireNonNull(visitorContext, "Visitor context cannot be null"); } + private ElementAnnotationMetadataFactory defaultAnnotationMetadata(Object nativeType, + AnnotationMetadata annotationMetadata) { + JavaElementAnnotationMetadataFactory elementAnnotationMetadataFactory = visitorContext.getElementAnnotationMetadataFactory(); + return elementAnnotationMetadataFactory.overrideForNativeType(nativeType, element -> elementAnnotationMetadataFactory.build(element, annotationMetadata)); + } + @NonNull @Override - public JavaClassElement newClassElement( - @NonNull TypeElement type, - @NonNull AnnotationMetadata annotationMetadata) { + public JavaClassElement newClassElement(@NonNull TypeElement type, + @NonNull ElementAnnotationMetadataFactory annotationMetadataFactory) { ElementKind kind = type.getKind(); switch (kind) { case ENUM: return new JavaEnumElement( - type, - annotationMetadata, - visitorContext + type, + annotationMetadataFactory, + visitorContext ); case ANNOTATION_TYPE: return new JavaAnnotationElement( - type, - annotationMetadata, - visitorContext + type, + annotationMetadataFactory, + visitorContext ); default: return new JavaClassElement( - type, - annotationMetadata, - visitorContext + type, + annotationMetadataFactory, + visitorContext ); } } @NonNull @Override - public ClassElement newClassElement(@NonNull TypeElement type, @NonNull AnnotationMetadata annotationMetadata, @NonNull Map resolvedGenerics) { + public ClassElement newClassElement(@NonNull TypeElement type, + @NonNull ElementAnnotationMetadataFactory annotationMetadataFactory, + @NonNull Map resolvedGenerics) { ElementKind kind = type.getKind(); switch (kind) { case ENUM: return new JavaEnumElement( - type, - annotationMetadata, - visitorContext + type, + annotationMetadataFactory, + visitorContext ) { @NonNull @Override @@ -91,12 +108,12 @@ public Map getTypeArguments() { } }; case ANNOTATION_TYPE: - return new JavaAnnotationElement(type, annotationMetadata, visitorContext); + return new JavaAnnotationElement(type, annotationMetadataFactory, visitorContext); default: return new JavaClassElement( - type, - annotationMetadata, - visitorContext + type, + annotationMetadataFactory, + visitorContext ) { @NonNull @Override @@ -112,47 +129,41 @@ public Map getTypeArguments() { @NonNull @Override - public JavaClassElement newSourceClassElement(@NonNull TypeElement type, @NonNull AnnotationMetadata annotationMetadata) { + public JavaClassElement newSourceClassElement(@NonNull TypeElement type, @NonNull ElementAnnotationMetadataFactory annotationMetadataFactory) { ElementKind kind = type.getKind(); if (kind == ElementKind.ENUM) { return new JavaEnumElement( - type, - annotationMetadata, - visitorContext + type, + annotationMetadataFactory, + visitorContext ) { @NonNull @Override public BeanElementBuilder addAssociatedBean(@NonNull ClassElement type) { return new JavaBeanDefinitionBuilder( - this, - type, - new JavaConfigurationMetadataBuilder( - visitorContext.getElements(), - visitorContext.getTypes(), - visitorContext.getAnnotationUtils() - ), - visitorContext + this, + type, + ConfigurationMetadataBuilder.INSTANCE, + annotationMetadataFactory, + visitorContext ); } }; } else { return new JavaClassElement( - type, - annotationMetadata, - visitorContext + type, + annotationMetadataFactory, + visitorContext ) { @NonNull @Override public BeanElementBuilder addAssociatedBean(@NonNull ClassElement type) { return new JavaBeanDefinitionBuilder( - this, - type, - new JavaConfigurationMetadataBuilder( - visitorContext.getElements(), - visitorContext.getTypes(), - visitorContext.getAnnotationUtils() - ), - visitorContext + this, + type, + ConfigurationMetadataBuilder.INSTANCE, + annotationMetadataFactory, + visitorContext ); } }; @@ -161,28 +172,26 @@ public BeanElementBuilder addAssociatedBean(@NonNull ClassElement type) { @NonNull @Override - public JavaMethodElement newSourceMethodElement(ClassElement declaringClass, @NonNull ExecutableElement method, @NonNull AnnotationMetadata annotationMetadata) { - if (!(declaringClass instanceof JavaClassElement)) { - throw new IllegalArgumentException("Declaring class must be a JavaClassElement"); - } + public JavaMethodElement newSourceMethodElement(ClassElement declaringClass, + @NonNull ExecutableElement method, + @NonNull ElementAnnotationMetadataFactory annotationMetadataFactory) { + validateOwningClass(declaringClass); + failIfPostponeIsNeeded(method); return new JavaMethodElement( - (JavaClassElement) declaringClass, - method, - annotationMetadata, - visitorContext + (JavaClassElement) declaringClass, + method, + annotationMetadataFactory, + visitorContext ) { @NonNull @Override public BeanElementBuilder addAssociatedBean(@NonNull ClassElement type) { return new JavaBeanDefinitionBuilder( - this, - type, - new JavaConfigurationMetadataBuilder( - visitorContext.getElements(), - visitorContext.getTypes(), - visitorContext.getAnnotationUtils() - ), - visitorContext + this, + type, + ConfigurationMetadataBuilder.INSTANCE, + annotationMetadataFactory, + visitorContext ); } }; @@ -190,51 +199,47 @@ public BeanElementBuilder addAssociatedBean(@NonNull ClassElement type) { @NonNull @Override - public JavaMethodElement newMethodElement( - ClassElement declaringClass, - @NonNull ExecutableElement method, - @NonNull AnnotationMetadata annotationMetadata) { - if (!(declaringClass instanceof JavaClassElement)) { - throw new IllegalArgumentException("Declaring class must be a JavaClassElement"); - } + public JavaMethodElement newMethodElement(ClassElement owningType, + @NonNull ExecutableElement method, + @NonNull ElementAnnotationMetadataFactory annotationMetadataFactory) { + validateOwningClass(owningType); + failIfPostponeIsNeeded(method); return new JavaMethodElement( - (JavaClassElement) declaringClass, - method, - annotationMetadata, - visitorContext + (JavaClassElement) owningType, + method, + annotationMetadataFactory, + visitorContext ); } /** - * Constructs a method method element with the given generic type information. + * Constructs a method element with the given generic type information. * - * @param declaringClass The declaring class - * @param method The method - * @param annotationMetadata The annotation metadata - * @param genericTypes The generic type info + * @param declaringClass The declaring class + * @param method The method + * @param annotationMetadataFactory The annotationMetadataFactory + * @param genericTypes The generic type info * @return The method element */ - public JavaMethodElement newMethodElement( - ClassElement declaringClass, - @NonNull ExecutableElement method, - @NonNull AnnotationMetadata annotationMetadata, - @Nullable Map> genericTypes) { - if (!(declaringClass instanceof JavaClassElement)) { - throw new IllegalArgumentException("Declaring class must be a JavaClassElement"); - } + public JavaMethodElement newMethodElement(ClassElement declaringClass, + @NonNull ExecutableElement method, + @NonNull ElementAnnotationMetadataFactory annotationMetadataFactory, + @Nullable Map> genericTypes) { + validateOwningClass(declaringClass); + failIfPostponeIsNeeded(method); final JavaClassElement javaDeclaringClass = (JavaClassElement) declaringClass; final JavaVisitorContext javaVisitorContext = visitorContext; return new JavaMethodElement( - javaDeclaringClass, - method, - annotationMetadata, - javaVisitorContext + javaDeclaringClass, + method, + annotationMetadataFactory, + javaVisitorContext ) { @NonNull @Override - protected JavaParameterElement newParameterElement(@NonNull VariableElement variableElement, @NonNull AnnotationMetadata annotationMetadata1) { - return new JavaParameterElement(javaDeclaringClass, variableElement, annotationMetadata1, javaVisitorContext) { + protected JavaParameterElement newParameterElement(@NonNull MethodElement methodElement, @NonNull VariableElement variableElement) { + return new JavaParameterElement(javaDeclaringClass, methodElement, variableElement, elementAnnotationMetadataFactory, javaVisitorContext) { @NonNull @Override public ClassElement getGenericType() { @@ -261,65 +266,71 @@ public ClassElement getGenericReturnType() { @NonNull @Override - public JavaConstructorElement newConstructorElement(ClassElement declaringClass, @NonNull ExecutableElement constructor, @NonNull AnnotationMetadata annotationMetadata) { - if (!(declaringClass instanceof JavaClassElement)) { - throw new IllegalArgumentException("Declaring class must be a JavaClassElement"); - } + public JavaConstructorElement newConstructorElement(ClassElement declaringClass, + @NonNull ExecutableElement constructor, + @NonNull ElementAnnotationMetadataFactory annotationMetadataFactory) { + validateOwningClass(declaringClass); + failIfPostponeIsNeeded(constructor); return new JavaConstructorElement( - (JavaClassElement) declaringClass, - constructor, - annotationMetadata, - visitorContext + (JavaClassElement) declaringClass, + constructor, + annotationMetadataFactory, + visitorContext ); } @NonNull @Override - public JavaEnumConstantElement newEnumConstantElement(ClassElement declaringClass, @NonNull VariableElement enumConstant, @NonNull AnnotationMetadata annotationMetadata) { + public JavaEnumConstantElement newEnumConstantElement(ClassElement declaringClass, + @NonNull VariableElement enumConstant, + @NonNull ElementAnnotationMetadataFactory annotationMetadataFactory) { if (!(declaringClass instanceof JavaEnumElement)) { throw new IllegalArgumentException("Declaring class must be a JavaEnumElement"); } + failIfPostponeIsNeeded(enumConstant); return new JavaEnumConstantElement( - (JavaEnumElement) declaringClass, - enumConstant, - annotationMetadata, - visitorContext + (JavaEnumElement) declaringClass, + enumConstant, + annotationMetadataFactory, + visitorContext ); } @NonNull @Override - public JavaFieldElement newFieldElement(ClassElement declaringClass, @NonNull VariableElement field, @NonNull AnnotationMetadata annotationMetadata) { + public JavaFieldElement newFieldElement(ClassElement declaringClass, + @NonNull VariableElement field, + @NonNull ElementAnnotationMetadataFactory annotationMetadataFactory) { + failIfPostponeIsNeeded(field); return new JavaFieldElement( - (JavaClassElement) declaringClass, - field, - annotationMetadata, - visitorContext + (JavaClassElement) declaringClass, + field, + annotationMetadataFactory, + visitorContext ); } - @NonNull - @Override - public JavaFieldElement newFieldElement(@NonNull VariableElement field, @NonNull AnnotationMetadata annotationMetadata) { - return new JavaFieldElement( - field, - annotationMetadata, - visitorContext - ); + private void failIfPostponeIsNeeded(ExecutableElement executableElement) { + List parameters = executableElement.getParameters(); + for (VariableElement parameter : parameters) { + failIfPostponeIsNeeded(parameter); + } + TypeKind returnKind = executableElement.getReturnType().getKind(); + if (returnKind == TypeKind.ERROR) { + throw new PostponeToNextRoundException(); + } } - /** - * Creates a new parameter element for the given arguments. - * @param declaringClass The declaring class - * @param field The field - * @param annotationMetadata The annotation metadata - * @return The parameter element - */ - @NonNull - public JavaParameterElement newParameterElement(ClassElement declaringClass, @NonNull VariableElement field, @NonNull AnnotationMetadata annotationMetadata) { - if (!(declaringClass instanceof JavaClassElement)) { + private void failIfPostponeIsNeeded(VariableElement variableElement) { + TypeMirror type = variableElement.asType(); + if (type.getKind() == TypeKind.ERROR) { + throw new PostponeToNextRoundException(); + } + } + + private static void validateOwningClass(ClassElement owningClass) { + if (!(owningClass instanceof JavaClassElement)) { throw new IllegalArgumentException("Declaring class must be a JavaClassElement"); } - return new JavaParameterElement((JavaClassElement) declaringClass, field, annotationMetadata, visitorContext); } } diff --git a/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaEnumConstantElement.java b/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaEnumConstantElement.java index eccca6ac613..d52e5c8180b 100644 --- a/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaEnumConstantElement.java +++ b/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaEnumConstantElement.java @@ -15,15 +15,16 @@ */ package io.micronaut.annotation.processing.visitor; -import java.util.Set; - -import javax.lang.model.element.VariableElement; - import io.micronaut.core.annotation.AnnotationMetadata; import io.micronaut.core.annotation.Internal; import io.micronaut.inject.ast.ClassElement; +import io.micronaut.inject.ast.ElementAnnotationMetadataFactory; import io.micronaut.inject.ast.ElementModifier; import io.micronaut.inject.ast.EnumConstantElement; +import io.micronaut.inject.ast.MemberElement; + +import javax.lang.model.element.VariableElement; +import java.util.Set; /** * Implements the {@link io.micronaut.inject.ast.EnumElement} interface for Java. @@ -33,23 +34,33 @@ @Internal final class JavaEnumConstantElement extends AbstractJavaElement implements EnumConstantElement { - private final JavaVisitorContext visitorContext; private final VariableElement variableElement; - private final JavaClassElement declaringEnum; + private final JavaEnumElement declaringEnum; /** - * @param declaringEnum The declaring enum element - * @param variableElement The {@link javax.lang.model.element.ExecutableElement} - * @param annotationMetadata The annotation metadata - * @param visitorContext The visitor context + * @param declaringEnum The declaring enum element + * @param variableElement The {@link javax.lang.model.element.ExecutableElement} + * @param annotationMetadataFactory The annotation metadata factory + * @param visitorContext The visitor context */ - JavaEnumConstantElement(JavaEnumElement declaringEnum, VariableElement variableElement, - AnnotationMetadata annotationMetadata, JavaVisitorContext visitorContext) { - super(variableElement, annotationMetadata, visitorContext); + JavaEnumConstantElement(JavaEnumElement declaringEnum, + VariableElement variableElement, + ElementAnnotationMetadataFactory annotationMetadataFactory, + JavaVisitorContext visitorContext) { + super(variableElement, annotationMetadataFactory, visitorContext); this.declaringEnum = declaringEnum; this.variableElement = variableElement; - this.visitorContext = visitorContext; + } + + @Override + protected AbstractJavaElement copyThis() { + return new JavaEnumConstantElement(declaringEnum, variableElement, elementAnnotationMetadataFactory, visitorContext); + } + + @Override + public MemberElement withAnnotationMetadata(AnnotationMetadata annotationMetadata) { + return (MemberElement) super.withAnnotationMetadata(annotationMetadata); } @Override diff --git a/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaEnumElement.java b/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaEnumElement.java index a23b9bf93ce..1de7dafc29f 100644 --- a/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaEnumElement.java +++ b/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaEnumElement.java @@ -15,9 +15,9 @@ */ package io.micronaut.annotation.processing.visitor; -import io.micronaut.core.annotation.AnnotationMetadata; import io.micronaut.core.annotation.Internal; import io.micronaut.inject.ast.ClassElement; +import io.micronaut.inject.ast.ElementAnnotationMetadataFactory; import io.micronaut.inject.ast.EnumConstantElement; import io.micronaut.inject.ast.EnumElement; @@ -25,7 +25,6 @@ import javax.lang.model.element.ElementKind; import javax.lang.model.element.TypeElement; import javax.lang.model.element.VariableElement; - import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -43,22 +42,27 @@ class JavaEnumElement extends JavaClassElement implements EnumElement { protected List values; /** - * @param classElement The {@link TypeElement} - * @param annotationMetadata The annotation metadata - * @param visitorContext The visitor context + * @param classElement The {@link TypeElement} + * @param annotationMetadataFactory The annotation metadata factory + * @param visitorContext The visitor context */ - JavaEnumElement(TypeElement classElement, AnnotationMetadata annotationMetadata, JavaVisitorContext visitorContext) { - this(classElement, annotationMetadata, visitorContext, 0); + JavaEnumElement(TypeElement classElement, + ElementAnnotationMetadataFactory annotationMetadataFactory, + JavaVisitorContext visitorContext) { + this(classElement, annotationMetadataFactory, visitorContext, 0); } /** - * @param classElement The {@link TypeElement} - * @param annotationMetadata The annotation metadata - * @param visitorContext The visitor context - * @param arrayDimensions The number of array dimensions + * @param classElement The {@link TypeElement} + * @param annotationMetadataFactory The annotation metadata factory + * @param visitorContext The visitor context + * @param arrayDimensions The number of array dimensions */ - JavaEnumElement(TypeElement classElement, AnnotationMetadata annotationMetadata, JavaVisitorContext visitorContext, int arrayDimensions) { - super(classElement, annotationMetadata, visitorContext, Collections.emptyList(), Collections.emptyMap(), arrayDimensions, false); + JavaEnumElement(TypeElement classElement, + ElementAnnotationMetadataFactory annotationMetadataFactory, + JavaVisitorContext visitorContext, + int arrayDimensions) { + super(classElement, annotationMetadataFactory, visitorContext, Collections.emptyList(), Collections.emptyMap(), arrayDimensions, false); } @Override @@ -86,9 +90,13 @@ private void initEnum() { for (Element element : nativeType.getEnclosedElements()) { if (element.getKind() == ElementKind.ENUM_CONSTANT) { values.add(element.getSimpleName().toString()); - enumConstants.add(new JavaEnumConstantElement(this, (VariableElement) element, - visitorContext.getAnnotationUtils().newAnnotationBuilder().build(element), - visitorContext)); + enumConstants.add( + new JavaEnumConstantElement( + this, + (VariableElement) element, + elementAnnotationMetadataFactory, + visitorContext) + ); } } values = Collections.unmodifiableList(values); @@ -97,6 +105,6 @@ private void initEnum() { @Override public ClassElement withArrayDimensions(int arrayDimensions) { - return new JavaEnumElement(classElement, getAnnotationMetadata(), visitorContext, arrayDimensions); + return new JavaEnumElement(classElement, elementAnnotationMetadataFactory, visitorContext, arrayDimensions); } } diff --git a/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaFieldElement.java b/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaFieldElement.java index 5990cd32f3d..2fd90445f3c 100644 --- a/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaFieldElement.java +++ b/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaFieldElement.java @@ -17,10 +17,11 @@ import io.micronaut.core.annotation.AnnotationMetadata; import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.NonNull; import io.micronaut.inject.ast.ClassElement; +import io.micronaut.inject.ast.ElementAnnotationMetadataFactory; import io.micronaut.inject.ast.FieldElement; -import io.micronaut.core.annotation.NonNull; import javax.lang.model.element.Element; import javax.lang.model.element.TypeElement; import javax.lang.model.element.VariableElement; @@ -35,49 +36,59 @@ @Internal class JavaFieldElement extends AbstractJavaElement implements FieldElement { - private final JavaVisitorContext visitorContext; private final VariableElement variableElement; - private JavaClassElement declaringElement; + private JavaClassElement owningType; private ClassElement typeElement; private ClassElement genericType; private ClassElement resolvedDeclaringClass; /** - * @param variableElement The {@link VariableElement} - * @param annotationMetadata The annotation metadata - * @param visitorContext The visitor context + * @param variableElement The {@link VariableElement} + * @param annotationMetadataFactory The annotation metadata factory + * @param visitorContext The visitor context */ - JavaFieldElement(VariableElement variableElement, AnnotationMetadata annotationMetadata, JavaVisitorContext visitorContext) { - super(variableElement, annotationMetadata, visitorContext); + JavaFieldElement(VariableElement variableElement, + ElementAnnotationMetadataFactory annotationMetadataFactory, + JavaVisitorContext visitorContext) { + super(variableElement, annotationMetadataFactory, visitorContext); this.variableElement = variableElement; - this.visitorContext = visitorContext; } /** - * @param declaringElement The declaring element - * @param variableElement The {@link VariableElement} - * @param annotationMetadata The annotation metadata - * @param visitorContext The visitor context + * @param owningType The declaring element + * @param variableElement The {@link VariableElement} + * @param annotationMetadataFactory The annotation metadata factory + * @param visitorContext The visitor context */ - JavaFieldElement(JavaClassElement declaringElement, + JavaFieldElement(JavaClassElement owningType, VariableElement variableElement, - AnnotationMetadata annotationMetadata, + ElementAnnotationMetadataFactory annotationMetadataFactory, JavaVisitorContext visitorContext) { - this(variableElement, annotationMetadata, visitorContext); - this.declaringElement = declaringElement; + this(variableElement, annotationMetadataFactory, visitorContext); + this.owningType = owningType; + } + + @Override + protected AbstractJavaElement copyThis() { + return new JavaFieldElement(owningType, variableElement, elementAnnotationMetadataFactory, visitorContext); + } + + @Override + public FieldElement withAnnotationMetadata(AnnotationMetadata annotationMetadata) { + return (FieldElement) super.withAnnotationMetadata(annotationMetadata); } @Override public ClassElement getGenericType() { if (this.genericType == null) { - if (declaringElement == null) { + if (owningType == null) { this.genericType = getType(); } else { this.genericType = mirrorToClassElement( - variableElement.asType(), - visitorContext, - declaringElement.getGenericTypeInfo(), - false + variableElement.asType(), + visitorContext, + owningType.getGenericTypeInfo(), + false ); } } @@ -112,19 +123,23 @@ public ClassElement getType() { @Override public ClassElement getDeclaringType() { if (resolvedDeclaringClass == null) { - Element enclosingElement = variableElement.getEnclosingElement(); if (enclosingElement instanceof TypeElement) { TypeElement te = (TypeElement) enclosingElement; - if (declaringElement.getName().equals(te.getQualifiedName().toString())) { - resolvedDeclaringClass = declaringElement; + if (owningType.getName().equals(te.getQualifiedName().toString())) { + resolvedDeclaringClass = owningType; } else { - resolvedDeclaringClass = mirrorToClassElement(te.asType(), visitorContext, declaringElement.getGenericTypeInfo()); + resolvedDeclaringClass = mirrorToClassElement(te.asType(), visitorContext, owningType.getGenericTypeInfo()); } } else { - return declaringElement; + return owningType; } } return resolvedDeclaringClass; } + + @Override + public ClassElement getOwningType() { + return owningType; + } } diff --git a/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaGenericPlaceholderElement.java b/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaGenericPlaceholderElement.java index 33658630445..18a1898327d 100644 --- a/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaGenericPlaceholderElement.java +++ b/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaGenericPlaceholderElement.java @@ -15,11 +15,11 @@ */ package io.micronaut.annotation.processing.visitor; -import io.micronaut.core.annotation.AnnotationMetadata; import io.micronaut.core.annotation.Internal; import io.micronaut.core.annotation.NonNull; import io.micronaut.inject.ast.ClassElement; import io.micronaut.inject.ast.Element; +import io.micronaut.inject.ast.ElementAnnotationMetadataFactory; import io.micronaut.inject.ast.GenericPlaceholderElement; import javax.lang.model.element.TypeParameterElement; @@ -41,18 +41,18 @@ final class JavaGenericPlaceholderElement extends JavaClassElement implements Ge final TypeVariable realTypeVariable; private final List bounds; - JavaGenericPlaceholderElement( - @NonNull TypeVariable realTypeVariable, - @NonNull List bounds, - int arrayDimensions) { + JavaGenericPlaceholderElement(@NonNull TypeVariable realTypeVariable, + @NonNull List bounds, + @NonNull ElementAnnotationMetadataFactory annotationMetadataFactory, + int arrayDimensions) { super( - bounds.get(0).classElement, - bounds.get(0).getAnnotationMetadata(), - bounds.get(0).visitorContext, - bounds.get(0).typeArguments, - bounds.get(0).getGenericTypeInfo(), - arrayDimensions, - true + bounds.get(0).classElement, + annotationMetadataFactory, + bounds.get(0).visitorContext, + bounds.get(0).typeArguments, + bounds.get(0).getGenericTypeInfo(), + arrayDimensions, + true ); this.realTypeVariable = realTypeVariable; this.bounds = bounds; @@ -60,7 +60,8 @@ final class JavaGenericPlaceholderElement extends JavaClassElement implements Ge @Override public Object getNativeType() { - return realTypeVariable; + // Native types should be always Element + return getParameterElement(); } @NonNull @@ -86,7 +87,7 @@ public Optional getDeclaringElement() { @Override public ClassElement withArrayDimensions(int arrayDimensions) { - return new JavaGenericPlaceholderElement(realTypeVariable, bounds, arrayDimensions); + return new JavaGenericPlaceholderElement(realTypeVariable, bounds, elementAnnotationMetadataFactory, arrayDimensions); } @Override @@ -95,8 +96,4 @@ public ClassElement foldBoundGenericTypes(@NonNull Function getReceiverType() { if (receiverType != null) { if (receiverType.getKind() != TypeKind.NONE) { final ClassElement classElement = mirrorToClassElement(receiverType, - visitorContext, - declaringClass.getGenericTypeInfo()); + visitorContext, + owningType.getGenericTypeInfo()); return Optional.of(classElement); } } @@ -95,11 +118,11 @@ public ClassElement[] getThrownTypes() { final List thrownTypes = executableElement.getThrownTypes(); if (!thrownTypes.isEmpty()) { return thrownTypes.stream() - .map(tm -> mirrorToClassElement( - tm, - visitorContext, - declaringClass.getGenericTypeInfo() - )).toArray(ClassElement[]::new); + .map(tm -> mirrorToClassElement( + tm, + visitorContext, + owningType.getGenericTypeInfo() + )).toArray(ClassElement[]::new); } return ClassElement.ZERO_CLASS_ELEMENTS; @@ -111,22 +134,28 @@ public boolean isDefault() { } @Override - public boolean overrides(MethodElement methodElement) { - if (methodElement instanceof JavaMethodElement) { - return visitorContext.getElements().overrides( - executableElement, - ((JavaMethodElement) methodElement).executableElement, - declaringClass.classElement - ); - } - return false; + public boolean overrides(MethodElement overridden) { +// if (this.equals(overridden) || isStatic() || overridden.isStatic()) { +// return false; +// } +// if (overridden instanceof JavaMethodElement) { +// boolean overrides = visitorContext.getElements().overrides( +// executableElement, +// ((JavaMethodElement) overridden).executableElement, +// owningType.classElement +// ); +// if (overrides) { +// return true; +// } +// } + return MethodElement.super.overrides(overridden); } @NonNull @Override public ClassElement getGenericReturnType() { if (this.genericReturnType == null) { - this.genericReturnType = returnType(declaringClass.getGenericTypeInfo()); + this.genericReturnType = returnType(owningType.getGenericTypeInfo()); } return this.genericReturnType; } @@ -143,8 +172,8 @@ public ClassElement getReturnType() { @Override public List getDeclaredTypeVariables() { return executableElement.getTypeParameters().stream() - .map(tpe -> (GenericPlaceholderElement) mirrorToClassElement(tpe.asType(), visitorContext)) - .collect(Collectors.toList()); + .map(tpe -> (GenericPlaceholderElement) mirrorToClassElement(tpe.asType(), visitorContext)) + .collect(Collectors.toList()); } @Override @@ -158,19 +187,13 @@ public ParameterElement[] getParameters() { if (this.parameters == null) { List parameters = executableElement.getParameters(); List elts = new ArrayList<>(parameters.size()); - for (Iterator i = parameters.iterator(); i.hasNext();) { + for (Iterator i = parameters.iterator(); i.hasNext(); ) { VariableElement variableElement = i.next(); - if (! i.hasNext() && isSuspend(variableElement)) { - this.continuationParameter = newParameterElement(variableElement, AnnotationMetadata.EMPTY_METADATA); + if (!i.hasNext() && isSuspend(variableElement)) { + this.continuationParameter = newParameterElement(this, variableElement); continue; } - AnnotationMetadata annotationMetadata = visitorContext.getAnnotationUtils() - .getAnnotationMetadata(getFieldElementForWriter(), variableElement); - JavaParameterElement javaParameterElement = newParameterElement(variableElement, annotationMetadata); - if (annotationMetadata.hasDeclaredAnnotation("org.jetbrains.annotations.Nullable")) { - javaParameterElement.annotate("javax.annotation.Nullable").getAnnotationMetadata(); - } - elts.add(javaParameterElement); + elts.add(newParameterElement(this, variableElement)); } this.parameters = elts.toArray(new ParameterElement[0]); } @@ -178,13 +201,10 @@ public ParameterElement[] getParameters() { } @Override - public MethodElement withNewParameters(ParameterElement... newParameters) { - return new JavaMethodElement(declaringClass, executableElement, getAnnotationMetadata(), visitorContext) { - @Override - public ParameterElement[] getParameters() { - return ArrayUtils.concat(super.getParameters(), newParameters); - } - }; + public MethodElement withNewOwningType(ClassElement owningType) { + JavaMethodElement javaMethodElement = new JavaMethodElement((JavaClassElement) owningType, executableElement, elementAnnotationMetadataFactory, visitorContext); + copyValues(javaMethodElement); + return javaMethodElement; } @Override @@ -199,29 +219,29 @@ public ParameterElement[] getSuspendParameters() { /** * Creates a new parameter element for the given args. + * + * @param methodElement The method element * @param variableElement The variable element - * @param annotationMetadata The annotation metadata * @return The parameter element */ @NonNull - protected JavaParameterElement newParameterElement(@NonNull VariableElement variableElement, @NonNull AnnotationMetadata annotationMetadata) { - return new JavaParameterElement(declaringClass, variableElement, annotationMetadata, visitorContext); + protected JavaParameterElement newParameterElement(@NonNull MethodElement methodElement, @NonNull VariableElement variableElement) { + return new JavaParameterElement(owningType, methodElement, variableElement, elementAnnotationMetadataFactory, visitorContext); } @Override - public ClassElement getDeclaringType() { + public JavaClassElement getDeclaringType() { if (resolvedDeclaringClass == null) { - Element enclosingElement = executableElement.getEnclosingElement(); if (enclosingElement instanceof TypeElement) { TypeElement te = (TypeElement) enclosingElement; - if (declaringClass.getName().equals(te.getQualifiedName().toString())) { - resolvedDeclaringClass = declaringClass; + if (owningType.getName().equals(te.getQualifiedName().toString())) { + resolvedDeclaringClass = owningType; } else { - resolvedDeclaringClass = mirrorToClassElement(te.asType(), visitorContext, declaringClass.getGenericTypeInfo()); + resolvedDeclaringClass = (JavaClassElement) mirrorToClassElement(te.asType(), visitorContext, owningType.getGenericTypeInfo()); } } else { - return declaringClass; + return owningType; } } return resolvedDeclaringClass; @@ -229,11 +249,12 @@ public ClassElement getDeclaringType() { @Override public ClassElement getOwningType() { - return declaringClass; + return owningType; } /** * The return type for the given info. + * * @param info The info * @return The return type */ @@ -269,27 +290,4 @@ private boolean isSuspend(VariableElement ve) { return false; } - private Element getFieldElementForWriter() { - String[] writerPrefixes = getAnnotationMetadata() - .getValue(AccessorsStyle.class, "writePrefixes", String[].class) - .orElse(new String[]{AccessorsStyle.DEFAULT_WRITE_PREFIX}); - - final String methodName = getName(); - if (!NameUtils.isWriterName(methodName, writerPrefixes) || executableElement.getParameters().size() != 1) { - return null; // not a writer - } - - Element classElement = executableElement.getEnclosingElement(); - if (!(classElement instanceof TypeElement)) { - return null; // not within a class - } - - final String fieldName = NameUtils.getPropertyNameForSetter(methodName, writerPrefixes); - - // Return the field corresponding to this writer. - return (Element) getDeclaringType() - .getEnclosedElement(ElementQuery.ALL_FIELDS.named(fieldName::equals)) - .map(io.micronaut.inject.ast.Element::getNativeType) - .orElse(null); - } } diff --git a/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaPackageElement.java b/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaPackageElement.java index ddc1b51b867..c1c37fbd6c9 100644 --- a/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaPackageElement.java +++ b/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaPackageElement.java @@ -15,8 +15,8 @@ */ package io.micronaut.annotation.processing.visitor; -import io.micronaut.core.annotation.AnnotationMetadata; import io.micronaut.core.annotation.Internal; +import io.micronaut.inject.ast.ElementAnnotationMetadataFactory; import javax.lang.model.element.PackageElement; @@ -32,18 +32,22 @@ public class JavaPackageElement extends AbstractJavaElement implements io.micron private final PackageElement element; /** - * @param element The {@link PackageElement} - * @param annotationMetadata The Annotation metadata - * @param visitorContext The Java visitor context + * @param element The {@link PackageElement} + * @param annotationMetadataFactory The annotation metadata factory + * @param visitorContext The Java visitor context */ - public JavaPackageElement( - PackageElement element, - AnnotationMetadata annotationMetadata, - JavaVisitorContext visitorContext) { - super(element, annotationMetadata, visitorContext); + public JavaPackageElement(PackageElement element, + ElementAnnotationMetadataFactory annotationMetadataFactory, + JavaVisitorContext visitorContext) { + super(element, annotationMetadataFactory, visitorContext); this.element = element; } + @Override + protected AbstractJavaElement copyThis() { + return new JavaPackageElement(element, elementAnnotationMetadataFactory, visitorContext); + } + @Override public String getName() { return element.getQualifiedName().toString(); @@ -53,4 +57,9 @@ public String getName() { public String getSimpleName() { return element.getSimpleName().toString(); } + + @Override + public boolean isUnnamed() { + return element.isUnnamed(); + } } diff --git a/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaParameterElement.java b/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaParameterElement.java index 9fb6b4057f2..84445f521fe 100644 --- a/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaParameterElement.java +++ b/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaParameterElement.java @@ -17,10 +17,12 @@ import io.micronaut.core.annotation.AnnotationMetadata; import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.NonNull; import io.micronaut.inject.ast.ClassElement; +import io.micronaut.inject.ast.ElementAnnotationMetadataFactory; +import io.micronaut.inject.ast.MethodElement; import io.micronaut.inject.ast.ParameterElement; -import io.micronaut.core.annotation.NonNull; import javax.lang.model.element.VariableElement; import javax.lang.model.type.TypeMirror; import java.util.Map; @@ -34,23 +36,40 @@ @Internal class JavaParameterElement extends AbstractJavaElement implements ParameterElement { - private final JavaVisitorContext visitorContext; - private final JavaClassElement declaringClass; + private final JavaClassElement owningType; + private final MethodElement methodElement; + private final VariableElement variableElement; private ClassElement typeElement; private ClassElement genericTypeElement; /** * Default constructor. * - * @param declaringClass The declaring class - * @param element The variable element - * @param annotationMetadata The annotation metadata - * @param visitorContext The visitor context + * @param owningType The owning class + * @param methodElement The method element + * @param element The variable element + * @param annotationMetadataFactory The annotation metadata factory + * @param visitorContext The visitor context */ - JavaParameterElement(JavaClassElement declaringClass, VariableElement element, AnnotationMetadata annotationMetadata, JavaVisitorContext visitorContext) { - super(element, annotationMetadata, visitorContext); - this.declaringClass = declaringClass; - this.visitorContext = visitorContext; + JavaParameterElement(JavaClassElement owningType, + MethodElement methodElement, + VariableElement element, + ElementAnnotationMetadataFactory annotationMetadataFactory, + JavaVisitorContext visitorContext) { + super(element, annotationMetadataFactory, visitorContext); + this.owningType = owningType; + this.methodElement = methodElement; + this.variableElement = element; + } + + @Override + protected AbstractJavaElement copyThis() { + return new JavaParameterElement(owningType, methodElement, variableElement, elementAnnotationMetadataFactory, visitorContext); + } + + @Override + public ParameterElement withAnnotationMetadata(AnnotationMetadata annotationMetadata) { + return (ParameterElement) super.withAnnotationMetadata(annotationMetadata); } @Override @@ -83,14 +102,20 @@ public ClassElement getType() { public ClassElement getGenericType() { if (this.genericTypeElement == null) { TypeMirror returnType = getNativeType().asType(); - Map> declaredGenericInfo = declaringClass.getGenericTypeInfo(); + Map> declaredGenericInfo = owningType.getGenericTypeInfo(); this.genericTypeElement = parameterizedClassElement(returnType, visitorContext, declaredGenericInfo); } return this.genericTypeElement; } + @Override + public MethodElement getMethodElement() { + return methodElement; + } + @Override public VariableElement getNativeType() { return (VariableElement) super.getNativeType(); } + } diff --git a/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaPropertyElement.java b/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaPropertyElement.java index a60f17760ec..89bbfd56126 100644 --- a/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaPropertyElement.java +++ b/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaPropertyElement.java @@ -16,16 +16,27 @@ package io.micronaut.annotation.processing.visitor; import io.micronaut.core.annotation.AnnotationMetadata; +import io.micronaut.core.annotation.AnnotationMetadataDelegate; +import io.micronaut.core.annotation.AnnotationValue; +import io.micronaut.core.annotation.AnnotationValueBuilder; import io.micronaut.core.annotation.Internal; -import io.micronaut.core.annotation.NonNull; +import io.micronaut.core.annotation.Nullable; +import io.micronaut.inject.annotation.AnnotationMetadataHierarchy; import io.micronaut.inject.ast.ClassElement; +import io.micronaut.inject.ast.ElementAnnotationMetadataFactory; +import io.micronaut.inject.ast.FieldElement; +import io.micronaut.inject.ast.MemberElement; +import io.micronaut.inject.ast.MethodElement; +import io.micronaut.inject.ast.ElementMutableAnnotationMetadata; import io.micronaut.inject.ast.PropertyElement; import javax.lang.model.element.Element; -import javax.lang.model.element.TypeElement; -import javax.lang.model.type.TypeMirror; -import java.util.Collections; -import java.util.Map; +import java.lang.annotation.Annotation; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.function.Consumer; +import java.util.function.Predicate; /** * Models a {@link PropertyElement} for Java. @@ -34,70 +45,218 @@ * @since 1.0 */ @Internal -class JavaPropertyElement extends AbstractJavaElement implements PropertyElement { +final class JavaPropertyElement extends AbstractJavaElement implements PropertyElement { - private final String name; private final ClassElement type; - private final boolean readOnly; - private final ClassElement declaringElement; - private final JavaVisitorContext visitorContext; - - /** - * Default constructor. - * - * @param declaringElement The declaring element - * @param rootElement The element - * @param annotationMetadata The annotation metadata - * @param name The name - * @param type The type - * @param readOnly Whether it is read only - * @param visitorContext The java visitor context - */ - JavaPropertyElement( - ClassElement declaringElement, - Element rootElement, - AnnotationMetadata annotationMetadata, - String name, - ClassElement type, - boolean readOnly, - JavaVisitorContext visitorContext) { - super(rootElement, annotationMetadata, visitorContext); - this.name = name; + private final String name; + private final AccessKind readAccessKind; + private final AccessKind writeAccessKind; + private final ClassElement owningElement; + @Nullable + private final MethodElement getter; + @Nullable + private final MethodElement setter; + @Nullable + private final FieldElement field; + private final boolean excluded; + private final ElementMutableAnnotationMetadata annotationMetadata; + + JavaPropertyElement(ClassElement owningElement, + ClassElement type, + MethodElement getter, + MethodElement setter, + FieldElement field, + ElementAnnotationMetadataFactory annotationMetadataFactory, + String name, + AccessKind readAccessKind, + AccessKind writeAccessKind, + boolean excluded, + JavaVisitorContext visitorContext) { + super(selectNativeType(getter, setter, field), annotationMetadataFactory, visitorContext); this.type = type; - this.readOnly = readOnly; - this.declaringElement = declaringElement; - this.visitorContext = visitorContext; + this.getter = getter; + this.setter = setter; + this.field = field; + this.name = name; + this.readAccessKind = readAccessKind; + this.writeAccessKind = writeAccessKind; + this.owningElement = owningElement; + this.excluded = excluded; + List elements = new ArrayList<>(3); + if (setter != null) { + elements.add(setter); + } + if (field != null) { + elements.add(field); + } + if (getter != null) { + elements.add(getter); + } + // The instance AnnotationMetadata of each element can change after a modification + // Set annotation metadata as actual elements so the changes are reflected + AnnotationMetadata propertyAnnotationMetadata; + if (elements.size() == 1) { + propertyAnnotationMetadata = elements.iterator().next(); + } else { + propertyAnnotationMetadata = new AnnotationMetadataHierarchy( + true, + elements.stream().map(e -> { + if (e instanceof MethodElement) { + return new AnnotationMetadataDelegate() { + @Override + public AnnotationMetadata getAnnotationMetadata() { + // Exclude type metadata + return e.getAnnotationMetadata().getDeclaredMetadata(); + } + }; + } + return e; + }).toArray(AnnotationMetadata[]::new) + ); + } + annotationMetadata = new ElementMutableAnnotationMetadata() { + + @Override + public io.micronaut.inject.ast.Element annotate(AnnotationValue annotationValue) { + for (MemberElement memberElement : elements) { + memberElement.annotate(annotationValue); + } + return JavaPropertyElement.this; + } + + @Override + public io.micronaut.inject.ast.Element annotate(String annotationType, Consumer> consumer) { + for (MemberElement memberElement : elements) { + memberElement.annotate(annotationType, consumer); + } + return JavaPropertyElement.this; + } + + @Override + public io.micronaut.inject.ast.Element annotate(Class annotationType) { + for (MemberElement memberElement : elements) { + memberElement.annotate(annotationType); + } + return JavaPropertyElement.this; + } + + @Override + public io.micronaut.inject.ast.Element annotate(String annotationType) { + for (MemberElement memberElement : elements) { + memberElement.annotate(annotationType); + } + return JavaPropertyElement.this; + } + + @Override + public io.micronaut.inject.ast.Element annotate(Class annotationType, Consumer> consumer) { + for (MemberElement memberElement : elements) { + memberElement.annotate(annotationType, consumer); + } + return JavaPropertyElement.this; + } + + @Override + public io.micronaut.inject.ast.Element removeAnnotation(String annotationType) { + for (MemberElement memberElement : elements) { + memberElement.removeAnnotation(annotationType); + } + return JavaPropertyElement.this; + } + + @Override + public io.micronaut.inject.ast.Element removeAnnotationIf(Predicate> predicate) { + for (MemberElement memberElement : elements) { + memberElement.removeAnnotationIf(predicate); + } + return JavaPropertyElement.this; + } + + @Override + public AnnotationMetadata getAnnotationMetadata() { + return propertyAnnotationMetadata; + } + }; } @Override - public ClassElement getGenericType() { - Map> declaredGenericInfo; - if (declaringElement instanceof JavaClassElement) { - declaredGenericInfo = ((JavaClassElement) declaringElement).getGenericTypeInfo(); - } else { - declaredGenericInfo = Collections.emptyMap(); + protected AbstractJavaElement copyThis() { + return new JavaPropertyElement(owningElement, type, getter, setter, field, elementAnnotationMetadataFactory, name, readAccessKind, writeAccessKind, excluded, visitorContext); + } + + @Override + public PropertyElement withAnnotationMetadata(AnnotationMetadata annotationMetadata) { + return (PropertyElement) super.withAnnotationMetadata(annotationMetadata); + } + + private static Element selectNativeType(MethodElement getter, + MethodElement setter, + FieldElement field) { + if (getter != null) { + return (Element) getter.getNativeType(); + } + if (setter != null) { + return (Element) setter.getNativeType(); + } + if (field != null) { + return (Element) field.getNativeType(); } - return parameterizedClassElement(((TypeElement) type.getNativeType()).asType(), visitorContext, declaredGenericInfo); + throw new IllegalStateException(); + } + + @Override + public boolean isExcluded() { + return excluded; + } + + @Override + public ElementMutableAnnotationMetadata getAnnotationMetadata() { + return annotationMetadata; + } + + @Override + public ClassElement getType() { + return type; + } + + @Override + public ClassElement getGenericType() { + return type; // Already generic + } + + @Override + public Optional getField() { + return Optional.ofNullable(field); + } + + @Override + public Optional getWriteMethod() { + return Optional.ofNullable(setter); + } + + @Override + public Optional getReadMethod() { + return Optional.ofNullable(getter); } @Override public boolean isPrimitive() { - return type.isPrimitive(); + return getType().isPrimitive(); } @Override public boolean isArray() { - return type.isArray(); + return getType().isArray(); } @Override public int getArrayDimensions() { - return type.getArrayDimensions(); + return getType().getArrayDimensions(); } @Override public String getName() { - return this.name; + return name; } @Override @@ -105,19 +264,57 @@ public String toString() { return name; } - @NonNull @Override - public ClassElement getType() { - return type; + public AccessKind getReadAccessKind() { + return readAccessKind; + } + + @Override + public AccessKind getWriteAccessKind() { + return writeAccessKind; } @Override public boolean isReadOnly() { - return readOnly; + switch (readAccessKind) { + case METHOD: + return setter == null; + case FIELD: + return field == null || field.isFinal(); + default: + throw new IllegalStateException(); + } + } + + @Override + public boolean isWriteOnly() { + switch (writeAccessKind) { + case METHOD: + return getter == null; + case FIELD: + return field == null; + default: + throw new IllegalStateException(); + } } @Override public ClassElement getDeclaringType() { - return declaringElement; + if (field != null) { + return field.getDeclaringType(); + } + if (getter != null) { + return getter.getDeclaringType(); + } + if (setter != null) { + return setter.getDeclaringType(); + } + throw new IllegalStateException(); } + + @Override + public ClassElement getOwningType() { + return owningElement; + } + } diff --git a/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaVisitorContext.java b/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaVisitorContext.java index 33f1251316e..8d46b88d4f1 100644 --- a/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaVisitorContext.java +++ b/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaVisitorContext.java @@ -15,15 +15,15 @@ */ package io.micronaut.annotation.processing.visitor; -import io.micronaut.annotation.processing.JavaConfigurationMetadataBuilder; -import io.micronaut.core.annotation.NonNull; -import io.micronaut.core.annotation.Nullable; import io.micronaut.annotation.processing.AnnotationProcessingOutputVisitor; import io.micronaut.annotation.processing.AnnotationUtils; import io.micronaut.annotation.processing.GenericUtils; +import io.micronaut.annotation.processing.JavaAnnotationMetadataBuilder; +import io.micronaut.annotation.processing.JavaElementAnnotationMetadataFactory; import io.micronaut.annotation.processing.ModelUtils; -import io.micronaut.core.annotation.AnnotationMetadata; import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.NonNull; +import io.micronaut.core.annotation.Nullable; import io.micronaut.core.convert.ArgumentConversionContext; import io.micronaut.core.convert.value.MutableConvertibleValues; import io.micronaut.core.reflect.ReflectionUtils; @@ -31,8 +31,10 @@ import io.micronaut.core.util.CollectionUtils; import io.micronaut.core.util.StringUtils; import io.micronaut.inject.ast.ClassElement; +import io.micronaut.inject.ast.ElementAnnotationMetadataFactory; import io.micronaut.inject.ast.beans.BeanElement; import io.micronaut.inject.ast.beans.BeanElementBuilder; +import io.micronaut.inject.configuration.ConfigurationMetadataBuilder; import io.micronaut.inject.util.VisitorContextUtils; import io.micronaut.inject.visitor.BeanElementVisitorContext; import io.micronaut.inject.visitor.TypeElementVisitor; @@ -55,7 +57,15 @@ import java.io.OutputStream; import java.lang.reflect.Method; import java.net.URL; -import java.util.*; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Enumeration; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -83,6 +93,8 @@ public class JavaVisitorContext implements VisitorContext, BeanElementVisitorCon private final TypeElementVisitor.VisitorKind visitorKind; private @Nullable JavaFileManager standardFileManager; + private final JavaAnnotationMetadataBuilder annotationMetadataBuilder; + private final JavaElementAnnotationMetadataFactory elementAnnotationMetadataFactory; /** * The default constructor. @@ -99,16 +111,16 @@ public class JavaVisitorContext implements VisitorContext, BeanElementVisitorCon * @param visitorKind The visitor kind */ public JavaVisitorContext( - ProcessingEnvironment processingEnv, - Messager messager, - Elements elements, - AnnotationUtils annotationUtils, - Types types, - ModelUtils modelUtils, - GenericUtils genericUtils, - Filer filer, - MutableConvertibleValues visitorAttributes, - TypeElementVisitor.VisitorKind visitorKind) { + ProcessingEnvironment processingEnv, + Messager messager, + Elements elements, + AnnotationUtils annotationUtils, + Types types, + ModelUtils modelUtils, + GenericUtils genericUtils, + Filer filer, + MutableConvertibleValues visitorAttributes, + TypeElementVisitor.VisitorKind visitorKind) { this.messager = messager; this.elements = elements; this.annotationUtils = annotationUtils; @@ -120,6 +132,8 @@ public JavaVisitorContext( this.processingEnv = processingEnv; this.elementFactory = new JavaElementFactory(this); this.visitorKind = visitorKind; + this.annotationMetadataBuilder = new JavaAnnotationMetadataBuilder(elements, messager, annotationUtils, modelUtils); + this.elementAnnotationMetadataFactory = new JavaElementAnnotationMetadataFactory(false, this.annotationMetadataBuilder); } /** @@ -146,7 +160,7 @@ public Iterable getClasspathResources(@NonNull String path) { if (standardFileManager != null) { try { final ClassLoader classLoader = standardFileManager - .getClassLoader(StandardLocation.CLASS_PATH); + .getClassLoader(StandardLocation.CLASS_PATH); if (classLoader != null) { final Enumeration resources = classLoader.getResources(path); @@ -161,14 +175,18 @@ public Iterable getClasspathResources(@NonNull String path) { @Override public Optional getClassElement(String name) { + return getClassElement(name, elementAnnotationMetadataFactory); + } + + @Override + public Optional getClassElement(String name, ElementAnnotationMetadataFactory annotationMetadataFactory) { TypeElement typeElement = elements.getTypeElement(name); if (typeElement == null) { // maybe inner class? typeElement = elements.getTypeElement(name.replace('$', '.')); } - return Optional.ofNullable(typeElement).map(typeElement1 -> - elementFactory.newClassElement(typeElement1, annotationUtils.getAnnotationMetadata(typeElement1)) - ); + return Optional.ofNullable(typeElement) + .map(typeElement1 -> elementFactory.newClassElement(typeElement1, annotationMetadataFactory)); } @Override @@ -192,6 +210,16 @@ public JavaElementFactory getElementFactory() { return elementFactory; } + @Override + public JavaElementAnnotationMetadataFactory getElementAnnotationMetadataFactory() { + return elementAnnotationMetadataFactory; + } + + @Override + public JavaAnnotationMetadataBuilder getAnnotationMetadataBuilder() { + return annotationMetadataBuilder; + } + @Override public void info(String message, @Nullable io.micronaut.inject.ast.Element element) { printMessage(message, Diagnostic.Kind.NOTE, element); @@ -230,7 +258,7 @@ private void printMessage(String message, Diagnostic.Kind kind, @Nullable io.mic @Override public OutputStream visitClass(String classname, @Nullable io.micronaut.inject.ast.Element originatingElement) throws IOException { - return outputVisitor.visitClass(classname, new io.micronaut.inject.ast.Element[]{ originatingElement }); + return outputVisitor.visitClass(classname, new io.micronaut.inject.ast.Element[]{originatingElement}); } @Override @@ -331,11 +359,11 @@ public Map getOptions() { Map systemPropsOptions = VisitorContextUtils.getSystemOptions(); // Merge both options, with system props overriding on duplications return Stream.of(processorOptions, systemPropsOptions) - .flatMap(map -> map.entrySet().stream()) - .collect(Collectors.toMap( - Map.Entry::getKey, - Map.Entry::getValue, - (v1, v2) -> StringUtils.isNotEmpty(v2) ? v2 : v1)); + .flatMap(map -> map.entrySet().stream()) + .collect(Collectors.toMap( + Map.Entry::getKey, + Map.Entry::getValue, + (v1, v2) -> StringUtils.isNotEmpty(v2) ? v2 : v1)); } @Override @@ -373,13 +401,11 @@ public Optional get(CharSequence name, ArgumentConversionContext conve private void populateClassElements(@NonNull String[] stereotypes, PackageElement packageElement, List classElements) { final List enclosedElements = packageElement.getEnclosedElements(); - boolean includeAll = Arrays.equals(stereotypes, new String[] { "*" }); + boolean includeAll = Arrays.equals(stereotypes, new String[]{"*"}); for (Element enclosedElement : enclosedElements) { if (enclosedElement instanceof TypeElement) { - final AnnotationMetadata annotationMetadata = annotationUtils.getAnnotationMetadata(enclosedElement); - if (includeAll || Arrays.stream(stereotypes).anyMatch(annotationMetadata::hasStereotype)) { - JavaClassElement classElement = elementFactory.newClassElement((TypeElement) enclosedElement, annotationMetadata); - + JavaClassElement classElement = elementFactory.newClassElement((TypeElement) enclosedElement, elementAnnotationMetadataFactory); + if (includeAll || Arrays.stream(stereotypes).anyMatch(classElement::hasStereotype)) { if (!classElement.isAbstract()) { classElements.add(classElement); } @@ -401,7 +427,7 @@ private Optional getStandardFileManager(ProcessingEnvironment p final Optional getMethod = ReflectionUtils.getMethod(context.getClass(), "get", Class.class); this.standardFileManager = (JavaFileManager) - getMethod.map(method -> ReflectionUtils.invokeMethod(context, method, JavaFileManager.class)).orElse(null); + getMethod.map(method -> ReflectionUtils.invokeMethod(context, method, JavaFileManager.class)).orElse(null); } } catch (Exception e) { // ignore @@ -433,6 +459,7 @@ public List getBeanElementBuilders() { /** * Adds a java bean definition builder. + * * @param javaBeanDefinitionBuilder The bean builder */ @Internal @@ -442,12 +469,12 @@ void addBeanDefinitionBuilder(JavaBeanDefinitionBuilder javaBeanDefinitionBuilde @Override public BeanElementBuilder addAssociatedBean(io.micronaut.inject.ast.Element originatingElement, ClassElement type) { - JavaBeanDefinitionBuilder javaBeanDefinitionBuilder = new JavaBeanDefinitionBuilder( - originatingElement, - type, - new JavaConfigurationMetadataBuilder(elements, types, annotationUtils), - this + return new JavaBeanDefinitionBuilder( + originatingElement, + type, + ConfigurationMetadataBuilder.INSTANCE, + type instanceof AbstractJavaElement ? ((AbstractJavaElement) type).elementAnnotationMetadataFactory : elementAnnotationMetadataFactory, + this ); - return javaBeanDefinitionBuilder; } } diff --git a/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaWildcardElement.java b/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaWildcardElement.java index a48df347efe..e290f967531 100644 --- a/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaWildcardElement.java +++ b/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaWildcardElement.java @@ -19,6 +19,7 @@ import io.micronaut.core.annotation.NonNull; import io.micronaut.inject.ast.ArrayableClassElement; import io.micronaut.inject.ast.ClassElement; +import io.micronaut.inject.ast.ElementAnnotationMetadataFactory; import io.micronaut.inject.ast.WildcardElement; import javax.lang.model.type.WildcardType; @@ -38,13 +39,16 @@ final class JavaWildcardElement extends JavaClassElement implements WildcardElem private final List upperBounds; private final List lowerBounds; - JavaWildcardElement(@NonNull WildcardType wildcardType, @NonNull List upperBounds, @NonNull List lowerBounds) { + JavaWildcardElement(ElementAnnotationMetadataFactory elementAnnotationMetadataFactory, + @NonNull WildcardType wildcardType, + @NonNull List upperBounds, + @NonNull List lowerBounds) { super( - upperBounds.get(0).classElement, - upperBounds.get(0).getAnnotationMetadata(), - upperBounds.get(0).visitorContext, - upperBounds.get(0).typeArguments, - upperBounds.get(0).getGenericTypeInfo() + upperBounds.get(0).classElement, + elementAnnotationMetadataFactory, + upperBounds.get(0).visitorContext, + upperBounds.get(0).typeArguments, + upperBounds.get(0).getGenericTypeInfo() ); this.wildcardType = wildcardType; this.upperBounds = upperBounds; @@ -80,7 +84,7 @@ public ClassElement withArrayDimensions(int arrayDimensions) { public ClassElement foldBoundGenericTypes(@NonNull Function fold) { List upperBounds = this.upperBounds.stream().map(ele -> toJavaClassElement(ele.foldBoundGenericTypes(fold))).collect(Collectors.toList()); List lowerBounds = this.lowerBounds.stream().map(ele -> toJavaClassElement(ele.foldBoundGenericTypes(fold))).collect(Collectors.toList()); - return fold.apply(upperBounds.contains(null) || lowerBounds.contains(null) ? null : new JavaWildcardElement(wildcardType, upperBounds, lowerBounds)); + return fold.apply(upperBounds.contains(null) || lowerBounds.contains(null) ? null : new JavaWildcardElement(elementAnnotationMetadataFactory, wildcardType, upperBounds, lowerBounds)); } private JavaClassElement toJavaClassElement(ClassElement element) { @@ -90,10 +94,10 @@ private JavaClassElement toJavaClassElement(ClassElement element) { if (element.isWildcard() || element.isGenericPlaceholder()) { throw new UnsupportedOperationException("Cannot convert wildcard / free type variable to JavaClassElement"); } else { - return (JavaClassElement) ((ArrayableClassElement) visitorContext.getClassElement(element.getName()) - .orElseThrow(() -> new UnsupportedOperationException("Cannot convert ClassElement to JavaClassElement, class was not found on the visitor context"))) - .withArrayDimensions(element.getArrayDimensions()) - .withBoundGenericTypes(element.getBoundGenericTypes()); + return (JavaClassElement) ((ArrayableClassElement) visitorContext.getClassElement(element.getName(), elementAnnotationMetadataFactory) + .orElseThrow(() -> new UnsupportedOperationException("Cannot convert ClassElement to JavaClassElement, class was not found on the visitor context"))) + .withArrayDimensions(element.getArrayDimensions()) + .withBoundGenericTypes(element.getBoundGenericTypes()); } } } diff --git a/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/LoadedVisitor.java b/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/LoadedVisitor.java index 1ef346f6449..104ac36b67c 100644 --- a/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/LoadedVisitor.java +++ b/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/LoadedVisitor.java @@ -22,9 +22,8 @@ import io.micronaut.core.reflect.GenericTypeUtils; import io.micronaut.inject.visitor.TypeElementVisitor; -import io.micronaut.core.annotation.Nullable; import javax.annotation.processing.ProcessingEnvironment; -import javax.lang.model.element.*; +import javax.lang.model.element.TypeElement; import javax.lang.model.type.TypeMirror; import java.util.List; @@ -44,22 +43,15 @@ public class LoadedVisitor implements Ordered { private final TypeElementVisitor visitor; private final String classAnnotation; private final String elementAnnotation; - private final JavaVisitorContext visitorContext; - private final JavaElementFactory elementFactory; - private JavaClassElement rootClassElement; /** * @param visitor The {@link TypeElementVisitor} - * @param visitorContext The visitor context * @param genericUtils The generic utils * @param processingEnvironment The {@link ProcessingEnvironment} */ public LoadedVisitor(TypeElementVisitor visitor, - JavaVisitorContext visitorContext, GenericUtils genericUtils, ProcessingEnvironment processingEnvironment) { - this.visitorContext = visitorContext; - this.elementFactory = visitorContext.getElementFactory(); this.visitor = visitor; Class aClass = visitor.getClass(); @@ -113,15 +105,13 @@ public TypeElementVisitor getVisitor() { } /** - * @param typeElement The class element + * @param annotationMetadata The annotation data * @return True if the class element should be visited */ - public boolean matches(TypeElement typeElement) { + public boolean matchesClass(AnnotationMetadata annotationMetadata) { if (classAnnotation.equals("java.lang.Object")) { return true; } - AnnotationMetadata annotationMetadata = visitorContext.getAnnotationUtils().getAnnotationMetadata(typeElement); - return annotationMetadata.hasStereotype(classAnnotation); } @@ -129,71 +119,13 @@ public boolean matches(TypeElement typeElement) { * @param annotationMetadata The annotation data * @return True if the element should be visited */ - public boolean matches(AnnotationMetadata annotationMetadata) { + public boolean matchesElement(AnnotationMetadata annotationMetadata) { if (elementAnnotation.equals("java.lang.Object")) { return true; } return annotationMetadata.hasStereotype(elementAnnotation); } - /** - * Invoke the underlying visitor for the given element. - * - * @param element The element to visit - * @param annotationMetadata The annotation data for the node - * - * @return The element if one was created or null - */ - public @Nullable io.micronaut.inject.ast.Element visit( - Element element, AnnotationMetadata annotationMetadata) { - if (element instanceof VariableElement) { - if (element.getKind() == ElementKind.ENUM_CONSTANT) { - final JavaEnumConstantElement e = elementFactory.newEnumConstantElement(rootClassElement, (VariableElement) element, annotationMetadata); - visitor.visitEnumConstant( - e, - visitorContext - ); - return e; - } else { - final JavaFieldElement e = elementFactory.newFieldElement(rootClassElement, (VariableElement) element, annotationMetadata); - visitor.visitField( - e, - visitorContext - ); - return e; - } - } else if (element instanceof ExecutableElement) { - ExecutableElement executableElement = (ExecutableElement) element; - if (rootClassElement != null) { - - if (executableElement.getSimpleName().toString().equals("")) { - final JavaConstructorElement e = elementFactory.newConstructorElement(rootClassElement, executableElement, annotationMetadata); - visitor.visitConstructor( - e, - visitorContext - ); - return e; - } else { - final JavaMethodElement e = elementFactory.newSourceMethodElement(rootClassElement, executableElement, annotationMetadata); - visitor.visitMethod( - e, - visitorContext - ); - return e; - } - } - } else if (element instanceof TypeElement) { - TypeElement typeElement = (TypeElement) element; - this.rootClassElement = elementFactory.newSourceClassElement(typeElement, annotationMetadata); - visitor.visitClass( - rootClassElement, - visitorContext - ); - return rootClassElement; - } - return null; - } - @Override public String toString() { return visitor.toString(); diff --git a/inject-java/src/test/groovy/io/micronaut/annotation/JavaEnumElementSpec.groovy b/inject-java/src/test/groovy/io/micronaut/annotation/JavaEnumElementSpec.groovy index 9263176707c..5565e82ad91 100644 --- a/inject-java/src/test/groovy/io/micronaut/annotation/JavaEnumElementSpec.groovy +++ b/inject-java/src/test/groovy/io/micronaut/annotation/JavaEnumElementSpec.groovy @@ -44,7 +44,7 @@ package test; enum MyEnum { @io.micronaut.annotation.EnumConstantAnn("C") - A, + A, B } """) diff --git a/inject-java/src/test/groovy/io/micronaut/annotation/mapping/AnnotationMappingSpec.groovy b/inject-java/src/test/groovy/io/micronaut/annotation/mapping/AnnotationMappingSpec.groovy index 63446b5cfd3..451550e7aee 100644 --- a/inject-java/src/test/groovy/io/micronaut/annotation/mapping/AnnotationMappingSpec.groovy +++ b/inject-java/src/test/groovy/io/micronaut/annotation/mapping/AnnotationMappingSpec.groovy @@ -34,11 +34,11 @@ class MyEntity { public MyEntity(final Key id) { this.id = id; } - + public Key getId() { return this.id; } - + public void setId(final Key id) { this.id = id; } @@ -60,11 +60,11 @@ class MyEntity { public MyEntity(final Key id) { this.id = id; } - + public Key getId() { return this.id; } - + public void setId(final Key id) { this.id = id; } @@ -81,7 +81,7 @@ class Key { idWithoutMapped.hasStereotype(Id.class) idWithoutMapped.hasStereotype(EmbeddedId.class) idWithMapped.getAnnotationNames() == (idWithoutMapped.getAnnotationNames() + ["io.micronaut.annotation.mapping.CustomEmbeddedId"]) - idWithMapped.getDeclaredAnnotationNames() == idWithoutMapped.getDeclaredAnnotationNames() + idWithMapped.getDeclaredAnnotationNames() == (idWithoutMapped.getDeclaredAnnotationNames() + ["io.micronaut.annotation.mapping.CustomEmbeddedId"]) } void "test @NonNull stereotype from @Nullable"() { diff --git a/inject-java/src/test/groovy/io/micronaut/annotation/processing/visitor/JavaReconstructionSpec.groovy b/inject-java/src/test/groovy/io/micronaut/annotation/processing/visitor/JavaReconstructionSpec.groovy index af57aaa3d51..e4c41a800f1 100644 --- a/inject-java/src/test/groovy/io/micronaut/annotation/processing/visitor/JavaReconstructionSpec.groovy +++ b/inject-java/src/test/groovy/io/micronaut/annotation/processing/visitor/JavaReconstructionSpec.groovy @@ -426,7 +426,8 @@ class Test { List placeholders = fieldType.getDeclaredGenericPlaceholders() expect: - placeholders.every { it.nativeType.class.simpleName == "TypeVar" } + // Native types should be Element if possible + placeholders.every { it.nativeType.class.simpleName == "TypeVariableSymbol" } reconstructTypeSignature(fieldType.foldBoundGenericTypes { if (it.isGenericPlaceholder() && ((GenericPlaceholderElement) it).variableName == 'T') { return ClassElement.of(String) diff --git a/inject-java/src/test/groovy/io/micronaut/aop/compile/OriginatingElementsSpec.groovy b/inject-java/src/test/groovy/io/micronaut/aop/compile/OriginatingElementsSpec.groovy index cec41d51ebd..35bee8ace84 100644 --- a/inject-java/src/test/groovy/io/micronaut/aop/compile/OriginatingElementsSpec.groovy +++ b/inject-java/src/test/groovy/io/micronaut/aop/compile/OriginatingElementsSpec.groovy @@ -6,9 +6,6 @@ import io.micronaut.inject.writer.BeanDefinitionVisitor import io.micronaut.inject.writer.StaticOriginatingElements import spock.util.environment.RestoreSystemProperties -import javax.validation.constraints.Min -import javax.validation.constraints.NotBlank - class OriginatingElementsSpec extends AbstractTypeElementSpec { def cleanup() { @@ -175,9 +172,9 @@ class MyBean extends MyBase { } abstract class MyBase { - + private ConversionService conversionService; - + @Inject void setConversionService(ConversionService conversionService) { this.conversionService = conversionService; @@ -230,7 +227,7 @@ interface MyBean extends MyInterface { StaticOriginatingElements.INSTANCE.originatingElements.size() == 2 StaticOriginatingElements.INSTANCE.originatingElements[0].name == 'test.MyBean' StaticOriginatingElements.INSTANCE.originatingElements[1].name == 'test.MyInterface' - + beanDefinition.getExecutableMethods().size() == 2 } @RestoreSystemProperties diff --git a/inject-java/src/test/groovy/io/micronaut/inject/annotation/AddStereotypesFromVisitorSpec.groovy b/inject-java/src/test/groovy/io/micronaut/inject/annotation/AddStereotypesFromVisitorSpec.groovy index c96e77d3ef1..7804f58ed24 100644 --- a/inject-java/src/test/groovy/io/micronaut/inject/annotation/AddStereotypesFromVisitorSpec.groovy +++ b/inject-java/src/test/groovy/io/micronaut/inject/annotation/AddStereotypesFromVisitorSpec.groovy @@ -3,7 +3,6 @@ package io.micronaut.inject.annotation import io.micronaut.annotation.processing.test.AbstractTypeElementSpec import io.micronaut.aop.Intercepted import io.micronaut.aop.InterceptorBinding -import io.micronaut.aop.simple.Mutating import io.micronaut.core.annotation.AnnotationUtil import io.micronaut.core.annotation.AnnotationValueBuilder import io.micronaut.inject.ast.ClassElement @@ -33,7 +32,7 @@ import java.util.Locale; @MyScope class TestBean { - @MyQualifier public Other other; + @MyQualifier public Other other; } @MyQualifier diff --git a/inject-java/src/test/groovy/io/micronaut/inject/annotation/AnnotatedFieldWithSetterSpec.groovy b/inject-java/src/test/groovy/io/micronaut/inject/annotation/AnnotatedFieldWithSetterSpec.groovy index 34ea43569c2..4d800e5a0cf 100644 --- a/inject-java/src/test/groovy/io/micronaut/inject/annotation/AnnotatedFieldWithSetterSpec.groovy +++ b/inject-java/src/test/groovy/io/micronaut/inject/annotation/AnnotatedFieldWithSetterSpec.groovy @@ -1,23 +1,15 @@ package io.micronaut.inject.annotation -import io.micronaut.annotation.processing.JavaAnnotationMetadataBuilder import io.micronaut.annotation.processing.test.AbstractTypeElementSpec import io.micronaut.context.ApplicationContext -import io.micronaut.core.annotation.AccessorsStyle import io.micronaut.core.annotation.AnnotationMetadata import io.micronaut.core.annotation.AnnotationValue import io.micronaut.core.convert.format.MapFormat -import io.micronaut.core.naming.NameUtils import io.micronaut.core.naming.conventions.StringConvention -import org.intellij.lang.annotations.Language +import io.micronaut.inject.ast.ClassElement +import io.micronaut.inject.ast.PropertyElement import spock.lang.Issue -import javax.lang.model.element.Element -import javax.lang.model.element.ElementKind -import javax.lang.model.element.ExecutableElement -import javax.lang.model.element.TypeElement -import javax.lang.model.element.VariableElement - @Issue('https://github.com/micronaut-projects/micronaut-core/issues/4308') class AnnotatedFieldWithSetterSpec extends AbstractTypeElementSpec { @@ -35,7 +27,7 @@ import java.util.Map; class AnnotatedFieldWithSetter { @MapFormat(keyFormat = StringConvention.RAW) private Map animals; - + public void setAnimals(Map animals) { this.animals = animals; } @@ -43,10 +35,12 @@ class AnnotatedFieldWithSetter { ''' when: - AnnotationMetadata metadata = buildArgumentAnnotationMetadataForSetterAndField(code, 'setAnimals') - AnnotationValue annotationValue = metadata.getAnnotation(MapFormat) + ClassElement classElement = buildClassElement(code) + PropertyElement propertyElement = classElement.getBeanProperties().iterator().next() then: + AnnotationMetadata metadata = propertyElement.getAnnotationMetadata() + AnnotationValue annotationValue = metadata.getAnnotation(MapFormat) annotationValue != null annotationValue.get('keyFormat', StringConvention).orElse(null) == StringConvention.RAW } @@ -66,35 +60,4 @@ class AnnotatedFieldWithSetter { context.close() } - - private AnnotationMetadata buildArgumentAnnotationMetadataForSetterAndField(@Language("java") String cls, String methodName) { - TypeElement element = buildTypeElement(cls) - ExecutableElement method = (ExecutableElement) element.enclosedElements.find { - it.kind == ElementKind.METHOD && it.simpleName.contentEquals(methodName) - } - if (!method) { - throw new RuntimeException("Method ${methodName} not found.") - } - - JavaAnnotationMetadataBuilder builder = newJavaAnnotationBuilder() - AnnotationMetadata methodAnnotationMetadata = builder.build(method) - - String[] writerPrefixes = methodAnnotationMetadata - .getValue(AccessorsStyle.class, "writePrefixes", String[].class) - .orElse(new String[]{AccessorsStyle.DEFAULT_WRITE_PREFIX}) - - if (!NameUtils.isWriterName(methodName, writerPrefixes) || method.parameters.size() != 1) { - throw new RuntimeException("Method ${methodName} is not a setter.") - } - - VariableElement argument = method.parameters.first() - String expectedFieldName = NameUtils.getPropertyNameForSetter(methodName, writerPrefixes) - - Element field = element.enclosedElements.find { - it.kind == ElementKind.FIELD && it.simpleName.contentEquals(expectedFieldName) - } - - argument != null ? builder.buildForParent(field, argument) : null - } - } diff --git a/inject-java/src/test/groovy/io/micronaut/inject/annotation/AnnotationMetadataHierarchySpec.groovy b/inject-java/src/test/groovy/io/micronaut/inject/annotation/AnnotationMetadataHierarchySpec.groovy index f36c57ee8ea..a14b1cdddfc 100644 --- a/inject-java/src/test/groovy/io/micronaut/inject/annotation/AnnotationMetadataHierarchySpec.groovy +++ b/inject-java/src/test/groovy/io/micronaut/inject/annotation/AnnotationMetadataHierarchySpec.groovy @@ -1,14 +1,64 @@ package io.micronaut.inject.annotation import io.micronaut.annotation.processing.test.AbstractTypeElementSpec +import io.micronaut.context.annotation.ConfigurationReader import io.micronaut.context.annotation.Executable import io.micronaut.context.annotation.Property import io.micronaut.core.annotation.AnnotationMetadata import io.micronaut.core.annotation.AnnotationUtil import io.micronaut.core.annotation.AnnotationValue +import org.intellij.lang.annotations.Language class AnnotationMetadataHierarchySpec extends AbstractTypeElementSpec{ + void "test merge annotation metadata"() { + when: + @Language("java") + def source = '''\ +package test; + +import io.micronaut.context.annotation.*; + +@Property(name = "myprop", value = "xyz") +class Test { + + @Property(name = "myprop", value = "abc") + void someMethod() {} +} +''' + AnnotationMetadata methodMetadata = buildMethodAnnotationMetadata(source, 'someMethod') + AnnotationMetadata typeMetadata = buildTypeAnnotationMetadata(source) + AnnotationMetadata annotationMetadata = new AnnotationMetadataHierarchy(typeMetadata, methodMetadata).merge() + + then: + annotationMetadata.stringValue(Property.class).get() == "abc" + } + + void "test merge annotation metadata2"() { + when: + @Language("java") + def source = '''\ +package test; + +import io.micronaut.context.annotation.*; + +@ConfigurationProperties(value = "xyz", includes = "abc", excludes = "lol") +class Test { + + @ConfigurationProperties(value = "qwe", excludes = "foo") + void someMethod() {} +} +''' + AnnotationMetadata methodMetadata = buildMethodAnnotationMetadata(source, 'someMethod') + AnnotationMetadata typeMetadata = buildTypeAnnotationMetadata(source) + AnnotationMetadata annotationMetadata = new AnnotationMetadataHierarchy(typeMetadata, methodMetadata).merge() + + then: + annotationMetadata.stringValue(ConfigurationReader.class, "prefix").get() == "qwe" + annotationMetadata.stringValue(ConfigurationReader.class, "includes").get() == "abc" + annotationMetadata.stringValue(ConfigurationReader.class, "excludes").get() == "foo" + } + void "test basic method annotation metadata"() { given: @@ -24,7 +74,7 @@ class Test { void someMethod() {} } ''' - AnnotationMetadata methodMetadata = buildDeclaredMethodAnnotationMetadata(source, 'someMethod') + AnnotationMetadata methodMetadata = buildMethodAnnotationMetadata(source, 'someMethod') AnnotationMetadata typeMetadata = buildTypeAnnotationMetadata(source) AnnotationMetadata annotationMetadata = new AnnotationMetadataHierarchy(typeMetadata, methodMetadata) @@ -63,9 +113,9 @@ package test; import io.micronaut.inject.annotation.repeatable.*; import io.micronaut.context.annotation.*; -@Property(name="prop2", value="value2") -@Property(name="prop3", value="value33") -@Property(name="prop4", value="value4") +@Property(name="prop2", value="value2") +@Property(name="prop3", value="value33") +@Property(name="prop4", value="value4") class Test { } ''') @@ -108,7 +158,7 @@ class Test { void someMethod() {} } ''' - AnnotationMetadata methodMetadata = buildDeclaredMethodAnnotationMetadata(source, 'someMethod') + AnnotationMetadata methodMetadata = buildMethodAnnotationMetadata(source, 'someMethod') AnnotationMetadata typeMetadata = buildTypeAnnotationMetadata(source) AnnotationMetadata annotationMetadata = new AnnotationMetadataHierarchy(typeMetadata, methodMetadata) diff --git a/inject-java/src/test/groovy/io/micronaut/inject/annotation/AnnotationMetadataWriterSpec.groovy b/inject-java/src/test/groovy/io/micronaut/inject/annotation/AnnotationMetadataWriterSpec.groovy index 3c31456b3cc..2b14629a97e 100644 --- a/inject-java/src/test/groovy/io/micronaut/inject/annotation/AnnotationMetadataWriterSpec.groovy +++ b/inject-java/src/test/groovy/io/micronaut/inject/annotation/AnnotationMetadataWriterSpec.groovy @@ -16,6 +16,7 @@ package io.micronaut.inject.annotation import io.micrometer.core.annotation.Timed +import io.micronaut.annotation.processing.test.AbstractTypeElementSpec import io.micronaut.aop.Around import io.micronaut.aop.introduction.StubIntroducer import io.micronaut.context.annotation.Primary @@ -23,24 +24,17 @@ import io.micronaut.context.annotation.Property import io.micronaut.context.annotation.Requirements import io.micronaut.context.annotation.Requires import io.micronaut.context.annotation.Type -import io.micronaut.core.annotation.AnnotationMetadata import io.micronaut.core.annotation.AnnotationClassValue +import io.micronaut.core.annotation.AnnotationMetadata import io.micronaut.core.annotation.AnnotationUtil import io.micronaut.core.annotation.AnnotationValue -import io.micronaut.core.annotation.Introspected import io.micronaut.core.annotation.TypeHint -import io.micronaut.annotation.processing.test.AbstractTypeElementSpec import io.micronaut.inject.BeanDefinition import io.micronaut.retry.annotation.Recoverable - -import jakarta.inject.Qualifier -import jakarta.inject.Scope -import jakarta.inject.Singleton import spock.lang.Unroll import java.lang.annotation.Documented import java.lang.annotation.Retention - /** * @author Graeme Rocher * @since 1.0 @@ -58,7 +52,7 @@ import io.micronaut.inject.annotation.Outer; @Outer.Inner class Test { - + } """) @@ -124,7 +118,7 @@ class Test { expect: metadata != null metadata.declaredAnnotationNames.size() == 1 - metadata.declaredStereotypes == null + metadata.declaredStereotypeAnnotationNames.size() == 0 metadata.annotationNames.size() == 1 } @@ -570,9 +564,9 @@ import io.micronaut.context.annotation.*; @jakarta.inject.Singleton class Test { - @Property(name="prop2", value="value2") - @Property(name="prop3", value="value33") - @Property(name="prop4", value="value4") + @Property(name="prop2", value="value2") + @Property(name="prop3", value="value33") + @Property(name="prop4", value="value4") @io.micronaut.context.annotation.Executable void someMethod() {} } @@ -608,9 +602,9 @@ import io.micronaut.context.annotation.*; @jakarta.inject.Singleton class Test { - @Property(name="prop2", value="value2") - @Property(name="prop3", value="value33") - @Property(name="prop4", value="value4") + @Property(name="prop2", value="value2") + @Property(name="prop3", value="value33") + @Property(name="prop4", value="value4") @io.micronaut.context.annotation.Executable void someMethod() {} } @@ -646,9 +640,9 @@ import io.micronaut.context.annotation.*; @jakarta.inject.Singleton class Test { - @Property(name="prop2", value="value2") - @Property(name="prop3", value="value33") - @Property(name="prop4", value="value4") + @Property(name="prop2", value="value2") + @Property(name="prop3", value="value33") + @Property(name="prop4", value="value4") @io.micronaut.context.annotation.Executable void someMethod() {} } diff --git a/inject-java/src/test/groovy/io/micronaut/inject/annotation/RemoveAnnotationSpec.groovy b/inject-java/src/test/groovy/io/micronaut/inject/annotation/RemoveAnnotationSpec.groovy index 9912e2332e7..44b22fc78a8 100644 --- a/inject-java/src/test/groovy/io/micronaut/inject/annotation/RemoveAnnotationSpec.groovy +++ b/inject-java/src/test/groovy/io/micronaut/inject/annotation/RemoveAnnotationSpec.groovy @@ -29,7 +29,7 @@ import io.micronaut.context.annotation.Bean; @Bean class Test { -} +} ''') expect: definition @@ -54,7 +54,7 @@ import io.micronaut.context.annotation.Bean; @Bean class Test { -} +} ''') expect: definition @@ -78,7 +78,7 @@ import io.micronaut.context.annotation.Bean; @Bean class Test { -} +} ''') expect:"The ScopeOne stereotype was removed but Scope remains as it was not removed" definition @@ -103,7 +103,7 @@ import io.micronaut.context.annotation.Bean; @Bean class Test { -} +} ''') expect: definition @@ -123,11 +123,11 @@ package removeann; import io.micronaut.inject.annotation.Trace; import io.micronaut.context.annotation.Bean; -@Trace(type = String.class, types = String.class) +@Trace(type = String.class, types = String.class) @Bean class Test { -} +} ''') expect: definition @@ -146,10 +146,10 @@ package removeann; import io.micronaut.inject.annotation.Trace; import io.micronaut.context.annotation.Bean; -@Trace(type = String.class, types = String.class) +@Trace(type = String.class, types = String.class) class Test { -} +} ''') expect: definition == null @@ -170,7 +170,7 @@ import io.micronaut.inject.annotation.repeatable.Topic; @Bean class Test { -} +} ''') expect: definition diff --git a/inject-java/src/test/groovy/io/micronaut/inject/beanbuilder/ApplyAopToMethodVisitor.java b/inject-java/src/test/groovy/io/micronaut/inject/beanbuilder/ApplyAopToMethodVisitor.java index 5eb7504bd8e..7c81033e005 100644 --- a/inject-java/src/test/groovy/io/micronaut/inject/beanbuilder/ApplyAopToMethodVisitor.java +++ b/inject-java/src/test/groovy/io/micronaut/inject/beanbuilder/ApplyAopToMethodVisitor.java @@ -2,7 +2,6 @@ import io.micronaut.core.annotation.AnnotationUtil; import io.micronaut.core.annotation.AnnotationValue; -import io.micronaut.core.annotation.AnnotationValueBuilder; import io.micronaut.inject.ast.ClassElement; import io.micronaut.inject.ast.ElementQuery; import io.micronaut.inject.visitor.TypeElementVisitor; diff --git a/inject-java/src/test/groovy/io/micronaut/inject/beanbuilder/BuildElementBuilderProcessedMethodsSpec.groovy b/inject-java/src/test/groovy/io/micronaut/inject/beanbuilder/BuildElementBuilderProcessedMethodsSpec.groovy index 748041fdc23..0a0bf9f1791 100644 --- a/inject-java/src/test/groovy/io/micronaut/inject/beanbuilder/BuildElementBuilderProcessedMethodsSpec.groovy +++ b/inject-java/src/test/groovy/io/micronaut/inject/beanbuilder/BuildElementBuilderProcessedMethodsSpec.groovy @@ -21,7 +21,7 @@ import jakarta.inject.Singleton; @Singleton class Foo { - + } ''') diff --git a/inject-java/src/test/groovy/io/micronaut/inject/beanbuilder/TestBeanDefiningVisitor.java b/inject-java/src/test/groovy/io/micronaut/inject/beanbuilder/TestBeanDefiningVisitor.java index 5da219e35e9..6a1a8c2e657 100644 --- a/inject-java/src/test/groovy/io/micronaut/inject/beanbuilder/TestBeanDefiningVisitor.java +++ b/inject-java/src/test/groovy/io/micronaut/inject/beanbuilder/TestBeanDefiningVisitor.java @@ -4,7 +4,6 @@ import io.micronaut.aop.InterceptorBinding; import io.micronaut.aop.InterceptorBindingDefinitions; import io.micronaut.aop.InterceptorKind; -import io.micronaut.aop.MethodInterceptor; import io.micronaut.context.annotation.Bean; import io.micronaut.context.annotation.Executable; import io.micronaut.context.env.Environment; diff --git a/inject-java/src/test/groovy/io/micronaut/inject/configproperties/ConfigPropertiesParseSpec.groovy b/inject-java/src/test/groovy/io/micronaut/inject/configproperties/ConfigPropertiesParseSpec.groovy index 559ac3059e5..69a903db13b 100644 --- a/inject-java/src/test/groovy/io/micronaut/inject/configproperties/ConfigPropertiesParseSpec.groovy +++ b/inject-java/src/test/groovy/io/micronaut/inject/configproperties/ConfigPropertiesParseSpec.groovy @@ -4,6 +4,7 @@ import io.micronaut.context.ApplicationContext import io.micronaut.context.BeanContext import io.micronaut.context.annotation.ConfigurationReader import io.micronaut.context.annotation.Property +import io.micronaut.context.annotation.PropertySource import io.micronaut.core.convert.format.ReadableBytes import io.micronaut.annotation.processing.test.AbstractTypeElementSpec import io.micronaut.inject.BeanDefinition @@ -32,7 +33,7 @@ class MyConfig { public void setHost(String host) { this.host = host; } - + @ConfigurationProperties("baz") static class ChildConfig extends ParentConfig { protected String stuff; @@ -41,7 +42,7 @@ class MyConfig { class ParentConfig { private String foo; - + public void setFoo(String foo) { this.foo = foo; } @@ -80,7 +81,7 @@ class MyConfig { public void setHost(String host) { this.host = host; } - + @ConfigurationProperties("baz") static class ChildConfig { protected String stuff; @@ -116,15 +117,15 @@ class MyConfig { public void setHost(String host) { this.host = host; } - + @ConfigurationProperties("baz") static class ChildConfig { String stuff; - + public String getStuff() { return stuff; } - + public void setStuff(String stuff) { this.stuff = stuff; } @@ -160,27 +161,27 @@ class MyConfig { public void setHost(String host) { this.host = host; } - + @ConfigurationProperties("baz") static class ChildConfig { String stuff; - + public String getStuff() { return stuff; } - + public void setStuff(String stuff) { this.stuff = stuff; } - + @ConfigurationProperties("more") static class MoreConfig { String stuff; - + public String getStuff() { return stuff; } - + public void setStuff(String stuff) { this.stuff = stuff; } @@ -216,15 +217,15 @@ class MyConfig extends ParentConfig { public void setHost(String host) { this.host = host; } - + @ConfigurationProperties("baz") static class ChildConfig { String stuff; - + public String getStuff() { return stuff; } - + public void setStuff(String stuff) { this.stuff = stuff; } @@ -379,7 +380,7 @@ import java.time.Duration; @ConfigurationProperties("http.client") public class HttpClientConfiguration { private int maxContentLength = 1024 * 1024 * 10; // 10MB; - + void setMaxContentLength(@ReadableBytes int maxContentLength) { this.maxContentLength = maxContentLength; } @@ -407,7 +408,7 @@ import java.time.Duration; @ConfigurationProperties("http.client") public class HttpClientConfiguration { private int maxContentLength = 1024 * 1024 * 10; // 10MB; - + public void setMaxContentLength(@ReadableBytes int maxContentLength) { this.maxContentLength = maxContentLength; } @@ -420,8 +421,10 @@ public class HttpClientConfiguration { then: beanDefinition.injectedFields.size() == 0 beanDefinition.injectedMethods.size() == 1 - beanDefinition.injectedMethods[0].arguments[0].synthesizeAll().size() == 1 + beanDefinition.injectedMethods[0].arguments[0].synthesizeAll().size() == 2 beanDefinition.injectedMethods[0].arguments[0].synthesize(ReadableBytes) + // This should be removed in Micronaut 4 + beanDefinition.injectedMethods[0].arguments[0].synthesize(PropertySource) } void "test different inject types for config properties"() { @@ -441,8 +444,8 @@ class MyProperties { public void setSetterTest(String s) { this.internalField = s; } - - public String getSetter() { return this.internalField; } + + public String getSetter() { return this.internalField; } } ''') then: @@ -492,18 +495,18 @@ class MyProperties extends Parent { public void setSetterTest(String s) { this.internalField = s; } - - public String getSetter() { return this.internalField; } + + public String getSetter() { return this.internalField; } } class Parent { private String parentField; - + public void setParentTest(String s) { this.parentField = s; } - - public String getParentTest() { return this.parentField; } + + public String getParentTest() { return this.parentField; } } ''') then: @@ -560,12 +563,12 @@ public class FooConfigurationProperties { public void setIssuer(String issuer) { this.issuer = issuer; } - + //isEnabled field maps to setEnabled method public void setEnabled(boolean enabled) { this.isEnabled = enabled; } - + //isOther field does not map to setOther method because its the class and not primitive public void setOther(Boolean other) { this.isOther = other; @@ -612,7 +615,7 @@ class MyConfig { package test; import io.micronaut.context.annotation.*; - + @ConfigurationProperties(value = "foo", includes = {"publicField", "parentPublicField"}) class MyProperties extends Parent { public String publicField; @@ -719,16 +722,16 @@ import io.micronaut.inject.configuration.Engine; @ConfigurationProperties(value = "foo", excludes = {"engine", "engine2"}) class MyProperties extends Parent { - @ConfigurationBuilder(prefixes = "with") + @ConfigurationBuilder(prefixes = "with") Engine.Builder engine = Engine.builder(); - + private Engine.Builder engine2 = Engine.builder(); - - @ConfigurationBuilder(configurationPrefix = "two", prefixes = "with") + + @ConfigurationBuilder(configurationPrefix = "two", prefixes = "with") public void setEngine2(Engine.Builder engine3) { this.engine2 = engine3; } - + public Engine.Builder getEngine2() { return engine2; } @@ -885,16 +888,16 @@ import jakarta.annotation.PostConstruct; public class EntityProperties { private String prop; - + @PostConstruct public void init() { System.out.println("prop = " + prop); } - + public String getProp() { return prop; } - + public void setProp(String prop) { this.prop = prop; } diff --git a/inject-java/src/test/groovy/io/micronaut/inject/configproperties/ConfigurationMetadataSpec.groovy b/inject-java/src/test/groovy/io/micronaut/inject/configproperties/ConfigurationMetadataSpec.groovy new file mode 100644 index 00000000000..b2c6e437b44 --- /dev/null +++ b/inject-java/src/test/groovy/io/micronaut/inject/configproperties/ConfigurationMetadataSpec.groovy @@ -0,0 +1,514 @@ +/* + * Copyright 2017-2019 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.inject.configproperties + +import com.fasterxml.jackson.databind.ObjectMapper +import io.micronaut.annotation.processing.ConfigurationMetadataProcessor +import io.micronaut.annotation.processing.test.AbstractTypeElementSpec +import io.micronaut.annotation.processing.test.JavaParser +import org.intellij.lang.annotations.Language + +import javax.annotation.processing.Processor + +class ConfigurationMetadataSpec extends AbstractTypeElementSpec { + + @Override + protected JavaParser newJavaParser() { + return new JavaParser() { + + @Override + protected List getAnnotationProcessors() { + def processors = super.getAnnotationProcessors() + processors.add(new ConfigurationMetadataProcessor()) + return processors + } + } + } + + private boolean jsonEquals(@Language("json") String provided, @Language("json") String expected) { + ObjectMapper mapper = new ObjectMapper() + def providedMap = mapper.readValue(provided, Map.class) + def expectedMap = mapper.readValue(expected, Map.class) + def providedJson = mapper.writeValueAsString(providedMap) + def expectedJson = mapper.writeValueAsString(expectedMap) + return providedJson == expectedJson + } + + protected String buildConfigurationMetadata(@Language("java") String cls) { + return super.buildAndReadResourceAsString("META-INF/spring-configuration-metadata.json", cls) + } + + void "test configuration builder on method"() { + when: + String metadataJson = buildConfigurationMetadata(''' +package test; + +import io.micronaut.context.annotation.*; + +@ConfigurationProperties("test") +class MyProperties { + + private Test test; + + @ConfigurationBuilder(factoryMethod="build") + void setTest(Test test) { + this.test = test; + } + + Test getTest() { + return this.test; + } + +} + +class Test { + private String foo; + private Test() {} + public void setFoo(String s) { + this.foo = s; + } + public String getFoo() { + return foo; + } + + static Test build() { + return new Test(); + } +} +''') + + then: + jsonEquals(metadataJson, ''' +{"groups":[{"name":"test","type":"test.MyProperties"}],"properties":[{"name":"test.foo","type":"java.lang.String","sourceType":"test.MyProperties"}]} +''') + } + + void "test configuration builder with includes"() { + when: + String metadataJson = buildConfigurationMetadata(''' +package test; + +import io.micronaut.context.annotation.*; + +@ConfigurationProperties("test") +class MyProperties { + + @ConfigurationBuilder(factoryMethod="build", includes="foo") + Test test; + +} + +class Test { + private String foo; + private String bar; + private Test() {} + public void setFoo(String s) { + this.foo = s; + } + public String getFoo() { + return foo; + } + public void setBar(String s) { + this.bar = s; + } + public String getBar() { + return bar; + } + + static Test build() { + return new Test(); + } +} +''') + then: + jsonEquals(metadataJson, ''' +{"groups":[{"name":"test","type":"test.MyProperties"}],"properties":[{"name":"test.foo","type":"java.lang.String","sourceType":"test.MyProperties"}]} +''') + } + + void "test configuration builder with factory method"() { + when: + String metadataJson = buildConfigurationMetadata(''' +package test; + +import io.micronaut.context.annotation.*; + +@ConfigurationProperties("test") +class MyProperties { + + @ConfigurationBuilder(factoryMethod="build") + Test test; + +} + +class Test { + private String foo; + private Test() {} + public void setFoo(String s) { + this.foo = s; + } + public String getFoo() { + return foo; + } + + static Test build() { + return new Test(); + } +} +''') + then: + jsonEquals(metadataJson, ''' +{"groups":[{"name":"test","type":"test.MyProperties"}],"properties":[{"name":"test.foo","type":"java.lang.String","sourceType":"test.MyProperties"}]} +''') + } + + void "test with setters that return void"() { + when: + String metadataJson = buildConfigurationMetadata(''' +package test; + +import io.micronaut.context.annotation.*; +import java.lang.Deprecated; + +@ConfigurationProperties("test") +class MyProperties { + + @ConfigurationBuilder + Test test = new Test(); + + +} + +class Test { + private String foo; + private int bar; + private Long baz; + + public void setFoo(String s) { this.foo = s;} + public void setBar(int s) {this.bar = s;} + @Deprecated + public void setBaz(Long s) {this.baz = s;} + + public String getFoo() { return this.foo; } + public int getBar() { return this.bar; } + public Long getBaz() { return this.baz; } +} +''') + then: + jsonEquals(metadataJson, ''' +{ + "groups": [ + { + "name": "test", + "type": "test.MyProperties" + } + ], + "properties": [ + { + "name": "test.foo", + "type": "java.lang.String", + "sourceType": "test.MyProperties" + }, + { + "name": "test.bar", + "type": "int", + "sourceType": "test.MyProperties" + } + ] +} +''') + } + + void "test different inject types for config properties"() { + when: + String metadataJson = buildConfigurationMetadata(''' +package test; + +import io.micronaut.context.annotation.*; +import org.neo4j.driver.v1.*; + +@ConfigurationProperties("neo4j.test") +class Neo4jProperties { + protected java.net.URI uri; + + @ConfigurationBuilder( + prefixes="with", + allowZeroArgs=true + ) + Config.ConfigBuilder options = Config.build(); + + +} +''') + then: + jsonEquals(metadataJson, ''' +{ + "groups": [ + { + "name": "neo4j.test", + "type": "test.Neo4jProperties" + } + ], + "properties": [ + { + "name": "neo4j.test.uri", + "type": "java.net.URI", + "sourceType": "test.Neo4jProperties" + }, + { + "name": "neo4j.test.logging", + "type": "org.neo4j.driver.v1.Logging", + "sourceType": "test.Neo4jProperties" + }, + { + "name": "neo4j.test.leaked-sessions-logging", + "type": "boolean", + "sourceType": "test.Neo4jProperties" + }, + { + "name": "neo4j.test.max-idle-sessions", + "type": "int", + "sourceType": "test.Neo4jProperties" + }, + { + "name": "neo4j.test.connection-liveness-check-timeout", + "type": "java.time.Duration", + "sourceType": "org.neo4j.driver.v1.Config$ConfigBuilder" + }, + { + "name": "neo4j.test.encryption", + "type": "boolean", + "sourceType": "test.Neo4jProperties" + }, + { + "name": "neo4j.test.trust-strategy", + "type": "org.neo4j.driver.v1.Config$TrustStrategy", + "sourceType": "test.Neo4jProperties" + }, + { + "name": "neo4j.test.connection-timeout", + "type": "java.time.Duration", + "sourceType": "org.neo4j.driver.v1.Config$ConfigBuilder" + }, + { + "name": "neo4j.test.max-transaction-retry-time", + "type": "java.time.Duration", + "sourceType": "org.neo4j.driver.v1.Config$ConfigBuilder" + } + ] +} +''') + } + + void "test specifying a configuration prefix"() { + when: + String metadataJson = buildConfigurationMetadata(''' +package test; + +import io.micronaut.context.annotation.*; +import org.neo4j.driver.v1.*; + +@ConfigurationProperties("neo4j.test") +class Neo4jProperties { + protected java.net.URI uri; + + @ConfigurationBuilder( + prefixes="with", + allowZeroArgs=true, + configurationPrefix="options" + ) + Config.ConfigBuilder options = Config.build(); + + +} +''') + then: + jsonEquals(metadataJson, ''' +{ + "groups": [ + { + "name": "neo4j.test", + "type": "test.Neo4jProperties" + } + ], + "properties": [ + { + "name": "neo4j.test.uri", + "type": "java.net.URI", + "sourceType": "test.Neo4jProperties" + }, + { + "name": "neo4j.test.options.logging", + "type": "org.neo4j.driver.v1.Logging", + "sourceType": "test.Neo4jProperties" + }, + { + "name": "neo4j.test.options.leaked-sessions-logging", + "type": "boolean", + "sourceType": "test.Neo4jProperties" + }, + { + "name": "neo4j.test.options.max-idle-sessions", + "type": "int", + "sourceType": "test.Neo4jProperties" + }, + { + "name": "neo4j.test.options.connection-liveness-check-timeout", + "type": "java.time.Duration", + "sourceType": "org.neo4j.driver.v1.Config$ConfigBuilder" + }, + { + "name": "neo4j.test.options.encryption", + "type": "boolean", + "sourceType": "test.Neo4jProperties" + }, + { + "name": "neo4j.test.options.trust-strategy", + "type": "org.neo4j.driver.v1.Config$TrustStrategy", + "sourceType": "test.Neo4jProperties" + }, + { + "name": "neo4j.test.options.connection-timeout", + "type": "java.time.Duration", + "sourceType": "org.neo4j.driver.v1.Config$ConfigBuilder" + }, + { + "name": "neo4j.test.options.max-transaction-retry-time", + "type": "java.time.Duration", + "sourceType": "org.neo4j.driver.v1.Config$ConfigBuilder" + } + ] +} +''') + } + + void "test inner"() { + when: + String metadataJson = buildConfigurationMetadata(''' +package test; + +import io.micronaut.context.annotation.ConfigurationProperties; + +import java.util.List; + +@ConfigurationProperties("foo.bar") +class MyConfigInner { + + private List innerVals; + + public List getInnerVals() { + return innerVals; + } + + public void setInnerVals(List innerVals) { + this.innerVals = innerVals; + } + + public static class InnerVal { + + private Integer expireUnsignedSeconds; + + public Integer getExpireUnsignedSeconds() { + return expireUnsignedSeconds; + } + + public void setExpireUnsignedSeconds(Integer expireUnsignedSeconds) { + this.expireUnsignedSeconds = expireUnsignedSeconds; + } + } + +} +''') + then: + jsonEquals(metadataJson, ''' +{"groups":[{"name":"foo.bar","type":"test.MyConfigInner"}],"properties":[{"name":"foo.bar.inner-vals","type":"java.util.List","sourceType":"test.MyConfigInner"}]} +''') + } + + void "test inheritance"() { + when: + String metadataJson = buildConfigurationMetadata(''' +package test; + +import io.micronaut.context.annotation.*; +import java.time.Duration; + +@ConfigurationProperties("foo.bar") +class MyConfig extends ParentConfig { + String host; + + + public String getHost() { + return host; + } + + public void setHost(String host) { + this.host = host; + } + + @ConfigurationProperties("baz") + static class ChildConfig { + String stuff; + + public String getStuff() { + return stuff; + } + + public void setStuff(String stuff) { + this.stuff = stuff; + } + } +} + +@ConfigurationProperties("parent") +class ParentConfig { + +} +''') + then: + jsonEquals(metadataJson, ''' +{ + "groups": [ + { + "name": "parent.foo.bar", + "type": "test.MyConfig" + }, + { + "name": "parent.foo.bar.baz", + "type": "test.MyConfig$ChildConfig" + }, + { + "name": "parent", + "type": "test.ParentConfig" + } + ], + "properties": [ + { + "name": "parent.foo.bar.host", + "type": "java.lang.String", + "sourceType": "test.MyConfig" + }, + { + "name": "parent.foo.bar.baz.stuff", + "type": "java.lang.String", + "sourceType": "test.MyConfig$ChildConfig" + } + ] +} +''') + } + +} diff --git a/inject-java/src/test/groovy/io/micronaut/inject/configproperties/InheritedConfigurationReaderPrefixSpec.groovy b/inject-java/src/test/groovy/io/micronaut/inject/configproperties/InheritedConfigurationReaderPrefixSpec.groovy new file mode 100644 index 00000000000..a12185ab02c --- /dev/null +++ b/inject-java/src/test/groovy/io/micronaut/inject/configproperties/InheritedConfigurationReaderPrefixSpec.groovy @@ -0,0 +1,131 @@ +/* + * Copyright 2017-2019 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.inject.configproperties + +import io.micronaut.annotation.processing.test.AbstractTypeElementSpec +import io.micronaut.context.annotation.Property +import io.micronaut.inject.BeanDefinition + +class InheritedConfigurationReaderPrefixSpec extends AbstractTypeElementSpec { + + void "property path is broken because alias is pointing to another alias"() { + given: + BeanDefinition beanDefinition = buildBeanDefinition('io.micronaut.inject.configproperties.MyBean', """ +package io.micronaut.inject.configproperties; + +@TestEndpoint1("simple") +class MyBean { + String myValue; + + public String getMyValue() { + return myValue; + } + + public void setMyValue(String myValue) { + this.myValue = myValue; + } +} + +""") + + expect: + beanDefinition.getInjectedMethods()[0].name == 'setMyValue' + def metadata = beanDefinition.getInjectedMethods()[0].getAnnotationMetadata() + metadata.hasAnnotation(Property) + metadata.getValue(Property, "name", String).get() == 'endpoints.my-value' + } + + void "property path is overriding the existing one without base prefix"() { + given: + BeanDefinition beanDefinition = buildBeanDefinition('io.micronaut.inject.configproperties.MyBean', """ +package io.micronaut.inject.configproperties; + +@TestEndpoint2("simple") +class MyBean { + String myValue; + + public String getMyValue() { + return myValue; + } + + public void setMyValue(String myValue) { + this.myValue = myValue; + } +} + +""") + + expect: + beanDefinition.getInjectedMethods()[0].name == 'setMyValue' + def metadata = beanDefinition.getInjectedMethods()[0].getAnnotationMetadata() + metadata.hasAnnotation(Property) + metadata.getValue(Property, "name", String).get() == 'simple.my-value' + } + + void "property path is broken because alias is pointing to another alias 2"() { + given: + BeanDefinition beanDefinition = buildBeanDefinition('io.micronaut.inject.configproperties.MyBean', """ +package io.micronaut.inject.configproperties; + +@TestEndpoint3("simple") +class MyBean { + String myValue; + + public String getMyValue() { + return myValue; + } + + public void setMyValue(String myValue) { + this.myValue = myValue; + } +} + +""") + + expect: + beanDefinition.getInjectedMethods()[0].name == 'setMyValue' + def metadata = beanDefinition.getInjectedMethods()[0].getAnnotationMetadata() + metadata.hasAnnotation(Property) + metadata.getValue(Property, "name", String).get() == 'endpoints.my-value' + } + + void "property path is overriding the existing one"() { + given: + BeanDefinition beanDefinition = buildBeanDefinition('io.micronaut.inject.configproperties.MyBean', """ +package io.micronaut.inject.configproperties; + +@TestEndpoint4("simple") +class MyBean { + String myValue; + + public String getMyValue() { + return myValue; + } + + public void setMyValue(String myValue) { + this.myValue = myValue; + } +} + +""") + + expect: + beanDefinition.getInjectedMethods()[0].name == 'setMyValue' + def metadata = beanDefinition.getInjectedMethods()[0].getAnnotationMetadata() + metadata.hasAnnotation(Property) + metadata.getValue(Property, "name", String).get() == 'endpoints.simple.my-value' + } +} diff --git a/inject-groovy/src/test/groovy/io/micronaut/inject/configproperties/TestEndpoint.java b/inject-java/src/test/groovy/io/micronaut/inject/configproperties/TestEndpoint.java similarity index 100% rename from inject-groovy/src/test/groovy/io/micronaut/inject/configproperties/TestEndpoint.java rename to inject-java/src/test/groovy/io/micronaut/inject/configproperties/TestEndpoint.java diff --git a/inject-java/src/test/groovy/io/micronaut/inject/configproperties/TestEndpoint1.java b/inject-java/src/test/groovy/io/micronaut/inject/configproperties/TestEndpoint1.java new file mode 100644 index 00000000000..a7a18b3eaea --- /dev/null +++ b/inject-java/src/test/groovy/io/micronaut/inject/configproperties/TestEndpoint1.java @@ -0,0 +1,49 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.inject.configproperties; + +import io.micronaut.context.annotation.AliasFor; +import io.micronaut.context.annotation.ConfigurationReader; +import jakarta.inject.Singleton; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +@Documented +@Retention(RUNTIME) +@Target(ElementType.TYPE) +@Singleton +@ConfigurationReader(prefix = "endpoints") +public @interface TestEndpoint1 { + /** + * @return The ID of the endpoint + */ + @AliasFor(annotation = ConfigurationReader.class, member = "value") + @AliasFor(member = "id") + String value() default ""; + + /** + * @return The ID of the endpoint + */ + @AliasFor(member = "value") + @AliasFor(annotation = ConfigurationReader.class, member = "value") + String id() default ""; + +} diff --git a/inject-java/src/test/groovy/io/micronaut/inject/configproperties/TestEndpoint2.java b/inject-java/src/test/groovy/io/micronaut/inject/configproperties/TestEndpoint2.java new file mode 100644 index 00000000000..da2c8fd672c --- /dev/null +++ b/inject-java/src/test/groovy/io/micronaut/inject/configproperties/TestEndpoint2.java @@ -0,0 +1,49 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.inject.configproperties; + +import io.micronaut.context.annotation.AliasFor; +import io.micronaut.context.annotation.ConfigurationReader; +import jakarta.inject.Singleton; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +@Documented +@Retention(RUNTIME) +@Target(ElementType.TYPE) +@Singleton +@ConfigurationReader(prefix = "endpoints") +public @interface TestEndpoint2 { + /** + * @return The ID of the endpoint + */ + @AliasFor(annotation = ConfigurationReader.class, member = ConfigurationReader.PREFIX) + @AliasFor(member = "id") + String value() default ""; + + /** + * @return The ID of the endpoint + */ + @AliasFor(member = "value") + @AliasFor(annotation = ConfigurationReader.class, member = ConfigurationReader.PREFIX) + String id() default ""; + +} diff --git a/inject-java/src/test/groovy/io/micronaut/inject/configproperties/TestEndpoint3.java b/inject-java/src/test/groovy/io/micronaut/inject/configproperties/TestEndpoint3.java new file mode 100644 index 00000000000..6bec93a75a9 --- /dev/null +++ b/inject-java/src/test/groovy/io/micronaut/inject/configproperties/TestEndpoint3.java @@ -0,0 +1,49 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.inject.configproperties; + +import io.micronaut.context.annotation.AliasFor; +import io.micronaut.context.annotation.ConfigurationReader; +import jakarta.inject.Singleton; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +@Documented +@Retention(RUNTIME) +@Target(ElementType.TYPE) +@Singleton +@ConfigurationReader(basePrefix = "endpoints") +public @interface TestEndpoint3 { + /** + * @return The ID of the endpoint + */ + @AliasFor(annotation = ConfigurationReader.class, member = "value") + @AliasFor(member = "id") + String value() default ""; + + /** + * @return The ID of the endpoint + */ + @AliasFor(member = "value") + @AliasFor(annotation = ConfigurationReader.class, member = "value") + String id() default ""; + +} diff --git a/inject-java/src/test/groovy/io/micronaut/inject/configproperties/TestEndpoint4.java b/inject-java/src/test/groovy/io/micronaut/inject/configproperties/TestEndpoint4.java new file mode 100644 index 00000000000..11d9a85df9e --- /dev/null +++ b/inject-java/src/test/groovy/io/micronaut/inject/configproperties/TestEndpoint4.java @@ -0,0 +1,49 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.inject.configproperties; + +import io.micronaut.context.annotation.AliasFor; +import io.micronaut.context.annotation.ConfigurationReader; +import jakarta.inject.Singleton; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +@Documented +@Retention(RUNTIME) +@Target(ElementType.TYPE) +@Singleton +@ConfigurationReader(basePrefix = "endpoints") +public @interface TestEndpoint4 { + /** + * @return The ID of the endpoint + */ + @AliasFor(annotation = ConfigurationReader.class, member = ConfigurationReader.PREFIX) + @AliasFor(member = "id") + String value() default ""; + + /** + * @return The ID of the endpoint + */ + @AliasFor(member = "value") + @AliasFor(annotation = ConfigurationReader.class, member = ConfigurationReader.PREFIX) + String id() default ""; + +} diff --git a/inject-java/src/test/groovy/io/micronaut/inject/configuration/JavaConfigurationMetadataBuilderSpec.groovy b/inject-java/src/test/groovy/io/micronaut/inject/configuration/JavaConfigurationMetadataBuilderSpec.groovy deleted file mode 100644 index 9965266327c..00000000000 --- a/inject-java/src/test/groovy/io/micronaut/inject/configuration/JavaConfigurationMetadataBuilderSpec.groovy +++ /dev/null @@ -1,478 +0,0 @@ -/* - * Copyright 2017-2019 original authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.micronaut.inject.configuration - - -import groovy.json.JsonSlurper -import io.micronaut.annotation.processing.AnnotationUtils -import io.micronaut.annotation.processing.GenericUtils -import io.micronaut.annotation.processing.JavaConfigurationMetadataBuilder -import io.micronaut.annotation.processing.ModelUtils -import io.micronaut.annotation.processing.test.AbstractTypeElementSpec -import io.micronaut.annotation.processing.test.JavaParser - -import javax.lang.model.element.TypeElement -/** - * @author Graeme Rocher - * @since 1.0 - */ -class JavaConfigurationMetadataBuilderSpec extends AbstractTypeElementSpec { - - void "test build configuration metadata with annotation aliases"() { - given: - TypeElement element = buildTypeElement(''' -package test; - -import io.micronaut.context.annotation.*; -import io.micronaut.inject.annotation.*; - -@MultipleAlias("foo") -class MyProperties { - protected String fieldTest = "unconfigured"; - private String internalField = "unconfigured"; - - public void setSetterTest(String s) { - this.internalField = s; - } - - public String getSetter() { return this.internalField; } -} -''') - - when: - def builder = createBuilder() - def configurationMetadata = builder.visitProperties(element, "some description") - def propertyMetadata = builder.visitProperty(element, element, "java.lang.String", "setterTest", "some description", null) - - then: - builder.configurations.size() == 1 - configurationMetadata.name == 'foo' - configurationMetadata.description == 'some description' - configurationMetadata.type == 'test.MyProperties' - - builder.properties.size() == 1 - propertyMetadata.name == 'setterTest' - propertyMetadata.path == 'foo.setter-test' - propertyMetadata.type == 'java.lang.String' - propertyMetadata.declaringType == 'test.MyProperties' - propertyMetadata.description == 'some description' - - - when:"the config metadata is converted to JSON" - def sw = new StringWriter() - configurationMetadata.writeTo(sw) - def text = sw.toString() - def json = new JsonSlurper().parseText(text) - - then:"the json is correct" - json.type == configurationMetadata.type - json.name == configurationMetadata.name - json.description == configurationMetadata.description - - - when:"the property metadata is converted to JSON " - - sw = new StringWriter() - propertyMetadata.writeTo(sw) - text = sw.toString() - println "text = $text" - json = new JsonSlurper().parseText(text) - - then:"the json is correct" - json.type == propertyMetadata.type - json.name == propertyMetadata.path - json.sourceType == propertyMetadata.declaringType - json.description == propertyMetadata.description - } - - void "test build configuration metadata for simple properties"() { - given: - TypeElement element = buildTypeElement(''' -package test; - -import io.micronaut.context.annotation.*; - -@ConfigurationProperties("foo") -class MyProperties { - protected String fieldTest = "unconfigured"; - private String internalField = "unconfigured"; - - public void setSetterTest(String s) { - this.internalField = s; - } - - public String getSetter() { return this.internalField; } -} -''') - - when: - def builder = createBuilder() - def configurationMetadata = builder.visitProperties(element, "some description") - def propertyMetadata = builder.visitProperty(element, element, "java.lang.String", "setterTest", "some description", null) - - then: - builder.configurations.size() == 1 - configurationMetadata.name == 'foo' - configurationMetadata.description == 'some description' - configurationMetadata.type == 'test.MyProperties' - - builder.properties.size() == 1 - propertyMetadata.name == 'setterTest' - propertyMetadata.path == 'foo.setter-test' - propertyMetadata.type == 'java.lang.String' - propertyMetadata.declaringType == 'test.MyProperties' - propertyMetadata.description == 'some description' - - - when:"the config metadata is converted to JSON" - def sw = new StringWriter() - configurationMetadata.writeTo(sw) - def text = sw.toString() - def json = new JsonSlurper().parseText(text) - - then:"the json is correct" - json.type == configurationMetadata.type - json.name == configurationMetadata.name - json.description == configurationMetadata.description - - - when:"the property metadata is converted to JSON " - - sw = new StringWriter() - propertyMetadata.writeTo(sw) - text = sw.toString() - println "text = $text" - json = new JsonSlurper().parseText(text) - - then:"the json is correct" - json.type == propertyMetadata.type - json.name == propertyMetadata.path - json.sourceType == propertyMetadata.declaringType - json.description == propertyMetadata.description - } - - - void "test build configuration metadata for inner class properties"() { - given: - TypeElement element = buildTypeElement(''' -package test; - -import io.micronaut.context.annotation.*; - -@ConfigurationProperties("foo") -class MyProperties { - protected String fieldTest = "unconfigured"; - private String internalField = "unconfigured"; - - public void setSetterTest(String s) { - this.internalField = s; - } - - public String getSetter() { return this.internalField; } - - - @ConfigurationProperties("inner") - static class InnerProperties { - protected String foo; - } -} -''') - - - when: - - - JavaConfigurationMetadataBuilder builder = createBuilder() - element = element.enclosedElements[0] - builder.visitProperties(element, "some description") - builder.visitProperty(element, element, "java.lang.String", "foo", "some description", null) - - then: - builder.configurations.size() == 1 - builder.configurations[0].name == 'foo.inner' - builder.configurations[0].description == 'some description' - builder.configurations[0].type == 'test.MyProperties.InnerProperties' - - builder.properties.size() == 1 - builder.properties[0].name == 'foo' - builder.properties[0].path == 'foo.inner.foo' - builder.properties[0].type == 'java.lang.String' - builder.properties[0].declaringType == 'test.MyProperties.InnerProperties' - builder.properties[0].description == 'some description' - } - - - void "test build configuration metadata for multi level inner class properties"() { - given: - TypeElement element = buildTypeElement(''' -package test; - -import io.micronaut.context.annotation.*; - -@ConfigurationProperties("foo") -class MyProperties { - protected String fieldTest = "unconfigured"; - private String internalField = "unconfigured"; - - public void setSetterTest(String s) { - this.internalField = s; - } - - public String getSetter() { return this.internalField; } - - - @ConfigurationProperties("inner") - static class InnerProperties { - protected String foo; - - @ConfigurationProperties("nested") - static class NestedProperties { - protected String foo; - } - } -} -''') - - - when: - - - JavaConfigurationMetadataBuilder builder = createBuilder() - element = element.enclosedElements[0].enclosedElements[0] - builder.visitProperties(element, "some description") - builder.visitProperty(element, element, "java.lang.String", "foo", "some description", null) - - then: - builder.configurations.size() == 1 - builder.configurations[0].name == 'foo.inner.nested' - builder.configurations[0].description == 'some description' - builder.configurations[0].type == 'test.MyProperties.InnerProperties.NestedProperties' - - builder.properties.size() == 1 - builder.properties[0].name == 'foo' - builder.properties[0].path == 'foo.inner.nested.foo' - builder.properties[0].type == 'java.lang.String' - builder.properties[0].declaringType == 'test.MyProperties.InnerProperties.NestedProperties' - builder.properties[0].description == 'some description' - } - - void "test build configuration metadata for single level inheritance properties"() { - given: - TypeElement element = buildTypeElement(''' -package test; - -import io.micronaut.context.annotation.*; - -@ConfigurationProperties("child") -class ChildProperties extends ParentProperties { - protected String prop1 = "unconfigured"; -} - -@ConfigurationProperties("parent") -class ParentProperties { - protected String prop2 = "test"; -} -''') - - when: - def builder = createBuilder() - builder.visitProperties(element, "some description") - builder.visitProperty(element, element, "java.lang.String", "setterTest", "some description", null) - - then: - builder.configurations.size() == 1 - builder.configurations[0].name == 'parent.child' - builder.configurations[0].description == 'some description' - builder.configurations[0].type == 'test.ChildProperties' - - builder.properties.size() == 1 - builder.properties[0].name == 'setterTest' - builder.properties[0].path == 'parent.child.setter-test' - builder.properties[0].type == 'java.lang.String' - builder.properties[0].declaringType == 'test.ChildProperties' - builder.properties[0].description == 'some description' - } - - void "test build configuration metadata for multi level inheritance properties"() { - given: - TypeElement element = buildTypeElement(''' -package test; - -import io.micronaut.context.annotation.*; - -@ConfigurationProperties("child") -class ChildProperties extends ParentProperties { - protected String prop1 = "unconfigured"; -} - -@ConfigurationProperties("parent") -class ParentProperties extends GrandParentProperties { - protected String prop2 = "test"; -} - -@ConfigurationProperties("grand") -class GrandParentProperties { - protected String prop3 = "test"; -} -''') - - when: - def builder = createBuilder() - builder.visitProperties(element, "some description") - builder.visitProperty(element, element, "java.lang.String", "setterTest", "some description", null) - - then: - builder.configurations.size() == 1 - builder.configurations[0].name == 'grand.parent.child' - builder.configurations[0].description == 'some description' - builder.configurations[0].type == 'test.ChildProperties' - - builder.properties.size() == 1 - builder.properties[0].name == 'setterTest' - builder.properties[0].path == 'grand.parent.child.setter-test' - builder.properties[0].type == 'java.lang.String' - builder.properties[0].declaringType == 'test.ChildProperties' - builder.properties[0].description == 'some description' - } - - - - void "test build configuration metadata for multi level inheritance inner properties"() { - given: - TypeElement element = buildTypeElement(''' -package test; - -import io.micronaut.context.annotation.*; - -@ConfigurationProperties("child") -class ChildProperties extends ParentProperties { - protected String prop1 = "unconfigured"; - - - @ConfigurationProperties("inner") - static class InnerProperties { - protected String foo; - } -} - -@ConfigurationProperties("parent") -class ParentProperties extends GrandParentProperties { - protected String prop2 = "test"; -} - -@ConfigurationProperties("grand") -class GrandParentProperties { - protected String prop3 = "test"; -} -''') - - when: - def builder = createBuilder() - element = element.enclosedElements[0] - builder.visitProperties(element, "some description") - builder.visitProperty(element, element, "java.lang.String", "foo", "some description", null) - - then: - builder.configurations.size() == 1 - builder.configurations[0].name == 'grand.parent.child.inner' - builder.configurations[0].description == 'some description' - builder.configurations[0].type == 'test.ChildProperties.InnerProperties' - - builder.properties.size() == 1 - builder.properties[0].name == 'foo' - builder.properties[0].path == 'grand.parent.child.inner.foo' - builder.properties[0].type == 'java.lang.String' - builder.properties[0].declaringType == 'test.ChildProperties.InnerProperties' - builder.properties[0].description == 'some description' - } - - - - void "test build configuration metadata for multi level inheritance inner inheritance properties"() { - given: - TypeElement element = buildTypeElement(''' -package test; - -import io.micronaut.context.annotation.*; - -@ConfigurationProperties("child") -class ChildProperties extends ParentProperties { - protected String prop1 = "unconfigured"; - - - - - @ConfigurationProperties("inner") - static class InnerProperties extends InnerParentProperties { - protected String foo; - } - - @ConfigurationProperties("innerParent") - static class InnerParentProperties { - protected String innerParent; - } -} - -@ConfigurationProperties("parent") -class ParentProperties extends GrandParentProperties { - protected String prop2 = "test"; -} - -@ConfigurationProperties("grand") -class GrandParentProperties { - protected String prop3 = "test"; -} -''') - - when: - def builder = createBuilder() - element = element.enclosedElements[0] - builder.visitProperties(element, "some description") - builder.visitProperty(element, element, "java.lang.String", "foo", "some description", null) - - then: - builder.configurations.size() == 1 - builder.configurations[0].name == 'grand.parent.child.inner-parent.inner' - builder.configurations[0].description == 'some description' - builder.configurations[0].type == 'test.ChildProperties.InnerProperties' - - builder.properties.size() == 1 - builder.properties[0].name == 'foo' - builder.properties[0].path == 'grand.parent.child.inner-parent.inner.foo' - builder.properties[0].type == 'java.lang.String' - builder.properties[0].declaringType == 'test.ChildProperties.InnerProperties' - builder.properties[0].description == 'some description' - } - - protected JavaConfigurationMetadataBuilder createBuilder() { - def javaParser = new JavaParser() - def javacTask = javaParser.getJavacTask() - def elements = javacTask.elements - def types = javacTask.types - def env = javaParser.processingEnv - ModelUtils modelUtils = new ModelUtils(elements, types) {} - GenericUtils genericUtils = new GenericUtils(elements, types, modelUtils) {} - AnnotationUtils annotationUtils = new AnnotationUtils(env, elements, env.messager, env.typeUtils, modelUtils,genericUtils, env.filer) { - } - - JavaConfigurationMetadataBuilder builder = new JavaConfigurationMetadataBuilder( - elements, - types, - new AnnotationUtils(env, elements, env.messager, env.typeUtils, modelUtils, genericUtils, env.filer) {} - ) - builder - } -} diff --git a/inject-java/src/test/groovy/io/micronaut/inject/executable/BookController.java b/inject-java/src/test/groovy/io/micronaut/inject/executable/BookController.java index 5e3e4a09f20..8d204b20592 100644 --- a/inject-java/src/test/groovy/io/micronaut/inject/executable/BookController.java +++ b/inject-java/src/test/groovy/io/micronaut/inject/executable/BookController.java @@ -112,7 +112,6 @@ static protected String showProtectedStatic(Long id) { return String.format("%d - The Stand", id); } - @Executable static private String showPrivateStatic(Long id) { return String.format("%d - The Stand", id); } diff --git a/inject-java/src/test/groovy/io/micronaut/inject/qualifiers/replaces/AnnotateReplacesSpec.groovy b/inject-java/src/test/groovy/io/micronaut/inject/qualifiers/replaces/AnnotateReplacesSpec.groovy index c282a0967e3..bd945eaa7d9 100644 --- a/inject-java/src/test/groovy/io/micronaut/inject/qualifiers/replaces/AnnotateReplacesSpec.groovy +++ b/inject-java/src/test/groovy/io/micronaut/inject/qualifiers/replaces/AnnotateReplacesSpec.groovy @@ -34,7 +34,7 @@ import java.util.List; @Singleton class Catalog { @Inject - public PaymentProcessor paymentProcessor; + public PaymentProcessor paymentProcessor; } @Singleton @Factory @@ -148,4 +148,4 @@ class Product { @Retention(RetentionPolicy.RUNTIME) @interface TestSpecializes {} @Retention(RetentionPolicy.RUNTIME) -@interface TestProduces {} \ No newline at end of file +@interface TestProduces {} diff --git a/inject-java/src/test/groovy/io/micronaut/inject/requires/RequiresBeanPropertiesSpec.groovy b/inject-java/src/test/groovy/io/micronaut/inject/requires/RequiresBeanPropertiesSpec.groovy index 9786ebc905f..da05b6d7754 100644 --- a/inject-java/src/test/groovy/io/micronaut/inject/requires/RequiresBeanPropertiesSpec.groovy +++ b/inject-java/src/test/groovy/io/micronaut/inject/requires/RequiresBeanPropertiesSpec.groovy @@ -288,11 +288,11 @@ import jakarta.inject.Singleton; @ConfigurationProperties("outer") class ParentConfig { private String simpleConfigProperty; - + public String getSimpleConfigProperty() { return simpleConfigProperty; } - + public void setSimpleConfigProperty(String simpleConfigProperty) { this.simpleConfigProperty = simpleConfigProperty; } @@ -301,11 +301,11 @@ class ParentConfig { @ConfigurationProperties("inherited") class InheritedConfig extends ParentConfig { private String inheritedProperty; - + public String getInheritedProperty() { return inheritedProperty; } - + public void setInheritedProperty(String inheritedProperty) { this.inheritedProperty = inheritedProperty; } @@ -626,7 +626,7 @@ class AccessorStyleFirstConfig { private String firstAccessorStyleProperty; private String secondAccessorStyleProperty; - + public String readFirstAccessorStyleProperty() { return firstAccessorStyleProperty; } @@ -717,7 +717,7 @@ class PrimitiveConfig public int getIntProperty() { return intProperty; } - + public void setIntProperty(int intProperty) { this.intProperty = intProperty; } @@ -725,11 +725,11 @@ class PrimitiveConfig public int getAnotherIntProperty() { return anotherIntProperty; } - + public void setAnotherIntProperty(int anotherIntProperty) { this.anotherIntProperty = anotherIntProperty; } - + public boolean isBoolProperty() { return boolProperty; } @@ -779,11 +779,11 @@ class NotConfigurationProperties public int getIntProperty() { return intProperty; } - + public void setIntProperty(int intProperty) { this.intProperty = intProperty; } - + public boolean isBoolProperty() { return boolProperty; } @@ -791,11 +791,11 @@ class NotConfigurationProperties public void setBoolProperty(boolean boolProperty) { this.boolProperty = boolProperty; } - + public String getStringProperty() { return stringProperty; } - + public void setStringProperty(String stringProperty) { this.stringProperty = stringProperty; } @@ -854,7 +854,7 @@ class RequiredBean public void setStringProperty(String stringProperty) { this.stringProperty = stringProperty; } - + @ConfigurationProperties("inner") public static class InnerConfig { private String innerProperty = "default value"; @@ -862,7 +862,7 @@ class RequiredBean public String getInnerProperty() { return innerProperty; } - + public void setInnerProperty(String innerProperty) { this.innerProperty = innerProperty; } @@ -916,8 +916,8 @@ interface Configuration extends Toggleable {} class ConfigurationImpl implements Configuration { private boolean enabled = false; - - @Override + + @Override public boolean isEnabled() { return this.enabled; } diff --git a/inject-java/src/test/groovy/io/micronaut/visitors/AllElementsVisitor.java b/inject-java/src/test/groovy/io/micronaut/visitors/AllElementsVisitor.java index ed2b605aa85..c17f3466d2e 100644 --- a/inject-java/src/test/groovy/io/micronaut/visitors/AllElementsVisitor.java +++ b/inject-java/src/test/groovy/io/micronaut/visitors/AllElementsVisitor.java @@ -15,6 +15,7 @@ */ package io.micronaut.visitors; +import io.micronaut.core.annotation.AnnotationMetadataProvider; import io.micronaut.http.annotation.Controller; import io.micronaut.inject.ast.ClassElement; import io.micronaut.inject.ast.Element; @@ -41,18 +42,25 @@ public void start(VisitorContext visitorContext) { @Override public void visitClass(ClassElement element, VisitorContext context) { visit(element); + // Java 9+ doesn't allow resolving elements was the compiler + // is finished being used so this test cannot be made to work beyond Java 8 the way it is currently written + element.getBeanProperties(); // Preload properties for tests otherwise it fails because the compiler is done + element.getAnnotationMetadata(); VISITED_CLASS_ELEMENTS.add(element); } @Override public void visitMethod(MethodElement element, VisitorContext context) { VISITED_METHOD_ELEMENTS.add(element); + element.getReturnType().getBeanProperties().forEach(AnnotationMetadataProvider::getAnnotationMetadata); // Preload + element.getAnnotationMetadata(); visit(element); } @Override public void visitField(FieldElement element, VisitorContext context) { visit(element); + element.getAnnotationMetadata(); } private void visit(Element element) { diff --git a/inject-java/src/test/groovy/io/micronaut/visitors/PropertyElementSpec.groovy b/inject-java/src/test/groovy/io/micronaut/visitors/PropertyElementSpec.groovy index 9d59d286c0d..c63a2abcaf2 100644 --- a/inject-java/src/test/groovy/io/micronaut/visitors/PropertyElementSpec.groovy +++ b/inject-java/src/test/groovy/io/micronaut/visitors/PropertyElementSpec.groovy @@ -18,9 +18,7 @@ package io.micronaut.visitors import io.micronaut.http.annotation.Get import io.micronaut.annotation.processing.test.AbstractTypeElementSpec import io.micronaut.inject.ast.ClassElement -import io.micronaut.inject.ast.PropertyElement import spock.lang.IgnoreIf -import spock.lang.Specification import spock.util.environment.Jvm import javax.annotation.Nullable @@ -46,7 +44,6 @@ record Book( @javax.validation.constraints.NotBlank String title, int pages) {} beanProperties.every { it.readOnly } } - // Java 9+ doesn't allow resolving elements was the compiler // is finished being used so this test cannot be made to work beyond Java 8 the way it is currently written @IgnoreIf({ Jvm.current.isJava9Compatible() }) @@ -59,21 +56,21 @@ import io.micronaut.http.annotation.Get; @Controller("/test") public class TestController { - + private int age; @javax.annotation.Nullable private String name; @javax.annotation.Nullable private String description; - + /** * The age */ @Get("/getMethod") public int getAge() { return age; - } - + } + /** * The age */ @@ -85,7 +82,7 @@ public class TestController { public String getName() { return name; } - + @javax.validation.constraints.NotBlank public void setName(@javax.validation.constraints.NotBlank String n) { name = n; @@ -130,10 +127,10 @@ import jakarta.inject.Inject; @Controller("/test") public class TestController { - + private int age; private T name; - + public int getAge() { return age; } @@ -141,7 +138,7 @@ public class TestController { public T getName() { return name; } - + public void setName(T n) { name = n; } @@ -168,9 +165,9 @@ import jakarta.inject.Inject; @Controller("/test") public class TestController { - + private Response age; - + public Response getAge() { return age; } diff --git a/inject/src/main/java/io/micronaut/context/DefaultApplicationContext.java b/inject/src/main/java/io/micronaut/context/DefaultApplicationContext.java index 3230a4b5658..97e00c7ced7 100644 --- a/inject/src/main/java/io/micronaut/context/DefaultApplicationContext.java +++ b/inject/src/main/java/io/micronaut/context/DefaultApplicationContext.java @@ -422,7 +422,7 @@ private void transformEachPropertyBeanDefinition(BeanResolutionContext resol BeanDefinition candidate, List> transformedCandidates) { boolean isList = candidate.booleanValue(EachProperty.class, "list").orElse(false); - String property = candidate.stringValue(ConfigurationReader.class, "prefix") + String property = candidate.stringValue(ConfigurationReader.class, ConfigurationReader.PREFIX) .map(prefix -> //strip the .* or [*] prefix.substring(0, prefix.length() - (isList ? 3 : 2))) diff --git a/inject/src/main/java/io/micronaut/context/annotation/BeanProperties.java b/inject/src/main/java/io/micronaut/context/annotation/BeanProperties.java new file mode 100644 index 00000000000..d6727c76fdb --- /dev/null +++ b/inject/src/main/java/io/micronaut/context/annotation/BeanProperties.java @@ -0,0 +1,134 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.context.annotation; + +import io.micronaut.core.annotation.Experimental; + +import java.lang.annotation.Annotation; +import java.lang.annotation.Documented; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; + +import static java.lang.annotation.RetentionPolicy.SOURCE; + +/** + * Bean properties configuration annotation. + * The annotation allows to change different options how beans should be detected. + * This configuration is used directly to produce bean properties for: + * introspection, configuration reader and configuration factory. + * + * @author Denis Stepanov + * @since 4.0.0 + */ +@Experimental +@Documented +@Retention(SOURCE) +@Inherited +public @interface BeanProperties { + + String MEMBER_ACCESS_KIND = "accessKind"; + String MEMBER_VISIBILITY = "visibility"; + String MEMBER_INCLUDES = "includes"; + String MEMBER_EXCLUDES = "excludes"; + String MEMBER_ALLOW_WRITE_WITH_ZERO_ARGS = "allowWriteWithZeroArgs"; + String MEMBER_ALLOW_WRITE_WITH_MULTIPLE_ARGS = "allowWriteWithMultipleArgs"; + String MEMBER_EXCLUDED_ANNOTATIONS = "excludedAnnotations"; + + /** + *

The default access type is {@link AccessKind#METHOD} which treats only public JavaBean getters or Java record components as properties. By specifying {@link AccessKind#FIELD}, public or package-protected fields will be used instead.

+ * + *

If both {@link AccessKind#FIELD} and {@link AccessKind#METHOD} are specified then the order as they appear in the annotation will be used to determine whether the field or method will be used in the case where both exist.

+ * + * @return The access type. Defaults to {@link AccessKind#METHOD} + */ + AccessKind[] accessKind() default {AccessKind.METHOD}; + + /** + * Allows specifying the visibility policy to use to control which fields and methods are included. + * + * @return The visibility policies + */ + Visibility[] visibility() default {Visibility.DEFAULT}; + + /** + * The property names to include. Defaults to all properties. + * + * @return The names of the properties + */ + String[] includes() default {}; + + /** + * The property names to excludes. Defaults to excluding none. + * + * @return The names of the properties + */ + String[] excludes() default {}; + + /** + *

Some APIs allow zero argument setters to set boolean flags such as {@code setDebug()}. These by default are + * not processed unless the value of this annotation is set to true.

+ * + *

Note that this attribute works in conjunction with {@link io.micronaut.core.annotation.AccessorsStyle#writePrefixes()} to allow other styles such as + * {@code withDebug()}

+ * + * @return True if zero arg setters should be processed + */ + boolean allowWriteWithZeroArgs() default false; + + /** + * Some APIs allow multiple argument setters to convert the value {@code withDuration(long, TimeUnit)}. + * + * @return True if multiple arg setters should be processed + */ + boolean allowWriteWithMultipleArgs() default false; + + /** + * The annotation types that if present on the property cause the property to be excluded from results. + * + * @return The annotation types + */ + Class[] excludedAnnotations() default {}; + + /** + * The access type for bean properties. + */ + enum AccessKind { + /** + * Allows the use of public or package-protected fields to represent bean properties. + */ + FIELD, + /** + * The default behaviour which is to favour public getters for bean properties. + */ + METHOD + } + + /** + * Visibility policy for bean properties and fields. + */ + enum Visibility { + + /** + * Only public methods and/or fields are included. + */ + PUBLIC, + + /** + * The default behaviour which in addition to public getters and setters will also include package protected fields if an {@link BeanProperties.AccessKind} of {@link BeanProperties.AccessKind#FIELD} is specified. + */ + DEFAULT + } +} diff --git a/inject/src/main/java/io/micronaut/context/annotation/ConfigurationBuilder.java b/inject/src/main/java/io/micronaut/context/annotation/ConfigurationBuilder.java index 1da313f6797..9c8254f30e6 100644 --- a/inject/src/main/java/io/micronaut/context/annotation/ConfigurationBuilder.java +++ b/inject/src/main/java/io/micronaut/context/annotation/ConfigurationBuilder.java @@ -17,13 +17,13 @@ import io.micronaut.core.annotation.AccessorsStyle; -import static java.lang.annotation.RetentionPolicy.RUNTIME; - import java.lang.annotation.Documented; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.Target; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + /** *

An annotation applicable to a field or method of a {@link ConfigurationProperties} instance that allows to * customize the behaviour of properties that are builders themselves.

@@ -76,6 +76,7 @@ * * @return True if zero arg setters should be processed */ + @AliasFor(annotation = BeanProperties.class, member = BeanProperties.MEMBER_ALLOW_WRITE_WITH_ZERO_ARGS) boolean allowZeroArgs() default false; /** @@ -93,10 +94,12 @@ /** * @return The names of the properties to include */ + @AliasFor(annotation = BeanProperties.class, member = BeanProperties.MEMBER_INCLUDES) String[] includes() default {}; /** * @return The names of the properties to exclude */ + @AliasFor(annotation = BeanProperties.class, member = BeanProperties.MEMBER_EXCLUDES) String[] excludes() default {}; } diff --git a/inject/src/main/java/io/micronaut/context/annotation/ConfigurationProperties.java b/inject/src/main/java/io/micronaut/context/annotation/ConfigurationProperties.java index d77f42abf09..40119ab39ee 100644 --- a/inject/src/main/java/io/micronaut/context/annotation/ConfigurationProperties.java +++ b/inject/src/main/java/io/micronaut/context/annotation/ConfigurationProperties.java @@ -51,7 +51,7 @@ * * @return The prefix to use to resolve the properties */ - @AliasFor(annotation = ConfigurationReader.class, member = "value") + @AliasFor(annotation = ConfigurationReader.class, member = ConfigurationReader.PREFIX) String value(); /** @@ -68,12 +68,14 @@ * @return The names of the properties to include */ @AliasFor(annotation = ConfigurationReader.class, member = "includes") + @AliasFor(annotation = BeanProperties.class, member = BeanProperties.MEMBER_INCLUDES) String[] includes() default {}; /** * @return The names of the properties to exclude */ @AliasFor(annotation = ConfigurationReader.class, member = "excludes") + @AliasFor(annotation = BeanProperties.class, member = BeanProperties.MEMBER_EXCLUDES) String[] excludes() default {}; } diff --git a/inject/src/main/java/io/micronaut/context/annotation/ConfigurationReader.java b/inject/src/main/java/io/micronaut/context/annotation/ConfigurationReader.java index a27177f2889..c2c3a62f833 100644 --- a/inject/src/main/java/io/micronaut/context/annotation/ConfigurationReader.java +++ b/inject/src/main/java/io/micronaut/context/annotation/ConfigurationReader.java @@ -15,13 +15,13 @@ */ package io.micronaut.context.annotation; -import static java.lang.annotation.RetentionPolicy.RUNTIME; - import java.lang.annotation.Documented; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.Target; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + /** *

A meta annotation for use with other annotations to indicate that the annotation reads configuration.

* @@ -34,11 +34,22 @@ @Target(ElementType.ANNOTATION_TYPE) public @interface ConfigurationReader { + /** + * The prefix name. + */ + String PREFIX = "prefix"; + + /** + * The base prefix name. + */ + String BASE_PREFIX = "basePrefix"; + /** * The prefix to use when resolving properties. The prefix should be defined in kebab case. Example: my-app.foo. * * @return The configuration entry to read */ + @AliasFor(member = PREFIX) String value() default ""; /** @@ -46,13 +57,22 @@ */ String prefix() default ""; + /** + * The base prefix to prepend to the original prefix. + * @return The base prefix + * @since 4.0.0 + */ + String basePrefix() default ""; + /** * @return The names of the properties to include */ + @AliasFor(annotation = BeanProperties.class, member = BeanProperties.MEMBER_INCLUDES) String[] includes() default {}; /** * @return The names of the properties to exclude */ + @AliasFor(annotation = BeanProperties.class, member = BeanProperties.MEMBER_EXCLUDES) String[] excludes() default {}; } diff --git a/inject/src/main/java/io/micronaut/context/annotation/EachProperty.java b/inject/src/main/java/io/micronaut/context/annotation/EachProperty.java index 22ebe29a1e3..ed667078c3c 100644 --- a/inject/src/main/java/io/micronaut/context/annotation/EachProperty.java +++ b/inject/src/main/java/io/micronaut/context/annotation/EachProperty.java @@ -17,13 +17,13 @@ import jakarta.inject.Singleton; -import static java.lang.annotation.RetentionPolicy.RUNTIME; - import java.lang.annotation.Documented; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.Target; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + /** *

This annotation allows driving the production of {@link Bean} definitions from either configuration or the * presence of another bean definition

@@ -89,7 +89,7 @@ * * @return The property that this bean is driven by */ - @AliasFor(annotation = ConfigurationReader.class, member = "value") + @AliasFor(annotation = ConfigurationReader.class, member = ConfigurationReader.PREFIX) String value(); /** diff --git a/inject/src/main/java/io/micronaut/context/visitor/ConfigurationReaderVisitor.java b/inject/src/main/java/io/micronaut/context/visitor/ConfigurationReaderVisitor.java new file mode 100644 index 00000000000..ca708330ac4 --- /dev/null +++ b/inject/src/main/java/io/micronaut/context/visitor/ConfigurationReaderVisitor.java @@ -0,0 +1,144 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.context.visitor; + +import io.micronaut.context.annotation.ConfigurationReader; +import io.micronaut.context.annotation.EachProperty; +import io.micronaut.context.annotation.Property; +import io.micronaut.core.annotation.AccessorsStyle; +import io.micronaut.core.annotation.AnnotationMetadata; +import io.micronaut.core.annotation.AnnotationUtil; +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.Introspected; +import io.micronaut.core.annotation.NonNull; +import io.micronaut.core.bind.annotation.Bindable; +import io.micronaut.core.naming.NameUtils; +import io.micronaut.inject.validation.RequiresValidation; +import io.micronaut.inject.ast.ClassElement; +import io.micronaut.inject.ast.MethodElement; +import io.micronaut.inject.configuration.ConfigurationMetadata; +import io.micronaut.inject.configuration.ConfigurationMetadataBuilder; +import io.micronaut.inject.visitor.TypeElementVisitor; +import io.micronaut.inject.visitor.VisitorContext; + +/** + * The visitor adds Validated annotation if one of the parameters is a constraint or @Valid. + * + * @author Denis Stepanov + * @since 3.7.0 + */ +@Internal +public class ConfigurationReaderVisitor implements TypeElementVisitor { + + private static final String ANN_CONFIGURATION_ADVICE = "io.micronaut.runtime.context.env.ConfigurationAdvice"; + + private final ConfigurationMetadataBuilder metadataBuilder = ConfigurationMetadataBuilder.INSTANCE; + private String[] readPrefixes; + + @NonNull + @Override + public VisitorKind getVisitorKind() { + return VisitorKind.ISOLATING; + } + + @Override + public void finish(VisitorContext visitorContext) { + reset(); + } + + @Override + public void visitClass(ClassElement classElement, VisitorContext context) { + reset(); + + if (!classElement.hasStereotype(ConfigurationReader.class)) { + return; + } + + ConfigurationMetadata configurationMetadata = metadataBuilder.visitProperties(classElement); + if (configurationMetadata != null) { + classElement.annotate(ConfigurationReader.class, (builder) -> builder.member(ConfigurationReader.PREFIX, configurationMetadata.getName())); + } + + if (classElement.isInterface()) { + classElement.annotate(ANN_CONFIGURATION_ADVICE); + } + if (classElement.hasStereotype(RequiresValidation.class)) { + classElement.annotate(Introspected.class); + } + + AnnotationMetadata annotationMetadata = classElement.getAnnotationMetadata(); + readPrefixes = annotationMetadata.getValue(AccessorsStyle.class, "readPrefixes", String[].class) + .orElse(new String[]{AccessorsStyle.DEFAULT_READ_PREFIX}); + } + + private void reset() { + readPrefixes = null; + } + + @Override + public void visitMethod(MethodElement method, VisitorContext context) { + if (method.isAbstract()) { + visitAbstractMethod(method, context); + } + } + + private void visitAbstractMethod(MethodElement method, VisitorContext context) { + String methodName = method.getName(); + if (!isGetter(methodName)) { + context.fail("Only getter methods are allowed on @ConfigurationProperties interfaces: " + method + ". You can change the accessors using @AccessorsStyle annotation", method.getOwningType()); + return; + } + if (method.hasParameters()) { + context.fail("Only zero argument getter methods are allowed on @ConfigurationProperties interfaces: " + method, method); + return; + } + if ("void".equals(method.getReturnType().getName())) { + context.fail("Getter methods must return a value @ConfigurationProperties interfaces: " + method, method); + return; + } + + final String propertyName = getPropertyNameForGetter(methodName); + + String path = metadataBuilder.visitProperty( + method.getOwningType(), + method.getOwningType(), // interface methods don't inherit the prefix + method.getReturnType(), + propertyName, + method.getDocumentation().orElse(null), + method.getAnnotationMetadata().stringValue(Bindable.class, "defaultValue").orElse(null) + ).getPath(); + + method.annotate(Property.class, builder -> builder.member("name", path)); + + method.annotate(ANN_CONFIGURATION_ADVICE, annBuilder -> { + if (!method.getReturnType().isPrimitive() && method.getReturnType().hasStereotype(AnnotationUtil.SCOPE)) { + annBuilder.member("bean", true); + } + if (method.hasStereotype(EachProperty.class)) { + annBuilder.member("iterable", true); + } + }); + } + + private String getPropertyNameForGetter(String methodName) { + return NameUtils.getPropertyNameForGetter(methodName, readPrefixes); + } + + private boolean isGetter(String methodName) { + return NameUtils.isReaderName(methodName, readPrefixes); + } + +} diff --git a/validation/src/main/java/io/micronaut/validation/executable/ExecutableVisitor.java b/inject/src/main/java/io/micronaut/context/visitor/ExecutableVisitor.java similarity index 91% rename from validation/src/main/java/io/micronaut/validation/executable/ExecutableVisitor.java rename to inject/src/main/java/io/micronaut/context/visitor/ExecutableVisitor.java index aa89f84a7a9..d163b4d4935 100644 --- a/validation/src/main/java/io/micronaut/validation/executable/ExecutableVisitor.java +++ b/inject/src/main/java/io/micronaut/context/visitor/ExecutableVisitor.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.micronaut.validation.executable; +package io.micronaut.context.visitor; import io.micronaut.context.annotation.Executable; import io.micronaut.core.annotation.Internal; @@ -40,9 +40,7 @@ public VisitorKind getVisitorKind() { @Override public void visitMethod(MethodElement element, VisitorContext context) { - ParameterElement[] parameters = element.getParameters(); - - for (ParameterElement parameter : parameters) { + for (ParameterElement parameter : element.getParameters()) { if (parameter.getType().isPrimitive() && parameter.isNullable()) { context.warn("@Nullable on primitive types will allow the method to be executed at runtime with null values, causing an exception", parameter); } diff --git a/validation/src/main/java/io/micronaut/validation/internal/InternalApiTypeElementVisitor.java b/inject/src/main/java/io/micronaut/context/visitor/InternalApiTypeElementVisitor.java similarity index 95% rename from validation/src/main/java/io/micronaut/validation/internal/InternalApiTypeElementVisitor.java rename to inject/src/main/java/io/micronaut/context/visitor/InternalApiTypeElementVisitor.java index 80d9bf10379..e2d5faaf40f 100644 --- a/validation/src/main/java/io/micronaut/validation/internal/InternalApiTypeElementVisitor.java +++ b/inject/src/main/java/io/micronaut/context/visitor/InternalApiTypeElementVisitor.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.micronaut.validation.internal; +package io.micronaut.context.visitor; import io.micronaut.core.annotation.Experimental; import io.micronaut.core.annotation.Internal; @@ -34,6 +34,7 @@ * @author James Kleeh * @since 1.1.0 */ +@Internal public class InternalApiTypeElementVisitor implements TypeElementVisitor { private static final String IO_MICRONAUT = "io.micronaut"; @@ -69,7 +70,7 @@ public void visitField(FieldElement element, VisitorContext context) { } private void warnMember(MemberElement element, VisitorContext context) { - if (!element.getDeclaringType().getName().startsWith(IO_MICRONAUT)) { + if (!element.getOwningType().getName().startsWith(IO_MICRONAUT)) { warn(element, context); } } diff --git a/inject/src/main/java/io/micronaut/context/visitor/ValidationVisitor.java b/inject/src/main/java/io/micronaut/context/visitor/ValidationVisitor.java new file mode 100644 index 00000000000..400af64f822 --- /dev/null +++ b/inject/src/main/java/io/micronaut/context/visitor/ValidationVisitor.java @@ -0,0 +1,110 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.context.visitor; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.NextMajorVersion; +import io.micronaut.core.annotation.NonNull; +import io.micronaut.inject.validation.RequiresValidation; +import io.micronaut.inject.ast.ClassElement; +import io.micronaut.inject.ast.ConstructorElement; +import io.micronaut.inject.ast.Element; +import io.micronaut.inject.ast.FieldElement; +import io.micronaut.inject.ast.MethodElement; +import io.micronaut.inject.visitor.TypeElementVisitor; +import io.micronaut.inject.visitor.VisitorContext; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +/** + * The visitor adds Validated annotation if one of the parameters is a constraint or @Valid. + * + * @author Denis Stepanov + * @since 4.0.0 + */ +@Internal +@NextMajorVersion("This class needs to be moved to the validation module") +public class ValidationVisitor implements TypeElementVisitor { + + private static final String ANN_CONSTRAINT = "javax.validation.Constraint"; + private static final String ANN_VALID = "javax.validation.Valid"; + + private ClassElement classElement; + + @Override + public Set getSupportedAnnotationNames() { + return new HashSet<>(Arrays.asList(ANN_CONSTRAINT, ANN_VALID)); + } + + @Override + public int getOrder() { + return 10; // Should run before ConfigurationReaderVisitor + } + + @NonNull + @Override + public VisitorKind getVisitorKind() { + return VisitorKind.ISOLATING; + } + + @Override + public void visitClass(ClassElement element, VisitorContext context) { + classElement = element; + } + + @Override + public void visitConstructor(ConstructorElement element, VisitorContext context) { + if (classElement == null) { + return; + } + if (requiresValidation(element) || parametersRequireValidation(element)) { + element.annotate(RequiresValidation.class); + classElement.annotate(RequiresValidation.class); + } + } + + @Override + public void visitMethod(MethodElement element, VisitorContext context) { + if (classElement == null) { + return; + } + if (requiresValidation(element) || parametersRequireValidation(element)) { + element.annotate(RequiresValidation.class); + classElement.annotate(RequiresValidation.class); + } + } + + @Override + public void visitField(FieldElement element, VisitorContext context) { + if (classElement == null) { + return; + } + if (requiresValidation(element)) { + element.annotate(RequiresValidation.class); + classElement.annotate(RequiresValidation.class); + } + } + + private boolean parametersRequireValidation(MethodElement element) { + return Arrays.stream(element.getParameters()).anyMatch(this::requiresValidation); + } + + private boolean requiresValidation(Element e) { + return e.hasStereotype(ANN_VALID) || e.hasStereotype(ANN_CONSTRAINT); + } +} diff --git a/inject/src/main/java/io/micronaut/inject/BeanDefinition.java b/inject/src/main/java/io/micronaut/inject/BeanDefinition.java index 3571c63fdbc..a3f7a023979 100644 --- a/inject/src/main/java/io/micronaut/inject/BeanDefinition.java +++ b/inject/src/main/java/io/micronaut/inject/BeanDefinition.java @@ -426,7 +426,7 @@ default boolean isAbstract() { * @return The qualifier or null if this isn't one */ default @Nullable Qualifier getDeclaredQualifier() { - AnnotationMetadata annotationMetadata = getAnnotationMetadata(); + AnnotationMetadata annotationMetadata = getTargetAnnotationMetadata(); if (annotationMetadata instanceof AnnotationMetadataHierarchy) { // Beans created by a factory will have AnnotationMetadataHierarchy = producing element + factory class // All qualifiers are removed from the factory class anyway, so we can skip the hierarchy diff --git a/inject/src/main/java/io/micronaut/inject/annotation/AbstractAnnotationMetadataBuilder.java b/inject/src/main/java/io/micronaut/inject/annotation/AbstractAnnotationMetadataBuilder.java index 2eb6fe18c5a..ca4e2eff90c 100644 --- a/inject/src/main/java/io/micronaut/inject/annotation/AbstractAnnotationMetadataBuilder.java +++ b/inject/src/main/java/io/micronaut/inject/annotation/AbstractAnnotationMetadataBuilder.java @@ -15,8 +15,23 @@ */ package io.micronaut.inject.annotation; -import io.micronaut.context.annotation.*; -import io.micronaut.core.annotation.*; +import io.micronaut.context.annotation.AliasFor; +import io.micronaut.context.annotation.Aliases; +import io.micronaut.context.annotation.DefaultScope; +import io.micronaut.context.annotation.NonBinding; +import io.micronaut.context.annotation.Type; +import io.micronaut.core.annotation.AnnotatedElement; +import io.micronaut.core.annotation.AnnotationClassValue; +import io.micronaut.core.annotation.AnnotationMetadata; +import io.micronaut.core.annotation.AnnotationMetadataDelegate; +import io.micronaut.core.annotation.AnnotationUtil; +import io.micronaut.core.annotation.AnnotationValue; +import io.micronaut.core.annotation.AnnotationValueBuilder; +import io.micronaut.core.annotation.Experimental; +import io.micronaut.core.annotation.InstantiatedMember; +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.NonNull; +import io.micronaut.core.annotation.Nullable; import io.micronaut.core.io.service.SoftServiceLoader; import io.micronaut.core.naming.NameUtils; import io.micronaut.core.util.CollectionUtils; @@ -24,14 +39,23 @@ import io.micronaut.core.value.OptionalValues; import io.micronaut.inject.qualifiers.InterceptorBindingQualifier; import io.micronaut.inject.visitor.VisitorContext; - -import io.micronaut.core.annotation.NonNull; -import io.micronaut.core.annotation.Nullable; import jakarta.inject.Qualifier; import java.lang.annotation.Annotation; import java.lang.annotation.RetentionPolicy; -import java.util.*; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.ListIterator; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; import java.util.function.BiConsumer; import java.util.function.Predicate; import java.util.function.Supplier; @@ -55,10 +79,10 @@ public abstract class AbstractAnnotationMetadataBuilder { private static final Map>> ANNOTATION_MAPPERS = new HashMap<>(10); private static final Map>> ANNOTATION_TRANSFORMERS = new HashMap<>(5); private static final Map> ANNOTATION_REMAPPERS = new HashMap<>(5); - private static final Map MUTATED_ANNOTATION_METADATA = new HashMap<>(100); + private static final Map, CachedAnnotationMetadata> MUTATED_ANNOTATION_METADATA = new HashMap<>(100); private static final Map> NON_BINDING_CACHE = new HashMap<>(50); private static final List DEFAULT_ANNOTATE_EXCLUDES = Arrays.asList(Internal.class.getName(), - Experimental.class.getName()); + Experimental.class.getName()); private static final Map> ANNOTATION_DEFAULTS = new HashMap<>(20); static { @@ -136,9 +160,12 @@ private AnnotationMetadata metadataForError(RuntimeException e) { */ public AnnotationMetadata buildDeclared(T element) { DefaultAnnotationMetadata annotationMetadata = new MutableAnnotationMetadata(); - try { - AnnotationMetadata metadata = buildInternal(null, element, annotationMetadata, true, true, true); + AnnotationMetadata metadata = buildInternalMulti( + Collections.emptyList(), + element, + annotationMetadata, true, true, true + ); if (metadata.isEmpty()) { return AnnotationMetadata.EMPTY_METADATA; } @@ -151,8 +178,8 @@ public AnnotationMetadata buildDeclared(T element) { /** * Build only metadata for declared annotations. * - * @param element The element - * @param annotations The annotations + * @param element The element + * @param annotations The annotations * @param includeTypeAnnotations Whether to include type level annotations in the metadata for the element * @return The {@link AnnotationMetadata} */ @@ -160,12 +187,14 @@ public AnnotationMetadata buildDeclared(T element, List annotations if (CollectionUtils.isEmpty(annotations)) { return AnnotationMetadata.EMPTY_METADATA; } - DefaultAnnotationMetadata annotationMetadata = new MutableAnnotationMetadata(); if (includeTypeAnnotations) { - buildInternal(element, element, annotationMetadata, false, true, true); + buildInternalMulti( + Collections.emptyList(), + element, + annotationMetadata, false, true, true + ); } - try { includeAnnotations(annotationMetadata, element, false, true, annotations, true); if (annotationMetadata.isEmpty()) { @@ -177,208 +206,68 @@ public AnnotationMetadata buildDeclared(T element, List annotations } } - /** - * Build metadata for the given element, including any metadata that is inherited via method or type overrides. - * - * @param element The element - * @return The {@link AnnotationMetadata} - */ - public AnnotationMetadata buildOverridden(T element) { - final AnnotationMetadata existing = MUTATED_ANNOTATION_METADATA.get(new MetadataKey(getDeclaringType(element), element)); - if (existing != null) { - return existing; - } else { - - DefaultAnnotationMetadata annotationMetadata = new MutableAnnotationMetadata(); - - try { - AnnotationMetadata metadata = buildInternal(null, element, annotationMetadata, false, false, true); - if (metadata.isEmpty()) { - return AnnotationMetadata.EMPTY_METADATA; - } - return metadata; - } catch (RuntimeException e) { - return metadataForError(e); - } - } - } - /** * Build the meta data for the given element. If the element is a method the class metadata will be included. * - * @param element The element + * @param owningType The owning type + * @param methodElement The method element + * @param parameterElement The parameter element * @return The {@link AnnotationMetadata} */ - public AnnotationMetadata build(T element) { - String declaringType = getDeclaringType(element); - return build(declaringType, element); + public CachedAnnotationMetadata lookupOrBuildForParameter(T owningType, T methodElement, T parameterElement) { + return lookupOrBuild(true, owningType, methodElement, parameterElement); } /** - * Build the meta data for the given element. If the element is a method the class metadata will be included. + * Build the meta data for the given element. * - * @param declaringType The declaring type - * @param element The element + * @param typeElement The element * @return The {@link AnnotationMetadata} */ - public AnnotationMetadata build(String declaringType, T element) { - final AnnotationMetadata existing = lookupExisting(declaringType, element); - if (existing != null) { - return existing; - } else { - - DefaultAnnotationMetadata annotationMetadata = new MutableAnnotationMetadata(); - - try { - AnnotationMetadata metadata = buildInternal(null, element, annotationMetadata, true, false, true); - if (metadata.isEmpty()) { - return AnnotationMetadata.EMPTY_METADATA; - } - return metadata; - } catch (RuntimeException e) { - return metadataForError(e); - } - } + public CachedAnnotationMetadata lookupOrBuildForType(T typeElement) { + return lookupOrBuild(true, typeElement); } /** - * Whether the element is a field, method, class or constructor. + * Build the metadata for the given method element excluding any class metadata. * - * @param element The element - * @return True if it is - */ - protected abstract boolean isMethodOrClassElement(T element); - - /** - * Obtains the declaring type for an element. - * - * @param element The element - * @return The declaring type - */ - protected abstract @Nullable - String getDeclaringType(@NonNull T element); - - /** - * Build the meta data for the given method element excluding any class metadata. - * - * @param element The element - * @return The {@link AnnotationMetadata} - */ - public AnnotationMetadata buildForMethod(T element) { - String declaringType = getDeclaringType(element); - final AnnotationMetadata existing = lookupExisting(declaringType, element); - if (existing != null) { - return existing; - } else { - DefaultAnnotationMetadata annotationMetadata = new MutableAnnotationMetadata(); - return buildInternal(null, element, annotationMetadata, false, false, true); - } - } - - /** - * Get the annotation metadata for the given element and the given parent. - * This method is used for cases when you need to combine annotation metadata for - * two elements, for example a JavaBean property where the field and the method metadata - * need to be combined. - * - * @param parent The parent element - * @param element The element - * @return The {@link AnnotationMetadata} - */ - public AnnotationMetadata buildForParent(T parent, T element) { - return buildForParents(parent == null ? Collections.emptyList() : Collections.singletonList(parent), element); - } - - /** - * Get the annotation metadata for the given element and the given parents. - * This method is used for cases when you need to combine annotation metadata for - * two elements, for example a JavaBean property where the field and the method metadata - * need to be combined. - * - * @param parents The parent elements - * @param element The element - * @return The {@link AnnotationMetadata} + * @param owningType The owningType + * @param element The element + * @return The {@link CachedAnnotationMetadata} */ - public AnnotationMetadata buildForParents(List parents, T element) { - String declaringType = getDeclaringType(element); - return buildForParents(declaringType, parents, element); + public CachedAnnotationMetadata lookupOrBuildForMethod(T owningType, T element) { + return lookupOrBuild(false, owningType, element); } /** - * Build the meta data for the given parent and method element excluding any class metadata. + * Build the metadata for the given field element excluding any class metadata. * - * @param declaringType The declaring type - * @param parent The parent element - * @param element The element - * @return The {@link AnnotationMetadata} + * @param owningType The owningType + * @param element The element + * @return The {@link CachedAnnotationMetadata} */ - public AnnotationMetadata buildForParent(String declaringType, T parent, T element) { - return buildForParents(declaringType, - parent == null ? Collections.emptyList() : Collections.singletonList(parent), - element); + public CachedAnnotationMetadata lookupOrBuildForField(T owningType, T element) { + return lookupOrBuild(false, owningType, element); } - /** - * Build the meta data for the given parents and method element excluding any class metadata. - * - * @param declaringType The declaring type - * @param parents The parent elements - * @param element The element - * @return The {@link AnnotationMetadata} - */ - public AnnotationMetadata buildForParents(String declaringType, List parents, T element) { - final AnnotationMetadata existing = lookupExisting(declaringType, element); - DefaultAnnotationMetadata annotationMetadata; - if (existing instanceof DefaultAnnotationMetadata) { - // ugly, but will have to do - annotationMetadata = ((DefaultAnnotationMetadata) existing).clone(); - if (parents.isEmpty()) { - // Don't need to do anything with existing - return annotationMetadata; - } - } else if (existing instanceof AnnotationMetadataHierarchy) { - final AnnotationMetadata declaredMetadata = ((AnnotationMetadataHierarchy) existing).getDeclaredMetadata(); - if (declaredMetadata instanceof DefaultAnnotationMetadata) { - annotationMetadata = ((DefaultAnnotationMetadata) declaredMetadata).clone(); - } else { - annotationMetadata = new MutableAnnotationMetadata(); - } - if (parents.isEmpty()) { - // Don't need to do anything with existing - return annotationMetadata; - } - } else { - annotationMetadata = new MutableAnnotationMetadata(); - } - return buildInternalMulti(parents, element, annotationMetadata, false, false, true); + private CachedAnnotationMetadata lookupOrBuild(boolean inheritTypeAnnotations, T... elements) { + return lookupExisting(elements, () -> { + T element = elements[elements.length - 1]; + return buildInternal(inheritTypeAnnotations, false, element); + }); } - /** - * Build the meta data for the given method element excluding any class metadata. - * - * @param parent The parent element - * @param element The element - * @param inheritTypeAnnotations Whether to inherit annotations from type as stereotypes - * @return The {@link AnnotationMetadata} - */ - public AnnotationMetadata buildForParent(T parent, T element, boolean inheritTypeAnnotations) { - String declaringType = getDeclaringType(element); - final AnnotationMetadata existing = lookupExisting(declaringType, element); - DefaultAnnotationMetadata annotationMetadata; - if (existing instanceof DefaultAnnotationMetadata) { - // ugly, but will have to do - annotationMetadata = ((DefaultAnnotationMetadata) existing).clone(); - } else if (existing instanceof AnnotationMetadataHierarchy) { - final AnnotationMetadata declaredMetadata = existing.getDeclaredMetadata(); - if (declaredMetadata instanceof DefaultAnnotationMetadata) { - annotationMetadata = ((DefaultAnnotationMetadata) declaredMetadata).clone(); - } else { - annotationMetadata = new MutableAnnotationMetadata(); - } - } else { - annotationMetadata = new MutableAnnotationMetadata(); + private AnnotationMetadata buildInternal(boolean inheritTypeAnnotations, boolean declaredOnly, T element) { + DefaultAnnotationMetadata annotationMetadata = new MutableAnnotationMetadata(); + try { + return buildInternalMulti( + Collections.emptyList(), + element, + annotationMetadata, inheritTypeAnnotations, declaredOnly, true + ); + } catch (RuntimeException e) { + return metadataForError(e); } - return buildInternal(parent, element, annotationMetadata, inheritTypeAnnotations, false, true); } /** @@ -410,7 +299,7 @@ public AnnotationMetadata buildForParent(T parent, T element, boolean inheritTyp /** * Checks whether any annotations are present on the given element. * - * @param element The element + * @param element The element * @return True if the annotation is present */ protected abstract boolean hasAnnotations(T element); @@ -425,6 +314,7 @@ public AnnotationMetadata buildForParent(T parent, T element, boolean inheritTyp /** * Get the name for the given element. + * * @param element The element * @return The name */ @@ -433,7 +323,7 @@ public AnnotationMetadata buildForParent(T parent, T element, boolean inheritTyp /** * Obtain the annotations for the given type. This method * is also responsible for unwrapping repeatable annotations. - * + *

* For example, {@code @Parent(value = {@Child, @Child})} should result in the two * child annotations being returned from this method instead of the * parent annotation. @@ -464,12 +354,12 @@ public AnnotationMetadata buildForParent(T parent, T element, boolean inheritTyp * @param annotationValues The values to populate */ protected abstract void readAnnotationRawValues( - T originatingElement, - String annotationName, - T member, - String memberName, - Object annotationValue, - Map annotationValues); + T originatingElement, + String annotationName, + T member, + String memberName, + Object annotationValue, + Map annotationValues); /** * Validates an annotation value. @@ -492,7 +382,7 @@ protected void validateAnnotationValue(T originatingElement, final AnnotatedElementValidator elementValidator = getElementValidator(); if (elementValidator != null && !erroneousElements.contains(member)) { boolean shouldValidate = !(annotationName.equals(AliasFor.class.getName())) && - (!(resolvedValue instanceof String) || !resolvedValue.toString().contains("${")); + (!(resolvedValue instanceof String) || !resolvedValue.toString().contains("${")); if (shouldValidate) { shouldValidate = isValidationRequired(member); } @@ -531,6 +421,7 @@ public AnnotationMetadata getAnnotationMetadata() { /** * Return whether the given member requires validation. + * * @param member The member * @return True if it is */ @@ -558,7 +449,7 @@ AnnotatedElementValidator getElementValidator() { * Adds an warning. * * @param originatingElement The originating element - * @param warning The warning + * @param warning The warning */ protected abstract void addWarning(@NonNull T originatingElement, @NonNull String warning); @@ -658,19 +549,19 @@ protected io.micronaut.core.annotation.AnnotationValue readNestedAnnotationValue if (aliasMember.isPresent() && !(aliasAnnotation.isPresent() || aliasAnnotationName.isPresent())) { String aliasedNamed = aliasMember.get().toString(); readAnnotationRawValues(originatingElement, - annotationTypeName, - member, - aliasedNamed, - annotationValue, - resolvedValues); + annotationTypeName, + member, + aliasedNamed, + annotationValue, + resolvedValues); } String memberName = getAnnotationMemberName(member); readAnnotationRawValues(originatingElement, - annotationTypeName, - member, - memberName, - annotationValue, - resolvedValues); + annotationTypeName, + member, + memberName, + annotationValue, + resolvedValues); } av = new io.micronaut.core.annotation.AnnotationValue(annotationTypeName, resolvedValues); } @@ -699,51 +590,51 @@ protected io.micronaut.core.annotation.AnnotationValue readNestedAnnotationValue * @return The annotation values */ protected Map populateAnnotationData( - T originatingElement, - @Nullable T parent, - A annotationMirror, - DefaultAnnotationMetadata metadata, - boolean isDeclared, - RetentionPolicy retentionPolicy, - boolean allowAliases) { + T originatingElement, + @Nullable T parent, + A annotationMirror, + DefaultAnnotationMetadata metadata, + boolean isDeclared, + RetentionPolicy retentionPolicy, + boolean allowAliases) { return populateAnnotationData( - originatingElement, - parent == originatingElement, - annotationMirror, - metadata, - isDeclared, - retentionPolicy, - allowAliases + originatingElement, + parent == originatingElement, + annotationMirror, + metadata, + isDeclared, + retentionPolicy, + allowAliases ); } /** * Populate the annotation data for the given annotation. * - * @param originatingElement The element the annotation data originates from + * @param originatingElement The element the annotation data originates from * @param originatingElementIsSameParent Whether the originating element is considered a parent element - * @param annotationMirror The annotation - * @param metadata the metadata - * @param isDeclared Is the annotation a declared annotation - * @param retentionPolicy The retention policy - * @param allowAliases Whether aliases are allowed + * @param annotationMirror The annotation + * @param metadata the metadata + * @param isDeclared Is the annotation a declared annotation + * @param retentionPolicy The retention policy + * @param allowAliases Whether aliases are allowed * @return The annotation values */ protected Map populateAnnotationData( - T originatingElement, - boolean originatingElementIsSameParent, - A annotationMirror, - DefaultAnnotationMetadata metadata, - boolean isDeclared, - RetentionPolicy retentionPolicy, - boolean allowAliases) { + T originatingElement, + boolean originatingElementIsSameParent, + A annotationMirror, + DefaultAnnotationMetadata metadata, + boolean isDeclared, + RetentionPolicy retentionPolicy, + boolean allowAliases) { String annotationName = getAnnotationTypeName(annotationMirror); if (retentionPolicy == RetentionPolicy.RUNTIME) { processAnnotationDefaults(originatingElement, - metadata, - annotationName, - () -> readAnnotationDefaultValues(annotationMirror)); + metadata, + annotationName, + () -> readAnnotationDefaultValues(annotationMirror)); } List parentAnnotations = new ArrayList<>(); @@ -766,8 +657,8 @@ protected Map populateAnnotationData( if (hasAnnotations(member)) { final DefaultAnnotationMetadata memberMetadata = new DefaultAnnotationMetadata(); final List annotationsForMember = getAnnotationsForType(member) - .stream().filter((a) -> !getAnnotationTypeName(a).equals(annotationName)) - .collect(Collectors.toList()); + .stream().filter((a) -> !getAnnotationTypeName(a).equals(annotationName)) + .collect(Collectors.toList()); includeAnnotations(memberMetadata, member, false, true, annotationsForMember, false); boolean isInstantiatedMember = memberMetadata.hasAnnotation(InstantiatedMember.class); @@ -788,14 +679,14 @@ protected Map populateAnnotationData( if (allowAliases) { handleAnnotationAlias( - originatingElement, - metadata, - isDeclared, - annotationName, - parentAnnotations, - annotationValues, - member, - annotationValue + originatingElement, + metadata, + isDeclared, + annotationName, + parentAnnotations, + annotationValues, + member, + annotationValue ); } } @@ -803,11 +694,11 @@ protected Map populateAnnotationData( if (!nonBindingMembers.isEmpty()) { T annotationType = getTypeForAnnotation(annotationMirror); if (hasAnnotation(annotationType, AnnotationUtil.QUALIFIER) || - hasAnnotation(annotationType, Qualifier.class)) { + hasAnnotation(annotationType, Qualifier.class)) { metadata.addDeclaredStereotype( - Collections.singletonList(getAnnotationTypeName(annotationMirror)), - AnnotationUtil.QUALIFIER, - Collections.singletonMap("nonBinding", nonBindingMembers) + Collections.singletonList(getAnnotationTypeName(annotationMirror)), + AnnotationUtil.QUALIFIER, + Collections.singletonMap("nonBinding", nonBindingMembers) ); } } @@ -830,15 +721,15 @@ protected Map populateAnnotationData( if (repeatableName != null) { if (isDeclared) { metadata.addDeclaredRepeatable( - repeatableName, - av, - retentionPolicy + repeatableName, + av, + retentionPolicy ); } else { metadata.addRepeatable( - repeatableName, - av, - retentionPolicy + repeatableName, + av, + retentionPolicy ); } } else { @@ -846,15 +737,15 @@ protected Map populateAnnotationData( if (isDeclared) { metadata.addDeclaredAnnotation( - mappedAnnotationName, - values, - retentionPolicy + mappedAnnotationName, + values, + retentionPolicy ); } else { metadata.addAnnotation( - mappedAnnotationName, - values, - retentionPolicy + mappedAnnotationName, + values, + retentionPolicy ); } @@ -867,31 +758,31 @@ protected Map populateAnnotationData( T member = getAnnotationMember(annMirror, key); if (member != null) { handleAnnotationAlias( - originatingElement, - metadata, - isDeclared, - mappedAnnotationName, - Collections.emptyList(), - annotationValues, - member, - value + originatingElement, + metadata, + isDeclared, + mappedAnnotationName, + Collections.emptyList(), + annotationValues, + member, + value ); } }); if (finalRetentionPolicy == RetentionPolicy.RUNTIME) { processAnnotationDefaults(originatingElement, - metadata, - mappedAnnotationName, - () -> readAnnotationDefaultValues(mappedAnnotationName, annMirror)); + metadata, + mappedAnnotationName, + () -> readAnnotationDefaultValues(mappedAnnotationName, annMirror)); } final ArrayList parents = new ArrayList<>(); processAnnotationStereotype( - parents, - annMirror, - mappedAnnotationName, - metadata, - isDeclared, - isInheritedAnnotationType(annMirror) || originatingElementIsSameParent); + parents, + annMirror, + mappedAnnotationName, + metadata, + isDeclared, + isInheritedAnnotationType(annMirror) || originatingElementIsSameParent); }); } @@ -918,53 +809,54 @@ private void handleAnnotationAlias(T originatingElement, for (AnnotationValue av : values) { OptionalValues aliasForValues = OptionalValues.of(Object.class, av.getValues()); processAnnotationAlias( - originatingElement, - annotationName, - member, metadata, - isDeclared, - parentAnnotations, - annotationValues, - annotationValue, - aliasForValues + originatingElement, + annotationName, + member, metadata, + isDeclared, + parentAnnotations, + annotationValues, + annotationValue, + aliasForValues ); } } readAnnotationRawValues(originatingElement, - annotationName, - member, - getAnnotationMemberName(member), - annotationValue, - annotationValues); + annotationName, + member, + getAnnotationMemberName(member), + annotationValue, + annotationValues); } else { OptionalValues aliasForValues = getAnnotationValues( - originatingElement, - member, - AliasFor.class + originatingElement, + member, + AliasFor.class ); processAnnotationAlias( - originatingElement, - annotationName, - member, - metadata, - isDeclared, - parentAnnotations, - annotationValues, - annotationValue, - aliasForValues + originatingElement, + annotationName, + member, + metadata, + isDeclared, + parentAnnotations, + annotationValues, + annotationValue, + aliasForValues ); readAnnotationRawValues(originatingElement, - annotationName, - member, - getAnnotationMemberName(member), - annotationValue, - annotationValues); + annotationName, + member, + getAnnotationMemberName(member), + annotationValue, + annotationValues); } } /** * Get the annotation member. + * * @param originatingElement The originatig element - * @param member The member + * @param member The member * @return The annotation member */ protected abstract @Nullable @@ -972,6 +864,7 @@ private void handleAnnotationAlias(T originatingElement, /** * Obtain the annotation mappers for the given annotation name. + * * @param annotationName The annotation name * @return The mappers */ @@ -982,6 +875,7 @@ List> getAnnotationMappers(@NonNull Strin /** * Obtain the transformers mappers for the given annotation name. + * * @param annotationName The annotation name * @return The transformers */ @@ -1009,9 +903,9 @@ private void processAnnotationDefaults(T originatingElement, defaultValues = getAnnotationDefaults(originatingElement, annotationName, elementDefaultValues.get()); if (defaultValues != null) { ANNOTATION_DEFAULTS.put(annotationName, defaultValues.entrySet().stream() - .collect(Collectors.toMap( - (entry) -> entry.getKey().toString(), - Map.Entry::getValue))); + .collect(Collectors.toMap( + (entry) -> entry.getKey().toString(), + Map.Entry::getValue))); } else { defaultValues = Collections.emptyMap(); } @@ -1030,11 +924,11 @@ private Map getAnnotationDefaults(T originatingElement, if (!defaultValues.containsKey(memberName)) { Object annotationValue = entry.getValue(); readAnnotationRawValues(originatingElement, - annotationName, - member, - memberName, - annotationValue, - defaultValues); + annotationName, + member, + memberName, + annotationValue, + defaultValues); } } return defaultValues; @@ -1043,20 +937,21 @@ private Map getAnnotationDefaults(T originatingElement, } } - private AnnotationMetadata lookupExisting(String declaringType, T element) { - return MUTATED_ANNOTATION_METADATA.get(new MetadataKey(declaringType, element)); + @NonNull + private CachedAnnotationMetadata lookupExisting(T[] elements, Supplier annotationMetadataSupplier) { + return MUTATED_ANNOTATION_METADATA.computeIfAbsent(new MetadataKey<>(elements), metadataKey -> new DefaultCachedAnnotationMetadata(annotationMetadataSupplier.get())); } private void processAnnotationAlias( - T originatingElement, - String annotationName, - T member, - DefaultAnnotationMetadata metadata, - boolean isDeclared, - List parentAnnotations, - Map annotationValues, - Object annotationValue, - OptionalValues aliasForValues) { + T originatingElement, + String annotationName, + T member, + DefaultAnnotationMetadata metadata, + boolean isDeclared, + List parentAnnotations, + Map annotationValues, + Object annotationValue, + OptionalValues aliasForValues) { Optional aliasAnnotation = aliasForValues.get("annotation"); Optional aliasAnnotationName = aliasForValues.get("annotationName"); Optional aliasMember = aliasForValues.get("member"); @@ -1082,10 +977,10 @@ private void processAnnotationAlias( if (annotationMirror.isPresent()) { final T annotationTypeMirror = annotationMirror.get(); processAnnotationDefaults(originatingElement, - metadata, - aliasedAnnotationName, - () -> readAnnotationDefaultValues(aliasedAnnotationName, - annotationTypeMirror)); + metadata, + aliasedAnnotationName, + () -> readAnnotationDefaultValues(aliasedAnnotationName, + annotationTypeMirror)); retentionPolicy = getRetentionPolicy(annotationTypeMirror); repeatableName = getRepeatableNameForType(annotationTypeMirror); } @@ -1093,36 +988,36 @@ private void processAnnotationAlias( if (isDeclared) { if (StringUtils.isNotEmpty(repeatableName)) { metadata.addDeclaredRepeatableStereotype( - parentAnnotations, - repeatableName, - AnnotationValue.builder(aliasedAnnotationName, retentionPolicy) - .members(Collections.singletonMap(aliasedMemberName, v)) - .build() + parentAnnotations, + repeatableName, + AnnotationValue.builder(aliasedAnnotationName, retentionPolicy) + .members(Collections.singletonMap(aliasedMemberName, v)) + .build() ); } else { metadata.addDeclaredStereotype( - Collections.emptyList(), - aliasedAnnotationName, - Collections.singletonMap(aliasedMemberName, v), - retentionPolicy + Collections.emptyList(), + aliasedAnnotationName, + Collections.singletonMap(aliasedMemberName, v), + retentionPolicy ); } } else { if (StringUtils.isNotEmpty(repeatableName)) { metadata.addRepeatableStereotype( - parentAnnotations, - repeatableName, - AnnotationValue.builder(aliasedAnnotationName, retentionPolicy) - .members(Collections.singletonMap(aliasedMemberName, v)) - .build() + parentAnnotations, + repeatableName, + AnnotationValue.builder(aliasedAnnotationName, retentionPolicy) + .members(Collections.singletonMap(aliasedMemberName, v)) + .build() ); } else { metadata.addStereotype( - Collections.emptyList(), - aliasedAnnotationName, - Collections.singletonMap(aliasedMemberName, v), - retentionPolicy + Collections.emptyList(), + aliasedAnnotationName, + Collections.singletonMap(aliasedMemberName, v), + retentionPolicy ); } } @@ -1130,19 +1025,19 @@ private void processAnnotationAlias( if (annotationMirror.isPresent()) { final T am = annotationMirror.get(); processAnnotationStereotype( - Collections.singletonList(aliasedAnnotationName), - am, - aliasedAnnotationName, - metadata, - isDeclared, - isInheritedAnnotationType(am) + Collections.singletonList(aliasedAnnotationName), + am, + aliasedAnnotationName, + metadata, + isDeclared, + isInheritedAnnotationType(am) ); } else { processAnnotationStereotype( - Collections.singletonList(aliasedAnnotationName), - remappedAnnotation, - metadata, - isDeclared); + Collections.singletonList(aliasedAnnotationName), + remappedAnnotation, + metadata, + isDeclared); } } } @@ -1166,27 +1061,13 @@ private void processAnnotationAlias( protected abstract @NonNull RetentionPolicy getRetentionPolicy(@NonNull T annotation); - private AnnotationMetadata buildInternal( - T parent, - T element, - DefaultAnnotationMetadata annotationMetadata, - boolean inheritTypeAnnotations, - boolean declaredOnly, - boolean allowAliases) { - return buildInternalMulti( - parent == null ? Collections.emptyList() : Collections.singletonList(parent), - element, - annotationMetadata, inheritTypeAnnotations, declaredOnly, allowAliases - ); - } - private AnnotationMetadata buildInternalMulti( - List parents, - T element, - DefaultAnnotationMetadata annotationMetadata, - boolean inheritTypeAnnotations, - boolean declaredOnly, - boolean allowAliases) { + List parents, + T element, + DefaultAnnotationMetadata annotationMetadata, + boolean inheritTypeAnnotations, + boolean declaredOnly, + boolean allowAliases) { List hierarchy = buildHierarchy(element, inheritTypeAnnotations, declaredOnly); for (T parent : parents) { final List parentHierarchy = buildHierarchy(parent, inheritTypeAnnotations, declaredOnly); @@ -1208,17 +1089,17 @@ private AnnotationMetadata buildInternalMulti( } includeAnnotations( - annotationMetadata, - currentElement, - parents.contains(currentElement), - currentElement == element, - annotationHierarchy, - allowAliases + annotationMetadata, + currentElement, + parents.contains(currentElement), + currentElement == element, + annotationHierarchy, + allowAliases ); } if (!annotationMetadata.hasDeclaredStereotype(AnnotationUtil.SCOPE) && annotationMetadata.hasDeclaredStereotype( - DefaultScope.class)) { + DefaultScope.class)) { Optional value = annotationMetadata.stringValue(DefaultScope.class); value.ifPresent(name -> annotationMetadata.addDeclaredAnnotation(name, Collections.emptyMap())); } @@ -1241,45 +1122,45 @@ private void includeAnnotations(DefaultAnnotationMetadata annotationMetadata, } if (DEPRECATED_ANNOTATION_NAMES.containsKey(annotationName)) { addWarning(element, - "Usages of deprecated annotation " + annotationName + " found. You should use " + DEPRECATED_ANNOTATION_NAMES.get( - annotationName) + " instead."); + "Usages of deprecated annotation " + annotationName + " found. You should use " + DEPRECATED_ANNOTATION_NAMES.get( + annotationName) + " instead."); } final T annotationType = getTypeForAnnotation(annotationMirror); RetentionPolicy retentionPolicy = getRetentionPolicy(annotationType); Map annotationValues = populateAnnotationData( - element, - originatingElementIsSameParent, - annotationMirror, - annotationMetadata, - isDeclared, - retentionPolicy, - allowAliases + element, + originatingElementIsSameParent, + annotationMirror, + annotationMetadata, + isDeclared, + retentionPolicy, + allowAliases ); if (isDeclared) { applyTransformations( + listIterator, + annotationMetadata, + true, + annotationType, + annotationValues, + Collections.emptyList(), + null, + annotationMetadata::addDeclaredRepeatable, + annotationMetadata::addDeclaredAnnotation); + } else { + if (isInheritedAnnotation(annotationMirror) || originatingElementIsSameParent) { + applyTransformations( listIterator, annotationMetadata, - true, + false, annotationType, annotationValues, Collections.emptyList(), null, - annotationMetadata::addDeclaredRepeatable, - annotationMetadata::addDeclaredAnnotation); - } else { - if (isInheritedAnnotation(annotationMirror) || originatingElementIsSameParent) { - applyTransformations( - listIterator, - annotationMetadata, - false, - annotationType, - annotationValues, - Collections.emptyList(), - null, - annotationMetadata::addRepeatable, - annotationMetadata::addAnnotation); + annotationMetadata::addRepeatable, + annotationMetadata::addAnnotation); } else { listIterator.remove(); } @@ -1290,17 +1171,18 @@ private void includeAnnotations(DefaultAnnotationMetadata annotationMetadata, String packageName = NameUtils.getPackageName(annotationTypeName); if (!AnnotationUtil.STEREOTYPE_EXCLUDES.contains(packageName)) { processAnnotationStereotype(element, - originatingElementIsSameParent, - annotationMirror, - annotationMetadata, - isDeclared); + originatingElementIsSameParent, + annotationMirror, + annotationMetadata, + isDeclared); } } } /** * Is the given annotation excluded for the specified element. - * @param element The element + * + * @param element The element * @param annotationName The annotation name * @return True if it is excluded */ @@ -1310,6 +1192,7 @@ protected boolean isExcludedAnnotation(@NonNull T element, @NonNull String annot /** * Test whether the annotation mirror is inherited. + * * @param annotationMirror The mirror * @return True if it is */ @@ -1317,19 +1200,20 @@ protected boolean isExcludedAnnotation(@NonNull T element, @NonNull String annot /** * Test whether the annotation mirror is inherited. + * * @param annotationType The mirror * @return True if it is */ protected abstract boolean isInheritedAnnotationType(@NonNull T annotationType); private void buildStereotypeHierarchy( - List parents, - T element, - DefaultAnnotationMetadata metadata, - boolean isDeclared, - boolean isInherited, - boolean allowAliases, - List excludes) { + List parents, + T element, + DefaultAnnotationMetadata metadata, + boolean isDeclared, + boolean isInherited, + boolean allowAliases, + List excludes) { List annotationMirrors = getAnnotationsForType(element); LinkedList> interceptorBindings = new LinkedList<>(); @@ -1357,26 +1241,26 @@ private void buildStereotypeHierarchy( final T annotationTypeMirror = getTypeForAnnotation(annotationMirror); final RetentionPolicy retentionPolicy = getRetentionPolicy(annotationTypeMirror); Map data = populateAnnotationData( - element, - null, - annotationMirror, - metadata, - isDeclared, - retentionPolicy, - allowAliases + element, + null, + annotationMirror, + metadata, + isDeclared, + retentionPolicy, + allowAliases ); handleAnnotationStereotype( - parents, - metadata, - isDeclared, - isInherited, - interceptorBindings, - lastParent, - listIterator, - annotationTypeMirror, - annotationName, - data + parents, + metadata, + isDeclared, + isInherited, + interceptorBindings, + lastParent, + listIterator, + annotationTypeMirror, + annotationName, + data ); } } @@ -1385,64 +1269,64 @@ private void buildStereotypeHierarchy( // now add meta annotations for (A annotationMirror : topLevel) { processAnnotationStereotype( - parents, - annotationMirror, - metadata, - isDeclared, - isInherited + parents, + annotationMirror, + metadata, + isDeclared, + isInherited ); } } if (lastParent != null) { - AnnotationMetadata modifiedStereotypes = MUTATED_ANNOTATION_METADATA.get(new MetadataKey(lastParent, element)); - if (modifiedStereotypes != null) { + AnnotationMetadata modifiedStereotypes = MUTATED_ANNOTATION_METADATA.get(new MetadataKey<>(element)); + if (modifiedStereotypes != null && !modifiedStereotypes.isEmpty()) { Set annotationNames = modifiedStereotypes.getAnnotationNames(); handleModifiedStereotypes(parents, - metadata, - isDeclared, - isInherited, - excludes, - interceptorBindings, - lastParent, - modifiedStereotypes); + metadata, + isDeclared, + isInherited, + excludes, + interceptorBindings, + lastParent, + modifiedStereotypes); for (String annotationName : annotationNames) { AnnotationValue a = modifiedStereotypes.getAnnotation(annotationName); if (a != null) { String stereotypeName = a.getAnnotationName(); if (!AnnotationUtil.INTERNAL_ANNOTATION_NAMES.contains(stereotypeName) && !excludes.contains( - stereotypeName)) { + stereotypeName)) { final T annotationType = getAnnotationMirror(stereotypeName).orElse(null); if (annotationType != null) { Map values = a.getValues(); handleAnnotationStereotype( - parents, - metadata, - isDeclared, - isInherited, - interceptorBindings, - lastParent, - null, - annotationType, - annotationName, - values + parents, + metadata, + isDeclared, + isInherited, + interceptorBindings, + lastParent, + null, + annotationType, + annotationName, + values ); } else { // a meta annotation not actually on the classpath if (isDeclared) { metadata.addDeclaredStereotype( - parents, - stereotypeName, - a.getValues(), - a.getRetentionPolicy() + parents, + stereotypeName, + a.getValues(), + a.getRetentionPolicy() ); } else { metadata.addStereotype( - parents, - stereotypeName, - a.getValues(), - a.getRetentionPolicy() + parents, + stereotypeName, + a.getValues(), + a.getRetentionPolicy() ); } } @@ -1458,13 +1342,13 @@ private void buildStereotypeHierarchy( if (isDeclared) { metadata.addDeclaredRepeatable( - AnnotationUtil.ANN_INTERCEPTOR_BINDINGS, - interceptorBinding.build() + AnnotationUtil.ANN_INTERCEPTOR_BINDINGS, + interceptorBinding.build() ); } else { metadata.addRepeatable( - AnnotationUtil.ANN_INTERCEPTOR_BINDINGS, - interceptorBinding.build() + AnnotationUtil.ANN_INTERCEPTOR_BINDINGS, + interceptorBinding.build() ); } } @@ -1472,44 +1356,44 @@ private void buildStereotypeHierarchy( } private void handleModifiedStereotypes(List parents, - DefaultAnnotationMetadata metadata, - boolean isDeclared, - boolean isInherited, - List excludes, - LinkedList> interceptorBindings, - String lastParent, - AnnotationMetadata modifiedStereotypes) { + DefaultAnnotationMetadata metadata, + boolean isDeclared, + boolean isInherited, + List excludes, + LinkedList> interceptorBindings, + String lastParent, + AnnotationMetadata modifiedStereotypes) { final Set stereotypeAnnotationNames = modifiedStereotypes.getStereotypeAnnotationNames(); for (String stereotypeName : stereotypeAnnotationNames) { final AnnotationValue a = modifiedStereotypes.getAnnotation(stereotypeName); if (a != null && !AnnotationUtil.INTERNAL_ANNOTATION_NAMES.contains(stereotypeName) && !excludes.contains( - stereotypeName)) { + stereotypeName)) { final T annotationType = getAnnotationMirror(stereotypeName).orElse(null); final List stereotypeParents = modifiedStereotypes.getAnnotationNamesByStereotype( - stereotypeName); + stereotypeName); List resolvedParents = new ArrayList<>(parents); resolvedParents.addAll(stereotypeParents); Map values = a.getValues(); if (annotationType != null) { handleAnnotationStereotype( - resolvedParents, - metadata, - isDeclared, - isInherited, - interceptorBindings, - lastParent, - null, - annotationType, - stereotypeName, - values + resolvedParents, + metadata, + isDeclared, + isInherited, + interceptorBindings, + lastParent, + null, + annotationType, + stereotypeName, + values ); } else { metadata.addStereotype( - resolvedParents, - stereotypeName, - values, - RetentionPolicy.RUNTIME + resolvedParents, + stereotypeName, + values, + RetentionPolicy.RUNTIME ); } } @@ -1517,16 +1401,16 @@ private void handleModifiedStereotypes(List parents, } private void handleAnnotationStereotype( - List parents, - DefaultAnnotationMetadata metadata, - boolean isDeclared, - boolean isInherited, - LinkedList> interceptorBindings, - String lastParent, - @Nullable ListIterator listIterator, - T annotationType, - String annotationName, - Map data) { + List parents, + DefaultAnnotationMetadata metadata, + boolean isDeclared, + boolean isInherited, + LinkedList> interceptorBindings, + String lastParent, + @Nullable ListIterator listIterator, + T annotationType, + String annotationName, + Map data) { addToInterceptorBindingsIfNecessary(interceptorBindings, lastParent, annotationName); final boolean hasInterceptorBinding = !interceptorBindings.isEmpty(); @@ -1563,12 +1447,12 @@ private void handleAnnotationStereotype( if (isDeclared) { applyTransformations(listIterator, metadata, true, annotationType, data, parents, interceptorBindings, - (string, av) -> metadata.addDeclaredRepeatableStereotype(parents, string, av), - (string, values, rp) -> metadata.addDeclaredStereotype(parents, string, values, rp)); + (string, av) -> metadata.addDeclaredRepeatableStereotype(parents, string, av), + (string, values, rp) -> metadata.addDeclaredStereotype(parents, string, values, rp)); } else if (isInherited) { applyTransformations(listIterator, metadata, false, annotationType, data, parents, interceptorBindings, - (string, av) -> metadata.addRepeatableStereotype(parents, string, av), - (string, values, rp) -> metadata.addStereotype(parents, string, values, rp)); + (string, av) -> metadata.addRepeatableStereotype(parents, string, av), + (string, values, rp) -> metadata.addStereotype(parents, string, values, rp)); } else { if (listIterator != null) { listIterator.remove(); @@ -1600,12 +1484,12 @@ private void handleMemberBinding(DefaultAnnotationMetadata metadata, String last values.keySet().removeAll(nonBinding); } final AnnotationValueBuilder builder = - AnnotationValue - .builder(lastParent) - .members(values); + AnnotationValue + .builder(lastParent) + .members(values); data.put( - InterceptorBindingQualifier.META_MEMBER_MEMBERS, - builder.build() + InterceptorBindingQualifier.META_MEMBER_MEMBERS, + builder.build() ); } @@ -1615,6 +1499,7 @@ private void handleMemberBinding(DefaultAnnotationMetadata metadata, String last /** * Gets the annotation members for the given type. + * * @param annotationType The annotation type * @return The members * @since 3.3.0 @@ -1625,7 +1510,7 @@ private void handleMemberBinding(DefaultAnnotationMetadata metadata, String last /** * Returns true if a simple meta annotation is present for the given element and annotation type. * - * @param element The element + * @param element The element * @param simpleName The simple name, ie {@link Class#getSimpleName()} * @return True an annotation with the given simple name exists on the element */ @@ -1638,16 +1523,16 @@ private void addToInterceptorBindingsIfNecessary(LinkedList interceptorBinding = null; if (AnnotationUtil.ANN_AROUND.equals(annotationName) || AnnotationUtil.ANN_INTERCEPTOR_BINDING.equals(annotationName)) { interceptorBinding = AnnotationValue.builder(AnnotationUtil.ANN_INTERCEPTOR_BINDING) - .member(AnnotationMetadata.VALUE_MEMBER, new AnnotationClassValue<>(lastParent)) - .member("kind", "AROUND"); + .member(AnnotationMetadata.VALUE_MEMBER, new AnnotationClassValue<>(lastParent)) + .member("kind", "AROUND"); } else if (AnnotationUtil.ANN_INTRODUCTION.equals(annotationName)) { interceptorBinding = AnnotationValue.builder(AnnotationUtil.ANN_INTERCEPTOR_BINDING) - .member(AnnotationMetadata.VALUE_MEMBER, new AnnotationClassValue<>(lastParent)) - .member("kind", "INTRODUCTION"); + .member(AnnotationMetadata.VALUE_MEMBER, new AnnotationClassValue<>(lastParent)) + .member("kind", "INTRODUCTION"); } else if (AnnotationUtil.ANN_AROUND_CONSTRUCT.equals(annotationName)) { interceptorBinding = AnnotationValue.builder(AnnotationUtil.ANN_INTERCEPTOR_BINDING) - .member(AnnotationMetadata.VALUE_MEMBER, new AnnotationClassValue<>(lastParent)) - .member("kind", "AROUND_CONSTRUCT"); + .member(AnnotationMetadata.VALUE_MEMBER, new AnnotationClassValue<>(lastParent)) + .member("kind", "AROUND_CONSTRUCT"); } if (interceptorBinding != null) { interceptorBindings.add(interceptorBinding); @@ -1656,11 +1541,11 @@ private void addToInterceptorBindingsIfNecessary(LinkedList parents, - AnnotationValue annotationValue, - DefaultAnnotationMetadata metadata, - boolean isDeclared, - List excludes) { + List parents, + AnnotationValue annotationValue, + DefaultAnnotationMetadata metadata, + boolean isDeclared, + List excludes) { List> annotationMirrors = annotationValue.getStereotypes(); LinkedList> interceptorBindings = new LinkedList<>(); @@ -1732,13 +1617,13 @@ private void buildStereotypeHierarchy( if (isDeclared) { metadata.addDeclaredRepeatable( - AnnotationUtil.ANN_INTERCEPTOR_BINDINGS, - interceptorBinding.build() + AnnotationUtil.ANN_INTERCEPTOR_BINDINGS, + interceptorBinding.build() ); } else { metadata.addRepeatable( - AnnotationUtil.ANN_INTERCEPTOR_BINDINGS, - interceptorBinding.build() + AnnotationUtil.ANN_INTERCEPTOR_BINDINGS, + interceptorBinding.build() ); } } @@ -1746,42 +1631,42 @@ private void buildStereotypeHierarchy( } private void processAnnotationStereotype( - T element, - boolean originatingElementIsSameParent, - A annotationMirror, - DefaultAnnotationMetadata annotationMetadata, - boolean isDeclared) { + T element, + boolean originatingElementIsSameParent, + A annotationMirror, + DefaultAnnotationMetadata annotationMetadata, + boolean isDeclared) { T annotationType = getTypeForAnnotation(annotationMirror); String parentAnnotationName = getAnnotationTypeName(annotationMirror); if (!parentAnnotationName.endsWith(".Nullable")) { processAnnotationStereotypes( - annotationMetadata, - isDeclared, - isInheritedAnnotation(annotationMirror) || originatingElementIsSameParent, - annotationType, - parentAnnotationName, - Collections.emptyList() + annotationMetadata, + isDeclared, + isInheritedAnnotation(annotationMirror) || originatingElementIsSameParent, + annotationType, + parentAnnotationName, + Collections.emptyList() ); } } private void processAnnotationStereotypes( - DefaultAnnotationMetadata annotationMetadata, - boolean isDeclared, - boolean isInherited, - T annotationType, - String annotationName, - List excludes) { + DefaultAnnotationMetadata annotationMetadata, + boolean isDeclared, + boolean isInherited, + T annotationType, + String annotationName, + List excludes) { List parentAnnotations = new ArrayList<>(); parentAnnotations.add(annotationName); buildStereotypeHierarchy( - parentAnnotations, - annotationType, - annotationMetadata, - isDeclared, - isInherited, - true, - excludes + parentAnnotations, + annotationType, + annotationMetadata, + isDeclared, + isInherited, + true, + excludes ); } @@ -1792,48 +1677,48 @@ private void processAnnotationStereotypes(DefaultAnnotationMetadata annotationMe List parentAnnotations = new ArrayList<>(parents); parentAnnotations.add(annotation.getAnnotationName()); buildStereotypeHierarchy( - parentAnnotations, - annotation, - annotationMetadata, - isDeclared, - Collections.emptyList() + parentAnnotations, + annotation, + annotationMetadata, + isDeclared, + Collections.emptyList() ); } private void processAnnotationStereotype( - List parents, - A annotationMirror, - DefaultAnnotationMetadata metadata, - boolean isDeclared, - boolean isInherited) { + List parents, + A annotationMirror, + DefaultAnnotationMetadata metadata, + boolean isDeclared, + boolean isInherited) { T typeForAnnotation = getTypeForAnnotation(annotationMirror); String annotationTypeName = getAnnotationTypeName(annotationMirror); processAnnotationStereotype(parents, typeForAnnotation, annotationTypeName, metadata, isDeclared, isInherited); } private void processAnnotationStereotype( - List parents, - T annotationType, - String annotationTypeName, - DefaultAnnotationMetadata metadata, - boolean isDeclared, - boolean isInherited) { + List parents, + T annotationType, + String annotationTypeName, + DefaultAnnotationMetadata metadata, + boolean isDeclared, + boolean isInherited) { List stereoTypeParents = new ArrayList<>(parents); stereoTypeParents.add(annotationTypeName); buildStereotypeHierarchy(stereoTypeParents, - annotationType, - metadata, - isDeclared, - isInherited, - true, - Collections.emptyList()); + annotationType, + metadata, + isDeclared, + isInherited, + true, + Collections.emptyList()); } private void processAnnotationStereotype( - List parents, - AnnotationValue annotationType, - DefaultAnnotationMetadata metadata, - boolean isDeclared) { + List parents, + AnnotationValue annotationType, + DefaultAnnotationMetadata metadata, + boolean isDeclared) { List stereoTypeParents = new ArrayList<>(parents); stereoTypeParents.add(annotationType.getAnnotationName()); buildStereotypeHierarchy(stereoTypeParents, annotationType, metadata, isDeclared, Collections.emptyList()); @@ -1849,28 +1734,28 @@ private void applyTransformations(@Nullable ListIterator hierarchyI BiConsumer addRepeatableAnnotation, TriConsumer, RetentionPolicy> addAnnotation) { applyTransformationsForAnnotationType( - hierarchyIterator, - annotationMetadata, - isDeclared, - annotationType, - data, - parents, - interceptorBindings, - addRepeatableAnnotation, - addAnnotation + hierarchyIterator, + annotationMetadata, + isDeclared, + annotationType, + data, + parents, + interceptorBindings, + addRepeatableAnnotation, + addAnnotation ); } private void applyTransformationsForAnnotationType( - @Nullable ListIterator hierarchyIterator, - DefaultAnnotationMetadata annotationMetadata, - boolean isDeclared, - @NonNull T annotationType, - Map data, - List parents, - @Nullable LinkedList> interceptorBindings, - BiConsumer addRepeatableAnnotation, - TriConsumer, RetentionPolicy> addAnnotation) { + @Nullable ListIterator hierarchyIterator, + DefaultAnnotationMetadata annotationMetadata, + boolean isDeclared, + @NonNull T annotationType, + Map data, + List parents, + @Nullable LinkedList> interceptorBindings, + BiConsumer addRepeatableAnnotation, + TriConsumer, RetentionPolicy> addAnnotation) { String annotationName = getElementName(annotationType); String packageName = NameUtils.getPackageName(annotationName); String repeatableName = getRepeatableNameForType(annotationType); @@ -1884,16 +1769,16 @@ private void applyTransformationsForAnnotationType( if (repeatableName != null) { if (!remapped && !transformed) { io.micronaut.core.annotation.AnnotationValue av = new io.micronaut.core.annotation.AnnotationValue(annotationName, - data); + data); addRepeatableAnnotation.accept(repeatableName, av); } else if (remapped) { VisitorContext visitorContext = createVisitorContext(); io.micronaut.core.annotation.AnnotationValue av = - new io.micronaut.core.annotation.AnnotationValue<>(annotationName, data); + new io.micronaut.core.annotation.AnnotationValue<>(annotationName, data); AnnotationValue repeatableAnn = AnnotationValue.builder(repeatableName) - .values(av) - .build(); + .values(av) + .build(); boolean wasRemapped = false; for (AnnotationRemapper annotationRemapper : annotationRemappers) { List> remappedRepeatable = annotationRemapper.remap(repeatableAnn, visitorContext); @@ -1919,7 +1804,7 @@ private void applyTransformationsForAnnotationType( } else { VisitorContext visitorContext = createVisitorContext(); io.micronaut.core.annotation.AnnotationValue av = - new io.micronaut.core.annotation.AnnotationValue<>(annotationName, data); + new io.micronaut.core.annotation.AnnotationValue<>(annotationName, data); AnnotationValue repeatableAnn = AnnotationValue.builder(repeatableName).values(av).build(); final List> repeatableTransformers = getAnnotationTransformers(repeatableName); if (hierarchyIterator != null) { @@ -1928,7 +1813,7 @@ private void applyTransformationsForAnnotationType( if (CollectionUtils.isNotEmpty(repeatableTransformers)) { for (AnnotationTransformer repeatableTransformer : repeatableTransformers) { final List> transformedRepeatable = repeatableTransformer.transform(repeatableAnn, - visitorContext); + visitorContext); for (AnnotationValue annotationValue : transformedRepeatable) { for (AnnotationTransformer transformer : annotationTransformers) { final List> tav = transformer.transform(av, visitorContext); @@ -1938,9 +1823,9 @@ private void applyTransformationsForAnnotationType( addTransformedStereotypes(annotationMetadata, isDeclared, value, parents); } else { addTransformedStereotypes(annotationMetadata, - isDeclared, - value.getAnnotationName(), - parents); + isDeclared, + value.getAnnotationName(), + parents); } } } @@ -1966,8 +1851,8 @@ private void applyTransformationsForAnnotationType( addAnnotation.accept(annotationName, data, retentionPolicy); } else if (remapped) { io.micronaut.core.annotation.AnnotationValue av = new io.micronaut.core.annotation.AnnotationValue( - annotationName, - data); + annotationName, + data); VisitorContext visitorContext = createVisitorContext(); boolean wasRemapped = false; @@ -1982,11 +1867,11 @@ private void applyTransformationsForAnnotationType( } else { wasRemapped = true; final String transformedAnnotationName = handleTransformedAnnotationValue(parents, - interceptorBindings, - addRepeatableAnnotation, - addAnnotation, - annotationValue, - annotationMetadata); + interceptorBindings, + addRepeatableAnnotation, + addAnnotation, + annotationValue, + annotationMetadata); if (CollectionUtils.isNotEmpty(annotationValue.getStereotypes())) { addTransformedStereotypes(annotationMetadata, isDeclared, annotationValue, parents); } else { @@ -2001,7 +1886,7 @@ private void applyTransformationsForAnnotationType( } } else { io.micronaut.core.annotation.AnnotationValue av = - new io.micronaut.core.annotation.AnnotationValue<>(annotationName, data); + new io.micronaut.core.annotation.AnnotationValue<>(annotationName, data); VisitorContext visitorContext = createVisitorContext(); if (hierarchyIterator != null) { hierarchyIterator.remove(); @@ -2010,11 +1895,11 @@ private void applyTransformationsForAnnotationType( final List> transformedValues = annotationTransformer.transform(av, visitorContext); for (AnnotationValue transformedValue : transformedValues) { final String transformedAnnotationName = handleTransformedAnnotationValue(parents, - interceptorBindings, - addRepeatableAnnotation, - addAnnotation, - transformedValue, - annotationMetadata + interceptorBindings, + addRepeatableAnnotation, + addAnnotation, + transformedValue, + annotationMetadata ); if (CollectionUtils.isNotEmpty(transformedValue.getStereotypes())) { @@ -2036,11 +1921,11 @@ private String handleTransformedAnnotationValue(List parents, DefaultAnnotationMetadata annotationMetadata) { final String transformedAnnotationName = transformedValue.getAnnotationName(); addTransformedInterceptorBindingsIfNecessary( - parents, - interceptorBindings, - transformedValue, - transformedAnnotationName, - annotationMetadata + parents, + interceptorBindings, + transformedValue, + transformedAnnotationName, + annotationMetadata ); final String transformedRepeatableName; @@ -2049,8 +1934,8 @@ private String handleTransformedAnnotationValue(List parents, // wrap with exception handling just in case there is any problems loading the type try { resolvedName = getAnnotationMirror(transformedAnnotationName) - .map(this::getRepeatableNameForType) - .orElse(null); + .map(this::getRepeatableNameForType) + .orElse(null); } catch (Exception e) { // ignore } @@ -2063,8 +1948,8 @@ private String handleTransformedAnnotationValue(List parents, addRepeatableAnnotation.accept(transformedRepeatableName, transformedValue); } else { addAnnotation.accept(transformedAnnotationName, - transformedValue.getValues(), - transformedValue.getRetentionPolicy()); + transformedValue.getValues(), + transformedValue.getRetentionPolicy()); } return transformedAnnotationName; } @@ -2075,10 +1960,10 @@ private void addTransformedInterceptorBindingsIfNecessary(List parents, String transformedAnnotationName, DefaultAnnotationMetadata annotationMetadata) { if (interceptorBindings != null && !parents.isEmpty() && AnnotationUtil.ANN_INTERCEPTOR_BINDING.equals( - transformedAnnotationName)) { + transformedAnnotationName)) { final AnnotationValueBuilder newBuilder = AnnotationValue - .builder(transformedAnnotationName, transformedValue.getRetentionPolicy()) - .members(transformedValue.getValues()); + .builder(transformedAnnotationName, transformedValue.getRetentionPolicy()) + .members(transformedValue.getValues()); if (!transformedValue.contains(AnnotationMetadata.VALUE_MEMBER)) { newBuilder.value(parents.get(parents.size() - 1)); } @@ -2087,9 +1972,9 @@ private void addTransformedInterceptorBindingsIfNecessary(List parents, final String parent = CollectionUtils.last(parents); final HashMap data = new HashMap<>(transformedValue.getValues()); handleMemberBinding( - annotationMetadata, - parent, - data + annotationMetadata, + parent, + data ); newBuilder.members(data); } @@ -2127,8 +2012,8 @@ private List> remapAnnotation(String annotationName) { private boolean isRepeatableCandidate(String transformedAnnotationName) { return !AnnotationUtil.INTERNAL_ANNOTATION_NAMES.contains(transformedAnnotationName) && - !AnnotationUtil.NULLABLE.equals(transformedAnnotationName) && - !AnnotationUtil.NON_NULL.equals(transformedAnnotationName); + !AnnotationUtil.NULLABLE.equals(transformedAnnotationName) && + !AnnotationUtil.NON_NULL.equals(transformedAnnotationName); } private void addTransformedStereotypes(DefaultAnnotationMetadata annotationMetadata, @@ -2139,12 +2024,12 @@ private void addTransformedStereotypes(DefaultAnnotationMetadata annotationMetad String packageName = NameUtils.getPackageName(transformedAnnotationName); if (!AnnotationUtil.STEREOTYPE_EXCLUDES.contains(packageName)) { getAnnotationMirror(transformedAnnotationName).ifPresent(a -> processAnnotationStereotypes( - annotationMetadata, - isDeclared, - false, - a, - transformedAnnotationName, - parents + annotationMetadata, + isDeclared, + false, + a, + transformedAnnotationName, + parents )); } } @@ -2159,10 +2044,10 @@ private void addTransformedStereotypes(DefaultAnnotationMetadata annotationMetad String packageName = NameUtils.getPackageName(transformedAnnotationName); if (!AnnotationUtil.STEREOTYPE_EXCLUDES.contains(packageName)) { processAnnotationStereotypes( - annotationMetadata, - isDeclared, - transformedAnnotation, - parents); + annotationMetadata, + isDeclared, + transformedAnnotation, + parents); } } } @@ -2170,28 +2055,15 @@ private void addTransformedStereotypes(DefaultAnnotationMetadata annotationMetad /** * Used to store metadata mutations at compilation time. Not for public consumption. * - * @param declaringType The declaring type - * @param element The element - * @param metadata The metadata - */ - @Internal - public static void addMutatedMetadata(String declaringType, Object element, AnnotationMetadata metadata) { - if (element != null && metadata != null) { - MUTATED_ANNOTATION_METADATA.put(new MetadataKey(declaringType, element), metadata); - } - } - - /** - * Used to store metadata mutations at compilation time. Not for public consumption. - * - * @param declaringType The declaring type - * @param element The element + * @param owningType The owning type + * @param element The element * @return True if the annotation metadata was mutated */ @Internal - public static boolean isMetadataMutated(String declaringType, Object element) { + public boolean isMetadataMutated(T owningType, T element) { if (element != null) { - return MUTATED_ANNOTATION_METADATA.containsKey(new MetadataKey(declaringType, element)); + CachedAnnotationMetadata entry = MUTATED_ANNOTATION_METADATA.get(new MetadataKey(owningType, element)); + return entry != null && entry.isMutated(); } return false; } @@ -2231,10 +2103,10 @@ public static void copyToRuntime() { @Internal public static boolean isAnnotationMapped(@Nullable String annotationName) { return annotationName != null && - ( - ANNOTATION_MAPPERS.containsKey(annotationName) || - ANNOTATION_TRANSFORMERS.containsKey(annotationName) || - ANNOTATION_TRANSFORMERS.keySet().stream().anyMatch(annotationName::startsWith)); + ( + ANNOTATION_MAPPERS.containsKey(annotationName) || + ANNOTATION_TRANSFORMERS.containsKey(annotationName) || + ANNOTATION_TRANSFORMERS.keySet().stream().anyMatch(annotationName::startsWith)); } /** @@ -2264,46 +2136,46 @@ public static Set getMappedAnnotationPackages() { * @return The mutated metadata */ public AnnotationMetadata annotate( - AnnotationMetadata annotationMetadata, - AnnotationValue annotationValue) { + AnnotationMetadata annotationMetadata, + AnnotationValue annotationValue) { String annotationName = annotationValue.getAnnotationName(); final boolean isReference = annotationMetadata instanceof AnnotationMetadataReference; boolean isReferenceOrEmpty = annotationMetadata == AnnotationMetadata.EMPTY_METADATA || isReference; if (annotationMetadata instanceof DefaultAnnotationMetadata || isReferenceOrEmpty) { final DefaultAnnotationMetadata defaultMetadata = isReferenceOrEmpty - ? new MutableAnnotationMetadata() - : (DefaultAnnotationMetadata) annotationMetadata; + ? new MutableAnnotationMetadata() + : (DefaultAnnotationMetadata) annotationMetadata; T annotationMirror = getAnnotationMirror(annotationName).orElse(null); if (annotationMirror != null) { applyTransformationsForAnnotationType( - null, - defaultMetadata, - true, - annotationMirror, - annotationValue.getValues(), - Collections.emptyList(), - new LinkedList<>(), - defaultMetadata::addDeclaredRepeatable, - defaultMetadata::addDeclaredAnnotation + null, + defaultMetadata, + true, + annotationMirror, + annotationValue.getValues(), + Collections.emptyList(), + new LinkedList<>(), + defaultMetadata::addDeclaredRepeatable, + defaultMetadata::addDeclaredAnnotation ); processAnnotationDefaults( - annotationMirror, - defaultMetadata, - annotationName, - () -> readAnnotationDefaultValues(annotationName, annotationMirror) + annotationMirror, + defaultMetadata, + annotationName, + () -> readAnnotationDefaultValues(annotationName, annotationMirror) ); processAnnotationStereotypes( - defaultMetadata, - true, - isInheritedAnnotationType(annotationMirror), - annotationMirror, - annotationName, - DEFAULT_ANNOTATE_EXCLUDES + defaultMetadata, + true, + isInheritedAnnotationType(annotationMirror), + annotationMirror, + annotationName, + DEFAULT_ANNOTATE_EXCLUDES ); } else { defaultMetadata.addDeclaredAnnotation( - annotationName, - annotationValue.getValues() + annotationName, + annotationValue.getValues() ); } @@ -2317,16 +2189,18 @@ public AnnotationMetadata annotate( AnnotationMetadataHierarchy hierarchy = (AnnotationMetadataHierarchy) annotationMetadata; AnnotationMetadata declaredMetadata = annotate(hierarchy.getDeclaredMetadata(), annotationValue); return hierarchy.createSibling( - declaredMetadata + declaredMetadata ); + } else { + throw new IllegalStateException("Unrecognized annotation metadata: " + annotationMetadata); } - return annotationMetadata; } /** * Removes an annotation from the given annotation metadata. + * * @param annotationMetadata The annotation metadata - * @param annotationType The annotation type + * @param annotationType The annotation type * @return The updated metadata * @since 3.0.0 */ @@ -2355,19 +2229,21 @@ public AnnotationMetadata removeAnnotation(AnnotationMetadata annotationMetadata if (isHierarchy) { return ((AnnotationMetadataHierarchy) annotationMetadata).createSibling( - declaredMetadata + declaredMetadata ); } else { return declaredMetadata; } + } else { + throw new IllegalStateException("Unrecognized annotation metadata: " + annotationMetadata); } - return annotationMetadata; } /** * Removes an annotation from the given annotation metadata. + * * @param annotationMetadata The annotation metadata - * @param annotationType The annotation type + * @param annotationType The annotation type * @return The updated metadata * @since 3.0.0 */ @@ -2396,26 +2272,27 @@ public AnnotationMetadata removeStereotype(AnnotationMetadata annotationMetadata if (isHierarchy) { return ((AnnotationMetadataHierarchy) annotationMetadata).createSibling( - declaredMetadata + declaredMetadata ); } else { return declaredMetadata; } + } else { + throw new IllegalStateException("Unrecognized annotation metadata: " + annotationMetadata); } - return annotationMetadata; } /** * Removes an annotation from the metadata for the given predicate. + * * @param annotationMetadata The annotation metadata - * @param predicate The predicate - * @param The annotation type + * @param predicate The predicate + * @param The annotation type * @return The potentially modified metadata */ - public @NonNull - AnnotationMetadata removeAnnotationIf( - @NonNull AnnotationMetadata annotationMetadata, - @NonNull Predicate> predicate) { + public @NonNull AnnotationMetadata removeAnnotationIf( + @NonNull AnnotationMetadata annotationMetadata, + @NonNull Predicate> predicate) { // we only care if the metadata is an hierarchy or default mutable final boolean isHierarchy = annotationMetadata instanceof AnnotationMetadataHierarchy; AnnotationMetadata declaredMetadata = annotationMetadata; @@ -2431,13 +2308,43 @@ AnnotationMetadata removeAnnotationIf( if (isHierarchy) { return ((AnnotationMetadataHierarchy) annotationMetadata).createSibling( - declaredMetadata + declaredMetadata ); } else { return declaredMetadata; } + } else { + throw new IllegalStateException("Unrecognized annotation metadata: " + annotationMetadata); } - return annotationMetadata; + } + + /** + * The caching entry. + * + * @author Denis Stepanov + * @since 4.0.0 + */ + public interface CachedAnnotationMetadata extends AnnotationMetadataDelegate { + + /** + * @return annotation metadata in the cache or empty + */ + @NonNull + @Override + AnnotationMetadata getAnnotationMetadata(); + + /** + * @return Is mutated? + */ + boolean isMutated(); + + /** + * Modify the annotation metadata in the cache. + * + * @param annotationMetadata new value + */ + void update(@NonNull AnnotationMetadata annotationMetadata); + } /** @@ -2446,12 +2353,12 @@ AnnotationMetadata removeAnnotationIf( * @param the element type */ private static class MetadataKey { - final String declaringName; - final T element; + final T[] elements; + final int hashCode; - MetadataKey(String declaringName, T element) { - this.declaringName = declaringName; - this.element = element; + MetadataKey(T... elements) { + this.elements = elements; + this.hashCode = Objects.hash(elements); } @Override @@ -2462,18 +2369,53 @@ public boolean equals(Object o) { if (o == null || getClass() != o.getClass()) { return false; } - MetadataKey that = (MetadataKey) o; - return declaringName.equals(that.declaringName) && - element.equals(that.element); + MetadataKey that = (MetadataKey) o; + return Arrays.equals(elements, that.elements); } @Override public int hashCode() { - return Objects.hash(declaringName, element); + return hashCode; } } - private static interface TriConsumer { + private interface TriConsumer { void accept(T t, U u, V v); } + + private static final class DefaultCachedAnnotationMetadata implements CachedAnnotationMetadata { + @Nullable + private AnnotationMetadata annotationMetadata; + private boolean isMutated; + + public DefaultCachedAnnotationMetadata(AnnotationMetadata annotationMetadata) { + if (annotationMetadata instanceof AbstractAnnotationMetadataBuilder.CachedAnnotationMetadata) { + throw new IllegalStateException(); + } + this.annotationMetadata = annotationMetadata; + } + + @Override + public boolean isMutated() { + return isMutated; + } + + @Override + public AnnotationMetadata getAnnotationMetadata() { + if (annotationMetadata == null || annotationMetadata.isEmpty()) { + return AnnotationMetadata.EMPTY_METADATA; + } + return annotationMetadata; + } + + @Override + public void update(AnnotationMetadata annotationMetadata) { + if (annotationMetadata instanceof AbstractAnnotationMetadataBuilder.CachedAnnotationMetadata) { + throw new IllegalStateException(); + } + this.annotationMetadata = annotationMetadata; + isMutated = true; + } + } + } diff --git a/inject/src/main/java/io/micronaut/inject/annotation/AbstractElementAnnotationMetadataFactory.java b/inject/src/main/java/io/micronaut/inject/annotation/AbstractElementAnnotationMetadataFactory.java new file mode 100644 index 00000000000..17a5281026a --- /dev/null +++ b/inject/src/main/java/io/micronaut/inject/annotation/AbstractElementAnnotationMetadataFactory.java @@ -0,0 +1,378 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.inject.annotation; + +import io.micronaut.core.annotation.AnnotationMetadata; +import io.micronaut.core.annotation.AnnotationValue; +import io.micronaut.core.annotation.AnnotationValueBuilder; +import io.micronaut.core.annotation.NonNull; +import io.micronaut.core.annotation.Nullable; +import io.micronaut.core.util.ArgumentUtils; +import io.micronaut.inject.ast.ClassElement; +import io.micronaut.inject.ast.ConstructorElement; +import io.micronaut.inject.ast.Element; +import io.micronaut.inject.ast.ElementAnnotationMetadata; +import io.micronaut.inject.ast.ElementAnnotationMetadataFactory; +import io.micronaut.inject.ast.EnumConstantElement; +import io.micronaut.inject.ast.FieldElement; +import io.micronaut.inject.ast.MethodElement; +import io.micronaut.inject.ast.ElementMutableAnnotationMetadata; +import io.micronaut.inject.ast.PackageElement; +import io.micronaut.inject.ast.ParameterElement; +import io.micronaut.inject.ast.PropertyElement; + +import java.lang.annotation.Annotation; +import java.util.function.Consumer; +import java.util.function.Predicate; + +/** + * Abstract element annotation metadata factory. + * + * @param The element type + * @param The annotation type + * @author Denis Stepanov + * @since 4.0.0 + */ +public abstract class AbstractElementAnnotationMetadataFactory implements ElementAnnotationMetadataFactory { + + protected final boolean isReadOnly; + protected final AbstractAnnotationMetadataBuilder metadataBuilder; + + protected AbstractElementAnnotationMetadataFactory(boolean isReadOnly, AbstractAnnotationMetadataBuilder metadataBuilder) { + this.isReadOnly = isReadOnly; + this.metadataBuilder = metadataBuilder; + } + + @Override + public ElementAnnotationMetadata build(Element element) { + return build(element, null); + } + + @Override + public ElementAnnotationMetadata build(Element element, AnnotationMetadata defaultAnnotationMetadata) { + if (element instanceof ClassElement) { + ClassElement classElement = (ClassElement) element; + return buildForClass(defaultAnnotationMetadata, classElement); + } + if (element instanceof ConstructorElement) { + ConstructorElement constructorElement = (ConstructorElement) element; + return buildForConstructor(defaultAnnotationMetadata, constructorElement); + } + if (element instanceof MethodElement) { + MethodElement methodElement = (MethodElement) element; + return buildForMethod(defaultAnnotationMetadata, methodElement); + } + if (element instanceof FieldElement) { + FieldElement fieldElement = (FieldElement) element; + return buildForField(defaultAnnotationMetadata, fieldElement); + } + if (element instanceof ParameterElement) { + ParameterElement parameterElement = (ParameterElement) element; + return buildForParameter(defaultAnnotationMetadata, parameterElement); + } + if (element instanceof PackageElement) { + PackageElement packageElement = (PackageElement) element; + return buildForPackage(defaultAnnotationMetadata, packageElement); + } + if (element instanceof PropertyElement) { + PropertyElement propertyElement = (PropertyElement) element; + return buildForProperty(defaultAnnotationMetadata, propertyElement); + } + if (element instanceof EnumConstantElement) { + EnumConstantElement enumConstantElement = (EnumConstantElement) element; + return buildForEnumConstantElement(defaultAnnotationMetadata, enumConstantElement); + } + throw new IllegalStateException("Unknown element: " + element.getClass() + " with native type: " + element.getNativeType()); + } + + @NonNull + private AbstractElementAnnotationMetadata buildForProperty(@Nullable AnnotationMetadata defaultAnnotationMetadata, @NonNull PropertyElement propertyElement) { + return new AbstractElementAnnotationMetadata(defaultAnnotationMetadata) { + + @Override + protected AbstractAnnotationMetadataBuilder.CachedAnnotationMetadata lookup() { + throw new IllegalStateException("Properties should combine annotations for it's elements!"); + } + + @Override + public String toString() { + return propertyElement.toString(); + } + }; + } + + @NonNull + private AbstractElementAnnotationMetadata buildForEnumConstantElement(@Nullable AnnotationMetadata defaultAnnotationMetadata, + @NonNull EnumConstantElement enumConstantElement) { + return new AbstractElementAnnotationMetadata(defaultAnnotationMetadata) { + + @Override + protected AbstractAnnotationMetadataBuilder.CachedAnnotationMetadata lookup() { + return metadataBuilder.lookupOrBuildForField( + (K) enumConstantElement.getOwningType().getNativeType(), + (K) enumConstantElement.getNativeType() + ); + } + + @Override + public String toString() { + return enumConstantElement.toString(); + } + }; + } + + @NonNull + private AbstractElementAnnotationMetadata buildForPackage(@Nullable AnnotationMetadata defaultAnnotationMetadata, @NonNull PackageElement packageElement) { + return new AbstractElementAnnotationMetadata(defaultAnnotationMetadata) { + + @Override + protected AbstractAnnotationMetadataBuilder.CachedAnnotationMetadata lookup() { + return metadataBuilder.lookupOrBuildForType((K) packageElement.getNativeType()); + } + + @Override + public String toString() { + return packageElement.toString(); + } + }; + } + + @NonNull + private AbstractElementAnnotationMetadata buildForParameter(@Nullable AnnotationMetadata defaultAnnotationMetadata, @NonNull ParameterElement parameterElement) { + return new AbstractElementAnnotationMetadata(defaultAnnotationMetadata) { + + @Override + protected AbstractAnnotationMetadataBuilder.CachedAnnotationMetadata lookup() { + return metadataBuilder.lookupOrBuildForParameter( + (K) parameterElement.getMethodElement().getOwningType().getNativeType(), + (K) parameterElement.getMethodElement().getNativeType(), + (K) parameterElement.getNativeType() + ); + } + + @Override + public String toString() { + return parameterElement.toString(); + } + + }; + } + + @NonNull + private AbstractElementAnnotationMetadata buildForField(@Nullable AnnotationMetadata defaultAnnotationMetadata, @NonNull FieldElement fieldElement) { + return new AbstractElementAnnotationMetadata(defaultAnnotationMetadata) { + + @Override + protected AbstractAnnotationMetadataBuilder.CachedAnnotationMetadata lookup() { + return metadataBuilder.lookupOrBuildForField( + (K) fieldElement.getOwningType().getNativeType(), + (K) fieldElement.getNativeType() + ); + } + + @Override + public String toString() { + return fieldElement.toString(); + } + }; + } + + @NonNull + private AbstractElementAnnotationMetadata buildForMethod(@Nullable AnnotationMetadata defaultAnnotationMetadata, + @NonNull MethodElement methodElement) { + return new AbstractElementAnnotationMetadata(isReadOnly, methodElement.getOwningType(), defaultAnnotationMetadata) { + + @Override + protected AbstractAnnotationMetadataBuilder.CachedAnnotationMetadata lookup() { + return metadataBuilder.lookupOrBuildForMethod((K) methodElement.getOwningType().getNativeType(), (K) methodElement.getNativeType()); + } + + @Override + public String toString() { + return methodElement.toString(); + } + }; + } + + @NonNull + private AbstractElementAnnotationMetadata buildForConstructor(@Nullable AnnotationMetadata defaultAnnotationMetadata, + @NonNull ConstructorElement constructorElement) { + return new AbstractElementAnnotationMetadata(defaultAnnotationMetadata) { + + @Override + protected AbstractAnnotationMetadataBuilder.CachedAnnotationMetadata lookup() { + return metadataBuilder.lookupOrBuildForMethod( + (K) constructorElement.getOwningType().getNativeType(), + (K) constructorElement.getNativeType() + ); + } + + @Override + public String toString() { + return constructorElement.toString(); + } + }; + } + + @NonNull + private AbstractElementAnnotationMetadata buildForClass(@Nullable AnnotationMetadata defaultAnnotationMetadata, @NonNull ClassElement classElement) { + return new AbstractElementAnnotationMetadata(defaultAnnotationMetadata) { + + @Override + protected AbstractAnnotationMetadataBuilder.CachedAnnotationMetadata lookup() { + return metadataBuilder.lookupOrBuildForType((K) classElement.getNativeType()); + } + + @Override + public String toString() { + return classElement.toString(); + } + }; + } + + /** + * Abstract implementation of {@link ElementAnnotationMetadata}. + * + * @author Denis Stepanov + * @since 4.0.0 + */ + protected abstract class AbstractElementAnnotationMetadata implements ElementAnnotationMetadata { + + protected AnnotationMetadata preloadedAnnotationMetadata; + private final boolean readOnly; + private AbstractAnnotationMetadataBuilder.CachedAnnotationMetadata cachedAnnotationMetadata; + private final ClassElement classElement; + + protected AbstractElementAnnotationMetadata(@Nullable AnnotationMetadata annotationMetadata) { + this(AbstractElementAnnotationMetadataFactory.this.isReadOnly, annotationMetadata); + } + + protected AbstractElementAnnotationMetadata(boolean readOnly, @Nullable AnnotationMetadata annotationMetadata) { + this(readOnly, null, annotationMetadata); + } + + protected AbstractElementAnnotationMetadata(boolean readOnly, + ClassElement classElement, + @Nullable AnnotationMetadata annotationMetadata) { + this.readOnly = readOnly; + this.classElement = classElement; + this.preloadedAnnotationMetadata = annotationMetadata; + if (preloadedAnnotationMetadata instanceof AbstractAnnotationMetadataBuilder.CachedAnnotationMetadata) { + throw new IllegalStateException(); + } + if (preloadedAnnotationMetadata instanceof ElementAnnotationMetadata) { + throw new IllegalStateException(); + } + } + + protected abstract AbstractAnnotationMetadataBuilder.CachedAnnotationMetadata lookup(); + + private AbstractAnnotationMetadataBuilder.CachedAnnotationMetadata getCacheEntry() { + if (cachedAnnotationMetadata == null) { + cachedAnnotationMetadata = lookup(); + } + return cachedAnnotationMetadata; + } + + @Override + public AnnotationMetadata getAnnotationMetadata() { + if (preloadedAnnotationMetadata != null) { + if (classElement != null) { + if (preloadedAnnotationMetadata instanceof AnnotationMetadataHierarchy) { + return preloadedAnnotationMetadata; + } + return new AnnotationMetadataHierarchy(classElement, preloadedAnnotationMetadata); + } + return preloadedAnnotationMetadata; + } + if (classElement != null) { + return new AnnotationMetadataHierarchy(classElement, getCacheEntry()); + } + return getCacheEntry(); + } + + private AnnotationMetadata getAnnotationMetadataToModify() { + if (preloadedAnnotationMetadata != null) { + if (preloadedAnnotationMetadata instanceof AnnotationMetadataHierarchy) { + return preloadedAnnotationMetadata.getDeclaredMetadata().copyAnnotationMetadata(); + } + return preloadedAnnotationMetadata; + } + return getCacheEntry().copyAnnotationMetadata(); + } + + private AnnotationMetadata replaceAnnotationsInternal(AnnotationMetadata annotationMetadata) { + if (annotationMetadata instanceof AbstractAnnotationMetadataBuilder.CachedAnnotationMetadata) { + throw new IllegalStateException(); + } + if (annotationMetadata instanceof ElementMutableAnnotationMetadata) { + throw new IllegalStateException(); + } + if (annotationMetadata.isEmpty()) { + annotationMetadata = AnnotationMetadata.EMPTY_METADATA; + } + if (!readOnly) { + if (annotationMetadata instanceof AnnotationMetadataHierarchy) { + throw new IllegalStateException("Not supported to cache AnnotationMetadataHierarchy"); + } + getCacheEntry().update(annotationMetadata); + preloadedAnnotationMetadata = null; + } else { + preloadedAnnotationMetadata = annotationMetadata; + } + return getAnnotationMetadata(); + } + + @Override + public AnnotationMetadata annotate(@NonNull String annotationType, @NonNull Consumer> consumer) { + ArgumentUtils.requireNonNull("annotationType", annotationType); + AnnotationValueBuilder builder = AnnotationValue.builder(annotationType); + //noinspection ConstantConditions + if (consumer != null) { + consumer.accept(builder); + AnnotationValue av = builder.build(); + return replaceAnnotationsInternal(metadataBuilder.annotate(getAnnotationMetadataToModify(), av)); + } + return getAnnotationMetadata(); + } + + @Override + public AnnotationMetadata annotate(AnnotationValue annotationValue) { + ArgumentUtils.requireNonNull("annotationValue", annotationValue); + return replaceAnnotationsInternal(metadataBuilder.annotate(getAnnotationMetadataToModify(), annotationValue)); + } + + @Override + public AnnotationMetadata removeAnnotation(@NonNull String annotationType) { + ArgumentUtils.requireNonNull("annotationType", annotationType); + return replaceAnnotationsInternal(metadataBuilder.removeAnnotation(getAnnotationMetadataToModify(), annotationType)); + } + + @Override + public AnnotationMetadata removeAnnotationIf(@NonNull Predicate> predicate) { + ArgumentUtils.requireNonNull("predicate", predicate); + return replaceAnnotationsInternal(metadataBuilder.removeAnnotationIf(getAnnotationMetadataToModify(), predicate)); + } + + @Override + public AnnotationMetadata removeStereotype(@NonNull String annotationType) { + ArgumentUtils.requireNonNull("annotationType", annotationType); + return replaceAnnotationsInternal(metadataBuilder.removeStereotype(getAnnotationMetadataToModify(), annotationType)); + } + + } + +} diff --git a/inject/src/main/java/io/micronaut/inject/annotation/AbstractEnvironmentAnnotationMetadata.java b/inject/src/main/java/io/micronaut/inject/annotation/AbstractEnvironmentAnnotationMetadata.java index b8a5609bd31..112fee14961 100644 --- a/inject/src/main/java/io/micronaut/inject/annotation/AbstractEnvironmentAnnotationMetadata.java +++ b/inject/src/main/java/io/micronaut/inject/annotation/AbstractEnvironmentAnnotationMetadata.java @@ -177,19 +177,19 @@ public Optional classValue(@NonNull Class annotatio } @Override - public Optional enumValue(@NonNull String annotation, @NonNull String member, Class enumType) { + public > Optional enumValue(@NonNull String annotation, @NonNull String member, Class enumType) { Function valueMapper = getEnvironmentValueMapper(); return environmentAnnotationMetadata.enumValue(annotation, member, enumType, valueMapper); } @Override - public Optional enumValue(@NonNull Class annotation, @NonNull String member, Class enumType) { + public > Optional enumValue(@NonNull Class annotation, @NonNull String member, Class enumType) { Function valueMapper = getEnvironmentValueMapper(); return environmentAnnotationMetadata.enumValue(annotation, member, enumType, valueMapper); } @Override - public E[] enumValues(@NonNull String annotation, @NonNull String member, Class enumType) { + public > E[] enumValues(@NonNull String annotation, @NonNull String member, Class enumType) { Function valueMapper = getEnvironmentValueMapper(); return environmentAnnotationMetadata.enumValues(annotation, member, enumType, valueMapper); } @@ -492,6 +492,16 @@ public boolean hasDeclaredStereotype(@Nullable String annotation) { return environmentAnnotationMetadata.getDefaultValue(annotation, member, requiredType); } + @Override + public AnnotationMetadata copyAnnotationMetadata() { + return environmentAnnotationMetadata.copyAnnotationMetadata(); + } + + @Override + public AnnotationMetadata getTargetAnnotationMetadata() { + return environmentAnnotationMetadata.getTargetAnnotationMetadata(); + } + /** * Resolves the {@link Environment} for this metadata. * diff --git a/inject/src/main/java/io/micronaut/inject/annotation/AnnotationMetadataHierarchy.java b/inject/src/main/java/io/micronaut/inject/annotation/AnnotationMetadataHierarchy.java index de9c574c309..f98163bba3e 100644 --- a/inject/src/main/java/io/micronaut/inject/annotation/AnnotationMetadataHierarchy.java +++ b/inject/src/main/java/io/micronaut/inject/annotation/AnnotationMetadataHierarchy.java @@ -18,14 +18,29 @@ import io.micronaut.core.annotation.AnnotationMetadata; import io.micronaut.core.annotation.AnnotationUtil; import io.micronaut.core.annotation.AnnotationValue; +import io.micronaut.core.annotation.Internal; import io.micronaut.core.annotation.NonNull; import io.micronaut.core.annotation.Nullable; import io.micronaut.core.type.Argument; import io.micronaut.core.util.ArrayUtils; import io.micronaut.core.value.OptionalValues; + import java.lang.annotation.Annotation; import java.lang.reflect.Array; -import java.util.*; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedHashMap; +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.function.BiFunction; import java.util.function.Function; import java.util.stream.Stream; @@ -47,6 +62,7 @@ public final class AnnotationMetadataHierarchy implements AnnotationMetadata, En public static final AnnotationMetadata[] EMPTY_HIERARCHY = {AnnotationMetadata.EMPTY_METADATA, AnnotationMetadata.EMPTY_METADATA}; private final AnnotationMetadata[] hierarchy; + private final boolean delegateDeclaredToAllElements; /** * Default constructor. @@ -54,17 +70,20 @@ public final class AnnotationMetadataHierarchy implements AnnotationMetadata, En * @param hierarchy The annotation hierarchy */ public AnnotationMetadataHierarchy(AnnotationMetadata... hierarchy) { + this(false, hierarchy); + } + + /** + * Default constructor. + * + * @param hierarchy The annotation hierarchy + * @param delegateDeclaredToAllElements The delegate declared to all elements + */ + @Internal + public AnnotationMetadataHierarchy(boolean delegateDeclaredToAllElements, AnnotationMetadata... hierarchy) { + this.delegateDeclaredToAllElements = delegateDeclaredToAllElements; if (ArrayUtils.isNotEmpty(hierarchy)) { - // place the first in the hierarchy first - final int len = hierarchy.length; - if (len > 1) { - for (int i = 0; i < len / 2; i++) { - AnnotationMetadata temp = hierarchy[i]; - final int pos = len - i - 1; - hierarchy[i] = hierarchy[pos]; - hierarchy[pos] = temp; - } - } + ArrayUtils.reverse(hierarchy); this.hierarchy = hierarchy; } else { this.hierarchy = EMPTY_HIERARCHY; @@ -73,6 +92,7 @@ public AnnotationMetadataHierarchy(AnnotationMetadata... hierarchy) { /** * Copy constructor. + * * @param existing Existing * @param newChild new child */ @@ -80,6 +100,7 @@ private AnnotationMetadataHierarchy(AnnotationMetadata[] existing, AnnotationMet hierarchy = new AnnotationMetadata[existing.length]; System.arraycopy(existing, 0, hierarchy, 0, existing.length); hierarchy[0] = newChild; + delegateDeclaredToAllElements = false; } @Override @@ -121,6 +142,7 @@ public AnnotationMetadata getRootMetadata() { /** * Create a new hierarchy instance from this metadata using this metadata's parents. + * * @param child The child annotation metadata * @return A new sibling */ @@ -162,10 +184,10 @@ public T[] synthesizeAnnotationsByType(Class annotatio return (T[]) AnnotationUtil.ZERO_ANNOTATIONS; } return Stream.of(hierarchy) - .flatMap(am -> am.getAnnotationValuesByType(annotationClass).stream()) - .distinct() - .map(entries -> AnnotationMetadataSupport.buildAnnotation(annotationClass, entries)) - .toArray(value -> (T[]) Array.newInstance(annotationClass, value)); + .flatMap(am -> am.getAnnotationValuesByType(annotationClass).stream()) + .distinct() + .map(entries -> AnnotationMetadataSupport.buildAnnotation(annotationClass, entries)) + .toArray(value -> (T[]) Array.newInstance(annotationClass, value)); } @SuppressWarnings("unchecked") @@ -174,10 +196,10 @@ public T[] synthesizeDeclaredAnnotationsByType(Class a return (T[]) AnnotationUtil.ZERO_ANNOTATIONS; } return Stream.of(hierarchy) - .flatMap(am -> am.getAnnotationValuesByType(annotationClass).stream()) - .distinct() - .map(entries -> AnnotationMetadataSupport.buildAnnotation(annotationClass, entries)) - .toArray(value -> (T[]) Array.newInstance(annotationClass, value)); + .flatMap(am -> am.getAnnotationValuesByType(annotationClass).stream()) + .distinct() + .map(entries -> AnnotationMetadataSupport.buildAnnotation(annotationClass, entries)) + .toArray(value -> (T[]) Array.newInstance(annotationClass, value)); } @Nullable @@ -201,36 +223,52 @@ public T synthesize(@NonNull Class annotationClass) { @Nullable @Override public T synthesizeDeclared(@NonNull Class annotationClass) { + if (delegateDeclaredToAllElements) { + return merge().synthesizeDeclared(annotationClass); + } return hierarchy[0].synthesize(annotationClass); } @NonNull @Override public Optional> findAnnotation(@NonNull String annotation) { - AnnotationValue ann = null; - for (AnnotationMetadata annotationMetadata : hierarchy) { - final AnnotationValue av = annotationMetadata.getAnnotation(annotation); - if (av != null) { - if (ann == null) { - ann = av; - } else { - final Map values = av.getValues(); - final Map existing = ann.getValues(); - Map newValues = new LinkedHashMap<>(values.size() + existing.size()); - newValues.putAll(existing); - for (Map.Entry entry : values.entrySet()) { - newValues.putIfAbsent(entry.getKey(), entry.getValue()); - } - ann = new AnnotationValue<>(annotation, newValues, AnnotationMetadataSupport.getDefaultValues(annotation)); - } - } + AnnotationValue existing = null; + for (AnnotationMetadata annotationMetadata : hierarchy) { + existing = mergeValue(annotation, existing, annotationMetadata.getAnnotation(annotation)); + } + return Optional.ofNullable(existing); + } + + @Nullable + private AnnotationValue mergeValue(@NonNull String annotation, + @Nullable AnnotationValue existingValue, + @Nullable AnnotationValue newValud) { + if (newValud == null) { + return existingValue; + } + if (existingValue == null) { + return newValud; + } + final Map values = newValud.getValues(); + final Map existing = existingValue.getValues(); + Map newValues = new LinkedHashMap<>(values.size() + existing.size()); + newValues.putAll(existing); + for (Map.Entry entry : values.entrySet()) { + newValues.putIfAbsent(entry.getKey(), entry.getValue()); } - return Optional.ofNullable(ann); + return new AnnotationValue<>(annotation, newValues, AnnotationMetadataSupport.getDefaultValues(annotation)); } @NonNull @Override public Optional> findDeclaredAnnotation(@NonNull String annotation) { + if (delegateDeclaredToAllElements) { + AnnotationValue existing = null; + for (AnnotationMetadata annotationMetadata : hierarchy) { + existing = mergeValue(annotation, existing, annotationMetadata.getDeclaredAnnotation(annotation)); + } + return Optional.ofNullable(existing); + } return hierarchy[0].findDeclaredAnnotation(annotation); } @@ -333,7 +371,7 @@ public OptionalDouble doubleValue(@NonNull String annotation, @NonNull String me } @Override - public Optional enumValue(@NonNull Class annotation, @NonNull String member, Class enumType) { + public > Optional enumValue(@NonNull Class annotation, @NonNull String member, Class enumType) { for (AnnotationMetadata annotationMetadata : hierarchy) { final Optional o = annotationMetadata.enumValue(annotation, member, enumType); if (o.isPresent()) { @@ -387,6 +425,13 @@ public List> getAnnotationValuesBySter @NonNull @Override public Set getDeclaredAnnotationNames() { + if (delegateDeclaredToAllElements) { + Set set = new HashSet<>(); + for (AnnotationMetadata am : hierarchy) { + set.addAll(am.getDeclaredAnnotationNames()); + } + return set; + } return hierarchy[0].getDeclaredAnnotationNames(); } @@ -443,10 +488,16 @@ public List> getAnnotationValuesByName if (annotationType == null) { return Collections.emptyList(); } + return mergeAnnotationValues(annotationType, AnnotationMetadata::getAnnotationValuesByName); + } + + @NonNull + private List> mergeAnnotationValues(V annotationType, + BiFunction>> fn) { List> list = new ArrayList<>(10); Set> uniqueValues = new HashSet<>(10); for (AnnotationMetadata am : hierarchy) { - for (AnnotationValue tAnnotationValue : am.getAnnotationValuesByName(annotationType)) { + for (AnnotationValue tAnnotationValue : fn.apply(am, annotationType)) { if (uniqueValues.add(tAnnotationValue)) { list.add(tAnnotationValue); } @@ -458,16 +509,30 @@ public List> getAnnotationValuesByName @NonNull @Override public List> getDeclaredAnnotationValuesByType(@NonNull Class annotationType) { + if (delegateDeclaredToAllElements) { + return mergeAnnotationValues(annotationType, AnnotationMetadata::getDeclaredAnnotationValuesByType); + } return hierarchy[0].getDeclaredAnnotationValuesByType(annotationType); } @Override public List> getDeclaredAnnotationValuesByName(String annotationType) { + if (delegateDeclaredToAllElements) { + return mergeAnnotationValues(annotationType, AnnotationMetadata::getDeclaredAnnotationValuesByName); + } return hierarchy[0].getDeclaredAnnotationValuesByName(annotationType); } @Override public boolean hasDeclaredAnnotation(@Nullable String annotation) { + if (delegateDeclaredToAllElements) { + for (AnnotationMetadata annotationMetadata : hierarchy) { + if (annotationMetadata.hasDeclaredAnnotation(annotation)) { + return true; + } + } + return false; + } return hierarchy[0].hasDeclaredAnnotation(annotation); } @@ -493,16 +558,24 @@ public boolean hasStereotype(@Nullable String annotation) { @Override public boolean hasDeclaredStereotype(@Nullable String annotation) { + if (delegateDeclaredToAllElements) { + for (AnnotationMetadata annotationMetadata : hierarchy) { + if (annotationMetadata.hasDeclaredStereotype(annotation)) { + return true; + } + } + return false; + } return hierarchy[0].hasDeclaredStereotype(annotation); } @Override - public Optional enumValue(String annotation, String member, Class enumType) { + public > Optional enumValue(String annotation, String member, Class enumType) { return enumValue(annotation, member, enumType, null); } @Override - public E[] enumValues(String annotation, String member, Class enumType) { + public > E[] enumValues(String annotation, String member, Class enumType) { return enumValues(annotation, member, enumType, null); } @@ -529,7 +602,7 @@ public Map getDefaultValues(@NonNull String annotation) { } @Override - public Optional enumValue(@NonNull Class annotation, @NonNull String member, Class enumType, @Nullable Function valueMapper) { + public > Optional enumValue(@NonNull Class annotation, @NonNull String member, Class enumType, @Nullable Function valueMapper) { for (AnnotationMetadata annotationMetadata : hierarchy) { final Optional o; if (annotationMetadata instanceof EnvironmentAnnotationMetadata) { @@ -545,7 +618,7 @@ public Optional enumValue(@NonNull Class Optional enumValue(@NonNull String annotation, @NonNull String member, Class enumType, @Nullable Function valueMapper) { + public > Optional enumValue(@NonNull String annotation, @NonNull String member, Class enumType, @Nullable Function valueMapper) { for (AnnotationMetadata annotationMetadata : hierarchy) { final Optional o; if (annotationMetadata instanceof EnvironmentAnnotationMetadata) { @@ -561,7 +634,7 @@ public Optional enumValue(@NonNull String annotation, @NonNu } @Override - public E[] enumValues(@NonNull Class annotation, @NonNull String member, Class enumType, @Nullable Function valueMapper) { + public > E[] enumValues(@NonNull Class annotation, @NonNull String member, Class enumType, @Nullable Function valueMapper) { E[] values = hierarchy[0].enumValues(annotation, member, enumType); for (int i = 1; i < hierarchy.length; i++) { AnnotationMetadata annotationMetadata = hierarchy[i]; @@ -575,7 +648,7 @@ public E[] enumValues(@NonNull Class anno } @Override - public E[] enumValues(@NonNull String annotation, @NonNull String member, Class enumType, @Nullable Function valueMapper) { + public > E[] enumValues(@NonNull String annotation, @NonNull String member, Class enumType, @Nullable Function valueMapper) { E[] values = hierarchy[0].enumValues(annotation, member, enumType); for (int i = 1; i < hierarchy.length; i++) { AnnotationMetadata annotationMetadata = hierarchy[i]; @@ -725,7 +798,7 @@ public OptionalLong longValue(Class annotation, String mem } @Override - public E[] enumValues(Class annotation, String member, Class enumType) { + public > E[] enumValues(Class annotation, String member, Class enumType) { return enumValues(annotation, member, enumType, null); } @@ -955,4 +1028,48 @@ public Optional findRepeatableAnnotation(String annotation) { } return Optional.empty(); } + + /** + * Merges the hierarchy into one {@link MutableAnnotationMetadata}. + * + * @return merged metadata + * @since 4.0.0 + */ + public MutableAnnotationMetadata merge() { + MutableAnnotationMetadata newAnnotationMetadata = new MutableAnnotationMetadata(); + for (AnnotationMetadata annotationMetadata : hierarchy) { + annotationMetadata = annotationMetadata.getTargetAnnotationMetadata(); + if (annotationMetadata.isEmpty()) { + continue; + } + if (annotationMetadata instanceof AnnotationMetadataHierarchy) { + newAnnotationMetadata.addAnnotationMetadata(((AnnotationMetadataHierarchy) annotationMetadata).merge()); + } else if (annotationMetadata instanceof DefaultAnnotationMetadata) { + newAnnotationMetadata.addAnnotationMetadata((DefaultAnnotationMetadata) annotationMetadata); + } else { + throw new IllegalStateException("Unknown instance of AnnotationMetadata: " + annotationMetadata.getClass()); + } + } + return newAnnotationMetadata; + } + + @Override + public AnnotationMetadata copyAnnotationMetadata() { + AnnotationMetadata[] copy = new AnnotationMetadata[hierarchy.length]; + System.arraycopy(hierarchy, 0, copy, 0, hierarchy.length); + ArrayUtils.reverse(copy); + return new AnnotationMetadataHierarchy( + delegateDeclaredToAllElements, + Arrays.stream(copy).map(AnnotationMetadata::copyAnnotationMetadata).toArray(AnnotationMetadata[]::new) + ); + } + + /** + * The size of the hierarchy. + * @return The size + * @since 4.0.0 + */ + public int size() { + return hierarchy.length; + } } diff --git a/inject/src/main/java/io/micronaut/inject/annotation/AnnotationMetadataReference.java b/inject/src/main/java/io/micronaut/inject/annotation/AnnotationMetadataReference.java index 85315dc325e..2e10500c7b4 100644 --- a/inject/src/main/java/io/micronaut/inject/annotation/AnnotationMetadataReference.java +++ b/inject/src/main/java/io/micronaut/inject/annotation/AnnotationMetadataReference.java @@ -52,4 +52,10 @@ public AnnotationMetadata getAnnotationMetadata() { public String getClassName() { return className; } + + @Override + public AnnotationMetadata getTargetAnnotationMetadata() { + // Don't unwrap the reference + return this; + } } diff --git a/inject/src/main/java/io/micronaut/inject/annotation/AnnotationMetadataSupport.java b/inject/src/main/java/io/micronaut/inject/annotation/AnnotationMetadataSupport.java index bfa2f64b57e..62bcb0c3098 100644 --- a/inject/src/main/java/io/micronaut/inject/annotation/AnnotationMetadataSupport.java +++ b/inject/src/main/java/io/micronaut/inject/annotation/AnnotationMetadataSupport.java @@ -94,9 +94,12 @@ public final class AnnotationMetadataSupport { ANNOTATION_TYPES.put(ann.getName(), ann) ); + for (Map.Entry, Class> e : getCoreRepeatableAnnotations()) { REPEATABLE_ANNOTATIONS.put(e.getKey().getName(), e.getValue().getName()); } + + REPEATABLE_ANNOTATIONS.put("io.micronaut.aop.InterceptorBinding", "io.micronaut.aop.InterceptorBindingDefinitions"); } /** diff --git a/inject/src/main/java/io/micronaut/inject/annotation/AnnotationMetadataWriter.java b/inject/src/main/java/io/micronaut/inject/annotation/AnnotationMetadataWriter.java index 2da33ae9c52..778cb87de53 100644 --- a/inject/src/main/java/io/micronaut/inject/annotation/AnnotationMetadataWriter.java +++ b/inject/src/main/java/io/micronaut/inject/annotation/AnnotationMetadataWriter.java @@ -17,6 +17,7 @@ import io.micronaut.core.annotation.AnnotationClassValue; import io.micronaut.core.annotation.AnnotationMetadata; +import io.micronaut.core.annotation.AnnotationMetadataDelegate; import io.micronaut.core.annotation.AnnotationUtil; import io.micronaut.core.annotation.Internal; import io.micronaut.core.annotation.UsedByGeneratedCode; @@ -182,6 +183,9 @@ public AnnotationMetadataWriter( boolean writeAnnotationDefaults) { super(originatingElement); this.className = className + AnnotationMetadata.CLASS_NAME_SUFFIX; + if (annotationMetadata instanceof AnnotationMetadataDelegate) { + annotationMetadata = ((AnnotationMetadataDelegate) annotationMetadata).getAnnotationMetadata(); + } if (annotationMetadata instanceof DefaultAnnotationMetadata) { this.annotationMetadata = annotationMetadata; } else if (annotationMetadata instanceof AnnotationMetadataHierarchy) { @@ -333,7 +337,10 @@ private static void pushNewAnnotationMetadataOrReference( Map defaultsStorage, Map loadTypeMethods, AnnotationMetadata annotationMetadata) { - if (annotationMetadata instanceof DefaultAnnotationMetadata) { + annotationMetadata = annotationMetadata.getTargetAnnotationMetadata(); + if (annotationMetadata.isEmpty()) { + generatorAdapter.getStatic(Type.getType(AnnotationMetadata.class), "EMPTY_METADATA", Type.getType(AnnotationMetadata.class)); + } else if (annotationMetadata instanceof DefaultAnnotationMetadata) { instantiateNewMetadata( owningType, classWriter, @@ -345,7 +352,7 @@ private static void pushNewAnnotationMetadataOrReference( } else if (annotationMetadata instanceof AnnotationMetadataReference) { pushAnnotationMetadataReference(generatorAdapter, (AnnotationMetadataReference) annotationMetadata); } else { - generatorAdapter.getStatic(Type.getType(AnnotationMetadata.class), "EMPTY_METADATA", Type.getType(AnnotationMetadata.class)); + throw new IllegalStateException("Unknown annotation metadata: " + annotationMetadata); } } diff --git a/inject/src/main/java/io/micronaut/inject/annotation/DefaultAnnotationMetadata.java b/inject/src/main/java/io/micronaut/inject/annotation/DefaultAnnotationMetadata.java index 1d80d138ccf..90bce9481f4 100644 --- a/inject/src/main/java/io/micronaut/inject/annotation/DefaultAnnotationMetadata.java +++ b/inject/src/main/java/io/micronaut/inject/annotation/DefaultAnnotationMetadata.java @@ -85,10 +85,12 @@ public class DefaultAnnotationMetadata extends AbstractAnnotationMetadata implem Map> annotationsByStereotype; @Nullable Map> annotationDefaultValues; + @Nullable Map repeated = null; - + @Nullable + Set sourceRetentionAnnotations; private Map annotationValuesByType = new ConcurrentHashMap<>(2); - private Set sourceRetentionAnnotations; + private final boolean hasPropertyExpressions; // This should be removed in the next major version private final boolean useRepeatableDefaults; @@ -229,22 +231,22 @@ public boolean isPresent(@NonNull String annotation, @NonNull String member) { } @Override - public Optional enumValue(@NonNull String annotation, Class enumType) { + public > Optional enumValue(@NonNull String annotation, Class enumType) { return enumValue(annotation, VALUE_MEMBER, enumType, null); } @Override - public Optional enumValue(@NonNull String annotation, @NonNull String member, Class enumType) { + public > Optional enumValue(@NonNull String annotation, @NonNull String member, Class enumType) { return enumValue(annotation, member, enumType, null); } @Override - public Optional enumValue(@NonNull Class annotation, Class enumType) { + public > Optional enumValue(@NonNull Class annotation, Class enumType) { return enumValue(annotation, VALUE_MEMBER, enumType); } @Override - public Optional enumValue(@NonNull Class annotation, @NonNull String member, Class enumType) { + public > Optional enumValue(@NonNull Class annotation, @NonNull String member, Class enumType) { return enumValue(annotation, member, enumType, null); } @@ -260,7 +262,7 @@ public Optional enumValue(@NonNull Class Optional enumValue(@NonNull Class annotation, @NonNull String member, Class enumType, @Nullable Function valueMapper) { + public > Optional enumValue(@NonNull Class annotation, @NonNull String member, Class enumType, @Nullable Function valueMapper) { ArgumentUtils.requireNonNull("annotation", annotation); ArgumentUtils.requireNonNull("member", member); final Repeatable repeatable = annotation.getAnnotation(Repeatable.class); @@ -276,27 +278,27 @@ public Optional enumValue(@NonNull Class E[] enumValues(@NonNull String annotation, Class enumType) { + public > E[] enumValues(@NonNull String annotation, Class enumType) { return enumValues(annotation, VALUE_MEMBER, enumType, null); } @Override - public E[] enumValues(@NonNull String annotation, @NonNull String member, Class enumType) { + public > E[] enumValues(@NonNull String annotation, @NonNull String member, Class enumType) { return enumValues(annotation, member, enumType, null); } @Override - public E[] enumValues(@NonNull Class annotation, Class enumType) { + public > E[] enumValues(@NonNull Class annotation, Class enumType) { return enumValues(annotation, VALUE_MEMBER, enumType, null); } @Override - public E[] enumValues(@NonNull Class annotation, @NonNull String member, Class enumType) { + public > E[] enumValues(@NonNull Class annotation, @NonNull String member, Class enumType) { return enumValues(annotation, member, enumType, null); } @Override - public E[] enumValues(@NonNull Class annotation, @NonNull String member, Class enumType, @Nullable Function valueMapper) { + public > E[] enumValues(@NonNull Class annotation, @NonNull String member, Class enumType, @Nullable Function valueMapper) { ArgumentUtils.requireNonNull("annotation", annotation); ArgumentUtils.requireNonNull("enumType", enumType); final Repeatable repeatable = annotation.getAnnotation(Repeatable.class); @@ -313,7 +315,7 @@ public E[] enumValues(@NonNull Class anno } @Override - public E[] enumValues(@NonNull String annotation, @NonNull String member, Class enumType, @Nullable Function valueMapper) { + public > E[] enumValues(@NonNull String annotation, @NonNull String member, Class enumType, @Nullable Function valueMapper) { ArgumentUtils.requireNonNull("annotation", annotation); ArgumentUtils.requireNonNull("enumType", enumType); Object v = getRawValue(annotation, member); @@ -332,18 +334,18 @@ public E[] enumValues(@NonNull String annotation, @NonNull Stri */ @Override @Internal - public Optional enumValue(@NonNull String annotation, @NonNull String member, Class enumType, @Nullable Function valueMapper) { + public > Optional enumValue(@NonNull String annotation, @NonNull String member, Class enumType, @Nullable Function valueMapper) { Object rawValue = getRawSingleValue(annotation, member, valueMapper); return enumValueOf(enumType, rawValue); } - private Optional enumValueOf(Class enumType, Object rawValue) { + private > Optional enumValueOf(Class enumType, Object rawValue) { if (rawValue != null) { if (enumType.isInstance(rawValue)) { return Optional.of((E) rawValue); } else { try { - return Optional.of((E) Enum.valueOf(enumType, rawValue.toString())); + return Optional.of(Enum.valueOf(enumType, rawValue.toString())); } catch (Exception e) { return Optional.empty(); } @@ -1427,6 +1429,11 @@ public Optional findRepeatableAnnotation(String annotation) { return Optional.ofNullable(AnnotationMetadataSupport.getRepeatableAnnotation(annotation)); } + @Override + public AnnotationMetadata copyAnnotationMetadata() { + return clone(); + } + @Override public DefaultAnnotationMetadata clone() { DefaultAnnotationMetadata cloned = new DefaultAnnotationMetadata( @@ -1440,39 +1447,41 @@ public DefaultAnnotationMetadata clone() { if (repeated != null) { cloned.repeated = new HashMap<>(repeated); } + if (sourceRetentionAnnotations != null) { + cloned.sourceRetentionAnnotations = new HashSet<>(sourceRetentionAnnotations); + } + if (annotationDefaultValues != null) { + cloned.annotationDefaultValues = cloneMapOfMapValue(annotationDefaultValues); + } return cloned; } protected final Map> cloneMapOfMapValue(Map> toClone) { return toClone.entrySet().stream() .map(e -> new AbstractMap.SimpleEntry<>(e.getKey(), cloneMap(e.getValue()))) - .collect(Collectors.toMap(AbstractMap.SimpleEntry::getKey, AbstractMap.SimpleEntry::getValue, (a, b) -> a, () -> { - if (toClone instanceof HashMap) { - return new HashMap<>(); - } - return new LinkedHashMap<>(); - })); + .collect(Collectors.toMap(AbstractMap.SimpleEntry::getKey, AbstractMap.SimpleEntry::getValue, (a, b) -> a, LinkedHashMap::new)); } protected final Map> cloneMapOfListValue(Map> toClone) { return toClone.entrySet().stream() .map(e -> new AbstractMap.SimpleEntry<>(e.getKey(), new ArrayList<>(e.getValue()))) - .collect(Collectors.toMap(AbstractMap.SimpleEntry::getKey, AbstractMap.SimpleEntry::getValue, (a, b) -> a, () -> { - if (toClone instanceof HashMap) { - return new HashMap<>(); - } - return new LinkedHashMap<>(); - })); + .collect(Collectors.toMap(AbstractMap.SimpleEntry::getKey, AbstractMap.SimpleEntry::getValue, (a, b) -> a, LinkedHashMap::new)); } protected final Map cloneMap(Map map) { - if (map instanceof HashMap) { - return (Map) ((HashMap) map).clone(); - } + Map newMap; if (map instanceof LinkedHashMap) { - return (Map) ((LinkedHashMap) map).clone(); + newMap = (Map) ((LinkedHashMap) map).clone(); + } else { + newMap = new LinkedHashMap<>(map); + } + for (Map.Entry entry : newMap.entrySet()) { + if (entry.getValue() instanceof Set) { + LinkedHashSet newValue = new LinkedHashSet<>((Collection) entry.getValue()); + entry.setValue((V) newValue); + } } - return new HashMap<>(map); + return new HashMap<>(newMap); } /** @@ -2136,6 +2145,86 @@ public static AnnotationMetadata mutateMember( return mutateMember(annotationMetadata, annotationName, Collections.singletonMap(member, value)); } + /** + * Include the annotation metadata from the other instance of {@link DefaultAnnotationMetadata}. + * @param annotationMetadata The annotation metadata + * @since 4.0.0 + */ + @Internal + protected void addAnnotationMetadata(DefaultAnnotationMetadata annotationMetadata) { + if (annotationMetadata.declaredAnnotations != null && !annotationMetadata.declaredAnnotations.isEmpty()) { + if (declaredAnnotations == null) { + declaredAnnotations = new LinkedHashMap<>(); + } + for (Map.Entry> entry : annotationMetadata.declaredAnnotations.entrySet()) { + putValues(entry.getKey(), entry.getValue(), declaredAnnotations); + } + } + if (annotationMetadata.declaredStereotypes != null && !annotationMetadata.declaredStereotypes.isEmpty()) { + if (declaredStereotypes == null) { + declaredStereotypes = new LinkedHashMap<>(); + } + for (Map.Entry> entry : annotationMetadata.declaredStereotypes.entrySet()) { + putValues(entry.getKey(), entry.getValue(), declaredStereotypes); + } + } + if (annotationMetadata.allStereotypes != null && !annotationMetadata.allStereotypes.isEmpty()) { + if (allStereotypes == null) { + allStereotypes = new LinkedHashMap<>(); + } + for (Map.Entry> entry : annotationMetadata.allStereotypes.entrySet()) { + putValues(entry.getKey(), entry.getValue(), allStereotypes); + } + } + if (annotationMetadata.allAnnotations != null && !annotationMetadata.allAnnotations.isEmpty()) { + if (allAnnotations == null) { + allAnnotations = new LinkedHashMap<>(); + } + for (Map.Entry> entry : annotationMetadata.allAnnotations.entrySet()) { + putValues(entry.getKey(), entry.getValue(), allAnnotations); + } + } + Map> source = annotationMetadata.annotationsByStereotype; + if (source != null && !source.isEmpty()) { + if (annotationsByStereotype == null) { + annotationsByStereotype = new LinkedHashMap<>(); + } + for (Map.Entry> entry : source.entrySet()) { + String ann = entry.getKey(); + List prevValues = annotationsByStereotype.get(ann); + if (prevValues == null) { + annotationsByStereotype.put(ann, new ArrayList<>(entry.getValue())); + } else { + Set prevValuesSet = new LinkedHashSet<>(prevValues); + prevValuesSet.addAll(entry.getValue()); + annotationsByStereotype.put(ann, new ArrayList<>(prevValuesSet)); + } + } + } + if (annotationMetadata.repeated != null) { + if (repeated == null) { + repeated = new LinkedHashMap<>(annotationMetadata.repeated); + } else { + repeated.putAll(annotationMetadata.repeated); + } + } + if (annotationMetadata.sourceRetentionAnnotations != null) { + if (sourceRetentionAnnotations == null) { + sourceRetentionAnnotations = new HashSet<>(annotationMetadata.sourceRetentionAnnotations); + } else { + sourceRetentionAnnotations.addAll(annotationMetadata.sourceRetentionAnnotations); + } + } + if (annotationMetadata.annotationDefaultValues != null) { + if (annotationDefaultValues == null) { + annotationDefaultValues = new LinkedHashMap<>(annotationMetadata.annotationDefaultValues); + } else { + // No need to merge values + annotationDefaultValues.putAll(annotationMetadata.annotationDefaultValues); + } + } + } + /** * Contributes defaults to the given target. * @@ -2146,8 +2235,9 @@ public static AnnotationMetadata mutateMember( */ @Internal public static void contributeDefaults(AnnotationMetadata target, AnnotationMetadata source) { + source = source.getTargetAnnotationMetadata(); if (source instanceof AnnotationMetadataHierarchy) { - source = source.getDeclaredMetadata(); + source = ((AnnotationMetadataHierarchy) source).merge(); } if (target instanceof DefaultAnnotationMetadata && source instanceof DefaultAnnotationMetadata) { DefaultAnnotationMetadata damTarget = (DefaultAnnotationMetadata) target; @@ -2180,8 +2270,9 @@ public static void contributeDefaults(AnnotationMetadata target, AnnotationMetad */ @Internal public static void contributeRepeatable(AnnotationMetadata target, AnnotationMetadata source) { + source = source.getTargetAnnotationMetadata(); if (source instanceof AnnotationMetadataHierarchy) { - source = source.getDeclaredMetadata(); + source = ((AnnotationMetadataHierarchy) source).merge(); } if (target instanceof DefaultAnnotationMetadata && source instanceof DefaultAnnotationMetadata) { DefaultAnnotationMetadata damTarget = (DefaultAnnotationMetadata) target; diff --git a/inject/src/main/java/io/micronaut/inject/annotation/EnvironmentAnnotationMetadata.java b/inject/src/main/java/io/micronaut/inject/annotation/EnvironmentAnnotationMetadata.java index df25795ce12..f1704d17701 100644 --- a/inject/src/main/java/io/micronaut/inject/annotation/EnvironmentAnnotationMetadata.java +++ b/inject/src/main/java/io/micronaut/inject/annotation/EnvironmentAnnotationMetadata.java @@ -51,7 +51,7 @@ default boolean hasPropertyExpressions() { * @return The enum value */ @Internal - Optional enumValue(@NonNull Class annotation, @NonNull String member, Class enumType, @Nullable Function valueMapper); + > Optional enumValue(@NonNull Class annotation, @NonNull String member, Class enumType, @Nullable Function valueMapper); /** * Retrieve the enum value and optionally map its value. @@ -63,7 +63,7 @@ default boolean hasPropertyExpressions() { * @return The enum value */ @Internal - Optional enumValue(@NonNull String annotation, @NonNull String member, Class enumType, @Nullable Function valueMapper); + > Optional enumValue(@NonNull String annotation, @NonNull String member, Class enumType, @Nullable Function valueMapper); /** * Retrieve the enum values and optionally map its value. @@ -75,7 +75,7 @@ default boolean hasPropertyExpressions() { * @return The enum value */ @Internal - E[] enumValues(@NonNull Class annotation, @NonNull String member, Class enumType, @Nullable Function valueMapper); + > E[] enumValues(@NonNull Class annotation, @NonNull String member, Class enumType, @Nullable Function valueMapper); /** * Retrieve the enum values and optionally map its value. @@ -87,7 +87,7 @@ default boolean hasPropertyExpressions() { * @return The enum value */ @Internal - E[] enumValues(@NonNull String annotation, @NonNull String member, Class enumType, @Nullable Function valueMapper); + > E[] enumValues(@NonNull String annotation, @NonNull String member, Class enumType, @Nullable Function valueMapper); /** * Retrieve the class value and optionally map its value. diff --git a/inject/src/main/java/io/micronaut/inject/annotation/MutableAnnotationMetadata.java b/inject/src/main/java/io/micronaut/inject/annotation/MutableAnnotationMetadata.java index 1a6d0df7bf9..3338560d426 100644 --- a/inject/src/main/java/io/micronaut/inject/annotation/MutableAnnotationMetadata.java +++ b/inject/src/main/java/io/micronaut/inject/annotation/MutableAnnotationMetadata.java @@ -16,6 +16,7 @@ package io.micronaut.inject.annotation; import io.micronaut.context.env.DefaultPropertyPlaceholderResolver; +import io.micronaut.core.annotation.AnnotationMetadata; import io.micronaut.core.annotation.AnnotationValue; import io.micronaut.core.annotation.NonNull; import io.micronaut.core.annotation.Nullable; @@ -25,6 +26,7 @@ import java.lang.annotation.RetentionPolicy; import java.util.Arrays; import java.util.HashMap; +import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -63,6 +65,30 @@ private MutableAnnotationMetadata(@Nullable Map(repeated); } + if (sourceRetentionAnnotations != null) { + cloned.sourceRetentionAnnotations = new HashSet<>(sourceRetentionAnnotations); + } + if (annotationDefaultValues != null) { + cloned.annotationDefaultValues = cloneMapOfMapValue(annotationDefaultValues); + } + cloned.hasPropertyExpressions = hasPropertyExpressions; return cloned; } diff --git a/inject/src/main/java/io/micronaut/inject/ast/BeanPropertiesQuery.java b/inject/src/main/java/io/micronaut/inject/ast/BeanPropertiesQuery.java new file mode 100644 index 00000000000..5b67b3f20be --- /dev/null +++ b/inject/src/main/java/io/micronaut/inject/ast/BeanPropertiesQuery.java @@ -0,0 +1,173 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.inject.ast; + +import io.micronaut.context.annotation.BeanProperties; +import io.micronaut.context.annotation.ConfigurationBuilder; +import io.micronaut.context.annotation.ConfigurationProperties; +import io.micronaut.core.annotation.AccessorsStyle; +import io.micronaut.core.annotation.AnnotationMetadata; +import io.micronaut.core.annotation.AnnotationValue; +import io.micronaut.core.util.CollectionUtils; + +import java.util.Arrays; +import java.util.Collections; +import java.util.EnumSet; +import java.util.HashSet; +import java.util.Set; + +/** + * The bean properties configuration. + * + * @author Denis Stepanov + * @since 4.0.0 + */ +public final class BeanPropertiesQuery { + + private BeanProperties.Visibility visibility = BeanProperties.Visibility.DEFAULT; + private Set accessKinds = EnumSet.of(BeanProperties.AccessKind.METHOD); + private Set includes = Collections.emptySet(); + private Set excludes = Collections.emptySet(); + private String[] readPrefixes = new String[]{AccessorsStyle.DEFAULT_READ_PREFIX}; + private String[] writePrefixes = new String[]{AccessorsStyle.DEFAULT_WRITE_PREFIX}; + private boolean allowSetterWithZeroArgs; + private boolean allowSetterWithMultipleArgs; + private boolean allowStaticProperties; + private Set excludedAnnotations = Collections.emptySet(); + + public static BeanPropertiesQuery of(AnnotationMetadata annotationMetadata) { + BeanPropertiesQuery conf = new BeanPropertiesQuery(); + Set includes = new HashSet<>(); + Set excludes = new HashSet<>(); + + AnnotationValue annotation = annotationMetadata.getAnnotation(BeanProperties.class); + if (annotation != null) { + annotation.enumValue(BeanProperties.MEMBER_VISIBILITY, BeanProperties.Visibility.class) + .ifPresent(conf::setVisibility); + if (annotation.isPresent(BeanProperties.MEMBER_ACCESS_KIND)) { + conf.setAccessKinds( + annotation.enumValuesSet(BeanProperties.MEMBER_ACCESS_KIND, BeanProperties.AccessKind.class) + ); + } + annotation.booleanValue(BeanProperties.MEMBER_ALLOW_WRITE_WITH_ZERO_ARGS) + .ifPresent(conf::setAllowSetterWithZeroArgs); + annotation.booleanValue(BeanProperties.MEMBER_ALLOW_WRITE_WITH_MULTIPLE_ARGS) + .ifPresent(conf::setAllowSetterWithMultipleArgs); + + includes.addAll(Arrays.asList(annotation.stringValues(BeanProperties.MEMBER_INCLUDES))); + excludes.addAll(Arrays.asList(annotation.stringValues(BeanProperties.MEMBER_EXCLUDES))); + + conf.setExcludedAnnotations(CollectionUtils.setOf(annotation.stringValues(BeanProperties.MEMBER_EXCLUDED_ANNOTATIONS))); + } + + // TODO: investigate why aliases aren't propagated + includes.addAll(Arrays.asList(annotationMetadata.stringValues(ConfigurationProperties.class, BeanProperties.MEMBER_INCLUDES))); + excludes.addAll(Arrays.asList(annotationMetadata.stringValues(ConfigurationProperties.class, BeanProperties.MEMBER_EXCLUDES))); + + includes.addAll(Arrays.asList(annotationMetadata.stringValues(ConfigurationBuilder.class, BeanProperties.MEMBER_INCLUDES))); + excludes.addAll(Arrays.asList(annotationMetadata.stringValues(ConfigurationBuilder.class, BeanProperties.MEMBER_EXCLUDES))); + + conf.setIncludes(includes); + conf.setExcludes(excludes); + + annotationMetadata.getValue(AccessorsStyle.class, "readPrefixes", String[].class) + .ifPresent(conf::setReadPrefixes); + annotationMetadata.getValue(AccessorsStyle.class, "writePrefixes", String[].class) + .ifPresent(conf::setWritePrefixes); + + return conf; + } + + public BeanProperties.Visibility getVisibility() { + return visibility; + } + + public void setVisibility(BeanProperties.Visibility visibility) { + this.visibility = visibility; + } + + public Set getAccessKinds() { + return accessKinds; + } + + public void setAccessKinds(Set accessKinds) { + this.accessKinds = accessKinds; + } + + public Set getIncludes() { + return includes; + } + + public void setIncludes(Set includes) { + this.includes = includes; + } + + public Set getExcludes() { + return excludes; + } + + public void setExcludes(Set excludes) { + this.excludes = excludes; + } + + public String[] getReadPrefixes() { + return readPrefixes; + } + + public void setReadPrefixes(String[] readPrefixes) { + this.readPrefixes = readPrefixes; + } + + public String[] getWritePrefixes() { + return writePrefixes; + } + + public void setWritePrefixes(String[] writePrefixes) { + this.writePrefixes = writePrefixes; + } + + public boolean isAllowSetterWithZeroArgs() { + return allowSetterWithZeroArgs; + } + + public void setAllowSetterWithZeroArgs(boolean allowSetterWithZeroArgs) { + this.allowSetterWithZeroArgs = allowSetterWithZeroArgs; + } + + public boolean isAllowSetterWithMultipleArgs() { + return allowSetterWithMultipleArgs; + } + + public void setAllowSetterWithMultipleArgs(boolean allowSetterWithMultipleArgs) { + this.allowSetterWithMultipleArgs = allowSetterWithMultipleArgs; + } + + public boolean isAllowStaticProperties() { + return allowStaticProperties; + } + + public void setAllowStaticProperties(boolean allowStaticProperties) { + this.allowStaticProperties = allowStaticProperties; + } + + public Set getExcludedAnnotations() { + return excludedAnnotations; + } + + public void setExcludedAnnotations(Set excludedAnnotations) { + this.excludedAnnotations = excludedAnnotations; + } +} diff --git a/inject/src/main/java/io/micronaut/inject/ast/ClassElement.java b/inject/src/main/java/io/micronaut/inject/ast/ClassElement.java index aafbd47c45c..9a1c8d43eaf 100644 --- a/inject/src/main/java/io/micronaut/inject/ast/ClassElement.java +++ b/inject/src/main/java/io/micronaut/inject/ast/ClassElement.java @@ -16,6 +16,8 @@ package io.micronaut.inject.ast; import io.micronaut.core.annotation.AnnotationMetadata; +import io.micronaut.core.annotation.AnnotationUtil; +import io.micronaut.core.annotation.Creator; import io.micronaut.core.annotation.Experimental; import io.micronaut.core.annotation.Internal; import io.micronaut.core.annotation.NonNull; @@ -52,6 +54,7 @@ public interface ClassElement extends TypedElement { /** * Constant for an empty class element array. + * * @since 3.1.0 */ ClassElement[] ZERO_CLASS_ELEMENTS = new ClassElement[0]; @@ -76,8 +79,8 @@ default boolean isTypeVariable() { } /** - * @see GenericPlaceholderElement * @return Whether this is a generic placeholder. + * @see GenericPlaceholderElement * @since 3.1.0 */ @Experimental @@ -86,8 +89,8 @@ default boolean isGenericPlaceholder() { } /** - * @see WildcardElement * @return Whether this is a wildcard. + * @see WildcardElement */ @Experimental default boolean isWildcard() { @@ -140,6 +143,7 @@ default boolean isRecord() { /** * Is this type an inner class. + * * @return True if it is an inner class * @since 2.1.2 */ @@ -149,6 +153,7 @@ default boolean isInner() { /** * Whether this element is an enum. + * * @return True if it is an enum */ default boolean isEnum() { @@ -169,7 +174,31 @@ default boolean isProxy() { * @return The primary constructor if one is present */ default @NonNull Optional getPrimaryConstructor() { - return Optional.empty(); + Optional staticCreator = findStaticCreator(); + if (staticCreator.isPresent()) { + return staticCreator; + } + if (isInner() && !isStatic()) { + // only static inner classes can be constructed + return Optional.empty(); + } + List constructors = getAccessibleConstructors(); + if (constructors.isEmpty()) { + return Optional.empty(); + } + if (constructors.size() == 1) { + return Optional.of(constructors.get(0)); + } + Optional annotatedConstructor = constructors.stream() + .filter(c -> c.hasStereotype(AnnotationUtil.INJECT) || c.hasStereotype(Creator.class)) + .findFirst(); + if (annotatedConstructor.isPresent()) { + return annotatedConstructor.map(c -> c); + } + return constructors.stream() + .filter(io.micronaut.inject.ast.Element::isPublic) + .map(c -> c) + .findFirst(); } /** @@ -178,8 +207,122 @@ default boolean isProxy() { * * @return The default constructor if one is present */ - default @NonNull Optional getDefaultConstructor() { - return Optional.empty(); + default Optional getDefaultConstructor() { + Optional staticCreator = findDefaultStaticCreator(); + if (staticCreator.isPresent()) { + return staticCreator; + } + if (isInner() && !isStatic()) { + // only static inner classes can be constructed + return Optional.empty(); + } + List constructors = getAccessibleConstructors() + .stream() + .filter(ctor -> ctor.getParameters().length == 0) + .collect(Collectors.toList()); + if (constructors.isEmpty()) { + return Optional.empty(); + } + if (constructors.size() == 1) { + return Optional.of(constructors.get(0)); + } + return constructors.stream() + .filter(Element::isPublic) + .map(c -> c) + .findFirst(); + + } + + /** + * Find and return a single primary static creator. If more than creator candidate exists, then return empty unless a static + * creator is found that is annotated with {@link io.micronaut.core.annotation.Creator}. + * + * @return The primary creator if one is present + */ + default Optional findStaticCreator() { + List staticCreators = getAccessibleStaticCreators(); + if (staticCreators.isEmpty()) { + return Optional.empty(); + } + if (staticCreators.size() == 1) { + return Optional.of(staticCreators.get(0)); + } + //Can be multiple static @Creator methods. Prefer one with args here. The no arg method (if present) will + //be picked up by findDefaultStaticCreator + List withArgs = staticCreators.stream().filter(method -> method.getParameters().length > 0).collect(Collectors.toList()); + if (withArgs.size() == 1) { + return Optional.of(withArgs.get(0)); + } else { + staticCreators = withArgs; + } + return staticCreators.stream().filter(Element::isPublic).findFirst(); + } + + /** + * Find and return a single default static creator. A default static creator is one + * without arguments that is accessible. + * * + * @return a static creator + * @since 4.0.0 + */ + default Optional findDefaultStaticCreator() { + List staticCreators = getAccessibleStaticCreators() + .stream() + .filter(c -> c.getParameters().length == 0) + .collect(Collectors.toList()); + if (staticCreators.isEmpty()) { + return Optional.empty(); + } + if (staticCreators.size() == 1) { + return Optional.of(staticCreators.get(0)); + } + return staticCreators.stream().filter(Element::isPublic).findFirst(); + } + + /** + * Find accessible constructors. + * + * @return accessible constructors + * @since 4.0.0 + */ + @NonNull + default List getAccessibleConstructors() { + return getEnclosedElements(ElementQuery.CONSTRUCTORS) + .stream() + .filter(ctor -> !ctor.isPrivate()) + .collect(Collectors.toList()); + } + + /** + * Get accessible static creators. + * A static creator is a static method annotated with {@link Creator} that can be used to create the class. + * For enums "valueOf" is picked as a static creator. + * + * @return static creators + * @since 4.0.0 + */ + @NonNull + default List getAccessibleStaticCreators() { + List creators = getEnclosedElements(ElementQuery.ALL_METHODS + .onlyDeclared() + .onlyStatic() + .onlyAccessible() + .annotated(annotationMetadata -> annotationMetadata.hasStereotype(Creator.class)) + ) + .stream() + .filter(method -> method.getReturnType().isAssignable(this)) + .collect(Collectors.toList()); + if (creators.isEmpty() && isEnum()) { + return getEnclosedElements(ElementQuery.ALL_METHODS + .named("valueOf") + .onlyStatic() + .onlyAccessible() + ) + .stream() + .filter(method -> method.getReturnType().isAssignable(this)) + .collect(Collectors.toList()); + } + return creators; } /** @@ -238,15 +381,41 @@ default PackageElement getPackage() { * * @return The bean properties for this class element */ + @NonNull default List getBeanProperties() { return Collections.emptyList(); } + /** + * Returns the synthetic bean properties. The properties where one of the methods (getter or setter) + * is synthetic - not user defined but created by the compiler. + * + * @return The bean properties for this class element + * @since 4.0.0 + */ + @NonNull + default List getSyntheticBeanProperties() { + return Collections.emptyList(); + } + + /** + * Returns the bean properties (getters and setters) for this class element based on custom configuration. + * + * @param beanPropertiesQuery The configuration + * @return The bean properties for this class element + * @since 4.0.0 + */ + @NonNull + default List getBeanProperties(BeanPropertiesQuery beanPropertiesQuery) { + return Collections.emptyList(); + } + /** * Return all the fields of this class element. * * @return The fields */ + @NonNull default List getFields() { return getEnclosedElements(ElementQuery.ALL_FIELDS); } @@ -255,10 +424,11 @@ default List getFields() { * Return the elements that match the given query. * * @param query The query to use. - * @param The element type + * @param The element type * @return The fields * @since 2.3.0 */ + @NonNull default List getEnclosedElements(@NonNull ElementQuery query) { return Collections.emptyList(); } @@ -277,7 +447,7 @@ default Optional getEnclosingType() { * Return the first enclosed element matching the given query. * * @param query The query to use. - * @param The element type + * @param The element type * @return The fields * @since 2.3.0 */ @@ -380,7 +550,6 @@ default ClassElement withBoundGenericTypes(@NonNull List * *

This also means that this method may return {@code null} if the top-level fold operation returned {@code null}.

* - * * @param fold The fold operation to apply recursively to all component types. * @return The folded type. * @since 3.1.0 @@ -456,7 +625,8 @@ default boolean isAssignable(Class type) { * * @return A new class element */ - @NonNull ClassElement toArray(); + @NonNull + ClassElement toArray(); /** * Dereference a class element denoting an array type by converting it to its element type. @@ -465,13 +635,15 @@ default boolean isAssignable(Class type) { * @return A new class element * @throws IllegalStateException if this class element doesn't denote an array type */ - @NonNull ClassElement fromArray(); + @NonNull + ClassElement fromArray(); /** * This method adds an associated bean using this class element as the originating element. * *

Note that this method can only be called on classes being directly compiled by Micronaut. If the ClassElement is * loaded from pre-compiled code an {@link UnsupportedOperationException} will be thrown.

+ * * @param type The type of the bean * @return A bean builder */ @@ -480,14 +652,31 @@ BeanElementBuilder addAssociatedBean(@NonNull ClassElement type) { throw new UnsupportedOperationException("Element of type [" + getClass() + "] does not support adding associated beans at compilation time"); } + @Override + default ClassElement withAnnotationMetadata(AnnotationMetadata annotationMetadata) { + return (ClassElement) TypedElement.super.withAnnotationMetadata(annotationMetadata); + } + + /** + * Copies this element and overrides its type arguments. + * + * @param typeArguments The type arguments + * @return A new element + * @since 4.0.0 + */ + default ClassElement withTypeArguments(Map typeArguments) { + throw new UnsupportedOperationException("Element of type [" + getClass() + "] does not support copy constructor"); + } + /** * Create a class element for the given simple type. + * * @param type The type * @return The class element */ static @NonNull ClassElement of(@NonNull Class type) { return new ReflectClassElement( - Objects.requireNonNull(type, "Type cannot be null") + Objects.requireNonNull(type, "Type cannot be null") ); } @@ -517,8 +706,8 @@ static ClassElement of(@NonNull Type type) { @Override public List getBoundGenericTypes() { return Arrays.stream(pType.getActualTypeArguments()) - .map(ClassElement::of) - .collect(Collectors.toList()); + .map(ClassElement::of) + .collect(Collectors.toList()); } }; } else if (type instanceof GenericArrayType) { @@ -530,20 +719,21 @@ public List getBoundGenericTypes() { /** * Create a class element for the given simple type. - * @param type The type + * + * @param type The type * @param annotationMetadata The annotation metadata - * @param typeArguments The type arguments + * @param typeArguments The type arguments * @return The class element * @since 2.4.0 */ static @NonNull ClassElement of( - @NonNull Class type, - @NonNull AnnotationMetadata annotationMetadata, - @NonNull Map typeArguments) { + @NonNull Class type, + @NonNull AnnotationMetadata annotationMetadata, + @NonNull Map typeArguments) { Objects.requireNonNull(annotationMetadata, "Annotation metadata cannot be null"); Objects.requireNonNull(typeArguments, "Type arguments cannot be null"); return new ReflectClassElement( - Objects.requireNonNull(type, "Type cannot be null") + Objects.requireNonNull(type, "Type cannot be null") ) { @Override public AnnotationMetadata getAnnotationMetadata() { @@ -559,14 +749,15 @@ public Map getTypeArguments() { @Override public List getBoundGenericTypes() { return getDeclaredGenericPlaceholders().stream() - .map(tv -> typeArguments.get(tv.getVariableName())) - .collect(Collectors.toList()); + .map(tv -> typeArguments.get(tv.getVariableName())) + .collect(Collectors.toList()); } }; } /** * Create a class element for the given simple type. + * * @param typeName The type * @return The class element */ @@ -577,8 +768,9 @@ public List getBoundGenericTypes() { /** * Create a class element for the given simple type. - * @param typeName The type - * @param isInterface Is the type an interface + * + * @param typeName The type + * @param isInterface Is the type an interface * @param annotationMetadata The annotation metadata * @return The class element */ @@ -589,10 +781,11 @@ public List getBoundGenericTypes() { /** * Create a class element for the given simple type. - * @param typeName The type - * @param isInterface Is the type an interface + * + * @param typeName The type + * @param isInterface Is the type an interface * @param annotationMetadata The annotation metadata - * @param typeArguments The type arguments + * @param typeArguments The type arguments * @return The class element */ @Internal diff --git a/inject/src/main/java/io/micronaut/inject/ast/DefaultElementQuery.java b/inject/src/main/java/io/micronaut/inject/ast/DefaultElementQuery.java index ad8b8bbacc9..269a0793ac6 100644 --- a/inject/src/main/java/io/micronaut/inject/ast/DefaultElementQuery.java +++ b/inject/src/main/java/io/micronaut/inject/ast/DefaultElementQuery.java @@ -44,30 +44,34 @@ final class DefaultElementQuery implements ElementQuery, E private final List> elementPredicates; private final List> typePredicates; private final boolean onlyInstance; + private final boolean onlyStatic; private final boolean includeEnumConstants; private final boolean includeOverriddenMethods; private final boolean includeHiddenElements; + private final boolean excludePropertyElements; DefaultElementQuery(Class elementType) { - this(elementType, null, false, false, false, false, false, false, false, false, null, null, null, null, null); + this(elementType, null, false, false, false, false, false, false, false, false, false, false, null, null, null, null, null); } @SuppressWarnings("checkstyle:ParameterNumber") DefaultElementQuery( - Class elementType, - ClassElement onlyAccessibleType, - boolean onlyDeclared, - boolean onlyAbstract, - boolean onlyConcrete, - boolean onlyInjected, - boolean onlyInstance, - boolean includeEnumConstants, - boolean includeOverriddenMethods, - boolean includeHiddenElements, - List> annotationPredicates, - List>> modifiersPredicates, - List> elementPredicates, - List> namePredicates, List> typePredicates) { + Class elementType, + ClassElement onlyAccessibleType, + boolean onlyDeclared, + boolean onlyAbstract, + boolean onlyConcrete, + boolean onlyInjected, + boolean onlyInstance, + boolean onlyStatic, + boolean includeEnumConstants, + boolean includeOverriddenMethods, + boolean includeHiddenElements, + boolean excludePropertyElements, + List> annotationPredicates, + List>> modifiersPredicates, + List> elementPredicates, + List> namePredicates, List> typePredicates) { this.elementType = elementType; this.onlyAccessibleType = onlyAccessibleType; this.onlyDeclared = onlyDeclared; @@ -79,9 +83,11 @@ final class DefaultElementQuery implements ElementQuery, E this.modifiersPredicates = modifiersPredicates; this.elementPredicates = elementPredicates; this.onlyInstance = onlyInstance; + this.onlyStatic = onlyStatic; this.includeEnumConstants = includeEnumConstants; this.includeOverriddenMethods = includeOverriddenMethods; this.includeHiddenElements = includeHiddenElements; + this.excludePropertyElements = excludePropertyElements; this.typePredicates = typePredicates; } @@ -128,6 +134,11 @@ public boolean isOnlyInstance() { return onlyInstance; } + @Override + public boolean isOnlyStatic() { + return onlyStatic; + } + @Override public boolean isIncludeEnumConstants() { return includeEnumConstants; @@ -143,6 +154,11 @@ public boolean isIncludeHiddenElements() { return includeHiddenElements; } + @Override + public boolean isExcludePropertyElements() { + return excludePropertyElements; + } + @Override public List> getNamePredicates() { if (namePredicates == null) { @@ -188,164 +204,218 @@ public List> getElementPredicates() { @Override public ElementQuery onlyDeclared() { return new DefaultElementQuery<>( - elementType, onlyAccessibleType, - true, - onlyAbstract, onlyConcrete, - onlyInjected, - onlyInstance, - includeEnumConstants, - includeOverriddenMethods, - includeHiddenElements, - annotationPredicates, modifiersPredicates, elementPredicates, namePredicates, - typePredicates); + elementType, onlyAccessibleType, + true, + onlyAbstract, onlyConcrete, + onlyInjected, + onlyInstance, + onlyStatic, + includeEnumConstants, + includeOverriddenMethods, + includeHiddenElements, + excludePropertyElements, + annotationPredicates, modifiersPredicates, elementPredicates, namePredicates, + typePredicates); } @Override public ElementQuery onlyInjected() { final List> annotationPredicates = this.annotationPredicates != null ? new ArrayList<>(this.annotationPredicates) : new ArrayList<>(1); annotationPredicates.add((metadata) -> - metadata.hasDeclaredAnnotation(AnnotationUtil.INJECT) || + metadata.hasDeclaredAnnotation(AnnotationUtil.INJECT) || (metadata.hasDeclaredStereotype(AnnotationUtil.QUALIFIER) && !metadata.hasDeclaredAnnotation(Bean.class)) || metadata.hasDeclaredAnnotation(AnnotationUtil.PRE_DESTROY) || metadata.hasDeclaredAnnotation(AnnotationUtil.POST_CONSTRUCT)); return new DefaultElementQuery<>( - elementType, onlyAccessibleType, - onlyDeclared, - onlyAbstract, - onlyConcrete, - true, - onlyInstance, - includeEnumConstants, - includeOverriddenMethods, - includeHiddenElements, - annotationPredicates, modifiersPredicates, elementPredicates, namePredicates, - typePredicates); + elementType, onlyAccessibleType, + onlyDeclared, + onlyAbstract, + onlyConcrete, + true, + onlyInstance, + onlyStatic, + includeEnumConstants, + includeOverriddenMethods, + includeHiddenElements, + excludePropertyElements, + annotationPredicates, modifiersPredicates, elementPredicates, namePredicates, + typePredicates); } @NonNull @Override public ElementQuery onlyConcrete() { return new DefaultElementQuery<>( - elementType, onlyAccessibleType, - onlyDeclared, - onlyAbstract, true, - onlyInjected, - onlyInstance, - includeEnumConstants, - includeOverriddenMethods, - includeHiddenElements, - annotationPredicates, modifiersPredicates, elementPredicates, namePredicates, - typePredicates); + elementType, onlyAccessibleType, + onlyDeclared, + onlyAbstract, true, + onlyInjected, + onlyInstance, + onlyStatic, + includeEnumConstants, + includeOverriddenMethods, + includeHiddenElements, + excludePropertyElements, + annotationPredicates, modifiersPredicates, elementPredicates, namePredicates, + typePredicates); } @NonNull @Override public ElementQuery onlyAbstract() { return new DefaultElementQuery<>( - elementType, onlyAccessibleType, - onlyDeclared, - true, onlyConcrete, - onlyInjected, - onlyInstance, - includeEnumConstants, - includeOverriddenMethods, - includeHiddenElements, - annotationPredicates, modifiersPredicates, elementPredicates, namePredicates, - typePredicates); + elementType, onlyAccessibleType, + onlyDeclared, + true, onlyConcrete, + onlyInjected, + onlyInstance, + onlyStatic, + includeEnumConstants, + includeOverriddenMethods, + includeHiddenElements, + excludePropertyElements, + annotationPredicates, modifiersPredicates, elementPredicates, namePredicates, + typePredicates); } @NonNull @Override public ElementQuery onlyAccessible() { return new DefaultElementQuery<>( - elementType, - ONLY_ACCESSIBLE_MARKER, - onlyDeclared, - onlyAbstract, onlyConcrete, - onlyInjected, - onlyInstance, - includeEnumConstants, - includeOverriddenMethods, - includeHiddenElements, - annotationPredicates, modifiersPredicates, elementPredicates, namePredicates, - typePredicates); + elementType, + ONLY_ACCESSIBLE_MARKER, + onlyDeclared, + onlyAbstract, onlyConcrete, + onlyInjected, + onlyInstance, + onlyStatic, + includeEnumConstants, + includeOverriddenMethods, + includeHiddenElements, + excludePropertyElements, + annotationPredicates, modifiersPredicates, elementPredicates, namePredicates, + typePredicates); } @NonNull @Override public ElementQuery onlyAccessible(ClassElement fromType) { return new DefaultElementQuery<>( - elementType, - fromType, - onlyDeclared, - onlyAbstract, onlyConcrete, - onlyInjected, - onlyInstance, - includeEnumConstants, - includeOverriddenMethods, - includeHiddenElements, - annotationPredicates, modifiersPredicates, elementPredicates, namePredicates, - typePredicates); + elementType, + fromType, + onlyDeclared, + onlyAbstract, onlyConcrete, + onlyInjected, + onlyInstance, + onlyStatic, + includeEnumConstants, + includeOverriddenMethods, + includeHiddenElements, + excludePropertyElements, + annotationPredicates, modifiersPredicates, elementPredicates, namePredicates, + typePredicates); } @Override public ElementQuery onlyInstance() { return new DefaultElementQuery<>( - elementType, onlyAccessibleType, - onlyDeclared, - onlyAbstract, onlyConcrete, - onlyInjected, - true, - includeEnumConstants, - includeOverriddenMethods, - includeHiddenElements, - annotationPredicates, modifiersPredicates, elementPredicates, namePredicates, - typePredicates); + elementType, onlyAccessibleType, + onlyDeclared, + onlyAbstract, onlyConcrete, + onlyInjected, + true, + onlyStatic, + includeEnumConstants, + includeOverriddenMethods, + includeHiddenElements, + excludePropertyElements, + annotationPredicates, modifiersPredicates, elementPredicates, namePredicates, + typePredicates); + } + + @Override + public ElementQuery onlyStatic() { + return new DefaultElementQuery<>( + elementType, onlyAccessibleType, + onlyDeclared, + onlyAbstract, onlyConcrete, + onlyInjected, + onlyInstance, + true, + includeEnumConstants, + includeOverriddenMethods, + includeHiddenElements, + excludePropertyElements, + annotationPredicates, modifiersPredicates, elementPredicates, namePredicates, + typePredicates); } @Override public ElementQuery includeEnumConstants() { return new DefaultElementQuery<>( - elementType, onlyAccessibleType, - onlyDeclared, - onlyAbstract, onlyConcrete, - onlyInjected, - onlyInstance, - true, - includeOverriddenMethods, - includeHiddenElements, - annotationPredicates, modifiersPredicates, elementPredicates, namePredicates, - typePredicates); + elementType, onlyAccessibleType, + onlyDeclared, + onlyAbstract, onlyConcrete, + onlyInjected, + onlyInstance, + onlyStatic, + true, + includeOverriddenMethods, + includeHiddenElements, + excludePropertyElements, + annotationPredicates, modifiersPredicates, elementPredicates, namePredicates, + typePredicates); } @Override public ElementQuery includeOverriddenMethods() { return new DefaultElementQuery<>( - elementType, onlyAccessibleType, - onlyDeclared, - onlyAbstract, onlyConcrete, - onlyInjected, - onlyInstance, - includeEnumConstants, - true, - includeHiddenElements, - annotationPredicates, modifiersPredicates, elementPredicates, namePredicates, - typePredicates); + elementType, onlyAccessibleType, + onlyDeclared, + onlyAbstract, onlyConcrete, + onlyInjected, + onlyInstance, + onlyStatic, + includeEnumConstants, + true, + includeHiddenElements, + excludePropertyElements, + annotationPredicates, modifiersPredicates, elementPredicates, namePredicates, + typePredicates); } @Override public ElementQuery includeHiddenElements() { return new DefaultElementQuery<>( - elementType, onlyAccessibleType, - onlyDeclared, - onlyAbstract, onlyConcrete, - onlyInjected, - onlyInstance, - includeEnumConstants, - includeOverriddenMethods, - true, - annotationPredicates, modifiersPredicates, elementPredicates, namePredicates, - typePredicates); + elementType, onlyAccessibleType, + onlyDeclared, + onlyAbstract, onlyConcrete, + onlyInjected, + onlyInstance, + onlyStatic, + includeEnumConstants, + includeOverriddenMethods, + true, + excludePropertyElements, + annotationPredicates, modifiersPredicates, elementPredicates, namePredicates, + typePredicates); + } + + @Override + public ElementQuery excludePropertyElements() { + return new DefaultElementQuery<>( + elementType, onlyAccessibleType, + onlyDeclared, + onlyAbstract, onlyConcrete, + onlyInjected, + onlyInstance, + onlyStatic, + includeEnumConstants, + includeOverriddenMethods, + includeHiddenElements, + true, + annotationPredicates, modifiersPredicates, elementPredicates, namePredicates, + typePredicates); } @NonNull @@ -360,21 +430,23 @@ public ElementQuery named(@NonNull Predicate predicate) { namePredicates = Collections.singletonList(predicate); } return new DefaultElementQuery<>( - elementType, - onlyAccessibleType, - onlyDeclared, - onlyAbstract, - onlyConcrete, - onlyInjected, - onlyInstance, - includeEnumConstants, - includeOverriddenMethods, - includeHiddenElements, - annotationPredicates, - modifiersPredicates, - elementPredicates, - namePredicates, - typePredicates); + elementType, + onlyAccessibleType, + onlyDeclared, + onlyAbstract, + onlyConcrete, + onlyInjected, + onlyInstance, + onlyStatic, + includeEnumConstants, + includeOverriddenMethods, + includeHiddenElements, + excludePropertyElements, + annotationPredicates, + modifiersPredicates, + elementPredicates, + namePredicates, + typePredicates); } @NonNull @@ -389,21 +461,23 @@ public ElementQuery typed(@NonNull Predicate predicate) { typePredicates = Collections.singletonList(predicate); } return new DefaultElementQuery<>( - elementType, - onlyAccessibleType, - onlyDeclared, - onlyAbstract, - onlyConcrete, - onlyInjected, - onlyInstance, - includeEnumConstants, - includeOverriddenMethods, - includeHiddenElements, - annotationPredicates, - modifiersPredicates, - elementPredicates, - namePredicates, - typePredicates); + elementType, + onlyAccessibleType, + onlyDeclared, + onlyAbstract, + onlyConcrete, + onlyInjected, + onlyInstance, + onlyStatic, + includeEnumConstants, + includeOverriddenMethods, + includeHiddenElements, + excludePropertyElements, + annotationPredicates, + modifiersPredicates, + elementPredicates, + namePredicates, + typePredicates); } @NonNull @@ -418,16 +492,18 @@ public ElementQuery annotated(@NonNull Predicate predicat annotationPredicates = Collections.singletonList(predicate); } return new DefaultElementQuery<>( - elementType, onlyAccessibleType, - onlyDeclared, - onlyAbstract, onlyConcrete, - onlyInjected, - onlyInstance, - includeEnumConstants, - includeOverriddenMethods, - includeHiddenElements, - annotationPredicates, modifiersPredicates, elementPredicates, namePredicates, - typePredicates); + elementType, onlyAccessibleType, + onlyDeclared, + onlyAbstract, onlyConcrete, + onlyInjected, + onlyInstance, + onlyStatic, + includeEnumConstants, + includeOverriddenMethods, + includeHiddenElements, + excludePropertyElements, + annotationPredicates, modifiersPredicates, elementPredicates, namePredicates, + typePredicates); } @NonNull @@ -442,15 +518,17 @@ public ElementQuery modifiers(@NonNull Predicate> predic modifierPredicates = Collections.singletonList(predicate); } return new DefaultElementQuery<>( - elementType, onlyAccessibleType, - onlyDeclared, onlyAbstract, onlyConcrete, - onlyInjected, - onlyInstance, - includeEnumConstants, - includeOverriddenMethods, - includeHiddenElements, - annotationPredicates, modifierPredicates, elementPredicates, namePredicates, - typePredicates); + elementType, onlyAccessibleType, + onlyDeclared, onlyAbstract, onlyConcrete, + onlyInjected, + onlyInstance, + onlyStatic, + includeEnumConstants, + includeOverriddenMethods, + includeHiddenElements, + excludePropertyElements, + annotationPredicates, modifierPredicates, elementPredicates, namePredicates, + typePredicates); } @NonNull @@ -465,14 +543,17 @@ public ElementQuery filter(@NonNull Predicate predicate) { elementPredicates = Collections.singletonList(predicate); } return new DefaultElementQuery<>( - elementType, onlyAccessibleType, - onlyDeclared, onlyAbstract, onlyConcrete, - onlyInjected, - onlyInstance, includeEnumConstants, - includeOverriddenMethods, - includeHiddenElements, - annotationPredicates, modifiersPredicates, elementPredicates, namePredicates, - typePredicates); + elementType, onlyAccessibleType, + onlyDeclared, onlyAbstract, onlyConcrete, + onlyInjected, + onlyInstance, + onlyStatic, + includeEnumConstants, + includeOverriddenMethods, + includeHiddenElements, + excludePropertyElements, + annotationPredicates, modifiersPredicates, elementPredicates, namePredicates, + typePredicates); } @NonNull diff --git a/inject/src/main/java/io/micronaut/inject/ast/Element.java b/inject/src/main/java/io/micronaut/inject/ast/Element.java index c776e04e9e8..4646221626e 100644 --- a/inject/src/main/java/io/micronaut/inject/ast/Element.java +++ b/inject/src/main/java/io/micronaut/inject/ast/Element.java @@ -16,20 +16,13 @@ package io.micronaut.inject.ast; import io.micronaut.core.annotation.AnnotatedElement; -import io.micronaut.core.annotation.AnnotationMetadataDelegate; -import io.micronaut.core.annotation.AnnotationValue; -import io.micronaut.core.annotation.AnnotationValueBuilder; +import io.micronaut.core.annotation.AnnotationMetadata; +import io.micronaut.core.annotation.NonNull; import io.micronaut.core.naming.Described; -import io.micronaut.core.util.ArgumentUtils; -import io.micronaut.core.annotation.NonNull; -import java.lang.annotation.Annotation; import java.util.Collections; -import java.util.Objects; import java.util.Optional; import java.util.Set; -import java.util.function.Consumer; -import java.util.function.Predicate; /** * Stores data about a compile time element. The underlying object can be a class, field, or method. @@ -38,7 +31,8 @@ * @author graemerocher * @since 1.0 */ -public interface Element extends AnnotationMetadataDelegate, AnnotatedElement, Described { +public interface Element extends ElementMutableAnnotationMetadata, AnnotatedElement, Described { + /** * An empty array of elements. * @since 2.1.1 @@ -59,6 +53,16 @@ default boolean isPackagePrivate() { return false; } + /** + * Checks if the current element is synthetic - not user defined but created by the compiler. + * + * @return True if the element is synthetic. + * @since 4.0.0 + */ + default boolean isSynthetic() { + return false; + } + /** * @return True if the element is protected. */ @@ -85,136 +89,6 @@ default Set getModifiers() { return Collections.emptySet(); } - /** - * Annotate this element with the given annotation type. If the annotation is already present then - * any values populated by the builder will be merged/overridden with the existing values. - * - * @param annotationType The annotation type - * @param consumer A function that receives the {@link AnnotationValueBuilder} - * @param The annotation generic type - * @return This element - */ - @NonNull - default Element annotate(@NonNull String annotationType, @NonNull Consumer> consumer) { - throw new UnsupportedOperationException("Element of type [" + getClass() + "] does not support adding annotations at compilation time"); - } - - /** - * Removes an annotation of the given type from the element. - * - *

If the annotation features any stereotypes these will also be removed unless there are other - * annotations that reference the stereotype to be removed.

- * - *

In the case of repeatable annotations this method will remove all repeated annotations, effectively - * clearing out all declared repeated annotations of the given type.

- * - * @param annotationType The annotation type - * @return This element - * @since 3.0.0 - */ - default Element removeAnnotation(@NonNull String annotationType) { - throw new UnsupportedOperationException("Element of type [" + getClass() + "] does not support removing annotations at compilation time"); - } - - /** - * @see #removeAnnotation(String) - * @param annotationType The annotation type - * @param The annotation generic type - * @return This element - * @since 3.0.0 - */ - default Element removeAnnotation(@NonNull Class annotationType) { - return removeAnnotation(Objects.requireNonNull(annotationType).getName()); - } - - /** - * Removes all annotations that pass the given predicate. - * @param predicate The predicate - * @param The annotation generic type - * @return This element - * @since 3.0.0 - */ - default Element removeAnnotationIf(@NonNull Predicate> predicate) { - throw new UnsupportedOperationException("Element of type [" + getClass() + "] does not support removing annotations at compilation time"); - } - - /** - * Removes a stereotype of the given name from the element. - * @param annotationType The annotation type - * @return This element - * @since 3.0.0 - */ - default Element removeStereotype(@NonNull String annotationType) { - throw new UnsupportedOperationException("Element of type [" + getClass() + "] does not support removing annotations at compilation time"); - } - - /** - * Removes a stereotype annotation of the given type from the element. - * @param annotationType The annotation type - * @param The annotation generic type - * @return This element - * @since 3.0.0 - */ - default Element removeStereotype(@NonNull Class annotationType) { - return removeStereotype(Objects.requireNonNull(annotationType).getName()); - } - - /** - * Annotate this element with the given annotation type. If the annotation is already present then - * any values populated by the builder will be merged/overridden with the existing values. - * - * @param annotationType The annotation type - * @return This element - */ - @NonNull - default Element annotate(@NonNull String annotationType) { - return annotate(annotationType, annotationValueBuilder -> { }); - } - - /** - * Annotate this element with the given annotation type. If the annotation is already present then - * any values populated by the builder will be merged/overridden with the existing values. - * - * @param annotationType The annotation type - * @param consumer A function that receives the {@link AnnotationValueBuilder} - * @param The annotation generic type - * @return This element - */ - @NonNull - default Element annotate(@NonNull Class annotationType, @NonNull Consumer> consumer) { - ArgumentUtils.requireNonNull("annotationType", annotationType); - ArgumentUtils.requireNonNull("consumer", consumer); - return annotate(annotationType.getName(), consumer); - } - - /** - * Annotate this element with the given annotation type. If the annotation is already present then - * any values populated by the builder will be merged/overridden with the existing values. - * - * @param annotationType The annotation type - * @param The annotation generic type - * @return This element - */ - @NonNull - default Element annotate(@NonNull Class annotationType) { - ArgumentUtils.requireNonNull("annotationType", annotationType); - return annotate(annotationType.getName(), annotationValueBuilder -> { }); - } - - /** - * Annotate this element with the given annotation type. If the annotation is already present then - * any values populated by the builder will be merged/overridden with the existing values. - * - * @param annotationValue The annotation type - * @param The annotation generic type - * @return This element - * @since 3.0.0 - */ - @NonNull - default Element annotate(@NonNull AnnotationValue annotationValue) { - throw new UnsupportedOperationException("Element of type [" + getClass() + "] does not support adding annotations at compilation time"); - } - /** * The simple name of the element. For a class this will be the name without the package. * @@ -274,4 +148,15 @@ default String getDescription(boolean simple) { return getName(); } } + + /** + * Copies this element and overrides its annotations. + * + * @param annotationMetadata The annotation metadata + * @return A new element + * @since 4.0.0 + */ + default Element withAnnotationMetadata(AnnotationMetadata annotationMetadata) { + throw new UnsupportedOperationException("Element of type [" + getClass() + "] does not support copy constructor"); + } } diff --git a/validation/src/main/java/io/micronaut/validation/executable/package-info.java b/inject/src/main/java/io/micronaut/inject/ast/ElementAnnotationMetadata.java similarity index 64% rename from validation/src/main/java/io/micronaut/validation/executable/package-info.java rename to inject/src/main/java/io/micronaut/inject/ast/ElementAnnotationMetadata.java index 603b427a09a..0a199a3b7d7 100644 --- a/validation/src/main/java/io/micronaut/validation/executable/package-info.java +++ b/inject/src/main/java/io/micronaut/inject/ast/ElementAnnotationMetadata.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2020 original authors + * Copyright 2017-2022 original authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,7 +13,15 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +package io.micronaut.inject.ast; + +import io.micronaut.core.annotation.AnnotationMetadata; + /** - * Package to organize classes responsible for validating executable methods at compile time. + * Element's annotation metadata. + * + * @author Denis Stepanov + * @since 4.0.0 */ -package io.micronaut.validation.executable; +public interface ElementAnnotationMetadata extends ElementMutableAnnotationMetadata { +} diff --git a/inject/src/main/java/io/micronaut/inject/ast/ElementAnnotationMetadataFactory.java b/inject/src/main/java/io/micronaut/inject/ast/ElementAnnotationMetadataFactory.java new file mode 100644 index 00000000000..b966ea4b941 --- /dev/null +++ b/inject/src/main/java/io/micronaut/inject/ast/ElementAnnotationMetadataFactory.java @@ -0,0 +1,99 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.inject.ast; + +import io.micronaut.core.annotation.AnnotationMetadata; +import io.micronaut.core.annotation.Experimental; +import io.micronaut.core.annotation.NonNull; + +import java.util.function.Function; + +/** + * Element's annotation metadata factory. + * + * @author Denis Stepanov + * @since 4.0.0 + */ +public interface ElementAnnotationMetadataFactory { + + /** + * Build new element annotation metadata from the element. + * + * @param element The element + * @return the element's metadata + */ + @NonNull + ElementAnnotationMetadata build(@NonNull Element element); + + /** + * Build new element annotation metadata from the element with preloaded annotations. + * This method will avoid fetching default annotation metadata from cache. + * + * @param element The element + * @param annotationMetadata The preloaded annotation + * @return the element's metadata + */ + @NonNull + ElementAnnotationMetadata build(@NonNull Element element, @NonNull AnnotationMetadata annotationMetadata); + + /** + * Makes this factory read-only. No modification to the annotation metadata should be persisted into the shared cache. + * + * @return read-only factory + */ + @NonNull + ElementAnnotationMetadataFactory readOnly(); + + /** + * Creates a factory wrapper that would override the annotation metadata value for the provided native type. + * @param nativeType The native type + * @param fn The function to build the annotation metadata + * @return a new factory + */ + @Experimental + @NonNull + default ElementAnnotationMetadataFactory overrideForNativeType(Object nativeType, + Function fn) { + ElementAnnotationMetadataFactory thisFactory = this; + return new ElementAnnotationMetadataFactory() { + + private boolean fetched; + + @Override + public ElementAnnotationMetadata build(Element element) { + if (!fetched && element.getNativeType().equals(nativeType)) { + fetched = true; + return fn.apply(element); + } + return thisFactory.build(element); + } + + @Override + public ElementAnnotationMetadata build(Element element, AnnotationMetadata annotationMetadata) { + if (!fetched && element.getNativeType().equals(nativeType)) { + fetched = true; + return fn.apply(element); + } + return thisFactory.build(element, annotationMetadata); + } + + @Override + public ElementAnnotationMetadataFactory readOnly() { + throw new IllegalStateException("Not supported!"); + } + }; + } +} diff --git a/inject/src/main/java/io/micronaut/inject/ast/ElementFactory.java b/inject/src/main/java/io/micronaut/inject/ast/ElementFactory.java index b9dd5fd16c2..e289cf43811 100644 --- a/inject/src/main/java/io/micronaut/inject/ast/ElementFactory.java +++ b/inject/src/main/java/io/micronaut/inject/ast/ElementFactory.java @@ -16,7 +16,6 @@ package io.micronaut.inject.ast; import io.micronaut.core.annotation.NonNull; -import io.micronaut.core.annotation.AnnotationMetadata; import java.util.Map; @@ -31,135 +30,115 @@ * @since 2.3.0 */ public interface ElementFactory { + /** * Builds a new class element for the given type. * - * @param type The type - * @param annotationMetadata The resolved annotation metadata + * @param type The type + * @param annotationMetadataFactory The element annotation metadata factory * @return The class element + * @since 4.0.0 */ @NonNull - ClassElement newClassElement( - @NonNull C type, - @NonNull AnnotationMetadata annotationMetadata); + ClassElement newClassElement(@NonNull C type, ElementAnnotationMetadataFactory annotationMetadataFactory); /** * Builds a new class element for the given type. * - * @param type The type - * @param annotationMetadata The resolved annotation metadata - * @param resolvedGenerics The resolved generics + * @param type The type + * @param annotationMetadataFactory The element annotation metadata factory + * @param resolvedGenerics The resolved generics * @return The class element - * @since 3.0.0 + * @since 4.0.0 */ @NonNull - ClassElement newClassElement( - @NonNull C type, - @NonNull AnnotationMetadata annotationMetadata, - @NonNull Map resolvedGenerics); + ClassElement newClassElement(@NonNull C type, + @NonNull ElementAnnotationMetadataFactory annotationMetadataFactory, + @NonNull Map resolvedGenerics); /** * Builds a new source class element for the given type. This method - * differs from {@link #newClassElement(Object, AnnotationMetadata)} in that + * differs from {@link #newClassElement(Object, ElementAnnotationMetadataFactory)} in that * it should only be called from elements that are known to originate from source code. * - * @param type The type - * @param annotationMetadata The resolved annotation metadata + * @param type The type + * @param elementAnnotationMetadataFactory The element annotation metadata factory * @return The class element - * @since 3.0.0 + * @since 4.0.0 */ @NonNull - ClassElement newSourceClassElement( - @NonNull C type, - @NonNull AnnotationMetadata annotationMetadata); + ClassElement newSourceClassElement(@NonNull C type, @NonNull ElementAnnotationMetadataFactory elementAnnotationMetadataFactory); /** * Builds a new source method element for the given method. This method - * differs from {@link #newMethodElement(ClassElement, Object, AnnotationMetadata)} in that + * differs from {@link #newMethodElement(ClassElement, Object, ElementAnnotationMetadataFactory)} in that * it should only be called from elements that are known to originate from source code. * - * @param declaringClass The declaring class - * @param method The method - * @param annotationMetadata The resolved annotation metadata + * @param owningClass The owning class + * @param method The method + * @param elementAnnotationMetadataFactory The element annotation metadata factory * @return The class element - * @since 3.0.0 + * @since 4.0.0 */ @NonNull - MethodElement newSourceMethodElement( - ClassElement declaringClass, - @NonNull M method, - @NonNull AnnotationMetadata annotationMetadata); + MethodElement newSourceMethodElement(@NonNull ClassElement owningClass, + @NonNull M method, + @NonNull ElementAnnotationMetadataFactory elementAnnotationMetadataFactory); /** * Builds a new method element for the given type. * - * @param declaringClass The declaring class - * @param method The method - * @param annotationMetadata The resolved annotation metadata + * @param owningClass The owning class + * @param method The method + * @param elementAnnotationMetadataFactory The element annotation metadata factory * @return The method element + * @since 4.0.0 */ @NonNull - MethodElement newMethodElement( - ClassElement declaringClass, - @NonNull M method, - @NonNull AnnotationMetadata annotationMetadata); - + MethodElement newMethodElement(@NonNull ClassElement owningClass, + @NonNull M method, + @NonNull ElementAnnotationMetadataFactory elementAnnotationMetadataFactory); /** * Builds a new constructor element for the given type. * - * @param declaringClass The declaring class - * @param constructor The constructor - * @param annotationMetadata The resolved annotation metadata + * @param owningClass The owning class + * @param constructor The constructor + * @param elementAnnotationMetadataFactory The element annotation metadata factory * @return The constructor element + * @since 4.0.0 */ @NonNull - ConstructorElement newConstructorElement( - ClassElement declaringClass, - @NonNull M constructor, - @NonNull AnnotationMetadata annotationMetadata); + ConstructorElement newConstructorElement(@NonNull ClassElement owningClass, + @NonNull M constructor, + @NonNull ElementAnnotationMetadataFactory elementAnnotationMetadataFactory); /** * Builds a new enum constant element for the given type. * - * @param declaringClass The declaring class - * @param enumConstant The enum constant - * @param annotationMetadata The resolved annotation metadata - * + * @param owningClass The owning class + * @param enumConstant The enum constant + * @param elementAnnotationMetadataFactory The element annotation metadata factory * @return The enum constant element - * - * @since 3.6.0 + * @since 4.0.0 */ @NonNull - EnumConstantElement newEnumConstantElement( - ClassElement declaringClass, - @NonNull F enumConstant, - @NonNull AnnotationMetadata annotationMetadata); + EnumConstantElement newEnumConstantElement(@NonNull ClassElement owningClass, + @NonNull F enumConstant, + @NonNull ElementAnnotationMetadataFactory elementAnnotationMetadataFactory); /** * Builds a new field element for the given type. * - * @param declaringClass The declaring class - * @param field The field - * @param annotationMetadata The resolved annotation metadata - * @return The field element - */ - @NonNull - FieldElement newFieldElement( - ClassElement declaringClass, - @NonNull F field, - @NonNull AnnotationMetadata annotationMetadata); - - /** - * Builds a new field element for the given field. - * - * @param field The field - * @param annotationMetadata The resolved annotation metadata + * @param owningClass The owning class + * @param field The field + * @param elementAnnotationMetadataFactory The element annotation metadata factory * @return The field element + * @since 4.0.0 */ @NonNull - FieldElement newFieldElement( - @NonNull F field, - @NonNull AnnotationMetadata annotationMetadata); + FieldElement newFieldElement(@NonNull ClassElement owningClass, + @NonNull F field, + @NonNull ElementAnnotationMetadataFactory elementAnnotationMetadataFactory); } diff --git a/inject/src/main/java/io/micronaut/inject/ast/ElementMutableAnnotationMetadata.java b/inject/src/main/java/io/micronaut/inject/ast/ElementMutableAnnotationMetadata.java new file mode 100644 index 00000000000..674af46ad98 --- /dev/null +++ b/inject/src/main/java/io/micronaut/inject/ast/ElementMutableAnnotationMetadata.java @@ -0,0 +1,168 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.inject.ast; + +import io.micronaut.core.annotation.AnnotationMetadataDelegate; +import io.micronaut.core.annotation.AnnotationValue; +import io.micronaut.core.annotation.AnnotationValueBuilder; +import io.micronaut.core.annotation.NonNull; +import io.micronaut.core.util.ArgumentUtils; + +import java.lang.annotation.Annotation; +import java.util.Objects; +import java.util.function.Consumer; +import java.util.function.Predicate; + +/** + * Mutable annotation metadata. + * + * @param The return type + * @author Denis Stepanov + * @since 4.0.0 + */ +public interface ElementMutableAnnotationMetadata extends AnnotationMetadataDelegate { + + /** + * Annotate this element with the given annotation type. If the annotation is already present then + * any values populated by the builder will be merged/overridden with the existing values. + * + * @param annotationType The annotation type + * @param consumer A function that receives the {@link AnnotationValueBuilder} + * @param The annotation generic type + * @return This element + */ + @NonNull + default Rtr annotate(@NonNull String annotationType, @NonNull Consumer> consumer) { + throw new UnsupportedOperationException("Element of type [" + getClass() + "] does not support adding annotations at compilation time"); + } + + /** + * Removes an annotation of the given type from the element. + * + *

If the annotation features any stereotypes these will also be removed unless there are other + * annotations that reference the stereotype to be removed.

+ * + *

In the case of repeatable annotations this method will remove all repeated annotations, effectively + * clearing out all declared repeated annotations of the given type.

+ * + * @param annotationType The annotation type + * @return This element + */ + @NonNull + default Rtr removeAnnotation(@NonNull String annotationType) { + throw new UnsupportedOperationException("Element of type [" + getClass() + "] does not support removing annotations at compilation time"); + } + + /** + * @see #removeAnnotation(String) + * @param annotationType The annotation type + * @param The annotation generic type + * @return This element + */ + @NonNull + default Rtr removeAnnotation(@NonNull Class annotationType) { + return removeAnnotation(Objects.requireNonNull(annotationType).getName()); + } + + /** + * Removes all annotations that pass the given predicate. + * @param predicate The predicate + * @param The annotation generic type + * @return This element + */ + @NonNull + default Rtr removeAnnotationIf(@NonNull Predicate> predicate) { + throw new UnsupportedOperationException("Element of type [" + getClass() + "] does not support removing annotations at compilation time"); + } + + /** + * Removes a stereotype of the given name from the element. + * @param annotationType The annotation type + * @return This element + */ + @NonNull + default Rtr removeStereotype(@NonNull String annotationType) { + throw new UnsupportedOperationException("Element of type [" + getClass() + "] does not support removing annotations at compilation time"); + } + + /** + * Removes a stereotype annotation of the given type from the element. + * @param annotationType The annotation type + * @param The annotation generic type + * @return This element + */ + @NonNull + default Rtr removeStereotype(@NonNull Class annotationType) { + return removeStereotype(Objects.requireNonNull(annotationType).getName()); + } + + /** + * Annotate this element with the given annotation type. If the annotation is already present then + * any values populated by the builder will be merged/overridden with the existing values. + * + * @param annotationType The annotation type + * @return This element + */ + @NonNull + default Rtr annotate(@NonNull String annotationType) { + return annotate(annotationType, annotationValueBuilder -> { }); + } + + /** + * Annotate this element with the given annotation type. If the annotation is already present then + * any values populated by the builder will be merged/overridden with the existing values. + * + * @param annotationType The annotation type + * @param consumer A function that receives the {@link AnnotationValueBuilder} + * @param The annotation generic type + * @return This element + */ + @NonNull + default Rtr annotate(@NonNull Class annotationType, @NonNull Consumer> consumer) { + ArgumentUtils.requireNonNull("annotationType", annotationType); + ArgumentUtils.requireNonNull("consumer", consumer); + return annotate(annotationType.getName(), consumer); + } + + /** + * Annotate this element with the given annotation type. If the annotation is already present then + * any values populated by the builder will be merged/overridden with the existing values. + * + * @param annotationType The annotation type + * @param The annotation generic type + * @return This element + */ + @NonNull + default Rtr annotate(@NonNull Class annotationType) { + ArgumentUtils.requireNonNull("annotationType", annotationType); + return annotate(annotationType.getName(), annotationValueBuilder -> { }); + } + + /** + * Annotate this element with the given annotation type. If the annotation is already present then + * any values populated by the builder will be merged/overridden with the existing values. + * + * @param annotationValue The annotation type + * @param The annotation generic type + * @return This element + * @since 3.0.0 + */ + @NonNull + default Rtr annotate(@NonNull AnnotationValue annotationValue) { + throw new UnsupportedOperationException("Element of type [" + getClass() + "] does not support adding annotations at compilation time"); + } + +} diff --git a/inject/src/main/java/io/micronaut/inject/ast/ElementMutableAnnotationMetadataDelegate.java b/inject/src/main/java/io/micronaut/inject/ast/ElementMutableAnnotationMetadataDelegate.java new file mode 100644 index 00000000000..1ad9e0b65ba --- /dev/null +++ b/inject/src/main/java/io/micronaut/inject/ast/ElementMutableAnnotationMetadataDelegate.java @@ -0,0 +1,116 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.inject.ast; + +import io.micronaut.core.annotation.AnnotationValue; +import io.micronaut.core.annotation.AnnotationValueBuilder; +import io.micronaut.core.annotation.NonNull; + +import java.lang.annotation.Annotation; +import java.util.function.Consumer; +import java.util.function.Predicate; + +/** + * Mutable annotation metadata provider. + * + * @param The return type + * @author Denis Stepanov + * @since 4.0.0 + */ +public interface ElementMutableAnnotationMetadataDelegate extends ElementMutableAnnotationMetadata { + + /** + * Provides the return type instance. + * + * @return the return instance + */ + Rtr getReturnInstance(); + + @Override + @NonNull + ElementMutableAnnotationMetadata getAnnotationMetadata(); + + @Override + @NonNull + default Rtr annotate(@NonNull String annotationType, @NonNull Consumer> consumer) { + getAnnotationMetadata().annotate(annotationType, consumer); + return getReturnInstance(); + } + + @Override + @NonNull + default Rtr removeAnnotation(@NonNull String annotationType) { + getAnnotationMetadata().removeAnnotation(annotationType); + return getReturnInstance(); + } + + @Override + @NonNull + default Rtr removeAnnotation(@NonNull Class annotationType) { + getAnnotationMetadata().removeAnnotation(annotationType); + return getReturnInstance(); + } + + @Override + @NonNull + default Rtr removeAnnotationIf(@NonNull Predicate> predicate) { + getAnnotationMetadata().removeAnnotationIf(predicate); + return getReturnInstance(); + } + + @Override + @NonNull + default Rtr removeStereotype(@NonNull String annotationType) { + getAnnotationMetadata().removeStereotype(annotationType); + return getReturnInstance(); + } + + @Override + @NonNull + default Rtr removeStereotype(@NonNull Class annotationType) { + getAnnotationMetadata().removeStereotype(annotationType); + return getReturnInstance(); + } + + @Override + @NonNull + default Rtr annotate(@NonNull String annotationType) { + getAnnotationMetadata().annotate(annotationType); + return getReturnInstance(); + } + + @Override + @NonNull + default Rtr annotate(@NonNull Class annotationType, @NonNull Consumer> consumer) { + getAnnotationMetadata().annotate(annotationType, consumer); + return getReturnInstance(); + } + + @Override + @NonNull + default Rtr annotate(@NonNull Class annotationType) { + getAnnotationMetadata().annotate(annotationType); + return getReturnInstance(); + } + + @Override + @NonNull + default Rtr annotate(@NonNull AnnotationValue annotationValue) { + getAnnotationMetadata().annotate(annotationValue); + return getReturnInstance(); + } + +} diff --git a/inject/src/main/java/io/micronaut/inject/ast/ElementQuery.java b/inject/src/main/java/io/micronaut/inject/ast/ElementQuery.java index 7a424f0896e..18c44ec65d2 100644 --- a/inject/src/main/java/io/micronaut/inject/ast/ElementQuery.java +++ b/inject/src/main/java/io/micronaut/inject/ast/ElementQuery.java @@ -15,6 +15,7 @@ */ package io.micronaut.inject.ast; +import io.micronaut.core.annotation.Experimental; import io.micronaut.core.annotation.NonNull; import io.micronaut.core.annotation.AnnotationMetadata; @@ -116,6 +117,21 @@ public interface ElementQuery { */ ElementQuery onlyInstance(); + /** + * Indicates to return only static methods/fields. + * @return The query + * @since 4.0.0 + */ + ElementQuery onlyStatic(); + + /** + * Indicates to exclude any property elements (read write methods and a field). + * @return The query + * @since 4.0.0 + */ + @Experimental + ElementQuery excludePropertyElements(); + /** * Indicates to include enum constants, only applicable for fields query. * @since 3.4.0 @@ -248,6 +264,12 @@ interface Result { */ boolean isOnlyInstance(); + /** + * @return Whether to return only static methods / fields + * @since 4.0.0 + */ + boolean isOnlyStatic(); + /** * @return Whether to include enum constants * @since 3.4.0 @@ -266,6 +288,12 @@ interface Result { */ boolean isIncludeHiddenElements(); + /** + * @return Whether to exclude property elements + * @since 4.0.0 + */ + boolean isExcludePropertyElements(); + /** * @return The name predicates */ diff --git a/inject/src/main/java/io/micronaut/inject/ast/FieldElement.java b/inject/src/main/java/io/micronaut/inject/ast/FieldElement.java index 2afd25c3e3a..28b244291af 100644 --- a/inject/src/main/java/io/micronaut/inject/ast/FieldElement.java +++ b/inject/src/main/java/io/micronaut/inject/ast/FieldElement.java @@ -15,6 +15,7 @@ */ package io.micronaut.inject.ast; +import io.micronaut.core.annotation.AnnotationMetadata; import io.micronaut.core.annotation.NonNull; /** @@ -41,4 +42,9 @@ default String getDescription(boolean simple) { return getType().getName() + " " + getName(); } } + + @Override + default FieldElement withAnnotationMetadata(AnnotationMetadata annotationMetadata) { + return (FieldElement) MemberElement.super.withAnnotationMetadata(annotationMetadata); + } } diff --git a/inject/src/main/java/io/micronaut/inject/ast/MemberElement.java b/inject/src/main/java/io/micronaut/inject/ast/MemberElement.java index 5249c38785c..bede1e68a17 100644 --- a/inject/src/main/java/io/micronaut/inject/ast/MemberElement.java +++ b/inject/src/main/java/io/micronaut/inject/ast/MemberElement.java @@ -15,6 +15,7 @@ */ package io.micronaut.inject.ast; +import io.micronaut.core.annotation.AnnotationMetadata; import io.micronaut.core.annotation.NonNull; import io.micronaut.core.annotation.ReflectiveAccess; @@ -116,6 +117,10 @@ default boolean isAccessible() { */ default boolean isAccessible(@NonNull ClassElement callingType) { return !isReflectionRequired(callingType) || hasAnnotation(ReflectiveAccess.class); + } + @Override + default MemberElement withAnnotationMetadata(AnnotationMetadata annotationMetadata) { + return (MemberElement) Element.super.withAnnotationMetadata(annotationMetadata); } } diff --git a/inject/src/main/java/io/micronaut/inject/ast/MethodElement.java b/inject/src/main/java/io/micronaut/inject/ast/MethodElement.java index daaa61b7237..a4dda85200c 100644 --- a/inject/src/main/java/io/micronaut/inject/ast/MethodElement.java +++ b/inject/src/main/java/io/micronaut/inject/ast/MethodElement.java @@ -15,15 +15,24 @@ */ package io.micronaut.inject.ast; -import io.micronaut.core.annotation.NonNull; import io.micronaut.core.annotation.AnnotationMetadata; +import io.micronaut.core.annotation.AnnotationMetadataProvider; +import io.micronaut.core.annotation.AnnotationValue; +import io.micronaut.core.annotation.AnnotationValueBuilder; +import io.micronaut.core.annotation.NonNull; +import io.micronaut.core.annotation.Nullable; +import io.micronaut.core.util.ArgumentUtils; import io.micronaut.core.util.ArrayUtils; +import io.micronaut.inject.annotation.AbstractAnnotationMetadataBuilder; import io.micronaut.inject.ast.beans.BeanElementBuilder; +import java.lang.annotation.Annotation; import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Optional; +import java.util.function.Consumer; +import java.util.function.Predicate; import java.util.stream.Collectors; /** @@ -75,21 +84,49 @@ default ClassElement[] getThrownTypes() { /** * @return The method parameters */ - @NonNull ParameterElement[] getParameters(); + @NonNull + ParameterElement[] getParameters(); /** * Takes this method element and transforms into a new method element with the given parameters appended to the existing parameters. + * * @param newParameters The new parameters * @return A new method element * @since 2.3.0 */ - @NonNull MethodElement withNewParameters(@NonNull ParameterElement...newParameters); + @NonNull + default MethodElement withNewParameters(@NonNull ParameterElement... newParameters) { + return withParameters(ArrayUtils.concat(getParameters(), newParameters)); + } + + /** + * Takes this method element and transforms into a new method element with the given parameters. + * + * @param newParameters The new parameters + * @return A new method element + * @since 4.0.0 + */ + @NonNull + MethodElement withParameters(@NonNull ParameterElement... newParameters); + + /** + * Returns a new method with a new owning type. + * + * @param owningType The owning type. + * @return A new method element + * @since 4.0.0 + */ + @NonNull + default MethodElement withNewOwningType(@NonNull ClassElement owningType) { + throw new IllegalStateException("Not supported to change the owning type!"); + } /** * This method adds an associated bean using this method element as the originating element. * *

Note that this method can only be called on classes being directly compiled by Micronaut. If the ClassElement is * loaded from pre-compiled code an {@link UnsupportedOperationException} will be thrown.

+ * * @param type The type of the bean * @return A bean builder */ @@ -100,6 +137,7 @@ BeanElementBuilder addAssociatedBean(@NonNull ClassElement type) { /** * If {@link #isSuspend()} returns true this method exposes the continuation parameter in addition to the other parameters of the method. + * * @return The suspend parameters * @since 2.3.0 */ @@ -118,6 +156,7 @@ default boolean hasParameters() { /** * Is the method a Kotlin suspend function. + * * @return True if it is. * @since 2.3.0 */ @@ -127,6 +166,7 @@ default boolean isSuspend() { /** * Is the method a default method on an interfaces. + * * @return True if it is. * @since 2.3.0 */ @@ -146,6 +186,7 @@ default boolean isDefault() { /** * Get the method description. + * * @param simple If simple type names are to be used * @return The method description */ @@ -157,32 +198,71 @@ default boolean isDefault() { /** * Checks if this method element overrides another. + * * @param overridden Possible overridden method * @return true if this overrides passed method element * @since 3.1 */ default boolean overrides(@NonNull MethodElement overridden) { - return false; + if (this.equals(overridden) || isStatic() || overridden.isStatic()) { + return false; + } + MethodElement newMethod = this; + if (newMethod.isAbstract() && !newMethod.isDefault() && (!overridden.isAbstract() || overridden.isDefault())) { + return false; + } + if (!newMethod.getName().equals(overridden.getName()) || overridden.getParameters().length != newMethod.getParameters().length) { + return false; + } + for (int i = 0; i < overridden.getParameters().length; i++) { + ParameterElement existingParameter = overridden.getParameters()[i]; + ParameterElement newParameter = newMethod.getParameters()[i]; + ClassElement existingType = existingParameter.getGenericType(); + ClassElement newType = newParameter.getGenericType(); + if (!newType.isAssignable(existingType)) { + return false; + } + } + ClassElement existingReturnType = overridden.getReturnType().getGenericType(); + ClassElement newTypeReturn = newMethod.getReturnType().getGenericType(); + if (!newTypeReturn.isAssignable(existingReturnType)) { + return false; + } + // Cannot override existing private/package private methods even if the signature is the same + if (overridden.isPrivate()) { + return false; + } + if (overridden.isPackagePrivate()) { + return newMethod.getDeclaringType().getPackageName().equals(overridden.getDeclaringType().getPackageName()); + } + return true; } /** * Creates a {@link MethodElement} for the given parameters. - * @param declaredType The declaring type + * + * @param declaredType The declaring type * @param annotationMetadata The annotation metadata - * @param returnType The return type - * @param genericReturnType The generic return type - * @param name The name - * @param parameterElements The parameter elements + * @param returnType The return type + * @param genericReturnType The generic return type + * @param name The name + * @param parameterElements The parameter elements * @return The method element */ static @NonNull MethodElement of( - @NonNull ClassElement declaredType, - @NonNull AnnotationMetadata annotationMetadata, - @NonNull ClassElement returnType, - @NonNull ClassElement genericReturnType, - @NonNull String name, - ParameterElement...parameterElements) { + @NonNull ClassElement declaredType, + @NonNull AnnotationMetadata annotationMetadata, + @NonNull ClassElement returnType, + @NonNull ClassElement genericReturnType, + @NonNull String name, + ParameterElement... parameterElements) { return new MethodElement() { + + @Override + public boolean isSynthetic() { + return true; + } + @NonNull @Override public ClassElement getReturnType() { @@ -201,14 +281,14 @@ public ParameterElement[] getParameters() { } @Override - public MethodElement withNewParameters(ParameterElement... newParameters) { + public MethodElement withParameters(ParameterElement... newParameters) { return MethodElement.of( - declaredType, - annotationMetadata, - returnType, - genericReturnType, - name, - ArrayUtils.concat(parameterElements, newParameters) + declaredType, + annotationMetadata, + returnType, + genericReturnType, + name, + newParameters ); } @@ -249,6 +329,202 @@ public boolean isPublic() { public Object getNativeType() { throw new UnsupportedOperationException("No native method type present"); } + + @Override + public String toString() { + return getDeclaringType().getName() + "." + name + "(..)"; + } + }; + } + + /** + * Creates a {@link MethodElement} for the given parameters. + * + * @param owningType The owing type + * @param declaringType The declaring type + * @param annotationMetadataProvider The annotation metadata provider + * @param metadataBuilder The metadata builder + * @param returnType The return type + * @param genericReturnType The generic return type + * @param name The name + * @param isStatic Is static + * @param isFinal Is final + * @param parameterElements The parameter elements + * @return The method element + * @since 4.0.0 + */ + static @NonNull MethodElement of( + @NonNull ClassElement owningType, + @NonNull ClassElement declaringType, + @NonNull AnnotationMetadataProvider annotationMetadataProvider, + @NonNull AbstractAnnotationMetadataBuilder metadataBuilder, + @NonNull ClassElement returnType, + @NonNull ClassElement genericReturnType, + @NonNull String name, + boolean isStatic, + boolean isFinal, + ParameterElement... parameterElements) { + return new MethodElement() { + + private @Nullable AnnotationMetadata annotationMetadata; + + @Override + public boolean isSynthetic() { + return true; + } + + @NonNull + @Override + public ClassElement getReturnType() { + return returnType; + } + + @NonNull + @Override + public ClassElement getGenericReturnType() { + return genericReturnType; + } + + @Override + public ParameterElement[] getParameters() { + return parameterElements; + } + + @Override + public MethodElement withParameters(ParameterElement... newParameters) { + return MethodElement.of( + owningType, + declaringType, + annotationMetadataProvider, + metadataBuilder, + returnType, + genericReturnType, + name, + isStatic, + isFinal, + newParameters + ); + } + + @NonNull + @Override + public AnnotationMetadata getAnnotationMetadata() { + if (annotationMetadata == null) { + annotationMetadata = annotationMetadataProvider.getAnnotationMetadata().copyAnnotationMetadata(); + } + return annotationMetadata; + } + + @Override + public ClassElement getOwningType() { + return owningType; + } + + @Override + public ClassElement getDeclaringType() { + return declaringType; + } + + @NonNull + @Override + public String getName() { + return name; + } + + @Override + public boolean isPackagePrivate() { + return false; + } + + @Override + public boolean isProtected() { + return false; + } + + @Override + public boolean isPublic() { + return true; + } + + @Override + public boolean isStatic() { + return isStatic; + } + + @Override + public boolean isFinal() { + return isFinal; + } + + @Override + public Element annotate(@NonNull String annotationType, @NonNull Consumer> consumer) { + ArgumentUtils.requireNonNull("annotationType", annotationType); + AnnotationValueBuilder builder = AnnotationValue.builder(annotationType); + //noinspection ConstantConditions + if (consumer != null) { + + consumer.accept(builder); + AnnotationValue av = builder.build(); + this.annotationMetadata = metadataBuilder.annotate(getAnnotationMetadata(), av); + } + return this; + } + + @Override + public Element annotate(AnnotationValue annotationValue) { + ArgumentUtils.requireNonNull("annotationValue", annotationValue); + annotationMetadata = metadataBuilder.annotate(getAnnotationMetadata(), annotationValue); + return this; + } + + @Override + public Element removeAnnotation(@NonNull String annotationType) { + ArgumentUtils.requireNonNull("annotationType", annotationType); + annotationMetadata = metadataBuilder.removeAnnotation(getAnnotationMetadata(), annotationType); + return this; + } + + @Override + public Element removeAnnotationIf(@NonNull Predicate> predicate) { + ArgumentUtils.requireNonNull("predicate", predicate); + annotationMetadata = metadataBuilder.removeAnnotationIf(getAnnotationMetadata(), predicate); + return this; + + } + + @Override + public Element removeStereotype(@NonNull String annotationType) { + ArgumentUtils.requireNonNull("annotationType", annotationType); + annotationMetadata = metadataBuilder.removeStereotype(getAnnotationMetadata(), annotationType); + return this; + } + + @NonNull + @Override + public Object getNativeType() { + throw new UnsupportedOperationException("No native method type present"); + } + + @Override + public MethodElement withAnnotationMetadata(AnnotationMetadata annotationMetadata) { + return MethodElement.of(owningType, declaringType, new AnnotationMetadataProvider() { + @Override + public AnnotationMetadata getAnnotationMetadata() { + return annotationMetadata; + } + }, metadataBuilder, + returnType, genericReturnType, name, isStatic, isFinal, parameterElements); + } + + @Override + public String toString() { + return getDeclaringType().getName() + "." + name + "(..)"; + } }; } + + @Override + default MethodElement withAnnotationMetadata(AnnotationMetadata annotationMetadata) { + return (MethodElement) MemberElement.super.withAnnotationMetadata(annotationMetadata); + } } diff --git a/inject/src/main/java/io/micronaut/inject/ast/PackageElement.java b/inject/src/main/java/io/micronaut/inject/ast/PackageElement.java index 7c0815938ad..4f152e4e4ca 100644 --- a/inject/src/main/java/io/micronaut/inject/ast/PackageElement.java +++ b/inject/src/main/java/io/micronaut/inject/ast/PackageElement.java @@ -15,9 +15,10 @@ */ package io.micronaut.inject.ast; -import java.util.Objects; - import io.micronaut.core.annotation.NonNull; +import io.micronaut.core.util.StringUtils; + +import java.util.Objects; /** * Models a package in source code. @@ -38,7 +39,16 @@ public interface PackageElement extends Element { */ static @NonNull PackageElement of(@NonNull String name) { Objects.requireNonNull(name, "Name cannot be null"); - return new SimplePackageElement(name); } + + /** + * Is unnamed package? + * + * @return true if unnamed + * @since 4.0.0 + */ + default boolean isUnnamed() { + return StringUtils.isEmpty(getName()); + } } diff --git a/inject/src/main/java/io/micronaut/inject/ast/ParameterElement.java b/inject/src/main/java/io/micronaut/inject/ast/ParameterElement.java index 42023ee0a8c..13ae4ba96c4 100644 --- a/inject/src/main/java/io/micronaut/inject/ast/ParameterElement.java +++ b/inject/src/main/java/io/micronaut/inject/ast/ParameterElement.java @@ -15,8 +15,18 @@ */ package io.micronaut.inject.ast; +import io.micronaut.core.annotation.AnnotationMetadata; +import io.micronaut.core.annotation.AnnotationMetadataProvider; +import io.micronaut.core.annotation.AnnotationValue; +import io.micronaut.core.annotation.AnnotationValueBuilder; import io.micronaut.core.annotation.NonNull; +import io.micronaut.core.util.ArgumentUtils; +import io.micronaut.inject.annotation.AbstractAnnotationMetadataBuilder; + +import java.lang.annotation.Annotation; import java.util.Objects; +import java.util.function.Consumer; +import java.util.function.Predicate; /** * Represents a parameter to a method or constructor. @@ -43,6 +53,20 @@ default String getDescription(boolean simple) { } } + /** + * Return method associated with this parameter. + * + * @return The method element + */ + default MethodElement getMethodElement() { + throw new IllegalStateException("Method element is not supported!"); + } + + @Override + default ParameterElement withAnnotationMetadata(AnnotationMetadata annotationMetadata) { + return (ParameterElement) TypedElement.super.withAnnotationMetadata(annotationMetadata); + } + /** * Creates a parameter element for a simple type and name. * @@ -63,10 +87,95 @@ default String getDescription(boolean simple) { * @since 2.4.0 */ static @NonNull ParameterElement of( - @NonNull ClassElement type, - @NonNull String name) { + @NonNull ClassElement type, + @NonNull String name) { Objects.requireNonNull(name, "Name cannot be null"); Objects.requireNonNull(type, "Type cannot be null"); return new ReflectParameterElement(type, name); } + + /** + * Creates a parameter element for the given arguments. + * + * @param type The element type + * @param name The name + * @param annotationMetadataProvider The name + * @param metadataBuilder The name + * @return The parameter element + * @since 4.0.0 + */ + static @NonNull ParameterElement of( + @NonNull ClassElement type, + @NonNull String name, + @NonNull AnnotationMetadataProvider annotationMetadataProvider, + @NonNull AbstractAnnotationMetadataBuilder metadataBuilder) { + Objects.requireNonNull(name, "Name cannot be null"); + Objects.requireNonNull(type, "Type cannot be null"); + return new ReflectParameterElement(type, name) { + + private AnnotationMetadata annotationMetadata; + + @Override + public ParameterElement withAnnotationMetadata(AnnotationMetadata annotationMetadata) { + return of(type, name, new AnnotationMetadataProvider() { + @Override + public AnnotationMetadata getAnnotationMetadata() { + return annotationMetadata; + } + }, metadataBuilder); + } + + @Override + public Element annotate(@NonNull String annotationType, @NonNull Consumer> consumer) { + ArgumentUtils.requireNonNull("annotationType", annotationType); + AnnotationValueBuilder builder = AnnotationValue.builder(annotationType); + //noinspection ConstantConditions + if (consumer != null) { + + consumer.accept(builder); + AnnotationValue av = builder.build(); + this.annotationMetadata = metadataBuilder.annotate(getAnnotationMetadata(), av); + } + return this; + } + + @Override + public AnnotationMetadata getAnnotationMetadata() { + if (annotationMetadata == null) { + annotationMetadata = annotationMetadataProvider.getAnnotationMetadata(); + } + return annotationMetadata; + } + + @Override + public Element annotate(AnnotationValue annotationValue) { + ArgumentUtils.requireNonNull("annotationValue", annotationValue); + annotationMetadata = metadataBuilder.annotate(getAnnotationMetadata(), annotationValue); + return this; + } + + @Override + public Element removeAnnotation(@NonNull String annotationType) { + ArgumentUtils.requireNonNull("annotationType", annotationType); + annotationMetadata = metadataBuilder.removeAnnotation(getAnnotationMetadata(), annotationType); + return this; + } + + @Override + public Element removeAnnotationIf(@NonNull Predicate> predicate) { + ArgumentUtils.requireNonNull("predicate", predicate); + annotationMetadata = metadataBuilder.removeAnnotationIf(getAnnotationMetadata(), predicate); + return this; + + } + + @Override + public Element removeStereotype(@NonNull String annotationType) { + ArgumentUtils.requireNonNull("annotationType", annotationType); + annotationMetadata = metadataBuilder.removeStereotype(getAnnotationMetadata(), annotationType); + return this; + } + + }; + } } diff --git a/inject/src/main/java/io/micronaut/inject/ast/PrimitiveElement.java b/inject/src/main/java/io/micronaut/inject/ast/PrimitiveElement.java index f55d317daa6..5cb486ab98e 100644 --- a/inject/src/main/java/io/micronaut/inject/ast/PrimitiveElement.java +++ b/inject/src/main/java/io/micronaut/inject/ast/PrimitiveElement.java @@ -17,32 +17,34 @@ import io.micronaut.core.annotation.NonNull; import io.micronaut.core.annotation.AnnotationMetadata; +import io.micronaut.core.annotation.Nullable; /** * A {@link ClassElement} of primitive types. */ public final class PrimitiveElement implements ArrayableClassElement { - public static final PrimitiveElement VOID = new PrimitiveElement("void"); - public static final PrimitiveElement BOOLEAN = new PrimitiveElement("boolean"); - public static final PrimitiveElement INT = new PrimitiveElement("int"); - public static final PrimitiveElement CHAR = new PrimitiveElement("char"); - public static final PrimitiveElement LONG = new PrimitiveElement("long"); - public static final PrimitiveElement FLOAT = new PrimitiveElement("float"); - public static final PrimitiveElement DOUBLE = new PrimitiveElement("double"); - public static final PrimitiveElement SHORT = new PrimitiveElement("short"); - public static final PrimitiveElement BYTE = new PrimitiveElement("byte"); + public static final PrimitiveElement VOID = new PrimitiveElement("void", null); + public static final PrimitiveElement BOOLEAN = new PrimitiveElement("boolean", Boolean.class); + public static final PrimitiveElement INT = new PrimitiveElement("int", Integer.class); + public static final PrimitiveElement CHAR = new PrimitiveElement("char", Character.class); + public static final PrimitiveElement LONG = new PrimitiveElement("long", Long.class); + public static final PrimitiveElement FLOAT = new PrimitiveElement("float", Float.class); + public static final PrimitiveElement DOUBLE = new PrimitiveElement("double", Double.class); + public static final PrimitiveElement SHORT = new PrimitiveElement("short", Short.class); + public static final PrimitiveElement BYTE = new PrimitiveElement("byte", Byte.class); private static final PrimitiveElement[] PRIMITIVES = new PrimitiveElement[] {INT, CHAR, BOOLEAN, LONG, FLOAT, DOUBLE, SHORT, BYTE, VOID}; private final String typeName; private final int arrayDimensions; + private final String boxedTypeName; /** * Default constructor. * @param name The type name */ - private PrimitiveElement(String name) { - this(name, 0); + private PrimitiveElement(String name, @Nullable Class boxedType) { + this(name, boxedType == null ? "<>" : boxedType.getName(), 0); } /** @@ -50,19 +52,28 @@ private PrimitiveElement(String name) { * @param name The type name * @param arrayDimensions The number of array dimensions */ - private PrimitiveElement(String name, int arrayDimensions) { + private PrimitiveElement(String name, String boxedTypeName, int arrayDimensions) { this.typeName = name; this.arrayDimensions = arrayDimensions; + this.boxedTypeName = boxedTypeName; } @Override public boolean isAssignable(String type) { - return typeName.equals(type); + return typeName.equals(type) || boxedTypeName.equals(type) || Object.class.getName().equals(type); } @Override public boolean isAssignable(ClassElement type) { - return this.typeName.equals(type.getName()); + if (this == type) { + return true; + } + if (isArray()) { + if (!type.isPrimitive() || !type.isArray() || type.getArrayDimensions() != getArrayDimensions()) { + return false; + } + } + return isAssignable(type.getName()); } @Override @@ -104,7 +115,7 @@ public AnnotationMetadata getAnnotationMetadata() { @Override public ClassElement withArrayDimensions(int arrayDimensions) { - return new PrimitiveElement(typeName, arrayDimensions); + return new PrimitiveElement(typeName, boxedTypeName, arrayDimensions); } @Override @@ -120,4 +131,13 @@ public static PrimitiveElement valueOf(String name) { } throw new IllegalArgumentException(String.format("No primitive found for name: %s", name)); } + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder("PrimitiveElement{"); + sb.append("typeName='").append(typeName).append('\''); + sb.append(", arrayDimensions=").append(arrayDimensions); + sb.append('}'); + return sb.toString(); + } } diff --git a/inject/src/main/java/io/micronaut/inject/ast/PropertyElement.java b/inject/src/main/java/io/micronaut/inject/ast/PropertyElement.java index 44816e371d8..9e749237e9c 100644 --- a/inject/src/main/java/io/micronaut/inject/ast/PropertyElement.java +++ b/inject/src/main/java/io/micronaut/inject/ast/PropertyElement.java @@ -16,6 +16,7 @@ package io.micronaut.inject.ast; import io.micronaut.core.annotation.NonNull; + import java.util.Optional; /** @@ -25,6 +26,7 @@ * @since 1.0 */ public interface PropertyElement extends TypedElement, MemberElement { + /** * @return The type of the property */ @@ -32,6 +34,16 @@ public interface PropertyElement extends TypedElement, MemberElement { @Override ClassElement getType(); + /** + * Return true the property is excluded. + * + * @return True if the property is excluded + * @since 4.0.0 + */ + default boolean isExcluded() { + return false; + } + /** * Return true only if the property has a getter but no setter. * @@ -42,16 +54,92 @@ default boolean isReadOnly() { } /** - * @return The name of the method used to write the property + * Return true only if the property doesn't support modifying the value. + * + * @return True if the property is write only. + * @since 4.0.0 + */ + default boolean isWriteOnly() { + return !getReadMethod().isPresent(); + } + + /** + * The field representing the property. + * NOTE: The field can be returned even if getter/setter are present. + * + * @return The field + * @since 4.0.0 + */ + default Optional getField() { + return Optional.empty(); + } + + /** + * @return The method to write the property + * @since 4.0.0 */ default Optional getWriteMethod() { return Optional.empty(); } /** - * @return The name of the method used to read the property + * @return The method to read the property */ default Optional getReadMethod() { return Optional.empty(); } + + /** + * @return The member to read the property + * @since 4.0.0 + */ + default Optional getReadMember() { + if (getReadAccessKind() == AccessKind.METHOD) { + return getReadMethod(); + } + return getField(); + } + + /** + * @return The member to write the property + * @since 4.0.0 + */ + default Optional getWriteMember() { + if (getWriteAccessKind() == AccessKind.METHOD) { + return getWriteMethod(); + } + return getField(); + } + + /** + * @return The read access kind of the property + * @since 4.0.0 + */ + default AccessKind getReadAccessKind() { + return AccessKind.METHOD; + } + + /** + * @return The write access kind of the property + * @since 4.0.0 + */ + default AccessKind getWriteAccessKind() { + return AccessKind.METHOD; + } + + /** + * The access type for bean properties. + * @since 4.0.0 + */ + enum AccessKind { + /** + * Allows the use of public or package-protected fields to represent bean properties. + */ + FIELD, + /** + * The default behaviour which is to favour public getters for bean properties. + */ + METHOD + } + } diff --git a/inject/src/main/java/io/micronaut/inject/ast/ReflectParameterElement.java b/inject/src/main/java/io/micronaut/inject/ast/ReflectParameterElement.java index f11256a735b..a846fdbce01 100644 --- a/inject/src/main/java/io/micronaut/inject/ast/ReflectParameterElement.java +++ b/inject/src/main/java/io/micronaut/inject/ast/ReflectParameterElement.java @@ -21,6 +21,7 @@ import io.micronaut.core.annotation.NonNull; import io.micronaut.inject.annotation.DefaultAnnotationMetadata; import io.micronaut.inject.annotation.MutableAnnotationMetadata; + import java.lang.annotation.Annotation; import java.util.function.Consumer; @@ -30,7 +31,7 @@ * @author graemerocher * @since 2.3.0 */ -final class ReflectParameterElement implements ParameterElement { +class ReflectParameterElement implements ParameterElement { private final ClassElement classElement; private final String name; private AnnotationMetadata annotationMetadata = AnnotationMetadata.EMPTY_METADATA; @@ -105,4 +106,5 @@ public Element annotate(@NonNull String annotationType, @ } return this; } + } diff --git a/inject/src/main/java/io/micronaut/inject/ast/beans/BeanElement.java b/inject/src/main/java/io/micronaut/inject/ast/beans/BeanElement.java index d0c8fc1b6ea..bf57df8696d 100644 --- a/inject/src/main/java/io/micronaut/inject/ast/beans/BeanElement.java +++ b/inject/src/main/java/io/micronaut/inject/ast/beans/BeanElement.java @@ -48,7 +48,7 @@ public interface BeanElement extends Element { Element getOriginatingElement(); /** - * Returns the declaring {@link io.micronaut.inject.ast.ClassElement} which may differ + * Returns the declaring {@link ClassElement} which may differ * from the {@link #getBeanTypes()} in the case of factory beans. * * @return The declaring class of the bean. @@ -57,7 +57,7 @@ public interface BeanElement extends Element { ClassElement getDeclaringClass(); /** - * The element that produces the bean, this could be a {@link io.micronaut.inject.ast.ClassElement} for regular beans, + * The element that produces the bean, this could be a {@link ClassElement} for regular beans, * or either a {@link io.micronaut.inject.ast.MethodElement} or {@link io.micronaut.inject.ast.FieldElement} for factory beans. * * @return The producing element diff --git a/inject/src/main/java/io/micronaut/inject/ast/utils/AstBeanPropertiesUtils.java b/inject/src/main/java/io/micronaut/inject/ast/utils/AstBeanPropertiesUtils.java new file mode 100644 index 00000000000..17c37378017 --- /dev/null +++ b/inject/src/main/java/io/micronaut/inject/ast/utils/AstBeanPropertiesUtils.java @@ -0,0 +1,387 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.inject.ast.utils; + +import io.micronaut.context.annotation.BeanProperties; +import io.micronaut.context.annotation.Property; +import io.micronaut.context.annotation.Value; +import io.micronaut.core.annotation.AnnotationUtil; +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.naming.NameUtils; +import io.micronaut.inject.ast.BeanPropertiesQuery; +import io.micronaut.inject.ast.ClassElement; +import io.micronaut.inject.ast.ElementQuery; +import io.micronaut.inject.ast.FieldElement; +import io.micronaut.inject.ast.MemberElement; +import io.micronaut.inject.ast.MethodElement; +import io.micronaut.inject.ast.PrimitiveElement; +import io.micronaut.inject.ast.PropertyElement; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.function.Function; +import java.util.function.Supplier; + +/** + * The AST bean properties utils. + * + * @author Denis Stepanov + * @since 4.0.0 + */ +@Internal +public final class AstBeanPropertiesUtils { + + private AstBeanPropertiesUtils() { + } + + /** + * Resolve the bean properties based on the configuration. + * + * @param configuration The configuration + * @param classElement The class element + * @param methodsSupplier The methods supplier + * @param fieldSupplier The fields supplier + * @param excludeElementsInRole Should exclude elements in role? + * @param propertyFields The fields that are properties + * @param customReaderPropertyNameResolver Custom resolver of the property name from the reader + * @param customWriterPropertyNameResolver Custom resolver of the property name from the writer + * @param propertyCreator The property creator + * @return the list of properties + */ + public static List resolveBeanProperties(BeanPropertiesQuery configuration, + ClassElement classElement, + Supplier> methodsSupplier, + Supplier> fieldSupplier, + boolean excludeElementsInRole, + Set propertyFields, + Function> customReaderPropertyNameResolver, + Function> customWriterPropertyNameResolver, + Function propertyCreator) { + BeanProperties.Visibility visibility = configuration.getVisibility(); + Set accessKinds = configuration.getAccessKinds(); + + Set includes = configuration.getIncludes(); + Set excludes = configuration.getExcludes(); + String[] readPrefixes = configuration.getReadPrefixes(); + String[] writePrefixes = configuration.getWritePrefixes(); + + Map props = new LinkedHashMap<>(); + for (MethodElement methodElement : methodsSupplier.get()) { + // Records include everything + if (methodElement.isStatic() && !configuration.isAllowStaticProperties() + || !excludeElementsInRole && (methodElement.hasDeclaredAnnotation(AnnotationUtil.INJECT) + || methodElement.hasDeclaredAnnotation(AnnotationUtil.PRE_DESTROY) + || methodElement.hasDeclaredAnnotation(AnnotationUtil.POST_CONSTRUCT)) + ) { + continue; + } + String methodName = methodElement.getName(); + if (methodName.contains("$") || methodName.equals("getMetaClass")) { + continue; + } + boolean isAccessor = canMethodBeUsedForAccess(methodElement, accessKinds, visibility); + if (classElement.isRecord()) { + if (!isAccessor) { + continue; + } + String propertyName = methodElement.getSimpleName(); + processRecord(props, methodElement, propertyName); + } else if (NameUtils.isReaderName(methodName, readPrefixes) && methodElement.getParameters().length == 0) { + String propertyName = customReaderPropertyNameResolver.apply(methodElement) + .orElseGet(() -> NameUtils.getPropertyNameForGetter(methodName, readPrefixes)); + processGetter(props, methodElement, propertyName, isAccessor); + } else if (NameUtils.isWriterName(methodName, writePrefixes) + && (methodElement.getParameters().length == 1 + || configuration.isAllowSetterWithZeroArgs() && methodElement.getParameters().length == 0 + || configuration.isAllowSetterWithMultipleArgs() && methodElement.getParameters().length > 1)) { + String propertyName = customWriterPropertyNameResolver.apply(methodElement) + .orElseGet(() -> NameUtils.getPropertyNameForSetter(methodName, writePrefixes)); + processSetter(props, methodElement, propertyName, isAccessor); + } + } + for (FieldElement fieldElement : fieldSupplier.get()) { + if (fieldElement.isStatic() && !configuration.isAllowStaticProperties() + || !excludeElementsInRole && (fieldElement.hasDeclaredAnnotation(AnnotationUtil.INJECT) + || fieldElement.hasStereotype(Value.class) + || fieldElement.hasStereotype(Property.class)) + ) { + continue; + } + String propertyName = fieldElement.getSimpleName(); + boolean isAccessor = propertyFields.contains(propertyName) || canFieldBeUsedForAccess(fieldElement, accessKinds, visibility); + if (!isAccessor && !props.containsKey(propertyName)) { + continue; + } + BeanPropertyData beanPropertyData = props.computeIfAbsent(propertyName, BeanPropertyData::new); + resolveReadAccessForField(fieldElement, isAccessor, beanPropertyData); + resolveWriteAccessForField(fieldElement, isAccessor, beanPropertyData); + } + if (!props.isEmpty()) { + List beanProperties = new ArrayList<>(props.size()); + for (Map.Entry entry : props.entrySet()) { + String propertyName = entry.getKey(); + BeanPropertyData value = entry.getValue(); + if (value.readAccessKind != null || value.writeAccessKind != null) { + value.isExcluded = shouldExclude(includes, excludes, propertyName) + || isExcludedByAnnotations(configuration, value) + || isExcludedBecauseOfMissingAccess(value); + PropertyElement propertyElement = propertyCreator.apply(value); + if (propertyElement != null) { + beanProperties.add(propertyElement); + } + } + } + return beanProperties; + } + return Collections.emptyList(); + } + + private static boolean isExcludedBecauseOfMissingAccess(BeanPropertyData value) { + if (value.readAccessKind == BeanProperties.AccessKind.METHOD + && value.getter == null + && value.writeAccessKind == BeanProperties.AccessKind.METHOD + && value.setter == null) { + return true; + } + if (value.readAccessKind == BeanProperties.AccessKind.FIELD + && value.writeAccessKind == BeanProperties.AccessKind.FIELD + && value.field == null) { + return true; + } + return value.readAccessKind == null && value.writeAccessKind == null; + } + + private static boolean isExcludedByAnnotations(BeanPropertiesQuery conf, BeanPropertyData value) { + if (conf.getExcludedAnnotations().isEmpty()) { + return false; + } + if (value.field != null && conf.getExcludedAnnotations().stream().anyMatch(value.field::hasAnnotation)) { + return true; + } + if (value.getter != null && conf.getExcludedAnnotations().stream().anyMatch(value.getter::hasAnnotation)) { + return true; + } + return (value.setter != null && conf.getExcludedAnnotations().stream().anyMatch(value.setter::hasAnnotation)); + } + + private static void processRecord(Map props, MethodElement methodElement, String propertyName) { + BeanPropertyData beanPropertyData = props.computeIfAbsent(propertyName, BeanPropertyData::new); + beanPropertyData.getter = methodElement; + beanPropertyData.readAccessKind = BeanProperties.AccessKind.METHOD; + beanPropertyData.type = beanPropertyData.getter.getGenericReturnType(); + } + + private static void processGetter(Map props, MethodElement methodElement, String propertyName, boolean isAccessor) { + BeanPropertyData beanPropertyData = props.computeIfAbsent(propertyName, BeanPropertyData::new); + beanPropertyData.getter = methodElement; + if (isAccessor) { + beanPropertyData.readAccessKind = BeanProperties.AccessKind.METHOD; + } + ClassElement genericReturnType = beanPropertyData.getter.getGenericReturnType(); + ClassElement getterType = unwrapType(genericReturnType); + if (beanPropertyData.type != null) { + if (!getterType.isAssignable(unwrapType(beanPropertyData.type))) { + beanPropertyData.getter = null; // not a compatible getter + beanPropertyData.readAccessKind = null; + } + } else { + beanPropertyData.type = genericReturnType; + } + } + + private static void processSetter(Map props, MethodElement methodElement, String propertyName, boolean isAccessor) { + BeanPropertyData beanPropertyData = props.computeIfAbsent(propertyName, BeanPropertyData::new); + ClassElement paramType = methodElement.getParameters().length == 0 ? PrimitiveElement.BOOLEAN : methodElement.getParameters()[0].getGenericType(); + ClassElement setterType = unwrapType(paramType); + if (setterType != null && beanPropertyData.setter != null) { + if (setterType.isAssignable(unwrapType(beanPropertyData.type))) { + // Override the setter because the type is higher + beanPropertyData.setter = methodElement; + } + return; + } + beanPropertyData.setter = methodElement; + if (isAccessor) { + beanPropertyData.writeAccessKind = BeanProperties.AccessKind.METHOD; + } + if (beanPropertyData.type != null) { + if (setterType != null && !setterType.isAssignable(unwrapType(beanPropertyData.type))) { + beanPropertyData.setter = null; // not a compatible setter + beanPropertyData.writeAccessKind = null; + } + } else { + beanPropertyData.type = paramType; + } + } + + private static ClassElement unwrapType(ClassElement type) { + if (type.isOptional()) { + return type.getFirstTypeArgument().orElse(type); + } + return type; + } + + private static void resolveWriteAccessForField(FieldElement fieldElement, boolean isAccessor, BeanPropertyData beanPropertyData) { + if (fieldElement.isFinal()) { + return; + } + ClassElement fieldType = unwrapType(fieldElement.getGenericType()); + if (beanPropertyData.setter == null) { + if (beanPropertyData.type != null) { + if (fieldType.isAssignable(unwrapType(beanPropertyData.type))) { + beanPropertyData.field = fieldElement; + if (isAccessor) { + beanPropertyData.writeAccessKind = BeanProperties.AccessKind.FIELD; + } + } + // Else: not compatible field + } else { + beanPropertyData.field = fieldElement; + beanPropertyData.type = fieldElement.getGenericType(); + if (isAccessor) { + beanPropertyData.writeAccessKind = BeanProperties.AccessKind.FIELD; + } + } + } else { + beanPropertyData.field = fieldElement; + } + } + + private static void resolveReadAccessForField(FieldElement fieldElement, boolean isAccessor, BeanPropertyData beanPropertyData) { + ClassElement unwrappedFieldType = unwrapType(fieldElement.getGenericType()); + if (beanPropertyData.getter == null) { + if (beanPropertyData.type != null) { + if (unwrappedFieldType.isAssignable(unwrapType(beanPropertyData.type))) { + // Override the existing type to include generic annotations + if (beanPropertyData.type.isAssignable(fieldElement.getGenericType())) { + beanPropertyData.type = fieldElement.getGenericType(); + } + beanPropertyData.field = fieldElement; + if (isAccessor) { + beanPropertyData.readAccessKind = BeanProperties.AccessKind.FIELD; + } + } + // Else: not compatible field + } else { + beanPropertyData.field = fieldElement; + beanPropertyData.type = fieldElement.getGenericType(); + if (isAccessor) { + beanPropertyData.readAccessKind = BeanProperties.AccessKind.FIELD; + } + } + } else { + beanPropertyData.field = fieldElement; + if (beanPropertyData.type.isAssignable(fieldElement.getGenericType())) { + // Override the existing type to include generic annotations + beanPropertyData.type = fieldElement.getGenericType(); + } + } + } + + private static boolean canFieldBeUsedForAccess(FieldElement fieldElement, + Set accessKinds, + BeanProperties.Visibility visibility) { + if (fieldElement.getOwningType().isRecord()) { + return false; + } + if (accessKinds.contains(BeanProperties.AccessKind.FIELD)) { + return isAccessible(fieldElement, visibility); + } + return false; + } + + private static boolean canMethodBeUsedForAccess(MethodElement methodElement, + Set accessKinds, + BeanProperties.Visibility visibility) { + return accessKinds.contains(BeanProperties.AccessKind.METHOD) && isAccessible(methodElement, visibility); + } + + private static boolean isAccessible(MemberElement memberElement, BeanProperties.Visibility visibility) { + switch (visibility) { + case DEFAULT: + return !memberElement.isPrivate() && (memberElement.isAccessible() || memberElement.getDeclaringType().hasDeclaredStereotype(BeanProperties.class)); + case PUBLIC: + return memberElement.isPublic(); + default: + return false; + } + } + + public static List getSubtypeFirstMethods(ClassElement classElement) { + List methods = classElement.getEnclosedElements(ElementQuery.ALL_METHODS.onlyInstance()); + List result = new ArrayList<>(methods.size()); + List other = new ArrayList<>(methods.size()); + // Process subtype methods first + for (MethodElement methodElement : methods) { + if (methodElement.getDeclaringType().equals(classElement)) { + other.add(methodElement); + } else { + result.add(methodElement); + } + } + result.addAll(other); + return result; + + } + + public static List getSubtypeFirstFields(ClassElement classElement) { + List fields = classElement.getEnclosedElements(ElementQuery.ALL_FIELDS); + List result = new ArrayList<>(fields.size()); + List other = new ArrayList<>(fields.size()); + // Process subtype fields first + for (FieldElement fieldElement : fields) { + if (fieldElement.getDeclaringType().equals(classElement)) { + other.add(fieldElement); + } else { + result.add(fieldElement); + } + } + result.addAll(other); + return result; + } + + private static boolean shouldExclude(Set includes, Set excludes, String propertyName) { + if (!includes.isEmpty() && !includes.contains(propertyName)) { + return true; + } + return !excludes.isEmpty() && excludes.contains(propertyName); + } + + /** + * Internal holder class for getters and setters. + */ + @SuppressWarnings("VisibilityModifier") + public static final class BeanPropertyData { + public ClassElement type; + public MethodElement getter; + public MethodElement setter; + public FieldElement field; + public BeanProperties.AccessKind readAccessKind; + public BeanProperties.AccessKind writeAccessKind; + public final String propertyName; + public boolean isExcluded; + + public BeanPropertyData(String propertyName) { + this.propertyName = propertyName; + } + } + +} diff --git a/inject/src/main/java/io/micronaut/inject/beans/visitor/BeanIntrospectionWriter.java b/inject/src/main/java/io/micronaut/inject/beans/visitor/BeanIntrospectionWriter.java index 25af490ec81..94b2b22866d 100644 --- a/inject/src/main/java/io/micronaut/inject/beans/visitor/BeanIntrospectionWriter.java +++ b/inject/src/main/java/io/micronaut/inject/beans/visitor/BeanIntrospectionWriter.java @@ -967,7 +967,8 @@ private ClassWriter generateClassBytes(ClassWriter classWriter) { } private void pushAnnotationMetadata(ClassWriter classWriter, GeneratorAdapter staticInit, AnnotationMetadata annotationMetadata) { - if (annotationMetadata == AnnotationMetadata.EMPTY_METADATA || annotationMetadata.isEmpty()) { + annotationMetadata = annotationMetadata.getTargetAnnotationMetadata(); + if (annotationMetadata.isEmpty()) { staticInit.push((String) null); } else if (annotationMetadata instanceof AnnotationMetadataReference) { AnnotationMetadataReference reference = (AnnotationMetadataReference) annotationMetadata; @@ -990,7 +991,7 @@ private void pushAnnotationMetadata(ClassWriter classWriter, GeneratorAdapter st defaults, loadTypeMethods); } else { - staticInit.push((String) null); + throw new IllegalStateException("Unknown annotation metadata: " + annotationMetadata); } } diff --git a/inject/src/main/java/io/micronaut/inject/beans/visitor/IntrospectedTypeElementVisitor.java b/inject/src/main/java/io/micronaut/inject/beans/visitor/IntrospectedTypeElementVisitor.java index e474a9e52b0..4c05ce8bf8d 100644 --- a/inject/src/main/java/io/micronaut/inject/beans/visitor/IntrospectedTypeElementVisitor.java +++ b/inject/src/main/java/io/micronaut/inject/beans/visitor/IntrospectedTypeElementVisitor.java @@ -15,7 +15,6 @@ */ package io.micronaut.inject.beans.visitor; -import io.micronaut.context.annotation.ConfigurationReader; import io.micronaut.context.annotation.Executable; import io.micronaut.core.annotation.AccessorsStyle; import io.micronaut.core.annotation.AnnotationClassValue; @@ -23,20 +22,34 @@ import io.micronaut.core.annotation.AnnotationValue; import io.micronaut.core.annotation.Internal; import io.micronaut.core.annotation.Introspected; +import io.micronaut.core.annotation.NonNull; +import io.micronaut.core.annotation.Nullable; import io.micronaut.core.naming.NameUtils; import io.micronaut.core.util.ArrayUtils; import io.micronaut.core.util.CollectionUtils; import io.micronaut.inject.annotation.AnnotationMetadataHierarchy; -import io.micronaut.inject.ast.*; +import io.micronaut.inject.annotation.MutableAnnotationMetadata; +import io.micronaut.inject.ast.ClassElement; +import io.micronaut.inject.ast.ElementModifier; +import io.micronaut.inject.ast.ElementQuery; +import io.micronaut.inject.ast.FieldElement; +import io.micronaut.inject.ast.MethodElement; +import io.micronaut.inject.ast.ParameterElement; +import io.micronaut.inject.ast.PropertyElement; import io.micronaut.inject.visitor.TypeElementVisitor; import io.micronaut.inject.visitor.VisitorContext; import io.micronaut.inject.writer.ClassGenerationException; -import io.micronaut.core.annotation.NonNull; -import io.micronaut.core.annotation.Nullable; - import java.io.IOException; -import java.util.*; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Predicate; import java.util.stream.Collectors; @@ -70,7 +83,6 @@ public class IntrospectedTypeElementVisitor implements TypeElementVisitor writers = new LinkedHashMap<>(10); private List abstractIntrospections = new ArrayList<>(); private AbstractIntrospection currentAbstractIntrospection; - private ClassElement currentClassElement; @Override public int getOrder() { @@ -81,26 +93,15 @@ public int getOrder() { @Override public void visitClass(ClassElement element, VisitorContext context) { // reset - currentClassElement = null; currentAbstractIntrospection = null; if (!element.isPrivate() && element.hasStereotype(Introspected.class)) { final AnnotationValue introspected = element.getAnnotation(Introspected.class); if (introspected != null && !writers.containsKey(element.getName())) { - currentClassElement = element; processIntrospected(element, context, introspected); } } } - @Override - public void visitConstructor(ConstructorElement element, VisitorContext context) { - final ClassElement declaringType = element.getDeclaringType(); - if (element.getDeclaringType().hasStereotype(ConfigurationReader.class)) { - final ParameterElement[] parameters = element.getParameters(); - introspectIfValidated(context, declaringType, parameters); - } - } - private boolean isIntrospected(VisitorContext context, ClassElement c) { return writers.containsKey(c.getName()) || context.getClassElement(c.getPackageName() + ".$" + c.getSimpleName() + "$Introspection").isPresent(); } @@ -115,13 +116,6 @@ public void visitMethod(MethodElement element, VisitorContext context) { final String[] writePrefixes = declaringType.getValue(AccessorsStyle.class, "writePrefixes", String[].class) .orElse(new String[]{AccessorsStyle.DEFAULT_WRITE_PREFIX}); - if (declaringType.hasStereotype(ConfigurationReader.class) && NameUtils.isReaderName(methodName, readPrefixes) && !writers.containsKey(declaringType.getName())) { - final boolean hasConstraints = element.hasStereotype(JAVAX_VALIDATION_CONSTRAINT) || element.hasStereotype(JAVAX_VALIDATION_VALID); - if (hasConstraints) { - processIntrospected(declaringType, context, AnnotationValue.builder(Introspected.class).build()); - } - } - if (currentAbstractIntrospection != null) { if (NameUtils.isReaderName(methodName, readPrefixes) && element.getParameters().length == 0) { final String propertyName = NameUtils.getPropertyNameForGetter(methodName, readPrefixes); @@ -143,28 +137,6 @@ public void visitMethod(MethodElement element, VisitorContext context) { } } - @Override - public void visitField(FieldElement element, VisitorContext context) { - final ClassElement declaringType = element.getDeclaringType(); - if (declaringType.hasStereotype(ConfigurationReader.class) && !writers.containsKey(declaringType.getName())) { - final boolean hasConstraints = element.hasStereotype(JAVAX_VALIDATION_CONSTRAINT) || element.hasStereotype(JAVAX_VALIDATION_VALID); - if (hasConstraints) { - processIntrospected(declaringType, context, AnnotationValue.builder(Introspected.class).build()); - } - } - } - - private void introspectIfValidated(VisitorContext context, ClassElement declaringType, ParameterElement[] parameters) { - if (!writers.containsKey(declaringType.getName())) { - final boolean hasConstraints = Arrays.stream(parameters).anyMatch(e -> - e.hasStereotype(JAVAX_VALIDATION_CONSTRAINT) || e.hasStereotype(JAVAX_VALIDATION_VALID) - ); - if (hasConstraints) { - processIntrospected(declaringType, context, AnnotationValue.builder(Introspected.class).build()); - } - } - } - private void processIntrospected(ClassElement element, VisitorContext context, AnnotationValue introspected) { final String[] packages = introspected.stringValues("packages"); final AnnotationClassValue[] classes = introspected.get("classes", AnnotationClassValue[].class, new AnnotationClassValue[0]); @@ -276,7 +248,7 @@ private void processIntrospected(ClassElement element, VisitorContext context, A final BeanIntrospectionWriter writer = new BeanIntrospectionWriter( element, - metadata ? element.getAnnotationMetadata() : null + metadata ? MutableAnnotationMetadata.of(element.getAnnotationMetadata()) : null ); processElement( @@ -301,7 +273,6 @@ public VisitorKind getVisitorKind() { @Override public void finish(VisitorContext visitorContext) { - try { for (AbstractIntrospection abstractIntrospection : abstractIntrospections) { final Collection properties = abstractIntrospection.properties.values(); @@ -484,6 +455,12 @@ private void processBeanProperties( continue; } + AnnotationMetadata annotationMetadata; + if (metadata) { + annotationMetadata = MutableAnnotationMetadata.of(beanProperty.getAnnotationMetadata()); + } else { + annotationMetadata = null; + } writer.visitProperty( type, genericType, @@ -491,7 +468,7 @@ private void processBeanProperties( beanProperty.getReadMethod().orElse(null), beanProperty.getWriteMethod().orElse(null), beanProperty.isReadOnly(), - metadata ? beanProperty.getAnnotationMetadata() : null, + annotationMetadata, genericType.getTypeArguments() ); diff --git a/inject/src/main/java/io/micronaut/inject/configuration/ConfigurationMetadataBuilder.java b/inject/src/main/java/io/micronaut/inject/configuration/ConfigurationMetadataBuilder.java index 01e49824db2..44aba8da662 100644 --- a/inject/src/main/java/io/micronaut/inject/configuration/ConfigurationMetadataBuilder.java +++ b/inject/src/main/java/io/micronaut/inject/configuration/ConfigurationMetadataBuilder.java @@ -16,19 +16,22 @@ package io.micronaut.inject.configuration; import io.micronaut.context.annotation.ConfigurationReader; -import io.micronaut.core.annotation.AnnotationMetadata; -import io.micronaut.core.naming.NameUtils; -import io.micronaut.core.util.CollectionUtils; import io.micronaut.core.annotation.NonNull; import io.micronaut.core.annotation.Nullable; +import io.micronaut.core.naming.NameUtils; +import io.micronaut.core.util.CollectionUtils; +import io.micronaut.inject.ast.ClassElement; import io.micronaut.inject.ast.Element; +import io.micronaut.inject.writer.OriginatingElements; import java.io.IOException; import java.io.Writer; import java.util.ArrayList; import java.util.Collections; import java.util.List; -import java.util.Optional; + +import static io.micronaut.inject.configuration.ConfigurationUtils.buildPropertyPath; +import static io.micronaut.inject.configuration.ConfigurationUtils.getRequiredTypePath; /** *

A builder for producing metadata for the available {@link io.micronaut.context.annotation.ConfigurationProperties}.

@@ -36,19 +39,25 @@ *

This data can then be subsequently written to a format readable by IDEs * (like spring-configuration-metadata.json for example).

* - * @param The type * @author Graeme Rocher + * @author Denis Stepanov * @since 1.0 */ -public abstract class ConfigurationMetadataBuilder { - private static ConfigurationMetadataBuilder currentBuilder = null; +public class ConfigurationMetadataBuilder { + + @SuppressWarnings({"StaticVariableName", "VisibilityModifier"}) + public static ConfigurationMetadataBuilder INSTANCE = new ConfigurationMetadataBuilder(); + + private final OriginatingElements originatingElements = OriginatingElements.of(); private final List properties = new ArrayList<>(); private final List configurations = new ArrayList<>(); /** * @return The originating elements for the builder. */ - public abstract @NonNull Element[] getOriginatingElements(); + public @NonNull Element[] getOriginatingElements() { + return originatingElements.getOriginatingElements(); + } /** * @return The properties @@ -74,33 +83,18 @@ public boolean hasMetadata() { /** * Visit a {@link io.micronaut.context.annotation.ConfigurationProperties} class. * - * @param type The type of the {@link io.micronaut.context.annotation.ConfigurationProperties} - * @param description A description - * @return This {@link ConfigurationMetadata} - */ - public ConfigurationMetadata visitProperties(T type, - @Nullable String description) { - - AnnotationMetadata annotationMetadata = getAnnotationMetadata(type); - return visitProperties(type, description, annotationMetadata); - } - - /** - * Visit a {@link io.micronaut.context.annotation.ConfigurationProperties} class. - * - * @param type The type of the {@link io.micronaut.context.annotation.ConfigurationProperties} - * @param description A description - * @param annotationMetadata the annotation metadata + * @param classElement The type of the {@link io.micronaut.context.annotation.ConfigurationProperties} * @return This {@link ConfigurationMetadata} */ - public ConfigurationMetadata visitProperties(T type, @Nullable String description, @NonNull AnnotationMetadata annotationMetadata) { - String path = buildTypePath(type, type, annotationMetadata); + public ConfigurationMetadata visitProperties(ClassElement classElement) { + originatingElements.addOriginatingElement(classElement); + String path = getRequiredTypePath(classElement); ConfigurationMetadata configurationMetadata = new ConfigurationMetadata(); configurationMetadata.name = NameUtils.hyphenate(path, true); - configurationMetadata.type = getTypeString(type); - configurationMetadata.description = description; - configurationMetadata.includes = CollectionUtils.setOf(annotationMetadata.stringValues(ConfigurationReader.class, "includes")); - configurationMetadata.excludes = CollectionUtils.setOf(annotationMetadata.stringValues(ConfigurationReader.class, "excludes")); + configurationMetadata.type = classElement.getType().getName(); + configurationMetadata.description = classElement.getDocumentation().orElse(null); + configurationMetadata.includes = CollectionUtils.setOf(classElement.stringValues(ConfigurationReader.class, "includes")); + configurationMetadata.excludes = CollectionUtils.setOf(classElement.stringValues(ConfigurationReader.class, "excludes")); this.configurations.add(configurationMetadata); return configurationMetadata; } @@ -117,136 +111,26 @@ public ConfigurationMetadata visitProperties(T type, @Nullable String descriptio * enums etc.) * @return This property metadata */ - public PropertyMetadata visitProperty(T owningType, - T declaringType, - String propertyType, + public PropertyMetadata visitProperty(ClassElement owningType, + ClassElement declaringType, + ClassElement propertyType, String name, @Nullable String description, @Nullable String defaultValue) { + originatingElements.addOriginatingElement(owningType); + originatingElements.addOriginatingElement(declaringType); PropertyMetadata metadata = new PropertyMetadata(); - metadata.declaringType = getTypeString(declaringType); + metadata.declaringType = declaringType.getName(); metadata.name = name; metadata.path = NameUtils.hyphenate(buildPropertyPath(owningType, declaringType, name), true); - metadata.type = propertyType; + metadata.type = propertyType.getType().getName(); metadata.description = description; metadata.defaultValue = defaultValue; properties.add(metadata); return metadata; } - /** - * Visit a configuration property on the last declared properties instance. - * - * @param propertyType The property type - * @param name The property name - * @param description A description for the property - * @param defaultValue The default value of the property (only used for constant values such as strings, numbers, - * enums etc.) - * @return This property metadata or null if no existing configuration is active - */ - public PropertyMetadata visitProperty(String propertyType, - String name, - @Nullable String description, - @Nullable String defaultValue) { - - if (!configurations.isEmpty()) { - ConfigurationMetadata last = configurations.get(configurations.size() - 1); - PropertyMetadata metadata = new PropertyMetadata(); - metadata.declaringType = last.type; - metadata.name = name; - metadata.path = NameUtils.hyphenate(last.name + "." + name, true); - metadata.type = propertyType; - metadata.description = description; - metadata.defaultValue = defaultValue; - properties.add(metadata); - return metadata; - } - return null; - } - - /** - *

Build a property path for the given declaring type and property name.

- * - *

For {@link io.micronaut.context.annotation.ConfigurationProperties} that path is a property is - * established by looking at the value of the {@link io.micronaut.context.annotation.ConfigurationProperties} and - * then calculating the path based on the inheritance tree.

- * - *

For example consider the following classes:

- * - *

-     *  {@literal @}ConfigurationProperties("parent")
-     *   public class ParentProperties {
-     *      String foo;
-     *   }
-     *
-     *  {@literal @}ConfigurationProperties("child")
-     *   public class ChildProperties extends ParentProperties {
-     *      String bar;
-     *   }
-     * 
- * - *

The path of the property {@code foo} will be "parent.foo" whilst the path of the property {@code bar} will - * be "parent.child.bar" factoring in the class hierarchy

- * - *

Inner classes hierarchies are also taken into account

- * - * @param owningType The owning type - * @param declaringType The declaring type - * @param propertyName The property name - * @return The property path - */ - protected abstract String buildPropertyPath(T owningType, T declaringType, String propertyName); - - /** - * Variation of {@link #buildPropertyPath(Object, Object, String)} for types. - * - * @param owningType The owning type - * @param declaringType The type - * @return The type path - */ - protected abstract String buildTypePath(T owningType, T declaringType); - - /** - * Variation of {@link #buildPropertyPath(Object, Object, String)} for types. - * - * @param owningType The owning type - * @param declaringType The type - * @param annotationMetadata The annotation metadata - * @return The type path - */ - protected abstract String buildTypePath(T owningType, T declaringType, AnnotationMetadata annotationMetadata); - - /** - * Convert the given type to a string. - * - * @param type The type - * @return The string - */ - protected abstract String getTypeString(T type); - - /** - * @param type The type - * @return The annotation metadata for the type - */ - protected abstract AnnotationMetadata getAnnotationMetadata(T type); - - /** - * Obtains the currently active metadata builder. - * @return The builder - */ - public static Optional> getConfigurationMetadataBuilder() { - return Optional.ofNullable(currentBuilder); - } - - /** - * Sets or clears the current {@link ConfigurationMetadataBuilder}. - * @param builder the builder - */ - public static void setConfigurationMetadataBuilder(@Nullable ConfigurationMetadataBuilder builder) { - currentBuilder = builder; - } - /** * Quote a string. * diff --git a/inject/src/main/java/io/micronaut/inject/configuration/ConfigurationMetadataWriter.java b/inject/src/main/java/io/micronaut/inject/configuration/ConfigurationMetadataWriter.java index 665b472939b..14f5fb1a792 100644 --- a/inject/src/main/java/io/micronaut/inject/configuration/ConfigurationMetadataWriter.java +++ b/inject/src/main/java/io/micronaut/inject/configuration/ConfigurationMetadataWriter.java @@ -34,5 +34,5 @@ public interface ConfigurationMetadataWriter { * @param classWriterOutputVisitor The class output visitor * @throws IOException If an error occurred writing output */ - void write(ConfigurationMetadataBuilder metadataBuilder, ClassWriterOutputVisitor classWriterOutputVisitor) throws IOException; + void write(ConfigurationMetadataBuilder metadataBuilder, ClassWriterOutputVisitor classWriterOutputVisitor) throws IOException; } diff --git a/inject/src/main/java/io/micronaut/inject/configuration/ConfigurationUtils.java b/inject/src/main/java/io/micronaut/inject/configuration/ConfigurationUtils.java new file mode 100644 index 00000000000..61026c71948 --- /dev/null +++ b/inject/src/main/java/io/micronaut/inject/configuration/ConfigurationUtils.java @@ -0,0 +1,140 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.inject.configuration; + +import io.micronaut.context.annotation.ConfigurationReader; +import io.micronaut.context.annotation.EachProperty; +import io.micronaut.core.annotation.AnnotationMetadata; +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.util.StringUtils; +import io.micronaut.inject.ast.ClassElement; + +import java.util.Objects; +import java.util.Optional; + +/** + * An util class to calculate configuration paths. + * + * @author Denis Stepanov + * @since 4.0.0 + */ +@Internal +public final class ConfigurationUtils { + + private static final String PREFIX_CALCULATED = "prefixCalculated"; + + public static String buildPropertyPath(ClassElement owningType, ClassElement declaringType, String propertyName) { + String typePath; + if (declaringType.hasAnnotation(ConfigurationReader.class)) { + typePath = getRequiredTypePath(declaringType); + } else { + typePath = getRequiredTypePath(owningType); + } + String s = typePath + "." + propertyName; + return s; + } + + public static String getRequiredTypePath(ClassElement classElement) { + return getTypePath(classElement).orElseThrow(() -> new IllegalStateException("Prefix is required for " + classElement)); + } + + public static Optional getTypePath(ClassElement classElement) { + if (!classElement.hasStereotype(ConfigurationReader.class)) { + return Optional.empty(); + } + if (classElement.booleanValue(ConfigurationReader.class, PREFIX_CALCULATED).orElse(false)) { + return classElement.stringValue(ConfigurationReader.class, ConfigurationReader.PREFIX); + } + String path = getPath(classElement); + path = prependSuperclasses(classElement, path); + Optional inner = classElement.getEnclosingType(); + if (classElement.isInner() && inner.isPresent()) { + ClassElement enclosingType = inner.get(); + String parentPrefix = getTypePath(enclosingType).orElse(""); + path = combinePaths(parentPrefix, path); + } + + String finalPath = path; + classElement.annotate(ConfigurationReader.class, builder -> builder.member(ConfigurationReader.PREFIX, finalPath).member(PREFIX_CALCULATED, true)); + return Optional.of(path); + } + + private static String combinePaths(String p1, String p2) { + if (StringUtils.isNotEmpty(p1) && StringUtils.isNotEmpty(p2)) { + return p1 + "." + p2; + } + if (StringUtils.isNotEmpty(p1)) { + return p1; + } + return p2; + } + + private static String getPath(AnnotationMetadata annotationMetadata) { + Optional basePrefixOptional = annotationMetadata.stringValue(ConfigurationReader.class, ConfigurationReader.BASE_PREFIX); + Optional prefixOptional = annotationMetadata.stringValue(ConfigurationReader.class, ConfigurationReader.PREFIX); + String prefix = null; + if (basePrefixOptional.isPresent()) { + if (!prefixOptional.isPresent()) { + prefix = basePrefixOptional.get(); + } else { + prefix = prefixOptional.map(p -> basePrefixOptional.get() + "." + p).orElse(null); + } + } else { + prefix = prefixOptional.orElse(null); + } + if (annotationMetadata.hasDeclaredAnnotation(EachProperty.class)) { + Objects.requireNonNull(prefix); + if (annotationMetadata.booleanValue(EachProperty.class, "list").orElse(false)) { + return prefix + "[*]"; + } else { + return prefix + ".*"; + } + } + if (prefix == null) { + return ""; + } + return prefix; + } + + private static String prependSuperclasses(ClassElement declaringType, String path) { + if (declaringType.isInterface()) { + ClassElement superInterface = resolveSuperInterface(declaringType); + while (superInterface != null) { + Optional parentConfig = getTypePath(superInterface); + if (parentConfig.isPresent()) { + path = combinePaths(parentConfig.get(), path); + } + superInterface = resolveSuperInterface(superInterface); + } + } else { + Optional optionalSuperType = declaringType.getSuperType(); + while (optionalSuperType.isPresent()) { + ClassElement superType = optionalSuperType.get(); + Optional parentConfig = getTypePath(superType); + if (parentConfig.isPresent()) { + path = combinePaths(parentConfig.get(), path); + } + optionalSuperType = superType.getSuperType(); + } + } + return path; + } + + private static ClassElement resolveSuperInterface(ClassElement declaringType) { + return declaringType.getInterfaces().stream().filter(tm -> tm.hasStereotype(ConfigurationReader.class)).findFirst().orElse(null); + } + +} diff --git a/inject/src/main/java/io/micronaut/inject/configuration/JsonConfigurationMetadataWriter.java b/inject/src/main/java/io/micronaut/inject/configuration/JsonConfigurationMetadataWriter.java index 7f552639404..3bb5355fe45 100644 --- a/inject/src/main/java/io/micronaut/inject/configuration/JsonConfigurationMetadataWriter.java +++ b/inject/src/main/java/io/micronaut/inject/configuration/JsonConfigurationMetadataWriter.java @@ -35,7 +35,7 @@ public class JsonConfigurationMetadataWriter implements ConfigurationMetadataWriter { @Override - public void write(ConfigurationMetadataBuilder metadataBuilder, ClassWriterOutputVisitor classWriterOutputVisitor) throws IOException { + public void write(ConfigurationMetadataBuilder metadataBuilder, ClassWriterOutputVisitor classWriterOutputVisitor) throws IOException { Optional opt = classWriterOutputVisitor.visitMetaInfFile(getFileName(), metadataBuilder.getOriginatingElements()); if (opt.isPresent()) { GeneratedFile file = opt.get(); diff --git a/inject/src/main/java/io/micronaut/inject/mappers/ConfigurationBuilderToBeanPropertiesMapper.java b/inject/src/main/java/io/micronaut/inject/mappers/ConfigurationBuilderToBeanPropertiesMapper.java new file mode 100644 index 00000000000..44bde8435cc --- /dev/null +++ b/inject/src/main/java/io/micronaut/inject/mappers/ConfigurationBuilderToBeanPropertiesMapper.java @@ -0,0 +1,56 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.inject.mappers; + +import io.micronaut.context.annotation.BeanProperties; +import io.micronaut.context.annotation.ConfigurationBuilder; +import io.micronaut.core.annotation.AnnotationValue; +import io.micronaut.core.annotation.Internal; +import io.micronaut.inject.annotation.TypedAnnotationMapper; +import io.micronaut.inject.visitor.VisitorContext; + +import java.util.Collections; +import java.util.List; + +/** + * Map values of {@link ConfigurationBuilder} to {@link BeanProperties}. + * + * @author Denis Stepanov + * @since 4.0.0 + */ +@Internal +public final class ConfigurationBuilderToBeanPropertiesMapper implements TypedAnnotationMapper { + + @Override + public List> map(AnnotationValue annotation, VisitorContext visitorContext) { + return Collections.singletonList( + AnnotationValue.builder(BeanProperties.class) + // Configuration properties also includes fields + .member(BeanProperties.MEMBER_ACCESS_KIND, new BeanProperties.AccessKind[]{BeanProperties.AccessKind.METHOD}) + .member(BeanProperties.MEMBER_VISIBILITY, BeanProperties.Visibility.DEFAULT) + .member(BeanProperties.MEMBER_INCLUDES, annotation.stringValues(BeanProperties.MEMBER_INCLUDES)) + .member(BeanProperties.MEMBER_EXCLUDES, annotation.stringValues(BeanProperties.MEMBER_EXCLUDES)) + .member(BeanProperties.MEMBER_ALLOW_WRITE_WITH_ZERO_ARGS, annotation.booleanValue("allowZeroArgs").orElse(false)) + .member(BeanProperties.MEMBER_ALLOW_WRITE_WITH_MULTIPLE_ARGS, true) + .build() + ); + } + + @Override + public Class annotationType() { + return ConfigurationBuilder.class; + } +} diff --git a/inject/src/main/java/io/micronaut/inject/mappers/ConfigurationPropertiesToBeanPropertiesMapper.java b/inject/src/main/java/io/micronaut/inject/mappers/ConfigurationPropertiesToBeanPropertiesMapper.java new file mode 100644 index 00000000000..10e5142aa03 --- /dev/null +++ b/inject/src/main/java/io/micronaut/inject/mappers/ConfigurationPropertiesToBeanPropertiesMapper.java @@ -0,0 +1,52 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.inject.mappers; + +import io.micronaut.context.annotation.ConfigurationReader; +import io.micronaut.core.annotation.AnnotationValue; +import io.micronaut.context.annotation.BeanProperties; +import io.micronaut.core.annotation.Internal; +import io.micronaut.inject.annotation.TypedAnnotationMapper; +import io.micronaut.inject.visitor.VisitorContext; + +import java.util.Collections; +import java.util.List; + +/** + * Map values of {@link ConfigurationReader} to {@link BeanProperties}. + * + * @author Denis Stepanov + * @since 4.0.0 + */ +@Internal +public final class ConfigurationPropertiesToBeanPropertiesMapper implements TypedAnnotationMapper { + + @Override + public List> map(AnnotationValue annotation, VisitorContext visitorContext) { + return Collections.singletonList( + AnnotationValue.builder(BeanProperties.class) + // Configuration properties also includes fields + .member(BeanProperties.MEMBER_ACCESS_KIND, new BeanProperties.AccessKind[]{BeanProperties.AccessKind.FIELD, BeanProperties.AccessKind.METHOD}) + .member(BeanProperties.MEMBER_VISIBILITY, BeanProperties.Visibility.DEFAULT) + .build() + ); + } + + @Override + public Class annotationType() { + return ConfigurationReader.class; + } +} diff --git a/inject/src/main/java/io/micronaut/inject/processing/AbstractBeanElementCreator.java b/inject/src/main/java/io/micronaut/inject/processing/AbstractBeanElementCreator.java new file mode 100644 index 00000000000..85c0f2b3fb4 --- /dev/null +++ b/inject/src/main/java/io/micronaut/inject/processing/AbstractBeanElementCreator.java @@ -0,0 +1,159 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.inject.processing; + +import io.micronaut.context.RequiresCondition; +import io.micronaut.context.annotation.Requires; +import io.micronaut.core.annotation.AnnotationMetadata; +import io.micronaut.core.annotation.AnnotationUtil; +import io.micronaut.core.annotation.AnnotationValue; +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.NextMajorVersion; +import io.micronaut.core.annotation.NonNull; +import io.micronaut.core.annotation.Nullable; +import io.micronaut.core.reflect.ClassUtils; +import io.micronaut.inject.annotation.AnnotationMetadataHierarchy; +import io.micronaut.inject.validation.RequiresValidation; +import io.micronaut.inject.ast.ClassElement; +import io.micronaut.inject.ast.MemberElement; +import io.micronaut.inject.visitor.VisitorContext; +import io.micronaut.inject.writer.BeanDefinitionVisitor; + +import java.lang.annotation.Annotation; +import java.util.Collection; +import java.util.LinkedList; +import java.util.List; +import java.util.function.Function; +import java.util.function.Predicate; + +/** + * Abstract shared functionality of the builder. + * + * @author Denis Stepanov + * @since 4.0.0 + */ +@Internal +abstract class AbstractBeanElementCreator implements BeanDefinitionCreator { + + public static final String ANN_VALIDATED = "io.micronaut.validation.Validated"; + protected static final String ANN_REQUIRES_VALIDATION = RequiresValidation.class.getName(); + + protected final ClassElement classElement; + protected final VisitorContext visitorContext; + protected final List beanDefinitionWriters = new LinkedList<>(); + + protected final AopHelper aopHelper; + + protected AbstractBeanElementCreator(ClassElement classElement, VisitorContext visitorContext) { + this.classElement = classElement; + this.visitorContext = visitorContext; + checkPackage(classElement); + String helperName = "io.micronaut.aop.writer.AopHelperImpl"; + try { + aopHelper = (AopHelper) ClassUtils.forName(helperName, getClass().getClassLoader()).get().newInstance(); + } catch (Exception e) { + throw new RuntimeException("Cannot create AOP helper class: " + helperName, e); + } + } + + @Override + public final Collection build() { + buildInternal(); + return beanDefinitionWriters; + } + + /** + * Build visitors. + */ + protected abstract void buildInternal(); + + private void checkPackage(ClassElement classElement) { + io.micronaut.inject.ast.PackageElement packageElement = classElement.getPackage(); + if (packageElement.isUnnamed()) { + throw new ProcessingException(classElement, "Micronaut beans cannot be in the default package"); + } + } + + /** + * Does the given metadata have AOP advice declared. + * + * @param annotationMetadata The annotation metadata + * @return True if it does + */ + @NextMajorVersion("Replace with InterceptedMethodUtil.hasAroundStereotype") + protected static boolean hasAroundStereotype(@Nullable AnnotationMetadata annotationMetadata) { + return hasAround(annotationMetadata, + annMetadata -> annMetadata.hasStereotype(AnnotationUtil.ANN_AROUND), + annMetdata -> annMetdata.getAnnotationValuesByName(AnnotationUtil.ANN_INTERCEPTOR_BINDING)); + } + + /** + * Does the given metadata have declared AOP advice. + * + * @param annotationMetadata The annotation metadata + * @return True if it does + */ + @NextMajorVersion("Replace with InterceptedMethodUtil.hasDeclaredAroundAdvice") + protected static boolean hasDeclaredAroundAdvice(@Nullable AnnotationMetadata annotationMetadata) { + return hasAround(annotationMetadata, + annMetadata -> annMetadata.hasDeclaredStereotype(AnnotationUtil.ANN_AROUND), + annMetdata -> annMetdata.getDeclaredAnnotationValuesByName(AnnotationUtil.ANN_INTERCEPTOR_BINDING)); + } + + private static boolean hasAround(@Nullable AnnotationMetadata annotationMetadata, + @NonNull Predicate hasFunction, + @NonNull Function>> interceptorBindingsFunction) { + if (annotationMetadata == null) { + return false; + } + if (hasFunction.test(annotationMetadata)) { + return true; + } else if (annotationMetadata.hasDeclaredStereotype(AnnotationUtil.ANN_INTERCEPTOR_BINDINGS)) { + return interceptorBindingsFunction.apply(annotationMetadata) + .stream().anyMatch(av -> + av.stringValue("kind").orElse("AROUND").equals("AROUND") + ); + } + + return false; + } + + protected void visitAnnotationMetadata(BeanDefinitionVisitor writer, AnnotationMetadata annotationMetadata) { + for (io.micronaut.core.annotation.AnnotationValue annotation : annotationMetadata.getAnnotationValuesByType(Requires.class)) { + annotation.stringValue(RequiresCondition.MEMBER_BEAN_PROPERTY) + .ifPresent(beanProperty -> { + annotation.stringValue(RequiresCondition.MEMBER_BEAN) + .map(className -> visitorContext.getClassElement(className, visitorContext.getElementAnnotationMetadataFactory().readOnly()).get()) + .ifPresent(classElement -> { + String requiredValue = annotation.stringValue().orElse(null); + String notEqualsValue = annotation.stringValue(RequiresCondition.MEMBER_NOT_EQUALS).orElse(null); + writer.visitAnnotationMemberPropertyInjectionPoint(classElement, beanProperty, requiredValue, notEqualsValue); + }); + }); + } + } + + public static AnnotationMetadata getElementAnnotationMetadata(MemberElement methodElement) { + // NOTE: if annotation processor modified the method's annotation + // annotationUtils.getAnnotationMetadata(method) will return AnnotationMetadataHierarchy of both method+class metadata + AnnotationMetadata annotationMetadata = methodElement.getTargetAnnotationMetadata(); + if (annotationMetadata instanceof AnnotationMetadataHierarchy) { + return annotationMetadata.getDeclaredMetadata(); + } + return annotationMetadata; + } + +} diff --git a/inject/src/main/java/io/micronaut/inject/processing/AopHelper.java b/inject/src/main/java/io/micronaut/inject/processing/AopHelper.java new file mode 100644 index 00000000000..9bccd52991b --- /dev/null +++ b/inject/src/main/java/io/micronaut/inject/processing/AopHelper.java @@ -0,0 +1,52 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.inject.processing; + +import io.micronaut.core.annotation.AnnotationMetadata; +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.NextMajorVersion; +import io.micronaut.inject.ast.ClassElement; +import io.micronaut.inject.ast.MethodElement; +import io.micronaut.inject.ast.TypedElement; +import io.micronaut.inject.visitor.VisitorContext; +import io.micronaut.inject.writer.BeanDefinitionVisitor; + +import java.util.concurrent.atomic.AtomicInteger; + +/** + * AOP helper to connect Inject module with AOP. + */ +@NextMajorVersion("Correct dependency graph") +@Internal +public interface AopHelper { + + BeanDefinitionVisitor visitAdaptedMethod(ClassElement classElement, + MethodElement sourceMethod, + AtomicInteger adaptedMethodIndex, + VisitorContext visitorContext); + + BeanDefinitionVisitor createIntroductionAopProxyWriter(ClassElement typeElement, + VisitorContext visitorContext); + + BeanDefinitionVisitor createAroundAopProxyWriter(BeanDefinitionVisitor existingWriter, + AnnotationMetadata producedAnnotationMetadata, + VisitorContext visitorContext, + boolean forceProxyTarget); + + boolean visitIntrospectedMethod(BeanDefinitionVisitor visitor, ClassElement typeElement, MethodElement methodElement); + + void visitAroundMethod(BeanDefinitionVisitor existingWriter, TypedElement beanType, MethodElement methodElement); +} diff --git a/inject/src/main/java/io/micronaut/inject/processing/AopIntroductionProxySupportedBeanElementCreator.java b/inject/src/main/java/io/micronaut/inject/processing/AopIntroductionProxySupportedBeanElementCreator.java new file mode 100644 index 00000000000..09da91fabff --- /dev/null +++ b/inject/src/main/java/io/micronaut/inject/processing/AopIntroductionProxySupportedBeanElementCreator.java @@ -0,0 +1,91 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.inject.processing; + +import io.micronaut.core.annotation.AnnotationMetadata; +import io.micronaut.core.annotation.Internal; +import io.micronaut.inject.ast.ClassElement; +import io.micronaut.inject.ast.MethodElement; +import io.micronaut.inject.ast.PropertyElement; +import io.micronaut.inject.visitor.VisitorContext; +import io.micronaut.inject.writer.BeanDefinitionVisitor; + +/** + * Ordinary bean with AOP introduction. + * + * @author Denis Stepanov + * @since 4.0.0 + */ +@Internal +final class AopIntroductionProxySupportedBeanElementCreator extends DeclaredBeanElementCreator { + + AopIntroductionProxySupportedBeanElementCreator(ClassElement classElement, VisitorContext visitorContext, boolean isAopProxy) { + super(classElement, visitorContext, isAopProxy); + } + + @Override + protected BeanDefinitionVisitor createBeanDefinitionVisitor() { + if (classElement.isFinal()) { + throw new ProcessingException(classElement, "Cannot apply AOP advice to final class. Class must be made non-final to support proxying: " + classElement.getName()); + } + aopProxyVisitor = aopHelper.createIntroductionAopProxyWriter(classElement, visitorContext); + beanDefinitionWriters.add(aopProxyVisitor); + MethodElement constructorElement = classElement.getPrimaryConstructor().orElse(null); + if (constructorElement != null) { + aopProxyVisitor.visitBeanDefinitionConstructor( + constructorElement, + constructorElement.isPrivate(), + visitorContext + ); + } else { + aopProxyVisitor.visitDefaultConstructor( + AnnotationMetadata.EMPTY_METADATA, + visitorContext + ); + } + return aopProxyVisitor; + } + + @Override + protected BeanDefinitionVisitor getAroundAopProxyVisitor(BeanDefinitionVisitor visitor, MethodElement methodElement) { + return aopProxyVisitor; + } + + @Override + protected boolean visitPropertyReadElement(BeanDefinitionVisitor visitor, PropertyElement propertyElement, MethodElement readElement) { + if (readElement.isAbstract() && aopHelper.visitIntrospectedMethod(visitor, classElement, readElement)) { + return true; + } + return super.visitPropertyReadElement(visitor, propertyElement, readElement); + } + + @Override + protected boolean visitPropertyWriteElement(BeanDefinitionVisitor visitor, PropertyElement propertyElement, MethodElement writeElement) { + if (writeElement.isAbstract() && aopHelper.visitIntrospectedMethod(visitor, classElement, writeElement)) { + return true; + } + return super.visitPropertyWriteElement(visitor, propertyElement, writeElement); + } + + @Override + protected boolean visitMethod(BeanDefinitionVisitor visitor, MethodElement methodElement) { + if (methodElement.isAbstract() && aopHelper.visitIntrospectedMethod(visitor, classElement, methodElement)) { + return true; + } + return super.visitMethod(visitor, methodElement); + } + +} diff --git a/inject/src/main/java/io/micronaut/inject/processing/BeanDefinitionCreator.java b/inject/src/main/java/io/micronaut/inject/processing/BeanDefinitionCreator.java new file mode 100644 index 00000000000..de9605d553d --- /dev/null +++ b/inject/src/main/java/io/micronaut/inject/processing/BeanDefinitionCreator.java @@ -0,0 +1,35 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.inject.processing; + +import io.micronaut.inject.writer.BeanDefinitionVisitor; + +import java.util.Collection; + +/** + * Builder that produces multiple Bean definitions represented by {@link BeanDefinitionVisitor}. + * + * @author Denis Stepanov + * @since 4.0.0 + */ +public interface BeanDefinitionCreator { + + /** + * @return produces Bean definitions + */ + Collection build(); + +} diff --git a/inject/src/main/java/io/micronaut/inject/processing/BeanDefinitionCreatorFactory.java b/inject/src/main/java/io/micronaut/inject/processing/BeanDefinitionCreatorFactory.java new file mode 100644 index 00000000000..a1e6dc43a8c --- /dev/null +++ b/inject/src/main/java/io/micronaut/inject/processing/BeanDefinitionCreatorFactory.java @@ -0,0 +1,169 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.inject.processing; + +import io.micronaut.context.annotation.Bean; +import io.micronaut.context.annotation.DefaultScope; +import io.micronaut.context.annotation.Executable; +import io.micronaut.context.annotation.Factory; +import io.micronaut.context.annotation.Property; +import io.micronaut.context.annotation.Value; +import io.micronaut.core.annotation.AnnotationMetadata; +import io.micronaut.core.annotation.AnnotationUtil; +import io.micronaut.core.annotation.AnnotationValue; +import io.micronaut.core.annotation.NonNull; +import io.micronaut.core.annotation.Nullable; +import io.micronaut.inject.ast.ClassElement; +import io.micronaut.inject.ast.ElementQuery; +import io.micronaut.inject.visitor.VisitorContext; + +import java.lang.annotation.Annotation; +import java.util.Collections; +import java.util.List; +import java.util.function.Function; +import java.util.function.Predicate; + +/** + * Bean definition builder factory. + * + * @author Denis Stepanov + * @since 4.0.0 + */ +public abstract class BeanDefinitionCreatorFactory { + + @NonNull + public static BeanDefinitionCreator produce(ClassElement classElement, VisitorContext visitorContext) { + boolean isAbstract = classElement.isAbstract(); + boolean isIntroduction = classElement.hasStereotype(AnnotationUtil.ANN_INTRODUCTION); + if (ConfigurationReaderBeanElementCreator.isConfigurationProperties(classElement)) { + if (classElement.isInterface()) { + return new IntroductionInterfaceBeanElementCreator(classElement, visitorContext, null); + } + return new ConfigurationReaderBeanElementCreator(classElement, visitorContext); + } + boolean aopProxyType = !isAbstract && isAopProxyType(classElement); + if (!isAbstract && classElement.hasStereotype(Factory.class)) { + return new FactoryBeanElementCreator(classElement, visitorContext, aopProxyType); + } + if (aopProxyType) { + if (isIntroduction) { + return new AopIntroductionProxySupportedBeanElementCreator(classElement, visitorContext, true); + } + return new DeclaredBeanElementCreator(classElement, visitorContext, true); + } + if (isIntroduction) { + if (classElement.isInterface()) { + return new IntroductionInterfaceBeanElementCreator(classElement, visitorContext, null); + } + return new AopIntroductionProxySupportedBeanElementCreator(classElement, visitorContext, false); + } + // NOTE: In Micronaut 3 abstract classes are allowed to be beans, but are not pickup to be beans just by having methods or fields with @Inject + if (isDeclaredBean(classElement) || (!isAbstract && (containsInjectMethod(classElement) || containsInjectField(classElement)))) { + if (classElement.hasStereotype("groovy.lang.Singleton")) { + throw new ProcessingException(classElement, "Class annotated with groovy.lang.Singleton instead of jakarta.inject.Singleton. Import jakarta.inject.Singleton to use Micronaut Dependency Injection."); + } + return new DeclaredBeanElementCreator(classElement, visitorContext, false); + } + return Collections::emptyList; + } + + private static boolean isDeclaredBean(ClassElement classElement) { + if (isDeclaredBeanInMetadata(classElement.getAnnotationMetadata())) { + return true; + } + if (classElement.isAbstract()) { + return false; + } + return classElement.hasStereotype(Executable.class) || + classElement.hasStereotype(AnnotationUtil.QUALIFIER) || + classElement.getPrimaryConstructor().map(constructor -> constructor.hasStereotype(AnnotationUtil.INJECT)).orElse(false); + } + + private static boolean containsInjectMethod(ClassElement classElement) { + return classElement.getEnclosedElement( + ElementQuery.ALL_METHODS.onlyConcrete() + .onlyDeclared() + .annotated(annotationMetadata -> annotationMetadata.hasDeclaredAnnotation(AnnotationUtil.INJECT)) + ).isPresent(); + } + + private static boolean containsInjectField(ClassElement classElement) { + return classElement.getEnclosedElement( + ElementQuery.ALL_FIELDS + .onlyDeclared() + .annotated(BeanDefinitionCreatorFactory::containsInjectPoint) + ).isPresent(); + } + + private static boolean containsInjectPoint(AnnotationMetadata annotationMetadata) { + return annotationMetadata.hasStereotype(AnnotationUtil.INJECT) + || annotationMetadata.hasStereotype(Value.class) + || annotationMetadata.hasStereotype(Property.class); + } + + private static boolean isAopProxyType(ClassElement classElement) { + return !classElement.isAssignable("io.micronaut.aop.Interceptor") && hasAroundStereotype(classElement.getAnnotationMetadata()); + } + + public static boolean isDeclaredBeanInMetadata(AnnotationMetadata concreteClassMetadata) { + return concreteClassMetadata.hasDeclaredStereotype(Bean.class) || + concreteClassMetadata.hasStereotype(AnnotationUtil.SCOPE) || + concreteClassMetadata.hasStereotype(DefaultScope.class); + } + + /** + * Does the given metadata have AOP advice declared. + * + * @param annotationMetadata The annotation metadata + * @return True if it does + */ + protected static boolean hasAroundStereotype(@Nullable AnnotationMetadata annotationMetadata) { + return hasAround(annotationMetadata, + annMetadata -> annMetadata.hasStereotype(AnnotationUtil.ANN_AROUND), + annMetdata -> annMetdata.getAnnotationValuesByName(AnnotationUtil.ANN_INTERCEPTOR_BINDING)); + } + + /** + * Does the given metadata have declared AOP advice. + * + * @param annotationMetadata The annotation metadata + * @return True if it does + */ + protected static boolean hasDeclaredAroundAdvice(@Nullable AnnotationMetadata annotationMetadata) { + return hasAround(annotationMetadata, + annMetadata -> annMetadata.hasDeclaredStereotype(AnnotationUtil.ANN_AROUND), + annMetdata -> annMetdata.getDeclaredAnnotationValuesByName(AnnotationUtil.ANN_INTERCEPTOR_BINDING)); + } + + private static boolean hasAround(@Nullable AnnotationMetadata annotationMetadata, + @NonNull Predicate hasFunction, + @NonNull Function>> interceptorBindingsFunction) { + if (annotationMetadata == null) { + return false; + } + if (hasFunction.test(annotationMetadata)) { + return true; + } else if (annotationMetadata.hasDeclaredStereotype(AnnotationUtil.ANN_INTERCEPTOR_BINDINGS)) { + return interceptorBindingsFunction.apply(annotationMetadata) + .stream().anyMatch(av -> + av.stringValue("kind").orElse("AROUND").equals("AROUND") + ); + } + + return false; + } + +} diff --git a/inject/src/main/java/io/micronaut/inject/processing/ConfigurationReaderBeanElementCreator.java b/inject/src/main/java/io/micronaut/inject/processing/ConfigurationReaderBeanElementCreator.java new file mode 100644 index 00000000000..c547d9113d1 --- /dev/null +++ b/inject/src/main/java/io/micronaut/inject/processing/ConfigurationReaderBeanElementCreator.java @@ -0,0 +1,313 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.inject.processing; + +import io.micronaut.context.annotation.ConfigurationBuilder; +import io.micronaut.context.annotation.ConfigurationInject; +import io.micronaut.context.annotation.ConfigurationReader; +import io.micronaut.context.annotation.Executable; +import io.micronaut.context.annotation.Parameter; +import io.micronaut.context.annotation.Property; +import io.micronaut.context.annotation.Value; +import io.micronaut.core.annotation.AnnotationMetadata; +import io.micronaut.core.annotation.AnnotationUtil; +import io.micronaut.core.annotation.AnnotationValue; +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.bind.annotation.Bindable; +import io.micronaut.inject.annotation.AnnotationMetadataHierarchy; +import io.micronaut.inject.annotation.MutableAnnotationMetadata; +import io.micronaut.inject.ast.BeanPropertiesQuery; +import io.micronaut.inject.ast.ClassElement; +import io.micronaut.inject.ast.FieldElement; +import io.micronaut.inject.ast.MemberElement; +import io.micronaut.inject.ast.MethodElement; +import io.micronaut.inject.ast.ParameterElement; +import io.micronaut.inject.ast.PropertyElement; +import io.micronaut.inject.configuration.ConfigurationMetadataBuilder; +import io.micronaut.inject.configuration.PropertyMetadata; +import io.micronaut.inject.visitor.VisitorContext; +import io.micronaut.inject.writer.BeanDefinitionVisitor; + +import java.time.Duration; +import java.util.Arrays; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.TimeUnit; + +/** + * Configuration reader bean builder. + * + * @author Denis Stepanov + * @since 4.0.0 + */ +@Internal +final class ConfigurationReaderBeanElementCreator extends DeclaredBeanElementCreator { + + private static final List CONSTRUCTOR_PARAMETERS_INJECTION_ANN = + Arrays.asList(Property.class.getName(), Value.class.getName(), Parameter.class.getName(), AnnotationUtil.QUALIFIER, AnnotationUtil.INJECT); + + private final ConfigurationMetadataBuilder metadataBuilder = ConfigurationMetadataBuilder.INSTANCE; + + ConfigurationReaderBeanElementCreator(ClassElement classElement, VisitorContext visitorContext) { + super(classElement, visitorContext, false); + } + + @Override + protected void applyConfigurationInjectionIfNecessary(BeanDefinitionVisitor visitor, + MethodElement constructor) { + if (!classElement.isRecord() && !constructor.hasAnnotation(ConfigurationInject.class)) { + return; + } + if (classElement.isRecord()) { + final List beanProperties = constructor + .getDeclaringType() + .getBeanProperties(); + final ParameterElement[] parameters = constructor.getParameters(); + if (beanProperties.size() == parameters.length) { + for (int i = 0; i < parameters.length; i++) { + ParameterElement parameter = parameters[i]; + final PropertyElement bp = beanProperties.get(i); + if (CONSTRUCTOR_PARAMETERS_INJECTION_ANN.stream().noneMatch(bp::hasStereotype)) { + processConfigurationConstructorParameter(parameter); + } + } + if (constructor.hasStereotype(ANN_REQUIRES_VALIDATION)) { + visitor.setValidated(true); + } + return; + } + } + processConfigurationInjectionConstructor(visitor, constructor); + } + + private void processConfigurationInjectionConstructor(BeanDefinitionVisitor visitor, + MethodElement constructor) { + for (ParameterElement parameter : constructor.getParameters()) { + if (CONSTRUCTOR_PARAMETERS_INJECTION_ANN.stream().noneMatch(parameter::hasStereotype)) { + processConfigurationConstructorParameter(parameter); + } + } + if (constructor.hasStereotype(ANN_REQUIRES_VALIDATION)) { + visitor.setValidated(true); + } + } + + private void processConfigurationConstructorParameter(ParameterElement parameter) { + if (!parameter.hasStereotype(AnnotationUtil.SCOPE)) { + final PropertyMetadata pm = metadataBuilder.visitProperty( + parameter.getMethodElement().getOwningType(), + parameter.getMethodElement().getDeclaringType(), + parameter.getGenericType(), + parameter.getName(), parameter.getDocumentation().orElse(null), + parameter.stringValue(Bindable.class, "defaultValue").orElse(null) + ); + parameter.annotate(Property.class, (builder) -> builder.member("name", pm.getPath())); + } + } + + public static boolean isConfigurationProperties(ClassElement classElement) { + return classElement.hasStereotype(ConfigurationReader.class); + } + + @Override + protected boolean visitAopMethod(BeanDefinitionVisitor visitor, MethodElement methodElement) { + return false; + } + + @Override + protected boolean processAsProperties() { + return true; + } + + @Override + protected boolean visitProperty(BeanDefinitionVisitor visitor, PropertyElement propertyElement) { + Optional readMethod = propertyElement.getReadMethod(); + Optional field = propertyElement.getField(); + if (propertyElement.hasStereotype(ConfigurationBuilder.class)) { + // Exclude / ignore shouldn't affect builders + if (readMethod.isPresent()) { + MethodElement methodElement = readMethod.get(); + ClassElement builderType = methodElement.getReturnType(); + visitor.visitConfigBuilderMethod( + builderType, + methodElement.getName(), + propertyElement.getAnnotationMetadata(), + null, + builderType.isInterface() + ); + visitConfigurationBuilder(visitor, propertyElement, builderType); + return true; + } + if (field.isPresent()) { + FieldElement fieldElement = field.get(); + if (fieldElement.isAccessible(classElement)) { + ClassElement builderType = fieldElement.getType(); + visitor.visitConfigBuilderField( + builderType, + fieldElement.getName(), + fieldElement.getAnnotationMetadata(), + metadataBuilder, + builderType.isInterface() + ); + visitConfigurationBuilder(visitor, propertyElement, builderType); + return true; + } + throw new ProcessingException(fieldElement, "ConfigurationBuilder applied to a non accessible (private or package-private/protected in a different package) field must have a corresponding non-private getter method."); + } + } else if (!propertyElement.isExcluded()) { + boolean claimed = false; + Optional writeMethod = propertyElement.getWriteMethod(); + if (propertyElement.getWriteAccessKind() == PropertyElement.AccessKind.METHOD && writeMethod.isPresent()) { + visitor.setValidated(visitor.isValidated() || propertyElement.hasAnnotation(ANN_REQUIRES_VALIDATION)); + MethodElement methodElement = writeMethod.get(); + ParameterElement parameter = methodElement.getParameters()[0]; + AnnotationMetadata annotationMetadata = new AnnotationMetadataHierarchy( + propertyElement, + parameter + ).merge(); + annotationMetadata = calculatePath(propertyElement, methodElement, annotationMetadata); + AnnotationMetadata finalAnnotationMetadata = annotationMetadata; + methodElement = methodElement + .withAnnotationMetadata(annotationMetadata) + .withParameters( + Arrays.stream(methodElement.getParameters()) + .map(p -> p == parameter ? parameter.withAnnotationMetadata(finalAnnotationMetadata) : p) + .toArray(ParameterElement[]::new) + ); + visitor.visitSetterValue(methodElement.getDeclaringType(), methodElement, annotationMetadata, methodElement.isReflectionRequired(classElement), true); + claimed = true; + } else if (propertyElement.getWriteAccessKind() == PropertyElement.AccessKind.FIELD && field.isPresent()) { + visitor.setValidated(visitor.isValidated() || propertyElement.hasAnnotation(ANN_REQUIRES_VALIDATION)); + FieldElement fieldElement = field.get(); + AnnotationMetadata annotationMetadata = MutableAnnotationMetadata.of(propertyElement.getAnnotationMetadata()); + annotationMetadata = calculatePath(propertyElement, fieldElement, annotationMetadata); + visitor.visitFieldValue(fieldElement.getDeclaringType(), fieldElement.withAnnotationMetadata(annotationMetadata), fieldElement.isReflectionRequired(classElement), true); + claimed = true; + } + if (readMethod.isPresent()) { + MethodElement methodElement = readMethod.get(); + if (methodElement.hasStereotype(Executable.class)) { + claimed |= visitExecutableMethod(visitor, methodElement); + } + } + return claimed; + } + return false; + } + + @Override + protected boolean visitField(BeanDefinitionVisitor visitor, FieldElement fieldElement) { + if (fieldElement.hasStereotype(ConfigurationBuilder.class) && !fieldElement.isAccessible(classElement)) { + throw new ProcessingException(fieldElement, "ConfigurationBuilder applied to a non accessible (private or package-private/protected in a different package) field must have a corresponding non-private getter method."); + } + return super.visitField(visitor, fieldElement); + } + + private AnnotationMetadata calculatePath(PropertyElement propertyElement, MemberElement writeMember, AnnotationMetadata annotationMetadata) { + String path = metadataBuilder.visitProperty( + writeMember.getOwningType(), + writeMember.getDeclaringType(), + propertyElement.getGenericType(), + propertyElement.getName(), + propertyElement.getDocumentation().orElse(null), + null + ).getPath(); + return visitorContext.getAnnotationMetadataBuilder().annotate(annotationMetadata, AnnotationValue.builder(Property.class).member("name", path).build()); + } + + @Override + protected boolean isInjectPointMethod(MemberElement memberElement) { + return super.isInjectPointMethod(memberElement) || memberElement.hasDeclaredStereotype(ConfigurationInject.class); + } + + private void visitConfigurationBuilder(BeanDefinitionVisitor visitor, + MemberElement builderElement, + ClassElement builderType) { + try { + String configurationPrefix = builderElement.stringValue(ConfigurationBuilder.class).map(v -> v + ".").orElse(""); + builderType.getBeanProperties(BeanPropertiesQuery.of(builderElement)) + .stream() + .filter(propertyElement -> { + if (propertyElement.isExcluded()) { + return false; + } + Optional writeMethod = propertyElement.getWriteMethod(); + if (!writeMethod.isPresent()) { + return false; + } + MethodElement methodElement = writeMethod.get(); + if (methodElement.hasStereotype(Deprecated.class) || !methodElement.isPublic()) { + return false; + } + return methodElement.getParameters().length <= 2; + }).forEach(propertyElement -> { + MethodElement methodElement = propertyElement.getWriteMethod().get(); + String propertyName = propertyElement.getName(); + ParameterElement[] params = methodElement.getParameters(); + int paramCount = params.length; + if (paramCount < 2) { + ParameterElement parameterElement = paramCount == 1 ? params[0] : null; + ClassElement parameterElementType = parameterElement == null ? null : parameterElement.getGenericType(); + + PropertyMetadata metadata = metadataBuilder.visitProperty( + classElement, + builderElement.getDeclaringType(), + propertyElement.getType(), + configurationPrefix + propertyName, + null, + null + ); + + visitor.visitConfigBuilderMethod( + propertyName, + methodElement.getReturnType(), + methodElement.getSimpleName(), + parameterElementType, + parameterElementType != null ? parameterElementType.getTypeArguments() : null, + metadata.getPath() + ); + } else if (paramCount == 2) { + // check the params are a long and a TimeUnit + ParameterElement first = params[0]; + ParameterElement second = params[1]; + ClassElement firstParamType = first.getType(); + ClassElement secondParamType = second.getType(); + + if (firstParamType.getSimpleName().equals("long") && secondParamType.isAssignable(TimeUnit.class)) { + PropertyMetadata metadata = metadataBuilder.visitProperty( + classElement, + methodElement.getDeclaringType(), + visitorContext.getClassElement(Duration.class.getName()).get(), + configurationPrefix + propertyName, + null, + null + ); + + visitor.visitConfigBuilderDurationMethod( + propertyName, + methodElement.getReturnType(), + methodElement.getSimpleName(), + metadata.getPath() + ); + } + } + }); + } finally { + visitor.visitConfigBuilderEnd(); + } + } + +} diff --git a/inject/src/main/java/io/micronaut/inject/processing/DeclaredBeanElementCreator.java b/inject/src/main/java/io/micronaut/inject/processing/DeclaredBeanElementCreator.java new file mode 100644 index 00000000000..4808666edff --- /dev/null +++ b/inject/src/main/java/io/micronaut/inject/processing/DeclaredBeanElementCreator.java @@ -0,0 +1,508 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.inject.processing; + +import io.micronaut.context.annotation.Executable; +import io.micronaut.context.annotation.Property; +import io.micronaut.context.annotation.Value; +import io.micronaut.core.annotation.AnnotationMetadata; +import io.micronaut.core.annotation.AnnotationUtil; +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.NextMajorVersion; +import io.micronaut.core.annotation.NonNull; +import io.micronaut.core.annotation.Nullable; +import io.micronaut.inject.ast.ClassElement; +import io.micronaut.inject.ast.ElementQuery; +import io.micronaut.inject.ast.FieldElement; +import io.micronaut.inject.ast.MemberElement; +import io.micronaut.inject.ast.MethodElement; +import io.micronaut.inject.ast.PropertyElement; +import io.micronaut.inject.visitor.VisitorContext; +import io.micronaut.inject.writer.BeanDefinitionVisitor; +import io.micronaut.inject.writer.BeanDefinitionWriter; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * Ordinary declared bean. + * + * @author Denis Stepanov + * @since 4.0.0 + */ +@Internal +class DeclaredBeanElementCreator extends AbstractBeanElementCreator { + + protected BeanDefinitionVisitor aopProxyVisitor; + protected final boolean isAopProxy; + private final AtomicInteger adaptedMethodIndex = new AtomicInteger(0); + + protected DeclaredBeanElementCreator(ClassElement classElement, VisitorContext visitorContext, boolean isAopProxy) { + super(classElement, visitorContext); + this.isAopProxy = isAopProxy; + } + + @Override + public final void buildInternal() { + BeanDefinitionVisitor beanDefinitionVisitor = createBeanDefinitionVisitor(); + if (isAopProxy) { + // Always create AOP proxy + getAroundAopProxyVisitor(beanDefinitionVisitor, null); + } + build(beanDefinitionVisitor); + } + + /** + * Create a bean definition visitor. + * + * @return the visitor + */ + @NonNull + protected BeanDefinitionVisitor createBeanDefinitionVisitor() { + BeanDefinitionVisitor beanDefinitionWriter = new BeanDefinitionWriter(classElement, visitorContext); + beanDefinitionWriters.add(beanDefinitionWriter); + beanDefinitionWriter.visitTypeArguments(classElement.getAllTypeArguments()); + visitAnnotationMetadata(beanDefinitionWriter, classElement.getAnnotationMetadata()); + MethodElement constructorElement = classElement.getPrimaryConstructor().orElse(null); + if (constructorElement != null) { + applyConfigurationInjectionIfNecessary(beanDefinitionWriter, constructorElement); + beanDefinitionWriter.visitBeanDefinitionConstructor(constructorElement, constructorElement.isPrivate(), visitorContext); + } else { + beanDefinitionWriter.visitDefaultConstructor(AnnotationMetadata.EMPTY_METADATA, visitorContext); + } + return beanDefinitionWriter; + } + + /** + * Create an AOP proxy visitor. + * + * @param visitor the parent visitor + * @param methodElement the method that is originating the AOP proxy + * @return The AOP proxy visitor + */ + protected BeanDefinitionVisitor getAroundAopProxyVisitor(BeanDefinitionVisitor visitor, @Nullable MethodElement methodElement) { + if (aopProxyVisitor == null) { + if (classElement.isFinal()) { + throw new ProcessingException(classElement, "Cannot apply AOP advice to final class. Class must be made non-final to support proxying: " + classElement.getName()); + } + aopProxyVisitor = aopHelper.createAroundAopProxyWriter( + visitor, + isAopProxy || methodElement == null ? classElement.getAnnotationMetadata() : methodElement.getAnnotationMetadata(), + visitorContext, + false + ); + beanDefinitionWriters.add(aopProxyVisitor); + MethodElement constructorElement = classElement.getPrimaryConstructor().orElse(null); + if (constructorElement != null) { + aopProxyVisitor.visitBeanDefinitionConstructor( + constructorElement, + constructorElement.isPrivate(), + visitorContext + ); + } else { + aopProxyVisitor.visitDefaultConstructor( + AnnotationMetadata.EMPTY_METADATA, + visitorContext + ); + } + aopProxyVisitor.visitSuperBeanDefinition(visitor.getBeanDefinitionName()); + } + return aopProxyVisitor; + } + + /** + * @return true if the class should be processed as a properties bean + */ + protected boolean processAsProperties() { + return false; + } + + private void build(BeanDefinitionVisitor visitor) { + Set processedFields = new HashSet<>(); + if (!processAsProperties()) { + for (PropertyElement propertyElement : classElement.getSyntheticBeanProperties()) { + propertyElement.getField().ifPresent(processedFields::add); + visitPropertyInternal(visitor, propertyElement); + } + } + ElementQuery fieldsQuery = ElementQuery.ALL_FIELDS.includeHiddenElements(); + ElementQuery membersQuery = ElementQuery.ALL_METHODS; + boolean processAsProperties = processAsProperties(); + if (processAsProperties) { + fieldsQuery = fieldsQuery.excludePropertyElements(); + membersQuery = membersQuery.excludePropertyElements(); + for (PropertyElement propertyElement : classElement.getBeanProperties()) { + visitPropertyInternal(visitor, propertyElement); + } + } + List fields = new ArrayList<>(classElement.getEnclosedElements(fieldsQuery)); + fields.removeIf(processedFields::contains); + List declaredFields = new ArrayList<>(fields.size()); + // Process subtype fields first + for (FieldElement fieldElement : fields) { + if (fieldElement.getDeclaringType().equals(classElement)) { + declaredFields.add(fieldElement); + } else { + visitFieldInternal(visitor, fieldElement); + } + } + List methods = classElement.getEnclosedElements(membersQuery); + List declaredMethods = new ArrayList<>(methods.size()); + // Process subtype methods first + for (MethodElement methodElement : methods) { + if (methodElement.getDeclaringType().equals(classElement)) { + declaredMethods.add(methodElement); + } else { + visitMethodInternal(visitor, methodElement); + } + } + for (FieldElement fieldElement : declaredFields) { + visitFieldInternal(visitor, fieldElement); + } + for (MethodElement methodElement : declaredMethods) { + visitMethodInternal(visitor, methodElement); + } + } + + private void visitFieldInternal(BeanDefinitionVisitor visitor, FieldElement fieldElement) { + boolean claimed = visitField(visitor, fieldElement); + if (claimed) { + addOriginatingElementIfNecessary(visitor, fieldElement); + } + } + + private void visitMethodInternal(BeanDefinitionVisitor visitor, MethodElement methodElement) { + if (methodElement.hasAnnotation(ANN_REQUIRES_VALIDATION)) { + methodElement.annotate(ANN_VALIDATED); + } + boolean claimed = visitMethod(visitor, methodElement); + if (claimed) { + addOriginatingElementIfNecessary(visitor, methodElement); + } + } + + private void visitPropertyInternal(BeanDefinitionVisitor visitor, PropertyElement propertyElement) { + if (propertyElement.hasAnnotation(ANN_REQUIRES_VALIDATION)) { + propertyElement.annotate(ANN_VALIDATED); + } + boolean claimed = visitProperty(visitor, propertyElement); + if (claimed) { + propertyElement.getReadMethod().ifPresent(element -> addOriginatingElementIfNecessary(visitor, element)); + propertyElement.getWriteMethod().ifPresent(element -> addOriginatingElementIfNecessary(visitor, element)); + propertyElement.getField().ifPresent(element -> addOriginatingElementIfNecessary(visitor, element)); + } + } + + /** + * Visit a property. + * + * @param visitor The visitor + * @param propertyElement The property + * @return true if processed + */ + protected boolean visitProperty(BeanDefinitionVisitor visitor, PropertyElement propertyElement) { + boolean claimed = false; + Optional writeMethod = propertyElement.getWriteMethod(); + if (writeMethod.isPresent()) { + MethodElement writeElement = writeMethod.get(); + claimed |= visitPropertyWriteElement(visitor, propertyElement, writeElement); + } + Optional readMethod = propertyElement.getReadMethod(); + if (readMethod.isPresent()) { + MethodElement readElement = readMethod.get(); + claimed |= visitPropertyReadElement(visitor, propertyElement, readElement); + } + // Process property's field if no methods were processed + Optional field = propertyElement.getField(); + if (!claimed && field.isPresent()) { + FieldElement writeElement = field.get(); + claimed = visitField(visitor, writeElement); + } + return claimed; + } + + /** + * Visit a property read element. + * + * @param visitor The visitor + * @param propertyElement The property + * @param readElement The read element + * @return true if processed + */ + protected boolean visitPropertyReadElement(BeanDefinitionVisitor visitor, + PropertyElement propertyElement, + MethodElement readElement) { + return visitAopAndExecutableMethod(visitor, readElement); + } + + /** + * Visit a property write element. + * + * @param visitor The visitor + * @param propertyElement The property + * @param writeElement The write element + * @return true if processed + */ + @NextMajorVersion("Require @ReflectiveAccess for private methods in Micronaut 4") + protected boolean visitPropertyWriteElement(BeanDefinitionVisitor visitor, + PropertyElement propertyElement, + MethodElement writeElement) { + if (visitInjectAndLifecycleMethod(visitor, writeElement)) { + return true; + } else if (!writeElement.isStatic() && getElementAnnotationMetadata(writeElement).hasStereotype(AnnotationUtil.QUALIFIER)) { + staticMethodCheck(writeElement); + // TODO: Require @ReflectiveAccess for private methods in Micronaut 4 + visitMethodInjectionPoint(visitor, writeElement); + return true; + } + return visitAopAndExecutableMethod(visitor, writeElement); + } + + /** + * Visit a method. + * + * @param visitor The visitor + * @param methodElement The method + * @return true if processed + */ + protected boolean visitMethod(BeanDefinitionVisitor visitor, MethodElement methodElement) { + if (visitInjectAndLifecycleMethod(visitor, methodElement)) { + return true; + } + return visitAopAndExecutableMethod(visitor, methodElement); + } + + @NextMajorVersion("Require @ReflectiveAccess for private methods in Micronaut 4") + private boolean visitInjectAndLifecycleMethod(BeanDefinitionVisitor visitor, MethodElement methodElement) { + // All the cases above are using executable methods + boolean claimed = false; + if (methodElement.hasDeclaredAnnotation(AnnotationUtil.POST_CONSTRUCT)) { + staticMethodCheck(methodElement); + // TODO: Require @ReflectiveAccess for private methods in Micronaut 4 + visitor.visitPostConstructMethod( + methodElement.getDeclaringType(), + methodElement, + methodElement.isReflectionRequired(classElement), + visitorContext); + claimed = true; + } + if (methodElement.hasDeclaredAnnotation(AnnotationUtil.PRE_DESTROY)) { + staticMethodCheck(methodElement); + // TODO: Require @ReflectiveAccess for private methods in Micronaut 4 + visitor.visitPreDestroyMethod( + methodElement.getDeclaringType(), + methodElement, + methodElement.isReflectionRequired(classElement), + visitorContext + ); + claimed = true; + } + if (claimed) { + return true; + } + if (!methodElement.isStatic() && isInjectPointMethod(methodElement)) { + staticMethodCheck(methodElement); + // TODO: Require @ReflectiveAccess for private methods in Micronaut 4 + visitMethodInjectionPoint(visitor, methodElement); + return true; + } + return false; + } + + private void visitMethodInjectionPoint(BeanDefinitionVisitor visitor, MethodElement methodElement) { + applyConfigurationInjectionIfNecessary(visitor, methodElement); + visitor.visitMethodInjectionPoint( + methodElement.getDeclaringType(), + methodElement, + methodElement.isReflectionRequired(classElement), + visitorContext + ); + } + + private boolean visitAopAndExecutableMethod(BeanDefinitionVisitor visitor, MethodElement methodElement) { + if (methodElement.isStatic() && isExplicitlyAnnotatedAsExecutable(methodElement)) { + // Only allow static executable methods when it's explicitly annotated with Executable.class + return false; + } + // This method requires pre-processing. See Executable#processOnStartup() + boolean preprocess = methodElement.isTrue(Executable.class, "processOnStartup"); + if (preprocess) { + visitor.setRequiresMethodProcessing(true); + } + if (methodElement.hasStereotype("io.micronaut.aop.Adapter")) { + staticMethodCheck(methodElement); + visitAdaptedMethod(visitor, methodElement); + return true; + } + if (visitAopMethod(visitor, methodElement)) { + return true; + } + return visitExecutableMethod(visitor, methodElement); + } + + /** + * Visit an AOP method. + * + * @param visitor The visitor + * @param methodElement The method + * @return true if processed + */ + protected boolean visitAopMethod(BeanDefinitionVisitor visitor, MethodElement methodElement) { + boolean aopDefinedOnClassAndPublicMethod = isAopProxy && (methodElement.isPublic() || methodElement.isPackagePrivate()); + AnnotationMetadata methodAnnotationMetadata = getElementAnnotationMetadata(methodElement); + if (aopDefinedOnClassAndPublicMethod || + !isAopProxy && hasAroundStereotype(methodAnnotationMetadata) || + hasDeclaredAroundAdvice(methodAnnotationMetadata) && !classElement.isAbstract()) { + if (methodElement.isFinal()) { + if (hasDeclaredAroundAdvice(methodAnnotationMetadata)) { + throw new ProcessingException(methodElement, "Method defines AOP advice but is declared final. Change the method to be non-final in order for AOP advice to be applied."); + } else if (!methodElement.isSynthetic() && aopDefinedOnClassAndPublicMethod && isDeclaredInThisClass(methodElement)) { + throw new ProcessingException(methodElement, "Public method inherits AOP advice but is declared final. Either make the method non-public or apply AOP advice only to public methods declared on the class."); + } + return false; + } else if (methodElement.isPrivate()) { + throw new ProcessingException(methodElement, "Method annotated as executable but is declared private. Change the method to be non-private in order for AOP advice to be applied."); + } else if (methodElement.isStatic()) { + throw new ProcessingException(methodElement, "Method defines AOP advice but is declared static"); + } + BeanDefinitionVisitor aopProxyVisitor = getAroundAopProxyVisitor(visitor, methodElement); + aopHelper.visitAroundMethod(aopProxyVisitor, classElement, methodElement); + return true; + } + return false; + } + + /** + * Apply configuration injection for the constructor. + * + * @param visitor The visitor + * @param constructor The constructor + */ + protected void applyConfigurationInjectionIfNecessary(BeanDefinitionVisitor visitor, + MethodElement constructor) { + } + + /** + * Is inject point method? + * + * @param memberElement The method + * @return true if it is + */ + protected boolean isInjectPointMethod(MemberElement memberElement) { + return memberElement.hasDeclaredStereotype(AnnotationUtil.INJECT); + } + + private void staticMethodCheck(MethodElement methodElement) { + if (methodElement.isStatic()) { + if (!isExplicitlyAnnotatedAsExecutable(methodElement)) { + throw new ProcessingException(methodElement, "Static methods only allowed when annotated with @Executable"); + } + failIfMethodNotAccessible(methodElement); + } + } + + private void failIfMethodNotAccessible(MethodElement methodElement) { + if (!methodElement.isAccessible(classElement)) { + throw new ProcessingException(methodElement, "Method is not accessible for the invocation. To invoke the method using reflection annotate it with @ReflectiveAccess"); + } + } + + private static boolean isExplicitlyAnnotatedAsExecutable(MethodElement methodElement) { + return !getElementAnnotationMetadata(methodElement).hasDeclaredAnnotation(Executable.class); + } + + /** + * Visit a field. + * + * @param visitor The visitor + * @param fieldElement The field + * @return true if processed + */ + protected boolean visitField(BeanDefinitionVisitor visitor, FieldElement fieldElement) { + if (fieldElement.isStatic() || fieldElement.isFinal()) { + return false; + } + AnnotationMetadata fieldAnnotationMetadata = fieldElement.getAnnotationMetadata(); + if (fieldAnnotationMetadata.hasStereotype(Value.class) || fieldAnnotationMetadata.hasStereotype(Property.class)) { + visitor.visitFieldValue(fieldElement.getDeclaringType(), fieldElement, fieldElement.isReflectionRequired(classElement), false); + return true; + } + if (fieldAnnotationMetadata.hasStereotype(AnnotationUtil.INJECT) + || fieldAnnotationMetadata.hasDeclaredStereotype(AnnotationUtil.QUALIFIER)) { + visitor.visitFieldInjectionPoint(fieldElement.getDeclaringType(), fieldElement, fieldElement.isReflectionRequired(classElement)); + return true; + } + return false; + } + + private void addOriginatingElementIfNecessary(BeanDefinitionVisitor writer, MemberElement memberElement) { + if (!memberElement.isSynthetic() && !isDeclaredInThisClass(memberElement)) { + writer.addOriginatingElement(memberElement.getDeclaringType()); + } + } + + /** + * Visit an executable method. + * + * @param visitor The visitor + * @param methodElement The method + * @return true if processed + */ + protected boolean visitExecutableMethod(BeanDefinitionVisitor visitor, MethodElement methodElement) { + if (!methodElement.hasStereotype(Executable.class)) { + return false; + } + if (getElementAnnotationMetadata(methodElement).hasStereotype(Executable.class)) { + // @Executable annotated on the method + // Throw error if it cannot be accessed without the reflection + if (!methodElement.isAccessible()) { + throw new ProcessingException(methodElement, "Method annotated as executable but is declared private. To invoke the method using reflection annotate it with @ReflectiveAccess"); + } + } else if (!isDeclaredInThisClass(methodElement) && !methodElement.getDeclaringType().hasStereotype(Executable.class)) { + // @Executable not annotated on the declared class or method + // Only include public methods + if (!methodElement.isPublic()) { + return false; + } + } + // else + // @Executable annotated on the class + // only include own accessible methods or the ones annotated with @ReflectiveAccess + if (methodElement.isAccessible() + || !methodElement.isPrivate() && methodElement.getClass().getSimpleName().contains("Groovy")) { + visitor.visitExecutableMethod(classElement, methodElement, visitorContext); + } + return true; + } + + private boolean isDeclaredInThisClass(MemberElement memberElement) { + return classElement.equals(memberElement.getDeclaringType()); + } + + private void visitAdaptedMethod(BeanDefinitionVisitor visitor, MethodElement sourceMethod) { + BeanDefinitionVisitor adapter = aopHelper + .visitAdaptedMethod(classElement, sourceMethod, adaptedMethodIndex, visitorContext); + if (adapter != null) { + visitor.visitExecutableMethod(sourceMethod.getDeclaringType(), sourceMethod, visitorContext); + beanDefinitionWriters.add(adapter); + } + } + +} diff --git a/inject/src/main/java/io/micronaut/inject/processing/FactoryBeanElementCreator.java b/inject/src/main/java/io/micronaut/inject/processing/FactoryBeanElementCreator.java new file mode 100644 index 00000000000..3e1dec15594 --- /dev/null +++ b/inject/src/main/java/io/micronaut/inject/processing/FactoryBeanElementCreator.java @@ -0,0 +1,301 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.inject.processing; + +import io.micronaut.context.annotation.Bean; +import io.micronaut.context.annotation.ConfigurationReader; +import io.micronaut.context.annotation.EachProperty; +import io.micronaut.context.annotation.Executable; +import io.micronaut.context.annotation.Value; +import io.micronaut.core.annotation.AnnotationMetadata; +import io.micronaut.core.annotation.AnnotationUtil; +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.util.StringUtils; +import io.micronaut.inject.annotation.AnnotationMetadataHierarchy; +import io.micronaut.inject.annotation.MutableAnnotationMetadata; +import io.micronaut.inject.ast.ClassElement; +import io.micronaut.inject.ast.ElementQuery; +import io.micronaut.inject.ast.FieldElement; +import io.micronaut.inject.ast.MemberElement; +import io.micronaut.inject.ast.MethodElement; +import io.micronaut.inject.ast.ParameterElement; +import io.micronaut.inject.ast.PropertyElement; +import io.micronaut.inject.configuration.ConfigurationUtils; +import io.micronaut.inject.visitor.VisitorContext; +import io.micronaut.inject.writer.BeanDefinitionVisitor; +import io.micronaut.inject.writer.BeanDefinitionWriter; +import io.micronaut.inject.writer.OriginatingElements; + +import java.util.Optional; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * Factory bean builder. + * + * @author Denis Stepanov + * @since 4.0.0 + */ +@Internal +final class FactoryBeanElementCreator extends DeclaredBeanElementCreator { + + private final AtomicInteger factoryMethodIndex = new AtomicInteger(); + + FactoryBeanElementCreator(ClassElement classElement, VisitorContext visitorContext, boolean isAopProxy) { + super(classElement, visitorContext, isAopProxy); + } + + @Override + protected boolean visitMethod(BeanDefinitionVisitor visitor, MethodElement methodElement) { + if (methodElement.hasDeclaredStereotype(Bean.class.getName(), AnnotationUtil.SCOPE)) { + visitBeanFactoryElement(visitor, methodElement.getGenericReturnType(), methodElement); + return true; + } + return super.visitMethod(visitor, methodElement); + } + + @Override + protected boolean visitField(BeanDefinitionVisitor visitor, FieldElement fieldElement) { + if (fieldElement.hasDeclaredStereotype(Bean.class.getName())) { + if (!fieldElement.isAccessible(classElement)) { + throw new ProcessingException(fieldElement, "Beans produced from fields cannot be private"); + } + visitBeanFactoryElement(visitor, fieldElement.getType(), fieldElement); + return true; + } + return super.visitField(visitor, fieldElement); + } + + @Override + protected boolean visitPropertyReadElement(BeanDefinitionVisitor visitor, PropertyElement propertyElement, MethodElement readElement) { + if (readElement.hasDeclaredStereotype(Bean.class.getName())) { + visitBeanFactoryElement(visitor, readElement.getGenericReturnType(), readElement); + return true; + } + return super.visitPropertyReadElement(visitor, propertyElement, readElement); + } + + @Override + protected boolean visitPropertyWriteElement(BeanDefinitionVisitor visitor, PropertyElement propertyElement, MethodElement writeElement) { + if (writeElement.hasDeclaredStereotype(Bean.class.getName())) { + return true; + } + return super.visitPropertyWriteElement(visitor, propertyElement, writeElement); + } + + void visitBeanFactoryElement(BeanDefinitionVisitor visitor, ClassElement producedType, MemberElement producingElement) { + if (producedType.isPrimitive()) { + BeanDefinitionWriter producedBeanDefinitionWriter = new BeanDefinitionWriter(producingElement, + OriginatingElements.of(producingElement), + visitorContext, + factoryMethodIndex.getAndIncrement() + ); + buildProducedBeanDefinition(producedBeanDefinitionWriter, producedType, producingElement, producingElement.getAnnotationMetadata()); + } else { + AnnotationMetadata producedTypeAnnotationMetadata = createProducedTypeAnnotationMetadata(producedType, producingElement); + producedType = producedType.withAnnotationMetadata(producedTypeAnnotationMetadata); + AnnotationMetadata producingElementAnnotationMetadata = createProducingElementAnnotationMetadata(producedTypeAnnotationMetadata); + producingElement = producingElement.withAnnotationMetadata(producingElementAnnotationMetadata); + + BeanDefinitionWriter producedBeanDefinitionWriter = new BeanDefinitionWriter( + producingElement, + OriginatingElements.of(producingElement), + visitorContext, + factoryMethodIndex.getAndIncrement() + ); + +// producingElement = producingElement.withAnnotationMetadata(producedTypeAnnotationMetadata); + buildProducedBeanDefinition(producedBeanDefinitionWriter, producedType, producingElement, producedType.getAnnotationMetadata()); + + if (producingElement instanceof MethodElement) { + MethodElement methodElement = (MethodElement) producingElement; + if (isAopProxy && visitAopMethod(visitor, methodElement)) { + return; + } + visitExecutableMethod(visitor, methodElement); + } + } + } + + private AnnotationMetadata createProducingElementAnnotationMetadata(AnnotationMetadata producedAnnotationMetadata) { + MutableAnnotationMetadata factoryClassAnnotationMetadata = MutableAnnotationMetadata.of(classElement.getAnnotationMetadata()); + + boolean modifiedFactoryClassAnnotationMetadata = false; + if (classElement.hasStereotype(AnnotationUtil.QUALIFIER)) { + // Don't propagate any qualifiers to the factories + for (String qualifier : classElement.getAnnotationNamesByStereotype(AnnotationUtil.QUALIFIER)) { + if (!producedAnnotationMetadata.hasStereotype(qualifier)) { + factoryClassAnnotationMetadata.removeAnnotation(qualifier); + modifiedFactoryClassAnnotationMetadata = true; + } + } + } + if (modifiedFactoryClassAnnotationMetadata) { + return new AnnotationMetadataHierarchy(factoryClassAnnotationMetadata, producedAnnotationMetadata); + } + return new AnnotationMetadataHierarchy(classElement, producedAnnotationMetadata); + } + + private AnnotationMetadata createProducedTypeAnnotationMetadata(ClassElement producedType, MemberElement producingElement) { + // Original logic is to combine producing element annotation metadata (method or field) with the produced type's annotation metadata + MutableAnnotationMetadata producedAnnotationMetadata = new AnnotationMetadataHierarchy( + producedType.getAnnotationMetadata(), + getElementAnnotationMetadata(producingElement) + ).merge(); + AnnotationMetadata producedTypeAnnotationMetadata = producedType.getAnnotationMetadata(); + AnnotationMetadata elementAnnotationMetadata = getElementAnnotationMetadata(producingElement); + + cleanupScopeAndQualifierAnnotations(producedAnnotationMetadata, producedTypeAnnotationMetadata, elementAnnotationMetadata); + return producedAnnotationMetadata; + } + + private void buildProducedBeanDefinition(BeanDefinitionWriter producedBeanDefinitionWriter, + ClassElement producedType, + MemberElement producingElement, + AnnotationMetadata producedAnnotationMetadata) { + + visitAnnotationMetadata(producedBeanDefinitionWriter, producedAnnotationMetadata); + producedBeanDefinitionWriter.visitTypeArguments(producedType.getAllTypeArguments()); + + beanDefinitionWriters.add(producedBeanDefinitionWriter); + + if (producedType.hasStereotype(EachProperty.class)) { + producedType.annotate(ConfigurationReader.class, builder -> builder.member(ConfigurationReader.PREFIX, ConfigurationUtils.getRequiredTypePath(producedType))); + } + + if (producingElement instanceof MethodElement) { + producedBeanDefinitionWriter.visitBeanFactoryMethod(classElement, (MethodElement) producingElement); + } else { + producedBeanDefinitionWriter.visitBeanFactoryField(classElement, (FieldElement) producingElement); + } + + if (hasAroundStereotype(producedAnnotationMetadata) && !producedType.isAssignable("io.micronaut.aop.Interceptor")) { + if (producedType.isArray()) { + throw new ProcessingException(producingElement, "Cannot apply AOP advice to arrays"); + } + if (producedType.isPrimitive()) { + throw new ProcessingException(producingElement, "Cannot apply AOP advice to primitive beans"); + } + if (producedType.isFinal()) { + throw new ProcessingException(producingElement, "Cannot apply AOP advice to final class. Class must be made non-final to support proxying: " + producedType.getName()); + } + + MethodElement constructorElement = producedType.getPrimaryConstructor().orElse(null); + if (!producedType.isInterface() && constructorElement != null && constructorElement.getParameters().length > 0) { + final String proxyTargetMode = producedAnnotationMetadata.stringValue(AnnotationUtil.ANN_AROUND, "proxyTargetMode").orElse("ERROR"); + switch (proxyTargetMode) { + case "ALLOW": + allowProxyConstruction(constructorElement); + break; + case "WARN": + allowProxyConstruction(constructorElement); + visitorContext.warn("The produced type of a @Factory method has constructor arguments and is proxied. " + + "This can lead to unexpected behaviour. See the javadoc for Around.ProxyTargetConstructorMode for more information: " + producingElement.getName(), producingElement); + break; + case "ERROR": + default: + throw new ProcessingException(producingElement, "The produced type from a factory which has AOP proxy advice specified must define an accessible no arguments constructor. " + + "Proxying types with constructor arguments can lead to unexpected behaviour. See the javadoc for for Around.ProxyTargetConstructorMode for more information and possible solutions: " + producingElement.getName()); + } + } + + BeanDefinitionVisitor aopProxyWriter = aopHelper.createAroundAopProxyWriter(producedBeanDefinitionWriter, producedAnnotationMetadata, visitorContext, true); + if (constructorElement != null) { + aopProxyWriter.visitBeanDefinitionConstructor(constructorElement, constructorElement.isReflectionRequired(), visitorContext); + } else { + aopProxyWriter.visitDefaultConstructor(AnnotationMetadata.EMPTY_METADATA, visitorContext); + } + aopProxyWriter.visitSuperBeanDefinitionFactory(producedBeanDefinitionWriter.getBeanDefinitionName()); + aopProxyWriter.visitTypeArguments(producedType.getAllTypeArguments()); + beanDefinitionWriters.add(aopProxyWriter); + + producedType.getEnclosedElements(ElementQuery.ALL_METHODS) + .stream() + .filter(m -> m.isPublic() && !m.isFinal() && !m.isStatic()) + .forEach(methodElement -> aopHelper.visitAroundMethod(aopProxyWriter, methodElement.getDeclaringType(), methodElement)); + + } else if (producedAnnotationMetadata.hasStereotype(Executable.class)) { + if (producedType.isArray()) { + throw new ProcessingException(producingElement, "Using '@Executable' is not allowed on array type beans"); + } + if (producedType.isPrimitive()) { + throw new ProcessingException(producingElement, "Using '@Executable' is not allowed on primitive type beans"); + } + producedType.getEnclosedElements(ElementQuery.ALL_METHODS) + .forEach(methodElement -> producedBeanDefinitionWriter.visitExecutableMethod(methodElement.getDeclaringType(), methodElement, visitorContext)); + } + + if (producedAnnotationMetadata.isPresent(Bean.class, "preDestroy")) { + if (producedType.isArray()) { + throw new ProcessingException(producingElement, "Using 'preDestroy' is not allowed on array type beans"); + } + if (producedType.isPrimitive()) { + throw new ProcessingException(producingElement, "Using 'preDestroy' is not allowed on primitive type beans"); + } + + producedType.getValue(Bean.class, "preDestroy", String.class).ifPresent(destroyMethodName -> { + if (StringUtils.isNotEmpty(destroyMethodName)) { + final Optional destroyMethod = producedType.getEnclosedElement(ElementQuery.ALL_METHODS.onlyAccessible(classElement) + .onlyInstance() + .named(destroyMethodName) + .filter((e) -> !e.hasParameters())); + if (destroyMethod.isPresent()) { + MethodElement destroyMethodElement = destroyMethod.get(); + producedBeanDefinitionWriter.visitPreDestroyMethod(producedType, destroyMethodElement, false, visitorContext); + } else { + throw new ProcessingException(producingElement, "@Bean method defines a preDestroy method that does not exist or is not public: " + destroyMethodName); + } + } + }); + } + } + + private void allowProxyConstruction(MethodElement constructor) { + final ParameterElement[] parameters = constructor.getParameters(); + for (ParameterElement parameter : parameters) { + if (parameter.isPrimitive() && !parameter.isArray()) { + final String name = parameter.getType().getName(); + if ("boolean".equals(name)) { + parameter.annotate(Value.class, (builder) -> builder.value(false)); + } else { + parameter.annotate(Value.class, (builder) -> builder.value(0)); + } + } else { + // allow null + parameter.annotate(AnnotationUtil.NULLABLE); + parameter.removeAnnotation(AnnotationUtil.NON_NULL); + } + } + } + + private void cleanupScopeAndQualifierAnnotations(MutableAnnotationMetadata producedAnnotationMetadata, AnnotationMetadata producedTypeAnnotationMetadata, AnnotationMetadata producingElementAnnotationMetadata) { + // If the producing element defines a scope don't inherit it from the type + if (producingElementAnnotationMetadata.hasStereotype(AnnotationUtil.SCOPE) || producingElementAnnotationMetadata.hasStereotype(AnnotationUtil.QUALIFIER)) { + // The producing element is declaring the scope then we should remove the scope defined by the type + for (String scope : producedTypeAnnotationMetadata.getAnnotationNamesByStereotype(AnnotationUtil.SCOPE)) { + if (!producingElementAnnotationMetadata.hasStereotype(scope)) { + producedAnnotationMetadata.removeAnnotation(scope); + } + } + // Remove any qualifier coming from the type + for (String qualifier : producedTypeAnnotationMetadata.getAnnotationNamesByStereotype(AnnotationUtil.QUALIFIER)) { + if (!producingElementAnnotationMetadata.hasStereotype(qualifier)) { + producedAnnotationMetadata.removeAnnotation(qualifier); + } + } + } + } + +} diff --git a/inject/src/main/java/io/micronaut/inject/processing/IntroductionInterfaceBeanElementCreator.java b/inject/src/main/java/io/micronaut/inject/processing/IntroductionInterfaceBeanElementCreator.java new file mode 100644 index 00000000000..01c712bccc5 --- /dev/null +++ b/inject/src/main/java/io/micronaut/inject/processing/IntroductionInterfaceBeanElementCreator.java @@ -0,0 +1,91 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.inject.processing; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.inject.ast.ClassElement; +import io.micronaut.inject.ast.ElementQuery; +import io.micronaut.inject.ast.MethodElement; +import io.micronaut.inject.visitor.VisitorContext; +import io.micronaut.inject.writer.BeanDefinitionVisitor; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** + * Introduction interface proxy builder. + * + * @author Denis Stepanov + * @since 4.0.0 + */ +@Internal +final class IntroductionInterfaceBeanElementCreator extends AbstractBeanElementCreator { + + private final String factoryBeanDefinitionName; + + IntroductionInterfaceBeanElementCreator(ClassElement classElement, VisitorContext visitorContext, String factoryBeanDefinitionName) { + super(classElement, visitorContext); + this.factoryBeanDefinitionName = factoryBeanDefinitionName; + } + + @Override + public void buildInternal() { + BeanDefinitionVisitor aopProxyWriter = aopHelper.createIntroductionAopProxyWriter(classElement, visitorContext); + aopProxyWriter.visitTypeArguments(classElement.getAllTypeArguments()); + + // Because we add validated interceptor in some cases, this needs to run before the constructor visit + if (classElement.hasAnnotation(ANN_REQUIRES_VALIDATION)) { + if (ConfigurationReaderBeanElementCreator.isConfigurationProperties(classElement)) { + // Configuration beans are validated at the startup and don't require validation advice + aopProxyWriter.setValidated(true); + } else { + for (MethodElement methodElement : classElement.getEnclosedElements(ElementQuery.ALL_METHODS.annotated(am -> am.hasAnnotation(ANN_REQUIRES_VALIDATION)))) { + methodElement.annotate(AbstractBeanElementCreator.ANN_VALIDATED); + } + } + } + + MethodElement constructorElement = classElement.getPrimaryConstructor().orElse(null); + if (constructorElement != null) { + aopProxyWriter.visitBeanDefinitionConstructor(constructorElement, constructorElement.isReflectionRequired(), visitorContext); + } else { + aopProxyWriter.visitDefaultConstructor(classElement, visitorContext); + } + if (factoryBeanDefinitionName != null) { + aopProxyWriter.visitSuperBeanDefinitionFactory(factoryBeanDefinitionName); + } + + // The introduction will include overridden methods* (find(List) <- find(Iterable)*) but ordinary class introduction doesn't + // Because of the caching we need to process declared methods first + Set processed = new HashSet<>(); + List declaredEnclosedElements = classElement.getEnclosedElements(ElementQuery.ALL_METHODS.includeHiddenElements().includeOverriddenMethods().onlyDeclared()); + for (MethodElement methodElement : declaredEnclosedElements) { + aopHelper.visitIntrospectedMethod(aopProxyWriter, classElement, methodElement); + processed.add(methodElement); + } + List otherEnclosedElements = classElement.getEnclosedElements(ElementQuery.ALL_METHODS.includeHiddenElements().includeOverriddenMethods()); + for (MethodElement methodElement : otherEnclosedElements) { + if (processed.contains(methodElement)) { + continue; + } + aopHelper.visitIntrospectedMethod(aopProxyWriter, classElement, methodElement); + } + + beanDefinitionWriters.add(aopProxyWriter); + } + +} diff --git a/inject/src/main/java/io/micronaut/inject/processing/ProcessingException.java b/inject/src/main/java/io/micronaut/inject/processing/ProcessingException.java new file mode 100644 index 00000000000..9d16d4ee7b4 --- /dev/null +++ b/inject/src/main/java/io/micronaut/inject/processing/ProcessingException.java @@ -0,0 +1,48 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.inject.processing; + +import io.micronaut.inject.ast.Element; + +/** + * The exception can be used to stop the processing and display an error associated to the element. + * + * @author Denis Stepanov + * @since 4.0.0 + */ +public final class ProcessingException extends RuntimeException { + + private final Object originatingElement; + private final String message; + + public ProcessingException(Element element, String message) { + this(element.getNativeType(), message); + } + + public ProcessingException(Object originatingElement, String message) { + this.originatingElement = originatingElement; + this.message = message; + } + + public Object getOriginatingElement() { + return originatingElement; + } + + @Override + public String getMessage() { + return message; + } +} diff --git a/inject/src/main/java/io/micronaut/inject/validation/RequiresValidation.java b/inject/src/main/java/io/micronaut/inject/validation/RequiresValidation.java new file mode 100644 index 00000000000..1fb3f43698a --- /dev/null +++ b/inject/src/main/java/io/micronaut/inject/validation/RequiresValidation.java @@ -0,0 +1,38 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.inject.validation; + +import io.micronaut.core.annotation.Internal; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.RetentionPolicy.SOURCE; + +/** + * Internal method marks a type, method or a field for validation. + * + * @author Denis Stepanov + * @since 4.0.0 + */ +@Documented +@Retention(SOURCE) +@Target({ElementType.METHOD, ElementType.TYPE, ElementType.FIELD}) +@Internal +public @interface RequiresValidation { +} diff --git a/inject/src/main/java/io/micronaut/inject/visitor/VisitorContext.java b/inject/src/main/java/io/micronaut/inject/visitor/VisitorContext.java index b0451388361..02c9e82f131 100644 --- a/inject/src/main/java/io/micronaut/inject/visitor/VisitorContext.java +++ b/inject/src/main/java/io/micronaut/inject/visitor/VisitorContext.java @@ -16,11 +16,14 @@ package io.micronaut.inject.visitor; import io.micronaut.core.annotation.Experimental; +import io.micronaut.core.annotation.Internal; import io.micronaut.core.annotation.NonNull; import io.micronaut.core.annotation.Nullable; import io.micronaut.core.convert.value.MutableConvertibleValues; +import io.micronaut.inject.annotation.AbstractAnnotationMetadataBuilder; import io.micronaut.inject.ast.ClassElement; import io.micronaut.inject.ast.Element; +import io.micronaut.inject.ast.ElementAnnotationMetadataFactory; import io.micronaut.inject.ast.ElementFactory; import io.micronaut.inject.writer.ClassWriterOutputVisitor; import io.micronaut.inject.writer.GeneratedFile; @@ -50,10 +53,31 @@ public interface VisitorContext extends MutableConvertibleValues, ClassW /** * Gets the element factory for this visitor context. + * * @return The element factory * @since 2.3.0 */ - @NonNull ElementFactory getElementFactory(); + @NonNull + ElementFactory getElementFactory(); + + /** + * Gets the element annotation metadata factory. + * + * @return The element annotation metadata factory + * @see 4.0.0 + */ + @NonNull + ElementAnnotationMetadataFactory getElementAnnotationMetadataFactory(); + + /** + * Gets the annotation metadata builder. + * + * @return The annotation metadata builder + * @see 4.0.0 + */ + @Internal + @NonNull + AbstractAnnotationMetadataBuilder getAnnotationMetadataBuilder(); /** * Allows printing informational messages. @@ -101,7 +125,7 @@ public interface VisitorContext extends MutableConvertibleValues, ClassW */ @Override @Experimental - Optional visitMetaInfFile(String path, Element...originatingElements); + Optional visitMetaInfFile(String path, Element... originatingElements); /** * Visit a file that will be located within the generated source directory. @@ -192,6 +216,30 @@ default Optional getClassElement(String name) { return Optional.empty(); } + /** + * This method will lookup another class element by name. If it cannot be found an empty optional will be returned. + * + * @param name The name + * @param annotationMetadataFactory The element annotation metadata factory + * @return The class element + * @since 4.0.0 + */ + default Optional getClassElement(String name, ElementAnnotationMetadataFactory annotationMetadataFactory) { + return Optional.empty(); + } + + /** + * This method will lookup another class element by name. If it cannot be found an exception thrown. + * + * @param name The name + * @param annotationMetadataFactory The element annotation metadata factory + * @return The class element + * @since 4.0.0 + */ + default ClassElement getRequiredClassElement(String name, ElementAnnotationMetadataFactory annotationMetadataFactory) { + return getClassElement(name, annotationMetadataFactory).orElseThrow(() -> new IllegalStateException("Unknown type: " + name)); + } + /** * This method will lookup another class element by name. If it cannot be found an empty optional will be returned. * @@ -207,7 +255,8 @@ default Optional getClassElement(Class type) { /** * Find all the classes within the given package and having the given annotation. - * @param aPackage The package + * + * @param aPackage The package * @param stereotypes The stereotypes * @return The class elements */ @@ -218,6 +267,7 @@ default Optional getClassElement(Class type) { /** * The annotation processor environment custom options. *

All options names MUST start with {@link VisitorContext#MICRONAUT_BASE_OPTION_NAME}

+ * * @return A Map with annotation processor runtime options * @see javax.annotation.processing.ProcessingEnvironment#getOptions() */ diff --git a/inject/src/main/java/io/micronaut/inject/writer/AbstractAnnotationMetadataWriter.java b/inject/src/main/java/io/micronaut/inject/writer/AbstractAnnotationMetadataWriter.java index bf4398c4f2a..c773e6b563b 100644 --- a/inject/src/main/java/io/micronaut/inject/writer/AbstractAnnotationMetadataWriter.java +++ b/inject/src/main/java/io/micronaut/inject/writer/AbstractAnnotationMetadataWriter.java @@ -16,8 +16,9 @@ package io.micronaut.inject.writer; import io.micronaut.core.annotation.AnnotationMetadata; -import io.micronaut.inject.annotation.AnnotationMetadataHierarchy; import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.NonNull; +import io.micronaut.inject.annotation.AnnotationMetadataHierarchy; import io.micronaut.inject.annotation.AnnotationMetadataReference; import io.micronaut.inject.annotation.AnnotationMetadataWriter; import io.micronaut.inject.annotation.DefaultAnnotationMetadata; @@ -27,8 +28,6 @@ import org.objectweb.asm.Type; import org.objectweb.asm.commons.GeneratorAdapter; -import io.micronaut.core.annotation.NonNull; - import java.util.HashMap; import java.util.Map; @@ -65,7 +64,7 @@ protected AbstractAnnotationMetadataWriter( boolean writeAnnotationDefaults) { super(originatingElements); this.targetClassType = getTypeReferenceForName(className); - this.annotationMetadata = annotationMetadata; + this.annotationMetadata = annotationMetadata.getTargetAnnotationMetadata(); this.writeAnnotationDefault = writeAnnotationDefaults; } @@ -82,7 +81,7 @@ protected AbstractAnnotationMetadataWriter( boolean writeAnnotationDefaults) { super(new Element[]{ originatingElement }); this.targetClassType = getTypeReferenceForName(className); - this.annotationMetadata = annotationMetadata; + this.annotationMetadata = annotationMetadata.getTargetAnnotationMetadata(); this.writeAnnotationDefault = writeAnnotationDefaults; } @@ -95,7 +94,8 @@ protected void writeGetAnnotationMetadataMethod(ClassWriter classWriter) { // in order to save memory of a method doesn't have any annotations of its own but merely references class metadata // then we setup an annotation metadata reference from the method to the class (or inherited method) metadata - if (annotationMetadata == AnnotationMetadata.EMPTY_METADATA) { + AnnotationMetadata annotationMetadata = this.annotationMetadata.getTargetAnnotationMetadata(); + if (annotationMetadata.isEmpty()) { annotationMetadataMethod.getStatic(Type.getType(AnnotationMetadata.class), "EMPTY_METADATA", Type.getType(AnnotationMetadata.class)); } else if (annotationMetadata instanceof AnnotationMetadataReference) { AnnotationMetadataReference reference = (AnnotationMetadataReference) annotationMetadata; @@ -137,16 +137,28 @@ protected void writeAnnotationMetadataStaticInitializer(ClassWriter classWriter, // write the static initializers for the annotation metadata GeneratorAdapter staticInit = visitStaticInitializer(classWriter); staticInit.visitCode(); - if (writeAnnotationDefault && annotationMetadata instanceof DefaultAnnotationMetadata) { - DefaultAnnotationMetadata dam = (DefaultAnnotationMetadata) annotationMetadata; - AnnotationMetadataWriter.writeAnnotationDefaults( - targetClassType, - classWriter, - staticInit, - dam, - defaults, - loadTypeMethods - ); + if (writeAnnotationDefault) { + AnnotationMetadata annotationMetadata = this.annotationMetadata.getTargetAnnotationMetadata(); + if (annotationMetadata.isEmpty()) { + staticInit.getStatic(Type.getType(AnnotationMetadata.class), "EMPTY_METADATA", Type.getType(AnnotationMetadata.class)); + } else { + if (annotationMetadata instanceof AnnotationMetadataHierarchy) { + annotationMetadata = ((AnnotationMetadataHierarchy) annotationMetadata).merge(); + } + if (annotationMetadata instanceof DefaultAnnotationMetadata) { + DefaultAnnotationMetadata dam = (DefaultAnnotationMetadata) annotationMetadata; + AnnotationMetadataWriter.writeAnnotationDefaults( + targetClassType, + classWriter, + staticInit, + dam, + defaults, + loadTypeMethods + ); + } else { + throw new IllegalStateException("Unknown annotation metadata: " + annotationMetadata); + } + } } staticInit.visitLabel(new Label()); initializeAnnotationMetadata(staticInit, classWriter, defaults); @@ -165,7 +177,10 @@ protected void initializeAnnotationMetadata(GeneratorAdapter staticInit, ClassWr Type annotationMetadataType = Type.getType(AnnotationMetadata.class); classWriter.visitField(ACC_PUBLIC | ACC_FINAL | ACC_STATIC, FIELD_ANNOTATION_METADATA, annotationMetadataType.getDescriptor(), null, null); - if (annotationMetadata instanceof DefaultAnnotationMetadata) { + AnnotationMetadata annotationMetadata = this.annotationMetadata.getTargetAnnotationMetadata(); + if (annotationMetadata.isEmpty()) { + staticInit.getStatic(Type.getType(AnnotationMetadata.class), "EMPTY_METADATA", Type.getType(AnnotationMetadata.class)); + } else if (annotationMetadata instanceof DefaultAnnotationMetadata) { AnnotationMetadataWriter.instantiateNewMetadata( targetClassType, classWriter, @@ -184,7 +199,7 @@ protected void initializeAnnotationMetadata(GeneratorAdapter staticInit, ClassWr loadTypeMethods ); } else { - staticInit.getStatic(Type.getType(AnnotationMetadata.class), "EMPTY_METADATA", Type.getType(AnnotationMetadata.class)); + throw new IllegalStateException("Unknown annotation metadata: " + annotationMetadata); } staticInit.putStatic(targetClassType, FIELD_ANNOTATION_METADATA, annotationMetadataType); diff --git a/inject/src/main/java/io/micronaut/inject/writer/AbstractBeanDefinitionBuilder.java b/inject/src/main/java/io/micronaut/inject/writer/AbstractBeanDefinitionBuilder.java index 81a62ad0474..daff56b068c 100644 --- a/inject/src/main/java/io/micronaut/inject/writer/AbstractBeanDefinitionBuilder.java +++ b/inject/src/main/java/io/micronaut/inject/writer/AbstractBeanDefinitionBuilder.java @@ -34,7 +34,7 @@ import io.micronaut.inject.ast.ClassElement; import io.micronaut.inject.ast.ConstructorElement; import io.micronaut.inject.ast.Element; -import io.micronaut.inject.ast.ElementFactory; +import io.micronaut.inject.ast.ElementAnnotationMetadataFactory; import io.micronaut.inject.ast.ElementModifier; import io.micronaut.inject.ast.ElementQuery; import io.micronaut.inject.ast.FieldElement; @@ -97,8 +97,9 @@ public abstract class AbstractBeanDefinitionBuilder implements BeanElementBuilde } } }; - protected final ConfigurationMetadataBuilder metadataBuilder; + protected final ConfigurationMetadataBuilder metadataBuilder; protected final VisitorContext visitorContext; + protected final ElementAnnotationMetadataFactory elementAnnotationMetadataFactory; private final Element originatingElement; private final ClassElement originatingType; private final ClassElement beanType; @@ -118,17 +119,21 @@ public abstract class AbstractBeanDefinitionBuilder implements BeanElementBuilde /** * Default constructor. - * @param originatingElement The originating element - * @param beanType The bean type - * @param metadataBuilder the metadata builder - * @param visitorContext the visitor context + * + * @param originatingElement The originating element + * @param beanType The bean type + * @param metadataBuilder the metadata builder + * @param visitorContext the visitor context + * @param elementAnnotationMetadataFactory The element annotation metadata factory */ protected AbstractBeanDefinitionBuilder( - Element originatingElement, - ClassElement beanType, - ConfigurationMetadataBuilder metadataBuilder, - VisitorContext visitorContext) { + Element originatingElement, + ClassElement beanType, + ConfigurationMetadataBuilder metadataBuilder, + VisitorContext visitorContext, + ElementAnnotationMetadataFactory elementAnnotationMetadataFactory) { this.originatingElement = originatingElement; + this.elementAnnotationMetadataFactory = elementAnnotationMetadataFactory; if (originatingElement instanceof MethodElement) { this.originatingType = ((MethodElement) originatingElement).getDeclaringType(); } else if (originatingElement instanceof ClassElement) { @@ -141,12 +146,7 @@ protected AbstractBeanDefinitionBuilder( this.visitorContext = visitorContext; this.identifier = BEAN_COUNTER.computeIfAbsent(beanType.getName(), (s) -> new AtomicInteger(0)) .getAndIncrement(); - final AnnotationMetadata annotationMetadata = beanType.getAnnotationMetadata(); - if (annotationMetadata instanceof MutableAnnotationMetadata) { - this.annotationMetadata = ((MutableAnnotationMetadata) annotationMetadata).clone(); - } else { - this.annotationMetadata = new MutableAnnotationMetadata(); - } + this.annotationMetadata = MutableAnnotationMetadata.of(beanType.getAnnotationMetadata()); this.annotationMetadata.addDeclaredAnnotation(Bean.class.getName(), Collections.emptyMap()); this.constructorElement = initConstructor(beanType); } @@ -565,14 +565,10 @@ protected void visitInterceptedMethods(BiConsumer c @SuppressWarnings({"rawtypes", "unchecked"}) private void handleMethod(ClassElement beanClass, MethodElement method, BiConsumer consumer) { - ElementFactory elementFactory = visitorContext.getElementFactory(); - AnnotationMetadataHierarchy fusedMetadata = new AnnotationMetadataHierarchy(getAnnotationMetadata(), method.getAnnotationMetadata()); - MethodElement finalMethod = elementFactory.newMethodElement( - beanClass, - method.getNativeType(), - fusedMetadata + consumer.accept( + beanClass, + method.withAnnotationMetadata(new AnnotationMetadataHierarchy(getAnnotationMetadata(), method.getAnnotationMetadata())) ); - consumer.accept(beanClass, finalMethod); } /** @@ -810,7 +806,6 @@ protected BeanDefinitionVisitor createBeanDefinitionWriter() { return new BeanDefinitionWriter( this, OriginatingElements.of(originatingElement), - metadataBuilder, visitorContext, identifier ); @@ -823,8 +818,7 @@ private void visitField(BeanDefinitionVisitor beanDefinitionWriter, beanDefinitionWriter.visitFieldValue( injectedField.getDeclaringType(), injectedField, - ibf.isReflectionRequired(), - ibf.isDeclaredNullable() + ibf.isReflectionRequired(), ibf.isDeclaredNullable() ); } else { beanDefinitionWriter.visitFieldInjectionPoint( @@ -880,18 +874,13 @@ private void visitField(BeanDefinitionVisitor beanDefinitionWriter, * @param The element type */ private abstract class InternalBeanElement implements Element { + protected AnnotationMetadata currentMetadata; private final E element; private final MutableAnnotationMetadata elementMetadata; - private AnnotationMetadata currentMetadata; - private InternalBeanElement(E element) { + private InternalBeanElement(E element, MutableAnnotationMetadata elementMetadata) { this.element = element; - final AnnotationMetadata annotationMetadata = element.getAnnotationMetadata(); - if (annotationMetadata instanceof MutableAnnotationMetadata) { - this.elementMetadata = ((MutableAnnotationMetadata) annotationMetadata).clone(); - } else { - this.elementMetadata = new MutableAnnotationMetadata(); - } + this.elementMetadata = elementMetadata; } @Override @@ -1000,7 +989,7 @@ private InternalBeanElementMethod(MethodElement methodElement, boolean requiresR private InternalBeanElementMethod(MethodElement methodElement, boolean requiresReflection, BeanParameterElement[] beanParameters) { - super(methodElement); + super(methodElement, MutableAnnotationMetadata.of(methodElement.getAnnotationMetadata().getDeclaredMetadata())); this.methodElement = methodElement; this.requiresReflection = requiresReflection; this.beanParameters = beanParameters; @@ -1133,8 +1122,14 @@ public ClassElement getGenericReturnType() { @NonNull @Override - public MethodElement withNewParameters(@NonNull ParameterElement... newParameters) { - this.beanParameters = initBeanParameters(ArrayUtils.concat(beanParameters, newParameters)); + public MethodElement withParameters(@NonNull ParameterElement... newParameters) { + this.beanParameters = initBeanParameters(newParameters); + return this; + } + + @Override + public MethodElement withAnnotationMetadata(AnnotationMetadata annotationMetadata) { + this.currentMetadata = annotationMetadata; return this; } @@ -1162,7 +1157,7 @@ private final class InternalBeanConstructorElement extends InternalBeanElement typeArguments = genericType.getTypeArguments(); final Map resolved = resolveTypeArguments(typeArguments, types); if (resolved != null) { - final String typeName = genericType.getName(); - this.genericType = ClassElement.of( - typeName, - genericType.isInterface(), - getAnnotationMetadata(), - resolved - ); + this.genericType = genericType.withTypeArguments(resolved).withAnnotationMetadata(getAnnotationMetadata()); } return this; } @@ -1340,7 +1329,7 @@ private final class InternalBeanParameter extends InternalBeanElement typeArguments = genericType.getTypeArguments(); final Map resolved = resolveTypeArguments(typeArguments, types); if (resolved != null) { - - final ElementFactory elementFactory = visitorContext.getElementFactory(); - this.genericType = elementFactory.newClassElement( - genericType.getNativeType(), - getAnnotationMetadata(), - resolved - ); + this.genericType = genericType.withTypeArguments(resolved).withAnnotationMetadata(getAnnotationMetadata()); } return this; } diff --git a/inject/src/main/java/io/micronaut/inject/writer/AbstractClassFileWriter.java b/inject/src/main/java/io/micronaut/inject/writer/AbstractClassFileWriter.java index 4c9a3c890e0..c6eeac5db89 100644 --- a/inject/src/main/java/io/micronaut/inject/writer/AbstractClassFileWriter.java +++ b/inject/src/main/java/io/micronaut/inject/writer/AbstractClassFileWriter.java @@ -29,6 +29,7 @@ import io.micronaut.inject.annotation.AnnotationMetadataReference; import io.micronaut.inject.annotation.AnnotationMetadataWriter; import io.micronaut.inject.annotation.DefaultAnnotationMetadata; +import io.micronaut.inject.annotation.MutableAnnotationMetadata; import io.micronaut.inject.ast.ClassElement; import io.micronaut.inject.ast.Element; import io.micronaut.inject.ast.GenericPlaceholderElement; @@ -268,7 +269,7 @@ private static void pushTypeArgumentElements( ClassElement classElement = entry.getValue(); Type classReference = JavaModelUtils.getTypeReference(classElement); Map typeArguments = classElement.getTypeArguments(); - if (CollectionUtils.isNotEmpty(typeArguments) || classElement.getAnnotationMetadata() != AnnotationMetadata.EMPTY_METADATA) { + if (CollectionUtils.isNotEmpty(typeArguments) || !classElement.getAnnotationMetadata().isEmpty()) { buildArgumentWithGenerics( owningType, declaringClassWriter, @@ -386,8 +387,8 @@ protected static void buildArgumentWithGenerics( // 2nd argument: the name generatorAdapter.push(argumentName); - AnnotationMetadata annotationMetadata = classElement.getAnnotationMetadata(); - boolean hasAnnotationMetadata = annotationMetadata != AnnotationMetadata.EMPTY_METADATA; + AnnotationMetadata annotationMetadata = MutableAnnotationMetadata.of(classElement.getAnnotationMetadata()); + boolean hasAnnotationMetadata = !annotationMetadata.isEmpty(); if (!hasAnnotationMetadata && typeArguments.isEmpty()) { invokeInterfaceStaticMethod( @@ -591,6 +592,8 @@ protected static void pushCreateArgument( Map typeArguments, Map defaults, Map loadTypeMethods) { + annotationMetadata = MutableAnnotationMetadata.of(annotationMetadata); + Type argumentType = JavaModelUtils.getTypeReference(typedElement); // 1st argument: The type @@ -599,7 +602,7 @@ protected static void pushCreateArgument( // 2nd argument: The argument name generatorAdapter.push(argumentName); - boolean hasAnnotations = !annotationMetadata.isEmpty() && annotationMetadata instanceof DefaultAnnotationMetadata; + boolean hasAnnotations = !annotationMetadata.isEmpty(); boolean hasTypeArguments = typeArguments != null && !typeArguments.isEmpty(); boolean isGenericPlaceholder = typedElement instanceof GenericPlaceholderElement; boolean isTypeVariable = isGenericPlaceholder || ((typedElement instanceof ClassElement) && ((ClassElement) typedElement).isTypeVariable()); diff --git a/inject/src/main/java/io/micronaut/inject/writer/BeanDefinitionReferenceWriter.java b/inject/src/main/java/io/micronaut/inject/writer/BeanDefinitionReferenceWriter.java index 268f1d4fc39..dae28ce8806 100644 --- a/inject/src/main/java/io/micronaut/inject/writer/BeanDefinitionReferenceWriter.java +++ b/inject/src/main/java/io/micronaut/inject/writer/BeanDefinitionReferenceWriter.java @@ -16,7 +16,11 @@ package io.micronaut.inject.writer; import io.micronaut.context.AbstractInitializableBeanDefinitionReference; -import io.micronaut.context.annotation.*; +import io.micronaut.context.annotation.Bean; +import io.micronaut.context.annotation.ConfigurationReader; +import io.micronaut.context.annotation.DefaultScope; +import io.micronaut.context.annotation.Primary; +import io.micronaut.context.annotation.Requires; import io.micronaut.core.annotation.AnnotationMetadata; import io.micronaut.core.annotation.AnnotationUtil; import io.micronaut.core.annotation.Internal; diff --git a/inject/src/main/java/io/micronaut/inject/writer/BeanDefinitionVisitor.java b/inject/src/main/java/io/micronaut/inject/writer/BeanDefinitionVisitor.java index 524fae15980..e7894b1fdff 100644 --- a/inject/src/main/java/io/micronaut/inject/writer/BeanDefinitionVisitor.java +++ b/inject/src/main/java/io/micronaut/inject/writer/BeanDefinitionVisitor.java @@ -212,11 +212,13 @@ void visitDefaultConstructor( * * @param declaringType The declaring type * @param methodElement The method element + * @param annotationMetadata The annotationMetadata * @param requiresReflection Whether the setter requires reflection * @param isOptional Whether the setter is optional */ void visitSetterValue(TypedElement declaringType, MethodElement methodElement, + AnnotationMetadata annotationMetadata, boolean requiresReflection, boolean isOptional); @@ -306,8 +308,7 @@ void visitAnnotationMemberPropertyInjectionPoint(TypedElement annotationMemberBe */ void visitFieldValue(TypedElement declaringType, FieldElement fieldElement, - boolean requiresReflection, - boolean isOptional); + boolean requiresReflection, boolean isOptional); /** * @return The package name of the bean @@ -338,7 +339,7 @@ void visitConfigBuilderField( ClassElement type, String field, AnnotationMetadata annotationMetadata, - ConfigurationMetadataBuilder metadataBuilder, + ConfigurationMetadataBuilder metadataBuilder, boolean isInterface); /** @@ -355,42 +356,40 @@ void visitConfigBuilderMethod( ClassElement type, String methodName, AnnotationMetadata annotationMetadata, - ConfigurationMetadataBuilder metadataBuilder, + ConfigurationMetadataBuilder metadataBuilder, boolean isInterface); /** * Visit a configuration builder method. * - * @param prefix The prefix used for the method - * @param returnType The return type - * @param methodName The method name - * @param paramType The method type - * @param generics The generic types of the method - * @param path The property path + * @param propertyName The property name + * @param returnType The return type + * @param methodName The method name + * @param paramType The method type + * @param generics The generic types of the method + * @param path The property path * @see io.micronaut.context.annotation.ConfigurationBuilder */ - void visitConfigBuilderMethod( - String prefix, - ClassElement returnType, - String methodName, - @Nullable ClassElement paramType, - Map generics, - String path); + void visitConfigBuilderMethod(String propertyName, + ClassElement returnType, + String methodName, + @Nullable ClassElement paramType, + Map generics, + String path); /** * Visit a configuration builder method that accepts a long and a TimeUnit. * - * @param prefix The prefix used for the method - * @param returnType The return type - * @param methodName The method name - * @param path The property path + * @param propertyName The property name + * @param returnType The return type + * @param methodName The method name + * @param path The property path * @see io.micronaut.context.annotation.ConfigurationBuilder */ - void visitConfigBuilderDurationMethod( - String prefix, - ClassElement returnType, - String methodName, - String path); + void visitConfigBuilderDurationMethod(String propertyName, + ClassElement returnType, + String methodName, + String path); /** * Finalize a configuration builder field. diff --git a/inject/src/main/java/io/micronaut/inject/writer/BeanDefinitionWriter.java b/inject/src/main/java/io/micronaut/inject/writer/BeanDefinitionWriter.java index 30026a60bcf..51ad89062bc 100644 --- a/inject/src/main/java/io/micronaut/inject/writer/BeanDefinitionWriter.java +++ b/inject/src/main/java/io/micronaut/inject/writer/BeanDefinitionWriter.java @@ -27,7 +27,6 @@ import io.micronaut.context.annotation.Any; import io.micronaut.context.annotation.Bean; import io.micronaut.context.annotation.ConfigurationBuilder; -import io.micronaut.context.annotation.ConfigurationInject; import io.micronaut.context.annotation.ConfigurationProperties; import io.micronaut.context.annotation.ConfigurationReader; import io.micronaut.context.annotation.DefaultScope; @@ -51,7 +50,6 @@ import io.micronaut.core.annotation.NonNull; import io.micronaut.core.annotation.Nullable; import io.micronaut.core.beans.BeanConstructor; -import io.micronaut.core.bind.annotation.Bindable; import io.micronaut.core.naming.NameUtils; import io.micronaut.core.reflect.ClassUtils; import io.micronaut.core.reflect.InstantiationUtils; @@ -92,7 +90,6 @@ import io.micronaut.inject.ast.beans.BeanElement; import io.micronaut.inject.ast.beans.BeanElementBuilder; import io.micronaut.inject.configuration.ConfigurationMetadataBuilder; -import io.micronaut.inject.configuration.PropertyMetadata; import io.micronaut.inject.processing.JavaModelUtils; import io.micronaut.inject.qualifiers.AnyQualifier; import io.micronaut.inject.qualifiers.Qualifiers; @@ -166,7 +163,6 @@ public class BeanDefinitionWriter extends AbstractClassFileWriter implements Bea public static final String OMIT_CONFPROP_INJECTION_POINTS = "micronaut.processing.omit.confprop.injectpoints"; public static final String CLASS_SUFFIX = "$Definition"; - private static final String ANN_CONSTRAINT = "javax.validation.Constraint"; private static final Constructor CONSTRUCTOR_ABSTRACT_CONSTRUCTOR_IP = ReflectionUtils.findConstructor( AbstractConstructorInjectionPoint.class, @@ -502,7 +498,6 @@ public class BeanDefinitionWriter extends AbstractClassFileWriter implements Bea private final boolean isInterface; private final boolean isAbstract; private final boolean isConfigurationProperties; - private final ConfigurationMetadataBuilder metadataBuilder; private final Element beanProducingElement; private final ClassElement beanTypeElement; private final VisitorContext visitorContext; @@ -555,13 +550,11 @@ public class BeanDefinitionWriter extends AbstractClassFileWriter implements Bea * Creates a bean definition writer. * * @param classElement The class element - * @param metadataBuilder The configuration metadata builder * @param visitorContext The visitor context */ public BeanDefinitionWriter(ClassElement classElement, - ConfigurationMetadataBuilder metadataBuilder, VisitorContext visitorContext) { - this(classElement, OriginatingElements.of(classElement), metadataBuilder, visitorContext, null); + this(classElement, OriginatingElements.of(classElement), visitorContext, null); } /** @@ -569,14 +562,12 @@ public BeanDefinitionWriter(ClassElement classElement, * * @param classElement The class element * @param originatingElements The originating elements - * @param metadataBuilder The configuration metadata builder * @param visitorContext The visitor context */ public BeanDefinitionWriter(ClassElement classElement, OriginatingElements originatingElements, - ConfigurationMetadataBuilder metadataBuilder, VisitorContext visitorContext) { - this(classElement, originatingElements, metadataBuilder, visitorContext, null); + this(classElement, originatingElements, visitorContext, null); } /** @@ -584,17 +575,14 @@ public BeanDefinitionWriter(ClassElement classElement, * * @param beanProducingElement The bean producing element * @param originatingElements The originating elements - * @param metadataBuilder The configuration metadata builder * @param visitorContext The visitor context * @param uniqueIdentifier An optional unique identifier to include in the bean name */ public BeanDefinitionWriter(Element beanProducingElement, OriginatingElements originatingElements, - ConfigurationMetadataBuilder metadataBuilder, VisitorContext visitorContext, @Nullable Integer uniqueIdentifier) { super(originatingElements); - this.metadataBuilder = metadataBuilder; this.classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS | ClassWriter.COMPUTE_FRAMES); this.beanProducingElement = beanProducingElement; if (beanProducingElement instanceof ClassElement) { @@ -667,7 +655,7 @@ public BeanDefinitionWriter(Element beanProducingElement, throw new IllegalArgumentException("Unsupported element type: " + beanProducingElement.getClass().getName()); } this.isPrimitiveBean = beanTypeElement.isPrimitive() && !beanTypeElement.isArray(); - this.annotationMetadata = beanProducingElement.getAnnotationMetadata(); + this.annotationMetadata = beanProducingElement.getTargetAnnotationMetadata(); this.beanDefinitionType = getTypeReferenceForName(this.beanDefinitionName); this.beanType = getTypeReference(beanTypeElement); this.beanDefinitionInternalName = getInternalName(this.beanDefinitionName); @@ -927,8 +915,6 @@ public void visitBeanDefinitionConstructor(MethodElement constructor, boolean requiresReflection, VisitorContext visitorContext) { if (this.constructor == null) { - applyConfigurationInjectionIfNecessary(constructor); - this.constructor = constructor; this.constructorRequiresReflection = requiresReflection; @@ -940,70 +926,6 @@ public void visitBeanDefinitionConstructor(MethodElement constructor, } } - private void applyConfigurationInjectionIfNecessary(MethodElement constructor) { - final boolean isRecordConfig = isRecordConfig(constructor); - if (isRecordConfig || constructor.hasAnnotation(ConfigurationInject.class)) { - final List injectionTypes = - Arrays.asList(Property.class.getName(), Value.class.getName(), Parameter.class.getName(), AnnotationUtil.QUALIFIER, AnnotationUtil.INJECT); - - if (isRecordConfig) { - final List beanProperties = constructor - .getDeclaringType() - .getBeanProperties(); - final ParameterElement[] parameters = constructor.getParameters(); - if (beanProperties.size() == parameters.length) { - for (int i = 0; i < parameters.length; i++) { - ParameterElement parameter = parameters[i]; - final PropertyElement bp = beanProperties.get(i); - final AnnotationMetadata beanPropertyMetadata = bp.getAnnotationMetadata(); - AnnotationMetadata annotationMetadata = parameter.getAnnotationMetadata(); - if (injectionTypes.stream().noneMatch(beanPropertyMetadata::hasStereotype)) { - processConfigurationConstructorParameter(parameter, annotationMetadata); - } - if (annotationMetadata.hasStereotype(ANN_CONSTRAINT)) { - setValidated(true); - } - } - } else { - processConfigurationInjectionConstructor(constructor, injectionTypes); - } - } else { - processConfigurationInjectionConstructor(constructor, injectionTypes); - } - } - - } - - private void processConfigurationInjectionConstructor(MethodElement constructor, List injectionTypes) { - ParameterElement[] parameters = constructor.getParameters(); - for (ParameterElement parameter : parameters) { - AnnotationMetadata annotationMetadata = parameter.getAnnotationMetadata(); - if (injectionTypes.stream().noneMatch(annotationMetadata::hasStereotype)) { - processConfigurationConstructorParameter(parameter, annotationMetadata); - } - if (annotationMetadata.hasStereotype(ANN_CONSTRAINT)) { - setValidated(true); - } - } - } - - private void processConfigurationConstructorParameter(ParameterElement parameter, AnnotationMetadata annotationMetadata) { - ClassElement parameterType = parameter.getGenericType(); - if (!parameterType.hasStereotype(AnnotationUtil.SCOPE)) { - final PropertyMetadata pm = metadataBuilder.visitProperty( - parameterType.getName(), - parameter.getName(), parameter.getDocumentation().orElse(null), - annotationMetadata.stringValue(Bindable.class, "defaultValue").orElse(null) - ); - parameter.annotate(Property.class, (builder) -> builder.member("name", pm.getPath())); - } - } - - private boolean isRecordConfig(MethodElement constructor) { - ClassElement declaringType = constructor.getDeclaringType(); - return declaringType.isRecord() && declaringType.hasStereotype(ConfigurationReader.class); - } - @Override public void visitDefaultConstructor(AnnotationMetadata annotationMetadata, VisitorContext visitorContext) { if (this.constructor == null) { @@ -1082,6 +1004,7 @@ public void visitBeanDefinitionEnd() { staticInit, JavaModelUtils.getTypeReference(methodVisitData.beanType), methodVisitData.methodElement, + methodVisitData.getAnnotationMetadata(), methodVisitData.requiresReflection, methodVisitData.isPostConstruct(), methodVisitData.isPreDestroy() @@ -1103,6 +1026,7 @@ public void visitBeanDefinitionEnd() { staticInit, JavaModelUtils.getTypeReference(fieldVisitData.beanType), fieldVisitData.fieldElement, + fieldVisitData.annotationMetadata, fieldVisitData.requiresReflection ) ); @@ -1427,13 +1351,13 @@ public void accept(ClassWriterOutputVisitor visitor) throws IOException { public void visitSetterValue( TypedElement declaringType, MethodElement methodElement, + AnnotationMetadata annotationMetadata, boolean requiresReflection, boolean isOptional) { if (!requiresReflection) { ParameterElement parameter = methodElement.getParameters()[0]; - AnnotationMetadataHierarchy annotationMetadata = new AnnotationMetadataHierarchy(parameter.getAnnotationMetadata(), methodElement.getAnnotationMetadata()); Label falseCondition = isOptional ? pushPropertyContainsCheck( injectMethodVisitor, @@ -1442,31 +1366,30 @@ public void visitSetterValue( annotationMetadata ) : null; - AnnotationMetadata currentAnnotationMetadata = methodElement.getAnnotationMetadata(); ClassElement genericType = parameter.getGenericType(); - if (isConfigurationProperties && isValueType(currentAnnotationMetadata)) { + if (isConfigurationProperties && isValueType(annotationMetadata)) { injectMethodVisitor.loadLocal(injectInstanceLocalVarIndex, beanType); - Optional property = currentAnnotationMetadata.stringValue(Property.class, "name"); - Optional valueValue = parameter.stringValue(Value.class); + Optional property = annotationMetadata.stringValue(Property.class, "name"); + Optional valueValue = annotationMetadata.stringValue(Value.class); if (isInnerType(genericType)) { boolean isArray = genericType.isArray(); boolean isCollection = genericType.isAssignable(Collection.class); if (isCollection || isArray) { ClassElement typeArgument = genericType.isArray() ? genericType.fromArray() : genericType.getFirstTypeArgument().orElse(null); if (typeArgument != null && !typeArgument.isPrimitive()) { - pushInvokeGetBeansOfTypeForSetter(injectMethodVisitor, methodElement.getName(), parameter); + pushInvokeGetBeansOfTypeForSetter(injectMethodVisitor, methodElement.getName(), parameter, annotationMetadata); } else { - pushInvokeGetBeanForSetter(injectMethodVisitor, methodElement.getName(), parameter); + pushInvokeGetBeanForSetter(injectMethodVisitor, methodElement.getName(), parameter, annotationMetadata); } } else { - pushInvokeGetBeanForSetter(injectMethodVisitor, methodElement.getName(), parameter); + pushInvokeGetBeanForSetter(injectMethodVisitor, methodElement.getName(), parameter, annotationMetadata); } } else if (property.isPresent()) { - pushInvokeGetPropertyValueForSetter(injectMethodVisitor, methodElement.getName(), parameter, property.get()); + pushInvokeGetPropertyValueForSetter(injectMethodVisitor, methodElement.getName(), parameter, property.get(), annotationMetadata); } else if (valueValue.isPresent()) { - pushInvokeGetPropertyPlaceholderValueForSetter(injectMethodVisitor, methodElement.getName(), parameter, valueValue.get()); + pushInvokeGetPropertyPlaceholderValueForSetter(injectMethodVisitor, methodElement.getName(), parameter, valueValue.get(), annotationMetadata); } else { throw new IllegalStateException(); } @@ -1485,8 +1408,8 @@ public void visitSetterValue( final MethodVisitData methodVisitData = new MethodVisitData( declaringType, methodElement, - false - ); + false, + annotationMetadata); methodInjectionPoints.add(methodVisitData); allMethodVisits.add(methodVisitData); currentMethodIndex++; @@ -1495,8 +1418,8 @@ public void visitSetterValue( final MethodVisitData methodVisitData = new MethodVisitData( declaringType, methodElement, - false - ); + false, + annotationMetadata); visitMethodInjectionPointInternal(methodVisitData, injectMethodVisitor, injectInstanceLocalVarIndex); methodInjectionPoints.add(methodVisitData); allMethodVisits.add(methodVisitData); @@ -1510,8 +1433,8 @@ public void visitSetterValue( final MethodVisitData methodVisitData = new MethodVisitData( declaringType, methodElement, - false - ); + false, + annotationMetadata); methodInjectionPoints.add(methodVisitData); allMethodVisits.add(methodVisitData); currentMethodIndex++; @@ -1526,7 +1449,7 @@ public void visitPostConstructMethod(TypedElement declaringType, visitPostConstructMethodDefinition(false); // for "super bean definitions" we just delegate to super if (!superBeanDefinition || isInterceptedLifeCycleByType(this.annotationMetadata, "POST_CONSTRUCT")) { - MethodVisitData methodVisitData = new MethodVisitData(declaringType, methodElement, requiresReflection, true, false); + MethodVisitData methodVisitData = new MethodVisitData(declaringType, methodElement, requiresReflection, methodElement.getAnnotationMetadata(), true, false); postConstructMethodVisits.add(methodVisitData); allMethodVisits.add(methodVisitData); visitMethodInjectionPointInternal(methodVisitData, postConstructMethodVisitor, postConstructInstanceLocalVarIndex); @@ -1543,7 +1466,7 @@ public void visitPreDestroyMethod(TypedElement declaringType, if (!superBeanDefinition || isInterceptedLifeCycleByType(this.annotationMetadata, "PRE_DESTROY")) { visitPreDestroyMethodDefinition(false); - MethodVisitData methodVisitData = new MethodVisitData(declaringType, methodElement, requiresReflection, false, true); + MethodVisitData methodVisitData = new MethodVisitData(declaringType, methodElement, requiresReflection, methodElement.getAnnotationMetadata(), false, true); preDestroyMethodVisits.add(methodVisitData); allMethodVisits.add(methodVisitData); visitMethodInjectionPointInternal(methodVisitData, preDestroyMethodVisitor, preDestroyInstanceLocalVarIndex); @@ -1554,10 +1477,9 @@ public void visitPreDestroyMethod(TypedElement declaringType, @Override public void visitMethodInjectionPoint(TypedElement declaringType, MethodElement methodElement, - boolean requiresReflection, VisitorContext visitorContext) { - applyConfigurationInjectionIfNecessary(methodElement); - - MethodVisitData methodVisitData = new MethodVisitData(declaringType, methodElement, requiresReflection); + boolean requiresReflection, + VisitorContext visitorContext) { + MethodVisitData methodVisitData = new MethodVisitData(declaringType, methodElement, requiresReflection, methodElement.getAnnotationMetadata()); methodInjectionPoints.add(methodVisitData); allMethodVisits.add(methodVisitData); visitMethodInjectionPointInternal(methodVisitData, injectMethodVisitor, injectInstanceLocalVarIndex); @@ -1590,11 +1512,9 @@ public int visitExecutableMethod(TypedElement declaringType, String interceptedProxyClassName, String interceptedProxyBridgeMethodName) { - AnnotationMetadata annotationMetadata = methodElement.getAnnotationMetadata(); - DefaultAnnotationMetadata.contributeDefaults( this.annotationMetadata, - annotationMetadata + methodElement.getAnnotationMetadata() ); for (ParameterElement parameterElement : methodElement.getSuspendParameters()) { DefaultAnnotationMetadata.contributeDefaults( @@ -1668,7 +1588,6 @@ public void visitConfigBuilderField( field, false, annotationMetadata, - metadataBuilder, isInterface); } @@ -1706,17 +1625,17 @@ public void visitConfigBuilderMethod( )); } - this.currentConfigBuilderState = new ConfigBuilderState(type, methodName, true, annotationMetadata, metadataBuilder, isInterface); + this.currentConfigBuilderState = new ConfigBuilderState(type, methodName, true, annotationMetadata, isInterface); } @Override public void visitConfigBuilderDurationMethod( - String prefix, + String propertyName, ClassElement returnType, String methodName, String path) { visitConfigBuilderMethodInternal( - prefix, + propertyName, returnType, methodName, ClassElement.of(Duration.class), @@ -1728,7 +1647,7 @@ public void visitConfigBuilderDurationMethod( @Override public void visitConfigBuilderMethod( - String prefix, + String propertyName, ClassElement returnType, String methodName, ClassElement paramType, @@ -1736,7 +1655,7 @@ public void visitConfigBuilderMethod( String path) { visitConfigBuilderMethodInternal( - prefix, + propertyName, returnType, methodName, paramType, @@ -1772,12 +1691,13 @@ public void visitFieldInjectionPoint( FieldElement fieldElement, boolean requiresReflection) { - visitFieldInjectionPointInternal(declaringType, fieldElement, requiresReflection); + visitFieldInjectionPointInternal(declaringType, fieldElement, fieldElement.getAnnotationMetadata(), requiresReflection); } private void visitFieldInjectionPointInternal( TypedElement declaringType, FieldElement fieldElement, + AnnotationMetadata annotationMetadata, boolean requiresReflection) { boolean requiresGenericType = false; @@ -1813,6 +1733,7 @@ private void visitFieldInjectionPointInternal( visitFieldInjectionPointInternal( declaringType, fieldElement, + annotationMetadata, requiresReflection, methodToInvoke, isArray, @@ -1870,20 +1791,19 @@ public void visitAnnotationMemberPropertyInjectionPoint(TypedElement annotationM } @Override - public void visitFieldValue( - TypedElement declaringType, - FieldElement fieldElement, - boolean requiresReflection, - boolean isOptional) { - - Label falseCondition = isOptional ? pushPropertyContainsCheck(injectMethodVisitor, fieldElement.getType(), fieldElement.getName(), fieldElement.getAnnotationMetadata()) : null; + public void visitFieldValue(TypedElement declaringType, + FieldElement fieldElement, + boolean requiresReflection, boolean isOptional) { + AnnotationMetadata annotationMetadata = fieldElement.getAnnotationMetadata(); + Label falseCondition = isOptional ? pushPropertyContainsCheck(injectMethodVisitor, fieldElement.getType(), fieldElement.getName(), annotationMetadata) : null; if (isInnerType(fieldElement.getGenericType())) { - visitFieldInjectionPointInternal(declaringType, fieldElement, requiresReflection); + visitFieldInjectionPointInternal(declaringType, fieldElement, annotationMetadata, requiresReflection); } else if (!isConfigurationProperties || requiresReflection) { visitFieldInjectionPointInternal( declaringType, fieldElement, + annotationMetadata, requiresReflection, GET_VALUE_FOR_FIELD, isOptional, @@ -1891,19 +1811,19 @@ public void visitFieldValue( } else { injectMethodVisitor.loadLocal(injectInstanceLocalVarIndex, beanType); - Optional property = fieldElement.stringValue(Property.class, "name"); + Optional property = annotationMetadata.stringValue(Property.class, "name"); if (property.isPresent()) { - pushInvokeGetPropertyValueForField(injectMethodVisitor, fieldElement, property.get()); + pushInvokeGetPropertyValueForField(injectMethodVisitor, fieldElement, annotationMetadata, property.get()); } else { - Optional valueValue = fieldElement.stringValue(Value.class); + Optional valueValue = annotationMetadata.stringValue(Value.class); if (valueValue.isPresent()) { - pushInvokeGetPropertyPlaceholderValueForField(injectMethodVisitor, fieldElement, valueValue.get()); + pushInvokeGetPropertyPlaceholderValueForField(injectMethodVisitor, fieldElement, annotationMetadata, valueValue.get()); } } putField(injectMethodVisitor, fieldElement, requiresReflection, declaringType); if (keepConfPropInjectPoints) { - fieldInjectionPoints.add(new FieldVisitData(declaringType, fieldElement, requiresReflection)); + fieldInjectionPoints.add(new FieldVisitData(declaringType, fieldElement, annotationMetadata, requiresReflection)); currentFieldIndex++; } @@ -1914,7 +1834,7 @@ public void visitFieldValue( } } - private void pushInvokeGetPropertyValueForField(GeneratorAdapter injectMethodVisitor, FieldElement fieldElement, String value) { + private void pushInvokeGetPropertyValueForField(GeneratorAdapter injectMethodVisitor, FieldElement fieldElement, AnnotationMetadata annotationMetadata, String value) { // load 'this' injectMethodVisitor.loadThis(); // 1st argument load BeanResolutionContext @@ -1922,8 +1842,9 @@ private void pushInvokeGetPropertyValueForField(GeneratorAdapter injectMethodVis // 2nd argument load BeanContext injectMethodVisitor.loadArg(1); // 3rd argument the method index - MutableAnnotationMetadata mutableAnnotationMetadata = ((MutableAnnotationMetadata) fieldElement.getAnnotationMetadata()).clone(); - removeAnnotations(mutableAnnotationMetadata, PropertySource.class.getName(), Property.class.getName()); + + annotationMetadata = MutableAnnotationMetadata.of(annotationMetadata); + removeAnnotations(annotationMetadata, PropertySource.class.getName(), Property.class.getName()); if (keepConfPropInjectPoints) { resolveFieldArgument(injectMethodVisitor, currentFieldIndex); @@ -1935,7 +1856,7 @@ private void pushInvokeGetPropertyValueForField(GeneratorAdapter injectMethodVis injectMethodVisitor, fieldElement.getName(), fieldElement.getGenericType(), - mutableAnnotationMetadata, + annotationMetadata, fieldElement.getGenericType().getTypeArguments(), new HashMap<>(), loadTypeMethods @@ -1951,7 +1872,7 @@ private void pushInvokeGetPropertyValueForField(GeneratorAdapter injectMethodVis pushCastToType(injectMethodVisitor, fieldElement.getType()); } - private void pushInvokeGetPropertyPlaceholderValueForField(GeneratorAdapter injectMethodVisitor, FieldElement fieldElement, String value) { + private void pushInvokeGetPropertyPlaceholderValueForField(GeneratorAdapter injectMethodVisitor, FieldElement fieldElement, AnnotationMetadata annotationMetadata, String value) { // load 'this' injectMethodVisitor.loadThis(); // 1st argument load BeanResolutionContext @@ -1960,8 +1881,8 @@ private void pushInvokeGetPropertyPlaceholderValueForField(GeneratorAdapter inje injectMethodVisitor.loadArg(1); // 3rd argument the method index - MutableAnnotationMetadata mutableAnnotationMetadata = ((MutableAnnotationMetadata) fieldElement.getAnnotationMetadata()).clone(); - removeAnnotations(mutableAnnotationMetadata, PropertySource.class.getName(), Property.class.getName()); + annotationMetadata = MutableAnnotationMetadata.of(annotationMetadata); + removeAnnotations(annotationMetadata, PropertySource.class.getName(), Property.class.getName()); if (keepConfPropInjectPoints) { resolveFieldArgument(injectMethodVisitor, currentFieldIndex); @@ -1973,7 +1894,7 @@ private void pushInvokeGetPropertyPlaceholderValueForField(GeneratorAdapter inje injectMethodVisitor, fieldElement.getName(), fieldElement.getGenericType(), - mutableAnnotationMetadata, + annotationMetadata, fieldElement.getGenericType().getTypeArguments(), new HashMap<>(), loadTypeMethods @@ -1988,7 +1909,7 @@ private void pushInvokeGetPropertyPlaceholderValueForField(GeneratorAdapter inje } private void visitConfigBuilderMethodInternal( - String prefix, + String propertyName, ClassElement returnType, String methodName, ClassElement paramType, @@ -2002,8 +1923,6 @@ private void visitConfigBuilderMethodInternal( boolean isResolveBuilderViaMethodCall = currentConfigBuilderState.isMethod(); GeneratorAdapter injectMethodVisitor = this.injectMethodVisitor; - String propertyName = NameUtils.hyphenate(NameUtils.decapitalize(methodName.substring(prefix.length())), true); - boolean zeroArgs = paramType == null; // Optional optional = AbstractBeanDefinition.getValueForPath(...) @@ -2129,16 +2048,15 @@ private int pushGetValueForPathCall(GeneratorAdapter injectMethodVisitor, ClassE private void visitFieldInjectionPointInternal( TypedElement declaringType, FieldElement fieldElement, + AnnotationMetadata annotationMetadata, boolean requiresReflection, Method methodToInvoke, boolean isArray, boolean requiresGenericType) { - AnnotationMetadata annotationMetadata = fieldElement.getAnnotationMetadata(); autoApplyNamedIfPresent(fieldElement, annotationMetadata); DefaultAnnotationMetadata.contributeDefaults(this.annotationMetadata, annotationMetadata); DefaultAnnotationMetadata.contributeRepeatable(this.annotationMetadata, fieldElement.getGenericField()); - Type declaringTypeRef = JavaModelUtils.getTypeReference(declaringType); GeneratorAdapter injectMethodVisitor = this.injectMethodVisitor; @@ -2171,7 +2089,7 @@ private void visitFieldInjectionPointInternal( } putField(injectMethodVisitor, fieldElement, requiresReflection, declaringType); currentFieldIndex++; - fieldInjectionPoints.add(new FieldVisitData(declaringType, fieldElement, requiresReflection)); + fieldInjectionPoints.add(new FieldVisitData(declaringType, fieldElement, annotationMetadata, requiresReflection)); } private void putField(GeneratorAdapter injectMethodVisitor, FieldElement fieldElement, boolean requiresReflection, TypedElement declaringType) { @@ -2387,7 +2305,7 @@ private void visitMethodInjectionPointInternal(MethodVisitData methodVisitData, MethodElement methodElement = methodVisitData.getMethodElement(); - final AnnotationMetadata annotationMetadata = methodElement.getAnnotationMetadata(); + final AnnotationMetadata annotationMetadata = methodVisitData.getAnnotationMetadata(); final List argumentTypes = Arrays.asList(methodElement.getParameters()); applyDefaultNamedToParameters(argumentTypes); final TypedElement declaringType = methodVisitData.beanType; @@ -2585,7 +2503,7 @@ private void pushInvokeGetPropertyPlaceholderValueForMethod(GeneratorAdapter inj pushCastToType(injectMethodVisitor, entry); } - private void pushInvokeGetPropertyValueForSetter(GeneratorAdapter injectMethodVisitor, String setterName, ParameterElement entry, String value) { + private void pushInvokeGetPropertyValueForSetter(GeneratorAdapter injectMethodVisitor, String setterName, ParameterElement entry, String value, AnnotationMetadata annotationMetadata) { // load 'this' injectMethodVisitor.loadThis(); // 1st argument load BeanResolutionContext @@ -2595,13 +2513,8 @@ private void pushInvokeGetPropertyValueForSetter(GeneratorAdapter injectMethodVi // 3rd argument the method name injectMethodVisitor.push(setterName); - AnnotationMetadata annotationMetadata; - if (entry.getAnnotationMetadata() instanceof MutableAnnotationMetadata) { - annotationMetadata = ((MutableAnnotationMetadata) entry.getAnnotationMetadata()).clone(); - removeAnnotations(annotationMetadata, PropertySource.class.getName(), Property.class.getName()); - } else { - annotationMetadata = entry.getAnnotationMetadata(); - } + annotationMetadata = MutableAnnotationMetadata.of(annotationMetadata); + removeAnnotations(annotationMetadata, PropertySource.class.getName(), Property.class.getName()); // 4th argument the argument if (keepConfPropInjectPoints) { @@ -2630,7 +2543,7 @@ private void pushInvokeGetPropertyValueForSetter(GeneratorAdapter injectMethodVi pushCastToType(injectMethodVisitor, entry); } - private void pushInvokeGetBeanForSetter(GeneratorAdapter injectMethodVisitor, String setterName, ParameterElement entry) { + private void pushInvokeGetBeanForSetter(GeneratorAdapter injectMethodVisitor, String setterName, ParameterElement entry, AnnotationMetadata annotationMetadata) { // load 'this' injectMethodVisitor.loadThis(); // 1st argument load BeanResolutionContext @@ -2640,13 +2553,8 @@ private void pushInvokeGetBeanForSetter(GeneratorAdapter injectMethodVisitor, St // 3rd argument the method name injectMethodVisitor.push(setterName); - AnnotationMetadata annotationMetadata; - if (entry.getAnnotationMetadata() instanceof MutableAnnotationMetadata) { - annotationMetadata = ((MutableAnnotationMetadata) entry.getAnnotationMetadata()).clone(); - removeAnnotations(annotationMetadata, PropertySource.class.getName(), Property.class.getName()); - } else { - annotationMetadata = entry.getAnnotationMetadata(); - } + annotationMetadata = MutableAnnotationMetadata.of(annotationMetadata); + removeAnnotations(annotationMetadata, PropertySource.class.getName(), Property.class.getName()); // 4th argument the argument if (keepConfPropInjectPoints) { @@ -2674,7 +2582,7 @@ private void pushInvokeGetBeanForSetter(GeneratorAdapter injectMethodVisitor, St pushCastToType(injectMethodVisitor, entry); } - private void pushInvokeGetBeansOfTypeForSetter(GeneratorAdapter injectMethodVisitor, String setterName, ParameterElement entry) { + private void pushInvokeGetBeansOfTypeForSetter(GeneratorAdapter injectMethodVisitor, String setterName, ParameterElement entry, AnnotationMetadata annotationMetadata) { // load 'this' injectMethodVisitor.loadThis(); // 1st argument load BeanResolutionContext @@ -2684,13 +2592,8 @@ private void pushInvokeGetBeansOfTypeForSetter(GeneratorAdapter injectMethodVisi // 3rd argument the method name injectMethodVisitor.push(setterName); - AnnotationMetadata annotationMetadata; - if (entry.getAnnotationMetadata() instanceof MutableAnnotationMetadata) { - annotationMetadata = ((MutableAnnotationMetadata) entry.getAnnotationMetadata()).clone(); - removeAnnotations(annotationMetadata, PropertySource.class.getName(), Property.class.getName()); - } else { - annotationMetadata = entry.getAnnotationMetadata(); - } + annotationMetadata = MutableAnnotationMetadata.of(annotationMetadata); + removeAnnotations(annotationMetadata, PropertySource.class.getName(), Property.class.getName()); // 4th argument the argument ClassElement genericType = entry.getGenericType(); @@ -2732,7 +2635,7 @@ private void pushInvokeGetBeansOfTypeForSetter(GeneratorAdapter injectMethodVisi pushCastToType(injectMethodVisitor, entry); } - private void pushInvokeGetPropertyPlaceholderValueForSetter(GeneratorAdapter injectMethodVisitor, String setterName, ParameterElement entry, String value) { + private void pushInvokeGetPropertyPlaceholderValueForSetter(GeneratorAdapter injectMethodVisitor, String setterName, ParameterElement entry, String value, AnnotationMetadata annotationMetadata) { // load 'this' injectMethodVisitor.loadThis(); // 1st argument load BeanResolutionContext @@ -2743,13 +2646,8 @@ private void pushInvokeGetPropertyPlaceholderValueForSetter(GeneratorAdapter inj injectMethodVisitor.push(setterName); // 4th argument the argument - AnnotationMetadata annotationMetadata; - if (entry.getAnnotationMetadata() instanceof MutableAnnotationMetadata) { - annotationMetadata = ((MutableAnnotationMetadata) entry.getAnnotationMetadata()).clone(); - removeAnnotations(annotationMetadata, PropertySource.class.getName(), Property.class.getName()); - } else { - annotationMetadata = entry.getAnnotationMetadata(); - } + annotationMetadata = MutableAnnotationMetadata.of(annotationMetadata); + removeAnnotations(annotationMetadata, PropertySource.class.getName(), Property.class.getName()); if (keepConfPropInjectPoints) { resolveMethodArgument(injectMethodVisitor, currentMethodIndex, 0); @@ -3988,10 +3886,10 @@ private void visitBeanDefinitionConstructorInternal( List parameterList = Arrays.asList(parameters); applyDefaultNamedToParameters(parameterList); - pushNewMethodReference(staticInit, JavaModelUtils.getTypeReference(methodElement.getDeclaringType()), methodElement, requiresReflection, false, false); + pushNewMethodReference(staticInit, JavaModelUtils.getTypeReference(methodElement.getDeclaringType()), methodElement, methodElement.getAnnotationMetadata(), requiresReflection, false, false); } else if (constructor instanceof FieldElement) { FieldElement fieldConstructor = (FieldElement) constructor; - pushNewFieldReference(staticInit, JavaModelUtils.getTypeReference(fieldConstructor.getDeclaringType()), fieldConstructor, constructorRequiresReflection); + pushNewFieldReference(staticInit, JavaModelUtils.getTypeReference(fieldConstructor.getDeclaringType()), fieldConstructor, fieldConstructor.getAnnotationMetadata(), constructorRequiresReflection); } else { throw new IllegalArgumentException("Unexpected constructor: " + constructor); } @@ -4128,10 +4026,14 @@ private boolean isIterable(AnnotationMetadata annotationMetadata) { return annotationMetadata.hasDeclaredStereotype(EachProperty.class) || annotationMetadata.hasDeclaredStereotype(EachBean.class); } - private void pushNewMethodReference(GeneratorAdapter staticInit, Type beanType, MethodElement methodElement, + private void pushNewMethodReference(GeneratorAdapter staticInit, + Type beanType, + MethodElement methodElement, + AnnotationMetadata annotationMetadata, boolean requiresReflection, boolean isPostConstructMethod, boolean isPreDestroyMethod) { + annotationMetadata = annotationMetadata.getTargetAnnotationMetadata(); for (ParameterElement value : methodElement.getParameters()) { DefaultAnnotationMetadata.contributeDefaults(this.annotationMetadata, value.getAnnotationMetadata()); DefaultAnnotationMetadata.contributeRepeatable(this.annotationMetadata, value.getGenericType()); @@ -4157,7 +4059,10 @@ private void pushNewMethodReference(GeneratorAdapter staticInit, Type beanType, ); } // 4: annotationMetadata - pushAnnotationMetadata(staticInit, methodElement.getAnnotationMetadata()); + if (annotationMetadata instanceof AnnotationMetadataHierarchy) { + annotationMetadata = ((AnnotationMetadataHierarchy) annotationMetadata).merge(); + } + pushAnnotationMetadata(staticInit, annotationMetadata); // 5: requiresReflection staticInit.push(requiresReflection); if (isPreDestroyMethod || isPostConstructMethod) { @@ -4171,7 +4076,7 @@ private void pushNewMethodReference(GeneratorAdapter staticInit, Type beanType, } } - private void pushNewFieldReference(GeneratorAdapter staticInit, Type declaringType, FieldElement fieldElement, boolean requiresReflection) { + private void pushNewFieldReference(GeneratorAdapter staticInit, Type declaringType, FieldElement fieldElement, AnnotationMetadata annotationMetadata, boolean requiresReflection) { staticInit.newInstance(Type.getType(AbstractInitializableBeanDefinition.FieldReference.class)); staticInit.dup(); // 1: declaringType @@ -4184,7 +4089,7 @@ private void pushNewFieldReference(GeneratorAdapter staticInit, Type declaringTy staticInit, fieldElement.getName(), fieldElement.getGenericType(), - fieldElement.getAnnotationMetadata(), + annotationMetadata, fieldElement.getGenericType().getTypeArguments(), defaultsStorage, loadTypeMethods @@ -4211,6 +4116,7 @@ private void pushNewAnnotationReference(GeneratorAdapter staticInit, Type refere } private void pushAnnotationMetadata(GeneratorAdapter staticInit, AnnotationMetadata annotationMetadata) { + annotationMetadata = annotationMetadata.getTargetAnnotationMetadata(); if (annotationMetadata == AnnotationMetadata.EMPTY_METADATA || annotationMetadata.isEmpty()) { staticInit.push((String) null); } else if (annotationMetadata instanceof AnnotationMetadataHierarchy) { @@ -4230,7 +4136,7 @@ private void pushAnnotationMetadata(GeneratorAdapter staticInit, AnnotationMetad defaultsStorage, loadTypeMethods); } else { - staticInit.push((String) null); + throw new IllegalStateException("Unknown annotation metadata: " + annotationMetadata.getClass().getName()); } } @@ -4534,14 +4440,17 @@ public AnnotationVisitData(TypedElement memberBeanType, private static final class FieldVisitData { final TypedElement beanType; final FieldElement fieldElement; + final AnnotationMetadata annotationMetadata; final boolean requiresReflection; FieldVisitData( TypedElement beanType, FieldElement fieldElement, + AnnotationMetadata annotationMetadata, boolean requiresReflection) { this.beanType = beanType; this.fieldElement = fieldElement; + this.annotationMetadata = annotationMetadata; this.requiresReflection = requiresReflection; } @@ -4555,6 +4464,7 @@ public static final class MethodVisitData { private final TypedElement beanType; private final boolean requiresReflection; private final MethodElement methodElement; + private final AnnotationMetadata annotationMetadata; private final boolean postConstruct; private final boolean preDestroy; @@ -4564,27 +4474,32 @@ public static final class MethodVisitData { * @param beanType The declaring type * @param methodElement The method element * @param requiresReflection Whether reflection is required + * @param annotationMetadata */ MethodVisitData( - TypedElement beanType, - MethodElement methodElement, - boolean requiresReflection) { + TypedElement beanType, + MethodElement methodElement, + boolean requiresReflection, + AnnotationMetadata annotationMetadata) { this.beanType = beanType; this.requiresReflection = requiresReflection; this.methodElement = methodElement; + this.annotationMetadata = annotationMetadata; this.postConstruct = false; this.preDestroy = false; } MethodVisitData( - TypedElement beanType, - MethodElement methodElement, - boolean requiresReflection, - boolean postConstruct, - boolean preDestroy) { + TypedElement beanType, + MethodElement methodElement, + boolean requiresReflection, + AnnotationMetadata annotationMetadata, + boolean postConstruct, + boolean preDestroy) { this.beanType = beanType; this.requiresReflection = requiresReflection; this.methodElement = methodElement; + this.annotationMetadata = annotationMetadata; this.postConstruct = postConstruct; this.preDestroy = preDestroy; } @@ -4596,6 +4511,13 @@ public MethodElement getMethodElement() { return methodElement; } + /** + * @return The annotationMetadata + */ + public AnnotationMetadata getAnnotationMetadata() { + return annotationMetadata; + } + /** * @return The declaring type object. */ diff --git a/inject/src/main/java/io/micronaut/inject/writer/ConfigBuilderState.java b/inject/src/main/java/io/micronaut/inject/writer/ConfigBuilderState.java index 221d28f0112..14524b69e53 100644 --- a/inject/src/main/java/io/micronaut/inject/writer/ConfigBuilderState.java +++ b/inject/src/main/java/io/micronaut/inject/writer/ConfigBuilderState.java @@ -18,7 +18,6 @@ import io.micronaut.core.annotation.AnnotationMetadata; import io.micronaut.core.annotation.Internal; import io.micronaut.inject.ast.ClassElement; -import io.micronaut.inject.configuration.ConfigurationMetadataBuilder; import io.micronaut.inject.processing.JavaModelUtils; import org.objectweb.asm.Type; @@ -35,7 +34,6 @@ class ConfigBuilderState { private final String name; private final Type type; private final boolean invokeMethod; - private final ConfigurationMetadataBuilder metadataBuilder; private final AnnotationMetadata annotationMetadata; private final boolean isInterface; @@ -45,25 +43,16 @@ class ConfigBuilderState { * @param name The name of the field or method * @param isMethod Is the configuration builder resolver a method * @param annotationMetadata The annotation metadata - * @param metadataBuilder The metadata builder * @param isInterface Whether the type is an interface or not */ - ConfigBuilderState(ClassElement type, String name, boolean isMethod, AnnotationMetadata annotationMetadata, ConfigurationMetadataBuilder metadataBuilder, boolean isInterface) { + ConfigBuilderState(ClassElement type, String name, boolean isMethod, AnnotationMetadata annotationMetadata, boolean isInterface) { this.type = JavaModelUtils.getTypeReference(type); this.name = name; this.invokeMethod = isMethod; - this.metadataBuilder = metadataBuilder; this.annotationMetadata = annotationMetadata; this.isInterface = isInterface; } - /** - * @return The configuration metadata builder - */ - public ConfigurationMetadataBuilder getMetadataBuilder() { - return metadataBuilder; - } - /** * @return The name */ diff --git a/inject/src/main/java/io/micronaut/inject/writer/ExecutableMethodsDefinitionWriter.java b/inject/src/main/java/io/micronaut/inject/writer/ExecutableMethodsDefinitionWriter.java index 3cc3ad2fc29..a9fcf1c7037 100644 --- a/inject/src/main/java/io/micronaut/inject/writer/ExecutableMethodsDefinitionWriter.java +++ b/inject/src/main/java/io/micronaut/inject/writer/ExecutableMethodsDefinitionWriter.java @@ -364,13 +364,19 @@ private void pushNewMethodReference(ClassWriter classWriter, Type typeReference = JavaModelUtils.getTypeReference(declaringType.getType()); staticInit.push(typeReference); // 2: annotationMetadata - AnnotationMetadata annotationMetadata = methodElement.getAnnotationMetadata(); + AnnotationMetadata annotationMetadata = methodElement.getTargetAnnotationMetadata(); if (annotationMetadata instanceof AnnotationMetadataHierarchy) { - annotationMetadata = new AnnotationMetadataHierarchy( - new AnnotationMetadataReference(beanDefinitionReferenceClassName, annotationMetadata), + AnnotationMetadataHierarchy hierarchy = (AnnotationMetadataHierarchy) annotationMetadata; + if (hierarchy.size() != 2) { + throw new IllegalStateException("Expected the size of 2"); + } + if (hierarchy.getRootMetadata().equals(methodElement.getOwningType())) { + annotationMetadata = new AnnotationMetadataHierarchy( + new AnnotationMetadataReference(beanDefinitionReferenceClassName, methodElement.getOwningType()), annotationMetadata.getDeclaredMetadata() - ); + ); + } } pushAnnotationMetadata(classWriter, staticInit, annotationMetadata); @@ -435,7 +441,7 @@ private void pushAnnotationMetadata(ClassWriter classWriter, GeneratorAdapter st defaultsStorage, loadTypeMethods); } else { - staticInit.push((String) null); + throw new IllegalStateException("Unknown metadata: " + annotationMetadata); } } } diff --git a/inject/src/main/resources/META-INF/services/io.micronaut.inject.annotation.AnnotationMapper b/inject/src/main/resources/META-INF/services/io.micronaut.inject.annotation.AnnotationMapper index 6fab0a571f5..abe2d750895 100644 --- a/inject/src/main/resources/META-INF/services/io.micronaut.inject.annotation.AnnotationMapper +++ b/inject/src/main/resources/META-INF/services/io.micronaut.inject.annotation.AnnotationMapper @@ -5,4 +5,6 @@ io.micronaut.inject.beans.visitor.EntityReflectiveAccessAnnotationMapper io.micronaut.inject.beans.visitor.MappedSuperClassIntrospectionMapper io.micronaut.inject.beans.visitor.JsonCreatorAnnotationMapper io.micronaut.inject.beans.visitor.jakarta.persistence.JakartaEntityIntrospectedAnnotationMapper -io.micronaut.inject.beans.visitor.jakarta.persistence.JakartaMappedSuperClassIntrospectionMapper \ No newline at end of file +io.micronaut.inject.beans.visitor.jakarta.persistence.JakartaMappedSuperClassIntrospectionMapper +io.micronaut.inject.mappers.ConfigurationBuilderToBeanPropertiesMapper +io.micronaut.inject.mappers.ConfigurationPropertiesToBeanPropertiesMapper diff --git a/inject/src/main/resources/META-INF/services/io.micronaut.inject.visitor.TypeElementVisitor b/inject/src/main/resources/META-INF/services/io.micronaut.inject.visitor.TypeElementVisitor index 16622c60a5a..b728d9ef8d5 100644 --- a/inject/src/main/resources/META-INF/services/io.micronaut.inject.visitor.TypeElementVisitor +++ b/inject/src/main/resources/META-INF/services/io.micronaut.inject.visitor.TypeElementVisitor @@ -1,3 +1,8 @@ io.micronaut.inject.beans.visitor.IntrospectedTypeElementVisitor io.micronaut.context.visitor.BeanImportVisitor io.micronaut.context.visitor.ContextConfigurerVisitor +io.micronaut.context.visitor.ExecutableVisitor +io.micronaut.context.visitor.ValidationVisitor +io.micronaut.context.visitor.InternalApiTypeElementVisitor +io.micronaut.context.visitor.ConfigurationReaderVisitor + diff --git a/management/src/main/java/io/micronaut/management/endpoint/annotation/Endpoint.java b/management/src/main/java/io/micronaut/management/endpoint/annotation/Endpoint.java index 214c96bac0b..44d2f7db71f 100644 --- a/management/src/main/java/io/micronaut/management/endpoint/annotation/Endpoint.java +++ b/management/src/main/java/io/micronaut/management/endpoint/annotation/Endpoint.java @@ -39,7 +39,7 @@ @Retention(RUNTIME) @Target(ElementType.TYPE) @Singleton -@ConfigurationReader(prefix = EndpointConfiguration.PREFIX) +@ConfigurationReader(basePrefix = EndpointConfiguration.PREFIX) @Requires(condition = EndpointEnabledCondition.class) public @interface Endpoint { @@ -66,7 +66,7 @@ /** * @return The ID of the endpoint */ - @AliasFor(annotation = ConfigurationReader.class, member = "value") + @AliasFor(annotation = ConfigurationReader.class, member = ConfigurationReader.PREFIX) @AliasFor(member = "id") String value() default ""; @@ -74,13 +74,13 @@ * @return The ID of the endpoint */ @AliasFor(member = "value") - @AliasFor(annotation = ConfigurationReader.class, member = "value") + @AliasFor(annotation = ConfigurationReader.class, member = ConfigurationReader.PREFIX) String id() default ""; /** * @return The default prefix to use */ - @AliasFor(annotation = ConfigurationReader.class, member = "prefix") + @AliasFor(annotation = ConfigurationReader.class, member = ConfigurationReader.BASE_PREFIX) String prefix() default DEFAULT_PREFIX; /** diff --git a/router/src/test/groovy/io/micronaut/ast/groovy/annotation/GroovyAnnotationMetadataBuilderSpec.groovy b/router/src/test/groovy/io/micronaut/ast/groovy/annotation/GroovyAnnotationMetadataBuilderSpec.groovy index 2acd2ff7658..79d6941ef75 100644 --- a/router/src/test/groovy/io/micronaut/ast/groovy/annotation/GroovyAnnotationMetadataBuilderSpec.groovy +++ b/router/src/test/groovy/io/micronaut/ast/groovy/annotation/GroovyAnnotationMetadataBuilderSpec.groovy @@ -21,6 +21,7 @@ import io.micronaut.http.HttpStatus import io.micronaut.http.MediaType import io.micronaut.http.annotation.Consumes import io.micronaut.http.annotation.Error +import io.micronaut.inject.BeanDefinition /** * @author Graeme Rocher @@ -61,7 +62,7 @@ class FormController { void "test controller action annotation metadata"() { given: - AnnotationMetadata metadata = buildMethodAnnotationMetadata("test.FormController", '''\ + BeanDefinition beanDefinition = buildBeanDefinition("test.FormController", '''\ package test; import io.micronaut.web.router.annotation.* @@ -75,11 +76,11 @@ class FormController { "name: $name, age: $age" } } -''', 'simple') +''') expect: - metadata != null - metadata.hasStereotype(Consumes) - metadata.getValue(Consumes,MediaType[].class).isPresent() + beanDefinition.getExecutableMethods().size() == 1 != null + beanDefinition.getExecutableMethods().iterator().next().hasStereotype(Consumes) + beanDefinition.getExecutableMethods().iterator().next().getValue(Consumes,MediaType[].class).isPresent() } } diff --git a/runtime/src/test/groovy/io/micronaut/support/AbstractBeanDefinitionSpec.groovy b/runtime/src/test/groovy/io/micronaut/support/AbstractBeanDefinitionSpec.groovy index 28fa1f3714a..43f69f7acc7 100644 --- a/runtime/src/test/groovy/io/micronaut/support/AbstractBeanDefinitionSpec.groovy +++ b/runtime/src/test/groovy/io/micronaut/support/AbstractBeanDefinitionSpec.groovy @@ -16,19 +16,11 @@ package io.micronaut.support import groovy.transform.CompileStatic -import io.micronaut.ast.groovy.annotation.GroovyAnnotationMetadataBuilder import io.micronaut.ast.groovy.utils.InMemoryByteCodeGroovyClassLoader -import io.micronaut.core.annotation.AnnotationMetadata import io.micronaut.core.naming.NameUtils import io.micronaut.inject.BeanDefinition -import io.micronaut.inject.annotation.AnnotationMetadataWriter import io.micronaut.inject.writer.BeanDefinitionWriter -import org.codehaus.groovy.ast.ASTNode -import org.codehaus.groovy.ast.ClassNode -import org.codehaus.groovy.ast.MethodNode -import org.codehaus.groovy.ast.builder.AstBuilder import spock.lang.Specification - /** * @author graemerocher * @since 1.0 @@ -46,48 +38,4 @@ abstract class AbstractBeanDefinitionSpec extends Specification { return (BeanDefinition)classLoader.loadClass(beanFullName).newInstance() } - AnnotationMetadata buildTypeAnnotationMetadata(String cls, String source) { - ASTNode[] nodes = new AstBuilder().buildFromString(source) - - ClassNode element = nodes ? nodes.find { it instanceof ClassNode && it.name == cls } : null - GroovyAnnotationMetadataBuilder builder = new GroovyAnnotationMetadataBuilder() - AnnotationMetadata metadata = element != null ? builder.build(element) : null - return metadata - } - - AnnotationMetadata buildMethodAnnotationMetadata(String cls, String source, String methodName) { - ClassNode element = buildClassNode(source, cls) - MethodNode method = element.getMethods(methodName)[0] - GroovyAnnotationMetadataBuilder builder = new GroovyAnnotationMetadataBuilder() - AnnotationMetadata metadata = method != null ? builder.build(method) : null - return metadata - } - - ClassNode buildClassNode(String source, String cls) { - ASTNode[] nodes = new AstBuilder().buildFromString(source) - - ClassNode element = nodes ? nodes.find { it instanceof ClassNode && it.name == cls } : null - return element - } - - - protected AnnotationMetadata writeAndLoadMetadata(String className, AnnotationMetadata toWrite) { - def stream = new ByteArrayOutputStream() - new AnnotationMetadataWriter(className, toWrite) - .writeTo(stream) - className = className + AnnotationMetadata.CLASS_NAME_SUFFIX - ClassLoader classLoader = new ClassLoader() { - @Override - protected Class findClass(String name) throws ClassNotFoundException { - if (name == className) { - def bytes = stream.toByteArray() - return defineClass(name, bytes, 0, bytes.length) - } - return super.findClass(name) - } - } - - AnnotationMetadata metadata = (AnnotationMetadata) classLoader.loadClass(className).newInstance() - return metadata - } } diff --git a/validation/src/main/java/io/micronaut/validation/validator/constraints/DefaultConstraintValidators.java b/validation/src/main/java/io/micronaut/validation/validator/constraints/DefaultConstraintValidators.java index 5d85b932beb..32f431ca3a7 100644 --- a/validation/src/main/java/io/micronaut/validation/validator/constraints/DefaultConstraintValidators.java +++ b/validation/src/main/java/io/micronaut/validation/validator/constraints/DefaultConstraintValidators.java @@ -460,8 +460,9 @@ public Optional> findConstra validatorCache.put(key, local.get()); return (Optional) local; } else if (beanContext != null) { - final ConstraintValidator cv = beanContext - .findBean(ConstraintValidator.class, qualifier).orElse(ConstraintValidator.VALID); + Optional bean = beanContext + .findBean(ConstraintValidator.class, qualifier); + final ConstraintValidator cv = bean.orElse(ConstraintValidator.VALID); validatorCache.put(key, cv); if (cv != ConstraintValidator.VALID) { return Optional.of(cv); diff --git a/validation/src/main/resources/META-INF/services/io.micronaut.inject.visitor.TypeElementVisitor b/validation/src/main/resources/META-INF/services/io.micronaut.inject.visitor.TypeElementVisitor deleted file mode 100644 index 82eb87a7f60..00000000000 --- a/validation/src/main/resources/META-INF/services/io.micronaut.inject.visitor.TypeElementVisitor +++ /dev/null @@ -1,3 +0,0 @@ -io.micronaut.validation.async.AsyncTypeElementVisitor -io.micronaut.validation.internal.InternalApiTypeElementVisitor -io.micronaut.validation.executable.ExecutableVisitor \ No newline at end of file diff --git a/validation/src/test/groovy/io/micronaut/validation/Foo.java b/validation/src/test/groovy/io/micronaut/validation/Foo.java index defbc6d1a6c..26fe3bd1465 100644 --- a/validation/src/test/groovy/io/micronaut/validation/Foo.java +++ b/validation/src/test/groovy/io/micronaut/validation/Foo.java @@ -29,7 +29,7 @@ * @author Graeme Rocher * @since 1.0 */ -@Singleton +@Singleton // Groovy @Singleton!!! @Validated public class Foo { @@ -77,4 +77,4 @@ public String getProp() { public void setProp(@NotNull String prop) { this.prop = prop; } -} \ No newline at end of file +} diff --git a/validation/src/test/groovy/io/micronaut/validation/validator/ValidatorSpec.groovy b/validation/src/test/groovy/io/micronaut/validation/validator/ValidatorSpec.groovy index 9fd8e308324..a75c8219c7a 100644 --- a/validation/src/test/groovy/io/micronaut/validation/validator/ValidatorSpec.groovy +++ b/validation/src/test/groovy/io/micronaut/validation/validator/ValidatorSpec.groovy @@ -11,6 +11,7 @@ import spock.lang.Ignore import spock.lang.Shared import spock.lang.Specification +import javax.validation.ConstraintViolationException import javax.validation.ElementKind import javax.validation.Valid import javax.validation.ValidatorFactory @@ -335,11 +336,21 @@ class ValidatorSpec extends Specification { when: arrayTest = applicationContext.createBean(ArrayTest) - arrayTest.integers = [30,10,60] as int[] + arrayTest.integers = [30, 10, 60] as int[] // Groovy property method access and validation + + then: + def e = thrown(ConstraintViolationException) + e.message.contains "setIntegers.integers[0]: must be less than or equal to 20" + e.message.contains "setIntegers.integers[2]: must be less than or equal to 20" + + when: + arrayTest = new ArrayTest() + arrayTest.integers = [30, 10, 60] as int[] // No interceptor + def violations = validator.forExecutables().validateParameters( - new ArrayTest(), - ArrayTest.getDeclaredMethod("saveChild", ArrayTest.class), - [arrayTest] as Object[] + new ArrayTest(), + ArrayTest.getDeclaredMethod("saveChild", ArrayTest.class), + [arrayTest] as Object[] ).toList().sort({ it -> it.propertyPath.toString() }) then: From 0a9590736f834a7b86856b06176abffdadcc15ca Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Wed, 12 Oct 2022 17:51:59 +0200 Subject: [PATCH 106/743] fix(deps): update managed-swagger to v2.2.3 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index a9c9875415a..7048df75427 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -138,7 +138,7 @@ managed-spock = "2.0-groovy-3.0" managed-spotbugs = "4.7.1" managed-spring = "5.3.23" managed-springboot = "2.7.0" -managed-swagger = "2.2.2" +managed-swagger = "2.2.3" managed-validation = "2.0.1.Final" managed-testcontainers = "1.17.3" managed-snakeyaml = "1.32" From 5616d727d414119922f24a43aac4e941a947a715 Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Wed, 12 Oct 2022 17:52:28 +0200 Subject: [PATCH 107/743] fix(deps): update managed-testcontainers to v1.17.5 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 7048df75427..52fbe260879 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -140,7 +140,7 @@ managed-spring = "5.3.23" managed-springboot = "2.7.0" managed-swagger = "2.2.3" managed-validation = "2.0.1.Final" -managed-testcontainers = "1.17.3" +managed-testcontainers = "1.17.5" managed-snakeyaml = "1.32" micronaut-docs = "2.0.0" From 172945eca469b31fa09e4666f71f7458e78640f3 Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Wed, 12 Oct 2022 17:52:56 +0200 Subject: [PATCH 108/743] fix(deps): update junit5 monorepo to v5.9.1 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 52fbe260879..05a630cd6a3 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -36,7 +36,7 @@ wiremock = "2.33.2" managed-dekorate = "1.0.3" managed-elasticsearch = "7.16.3" managed-ignite = "2.13.0" -managed-junit5 = "5.9.0" +managed-junit5 = "5.9.1" managed-kotlin = "1.6.21" managed-kotlin-coroutines = "1.5.1" managed-google-function-framework = "1.0.4" From 3bcd8294fe22c4f908d1d9d1825ec6f489e3f148 Mon Sep 17 00:00:00 2001 From: Jonas Konrad Date: Wed, 12 Oct 2022 18:27:05 +0200 Subject: [PATCH 109/743] Do not log channel close exceptions when writing response (#8149) I don't know how to write a test for it, unfortunately. Fixes #8144 --- .../io/micronaut/http/server/netty/RoutingInBoundHandler.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/RoutingInBoundHandler.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/RoutingInBoundHandler.java index cbf7f9b8d58..63aebb4afcf 100644 --- a/http-server-netty/src/main/java/io/micronaut/http/server/netty/RoutingInBoundHandler.java +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/RoutingInBoundHandler.java @@ -1209,7 +1209,7 @@ private void writeFinalNettyResponse(MutableHttpResponse message, HttpRequest try { if (!future.isSuccess()) { final Throwable throwable = future.cause(); - if (!(throwable instanceof ClosedChannelException)) { + if (!isIgnorable(throwable)) { if (throwable instanceof Http2Exception.StreamException) { Http2Exception.StreamException se = (Http2Exception.StreamException) throwable; if (se.error() == Http2Error.STREAM_CLOSED) { @@ -1453,7 +1453,7 @@ private ByteBuf encodeBodyAsByteBuf( * @param cause The cause * @return True if it can be ignored. */ - protected boolean isIgnorable(Throwable cause) { + final boolean isIgnorable(Throwable cause) { if (cause instanceof ClosedChannelException || cause.getCause() instanceof ClosedChannelException) { return true; } From 5edc4768fa2ac1b2b64b8bf52da1199f74e358ce Mon Sep 17 00:00:00 2001 From: Jonas Konrad Date: Wed, 12 Oct 2022 18:27:39 +0200 Subject: [PATCH 110/743] Support custom HTTP status codes (#8147) This patch adds support for custom status codes to various HTTP server and client classes. - On HttpResponse, the code()/reason() getters and the corresponding setter become the "primary" accessors for the HTTP status. Usages of status() and getStatus() are replaced where possible. - HttpStatus instances are only computed lazily when the user calls status(), which will produce an error for unknown status codes. If the method is not called, there is no error. - Various HttpResponse implementations are changed to not store HttpStatus. Fixes #4791 --- .../http/client/ServiceHttpClientFactory.java | 10 ++-- .../HttpClientResponseException.java | 2 +- .../http/client/netty/DefaultHttpClient.java | 49 +++++++------------ .../netty/FullNettyClientHttpResponse.java | 10 ++-- .../netty/NettyStreamedHttpResponse.java | 24 ++++----- .../http/client/InvalidStatusSpec.groovy | 46 ++++++++++------- .../FullNettyClientHttpResponseSpec.groovy | 14 ++++-- .../NettyStreamedHttpResponseSpec.groovy | 13 +++-- .../http/netty/NettyMutableHttpResponse.java | 16 +++--- .../netty/NettyHttpResponseFactory.java | 14 +++++- .../server/netty/RoutingInBoundHandler.java | 9 ++-- .../netty/encoders/HttpResponseEncoder.java | 2 +- .../micronaut/http/server/RouteExecutor.java | 8 ++- .../HateoasErrorResponseProcessor.java | 4 +- .../java/io/micronaut/http/HttpResponse.java | 26 +++++++--- .../micronaut/http/HttpResponseFactory.java | 10 ++++ .../micronaut/http/HttpResponseWrapper.java | 9 +++- .../java/io/micronaut/http/HttpStatus.java | 21 ++++++++ .../micronaut/http/MutableHttpResponse.java | 15 +++--- .../http/simple/SimpleHttpResponse.java | 26 +++++++--- .../simple/SimpleHttpResponseFactory.java | 5 ++ 21 files changed, 201 insertions(+), 132 deletions(-) diff --git a/http-client-core/src/main/java/io/micronaut/http/client/ServiceHttpClientFactory.java b/http-client-core/src/main/java/io/micronaut/http/client/ServiceHttpClientFactory.java index f7250d46473..ca9f17c7d95 100644 --- a/http-client-core/src/main/java/io/micronaut/http/client/ServiceHttpClientFactory.java +++ b/http-client-core/src/main/java/io/micronaut/http/client/ServiceHttpClientFactory.java @@ -27,11 +27,11 @@ import io.micronaut.discovery.StaticServiceInstanceList; import io.micronaut.http.HttpRequest; import io.micronaut.http.HttpResponse; -import io.micronaut.http.HttpStatus; import io.micronaut.http.client.exceptions.HttpClientResponseException; import io.micronaut.runtime.server.event.ServerStartupEvent; import io.micronaut.scheduling.TaskScheduler; import reactor.core.publisher.Flux; + import java.net.URI; import java.time.Duration; import java.util.Collection; @@ -113,12 +113,12 @@ ApplicationEventListener healthCheckStarter(@Parameter Servi return Flux.just((HttpResponse) responseException.getResponse()); } return Flux.just(HttpResponse.serverError()); - }).map(response -> Collections.singletonMap(originalURI, response.getStatus())); + }).map(response -> Collections.singletonMap(originalURI, response.code())); }).subscribe(uriToStatusMap -> { - Map.Entry entry = uriToStatusMap.entrySet().iterator().next(); + Map.Entry entry = uriToStatusMap.entrySet().iterator().next(); URI uri = entry.getKey(); - HttpStatus status = entry.getValue(); - if (status.getCode() >= 300) { + int status = entry.getValue(); + if (status >= 300) { loadBalancedURIs.remove(uri); } else if (!loadBalancedURIs.contains(uri)) { loadBalancedURIs.add(uri); diff --git a/http-client-core/src/main/java/io/micronaut/http/client/exceptions/HttpClientResponseException.java b/http-client-core/src/main/java/io/micronaut/http/client/exceptions/HttpClientResponseException.java index 8c078373c10..12a1f3678e6 100644 --- a/http-client-core/src/main/java/io/micronaut/http/client/exceptions/HttpClientResponseException.java +++ b/http-client-core/src/main/java/io/micronaut/http/client/exceptions/HttpClientResponseException.java @@ -101,7 +101,7 @@ private void initResponse(HttpResponse response) { private Argument getErrorType(HttpResponse response) { Optional contentType = response.getContentType(); Argument errorType = null; - if (contentType.isPresent() && response.getStatus().getCode() > 399) { + if (contentType.isPresent() && response.code() > 399) { MediaType mediaType = contentType.get(); if (errorDecoder != null) { errorType = errorDecoder.getErrorType(mediaType); diff --git a/http-client/src/main/java/io/micronaut/http/client/netty/DefaultHttpClient.java b/http-client/src/main/java/io/micronaut/http/client/netty/DefaultHttpClient.java index 8bf5d2456f7..0990fbd7597 100644 --- a/http-client/src/main/java/io/micronaut/http/client/netty/DefaultHttpClient.java +++ b/http-client/src/main/java/io/micronaut/http/client/netty/DefaultHttpClient.java @@ -859,10 +859,7 @@ private Flux>> exchangeStreamImpl(io.micronaut.ht traceBody("Response", byteBuf); } ByteBuffer byteBuffer = byteBufferFactory.wrap(byteBuf); - NettyStreamedHttpResponse> thisResponse = new NettyStreamedHttpResponse<>( - streamedHttpResponse, - response.status() - ); + NettyStreamedHttpResponse> thisResponse = new NettyStreamedHttpResponse<>(streamedHttpResponse); thisResponse.setBody(byteBuffer); return (HttpResponse>) new HttpResponseWrapper<>(thisResponse); }); @@ -1396,7 +1393,7 @@ public void onError(Throwable t) { public void onComplete() { try { FullHttpResponse fullHttpResponse = new DefaultFullHttpResponse(nettyResponse.protocolVersion(), nettyResponse.status(), buffer, nettyResponse.headers(), new DefaultHttpHeaders(true)); - final FullNettyClientHttpResponse fullNettyClientHttpResponse = new FullNettyClientHttpResponse<>(fullHttpResponse, response.status(), mediaTypeCodecRegistry, byteBufferFactory, (Argument) errorType, true); + final FullNettyClientHttpResponse fullNettyClientHttpResponse = new FullNettyClientHttpResponse<>(fullHttpResponse, mediaTypeCodecRegistry, byteBufferFactory, (Argument) errorType, true); fullNettyClientHttpResponse.onComplete(); emitter.error(customizeException(new HttpClientResponseException( fullHttpResponse.status().reasonPhrase(), @@ -1520,7 +1517,7 @@ private > Flux handleStreamHttpError( ) { boolean errorStatus = response.code() >= 400; if (errorStatus && failOnError) { - return Flux.error(customizeException(new HttpClientResponseException(response.getStatus().getReason(), response))); + return Flux.error(customizeException(new HttpClientResponseException(response.reason(), response))); } else { return Flux.just(response); } @@ -2079,22 +2076,12 @@ protected void channelReadInstrumented(ChannelHandlerContext ctx, R msg) throws return; } - HttpResponseStatus status = msg.status(); - int statusCode = status.code(); - HttpStatus httpStatus; - try { - httpStatus = HttpStatus.valueOf(statusCode); - } catch (IllegalArgumentException e) { - responsePromise.tryFailure(e); - return; - } - HttpHeaders headers = msg.headers(); if (log.isTraceEnabled()) { log.trace("HTTP Client Response Received ({}) for Request: {} {}", msg.status(), finalRequest.getMethodName(), finalRequest.getUri()); traceHeaders(headers); } - buildResponse(responsePromise, msg, httpStatus); + buildResponse(responsePromise, msg); } private void setRedirectHeaders(@Nullable io.micronaut.http.HttpRequest request, MutableHttpRequest redirectRequest) { @@ -2116,7 +2103,7 @@ private void setRedirectHeaders(@Nullable io.micronaut.http.HttpRequest reque protected abstract Function> makeRedirectHandler(io.micronaut.http.HttpRequest parentRequest, MutableHttpRequest redirectRequest); - protected abstract void buildResponse(Promise promise, R msg, HttpStatus httpStatus); + protected abstract void buildResponse(Promise promise, R msg); } private class FullHttpResponseHandler extends BaseHttpResponseHandler> { @@ -2175,21 +2162,21 @@ protected void channelReadInstrumented(ChannelHandlerContext channelHandlerConte } @Override - protected void buildResponse(Promise> promise, FullHttpResponse msg, HttpStatus httpStatus) { + protected void buildResponse(Promise> promise, FullHttpResponse msg) { try { if (log.isTraceEnabled()) { traceBody("Response", msg.content()); } - if (httpStatus == HttpStatus.NO_CONTENT) { + if (msg.status().equals(HttpResponseStatus.NO_CONTENT)) { // normalize the NO_CONTENT header, since http content aggregator adds it even if not present in the response msg.headers().remove(HttpHeaderNames.CONTENT_LENGTH); } - boolean convertBodyWithBodyType = httpStatus.getCode() < 400 || + boolean convertBodyWithBodyType = msg.status().code() < 400 || (!DefaultHttpClient.this.configuration.isExceptionOnErrorStatus() && bodyType.equalsType(errorType)); FullNettyClientHttpResponse response - = new FullNettyClientHttpResponse<>(msg, httpStatus, mediaTypeCodecRegistry, byteBufferFactory, bodyType, convertBodyWithBodyType); + = new FullNettyClientHttpResponse<>(msg, mediaTypeCodecRegistry, byteBufferFactory, bodyType, convertBodyWithBodyType); if (convertBodyWithBodyType) { promise.trySuccess(response); @@ -2203,13 +2190,13 @@ protected void buildResponse(Promise> promise, FullHttpR response.onComplete(); } catch (Exception t) { response.onComplete(); - promise.tryFailure(makeErrorBodyParseError(msg, httpStatus, t)); + promise.tryFailure(makeErrorBodyParseError(msg, t)); } } } catch (HttpClientResponseException t) { promise.tryFailure(t); } catch (Exception t) { - makeNormalBodyParseError(msg, httpStatus, t, cause -> { + makeNormalBodyParseError(msg, t, cause -> { if (!promise.tryFailure(cause) && log.isWarnEnabled()) { log.warn("Exception fired after handler completed: " + t.getMessage(), t); } @@ -2241,10 +2228,9 @@ public Argument getErrorType(MediaType mediaType) { /** * Create a {@link HttpClientResponseException} if parsing of the HTTP error body failed. */ - private HttpClientResponseException makeErrorBodyParseError(FullHttpResponse fullResponse, HttpStatus httpStatus, Throwable t) { + private HttpClientResponseException makeErrorBodyParseError(FullHttpResponse fullResponse, Throwable t) { FullNettyClientHttpResponse errorResponse = new FullNettyClientHttpResponse<>( fullResponse, - httpStatus, mediaTypeCodecRegistry, byteBufferFactory, null, @@ -2260,10 +2246,9 @@ private HttpClientResponseException makeErrorBodyParseError(FullHttpResponse ful )); } - private void makeNormalBodyParseError(FullHttpResponse fullResponse, HttpStatus httpStatus, Throwable t, Consumer forward) { + private void makeNormalBodyParseError(FullHttpResponse fullResponse, Throwable t, Consumer forward) { FullNettyClientHttpResponse response = new FullNettyClientHttpResponse<>( fullResponse, - httpStatus, mediaTypeCodecRegistry, byteBufferFactory, null, @@ -2311,7 +2296,7 @@ public boolean acceptInboundMessage(Object msg) { } @Override - protected void buildResponse(Promise> promise, FullHttpResponse msg, HttpStatus httpStatus) { + protected void buildResponse(Promise> promise, FullHttpResponse msg) { Publisher bodyPublisher; if (msg.content() instanceof EmptyByteBuf) { bodyPublisher = Publishers.empty(); @@ -2324,7 +2309,7 @@ protected void buildResponse(Promise> promise, FullHttpR msg.headers(), bodyPublisher ); - promise.trySuccess(new NettyStreamedHttpResponse<>(nettyResponse, httpStatus)); + promise.trySuccess(new NettyStreamedHttpResponse<>(nettyResponse)); } @Override @@ -2344,8 +2329,8 @@ public boolean acceptInboundMessage(Object msg) { } @Override - protected void buildResponse(Promise> promise, StreamedHttpResponse msg, HttpStatus httpStatus) { - promise.trySuccess(new NettyStreamedHttpResponse<>(msg, httpStatus)); + protected void buildResponse(Promise> promise, StreamedHttpResponse msg) { + promise.trySuccess(new NettyStreamedHttpResponse<>(msg)); } @Override diff --git a/http-client/src/main/java/io/micronaut/http/client/netty/FullNettyClientHttpResponse.java b/http-client/src/main/java/io/micronaut/http/client/netty/FullNettyClientHttpResponse.java index d7e8c4e2ba6..d106a6d7b2f 100644 --- a/http-client/src/main/java/io/micronaut/http/client/netty/FullNettyClientHttpResponse.java +++ b/http-client/src/main/java/io/micronaut/http/client/netty/FullNettyClientHttpResponse.java @@ -61,7 +61,6 @@ public class FullNettyClientHttpResponse implements HttpResponse, Completa private static final Logger LOG = LoggerFactory.getLogger(DefaultHttpClient.class); - private final HttpStatus status; private final NettyHttpHeaders headers; private final NettyCookies nettyCookies; private final MutableConvertibleValues attributes; @@ -75,7 +74,6 @@ public class FullNettyClientHttpResponse implements HttpResponse, Completa /** * @param fullHttpResponse The full Http response - * @param httpStatus The Http status * @param mediaTypeCodecRegistry The media type codec registry * @param byteBufferFactory The byte buffer factory * @param bodyType The body type @@ -83,13 +81,11 @@ public class FullNettyClientHttpResponse implements HttpResponse, Completa */ FullNettyClientHttpResponse( FullHttpResponse fullHttpResponse, - HttpStatus httpStatus, MediaTypeCodecRegistry mediaTypeCodecRegistry, ByteBufferFactory byteBufferFactory, Argument bodyType, boolean convertBody) { - this.status = httpStatus; this.headers = new NettyHttpHeaders(fullHttpResponse.headers(), ConversionService.SHARED); this.attributes = new MutableConvertibleValuesMap<>(); this.nettyHttpResponse = fullHttpResponse; @@ -120,8 +116,8 @@ public String reason() { } @Override - public HttpStatus getStatus() { - return status; + public int code() { + return this.nettyHttpResponse.status().code(); } @Override @@ -217,7 +213,7 @@ public Optional getBody(Argument type) { converted = convertByteBuf(content, finalArgument); } } catch (RuntimeException e) { - if (status.getCode() < 400) { + if (code() < 400) { throw e; } else { if (LOG.isDebugEnabled()) { diff --git a/http-client/src/main/java/io/micronaut/http/client/netty/NettyStreamedHttpResponse.java b/http-client/src/main/java/io/micronaut/http/client/netty/NettyStreamedHttpResponse.java index b40a4480e35..9d9ed4d521a 100644 --- a/http-client/src/main/java/io/micronaut/http/client/netty/NettyStreamedHttpResponse.java +++ b/http-client/src/main/java/io/micronaut/http/client/netty/NettyStreamedHttpResponse.java @@ -22,7 +22,6 @@ import io.micronaut.core.convert.ConversionService; import io.micronaut.core.convert.value.MutableConvertibleValues; import io.micronaut.core.convert.value.MutableConvertibleValuesMap; -import io.micronaut.http.HttpStatus; import io.micronaut.http.MutableHttpHeaders; import io.micronaut.http.MutableHttpResponse; import io.micronaut.http.cookie.Cookie; @@ -34,9 +33,9 @@ import io.micronaut.http.netty.stream.StreamedHttpResponse; import io.netty.handler.codec.http.FullHttpResponse; import io.netty.handler.codec.http.HttpHeaderNames; +import io.netty.handler.codec.http.HttpResponseStatus; import io.netty.handler.codec.http.cookie.ServerCookieEncoder; -import java.util.Objects; import java.util.Optional; /** @@ -50,7 +49,6 @@ class NettyStreamedHttpResponse implements MutableHttpResponse, NettyHttpResponseBuilder { private final StreamedHttpResponse nettyResponse; - private HttpStatus status; private final NettyHttpHeaders headers; @GuardedBy("this") private NettyCookies nettyCookies; // initialized lazily @@ -59,11 +57,9 @@ class NettyStreamedHttpResponse implements MutableHttpResponse, NettyHttpR /** * @param response The streamed Http response - * @param httpStatus The Http status */ - NettyStreamedHttpResponse(StreamedHttpResponse response, HttpStatus httpStatus) { + NettyStreamedHttpResponse(StreamedHttpResponse response) { this.nettyResponse = response; - this.status = httpStatus; this.headers = new NettyHttpHeaders(response.headers(), ConversionService.SHARED); } @@ -75,13 +71,13 @@ public StreamedHttpResponse getNettyResponse() { } @Override - public String reason() { - return nettyResponse.status().reasonPhrase(); + public int code() { + return nettyResponse.status().code(); } @Override - public HttpStatus getStatus() { - return status; + public String reason() { + return nettyResponse.status().reasonPhrase(); } @Override @@ -175,8 +171,12 @@ public MutableHttpResponse body(@Nullable T body) { } @Override - public MutableHttpResponse status(HttpStatus status, CharSequence message) { - this.status = Objects.requireNonNull(status, "Status is required"); + public MutableHttpResponse status(int status, CharSequence message) { + if (message == null) { + nettyResponse.setStatus(HttpResponseStatus.valueOf(status)); + } else { + nettyResponse.setStatus(HttpResponseStatus.valueOf(status, message.toString())); + } return this; } } diff --git a/http-client/src/test/groovy/io/micronaut/http/client/InvalidStatusSpec.groovy b/http-client/src/test/groovy/io/micronaut/http/client/InvalidStatusSpec.groovy index 99f83595931..37d19bfe99a 100644 --- a/http-client/src/test/groovy/io/micronaut/http/client/InvalidStatusSpec.groovy +++ b/http-client/src/test/groovy/io/micronaut/http/client/InvalidStatusSpec.groovy @@ -1,11 +1,12 @@ package io.micronaut.http.client -import com.github.tomakehurst.wiremock.WireMockServer -import com.github.tomakehurst.wiremock.client.WireMock -import com.github.tomakehurst.wiremock.core.WireMockConfiguration + import io.micronaut.context.ApplicationContext -import io.micronaut.http.HttpRequest -import reactor.core.publisher.Flux +import io.micronaut.context.annotation.Requires +import io.micronaut.http.HttpResponse +import io.micronaut.http.annotation.Controller +import io.micronaut.http.annotation.Get +import io.micronaut.runtime.server.EmbeddedServer import spock.lang.AutoCleanup import spock.lang.Shared import spock.lang.Specification @@ -14,32 +15,39 @@ class InvalidStatusSpec extends Specification { @Shared @AutoCleanup - ApplicationContext context = ApplicationContext.run() + ApplicationContext context = ApplicationContext.run([ + 'spec.name': 'InvalidStatusSpec' + ]) void "test receiving an invalid status code"() { given: - WireMockServer wireMockServer = new WireMockServer(WireMockConfiguration.options().dynamicPort()) - wireMockServer.start() - wireMockServer.stubFor(WireMock.get("/status-only") - .willReturn(WireMock.status(519))) - StreamingHttpClient client = context.createBean(StreamingHttpClient, new URL("http://localhost:${wireMockServer.port()}")) + EmbeddedServer server = context.getBean(EmbeddedServer) + server.start() + StreamingHttpClient client = context.createBean(StreamingHttpClient, server.URL) when: - client.toBlocking().exchange("/status-only", String) + def response = client.toBlocking().exchange("/invalid-status", String) then: - def ex = thrown(IllegalArgumentException) - ex.message == "Invalid HTTP status code: 519" + response.code() == 290 when: - Flux.from(client.dataStream(HttpRequest.GET("/status-only"))).blockFirst() - + response.status() then: - ex = thrown(IllegalArgumentException) - ex.message == "Invalid HTTP status code: 519" + def ex = thrown(IllegalArgumentException) + ex.message == "Invalid HTTP status code: 290" cleanup: client.close() - wireMockServer.stop() + server.stop() + } + + @Controller('/invalid-status') + @Requires(property = 'spec.name', value = 'InvalidStatusSpec') + static class InvalidStatusController { + @Get + HttpResponse status() { + return HttpResponse.ok().status(290) + } } } diff --git a/http-client/src/test/groovy/io/micronaut/http/client/netty/FullNettyClientHttpResponseSpec.groovy b/http-client/src/test/groovy/io/micronaut/http/client/netty/FullNettyClientHttpResponseSpec.groovy index f587243f852..611e4c9ca4d 100644 --- a/http-client/src/test/groovy/io/micronaut/http/client/netty/FullNettyClientHttpResponseSpec.groovy +++ b/http-client/src/test/groovy/io/micronaut/http/client/netty/FullNettyClientHttpResponseSpec.groovy @@ -1,9 +1,15 @@ package io.micronaut.http.client.netty -import io.micronaut.http.HttpStatus + import io.micronaut.http.cookie.Cookie import io.micronaut.http.cookie.Cookies -import io.netty.handler.codec.http.* +import io.netty.handler.codec.http.DefaultFullHttpResponse +import io.netty.handler.codec.http.DefaultHttpHeaders +import io.netty.handler.codec.http.FullHttpResponse +import io.netty.handler.codec.http.HttpHeaderNames +import io.netty.handler.codec.http.HttpHeaders +import io.netty.handler.codec.http.HttpResponseStatus +import io.netty.handler.codec.http.HttpVersion import spock.lang.Specification class FullNettyClientHttpResponseSpec extends Specification { @@ -16,7 +22,7 @@ class FullNettyClientHttpResponseSpec extends Specification { fullHttpResponse.headers().set(httpHeaders) when: - FullNettyClientHttpResponse response = new FullNettyClientHttpResponse(fullHttpResponse, HttpStatus.OK, null, null, null, false) + FullNettyClientHttpResponse response = new FullNettyClientHttpResponse(fullHttpResponse, null, null, null, false) then: Cookies cookies = response.getCookies() @@ -39,7 +45,7 @@ class FullNettyClientHttpResponseSpec extends Specification { fullHttpResponse.headers().set(httpHeaders) when: - FullNettyClientHttpResponse response = new FullNettyClientHttpResponse(fullHttpResponse, HttpStatus.OK, null, null, null, false) + FullNettyClientHttpResponse response = new FullNettyClientHttpResponse(fullHttpResponse, null, null, null, false) then: Cookies cookies = response.getCookies() diff --git a/http-client/src/test/groovy/io/micronaut/http/client/netty/NettyStreamedHttpResponseSpec.groovy b/http-client/src/test/groovy/io/micronaut/http/client/netty/NettyStreamedHttpResponseSpec.groovy index 1dc62dde0c6..6eae29b79d9 100644 --- a/http-client/src/test/groovy/io/micronaut/http/client/netty/NettyStreamedHttpResponseSpec.groovy +++ b/http-client/src/test/groovy/io/micronaut/http/client/netty/NettyStreamedHttpResponseSpec.groovy @@ -1,11 +1,14 @@ package io.micronaut.http.client.netty -import io.micronaut.http.HttpStatus import io.micronaut.http.cookie.Cookie import io.micronaut.http.cookie.Cookies import io.micronaut.http.netty.stream.DefaultStreamedHttpResponse import io.micronaut.http.netty.stream.StreamedHttpResponse -import io.netty.handler.codec.http.* +import io.netty.handler.codec.http.DefaultHttpHeaders +import io.netty.handler.codec.http.HttpHeaderNames +import io.netty.handler.codec.http.HttpHeaders +import io.netty.handler.codec.http.HttpResponseStatus +import io.netty.handler.codec.http.HttpVersion import spock.lang.Specification class NettyStreamedHttpResponseSpec extends Specification { @@ -18,7 +21,7 @@ class NettyStreamedHttpResponseSpec extends Specification { streamedHttpResponse.headers().set(httpHeaders) when: - NettyStreamedHttpResponse response = new NettyStreamedHttpResponse(streamedHttpResponse, HttpStatus.OK) + NettyStreamedHttpResponse response = new NettyStreamedHttpResponse(streamedHttpResponse) then: Cookies cookies = response.getCookies() @@ -41,7 +44,7 @@ class NettyStreamedHttpResponseSpec extends Specification { streamedHttpResponse.headers().set(httpHeaders) when: - NettyStreamedHttpResponse response = new NettyStreamedHttpResponse(streamedHttpResponse, HttpStatus.OK) + NettyStreamedHttpResponse response = new NettyStreamedHttpResponse(streamedHttpResponse) then: Cookies cookies = response.getCookies() @@ -65,7 +68,7 @@ class NettyStreamedHttpResponseSpec extends Specification { streamedHttpResponse.headers().set(httpHeaders) when: - NettyStreamedHttpResponse response = new NettyStreamedHttpResponse(streamedHttpResponse, HttpStatus.OK) + NettyStreamedHttpResponse response = new NettyStreamedHttpResponse(streamedHttpResponse) response.cookie(Cookie.of("ADDED", "xyz").httpOnly(true).domain(".foo.com")) then: diff --git a/http-netty/src/main/java/io/micronaut/http/netty/NettyMutableHttpResponse.java b/http-netty/src/main/java/io/micronaut/http/netty/NettyMutableHttpResponse.java index 95277e4fb7d..6fdf7718823 100644 --- a/http-netty/src/main/java/io/micronaut/http/netty/NettyMutableHttpResponse.java +++ b/http-netty/src/main/java/io/micronaut/http/netty/NettyMutableHttpResponse.java @@ -181,8 +181,7 @@ public HttpHeaders getNettyHeaders() { @Override public String toString() { - HttpStatus status = getStatus(); - return status.getCode() + " " + status.getReason(); + return code() + " " + reason(); } @Override @@ -205,11 +204,6 @@ public MutableConvertibleValues getAttributes() { return attributes; } - @Override - public HttpStatus getStatus() { - return HttpStatus.valueOf(httpResponseStatus.code()); - } - @Override public int code() { return httpResponseStatus.code(); @@ -260,9 +254,11 @@ public Optional getBody(Argument type) { } @Override - public MutableHttpResponse status(HttpStatus status, CharSequence message) { - message = message == null ? status.getReason() : message; - httpResponseStatus = new HttpResponseStatus(status.getCode(), message.toString()); + public MutableHttpResponse status(int status, CharSequence message) { + if (message == null) { + message = HttpStatus.getDefaultReason(status); + } + httpResponseStatus = new HttpResponseStatus(status, message.toString()); return this; } diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/NettyHttpResponseFactory.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/NettyHttpResponseFactory.java index e56d89b8c38..180b36cd1d3 100644 --- a/http-server-netty/src/main/java/io/micronaut/http/server/netty/NettyHttpResponseFactory.java +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/NettyHttpResponseFactory.java @@ -57,7 +57,19 @@ public MutableHttpResponse status(HttpStatus status, String reason) { if (reason == null) { nettyStatus = HttpResponseStatus.valueOf(status.getCode()); } else { - nettyStatus = new HttpResponseStatus(status.getCode(), reason); + nettyStatus = HttpResponseStatus.valueOf(status.getCode(), reason); + } + + return new NettyMutableHttpResponse(HttpVersion.HTTP_1_1, nettyStatus, ConversionService.SHARED); + } + + @Override + public MutableHttpResponse status(int status, String reason) { + HttpResponseStatus nettyStatus; + if (reason == null) { + nettyStatus = HttpResponseStatus.valueOf(status); + } else { + nettyStatus = HttpResponseStatus.valueOf(status, reason); } return new NettyMutableHttpResponse(HttpVersion.HTTP_1_1, nettyStatus, ConversionService.SHARED); diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/RoutingInBoundHandler.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/RoutingInBoundHandler.java index 44b6da7d8ff..2d09008fb93 100644 --- a/http-server-netty/src/main/java/io/micronaut/http/server/netty/RoutingInBoundHandler.java +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/RoutingInBoundHandler.java @@ -1195,7 +1195,7 @@ private void encodeResponseBody( } private void writeFinalNettyResponse(MutableHttpResponse message, HttpRequest request, ChannelHandlerContext context) { - HttpStatus httpStatus = message.status(); + int httpStatus = message.code(); final io.micronaut.http.HttpVersion httpVersion = request.getHttpVersion(); final boolean isHttp2 = httpVersion == io.micronaut.http.HttpVersion.HTTP_2_0; @@ -1233,7 +1233,7 @@ private void writeFinalNettyResponse(MutableHttpResponse message, HttpRequest // default Connection header if not set explicitly if (!isHttp2) { if (!message.getHeaders().contains(HttpHeaders.CONNECTION)) { - if (!decodeError && (httpStatus.getCode() < 500 || serverConfiguration.isKeepAliveOnServerError())) { + if (!decodeError && (httpStatus < 500 || serverConfiguration.isKeepAliveOnServerError())) { message.getHeaders().set(HttpHeaders.CONNECTION, HttpHeaderValues.KEEP_ALIVE); } else { message.getHeaders().set(HttpHeaders.CONNECTION, HttpHeaderValues.CLOSE); @@ -1251,7 +1251,7 @@ private void writeFinalNettyResponse(MutableHttpResponse message, HttpRequest if (!isHttp2) { if (!nettyHeaders.contains(HttpHeaderNames.CONNECTION)) { boolean expectKeepAlive = nettyResponse.protocolVersion().isKeepAliveDefault() || request.getHeaders().isKeepAlive(); - if (!decodeError && (expectKeepAlive || httpStatus.getCode() < 500 || serverConfiguration.isKeepAliveOnServerError())) { + if (!decodeError && (expectKeepAlive || httpStatus < 500 || serverConfiguration.isKeepAliveOnServerError())) { nettyHeaders.set(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE); } else { nettyHeaders.set(HttpHeaderNames.CONNECTION, HttpHeaderValues.CLOSE); @@ -1342,13 +1342,12 @@ private MutableHttpResponse toMutableResponse(HttpResponse message) { @NonNull private NettyMutableHttpResponse createNettyResponse(HttpResponse message) { - HttpStatus httpStatus = message.status(); Object body = message.body(); io.netty.handler.codec.http.HttpHeaders nettyHeaders = new DefaultHttpHeaders(serverConfiguration.isValidateHeaders()); message.getHeaders().forEach((BiConsumer>) nettyHeaders::set); return new NettyMutableHttpResponse<>( HttpVersion.HTTP_1_1, - HttpResponseStatus.valueOf(httpStatus.getCode(), httpStatus.getReason()), + HttpResponseStatus.valueOf(message.code(), message.reason()), body instanceof ByteBuf ? body : null, ConversionService.SHARED ); diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/encoders/HttpResponseEncoder.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/encoders/HttpResponseEncoder.java index 854868514cc..7110153926d 100644 --- a/http-server-netty/src/main/java/io/micronaut/http/server/netty/encoders/HttpResponseEncoder.java +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/encoders/HttpResponseEncoder.java @@ -121,7 +121,7 @@ protected void encode(ChannelHandlerContext context, MutableHttpResponse resp ByteBuf body = b instanceof ByteBuf ? (ByteBuf) b : Unpooled.buffer(0); FullHttpResponse nettyResponse = new DefaultFullHttpResponse( HttpVersion.HTTP_1_1, - HttpResponseStatus.valueOf(response.status().getCode(), response.status().getReason()), + HttpResponseStatus.valueOf(response.code(), response.reason()), body, nettyHeaders, EmptyHttpHeaders.INSTANCE diff --git a/http-server/src/main/java/io/micronaut/http/server/RouteExecutor.java b/http-server/src/main/java/io/micronaut/http/server/RouteExecutor.java index 4ece878cd99..871a9056818 100644 --- a/http-server/src/main/java/io/micronaut/http/server/RouteExecutor.java +++ b/http-server/src/main/java/io/micronaut/http/server/RouteExecutor.java @@ -516,11 +516,10 @@ private RouteMatch findErrorRoute(Throwable cause, private Publisher> handleStatusException(HttpRequest request, MutableHttpResponse response) { - HttpStatus status = response.status(); RouteInfo routeInfo = response.getAttribute(HttpAttributes.ROUTE_INFO, RouteInfo.class).orElse(null); - if (status.getCode() >= 400 && routeInfo != null && !routeInfo.isErrorRoute()) { - RouteMatch statusRoute = findStatusRoute(request, status, routeInfo); + if (response.code() >= 400 && routeInfo != null && !routeInfo.isErrorRoute()) { + RouteMatch statusRoute = findStatusRoute(request, response.status(), routeInfo); if (statusRoute != null) { return executeRoute( @@ -579,8 +578,7 @@ private MutableHttpResponse toMutableResponse(HttpResponse message) { if (message instanceof MutableHttpResponse) { mutableHttpResponse = (MutableHttpResponse) message; } else { - HttpStatus httpStatus = message.status(); - mutableHttpResponse = HttpResponse.status(httpStatus, httpStatus.getReason()); + mutableHttpResponse = HttpResponse.status(message.code(), message.reason()); mutableHttpResponse.body(message.body()); message.getHeaders().forEach((name, value) -> { for (String val : value) { diff --git a/http-server/src/main/java/io/micronaut/http/server/exceptions/response/HateoasErrorResponseProcessor.java b/http-server/src/main/java/io/micronaut/http/server/exceptions/response/HateoasErrorResponseProcessor.java index 756c4106b4c..64c28272121 100644 --- a/http-server/src/main/java/io/micronaut/http/server/exceptions/response/HateoasErrorResponseProcessor.java +++ b/http-server/src/main/java/io/micronaut/http/server/exceptions/response/HateoasErrorResponseProcessor.java @@ -64,13 +64,13 @@ public MutableHttpResponse processResponse(@NonNull ErrorContext erro } JsonError error; if (!errorContext.hasErrors()) { - error = new JsonError(response.getStatus().getReason()); + error = new JsonError(response.reason()); } else if (errorContext.getErrors().size() == 1 && !alwaysSerializeErrorsAsList) { Error jsonError = errorContext.getErrors().get(0); error = new JsonError(jsonError.getMessage()); jsonError.getPath().ifPresent(error::path); } else { - error = new JsonError(response.getStatus().getReason()); + error = new JsonError(response.reason()); List errors = new ArrayList<>(); for (Error jsonError : errorContext.getErrors()) { errors.add(new JsonError(jsonError.getMessage()).path(jsonError.getPath().orElse(null))); diff --git a/http/src/main/java/io/micronaut/http/HttpResponse.java b/http/src/main/java/io/micronaut/http/HttpResponse.java index 41242145a1a..a4df598304c 100644 --- a/http/src/main/java/io/micronaut/http/HttpResponse.java +++ b/http/src/main/java/io/micronaut/http/HttpResponse.java @@ -15,11 +15,11 @@ */ package io.micronaut.http; +import io.micronaut.core.annotation.Nullable; import io.micronaut.http.cookie.Cookie; import io.micronaut.http.cookie.Cookies; import io.micronaut.http.exceptions.UriSyntaxException; -import io.micronaut.core.annotation.Nullable; import java.net.URI; import java.net.URISyntaxException; import java.util.Optional; @@ -37,7 +37,9 @@ public interface HttpResponse extends HttpMessage { /** * @return The current status */ - HttpStatus getStatus(); + default HttpStatus getStatus() { + return HttpStatus.valueOf(code()); + } @Override default HttpResponse setAttribute(CharSequence name, Object value) { @@ -74,16 +76,12 @@ default HttpStatus status() { /** * @return The response status code */ - default int code() { - return getStatus().getCode(); - } + int code(); /** * @return The HTTP status reason phrase */ - default String reason() { - return getStatus().getReason(); - } + String reason(); /** * Return an {@link io.micronaut.http.HttpStatus#OK} response with an empty body. @@ -388,6 +386,18 @@ static MutableHttpResponse status(HttpStatus status, String reason) { return HttpResponseFactory.INSTANCE.status(status, reason); } + /** + * Return a response for the given status. + * + * @param status The status + * @param reason An alternatively reason message + * @param The response type + * @return The response + */ + static MutableHttpResponse status(int status, String reason) { + return HttpResponseFactory.INSTANCE.status(status, reason); + } + /** * Helper method for defining URIs. Rethrows checked exceptions as. * diff --git a/http/src/main/java/io/micronaut/http/HttpResponseFactory.java b/http/src/main/java/io/micronaut/http/HttpResponseFactory.java index 508fca6ee05..36ba91601ba 100644 --- a/http/src/main/java/io/micronaut/http/HttpResponseFactory.java +++ b/http/src/main/java/io/micronaut/http/HttpResponseFactory.java @@ -47,6 +47,16 @@ public interface HttpResponseFactory { */ MutableHttpResponse status(HttpStatus status, String reason); + /** + * Return a response for the given status. + * + * @param status The status + * @param reason An alternatively reason message + * @param The response type + * @return The response + */ + MutableHttpResponse status(int status, String reason); + /** * Return a response for the given status. * diff --git a/http/src/main/java/io/micronaut/http/HttpResponseWrapper.java b/http/src/main/java/io/micronaut/http/HttpResponseWrapper.java index cb1d3abf009..51f2c674836 100644 --- a/http/src/main/java/io/micronaut/http/HttpResponseWrapper.java +++ b/http/src/main/java/io/micronaut/http/HttpResponseWrapper.java @@ -31,8 +31,13 @@ public HttpResponseWrapper(HttpResponse delegate) { } @Override - public HttpStatus getStatus() { - return getDelegate().getStatus(); + public int code() { + return getDelegate().code(); + } + + @Override + public String reason() { + return getDelegate().reason(); } @Override diff --git a/http/src/main/java/io/micronaut/http/HttpStatus.java b/http/src/main/java/io/micronaut/http/HttpStatus.java index 0ee515fec0a..bbe0a2480f8 100644 --- a/http/src/main/java/io/micronaut/http/HttpStatus.java +++ b/http/src/main/java/io/micronaut/http/HttpStatus.java @@ -15,6 +15,9 @@ */ package io.micronaut.http; +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.NonNull; + import java.util.Objects; /** @@ -266,6 +269,24 @@ public static HttpStatus valueOf(int code) { } } + /** + * Get the default reason phrase for the given status code, if it is a known status code. + * + * @param code The status code + * @return The default reason phrase, or {@code "CUSTOM"} if the code is unknown. + */ + @Internal + @NonNull + public static String getDefaultReason(int code) { + HttpStatus httpStatus; + try { + httpStatus = valueOf(code); + } catch (IllegalArgumentException e) { + return "CUSTOM"; + } + return httpStatus.getReason(); + } + @Override public int length() { return name().length(); diff --git a/http/src/main/java/io/micronaut/http/MutableHttpResponse.java b/http/src/main/java/io/micronaut/http/MutableHttpResponse.java index a6b0e6aedbe..3ef2d8b5742 100644 --- a/http/src/main/java/io/micronaut/http/MutableHttpResponse.java +++ b/http/src/main/java/io/micronaut/http/MutableHttpResponse.java @@ -15,9 +15,9 @@ */ package io.micronaut.http; +import io.micronaut.core.annotation.Nullable; import io.micronaut.http.cookie.Cookie; -import io.micronaut.core.annotation.Nullable; import java.nio.charset.Charset; import java.util.Collections; import java.util.Locale; @@ -72,7 +72,12 @@ default MutableHttpResponse cookies(Set cookies) { * @param message The message * @return This response object */ - MutableHttpResponse status(HttpStatus status, CharSequence message); + default MutableHttpResponse status(HttpStatus status, CharSequence message) { + if (message == null) { + message = status.getReason(); + } + return status(status.getCode(), message); + } @Override default MutableHttpResponse headers(Consumer headers) { @@ -152,7 +157,7 @@ default MutableHttpResponse locale(Locale locale) { * @return This response object */ default MutableHttpResponse status(int status) { - return status(HttpStatus.valueOf(status)); + return status(status, null); } /** @@ -162,9 +167,7 @@ default MutableHttpResponse status(int status) { * @param message The message * @return This response object */ - default MutableHttpResponse status(int status, CharSequence message) { - return status(HttpStatus.valueOf(status), message); - } + MutableHttpResponse status(int status, CharSequence message); /** * Sets the response status. diff --git a/http/src/main/java/io/micronaut/http/simple/SimpleHttpResponse.java b/http/src/main/java/io/micronaut/http/simple/SimpleHttpResponse.java index 472336651eb..1b83622a15b 100644 --- a/http/src/main/java/io/micronaut/http/simple/SimpleHttpResponse.java +++ b/http/src/main/java/io/micronaut/http/simple/SimpleHttpResponse.java @@ -15,6 +15,7 @@ */ package io.micronaut.http.simple; +import io.micronaut.core.annotation.Nullable; import io.micronaut.core.annotation.TypeHint; import io.micronaut.core.convert.ConversionService; import io.micronaut.core.convert.value.MutableConvertibleValues; @@ -26,7 +27,6 @@ import io.micronaut.http.cookie.Cookies; import io.micronaut.http.simple.cookies.SimpleCookies; -import io.micronaut.core.annotation.Nullable; import java.util.Optional; import java.util.Set; @@ -45,7 +45,9 @@ class SimpleHttpResponse implements MutableHttpResponse { private final SimpleCookies cookies = new SimpleCookies(ConversionService.SHARED); private final MutableConvertibleValues attributes = new MutableConvertibleValuesMap<>(); - private HttpStatus status = HttpStatus.OK; + private int status = HttpStatus.OK.getCode(); + private String reason = HttpStatus.OK.getReason(); + private Object body; @Override @@ -84,14 +86,24 @@ public MutableHttpResponse body(@Nullable T body) { } @Override - public MutableHttpResponse status(HttpStatus status, CharSequence message) { - this.status = status; - return this; + public int code() { + return status; } @Override - public HttpStatus getStatus() { - return this.status; + public String reason() { + return reason; + } + + @Override + public MutableHttpResponse status(int status, CharSequence message) { + this.status = status; + if (message == null) { + this.reason = HttpStatus.getDefaultReason(status); + } else { + this.reason = message.toString(); + } + return this; } /** diff --git a/http/src/main/java/io/micronaut/http/simple/SimpleHttpResponseFactory.java b/http/src/main/java/io/micronaut/http/simple/SimpleHttpResponseFactory.java index 8bc0e0ced12..bc57ee44817 100644 --- a/http/src/main/java/io/micronaut/http/simple/SimpleHttpResponseFactory.java +++ b/http/src/main/java/io/micronaut/http/simple/SimpleHttpResponseFactory.java @@ -39,6 +39,11 @@ public MutableHttpResponse status(HttpStatus status, String reason) { return new SimpleHttpResponse().status(status, reason); } + @Override + public MutableHttpResponse status(int status, String reason) { + return new SimpleHttpResponse().status(status, reason); + } + @Override public MutableHttpResponse status(HttpStatus status, T body) { return new SimpleHttpResponse().status(status).body(body); From 81e2fcf875f03cd29a6b056b0e68bbd71518f47f Mon Sep 17 00:00:00 2001 From: Denis Stepanov Date: Thu, 13 Oct 2022 09:38:08 +0200 Subject: [PATCH 111/743] Allow for Bean Introspection to use different accessor for read and write. Support annotations with retention CLASS. (#8148) --- .../GroovyAnnotationMetadataBuilder.java | 4 + .../groovy/visitor/GroovyPropertyElement.java | 4 +- .../inject/annotation/RetentionSpec.groovy | 134 +++++ .../annotation/SourceRetentionSpec.groovy | 28 - .../ConfigurationPropertiesSpec.groovy | 2 + .../visitor/BeanIntrospectionSpec.groovy | 51 +- .../io/micronaut/inject/visitor/Test.groovy | 13 - .../inject/visitor/TestFieldAccess.groovy | 18 + .../beans/BeanIntrospectionSpec.groovy | 29 +- .../visitor/JavaPropertyElement.java | 4 +- .../inject/annotation/RetentionSpec.groovy | 136 +++++ .../annotation/SourceRetentionSpec.groovy | 30 -- .../ConfigurationPropertiesSpec.groovy | 2 + .../context/annotation/BeanProperties.java | 7 +- .../annotation/ConfigurationBuilder.java | 1 + .../annotation/ConfigurationReader.java | 1 + .../annotation/AnnotationMetadataWriter.java | 9 +- .../annotation/DefaultAnnotationMetadata.java | 3 +- .../micronaut/inject/ast/PropertyElement.java | 4 +- .../visitor/BeanIntrospectionWriter.java | 220 ++++---- .../IntrospectedTypeElementVisitor.java | 477 ++++-------------- ...ationPropertiesToBeanPropertiesMapper.java | 52 -- ...> IntrospectedToBeanPropertiesMapper.java} | 31 +- .../inject/writer/DispatchWriter.java | 9 + ...cronaut.inject.annotation.AnnotationMapper | 3 +- src/main/docs/guide/appendix/breaks.adoc | 8 + 26 files changed, 584 insertions(+), 696 deletions(-) create mode 100644 inject-groovy/src/test/groovy/io/micronaut/inject/annotation/RetentionSpec.groovy delete mode 100644 inject-groovy/src/test/groovy/io/micronaut/inject/annotation/SourceRetentionSpec.groovy delete mode 100644 inject-groovy/src/test/groovy/io/micronaut/inject/visitor/Test.groovy create mode 100644 inject-groovy/src/test/groovy/io/micronaut/inject/visitor/TestFieldAccess.groovy create mode 100644 inject-java/src/test/groovy/io/micronaut/inject/annotation/RetentionSpec.groovy delete mode 100644 inject-java/src/test/groovy/io/micronaut/inject/annotation/SourceRetentionSpec.groovy delete mode 100644 inject/src/main/java/io/micronaut/inject/mappers/ConfigurationPropertiesToBeanPropertiesMapper.java rename inject/src/main/java/io/micronaut/inject/mappers/{ConfigurationBuilderToBeanPropertiesMapper.java => IntrospectedToBeanPropertiesMapper.java} (52%) diff --git a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/annotation/GroovyAnnotationMetadataBuilder.java b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/annotation/GroovyAnnotationMetadataBuilder.java index 1cfc1f83842..f2b7a63fb02 100644 --- a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/annotation/GroovyAnnotationMetadataBuilder.java +++ b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/annotation/GroovyAnnotationMetadataBuilder.java @@ -233,6 +233,10 @@ protected String getRepeatableNameForType(AnnotatedNode annotationType) { @Override protected Optional getAnnotationMirror(String annotationName) { + Optional classNode = compilationUnit == null ? Optional.empty() : Optional.ofNullable(compilationUnit.getClassNode(annotationName)); + if (classNode.isPresent()) { + return classNode; + } ClassNode cn = ClassUtils.forName(annotationName, GroovyAnnotationMetadataBuilder.class.getClassLoader()) .map(ClassHelper::make) .orElseGet(() -> ClassHelper.make(annotationName)); diff --git a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyPropertyElement.java b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyPropertyElement.java index f14a6caaf40..e6ca6095f20 100644 --- a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyPropertyElement.java +++ b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyPropertyElement.java @@ -289,7 +289,7 @@ public AccessKind getWriteAccessKind() { @Override public boolean isReadOnly() { - switch (readAccessKind) { + switch (writeAccessKind) { case METHOD: return setter == null; case FIELD: @@ -301,7 +301,7 @@ public boolean isReadOnly() { @Override public boolean isWriteOnly() { - switch (writeAccessKind) { + switch (readAccessKind) { case METHOD: return getter == null; case FIELD: diff --git a/inject-groovy/src/test/groovy/io/micronaut/inject/annotation/RetentionSpec.groovy b/inject-groovy/src/test/groovy/io/micronaut/inject/annotation/RetentionSpec.groovy new file mode 100644 index 00000000000..3b489378b0d --- /dev/null +++ b/inject-groovy/src/test/groovy/io/micronaut/inject/annotation/RetentionSpec.groovy @@ -0,0 +1,134 @@ +package io.micronaut.inject.annotation + +import io.micronaut.ast.transform.test.AbstractBeanDefinitionSpec +import io.micronaut.inject.BeanDefinition + +import java.lang.annotation.Native + +class RetentionSpec extends AbstractBeanDefinitionSpec { + void "test source retention annotations are not retained"() { + given: + BeanDefinition definition = buildBeanDefinition('test.Test',''' +package test; + +@javax.inject.Singleton +class Test { + + + @javax.inject.Inject + @java.lang.annotation.Native + protected String someField; + +} +''') + + expect: + !definition.injectedFields.first().annotationMetadata.hasAnnotation(Native) + } + + void "test missing retention or CLASS retention annotations are excluded from runtime"() { + given: + BeanDefinition definition = buildBeanDefinition('test.Test','''\ +package test; + +import jakarta.inject.*; +import java.lang.annotation.Documented; +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; + +@Missing1 +@Missing2 +@Missing3 +@NotMissing1 +@NotMissing5 +@Singleton +class Test { + +} + +@Inherited +@Missing4 +@Documented +@NotMissing2 +@Retention(RetentionPolicy.SOURCE) +@Target([ElementType.METHOD, ElementType.ANNOTATION_TYPE, ElementType.TYPE, ElementType.FIELD]) +@interface Missing1 { +} + +@Inherited +@Documented +@NotMissing3 +@Retention(RetentionPolicy.CLASS) +@Target([ElementType.METHOD, ElementType.ANNOTATION_TYPE, ElementType.TYPE, ElementType.FIELD]) +@interface Missing2 { +} + +@Inherited +@NotMissing4 +@Documented +@Retention(RetentionPolicy.SOURCE) +@Target([ElementType.METHOD, ElementType.ANNOTATION_TYPE, ElementType.TYPE, ElementType.FIELD]) +@interface Missing3 { +} + +@Inherited +@Documented +@Retention(RetentionPolicy.SOURCE) +@Target([ElementType.METHOD, ElementType.ANNOTATION_TYPE, ElementType.TYPE, ElementType.FIELD]) +@interface Missing4 { +} + +@Inherited +@Missing1 +@Missing2 +@Missing3 +@NotMissing6 +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target([ElementType.METHOD, ElementType.ANNOTATION_TYPE, ElementType.TYPE, ElementType.FIELD]) +@interface NotMissing1 { +} + +@Inherited +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target([ElementType.METHOD, ElementType.ANNOTATION_TYPE, ElementType.TYPE, ElementType.FIELD]) +@interface NotMissing2 { +} + +@Inherited +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target([ElementType.METHOD, ElementType.ANNOTATION_TYPE, ElementType.TYPE, ElementType.FIELD]) +@interface NotMissing3 { +} + +@Inherited +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target([ElementType.METHOD, ElementType.ANNOTATION_TYPE, ElementType.TYPE, ElementType.FIELD]) +@interface NotMissing4 { +} + +@Inherited +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target([ElementType.METHOD, ElementType.ANNOTATION_TYPE, ElementType.TYPE, ElementType.FIELD]) +@interface NotMissing5 { +} + +@Inherited +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target([ElementType.METHOD, ElementType.ANNOTATION_TYPE, ElementType.TYPE, ElementType.FIELD]) +@interface NotMissing6 { +} + +''') + Set allAnnotations = definition.getAnnotationNames() + definition.getStereotypeAnnotationNames() + expect: + allAnnotations == ["javax.inject.Singleton", "javax.inject.Scope", "test.NotMissing1", "test.NotMissing2", "test.NotMissing3", "test.NotMissing4", "test.NotMissing5", "test.NotMissing6"] as Set + } +} diff --git a/inject-groovy/src/test/groovy/io/micronaut/inject/annotation/SourceRetentionSpec.groovy b/inject-groovy/src/test/groovy/io/micronaut/inject/annotation/SourceRetentionSpec.groovy deleted file mode 100644 index 2b6436c9839..00000000000 --- a/inject-groovy/src/test/groovy/io/micronaut/inject/annotation/SourceRetentionSpec.groovy +++ /dev/null @@ -1,28 +0,0 @@ -package io.micronaut.inject.annotation - -import io.micronaut.ast.transform.test.AbstractBeanDefinitionSpec -import io.micronaut.inject.BeanDefinition - -import java.lang.annotation.Native - -class SourceRetentionSpec extends AbstractBeanDefinitionSpec { - void "test source retention annotations are not retained"() { - given: - BeanDefinition definition = buildBeanDefinition('test.Test',''' -package test; - -@javax.inject.Singleton -class Test { - - - @javax.inject.Inject - @java.lang.annotation.Native - protected String someField; - -} -''') - - expect: - !definition.injectedFields.first().annotationMetadata.hasAnnotation(Native) - } -} diff --git a/inject-groovy/src/test/groovy/io/micronaut/inject/configproperties/ConfigurationPropertiesSpec.groovy b/inject-groovy/src/test/groovy/io/micronaut/inject/configproperties/ConfigurationPropertiesSpec.groovy index 9be0a574594..c4d12c1232a 100644 --- a/inject-groovy/src/test/groovy/io/micronaut/inject/configproperties/ConfigurationPropertiesSpec.groovy +++ b/inject-groovy/src/test/groovy/io/micronaut/inject/configproperties/ConfigurationPropertiesSpec.groovy @@ -17,6 +17,7 @@ package io.micronaut.inject.configproperties import io.micronaut.context.ApplicationContext import io.micronaut.context.DefaultApplicationContext +import io.micronaut.context.annotation.BeanProperties import io.micronaut.context.env.PropertySource import spock.lang.Specification /** @@ -61,6 +62,7 @@ class ConfigurationPropertiesSpec extends Specification { config.defaultValue == 9999 config.primitiveDefaultValue == 9999 config.inner.enabled + !applicationContext.getBeanDefinition(MyConfig).getAnnotation(BeanProperties.class) } } diff --git a/inject-groovy/src/test/groovy/io/micronaut/inject/visitor/BeanIntrospectionSpec.groovy b/inject-groovy/src/test/groovy/io/micronaut/inject/visitor/BeanIntrospectionSpec.groovy index 1c69849e4e1..6e8691a2c7f 100644 --- a/inject-groovy/src/test/groovy/io/micronaut/inject/visitor/BeanIntrospectionSpec.groovy +++ b/inject-groovy/src/test/groovy/io/micronaut/inject/visitor/BeanIntrospectionSpec.groovy @@ -107,7 +107,7 @@ class Test { instance.invoked } - void 'test favor field access'() { + void 'test use getter to read and field to write'() { given: BeanIntrospection introspection = buildBeanIntrospection('fieldaccess.Test','''\ package fieldaccess @@ -138,48 +138,26 @@ class Test { then:'the new value is reflected' one.get(instance) == 'test' - !instance.invoked + instance.invoked and:'unsafe access is working' (one as UnsafeBeanProperty).getUnsafe(instance) == 'test' - !instance.invoked when: (one as UnsafeBeanProperty).setUnsafe(instance, "test2") then: (one as UnsafeBeanProperty).getUnsafe(instance) == 'test2' - !instance.invoked } - // @PackageScope is commented out because type element visitors are run before it - // is processed because they visitors and the package scope transformation run in - // the same phase and there is no way to set the order void 'test field access only'() { given: - BeanIntrospection introspection = buildBeanIntrospection('fieldaccess.Test','''\ -package fieldaccess - -import io.micronaut.core.annotation.* - -@Introspected(accessKind=Introspected.AccessKind.FIELD) -class Test { - public String one // read/write - public final int two // read-only - @groovy.transform.PackageScope String three // package protected - protected String four // not included since protected - private String five // not included since private - - Test(int two) { - this.two = two - } -} -'''); + // Using a real class to prevent classloader permissions issue + def introspection = BeanIntrospection.getIntrospection(TestFieldAccess) when: def properties = introspection.getBeanProperties() then: -// properties.size() == 3 - properties.size() == 2 + properties.size() == 4 def one = introspection.getRequiredProperty("one", String) one.isReadWrite() @@ -187,8 +165,11 @@ class Test { def two = introspection.getRequiredProperty("two", int.class) two.isReadOnly() -// def three = introspection.getRequiredProperty("three", String) -// three.isReadWrite() + def three = introspection.getRequiredProperty("three", String) + three.isReadWrite() + + def four = introspection.getRequiredProperty("four", String) + four.isReadWrite() when:'a field is set' def instance = introspection.instantiate(10) @@ -202,6 +183,18 @@ class Test { then:'the new value is reflected' two.get(instance) == 20 + + when: + four.set(instance, "test") + + then: + four.get(instance) == "test" + + when: + instance = four.withValue(instance, "test2") + + then: + four.get(instance) == "test2" } // @PackageScope is commented out because type element visitors are run before it diff --git a/inject-groovy/src/test/groovy/io/micronaut/inject/visitor/Test.groovy b/inject-groovy/src/test/groovy/io/micronaut/inject/visitor/Test.groovy deleted file mode 100644 index d05ee337763..00000000000 --- a/inject-groovy/src/test/groovy/io/micronaut/inject/visitor/Test.groovy +++ /dev/null @@ -1,13 +0,0 @@ -package io.micronaut.inject.visitor; - -import io.micronaut.core.annotation.* - -@Introspected(accessKind=[Introspected.AccessKind.FIELD, Introspected.AccessKind.METHOD]) -class Test { - public String one - public boolean invoked = false - public String getOne() { - invoked = true - one - } -} diff --git a/inject-groovy/src/test/groovy/io/micronaut/inject/visitor/TestFieldAccess.groovy b/inject-groovy/src/test/groovy/io/micronaut/inject/visitor/TestFieldAccess.groovy new file mode 100644 index 00000000000..a9d864d08f5 --- /dev/null +++ b/inject-groovy/src/test/groovy/io/micronaut/inject/visitor/TestFieldAccess.groovy @@ -0,0 +1,18 @@ +package io.micronaut.inject.visitor + + +import groovy.transform.PackageScope +import io.micronaut.core.annotation.Introspected + +@Introspected(accessKind=Introspected.AccessKind.FIELD) +class TestFieldAccess { + public String one // read/write + public final int two // read-only + @PackageScope String three // package protected + protected String four // protected can be read from the same package + private String five // not included since private + + TestFieldAccess(int two) { + this.two = two + } +} diff --git a/inject-java-test/src/test/groovy/io/micronaut/inject/visitor/beans/BeanIntrospectionSpec.groovy b/inject-java-test/src/test/groovy/io/micronaut/inject/visitor/beans/BeanIntrospectionSpec.groovy index d78c9b84c54..534eb21340d 100644 --- a/inject-java-test/src/test/groovy/io/micronaut/inject/visitor/beans/BeanIntrospectionSpec.groovy +++ b/inject-java-test/src/test/groovy/io/micronaut/inject/visitor/beans/BeanIntrospectionSpec.groovy @@ -145,7 +145,7 @@ class Test { instance.invoked } - void 'test favor field access'() { + void 'test use getter to read and field to write'() { given: BeanIntrospection introspection = buildBeanIntrospection('fieldaccess.Test','''\ package fieldaccess; @@ -176,10 +176,10 @@ class Test { then:'the new value is reflected' one.get(instance) == 'test' - !instance.invoked + instance.invoked // The property was accessed with getter } - void 'test favor field access with custom getter'() { + void 'test use filed to read and setter to write'() { given: BeanIntrospection introspection = buildBeanIntrospection('fieldaccess.Test','''\ package fieldaccess; @@ -211,7 +211,7 @@ class Test { then:'the new value is reflected' one.get(instance) == 'test' - !instance.invoked + instance.invoked // Setter invoked } void 'test field access only'() { @@ -227,7 +227,7 @@ class Test { public String one; // read/write public final int two; // read-only String three; // package protected - protected String four; // not included since protected + protected String four; // protected can be accessed from the same package private String five; // not included since private Test(int two) { @@ -239,7 +239,7 @@ class Test { def properties = introspection.getBeanProperties() then: - properties.size() == 3 + properties.size() == 4 def one = introspection.getRequiredProperty("one", String) one.isReadWrite() @@ -250,6 +250,9 @@ class Test { def three = introspection.getRequiredProperty("three", String) three.isReadWrite() + def four = introspection.getRequiredProperty("four", String) + four.isReadWrite() + when:'a field is set' def instance = introspection.instantiate(10) one.set(instance, "test") @@ -262,6 +265,18 @@ class Test { then:'the new value is reflected' two.get(instance) == 20 + + when: + four.set(instance, "test") + + then: + four.get(instance) == "test" + + when: + instance = four.withValue(instance, "test2") + + then: + four.get(instance) == "test2" } void 'test field access only - public only'() { @@ -985,7 +1000,7 @@ interface GenBase { then: def e = thrown(UnsupportedOperationException) - e.message =='Cannot mutate property [name] that is not mutable via a setter method or constructor argument for type: test.Foo' + e.message =='Cannot mutate property [name] that is not mutable via a setter method, field or constructor argument for type: test.Foo' } void "test bean introspection with property of generic superclass"() { diff --git a/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaPropertyElement.java b/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaPropertyElement.java index 89bbfd56126..e6b1f768e9f 100644 --- a/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaPropertyElement.java +++ b/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaPropertyElement.java @@ -276,7 +276,7 @@ public AccessKind getWriteAccessKind() { @Override public boolean isReadOnly() { - switch (readAccessKind) { + switch (writeAccessKind) { case METHOD: return setter == null; case FIELD: @@ -288,7 +288,7 @@ public boolean isReadOnly() { @Override public boolean isWriteOnly() { - switch (writeAccessKind) { + switch (readAccessKind) { case METHOD: return getter == null; case FIELD: diff --git a/inject-java/src/test/groovy/io/micronaut/inject/annotation/RetentionSpec.groovy b/inject-java/src/test/groovy/io/micronaut/inject/annotation/RetentionSpec.groovy new file mode 100644 index 00000000000..c329fa6fe0b --- /dev/null +++ b/inject-java/src/test/groovy/io/micronaut/inject/annotation/RetentionSpec.groovy @@ -0,0 +1,136 @@ +package io.micronaut.inject.annotation + + +import io.micronaut.annotation.processing.test.AbstractTypeElementSpec +import io.micronaut.inject.BeanDefinition + +import java.lang.annotation.Native + +class RetentionSpec extends AbstractTypeElementSpec { + + void "test source retention annotations are not retained"() { + given: + BeanDefinition definition = buildBeanDefinition('test.Test',''' +package test; + +@jakarta.inject.Singleton +class Test { + + + @jakarta.inject.Inject + @java.lang.annotation.Native + String someField; + +} +''') + + expect:"source retention annotations are not retained at runtime" + !definition.injectedFields.first().annotationMetadata.hasAnnotation(Native) + } + + void "test missing retention or CLASS retention annotations are excluded from runtime"() { + given: + BeanDefinition definition = buildBeanDefinition('test.Test','''\ +package test; + +import jakarta.inject.*; +import java.lang.annotation.Documented; +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; + +@Missing1 +@Missing2 +@Missing3 +@NotMissing1 +@NotMissing5 +@Singleton +class Test { + +} + +@Inherited +@Missing4 +@Documented +@NotMissing2 +@Retention(RetentionPolicy.SOURCE) +@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE, ElementType.TYPE, ElementType.FIELD}) +@interface Missing1 { +} + +@Inherited +@Documented +@NotMissing3 +@Retention(RetentionPolicy.CLASS) +@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE, ElementType.TYPE, ElementType.FIELD}) +@interface Missing2 { +} + +@Inherited +@NotMissing4 +@Documented +@Retention(RetentionPolicy.SOURCE) +@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE, ElementType.TYPE, ElementType.FIELD}) +@interface Missing3 { +} + +@Inherited +@Documented +@Retention(RetentionPolicy.SOURCE) +@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE, ElementType.TYPE, ElementType.FIELD}) +@interface Missing4 { +} + +@Inherited +@Missing1 +@Missing2 +@Missing3 +@NotMissing6 +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE, ElementType.TYPE, ElementType.FIELD}) +@interface NotMissing1 { +} + +@Inherited +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE, ElementType.TYPE, ElementType.FIELD}) +@interface NotMissing2 { +} + +@Inherited +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE, ElementType.TYPE, ElementType.FIELD}) +@interface NotMissing3 { +} + +@Inherited +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE, ElementType.TYPE, ElementType.FIELD}) +@interface NotMissing4 { +} + +@Inherited +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE, ElementType.TYPE, ElementType.FIELD}) +@interface NotMissing5 { +} + +@Inherited +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE, ElementType.TYPE, ElementType.FIELD}) +@interface NotMissing6 { +} + +''') + Set allAnnotations = definition.getAnnotationNames() + definition.getStereotypeAnnotationNames() + expect: + allAnnotations == ["javax.inject.Singleton", "javax.inject.Scope", "test.NotMissing1", "test.NotMissing2", "test.NotMissing3", "test.NotMissing4", "test.NotMissing5", "test.NotMissing6"] as Set + } +} diff --git a/inject-java/src/test/groovy/io/micronaut/inject/annotation/SourceRetentionSpec.groovy b/inject-java/src/test/groovy/io/micronaut/inject/annotation/SourceRetentionSpec.groovy deleted file mode 100644 index 076613163b0..00000000000 --- a/inject-java/src/test/groovy/io/micronaut/inject/annotation/SourceRetentionSpec.groovy +++ /dev/null @@ -1,30 +0,0 @@ -package io.micronaut.inject.annotation - - -import io.micronaut.annotation.processing.test.AbstractTypeElementSpec -import io.micronaut.inject.BeanDefinition - -import java.lang.annotation.Native - -class SourceRetentionSpec extends AbstractTypeElementSpec { - - void "test source retention annotations are not retained"() { - given: - BeanDefinition definition = buildBeanDefinition('test.Test',''' -package test; - -@jakarta.inject.Singleton -class Test { - - - @jakarta.inject.Inject - @java.lang.annotation.Native - String someField; - -} -''') - - expect:"source retention annotations are not retained at runtime" - !definition.injectedFields.first().annotationMetadata.hasAnnotation(Native) - } -} diff --git a/inject-java/src/test/groovy/io/micronaut/inject/configproperties/ConfigurationPropertiesSpec.groovy b/inject-java/src/test/groovy/io/micronaut/inject/configproperties/ConfigurationPropertiesSpec.groovy index 509f45adc38..2952e699d0c 100644 --- a/inject-java/src/test/groovy/io/micronaut/inject/configproperties/ConfigurationPropertiesSpec.groovy +++ b/inject-java/src/test/groovy/io/micronaut/inject/configproperties/ConfigurationPropertiesSpec.groovy @@ -17,6 +17,7 @@ package io.micronaut.inject.configproperties import io.micronaut.context.ApplicationContext import io.micronaut.context.DefaultApplicationContext +import io.micronaut.context.annotation.BeanProperties import io.micronaut.context.env.PropertySource import io.micronaut.core.util.CollectionUtils import spock.lang.Specification @@ -37,6 +38,7 @@ class ConfigurationPropertiesSpec extends Specification { ctx.getBean(MyConfig).map.get("key1").get("key2").property == 10 ctx.getBean(MyConfig).map.get("key1").get("key2").property2 ctx.getBean(MyConfig).map.get("key1").get("key2").property2.property == 10 + !ctx.getBeanDefinition(MyConfig).getAnnotation(BeanProperties.class) cleanup: ctx.close() diff --git a/inject/src/main/java/io/micronaut/context/annotation/BeanProperties.java b/inject/src/main/java/io/micronaut/context/annotation/BeanProperties.java index d6727c76fdb..122535caa96 100644 --- a/inject/src/main/java/io/micronaut/context/annotation/BeanProperties.java +++ b/inject/src/main/java/io/micronaut/context/annotation/BeanProperties.java @@ -19,10 +19,12 @@ import java.lang.annotation.Annotation; import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; import java.lang.annotation.Inherited; import java.lang.annotation.Retention; +import java.lang.annotation.Target; -import static java.lang.annotation.RetentionPolicy.SOURCE; +import static java.lang.annotation.RetentionPolicy.CLASS; /** * Bean properties configuration annotation. @@ -35,7 +37,8 @@ */ @Experimental @Documented -@Retention(SOURCE) +@Target({ElementType.ANNOTATION_TYPE, ElementType.TYPE}) +@Retention(CLASS) @Inherited public @interface BeanProperties { diff --git a/inject/src/main/java/io/micronaut/context/annotation/ConfigurationBuilder.java b/inject/src/main/java/io/micronaut/context/annotation/ConfigurationBuilder.java index 9c8254f30e6..9bd98dd465a 100644 --- a/inject/src/main/java/io/micronaut/context/annotation/ConfigurationBuilder.java +++ b/inject/src/main/java/io/micronaut/context/annotation/ConfigurationBuilder.java @@ -34,6 +34,7 @@ @Documented @Retention(RUNTIME) @Target({ElementType.FIELD, ElementType.METHOD}) +@BeanProperties(accessKind = BeanProperties.AccessKind.METHOD, visibility = BeanProperties.Visibility.DEFAULT, allowWriteWithMultipleArgs = true, allowWriteWithZeroArgs = true) public @interface ConfigurationBuilder { /** diff --git a/inject/src/main/java/io/micronaut/context/annotation/ConfigurationReader.java b/inject/src/main/java/io/micronaut/context/annotation/ConfigurationReader.java index c2c3a62f833..53bb2fee400 100644 --- a/inject/src/main/java/io/micronaut/context/annotation/ConfigurationReader.java +++ b/inject/src/main/java/io/micronaut/context/annotation/ConfigurationReader.java @@ -32,6 +32,7 @@ @Documented @Retention(RUNTIME) @Target(ElementType.ANNOTATION_TYPE) +@BeanProperties(accessKind = {BeanProperties.AccessKind.METHOD, BeanProperties.AccessKind.FIELD}, visibility = BeanProperties.Visibility.DEFAULT) public @interface ConfigurationReader { /** diff --git a/inject/src/main/java/io/micronaut/inject/annotation/AnnotationMetadataWriter.java b/inject/src/main/java/io/micronaut/inject/annotation/AnnotationMetadataWriter.java index 778cb87de53..c3e1b5e311b 100644 --- a/inject/src/main/java/io/micronaut/inject/annotation/AnnotationMetadataWriter.java +++ b/inject/src/main/java/io/micronaut/inject/annotation/AnnotationMetadataWriter.java @@ -487,7 +487,14 @@ private static void instantiateInternal( // 4th argument: all annotations pushCreateAnnotationData(owningType, declaringClassWriter, generatorAdapter, annotationMetadata.allAnnotations, defaultsStorage, loadTypeMethods, annotationMetadata.getSourceRetentionAnnotations()); // 5th argument: annotations by stereotype - pushStringMapOf(generatorAdapter, annotationMetadata.annotationsByStereotype, false, Collections.emptyList(), list -> pushListOfString(generatorAdapter, list)); + Map> annotationsByStereotype = annotationMetadata.annotationsByStereotype; + if (annotationMetadata.getSourceRetentionAnnotations() != null && annotationsByStereotype != null) { + annotationsByStereotype = new LinkedHashMap<>(annotationsByStereotype); + for (String sourceRetentionAnnotation : annotationMetadata.getSourceRetentionAnnotations()) { + annotationsByStereotype.remove(sourceRetentionAnnotation); + } + } + pushStringMapOf(generatorAdapter, annotationsByStereotype, false, Collections.emptyList(), list -> pushListOfString(generatorAdapter, list)); // 6th argument: has property expressions generatorAdapter.push(annotationMetadata.hasPropertyExpressions()); // 7th argument: use repeatable annotations diff --git a/inject/src/main/java/io/micronaut/inject/annotation/DefaultAnnotationMetadata.java b/inject/src/main/java/io/micronaut/inject/annotation/DefaultAnnotationMetadata.java index 90bce9481f4..5e98d98f6ba 100644 --- a/inject/src/main/java/io/micronaut/inject/annotation/DefaultAnnotationMetadata.java +++ b/inject/src/main/java/io/micronaut/inject/annotation/DefaultAnnotationMetadata.java @@ -1944,7 +1944,8 @@ private void addAnnotation(String annotation, } putValues(annotation, values, allAnnotations); - if (retentionPolicy == RetentionPolicy.SOURCE) { + // Annotations with retention CLASS need not be retained at run time + if (retentionPolicy == RetentionPolicy.SOURCE || retentionPolicy == RetentionPolicy.CLASS) { addSourceRetentionAnnotation(annotation); } } diff --git a/inject/src/main/java/io/micronaut/inject/ast/PropertyElement.java b/inject/src/main/java/io/micronaut/inject/ast/PropertyElement.java index 9e749237e9c..e7438b6ec69 100644 --- a/inject/src/main/java/io/micronaut/inject/ast/PropertyElement.java +++ b/inject/src/main/java/io/micronaut/inject/ast/PropertyElement.java @@ -50,7 +50,7 @@ default boolean isExcluded() { * @return True if the property is read only. */ default boolean isReadOnly() { - return !getWriteMethod().isPresent(); + return !getWriteMember().isPresent(); } /** @@ -60,7 +60,7 @@ default boolean isReadOnly() { * @since 4.0.0 */ default boolean isWriteOnly() { - return !getReadMethod().isPresent(); + return !getReadMember().isPresent(); } /** diff --git a/inject/src/main/java/io/micronaut/inject/beans/visitor/BeanIntrospectionWriter.java b/inject/src/main/java/io/micronaut/inject/beans/visitor/BeanIntrospectionWriter.java index 94b2b22866d..aba018ae87a 100644 --- a/inject/src/main/java/io/micronaut/inject/beans/visitor/BeanIntrospectionWriter.java +++ b/inject/src/main/java/io/micronaut/inject/beans/visitor/BeanIntrospectionWriter.java @@ -35,6 +35,7 @@ import io.micronaut.inject.ast.ConstructorElement; import io.micronaut.inject.ast.ElementQuery; import io.micronaut.inject.ast.FieldElement; +import io.micronaut.inject.ast.MemberElement; import io.micronaut.inject.ast.MethodElement; import io.micronaut.inject.ast.ParameterElement; import io.micronaut.inject.ast.TypedElement; @@ -117,7 +118,6 @@ final class BeanIntrospectionWriter extends AbstractAnnotationMetadataWriter { private MethodElement defaultConstructor; private final List beanProperties = new ArrayList<>(); - private final List beanFields = new ArrayList<>(); private final List beanMethods = new ArrayList<>(); private final DispatchWriter dispatchWriter; @@ -187,8 +187,8 @@ public Type getBeanType() { * @param type The property type * @param genericType The generic type * @param name The property name - * @param readMethod The read method - * @param writeMethod The write methodname + * @param readMember The read method + * @param writeMember The write methodname * @param isReadOnly Is the property read only * @param annotationMetadata The property annotation metadata * @param typeArguments The type arguments @@ -197,8 +197,8 @@ void visitProperty( @NonNull TypedElement type, @NonNull TypedElement genericType, @NonNull String name, - @Nullable MethodElement readMethod, - @Nullable MethodElement writeMethod, + @Nullable MemberElement readMember, + @Nullable MemberElement writeMember, boolean isReadOnly, @Nullable AnnotationMetadata annotationMetadata, @Nullable Map typeArguments) { @@ -216,18 +216,30 @@ void visitProperty( } } - int readMethodIndex = -1; - if (readMethod != null) { - readMethodIndex = dispatchWriter.addMethod(classElement, readMethod, true); + int readDispatchIndex = -1; + if (readMember != null) { + if (readMember instanceof MethodElement) { + readDispatchIndex = dispatchWriter.addMethod(classElement, (MethodElement) readMember, true); + } else if (readMember instanceof FieldElement) { + readDispatchIndex = dispatchWriter.addGetField((FieldElement) readMember); + } else { + throw new IllegalStateException(); + } } - int writeMethodIndex = -1; + int writeDispatchIndex = -1; int withMethodIndex = -1; - if (writeMethod != null) { - writeMethodIndex = dispatchWriter.addMethod(classElement, writeMethod, true); + if (writeMember != null) { + if (writeMember instanceof MethodElement) { + writeDispatchIndex = dispatchWriter.addMethod(classElement, (MethodElement) writeMember, true); + } else if (writeMember instanceof FieldElement) { + writeDispatchIndex = dispatchWriter.addSetField((FieldElement) writeMember); + } else { + throw new IllegalStateException(); + } } boolean isMutable = !isReadOnly || hasAssociatedConstructorArgument(name, genericType); if (isMutable) { - if (writeMethod == null) { + if (writeMember == null) { final String prefix = this.annotationMetadata.stringValue(Introspected.class, "withPrefix").orElse("with"); ElementQuery elementQuery = ElementQuery.of(MethodElement.class) .onlyAccessible() @@ -254,7 +266,7 @@ void visitProperty( } else { withMethodIndex = dispatchWriter.addDispatchTarget(new ExceptionDispatchTarget( UnsupportedOperationException.class, - "Cannot mutate property [" + name + "] that is not mutable via a setter method or constructor argument for type: " + beanType.getClassName() + "Cannot mutate property [" + name + "] that is not mutable via a setter method, field or constructor argument for type: " + beanType.getClassName() )); } @@ -263,8 +275,8 @@ void visitProperty( name, annotationMetadata, typeArguments, - readMethodIndex, - writeMethodIndex, + readDispatchIndex, + writeDispatchIndex, withMethodIndex, isReadOnly )); @@ -282,17 +294,6 @@ public void visitBeanMethod(MethodElement element) { } } - /** - * Visits a bean field. - * - * @param beanField The field - */ - public void visitBeanField(FieldElement beanField) { - int getDispatchIndex = dispatchWriter.addGetField(beanField); - int setDispatchIndex = dispatchWriter.addSetField(beanField); - beanFields.add(new BeanFieldData(beanField, getDispatchIndex, setDispatchIndex)); - } - /** * Builds an index for the given property and annotation. * @@ -345,12 +346,12 @@ private void buildStaticInit(ClassWriter classWriter) { staticInit.putStatic(introspectionType, FIELD_CONSTRUCTOR_ARGUMENTS, args); } } - if (!beanProperties.isEmpty() || !beanFields.isEmpty()) { + if (!beanProperties.isEmpty()) { Type beanPropertiesRefs = Type.getType(AbstractInitializableBeanIntrospection.BeanPropertyRef[].class); classWriter.visitField(ACC_PRIVATE | ACC_FINAL | ACC_STATIC, FIELD_BEAN_PROPERTIES_REFERENCES, beanPropertiesRefs.getDescriptor(), null, null); - int size = beanProperties.size() + beanFields.size(); + int size = beanProperties.size(); pushNewArray(staticInit, AbstractInitializableBeanIntrospection.BeanPropertyRef.class, size); int i = 0; @@ -363,15 +364,6 @@ private void buildStaticInit(ClassWriter classWriter) { ) ); } - for (BeanFieldData beanFieldData : beanFields) { - pushStoreInArray(staticInit, i++, size, () -> - pushBeanPropertyReference( - classWriter, - staticInit, - beanFieldData - ) - ); - } staticInit.putStatic(introspectionType, FIELD_BEAN_PROPERTIES_REFERENCES, beanPropertiesRefs); } if (!beanMethods.isEmpty()) { @@ -396,7 +388,7 @@ private void buildStaticInit(ClassWriter classWriter) { for (String annotationName : indexByAnnotations.keySet()) { int[] indexes = indexByAnnotations.get(annotationName) .stream() - .mapToInt(prop -> getPropertyIndex(prop)) + .mapToInt(this::getPropertyIndex) .toArray(); String newIndexField = "INDEX_" + (++indexesIndex); @@ -434,8 +426,8 @@ private void pushBeanPropertyReference(ClassWriter classWriter, defaults, loadTypeMethods ); - staticInit.push(beanPropertyData.getMethodDispatchIndex); - staticInit.push(beanPropertyData.setMethodDispatchIndex); + staticInit.push(beanPropertyData.getDispatchIndex); + staticInit.push(beanPropertyData.setDispatchIndex); staticInit.push(beanPropertyData.withMethodDispatchIndex); staticInit.push(beanPropertyData.isReadOnly); staticInit.push(!beanPropertyData.isReadOnly || hasAssociatedConstructorArgument(beanPropertyData.name, beanPropertyData.typedElement)); @@ -451,41 +443,6 @@ private void pushBeanPropertyReference(ClassWriter classWriter, boolean.class); } - private void pushBeanPropertyReference(ClassWriter classWriter, - GeneratorAdapter staticInit, - BeanFieldData beanFieldData) { - staticInit.newInstance(Type.getType(AbstractInitializableBeanIntrospection.BeanPropertyRef.class)); - staticInit.dup(); - - pushCreateArgument( - beanType.getClassName(), - introspectionType, - classWriter, - staticInit, - beanFieldData.beanField.getName(), - beanFieldData.beanField.getGenericType(), - beanFieldData.beanField.getAnnotationMetadata(), - beanFieldData.beanField.getGenericType().getTypeArguments(), - defaults, - loadTypeMethods - ); - staticInit.push(beanFieldData.getDispatchIndex); - staticInit.push(beanFieldData.setDispatchIndex); - staticInit.push(-1); - staticInit.push(beanFieldData.beanField.isFinal()); - staticInit.push(!beanFieldData.beanField.isFinal() || hasAssociatedConstructorArgument(beanFieldData.beanField.getName(), beanFieldData.beanField)); - - invokeConstructor( - staticInit, - AbstractInitializableBeanIntrospection.BeanPropertyRef.class, - Argument.class, - int.class, - int.class, - int.class, - boolean.class, - boolean.class); - } - private void pushBeanMethodReference(ClassWriter classWriter, GeneratorAdapter staticInit, BeanMethodData beanMethodData) { @@ -586,7 +543,7 @@ private void writeIntrospectionClass(ClassWriterOutputVisitor classWriterOutputV constructorWriter.push((String) null); } - if (beanProperties.isEmpty() && beanFields.isEmpty()) { + if (beanProperties.isEmpty()) { constructorWriter.push((String) null); } else { constructorWriter.getStatic(introspectionType, @@ -660,9 +617,6 @@ protected Set getKeys() { for (BeanPropertyData prop : beanProperties) { keys.add(prop.name); } - for (BeanFieldData field : beanFields) { - keys.add(field.beanField.getName()); - } return keys; } @@ -838,10 +792,6 @@ private int getPropertyIndex(String propertyName) { if (beanPropertyData != null) { return beanProperties.indexOf(beanPropertyData); } - BeanFieldData beanFieldData = beanFields.stream().filter(f -> f.beanField.getName().equals(propertyName)).findFirst().orElse(null); - if (beanFieldData != null) { - return beanProperties.size() + beanFields.indexOf(beanFieldData); - } throw new IllegalStateException("Property not found: " + propertyName + " " + classElement.getName()); } @@ -1095,16 +1045,28 @@ public void writeDispatchOne(GeneratorAdapter writer) { .filter(bp -> bp.name.equals(parameterName)) .findAny().orElse(null); - int getMethodDispatchIndex = prop == null ? -1 : prop.getMethodDispatchIndex; - if (getMethodDispatchIndex != -1) { - DispatchWriter.MethodDispatchTarget methodDispatchTarget = (DispatchWriter.MethodDispatchTarget) dispatchWriter.getDispatchTargets().get(getMethodDispatchIndex); - MethodElement readMethod = methodDispatchTarget.getMethodElement(); - if (readMethod.getGenericReturnType().isAssignable(parameter.getGenericType())) { - constructorArguments[i] = readMethod; + int readDispatchIndex = prop == null ? -1 : prop.getDispatchIndex; + if (readDispatchIndex != -1) { + Object member; + ClassElement propertyType; + DispatchWriter.DispatchTarget dispatchTarget = dispatchWriter.getDispatchTargets().get(readDispatchIndex); + if (dispatchTarget instanceof DispatchWriter.MethodDispatchTarget) { + MethodElement methodElement = ((DispatchWriter.MethodDispatchTarget) dispatchTarget).getMethodElement(); + propertyType = methodElement.getGenericReturnType(); + member = methodElement; + } else if (dispatchTarget instanceof DispatchWriter.FieldGetDispatchTarget) { + FieldElement field = ((DispatchWriter.FieldGetDispatchTarget) dispatchTarget).getField(); + propertyType = field.getGenericType(); + member = field; + } else { + throw new IllegalStateException(); + } + if (propertyType.isAssignable(parameter.getGenericType())) { + constructorArguments[i] = member; constructorProps.add(prop); } else { isMutable = false; - nonMutableMessage = "Cannot create copy of type [" + beanType.getClassName() + "]. Property of type [" + readMethod.getGenericReturnType().getName() + "] is not assignable to constructor argument [" + parameterName + "]"; + nonMutableMessage = "Cannot create copy of type [" + beanType.getClassName() + "]. Property of type [" + propertyType.getName() + "] is not assignable to constructor argument [" + parameterName + "]"; } } else { isMutable = false; @@ -1130,16 +1092,22 @@ public void writeDispatchOne(GeneratorAdapter writer) { if (!parameter.isPrimitive()) { pushBoxPrimitiveIfNecessary(parameter, constructorWriter); } - } else { + } else if (constructorArgument instanceof MethodElement) { MethodElement readMethod = (MethodElement) constructorArgument; constructorWriter.loadLocal(prevBeanTypeLocal, beanType); invokeMethod(constructorWriter, readMethod); + } else if (constructorArgument instanceof FieldElement) { + FieldElement fieldElement = (FieldElement) constructorArgument; + constructorWriter.loadLocal(prevBeanTypeLocal, beanType); + invokeGetField(constructorWriter, fieldElement); + } else { + throw new IllegalStateException(); } } }); List readWriteProps = beanProperties.stream() - .filter(bp -> bp.setMethodDispatchIndex != -1 && bp.getMethodDispatchIndex != -1 && !constructorProps.contains(bp)) + .filter(bp -> bp.setDispatchIndex != -1 && bp.getDispatchIndex != -1 && !constructorProps.contains(bp)) .collect(Collectors.toList()); if (!readWriteProps.isEmpty()) { @@ -1147,19 +1115,35 @@ public void writeDispatchOne(GeneratorAdapter writer) { writer.storeLocal(beanTypeLocal, beanType); for (BeanPropertyData readWriteProp : readWriteProps) { + DispatchWriter.DispatchTarget readDispatch = dispatchWriter.getDispatchTargets().get(readWriteProp.getDispatchIndex); + if (readDispatch instanceof DispatchWriter.MethodDispatchTarget) { + MethodElement readMethod = ((DispatchWriter.MethodDispatchTarget) readDispatch).getMethodElement(); + writer.loadLocal(beanTypeLocal, beanType); + writer.loadLocal(prevBeanTypeLocal, beanType); + invokeMethod(writer, readMethod); + } else if (readDispatch instanceof DispatchWriter.FieldGetDispatchTarget) { + FieldElement fieldElement = ((DispatchWriter.FieldGetDispatchTarget) readDispatch).getField(); + writer.loadLocal(beanTypeLocal, beanType); + writer.loadLocal(prevBeanTypeLocal, beanType); + invokeGetField(writer, fieldElement); + } else { + throw new IllegalStateException(); + } - MethodElement writeMethod = ((DispatchWriter.MethodDispatchTarget) dispatchWriter - .getDispatchTargets().get(readWriteProp.setMethodDispatchIndex)).getMethodElement(); - MethodElement readMethod = ((DispatchWriter.MethodDispatchTarget) dispatchWriter - .getDispatchTargets().get(readWriteProp.getMethodDispatchIndex)).getMethodElement(); - - writer.loadLocal(beanTypeLocal, beanType); - writer.loadLocal(prevBeanTypeLocal, beanType); - invokeMethod(writer, readMethod); - ClassElement writeReturnType = invokeMethod(writer, writeMethod); - if (!writeReturnType.getName().equals("void")) { - writer.pop(); + DispatchWriter.DispatchTarget writeDispatch = dispatchWriter.getDispatchTargets().get(readWriteProp.setDispatchIndex); + if (writeDispatch instanceof DispatchWriter.MethodDispatchTarget) { + MethodElement writeMethod = ((DispatchWriter.MethodDispatchTarget) writeDispatch).getMethodElement(); + ClassElement writeReturnType = invokeMethod(writer, writeMethod); + if (!writeReturnType.getName().equals("void")) { + writer.pop(); + } + } else if (writeDispatch instanceof DispatchWriter.FieldSetDispatchTarget) { + FieldElement fieldElement = ((DispatchWriter.FieldSetDispatchTarget) writeDispatch).getField(); + invokeSetField(writer, fieldElement); + } else { + throw new IllegalStateException(); } + } writer.loadLocal(beanTypeLocal, beanType); } @@ -1179,21 +1163,13 @@ private ClassElement invokeMethod(GeneratorAdapter mutateMethod, MethodElement m } return returnType; } - } - - private static final class BeanFieldData { - @NotNull - final FieldElement beanField; - final int getDispatchIndex; - final int setDispatchIndex; + private void invokeGetField(GeneratorAdapter mutateMethod, FieldElement field) { + mutateMethod.getField(beanType, field.getName(), JavaModelUtils.getTypeReference(field.getType())); + } - private BeanFieldData(FieldElement beanField, - int getDispatchIndex, - int setDispatchIndex) { - this.beanField = beanField; - this.getDispatchIndex = getDispatchIndex; - this.setDispatchIndex = setDispatchIndex; + private void invokeSetField(GeneratorAdapter mutateMethod, FieldElement field) { + mutateMethod.putField(beanType, field.getName(), JavaModelUtils.getTypeReference(field.getType())); } } @@ -1219,8 +1195,8 @@ private static final class BeanPropertyData { @Nullable final Map typeArguments; - final int getMethodDispatchIndex; - final int setMethodDispatchIndex; + final int getDispatchIndex; + final int setDispatchIndex; final int withMethodDispatchIndex; final boolean isReadOnly; @@ -1228,16 +1204,16 @@ private BeanPropertyData(@NonNull TypedElement typedElement, @NonNull String name, @Nullable AnnotationMetadata annotationMetadata, @Nullable Map typeArguments, - int getMethodDispatchIndex, - int setMethodDispatchIndex, + int getDispatchIndex, + int setDispatchIndex, int withMethodDispatchIndex, boolean isReadOnly) { this.typedElement = typedElement; this.name = name; this.annotationMetadata = annotationMetadata == null ? AnnotationMetadata.EMPTY_METADATA : annotationMetadata; this.typeArguments = typeArguments; - this.getMethodDispatchIndex = getMethodDispatchIndex; - this.setMethodDispatchIndex = setMethodDispatchIndex; + this.getDispatchIndex = getDispatchIndex; + this.setDispatchIndex = setDispatchIndex; this.withMethodDispatchIndex = withMethodDispatchIndex; this.isReadOnly = isReadOnly; } diff --git a/inject/src/main/java/io/micronaut/inject/beans/visitor/IntrospectedTypeElementVisitor.java b/inject/src/main/java/io/micronaut/inject/beans/visitor/IntrospectedTypeElementVisitor.java index 4c05ce8bf8d..e5468a2287e 100644 --- a/inject/src/main/java/io/micronaut/inject/beans/visitor/IntrospectedTypeElementVisitor.java +++ b/inject/src/main/java/io/micronaut/inject/beans/visitor/IntrospectedTypeElementVisitor.java @@ -16,42 +16,32 @@ package io.micronaut.inject.beans.visitor; import io.micronaut.context.annotation.Executable; -import io.micronaut.core.annotation.AccessorsStyle; import io.micronaut.core.annotation.AnnotationClassValue; import io.micronaut.core.annotation.AnnotationMetadata; import io.micronaut.core.annotation.AnnotationValue; import io.micronaut.core.annotation.Internal; import io.micronaut.core.annotation.Introspected; import io.micronaut.core.annotation.NonNull; -import io.micronaut.core.annotation.Nullable; -import io.micronaut.core.naming.NameUtils; import io.micronaut.core.util.ArrayUtils; import io.micronaut.core.util.CollectionUtils; import io.micronaut.inject.annotation.AnnotationMetadataHierarchy; -import io.micronaut.inject.annotation.MutableAnnotationMetadata; import io.micronaut.inject.ast.ClassElement; import io.micronaut.inject.ast.ElementModifier; import io.micronaut.inject.ast.ElementQuery; -import io.micronaut.inject.ast.FieldElement; import io.micronaut.inject.ast.MethodElement; -import io.micronaut.inject.ast.ParameterElement; import io.micronaut.inject.ast.PropertyElement; import io.micronaut.inject.visitor.TypeElementVisitor; import io.micronaut.inject.visitor.VisitorContext; import io.micronaut.inject.writer.ClassGenerationException; import java.io.IOException; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; +import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.concurrent.atomic.AtomicInteger; -import java.util.function.Predicate; import java.util.stream.Collectors; /** @@ -71,18 +61,14 @@ public class IntrospectedTypeElementVisitor implements TypeElementVisitor ANN_CONSTRAINT = AnnotationValue.builder(Introspected.IndexedAnnotation.class) - .member("annotation", new AnnotationClassValue<>(JAVAX_VALIDATION_CONSTRAINT)) - .build(); + .member("annotation", new AnnotationClassValue<>(JAVAX_VALIDATION_CONSTRAINT)) + .build(); private static final String JAVAX_VALIDATION_VALID = "javax.validation.Valid"; private static final AnnotationValue ANN_VALID = AnnotationValue.builder(Introspected.IndexedAnnotation.class) - .member("annotation", new AnnotationClassValue<>(JAVAX_VALIDATION_VALID)) - .build(); - private static final Introspected.AccessKind[] DEFAULT_ACCESS_KIND = { Introspected.AccessKind.METHOD }; - private static final Introspected.Visibility[] DEFAULT_VISIBILITY = { Introspected.Visibility.DEFAULT }; + .member("annotation", new AnnotationClassValue<>(JAVAX_VALIDATION_VALID)) + .build(); private Map writers = new LinkedHashMap<>(10); - private List abstractIntrospections = new ArrayList<>(); - private AbstractIntrospection currentAbstractIntrospection; @Override public int getOrder() { @@ -92,8 +78,6 @@ public int getOrder() { @Override public void visitClass(ClassElement element, VisitorContext context) { - // reset - currentAbstractIntrospection = null; if (!element.isPrivate() && element.hasStereotype(Introspected.class)) { final AnnotationValue introspected = element.getAnnotation(Introspected.class); if (introspected != null && !writers.containsKey(element.getName())) { @@ -106,112 +90,47 @@ private boolean isIntrospected(VisitorContext context, ClassElement c) { return writers.containsKey(c.getName()) || context.getClassElement(c.getPackageName() + ".$" + c.getSimpleName() + "$Introspection").isPresent(); } - @Override - public void visitMethod(MethodElement element, VisitorContext context) { - final ClassElement declaringType = element.getDeclaringType(); - final String methodName = element.getName(); - - final String[] readPrefixes = declaringType.getValue(AccessorsStyle.class, "readPrefixes", String[].class) - .orElse(new String[]{AccessorsStyle.DEFAULT_READ_PREFIX}); - final String[] writePrefixes = declaringType.getValue(AccessorsStyle.class, "writePrefixes", String[].class) - .orElse(new String[]{AccessorsStyle.DEFAULT_WRITE_PREFIX}); - - if (currentAbstractIntrospection != null) { - if (NameUtils.isReaderName(methodName, readPrefixes) && element.getParameters().length == 0) { - final String propertyName = NameUtils.getPropertyNameForGetter(methodName, readPrefixes); - final AbstractPropertyElement propertyElement = currentAbstractIntrospection.properties.computeIfAbsent(propertyName, s -> new AbstractPropertyElement( - element.getDeclaringType(), - element.getReturnType(), - propertyName - )); - propertyElement.readMethod = element; - } else if (NameUtils.isWriterName(methodName, writePrefixes) && element.getParameters().length == 1) { - final String propertyName = NameUtils.getPropertyNameForSetter(methodName, writePrefixes); - final AbstractPropertyElement propertyElement = currentAbstractIntrospection.properties.computeIfAbsent(propertyName, s -> new AbstractPropertyElement( - element.getDeclaringType(), - element.getParameters()[0].getType(), - propertyName - )); - propertyElement.writeMethod = element; - } - } - } - private void processIntrospected(ClassElement element, VisitorContext context, AnnotationValue introspected) { final String[] packages = introspected.stringValues("packages"); final AnnotationClassValue[] classes = introspected.get("classes", AnnotationClassValue[].class, new AnnotationClassValue[0]); final boolean metadata = introspected.booleanValue("annotationMetadata").orElse(true); - - final Set includes = CollectionUtils.setOf(introspected.stringValues("includes")); - final Set excludes = CollectionUtils.setOf(introspected.stringValues("excludes")); - final Set excludedAnnotations = CollectionUtils.setOf(introspected.stringValues("excludedAnnotations")); final Set includedAnnotations = CollectionUtils.setOf(introspected.stringValues("includedAnnotations")); - final Set indexedAnnotations; - final Set toIndex = CollectionUtils.setOf(introspected.get("indexed", AnnotationValue[].class, new AnnotationValue[0])); - Introspected.AccessKind[] accessKinds = introspected.enumValues("accessKind", Introspected.AccessKind.class); - Introspected.Visibility[] visibilities = - introspected.enumValues("visibility", Introspected.Visibility.class); - if (ArrayUtils.isEmpty(accessKinds)) { - accessKinds = DEFAULT_ACCESS_KIND; - } - if (ArrayUtils.isEmpty(visibilities)) { - visibilities = DEFAULT_VISIBILITY; - } - Introspected.AccessKind[] finalAccessKinds = accessKinds; - Introspected.Visibility[] finalVisibilities = visibilities; - + final Set indexedAnnotations; if (CollectionUtils.isEmpty(toIndex)) { - indexedAnnotations = CollectionUtils.setOf( - ANN_CONSTRAINT, - ANN_VALID - ); + indexedAnnotations = CollectionUtils.setOf(ANN_CONSTRAINT, ANN_VALID); } else { - toIndex.addAll( - CollectionUtils.setOf( - ANN_CONSTRAINT, - ANN_VALID - ) - ); + toIndex.addAll(CollectionUtils.setOf(ANN_CONSTRAINT, ANN_VALID)); indexedAnnotations = toIndex; } if (ArrayUtils.isNotEmpty(classes)) { AtomicInteger index = new AtomicInteger(0); for (AnnotationClassValue aClass : classes) { - final Optional classElement = context.getClassElement(aClass.getName()); - - - classElement.ifPresent(ce -> { + context.getClassElement(aClass.getName()).ifPresent(ce -> { if (ce.isPublic() && !isIntrospected(context, ce)) { final AnnotationMetadata typeMetadata = ce.getAnnotationMetadata(); final AnnotationMetadata resolvedMetadata = typeMetadata == AnnotationMetadata.EMPTY_METADATA - ? element.getAnnotationMetadata() - : new AnnotationMetadataHierarchy(element.getAnnotationMetadata(), typeMetadata); + ? element.getAnnotationMetadata() + : new AnnotationMetadataHierarchy(element.getAnnotationMetadata(), typeMetadata); final BeanIntrospectionWriter writer = new BeanIntrospectionWriter( - element.getName(), - index.getAndIncrement(), - element, - ce, - metadata ? resolvedMetadata : null + element.getName(), + index.getAndIncrement(), + element, + ce, + metadata ? resolvedMetadata : null ); processElement( - metadata, - includes, - excludes, - excludedAnnotations, - indexedAnnotations, - ce, - writer, - finalVisibilities, - finalAccessKinds + metadata, + indexedAnnotations, + ce, + writer ); } }); } } else if (ArrayUtils.isNotEmpty(packages)) { - if (includedAnnotations.isEmpty()) { context.fail("When specifying 'packages' you must also specify 'includedAnnotations' to limit scanning", element); } else { @@ -223,45 +142,23 @@ private void processIntrospected(ClassElement element, VisitorContext context, A continue; } final BeanIntrospectionWriter writer = new BeanIntrospectionWriter( - element.getName(), - j++, - element, - classElement, - metadata ? element.getAnnotationMetadata() : null + element.getName(), + j++, + element, + classElement, + metadata ? element.getAnnotationMetadata() : null ); - processElement( - metadata, - includes, - excludes, - excludedAnnotations, - indexedAnnotations, - classElement, - writer, - finalVisibilities, - finalAccessKinds - ); + processElement(metadata, indexedAnnotations, classElement, writer); } } } } else { - final BeanIntrospectionWriter writer = new BeanIntrospectionWriter( - element, - metadata ? MutableAnnotationMetadata.of(element.getAnnotationMetadata()) : null - ); - - processElement( - metadata, - includes, - excludes, - excludedAnnotations, - indexedAnnotations, - element, - writer, - finalVisibilities, - finalAccessKinds + element, + metadata ? element.getAnnotationMetadata() : null ); + processElement(metadata, indexedAnnotations, element, writer); } } @@ -274,23 +171,6 @@ public VisitorKind getVisitorKind() { @Override public void finish(VisitorContext visitorContext) { try { - for (AbstractIntrospection abstractIntrospection : abstractIntrospections) { - final Collection properties = abstractIntrospection.properties.values(); - if (CollectionUtils.isNotEmpty(properties)) { - processBeanProperties( - abstractIntrospection.writer, - properties, - abstractIntrospection.includes, - abstractIntrospection.excludes, - abstractIntrospection.ignored, - abstractIntrospection.indexedAnnotations, - abstractIntrospection.metadata - ); - writers.put(abstractIntrospection.writer.getBeanType().getClassName(), abstractIntrospection.writer); - } - - } - if (!writers.isEmpty()) { for (BeanIntrospectionWriter writer : writers.values()) { try { @@ -301,280 +181,97 @@ public void finish(VisitorContext visitorContext) { } } } finally { - abstractIntrospections.clear(); writers.clear(); } } - private void processElement( - boolean metadata, - Set includes, - Set excludes, - Set excludedAnnotations, - Set indexedAnnotations, - ClassElement ce, - BeanIntrospectionWriter writer, - Introspected.Visibility[] visibilities, - Introspected.AccessKind...accessKinds) { + private void processElement(boolean metadata, + Set indexedAnnotations, + ClassElement ce, + BeanIntrospectionWriter writer) { + List beanProperties = ce.getBeanProperties().stream() + .filter(p -> !p.isExcluded()) + .collect(Collectors.toList()); Optional constructorElement = ce.getPrimaryConstructor(); - if (ce.isAbstract() && !constructorElement.isPresent() && ce.hasStereotype(Introspected.class)) { - currentAbstractIntrospection = new AbstractIntrospection( - writer, - includes, - excludes, - excludedAnnotations, - indexedAnnotations, - metadata - ); - abstractIntrospections.add(currentAbstractIntrospection); - } else { - final List accessKindSet = Arrays.asList(accessKinds); - final Set visibilitySet = CollectionUtils.setOf(visibilities); - List beanProperties = accessKindSet.contains(Introspected.AccessKind.METHOD) ? ce.getBeanProperties() : Collections.emptyList(); - - final List beanFields; - - if (accessKindSet.contains(Introspected.AccessKind.FIELD)) { - Predicate nameFilter = null; - if (accessKindSet.iterator().next() == Introspected.AccessKind.METHOD) { - // prioritize methods - List finalBeanProperties = beanProperties; - nameFilter = (name) -> { - for (PropertyElement beanProperty : finalBeanProperties) { - if (name.equals(beanProperty.getName())) { - return false; - } - } - return true; - }; - } - ElementQuery query; - if (visibilitySet.contains(Introspected.Visibility.DEFAULT)) { - query = ElementQuery.of(FieldElement.class) - .onlyAccessible() - .modifiers((modifiers) -> !modifiers.contains(ElementModifier.STATIC) && !modifiers.contains(ElementModifier.PROTECTED)); - - } else { - query = ElementQuery.of(FieldElement.class) - .modifiers((modifiers) -> - !modifiers.contains(ElementModifier.STATIC) && - visibilitySet.stream().anyMatch(v -> - modifiers.contains(ElementModifier.valueOf(v.name())))); - } - if (nameFilter != null) { - query = query.named(nameFilter); - } - beanFields = ce.getEnclosedElements(query); - } else { - beanFields = Collections.emptyList(); - } - - if (!beanFields.isEmpty() && !beanProperties.isEmpty()) { - // filter out properties that use field access - beanProperties = beanProperties.stream().filter(pe -> - beanFields.stream().noneMatch(fieldElement -> fieldElement.getName().equals(pe.getName())) - ).collect(Collectors.toList()); - } - - final MethodElement constructor = constructorElement.orElse(null); - process( - constructor, - ce.getDefaultConstructor().orElse(null), - writer, - beanProperties, - includes, - excludes, - excludedAnnotations, - indexedAnnotations, - metadata - ); - - for (FieldElement beanField : beanFields) { - writer.visitBeanField(beanField); - } - - ElementQuery query = ElementQuery.of(MethodElement.class) - .onlyAccessible() - .modifiers((modifiers) -> !modifiers.contains(ElementModifier.STATIC)) - .annotated((am) -> am.hasStereotype(Executable.class)); - List executableMethods = ce.getEnclosedElements(query); - for (MethodElement executableMethod : executableMethods) { - writer.visitBeanMethod(executableMethod); + constructorElement.ifPresent(constructorEl -> { + if (ArrayUtils.isNotEmpty(constructorEl.getParameters())) { + writer.visitConstructor(constructorEl); } - } - } - - private void process( - @Nullable MethodElement constructorElement, - @Nullable MethodElement defaultConstructor, - BeanIntrospectionWriter writer, - List beanProperties, - Set includes, - Set excludes, - Set ignored, - Set indexedAnnotations, - boolean metadata, - Introspected.AccessKind...accessKind) { + }); + ce.getDefaultConstructor().ifPresent(writer::visitDefaultConstructor); - if (constructorElement != null) { - final ParameterElement[] parameters = constructorElement.getParameters(); - if (ArrayUtils.isNotEmpty(parameters)) { - writer.visitConstructor(constructorElement); - } - } - if (defaultConstructor != null) { - writer.visitDefaultConstructor(defaultConstructor); - } - - processBeanProperties(writer, beanProperties, includes, excludes, ignored, indexedAnnotations, metadata); - - writers.put(writer.getBeanType().getClassName(), writer); - } - - private void processBeanProperties( - BeanIntrospectionWriter writer, - Collection beanProperties, - Set includes, - Set excludes, - Set ignored, - Set indexedAnnotations, - boolean metadata) { for (PropertyElement beanProperty : beanProperties) { - final ClassElement type = beanProperty.getType(); - final ClassElement genericType = beanProperty.getGenericType(); - - final String name = beanProperty.getName(); - if (!includes.isEmpty() && !includes.contains(name)) { - continue; - } - if (!excludes.isEmpty() && excludes.contains(name)) { - continue; - } - - if (!ignored.isEmpty() && ignored.stream().anyMatch(beanProperty::hasAnnotation)) { + if (beanProperty.isExcluded()) { continue; } - AnnotationMetadata annotationMetadata; if (metadata) { - annotationMetadata = MutableAnnotationMetadata.of(beanProperty.getAnnotationMetadata()); + annotationMetadata = beanProperty.getTargetAnnotationMetadata(); + if (annotationMetadata instanceof AnnotationMetadataHierarchy) { + annotationMetadata = ((AnnotationMetadataHierarchy) annotationMetadata).merge(); + } } else { annotationMetadata = null; } + writer.visitProperty( - type, - genericType, - name, - beanProperty.getReadMethod().orElse(null), - beanProperty.getWriteMethod().orElse(null), - beanProperty.isReadOnly(), - annotationMetadata, - genericType.getTypeArguments() + beanProperty.getType(), + beanProperty.getGenericType(), + beanProperty.getName(), + beanProperty.getReadMember().orElse(null), + beanProperty.getWriteMember().orElse(null), + beanProperty.isReadOnly(), + annotationMetadata, + beanProperty.getGenericType().getTypeArguments() ); for (AnnotationValue indexedAnnotation : indexedAnnotations) { indexedAnnotation.get("annotation", String.class).ifPresent(annotationName -> { if (beanProperty.hasStereotype(annotationName)) { writer.indexProperty( - annotationName, - name, - indexedAnnotation.get("member", String.class) - .flatMap(m -> beanProperty.getValue(annotationName, m, String.class)).orElse(null) + annotationName, + beanProperty.getName(), + indexedAnnotation.get("member", String.class) + .flatMap(m1 -> beanProperty.getValue(annotationName, m1, String.class)).orElse(null) ); } }); - } } - } - /** - * Holder for an abstract introspection. - */ - private class AbstractIntrospection { - final BeanIntrospectionWriter writer; - final Set includes; - final Set excludes; - final Set ignored; - final Set indexedAnnotations; - final boolean metadata; - final Map properties = new LinkedHashMap<>(); + writers.put(writer.getBeanType().getClassName(), writer); - public AbstractIntrospection( - BeanIntrospectionWriter writer, - Set includes, - Set excludes, - Set ignored, - Set indexedAnnotations, - boolean metadata) { - this.writer = writer; - this.includes = includes; - this.excludes = excludes; - this.ignored = ignored; - this.indexedAnnotations = indexedAnnotations; - this.metadata = metadata; + if (!ce.isAbstract() || constructorElement.isPresent() || !ce.hasStereotype(Introspected.class)) { + addExecutableMethods(ce, writer, beanProperties); } } - /** - * Used to accumulate property elements for abstract types. - */ - private static class AbstractPropertyElement implements PropertyElement { - - private final ClassElement declaringType; - private final ClassElement type; - private final String name; - - private MethodElement writeMethod; - private MethodElement readMethod; - - AbstractPropertyElement(ClassElement declaringType, ClassElement type, String name) { - this.declaringType = declaringType; - this.type = type; - this.name = name; - } - - @Override - public Optional getWriteMethod() { - return Optional.ofNullable(writeMethod); - } - - @Override - public Optional getReadMethod() { - return Optional.ofNullable(readMethod); - } - - @NonNull - @Override - public ClassElement getType() { - return type; - } - - @Override - public ClassElement getDeclaringType() { - return declaringType; - } - - @NonNull - @Override - public String getName() { - return name; - } - - @Override - public boolean isProtected() { - return false; - } - - @Override - public boolean isPublic() { - return true; + private void addExecutableMethods(ClassElement ce, BeanIntrospectionWriter writer, List beanProperties) { + Set added = new HashSet<>(); + for (PropertyElement beanProperty : beanProperties) { + if (beanProperty.isExcluded()) { + continue; + } + beanProperty.getReadMethod().filter(m -> m.hasStereotype(Executable.class)).ifPresent(methodElement -> { + added.add(methodElement); + writer.visitBeanMethod(methodElement); + }); + beanProperty.getWriteMethod().filter(m -> m.hasStereotype(Executable.class)).ifPresent(methodElement -> { + added.add(methodElement); + writer.visitBeanMethod(methodElement); + }); } - - @NonNull - @Override - public Object getNativeType() { - return this; + ElementQuery query = ElementQuery.of(MethodElement.class) + .onlyAccessible() + .modifiers((modifiers) -> !modifiers.contains(ElementModifier.STATIC)) + .annotated((am) -> am.hasStereotype(Executable.class)); + List executableMethods = ce.getEnclosedElements(query); + for (MethodElement executableMethod : executableMethods) { + if (added.contains(executableMethod)) { + continue; + } + added.add(executableMethod); + writer.visitBeanMethod(executableMethod); } } diff --git a/inject/src/main/java/io/micronaut/inject/mappers/ConfigurationPropertiesToBeanPropertiesMapper.java b/inject/src/main/java/io/micronaut/inject/mappers/ConfigurationPropertiesToBeanPropertiesMapper.java deleted file mode 100644 index 10e5142aa03..00000000000 --- a/inject/src/main/java/io/micronaut/inject/mappers/ConfigurationPropertiesToBeanPropertiesMapper.java +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Copyright 2017-2022 original authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.micronaut.inject.mappers; - -import io.micronaut.context.annotation.ConfigurationReader; -import io.micronaut.core.annotation.AnnotationValue; -import io.micronaut.context.annotation.BeanProperties; -import io.micronaut.core.annotation.Internal; -import io.micronaut.inject.annotation.TypedAnnotationMapper; -import io.micronaut.inject.visitor.VisitorContext; - -import java.util.Collections; -import java.util.List; - -/** - * Map values of {@link ConfigurationReader} to {@link BeanProperties}. - * - * @author Denis Stepanov - * @since 4.0.0 - */ -@Internal -public final class ConfigurationPropertiesToBeanPropertiesMapper implements TypedAnnotationMapper { - - @Override - public List> map(AnnotationValue annotation, VisitorContext visitorContext) { - return Collections.singletonList( - AnnotationValue.builder(BeanProperties.class) - // Configuration properties also includes fields - .member(BeanProperties.MEMBER_ACCESS_KIND, new BeanProperties.AccessKind[]{BeanProperties.AccessKind.FIELD, BeanProperties.AccessKind.METHOD}) - .member(BeanProperties.MEMBER_VISIBILITY, BeanProperties.Visibility.DEFAULT) - .build() - ); - } - - @Override - public Class annotationType() { - return ConfigurationReader.class; - } -} diff --git a/inject/src/main/java/io/micronaut/inject/mappers/ConfigurationBuilderToBeanPropertiesMapper.java b/inject/src/main/java/io/micronaut/inject/mappers/IntrospectedToBeanPropertiesMapper.java similarity index 52% rename from inject/src/main/java/io/micronaut/inject/mappers/ConfigurationBuilderToBeanPropertiesMapper.java rename to inject/src/main/java/io/micronaut/inject/mappers/IntrospectedToBeanPropertiesMapper.java index 44bde8435cc..5e1e4559690 100644 --- a/inject/src/main/java/io/micronaut/inject/mappers/ConfigurationBuilderToBeanPropertiesMapper.java +++ b/inject/src/main/java/io/micronaut/inject/mappers/IntrospectedToBeanPropertiesMapper.java @@ -16,9 +16,9 @@ package io.micronaut.inject.mappers; import io.micronaut.context.annotation.BeanProperties; -import io.micronaut.context.annotation.ConfigurationBuilder; import io.micronaut.core.annotation.AnnotationValue; -import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.Introspected; +import io.micronaut.core.util.ArrayUtils; import io.micronaut.inject.annotation.TypedAnnotationMapper; import io.micronaut.inject.visitor.VisitorContext; @@ -26,31 +26,36 @@ import java.util.List; /** - * Map values of {@link ConfigurationBuilder} to {@link BeanProperties}. + * Map values of {@link Introspected} to {@link BeanProperties}. * * @author Denis Stepanov * @since 4.0.0 */ -@Internal -public final class ConfigurationBuilderToBeanPropertiesMapper implements TypedAnnotationMapper { +public final class IntrospectedToBeanPropertiesMapper implements TypedAnnotationMapper { @Override - public List> map(AnnotationValue annotation, VisitorContext visitorContext) { + public List> map(AnnotationValue annotation, VisitorContext visitorContext) { + Introspected.AccessKind[] accessKinds = annotation.enumValues(BeanProperties.MEMBER_ACCESS_KIND, Introspected.AccessKind.class); + Introspected.Visibility[] visibilities = annotation.enumValues(BeanProperties.MEMBER_VISIBILITY, Introspected.Visibility.class); + if (ArrayUtils.isEmpty(accessKinds)) { + accessKinds = Introspected.DEFAULT_ACCESS_KIND; + } + if (ArrayUtils.isEmpty(visibilities)) { + visibilities = Introspected.DEFAULT_VISIBILITY; + } return Collections.singletonList( AnnotationValue.builder(BeanProperties.class) - // Configuration properties also includes fields - .member(BeanProperties.MEMBER_ACCESS_KIND, new BeanProperties.AccessKind[]{BeanProperties.AccessKind.METHOD}) - .member(BeanProperties.MEMBER_VISIBILITY, BeanProperties.Visibility.DEFAULT) + .member(BeanProperties.MEMBER_ACCESS_KIND, accessKinds) + .member(BeanProperties.MEMBER_VISIBILITY, visibilities) .member(BeanProperties.MEMBER_INCLUDES, annotation.stringValues(BeanProperties.MEMBER_INCLUDES)) .member(BeanProperties.MEMBER_EXCLUDES, annotation.stringValues(BeanProperties.MEMBER_EXCLUDES)) - .member(BeanProperties.MEMBER_ALLOW_WRITE_WITH_ZERO_ARGS, annotation.booleanValue("allowZeroArgs").orElse(false)) - .member(BeanProperties.MEMBER_ALLOW_WRITE_WITH_MULTIPLE_ARGS, true) + .member(BeanProperties.MEMBER_EXCLUDED_ANNOTATIONS, annotation.stringValues(BeanProperties.MEMBER_EXCLUDED_ANNOTATIONS)) .build() ); } @Override - public Class annotationType() { - return ConfigurationBuilder.class; + public Class annotationType() { + return Introspected.class; } } diff --git a/inject/src/main/java/io/micronaut/inject/writer/DispatchWriter.java b/inject/src/main/java/io/micronaut/inject/writer/DispatchWriter.java index 301a64ea864..23b9af20570 100644 --- a/inject/src/main/java/io/micronaut/inject/writer/DispatchWriter.java +++ b/inject/src/main/java/io/micronaut/inject/writer/DispatchWriter.java @@ -412,6 +412,10 @@ public void writeDispatchOne(GeneratorAdapter writer) { pushBoxPrimitiveIfNecessary(propertyType, writer); } + @NotNull + public FieldElement getField() { + return beanField; + } } /** @@ -458,6 +462,11 @@ public void writeDispatchOne(GeneratorAdapter writer) { // push null return type writer.push((String) null); } + + @NotNull + public FieldElement getField() { + return beanField; + } } /** diff --git a/inject/src/main/resources/META-INF/services/io.micronaut.inject.annotation.AnnotationMapper b/inject/src/main/resources/META-INF/services/io.micronaut.inject.annotation.AnnotationMapper index abe2d750895..ab2c4c0cdcb 100644 --- a/inject/src/main/resources/META-INF/services/io.micronaut.inject.annotation.AnnotationMapper +++ b/inject/src/main/resources/META-INF/services/io.micronaut.inject.annotation.AnnotationMapper @@ -6,5 +6,4 @@ io.micronaut.inject.beans.visitor.MappedSuperClassIntrospectionMapper io.micronaut.inject.beans.visitor.JsonCreatorAnnotationMapper io.micronaut.inject.beans.visitor.jakarta.persistence.JakartaEntityIntrospectedAnnotationMapper io.micronaut.inject.beans.visitor.jakarta.persistence.JakartaMappedSuperClassIntrospectionMapper -io.micronaut.inject.mappers.ConfigurationBuilderToBeanPropertiesMapper -io.micronaut.inject.mappers.ConfigurationPropertiesToBeanPropertiesMapper +io.micronaut.inject.mappers.IntrospectedToBeanPropertiesMapper diff --git a/src/main/docs/guide/appendix/breaks.adoc b/src/main/docs/guide/appendix/breaks.adoc index ae91f9b645b..7e42bcc1522 100644 --- a/src/main/docs/guide/appendix/breaks.adoc +++ b/src/main/docs/guide/appendix/breaks.adoc @@ -26,6 +26,14 @@ The `javax.annotation` library is no longer a directory dependency. Any referenc Kotlin has been updated to 1.7.20, which may cause issues when compiling or linking to Kotlin libraries. +==== Bean Introspection changes + +Before, when both METHOD and FIELD were set as the access kind, the bean introspection would choose the same access type to get and set the property value. In Micronaut 4, the accessors can be of different kinds: a field to get and a method to set, and vice versa. + +==== Annotations with retention CLASS are excluded at runtime + +Annotations with the retention CLASS are not available in the annotation metadata at the runtime. + == 3.3.0 - The <> is now disabled by default. To enable it, you must update your endpoint config: From e4cf51df7fba6f8693b048cfeebc19682654a37e Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 13 Oct 2022 10:27:32 +0200 Subject: [PATCH 112/743] fix(deps): update dependency io.micronaut.coherence:micronaut-coherence-bom to v3.7.1 (#8135) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 2b3f79e02a7..f7e2d7e6f7b 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -75,7 +75,7 @@ managed-micronaut-aws = "3.9.2" managed-micronaut-azure = "3.5.0" managed-micronaut-cache = "3.5.0" managed-micronaut-cassandra = "5.1.1" -managed-micronaut-coherence = "3.5.1" +managed-micronaut-coherence = "3.7.1" managed-micronaut-crac = "1.0.1" managed-micronaut-data = "3.8.0" managed-micronaut-discovery = "3.2.0" From ad073cff933e148d8f43504163402432821ec282 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 13 Oct 2022 10:27:43 +0200 Subject: [PATCH 113/743] fix(deps): update dependency org.codehaus.groovy:groovy-test to v3.0.13 (#8132) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index f7e2d7e6f7b..f472fed5e25 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -7,7 +7,7 @@ caffeine = "2.9.3" compile-testing = "0.19" geb = "3.4.1" -geb-groovy = "3.0.12" +geb-groovy = "3.0.13" geb-spock = "2.2-groovy-3.0" hibernate = "5.5.9.Final" From e6a80096d5b47a15dcabaa68575600394bbfb993 Mon Sep 17 00:00:00 2001 From: Jonas Konrad Date: Fri, 14 Oct 2022 10:11:36 +0200 Subject: [PATCH 114/743] Fix HttpClient.start (#8155) This fixes the tests in micronaut-reactor, and copies the relevant test to this repo as well. --- .../http/client/netty/ConnectionManager.java | 23 +++++++++---- .../http/client/HttpClientCloseSpec.groovy | 34 +++++++++++++++++++ 2 files changed, 50 insertions(+), 7 deletions(-) create mode 100644 http-client/src/test/groovy/io/micronaut/http/client/HttpClientCloseSpec.groovy diff --git a/http-client/src/main/java/io/micronaut/http/client/netty/ConnectionManager.java b/http-client/src/main/java/io/micronaut/http/client/netty/ConnectionManager.java index 6024bd86932..28d8755172c 100644 --- a/http-client/src/main/java/io/micronaut/http/client/netty/ConnectionManager.java +++ b/http-client/src/main/java/io/micronaut/http/client/netty/ConnectionManager.java @@ -148,7 +148,8 @@ final class ConnectionManager { private EventLoopGroup group; private final boolean shutdownGroup; private final ThreadFactory threadFactory; - private final Bootstrap bootstrap; + private final ChannelFactory socketChannelFactory; + private Bootstrap bootstrap; private final HttpClientConfiguration configuration; @Nullable private final Long readTimeoutMillis; @@ -179,6 +180,7 @@ final class ConnectionManager { this.log = log; this.httpVersion = httpVersion; this.threadFactory = threadFactory; + this.socketChannelFactory = socketChannelFactory; this.configuration = configuration; this.instrumenter = instrumenter; this.clientCustomizer = clientCustomizer; @@ -201,10 +203,7 @@ final class ConnectionManager { shutdownGroup = true; } - this.bootstrap = new Bootstrap(); - this.bootstrap.group(group) - .channelFactory(socketChannelFactory) - .option(ChannelOption.SO_KEEPALIVE, true); + initBootstrap(); final ChannelHealthChecker channelHealthChecker = channel -> channel.eventLoop().newSucceededFuture(channel.isActive() && !ConnectTTLHandler.isChannelExpired(channel)); @@ -306,8 +305,18 @@ private static NioEventLoopGroup createEventLoopGroup(HttpClientConfiguration co * @see DefaultHttpClient#start() */ public void start() { - group = createEventLoopGroup(configuration, threadFactory); - bootstrap.group(group); + // only need to start new group if it's managed by us + if (shutdownGroup) { + group = createEventLoopGroup(configuration, threadFactory); + initBootstrap(); // rebuild bootstrap with new group + } + } + + private void initBootstrap() { + this.bootstrap = new Bootstrap(); + this.bootstrap.group(group) + .channelFactory(socketChannelFactory) + .option(ChannelOption.SO_KEEPALIVE, true); } /** diff --git a/http-client/src/test/groovy/io/micronaut/http/client/HttpClientCloseSpec.groovy b/http-client/src/test/groovy/io/micronaut/http/client/HttpClientCloseSpec.groovy new file mode 100644 index 00000000000..027f0667b8b --- /dev/null +++ b/http-client/src/test/groovy/io/micronaut/http/client/HttpClientCloseSpec.groovy @@ -0,0 +1,34 @@ +package io.micronaut.http.client + +import spock.lang.Specification +import spock.util.concurrent.PollingConditions + +class HttpClientCloseSpec extends Specification { + void "confirm HttpClient can be stopped"() { + given: + HttpClient client = HttpClient.create(new URL("http://localhost")) + + expect: + client.isRunning() + + when: + client.stop() + + then: + new PollingConditions().eventually { + !client.isRunning() + } + + when: + client.start() + + then: + new PollingConditions().eventually { + client.isRunning() + } + + cleanup: + client.close() + + } +} From ddaf784babbd817eaa8bb95f4a4e519611a8864e Mon Sep 17 00:00:00 2001 From: Denis Stepanov Date: Fri, 14 Oct 2022 15:40:25 +0200 Subject: [PATCH 115/743] Fix `ClassElement` equals with primitive and bean property type (#8163) --- .../inject/visitor/ClassElementSpec.groovy | 24 ++++++ .../beans/BeanIntrospectionSpec.groovy | 78 +++++++++++++++++++ .../visitor/AbstractJavaElement.java | 4 + .../visitors/ClassElementSpec.groovy | 22 ++++++ .../ast/utils/AstBeanPropertiesUtils.java | 13 ++++ 5 files changed, 141 insertions(+) diff --git a/inject-groovy/src/test/groovy/io/micronaut/inject/visitor/ClassElementSpec.groovy b/inject-groovy/src/test/groovy/io/micronaut/inject/visitor/ClassElementSpec.groovy index 7d5563448b1..0cc022f5dd6 100644 --- a/inject-groovy/src/test/groovy/io/micronaut/inject/visitor/ClassElementSpec.groovy +++ b/inject-groovy/src/test/groovy/io/micronaut/inject/visitor/ClassElementSpec.groovy @@ -26,6 +26,7 @@ import io.micronaut.inject.ast.FieldElement import io.micronaut.inject.ast.MemberElement import io.micronaut.inject.ast.MethodElement import io.micronaut.inject.ast.PackageElement +import io.micronaut.inject.ast.PrimitiveElement import io.micronaut.inject.ast.PropertyElement import io.micronaut.inject.ast.TypedElement import spock.lang.Issue @@ -47,6 +48,29 @@ class ClassElementSpec extends AbstractBeanDefinitionSpec { AllElementsVisitor.clearVisited() } + void "test equals with primitive"() { + given: + def element = buildClassElement(""" +package test + +class Test { + +boolean test1 + +} +""") + + expect: + element != PrimitiveElement.BOOLEAN + element != PrimitiveElement.VOID + element != PrimitiveElement.BOOLEAN.withArrayDimensions(4) + PrimitiveElement.VOID != element + PrimitiveElement.INT != element + PrimitiveElement.INT.withArrayDimensions(2) != element + element.getFields().get(0).getType() == PrimitiveElement.BOOLEAN + PrimitiveElement.BOOLEAN == element.getFields().get(0).getType() + } + @Issue("https://github.com/micronaut-projects/micronaut-openapi/issues/670") void "test correct properties decaliring class with inheritance"() { given: diff --git a/inject-java-test/src/test/groovy/io/micronaut/inject/visitor/beans/BeanIntrospectionSpec.groovy b/inject-java-test/src/test/groovy/io/micronaut/inject/visitor/beans/BeanIntrospectionSpec.groovy index 534eb21340d..da51c8f6368 100644 --- a/inject-java-test/src/test/groovy/io/micronaut/inject/visitor/beans/BeanIntrospectionSpec.groovy +++ b/inject-java-test/src/test/groovy/io/micronaut/inject/visitor/beans/BeanIntrospectionSpec.groovy @@ -77,6 +77,84 @@ class Test { introspection.beanMethods.first().returnType.type == CharSequence[].class } + void "test property type is defined by its setter"() { + given: + def introspection = buildBeanIntrospection('test.Test', ''' +package test; + +import io.micronaut.core.annotation.Introspected; +import io.micronaut.context.annotation.Executable; +import io.micronaut.core.annotation.Nullable; +import java.util.Optional; + +@Introspected +class Test { + @Nullable + private String foo; + + public Optional getFoo() { + return Optional.ofNullable(foo); + } + + public void setFoo(@Nullable String foo) { + this.foo = foo; + } +} +''') + expect: + introspection.getProperty("foo").get().type == String.class + } + + void "test property type is defined by its writer field"() { + given: + def introspection = buildBeanIntrospection('test.Test', ''' +package test; + +import io.micronaut.core.annotation.Introspected; +import io.micronaut.context.annotation.Executable; +import io.micronaut.core.annotation.Nullable; +import java.util.Optional; + +@Introspected(accessKind = {Introspected.AccessKind.METHOD, Introspected.AccessKind.FIELD}) +class Test { + @Nullable + String foo; + + public Optional getFoo() { + return Optional.ofNullable(foo); + } + +} +''') + expect: + introspection.getProperty("foo").get().type == String.class + } + + void "test property type is not defined by its not accessible field"() { + given: + def introspection = buildBeanIntrospection('test.Test', ''' +package test; + +import io.micronaut.core.annotation.Introspected; +import io.micronaut.context.annotation.Executable; +import io.micronaut.core.annotation.Nullable; +import java.util.Optional; + +@Introspected(accessKind = {Introspected.AccessKind.METHOD, Introspected.AccessKind.FIELD}) +class Test { + @Nullable + private String foo; + + public Optional getFoo() { + return Optional.ofNullable(foo); + } + +} +''') + expect: + introspection.getProperty("foo").get().type == Optional.class + } + void 'test favor method access'() { given: BeanIntrospection introspection = buildBeanIntrospection('fieldaccess.Test','''\ diff --git a/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/AbstractJavaElement.java b/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/AbstractJavaElement.java index ab77fba80ba..acdd38abfa2 100644 --- a/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/AbstractJavaElement.java +++ b/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/AbstractJavaElement.java @@ -26,6 +26,7 @@ import io.micronaut.inject.ast.ElementMutableAnnotationMetadata; import io.micronaut.inject.ast.ElementMutableAnnotationMetadataDelegate; import io.micronaut.inject.ast.PrimitiveElement; +import io.micronaut.inject.ast.TypedElement; import javax.lang.model.element.AnnotationMirror; import javax.lang.model.element.Element; @@ -430,6 +431,9 @@ public boolean equals(Object o) { return false; } io.micronaut.inject.ast.Element that = (io.micronaut.inject.ast.Element) o; + if (that instanceof TypedElement && ((TypedElement) that).isPrimitive()) { + return false; + } return element.equals(that.getNativeType()); } diff --git a/inject-java/src/test/groovy/io/micronaut/visitors/ClassElementSpec.groovy b/inject-java/src/test/groovy/io/micronaut/visitors/ClassElementSpec.groovy index d0c427e8706..6066b0d5789 100644 --- a/inject-java/src/test/groovy/io/micronaut/visitors/ClassElementSpec.groovy +++ b/inject-java/src/test/groovy/io/micronaut/visitors/ClassElementSpec.groovy @@ -29,6 +29,7 @@ import io.micronaut.inject.ast.FieldElement import io.micronaut.inject.ast.MemberElement import io.micronaut.inject.ast.MethodElement import io.micronaut.inject.ast.PackageElement +import io.micronaut.inject.ast.PrimitiveElement import jakarta.inject.Singleton import spock.lang.IgnoreIf import spock.lang.Issue @@ -41,6 +42,27 @@ import java.util.function.Supplier class ClassElementSpec extends AbstractTypeElementSpec { + void "test equals with primitive"() { + given: + def element = buildClassElement(""" +package test; + +class Test { + boolean test1; +} +""") + + expect: + element != PrimitiveElement.BOOLEAN + element != PrimitiveElement.VOID + element != PrimitiveElement.BOOLEAN.withArrayDimensions(4) + PrimitiveElement.VOID != element + PrimitiveElement.INT != element + PrimitiveElement.INT.withArrayDimensions(2) != element + element.getFields().get(0).getType() == PrimitiveElement.BOOLEAN + PrimitiveElement.BOOLEAN == element.getFields().get(0).getType() + } + void "test resolve receiver type on method"() { given: def element = buildClassElement(""" diff --git a/inject/src/main/java/io/micronaut/inject/ast/utils/AstBeanPropertiesUtils.java b/inject/src/main/java/io/micronaut/inject/ast/utils/AstBeanPropertiesUtils.java index 17c37378017..6b12705d136 100644 --- a/inject/src/main/java/io/micronaut/inject/ast/utils/AstBeanPropertiesUtils.java +++ b/inject/src/main/java/io/micronaut/inject/ast/utils/AstBeanPropertiesUtils.java @@ -27,6 +27,7 @@ import io.micronaut.inject.ast.FieldElement; import io.micronaut.inject.ast.MemberElement; import io.micronaut.inject.ast.MethodElement; +import io.micronaut.inject.ast.ParameterElement; import io.micronaut.inject.ast.PrimitiveElement; import io.micronaut.inject.ast.PropertyElement; @@ -139,6 +140,18 @@ public static List resolveBeanProperties(BeanPropertiesQuery co for (Map.Entry entry : props.entrySet()) { String propertyName = entry.getKey(); BeanPropertyData value = entry.getValue(); + // Define the property type based on its writer element + if (value.writeAccessKind == BeanProperties.AccessKind.FIELD && !value.field.getType().equals(value.type)) { + value.type = value.field.getGenericType(); + } else if (value.writeAccessKind == BeanProperties.AccessKind.METHOD + && value.setter != null + && value.setter.getParameters().length > 0) { + ParameterElement parameter = value.setter.getParameters()[0]; + if (!parameter.getType().equals(value.type)) { + value.type = parameter.getGenericType(); + } + } + if (value.readAccessKind != null || value.writeAccessKind != null) { value.isExcluded = shouldExclude(includes, excludes, propertyName) || isExcludedByAnnotations(configuration, value) From 8200898d1db6c1ed85353777d60233f36d0382b0 Mon Sep 17 00:00:00 2001 From: Jonas Konrad Date: Fri, 14 Oct 2022 16:22:13 +0200 Subject: [PATCH 116/743] Fix STREAM_PIPELINE_ATTRIBUTE (#8162) Apparently there was a race condition with AttributeKey.newInstance being called multiple times --- .../server/netty/HttpPipelineBuilder.java | 26 ++++--------------- .../http/server/netty/NettyHttpRequest.java | 2 +- 2 files changed, 6 insertions(+), 22 deletions(-) diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/HttpPipelineBuilder.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/HttpPipelineBuilder.java index 1d21a583f9a..22920c87aee 100644 --- a/http-server-netty/src/main/java/io/micronaut/http/server/netty/HttpPipelineBuilder.java +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/HttpPipelineBuilder.java @@ -17,6 +17,7 @@ import io.micronaut.core.annotation.NonNull; import io.micronaut.core.naming.Named; +import io.micronaut.core.util.SupplierUtil; import io.micronaut.http.context.event.HttpRequestReceivedEvent; import io.micronaut.http.netty.channel.ChannelPipelineCustomizer; import io.micronaut.http.netty.stream.HttpStreamsServerHandler; @@ -74,7 +75,7 @@ import java.time.Duration; import java.time.Instant; import java.util.concurrent.ThreadLocalRandom; -import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Supplier; /** * Helper class that manages the {@link ChannelPipeline} of incoming HTTP connections. @@ -86,6 +87,8 @@ * @author ywkat */ final class HttpPipelineBuilder { + static final Supplier> STREAM_PIPELINE_ATTRIBUTE = + SupplierUtil.memoized(() -> AttributeKey.newInstance("stream-pipeline")); private static final Logger LOG = LoggerFactory.getLogger(HttpPipelineBuilder.class); @@ -464,7 +467,7 @@ private void insertHttp2DownstreamHandlers() { * and netty requests, and routing. */ private void insertMicronautHandlers() { - channel.attr(StreamPipelineAttributeKeyHolder.getInstance()).set(this); + channel.attr(STREAM_PIPELINE_ATTRIBUTE.get()).set(this); pipeline.addLast(ChannelPipelineCustomizer.HANDLER_HTTP_COMPRESSOR, new SmartHttpContentCompressor(embeddedServices.getHttpCompressionStrategy())); pipeline.addLast(ChannelPipelineCustomizer.HANDLER_HTTP_DECOMPRESSOR, new HttpContentDecompressor()); @@ -518,23 +521,4 @@ private void registerMicronautChannelHandlers() { } } } - - // We need the AttributeKey to be static, as it's used in NettyHttpRequest, but we can't eagerly initialize it - // as it would fail in Graal - static final class StreamPipelineAttributeKeyHolder { - - private static final AtomicReference> INSTANCE = new AtomicReference<>(); - - private StreamPipelineAttributeKeyHolder() { - } - - static AttributeKey getInstance() { - return INSTANCE.updateAndGet(key -> { - if (key == null) { - return AttributeKey.newInstance("micronaut-stream-pipeline"); - } - return key; - }); - } - } } diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/NettyHttpRequest.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/NettyHttpRequest.java index 2f87d752d6d..fe7def6f3dd 100644 --- a/http-server-netty/src/main/java/io/micronaut/http/server/netty/NettyHttpRequest.java +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/NettyHttpRequest.java @@ -527,7 +527,7 @@ public PushCapableHttpRequest serverPush(@NonNull HttpRequest request) { ); int ourStream = ((Http2StreamChannel) channelHandlerContext.channel()).stream().id(); - HttpPipelineBuilder.StreamPipeline originalStreamPipeline = channelHandlerContext.channel().attr(HttpPipelineBuilder.StreamPipelineAttributeKeyHolder.getInstance()).get(); + HttpPipelineBuilder.StreamPipeline originalStreamPipeline = channelHandlerContext.channel().attr(HttpPipelineBuilder.STREAM_PIPELINE_ATTRIBUTE.get()).get(); new Http2StreamChannelBootstrap(channelHandlerContext.channel().parent()) .handler(new ChannelInitializer() { From 887bba1e11b69b1c5211c69d82330cc489c4bfbb Mon Sep 17 00:00:00 2001 From: Denis Stepanov Date: Fri, 14 Oct 2022 19:01:09 +0200 Subject: [PATCH 117/743] Fix use-case when `@Introspected` is added using `anotate` (#8158) * Fix use-case when ``@Introspected` is added using `anotate` * Remove unwanted changes --- .../beans/AnnotatedIntrospectedSpec.groovy | 63 +++++++++++++++++++ .../visitor/beans/MakeIntrospected.java | 19 ++++++ .../beans/MakeIntrospectedVisitor.java | 38 +++++++++++ ...rospectedToBeanPropertiesTransformer.java} | 13 ++-- ...cronaut.inject.annotation.AnnotationMapper | 1 - ...ut.inject.annotation.AnnotationTransformer | 1 + 6 files changed, 129 insertions(+), 6 deletions(-) create mode 100644 inject-java-test/src/test/groovy/io/micronaut/inject/visitor/beans/AnnotatedIntrospectedSpec.groovy create mode 100644 inject-java-test/src/test/groovy/io/micronaut/inject/visitor/beans/MakeIntrospected.java create mode 100644 inject-java-test/src/test/groovy/io/micronaut/inject/visitor/beans/MakeIntrospectedVisitor.java rename inject/src/main/java/io/micronaut/inject/mappers/{IntrospectedToBeanPropertiesMapper.java => IntrospectedToBeanPropertiesTransformer.java} (81%) diff --git a/inject-java-test/src/test/groovy/io/micronaut/inject/visitor/beans/AnnotatedIntrospectedSpec.groovy b/inject-java-test/src/test/groovy/io/micronaut/inject/visitor/beans/AnnotatedIntrospectedSpec.groovy new file mode 100644 index 00000000000..575940a5a8f --- /dev/null +++ b/inject-java-test/src/test/groovy/io/micronaut/inject/visitor/beans/AnnotatedIntrospectedSpec.groovy @@ -0,0 +1,63 @@ +package io.micronaut.inject.visitor.beans + +import io.micronaut.annotation.processing.TypeElementVisitorProcessor +import io.micronaut.annotation.processing.test.AbstractTypeElementSpec +import io.micronaut.annotation.processing.test.JavaParser +import io.micronaut.core.beans.BeanIntrospection +import io.micronaut.inject.beans.visitor.IntrospectedTypeElementVisitor +import io.micronaut.inject.visitor.TypeElementVisitor + +import javax.annotation.processing.SupportedAnnotationTypes + +class AnnotatedIntrospectedSpec extends AbstractTypeElementSpec { + + void "test make introspected"() { + given: + BeanIntrospection introspection = buildBeanIntrospection('test.Test', ''' +package test; + +import io.micronaut.core.annotation.*; +import javax.validation.constraints.*; +import java.util.*; + +@io.micronaut.inject.visitor.beans.MakeIntrospected +class Test extends Parent { + private String name; + public int foobar; + public String getName() { + return this.name; + } + public Test setName(String n) { + this.name = n; + return this; + } +} + +class Parent { + public String bar; +} +''') + + expect: + introspection != null + introspection.getPropertyNames() as Set == ["name", "bar", "foobar"] as Set + } + @Override + protected JavaParser newJavaParser() { + return new JavaParser() { + @Override + protected TypeElementVisitorProcessor getTypeElementVisitorProcessor() { + return new MyTypeElementVisitorProcessor() + } + } + } + + @SupportedAnnotationTypes("*") + static class MyTypeElementVisitorProcessor extends TypeElementVisitorProcessor { + + @Override + protected Collection findTypeElementVisitors() { + return [new MakeIntrospectedVisitor(), new IntrospectedTypeElementVisitor()] + } + } +} diff --git a/inject-java-test/src/test/groovy/io/micronaut/inject/visitor/beans/MakeIntrospected.java b/inject-java-test/src/test/groovy/io/micronaut/inject/visitor/beans/MakeIntrospected.java new file mode 100644 index 00000000000..c2455e24b25 --- /dev/null +++ b/inject-java-test/src/test/groovy/io/micronaut/inject/visitor/beans/MakeIntrospected.java @@ -0,0 +1,19 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.inject.visitor.beans; + +public @interface MakeIntrospected { +} diff --git a/inject-java-test/src/test/groovy/io/micronaut/inject/visitor/beans/MakeIntrospectedVisitor.java b/inject-java-test/src/test/groovy/io/micronaut/inject/visitor/beans/MakeIntrospectedVisitor.java new file mode 100644 index 00000000000..a0c3febb0a2 --- /dev/null +++ b/inject-java-test/src/test/groovy/io/micronaut/inject/visitor/beans/MakeIntrospectedVisitor.java @@ -0,0 +1,38 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.inject.visitor.beans; + +import io.micronaut.core.annotation.Introspected; +import io.micronaut.core.annotation.NonNull; +import io.micronaut.inject.ast.ClassElement; +import io.micronaut.inject.visitor.TypeElementVisitor; +import io.micronaut.inject.visitor.VisitorContext; + +public class MakeIntrospectedVisitor implements TypeElementVisitor { + + @Override + public void visitClass(ClassElement element, VisitorContext context) { + element.annotate(Introspected.class, builder -> builder.member("accessKind", Introspected.AccessKind.METHOD, Introspected.AccessKind.FIELD)); + } + + @NonNull + @Override + public VisitorKind getVisitorKind() { + return VisitorKind.ISOLATING; + } + +} + diff --git a/inject/src/main/java/io/micronaut/inject/mappers/IntrospectedToBeanPropertiesMapper.java b/inject/src/main/java/io/micronaut/inject/mappers/IntrospectedToBeanPropertiesTransformer.java similarity index 81% rename from inject/src/main/java/io/micronaut/inject/mappers/IntrospectedToBeanPropertiesMapper.java rename to inject/src/main/java/io/micronaut/inject/mappers/IntrospectedToBeanPropertiesTransformer.java index 5e1e4559690..a4c80bdcc90 100644 --- a/inject/src/main/java/io/micronaut/inject/mappers/IntrospectedToBeanPropertiesMapper.java +++ b/inject/src/main/java/io/micronaut/inject/mappers/IntrospectedToBeanPropertiesTransformer.java @@ -19,10 +19,10 @@ import io.micronaut.core.annotation.AnnotationValue; import io.micronaut.core.annotation.Introspected; import io.micronaut.core.util.ArrayUtils; -import io.micronaut.inject.annotation.TypedAnnotationMapper; +import io.micronaut.inject.annotation.TypedAnnotationTransformer; import io.micronaut.inject.visitor.VisitorContext; -import java.util.Collections; +import java.util.Arrays; import java.util.List; /** @@ -31,10 +31,12 @@ * @author Denis Stepanov * @since 4.0.0 */ -public final class IntrospectedToBeanPropertiesMapper implements TypedAnnotationMapper { +public final class IntrospectedToBeanPropertiesTransformer implements TypedAnnotationTransformer { @Override - public List> map(AnnotationValue annotation, VisitorContext visitorContext) { + public List> transform(AnnotationValue annotation, VisitorContext visitorContext) { + // We need to use AnnotationTransformer instead of AnnotationMapper + // Somehow it doesn't work when the annotation is added Introspected.AccessKind[] accessKinds = annotation.enumValues(BeanProperties.MEMBER_ACCESS_KIND, Introspected.AccessKind.class); Introspected.Visibility[] visibilities = annotation.enumValues(BeanProperties.MEMBER_VISIBILITY, Introspected.Visibility.class); if (ArrayUtils.isEmpty(accessKinds)) { @@ -43,7 +45,8 @@ public List> map(AnnotationValue annotation, Vi if (ArrayUtils.isEmpty(visibilities)) { visibilities = Introspected.DEFAULT_VISIBILITY; } - return Collections.singletonList( + return Arrays.asList( + annotation, AnnotationValue.builder(BeanProperties.class) .member(BeanProperties.MEMBER_ACCESS_KIND, accessKinds) .member(BeanProperties.MEMBER_VISIBILITY, visibilities) diff --git a/inject/src/main/resources/META-INF/services/io.micronaut.inject.annotation.AnnotationMapper b/inject/src/main/resources/META-INF/services/io.micronaut.inject.annotation.AnnotationMapper index ab2c4c0cdcb..0df05f868e7 100644 --- a/inject/src/main/resources/META-INF/services/io.micronaut.inject.annotation.AnnotationMapper +++ b/inject/src/main/resources/META-INF/services/io.micronaut.inject.annotation.AnnotationMapper @@ -6,4 +6,3 @@ io.micronaut.inject.beans.visitor.MappedSuperClassIntrospectionMapper io.micronaut.inject.beans.visitor.JsonCreatorAnnotationMapper io.micronaut.inject.beans.visitor.jakarta.persistence.JakartaEntityIntrospectedAnnotationMapper io.micronaut.inject.beans.visitor.jakarta.persistence.JakartaMappedSuperClassIntrospectionMapper -io.micronaut.inject.mappers.IntrospectedToBeanPropertiesMapper diff --git a/inject/src/main/resources/META-INF/services/io.micronaut.inject.annotation.AnnotationTransformer b/inject/src/main/resources/META-INF/services/io.micronaut.inject.annotation.AnnotationTransformer index 4916edc4e66..1ef11500700 100644 --- a/inject/src/main/resources/META-INF/services/io.micronaut.inject.annotation.AnnotationTransformer +++ b/inject/src/main/resources/META-INF/services/io.micronaut.inject.annotation.AnnotationTransformer @@ -4,3 +4,4 @@ io.micronaut.inject.annotation.internal.KotlinNullableMapper io.micronaut.inject.annotation.internal.KotlinNotNullMapper io.micronaut.inject.annotation.internal.JakartaPostConstructTransformer io.micronaut.inject.annotation.internal.JakartaPreDestroyTransformer +io.micronaut.inject.mappers.IntrospectedToBeanPropertiesTransformer From 099213cfc02e0212b2cff8c7e4d876f7ed78e0a6 Mon Sep 17 00:00:00 2001 From: Denis Stepanov Date: Mon, 17 Oct 2022 15:12:22 +0200 Subject: [PATCH 118/743] fix: Correct validation detection (#8175) --- ...gurationBasedOnRuntimeClassNodeSpec.groovy | 30 ---- .../context/visitor/ValidationVisitor.java | 26 ++- .../DeclaredBeanElementCreator.java | 2 +- .../inject/validation/RequiresValidation.java | 4 +- .../validation/ValidatedParseSpec.groovy | 65 ++++++- .../validation/ValidationVisitorSpec.groovy | 163 ++++++++++++++++++ 6 files changed, 246 insertions(+), 44 deletions(-) delete mode 100644 inject-java/src/test/groovy/io/micronaut/inject/dynamic/CreateConfigurationBasedOnRuntimeClassNodeSpec.groovy create mode 100644 validation/src/test/groovy/io/micronaut/validation/ValidationVisitorSpec.groovy diff --git a/inject-java/src/test/groovy/io/micronaut/inject/dynamic/CreateConfigurationBasedOnRuntimeClassNodeSpec.groovy b/inject-java/src/test/groovy/io/micronaut/inject/dynamic/CreateConfigurationBasedOnRuntimeClassNodeSpec.groovy deleted file mode 100644 index 98de5226a9b..00000000000 --- a/inject-java/src/test/groovy/io/micronaut/inject/dynamic/CreateConfigurationBasedOnRuntimeClassNodeSpec.groovy +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright 2017-2019 original authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.micronaut.inject.dynamic - -import spock.lang.Specification - -class CreateConfigurationBasedOnRuntimeClassNodeSpec extends Specification { -// @Ignore -// void "figure out what this test should be"() { -// given: -// -// when: -// -// then: -// null; -// } -} diff --git a/inject/src/main/java/io/micronaut/context/visitor/ValidationVisitor.java b/inject/src/main/java/io/micronaut/context/visitor/ValidationVisitor.java index 400af64f822..d10f41d2376 100644 --- a/inject/src/main/java/io/micronaut/context/visitor/ValidationVisitor.java +++ b/inject/src/main/java/io/micronaut/context/visitor/ValidationVisitor.java @@ -18,12 +18,13 @@ import io.micronaut.core.annotation.Internal; import io.micronaut.core.annotation.NextMajorVersion; import io.micronaut.core.annotation.NonNull; -import io.micronaut.inject.validation.RequiresValidation; import io.micronaut.inject.ast.ClassElement; import io.micronaut.inject.ast.ConstructorElement; import io.micronaut.inject.ast.Element; import io.micronaut.inject.ast.FieldElement; import io.micronaut.inject.ast.MethodElement; +import io.micronaut.inject.processing.ProcessingException; +import io.micronaut.inject.validation.RequiresValidation; import io.micronaut.inject.visitor.TypeElementVisitor; import io.micronaut.inject.visitor.VisitorContext; @@ -72,7 +73,7 @@ public void visitConstructor(ConstructorElement element, VisitorContext context) if (classElement == null) { return; } - if (requiresValidation(element) || parametersRequireValidation(element)) { + if (requiresValidation(element, true) || parametersRequireValidation(element, true)) { element.annotate(RequiresValidation.class); classElement.annotate(RequiresValidation.class); } @@ -83,7 +84,13 @@ public void visitMethod(MethodElement element, VisitorContext context) { if (classElement == null) { return; } - if (requiresValidation(element) || parametersRequireValidation(element)) { + boolean isPrivate = element.isPrivate(); + boolean isAbstract = element.getOwningType().isInterface() || element.getOwningType().isAbstract(); + boolean requireOnConstraint = isAbstract || !isPrivate; + if (requiresValidation(element, requireOnConstraint) || parametersRequireValidation(element, requireOnConstraint)) { + if (isPrivate) { + throw new ProcessingException(element, "Method annotated for validation but is declared private. Change the method to be non-private in order for AOP advice to be applied."); + } element.annotate(RequiresValidation.class); classElement.annotate(RequiresValidation.class); } @@ -94,17 +101,20 @@ public void visitField(FieldElement element, VisitorContext context) { if (classElement == null) { return; } - if (requiresValidation(element)) { + if (requiresValidation(element, true)) { element.annotate(RequiresValidation.class); classElement.annotate(RequiresValidation.class); } } - private boolean parametersRequireValidation(MethodElement element) { - return Arrays.stream(element.getParameters()).anyMatch(this::requiresValidation); + private boolean parametersRequireValidation(MethodElement element, boolean requireOnConstraint) { + return Arrays.stream(element.getParameters()).anyMatch(e -> requiresValidation(e, requireOnConstraint)); } - private boolean requiresValidation(Element e) { - return e.hasStereotype(ANN_VALID) || e.hasStereotype(ANN_CONSTRAINT); + private boolean requiresValidation(Element e, boolean requireOnConstraint) { + if (requireOnConstraint && e.hasStereotype(ANN_CONSTRAINT)) { + return true; + } + return e.hasStereotype(ANN_VALID); } } diff --git a/inject/src/main/java/io/micronaut/inject/processing/DeclaredBeanElementCreator.java b/inject/src/main/java/io/micronaut/inject/processing/DeclaredBeanElementCreator.java index 4808666edff..7f166cd243c 100644 --- a/inject/src/main/java/io/micronaut/inject/processing/DeclaredBeanElementCreator.java +++ b/inject/src/main/java/io/micronaut/inject/processing/DeclaredBeanElementCreator.java @@ -189,7 +189,7 @@ private void visitFieldInternal(BeanDefinitionVisitor visitor, FieldElement fiel } private void visitMethodInternal(BeanDefinitionVisitor visitor, MethodElement methodElement) { - if (methodElement.hasAnnotation(ANN_REQUIRES_VALIDATION)) { + if (methodElement.hasDeclaredAnnotation(ANN_REQUIRES_VALIDATION)) { methodElement.annotate(ANN_VALIDATED); } boolean claimed = visitMethod(visitor, methodElement); diff --git a/inject/src/main/java/io/micronaut/inject/validation/RequiresValidation.java b/inject/src/main/java/io/micronaut/inject/validation/RequiresValidation.java index 1fb3f43698a..a3db27e0791 100644 --- a/inject/src/main/java/io/micronaut/inject/validation/RequiresValidation.java +++ b/inject/src/main/java/io/micronaut/inject/validation/RequiresValidation.java @@ -22,7 +22,7 @@ import java.lang.annotation.Retention; import java.lang.annotation.Target; -import static java.lang.annotation.RetentionPolicy.SOURCE; +import static java.lang.annotation.RetentionPolicy.CLASS; /** * Internal method marks a type, method or a field for validation. @@ -31,7 +31,7 @@ * @since 4.0.0 */ @Documented -@Retention(SOURCE) +@Retention(CLASS) @Target({ElementType.METHOD, ElementType.TYPE, ElementType.FIELD}) @Internal public @interface RequiresValidation { diff --git a/validation/src/test/groovy/io/micronaut/validation/ValidatedParseSpec.groovy b/validation/src/test/groovy/io/micronaut/validation/ValidatedParseSpec.groovy index 36e898ad20d..ec5a1d0dbed 100644 --- a/validation/src/test/groovy/io/micronaut/validation/ValidatedParseSpec.groovy +++ b/validation/src/test/groovy/io/micronaut/validation/ValidatedParseSpec.groovy @@ -5,6 +5,7 @@ import io.micronaut.aop.Around import io.micronaut.inject.ProxyBeanDefinition import io.micronaut.inject.writer.BeanDefinitionVisitor import io.micronaut.inject.writer.BeanDefinitionWriter +import spock.lang.PendingFeature import java.time.LocalDate @@ -19,12 +20,12 @@ class Test { @io.micronaut.context.annotation.Executable public void setName(@javax.validation.constraints.NotBlank String name) { - + } - + @io.micronaut.context.annotation.Executable public void setName2(@javax.validation.Valid String name) { - + } } ''') @@ -57,4 +58,62 @@ interface ExchangeRates { expect: definition.findMethod("rate", LocalDate).get().hasStereotype(Validated) } + + + void "test a default method constraints on a declarative client makes it @Validated"() { + given: + def definition = buildBeanDefinition('validateparse3.ExchangeRates' + BeanDefinitionVisitor.PROXY_SUFFIX,''' +package validateparse3; + +import io.micronaut.http.annotation.Get; +import io.micronaut.http.client.annotation.Client; + +import javax.validation.constraints.PastOrPresent; +import java.time.LocalDate; + +@Client("https://exchangeratesapi.io") +interface ExchangeRates { + + @Get("{date}") + String rate(@PastOrPresent LocalDate date); + + default String rate2(@PastOrPresent LocalDate date) { + return null; + } +} +''') + + expect: + definition.findMethod("rate", LocalDate).get().hasStereotype(Validated) + definition.findMethod("rate2", LocalDate).get().hasStereotype(Validated) + } + + @PendingFeature + void "test a default method only constraints on a declarative client makes it @Validated"() { + given: + def definition = buildBeanDefinition('validateparse3.ExchangeRates' + BeanDefinitionVisitor.PROXY_SUFFIX,''' +package validateparse3; + +import io.micronaut.http.annotation.Get; +import io.micronaut.http.client.annotation.Client; + +import javax.validation.constraints.PastOrPresent; +import java.time.LocalDate; + +@Client("https://exchangeratesapi.io") +interface ExchangeRates { + + @Get("{date}") + String rate(LocalDate date); + + default String rate2(@PastOrPresent LocalDate date) { + return rate(date); + } +} +''') + + expect: + !definition.findMethod("rate", LocalDate).get().hasStereotype(Validated) + definition.findMethod("rate2", LocalDate).get().hasStereotype(Validated) + } } diff --git a/validation/src/test/groovy/io/micronaut/validation/ValidationVisitorSpec.groovy b/validation/src/test/groovy/io/micronaut/validation/ValidationVisitorSpec.groovy new file mode 100644 index 00000000000..74e4c1fd648 --- /dev/null +++ b/validation/src/test/groovy/io/micronaut/validation/ValidationVisitorSpec.groovy @@ -0,0 +1,163 @@ +/* + * Copyright 2017-2019 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.validation + +import io.micronaut.annotation.processing.test.AbstractTypeElementSpec +import io.micronaut.inject.BeanDefinition + +class ValidationVisitorSpec extends AbstractTypeElementSpec { + + void "test requires validation beans"() { + given: + BeanDefinition definition = buildBeanDefinition('test.FooService','''\ +package test; + +import io.micronaut.core.annotation.NonNull; +import jakarta.inject.Singleton; +import javax.annotation.Nonnull; +import javax.validation.Valid; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; + +@Singleton +class FooService { + + public Pojo bar(@Nonnull @NotNull @Valid Pojo pojo) { + return foo(pojo); + } + + @NonNull + private Pojo foo(@NonNull Pojo pojo) { + return pojo; + } + +} + +@Valid +class Pojo { + @NotBlank + private final String name; + + public Pojo(String name) { + this.name = name; + } + + public String getName() { + return name; + } +} + +''') + expect: + definition != null + definition.findPossibleMethods("bar").findFirst().get().hasAnnotation(Validated) + } + + void "test fails when @Valid is defined for the parameter on a private method"() { + when: + buildBeanDefinition('test.FooService','''\ +package test; + +import io.micronaut.core.annotation.NonNull; +import jakarta.inject.Singleton; +import javax.annotation.Nonnull; +import javax.validation.Valid; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; + +@Singleton +class FooService { + + public Pojo bar(@Nonnull @NotNull @Valid Pojo pojo) { + return foo(pojo); + } + + @NonNull + private Pojo foo(@NonNull @Valid Pojo pojo) { + return pojo; + } + +} + +@Valid +class Pojo { + @NotBlank + private final String name; + + public Pojo(String name) { + this.name = name; + } + + public String getName() { + return name; + } +} + +''') + then: + Throwable t = thrown() + t.message.contains 'Method annotated for validation but is declared private' + } + + void "test fails when @Valid is defined on a private method"() { + when: + buildBeanDefinition('test.FooService','''\ +package test; + +import io.micronaut.core.annotation.NonNull; +import jakarta.inject.Singleton; +import javax.annotation.Nonnull; +import javax.validation.Valid; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; + +@Singleton +class FooService { + + public Pojo bar(@Nonnull @NotNull @Valid Pojo pojo) { + return foo(pojo); + } + + @Valid + @NonNull + private Pojo foo(@NonNull Pojo pojo) { + return pojo; + } + +} + +@Valid +class Pojo { + @NotBlank + private final String name; + + public Pojo(String name) { + this.name = name; + } + + public String getName() { + return name; + } +} + +''') + then: + Throwable t = thrown() + t.message.contains 'Method annotated for validation but is declared private' + } + +} + From 9b9648a6f49ab08a3513713ebde669bc598ee025 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 17 Oct 2022 15:15:37 +0200 Subject: [PATCH 119/743] fix(deps): update dependency com.fasterxml.jackson.core:jackson-databind to v2.13.4.2 (#8168) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index f472fed5e25..7ac932fcbe5 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -57,7 +57,7 @@ managed-h2 = "2.1.210" managed-hystrix = "1.5.18" managed-jakarta-annotation-api = "2.1.1" managed-jackson = "2.13.4" -managed-jackson-databind = "2.13.4" +managed-jackson-databind = "2.13.4.2" managed-javax-annotation-api = "1.3.2" managed-jcache = "1.1.1" managed-jna = "5.12.1" From 796478e863099b81d9fed789e66f6e20af6dfa70 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 17 Oct 2022 15:16:00 +0200 Subject: [PATCH 120/743] chore(deps): update mikepenz/action-junit-report action to v3.5.2 (#8166) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/graalvm.yml | 2 +- .github/workflows/gradle.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/graalvm.yml b/.github/workflows/graalvm.yml index 8f3f1c04aaf..d0cf09395b7 100644 --- a/.github/workflows/graalvm.yml +++ b/.github/workflows/graalvm.yml @@ -58,7 +58,7 @@ jobs: PREDICTIVE_TEST_SELECTION: "${{ github.event_name == 'pull_request' && 'true' || 'false' }}" - name: Publish Test Report if: always() - uses: mikepenz/action-junit-report@v3.5.1 + uses: mikepenz/action-junit-report@v3.5.2 with: check_name: GraalVM CE CI / Test Report (Java ${{ matrix.java }}) report_paths: '**/build/test-results/test/TEST-*.xml' diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index 51d91606360..61dbb53d758 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -60,7 +60,7 @@ jobs: PREDICTIVE_TEST_SELECTION: "${{ github.event_name == 'pull_request' && 'true' || 'false' }}" - name: Publish Test Report if: always() - uses: mikepenz/action-junit-report@v3.5.1 + uses: mikepenz/action-junit-report@v3.5.2 with: check_name: Java CI / Test Report (${{ matrix.java }}) report_paths: '**/build/test-results/test/TEST-*.xml' From 41d72af61f968ebdd2c551b7fa003388b0c3c3c8 Mon Sep 17 00:00:00 2001 From: micronaut-build <65172877+micronaut-build@users.noreply.github.com> Date: Mon, 17 Oct 2022 09:16:20 -0400 Subject: [PATCH 121/743] build: Bump micronaut-views to 3.7.1 (#8161) --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 34426221054..cdf16097c46 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -122,7 +122,7 @@ managed-micronaut-test-resources = "1.1.2" managed-micronaut-toml = "1.1.1" managed-micronaut-tracing = "4.4.0" managed-micronaut-tracing-legacy = "3.2.7" -managed-micronaut-views = "3.6.0" +managed-micronaut-views = "3.7.1" managed-micronaut-xml = "3.1.0" managed-neo4j = "3.5.35" managed-neo4j-java-driver = "4.4.9" From 52613f579db73453a3f5c743c480fd3b3ea50d20 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 17 Oct 2022 15:16:39 +0200 Subject: [PATCH 122/743] chore(deps): update dependency io.micronaut.build.internal:micronaut-gradle-plugins to v5.3.15 (#8165) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- buildSrc/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/buildSrc/build.gradle b/buildSrc/build.gradle index 07d7bd4312b..d9eac6b8e19 100644 --- a/buildSrc/build.gradle +++ b/buildSrc/build.gradle @@ -9,7 +9,7 @@ repositories { dependencies { implementation "gradle.plugin.com.github.johnrengelman:shadow:7.1.2" implementation "org.aim42:htmlSanityCheck:1.1.6" - implementation "io.micronaut.build.internal:micronaut-gradle-plugins:5.3.14" + implementation "io.micronaut.build.internal:micronaut-gradle-plugins:5.3.15" implementation "org.tomlj:tomlj:1.0.0" implementation "me.champeau.gradle:japicmp-gradle-plugin:0.4.1" } From 46177980cf41bd4a764f87124ebe28b9d5772fd5 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 17 Oct 2022 15:16:39 +0200 Subject: [PATCH 123/743] chore(deps): update dependency io.micronaut.build.internal:micronaut-gradle-plugins to v5.3.15 (#8165) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- buildSrc/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/buildSrc/build.gradle b/buildSrc/build.gradle index ab8cb95e0e1..2481d6d5e77 100644 --- a/buildSrc/build.gradle +++ b/buildSrc/build.gradle @@ -9,6 +9,6 @@ repositories { dependencies { implementation "gradle.plugin.com.github.johnrengelman:shadow:7.1.2" implementation "org.aim42:htmlSanityCheck:1.1.6" - implementation "io.micronaut.build.internal:micronaut-gradle-plugins:5.3.14" + implementation "io.micronaut.build.internal:micronaut-gradle-plugins:5.3.15" implementation "org.tomlj:tomlj:1.0.0" } From 0d8f0fff4b0ed2144a06319db946db75775cbe5e Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 17 Oct 2022 15:16:00 +0200 Subject: [PATCH 124/743] chore(deps): update mikepenz/action-junit-report action to v3.5.2 (#8166) --- .github/workflows/graalvm.yml | 2 +- .github/workflows/gradle.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/graalvm.yml b/.github/workflows/graalvm.yml index 22b17459968..d0cf09395b7 100644 --- a/.github/workflows/graalvm.yml +++ b/.github/workflows/graalvm.yml @@ -58,7 +58,7 @@ jobs: PREDICTIVE_TEST_SELECTION: "${{ github.event_name == 'pull_request' && 'true' || 'false' }}" - name: Publish Test Report if: always() - uses: mikepenz/action-junit-report@v3.5.0 + uses: mikepenz/action-junit-report@v3.5.2 with: check_name: GraalVM CE CI / Test Report (Java ${{ matrix.java }}) report_paths: '**/build/test-results/test/TEST-*.xml' diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index 9ba193f0b0f..61dbb53d758 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -60,7 +60,7 @@ jobs: PREDICTIVE_TEST_SELECTION: "${{ github.event_name == 'pull_request' && 'true' || 'false' }}" - name: Publish Test Report if: always() - uses: mikepenz/action-junit-report@v3.5.0 + uses: mikepenz/action-junit-report@v3.5.2 with: check_name: Java CI / Test Report (${{ matrix.java }}) report_paths: '**/build/test-results/test/TEST-*.xml' From 96433841392be66288b7f522f0884edfff6a864a Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 17 Oct 2022 15:15:37 +0200 Subject: [PATCH 125/743] fix(deps): update dependency com.fasterxml.jackson.core:jackson-databind to v2.13.4.2 (#8168) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 05a630cd6a3..96d4491cd70 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -53,7 +53,7 @@ managed-h2 = "1.4.200" managed-hystrix = "1.5.18" managed-jakarta-annotation-api = "2.1.1" managed-jackson = "2.13.4" -managed-jackson-databind = "2.13.4" +managed-jackson-databind = "2.13.4.2" managed-javax-annotation-api = "1.3.2" managed-jcache = "1.1.1" managed-jna = "5.12.1" From 80d8495b4f852a5b65f868d522b3b69b64390b84 Mon Sep 17 00:00:00 2001 From: Tim Yates Date: Mon, 17 Oct 2022 14:24:06 +0100 Subject: [PATCH 126/743] build: Use v6.0.1 of the internal build tools (#8154) --- .github/workflows/central-sync.yml | 2 +- .github/workflows/corretto.yml | 2 +- .github/workflows/graalvm.yml | 2 +- .github/workflows/gradle.yml | 6 +++--- .github/workflows/publish-snapshot.yml | 2 +- .github/workflows/release.yml | 2 +- .github/workflows/sonarqube.yml | 2 +- .../io.micronaut.build.internal.convention-base.gradle | 7 ++----- .../annotation/processing/test/JavaParser.java | 10 ---------- settings.gradle | 2 +- 10 files changed, 12 insertions(+), 25 deletions(-) diff --git a/.github/workflows/central-sync.yml b/.github/workflows/central-sync.yml index 3d550abb3ce..add5cc43de0 100644 --- a/.github/workflows/central-sync.yml +++ b/.github/workflows/central-sync.yml @@ -23,7 +23,7 @@ jobs: uses: actions/setup-java@v3 with: distribution: 'adopt' - java-version: '11' + java-version: '17' - name: Publish to Sonatype OSSRH env: SONATYPE_USERNAME: ${{ secrets.SONATYPE_USERNAME }} diff --git a/.github/workflows/corretto.yml b/.github/workflows/corretto.yml index d1363557345..b28e0a751b5 100644 --- a/.github/workflows/corretto.yml +++ b/.github/workflows/corretto.yml @@ -8,7 +8,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - java: ['8', '11'] + java: ['17'] container: amazoncorretto:${{ matrix.java }} steps: - name: Display Java and Linux version diff --git a/.github/workflows/graalvm.yml b/.github/workflows/graalvm.yml index d0cf09395b7..23d722775de 100644 --- a/.github/workflows/graalvm.yml +++ b/.github/workflows/graalvm.yml @@ -19,7 +19,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - java: ['11', '17'] + java: ['17'] graalvm: ['latest', 'dev'] steps: # https://github.com/actions/virtual-environments/issues/709 diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index 61dbb53d758..6f29dbb7281 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -19,7 +19,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - java: ['8', '11', '17'] + java: ['17'] steps: # https://github.com/actions/virtual-environments/issues/709 - name: Free disk space @@ -72,7 +72,7 @@ jobs: name: binary-compatibility-reports path: "**/build/reports/binary-compatibility-*.html" - name: Publish to Sonatype Snapshots - if: success() && github.event_name == 'push' && matrix.java == '11' + if: success() && github.event_name == 'push' && matrix.java == '17' env: SONATYPE_USERNAME: ${{ secrets.SONATYPE_USERNAME }} SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }} @@ -88,7 +88,7 @@ jobs: if_true: "micronaut-projects/micronaut-docs" if_false: ${{ github.repository }} - name: Publish to Github Pages - if: success() && github.event_name == 'push' && matrix.java == '11' + if: success() && github.event_name == 'push' && matrix.java == '17' uses: micronaut-projects/github-pages-deploy-action@master env: TARGET_REPOSITORY: ${{ steps.docs_target.outputs.value }} diff --git a/.github/workflows/publish-snapshot.yml b/.github/workflows/publish-snapshot.yml index 8b70c7e48fe..5c8bd2933a2 100644 --- a/.github/workflows/publish-snapshot.yml +++ b/.github/workflows/publish-snapshot.yml @@ -21,7 +21,7 @@ jobs: uses: actions/setup-java@v3 with: distribution: 'adopt' - java-version: '11' + java-version: '17' - name: Publish to Sonatype Snapshots if: success() env: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 13dcbbcada9..8d933603484 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -20,7 +20,7 @@ jobs: uses: actions/setup-java@v3 with: distribution: 'adopt' - java-version: '11' + java-version: '17' - name: Set the current release version id: release_version run: echo ::set-output name=release_version::${GITHUB_REF:11} diff --git a/.github/workflows/sonarqube.yml b/.github/workflows/sonarqube.yml index 93ae8a9498c..4ed20609f83 100644 --- a/.github/workflows/sonarqube.yml +++ b/.github/workflows/sonarqube.yml @@ -38,7 +38,7 @@ jobs: uses: actions/setup-java@v3 with: distribution: 'adopt' - java-version: 11 + java-version: 17 - name: Optional setup step env: GRADLE_ENTERPRISE_ACCESS_KEY: ${{ secrets.GRADLE_ENTERPRISE_ACCESS_KEY }} diff --git a/buildSrc/src/main/groovy/io.micronaut.build.internal.convention-base.gradle b/buildSrc/src/main/groovy/io.micronaut.build.internal.convention-base.gradle index 76fde10763c..57170d7f9fe 100644 --- a/buildSrc/src/main/groovy/io.micronaut.build.internal.convention-base.gradle +++ b/buildSrc/src/main/groovy/io.micronaut.build.internal.convention-base.gradle @@ -47,11 +47,8 @@ tasks.withType(Jar).configureEach { } configurations.all { - resolutionStrategy.eachDependency { DependencyResolveDetails details -> - if (details.requested.group == 'org.codehaus.groovy') { - details.useTarget("org.apache.groovy:${details.requested.name}:${details.requested.version}") - details.because "Plugin 'io.micronaut.build.internal.common' isn't Groovy 4 yet and it's pulling in old versions" - } + resolutionStrategy.dependencySubstitution { + substitute module("io.micronaut:micronaut-inject-groovy") using project(":inject-groovy") because "we want to test with what we're building" } } diff --git a/inject-java-test/src/main/java/io/micronaut/annotation/processing/test/JavaParser.java b/inject-java-test/src/main/java/io/micronaut/annotation/processing/test/JavaParser.java index 9fea4bb940f..9d8f398d13e 100644 --- a/inject-java-test/src/main/java/io/micronaut/annotation/processing/test/JavaParser.java +++ b/inject-java-test/src/main/java/io/micronaut/annotation/processing/test/JavaParser.java @@ -336,16 +336,6 @@ private Set getCompilerOptions() { @Override public void close() { - if (compiler != null) { - try { - // avoid illegal access - if (!Jvm.getCurrent().isJava15Compatible()) { - ((com.sun.tools.javac.main.JavaCompiler) compiler).close(); - } - } catch (Exception e) { - // ignore - } - } if (fileManager != null) { try { fileManager.close(); diff --git a/settings.gradle b/settings.gradle index 277e478b387..4d03f80210b 100644 --- a/settings.gradle +++ b/settings.gradle @@ -14,7 +14,7 @@ pluginManagement { } plugins { - id 'io.micronaut.build.shared.settings' version '5.3.14' + id 'io.micronaut.build.shared.settings' version '6.0.1' } rootProject.name = 'micronaut' From c837a6c114abb4fa953a6f76894e84ba7164a8a4 Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Mon, 17 Oct 2022 15:33:52 +0200 Subject: [PATCH 127/743] build: update data, test and views dependencies - Build Micronaut data to 3.8.1 - Build Micronaut Test to 3.7.0 - Build Micronaut Views to 3.7.1 --- gradle/libs.versions.toml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 7ac932fcbe5..581d06cbc02 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -77,7 +77,7 @@ managed-micronaut-cache = "3.5.0" managed-micronaut-cassandra = "5.1.1" managed-micronaut-coherence = "3.7.1" managed-micronaut-crac = "1.0.1" -managed-micronaut-data = "3.8.0" +managed-micronaut-data = "3.8.1" managed-micronaut-discovery = "3.2.0" managed-micronaut-elasticsearch = "4.3.0" managed-micronaut-email = "1.4.0" @@ -121,12 +121,12 @@ managed-micronaut-serialization = "1.3.2" managed-micronaut-servlet = "3.3.1" managed-micronaut-spring = "4.3.1" managed-micronaut-sql = "4.7.2" -managed-micronaut-test = "3.6.2" +managed-micronaut-test = "3.7.0" managed-micronaut-test-resources = "1.1.2" managed-micronaut-toml = "1.1.1" managed-micronaut-tracing = "4.4.0" managed-micronaut-tracing-legacy = "3.2.7" -managed-micronaut-views = "3.6.0" +managed-micronaut-views = "3.7.1" managed-micronaut-xml = "3.1.0" managed-neo4j = "3.5.35" managed-neo4j-java-driver = "4.4.9" From fdb25db7bd9913b1ac73a107bd205290c6100422 Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Tue, 18 Oct 2022 07:25:18 +0200 Subject: [PATCH 128/743] fix: PropertySourcePropertyResolver.getAllProperties behaviour when StringConvention.RAW (#8178) * fixes bug with PropertySourcePropertyResolver.getAllProperties and extents unit test * remove code smells Co-authored-by: Pavel Hurynovich <8087963+hurynovich@users.noreply.github.com> --- .../context/env/PropertySourcePropertyResolver.java | 3 +-- .../env/PropertySourcePropertyResolverSpec.groovy | 9 ++++++++- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/inject/src/main/java/io/micronaut/context/env/PropertySourcePropertyResolver.java b/inject/src/main/java/io/micronaut/context/env/PropertySourcePropertyResolver.java index 88484b71951..b36fe35424e 100644 --- a/inject/src/main/java/io/micronaut/context/env/PropertySourcePropertyResolver.java +++ b/inject/src/main/java/io/micronaut/context/env/PropertySourcePropertyResolver.java @@ -385,9 +385,8 @@ private String cacheKey(@NonNull String name, Class requiredType) { public Map getAllProperties(StringConvention keyConvention, MapFormat.MapTransformation transformation) { Map map = new HashMap<>(); boolean isNested = transformation == MapFormat.MapTransformation.NESTED; - Arrays - .stream(catalog) + .stream(getCatalog(keyConvention == StringConvention.RAW ? PropertyCatalog.RAW : PropertyCatalog.GENERATED)) .filter(Objects::nonNull) .map(Map::entrySet) .flatMap(Collection::stream) diff --git a/inject/src/test/groovy/io/micronaut/context/env/PropertySourcePropertyResolverSpec.groovy b/inject/src/test/groovy/io/micronaut/context/env/PropertySourcePropertyResolverSpec.groovy index 00c619437ab..08cdbe8d8d5 100644 --- a/inject/src/test/groovy/io/micronaut/context/env/PropertySourcePropertyResolverSpec.groovy +++ b/inject/src/test/groovy/io/micronaut/context/env/PropertySourcePropertyResolverSpec.groovy @@ -530,7 +530,8 @@ class PropertySourcePropertyResolverSpec extends Specification { 'my.property.one' : 'one', 'my.property.two' : '${foo.bar}', 'my.property.three': 'three', - 'test-key.convention-test': 'key' + 'test-key.convention-test': 'key', + 'FranKen_Ste-in.property' : 'Victor' ] PropertySourcePropertyResolver resolver = new PropertySourcePropertyResolver( PropertySource.of("test", values)) @@ -542,6 +543,12 @@ class PropertySourcePropertyResolverSpec extends Specification { resolver.getAllProperties(StringConvention.RAW, MapFormat.MapTransformation.NESTED).get('my').get('property').get('two') == 'two' resolver.getAllProperties(StringConvention.CAMEL_CASE, MapFormat.MapTransformation.FLAT).get('testKey.conventionTest') == 'key' resolver.getAllProperties(StringConvention.CAMEL_CASE, MapFormat.MapTransformation.NESTED).get('testKey').get('conventionTest') == 'key' + resolver.getAllProperties(StringConvention.CAMEL_CASE_CAPITALIZED, MapFormat.MapTransformation.FLAT).get('FranKenSteIn.property') == 'Victor' + resolver.getAllProperties(StringConvention.CAMEL_CASE, MapFormat.MapTransformation.FLAT).get('franKenSteIn.property') == 'Victor' + resolver.getAllProperties(StringConvention.HYPHENATED, MapFormat.MapTransformation.FLAT).get('fran-ken-ste-in.property') == 'Victor' + resolver.getAllProperties(StringConvention.RAW, MapFormat.MapTransformation.FLAT).get('FranKen_Ste-in.property') == 'Victor' + resolver.getAllProperties(StringConvention.UNDER_SCORE_SEPARATED, MapFormat.MapTransformation.FLAT).get('FRAN_KEN_STE_IN_PROPERTY') == 'Victor' + resolver.getAllProperties(StringConvention.UNDER_SCORE_SEPARATED_LOWER_CASE, MapFormat.MapTransformation.FLAT).get('fran_ken_ste_in.property') == 'Victor' } void "test inner properties"() { From 656702d915c0a45600071e81f0ed26840ada60dd Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Wed, 12 Oct 2022 17:50:57 +0200 Subject: [PATCH 129/743] fix(deps): update dependency io.micronaut.graphql:micronaut-graphql to v3.2.0 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index dacadac7a3d..60f09e48eab 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -79,7 +79,7 @@ managed-micronaut-elasticsearch = "4.3.0" managed-micronaut-email = "1.4.0" managed-micronaut-flyway = "5.4.1" managed-micronaut-gcp = "4.6.0" -managed-micronaut-graphql = "3.1.0" +managed-micronaut-graphql = "3.2.0" managed-micronaut-groovy = "3.3.1" managed-micronaut-grpc = "3.3.1" managed-micronaut-hibernate-validator = "3.2.0" From bc7dc2c6b176ca014d4171cc8acd3b09d7817b18 Mon Sep 17 00:00:00 2001 From: micronaut-build Date: Tue, 18 Oct 2022 09:21:27 +0000 Subject: [PATCH 130/743] [skip ci] Release v3.7.2 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 35cdf627e8a..002143d95f9 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -projectVersion=3.7.2-SNAPSHOT +projectVersion=3.7.2 projectGroupId=io.micronaut title=Micronaut projectDesc=Natively Cloud Native From b9ef51f62f9174c680e969b15cf51f843dd77142 Mon Sep 17 00:00:00 2001 From: micronaut-build Date: Tue, 18 Oct 2022 09:32:17 +0000 Subject: [PATCH 131/743] Back to 3.7.3-SNAPSHOT --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 002143d95f9..232865ccc6e 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -projectVersion=3.7.2 +projectVersion=3.7.3-SNAPSHOT projectGroupId=io.micronaut title=Micronaut projectDesc=Natively Cloud Native From 7980db262f5d1985ad9ececf2893d57cac9cbdf4 Mon Sep 17 00:00:00 2001 From: Tim Yates Date: Tue, 18 Oct 2022 15:04:42 +0100 Subject: [PATCH 132/743] Configuration props for inner classes are now dollar prefixed (#8183) Now we publish on Java 17, this output filename has changed from: ``` file.OuterClass.Inner.adoc ``` to ``` file.OuterClass$Inner.adoc ``` This fixes the `validateAssembleDocs` task that occurs prior to publishing --- src/main/docs/guide/httpServer/hostResolution.adoc | 2 +- src/main/docs/guide/httpServer/localeResolution.adoc | 2 +- .../docs/guide/httpServer/serverConfiguration/threadPools.adoc | 2 +- src/main/docs/guide/httpServer/transfers.adoc | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/docs/guide/httpServer/hostResolution.adoc b/src/main/docs/guide/httpServer/hostResolution.adoc index 1e6aa3c93e6..71d7458a555 100644 --- a/src/main/docs/guide/httpServer/hostResolution.adoc +++ b/src/main/docs/guide/httpServer/hostResolution.adoc @@ -11,6 +11,6 @@ The default implementation looks for host information in the following places in The behavior of which headers to pull the relevant data can be changed with the following configuration: -include::{includedir}configurationProperties/io.micronaut.http.server.HttpServerConfiguration.HostResolutionConfiguration.adoc[] +include::{includedir}configurationProperties/io.micronaut.http.server.HttpServerConfiguration$HostResolutionConfiguration.adoc[] The above configuration also supports an allowed host list. Configuring this list ensures any resolved host matches one of the supplied regular expression patterns. That is useful to prevent host cache poisoning attacks and is recommended to be configured. diff --git a/src/main/docs/guide/httpServer/localeResolution.adoc b/src/main/docs/guide/httpServer/localeResolution.adoc index 9ae6ceadc2d..ae8b9682d12 100644 --- a/src/main/docs/guide/httpServer/localeResolution.adoc +++ b/src/main/docs/guide/httpServer/localeResolution.adoc @@ -4,7 +4,7 @@ The api:core.util.LocaleResolver[] API does not need to be used directly. Simply There are several configuration options to control how to resolve the locale: -include::{includedir}configurationProperties/io.micronaut.http.server.HttpServerConfiguration.HttpLocaleResolutionConfigurationProperties.adoc[] +include::{includedir}configurationProperties/io.micronaut.http.server.HttpServerConfiguration$HttpLocaleResolutionConfigurationProperties.adoc[] Locales can be configured in the "en_GB" format, or in the BCP 47 (Language tag) format. If multiple methods are configured, the fixed locale takes precedence, followed by session/cookie, then header. diff --git a/src/main/docs/guide/httpServer/serverConfiguration/threadPools.adoc b/src/main/docs/guide/httpServer/serverConfiguration/threadPools.adoc index fb9aaee4b0c..09a7e43c057 100644 --- a/src/main/docs/guide/httpServer/serverConfiguration/threadPools.adoc +++ b/src/main/docs/guide/httpServer/serverConfiguration/threadPools.adoc @@ -4,7 +4,7 @@ The Netty worker event loop uses the "default" named event loop group. This can IMPORTANT: The event loop configuration under `micronaut.server.netty.worker` is only used if the `event-loop-group` is set to a name which doesn't correspond to any `micronaut.netty.event-loops` configuration. This behavior is deprecated and will be removed in a future version. Use `micronaut.netty.event-loops.*` for any event loop group configuration beyond setting the name through `event-loop-group`. This does not apply to the parent event loop configuration (`micronaut.server.netty.parent`). -include::{includedir}configurationProperties/io.micronaut.http.server.netty.configuration.NettyHttpServerConfiguration.Worker.adoc[] +include::{includedir}configurationProperties/io.micronaut.http.server.netty.configuration.NettyHttpServerConfiguration$Worker.adoc[] TIP: The parent event loop can be configured with `micronaut.server.netty.parent` with the same configuration options. diff --git a/src/main/docs/guide/httpServer/transfers.adoc b/src/main/docs/guide/httpServer/transfers.adoc index 6a57dd1e630..fbd3808bde6 100644 --- a/src/main/docs/guide/httpServer/transfers.adoc +++ b/src/main/docs/guide/httpServer/transfers.adoc @@ -44,4 +44,4 @@ By default, file responses include caching headers. The following options determ include::{includedir}configurationProperties/io.micronaut.http.server.netty.types.files.FileTypeHandlerConfiguration.adoc[] -include::{includedir}configurationProperties/io.micronaut.http.server.netty.types.files.FileTypeHandlerConfiguration.CacheControlConfiguration.adoc[] +include::{includedir}configurationProperties/io.micronaut.http.server.netty.types.files.FileTypeHandlerConfiguration$CacheControlConfiguration.adoc[] From 537148d55fd249d02fdaf80c7ad4c5a67f0393b3 Mon Sep 17 00:00:00 2001 From: Tim Yates Date: Wed, 19 Oct 2022 09:36:36 +0100 Subject: [PATCH 133/743] Remove JacksonConfig construtor from HateoasErrorResponseProcessor (#8186) * Remove JacksonConfig construtor from HateoasErrorResponseProcessor Removing it will fix using Groovy with Serde and the default ErrorResponseProcessor fixes #8184 * Fix JAPIcmp --- config/accepted-api-changes.json | 5 +++++ .../response/HateoasErrorResponseProcessor.java | 11 ----------- 2 files changed, 5 insertions(+), 11 deletions(-) diff --git a/config/accepted-api-changes.json b/config/accepted-api-changes.json index b16d69f0b93..95d86c6ad51 100644 --- a/config/accepted-api-changes.json +++ b/config/accepted-api-changes.json @@ -338,5 +338,10 @@ "type": "io.micronaut.core.io.service.StreamSoftServiceLoader", "member": "Constructor io.micronaut.core.io.service.StreamSoftServiceLoader()", "reason": "Deprecated and removed for Micronaut 4. Use io.micronaut.core.io.service.SoftServiceLoader#collectAll(java.util.Collection)." + }, + { + "type": "io.micronaut.http.server.exceptions.response.HateoasErrorResponseProcessor", + "member": "Constructor io.micronaut.http.server.exceptions.response.HateoasErrorResponseProcessor(io.micronaut.jackson.JacksonConfiguration)", + "reason": "Removed as was deprecated in 3.8.x" } ] diff --git a/http-server/src/main/java/io/micronaut/http/server/exceptions/response/HateoasErrorResponseProcessor.java b/http-server/src/main/java/io/micronaut/http/server/exceptions/response/HateoasErrorResponseProcessor.java index 64c28272121..de7a8e2b893 100644 --- a/http-server/src/main/java/io/micronaut/http/server/exceptions/response/HateoasErrorResponseProcessor.java +++ b/http-server/src/main/java/io/micronaut/http/server/exceptions/response/HateoasErrorResponseProcessor.java @@ -23,7 +23,6 @@ import io.micronaut.http.hateoas.JsonError; import io.micronaut.http.hateoas.Link; import io.micronaut.http.hateoas.Resource; -import io.micronaut.jackson.JacksonConfiguration; import io.micronaut.json.JsonConfiguration; import jakarta.inject.Singleton; @@ -46,16 +45,6 @@ public HateoasErrorResponseProcessor(JsonConfiguration jacksonConfiguration) { this.alwaysSerializeErrorsAsList = jacksonConfiguration.isAlwaysSerializeErrorsAsList(); } - /** - * Constructor for binary compatibility. Equivalent to - * {@link HateoasErrorResponseProcessor#HateoasErrorResponseProcessor(JsonConfiguration)} - * - * @param jacksonConfiguration the configuration to use for processing. - */ - public HateoasErrorResponseProcessor(JacksonConfiguration jacksonConfiguration) { - this((JsonConfiguration) jacksonConfiguration); - } - @Override @NonNull public MutableHttpResponse processResponse(@NonNull ErrorContext errorContext, @NonNull MutableHttpResponse response) { From b0cc35f1d1087176f68e97e057aaf7f192fb13eb Mon Sep 17 00:00:00 2001 From: Tim Yates Date: Wed, 19 Oct 2022 09:36:53 +0100 Subject: [PATCH 134/743] Deprecate JacksonConfig construtor for removal in 4.0.0 (#8185) Removing it will fix using Groovy with Serde and the default ErrorResponseProcessor --- .../exceptions/response/HateoasErrorResponseProcessor.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/http-server/src/main/java/io/micronaut/http/server/exceptions/response/HateoasErrorResponseProcessor.java b/http-server/src/main/java/io/micronaut/http/server/exceptions/response/HateoasErrorResponseProcessor.java index 756c4106b4c..a7a437c6a52 100644 --- a/http-server/src/main/java/io/micronaut/http/server/exceptions/response/HateoasErrorResponseProcessor.java +++ b/http-server/src/main/java/io/micronaut/http/server/exceptions/response/HateoasErrorResponseProcessor.java @@ -51,7 +51,9 @@ public HateoasErrorResponseProcessor(JsonConfiguration jacksonConfiguration) { * {@link HateoasErrorResponseProcessor#HateoasErrorResponseProcessor(JsonConfiguration)} * * @param jacksonConfiguration the configuration to use for processing. + * @deprecated Use {@link HateoasErrorResponseProcessor(JsonConfiguration)} */ + @Deprecated public HateoasErrorResponseProcessor(JacksonConfiguration jacksonConfiguration) { this((JsonConfiguration) jacksonConfiguration); } From 50ae5bc7038cbcdf02a4c91b4ef0e5985af2ef08 Mon Sep 17 00:00:00 2001 From: Denis Stepanov Date: Wed, 19 Oct 2022 10:37:20 +0200 Subject: [PATCH 135/743] Fix NPE in Groovy processing (#8182) * Fix NPE in Groovy processing * Update AstMessageUtils.groovy --- .../io/micronaut/ast/groovy/utils/AstMessageUtils.groovy | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/utils/AstMessageUtils.groovy b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/utils/AstMessageUtils.groovy index b67e33cd509..bad432d60af 100644 --- a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/utils/AstMessageUtils.groovy +++ b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/utils/AstMessageUtils.groovy @@ -16,6 +16,7 @@ package io.micronaut.ast.groovy.utils import groovy.transform.CompileStatic +import io.micronaut.core.annotation.Nullable import org.codehaus.groovy.ast.ASTNode import org.codehaus.groovy.control.Janitor import org.codehaus.groovy.control.SourceUnit @@ -39,8 +40,8 @@ class AstMessageUtils { * @param node The AST node * @param message The message */ - static void warning(final SourceUnit sourceUnit, final ASTNode node, final String message) { - final String sample = sourceUnit.getSample(node.getLineNumber(), node.getColumnNumber(), new Janitor()) + static void warning(SourceUnit sourceUnit, @Nullable ASTNode node, String message) { + final String sample = node ? sourceUnit.getSample(node.getLineNumber(), node.getColumnNumber(), new Janitor()) : null if (sample) { System.err.println("""WARNING: $message @@ -50,7 +51,7 @@ $sample""") } } - static void error(SourceUnit sourceUnit, ASTNode expr, String errorMessage) { + static void error(SourceUnit sourceUnit, @Nullable ASTNode expr, String errorMessage) { if (expr == null) { error(sourceUnit, errorMessage) } else { From 008e96b1085b7c18adf456c7765f2ba0f45b8355 Mon Sep 17 00:00:00 2001 From: Tim Yates Date: Wed, 19 Oct 2022 11:10:26 +0100 Subject: [PATCH 136/743] fix: Hateoas/Groovy/Serde error in a binary compatible way (#8189) Fixes #8184 When trying to use Groovy and Serde and the default ErrorProcessor, Groovy throws a ClassNotFound exception. This fix adds a replacement for the HateoasErrorResponseProcessor that (if groovy-runtime is on the classpath) doesn't use JacksonConfiguration. It also deprecates the JacksonConfiguration constructor for removal in 4.0.0 --- gradle/libs.versions.toml | 1 + http-server/build.gradle | 1 + .../HateoasErrorResponseProcessor.java | 7 +++ ...eoasErrorResponseProcessorReplacement.java | 50 +++++++++++++++++++ test-suite-groovy/build.gradle | 1 + 5 files changed, 60 insertions(+) create mode 100644 http-server/src/main/java/io/micronaut/http/server/exceptions/response/HateoasErrorResponseProcessorReplacement.java diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 96d4491cd70..de0095d1313 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -413,6 +413,7 @@ log4j = { module = "org.apache.logging.log4j:log4j-core", version.ref = "log4j" logbook-netty = { module = "org.zalando:logbook-netty", version.ref = "logbook-netty" } micronaut-docs = { module = "io.micronaut.docs:micronaut-docs-asciidoc-config-props", version.ref = "micronaut-docs" } +micronaut-runtime-groovy = { module = "io.micronaut.groovy:micronaut-runtime-groovy", version.ref = "managed-micronaut-groovy" } mysql-driver = { module = "mysql:mysql-connector-java" } diff --git a/http-server/build.gradle b/http-server/build.gradle index e3e9eb0371a..26e9b8486aa 100644 --- a/http-server/build.gradle +++ b/http-server/build.gradle @@ -8,6 +8,7 @@ dependencies { api project(":router") compileOnly libs.kotlinx.coroutines.core compileOnly libs.kotlinx.coroutines.reactor + compileOnly(libs.micronaut.runtime.groovy) implementation libs.managed.reactor annotationProcessor project(":inject-java") diff --git a/http-server/src/main/java/io/micronaut/http/server/exceptions/response/HateoasErrorResponseProcessor.java b/http-server/src/main/java/io/micronaut/http/server/exceptions/response/HateoasErrorResponseProcessor.java index 756c4106b4c..f5164aca9f5 100644 --- a/http-server/src/main/java/io/micronaut/http/server/exceptions/response/HateoasErrorResponseProcessor.java +++ b/http-server/src/main/java/io/micronaut/http/server/exceptions/response/HateoasErrorResponseProcessor.java @@ -50,8 +50,10 @@ public HateoasErrorResponseProcessor(JsonConfiguration jacksonConfiguration) { * Constructor for binary compatibility. Equivalent to * {@link HateoasErrorResponseProcessor#HateoasErrorResponseProcessor(JsonConfiguration)} * + * @deprecated Use {@link HateoasErrorResponseProcessor#HateoasErrorResponseProcessor(JsonConfiguration)} instead. * @param jacksonConfiguration the configuration to use for processing. */ + @Deprecated public HateoasErrorResponseProcessor(JacksonConfiguration jacksonConfiguration) { this((JsonConfiguration) jacksonConfiguration); } @@ -59,6 +61,11 @@ public HateoasErrorResponseProcessor(JacksonConfiguration jacksonConfiguration) @Override @NonNull public MutableHttpResponse processResponse(@NonNull ErrorContext errorContext, @NonNull MutableHttpResponse response) { + return getJsonErrorMutableHttpResponse(alwaysSerializeErrorsAsList, errorContext, response); + } + + @NonNull + static MutableHttpResponse getJsonErrorMutableHttpResponse(boolean alwaysSerializeErrorsAsList, ErrorContext errorContext, MutableHttpResponse response) { if (errorContext.getRequest().getMethod() == HttpMethod.HEAD) { return (MutableHttpResponse) response; } diff --git a/http-server/src/main/java/io/micronaut/http/server/exceptions/response/HateoasErrorResponseProcessorReplacement.java b/http-server/src/main/java/io/micronaut/http/server/exceptions/response/HateoasErrorResponseProcessorReplacement.java new file mode 100644 index 00000000000..c0e793c44bd --- /dev/null +++ b/http-server/src/main/java/io/micronaut/http/server/exceptions/response/HateoasErrorResponseProcessorReplacement.java @@ -0,0 +1,50 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.http.server.exceptions.response; + +import io.micronaut.context.annotation.Replaces; +import io.micronaut.context.annotation.Requires; +import io.micronaut.context.env.groovy.GroovyPropertySourceLoader; +import io.micronaut.core.annotation.NonNull; +import io.micronaut.http.MutableHttpResponse; +import io.micronaut.http.hateoas.JsonError; +import io.micronaut.json.JsonConfiguration; +import jakarta.inject.Singleton; + +/** + * @deprecated Replacement is no longer necessary for Micronaut Framework 4.0 since {@link io.micronaut.http.server.exceptions.response.HateoasErrorResponseProcessor} jackson constructor will be removed. + * @author Tim Yates + * @since 3.7.3 + */ +@Deprecated +@Singleton +@Requires(classes = GroovyPropertySourceLoader.class) +@Replaces(HateoasErrorResponseProcessor.class) +public class HateoasErrorResponseProcessorReplacement implements ErrorResponseProcessor { + + private final boolean alwaysSerializeErrorsAsList; + + public HateoasErrorResponseProcessorReplacement(JsonConfiguration jacksonConfiguration) { + this.alwaysSerializeErrorsAsList = jacksonConfiguration.isAlwaysSerializeErrorsAsList(); + } + + @Override + @NonNull + public MutableHttpResponse processResponse(@NonNull ErrorContext errorContext, + @NonNull MutableHttpResponse response) { + return HateoasErrorResponseProcessor.getJsonErrorMutableHttpResponse(alwaysSerializeErrorsAsList, errorContext, response); + } +} diff --git a/test-suite-groovy/build.gradle b/test-suite-groovy/build.gradle index 9aab800b056..19a331651c9 100644 --- a/test-suite-groovy/build.gradle +++ b/test-suite-groovy/build.gradle @@ -28,6 +28,7 @@ dependencies { testImplementation project(":function-web") testRuntimeOnly(platform(libs.boms.micronaut.aws)) testRuntimeOnly libs.aws.java.sdk.lambda + testRuntimeOnly(libs.micronaut.runtime.groovy) testImplementation libs.managed.reactor } From 0f3420933debfde31a6cf64d781ee7469d318415 Mon Sep 17 00:00:00 2001 From: Jonas Konrad Date: Wed, 19 Oct 2022 18:01:29 +0200 Subject: [PATCH 137/743] New HTTP client connection pooling (#8100) This PR adds a new connection pooling infrastructure to the HTTP client. The biggest changes are in ConnectionManager, which contains network setup code, and in the new PoolResizer class, which handles pool dynamics (it calls into ConnectionManager.Pool to assign requests, establish new connections, etc, but does no network setup by itself). Changes in no particular order: - The `http-version` config setting has been replaced by two settings. `plaintext-mode` applies to `http` URLs and can be set to either `HTTP_1` or `H2C`. `alpn-modes` applies to `https` URLs, and determines the protocols that should be supported in protocol negotiation (it's a list, with the supported values `h2` and `http/1.1`). The old http-version setting remains for compatibility, and should remain for 4.0. It is also used in some test cases. - connection pooling now applies to all connections (except websockets), not just exchange calls, and it is enabled by default. - HTTP2 connections now use a channel-per-stream model (like #6842), and allow concurrent requests on one connection. - Connection pool configuration is more fine-grained, with separate settings for HTTP1 and HTTP2. - Pooled connections are now tracked using netty's resource leak detection, to avoid "dangling" connections in the pool (note: resource leak detection is still only tested widely for the tests in the http-server-netty module atm). - One minor fix in the http server that came up in the test suite because connection pooling is on by default now. There are still some TODOs in this PR. Some relate to pending netty improvements (https://github.com/netty/netty/issues/12827 , https://github.com/netty/netty/pull/12830), but work fine for the moment. Some are future improvements that could be made, but that I want to do in separate PRs. --- .../http/client/HttpClientConfiguration.java | 189 +- .../http/client/HttpClientRegistry.java | 18 +- .../http/client/HttpVersionSelection.java | 197 ++ .../http/client/ServiceHttpClientFactory.java | 2 +- .../http/client/annotation/Client.java | 32 + .../client/netty/CancellableMonoSink.java | 140 ++ .../http/client/netty/ConnectTTLHandler.java | 88 - .../http/client/netty/ConnectionManager.java | 1682 ++++++++--------- .../http/client/netty/DefaultHttpClient.java | 351 ++-- .../netty/DefaultNettyHttpClientRegistry.java | 36 +- .../netty/HttpLineBasedFrameDecoder.java | 103 + .../http/client/netty/IdleTimeoutHandler.java | 50 - ...ava => InitialConnectionErrorHandler.java} | 37 +- .../netty/MutableHttpRequestWrapper.java | 131 ++ .../client/netty/NettyClientCustomizer.java | 6 + .../http/client/netty/PoolResizer.java | 282 +++ .../netty/ssl/NettyClientSslBuilder.java | 34 +- .../NettyWebSocketClientHandler.java | 9 +- .../http/client/ConnectTTLHandlerSpec.groovy | 41 - .../http/client/ConnectionTTLSpec.groovy | 27 +- .../http/client/IdleTimeoutSpec.groovy | 32 +- .../http/client/ReadTimeoutSpec.groovy | 3 +- .../http/client/SslRefreshSpec.groovy | 5 +- .../DefaultHttpClientConfigurationSpec.groovy | 1 - .../client/netty/ConnectionManagerSpec.groovy | 1226 ++++++++++++ .../DefaultNettyHttpClientRegistrySpec.groovy | 29 +- .../http/client/netty/DummyChannelId.groovy | 26 + .../http/client/netty/EmbeddedTestUtil.groovy | 108 ++ .../channel/ChannelPipelineCustomizer.java | 6 + .../stream/HttpStreamsClientHandler.java | 3 +- .../stream/HttpStreamsServerHandler.java | 20 +- .../server/netty/RoutingInBoundHandler.java | 2 +- .../NettyHttpServerConfiguration.java | 2 +- .../netty/binding/HttpResponseSpec.groovy | 71 +- .../netty/binding/NettyHttpServerSpec.groovy | 8 +- .../NettyHttpServerConfigurationSpec.groovy | 8 +- .../websocket/BinaryWebSocketSpec.groovy | 67 +- .../clientConfiguration.adoc | 20 +- .../netty/LogbookNettyClientCustomizer.groovy | 10 +- .../netty/LogbookNettyClientCustomizer.kt | 6 +- .../http/client/http2/Http2RequestSpec.groovy | 2 + .../http2/Http2AccessLoggerSpec.groovy | 12 +- .../netty/LogbookNettyClientCustomizer.java | 6 +- 43 files changed, 3694 insertions(+), 1434 deletions(-) create mode 100644 http-client-core/src/main/java/io/micronaut/http/client/HttpVersionSelection.java create mode 100644 http-client/src/main/java/io/micronaut/http/client/netty/CancellableMonoSink.java delete mode 100644 http-client/src/main/java/io/micronaut/http/client/netty/ConnectTTLHandler.java create mode 100644 http-client/src/main/java/io/micronaut/http/client/netty/HttpLineBasedFrameDecoder.java delete mode 100644 http-client/src/main/java/io/micronaut/http/client/netty/IdleTimeoutHandler.java rename http-client/src/main/java/io/micronaut/http/client/netty/{IdlingConnectionHandler.java => InitialConnectionErrorHandler.java} (52%) create mode 100644 http-client/src/main/java/io/micronaut/http/client/netty/MutableHttpRequestWrapper.java create mode 100644 http-client/src/main/java/io/micronaut/http/client/netty/PoolResizer.java delete mode 100644 http-client/src/test/groovy/io/micronaut/http/client/ConnectTTLHandlerSpec.groovy create mode 100644 http-client/src/test/groovy/io/micronaut/http/client/netty/ConnectionManagerSpec.groovy create mode 100644 http-client/src/test/groovy/io/micronaut/http/client/netty/DummyChannelId.groovy create mode 100644 http-client/src/test/groovy/io/micronaut/http/client/netty/EmbeddedTestUtil.groovy diff --git a/http-client-core/src/main/java/io/micronaut/http/client/HttpClientConfiguration.java b/http-client-core/src/main/java/io/micronaut/http/client/HttpClientConfiguration.java index 16119133eae..29a284cc561 100644 --- a/http-client-core/src/main/java/io/micronaut/http/client/HttpClientConfiguration.java +++ b/http-client-core/src/main/java/io/micronaut/http/client/HttpClientConfiguration.java @@ -36,8 +36,11 @@ import java.nio.charset.StandardCharsets; import java.time.Duration; import java.time.temporal.ChronoUnit; +import java.util.Arrays; import java.util.Collections; +import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Optional; import java.util.OptionalInt; import java.util.concurrent.ThreadFactory; @@ -145,7 +148,16 @@ public abstract class HttpClientConfiguration { private String eventLoopGroup = "default"; - private HttpVersion httpVersion = HttpVersion.HTTP_1_1; + @Deprecated + @Nullable + private HttpVersion httpVersion = null; + + private HttpVersionSelection.PlaintextMode plaintextMode = HttpVersionSelection.PlaintextMode.HTTP_1; + + private List alpnModes = Arrays.asList( + HttpVersionSelection.ALPN_HTTP_2, + HttpVersionSelection.ALPN_HTTP_1 + ); private LogLevel logLevel; @@ -201,7 +213,11 @@ public HttpClientConfiguration(HttpClientConfiguration copy) { /** * The HTTP version to use. Defaults to {@link HttpVersion#HTTP_1_1}. * @return The http version + * @deprecated There are now separate settings for HTTP and HTTPS connections. To configure + * HTTP connections (e.g. for h2c), use {@link #plaintextMode}. To configure ALPN, set + * {@link #alpnModes}. */ + @Deprecated public HttpVersion getHttpVersion() { return httpVersion; } @@ -209,7 +225,11 @@ public HttpVersion getHttpVersion() { /** * Sets the HTTP version to use. Defaults to {@link HttpVersion#HTTP_1_1}. * @param httpVersion The http version + * @deprecated There are now separate settings for HTTP and HTTPS connections. To configure + * HTTP connections (e.g. for h2c), use {@link #plaintextMode}. To configure ALPN, set + * {@link #alpnModes}. */ + @Deprecated public void setHttpVersion(HttpVersion httpVersion) { if (httpVersion != null) { this.httpVersion = httpVersion; @@ -637,6 +657,58 @@ public Proxy resolveProxy(boolean isSsl, String host, int port) { } } + /** + * The connection mode to use for plaintext (http as opposed to https) connections. + *
+ * Note: If {@link #httpVersion} is set, this setting is ignored! + * + * @return The plaintext connection mode. + * @since 4.0.0 + */ + @NonNull + public HttpVersionSelection.PlaintextMode getPlaintextMode() { + return plaintextMode; + } + + /** + * The connection mode to use for plaintext (http as opposed to https) connections. + *
+ * Note: If {@link #httpVersion} is set, this setting is ignored! + * + * @param plaintextMode The plaintext connection mode. + * @since 4.0.0 + */ + public void setPlaintextMode(@NonNull HttpVersionSelection.PlaintextMode plaintextMode) { + this.plaintextMode = Objects.requireNonNull(plaintextMode, "plaintextMode"); + } + + /** + * The protocols to support for TLS ALPN. If HTTP 2 is included, this will also restrict the + * TLS cipher suites to those supported by the HTTP 2 standard. + *
+ * Note: If {@link #httpVersion} is set, this setting is ignored! + * + * @return The supported ALPN protocols. + * @since 4.0.0 + */ + @NonNull + public List getAlpnModes() { + return alpnModes; + } + + /** + * The protocols to support for TLS ALPN. If HTTP 2 is included, this will also restrict the + * TLS cipher suites to those supported by the HTTP 2 standard. + *
+ * Note: If {@link #httpVersion} is set, this setting is ignored! + * + * @param alpnModes The supported ALPN protocols. + * @since 4.0.0 + */ + public void setAlpnModes(@NonNull List alpnModes) { + this.alpnModes = Objects.requireNonNull(alpnModes, "alpnModes"); + } + /** * Configuration for the HTTP client connnection pool. */ @@ -650,15 +722,13 @@ public static class ConnectionPoolConfiguration implements Toggleable { * The default enable value. */ @SuppressWarnings("WeakerAccess") - public static final boolean DEFAULT_ENABLED = false; + public static final boolean DEFAULT_ENABLED = true; - /** - * The default max connections value. - */ - @SuppressWarnings("WeakerAccess") - public static final int DEFAULT_MAXCONNECTIONS = -1; + private int maxPendingConnections = 4; - private int maxConnections = DEFAULT_MAXCONNECTIONS; + private int maxConcurrentRequestsPerHttp2Connection = Integer.MAX_VALUE; + private int maxConcurrentHttp1Connections = Integer.MAX_VALUE; + private int maxConcurrentHttp2Connections = 1; private int maxPendingAcquires = Integer.MAX_VALUE; @@ -685,24 +755,6 @@ public void setEnabled(boolean enabled) { this.enabled = enabled; } - /** - * The maximum number of connections. Defaults to ({@value io.micronaut.http.client.HttpClientConfiguration.ConnectionPoolConfiguration#DEFAULT_MAXCONNECTIONS}); no maximum. - * - * @return The max connections - */ - public int getMaxConnections() { - return maxConnections; - } - - /** - * Sets the maximum number of connections. Defaults to no maximum. - * - * @param maxConnections The count - */ - public void setMaxConnections(int maxConnections) { - this.maxConnections = maxConnections; - } - /** * Maximum number of futures awaiting connection acquisition. Defaults to no maximum. * @@ -738,5 +790,90 @@ public Optional getAcquireTimeout() { public void setAcquireTimeout(@Nullable Duration acquireTimeout) { this.acquireTimeout = acquireTimeout; } + + /** + * The maximum number of pending (new) connections before they are assigned to a + * pool. + * + * @return The maximum number of pending connections + * @since 4.0.0 + */ + public int getMaxPendingConnections() { + return maxPendingConnections; + } + + /** + * The maximum number of pending (new) connections before they are assigned to a + * pool. + * + * @param maxPendingConnections The maximum number of pending connections + * @since 4.0.0 + */ + public void setMaxPendingConnections(int maxPendingConnections) { + this.maxPendingConnections = maxPendingConnections; + } + + /** + * The maximum number of requests (streams) that can run concurrently on one HTTP2 + * connection. + * + * @return The maximum concurrent request count + * @since 4.0.0 + */ + public int getMaxConcurrentRequestsPerHttp2Connection() { + return maxConcurrentRequestsPerHttp2Connection; + } + + /** + * The maximum number of requests (streams) that can run concurrently on one HTTP2 + * connection. + * + * @param maxConcurrentRequestsPerHttp2Connection The maximum concurrent request count + * @since 4.0.0 + */ + public void setMaxConcurrentRequestsPerHttp2Connection(int maxConcurrentRequestsPerHttp2Connection) { + this.maxConcurrentRequestsPerHttp2Connection = maxConcurrentRequestsPerHttp2Connection; + } + + /** + * The maximum number of concurrent HTTP1 connections in the pool. + * + * @return The maximum concurrent connection count + * @since 4.0.0 + */ + public int getMaxConcurrentHttp1Connections() { + return maxConcurrentHttp1Connections; + } + + /** + * The maximum number of concurrent HTTP1 connections in the pool. + * + * @param maxConcurrentHttp1Connections The maximum concurrent connection count + * @since 4.0.0 + */ + public void setMaxConcurrentHttp1Connections(int maxConcurrentHttp1Connections) { + this.maxConcurrentHttp1Connections = maxConcurrentHttp1Connections; + } + + /** + * The maximum number of concurrent HTTP2 connections in the pool. + * + * @return The maximum concurrent connection count + * @since 4.0.0 + */ + public int getMaxConcurrentHttp2Connections() { + return maxConcurrentHttp2Connections; + } + + /** + * The maximum number of concurrent HTTP2 connections in the pool. + * + * @param maxConcurrentHttp2Connections The maximum concurrent connection count + * @since 4.0.0 + */ + public void setMaxConcurrentHttp2Connections(int maxConcurrentHttp2Connections) { + this.maxConcurrentHttp2Connections = maxConcurrentHttp2Connections; + } } + } diff --git a/http-client-core/src/main/java/io/micronaut/http/client/HttpClientRegistry.java b/http-client-core/src/main/java/io/micronaut/http/client/HttpClientRegistry.java index 8872c9927bf..068a1c315de 100644 --- a/http-client-core/src/main/java/io/micronaut/http/client/HttpClientRegistry.java +++ b/http-client-core/src/main/java/io/micronaut/http/client/HttpClientRegistry.java @@ -47,9 +47,25 @@ public interface HttpClientRegistry { * @param clientId The client ID * @param path The path (Optional) * @return The client + * @deprecated Use {@link #getClient(HttpVersionSelection, String, String)} instead */ + @Deprecated @NonNull - T getClient(HttpVersion httpVersion, @NonNull String clientId, @Nullable String path); + default T getClient(HttpVersion httpVersion, @NonNull String clientId, @Nullable String path) { + return getClient(HttpVersionSelection.forLegacyVersion(httpVersion), clientId, path); + } + + /** + * Return the client for the client ID and path. + * + * @param httpVersion The HTTP version + * @param clientId The client ID + * @param path The path (Optional) + * @return The client + * @since 4.0.0 + */ + @NonNull + T getClient(@NonNull HttpVersionSelection httpVersion, @NonNull String clientId, @Nullable String path); /** * Resolves a {@link HttpClient} for the given injection point. diff --git a/http-client-core/src/main/java/io/micronaut/http/client/HttpVersionSelection.java b/http-client-core/src/main/java/io/micronaut/http/client/HttpVersionSelection.java new file mode 100644 index 00000000000..4d8d8afce7a --- /dev/null +++ b/http-client-core/src/main/java/io/micronaut/http/client/HttpVersionSelection.java @@ -0,0 +1,197 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.http.client; + +import io.micronaut.core.annotation.AnnotationMetadata; +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.NonNull; +import io.micronaut.core.annotation.Nullable; +import io.micronaut.http.HttpVersion; +import io.micronaut.http.client.annotation.Client; + +import java.util.Arrays; + +/** + * This class collects information about HTTP client protocol version settings, such as the + * {@link PlaintextMode} and the ALPN configuration. + * + * @author Jonas Konrad + * @since 4.0 + */ +public final class HttpVersionSelection { + /** + * ALPN protocol ID for HTTP/1.1. + */ + public static final String ALPN_HTTP_1 = "http/1.1"; + /** + * ALPN protocol ID for HTTP/2. + */ + public static final String ALPN_HTTP_2 = "h2"; + + private static final HttpVersionSelection LEGACY_1 = new HttpVersionSelection( + PlaintextMode.HTTP_1, + false, + new String[]{ALPN_HTTP_1}, + false + ); + + private static final HttpVersionSelection LEGACY_2 = new HttpVersionSelection( + PlaintextMode.H2C, + true, + new String[]{ALPN_HTTP_1, ALPN_HTTP_2}, + true + ); + + private final PlaintextMode plaintextMode; + private final boolean alpn; + private final String[] alpnSupportedProtocols; + private final boolean http2CipherSuites; + + private HttpVersionSelection(@NonNull PlaintextMode plaintextMode, boolean alpn, @NonNull String[] alpnSupportedProtocols, boolean http2CipherSuites) { + this.plaintextMode = plaintextMode; + this.alpn = alpn; + this.alpnSupportedProtocols = alpnSupportedProtocols; + this.http2CipherSuites = http2CipherSuites; + } + + /** + * Get the {@link HttpVersionSelection} that matches Micronaut HTTP client 3.x behavior for the + * given version setting. + * + * @param httpVersion The HTTP version as configured for Micronaut HTTP client 3.x + * @return The version selection + */ + @NonNull + public static HttpVersionSelection forLegacyVersion(@NonNull HttpVersion httpVersion) { + switch (httpVersion) { + case HTTP_1_0: + case HTTP_1_1: + return LEGACY_1; + case HTTP_2_0: + return LEGACY_2; + default: + throw new IllegalArgumentException("HTTP version " + httpVersion + " not supported here"); + } + } + + /** + * Construct a version selection from the given client configuration. + * + * @param clientConfiguration The client configuration + * @return The configured version selection + */ + public static HttpVersionSelection forClientConfiguration(HttpClientConfiguration clientConfiguration) { + @SuppressWarnings("deprecation") + HttpVersion legacyHttpVersion = clientConfiguration.getHttpVersion(); + if (legacyHttpVersion != null) { + return forLegacyVersion(legacyHttpVersion); + } else { + String[] alpnModes = clientConfiguration.getAlpnModes().toArray(new String[0]); + return new HttpVersionSelection( + clientConfiguration.getPlaintextMode(), + true, + alpnModes, + Arrays.asList(alpnModes).contains(ALPN_HTTP_2) + ); + } + } + + /** + * Infer the version selection for the given {@link Client} annotation, if any version settings + * are set. + * + * @param metadata The annotation metadata possibly containing a {@link Client} annotation + * @return The configured version selection, or {@code null} if the version is not explicitly + * set and should be inherited from the normal configuration instead. + */ + @Internal + @Nullable + public static HttpVersionSelection forClientAnnotation(AnnotationMetadata metadata) { + HttpVersion legacyHttpVersion = + metadata.enumValue(Client.class, "httpVersion", HttpVersion.class).orElse(null); + if (legacyHttpVersion != null) { + return forLegacyVersion(legacyHttpVersion); + } else { + String[] alpnModes = metadata.stringValues(Client.class, "alpnModes"); + PlaintextMode plaintextMode = metadata.enumValue(Client.class, "plaintextMode", PlaintextMode.class) + .orElse(null); + if (alpnModes.length == 0 && plaintextMode == null) { + // nothing set at all, default to client configuration + return null; + } + + // defaults + if (alpnModes.length == 0) { + alpnModes = new String[]{ALPN_HTTP_2, ALPN_HTTP_1}; + } + if (plaintextMode == null) { + plaintextMode = PlaintextMode.HTTP_1; + } + return new HttpVersionSelection( + plaintextMode, + true, + alpnModes, + Arrays.asList(alpnModes).contains(ALPN_HTTP_2) + ); + } + } + + /** + * @return Connection mode to use for plaintext connections + */ + @Internal + public PlaintextMode getPlaintextMode() { + return plaintextMode; + } + + /** + * @return Protocols that should be shown as supported via ALPN + */ + @Internal + public String[] getAlpnSupportedProtocols() { + return alpnSupportedProtocols; + } + + /** + * @return Whether ALPN should be used + */ + @Internal + public boolean isAlpn() { + return alpn; + } + + /** + * @return Whether TLS cipher suites should be constrained to those defined by the HTTP/2 spec + */ + @Internal + public boolean isHttp2CipherSuites() { + return http2CipherSuites; + } + + /** + * The connection mode to use for plaintext (non-TLS) connections. + */ + public enum PlaintextMode { + /** + * Normal HTTP/1.1 connection. + */ + HTTP_1, + /** + * HTTP/2 cleartext upgrade from HTTP/1.1. + */ + H2C, + } +} diff --git a/http-client-core/src/main/java/io/micronaut/http/client/ServiceHttpClientFactory.java b/http-client-core/src/main/java/io/micronaut/http/client/ServiceHttpClientFactory.java index ca9f17c7d95..2aed9421670 100644 --- a/http-client-core/src/main/java/io/micronaut/http/client/ServiceHttpClientFactory.java +++ b/http-client-core/src/main/java/io/micronaut/http/client/ServiceHttpClientFactory.java @@ -98,7 +98,7 @@ ApplicationEventListener healthCheckStarter(@Parameter Servi Collection loadBalancedURIs = instanceList.getLoadBalancedURIs(); final HttpClient httpClient = clientFactory.get() .getClient( - configuration.getHttpVersion(), + HttpVersionSelection.forClientConfiguration(configuration), configuration.getServiceId(), configuration.getPath().orElse(null)); final Duration initialDelay = configuration.getHealthCheckInterval(); diff --git a/http-client-core/src/main/java/io/micronaut/http/client/annotation/Client.java b/http-client-core/src/main/java/io/micronaut/http/client/annotation/Client.java index 71794cdb4f4..5eb3cb824ac 100644 --- a/http-client-core/src/main/java/io/micronaut/http/client/annotation/Client.java +++ b/http-client-core/src/main/java/io/micronaut/http/client/annotation/Client.java @@ -18,8 +18,10 @@ import io.micronaut.aop.Introduction; import io.micronaut.context.annotation.AliasFor; import io.micronaut.context.annotation.Type; +import io.micronaut.core.annotation.NonNull; import io.micronaut.http.HttpVersion; import io.micronaut.http.client.HttpClientConfiguration; +import io.micronaut.http.client.HttpVersionSelection; import io.micronaut.http.client.interceptor.HttpClientIntroductionAdvice; import io.micronaut.http.hateoas.JsonError; import io.micronaut.retry.annotation.Recoverable; @@ -80,6 +82,36 @@ * The HTTP version. * * @return The HTTP version of the client. + * @deprecated There are now separate settings for HTTP and HTTPS connections. To configure + * HTTP connections (e.g. for h2c), use {@link #plaintextMode}. To configure ALPN, set + * {@link #alpnModes}. */ + @Deprecated HttpVersion httpVersion() default HttpVersion.HTTP_1_1; + + /** + * The connection mode to use for plaintext (http as opposed to https) connections. + *
+ * Note: If {@link #httpVersion} is set, this setting is ignored! + * + * @return The plaintext connection mode. + * @since 4.0.0 + */ + @NonNull + HttpVersionSelection.PlaintextMode plaintextMode() default HttpVersionSelection.PlaintextMode.HTTP_1; + + /** + * The protocols to support for TLS ALPN. If HTTP 2 is included, this will also restrict the + * TLS cipher suites to those supported by the HTTP 2 standard. + *
+ * Note: If {@link #httpVersion} is set, this setting is ignored! + * + * @return The supported ALPN protocols. + * @since 4.0.0 + */ + @NonNull + String[] alpnModes() default { + HttpVersionSelection.ALPN_HTTP_2, + HttpVersionSelection.ALPN_HTTP_1 + }; } diff --git a/http-client/src/main/java/io/micronaut/http/client/netty/CancellableMonoSink.java b/http-client/src/main/java/io/micronaut/http/client/netty/CancellableMonoSink.java new file mode 100644 index 00000000000..c56a84accf5 --- /dev/null +++ b/http-client/src/main/java/io/micronaut/http/client/netty/CancellableMonoSink.java @@ -0,0 +1,140 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.http.client.netty; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.NonNull; +import org.reactivestreams.Publisher; +import org.reactivestreams.Subscriber; +import org.reactivestreams.Subscription; +import reactor.core.publisher.Mono; +import reactor.core.publisher.Sinks; + +/** + * Version of {@link Sinks#one()} where cancellation of the {@link Mono} will make future emit + * calls fail. + * + * @param Element type + */ +@Internal +final class CancellableMonoSink implements Publisher, Sinks.One, Subscription { + private static final Object EMPTY = new Object(); + + private T value; + private Throwable failure; + private boolean complete = false; + private Subscriber subscriber = null; + private boolean subscriberWaiting = false; + + @Override + public synchronized void subscribe(Subscriber s) { + if (this.subscriber != null) { + s.onError(new IllegalStateException("Only one subscriber allowed")); + } + subscriber = s; + subscriber.onSubscribe(this); + } + + private void tryForward() { + if (subscriberWaiting && complete) { + if (failure == null) { + if (value != EMPTY) { + subscriber.onNext(value); + } + subscriber.onComplete(); + } else { + subscriber.onError(failure); + } + } + } + + @NonNull + @Override + public synchronized Sinks.EmitResult tryEmitValue(T value) { + if (complete) { + return Sinks.EmitResult.FAIL_OVERFLOW; + } else { + this.value = value; + complete = true; + tryForward(); + return Sinks.EmitResult.OK; + } + } + + @Override + public void emitValue(T value, @NonNull Sinks.EmitFailureHandler failureHandler) { + throw new UnsupportedOperationException(); + } + + @SuppressWarnings("unchecked") + @NonNull + @Override + public Sinks.EmitResult tryEmitEmpty() { + return tryEmitValue((T) EMPTY); + } + + @NonNull + @Override + public synchronized Sinks.EmitResult tryEmitError(@NonNull Throwable error) { + if (complete) { + return Sinks.EmitResult.FAIL_OVERFLOW; + } else { + this.failure = error; + complete = true; + tryForward(); + return Sinks.EmitResult.OK; + } + } + + @Override + public void emitEmpty(@NonNull Sinks.EmitFailureHandler failureHandler) { + throw new UnsupportedOperationException(); + } + + @Override + public void emitError(@NonNull Throwable error, @NonNull Sinks.EmitFailureHandler failureHandler) { + throw new UnsupportedOperationException(); + } + + @Override + public synchronized int currentSubscriberCount() { + return subscriber == null ? 0 : 1; + } + + @NonNull + @Override + public Mono asMono() { + return Mono.from(this); + } + + @Override + public Object scanUnsafe(@NonNull Attr key) { + return null; + } + + @Override + public synchronized void request(long n) { + if (n > 0 && !subscriberWaiting) { + subscriberWaiting = true; + tryForward(); + } + } + + @Override + public synchronized void cancel() { + complete = true; + } +} diff --git a/http-client/src/main/java/io/micronaut/http/client/netty/ConnectTTLHandler.java b/http-client/src/main/java/io/micronaut/http/client/netty/ConnectTTLHandler.java deleted file mode 100644 index 724611fb18a..00000000000 --- a/http-client/src/main/java/io/micronaut/http/client/netty/ConnectTTLHandler.java +++ /dev/null @@ -1,88 +0,0 @@ -/* - * Copyright 2017-2020 original authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.micronaut.http.client.netty; - -import io.netty.channel.Channel; -import io.netty.channel.ChannelDuplexHandler; -import io.netty.channel.ChannelHandlerContext; -import io.netty.util.AttributeKey; -import io.netty.util.concurrent.ScheduledFuture; - -import java.util.concurrent.TimeUnit; - -/** - * A handler that will close channels after they have reached their time-to-live, regardless of usage. - * - * channels that are in use will be closed when they are next - * released to the underlying connection pool. - */ -public class ConnectTTLHandler extends ChannelDuplexHandler { - - public static final AttributeKey RELEASE_CHANNEL = AttributeKey.newInstance("release_channel"); - - private final Long connectionTtlMillis; - private ScheduledFuture channelKiller; - - /** - * Construct ConnectTTLHandler for given arguments. - * @param connectionTtlMillis The configured connect-ttl - */ - public ConnectTTLHandler(Long connectionTtlMillis) { - if (connectionTtlMillis <= 0) { - throw new IllegalArgumentException("connectTTL must be positive"); - } - this.connectionTtlMillis = connectionTtlMillis; - } - - /** - * Will schedule a task when the handler added. - * @param ctx The context to use - * @throws Exception - */ - @Override - public void handlerAdded(ChannelHandlerContext ctx) throws Exception { - super.handlerAdded(ctx); - channelKiller = ctx.channel().eventLoop().schedule(() -> markChannelExpired(ctx), connectionTtlMillis, TimeUnit.MILLISECONDS); - } - - /** - * Will cancel the scheduled tasks when handler removed. - * @param ctx The context to use - */ - @Override - public void handlerRemoved(ChannelHandlerContext ctx) { - channelKiller.cancel(false); - } - - /** - * Will set RELEASE_CHANNEL as true for the channel attribute when connect-ttl is reached. - * @param ctx The context to use - */ - private void markChannelExpired(ChannelHandlerContext ctx) { - if (ctx.channel().isOpen()) { - ctx.channel().attr(RELEASE_CHANNEL).set(true); - } - } - - /** - * Indicates whether the channels connection ttl has expired. - * @param channel The channel to check - * @return true if the channels ttl has expired - */ - public static boolean isChannelExpired(Channel channel) { - return Boolean.TRUE.equals(channel.attr(ConnectTTLHandler.RELEASE_CHANNEL).get()); - } -} diff --git a/http-client/src/main/java/io/micronaut/http/client/netty/ConnectionManager.java b/http-client/src/main/java/io/micronaut/http/client/netty/ConnectionManager.java index 28d8755172c..7221852440c 100644 --- a/http-client/src/main/java/io/micronaut/http/client/netty/ConnectionManager.java +++ b/http-client/src/main/java/io/micronaut/http/client/netty/ConnectionManager.java @@ -20,71 +20,47 @@ import io.micronaut.core.annotation.Nullable; import io.micronaut.core.reflect.InstantiationUtils; import io.micronaut.core.util.StringUtils; -import io.micronaut.http.HttpVersion; +import io.micronaut.core.util.SupplierUtil; import io.micronaut.http.client.HttpClientConfiguration; +import io.micronaut.http.client.HttpVersionSelection; import io.micronaut.http.client.exceptions.HttpClientException; import io.micronaut.http.client.netty.ssl.NettyClientSslBuilder; import io.micronaut.http.netty.channel.ChannelPipelineCustomizer; -import io.micronaut.http.netty.channel.ChannelPipelineListener; import io.micronaut.http.netty.channel.NettyThreadFactory; -import io.micronaut.http.netty.stream.DefaultHttp2Content; -import io.micronaut.http.netty.stream.Http2Content; -import io.micronaut.http.netty.stream.HttpStreamsClientHandler; -import io.micronaut.http.netty.stream.StreamingInboundHttp2ToHttpAdapter; import io.micronaut.scheduling.instrument.Instrumentation; import io.micronaut.scheduling.instrument.InvocationInstrumenter; import io.micronaut.websocket.exceptions.WebSocketSessionException; import io.netty.bootstrap.Bootstrap; -import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; import io.netty.channel.Channel; import io.netty.channel.ChannelFactory; import io.netty.channel.ChannelFuture; -import io.netty.channel.ChannelFutureListener; import io.netty.channel.ChannelHandler; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelInboundHandlerAdapter; import io.netty.channel.ChannelInitializer; import io.netty.channel.ChannelOption; import io.netty.channel.ChannelPipeline; -import io.netty.channel.ChannelPromise; import io.netty.channel.EventLoopGroup; import io.netty.channel.nio.NioEventLoopGroup; -import io.netty.channel.pool.AbstractChannelPoolHandler; -import io.netty.channel.pool.AbstractChannelPoolMap; -import io.netty.channel.pool.ChannelHealthChecker; -import io.netty.channel.pool.ChannelPool; -import io.netty.channel.pool.ChannelPoolMap; -import io.netty.channel.pool.FixedChannelPool; -import io.netty.channel.pool.SimpleChannelPool; -import io.netty.channel.socket.SocketChannel; -import io.netty.handler.codec.LineBasedFrameDecoder; +import io.netty.handler.codec.DecoderException; import io.netty.handler.codec.http.DefaultFullHttpRequest; -import io.netty.handler.codec.http.DefaultHttpContent; -import io.netty.handler.codec.http.FullHttpMessage; import io.netty.handler.codec.http.HttpClientCodec; import io.netty.handler.codec.http.HttpClientUpgradeHandler; -import io.netty.handler.codec.http.HttpContent; import io.netty.handler.codec.http.HttpContentDecompressor; import io.netty.handler.codec.http.HttpHeaderNames; -import io.netty.handler.codec.http.HttpMessage; import io.netty.handler.codec.http.HttpMethod; import io.netty.handler.codec.http.HttpObjectAggregator; -import io.netty.handler.codec.http.HttpUtil; -import io.netty.handler.codec.http.LastHttpContent; import io.netty.handler.codec.http.websocketx.extensions.compression.WebSocketClientCompressionHandler; -import io.netty.handler.codec.http2.DefaultHttp2Connection; -import io.netty.handler.codec.http2.DelegatingDecompressorFrameListener; import io.netty.handler.codec.http2.Http2ClientUpgradeCodec; -import io.netty.handler.codec.http2.Http2Connection; -import io.netty.handler.codec.http2.Http2FrameListener; +import io.netty.handler.codec.http2.Http2FrameCodec; +import io.netty.handler.codec.http2.Http2FrameCodecBuilder; import io.netty.handler.codec.http2.Http2FrameLogger; -import io.netty.handler.codec.http2.Http2Settings; -import io.netty.handler.codec.http2.Http2Stream; -import io.netty.handler.codec.http2.HttpConversionUtil; -import io.netty.handler.codec.http2.HttpToHttp2ConnectionHandler; -import io.netty.handler.codec.http2.HttpToHttp2ConnectionHandlerBuilder; -import io.netty.handler.codec.http2.InboundHttp2ToHttpAdapterBuilder; +import io.netty.handler.codec.http2.Http2MultiplexHandler; +import io.netty.handler.codec.http2.Http2SettingsFrame; +import io.netty.handler.codec.http2.Http2StreamChannel; +import io.netty.handler.codec.http2.Http2StreamChannelBootstrap; +import io.netty.handler.codec.http2.Http2StreamFrameToHttpObjectCodec; import io.netty.handler.logging.LoggingHandler; import io.netty.handler.proxy.HttpProxyHandler; import io.netty.handler.proxy.Socks5ProxyHandler; @@ -92,89 +68,97 @@ import io.netty.handler.ssl.ApplicationProtocolNegotiationHandler; import io.netty.handler.ssl.SslContext; import io.netty.handler.ssl.SslHandler; -import io.netty.handler.timeout.IdleStateEvent; +import io.netty.handler.ssl.SslHandshakeCompletionEvent; import io.netty.handler.timeout.IdleStateHandler; +import io.netty.handler.timeout.ReadTimeoutException; import io.netty.handler.timeout.ReadTimeoutHandler; import io.netty.resolver.NoopAddressResolverGroup; -import io.netty.util.Attribute; -import io.netty.util.AttributeKey; import io.netty.util.ReferenceCountUtil; +import io.netty.util.ResourceLeakDetector; +import io.netty.util.ResourceLeakDetectorFactory; +import io.netty.util.ResourceLeakTracker; import io.netty.util.concurrent.Future; import io.netty.util.concurrent.GenericFutureListener; -import io.netty.util.concurrent.Promise; -import org.reactivestreams.Publisher; +import io.netty.util.concurrent.ScheduledFuture; import org.slf4j.Logger; -import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.core.publisher.Sinks; -import reactor.core.scheduler.Scheduler; import reactor.core.scheduler.Schedulers; +import javax.net.ssl.SSLException; import java.net.InetSocketAddress; import java.net.Proxy; import java.net.SocketAddress; import java.time.Duration; -import java.util.Collection; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; import java.util.Map; import java.util.Optional; import java.util.OptionalInt; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ThreadFactory; import java.util.concurrent.TimeUnit; -import java.util.function.Consumer; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Supplier; /** * Connection manager for {@link DefaultHttpClient}. This class manages the lifecycle of netty * channels (wrapped in {@link PoolHandle}s), including pooling and timeouts. */ @Internal -final class ConnectionManager { - final ChannelPoolMap poolMap; +class ConnectionManager { final InvocationInstrumenter instrumenter; - final HttpVersion httpVersion; - - // not static to avoid build-time initialization by native image - private final AttributeKey CHANNEL_CUSTOMIZER_KEY = - AttributeKey.valueOf("micronaut.http.customizer"); - /** - * Future on a pooled channel that will be completed when the channel has fully connected (e.g. - * TLS handshake has completed). If unset, then no handshake is needed or it has already - * completed. - */ - private final AttributeKey> STREAM_CHANNEL_INITIALIZED = - AttributeKey.valueOf("micronaut.http.streamChannelInitialized"); - private final AttributeKey STREAM_KEY = AttributeKey.valueOf("micronaut.http2.stream"); + private final HttpVersionSelection httpVersion; private final Logger log; + private final Map pools = new ConcurrentHashMap<>(); private EventLoopGroup group; private final boolean shutdownGroup; private final ThreadFactory threadFactory; private final ChannelFactory socketChannelFactory; private Bootstrap bootstrap; private final HttpClientConfiguration configuration; - @Nullable - private final Long readTimeoutMillis; - @Nullable - private final Long connectionTimeAliveMillis; private final SslContext sslContext; private final NettyClientCustomizer clientCustomizer; - private final Collection pipelineListeners; private final String informationalServiceId; + /** + * Copy constructor used by the test suite to patch this manager. + * + * @param from Original connection manager + */ + ConnectionManager(ConnectionManager from) { + this.instrumenter = from.instrumenter; + this.httpVersion = from.httpVersion; + this.log = from.log; + this.group = from.group; + this.shutdownGroup = from.shutdownGroup; + this.threadFactory = from.threadFactory; + this.socketChannelFactory = from.socketChannelFactory; + this.bootstrap = from.bootstrap; + this.configuration = from.configuration; + this.sslContext = from.sslContext; + this.clientCustomizer = from.clientCustomizer; + this.informationalServiceId = from.informationalServiceId; + } + ConnectionManager( Logger log, @Nullable EventLoopGroup eventLoopGroup, - ThreadFactory threadFactory, + @Nullable ThreadFactory threadFactory, HttpClientConfiguration configuration, - HttpVersion httpVersion, + @Nullable HttpVersionSelection httpVersion, InvocationInstrumenter instrumenter, ChannelFactory socketChannelFactory, NettyClientSslBuilder nettyClientSslBuilder, NettyClientCustomizer clientCustomizer, - Collection pipelineListeners, String informationalServiceId) { if (httpVersion == null) { - httpVersion = configuration.getHttpVersion(); + httpVersion = HttpVersionSelection.forClientConfiguration(configuration); } this.log = log; @@ -184,16 +168,9 @@ final class ConnectionManager { this.configuration = configuration; this.instrumenter = instrumenter; this.clientCustomizer = clientCustomizer; - this.pipelineListeners = pipelineListeners; this.informationalServiceId = informationalServiceId; - this.connectionTimeAliveMillis = configuration.getConnectTtl() - .map(duration -> !duration.isNegative() ? duration.toMillis() : null) - .orElse(null); - this.readTimeoutMillis = configuration.getReadTimeout() - .map(duration -> !duration.isNegative() ? duration.toMillis() : null) - .orElse(null); - this.sslContext = nettyClientSslBuilder.build(configuration.getSslConfiguration(), httpVersion).orElse(null); + this.sslContext = nettyClientSslBuilder.build(configuration.getSslConfiguration(), httpVersion); if (eventLoopGroup != null) { group = eventLoopGroup; @@ -205,55 +182,6 @@ final class ConnectionManager { initBootstrap(); - final ChannelHealthChecker channelHealthChecker = channel -> channel.eventLoop().newSucceededFuture(channel.isActive() && !ConnectTTLHandler.isChannelExpired(channel)); - - HttpClientConfiguration.ConnectionPoolConfiguration connectionPoolConfiguration = configuration.getConnectionPoolConfiguration(); - // HTTP/2 defaults to keep alive connections so should we should always use a pool - if (connectionPoolConfiguration.isEnabled() || httpVersion == io.micronaut.http.HttpVersion.HTTP_2_0) { - int maxConnections = connectionPoolConfiguration.getMaxConnections(); - if (maxConnections > -1) { - poolMap = new AbstractChannelPoolMap() { - @Override - protected ChannelPool newPool(DefaultHttpClient.RequestKey key) { - Bootstrap newBootstrap = bootstrap.clone(group); - initBootstrapForProxy(newBootstrap, key.isSecure(), key.getHost(), key.getPort()); - newBootstrap.remoteAddress(key.getRemoteAddress()); - - AbstractChannelPoolHandler channelPoolHandler = newPoolHandler(key); - final long acquireTimeoutMillis = connectionPoolConfiguration.getAcquireTimeout().map(Duration::toMillis).orElse(-1L); - return new FixedChannelPool( - newBootstrap, - channelPoolHandler, - channelHealthChecker, - acquireTimeoutMillis > -1 ? FixedChannelPool.AcquireTimeoutAction.FAIL : null, - acquireTimeoutMillis, - maxConnections, - connectionPoolConfiguration.getMaxPendingAcquires() - - ); - } - }; - } else { - poolMap = new AbstractChannelPoolMap() { - @Override - protected ChannelPool newPool(DefaultHttpClient.RequestKey key) { - Bootstrap newBootstrap = bootstrap.clone(group); - initBootstrapForProxy(newBootstrap, key.isSecure(), key.getHost(), key.getPort()); - newBootstrap.remoteAddress(key.getRemoteAddress()); - - AbstractChannelPoolHandler channelPoolHandler = newPoolHandler(key); - return new SimpleChannelPool( - newBootstrap, - channelPoolHandler, - channelHealthChecker - ); - } - }; - } - } else { - this.poolMap = null; - } - Optional connectTimeout = configuration.getConnectTimeout(); connectTimeout.ifPresent(duration -> bootstrap.option( ChannelOption.CONNECT_TIMEOUT_MILLIS, @@ -301,6 +229,45 @@ private static NioEventLoopGroup createEventLoopGroup(HttpClientConfiguration co return group; } + /** + * For testing. + * + * @return Connected channels in all pools + * @since 4.0.0 + */ + @NonNull + @SuppressWarnings("unused") + List getChannels() { + List channels = new ArrayList<>(); + for (Pool pool : pools.values()) { + pool.forEachConnection(c -> channels.add(((Pool.ConnectionHolder) c).channel)); + } + return channels; + } + + /** + * For testing. + * + * @return Number of running requests + * @since 4.0.0 + */ + @SuppressWarnings("unused") + int liveRequestCount() { + AtomicInteger count = new AtomicInteger(); + for (Pool pool : pools.values()) { + pool.forEachConnection(c -> { + if (c instanceof Pool.Http1ConnectionHolder) { + if (((Pool.Http1ConnectionHolder) c).hasLiveRequests()) { + count.incrementAndGet(); + } + } else { + count.addAndGet(((Pool.Http2ConnectionHolder) c).liveRequests.get()); + } + }); + } + return count.get(); + } + /** * @see DefaultHttpClient#start() */ @@ -323,27 +290,8 @@ private void initBootstrap() { * @see DefaultHttpClient#stop() */ public void shutdown() { - if (poolMap instanceof Iterable) { - Iterable> i = (Iterable) poolMap; - for (Map.Entry entry : i) { - ChannelPool cp = entry.getValue(); - try { - if (cp instanceof SimpleChannelPool) { - addInstrumentedListener(((SimpleChannelPool) cp).closeAsync(), future -> { - if (!future.isSuccess()) { - final Throwable cause = future.cause(); - if (cause != null) { - log.error("Error shutting down HTTP client connection pool: " + cause.getMessage(), cause); - } - } - }); - } else { - cp.close(); - } - } catch (Exception cause) { - log.error("Error shutting down HTTP client connection pool: " + cause.getMessage(), cause); - } - } + for (Pool pool : pools.values()) { + pool.shutdown(); } if (shutdownGroup) { Duration shutdownTimeout = configuration.getShutdownTimeout() @@ -374,54 +322,23 @@ public boolean isRunning() { } /** - * Get a reactive scheduler that runs on the event loop group of this connection manager. - * - * @return A scheduler that runs on the event loop - */ - public Scheduler getEventLoopScheduler() { - return Schedulers.fromExecutor(group); - } - - /** - * Creates an initial connection to the given remote host. + * Use the bootstrap to connect to the given host. Also does some proxy setup. This method is + * protected: The test suite overrides it to return embedded channels instead. * - * @param requestKey The request key to connect to - * @param isStream Is the connection a stream connection - * @param isProxy Is this a streaming proxy - * @param acceptsEvents Whether the connection will accept events - * @param contextConsumer The logic to run once the channel is configured correctly - * @return A ChannelFuture - * @throws HttpClientException If the URI is invalid + * @param requestKey The host to connect to + * @param channelInitializer The initializer to use + * @return Future that terminates when the TCP connection is established. */ - private ChannelFuture doConnect( - DefaultHttpClient.RequestKey requestKey, - boolean isStream, - boolean isProxy, - boolean acceptsEvents, - Consumer contextConsumer) throws HttpClientException { - - SslContext sslCtx = buildSslContext(requestKey); + protected ChannelFuture doConnect(DefaultHttpClient.RequestKey requestKey, ChannelInitializer channelInitializer) { String host = requestKey.getHost(); int port = requestKey.getPort(); Bootstrap localBootstrap = bootstrap.clone(); - initBootstrapForProxy(localBootstrap, sslCtx != null, host, port); - localBootstrap.handler(new HttpClientInitializer( - sslCtx, - host, - port, - isStream, - isProxy, - acceptsEvents, - contextConsumer) - ); - return localBootstrap.connect(host, port); - } - - private void initBootstrapForProxy(Bootstrap localBootstrap, boolean sslCtx, String host, int port) { - Proxy proxy = configuration.resolveProxy(sslCtx, host, port); + Proxy proxy = configuration.resolveProxy(requestKey.isSecure(), host, port); if (proxy.type() != Proxy.Type.DIRECT) { localBootstrap.resolver(NoopAddressResolverGroup.INSTANCE); } + localBootstrap.handler(channelInitializer); + return localBootstrap.connect(host, port); } /** @@ -429,6 +346,7 @@ private void initBootstrapForProxy(Bootstrap localBootstrap, boolean sslCtx, Str * * @return The {@link SslContext} instance */ + @Nullable private SslContext buildSslContext(DefaultHttpClient.RequestKey requestKey) { final SslContext sslCtx; if (requestKey.isSecure()) { @@ -443,128 +361,14 @@ private SslContext buildSslContext(DefaultHttpClient.RequestKey requestKey) { return sslCtx; } - private PoolHandle mockPoolHandle(Channel channel) { - return new PoolHandle(null, channel); - } - - /** - * Get a connection for exchange-like (non-streaming) http client methods. - * - * @param requestKey The remote to connect to - * @param multipart Whether the request should be multipart - * @param acceptEvents Whether the response may be an event stream - * @return A mono that will complete once the channel is ready for transmission - */ - Mono connectForExchange(DefaultHttpClient.RequestKey requestKey, boolean multipart, boolean acceptEvents) { - return Mono.create(emitter -> { - if (poolMap != null && !multipart) { - try { - ChannelPool channelPool = poolMap.get(requestKey); - addInstrumentedListener(channelPool.acquire(), future -> { - if (future.isSuccess()) { - Channel channel = future.get(); - PoolHandle poolHandle = new PoolHandle(channelPool, channel); - Future initFuture = channel.attr(STREAM_CHANNEL_INITIALIZED).get(); - if (initFuture == null) { - emitter.success(poolHandle); - } else { - // we should wait until the handshake completes - addInstrumentedListener(initFuture, f -> { - emitter.success(poolHandle); - }); - } - } else { - Throwable cause = future.cause(); - emitter.error(customizeException(new HttpClientException("Connect Error: " + cause.getMessage(), cause))); - } - }); - } catch (HttpClientException e) { - emitter.error(e); - } - } else { - ChannelFuture connectionFuture = doConnect(requestKey, false, false, acceptEvents, null); - addInstrumentedListener(connectionFuture, future -> { - if (!future.isSuccess()) { - Throwable cause = future.cause(); - emitter.error(customizeException(new HttpClientException("Connect Error: " + cause.getMessage(), cause))); - } else { - emitter.success(mockPoolHandle(connectionFuture.channel())); - } - }); - } - }) - .delayUntil(this::delayUntilHttp2Ready) - .map(poolHandle -> { - addReadTimeoutHandler(poolHandle.channel.pipeline()); - return poolHandle; - }); - } - - private Publisher delayUntilHttp2Ready(PoolHandle poolHandle) { - Http2SettingsHandler settingsHandler = (Http2SettingsHandler) poolHandle.channel.pipeline().get(ChannelPipelineCustomizer.HANDLER_HTTP2_SETTINGS); - if (settingsHandler == null) { - return Flux.empty(); - } - Sinks.Empty empty = Sinks.empty(); - addInstrumentedListener(settingsHandler.promise, future -> { - if (future.isSuccess()) { - empty.tryEmitEmpty(); - } else { - poolHandle.taint(); - poolHandle.release(); - empty.tryEmitError(future.cause()); - } - }); - return empty.asMono(); - } - /** - * Get a connection for streaming http client methods. + * Get a connection for non-websocket http client methods. * * @param requestKey The remote to connect to - * @param isProxy Whether the request is for a {@link io.micronaut.http.client.ProxyHttpClient} call - * @param acceptEvents Whether the response may be an event stream * @return A mono that will complete once the channel is ready for transmission */ - Mono connectForStream(DefaultHttpClient.RequestKey requestKey, boolean isProxy, boolean acceptEvents) { - return Mono.create(emitter -> { - ChannelFuture channelFuture; - try { - if (httpVersion == HttpVersion.HTTP_2_0) { - - channelFuture = doConnect(requestKey, true, isProxy, acceptEvents, channelHandlerContext -> { - try { - final Channel channel = channelHandlerContext.channel(); - emitter.success(mockPoolHandle(channel)); - } catch (Exception e) { - emitter.error(e); - } - }); - } else { - channelFuture = doConnect(requestKey, true, isProxy, acceptEvents, null); - addInstrumentedListener(channelFuture, - (ChannelFutureListener) f -> { - if (f.isSuccess()) { - Channel channel = f.channel(); - emitter.success(mockPoolHandle(channel)); - } else { - Throwable cause = f.cause(); - emitter.error(customizeException(new HttpClientException("Connect error:" + cause.getMessage(), cause))); - } - }); - } - } catch (HttpClientException e) { - emitter.error(e); - return; - } - - // todo: on emitter dispose/cancel, close channel - }) - .delayUntil(this::delayUntilHttp2Ready) - .map(poolHandle -> { - addReadTimeoutHandler(poolHandle.channel.pipeline()); - return poolHandle; - }); + Mono connect(DefaultHttpClient.RequestKey requestKey) { + return pools.computeIfAbsent(requestKey, Pool::new).acquire(); } /** @@ -576,49 +380,48 @@ Mono connectForStream(DefaultHttpClient.RequestKey requestKey, boole * @return A mono that will complete when the handshakes complete */ Mono connectForWebsocket(DefaultHttpClient.RequestKey requestKey, ChannelHandler handler) { - Sinks.Empty initial = Sinks.empty(); - - Bootstrap bootstrap = this.bootstrap.clone(); - SslContext sslContext = buildSslContext(requestKey); - - bootstrap.remoteAddress(requestKey.getHost(), requestKey.getPort()); - initBootstrapForProxy(bootstrap, sslContext != null, requestKey.getHost(), requestKey.getPort()); - bootstrap.handler(new HttpClientInitializer( - sslContext, - requestKey.getHost(), - requestKey.getPort(), - false, - false, - false, - null - ) { + Sinks.Empty initial = new CancellableMonoSink<>(); + + ChannelFuture connectFuture = doConnect(requestKey, new ChannelInitializer() { @Override - protected void addFinalHandler(ChannelPipeline pipeline) { - pipeline.remove(ChannelPipelineCustomizer.HANDLER_HTTP_DECODER); - ReadTimeoutHandler readTimeoutHandler = pipeline.get(ReadTimeoutHandler.class); - if (readTimeoutHandler != null) { - pipeline.remove(readTimeoutHandler); + protected void initChannel(@NonNull Channel ch) { + addLogHandler(ch); + + SslContext sslContext = buildSslContext(requestKey); + if (sslContext != null) { + SslHandler sslHandler = sslContext.newHandler(ch.alloc(), requestKey.getHost(), requestKey.getPort()); + sslHandler.setHandshakeTimeoutMillis(configuration.getSslConfiguration().getHandshakeTimeout().toMillis()); + ch.pipeline().addLast(sslHandler); } + ch.pipeline() + .addLast(ChannelPipelineCustomizer.HANDLER_HTTP_CLIENT_CODEC, new HttpClientCodec()) + .addLast(ChannelPipelineCustomizer.HANDLER_HTTP_AGGREGATOR, new HttpObjectAggregator(configuration.getMaxContentLength())); + Optional readIdleTime = configuration.getReadIdleTimeout(); if (readIdleTime.isPresent()) { Duration duration = readIdleTime.get(); if (!duration.isNegative()) { - pipeline.addLast(ChannelPipelineCustomizer.HANDLER_IDLE_STATE, new IdleStateHandler(duration.toMillis(), duration.toMillis(), duration.toMillis(), TimeUnit.MILLISECONDS)); + ch.pipeline() + .addLast(ChannelPipelineCustomizer.HANDLER_IDLE_STATE, new IdleStateHandler(duration.toMillis(), duration.toMillis(), duration.toMillis(), TimeUnit.MILLISECONDS)); } } try { - pipeline.addLast(WebSocketClientCompressionHandler.INSTANCE); - pipeline.addLast(ChannelPipelineCustomizer.HANDLER_MICRONAUT_WEBSOCKET_CLIENT, handler); - initial.tryEmitEmpty(); + ch.pipeline().addLast(WebSocketClientCompressionHandler.INSTANCE); + ch.pipeline().addLast(ChannelPipelineCustomizer.HANDLER_MICRONAUT_WEBSOCKET_CLIENT, handler); + clientCustomizer.specializeForChannel(ch, NettyClientCustomizer.ChannelRole.CONNECTION).onInitialPipelineBuilt(); + if (initial.tryEmitEmpty().isSuccess()) { + return; + } } catch (Throwable e) { initial.tryEmitError(new WebSocketSessionException("Error opening WebSocket client session: " + e.getMessage(), e)); } + // failed + ch.close(); } }); - - addInstrumentedListener(bootstrap.connect(), future -> { + addInstrumentedListener(connectFuture, future -> { if (!future.isSuccess()) { initial.tryEmitError(future.cause()); } @@ -627,235 +430,6 @@ protected void addFinalHandler(ChannelPipeline pipeline) { return initial.asMono(); } - private AbstractChannelPoolHandler newPoolHandler(DefaultHttpClient.RequestKey key) { - return new AbstractChannelPoolHandler() { - @Override - public void channelCreated(Channel ch) { - Promise streamPipelineBuilt = ch.newPromise(); - ch.attr(STREAM_CHANNEL_INITIALIZED).set(streamPipelineBuilt); - - // make sure the future completes eventually - ChannelHandler failureHandler = new ChannelInboundHandlerAdapter() { - @Override - public void handlerRemoved(ChannelHandlerContext ctx) { - streamPipelineBuilt.trySuccess(null); - } - - @Override - public void channelInactive(ChannelHandlerContext ctx) { - streamPipelineBuilt.trySuccess(null); - ctx.fireChannelInactive(); - } - }; - ch.pipeline().addLast(failureHandler); - - ch.pipeline().addLast(ChannelPipelineCustomizer.HANDLER_HTTP_CLIENT_INIT, new HttpClientInitializer( - key.isSecure() ? sslContext : null, - key.getHost(), - key.getPort(), - false, - false, - false, - null - ) { - @Override - protected void addFinalHandler(ChannelPipeline pipeline) { - // no-op, don't add the stream handler which is not supported - // in the connection pooled scenario - } - - @Override - void onStreamPipelineBuilt() { - super.onStreamPipelineBuilt(); - streamPipelineBuilt.trySuccess(null); - ch.pipeline().remove(failureHandler); - ch.attr(STREAM_CHANNEL_INITIALIZED).set(null); - } - }); - - if (connectionTimeAliveMillis != null) { - ch.pipeline() - .addLast( - ChannelPipelineCustomizer.HANDLER_CONNECT_TTL, - new ConnectTTLHandler(connectionTimeAliveMillis) - ); - } - } - - @Override - public void channelReleased(Channel ch) { - Duration idleTimeout = configuration.getConnectionPoolIdleTimeout().orElse(Duration.ofNanos(0)); - ChannelPipeline pipeline = ch.pipeline(); - if (ch.isOpen()) { - ch.config().setAutoRead(true); - pipeline.addLast(IdlingConnectionHandler.INSTANCE); - if (idleTimeout.toNanos() > 0) { - pipeline.addLast(ChannelPipelineCustomizer.HANDLER_IDLE_STATE, new IdleStateHandler(idleTimeout.toNanos(), idleTimeout.toNanos(), 0, TimeUnit.NANOSECONDS)); - pipeline.addLast(IdleTimeoutHandler.INSTANCE); - } - } - - if (ConnectTTLHandler.isChannelExpired(ch) && ch.isOpen() && !ch.eventLoop().isShuttingDown()) { - ch.close(); - } - - removeReadTimeoutHandler(pipeline); - } - - @Override - public void channelAcquired(Channel ch) throws Exception { - ChannelPipeline pipeline = ch.pipeline(); - if (pipeline.context(IdlingConnectionHandler.INSTANCE) != null) { - pipeline.remove(IdlingConnectionHandler.INSTANCE); - } - if (pipeline.context(ChannelPipelineCustomizer.HANDLER_IDLE_STATE) != null) { - pipeline.remove(ChannelPipelineCustomizer.HANDLER_IDLE_STATE); - } - if (pipeline.context(IdleTimeoutHandler.INSTANCE) != null) { - pipeline.remove(IdleTimeoutHandler.INSTANCE); - } - } - }; - } - - /** - * Configures HTTP/2 for the channel when SSL is enabled. - * - * @param httpClientInitializer The client initializer - * @param ch The channel - * @param sslCtx The SSL context - * @param host The host - * @param port The port - * @param connectionHandler The connection handler - */ - private void configureHttp2Ssl( - HttpClientInitializer httpClientInitializer, - @NonNull SocketChannel ch, - @NonNull SslContext sslCtx, - String host, - int port, - HttpToHttp2ConnectionHandler connectionHandler) { - ChannelPipeline pipeline = ch.pipeline(); - // Specify Host in SSLContext New Handler to add TLS SNI Extension - pipeline.addLast(ChannelPipelineCustomizer.HANDLER_SSL, sslCtx.newHandler(ch.alloc(), host, port)); - // We must wait for the handshake to finish and the protocol to be negotiated before configuring - // the HTTP/2 components of the pipeline. - pipeline.addLast( - ChannelPipelineCustomizer.HANDLER_HTTP2_PROTOCOL_NEGOTIATOR, - new ApplicationProtocolNegotiationHandler(ApplicationProtocolNames.HTTP_2) { - - @Override - public void handlerRemoved(ChannelHandlerContext ctx) { - // the logic to send the request should only be executed once the HTTP/2 - // Connection Preface request has been sent. Once the Preface has been sent and - // removed then this handler is removed so we invoke the remaining logic once - // this handler removed - final Consumer contextConsumer = - httpClientInitializer.contextConsumer; - if (contextConsumer != null) { - contextConsumer.accept(ctx); - } - } - - @Override - protected void configurePipeline(ChannelHandlerContext ctx, String protocol) { - if (ApplicationProtocolNames.HTTP_2.equals(protocol)) { - ChannelPipeline p = ctx.pipeline(); - if (httpClientInitializer.stream) { - // stream consumer manages backpressure and reads - ctx.channel().config().setAutoRead(false); - } - p.addLast( - ChannelPipelineCustomizer.HANDLER_HTTP2_SETTINGS, - new Http2SettingsHandler(ch.newPromise()) - ); - httpClientInitializer.addEventStreamHandlerIfNecessary(p); - httpClientInitializer.addFinalHandler(p); - for (ChannelPipelineListener pipelineListener : pipelineListeners) { - pipelineListener.onConnect(p); - } - } else if (ApplicationProtocolNames.HTTP_1_1.equals(protocol)) { - ChannelPipeline p = ctx.pipeline(); - httpClientInitializer.addHttp1Handlers(p); - } else { - ctx.close(); - throw customizeException(new HttpClientException("Unknown Protocol: " + protocol)); - } - httpClientInitializer.onStreamPipelineBuilt(); - } - }); - - pipeline.addLast(ChannelPipelineCustomizer.HANDLER_HTTP2_CONNECTION, connectionHandler); - } - - /** - * Configures HTTP/2 handling for plaintext (non-SSL) connections. - * - * @param httpClientInitializer The client initializer - * @param ch The channel - * @param connectionHandler The connection handler - */ - private void configureHttp2ClearText( - HttpClientInitializer httpClientInitializer, - @NonNull SocketChannel ch, - @NonNull HttpToHttp2ConnectionHandler connectionHandler) { - HttpClientCodec sourceCodec = new HttpClientCodec(); - Http2ClientUpgradeCodec upgradeCodec = new Http2ClientUpgradeCodec(ChannelPipelineCustomizer.HANDLER_HTTP2_CONNECTION, connectionHandler); - HttpClientUpgradeHandler upgradeHandler = new HttpClientUpgradeHandler(sourceCodec, upgradeCodec, 65536); - - final ChannelPipeline pipeline = ch.pipeline(); - pipeline.addLast(ChannelPipelineCustomizer.HANDLER_HTTP_CLIENT_CODEC, sourceCodec); - httpClientInitializer.settingsHandler = new Http2SettingsHandler(ch.newPromise()); - pipeline.addLast(upgradeHandler); - pipeline.addLast(new ChannelInboundHandlerAdapter() { - @Override - public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception { - ctx.fireUserEventTriggered(evt); - if (evt instanceof HttpClientUpgradeHandler.UpgradeEvent) { - httpClientInitializer.onStreamPipelineBuilt(); - ctx.pipeline().remove(this); - } - } - }); - pipeline.addLast(ChannelPipelineCustomizer.HANDLER_HTTP2_UPGRADE_REQUEST, new H2cUpgradeRequestHandler(httpClientInitializer)); - } - - /** - * Creates a new {@link HttpToHttp2ConnectionHandlerBuilder} for the given HTTP/2 connection object and config. - * - * @param connection The connection - * @param configuration The configuration - * @param stream Whether this is a stream request - * @return The {@link HttpToHttp2ConnectionHandlerBuilder} - */ - @NonNull - private static HttpToHttp2ConnectionHandlerBuilder newHttp2ConnectionHandlerBuilder( - @NonNull Http2Connection connection, @NonNull HttpClientConfiguration configuration, boolean stream) { - final HttpToHttp2ConnectionHandlerBuilder builder = new HttpToHttp2ConnectionHandlerBuilder(); - builder.validateHeaders(true); - final Http2FrameListener http2ToHttpAdapter; - - if (!stream) { - http2ToHttpAdapter = new InboundHttp2ToHttpAdapterBuilder(connection) - .maxContentLength(configuration.getMaxContentLength()) - .validateHttpHeaders(true) - .propagateSettings(true) - .build(); - - } else { - http2ToHttpAdapter = new StreamingInboundHttp2ToHttpAdapter( - connection, - configuration.getMaxContentLength() - ); - } - return builder - .connection(connection) - .frameListener(new DelegatingDecompressorFrameListener( - connection, - http2ToHttpAdapter)); - - } - private void configureProxy(ChannelPipeline pipeline, boolean secure, String host, int port) { Proxy proxy = configuration.resolveProxy(secure, host, port); if (Proxy.NO_PROXY.equals(proxy)) { @@ -898,10 +472,11 @@ private void configureProxy(ChannelPipeline pipeline, boolean secure, String hos } } - > Future addInstrumentedListener( + > void addInstrumentedListener( Future channelFuture, GenericFutureListener listener) { - return channelFuture.addListener(f -> { + channelFuture.addListener(f -> { try (Instrumentation ignored = instrumenter.newInstrumentation()) { + //noinspection unchecked listener.operationComplete((C) f); } }); @@ -912,423 +487,758 @@ private E customizeException(E exc) { return exc; } - private void addReadTimeoutHandler(ChannelPipeline pipeline) { - if (readTimeoutMillis != null) { - if (httpVersion == HttpVersion.HTTP_2_0) { - pipeline.addBefore( - ChannelPipelineCustomizer.HANDLER_HTTP2_CONNECTION, - ChannelPipelineCustomizer.HANDLER_READ_TIMEOUT, - new ReadTimeoutHandler(readTimeoutMillis, TimeUnit.MILLISECONDS) - ); - } else { - pipeline.addBefore( - ChannelPipelineCustomizer.HANDLER_HTTP_CLIENT_CODEC, - ChannelPipelineCustomizer.HANDLER_READ_TIMEOUT, - new ReadTimeoutHandler(readTimeoutMillis, TimeUnit.MILLISECONDS)); + private Http2FrameCodec makeFrameCodec() { + Http2FrameCodecBuilder builder = Http2FrameCodecBuilder.forClient(); + configuration.getLogLevel().ifPresent(logLevel -> { + try { + final io.netty.handler.logging.LogLevel nettyLevel = + io.netty.handler.logging.LogLevel.valueOf(logLevel.name()); + builder.frameLogger(new Http2FrameLogger(nettyLevel, DefaultHttpClient.class)); + } catch (IllegalArgumentException e) { + throw customizeException(new HttpClientException("Unsupported log level: " + logLevel)); } - } + }); + return builder.build(); } - private void removeReadTimeoutHandler(ChannelPipeline pipeline) { - if (readTimeoutMillis != null && pipeline.context(ChannelPipelineCustomizer.HANDLER_READ_TIMEOUT) != null) { - pipeline.remove(ChannelPipelineCustomizer.HANDLER_READ_TIMEOUT); - } + /** + * Initializer for HTTP1.1, called either in plaintext mode, or after ALPN in TLS. + * + * @param ch The plaintext channel + */ + private void initHttp1(Channel ch) { + addLogHandler(ch); + + ch.pipeline() + .addLast(ChannelPipelineCustomizer.HANDLER_HTTP_CLIENT_CODEC, new HttpClientCodec()) + .addLast(ChannelPipelineCustomizer.HANDLER_HTTP_DECODER, new HttpContentDecompressor()); + } + + private void addLogHandler(Channel ch) { + configuration.getLogLevel().ifPresent(logLevel -> { + try { + final io.netty.handler.logging.LogLevel nettyLevel = + io.netty.handler.logging.LogLevel.valueOf(logLevel.name()); + ch.pipeline().addLast(new LoggingHandler(DefaultHttpClient.class, nettyLevel)); + } catch (IllegalArgumentException e) { + throw customizeException(new HttpClientException("Unsupported log level: " + logLevel)); + } + }); } /** - * A handler that triggers the cleartext upgrade to HTTP/2 by sending an initial HTTP request. + * Initializer for HTTP2 multiplexing, called either in h2c mode, or after ALPN in TLS. The + * channel should already contain a {@link #makeFrameCodec() frame codec} that does the HTTP2 + * parsing, this method adds the handlers that do multiplexing, error handling, etc. + * + * @param pool The pool to add the connection to once the handshake is done + * @param ch The plaintext channel + * @param connectionCustomizer Customizer for the connection */ - private class H2cUpgradeRequestHandler extends ChannelInboundHandlerAdapter { + private void initHttp2(Pool pool, Channel ch, NettyClientCustomizer connectionCustomizer) { + Http2MultiplexHandler multiplexHandler = new Http2MultiplexHandler(new ChannelInitializer() { + @Override + protected void initChannel(@NonNull Http2StreamChannel ch) throws Exception { + log.warn("Server opened HTTP2 stream {}, closing immediately", ch.stream().id()); + ch.close(); + } + }, new ChannelInitializer() { + @Override + protected void initChannel(@NonNull Http2StreamChannel ch) throws Exception { + // discard any response data for the upgrade request + ch.close(); + } + }); + ch.pipeline().addLast(multiplexHandler); + ch.pipeline().addLast(ChannelPipelineCustomizer.HANDLER_HTTP2_SETTINGS, new ChannelInboundHandlerAdapter() { + @Override + public void channelRead(@NonNull ChannelHandlerContext ctx, @NonNull Object msg) throws Exception { + if (msg instanceof Http2SettingsFrame) { + ctx.pipeline().remove(ChannelPipelineCustomizer.HANDLER_HTTP2_SETTINGS); + ctx.pipeline().remove(ChannelPipelineCustomizer.HANDLER_INITIAL_ERROR); + pool.new Http2ConnectionHolder(ch, connectionCustomizer).init(); + return; + } else { + log.warn("Premature frame: {}", msg.getClass()); + } + + super.channelRead(ctx, msg); + } + }); + // stream frames should be handled by the multiplexer + ch.pipeline().addLast(new ChannelInboundHandlerAdapter() { + @Override + public void handlerAdded(ChannelHandlerContext ctx) throws Exception { + ctx.read(); + } - private final HttpClientInitializer initializer; + @Override + public void channelRead(@NonNull ChannelHandlerContext ctx, @NonNull Object msg) throws Exception { + log.warn("Unexpected message on HTTP2 connection channel: {}", msg); + ReferenceCountUtil.release(msg); + ctx.read(); + } + }); + } + + /** + * Initializer for TLS channels. After ALPN we will proceed either with + * {@link #initHttp1(Channel)} or {@link #initHttp2(Pool, Channel, NettyClientCustomizer)}. + */ + private final class AdaptiveAlpnChannelInitializer extends ChannelInitializer { + private final Pool pool; + + private final SslContext sslContext; + private final String host; + private final int port; + + AdaptiveAlpnChannelInitializer(Pool pool, + SslContext sslContext, + String host, + int port) { + this.pool = pool; + this.sslContext = sslContext; + this.host = host; + this.port = port; + } /** - * Default constructor. - * - * @param initializer The initializer + * @param ch The channel */ - public H2cUpgradeRequestHandler(HttpClientInitializer initializer) { - this.initializer = initializer; + @Override + protected void initChannel(@NonNull Channel ch) { + NettyClientCustomizer channelCustomizer = clientCustomizer.specializeForChannel(ch, NettyClientCustomizer.ChannelRole.CONNECTION); + + configureProxy(ch.pipeline(), true, host, port); + + SslHandler sslHandler = sslContext.newHandler(ch.alloc(), host, port); + sslHandler.setHandshakeTimeoutMillis(configuration.getSslConfiguration().getHandshakeTimeout().toMillis()); + ch.pipeline() + .addLast(ChannelPipelineCustomizer.HANDLER_SSL, sslHandler) + .addLast( + ChannelPipelineCustomizer.HANDLER_HTTP2_PROTOCOL_NEGOTIATOR, + // if the server doesn't do ALPN, fall back to HTTP 1 + new ApplicationProtocolNegotiationHandler(ApplicationProtocolNames.HTTP_1_1) { + @Override + protected void configurePipeline(ChannelHandlerContext ctx, String protocol) { + if (ApplicationProtocolNames.HTTP_2.equals(protocol)) { + ctx.pipeline().addLast(ChannelPipelineCustomizer.HANDLER_HTTP2_CONNECTION, makeFrameCodec()); + initHttp2(pool, ctx.channel(), channelCustomizer); + } else if (ApplicationProtocolNames.HTTP_1_1.equals(protocol)) { + initHttp1(ctx.channel()); + pool.new Http1ConnectionHolder(ch, channelCustomizer).init(false); + ctx.pipeline().remove(ChannelPipelineCustomizer.HANDLER_INITIAL_ERROR); + } else { + ctx.close(); + throw customizeException(new HttpClientException("Unknown Protocol: " + protocol)); + } + } + + @Override + public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception { + if (evt instanceof SslHandshakeCompletionEvent) { + SslHandshakeCompletionEvent event = (SslHandshakeCompletionEvent) evt; + if (!event.isSuccess()) { + InitialConnectionErrorHandler.setFailureCause(ctx.channel(), event.cause()); + } + } + super.userEventTriggered(ctx, evt); + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { + // let the HANDLER_INITIAL_ERROR handle the failure + if (cause instanceof DecoderException && cause.getCause() instanceof SSLException) { + // unwrap DecoderException + cause = cause.getCause(); + } + ctx.fireExceptionCaught(cause); + } + }) + .addLast(ChannelPipelineCustomizer.HANDLER_INITIAL_ERROR, pool.initialErrorHandler); + + channelCustomizer.onInitialPipelineBuilt(); } + } - @Override - public void channelActive(ChannelHandlerContext ctx) { - // Done with this handler, remove it from the pipeline. - final ChannelPipeline pipeline = ctx.pipeline(); - - pipeline.addLast(ChannelPipelineCustomizer.HANDLER_HTTP2_SETTINGS, initializer.settingsHandler); - DefaultFullHttpRequest upgradeRequest = - new DefaultFullHttpRequest(io.netty.handler.codec.http.HttpVersion.HTTP_1_1, HttpMethod.GET, "/", Unpooled.EMPTY_BUFFER); - - // Set HOST header as the remote peer may require it. - InetSocketAddress remote = (InetSocketAddress) ctx.channel().remoteAddress(); - String hostString = remote.getHostString(); - if (hostString == null) { - hostString = remote.getAddress().getHostAddress(); - } - upgradeRequest.headers().set(HttpHeaderNames.HOST, hostString + ':' + remote.getPort()); - ctx.writeAndFlush(upgradeRequest); + /** + * Initializer for H2C connections. Will proceed with + * {@link #initHttp2(Pool, Channel, NettyClientCustomizer)} when the upgrade is done. + */ + private final class Http2UpgradeInitializer extends ChannelInitializer { + private final Pool pool; - ctx.fireChannelActive(); - if (initializer.contextConsumer != null) { - initializer.contextConsumer.accept(ctx); - } - initializer.addFinalHandler(pipeline); + Http2UpgradeInitializer(Pool pool) { + this.pool = pool; } @Override - public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { - if (msg instanceof HttpMessage) { - int streamId = ((HttpMessage) msg).headers().getInt(HttpConversionUtil.ExtensionHeaderNames.STREAM_ID.text(), -1); - if (streamId == 1) { - // ignore this message - if (log.isDebugEnabled()) { - log.debug("Received response on HTTP2 stream 1, the stream used to respond to the initial upgrade request. Ignoring."); - } - ReferenceCountUtil.release(msg); - if (msg instanceof LastHttpContent) { - ctx.pipeline().remove(this); + protected void initChannel(@NonNull Channel ch) throws Exception { + NettyClientCustomizer connectionCustomizer = clientCustomizer.specializeForChannel(ch, NettyClientCustomizer.ChannelRole.CONNECTION); + + Http2FrameCodec frameCodec = makeFrameCodec(); + + HttpClientCodec sourceCodec = new HttpClientCodec(); + Http2ClientUpgradeCodec upgradeCodec = new Http2ClientUpgradeCodec(frameCodec, + new ChannelInitializer() { + @Override + protected void initChannel(@NonNull Channel ch) throws Exception { + ch.pipeline().addLast(ChannelPipelineCustomizer.HANDLER_HTTP2_CONNECTION, frameCodec); + initHttp2(pool, ch, connectionCustomizer); } - return; + }); + HttpClientUpgradeHandler upgradeHandler = new HttpClientUpgradeHandler(sourceCodec, upgradeCodec, 65536); + + ch.pipeline().addLast(ChannelPipelineCustomizer.HANDLER_HTTP_CLIENT_CODEC, sourceCodec); + ch.pipeline().addLast(upgradeHandler); + + ch.pipeline().addLast(ChannelPipelineCustomizer.HANDLER_HTTP2_UPGRADE_REQUEST, new ChannelInboundHandlerAdapter() { + @Override + public void channelActive(@NonNull ChannelHandlerContext ctx) throws Exception { + DefaultFullHttpRequest upgradeRequest = + new DefaultFullHttpRequest(io.netty.handler.codec.http.HttpVersion.HTTP_1_1, HttpMethod.GET, "/", Unpooled.EMPTY_BUFFER); + + // Set HOST header as the remote peer may require it. + upgradeRequest.headers().set(HttpHeaderNames.HOST, pool.requestKey.getHost() + ':' + pool.requestKey.getPort()); + ctx.writeAndFlush(upgradeRequest); + ctx.pipeline().remove(ChannelPipelineCustomizer.HANDLER_HTTP2_UPGRADE_REQUEST); + // read the upgrade response + ctx.read(); + + super.channelActive(ctx); } - } + }); + ch.pipeline().addLast(ChannelPipelineCustomizer.HANDLER_INITIAL_ERROR, pool.initialErrorHandler); - super.channelRead(ctx, msg); + connectionCustomizer.onInitialPipelineBuilt(); } } /** - * Reads the first {@link Http2Settings} object and notifies a {@link io.netty.channel.ChannelPromise}. + * Handle for a pooled connection. One pool handle generally corresponds to one request, and + * once the request and response are done, the handle is {@link #release() released} and a new + * request can claim the same connection. */ - private class Http2SettingsHandler extends - SimpleChannelInboundHandlerInstrumented { - final ChannelPromise promise; + abstract static class PoolHandle { + private static final Supplier> LEAK_DETECTOR = SupplierUtil.memoized(() -> + ResourceLeakDetectorFactory.instance().newResourceLeakDetector(PoolHandle.class)); - /** - * Create new instance. - * - * @param promise Promise object used to notify when first settings are received - */ - Http2SettingsHandler(ChannelPromise promise) { - super(instrumenter); - this.promise = promise; - } + final boolean http2; + final Channel channel; - @Override - protected void channelReadInstrumented(ChannelHandlerContext ctx, Http2Settings msg) { - promise.setSuccess(); + boolean released = false; - // Only care about the first settings message - ctx.pipeline().remove(this); - } + private final ResourceLeakTracker tracker = LEAK_DETECTOR.get().track(this); - @Override - public void channelInactive(ChannelHandlerContext ctx) throws Exception { - super.channelInactive(ctx); - if (!promise.isDone()) { - promise.tryFailure(new HttpClientException("Channel became inactive before settings frame was received")); - } + private PoolHandle(boolean http2, Channel channel) { + this.http2 = http2; + this.channel = channel; } - @Override - public void handlerRemoved(ChannelHandlerContext ctx) throws Exception { - super.handlerRemoved(ctx); - if (!promise.isDone()) { - promise.tryFailure(new HttpClientException("Handler was removed before settings frame was received")); + /** + * Prevent this connection from being reused, e.g. because garbage was written because of + * an error. + */ + abstract void taint(); + + /** + * Close this connection or release it back to the pool. + */ + void release() { + if (released) { + throw new IllegalStateException("Already released"); + } + released = true; + if (tracker != null) { + tracker.close(this); } } + + /** + * Whether this connection may be returned to a connection pool (i.e. should be kept + * keepalive). + * + * @return Whether this connection may be reused + */ + abstract boolean canReturn(); + + /** + * Notify any {@link NettyClientCustomizer} that the request pipeline has been built. + */ + abstract void notifyRequestPipelineBuilt(); } /** - * Initializes the HTTP client channel. + * This class represents one pool, and matches to exactly one + * {@link io.micronaut.http.client.netty.DefaultHttpClient.RequestKey} (i.e. host, port and + * protocol are the same for one pool). + *

+ * The superclass {@link PoolResizer} handles pool size management, this class just implements + * the HTTP parts. */ - private class HttpClientInitializer extends ChannelInitializer { - - final SslContext sslContext; - final String host; - final int port; - final boolean stream; - final boolean proxy; - final boolean acceptsEvents; - Http2SettingsHandler settingsHandler; - final Consumer contextConsumer; - private NettyClientCustomizer channelCustomizer; + private final class Pool extends PoolResizer { + private final DefaultHttpClient.RequestKey requestKey; /** - * @param sslContext The ssl context - * @param host The host - * @param port The port - * @param stream Whether is stream - * @param proxy Is this a streaming proxy - * @param acceptsEvents Whether an event stream is accepted - * @param contextConsumer The context consumer + * {@link ChannelHandler} that is added to a connection to report failures during + * handshakes. It's removed once the connection is established and processes requests. */ - protected HttpClientInitializer(SslContext sslContext, - String host, - int port, - boolean stream, - boolean proxy, - boolean acceptsEvents, - Consumer contextConsumer) { - this.sslContext = sslContext; - this.stream = stream; - this.host = host; - this.port = port; - this.proxy = proxy; - this.acceptsEvents = acceptsEvents; - this.contextConsumer = contextConsumer; + private final InitialConnectionErrorHandler initialErrorHandler = new InitialConnectionErrorHandler() { + @Override + protected void onNewConnectionFailure(@Nullable Throwable cause) throws Exception { + Pool.this.onNewConnectionFailure(cause); + } + }; + + Pool(DefaultHttpClient.RequestKey requestKey) { + super(log, configuration.getConnectionPoolConfiguration()); + this.requestKey = requestKey; + } + + Mono acquire() { + Sinks.One sink = new CancellableMonoSink<>(); + addPendingRequest(sink); + Optional acquireTimeout = configuration.getConnectionPoolConfiguration().getAcquireTimeout(); + //noinspection OptionalIsPresent + if (acquireTimeout.isPresent()) { + return sink.asMono().timeout(acquireTimeout.get(), Schedulers.fromExecutor(group)); + } else { + return sink.asMono(); + } } - /** - * @param ch The channel - */ @Override - protected void initChannel(SocketChannel ch) { - channelCustomizer = clientCustomizer.specializeForChannel(ch, NettyClientCustomizer.ChannelRole.CONNECTION); - ch.attr(CHANNEL_CUSTOMIZER_KEY).set(channelCustomizer); - - ChannelPipeline p = ch.pipeline(); - - configureProxy(p, sslContext != null, host, port); - - if (httpVersion == HttpVersion.HTTP_2_0) { - final Http2Connection connection = new DefaultHttp2Connection(false); - final HttpToHttp2ConnectionHandlerBuilder builder = - newHttp2ConnectionHandlerBuilder(connection, configuration, stream); - - configuration.getLogLevel().ifPresent(logLevel -> { - try { - final io.netty.handler.logging.LogLevel nettyLevel = io.netty.handler.logging.LogLevel.valueOf( - logLevel.name() - ); - builder.frameLogger(new Http2FrameLogger(nettyLevel, DefaultHttpClient.class)); - } catch (IllegalArgumentException e) { - throw customizeException(new HttpClientException("Unsupported log level: " + logLevel)); - } - }); - HttpToHttp2ConnectionHandler connectionHandler = builder - .build(); - if (sslContext != null) { - configureHttp2Ssl(this, ch, sslContext, host, port, connectionHandler); + void onNewConnectionFailure(@Nullable Throwable error) throws Exception { + super.onNewConnectionFailure(error); + // to avoid an infinite loop, fail one pending request. + Sinks.One pending = pollPendingRequest(); + if (pending != null) { + HttpClientException wrapped; + if (error == null) { + // no failure observed, but channel closed + wrapped = new HttpClientException("Unknown connect error"); } else { - configureHttp2ClearText(this, ch, connectionHandler); + wrapped = new HttpClientException("Connect Error: " + error.getMessage(), error); } - channelCustomizer.onInitialPipelineBuilt(); - } else { - if (stream) { - // for streaming responses we disable auto read - // so that the consumer is in charge of back pressure - ch.config().setAutoRead(false); - } - - configuration.getLogLevel().ifPresent(logLevel -> { - try { - final io.netty.handler.logging.LogLevel nettyLevel = io.netty.handler.logging.LogLevel.valueOf( - logLevel.name() - ); - p.addLast(new LoggingHandler(DefaultHttpClient.class, nettyLevel)); - } catch (IllegalArgumentException e) { - throw customizeException(new HttpClientException("Unsupported log level: " + logLevel)); - } - }); - - if (sslContext != null) { - SslHandler sslHandler = sslContext.newHandler(ch.alloc(), host, port); - sslHandler.setHandshakeTimeoutMillis(configuration.getSslConfiguration().getHandshakeTimeout().toMillis()); - p.addLast(ChannelPipelineCustomizer.HANDLER_SSL, sslHandler); + if (pending.tryEmitError(customizeException(wrapped)) == Sinks.EmitResult.OK) { + // no need to log + return; } + } + log.error("Failed to connect to remote", error); + } - // Pool connections require alternative timeout handling - if (poolMap == null) { - // read timeout settings are not applied to streamed requests. - // instead idle timeout settings are applied. - if (stream) { - Optional readIdleTime = configuration.getReadIdleTimeout(); - if (readIdleTime.isPresent()) { - Duration duration = readIdleTime.get(); - if (!duration.isNegative()) { - p.addLast(ChannelPipelineCustomizer.HANDLER_IDLE_STATE, new IdleStateHandler( - duration.toMillis(), - duration.toMillis(), - duration.toMillis(), - TimeUnit.MILLISECONDS - )); + @Override + void openNewConnection() { + // open a new connection + ChannelInitializer initializer; + if (requestKey.isSecure()) { + initializer = new AdaptiveAlpnChannelInitializer( + this, + buildSslContext(requestKey), + requestKey.getHost(), + requestKey.getPort() + ); + } else { + switch (httpVersion.getPlaintextMode()) { + case HTTP_1: + initializer = new ChannelInitializer() { + @Override + protected void initChannel(@NonNull Channel ch) throws Exception { + configureProxy(ch.pipeline(), false, requestKey.getHost(), requestKey.getPort()); + initHttp1(ch); + ch.pipeline().addLast(ChannelPipelineCustomizer.HANDLER_ACTIVITY_LISTENER, new ChannelInboundHandlerAdapter() { + @Override + public void channelActive(@NonNull ChannelHandlerContext ctx) throws Exception { + super.channelActive(ctx); + ctx.pipeline().remove(this); + NettyClientCustomizer channelCustomizer = clientCustomizer.specializeForChannel(ch, NettyClientCustomizer.ChannelRole.CONNECTION); + new Http1ConnectionHolder(ch, channelCustomizer).init(true); + } + }); } - } - } + }; + break; + case H2C: + initializer = new Http2UpgradeInitializer(this); + break; + default: + throw new AssertionError("Unknown plaintext mode"); } - - addHttp1Handlers(p); - channelCustomizer.onInitialPipelineBuilt(); - onStreamPipelineBuilt(); } + addInstrumentedListener(doConnect(requestKey, initializer), future -> { + if (!future.isSuccess()) { + onNewConnectionFailure(future.cause()); + } + }); } - /** - * Called when the stream pipeline is fully set up (all handshakes completed) and we can - * start processing requests. - */ - void onStreamPipelineBuilt() { - channelCustomizer.onStreamPipelineBuilt(); + public void shutdown() { + forEachConnection(c -> ((ConnectionHolder) c).channel.close()); } - void addHttp1Handlers(ChannelPipeline p) { - p.addLast(ChannelPipelineCustomizer.HANDLER_HTTP_CLIENT_CODEC, new HttpClientCodec()); + /** + * Base class for one HTTP1/HTTP2 connection. + */ + abstract class ConnectionHolder extends ResizerConnection { + final Channel channel; + final NettyClientCustomizer connectionCustomizer; + /** + * Future for the scheduled task that runs when the configured time-to-live for the + * connection passes. + */ + @Nullable + ScheduledFuture ttlFuture; + volatile boolean windDownConnection = false; + + ConnectionHolder(Channel channel, NettyClientCustomizer connectionCustomizer) { + this.channel = channel; + this.connectionCustomizer = connectionCustomizer; + } - p.addLast(ChannelPipelineCustomizer.HANDLER_HTTP_DECODER, new HttpContentDecompressor()); + /** + * Add connection-level timeout-related handlers to the channel + * (read timeout, TTL, ...). + * + * @param before Reference handler name, the timeout handlers will be placed before + * this handler. + */ + final void addTimeoutHandlers(String before) { + // read timeout handles timeouts *during* a request + configuration.getReadTimeout() + .ifPresent(dur -> channel.pipeline().addBefore(before, ChannelPipelineCustomizer.HANDLER_READ_TIMEOUT, new ReadTimeoutHandler(dur.toNanos(), TimeUnit.NANOSECONDS) { + @Override + protected void readTimedOut(ChannelHandlerContext ctx) { + if (hasLiveRequests()) { + fireReadTimeout(ctx); + ctx.close(); + } + } + })); + // pool idle timeout happens *outside* a request + configuration.getConnectionPoolIdleTimeout() + .ifPresent(dur -> channel.pipeline().addBefore(before, ChannelPipelineCustomizer.HANDLER_IDLE_STATE, new ReadTimeoutHandler(dur.toNanos(), TimeUnit.NANOSECONDS) { + @Override + protected void readTimedOut(ChannelHandlerContext ctx) { + if (!hasLiveRequests()) { + ctx.close(); + } + } + })); + configuration.getConnectTtl().ifPresent(ttl -> + ttlFuture = channel.eventLoop().schedule(this::windDownConnection, ttl.toNanos(), TimeUnit.NANOSECONDS)); + channel.pipeline().addBefore(before, "connection-cleaner", new ChannelInboundHandlerAdapter() { + boolean inactiveCalled = false; - int maxContentLength = configuration.getMaxContentLength(); + @Override + public void channelInactive(@NonNull ChannelHandlerContext ctx) throws Exception { + super.channelInactive(ctx); + if (!inactiveCalled) { + inactiveCalled = true; + onInactive(); + } + } - if (!stream) { - p.addLast(ChannelPipelineCustomizer.HANDLER_HTTP_AGGREGATOR, new HttpObjectAggregator(maxContentLength) { @Override - protected void finishAggregation(FullHttpMessage aggregated) throws Exception { - if (!HttpUtil.isContentLengthSet(aggregated)) { - if (aggregated.content().readableBytes() > 0) { - super.finishAggregation(aggregated); - } + public void handlerRemoved(ChannelHandlerContext ctx) { + if (!inactiveCalled) { + inactiveCalled = true; + onInactive(); } } }); } - addEventStreamHandlerIfNecessary(p); - addFinalHandler(p); - for (ChannelPipelineListener pipelineListener : pipelineListeners) { - pipelineListener.onConnect(p); + + /** + * Stop accepting new requests on this connection, but finish up the running requests + * if possible. + */ + void windDownConnection() { + windDownConnection = true; + } + + /** + * Send the finished pool handle to the given requester, if possible. + * + * @param sink The request for a pool handle + * @param ph The pool handle + */ + final void emitPoolHandle(Sinks.One sink, PoolHandle ph) { + Sinks.EmitResult emitResult = sink.tryEmitValue(ph); + if (emitResult.isFailure()) { + ph.release(); + } else { + if (!configuration.getConnectionPoolConfiguration().isEnabled()) { + // if pooling is off, release the connection after this. + windDownConnection(); + } + } + } + + @Override + public boolean dispatch(Sinks.One sink) { + if (!tryEarmarkForRequest()) { + return false; + } + + if (channel.eventLoop().inEventLoop()) { + dispatch0(sink); + } else { + channel.eventLoop().execute(() -> dispatch0(sink)); + } + return true; + } + + /** + * Called on event loop only. Dispatch a stream/connection to the given pool + * handle request. + * + * @param sink The request for a pool handle + */ + abstract void dispatch0(Sinks.One sink); + + /** + * Try to add a new request to this connection. This is called outside the event loop, + * and if this succeeds, we will proceed with a {@link #dispatch0} call on the + * event loop. + * + * @return {@code true} if the request may be added to this connection + */ + abstract boolean tryEarmarkForRequest(); + + /** + * @return {@code true} iff there are any requests running on this connection. + */ + abstract boolean hasLiveRequests(); + + /** + * Send a read timeout exception to all requests on this connection. + * + * @param ctx The connection-level channel handler context to use. + */ + abstract void fireReadTimeout(ChannelHandlerContext ctx); + + /** + * Called when the connection becomes inactive, i.e. on disconnect. + */ + void onInactive() { + if (ttlFuture != null) { + ttlFuture.cancel(false); + } + windDownConnection = true; } } - void addEventStreamHandlerIfNecessary(ChannelPipeline p) { - // if the content type is a SSE event stream we add a decoder - // to delimit the content by lines (unless we are proxying the stream) - if (acceptsEventStream() && !proxy) { - p.addLast(ChannelPipelineCustomizer.HANDLER_MICRONAUT_SSE_EVENT_STREAM, new LineBasedFrameDecoder(configuration.getMaxContentLength(), true, true) { + final class Http1ConnectionHolder extends ConnectionHolder { + private final AtomicBoolean hasLiveRequest = new AtomicBoolean(false); + + Http1ConnectionHolder(Channel channel, NettyClientCustomizer connectionCustomizer) { + super(channel, connectionCustomizer); + } + + void init(boolean fireInitialPipelineBuilt) { + addTimeoutHandlers( + requestKey.isSecure() ? + ChannelPipelineCustomizer.HANDLER_SSL : + ChannelPipelineCustomizer.HANDLER_HTTP_CLIENT_CODEC + ); + + if (fireInitialPipelineBuilt) { + connectionCustomizer.onInitialPipelineBuilt(); + } + connectionCustomizer.onStreamPipelineBuilt(); + + onNewConnectionEstablished1(this); + } + + @Override + boolean tryEarmarkForRequest() { + return !windDownConnection && hasLiveRequest.compareAndSet(false, true); + } + + @Override + boolean hasLiveRequests() { + return hasLiveRequest.get(); + } + + @Override + void fireReadTimeout(ChannelHandlerContext ctx) { + ctx.fireExceptionCaught(ReadTimeoutException.INSTANCE); + } + + @Override + void dispatch0(Sinks.One sink) { + if (!channel.isActive()) { + returnPendingRequest(sink); + return; + } + PoolHandle ph = new PoolHandle(false, channel) { + final ChannelHandlerContext lastContext = channel.pipeline().lastContext(); @Override - public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { - if (msg instanceof HttpContent) { - if (msg instanceof LastHttpContent) { - super.channelRead(ctx, msg); - } else { - Attribute streamKey = ctx.channel().attr(STREAM_KEY); - if (msg instanceof Http2Content) { - streamKey.set(((Http2Content) msg).stream()); - } - try { - super.channelRead(ctx, ((HttpContent) msg).content()); - } finally { - streamKey.set(null); - } + void taint() { + windDownConnection = true; + } + + @Override + void release() { + super.release(); + if (!windDownConnection) { + ChannelHandlerContext newLast = channel.pipeline().lastContext(); + if (lastContext != newLast) { + log.warn("BUG - Handler not removed: {}", newLast); + taint(); } + } + if (!windDownConnection) { + hasLiveRequest.set(false); + markConnectionAvailable(); } else { - super.channelRead(ctx, msg); + channel.close(); } } - }); - - p.addLast(ChannelPipelineCustomizer.HANDLER_MICRONAUT_SSE_CONTENT, new SimpleChannelInboundHandlerInstrumented(instrumenter, false) { @Override - public boolean acceptInboundMessage(Object msg) { - return msg instanceof ByteBuf; + boolean canReturn() { + return !windDownConnection; } @Override - protected void channelReadInstrumented(ChannelHandlerContext ctx, ByteBuf msg) { - try { - Attribute streamKey = ctx.channel().attr(STREAM_KEY); - Http2Stream http2Stream = streamKey.get(); - if (http2Stream != null) { - ctx.fireChannelRead(new DefaultHttp2Content(msg.copy(), http2Stream)); - } else { - ctx.fireChannelRead(new DefaultHttpContent(msg.copy())); - } - } finally { - msg.release(); - } + void notifyRequestPipelineBuilt() { + connectionCustomizer.onRequestPipelineBuilt(); } - }); + }; + emitPoolHandle(sink, ph); + } + private void returnPendingRequest(Sinks.One sink) { + // failed, but the pending request may still work on another connection. + addPendingRequest(sink); + hasLiveRequest.set(false); } - } - /** - * Allows overriding the final handler added to the pipeline. - * - * @param pipeline The pipeline - */ - protected void addFinalHandler(ChannelPipeline pipeline) { - pipeline.addLast( - ChannelPipelineCustomizer.HANDLER_HTTP_STREAM, - new HttpStreamsClientHandler() { - @Override - public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception { - if (evt instanceof IdleStateEvent) { - // close the connection if it is idle for too long - ctx.close(); - } - super.userEventTriggered(ctx, evt); + @Override + void windDownConnection() { + super.windDownConnection(); + if (!hasLiveRequest.get()) { + channel.close(); } - }); - } + } - private boolean acceptsEventStream() { - return this.acceptsEvents; + @Override + void onInactive() { + super.onInactive(); + onConnectionInactive1(this); + } } - } - final class PoolHandle { - final Channel channel; - private final ChannelPool channelPool; - private boolean canReturn; + final class Http2ConnectionHolder extends ConnectionHolder { + private final AtomicInteger liveRequests = new AtomicInteger(0); + private final Set liveStreamChannels = new HashSet<>(); // todo: https://github.com/netty/netty/pull/12830 - private PoolHandle(ChannelPool channelPool, Channel channel) { - this.channel = channel; - this.channelPool = channelPool; - this.canReturn = channelPool != null; - } + Http2ConnectionHolder(Channel channel, NettyClientCustomizer customizer) { + super(channel, customizer); + } - /** - * Prevent this connection from being reused. - */ - void taint() { - canReturn = false; - } + void init() { + addTimeoutHandlers( + requestKey.isSecure() ? + ChannelPipelineCustomizer.HANDLER_SSL : + ChannelPipelineCustomizer.HANDLER_HTTP2_CONNECTION + ); - /** - * Close this connection or release it back to the pool. - */ - void release() { - if (channelPool != null) { - removeReadTimeoutHandler(channel.pipeline()); - if (!canReturn) { - channel.closeFuture().addListener((future -> - channelPool.release(channel) - )); - } else { - channelPool.release(channel); + connectionCustomizer.onStreamPipelineBuilt(); + + onNewConnectionEstablished2(this); + } + + @Override + boolean tryEarmarkForRequest() { + return !windDownConnection && incrementWithLimit(liveRequests, configuration.getConnectionPoolConfiguration().getMaxConcurrentRequestsPerHttp2Connection()); + } + + @Override + boolean hasLiveRequests() { + return liveRequests.get() > 0; + } + + @Override + void fireReadTimeout(ChannelHandlerContext ctx) { + for (Channel sc : liveStreamChannels) { + sc.pipeline().fireExceptionCaught(ReadTimeoutException.INSTANCE); } - } else { - // just close it to prevent any future reads without a handler registered - channel.close(); } - } - /** - * Whether this connection may be returned to a connection pool (i.e. should be kept - * keepalive). - * - * @return Whether this connection may be reused - */ - public boolean canReturn() { - return canReturn; - } + @Override + void dispatch0(Sinks.One sink) { + if (!channel.isActive() || windDownConnection) { + returnPendingRequest(sink); + return; + } + addInstrumentedListener(new Http2StreamChannelBootstrap(channel).open(), (Future future) -> { + if (future.isSuccess()) { + Http2StreamChannel streamChannel = future.get(); + streamChannel.pipeline() + .addLast(new Http2StreamFrameToHttpObjectCodec(false)) + .addLast(ChannelPipelineCustomizer.HANDLER_HTTP_DECOMPRESSOR, new HttpContentDecompressor()); + NettyClientCustomizer streamCustomizer = connectionCustomizer.specializeForChannel(streamChannel, NettyClientCustomizer.ChannelRole.HTTP2_STREAM); + PoolHandle ph = new PoolHandle(true, streamChannel) { + @Override + void taint() { + // do nothing, we don't reuse stream channels + } - /** - * Notify any {@link NettyClientCustomizer} that the request pipeline has been built. - */ - void notifyRequestPipelineBuilt() { - channel.attr(CHANNEL_CUSTOMIZER_KEY).get().onRequestPipelineBuilt(); + @Override + void release() { + super.release(); + liveStreamChannels.remove(streamChannel); + streamChannel.close(); + int newCount = liveRequests.decrementAndGet(); + if (windDownConnection && newCount <= 0) { + Http2ConnectionHolder.this.channel.close(); + } else { + markConnectionAvailable(); + } + } + + @Override + boolean canReturn() { + return true; + } + + @Override + void notifyRequestPipelineBuilt() { + streamCustomizer.onRequestPipelineBuilt(); + } + }; + liveStreamChannels.add(streamChannel); + emitPoolHandle(sink, ph); + } else { + log.debug("Failed to open http2 stream", future.cause()); + returnPendingRequest(sink); + } + }); + } + + private void returnPendingRequest(Sinks.One sink) { + // failed, but the pending request may still work on another connection. + addPendingRequest(sink); + liveRequests.decrementAndGet(); + } + + @Override + void windDownConnection() { + super.windDownConnection(); + if (liveRequests.get() == 0) { + channel.close(); + } + } + + @Override + void onInactive() { + super.onInactive(); + onConnectionInactive2(this); + } } } } diff --git a/http-client/src/main/java/io/micronaut/http/client/netty/DefaultHttpClient.java b/http-client/src/main/java/io/micronaut/http/client/netty/DefaultHttpClient.java index 0990fbd7597..c1cdb016b40 100644 --- a/http-client/src/main/java/io/micronaut/http/client/netty/DefaultHttpClient.java +++ b/http-client/src/main/java/io/micronaut/http/client/netty/DefaultHttpClient.java @@ -47,6 +47,7 @@ import io.micronaut.http.client.DefaultHttpClientConfiguration; import io.micronaut.http.client.HttpClient; import io.micronaut.http.client.HttpClientConfiguration; +import io.micronaut.http.client.HttpVersionSelection; import io.micronaut.http.client.LoadBalancer; import io.micronaut.http.client.ProxyHttpClient; import io.micronaut.http.client.ProxyRequestOptions; @@ -80,8 +81,8 @@ import io.micronaut.http.netty.NettyHttpRequestBuilder; import io.micronaut.http.netty.NettyHttpResponseBuilder; import io.micronaut.http.netty.channel.ChannelPipelineCustomizer; -import io.micronaut.http.netty.channel.ChannelPipelineListener; import io.micronaut.http.netty.stream.DefaultStreamedHttpResponse; +import io.micronaut.http.netty.stream.HttpStreamsClientHandler; import io.micronaut.http.netty.stream.JsonSubscriber; import io.micronaut.http.netty.stream.StreamedHttpRequest; import io.micronaut.http.netty.stream.StreamedHttpResponse; @@ -112,6 +113,7 @@ import io.netty.channel.ChannelFactory; import io.netty.channel.ChannelFuture; import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; import io.netty.channel.ChannelPipeline; import io.netty.channel.EventLoopGroup; import io.netty.channel.MultithreadEventLoopGroup; @@ -122,16 +124,19 @@ import io.netty.handler.codec.http.DefaultHttpHeaders; import io.netty.handler.codec.http.DefaultLastHttpContent; import io.netty.handler.codec.http.EmptyHttpHeaders; +import io.netty.handler.codec.http.FullHttpMessage; import io.netty.handler.codec.http.FullHttpRequest; import io.netty.handler.codec.http.FullHttpResponse; import io.netty.handler.codec.http.HttpContent; import io.netty.handler.codec.http.HttpHeaderNames; import io.netty.handler.codec.http.HttpHeaderValues; import io.netty.handler.codec.http.HttpHeaders; +import io.netty.handler.codec.http.HttpObjectAggregator; import io.netty.handler.codec.http.HttpRequest; import io.netty.handler.codec.http.HttpResponseStatus; import io.netty.handler.codec.http.HttpScheme; import io.netty.handler.codec.http.HttpUtil; +import io.netty.handler.codec.http.LastHttpContent; import io.netty.handler.codec.http.multipart.DefaultHttpDataFactory; import io.netty.handler.codec.http.multipart.FileUpload; import io.netty.handler.codec.http.multipart.HttpDataFactory; @@ -144,6 +149,7 @@ import io.netty.util.CharsetUtil; import io.netty.util.ReferenceCountUtil; import io.netty.util.concurrent.DefaultThreadFactory; +import io.netty.util.concurrent.Future; import io.netty.util.concurrent.Promise; import org.reactivestreams.Processor; import org.reactivestreams.Publisher; @@ -172,6 +178,7 @@ import java.util.Collections; import java.util.List; import java.util.Map; +import java.util.NoSuchElementException; import java.util.Objects; import java.util.Optional; import java.util.concurrent.ThreadFactory; @@ -232,7 +239,7 @@ public class DefaultHttpClient implements protected MediaTypeCodecRegistry mediaTypeCodecRegistry; protected ByteBufferFactory byteBufferFactory = new NettyByteBufferFactory(); - final ConnectionManager connectionManager; + ConnectionManager connectionManager; private final List> clientFilterEntries; private final LoadBalancer loadBalancer; @@ -245,6 +252,7 @@ public class DefaultHttpClient implements private final RequestBinderRegistry requestBinderRegistry; private final List invocationInstrumenterFactories; private final String informationalServiceId; + private final ConversionService conversionService; /** * Construct a client for the given arguments. @@ -269,7 +277,7 @@ public DefaultHttpClient(@Nullable LoadBalancer loadBalancer, List invocationInstrumenterFactories, HttpClientFilter... filters) { this(loadBalancer, - configuration.getHttpVersion(), + null, configuration, contextPath, new DefaultHttpClientFilterResolver(annotationMetadataResolver, Arrays.asList(filters)), @@ -281,9 +289,10 @@ public DefaultHttpClient(@Nullable LoadBalancer loadBalancer, new DefaultRequestBinderRegistry(ConversionService.SHARED), null, NioSocketChannel::new, - Collections.emptySet(), CompositeNettyClientCustomizer.EMPTY, - invocationInstrumenterFactories, null); + invocationInstrumenterFactories, + null, + ConversionService.SHARED); } /** @@ -301,13 +310,12 @@ public DefaultHttpClient(@Nullable LoadBalancer loadBalancer, * @param requestBinderRegistry The request binder registry * @param eventLoopGroup The event loop group to use * @param socketChannelFactory The socket channel factory - * @param pipelineListeners The listeners to call for pipeline customization * @param clientCustomizer The pipeline customizer * @param invocationInstrumenterFactories The invocation instrumeter factories to instrument netty handlers execution with * @param informationalServiceId Optional service ID that will be passed to exceptions created by this client */ public DefaultHttpClient(@Nullable LoadBalancer loadBalancer, - @Nullable io.micronaut.http.HttpVersion explicitHttpVersion, + @Nullable HttpVersionSelection explicitHttpVersion, @NonNull HttpClientConfiguration configuration, @Nullable String contextPath, @NonNull HttpClientFilterResolver filterResolver, @@ -319,10 +327,10 @@ public DefaultHttpClient(@Nullable LoadBalancer loadBalancer, @NonNull RequestBinderRegistry requestBinderRegistry, @Nullable EventLoopGroup eventLoopGroup, @NonNull ChannelFactory socketChannelFactory, - Collection pipelineListeners, NettyClientCustomizer clientCustomizer, List invocationInstrumenterFactories, - @Nullable String informationalServiceId + @Nullable String informationalServiceId, + @NonNull ConversionService conversionService ) { ArgumentUtils.requireNonNull("nettyClientSslBuilder", nettyClientSslBuilder); ArgumentUtils.requireNonNull("codecRegistry", codecRegistry); @@ -359,6 +367,7 @@ public DefaultHttpClient(@Nullable LoadBalancer loadBalancer, this.webSocketRegistry = webSocketBeanRegistry != null ? webSocketBeanRegistry : WebSocketBeanRegistry.EMPTY; this.requestBinderRegistry = requestBinderRegistry; this.informationalServiceId = informationalServiceId; + this.conversionService = conversionService; this.connectionManager = new ConnectionManager( log, @@ -370,7 +379,6 @@ public DefaultHttpClient(@Nullable LoadBalancer loadBalancer, socketChannelFactory, nettyClientSslBuilder, clientCustomizer, - pipelineListeners, informationalServiceId); } @@ -523,6 +531,11 @@ public O retrieve(io.micronaut.http.HttpRequest request, Argument MutableHttpRequest toMutableRequest(io.micronaut.http.HttpRequest request) { + return MutableHttpRequestWrapper.wrapIfNecessary(conversionService, request); + } + @SuppressWarnings("SubscriberImplementation") @Override public Publisher>> eventStream(@NonNull io.micronaut.http.HttpRequest request) { @@ -691,7 +704,7 @@ public Publisher> dataStream(@NonNull io.micronaut.http.HttpRe public Publisher> dataStream(@NonNull io.micronaut.http.HttpRequest request, @NonNull Argument errorType) { final io.micronaut.http.HttpRequest parentRequest = ServerRequestContext.currentRequest().orElse(null); return new MicronautFlux<>(Flux.from(resolveRequestURI(request)) - .flatMap(requestURI -> dataStreamImpl(request, errorType, parentRequest, requestURI))) + .flatMap(requestURI -> dataStreamImpl(toMutableRequest(request), errorType, parentRequest, requestURI))) .doAfterNext(buffer -> { Object o = buffer.asNativeBuffer(); if (o instanceof ByteBuf) { @@ -712,7 +725,7 @@ public Publisher>> exchangeStre public Publisher>> exchangeStream(@NonNull io.micronaut.http.HttpRequest request, @NonNull Argument errorType) { io.micronaut.http.HttpRequest parentRequest = ServerRequestContext.currentRequest().orElse(null); return new MicronautFlux<>(Flux.from(resolveRequestURI(request)) - .flatMap(uri -> exchangeStreamImpl(parentRequest, request, errorType, uri))) + .flatMap(uri -> exchangeStreamImpl(parentRequest, toMutableRequest(request), errorType, uri))) .doAfterNext(byteBufferHttpResponse -> { ByteBuffer buffer = byteBufferHttpResponse.body(); if (buffer instanceof ReferenceCounted) { @@ -730,7 +743,7 @@ public Publisher jsonStream(@NonNull io.micronaut.http.HttpRequest public Publisher jsonStream(@NonNull io.micronaut.http.HttpRequest request, @NonNull Argument type, @NonNull Argument errorType) { final io.micronaut.http.HttpRequest parentRequest = ServerRequestContext.currentRequest().orElse(null); return Flux.from(resolveRequestURI(request)) - .flatMap(requestURI -> jsonStreamImpl(parentRequest, request, type, errorType, requestURI)); + .flatMap(requestURI -> jsonStreamImpl(parentRequest, toMutableRequest(request), type, errorType, requestURI)); } @SuppressWarnings("unchecked") @@ -749,7 +762,7 @@ public Publisher> exchange(@NonNull final io.micronaut.http.HttpRequest parentRequest = ServerRequestContext.currentRequest().orElse(null); Publisher uriPublisher = resolveRequestURI(request); return Flux.from(uriPublisher) - .switchMap(uri -> exchangeImpl(uri, parentRequest, request, bodyType, errorType)); + .switchMap(uri -> exchangeImpl(uri, parentRequest, toMutableRequest(request), bodyType, errorType)); } @Override @@ -844,8 +857,8 @@ private Publisher connectWebSocket(URI uri, MutableHttpRequest request .then(handler.getHandshakeCompletedMono()); } - private Flux>> exchangeStreamImpl(io.micronaut.http.HttpRequest parentRequest, io.micronaut.http.HttpRequest request, Argument errorType, URI requestURI) { - Flux> streamResponsePublisher = Flux.from(buildStreamExchange(parentRequest, request, requestURI, errorType)); + private Flux>> exchangeStreamImpl(io.micronaut.http.HttpRequest parentRequest, MutableHttpRequest request, Argument errorType, URI requestURI) { + Flux> streamResponsePublisher = Flux.from(buildStreamExchange(parentRequest, request, requestURI, errorType)); return streamResponsePublisher.switchMap(response -> { StreamedHttpResponse streamedHttpResponse = NettyHttpResponseBuilder.toStreamResponse(response); Flux httpContentReactiveSequence = Flux.from(streamedHttpResponse); @@ -863,19 +876,11 @@ private Flux>> exchangeStreamImpl(io.micronaut.ht thisResponse.setBody(byteBuffer); return (HttpResponse>) new HttpResponseWrapper<>(thisResponse); }); - }).doOnTerminate(() -> { - final Object o = request.getAttribute(NettyClientHttpRequest.CHANNEL).orElse(null); - if (o instanceof Channel) { - final Channel c = (Channel) o; - if (c.isOpen()) { - c.close(); - } - } }); } - private Flux jsonStreamImpl(io.micronaut.http.HttpRequest parentRequest, io.micronaut.http.HttpRequest request, Argument type, Argument errorType, URI requestURI) { - Flux> streamResponsePublisher = + private Flux jsonStreamImpl(io.micronaut.http.HttpRequest parentRequest, MutableHttpRequest request, Argument type, Argument errorType, URI requestURI) { + Flux> streamResponsePublisher = Flux.from(buildStreamExchange(parentRequest, request, requestURI, errorType)); return streamResponsePublisher.switchMap(response -> { if (!(response instanceof NettyStreamedHttpResponse)) { @@ -907,19 +912,11 @@ private Flux jsonStreamImpl(io.micronaut.http.HttpRequest parentReq }, streamArray); return Flux.from(jsonProcessor) .map(jsonNode -> mediaTypeCodec.decode(type, jsonNode)); - }).doOnTerminate(() -> { - final Object o = request.getAttribute(NettyClientHttpRequest.CHANNEL).orElse(null); - if (o instanceof Channel) { - final Channel c = (Channel) o; - if (c.isOpen()) { - c.close(); - } - } }); } - private Flux> dataStreamImpl(io.micronaut.http.HttpRequest request, Argument errorType, io.micronaut.http.HttpRequest parentRequest, URI requestURI) { - Flux> streamResponsePublisher = Flux.from(buildStreamExchange(parentRequest, request, requestURI, errorType)); + private Flux> dataStreamImpl(MutableHttpRequest request, Argument errorType, io.micronaut.http.HttpRequest parentRequest, URI requestURI) { + Flux> streamResponsePublisher = Flux.from(buildStreamExchange(parentRequest, request, requestURI, errorType)); Function> contentMapper = message -> { ByteBuf byteBuf = message.content(); return byteBufferFactory.wrap(byteBuf); @@ -933,15 +930,6 @@ private Flux> dataStreamImpl(io.micronaut.http.HttpRequest return httpContentReactiveSequence .filter(message -> !(message.content() instanceof EmptyByteBuf)) .map(contentMapper); - }) - .doOnTerminate(() -> { - final Object o = request.getAttribute(NettyClientHttpRequest.CHANNEL).orElse(null); - if (o instanceof Channel) { - final Channel c = (Channel) o; - if (c.isOpen()) { - c.close(); - } - } }); } @@ -949,14 +937,14 @@ private Flux> dataStreamImpl(io.micronaut.http.HttpRequest * Implementation of {@link #jsonStream}, {@link #dataStream}, {@link #exchangeStream}. */ @SuppressWarnings("MagicNumber") - private Publisher> buildStreamExchange( + private Publisher> buildStreamExchange( @Nullable io.micronaut.http.HttpRequest parentRequest, - @NonNull io.micronaut.http.HttpRequest request, + @NonNull MutableHttpRequest request, @NonNull URI requestURI, @Nullable Argument errorType) { - AtomicReference> requestWrapper = new AtomicReference<>(request); - Flux> streamResponsePublisher = connectAndStream(parentRequest, request, requestURI, requestWrapper, false, true); + AtomicReference> requestWrapper = new AtomicReference<>(request); + Flux> streamResponsePublisher = connectAndStream(parentRequest, request, requestURI, requestWrapper, false, true); streamResponsePublisher = readBodyOnError(errorType, streamResponsePublisher); @@ -965,7 +953,7 @@ private Publisher> buildStreamExchange( applyFilterToResponsePublisher(parentRequest, request, requestURI, requestWrapper, streamResponsePublisher) ); - return streamResponsePublisher.subscribeOn(connectionManager.getEventLoopScheduler()); + return streamResponsePublisher; } @Override @@ -978,15 +966,13 @@ public Publisher> proxy(@NonNull io.micronaut.http.HttpRe Objects.requireNonNull(options, "options"); return Flux.from(resolveRequestURI(request)) .flatMap(requestURI -> { - io.micronaut.http.MutableHttpRequest httpRequest = request instanceof MutableHttpRequest - ? (io.micronaut.http.MutableHttpRequest) request - : request.mutate(); + io.micronaut.http.MutableHttpRequest httpRequest = toMutableRequest(request); if (!options.isRetainHostHeader()) { httpRequest.headers(headers -> headers.remove(HttpHeaderNames.HOST)); } - AtomicReference> requestWrapper = new AtomicReference<>(httpRequest); - Flux> proxyResponsePublisher = connectAndStream(request, request, requestURI, requestWrapper, true, false); + AtomicReference> requestWrapper = new AtomicReference<>(httpRequest); + Flux> proxyResponsePublisher = connectAndStream(request, request, requestURI, requestWrapper, true, false); // apply filters //noinspection unchecked proxyResponsePublisher = Flux.from( @@ -1002,11 +988,11 @@ public Publisher> proxy(@NonNull io.micronaut.http.HttpRe }); } - private Flux> connectAndStream( + private Flux> connectAndStream( io.micronaut.http.HttpRequest parentRequest, io.micronaut.http.HttpRequest request, URI requestURI, - AtomicReference> requestWrapper, + AtomicReference> requestWrapper, boolean isProxy, boolean failOnError ) { @@ -1016,8 +1002,42 @@ private Flux> connectAndStream( } catch (Exception e) { return Flux.error(e); } - return connectionManager.connectForStream(requestKey, isProxy, isAcceptEvents(request)).flatMapMany(poolHandle -> { + return connectionManager.connect(requestKey).flatMapMany(poolHandle -> { request.setAttribute(NettyClientHttpRequest.CHANNEL, poolHandle.channel); + + boolean sse = !isProxy && isAcceptEvents(request); + poolHandle.channel.pipeline().addLast(new ChannelInboundHandlerAdapter() { + boolean ignoreOneLast = false; + + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { + if (msg instanceof io.netty.handler.codec.http.HttpResponse && + ((io.netty.handler.codec.http.HttpResponse) msg).status().equals(HttpResponseStatus.CONTINUE)) { + ignoreOneLast = true; + } + + super.channelRead(ctx, msg); + + if (msg instanceof LastHttpContent) { + if (ignoreOneLast) { + ignoreOneLast = false; + } else { + ctx.pipeline().remove(ChannelPipelineCustomizer.HANDLER_HTTP_STREAM); + ctx.pipeline().remove(this); + } + } + } + + @Override + public void handlerRemoved(ChannelHandlerContext ctx) throws Exception { + poolHandle.release(); + } + }); + if (sse) { + poolHandle.channel.pipeline().addLast(HttpLineBasedFrameDecoder.NAME, new HttpLineBasedFrameDecoder(configuration.getMaxContentLength(), true, true)); + } + poolHandle.channel.pipeline().addLast(ChannelPipelineCustomizer.HANDLER_HTTP_STREAM, new HttpStreamsClientHandler()); + return this.streamRequestThroughChannel( parentRequest, requestWrapper.get(), @@ -1034,10 +1054,10 @@ private Flux> connectAndStream( private Publisher> exchangeImpl( URI requestURI, io.micronaut.http.HttpRequest parentRequest, - io.micronaut.http.HttpRequest request, + MutableHttpRequest request, @NonNull Argument bodyType, @NonNull Argument errorType) { - AtomicReference> requestWrapper = new AtomicReference<>(request); + AtomicReference> requestWrapper = new AtomicReference<>(request); RequestKey requestKey; try { @@ -1046,9 +1066,22 @@ private Publisher> exchang return Flux.error(e); } - Mono handlePublisher = connectionManager.connectForExchange(requestKey, MediaType.MULTIPART_FORM_DATA_TYPE.equals(request.getContentType().orElse(null)), isAcceptEvents(request)); + Mono handlePublisher = connectionManager.connect(requestKey); Flux> responsePublisher = handlePublisher.flatMapMany(poolHandle -> { + poolHandle.channel.pipeline() + .addLast(ChannelPipelineCustomizer.HANDLER_HTTP_AGGREGATOR, new HttpObjectAggregator(configuration.getMaxContentLength()) { + @Override + protected void finishAggregation(FullHttpMessage aggregated) throws Exception { + // only set content-length if there's any content + if (!HttpUtil.isContentLengthSet(aggregated) && + aggregated.content().readableBytes() > 0) { + super.finishAggregation(aggregated); + } + } + }) + .addLast(ChannelPipelineCustomizer.HANDLER_HTTP_STREAM, new HttpStreamsClientHandler()); + return Flux.create(emitter -> { try { sendRequestThroughChannel( @@ -1056,7 +1089,6 @@ private Publisher> exchang bodyType, errorType, emitter, - poolHandle.channel, requestKey.isSecure(), poolHandle ); @@ -1082,7 +1114,7 @@ private Publisher> exchang final Duration rt = readTimeout.get(); if (!rt.isNegative()) { Duration duration = rt.plus(Duration.ofSeconds(1)); - finalReactiveSequence = finalReactiveSequence.timeout(duration) + finalReactiveSequence = finalReactiveSequence.timeout(duration) // todo: move to CM .onErrorResume(throwable -> { if (throwable instanceof TimeoutException) { return Flux.error(ReadTimeoutException.TIMEOUT_EXCEPTION); @@ -1167,11 +1199,11 @@ protected Object getLoadBalancerDiscriminator() { return null; } - private > Publisher applyFilterToResponsePublisher( + private > Publisher applyFilterToResponsePublisher( io.micronaut.http.HttpRequest parentRequest, io.micronaut.http.HttpRequest request, URI requestURI, - AtomicReference> requestWrapper, + AtomicReference> requestWrapper, Publisher responsePublisher) { if (request instanceof MutableHttpRequest) { @@ -1253,7 +1285,7 @@ protected NettyRequestWriter buildNettyRequest( if (Publishers.isConvertibleToPublisher(bodyValue)) { boolean isSingle = Publishers.isSingle(bodyValue.getClass()); - Publisher publisher = ConversionService.SHARED.convert(bodyValue, Publisher.class).orElseThrow(() -> + Publisher publisher = conversionService.convert(bodyValue, Publisher.class).orElseThrow(() -> new IllegalArgumentException("Unconvertible reactive type: " + bodyValue) ); @@ -1335,7 +1367,7 @@ protected NettyRequestWriter buildNettyRequest( .orElse(null); } if (bodyContent == null) { - bodyContent = ConversionService.SHARED.convert(bodyValue, ByteBuf.class).orElseThrow(() -> + bodyContent = conversionService.convert(bodyValue, ByteBuf.class).orElseThrow(() -> customizeException(new HttpClientException("Body [" + bodyValue + "] cannot be encoded to content type [" + requestContentType + "]. No possible codecs or converters found.")) ); } @@ -1359,7 +1391,7 @@ protected NettyRequestWriter buildNettyRequest( return new NettyRequestWriter(nettyRequest, postRequestEncoder); } - private Flux> readBodyOnError(@Nullable Argument errorType, @NonNull Flux> publisher) { + private Flux> readBodyOnError(@Nullable Argument errorType, @NonNull Flux> publisher) { if (errorType != null && errorType != HttpClient.DEFAULT_ERROR_TYPE) { return publisher.onErrorResume(clientException -> { if (clientException instanceof HttpClientResponseException) { @@ -1441,7 +1473,6 @@ private void sendRequestThroughChannel( Argument bodyType, Argument errorType, FluxSink> emitter, - Channel channel, boolean secure, ConnectionManager.PoolHandle poolHandle) throws HttpPostRequestEncoder.ErrorDataEncoderException { URI requestURI = finalRequest.getUri(); @@ -1467,11 +1498,11 @@ private void sendRequestThroughChannel( HttpRequest nettyRequest = requestWriter.getNettyRequest(); prepareHttpHeaders( - requestURI, - finalRequest, - nettyRequest, - permitsBody, - !poolHandle.canReturn() + poolHandle, + requestURI, + finalRequest, + nettyRequest, + permitsBody ); if (log.isDebugEnabled()) { @@ -1482,8 +1513,8 @@ private void sendRequestThroughChannel( traceRequest(finalRequest, nettyRequest); } - Promise> responsePromise = channel.eventLoop().newPromise(); - channel.pipeline().addLast(ChannelPipelineCustomizer.HANDLER_MICRONAUT_FULL_HTTP_RESPONSE, + Promise> responsePromise = poolHandle.channel.eventLoop().newPromise(); + poolHandle.channel.pipeline().addLast(ChannelPipelineCustomizer.HANDLER_MICRONAUT_FULL_HTTP_RESPONSE, new FullHttpResponseHandler<>(responsePromise, poolHandle, secure, finalRequest, bodyType, errorType)); poolHandle.notifyRequestPipelineBuilt(); Publisher> publisher = new NettyFuturePublisher<>(responsePromise, true); @@ -1493,16 +1524,16 @@ private void sendRequestThroughChannel( } publisher.subscribe(new ForwardingSubscriber<>(emitter)); - requestWriter.write(channel, secure, emitter); + requestWriter.write(poolHandle, secure, emitter); } - private Flux> streamRequestThroughChannel( + private Flux> streamRequestThroughChannel( io.micronaut.http.HttpRequest parentRequest, - io.micronaut.http.HttpRequest request, + MutableHttpRequest request, ConnectionManager.PoolHandle poolHandle, boolean failOnError, boolean secure) { - return Flux.>create(sink -> { + return Flux.>create(sink -> { try { streamRequestThroughChannel0(parentRequest, request, sink, poolHandle, secure); } catch (HttpPostRequestEncoder.ErrorDataEncoderException e) { @@ -1525,32 +1556,45 @@ private > Flux handleStreamHttpError( private void streamRequestThroughChannel0( io.micronaut.http.HttpRequest parentRequest, - final io.micronaut.http.HttpRequest finalRequest, - FluxSink emitter, + MutableHttpRequest request, + FluxSink> emitter, ConnectionManager.PoolHandle poolHandle, boolean secure) throws HttpPostRequestEncoder.ErrorDataEncoderException { - NettyRequestWriter requestWriter = prepareRequest( - finalRequest, - finalRequest.getUri(), - emitter + URI requestURI = request.getUri(); + boolean permitsBody = io.micronaut.http.HttpMethod.permitsRequestBody(request.getMethod()); + NettyRequestWriter requestWriter = buildNettyRequest( + request, + requestURI, + request + .getContentType() + .orElse(MediaType.APPLICATION_JSON_TYPE), + permitsBody, + null, + throwable -> { + if (!emitter.isCancelled()) { + emitter.error(throwable); + } + } ); + prepareHttpHeaders(poolHandle, requestURI, request, requestWriter.getNettyRequest(), permitsBody); + HttpRequest nettyRequest = requestWriter.getNettyRequest(); - Promise> responsePromise = poolHandle.channel.eventLoop().newPromise(); + Promise> responsePromise = poolHandle.channel.eventLoop().newPromise(); ChannelPipeline pipeline = poolHandle.channel.pipeline(); - pipeline.addLast(ChannelPipelineCustomizer.HANDLER_MICRONAUT_HTTP_RESPONSE_FULL, new StreamFullHttpResponseHandler(responsePromise, parentRequest, finalRequest)); - pipeline.addLast(ChannelPipelineCustomizer.HANDLER_MICRONAUT_HTTP_RESPONSE_STREAM, new StreamStreamHttpResponseHandler(responsePromise, parentRequest, finalRequest)); + pipeline.addLast(ChannelPipelineCustomizer.HANDLER_MICRONAUT_HTTP_RESPONSE_FULL, new StreamFullHttpResponseHandler(responsePromise, parentRequest, request)); + pipeline.addLast(ChannelPipelineCustomizer.HANDLER_MICRONAUT_HTTP_RESPONSE_STREAM, new StreamStreamHttpResponseHandler(responsePromise, parentRequest, request)); poolHandle.notifyRequestPipelineBuilt(); if (log.isDebugEnabled()) { - debugRequest(finalRequest.getUri(), nettyRequest); + debugRequest(request.getUri(), nettyRequest); } if (log.isTraceEnabled()) { - traceRequest(finalRequest, nettyRequest); + traceRequest(request, nettyRequest); } - requestWriter.write(poolHandle.channel, secure, emitter); - responsePromise.addListener(future -> { + requestWriter.write(poolHandle, secure, emitter); + responsePromise.addListener((Future> future) -> { if (future.isSuccess()) { emitter.next(future.getNow()); emitter.complete(); @@ -1580,23 +1624,22 @@ private String getHostHeader(URI requestURI) { } private void prepareHttpHeaders( - URI requestURI, - io.micronaut.http.HttpRequest request, - io.netty.handler.codec.http.HttpRequest nettyRequest, - boolean permitsBody, - boolean closeConnection) { + ConnectionManager.PoolHandle poolHandle, + URI requestURI, + io.micronaut.http.HttpRequest request, + HttpRequest nettyRequest, + boolean permitsBody) { HttpHeaders headers = nettyRequest.headers(); if (!headers.contains(HttpHeaderNames.HOST)) { headers.set(HttpHeaderNames.HOST, getHostHeader(requestURI)); } - // HTTP/2 assumes keep-alive connections - if (connectionManager.httpVersion != io.micronaut.http.HttpVersion.HTTP_2_0) { - if (closeConnection) { - headers.set(HttpHeaderNames.CONNECTION, HttpHeaderValues.CLOSE); - } else { + if (!poolHandle.http2) { + if (poolHandle.canReturn()) { headers.set(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE); + } else { + headers.set(HttpHeaderNames.CONNECTION, HttpHeaderValues.CLOSE); } } @@ -1621,7 +1664,7 @@ private void prepareHttpHeaders( } } - private ClientFilterChain buildChain(AtomicReference> requestWrapper, List filters) { + private ClientFilterChain buildChain(AtomicReference> requestWrapper, List filters) { AtomicInteger integer = new AtomicInteger(); int len = filters.size(); return new ClientFilterChain() { @@ -1668,7 +1711,7 @@ private HttpPostRequestEncoder buildFormDataRequest(MutableHttpRequest clientHtt } private void addBodyAttribute(HttpPostRequestEncoder postRequestEncoder, String key, Object value) throws HttpPostRequestEncoder.ErrorDataEncoderException { - Optional converted = ConversionService.SHARED.convert(value, String.class); + Optional converted = conversionService.convert(value, String.class); if (converted.isPresent()) { postRequestEncoder.addBodyAttribute(key, converted.get()); } @@ -1784,37 +1827,6 @@ private static MediaTypeCodecRegistry createDefaultMediaTypeRegistry() { ); } - private NettyRequestWriter prepareRequest( - io.micronaut.http.HttpRequest request, - URI requestURI, - FluxSink> emitter) throws HttpPostRequestEncoder.ErrorDataEncoderException { - MediaType requestContentType = request - .getContentType() - .orElse(MediaType.APPLICATION_JSON_TYPE); - - boolean permitsBody = io.micronaut.http.HttpMethod.permitsRequestBody(request.getMethod()); - - if (!(request instanceof MutableHttpRequest)) { - throw new IllegalArgumentException("A MutableHttpRequest is required"); - } - MutableHttpRequest clientHttpRequest = (MutableHttpRequest) request; - NettyRequestWriter requestWriter = buildNettyRequest( - clientHttpRequest, - requestURI, - requestContentType, - permitsBody, - null, - throwable -> { - if (!emitter.isCancelled()) { - emitter.error(throwable); - } - } - ); - io.netty.handler.codec.http.HttpRequest nettyRequest = requestWriter.getNettyRequest(); - prepareHttpHeaders(requestURI, request, nettyRequest, permitsBody, true); - return requestWriter; - } - private @NonNull InvocationInstrumenter combineFactories() { if (CollectionUtils.isEmpty(invocationInstrumenterFactories)) { return NOOP; @@ -1949,23 +1961,21 @@ private class NettyRequestWriter { * @param channelPool The channel pool * @param emitter The emitter */ - protected void write(Channel channel, boolean isSecure, FluxSink emitter) { - final ChannelPipeline pipeline = channel.pipeline(); - if (connectionManager.httpVersion == io.micronaut.http.HttpVersion.HTTP_2_0) { + protected void write(ConnectionManager.PoolHandle poolHandle, boolean isSecure, FluxSink emitter) { + if (poolHandle.http2) { + // todo: move to ConnectionManager, DefaultHttpClient shouldn't care about the scheme if (isSecure) { nettyRequest.headers().add(AbstractNettyHttpRequest.HTTP2_SCHEME, HttpScheme.HTTPS); } else { nettyRequest.headers().add(AbstractNettyHttpRequest.HTTP2_SCHEME, HttpScheme.HTTP); } } - processRequestWrite(channel, emitter, pipeline); - } - private void processRequestWrite(Channel channel, FluxSink emitter, ChannelPipeline pipeline) { + Channel channel = poolHandle.channel; ChannelFuture writeFuture; if (encoder != null && encoder.isChunked()) { channel.attr(AttributeKey.valueOf(ChannelPipelineCustomizer.HANDLER_HTTP_CHUNK)).set(true); - pipeline.addAfter(ChannelPipelineCustomizer.HANDLER_HTTP_STREAM, ChannelPipelineCustomizer.HANDLER_HTTP_CHUNK, new ChunkedWriteHandler()); + channel.pipeline().addAfter(ChannelPipelineCustomizer.HANDLER_HTTP_STREAM, ChannelPipelineCustomizer.HANDLER_HTTP_CHUNK, new ChunkedWriteHandler()); channel.write(nettyRequest); writeFuture = channel.writeAndFlush(encoder); } else { @@ -1975,6 +1985,7 @@ private void processRequestWrite(Channel channel, FluxSink emitter, ChannelPi connectionManager.addInstrumentedListener(writeFuture, f -> { try { if (!f.isSuccess()) { + poolHandle.taint(); if (!emitter.isCancelled()) { emitter.error(f.cause()); } @@ -2010,11 +2021,11 @@ private static class CurrentEvent { } private abstract class BaseHttpResponseHandler extends SimpleChannelInboundHandlerInstrumented { - private final Promise responsePromise; + private final Promise responsePromise; private final io.micronaut.http.HttpRequest parentRequest; private final io.micronaut.http.HttpRequest finalRequest; - public BaseHttpResponseHandler(Promise responsePromise, io.micronaut.http.HttpRequest parentRequest, io.micronaut.http.HttpRequest finalRequest) { + public BaseHttpResponseHandler(Promise responsePromise, io.micronaut.http.HttpRequest parentRequest, io.micronaut.http.HttpRequest finalRequest) { super(connectionManager.instrumenter); this.responsePromise = responsePromise; this.parentRequest = parentRequest; @@ -2082,6 +2093,7 @@ protected void channelReadInstrumented(ChannelHandlerContext ctx, R msg) throws traceHeaders(headers); } buildResponse(responsePromise, msg); + removeHandler(ctx); } private void setRedirectHeaders(@Nullable io.micronaut.http.HttpRequest request, MutableHttpRequest redirectRequest) { @@ -2101,6 +2113,8 @@ private void setRedirectHeaders(@Nullable io.micronaut.http.HttpRequest reque } } + protected abstract void removeHandler(ChannelHandlerContext ctx); + protected abstract Function> makeRedirectHandler(io.micronaut.http.HttpRequest parentRequest, MutableHttpRequest redirectRequest); protected abstract void buildResponse(Promise promise, R msg); @@ -2161,6 +2175,11 @@ protected void channelReadInstrumented(ChannelHandlerContext channelHandlerConte } } + @Override + protected void removeHandler(ChannelHandlerContext ctx) { + // done in channelReadInstrumented + } + @Override protected void buildResponse(Promise> promise, FullHttpResponse msg) { try { @@ -2274,6 +2293,12 @@ public Argument getErrorType(MediaType mediaType) { @Override public void handlerRemoved(ChannelHandlerContext ctx) { + ctx.pipeline().remove(ChannelPipelineCustomizer.HANDLER_HTTP_AGGREGATOR); + try { + ctx.pipeline().remove(ChannelPipelineCustomizer.HANDLER_HTTP_CHUNK); + } catch (NoSuchElementException ignored) { + } + ctx.pipeline().remove(ChannelPipelineCustomizer.HANDLER_HTTP_STREAM); poolHandle.release(); } @@ -2285,8 +2310,12 @@ public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { } } - private class StreamFullHttpResponseHandler extends BaseHttpResponseHandler> { - public StreamFullHttpResponseHandler(Promise> responsePromise, io.micronaut.http.HttpRequest parentRequest, io.micronaut.http.HttpRequest finalRequest) { + private class StreamFullHttpResponseHandler extends BaseHttpResponseHandler> { + public StreamFullHttpResponseHandler( + Promise> responsePromise, + io.micronaut.http.HttpRequest parentRequest, + io.micronaut.http.HttpRequest finalRequest) { + super(responsePromise, parentRequest, finalRequest); } @@ -2296,7 +2325,13 @@ public boolean acceptInboundMessage(Object msg) { } @Override - protected void buildResponse(Promise> promise, FullHttpResponse msg) { + protected void removeHandler(ChannelHandlerContext ctx) { + ctx.pipeline().remove(ChannelPipelineCustomizer.HANDLER_MICRONAUT_HTTP_RESPONSE_FULL); + ctx.pipeline().remove(ChannelPipelineCustomizer.HANDLER_MICRONAUT_HTTP_RESPONSE_STREAM); + } + + @Override + protected void buildResponse(Promise> promise, FullHttpResponse msg) { Publisher bodyPublisher; if (msg.content() instanceof EmptyByteBuf) { bodyPublisher = Publishers.empty(); @@ -2313,13 +2348,17 @@ protected void buildResponse(Promise> promise, FullHttpR } @Override - protected Function>> makeRedirectHandler(io.micronaut.http.HttpRequest parentRequest, MutableHttpRequest redirectRequest) { + protected Function>> makeRedirectHandler(io.micronaut.http.HttpRequest parentRequest, MutableHttpRequest redirectRequest) { return uri -> buildStreamExchange(parentRequest, redirectRequest, uri, null); } } - private class StreamStreamHttpResponseHandler extends BaseHttpResponseHandler> { - public StreamStreamHttpResponseHandler(Promise> responsePromise, io.micronaut.http.HttpRequest parentRequest, io.micronaut.http.HttpRequest finalRequest) { + private class StreamStreamHttpResponseHandler extends BaseHttpResponseHandler> { + public StreamStreamHttpResponseHandler( + Promise> responsePromise, + io.micronaut.http.HttpRequest parentRequest, + io.micronaut.http.HttpRequest finalRequest) { + super(responsePromise, parentRequest, finalRequest); } @@ -2329,12 +2368,18 @@ public boolean acceptInboundMessage(Object msg) { } @Override - protected void buildResponse(Promise> promise, StreamedHttpResponse msg) { + protected void removeHandler(ChannelHandlerContext ctx) { + ctx.pipeline().remove(ChannelPipelineCustomizer.HANDLER_MICRONAUT_HTTP_RESPONSE_FULL); + ctx.pipeline().remove(ChannelPipelineCustomizer.HANDLER_MICRONAUT_HTTP_RESPONSE_STREAM); + } + + @Override + protected void buildResponse(Promise> promise, StreamedHttpResponse msg) { promise.trySuccess(new NettyStreamedHttpResponse<>(msg)); } @Override - protected Function>> makeRedirectHandler(io.micronaut.http.HttpRequest parentRequest, MutableHttpRequest redirectRequest) { + protected Function>> makeRedirectHandler(io.micronaut.http.HttpRequest parentRequest, MutableHttpRequest redirectRequest) { return uri -> buildStreamExchange(parentRequest, redirectRequest, uri, null); } } diff --git a/http-client/src/main/java/io/micronaut/http/client/netty/DefaultNettyHttpClientRegistry.java b/http-client/src/main/java/io/micronaut/http/client/netty/DefaultNettyHttpClientRegistry.java index 6e0698a2a6e..07acfebeee2 100644 --- a/http-client/src/main/java/io/micronaut/http/client/netty/DefaultNettyHttpClientRegistry.java +++ b/http-client/src/main/java/io/micronaut/http/client/netty/DefaultNettyHttpClientRegistry.java @@ -27,20 +27,20 @@ import io.micronaut.core.annotation.Nullable; import io.micronaut.core.convert.ConversionService; import io.micronaut.core.util.StringUtils; -import io.micronaut.http.HttpVersion; import io.micronaut.http.MediaType; import io.micronaut.http.annotation.FilterMatcher; import io.micronaut.http.bind.DefaultRequestBinderRegistry; import io.micronaut.http.bind.RequestBinderRegistry; -import io.micronaut.http.client.HttpClientRegistry; -import io.micronaut.http.client.StreamingHttpClientRegistry; -import io.micronaut.http.client.ProxyHttpClient; -import io.micronaut.http.client.HttpClientConfiguration; import io.micronaut.http.client.HttpClient; -import io.micronaut.http.client.StreamingHttpClient; -import io.micronaut.http.client.ProxyHttpClientRegistry; +import io.micronaut.http.client.HttpClientConfiguration; +import io.micronaut.http.client.HttpClientRegistry; +import io.micronaut.http.client.HttpVersionSelection; import io.micronaut.http.client.LoadBalancer; import io.micronaut.http.client.LoadBalancerResolver; +import io.micronaut.http.client.ProxyHttpClient; +import io.micronaut.http.client.ProxyHttpClientRegistry; +import io.micronaut.http.client.StreamingHttpClient; +import io.micronaut.http.client.StreamingHttpClientRegistry; import io.micronaut.http.client.annotation.Client; import io.micronaut.http.client.exceptions.HttpClientException; import io.micronaut.http.client.filter.ClientFilterResolutionContext; @@ -58,8 +58,8 @@ import io.micronaut.http.netty.channel.EventLoopGroupRegistry; import io.micronaut.inject.InjectionPoint; import io.micronaut.inject.qualifiers.Qualifiers; -import io.micronaut.json.JsonMapper; import io.micronaut.json.JsonFeatures; +import io.micronaut.json.JsonMapper; import io.micronaut.json.codec.MapperMediaTypeCodec; import io.micronaut.scheduling.instrument.InvocationInstrumenterFactory; import io.micronaut.websocket.WebSocketClient; @@ -157,7 +157,7 @@ public DefaultNettyHttpClientRegistry( @NonNull @Override - public HttpClient getClient(HttpVersion httpVersion, @NonNull String clientId, @Nullable String path) { + public HttpClient getClient(@NonNull HttpVersionSelection httpVersion, @NonNull String clientId, @Nullable String path) { final ClientKey key = new ClientKey( httpVersion, clientId, @@ -391,7 +391,7 @@ private DefaultHttpClient getClient(ClientKey key, BeanContext beanContext, Anno private DefaultHttpClient buildClient( LoadBalancer loadBalancer, - HttpVersion httpVersion, + HttpVersionSelection httpVersion, HttpClientConfiguration configuration, String clientId, String contextPath, @@ -399,6 +399,7 @@ private DefaultHttpClient buildClient( AnnotationMetadata annotationMetadata) { EventLoopGroup eventLoopGroup = resolveEventLoopGroup(configuration, beanContext); + ConversionService conversionService = beanContext.getBean(ConversionService.class); return new DefaultHttpClient( loadBalancer, httpVersion, @@ -414,14 +415,14 @@ private DefaultHttpClient buildClient( codecRegistry, WebSocketBeanRegistry.forClient(beanContext), beanContext.findBean(RequestBinderRegistry.class).orElseGet(() -> - new DefaultRequestBinderRegistry(ConversionService.SHARED) + new DefaultRequestBinderRegistry(conversionService) ), eventLoopGroup, resolveSocketChannelFactory(configuration, beanContext), - pipelineListeners, clientCustomizer, invocationInstrumenterFactories, - clientId + clientId, + conversionService ); } @@ -476,8 +477,7 @@ private ChannelFactory resolveSocketChannelFactory(HttpClientConfiguration confi } private ClientKey getClientKey(AnnotationMetadata metadata) { - final HttpVersion httpVersion = - metadata.enumValue(Client.class, "httpVersion", HttpVersion.class).orElse(null); + HttpVersionSelection httpVersionSelection = HttpVersionSelection.forClientAnnotation(metadata); String clientId = metadata.stringValue(Client.class).orElse(null); String path = metadata.stringValue(Client.class, "path").orElse(null); List filterAnnotation = metadata @@ -486,7 +486,7 @@ private ClientKey getClientKey(AnnotationMetadata metadata) { metadata.classValue(Client.class, "configuration").orElse(null); JsonFeatures jsonFeatures = jsonMapper.detectFeatures(metadata).orElse(null); - return new ClientKey(httpVersion, clientId, filterAnnotation, path, configurationClass, jsonFeatures); + return new ClientKey(httpVersionSelection, clientId, filterAnnotation, path, configurationClass, jsonFeatures); } private static MediaTypeCodec createNewJsonCodec(BeanContext beanContext, JsonFeatures jsonFeatures) { @@ -502,7 +502,7 @@ private static MapperMediaTypeCodec getJsonCodec(BeanContext beanContext) { */ @Internal private static final class ClientKey { - final HttpVersion httpVersion; + final HttpVersionSelection httpVersion; final String clientId; final List filterAnnotations; final String path; @@ -510,7 +510,7 @@ private static final class ClientKey { final JsonFeatures jsonFeatures; ClientKey( - HttpVersion httpVersion, + HttpVersionSelection httpVersion, String clientId, List filterAnnotations, String path, diff --git a/http-client/src/main/java/io/micronaut/http/client/netty/HttpLineBasedFrameDecoder.java b/http-client/src/main/java/io/micronaut/http/client/netty/HttpLineBasedFrameDecoder.java new file mode 100644 index 00000000000..eacc0125f52 --- /dev/null +++ b/http-client/src/main/java/io/micronaut/http/client/netty/HttpLineBasedFrameDecoder.java @@ -0,0 +1,103 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.http.client.netty; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.NonNull; +import io.micronaut.http.netty.channel.ChannelPipelineCustomizer; +import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; +import io.netty.handler.codec.LineBasedFrameDecoder; +import io.netty.handler.codec.http.DefaultHttpContent; +import io.netty.handler.codec.http.HttpContent; +import io.netty.handler.codec.http.HttpResponse; +import io.netty.handler.codec.http.HttpResponseStatus; +import io.netty.handler.codec.http.LastHttpContent; + +/** + * Variant of {@link LineBasedFrameDecoder} that accepts + * {@link io.netty.handler.codec.http.HttpContent} data. Note: this handler removes itself when the + * response has been consumed. + * + * @since 4.0.0 + */ +@Internal +final class HttpLineBasedFrameDecoder extends LineBasedFrameDecoder { + static final String NAME = ChannelPipelineCustomizer.HANDLER_MICRONAUT_SSE_EVENT_STREAM; + + private boolean ignoreOneLast = false; + + HttpLineBasedFrameDecoder(int maxLength, boolean stripDelimiter, boolean failFast) { + super(maxLength, stripDelimiter, failFast); + } + + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { + if (msg instanceof HttpResponse && + ((HttpResponse) msg).status().equals(HttpResponseStatus.CONTINUE)) { + ignoreOneLast = true; + } + + if (msg instanceof HttpContent) { + super.channelRead(ctx, ((HttpContent) msg).content()); + } else { + ctx.fireChannelRead(msg); + } + + if (msg instanceof LastHttpContent) { + if (ignoreOneLast) { + ignoreOneLast = false; + } else { + // first, remove the handler so that LineBasedFrameDecoder flushes any further + // data. Then forward the LastHttpContent. + ctx.pipeline().remove(NAME); + ctx.fireChannelRead(LastHttpContent.EMPTY_LAST_CONTENT); + } + } + } + + @Override + public void handlerAdded(ChannelHandlerContext ctx) { + ctx.pipeline().addAfter(NAME, Wrap.NAME, Wrap.INSTANCE); + } + + @Override + protected void handlerRemoved0(ChannelHandlerContext ctx) { + ctx.pipeline().remove(Wrap.NAME); + } + + @Sharable + private static class Wrap extends ChannelInboundHandlerAdapter { + static final ChannelHandler INSTANCE = new Wrap(); + static final String NAME = ChannelPipelineCustomizer.HANDLER_MICRONAUT_SSE_CONTENT; + + @Override + public void channelRead(@NonNull ChannelHandlerContext ctx, @NonNull Object msg) throws Exception { + if (msg instanceof ByteBuf) { + ByteBuf buffer = (ByteBuf) msg; + // todo: this is necessary because downstream handlers sometimes do the + // `if (refcnt > 0) release` pattern. We should eventually fix that. + ByteBuf copy = buffer.copy(); + buffer.release(); + ctx.fireChannelRead(new DefaultHttpContent(copy)); + } else { + ctx.fireChannelRead(msg); + } + } + } +} diff --git a/http-client/src/main/java/io/micronaut/http/client/netty/IdleTimeoutHandler.java b/http-client/src/main/java/io/micronaut/http/client/netty/IdleTimeoutHandler.java deleted file mode 100644 index c2242e22535..00000000000 --- a/http-client/src/main/java/io/micronaut/http/client/netty/IdleTimeoutHandler.java +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Copyright 2017-2020 original authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.micronaut.http.client.netty; - -import io.micronaut.core.annotation.Internal; -import io.netty.channel.ChannelDuplexHandler; -import io.netty.channel.ChannelHandler; -import io.netty.channel.ChannelHandlerContext; -import io.netty.channel.ChannelInboundHandler; -import io.netty.handler.timeout.IdleState; -import io.netty.handler.timeout.IdleStateEvent; - -/** - * This class is responsible for detecting idle timeout events, upon which the channel in the pool is closed. - * - * @author Dan Maas - * @since 2.2.4 - */ -@ChannelHandler.Sharable -@Internal -final class IdleTimeoutHandler extends ChannelDuplexHandler { - - static final ChannelInboundHandler INSTANCE = new IdleTimeoutHandler(); - - private IdleTimeoutHandler() { - } - - @Override - public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception { - if (evt instanceof IdleStateEvent) { - IdleStateEvent e = (IdleStateEvent) evt; - if (e.state() == IdleState.READER_IDLE || e.state() == IdleState.WRITER_IDLE) { - ctx.close(); - } - } - } -} diff --git a/http-client/src/main/java/io/micronaut/http/client/netty/IdlingConnectionHandler.java b/http-client/src/main/java/io/micronaut/http/client/netty/InitialConnectionErrorHandler.java similarity index 52% rename from http-client/src/main/java/io/micronaut/http/client/netty/IdlingConnectionHandler.java rename to http-client/src/main/java/io/micronaut/http/client/netty/InitialConnectionErrorHandler.java index d4e931566a2..5c6aea4d4f3 100644 --- a/http-client/src/main/java/io/micronaut/http/client/netty/IdlingConnectionHandler.java +++ b/http-client/src/main/java/io/micronaut/http/client/netty/InitialConnectionErrorHandler.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2020 original authors + * Copyright 2017-2022 original authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,36 +16,37 @@ package io.micronaut.http.client.netty; import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.Nullable; +import io.netty.channel.Channel; import io.netty.channel.ChannelHandler; import io.netty.channel.ChannelHandlerContext; -import io.netty.channel.ChannelInboundHandler; import io.netty.channel.ChannelInboundHandlerAdapter; -import io.netty.util.ReferenceCountUtil; +import io.netty.util.AttributeKey; /** - * This handler prevents reading a channel when it is not being used by the connection pool. - * - * @author Dan Maas - * @since 2.2.4 + * Handler for connection failures that happen during the handshake phases of a connection. */ -@ChannelHandler.Sharable @Internal -final class IdlingConnectionHandler extends ChannelInboundHandlerAdapter { - - static final ChannelInboundHandler INSTANCE = new IdlingConnectionHandler(); - - private IdlingConnectionHandler() { - } +@ChannelHandler.Sharable +abstract class InitialConnectionErrorHandler extends ChannelInboundHandlerAdapter { + private static final AttributeKey FAILURE_KEY = + AttributeKey.valueOf(InitialConnectionErrorHandler.class, "FAILURE_KEY"); @Override - public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { - ReferenceCountUtil.release(msg); + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { + setFailureCause(ctx.channel(), cause); ctx.close(); } + static void setFailureCause(Channel channel, Throwable cause) { + channel.attr(FAILURE_KEY).set(cause); + } + @Override - public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { - ctx.close(); + public void channelInactive(ChannelHandlerContext ctx) throws Exception { + super.channelInactive(ctx); + onNewConnectionFailure(ctx.channel().attr(FAILURE_KEY).get()); } + protected abstract void onNewConnectionFailure(@Nullable Throwable cause) throws Exception; } diff --git a/http-client/src/main/java/io/micronaut/http/client/netty/MutableHttpRequestWrapper.java b/http-client/src/main/java/io/micronaut/http/client/netty/MutableHttpRequestWrapper.java new file mode 100644 index 00000000000..8e1343be39f --- /dev/null +++ b/http-client/src/main/java/io/micronaut/http/client/netty/MutableHttpRequestWrapper.java @@ -0,0 +1,131 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.http.client.netty; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.NonNull; +import io.micronaut.core.annotation.Nullable; +import io.micronaut.core.convert.ConversionContext; +import io.micronaut.core.convert.ConversionService; +import io.micronaut.core.type.Argument; +import io.micronaut.http.HttpRequest; +import io.micronaut.http.HttpRequestWrapper; +import io.micronaut.http.MutableHttpHeaders; +import io.micronaut.http.MutableHttpParameters; +import io.micronaut.http.MutableHttpRequest; +import io.micronaut.http.cookie.Cookie; + +import java.net.URI; +import java.util.Optional; + +/** + * Wrapper around an immutable {@link HttpRequest} that allows mutation. + * + * @param Body type + * @since 4.0.0 + */ +@Internal +final class MutableHttpRequestWrapper extends HttpRequestWrapper implements MutableHttpRequest { + private final ConversionService conversionService; + + @Nullable + private B body; + @Nullable + private URI uri; + + MutableHttpRequestWrapper(ConversionService conversionService, HttpRequest delegate) { + super(delegate); + this.conversionService = conversionService; + } + + static MutableHttpRequest wrapIfNecessary(ConversionService conversionService, HttpRequest request) { + if (request instanceof MutableHttpRequest) { + return (MutableHttpRequest) request; + } else { + return new MutableHttpRequestWrapper<>(conversionService, request); + } + } + + @NonNull + @Override + public Optional getBody() { + if (body == null) { + return getDelegate().getBody(); + } else { + return Optional.of(body); + } + } + + @NonNull + @Override + public Optional getBody(@NonNull Class type) { + if (body == null) { + return getDelegate().getBody(type); + } else { + return conversionService.convert(body, ConversionContext.of(type)); + } + } + + @NonNull + @Override + public Optional getBody(@NonNull Argument type) { + if (body == null) { + return getDelegate().getBody(type); + } else { + return conversionService.convert(body, ConversionContext.of(type)); + } + } + + @Override + public MutableHttpRequest cookie(Cookie cookie) { + throw new UnsupportedOperationException(); + } + + @Override + public MutableHttpRequest uri(URI uri) { + this.uri = uri; + return this; + } + + @Override + @NonNull + public URI getUri() { + if (uri == null) { + return getDelegate().getUri(); + } else { + return uri; + } + } + + @NonNull + @Override + public MutableHttpParameters getParameters() { + return (MutableHttpParameters) super.getParameters(); + } + + @NonNull + @Override + public MutableHttpHeaders getHeaders() { + return (MutableHttpHeaders) super.getHeaders(); + } + + @SuppressWarnings("unchecked") + @Override + public MutableHttpRequest body(T body) { + this.body = (B) body; + return (MutableHttpRequest) this; + } +} diff --git a/http-client/src/main/java/io/micronaut/http/client/netty/NettyClientCustomizer.java b/http-client/src/main/java/io/micronaut/http/client/netty/NettyClientCustomizer.java index cfe383afbea..a95e029f37f 100644 --- a/http-client/src/main/java/io/micronaut/http/client/netty/NettyClientCustomizer.java +++ b/http-client/src/main/java/io/micronaut/http/client/netty/NettyClientCustomizer.java @@ -79,6 +79,12 @@ enum ChannelRole { * {@link io.netty.channel.socket.SocketChannel}, representing an HTTP connection. */ CONNECTION, + /** + * The channel is a HTTP2 stream channel. + * + * @since 4.0.0 + */ + HTTP2_STREAM, } /** diff --git a/http-client/src/main/java/io/micronaut/http/client/netty/PoolResizer.java b/http-client/src/main/java/io/micronaut/http/client/netty/PoolResizer.java new file mode 100644 index 00000000000..2a0a28e65c4 --- /dev/null +++ b/http-client/src/main/java/io/micronaut/http/client/netty/PoolResizer.java @@ -0,0 +1,282 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.http.client.netty; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.Nullable; +import io.micronaut.http.client.HttpClientConfiguration; +import io.micronaut.http.client.exceptions.HttpClientException; +import org.slf4j.Logger; +import reactor.core.publisher.Sinks; + +import java.util.Deque; +import java.util.List; +import java.util.concurrent.ConcurrentLinkedDeque; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Consumer; + +/** + * This class handles the sizing of a connection pool to conform to the configuration in + * {@link io.micronaut.http.client.HttpClientConfiguration.ConnectionPoolConfiguration}. + *

+ * This class consists of various mutator methods (e.g. {@link #addPendingRequest}) that + * may be called concurrently and in a reentrant fashion (e.g. inside {@link #openNewConnection()}). + * These mutator methods update their respective fields and then mark this class as + * {@link #dirty()}. The state management logic ensures that {@link #doSomeWork()} is called in a + * serialized fashion (no concurrency or reentrancy) at least once after each {@link #dirty()} + * call. + */ +@Internal +abstract class PoolResizer { + private final Logger log; + private final HttpClientConfiguration.ConnectionPoolConfiguration connectionPoolConfiguration; + + private final AtomicReference state = new AtomicReference<>(WorkState.IDLE); + + private final AtomicInteger pendingConnectionCount = new AtomicInteger(0); + + private final Deque> pendingRequests = new ConcurrentLinkedDeque<>(); + private final List http1Connections = new CopyOnWriteArrayList<>(); + private final List http2Connections = new CopyOnWriteArrayList<>(); + + PoolResizer(Logger log, HttpClientConfiguration.ConnectionPoolConfiguration connectionPoolConfiguration) { + this.log = log; + this.connectionPoolConfiguration = connectionPoolConfiguration; + } + + private void dirty() { + WorkState before = state.getAndUpdate(ws -> { + if (ws == WorkState.IDLE) { + return WorkState.ACTIVE_WITHOUT_PENDING_WORK; + } else { + return WorkState.ACTIVE_WITH_PENDING_WORK; + } + }); + if (before != WorkState.IDLE) { + // already in one of the active states, another thread will take care of our changes + return; + } + // we were in idle state, this thread will handle the changes. + while (true) { + try { + doSomeWork(); + } catch (Throwable t) { + // this is probably an irrecoverable failure, we need to bail immediately, but + // avoid locking up the state. Another thread might be able to continue work. + state.set(WorkState.IDLE); + throw t; + } + + WorkState endState = state.updateAndGet(ws -> { + if (ws == WorkState.ACTIVE_WITH_PENDING_WORK) { + return WorkState.ACTIVE_WITHOUT_PENDING_WORK; + } else { + return WorkState.IDLE; + } + }); + if (endState == WorkState.IDLE) { + // nothing else to do \o/ + break; + } + } + } + + private void doSomeWork() { + while (true) { + Sinks.One toDispatch = pendingRequests.pollFirst(); + if (toDispatch == null) { + break; + } + boolean dispatched = false; + for (ResizerConnection c : http2Connections) { + if (dispatchSafe(c, toDispatch)) { + dispatched = true; + break; + } + } + if (!dispatched) { + for (ResizerConnection c : http1Connections) { + if (dispatchSafe(c, toDispatch)) { + dispatched = true; + break; + } + } + } + if (!dispatched) { + pendingRequests.addFirst(toDispatch); + break; + } + } + + // snapshot our fields + int pendingRequestCount = this.pendingRequests.size(); + int pendingConnectionCount = this.pendingConnectionCount.get(); + int http1ConnectionCount = this.http1Connections.size(); + int http2ConnectionCount = this.http2Connections.size(); + + if (pendingRequestCount == 0) { + // if there are no pending requests, there is nothing to do. + return; + } + int connectionsToOpen = pendingRequestCount - pendingConnectionCount; + // make sure we won't exceed our config setting for pending connections + connectionsToOpen = Math.min(connectionsToOpen, connectionPoolConfiguration.getMaxPendingConnections() - pendingConnectionCount); + // limit the connection count to the protocol-specific settings, but only if that protocol was seen for this pool. + if (http1ConnectionCount > 0) { + connectionsToOpen = Math.min(connectionsToOpen, connectionPoolConfiguration.getMaxConcurrentHttp1Connections() - http1ConnectionCount); + } + if (http2ConnectionCount > 0) { + connectionsToOpen = Math.min(connectionsToOpen, connectionPoolConfiguration.getMaxConcurrentHttp2Connections() - http2ConnectionCount); + } + + if (connectionsToOpen > 0) { + this.pendingConnectionCount.addAndGet(connectionsToOpen); + for (int i = 0; i < connectionsToOpen; i++) { + try { + openNewConnection(); + } catch (Exception e) { + try { + onNewConnectionFailure(e); + } catch (Exception f) { + log.error("Internal error", f); + } + } + } + dirty(); + } + } + + private boolean dispatchSafe(ResizerConnection connection, Sinks.One toDispatch) { + try { + return connection.dispatch(toDispatch); + } catch (Exception e) { + try { + if (toDispatch.tryEmitError(e) != Sinks.EmitResult.OK) { + // this is probably fine, log it anyway + log.debug("Failure during connection dispatch operation, but dispatch request was already complete.", e); + } + } catch (Exception f) { + log.error("Internal error", f); + } + return true; + } + } + + abstract void openNewConnection() throws Exception; + + static boolean incrementWithLimit(AtomicInteger variable, int limit) { + while (true) { + int old = variable.get(); + if (old >= limit) { + return false; + } + if (variable.compareAndSet(old, old + 1)) { + return true; + } + } + } + + // can be overridden, so `throws Exception` ensures we handle any errors + void onNewConnectionFailure(@Nullable Throwable error) throws Exception { + // todo: implement a circuit breaker here? right now, we just fail one connection in the + // subclass implementation, but maybe we should do more. + pendingConnectionCount.decrementAndGet(); + dirty(); + } + + final void onNewConnectionEstablished1(ResizerConnection connection) { + http1Connections.add(connection); + pendingConnectionCount.decrementAndGet(); + dirty(); + } + + final void onNewConnectionEstablished2(ResizerConnection connection) { + http2Connections.add(connection); + pendingConnectionCount.decrementAndGet(); + dirty(); + } + + final void onConnectionInactive1(ResizerConnection connection) { + http1Connections.remove(connection); + dirty(); + } + + final void onConnectionInactive2(ResizerConnection connection) { + http2Connections.remove(connection); + dirty(); + } + + final void addPendingRequest(Sinks.One sink) { + if (pendingRequests.size() >= connectionPoolConfiguration.getMaxPendingAcquires()) { + sink.tryEmitError(new HttpClientException("Cannot acquire connection, exceeded max pending acquires configuration")); + return; + } + pendingRequests.addLast(sink); + dirty(); + } + + @Nullable + final Sinks.One pollPendingRequest() { + Sinks.One req = pendingRequests.pollFirst(); + if (req != null) { + dirty(); + } + return req; + } + + final void markConnectionAvailable() { + dirty(); + } + + final void forEachConnection(Consumer c) { + for (ResizerConnection http1Connection : http1Connections) { + c.accept(http1Connection); + } + for (ResizerConnection http2Connection : http2Connections) { + c.accept(http2Connection); + } + } + + private enum WorkState { + /** + * There are no pending changes, and nobody is currently executing {@link #doSomeWork()}. + */ + IDLE, + /** + * Someone is currently executing {@link #doSomeWork()}, but there were further changes + * after {@link #doSomeWork()} was called, so it needs to be called again. + */ + ACTIVE_WITH_PENDING_WORK, + /** + * Someone is currently executing {@link #doSomeWork()}, and there were no other changes + * since then. + */ + ACTIVE_WITHOUT_PENDING_WORK, + } + + abstract static class ResizerConnection { + /** + * Attempt to dispatch a stream on this connection. + * + * @param sink The pending request that wants to acquire this connection + * @return {@code true} if the acquisition may succeed (if it fails later, the pending + * request must be readded), or {@code false} if it fails immediately + */ + abstract boolean dispatch(Sinks.One sink) throws Exception; + } +} diff --git a/http-client/src/main/java/io/micronaut/http/client/netty/ssl/NettyClientSslBuilder.java b/http-client/src/main/java/io/micronaut/http/client/netty/ssl/NettyClientSslBuilder.java index 9c043c6c91f..e8e3864415f 100644 --- a/http-client/src/main/java/io/micronaut/http/client/netty/ssl/NettyClientSslBuilder.java +++ b/http-client/src/main/java/io/micronaut/http/client/netty/ssl/NettyClientSslBuilder.java @@ -17,8 +17,10 @@ import io.micronaut.context.annotation.BootstrapContextCompatible; import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.Nullable; import io.micronaut.core.io.ResourceResolver; import io.micronaut.http.HttpVersion; +import io.micronaut.http.client.HttpVersionSelection; import io.micronaut.http.ssl.AbstractClientSslConfiguration; import io.micronaut.http.ssl.ClientAuthentication; import io.micronaut.http.ssl.SslBuilder; @@ -26,7 +28,6 @@ import io.micronaut.http.ssl.SslConfigurationException; import io.netty.handler.codec.http2.Http2SecurityUtil; import io.netty.handler.ssl.ApplicationProtocolConfig; -import io.netty.handler.ssl.ApplicationProtocolNames; import io.netty.handler.ssl.ClientAuth; import io.netty.handler.ssl.SslContext; import io.netty.handler.ssl.SslContextBuilder; @@ -53,7 +54,7 @@ @Singleton @Internal @BootstrapContextCompatible -public class NettyClientSslBuilder extends SslBuilder { +public final class NettyClientSslBuilder extends SslBuilder { private static final Logger LOG = LoggerFactory.getLogger(NettyClientSslBuilder.class); /** @@ -71,20 +72,24 @@ public Optional build(SslConfiguration ssl) { @Override public Optional build(SslConfiguration ssl, HttpVersion httpVersion) { + return Optional.ofNullable(build(ssl, HttpVersionSelection.forLegacyVersion(httpVersion))); + } + + @Nullable + public SslContext build(SslConfiguration ssl, HttpVersionSelection versionSelection) { if (!ssl.isEnabled()) { - return Optional.empty(); + return null; } - final boolean isHttp2 = httpVersion == HttpVersion.HTTP_2_0; SslContextBuilder sslBuilder = SslContextBuilder - .forClient() - .keyManager(getKeyManagerFactory(ssl)) - .trustManager(getTrustManagerFactory(ssl)); + .forClient() + .keyManager(getKeyManagerFactory(ssl)) + .trustManager(getTrustManagerFactory(ssl)); if (ssl.getProtocols().isPresent()) { sslBuilder.protocols(ssl.getProtocols().get()); } if (ssl.getCiphers().isPresent()) { sslBuilder = sslBuilder.ciphers(Arrays.asList(ssl.getCiphers().get())); - } else if (isHttp2) { + } else if (versionSelection.isHttp2CipherSuites()) { sslBuilder.ciphers(Http2SecurityUtil.CIPHERS, SupportedCipherSuiteFilter.INSTANCE); } if (ssl.getClientAuthentication().isPresent()) { @@ -95,20 +100,19 @@ public Optional build(SslConfiguration ssl, HttpVersion httpVersion) sslBuilder = sslBuilder.clientAuth(ClientAuth.OPTIONAL); } } - if (isHttp2) { + if (versionSelection.isAlpn()) { SslProvider provider = SslProvider.isAlpnSupported(SslProvider.OPENSSL) ? SslProvider.OPENSSL : SslProvider.JDK; sslBuilder.sslProvider(provider); sslBuilder.applicationProtocolConfig(new ApplicationProtocolConfig( - ApplicationProtocolConfig.Protocol.ALPN, - ApplicationProtocolConfig.SelectorFailureBehavior.NO_ADVERTISE, - ApplicationProtocolConfig.SelectedListenerFailureBehavior.ACCEPT, - ApplicationProtocolNames.HTTP_1_1, - ApplicationProtocolNames.HTTP_2 + ApplicationProtocolConfig.Protocol.ALPN, + ApplicationProtocolConfig.SelectorFailureBehavior.NO_ADVERTISE, + ApplicationProtocolConfig.SelectedListenerFailureBehavior.ACCEPT, + versionSelection.getAlpnSupportedProtocols() )); } try { - return Optional.of(sslBuilder.build()); + return sslBuilder.build(); } catch (SSLException ex) { throw new SslConfigurationException("An error occurred while setting up SSL", ex); } diff --git a/http-client/src/main/java/io/micronaut/http/client/netty/websocket/NettyWebSocketClientHandler.java b/http-client/src/main/java/io/micronaut/http/client/netty/websocket/NettyWebSocketClientHandler.java index 5e825b38400..0bf36fc9700 100644 --- a/http-client/src/main/java/io/micronaut/http/client/netty/websocket/NettyWebSocketClientHandler.java +++ b/http-client/src/main/java/io/micronaut/http/client/netty/websocket/NettyWebSocketClientHandler.java @@ -146,7 +146,14 @@ public void handlerAdded(final ChannelHandlerContext ctx) { @Override public void channelActive(final ChannelHandlerContext ctx) { - handshaker.handshake(ctx.channel()); + handshaker.handshake(ctx.channel()).addListener(future -> { + if (future.isSuccess()) { + ctx.channel().config().setAutoRead(true); + ctx.read(); + } else { + handshakeFuture.tryFailure(future.cause()); + } + }); } @Override diff --git a/http-client/src/test/groovy/io/micronaut/http/client/ConnectTTLHandlerSpec.groovy b/http-client/src/test/groovy/io/micronaut/http/client/ConnectTTLHandlerSpec.groovy deleted file mode 100644 index e684cc596cc..00000000000 --- a/http-client/src/test/groovy/io/micronaut/http/client/ConnectTTLHandlerSpec.groovy +++ /dev/null @@ -1,41 +0,0 @@ -package io.micronaut.http.client - -import io.micronaut.http.client.netty.ConnectTTLHandler -import io.netty.channel.ChannelHandlerContext -import io.netty.channel.embedded.EmbeddedChannel -import spock.lang.Specification - - -class ConnectTTLHandlerSpec extends Specification{ - - - def "RELEASE_CHANNEL should be true for those channels who's connect-ttl is reached"(){ - - setup: - MockChannel channel = new MockChannel(); - ChannelHandlerContext context = Mock() - - when: - new ConnectTTLHandler(1).handlerAdded(context) - channel.runAllPendingTasks() - - then: - _ * context.channel() >> channel - - channel.attr(ConnectTTLHandler.RELEASE_CHANNEL) - - } - - class MockChannel extends EmbeddedChannel { - MockChannel() throws Exception { - super.doRegister() - } - - void runAllPendingTasks() throws InterruptedException { - super.runPendingTasks() - while (runScheduledPendingTasks() != -1) { - Thread.sleep(1) - } - } - } -} diff --git a/http-client/src/test/groovy/io/micronaut/http/client/ConnectionTTLSpec.groovy b/http-client/src/test/groovy/io/micronaut/http/client/ConnectionTTLSpec.groovy index e2660359945..dc03f800445 100644 --- a/http-client/src/test/groovy/io/micronaut/http/client/ConnectionTTLSpec.groovy +++ b/http-client/src/test/groovy/io/micronaut/http/client/ConnectionTTLSpec.groovy @@ -8,15 +8,12 @@ import io.micronaut.http.annotation.Controller import io.micronaut.http.annotation.Get import io.micronaut.runtime.server.EmbeddedServer import io.netty.channel.Channel -import io.netty.channel.pool.AbstractChannelPoolMap import spock.lang.AutoCleanup import spock.lang.Retry import spock.lang.Shared import spock.lang.Specification import spock.util.concurrent.PollingConditions -import java.lang.reflect.Field - @Retry class ConnectionTTLSpec extends Specification { @@ -37,7 +34,7 @@ class ConnectionTTLSpec extends Specification { when:"make first request" httpClient.toBlocking().retrieve(HttpRequest.GET('/connectTTL/'),String) - Channel ch = getQueuedChannels(httpClient).first + Channel ch = getQueuedChannels(httpClient).get(0) then:"ensure that connection is open as connect-ttl is not reached" getQueuedChannels(httpClient).size() == 1 @@ -67,11 +64,11 @@ class ConnectionTTLSpec extends Specification { when:"make first request" httpClient.toBlocking().retrieve(HttpRequest.GET('/connectTTL/'),String) - Deque deque = getQueuedChannels(httpClient) + List deque = getQueuedChannels(httpClient) then:"ensure that connection is open as connect-ttl is not reached" new PollingConditions().eventually { - deque.first.isOpen() + deque.get(0).isOpen() } when:"make another request after some time" @@ -80,7 +77,7 @@ class ConnectionTTLSpec extends Specification { then:"ensure channel is still open" new PollingConditions().eventually { - deque.first.isOpen() + deque.get(0).isOpen() } cleanup: @@ -99,11 +96,11 @@ class ConnectionTTLSpec extends Specification { when:"make first request" httpClient.toBlocking().retrieve(HttpRequest.GET('/connectTTL/'),String) - Deque deque = getQueuedChannels(httpClient) + Collection deque = getQueuedChannels(httpClient) then:"ensure that connection is open as connect-ttl is not reached" new PollingConditions().eventually { - deque.first.isOpen() + deque.get(0).isOpen() } when:"make another request" @@ -111,7 +108,7 @@ class ConnectionTTLSpec extends Specification { then:"ensure channel is still open" new PollingConditions().eventually { - deque.first.isOpen() + deque.get(0).isOpen() } cleanup: @@ -130,7 +127,7 @@ class ConnectionTTLSpec extends Specification { when:"make first request" httpClient.toBlocking().retrieve(HttpRequest.GET('/connectTTL/'),String) - Channel ch = getQueuedChannels(httpClient).first + Channel ch = getQueuedChannels(httpClient).get(0) then:"ensure that connection is open as connect-ttl is not reached" getQueuedChannels(httpClient).size() == 1 @@ -149,12 +146,8 @@ class ConnectionTTLSpec extends Specification { clientContext.close() } - Deque getQueuedChannels(HttpClient client) { - AbstractChannelPoolMap poolMap = client.connectionManager.poolMap - Field mapField = AbstractChannelPoolMap.getDeclaredField("map") - mapField.setAccessible(true) - Map innerMap = mapField.get(poolMap) - return innerMap.values().first().deque + List getQueuedChannels(HttpClient client) { + return client.connectionManager.channels } @Requires(property = 'spec.name', value = 'ConnectionTTLSpec') diff --git a/http-client/src/test/groovy/io/micronaut/http/client/IdleTimeoutSpec.groovy b/http-client/src/test/groovy/io/micronaut/http/client/IdleTimeoutSpec.groovy index a65805a92bf..c5edd768f3f 100644 --- a/http-client/src/test/groovy/io/micronaut/http/client/IdleTimeoutSpec.groovy +++ b/http-client/src/test/groovy/io/micronaut/http/client/IdleTimeoutSpec.groovy @@ -8,15 +8,12 @@ import io.micronaut.http.annotation.Controller import io.micronaut.http.annotation.Get import io.micronaut.runtime.server.EmbeddedServer import io.netty.channel.Channel -import io.netty.channel.pool.AbstractChannelPoolMap import spock.lang.AutoCleanup import spock.lang.Retry import spock.lang.Shared import spock.lang.Specification import spock.util.concurrent.PollingConditions -import java.lang.reflect.Field - @Retry class IdleTimeoutSpec extends Specification { @@ -35,11 +32,10 @@ class IdleTimeoutSpec extends Specification { when: "make first request" httpClient.toBlocking().retrieve(HttpRequest.GET('/idleTimeout/'), String) - Deque deque = getQueuedChannels(httpClient) - Channel ch1 = deque.first + Channel ch1 = getQueuedChannels(httpClient).get(0) then: "ensure that connection is open as connection-pool-idle-timeout is not reached" - deque.size() == 1 + getQueuedChannels(httpClient).size() == 1 ch1.isOpen() new PollingConditions(timeout: 2).eventually { !ch1.isOpen() @@ -50,14 +46,14 @@ class IdleTimeoutSpec extends Specification { then: new PollingConditions().eventually { - assert deque.size() > 0 + assert getQueuedChannels(httpClient).size() > 0 } when: - Channel ch2 = deque.first + Channel ch2 = getQueuedChannels(httpClient).get(0) then: "ensure channel 2 is open and channel 2 != channel 1" - deque.size() == 1 + getQueuedChannels(httpClient).size() == 1 ch1 != ch2 ch2.isOpen() new PollingConditions(timeout: 2).eventually { @@ -79,13 +75,13 @@ class IdleTimeoutSpec extends Specification { when: "make first request" httpClient.toBlocking().retrieve(HttpRequest.GET('/idleTimeout/'), String) - Deque deque = getQueuedChannels(httpClient) - Channel ch1 = deque.first + List deque = getQueuedChannels(httpClient) + Channel ch1 = deque.get(0) then: "ensure that connection is open as connection-pool-idle-timeout is not reached" deque.size() == 1 new PollingConditions().eventually { - deque.first.isOpen() + deque.get(0).isOpen() } when: "make another request" @@ -97,13 +93,13 @@ class IdleTimeoutSpec extends Specification { } when: - Channel ch2 = deque.first + Channel ch2 = deque.get(0) then: "ensure channel is still open" ch1 == ch2 deque.size() == 1 new PollingConditions().eventually { - deque.first.isOpen() + deque.get(0).isOpen() } cleanup: @@ -111,12 +107,8 @@ class IdleTimeoutSpec extends Specification { clientContext.close() } - Deque getQueuedChannels(HttpClient client) { - AbstractChannelPoolMap poolMap = client.connectionManager.poolMap - Field mapField = AbstractChannelPoolMap.getDeclaredField("map") - mapField.setAccessible(true) - Map innerMap = mapField.get(poolMap) - return innerMap.values().first().deque + List getQueuedChannels(HttpClient client) { + return client.connectionManager.channels } @Requires(property = 'spec.name', value = 'IdleTimeoutSpec') diff --git a/http-client/src/test/groovy/io/micronaut/http/client/ReadTimeoutSpec.groovy b/http-client/src/test/groovy/io/micronaut/http/client/ReadTimeoutSpec.groovy index a04d8e137c0..9debe5c3b98 100644 --- a/http-client/src/test/groovy/io/micronaut/http/client/ReadTimeoutSpec.groovy +++ b/http-client/src/test/groovy/io/micronaut/http/client/ReadTimeoutSpec.groovy @@ -298,11 +298,10 @@ class ReadTimeoutSpec extends Specification { .filter { it.clientId == "http://localhost:${embeddedServer.getPort()}" } .findFirst() .get() - def pool = getPool(clients.get(clientKey)) then:"Connections are not leaked" conditions.eventually { - pool.acquiredChannelCount() == 0 + clients.get(clientKey).connectionManager.liveRequestCount() == 0 } cleanup: diff --git a/http-client/src/test/groovy/io/micronaut/http/client/SslRefreshSpec.groovy b/http-client/src/test/groovy/io/micronaut/http/client/SslRefreshSpec.groovy index 40a68f9eb8b..90770f722d8 100644 --- a/http-client/src/test/groovy/io/micronaut/http/client/SslRefreshSpec.groovy +++ b/http-client/src/test/groovy/io/micronaut/http/client/SslRefreshSpec.groovy @@ -43,7 +43,10 @@ class SslRefreshSpec extends Specification { 'micronaut.http.client.ssl.client-authentication': 'NEED', 'micronaut.http.client.ssl.key-store.path': 'classpath:certs/client1.p12', 'micronaut.http.client.ssl.key-store.password': 'secret', - 'micronaut.http.client.ssl.insecure-trust-all-certificates': true + 'micronaut.http.client.ssl.insecure-trust-all-certificates': true, + 'micronaut.http.client.pool.enabled': false, + // need to force http1 because our ciphers are not supported by http2 + 'micronaut.http.client.http-version': '1.1', ] @Shared @AutoCleanup EmbeddedServer embeddedServer = ApplicationContext .builder() diff --git a/http-client/src/test/groovy/io/micronaut/http/client/config/DefaultHttpClientConfigurationSpec.groovy b/http-client/src/test/groovy/io/micronaut/http/client/config/DefaultHttpClientConfigurationSpec.groovy index ec839c1b0ae..6a2ed45e48b 100644 --- a/http-client/src/test/groovy/io/micronaut/http/client/config/DefaultHttpClientConfigurationSpec.groovy +++ b/http-client/src/test/groovy/io/micronaut/http/client/config/DefaultHttpClientConfigurationSpec.groovy @@ -75,7 +75,6 @@ class DefaultHttpClientConfigurationSpec extends Specification { where: key | property | value | expected 'enabled' | 'enabled' | 'false' | false - 'max-connections' | 'maxConnections' | '10' | 10 } void "test overriding logger for the client"() { diff --git a/http-client/src/test/groovy/io/micronaut/http/client/netty/ConnectionManagerSpec.groovy b/http-client/src/test/groovy/io/micronaut/http/client/netty/ConnectionManagerSpec.groovy new file mode 100644 index 00000000000..3aba2aab49f --- /dev/null +++ b/http-client/src/test/groovy/io/micronaut/http/client/netty/ConnectionManagerSpec.groovy @@ -0,0 +1,1226 @@ +package io.micronaut.http.client.netty + +import io.micronaut.context.ApplicationContext +import io.micronaut.context.event.BeanCreatedEvent +import io.micronaut.context.event.BeanCreatedEventListener +import io.micronaut.http.HttpRequest +import io.micronaut.http.HttpResponse +import io.micronaut.http.HttpStatus +import io.micronaut.http.HttpVersion +import io.micronaut.http.MediaType +import io.micronaut.http.client.HttpClient +import io.micronaut.http.client.StreamingHttpClient +import io.micronaut.http.client.exceptions.ReadTimeoutException +import io.micronaut.http.client.multipart.MultipartBody +import io.micronaut.http.netty.channel.ChannelPipelineCustomizer +import io.micronaut.http.server.netty.ssl.CertificateProvidedSslBuilder +import io.micronaut.http.ssl.SslConfiguration +import io.micronaut.websocket.WebSocketSession +import io.micronaut.websocket.annotation.ClientWebSocket +import io.micronaut.websocket.annotation.OnMessage +import io.netty.buffer.ByteBufAllocator +import io.netty.buffer.Unpooled +import io.netty.channel.Channel +import io.netty.channel.ChannelFuture +import io.netty.channel.ChannelHandler +import io.netty.channel.ChannelHandlerContext +import io.netty.channel.ChannelId +import io.netty.channel.ChannelInboundHandlerAdapter +import io.netty.channel.ChannelInitializer +import io.netty.channel.ChannelPromise +import io.netty.channel.ServerChannel +import io.netty.channel.embedded.EmbeddedChannel +import io.netty.handler.codec.http.DefaultFullHttpResponse +import io.netty.handler.codec.http.DefaultHttpContent +import io.netty.handler.codec.http.DefaultHttpResponse +import io.netty.handler.codec.http.DefaultLastHttpContent +import io.netty.handler.codec.http.FullHttpRequest +import io.netty.handler.codec.http.HttpContentCompressor +import io.netty.handler.codec.http.HttpMethod +import io.netty.handler.codec.http.HttpObjectAggregator +import io.netty.handler.codec.http.HttpResponseStatus +import io.netty.handler.codec.http.HttpServerCodec +import io.netty.handler.codec.http.HttpServerUpgradeHandler +import io.netty.handler.codec.http.LastHttpContent +import io.netty.handler.codec.http.websocketx.TextWebSocketFrame +import io.netty.handler.codec.http.websocketx.WebSocketServerHandshakerFactory +import io.netty.handler.codec.http2.DefaultHttp2DataFrame +import io.netty.handler.codec.http2.DefaultHttp2Headers +import io.netty.handler.codec.http2.DefaultHttp2HeadersFrame +import io.netty.handler.codec.http2.Http2FrameCodec +import io.netty.handler.codec.http2.Http2FrameCodecBuilder +import io.netty.handler.codec.http2.Http2FrameStream +import io.netty.handler.codec.http2.Http2FrameStreamEvent +import io.netty.handler.codec.http2.Http2Headers +import io.netty.handler.codec.http2.Http2HeadersFrame +import io.netty.handler.codec.http2.Http2ResetFrame +import io.netty.handler.codec.http2.Http2ServerUpgradeCodec +import io.netty.handler.codec.http2.Http2SettingsAckFrame +import io.netty.handler.codec.http2.Http2SettingsFrame +import io.netty.handler.codec.http2.Http2Stream +import io.netty.handler.logging.LogLevel +import io.netty.handler.logging.LoggingHandler +import io.netty.handler.ssl.ApplicationProtocolNegotiationHandler +import io.netty.handler.ssl.SslContextBuilder +import io.netty.handler.ssl.util.SelfSignedCertificate +import io.netty.util.AsciiString +import io.netty.util.concurrent.GenericFutureListener +import jakarta.inject.Singleton +import org.spockframework.runtime.model.parallel.ExecutionMode +import reactor.core.publisher.Flux +import reactor.core.publisher.Mono +import spock.lang.Execution +import spock.lang.Specification +import spock.lang.Unroll + +import java.nio.charset.StandardCharsets +import java.util.concurrent.CompletableFuture +import java.util.concurrent.ExecutionException +import java.util.concurrent.Future +import java.util.concurrent.TimeUnit +import java.util.zip.GZIPOutputStream + +@Execution(ExecutionMode.CONCURRENT) +class ConnectionManagerSpec extends Specification { + private static void patch(DefaultHttpClient httpClient, EmbeddedTestConnectionBase... connections) { + httpClient.connectionManager = new ConnectionManager(httpClient.connectionManager) { + int i = 0 + + @Override + protected ChannelFuture doConnect(DefaultHttpClient.RequestKey requestKey, ChannelInitializer channelInitializer) { + try { + def connection = connections[i++] + connection.clientChannel = new EmbeddedChannel(new DummyChannelId('client' + i), connection.clientInitializer, channelInitializer) + def promise = connection.clientChannel.newPromise() + promise.setSuccess() + return promise + } catch (Throwable t) { + // print it immediately to make sure it's not swallowed + t.printStackTrace() + throw t + } + } + } + } + + def 'simple http2 get'() { + def ctx = ApplicationContext.run([ + 'micronaut.http.client.ssl.insecure-trust-all-certificates': true, + ]) + def client = ctx.getBean(DefaultHttpClient) + + def conn = new EmbeddedTestConnectionHttp2() + conn.setupHttp2Tls() + patch(client, conn) + + def future = conn.testExchangeRequest(client) + conn.exchangeSettings() + conn.testExchangeResponse(future) + + cleanup: + client.close() + ctx.close() + } + + def 'http2 streaming get'() { + def ctx = ApplicationContext.run([ + 'micronaut.http.client.ssl.insecure-trust-all-certificates': true, + ]) + def client = ctx.getBean(DefaultHttpClient) + + def conn = new EmbeddedTestConnectionHttp2() + conn.setupHttp2Tls() + patch(client, conn) + + def r1 = conn.testStreamingRequest(client) + conn.exchangeSettings() + conn.testStreamingResponse(r1) + + cleanup: + client.close() + ctx.close() + } + + def 'simple http1 get'() { + def ctx = ApplicationContext.run() + def client = ctx.getBean(DefaultHttpClient) + + def conn = new EmbeddedTestConnectionHttp1() + conn.setupHttp1() + patch(client, conn) + + conn.testExchangeResponse(conn.testExchangeRequest(client)) + + cleanup: + client.close() + ctx.close() + } + + def 'http1 get with compression'() { + def ctx = ApplicationContext.run() + def client = ctx.getBean(DefaultHttpClient) + + def conn = new EmbeddedTestConnectionHttp1() + conn.setupHttp1() + conn.serverChannel.pipeline().addLast(new HttpContentCompressor()) + patch(client, conn) + + def future = Mono.from(client.exchange( + HttpRequest.GET('http://example.com/foo').header('accept-encoding', 'gzip'), String)).toFuture() + future.exceptionally(t -> t.printStackTrace()) + conn.advance() + + assert conn.serverChannel.readInbound() instanceof io.netty.handler.codec.http.HttpRequest + + def response = new DefaultFullHttpResponse(io.netty.handler.codec.http.HttpVersion.HTTP_1_1, HttpResponseStatus.OK, Unpooled.wrappedBuffer("foo".bytes)) + response.headers().add('content-length', 3) + conn.serverChannel.writeOutbound(response) + + conn.advance() + assert future.get().status() == HttpStatus.OK + assert future.get().body() == 'foo' + + cleanup: + client.close() + ctx.close() + } + + def 'http2 get with compression'() { + def ctx = ApplicationContext.run([ + 'micronaut.http.client.ssl.insecure-trust-all-certificates': true, + ]) + def client = ctx.getBean(DefaultHttpClient) + + def conn = new EmbeddedTestConnectionHttp2() + conn.setupHttp2Tls() + patch(client, conn) + + def future = Mono.from(client.exchange('https://example.com/foo', String)).toFuture() + future.exceptionally(t -> t.printStackTrace()) + conn.exchangeSettings() + + Http2HeadersFrame request = conn.serverChannel.readInbound() + def responseHeaders = new DefaultHttp2Headers() + responseHeaders.add(Http2Headers.PseudoHeaderName.STATUS.value(), "200") + responseHeaders.add('content-encoding', "gzip") + conn.serverChannel.writeOutbound(new DefaultHttp2HeadersFrame(responseHeaders, false).stream(request.stream())) + def compressedOut = new ByteArrayOutputStream() + try (OutputStream os = new GZIPOutputStream(compressedOut)) { + os.write('foo'.bytes) + } + conn.serverChannel.writeOutbound(new DefaultHttp2DataFrame(Unpooled.wrappedBuffer(compressedOut.toByteArray()), true).stream(request.stream())) + + conn.advance() + def response = future.get() + assert response.status() == HttpStatus.OK + assert response.body() == 'foo' + + cleanup: + client.close() + ctx.close() + } + + def 'simple http1 tls get'() { + def ctx = ApplicationContext.run([ + 'micronaut.http.client.ssl.insecure-trust-all-certificates': true, + ]) + def client = ctx.getBean(DefaultHttpClient) + + def conn = new EmbeddedTestConnectionHttp1() + conn.setupHttp1Tls() + patch(client, conn) + + conn.testExchangeResponse(conn.testExchangeRequest(client)) + + cleanup: + client.close() + ctx.close() + } + + def 'simple h2c get'() { + def ctx = ApplicationContext.run([ + 'micronaut.http.client.plaintext-mode': 'h2c', + ]) + def client = ctx.getBean(DefaultHttpClient) + + def conn = new EmbeddedTestConnectionHttp2() + conn.setupH2c() + patch(client, conn) + + def future = conn.testExchangeRequest(client) + conn.exchangeH2c() + conn.testExchangeResponse(future) + + cleanup: + client.close() + ctx.close() + } + + def 'http1 streaming get'() { + def ctx = ApplicationContext.run() + def client = ctx.getBean(DefaultHttpClient) + + def conn = new EmbeddedTestConnectionHttp1() + conn.setupHttp1() + patch(client, conn) + + conn.testStreamingResponse(conn.testStreamingRequest(client)) + + cleanup: + client.close() + ctx.close() + } + + def 'http2 concurrent stream'() { + given: + def ctx = ApplicationContext.run([ + 'micronaut.http.client.ssl.insecure-trust-all-certificates': true, + ]) + def client = ctx.getBean(DefaultHttpClient) + + def conn1 = new EmbeddedTestConnectionHttp2() + conn1.setupHttp2Tls() + def conn2 = new EmbeddedTestConnectionHttp2() + conn2.setupHttp2Tls() + patch(client, conn1, conn2) + + when: + // start two requests. this will open two connections + def f1 = Mono.from(client.exchange('https://example.com/r1')).toFuture() + f1.exceptionally(t -> t.printStackTrace()) + def f2 = Mono.from(client.exchange('https://example.com/r2')).toFuture() + f2.exceptionally(t -> t.printStackTrace()) + + then: + // no data yet, haven't finished the handshake + conn1.serverChannel.readInbound() == null + + when: + // finish handshake for first connection + conn1.exchangeSettings() + then: + // both requests immediately go to the first connection + def req1 = conn1.serverChannel. readInbound() + req1.headers().get(Http2Headers.PseudoHeaderName.PATH.value()) == '/r1' + def req2 = conn1.serverChannel. readInbound() + req2.stream().id() != req1.stream().id() + req2.headers().get(Http2Headers.PseudoHeaderName.PATH.value()) == '/r2' + + when: + // start a third request, this should reuse the existing connection + def f3 = Mono.from(client.exchange('https://example.com/r3')).toFuture() + f3.exceptionally(t -> t.printStackTrace()) + conn1.advance() + then: + def req3 = conn1.serverChannel. readInbound() + req3.stream().id() != req1.stream().id() + req3.stream().id() != req2.stream().id() + req3.headers().get(Http2Headers.PseudoHeaderName.PATH.value()) == '/r3' + + // finish up the third request + when: + conn1.respondOk(req3.stream()) + conn1.advance() + then: + f3.get().status() == HttpStatus.OK + + // finish up the second and first request + when: + conn1.respondOk(req2.stream()) + conn1.respondOk(req1.stream()) + conn1.advance() + then: + f1.get().status() == HttpStatus.OK + f2.get().status() == HttpStatus.OK + + cleanup: + client.close() + ctx.close() + } + + def 'http1 reuse'() { + def ctx = ApplicationContext.run() + def client = ctx.getBean(DefaultHttpClient) + + def conn = new EmbeddedTestConnectionHttp1() + conn.setupHttp1() + patch(client, conn) + + conn.testExchangeResponse(conn.testExchangeRequest(client)) + + Queue responseData1 = conn.testStreamingRequest(client) + conn.testStreamingResponse(responseData1) + conn.testExchangeResponse(conn.testExchangeRequest(client)) + Queue responseData = conn.testStreamingRequest(client) + conn.testStreamingResponse(responseData) + + cleanup: + client.close() + ctx.close() + } + + def 'http1 plain text customization'() { + given: + def ctx = ApplicationContext.run() + def client = ctx.getBean(DefaultHttpClient) + def tracker = ctx.getBean(CustomizerTracker) + + def conn = new EmbeddedTestConnectionHttp1() + conn.setupHttp1() + patch(client, conn) + + when: + conn.testExchangeResponse(conn.testExchangeRequest(client)) + + Queue responseData = conn.testStreamingRequest(client) + conn.testStreamingResponse(responseData) + + then: + def outerChannel = tracker.initialPipelineBuilt.poll() + outerChannel.channel == conn.clientChannel + outerChannel.handlerNames.contains(ChannelPipelineCustomizer.HANDLER_HTTP_CLIENT_CODEC) + outerChannel.handlerNames.contains(ChannelPipelineCustomizer.HANDLER_HTTP_DECODER) + !outerChannel.handlerNames.contains(ChannelPipelineCustomizer.HANDLER_HTTP_AGGREGATOR) + !outerChannel.handlerNames.contains(ChannelPipelineCustomizer.HANDLER_HTTP_STREAM) + tracker.initialPipelineBuilt.isEmpty() + + def innerChannel = tracker.streamPipelineBuilt.poll() + innerChannel.channel == conn.clientChannel + innerChannel.handlerNames == outerChannel.handlerNames + tracker.streamPipelineBuilt.isEmpty() + + def req1Channel = tracker.requestPipelineBuilt.poll() + req1Channel.channel == conn.clientChannel + req1Channel.handlerNames.contains(ChannelPipelineCustomizer.HANDLER_HTTP_AGGREGATOR) + req1Channel.handlerNames.contains(ChannelPipelineCustomizer.HANDLER_MICRONAUT_FULL_HTTP_RESPONSE) + + def req2Channel = tracker.requestPipelineBuilt.poll() + req2Channel.channel == conn.clientChannel + req2Channel.handlerNames.contains(ChannelPipelineCustomizer.HANDLER_MICRONAUT_HTTP_RESPONSE_FULL) + req2Channel.handlerNames.contains(ChannelPipelineCustomizer.HANDLER_MICRONAUT_HTTP_RESPONSE_STREAM) + + tracker.requestPipelineBuilt.isEmpty() + + cleanup: + client.close() + ctx.close() + } + + def 'http2 customization'(boolean secure) { + given: + def ctx = ApplicationContext.run([ + 'micronaut.http.client.ssl.insecure-trust-all-certificates': true, + 'micronaut.http.client.plaintext-mode': 'h2c', + ]) + def client = ctx.getBean(DefaultHttpClient) + def tracker = ctx.getBean(CustomizerTracker) + + def conn = new EmbeddedTestConnectionHttp2() + if (secure) { + conn.setupHttp2Tls() + } else { + conn.setupH2c() + } + patch(client, conn) + + when: + def r1 = conn.testExchangeRequest(client) + if (secure) { + conn.exchangeSettings() + } else { + conn.exchangeH2c() + } + conn.testExchangeResponse(r1) + + def r2 = conn.testStreamingRequest(client) + conn.testStreamingResponse(r2) + + then: + def outerChannel = tracker.initialPipelineBuilt.poll() + outerChannel.channel == conn.clientChannel + outerChannel.handlerNames.contains(ChannelPipelineCustomizer.HANDLER_SSL) == secure + !outerChannel.handlerNames.contains(ChannelPipelineCustomizer.HANDLER_HTTP2_CONNECTION) + tracker.initialPipelineBuilt.isEmpty() + + def innerChannel = tracker.streamPipelineBuilt.poll() + innerChannel.channel == conn.clientChannel + innerChannel.handlerNames.contains(ChannelPipelineCustomizer.HANDLER_HTTP2_CONNECTION) + tracker.streamPipelineBuilt.isEmpty() + + def req1Channel = tracker.requestPipelineBuilt.poll() + req1Channel.role == NettyClientCustomizer.ChannelRole.HTTP2_STREAM + req1Channel.channel !== conn.clientChannel + req1Channel.handlerNames.contains(ChannelPipelineCustomizer.HANDLER_HTTP_AGGREGATOR) + req1Channel.handlerNames.contains(ChannelPipelineCustomizer.HANDLER_MICRONAUT_FULL_HTTP_RESPONSE) + + def req2Channel = tracker.requestPipelineBuilt.poll() + req2Channel.role == NettyClientCustomizer.ChannelRole.HTTP2_STREAM + req2Channel.channel !== conn.clientChannel + req2Channel.handlerNames.contains(ChannelPipelineCustomizer.HANDLER_MICRONAUT_HTTP_RESPONSE_FULL) + req2Channel.handlerNames.contains(ChannelPipelineCustomizer.HANDLER_MICRONAUT_HTTP_RESPONSE_STREAM) + + tracker.requestPipelineBuilt.isEmpty() + + cleanup: + client.close() + ctx.close() + + where: + secure << [true, false] + } + + def 'http1 exchange read timeout'() { + def ctx = ApplicationContext.run([ + 'micronaut.http.client.read-timeout': '5s', + ]) + def client = ctx.getBean(DefaultHttpClient) + + def conn = new EmbeddedTestConnectionHttp1() + conn.setupHttp1() + patch(client, conn) + + // do one request + conn.testExchangeResponse(conn.testExchangeRequest(client)) + conn.clientChannel.unfreezeTime() + // connection is in reserve, should not time out + TimeUnit.SECONDS.sleep(10) + conn.advance() + + // second request + def future = Mono.from(client.exchange('http://example.com/foo', String)).toFuture() + conn.advance() + + // todo: move to advanceTime once IdleStateHandler supports it + TimeUnit.SECONDS.sleep(5) + conn.advance() + + assert future.isDone() + when: + future.get() + then: + def e = thrown ExecutionException + e.cause instanceof ReadTimeoutException + + cleanup: + client.close() + ctx.close() + } + + def 'http2 exchange read timeout'() { + def ctx = ApplicationContext.run([ + 'micronaut.http.client.ssl.insecure-trust-all-certificates': true, + 'micronaut.http.client.read-timeout': '5s', + ]) + def client = ctx.getBean(DefaultHttpClient) + + def conn = new EmbeddedTestConnectionHttp2() + conn.setupHttp2Tls() + patch(client, conn) + + // one request opens the connection + def r1 = conn.testExchangeRequest(client) + conn.exchangeSettings() + conn.testExchangeResponse(r1) + conn.clientChannel.unfreezeTime() + + // connection is in reserve, should not time out + TimeUnit.SECONDS.sleep(10) + conn.advance() + + // second request + def future = Mono.from(client.exchange('https://example.com/foo', String)).toFuture() + conn.advance() + + // todo: move to advanceTime once IdleStateHandler supports it + TimeUnit.SECONDS.sleep(5) + conn.advance() + + assert future.isDone() + when: + future.get() + then: + def e = thrown ExecutionException + e.cause instanceof ReadTimeoutException + + cleanup: + client.close() + ctx.close() + } + + def 'http1 ttl'() { + def ctx = ApplicationContext.run([ + 'micronaut.http.client.connect-ttl': '100s', + ]) + def client = ctx.getBean(DefaultHttpClient) + + def conn1 = new EmbeddedTestConnectionHttp1() + conn1.setupHttp1() + def conn2 = new EmbeddedTestConnectionHttp1() + conn2.setupHttp1() + patch(client, conn1, conn2) + + def r1 = conn1.testExchangeRequest(client) + conn1.clientChannel.advanceTimeBy(101, TimeUnit.SECONDS) + conn1.testExchangeResponse(r1) + + // conn1 should expire now, conn2 will be the next connection + conn2.testExchangeResponse(conn2.testExchangeRequest(client)) + + cleanup: + client.close() + ctx.close() + } + + def 'http2 ttl'() { + def ctx = ApplicationContext.run([ + 'micronaut.http.client.ssl.insecure-trust-all-certificates': true, + 'micronaut.http.client.connect-ttl': '100s', + ]) + def client = ctx.getBean(DefaultHttpClient) + + def conn1 = new EmbeddedTestConnectionHttp2() + conn1.setupHttp2Tls() + def conn2 = new EmbeddedTestConnectionHttp2() + conn2.setupHttp2Tls() + patch(client, conn1, conn2) + + def r1 = conn1.testExchangeRequest(client) + conn1.exchangeSettings() + conn1.clientChannel.advanceTimeBy(101, TimeUnit.SECONDS) + conn1.testExchangeResponse(r1) + + // conn1 should expire now, conn2 will be the next connection + def r2 = conn2.testExchangeRequest(client) + conn2.exchangeSettings() + conn2.testExchangeResponse(r2) + + cleanup: + client.close() + ctx.close() + } + + def 'http1 pool timeout'() { + def ctx = ApplicationContext.run([ + 'micronaut.http.client.connection-pool-idle-timeout': '5s', + ]) + def client = ctx.getBean(DefaultHttpClient) + + def conn1 = new EmbeddedTestConnectionHttp1() + conn1.setupHttp1() + def conn2 = new EmbeddedTestConnectionHttp1() + conn2.setupHttp1() + patch(client, conn1, conn2) + + conn1.testExchangeResponse(conn1.testExchangeRequest(client)) + conn1.clientChannel.unfreezeTime() + // todo: move to advanceTime once IdleStateHandler supports it + TimeUnit.SECONDS.sleep(5) + conn1.advance() + // conn1 should expire now, conn2 will be the next connection + conn2.testExchangeResponse(conn2.testExchangeRequest(client)) + + cleanup: + client.close() + ctx.close() + } + + @Unroll + def 'websocket ssl=#secure'(boolean secure) { + def ctx = ApplicationContext.run([ + 'micronaut.http.client.ssl.insecure-trust-all-certificates': true, + 'micronaut.http.client.connect-ttl': '100s', + ]) + def client = ctx.getBean(DefaultHttpClient) + + def conn = new EmbeddedTestConnectionHttp1() + if (secure) { + conn.setupHttp1Tls() + } else { + conn.setupHttp1() + } + conn.serverChannel.pipeline().addLast(new HttpObjectAggregator(1024)) + patch(client, conn) + + def uri = conn.scheme + "://example.com/foo" + Mono.from(client.connect(Ws, uri)).subscribe() + conn.advance() + io.netty.handler.codec.http.HttpRequest req = conn.serverChannel.readInbound() + def handshaker = new WebSocketServerHandshakerFactory(uri, null, false).newHandshaker(req) + handshaker.handshake(conn.serverChannel, req) + conn.advance() + + conn.serverChannel.writeOutbound(new TextWebSocketFrame('foo')) + conn.advance() + TextWebSocketFrame response = conn.serverChannel.readInbound() + assert response.text() == 'received: foo' + + cleanup: + client.close() + ctx.close() + + where: + secure << [true, false] + } + + @ClientWebSocket + static class Ws implements AutoCloseable { + @Override + void close() throws Exception { + } + + @OnMessage + def onMessage(String msg, WebSocketSession session) { + return session.send('received: ' + msg) + } + } + + def 'cancel pool acquisition'() { + def ctx = ApplicationContext.run() + def client = ctx.getBean(DefaultHttpClient) + + def conn = new EmbeddedTestConnectionHttp1() + conn.setupHttp1() + + ChannelPromise delayPromise + def normalInit = conn.clientInitializer + // hack: delay the channelActive call until we complete delayPromise + conn.clientInitializer = new ChannelInitializer() { + @Override + protected void initChannel(EmbeddedChannel ch) throws Exception { + ch.pipeline().addLast(new ChannelInboundHandlerAdapter() { + @Override + void channelActive(ChannelHandlerContext chtx) throws Exception { + delayPromise = chtx.newPromise() + delayPromise.addListener(new GenericFutureListener>() { + @Override + void operationComplete(io.netty.util.concurrent.Future future) throws Exception { + chtx.fireChannelActive() + } + }) + } + }) + ch.pipeline().addLast(normalInit) + } + } + + patch(client, conn) + + def subscription = Mono.from(client.exchange(conn.scheme + '://example.com/foo')).subscribe() + conn.advance() + subscription.dispose() + // this completes the handshake + delayPromise.setSuccess() + conn.advance() + + conn.testExchangeResponse(conn.testExchangeRequest(client)) + + cleanup: + client.close() + ctx.close() + } + + def 'max pending acquires'() { + def ctx = ApplicationContext.run([ + 'micronaut.http.client.pool.max-pending-acquires': 5, + 'micronaut.http.client.pool.max-pending-connections': 1, + ]) + def client = ctx.getBean(DefaultHttpClient) + + def conn = new EmbeddedTestConnectionHttp1() + conn.setupHttp1() + + ChannelPromise delayPromise + def normalInit = conn.clientInitializer + // hack: delay the channelActive call until we complete delayPromise + conn.clientInitializer = new ChannelInitializer() { + @Override + protected void initChannel(EmbeddedChannel ch) throws Exception { + ch.pipeline().addLast(new ChannelInboundHandlerAdapter() { + @Override + void channelActive(ChannelHandlerContext chtx) throws Exception { + delayPromise = chtx.newPromise() + delayPromise.addListener(new GenericFutureListener>() { + @Override + void operationComplete(io.netty.util.concurrent.Future future) throws Exception { + chtx.fireChannelActive() + } + }) + } + }) + ch.pipeline().addLast(normalInit) + } + } + + patch(client, conn) + + List> futures = new ArrayList<>() + for (int i = 0; i < 6; i++) { + futures.add(Mono.from(client.exchange(conn.scheme + '://example.com/foo')).toFuture()) + } + conn.advance() + + for (int i = 0; i < 5; i++) { + assert !futures.get(i).isDone() + } + assert futures.get(5).isDone() + assert futures.get(5).completedExceptionally + + cleanup: + client.close() + ctx.close() + } + + def 'max http1 connections'() { + def ctx = ApplicationContext.run([ + 'micronaut.http.client.pool.max-pending-connections': 1, + 'micronaut.http.client.pool.max-concurrent-http1-connections': 2, + ]) + def client = ctx.getBean(DefaultHttpClient) + + def conn1 = new EmbeddedTestConnectionHttp1() + conn1.setupHttp1() + def conn2 = new EmbeddedTestConnectionHttp1() + conn2.setupHttp1() + + patch(client, conn1, conn2) + + // we open four requests, the first two of which will open connections. + List>> futures = [ + conn1.testExchangeRequest(client), + conn2.testExchangeRequest(client), + conn1.testExchangeRequest(client), + conn1.testExchangeRequest(client), + ] + + conn1.testExchangeResponse(futures.get(0)) + conn1.testExchangeResponse(futures.get(2)) + conn1.testExchangeResponse(futures.get(3)) + conn2.testExchangeResponse(futures.get(1)) + + cleanup: + client.close() + ctx.close() + } + + def 'multipart request'() { + def ctx = ApplicationContext.run() + def client = ctx.getBean(DefaultHttpClient) + + def conn = new EmbeddedTestConnectionHttp1() + conn.setupHttp1() + patch(client, conn) + conn.serverChannel.pipeline().addLast(new HttpObjectAggregator(1024)) + + def future = Mono.from(client.exchange(HttpRequest.POST(conn.scheme + '://example.com/foo', MultipartBody.builder() + .addPart('foo', 'fn', MediaType.TEXT_PLAIN_TYPE, 'bar'.bytes) + .build()) + .contentType(MediaType.MULTIPART_FORM_DATA), String)).toFuture() + future.exceptionally(t -> t.printStackTrace()) + conn.advance() + + FullHttpRequest request = conn.serverChannel.readInbound() + assert request.uri() == '/foo' + assert request.method() == HttpMethod.POST + assert request.headers().get('host') == 'example.com' + assert request.headers().get("connection") == "keep-alive" + assert request.content().isReadable(100) // cba to check the exact content + + def response = new DefaultFullHttpResponse(io.netty.handler.codec.http.HttpVersion.HTTP_1_1, HttpResponseStatus.OK, Unpooled.wrappedBuffer('foo'.bytes)) + response.headers().add("Content-Length", 3) + conn.serverChannel.writeOutbound(response) + conn.advance() + assert future.get().body() == 'foo' + + cleanup: + client.close() + ctx.close() + } + + def 'publisher request'() { + def ctx = ApplicationContext.run() + def client = ctx.getBean(DefaultHttpClient) + + def conn = new EmbeddedTestConnectionHttp1() + conn.setupHttp1() + patch(client, conn) + conn.serverChannel.pipeline().addLast(new HttpObjectAggregator(1024)) + + def future = Mono.from(client.exchange(HttpRequest.POST(conn.scheme + '://example.com/foo', Flux.fromIterable([1,2,3,4,5])) + .contentType(MediaType.APPLICATION_JSON_TYPE), String)).toFuture() + future.exceptionally(t -> t.printStackTrace()) + conn.advance() + + FullHttpRequest request = conn.serverChannel.readInbound() + assert request.uri() == '/foo' + assert request.method() == HttpMethod.POST + assert request.headers().get('host') == 'example.com' + assert request.headers().get("connection") == "keep-alive" + assert request.content().toString(StandardCharsets.UTF_8) == '[1,2,3,4,5]' + + def response = new DefaultFullHttpResponse(io.netty.handler.codec.http.HttpVersion.HTTP_1_1, HttpResponseStatus.OK, Unpooled.wrappedBuffer('foo'.bytes)) + response.headers().add("Content-Length", 3) + conn.serverChannel.writeOutbound(response) + conn.advance() + assert future.get().body() == 'foo' + + cleanup: + client.close() + ctx.close() + } + + def 'connection pool disabled http1'() { + def ctx = ApplicationContext.run([ + 'micronaut.http.client.pool.enabled': false, + ]) + def client = ctx.getBean(DefaultHttpClient) + + def conn1 = new EmbeddedTestConnectionHttp1() + conn1.setupHttp1() + def conn2 = new EmbeddedTestConnectionHttp1() + conn2.setupHttp1() + patch(client, conn1, conn2) + + def r1 = conn1.testExchangeRequest(client) + conn1.testExchangeResponse(r1, "close") + + def r2 = conn2.testExchangeRequest(client) + conn2.testExchangeResponse(r2, "close") + + cleanup: + client.close() + ctx.close() + } + + def 'connection pool disabled http2'() { + def ctx = ApplicationContext.run([ + 'micronaut.http.client.pool.enabled': false, + 'micronaut.http.client.ssl.insecure-trust-all-certificates': true, + ]) + def client = ctx.getBean(DefaultHttpClient) + + def conn1 = new EmbeddedTestConnectionHttp2() + conn1.setupHttp2Tls() + def conn2 = new EmbeddedTestConnectionHttp2() + conn2.setupHttp2Tls() + patch(client, conn1, conn2) + + def r1 = conn1.testExchangeRequest(client) + conn1.exchangeSettings() + conn1.testExchangeResponse(r1) + + def r2 = conn2.testExchangeRequest(client) + conn2.exchangeSettings() + conn2.testExchangeResponse(r2) + + cleanup: + client.close() + ctx.close() + } + + static class EmbeddedTestConnectionBase { + final EmbeddedChannel serverChannel + EmbeddedChannel clientChannel + ChannelInitializer clientInitializer = new ChannelInitializer() { + @Override + protected void initChannel(EmbeddedChannel ch) throws Exception { + ch.freezeTime() + ch.config().setAutoRead(false) + EmbeddedTestUtil.connect(serverChannel, ch) + } + } + + EmbeddedTestConnectionBase() { + serverChannel = new EmbeddedServerChannel(new DummyChannelId('server')) + serverChannel.freezeTime() + serverChannel.config().setAutoRead(true) + } + + final void advance() { + EmbeddedTestUtil.advance(serverChannel, clientChannel) + } + } + + static class EmbeddedServerChannel extends EmbeddedChannel implements ServerChannel { + EmbeddedServerChannel(ChannelId channelId) { + super(channelId) + } + } + + static class EmbeddedTestConnectionHttp1 extends EmbeddedTestConnectionBase { + private String scheme + + void setupHttp1() { + scheme = 'http' + serverChannel.pipeline() + .addLast(new HttpServerCodec()) + } + + void setupHttp1Tls() { + def certificate = new SelfSignedCertificate() + def builder = SslContextBuilder.forServer(certificate.key(), certificate.cert()) + CertificateProvidedSslBuilder.setupSslBuilder(builder, new SslConfiguration(), HttpVersion.HTTP_1_1); + def tlsHandler = builder.build().newHandler(ByteBufAllocator.DEFAULT) + + scheme = 'https' + serverChannel.pipeline() + .addLast(tlsHandler) + .addLast(new HttpServerCodec()) + } + + void respondOk() { + def response = new DefaultFullHttpResponse(io.netty.handler.codec.http.HttpVersion.HTTP_1_1, HttpResponseStatus.OK) + response.headers().add('content-length', 0) + serverChannel.writeOutbound(response) + } + + CompletableFuture> testExchangeRequest(HttpClient client) { + def future = Mono.from(client.exchange(scheme + '://example.com/foo')).toFuture() + future.exceptionally(t -> t.printStackTrace()) + advance() + return future + } + + void testExchangeResponse(CompletableFuture> future, String connectionHeader = "keep-alive") { + io.netty.handler.codec.http.HttpRequest request = serverChannel.readInbound() + assert request.uri() == '/foo' + assert request.method() == HttpMethod.GET + assert request.headers().get('host') == 'example.com' + assert request.headers().get("connection") == connectionHeader + + def tail = serverChannel.readInbound() + assert tail == null || tail instanceof LastHttpContent + + respondOk() + advance() + + assert future.get().status() == HttpStatus.OK + } + + private Queue testStreamingRequest(StreamingHttpClient client) { + def responseData = new ArrayDeque() + Flux.from(client.dataStream(HttpRequest.GET(scheme + '://example.com/foo'))) + .doOnError(t -> t.printStackTrace()) + .doOnComplete(() -> responseData.add("END")) + .subscribe(b -> responseData.add(b.toString(StandardCharsets.UTF_8))) + responseData + } + + private void testStreamingResponse(Queue responseData) { + advance() + + io.netty.handler.codec.http.HttpRequest request = serverChannel.readInbound() + assert request.uri() == '/foo' + assert request.method() == HttpMethod.GET + assert request.headers().get('host') == 'example.com' + assert request.headers().get("connection") == "keep-alive" + + def tail = serverChannel.readInbound() + assert tail == null || tail instanceof LastHttpContent + + def response = new DefaultHttpResponse(io.netty.handler.codec.http.HttpVersion.HTTP_1_1, HttpResponseStatus.OK) + response.headers().add('content-length', 6) + serverChannel.writeOutbound(response) + serverChannel.writeOutbound(new DefaultHttpContent(Unpooled.wrappedBuffer('foo'.bytes))) + advance() + + assert responseData.poll() == 'foo' + assert responseData.isEmpty() + + serverChannel.writeOutbound(new DefaultLastHttpContent(Unpooled.wrappedBuffer('bar'.bytes))) + advance() + + assert responseData.poll() == 'bar' + assert responseData.poll() == 'END' + } + } + + static class EmbeddedTestConnectionHttp2 extends EmbeddedTestConnectionBase { + private String scheme + Http2FrameStream h2cResponseStream + + void setupHttp2Tls() { + scheme = 'https' + + def certificate = new SelfSignedCertificate() + def builder = SslContextBuilder.forServer(certificate.key(), certificate.cert()) + CertificateProvidedSslBuilder.setupSslBuilder(builder, new SslConfiguration(), HttpVersion.HTTP_2_0); + def tlsHandler = builder.build().newHandler(ByteBufAllocator.DEFAULT) + + serverChannel.pipeline() + .addLast(tlsHandler) + .addLast(new ApplicationProtocolNegotiationHandler("h2") { + @Override + protected void configurePipeline(ChannelHandlerContext chtx, String protocol) throws Exception { + chtx.pipeline() + .addLast(Http2FrameCodecBuilder.forServer().build()) + } + }) + } + + void setupH2c() { + scheme = 'http' + + ChannelHandler responseStreamHandler = new ChannelInboundHandlerAdapter() { + @Override + void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception { + if (evt instanceof Http2FrameStreamEvent && evt.stream().id() == 1) { + h2cResponseStream = evt.stream() + } + + super.userEventTriggered(ctx, evt) + } + } + Http2FrameCodec frameCodec = Http2FrameCodecBuilder.forServer() + .build() + HttpServerUpgradeHandler.UpgradeCodecFactory upgradeCodecFactory = protocol -> { + if (AsciiString.contentEquals("h2c", protocol)) { + return new Http2ServerUpgradeCodec(frameCodec, responseStreamHandler) + } else { + return null + } + } + + HttpServerCodec sourceCodec = new HttpServerCodec() + serverChannel.pipeline() + .addLast(new LoggingHandler(LogLevel.INFO)) + .addLast(sourceCodec) + .addLast(new HttpServerUpgradeHandler(sourceCodec, upgradeCodecFactory, 1024)) + } + + void exchangeSettings() { + advance() + + assert serverChannel.readInbound() instanceof Http2SettingsFrame + assert serverChannel.readInbound() instanceof Http2SettingsAckFrame + } + + void exchangeH2c() { + advance() + + Http2HeadersFrame upgradeRequest = serverChannel.readInbound() + assert upgradeRequest.headers().get(Http2Headers.PseudoHeaderName.METHOD.value()) == 'GET' + assert upgradeRequest.headers().get(Http2Headers.PseudoHeaderName.PATH.value()) == '/' + assert upgradeRequest.headers().get(Http2Headers.PseudoHeaderName.AUTHORITY.value()) == 'example.com:80' + assert upgradeRequest.headers().get('content-length') == '0' + // client closes the stream immediately + assert upgradeRequest.stream().state() == Http2Stream.State.CLOSED + + assert serverChannel.readInbound() instanceof Http2SettingsFrame + assert serverChannel.readInbound() instanceof Http2ResetFrame + assert serverChannel.readInbound() instanceof Http2SettingsAckFrame + } + + void respondOk(Http2FrameStream stream) { + def responseHeaders = new DefaultHttp2Headers() + responseHeaders.add(Http2Headers.PseudoHeaderName.STATUS.value(), "200") + serverChannel.writeOutbound(new DefaultHttp2HeadersFrame(responseHeaders, true).stream(stream)) + } + + Future> testExchangeRequest(HttpClient client) { + def future = Mono.from(client.exchange(scheme + '://example.com/foo')).toFuture() + future.exceptionally(t -> t.printStackTrace()) + return future + } + + void testExchangeResponse(Future> future) { + Http2HeadersFrame request = serverChannel.readInbound() + assert request.headers().get(Http2Headers.PseudoHeaderName.PATH.value()) == '/foo' + assert request.headers().get(Http2Headers.PseudoHeaderName.SCHEME.value()) == scheme + assert request.headers().get(Http2Headers.PseudoHeaderName.AUTHORITY.value()) == 'example.com' + assert request.headers().get(Http2Headers.PseudoHeaderName.METHOD.value()) == 'GET' + + respondOk(request.stream()) + advance() + + def response = future.get() + assert response.status() == HttpStatus.OK + } + + Queue testStreamingRequest(StreamingHttpClient client) { + def responseData = new ArrayDeque() + Flux.from(client.dataStream(HttpRequest.GET(scheme + '://example.com/foo'))) + .doOnError(t -> t.printStackTrace()) + .doOnComplete(() -> responseData.add("END")) + .subscribe(b -> responseData.add(b.toString(StandardCharsets.UTF_8))) + return responseData + } + + void testStreamingResponse(Queue responseData) { + advance() + Http2HeadersFrame request = serverChannel.readInbound() + assert request.headers().get(Http2Headers.PseudoHeaderName.PATH.value()) == '/foo' + assert request.headers().get(Http2Headers.PseudoHeaderName.SCHEME.value()) == scheme + assert request.headers().get(Http2Headers.PseudoHeaderName.AUTHORITY.value()) == 'example.com' + assert request.headers().get(Http2Headers.PseudoHeaderName.METHOD.value()) == 'GET' + + def responseHeaders = new DefaultHttp2Headers() + responseHeaders.add(Http2Headers.PseudoHeaderName.STATUS.value(), "200") + serverChannel.writeOutbound(new DefaultHttp2HeadersFrame(responseHeaders, false).stream(request.stream())) + serverChannel.writeOutbound(new DefaultHttp2DataFrame(Unpooled.wrappedBuffer('foo'.bytes)).stream(request.stream())) + advance() + + assert responseData.poll() == 'foo' + assert responseData.isEmpty() + + serverChannel.writeOutbound(new DefaultHttp2DataFrame(Unpooled.wrappedBuffer('bar'.bytes), true).stream(request.stream())) + advance() + + assert responseData.poll() == 'bar' + assert responseData.poll() == 'END' + } + } + + @Singleton + static class CustomizerTracker implements NettyClientCustomizer, BeanCreatedEventListener { + final Queue initialPipelineBuilt = new ArrayDeque<>() + final Queue streamPipelineBuilt = new ArrayDeque<>() + final Queue requestPipelineBuilt = new ArrayDeque<>() + + @Override + NettyClientCustomizer specializeForChannel(Channel channel, ChannelRole role) { + return new NettyClientCustomizer() { + @Override + NettyClientCustomizer specializeForChannel(Channel channel_, ChannelRole role_) { + return CustomizerTracker.this.specializeForChannel(channel_, role_) + } + + Snapshot snap() { + return new Snapshot(channel, role, channel.pipeline().names()) + } + + @Override + void onInitialPipelineBuilt() { + initialPipelineBuilt.add(snap()) + } + + @Override + void onStreamPipelineBuilt() { + streamPipelineBuilt.add(snap()) + } + + @Override + void onRequestPipelineBuilt() { + requestPipelineBuilt.add(snap()) + } + } + } + + @Override + Registry onCreated(BeanCreatedEvent event) { + event.getBean().register(this) + return event.getBean() + } + + static class Snapshot { + final Channel channel + final ChannelRole role + final List handlerNames + + Snapshot(Channel channel, ChannelRole role, List handlerNames) { + this.channel = channel + this.role = role + this.handlerNames = handlerNames + } + } + } +} diff --git a/http-client/src/test/groovy/io/micronaut/http/client/netty/DefaultNettyHttpClientRegistrySpec.groovy b/http-client/src/test/groovy/io/micronaut/http/client/netty/DefaultNettyHttpClientRegistrySpec.groovy index 5abda077f12..2aa503905ad 100644 --- a/http-client/src/test/groovy/io/micronaut/http/client/netty/DefaultNettyHttpClientRegistrySpec.groovy +++ b/http-client/src/test/groovy/io/micronaut/http/client/netty/DefaultNettyHttpClientRegistrySpec.groovy @@ -1,17 +1,13 @@ package io.micronaut.http.client.netty import io.micronaut.context.ApplicationContext -import io.micronaut.context.BeanContext import io.micronaut.context.annotation.Requires import io.micronaut.context.event.BeanCreatedEvent import io.micronaut.context.event.BeanCreatedEventListener import io.micronaut.http.annotation.Get import io.micronaut.http.client.HttpClient import io.micronaut.http.client.annotation.Client -import io.micronaut.http.netty.channel.ChannelPipelineCustomizer -import io.micronaut.runtime.server.EmbeddedServer -import io.micronaut.test.extensions.spock.annotation.MicronautTest -import io.netty.util.Attribute +import io.netty.channel.Channel import io.netty.util.AttributeKey import jakarta.inject.Inject import jakarta.inject.Singleton @@ -88,22 +84,27 @@ class DefaultNettyHttpClientRegistrySpec extends Specification { @Requires(property = 'spec.name', value = 'DefaultNettyHttpClientRegistrySpec') @Singleton - static class MyCustomizer implements BeanCreatedEventListener { + static class MyCustomizer implements BeanCreatedEventListener { static final AttributeKey CUSTOMIZED = AttributeKey.valueOf('micronaut.test.customized') def connected = 0 def duplicate = false @Override - ChannelPipelineCustomizer onCreated(BeanCreatedEvent event) { - event.bean.doOnConnect { - if (it.channel().hasAttr(CUSTOMIZED)) { - duplicate = true + NettyClientCustomizer.Registry onCreated(BeanCreatedEvent event) { + event.bean.register(new NettyClientCustomizer() { + @Override + NettyClientCustomizer specializeForChannel(Channel channel, NettyClientCustomizer.ChannelRole role) { + if (role == NettyClientCustomizer.ChannelRole.CONNECTION) { + if (channel.hasAttr(CUSTOMIZED)) { + duplicate = true + } + channel.attr(CUSTOMIZED).set(true) + connected++ + } + return this } - it.channel().attr(CUSTOMIZED).set(true) - connected++ - return it - } + }) return event.bean } } diff --git a/http-client/src/test/groovy/io/micronaut/http/client/netty/DummyChannelId.groovy b/http-client/src/test/groovy/io/micronaut/http/client/netty/DummyChannelId.groovy new file mode 100644 index 00000000000..c5d6af6d3ff --- /dev/null +++ b/http-client/src/test/groovy/io/micronaut/http/client/netty/DummyChannelId.groovy @@ -0,0 +1,26 @@ +package io.micronaut.http.client.netty + +import io.netty.channel.ChannelId + +class DummyChannelId implements ChannelId { + final String name + + DummyChannelId(String name) { + this.name = name + } + + @Override + String asShortText() { + return name + } + + @Override + String asLongText() { + return name + } + + @Override + int compareTo(ChannelId o) { + return asLongText() <=> o.asLongText() + } +} diff --git a/http-client/src/test/groovy/io/micronaut/http/client/netty/EmbeddedTestUtil.groovy b/http-client/src/test/groovy/io/micronaut/http/client/netty/EmbeddedTestUtil.groovy new file mode 100644 index 00000000000..38441839f08 --- /dev/null +++ b/http-client/src/test/groovy/io/micronaut/http/client/netty/EmbeddedTestUtil.groovy @@ -0,0 +1,108 @@ +package io.micronaut.http.client.netty + +import io.netty.buffer.ByteBuf +import io.netty.buffer.CompositeByteBuf +import io.netty.channel.ChannelHandlerContext +import io.netty.channel.ChannelOutboundHandlerAdapter +import io.netty.channel.ChannelPromise +import io.netty.channel.embedded.EmbeddedChannel + +// todo: can we unify this with the util class in http-server-netty tests? +class EmbeddedTestUtil { + static void advance(EmbeddedChannel... channels) { + boolean advanced + do { + advanced = false + for (EmbeddedChannel channel : channels) { + if (channel.hasPendingTasks()) { + advanced = true + channel.runPendingTasks() + } + channel.checkException() + } + } while (advanced); + } + + static void connect(EmbeddedChannel server, EmbeddedChannel client) { + new ConnectionDirection(server, client).register() + new ConnectionDirection(client, server).register() + } + + private static class ConnectionDirection { + final EmbeddedChannel source + final EmbeddedChannel dest + CompositeByteBuf sourceQueue + final List sourceQueueFutures = new ArrayList<>(); + final Queue destQueue = new ArrayDeque<>() + boolean readPending + + ConnectionDirection(EmbeddedChannel source, EmbeddedChannel dest) { + this.source = source + this.dest = dest + } + + private void forwardNow(ByteBuf msg) { + if (!dest.isOpen()) { + return + } + dest.writeOneInbound(msg) + dest.pipeline().fireChannelReadComplete() + } + + void register() { + source.pipeline().addFirst(new ChannelOutboundHandlerAdapter() { + @Override + void write(ChannelHandlerContext ctx_, Object msg, ChannelPromise promise) throws Exception { + if (!(msg instanceof ByteBuf)) { + throw new IllegalArgumentException("Can only forward bytes, got " + msg) + } + if (!msg.isReadable()) { + // no data + msg.release() + promise.setSuccess() + return + } + + if (sourceQueue == null) { + sourceQueue = ((ByteBuf) msg).alloc().compositeBuffer() + } + sourceQueue.addComponent(true, (ByteBuf) msg) + if (!promise.isVoid()) { + sourceQueueFutures.add(promise) + } + } + + @Override + void flush(ChannelHandlerContext ctx_) throws Exception { + if (sourceQueue != null) { + ByteBuf packet = sourceQueue + sourceQueue = null + + for (ChannelPromise promise : sourceQueueFutures) { + promise.trySuccess() + } + sourceQueueFutures.clear() + + if (readPending || dest.config().isAutoRead()) { + dest.eventLoop().execute(() -> forwardNow(packet)) + readPending = false + } else { + destQueue.add(packet) + } + } + } + }) + dest.pipeline().addFirst(new ChannelOutboundHandlerAdapter() { + @Override + void read(ChannelHandlerContext ctx) throws Exception { + if (destQueue.isEmpty()) { + readPending = true + } else { + ByteBuf msg = destQueue.poll() + ctx.channel().eventLoop().execute(() -> forwardNow(msg)) + } + } + }) + } + } +} diff --git a/http-netty/src/main/java/io/micronaut/http/netty/channel/ChannelPipelineCustomizer.java b/http-netty/src/main/java/io/micronaut/http/netty/channel/ChannelPipelineCustomizer.java index 9883e5779dc..746774dc1c3 100644 --- a/http-netty/src/main/java/io/micronaut/http/netty/channel/ChannelPipelineCustomizer.java +++ b/http-netty/src/main/java/io/micronaut/http/netty/channel/ChannelPipelineCustomizer.java @@ -56,6 +56,12 @@ public interface ChannelPipelineCustomizer { String HANDLER_WEBSOCKET_UPGRADE = "websocket-upgrade-handler"; String HANDLER_MICRONAUT_INBOUND = "micronaut-inbound-handler"; String HANDLER_ACCESS_LOGGER = "http-access-logger"; + String HANDLER_INITIAL_ERROR = "initial-error"; + /** + * Handler that listens for channelActive to trigger, which will finish up the connection + * setup. + */ + String HANDLER_ACTIVITY_LISTENER = "activity-listener"; /** * @return Is this customizer the client. diff --git a/http-netty/src/main/java/io/micronaut/http/netty/stream/HttpStreamsClientHandler.java b/http-netty/src/main/java/io/micronaut/http/netty/stream/HttpStreamsClientHandler.java index f7702f69059..3ff8be28cda 100644 --- a/http-netty/src/main/java/io/micronaut/http/netty/stream/HttpStreamsClientHandler.java +++ b/http-netty/src/main/java/io/micronaut/http/netty/stream/HttpStreamsClientHandler.java @@ -88,7 +88,6 @@ protected boolean hasBody(HttpResponse response) { return true; } - if (HttpUtil.isContentLengthSet(response)) { return HttpUtil.getContentLength(response) > 0; } @@ -183,7 +182,7 @@ public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception @Override public void write(final ChannelHandlerContext ctx, Object msg, final ChannelPromise promise) throws Exception { - if (ctx.channel().attr(AttributeKey.valueOf(ChannelPipelineCustomizer.HANDLER_HTTP_CHUNK)).get() == Boolean.TRUE) { + if (Boolean.TRUE.equals(ctx.channel().attr(AttributeKey.valueOf(ChannelPipelineCustomizer.HANDLER_HTTP_CHUNK)).get())) { ctx.write(msg, promise); } else { super.write(ctx, msg, promise); diff --git a/http-netty/src/main/java/io/micronaut/http/netty/stream/HttpStreamsServerHandler.java b/http-netty/src/main/java/io/micronaut/http/netty/stream/HttpStreamsServerHandler.java index f819aad2bc3..a2d4cf67584 100644 --- a/http-netty/src/main/java/io/micronaut/http/netty/stream/HttpStreamsServerHandler.java +++ b/http-netty/src/main/java/io/micronaut/http/netty/stream/HttpStreamsServerHandler.java @@ -23,7 +23,16 @@ import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelPipeline; import io.netty.channel.ChannelPromise; -import io.netty.handler.codec.http.*; +import io.netty.handler.codec.http.DefaultFullHttpResponse; +import io.netty.handler.codec.http.FullHttpRequest; +import io.netty.handler.codec.http.FullHttpResponse; +import io.netty.handler.codec.http.HttpContent; +import io.netty.handler.codec.http.HttpHeaderNames; +import io.netty.handler.codec.http.HttpRequest; +import io.netty.handler.codec.http.HttpResponse; +import io.netty.handler.codec.http.HttpResponseStatus; +import io.netty.handler.codec.http.HttpUtil; +import io.netty.handler.codec.http.HttpVersion; import io.netty.handler.codec.http.websocketx.WebSocketFrame; import io.netty.handler.codec.http.websocketx.WebSocketServerHandshaker; import io.netty.handler.codec.http.websocketx.WebSocketVersion; @@ -211,6 +220,15 @@ protected void consumedInMessage(ChannelHandlerContext ctx) { webSocketResponse = null; webSocketResponseChannelPromise = null; } + if (inFlight == 0) { + // normally, after writing the response, the routing handler triggers a read() for the + // next request. However, if at this point the request is not fully read yet (e.g. + // still missing a LastHttpContent), then that read() call will simply read the + // remaining content, and the HandlerPublisher also won't trigger more read()s since + // it's complete. To prevent the connection from being stuck in that case, we trigger a + // read here. + ctx.read(); + } } private void handleWebSocketResponse(ChannelHandlerContext ctx, HttpResponse message, ChannelPromise promise) { diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/RoutingInBoundHandler.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/RoutingInBoundHandler.java index 01ad951e2c9..41c9c9dca87 100644 --- a/http-server-netty/src/main/java/io/micronaut/http/server/netty/RoutingInBoundHandler.java +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/RoutingInBoundHandler.java @@ -1251,7 +1251,7 @@ private void writeFinalNettyResponse(MutableHttpResponse message, HttpRequest if (!isHttp2) { if (!nettyHeaders.contains(HttpHeaderNames.CONNECTION)) { boolean expectKeepAlive = nettyResponse.protocolVersion().isKeepAliveDefault() || request.getHeaders().isKeepAlive(); - if (!decodeError && (expectKeepAlive || httpStatus < 500 || serverConfiguration.isKeepAliveOnServerError())) { + if (!decodeError && expectKeepAlive && (httpStatus < 500 || serverConfiguration.isKeepAliveOnServerError())) { nettyHeaders.set(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE); } else { nettyHeaders.set(HttpHeaderNames.CONNECTION, HttpHeaderValues.CLOSE); diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/configuration/NettyHttpServerConfiguration.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/configuration/NettyHttpServerConfiguration.java index 4c05c0dcae3..d615bb1dc8a 100644 --- a/http-server-netty/src/main/java/io/micronaut/http/server/netty/configuration/NettyHttpServerConfiguration.java +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/configuration/NettyHttpServerConfiguration.java @@ -111,7 +111,7 @@ public class NettyHttpServerConfiguration extends HttpServerConfiguration { * The default configuration for boolean flag indicating whether to add connection header `keep-alive` to responses with HttpStatus > 499. */ @SuppressWarnings("WeakerAccess") - public static final boolean DEFAULT_KEEP_ALIVE_ON_SERVER_ERROR = false; + public static final boolean DEFAULT_KEEP_ALIVE_ON_SERVER_ERROR = true; private static final Logger LOG = LoggerFactory.getLogger(NettyHttpServerConfiguration.class); diff --git a/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/binding/HttpResponseSpec.groovy b/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/binding/HttpResponseSpec.groovy index 2c2ea5d784e..2b61ac1134e 100644 --- a/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/binding/HttpResponseSpec.groovy +++ b/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/binding/HttpResponseSpec.groovy @@ -24,18 +24,13 @@ import io.micronaut.http.HttpStatus import io.micronaut.http.annotation.Controller import io.micronaut.http.annotation.Get import io.micronaut.http.client.HttpClient -import io.micronaut.http.client.DefaultHttpClientConfiguration import io.micronaut.http.client.exceptions.HttpClientResponseException import io.micronaut.http.server.netty.AbstractMicronautSpec -import io.micronaut.runtime.Micronaut import io.micronaut.runtime.server.EmbeddedServer import reactor.core.publisher.Flux import spock.lang.Shared import spock.lang.Unroll -import java.time.Duration -import java.time.temporal.ChronoUnit - /** * @author Graeme Rocher * @since 1.0 @@ -71,19 +66,19 @@ class HttpResponseSpec extends AbstractMicronautSpec { where: action | status | body | headers - "ok" | HttpStatus.OK | null | [connection: 'close'] - "ok-with-body" | HttpStatus.OK | "some text" | ['content-length': '9', 'content-type': 'text/plain'] + [connection: 'close'] - "error-with-body" | HttpStatus.INTERNAL_SERVER_ERROR | "some text" | ['content-length': '9', 'content-type': 'text/plain'] + [connection: 'close'] - "ok-with-body-object" | HttpStatus.OK | '{"name":"blah","age":10}' | defaultHeaders + ['content-length': '24', 'content-type': 'application/json'] + [connection: 'close'] - "status" | HttpStatus.MOVED_PERMANENTLY | null | [connection: 'close'] - "created-body" | HttpStatus.CREATED | '{"name":"blah","age":10}' | defaultHeaders + ['content-length': '24', 'content-type': 'application/json'] + [connection: 'close'] - "created-uri" | HttpStatus.CREATED | null | [connection: 'close', 'location': 'http://test.com'] - "created-body-uri" | HttpStatus.CREATED | '{"name":"blah","age":10}' | defaultHeaders + ['content-length': '24', 'content-type': 'application/json'] + [connection: 'close', 'location': 'http://test.com'] - "accepted" | HttpStatus.ACCEPTED | null | [connection: 'close'] - "accepted-uri" | HttpStatus.ACCEPTED | null | [connection: 'close', 'location': 'http://example.com'] - "disallow" | HttpStatus.METHOD_NOT_ALLOWED | null | [connection: "close", 'allow': 'DELETE'] - "optional-response/false" | HttpStatus.OK | null | [connection: 'close'] - "optional-response/true" | HttpStatus.NOT_FOUND | null | ['content-type': 'application/json', 'content-length': '162', connection: 'close'] + "ok" | HttpStatus.OK | null | [connection: 'keep-alive'] + "ok-with-body" | HttpStatus.OK | "some text" | ['content-length': '9', 'content-type': 'text/plain'] + [connection: 'keep-alive'] + "error-with-body" | HttpStatus.INTERNAL_SERVER_ERROR | "some text" | ['content-length': '9', 'content-type': 'text/plain'] + [connection: 'keep-alive'] + "ok-with-body-object" | HttpStatus.OK | '{"name":"blah","age":10}' | defaultHeaders + ['content-length': '24', 'content-type': 'application/json'] + [connection: 'keep-alive'] + "status" | HttpStatus.MOVED_PERMANENTLY | null | [connection: 'keep-alive'] + "created-body" | HttpStatus.CREATED | '{"name":"blah","age":10}' | defaultHeaders + ['content-length': '24', 'content-type': 'application/json'] + [connection: 'keep-alive'] + "created-uri" | HttpStatus.CREATED | null | [connection: 'keep-alive', 'location': 'http://test.com'] + "created-body-uri" | HttpStatus.CREATED | '{"name":"blah","age":10}' | defaultHeaders + ['content-length': '24', 'content-type': 'application/json'] + [connection: 'keep-alive', 'location': 'http://test.com'] + "accepted" | HttpStatus.ACCEPTED | null | [connection: 'keep-alive'] + "accepted-uri" | HttpStatus.ACCEPTED | null | [connection: 'keep-alive', 'location': 'http://example.com'] + "disallow" | HttpStatus.METHOD_NOT_ALLOWED | null | [connection: "keep-alive", 'allow': 'DELETE'] + "optional-response/false" | HttpStatus.OK | null | [connection: 'keep-alive'] + "optional-response/true" | HttpStatus.NOT_FOUND | null | ['content-type': 'application/json', 'content-length': '162', connection: 'keep-alive'] } @@ -104,7 +99,7 @@ class HttpResponseSpec extends AbstractMicronautSpec { } def responseBody = response.body.orElse(null) - def defaultHeaders = [connection: 'close'] + def defaultHeaders = [connection: 'keep-alive'] then: response.code() == status.code @@ -113,15 +108,15 @@ class HttpResponseSpec extends AbstractMicronautSpec { where: action | status | body | headers - "ok" | HttpStatus.OK | null | [connection: 'close'] - "ok-with-body" | HttpStatus.OK | "some text" | ['content-length': '9', 'content-type': 'text/plain'] + [connection: 'close'] - "error-with-body" | HttpStatus.INTERNAL_SERVER_ERROR | "some text" | ['content-length': '9', 'content-type': 'text/plain'] + [connection: 'close'] - "ok-with-body-object" | HttpStatus.OK | '{"name":"blah","age":10}' | defaultHeaders + ['content-length': '24', 'content-type': 'application/json'] + [connection: 'close'] - "status" | HttpStatus.MOVED_PERMANENTLY | null | [connection: 'close'] - "created-body" | HttpStatus.CREATED | '{"name":"blah","age":10}' | defaultHeaders + ['content-length': '24', 'content-type': 'application/json'] + [connection: 'close'] - "created-uri" | HttpStatus.CREATED | null | [connection: 'close', 'location': 'http://test.com'] - "accepted" | HttpStatus.ACCEPTED | null | [connection: 'close'] - "accepted-uri" | HttpStatus.ACCEPTED | null | [connection: 'close', 'location': 'http://example.com'] + "ok" | HttpStatus.OK | null | [connection: 'keep-alive'] + "ok-with-body" | HttpStatus.OK | "some text" | ['content-length': '9', 'content-type': 'text/plain'] + [connection: 'keep-alive'] + "error-with-body" | HttpStatus.INTERNAL_SERVER_ERROR | "some text" | ['content-length': '9', 'content-type': 'text/plain'] + [connection: 'keep-alive'] + "ok-with-body-object" | HttpStatus.OK | '{"name":"blah","age":10}' | defaultHeaders + ['content-length': '24', 'content-type': 'application/json'] + [connection: 'keep-alive'] + "status" | HttpStatus.MOVED_PERMANENTLY | null | [connection: 'keep-alive'] + "created-body" | HttpStatus.CREATED | '{"name":"blah","age":10}' | defaultHeaders + ['content-length': '24', 'content-type': 'application/json'] + [connection: 'keep-alive'] + "created-uri" | HttpStatus.CREATED | null | [connection: 'keep-alive', 'location': 'http://test.com'] + "accepted" | HttpStatus.ACCEPTED | null | [connection: 'keep-alive'] + "accepted-uri" | HttpStatus.ACCEPTED | null | [connection: 'keep-alive', 'location': 'http://example.com'] } void "test content encoding"() { @@ -232,9 +227,13 @@ class HttpResponseSpec extends AbstractMicronautSpec { server.close() } - void "test keep alive connection header is not set by default for > 499 response"() { + void "test keep alive connection header is not set for 500 response"() { when: - EmbeddedServer server = ApplicationContext.run(EmbeddedServer, ['micronaut.server.date-header': false, (SPEC_NAME_PROPERTY):getClass().simpleName]) + EmbeddedServer server = ApplicationContext.run(EmbeddedServer, [ + 'micronaut.server.netty.keepAliveOnServerError': false, + 'micronaut.server.date-header': false, + (SPEC_NAME_PROPERTY):getClass().simpleName, + ]) ApplicationContext ctx = server.getApplicationContext() HttpClient client = ctx.createBean(HttpClient, server.getURL()) @@ -253,19 +252,13 @@ class HttpResponseSpec extends AbstractMicronautSpec { server.close() } - void "test connection header is defaulted to keep-alive when configured to true for > 499 response"() { + void "test connection header is defaulted to keep-alive by default for > 499 response"() { when: - DefaultHttpClientConfiguration config = new DefaultHttpClientConfiguration() - - // The client will explicitly request "Connection: close" unless using a connection pool, so set it up - config.connectionPoolConfiguration.enabled = true - EmbeddedServer server = ApplicationContext.run(EmbeddedServer, [ - (SPEC_NAME_PROPERTY):getClass().simpleName, - 'micronaut.server.netty.keepAliveOnServerError':true + (SPEC_NAME_PROPERTY):getClass().simpleName ]) def ctx = server.getApplicationContext() - HttpClient client = ctx.createBean(HttpClient, embeddedServer.getURL(), config) + HttpClient client = ctx.createBean(HttpClient, server.getURL()) Flux.from(client.exchange( HttpRequest.GET('/test-header/fail') diff --git a/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/binding/NettyHttpServerSpec.groovy b/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/binding/NettyHttpServerSpec.groovy index fc59c5aaad7..6ac5f9fd84d 100644 --- a/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/binding/NettyHttpServerSpec.groovy +++ b/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/binding/NettyHttpServerSpec.groovy @@ -20,7 +20,11 @@ import io.micronaut.context.env.Environment import io.micronaut.context.env.PropertySource import io.micronaut.context.event.StartupEvent import io.micronaut.core.io.socket.SocketUtils -import io.micronaut.http.* +import io.micronaut.http.HttpHeaders +import io.micronaut.http.HttpMethod +import io.micronaut.http.HttpRequest +import io.micronaut.http.HttpResponse +import io.micronaut.http.HttpStatus import io.micronaut.http.annotation.Controller import io.micronaut.http.annotation.Get import io.micronaut.http.annotation.Put @@ -32,7 +36,6 @@ import io.micronaut.runtime.Micronaut import io.micronaut.runtime.event.annotation.EventListener import io.micronaut.runtime.server.EmbeddedServer import jakarta.inject.Singleton -import reactor.core.publisher.Flux import spock.lang.Retry import spock.lang.Specification import spock.lang.Stepwise @@ -179,7 +182,6 @@ class NettyHttpServerSpec extends Specification { DefaultHttpClientConfiguration config = new DefaultHttpClientConfiguration() // The client will explicitly request "Connection: close" unless using a connection pool, so set it up config.connectionPoolConfiguration.enabled = true - config.connectionPoolConfiguration.maxConnections = 2; config.connectionPoolConfiguration.acquireTimeout = Duration.of(3, ChronoUnit.SECONDS); ApplicationContext applicationContext = Micronaut.run() diff --git a/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/configuration/NettyHttpServerConfigurationSpec.groovy b/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/configuration/NettyHttpServerConfigurationSpec.groovy index 06a32e21179..8eec5b1c913 100644 --- a/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/configuration/NettyHttpServerConfigurationSpec.groovy +++ b/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/configuration/NettyHttpServerConfigurationSpec.groovy @@ -472,17 +472,17 @@ class NettyHttpServerConfigurationSpec extends Specification { NettyHttpServerConfiguration config = beanContext.getBean(NettyHttpServerConfiguration) then: - !config.keepAliveOnServerError + config.keepAliveOnServerError cleanup: beanContext.close() } - void "test keepAlive configuration set to true"() { + void "test keepAlive configuration set to false"() { given: ApplicationContext beanContext = new DefaultApplicationContext("test") beanContext.environment.addPropertySource(PropertySource.of("test", - ['micronaut.server.netty.keepAliveOnServerError': true] + ['micronaut.server.netty.keepAliveOnServerError': false] )) beanContext.start() @@ -490,7 +490,7 @@ class NettyHttpServerConfigurationSpec extends Specification { NettyHttpServerConfiguration config = beanContext.getBean(NettyHttpServerConfiguration) then: - config.keepAliveOnServerError + !config.keepAliveOnServerError cleanup: beanContext.close() diff --git a/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/websocket/BinaryWebSocketSpec.groovy b/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/websocket/BinaryWebSocketSpec.groovy index a4bd3a39ca2..4fc32dac55c 100644 --- a/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/websocket/BinaryWebSocketSpec.groovy +++ b/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/websocket/BinaryWebSocketSpec.groovy @@ -19,10 +19,12 @@ import io.micronaut.context.ApplicationContext import io.micronaut.context.annotation.Requires import io.micronaut.context.event.BeanCreatedEvent import io.micronaut.context.event.BeanCreatedEventListener -import io.micronaut.http.netty.channel.ChannelPipelineCustomizer +import io.micronaut.http.client.netty.NettyClientCustomizer +import io.micronaut.http.server.netty.NettyServerCustomizer import io.micronaut.runtime.server.EmbeddedServer import io.micronaut.websocket.WebSocketClient import io.netty.buffer.Unpooled +import io.netty.channel.Channel import io.netty.channel.ChannelHandlerContext import io.netty.channel.ChannelOutboundHandlerAdapter import io.netty.channel.ChannelPipeline @@ -218,7 +220,8 @@ class BinaryWebSocketSpec extends Specification { 'spec.name' : 'test per-message compression', 'micronaut.server.port': -1 ]) - def compressionDetectionCustomizer = ctx.getBean(CompressionDetectionCustomizer) + def cdcServer = ctx.getBean(CompressionDetectionCustomizerServer) + def cdcClient = ctx.getBean(CompressionDetectionCustomizerClient) EmbeddedServer embeddedServer = ctx.getBean(EmbeddedServer) embeddedServer.start() PollingConditions conditions = new PollingConditions(timeout: 15, delay: 0.5) @@ -237,11 +240,12 @@ class BinaryWebSocketSpec extends Specification { fred.replies.size() == 1 } - compressionDetectionCustomizer.getPipelines().size() == 4 + cdcServer.getPipelines().size() == 2 + cdcClient.getPipelines().size() == 2 when: "A message is sent" List interceptors = new ArrayList<>() - for (ChannelPipeline pipeline : compressionDetectionCustomizer.getPipelines()) { + for (ChannelPipeline pipeline : cdcServer.getPipelines() + cdcClient.getPipelines()) { def interceptor = new MessageInterceptor() if (pipeline.get('ws-encoder') != null) { pipeline.addAfter('ws-encoder', 'MessageInterceptor', interceptor) @@ -268,17 +272,62 @@ class BinaryWebSocketSpec extends Specification { @Singleton @Requires(property = 'spec.name', value = 'test per-message compression') - static class CompressionDetectionCustomizer implements BeanCreatedEventListener { + static class CompressionDetectionCustomizerServer implements BeanCreatedEventListener { List pipelines = Collections.synchronizedList(new ArrayList<>()) @Override - ChannelPipelineCustomizer onCreated(BeanCreatedEvent event) { - event.getBean().doOnConnect { - pipelines.add(it) - return it + NettyServerCustomizer.Registry onCreated(BeanCreatedEvent event) { + event.getBean().register(new Customizer(null)) + return event.bean + } + + class Customizer implements NettyServerCustomizer { + final Channel channel + + Customizer(Channel channel) { + this.channel = channel + } + + @Override + NettyServerCustomizer specializeForChannel(Channel channel, ChannelRole role) { + return new Customizer(channel) } + + @Override + void onInitialPipelineBuilt() { + pipelines.add(channel.pipeline()) + } + } + } + + @Singleton + @Requires(property = 'spec.name', value = 'test per-message compression') + static class CompressionDetectionCustomizerClient implements BeanCreatedEventListener { + List pipelines = Collections.synchronizedList(new ArrayList<>()) + + @Override + NettyClientCustomizer.Registry onCreated(BeanCreatedEvent event) { + event.getBean().register(new Customizer(null)) return event.bean } + + class Customizer implements NettyClientCustomizer { + final Channel channel + + Customizer(Channel channel) { + this.channel = channel + } + + @Override + NettyClientCustomizer specializeForChannel(Channel channel, ChannelRole role) { + return new Customizer(channel) + } + + @Override + void onInitialPipelineBuilt() { + pipelines.add(channel.pipeline()) + } + } } static class MessageInterceptor extends ChannelOutboundHandlerAdapter { diff --git a/src/main/docs/guide/httpClient/lowLevelHttpClient/clientConfiguration.adoc b/src/main/docs/guide/httpClient/lowLevelHttpClient/clientConfiguration.adoc index 0b65eb51d34..aedaeffbe25 100644 --- a/src/main/docs/guide/httpClient/lowLevelHttpClient/clientConfiguration.adoc +++ b/src/main/docs/guide/httpClient/lowLevelHttpClient/clientConfiguration.adoc @@ -75,9 +75,17 @@ Alternatively, if you don't use service discovery you can use the `configuration ReactorHttpClient httpClient; ---- -=== Using HTTP Client Connection Pooling +=== Connection Pooling and HTTP/2 -A client that handles a significant number of requests will benefit from enabling HTTP client connection pooling. The following configuration enables pooling for the `foo` client: +Connections using normal HTTP (without TLS/SSL) use HTTP/1.1. This can be configured using the `plaintext-mode` configuration option. + +Secure connections (i.e. HTTP**S**, with TLS/SSL) use a feature called "Application Layer Protocol Negotiation" (ALPN) that is part of TLS to select the HTTP version. If the server supports HTTP/2, the Micronaut HTTP Client will use that capability by default, but if it doesn't, HTTP/1.1 is still supported. This is configured using the `alpn-modes` option, which is a list of supported ALPN protocol IDs (`"h2"` and `"http/1.1"`). + +NOTE: The HTTP/2 standard forbids the use of certain less secure TLS cipher suites for HTTP/2 connections. When the HTTP client supports HTTP/2 (which is the default), it will not support those cipher suites. Removing `"h2"` from `alpn-modes` will enable support for all cipher suites. + +Each HTTP/1.1 connection can only support one request at a time, but can be reused for subsequent requests using the `keep-alive` mechanism. HTTP/2 connections can support any number of concurrent requests. + +To remove the overhead of opening a new connection for each request, the Micronaut HTTP Client will reuse HTTP connections wherever possible. They are managed in a _connection pool_. HTTP/1.1 connections are kept around using keep-alive and are used for new requests, and for HTTP/2, a single connection is used for all requests. .Manually configuring HTTP services [source,yaml] @@ -90,15 +98,15 @@ micronaut: - http://foo1 - http://foo2 pool: - enabled: true # <1> - max-connections: 50 # <2> + max-concurrent-http1-connections: 50 # <1> ---- -<1> Enables the pool -<2> Sets the maximum number of connections in the pool +<1> Limit maximum concurrent HTTP/1.1 connections See the API for link:{api}/io/micronaut/http/client/HttpClientConfiguration.ConnectionPoolConfiguration.html[ConnectionPoolConfiguration] for details on available pool configuration options. +By setting the `pool.enabled` property to `false`, you can disable connection reuse. The pool is still used and other configuration options (e.g. concurrent HTTP 1 connections) still apply, but one connection will only serve one request. + === Configuring Event Loop Groups By default, Micronaut shares a common Netty `EventLoopGroup` for worker threads and all HTTP client threads. diff --git a/test-suite-groovy/src/test/groovy/io/micronaut/docs/netty/LogbookNettyClientCustomizer.groovy b/test-suite-groovy/src/test/groovy/io/micronaut/docs/netty/LogbookNettyClientCustomizer.groovy index de84e9777b7..a4d6b4b1aa5 100644 --- a/test-suite-groovy/src/test/groovy/io/micronaut/docs/netty/LogbookNettyClientCustomizer.groovy +++ b/test-suite-groovy/src/test/groovy/io/micronaut/docs/netty/LogbookNettyClientCustomizer.groovy @@ -1,12 +1,13 @@ package io.micronaut.docs.netty -import io.micronaut.context.annotation.Requires; +import io.micronaut.context.annotation.Requires +import io.micronaut.context.event.BeanCreatedEvent; // tag::imports[] -import io.micronaut.context.event.BeanCreatedEvent import io.micronaut.context.event.BeanCreatedEventListener import io.micronaut.http.client.netty.NettyClientCustomizer +import io.micronaut.http.netty.channel.ChannelPipelineCustomizer import io.netty.channel.Channel import jakarta.inject.Singleton import org.zalando.logbook.Logbook @@ -47,8 +48,9 @@ class LogbookNettyClientCustomizer } @Override - void onStreamPipelineBuilt() { - channel.pipeline().addLast( // <5> + void onRequestPipelineBuilt() { + channel.pipeline().addBefore( // <5> + ChannelPipelineCustomizer.HANDLER_HTTP_STREAM, "logbook", new LogbookClientHandler(logbook) ) diff --git a/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/netty/LogbookNettyClientCustomizer.kt b/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/netty/LogbookNettyClientCustomizer.kt index 5b980a2fcb8..7fdeb32a624 100644 --- a/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/netty/LogbookNettyClientCustomizer.kt +++ b/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/netty/LogbookNettyClientCustomizer.kt @@ -6,6 +6,7 @@ import io.micronaut.context.event.BeanCreatedEvent import io.micronaut.context.event.BeanCreatedEventListener import io.micronaut.http.client.netty.NettyClientCustomizer import io.micronaut.http.client.netty.NettyClientCustomizer.ChannelRole +import io.micronaut.http.netty.channel.ChannelPipelineCustomizer import io.netty.channel.Channel import jakarta.inject.Singleton import org.zalando.logbook.Logbook @@ -29,8 +30,9 @@ class LogbookNettyClientCustomizer(private val logbook: Logbook) : override fun specializeForChannel(channel: Channel, role: ChannelRole) = Customizer(channel) // <4> - override fun onStreamPipelineBuilt() { - channel!!.pipeline().addLast( // <5> + override fun onRequestPipelineBuilt() { + channel!!.pipeline().addBefore( // <5> + ChannelPipelineCustomizer.HANDLER_HTTP_STREAM, "logbook", LogbookClientHandler(logbook) ) diff --git a/test-suite/src/test/groovy/io/micronaut/http/client/http2/Http2RequestSpec.groovy b/test-suite/src/test/groovy/io/micronaut/http/client/http2/Http2RequestSpec.groovy index 88407a6ecd4..561bb44d1e8 100644 --- a/test-suite/src/test/groovy/io/micronaut/http/client/http2/Http2RequestSpec.groovy +++ b/test-suite/src/test/groovy/io/micronaut/http/client/http2/Http2RequestSpec.groovy @@ -152,6 +152,7 @@ class Http2RequestSpec extends Specification { "micronaut.server.http-version" : "2.0", 'micronaut.server.ssl.buildSelfSigned': true, 'micronaut.server.ssl.port': -1, + "micronaut.http.client.http-version" : "1.1", "micronaut.http.client.log-level" : "TRACE", "micronaut.server.netty.log-level" : "TRACE", 'micronaut.http.client.ssl.insecure-trust-all-certificates': true @@ -198,6 +199,7 @@ class Http2RequestSpec extends Specification { "micronaut.server.http-version" : "2.0", 'micronaut.server.ssl.buildSelfSigned': true, 'micronaut.server.ssl.port': -1, + "micronaut.http.client.http-version" : "1.1", "micronaut.http.client.log-level" : "TRACE", "micronaut.server.netty.log-level" : "TRACE" ]) diff --git a/test-suite/src/test/groovy/io/micronaut/http2/Http2AccessLoggerSpec.groovy b/test-suite/src/test/groovy/io/micronaut/http2/Http2AccessLoggerSpec.groovy index fcfc136201f..8839249b5d0 100644 --- a/test-suite/src/test/groovy/io/micronaut/http2/Http2AccessLoggerSpec.groovy +++ b/test-suite/src/test/groovy/io/micronaut/http2/Http2AccessLoggerSpec.groovy @@ -1,22 +1,21 @@ package io.micronaut.http2 +import ch.qos.logback.classic.Logger import ch.qos.logback.classic.spi.ILoggingEvent import ch.qos.logback.core.AppenderBase -import ch.qos.logback.classic.Logger -import io.micronaut.http.client.HttpClient -import io.micronaut.http.client.StreamingHttpClient -import org.reactivestreams.Publisher -import org.slf4j.LoggerFactory - import io.micronaut.context.ApplicationContext import io.micronaut.core.type.Argument import io.micronaut.docs.server.json.Person import io.micronaut.http.HttpRequest import io.micronaut.http.MediaType import io.micronaut.http.annotation.Get +import io.micronaut.http.client.HttpClient +import io.micronaut.http.client.StreamingHttpClient import io.micronaut.http.client.annotation.Client import io.micronaut.http.sse.Event import io.micronaut.runtime.server.EmbeddedServer +import org.reactivestreams.Publisher +import org.slf4j.LoggerFactory import reactor.core.publisher.Flux import spock.lang.AutoCleanup import spock.lang.Shared @@ -144,6 +143,7 @@ class Http2AccessLoggerSpec extends Specification { 'micronaut.server.ssl.buildSelfSigned': true, 'micronaut.server.ssl.port': -1, "micronaut.http.client.log-level" : "TRACE", + "micronaut.http.client.http-version" : "1.1", "micronaut.server.netty.log-level" : "TRACE", 'micronaut.server.netty.access-logger.enabled': true, 'micronaut.http.client.ssl.insecure-trust-all-certificates': true diff --git a/test-suite/src/test/java/io/micronaut/docs/netty/LogbookNettyClientCustomizer.java b/test-suite/src/test/java/io/micronaut/docs/netty/LogbookNettyClientCustomizer.java index a2d0069f968..6f37b73fc11 100644 --- a/test-suite/src/test/java/io/micronaut/docs/netty/LogbookNettyClientCustomizer.java +++ b/test-suite/src/test/java/io/micronaut/docs/netty/LogbookNettyClientCustomizer.java @@ -8,7 +8,6 @@ import io.micronaut.http.client.netty.NettyClientCustomizer; import io.micronaut.http.netty.channel.ChannelPipelineCustomizer; import io.netty.channel.Channel; -import io.netty.channel.ChannelPipeline; import jakarta.inject.Singleton; import org.zalando.logbook.Logbook; import org.zalando.logbook.netty.LogbookClientHandler; @@ -47,8 +46,9 @@ public NettyClientCustomizer specializeForChannel(Channel channel, ChannelRole r } @Override - public void onStreamPipelineBuilt() { - channel.pipeline().addLast( // <5> + public void onRequestPipelineBuilt() { + channel.pipeline().addBefore( // <5> + ChannelPipelineCustomizer.HANDLER_HTTP_STREAM, "logbook", new LogbookClientHandler(logbook) ); From 5a72eaeee3187c50ba328f74a81a43be16e64c87 Mon Sep 17 00:00:00 2001 From: micronaut-build <65172877+micronaut-build@users.noreply.github.com> Date: Thu, 20 Oct 2022 05:16:58 -0400 Subject: [PATCH 138/743] Bump micronaut-redis to 5.3.1 (#8193) --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index de0095d1313..796a4d4047b 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -107,7 +107,7 @@ managed-micronaut-problem = "2.5.1" managed-micronaut-rabbitmq = "3.3.0" managed-micronaut-r2dbc = "4.0.0" managed-micronaut-reactor = "2.4.1" -managed-micronaut-redis = "5.3.0" +managed-micronaut-redis = "5.3.1" managed-micronaut-rss = "3.2.0" managed-micronaut-rxjava1 = "1.0.0" managed-micronaut-rxjava2 = "1.3.0" From 84d9b7a60b4d5460bdcd02db9203b01cd91d6f28 Mon Sep 17 00:00:00 2001 From: Denis Stepanov Date: Fri, 21 Oct 2022 11:44:21 +0400 Subject: [PATCH 139/743] Allow to query beans without any qualifier (#8177) * Allow to query beans without any qualifier * Fix Sonar --- .../inject/context/register/Abc.java | 4 ++ .../inject/context/register/AbcFactory.java | 13 +++++ ...SingletonWithDifferentQualifierSpec.groovy | 43 +++++++++++++++ .../inject/qualifiers/NoneQualifier.java | 54 +++++++++++++++++++ .../inject/qualifiers/Qualifiers.java | 12 +++++ 5 files changed, 126 insertions(+) create mode 100644 inject-java/src/test/groovy/io/micronaut/inject/context/register/Abc.java create mode 100644 inject-java/src/test/groovy/io/micronaut/inject/context/register/AbcFactory.java create mode 100644 inject-java/src/test/groovy/io/micronaut/inject/context/register/RegisterASingletonWithDifferentQualifierSpec.groovy create mode 100644 inject/src/main/java/io/micronaut/inject/qualifiers/NoneQualifier.java diff --git a/inject-java/src/test/groovy/io/micronaut/inject/context/register/Abc.java b/inject-java/src/test/groovy/io/micronaut/inject/context/register/Abc.java new file mode 100644 index 00000000000..a53746d9def --- /dev/null +++ b/inject-java/src/test/groovy/io/micronaut/inject/context/register/Abc.java @@ -0,0 +1,4 @@ +package io.micronaut.inject.context.register; + +class Abc { +} diff --git a/inject-java/src/test/groovy/io/micronaut/inject/context/register/AbcFactory.java b/inject-java/src/test/groovy/io/micronaut/inject/context/register/AbcFactory.java new file mode 100644 index 00000000000..43b2e7cdea0 --- /dev/null +++ b/inject-java/src/test/groovy/io/micronaut/inject/context/register/AbcFactory.java @@ -0,0 +1,13 @@ +package io.micronaut.inject.context.register; + +import io.micronaut.context.annotation.Factory; +import jakarta.inject.Singleton; + +@Factory +class AbcFactory { + + @Singleton + Abc produce() { + return new Abc(); + } +} diff --git a/inject-java/src/test/groovy/io/micronaut/inject/context/register/RegisterASingletonWithDifferentQualifierSpec.groovy b/inject-java/src/test/groovy/io/micronaut/inject/context/register/RegisterASingletonWithDifferentQualifierSpec.groovy new file mode 100644 index 00000000000..8606f4ab50f --- /dev/null +++ b/inject-java/src/test/groovy/io/micronaut/inject/context/register/RegisterASingletonWithDifferentQualifierSpec.groovy @@ -0,0 +1,43 @@ +package io.micronaut.inject.context.register + + +import io.micronaut.context.DefaultBeanContext +import io.micronaut.inject.qualifiers.Qualifiers +import spock.lang.Specification + +class RegisterASingletonWithDifferentQualifierSpec extends Specification { + + def "test registering the same singleton class with a different qualifier"() { + given: + def ctx = DefaultBeanContext.run() + + when: + def q1 = ctx.createBean(Abc, Qualifiers.none()) + ctx.registerSingleton(Abc.class, q1, Qualifiers.byName("ONE")) + then: + noExceptionThrown() + + when: + def q2 = ctx.getBean(Abc, Qualifiers.byName("ONE")) + then: + q1 == q2 + + when: + def q3 = ctx.createBean(Abc, Qualifiers.none()) + ctx.registerSingleton(Abc.class, q3, Qualifiers.byName("TWO")) + def q4 = ctx.getBean(Abc, Qualifiers.byName("TWO")) + + then: + q1 == q2 + q1 != q3 + q1 != q4 + q3 == q4 + and: + ctx.getBeanRegistrations(Abc).size() == 3 + + cleanup: + ctx.close() + } +} + + diff --git a/inject/src/main/java/io/micronaut/inject/qualifiers/NoneQualifier.java b/inject/src/main/java/io/micronaut/inject/qualifiers/NoneQualifier.java new file mode 100644 index 00000000000..c5338437d48 --- /dev/null +++ b/inject/src/main/java/io/micronaut/inject/qualifiers/NoneQualifier.java @@ -0,0 +1,54 @@ +/* + * Copyright 2017-2021 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.inject.qualifiers; + +import io.micronaut.context.Qualifier; +import io.micronaut.core.annotation.AnnotationUtil; +import io.micronaut.core.annotation.Internal; +import io.micronaut.inject.BeanDefinition; +import io.micronaut.inject.BeanType; + +import java.util.stream.Stream; + +/** + * A qualifier to lookup beans without any qualifier. + * + * @param The generic type + * @since 3.8.0 + */ +@Internal +final class NoneQualifier implements Qualifier { + @SuppressWarnings("rawtypes") + public static final NoneQualifier INSTANCE = new NoneQualifier(); + + private NoneQualifier() { + } + + @Override + public > Stream reduce(Class beanType, Stream candidates) { + return candidates.filter(candidate -> { + if (candidate instanceof BeanDefinition) { + return ((BeanDefinition) candidate).getDeclaredQualifier() == null; + } + return !candidate.getAnnotationMetadata().hasDeclaredAnnotation(AnnotationUtil.QUALIFIER); + }); + } + + @Override + public String toString() { + return "None"; + } +} diff --git a/inject/src/main/java/io/micronaut/inject/qualifiers/Qualifiers.java b/inject/src/main/java/io/micronaut/inject/qualifiers/Qualifiers.java index a574bca4ab9..4108f04361c 100644 --- a/inject/src/main/java/io/micronaut/inject/qualifiers/Qualifiers.java +++ b/inject/src/main/java/io/micronaut/inject/qualifiers/Qualifiers.java @@ -61,6 +61,18 @@ public static Qualifier any() { return AnyQualifier.INSTANCE; } + /** + * Allows looking up for beans without any qualifier. + * + * @param The generic type + * @return The none qualifier. + * @since 3.8.0 + */ + @SuppressWarnings("unchecked") + public static Qualifier none() { + return NoneQualifier.INSTANCE; + } + /** * Build a qualifier for the given argument. * From 219cbdc468030d85bc96c919d022b3d334c5df06 Mon Sep 17 00:00:00 2001 From: SplotyCode <31861387+SplotyCode@users.noreply.github.com> Date: Fri, 21 Oct 2022 12:21:57 +0200 Subject: [PATCH 140/743] Support http status 103 and 425 (#8123) Signed-off-by: david.scandurra Signed-off-by: david.scandurra --- http/src/main/java/io/micronaut/http/HttpStatus.java | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/http/src/main/java/io/micronaut/http/HttpStatus.java b/http/src/main/java/io/micronaut/http/HttpStatus.java index bbe0a2480f8..90b941c40a0 100644 --- a/http/src/main/java/io/micronaut/http/HttpStatus.java +++ b/http/src/main/java/io/micronaut/http/HttpStatus.java @@ -30,6 +30,7 @@ public enum HttpStatus implements CharSequence { CONTINUE(100, "Continue"), SWITCHING_PROTOCOLS(101, "Switching Protocols"), PROCESSING(102, "Processing"), + EARLY_HINTS(103, "Early Hints"), OK(200, "Ok"), CREATED(201, "Created"), ACCEPTED(202, "Accepted"), @@ -69,10 +70,11 @@ public enum HttpStatus implements CharSequence { EXPECTATION_FAILED(417, "Expectation Failed"), I_AM_A_TEAPOT(418, "I am a teapot"), ENHANCE_YOUR_CALM(420, "Enhance your calm"), + MISDIRECTED_REQUEST(421, "Misdirected Request"), UNPROCESSABLE_ENTITY(422, "Unprocessable Entity"), LOCKED(423, "Locked"), FAILED_DEPENDENCY(424, "Failed Dependency"), - UNORDERED_COLLECTION(425, "Unordered Collection"), + TOO_EARLY(425, "Too Early"), UPGRADE_REQUIRED(426, "Upgrade Required"), PRECONDITION_REQUIRED(428, "Precondition Required"), TOO_MANY_REQUESTS(429, "Too Many Requests"), @@ -136,6 +138,8 @@ public static HttpStatus valueOf(int code) { return SWITCHING_PROTOCOLS; case 102: return PROCESSING; + case 103: + return EARLY_HINTS; case 200: return OK; case 201: @@ -214,6 +218,8 @@ public static HttpStatus valueOf(int code) { return I_AM_A_TEAPOT; case 420: return ENHANCE_YOUR_CALM; + case 421: + return MISDIRECTED_REQUEST; case 422: return UNPROCESSABLE_ENTITY; case 423: @@ -221,7 +227,7 @@ public static HttpStatus valueOf(int code) { case 424: return FAILED_DEPENDENCY; case 425: - return UNORDERED_COLLECTION; + return TOO_EARLY; case 426: return UPGRADE_REQUIRED; case 428: From 77f84595e130891493fb2285577c1a4c7cd63763 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 23 Oct 2022 07:37:33 +0200 Subject: [PATCH 141/743] fix(deps): update managed-micrometer to v1.9.5 (#8207) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 581d06cbc02..e8de0d875be 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -68,7 +68,7 @@ managed-logback = "1.2.11" managed-lombok = "1.18.24" managed-maven-native-plugin = "0.9.13" managed-methvin-directory-watcher = "0.16.1" -managed-micrometer = "1.9.4" +managed-micrometer = "1.9.5" managed-micronaut-acme = "3.2.0" managed-micronaut-aot = "1.1.1" managed-micronaut-aws = "3.9.2" From 4b840c6a5f25d6cbfdd25195126a6f517f28a843 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 23 Oct 2022 07:37:45 +0200 Subject: [PATCH 142/743] fix(deps): update dependency io.projectreactor:reactor-core to v3.4.24 (#8205) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index e8de0d875be..767f4c98f9a 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -134,7 +134,7 @@ managed-netty = "4.1.82.Final" managed-reactive-pg-client = "0.11.4" managed-reactive-streams = "1.0.4" # This should be kept aligned with https://github.com/micronaut-projects/micronaut-reactor/blob/master/gradle.properties from the BOM -managed-reactor = "3.4.23" +managed-reactor = "3.4.24" managed-rxjava1 = "1.3.8" managed-rxjava1-interop = "0.13.7" managed-slf4j = "1.7.36" From 919c9ffc9ded78fe9f519adabcdf13b87f37ef35 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 23 Oct 2022 07:38:02 +0200 Subject: [PATCH 143/743] fix(deps): update dependency io.micronaut.redis:micronaut-redis-bom to v5.3.1 (#8204) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 767f4c98f9a..5df1ccad2c7 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -111,7 +111,7 @@ managed-micronaut-problem = "2.5.1" managed-micronaut-rabbitmq = "3.3.0" managed-micronaut-r2dbc = "4.0.0" managed-micronaut-reactor = "2.4.1" -managed-micronaut-redis = "5.3.0" +managed-micronaut-redis = "5.3.1" managed-micronaut-rss = "3.2.0" managed-micronaut-rxjava1 = "1.0.0" managed-micronaut-rxjava2 = "1.3.0" From c4d262c67a9f9d01bd98faa3ff6c28fcf2ff5977 Mon Sep 17 00:00:00 2001 From: A-Maged Date: Sun, 23 Oct 2022 09:38:36 +0400 Subject: [PATCH 144/743] doc: spelling mistake (#8201) --- src/main/docs/guide/introduction/whatsNew.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/docs/guide/introduction/whatsNew.adoc b/src/main/docs/guide/introduction/whatsNew.adoc index 442642fb330..090b6773ed1 100644 --- a/src/main/docs/guide/introduction/whatsNew.adoc +++ b/src/main/docs/guide/introduction/whatsNew.adoc @@ -60,7 +60,7 @@ public class AppHttpClientFilter implements HttpClientFilter { This version upgrades https://netty.io[Netty] from 4.1.77 to 4.1.79. Moreover, it contains improvements to the API to https://docs.micronaut.io/snapshot/guide/#nettyClientPipeline[configure the Netty Client Pipeline] and to https://docs.micronaut.io/snapshot/guide/#nettyServerPipeline[configure the Netty Server Pipeline]. -=== Improvements to HtttpClientException +=== Improvements to HttpClientException If present a `serviceId` field is populated in the `HttpClientException` and shown in the exception message. From 776afdd87baf112f86db877de550afd85582c355 Mon Sep 17 00:00:00 2001 From: Denis Stepanov Date: Mon, 24 Oct 2022 13:20:41 +0400 Subject: [PATCH 145/743] Match interceptors by all the bindings it defines (#8174) --- .../aop/chain/DefaultInterceptorRegistry.java | 227 ++++++-------- .../core/annotation/AnnotationValue.java | 12 +- .../aop/compile/AroundCompileSpec.groovy | 278 ++++++++++++++++-- .../aop/compile/AroundCompileSpec.groovy | 205 +++++++++---- src/main/docs/guide/appendix/breaks.adoc | 4 + 5 files changed, 508 insertions(+), 218 deletions(-) diff --git a/aop/src/main/java/io/micronaut/aop/chain/DefaultInterceptorRegistry.java b/aop/src/main/java/io/micronaut/aop/chain/DefaultInterceptorRegistry.java index 74930906688..ded51e71a62 100644 --- a/aop/src/main/java/io/micronaut/aop/chain/DefaultInterceptorRegistry.java +++ b/aop/src/main/java/io/micronaut/aop/chain/DefaultInterceptorRegistry.java @@ -15,11 +15,6 @@ */ package io.micronaut.aop.chain; -import java.lang.annotation.Annotation; -import java.util.Collection; -import java.util.List; -import java.util.stream.Stream; - import io.micronaut.aop.Adapter; import io.micronaut.aop.ConstructorInterceptor; import io.micronaut.aop.Interceptor; @@ -42,6 +37,11 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.lang.annotation.Annotation; +import java.util.Collection; +import java.util.List; +import java.util.stream.Stream; + import static io.micronaut.inject.qualifiers.InterceptorBindingQualifier.META_MEMBER_MEMBERS; /** @@ -62,31 +62,31 @@ public DefaultInterceptorRegistry(BeanContext beanContext) { @Override @NonNull public Interceptor[] resolveInterceptors( - @NonNull Executable method, - @NonNull Collection>> interceptors, - @NonNull InterceptorKind interceptorKind) { + @NonNull Executable method, + @NonNull Collection>> interceptors, + @NonNull InterceptorKind interceptorKind) { final AnnotationMetadata annotationMetadata = method.getAnnotationMetadata(); if (interceptors.isEmpty()) { return resolveToNone((ExecutableMethod) method, interceptorKind, annotationMetadata); } else { instrumentAnnotationMetadata(beanContext, method); final Collection> applicableBindings - = AbstractInterceptorChain.resolveInterceptorValues( - annotationMetadata, - interceptorKind + = AbstractInterceptorChain.resolveInterceptorValues( + annotationMetadata, + interceptorKind ); if (applicableBindings.isEmpty()) { return resolveToNone((ExecutableMethod) method, interceptorKind, annotationMetadata); } else { @SuppressWarnings({"unchecked", - "rawtypes"}) final Interceptor[] resolvedInterceptors = - (Interceptor[]) interceptorStream( - method.getDeclaringType(), - (Collection) interceptors, - interceptorKind, - applicableBindings - ).filter(bean -> (bean instanceof MethodInterceptor) || !(bean instanceof ConstructorInterceptor)) - .toArray(Interceptor[]::new); + "rawtypes"}) final Interceptor[] resolvedInterceptors = + (Interceptor[]) interceptorStream( + method.getDeclaringType(), + (Collection) interceptors, + interceptorKind, + applicableBindings + ).filter(bean -> (bean instanceof MethodInterceptor) || !(bean instanceof ConstructorInterceptor)) + .toArray(Interceptor[]::new); if (LOG.isTraceEnabled()) { LOG.trace("Resolved {} {} interceptors out of a possible {} for method: {} - {}", resolvedInterceptors.length, interceptorKind, interceptors.size(), method.getDeclaringType(), method instanceof Described ? ((Described) method).getDescription(true) : method.toString()); for (int i = 0; i < resolvedInterceptors.length; i++) { @@ -106,7 +106,7 @@ private Interceptor[] resolveToNone(ExecutableMethod method, AnnotationMetadata annotationMetadata) { if (interceptorKind == InterceptorKind.INTRODUCTION) { if (annotationMetadata.hasStereotype(Adapter.class)) { - return new MethodInterceptor[] { new AdapterIntroduction(beanContext, method) }; + return new MethodInterceptor[]{new AdapterIntroduction(beanContext, method)}; } else { throw new IllegalStateException("At least one @Introduction method interceptor required, but missing for method: " + method.getDescription(true) + ". Check if your @Introduction stereotype annotation is marked with @Retention(RUNTIME) and @InterceptorBean(..) with the interceptor type. Otherwise do not load @Introduction beans if their interceptor definitions are missing!"); @@ -119,138 +119,99 @@ private Interceptor[] resolveToNone(ExecutableMethod method, private Stream> interceptorStream(Class declaringType, Collection>> interceptors, InterceptorKind interceptorKind, - Collection> applicableBindings) { + Collection> interceptPointBindings) { return interceptors.stream() - .filter(beanRegistration -> { - final List> typeArgs = beanRegistration.getBeanDefinition().getTypeArguments(ConstructorInterceptor.class); - if (typeArgs.isEmpty()) { + .filter(beanRegistration -> { + final List> typeArgs = beanRegistration.getBeanDefinition().getTypeArguments(ConstructorInterceptor.class); + if (typeArgs.isEmpty()) { + return true; + } else { + final Class applicableType = typeArgs.iterator().next().getType(); + return applicableType.isAssignableFrom(declaringType); + } + }) + .filter(beanRegistration -> { + // does the annotation metadata contain @InterceptorBinding(interceptorType=SomeInterceptor.class) + // this behaviour is in place for backwards compatible for the old @Type(SomeInterceptor.class) approach + // In this case we don't care about any qualifiers + for (AnnotationValue applicableValue : interceptPointBindings) { + if (isApplicableByType(beanRegistration, applicableValue)) { return true; - } else { - final Class applicableType = typeArgs.iterator().next().getType(); - return applicableType.isAssignableFrom(declaringType); } - }) - .filter(beanRegistration -> { - // these are the binding declared on the interceptor itself - // an interceptor can declare one or more bindings - final Collection> interceptorValues = AbstractInterceptorChain - .resolveInterceptorValues( - beanRegistration.getBeanDefinition().getAnnotationMetadata(), interceptorKind - ); - // if there are no binding try simply match by interceptor type - if (interceptorValues.isEmpty()) { - // does the annotation metadata contain @InterceptorBinding(interceptorType=SomeInterceptor.class) - // that matches the list of interceptors ? - // this behaviour is in place for backwards compatible for the old @Type(SomeInterceptor.class) approach - for (AnnotationValue applicableValue : applicableBindings) { - if (isApplicableByType(beanRegistration, applicableValue)) { - return true; - } - } + } + // these are the binding declared on the interceptor itself + // an interceptor can declare one or more bindings + final Collection> interceptorValues = AbstractInterceptorChain + .resolveInterceptorValues( + beanRegistration.getBeanDefinition().getAnnotationMetadata(), interceptorKind + ); + if (interceptorValues.isEmpty()) { + // Bean is an interceptor but no bindings??? + return false; + } + // loop through the bindings on the interceptor and make sure that + // the intercept point has the same once + for (AnnotationValue interceptorAnnotationValue : interceptorValues) { + if (!matches(interceptorAnnotationValue, interceptPointBindings)) { return false; - } else { - if (interceptorValues.size() == 1) { - // single interceptor binding, fast path - final AnnotationValue interceptorBinding = interceptorValues.iterator().next(); - final AnnotationValue memberBinding = interceptorBinding - .getAnnotation(META_MEMBER_MEMBERS).orElse(null); - final String annotationName = interceptorBinding.stringValue().orElse(null); - if (annotationName != null) { - // any match - for (AnnotationValue applicableBinding : applicableBindings) { - if (isApplicableByType(beanRegistration, - applicableBinding)) { - return true; - } else if (annotationName.equals(applicableBinding.stringValue().orElse(null))) { - if (memberBinding != null) { - final AnnotationValue otherMembers = - applicableBinding.getAnnotation(META_MEMBER_MEMBERS).orElse(null); - if (memberBinding.equals(otherMembers)) { - return true; - } - } else { - return true; - } - } - } - } - return false; - } else { - // multiple binding case. - boolean isApplicationByBinding = true; - // loop through the bindings on the interceptor and make sure that - // the binding on the injection point matches up - for (AnnotationValue annotationValue : applicableBindings) { - final AnnotationValue memberBinding = annotationValue - .getAnnotation(META_MEMBER_MEMBERS).orElse(null); - final String annotationName = annotationValue.stringValue().orElse(null); - - if (annotationName != null) { - boolean interceptorApplicable = true; - for (AnnotationValue applicableValue : interceptorValues) { - if (isApplicableByType(beanRegistration, - applicableValue)) { - return true; - } - - // does the annotation metadata of the interceptor definition contain - // @InterceptorBinding(SomeAnnotation.class) ? - if (annotationName.equals(applicableValue.stringValue().orElse(null))) { - // if it does do we need to bind the members - if (memberBinding != null) { - final AnnotationValue otherMembers = - applicableValue.getAnnotation(META_MEMBER_MEMBERS).orElse(null); - interceptorApplicable = memberBinding.equals(otherMembers); - if (interceptorApplicable) { - break; - } - } else { - interceptorApplicable = true; - break; - } - } else { - interceptorApplicable = false; - } - } - isApplicationByBinding = interceptorApplicable; - if (!isApplicationByBinding) { - break; - } - } - } - return isApplicationByBinding; - } - } - }).sorted(OrderUtil.COMPARATOR) - .map(BeanRegistration::getBean); + } + return true; + }).sorted(OrderUtil.COMPARATOR) + .map(BeanRegistration::getBean); + } + + private boolean matches(AnnotationValue interceptorAnnotationValue, Collection> interceptPointBindings) { + final AnnotationValue memberBinding = interceptorAnnotationValue + .getAnnotation(META_MEMBER_MEMBERS).orElse(null); + final String annotationName = interceptorAnnotationValue.stringValue().orElse(null); + if (annotationName == null) { + // This shouldn't happen + return false; + } + for (AnnotationValue applicableValue : interceptPointBindings) { + String interceptPointAnnotation = applicableValue.stringValue().orElse(null); + if (!annotationName.equals(interceptPointAnnotation)) { + continue; + } + if (memberBinding == null) { + return true; + } + AnnotationValue otherMembers = + applicableValue.getAnnotation(META_MEMBER_MEMBERS).orElse(null); + if (!memberBinding.equals(otherMembers)) { + continue; + } + return true; + } + return false; } private boolean isApplicableByType(BeanRegistration> beanRegistration, AnnotationValue applicableValue) { return applicableValue.classValue("interceptorType") - .map(t -> t.isInstance(beanRegistration.getBean())).orElse(false); + .map(t -> t.isInstance(beanRegistration.getBean())).orElse(false); } @Override @NonNull - public Interceptor[] resolveConstructorInterceptors( - @NonNull BeanConstructor constructor, - @NonNull Collection>> interceptors) { + public Interceptor[] resolveConstructorInterceptors( + @NonNull BeanConstructor constructor, + @NonNull Collection>> interceptors) { instrumentAnnotationMetadata(beanContext, constructor); final Collection> applicableBindings - = AbstractInterceptorChain.resolveInterceptorValues( - constructor.getAnnotationMetadata(), - InterceptorKind.AROUND_CONSTRUCT + = AbstractInterceptorChain.resolveInterceptorValues( + constructor.getAnnotationMetadata(), + InterceptorKind.AROUND_CONSTRUCT ); final Interceptor[] resolvedInterceptors = interceptorStream( - constructor.getDeclaringBeanType(), - interceptors, - InterceptorKind.AROUND_CONSTRUCT, - applicableBindings + constructor.getDeclaringBeanType(), + interceptors, + InterceptorKind.AROUND_CONSTRUCT, + applicableBindings ) - .filter(bean -> (bean instanceof ConstructorInterceptor) || !(bean instanceof MethodInterceptor)) - .toArray(Interceptor[]::new); + .filter(bean -> (bean instanceof ConstructorInterceptor) || !(bean instanceof MethodInterceptor)) + .toArray(Interceptor[]::new); if (LOG.isTraceEnabled()) { LOG.trace("Resolved {} {} interceptors out of a possible {} for constructor: {} - {}", resolvedInterceptors.length, InterceptorKind.AROUND_CONSTRUCT, interceptors.size(), constructor.getDeclaringBeanType(), constructor.getDescription(true)); for (int i = 0; i < resolvedInterceptors.length; i++) { diff --git a/core/src/main/java/io/micronaut/core/annotation/AnnotationValue.java b/core/src/main/java/io/micronaut/core/annotation/AnnotationValue.java index b0060cf2a63..9fa33f94a7a 100644 --- a/core/src/main/java/io/micronaut/core/annotation/AnnotationValue.java +++ b/core/src/main/java/io/micronaut/core/annotation/AnnotationValue.java @@ -1218,11 +1218,21 @@ public String toString() { if (values.isEmpty()) { return "@" + annotationName; } else { - return "@" + annotationName + "(" + values.entrySet().stream().map(entry -> entry.getKey() + "=" + entry.getValue()).collect( + return "@" + annotationName + "(" + values.entrySet().stream().map(entry -> entry.getKey() + "=" + toStringValue(entry.getValue())).collect( Collectors.joining(", ")) + ")"; } } + private String toStringValue(Object object) { + if (object == null) { + return "null"; + } + if (object instanceof Object[]) { + return Arrays.deepToString((Object[]) object); + } + return object.toString(); + } + @Override public int hashCode() { return 31 * annotationName.hashCode() + AnnotationUtil.calculateHashCode(getValues()); diff --git a/inject-groovy/src/test/groovy/io/micronaut/aop/compile/AroundCompileSpec.groovy b/inject-groovy/src/test/groovy/io/micronaut/aop/compile/AroundCompileSpec.groovy index d6d542d6860..ffabb7f7be7 100644 --- a/inject-groovy/src/test/groovy/io/micronaut/aop/compile/AroundCompileSpec.groovy +++ b/inject-groovy/src/test/groovy/io/micronaut/aop/compile/AroundCompileSpec.groovy @@ -34,9 +34,9 @@ import static java.lang.annotation.RetentionPolicy.RUNTIME; class MyBean { @TestAnn void test() { - + } - + } @Retention(RUNTIME) @@ -52,7 +52,7 @@ class TestInterceptor implements Interceptor { invoked = true; return context.proceed(); } -} +} ''') def instance = getBean(context, 'mapperbinding.MyBean') @@ -82,12 +82,12 @@ import io.micronaut.aop.simple.*; class MyBean { @TestAnn void test() { - + } - + @TestAnn2 void test2() { - + } } @@ -111,7 +111,7 @@ class TestInterceptor implements Interceptor { invoked = true; return context.proceed(); } -} +} @InterceptorBean(TestAnn2.class) class AnotherInterceptor implements Interceptor { @@ -121,7 +121,7 @@ class AnotherInterceptor implements Interceptor { invoked = true; return context.proceed(); } -} +} ''') def instance = getBean(context, 'annbinding2.MyBean') def interceptor = getBean(context, 'annbinding2.TestInterceptor') @@ -177,7 +177,7 @@ class TestInterceptor implements Interceptor { invoked = true; return context.proceed(); } -} +} @Singleton class AnotherInterceptor implements Interceptor { @@ -187,7 +187,7 @@ class AnotherInterceptor implements Interceptor { invoked = true; return context.proceed(); } -} +} ''') def instance = getBean(context, 'annbinding1.MyBean') def interceptor = getBean(context, 'annbinding1.TestInterceptor') @@ -234,7 +234,7 @@ class TestInterceptor implements Interceptor { invoked = true; return context.proceed(); } -} +} @Singleton class AnotherInterceptor implements Interceptor { @@ -244,7 +244,7 @@ class AnotherInterceptor implements Interceptor { invoked = true; return context.proceed(); } -} +} ''') def instance = getBean(context, 'justaround.MyBean') def interceptor = getBean(context, 'justaround.TestInterceptor') @@ -397,9 +397,9 @@ class MyBean { @TestAnn @TestAnn2 void test() { - + } - + } @Retention(RUNTIME) @@ -422,7 +422,7 @@ class TestInterceptor implements Interceptor { invoked = true; return context.proceed(); } -} +} @InterceptorBean(TestAnn2.class) class AnotherInterceptor implements Interceptor { @@ -432,7 +432,7 @@ class AnotherInterceptor implements Interceptor { invoked = true; return context.proceed(); } -} +} ''') def instance = getBean(context, 'annbinding2.MyBean') def interceptor = getBean(context, 'annbinding2.TestInterceptor') @@ -450,6 +450,225 @@ class AnotherInterceptor implements Interceptor { context.close() } + void 'test multiple interceptor binding'() { + given: + ApplicationContext context = buildContext(''' +package multiplebinding; + +import java.lang.annotation.*; +import io.micronaut.aop.*; +import io.micronaut.context.annotation.NonBinding; +import static java.lang.annotation.RetentionPolicy.RUNTIME; +import jakarta.inject.Singleton; + +@Retention(RUNTIME) +@InterceptorBinding(kind = InterceptorKind.AROUND) +@interface Deadly { + +} + +@Retention(RUNTIME) +@InterceptorBinding(kind = InterceptorKind.AROUND) +@interface Fast { +} + +@Retention(RUNTIME) +@InterceptorBinding(kind = InterceptorKind.AROUND) +@interface Slow { +} + +@UFO +@Inherited +@Retention(RUNTIME) +@InterceptorBinding(kind = InterceptorKind.AROUND) +@interface MissileAnn { +} + +@Inherited +@Retention(RUNTIME) +@InterceptorBinding(kind = InterceptorKind.AROUND) +@interface UFO { +} + +@MissileAnn +interface Missile { + void fire(); +} + +@Fast +@Deadly +@Singleton +class FastAndDeadlyMissile implements Missile { + public void fire() { + } +} + +@Deadly +@Singleton +class DeadlyMissile implements Missile { + public void fire() { + } +} + +@Deadly +@Singleton +class GuidedMissile implements Missile { + + @Slow + public void lockAndFire() { + } + + @Fast + public void fire() { + } + +} + +@Slow +@Deadly +@Singleton +class SlowMissile implements Missile { + public void fire() { + } +} + +@Fast +@Deadly +@MissileAnn +@Singleton +class FastDeadlyInterceptor implements MethodInterceptor { + public boolean intercepted = false; + + @Override public Object intercept(MethodInvocationContext context) { + intercepted = true; + return context.proceed(); + } +} + +@Slow +@Deadly +@MissileAnn +@Singleton +class SlowDeadlyInterceptor implements MethodInterceptor { + public boolean intercepted = false; + + @Override public Object intercept(MethodInvocationContext context) { + intercepted = true; + return context.proceed(); + } +} + +@Deadly +@UFO +@Singleton +class DeadlyInterceptor implements MethodInterceptor { + public boolean intercepted = false; + + @Override public Object intercept(MethodInvocationContext context) { + intercepted = true; + return context.proceed(); + } + + public void reset() { + intercepted = false; + } +} + +@UFO +@Singleton +class UFOInterceptor implements MethodInterceptor { + public boolean intercepted = false; + + @Override public Object intercept(MethodInvocationContext context) { + intercepted = true; + return context.proceed(); + } + + public void reset() { + intercepted = false; + } +} + +''') + def fastDeadlyInterceptor = getBean(context, 'multiplebinding.FastDeadlyInterceptor') + def slowDeadlyInterceptor = getBean(context, 'multiplebinding.SlowDeadlyInterceptor') + def deadlyInterceptor = getBean(context, 'multiplebinding.DeadlyInterceptor') + def ufoInterceptor = getBean(context, 'multiplebinding.UFOInterceptor') + + when: + fastDeadlyInterceptor.intercepted = false + slowDeadlyInterceptor.intercepted = false + deadlyInterceptor.intercepted = false + ufoInterceptor.intercepted = false + def guidedMissile = getBean(context, 'multiplebinding.GuidedMissile'); + guidedMissile.fire() + + then: + fastDeadlyInterceptor.intercepted + !slowDeadlyInterceptor.intercepted + deadlyInterceptor.intercepted + ufoInterceptor.intercepted + + when: + fastDeadlyInterceptor.intercepted = false + slowDeadlyInterceptor.intercepted = false + deadlyInterceptor.intercepted = false + ufoInterceptor.intercepted = false + guidedMissile = getBean(context, 'multiplebinding.GuidedMissile'); + guidedMissile.lockAndFire() + + then: + !fastDeadlyInterceptor.intercepted + slowDeadlyInterceptor.intercepted + deadlyInterceptor.intercepted + ufoInterceptor.intercepted + + when: + fastDeadlyInterceptor.intercepted = false + slowDeadlyInterceptor.intercepted = false + deadlyInterceptor.intercepted = false + ufoInterceptor.intercepted = false + def fastAndDeadlyMissile = getBean(context, 'multiplebinding.FastAndDeadlyMissile'); + fastAndDeadlyMissile.fire() + + then: + fastDeadlyInterceptor.intercepted + !slowDeadlyInterceptor.intercepted + deadlyInterceptor.intercepted + ufoInterceptor.intercepted + + when: + fastDeadlyInterceptor.intercepted = false + slowDeadlyInterceptor.intercepted = false + deadlyInterceptor.intercepted = false + ufoInterceptor.intercepted = false + def slowMissile = getBean(context, 'multiplebinding.SlowMissile'); + slowMissile.fire() + + then: + !fastDeadlyInterceptor.intercepted + slowDeadlyInterceptor.intercepted + deadlyInterceptor.intercepted + ufoInterceptor.intercepted + + when: + fastDeadlyInterceptor.intercepted = false + slowDeadlyInterceptor.intercepted = false + deadlyInterceptor.intercepted = false + ufoInterceptor.intercepted = false + def anyMissile = getBean(context, 'multiplebinding.DeadlyMissile'); + anyMissile.fire() + + then: + !fastDeadlyInterceptor.intercepted + !slowDeadlyInterceptor.intercepted + deadlyInterceptor.intercepted + ufoInterceptor.intercepted + + cleanup: + context.close() + } + void 'test multiple annotations on an interceptor and method'() { given: ApplicationContext context = buildContext(''' @@ -467,9 +686,9 @@ class MyBean { @TestAnn @TestAnn2 void test() { - + } - + } @Retention(RUNTIME) @@ -524,14 +743,17 @@ class MyBean { @TestAnn void test() { - } - + @TestAnn2 void test2() { - } - + + @TestAnn + @TestAnn2 + void testBoth() { + } + } @Retention(RUNTIME) @@ -547,7 +769,7 @@ class MyBean { } -@InterceptorBean([ TestAnn.class, TestAnn2.class ]) +@InterceptorBean([TestAnn.class, TestAnn2.class]) class TestInterceptor implements Interceptor { long count = 0; @Override @@ -564,13 +786,19 @@ class TestInterceptor implements Interceptor { instance.test() then: - interceptor.count == 1 + interceptor.count == 0 when: instance.test2() then: - interceptor.count == 2 + interceptor.count == 0 + + when: + instance.testBoth() + + then: + interceptor.count == 1 cleanup: context.close() diff --git a/inject-java/src/test/groovy/io/micronaut/aop/compile/AroundCompileSpec.groovy b/inject-java/src/test/groovy/io/micronaut/aop/compile/AroundCompileSpec.groovy index 6d3d5f384cf..38037b5cb79 100644 --- a/inject-java/src/test/groovy/io/micronaut/aop/compile/AroundCompileSpec.groovy +++ b/inject-java/src/test/groovy/io/micronaut/aop/compile/AroundCompileSpec.groovy @@ -36,9 +36,9 @@ import io.micronaut.aop.simple.*; class MyBean { @TestAnn2 void test() { - + } - + } @Retention(RUNTIME) @@ -61,7 +61,7 @@ class TestInterceptor implements Interceptor { invoked = true; return context.proceed(); } -} +} ''') def instance = getBean(context, 'annbinding2.MyBean') @@ -93,9 +93,9 @@ import static java.lang.annotation.RetentionPolicy.RUNTIME; class MyBean { @TestAnn void test() { - + } - + } @Retention(RUNTIME) @@ -111,7 +111,7 @@ class TestInterceptor implements Interceptor { invoked = true; return context.proceed(); } -} +} ''') def instance = getBean(context, 'mapperbinding.MyBean') @@ -165,7 +165,7 @@ class TestInterceptor implements Interceptor { invoked = true; return context.proceed(); } -} +} @Singleton @TestAnn(num=2) @@ -176,7 +176,7 @@ class TestInterceptor2 implements Interceptor { invoked = true; return context.proceed(); } -} +} ''') def instance = getBean(context, 'mapperbindingmembers.MyBean') @@ -208,12 +208,12 @@ import io.micronaut.aop.simple.*; class MyBean { @TestAnn void test() { - + } - + @TestAnn2 void test2() { - + } } @@ -237,7 +237,7 @@ class TestInterceptor implements Interceptor { invoked = true; return context.proceed(); } -} +} @InterceptorBean(TestAnn2.class) class AnotherInterceptor implements Interceptor { @@ -247,7 +247,7 @@ class AnotherInterceptor implements Interceptor { invoked = true; return context.proceed(); } -} +} ''') def instance = getBean(context, 'annbinding2.MyBean') def interceptor = getBean(context, 'annbinding2.TestInterceptor') @@ -304,7 +304,7 @@ class TestInterceptor implements Interceptor { invoked = true; return context.proceed(); } -} +} @Singleton class AnotherInterceptor implements Interceptor { @@ -314,7 +314,7 @@ class AnotherInterceptor implements Interceptor { invoked = true; return context.proceed(); } -} +} ''') def instance = getBean(context, 'annbinding1.MyBean') def interceptor = getBean(context, 'annbinding1.TestInterceptor') @@ -357,6 +357,20 @@ import jakarta.inject.Singleton; @interface Slow { } +@UFO +@Inherited +@Retention(RUNTIME) +@InterceptorBinding(kind = InterceptorKind.AROUND) +@interface MissileAnn { +} + +@Inherited +@Retention(RUNTIME) +@InterceptorBinding(kind = InterceptorKind.AROUND) +@interface UFO { +} + +@MissileAnn interface Missile { void fire(); } @@ -371,20 +385,20 @@ class FastAndDeadlyMissile implements Missile { @Deadly @Singleton -class AnyDeadlyMissile implements Missile { +class DeadlyMissile implements Missile { public void fire() { } } +@Deadly @Singleton class GuidedMissile implements Missile { + @Slow - @Deadly public void lockAndFire() { } @Fast - @Deadly public void fire() { } @@ -400,8 +414,9 @@ class SlowMissile implements Missile { @Fast @Deadly +@MissileAnn @Singleton -class MissileInterceptor implements MethodInterceptor { +class FastDeadlyInterceptor implements MethodInterceptor { public boolean intercepted = false; @Override public Object intercept(MethodInvocationContext context) { @@ -412,60 +427,123 @@ class MissileInterceptor implements MethodInterceptor { @Slow @Deadly +@MissileAnn +@Singleton +class SlowDeadlyInterceptor implements MethodInterceptor { + public boolean intercepted = false; + + @Override public Object intercept(MethodInvocationContext context) { + intercepted = true; + return context.proceed(); + } +} + +@Deadly +@UFO +@Singleton +class DeadlyInterceptor implements MethodInterceptor { + public boolean intercepted = false; + + @Override public Object intercept(MethodInvocationContext context) { + intercepted = true; + return context.proceed(); + } + + public void reset() { + intercepted = false; + } +} + +@UFO @Singleton -class LockInterceptor implements MethodInterceptor { +class UFOInterceptor implements MethodInterceptor { public boolean intercepted = false; @Override public Object intercept(MethodInvocationContext context) { intercepted = true; return context.proceed(); } + + public void reset() { + intercepted = false; + } } ''') - def missileInterceptor = getBean(context, 'multiplebinding.MissileInterceptor') - def lockInterceptor = getBean(context, 'multiplebinding.LockInterceptor') + def fastDeadlyInterceptor = getBean(context, 'multiplebinding.FastDeadlyInterceptor') + def slowDeadlyInterceptor = getBean(context, 'multiplebinding.SlowDeadlyInterceptor') + def deadlyInterceptor = getBean(context, 'multiplebinding.DeadlyInterceptor') + def ufoInterceptor = getBean(context, 'multiplebinding.UFOInterceptor') when: - missileInterceptor.intercepted = false - lockInterceptor.intercepted = false + fastDeadlyInterceptor.intercepted = false + slowDeadlyInterceptor.intercepted = false + deadlyInterceptor.intercepted = false + ufoInterceptor.intercepted = false def guidedMissile = getBean(context, 'multiplebinding.GuidedMissile'); guidedMissile.fire() then: - missileInterceptor.intercepted - !lockInterceptor.intercepted + fastDeadlyInterceptor.intercepted + !slowDeadlyInterceptor.intercepted + deadlyInterceptor.intercepted + ufoInterceptor.intercepted + + when: + fastDeadlyInterceptor.intercepted = false + slowDeadlyInterceptor.intercepted = false + deadlyInterceptor.intercepted = false + ufoInterceptor.intercepted = false + guidedMissile = getBean(context, 'multiplebinding.GuidedMissile'); + guidedMissile.lockAndFire() + + then: + !fastDeadlyInterceptor.intercepted + slowDeadlyInterceptor.intercepted + deadlyInterceptor.intercepted + ufoInterceptor.intercepted when: - missileInterceptor.intercepted = false - lockInterceptor.intercepted = false + fastDeadlyInterceptor.intercepted = false + slowDeadlyInterceptor.intercepted = false + deadlyInterceptor.intercepted = false + ufoInterceptor.intercepted = false def fastAndDeadlyMissile = getBean(context, 'multiplebinding.FastAndDeadlyMissile'); fastAndDeadlyMissile.fire() then: - missileInterceptor.intercepted - !lockInterceptor.intercepted + fastDeadlyInterceptor.intercepted + !slowDeadlyInterceptor.intercepted + deadlyInterceptor.intercepted + ufoInterceptor.intercepted when: - missileInterceptor.intercepted = false - lockInterceptor.intercepted = false + fastDeadlyInterceptor.intercepted = false + slowDeadlyInterceptor.intercepted = false + deadlyInterceptor.intercepted = false + ufoInterceptor.intercepted = false def slowMissile = getBean(context, 'multiplebinding.SlowMissile'); slowMissile.fire() then: - !missileInterceptor.intercepted - lockInterceptor.intercepted + !fastDeadlyInterceptor.intercepted + slowDeadlyInterceptor.intercepted + deadlyInterceptor.intercepted + ufoInterceptor.intercepted when: - missileInterceptor.intercepted = false - lockInterceptor.intercepted = false - def anyMissile = getBean(context, 'multiplebinding.AnyDeadlyMissile'); + fastDeadlyInterceptor.intercepted = false + slowDeadlyInterceptor.intercepted = false + deadlyInterceptor.intercepted = false + ufoInterceptor.intercepted = false + def anyMissile = getBean(context, 'multiplebinding.DeadlyMissile'); anyMissile.fire() then: - missileInterceptor.intercepted - lockInterceptor.intercepted - + !fastDeadlyInterceptor.intercepted + !slowDeadlyInterceptor.intercepted + deadlyInterceptor.intercepted + ufoInterceptor.intercepted cleanup: context.close() @@ -487,10 +565,10 @@ import jakarta.inject.Singleton; class MyBean { void test() { } - + @TestAnn(num=2) // overrides binding on type void test2() { - + } } @@ -499,7 +577,7 @@ class MyBean { @InterceptorBinding(bindMembers = true) @interface TestAnn { int num(); - + @NonBinding boolean debug() default false; } @@ -513,7 +591,7 @@ class TestInterceptor implements Interceptor { invoked = true; return context.proceed(); } -} +} @InterceptorBean(TestAnn.class) @TestAnn(num = 2) @@ -524,7 +602,7 @@ class AnotherInterceptor implements Interceptor { invoked = true; return context.proceed(); } -} +} ''') def instance = getBean(context, 'memberbinding.MyBean') def interceptor = getBean(context, 'memberbinding.TestInterceptor') @@ -581,7 +659,7 @@ class TestInterceptor implements Interceptor { invoked = true; return context.proceed(); } -} +} @Singleton class AnotherInterceptor implements Interceptor { @@ -591,7 +669,7 @@ class AnotherInterceptor implements Interceptor { invoked = true; return context.proceed(); } -} +} ''') def instance = getBean(context, 'justaround.MyBean') def interceptor = getBean(context, 'justaround.TestInterceptor') @@ -722,9 +800,9 @@ class MyBean { @TestAnn @TestAnn2 void test() { - + } - + } @Retention(RUNTIME) @@ -747,7 +825,7 @@ class TestInterceptor implements Interceptor { invoked = true; return context.proceed(); } -} +} @InterceptorBean(TestAnn2.class) class AnotherInterceptor implements Interceptor { @@ -757,7 +835,7 @@ class AnotherInterceptor implements Interceptor { invoked = true; return context.proceed(); } -} +} ''') def instance = getBean(context, 'annbinding2.MyBean') def interceptor = getBean(context, 'annbinding2.TestInterceptor') @@ -792,9 +870,9 @@ class MyBean { @TestAnn @TestAnn2 void test() { - + } - + } @Retention(RUNTIME) @@ -849,14 +927,17 @@ class MyBean { @TestAnn void test() { - } - + @TestAnn2 void test2() { - } - + + @TestAnn + @TestAnn2 + void testBoth() { + } + } @Retention(RUNTIME) @@ -889,13 +970,19 @@ class TestInterceptor implements Interceptor { instance.test() then: - interceptor.count == 1 + interceptor.count == 0 when: instance.test2() then: - interceptor.count == 2 + interceptor.count == 0 + + when: + instance.testBoth() + + then: + interceptor.count == 1 cleanup: context.close() diff --git a/src/main/docs/guide/appendix/breaks.adoc b/src/main/docs/guide/appendix/breaks.adoc index 7e42bcc1522..2b8ec43ad38 100644 --- a/src/main/docs/guide/appendix/breaks.adoc +++ b/src/main/docs/guide/appendix/breaks.adoc @@ -34,6 +34,10 @@ Before, when both METHOD and FIELD were set as the access kind, the bean introsp Annotations with the retention CLASS are not available in the annotation metadata at the runtime. +==== Interceptors with multiple interceptor bindings annotations + +Interceptors with multiple interceptor bindings annotations now require the same set of annotations to be present at the intercepted point. In the Micronaut 3 an interceptor with multiple binding annotations would need at least one of the binding annotations to be present at the intercepted point. + == 3.3.0 - The <> is now disabled by default. To enable it, you must update your endpoint config: From 124a6565e95b5e361c105faed1dceb7a3cca1c20 Mon Sep 17 00:00:00 2001 From: Alexey Zhokhov Date: Mon, 24 Oct 2022 17:33:11 +0800 Subject: [PATCH 146/743] Let SemanticVersion.isAtLeastMajorMinor be able to compare two different major versions. (#8208) --- .../io/micronaut/core/version/SemanticVersion.java | 8 +++++++- .../micronaut/core/version/SemanticVersionSpec.groovy | 11 +++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/core/src/main/java/io/micronaut/core/version/SemanticVersion.java b/core/src/main/java/io/micronaut/core/version/SemanticVersion.java index 3fa74c89506..08d671bbaf8 100644 --- a/core/src/main/java/io/micronaut/core/version/SemanticVersion.java +++ b/core/src/main/java/io/micronaut/core/version/SemanticVersion.java @@ -121,6 +121,12 @@ public static boolean isAtLeast(String version, String requiredVersion) { } private static boolean isAtLeastMajorMinorImpl(SemanticVersion version, int majorVersion, int minorVersion) { - return version != null && version.major >= majorVersion && version.minor >= minorVersion; + if (version != null) { + if (version.major == majorVersion) { + return version.minor >= minorVersion; + } + return version.major > majorVersion; + } + return false; } } diff --git a/core/src/test/groovy/io/micronaut/core/version/SemanticVersionSpec.groovy b/core/src/test/groovy/io/micronaut/core/version/SemanticVersionSpec.groovy index f64c4700d11..7d9e5958234 100644 --- a/core/src/test/groovy/io/micronaut/core/version/SemanticVersionSpec.groovy +++ b/core/src/test/groovy/io/micronaut/core/version/SemanticVersionSpec.groovy @@ -19,4 +19,15 @@ class SemanticVersionSpec extends Specification { semver << ["1.0.0", "1.0.0.M1", "1.0.0.RC1", "1.0.0.BUILD-SNAPSHOT", "1.0.0-M1", "1.0.0-RC1", "1.0.0-BUILD-SNAPSHOT", "1.0.0-SNAPSHOT"] } + void "it compare two different major versions: #semver"(String semver) { + expect: + SemanticVersion.isAtLeastMajorMinor(semver, 3, 3) + SemanticVersion.isAtLeastMajorMinor(semver, 3, 0) + !SemanticVersion.isAtLeastMajorMinor(semver, 5, 3) + !SemanticVersion.isAtLeastMajorMinor(semver, 5, 0) + + where: + semver << ["4.0.0", "4.0.0.M1", "4.0.0.RC1", "4.0.0.BUILD-SNAPSHOT", "4.0.0-M1", "4.0.0-RC1", "4.0.0-BUILD-SNAPSHOT", "4.0.0-SNAPSHOT"] + } + } From aacc3e56850bc4046151d1ffae31c372e3dac9cb Mon Sep 17 00:00:00 2001 From: Denis Stepanov Date: Mon, 24 Oct 2022 13:44:08 +0400 Subject: [PATCH 147/743] Reuse enclosed elements querying for Java and Groovy. (#8197) * Reuse enclosed elements querying for Java and Groovy. * Added a way to query methods and fields together. --- .../groovy/TypeElementVisitorTransform.groovy | 55 +- .../groovy/visitor/GroovyClassElement.java | 508 ++++++------------ .../visitor/GroovyEnumConstantElement.java | 6 +- .../visitor/GroovyDocumentationSpec.groovy | 2 +- .../inject/visitor/ClassElementSpec.groovy | 110 +++- .../processing/visitor/JavaClassElement.java | 417 +++++--------- .../visitor/JavaConstructorElement.java | 11 + .../visitor/JavaEnumConstantElement.java | 6 +- .../processing/visitor/JavaFieldElement.java | 9 + .../processing/visitor/JavaMethodElement.java | 9 + .../inject/ast/ConstructorElement.java | 10 + .../io/micronaut/inject/ast/ElementQuery.java | 7 + .../inject/ast/EnumConstantElement.java | 4 +- .../io/micronaut/inject/ast/FieldElement.java | 19 + .../micronaut/inject/ast/MemberElement.java | 11 + .../micronaut/inject/ast/MethodElement.java | 32 ++ .../ast/utils/AstBeanPropertiesUtils.java | 34 -- .../ast/utils/EnclosedElementsQuery.java | 287 ++++++++++ .../DeclaredBeanElementCreator.java | 47 +- .../processing/FactoryBeanElementCreator.java | 6 +- .../inject/SharedInjectionSpec.groovy | 50 ++ 21 files changed, 878 insertions(+), 762 deletions(-) create mode 100644 inject/src/main/java/io/micronaut/inject/ast/utils/EnclosedElementsQuery.java create mode 100644 test-suite/src/test/groovy/io/micronaut/inject/SharedInjectionSpec.groovy diff --git a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/TypeElementVisitorTransform.groovy b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/TypeElementVisitorTransform.groovy index 6dbfad35078..afc2a929536 100644 --- a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/TypeElementVisitorTransform.groovy +++ b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/TypeElementVisitorTransform.groovy @@ -17,23 +17,23 @@ package io.micronaut.ast.groovy import groovy.transform.CompilationUnitAware import groovy.transform.CompileStatic -import groovy.transform.PackageScope import io.micronaut.ast.groovy.visitor.GroovyClassElement import io.micronaut.ast.groovy.visitor.GroovyVisitorContext import io.micronaut.ast.groovy.visitor.LoadedVisitor import io.micronaut.core.annotation.Generated import io.micronaut.core.order.OrderUtil -import io.micronaut.inject.processing.ProcessingException import io.micronaut.inject.ast.ClassElement import io.micronaut.inject.ast.ElementQuery +import io.micronaut.inject.ast.EnumConstantElement +import io.micronaut.inject.ast.FieldElement +import io.micronaut.inject.ast.MemberElement import io.micronaut.inject.ast.MethodElement import io.micronaut.inject.ast.PropertyElement +import io.micronaut.inject.processing.ProcessingException import io.micronaut.inject.writer.AbstractBeanDefinitionBuilder import org.codehaus.groovy.ast.ASTNode -import org.codehaus.groovy.ast.AnnotatedNode import org.codehaus.groovy.ast.ClassNode import org.codehaus.groovy.ast.ConstructorNode -import org.codehaus.groovy.ast.FieldNode import org.codehaus.groovy.ast.InnerClassNode import org.codehaus.groovy.ast.ModuleNode import org.codehaus.groovy.control.CompilationUnit @@ -43,8 +43,6 @@ import org.codehaus.groovy.transform.ASTTransformation import org.codehaus.groovy.transform.GroovyASTTransformation import java.lang.reflect.Modifier - -import static org.codehaus.groovy.ast.ClassHelper.makeCached /** * Executes type element visitors. * @@ -132,10 +130,6 @@ class TypeElementVisitorTransform implements ASTTransformation, CompilationUnitA this.visitorContext = visitorContext } - protected boolean isPackagePrivate(AnnotatedNode annotatedNode, int modifiers) { - return ((!Modifier.isProtected(modifiers) && !Modifier.isPublic(modifiers) && !Modifier.isPrivate(modifiers)) || !annotatedNode.getAnnotations(makeCached(PackageScope)).isEmpty()) - } - void visitClass(ClassNode node) { if (targetClassElement.getNativeType() != node) { targetClassElement = visitorContext.getElementFactory().newSourceClassElement(node, visitorContext.getElementAnnotationMetadataFactory()) @@ -146,21 +140,21 @@ class TypeElementVisitorTransform implements ASTTransformation, CompilationUnitA } } GroovyClassElement classElement = targetClassElement as GroovyClassElement - // Pre cache methods because of their source flag - def methods = classElement.getSourceEnclosedElements(ElementQuery.ALL_METHODS) - def properties = classElement.getSyntheticBeanProperties() for (PropertyElement pn : (properties)) { visitNativeProperty(pn) } - for (FieldNode fn : node.getFields()) { - visitField(fn) - } for (ConstructorNode cn : node.getDeclaredConstructors()) { visitConstructor(cn) } - for (MethodElement methodElement : methods) { - visitMethod(methodElement) + for (MemberElement memberElement : classElement.getSourceEnclosedElements(ElementQuery.ALL_FIELD_AND_METHODS)) { + if (memberElement instanceof FieldElement) { + visitField(memberElement) + } else if (memberElement instanceof MethodElement) { + visitMethod(memberElement) + } else { + throw new IllegalStateException("Unknown element: " + memberElement) + } } } @@ -182,29 +176,18 @@ class TypeElementVisitorTransform implements ASTTransformation, CompilationUnitA } } - void visitField(FieldNode fieldNode) { - if (fieldNode.name == 'metaClass') return - int modifiers = fieldNode.modifiers - if (Modifier.isFinal(modifiers) || Modifier.isStatic(modifiers)) { - return - } - if (fieldNode.isSynthetic() && !isPackagePrivate(fieldNode, fieldNode.modifiers)) { - return - } - if (fieldNode.enum) { - def e = visitorContext.getElementFactory() - .newEnumConstantElement(targetClassElement, fieldNode, visitorContext.getElementAnnotationMetadataFactory()) + void visitField(FieldElement fieldElement) { + if (fieldElement instanceof EnumConstantElement) { + EnumConstantElement enumConstantElement = fieldElement for (LoadedVisitor it : typeElementVisitors) { - if (it.matchesElement(e)) { - it.getVisitor().visitEnumConstant(e, visitorContext) + if (it.matchesElement(enumConstantElement)) { + it.getVisitor().visitEnumConstant(enumConstantElement, visitorContext) } } } else { - def e = visitorContext.getElementFactory() - .newFieldElement(targetClassElement, fieldNode, visitorContext.getElementAnnotationMetadataFactory()) for (LoadedVisitor it : typeElementVisitors) { - if (it.matchesElement(e)) { - it.getVisitor().visitField(e, visitorContext) + if (it.matchesElement(fieldElement)) { + it.getVisitor().visitField(fieldElement, visitorContext) } } } diff --git a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyClassElement.java b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyClassElement.java index f62f672569e..ee3bacd2a9c 100644 --- a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyClassElement.java +++ b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyClassElement.java @@ -41,12 +41,14 @@ import io.micronaut.inject.ast.ElementQuery; import io.micronaut.inject.ast.FieldElement; import io.micronaut.inject.ast.GenericPlaceholderElement; +import io.micronaut.inject.ast.MemberElement; import io.micronaut.inject.ast.MethodElement; import io.micronaut.inject.ast.PackageElement; import io.micronaut.inject.ast.ParameterElement; import io.micronaut.inject.ast.PrimitiveElement; import io.micronaut.inject.ast.PropertyElement; import io.micronaut.inject.ast.utils.AstBeanPropertiesUtils; +import io.micronaut.inject.ast.utils.EnclosedElementsQuery; import org.codehaus.groovy.ast.AnnotatedNode; import org.codehaus.groovy.ast.ClassHelper; import org.codehaus.groovy.ast.ClassNode; @@ -65,9 +67,7 @@ import java.util.Arrays; import java.util.Collection; import java.util.Collections; -import java.util.HashMap; import java.util.HashSet; -import java.util.Iterator; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; @@ -75,19 +75,19 @@ import java.util.Objects; import java.util.Optional; import java.util.Set; +import java.util.Spliterator; +import java.util.Spliterators; import java.util.concurrent.atomic.AtomicReference; import java.util.function.Predicate; import java.util.stream.Collectors; import java.util.stream.Stream; - -import static groovyjarjarasm.asm.Opcodes.ACC_PRIVATE; -import static groovyjarjarasm.asm.Opcodes.ACC_PROTECTED; -import static groovyjarjarasm.asm.Opcodes.ACC_PUBLIC; +import java.util.stream.StreamSupport; /** * A class element returning data from a {@link ClassNode}. * * @author James Kleeh + * @author Denis Stepanov * @since 1.0 */ @Internal @@ -122,8 +122,9 @@ public class GroovyClassElement extends AbstractGroovyElement implements Arrayab private Map> genericInfo; private List properties; private List nativeProperties; - private Map elementsMap = new HashMap<>(); private Map resolvedTypeArguments; + private final GroovyEnclosedElementsQuery groovyEnclosedElementsQuery = new GroovyEnclosedElementsQuery(false); + private final GroovyEnclosedElementsQuery groovySourceEnclosedElementsQuery = new GroovyEnclosedElementsQuery(true); /** * @param visitorContext The visitor context @@ -207,7 +208,7 @@ public boolean isTypeVariable() { @Override public List getEnclosedElements(@NonNull ElementQuery query) { - return getEnclosedElements(query, false); + return groovyEnclosedElementsQuery.getEnclosedElements(this, query); } /** @@ -216,333 +217,11 @@ public List getEnclosedElements(@NonNull ElementQuery * {@link io.micronaut.inject.ast.ElementFactory#newSourceMethodElement(ClassElement, Object, ElementAnnotationMetadataFactory)}. * * @param query The query - * @param The element type + * @param The element type * @return The list of elements */ public final List getSourceEnclosedElements(@NonNull ElementQuery query) { - return getEnclosedElements(query, true); - } - - private List getEnclosedElements(@NonNull ElementQuery query, boolean isSource) { - Objects.requireNonNull(query, "Query cannot be null"); - ElementQuery.Result result = query.result(); - boolean onlyDeclared = result.isOnlyDeclared(); - boolean onlyAccessible = result.isOnlyAccessible(); - boolean onlyAbstract = result.isOnlyAbstract(); - boolean onlyConcrete = result.isOnlyConcrete(); - boolean onlyInstance = result.isOnlyInstance(); - boolean onlyStatic = result.isOnlyStatic(); - boolean excludePropertyElements = result.isExcludePropertyElements(); - Set excludeMethodNodes; - Set excludeFieldNodes; - if (excludePropertyElements) { - excludeMethodNodes = new HashSet<>(); - excludeFieldNodes = new HashSet<>(); - for (PropertyElement excludePropertyElement : getBeanProperties()) { - excludePropertyElement.getReadMethod() - .filter(m -> !m.isSynthetic()) - .ifPresent(methodElement -> excludeMethodNodes.add((AnnotatedNode) methodElement.getNativeType())); - excludePropertyElement.getWriteMethod() - .filter(m -> !m.isSynthetic()) - .ifPresent(methodElement -> excludeMethodNodes.add((AnnotatedNode) methodElement.getNativeType())); - excludePropertyElement.getField().ifPresent(fieldElement -> excludeFieldNodes.add((AnnotatedNode) fieldElement.getNativeType())); - } - } else { - excludeMethodNodes = Collections.emptySet(); - excludeFieldNodes = Collections.emptySet(); - } - - List> namePredicates = result.getNamePredicates(); - List> typePredicates = result.getTypePredicates(); - List> annotationPredicates = result.getAnnotationPredicates(); - List> elementPredicates = result.getElementPredicates(); - List>> modifierPredicates = result.getModifierPredicates(); - List elements; - Class elementType = result.getElementType(); - if (elementType == MethodElement.class) { - Predicate methodNodePredicate = methodNode -> { - for (Predicate predicate : namePredicates) { - if (!predicate.test(methodNode.getName())) { - return false; - } - } - return !JUNK_METHOD_FILTER.test(methodNode); - }; - List methods; - if (onlyDeclared) { - methods = classNode.getMethods().stream().filter(methodNodePredicate).map(mn -> toMethodElement(mn, isSource)).collect(Collectors.toList()); - } else { - methods = new ArrayList<>(getAllMethods(classNode, methodNodePredicate, result.isIncludeOverriddenMethods(), isSource)); - } - - Iterator i = methods.iterator(); - while (i.hasNext()) { - MethodElement method = i.next(); - if (onlyAbstract && !method.isAbstract()) { - i.remove(); - continue; - } - if (onlyConcrete && method.isAbstract()) { - i.remove(); - continue; - } - if (onlyInstance && method.isStatic()) { - i.remove(); - continue; - } - if (onlyStatic && !method.isStatic()) { - i.remove(); - continue; - } - if (onlyAccessible) { - final ClassElement accessibleFromType = result.getOnlyAccessibleFromType().orElse(this); - if (!method.isAccessible(accessibleFromType)) { - i.remove(); - continue; - } - } - if (!modifierPredicates.isEmpty()) { - Set elementModifiers = method.getModifiers(); - if (!modifierPredicates.stream().allMatch(p -> p.test(elementModifiers))) { - i.remove(); - continue; - } - } - if (excludeMethodNodes.contains(method.getNativeType())) { - i.remove(); - } - } - if (!typePredicates.isEmpty()) { - methods.removeIf(e -> !typePredicates.stream().allMatch(p -> p.test(e.getGenericReturnType()))); - } - elements = (List) methods; - } else if (elementType == ConstructorElement.class) { - List constructors = new ArrayList<>(classNode.getDeclaredConstructors()); - if (!onlyDeclared) { - ClassNode superClass = classNode.getSuperClass(); - while (superClass != null) { - // don't include constructors on enum, record... – matches behavior of JavaClassElement - if (superClass.getPackageName().equals("java.lang")) { - break; - } - constructors.addAll(superClass.getDeclaredConstructors()); - superClass = superClass.getSuperClass(); - } - } - for (Iterator i = constructors.iterator(); i.hasNext(); ) { - ConstructorNode constructor = i.next(); - // we don't listen to the user here, we never return static initializers. This matches behavior of JavaClassElement - if (constructor.isStatic()) { - i.remove(); - continue; - } - if (onlyAccessible) { - final ClassElement accessibleFromType = result.getOnlyAccessibleFromType().orElse(this); - if (constructor.isPrivate()) { - i.remove(); - continue; - } else if (!constructor.getDeclaringClass().getName().equals(accessibleFromType.getName())) { - // inaccessible through package scope - if (constructor.isPackageScope() && !constructor.getDeclaringClass().getPackageName().equals(accessibleFromType.getPackageName())) { - i.remove(); - continue; - } - } - } - if (!modifierPredicates.isEmpty()) { - Set elementModifiers = resolveModifiers(constructor); - if (!modifierPredicates.stream().allMatch(p -> p.test(elementModifiers))) { - i.remove(); - } - } - } - - //noinspection unchecked - elements = constructors.stream() - .map(constructorNode -> (T) asConstructor(constructorNode)) - .collect(Collectors.toList()); - } else if (elementType == FieldElement.class) { - List fields; - if (onlyDeclared) { - List initialFields = classNode.getFields(); - fields = findRelevantFields(onlyAccessible, result.getOnlyAccessibleFromType().orElse(this), initialFields, namePredicates, modifierPredicates); - } else { - fields = new ArrayList<>(classNode.getFields()); - ClassNode superClass = classNode.getSuperClass(); - while (superClass != null && !superClass.equals(ClassHelper.OBJECT_TYPE)) { - fields.addAll(superClass.getFields()); - superClass = superClass.getSuperClass(); - } - fields = findRelevantFields(onlyAccessible, result.getOnlyAccessibleFromType().orElse(this), fields, namePredicates, modifierPredicates); - } - Stream fieldStream = fields.stream().filter(f -> !excludeFieldNodes.contains(f)); - if (onlyInstance) { - fieldStream = fieldStream.filter((fn) -> !fn.isStatic()); - } else if (onlyStatic) { - fieldStream = fieldStream.filter(FieldNode::isStatic); - } - elements = fieldStream - .map(fieldNode -> (T) elementsMap.computeIfAbsent(fieldNode, annotatedNode -> visitorContext.getElementFactory().newFieldElement(this, fieldNode, elementAnnotationMetadataFactory))) - .collect(Collectors.toList()); - if (!typePredicates.isEmpty()) { - elements.removeIf(e -> !typePredicates.stream().allMatch(p -> p.test(((FieldElement) e).getGenericField()))); - } - } else if (elementType == ClassElement.class) { - Iterator i = classNode.getInnerClasses(); - List innerClasses = new ArrayList<>(); - while (i.hasNext()) { - InnerClassNode innerClassNode = i.next(); - if (onlyAbstract && !innerClassNode.isAbstract()) { - continue; - } - if (onlyConcrete && innerClassNode.isAbstract()) { - continue; - } - if (onlyAccessible) { - if (Modifier.isPrivate(innerClassNode.getModifiers())) { - continue; - } - } - if (!modifierPredicates.isEmpty()) { - Set elementModifiers = resolveModifiers(innerClassNode); - if (!modifierPredicates.stream().allMatch(p -> p.test(elementModifiers))) { - continue; - } - } - if (!namePredicates.isEmpty()) { - if (!namePredicates.stream().allMatch(p -> p.test(innerClassNode.getName()))) { - continue; - } - } - ClassElement classElement = visitorContext.getElementFactory().newClassElement(innerClassNode, elementAnnotationMetadataFactory); - if (!typePredicates.isEmpty()) { - if (!typePredicates.stream().allMatch(p -> p.test(classElement))) { - continue; - } - } - - innerClasses.add((T) classElement); - } - elements = innerClasses; - } else { - elements = Collections.emptyList(); - } - if (!elements.isEmpty()) { - if (!annotationPredicates.isEmpty()) { - elements.removeIf(e -> !annotationPredicates.stream().allMatch(p -> p.test(e.getAnnotationMetadata()))); - } - if (!elements.isEmpty() && !elementPredicates.isEmpty()) { - elements.removeIf(e -> !elementPredicates.stream().allMatch(p -> p.test(e))); - } - } - return elements; - } - - private List findRelevantFields( - boolean onlyAccessible, - ClassElement onlyAccessibleType, - List initialFields, - List> namePredicates, - List>> modifierPredicates) { - List filteredFields = new ArrayList<>(initialFields.size()); - - elementLoop: - for (FieldNode fn : initialFields) { - if (JUNK_FIELD_FILTER.test(fn)) { - continue; - } - if (onlyAccessible && fn.isPrivate()) { - continue; - } else if (onlyAccessible && isPackageScope(fn)) { - if (!fn.getDeclaringClass().getPackageName().equals(onlyAccessibleType.getPackageName())) { - continue; - } - } - if (!modifierPredicates.isEmpty()) { - final Set elementModifiers = resolveModifiers(fn); - for (Predicate> modifierPredicate : modifierPredicates) { - if (!modifierPredicate.test(elementModifiers)) { - continue elementLoop; - } - } - } - if (!namePredicates.isEmpty()) { - String name = fn.getName(); - for (Predicate namePredicate : namePredicates) { - if (!namePredicate.test(name)) { - continue elementLoop; - } - } - } - filteredFields.add(fn); - } - return filteredFields; - } - - private Collection getAllMethods(ClassNode classNode, - Predicate methodNodePredicate, - boolean includeOverriddenMethods, - boolean isSource) { - // This method will return private/package private methods that - // cannot be overridden by defining a method with the same signature - Set methods = new LinkedHashSet<>(); - Map methodElements = new HashMap<>(); - List> hierarchy = new ArrayList<>(); - collectHierarchyMethods(classNode, methodNodePredicate, hierarchy); - for (List classMethods : hierarchy) { - Set addedFromClassMethods = new LinkedHashSet<>(); - for (MethodNode methodNode : classMethods) { - MethodElement newMethod = methodElements.computeIfAbsent(methodNode, mn -> toMethodElement(mn, isSource)); - for (Iterator iterator = methods.iterator(); iterator.hasNext(); ) { - MethodElement existingMethod = iterator.next(); - if (!includeOverriddenMethods && newMethod.overrides(existingMethod)) { - iterator.remove(); - addedFromClassMethods.add(newMethod); - } - } - addedFromClassMethods.add(newMethod); - } - methods.addAll(addedFromClassMethods); - } - return methods; - } - - private static void collectHierarchyMethods(ClassNode classNode, - Predicate methodNodePredicate, - List> hierarchy) { - if (Object.class.getName().equals(classNode.getName()) - || Enum.class.getName().equals(classNode.getName()) - || GroovyObjectSupport.class.getName().equals(classNode.getName()) - || Script.class.getName().equals(classNode.getName())) { - return; - } - ClassNode parent = classNode.getSuperClass(); - if (parent != null) { - collectHierarchyMethods(parent, methodNodePredicate, hierarchy); - } - for (ClassNode iface : classNode.getInterfaces()) { - if (iface.getName().equals(GroovyObject.class.getName())) { - continue; - } - List> interfaceMethods = new ArrayList<>(); - collectHierarchyMethods(iface, methodNodePredicate, interfaceMethods); - interfaceMethods.forEach(methodNodes -> methodNodes.removeIf(methodNode -> (methodNode.getModifiers() & Opcodes.ACC_SYNTHETIC) != 0)); - hierarchy.addAll(interfaceMethods); - } - hierarchy.add(classNode.getMethods().stream().filter(methodNodePredicate).collect(Collectors.toList())); - } - - private GroovyMethodElement toMethodElement(MethodNode methodNode, boolean isSource) { - return (GroovyMethodElement) elementsMap.computeIfAbsent(methodNode, annotatedNode -> { - if (isSource) { - return visitorContext.getElementFactory().newSourceMethodElement(this, methodNode, elementAnnotationMetadataFactory); - } - return visitorContext.getElementFactory().newMethodElement(this, methodNode, elementAnnotationMetadataFactory); - }); - } - - private boolean isPackageScope(FieldNode fn) { - return (fn.getModifiers() & (ACC_PUBLIC | ACC_PRIVATE | ACC_PROTECTED)) == 0; + return groovySourceEnclosedElementsQuery.getEnclosedElements(this, query); } @Override @@ -811,8 +490,8 @@ public List getSyntheticBeanProperties() { Set nativeProps = getPropertyNodes().stream().map(PropertyNode::getName).collect(Collectors.toCollection(LinkedHashSet::new)); nativeProperties = AstBeanPropertiesUtils.resolveBeanProperties(configuration, this, - () -> AstBeanPropertiesUtils.getSubtypeFirstMethods(this), - () -> AstBeanPropertiesUtils.getSubtypeFirstFields(this), + () -> getEnclosedElements(ElementQuery.ALL_METHODS.onlyInstance()), + () -> getPropertyNodes().stream().map(propertyNode -> visitorContext.getElementFactory().newFieldElement(this, propertyNode.getField(), elementAnnotationMetadataFactory)).collect(Collectors.toList()), true, nativeProps, methodElement -> Optional.empty(), @@ -827,8 +506,8 @@ public List getBeanProperties(BeanPropertiesQuery beanPropertie Set nativeProps = getPropertyNodes().stream().map(PropertyNode::getName).collect(Collectors.toCollection(LinkedHashSet::new)); return AstBeanPropertiesUtils.resolveBeanProperties(beanPropertiesQuery, this, - () -> AstBeanPropertiesUtils.getSubtypeFirstMethods(this), - () -> AstBeanPropertiesUtils.getSubtypeFirstFields(this), + () -> getEnclosedElements(ElementQuery.ALL_METHODS.onlyInstance()), + () -> getEnclosedElements(ElementQuery.ALL_FIELDS), true, nativeProps, methodElement -> Optional.empty(), @@ -1080,8 +759,7 @@ protected final ClassElement toClassElement(ClassNode classNode) { @NonNull @Override - public ClassElement withBoundGenericTypes(@NonNull List typeArguments) { + public ClassElement withBoundGenericTypes(@NonNull List typeArguments) { // we can't create a new ClassNode, so we have to go this route. GroovyClassElement copy = new GroovyClassElement(visitorContext, classNode, elementAnnotationMetadataFactory); copy.overrideBoundGenericTypes = typeArguments; @@ -1104,8 +782,158 @@ private List getPropertyNodes() { return propertyElements; } - private MethodElement asConstructor(ConstructorNode cn) { - return visitorContext.getElementFactory().newConstructorElement(this, cn, elementAnnotationMetadataFactory); + /** + * The groovy elements query helper. + */ + private final class GroovyEnclosedElementsQuery extends EnclosedElementsQuery { + + private final boolean isSource; + + private GroovyEnclosedElementsQuery(boolean isSource) { + this.isSource = isSource; + } + + @Override + protected Set getExcludedNativeElements(ElementQuery.Result result) { + if (result.isExcludePropertyElements()) { + Set excluded = new HashSet<>(); + for (PropertyElement excludePropertyElement : getBeanProperties()) { + excludePropertyElement.getReadMethod() + .filter(m -> !m.isSynthetic()) + .ifPresent(methodElement -> excluded.add((AnnotatedNode) methodElement.getNativeType())); + excludePropertyElement.getWriteMethod() + .filter(m -> !m.isSynthetic()) + .ifPresent(methodElement -> excluded.add((AnnotatedNode) methodElement.getNativeType())); + excludePropertyElement.getField().ifPresent(fieldElement -> excluded.add((AnnotatedNode) fieldElement.getNativeType())); + } + return excluded; + } + return super.getExcludedNativeElements(result); + } + + @Override + protected ClassNode getSuperClass(ClassNode classNode) { + return classNode.getSuperClass(); + } + + @Override + protected Collection getInterfaces(ClassNode classNode) { + return Arrays.stream(classNode.getInterfaces()) + .filter(interfaceNode -> !interfaceNode.getName().equals(GroovyObject.class.getName())) + .toList(); + } + + @Override + protected List getEnclosedElements(ClassNode classNode, + ElementQuery.Result result) { + Class elementType = result.getElementType(); + return getEnclosedElements(classNode, result, elementType); + } + + private List getEnclosedElements(ClassNode classNode, ElementQuery.Result result, Class elementType) { + if (elementType == MemberElement.class) { + return Stream.concat( + getEnclosedElements(classNode, result, FieldElement.class).stream(), + getEnclosedElements(classNode, result, MethodElement.class).stream() + ).toList(); + } else if (elementType == MethodElement.class) { + return classNode.getMethods() + .stream() + .filter(methodNode -> !JUNK_METHOD_FILTER.test(methodNode) && (methodNode.getModifiers() & Opcodes.ACC_SYNTHETIC) == 0) + .map(m -> m) + .toList(); + } else if (elementType == FieldElement.class) { + return classNode.getFields().stream() + .filter(fieldNode -> (!fieldNode.isEnum() || result.isIncludeEnumConstants()) && !JUNK_FIELD_FILTER.test(fieldNode) && (fieldNode.getModifiers() & Opcodes.ACC_SYNTHETIC) == 0) + .map(m -> m) + .toList(); + + } else if (elementType == ConstructorElement.class) { + return classNode.getDeclaredConstructors() + .stream() + .filter(methodNode -> !JUNK_METHOD_FILTER.test(methodNode) && (methodNode.getModifiers() & Opcodes.ACC_SYNTHETIC) == 0) + .map(m -> m) + .toList(); + } else if (elementType == ClassElement.class) { + return StreamSupport.stream( + Spliterators.spliteratorUnknownSize(classNode.getInnerClasses(), Spliterator.ORDERED), + false) + .filter(innerClassNode -> (innerClassNode.getModifiers() & Opcodes.ACC_SYNTHETIC) == 0) + .map(m -> m) + .toList(); + } else { + throw new IllegalStateException("Unknown result type: " + elementType); + } + } + + @Override + protected boolean excludeClass(ClassNode classNode) { + String packageName = Objects.requireNonNullElse(classNode.getPackageName(), ""); + if (packageName.startsWith("org.spockframework.lang") || packageName.startsWith("spock.mock") || packageName.startsWith("spock.lang")) { + // Performance optimization to exclude Spock;s deep hierarchy + return true; + } + String className = classNode.getName(); + return Object.class.getName().equals(className) + || Enum.class.getName().equals(className) + || GroovyObjectSupport.class.getName().equals(className) + || Script.class.getName().equals(className); + } + + @Override + protected Element toAstElement(AnnotatedNode enclosedElement) { + final GroovyElementFactory elementFactory = visitorContext.getElementFactory(); + if (isSource) { + if (!(enclosedElement instanceof ConstructorNode) && enclosedElement instanceof MethodNode methodNode) { + return elementFactory.newSourceMethodElement( + GroovyClassElement.this, + methodNode, + elementAnnotationMetadataFactory + ); + } + if (enclosedElement instanceof ClassNode cn) { + return elementFactory.newSourceClassElement( + cn, + elementAnnotationMetadataFactory + ); + } + } + if (enclosedElement instanceof ConstructorNode constructorNode) { + return elementFactory.newConstructorElement( + GroovyClassElement.this, + constructorNode, + elementAnnotationMetadataFactory + ); + } + if (enclosedElement instanceof MethodNode methodNode) { + return elementFactory.newMethodElement( + GroovyClassElement.this, + methodNode, + elementAnnotationMetadataFactory + ); + } + if (enclosedElement instanceof FieldNode fieldNode) { + if (fieldNode.isEnum()) { + return elementFactory.newEnumConstantElement( + GroovyClassElement.this, + fieldNode, + elementAnnotationMetadataFactory + ); + } + return elementFactory.newFieldElement( + GroovyClassElement.this, + fieldNode, + elementAnnotationMetadataFactory + ); + } + if (enclosedElement instanceof ClassNode cn) { + return elementFactory.newClassElement( + cn, + elementAnnotationMetadataFactory + ); + } + throw new IllegalStateException("Unknown element: " + enclosedElement); + } } } diff --git a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyEnumConstantElement.java b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyEnumConstantElement.java index 26e6d426af8..0d47d24e4df 100644 --- a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyEnumConstantElement.java +++ b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyEnumConstantElement.java @@ -22,7 +22,7 @@ import io.micronaut.inject.ast.ElementAnnotationMetadataFactory; import io.micronaut.inject.ast.ElementModifier; import io.micronaut.inject.ast.EnumConstantElement; -import io.micronaut.inject.ast.MemberElement; +import io.micronaut.inject.ast.FieldElement; import org.codehaus.groovy.ast.AnnotatedNode; import org.codehaus.groovy.ast.FieldNode; @@ -61,8 +61,8 @@ protected AbstractGroovyElement copyConstructor() { } @Override - public MemberElement withAnnotationMetadata(AnnotationMetadata annotationMetadata) { - return (MemberElement) super.withAnnotationMetadata(annotationMetadata); + public FieldElement withAnnotationMetadata(AnnotationMetadata annotationMetadata) { + return (FieldElement) super.withAnnotationMetadata(annotationMetadata); } @Override diff --git a/inject-groovy/src/test/groovy/io/micronaut/ast/groovy/visitor/GroovyDocumentationSpec.groovy b/inject-groovy/src/test/groovy/io/micronaut/ast/groovy/visitor/GroovyDocumentationSpec.groovy index 430bae9cc3c..70d2e7a07cd 100644 --- a/inject-groovy/src/test/groovy/io/micronaut/ast/groovy/visitor/GroovyDocumentationSpec.groovy +++ b/inject-groovy/src/test/groovy/io/micronaut/ast/groovy/visitor/GroovyDocumentationSpec.groovy @@ -43,7 +43,7 @@ class Test extends SuperClass { expect: classElement.getDocumentation().get() == 'This is class level docs' - classElement.getFields().get(0).getDocumentation().get() == 'This is property level docs' + classElement.getFields().find {it.name == "tenant" }.getDocumentation().get() == 'This is property level docs' classElement.getEnclosedElements(ElementQuery.of(MethodElement.class).named("getTenant")).get(0).getDocumentation().get() == 'This is method level docs' classElement.getEnclosedElements(ElementQuery.of(MethodElement.class).named("setTenant")).get(0).getDocumentation().get() == 'This is method level docs' } diff --git a/inject-groovy/src/test/groovy/io/micronaut/inject/visitor/ClassElementSpec.groovy b/inject-groovy/src/test/groovy/io/micronaut/inject/visitor/ClassElementSpec.groovy index 0cc022f5dd6..ea4190c06f4 100644 --- a/inject-groovy/src/test/groovy/io/micronaut/inject/visitor/ClassElementSpec.groovy +++ b/inject-groovy/src/test/groovy/io/micronaut/inject/visitor/ClassElementSpec.groovy @@ -15,8 +15,8 @@ */ package io.micronaut.inject.visitor -import io.micronaut.ast.transform.test.AbstractBeanDefinitionSpec import io.micronaut.ast.groovy.TypeElementVisitorStart +import io.micronaut.ast.transform.test.AbstractBeanDefinitionSpec import io.micronaut.context.exceptions.BeanContextException import io.micronaut.inject.ast.ClassElement import io.micronaut.inject.ast.ElementModifier @@ -363,7 +363,9 @@ interface AnotherInterface { void "test find matching methods using ElementQuery"() { given: ClassElement classElement = buildClassElement(''' -package elementquery; +package elementquery + +import groovy.transform.PackageScope; class Test extends SuperType implements AnotherInterface, SomeInt { @@ -388,7 +390,8 @@ class Test extends SuperType implements AnotherInterface, SomeInt { } class SuperType { - protected boolean s1; + @PackageScope + boolean s1; private boolean s2; private boolean privateMethod() { return true; @@ -415,15 +418,15 @@ interface AnotherInterface { boolean publicMethod(); } ''') - when:"all methods are retrieved" + when: "all methods are retrieved" def allMethods = classElement.getEnclosedElements(ElementQuery.ALL_METHODS) - then:"All methods, including non-accessible are returned but not overridden" + then: "All methods, including non-accessible are returned but not overridden" allMethods.size() == 7 - allMethods.find { it.name == 'publicMethod'}.declaringType.simpleName == 'Test' - allMethods.find { it.name == 'otherSuper'}.declaringType.simpleName == 'SuperType' + allMethods.find { it.name == 'publicMethod' }.declaringType.simpleName == 'Test' + allMethods.find { it.name == 'otherSuper' }.declaringType.simpleName == 'SuperType' - when:"obtaining only the declared methods" + when: "obtaining only the declared methods" def declared = classElement.getEnclosedElements(ElementQuery.of(MethodElement).onlyDeclared()) then:"The declared are correct" @@ -431,31 +434,32 @@ interface AnotherInterface { // part of the methods declared by classNode.getMethods() and there is no way to distinguish them declared*.name as Set == ['privateMethod', 'packagePrivateMethod', 'publicMethod', 'staticMethod', 'itfeMethod'] as Set - when:"Accessible methods are retrieved" + when: "Accessible methods are retrieved" def accessible = classElement.getEnclosedElements(ElementQuery.of(MethodElement).onlyAccessible()) - then:"Only accessible methods, excluding those that require reflection" + then: "Only accessible methods, excluding those that require reflection" + accessible.size() == 5 accessible*.name as Set == ['otherSuper', 'itfeMethod', 'publicMethod', 'packagePrivateMethod', 'staticMethod'] as Set - when:"static methods are resolved" + when: "static methods are resolved" def staticMethods = classElement.getEnclosedElements(ElementQuery.ALL_METHODS.modifiers({ it.contains(ElementModifier.STATIC) })) - then:"We only get statics" + then: "We only get statics" staticMethods.size() == 1 staticMethods.first().name == 'staticMethod' - when:"All fields are retrieved" + when: "All fields are retrieved" def allFields = classElement.getEnclosedElements(ElementQuery.ALL_FIELDS) - then:"we get everything" + then: "we get everything" allFields.size() == 4 - when:"Accessible fields are retrieved" + when: "Accessible fields are retrieved" def accessibleFields = classElement.getEnclosedElements(ElementQuery.ALL_FIELDS.onlyAccessible()) - then:"we get everything" + then: "we get everything" accessibleFields.size() == 2 accessibleFields*.name as Set == ['s1', 't1'] as Set } @@ -737,9 +741,6 @@ enum Test { when: List allFields = classElement.getEnclosedElements(ElementQuery.ALL_FIELDS) List expected = [ - 'A', - 'B', - 'C', 'publicStaticFinalField', 'publicStaticField', 'publicFinalField', @@ -756,10 +757,8 @@ enum Test { 'privateStaticField', 'privateFinalField', 'privateField', - 'MIN_VALUE', - 'MAX_VALUE', - 'name', - 'ordinal', + 'MIN_VALUE', // Extra field in Groovy + 'MAX_VALUE' // Extra field in Groovy ] then: @@ -770,6 +769,7 @@ enum Test { when: allFields = classElement.getEnclosedElements(ElementQuery.ALL_FIELDS.includeEnumConstants()) + expected = ['A', 'B', 'C'] + expected then: for (String name : allFields*.name) { @@ -864,4 +864,68 @@ class MyBean implements MyInt { allMethods.get(0).isAbstract() == false allMethods.get(0).isDefault() == false } + + void "test synthetic properties aren't removed"() { + given: + ClassElement classElement = buildClassElement('elementquery.SuccessfulTest', ''' +package elementquery + +import io.micronaut.context.ApplicationContext; +import spock.lang.Shared +import spock.lang.Specification + +import jakarta.inject.Inject + +class AbstractExample extends Specification { + + @Inject + @Shared + ApplicationContext sharedCtx + + @Inject + ApplicationContext ctx + +} + +class FailingTest extends AbstractExample { + + def 'injection is not null'() { + expect: + ctx != null + } + + def 'shared injection is not null'() { + expect: + sharedCtx != null + } +} + +class SuccessfulTest extends AbstractExample { + + @Shared + @Inject + ApplicationContext dummy + + def 'injection is not null'() { + expect: + ctx != null + } + + def 'shared injection is not null'() { + expect: + sharedCtx != null + } +} + + +''') + when: + def props = classElement.getSyntheticBeanProperties() + def allFields = classElement.getEnclosedElements(ElementQuery.ALL_FIELDS) + then: + props.size() == 3 + props[0].name == "ctx" + props[1].name.contains "dummy" + props[2].name.contains "sharedCtx" + } } diff --git a/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaClassElement.java b/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaClassElement.java index 04227a7a96a..0fd21dbf9a7 100644 --- a/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaClassElement.java +++ b/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaClassElement.java @@ -29,7 +29,6 @@ import io.micronaut.inject.ast.ClassElement; import io.micronaut.inject.ast.ConstructorElement; import io.micronaut.inject.ast.ElementAnnotationMetadataFactory; -import io.micronaut.inject.ast.ElementModifier; import io.micronaut.inject.ast.ElementQuery; import io.micronaut.inject.ast.FieldElement; import io.micronaut.inject.ast.GenericPlaceholderElement; @@ -39,6 +38,7 @@ import io.micronaut.inject.ast.PropertyElement; import io.micronaut.inject.ast.WildcardElement; import io.micronaut.inject.ast.utils.AstBeanPropertiesUtils; +import io.micronaut.inject.ast.utils.EnclosedElementsQuery; import io.micronaut.inject.processing.JavaModelUtils; import javax.lang.model.element.Element; @@ -51,24 +51,19 @@ import javax.lang.model.type.DeclaredType; import javax.lang.model.type.TypeMirror; import javax.lang.model.type.TypeVariable; -import javax.lang.model.util.Elements; import javax.lang.model.util.Types; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; -import java.util.HashMap; +import java.util.EnumSet; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedHashMap; -import java.util.LinkedHashSet; import java.util.List; import java.util.Map; -import java.util.Objects; import java.util.Optional; import java.util.Set; -import java.util.function.BiPredicate; import java.util.function.Function; -import java.util.function.Predicate; import java.util.stream.Collectors; /** @@ -76,6 +71,7 @@ * * @author James Kleeh * @author graemerocher + * @author Denis Stepanov * @since 1.0 */ @Internal @@ -88,12 +84,11 @@ public class JavaClassElement extends AbstractJavaElement implements ArrayableCl private final boolean isTypeVariable; private List beanProperties; private Map> genericTypeInfo; - private List enclosedElements; private String simpleName; private String name; private String packageName; - private final Map elementsCache = new HashMap<>(); private Map resolvedTypeArguments; + private final JavaEnclosedElementsQuery enclosedElementsQuery = new JavaEnclosedElementsQuery(); /** * @param classElement The {@link TypeElement} @@ -274,8 +269,7 @@ public Optional getSuperType() { if (superclass != null) { final Element element = visitorContext.getTypes().asElement(superclass); - if (element instanceof TypeElement) { - TypeElement superElement = (TypeElement) element; + if (element instanceof TypeElement superElement) { if (!Object.class.getName().equals(superElement.getQualifiedName().toString())) { // if super type has type arguments, then build a parameterized ClassElement if (superclass instanceof DeclaredType && !((DeclaredType) superclass).getTypeArguments().isEmpty()) { @@ -470,277 +464,10 @@ private boolean isKotlinClass(Element element) { @Override public List getEnclosedElements(@NonNull ElementQuery query) { - Objects.requireNonNull(query, "Query cannot be null"); - ElementQuery.Result result = query.result(); - Set excludeElements; - if (result.isExcludePropertyElements()) { - excludeElements = new HashSet<>(); - for (PropertyElement excludePropertyElement : getBeanProperties()) { - excludePropertyElement.getReadMethod().ifPresent(methodElement -> excludeElements.add((Element) methodElement.getNativeType())); - excludePropertyElement.getWriteMethod().ifPresent(methodElement -> excludeElements.add((Element) methodElement.getNativeType())); - excludePropertyElement.getField().ifPresent(fieldElement -> excludeElements.add((Element) fieldElement.getNativeType())); - } - } else { - excludeElements = Collections.emptySet(); - } - Elements elements = visitorContext.getElements(); - ElementKind kind = getElementKind(result.getElementType()); - Predicate predicate = element -> { - Element enclosingElement = element.getEnclosingElement(); - if (enclosingElement instanceof TypeElement - && ((TypeElement) enclosingElement).getQualifiedName().toString().equals(Enum.class.getName()) - && element.getKind() == ElementKind.FIELD) { - // Skip any fields on Enum but allow to query methods - return false; - } - ElementKind enclosedElementKind = element.getKind(); - return enclosedElementKind == kind - || result.isIncludeEnumConstants() && kind == ElementKind.FIELD && enclosedElementKind == ElementKind.ENUM_CONSTANT - || (enclosedElementKind == ElementKind.ENUM && kind == ElementKind.CLASS); - }; - - Predicate filter = element -> { - if (excludeElements.contains(element.getNativeType())) { - return false; - } - List> elementPredicates = result.getElementPredicates(); - if (!elementPredicates.isEmpty()) { - for (Predicate elementPredicate : elementPredicates) { - if (!elementPredicate.test((T) element)) { - return false; - } - } - } - if (element instanceof MethodElement) { - MethodElement methodElement = (MethodElement) element; - if (result.isOnlyAbstract()) { - if (methodElement.getDeclaringType().isInterface() && methodElement.isDefault()) { - return false; - } else if (!element.isAbstract()) { - return false; - } - } else if (result.isOnlyConcrete()) { - if (methodElement.getDeclaringType().isInterface() && !methodElement.isDefault()) { - return false; - } else if (element.isAbstract()) { - return false; - } - } - } - if (result.isOnlyInstance() && element.isStatic()) { - return false; - } else if (result.isOnlyStatic() && !element.isStatic()) { - return false; - } - if (result.isOnlyAccessible()) { - // exclude private members - // exclude synthetic members or bridge methods that start with $ - if (element.isPrivate() || element.getName().startsWith("$")) { - return false; - } - if (element instanceof MemberElement && !((MemberElement) element).isAccessible()) { - return false; - } - } - if (!result.getModifierPredicates().isEmpty()) { - Set modifiers = element.getModifiers(); - for (Predicate> modifierPredicate : result.getModifierPredicates()) { - if (!modifierPredicate.test(modifiers)) { - return false; - } - } - } - if (!result.getNamePredicates().isEmpty()) { - for (Predicate namePredicate : result.getNamePredicates()) { - if (!namePredicate.test(element.getName())) { - return false; - } - } - } - if (!result.getAnnotationPredicates().isEmpty()) { - for (Predicate annotationPredicate : result.getAnnotationPredicates()) { - if (!annotationPredicate.test(element)) { - return false; - } - } - } - if (!result.getTypePredicates().isEmpty()) { - for (Predicate typePredicate : result.getTypePredicates()) { - ClassElement classElement; - if (element instanceof ConstructorElement) { - classElement = JavaClassElement.this; - } else if (element instanceof MethodElement) { - classElement = ((MethodElement) element).getGenericReturnType(); - } else if (element instanceof ClassElement) { - classElement = (ClassElement) element; - } else { - classElement = ((FieldElement) element).getGenericField(); - } - if (!typePredicate.test(classElement)) { - return false; - } - } - } -// TODO: FIX only injected -// if (result.isOnlyInjected() && !element.hasDeclaredAnnotation(AnnotationUtil.INJECT)) { -// return false; -// } - return true; - }; - - BiPredicate reduce; - if (result.isIncludeHiddenElements() && result.isIncludeOverriddenMethods()) { - reduce = (t1, t2) -> false; - } else { - reduce = (newElement, existingElement) -> { - if (!result.isIncludeHiddenElements() && hidden(elements, newElement, existingElement)) { - return true; - } - if (!result.isIncludeOverriddenMethods()) { - if (kind == ElementKind.METHOD) { - if (newElement instanceof MethodElement && existingElement instanceof MethodElement) { - return ((MethodElement) newElement).overrides((MethodElement) existingElement); - } - } - } - return false; - }; + if (getNativeType() instanceof TypeElement) { + return enclosedElementsQuery.getEnclosedElements(this, query); } - return (List) Collections.unmodifiableList( - getAllElements(classElement, result.isOnlyDeclared(), predicate, reduce) - .stream() - .filter(filter) - .collect(Collectors.toList()) - ); - } - - private static boolean hidden(Elements elements, io.micronaut.inject.ast.Element newElement, io.micronaut.inject.ast.Element existingElement) { - if (newElement instanceof MemberElement) { - if (newElement.isStatic() && ((MemberElement) newElement).getDeclaringType().isInterface()) { - return false; - } - } - return elements.hides((Element) newElement.getNativeType(), (Element) existingElement.getNativeType()); - } - - private Collection getAllElements(TypeElement classNode, - boolean onlyDeclared, - Predicate predicate, - BiPredicate reduce) { - Set elements = new LinkedHashSet<>(); - List> hierarchy = new ArrayList<>(); - collectHierarchy(classNode, onlyDeclared, predicate, hierarchy); - for (List classElements : hierarchy) { - Set addedFromClassElements = new LinkedHashSet<>(); - classElements: - for (Element element : classElements) { - io.micronaut.inject.ast.Element newElement = elementsCache.computeIfAbsent(element, this::toAstElement); - for (Iterator iterator = elements.iterator(); iterator.hasNext(); ) { - io.micronaut.inject.ast.Element existingElement = iterator.next(); - if (newElement.equals(existingElement)) { - continue; - } - if (reduce.test(newElement, existingElement)) { - iterator.remove(); - addedFromClassElements.add(newElement); - } else if (reduce.test(existingElement, newElement)) { - continue classElements; - } - } - addedFromClassElements.add(newElement); - } - elements.addAll(addedFromClassElements); - } - return elements; - } - - private void collectHierarchy(TypeElement classNode, - boolean onlyDeclared, - Predicate predicate, - List> hierarchy) { - if (classNode.getQualifiedName().toString().equals(Object.class.getName()) - || classNode.getQualifiedName().toString().equals(Enum.class.getName())) { - return; - } - if (!onlyDeclared) { - TypeMirror superclass = classNode.getSuperclass(); - if (superclass instanceof DeclaredType) { - DeclaredType dt = (DeclaredType) superclass; - TypeElement element = (TypeElement) dt.asElement(); - collectHierarchy(element, false, predicate, hierarchy); - } - for (TypeMirror ifaceMirror : classNode.getInterfaces()) { - final Element ifaceEl = visitorContext.getTypes().asElement(ifaceMirror); - if (ifaceEl instanceof TypeElement) { - TypeElement iface = (TypeElement) ifaceEl; - List> interfaceElements = new ArrayList<>(); - collectHierarchy(iface, false, predicate, interfaceElements); - hierarchy.addAll(interfaceElements); - } - } - } - List enclosedElements; - if (classNode == classElement) { - if (this.enclosedElements == null) { - this.enclosedElements = classElement.getEnclosedElements(); - } - enclosedElements = this.enclosedElements; - } else { - enclosedElements = classNode.getEnclosedElements(); - } - hierarchy.add(enclosedElements.stream().filter(predicate).collect(Collectors.toList())); - } - - private io.micronaut.inject.ast.Element toAstElement(Element enclosedElement) { - final JavaElementFactory elementFactory = visitorContext.getElementFactory(); - switch (enclosedElement.getKind()) { - case METHOD: - return elementFactory.newMethodElement( - JavaClassElement.this, - (ExecutableElement) enclosedElement, - elementAnnotationMetadataFactory, - genericTypeInfo - ); - case FIELD: - return elementFactory.newFieldElement( - JavaClassElement.this, - (VariableElement) enclosedElement, - elementAnnotationMetadataFactory - ); - case ENUM_CONSTANT: - return elementFactory.newEnumConstantElement( - JavaClassElement.this, - (VariableElement) enclosedElement, - elementAnnotationMetadataFactory - ); - case CONSTRUCTOR: - return elementFactory.newConstructorElement( - JavaClassElement.this, - (ExecutableElement) enclosedElement, - elementAnnotationMetadataFactory - ); - case CLASS: - case ENUM: - return elementFactory.newClassElement( - (TypeElement) enclosedElement, - elementAnnotationMetadataFactory - ); - default: - return null; - } - } - - private ElementKind getElementKind(Class elementType) { - if (elementType == MethodElement.class) { - return ElementKind.METHOD; - } else if (elementType == FieldElement.class) { - return ElementKind.FIELD; - } else if (elementType == ConstructorElement.class) { - return ElementKind.CONSTRUCTOR; - } else if (elementType == ClassElement.class) { - return ElementKind.CLASS; - } - throw new IllegalArgumentException("Unsupported element type for query: " + elementType); + return Collections.emptyList(); } @Override @@ -791,8 +518,7 @@ public PackageElement getPackage() { while (enclosingElement != null && enclosingElement.getKind() != ElementKind.PACKAGE) { enclosingElement = enclosingElement.getEnclosingElement(); } - if (enclosingElement instanceof javax.lang.model.element.PackageElement) { - javax.lang.model.element.PackageElement packageElement = (javax.lang.model.element.PackageElement) enclosingElement; + if (enclosingElement instanceof javax.lang.model.element.PackageElement packageElement) { return new JavaPackageElement( packageElement, elementAnnotationMetadataFactory, @@ -876,8 +602,7 @@ public List getAccessibleStaticCreators() { public Optional getEnclosingType() { if (isInner()) { Element enclosingElement = this.classElement.getEnclosingElement(); - if (enclosingElement instanceof TypeElement) { - TypeElement typeElement = (TypeElement) enclosingElement; + if (enclosingElement instanceof TypeElement typeElement) { return Optional.of(visitorContext.getElementFactory().newClassElement( typeElement, elementAnnotationMetadataFactory @@ -1066,4 +791,126 @@ Map> getGenericTypeInfo() { return genericTypeInfo; } + private final class JavaEnclosedElementsQuery extends EnclosedElementsQuery { + + private List enclosedElements; + + @Override + protected Set getExcludedNativeElements(ElementQuery.Result result) { + if (result.isExcludePropertyElements()) { + Set excludeElements = new HashSet<>(); + for (PropertyElement excludePropertyElement : getBeanProperties()) { + excludePropertyElement.getReadMethod().ifPresent(methodElement -> excludeElements.add((Element) methodElement.getNativeType())); + excludePropertyElement.getWriteMethod().ifPresent(methodElement -> excludeElements.add((Element) methodElement.getNativeType())); + excludePropertyElement.getField().ifPresent(fieldElement -> excludeElements.add((Element) fieldElement.getNativeType())); + } + return excludeElements; + } + return Collections.emptySet(); + } + + @Override + protected TypeElement getSuperClass(TypeElement classNode) { + TypeMirror superclass = classNode.getSuperclass(); + if (superclass instanceof DeclaredType dt) { + Element element = dt.asElement(); + if (element instanceof TypeElement) { + return (TypeElement) element; + } + } + return null; + } + + @Override + protected Collection getInterfaces(TypeElement classNode) { + List interfaces = classNode.getInterfaces(); + Collection result = new ArrayList<>(interfaces.size()); + for (TypeMirror ifaceMirror : interfaces) { + final Element ifaceEl = visitorContext.getTypes().asElement(ifaceMirror); + if (ifaceEl instanceof TypeElement) { + result.add((TypeElement) ifaceEl); + } + } + return result; + } + + @Override + protected List getEnclosedElements(TypeElement classNode, ElementQuery.Result result) { + List ee; + if (classNode == classElement) { + ee = getEnclosedElements(); + } else { + ee = classNode.getEnclosedElements(); + } + EnumSet elementKinds = getElementKind(result); + return ee.stream().filter(element -> elementKinds.contains(element.getKind())).collect(Collectors.toList()); + } + + @Override + protected boolean excludeClass(TypeElement classNode) { + return classNode.getQualifiedName().toString().equals(Object.class.getName()) + || classNode.getQualifiedName().toString().equals(Enum.class.getName()); + } + + @Override + protected io.micronaut.inject.ast.Element toAstElement(Element enclosedElement) { + final JavaElementFactory elementFactory = visitorContext.getElementFactory(); + return switch (enclosedElement.getKind()) { + case METHOD -> elementFactory.newMethodElement( + JavaClassElement.this, + (ExecutableElement) enclosedElement, + elementAnnotationMetadataFactory, + genericTypeInfo + ); + case FIELD -> elementFactory.newFieldElement( + JavaClassElement.this, + (VariableElement) enclosedElement, + elementAnnotationMetadataFactory + ); + case ENUM_CONSTANT -> elementFactory.newEnumConstantElement( + JavaClassElement.this, + (VariableElement) enclosedElement, + elementAnnotationMetadataFactory + ); + case CONSTRUCTOR -> elementFactory.newConstructorElement( + JavaClassElement.this, + (ExecutableElement) enclosedElement, + elementAnnotationMetadataFactory + ); + case CLASS, ENUM -> elementFactory.newClassElement( + (TypeElement) enclosedElement, + elementAnnotationMetadataFactory + ); + default -> throw new IllegalStateException("Unknown element: " + enclosedElement); + }; + } + + private List getEnclosedElements() { + if (enclosedElements == null) { + enclosedElements = classElement.getEnclosedElements(); + } + return enclosedElements; + } + + private EnumSet getElementKind(ElementQuery.Result result) { + Class elementType = result.getElementType(); + if (elementType == MemberElement.class) { + return EnumSet.of(ElementKind.FIELD, ElementKind.METHOD); + } else if (elementType == MethodElement.class) { + return EnumSet.of(ElementKind.METHOD); + } else if (elementType == FieldElement.class) { + if (result.isIncludeEnumConstants()) { + return EnumSet.of(ElementKind.FIELD, ElementKind.ENUM_CONSTANT); + } + return EnumSet.of(ElementKind.FIELD); + } else if (elementType == ConstructorElement.class) { + return EnumSet.of(ElementKind.CONSTRUCTOR); + } else if (elementType == ClassElement.class) { + return EnumSet.of(ElementKind.CLASS, ElementKind.ENUM); + } + throw new IllegalArgumentException("Unsupported element type for query: " + elementType); + } + + } + } diff --git a/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaConstructorElement.java b/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaConstructorElement.java index 81d8450e3c4..13515068fd1 100644 --- a/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaConstructorElement.java +++ b/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaConstructorElement.java @@ -18,6 +18,7 @@ import io.micronaut.core.annotation.Internal; import io.micronaut.inject.ast.ConstructorElement; import io.micronaut.inject.ast.ElementAnnotationMetadataFactory; +import io.micronaut.inject.ast.MemberElement; import io.micronaut.inject.ast.MethodElement; import io.micronaut.inject.ast.ParameterElement; @@ -54,4 +55,14 @@ public ParameterElement[] getParameters() { } }; } + + @Override + public boolean overrides(MethodElement overridden) { + return false; + } + + @Override + public boolean hides(MemberElement hidden) { + return false; + } } diff --git a/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaEnumConstantElement.java b/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaEnumConstantElement.java index d52e5c8180b..025c3189f2a 100644 --- a/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaEnumConstantElement.java +++ b/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaEnumConstantElement.java @@ -21,7 +21,7 @@ import io.micronaut.inject.ast.ElementAnnotationMetadataFactory; import io.micronaut.inject.ast.ElementModifier; import io.micronaut.inject.ast.EnumConstantElement; -import io.micronaut.inject.ast.MemberElement; +import io.micronaut.inject.ast.FieldElement; import javax.lang.model.element.VariableElement; import java.util.Set; @@ -59,8 +59,8 @@ protected AbstractJavaElement copyThis() { } @Override - public MemberElement withAnnotationMetadata(AnnotationMetadata annotationMetadata) { - return (MemberElement) super.withAnnotationMetadata(annotationMetadata); + public FieldElement withAnnotationMetadata(AnnotationMetadata annotationMetadata) { + return (FieldElement) super.withAnnotationMetadata(annotationMetadata); } @Override diff --git a/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaFieldElement.java b/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaFieldElement.java index 2fd90445f3c..18fff9c8653 100644 --- a/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaFieldElement.java +++ b/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaFieldElement.java @@ -21,6 +21,7 @@ import io.micronaut.inject.ast.ClassElement; import io.micronaut.inject.ast.ElementAnnotationMetadataFactory; import io.micronaut.inject.ast.FieldElement; +import io.micronaut.inject.ast.MemberElement; import javax.lang.model.element.Element; import javax.lang.model.element.TypeElement; @@ -138,6 +139,14 @@ public ClassElement getDeclaringType() { return resolvedDeclaringClass; } + @Override + public boolean hides(MemberElement hidden) { + if (isStatic() && getDeclaringType().isInterface()) { + return false; + } + return visitorContext.getElements().hides((Element) getNativeType(), (Element) hidden.getNativeType()); + } + @Override public ClassElement getOwningType() { return owningType; diff --git a/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaMethodElement.java b/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaMethodElement.java index 8bf8f8eade9..9bd98f513ef 100644 --- a/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaMethodElement.java +++ b/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaMethodElement.java @@ -23,6 +23,7 @@ import io.micronaut.inject.ast.ClassElement; import io.micronaut.inject.ast.ElementAnnotationMetadataFactory; import io.micronaut.inject.ast.GenericPlaceholderElement; +import io.micronaut.inject.ast.MemberElement; import io.micronaut.inject.ast.MethodElement; import io.micronaut.inject.ast.ParameterElement; import io.micronaut.inject.ast.PrimitiveElement; @@ -151,6 +152,14 @@ public boolean overrides(MethodElement overridden) { return MethodElement.super.overrides(overridden); } + @Override + public boolean hides(MemberElement hidden) { + if (isStatic() && getDeclaringType().isInterface()) { + return false; + } + return visitorContext.getElements().hides((Element) getNativeType(), (Element) hidden.getNativeType()); + } + @NonNull @Override public ClassElement getGenericReturnType() { diff --git a/inject/src/main/java/io/micronaut/inject/ast/ConstructorElement.java b/inject/src/main/java/io/micronaut/inject/ast/ConstructorElement.java index 9506f3c8b9e..9fa039961e4 100644 --- a/inject/src/main/java/io/micronaut/inject/ast/ConstructorElement.java +++ b/inject/src/main/java/io/micronaut/inject/ast/ConstructorElement.java @@ -35,4 +35,14 @@ default String getName() { default ClassElement getReturnType() { return getDeclaringType(); } + + @Override + default boolean hides(MemberElement memberElement) { + return false; + } + + @Override + default boolean overrides(MethodElement overridden) { + return false; + } } diff --git a/inject/src/main/java/io/micronaut/inject/ast/ElementQuery.java b/inject/src/main/java/io/micronaut/inject/ast/ElementQuery.java index 18c44ec65d2..532519bbe29 100644 --- a/inject/src/main/java/io/micronaut/inject/ast/ElementQuery.java +++ b/inject/src/main/java/io/micronaut/inject/ast/ElementQuery.java @@ -51,6 +51,13 @@ public interface ElementQuery { */ ElementQuery ALL_METHODS = ElementQuery.of(MethodElement.class); + /** + * Constant to retrieve all methods and fields. + * + * @since 4.0.0 + */ + ElementQuery ALL_FIELD_AND_METHODS = ElementQuery.of(MemberElement.class); + /** * Constant to retrieve instance constructors, not including those of the parent class. */ diff --git a/inject/src/main/java/io/micronaut/inject/ast/EnumConstantElement.java b/inject/src/main/java/io/micronaut/inject/ast/EnumConstantElement.java index ac3896413b4..2a408d83213 100644 --- a/inject/src/main/java/io/micronaut/inject/ast/EnumConstantElement.java +++ b/inject/src/main/java/io/micronaut/inject/ast/EnumConstantElement.java @@ -23,9 +23,9 @@ * * @since 3.6.0 */ -public interface EnumConstantElement extends TypedElement, MemberElement { +public interface EnumConstantElement extends FieldElement { - Set ENUM_CONSTANT_MODIFIERS = new HashSet() {{ + Set ENUM_CONSTANT_MODIFIERS = new HashSet<>() {{ add(ElementModifier.PUBLIC); add(ElementModifier.STATIC); add(ElementModifier.FINAL); diff --git a/inject/src/main/java/io/micronaut/inject/ast/FieldElement.java b/inject/src/main/java/io/micronaut/inject/ast/FieldElement.java index 28b244291af..8689aaeffc3 100644 --- a/inject/src/main/java/io/micronaut/inject/ast/FieldElement.java +++ b/inject/src/main/java/io/micronaut/inject/ast/FieldElement.java @@ -25,8 +25,10 @@ * @since 1.0 */ public interface FieldElement extends TypedElement, MemberElement { + /** * Obtain the generic type with the associated annotation metadata for the field. + * * @return The generic field */ default ClassElement getGenericField() { @@ -47,4 +49,21 @@ default String getDescription(boolean simple) { default FieldElement withAnnotationMetadata(AnnotationMetadata annotationMetadata) { return (FieldElement) MemberElement.super.withAnnotationMetadata(annotationMetadata); } + + @Override + default boolean hides(@NonNull MemberElement memberElement) { + if (memberElement instanceof FieldElement hidden) { + if (equals(hidden) || isStatic() && getDeclaringType().isInterface() || hidden.isPrivate()) { + return false; + } + if (!getName().equals(hidden.getName()) || !getDeclaringType().isAssignable(hidden.getDeclaringType())) { + return false; + } + if (hidden.isPackagePrivate()) { + return getDeclaringType().getPackageName().equals(hidden.getDeclaringType().getPackageName()); + } + return true; + } + return false; + } } diff --git a/inject/src/main/java/io/micronaut/inject/ast/MemberElement.java b/inject/src/main/java/io/micronaut/inject/ast/MemberElement.java index bede1e68a17..3c6e0556b10 100644 --- a/inject/src/main/java/io/micronaut/inject/ast/MemberElement.java +++ b/inject/src/main/java/io/micronaut/inject/ast/MemberElement.java @@ -123,4 +123,15 @@ default boolean isAccessible(@NonNull ClassElement callingType) { default MemberElement withAnnotationMetadata(AnnotationMetadata annotationMetadata) { return (MemberElement) Element.super.withAnnotationMetadata(annotationMetadata); } + + /** + * Checks if this member element hides another. + * + * @param hidden The possibly hidden element + * @return true if this member element hides passed field element + * @since 4.0.0 + */ + default boolean hides(@NonNull MemberElement hidden) { + return false; + } } diff --git a/inject/src/main/java/io/micronaut/inject/ast/MethodElement.java b/inject/src/main/java/io/micronaut/inject/ast/MethodElement.java index a4dda85200c..cfb0506f522 100644 --- a/inject/src/main/java/io/micronaut/inject/ast/MethodElement.java +++ b/inject/src/main/java/io/micronaut/inject/ast/MethodElement.java @@ -238,6 +238,38 @@ default boolean overrides(@NonNull MethodElement overridden) { return true; } + @Override + default boolean hides(@NonNull MemberElement memberElement) { + if (memberElement instanceof MethodElement hidden) { + if (equals(hidden) || isStatic() || hidden.isStatic() || hidden.isPrivate()) { + return false; + } + MethodElement newMethod = this; + if (!newMethod.getName().equals(hidden.getName()) || hidden.getParameters().length != newMethod.getParameters().length) { + return false; + } + for (int i = 0; i < hidden.getParameters().length; i++) { + ParameterElement existingParameter = hidden.getParameters()[i]; + ParameterElement newParameter = newMethod.getParameters()[i]; + ClassElement existingType = existingParameter.getGenericType(); + ClassElement newType = newParameter.getGenericType(); + if (!newType.isAssignable(existingType)) { + return false; + } + } + ClassElement existingReturnType = hidden.getReturnType().getGenericType(); + ClassElement newTypeReturn = newMethod.getReturnType().getGenericType(); + if (!newTypeReturn.isAssignable(existingReturnType)) { + return false; + } + if (hidden.isPackagePrivate()) { + return newMethod.getDeclaringType().getPackageName().equals(hidden.getDeclaringType().getPackageName()); + } + return true; + } + return false; + } + /** * Creates a {@link MethodElement} for the given parameters. * diff --git a/inject/src/main/java/io/micronaut/inject/ast/utils/AstBeanPropertiesUtils.java b/inject/src/main/java/io/micronaut/inject/ast/utils/AstBeanPropertiesUtils.java index 6b12705d136..e49d8ac81c0 100644 --- a/inject/src/main/java/io/micronaut/inject/ast/utils/AstBeanPropertiesUtils.java +++ b/inject/src/main/java/io/micronaut/inject/ast/utils/AstBeanPropertiesUtils.java @@ -23,7 +23,6 @@ import io.micronaut.core.naming.NameUtils; import io.micronaut.inject.ast.BeanPropertiesQuery; import io.micronaut.inject.ast.ClassElement; -import io.micronaut.inject.ast.ElementQuery; import io.micronaut.inject.ast.FieldElement; import io.micronaut.inject.ast.MemberElement; import io.micronaut.inject.ast.MethodElement; @@ -338,39 +337,6 @@ private static boolean isAccessible(MemberElement memberElement, BeanProperties. } } - public static List getSubtypeFirstMethods(ClassElement classElement) { - List methods = classElement.getEnclosedElements(ElementQuery.ALL_METHODS.onlyInstance()); - List result = new ArrayList<>(methods.size()); - List other = new ArrayList<>(methods.size()); - // Process subtype methods first - for (MethodElement methodElement : methods) { - if (methodElement.getDeclaringType().equals(classElement)) { - other.add(methodElement); - } else { - result.add(methodElement); - } - } - result.addAll(other); - return result; - - } - - public static List getSubtypeFirstFields(ClassElement classElement) { - List fields = classElement.getEnclosedElements(ElementQuery.ALL_FIELDS); - List result = new ArrayList<>(fields.size()); - List other = new ArrayList<>(fields.size()); - // Process subtype fields first - for (FieldElement fieldElement : fields) { - if (fieldElement.getDeclaringType().equals(classElement)) { - other.add(fieldElement); - } else { - result.add(fieldElement); - } - } - result.addAll(other); - return result; - } - private static boolean shouldExclude(Set includes, Set excludes, String propertyName) { if (!includes.isEmpty() && !includes.contains(propertyName)) { return true; diff --git a/inject/src/main/java/io/micronaut/inject/ast/utils/EnclosedElementsQuery.java b/inject/src/main/java/io/micronaut/inject/ast/utils/EnclosedElementsQuery.java new file mode 100644 index 00000000000..a55912414a7 --- /dev/null +++ b/inject/src/main/java/io/micronaut/inject/ast/utils/EnclosedElementsQuery.java @@ -0,0 +1,287 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.inject.ast.utils; + +import io.micronaut.core.annotation.AnnotationMetadata; +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.NonNull; +import io.micronaut.core.annotation.Nullable; +import io.micronaut.inject.ast.ClassElement; +import io.micronaut.inject.ast.ConstructorElement; +import io.micronaut.inject.ast.ElementModifier; +import io.micronaut.inject.ast.ElementQuery; +import io.micronaut.inject.ast.FieldElement; +import io.micronaut.inject.ast.MemberElement; +import io.micronaut.inject.ast.MethodElement; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.function.BiPredicate; +import java.util.function.Predicate; + +/** + * The elements query helper. + * + * @param The class native element type + * @param The native element type + * @author Denis Stepanov + * @since 4.0.0 + */ +@Internal +public abstract class EnclosedElementsQuery { + + private final Map elementsCache = new HashMap<>(); + + /** + * Return the elements that match the given query. + * + * @param classElement The class element + * @param query The query to use. + * @param The element type + * @return The fields + */ + public List getEnclosedElements(ClassElement classElement, @NonNull ElementQuery query) { + Objects.requireNonNull(query, "Query cannot be null"); + ElementQuery.Result result = query.result(); + Set excludeElements = getExcludedNativeElements(result); + Predicate filter = element -> { + if (excludeElements.contains(element.getNativeType())) { + return false; + } + List> elementPredicates = result.getElementPredicates(); + if (!elementPredicates.isEmpty()) { + for (Predicate elementPredicate : elementPredicates) { + if (!elementPredicate.test((T) element)) { + return false; + } + } + } + if (element instanceof MethodElement methodElement) { + if (result.isOnlyAbstract()) { + if (methodElement.getDeclaringType().isInterface() && methodElement.isDefault()) { + return false; + } else if (!element.isAbstract()) { + return false; + } + } else if (result.isOnlyConcrete()) { + if (methodElement.getDeclaringType().isInterface() && !methodElement.isDefault()) { + return false; + } else if (element.isAbstract()) { + return false; + } + } + } + if (result.isOnlyInstance() && element.isStatic()) { + return false; + } else if (result.isOnlyStatic() && !element.isStatic()) { + return false; + } + if (result.isOnlyAccessible()) { + // exclude private members + // exclude synthetic members or bridge methods that start with $ + if (element.isPrivate() || element.getName().startsWith("$")) { + return false; + } + if (element instanceof MemberElement && !((MemberElement) element).isAccessible()) { + return false; + } + } + if (!result.getModifierPredicates().isEmpty()) { + Set modifiers = element.getModifiers(); + for (Predicate> modifierPredicate : result.getModifierPredicates()) { + if (!modifierPredicate.test(modifiers)) { + return false; + } + } + } + if (!result.getNamePredicates().isEmpty()) { + for (Predicate namePredicate : result.getNamePredicates()) { + if (!namePredicate.test(element.getName())) { + return false; + } + } + } + if (!result.getAnnotationPredicates().isEmpty()) { + for (Predicate annotationPredicate : result.getAnnotationPredicates()) { + if (!annotationPredicate.test(element)) { + return false; + } + } + } + if (!result.getTypePredicates().isEmpty()) { + for (Predicate typePredicate : result.getTypePredicates()) { + ClassElement ce; + if (element instanceof ConstructorElement) { + ce = classElement; + } else if (element instanceof MethodElement) { + ce = ((MethodElement) element).getGenericReturnType(); + } else if (element instanceof ClassElement) { + ce = (ClassElement) element; + } else if (element instanceof FieldElement) { + ce = ((FieldElement) element).getGenericField(); + } else { + throw new IllegalStateException("Unknown element: " + element); + } + if (!typePredicate.test(ce)) { + return false; + } + } + } + return true; + }; + return (List) getAllElements((C) classElement.getNativeType(), result.isOnlyDeclared(), (t1, t2) -> reduceElements(t1, t2, result), result) + .stream() + .filter(filter) + .toList(); + } + + private boolean reduceElements(io.micronaut.inject.ast.Element newElement, + io.micronaut.inject.ast.Element existingElement, + ElementQuery.Result result) { + if (!result.isIncludeHiddenElements()) { + if (newElement instanceof FieldElement && existingElement instanceof FieldElement) { + return ((FieldElement) newElement).hides((FieldElement) existingElement); + } + if (newElement instanceof MethodElement && existingElement instanceof MethodElement) { + if (((MethodElement) newElement).hides((MethodElement) existingElement)) { + return true; + } + } + } + if (!result.isIncludeOverriddenMethods()) { + if (newElement instanceof MethodElement && existingElement instanceof MethodElement) { + return ((MethodElement) newElement).overrides((MethodElement) existingElement); + } + } + return false; + } + + private Collection getAllElements(C classNode, + boolean onlyDeclared, + BiPredicate reduce, + ElementQuery.Result result) { + Set elements = new LinkedHashSet<>(); + List> hierarchy = new ArrayList<>(); + collectHierarchy(classNode, onlyDeclared, hierarchy, result); + for (List classElements : hierarchy) { + Set addedFromClassElements = new LinkedHashSet<>(); + classElements: + for (N element : classElements) { + io.micronaut.inject.ast.Element newElement = elementsCache.computeIfAbsent(element, this::toAstElement); + for (Iterator iterator = elements.iterator(); iterator.hasNext(); ) { + io.micronaut.inject.ast.Element existingElement = iterator.next(); + if (newElement.equals(existingElement)) { + continue; + } + if (reduce.test(newElement, existingElement)) { + iterator.remove(); + addedFromClassElements.add(newElement); + } else if (reduce.test(existingElement, newElement)) { + continue classElements; + } + } + addedFromClassElements.add(newElement); + } + elements.addAll(addedFromClassElements); + } + return elements; + } + + private void collectHierarchy(C classNode, + boolean onlyDeclared, + List> hierarchy, + ElementQuery.Result result) { + if (excludeClass(classNode)) { + return; + } + if (!onlyDeclared) { + C superclass = getSuperClass(classNode); + if (superclass != null) { + collectHierarchy(superclass, false, hierarchy, result); + } + for (C interfaceNode : getInterfaces(classNode)) { + List> interfaceElements = new ArrayList<>(); + collectHierarchy(interfaceNode, false, interfaceElements, result); + hierarchy.addAll(interfaceElements); + } + } + hierarchy.add(getEnclosedElements(classNode, result)); + } + + /** + * Provides a collection of the native elements to exclude. + * + * @param result The result + * @return the collection of excluded elements + */ + protected Set getExcludedNativeElements(@NonNull ElementQuery.Result result) { + return Collections.emptySet(); + } + + /** + * Extracts the super class. + * + * @param classNode The class + * @return The super calss + */ + @Nullable + protected abstract C getSuperClass(C classNode); + + /** + * Extracts the interfaces of the class. + * + * @param classNode The class + * @return The interfaces + */ + @NonNull + protected abstract Collection getInterfaces(C classNode); + + /** + * Extracts the enclosed elements of the class. + * + * @param classNode The class + * @param result The query result + * @return The enclosed elements + */ + @NonNull + protected abstract List getEnclosedElements(C classNode, ElementQuery.Result result); + + /** + * Checks if the class needs to be excluded. + * + * @param classNode The class + * @return true if to exclude + */ + protected abstract boolean excludeClass(C classNode); + + /** + * Converts the native element to the AST element. + * + * @param enclosedElement The native element. + * @return The AST element + */ + @NonNull + protected abstract io.micronaut.inject.ast.Element toAstElement(N enclosedElement); + +} diff --git a/inject/src/main/java/io/micronaut/inject/processing/DeclaredBeanElementCreator.java b/inject/src/main/java/io/micronaut/inject/processing/DeclaredBeanElementCreator.java index 7f166cd243c..a8c526745b8 100644 --- a/inject/src/main/java/io/micronaut/inject/processing/DeclaredBeanElementCreator.java +++ b/inject/src/main/java/io/micronaut/inject/processing/DeclaredBeanElementCreator.java @@ -136,49 +136,30 @@ protected boolean processAsProperties() { private void build(BeanDefinitionVisitor visitor) { Set processedFields = new HashSet<>(); - if (!processAsProperties()) { - for (PropertyElement propertyElement : classElement.getSyntheticBeanProperties()) { - propertyElement.getField().ifPresent(processedFields::add); + ElementQuery memberQuery = ElementQuery.ALL_FIELD_AND_METHODS.includeHiddenElements(); + if (processAsProperties()) { + memberQuery = memberQuery.excludePropertyElements(); + for (PropertyElement propertyElement : classElement.getBeanProperties()) { visitPropertyInternal(visitor, propertyElement); } - } - ElementQuery fieldsQuery = ElementQuery.ALL_FIELDS.includeHiddenElements(); - ElementQuery membersQuery = ElementQuery.ALL_METHODS; - boolean processAsProperties = processAsProperties(); - if (processAsProperties) { - fieldsQuery = fieldsQuery.excludePropertyElements(); - membersQuery = membersQuery.excludePropertyElements(); - for (PropertyElement propertyElement : classElement.getBeanProperties()) { + } else { + for (PropertyElement propertyElement : classElement.getSyntheticBeanProperties()) { + propertyElement.getField().ifPresent(processedFields::add); visitPropertyInternal(visitor, propertyElement); } } - List fields = new ArrayList<>(classElement.getEnclosedElements(fieldsQuery)); - fields.removeIf(processedFields::contains); - List declaredFields = new ArrayList<>(fields.size()); + List memberElements = new ArrayList<>(classElement.getEnclosedElements(memberQuery)); + memberElements.removeIf(processedFields::contains); // Process subtype fields first - for (FieldElement fieldElement : fields) { - if (fieldElement.getDeclaringType().equals(classElement)) { - declaredFields.add(fieldElement); - } else { + for (MemberElement memberElement : memberElements) { + if (memberElement instanceof FieldElement fieldElement) { visitFieldInternal(visitor, fieldElement); - } - } - List methods = classElement.getEnclosedElements(membersQuery); - List declaredMethods = new ArrayList<>(methods.size()); - // Process subtype methods first - for (MethodElement methodElement : methods) { - if (methodElement.getDeclaringType().equals(classElement)) { - declaredMethods.add(methodElement); - } else { + } else if (memberElement instanceof MethodElement methodElement) { visitMethodInternal(visitor, methodElement); + } else { + throw new IllegalStateException("Unknown element"); } } - for (FieldElement fieldElement : declaredFields) { - visitFieldInternal(visitor, fieldElement); - } - for (MethodElement methodElement : declaredMethods) { - visitMethodInternal(visitor, methodElement); - } } private void visitFieldInternal(BeanDefinitionVisitor visitor, FieldElement fieldElement) { diff --git a/inject/src/main/java/io/micronaut/inject/processing/FactoryBeanElementCreator.java b/inject/src/main/java/io/micronaut/inject/processing/FactoryBeanElementCreator.java index 3e1dec15594..fe9ded31e0c 100644 --- a/inject/src/main/java/io/micronaut/inject/processing/FactoryBeanElementCreator.java +++ b/inject/src/main/java/io/micronaut/inject/processing/FactoryBeanElementCreator.java @@ -39,6 +39,7 @@ import io.micronaut.inject.writer.BeanDefinitionWriter; import io.micronaut.inject.writer.OriginatingElements; +import java.util.List; import java.util.Optional; import java.util.concurrent.atomic.AtomicInteger; @@ -221,9 +222,10 @@ private void buildProducedBeanDefinition(BeanDefinitionWriter producedBeanDefini aopProxyWriter.visitTypeArguments(producedType.getAllTypeArguments()); beanDefinitionWriters.add(aopProxyWriter); - producedType.getEnclosedElements(ElementQuery.ALL_METHODS) + List methodElements = producedType.getEnclosedElements(ElementQuery.ALL_METHODS) .stream() - .filter(m -> m.isPublic() && !m.isFinal() && !m.isStatic()) + .filter(m -> m.isPublic() && !m.isFinal() && !m.isStatic()).toList(); + methodElements .forEach(methodElement -> aopHelper.visitAroundMethod(aopProxyWriter, methodElement.getDeclaringType(), methodElement)); } else if (producedAnnotationMetadata.hasStereotype(Executable.class)) { diff --git a/test-suite/src/test/groovy/io/micronaut/inject/SharedInjectionSpec.groovy b/test-suite/src/test/groovy/io/micronaut/inject/SharedInjectionSpec.groovy new file mode 100644 index 00000000000..282b419b543 --- /dev/null +++ b/test-suite/src/test/groovy/io/micronaut/inject/SharedInjectionSpec.groovy @@ -0,0 +1,50 @@ +package io.micronaut.inject + +import io.micronaut.context.ApplicationContext +import io.micronaut.test.extensions.spock.annotation.MicronautTest +import jakarta.inject.Inject +import spock.lang.Shared +import spock.lang.Specification + +class AbstractExample extends Specification { + + @Inject + @Shared + ApplicationContext sharedCtx + + @Inject + ApplicationContext ctx + +} + +@MicronautTest +class FailingTest extends AbstractExample { + + def 'injection is not null'() { + expect: + ctx != null + } + + def 'shared injection is not null'() { + expect: + sharedCtx != null + } +} + +@MicronautTest +class SuccessfulTest extends AbstractExample { + + @Shared + @Inject + ApplicationContext dummy + + def 'injection is not null'() { + expect: + ctx != null + } + + def 'shared injection is not null'() { + expect: + sharedCtx != null + } +} From 29f22956276f530119f04a8d896a70183b7202eb Mon Sep 17 00:00:00 2001 From: Graeme Rocher Date: Mon, 24 Oct 2022 12:27:35 +0200 Subject: [PATCH 148/743] Correct record constructor search algorithm (#8216) Modifies ModelUtils.concreteConstructorFor to do a comparison of the record components vs the constructor parameters to ensure the correct constructor is picked for records. Fixes #8187 --- .../beans/BeanIntrospectionSpec.groovy | 277 ++++++++++-------- .../annotation/processing/ModelUtils.java | 30 ++ 2 files changed, 186 insertions(+), 121 deletions(-) diff --git a/inject-java-test/src/test/groovy/io/micronaut/inject/visitor/beans/BeanIntrospectionSpec.groovy b/inject-java-test/src/test/groovy/io/micronaut/inject/visitor/beans/BeanIntrospectionSpec.groovy index 0468e72d21f..fcef82b024f 100644 --- a/inject-java-test/src/test/groovy/io/micronaut/inject/visitor/beans/BeanIntrospectionSpec.groovy +++ b/inject-java-test/src/test/groovy/io/micronaut/inject/visitor/beans/BeanIntrospectionSpec.groovy @@ -62,7 +62,7 @@ class Test { public void setArray(T[] array) { this.array = array; } - + @Executable T[] myMethod() { return array; @@ -90,7 +90,7 @@ class Test { public String getOne() { this.invoked = true; return one; - } + } } '''); @@ -158,7 +158,7 @@ class Test { public String getOne() { this.invoked = true; return one; - } + } } '''); @@ -227,7 +227,7 @@ class Test { String three; // package protected protected String four; // not included since protected private String five; // not included since private - + Test(int two) { this.two = two; } @@ -280,7 +280,7 @@ class Test { String three; // package protected protected String four; // not included since protected private String five; // not included since private - + Test(int two) { this.two = two; } @@ -309,7 +309,7 @@ package beanctor; public class Test { private final String another; - + @com.fasterxml.jackson.annotation.JsonCreator Test(String another) { this.another = another; @@ -356,7 +356,7 @@ public class MethodTest extends SuperType implements SomeInt { public String invokeMe(String str) { return str; } - + @Executable int invokePrim(int i) { return i; @@ -368,7 +368,7 @@ class SuperType { String superMethod(String str) { return str; } - + @Executable public String invokeMe(String str) { return str; @@ -380,7 +380,7 @@ interface SomeInt { default boolean ok() { return true; } - + default String getName() { return "ok"; } @@ -494,7 +494,7 @@ import java.net.URL; public class CopyMe { private final String another; - + CopyMe(String another) { this.another = another; } @@ -502,7 +502,7 @@ public class CopyMe { public String getAnother() { return another; } - + public CopyMe alterAnother(String a) { return this.another == a ? this : new CopyMe(a.toUpperCase()); } @@ -538,7 +538,7 @@ public class CopyMe { private URL url; private final String name; private final String another; - + CopyMe(String name, String another) { this.name = name; this.another = another; @@ -551,15 +551,15 @@ public class CopyMe { public void setUrl(URL url) { this.url = url; } - + public String getName() { return name; } - + public String getAnother() { return another; } - + public CopyMe withAnother(String a) { return this.another == a ? this : new CopyMe(this.name, a.toUpperCase()); } @@ -629,6 +629,41 @@ public record Foo(int x, int y){ obj.y() == 10 } + @Requires({ jvm.isJava14Compatible() }) + @Issue('https://github.com/micronaut-projects/micronaut-core/issues/8187') + void "test secondary constructor for Java 14+ records with initializer"() { + given: + BeanIntrospection introspection = buildBeanIntrospection('test.Foo', ''' +package test; + +import io.micronaut.core.annotation.Creator; +import java.util.List; +import javax.validation.constraints.Min; + +@io.micronaut.core.annotation.Introspected +public record Foo(int x, int y){ + public Foo { + if (x < 0) { + throw new IllegalArgumentException("Invalid argument"); + } + } + public Foo(int x) { + this(x, 20); + } + public Foo() { + this(20, 20); + } +} +''') + when: + def obj = introspection.instantiate(5, 10) + + then: + introspection.getConstructorArguments().length == 2 + obj.x() == 5 + obj.y() == 10 + } + @Requires({ jvm.isJava14Compatible() }) void "test serializing records respects json annotations"() { given: @@ -729,7 +764,7 @@ public class Foo { public List getValue() { return value; } - + public void setValue(List value) { this.value = value; } @@ -850,7 +885,7 @@ public interface MyInterface { @Executable default String name() { return getName(); - } + } } class MyImpl implements MyInterface { @@ -884,7 +919,7 @@ class Test {} public interface MyInterface { @Executable - String name(); + String name(); } class MyImpl implements MyInterface { @@ -1000,7 +1035,7 @@ class Foo extends GenBase { abstract class GenBase { abstract T getName(); - + public T getOther() { return (T) "other"; } @@ -1032,7 +1067,7 @@ class Foo implements GenBase { public Long getValue() { return value; } - + public void setValue(Long value) { this.value = value; } @@ -1040,7 +1075,7 @@ class Foo implements GenBase { interface GenBase { T getValue(); - + void setValue(T t); } ''') @@ -1070,7 +1105,7 @@ import io.micronaut.core.annotation.Creator; @io.micronaut.core.annotation.Introspected interface Foo { String getName(); - + @Creator static Foo create(String name) { return () -> name; @@ -1097,7 +1132,7 @@ import io.micronaut.core.annotation.Creator; @io.micronaut.core.annotation.Introspected interface Foo { String getName(); - + @Creator static Foo create(String name) { return () -> name; @@ -1388,11 +1423,11 @@ public class ValidatedConfig { public void setUrl(URL url) { this.url = url; } - + public static class Inner { - + } - + } ''') expect: @@ -1455,21 +1490,21 @@ class ValidatedConfig { public void setUrl(URL url) { this.url = url; } - + public static class Inner { - + } - + @ConfigurationProperties("another") static class Another { - + private URL url; @NotNull public URL getUrl() { return url; } - + public void setUrl(URL url) { this.url = url; } @@ -1633,17 +1668,17 @@ import java.time.Duration; class MyConfig { private String host; private int serverPort; - + @ConfigurationInject MyConfig(@javax.validation.constraints.NotBlank String host, int serverPort) { this.host = host; this.serverPort = serverPort; } - + public String getHost() { return host; } - + public int getServerPort() { return serverPort; } @@ -1705,7 +1740,7 @@ public class Test { public > Test(int num, String str, Class enumClass) { this(num, str + enumClass.getName()); } - + @Creator public > Test(int num, String str) { this.num = num; @@ -1740,7 +1775,7 @@ class Test { } public String getTest() { return test; - } + } } @@ -1760,7 +1795,7 @@ class Test { private T child; public T getChild() { return child; - } + } } class B {} @@ -1800,7 +1835,7 @@ class Address { @NotBlank(groups = GroupThree.class, message = "different message") @Size(min = 5, max = 20, groups = GroupTwo.class) private String street; - + public String getStreet() { return this.street; } @@ -1834,7 +1869,7 @@ class Book { this.title = title; this.author = author; } - + @io.micronaut.core.annotation.Creator public Book(String title) { this.title = title; @@ -1843,7 +1878,7 @@ class Book { public String getTitle() { return title; } - + void setTitle(String title) { this.title = title; } @@ -1908,12 +1943,12 @@ class Book { private String title; public Book() { - } + } public String getTitle() { return title; } - + public void setTitle(String title) { this.title = title; } @@ -1967,23 +2002,23 @@ import com.fasterxml.jackson.annotation.*; class Test { private String name; private int age; - + @JsonCreator Test(@JsonProperty("name") String name) { this.name = name; } - + Test(int age) { this.age = age; } - + public int getAge() { return age; } public void setAge(int age) { this.age = age; } - + public String getName() { return this.name; } @@ -2140,11 +2175,11 @@ import java.util.*; @Introspected class Test { private Status status; - + public void setStatus(Status status) { this.status = status; } - + public Status getStatus() { return this.status; } @@ -2200,9 +2235,9 @@ class Test { @Size(max=100) private int age; private int[] primitiveArray; - + private long v; - + public Test(String name, @Size(max=100) int age, int[] primitiveArray) { this.name = name; this.age = age; @@ -2219,28 +2254,28 @@ class Test { public void setAge(int age) { this.age = age; } - + public void setId(Long id) { this.id = id; } - + public Long getId() { return this.id; } - + public void setVersion(Long version) { this.version = version; } - + public Long getVersion() { return this.version; } - + @Version public long getAnotherVersion() { return v; } - + public void setAnotherVersion(long v) { this.v = v; } @@ -2314,8 +2349,8 @@ class Test { private String name; @Size(max=100) private int age; - - + + public String getName() { return this.name; } @@ -2328,19 +2363,19 @@ class Test { public void setAge(int age) { this.age = age; } - + public void setId(Long id) { this.id = id; } - + public Long getId() { return this.id; } - + public void setVersion(Long version) { this.version = version; } - + public Long getVersion() { return this.version; } @@ -2491,33 +2526,33 @@ class Test extends ParentBean { private String name; @Size(max=100) private int age; - + private List list; private String[] stringArray; private int[] primitiveArray; private boolean flag; private TypeConverter genericsTest; private TypeConverter genericsArrayTest; - + public TypeConverter getGenericsTest() { return genericsTest; } - + public TypeConverter getGenericsArrayTest() { return genericsArrayTest; } - + public String getReadOnly() { return readOnly; } public boolean isFlag() { return flag; } - + public void setFlag(boolean flag) { this.flag = flag; } - + public String getName() { return this.name; } @@ -2530,11 +2565,11 @@ class Test extends ParentBean { public void setAge(int age) { this.age = age; } - + public List getList() { return this.list; } - + public void setList(List l) { this.list = l; } @@ -2558,11 +2593,11 @@ class Test extends ParentBean { class ParentBean { private List listOfBytes; - + public List getListOfBytes() { return this.listOfBytes; } - + public void setListOfBytes(List list) { this.listOfBytes = list; } @@ -2867,12 +2902,12 @@ import com.fasterxml.jackson.annotation.*; @Introspected class Test { private Map properties; - + @Creator Test(Map properties) { this.properties = properties; } - + public Map getProperties() { return properties; } @@ -2895,16 +2930,16 @@ import io.micronaut.core.annotation.*; @Introspected class Test { private String name; - + private Test(String name) { this.name = name; } - + @Creator public static Test forName(String name) { return new Test(name); } - + public String getName() { return name; } @@ -2990,16 +3025,16 @@ import io.micronaut.core.annotation.*; @Introspected class Test { private String name; - + private Test(String name) { this.name = name; } - + @Creator public static Test forName() { return new Test("default"); } - + public String getName() { return name; } @@ -3037,21 +3072,21 @@ import io.micronaut.core.annotation.*; @Introspected class Test { private String name; - + private Test(String name) { this.name = name; } - + @Creator public static Test forName() { return new Test("default"); } - + @Creator public static Test forName(String name) { return new Test(name); } - + public String getName() { return name; } @@ -3091,22 +3126,22 @@ class Test { private final String name; public static final Companion Companion = new Companion(); - + public final String getName() { return name; } - + private Test(String name) { this.name = name; } - + public static final class Companion { - + @Creator public final Test forName(String name) { return new Test(name); } - + private Companion() { } } @@ -3165,7 +3200,7 @@ public enum Test { Test(int number) { this.number = number; } - + public int getNumber() { return number; } @@ -3286,7 +3321,7 @@ import java.util.Map; class Test { public Test(Map> map) { - + } } @@ -3320,7 +3355,7 @@ class Test { public Test() { } - + public int[] getOneDimension() { return oneDimension; } @@ -3336,7 +3371,7 @@ class Test { public void setTwoDimensions(int[][] twoDimensions) { this.twoDimensions = twoDimensions; } - + public int[][][] getThreeDimensions() { return threeDimensions; } @@ -3394,13 +3429,13 @@ class Test { public Test() { } - + public String[] getOneDimension() { return oneDimension; } public void setOneDimension(String[] oneDimension) { - this.oneDimension = oneDimension; + this.oneDimension = oneDimension; } public String[][] getTwoDimensions() { @@ -3410,7 +3445,7 @@ class Test { public void setTwoDimensions(String[][] twoDimensions) { this.twoDimensions = twoDimensions; } - + public String[][][] getThreeDimensions() { return threeDimensions; } @@ -3469,13 +3504,13 @@ class Test { public Test() { } - + public SomeEnum[] getOneDimension() { return oneDimension; } public void setOneDimension(SomeEnum[] oneDimension) { - this.oneDimension = oneDimension; + this.oneDimension = oneDimension; } public SomeEnum[][] getTwoDimensions() { @@ -3485,7 +3520,7 @@ class Test { public void setTwoDimensions(SomeEnum[][] twoDimensions) { this.twoDimensions = twoDimensions; } - + public SomeEnum[][][] getThreeDimensions() { return threeDimensions; } @@ -3600,11 +3635,11 @@ import io.micronaut.core.annotation.Introspected; class Test { private final String xForwardedFor; - + Test(String xForwardedFor) { this.xForwardedFor = xForwardedFor; } - + public String getXForwardedFor() { return xForwardedFor; } @@ -3665,19 +3700,19 @@ import io.micronaut.core.annotation.Introspected; abstract class Test { private String name; private String author; - + public String getName() { return name; } - + public void setName(String name) { this.name = name; } - + public String getAuthor() { return author; } - + public void setAuthor(String author) { this.author = author; } @@ -3771,23 +3806,23 @@ import io.micronaut.core.annotation.Introspected; abstract class Test { private String name; private String author; - + public String getName() { return name; } - + public void setName(String name) { this.name = name; } - + public String getAuthor() { return author; } - + public void setAuthor(String author) { this.author = author; } - + public int getAge() { return 0; } @@ -3851,7 +3886,7 @@ import java.util.Set; public class Test { private Set authors; - + public Set getAuthors() { return authors; } @@ -3893,7 +3928,7 @@ public class Test { public String getFoo() { return "bar"; } - + @JsonProperty public void setFoo(String s) { } @@ -3947,11 +3982,11 @@ import io.micronaut.core.annotation.Introspected; public class Test { @JsonProperty String foo; - + public String getFoo() { return foo; } - + public void setFoo(String s) { this.foo = s; } @@ -4008,12 +4043,12 @@ import io.micronaut.core.annotation.Introspected; public class Test { @JsonProperty("field") String foo; - + @JsonProperty("getter") public String getFoo() { return foo; } - + @JsonProperty("setter") public void setFoo(String s) { this.foo = s; @@ -4073,11 +4108,11 @@ import io.micronaut.core.annotation.Introspected; public class Test { @JsonProperty("field") String foo; - + public String getFoo() { return foo; } - + @JsonProperty("setter") public void setFoo(String s) { this.foo = s; @@ -4102,15 +4137,15 @@ import io.micronaut.core.annotation.Introspected; @Introspected public class Test { private final java.util.ArrayList foo; - + public Test(java.util.List foo) { this.foo = null; } - + public java.util.List getFoo() { return null; } - + } ''') diff --git a/inject-java/src/main/java/io/micronaut/annotation/processing/ModelUtils.java b/inject-java/src/main/java/io/micronaut/annotation/processing/ModelUtils.java index 8b1ee457771..77f6bd5ac9c 100644 --- a/inject-java/src/main/java/io/micronaut/annotation/processing/ModelUtils.java +++ b/inject-java/src/main/java/io/micronaut/annotation/processing/ModelUtils.java @@ -21,6 +21,7 @@ import io.micronaut.core.annotation.Creator; import io.micronaut.core.annotation.Internal; import io.micronaut.core.naming.NameUtils; +import io.micronaut.core.reflect.ReflectionUtils; import io.micronaut.inject.ast.ClassElement; import io.micronaut.inject.processing.JavaModelUtils; @@ -31,6 +32,7 @@ import javax.lang.model.util.ElementFilter; import javax.lang.model.util.Elements; import javax.lang.model.util.Types; +import java.lang.reflect.Method; import java.util.*; import java.util.stream.Collectors; @@ -45,6 +47,7 @@ */ @Internal public class ModelUtils { + private static final Method RECORD_COMPONENTS_METHODS = ReflectionUtils.findMethod(TypeElement.class, "getRecordComponents").orElse(null); private final Elements elementUtils; private final Types typeUtils; @@ -197,6 +200,7 @@ String setterNameFor(String fieldName) { * @return The constructor */ @Nullable + @SuppressWarnings("java:S1119") public ExecutableElement concreteConstructorFor(TypeElement classElement, AnnotationUtils annotationUtils) { if (JavaModelUtils.isRecord(classElement)) { final List constructors = ElementFilter @@ -205,7 +209,33 @@ public ExecutableElement concreteConstructorFor(TypeElement classElement, Annota if (element.isPresent()) { return element.get(); } else { + if (RECORD_COMPONENTS_METHODS != null) { + List recordComponents = ReflectionUtils.invokeMethod(classElement, RECORD_COMPONENTS_METHODS); + if (recordComponents != null) { + + // pick the constructor with the same number of arguments + // and where the types match the record components + constructorSearch: for (ExecutableElement constructor : constructors) { + List parameters = constructor.getParameters(); + if (parameters.size() == recordComponents.size()) { + for (int i = 0; i < parameters.size(); i++) { + VariableElement vt = parameters.get(i); + Element rct = recordComponents.get(i); + TypeMirror leftType = typeUtils.erasure(vt.asType()); + TypeMirror rightType = typeUtils.erasure(rct.asType()); + if (!leftType.equals(rightType)) { + // types don't match, continue searching constructors + continue constructorSearch; + } + } + } else { + continue; + } + return constructor; + } + } + } // with records the record constructor is always the last constructor return constructors.get(constructors.size() - 1); } From 1a4c6c44d53d5cf9617113f29a0c417e3202d72d Mon Sep 17 00:00:00 2001 From: Denis Stepanov Date: Tue, 25 Oct 2022 14:04:57 +0400 Subject: [PATCH 149/743] Use bean property writer type as the property type. Correct annotations creation for fields. (#8223) Co-authored-by: altro3 --- .../inject/visitor/AllElementsVisitor.java | 28 ++- .../inject/visitor/PropertyElementSpec.groovy | 163 +++++++++++++++++ .../processing/visitor/JavaFieldElement.java | 2 +- .../visitors/AllElementsVisitor.java | 25 ++- .../visitors/PropertyElementSpec.groovy | 165 +++++++++++++++++- .../AbstractAnnotationMetadataBuilder.java | 2 +- .../ast/utils/AstBeanPropertiesUtils.java | 19 +- 7 files changed, 384 insertions(+), 20 deletions(-) diff --git a/inject-groovy/src/test/groovy/io/micronaut/inject/visitor/AllElementsVisitor.java b/inject-groovy/src/test/groovy/io/micronaut/inject/visitor/AllElementsVisitor.java index ecd1b89be12..dd4ccbd0ba3 100644 --- a/inject-groovy/src/test/groovy/io/micronaut/inject/visitor/AllElementsVisitor.java +++ b/inject-groovy/src/test/groovy/io/micronaut/inject/visitor/AllElementsVisitor.java @@ -15,11 +15,13 @@ */ package io.micronaut.inject.visitor; +import io.micronaut.core.annotation.AnnotationMetadataProvider; import io.micronaut.http.annotation.Controller; import io.micronaut.inject.ast.ClassElement; import io.micronaut.inject.ast.Element; import io.micronaut.inject.ast.FieldElement; import io.micronaut.inject.ast.MethodElement; +import io.micronaut.inject.ast.TypedElement; import java.util.*; @@ -63,18 +65,42 @@ public void finish(VisitorContext visitorContext) { @Override public void visitClass(ClassElement element, VisitorContext context) { visit(element); + element.getBeanProperties(); // Preload properties for tests otherwise it fails because the compiler is done + element.getAnnotationMetadata(); VISITED_CLASS_ELEMENTS.add(element); } @Override public void visitMethod(MethodElement element, VisitorContext context) { - visit(element); VISITED_METHOD_ELEMENTS.add(element); + // Preload + element.getReturnType().getBeanProperties().forEach(AnnotationMetadataProvider::getAnnotationMetadata); + Arrays.stream(element.getParameters()).flatMap(p -> p.getType().getBeanProperties().stream()).forEach(propertyElement -> { + initialize(propertyElement); + propertyElement.getField().ifPresent(this::initialize); + propertyElement.getWriteMethod().ifPresent(methodElement -> { + initialize(methodElement.getReturnType()); + Arrays.stream(methodElement.getParameters()).forEach(this::initialize); + }); + propertyElement.getReadMethod().ifPresent(methodElement -> { + initialize(methodElement.getReturnType()); + Arrays.stream(methodElement.getParameters()).forEach(this::initialize); + }); + }); + element.getAnnotationMetadata(); + visit(element); + } + + private void initialize(TypedElement typedElement) { + typedElement.getAnnotationMetadata(); + typedElement.getType().getAnnotationMetadata(); + typedElement.getGenericType().getAnnotationMetadata(); } @Override public void visitField(FieldElement element, VisitorContext context) { visit(element); + element.getAnnotationMetadata(); } void visit(Element element) { diff --git a/inject-groovy/src/test/groovy/io/micronaut/inject/visitor/PropertyElementSpec.groovy b/inject-groovy/src/test/groovy/io/micronaut/inject/visitor/PropertyElementSpec.groovy index f5335f79c3d..f0a2746c786 100644 --- a/inject-groovy/src/test/groovy/io/micronaut/inject/visitor/PropertyElementSpec.groovy +++ b/inject-groovy/src/test/groovy/io/micronaut/inject/visitor/PropertyElementSpec.groovy @@ -155,4 +155,167 @@ class Response { AllElementsVisitor.VISITED_METHOD_ELEMENTS[1].returnType.beanProperties.size() == 1 AllElementsVisitor.VISITED_METHOD_ELEMENTS[1].returnType.beanProperties[0].type.name == 'java.lang.Integer' } + + void "test get annotations from type after bean properties "() { + buildBeanDefinition('test.TestController', ''' +package test; + +import io.micronaut.core.annotation.Introspected; +import io.micronaut.http.annotation.Body; +import io.micronaut.http.annotation.Controller; +import io.micronaut.http.annotation.Post; + +@Controller +class TestController { + + @Post("/path") + public void processSync(@Body MyDto dto) { + } +} + +class MyDto { + + private Parameters parameters; + + public Parameters getParameters() { + return parameters; + } + + public void setParameters(Parameters parameters) { + this.parameters = parameters; + } +} + +@Introspected +class Parameters { + + private Integer stampWidth; + private Integer stampHeight; + private int pageNumber; + + public Integer getStampWidth() { + return stampWidth; + } + + public void setStampWidth(Integer stampWidth) { + this.stampWidth = stampWidth; + } + + public Integer getStampHeight() { + return stampHeight; + } + + public void setStampHeight(Integer stampHeight) { + this.stampHeight = stampHeight; + } + + public int getPageNumber() { + return pageNumber; + } + + public void setPageNumber(int pageNumber) { + this.pageNumber = pageNumber; + } +} +''') + expect: + AllElementsVisitor.VISITED_CLASS_ELEMENTS.size() == 1 + + def method = AllElementsVisitor.VISITED_METHOD_ELEMENTS[0] + def parameter = method.parameters[0] + + def beanProperty = parameter.type.beanProperties.get(0) + beanProperty.type.annotationNames.sort() == [ + 'io.micronaut.context.annotation.BeanProperties', + 'io.micronaut.core.annotation.Introspected' + ] + beanProperty.field.get().type.annotationNames.sort() == [ + 'io.micronaut.context.annotation.BeanProperties', + 'io.micronaut.core.annotation.Introspected' + ] + beanProperty.readMethod.get().returnType.annotationNames.sort() == [ + 'io.micronaut.context.annotation.BeanProperties', + 'io.micronaut.core.annotation.Introspected' + ] + beanProperty.writeMethod.get().parameters[0].type.annotationNames.sort() == [ + 'io.micronaut.context.annotation.BeanProperties', + 'io.micronaut.core.annotation.Introspected' + ] + } + + void "test get annotations from type after bean properties for field access"() { + buildBeanDefinition('test.TestController', ''' +package test; + +import io.micronaut.core.annotation.Introspected; +import io.micronaut.http.annotation.Body; +import io.micronaut.http.annotation.Controller; +import io.micronaut.http.annotation.Post; + +@Controller +class TestController { + + @Post("/path") + public void processSync(@Body MyDto dto) { + } +} + +@Introspected(accessKind = Introspected.AccessKind.FIELD) +class MyDto { + + public Parameters parameters; +} + +@Introspected +class Parameters { + + private Integer stampWidth; + private Integer stampHeight; + private int pageNumber; + + public Integer getStampWidth() { + return stampWidth; + } + + public void setStampWidth(Integer stampWidth) { + this.stampWidth = stampWidth; + } + + public Integer getStampHeight() { + return stampHeight; + } + + public void setStampHeight(Integer stampHeight) { + this.stampHeight = stampHeight; + } + + public int getPageNumber() { + return pageNumber; + } + + public void setPageNumber(int pageNumber) { + this.pageNumber = pageNumber; + } +} +''') + expect: + AllElementsVisitor.VISITED_CLASS_ELEMENTS.size() == 1 + + def method = AllElementsVisitor.VISITED_METHOD_ELEMENTS[0] + def parameter = method.parameters[0] + + def beanProperty = parameter.type.beanProperties.get(0) + beanProperty.type.annotationNames.sort() == [ + 'io.micronaut.context.annotation.BeanProperties', + 'io.micronaut.core.annotation.Introspected' + ] + beanProperty.field.get().type.annotationNames.sort() == [ + 'io.micronaut.context.annotation.BeanProperties', + 'io.micronaut.core.annotation.Introspected' + ] + beanProperty.field.get().genericType.annotationNames.sort() == [ + 'io.micronaut.context.annotation.BeanProperties', + 'io.micronaut.core.annotation.Introspected' + ] + } } diff --git a/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaFieldElement.java b/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaFieldElement.java index 18fff9c8653..abcba4abe24 100644 --- a/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaFieldElement.java +++ b/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaFieldElement.java @@ -89,7 +89,7 @@ public ClassElement getGenericType() { variableElement.asType(), visitorContext, owningType.getGenericTypeInfo(), - false + true ); } } diff --git a/inject-java/src/test/groovy/io/micronaut/visitors/AllElementsVisitor.java b/inject-java/src/test/groovy/io/micronaut/visitors/AllElementsVisitor.java index c17f3466d2e..b82823eed81 100644 --- a/inject-java/src/test/groovy/io/micronaut/visitors/AllElementsVisitor.java +++ b/inject-java/src/test/groovy/io/micronaut/visitors/AllElementsVisitor.java @@ -21,10 +21,12 @@ import io.micronaut.inject.ast.Element; import io.micronaut.inject.ast.FieldElement; import io.micronaut.inject.ast.MethodElement; +import io.micronaut.inject.ast.TypedElement; import io.micronaut.inject.visitor.TypeElementVisitor; import io.micronaut.inject.visitor.VisitorContext; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; public class AllElementsVisitor implements TypeElementVisitor { @@ -42,8 +44,6 @@ public void start(VisitorContext visitorContext) { @Override public void visitClass(ClassElement element, VisitorContext context) { visit(element); - // Java 9+ doesn't allow resolving elements was the compiler - // is finished being used so this test cannot be made to work beyond Java 8 the way it is currently written element.getBeanProperties(); // Preload properties for tests otherwise it fails because the compiler is done element.getAnnotationMetadata(); VISITED_CLASS_ELEMENTS.add(element); @@ -52,11 +52,30 @@ public void visitClass(ClassElement element, VisitorContext context) { @Override public void visitMethod(MethodElement element, VisitorContext context) { VISITED_METHOD_ELEMENTS.add(element); - element.getReturnType().getBeanProperties().forEach(AnnotationMetadataProvider::getAnnotationMetadata); // Preload + // Preload + element.getReturnType().getBeanProperties().forEach(AnnotationMetadataProvider::getAnnotationMetadata); + Arrays.stream(element.getParameters()).flatMap(p -> p.getType().getBeanProperties().stream()).forEach(propertyElement -> { + initialize(propertyElement); + propertyElement.getField().ifPresent(this::initialize); + propertyElement.getWriteMethod().ifPresent(methodElement -> { + initialize(methodElement.getReturnType()); + Arrays.stream(methodElement.getParameters()).forEach(this::initialize); + }); + propertyElement.getReadMethod().ifPresent(methodElement -> { + initialize(methodElement.getReturnType()); + Arrays.stream(methodElement.getParameters()).forEach(this::initialize); + }); + }); element.getAnnotationMetadata(); visit(element); } + private void initialize(TypedElement typedElement) { + typedElement.getAnnotationMetadata(); + typedElement.getType().getAnnotationMetadata(); + typedElement.getGenericType().getAnnotationMetadata(); + } + @Override public void visitField(FieldElement element, VisitorContext context) { visit(element); diff --git a/inject-java/src/test/groovy/io/micronaut/visitors/PropertyElementSpec.groovy b/inject-java/src/test/groovy/io/micronaut/visitors/PropertyElementSpec.groovy index c63a2abcaf2..0d0f45a3550 100644 --- a/inject-java/src/test/groovy/io/micronaut/visitors/PropertyElementSpec.groovy +++ b/inject-java/src/test/groovy/io/micronaut/visitors/PropertyElementSpec.groovy @@ -15,8 +15,8 @@ */ package io.micronaut.visitors -import io.micronaut.http.annotation.Get import io.micronaut.annotation.processing.test.AbstractTypeElementSpec +import io.micronaut.http.annotation.Get import io.micronaut.inject.ast.ClassElement import spock.lang.IgnoreIf import spock.util.environment.Jvm @@ -199,4 +199,167 @@ class Response { AllElementsVisitor.VISITED_METHOD_ELEMENTS[1].returnType.beanProperties.size() == 1 AllElementsVisitor.VISITED_METHOD_ELEMENTS[1].returnType.beanProperties[0].type.name == 'java.lang.Integer' } + + void "test get annotations from type after bean properties "() { + buildBeanDefinition('test.TestController', ''' +package test; + +import io.micronaut.core.annotation.Introspected; +import io.micronaut.http.annotation.Body; +import io.micronaut.http.annotation.Controller; +import io.micronaut.http.annotation.Post; + +@Controller +class TestController { + + @Post("/path") + public void processSync(@Body MyDto dto) { + } +} + +class MyDto { + + private Parameters parameters; + + public Parameters getParameters() { + return parameters; + } + + public void setParameters(Parameters parameters) { + this.parameters = parameters; + } +} + +@Introspected +class Parameters { + + private Integer stampWidth; + private Integer stampHeight; + private int pageNumber; + + public Integer getStampWidth() { + return stampWidth; + } + + public void setStampWidth(Integer stampWidth) { + this.stampWidth = stampWidth; + } + + public Integer getStampHeight() { + return stampHeight; + } + + public void setStampHeight(Integer stampHeight) { + this.stampHeight = stampHeight; + } + + public int getPageNumber() { + return pageNumber; + } + + public void setPageNumber(int pageNumber) { + this.pageNumber = pageNumber; + } +} +''') + expect: + AllElementsVisitor.VISITED_CLASS_ELEMENTS.size() == 1 + + def method = AllElementsVisitor.VISITED_METHOD_ELEMENTS[0] + def parameter = method.parameters[0] + + def beanProperty = parameter.type.beanProperties.get(0) + beanProperty.type.annotationNames.sort() == [ + 'io.micronaut.context.annotation.BeanProperties', + 'io.micronaut.core.annotation.Introspected' + ] + beanProperty.field.get().type.annotationNames.sort() == [ + 'io.micronaut.context.annotation.BeanProperties', + 'io.micronaut.core.annotation.Introspected' + ] + beanProperty.readMethod.get().returnType.annotationNames.sort() == [ + 'io.micronaut.context.annotation.BeanProperties', + 'io.micronaut.core.annotation.Introspected' + ] + beanProperty.writeMethod.get().parameters[0].type.annotationNames.sort() == [ + 'io.micronaut.context.annotation.BeanProperties', + 'io.micronaut.core.annotation.Introspected' + ] + } + + void "test get annotations from type after bean properties for field access"() { + buildBeanDefinition('test.TestController', ''' +package test; + +import io.micronaut.core.annotation.Introspected; +import io.micronaut.http.annotation.Body; +import io.micronaut.http.annotation.Controller; +import io.micronaut.http.annotation.Post; + +@Controller +class TestController { + + @Post("/path") + public void processSync(@Body MyDto dto) { + } +} + +@Introspected(accessKind = Introspected.AccessKind.FIELD) +class MyDto { + + public Parameters parameters; +} + +@Introspected +class Parameters { + + private Integer stampWidth; + private Integer stampHeight; + private int pageNumber; + + public Integer getStampWidth() { + return stampWidth; + } + + public void setStampWidth(Integer stampWidth) { + this.stampWidth = stampWidth; + } + + public Integer getStampHeight() { + return stampHeight; + } + + public void setStampHeight(Integer stampHeight) { + this.stampHeight = stampHeight; + } + + public int getPageNumber() { + return pageNumber; + } + + public void setPageNumber(int pageNumber) { + this.pageNumber = pageNumber; + } +} +''') + expect: + AllElementsVisitor.VISITED_CLASS_ELEMENTS.size() == 1 + + def method = AllElementsVisitor.VISITED_METHOD_ELEMENTS[0] + def parameter = method.parameters[0] + + def beanProperty = parameter.type.beanProperties.get(0) + beanProperty.type.annotationNames.sort() == [ + 'io.micronaut.context.annotation.BeanProperties', + 'io.micronaut.core.annotation.Introspected' + ] + beanProperty.field.get().type.annotationNames.sort() == [ + 'io.micronaut.context.annotation.BeanProperties', + 'io.micronaut.core.annotation.Introspected' + ] + beanProperty.field.get().genericType.annotationNames.sort() == [ + 'io.micronaut.context.annotation.BeanProperties', + 'io.micronaut.core.annotation.Introspected' + ] + } } diff --git a/inject/src/main/java/io/micronaut/inject/annotation/AbstractAnnotationMetadataBuilder.java b/inject/src/main/java/io/micronaut/inject/annotation/AbstractAnnotationMetadataBuilder.java index ca4e2eff90c..2b31fc7751d 100644 --- a/inject/src/main/java/io/micronaut/inject/annotation/AbstractAnnotationMetadataBuilder.java +++ b/inject/src/main/java/io/micronaut/inject/annotation/AbstractAnnotationMetadataBuilder.java @@ -215,7 +215,7 @@ public AnnotationMetadata buildDeclared(T element, List annotations * @return The {@link AnnotationMetadata} */ public CachedAnnotationMetadata lookupOrBuildForParameter(T owningType, T methodElement, T parameterElement) { - return lookupOrBuild(true, owningType, methodElement, parameterElement); + return lookupOrBuild(false, owningType, methodElement, parameterElement); } /** diff --git a/inject/src/main/java/io/micronaut/inject/ast/utils/AstBeanPropertiesUtils.java b/inject/src/main/java/io/micronaut/inject/ast/utils/AstBeanPropertiesUtils.java index e49d8ac81c0..a5320d80d50 100644 --- a/inject/src/main/java/io/micronaut/inject/ast/utils/AstBeanPropertiesUtils.java +++ b/inject/src/main/java/io/micronaut/inject/ast/utils/AstBeanPropertiesUtils.java @@ -26,7 +26,6 @@ import io.micronaut.inject.ast.FieldElement; import io.micronaut.inject.ast.MemberElement; import io.micronaut.inject.ast.MethodElement; -import io.micronaut.inject.ast.ParameterElement; import io.micronaut.inject.ast.PrimitiveElement; import io.micronaut.inject.ast.PropertyElement; @@ -145,10 +144,7 @@ public static List resolveBeanProperties(BeanPropertiesQuery co } else if (value.writeAccessKind == BeanProperties.AccessKind.METHOD && value.setter != null && value.setter.getParameters().length > 0) { - ParameterElement parameter = value.setter.getParameters()[0]; - if (!parameter.getType().equals(value.type)) { - value.type = parameter.getGenericType(); - } + value.type = value.setter.getParameters()[0].getGenericType(); } if (value.readAccessKind != null || value.writeAccessKind != null) { @@ -327,14 +323,11 @@ private static boolean canMethodBeUsedForAccess(MethodElement methodElement, } private static boolean isAccessible(MemberElement memberElement, BeanProperties.Visibility visibility) { - switch (visibility) { - case DEFAULT: - return !memberElement.isPrivate() && (memberElement.isAccessible() || memberElement.getDeclaringType().hasDeclaredStereotype(BeanProperties.class)); - case PUBLIC: - return memberElement.isPublic(); - default: - return false; - } + return switch (visibility) { + case DEFAULT -> + !memberElement.isPrivate() && (memberElement.isAccessible() || memberElement.getDeclaringType().hasDeclaredStereotype(BeanProperties.class)); + case PUBLIC -> memberElement.isPublic(); + }; } private static boolean shouldExclude(Set includes, Set excludes, String propertyName) { From 3d7dc765538b63ccc80b14d88523c28425c0f3ef Mon Sep 17 00:00:00 2001 From: Denis Stepanov Date: Tue, 25 Oct 2022 21:38:11 +0400 Subject: [PATCH 150/743] Select possibly the property field's type if it has type annotations (#8225) --- .../processing/JavaAnnotationMetadataBuilder.java | 2 +- .../AbstractAnnotationMetadataBuilder.java | 4 +++- .../inject/ast/utils/AstBeanPropertiesUtils.java | 13 ++++++++++++- 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/inject-java/src/main/java/io/micronaut/annotation/processing/JavaAnnotationMetadataBuilder.java b/inject-java/src/main/java/io/micronaut/annotation/processing/JavaAnnotationMetadataBuilder.java index 361ef5de9d1..ff175ef767a 100644 --- a/inject-java/src/main/java/io/micronaut/annotation/processing/JavaAnnotationMetadataBuilder.java +++ b/inject-java/src/main/java/io/micronaut/annotation/processing/JavaAnnotationMetadataBuilder.java @@ -242,7 +242,7 @@ protected Element getTypeForAnnotation(AnnotationMirror annotationMirror) { @Override protected List getAnnotationsForType(Element element) { List annotationMirrors = new ArrayList<>(element.getAnnotationMirrors()); - annotationMirrors.removeIf(mirror -> getAnnotationTypeName(mirror).equals(AnnotationUtil.KOTLIN_METADATA)); + annotationMirrors.removeIf(mirror -> EXCLUDES.contains(getAnnotationTypeName(mirror))); List expanded = new ArrayList<>(annotationMirrors.size()); for (AnnotationMirror annotation : annotationMirrors) { boolean repeatable = false; diff --git a/inject/src/main/java/io/micronaut/inject/annotation/AbstractAnnotationMetadataBuilder.java b/inject/src/main/java/io/micronaut/inject/annotation/AbstractAnnotationMetadataBuilder.java index 2b31fc7751d..9797c497988 100644 --- a/inject/src/main/java/io/micronaut/inject/annotation/AbstractAnnotationMetadataBuilder.java +++ b/inject/src/main/java/io/micronaut/inject/annotation/AbstractAnnotationMetadataBuilder.java @@ -82,9 +82,11 @@ public abstract class AbstractAnnotationMetadataBuilder { private static final Map, CachedAnnotationMetadata> MUTATED_ANNOTATION_METADATA = new HashMap<>(100); private static final Map> NON_BINDING_CACHE = new HashMap<>(50); private static final List DEFAULT_ANNOTATE_EXCLUDES = Arrays.asList(Internal.class.getName(), - Experimental.class.getName()); + Experimental.class.getName(), "jdk.internal.ValueBased"); private static final Map> ANNOTATION_DEFAULTS = new HashMap<>(20); + protected static final List EXCLUDES = Arrays.asList(AnnotationUtil.KOTLIN_METADATA, "jdk.internal.ValueBased"); + static { for (AnnotationMapper mapper : SoftServiceLoader.load(AnnotationMapper.class, AbstractAnnotationMetadataBuilder.class.getClassLoader()) .disableFork().collectAll()) { diff --git a/inject/src/main/java/io/micronaut/inject/ast/utils/AstBeanPropertiesUtils.java b/inject/src/main/java/io/micronaut/inject/ast/utils/AstBeanPropertiesUtils.java index a5320d80d50..ff2257417dd 100644 --- a/inject/src/main/java/io/micronaut/inject/ast/utils/AstBeanPropertiesUtils.java +++ b/inject/src/main/java/io/micronaut/inject/ast/utils/AstBeanPropertiesUtils.java @@ -146,7 +146,14 @@ public static List resolveBeanProperties(BeanPropertiesQuery co && value.setter.getParameters().length > 0) { value.type = value.setter.getParameters()[0].getGenericType(); } - + // In a case when the field's type is the same as the selected property type, + // and it has more type arguments annotations - use it as the property type + if (value.field != null + && value.field.getType().equals(value.type) + && hasGenericTypeAnnotations(value.field.getType()) + && !hasGenericTypeAnnotations(value.type.getType())) { + value.type = value.field.getGenericType(); + } if (value.readAccessKind != null || value.writeAccessKind != null) { value.isExcluded = shouldExclude(includes, excludes, propertyName) || isExcludedByAnnotations(configuration, value) @@ -162,6 +169,10 @@ public static List resolveBeanProperties(BeanPropertiesQuery co return Collections.emptyList(); } + private static boolean hasGenericTypeAnnotations(ClassElement cl) { + return cl.getTypeArguments().values().stream().anyMatch(t -> !t.getAnnotationMetadata().isEmpty()); + } + private static boolean isExcludedBecauseOfMissingAccess(BeanPropertyData value) { if (value.readAccessKind == BeanProperties.AccessKind.METHOD && value.getter == null From 5ca9a536ea6c9ef0d06b209405dd2f7dd1aee24c Mon Sep 17 00:00:00 2001 From: Denis Stepanov Date: Wed, 26 Oct 2022 12:35:02 +0400 Subject: [PATCH 151/743] HTTP Netty server executing route refactoring (#8217) --- .../CompletableFutureExecutionFlow.java | 45 + .../CompletableFutureExecutionFlowImpl.java | 88 ++ .../core/execution/ExecutionFlow.java | 170 ++++ .../execution/ImperativeExecutionFlow.java | 52 ++ .../ImperativeExecutionFlowImpl.java | 143 +++ .../server/netty/RoutingInBoundHandler.java | 584 ++++-------- .../NettyServerWebSocketUpgradeHandler.java | 140 ++- .../server/netty/MaxRequestSizeSpec.groovy | 3 +- .../http/server/netty/errors/ErrorSpec.groovy | 23 +- .../netty/stack/InvocationStackSpec.groovy | 253 ++++++ .../threading/ThreadSelectionSpec.groovy | 44 + .../micronaut/http/server/RouteExecutor.java | 833 +++++++++++------- .../inject/MethodExecutionHandle.java | 2 +- .../json/codec/MapperMediaTypeCodec.java | 11 +- .../execution/ReactiveExecutionFlow.java | 91 ++ .../execution/ReactorExecutionFlowImpl.java | 151 ++++ .../server/suspend/SuspendControllerSpec.kt | 17 +- 17 files changed, 1822 insertions(+), 828 deletions(-) create mode 100644 core/src/main/java/io/micronaut/core/execution/CompletableFutureExecutionFlow.java create mode 100644 core/src/main/java/io/micronaut/core/execution/CompletableFutureExecutionFlowImpl.java create mode 100644 core/src/main/java/io/micronaut/core/execution/ExecutionFlow.java create mode 100644 core/src/main/java/io/micronaut/core/execution/ImperativeExecutionFlow.java create mode 100644 core/src/main/java/io/micronaut/core/execution/ImperativeExecutionFlowImpl.java create mode 100644 http-server-netty/src/test/groovy/io/micronaut/http/server/netty/stack/InvocationStackSpec.groovy create mode 100644 runtime/src/main/java/io/micronaut/reactive/reactor/execution/ReactiveExecutionFlow.java create mode 100644 runtime/src/main/java/io/micronaut/reactive/reactor/execution/ReactorExecutionFlowImpl.java diff --git a/core/src/main/java/io/micronaut/core/execution/CompletableFutureExecutionFlow.java b/core/src/main/java/io/micronaut/core/execution/CompletableFutureExecutionFlow.java new file mode 100644 index 00000000000..0274572e7e2 --- /dev/null +++ b/core/src/main/java/io/micronaut/core/execution/CompletableFutureExecutionFlow.java @@ -0,0 +1,45 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.core.execution; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.NonNull; + +import java.util.concurrent.CompletableFuture; + +/** + * The completable future execution flow. + * + * @param The value type + * @author Denis Stepnov + * @since 4.0.0 + */ +@Internal +public interface CompletableFutureExecutionFlow extends ExecutionFlow { + + /** + * Create a completable future flow representing a value. + * + * @param value The value + * @param The value type + * @return a new flow + */ + @NonNull + static ExecutionFlow just(@NonNull CompletableFuture value) { + return (ExecutionFlow) new CompletableFutureExecutionFlowImpl((CompletableFuture) value); + } + +} diff --git a/core/src/main/java/io/micronaut/core/execution/CompletableFutureExecutionFlowImpl.java b/core/src/main/java/io/micronaut/core/execution/CompletableFutureExecutionFlowImpl.java new file mode 100644 index 00000000000..546a7e24abb --- /dev/null +++ b/core/src/main/java/io/micronaut/core/execution/CompletableFutureExecutionFlowImpl.java @@ -0,0 +1,88 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.core.execution; + +import io.micronaut.core.annotation.Internal; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; +import java.util.function.BiConsumer; +import java.util.function.Function; +import java.util.function.Supplier; + +/** + * The completable future execution flow implementation. + * + * @author Denis Stepanov + * @since 4.0.0 + */ +@Internal +final class CompletableFutureExecutionFlowImpl implements CompletableFutureExecutionFlow { + + private CompletableFuture stage; + + CompletableFutureExecutionFlowImpl(CompletableFuture stage) { + this.stage = stage; + } + + @Override + public ExecutionFlow flatMap(Function> transformer) { + stage = stage.thenCompose(value -> { + if (value != null) { + return (CompletionStage) transformer.apply(value).toCompletableFuture(); + } + return CompletableFuture.completedFuture(null); + }); + return (ExecutionFlow) this; + } + + @Override + public ExecutionFlow then(Supplier> supplier) { + stage = stage.thenCompose(value -> (CompletionStage) supplier.get().toCompletableFuture()); + return (ExecutionFlow) this; + } + + @Override + public ExecutionFlow map(Function function) { + stage = stage.thenApply(function::apply); + return (ExecutionFlow) this; + } + + @Override + public ExecutionFlow onErrorResume(Function> fallback) { + stage = stage.exceptionallyCompose(throwable -> (CompletionStage) fallback.apply(throwable).toCompletableFuture()); + return this; + } + + @Override + public ExecutionFlow putInContext(String key, Object value) { + return this; + } + + @Override + public void onComplete(BiConsumer fn) { + stage.handle((o, throwable) -> { + fn.accept(o, throwable); + return null; + }); + } + + @Override + public CompletableFuture toCompletableFuture() { + return stage; + } + +} diff --git a/core/src/main/java/io/micronaut/core/execution/ExecutionFlow.java b/core/src/main/java/io/micronaut/core/execution/ExecutionFlow.java new file mode 100644 index 00000000000..2dc2796258d --- /dev/null +++ b/core/src/main/java/io/micronaut/core/execution/ExecutionFlow.java @@ -0,0 +1,170 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.core.execution; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.NonNull; +import io.micronaut.core.annotation.Nullable; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Executor; +import java.util.function.BiConsumer; +import java.util.function.Function; +import java.util.function.Supplier; + +/** + * The execution flow class represents a data flow which state can be represented as a simple imperative flow or an async/reactive. + * The state can be resolved or lazy - based on the implementation. + * NOTE: The instance of the flow is not supposed to be used after a mapping operator is used. + * + * @param The flow type + * @author Denis Stepanov + * @since 4.0.0 + */ +@Internal +public interface ExecutionFlow { + + /** + * Create a simple flow representing a value. + * + * @param value The value + * @param The value type + * @return a new flow + */ + @NonNull + static ExecutionFlow just(@Nullable K value) { + return (ExecutionFlow) new ImperativeExecutionFlowImpl(value, null); + } + + /** + * Create a simple flow representing an error. + * + * @param e The exception + * @param The value type + * @return a new flow + */ + @NonNull + static ExecutionFlow error(@NonNull Throwable e) { + return (ExecutionFlow) new ImperativeExecutionFlowImpl(null, e); + } + + /** + * Create a simple flow representing an empty state. + * + * @param The flow value type + * @return a new flow + */ + @NonNull + static ExecutionFlow empty() { + return (ExecutionFlow) new ImperativeExecutionFlowImpl(null, null); + } + + /** + * Create a flow by invoking a supplier asynchronously. + * + * @param executor The executor + * @param supplier The supplier + * @param The flow value type + * @return a new flow + */ + @NonNull + static ExecutionFlow async(@NonNull Executor executor, @NonNull Supplier> supplier) { + CompletableFuture completableFuture = new CompletableFuture<>(); + executor.execute(() -> supplier.get().onComplete((t, throwable) -> { + if (throwable != null) { + completableFuture.completeExceptionally(throwable); + } else { + completableFuture.complete(t); + } + })); + return CompletableFutureExecutionFlow.just(completableFuture); + } + + /** + * Map a not-empty value. + * + * @param transformer The value transformer + * @param New value Type + * @return a new flow + */ + @NonNull + ExecutionFlow map(@NonNull Function transformer); + + /** + * Map a not-empty value to a new flow. + * + * @param transformer The value transformer + * @param New value Type + * @return a new flow + */ + @NonNull + ExecutionFlow flatMap(@NonNull Function> transformer); + + /** + * Supply a new flow after the existing flow value is resolved. + * + * @param supplier The supplier + * @param New value Type + * @return a new flow + */ + @NonNull + ExecutionFlow then(@NonNull Supplier> supplier); + + /** + * Supply a new flow if the existing flow is erroneous. + * + * @param fallback The fallback + * @return a new flow + */ + @NonNull + ExecutionFlow onErrorResume(@NonNull Function> fallback); + + /** + * Store a contextual value. + * + * @param key The key + * @param value The value + * @return a new flow + */ + @NonNull + ExecutionFlow putInContext(@NonNull String key, @NonNull Object value); + + /** + * Invokes a provided function when the flow is resolved. + * + * @param fn The function + */ + void onComplete(@NonNull BiConsumer fn); + + /** + * Converts the existing flow into the completable future. + * + * @return a {@link CompletableFuture} that represents the state if this flow. + */ + @NonNull + default CompletableFuture toCompletableFuture() { + CompletableFuture completableFuture = new CompletableFuture<>(); + onComplete((value, throwable) -> { + if (throwable != null) { + CompletableFuture.failedFuture(throwable); + } + CompletableFuture.completedFuture(value); + }); + return completableFuture; + } + +} + diff --git a/core/src/main/java/io/micronaut/core/execution/ImperativeExecutionFlow.java b/core/src/main/java/io/micronaut/core/execution/ImperativeExecutionFlow.java new file mode 100644 index 00000000000..a676249e8e3 --- /dev/null +++ b/core/src/main/java/io/micronaut/core/execution/ImperativeExecutionFlow.java @@ -0,0 +1,52 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.core.execution; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.NonNull; +import io.micronaut.core.annotation.Nullable; + +import java.util.Map; + +/** + * The imperative execution flow. + * + * @param The value type + * @author Denis Stepnov + * @since 4.0.0 + */ +@Internal +public interface ImperativeExecutionFlow extends ExecutionFlow { + + /** + * @return The value if present + */ + @Nullable + T getValue(); + + /** + * @return The exception if present + */ + @Nullable + Throwable getError(); + + /** + * @return The context if present + */ + @NonNull + Map getContext(); + +} diff --git a/core/src/main/java/io/micronaut/core/execution/ImperativeExecutionFlowImpl.java b/core/src/main/java/io/micronaut/core/execution/ImperativeExecutionFlowImpl.java new file mode 100644 index 00000000000..4e926dd999b --- /dev/null +++ b/core/src/main/java/io/micronaut/core/execution/ImperativeExecutionFlowImpl.java @@ -0,0 +1,143 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.core.execution; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.Nullable; + +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.function.BiConsumer; +import java.util.function.Function; +import java.util.function.Supplier; + +/** + * The imperative execution flow implementation. + * + * @author Denis Stepanov + * @since 4.0.0 + */ +@Internal +final class ImperativeExecutionFlowImpl implements ImperativeExecutionFlow { + + @Nullable + private Object value; + @Nullable + private Throwable error; + @Nullable + private Map context; + + ImperativeExecutionFlowImpl(T value, Throwable error) { + this.value = value; + this.error = error; + } + + @Nullable + @Override + public Object getValue() { + return value; + } + + @Nullable + @Override + public Throwable getError() { + return error; + } + + @Override + public Map getContext() { + return context == null ? Collections.emptyMap() : context; + } + + @Override + public ExecutionFlow flatMap(Function> transformer) { + if (error == null) { + try { + if (value != null) { + return (ExecutionFlow) transformer.apply(value); + } + } catch (Throwable e) { + error = e; + value = null; + } + } + return (ExecutionFlow) this; + } + + @Override + public ExecutionFlow then(Supplier> supplier) { + if (error == null) { + try { + return (ExecutionFlow) supplier.get(); + } catch (Throwable e) { + error = e; + value = null; + } + } + return (ExecutionFlow) this; + } + + @Override + public ExecutionFlow map(Function transformer) { + if (error == null) { + try { + value = transformer.apply(value); + } catch (Throwable e) { + error = e; + value = null; + } + } + return (ExecutionFlow) this; + } + + @Override + public ExecutionFlow onErrorResume(Function> fallback) { + if (error != null) { + try { + return (ExecutionFlow) fallback.apply(error); + } catch (Throwable e) { + error = e; + value = null; + } + } + return this; + } + + @Override + public ExecutionFlow putInContext(String key, Object value) { + if (context == null) { + context = new LinkedHashMap<>(); + } + context.put(key, value); + return this; + } + + @Override + public void onComplete(BiConsumer fn) { + fn.accept(value, error); + } + + @Override + public CompletableFuture toCompletableFuture() { + if (error != null) { + return CompletableFuture.failedFuture(error); + } + return CompletableFuture.completedFuture(value); + } + +} diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/RoutingInBoundHandler.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/RoutingInBoundHandler.java index 41c9c9dca87..1aa25b728bb 100644 --- a/http-server-netty/src/main/java/io/micronaut/http/server/netty/RoutingInBoundHandler.java +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/RoutingInBoundHandler.java @@ -23,12 +23,12 @@ import io.micronaut.core.async.publisher.Publishers; import io.micronaut.core.async.subscriber.CompletionAwareSubscriber; import io.micronaut.core.convert.ConversionService; +import io.micronaut.core.execution.ExecutionFlow; import io.micronaut.core.io.Writable; import io.micronaut.core.io.buffer.ByteBuffer; import io.micronaut.core.io.buffer.ReferenceCounted; import io.micronaut.core.reflect.ClassUtils; import io.micronaut.core.type.Argument; -import io.micronaut.core.util.CollectionUtils; import io.micronaut.http.HttpAttributes; import io.micronaut.http.HttpHeaders; import io.micronaut.http.HttpMethod; @@ -52,8 +52,6 @@ import io.micronaut.http.server.RouteExecutor; import io.micronaut.http.server.binding.RequestArgumentSatisfier; import io.micronaut.http.server.exceptions.InternalServerException; -import io.micronaut.http.server.exceptions.response.ErrorContext; -import io.micronaut.http.server.exceptions.response.ErrorResponseProcessor; import io.micronaut.http.server.netty.configuration.NettyHttpServerConfiguration; import io.micronaut.http.server.netty.multipart.NettyCompletedFileUpload; import io.micronaut.http.server.netty.multipart.NettyPartData; @@ -63,13 +61,11 @@ import io.micronaut.http.server.netty.types.files.NettyStreamedFileCustomizableResponseType; import io.micronaut.http.server.netty.types.files.NettySystemFileCustomizableResponseType; import io.micronaut.http.server.types.files.FileCustomizableResponseType; +import io.micronaut.reactive.reactor.execution.ReactiveExecutionFlow; import io.micronaut.runtime.http.codec.TextPlainCodec; -import io.micronaut.web.router.MethodBasedRouteMatch; import io.micronaut.web.router.RouteInfo; import io.micronaut.web.router.RouteMatch; import io.micronaut.web.router.Router; -import io.micronaut.web.router.UriRouteMatch; -import io.micronaut.web.router.exceptions.DuplicateRouteException; import io.micronaut.web.router.resource.StaticResourceResolver; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufHolder; @@ -120,10 +116,8 @@ import java.util.ArrayList; import java.util.Collection; import java.util.Collections; -import java.util.HashSet; import java.util.List; import java.util.Optional; -import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutorService; import java.util.concurrent.atomic.AtomicBoolean; @@ -133,9 +127,6 @@ import java.util.function.LongConsumer; import java.util.function.Supplier; import java.util.regex.Pattern; -import java.util.stream.Collectors; - -import static io.micronaut.http.HttpAttributes.AVAILABLE_HTTP_METHODS; /** * Internal implementation of the {@link io.netty.channel.ChannelInboundHandler} for Micronaut. @@ -146,20 +137,19 @@ @Internal @Sharable @SuppressWarnings("FileLength") -class RoutingInBoundHandler extends SimpleChannelInboundHandler> { +class RoutingInBoundHandler extends SimpleChannelInboundHandler> implements RouteExecutor.StaticResourceResponseFinder { private static final Logger LOG = LoggerFactory.getLogger(RoutingInBoundHandler.class); /* * Also present in {@link RouteExecutor}. */ private static final Pattern IGNORABLE_ERROR_MESSAGE = Pattern.compile( - "^.*(?:connection (?:reset|closed|abort|broken)|broken pipe).*$", Pattern.CASE_INSENSITIVE); + "^.*(?:connection (?:reset|closed|abort|broken)|broken pipe).*$", Pattern.CASE_INSENSITIVE); private static final Argument ARGUMENT_PART_DATA = Argument.of(PartData.class); private final Router router; private final StaticResourceResolver staticResourceResolver; private final NettyHttpServerConfiguration serverConfiguration; private final HttpContentProcessorResolver httpContentProcessorResolver; - private final ErrorResponseProcessor errorResponseProcessor; private final RequestArgumentSatisfier requestArgumentSatisfier; private final MediaTypeCodecRegistry mediaTypeCodecRegistry; private final NettyCustomizableResponseTypeHandlerRegistry customizableResponseTypeHandlerRegistry; @@ -178,12 +168,12 @@ class RoutingInBoundHandler extends SimpleChannelInboundHandler ioExecutor, - HttpContentProcessorResolver httpContentProcessorResolver, - ApplicationEventPublisher terminateEventPublisher) { + NettyHttpServerConfiguration serverConfiguration, + NettyCustomizableResponseTypeHandlerRegistry customizableResponseTypeHandlerRegistry, + NettyEmbeddedServices embeddedServerContext, + Supplier ioExecutor, + HttpContentProcessorResolver httpContentProcessorResolver, + ApplicationEventPublisher terminateEventPublisher) { this.mediaTypeCodecRegistry = embeddedServerContext.getMediaTypeCodecRegistry(); this.customizableResponseTypeHandlerRegistry = customizableResponseTypeHandlerRegistry; this.staticResourceResolver = embeddedServerContext.getStaticResourceResolver(); @@ -192,7 +182,6 @@ class RoutingInBoundHandler extends SimpleChannelInboundHandler multipartEnabled = serverConfiguration.getMultipart().getEnabled(); this.multipartEnabled = !multipartEnabled.isPresent() || multipartEnabled.get(); @@ -277,362 +266,120 @@ public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { ctx.writeAndFlush(new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.INTERNAL_SERVER_ERROR)); return; } - ServerRequestContext.set(nettyHttpRequest); - filterAndEncodeResponse( - ctx, - nettyHttpRequest, - routeExecutor.onError(cause, nettyHttpRequest)); + routeExecutor.filterPublisher(new AtomicReference<>(nettyHttpRequest), () -> routeExecutor.onError(cause, nettyHttpRequest)) + .onComplete((response, throwable) -> writeResponse(ctx, nettyHttpRequest, response, throwable)); } @Override - protected void channelRead0(ChannelHandlerContext ctx, io.micronaut.http.HttpRequest request) { + protected void channelRead0(ChannelHandlerContext ctx, io.micronaut.http.HttpRequest httpRequest) { + NettyHttpRequest nettyHttpRequest = (NettyHttpRequest) httpRequest; + ctx.channel().config().setAutoRead(false); - io.micronaut.http.HttpMethod httpMethod = request.getMethod(); - String requestPath = request.getUri().getPath(); - ServerRequestContext.set(request); if (LOG.isDebugEnabled()) { - LOG.debug("Request {} {}", httpMethod, request.getUri()); + HttpMethod httpMethod = httpRequest.getMethod(); + ServerRequestContext.set(httpRequest); + LOG.debug("Request {} {}", httpMethod, httpRequest.getUri()); } - NettyHttpRequest nettyHttpRequest = (NettyHttpRequest) request; io.netty.handler.codec.http.HttpRequest nativeRequest = nettyHttpRequest.getNativeRequest(); + + RouteExecutor.RequestBodyReader requestBodyReader = (routeMatch, hr) -> { + // handle decoding failure + DecoderResult decoderResult = nativeRequest.decoderResult(); + if (decoderResult.isFailure()) { + return ExecutionFlow.error(decoderResult.cause()); + } + // try to fulfill the argument requirements of the route + RouteMatch route = requestArgumentSatisfier.fulfillArgumentRequirements(routeMatch, httpRequest, false); + + Optional> bodyArgument = route.getBodyArgument() + .filter(argument -> argument.getAnnotationMetadata().hasAnnotation(Body.class)); + + // The request body is required, so at this point we must have a StreamedHttpRequest + io.netty.handler.codec.http.HttpRequest nativeRequest1 = nettyHttpRequest.getNativeRequest(); + if (!route.isExecutable() && + HttpMethod.permitsRequestBody(nettyHttpRequest.getMethod()) && + nativeRequest1 instanceof StreamedHttpRequest && + (bodyArgument.isEmpty() || !route.isSatisfied(bodyArgument.get().getName()))) { + return ReactiveExecutionFlow.fromPublisher( + Mono.create(emitter -> httpContentProcessorResolver.resolve(nettyHttpRequest, route) + .subscribe(buildSubscriber(nettyHttpRequest, route, emitter)) + )); + } + ctx.read(); + return ExecutionFlow.just(route); + }; + + ExecutionFlow> responseFlow; + // handle decoding failure DecoderResult decoderResult = nativeRequest.decoderResult(); if (decoderResult.isFailure()) { Throwable cause = decoderResult.cause(); HttpStatus status = cause instanceof TooLongFrameException ? HttpStatus.REQUEST_ENTITY_TOO_LARGE : HttpStatus.BAD_REQUEST; - handleStatusError( - ctx, - nettyHttpRequest, - HttpResponse.status(status), - status.getReason() + responseFlow = routeExecutor.onStatusError( + requestBodyReader, + httpRequest, + HttpResponse.status(status), + status.getReason() ); - return; - } - - MediaType contentType = request.getContentType().orElse(null); - final String requestMethodName = request.getMethodName(); - - if (!multipartEnabled && - contentType != null && - contentType.equals(MediaType.MULTIPART_FORM_DATA_TYPE)) { - if (LOG.isDebugEnabled()) { - LOG.debug("Multipart uploads have been disabled via configuration. Rejected request for URI {}, method {}, and content type {}", request.getUri(), - requestMethodName, contentType); - } - - handleStatusError( - ctx, - nettyHttpRequest, - HttpResponse.status(HttpStatus.UNSUPPORTED_MEDIA_TYPE), - "Content Type [" + contentType + "] not allowed"); - return; - } - - UriRouteMatch routeMatch = null; - - List> uriRoutes = router.findAllClosest(request); - - if (uriRoutes.size() > 1) { - throw new DuplicateRouteException(requestPath, uriRoutes); - } else if (uriRoutes.size() == 1) { - routeMatch = uriRoutes.get(0); - setRouteAttributes(request, routeMatch); - } - - if (routeMatch == null && request.getMethod().equals(HttpMethod.OPTIONS)) { - List> anyUriRoutes = router.findAny(request.getUri().toString(), request) - .collect(Collectors.toList()); - if (!anyUriRoutes.isEmpty()) { - setRouteAttributes(request, anyUriRoutes.get(0)); - request.setAttribute(AVAILABLE_HTTP_METHODS, anyUriRoutes.stream().map(UriRouteMatch::getHttpMethod).collect(Collectors.toList())); - } - } - - RouteMatch route; - - if (routeMatch == null) { - - //Check if there is a file for the route before returning route not found - Optional optionalFile = matchFile(requestPath); - - if (optionalFile.isPresent()) { - filterAndEncodeResponse(ctx, nettyHttpRequest, Flux.just(HttpResponse.ok(optionalFile.get()))); - return; - } - - if (LOG.isDebugEnabled()) { - LOG.debug("No matching route: {} {}", httpMethod, request.getUri()); - } - - // if there is no route present try to locate a route that matches a different HTTP method - final List> anyMatchingRoutes = router - .findAny(request.getUri().toString(), request) - .collect(Collectors.toList()); - final Collection acceptedTypes = request.accept(); - final boolean hasAcceptHeader = CollectionUtils.isNotEmpty(acceptedTypes); - - Set acceptableContentTypes = contentType != null ? new HashSet<>(5) : null; - Set allowedMethods = new HashSet<>(5); - Set produceableContentTypes = hasAcceptHeader ? new HashSet<>(5) : null; - for (UriRouteMatch anyRoute : anyMatchingRoutes) { - final String routeMethod = anyRoute.getRoute().getHttpMethodName(); - if (!requestMethodName.equals(routeMethod)) { - allowedMethods.add(routeMethod); - } - if (contentType != null && !anyRoute.doesConsume(contentType)) { - acceptableContentTypes.addAll(anyRoute.getRoute().getConsumes()); - } - if (hasAcceptHeader && !anyRoute.doesProduce(acceptedTypes)) { - produceableContentTypes.addAll(anyRoute.getRoute().getProduces()); - } - } - - if (CollectionUtils.isNotEmpty(acceptableContentTypes)) { - if (LOG.isDebugEnabled()) { - LOG.debug("Content type not allowed for URI {}, method {}, and content type {}", request.getUri(), - requestMethodName, contentType); - } - - handleStatusError( - ctx, - nettyHttpRequest, - HttpResponse.status(HttpStatus.UNSUPPORTED_MEDIA_TYPE), - "Content Type [" + contentType + "] not allowed. Allowed types: " + acceptableContentTypes); - return; - } - - if (CollectionUtils.isNotEmpty(produceableContentTypes)) { - if (LOG.isDebugEnabled()) { - LOG.debug("Content type not allowed for URI {}, method {}, and content type {}", request.getUri(), - requestMethodName, contentType); - } - - handleStatusError( - ctx, - nettyHttpRequest, - HttpResponse.status(HttpStatus.NOT_ACCEPTABLE), - "Specified Accept Types " + acceptedTypes + " not supported. Supported types: " + produceableContentTypes); - return; - } - - if (!allowedMethods.isEmpty()) { - - if (LOG.isDebugEnabled()) { - LOG.debug("Method not allowed for URI {} and method {}", request.getUri(), requestMethodName); - } - - handleStatusError( - ctx, - nettyHttpRequest, - HttpResponse.notAllowedGeneric(allowedMethods), - "Method [" + requestMethodName + "] not allowed for URI [" + request.getUri() + "]. Allowed methods: " + allowedMethods); - return; - } else { - handleStatusError(ctx, nettyHttpRequest, HttpResponse.status(HttpStatus.NOT_FOUND), "Page Not Found"); - } - return; - } else { - route = routeMatch; + responseFlow = routeExecutor.executeRoute(requestBodyReader, httpRequest, multipartEnabled, this); } + responseFlow + .onComplete((response, throwable) -> writeResponse(ctx, nettyHttpRequest, response, throwable)); + } - if (LOG.isTraceEnabled()) { - if (route instanceof MethodBasedRouteMatch) { - LOG.trace("Matched route {} - {} to controller {}", requestMethodName, requestPath, route.getDeclaringType()); - } else { - LOG.trace("Matched route {} - {}", requestMethodName, requestPath); - } + private void writeResponse(ChannelHandlerContext ctx, + NettyHttpRequest nettyHttpRequest, + MutableHttpResponse response, + Throwable throwable) { + if (throwable != null) { + response = routeExecutor.createDefaultErrorResponse(nettyHttpRequest, throwable); } - // all ok proceed to try and execute the route - if (route.isWebSocketRoute()) { - handleStatusError( + if (response == null) { + ctx.read(); + } else { + try { + encodeHttpResponse( ctx, nettyHttpRequest, - HttpResponse.status(HttpStatus.BAD_REQUEST), - "Not a WebSocket request"); - } else { - handleRouteMatch(route, nettyHttpRequest, ctx); - } - } - - private void setRouteAttributes(HttpRequest request, UriRouteMatch route) { - request.setAttribute(HttpAttributes.ROUTE, route.getRoute()); - request.setAttribute(HttpAttributes.ROUTE_MATCH, route); - request.setAttribute(HttpAttributes.ROUTE_INFO, route); - request.setAttribute(HttpAttributes.URI_TEMPLATE, route.getRoute().getUriMatchTemplate().toString()); - } - - private void handleStatusError( - ChannelHandlerContext ctx, - NettyHttpRequest nettyHttpRequest, - MutableHttpResponse defaultResponse, - String message) { - Optional> statusRoute = router.findStatusRoute(defaultResponse.status(), nettyHttpRequest); - if (statusRoute.isPresent()) { - RouteMatch routeMatch = statusRoute.get(); - handleRouteMatch(routeMatch, nettyHttpRequest, ctx); - } else { - if (nettyHttpRequest.getMethod() != HttpMethod.HEAD) { - defaultResponse = errorResponseProcessor.processResponse(ErrorContext.builder(nettyHttpRequest) - .errorMessage(message) - .build(), defaultResponse); - if (!defaultResponse.getContentType().isPresent()) { - defaultResponse = defaultResponse.contentType(MediaType.APPLICATION_JSON_TYPE); - } - } - filterAndEncodeResponse( + response, + null, + response.body() + ); + } catch (Throwable e) { + response = routeExecutor.createDefaultErrorResponse(nettyHttpRequest, e); + encodeHttpResponse( ctx, nettyHttpRequest, - Publishers.just(defaultResponse) - ); + response, + null, + response.body() + ); + } } } - private void filterAndEncodeResponse( - ChannelHandlerContext channelContext, - NettyHttpRequest request, - Publisher> responsePublisher) { - AtomicReference> requestReference = new AtomicReference<>(request); - - Flux.from(routeExecutor.filterPublisher(requestReference, responsePublisher)) - .contextWrite(ctx -> ctx.put(ServerRequestContext.KEY, request)) - .subscribe(new Subscriber>() { - Subscription subscription; - AtomicBoolean empty = new AtomicBoolean(); - @Override - public void onSubscribe(Subscription s) { - this.subscription = s; - s.request(1); - } - - @Override - public void onNext(MutableHttpResponse response) { - empty.set(false); - encodeHttpResponse( - channelContext, - request, - response, - null, - response.body() - ); - subscription.request(1); - } - - @Override - public void onError(Throwable t) { - empty.set(false); - final MutableHttpResponse response = routeExecutor.createDefaultErrorResponse(request, t); - encodeHttpResponse( - channelContext, - request, - response, - null, - response.body() - ); - } - - @Override - public void onComplete() { - if (empty.get()) { - channelContext.read(); - } - } - }); - } - - private Optional matchFile(String path) { - Optional optionalUrl = staticResourceResolver.resolve(path); - + @Override + public FileCustomizableResponseType find(HttpRequest httpRequest) { + Optional optionalUrl = staticResourceResolver.resolve(httpRequest.getUri().getPath()); if (optionalUrl.isPresent()) { try { URL url = optionalUrl.get(); if (url.getProtocol().equals("file")) { File file = Paths.get(url.toURI()).toFile(); if (file.exists() && !file.isDirectory() && file.canRead()) { - return Optional.of(new NettySystemFileCustomizableResponseType(file)); + return new NettySystemFileCustomizableResponseType(file); } } - - return Optional.of(new NettyStreamedFileCustomizableResponseType(url)); + return new NettyStreamedFileCustomizableResponseType(url); } catch (URISyntaxException e) { //no-op } } - - return Optional.empty(); - } - - private void handleRouteMatch( - RouteMatch originalRoute, - NettyHttpRequest request, - ChannelHandlerContext context) { - - // try to fulfill the argument requirements of the route - RouteMatch route = requestArgumentSatisfier.fulfillArgumentRequirements(originalRoute, request, false); - - Optional> bodyArgument = route.getBodyArgument() - .filter(argument -> argument.getAnnotationMetadata().hasAnnotation(Body.class)); - - // The request body is required, so at this point we must have a StreamedHttpRequest - io.netty.handler.codec.http.HttpRequest nativeRequest = request.getNativeRequest(); - Flux> routeMatchPublisher; - if (!route.isExecutable() && - io.micronaut.http.HttpMethod.permitsRequestBody(request.getMethod()) && - nativeRequest instanceof StreamedHttpRequest && - (!bodyArgument.isPresent() || !route.isSatisfied(bodyArgument.get().getName()))) { - routeMatchPublisher = Mono.>create(emitter -> httpContentProcessorResolver.resolve(request, route) - .subscribe(buildSubscriber(request, route, emitter)) - ).flux(); - } else { - context.read(); - routeMatchPublisher = Flux.just(route); - } - - final Flux> routeResponse = routeExecutor.executeRoute( - request, - true, - routeMatchPublisher - ); - routeResponse - .contextWrite(ctx -> ctx.put(ServerRequestContext.KEY, request)) - .subscribe(new CompletionAwareSubscriber>() { - @Override - protected void doOnSubscribe(Subscription subscription) { - subscription.request(1); - } - - @Override - protected void doOnNext(HttpResponse message) { - encodeHttpResponse( - context, - request, - toMutableResponse(message), - (Argument) route.getBodyType(), - message.body() - ); - subscription.request(1); - } - - @Override - protected void doOnError(Throwable throwable) { - final MutableHttpResponse defaultErrorResponse = routeExecutor - .createDefaultErrorResponse(request, throwable); - encodeHttpResponse( - context, - request, - defaultErrorResponse, - (Argument) route.getBodyType(), - defaultErrorResponse.body() - ); - } - - @Override - protected void doOnComplete() { - // assume exactly one message has been sent (onNext) - } - }); + return null; } private Subscriber buildSubscriber(NettyHttpRequest request, @@ -666,11 +413,11 @@ Flux processFlowable(Sinks.Many many, HttpDataReference dataReference, b flux = flux.doOnRequest(onRequest); } return flux - .doAfterTerminate(() -> { - if (controlsFlow) { - dataReference.destroy(); - } - }); + .doAfterTerminate(() -> { + if (controlsFlow) { + dataReference.destroy(); + } + }); } @Override @@ -706,8 +453,7 @@ private void doOnNext0(Object message) { boolean executed = this.executed.get(); if (message instanceof ByteBufHolder) { - if (message instanceof HttpData) { - HttpData data = (HttpData) message; + if (message instanceof HttpData data) { if (LOG.isTraceEnabled()) { LOG.trace("Received HTTP Data for request [{}]: {}", request, message); @@ -736,8 +482,8 @@ private void doOnNext0(Object message) { Sinks.Many namedSubject = subjectsByDataName.computeIfAbsent(name, key -> makeDownstreamUnicastProcessor()); chunkedProcessing = PartData.class.equals(typeVariableType) || - Publishers.isConvertibleToPublisher(typeVariableType) || - ClassUtils.isJavaLangType(typeVariableType); + Publishers.isConvertibleToPublisher(typeVariableType) || + ClassUtils.isJavaLangType(typeVariableType); if (Publishers.isConvertibleToPublisher(typeVariableType)) { boolean streamingFileUpload = StreamingFileUpload.class.isAssignableFrom(typeVariableType); @@ -752,10 +498,10 @@ private void doOnNext0(Object message) { Flux flowable = processFlowable(childSubject, dataReference, true); if (streamingFileUpload && data instanceof FileUpload) { namedSubject.tryEmitNext(new NettyStreamingFileUpload( - (FileUpload) data, - serverConfiguration.getMultipart(), - getIoExecutor(), - (Flux) flowable)); + (FileUpload) data, + serverConfiguration.getMultipart(), + getIoExecutor(), + (Flux) flowable)); } else { namedSubject.tryEmitNext(flowable); } @@ -794,14 +540,14 @@ private void doOnNext0(Object message) { } if (data instanceof FileUpload && - StreamingFileUpload.class.isAssignableFrom(argument.getType())) { + StreamingFileUpload.class.isAssignableFrom(argument.getType())) { dataReference.upload.getAndUpdate(upload -> { if (upload == null) { return new NettyStreamingFileUpload( - (FileUpload) data, - serverConfiguration.getMultipart(), - getIoExecutor(), - (Flux) processFlowable(subject, dataReference, true)); + (FileUpload) data, + serverConfiguration.getMultipart(), + getIoExecutor(), + (Flux) processFlowable(subject, dataReference, true)); } return upload; }); @@ -931,7 +677,6 @@ private void executeRoute() { } else { return new CompletionAwareSubscriber() { private Subscription s; - private RouteMatch routeMatch = finalRoute; private AtomicBoolean executed = new AtomicBoolean(false); @Override @@ -961,7 +706,7 @@ protected void doOnError(Throwable t) { @Override protected void doOnComplete() { if (executed.compareAndSet(false, true)) { - emitter.success(routeMatch); + emitter.success(finalRoute); } } }; @@ -984,11 +729,11 @@ private ExecutorService getIoExecutor() { } private void encodeHttpResponse( - ChannelHandlerContext context, - NettyHttpRequest nettyRequest, - MutableHttpResponse response, - @Nullable Argument bodyType, - Object body) { + ChannelHandlerContext context, + NettyHttpRequest nettyRequest, + MutableHttpResponse response, + @Nullable Argument bodyType, + Object body) { boolean isNotHead = nettyRequest.getMethod() != HttpMethod.HEAD; if (isNotHead) { @@ -1002,51 +747,51 @@ private void encodeHttpResponse( response.body(byteBuf); if (!response.getContentType().isPresent()) { response.getAttribute(HttpAttributes.ROUTE_INFO, RouteInfo.class).ifPresent((routeInfo) -> - response.contentType(routeExecutor.resolveDefaultResponseContentType(nettyRequest, routeInfo))); + response.contentType(routeExecutor.resolveDefaultResponseContentType(nettyRequest, routeInfo))); } writeFinalNettyResponse( - response, - nettyRequest, - context + response, + nettyRequest, + context ); } catch (IOException e) { final MutableHttpResponse errorResponse = routeExecutor.createDefaultErrorResponse(nettyRequest, e); writeFinalNettyResponse( - errorResponse, - nettyRequest, - context + errorResponse, + nettyRequest, + context ); } }); } else if (body instanceof Publisher) { response.body(null); DelegateStreamedHttpResponse streamedResponse = new DelegateStreamedHttpResponse( - toNettyResponse(response), - mapToHttpContent(nettyRequest, response, body, context) + toNettyResponse(response), + mapToHttpContent(nettyRequest, response, body, context) ); context.writeAndFlush(streamedResponse); context.read(); } else { encodeResponseBody( - context, - nettyRequest, - response, - bodyType, - body + context, + nettyRequest, + response, + bodyType, + body ); writeFinalNettyResponse( - response, - nettyRequest, - context + response, + nettyRequest, + context ); } } else { response.body(null); writeFinalNettyResponse( - response, - nettyRequest, - context + response, + nettyRequest, + context ); } } @@ -1062,7 +807,7 @@ private Flux mapToHttpContent(NettyHttpRequest request, mediaType = routeExecutor.resolveDefaultResponseContentType(request, routeInfo); } boolean isJson = mediaType != null && mediaType.getExtension().equals(MediaType.EXTENSION_JSON) && - isJsonFormattable(hasRouteInfo ? routeInfo.getBodyType() : null); + isJsonFormattable(hasRouteInfo ? routeInfo.getBodyType() : null); NettyByteBufferFactory byteBufferFactory = new NettyByteBufferFactory(context.alloc()); Flux bodyPublisher = Flux.from(Publishers.convertPublisher(body, Publisher.class)); @@ -1087,7 +832,7 @@ private Flux mapToHttpContent(NettyHttpRequest request, } else { MediaTypeCodec codec = mediaTypeCodecRegistry.findCodec(finalMediaType, message.getClass()).orElse( - new TextPlainCodec(serverConfiguration.getDefaultCharset())); + new TextPlainCodec(serverConfiguration.getDefaultCharset())); if (LOG.isTraceEnabled()) { LOG.trace("Encoding emitted response object [{}] using codec: {}", message, codec); @@ -1117,11 +862,11 @@ private Flux mapToHttpContent(NettyHttpRequest request, } httpContentPublisher = httpContentPublisher - .contextWrite(reactorContext -> reactorContext.put(ServerRequestContext.KEY, request)) - .doOnNext(httpContent -> - // once an http content is written, read the next item if it is available - context.read()) - .doAfterTerminate(() -> cleanupRequest(context, request)); + .contextWrite(reactorContext -> reactorContext.put(ServerRequestContext.KEY, request)) + .doOnNext(httpContent -> + // once an http content is written, read the next item if it is available + context.read()) + .doAfterTerminate(() -> cleanupRequest(context, request)); return httpContentPublisher; } @@ -1135,22 +880,22 @@ private boolean isJsonFormattable(Argument argument) { javaType = argument.getFirstTypeVariable().orElse(Argument.OBJECT_ARGUMENT).getType(); } return !(javaType == byte[].class - || ByteBuffer.class.isAssignableFrom(javaType) - || ByteBuf.class.isAssignableFrom(javaType)); + || ByteBuffer.class.isAssignableFrom(javaType) + || ByteBuf.class.isAssignableFrom(javaType)); } private void encodeResponseBody( - ChannelHandlerContext context, - HttpRequest request, - MutableHttpResponse message, - @Nullable Argument bodyType, - Object body) { + ChannelHandlerContext context, + HttpRequest request, + MutableHttpResponse message, + @Nullable Argument bodyType, + Object body) { if (body == null) { return; } Optional typeHandler = customizableResponseTypeHandlerRegistry - .findTypeHandler(body.getClass()); + .findTypeHandler(body.getClass()); if (typeHandler.isPresent()) { NettyCustomizableResponseTypeHandler th = typeHandler.get(); setBodyContent(message, new NettyCustomizableResponseTypeHandlerInvoker(th, body)); @@ -1158,9 +903,9 @@ private void encodeResponseBody( MediaType mediaType = message.getContentType().orElse(null); if (mediaType == null) { mediaType = message.getAttribute(HttpAttributes.ROUTE_INFO, RouteInfo.class) - .map(routeInfo -> routeExecutor.resolveDefaultResponseContentType(request, routeInfo)) - // RouteExecutor will pick json by default, so we do too - .orElse(MediaType.APPLICATION_JSON_TYPE); + .map(routeInfo -> routeExecutor.resolveDefaultResponseContentType(request, routeInfo)) + // RouteExecutor will pick json by default, so we do too + .orElse(MediaType.APPLICATION_JSON_TYPE); message.contentType(mediaType); } if (body instanceof CharSequence) { @@ -1201,7 +946,7 @@ private void writeFinalNettyResponse(MutableHttpResponse message, HttpRequest final boolean isHttp2 = httpVersion == io.micronaut.http.HttpVersion.HTTP_2_0; boolean decodeError = request instanceof NettyHttpRequest && - ((NettyHttpRequest) request).getNativeRequest().decoderResult().isFailure(); + ((NettyHttpRequest) request).getNativeRequest().decoderResult().isFailure(); GenericFutureListener> requestCompletor = future -> { try { @@ -1307,18 +1052,18 @@ public void onComplete() { } private void syncWriteAndFlushNettyResponse( - ChannelHandlerContext context, - HttpRequest request, - io.netty.handler.codec.http.HttpResponse nettyResponse, - GenericFutureListener> requestCompletor + ChannelHandlerContext context, + HttpRequest request, + io.netty.handler.codec.http.HttpResponse nettyResponse, + GenericFutureListener> requestCompletor ) { context.writeAndFlush(nettyResponse).addListener(requestCompletor); if (LOG.isDebugEnabled()) { LOG.debug("Response {} - {} {}", - nettyResponse.status().code(), - request.getMethodName(), - request.getUri()); + nettyResponse.status().code(), + request.getMethodName(), + request.getUri()); } } @@ -1346,10 +1091,10 @@ private NettyMutableHttpResponse createNettyResponse(HttpResponse message) io.netty.handler.codec.http.HttpHeaders nettyHeaders = new DefaultHttpHeaders(serverConfiguration.isValidateHeaders()); message.getHeaders().forEach((BiConsumer>) nettyHeaders::set); return new NettyMutableHttpResponse<>( - HttpVersion.HTTP_1_1, - HttpResponseStatus.valueOf(message.code(), message.reason()), - body instanceof ByteBuf ? body : null, - ConversionService.SHARED + HttpVersion.HTTP_1_1, + HttpResponseStatus.valueOf(message.code(), message.reason()), + body instanceof ByteBuf ? body : null, + ConversionService.SHARED ); } @@ -1385,11 +1130,11 @@ private MutableHttpResponse setBodyContent(MutableHttpResponse response, O } private ByteBuf encodeBodyAsByteBuf( - @Nullable Argument bodyType, - Object body, - MediaTypeCodec codec, - ChannelHandlerContext context, - HttpRequest request) { + @Nullable Argument bodyType, + Object body, + MediaTypeCodec codec, + ChannelHandlerContext context, + HttpRequest request) { ByteBuf byteBuf; if (body instanceof ByteBuf) { byteBuf = (ByteBuf) body; @@ -1437,6 +1182,7 @@ private ByteBuf encodeBodyAsByteBuf( /** * Is the exception ignorable by Micronaut. + * * @param cause The cause * @return True if it can be ignored. */ diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/websocket/NettyServerWebSocketUpgradeHandler.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/websocket/NettyServerWebSocketUpgradeHandler.java index b61f1b7d726..cd4d283e217 100644 --- a/http-server-netty/src/main/java/io/micronaut/http/server/netty/websocket/NettyServerWebSocketUpgradeHandler.java +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/websocket/NettyServerWebSocketUpgradeHandler.java @@ -17,6 +17,7 @@ import io.micronaut.core.annotation.Internal; import io.micronaut.core.annotation.NonNull; +import io.micronaut.core.execution.ExecutionFlow; import io.micronaut.core.util.StringUtils; import io.micronaut.http.HttpAttributes; import io.micronaut.http.HttpMethod; @@ -32,9 +33,9 @@ import io.micronaut.http.netty.NettyHttpHeaders; import io.micronaut.http.netty.channel.ChannelPipelineCustomizer; import io.micronaut.http.netty.websocket.WebSocketSessionRepository; +import io.micronaut.http.server.RouteExecutor; import io.micronaut.http.server.netty.NettyEmbeddedServices; import io.micronaut.http.server.netty.NettyHttpRequest; -import io.micronaut.http.server.RouteExecutor; import io.micronaut.web.router.Router; import io.micronaut.web.router.UriRouteMatch; import io.micronaut.websocket.CloseReason; @@ -58,19 +59,13 @@ import io.netty.handler.codec.http.websocketx.WebSocketServerHandshakerFactory; import io.netty.handler.ssl.SslHandler; import io.netty.util.AsciiString; -import org.reactivestreams.Publisher; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; -import reactor.core.scheduler.Scheduler; -import reactor.core.scheduler.Schedulers; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.concurrent.atomic.AtomicReference; -import java.util.function.Consumer; /** * Handles WebSocket upgrade requests. @@ -106,9 +101,8 @@ public class NettyServerWebSocketUpgradeHandler extends SimpleChannelInboundHand * @param embeddedServices The embedded server services * @param webSocketSessionRepository The websocket session repository */ - public NettyServerWebSocketUpgradeHandler( - NettyEmbeddedServices embeddedServices, - WebSocketSessionRepository webSocketSessionRepository) { + public NettyServerWebSocketUpgradeHandler(NettyEmbeddedServices embeddedServices, + WebSocketSessionRepository webSocketSessionRepository) { this.router = embeddedServices.getRouter(); this.binderRegistry = embeddedServices.getRequestArgumentSatisfier().getBinderRegistry(); this.webSocketBeanRegistry = embeddedServices.getWebSocketBeanRegistry(); @@ -136,79 +130,81 @@ protected final void channelRead0(ChannelHandlerContext ctx, NettyHttpRequest ServerRequestContext.set(msg); Optional> optionalRoute = router.find(HttpMethod.GET, msg.getUri().toString(), msg) - .filter(rm -> rm.isAnnotationPresent(OnMessage.class) || rm.isAnnotationPresent(OnOpen.class)) - .findFirst(); + .filter(rm -> rm.isAnnotationPresent(OnMessage.class) || rm.isAnnotationPresent(OnOpen.class)) + .findFirst(); MutableHttpResponse proceed = HttpResponse.ok(); AtomicReference> requestReference = new AtomicReference<>(msg); - Flux> responsePublisher; - if (optionalRoute.isPresent()) { - UriRouteMatch rm = optionalRoute.get(); - msg.setAttribute(HttpAttributes.ROUTE_MATCH, rm); - msg.setAttribute(HttpAttributes.ROUTE_INFO, rm); - proceed.setAttribute(HttpAttributes.ROUTE_MATCH, rm); - proceed.setAttribute(HttpAttributes.ROUTE_INFO, rm); - responsePublisher = Flux.just(proceed); - } else { - responsePublisher = routeExecutor.onError(new HttpStatusException(HttpStatus.NOT_FOUND, "WebSocket Not Found"), msg); - } - - Publisher> finalPublisher = routeExecutor.filterPublisher(requestReference, responsePublisher); - - final Scheduler scheduler = Schedulers.fromExecutorService(ctx.channel().eventLoop()); - Mono.from(finalPublisher) - .publishOn(scheduler) - .subscribeOn(scheduler) - .contextWrite(reactorContext -> reactorContext.put(ServerRequestContext.KEY, requestReference.get())) - .subscribe((Consumer>) actualResponse -> { - if (cancelUpgrade) { - if (LOG.isDebugEnabled()) { - LOG.debug("Cancelling websocket upgrade, handler was removed while request was processing"); - } - return; - } + ExecutionFlow> responseFlow = ExecutionFlow.async(ctx.channel().eventLoop(), () -> routeExecutor.filterPublisher(requestReference, () -> { + ExecutionFlow> response; + if (optionalRoute.isPresent()) { + UriRouteMatch rm = optionalRoute.get(); + msg.setAttribute(HttpAttributes.ROUTE_MATCH, rm); + msg.setAttribute(HttpAttributes.ROUTE_INFO, rm); + proceed.setAttribute(HttpAttributes.ROUTE_MATCH, rm); + proceed.setAttribute(HttpAttributes.ROUTE_INFO, rm); + response = ExecutionFlow.just(proceed); + } else { + response = routeExecutor.onError(new HttpStatusException(HttpStatus.NOT_FOUND, "WebSocket Not Found"), msg); + } + response.putInContext(ServerRequestContext.KEY, requestReference.get()); + return response; + })); + responseFlow.onComplete((response, throwable) -> { + if (response != null) { + writeResponse(ctx, msg, proceed, response); + } + }); + } - if (actualResponse == proceed) { - UriRouteMatch routeMatch = actualResponse.getAttribute(HttpAttributes.ROUTE_MATCH, UriRouteMatch.class).get(); - //Adding new handler to the existing pipeline to handle WebSocket Messages - WebSocketBean webSocketBean = webSocketBeanRegistry.getWebSocket(routeMatch.getTarget().getClass()); + private void writeResponse(ChannelHandlerContext ctx, NettyHttpRequest msg, MutableHttpResponse proceed, MutableHttpResponse actualResponse) { + if (cancelUpgrade) { + if (LOG.isDebugEnabled()) { + LOG.debug("Cancelling websocket upgrade, handler was removed while request was processing"); + } + return; + } - handleHandshake(ctx, msg, webSocketBean, actualResponse); + if (actualResponse == proceed) { + UriRouteMatch routeMatch = actualResponse.getAttribute(HttpAttributes.ROUTE_MATCH, UriRouteMatch.class) + .orElseThrow(() -> new IllegalStateException("Route match is required!")); + //Adding new handler to the existing pipeline to handle WebSocket Messages + WebSocketBean webSocketBean = webSocketBeanRegistry.getWebSocket(routeMatch.getTarget().getClass()); - ChannelPipeline pipeline = ctx.pipeline(); + handleHandshake(ctx, msg, webSocketBean, actualResponse); - try { - // re-configure the pipeline - NettyServerWebSocketHandler webSocketHandler = new NettyServerWebSocketHandler( - nettyEmbeddedServices, - webSocketSessionRepository, - handshaker, - webSocketBean, - msg, - routeMatch, - ctx, - routeExecutor.getCoroutineHelper().orElse(null)); - pipeline.addBefore(ctx.name(), NettyServerWebSocketHandler.ID, webSocketHandler); + ChannelPipeline pipeline = ctx.pipeline(); - pipeline.remove(ChannelPipelineCustomizer.HANDLER_HTTP_STREAM); - pipeline.remove(NettyServerWebSocketUpgradeHandler.this); - ChannelHandler accessLoggerHandler = pipeline.get(ChannelPipelineCustomizer.HANDLER_ACCESS_LOGGER); - if (accessLoggerHandler != null) { - pipeline.remove(accessLoggerHandler); - } + try { + // re-configure the pipeline + NettyServerWebSocketHandler webSocketHandler = new NettyServerWebSocketHandler( + nettyEmbeddedServices, + webSocketSessionRepository, + handshaker, + webSocketBean, + msg, + routeMatch, + ctx, + routeExecutor.getCoroutineHelper().orElse(null)); + pipeline.addBefore(ctx.name(), NettyServerWebSocketHandler.ID, webSocketHandler); - } catch (Throwable e) { - if (LOG.isErrorEnabled()) { - LOG.error("Error opening WebSocket: " + e.getMessage(), e); - } - ctx.writeAndFlush(new CloseWebSocketFrame(CloseReason.INTERNAL_ERROR.getCode(), CloseReason.INTERNAL_ERROR.getReason())); - } - } else { - ctx.writeAndFlush(actualResponse); - } - }); + pipeline.remove(ChannelPipelineCustomizer.HANDLER_HTTP_STREAM); + pipeline.remove(NettyServerWebSocketUpgradeHandler.this); + ChannelHandler accessLoggerHandler = pipeline.get(ChannelPipelineCustomizer.HANDLER_ACCESS_LOGGER); + if (accessLoggerHandler != null) { + pipeline.remove(accessLoggerHandler); + } + } catch (Throwable e) { + if (LOG.isErrorEnabled()) { + LOG.error("Error opening WebSocket: " + e.getMessage(), e); + } + ctx.writeAndFlush(new CloseWebSocketFrame(CloseReason.INTERNAL_ERROR.getCode(), CloseReason.INTERNAL_ERROR.getReason())); + } + } else { + ctx.writeAndFlush(actualResponse); + } } /** diff --git a/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/MaxRequestSizeSpec.groovy b/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/MaxRequestSizeSpec.groovy index 978e6e00da6..79307ec02b7 100644 --- a/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/MaxRequestSizeSpec.groovy +++ b/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/MaxRequestSizeSpec.groovy @@ -2,6 +2,7 @@ package io.micronaut.http.server.netty import io.micronaut.context.ApplicationContext import io.micronaut.core.annotation.NonNull +import io.micronaut.core.async.annotation.SingleResult import io.micronaut.http.HttpRequest import io.micronaut.http.MediaType import io.micronaut.http.annotation.Body @@ -50,10 +51,8 @@ import io.netty.handler.ssl.SupportedCipherSuiteFilter import io.netty.handler.ssl.util.InsecureTrustManagerFactory import org.reactivestreams.Publisher import reactor.core.publisher.Flux -import io.micronaut.core.async.annotation.SingleResult import spock.lang.Ignore import spock.lang.Issue -import spock.lang.PendingFeature import spock.lang.Specification import spock.util.concurrent.PollingConditions diff --git a/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/errors/ErrorSpec.groovy b/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/errors/ErrorSpec.groovy index 0c9b5f68a66..bf5e3fb3656 100644 --- a/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/errors/ErrorSpec.groovy +++ b/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/errors/ErrorSpec.groovy @@ -18,20 +18,21 @@ package io.micronaut.http.server.netty.errors import groovy.json.JsonSlurper import io.micronaut.context.annotation.Property import io.micronaut.core.annotation.NonNull -import io.micronaut.http.* -import io.micronaut.http.annotation.Body +import io.micronaut.core.async.annotation.SingleResult +import io.micronaut.http.HttpHeaders +import io.micronaut.http.HttpRequest +import io.micronaut.http.HttpResponse +import io.micronaut.http.HttpStatus +import io.micronaut.http.MediaType import io.micronaut.http.annotation.Controller import io.micronaut.http.annotation.Error import io.micronaut.http.annotation.Get import io.micronaut.http.annotation.Post import io.micronaut.http.annotation.Produces import io.micronaut.http.client.exceptions.HttpClientResponseException -import io.micronaut.http.hateoas.JsonError import io.micronaut.http.server.exceptions.ExceptionHandler import io.micronaut.http.server.netty.AbstractMicronautSpec import io.netty.bootstrap.Bootstrap -import io.netty.buffer.ByteBuf -import io.netty.buffer.CompositeByteBuf import io.netty.buffer.Unpooled import io.netty.channel.Channel import io.netty.channel.ChannelHandlerContext @@ -40,21 +41,16 @@ import io.netty.channel.ChannelInitializer import io.netty.channel.nio.NioEventLoopGroup import io.netty.channel.socket.nio.NioSocketChannel import io.netty.handler.codec.http.FullHttpResponse -import io.netty.handler.codec.http.HttpClientCodec import io.netty.handler.codec.http.HttpObjectAggregator import io.netty.handler.codec.http.HttpResponseDecoder import jakarta.inject.Singleton import org.reactivestreams.Publisher import reactor.core.publisher.Flux import reactor.core.publisher.Mono -import io.micronaut.core.async.annotation.SingleResult import spock.lang.Issue -import spock.lang.PendingFeature import spock.lang.Timeout import java.nio.charset.StandardCharsets -import java.util.concurrent.CopyOnWriteArrayList - /** * Tests for different kinds of errors and the expected responses * @@ -214,7 +210,6 @@ class ErrorSpec extends AbstractMicronautSpec { response.getBody(Map).get()._embedded.errors[0].message.contains('foo') } - @PendingFeature @Issue('https://github.com/micronaut-projects/micronaut-core/issues/7786') void "test encoding error with handler"() { given: @@ -230,13 +225,13 @@ class ErrorSpec extends AbstractMicronautSpec { expect: response.code() == HttpStatus.INTERNAL_SERVER_ERROR.code response.header(HttpHeaders.CONTENT_TYPE) == MediaType.APPLICATION_JSON - response.getBody(Map).get()._embedded.errors[0].message.contains('Server error') + response.getBody(Map).get()._embedded.errors[0].message.contains('foo') } void "test encoding error with handler loop"() { given: HttpResponse response = Flux.from(rxClient.exchange( - HttpRequest.GET('/errors/encoding-error/handled') + HttpRequest.GET('/errors/encoding-error/handled/loop') )).onErrorResume(t -> { if (t instanceof HttpClientResponseException) { return Flux.just(((HttpClientResponseException) t).response) @@ -422,7 +417,7 @@ X-Long-Header: $longString\r @Error HttpResponse error(HttpRequest request, Throwable e) { - HttpResponse.serverError("Server error") + HttpResponse.serverError("Handled server error") } } diff --git a/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/stack/InvocationStackSpec.groovy b/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/stack/InvocationStackSpec.groovy new file mode 100644 index 00000000000..a1c97693240 --- /dev/null +++ b/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/stack/InvocationStackSpec.groovy @@ -0,0 +1,253 @@ +package io.micronaut.http.server.netty.stack + +import io.micronaut.context.ApplicationContext +import io.micronaut.context.annotation.Requires +import io.micronaut.core.annotation.NonBlocking +import io.micronaut.http.HttpRequest +import io.micronaut.http.HttpResponse +import io.micronaut.http.MutableHttpResponse +import io.micronaut.http.annotation.Controller +import io.micronaut.http.annotation.Error +import io.micronaut.http.annotation.Filter +import io.micronaut.http.annotation.Get +import io.micronaut.http.client.annotation.Client +import io.micronaut.http.filter.HttpServerFilter +import io.micronaut.http.filter.ServerFilterChain +import io.micronaut.runtime.server.EmbeddedServer +import io.micronaut.scheduling.TaskExecutors +import io.micronaut.scheduling.annotation.ExecuteOn +import jakarta.inject.Inject +import org.reactivestreams.Publisher +import spock.lang.Specification +import spock.lang.Unroll + +import java.util.concurrent.atomic.AtomicBoolean + +class InvocationStackSpec extends Specification { + + @Unroll + void "test stack size for #method"() { + given: + EmbeddedServer embeddedServer = ApplicationContext.run(EmbeddedServer, ['spec': getClass().getSimpleName()]) + StackCheckClient client = embeddedServer.applicationContext.getBean(StackCheckClient) + + expect: + client."$method"() + + cleanup: + embeddedServer.close() + + where: + method << ["blocking", "nonblocking", "executeOn", + "withOneReactiveFilter", "withOneReactiveFilterExecuteOn", + "withTwoReactiveFilters", "withTwoReactiveFiltersExecuteOn", + "exception", "throwsExecuteOnEx"] + } + + @Requires(property = "spec", value = "InvocationStackSpec") + @Client("/stack-check") + static interface StackCheckClient { + @Get("/blocking") + String blocking() + + @Get("/nonblocking") + String nonblocking() + + @Get("/with-one-reactive-filter") + String withOneReactiveFilter() + + @Get("/with-one-reactive-filter-execute-on") + String withOneReactiveFilterExecuteOn() + + @Get("/with-two-reactive-filters") + String withTwoReactiveFilters() + + @Get("/with-two-reactive-filters-execute-on") + String withTwoReactiveFiltersExecuteOn() + + @Get("/execute-on") + String executeOn() + + @Get("/exception") + String exception() + + @Get("/exception-execute-on") + String throwsExecuteOnEx() + } + + @Requires(property = "spec", value = "InvocationStackSpec") + @Controller("/stack-check") + static class StackCheckController { + + @Inject + MyOneFilter oneFilter + + @Inject + MyTwoFilter1 twoFilters1 + @Inject + MyTwoFilter2 twoFilters2 + + @Get("/blocking") + String blocking() { + checkInvocationStack(false) + return "OK" + } + + @Get("/nonblocking") + @NonBlocking + String nonblocking() { + checkInvocationStack(false) + return "OK" + } + + @Get("/with-one-reactive-filter") + String withOneReactiveFilter() { + if (!oneFilter.getExecuted().get()) { + throw new IllegalStateException() + } + checkInvocationStack(false) + return "OK" + } + + @ExecuteOn(TaskExecutors.IO) + @Get("/with-one-reactive-filter-execute-on") + String withOneReactiveFilterExecuteOn() { + if (!oneFilter.getExecuted().get()) { + throw new IllegalStateException() + } + checkInvocationStack(true) + return "OK" + } + + @Get("/with-two-reactive-filters") + String withTwoReactiveFilters() { + if (!twoFilters1.getExecuted().get() || !twoFilters2.getExecuted().get()) { + throw new IllegalStateException() + } + checkInvocationStack(false) + return "OK" + } + + @Get("/with-two-reactive-filters-execute-on") + String withTwoReactiveFiltersExecuteOn() { + if (!twoFilters1.getExecuted().get() || !twoFilters2.getExecuted().get()) { + throw new IllegalStateException() + } + checkInvocationStack(true) + return "OK" + } + + @Get("/execute-on") + @ExecuteOn(TaskExecutors.IO) + String scheduleBlocking() { + checkInvocationStack(true) + return "OK" + } + + @Get("/exception") + String throwsEx() { + checkInvocationStack(false) + throw new MyException() + } + + @Error(MyException) + HttpResponse onException(MyException e) { + checkInvocationStack(false) + return HttpResponse.ok("OK") + } + + @Get("/exception-execute-on") + String throwsExecuteOnEx() { + checkInvocationStack(false) + throw new MyException2() + } + + @ExecuteOn(TaskExecutors.IO) + @Error(MyException2) + HttpResponse onExceptionExecuteOn(MyException2 e) { + checkInvocationStack(true) + return HttpResponse.ok("OK") + } + + void checkInvocationStack(boolean allowExecutor) { + for (StackTraceElement s in new RuntimeException().getStackTrace()) { + if (!isKnownStack(s.className, allowExecutor)) { + throw new RuntimeException("Unknown stack member: " + s.className); + } + } + } + + boolean isKnownStack(String className, boolean allowExecutor) { + if (allowExecutor && className.startsWith("java.util.concurrent")) { + return true + } + if (className.startsWith("io.netty")) { + return true + } + if (className.startsWith("io.micronaut")) { + return true + } + if (className.startsWith("jdk.internal") || className.startsWith("java.lang")) { + return true // Java + } + if (className.startsWith("org.codehaus.groovy") || className.startsWith("org.apache.groovy")) { + return true // Spock + } + return false + } + + } + + static class MyException extends RuntimeException { + + MyException() { + super(Thread.currentThread().getName()) + } + + } + + static class MyException2 extends RuntimeException { + + MyException2() { + super(Thread.currentThread().getName()) + } + + } + + @Filter("/stack-check/with-one-reactive-filter*") + static class MyOneFilter implements HttpServerFilter { + + final AtomicBoolean executed = new AtomicBoolean(false) + + @Override + Publisher> doFilter(HttpRequest request, ServerFilterChain chain) { + executed.set(true) + return chain.proceed(request) + } + } + + @Filter("/stack-check/with-two-reactive-filters*") + static class MyTwoFilter1 implements HttpServerFilter { + + final AtomicBoolean executed = new AtomicBoolean(false) + + @Override + Publisher> doFilter(HttpRequest request, ServerFilterChain chain) { + executed.set(true) + return chain.proceed(request) + } + } + + @Filter("/stack-check/with-two-reactive-filters*") + static class MyTwoFilter2 implements HttpServerFilter { + + final AtomicBoolean executed = new AtomicBoolean(false) + + @Override + Publisher> doFilter(HttpRequest request, ServerFilterChain chain) { + executed.set(true) + return chain.proceed(request) + } + } + +} diff --git a/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/threading/ThreadSelectionSpec.groovy b/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/threading/ThreadSelectionSpec.groovy index ead32ae7144..e06928be552 100644 --- a/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/threading/ThreadSelectionSpec.groovy +++ b/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/threading/ThreadSelectionSpec.groovy @@ -9,6 +9,7 @@ import io.micronaut.http.MediaType import io.micronaut.http.MutableHttpResponse import io.micronaut.http.annotation.Body import io.micronaut.http.annotation.Controller +import io.micronaut.http.annotation.Error import io.micronaut.http.annotation.Filter import io.micronaut.http.annotation.Get import io.micronaut.http.annotation.Post @@ -107,6 +108,28 @@ class ThreadSelectionSpec extends Specification { ThreadSelection.MANUAL | "controller: $LOOP" | "handler: $LOOP" | "handler: $IO" } + void "test thread selection for error route"() { + given: + EmbeddedServer embeddedServer = ApplicationContext.run(EmbeddedServer, ['micronaut.server.thread-selection': strategy]) + ThreadSelectionClient client = embeddedServer.applicationContext.getBean(ThreadSelectionClient) + + when: + def exResult = client.throwsExErrorRoute() + + then: + exResult.contains(controller) + exResult.contains(handler) + + cleanup: + embeddedServer.close() + + where: + strategy | controller | handler + ThreadSelection.AUTO | "controller: $IO" | "handler: $IO" + ThreadSelection.IO | "controller: $IO" | "handler: $IO" + ThreadSelection.MANUAL | "controller: $LOOP" | "handler: $LOOP" + } + void "test injecting an executor service does not inject the Netty event loop"() { given: EmbeddedServer embeddedServer = ApplicationContext.run(EmbeddedServer) @@ -151,6 +174,9 @@ class ThreadSelectionSpec extends Specification { @Get("/exception") String exception() + @Get("/exception-error-route") + String throwsExErrorRoute() + @Get("/scheduleexception") String scheduleException() } @@ -217,10 +243,20 @@ class ThreadSelectionSpec extends Specification { throw new MyException() } + @Get("/exception-error-route") + String throwsExErrorRoute() { + throw new MyExceptionWithErrorRoute() + } + @Get("/scheduleexception") String throwsScheduledEx() { throw new MyExceptionScheduled() } + + @Error(MyExceptionWithErrorRoute.class) + HttpResponse errorRoute(MyExceptionWithErrorRoute e) { + return HttpResponse.ok("handler: ${Thread.currentThread().name}, controller: " + e.getMessage()) + } } @Filter("/thread-selection/alter**") @@ -245,6 +281,14 @@ class ThreadSelectionSpec extends Specification { } + static class MyExceptionWithErrorRoute extends RuntimeException { + + MyExceptionWithErrorRoute() { + super(Thread.currentThread().getName()) + } + + } + static class MyExceptionScheduled extends RuntimeException { MyExceptionScheduled() { diff --git a/http-server/src/main/java/io/micronaut/http/server/RouteExecutor.java b/http-server/src/main/java/io/micronaut/http/server/RouteExecutor.java index 871a9056818..0ffb3607ff6 100644 --- a/http-server/src/main/java/io/micronaut/http/server/RouteExecutor.java +++ b/http-server/src/main/java/io/micronaut/http/server/RouteExecutor.java @@ -20,10 +20,13 @@ import io.micronaut.core.annotation.Internal; import io.micronaut.core.annotation.NonNull; import io.micronaut.core.annotation.Nullable; +import io.micronaut.reactive.reactor.execution.ReactiveExecutionFlow; import io.micronaut.core.async.publisher.Publishers; +import io.micronaut.core.execution.ExecutionFlow; import io.micronaut.core.io.buffer.ReferenceCounted; import io.micronaut.core.type.Argument; import io.micronaut.core.type.ReturnType; +import io.micronaut.core.util.CollectionUtils; import io.micronaut.http.HttpAttributes; import io.micronaut.http.HttpHeaders; import io.micronaut.http.HttpMethod; @@ -42,6 +45,7 @@ import io.micronaut.http.server.exceptions.ExceptionHandler; import io.micronaut.http.server.exceptions.response.ErrorContext; import io.micronaut.http.server.exceptions.response.ErrorResponseProcessor; +import io.micronaut.http.server.types.files.FileCustomizableResponseType; import io.micronaut.inject.BeanDefinition; import io.micronaut.inject.BeanType; import io.micronaut.inject.ExecutableMethod; @@ -52,11 +56,14 @@ import io.micronaut.web.router.RouteInfo; import io.micronaut.web.router.RouteMatch; import io.micronaut.web.router.Router; +import io.micronaut.web.router.UriRouteMatch; +import io.micronaut.web.router.exceptions.DuplicateRouteException; import io.micronaut.web.router.exceptions.UnsatisfiedRouteException; import jakarta.inject.Singleton; import org.reactivestreams.Publisher; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import reactor.core.CorePublisher; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.core.scheduler.Scheduler; @@ -65,21 +72,25 @@ import java.io.IOException; import java.time.LocalDateTime; import java.util.ArrayList; +import java.util.Collection; import java.util.Collections; +import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Optional; +import java.util.Set; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionException; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; -import java.util.function.Function; import java.util.function.Supplier; import java.util.regex.Pattern; +import java.util.stream.Collectors; import static io.micronaut.core.util.KotlinUtils.isKotlinCoroutineSuspended; +import static io.micronaut.http.HttpAttributes.AVAILABLE_HTTP_METHODS; import static io.micronaut.inject.util.KotlinExecutableMethodUtils.isKotlinFunctionReturnTypeUnit; /** @@ -96,7 +107,7 @@ public final class RouteExecutor { * Also present in netty RoutingInBoundHandler. */ private static final Pattern IGNORABLE_ERROR_MESSAGE = Pattern.compile( - "^.*(?:connection (?:reset|closed|abort|broken)|broken pipe).*$", Pattern.CASE_INSENSITIVE); + "^.*(?:connection (?:reset|closed|abort|broken)|broken pipe).*$", Pattern.CASE_INSENSITIVE); private final Router router; private final BeanContext beanContext; @@ -167,6 +178,187 @@ public Optional getCoroutineHelper() { return coroutineHelper; } + @NonNull + public ExecutionFlow> executeRoute(RequestBodyReader requestBodyReader, + HttpRequest httpRequest, + boolean multipartEnabled, + StaticResourceResponseFinder staticResourceResponseFinder) { + ServerRequestContext.set(httpRequest); + + MediaType contentType = httpRequest.getContentType().orElse(null); + if (!multipartEnabled && + contentType != null && + contentType.equals(MediaType.MULTIPART_FORM_DATA_TYPE)) { + if (LOG.isDebugEnabled()) { + LOG.debug("Multipart uploads have been disabled via configuration. Rejected request for URI {}, method {}, and content type {}", httpRequest.getUri(), + httpRequest.getMethodName(), contentType); + } + return onStatusError( + requestBodyReader, + httpRequest, + HttpResponse.status(HttpStatus.UNSUPPORTED_MEDIA_TYPE), + "Content Type [" + contentType + "] not allowed"); + } + + UriRouteMatch routeMatch = findRouteMatch(httpRequest); + if (routeMatch == null) { + //Check if there is a file for the route before returning route not found + FileCustomizableResponseType fileCustomizableResponseType = staticResourceResponseFinder.find(httpRequest); + if (fileCustomizableResponseType != null) { + return filterPublisher(new AtomicReference<>(httpRequest), () -> ExecutionFlow.just(HttpResponse.ok(fileCustomizableResponseType))); + } + return onRouteMiss(requestBodyReader, httpRequest); + } + + setRouteAttributes(httpRequest, routeMatch); + + if (LOG.isTraceEnabled()) { + String requestPath = httpRequest.getUri().getPath(); + if (routeMatch instanceof MethodBasedRouteMatch) { + LOG.trace("Matched route {} - {} to controller {}", httpRequest.getMethodName(), requestPath, routeMatch.getDeclaringType()); + } else { + LOG.trace("Matched route {} - {}", httpRequest.getMethodName(), requestPath); + } + } + // all ok proceed to try and execute the route + if (routeMatch.isWebSocketRoute()) { + return onStatusError( + requestBodyReader, + httpRequest, + HttpResponse.status(HttpStatus.BAD_REQUEST), + "Not a WebSocket request"); + } + return executeRoute( + new AtomicReference<>(httpRequest), + true, + true, + requestBodyReader.read(routeMatch, httpRequest) + ); + } + + @Nullable + private UriRouteMatch findRouteMatch(HttpRequest httpRequest) { + UriRouteMatch routeMatch = null; + + List> uriRoutes = router.findAllClosest(httpRequest); + if (uriRoutes.size() > 1) { + throw new DuplicateRouteException(httpRequest.getUri().getPath(), uriRoutes); + } else if (uriRoutes.size() == 1) { + routeMatch = uriRoutes.get(0); + } + + if (routeMatch == null && httpRequest.getMethod().equals(HttpMethod.OPTIONS)) { + List> anyUriRoutes = router.findAny(httpRequest.getUri().toString(), httpRequest).toList(); + if (!anyUriRoutes.isEmpty()) { + setRouteAttributes(httpRequest, anyUriRoutes.get(0)); + httpRequest.setAttribute(AVAILABLE_HTTP_METHODS, anyUriRoutes.stream().map(UriRouteMatch::getHttpMethod).toList()); + } + } + return routeMatch; + } + + private ExecutionFlow> onRouteMiss(RequestBodyReader requestBodyReader, + HttpRequest httpRequest) { + HttpMethod httpMethod = httpRequest.getMethod(); + String requestMethodName = httpRequest.getMethodName(); + MediaType contentType = httpRequest.getContentType().orElse(null); + + if (LOG.isDebugEnabled()) { + LOG.debug("No matching route: {} {}", httpMethod, httpRequest.getUri()); + } + + // if there is no route present try to locate a route that matches a different HTTP method + final List> anyMatchingRoutes = router + .findAny(httpRequest.getUri().toString(), httpRequest).toList(); + final Collection acceptedTypes = httpRequest.accept(); + final boolean hasAcceptHeader = CollectionUtils.isNotEmpty(acceptedTypes); + + Set acceptableContentTypes = contentType != null ? new HashSet<>(5) : null; + Set allowedMethods = new HashSet<>(5); + Set produceableContentTypes = hasAcceptHeader ? new HashSet<>(5) : null; + for (UriRouteMatch anyRoute : anyMatchingRoutes) { + final String routeMethod = anyRoute.getRoute().getHttpMethodName(); + if (!requestMethodName.equals(routeMethod)) { + allowedMethods.add(routeMethod); + } + if (contentType != null && !anyRoute.doesConsume(contentType)) { + acceptableContentTypes.addAll(anyRoute.getRoute().getConsumes()); + } + if (hasAcceptHeader && !anyRoute.doesProduce(acceptedTypes)) { + produceableContentTypes.addAll(anyRoute.getRoute().getProduces()); + } + } + + if (CollectionUtils.isNotEmpty(acceptableContentTypes)) { + if (LOG.isDebugEnabled()) { + LOG.debug("Content type not allowed for URI {}, method {}, and content type {}", httpRequest.getUri(), + requestMethodName, contentType); + } + return onStatusError( + requestBodyReader, + httpRequest, + HttpResponse.status(HttpStatus.UNSUPPORTED_MEDIA_TYPE), + "Content Type [" + contentType + "] not allowed. Allowed types: " + acceptableContentTypes); + } + if (CollectionUtils.isNotEmpty(produceableContentTypes)) { + if (LOG.isDebugEnabled()) { + LOG.debug("Content type not allowed for URI {}, method {}, and content type {}", httpRequest.getUri(), + requestMethodName, contentType); + } + return onStatusError( + requestBodyReader, + httpRequest, + HttpResponse.status(HttpStatus.NOT_ACCEPTABLE), + "Specified Accept Types " + acceptedTypes + " not supported. Supported types: " + produceableContentTypes); + } + if (!allowedMethods.isEmpty()) { + if (LOG.isDebugEnabled()) { + LOG.debug("Method not allowed for URI {} and method {}", httpRequest.getUri(), requestMethodName); + } + return onStatusError( + requestBodyReader, + httpRequest, + HttpResponse.notAllowedGeneric(allowedMethods), + "Method [" + requestMethodName + "] not allowed for URI [" + httpRequest.getUri() + "]. Allowed methods: " + allowedMethods); + } + return onStatusError(requestBodyReader, + httpRequest, + HttpResponse.status(HttpStatus.NOT_FOUND), + "Page Not Found"); + } + + public ExecutionFlow> onStatusError(RequestBodyReader requestBodyReader, + HttpRequest httpRequest, + MutableHttpResponse defaultResponse, + String message) { + Optional> statusRoute = router.findStatusRoute(defaultResponse.status(), httpRequest); + if (statusRoute.isPresent()) { + return executeRoute( + new AtomicReference<>(httpRequest), + true, + true, + requestBodyReader.read(statusRoute.get(), httpRequest) + ); + } + if (httpRequest.getMethod() != HttpMethod.HEAD) { + defaultResponse = errorResponseProcessor.processResponse(ErrorContext.builder(httpRequest) + .errorMessage(message) + .build(), defaultResponse); + if (defaultResponse.getContentType().isEmpty()) { + defaultResponse = defaultResponse.contentType(MediaType.APPLICATION_JSON_TYPE); + } + } + MutableHttpResponse finalDefaultResponse = defaultResponse; + return filterPublisher(new AtomicReference<>(httpRequest), () -> ExecutionFlow.just(finalDefaultResponse)); + } + + private void setRouteAttributes(HttpRequest request, UriRouteMatch route) { + request.setAttribute(HttpAttributes.ROUTE, route.getRoute()); + request.setAttribute(HttpAttributes.ROUTE_MATCH, route); + request.setAttribute(HttpAttributes.ROUTE_INFO, route); + request.setAttribute(HttpAttributes.URI_TEMPLATE, route.getRoute().getUriMatchTemplate().toString()); + } + /** * Creates a response publisher to represent the response after being handled * by any available error route or exception handler. @@ -175,9 +367,10 @@ public Optional getCoroutineHelper() { * @param httpRequest The request that caused the exception * @return A response publisher */ - public Flux> onError(Throwable t, HttpRequest httpRequest) { - // find the origination of of the route - Class declaringType = httpRequest.getAttribute(HttpAttributes.ROUTE_INFO, RouteInfo.class).map(RouteInfo::getDeclaringType).orElse(null); + public ExecutionFlow> onError(Throwable t, HttpRequest httpRequest) { + // find the origination of the route + Optional previousRequestRouteInfo = httpRequest.getAttribute(HttpAttributes.ROUTE_INFO, RouteInfo.class); + Class declaringType = previousRequestRouteInfo.map(RouteInfo::getDeclaringType).orElse(null); final Throwable cause; // top level exceptions returned by CompletableFutures. These always wrap the real exception thrown. @@ -188,24 +381,23 @@ public Flux> onError(Throwable t, HttpRequest httpRequ } RouteMatch errorRoute = findErrorRoute(cause, declaringType, httpRequest); - if (errorRoute != null) { if (serverConfiguration.isLogHandledExceptions()) { logException(cause); } try { AtomicReference> requestReference = new AtomicReference<>(httpRequest); - return buildRouteResponsePublisher( - requestReference, - Flux.just(errorRoute)) - .doOnNext(response -> response.setAttribute(HttpAttributes.EXCEPTION, cause)) - .onErrorResume(throwable -> createDefaultErrorResponsePublisher(requestReference.get(), throwable)); + return executeRoute(requestReference, false, false, ExecutionFlow.just(errorRoute)) + .>map(response -> { + response.setAttribute(HttpAttributes.EXCEPTION, cause); + return response; + }) + .onErrorResume(throwable -> createDefaultErrorResponseFlow(requestReference.get(), throwable)); } catch (Throwable e) { - return createDefaultErrorResponsePublisher(httpRequest, e).flux(); + return createDefaultErrorResponseFlow(httpRequest, e); } } else { Optional> optionalDefinition = beanContext.findBeanDefinition(ExceptionHandler.class, Qualifiers.byTypeArgumentsClosest(cause.getClass(), Object.class)); - if (optionalDefinition.isPresent()) { BeanDefinition handlerDefinition = optionalDefinition.get(); final Optional> optionalMethod = handlerDefinition.findPossibleMethods("handle").findFirst(); @@ -213,7 +405,7 @@ public Flux> onError(Throwable t, HttpRequest httpRequ if (optionalMethod.isPresent()) { routeInfo = new ExecutableRouteInfo(optionalMethod.get(), true); } else { - routeInfo = new RouteInfo() { + routeInfo = new RouteInfo<>() { @Override public ReturnType getReturnType() { return ReturnType.of(Object.class); @@ -232,41 +424,42 @@ public boolean isErrorRoute() { @Override public List getProduces() { return MediaType.fromType(getDeclaringType()) - .map(Collections::singletonList) - .orElse(Collections.emptyList()); + .map(Collections::singletonList) + .orElse(Collections.emptyList()); } }; } - Flux> reactiveSequence = Flux.defer(() -> { - ExceptionHandler handler = beanContext.getBean(handlerDefinition); + Supplier>> responseSupplier = () -> { + ExceptionHandler handler = beanContext.getBean(handlerDefinition); try { if (serverConfiguration.isLogHandledExceptions()) { logException(cause); } Object result = handler.handle(httpRequest, cause); - return createResponseForBody(httpRequest, result, routeInfo); } catch (Throwable e) { - return createDefaultErrorResponsePublisher(httpRequest, e); + return createDefaultErrorResponseFlow(httpRequest, e); } - }); + }; + ExecutionFlow> responseFlow; final ExecutorService executor = findExecutor(routeInfo); if (executor != null) { - reactiveSequence = applyExecutorToPublisher(reactiveSequence, executor); - } - return reactiveSequence - .doOnNext(response -> response.setAttribute(HttpAttributes.EXCEPTION, cause)) - .onErrorResume(throwable -> createDefaultErrorResponsePublisher(httpRequest, throwable)); - } else { - if (isIgnorable(cause)) { - logIgnoredException(cause); - return Flux.empty(); + responseFlow = ExecutionFlow.async(executor, responseSupplier); } else { - return createDefaultErrorResponsePublisher( - httpRequest, - cause).flux(); + responseFlow = responseSupplier.get(); } + return responseFlow + .>map(response -> { + response.setAttribute(HttpAttributes.EXCEPTION, cause); + return response; + }) + .onErrorResume(throwable -> createDefaultErrorResponseFlow(httpRequest, throwable)); + } + if (isIgnorable(cause)) { + logIgnoredException(cause); + return ExecutionFlow.empty(); } + return createDefaultErrorResponseFlow(httpRequest, cause); } } @@ -300,10 +493,10 @@ public boolean isErrorRoute() { } }); MutableHttpResponse mutableHttpResponse = errorResponseProcessor.processResponse( - ErrorContext.builder(httpRequest) - .cause(cause) - .errorMessage("Internal Server Error: " + cause.getMessage()) - .build(), response); + ErrorContext.builder(httpRequest) + .cause(cause) + .errorMessage("Internal Server Error: " + cause.getMessage()) + .build(), response); applyConfiguredHeaders(mutableHttpResponse.getHeaders()); if (!mutableHttpResponse.getContentType().isPresent() && httpRequest.getMethod() != HttpMethod.HEAD) { return mutableHttpResponse.contentType(MediaType.APPLICATION_JSON_TYPE); @@ -338,50 +531,25 @@ public MediaType resolveDefaultResponseContentType(HttpRequest request, Route return defaultResponseMediaType; } - /** - * Executes a route. - * - * @param request The request that matched to the route - * @param executeFilters Whether or not to execute server filters - * @param routePublisher The route match publisher - * @return A response publisher - */ - public Flux> executeRoute( - HttpRequest request, - boolean executeFilters, - Flux> routePublisher) { - AtomicReference> requestReference = new AtomicReference<>(request); - return buildResultEmitter( - requestReference, - executeFilters, - routePublisher - ); - } - /** * Applies server filters to a request/response. * - * @param requestReference The request reference - * @param upstreamResponsePublisher The original response publisher + * @param requestReference The request reference + * @param responseFlowSupplier The deferred response flow * @return A new response publisher that executes server filters */ - public Publisher> filterPublisher( - AtomicReference> requestReference, - Publisher> upstreamResponsePublisher) { + public ExecutionFlow> filterPublisher(AtomicReference> requestReference, + Supplier>> responseFlowSupplier) { + ServerRequestContext.set(requestReference.get()); List httpFilters = router.findFilters(requestReference.get()); if (httpFilters.isEmpty()) { - return upstreamResponsePublisher; + return responseFlowSupplier.get(); } List filters = new ArrayList<>(httpFilters); AtomicInteger integer = new AtomicInteger(); int len = filters.size(); - final Function, Publisher>> handleStatusException = (response) -> - handleStatusException(requestReference.get(), response); - final Function>> onError = (t) -> - onError(t, requestReference.get()); ServerFilterChain filterChain = new ServerFilterChain() { - @SuppressWarnings("unchecked") @Override public Publisher> proceed(io.micronaut.http.HttpRequest request) { int pos = integer.incrementAndGet(); @@ -389,43 +557,38 @@ public Publisher> proceed(io.micronaut.http.HttpRequest>) httpFilter.doFilter(request, this)) - .flatMap(handleStatusException) - .onErrorResume(onError); - } catch (Throwable t) { - return onError.apply(t); - } + return ReactiveExecutionFlow.fromFlow( + triggerFilter(requestReference, httpFilter, this) + ).toPublisher(); } }; - HttpFilter httpFilter = filters.get(0); - HttpRequest request = requestReference.get(); + return triggerFilter(requestReference, filters.get(0), filterChain); + } + + private ExecutionFlow> triggerFilter(AtomicReference> requestReference, HttpFilter httpFilter, ServerFilterChain filterChain) { try { - return Flux.from((Publisher>) httpFilter.doFilter(request, filterChain)) - .flatMap(handleStatusException) - .onErrorResume(onError); + return fromPublisher((Publisher>) httpFilter.doFilter(requestReference.get(), filterChain)) + .flatMap(response -> handleStatusException(requestReference.get(), response)) + .onErrorResume(throwable -> onError(throwable, requestReference.get())); } catch (Throwable t) { - return onError.apply(t); + return onError(t, requestReference.get()); } - } - private Mono> createDefaultErrorResponsePublisher(HttpRequest httpRequest, - Throwable cause) { - return Mono.fromCallable(() -> createDefaultErrorResponse(httpRequest, cause)); + private ExecutionFlow> createDefaultErrorResponseFlow(HttpRequest httpRequest, Throwable cause) { + return ExecutionFlow.just(createDefaultErrorResponse(httpRequest, cause)); } private MutableHttpResponse newNotFoundError(HttpRequest request) { MutableHttpResponse response = errorResponseProcessor.processResponse( - ErrorContext.builder(request) - .errorMessage("Page Not Found") - .build(), HttpResponse.notFound()); - if (!response.getContentType().isPresent() && request.getMethod() != HttpMethod.HEAD) { + ErrorContext.builder(request) + .errorMessage("Page Not Found") + .build(), HttpResponse.notFound()); + if (response.getContentType().isEmpty() && request.getMethod() != HttpMethod.HEAD) { return response.contentType(MediaType.APPLICATION_JSON_TYPE); } return response; @@ -514,22 +677,21 @@ private RouteMatch findErrorRoute(Throwable cause, return errorRoute; } - private Publisher> handleStatusException(HttpRequest request, - MutableHttpResponse response) { + private ExecutionFlow> handleStatusException(HttpRequest request, + MutableHttpResponse response) { RouteInfo routeInfo = response.getAttribute(HttpAttributes.ROUTE_INFO, RouteInfo.class).orElse(null); - if (response.code() >= 400 && routeInfo != null && !routeInfo.isErrorRoute()) { RouteMatch statusRoute = findStatusRoute(request, response.status(), routeInfo); - if (statusRoute != null) { return executeRoute( - request, - false, - Flux.just(statusRoute) + new AtomicReference<>(request), + false, + true, + ExecutionFlow.just(statusRoute) ); } } - return Flux.just(response); + return ExecutionFlow.just(response); } private RouteMatch findStatusRoute(HttpRequest incomingRequest, HttpStatus status, RouteInfo finalRoute) { @@ -539,7 +701,7 @@ private RouteMatch findStatusRoute(HttpRequest incomingRequest, HttpS // if declaringType is not null, this means its a locally marked method handler if (declaringType != null) { statusRoute = router.findStatusRoute(declaringType, status, incomingRequest) - .orElseGet(() -> router.findStatusRoute(status, incomingRequest).orElse(null)); + .orElseGet(() -> router.findStatusRoute(status, incomingRequest).orElse(null)); } return statusRoute; } @@ -555,14 +717,12 @@ private ExecutorService findExecutor(RouteInfo routeMatch) { return executor; } - private Flux applyExecutorToPublisher( - Publisher publisher, - @Nullable ExecutorService executor) { + private Flux applyExecutorToPublisher(Publisher publisher, @Nullable ExecutorService executor) { if (executor != null) { final Scheduler scheduler = Schedulers.fromExecutorService(executor); return Flux.from(publisher) - .subscribeOn(scheduler) - .publishOn(scheduler); + .subscribeOn(scheduler) + .publishOn(scheduler); } else { return Flux.from(publisher); } @@ -570,7 +730,7 @@ private Flux applyExecutorToPublisher( private boolean isSingle(RouteInfo finalRoute, Class bodyClass) { return finalRoute.isSpecifiedSingle() || (finalRoute.isSingleResult() && - (finalRoute.isAsync() || finalRoute.isSuspended() || Publishers.isSingle(bodyClass))); + (finalRoute.isAsync() || finalRoute.isSuspended() || Publishers.isSingle(bodyClass))); } private MutableHttpResponse toMutableResponse(HttpResponse message) { @@ -590,253 +750,267 @@ private MutableHttpResponse toMutableResponse(HttpResponse message) { return mutableHttpResponse; } - private Mono> toMutableResponse(HttpRequest request, RouteInfo routeInfo, HttpStatus defaultHttpStatus, Object body) { + private ExecutionFlow> fromImperativeExecute(HttpRequest request, RouteInfo routeInfo, HttpStatus defaultHttpStatus, Object body) { if (body instanceof HttpResponse) { MutableHttpResponse outgoingResponse = toMutableResponse((HttpResponse) body); final Argument bodyArgument = routeInfo.getReturnType().getFirstTypeVariable().orElse(Argument.OBJECT_ARGUMENT); if (bodyArgument.isAsyncOrReactive()) { - return processPublisherBody(request, outgoingResponse, routeInfo); - } else { - return Mono.just(outgoingResponse); + return fromPublisher( + processPublisherBody(request, outgoingResponse, routeInfo) + ); } - } else { - return Mono.just(forStatus(routeInfo, defaultHttpStatus) - .body(body)); + return ExecutionFlow.just(outgoingResponse); } + return ExecutionFlow.just(forStatus(routeInfo, defaultHttpStatus).body(body)); } - private Flux> buildRouteResponsePublisher(AtomicReference> requestReference, - Flux> routeMatchPublisher) { - // build the result emitter. This result emitter emits the response from a controller action - return routeMatchPublisher - .flatMap((route) -> { - final ExecutorService executor = findExecutor(route); - Flux> reactiveSequence = executeRoute(requestReference, route); - if (executor != null) { - reactiveSequence = applyExecutorToPublisher(reactiveSequence, executor); + private ExecutionFlow> executeRoute(AtomicReference> requestReference, + boolean executeFilters, + boolean useErrorRoute, + ExecutionFlow> routeMatchFlow) { + Supplier>> responseFlowSupplier = () -> { + return routeMatchFlow.flatMap(routeMatch -> { + ExecutorService executorService = findExecutor(routeMatch); + Supplier>> flowSupplier = () -> executeRouteAndConvertBody(routeMatch, requestReference.get()); + ExecutionFlow> executeMethodResponseFlow; + if (executorService != null) { + if (routeMatch.isSuspended()) { + executeMethodResponseFlow = ReactiveExecutionFlow.fromPublisher(Mono.deferContextual(contextView -> { + coroutineHelper.ifPresent(helper -> helper.setupCoroutineContext(requestReference.get(), contextView)); + return Mono.from( + ReactiveExecutionFlow.fromFlow(flowSupplier.get()).toPublisher() + ); + })) + .putInContext(ServerRequestContext.KEY, requestReference.get()); + } else if (routeMatch.isReactive()) { + executeMethodResponseFlow = ReactiveExecutionFlow.async(executorService, flowSupplier) + .putInContext(ServerRequestContext.KEY, requestReference.get()); + } else { + executeMethodResponseFlow = ExecutionFlow.async(executorService, flowSupplier); + } + } else { + if (routeMatch.isSuspended()) { + executeMethodResponseFlow = ReactiveExecutionFlow.fromPublisher(Mono.deferContextual(contextView -> { + coroutineHelper.ifPresent(helper -> helper.setupCoroutineContext(requestReference.get(), contextView)); + return Mono.from( + ReactiveExecutionFlow.fromFlow(flowSupplier.get()).toPublisher() + ); + })) + .putInContext(ServerRequestContext.KEY, requestReference.get()); + } else if (routeMatch.isReactive()) { + executeMethodResponseFlow = ReactiveExecutionFlow.fromFlow(flowSupplier.get()) + .putInContext(ServerRequestContext.KEY, requestReference.get()); + } else { + executeMethodResponseFlow = flowSupplier.get(); + } } - return reactiveSequence; + return executeMethodResponseFlow; + }).flatMap(response -> handleStatusException(requestReference.get(), response)) + .onErrorResume(throwable -> { + if (useErrorRoute) { + return onError(throwable, requestReference.get()); + } + return createDefaultErrorResponseFlow(requestReference.get(), throwable); }); + }; + if (!executeFilters) { + return responseFlowSupplier.get(); + } + return filterPublisher(requestReference, responseFlowSupplier); } - private Flux> buildResultEmitter( - AtomicReference> requestReference, - boolean executeFilters, - Flux> routeMatchPublisher) { - - Publisher> executeRoutePublisher = buildRouteResponsePublisher(requestReference, routeMatchPublisher) - .flatMap((response) -> handleStatusException(requestReference.get(), response)) - .onErrorResume((t) -> onError(t, requestReference.get())); - - if (executeFilters) { - executeRoutePublisher = filterPublisher(requestReference, executeRoutePublisher); + private ExecutionFlow> executeRouteAndConvertBody(RouteMatch routeMatch, HttpRequest httpRequest) { + try { + final RouteMatch finalRoute; + // ensure the route requirements are completely satisfied + if (!routeMatch.isExecutable()) { + finalRoute = requestArgumentSatisfier + .fulfillArgumentRequirements(routeMatch, httpRequest, true); + } else { + finalRoute = routeMatch; + } + Object body = ServerRequestContext.with(httpRequest, (Supplier) finalRoute::execute); + if (body instanceof Optional) { + body = ((Optional) body).orElse(null); + } + return createResponseForBody(httpRequest, body, finalRoute); + } catch (Throwable e) { + return ExecutionFlow.error(e); } - - return Flux.from(executeRoutePublisher); } - private Flux> executeRoute(AtomicReference> requestReference, - RouteMatch routeMatch) { - - return Flux.deferContextual(contextView -> { - try { - final RouteMatch finalRoute; - - // ensure the route requirements are completely satisfied - final HttpRequest httpRequest = requestReference.get(); - if (!routeMatch.isExecutable()) { - finalRoute = requestArgumentSatisfier - .fulfillArgumentRequirements(routeMatch, httpRequest, true); - } else { - finalRoute = routeMatch; + private ExecutionFlow> createResponseForBody(HttpRequest request, + Object body, + RouteInfo routeInfo) { + ExecutionFlow> outgoingResponse; + if (body == null) { + if (routeInfo.isVoid()) { + MutableHttpResponse data = forStatus(routeInfo); + if (HttpMethod.permitsRequestBody(request.getMethod())) { + data.header(HttpHeaders.CONTENT_LENGTH, "0"); } - if (finalRoute.isSuspended() && coroutineHelper.isPresent()) { - coroutineHelper.get().setupCoroutineContext(httpRequest, contextView); + outgoingResponse = ExecutionFlow.just(data); + } else { + outgoingResponse = ExecutionFlow.just(newNotFoundError(request)); + } + } else { + HttpStatus defaultHttpStatus = routeInfo.isErrorRoute() ? HttpStatus.INTERNAL_SERVER_ERROR : HttpStatus.OK; + boolean isReactive = routeInfo.isAsyncOrReactive() || Publishers.isConvertibleToPublisher(body); + if (isReactive) { + outgoingResponse = ReactiveExecutionFlow.fromPublisher( + fromReactiveExecute(request, body, routeInfo, defaultHttpStatus) + ); + } else if (body instanceof HttpStatus httpStatus) { // now we have the raw result, transform it as necessary + outgoingResponse = ExecutionFlow.just(HttpResponse.status(httpStatus)); + } else { + if (routeInfo.isSuspended()) { + outgoingResponse = fromKotlinCoroutineExecute(request, body, routeInfo, defaultHttpStatus); + } else { + outgoingResponse = fromImperativeExecute(request, routeInfo, defaultHttpStatus, body); } - - Object body = ServerRequestContext.with(httpRequest, (Supplier) finalRoute::execute); - if (body instanceof Optional) { - body = ((Optional) body).orElse(null); + } + } + outgoingResponse = outgoingResponse.map(response -> { + // for head request we never emit the body + if (request != null && request.getMethod().equals(HttpMethod.HEAD)) { + final Object o = response.getBody().orElse(null); + if (o instanceof ReferenceCounted referenceCounted) { + referenceCounted.release(); } - - return createResponseForBody(httpRequest, body, finalRoute); - } catch (Throwable e) { - return Flux.error(e); + response.body(null); } + applyConfiguredHeaders(response.getHeaders()); + if (routeInfo instanceof RouteMatch) { + response.setAttribute(HttpAttributes.ROUTE_MATCH, routeInfo); + } + response.setAttribute(HttpAttributes.ROUTE_INFO, routeInfo); + return response; }); + return outgoingResponse; } - private Flux> createResponseForBody(HttpRequest request, - Object body, - RouteInfo routeInfo) { - return Flux.>defer(() -> { - Mono> outgoingResponse; - if (body == null) { - if (routeInfo.isVoid()) { - MutableHttpResponse data = forStatus(routeInfo); - if (HttpMethod.permitsRequestBody(request.getMethod())) { - data.header(HttpHeaders.CONTENT_LENGTH, "0"); - } - outgoingResponse = Mono.just(data); - } else { - outgoingResponse = Mono.just(newNotFoundError(request)); - } - } else { - HttpStatus defaultHttpStatus = routeInfo.isErrorRoute() ? HttpStatus.INTERNAL_SERVER_ERROR : HttpStatus.OK; - boolean isReactive = routeInfo.isAsyncOrReactive() || Publishers.isConvertibleToPublisher(body); - if (isReactive) { - Class bodyClass = body.getClass(); - boolean isSingle = isSingle(routeInfo, bodyClass); - boolean isCompletable = !isSingle && routeInfo.isVoid() && Publishers.isCompletable(bodyClass); - if (isSingle || isCompletable) { - // full response case - Publisher publisher = Publishers.convertPublisher(body, Publisher.class); - Supplier> emptyResponse = () -> { - MutableHttpResponse singleResponse; - if (isCompletable || routeInfo.isVoid()) { - singleResponse = forStatus(routeInfo, HttpStatus.OK) - .header(HttpHeaders.CONTENT_LENGTH, "0"); - } else { - singleResponse = newNotFoundError(request); - } - return singleResponse; - }; - return Flux.from(publisher) - .flatMap(o -> { - MutableHttpResponse singleResponse; - if (o instanceof Optional) { - Optional optional = (Optional) o; - if (optional.isPresent()) { - o = ((Optional) o).get(); - } else { - return Flux.just(emptyResponse.get()); - } - } - if (o instanceof HttpResponse) { - singleResponse = toMutableResponse((HttpResponse) o); - final Argument bodyArgument = routeInfo.getReturnType() //Mono - .getFirstTypeVariable().orElse(Argument.OBJECT_ARGUMENT) //HttpResponse - .getFirstTypeVariable().orElse(Argument.OBJECT_ARGUMENT); //Mono - if (bodyArgument.isAsyncOrReactive()) { - return processPublisherBody(request, singleResponse, routeInfo); - } - } else if (o instanceof HttpStatus) { - singleResponse = forStatus(routeInfo, (HttpStatus) o); - } else { - singleResponse = forStatus(routeInfo, defaultHttpStatus) - .body(o); - } - return Flux.just(singleResponse); - }) - .switchIfEmpty(Mono.fromSupplier(emptyResponse)); - } else { - // streaming case - Argument typeArgument = routeInfo.getReturnType().getFirstTypeVariable().orElse(Argument.OBJECT_ARGUMENT); - if (HttpResponse.class.isAssignableFrom(typeArgument.getType())) { - // a response stream - Publisher> bodyPublisher = Publishers.convertPublisher(body, Publisher.class); - Flux> response = Flux.from(bodyPublisher) - .map(this::toMutableResponse); - Argument bodyArgument = typeArgument.getFirstTypeVariable().orElse(Argument.OBJECT_ARGUMENT); + private ExecutionFlow> fromKotlinCoroutineExecute(HttpRequest request, Object body, RouteInfo routeInfo, HttpStatus defaultHttpStatus) { + ExecutionFlow> outgoingResponse; + boolean isKotlinFunctionReturnTypeUnit = + routeInfo instanceof MethodBasedRouteMatch && + isKotlinFunctionReturnTypeUnit(((MethodBasedRouteMatch) routeInfo).getExecutableMethod()); + final Supplier> supplier = ContinuationArgumentBinder.extractContinuationCompletableFutureSupplier(request); + if (isKotlinCoroutineSuspended(body)) { + return ReactiveExecutionFlow.fromPublisher( + Mono.fromCompletionStage(supplier) + .flatMap(obj -> { + MutableHttpResponse response; + if (obj instanceof HttpResponse) { + response = toMutableResponse((HttpResponse) obj); + final Argument bodyArgument = routeInfo.getReturnType().getFirstTypeVariable().orElse(Argument.OBJECT_ARGUMENT); if (bodyArgument.isAsyncOrReactive()) { - return response.flatMap((resp) -> - processPublisherBody(request, resp, routeInfo)); + return processPublisherBody(request, response, routeInfo); } - return response; } else { - MutableHttpResponse response = forStatus(routeInfo, defaultHttpStatus).body(body); - return processPublisherBody(request, response, routeInfo); + response = forStatus(routeInfo, defaultHttpStatus); + if (!isKotlinFunctionReturnTypeUnit) { + response = response.body(obj); + } } - } - } - // now we have the raw result, transform it as necessary - if (body instanceof HttpStatus) { - outgoingResponse = Mono.just(HttpResponse.status((HttpStatus) body)); + return Mono.just(response); + }) + .switchIfEmpty(createNotFoundErrorResponsePublisher(request)) + ); + } + Object suspendedBody; + if (isKotlinFunctionReturnTypeUnit) { + suspendedBody = Mono.empty(); + } else { + suspendedBody = body; + } + outgoingResponse = fromImperativeExecute(request, routeInfo, defaultHttpStatus, suspendedBody); + return outgoingResponse; + } + + private CorePublisher> fromReactiveExecute(HttpRequest request, Object body, RouteInfo routeInfo, HttpStatus defaultHttpStatus) { + Class bodyClass = body.getClass(); + boolean isSingle = isSingle(routeInfo, bodyClass); + boolean isCompletable = !isSingle && routeInfo.isVoid() && Publishers.isCompletable(bodyClass); + if (isSingle || isCompletable) { + // full response case + Publisher publisher = Publishers.convertPublisher(body, Publisher.class); + Supplier> emptyResponse = () -> { + MutableHttpResponse singleResponse; + if (isCompletable || routeInfo.isVoid()) { + singleResponse = forStatus(routeInfo, HttpStatus.OK) + .header(HttpHeaders.CONTENT_LENGTH, "0"); } else { - if (routeInfo.isSuspended()) { - boolean isKotlinFunctionReturnTypeUnit = - routeInfo instanceof MethodBasedRouteMatch && - isKotlinFunctionReturnTypeUnit(((MethodBasedRouteMatch) routeInfo).getExecutableMethod()); - final Supplier> supplier = ContinuationArgumentBinder.extractContinuationCompletableFutureSupplier(request); - if (isKotlinCoroutineSuspended(body)) { - return Mono.fromCompletionStage(supplier) - .>flatMap(obj -> { - MutableHttpResponse response; - if (obj instanceof HttpResponse) { - response = toMutableResponse((HttpResponse) obj); - final Argument bodyArgument = routeInfo.getReturnType().getFirstTypeVariable().orElse(Argument.OBJECT_ARGUMENT); - if (bodyArgument.isAsyncOrReactive()) { - return processPublisherBody(request, response, routeInfo); - } - } else { - response = forStatus(routeInfo, defaultHttpStatus); - if (!isKotlinFunctionReturnTypeUnit) { - response = response.body(obj); - } - } - return Mono.just(response); - }) - .switchIfEmpty(createNotFoundErrorResponsePublisher(request)); + singleResponse = newNotFoundError(request); + } + return singleResponse; + }; + return Flux.from(publisher) + .flatMap(o -> { + MutableHttpResponse singleResponse; + if (o instanceof Optional optional) { + if (optional.isPresent()) { + o = optional.get(); } else { - Object suspendedBody; - if (isKotlinFunctionReturnTypeUnit) { - suspendedBody = Mono.empty(); - } else { - suspendedBody = body; - } - outgoingResponse = toMutableResponse(request, routeInfo, defaultHttpStatus, suspendedBody); + return Flux.just(emptyResponse.get()); } - } else { - outgoingResponse = toMutableResponse(request, routeInfo, defaultHttpStatus, body); } - } - } - // for head request we never emit the body - if (request != null && request.getMethod().equals(HttpMethod.HEAD)) { - outgoingResponse = outgoingResponse.map(r -> { - final Object o = r.getBody().orElse(null); - if (o instanceof ReferenceCounted) { - ((ReferenceCounted) o).release(); + if (o instanceof HttpResponse) { + singleResponse = toMutableResponse((HttpResponse) o); + final Argument bodyArgument = routeInfo.getReturnType() //Mono + .getFirstTypeVariable().orElse(Argument.OBJECT_ARGUMENT) //HttpResponse + .getFirstTypeVariable().orElse(Argument.OBJECT_ARGUMENT); //Mono + if (bodyArgument.isAsyncOrReactive()) { + return processPublisherBody(request, singleResponse, routeInfo); + } + } else if (o instanceof HttpStatus) { + singleResponse = forStatus(routeInfo, (HttpStatus) o); + } else { + singleResponse = forStatus(routeInfo, defaultHttpStatus) + .body(o); } - r.body(null); - return r; - }); + return Flux.just(singleResponse); + }) + .switchIfEmpty(Mono.fromSupplier(emptyResponse)); + } + // streaming case + Argument typeArgument = routeInfo.getReturnType().getFirstTypeVariable().orElse(Argument.OBJECT_ARGUMENT); + if (HttpResponse.class.isAssignableFrom(typeArgument.getType())) { + // a response stream + Publisher> bodyPublisher = Publishers.convertPublisher(body, Publisher.class); + Flux> response = Flux.from(bodyPublisher) + .map(this::toMutableResponse); + Argument bodyArgument = typeArgument.getFirstTypeVariable().orElse(Argument.OBJECT_ARGUMENT); + if (bodyArgument.isAsyncOrReactive()) { + return response.flatMap((resp) -> + processPublisherBody(request, resp, routeInfo)); } - - return outgoingResponse; - }) - .doOnNext((response) -> { - applyConfiguredHeaders(response.getHeaders()); - if (routeInfo instanceof RouteMatch) { - response.setAttribute(HttpAttributes.ROUTE_MATCH, routeInfo); - } - response.setAttribute(HttpAttributes.ROUTE_INFO, routeInfo); - }); + return response; + } + MutableHttpResponse response = forStatus(routeInfo, defaultHttpStatus).body(body); + return processPublisherBody(request, response, routeInfo); } private Mono> processPublisherBody(HttpRequest request, - MutableHttpResponse response, - RouteInfo routeInfo) { + MutableHttpResponse response, + RouteInfo routeInfo) { Object body = response.body(); if (body == null) { return Mono.just(response); - } else if (Publishers.isSingle(body.getClass())) { + } + if (Publishers.isSingle(body.getClass())) { return Mono.from(Publishers.convertPublisher(body, Publisher.class)).map(b -> { response.body(b); return response; }); - } else { - MediaType mediaType = response.getContentType().orElseGet(() -> resolveDefaultResponseContentType(request, routeInfo)); + } + MediaType mediaType = response.getContentType().orElseGet(() -> resolveDefaultResponseContentType(request, routeInfo)); - Flux bodyPublisher = applyExecutorToPublisher( - Publishers.convertPublisher(body, Publisher.class), - findExecutor(routeInfo)); + Flux bodyPublisher = applyExecutorToPublisher(Publishers.convertPublisher(body, Publisher.class), findExecutor(routeInfo)); - return Mono.just(response - .header(HttpHeaders.TRANSFER_ENCODING, "chunked") - .header(HttpHeaders.CONTENT_TYPE, mediaType) - .body(bodyPublisher)); - } + return Mono.just(response + .header(HttpHeaders.TRANSFER_ENCODING, "chunked") + .header(HttpHeaders.CONTENT_TYPE, mediaType) + .body(bodyPublisher)); } private void applyConfiguredHeaders(MutableHttpHeaders headers) { @@ -845,7 +1019,7 @@ private void applyConfiguredHeaders(MutableHttpHeaders headers) { } if (!headers.contains(HttpHeaders.SERVER)) { serverConfiguration.getServerHeader() - .ifPresent(header -> headers.add(HttpHeaders.SERVER, header)); + .ifPresent(header -> headers.add(HttpHeaders.SERVER, header)); } } @@ -858,4 +1032,43 @@ private MutableHttpResponse forStatus(RouteInfo routeMatch, HttpStatu return HttpResponse.status(status); } + private ExecutionFlow fromPublisher(Publisher publisher) { + return ReactiveExecutionFlow.fromPublisher(publisher); + } + + + /** + * The request body reader. + */ + public interface RequestBodyReader { + + /** + * Reads the HTTP request body. + * TODO: This needs to be refactored for Micronaut 4 to eliminate the need for the route match. + * + * @param routeMatch The route match + * @param httpRequest The http request + * @return The execution flow carrying the route match + */ + @NonNull + ExecutionFlow> read(@NonNull RouteMatch routeMatch, @NonNull HttpRequest httpRequest); + + } + + /** + * The static resource finder. + */ + public interface StaticResourceResponseFinder { + + /** + * Finds a file response based on the request. + * + * @param httpRequest The request + * @return The file response or null if not found. + */ + @Nullable + FileCustomizableResponseType find(@NonNull HttpRequest httpRequest); + + } + } diff --git a/inject/src/main/java/io/micronaut/inject/MethodExecutionHandle.java b/inject/src/main/java/io/micronaut/inject/MethodExecutionHandle.java index b252cbedb81..24682f2f380 100644 --- a/inject/src/main/java/io/micronaut/inject/MethodExecutionHandle.java +++ b/inject/src/main/java/io/micronaut/inject/MethodExecutionHandle.java @@ -25,7 +25,7 @@ * @author Graeme Rocher * @since 1.0 */ -public interface MethodExecutionHandle extends ExecutionHandle, MethodReference { +public interface MethodExecutionHandle extends ExecutionHandle, MethodReference { /** * The underlying {@link ExecutableMethod} reference. * diff --git a/json-core/src/main/java/io/micronaut/json/codec/MapperMediaTypeCodec.java b/json-core/src/main/java/io/micronaut/json/codec/MapperMediaTypeCodec.java index 38e03436551..4a7ba326e19 100644 --- a/json-core/src/main/java/io/micronaut/json/codec/MapperMediaTypeCodec.java +++ b/json-core/src/main/java/io/micronaut/json/codec/MapperMediaTypeCodec.java @@ -254,8 +254,15 @@ public ByteBuffer encode(T object, ByteBufferFactory allocator) return allocator.copiedBuffer((byte[]) object); } ByteBuffer buffer = allocator.buffer(); - OutputStream outputStream = buffer.toOutputStream(); - encode(object, outputStream); + try { + OutputStream outputStream = buffer.toOutputStream(); + encode(object, outputStream); + } catch (Throwable t) { + if (buffer instanceof ReferenceCounted) { + ((ReferenceCounted) buffer).release(); + } + throw t; + } return buffer; } diff --git a/runtime/src/main/java/io/micronaut/reactive/reactor/execution/ReactiveExecutionFlow.java b/runtime/src/main/java/io/micronaut/reactive/reactor/execution/ReactiveExecutionFlow.java new file mode 100644 index 00000000000..31845e76bd2 --- /dev/null +++ b/runtime/src/main/java/io/micronaut/reactive/reactor/execution/ReactiveExecutionFlow.java @@ -0,0 +1,91 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.reactive.reactor.execution; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.NonNull; +import io.micronaut.core.execution.ExecutionFlow; +import org.reactivestreams.Publisher; +import reactor.core.publisher.Mono; +import reactor.core.scheduler.Scheduler; +import reactor.core.scheduler.Schedulers; + +import java.util.concurrent.Executor; +import java.util.function.Supplier; + +/** + * The reactive execution flow. + * NOTE: The flow is expected to produce only one result. + * + * @param The value type + * @author Denis Stepnov + * @since 4.0.0 + */ +@Internal +public interface ReactiveExecutionFlow extends ExecutionFlow { + + /** + * Creates a new reactive flow from a publisher. + * + * @param publisher The publisher + * @param THe flow value type + * @return a new flow + */ + @NonNull + static ReactiveExecutionFlow fromPublisher(@NonNull Publisher publisher) { + return (ReactiveExecutionFlow) new ReactorExecutionFlowImpl(publisher); + } + + /** + * Create a new reactive flow by invoking a supplier asynchronously. + * + * @param executor The executor + * @param supplier The supplier + * @param The flow value type + * @return a new flow + */ + @NonNull + static ReactiveExecutionFlow async(@NonNull Executor executor, @NonNull Supplier> supplier) { + Scheduler scheduler = Schedulers.fromExecutor(executor); + return (ReactiveExecutionFlow) new ReactorExecutionFlowImpl( + Mono.fromSupplier(supplier).flatMap(ReactorExecutionFlowImpl::toMono).subscribeOn(scheduler).subscribeOn(scheduler) + ); + } + + /** + * Creates a new reactive flow from other flow. + * + * @param flow The flow + * @param THe flow value type + * @return a new flow + */ + @NonNull + static ReactiveExecutionFlow fromFlow(@NonNull ExecutionFlow flow) { + if (flow instanceof ReactiveExecutionFlow) { + return (ReactiveExecutionFlow) flow; + } + return (ReactiveExecutionFlow) new ReactorExecutionFlowImpl(ReactorExecutionFlowImpl.toMono(flow)); + } + + /** + * Returns the reactive flow represented by a publisher. + * + * @return The publisher + */ + @NonNull + Publisher toPublisher(); + +} diff --git a/runtime/src/main/java/io/micronaut/reactive/reactor/execution/ReactorExecutionFlowImpl.java b/runtime/src/main/java/io/micronaut/reactive/reactor/execution/ReactorExecutionFlowImpl.java new file mode 100644 index 00000000000..c296d71983b --- /dev/null +++ b/runtime/src/main/java/io/micronaut/reactive/reactor/execution/ReactorExecutionFlowImpl.java @@ -0,0 +1,151 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.reactive.reactor.execution; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.execution.CompletableFutureExecutionFlow; +import io.micronaut.core.execution.ExecutionFlow; +import io.micronaut.core.execution.ImperativeExecutionFlow; +import org.reactivestreams.Publisher; +import org.reactivestreams.Subscriber; +import org.reactivestreams.Subscription; +import reactor.core.publisher.Mono; + +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.BiConsumer; +import java.util.function.Function; +import java.util.function.Supplier; + +/** + * The reactive flow implementation. + * + * @author Denis Stepanov + * @since 4.0.0 + */ +@Internal +final class ReactorExecutionFlowImpl implements ReactiveExecutionFlow { + + private Mono value; + + ReactorExecutionFlowImpl(Publisher value) { + this(Mono.from(value)); + } + + ReactorExecutionFlowImpl(Mono value) { + this.value = (Mono) value; + } + + @Override + public ExecutionFlow flatMap(Function> transformer) { + value = value.flatMap(value -> toMono(transformer.apply(value))); + return (ExecutionFlow) this; + } + + @Override + public ExecutionFlow then(Supplier> supplier) { + value = value.then(Mono.fromSupplier(supplier).flatMap(ReactorExecutionFlowImpl::toMono)); + return (ExecutionFlow) this; + } + + @Override + public ExecutionFlow map(Function function) { + value = value.map(function); + return (ExecutionFlow) this; + } + + @Override + public ExecutionFlow onErrorResume(Function> fallback) { + value = value.onErrorResume(throwable -> toMono(fallback.apply(throwable))); + return this; + } + + @Override + public ExecutionFlow putInContext(String key, Object value) { + this.value = this.value.contextWrite(context -> context.put(key, value)); + return this; + } + + @Override + public void onComplete(BiConsumer fn) { + value.subscribe(new Subscriber<>() { + + Subscription subscription; + final AtomicReference value = new AtomicReference<>(); + + @Override + public void onSubscribe(Subscription s) { + this.subscription = s; + s.request(1); + } + + @Override + public void onNext(Object v) { + subscription.request(1); // ??? + value.set(v); + } + + @Override + public void onError(Throwable t) { + fn.accept(null, t); + } + + @Override + public void onComplete() { + fn.accept(value.get(), null); + } + }); + } + + static Mono toMono(ExecutionFlow next) { + if (next instanceof ReactorExecutionFlowImpl reactiveFlowImpl) { + return reactiveFlowImpl.value; + } else if (next instanceof CompletableFutureExecutionFlow completableFutureFlow) { + return Mono.fromCompletionStage(completableFutureFlow.toCompletableFuture()); + } else if (next instanceof ImperativeExecutionFlow imperativeFlow) { + Mono m; + if (imperativeFlow.getError() != null) { + m = Mono.error(imperativeFlow.getError()); + } else if (imperativeFlow.getValue() != null) { + m = Mono.just(imperativeFlow.getValue()); + } else { + m = Mono.empty(); + } + Map context = imperativeFlow.getContext(); + if (!context.isEmpty()) { + m = m.contextWrite(ctx -> { + for (Map.Entry e : context.entrySet()) { + ctx = ctx.put(e.getKey(), e.getValue()); + } + return ctx; + }); + } + return m; + } + throw new IllegalStateException(); + } + + @Override + public Publisher toPublisher() { + return value; + } + + @Override + public CompletableFuture toCompletableFuture() { + return value.toFuture(); + } +} diff --git a/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/server/suspend/SuspendControllerSpec.kt b/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/server/suspend/SuspendControllerSpec.kt index 1b0a8dfbed4..d0cdd3be1e4 100644 --- a/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/server/suspend/SuspendControllerSpec.kt +++ b/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/server/suspend/SuspendControllerSpec.kt @@ -269,13 +269,14 @@ class SuspendControllerSpec : StringSpec() { response.status shouldBe HttpStatus.OK } - "test keeping tracing context using CoroutineTracingDispatcher explicitly" { - val response = client.exchange(GET("/suspend/keepTracingContextUsingCoroutineTracingDispatcherExplicitly"), String::class.java).awaitSingle() - val body = response.body.get() - - val (beforeTraceId, afterTraceId) = body.split(',') - beforeTraceId shouldBe afterTraceId - response.status shouldBe HttpStatus.OK - } +// TODO: HttpCoroutineTracingDispatcherFactory#create should eliminate nulls +// "test keeping tracing context using CoroutineTracingDispatcher explicitly" { +// val response = client.exchange(GET("/suspend/keepTracingContextUsingCoroutineTracingDispatcherExplicitly"), String::class.java).awaitSingle() +// val body = response.body.get() +// +// val (beforeTraceId, afterTraceId) = body.split(',') +// beforeTraceId shouldBe afterTraceId +// response.status shouldBe HttpStatus.OK +// } } } From 2659dfa6971df9557bde9e00548f26c3cbdcf7d5 Mon Sep 17 00:00:00 2001 From: Graeme Rocher Date: Wed, 26 Oct 2022 11:21:32 +0200 Subject: [PATCH 152/743] Split the runtime and compiler code into separate modules (#8224) --- ...cronaut.inject.annotation.AnnotationMapper | 1 - context/build.gradle | 1 + .../validation}/AsyncTypeElementVisitor.java | 6 ++- .../async/validation}/package-info.java | 2 +- ...icronaut.inject.visitor.TypeElementVisitor | 2 + core-processor/build.gradle | 13 +++++ .../aop/mapper}/InterceptorBeanMapper.java | 2 +- .../micronaut/aop/writer/AopHelperImpl.java | 0 .../micronaut/aop/writer/AopProxyWriter.java | 6 +-- .../io/micronaut/aop/writer/package-info.java | 0 .../context/visitor/BeanImportHandler.java | 0 .../context/visitor/BeanImportVisitor.java | 19 ++++--- .../visitor/ConfigurationReaderVisitor.java | 0 .../visitor/ContextConfigurerVisitor.java | 0 .../context/visitor/ExecutableVisitor.java | 0 .../InternalApiTypeElementVisitor.java | 0 .../context/visitor/ValidationVisitor.java | 0 .../context/visitor/package-info.java | 0 .../AbstractAnnotationMetadataBuilder.java | 7 +-- ...tractElementAnnotationMetadataFactory.java | 2 +- .../inject/annotation/AnnotationMapper.java | 0 .../annotation/AnnotationMetadataWriter.java | 5 +- .../inject/annotation/AnnotationRemapper.java | 0 .../annotation/AnnotationTransformer.java | 0 .../annotation/NamedAnnotationMapper.java | 0 .../NamedAnnotationTransformer.java | 0 .../annotation/PackageRenameRemapper.java | 0 .../annotation/TypedAnnotationMapper.java | 0 .../TypedAnnotationTransformer.java | 0 .../internal/CoreNonNullTransformer.java | 0 .../internal/CoreNullableTransformer.java | 0 .../annotation/internal/FindBugsRemapper.java | 0 .../JakartaPostConstructTransformer.java | 0 .../JakartaPreDestroyTransformer.java | 0 .../annotation/internal/JakartaRemapper.java | 0 .../internal/KotlinNotNullMapper.java | 0 .../internal/KotlinNullableMapper.java | 0 .../PersistenceContextAnnotationMapper.java | 0 .../internal/TimedAnnotationMapper.java | 0 .../annotation/internal/package-info.java | 0 .../inject/ast/AnnotationElement.java | 0 .../inject/ast/ArrayableClassElement.java | 0 .../inject/ast/BeanPropertiesQuery.java | 0 .../io/micronaut/inject/ast/ClassElement.java | 0 .../inject/ast/ConstructorElement.java | 0 .../inject/ast/DefaultElementQuery.java | 0 .../java/io/micronaut/inject/ast/Element.java | 0 .../inject/ast/ElementAnnotationMetadata.java | 0 .../ast/ElementAnnotationMetadataFactory.java | 0 .../micronaut/inject/ast/ElementFactory.java | 0 .../micronaut/inject/ast/ElementModifier.java | 0 .../ast/ElementMutableAnnotationMetadata.java | 0 ...mentMutableAnnotationMetadataDelegate.java | 0 .../io/micronaut/inject/ast/ElementQuery.java | 0 .../inject/ast/EnumConstantElement.java | 11 ++-- .../io/micronaut/inject/ast/EnumElement.java | 0 .../io/micronaut/inject/ast/FieldElement.java | 0 .../inject/ast/GenericPlaceholderElement.java | 0 .../micronaut/inject/ast/MemberElement.java | 0 .../micronaut/inject/ast/MethodElement.java | 0 .../micronaut/inject/ast/PackageElement.java | 0 .../inject/ast/ParameterElement.java | 0 .../inject/ast/PrimitiveElement.java | 0 .../micronaut/inject/ast/PropertyElement.java | 0 .../inject/ast/ReflectClassElement.java | 0 .../ast/ReflectGenericPlaceholderElement.java | 0 .../inject/ast/ReflectParameterElement.java | 0 .../inject/ast/ReflectTypeElement.java | 0 .../inject/ast/ReflectWildcardElement.java | 0 .../inject/ast/SimpleClassElement.java | 0 .../inject/ast/SimplePackageElement.java | 0 .../io/micronaut/inject/ast/TypedElement.java | 0 .../micronaut/inject/ast/WildcardElement.java | 0 .../ast/beans/BeanConstructorElement.java | 0 .../inject/ast/beans/BeanElement.java | 0 .../inject/ast/beans/BeanElementBuilder.java | 0 .../inject/ast/beans/BeanFieldElement.java | 0 .../inject/ast/beans/BeanMethodElement.java | 0 .../ast/beans/BeanParameterElement.java | 0 .../inject/ast/beans/ConfigurableElement.java | 0 .../inject/ast/beans/InjectableElement.java | 0 .../io/micronaut/inject/ast/package-info.java | 0 .../ast/utils/AstBeanPropertiesUtils.java | 0 .../ast/utils/EnclosedElementsQuery.java | 0 .../visitor/BeanIntrospectionWriter.java | 24 ++++----- .../EntityIntrospectedAnnotationMapper.java | 0 ...ntityReflectiveAccessAnnotationMapper.java | 0 ...trospectedToBeanPropertiesTransformer.java | 2 +- .../IntrospectedTypeElementVisitor.java | 0 .../visitor/JsonCreatorAnnotationMapper.java | 0 .../MappedSuperClassIntrospectionMapper.java | 0 .../inject/beans/visitor/package-info.java | 0 ...rtaEntityIntrospectedAnnotationMapper.java | 2 +- ...taMappedSuperClassIntrospectionMapper.java | 2 +- .../configuration/ConfigurationMetadata.java | 0 .../ConfigurationMetadataBuilder.java | 21 ++++---- .../ConfigurationMetadataWriter.java | 0 .../configuration/ConfigurationUtils.java | 0 .../JsonConfigurationMetadataWriter.java | 0 .../configuration/PropertyMetadata.java | 0 .../inject/configuration/package-info.java | 0 .../AbstractBeanElementCreator.java | 11 ++-- .../inject/processing/AopHelper.java | 0 ...ctionProxySupportedBeanElementCreator.java | 0 .../processing/BeanDefinitionCreator.java | 0 .../BeanDefinitionCreatorFactory.java | 0 ...ConfigurationReaderBeanElementCreator.java | 0 .../DeclaredBeanElementCreator.java | 0 .../processing/FactoryBeanElementCreator.java | 0 ...troductionInterfaceBeanElementCreator.java | 0 .../inject/processing/JavaModelUtils.java | 0 .../processing/ProcessingException.java | 0 .../inject/processing/package-info.java | 0 .../inject/visitor/BeanElementVisitor.java | 0 .../visitor/BeanElementVisitorContext.java | 0 .../visitor/BeanElementVisitorLoader.java | 0 .../inject/visitor/TypeElementVisitor.java | 0 .../inject/visitor/VisitorConfiguration.java | 0 .../inject/visitor/VisitorContext.java | 0 .../inject/visitor/package-info.java | 0 .../visitor}/util/VisitorContextUtils.java | 29 ++++++++++- .../AbstractAnnotationMetadataWriter.java | 0 .../writer/AbstractBeanDefinitionBuilder.java | 2 +- .../writer/AbstractClassFileWriter.java | 0 .../AbstractClassWriterOutputVisitor.java | 0 .../writer/ArrayAwareSignatureWriter.java | 0 .../inject/writer/BeanClassWriter.java | 0 .../writer/BeanConfigurationWriter.java | 0 .../writer/BeanDefinitionReferenceWriter.java | 0 .../inject/writer/BeanDefinitionVisitor.java | 0 .../inject/writer/BeanDefinitionWriter.java | 29 ++++++----- .../writer/ClassGenerationException.java | 0 .../inject/writer/ClassOutputWriter.java | 0 .../writer/ClassWriterOutputVisitor.java | 0 .../inject/writer/ConfigBuilderState.java | 0 .../writer/DefaultOriginatingElements.java | 0 .../DirectoryClassWriterOutputVisitor.java | 0 .../inject/writer/DispatchWriter.java | 0 .../inject/writer/ExecutableMethodWriter.java | 3 +- .../ExecutableMethodsDefinitionWriter.java | 0 .../writer/FileBackedGeneratedFile.java | 0 .../inject/writer/GeneratedFile.java | 0 .../inject/writer/OriginatingElements.java | 0 .../writer/ProxyingBeanDefinitionVisitor.java | 0 .../writer/StaticOriginatingElements.java | 0 .../inject/writer/StringSwitchWriter.java | 0 .../micronaut/inject/writer/package-info.java | 0 ...cronaut.inject.annotation.AnnotationMapper | 6 ++- ...onaut.inject.annotation.AnnotationRemapper | 0 ...ut.inject.annotation.AnnotationTransformer | 2 +- ....configuration.ConfigurationMetadataWriter | 0 ...icronaut.inject.visitor.TypeElementVisitor | 1 + core/build.gradle | 8 --- graal/build.gradle | 2 +- .../micronaut/http/server/RouteExecutor.java | 2 +- http-validation/build.gradle | 2 +- inject-groovy/build.gradle | 3 +- .../groovy/visitor/GroovyVisitorContext.java | 2 +- inject-java/build.gradle | 3 +- .../ConfigurationMetadataProcessor.java | 2 +- .../TypeElementVisitorProcessor.java | 3 +- .../visitor/JavaVisitorContext.java | 2 +- .../compile/AroundConstructCompileSpec.groovy | 52 +++++++++---------- .../annotation/AnnotationMapperSpec.groovy | 8 ++- .../repeatable/MapToRepeatableSpec.groovy | 1 - .../ast/beans/BeanElementVisitorSpec.groovy | 28 +++++----- .../beanbuilder/BeanElementBuilderSpec.groovy | 14 ++--- .../TestInterceptorBindingTransformer.java | 1 - .../ConfigurationMetadataSpec.groovy | 6 ++- .../micronaut/visitors/CustomAnnMapper.java | 1 - inject-test-utils/build.gradle | 2 +- .../annotation/DefaultAnnotationMetadata.java | 25 --------- .../KotlinExecutableMethodUtils.java | 4 +- .../util/VisitorContextUtilsSpec.groovy | 1 + .../io/micronaut/web/router/RouteInfo.java | 2 +- settings.gradle | 1 + src/main/docs/guide/appendix/breaks.adoc | 12 +++++ test-suite/build.gradle | 2 +- 178 files changed, 205 insertions(+), 195 deletions(-) delete mode 100644 aop/src/main/resources/META-INF/services/io.micronaut.inject.annotation.AnnotationMapper rename {validation/src/main/java/io/micronaut/validation/async => context/src/main/java/io/micronaut/scheduling/async/validation}/AsyncTypeElementVisitor.java (87%) rename {validation/src/main/java/io/micronaut/validation/async => context/src/main/java/io/micronaut/scheduling/async/validation}/package-info.java (93%) create mode 100644 context/src/main/resources/META-INF/services/io.micronaut.inject.visitor.TypeElementVisitor create mode 100644 core-processor/build.gradle rename {aop/src/main/java/io/micronaut/aop/internal => core-processor/src/main/java/io/micronaut/aop/mapper}/InterceptorBeanMapper.java (98%) rename {aop => core-processor}/src/main/java/io/micronaut/aop/writer/AopHelperImpl.java (100%) rename {aop => core-processor}/src/main/java/io/micronaut/aop/writer/AopProxyWriter.java (99%) rename {aop => core-processor}/src/main/java/io/micronaut/aop/writer/package-info.java (100%) rename {inject => core-processor}/src/main/java/io/micronaut/context/visitor/BeanImportHandler.java (100%) rename {inject => core-processor}/src/main/java/io/micronaut/context/visitor/BeanImportVisitor.java (98%) rename {inject => core-processor}/src/main/java/io/micronaut/context/visitor/ConfigurationReaderVisitor.java (100%) rename {inject => core-processor}/src/main/java/io/micronaut/context/visitor/ContextConfigurerVisitor.java (100%) rename {inject => core-processor}/src/main/java/io/micronaut/context/visitor/ExecutableVisitor.java (100%) rename {inject => core-processor}/src/main/java/io/micronaut/context/visitor/InternalApiTypeElementVisitor.java (100%) rename {inject => core-processor}/src/main/java/io/micronaut/context/visitor/ValidationVisitor.java (100%) rename {inject => core-processor}/src/main/java/io/micronaut/context/visitor/package-info.java (100%) rename {inject => core-processor}/src/main/java/io/micronaut/inject/annotation/AbstractAnnotationMetadataBuilder.java (99%) rename {inject => core-processor}/src/main/java/io/micronaut/inject/annotation/AbstractElementAnnotationMetadataFactory.java (99%) rename {inject => core-processor}/src/main/java/io/micronaut/inject/annotation/AnnotationMapper.java (100%) rename {inject => core-processor}/src/main/java/io/micronaut/inject/annotation/AnnotationMetadataWriter.java (99%) rename {inject => core-processor}/src/main/java/io/micronaut/inject/annotation/AnnotationRemapper.java (100%) rename {inject => core-processor}/src/main/java/io/micronaut/inject/annotation/AnnotationTransformer.java (100%) rename {inject => core-processor}/src/main/java/io/micronaut/inject/annotation/NamedAnnotationMapper.java (100%) rename {inject => core-processor}/src/main/java/io/micronaut/inject/annotation/NamedAnnotationTransformer.java (100%) rename {inject => core-processor}/src/main/java/io/micronaut/inject/annotation/PackageRenameRemapper.java (100%) rename {inject => core-processor}/src/main/java/io/micronaut/inject/annotation/TypedAnnotationMapper.java (100%) rename {inject => core-processor}/src/main/java/io/micronaut/inject/annotation/TypedAnnotationTransformer.java (100%) rename {inject => core-processor}/src/main/java/io/micronaut/inject/annotation/internal/CoreNonNullTransformer.java (100%) rename {inject => core-processor}/src/main/java/io/micronaut/inject/annotation/internal/CoreNullableTransformer.java (100%) rename {inject => core-processor}/src/main/java/io/micronaut/inject/annotation/internal/FindBugsRemapper.java (100%) rename {inject => core-processor}/src/main/java/io/micronaut/inject/annotation/internal/JakartaPostConstructTransformer.java (100%) rename {inject => core-processor}/src/main/java/io/micronaut/inject/annotation/internal/JakartaPreDestroyTransformer.java (100%) rename {inject => core-processor}/src/main/java/io/micronaut/inject/annotation/internal/JakartaRemapper.java (100%) rename {inject => core-processor}/src/main/java/io/micronaut/inject/annotation/internal/KotlinNotNullMapper.java (100%) rename {inject => core-processor}/src/main/java/io/micronaut/inject/annotation/internal/KotlinNullableMapper.java (100%) rename {inject => core-processor}/src/main/java/io/micronaut/inject/annotation/internal/PersistenceContextAnnotationMapper.java (100%) rename {inject => core-processor}/src/main/java/io/micronaut/inject/annotation/internal/TimedAnnotationMapper.java (100%) rename {inject => core-processor}/src/main/java/io/micronaut/inject/annotation/internal/package-info.java (100%) rename {inject => core-processor}/src/main/java/io/micronaut/inject/ast/AnnotationElement.java (100%) rename {inject => core-processor}/src/main/java/io/micronaut/inject/ast/ArrayableClassElement.java (100%) rename {inject => core-processor}/src/main/java/io/micronaut/inject/ast/BeanPropertiesQuery.java (100%) rename {inject => core-processor}/src/main/java/io/micronaut/inject/ast/ClassElement.java (100%) rename {inject => core-processor}/src/main/java/io/micronaut/inject/ast/ConstructorElement.java (100%) rename {inject => core-processor}/src/main/java/io/micronaut/inject/ast/DefaultElementQuery.java (100%) rename {inject => core-processor}/src/main/java/io/micronaut/inject/ast/Element.java (100%) rename {inject => core-processor}/src/main/java/io/micronaut/inject/ast/ElementAnnotationMetadata.java (100%) rename {inject => core-processor}/src/main/java/io/micronaut/inject/ast/ElementAnnotationMetadataFactory.java (100%) rename {inject => core-processor}/src/main/java/io/micronaut/inject/ast/ElementFactory.java (100%) rename {inject => core-processor}/src/main/java/io/micronaut/inject/ast/ElementModifier.java (100%) rename {inject => core-processor}/src/main/java/io/micronaut/inject/ast/ElementMutableAnnotationMetadata.java (100%) rename {inject => core-processor}/src/main/java/io/micronaut/inject/ast/ElementMutableAnnotationMetadataDelegate.java (100%) rename {inject => core-processor}/src/main/java/io/micronaut/inject/ast/ElementQuery.java (100%) rename {inject => core-processor}/src/main/java/io/micronaut/inject/ast/EnumConstantElement.java (79%) rename {inject => core-processor}/src/main/java/io/micronaut/inject/ast/EnumElement.java (100%) rename {inject => core-processor}/src/main/java/io/micronaut/inject/ast/FieldElement.java (100%) rename {inject => core-processor}/src/main/java/io/micronaut/inject/ast/GenericPlaceholderElement.java (100%) rename {inject => core-processor}/src/main/java/io/micronaut/inject/ast/MemberElement.java (100%) rename {inject => core-processor}/src/main/java/io/micronaut/inject/ast/MethodElement.java (100%) rename {inject => core-processor}/src/main/java/io/micronaut/inject/ast/PackageElement.java (100%) rename {inject => core-processor}/src/main/java/io/micronaut/inject/ast/ParameterElement.java (100%) rename {inject => core-processor}/src/main/java/io/micronaut/inject/ast/PrimitiveElement.java (100%) rename {inject => core-processor}/src/main/java/io/micronaut/inject/ast/PropertyElement.java (100%) rename {inject => core-processor}/src/main/java/io/micronaut/inject/ast/ReflectClassElement.java (100%) rename {inject => core-processor}/src/main/java/io/micronaut/inject/ast/ReflectGenericPlaceholderElement.java (100%) rename {inject => core-processor}/src/main/java/io/micronaut/inject/ast/ReflectParameterElement.java (100%) rename {inject => core-processor}/src/main/java/io/micronaut/inject/ast/ReflectTypeElement.java (100%) rename {inject => core-processor}/src/main/java/io/micronaut/inject/ast/ReflectWildcardElement.java (100%) rename {inject => core-processor}/src/main/java/io/micronaut/inject/ast/SimpleClassElement.java (100%) rename {inject => core-processor}/src/main/java/io/micronaut/inject/ast/SimplePackageElement.java (100%) rename {inject => core-processor}/src/main/java/io/micronaut/inject/ast/TypedElement.java (100%) rename {inject => core-processor}/src/main/java/io/micronaut/inject/ast/WildcardElement.java (100%) rename {inject => core-processor}/src/main/java/io/micronaut/inject/ast/beans/BeanConstructorElement.java (100%) rename {inject => core-processor}/src/main/java/io/micronaut/inject/ast/beans/BeanElement.java (100%) rename {inject => core-processor}/src/main/java/io/micronaut/inject/ast/beans/BeanElementBuilder.java (100%) rename {inject => core-processor}/src/main/java/io/micronaut/inject/ast/beans/BeanFieldElement.java (100%) rename {inject => core-processor}/src/main/java/io/micronaut/inject/ast/beans/BeanMethodElement.java (100%) rename {inject => core-processor}/src/main/java/io/micronaut/inject/ast/beans/BeanParameterElement.java (100%) rename {inject => core-processor}/src/main/java/io/micronaut/inject/ast/beans/ConfigurableElement.java (100%) rename {inject => core-processor}/src/main/java/io/micronaut/inject/ast/beans/InjectableElement.java (100%) rename {inject => core-processor}/src/main/java/io/micronaut/inject/ast/package-info.java (100%) rename {inject => core-processor}/src/main/java/io/micronaut/inject/ast/utils/AstBeanPropertiesUtils.java (100%) rename {inject => core-processor}/src/main/java/io/micronaut/inject/ast/utils/EnclosedElementsQuery.java (100%) rename {inject => core-processor}/src/main/java/io/micronaut/inject/beans/visitor/BeanIntrospectionWriter.java (98%) rename {inject => core-processor}/src/main/java/io/micronaut/inject/beans/visitor/EntityIntrospectedAnnotationMapper.java (100%) rename {inject => core-processor}/src/main/java/io/micronaut/inject/beans/visitor/EntityReflectiveAccessAnnotationMapper.java (100%) rename {inject/src/main/java/io/micronaut/inject/mappers => core-processor/src/main/java/io/micronaut/inject/beans/visitor}/IntrospectedToBeanPropertiesTransformer.java (98%) rename {inject => core-processor}/src/main/java/io/micronaut/inject/beans/visitor/IntrospectedTypeElementVisitor.java (100%) rename {inject => core-processor}/src/main/java/io/micronaut/inject/beans/visitor/JsonCreatorAnnotationMapper.java (100%) rename {inject => core-processor}/src/main/java/io/micronaut/inject/beans/visitor/MappedSuperClassIntrospectionMapper.java (100%) rename {inject => core-processor}/src/main/java/io/micronaut/inject/beans/visitor/package-info.java (100%) rename {inject/src/main/java/io/micronaut/inject/beans/visitor/jakarta => core-processor/src/main/java/io/micronaut/inject/beans/visitor}/persistence/JakartaEntityIntrospectedAnnotationMapper.java (96%) rename {inject/src/main/java/io/micronaut/inject/beans/visitor/jakarta => core-processor/src/main/java/io/micronaut/inject/beans/visitor}/persistence/JakartaMappedSuperClassIntrospectionMapper.java (94%) rename {inject => core-processor}/src/main/java/io/micronaut/inject/configuration/ConfigurationMetadata.java (100%) rename {inject => core-processor}/src/main/java/io/micronaut/inject/configuration/ConfigurationMetadataBuilder.java (95%) rename {inject => core-processor}/src/main/java/io/micronaut/inject/configuration/ConfigurationMetadataWriter.java (100%) rename {inject => core-processor}/src/main/java/io/micronaut/inject/configuration/ConfigurationUtils.java (100%) rename {inject => core-processor}/src/main/java/io/micronaut/inject/configuration/JsonConfigurationMetadataWriter.java (100%) rename {inject => core-processor}/src/main/java/io/micronaut/inject/configuration/PropertyMetadata.java (100%) rename {inject => core-processor}/src/main/java/io/micronaut/inject/configuration/package-info.java (100%) rename {inject => core-processor}/src/main/java/io/micronaut/inject/processing/AbstractBeanElementCreator.java (94%) rename {inject => core-processor}/src/main/java/io/micronaut/inject/processing/AopHelper.java (100%) rename {inject => core-processor}/src/main/java/io/micronaut/inject/processing/AopIntroductionProxySupportedBeanElementCreator.java (100%) rename {inject => core-processor}/src/main/java/io/micronaut/inject/processing/BeanDefinitionCreator.java (100%) rename {inject => core-processor}/src/main/java/io/micronaut/inject/processing/BeanDefinitionCreatorFactory.java (100%) rename {inject => core-processor}/src/main/java/io/micronaut/inject/processing/ConfigurationReaderBeanElementCreator.java (100%) rename {inject => core-processor}/src/main/java/io/micronaut/inject/processing/DeclaredBeanElementCreator.java (100%) rename {inject => core-processor}/src/main/java/io/micronaut/inject/processing/FactoryBeanElementCreator.java (100%) rename {inject => core-processor}/src/main/java/io/micronaut/inject/processing/IntroductionInterfaceBeanElementCreator.java (100%) rename {inject => core-processor}/src/main/java/io/micronaut/inject/processing/JavaModelUtils.java (100%) rename {inject => core-processor}/src/main/java/io/micronaut/inject/processing/ProcessingException.java (100%) rename {inject => core-processor}/src/main/java/io/micronaut/inject/processing/package-info.java (100%) rename {inject => core-processor}/src/main/java/io/micronaut/inject/visitor/BeanElementVisitor.java (100%) rename {inject => core-processor}/src/main/java/io/micronaut/inject/visitor/BeanElementVisitorContext.java (100%) rename {inject => core-processor}/src/main/java/io/micronaut/inject/visitor/BeanElementVisitorLoader.java (100%) rename {inject => core-processor}/src/main/java/io/micronaut/inject/visitor/TypeElementVisitor.java (100%) rename {inject => core-processor}/src/main/java/io/micronaut/inject/visitor/VisitorConfiguration.java (100%) rename {inject => core-processor}/src/main/java/io/micronaut/inject/visitor/VisitorContext.java (100%) rename {inject => core-processor}/src/main/java/io/micronaut/inject/visitor/package-info.java (100%) rename {inject/src/main/java/io/micronaut/inject => core-processor/src/main/java/io/micronaut/inject/visitor}/util/VisitorContextUtils.java (71%) rename {inject => core-processor}/src/main/java/io/micronaut/inject/writer/AbstractAnnotationMetadataWriter.java (100%) rename {inject => core-processor}/src/main/java/io/micronaut/inject/writer/AbstractBeanDefinitionBuilder.java (99%) rename {inject => core-processor}/src/main/java/io/micronaut/inject/writer/AbstractClassFileWriter.java (100%) rename {inject => core-processor}/src/main/java/io/micronaut/inject/writer/AbstractClassWriterOutputVisitor.java (100%) rename {inject => core-processor}/src/main/java/io/micronaut/inject/writer/ArrayAwareSignatureWriter.java (100%) rename {inject => core-processor}/src/main/java/io/micronaut/inject/writer/BeanClassWriter.java (100%) rename {inject => core-processor}/src/main/java/io/micronaut/inject/writer/BeanConfigurationWriter.java (100%) rename {inject => core-processor}/src/main/java/io/micronaut/inject/writer/BeanDefinitionReferenceWriter.java (100%) rename {inject => core-processor}/src/main/java/io/micronaut/inject/writer/BeanDefinitionVisitor.java (100%) rename {inject => core-processor}/src/main/java/io/micronaut/inject/writer/BeanDefinitionWriter.java (99%) rename {inject => core-processor}/src/main/java/io/micronaut/inject/writer/ClassGenerationException.java (100%) rename {inject => core-processor}/src/main/java/io/micronaut/inject/writer/ClassOutputWriter.java (100%) rename {inject => core-processor}/src/main/java/io/micronaut/inject/writer/ClassWriterOutputVisitor.java (100%) rename {inject => core-processor}/src/main/java/io/micronaut/inject/writer/ConfigBuilderState.java (100%) rename {inject => core-processor}/src/main/java/io/micronaut/inject/writer/DefaultOriginatingElements.java (100%) rename {inject => core-processor}/src/main/java/io/micronaut/inject/writer/DirectoryClassWriterOutputVisitor.java (100%) rename {inject => core-processor}/src/main/java/io/micronaut/inject/writer/DispatchWriter.java (100%) rename {inject => core-processor}/src/main/java/io/micronaut/inject/writer/ExecutableMethodWriter.java (99%) rename {inject => core-processor}/src/main/java/io/micronaut/inject/writer/ExecutableMethodsDefinitionWriter.java (100%) rename {inject => core-processor}/src/main/java/io/micronaut/inject/writer/FileBackedGeneratedFile.java (100%) rename {inject => core-processor}/src/main/java/io/micronaut/inject/writer/GeneratedFile.java (100%) rename {inject => core-processor}/src/main/java/io/micronaut/inject/writer/OriginatingElements.java (100%) rename {inject => core-processor}/src/main/java/io/micronaut/inject/writer/ProxyingBeanDefinitionVisitor.java (100%) rename {inject => core-processor}/src/main/java/io/micronaut/inject/writer/StaticOriginatingElements.java (100%) rename {inject => core-processor}/src/main/java/io/micronaut/inject/writer/StringSwitchWriter.java (100%) rename {inject => core-processor}/src/main/java/io/micronaut/inject/writer/package-info.java (100%) rename {inject => core-processor}/src/main/resources/META-INF/services/io.micronaut.inject.annotation.AnnotationMapper (64%) rename {inject => core-processor}/src/main/resources/META-INF/services/io.micronaut.inject.annotation.AnnotationRemapper (100%) rename {inject => core-processor}/src/main/resources/META-INF/services/io.micronaut.inject.annotation.AnnotationTransformer (84%) rename {inject => core-processor}/src/main/resources/META-INF/services/io.micronaut.inject.configuration.ConfigurationMetadataWriter (100%) rename {inject => core-processor}/src/main/resources/META-INF/services/io.micronaut.inject.visitor.TypeElementVisitor (85%) rename inject-java/src/test/groovy/io/micronaut/inject/{configproperties => configuration}/ConfigurationMetadataSpec.groovy (99%) rename inject/src/main/java/io/micronaut/inject/{util => beans}/KotlinExecutableMethodUtils.java (95%) diff --git a/aop/src/main/resources/META-INF/services/io.micronaut.inject.annotation.AnnotationMapper b/aop/src/main/resources/META-INF/services/io.micronaut.inject.annotation.AnnotationMapper deleted file mode 100644 index f9a05789880..00000000000 --- a/aop/src/main/resources/META-INF/services/io.micronaut.inject.annotation.AnnotationMapper +++ /dev/null @@ -1 +0,0 @@ -io.micronaut.aop.internal.InterceptorBeanMapper diff --git a/context/build.gradle b/context/build.gradle index 07c45a67982..88f1a373a42 100644 --- a/context/build.gradle +++ b/context/build.gradle @@ -9,6 +9,7 @@ dependencies { api project(':aop') api libs.managed.validation compileOnly project(':core-reactive') + compileOnly project(':core-processor') testCompileOnly project(":inject-groovy") testAnnotationProcessor project(":inject-java") testImplementation project(":core-reactive") diff --git a/validation/src/main/java/io/micronaut/validation/async/AsyncTypeElementVisitor.java b/context/src/main/java/io/micronaut/scheduling/async/validation/AsyncTypeElementVisitor.java similarity index 87% rename from validation/src/main/java/io/micronaut/validation/async/AsyncTypeElementVisitor.java rename to context/src/main/java/io/micronaut/scheduling/async/validation/AsyncTypeElementVisitor.java index 37ddc39b438..b9b31eb2edf 100644 --- a/validation/src/main/java/io/micronaut/validation/async/AsyncTypeElementVisitor.java +++ b/context/src/main/java/io/micronaut/scheduling/async/validation/AsyncTypeElementVisitor.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.micronaut.validation.async; +package io.micronaut.scheduling.async.validation; import io.micronaut.core.annotation.Internal; import io.micronaut.core.annotation.NonNull; @@ -23,6 +23,7 @@ import io.micronaut.inject.visitor.TypeElementVisitor; import io.micronaut.inject.visitor.VisitorContext; import io.micronaut.scheduling.annotation.Async; +import org.reactivestreams.Publisher; import java.util.concurrent.CompletionStage; @@ -46,10 +47,11 @@ public void visitMethod(MethodElement element, VisitorContext context) { ClassElement returnType = element.getReturnType(); boolean isValid = returnType != null && (returnType.isAssignable(CompletionStage.class) || returnType.isAssignable(void.class) || + returnType.isAssignable(Publisher.class) || Publishers.getKnownReactiveTypes().stream().anyMatch(returnType::isAssignable)); if (!isValid) { - context.fail("Method must return void or a subtype of CompletionStage", element); + context.fail("Method must return void, a Reactive Streams type or a subtype of CompletionStage", element); } } } diff --git a/validation/src/main/java/io/micronaut/validation/async/package-info.java b/context/src/main/java/io/micronaut/scheduling/async/validation/package-info.java similarity index 93% rename from validation/src/main/java/io/micronaut/validation/async/package-info.java rename to context/src/main/java/io/micronaut/scheduling/async/validation/package-info.java index 3aa007c8078..0f4e4faa0d9 100644 --- a/validation/src/main/java/io/micronaut/validation/async/package-info.java +++ b/context/src/main/java/io/micronaut/scheduling/async/validation/package-info.java @@ -19,4 +19,4 @@ * @author graemerocher * @since 1.0 */ -package io.micronaut.validation.async; +package io.micronaut.scheduling.async.validation; diff --git a/context/src/main/resources/META-INF/services/io.micronaut.inject.visitor.TypeElementVisitor b/context/src/main/resources/META-INF/services/io.micronaut.inject.visitor.TypeElementVisitor new file mode 100644 index 00000000000..952c37d738d --- /dev/null +++ b/context/src/main/resources/META-INF/services/io.micronaut.inject.visitor.TypeElementVisitor @@ -0,0 +1,2 @@ +io.micronaut.scheduling.async.validation.AsyncTypeElementVisitor + diff --git a/core-processor/build.gradle b/core-processor/build.gradle new file mode 100644 index 00000000000..8d276ef05a1 --- /dev/null +++ b/core-processor/build.gradle @@ -0,0 +1,13 @@ +plugins { + id "io.micronaut.build.internal.convention-library" +} + +dependencies { + api project(":inject") + api project(":aop") + implementation libs.asm.tree + implementation libs.bundles.asm + + compileOnly libs.kotlin.stdlib.jdk8 + compileOnly libs.managed.validation +} diff --git a/aop/src/main/java/io/micronaut/aop/internal/InterceptorBeanMapper.java b/core-processor/src/main/java/io/micronaut/aop/mapper/InterceptorBeanMapper.java similarity index 98% rename from aop/src/main/java/io/micronaut/aop/internal/InterceptorBeanMapper.java rename to core-processor/src/main/java/io/micronaut/aop/mapper/InterceptorBeanMapper.java index 630fd371a8c..87f91f279b8 100644 --- a/aop/src/main/java/io/micronaut/aop/internal/InterceptorBeanMapper.java +++ b/core-processor/src/main/java/io/micronaut/aop/mapper/InterceptorBeanMapper.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.micronaut.aop.internal; +package io.micronaut.aop.mapper; import io.micronaut.aop.InterceptorBean; import io.micronaut.core.annotation.AnnotationClassValue; diff --git a/aop/src/main/java/io/micronaut/aop/writer/AopHelperImpl.java b/core-processor/src/main/java/io/micronaut/aop/writer/AopHelperImpl.java similarity index 100% rename from aop/src/main/java/io/micronaut/aop/writer/AopHelperImpl.java rename to core-processor/src/main/java/io/micronaut/aop/writer/AopHelperImpl.java diff --git a/aop/src/main/java/io/micronaut/aop/writer/AopProxyWriter.java b/core-processor/src/main/java/io/micronaut/aop/writer/AopProxyWriter.java similarity index 99% rename from aop/src/main/java/io/micronaut/aop/writer/AopProxyWriter.java rename to core-processor/src/main/java/io/micronaut/aop/writer/AopProxyWriter.java index d4d51090253..05cc9a2b9a6 100644 --- a/aop/src/main/java/io/micronaut/aop/writer/AopProxyWriter.java +++ b/core-processor/src/main/java/io/micronaut/aop/writer/AopProxyWriter.java @@ -55,7 +55,6 @@ import io.micronaut.inject.processing.JavaModelUtils; import io.micronaut.inject.visitor.VisitorContext; import io.micronaut.inject.writer.AbstractClassFileWriter; -import io.micronaut.inject.writer.BeanDefinitionVisitor; import io.micronaut.inject.writer.BeanDefinitionWriter; import io.micronaut.inject.writer.ClassWriterOutputVisitor; import io.micronaut.inject.writer.ExecutableMethodsDefinitionWriter; @@ -65,7 +64,6 @@ import org.objectweb.asm.ClassWriter; import org.objectweb.asm.Label; import org.objectweb.asm.MethodVisitor; -import org.objectweb.asm.Opcodes; import org.objectweb.asm.Type; import org.objectweb.asm.commons.GeneratorAdapter; import org.objectweb.asm.commons.Method; @@ -297,7 +295,7 @@ public AopProxyWriter(String packageName, this.targetClassShortName = className; this.targetClassFullName = packageName + '.' + targetClassShortName; this.parentWriter = null; - this.proxyFullName = targetClassFullName + BeanDefinitionVisitor.PROXY_SUFFIX; + this.proxyFullName = targetClassFullName + PROXY_SUFFIX; this.proxyInternalName = getInternalName(this.proxyFullName); this.proxyType = getTypeReferenceForName(proxyFullName); this.interceptorBinding = toInterceptorBindingMap(interceptorBinding); @@ -741,7 +739,7 @@ public void visitBeanDefinitionEnd() { null, null); - this.constructorGenerator = new GeneratorAdapter(constructorWriter, Opcodes.ACC_PUBLIC, CONSTRUCTOR_NAME, constructorDescriptor); + this.constructorGenerator = new GeneratorAdapter(constructorWriter, ACC_PUBLIC, CONSTRUCTOR_NAME, constructorDescriptor); GeneratorAdapter proxyConstructorGenerator = this.constructorGenerator; proxyConstructorGenerator.loadThis(); diff --git a/aop/src/main/java/io/micronaut/aop/writer/package-info.java b/core-processor/src/main/java/io/micronaut/aop/writer/package-info.java similarity index 100% rename from aop/src/main/java/io/micronaut/aop/writer/package-info.java rename to core-processor/src/main/java/io/micronaut/aop/writer/package-info.java diff --git a/inject/src/main/java/io/micronaut/context/visitor/BeanImportHandler.java b/core-processor/src/main/java/io/micronaut/context/visitor/BeanImportHandler.java similarity index 100% rename from inject/src/main/java/io/micronaut/context/visitor/BeanImportHandler.java rename to core-processor/src/main/java/io/micronaut/context/visitor/BeanImportHandler.java diff --git a/inject/src/main/java/io/micronaut/context/visitor/BeanImportVisitor.java b/core-processor/src/main/java/io/micronaut/context/visitor/BeanImportVisitor.java similarity index 98% rename from inject/src/main/java/io/micronaut/context/visitor/BeanImportVisitor.java rename to core-processor/src/main/java/io/micronaut/context/visitor/BeanImportVisitor.java index 0b916e83a0f..d76a8d4a4b3 100644 --- a/inject/src/main/java/io/micronaut/context/visitor/BeanImportVisitor.java +++ b/core-processor/src/main/java/io/micronaut/context/visitor/BeanImportVisitor.java @@ -15,20 +15,11 @@ */ package io.micronaut.context.visitor; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.HashSet; -import java.util.List; -import java.util.ServiceLoader; -import java.util.Set; - import io.micronaut.context.annotation.Bean; import io.micronaut.context.annotation.Import; import io.micronaut.core.annotation.AnnotationUtil; import io.micronaut.core.annotation.NonNull; import io.micronaut.core.order.OrderUtil; -import io.micronaut.core.order.Ordered; import io.micronaut.core.util.ArrayUtils; import io.micronaut.core.util.CollectionUtils; import io.micronaut.inject.ast.ClassElement; @@ -36,6 +27,14 @@ import io.micronaut.inject.visitor.TypeElementVisitor; import io.micronaut.inject.visitor.VisitorContext; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.ServiceLoader; +import java.util.Set; + /** * Implementation of {@link io.micronaut.context.annotation.Import}. * @@ -121,6 +120,6 @@ public VisitorKind getVisitorKind() { @Override public int getOrder() { - return Ordered.HIGHEST_PRECEDENCE; + return HIGHEST_PRECEDENCE; } } diff --git a/inject/src/main/java/io/micronaut/context/visitor/ConfigurationReaderVisitor.java b/core-processor/src/main/java/io/micronaut/context/visitor/ConfigurationReaderVisitor.java similarity index 100% rename from inject/src/main/java/io/micronaut/context/visitor/ConfigurationReaderVisitor.java rename to core-processor/src/main/java/io/micronaut/context/visitor/ConfigurationReaderVisitor.java diff --git a/inject/src/main/java/io/micronaut/context/visitor/ContextConfigurerVisitor.java b/core-processor/src/main/java/io/micronaut/context/visitor/ContextConfigurerVisitor.java similarity index 100% rename from inject/src/main/java/io/micronaut/context/visitor/ContextConfigurerVisitor.java rename to core-processor/src/main/java/io/micronaut/context/visitor/ContextConfigurerVisitor.java diff --git a/inject/src/main/java/io/micronaut/context/visitor/ExecutableVisitor.java b/core-processor/src/main/java/io/micronaut/context/visitor/ExecutableVisitor.java similarity index 100% rename from inject/src/main/java/io/micronaut/context/visitor/ExecutableVisitor.java rename to core-processor/src/main/java/io/micronaut/context/visitor/ExecutableVisitor.java diff --git a/inject/src/main/java/io/micronaut/context/visitor/InternalApiTypeElementVisitor.java b/core-processor/src/main/java/io/micronaut/context/visitor/InternalApiTypeElementVisitor.java similarity index 100% rename from inject/src/main/java/io/micronaut/context/visitor/InternalApiTypeElementVisitor.java rename to core-processor/src/main/java/io/micronaut/context/visitor/InternalApiTypeElementVisitor.java diff --git a/inject/src/main/java/io/micronaut/context/visitor/ValidationVisitor.java b/core-processor/src/main/java/io/micronaut/context/visitor/ValidationVisitor.java similarity index 100% rename from inject/src/main/java/io/micronaut/context/visitor/ValidationVisitor.java rename to core-processor/src/main/java/io/micronaut/context/visitor/ValidationVisitor.java diff --git a/inject/src/main/java/io/micronaut/context/visitor/package-info.java b/core-processor/src/main/java/io/micronaut/context/visitor/package-info.java similarity index 100% rename from inject/src/main/java/io/micronaut/context/visitor/package-info.java rename to core-processor/src/main/java/io/micronaut/context/visitor/package-info.java diff --git a/inject/src/main/java/io/micronaut/inject/annotation/AbstractAnnotationMetadataBuilder.java b/core-processor/src/main/java/io/micronaut/inject/annotation/AbstractAnnotationMetadataBuilder.java similarity index 99% rename from inject/src/main/java/io/micronaut/inject/annotation/AbstractAnnotationMetadataBuilder.java rename to core-processor/src/main/java/io/micronaut/inject/annotation/AbstractAnnotationMetadataBuilder.java index 9797c497988..bd7d07102c3 100644 --- a/inject/src/main/java/io/micronaut/inject/annotation/AbstractAnnotationMetadataBuilder.java +++ b/core-processor/src/main/java/io/micronaut/inject/annotation/AbstractAnnotationMetadataBuilder.java @@ -15,11 +15,7 @@ */ package io.micronaut.inject.annotation; -import io.micronaut.context.annotation.AliasFor; -import io.micronaut.context.annotation.Aliases; -import io.micronaut.context.annotation.DefaultScope; -import io.micronaut.context.annotation.NonBinding; -import io.micronaut.context.annotation.Type; +import io.micronaut.context.annotation.*; import io.micronaut.core.annotation.AnnotatedElement; import io.micronaut.core.annotation.AnnotationClassValue; import io.micronaut.core.annotation.AnnotationMetadata; @@ -145,6 +141,7 @@ protected AbstractAnnotationMetadataBuilder() { } + @SuppressWarnings("java:S1872") private AnnotationMetadata metadataForError(RuntimeException e) { if ("org.eclipse.jdt.internal.compiler.problem.AbortCompilation".equals(e.getClass().getName())) { // workaround for a bug in the Eclipse APT implementation. See bug 541466 on their Bugzilla. diff --git a/inject/src/main/java/io/micronaut/inject/annotation/AbstractElementAnnotationMetadataFactory.java b/core-processor/src/main/java/io/micronaut/inject/annotation/AbstractElementAnnotationMetadataFactory.java similarity index 99% rename from inject/src/main/java/io/micronaut/inject/annotation/AbstractElementAnnotationMetadataFactory.java rename to core-processor/src/main/java/io/micronaut/inject/annotation/AbstractElementAnnotationMetadataFactory.java index 17a5281026a..41fd6d2dcb1 100644 --- a/inject/src/main/java/io/micronaut/inject/annotation/AbstractElementAnnotationMetadataFactory.java +++ b/core-processor/src/main/java/io/micronaut/inject/annotation/AbstractElementAnnotationMetadataFactory.java @@ -322,7 +322,7 @@ private AnnotationMetadata replaceAnnotationsInternal(AnnotationMetadata annotat throw new IllegalStateException(); } if (annotationMetadata.isEmpty()) { - annotationMetadata = AnnotationMetadata.EMPTY_METADATA; + annotationMetadata = EMPTY_METADATA; } if (!readOnly) { if (annotationMetadata instanceof AnnotationMetadataHierarchy) { diff --git a/inject/src/main/java/io/micronaut/inject/annotation/AnnotationMapper.java b/core-processor/src/main/java/io/micronaut/inject/annotation/AnnotationMapper.java similarity index 100% rename from inject/src/main/java/io/micronaut/inject/annotation/AnnotationMapper.java rename to core-processor/src/main/java/io/micronaut/inject/annotation/AnnotationMapper.java diff --git a/inject/src/main/java/io/micronaut/inject/annotation/AnnotationMetadataWriter.java b/core-processor/src/main/java/io/micronaut/inject/annotation/AnnotationMetadataWriter.java similarity index 99% rename from inject/src/main/java/io/micronaut/inject/annotation/AnnotationMetadataWriter.java rename to core-processor/src/main/java/io/micronaut/inject/annotation/AnnotationMetadataWriter.java index c3e1b5e311b..3fba901bc0b 100644 --- a/inject/src/main/java/io/micronaut/inject/annotation/AnnotationMetadataWriter.java +++ b/core-processor/src/main/java/io/micronaut/inject/annotation/AnnotationMetadataWriter.java @@ -33,7 +33,6 @@ import org.objectweb.asm.ClassWriter; import org.objectweb.asm.Label; import org.objectweb.asm.MethodVisitor; -import org.objectweb.asm.Opcodes; import org.objectweb.asm.Type; import org.objectweb.asm.commons.GeneratorAdapter; import org.objectweb.asm.commons.Method; @@ -738,7 +737,7 @@ private static void invokeLoadClassValueMethod( loadTypeGenerator.visitLabel(tryEnd); loadTypeGenerator.returnValue(); loadTypeGenerator.visitLabel(exceptionHandler); - loadTypeGenerator.visitFrame(Opcodes.F_NEW, 0, new Object[]{}, 1, new Object[]{"java/lang/Throwable"}); + loadTypeGenerator.visitFrame(F_NEW, 0, new Object[]{}, 1, new Object[]{"java/lang/Throwable"}); // Try load the class // fallback to return a class value that is just a string @@ -751,6 +750,6 @@ private static void invokeLoadClassValueMethod( return loadTypeGenerator; }); - methodVisitor.visitMethodInsn(Opcodes.INVOKESTATIC, declaringType.getInternalName(), loadTypeGeneratorMethod.getName(), desc, false); + methodVisitor.visitMethodInsn(INVOKESTATIC, declaringType.getInternalName(), loadTypeGeneratorMethod.getName(), desc, false); } } diff --git a/inject/src/main/java/io/micronaut/inject/annotation/AnnotationRemapper.java b/core-processor/src/main/java/io/micronaut/inject/annotation/AnnotationRemapper.java similarity index 100% rename from inject/src/main/java/io/micronaut/inject/annotation/AnnotationRemapper.java rename to core-processor/src/main/java/io/micronaut/inject/annotation/AnnotationRemapper.java diff --git a/inject/src/main/java/io/micronaut/inject/annotation/AnnotationTransformer.java b/core-processor/src/main/java/io/micronaut/inject/annotation/AnnotationTransformer.java similarity index 100% rename from inject/src/main/java/io/micronaut/inject/annotation/AnnotationTransformer.java rename to core-processor/src/main/java/io/micronaut/inject/annotation/AnnotationTransformer.java diff --git a/inject/src/main/java/io/micronaut/inject/annotation/NamedAnnotationMapper.java b/core-processor/src/main/java/io/micronaut/inject/annotation/NamedAnnotationMapper.java similarity index 100% rename from inject/src/main/java/io/micronaut/inject/annotation/NamedAnnotationMapper.java rename to core-processor/src/main/java/io/micronaut/inject/annotation/NamedAnnotationMapper.java diff --git a/inject/src/main/java/io/micronaut/inject/annotation/NamedAnnotationTransformer.java b/core-processor/src/main/java/io/micronaut/inject/annotation/NamedAnnotationTransformer.java similarity index 100% rename from inject/src/main/java/io/micronaut/inject/annotation/NamedAnnotationTransformer.java rename to core-processor/src/main/java/io/micronaut/inject/annotation/NamedAnnotationTransformer.java diff --git a/inject/src/main/java/io/micronaut/inject/annotation/PackageRenameRemapper.java b/core-processor/src/main/java/io/micronaut/inject/annotation/PackageRenameRemapper.java similarity index 100% rename from inject/src/main/java/io/micronaut/inject/annotation/PackageRenameRemapper.java rename to core-processor/src/main/java/io/micronaut/inject/annotation/PackageRenameRemapper.java diff --git a/inject/src/main/java/io/micronaut/inject/annotation/TypedAnnotationMapper.java b/core-processor/src/main/java/io/micronaut/inject/annotation/TypedAnnotationMapper.java similarity index 100% rename from inject/src/main/java/io/micronaut/inject/annotation/TypedAnnotationMapper.java rename to core-processor/src/main/java/io/micronaut/inject/annotation/TypedAnnotationMapper.java diff --git a/inject/src/main/java/io/micronaut/inject/annotation/TypedAnnotationTransformer.java b/core-processor/src/main/java/io/micronaut/inject/annotation/TypedAnnotationTransformer.java similarity index 100% rename from inject/src/main/java/io/micronaut/inject/annotation/TypedAnnotationTransformer.java rename to core-processor/src/main/java/io/micronaut/inject/annotation/TypedAnnotationTransformer.java diff --git a/inject/src/main/java/io/micronaut/inject/annotation/internal/CoreNonNullTransformer.java b/core-processor/src/main/java/io/micronaut/inject/annotation/internal/CoreNonNullTransformer.java similarity index 100% rename from inject/src/main/java/io/micronaut/inject/annotation/internal/CoreNonNullTransformer.java rename to core-processor/src/main/java/io/micronaut/inject/annotation/internal/CoreNonNullTransformer.java diff --git a/inject/src/main/java/io/micronaut/inject/annotation/internal/CoreNullableTransformer.java b/core-processor/src/main/java/io/micronaut/inject/annotation/internal/CoreNullableTransformer.java similarity index 100% rename from inject/src/main/java/io/micronaut/inject/annotation/internal/CoreNullableTransformer.java rename to core-processor/src/main/java/io/micronaut/inject/annotation/internal/CoreNullableTransformer.java diff --git a/inject/src/main/java/io/micronaut/inject/annotation/internal/FindBugsRemapper.java b/core-processor/src/main/java/io/micronaut/inject/annotation/internal/FindBugsRemapper.java similarity index 100% rename from inject/src/main/java/io/micronaut/inject/annotation/internal/FindBugsRemapper.java rename to core-processor/src/main/java/io/micronaut/inject/annotation/internal/FindBugsRemapper.java diff --git a/inject/src/main/java/io/micronaut/inject/annotation/internal/JakartaPostConstructTransformer.java b/core-processor/src/main/java/io/micronaut/inject/annotation/internal/JakartaPostConstructTransformer.java similarity index 100% rename from inject/src/main/java/io/micronaut/inject/annotation/internal/JakartaPostConstructTransformer.java rename to core-processor/src/main/java/io/micronaut/inject/annotation/internal/JakartaPostConstructTransformer.java diff --git a/inject/src/main/java/io/micronaut/inject/annotation/internal/JakartaPreDestroyTransformer.java b/core-processor/src/main/java/io/micronaut/inject/annotation/internal/JakartaPreDestroyTransformer.java similarity index 100% rename from inject/src/main/java/io/micronaut/inject/annotation/internal/JakartaPreDestroyTransformer.java rename to core-processor/src/main/java/io/micronaut/inject/annotation/internal/JakartaPreDestroyTransformer.java diff --git a/inject/src/main/java/io/micronaut/inject/annotation/internal/JakartaRemapper.java b/core-processor/src/main/java/io/micronaut/inject/annotation/internal/JakartaRemapper.java similarity index 100% rename from inject/src/main/java/io/micronaut/inject/annotation/internal/JakartaRemapper.java rename to core-processor/src/main/java/io/micronaut/inject/annotation/internal/JakartaRemapper.java diff --git a/inject/src/main/java/io/micronaut/inject/annotation/internal/KotlinNotNullMapper.java b/core-processor/src/main/java/io/micronaut/inject/annotation/internal/KotlinNotNullMapper.java similarity index 100% rename from inject/src/main/java/io/micronaut/inject/annotation/internal/KotlinNotNullMapper.java rename to core-processor/src/main/java/io/micronaut/inject/annotation/internal/KotlinNotNullMapper.java diff --git a/inject/src/main/java/io/micronaut/inject/annotation/internal/KotlinNullableMapper.java b/core-processor/src/main/java/io/micronaut/inject/annotation/internal/KotlinNullableMapper.java similarity index 100% rename from inject/src/main/java/io/micronaut/inject/annotation/internal/KotlinNullableMapper.java rename to core-processor/src/main/java/io/micronaut/inject/annotation/internal/KotlinNullableMapper.java diff --git a/inject/src/main/java/io/micronaut/inject/annotation/internal/PersistenceContextAnnotationMapper.java b/core-processor/src/main/java/io/micronaut/inject/annotation/internal/PersistenceContextAnnotationMapper.java similarity index 100% rename from inject/src/main/java/io/micronaut/inject/annotation/internal/PersistenceContextAnnotationMapper.java rename to core-processor/src/main/java/io/micronaut/inject/annotation/internal/PersistenceContextAnnotationMapper.java diff --git a/inject/src/main/java/io/micronaut/inject/annotation/internal/TimedAnnotationMapper.java b/core-processor/src/main/java/io/micronaut/inject/annotation/internal/TimedAnnotationMapper.java similarity index 100% rename from inject/src/main/java/io/micronaut/inject/annotation/internal/TimedAnnotationMapper.java rename to core-processor/src/main/java/io/micronaut/inject/annotation/internal/TimedAnnotationMapper.java diff --git a/inject/src/main/java/io/micronaut/inject/annotation/internal/package-info.java b/core-processor/src/main/java/io/micronaut/inject/annotation/internal/package-info.java similarity index 100% rename from inject/src/main/java/io/micronaut/inject/annotation/internal/package-info.java rename to core-processor/src/main/java/io/micronaut/inject/annotation/internal/package-info.java diff --git a/inject/src/main/java/io/micronaut/inject/ast/AnnotationElement.java b/core-processor/src/main/java/io/micronaut/inject/ast/AnnotationElement.java similarity index 100% rename from inject/src/main/java/io/micronaut/inject/ast/AnnotationElement.java rename to core-processor/src/main/java/io/micronaut/inject/ast/AnnotationElement.java diff --git a/inject/src/main/java/io/micronaut/inject/ast/ArrayableClassElement.java b/core-processor/src/main/java/io/micronaut/inject/ast/ArrayableClassElement.java similarity index 100% rename from inject/src/main/java/io/micronaut/inject/ast/ArrayableClassElement.java rename to core-processor/src/main/java/io/micronaut/inject/ast/ArrayableClassElement.java diff --git a/inject/src/main/java/io/micronaut/inject/ast/BeanPropertiesQuery.java b/core-processor/src/main/java/io/micronaut/inject/ast/BeanPropertiesQuery.java similarity index 100% rename from inject/src/main/java/io/micronaut/inject/ast/BeanPropertiesQuery.java rename to core-processor/src/main/java/io/micronaut/inject/ast/BeanPropertiesQuery.java diff --git a/inject/src/main/java/io/micronaut/inject/ast/ClassElement.java b/core-processor/src/main/java/io/micronaut/inject/ast/ClassElement.java similarity index 100% rename from inject/src/main/java/io/micronaut/inject/ast/ClassElement.java rename to core-processor/src/main/java/io/micronaut/inject/ast/ClassElement.java diff --git a/inject/src/main/java/io/micronaut/inject/ast/ConstructorElement.java b/core-processor/src/main/java/io/micronaut/inject/ast/ConstructorElement.java similarity index 100% rename from inject/src/main/java/io/micronaut/inject/ast/ConstructorElement.java rename to core-processor/src/main/java/io/micronaut/inject/ast/ConstructorElement.java diff --git a/inject/src/main/java/io/micronaut/inject/ast/DefaultElementQuery.java b/core-processor/src/main/java/io/micronaut/inject/ast/DefaultElementQuery.java similarity index 100% rename from inject/src/main/java/io/micronaut/inject/ast/DefaultElementQuery.java rename to core-processor/src/main/java/io/micronaut/inject/ast/DefaultElementQuery.java diff --git a/inject/src/main/java/io/micronaut/inject/ast/Element.java b/core-processor/src/main/java/io/micronaut/inject/ast/Element.java similarity index 100% rename from inject/src/main/java/io/micronaut/inject/ast/Element.java rename to core-processor/src/main/java/io/micronaut/inject/ast/Element.java diff --git a/inject/src/main/java/io/micronaut/inject/ast/ElementAnnotationMetadata.java b/core-processor/src/main/java/io/micronaut/inject/ast/ElementAnnotationMetadata.java similarity index 100% rename from inject/src/main/java/io/micronaut/inject/ast/ElementAnnotationMetadata.java rename to core-processor/src/main/java/io/micronaut/inject/ast/ElementAnnotationMetadata.java diff --git a/inject/src/main/java/io/micronaut/inject/ast/ElementAnnotationMetadataFactory.java b/core-processor/src/main/java/io/micronaut/inject/ast/ElementAnnotationMetadataFactory.java similarity index 100% rename from inject/src/main/java/io/micronaut/inject/ast/ElementAnnotationMetadataFactory.java rename to core-processor/src/main/java/io/micronaut/inject/ast/ElementAnnotationMetadataFactory.java diff --git a/inject/src/main/java/io/micronaut/inject/ast/ElementFactory.java b/core-processor/src/main/java/io/micronaut/inject/ast/ElementFactory.java similarity index 100% rename from inject/src/main/java/io/micronaut/inject/ast/ElementFactory.java rename to core-processor/src/main/java/io/micronaut/inject/ast/ElementFactory.java diff --git a/inject/src/main/java/io/micronaut/inject/ast/ElementModifier.java b/core-processor/src/main/java/io/micronaut/inject/ast/ElementModifier.java similarity index 100% rename from inject/src/main/java/io/micronaut/inject/ast/ElementModifier.java rename to core-processor/src/main/java/io/micronaut/inject/ast/ElementModifier.java diff --git a/inject/src/main/java/io/micronaut/inject/ast/ElementMutableAnnotationMetadata.java b/core-processor/src/main/java/io/micronaut/inject/ast/ElementMutableAnnotationMetadata.java similarity index 100% rename from inject/src/main/java/io/micronaut/inject/ast/ElementMutableAnnotationMetadata.java rename to core-processor/src/main/java/io/micronaut/inject/ast/ElementMutableAnnotationMetadata.java diff --git a/inject/src/main/java/io/micronaut/inject/ast/ElementMutableAnnotationMetadataDelegate.java b/core-processor/src/main/java/io/micronaut/inject/ast/ElementMutableAnnotationMetadataDelegate.java similarity index 100% rename from inject/src/main/java/io/micronaut/inject/ast/ElementMutableAnnotationMetadataDelegate.java rename to core-processor/src/main/java/io/micronaut/inject/ast/ElementMutableAnnotationMetadataDelegate.java diff --git a/inject/src/main/java/io/micronaut/inject/ast/ElementQuery.java b/core-processor/src/main/java/io/micronaut/inject/ast/ElementQuery.java similarity index 100% rename from inject/src/main/java/io/micronaut/inject/ast/ElementQuery.java rename to core-processor/src/main/java/io/micronaut/inject/ast/ElementQuery.java diff --git a/inject/src/main/java/io/micronaut/inject/ast/EnumConstantElement.java b/core-processor/src/main/java/io/micronaut/inject/ast/EnumConstantElement.java similarity index 79% rename from inject/src/main/java/io/micronaut/inject/ast/EnumConstantElement.java rename to core-processor/src/main/java/io/micronaut/inject/ast/EnumConstantElement.java index 2a408d83213..3d9ae906ba0 100644 --- a/inject/src/main/java/io/micronaut/inject/ast/EnumConstantElement.java +++ b/core-processor/src/main/java/io/micronaut/inject/ast/EnumConstantElement.java @@ -15,7 +15,6 @@ */ package io.micronaut.inject.ast; -import java.util.HashSet; import java.util.Set; /** @@ -25,9 +24,9 @@ */ public interface EnumConstantElement extends FieldElement { - Set ENUM_CONSTANT_MODIFIERS = new HashSet<>() {{ - add(ElementModifier.PUBLIC); - add(ElementModifier.STATIC); - add(ElementModifier.FINAL); - }}; + Set ENUM_CONSTANT_MODIFIERS = Set.of( + ElementModifier.PUBLIC, + ElementModifier.STATIC, + ElementModifier.FINAL + ); } diff --git a/inject/src/main/java/io/micronaut/inject/ast/EnumElement.java b/core-processor/src/main/java/io/micronaut/inject/ast/EnumElement.java similarity index 100% rename from inject/src/main/java/io/micronaut/inject/ast/EnumElement.java rename to core-processor/src/main/java/io/micronaut/inject/ast/EnumElement.java diff --git a/inject/src/main/java/io/micronaut/inject/ast/FieldElement.java b/core-processor/src/main/java/io/micronaut/inject/ast/FieldElement.java similarity index 100% rename from inject/src/main/java/io/micronaut/inject/ast/FieldElement.java rename to core-processor/src/main/java/io/micronaut/inject/ast/FieldElement.java diff --git a/inject/src/main/java/io/micronaut/inject/ast/GenericPlaceholderElement.java b/core-processor/src/main/java/io/micronaut/inject/ast/GenericPlaceholderElement.java similarity index 100% rename from inject/src/main/java/io/micronaut/inject/ast/GenericPlaceholderElement.java rename to core-processor/src/main/java/io/micronaut/inject/ast/GenericPlaceholderElement.java diff --git a/inject/src/main/java/io/micronaut/inject/ast/MemberElement.java b/core-processor/src/main/java/io/micronaut/inject/ast/MemberElement.java similarity index 100% rename from inject/src/main/java/io/micronaut/inject/ast/MemberElement.java rename to core-processor/src/main/java/io/micronaut/inject/ast/MemberElement.java diff --git a/inject/src/main/java/io/micronaut/inject/ast/MethodElement.java b/core-processor/src/main/java/io/micronaut/inject/ast/MethodElement.java similarity index 100% rename from inject/src/main/java/io/micronaut/inject/ast/MethodElement.java rename to core-processor/src/main/java/io/micronaut/inject/ast/MethodElement.java diff --git a/inject/src/main/java/io/micronaut/inject/ast/PackageElement.java b/core-processor/src/main/java/io/micronaut/inject/ast/PackageElement.java similarity index 100% rename from inject/src/main/java/io/micronaut/inject/ast/PackageElement.java rename to core-processor/src/main/java/io/micronaut/inject/ast/PackageElement.java diff --git a/inject/src/main/java/io/micronaut/inject/ast/ParameterElement.java b/core-processor/src/main/java/io/micronaut/inject/ast/ParameterElement.java similarity index 100% rename from inject/src/main/java/io/micronaut/inject/ast/ParameterElement.java rename to core-processor/src/main/java/io/micronaut/inject/ast/ParameterElement.java diff --git a/inject/src/main/java/io/micronaut/inject/ast/PrimitiveElement.java b/core-processor/src/main/java/io/micronaut/inject/ast/PrimitiveElement.java similarity index 100% rename from inject/src/main/java/io/micronaut/inject/ast/PrimitiveElement.java rename to core-processor/src/main/java/io/micronaut/inject/ast/PrimitiveElement.java diff --git a/inject/src/main/java/io/micronaut/inject/ast/PropertyElement.java b/core-processor/src/main/java/io/micronaut/inject/ast/PropertyElement.java similarity index 100% rename from inject/src/main/java/io/micronaut/inject/ast/PropertyElement.java rename to core-processor/src/main/java/io/micronaut/inject/ast/PropertyElement.java diff --git a/inject/src/main/java/io/micronaut/inject/ast/ReflectClassElement.java b/core-processor/src/main/java/io/micronaut/inject/ast/ReflectClassElement.java similarity index 100% rename from inject/src/main/java/io/micronaut/inject/ast/ReflectClassElement.java rename to core-processor/src/main/java/io/micronaut/inject/ast/ReflectClassElement.java diff --git a/inject/src/main/java/io/micronaut/inject/ast/ReflectGenericPlaceholderElement.java b/core-processor/src/main/java/io/micronaut/inject/ast/ReflectGenericPlaceholderElement.java similarity index 100% rename from inject/src/main/java/io/micronaut/inject/ast/ReflectGenericPlaceholderElement.java rename to core-processor/src/main/java/io/micronaut/inject/ast/ReflectGenericPlaceholderElement.java diff --git a/inject/src/main/java/io/micronaut/inject/ast/ReflectParameterElement.java b/core-processor/src/main/java/io/micronaut/inject/ast/ReflectParameterElement.java similarity index 100% rename from inject/src/main/java/io/micronaut/inject/ast/ReflectParameterElement.java rename to core-processor/src/main/java/io/micronaut/inject/ast/ReflectParameterElement.java diff --git a/inject/src/main/java/io/micronaut/inject/ast/ReflectTypeElement.java b/core-processor/src/main/java/io/micronaut/inject/ast/ReflectTypeElement.java similarity index 100% rename from inject/src/main/java/io/micronaut/inject/ast/ReflectTypeElement.java rename to core-processor/src/main/java/io/micronaut/inject/ast/ReflectTypeElement.java diff --git a/inject/src/main/java/io/micronaut/inject/ast/ReflectWildcardElement.java b/core-processor/src/main/java/io/micronaut/inject/ast/ReflectWildcardElement.java similarity index 100% rename from inject/src/main/java/io/micronaut/inject/ast/ReflectWildcardElement.java rename to core-processor/src/main/java/io/micronaut/inject/ast/ReflectWildcardElement.java diff --git a/inject/src/main/java/io/micronaut/inject/ast/SimpleClassElement.java b/core-processor/src/main/java/io/micronaut/inject/ast/SimpleClassElement.java similarity index 100% rename from inject/src/main/java/io/micronaut/inject/ast/SimpleClassElement.java rename to core-processor/src/main/java/io/micronaut/inject/ast/SimpleClassElement.java diff --git a/inject/src/main/java/io/micronaut/inject/ast/SimplePackageElement.java b/core-processor/src/main/java/io/micronaut/inject/ast/SimplePackageElement.java similarity index 100% rename from inject/src/main/java/io/micronaut/inject/ast/SimplePackageElement.java rename to core-processor/src/main/java/io/micronaut/inject/ast/SimplePackageElement.java diff --git a/inject/src/main/java/io/micronaut/inject/ast/TypedElement.java b/core-processor/src/main/java/io/micronaut/inject/ast/TypedElement.java similarity index 100% rename from inject/src/main/java/io/micronaut/inject/ast/TypedElement.java rename to core-processor/src/main/java/io/micronaut/inject/ast/TypedElement.java diff --git a/inject/src/main/java/io/micronaut/inject/ast/WildcardElement.java b/core-processor/src/main/java/io/micronaut/inject/ast/WildcardElement.java similarity index 100% rename from inject/src/main/java/io/micronaut/inject/ast/WildcardElement.java rename to core-processor/src/main/java/io/micronaut/inject/ast/WildcardElement.java diff --git a/inject/src/main/java/io/micronaut/inject/ast/beans/BeanConstructorElement.java b/core-processor/src/main/java/io/micronaut/inject/ast/beans/BeanConstructorElement.java similarity index 100% rename from inject/src/main/java/io/micronaut/inject/ast/beans/BeanConstructorElement.java rename to core-processor/src/main/java/io/micronaut/inject/ast/beans/BeanConstructorElement.java diff --git a/inject/src/main/java/io/micronaut/inject/ast/beans/BeanElement.java b/core-processor/src/main/java/io/micronaut/inject/ast/beans/BeanElement.java similarity index 100% rename from inject/src/main/java/io/micronaut/inject/ast/beans/BeanElement.java rename to core-processor/src/main/java/io/micronaut/inject/ast/beans/BeanElement.java diff --git a/inject/src/main/java/io/micronaut/inject/ast/beans/BeanElementBuilder.java b/core-processor/src/main/java/io/micronaut/inject/ast/beans/BeanElementBuilder.java similarity index 100% rename from inject/src/main/java/io/micronaut/inject/ast/beans/BeanElementBuilder.java rename to core-processor/src/main/java/io/micronaut/inject/ast/beans/BeanElementBuilder.java diff --git a/inject/src/main/java/io/micronaut/inject/ast/beans/BeanFieldElement.java b/core-processor/src/main/java/io/micronaut/inject/ast/beans/BeanFieldElement.java similarity index 100% rename from inject/src/main/java/io/micronaut/inject/ast/beans/BeanFieldElement.java rename to core-processor/src/main/java/io/micronaut/inject/ast/beans/BeanFieldElement.java diff --git a/inject/src/main/java/io/micronaut/inject/ast/beans/BeanMethodElement.java b/core-processor/src/main/java/io/micronaut/inject/ast/beans/BeanMethodElement.java similarity index 100% rename from inject/src/main/java/io/micronaut/inject/ast/beans/BeanMethodElement.java rename to core-processor/src/main/java/io/micronaut/inject/ast/beans/BeanMethodElement.java diff --git a/inject/src/main/java/io/micronaut/inject/ast/beans/BeanParameterElement.java b/core-processor/src/main/java/io/micronaut/inject/ast/beans/BeanParameterElement.java similarity index 100% rename from inject/src/main/java/io/micronaut/inject/ast/beans/BeanParameterElement.java rename to core-processor/src/main/java/io/micronaut/inject/ast/beans/BeanParameterElement.java diff --git a/inject/src/main/java/io/micronaut/inject/ast/beans/ConfigurableElement.java b/core-processor/src/main/java/io/micronaut/inject/ast/beans/ConfigurableElement.java similarity index 100% rename from inject/src/main/java/io/micronaut/inject/ast/beans/ConfigurableElement.java rename to core-processor/src/main/java/io/micronaut/inject/ast/beans/ConfigurableElement.java diff --git a/inject/src/main/java/io/micronaut/inject/ast/beans/InjectableElement.java b/core-processor/src/main/java/io/micronaut/inject/ast/beans/InjectableElement.java similarity index 100% rename from inject/src/main/java/io/micronaut/inject/ast/beans/InjectableElement.java rename to core-processor/src/main/java/io/micronaut/inject/ast/beans/InjectableElement.java diff --git a/inject/src/main/java/io/micronaut/inject/ast/package-info.java b/core-processor/src/main/java/io/micronaut/inject/ast/package-info.java similarity index 100% rename from inject/src/main/java/io/micronaut/inject/ast/package-info.java rename to core-processor/src/main/java/io/micronaut/inject/ast/package-info.java diff --git a/inject/src/main/java/io/micronaut/inject/ast/utils/AstBeanPropertiesUtils.java b/core-processor/src/main/java/io/micronaut/inject/ast/utils/AstBeanPropertiesUtils.java similarity index 100% rename from inject/src/main/java/io/micronaut/inject/ast/utils/AstBeanPropertiesUtils.java rename to core-processor/src/main/java/io/micronaut/inject/ast/utils/AstBeanPropertiesUtils.java diff --git a/inject/src/main/java/io/micronaut/inject/ast/utils/EnclosedElementsQuery.java b/core-processor/src/main/java/io/micronaut/inject/ast/utils/EnclosedElementsQuery.java similarity index 100% rename from inject/src/main/java/io/micronaut/inject/ast/utils/EnclosedElementsQuery.java rename to core-processor/src/main/java/io/micronaut/inject/ast/utils/EnclosedElementsQuery.java diff --git a/inject/src/main/java/io/micronaut/inject/beans/visitor/BeanIntrospectionWriter.java b/core-processor/src/main/java/io/micronaut/inject/beans/visitor/BeanIntrospectionWriter.java similarity index 98% rename from inject/src/main/java/io/micronaut/inject/beans/visitor/BeanIntrospectionWriter.java rename to core-processor/src/main/java/io/micronaut/inject/beans/visitor/BeanIntrospectionWriter.java index aba018ae87a..e1d2ec28628 100644 --- a/inject/src/main/java/io/micronaut/inject/beans/visitor/BeanIntrospectionWriter.java +++ b/core-processor/src/main/java/io/micronaut/inject/beans/visitor/BeanIntrospectionWriter.java @@ -41,18 +41,17 @@ import io.micronaut.inject.ast.TypedElement; import io.micronaut.inject.beans.AbstractInitializableBeanIntrospection; import io.micronaut.inject.processing.JavaModelUtils; +import io.micronaut.inject.visitor.util.VisitorContextUtils; import io.micronaut.inject.writer.AbstractAnnotationMetadataWriter; import io.micronaut.inject.writer.ClassWriterOutputVisitor; import io.micronaut.inject.writer.DispatchWriter; import io.micronaut.inject.writer.StringSwitchWriter; import org.objectweb.asm.ClassWriter; import org.objectweb.asm.Label; -import org.objectweb.asm.Opcodes; import org.objectweb.asm.Type; import org.objectweb.asm.commons.GeneratorAdapter; import org.objectweb.asm.commons.Method; -import javax.validation.constraints.NotNull; import java.io.IOException; import java.io.OutputStream; import java.util.ArrayList; @@ -209,7 +208,7 @@ void visitProperty( ); if (typeArguments != null) { for (ClassElement element : typeArguments.values()) { - DefaultAnnotationMetadata.contributeRepeatable( + VisitorContextUtils.contributeRepeatable( this.annotationMetadata, element ); @@ -522,7 +521,7 @@ private void writeIntrospectionClass(ClassWriterOutputVisitor classWriterOutputV // retrieved from BeanIntrospectionReference.$ANNOTATION_METADATA constructorWriter.getStatic( targetClassType, - AbstractAnnotationMetadataWriter.FIELD_ANNOTATION_METADATA, Type.getType(AnnotationMetadata.class)); + FIELD_ANNOTATION_METADATA, Type.getType(AnnotationMetadata.class)); } if (constructor != null) { @@ -600,12 +599,12 @@ private void writeIntrospectionClass(ClassWriterOutputVisitor classWriterOutputV private void buildPropertyIndexOfMethod(ClassWriter classWriter) { GeneratorAdapter findMethod = new GeneratorAdapter(classWriter.visitMethod( - Opcodes.ACC_PUBLIC | Opcodes.ACC_FINAL, + ACC_PUBLIC | ACC_FINAL, PROPERTY_INDEX_OF.getName(), PROPERTY_INDEX_OF.getDescriptor(), null, null), - ACC_PUBLIC | Opcodes.ACC_FINAL, + ACC_PUBLIC | ACC_FINAL, PROPERTY_INDEX_OF.getName(), PROPERTY_INDEX_OF.getDescriptor() ); @@ -644,12 +643,12 @@ private void buildFindIndexedProperty(ClassWriter classWriter) { return; } GeneratorAdapter writer = new GeneratorAdapter(classWriter.visitMethod( - Opcodes.ACC_PROTECTED | Opcodes.ACC_FINAL, + ACC_PROTECTED | ACC_FINAL, FIND_INDEXED_PROPERTY_METHOD.getName(), FIND_INDEXED_PROPERTY_METHOD.getDescriptor(), null, null), - ACC_PROTECTED | Opcodes.ACC_FINAL, + ACC_PROTECTED | ACC_FINAL, FIND_INDEXED_PROPERTY_METHOD.getName(), FIND_INDEXED_PROPERTY_METHOD.getDescriptor() ); @@ -743,12 +742,12 @@ private void buildGetIndexedProperties(ClassWriter classWriter) { return; } GeneratorAdapter writer = new GeneratorAdapter(classWriter.visitMethod( - Opcodes.ACC_PUBLIC | Opcodes.ACC_FINAL, + ACC_PUBLIC | ACC_FINAL, GET_INDEXED_PROPERTIES.getName(), GET_INDEXED_PROPERTIES.getDescriptor(), null, null), - ACC_PUBLIC | Opcodes.ACC_FINAL, + ACC_PUBLIC | ACC_FINAL, GET_INDEXED_PROPERTIES.getName(), GET_INDEXED_PROPERTIES.getDescriptor() ); @@ -853,7 +852,7 @@ private void invokeBeanConstructor(GeneratorAdapter writer, MethodElement constr final String methodDescriptor = getMethodDescriptor(beanType, argumentTypes); Method method = new Method(constructor.getName(), methodDescriptor); if (classElement.isInterface()) { - writer.visitMethodInsn(Opcodes.INVOKESTATIC, beanType.getInternalName(), method.getName(), + writer.visitMethodInsn(INVOKESTATIC, beanType.getInternalName(), method.getName(), method.getDescriptor(), true); } else { writer.invokeStatic(beanType, method); @@ -923,7 +922,7 @@ private void pushAnnotationMetadata(ClassWriter classWriter, GeneratorAdapter st } else if (annotationMetadata instanceof AnnotationMetadataReference) { AnnotationMetadataReference reference = (AnnotationMetadataReference) annotationMetadata; String className = reference.getClassName(); - staticInit.getStatic(getTypeReferenceForName(className), AbstractAnnotationMetadataWriter.FIELD_ANNOTATION_METADATA, Type.getType(AnnotationMetadata.class)); + staticInit.getStatic(getTypeReferenceForName(className), FIELD_ANNOTATION_METADATA, Type.getType(AnnotationMetadata.class)); } else if (annotationMetadata instanceof AnnotationMetadataHierarchy) { AnnotationMetadataWriter.instantiateNewMetadataHierarchy( introspectionType, @@ -1174,7 +1173,6 @@ private void invokeSetField(GeneratorAdapter mutateMethod, FieldElement field) { } private static final class BeanMethodData { - @NotNull final MethodElement methodElement; final int dispatchIndex; diff --git a/inject/src/main/java/io/micronaut/inject/beans/visitor/EntityIntrospectedAnnotationMapper.java b/core-processor/src/main/java/io/micronaut/inject/beans/visitor/EntityIntrospectedAnnotationMapper.java similarity index 100% rename from inject/src/main/java/io/micronaut/inject/beans/visitor/EntityIntrospectedAnnotationMapper.java rename to core-processor/src/main/java/io/micronaut/inject/beans/visitor/EntityIntrospectedAnnotationMapper.java diff --git a/inject/src/main/java/io/micronaut/inject/beans/visitor/EntityReflectiveAccessAnnotationMapper.java b/core-processor/src/main/java/io/micronaut/inject/beans/visitor/EntityReflectiveAccessAnnotationMapper.java similarity index 100% rename from inject/src/main/java/io/micronaut/inject/beans/visitor/EntityReflectiveAccessAnnotationMapper.java rename to core-processor/src/main/java/io/micronaut/inject/beans/visitor/EntityReflectiveAccessAnnotationMapper.java diff --git a/inject/src/main/java/io/micronaut/inject/mappers/IntrospectedToBeanPropertiesTransformer.java b/core-processor/src/main/java/io/micronaut/inject/beans/visitor/IntrospectedToBeanPropertiesTransformer.java similarity index 98% rename from inject/src/main/java/io/micronaut/inject/mappers/IntrospectedToBeanPropertiesTransformer.java rename to core-processor/src/main/java/io/micronaut/inject/beans/visitor/IntrospectedToBeanPropertiesTransformer.java index a4c80bdcc90..8d95f53daa7 100644 --- a/inject/src/main/java/io/micronaut/inject/mappers/IntrospectedToBeanPropertiesTransformer.java +++ b/core-processor/src/main/java/io/micronaut/inject/beans/visitor/IntrospectedToBeanPropertiesTransformer.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.micronaut.inject.mappers; +package io.micronaut.inject.beans.visitor; import io.micronaut.context.annotation.BeanProperties; import io.micronaut.core.annotation.AnnotationValue; diff --git a/inject/src/main/java/io/micronaut/inject/beans/visitor/IntrospectedTypeElementVisitor.java b/core-processor/src/main/java/io/micronaut/inject/beans/visitor/IntrospectedTypeElementVisitor.java similarity index 100% rename from inject/src/main/java/io/micronaut/inject/beans/visitor/IntrospectedTypeElementVisitor.java rename to core-processor/src/main/java/io/micronaut/inject/beans/visitor/IntrospectedTypeElementVisitor.java diff --git a/inject/src/main/java/io/micronaut/inject/beans/visitor/JsonCreatorAnnotationMapper.java b/core-processor/src/main/java/io/micronaut/inject/beans/visitor/JsonCreatorAnnotationMapper.java similarity index 100% rename from inject/src/main/java/io/micronaut/inject/beans/visitor/JsonCreatorAnnotationMapper.java rename to core-processor/src/main/java/io/micronaut/inject/beans/visitor/JsonCreatorAnnotationMapper.java diff --git a/inject/src/main/java/io/micronaut/inject/beans/visitor/MappedSuperClassIntrospectionMapper.java b/core-processor/src/main/java/io/micronaut/inject/beans/visitor/MappedSuperClassIntrospectionMapper.java similarity index 100% rename from inject/src/main/java/io/micronaut/inject/beans/visitor/MappedSuperClassIntrospectionMapper.java rename to core-processor/src/main/java/io/micronaut/inject/beans/visitor/MappedSuperClassIntrospectionMapper.java diff --git a/inject/src/main/java/io/micronaut/inject/beans/visitor/package-info.java b/core-processor/src/main/java/io/micronaut/inject/beans/visitor/package-info.java similarity index 100% rename from inject/src/main/java/io/micronaut/inject/beans/visitor/package-info.java rename to core-processor/src/main/java/io/micronaut/inject/beans/visitor/package-info.java diff --git a/inject/src/main/java/io/micronaut/inject/beans/visitor/jakarta/persistence/JakartaEntityIntrospectedAnnotationMapper.java b/core-processor/src/main/java/io/micronaut/inject/beans/visitor/persistence/JakartaEntityIntrospectedAnnotationMapper.java similarity index 96% rename from inject/src/main/java/io/micronaut/inject/beans/visitor/jakarta/persistence/JakartaEntityIntrospectedAnnotationMapper.java rename to core-processor/src/main/java/io/micronaut/inject/beans/visitor/persistence/JakartaEntityIntrospectedAnnotationMapper.java index c86b009a80a..64f8d7167d9 100644 --- a/inject/src/main/java/io/micronaut/inject/beans/visitor/jakarta/persistence/JakartaEntityIntrospectedAnnotationMapper.java +++ b/core-processor/src/main/java/io/micronaut/inject/beans/visitor/persistence/JakartaEntityIntrospectedAnnotationMapper.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.micronaut.inject.beans.visitor.jakarta.persistence; +package io.micronaut.inject.beans.visitor.persistence; import io.micronaut.core.annotation.AnnotationValue; import io.micronaut.core.annotation.AnnotationValueBuilder; diff --git a/inject/src/main/java/io/micronaut/inject/beans/visitor/jakarta/persistence/JakartaMappedSuperClassIntrospectionMapper.java b/core-processor/src/main/java/io/micronaut/inject/beans/visitor/persistence/JakartaMappedSuperClassIntrospectionMapper.java similarity index 94% rename from inject/src/main/java/io/micronaut/inject/beans/visitor/jakarta/persistence/JakartaMappedSuperClassIntrospectionMapper.java rename to core-processor/src/main/java/io/micronaut/inject/beans/visitor/persistence/JakartaMappedSuperClassIntrospectionMapper.java index f04d1e04d85..774ec04b051 100644 --- a/inject/src/main/java/io/micronaut/inject/beans/visitor/jakarta/persistence/JakartaMappedSuperClassIntrospectionMapper.java +++ b/core-processor/src/main/java/io/micronaut/inject/beans/visitor/persistence/JakartaMappedSuperClassIntrospectionMapper.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.micronaut.inject.beans.visitor.jakarta.persistence; +package io.micronaut.inject.beans.visitor.persistence; import io.micronaut.core.annotation.Internal; import io.micronaut.core.annotation.NonNull; diff --git a/inject/src/main/java/io/micronaut/inject/configuration/ConfigurationMetadata.java b/core-processor/src/main/java/io/micronaut/inject/configuration/ConfigurationMetadata.java similarity index 100% rename from inject/src/main/java/io/micronaut/inject/configuration/ConfigurationMetadata.java rename to core-processor/src/main/java/io/micronaut/inject/configuration/ConfigurationMetadata.java diff --git a/inject/src/main/java/io/micronaut/inject/configuration/ConfigurationMetadataBuilder.java b/core-processor/src/main/java/io/micronaut/inject/configuration/ConfigurationMetadataBuilder.java similarity index 95% rename from inject/src/main/java/io/micronaut/inject/configuration/ConfigurationMetadataBuilder.java rename to core-processor/src/main/java/io/micronaut/inject/configuration/ConfigurationMetadataBuilder.java index 44aba8da662..d7b772f0b87 100644 --- a/inject/src/main/java/io/micronaut/inject/configuration/ConfigurationMetadataBuilder.java +++ b/core-processor/src/main/java/io/micronaut/inject/configuration/ConfigurationMetadataBuilder.java @@ -16,6 +16,7 @@ package io.micronaut.inject.configuration; import io.micronaut.context.annotation.ConfigurationReader; +import io.micronaut.core.annotation.Internal; import io.micronaut.core.annotation.NonNull; import io.micronaut.core.annotation.Nullable; import io.micronaut.core.naming.NameUtils; @@ -46,7 +47,7 @@ public class ConfigurationMetadataBuilder { @SuppressWarnings({"StaticVariableName", "VisibilityModifier"}) - public static ConfigurationMetadataBuilder INSTANCE = new ConfigurationMetadataBuilder(); + public static final ConfigurationMetadataBuilder INSTANCE = new ConfigurationMetadataBuilder(); private final OriginatingElements originatingElements = OriginatingElements.of(); private final List properties = new ArrayList<>(); @@ -153,17 +154,10 @@ static String quote(String string) { for (i = 0; i < len; i += 1) { c = string.charAt(i); switch (c) { - case '\\': - case '"': + case '\\', '"', '/': sb.append('\\'); sb.append(c); break; - case '/': - // if (b == '<') { - sb.append('\\'); - // } - sb.append(c); - break; case '\b': sb.append("\\b"); break; @@ -206,4 +200,13 @@ static void writeAttribute(Writer out, String name, String value) throws IOExcep out.write("\":"); out.write(quote(value)); } + + /** + * Reset the state. + */ + @Internal + public static void reset() { + INSTANCE.properties.clear(); + INSTANCE.configurations.clear(); + } } diff --git a/inject/src/main/java/io/micronaut/inject/configuration/ConfigurationMetadataWriter.java b/core-processor/src/main/java/io/micronaut/inject/configuration/ConfigurationMetadataWriter.java similarity index 100% rename from inject/src/main/java/io/micronaut/inject/configuration/ConfigurationMetadataWriter.java rename to core-processor/src/main/java/io/micronaut/inject/configuration/ConfigurationMetadataWriter.java diff --git a/inject/src/main/java/io/micronaut/inject/configuration/ConfigurationUtils.java b/core-processor/src/main/java/io/micronaut/inject/configuration/ConfigurationUtils.java similarity index 100% rename from inject/src/main/java/io/micronaut/inject/configuration/ConfigurationUtils.java rename to core-processor/src/main/java/io/micronaut/inject/configuration/ConfigurationUtils.java diff --git a/inject/src/main/java/io/micronaut/inject/configuration/JsonConfigurationMetadataWriter.java b/core-processor/src/main/java/io/micronaut/inject/configuration/JsonConfigurationMetadataWriter.java similarity index 100% rename from inject/src/main/java/io/micronaut/inject/configuration/JsonConfigurationMetadataWriter.java rename to core-processor/src/main/java/io/micronaut/inject/configuration/JsonConfigurationMetadataWriter.java diff --git a/inject/src/main/java/io/micronaut/inject/configuration/PropertyMetadata.java b/core-processor/src/main/java/io/micronaut/inject/configuration/PropertyMetadata.java similarity index 100% rename from inject/src/main/java/io/micronaut/inject/configuration/PropertyMetadata.java rename to core-processor/src/main/java/io/micronaut/inject/configuration/PropertyMetadata.java diff --git a/inject/src/main/java/io/micronaut/inject/configuration/package-info.java b/core-processor/src/main/java/io/micronaut/inject/configuration/package-info.java similarity index 100% rename from inject/src/main/java/io/micronaut/inject/configuration/package-info.java rename to core-processor/src/main/java/io/micronaut/inject/configuration/package-info.java diff --git a/inject/src/main/java/io/micronaut/inject/processing/AbstractBeanElementCreator.java b/core-processor/src/main/java/io/micronaut/inject/processing/AbstractBeanElementCreator.java similarity index 94% rename from inject/src/main/java/io/micronaut/inject/processing/AbstractBeanElementCreator.java rename to core-processor/src/main/java/io/micronaut/inject/processing/AbstractBeanElementCreator.java index 85c0f2b3fb4..b211ab17cd5 100644 --- a/inject/src/main/java/io/micronaut/inject/processing/AbstractBeanElementCreator.java +++ b/core-processor/src/main/java/io/micronaut/inject/processing/AbstractBeanElementCreator.java @@ -15,6 +15,7 @@ */ package io.micronaut.inject.processing; +import io.micronaut.aop.writer.AopHelperImpl; import io.micronaut.context.RequiresCondition; import io.micronaut.context.annotation.Requires; import io.micronaut.core.annotation.AnnotationMetadata; @@ -24,11 +25,10 @@ import io.micronaut.core.annotation.NextMajorVersion; import io.micronaut.core.annotation.NonNull; import io.micronaut.core.annotation.Nullable; -import io.micronaut.core.reflect.ClassUtils; import io.micronaut.inject.annotation.AnnotationMetadataHierarchy; -import io.micronaut.inject.validation.RequiresValidation; import io.micronaut.inject.ast.ClassElement; import io.micronaut.inject.ast.MemberElement; +import io.micronaut.inject.validation.RequiresValidation; import io.micronaut.inject.visitor.VisitorContext; import io.micronaut.inject.writer.BeanDefinitionVisitor; @@ -60,13 +60,8 @@ abstract class AbstractBeanElementCreator implements BeanDefinitionCreator { protected AbstractBeanElementCreator(ClassElement classElement, VisitorContext visitorContext) { this.classElement = classElement; this.visitorContext = visitorContext; + this.aopHelper = new AopHelperImpl(); checkPackage(classElement); - String helperName = "io.micronaut.aop.writer.AopHelperImpl"; - try { - aopHelper = (AopHelper) ClassUtils.forName(helperName, getClass().getClassLoader()).get().newInstance(); - } catch (Exception e) { - throw new RuntimeException("Cannot create AOP helper class: " + helperName, e); - } } @Override diff --git a/inject/src/main/java/io/micronaut/inject/processing/AopHelper.java b/core-processor/src/main/java/io/micronaut/inject/processing/AopHelper.java similarity index 100% rename from inject/src/main/java/io/micronaut/inject/processing/AopHelper.java rename to core-processor/src/main/java/io/micronaut/inject/processing/AopHelper.java diff --git a/inject/src/main/java/io/micronaut/inject/processing/AopIntroductionProxySupportedBeanElementCreator.java b/core-processor/src/main/java/io/micronaut/inject/processing/AopIntroductionProxySupportedBeanElementCreator.java similarity index 100% rename from inject/src/main/java/io/micronaut/inject/processing/AopIntroductionProxySupportedBeanElementCreator.java rename to core-processor/src/main/java/io/micronaut/inject/processing/AopIntroductionProxySupportedBeanElementCreator.java diff --git a/inject/src/main/java/io/micronaut/inject/processing/BeanDefinitionCreator.java b/core-processor/src/main/java/io/micronaut/inject/processing/BeanDefinitionCreator.java similarity index 100% rename from inject/src/main/java/io/micronaut/inject/processing/BeanDefinitionCreator.java rename to core-processor/src/main/java/io/micronaut/inject/processing/BeanDefinitionCreator.java diff --git a/inject/src/main/java/io/micronaut/inject/processing/BeanDefinitionCreatorFactory.java b/core-processor/src/main/java/io/micronaut/inject/processing/BeanDefinitionCreatorFactory.java similarity index 100% rename from inject/src/main/java/io/micronaut/inject/processing/BeanDefinitionCreatorFactory.java rename to core-processor/src/main/java/io/micronaut/inject/processing/BeanDefinitionCreatorFactory.java diff --git a/inject/src/main/java/io/micronaut/inject/processing/ConfigurationReaderBeanElementCreator.java b/core-processor/src/main/java/io/micronaut/inject/processing/ConfigurationReaderBeanElementCreator.java similarity index 100% rename from inject/src/main/java/io/micronaut/inject/processing/ConfigurationReaderBeanElementCreator.java rename to core-processor/src/main/java/io/micronaut/inject/processing/ConfigurationReaderBeanElementCreator.java diff --git a/inject/src/main/java/io/micronaut/inject/processing/DeclaredBeanElementCreator.java b/core-processor/src/main/java/io/micronaut/inject/processing/DeclaredBeanElementCreator.java similarity index 100% rename from inject/src/main/java/io/micronaut/inject/processing/DeclaredBeanElementCreator.java rename to core-processor/src/main/java/io/micronaut/inject/processing/DeclaredBeanElementCreator.java diff --git a/inject/src/main/java/io/micronaut/inject/processing/FactoryBeanElementCreator.java b/core-processor/src/main/java/io/micronaut/inject/processing/FactoryBeanElementCreator.java similarity index 100% rename from inject/src/main/java/io/micronaut/inject/processing/FactoryBeanElementCreator.java rename to core-processor/src/main/java/io/micronaut/inject/processing/FactoryBeanElementCreator.java diff --git a/inject/src/main/java/io/micronaut/inject/processing/IntroductionInterfaceBeanElementCreator.java b/core-processor/src/main/java/io/micronaut/inject/processing/IntroductionInterfaceBeanElementCreator.java similarity index 100% rename from inject/src/main/java/io/micronaut/inject/processing/IntroductionInterfaceBeanElementCreator.java rename to core-processor/src/main/java/io/micronaut/inject/processing/IntroductionInterfaceBeanElementCreator.java diff --git a/inject/src/main/java/io/micronaut/inject/processing/JavaModelUtils.java b/core-processor/src/main/java/io/micronaut/inject/processing/JavaModelUtils.java similarity index 100% rename from inject/src/main/java/io/micronaut/inject/processing/JavaModelUtils.java rename to core-processor/src/main/java/io/micronaut/inject/processing/JavaModelUtils.java diff --git a/inject/src/main/java/io/micronaut/inject/processing/ProcessingException.java b/core-processor/src/main/java/io/micronaut/inject/processing/ProcessingException.java similarity index 100% rename from inject/src/main/java/io/micronaut/inject/processing/ProcessingException.java rename to core-processor/src/main/java/io/micronaut/inject/processing/ProcessingException.java diff --git a/inject/src/main/java/io/micronaut/inject/processing/package-info.java b/core-processor/src/main/java/io/micronaut/inject/processing/package-info.java similarity index 100% rename from inject/src/main/java/io/micronaut/inject/processing/package-info.java rename to core-processor/src/main/java/io/micronaut/inject/processing/package-info.java diff --git a/inject/src/main/java/io/micronaut/inject/visitor/BeanElementVisitor.java b/core-processor/src/main/java/io/micronaut/inject/visitor/BeanElementVisitor.java similarity index 100% rename from inject/src/main/java/io/micronaut/inject/visitor/BeanElementVisitor.java rename to core-processor/src/main/java/io/micronaut/inject/visitor/BeanElementVisitor.java diff --git a/inject/src/main/java/io/micronaut/inject/visitor/BeanElementVisitorContext.java b/core-processor/src/main/java/io/micronaut/inject/visitor/BeanElementVisitorContext.java similarity index 100% rename from inject/src/main/java/io/micronaut/inject/visitor/BeanElementVisitorContext.java rename to core-processor/src/main/java/io/micronaut/inject/visitor/BeanElementVisitorContext.java diff --git a/inject/src/main/java/io/micronaut/inject/visitor/BeanElementVisitorLoader.java b/core-processor/src/main/java/io/micronaut/inject/visitor/BeanElementVisitorLoader.java similarity index 100% rename from inject/src/main/java/io/micronaut/inject/visitor/BeanElementVisitorLoader.java rename to core-processor/src/main/java/io/micronaut/inject/visitor/BeanElementVisitorLoader.java diff --git a/inject/src/main/java/io/micronaut/inject/visitor/TypeElementVisitor.java b/core-processor/src/main/java/io/micronaut/inject/visitor/TypeElementVisitor.java similarity index 100% rename from inject/src/main/java/io/micronaut/inject/visitor/TypeElementVisitor.java rename to core-processor/src/main/java/io/micronaut/inject/visitor/TypeElementVisitor.java diff --git a/inject/src/main/java/io/micronaut/inject/visitor/VisitorConfiguration.java b/core-processor/src/main/java/io/micronaut/inject/visitor/VisitorConfiguration.java similarity index 100% rename from inject/src/main/java/io/micronaut/inject/visitor/VisitorConfiguration.java rename to core-processor/src/main/java/io/micronaut/inject/visitor/VisitorConfiguration.java diff --git a/inject/src/main/java/io/micronaut/inject/visitor/VisitorContext.java b/core-processor/src/main/java/io/micronaut/inject/visitor/VisitorContext.java similarity index 100% rename from inject/src/main/java/io/micronaut/inject/visitor/VisitorContext.java rename to core-processor/src/main/java/io/micronaut/inject/visitor/VisitorContext.java diff --git a/inject/src/main/java/io/micronaut/inject/visitor/package-info.java b/core-processor/src/main/java/io/micronaut/inject/visitor/package-info.java similarity index 100% rename from inject/src/main/java/io/micronaut/inject/visitor/package-info.java rename to core-processor/src/main/java/io/micronaut/inject/visitor/package-info.java diff --git a/inject/src/main/java/io/micronaut/inject/util/VisitorContextUtils.java b/core-processor/src/main/java/io/micronaut/inject/visitor/util/VisitorContextUtils.java similarity index 71% rename from inject/src/main/java/io/micronaut/inject/util/VisitorContextUtils.java rename to core-processor/src/main/java/io/micronaut/inject/visitor/util/VisitorContextUtils.java index a0010003833..3688b376e49 100644 --- a/inject/src/main/java/io/micronaut/inject/util/VisitorContextUtils.java +++ b/core-processor/src/main/java/io/micronaut/inject/visitor/util/VisitorContextUtils.java @@ -13,10 +13,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.micronaut.inject.util; +package io.micronaut.inject.visitor.util; import io.micronaut.context.env.CachedEnvironment; +import io.micronaut.core.annotation.AnnotationMetadata; import io.micronaut.core.annotation.Internal; +import io.micronaut.inject.annotation.DefaultAnnotationMetadata; +import io.micronaut.inject.ast.ClassElement; import io.micronaut.inject.visitor.VisitorContext; import javax.annotation.processing.ProcessingEnvironment; @@ -69,4 +72,28 @@ public static Map getProcessorOptions(ProcessingEnvironment proc .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue))) .orElse(Collections.emptyMap()); } + + /** + * Contributes repeatable annotation metadata to the given class element. + * + *

WARNING: for internal use only be the framework

+ * + * @param target The target + * @param classElement The source + */ + @Internal + public static void contributeRepeatable(AnnotationMetadata target, ClassElement classElement) { + contributeRepeatable(target, classElement, new HashSet<>()); + } + + private static void contributeRepeatable(AnnotationMetadata target, ClassElement classElement, Set alreadySeen) { + alreadySeen.add(classElement); + DefaultAnnotationMetadata.contributeRepeatable(target, classElement.getAnnotationMetadata()); + for (ClassElement element : classElement.getTypeArguments().values()) { + if (alreadySeen.contains(classElement)) { + continue; + } + contributeRepeatable(target, element); + } + } } diff --git a/inject/src/main/java/io/micronaut/inject/writer/AbstractAnnotationMetadataWriter.java b/core-processor/src/main/java/io/micronaut/inject/writer/AbstractAnnotationMetadataWriter.java similarity index 100% rename from inject/src/main/java/io/micronaut/inject/writer/AbstractAnnotationMetadataWriter.java rename to core-processor/src/main/java/io/micronaut/inject/writer/AbstractAnnotationMetadataWriter.java diff --git a/inject/src/main/java/io/micronaut/inject/writer/AbstractBeanDefinitionBuilder.java b/core-processor/src/main/java/io/micronaut/inject/writer/AbstractBeanDefinitionBuilder.java similarity index 99% rename from inject/src/main/java/io/micronaut/inject/writer/AbstractBeanDefinitionBuilder.java rename to core-processor/src/main/java/io/micronaut/inject/writer/AbstractBeanDefinitionBuilder.java index daff56b068c..bdce24df08d 100644 --- a/inject/src/main/java/io/micronaut/inject/writer/AbstractBeanDefinitionBuilder.java +++ b/core-processor/src/main/java/io/micronaut/inject/writer/AbstractBeanDefinitionBuilder.java @@ -964,7 +964,7 @@ public Element removeStereotype(@NonNull String annotationType) { public > void with(Consumer consumer) { try { - this.currentMetadata = elementMetadata.isEmpty() ? AnnotationMetadata.EMPTY_METADATA : elementMetadata; + this.currentMetadata = elementMetadata.isEmpty() ? EMPTY_METADATA : elementMetadata; //noinspection unchecked consumer.accept((T) this); } finally { diff --git a/inject/src/main/java/io/micronaut/inject/writer/AbstractClassFileWriter.java b/core-processor/src/main/java/io/micronaut/inject/writer/AbstractClassFileWriter.java similarity index 100% rename from inject/src/main/java/io/micronaut/inject/writer/AbstractClassFileWriter.java rename to core-processor/src/main/java/io/micronaut/inject/writer/AbstractClassFileWriter.java diff --git a/inject/src/main/java/io/micronaut/inject/writer/AbstractClassWriterOutputVisitor.java b/core-processor/src/main/java/io/micronaut/inject/writer/AbstractClassWriterOutputVisitor.java similarity index 100% rename from inject/src/main/java/io/micronaut/inject/writer/AbstractClassWriterOutputVisitor.java rename to core-processor/src/main/java/io/micronaut/inject/writer/AbstractClassWriterOutputVisitor.java diff --git a/inject/src/main/java/io/micronaut/inject/writer/ArrayAwareSignatureWriter.java b/core-processor/src/main/java/io/micronaut/inject/writer/ArrayAwareSignatureWriter.java similarity index 100% rename from inject/src/main/java/io/micronaut/inject/writer/ArrayAwareSignatureWriter.java rename to core-processor/src/main/java/io/micronaut/inject/writer/ArrayAwareSignatureWriter.java diff --git a/inject/src/main/java/io/micronaut/inject/writer/BeanClassWriter.java b/core-processor/src/main/java/io/micronaut/inject/writer/BeanClassWriter.java similarity index 100% rename from inject/src/main/java/io/micronaut/inject/writer/BeanClassWriter.java rename to core-processor/src/main/java/io/micronaut/inject/writer/BeanClassWriter.java diff --git a/inject/src/main/java/io/micronaut/inject/writer/BeanConfigurationWriter.java b/core-processor/src/main/java/io/micronaut/inject/writer/BeanConfigurationWriter.java similarity index 100% rename from inject/src/main/java/io/micronaut/inject/writer/BeanConfigurationWriter.java rename to core-processor/src/main/java/io/micronaut/inject/writer/BeanConfigurationWriter.java diff --git a/inject/src/main/java/io/micronaut/inject/writer/BeanDefinitionReferenceWriter.java b/core-processor/src/main/java/io/micronaut/inject/writer/BeanDefinitionReferenceWriter.java similarity index 100% rename from inject/src/main/java/io/micronaut/inject/writer/BeanDefinitionReferenceWriter.java rename to core-processor/src/main/java/io/micronaut/inject/writer/BeanDefinitionReferenceWriter.java diff --git a/inject/src/main/java/io/micronaut/inject/writer/BeanDefinitionVisitor.java b/core-processor/src/main/java/io/micronaut/inject/writer/BeanDefinitionVisitor.java similarity index 100% rename from inject/src/main/java/io/micronaut/inject/writer/BeanDefinitionVisitor.java rename to core-processor/src/main/java/io/micronaut/inject/writer/BeanDefinitionVisitor.java diff --git a/inject/src/main/java/io/micronaut/inject/writer/BeanDefinitionWriter.java b/core-processor/src/main/java/io/micronaut/inject/writer/BeanDefinitionWriter.java similarity index 99% rename from inject/src/main/java/io/micronaut/inject/writer/BeanDefinitionWriter.java rename to core-processor/src/main/java/io/micronaut/inject/writer/BeanDefinitionWriter.java index 51ad89062bc..106dfaf8c1e 100644 --- a/inject/src/main/java/io/micronaut/inject/writer/BeanDefinitionWriter.java +++ b/core-processor/src/main/java/io/micronaut/inject/writer/BeanDefinitionWriter.java @@ -96,6 +96,7 @@ import io.micronaut.inject.visitor.BeanElementVisitor; import io.micronaut.inject.visitor.BeanElementVisitorContext; import io.micronaut.inject.visitor.VisitorContext; +import io.micronaut.inject.visitor.util.VisitorContextUtils; import jakarta.inject.Singleton; import org.objectweb.asm.ClassVisitor; import org.objectweb.asm.ClassWriter; @@ -1264,7 +1265,7 @@ private void pushStoreClassesAsSet(GeneratorAdapter writer, String[] classes) { ReflectionUtils.getRequiredMethod(Arrays.class, "asList", Object[].class) )); writer.invokeConstructor(Type.getType(HashSet.class), org.objectweb.asm.commons.Method.getMethod( - ReflectionUtils.findConstructor(HashSet.class, Collection.class).get() + ReflectionUtils.getRequiredInternalConstructor(HashSet.class, Collection.class) )); } else { pushClass(writer, classes[0]); @@ -1521,7 +1522,7 @@ public int visitExecutableMethod(TypedElement declaringType, this.annotationMetadata, parameterElement.getAnnotationMetadata() ); - DefaultAnnotationMetadata.contributeRepeatable( + VisitorContextUtils.contributeRepeatable( this.annotationMetadata, parameterElement.getGenericType() ); @@ -2056,7 +2057,7 @@ private void visitFieldInjectionPointInternal( autoApplyNamedIfPresent(fieldElement, annotationMetadata); DefaultAnnotationMetadata.contributeDefaults(this.annotationMetadata, annotationMetadata); - DefaultAnnotationMetadata.contributeRepeatable(this.annotationMetadata, fieldElement.getGenericField()); + VisitorContextUtils.contributeRepeatable(this.annotationMetadata, fieldElement.getGenericField()); GeneratorAdapter injectMethodVisitor = this.injectMethodVisitor; @@ -2113,7 +2114,7 @@ private void putField(GeneratorAdapter injectMethodVisitor, FieldElement fieldEl } private Label pushPropertyContainsCheck(GeneratorAdapter injectMethodVisitor, ClassElement propertyType, String propertyName, AnnotationMetadata annotationMetadata) { - Optional propertyValue = annotationMetadata.stringValue(Property.class, "name"); + String propertyValue = annotationMetadata.stringValue(Property.class, "name").orElse(propertyName); Label trueCondition = new Label(); Label falseCondition = new Label(); @@ -2123,7 +2124,7 @@ private Label pushPropertyContainsCheck(GeneratorAdapter injectMethodVisitor, Cl // 2nd argument load BeanContext injectMethodVisitor.loadArg(1); // 3rd argument push property name - injectMethodVisitor.push(propertyValue.get()); + injectMethodVisitor.push(propertyValue); if (isMultiValueProperty(propertyType)) { injectMethodVisitor.invokeVirtual(beanDefinitionType, CONTAINS_PROPERTIES_VALUE_METHOD); } else { @@ -2313,14 +2314,14 @@ private void visitMethodInjectionPointInternal(MethodVisitData methodVisitData, final boolean requiresReflection = methodVisitData.requiresReflection; final ClassElement returnType = methodElement.getReturnType(); DefaultAnnotationMetadata.contributeDefaults(this.annotationMetadata, annotationMetadata); - DefaultAnnotationMetadata.contributeRepeatable(this.annotationMetadata, returnType); + VisitorContextUtils.contributeRepeatable(this.annotationMetadata, returnType); boolean hasArguments = methodElement.hasParameters(); int argCount = hasArguments ? argumentTypes.size() : 0; Type declaringTypeRef = JavaModelUtils.getTypeReference(declaringType); boolean hasInjectScope = false; for (ParameterElement value : argumentTypes) { DefaultAnnotationMetadata.contributeDefaults(this.annotationMetadata, value.getAnnotationMetadata()); - DefaultAnnotationMetadata.contributeRepeatable(this.annotationMetadata, value.getGenericType()); + VisitorContextUtils.contributeRepeatable(this.annotationMetadata, value.getGenericType()); if (value.hasDeclaredAnnotation(InjectScope.class)) { hasInjectScope = true; } @@ -2689,7 +2690,7 @@ private void applyDefaultNamedToParameters(List argumentTypes) for (ParameterElement parameterElement : argumentTypes) { final AnnotationMetadata annotationMetadata = parameterElement.getAnnotationMetadata(); DefaultAnnotationMetadata.contributeDefaults(this.annotationMetadata, annotationMetadata); - DefaultAnnotationMetadata.contributeRepeatable(this.annotationMetadata, parameterElement.getGenericType()); + VisitorContextUtils.contributeRepeatable(this.annotationMetadata, parameterElement.getGenericType()); autoApplyNamedIfPresent(parameterElement, annotationMetadata); } } @@ -3776,18 +3777,18 @@ private boolean resolveArgumentGenericType(GeneratorAdapter visitor, ClassElemen } private void resolveInnerTypeArgumentIfNeeded(GeneratorAdapter visitor, ClassElement type) { - if (isInternalGenericTypeContainer(type.getFirstTypeArgument().get())) { + if (isInternalGenericTypeContainer(type.getFirstTypeArgument().orElse(null))) { resolveFirstTypeArgument(visitor); } } - private boolean isInternalGenericTypeContainer(ClassElement type) { - return type.isAssignable(BeanRegistration.class); + private boolean isInternalGenericTypeContainer(@Nullable ClassElement type) { + return type != null && type.isAssignable(BeanRegistration.class); } private void resolveFirstTypeArgument(GeneratorAdapter visitor) { visitor.invokeInterface(Type.getType(TypeVariableResolver.class), - org.objectweb.asm.commons.Method.getMethod(ReflectionUtils.findMethod(TypeVariableResolver.class, "getTypeParameters").get())); + org.objectweb.asm.commons.Method.getMethod(ReflectionUtils.getRequiredInternalMethod(TypeVariableResolver.class, "getTypeParameters"))); visitor.push(0); visitor.arrayLoad(Type.getType(Argument.class)); } @@ -3881,7 +3882,7 @@ private void visitBeanDefinitionConstructorInternal( MethodElement methodElement = (MethodElement) constructor; AnnotationMetadata constructorMetadata = methodElement.getAnnotationMetadata(); DefaultAnnotationMetadata.contributeDefaults(this.annotationMetadata, constructorMetadata); - DefaultAnnotationMetadata.contributeRepeatable(this.annotationMetadata, methodElement.getGenericReturnType()); + VisitorContextUtils.contributeRepeatable(this.annotationMetadata, methodElement.getGenericReturnType()); ParameterElement[] parameters = methodElement.getParameters(); List parameterList = Arrays.asList(parameters); applyDefaultNamedToParameters(parameterList); @@ -4036,7 +4037,7 @@ private void pushNewMethodReference(GeneratorAdapter staticInit, annotationMetadata = annotationMetadata.getTargetAnnotationMetadata(); for (ParameterElement value : methodElement.getParameters()) { DefaultAnnotationMetadata.contributeDefaults(this.annotationMetadata, value.getAnnotationMetadata()); - DefaultAnnotationMetadata.contributeRepeatable(this.annotationMetadata, value.getGenericType()); + VisitorContextUtils.contributeRepeatable(this.annotationMetadata, value.getGenericType()); } staticInit.newInstance(Type.getType(AbstractInitializableBeanDefinition.MethodReference.class)); staticInit.dup(); diff --git a/inject/src/main/java/io/micronaut/inject/writer/ClassGenerationException.java b/core-processor/src/main/java/io/micronaut/inject/writer/ClassGenerationException.java similarity index 100% rename from inject/src/main/java/io/micronaut/inject/writer/ClassGenerationException.java rename to core-processor/src/main/java/io/micronaut/inject/writer/ClassGenerationException.java diff --git a/inject/src/main/java/io/micronaut/inject/writer/ClassOutputWriter.java b/core-processor/src/main/java/io/micronaut/inject/writer/ClassOutputWriter.java similarity index 100% rename from inject/src/main/java/io/micronaut/inject/writer/ClassOutputWriter.java rename to core-processor/src/main/java/io/micronaut/inject/writer/ClassOutputWriter.java diff --git a/inject/src/main/java/io/micronaut/inject/writer/ClassWriterOutputVisitor.java b/core-processor/src/main/java/io/micronaut/inject/writer/ClassWriterOutputVisitor.java similarity index 100% rename from inject/src/main/java/io/micronaut/inject/writer/ClassWriterOutputVisitor.java rename to core-processor/src/main/java/io/micronaut/inject/writer/ClassWriterOutputVisitor.java diff --git a/inject/src/main/java/io/micronaut/inject/writer/ConfigBuilderState.java b/core-processor/src/main/java/io/micronaut/inject/writer/ConfigBuilderState.java similarity index 100% rename from inject/src/main/java/io/micronaut/inject/writer/ConfigBuilderState.java rename to core-processor/src/main/java/io/micronaut/inject/writer/ConfigBuilderState.java diff --git a/inject/src/main/java/io/micronaut/inject/writer/DefaultOriginatingElements.java b/core-processor/src/main/java/io/micronaut/inject/writer/DefaultOriginatingElements.java similarity index 100% rename from inject/src/main/java/io/micronaut/inject/writer/DefaultOriginatingElements.java rename to core-processor/src/main/java/io/micronaut/inject/writer/DefaultOriginatingElements.java diff --git a/inject/src/main/java/io/micronaut/inject/writer/DirectoryClassWriterOutputVisitor.java b/core-processor/src/main/java/io/micronaut/inject/writer/DirectoryClassWriterOutputVisitor.java similarity index 100% rename from inject/src/main/java/io/micronaut/inject/writer/DirectoryClassWriterOutputVisitor.java rename to core-processor/src/main/java/io/micronaut/inject/writer/DirectoryClassWriterOutputVisitor.java diff --git a/inject/src/main/java/io/micronaut/inject/writer/DispatchWriter.java b/core-processor/src/main/java/io/micronaut/inject/writer/DispatchWriter.java similarity index 100% rename from inject/src/main/java/io/micronaut/inject/writer/DispatchWriter.java rename to core-processor/src/main/java/io/micronaut/inject/writer/DispatchWriter.java diff --git a/inject/src/main/java/io/micronaut/inject/writer/ExecutableMethodWriter.java b/core-processor/src/main/java/io/micronaut/inject/writer/ExecutableMethodWriter.java similarity index 99% rename from inject/src/main/java/io/micronaut/inject/writer/ExecutableMethodWriter.java rename to core-processor/src/main/java/io/micronaut/inject/writer/ExecutableMethodWriter.java index ca31882eb40..454079236ad 100644 --- a/inject/src/main/java/io/micronaut/inject/writer/ExecutableMethodWriter.java +++ b/core-processor/src/main/java/io/micronaut/inject/writer/ExecutableMethodWriter.java @@ -29,6 +29,7 @@ import io.micronaut.inject.ast.ParameterElement; import io.micronaut.inject.ast.TypedElement; import io.micronaut.inject.processing.JavaModelUtils; +import io.micronaut.inject.visitor.util.VisitorContextUtils; import org.objectweb.asm.ClassWriter; import org.objectweb.asm.Label; import org.objectweb.asm.MethodVisitor; @@ -271,7 +272,7 @@ public void visitMethod(TypedElement declaringType, for (ParameterElement pe : argumentTypes) { DefaultAnnotationMetadata.contributeDefaults(this.annotationMetadata, pe.getAnnotationMetadata()); - DefaultAnnotationMetadata.contributeRepeatable(this.annotationMetadata, pe.getGenericType()); + VisitorContextUtils.contributeRepeatable(this.annotationMetadata, pe.getGenericType()); } // now invoke super(..) if no arg constructor invokeConstructor( diff --git a/inject/src/main/java/io/micronaut/inject/writer/ExecutableMethodsDefinitionWriter.java b/core-processor/src/main/java/io/micronaut/inject/writer/ExecutableMethodsDefinitionWriter.java similarity index 100% rename from inject/src/main/java/io/micronaut/inject/writer/ExecutableMethodsDefinitionWriter.java rename to core-processor/src/main/java/io/micronaut/inject/writer/ExecutableMethodsDefinitionWriter.java diff --git a/inject/src/main/java/io/micronaut/inject/writer/FileBackedGeneratedFile.java b/core-processor/src/main/java/io/micronaut/inject/writer/FileBackedGeneratedFile.java similarity index 100% rename from inject/src/main/java/io/micronaut/inject/writer/FileBackedGeneratedFile.java rename to core-processor/src/main/java/io/micronaut/inject/writer/FileBackedGeneratedFile.java diff --git a/inject/src/main/java/io/micronaut/inject/writer/GeneratedFile.java b/core-processor/src/main/java/io/micronaut/inject/writer/GeneratedFile.java similarity index 100% rename from inject/src/main/java/io/micronaut/inject/writer/GeneratedFile.java rename to core-processor/src/main/java/io/micronaut/inject/writer/GeneratedFile.java diff --git a/inject/src/main/java/io/micronaut/inject/writer/OriginatingElements.java b/core-processor/src/main/java/io/micronaut/inject/writer/OriginatingElements.java similarity index 100% rename from inject/src/main/java/io/micronaut/inject/writer/OriginatingElements.java rename to core-processor/src/main/java/io/micronaut/inject/writer/OriginatingElements.java diff --git a/inject/src/main/java/io/micronaut/inject/writer/ProxyingBeanDefinitionVisitor.java b/core-processor/src/main/java/io/micronaut/inject/writer/ProxyingBeanDefinitionVisitor.java similarity index 100% rename from inject/src/main/java/io/micronaut/inject/writer/ProxyingBeanDefinitionVisitor.java rename to core-processor/src/main/java/io/micronaut/inject/writer/ProxyingBeanDefinitionVisitor.java diff --git a/inject/src/main/java/io/micronaut/inject/writer/StaticOriginatingElements.java b/core-processor/src/main/java/io/micronaut/inject/writer/StaticOriginatingElements.java similarity index 100% rename from inject/src/main/java/io/micronaut/inject/writer/StaticOriginatingElements.java rename to core-processor/src/main/java/io/micronaut/inject/writer/StaticOriginatingElements.java diff --git a/inject/src/main/java/io/micronaut/inject/writer/StringSwitchWriter.java b/core-processor/src/main/java/io/micronaut/inject/writer/StringSwitchWriter.java similarity index 100% rename from inject/src/main/java/io/micronaut/inject/writer/StringSwitchWriter.java rename to core-processor/src/main/java/io/micronaut/inject/writer/StringSwitchWriter.java diff --git a/inject/src/main/java/io/micronaut/inject/writer/package-info.java b/core-processor/src/main/java/io/micronaut/inject/writer/package-info.java similarity index 100% rename from inject/src/main/java/io/micronaut/inject/writer/package-info.java rename to core-processor/src/main/java/io/micronaut/inject/writer/package-info.java diff --git a/inject/src/main/resources/META-INF/services/io.micronaut.inject.annotation.AnnotationMapper b/core-processor/src/main/resources/META-INF/services/io.micronaut.inject.annotation.AnnotationMapper similarity index 64% rename from inject/src/main/resources/META-INF/services/io.micronaut.inject.annotation.AnnotationMapper rename to core-processor/src/main/resources/META-INF/services/io.micronaut.inject.annotation.AnnotationMapper index 0df05f868e7..ca2ab670e9c 100644 --- a/inject/src/main/resources/META-INF/services/io.micronaut.inject.annotation.AnnotationMapper +++ b/core-processor/src/main/resources/META-INF/services/io.micronaut.inject.annotation.AnnotationMapper @@ -4,5 +4,7 @@ io.micronaut.inject.beans.visitor.EntityIntrospectedAnnotationMapper io.micronaut.inject.beans.visitor.EntityReflectiveAccessAnnotationMapper io.micronaut.inject.beans.visitor.MappedSuperClassIntrospectionMapper io.micronaut.inject.beans.visitor.JsonCreatorAnnotationMapper -io.micronaut.inject.beans.visitor.jakarta.persistence.JakartaEntityIntrospectedAnnotationMapper -io.micronaut.inject.beans.visitor.jakarta.persistence.JakartaMappedSuperClassIntrospectionMapper +io.micronaut.inject.beans.visitor.persistence.JakartaEntityIntrospectedAnnotationMapper +io.micronaut.inject.beans.visitor.persistence.JakartaMappedSuperClassIntrospectionMapper +io.micronaut.aop.mapper.InterceptorBeanMapper + diff --git a/inject/src/main/resources/META-INF/services/io.micronaut.inject.annotation.AnnotationRemapper b/core-processor/src/main/resources/META-INF/services/io.micronaut.inject.annotation.AnnotationRemapper similarity index 100% rename from inject/src/main/resources/META-INF/services/io.micronaut.inject.annotation.AnnotationRemapper rename to core-processor/src/main/resources/META-INF/services/io.micronaut.inject.annotation.AnnotationRemapper diff --git a/inject/src/main/resources/META-INF/services/io.micronaut.inject.annotation.AnnotationTransformer b/core-processor/src/main/resources/META-INF/services/io.micronaut.inject.annotation.AnnotationTransformer similarity index 84% rename from inject/src/main/resources/META-INF/services/io.micronaut.inject.annotation.AnnotationTransformer rename to core-processor/src/main/resources/META-INF/services/io.micronaut.inject.annotation.AnnotationTransformer index 1ef11500700..6904307151d 100644 --- a/inject/src/main/resources/META-INF/services/io.micronaut.inject.annotation.AnnotationTransformer +++ b/core-processor/src/main/resources/META-INF/services/io.micronaut.inject.annotation.AnnotationTransformer @@ -4,4 +4,4 @@ io.micronaut.inject.annotation.internal.KotlinNullableMapper io.micronaut.inject.annotation.internal.KotlinNotNullMapper io.micronaut.inject.annotation.internal.JakartaPostConstructTransformer io.micronaut.inject.annotation.internal.JakartaPreDestroyTransformer -io.micronaut.inject.mappers.IntrospectedToBeanPropertiesTransformer +io.micronaut.inject.beans.visitor.IntrospectedToBeanPropertiesTransformer diff --git a/inject/src/main/resources/META-INF/services/io.micronaut.inject.configuration.ConfigurationMetadataWriter b/core-processor/src/main/resources/META-INF/services/io.micronaut.inject.configuration.ConfigurationMetadataWriter similarity index 100% rename from inject/src/main/resources/META-INF/services/io.micronaut.inject.configuration.ConfigurationMetadataWriter rename to core-processor/src/main/resources/META-INF/services/io.micronaut.inject.configuration.ConfigurationMetadataWriter diff --git a/inject/src/main/resources/META-INF/services/io.micronaut.inject.visitor.TypeElementVisitor b/core-processor/src/main/resources/META-INF/services/io.micronaut.inject.visitor.TypeElementVisitor similarity index 85% rename from inject/src/main/resources/META-INF/services/io.micronaut.inject.visitor.TypeElementVisitor rename to core-processor/src/main/resources/META-INF/services/io.micronaut.inject.visitor.TypeElementVisitor index b728d9ef8d5..cc461fe73db 100644 --- a/inject/src/main/resources/META-INF/services/io.micronaut.inject.visitor.TypeElementVisitor +++ b/core-processor/src/main/resources/META-INF/services/io.micronaut.inject.visitor.TypeElementVisitor @@ -5,4 +5,5 @@ io.micronaut.context.visitor.ExecutableVisitor io.micronaut.context.visitor.ValidationVisitor io.micronaut.context.visitor.InternalApiTypeElementVisitor io.micronaut.context.visitor.ConfigurationReaderVisitor +io.micronaut.scheduling.async.validation.AsyncTypeElementVisitor diff --git a/core/build.gradle b/core/build.gradle index 8216747be5c..97e12a727e7 100644 --- a/core/build.gradle +++ b/core/build.gradle @@ -11,18 +11,10 @@ micronautBuild { } } -tasks.named("shadowJar") { - exclude "module-info.class" -} - dependencies { compileOnly libs.managed.jsr305 compileOnly libs.managed.graal compileOnly libs.kotlin.stdlib - compileOnly libs.asm.tree - - shadowCompile libs.bundles.asm - shadowCompile libs.asm.tree } spotless { diff --git a/graal/build.gradle b/graal/build.gradle index 01018e60c94..26667cb75c1 100644 --- a/graal/build.gradle +++ b/graal/build.gradle @@ -3,7 +3,7 @@ plugins { } dependencies { annotationProcessor project(":inject-java") - api project(":inject") + api project(":core-processor") implementation libs.managed.jackson.databind testImplementation project(":inject") diff --git a/http-server/src/main/java/io/micronaut/http/server/RouteExecutor.java b/http-server/src/main/java/io/micronaut/http/server/RouteExecutor.java index 0ffb3607ff6..846498b9a7c 100644 --- a/http-server/src/main/java/io/micronaut/http/server/RouteExecutor.java +++ b/http-server/src/main/java/io/micronaut/http/server/RouteExecutor.java @@ -91,7 +91,7 @@ import static io.micronaut.core.util.KotlinUtils.isKotlinCoroutineSuspended; import static io.micronaut.http.HttpAttributes.AVAILABLE_HTTP_METHODS; -import static io.micronaut.inject.util.KotlinExecutableMethodUtils.isKotlinFunctionReturnTypeUnit; +import static io.micronaut.inject.beans.KotlinExecutableMethodUtils.isKotlinFunctionReturnTypeUnit; /** * A class responsible for executing routes. diff --git a/http-validation/build.gradle b/http-validation/build.gradle index a778e559dce..4cfa3152eff 100644 --- a/http-validation/build.gradle +++ b/http-validation/build.gradle @@ -4,7 +4,7 @@ plugins { dependencies { annotationProcessor project(":inject-java") - implementation project(":inject") + implementation project(":core-processor") implementation project(":http-server") implementation project(":websocket") diff --git a/inject-groovy/build.gradle b/inject-groovy/build.gradle index a13e1a0ccd1..f979414609f 100644 --- a/inject-groovy/build.gradle +++ b/inject-groovy/build.gradle @@ -10,8 +10,7 @@ micronautBuild { } dependencies { - api project(":inject") - api project(':aop') + api project(":core-processor") api libs.managed.groovy // testImplementation 'javax.validation:validation-api:1.1.0.Final' testImplementation project(":context") diff --git a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyVisitorContext.java b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyVisitorContext.java index abeb5caa3a9..a09393117c2 100644 --- a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyVisitorContext.java +++ b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyVisitorContext.java @@ -33,7 +33,7 @@ import io.micronaut.inject.ast.ClassElement; import io.micronaut.inject.ast.Element; import io.micronaut.inject.ast.ElementAnnotationMetadataFactory; -import io.micronaut.inject.util.VisitorContextUtils; +import io.micronaut.inject.visitor.util.VisitorContextUtils; import io.micronaut.inject.visitor.VisitorContext; import io.micronaut.inject.writer.AbstractBeanDefinitionBuilder; import io.micronaut.inject.writer.ClassWriterOutputVisitor; diff --git a/inject-java/build.gradle b/inject-java/build.gradle index 2c7ffbae470..6d2e166435b 100644 --- a/inject-java/build.gradle +++ b/inject-java/build.gradle @@ -9,8 +9,7 @@ micronautBuild { } dependencies { - api project(":inject") - api project(':aop') + api project(":core-processor") if (!JavaVersion.current().isJava9Compatible()) { compileOnly files(org.gradle.internal.jvm.Jvm.current().toolsJar) diff --git a/inject-java/src/main/java/io/micronaut/annotation/processing/ConfigurationMetadataProcessor.java b/inject-java/src/main/java/io/micronaut/annotation/processing/ConfigurationMetadataProcessor.java index 37a4fbaf395..e47037e29a9 100644 --- a/inject-java/src/main/java/io/micronaut/annotation/processing/ConfigurationMetadataProcessor.java +++ b/inject-java/src/main/java/io/micronaut/annotation/processing/ConfigurationMetadataProcessor.java @@ -79,7 +79,7 @@ private void writeConfigurationMetadata() { } } } finally { - ConfigurationMetadataBuilder.INSTANCE = new ConfigurationMetadataBuilder(); + ConfigurationMetadataBuilder.reset(); } } diff --git a/inject-java/src/main/java/io/micronaut/annotation/processing/TypeElementVisitorProcessor.java b/inject-java/src/main/java/io/micronaut/annotation/processing/TypeElementVisitorProcessor.java index fce6d3726ac..49394e2fb27 100644 --- a/inject-java/src/main/java/io/micronaut/annotation/processing/TypeElementVisitorProcessor.java +++ b/inject-java/src/main/java/io/micronaut/annotation/processing/TypeElementVisitorProcessor.java @@ -364,8 +364,9 @@ Collection findCoreTypeElementVisitors( } return true; }).stream() + .filter(Objects::nonNull) // remove duplicate classes - .collect(Collectors.toMap(v -> v.getClass(), v -> v, (a, b) -> a)).values(); + .collect(Collectors.toMap(Object::getClass, v -> v, (a, b) -> a)).values(); } /** diff --git a/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaVisitorContext.java b/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaVisitorContext.java index 8d46b88d4f1..3fec7a82fad 100644 --- a/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaVisitorContext.java +++ b/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaVisitorContext.java @@ -35,7 +35,7 @@ import io.micronaut.inject.ast.beans.BeanElement; import io.micronaut.inject.ast.beans.BeanElementBuilder; import io.micronaut.inject.configuration.ConfigurationMetadataBuilder; -import io.micronaut.inject.util.VisitorContextUtils; +import io.micronaut.inject.visitor.util.VisitorContextUtils; import io.micronaut.inject.visitor.BeanElementVisitorContext; import io.micronaut.inject.visitor.TypeElementVisitor; import io.micronaut.inject.visitor.VisitorContext; diff --git a/inject-java/src/test/groovy/io/micronaut/aop/compile/AroundConstructCompileSpec.groovy b/inject-java/src/test/groovy/io/micronaut/aop/compile/AroundConstructCompileSpec.groovy index db2e4c8c96e..2aa04cd70b2 100644 --- a/inject-java/src/test/groovy/io/micronaut/aop/compile/AroundConstructCompileSpec.groovy +++ b/inject-java/src/test/groovy/io/micronaut/aop/compile/AroundConstructCompileSpec.groovy @@ -56,23 +56,23 @@ class MyBean { @TestAnn(num=1) class TestInterceptor implements ConstructorInterceptor { public boolean invoked = false; - @Override + @Override public Object intercept(ConstructorInvocationContext context) { invoked = true; return context.proceed(); } -} +} @Singleton @TestAnn(num=2) class TestInterceptor2 implements ConstructorInterceptor { public boolean invoked = false; - @Override + @Override public Object intercept(ConstructorInvocationContext context) { invoked = true; return context.proceed(); } -} +} ''') @@ -97,7 +97,7 @@ class TestInterceptor2 implements ConstructorInterceptor { void 'test around construct on type and constructor with proxy target + bind members'() { given: ApplicationContext context = buildContext(""" -package ctorbinding; +package ctorbinding; import java.lang.annotation.*; import io.micronaut.aop.*; @@ -175,7 +175,7 @@ class Interceptor2 implements ConstructorInterceptor { void 'test around construct on type and constructor with proxy target'() { given: ApplicationContext context = buildContext(""" -package ctorbinding; +package ctorbinding; import java.lang.annotation.*; import io.micronaut.aop.*; @@ -253,7 +253,7 @@ class Interceptor2 implements ConstructorInterceptor { void 'test around construct on type and constructor'() { given: ApplicationContext context = buildContext(""" -package ctorbinding; +package ctorbinding; import java.lang.annotation.*; import io.micronaut.aop.*; @@ -369,21 +369,21 @@ class MyOtherBean {} class TestConstructInterceptor implements ConstructorInterceptor { boolean invoked = false; Object[] parameters; - + @Override public Object intercept(ConstructorInvocationContext context) { invoked = true; parameters = context.getParameterValues(); return context.proceed(); } -} +} @Singleton @InterceptorBean(TestAnn.class) class TypeSpecificConstructInterceptor implements ConstructorInterceptor { boolean invoked = false; Object[] parameters; - + @Override public MyBean intercept(ConstructorInvocationContext context) { invoked = true; @@ -391,7 +391,7 @@ class TypeSpecificConstructInterceptor implements ConstructorInterceptor MyBean mb = context.proceed(); return mb; } -} +} @Singleton @InterceptorBinding(TestAnn.class) @@ -402,7 +402,7 @@ class TestInterceptor implements MethodInterceptor { invoked = true; return context.proceed(); } -} +} @Singleton class AnotherInterceptor implements Interceptor { @@ -412,7 +412,7 @@ class AnotherInterceptor implements Interceptor { invoked = true; return context.proceed(); } -} +} """) when: def interceptor = getBean(context, 'annbinding1.TestInterceptor') @@ -513,14 +513,14 @@ class MyOtherBean {} class TestConstructInterceptor implements ConstructorInterceptor { boolean invoked = false; Object[] parameters; - + @Override public Object intercept(ConstructorInvocationContext context) { invoked = true; parameters = context.getParameterValues(); return context.proceed(); } -} +} @Singleton @InterceptorBinding(TestAnn.class) @@ -531,7 +531,7 @@ class TestInterceptor implements MethodInterceptor { invoked = true; return context.proceed(); } -} +} @Singleton class AnotherInterceptor implements Interceptor { @@ -541,7 +541,7 @@ class AnotherInterceptor implements Interceptor { invoked = true; return context.proceed(); } -} +} """) when: def interceptor = getBean(context, 'annbinding1.TestInterceptor') @@ -610,7 +610,7 @@ import static java.lang.annotation.RetentionPolicy.RUNTIME; class MyBean { @TestAnn MyBean(io.micronaut.context.env.Environment env) {} - + void test() { } } @@ -627,14 +627,14 @@ class MyBean { class TestConstructInterceptor implements ConstructorInterceptor { boolean invoked = false; Object[] parameters; - + @Override public Object intercept(ConstructorInvocationContext context) { invoked = true; parameters = context.getParameterValues(); return context.proceed(); } -} +} """) when: @@ -697,7 +697,7 @@ class MyOtherBean {} @Factory class InterceptorFactory { boolean aroundConstructInvoked = false; - + @InterceptorBean(TestAnn.class) ConstructorInterceptor aroundIntercept() { return (context) -> { @@ -705,7 +705,7 @@ class InterceptorFactory { return context.proceed(); }; } - + } """) @@ -758,14 +758,14 @@ abstract class MyBean { class TestConstructInterceptor implements ConstructorInterceptor { boolean invoked = false; Object[] parameters; - + @Override public Object intercept(ConstructorInvocationContext context) { invoked = true; parameters = context.getParameterValues(); return context.proceed(); } -} +} @Singleton @InterceptorBinding(TestAnn.class) @@ -776,7 +776,7 @@ class TestInterceptor implements MethodInterceptor { invoked = true; return "good"; } -} +} @Singleton class AnotherInterceptor implements Interceptor { @@ -786,7 +786,7 @@ class AnotherInterceptor implements Interceptor { invoked = true; return context.proceed(); } -} +} """) when: def interceptor = getBean(context, 'annbinding1.TestInterceptor') diff --git a/inject-java/src/test/groovy/io/micronaut/inject/annotation/AnnotationMapperSpec.groovy b/inject-java/src/test/groovy/io/micronaut/inject/annotation/AnnotationMapperSpec.groovy index fd527d52e19..e403b7fa1c6 100644 --- a/inject-java/src/test/groovy/io/micronaut/inject/annotation/AnnotationMapperSpec.groovy +++ b/inject-java/src/test/groovy/io/micronaut/inject/annotation/AnnotationMapperSpec.groovy @@ -11,8 +11,6 @@ import io.micronaut.http.annotation.Header import io.micronaut.http.annotation.HttpMethodMapping import io.micronaut.http.annotation.QueryValue import io.micronaut.annotation.processing.test.AbstractTypeElementSpec -import io.micronaut.inject.BeanDefinition -import io.micronaut.inject.ExecutableMethod import io.micronaut.inject.visitor.VisitorContext import java.lang.annotation.Annotation @@ -35,7 +33,7 @@ class Test { Test create() { return new Test(); } - + } ''', 'create') @@ -66,7 +64,7 @@ class Test { Test create(@io.micronaut.inject.annotation.CustomHeader String query) { return new Test(); } - + } ''') @@ -90,7 +88,7 @@ class Test { @io.micronaut.inject.annotation.CustomGet void test() { - } + } } ''', 'test') diff --git a/inject-java/src/test/groovy/io/micronaut/inject/annotation/repeatable/MapToRepeatableSpec.groovy b/inject-java/src/test/groovy/io/micronaut/inject/annotation/repeatable/MapToRepeatableSpec.groovy index 0250332d0fb..9b776cb2bb0 100644 --- a/inject-java/src/test/groovy/io/micronaut/inject/annotation/repeatable/MapToRepeatableSpec.groovy +++ b/inject-java/src/test/groovy/io/micronaut/inject/annotation/repeatable/MapToRepeatableSpec.groovy @@ -5,7 +5,6 @@ import io.micronaut.context.annotation.Requirements import io.micronaut.context.annotation.Requires import io.micronaut.core.annotation.AnnotationValue import io.micronaut.core.annotation.NonNull -import io.micronaut.inject.BeanDefinition import io.micronaut.inject.annotation.AnnotationMapper import io.micronaut.inject.annotation.NamedAnnotationMapper import io.micronaut.inject.visitor.VisitorContext diff --git a/inject-java/src/test/groovy/io/micronaut/inject/ast/beans/BeanElementVisitorSpec.groovy b/inject-java/src/test/groovy/io/micronaut/inject/ast/beans/BeanElementVisitorSpec.groovy index e81a5301a88..80ed964a00b 100644 --- a/inject-java/src/test/groovy/io/micronaut/inject/ast/beans/BeanElementVisitorSpec.groovy +++ b/inject-java/src/test/groovy/io/micronaut/inject/ast/beans/BeanElementVisitorSpec.groovy @@ -22,20 +22,20 @@ import jakarta.inject.Singleton; @Named("blah") class Test implements Runnable { @Inject ConversionService conversionService; - + @Inject void setEnvironment(Environment environment) { - + } - + @Override public void run() { - + } } @Prototype class Excluded { - + } ''') @@ -65,14 +65,14 @@ import jakarta.inject.Singleton; @Named("blah") class Test implements Runnable { @Inject ConversionService conversionService; - + @Inject void setEnvironment(Environment environment) { - + } - + @Override public void run() { - + } } ''') @@ -110,19 +110,19 @@ import jakarta.inject.Singleton; @Factory class TestFactory implements Runnable { @Inject ConversionService conversionService; - + @Inject void setEnvironment(Environment environment) { - + } - + @Bean Test test() { return new Test(); } - + @Override public void run() { - + } } diff --git a/inject-java/src/test/groovy/io/micronaut/inject/beanbuilder/BeanElementBuilderSpec.groovy b/inject-java/src/test/groovy/io/micronaut/inject/beanbuilder/BeanElementBuilderSpec.groovy index f9e4018ef0c..8a66eeb2fd1 100644 --- a/inject-java/src/test/groovy/io/micronaut/inject/beanbuilder/BeanElementBuilderSpec.groovy +++ b/inject-java/src/test/groovy/io/micronaut/inject/beanbuilder/BeanElementBuilderSpec.groovy @@ -2,15 +2,9 @@ package io.micronaut.inject.beanbuilder import io.micronaut.annotation.processing.test.AbstractTypeElementSpec import io.micronaut.aop.Intercepted -import io.micronaut.context.ApplicationContext import io.micronaut.context.exceptions.NoSuchBeanException -import io.micronaut.core.annotation.NonNull -import io.micronaut.inject.BeanDefinition -import io.micronaut.inject.annotation.AnnotationTransformer -import io.micronaut.inject.qualifiers.Qualifiers import io.micronaut.inject.visitor.TypeElementVisitor -import java.lang.annotation.Annotation import java.util.function.Supplier class BeanElementBuilderSpec extends AbstractTypeElementSpec { @@ -30,7 +24,7 @@ class Test { Object invoke(CustomInvocationContext context) { invoked = true; return context.proceed(); - } + } } @jakarta.inject.Singleton @@ -97,7 +91,7 @@ class Test { Object invoke(CustomInvocationContext context) { invoked = true; return context.proceed(); - } + } } @jakarta.inject.Singleton @@ -164,7 +158,7 @@ class Test { Object invoke(CustomInvocationContext context) { invoked = true; return context.proceed(); - } + } } @jakarta.inject.Singleton @@ -231,7 +225,7 @@ class Test { Object invoke(CustomInvocationContext context) { invoked = true; return context.proceed(); - } + } } @jakarta.inject.Singleton diff --git a/inject-java/src/test/groovy/io/micronaut/inject/beanbuilder/TestInterceptorBindingTransformer.java b/inject-java/src/test/groovy/io/micronaut/inject/beanbuilder/TestInterceptorBindingTransformer.java index a4e1713e753..53ae5f2b39b 100644 --- a/inject-java/src/test/groovy/io/micronaut/inject/beanbuilder/TestInterceptorBindingTransformer.java +++ b/inject-java/src/test/groovy/io/micronaut/inject/beanbuilder/TestInterceptorBindingTransformer.java @@ -6,7 +6,6 @@ import io.micronaut.inject.visitor.VisitorContext; import java.util.Arrays; -import java.util.Collections; import java.util.List; public class TestInterceptorBindingTransformer implements TypedAnnotationTransformer { diff --git a/inject-java/src/test/groovy/io/micronaut/inject/configproperties/ConfigurationMetadataSpec.groovy b/inject-java/src/test/groovy/io/micronaut/inject/configuration/ConfigurationMetadataSpec.groovy similarity index 99% rename from inject-java/src/test/groovy/io/micronaut/inject/configproperties/ConfigurationMetadataSpec.groovy rename to inject-java/src/test/groovy/io/micronaut/inject/configuration/ConfigurationMetadataSpec.groovy index b2c6e437b44..3d3ca800bb6 100644 --- a/inject-java/src/test/groovy/io/micronaut/inject/configproperties/ConfigurationMetadataSpec.groovy +++ b/inject-java/src/test/groovy/io/micronaut/inject/configuration/ConfigurationMetadataSpec.groovy @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.micronaut.inject.configproperties +package io.micronaut.inject.configuration import com.fasterxml.jackson.databind.ObjectMapper import io.micronaut.annotation.processing.ConfigurationMetadataProcessor @@ -51,6 +51,10 @@ class ConfigurationMetadataSpec extends AbstractTypeElementSpec { return super.buildAndReadResourceAsString("META-INF/spring-configuration-metadata.json", cls) } + def setup() { + ConfigurationMetadataBuilder.reset() + } + void "test configuration builder on method"() { when: String metadataJson = buildConfigurationMetadata(''' diff --git a/inject-java/src/test/groovy/io/micronaut/visitors/CustomAnnMapper.java b/inject-java/src/test/groovy/io/micronaut/visitors/CustomAnnMapper.java index 4a78e2f95c6..d605db2ae94 100644 --- a/inject-java/src/test/groovy/io/micronaut/visitors/CustomAnnMapper.java +++ b/inject-java/src/test/groovy/io/micronaut/visitors/CustomAnnMapper.java @@ -2,7 +2,6 @@ import io.micronaut.core.annotation.AnnotationValue; import io.micronaut.inject.annotation.TypedAnnotationMapper; -import io.micronaut.inject.visitor.TypeElementVisitor; import io.micronaut.inject.visitor.VisitorContext; import java.util.Collections; diff --git a/inject-test-utils/build.gradle b/inject-test-utils/build.gradle index 12d31abebaf..d948e076183 100644 --- a/inject-test-utils/build.gradle +++ b/inject-test-utils/build.gradle @@ -7,5 +7,5 @@ dependencies { api(libs.managed.spock) { exclude module:'groovy-all' } - api project(":inject") + api project(":core-processor") } diff --git a/inject/src/main/java/io/micronaut/inject/annotation/DefaultAnnotationMetadata.java b/inject/src/main/java/io/micronaut/inject/annotation/DefaultAnnotationMetadata.java index 5e98d98f6ba..a0b62776273 100644 --- a/inject/src/main/java/io/micronaut/inject/annotation/DefaultAnnotationMetadata.java +++ b/inject/src/main/java/io/micronaut/inject/annotation/DefaultAnnotationMetadata.java @@ -31,7 +31,6 @@ import io.micronaut.core.util.CollectionUtils; import io.micronaut.core.util.StringUtils; import io.micronaut.core.value.OptionalValues; -import io.micronaut.inject.ast.ClassElement; import java.lang.annotation.Annotation; import java.lang.annotation.Repeatable; @@ -2288,30 +2287,6 @@ public static void contributeRepeatable(AnnotationMetadata target, AnnotationMet } } - /** - * Contributes repeatable annotation metadata to the given class element. - * - *

WARNING: for internal use only be the framework

- * - * @param target The target - * @param classElement The source - */ - @Internal - public static void contributeRepeatable(AnnotationMetadata target, ClassElement classElement) { - contributeRepeatable(target, classElement, new HashSet<>()); - } - - private static void contributeRepeatable(AnnotationMetadata target, ClassElement classElement, Set alreadySeen) { - alreadySeen.add(classElement); - contributeRepeatable(target, classElement.getAnnotationMetadata()); - for (ClassElement element : classElement.getTypeArguments().values()) { - if (alreadySeen.contains(classElement)) { - continue; - } - contributeRepeatable(target, element); - } - } - /** *

Sets a member of the given {@link AnnotationMetadata} return a new annotation metadata instance without * mutating the existing.

diff --git a/inject/src/main/java/io/micronaut/inject/util/KotlinExecutableMethodUtils.java b/inject/src/main/java/io/micronaut/inject/beans/KotlinExecutableMethodUtils.java similarity index 95% rename from inject/src/main/java/io/micronaut/inject/util/KotlinExecutableMethodUtils.java rename to inject/src/main/java/io/micronaut/inject/beans/KotlinExecutableMethodUtils.java index 1b1bc6c5a55..967963ebc95 100644 --- a/inject/src/main/java/io/micronaut/inject/util/KotlinExecutableMethodUtils.java +++ b/inject/src/main/java/io/micronaut/inject/beans/KotlinExecutableMethodUtils.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.micronaut.inject.util; +package io.micronaut.inject.beans; import io.micronaut.core.annotation.Internal; import io.micronaut.core.type.Argument; @@ -31,7 +31,7 @@ * @since 1.3.0 */ @Internal -public class KotlinExecutableMethodUtils { +public final class KotlinExecutableMethodUtils { /** * Kotlin suspend function return type check. * diff --git a/inject/src/test/groovy/io/micronaut/inject/util/VisitorContextUtilsSpec.groovy b/inject/src/test/groovy/io/micronaut/inject/util/VisitorContextUtilsSpec.groovy index 7adc5b4a204..ebee9366bda 100644 --- a/inject/src/test/groovy/io/micronaut/inject/util/VisitorContextUtilsSpec.groovy +++ b/inject/src/test/groovy/io/micronaut/inject/util/VisitorContextUtilsSpec.groovy @@ -1,5 +1,6 @@ package io.micronaut.inject.util +import io.micronaut.inject.visitor.util.VisitorContextUtils import spock.lang.Specification import spock.lang.Stepwise import spock.util.environment.RestoreSystemProperties diff --git a/router/src/main/java/io/micronaut/web/router/RouteInfo.java b/router/src/main/java/io/micronaut/web/router/RouteInfo.java index ca57ee83bbb..e7ba7c2dad0 100644 --- a/router/src/main/java/io/micronaut/web/router/RouteInfo.java +++ b/router/src/main/java/io/micronaut/web/router/RouteInfo.java @@ -27,7 +27,7 @@ import io.micronaut.http.annotation.Produces; import io.micronaut.http.annotation.Status; import io.micronaut.http.sse.Event; -import io.micronaut.inject.util.KotlinExecutableMethodUtils; +import io.micronaut.inject.beans.KotlinExecutableMethodUtils; import java.util.Arrays; import java.util.Collections; diff --git a/settings.gradle b/settings.gradle index 4d03f80210b..654c0df268b 100644 --- a/settings.gradle +++ b/settings.gradle @@ -25,6 +25,7 @@ include "parent" include "buffer-netty" include "core" include "core-reactive" +include "core-processor" include "context" include "function" include "function-client" diff --git a/src/main/docs/guide/appendix/breaks.adoc b/src/main/docs/guide/appendix/breaks.adoc index 2b8ec43ad38..8a2127546ba 100644 --- a/src/main/docs/guide/appendix/breaks.adoc +++ b/src/main/docs/guide/appendix/breaks.adoc @@ -4,6 +4,18 @@ This section documents breaking changes between Micronaut versions === Core Changes +==== Compilation Time API Split into new module + +In order to keep the runtime small all types and interfaces that are used at compilation time only (like the `io.micronaut.inject.ast` API) have been moved into a separate module: + +dependency::micronaut-core-processor[] + +If you are using types and interfaces from this module you should take care to split the compilation time and runtime logic of your module into separate modules. + +==== ASM No Longer Shaded + +https://asm.ow2.io/[ASM] is no longer shaded into the `io.micronaut.asm` package. If you depend on this library you should directly depend on the latest version of ASM. + ==== Caffeine No Longer Shaded https://github.com/ben-manes/caffeine[Caffeine] is no longer shaded into the `io.micronaut.caffeine` package. If you depend on this library you should directly depend on the latest version of Caffeine. diff --git a/test-suite/build.gradle b/test-suite/build.gradle index 8a7f190001b..cfb4f11f847 100644 --- a/test-suite/build.gradle +++ b/test-suite/build.gradle @@ -24,7 +24,7 @@ micronautBuild { dependencies { annotationProcessor project(":inject-java") - api project(":inject") + api project(":core-processor") testImplementation project(":context") testImplementation libs.managed.netty.codec.http From b98b0af4e1e3d31312569cc927d072c6e9a07439 Mon Sep 17 00:00:00 2001 From: micronaut-build <65172877+micronaut-build@users.noreply.github.com> Date: Wed, 26 Oct 2022 08:01:02 -0400 Subject: [PATCH 153/743] Bump micronaut-kafka to 4.4.1 (#8236) --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 796a4d4047b..a22acc28e98 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -87,7 +87,7 @@ managed-micronaut-ignite = "1.0.0.RC1" managed-micronaut-jaxrs = "3.4.0" managed-micronaut-jms = "2.1.0" managed-micronaut-jmx = "3.1.0" -managed-micronaut-kafka = "4.4.0" +managed-micronaut-kafka = "4.4.1" managed-micronaut-kotlin = "3.2.2" managed-micronaut-kubernetes = "3.4.0" managed-micronaut-micrometer = "4.6.1" From ee406bb8174609a98cc409d744f550f54a8fb344 Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Wed, 26 Oct 2022 15:41:15 +0200 Subject: [PATCH 154/743] build: update to SnakeYAML 1.33 (#8231) https://bitbucket.org/snakeyaml/snakeyaml/wiki/Changes --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index a22acc28e98..1dd5992e65c 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -141,7 +141,7 @@ managed-springboot = "2.7.0" managed-swagger = "2.2.3" managed-validation = "2.0.1.Final" managed-testcontainers = "1.17.5" -managed-snakeyaml = "1.32" +managed-snakeyaml = "1.33" micronaut-docs = "2.0.0" [libraries] From a2c0a8b4322359a3fd1d7786ee50119ab4c90d9a Mon Sep 17 00:00:00 2001 From: Denis Stepanov Date: Wed, 26 Oct 2022 17:47:25 +0400 Subject: [PATCH 155/743] Fix detecting bean for @ConfigurationInject injection (#8234) --- ...ConfigurationReaderBeanElementCreator.java | 21 +++- .../ConfigurationPropertiesInjectSpec.groovy | 49 ++++++++ ...figWithConstructorConfigurationInject.java | 118 ++++++++++++++++++ ...MyConfigWithMethodConfigurationInject.java | 118 ++++++++++++++++++ 4 files changed, 302 insertions(+), 4 deletions(-) create mode 100644 inject-java/src/test/groovy/io/micronaut/inject/configproperties/ConfigurationPropertiesInjectSpec.groovy create mode 100644 inject-java/src/test/groovy/io/micronaut/inject/configproperties/MyConfigWithConstructorConfigurationInject.java create mode 100644 inject-java/src/test/groovy/io/micronaut/inject/configproperties/MyConfigWithMethodConfigurationInject.java diff --git a/core-processor/src/main/java/io/micronaut/inject/processing/ConfigurationReaderBeanElementCreator.java b/core-processor/src/main/java/io/micronaut/inject/processing/ConfigurationReaderBeanElementCreator.java index c547d9113d1..cff8e1fe5e7 100644 --- a/core-processor/src/main/java/io/micronaut/inject/processing/ConfigurationReaderBeanElementCreator.java +++ b/core-processor/src/main/java/io/micronaut/inject/processing/ConfigurationReaderBeanElementCreator.java @@ -15,6 +15,8 @@ */ package io.micronaut.inject.processing; +import io.micronaut.context.BeanProvider; +import io.micronaut.context.annotation.Bean; import io.micronaut.context.annotation.ConfigurationBuilder; import io.micronaut.context.annotation.ConfigurationInject; import io.micronaut.context.annotation.ConfigurationReader; @@ -40,6 +42,7 @@ import io.micronaut.inject.configuration.PropertyMetadata; import io.micronaut.inject.visitor.VisitorContext; import io.micronaut.inject.writer.BeanDefinitionVisitor; +import jakarta.inject.Provider; import java.time.Duration; import java.util.Arrays; @@ -90,11 +93,11 @@ protected void applyConfigurationInjectionIfNecessary(BeanDefinitionVisitor visi return; } } - processConfigurationInjectionConstructor(visitor, constructor); + processConfigurationInjectionPoint(visitor, constructor); } - private void processConfigurationInjectionConstructor(BeanDefinitionVisitor visitor, - MethodElement constructor) { + private void processConfigurationInjectionPoint(BeanDefinitionVisitor visitor, + MethodElement constructor) { for (ParameterElement parameter : constructor.getParameters()) { if (CONSTRUCTOR_PARAMETERS_INJECTION_ANN.stream().noneMatch(parameter::hasStereotype)) { processConfigurationConstructorParameter(parameter); @@ -106,7 +109,7 @@ private void processConfigurationInjectionConstructor(BeanDefinitionVisitor visi } private void processConfigurationConstructorParameter(ParameterElement parameter) { - if (!parameter.hasStereotype(AnnotationUtil.SCOPE)) { + if (isPropertyParameter(parameter)) { final PropertyMetadata pm = metadataBuilder.visitProperty( parameter.getMethodElement().getOwningType(), parameter.getMethodElement().getDeclaringType(), @@ -118,6 +121,16 @@ private void processConfigurationConstructorParameter(ParameterElement parameter } } + private boolean isPropertyParameter(ParameterElement parameter) { + ClassElement parameterType = parameter.getGenericType(); + if (parameterType.isOptional() || parameterType.isAssignable(BeanProvider.class) || parameterType.isAssignable(Provider.class)) { + parameterType = parameterType.getFirstTypeArgument().orElse(parameterType); + // Get the class with type annotations + parameterType = visitorContext.getClassElement(parameterType.getCanonicalName()).orElse(parameterType); + } + return !parameterType.hasStereotype(AnnotationUtil.SCOPE) && !parameterType.hasStereotype(Bean.class); + } + public static boolean isConfigurationProperties(ClassElement classElement) { return classElement.hasStereotype(ConfigurationReader.class); } diff --git a/inject-java/src/test/groovy/io/micronaut/inject/configproperties/ConfigurationPropertiesInjectSpec.groovy b/inject-java/src/test/groovy/io/micronaut/inject/configproperties/ConfigurationPropertiesInjectSpec.groovy new file mode 100644 index 00000000000..4caae5a1291 --- /dev/null +++ b/inject-java/src/test/groovy/io/micronaut/inject/configproperties/ConfigurationPropertiesInjectSpec.groovy @@ -0,0 +1,49 @@ +package io.micronaut.inject.configproperties + +import io.micronaut.annotation.processing.test.AbstractTypeElementSpec +import io.micronaut.context.ApplicationContext + +class ConfigurationPropertiesInjectSpec extends AbstractTypeElementSpec { + + void "test @ConfigurationInject constructor with beans and other configs"() { + given: + when: + def context = ApplicationContext.run(['spec': getClass().getSimpleName(), 'foo.bar.host': 'test', 'foo.bar.server-port': '123', 'xyz.name': "33"]) + def config = context.getBean(MyConfigWithConstructorConfigurationInject) + + then: + config.host == 'test' + config.serverPort == 123 + config.otherBean + config.otherConfig.name == "33" + config.otherMissingConfig + config.otherSingleton + config.optionalOtherSingleton.get() + config.otherSingletonBeanProvider.get() == config.otherSingleton + config.otherSingletonProvider.get() == config.otherSingleton + + cleanup: + context.close() + } + + void "test @ConfigurationInject method with beans and other configs"() { + given: + when: + def context = ApplicationContext.run(['spec': getClass().getSimpleName(), 'foo.bar.host': 'test', 'foo.bar.server-port': '123', 'xyz.name': "33"]) + def config = context.getBean(MyConfigWithMethodConfigurationInject) + + then: + config.host == 'test' + config.serverPort == 123 + config.otherBean + config.otherConfig.name == "33" + config.otherMissingConfig + config.otherSingleton + config.optionalOtherSingleton.get() + config.otherSingletonBeanProvider.get() == config.otherSingleton + config.otherSingletonProvider.get() == config.otherSingleton + + cleanup: + context.close() + } +} diff --git a/inject-java/src/test/groovy/io/micronaut/inject/configproperties/MyConfigWithConstructorConfigurationInject.java b/inject-java/src/test/groovy/io/micronaut/inject/configproperties/MyConfigWithConstructorConfigurationInject.java new file mode 100644 index 00000000000..1e20c39c446 --- /dev/null +++ b/inject-java/src/test/groovy/io/micronaut/inject/configproperties/MyConfigWithConstructorConfigurationInject.java @@ -0,0 +1,118 @@ +package io.micronaut.inject.configproperties; + +import io.micronaut.context.BeanProvider; +import io.micronaut.context.annotation.Bean; +import io.micronaut.context.annotation.ConfigurationInject; +import io.micronaut.context.annotation.ConfigurationProperties; +import io.micronaut.context.annotation.Requires; +import jakarta.inject.Provider; +import jakarta.inject.Singleton; + +import java.util.Optional; + +@Requires(property = "spec", value = "ConfigurationPropertiesInjectSpec") +@ConfigurationProperties("foo.bar") +class MyConfigWithConstructorConfigurationInject { + private String host; + private int serverPort; + private MI_OtherConfig otherConfig; + private MI_OtherMissingConfig otherMissingConfig; + private MI_OtherBean otherBean; + private MI_OtherSingleton otherSingleton; + private Optional optionalOtherSingleton; + private BeanProvider otherSingletonBeanProvider; + private Provider otherSingletonProvider; + + @ConfigurationInject + MyConfigWithConstructorConfigurationInject(String host, + int serverPort, + MI_OtherConfig otherConfig, + MI_OtherMissingConfig otherMissingConfig, + MI_OtherBean otherBean, + MI_OtherSingleton otherSingleton, + Optional optionalOtherSingleton, + BeanProvider otherSingletonBeanProvider, + Provider otherSingletonProvider) { + this.host = host; + this.serverPort = serverPort; + this.otherConfig = otherConfig; + this.otherMissingConfig = otherMissingConfig; + this.otherBean = otherBean; + this.otherSingleton = otherSingleton; + this.optionalOtherSingleton = optionalOtherSingleton; + this.otherSingletonBeanProvider = otherSingletonBeanProvider; + this.otherSingletonProvider = otherSingletonProvider; + } + + public String getHost() { + return host; + } + + public int getServerPort() { + return serverPort; + } + + public MI_OtherBean getOtherBean() { + return otherBean; + } + + public MI_OtherConfig getOtherConfig() { + return otherConfig; + } + + public MI_OtherMissingConfig getOtherMissingConfig() { + return otherMissingConfig; + } + + public MI_OtherSingleton getOtherSingleton() { + return otherSingleton; + } + + public Optional getOptionalOtherSingleton() { + return optionalOtherSingleton; + } + + public BeanProvider getOtherSingletonBeanProvider() { + return otherSingletonBeanProvider; + } + + public Provider getOtherSingletonProvider() { + return otherSingletonProvider; + } +} + +@ConfigurationProperties("xyz") +class CI_OtherConfig { + + String name; + + public void setName(String name) { + this.name = name; + } + + public String getName() { + return name; + } +} + +@ConfigurationProperties("abc") +class CI_OtherMissingConfig { + + String name; + + public void setName(String name) { + this.name = name; + } + + public String getName() { + return name; + } +} + +@Bean +class CI_OtherBean { +} + +@Singleton +class CI_OtherSingleton { +} diff --git a/inject-java/src/test/groovy/io/micronaut/inject/configproperties/MyConfigWithMethodConfigurationInject.java b/inject-java/src/test/groovy/io/micronaut/inject/configproperties/MyConfigWithMethodConfigurationInject.java new file mode 100644 index 00000000000..8d7928e24be --- /dev/null +++ b/inject-java/src/test/groovy/io/micronaut/inject/configproperties/MyConfigWithMethodConfigurationInject.java @@ -0,0 +1,118 @@ +package io.micronaut.inject.configproperties; + +import io.micronaut.context.BeanProvider; +import io.micronaut.context.annotation.Bean; +import io.micronaut.context.annotation.ConfigurationInject; +import io.micronaut.context.annotation.ConfigurationProperties; +import io.micronaut.context.annotation.Requires; +import jakarta.inject.Provider; +import jakarta.inject.Singleton; + +import java.util.Optional; + +@Requires(property = "spec", value = "ConfigurationPropertiesInjectSpec") +@ConfigurationProperties("foo.bar") +class MyConfigWithMethodConfigurationInject { + private String host; + private int serverPort; + private MI_OtherConfig otherConfig; + private MI_OtherMissingConfig otherMissingConfig; + private MI_OtherBean otherBean; + private MI_OtherSingleton otherSingleton; + private Optional optionalOtherSingleton; + private BeanProvider otherSingletonBeanProvider; + private Provider otherSingletonProvider; + + @ConfigurationInject + void inject(String host, + int serverPort, + MI_OtherConfig otherConfig, + MI_OtherMissingConfig otherMissingConfig, + MI_OtherBean otherBean, + MI_OtherSingleton otherSingleton, + Optional optionalOtherSingleton, + BeanProvider otherSingletonBeanProvider, + Provider otherSingletonProvider) { + this.host = host; + this.serverPort = serverPort; + this.otherConfig = otherConfig; + this.otherMissingConfig = otherMissingConfig; + this.otherBean = otherBean; + this.otherSingleton = otherSingleton; + this.optionalOtherSingleton = optionalOtherSingleton; + this.otherSingletonBeanProvider = otherSingletonBeanProvider; + this.otherSingletonProvider = otherSingletonProvider; + } + + public String getHost() { + return host; + } + + public int getServerPort() { + return serverPort; + } + + public MI_OtherBean getOtherBean() { + return otherBean; + } + + public MI_OtherConfig getOtherConfig() { + return otherConfig; + } + + public MI_OtherMissingConfig getOtherMissingConfig() { + return otherMissingConfig; + } + + public MI_OtherSingleton getOtherSingleton() { + return otherSingleton; + } + + public Optional getOptionalOtherSingleton() { + return optionalOtherSingleton; + } + + public BeanProvider getOtherSingletonBeanProvider() { + return otherSingletonBeanProvider; + } + + public Provider getOtherSingletonProvider() { + return otherSingletonProvider; + } +} + +@ConfigurationProperties("xyz") +class MI_OtherConfig { + + String name; + + public void setName(String name) { + this.name = name; + } + + public String getName() { + return name; + } +} + +@ConfigurationProperties("abc") +class MI_OtherMissingConfig { + + String name; + + public void setName(String name) { + this.name = name; + } + + public String getName() { + return name; + } +} + +@Bean +class MI_OtherBean { +} + +@Singleton +class MI_OtherSingleton { +} From 64b53dd4a88ab0a94a5123088b214b3dbc9e0527 Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Wed, 26 Oct 2022 19:09:14 +0200 Subject: [PATCH 156/743] build: netty to 4.1.84.Final (#8233) see http://netty.io/news/2022/10/11/4-1-84-Final.html * fix: use https comment to avoid checkstyle error * fix: trimLeadingWhiteSpace to avoid DefaultHttpHeaders.HeaderValueValidator::validate failure after updating to Netty 4.1.84.Final because the value " Bar" was being passed. --- gradle/libs.versions.toml | 2 +- .../io/micronaut/http/netty/AbstractNettyHttpRequest.java | 2 +- .../main/java/io/micronaut/http/server/cors/CorsFilter.java | 5 ++++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 1dd5992e65c..8967bc03cdb 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -126,7 +126,7 @@ managed-micronaut-views = "3.6.0" managed-micronaut-xml = "3.1.0" managed-neo4j = "3.5.35" managed-neo4j-java-driver = "4.4.9" -managed-netty = "4.1.82.Final" +managed-netty = "4.1.84.Final" managed-reactive-pg-client = "0.11.4" managed-reactive-streams = "1.0.4" # This should be kept aligned with https://github.com/micronaut-projects/micronaut-reactor/blob/master/gradle.properties from the BOM diff --git a/http-netty/src/main/java/io/micronaut/http/netty/AbstractNettyHttpRequest.java b/http-netty/src/main/java/io/micronaut/http/netty/AbstractNettyHttpRequest.java index 3dc9935e97f..5015f1f0677 100644 --- a/http-netty/src/main/java/io/micronaut/http/netty/AbstractNettyHttpRequest.java +++ b/http-netty/src/main/java/io/micronaut/http/netty/AbstractNettyHttpRequest.java @@ -75,7 +75,7 @@ public AbstractNettyHttpRequest(io.netty.handler.codec.http.HttpRequest nettyReq this.conversionService = conversionService; URI fullUri = URI.create(nettyRequest.uri()); if (fullUri.getAuthority() != null || fullUri.getScheme() != null) { - // http://example.com/foo -> /foo + // https://example.com/foo -> /foo try { fullUri = new URI( null, // scheme diff --git a/http-server/src/main/java/io/micronaut/http/server/cors/CorsFilter.java b/http-server/src/main/java/io/micronaut/http/server/cors/CorsFilter.java index a33ddc51ec4..705b5c55fea 100644 --- a/http-server/src/main/java/io/micronaut/http/server/cors/CorsFilter.java +++ b/http-server/src/main/java/io/micronaut/http/server/cors/CorsFilter.java @@ -237,7 +237,10 @@ protected void setAllowHeaders(List optionalAllowHeaders, MutableHttpResponse response.header(ACCESS_CONTROL_ALLOW_HEADERS, headerValue); } } else { - allowHeaders.forEach(header -> response.header(ACCESS_CONTROL_ALLOW_HEADERS, header)); + allowHeaders + .stream() + .map(StringUtils::trimLeadingWhitespace) + .forEach(header -> response.header(ACCESS_CONTROL_ALLOW_HEADERS, header)); } } From 28eedbef99aca7e30a3f27e97719765efcac7cac Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 26 Oct 2022 19:10:17 +0200 Subject: [PATCH 157/743] fix(deps): update managed-swagger to v2.2.4 (#8209) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 5df1ccad2c7..82d5ec3e264 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -142,7 +142,7 @@ managed-spock = "2.2-groovy-4.0" managed-spotbugs = "4.7.1" managed-spring = "5.3.23" managed-springboot = "2.7.0" -managed-swagger = "2.2.3" +managed-swagger = "2.2.4" managed-validation = "2.0.1.Final" managed-testcontainers = "1.17.5" managed-snakeyaml = "1.32" From 48cb8bd772864a21f1de7492d3d580707e18dcbc Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 26 Oct 2022 19:10:39 +0200 Subject: [PATCH 158/743] fix(deps): update groovy monorepo to v4.0.6 (#8206) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 82d5ec3e264..0cb995381dc 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -52,7 +52,7 @@ managed-gorm-hibernate = "7.3.0" managed-graal-sdk = "22.0.0.2" managed-graal = "22.2.0" managed-graal-svm = "22.0.0.2" -managed-groovy = "4.0.5" +managed-groovy = "4.0.6" managed-h2 = "2.1.210" managed-hystrix = "1.5.18" managed-jakarta-annotation-api = "2.1.1" From 6abb25929047a6810370a620cae6c12bd6fdf5f0 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 26 Oct 2022 19:10:52 +0200 Subject: [PATCH 159/743] chore(deps): update dependency org.tomlj:tomlj to v1.1.0 (#8211) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- buildSrc/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/buildSrc/build.gradle b/buildSrc/build.gradle index d9eac6b8e19..06bb5a00382 100644 --- a/buildSrc/build.gradle +++ b/buildSrc/build.gradle @@ -10,6 +10,6 @@ dependencies { implementation "gradle.plugin.com.github.johnrengelman:shadow:7.1.2" implementation "org.aim42:htmlSanityCheck:1.1.6" implementation "io.micronaut.build.internal:micronaut-gradle-plugins:5.3.15" - implementation "org.tomlj:tomlj:1.0.0" + implementation "org.tomlj:tomlj:1.1.0" implementation "me.champeau.gradle:japicmp-gradle-plugin:0.4.1" } From 6cc990261d5fa01faf2544e87bc4b20e8bb5bc22 Mon Sep 17 00:00:00 2001 From: Jonas Konrad Date: Thu, 27 Oct 2022 07:05:13 +0200 Subject: [PATCH 160/743] feat: deprecate HttpStatus.UNORDERED_COLLECTION (#8199) see https://github.com/micronaut-projects/micronaut-core/pull/8123 * add TOO_EARLY --- http/src/main/java/io/micronaut/http/HttpStatus.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/http/src/main/java/io/micronaut/http/HttpStatus.java b/http/src/main/java/io/micronaut/http/HttpStatus.java index 0ee515fec0a..01b3806f649 100644 --- a/http/src/main/java/io/micronaut/http/HttpStatus.java +++ b/http/src/main/java/io/micronaut/http/HttpStatus.java @@ -69,7 +69,12 @@ public enum HttpStatus implements CharSequence { UNPROCESSABLE_ENTITY(422, "Unprocessable Entity"), LOCKED(423, "Locked"), FAILED_DEPENDENCY(424, "Failed Dependency"), + /** + * @deprecated Will be replaced by {@link #TOO_EARLY} in 4.0 + */ + @Deprecated UNORDERED_COLLECTION(425, "Unordered Collection"), + TOO_EARLY(425, "Too Early"), UPGRADE_REQUIRED(426, "Upgrade Required"), PRECONDITION_REQUIRED(428, "Precondition Required"), TOO_MANY_REQUESTS(429, "Too Many Requests"), From 693240c2e85b16279ddd5d709e3bb4b164f15238 Mon Sep 17 00:00:00 2001 From: Tim Yates Date: Thu, 27 Oct 2022 12:24:14 +0100 Subject: [PATCH 161/743] We require asm as an api dependency so it's pulled in transitively downstream (#8243) Without this, when we add mn.micronaut.core.processor to downstream modules, we get a NoClassDefFound exception on asm Opcodes --- core-processor/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core-processor/build.gradle b/core-processor/build.gradle index 8d276ef05a1..92a41fbfa4f 100644 --- a/core-processor/build.gradle +++ b/core-processor/build.gradle @@ -5,8 +5,8 @@ plugins { dependencies { api project(":inject") api project(":aop") - implementation libs.asm.tree - implementation libs.bundles.asm + api libs.asm.tree + api libs.bundles.asm compileOnly libs.kotlin.stdlib.jdk8 compileOnly libs.managed.validation From 128f78014fb387bb27e738e2a008c15126daf946 Mon Sep 17 00:00:00 2001 From: Tim Yates Date: Thu, 27 Oct 2022 15:04:47 +0100 Subject: [PATCH 162/743] This class has moved (#8244) So currently, :validateAssembleDocs is failing when publishing a SNAPSHOT --- src/main/docs/guide/ioc/annotationMetadata.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/docs/guide/ioc/annotationMetadata.adoc b/src/main/docs/guide/ioc/annotationMetadata.adoc index 8985ce2ee49..3b6ab175922 100644 --- a/src/main/docs/guide/ioc/annotationMetadata.adoc +++ b/src/main/docs/guide/ioc/annotationMetadata.adoc @@ -100,7 +100,7 @@ The following is an example `AnnotationMapper` that improves the introspection c .EntityIntrospectedAnnotationMapper Mapper Example [source,java] ---- -include::inject/src/main/java/io/micronaut/inject/beans/visitor/EntityIntrospectedAnnotationMapper.java[indent=0, tag="class"] +include::core-processor/src/main/java/io/micronaut/inject/beans/visitor/EntityIntrospectedAnnotationMapper.java[indent=0, tag="class"] ---- <1> The `map` method receives a api:io.micronaut.core.annotation.AnnotationValue[] with the values for the annotation. From 5ed27eff418ab6cc23287d232ef97f8c1d300626 Mon Sep 17 00:00:00 2001 From: Dennis Winter Date: Thu, 27 Oct 2022 17:40:04 +0200 Subject: [PATCH 163/743] doc: recommend not to use private fields with @Value (#8239) --- src/main/docs/guide/config/valueAnnotation.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/docs/guide/config/valueAnnotation.adoc b/src/main/docs/guide/config/valueAnnotation.adoc index cb6ea436dc9..cbb657506e9 100644 --- a/src/main/docs/guide/config/valueAnnotation.adoc +++ b/src/main/docs/guide/config/valueAnnotation.adoc @@ -6,7 +6,7 @@ Consider the following example: snippet::io.micronaut.docs.config.value.EngineImpl[tags="imports,class",indent=0,title="@Value Example"] -<1> The `@Value` annotation accepts a string that can have embedded placeholder values (the default value can be provided by specifying a value after the colon `:` character). +<1> The `@Value` annotation accepts a string that can have embedded placeholder values (the default value can be provided by specifying a value after the colon `:` character). Also try to avoid setting the member visibility to `private`, since this requires Micronaut Framework to use reflection. Prefer to use `protected`. <2> The injected value can then be used within code. Note that `@Value` can also be used to inject a static value. For example the following injects the number 10: From d3db0d03654dc1d6e50edcdef872a8e5b7ba1bc8 Mon Sep 17 00:00:00 2001 From: micronaut-build <65172877+micronaut-build@users.noreply.github.com> Date: Thu, 27 Oct 2022 12:39:17 -0400 Subject: [PATCH 164/743] build: bump micronaut-security to 3.7.2 (#8245) --- gradle.properties | 2 +- gradle/libs.versions.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle.properties b/gradle.properties index 232865ccc6e..642d05f6640 100644 --- a/gradle.properties +++ b/gradle.properties @@ -52,7 +52,7 @@ kotlin.stdlib.default.dependency=false # For the docs graalVersion=22.0.0.2 -micronautSecurityVersion=3.8.0 +micronautSecurityVersion=3.7.2 org.gradle.caching=true org.gradle.parallel=true diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 8967bc03cdb..53634620734 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -112,7 +112,7 @@ managed-micronaut-rss = "3.2.0" managed-micronaut-rxjava1 = "1.0.0" managed-micronaut-rxjava2 = "1.3.0" managed-micronaut-rxjava3 = "2.3.0" -managed-micronaut-security = "3.8.0" +managed-micronaut-security = "3.7.2" managed-micronaut-serialization = "1.3.2" managed-micronaut-servlet = "3.3.1" managed-micronaut-spring = "4.3.1" From c1fcb7188decbcb98451cef3a8ae0e81a085524f Mon Sep 17 00:00:00 2001 From: Graeme Rocher Date: Thu, 27 Oct 2022 21:47:39 +0200 Subject: [PATCH 165/743] Don't relocate ASM references (#8246) --- .../groovy/io.micronaut.build.internal.convention-library.gradle | 1 - 1 file changed, 1 deletion(-) diff --git a/buildSrc/src/main/groovy/io.micronaut.build.internal.convention-library.gradle b/buildSrc/src/main/groovy/io.micronaut.build.internal.convention-library.gradle index bab8bdd7022..450ca57fa2f 100644 --- a/buildSrc/src/main/groovy/io.micronaut.build.internal.convention-library.gradle +++ b/buildSrc/src/main/groovy/io.micronaut.build.internal.convention-library.gradle @@ -18,7 +18,6 @@ configurations { tasks.named("shadowJar") { configurations = [project.configurations.shadowCompile] - relocate "org.objectweb.asm", "io.micronaut.asm" } micronautBuild { From 8c0fc93e061d1f33da0082f79717082cd9094a8d Mon Sep 17 00:00:00 2001 From: micronaut-build <65172877+micronaut-build@users.noreply.github.com> Date: Fri, 28 Oct 2022 03:04:24 -0400 Subject: [PATCH 166/743] Bump micronaut-security to 3.8.1 (#8248) --- gradle.properties | 2 +- gradle/libs.versions.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle.properties b/gradle.properties index 642d05f6640..5e6329a4f34 100644 --- a/gradle.properties +++ b/gradle.properties @@ -52,7 +52,7 @@ kotlin.stdlib.default.dependency=false # For the docs graalVersion=22.0.0.2 -micronautSecurityVersion=3.7.2 +micronautSecurityVersion=3.8.1 org.gradle.caching=true org.gradle.parallel=true diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 53634620734..9214789fd4e 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -112,7 +112,7 @@ managed-micronaut-rss = "3.2.0" managed-micronaut-rxjava1 = "1.0.0" managed-micronaut-rxjava2 = "1.3.0" managed-micronaut-rxjava3 = "2.3.0" -managed-micronaut-security = "3.7.2" +managed-micronaut-security = "3.8.1" managed-micronaut-serialization = "1.3.2" managed-micronaut-servlet = "3.3.1" managed-micronaut-spring = "4.3.1" From e2e21990aa082cc2745de2d8ae502ad4e73d7ade Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Fri, 28 Oct 2022 09:19:12 +0200 Subject: [PATCH 167/743] Bump micronaut-servlet to 3.3.2 (#8249) --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 9214789fd4e..83f021efc12 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -114,7 +114,7 @@ managed-micronaut-rxjava2 = "1.3.0" managed-micronaut-rxjava3 = "2.3.0" managed-micronaut-security = "3.8.1" managed-micronaut-serialization = "1.3.2" -managed-micronaut-servlet = "3.3.1" +managed-micronaut-servlet = "3.3.2" managed-micronaut-spring = "4.3.1" managed-micronaut-sql = "4.7.2" managed-micronaut-test = "3.6.2" From 8b2d53ba6b6d93bf4279be9d7431ddfb9871c233 Mon Sep 17 00:00:00 2001 From: Denis Stepanov Date: Fri, 28 Oct 2022 16:03:13 +0700 Subject: [PATCH 168/743] Refactor `ConversionService` to remove mutable operations from `ConversionService.SHARED` (#8156) Co-authored-by: Graeme Rocher --- .../io/micronaut/aop/InterceptedMethod.java | 14 ++- .../CompletionStageInterceptedMethod.java | 6 +- .../intercepted/InterceptedMethodUtil.java | 12 +- .../PublisherInterceptedMethod.java | 6 +- .../buffer/netty/NettyByteBufferFactory.java | 4 +- .../time/TimeConverterRegistrar.java | 8 +- .../processor/ScheduledMethodProcessor.java | 4 +- .../ReactiveTypeConverterRegistrar.java | 12 +- ...ronaut.core.convert.TypeConverterRegistrar | 1 + .../AbstractAnnotatedArgumentBinder.java | 4 +- .../core/convert/ConversionService.java | 33 +----- .../core/convert/ConversionServiceAware.java | 35 ++++++ .../convert/ConversionServiceProvider.java | 24 +++- ...a => DefaultMutableConversionService.java} | 68 ++++------- .../convert/MutableConversionService.java | 63 ++++++++++ .../core/convert/TypeConverterRegistrar.java | 2 +- .../MultiValuesConverterFactory.java | 20 ++-- .../value/ConvertibleMultiValuesMap.java | 22 +++- .../core/convert/value/ConvertibleValues.java | 38 +++++- .../convert/value/ConvertibleValuesMap.java | 12 +- .../MutableConvertibleMultiValuesMap.java | 2 +- .../value/MutableConvertibleValuesMap.java | 2 +- .../core/serialize/JdkSerializer.java | 4 +- .../core/value/MapPropertyResolver.java | 2 +- .../DefaultConversionServiceSpec.groovy | 8 +- .../value/ConvertibleValuesSpec.groovy | 1 + .../web/AnnotatedFunctionRouteBuilder.java | 2 +- .../executor/StreamFunctionExecutor.java | 6 +- .../bind/DefaultHttpClientBinderRegistry.java | 2 +- ...QueryValueClientArgumentRequestBinder.java | 4 +- .../HttpClientIntroductionAdvice.java | 10 +- .../loadbalance/LoadBalancerConverters.java | 8 +- ...ronaut.core.convert.TypeConverterRegistrar | 1 + .../http/client/netty/DefaultHttpClient.java | 55 ++++++--- .../netty/FullNettyClientHttpResponse.java | 17 +-- .../netty/MutableHttpRequestWrapper.java | 11 +- .../client/netty/NettyClientHttpRequest.java | 10 +- .../netty/NettyStreamedHttpResponse.java | 5 +- .../NettyWebSocketClientHandler.java | 19 +-- .../micronaut/http/client/HttpPostSpec.groovy | 10 ++ .../FullNettyClientHttpResponseSpec.groovy | 6 +- .../NettyStreamedHttpResponseSpec.groovy | 7 +- .../http/netty/AbstractNettyHttpRequest.java | 2 +- .../http/netty/NettyHttpHeaders.java | 11 +- .../http/netty/NettyHttpParameters.java | 15 ++- .../KQueueChannelOptionFactory.java | 4 +- .../http/netty/cookies/NettyCookies.java | 7 +- .../AbstractNettyWebSocketHandler.java | 12 +- http-server-netty/build.gradle | 11 +- .../server/netty/HttpPipelineBuilder.java | 4 +- .../netty/NettyEmbeddedServerInstance.java | 16 ++- .../http/server/netty/NettyHttpRequest.java | 9 ++ .../http/server/netty/NettyHttpServer.java | 3 +- .../server/netty/RoutingInBoundHandler.java | 19 +-- .../netty/binders/NettyBinderRegistrar.java | 16 ++- .../netty/converters/NettyConverters.java | 7 +- .../netty/converters/NettyConvertersSpi.java | 4 +- .../netty/decoders/HttpRequestDecoder.java | 4 +- .../netty/encoders/HttpResponseEncoder.java | 10 +- .../NettyServerWebSocketHandler.java | 3 +- .../netty/ContentNegotiationSpec.groovy | 8 +- .../netty/binding/NettyHttpRequestSpec.groovy | 14 +-- .../binding/NettyHttpResponseSpec.groovy | 10 +- .../converters/ConverterRegistrySpec.groovy | 3 - .../server/netty/util/MockHttpHeaders.java | 8 +- .../websocket/ServerWebSocketProcessor.java | 2 +- .../http/server/util/MockHttpHeaders.java | 6 + .../routes/RouteValidationVisitor.java | 10 +- .../http/MediaTypeConvertersRegistrar.java | 21 ++-- .../io/micronaut/http/MutableHttpHeaders.java | 3 +- .../micronaut/http/MutableHttpParameters.java | 4 +- .../io/micronaut/http/MutableHttpRequest.java | 3 +- .../bind/DefaultRequestBinderRegistry.java | 58 +++++----- .../bind/binders/CookieAnnotationBinder.java | 2 +- .../binders/DefaultBodyAnnotationBinder.java | 2 +- .../bind/binders/HeaderAnnotationBinder.java | 2 +- .../binders/ParameterAnnotationBinder.java | 2 +- .../bind/binders/PartAnnotationBinder.java | 2 +- .../binders/PathVariableAnnotationBinder.java | 4 +- .../binders/QueryValueArgumentBinder.java | 5 +- .../RequestAttributeAnnotationBinder.java | 2 +- .../binders/RequestBeanAnnotationBinder.java | 2 +- .../converters/HttpConverterRegistrar.java | 88 +------------- .../SharedHttpConvertersRegistrar.java | 109 ++++++++++++++++++ .../http/simple/SimpleHttpHeaders.java | 7 +- .../http/simple/SimpleHttpParameters.java | 15 ++- .../http/simple/SimpleHttpRequest.java | 10 +- .../http/simple/cookies/SimpleCookies.java | 10 +- ...ronaut.core.convert.TypeConverterRegistrar | 1 + .../generics/InjectWithWildcardSpec.groovy | 6 +- .../ast/beans/BeanElementVisitorSpec.groovy | 10 +- .../inject/generics/WildCardInject.java | 6 +- .../AbstractInitializableBeanDefinition.java | 2 +- .../AbstractParametrizedBeanDefinition.java | 3 +- .../micronaut/context/ApplicationContext.java | 10 +- .../ApplicationContextConfiguration.java | 11 +- .../io/micronaut/context/BeanContext.java | 4 +- .../context/DefaultApplicationContext.java | 50 ++++---- .../DefaultApplicationContextBuilder.java | 24 ++-- .../micronaut/context/DefaultBeanContext.java | 31 +++-- .../converters/ContextConverterRegistrar.java | 4 +- .../context/env/DefaultEnvironment.java | 23 ++-- .../DefaultPropertyPlaceholderResolver.java | 2 +- .../io/micronaut/context/env/Environment.java | 4 +- .../env/PropertyExpressionResolver.java | 2 +- .../env/PropertySourcePropertyResolver.java | 4 +- .../AnnotationConvertersRegistrar.java | 4 +- .../PropertySourcePropertyResolverSpec.groovy | 4 +- .../convert/JacksonConverterRegistrar.java | 7 +- .../convert/ObjectNodeConvertibleValues.java | 4 +- .../json/convert/JsonConverterRegistrar.java | 7 +- .../convert/JsonNodeConvertibleValues.java | 9 +- .../AbstractEndpointRouteBuilder.java | 2 +- .../DeleteEndpointRouteBuilder.java | 2 +- .../processors/ReadEndpointRouteBuilder.java | 2 +- .../processors/WriteEndpointRouteBuilder.java | 2 +- .../web/router/AbstractRouteMatch.java | 8 +- .../router/AnnotatedFilterRouteBuilder.java | 2 +- .../router/AnnotatedMethodRouteBuilder.java | 2 +- .../web/router/DefaultRouteBuilder.java | 91 ++++++++++----- .../web/router/DefaultUriRouteMatch.java | 2 +- .../micronaut/web/router/ErrorRouteMatch.java | 2 +- .../web/router/StatusRouteMatch.java | 2 +- .../router/EachBeanRouteBuilderSpec.groovy | 2 +- .../converters/FlowConverterRegistrar.java | 4 +- .../runtime/http/codec/TextPlainCodec.java | 20 ++-- .../ConversionServiceIsResetSpec.groovy | 3 +- .../time/TimeConverterRegistrarSpec.groovy | 6 +- .../http/codec/TextPlainCodecSpec.groovy | 3 +- src/main/docs/guide/appendix/breaks.adoc | 5 + .../guide/config/customTypeConverter.adoc | 2 + src/main/docs/guide/httpServer/binding.adoc | 2 +- .../converters/MapToLocalDateConverter.groovy | 8 +- .../ShoppingCartRequestArgumentBinder.groovy | 4 +- .../converters/MapToLocalDateConverter.kt | 3 +- .../ShoppingCartRequestArgumentBinder.kt | 2 +- .../converters/MapToLocalDateConverter.java | 5 +- .../ShoppingCartRequestArgumentBinder.java | 4 +- .../bind/WebSocketStateBinderRegistry.java | 5 +- 139 files changed, 976 insertions(+), 600 deletions(-) rename {context/src/main/java/io/micronaut/runtime/converters/reactive => core-reactive/src/main/java/io/micronaut/core/async/converters}/ReactiveTypeConverterRegistrar.java (80%) create mode 100644 core-reactive/src/main/resources/META-INF/services/io.micronaut.core.convert.TypeConverterRegistrar create mode 100644 core/src/main/java/io/micronaut/core/convert/ConversionServiceAware.java rename context/src/main/java/io/micronaut/runtime/converters/reactive/package-info.java => core/src/main/java/io/micronaut/core/convert/ConversionServiceProvider.java (55%) rename core/src/main/java/io/micronaut/core/convert/{DefaultConversionService.java => DefaultMutableConversionService.java} (95%) create mode 100644 core/src/main/java/io/micronaut/core/convert/MutableConversionService.java create mode 100644 http-client-core/src/main/resources/META-INF/services/io.micronaut.core.convert.TypeConverterRegistrar create mode 100644 http/src/main/java/io/micronaut/http/converters/SharedHttpConvertersRegistrar.java diff --git a/aop/src/main/java/io/micronaut/aop/InterceptedMethod.java b/aop/src/main/java/io/micronaut/aop/InterceptedMethod.java index 5c4694f5652..877b4fa9702 100644 --- a/aop/src/main/java/io/micronaut/aop/InterceptedMethod.java +++ b/aop/src/main/java/io/micronaut/aop/InterceptedMethod.java @@ -18,6 +18,7 @@ import io.micronaut.aop.internal.intercepted.InterceptedMethodUtil; import io.micronaut.context.exceptions.ConfigurationException; import io.micronaut.core.annotation.Experimental; +import io.micronaut.core.convert.ConversionService; import io.micronaut.core.type.Argument; import org.reactivestreams.Publisher; @@ -40,7 +41,18 @@ public interface InterceptedMethod { * @return The {@link InterceptedMethod} */ static InterceptedMethod of(MethodInvocationContext context) { - return InterceptedMethodUtil.of(context); + return of(context, ConversionService.SHARED); + } + + /** + * Creates a new instance of intercept method supporting intercepting different reactive invocations. + * + * @param context The {@link MethodInvocationContext} + * @param conversionService The conversion service + * @return The {@link InterceptedMethod} + */ + static InterceptedMethod of(MethodInvocationContext context, ConversionService conversionService) { + return InterceptedMethodUtil.of(context, conversionService); } /** diff --git a/aop/src/main/java/io/micronaut/aop/internal/intercepted/CompletionStageInterceptedMethod.java b/aop/src/main/java/io/micronaut/aop/internal/intercepted/CompletionStageInterceptedMethod.java index 5a2ff647722..3c89ff79f0c 100644 --- a/aop/src/main/java/io/micronaut/aop/internal/intercepted/CompletionStageInterceptedMethod.java +++ b/aop/src/main/java/io/micronaut/aop/internal/intercepted/CompletionStageInterceptedMethod.java @@ -37,13 +37,13 @@ @Internal @Experimental class CompletionStageInterceptedMethod implements InterceptedMethod { - private final ConversionService conversionService = ConversionService.SHARED; - private final MethodInvocationContext context; + private final ConversionService conversionService; private final Argument returnTypeValue; - CompletionStageInterceptedMethod(MethodInvocationContext context) { + CompletionStageInterceptedMethod(MethodInvocationContext context, ConversionService conversionService) { this.context = context; + this.conversionService = conversionService; this.returnTypeValue = context.getReturnType().asArgument().getFirstTypeVariable().orElse(Argument.OBJECT_ARGUMENT); } diff --git a/aop/src/main/java/io/micronaut/aop/internal/intercepted/InterceptedMethodUtil.java b/aop/src/main/java/io/micronaut/aop/internal/intercepted/InterceptedMethodUtil.java index 7b8c6e42d49..43f6b224baf 100644 --- a/aop/src/main/java/io/micronaut/aop/internal/intercepted/InterceptedMethodUtil.java +++ b/aop/src/main/java/io/micronaut/aop/internal/intercepted/InterceptedMethodUtil.java @@ -26,6 +26,7 @@ import io.micronaut.core.annotation.Internal; import io.micronaut.core.annotation.NonNull; import io.micronaut.core.annotation.Nullable; +import io.micronaut.core.convert.ConversionService; import io.micronaut.core.type.ReturnType; import java.util.List; @@ -48,10 +49,13 @@ private InterceptedMethodUtil() { /** * Find possible {@link InterceptedMethod} implementation. * - * @param context The {@link MethodInvocationContext} + * @param context The {@link MethodInvocationContext} + * @param conversionService The {@link ConversionService} * @return The {@link InterceptedMethod} + * @since 4.0.0 */ - public static InterceptedMethod of(MethodInvocationContext context) { + @NonNull + public static InterceptedMethod of(@NonNull MethodInvocationContext context, @NonNull ConversionService conversionService) { if (context.isSuspend()) { KotlinInterceptedMethod kotlinInterceptedMethod = KotlinInterceptedMethod.of(context); if (kotlinInterceptedMethod != null) { @@ -65,9 +69,9 @@ public static InterceptedMethod of(MethodInvocationContext context) { // Micro Optimization return new SynchronousInterceptedMethod(context); } else if (CompletionStage.class.isAssignableFrom(returnTypeClass) || Future.class.isAssignableFrom(returnTypeClass)) { - return new CompletionStageInterceptedMethod(context); + return new CompletionStageInterceptedMethod(context, conversionService); } else if (PublisherInterceptedMethod.isConvertibleToPublisher(returnTypeClass)) { - return new PublisherInterceptedMethod(context); + return new PublisherInterceptedMethod(context, conversionService); } else { return new SynchronousInterceptedMethod(context); } diff --git a/aop/src/main/java/io/micronaut/aop/internal/intercepted/PublisherInterceptedMethod.java b/aop/src/main/java/io/micronaut/aop/internal/intercepted/PublisherInterceptedMethod.java index f2629bf11f1..828580781f3 100644 --- a/aop/src/main/java/io/micronaut/aop/internal/intercepted/PublisherInterceptedMethod.java +++ b/aop/src/main/java/io/micronaut/aop/internal/intercepted/PublisherInterceptedMethod.java @@ -40,13 +40,13 @@ @Experimental class PublisherInterceptedMethod implements InterceptedMethod { private static final boolean AVAILABLE = ClassUtils.isPresent("io.micronaut.core.async.publisher.Publishers", PublisherInterceptedMethod.class.getClassLoader()); - private final ConversionService conversionService = ConversionService.SHARED; - private final MethodInvocationContext context; + private final ConversionService conversionService; private final Argument returnTypeValue; - PublisherInterceptedMethod(MethodInvocationContext context) { + PublisherInterceptedMethod(MethodInvocationContext context, ConversionService conversionService) { this.context = context; + this.conversionService = conversionService; this.returnTypeValue = context.getReturnType().asArgument().getFirstTypeVariable().orElse(Argument.OBJECT_ARGUMENT); } diff --git a/buffer-netty/src/main/java/io/micronaut/buffer/netty/NettyByteBufferFactory.java b/buffer-netty/src/main/java/io/micronaut/buffer/netty/NettyByteBufferFactory.java index a4fc25a6624..388a01b3247 100644 --- a/buffer-netty/src/main/java/io/micronaut/buffer/netty/NettyByteBufferFactory.java +++ b/buffer-netty/src/main/java/io/micronaut/buffer/netty/NettyByteBufferFactory.java @@ -17,7 +17,7 @@ import io.micronaut.context.annotation.BootstrapContextCompatible; import io.micronaut.core.annotation.Internal; -import io.micronaut.core.convert.ConversionService; +import io.micronaut.core.convert.MutableConversionService; import io.micronaut.core.io.buffer.ByteBuffer; import io.micronaut.core.io.buffer.ByteBufferFactory; import io.netty.buffer.ByteBuf; @@ -59,7 +59,7 @@ public NettyByteBufferFactory(ByteBufAllocator allocator) { } @PostConstruct - final void register(ConversionService conversionService) { + final void register(MutableConversionService conversionService) { conversionService.addConverter(ByteBuf.class, ByteBuffer.class, DEFAULT::wrap); conversionService.addConverter(ByteBuffer.class, ByteBuf.class, byteBuffer -> { if (byteBuffer instanceof NettyByteBuffer) { diff --git a/context/src/main/java/io/micronaut/runtime/converters/time/TimeConverterRegistrar.java b/context/src/main/java/io/micronaut/runtime/converters/time/TimeConverterRegistrar.java index 3fcb8c5e28a..58bd8b6fa69 100644 --- a/context/src/main/java/io/micronaut/runtime/converters/time/TimeConverterRegistrar.java +++ b/context/src/main/java/io/micronaut/runtime/converters/time/TimeConverterRegistrar.java @@ -18,7 +18,7 @@ import io.micronaut.core.annotation.Internal; import io.micronaut.core.annotation.TypeHint; import io.micronaut.core.convert.ConversionContext; -import io.micronaut.core.convert.ConversionService; +import io.micronaut.core.convert.MutableConversionService; import io.micronaut.core.convert.TypeConverter; import io.micronaut.core.convert.TypeConverterRegistrar; import io.micronaut.core.convert.format.Format; @@ -81,7 +81,7 @@ public class TimeConverterRegistrar implements TypeConverterRegistrar { private static final int MILLIS = 3; @Override - public void register(ConversionService conversionService) { + public void register(MutableConversionService conversionService) { final BiFunction> durationConverter = (object, context) -> { String value = object.toString().trim(); if (value.startsWith("P")) { @@ -179,7 +179,7 @@ public void register(ConversionService conversionService) { addTemporalToDateConverter(conversionService, LocalDateTime.class, ldt -> ldt.toInstant(ZoneOffset.UTC)); } - private void addTemporalStringConverter(ConversionService conversionService, Class temporalType, DateTimeFormatter isoFormatter, TemporalQuery query) { + private void addTemporalStringConverter(MutableConversionService conversionService, Class temporalType, DateTimeFormatter isoFormatter, TemporalQuery query) { conversionService.addConverter(CharSequence.class, temporalType, (CharSequence object, Class targetType, ConversionContext context) -> { if (StringUtils.isEmpty(object)) { return Optional.empty(); @@ -213,7 +213,7 @@ private void addTemporalStringConverter(ConversionS }); } - private void addTemporalToDateConverter(ConversionService conversionService, Class temporalType, Function toInstant) { + private void addTemporalToDateConverter(MutableConversionService conversionService, Class temporalType, Function toInstant) { conversionService.addConverter(temporalType, Date.class, (T object, Class targetType, ConversionContext context) -> Optional.of(Date.from(toInstant.apply(object)))); } diff --git a/context/src/main/java/io/micronaut/scheduling/processor/ScheduledMethodProcessor.java b/context/src/main/java/io/micronaut/scheduling/processor/ScheduledMethodProcessor.java index 3e2b8288bc7..63d7beb84b5 100644 --- a/context/src/main/java/io/micronaut/scheduling/processor/ScheduledMethodProcessor.java +++ b/context/src/main/java/io/micronaut/scheduling/processor/ScheduledMethodProcessor.java @@ -66,7 +66,7 @@ public class ScheduledMethodProcessor implements ExecutableMethodProcessor conversionService; + private final ConversionService conversionService; private final Queue> scheduledTasks = new ConcurrentLinkedDeque<>(); private final TaskExceptionHandler taskExceptionHandler; @@ -76,7 +76,7 @@ public class ScheduledMethodProcessor implements ExecutableMethodProcessor> conversionService, TaskExceptionHandler taskExceptionHandler) { + public ScheduledMethodProcessor(BeanContext beanContext, Optional conversionService, TaskExceptionHandler taskExceptionHandler) { this.beanContext = beanContext; this.conversionService = conversionService.orElse(ConversionService.SHARED); this.taskExceptionHandler = taskExceptionHandler; diff --git a/context/src/main/java/io/micronaut/runtime/converters/reactive/ReactiveTypeConverterRegistrar.java b/core-reactive/src/main/java/io/micronaut/core/async/converters/ReactiveTypeConverterRegistrar.java similarity index 80% rename from context/src/main/java/io/micronaut/runtime/converters/reactive/ReactiveTypeConverterRegistrar.java rename to core-reactive/src/main/java/io/micronaut/core/async/converters/ReactiveTypeConverterRegistrar.java index 2c2e4938080..db161604c6e 100644 --- a/context/src/main/java/io/micronaut/runtime/converters/reactive/ReactiveTypeConverterRegistrar.java +++ b/core-reactive/src/main/java/io/micronaut/core/async/converters/ReactiveTypeConverterRegistrar.java @@ -13,14 +13,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.micronaut.runtime.converters.reactive; +package io.micronaut.core.async.converters; -import io.micronaut.context.annotation.Requires; +import io.micronaut.core.annotation.Internal; import io.micronaut.core.async.publisher.Publishers; -import io.micronaut.core.convert.ConversionService; +import io.micronaut.core.convert.MutableConversionService; import io.micronaut.core.convert.TypeConverterRegistrar; import org.reactivestreams.Publisher; -import jakarta.inject.Singleton; /** * Registers converters for Reactive types such as {@link Publisher}. @@ -28,12 +27,11 @@ * @author Sergio del Amo * @since 3.0.0 */ -@Singleton -@Requires(classes = Publishers.class) +@Internal public class ReactiveTypeConverterRegistrar implements TypeConverterRegistrar { @Override - public void register(ConversionService conversionService) { + public void register(MutableConversionService conversionService) { conversionService.addConverter(Object.class, Publisher.class, obj -> { if (obj instanceof Publisher) { return (Publisher) obj; diff --git a/core-reactive/src/main/resources/META-INF/services/io.micronaut.core.convert.TypeConverterRegistrar b/core-reactive/src/main/resources/META-INF/services/io.micronaut.core.convert.TypeConverterRegistrar new file mode 100644 index 00000000000..219feaa6e6a --- /dev/null +++ b/core-reactive/src/main/resources/META-INF/services/io.micronaut.core.convert.TypeConverterRegistrar @@ -0,0 +1 @@ +io.micronaut.core.async.converters.ReactiveTypeConverterRegistrar diff --git a/core/src/main/java/io/micronaut/core/bind/annotation/AbstractAnnotatedArgumentBinder.java b/core/src/main/java/io/micronaut/core/bind/annotation/AbstractAnnotatedArgumentBinder.java index 0d1afa1ef04..f8e2eefe96f 100644 --- a/core/src/main/java/io/micronaut/core/bind/annotation/AbstractAnnotatedArgumentBinder.java +++ b/core/src/main/java/io/micronaut/core/bind/annotation/AbstractAnnotatedArgumentBinder.java @@ -39,14 +39,14 @@ public abstract class AbstractAnnotatedArgumentBinder implements AnnotatedArgumentBinder { private static final String DEFAULT_VALUE_MEMBER = "defaultValue"; - private final ConversionService conversionService; + protected final ConversionService conversionService; /** * Constructor. * * @param conversionService conversionService */ - protected AbstractAnnotatedArgumentBinder(ConversionService conversionService) { + protected AbstractAnnotatedArgumentBinder(ConversionService conversionService) { this.conversionService = conversionService; } diff --git a/core/src/main/java/io/micronaut/core/convert/ConversionService.java b/core/src/main/java/io/micronaut/core/convert/ConversionService.java index e99afba7288..7178fcbcf86 100644 --- a/core/src/main/java/io/micronaut/core/convert/ConversionService.java +++ b/core/src/main/java/io/micronaut/core/convert/ConversionService.java @@ -15,49 +15,24 @@ */ package io.micronaut.core.convert; +import io.micronaut.core.annotation.Nullable; import io.micronaut.core.convert.exceptions.ConversionErrorException; import io.micronaut.core.type.Argument; -import io.micronaut.core.annotation.Nullable; + import java.util.Optional; -import java.util.function.Function; /** * A service for allowing conversion from one type to another. * - * @param The type * @author Graeme Rocher * @since 1.0 */ -public interface ConversionService { +public interface ConversionService { /** * The default shared conversion service. */ - ConversionService SHARED = new DefaultConversionService(); - - /** - * Adds a type converter. - * - * @param sourceType The source type - * @param targetType The target type - * @param typeConverter The type converter - * @param The source generic type - * @param The target generic type - * @return This conversion service - */ - Impl addConverter(Class sourceType, Class targetType, Function typeConverter); - - /** - * Adds a type converter. - * - * @param sourceType The source type - * @param targetType The target type - * @param typeConverter The type converter - * @param The source generic type - * @param The target generic type - * @return This conversion service - */ - Impl addConverter(Class sourceType, Class targetType, TypeConverter typeConverter); + ConversionService SHARED = new DefaultMutableConversionService(); /** * Attempts to convert the given object to the given target type. If conversion fails or is not possible an empty {@link Optional} is returned. diff --git a/core/src/main/java/io/micronaut/core/convert/ConversionServiceAware.java b/core/src/main/java/io/micronaut/core/convert/ConversionServiceAware.java new file mode 100644 index 00000000000..5385ed0b352 --- /dev/null +++ b/core/src/main/java/io/micronaut/core/convert/ConversionServiceAware.java @@ -0,0 +1,35 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.core.convert; + +import io.micronaut.core.annotation.NonNull; + +/** + * Interface used when the component requires to set up bean context's {@link ConversionService}. + * + * @author Denis Stepanov + * @since 4.0.0 + */ +public interface ConversionServiceAware { + + /** + * Sets the conversion service. + * + * @param conversionService The conversion service + */ + void setConversionService(@NonNull ConversionService conversionService); + +} diff --git a/context/src/main/java/io/micronaut/runtime/converters/reactive/package-info.java b/core/src/main/java/io/micronaut/core/convert/ConversionServiceProvider.java similarity index 55% rename from context/src/main/java/io/micronaut/runtime/converters/reactive/package-info.java rename to core/src/main/java/io/micronaut/core/convert/ConversionServiceProvider.java index fb87664eafb..1d8b7dc4db6 100644 --- a/context/src/main/java/io/micronaut/runtime/converters/reactive/package-info.java +++ b/core/src/main/java/io/micronaut/core/convert/ConversionServiceProvider.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2020 original authors + * Copyright 2017-2022 original authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,10 +13,24 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +package io.micronaut.core.convert; + +import io.micronaut.core.annotation.NonNull; + /** - * Contains classes for reactive streams conversion. + * Interface for a component to provide the access to its {@link ConversionService}. * - * @author Sergio del Amo - * @since 3.0.0 + * @author Denis Stepanov + * @since 4.0.0 */ -package io.micronaut.runtime.converters.reactive; +public interface ConversionServiceProvider { + + /** + * Provides the conversion service. + * + * @return the conversion service + */ + @NonNull + ConversionService getConversionService(); + +} diff --git a/core/src/main/java/io/micronaut/core/convert/DefaultConversionService.java b/core/src/main/java/io/micronaut/core/convert/DefaultMutableConversionService.java similarity index 95% rename from core/src/main/java/io/micronaut/core/convert/DefaultConversionService.java rename to core/src/main/java/io/micronaut/core/convert/DefaultMutableConversionService.java index fee4987e8a7..b57c7f4b00f 100644 --- a/core/src/main/java/io/micronaut/core/convert/DefaultConversionService.java +++ b/core/src/main/java/io/micronaut/core/convert/DefaultMutableConversionService.java @@ -17,7 +17,6 @@ import io.micronaut.core.annotation.AnnotationClassValue; import io.micronaut.core.annotation.AnnotationMetadata; -import io.micronaut.core.annotation.Internal; import io.micronaut.core.convert.converters.MultiValuesConverterFactory; import io.micronaut.core.convert.exceptions.ConversionErrorException; import io.micronaut.core.convert.format.Format; @@ -69,6 +68,7 @@ import java.util.List; import java.util.Locale; import java.util.Map; +import java.util.Objects; import java.util.Optional; import java.util.OptionalInt; import java.util.OptionalLong; @@ -85,7 +85,7 @@ * @author Graeme Rocher * @since 1.0 */ -public class DefaultConversionService implements ConversionService { +public class DefaultMutableConversionService implements MutableConversionService { private static final int CACHE_MAX = 150; private static final TypeConverter UNCONVERTIBLE = (object, targetType, context) -> Optional.empty(); @@ -98,7 +98,7 @@ public class DefaultConversionService implements ConversionService Optional convert(Object object, Class targetType, ConversionCon } targetType = targetType.isPrimitive() ? ReflectionUtils.getWrapperType(targetType) : targetType; - if (targetType.isInstance(object) && !Iterable.class.isInstance(object) && !Map.class.isInstance(object)) { + if (targetType.isInstance(object) && !(object instanceof Iterable) && !(object instanceof Map)) { return Optional.of((T) object); } @@ -123,7 +123,7 @@ public Optional convert(Object object, Class targetType, ConversionCon Optional formattingAnn = annotationMetadata.getAnnotationNameByStereotype(Format.class); String formattingAnnotation = formattingAnn.orElse(null); ConvertiblePair pair = new ConvertiblePair(sourceType, targetType, formattingAnnotation); - TypeConverter typeConverter = converterCache.get(pair); + TypeConverter typeConverter = converterCache.get(pair); if (typeConverter == null) { typeConverter = findTypeConverter(sourceType, targetType, formattingAnnotation); if (typeConverter == null) { @@ -141,7 +141,7 @@ public Optional convert(Object object, Class targetType, ConversionCon } } else { ConvertiblePair pair = new ConvertiblePair(sourceType, targetType, null); - TypeConverter typeConverter = converterCache.get(pair); + TypeConverter typeConverter = converterCache.get(pair); if (typeConverter == null) { typeConverter = findTypeConverter(sourceType, targetType, null); if (typeConverter == null) { @@ -166,7 +166,7 @@ public Optional convert(Object object, Class targetType, ConversionCon @Override public boolean canConvert(Class sourceType, Class targetType) { ConvertiblePair pair = new ConvertiblePair(sourceType, targetType, null); - TypeConverter typeConverter = converterCache.get(pair); + TypeConverter typeConverter = converterCache.get(pair); if (typeConverter == null) { typeConverter = findTypeConverter(sourceType, targetType, null); if (typeConverter != null) { @@ -179,40 +179,25 @@ public boolean canConvert(Class sourceType, Class targetType) { } @Override - public DefaultConversionService addConverter(Class sourceType, Class targetType, TypeConverter typeConverter) { + public void addConverter(Class sourceType, Class targetType, TypeConverter typeConverter) { ConvertiblePair pair = newPair(sourceType, targetType, typeConverter); typeConverters.put(pair, typeConverter); converterCache.put(pair, typeConverter); - return this; } @Override - public DefaultConversionService addConverter(Class sourceType, Class targetType, Function function) { + public void addConverter(Class sourceType, Class targetType, Function function) { ConvertiblePair pair = new ConvertiblePair(sourceType, targetType); TypeConverter typeConverter = TypeConverter.of(sourceType, targetType, function); typeConverters.put(pair, typeConverter); converterCache.put(pair, typeConverter); - return this; - } - - - /** - * Reset internal state. - * - * @since 3.5.3 - */ - @Internal - public void reset() { - typeConverters.clear(); - converterCache.clear(); - registerDefaultConverters(); } /** * Default Converters. */ @SuppressWarnings({"OptionalIsPresent", "unchecked"}) - protected void registerDefaultConverters() { + private void registerDefaultConverters() { // primitive array to wrapper array @SuppressWarnings("rawtypes") Function primitiveArrayToWrapperArray = ArrayUtils::toWrapperArray; @@ -235,7 +220,7 @@ protected void registerDefaultConverters() { addConverter(Object.class, List.class, (object, targetType, context) -> { Optional> firstTypeVariable = context.getFirstTypeVariable(); Argument argument = firstTypeVariable.orElse(Argument.OBJECT_ARGUMENT); - Optional converted = DefaultConversionService.this.convert(object, context.with(argument)); + Optional converted = DefaultMutableConversionService.this.convert(object, context.with(argument)); if (converted.isPresent()) { return Optional.of(Collections.singletonList(converted.get())); } @@ -246,7 +231,7 @@ protected void registerDefaultConverters() { addConverter(CharSequence.class, Class.class, (object, targetType, context) -> { ClassLoader classLoader = targetType.getClassLoader(); if (classLoader == null) { - classLoader = DefaultConversionService.class.getClassLoader(); + classLoader = DefaultMutableConversionService.class.getClassLoader(); } return ClassUtils.forName(object.toString(), classLoader); }); @@ -321,9 +306,9 @@ protected void registerDefaultConverters() { // InputStream -> Number addConverter(InputStream.class, Number.class, (object, targetType, context) -> { - Optional convert = DefaultConversionService.this.convert(object, String.class, context); + Optional convert = DefaultMutableConversionService.this.convert(object, String.class, context); if (convert.isPresent()) { - return convert.flatMap(val -> DefaultConversionService.this.convert(val, targetType, context)); + return convert.flatMap(val -> DefaultMutableConversionService.this.convert(val, targetType, context)); } return Optional.empty(); }); @@ -974,8 +959,8 @@ protected void registerDefaultConverters() { * @param Generic type * @return type converter */ - protected TypeConverter findTypeConverter(Class sourceType, Class targetType, String formattingAnnotation) { - TypeConverter typeConverter = UNCONVERTIBLE; + protected TypeConverter findTypeConverter(Class sourceType, Class targetType, String formattingAnnotation) { + TypeConverter typeConverter = UNCONVERTIBLE; List sourceHierarchy = ClassUtils.resolveHierarchy(sourceType); List targetHierarchy = ClassUtils.resolveHierarchy(targetType); for (Class sourceSuperType : sourceHierarchy) { @@ -1025,19 +1010,21 @@ private ConvertiblePair newPair(Class sourceType, Class targetType, /** * Binds the source and target. */ - private final class ConvertiblePair { - final Class source; - final Class target; + private static final class ConvertiblePair { + final Class source; + final Class target; final String formattingAnnotation; + final int hashCode; - ConvertiblePair(Class source, Class target) { + ConvertiblePair(Class source, Class target) { this(source, target, null); } - ConvertiblePair(Class source, Class target, String formattingAnnotation) { + ConvertiblePair(Class source, Class target, String formattingAnnotation) { this.source = source; this.target = target; this.formattingAnnotation = formattingAnnotation; + this.hashCode = Objects.hash(source, target, formattingAnnotation); } @Override @@ -1048,24 +1035,19 @@ public boolean equals(Object o) { if (o == null || getClass() != o.getClass()) { return false; } - ConvertiblePair pair = (ConvertiblePair) o; - if (!source.equals(pair.source)) { return false; } if (!target.equals(pair.target)) { return false; } - return formattingAnnotation != null ? formattingAnnotation.equals(pair.formattingAnnotation) : pair.formattingAnnotation == null; + return Objects.equals(formattingAnnotation, pair.formattingAnnotation); } @Override public int hashCode() { - int result = source.hashCode(); - result = 31 * result + target.hashCode(); - result = 31 * result + (formattingAnnotation != null ? formattingAnnotation.hashCode() : 0); - return result; + return hashCode; } } } diff --git a/core/src/main/java/io/micronaut/core/convert/MutableConversionService.java b/core/src/main/java/io/micronaut/core/convert/MutableConversionService.java new file mode 100644 index 00000000000..50a03b04eb7 --- /dev/null +++ b/core/src/main/java/io/micronaut/core/convert/MutableConversionService.java @@ -0,0 +1,63 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.core.convert; + +import io.micronaut.core.annotation.NonNull; + +import java.util.function.Function; + +/** + * A version of {@link ConversionService} that supports adding new converters. + * + * @author Denis Stepanov + * @since 4.0.0 + */ +public interface MutableConversionService extends ConversionService { + + /** + * Creates a new mutable conversion service that extends the shared conversion service. + * In most cases the mutable service from the bean context should be used. + * + * @return A new mutable conversion service. + */ + @NonNull + static MutableConversionService create() { + return new DefaultMutableConversionService(); + } + + /** + * Adds a type converter. + * + * @param sourceType The source type + * @param targetType The target type + * @param typeConverter The type converter + * @param The source generic type + * @param The target generic type + */ + void addConverter(@NonNull Class sourceType, @NonNull Class targetType, @NonNull Function typeConverter); + + /** + * Adds a type converter. + * + * @param sourceType The source type + * @param targetType The target type + * @param typeConverter The type converter + * @param The source generic type + * @param The target generic type + */ + void addConverter(@NonNull Class sourceType, @NonNull Class targetType, @NonNull TypeConverter typeConverter); + +} diff --git a/core/src/main/java/io/micronaut/core/convert/TypeConverterRegistrar.java b/core/src/main/java/io/micronaut/core/convert/TypeConverterRegistrar.java index 9baeacc5295..6d4e98fd465 100644 --- a/core/src/main/java/io/micronaut/core/convert/TypeConverterRegistrar.java +++ b/core/src/main/java/io/micronaut/core/convert/TypeConverterRegistrar.java @@ -31,5 +31,5 @@ public interface TypeConverterRegistrar { * * @param conversionService The {@link ConversionService} */ - void register(ConversionService conversionService); + void register(MutableConversionService conversionService); } diff --git a/core/src/main/java/io/micronaut/core/convert/converters/MultiValuesConverterFactory.java b/core/src/main/java/io/micronaut/core/convert/converters/MultiValuesConverterFactory.java index 22252128b90..59305ccd3ae 100644 --- a/core/src/main/java/io/micronaut/core/convert/converters/MultiValuesConverterFactory.java +++ b/core/src/main/java/io/micronaut/core/convert/converters/MultiValuesConverterFactory.java @@ -236,9 +236,9 @@ private static String joinStrings(Iterable strings, Character delimiter) */ private abstract static class AbstractConverterFromMultiValues implements FormattingTypeConverter { - protected ConversionService conversionService; + protected ConversionService conversionService; - AbstractConverterFromMultiValues(ConversionService conversionService) { + AbstractConverterFromMultiValues(ConversionService conversionService) { this.conversionService = conversionService; } @@ -336,7 +336,7 @@ public Class annotationType() { * A converter to convert from {@link ConvertibleMultiValues} to an {@link Iterable}. */ public static class MultiValuesToIterableConverter extends AbstractConverterFromMultiValues { - public MultiValuesToIterableConverter(ConversionService conversionService) { + public MultiValuesToIterableConverter(ConversionService conversionService) { super(conversionService); } @@ -407,7 +407,7 @@ private Optional convertValues(ArgumentConversionContext con * A converter to convert from {@link ConvertibleMultiValues} to an {@link Map}. */ public static class MultiValuesToMapConverter extends AbstractConverterFromMultiValues { - public MultiValuesToMapConverter(ConversionService conversionService) { + public MultiValuesToMapConverter(ConversionService conversionService) { super(conversionService); } @@ -484,7 +484,7 @@ private Optional convertValues(ArgumentConversionContext context, Map< */ public static class MultiValuesToObjectConverter extends AbstractConverterFromMultiValues { - public MultiValuesToObjectConverter(ConversionService conversionService) { + public MultiValuesToObjectConverter(ConversionService conversionService) { super(conversionService); } @@ -556,9 +556,9 @@ private Optional convertValues(ArgumentConversionContext context */ public abstract static class AbstractConverterToMultiValues implements FormattingTypeConverter { - protected ConversionService conversionService; + protected ConversionService conversionService; - public AbstractConverterToMultiValues(ConversionService conversionService) { + public AbstractConverterToMultiValues(ConversionService conversionService) { this.conversionService = conversionService; } @@ -655,7 +655,7 @@ public Class annotationType() { * A converter from {@link Iterable} to {@link ConvertibleMultiValues}. */ public static class IterableToMultiValuesConverter extends AbstractConverterToMultiValues { - public IterableToMultiValuesConverter(ConversionService conversionService) { + public IterableToMultiValuesConverter(ConversionService conversionService) { super(conversionService); } @@ -704,7 +704,7 @@ protected void addDeepObjectValues(ArgumentConversionContext context, St * A converter from {@link Map} to {@link ConvertibleMultiValues}. */ public static class MapToMultiValuesConverter extends AbstractConverterToMultiValues { - public MapToMultiValuesConverter(ConversionService conversionService) { + public MapToMultiValuesConverter(ConversionService conversionService) { super(conversionService); } @@ -759,7 +759,7 @@ protected void addDeepObjectValues(ArgumentConversionContext context, St * A converter from generic {@link Object} to {@link ConvertibleMultiValues}. */ public static class ObjectToMultiValuesConverter extends AbstractConverterToMultiValues { - public ObjectToMultiValuesConverter(ConversionService conversionService) { + public ObjectToMultiValuesConverter(ConversionService conversionService) { super(conversionService); } diff --git a/core/src/main/java/io/micronaut/core/convert/value/ConvertibleMultiValuesMap.java b/core/src/main/java/io/micronaut/core/convert/value/ConvertibleMultiValuesMap.java index 7354bbdb7a5..0dc0fc5012d 100644 --- a/core/src/main/java/io/micronaut/core/convert/value/ConvertibleMultiValuesMap.java +++ b/core/src/main/java/io/micronaut/core/convert/value/ConvertibleMultiValuesMap.java @@ -17,6 +17,7 @@ import io.micronaut.core.convert.ArgumentConversionContext; import io.micronaut.core.convert.ConversionService; +import io.micronaut.core.convert.ConversionServiceAware; import io.micronaut.core.type.Argument; import java.util.Collection; @@ -36,11 +37,15 @@ * @author Graeme Rocher * @since 1.0 */ -public class ConvertibleMultiValuesMap implements ConvertibleMultiValues { - public static final ConvertibleMultiValues EMPTY = new ConvertibleMultiValuesMap<>(Collections.emptyMap()); +public class ConvertibleMultiValuesMap implements ConvertibleMultiValues, ConversionServiceAware { + public static final ConvertibleMultiValues EMPTY = new ConvertibleMultiValuesMap(Collections.emptyMap()) { + @Override + public void setConversionService(ConversionService conversionService) { + } + }; protected final Map> values; - private final ConversionService conversionService; + private ConversionService conversionService; /** * Construct an empty {@link ConvertibleValuesMap}. @@ -64,7 +69,7 @@ public ConvertibleMultiValuesMap(Map> values) { * @param values The map * @param conversionService The conversion service */ - public ConvertibleMultiValuesMap(Map> values, ConversionService conversionService) { + public ConvertibleMultiValuesMap(Map> values, ConversionService conversionService) { this.values = wrapValues(values); this.conversionService = conversionService; } @@ -137,4 +142,13 @@ protected Map> wrapValues(Map> value return Collections.unmodifiableMap(values); } + @Override + public ConversionService getConversionService() { + return conversionService; + } + + @Override + public void setConversionService(ConversionService conversionService) { + this.conversionService = conversionService; + } } diff --git a/core/src/main/java/io/micronaut/core/convert/value/ConvertibleValues.java b/core/src/main/java/io/micronaut/core/convert/value/ConvertibleValues.java index 7b13e2edc87..66f40fcf3db 100644 --- a/core/src/main/java/io/micronaut/core/convert/value/ConvertibleValues.java +++ b/core/src/main/java/io/micronaut/core/convert/value/ConvertibleValues.java @@ -19,11 +19,21 @@ import io.micronaut.core.convert.ArgumentConversionContext; import io.micronaut.core.convert.ConversionContext; import io.micronaut.core.convert.ConversionService; +import io.micronaut.core.convert.ConversionServiceProvider; import io.micronaut.core.reflect.GenericTypeUtils; import io.micronaut.core.type.Argument; import io.micronaut.core.value.ValueResolver; -import java.util.*; +import java.util.Collection; +import java.util.Collections; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.NoSuchElementException; +import java.util.Objects; +import java.util.Optional; +import java.util.Properties; +import java.util.Set; import java.util.function.BiConsumer; import java.util.stream.Collectors; @@ -34,7 +44,7 @@ * @author Graeme Rocher * @since 1.0 */ -public interface ConvertibleValues extends ValueResolver, Iterable> { +public interface ConvertibleValues extends ValueResolver, Iterable>, ConversionServiceProvider { ConvertibleValues EMPTY = new ConvertibleValuesMap<>(Collections.emptyMap()); @@ -133,9 +143,9 @@ default Map asMap(Class keyType, Class valueType) { Map newMap = new LinkedHashMap<>(); for (Map.Entry entry : this) { String key = entry.getKey(); - Optional convertedKey = ConversionService.SHARED.convert(key, keyType); + Optional convertedKey = getConversionService().convert(key, keyType); if (convertedKey.isPresent()) { - Optional convertedValue = ConversionService.SHARED.convert(entry.getValue(), valueType); + Optional convertedValue = getConversionService().convert(entry.getValue(), valueType); convertedValue.ifPresent(vt -> newMap.put(convertedKey.get(), vt)); } } @@ -247,10 +257,23 @@ public V setValue(V value) { * @return The values */ static ConvertibleValues of(Map values) { + return of(values, ConversionService.SHARED); + } + + /** + * Creates a new {@link ConvertibleValues} for the values. + * + * @param values A map of values + * @param conversionService The conversion service + * @param The target generic type + * @return The values + * @since 4.0.0 + */ + static ConvertibleValues of(Map values, ConversionService conversionService) { if (values == null) { return ConvertibleValuesMap.empty(); } else { - return new ConvertibleValuesMap<>(values); + return new ConvertibleValuesMap<>(values, conversionService); } } @@ -264,4 +287,9 @@ static ConvertibleValues of(Map values) { static ConvertibleValues empty() { return ConvertibleValues.EMPTY; } + + @Override + default ConversionService getConversionService() { + return ConversionService.SHARED; + } } diff --git a/core/src/main/java/io/micronaut/core/convert/value/ConvertibleValuesMap.java b/core/src/main/java/io/micronaut/core/convert/value/ConvertibleValuesMap.java index aa63a3f0ae2..f8492761b48 100644 --- a/core/src/main/java/io/micronaut/core/convert/value/ConvertibleValuesMap.java +++ b/core/src/main/java/io/micronaut/core/convert/value/ConvertibleValuesMap.java @@ -18,6 +18,7 @@ import io.micronaut.core.annotation.Nullable; import io.micronaut.core.convert.ArgumentConversionContext; import io.micronaut.core.convert.ConversionService; +import io.micronaut.core.convert.ConversionServiceAware; import java.util.Collection; import java.util.Collections; @@ -34,10 +35,10 @@ * @param generic value * @since 1.0 */ -public class ConvertibleValuesMap implements ConvertibleValues { +public class ConvertibleValuesMap implements ConvertibleValues, ConversionServiceAware { protected final Map map; - private final ConversionService conversionService; + private ConversionService conversionService; /** * Constructor. @@ -59,7 +60,7 @@ public ConvertibleValuesMap(Map map) { * @param map map of values. * @param conversionService conversionService */ - public ConvertibleValuesMap(Map map, ConversionService conversionService) { + public ConvertibleValuesMap(Map map, ConversionService conversionService) { this.map = map; this.conversionService = conversionService; } @@ -104,4 +105,9 @@ public Collection values() { public static ConvertibleValues empty() { return EMPTY; } + + @Override + public void setConversionService(ConversionService conversionService) { + this.conversionService = conversionService; + } } diff --git a/core/src/main/java/io/micronaut/core/convert/value/MutableConvertibleMultiValuesMap.java b/core/src/main/java/io/micronaut/core/convert/value/MutableConvertibleMultiValuesMap.java index be9acbe3e3b..87f83ef0c74 100644 --- a/core/src/main/java/io/micronaut/core/convert/value/MutableConvertibleMultiValuesMap.java +++ b/core/src/main/java/io/micronaut/core/convert/value/MutableConvertibleMultiValuesMap.java @@ -47,7 +47,7 @@ public MutableConvertibleMultiValuesMap(Map> values) { * @param values The values * @param conversionService The conversion service */ - public MutableConvertibleMultiValuesMap(Map> values, ConversionService conversionService) { + public MutableConvertibleMultiValuesMap(Map> values, ConversionService conversionService) { super(values, conversionService); } diff --git a/core/src/main/java/io/micronaut/core/convert/value/MutableConvertibleValuesMap.java b/core/src/main/java/io/micronaut/core/convert/value/MutableConvertibleValuesMap.java index 16c1d95f818..856470bf71c 100644 --- a/core/src/main/java/io/micronaut/core/convert/value/MutableConvertibleValuesMap.java +++ b/core/src/main/java/io/micronaut/core/convert/value/MutableConvertibleValuesMap.java @@ -45,7 +45,7 @@ public MutableConvertibleValuesMap(Map map) { * @param map The map * @param conversionService The conversion service */ - public MutableConvertibleValuesMap(Map map, ConversionService conversionService) { + public MutableConvertibleValuesMap(Map map, ConversionService conversionService) { super(map, conversionService); } diff --git a/core/src/main/java/io/micronaut/core/serialize/JdkSerializer.java b/core/src/main/java/io/micronaut/core/serialize/JdkSerializer.java index cf9984f70e1..8e1462a8bf5 100644 --- a/core/src/main/java/io/micronaut/core/serialize/JdkSerializer.java +++ b/core/src/main/java/io/micronaut/core/serialize/JdkSerializer.java @@ -36,12 +36,12 @@ */ public class JdkSerializer implements ObjectSerializer { - private final ConversionService conversionService; + private final ConversionService conversionService; /** * @param conversionService The conversion service */ - public JdkSerializer(ConversionService conversionService) { + public JdkSerializer(ConversionService conversionService) { this.conversionService = conversionService; } diff --git a/core/src/main/java/io/micronaut/core/value/MapPropertyResolver.java b/core/src/main/java/io/micronaut/core/value/MapPropertyResolver.java index 369ae25b624..fe6b2d526f1 100644 --- a/core/src/main/java/io/micronaut/core/value/MapPropertyResolver.java +++ b/core/src/main/java/io/micronaut/core/value/MapPropertyResolver.java @@ -34,7 +34,7 @@ */ public class MapPropertyResolver implements PropertyResolver { private final Map map; - private final ConversionService conversionService; + private final ConversionService conversionService; /** * @param map The map to resolves the properties from diff --git a/core/src/test/groovy/io/micronaut/core/convert/DefaultConversionServiceSpec.groovy b/core/src/test/groovy/io/micronaut/core/convert/DefaultConversionServiceSpec.groovy index d83eaccf4cf..ba36af1a363 100644 --- a/core/src/test/groovy/io/micronaut/core/convert/DefaultConversionServiceSpec.groovy +++ b/core/src/test/groovy/io/micronaut/core/convert/DefaultConversionServiceSpec.groovy @@ -31,7 +31,7 @@ class DefaultConversionServiceSpec extends Specification { @Unroll void "test default conversion service converts a #sourceObject.class.name to a #targetType.name"() { given: - ConversionService conversionService = new DefaultConversionService() + ConversionService conversionService = new DefaultMutableConversionService() expect: conversionService.convert(sourceObject, targetType).get() == result @@ -70,7 +70,7 @@ class DefaultConversionServiceSpec extends Specification { void "test empty string conversion"() { given: - ConversionService conversionService = new DefaultConversionService() + ConversionService conversionService = new DefaultMutableConversionService() expect: !conversionService.convert("", targetType).isPresent() @@ -81,7 +81,7 @@ class DefaultConversionServiceSpec extends Specification { void "test convert required"() { given: - ConversionService conversionService = new DefaultConversionService() + ConversionService conversionService = new DefaultMutableConversionService() when: conversionService.convertRequired("junk", Integer) @@ -94,7 +94,7 @@ class DefaultConversionServiceSpec extends Specification { void "test conversion service with type arguments"() { given: - ConversionService conversionService = new DefaultConversionService() + ConversionService conversionService = new DefaultMutableConversionService() expect: conversionService.convert(sourceObject, targetType, ConversionContext.of(typeArguments)).get() == result diff --git a/core/src/test/groovy/io/micronaut/core/convert/value/ConvertibleValuesSpec.groovy b/core/src/test/groovy/io/micronaut/core/convert/value/ConvertibleValuesSpec.groovy index d139d38fc2e..b8814252e85 100644 --- a/core/src/test/groovy/io/micronaut/core/convert/value/ConvertibleValuesSpec.groovy +++ b/core/src/test/groovy/io/micronaut/core/convert/value/ConvertibleValuesSpec.groovy @@ -15,6 +15,7 @@ */ package io.micronaut.core.convert.value + import spock.lang.Specification class ConvertibleValuesSpec extends Specification { diff --git a/function-web/src/main/java/io/micronaut/function/web/AnnotatedFunctionRouteBuilder.java b/function-web/src/main/java/io/micronaut/function/web/AnnotatedFunctionRouteBuilder.java index 04ce2ac83c1..94cd450c02f 100644 --- a/function-web/src/main/java/io/micronaut/function/web/AnnotatedFunctionRouteBuilder.java +++ b/function-web/src/main/java/io/micronaut/function/web/AnnotatedFunctionRouteBuilder.java @@ -77,7 +77,7 @@ public class AnnotatedFunctionRouteBuilder public AnnotatedFunctionRouteBuilder( ExecutionHandleLocator executionHandleLocator, UriNamingStrategy uriNamingStrategy, - ConversionService conversionService, + ConversionService conversionService, MediaTypeCodecRegistry codecRegistry, @Value("${micronaut.function.context-path:/}") String contextPath) { super(executionHandleLocator, uriNamingStrategy, conversionService); diff --git a/function/src/main/java/io/micronaut/function/executor/StreamFunctionExecutor.java b/function/src/main/java/io/micronaut/function/executor/StreamFunctionExecutor.java index 53c4d180064..fc86979eef1 100644 --- a/function/src/main/java/io/micronaut/function/executor/StreamFunctionExecutor.java +++ b/function/src/main/java/io/micronaut/function/executor/StreamFunctionExecutor.java @@ -178,7 +178,7 @@ static void encode(Environment environment, LocalFunctionRegistry registry, Clas } private Object decodeInputArgument( - ConversionService conversionService, + ConversionService conversionService, LocalFunctionRegistry localFunctionRegistry, Argument arg, InputStream input) { @@ -204,7 +204,7 @@ private Object decodeInputArgument( } private Object decodeContext( - ConversionService conversionService, + ConversionService conversionService, Argument arg, Object context) { if (ClassUtils.isJavaLangType(arg.getType())) { @@ -216,7 +216,7 @@ private Object decodeContext( throw new CodecException("Unable to decode argument from stream: " + arg); } - private Object doConvertInput(ConversionService conversionService, Argument arg, Object object) { + private Object doConvertInput(ConversionService conversionService, Argument arg, Object object) { ArgumentConversionContext conversionContext = ConversionContext.of(arg); Optional convert = conversionService.convert(object, conversionContext); if (convert.isPresent()) { diff --git a/http-client-core/src/main/java/io/micronaut/http/client/bind/DefaultHttpClientBinderRegistry.java b/http-client-core/src/main/java/io/micronaut/http/client/bind/DefaultHttpClientBinderRegistry.java index 64dd91d4fa3..f04bc5659cd 100644 --- a/http-client-core/src/main/java/io/micronaut/http/client/bind/DefaultHttpClientBinderRegistry.java +++ b/http-client-core/src/main/java/io/micronaut/http/client/bind/DefaultHttpClientBinderRegistry.java @@ -80,7 +80,7 @@ public class DefaultHttpClientBinderRegistry implements HttpClientBinderRegistry * @param binders The request binders * @param beanContext The context to resolve beans */ - protected DefaultHttpClientBinderRegistry(ConversionService conversionService, + protected DefaultHttpClientBinderRegistry(ConversionService conversionService, List binders, BeanContext beanContext) { byType.put(Argument.of(HttpHeaders.class).typeHashCode(), (ClientArgumentRequestBinder) (context, uriContext, value, request) -> value.forEachValue(request::header)); diff --git a/http-client-core/src/main/java/io/micronaut/http/client/bind/binders/QueryValueClientArgumentRequestBinder.java b/http-client-core/src/main/java/io/micronaut/http/client/bind/binders/QueryValueClientArgumentRequestBinder.java index 21bfee9a010..71e7e840081 100644 --- a/http-client-core/src/main/java/io/micronaut/http/client/bind/binders/QueryValueClientArgumentRequestBinder.java +++ b/http-client-core/src/main/java/io/micronaut/http/client/bind/binders/QueryValueClientArgumentRequestBinder.java @@ -42,9 +42,9 @@ */ public class QueryValueClientArgumentRequestBinder implements AnnotatedClientArgumentRequestBinder { - private final ConversionService conversionService; + private final ConversionService conversionService; - public QueryValueClientArgumentRequestBinder(ConversionService conversionService) { + public QueryValueClientArgumentRequestBinder(ConversionService conversionService) { this.conversionService = conversionService; } diff --git a/http-client-core/src/main/java/io/micronaut/http/client/interceptor/HttpClientIntroductionAdvice.java b/http-client-core/src/main/java/io/micronaut/http/client/interceptor/HttpClientIntroductionAdvice.java index e13c3b90578..b6a30bcceae 100644 --- a/http-client-core/src/main/java/io/micronaut/http/client/interceptor/HttpClientIntroductionAdvice.java +++ b/http-client-core/src/main/java/io/micronaut/http/client/interceptor/HttpClientIntroductionAdvice.java @@ -114,7 +114,7 @@ public class HttpClientIntroductionAdvice implements MethodInterceptor clientFactory; - private final ConversionService conversionService; + private final ConversionService conversionService; /** * Constructor for advice class to setup things like Headers, Cookies, Parameters for Clients. @@ -130,7 +130,7 @@ public HttpClientIntroductionAdvice( JsonMediaTypeCodec jsonMediaTypeCodec, List transformers, HttpClientBinderRegistry binderRegistry, - ConversionService conversionService) { + ConversionService conversionService) { this.clientFactory = clientFactory; this.jsonMediaTypeCodec = jsonMediaTypeCodec; this.transformers = transformers != null ? transformers : Collections.emptyList(); @@ -194,7 +194,7 @@ public Object intercept(MethodInvocationContext context) { .orElse(argument.getName()); // Convert and put as path param if (argument.getAnnotationMetadata().hasStereotype(Format.class)) { - ConversionService.SHARED.convert(value, + conversionService.convert(value, ConversionContext.STRING.with(argument.getAnnotationMetadata())) .ifPresent(v -> pathParams.put(name, v)); } else { @@ -461,9 +461,9 @@ private Publisher httpClientResponseStreamingPublisher(StreamingHttpClient strea if (reactiveValueType == ByteBuffer.class) { return byteBufferPublisher; } else { - if (ConversionService.SHARED.canConvert(ByteBuffer.class, reactiveValueType)) { + if (conversionService.canConvert(ByteBuffer.class, reactiveValueType)) { // It would be nice if we could capture the TypeConverter here - return Publishers.map(byteBufferPublisher, value -> ConversionService.SHARED.convert(value, reactiveValueType).get()); + return Publishers.map(byteBufferPublisher, value -> conversionService.convert(value, reactiveValueType).get()); } else { throw new ConfigurationException("Cannot create the generated HTTP client's " + "required return type, since no TypeConverter from ByteBuffer to " + diff --git a/http-client-core/src/main/java/io/micronaut/http/client/loadbalance/LoadBalancerConverters.java b/http-client-core/src/main/java/io/micronaut/http/client/loadbalance/LoadBalancerConverters.java index 384626fd81d..00dac991eb4 100644 --- a/http-client-core/src/main/java/io/micronaut/http/client/loadbalance/LoadBalancerConverters.java +++ b/http-client-core/src/main/java/io/micronaut/http/client/loadbalance/LoadBalancerConverters.java @@ -15,10 +15,9 @@ */ package io.micronaut.http.client.loadbalance; -import io.micronaut.core.convert.ConversionService; +import io.micronaut.core.convert.MutableConversionService; import io.micronaut.core.convert.TypeConverterRegistrar; import io.micronaut.http.client.LoadBalancer; -import jakarta.inject.Singleton; import java.net.URI; import java.net.URISyntaxException; @@ -30,11 +29,10 @@ * @author graemerocher * @since 1.0 */ -@Singleton public class LoadBalancerConverters implements TypeConverterRegistrar { - @SuppressWarnings("deprecation") + @Override - public void register(ConversionService conversionService) { + public void register(MutableConversionService conversionService) { conversionService.addConverter(URI.class, LoadBalancer.class, LoadBalancer::fixed); conversionService.addConverter(URL.class, LoadBalancer.class, LoadBalancer::fixed); conversionService.addConverter(String.class, LoadBalancer.class, url -> { diff --git a/http-client-core/src/main/resources/META-INF/services/io.micronaut.core.convert.TypeConverterRegistrar b/http-client-core/src/main/resources/META-INF/services/io.micronaut.core.convert.TypeConverterRegistrar new file mode 100644 index 00000000000..a1eb0673490 --- /dev/null +++ b/http-client-core/src/main/resources/META-INF/services/io.micronaut.core.convert.TypeConverterRegistrar @@ -0,0 +1 @@ +io.micronaut.http.client.loadbalance.LoadBalancerConverters diff --git a/http-client/src/main/java/io/micronaut/http/client/netty/DefaultHttpClient.java b/http-client/src/main/java/io/micronaut/http/client/netty/DefaultHttpClient.java index c1cdb016b40..f09f399e286 100644 --- a/http-client/src/main/java/io/micronaut/http/client/netty/DefaultHttpClient.java +++ b/http-client/src/main/java/io/micronaut/http/client/netty/DefaultHttpClient.java @@ -24,6 +24,7 @@ import io.micronaut.core.async.publisher.Publishers; import io.micronaut.core.beans.BeanMap; import io.micronaut.core.convert.ConversionService; +import io.micronaut.core.convert.ConversionServiceAware; import io.micronaut.core.io.ResourceResolver; import io.micronaut.core.io.buffer.ByteBuffer; import io.micronaut.core.io.buffer.ByteBufferFactory; @@ -252,7 +253,7 @@ public class DefaultHttpClient implements private final RequestBinderRegistry requestBinderRegistry; private final List invocationInstrumenterFactories; private final String informationalServiceId; - private final ConversionService conversionService; + private final ConversionService conversionService; /** * Construct a client for the given arguments. @@ -265,6 +266,7 @@ public class DefaultHttpClient implements * @param codecRegistry The {@link MediaTypeCodecRegistry} to use for encoding and decoding objects * @param annotationMetadataResolver The annotation metadata resolver * @param invocationInstrumenterFactories The invocation instrumeter factories to instrument netty handlers execution with + * @param conversionService The conversion service * @param filters The filters to use */ public DefaultHttpClient(@Nullable LoadBalancer loadBalancer, @@ -275,6 +277,7 @@ public DefaultHttpClient(@Nullable LoadBalancer loadBalancer, MediaTypeCodecRegistry codecRegistry, @Nullable AnnotationMetadataResolver annotationMetadataResolver, List invocationInstrumenterFactories, + ConversionService conversionService, HttpClientFilter... filters) { this(loadBalancer, null, @@ -286,13 +289,13 @@ public DefaultHttpClient(@Nullable LoadBalancer loadBalancer, nettyClientSslBuilder, codecRegistry, WebSocketBeanRegistry.EMPTY, - new DefaultRequestBinderRegistry(ConversionService.SHARED), + new DefaultRequestBinderRegistry(conversionService), null, NioSocketChannel::new, CompositeNettyClientCustomizer.EMPTY, invocationInstrumenterFactories, null, - ConversionService.SHARED); + conversionService); } /** @@ -313,6 +316,7 @@ public DefaultHttpClient(@Nullable LoadBalancer loadBalancer, * @param clientCustomizer The pipeline customizer * @param invocationInstrumenterFactories The invocation instrumeter factories to instrument netty handlers execution with * @param informationalServiceId Optional service ID that will be passed to exceptions created by this client + * @param conversionService The conversionService */ public DefaultHttpClient(@Nullable LoadBalancer loadBalancer, @Nullable HttpVersionSelection explicitHttpVersion, @@ -330,7 +334,7 @@ public DefaultHttpClient(@Nullable LoadBalancer loadBalancer, NettyClientCustomizer clientCustomizer, List invocationInstrumenterFactories, @Nullable String informationalServiceId, - @NonNull ConversionService conversionService + ConversionService conversionService ) { ArgumentUtils.requireNonNull("nettyClientSslBuilder", nettyClientSslBuilder); ArgumentUtils.requireNonNull("codecRegistry", codecRegistry); @@ -406,7 +410,7 @@ public DefaultHttpClient(@Nullable URI uri, @NonNull HttpClientConfiguration con new NettyClientSslBuilder(new ResourceResolver()), createDefaultMediaTypeRegistry(), AnnotationMetadataResolver.DEFAULT, - Collections.emptyList()); + Collections.emptyList(), ConversionService.SHARED); } /** @@ -420,7 +424,7 @@ configuration, null, new DefaultThreadFactory(MultithreadEventLoopGroup.class), new NettyClientSslBuilder(new ResourceResolver()), createDefaultMediaTypeRegistry(), AnnotationMetadataResolver.DEFAULT, - invocationInstrumenterFactories); + invocationInstrumenterFactories, ConversionService.SHARED); } static boolean isAcceptEvents(io.micronaut.http.HttpRequest request) { @@ -539,6 +543,7 @@ private MutableHttpRequest toMutableRequest(io.micronaut.http.HttpRequest @SuppressWarnings("SubscriberImplementation") @Override public Publisher>> eventStream(@NonNull io.micronaut.http.HttpRequest request) { + setupConversionService(request); return eventStreamOrError(request, null); } @@ -671,11 +676,13 @@ public void onComplete() { @Override public Publisher> eventStream(@NonNull io.micronaut.http.HttpRequest request, @NonNull Argument eventType) { + setupConversionService(request); return eventStream(request, eventType, DEFAULT_ERROR_TYPE); } @Override public Publisher> eventStream(@NonNull io.micronaut.http.HttpRequest request, @NonNull Argument eventType, @NonNull Argument errorType) { + setupConversionService(request); return Flux.from(eventStreamOrError(request, errorType)).map(byteBufferEvent -> { ByteBuffer data = byteBufferEvent.getData(); Optional registeredCodec; @@ -697,11 +704,13 @@ public Publisher> eventStream(@NonNull io.micronaut.http.HttpReq @Override public Publisher> dataStream(@NonNull io.micronaut.http.HttpRequest request) { + setupConversionService(request); return dataStream(request, DEFAULT_ERROR_TYPE); } @Override public Publisher> dataStream(@NonNull io.micronaut.http.HttpRequest request, @NonNull Argument errorType) { + setupConversionService(request); final io.micronaut.http.HttpRequest parentRequest = ServerRequestContext.currentRequest().orElse(null); return new MicronautFlux<>(Flux.from(resolveRequestURI(request)) .flatMap(requestURI -> dataStreamImpl(toMutableRequest(request), errorType, parentRequest, requestURI))) @@ -723,6 +732,7 @@ public Publisher>> exchangeStre @Override public Publisher>> exchangeStream(@NonNull io.micronaut.http.HttpRequest request, @NonNull Argument errorType) { + setupConversionService(request); io.micronaut.http.HttpRequest parentRequest = ServerRequestContext.currentRequest().orElse(null); return new MicronautFlux<>(Flux.from(resolveRequestURI(request)) .flatMap(uri -> exchangeStreamImpl(parentRequest, toMutableRequest(request), errorType, uri))) @@ -741,7 +751,9 @@ public Publisher jsonStream(@NonNull io.micronaut.http.HttpRequest @Override public Publisher jsonStream(@NonNull io.micronaut.http.HttpRequest request, @NonNull Argument type, @NonNull Argument errorType) { + setupConversionService(request); final io.micronaut.http.HttpRequest parentRequest = ServerRequestContext.currentRequest().orElse(null); + setupConversionService(parentRequest); return Flux.from(resolveRequestURI(request)) .flatMap(requestURI -> jsonStreamImpl(parentRequest, toMutableRequest(request), type, errorType, requestURI)); } @@ -754,11 +766,13 @@ public Publisher> jsonStream(@NonNull io.micronaut.http. @Override public Publisher jsonStream(@NonNull io.micronaut.http.HttpRequest request, @NonNull Class type) { + setupConversionService(request); return jsonStream(request, io.micronaut.core.type.Argument.of(type)); } @Override public Publisher> exchange(@NonNull io.micronaut.http.HttpRequest request, @NonNull Argument bodyType, @NonNull Argument errorType) { + setupConversionService(request); final io.micronaut.http.HttpRequest parentRequest = ServerRequestContext.currentRequest().orElse(null); Publisher uriPublisher = resolveRequestURI(request); return Flux.from(uriPublisher) @@ -767,6 +781,7 @@ public Publisher> exchange(@NonNull @Override public Publisher retrieve(io.micronaut.http.HttpRequest request, Argument bodyType, Argument errorType) { + setupConversionService(request); // mostly same as default impl, but with exception customization return Flux.from(exchange(request, bodyType, errorType)).map(response -> { if (bodyType.getType() == HttpStatus.class) { @@ -790,6 +805,7 @@ public Publisher retrieve(io.micronaut.http.HttpRequest request, @Override public Publisher connect(Class clientEndpointType, io.micronaut.http.MutableHttpRequest request) { + setupConversionService(request); Publisher uriPublisher = resolveRequestURI(request); return Flux.from(uriPublisher) .switchMap(resolvedURI -> connectWebSocket(resolvedURI, request, clientEndpointType, null)); @@ -851,7 +867,8 @@ private Publisher connectWebSocket(URI uri, MutableHttpRequest request WebSocketClientHandshakerFactory.newHandshaker( webSocketURL, protocolVersion, subprotocol, true, customHeaders, maxFramePayloadLength), requestBinderRegistry, - mediaTypeCodecRegistry); + mediaTypeCodecRegistry, + conversionService); return connectionManager.connectForWebsocket(requestKey, handler) .then(handler.getHandshakeCompletedMono()); @@ -872,7 +889,7 @@ private Flux>> exchangeStreamImpl(io.micronaut.ht traceBody("Response", byteBuf); } ByteBuffer byteBuffer = byteBufferFactory.wrap(byteBuf); - NettyStreamedHttpResponse> thisResponse = new NettyStreamedHttpResponse<>(streamedHttpResponse); + NettyStreamedHttpResponse> thisResponse = new NettyStreamedHttpResponse<>(streamedHttpResponse, conversionService); thisResponse.setBody(byteBuffer); return (HttpResponse>) new HttpResponseWrapper<>(thisResponse); }); @@ -964,6 +981,7 @@ public Publisher> proxy(@NonNull io.micronaut.http.HttpRe @Override public Publisher> proxy(@NonNull io.micronaut.http.HttpRequest request, @NonNull ProxyRequestOptions options) { Objects.requireNonNull(options, "options"); + setupConversionService(request); return Flux.from(resolveRequestURI(request)) .flatMap(requestURI -> { io.micronaut.http.MutableHttpRequest httpRequest = toMutableRequest(request); @@ -988,6 +1006,12 @@ public Publisher> proxy(@NonNull io.micronaut.http.HttpRe }); } + private void setupConversionService(io.micronaut.http.HttpRequest httpRequest) { + if (httpRequest instanceof ConversionServiceAware) { + ((ConversionServiceAware) httpRequest).setConversionService(conversionService); + } + } + private Flux> connectAndStream( io.micronaut.http.HttpRequest parentRequest, io.micronaut.http.HttpRequest request, @@ -1425,7 +1449,7 @@ public void onError(Throwable t) { public void onComplete() { try { FullHttpResponse fullHttpResponse = new DefaultFullHttpResponse(nettyResponse.protocolVersion(), nettyResponse.status(), buffer, nettyResponse.headers(), new DefaultHttpHeaders(true)); - final FullNettyClientHttpResponse fullNettyClientHttpResponse = new FullNettyClientHttpResponse<>(fullHttpResponse, mediaTypeCodecRegistry, byteBufferFactory, (Argument) errorType, true); + final FullNettyClientHttpResponse fullNettyClientHttpResponse = new FullNettyClientHttpResponse<>(fullHttpResponse, mediaTypeCodecRegistry, byteBufferFactory, (Argument) errorType, true, conversionService); fullNettyClientHttpResponse.onComplete(); emitter.error(customizeException(new HttpClientResponseException( fullHttpResponse.status().reasonPhrase(), @@ -2195,7 +2219,7 @@ protected void buildResponse(Promise> promise, FullHttpR boolean convertBodyWithBodyType = msg.status().code() < 400 || (!DefaultHttpClient.this.configuration.isExceptionOnErrorStatus() && bodyType.equalsType(errorType)); FullNettyClientHttpResponse response - = new FullNettyClientHttpResponse<>(msg, mediaTypeCodecRegistry, byteBufferFactory, bodyType, convertBodyWithBodyType); + = new FullNettyClientHttpResponse<>(msg, mediaTypeCodecRegistry, byteBufferFactory, bodyType, convertBodyWithBodyType, conversionService); if (convertBodyWithBodyType) { promise.trySuccess(response); @@ -2253,7 +2277,8 @@ private HttpClientResponseException makeErrorBodyParseError(FullHttpResponse ful mediaTypeCodecRegistry, byteBufferFactory, null, - false + false, + conversionService ); // this onComplete call disables further parsing by HttpClientResponseException errorResponse.onComplete(); @@ -2271,7 +2296,8 @@ private void makeNormalBodyParseError(FullHttpResponse fullResponse, Throwable t mediaTypeCodecRegistry, byteBufferFactory, null, - false + false, + conversionService ); HttpClientResponseException clientResponseError = customizeException(new HttpClientResponseException( "Error decoding HTTP response body: " + t.getMessage(), @@ -2344,7 +2370,7 @@ protected void buildResponse(Promise> promise, Fu msg.headers(), bodyPublisher ); - promise.trySuccess(new NettyStreamedHttpResponse<>(nettyResponse)); + promise.trySuccess(new NettyStreamedHttpResponse<>(nettyResponse, conversionService)); } @Override @@ -2367,7 +2393,6 @@ public boolean acceptInboundMessage(Object msg) { return msg instanceof StreamedHttpResponse; } - @Override protected void removeHandler(ChannelHandlerContext ctx) { ctx.pipeline().remove(ChannelPipelineCustomizer.HANDLER_MICRONAUT_HTTP_RESPONSE_FULL); ctx.pipeline().remove(ChannelPipelineCustomizer.HANDLER_MICRONAUT_HTTP_RESPONSE_STREAM); @@ -2375,7 +2400,7 @@ protected void removeHandler(ChannelHandlerContext ctx) { @Override protected void buildResponse(Promise> promise, StreamedHttpResponse msg) { - promise.trySuccess(new NettyStreamedHttpResponse<>(msg)); + promise.trySuccess(new NettyStreamedHttpResponse<>(msg, conversionService)); } @Override diff --git a/http-client/src/main/java/io/micronaut/http/client/netty/FullNettyClientHttpResponse.java b/http-client/src/main/java/io/micronaut/http/client/netty/FullNettyClientHttpResponse.java index d106a6d7b2f..208174e6b55 100644 --- a/http-client/src/main/java/io/micronaut/http/client/netty/FullNettyClientHttpResponse.java +++ b/http-client/src/main/java/io/micronaut/http/client/netty/FullNettyClientHttpResponse.java @@ -71,6 +71,7 @@ public class FullNettyClientHttpResponse implements HttpResponse, Completa private final B body; private boolean complete; private byte[] bodyBytes; + private final ConversionService conversionService; /** * @param fullHttpResponse The full Http response @@ -78,20 +79,22 @@ public class FullNettyClientHttpResponse implements HttpResponse, Completa * @param byteBufferFactory The byte buffer factory * @param bodyType The body type * @param convertBody Whether to auto convert the body to bodyType + * @param conversionService The conversion service */ FullNettyClientHttpResponse( FullHttpResponse fullHttpResponse, MediaTypeCodecRegistry mediaTypeCodecRegistry, ByteBufferFactory byteBufferFactory, Argument bodyType, - boolean convertBody) { - - this.headers = new NettyHttpHeaders(fullHttpResponse.headers(), ConversionService.SHARED); + boolean convertBody, + ConversionService conversionService) { + this.conversionService = conversionService; + this.headers = new NettyHttpHeaders(fullHttpResponse.headers(), conversionService); this.attributes = new MutableConvertibleValuesMap<>(); this.nettyHttpResponse = fullHttpResponse; this.mediaTypeCodecRegistry = mediaTypeCodecRegistry; this.byteBufferFactory = byteBufferFactory; - this.nettyCookies = new NettyCookies(fullHttpResponse.headers(), ConversionService.SHARED); + this.nettyCookies = new NettyCookies(fullHttpResponse.headers(), conversionService); Class rawBodyType = bodyType != null ? bodyType.getType() : null; if (rawBodyType != null && !HttpStatus.class.isAssignableFrom(rawBodyType)) { if (HttpResponse.class.isAssignableFrom(bodyType.getType())) { @@ -200,7 +203,7 @@ public Optional getBody(Argument type) { ByteBuf bytebuf = (ByteBuf) ((ByteBuffer) b).asNativeBuffer(); return convertByteBuf(bytebuf, finalArgument); } else { - final Optional opt = ConversionService.SHARED.convert(b, ConversionContext.of(finalArgument)); + final Optional opt = conversionService.convert(b, ConversionContext.of(finalArgument)); if (!opt.isPresent()) { ByteBuf content = nettyHttpResponse.content(); return convertByteBuf(content, finalArgument); @@ -280,7 +283,7 @@ private Optional convertByteBuf(ByteBuf content, Argument type) { LOG.trace("Missing or unknown Content-Type received from server."); } // last chance, try type conversion - return ConversionService.SHARED.convert(content, ConversionContext.of(type)); + return conversionService.convert(content, ConversionContext.of(type)); } private Optional convertBytes(byte[] bytes, Argument type) { @@ -301,7 +304,7 @@ private Optional convertBytes(byte[] bytes, Argument type) { } } // last chance, try type conversion - return ConversionService.SHARED.convert(bytes, ConversionContext.of(type)); + return conversionService.convert(bytes, ConversionContext.of(type)); } @Override diff --git a/http-client/src/main/java/io/micronaut/http/client/netty/MutableHttpRequestWrapper.java b/http-client/src/main/java/io/micronaut/http/client/netty/MutableHttpRequestWrapper.java index 8e1343be39f..a02019d0212 100644 --- a/http-client/src/main/java/io/micronaut/http/client/netty/MutableHttpRequestWrapper.java +++ b/http-client/src/main/java/io/micronaut/http/client/netty/MutableHttpRequestWrapper.java @@ -39,19 +39,19 @@ */ @Internal final class MutableHttpRequestWrapper extends HttpRequestWrapper implements MutableHttpRequest { - private final ConversionService conversionService; + private ConversionService conversionService; @Nullable private B body; @Nullable private URI uri; - MutableHttpRequestWrapper(ConversionService conversionService, HttpRequest delegate) { + MutableHttpRequestWrapper(ConversionService conversionService, HttpRequest delegate) { super(delegate); this.conversionService = conversionService; } - static MutableHttpRequest wrapIfNecessary(ConversionService conversionService, HttpRequest request) { + static MutableHttpRequest wrapIfNecessary(ConversionService conversionService, HttpRequest request) { if (request instanceof MutableHttpRequest) { return (MutableHttpRequest) request; } else { @@ -128,4 +128,9 @@ public MutableHttpRequest body(T body) { this.body = (B) body; return (MutableHttpRequest) this; } + + @Override + public void setConversionService(ConversionService conversionService) { + this.conversionService = conversionService; + } } diff --git a/http-client/src/main/java/io/micronaut/http/client/netty/NettyClientHttpRequest.java b/http-client/src/main/java/io/micronaut/http/client/netty/NettyClientHttpRequest.java index 0320e56e74c..347fd2fe02e 100644 --- a/http-client/src/main/java/io/micronaut/http/client/netty/NettyClientHttpRequest.java +++ b/http-client/src/main/java/io/micronaut/http/client/netty/NettyClientHttpRequest.java @@ -76,6 +76,7 @@ class NettyClientHttpRequest implements MutableHttpRequest, NettyHttpReque private URI uri; private Object body; private NettyHttpParameters httpParameters; + private ConversionService conversionService = ConversionService.SHARED; /** * This constructor is actually required for the case of non-standard http methods. @@ -175,7 +176,7 @@ public Optional getBody(Class type) { @Override public Optional getBody(Argument type) { - return getBody().flatMap(b -> ConversionService.SHARED.convert(b, ConversionContext.of(type))); + return getBody().flatMap(b -> conversionService.convert(b, ConversionContext.of(type))); } @Override @@ -217,7 +218,7 @@ public URI getUri() { private NettyHttpParameters decodeParameters(URI uri) { QueryStringDecoder queryStringDecoder = createDecoder(uri); return new NettyHttpParameters(queryStringDecoder.parameters(), - ConversionService.SHARED, + conversionService, (name, value) -> { UriBuilder newUri = UriBuilder.of(getUri()); newUri.replaceQueryParam(name.toString(), value.toArray()); @@ -327,4 +328,9 @@ public HttpRequest toHttpRequest() { public boolean isStream() { return body instanceof Publisher; } + + @Override + public void setConversionService(ConversionService conversionService) { + this.conversionService = conversionService; + } } diff --git a/http-client/src/main/java/io/micronaut/http/client/netty/NettyStreamedHttpResponse.java b/http-client/src/main/java/io/micronaut/http/client/netty/NettyStreamedHttpResponse.java index 9d9ed4d521a..56e7aed9114 100644 --- a/http-client/src/main/java/io/micronaut/http/client/netty/NettyStreamedHttpResponse.java +++ b/http-client/src/main/java/io/micronaut/http/client/netty/NettyStreamedHttpResponse.java @@ -57,10 +57,11 @@ class NettyStreamedHttpResponse implements MutableHttpResponse, NettyHttpR /** * @param response The streamed Http response + * @param conversionService The conversion service */ - NettyStreamedHttpResponse(StreamedHttpResponse response) { + NettyStreamedHttpResponse(StreamedHttpResponse response, ConversionService conversionService) { this.nettyResponse = response; - this.headers = new NettyHttpHeaders(response.headers(), ConversionService.SHARED); + this.headers = new NettyHttpHeaders(response.headers(), conversionService); } /** diff --git a/http-client/src/main/java/io/micronaut/http/client/netty/websocket/NettyWebSocketClientHandler.java b/http-client/src/main/java/io/micronaut/http/client/netty/websocket/NettyWebSocketClientHandler.java index 0bf36fc9700..a7e7690d2a8 100644 --- a/http-client/src/main/java/io/micronaut/http/client/netty/websocket/NettyWebSocketClientHandler.java +++ b/http-client/src/main/java/io/micronaut/http/client/netty/websocket/NettyWebSocketClientHandler.java @@ -87,23 +87,26 @@ public class NettyWebSocketClientHandler extends AbstractNettyWebSocketHandle /** * Default constructor. - * @param request The originating request that created the WebSocket. - * @param webSocketBean The WebSocket client bean. - * @param handshaker The handshaker - * @param requestBinderRegistry The request binder registry + * + * @param request The originating request that created the WebSocket. + * @param webSocketBean The WebSocket client bean. + * @param handshaker The handshaker + * @param requestBinderRegistry The request binder registry * @param mediaTypeCodecRegistry The media type codec registry + * @param conversionService The conversionService */ public NettyWebSocketClientHandler( MutableHttpRequest request, WebSocketBean webSocketBean, final WebSocketClientHandshaker handshaker, RequestBinderRegistry requestBinderRegistry, - MediaTypeCodecRegistry mediaTypeCodecRegistry) { - super(null, requestBinderRegistry, mediaTypeCodecRegistry, webSocketBean, request, Collections.emptyMap(), handshaker.version(), handshaker.actualSubprotocol(), null); + MediaTypeCodecRegistry mediaTypeCodecRegistry, + ConversionService conversionService) { + super(null, requestBinderRegistry, mediaTypeCodecRegistry, webSocketBean, request, Collections.emptyMap(), handshaker.version(), handshaker.actualSubprotocol(), null, conversionService); this.codecRegistry = mediaTypeCodecRegistry; this.handshaker = handshaker; this.genericWebSocketBean = webSocketBean; - this.webSocketStateBinderRegistry = new WebSocketStateBinderRegistry(requestBinderRegistry != null ? requestBinderRegistry : new DefaultRequestBinderRegistry(ConversionService.SHARED)); + this.webSocketStateBinderRegistry = new WebSocketStateBinderRegistry(requestBinderRegistry != null ? requestBinderRegistry : new DefaultRequestBinderRegistry(conversionService), conversionService); String clientPath = webSocketBean.getBeanDefinition().stringValue(ClientWebSocket.class).orElse(""); UriMatchTemplate matchTemplate = UriMatchTemplate.of(clientPath); this.matchInfo = matchTemplate.match(request.getPath()).orElse(null); @@ -282,7 +285,7 @@ protected NettyWebSocketSession createWebSocketSession(ChannelHandlerContext ctx @Override public ConvertibleValues getUriVariables() { if (matchInfo != null) { - return ConvertibleValues.of(matchInfo.getVariableValues()); + return ConvertibleValues.of(matchInfo.getVariableValues(), conversionService); } return ConvertibleValues.empty(); } diff --git a/http-client/src/test/groovy/io/micronaut/http/client/HttpPostSpec.groovy b/http-client/src/test/groovy/io/micronaut/http/client/HttpPostSpec.groovy index 345729153c6..75de09a4989 100644 --- a/http-client/src/test/groovy/io/micronaut/http/client/HttpPostSpec.groovy +++ b/http-client/src/test/groovy/io/micronaut/http/client/HttpPostSpec.groovy @@ -19,6 +19,7 @@ import groovy.transform.EqualsAndHashCode import io.micronaut.context.annotation.Property import io.micronaut.context.annotation.Requires import io.micronaut.core.annotation.Introspected +import io.micronaut.core.convert.ConversionService import io.micronaut.core.type.Argument import io.micronaut.http.HttpRequest import io.micronaut.http.HttpResponse @@ -51,6 +52,9 @@ class HttpPostSpec extends Specification { @Inject PostClient postClient + @Inject + ConversionService conversionService + void "test send invalid http method"() { given: def book = new Book(title: "The Stand", pages: 1000) @@ -383,6 +387,12 @@ class HttpPostSpec extends Specification { when: def request = HttpRequest.POST('/', JsonObject.createObjectNode([:])) + then: + request.getBody(String).get() != '{}' + + when: + request.setConversionService(conversionService) + then: request.getBody(String).get() == '{}' } diff --git a/http-client/src/test/groovy/io/micronaut/http/client/netty/FullNettyClientHttpResponseSpec.groovy b/http-client/src/test/groovy/io/micronaut/http/client/netty/FullNettyClientHttpResponseSpec.groovy index 611e4c9ca4d..1728a0a4475 100644 --- a/http-client/src/test/groovy/io/micronaut/http/client/netty/FullNettyClientHttpResponseSpec.groovy +++ b/http-client/src/test/groovy/io/micronaut/http/client/netty/FullNettyClientHttpResponseSpec.groovy @@ -1,6 +1,6 @@ package io.micronaut.http.client.netty - +import io.micronaut.core.convert.ConversionService import io.micronaut.http.cookie.Cookie import io.micronaut.http.cookie.Cookies import io.netty.handler.codec.http.DefaultFullHttpResponse @@ -22,7 +22,7 @@ class FullNettyClientHttpResponseSpec extends Specification { fullHttpResponse.headers().set(httpHeaders) when: - FullNettyClientHttpResponse response = new FullNettyClientHttpResponse(fullHttpResponse, null, null, null, false) + FullNettyClientHttpResponse response = new FullNettyClientHttpResponse(fullHttpResponse, null, null, null, false, ConversionService.SHARED) then: Cookies cookies = response.getCookies() @@ -45,7 +45,7 @@ class FullNettyClientHttpResponseSpec extends Specification { fullHttpResponse.headers().set(httpHeaders) when: - FullNettyClientHttpResponse response = new FullNettyClientHttpResponse(fullHttpResponse, null, null, null, false) + FullNettyClientHttpResponse response = new FullNettyClientHttpResponse(fullHttpResponse, null, null, null, false, ConversionService.SHARED) then: Cookies cookies = response.getCookies() diff --git a/http-client/src/test/groovy/io/micronaut/http/client/netty/NettyStreamedHttpResponseSpec.groovy b/http-client/src/test/groovy/io/micronaut/http/client/netty/NettyStreamedHttpResponseSpec.groovy index 6eae29b79d9..8d488e29b39 100644 --- a/http-client/src/test/groovy/io/micronaut/http/client/netty/NettyStreamedHttpResponseSpec.groovy +++ b/http-client/src/test/groovy/io/micronaut/http/client/netty/NettyStreamedHttpResponseSpec.groovy @@ -1,5 +1,6 @@ package io.micronaut.http.client.netty +import io.micronaut.core.convert.ConversionService import io.micronaut.http.cookie.Cookie import io.micronaut.http.cookie.Cookies import io.micronaut.http.netty.stream.DefaultStreamedHttpResponse @@ -21,7 +22,7 @@ class NettyStreamedHttpResponseSpec extends Specification { streamedHttpResponse.headers().set(httpHeaders) when: - NettyStreamedHttpResponse response = new NettyStreamedHttpResponse(streamedHttpResponse) + NettyStreamedHttpResponse response = new NettyStreamedHttpResponse(streamedHttpResponse, ConversionService.SHARED) then: Cookies cookies = response.getCookies() @@ -44,7 +45,7 @@ class NettyStreamedHttpResponseSpec extends Specification { streamedHttpResponse.headers().set(httpHeaders) when: - NettyStreamedHttpResponse response = new NettyStreamedHttpResponse(streamedHttpResponse) + NettyStreamedHttpResponse response = new NettyStreamedHttpResponse(streamedHttpResponse, ConversionService.SHARED) then: Cookies cookies = response.getCookies() @@ -68,7 +69,7 @@ class NettyStreamedHttpResponseSpec extends Specification { streamedHttpResponse.headers().set(httpHeaders) when: - NettyStreamedHttpResponse response = new NettyStreamedHttpResponse(streamedHttpResponse) + NettyStreamedHttpResponse response = new NettyStreamedHttpResponse(streamedHttpResponse, ConversionService.SHARED) response.cookie(Cookie.of("ADDED", "xyz").httpOnly(true).domain(".foo.com")) then: diff --git a/http-netty/src/main/java/io/micronaut/http/netty/AbstractNettyHttpRequest.java b/http-netty/src/main/java/io/micronaut/http/netty/AbstractNettyHttpRequest.java index 6d368acb75f..7bdc7a23c33 100644 --- a/http-netty/src/main/java/io/micronaut/http/netty/AbstractNettyHttpRequest.java +++ b/http-netty/src/main/java/io/micronaut/http/netty/AbstractNettyHttpRequest.java @@ -54,7 +54,7 @@ public abstract class AbstractNettyHttpRequest extends DefaultAttributeMap im public static final AsciiString STREAM_ID = HttpConversionUtil.ExtensionHeaderNames.STREAM_ID.text(); public static final AsciiString HTTP2_SCHEME = HttpConversionUtil.ExtensionHeaderNames.SCHEME.text(); protected final io.netty.handler.codec.http.HttpRequest nettyRequest; - protected final ConversionService conversionService; + protected final ConversionService conversionService; protected final HttpMethod httpMethod; protected final URI uri; protected final String httpMethodName; diff --git a/http-netty/src/main/java/io/micronaut/http/netty/NettyHttpHeaders.java b/http-netty/src/main/java/io/micronaut/http/netty/NettyHttpHeaders.java index 668ef5107f6..0a46c1ebb34 100644 --- a/http-netty/src/main/java/io/micronaut/http/netty/NettyHttpHeaders.java +++ b/http-netty/src/main/java/io/micronaut/http/netty/NettyHttpHeaders.java @@ -50,7 +50,7 @@ public class NettyHttpHeaders implements MutableHttpHeaders { io.netty.handler.codec.http.HttpHeaders nettyHeaders; - final ConversionService conversionService; + ConversionService conversionService; /** * @param nettyHeaders The Netty Http headers @@ -233,4 +233,13 @@ public MutableHttpHeaders contentType(MediaType mediaType) { return add(HttpHeaderNames.CONTENT_TYPE, mediaType); } + @Override + public ConversionService getConversionService() { + return conversionService; + } + + @Override + public void setConversionService(ConversionService conversionService) { + this.conversionService = conversionService; + } } diff --git a/http-netty/src/main/java/io/micronaut/http/netty/NettyHttpParameters.java b/http-netty/src/main/java/io/micronaut/http/netty/NettyHttpParameters.java index ccd4ee8cd64..07a811e2062 100644 --- a/http-netty/src/main/java/io/micronaut/http/netty/NettyHttpParameters.java +++ b/http-netty/src/main/java/io/micronaut/http/netty/NettyHttpParameters.java @@ -19,7 +19,6 @@ import io.micronaut.core.annotation.Nullable; import io.micronaut.core.convert.ArgumentConversionContext; import io.micronaut.core.convert.ConversionService; -import io.micronaut.core.convert.value.ConvertibleMultiValues; import io.micronaut.core.convert.value.ConvertibleMultiValuesMap; import io.micronaut.http.MutableHttpParameters; @@ -44,7 +43,7 @@ public class NettyHttpParameters implements MutableHttpParameters { private final LinkedHashMap> valuesMap; - private final ConvertibleMultiValues values; + private final ConvertibleMultiValuesMap values; private final BiConsumer> onChange; /** @@ -53,7 +52,7 @@ public class NettyHttpParameters implements MutableHttpParameters { * @param onChange A callback for changes */ public NettyHttpParameters(Map> parameters, - ConversionService conversionService, + ConversionService conversionService, @Nullable BiConsumer> onChange) { this.valuesMap = new LinkedHashMap<>(parameters.size()); this.values = new ConvertibleMultiValuesMap<>(valuesMap, conversionService); @@ -108,4 +107,14 @@ public MutableHttpParameters add(CharSequence name, List values) { } return this; } + + @Override + public ConversionService getConversionService() { + return values.getConversionService(); + } + + @Override + public void setConversionService(ConversionService conversionService) { + values.setConversionService(conversionService); + } } diff --git a/http-netty/src/main/java/io/micronaut/http/netty/channel/converters/KQueueChannelOptionFactory.java b/http-netty/src/main/java/io/micronaut/http/netty/channel/converters/KQueueChannelOptionFactory.java index 22fbf983444..7f4a00ff0e6 100644 --- a/http-netty/src/main/java/io/micronaut/http/netty/channel/converters/KQueueChannelOptionFactory.java +++ b/http-netty/src/main/java/io/micronaut/http/netty/channel/converters/KQueueChannelOptionFactory.java @@ -18,7 +18,7 @@ import io.micronaut.context.annotation.Requires; import io.micronaut.context.env.Environment; import io.micronaut.core.annotation.Internal; -import io.micronaut.core.convert.ConversionService; +import io.micronaut.core.convert.MutableConversionService; import io.micronaut.core.convert.TypeConverterRegistrar; import io.micronaut.http.netty.channel.KQueueAvailabilityCondition; import io.netty.channel.ChannelOption; @@ -56,7 +56,7 @@ public Object convertValue(ChannelOption option, Object value, Environment en } @Override - public void register(ConversionService conversionService) { + public void register(MutableConversionService conversionService) { conversionService.addConverter( Map.class, AcceptFilter.class, diff --git a/http-netty/src/main/java/io/micronaut/http/netty/cookies/NettyCookies.java b/http-netty/src/main/java/io/micronaut/http/netty/cookies/NettyCookies.java index 7d0207c36a7..9b0364176e5 100644 --- a/http-netty/src/main/java/io/micronaut/http/netty/cookies/NettyCookies.java +++ b/http-netty/src/main/java/io/micronaut/http/netty/cookies/NettyCookies.java @@ -45,7 +45,7 @@ public class NettyCookies implements Cookies { private static final Logger LOG = LoggerFactory.getLogger(NettyCookies.class); - private final ConversionService conversionService; + private final ConversionService conversionService; private final Map cookies; /** @@ -132,4 +132,9 @@ public Optional get(CharSequence name, ArgumentConversionContext conve public Collection values() { return Collections.unmodifiableCollection(cookies.values()); } + + @Override + public ConversionService getConversionService() { + return conversionService; + } } diff --git a/http-netty/src/main/java/io/micronaut/http/netty/websocket/AbstractNettyWebSocketHandler.java b/http-netty/src/main/java/io/micronaut/http/netty/websocket/AbstractNettyWebSocketHandler.java index 405515fba9c..59a2a08d763 100644 --- a/http-netty/src/main/java/io/micronaut/http/netty/websocket/AbstractNettyWebSocketHandler.java +++ b/http-netty/src/main/java/io/micronaut/http/netty/websocket/AbstractNettyWebSocketHandler.java @@ -94,10 +94,11 @@ public abstract class AbstractNettyWebSocketHandler extends SimpleChannelInbound protected final WebSocketVersion webSocketVersion; protected final String subProtocol; protected final WebSocketSessionRepository webSocketSessionRepository; + protected final ConversionService conversionService; private final Argument bodyArgument; private final Argument pongArgument; private final AtomicBoolean closed = new AtomicBoolean(false); - private AtomicReference frameBuffer = new AtomicReference<>(); + private final AtomicReference frameBuffer = new AtomicReference<>(); /** * Default constructor. @@ -111,6 +112,7 @@ public abstract class AbstractNettyWebSocketHandler extends SimpleChannelInbound * @param version The websocket version being used * @param subProtocol The handler sub-protocol * @param webSocketSessionRepository The web socket repository if they are supported (like on the server), null otherwise + * @param conversionService The conversion service */ protected AbstractNettyWebSocketHandler( ChannelHandlerContext ctx, @@ -121,10 +123,11 @@ protected AbstractNettyWebSocketHandler( Map uriVariables, WebSocketVersion version, String subProtocol, - WebSocketSessionRepository webSocketSessionRepository) { + WebSocketSessionRepository webSocketSessionRepository, + ConversionService conversionService) { this.subProtocol = subProtocol; this.webSocketSessionRepository = webSocketSessionRepository; - this.webSocketBinder = new WebSocketStateBinderRegistry(binderRegistry); + this.webSocketBinder = new WebSocketStateBinderRegistry(binderRegistry, conversionService); this.uriVariables = uriVariables; this.webSocketBean = webSocketBean; this.originatingRequest = request; @@ -133,6 +136,7 @@ protected AbstractNettyWebSocketHandler( this.mediaTypeCodecRegistry = mediaTypeCodecRegistry; this.webSocketVersion = version; this.session = createWebSocketSession(ctx); + this.conversionService = conversionService; if (session != null) { @@ -402,7 +406,7 @@ protected void handleWebSocketFrame(ChannelHandlerContext ctx, WebSocketFrame ms } Argument bodyArgument = this.getBodyArgument(); - Optional converted = ConversionService.SHARED.convert(content, bodyArgument); + Optional converted = conversionService.convert(content, bodyArgument); content.release(); if (!converted.isPresent()) { diff --git a/http-server-netty/build.gradle b/http-server-netty/build.gradle index a568bdf19f5..09cc6f9c129 100644 --- a/http-server-netty/build.gradle +++ b/http-server-netty/build.gradle @@ -42,11 +42,12 @@ dependencies { testImplementation libs.bcpkix } - testImplementation(libs.managed.micronaut.xml) { - exclude module:'micronaut-inject' - exclude module:'micronaut-http' - exclude module:'micronaut-bom' - } +// Add Micronaut Jackson XML after v4 Migration +// testImplementation(libs.managed.micronaut.xml) { +// exclude module:'micronaut-inject' +// exclude module:'micronaut-http' +// exclude module:'micronaut-bom' +// } testImplementation libs.managed.jackson.databind // http impls for tests diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/HttpPipelineBuilder.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/HttpPipelineBuilder.java index 22920c87aee..c8c75603475 100644 --- a/http-server-netty/src/main/java/io/micronaut/http/server/netty/HttpPipelineBuilder.java +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/HttpPipelineBuilder.java @@ -132,8 +132,8 @@ final class HttpPipelineBuilder { embeddedServices.getEventPublisher(HttpRequestReceivedEvent.class)); responseEncoder = new HttpResponseEncoder( embeddedServices.getMediaTypeCodecRegistry(), - server.getServerConfiguration() - ); + server.getServerConfiguration(), + embeddedServices.getApplicationContext().getConversionService()); } boolean supportsSsl() { diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/NettyEmbeddedServerInstance.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/NettyEmbeddedServerInstance.java index e2f4a0c1276..66b2963789e 100644 --- a/http-server-netty/src/main/java/io/micronaut/http/server/netty/NettyEmbeddedServerInstance.java +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/NettyEmbeddedServerInstance.java @@ -20,6 +20,7 @@ import io.micronaut.context.annotation.Prototype; import io.micronaut.context.env.Environment; import io.micronaut.core.annotation.Internal; +import io.micronaut.core.convert.ConversionService; import io.micronaut.core.convert.value.ConvertibleValues; import io.micronaut.core.util.CollectionUtils; import io.micronaut.discovery.EmbeddedServerInstance; @@ -48,6 +49,7 @@ class NettyEmbeddedServerInstance implements EmbeddedServerInstance { private final Environment environment; private final List metadataContributors; private final BeanLocator beanLocator; + private final ConversionService conversionService; private ConvertibleValues instanceMetadata; @@ -57,19 +59,21 @@ class NettyEmbeddedServerInstance implements EmbeddedServerInstance { * @param environment The Environment * @param beanLocator The bean locator * @param metadataContributors The {@link ServiceInstanceMetadataContributor} + * @param conversionService The conversion service */ NettyEmbeddedServerInstance( - @Parameter String id, - @Parameter NettyHttpServer nettyHttpServer, - Environment environment, - BeanLocator beanLocator, - List metadataContributors) { + @Parameter String id, + @Parameter NettyHttpServer nettyHttpServer, + Environment environment, + BeanLocator beanLocator, + List metadataContributors, ConversionService conversionService) { this.id = id; this.nettyHttpServer = nettyHttpServer; this.environment = environment; this.beanLocator = beanLocator; this.metadataContributors = metadataContributors; + this.conversionService = conversionService; } @Override @@ -108,7 +112,7 @@ public ConvertibleValues getMetadata() { if (cloudMetadata != null) { cloudMetadata.putAll(metadata); } - instanceMetadata = ConvertibleValues.of(cloudMetadata); + instanceMetadata = ConvertibleValues.of(cloudMetadata, conversionService); } return instanceMetadata; } diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/NettyHttpRequest.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/NettyHttpRequest.java index fe7def6f3dd..f2b245a75cc 100644 --- a/http-server-netty/src/main/java/io/micronaut/http/server/netty/NettyHttpRequest.java +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/NettyHttpRequest.java @@ -627,9 +627,18 @@ public Optional convert(Argument valueType, Object value) { private class NettyMutableHttpRequest implements MutableHttpRequest, NettyHttpRequestBuilder { private URI uri = NettyHttpRequest.this.uri; + @Nullable private MutableHttpParameters httpParameters; + @Nullable private Object body; + @Override + public void setConversionService(ConversionService conversionService) { + if (httpParameters != null) { + httpParameters.setConversionService(conversionService); + } + } + @Override public MutableHttpRequest cookie(Cookie cookie) { if (cookie instanceof NettyCookie) { diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/NettyHttpServer.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/NettyHttpServer.java index 21dd006bdf5..7a31e8a0aa8 100644 --- a/http-server-netty/src/main/java/io/micronaut/http/server/netty/NettyHttpServer.java +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/NettyHttpServer.java @@ -176,7 +176,8 @@ public NettyHttpServer( nettyEmbeddedServices, ioExecutor, httpContentProcessorResolver, - httpRequestTerminatedEventPublisher + httpRequestTerminatedEventPublisher, + applicationContext.getConversionService() ); this.hostResolver = new DefaultHttpHostResolver(serverConfiguration, () -> NettyHttpServer.this); diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/RoutingInBoundHandler.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/RoutingInBoundHandler.java index 1aa25b728bb..42871f0a081 100644 --- a/http-server-netty/src/main/java/io/micronaut/http/server/netty/RoutingInBoundHandler.java +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/RoutingInBoundHandler.java @@ -158,6 +158,7 @@ class RoutingInBoundHandler extends SimpleChannelInboundHandler terminateEventPublisher; private final RouteExecutor routeExecutor; + private final ConversionService conversionService; /** * @param customizableResponseTypeHandlerRegistry The customizable response type handler registry @@ -166,6 +167,7 @@ class RoutingInBoundHandler extends SimpleChannelInboundHandler ioExecutor, HttpContentProcessorResolver httpContentProcessorResolver, - ApplicationEventPublisher terminateEventPublisher) { + ApplicationEventPublisher terminateEventPublisher, + ConversionService conversionService) { this.mediaTypeCodecRegistry = embeddedServerContext.getMediaTypeCodecRegistry(); this.customizableResponseTypeHandlerRegistry = customizableResponseTypeHandlerRegistry; this.staticResourceResolver = embeddedServerContext.getStaticResourceResolver(); @@ -186,6 +189,7 @@ class RoutingInBoundHandler extends SimpleChannelInboundHandler multipartEnabled = serverConfiguration.getMultipart().getEnabled(); this.multipartEnabled = !multipartEnabled.isPresent() || multipartEnabled.get(); this.routeExecutor = embeddedServerContext.getRouteExecutor(); + this.conversionService = conversionService; } @Override @@ -395,7 +399,6 @@ private Subscriber buildSubscriber(NettyHttpRequest request, final ConcurrentHashMap> subjectsByDataName = new ConcurrentHashMap<>(); final Collection> downstreamSubscribers = Collections.synchronizedList(new ArrayList<>()); final ConcurrentHashMap dataReferences = new ConcurrentHashMap<>(); - final ConversionService conversionService = ConversionService.SHARED; Subscription s; final LongConsumer onRequest = num -> pressureRequested.updateAndGet(p -> { long newVal = p - num; @@ -832,7 +835,7 @@ private Flux mapToHttpContent(NettyHttpRequest request, } else { MediaTypeCodec codec = mediaTypeCodecRegistry.findCodec(finalMediaType, message.getClass()).orElse( - new TextPlainCodec(serverConfiguration.getDefaultCharset())); + new TextPlainCodec(serverConfiguration.getDefaultCharset(), conversionService)); if (LOG.isTraceEnabled()) { LOG.trace("Encoding emitted response object [{}] using codec: {}", message, codec); @@ -931,7 +934,7 @@ private void encodeResponseBody( MediaTypeCodec codec = registeredCodec.get(); encodeBodyWithCodec(message, bodyType, body, codec, context, request); } else { - MediaTypeCodec defaultCodec = new TextPlainCodec(serverConfiguration.getDefaultCharset()); + MediaTypeCodec defaultCodec = new TextPlainCodec(serverConfiguration.getDefaultCharset(), conversionService); encodeBodyWithCodec(message, bodyType, body, defaultCodec, context, request); } } @@ -1091,10 +1094,10 @@ private NettyMutableHttpResponse createNettyResponse(HttpResponse message) io.netty.handler.codec.http.HttpHeaders nettyHeaders = new DefaultHttpHeaders(serverConfiguration.isValidateHeaders()); message.getHeaders().forEach((BiConsumer>) nettyHeaders::set); return new NettyMutableHttpResponse<>( - HttpVersion.HTTP_1_1, - HttpResponseStatus.valueOf(message.code(), message.reason()), - body instanceof ByteBuf ? body : null, - ConversionService.SHARED + HttpVersion.HTTP_1_1, + HttpResponseStatus.valueOf(message.code(), message.reason()), + body instanceof ByteBuf ? body : null, + conversionService ); } diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/binders/NettyBinderRegistrar.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/binders/NettyBinderRegistrar.java index d24acc1e0e0..07374ae7b8f 100644 --- a/http-server-netty/src/main/java/io/micronaut/http/server/netty/binders/NettyBinderRegistrar.java +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/binders/NettyBinderRegistrar.java @@ -20,7 +20,6 @@ import io.micronaut.context.event.BeanCreatedEvent; import io.micronaut.context.event.BeanCreatedEventListener; import io.micronaut.core.annotation.Internal; -import io.micronaut.core.annotation.Nullable; import io.micronaut.core.convert.ConversionService; import io.micronaut.http.bind.RequestBinderRegistry; import io.micronaut.http.server.HttpServerConfiguration; @@ -42,7 +41,7 @@ @Internal class NettyBinderRegistrar implements BeanCreatedEventListener { - private final ConversionService conversionService; + private final ConversionService conversionService; private final HttpContentProcessorResolver httpContentProcessorResolver; private final BeanLocator beanLocator; private final BeanProvider httpServerConfiguration; @@ -57,13 +56,12 @@ class NettyBinderRegistrar implements BeanCreatedEventListener conversionService, - HttpContentProcessorResolver httpContentProcessorResolver, - BeanLocator beanLocator, - BeanProvider httpServerConfiguration, - @Named(TaskExecutors.IO) BeanProvider executorService) { - this.conversionService = conversionService == null ? ConversionService.SHARED : conversionService; + NettyBinderRegistrar(ConversionService conversionService, + HttpContentProcessorResolver httpContentProcessorResolver, + BeanLocator beanLocator, + BeanProvider httpServerConfiguration, + @Named(TaskExecutors.IO) BeanProvider executorService) { + this.conversionService = conversionService; this.httpContentProcessorResolver = httpContentProcessorResolver; this.beanLocator = beanLocator; this.httpServerConfiguration = httpServerConfiguration; diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/converters/NettyConverters.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/converters/NettyConverters.java index 78e0de6ccf3..74c9f9f7700 100644 --- a/http-server-netty/src/main/java/io/micronaut/http/server/netty/converters/NettyConverters.java +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/converters/NettyConverters.java @@ -18,6 +18,7 @@ import io.micronaut.context.BeanProvider; import io.micronaut.core.annotation.Internal; import io.micronaut.core.convert.ConversionService; +import io.micronaut.core.convert.MutableConversionService; import io.micronaut.core.convert.TypeConverter; import io.micronaut.core.convert.TypeConverterRegistrar; import io.micronaut.core.naming.NameUtils; @@ -50,7 +51,7 @@ @Internal public class NettyConverters implements TypeConverterRegistrar { - private final ConversionService conversionService; + private final ConversionService conversionService; private final BeanProvider decoderRegistryProvider; private final ChannelOptionFactory channelOptionFactory; @@ -60,7 +61,7 @@ public class NettyConverters implements TypeConverterRegistrar { * @param decoderRegistryProvider The decoder registry provider * @param channelOptionFactory The decoder channel option factory */ - public NettyConverters(ConversionService conversionService, + public NettyConverters(ConversionService conversionService, //Prevent early initialization of the codecs BeanProvider decoderRegistryProvider, ChannelOptionFactory channelOptionFactory) { @@ -70,7 +71,7 @@ public NettyConverters(ConversionService conversionService, } @Override - public void register(ConversionService conversionService) { + public void register(MutableConversionService conversionService) { conversionService.addConverter( CharSequence.class, ChannelOption.class, diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/converters/NettyConvertersSpi.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/converters/NettyConvertersSpi.java index 683d0811a6b..3c9ec4e1161 100644 --- a/http-server-netty/src/main/java/io/micronaut/http/server/netty/converters/NettyConvertersSpi.java +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/converters/NettyConvertersSpi.java @@ -16,7 +16,7 @@ package io.micronaut.http.server.netty.converters; import io.micronaut.core.annotation.Internal; -import io.micronaut.core.convert.ConversionService; +import io.micronaut.core.convert.MutableConversionService; import io.micronaut.core.convert.TypeConverter; import io.micronaut.core.convert.TypeConverterRegistrar; import io.micronaut.http.multipart.CompletedFileUpload; @@ -47,7 +47,7 @@ @Internal public final class NettyConvertersSpi implements TypeConverterRegistrar { @Override - public void register(ConversionService conversionService) { + public void register(MutableConversionService conversionService) { conversionService.addConverter( ByteBuf.class, CharSequence.class, diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/decoders/HttpRequestDecoder.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/decoders/HttpRequestDecoder.java index 3d3fb12b6ef..c86816d4ce5 100644 --- a/http-server-netty/src/main/java/io/micronaut/http/server/netty/decoders/HttpRequestDecoder.java +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/decoders/HttpRequestDecoder.java @@ -54,7 +54,7 @@ public class HttpRequestDecoder extends MessageToMessageDecoder imp private static final Logger LOG = LoggerFactory.getLogger(NettyHttpServer.class); private final EmbeddedServer embeddedServer; - private final ConversionService conversionService; + private final ConversionService conversionService; private final HttpServerConfiguration configuration; private final ApplicationEventPublisher httpRequestReceivedEventPublisher; @@ -64,7 +64,7 @@ public class HttpRequestDecoder extends MessageToMessageDecoder imp * @param configuration The Http server configuration * @param httpRequestReceivedEventPublisher The publisher of {@link HttpRequestReceivedEvent} */ - public HttpRequestDecoder(EmbeddedServer embeddedServer, ConversionService conversionService, HttpServerConfiguration configuration, ApplicationEventPublisher httpRequestReceivedEventPublisher) { + public HttpRequestDecoder(EmbeddedServer embeddedServer, ConversionService conversionService, HttpServerConfiguration configuration, ApplicationEventPublisher httpRequestReceivedEventPublisher) { this.embeddedServer = embeddedServer; this.conversionService = conversionService; this.configuration = configuration; diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/encoders/HttpResponseEncoder.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/encoders/HttpResponseEncoder.java index 7110153926d..16fea285227 100644 --- a/http-server-netty/src/main/java/io/micronaut/http/server/netty/encoders/HttpResponseEncoder.java +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/encoders/HttpResponseEncoder.java @@ -17,6 +17,7 @@ import io.micronaut.buffer.netty.NettyByteBufferFactory; import io.micronaut.core.annotation.Internal; +import io.micronaut.core.convert.ConversionService; import io.micronaut.core.io.Writable; import io.micronaut.core.io.buffer.ByteBuffer; import io.micronaut.http.HttpHeaders; @@ -64,16 +65,19 @@ public class HttpResponseEncoder extends MessageToMessageEncoder resp response = encodeBodyWithCodec(response, body, codec, responseMediaType, context); } - MediaTypeCodec defaultCodec = new TextPlainCodec(serverConfiguration.getDefaultCharset()); + MediaTypeCodec defaultCodec = new TextPlainCodec(serverConfiguration.getDefaultCharset(), conversionService); response = encodeBodyWithCodec(response, body, defaultCodec, responseMediaType, context); } diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/websocket/NettyServerWebSocketHandler.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/websocket/NettyServerWebSocketHandler.java index 2f40262ab9f..31963261aeb 100644 --- a/http-server-netty/src/main/java/io/micronaut/http/server/netty/websocket/NettyServerWebSocketHandler.java +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/websocket/NettyServerWebSocketHandler.java @@ -111,7 +111,8 @@ public class NettyServerWebSocketHandler extends AbstractNettyWebSocketHandler { routeMatch.getVariableValues(), handshaker.version(), handshaker.selectedSubprotocol(), - webSocketSessionRepository); + webSocketSessionRepository, + nettyEmbeddedServices.getApplicationContext().getConversionService()); this.nettyEmbeddedServices = nettyEmbeddedServices; this.coroutineHelper = coroutineHelper; diff --git a/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/ContentNegotiationSpec.groovy b/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/ContentNegotiationSpec.groovy index 7fa8e5f935a..13c03141258 100644 --- a/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/ContentNegotiationSpec.groovy +++ b/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/ContentNegotiationSpec.groovy @@ -45,8 +45,9 @@ class ContentNegotiationSpec extends Specification { [new MediaType("application/json;q=0.5"), new MediaType("application/xml;q=0.9")] | XML [MediaType.APPLICATION_JSON_TYPE] | JSON [MediaType.APPLICATION_JSON_TYPE, MediaType.APPLICATION_XML_TYPE] | JSON - [MediaType.APPLICATION_XML_TYPE, MediaType.APPLICATION_JSON_TYPE] | XML - [MediaType.APPLICATION_XML_TYPE] | XML +// Add Micronaut Jackson XML after v4 Migration +// [MediaType.APPLICATION_XML_TYPE, MediaType.APPLICATION_JSON_TYPE] | XML +// [MediaType.APPLICATION_XML_TYPE] | XML [MediaType.TEXT_PLAIN_TYPE] | TEXT [MediaType.ALL_TYPE] | JSON @@ -72,7 +73,8 @@ class ContentNegotiationSpec extends Specification { contentType | expectedContentType | expectedBody null | MediaType.APPLICATION_JSON_TYPE | '{"name":"Fred","age":10}' MediaType.APPLICATION_JSON_TYPE | MediaType.APPLICATION_JSON_TYPE | '{"name":"Fred","age":10}' - MediaType.APPLICATION_XML_TYPE | MediaType.APPLICATION_XML_TYPE | 'Fred10' +// Add Micronaut Jackson XML after v4 Migration +// MediaType.APPLICATION_XML_TYPE | MediaType.APPLICATION_XML_TYPE | 'Fred10' } void "test send unacceptable type"() { diff --git a/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/binding/NettyHttpRequestSpec.groovy b/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/binding/NettyHttpRequestSpec.groovy index b211faf817e..ccaf8fc33a9 100644 --- a/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/binding/NettyHttpRequestSpec.groovy +++ b/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/binding/NettyHttpRequestSpec.groovy @@ -15,7 +15,7 @@ */ package io.micronaut.http.server.netty.binding -import io.micronaut.core.convert.DefaultConversionService +import io.micronaut.core.convert.DefaultMutableConversionService import io.micronaut.http.HttpHeaders import io.micronaut.http.HttpMethod import io.micronaut.http.MutableHttpRequest @@ -37,7 +37,7 @@ class NettyHttpRequestSpec extends Specification { void "test mutate request"() { given: DefaultFullHttpRequest nettyRequest = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, GET, "/foo/bar") - NettyHttpRequest request = new NettyHttpRequest(nettyRequest, Mock(ChannelHandlerContext), new DefaultConversionService(), new HttpServerConfiguration()) + NettyHttpRequest request = new NettyHttpRequest(nettyRequest, Mock(ChannelHandlerContext), new DefaultMutableConversionService(), new HttpServerConfiguration()) when: def altered = request.mutate() @@ -52,7 +52,7 @@ class NettyHttpRequestSpec extends Specification { void "test mutating a mutable request"() { given: DefaultFullHttpRequest nettyRequest = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, GET, "/foo/bar") - def request = new NettyHttpRequest(nettyRequest, Mock(ChannelHandlerContext), new DefaultConversionService(), new HttpServerConfiguration()) + def request = new NettyHttpRequest(nettyRequest, Mock(ChannelHandlerContext), new DefaultMutableConversionService(), new HttpServerConfiguration()) when: request = request.mutate() @@ -70,7 +70,7 @@ class NettyHttpRequestSpec extends Specification { void "test netty http request parameters"() { given: DefaultFullHttpRequest nettyRequest = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, method, uri) - NettyHttpRequest request = new NettyHttpRequest(nettyRequest, Mock(ChannelHandlerContext), new DefaultConversionService(), new HttpServerConfiguration()) + NettyHttpRequest request = new NettyHttpRequest(nettyRequest, Mock(ChannelHandlerContext), new DefaultMutableConversionService(), new HttpServerConfiguration()) String fullURI = request.uri.toString() String expectedPath = fullURI.indexOf('?') > -1 ? fullURI.substring(0, fullURI.indexOf('?')) : fullURI @@ -93,7 +93,7 @@ class NettyHttpRequestSpec extends Specification { nettyRequest.headers().add(header.key.toString(), header.value) } - NettyHttpRequest request = new NettyHttpRequest(nettyRequest, Mock(ChannelHandlerContext), new DefaultConversionService(), new HttpServerConfiguration()) + NettyHttpRequest request = new NettyHttpRequest(nettyRequest, Mock(ChannelHandlerContext), new DefaultMutableConversionService(), new HttpServerConfiguration()) String fullURI = request.uri.toString() String expectedPath = fullURI.indexOf('?') > -1 ? fullURI.substring(0, fullURI.indexOf('?')) : fullURI @@ -115,7 +115,7 @@ class NettyHttpRequestSpec extends Specification { nettyRequest.headers().add(header.key.toString(), header.value) } - NettyHttpRequest request = new NettyHttpRequest(nettyRequest, Mock(ChannelHandlerContext), new DefaultConversionService(), new HttpServerConfiguration()) + NettyHttpRequest request = new NettyHttpRequest(nettyRequest, Mock(ChannelHandlerContext), new DefaultMutableConversionService(), new HttpServerConfiguration()) String fullURI = request.uri.toString() String expectedPath = fullURI.indexOf('?') > -1 ? fullURI.substring(0, fullURI.indexOf('?')) : fullURI @@ -140,7 +140,7 @@ class NettyHttpRequestSpec extends Specification { nettyRequest.headers().add(header.key.toString(), header.value) } - NettyHttpRequest request = new NettyHttpRequest(nettyRequest, Mock(ChannelHandlerContext), new DefaultConversionService(), new HttpServerConfiguration()) + NettyHttpRequest request = new NettyHttpRequest(nettyRequest, Mock(ChannelHandlerContext), new DefaultMutableConversionService(), new HttpServerConfiguration()) String fullURI = request.uri.toString() String expectedPath = fullURI.indexOf('?') > -1 ? fullURI.substring(0, fullURI.indexOf('?')) : fullURI diff --git a/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/binding/NettyHttpResponseSpec.groovy b/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/binding/NettyHttpResponseSpec.groovy index fa128a5454f..8405902a017 100644 --- a/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/binding/NettyHttpResponseSpec.groovy +++ b/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/binding/NettyHttpResponseSpec.groovy @@ -15,7 +15,7 @@ */ package io.micronaut.http.server.netty.binding -import io.micronaut.core.convert.DefaultConversionService +import io.micronaut.core.convert.DefaultMutableConversionService import io.micronaut.http.HttpHeaders import io.micronaut.http.HttpStatus import io.micronaut.http.MutableHttpResponse @@ -37,7 +37,7 @@ class NettyHttpResponseSpec extends Specification { void "test add headers"() { given: DefaultFullHttpResponse nettyResponse = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK) - NettyMutableHttpResponse response = new NettyMutableHttpResponse(nettyResponse, new DefaultConversionService()) + NettyMutableHttpResponse response = new NettyMutableHttpResponse(nettyResponse, new DefaultMutableConversionService()) response.status(HttpStatus."$status") response.headers.add(header, value) @@ -54,7 +54,7 @@ class NettyHttpResponseSpec extends Specification { void "test add simple cookie"() { given: DefaultFullHttpResponse nettyResponse = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK) - MutableHttpResponse response = new NettyMutableHttpResponse(nettyResponse, new DefaultConversionService()) + MutableHttpResponse response = new NettyMutableHttpResponse(nettyResponse, new DefaultMutableConversionService()) response.status(HttpStatus."$status") response.cookie(Cookie.of("foo", "bar")) @@ -71,7 +71,7 @@ class NettyHttpResponseSpec extends Specification { void "test add cookie with max age"() { given: DefaultFullHttpResponse nettyResponse = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK) - MutableHttpResponse response = new NettyMutableHttpResponse(nettyResponse, new DefaultConversionService()) + MutableHttpResponse response = new NettyMutableHttpResponse(nettyResponse, new DefaultMutableConversionService()) response.status(HttpStatus."$status") response.cookie(Cookie.of("foo", "bar").maxAge(Duration.ofHours(2))) @@ -89,7 +89,7 @@ class NettyHttpResponseSpec extends Specification { void "test multiple cookies"() { given: DefaultFullHttpResponse nettyResponse = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK) - MutableHttpResponse response = new NettyMutableHttpResponse(nettyResponse, new DefaultConversionService()) + MutableHttpResponse response = new NettyMutableHttpResponse(nettyResponse, new DefaultMutableConversionService()) response.cookies([Cookie.of("a", "b"), Cookie.of("c", "d")] as Set) diff --git a/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/converters/ConverterRegistrySpec.groovy b/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/converters/ConverterRegistrySpec.groovy index 3bf9ecfd59a..d0fc17f93d0 100644 --- a/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/converters/ConverterRegistrySpec.groovy +++ b/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/converters/ConverterRegistrySpec.groovy @@ -21,7 +21,6 @@ import io.netty.buffer.ByteBuf import io.netty.buffer.CompositeByteBuf import io.netty.buffer.Unpooled import io.netty.channel.ChannelOption -import spock.lang.PendingFeature import spock.lang.Specification import java.nio.charset.StandardCharsets @@ -67,7 +66,6 @@ class ConverterRegistrySpec extends Specification { ctx1.close() } - @PendingFeature def "test convert string to channel option after context reset"() { given: ApplicationContext ctx1 = ApplicationContext.run() @@ -82,7 +80,6 @@ class ConverterRegistrySpec extends Specification { ctx2.stop() then: - // this fails, the converter has been removed by ctx2.stop() ctx1.getBean(ConversionService).convert("AUTO_READ", ChannelOption).get() == ChannelOption.AUTO_READ cleanup: diff --git a/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/util/MockHttpHeaders.java b/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/util/MockHttpHeaders.java index 02566db55e9..bade4c95f11 100644 --- a/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/util/MockHttpHeaders.java +++ b/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/util/MockHttpHeaders.java @@ -32,6 +32,7 @@ public class MockHttpHeaders implements MutableHttpHeaders { private final Map> headers; + private ConversionService conversionService = ConversionService.SHARED; public MockHttpHeaders(Map> headers) { this.headers = headers; @@ -88,6 +89,11 @@ public Collection> values() { @Override public Optional get(CharSequence name, ArgumentConversionContext conversionContext) { - return ConversionService.SHARED.convert(get(name), conversionContext); + return conversionService.convert(get(name), conversionContext); + } + + @Override + public void setConversionService(ConversionService conversionService) { + this.conversionService = conversionService; } } diff --git a/http-server/src/main/java/io/micronaut/http/server/websocket/ServerWebSocketProcessor.java b/http-server/src/main/java/io/micronaut/http/server/websocket/ServerWebSocketProcessor.java index 53aa4cb94fa..913fc9c1182 100644 --- a/http-server/src/main/java/io/micronaut/http/server/websocket/ServerWebSocketProcessor.java +++ b/http-server/src/main/java/io/micronaut/http/server/websocket/ServerWebSocketProcessor.java @@ -50,7 +50,7 @@ public class ServerWebSocketProcessor extends DefaultRouteBuilder implements Exe * @param uriNamingStrategy The {@link io.micronaut.web.router.RouteBuilder.UriNamingStrategy} * @param conversionService The {@link ConversionService} */ - ServerWebSocketProcessor(ExecutionHandleLocator executionHandleLocator, UriNamingStrategy uriNamingStrategy, ConversionService conversionService) { + ServerWebSocketProcessor(ExecutionHandleLocator executionHandleLocator, UriNamingStrategy uriNamingStrategy, ConversionService conversionService) { super(executionHandleLocator, uriNamingStrategy, conversionService); } diff --git a/http-server/src/test/groovy/io/micronaut/http/server/util/MockHttpHeaders.java b/http-server/src/test/groovy/io/micronaut/http/server/util/MockHttpHeaders.java index cb395d5a189..3942ae6f3d8 100644 --- a/http-server/src/test/groovy/io/micronaut/http/server/util/MockHttpHeaders.java +++ b/http-server/src/test/groovy/io/micronaut/http/server/util/MockHttpHeaders.java @@ -32,6 +32,7 @@ public class MockHttpHeaders implements MutableHttpHeaders { private final Map> headers; + private ConversionService conversionService = ConversionService.SHARED; public MockHttpHeaders(Map> headers) { this.headers = headers; @@ -90,4 +91,9 @@ public Collection> values() { public Optional get(CharSequence name, ArgumentConversionContext conversionContext) { return ConversionService.SHARED.convert(get(name), conversionContext); } + + @Override + public void setConversionService(ConversionService conversionService) { + this.conversionService = conversionService; + } } diff --git a/http-validation/src/main/java/io/micronaut/validation/routes/RouteValidationVisitor.java b/http-validation/src/main/java/io/micronaut/validation/routes/RouteValidationVisitor.java index 4256cb7719f..9c188bf7afd 100644 --- a/http-validation/src/main/java/io/micronaut/validation/routes/RouteValidationVisitor.java +++ b/http-validation/src/main/java/io/micronaut/validation/routes/RouteValidationVisitor.java @@ -21,13 +21,17 @@ import io.micronaut.context.env.DefaultPropertyPlaceholderResolver.Segment; import io.micronaut.core.annotation.AnnotationValue; import io.micronaut.core.annotation.NonNull; -import io.micronaut.core.convert.DefaultConversionService; +import io.micronaut.core.convert.ConversionService; import io.micronaut.core.util.CollectionUtils; import io.micronaut.http.uri.UriMatchTemplate; import io.micronaut.inject.ast.MethodElement; import io.micronaut.inject.visitor.TypeElementVisitor; import io.micronaut.inject.visitor.VisitorContext; -import io.micronaut.validation.routes.rules.*; +import io.micronaut.validation.routes.rules.ClientTypesRule; +import io.micronaut.validation.routes.rules.MissingParameterRule; +import io.micronaut.validation.routes.rules.NullableParameterRule; +import io.micronaut.validation.routes.rules.RequestBeanParameterRule; +import io.micronaut.validation.routes.rules.RouteValidationRule; import javax.annotation.processing.SupportedOptions; import java.util.ArrayList; @@ -51,7 +55,7 @@ public class RouteValidationVisitor implements TypeElementVisitor rules = new ArrayList<>(); private boolean skipValidation = false; - private final DefaultPropertyPlaceholderResolver resolver = new DefaultPropertyPlaceholderResolver(null, new DefaultConversionService()); + private final DefaultPropertyPlaceholderResolver resolver = new DefaultPropertyPlaceholderResolver(null, ConversionService.SHARED); @NonNull @Override diff --git a/http/src/main/java/io/micronaut/http/MediaTypeConvertersRegistrar.java b/http/src/main/java/io/micronaut/http/MediaTypeConvertersRegistrar.java index 5d4ed613bef..5dbd5fc0305 100644 --- a/http/src/main/java/io/micronaut/http/MediaTypeConvertersRegistrar.java +++ b/http/src/main/java/io/micronaut/http/MediaTypeConvertersRegistrar.java @@ -16,10 +16,12 @@ package io.micronaut.http; import io.micronaut.core.annotation.Internal; -import io.micronaut.core.convert.ConversionService; +import io.micronaut.core.convert.MutableConversionService; import io.micronaut.core.convert.TypeConverterRegistrar; import io.micronaut.core.util.StringUtils; +import java.util.Optional; + /** * The media type converters registrar. * @@ -30,13 +32,18 @@ public final class MediaTypeConvertersRegistrar implements TypeConverterRegistrar { @Override - public void register(ConversionService conversionService) { - conversionService.addConverter(CharSequence.class, MediaType.class, charSequence -> { - if (StringUtils.isNotEmpty(charSequence)) { - return MediaType.of(charSequence.toString()); + public void register(MutableConversionService conversionService) { + conversionService.addConverter(CharSequence.class, MediaType.class, (object, targetType, context) -> { + if (StringUtils.isEmpty(object)) { + return Optional.empty(); + } else { + try { + return Optional.of(MediaType.of(object.toString())); + } catch (IllegalArgumentException e) { + context.reject(e); + return Optional.empty(); } - return null; } - ); + }); } } diff --git a/http/src/main/java/io/micronaut/http/MutableHttpHeaders.java b/http/src/main/java/io/micronaut/http/MutableHttpHeaders.java index 6181984c451..3252d303ca5 100644 --- a/http/src/main/java/io/micronaut/http/MutableHttpHeaders.java +++ b/http/src/main/java/io/micronaut/http/MutableHttpHeaders.java @@ -15,6 +15,7 @@ */ package io.micronaut.http; +import io.micronaut.core.convert.ConversionServiceAware; import io.micronaut.core.type.MutableHeaders; import java.net.URI; @@ -35,7 +36,7 @@ * @author Graeme Rocher * @since 1.0 */ -public interface MutableHttpHeaders extends MutableHeaders, HttpHeaders { +public interface MutableHttpHeaders extends MutableHeaders, HttpHeaders, ConversionServiceAware { /** * The default GMT zone for date values. diff --git a/http/src/main/java/io/micronaut/http/MutableHttpParameters.java b/http/src/main/java/io/micronaut/http/MutableHttpParameters.java index 240e87c67c7..4960b76d730 100644 --- a/http/src/main/java/io/micronaut/http/MutableHttpParameters.java +++ b/http/src/main/java/io/micronaut/http/MutableHttpParameters.java @@ -15,6 +15,8 @@ */ package io.micronaut.http; +import io.micronaut.core.convert.ConversionServiceAware; + import java.util.Collections; import java.util.List; @@ -24,7 +26,7 @@ * @author Vladimir Orany * @since 1.0 */ -public interface MutableHttpParameters extends HttpParameters { +public interface MutableHttpParameters extends HttpParameters, ConversionServiceAware { /** * Adds a new http parameter. diff --git a/http/src/main/java/io/micronaut/http/MutableHttpRequest.java b/http/src/main/java/io/micronaut/http/MutableHttpRequest.java index 553a495dc52..96e9ae0f554 100644 --- a/http/src/main/java/io/micronaut/http/MutableHttpRequest.java +++ b/http/src/main/java/io/micronaut/http/MutableHttpRequest.java @@ -16,6 +16,7 @@ package io.micronaut.http; import io.micronaut.core.annotation.NonNull; +import io.micronaut.core.convert.ConversionServiceAware; import io.micronaut.core.util.ArrayUtils; import io.micronaut.http.cookie.Cookie; import io.micronaut.http.uri.UriBuilder; @@ -33,7 +34,7 @@ * @author Graeme Rocher * @since 1.0 */ -public interface MutableHttpRequest extends HttpRequest, MutableHttpMessage { +public interface MutableHttpRequest extends HttpRequest, MutableHttpMessage, ConversionServiceAware { /** * Sets the specified cookie on the request. diff --git a/http/src/main/java/io/micronaut/http/bind/DefaultRequestBinderRegistry.java b/http/src/main/java/io/micronaut/http/bind/DefaultRequestBinderRegistry.java index 7c8a6f34fb6..a38e8402512 100644 --- a/http/src/main/java/io/micronaut/http/bind/DefaultRequestBinderRegistry.java +++ b/http/src/main/java/io/micronaut/http/bind/DefaultRequestBinderRegistry.java @@ -23,16 +23,36 @@ import io.micronaut.core.type.Argument; import io.micronaut.core.util.CollectionUtils; import io.micronaut.core.util.clhm.ConcurrentLinkedHashMap; -import io.micronaut.core.util.StringUtils; -import io.micronaut.http.*; +import io.micronaut.http.FullHttpRequest; +import io.micronaut.http.HttpHeaders; +import io.micronaut.http.HttpMethod; +import io.micronaut.http.HttpParameters; +import io.micronaut.http.HttpRequest; +import io.micronaut.http.PushCapableHttpRequest; import io.micronaut.http.annotation.Body; -import io.micronaut.http.bind.binders.*; +import io.micronaut.http.bind.binders.AnnotatedRequestArgumentBinder; +import io.micronaut.http.bind.binders.ContinuationArgumentBinder; +import io.micronaut.http.bind.binders.CookieAnnotationBinder; +import io.micronaut.http.bind.binders.DefaultBodyAnnotationBinder; +import io.micronaut.http.bind.binders.HeaderAnnotationBinder; +import io.micronaut.http.bind.binders.ParameterAnnotationBinder; +import io.micronaut.http.bind.binders.PartAnnotationBinder; +import io.micronaut.http.bind.binders.PathVariableAnnotationBinder; +import io.micronaut.http.bind.binders.RequestArgumentBinder; +import io.micronaut.http.bind.binders.RequestAttributeAnnotationBinder; +import io.micronaut.http.bind.binders.RequestBeanAnnotationBinder; +import io.micronaut.http.bind.binders.TypedRequestArgumentBinder; import io.micronaut.http.cookie.Cookie; import io.micronaut.http.cookie.Cookies; import jakarta.inject.Inject; import jakarta.inject.Singleton; + import java.lang.annotation.Annotation; -import java.util.*; +import java.util.Arrays; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; import static io.micronaut.core.util.KotlinUtils.KOTLIN_COROUTINES_SUPPORTED; @@ -50,7 +70,7 @@ public class DefaultRequestBinderRegistry implements RequestBinderRegistry { private final Map, RequestArgumentBinder> byAnnotation = new LinkedHashMap<>(); private final Map byTypeAndAnnotation = new LinkedHashMap<>(); private final Map byType = new LinkedHashMap<>(); - private final ConversionService conversionService; + private final ConversionService conversionService; private final Map> argumentBinderCache = new ConcurrentLinkedHashMap.Builder>().maximumWeightedCapacity(CACHE_MAX_SIZE).build(); @@ -66,7 +86,8 @@ public DefaultRequestBinderRegistry(ConversionService conversionService, Request * @param conversionService The conversion service * @param binders The request argument binders */ - @Inject public DefaultRequestBinderRegistry(ConversionService conversionService, List binders) { + @Inject + public DefaultRequestBinderRegistry(ConversionService conversionService, List binders) { this.conversionService = conversionService; if (CollectionUtils.isNotEmpty(binders)) { @@ -75,7 +96,6 @@ public DefaultRequestBinderRegistry(ConversionService conversionService, Request } } - registerDefaultConverters(conversionService); registerDefaultAnnotationBinders(byAnnotation); byType.put(Argument.of(HttpHeaders.class).typeHashCode(), (RequestArgumentBinder) (argument, source) -> () -> Optional.of(source.getHeaders())); @@ -213,30 +233,6 @@ protected RequestArgumentBinder findBinder(Argument argument, Class conversionService) { - conversionService.addConverter( - CharSequence.class, - MediaType.class, (object, targetType, context) -> { - if (StringUtils.isEmpty(object)) { - return Optional.empty(); - } else { - final String str = object.toString(); - try { - return Optional.of(MediaType.of(str)); - } catch (IllegalArgumentException e) { - context.reject(e); - return Optional.empty(); - } - } - }); - - } - /** * @param byAnnotation The request argument binder */ diff --git a/http/src/main/java/io/micronaut/http/bind/binders/CookieAnnotationBinder.java b/http/src/main/java/io/micronaut/http/bind/binders/CookieAnnotationBinder.java index 0c7dc2f8964..0f02c97d895 100644 --- a/http/src/main/java/io/micronaut/http/bind/binders/CookieAnnotationBinder.java +++ b/http/src/main/java/io/micronaut/http/bind/binders/CookieAnnotationBinder.java @@ -38,7 +38,7 @@ public class CookieAnnotationBinder extends AbstractAnnotatedArgumentBinder conversionService) { + public CookieAnnotationBinder(ConversionService conversionService) { super(conversionService); } diff --git a/http/src/main/java/io/micronaut/http/bind/binders/DefaultBodyAnnotationBinder.java b/http/src/main/java/io/micronaut/http/bind/binders/DefaultBodyAnnotationBinder.java index 62bd82c555d..244b6038aee 100644 --- a/http/src/main/java/io/micronaut/http/bind/binders/DefaultBodyAnnotationBinder.java +++ b/http/src/main/java/io/micronaut/http/bind/binders/DefaultBodyAnnotationBinder.java @@ -36,7 +36,7 @@ */ public class DefaultBodyAnnotationBinder implements BodyArgumentBinder { - protected final ConversionService conversionService; + protected final ConversionService conversionService; /** * @param conversionService The conversion service diff --git a/http/src/main/java/io/micronaut/http/bind/binders/HeaderAnnotationBinder.java b/http/src/main/java/io/micronaut/http/bind/binders/HeaderAnnotationBinder.java index 4d685b50634..2572c350d97 100644 --- a/http/src/main/java/io/micronaut/http/bind/binders/HeaderAnnotationBinder.java +++ b/http/src/main/java/io/micronaut/http/bind/binders/HeaderAnnotationBinder.java @@ -39,7 +39,7 @@ public class HeaderAnnotationBinder extends AbstractAnnotatedArgumentBinder conversionService) { + public HeaderAnnotationBinder(ConversionService conversionService) { super(conversionService); } diff --git a/http/src/main/java/io/micronaut/http/bind/binders/ParameterAnnotationBinder.java b/http/src/main/java/io/micronaut/http/bind/binders/ParameterAnnotationBinder.java index 7184705242c..77f9d9e1def 100644 --- a/http/src/main/java/io/micronaut/http/bind/binders/ParameterAnnotationBinder.java +++ b/http/src/main/java/io/micronaut/http/bind/binders/ParameterAnnotationBinder.java @@ -43,7 +43,7 @@ public class ParameterAnnotationBinder extends AbstractAnnotatedArgumentBinde /** * @param conversionService The conversion service */ - public ParameterAnnotationBinder(ConversionService conversionService) { + public ParameterAnnotationBinder(ConversionService conversionService) { super(conversionService); this.queryValueArgumentBinder = new QueryValueArgumentBinder<>(conversionService); } diff --git a/http/src/main/java/io/micronaut/http/bind/binders/PartAnnotationBinder.java b/http/src/main/java/io/micronaut/http/bind/binders/PartAnnotationBinder.java index dc4ef36ac69..a1528871bd8 100644 --- a/http/src/main/java/io/micronaut/http/bind/binders/PartAnnotationBinder.java +++ b/http/src/main/java/io/micronaut/http/bind/binders/PartAnnotationBinder.java @@ -30,7 +30,7 @@ */ public class PartAnnotationBinder extends AbstractAnnotatedArgumentBinder> implements AnnotatedRequestArgumentBinder { - public PartAnnotationBinder(ConversionService conversionService) { + public PartAnnotationBinder(ConversionService conversionService) { super(conversionService); } diff --git a/http/src/main/java/io/micronaut/http/bind/binders/PathVariableAnnotationBinder.java b/http/src/main/java/io/micronaut/http/bind/binders/PathVariableAnnotationBinder.java index 144c294cb9a..0e552930c60 100644 --- a/http/src/main/java/io/micronaut/http/bind/binders/PathVariableAnnotationBinder.java +++ b/http/src/main/java/io/micronaut/http/bind/binders/PathVariableAnnotationBinder.java @@ -44,7 +44,7 @@ public class PathVariableAnnotationBinder extends AbstractAnnotatedArgumentBi /** * @param conversionService The conversion service */ - public PathVariableAnnotationBinder(ConversionService conversionService) { + public PathVariableAnnotationBinder(ConversionService conversionService) { super(conversionService); } @@ -78,7 +78,7 @@ public BindingResult bind(ArgumentConversionContext context, HttpRequest variableValues = ConvertibleValues.of(matchInfo.get().getVariableValues()); + final ConvertibleValues variableValues = ConvertibleValues.of(matchInfo.get().getVariableValues(), conversionService); if (bindAll) { Object value; // Only maps and POJOs will "bindAll", lists work like normal diff --git a/http/src/main/java/io/micronaut/http/bind/binders/QueryValueArgumentBinder.java b/http/src/main/java/io/micronaut/http/bind/binders/QueryValueArgumentBinder.java index 5be8643e1f7..7ee7d6a0add 100644 --- a/http/src/main/java/io/micronaut/http/bind/binders/QueryValueArgumentBinder.java +++ b/http/src/main/java/io/micronaut/http/bind/binders/QueryValueArgumentBinder.java @@ -44,16 +44,13 @@ public class QueryValueArgumentBinder extends AbstractAnnotatedArgumentBinder> implements AnnotatedRequestArgumentBinder { - private final ConversionService conversionService; - /** * Constructor. * * @param conversionService conversion service */ - public QueryValueArgumentBinder(ConversionService conversionService) { + public QueryValueArgumentBinder(ConversionService conversionService) { super(conversionService); - this.conversionService = conversionService; } @Override diff --git a/http/src/main/java/io/micronaut/http/bind/binders/RequestAttributeAnnotationBinder.java b/http/src/main/java/io/micronaut/http/bind/binders/RequestAttributeAnnotationBinder.java index 56e99e7c29c..cc63bac32f4 100644 --- a/http/src/main/java/io/micronaut/http/bind/binders/RequestAttributeAnnotationBinder.java +++ b/http/src/main/java/io/micronaut/http/bind/binders/RequestAttributeAnnotationBinder.java @@ -38,7 +38,7 @@ public class RequestAttributeAnnotationBinder extends AbstractAnnotatedArgume /** * @param conversionService conversionService */ - public RequestAttributeAnnotationBinder(ConversionService conversionService) { + public RequestAttributeAnnotationBinder(ConversionService conversionService) { super(conversionService); } diff --git a/http/src/main/java/io/micronaut/http/bind/binders/RequestBeanAnnotationBinder.java b/http/src/main/java/io/micronaut/http/bind/binders/RequestBeanAnnotationBinder.java index 6b8e58baea6..d7342c069ca 100644 --- a/http/src/main/java/io/micronaut/http/bind/binders/RequestBeanAnnotationBinder.java +++ b/http/src/main/java/io/micronaut/http/bind/binders/RequestBeanAnnotationBinder.java @@ -53,7 +53,7 @@ public class RequestBeanAnnotationBinder extends AbstractAnnotatedArgumentBin * @param requestBinderRegistry Original request binder registry * @param conversionService The conversion service */ - public RequestBeanAnnotationBinder(RequestBinderRegistry requestBinderRegistry, ConversionService conversionService) { + public RequestBeanAnnotationBinder(RequestBinderRegistry requestBinderRegistry, ConversionService conversionService) { super(conversionService); this.requestBinderRegistry = requestBinderRegistry; } diff --git a/http/src/main/java/io/micronaut/http/converters/HttpConverterRegistrar.java b/http/src/main/java/io/micronaut/http/converters/HttpConverterRegistrar.java index cd18a778b35..f0699e9bb78 100644 --- a/http/src/main/java/io/micronaut/http/converters/HttpConverterRegistrar.java +++ b/http/src/main/java/io/micronaut/http/converters/HttpConverterRegistrar.java @@ -16,20 +16,13 @@ package io.micronaut.http.converters; import io.micronaut.context.exceptions.ConfigurationException; -import io.micronaut.core.convert.ConversionService; +import io.micronaut.core.convert.MutableConversionService; import io.micronaut.core.convert.TypeConverterRegistrar; import io.micronaut.core.io.Readable; import io.micronaut.core.io.ResourceLoader; import io.micronaut.core.io.ResourceResolver; -import io.micronaut.http.HttpStatus; -import io.micronaut.http.HttpVersion; -import io.micronaut.http.MediaType; import jakarta.inject.Singleton; -import java.net.InetSocketAddress; -import java.net.MalformedURLException; -import java.net.ProxySelector; -import java.net.SocketAddress; import java.net.URL; import java.util.Optional; @@ -54,15 +47,7 @@ protected HttpConverterRegistrar(ResourceResolver resourceResolver) { } @Override - public void register(ConversionService conversionService) { - conversionService.addConverter(String.class, HttpVersion.class, s -> { - try { - return HttpVersion.valueOf(Double.parseDouble(s)); - } catch (NumberFormatException e) { - return HttpVersion.valueOf(s); - } - }); - conversionService.addConverter(Number.class, HttpVersion.class, s -> HttpVersion.valueOf(s.doubleValue())); + public void register(MutableConversionService conversionService) { conversionService.addConverter( CharSequence.class, Readable.class, @@ -86,74 +71,5 @@ public void register(ConversionService conversionService) { } ); - conversionService.addConverter( - CharSequence.class, - MediaType.class, - (object, targetType, context) -> { - try { - return Optional.of(MediaType.of(object)); - } catch (IllegalArgumentException e) { - context.reject(e); - return Optional.empty(); - } - } - ); - conversionService.addConverter( - Number.class, - HttpStatus.class, - (object, targetType, context) -> { - try { - HttpStatus status = HttpStatus.valueOf(object.shortValue()); - return Optional.of(status); - } catch (IllegalArgumentException e) { - context.reject(object, e); - return Optional.empty(); - } - } - ); - conversionService.addConverter( - CharSequence.class, - SocketAddress.class, - (object, targetType, context) -> { - String address = object.toString(); - try { - URL url = new URL(address); - int port = url.getPort(); - if (port == -1) { - port = url.getDefaultPort(); - } - if (port == -1) { - context.reject(object, new ConfigurationException("Failed to find a port in the given value")); - return Optional.empty(); - } - return Optional.of(InetSocketAddress.createUnresolved(url.getHost(), port)); - } catch (MalformedURLException malformedURLException) { - String[] parts = object.toString().split(":"); - if (parts.length == 2) { - try { - int port = Integer.parseInt(parts[1]); - return Optional.of(InetSocketAddress.createUnresolved(parts[0], port)); - } catch (IllegalArgumentException illegalArgumentException) { - context.reject(object, illegalArgumentException); - return Optional.empty(); - } - } else { - context.reject(object, new ConfigurationException("The address is not in a proper format of IP:PORT or a standard URL")); - return Optional.empty(); - } - } - } - ); - conversionService.addConverter( - CharSequence.class, - ProxySelector.class, - (object, targetType, context) -> { - if (object.toString().equals("default")) { - return Optional.of(ProxySelector.getDefault()); - } else { - return Optional.empty(); - } - } - ); } } diff --git a/http/src/main/java/io/micronaut/http/converters/SharedHttpConvertersRegistrar.java b/http/src/main/java/io/micronaut/http/converters/SharedHttpConvertersRegistrar.java new file mode 100644 index 00000000000..1924dae98df --- /dev/null +++ b/http/src/main/java/io/micronaut/http/converters/SharedHttpConvertersRegistrar.java @@ -0,0 +1,109 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.http.converters; + +import io.micronaut.context.exceptions.ConfigurationException; +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.convert.MutableConversionService; +import io.micronaut.core.convert.TypeConverterRegistrar; +import io.micronaut.http.HttpStatus; +import io.micronaut.http.HttpVersion; + +import java.net.InetSocketAddress; +import java.net.MalformedURLException; +import java.net.ProxySelector; +import java.net.SocketAddress; +import java.net.URL; +import java.util.Optional; + +/** + * Converter registrar for HTTP classes. + * + * @author Denis Stepanov + * @since 4.0.0 + */ +@Internal +public class SharedHttpConvertersRegistrar implements TypeConverterRegistrar { + + @Override + public void register(MutableConversionService conversionService) { + conversionService.addConverter(String.class, HttpVersion.class, s -> { + try { + return HttpVersion.valueOf(Double.parseDouble(s)); + } catch (NumberFormatException e) { + return HttpVersion.valueOf(s); + } + }); + conversionService.addConverter(Number.class, HttpVersion.class, s -> HttpVersion.valueOf(s.doubleValue())); + conversionService.addConverter( + Number.class, + HttpStatus.class, + (object, targetType, context) -> { + try { + HttpStatus status = HttpStatus.valueOf(object.shortValue()); + return Optional.of(status); + } catch (IllegalArgumentException e) { + context.reject(object, e); + return Optional.empty(); + } + } + ); + conversionService.addConverter( + CharSequence.class, + SocketAddress.class, + (object, targetType, context) -> { + String address = object.toString(); + try { + URL url = new URL(address); + int port = url.getPort(); + if (port == -1) { + port = url.getDefaultPort(); + } + if (port == -1) { + context.reject(object, new ConfigurationException("Failed to find a port in the given value")); + return Optional.empty(); + } + return Optional.of(InetSocketAddress.createUnresolved(url.getHost(), port)); + } catch (MalformedURLException malformedURLException) { + String[] parts = object.toString().split(":"); + if (parts.length == 2) { + try { + int port = Integer.parseInt(parts[1]); + return Optional.of(InetSocketAddress.createUnresolved(parts[0], port)); + } catch (IllegalArgumentException illegalArgumentException) { + context.reject(object, illegalArgumentException); + return Optional.empty(); + } + } else { + context.reject(object, new ConfigurationException("The address is not in a proper format of IP:PORT or a standard URL")); + return Optional.empty(); + } + } + } + ); + conversionService.addConverter( + CharSequence.class, + ProxySelector.class, + (object, targetType, context) -> { + if (object.toString().equals("default")) { + return Optional.of(ProxySelector.getDefault()); + } else { + return Optional.empty(); + } + } + ); + } +} diff --git a/http/src/main/java/io/micronaut/http/simple/SimpleHttpHeaders.java b/http/src/main/java/io/micronaut/http/simple/SimpleHttpHeaders.java index 451e376e5e5..243a38a40ae 100644 --- a/http/src/main/java/io/micronaut/http/simple/SimpleHttpHeaders.java +++ b/http/src/main/java/io/micronaut/http/simple/SimpleHttpHeaders.java @@ -31,7 +31,7 @@ public class SimpleHttpHeaders implements MutableHttpHeaders { private final MutableConvertibleMultiValuesMap headers = new MutableConvertibleMultiValuesMap<>(); - private final ConversionService conversionService; + private ConversionService conversionService; /** * Map-based implementation of {@link MutableHttpHeaders}. @@ -93,4 +93,9 @@ public MutableHttpHeaders remove(CharSequence header) { headers.remove(header.toString()); return this; } + + @Override + public void setConversionService(ConversionService conversionService) { + this.conversionService = conversionService; + } } diff --git a/http/src/main/java/io/micronaut/http/simple/SimpleHttpParameters.java b/http/src/main/java/io/micronaut/http/simple/SimpleHttpParameters.java index 99958735039..746ecd5ca17 100644 --- a/http/src/main/java/io/micronaut/http/simple/SimpleHttpParameters.java +++ b/http/src/main/java/io/micronaut/http/simple/SimpleHttpParameters.java @@ -17,11 +17,15 @@ import io.micronaut.core.convert.ArgumentConversionContext; import io.micronaut.core.convert.ConversionService; -import io.micronaut.core.convert.value.ConvertibleMultiValues; import io.micronaut.core.convert.value.ConvertibleMultiValuesMap; import io.micronaut.http.MutableHttpParameters; -import java.util.*; +import java.util.Collection; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; import java.util.stream.Collectors; /** @@ -35,7 +39,7 @@ public class SimpleHttpParameters implements MutableHttpParameters { private final Map> valuesMap; - private final ConvertibleMultiValues values; + private final ConvertibleMultiValuesMap values; /** * @param values The parameter values @@ -83,4 +87,9 @@ public MutableHttpParameters add(CharSequence name, List values) { valuesMap.put(name, values.stream().map(v -> v == null ? null : v.toString()).collect(Collectors.toList())); return this; } + + @Override + public void setConversionService(ConversionService conversionService) { + values.setConversionService(conversionService); + } } diff --git a/http/src/main/java/io/micronaut/http/simple/SimpleHttpRequest.java b/http/src/main/java/io/micronaut/http/simple/SimpleHttpRequest.java index 1ff230ea410..70e466762e5 100644 --- a/http/src/main/java/io/micronaut/http/simple/SimpleHttpRequest.java +++ b/http/src/main/java/io/micronaut/http/simple/SimpleHttpRequest.java @@ -40,7 +40,7 @@ */ public class SimpleHttpRequest implements MutableHttpRequest { - private final MutableConvertibleValues attributes = new MutableConvertibleValuesMap<>(); + private final MutableConvertibleValuesMap attributes = new MutableConvertibleValuesMap<>(); private final SimpleCookies cookies = new SimpleCookies(ConversionService.SHARED); private final SimpleHttpHeaders headers = new SimpleHttpHeaders(ConversionService.SHARED); private final SimpleHttpParameters parameters = new SimpleHttpParameters(ConversionService.SHARED); @@ -126,4 +126,12 @@ public MutableConvertibleValues getAttributes() { public Optional getBody() { return (Optional) Optional.ofNullable(this.body); } + + @Override + public void setConversionService(ConversionService conversionService) { + attributes.setConversionService(conversionService); + cookies.setConversionService(conversionService); + headers.setConversionService(conversionService); + parameters.setConversionService(conversionService); + } } diff --git a/http/src/main/java/io/micronaut/http/simple/cookies/SimpleCookies.java b/http/src/main/java/io/micronaut/http/simple/cookies/SimpleCookies.java index 4df12292cc7..9e541248ec3 100644 --- a/http/src/main/java/io/micronaut/http/simple/cookies/SimpleCookies.java +++ b/http/src/main/java/io/micronaut/http/simple/cookies/SimpleCookies.java @@ -17,6 +17,7 @@ import io.micronaut.core.convert.ArgumentConversionContext; import io.micronaut.core.convert.ConversionService; +import io.micronaut.core.convert.ConversionServiceAware; import io.micronaut.http.cookie.Cookie; import io.micronaut.http.cookie.Cookies; @@ -29,9 +30,9 @@ * @author Vladimir Orany * @since 1.0 */ -public class SimpleCookies implements Cookies { +public class SimpleCookies implements Cookies, ConversionServiceAware { - private final ConversionService conversionService; + private ConversionService conversionService; private final Map cookies; /** @@ -90,4 +91,9 @@ public Cookie put(CharSequence name, Cookie cookie) { public void putAll(Map cookies) { this.cookies.putAll(cookies); } + + @Override + public void setConversionService(ConversionService conversionService) { + this.conversionService = conversionService; + } } diff --git a/http/src/main/resources/META-INF/services/io.micronaut.core.convert.TypeConverterRegistrar b/http/src/main/resources/META-INF/services/io.micronaut.core.convert.TypeConverterRegistrar index 60730083bd7..e18c5aab449 100644 --- a/http/src/main/resources/META-INF/services/io.micronaut.core.convert.TypeConverterRegistrar +++ b/http/src/main/resources/META-INF/services/io.micronaut.core.convert.TypeConverterRegistrar @@ -1 +1,2 @@ io.micronaut.http.MediaTypeConvertersRegistrar +io.micronaut.http.converters.SharedHttpConvertersRegistrar diff --git a/inject-groovy/src/test/groovy/io/micronaut/inject/generics/InjectWithWildcardSpec.groovy b/inject-groovy/src/test/groovy/io/micronaut/inject/generics/InjectWithWildcardSpec.groovy index 876b4f0440d..df65b9880a5 100644 --- a/inject-groovy/src/test/groovy/io/micronaut/inject/generics/InjectWithWildcardSpec.groovy +++ b/inject-groovy/src/test/groovy/io/micronaut/inject/generics/InjectWithWildcardSpec.groovy @@ -43,15 +43,15 @@ class InjectWithWildcardSpec extends Specification { static class WildCardInject { // tests injecting field @Inject - protected ConversionService conversionService + protected ConversionService conversionService // tests injecting constructor - WildCardInject(ConversionService conversionService) { + WildCardInject(ConversionService conversionService) { } // tests injection method @Inject - void setConversionService(ConversionService conversionService) { + void setConversionService(ConversionService conversionService) { this.conversionService = conversionService } } diff --git a/inject-java/src/test/groovy/io/micronaut/inject/ast/beans/BeanElementVisitorSpec.groovy b/inject-java/src/test/groovy/io/micronaut/inject/ast/beans/BeanElementVisitorSpec.groovy index 80ed964a00b..606040851a7 100644 --- a/inject-java/src/test/groovy/io/micronaut/inject/ast/beans/BeanElementVisitorSpec.groovy +++ b/inject-java/src/test/groovy/io/micronaut/inject/ast/beans/BeanElementVisitorSpec.groovy @@ -16,12 +16,11 @@ import io.micronaut.context.env.Environment; import io.micronaut.core.convert.ConversionService; import jakarta.inject.Inject; import jakarta.inject.Named; -import jakarta.inject.Singleton; @Prototype @Named("blah") class Test implements Runnable { - @Inject ConversionService conversionService; + @Inject ConversionService conversionService; @Inject void setEnvironment(Environment environment) { @@ -59,12 +58,11 @@ import io.micronaut.context.env.Environment; import io.micronaut.core.convert.ConversionService; import jakarta.inject.Inject; import jakarta.inject.Named; -import jakarta.inject.Singleton; @Prototype @Named("blah") class Test implements Runnable { - @Inject ConversionService conversionService; + @Inject ConversionService conversionService; @Inject void setEnvironment(Environment environment) { @@ -103,13 +101,11 @@ import io.micronaut.context.annotation.Prototype; import io.micronaut.context.env.Environment; import io.micronaut.core.convert.ConversionService; import jakarta.inject.Inject; -import jakarta.inject.Named; -import jakarta.inject.Singleton; @Prototype @Factory class TestFactory implements Runnable { - @Inject ConversionService conversionService; + @Inject ConversionService conversionService; @Inject void setEnvironment(Environment environment) { diff --git a/inject-java/src/test/groovy/io/micronaut/inject/generics/WildCardInject.java b/inject-java/src/test/groovy/io/micronaut/inject/generics/WildCardInject.java index 9c64209e067..477aa89a84e 100644 --- a/inject-java/src/test/groovy/io/micronaut/inject/generics/WildCardInject.java +++ b/inject-java/src/test/groovy/io/micronaut/inject/generics/WildCardInject.java @@ -22,15 +22,15 @@ public class WildCardInject { // tests injecting field @Inject - protected ConversionService conversionService; + protected ConversionService conversionService; // tests injecting constructor - public WildCardInject(ConversionService conversionService) { + public WildCardInject(ConversionService conversionService) { } // tests injection method @Inject - public void setConversionService(ConversionService conversionService) { + public void setConversionService(ConversionService conversionService) { this.conversionService = conversionService; } } diff --git a/inject/src/main/java/io/micronaut/context/AbstractInitializableBeanDefinition.java b/inject/src/main/java/io/micronaut/context/AbstractInitializableBeanDefinition.java index 32c8fd2819f..d8b35c213cd 100644 --- a/inject/src/main/java/io/micronaut/context/AbstractInitializableBeanDefinition.java +++ b/inject/src/main/java/io/micronaut/context/AbstractInitializableBeanDefinition.java @@ -759,7 +759,7 @@ public final T build(BeanResolutionContext resolutionContext, } boolean requiresConversion = value != null && !requiredArgument.getType().isInstance(value); if (requiresConversion) { - Optional converted = ConversionService.SHARED.convert(value, requiredArgument.getType(), ConversionContext.of(requiredArgument)); + Optional converted = context.getConversionService().convert(value, requiredArgument.getType(), ConversionContext.of(requiredArgument)); Object finalValue = value; value = converted.orElseThrow(() -> new BeanInstantiationException(resolutionContext, "Invalid value [" + finalValue + "] for argument: " + argumentName)); requiredArgumentValues.put(argumentName, value); diff --git a/inject/src/main/java/io/micronaut/context/AbstractParametrizedBeanDefinition.java b/inject/src/main/java/io/micronaut/context/AbstractParametrizedBeanDefinition.java index 6ebb06a1fc5..521e0d1f78e 100644 --- a/inject/src/main/java/io/micronaut/context/AbstractParametrizedBeanDefinition.java +++ b/inject/src/main/java/io/micronaut/context/AbstractParametrizedBeanDefinition.java @@ -23,7 +23,6 @@ import io.micronaut.core.annotation.AnnotationUtil; import io.micronaut.core.annotation.Internal; import io.micronaut.core.convert.ConversionContext; -import io.micronaut.core.convert.ConversionService; import io.micronaut.core.type.Argument; import io.micronaut.inject.BeanDefinition; import io.micronaut.inject.ParametrizedBeanFactory; @@ -106,7 +105,7 @@ public final T build(BeanResolutionContext resolutionContext, Object value = requiredArgumentValues.get(argumentName); boolean requiresConversion = value != null && !requiredArgument.getType().isInstance(value); if (requiresConversion) { - Optional converted = ConversionService.SHARED.convert(value, requiredArgument.getType(), ConversionContext.of(requiredArgument)); + Optional converted = context.getConversionService().convert(value, requiredArgument.getType(), ConversionContext.of(requiredArgument)); Object finalValue = value; value = converted.orElseThrow(() -> new BeanInstantiationException(resolutionContext, "Invalid value [" + finalValue + "] for argument: " + argumentName)); requiredArgumentValues.put(argumentName, value); diff --git a/inject/src/main/java/io/micronaut/context/ApplicationContext.java b/inject/src/main/java/io/micronaut/context/ApplicationContext.java index cdb9f011b29..4b965d04521 100644 --- a/inject/src/main/java/io/micronaut/context/ApplicationContext.java +++ b/inject/src/main/java/io/micronaut/context/ApplicationContext.java @@ -19,13 +19,12 @@ import io.micronaut.context.env.PropertyPlaceholderResolver; import io.micronaut.context.env.PropertySource; import io.micronaut.context.env.SystemPropertiesPropertySource; -import io.micronaut.core.convert.ConversionService; +import io.micronaut.core.annotation.NonNull; +import io.micronaut.core.annotation.Nullable; import io.micronaut.core.util.ArgumentUtils; import io.micronaut.core.util.StringUtils; import io.micronaut.core.value.PropertyResolver; -import io.micronaut.core.annotation.NonNull; -import io.micronaut.core.annotation.Nullable; import java.util.Collections; import java.util.Map; import java.util.function.Consumer; @@ -59,11 +58,6 @@ */ public interface ApplicationContext extends BeanContext, PropertyResolver, PropertyPlaceholderResolver { - /** - * @return The default conversion service - */ - @NonNull ConversionService getConversionService(); - /** * @return The application environment */ diff --git a/inject/src/main/java/io/micronaut/context/ApplicationContextConfiguration.java b/inject/src/main/java/io/micronaut/context/ApplicationContextConfiguration.java index 00ed9676c0e..0ebedf19b12 100644 --- a/inject/src/main/java/io/micronaut/context/ApplicationContextConfiguration.java +++ b/inject/src/main/java/io/micronaut/context/ApplicationContextConfiguration.java @@ -15,11 +15,10 @@ */ package io.micronaut.context; -import io.micronaut.core.convert.ConversionService; -import io.micronaut.core.io.scan.ClassPathResourceLoader; - import io.micronaut.core.annotation.NonNull; import io.micronaut.core.annotation.Nullable; +import io.micronaut.core.convert.MutableConversionService; +import io.micronaut.core.io.scan.ClassPathResourceLoader; import java.util.Collections; import java.util.List; @@ -85,12 +84,12 @@ default boolean isEnvironmentPropertySource() { } /** - * The default conversion service to use. + * The optional conversion service to use. * * @return The conversion service */ - default @NonNull ConversionService getConversionService() { - return ConversionService.SHARED; + default Optional getConversionService() { + return Optional.empty(); } /** diff --git a/inject/src/main/java/io/micronaut/context/BeanContext.java b/inject/src/main/java/io/micronaut/context/BeanContext.java index f87c18c155c..0a1a2faac4b 100644 --- a/inject/src/main/java/io/micronaut/context/BeanContext.java +++ b/inject/src/main/java/io/micronaut/context/BeanContext.java @@ -20,6 +20,7 @@ import io.micronaut.core.annotation.NonNull; import io.micronaut.core.annotation.Nullable; import io.micronaut.core.attr.MutableAttributeHolder; +import io.micronaut.core.convert.ConversionServiceProvider; import io.micronaut.core.type.Argument; import io.micronaut.inject.BeanIdentifier; import io.micronaut.inject.validation.BeanDefinitionValidator; @@ -45,7 +46,8 @@ public interface BeanContext extends BeanDefinitionRegistry, ApplicationEventPublisher, AnnotationMetadataResolver, - MutableAttributeHolder { + MutableAttributeHolder, + ConversionServiceProvider { /** * Obtains the configuration for this context. diff --git a/inject/src/main/java/io/micronaut/context/DefaultApplicationContext.java b/inject/src/main/java/io/micronaut/context/DefaultApplicationContext.java index 97e00c7ced7..f3017274ef6 100644 --- a/inject/src/main/java/io/micronaut/context/DefaultApplicationContext.java +++ b/inject/src/main/java/io/micronaut/context/DefaultApplicationContext.java @@ -29,7 +29,7 @@ import io.micronaut.core.annotation.NonNull; import io.micronaut.core.annotation.Nullable; import io.micronaut.core.convert.ArgumentConversionContext; -import io.micronaut.core.convert.ConversionService; +import io.micronaut.core.convert.MutableConversionService; import io.micronaut.core.convert.TypeConverter; import io.micronaut.core.convert.TypeConverterRegistrar; import io.micronaut.core.io.scan.ClassPathResourceLoader; @@ -62,7 +62,6 @@ */ public class DefaultApplicationContext extends DefaultBeanContext implements ApplicationContext { - private final ConversionService conversionService; private final ClassPathResourceLoader resourceLoader; private final ApplicationContextConfiguration configuration; private Environment environment; @@ -116,7 +115,6 @@ public DefaultApplicationContext(@NonNull ApplicationContextConfiguration config super(configuration); ArgumentUtils.requireNonNull("configuration", configuration); this.configuration = configuration; - this.conversionService = createConversionService(); this.resourceLoader = configuration.getResourceLoader(); } @@ -162,25 +160,15 @@ private boolean isBootstrapPropertySourceLocatorPresent() { return false; } - /** - * Creates the default conversion service. - * - * @return The conversion service - */ - protected @NonNull - ConversionService createConversionService() { - return ConversionService.SHARED; - } - @Override - public @NonNull - ConversionService getConversionService() { - return conversionService; + @NonNull + public MutableConversionService getConversionService() { + return getEnvironment(); } @Override - public @NonNull - Environment getEnvironment() { + @NonNull + public Environment getEnvironment() { if (environment == null) { environment = createEnvironment(configuration); } @@ -188,16 +176,23 @@ Environment getEnvironment() { } @Override - public synchronized @NonNull - ApplicationContext start() { + @NonNull + public synchronized ApplicationContext start() { startEnvironment(); return (ApplicationContext) super.start(); } + @Override + protected void registerConversionService() { + // Conversion service is represented by the environment + } + @Override public synchronized @NonNull ApplicationContext stop() { - return (ApplicationContext) super.stop(); + ApplicationContext stop = (ApplicationContext) super.stop(); + environment = null; + return stop; } @Override @@ -517,6 +512,7 @@ public String resolveRequiredPlaceholders(String str) throws ConfigurationExcept * @param beanContext The bean context */ protected void initializeTypeConverters(BeanContext beanContext) { + MutableConversionService mutableConversionService = getConversionService(); for (BeanRegistration typeConverterRegistration : beanContext.getBeanRegistrations(TypeConverter.class)) { TypeConverter typeConverter = typeConverterRegistration.getBean(); List> typeArguments = typeConverterRegistration.getBeanDefinition().getTypeArguments(TypeConverter.class); @@ -524,12 +520,12 @@ protected void initializeTypeConverters(BeanContext beanContext) { Class source = typeArguments.get(0).getType(); Class target = typeArguments.get(1).getType(); if (!(source == Object.class && target == Object.class)) { - getConversionService().addConverter(source, target, typeConverter); + mutableConversionService.addConverter(source, target, typeConverter); } } } for (TypeConverterRegistrar registrar : beanContext.getBeansOfType(TypeConverterRegistrar.class)) { - registrar.register(conversionService); + registrar.register(mutableConversionService); } } @@ -583,7 +579,7 @@ private static class BootstrapEnvironment extends DefaultEnvironment { private List propertySourceList; - BootstrapEnvironment(ClassPathResourceLoader resourceLoader, ConversionService conversionService, ApplicationContextConfiguration configuration, String... activeEnvironments) { + BootstrapEnvironment(ClassPathResourceLoader resourceLoader, MutableConversionService conversionService, ApplicationContextConfiguration configuration, String... activeEnvironments) { super(new ApplicationContextConfiguration() { @Override public Optional getDeduceEnvironments() { @@ -621,8 +617,8 @@ public List getEnvironmentVariableExcludes() { @NonNull @Override - public ConversionService getConversionService() { - return conversionService; + public Optional getConversionService() { + return Optional.of(conversionService); } @NonNull @@ -812,7 +808,7 @@ private BootstrapPropertySourceLocator resolveBootstrapPropertySourceLocator(Str private BootstrapEnvironment createBootstrapEnvironment(String... environmentNames) { return new BootstrapEnvironment( resourceLoader, - conversionService, + mutableConversionService, configuration, environmentNames); } diff --git a/inject/src/main/java/io/micronaut/context/DefaultApplicationContextBuilder.java b/inject/src/main/java/io/micronaut/context/DefaultApplicationContextBuilder.java index ae86c07eba2..22009c07f7b 100644 --- a/inject/src/main/java/io/micronaut/context/DefaultApplicationContextBuilder.java +++ b/inject/src/main/java/io/micronaut/context/DefaultApplicationContextBuilder.java @@ -46,21 +46,21 @@ * @since 1.0 */ public class DefaultApplicationContextBuilder implements ApplicationContextBuilder, ApplicationContextConfiguration { - private List singletons = new ArrayList<>(); - private List environments = new ArrayList<>(); - private List defaultEnvironments = new ArrayList<>(); - private List packages = new ArrayList<>(); - private Map properties = new LinkedHashMap<>(); - private List propertySources = new ArrayList<>(); - private Collection configurationIncludes = new HashSet<>(); - private Collection configurationExcludes = new HashSet<>(); + private final List singletons = new ArrayList<>(); + private final List environments = new ArrayList<>(); + private final List defaultEnvironments = new ArrayList<>(); + private final List packages = new ArrayList<>(); + private final Map properties = new LinkedHashMap<>(); + private final List propertySources = new ArrayList<>(); + private final Collection configurationIncludes = new HashSet<>(); + private final Collection configurationExcludes = new HashSet<>(); private Boolean deduceEnvironments = null; private ClassLoader classLoader = getClass().getClassLoader(); private boolean envPropertySource = true; - private List envVarIncludes = new ArrayList<>(); - private List envVarExcludes = new ArrayList<>(); + private final List envVarIncludes = new ArrayList<>(); + private final List envVarExcludes = new ArrayList<>(); private String[] args = new String[0]; - private Set> eagerInitAnnotated = new HashSet<>(3); + private final Set> eagerInitAnnotated = new HashSet<>(3); private String[] overrideConfigLocations; private boolean banner = true; private ClassPathResourceLoader classPathResourceLoader; @@ -89,7 +89,7 @@ public boolean isAllowEmptyProviders() { } @Override - @NonNull + @NonNull public ApplicationContextBuilder enableDefaultPropertySources(boolean areEnabled) { this.enableDefaultPropertySources = areEnabled; return this; diff --git a/inject/src/main/java/io/micronaut/context/DefaultBeanContext.java b/inject/src/main/java/io/micronaut/context/DefaultBeanContext.java index b5066573566..f86d9febba6 100644 --- a/inject/src/main/java/io/micronaut/context/DefaultBeanContext.java +++ b/inject/src/main/java/io/micronaut/context/DefaultBeanContext.java @@ -64,8 +64,7 @@ import io.micronaut.core.annotation.Nullable; import io.micronaut.core.annotation.Order; import io.micronaut.core.annotation.UsedByGeneratedCode; -import io.micronaut.core.convert.ConversionService; -import io.micronaut.core.convert.DefaultConversionService; +import io.micronaut.core.convert.MutableConversionService; import io.micronaut.core.convert.TypeConverter; import io.micronaut.core.convert.TypeConverterRegistrar; import io.micronaut.core.convert.value.MutableConvertibleValues; @@ -238,6 +237,9 @@ public > Stream reduce(Class beanType, S private Set, List>> beanPreDestroyEventListeners; private Set, List>> beanDestroyedEventListeners; + @Nullable + private MutableConversionService conversionService; + /** * Construct a new bean context using the same classloader that loaded this DefaultBeanContext class. */ @@ -333,11 +335,10 @@ public synchronized BeanContext start() { if (!isRunning()) { if (initializing.compareAndSet(false, true)) { - // Reset possibly modified shared context - ((DefaultConversionService) ConversionService.SHARED).reset(); if (LOG.isDebugEnabled()) { LOG.debug("Starting BeanContext"); } + registerConversionService(); finalizeConfiguration(); if (LOG.isDebugEnabled()) { String activeConfigurations = beanConfigurations @@ -361,6 +362,13 @@ public synchronized BeanContext start() { return this; } + /** + * Registers conversion service. + */ + protected void registerConversionService() { + conversionService = MutableConversionService.create(); + registerSingleton(MutableConversionService.class, conversionService, null, false); + } /** * The close method will shut down the context calling {@link jakarta.annotation.PreDestroy} hooks on loaded @@ -421,7 +429,7 @@ public synchronized BeanContext stop() { beanCreationEventListeners = null; beanPreDestroyEventListeners = null; beanDestroyedEventListeners = null; - ((DefaultConversionService) ConversionService.SHARED).reset(); + conversionService = null; terminating.set(false); running.set(false); } @@ -1034,6 +1042,7 @@ private Map resolveArgumentValues(BeanResolutionContext reso if (LOG.isTraceEnabled()) { LOG.trace("Creating bean for parameters: {}", ArrayUtils.toString(args)); } + MutableConversionService conversionService = getConversionService(); Argument[] requiredArguments = ((ParametrizedBeanFactory) definition).getRequiredArguments(); Map argumentValues = new LinkedHashMap<>(requiredArguments.length); BeanResolutionContext.Path currentPath = resolutionContext.getPath(); @@ -1047,7 +1056,7 @@ private Map resolveArgumentValues(BeanResolutionContext reso if (argumentType.isInstance(val) && !CollectionUtils.isIterableOrMap(argumentType)) { argumentValues.put(requiredArgument.getName(), val); } else { - argumentValues.put(requiredArgument.getName(), ConversionService.SHARED.convert(val, requiredArgument).orElseThrow(() -> + argumentValues.put(requiredArgument.getName(), conversionService.convert(val, requiredArgument).orElseThrow(() -> new BeanInstantiationException(resolutionContext, "Invalid bean @Argument [" + requiredArgument + "]. Cannot convert object [" + val + "] to required type: " + argumentType) )); } @@ -2427,6 +2436,7 @@ private Map getRequiredArgumentValues(@NonNull BeanResolutio } else { convertedValues = new LinkedHashMap<>(); } + MutableConversionService conversionService = getConversionService(); if (convertedValues != null) { for (Argument requiredArgument : requiredArguments) { String argumentName = requiredArgument.getName(); @@ -2440,7 +2450,7 @@ private Map getRequiredArgumentValues(@NonNull BeanResolutio if (requiredArgument.getType().isInstance(val)) { convertedValue = val; } else { - convertedValue = ConversionService.SHARED.convert(val, requiredArgument).orElseThrow(() -> + convertedValue = conversionService.convert(val, requiredArgument).orElseThrow(() -> new BeanInstantiationException(resolutionContext, "Invalid bean argument [" + requiredArgument + "]. Cannot convert object [" + val + "] to required type: " + requiredArgument.getType()) ); } @@ -3649,7 +3659,7 @@ public Optional getAttribute(CharSequence name, Class type) { if (type.isInstance(o)) { return Optional.of((T) o); } else if (o != null) { - return ConversionService.SHARED.convert(o, type); + return getConversionService().convert(o, type); } } return Optional.empty(); @@ -3684,6 +3694,11 @@ public void finalizeConfiguration() { readAllBeanDefinitionClasses(); } + @Override + public MutableConversionService getConversionService() { + return conversionService; + } + /** * @param The type * @param The return type diff --git a/inject/src/main/java/io/micronaut/context/converters/ContextConverterRegistrar.java b/inject/src/main/java/io/micronaut/context/converters/ContextConverterRegistrar.java index 291032f0444..e05b3fa4ff3 100644 --- a/inject/src/main/java/io/micronaut/context/converters/ContextConverterRegistrar.java +++ b/inject/src/main/java/io/micronaut/context/converters/ContextConverterRegistrar.java @@ -17,7 +17,7 @@ import io.micronaut.context.BeanContext; import io.micronaut.core.annotation.Internal; -import io.micronaut.core.convert.ConversionService; +import io.micronaut.core.convert.MutableConversionService; import io.micronaut.core.convert.TypeConverterRegistrar; import io.micronaut.core.reflect.ClassUtils; import jakarta.inject.Singleton; @@ -49,7 +49,7 @@ public class ContextConverterRegistrar implements TypeConverterRegistrar { } @Override - public void register(ConversionService conversionService) { + public void register(MutableConversionService conversionService) { conversionService.addConverter(String[].class, Class[].class, (object, targetType, context) -> { Class[] classes = Arrays .stream(object) diff --git a/inject/src/main/java/io/micronaut/context/env/DefaultEnvironment.java b/inject/src/main/java/io/micronaut/context/env/DefaultEnvironment.java index b83fd78d29f..43b1f98a309 100644 --- a/inject/src/main/java/io/micronaut/context/env/DefaultEnvironment.java +++ b/inject/src/main/java/io/micronaut/context/env/DefaultEnvironment.java @@ -20,6 +20,7 @@ import io.micronaut.core.annotation.NonNull; import io.micronaut.core.annotation.Nullable; import io.micronaut.core.convert.ConversionContext; +import io.micronaut.core.convert.MutableConversionService; import io.micronaut.core.convert.TypeConverter; import io.micronaut.core.io.ResourceLoader; import io.micronaut.core.io.ResourceResolver; @@ -108,8 +109,8 @@ public class DefaultEnvironment extends PropertySourcePropertyResolver implement private final ClassLoader classLoader; private final Collection packages = new ConcurrentLinkedQueue<>(); private final BeanIntrospectionScanner annotationScanner; - private Collection configurationIncludes = new HashSet<>(3); - private Collection configurationExcludes = new HashSet<>(3); + private final Collection configurationIncludes = new HashSet<>(3); + private final Collection configurationExcludes = new HashSet<>(3); private final AtomicBoolean running = new AtomicBoolean(false); private Collection propertySourceLoaderList; private final Map loaderByFormatMap = new ConcurrentHashMap<>(); @@ -118,6 +119,7 @@ public class DefaultEnvironment extends PropertySourcePropertyResolver implement private final Boolean deduceEnvironments; private final ApplicationContextConfiguration configuration; private final Collection configLocations; + protected final MutableConversionService mutableConversionService; /** * Construct a new environment for the given configuration. @@ -125,7 +127,8 @@ public class DefaultEnvironment extends PropertySourcePropertyResolver implement * @param configuration The configuration */ public DefaultEnvironment(@NonNull ApplicationContextConfiguration configuration) { - super(configuration.getConversionService()); + super(configuration.getConversionService().orElseGet(MutableConversionService::create)); + this.mutableConversionService = (MutableConversionService) conversionService; this.configuration = configuration; this.resourceLoader = configuration.getResourceLoader(); @@ -302,24 +305,22 @@ public Map refreshAndDiff() { @Override public Optional convert(Object object, Class targetType, ConversionContext context) { - return conversionService.convert(object, targetType, context); + return mutableConversionService.convert(object, targetType, context); } @Override public boolean canConvert(Class sourceType, Class targetType) { - return conversionService.canConvert(sourceType, targetType); + return mutableConversionService.canConvert(sourceType, targetType); } @Override - public Environment addConverter(Class sourceType, Class targetType, TypeConverter typeConverter) { - conversionService.addConverter(sourceType, targetType, typeConverter); - return this; + public void addConverter(Class sourceType, Class targetType, TypeConverter typeConverter) { + mutableConversionService.addConverter(sourceType, targetType, typeConverter); } @Override - public Environment addConverter(Class sourceType, Class targetType, Function typeConverter) { - conversionService.addConverter(sourceType, targetType, typeConverter); - return this; + public void addConverter(Class sourceType, Class targetType, Function typeConverter) { + mutableConversionService.addConverter(sourceType, targetType, typeConverter); } @Override diff --git a/inject/src/main/java/io/micronaut/context/env/DefaultPropertyPlaceholderResolver.java b/inject/src/main/java/io/micronaut/context/env/DefaultPropertyPlaceholderResolver.java index 5894e91089c..869005f9c0b 100644 --- a/inject/src/main/java/io/micronaut/context/env/DefaultPropertyPlaceholderResolver.java +++ b/inject/src/main/java/io/micronaut/context/env/DefaultPropertyPlaceholderResolver.java @@ -53,7 +53,7 @@ public class DefaultPropertyPlaceholderResolver implements PropertyPlaceholderRe private static final char COLON = ':'; private final PropertyResolver environment; - private final ConversionService conversionService; + private final ConversionService conversionService; private final String prefix; private Collection expressionResolvers; diff --git a/inject/src/main/java/io/micronaut/context/env/Environment.java b/inject/src/main/java/io/micronaut/context/env/Environment.java index ff6c600b67f..11acfb4dbba 100644 --- a/inject/src/main/java/io/micronaut/context/env/Environment.java +++ b/inject/src/main/java/io/micronaut/context/env/Environment.java @@ -17,7 +17,7 @@ import io.micronaut.context.LifeCycle; import io.micronaut.core.annotation.Nullable; -import io.micronaut.core.convert.ConversionService; +import io.micronaut.core.convert.MutableConversionService; import io.micronaut.core.io.ResourceLoader; import io.micronaut.core.io.scan.BeanIntrospectionScanner; import io.micronaut.core.reflect.ClassUtils; @@ -53,7 +53,7 @@ * @author Graeme Rocher * @since 1.0 */ -public interface Environment extends PropertyResolver, LifeCycle, ConversionService, ResourceLoader { +public interface Environment extends PropertyResolver, LifeCycle, MutableConversionService, ResourceLoader { /** * Constant for the name micronaut. diff --git a/inject/src/main/java/io/micronaut/context/env/PropertyExpressionResolver.java b/inject/src/main/java/io/micronaut/context/env/PropertyExpressionResolver.java index 876fd9bbd0d..1c8b3e5f513 100644 --- a/inject/src/main/java/io/micronaut/context/env/PropertyExpressionResolver.java +++ b/inject/src/main/java/io/micronaut/context/env/PropertyExpressionResolver.java @@ -44,7 +44,7 @@ public interface PropertyExpressionResolver { */ @NonNull Optional resolve(@NonNull PropertyResolver propertyResolver, - @NonNull ConversionService conversionService, + @NonNull ConversionService conversionService, @NonNull String expression, @NonNull Class requiredType); diff --git a/inject/src/main/java/io/micronaut/context/env/PropertySourcePropertyResolver.java b/inject/src/main/java/io/micronaut/context/env/PropertySourcePropertyResolver.java index b36fe35424e..e694bf673c4 100644 --- a/inject/src/main/java/io/micronaut/context/env/PropertySourcePropertyResolver.java +++ b/inject/src/main/java/io/micronaut/context/env/PropertySourcePropertyResolver.java @@ -77,7 +77,7 @@ public class PropertySourcePropertyResolver implements PropertyResolver, AutoClo private static final Pattern RANDOM_PATTERN = Pattern.compile("\\$\\{" + RANDOM_PREFIX + "(" + RANDOM_UPPER_LIMIT + "|" + RANDOM_RANGE + ")?\\}"); private static final Object NO_VALUE = new Object(); private static final PropertyCatalog[] CONVENTIONS = {PropertyCatalog.GENERATED, PropertyCatalog.RAW}; - protected final ConversionService conversionService; + protected final ConversionService conversionService; protected final PropertyPlaceholderResolver propertyPlaceholderResolver; protected final Map propertySources = new ConcurrentHashMap<>(10); // properties are stored in an array of maps organized by character in the alphabet @@ -96,7 +96,7 @@ public class PropertySourcePropertyResolver implements PropertyResolver, AutoClo * * @param conversionService The {@link ConversionService} */ - public PropertySourcePropertyResolver(ConversionService conversionService) { + public PropertySourcePropertyResolver(ConversionService conversionService) { this.conversionService = conversionService; this.propertyPlaceholderResolver = new DefaultPropertyPlaceholderResolver(this, conversionService); } diff --git a/inject/src/main/java/io/micronaut/inject/annotation/AnnotationConvertersRegistrar.java b/inject/src/main/java/io/micronaut/inject/annotation/AnnotationConvertersRegistrar.java index 2805d19504a..924cddebc43 100644 --- a/inject/src/main/java/io/micronaut/inject/annotation/AnnotationConvertersRegistrar.java +++ b/inject/src/main/java/io/micronaut/inject/annotation/AnnotationConvertersRegistrar.java @@ -16,7 +16,7 @@ package io.micronaut.inject.annotation; import io.micronaut.core.annotation.Internal; -import io.micronaut.core.convert.ConversionService; +import io.micronaut.core.convert.MutableConversionService; import io.micronaut.core.convert.TypeConverterRegistrar; import io.micronaut.core.reflect.ClassUtils; @@ -36,7 +36,7 @@ public final class AnnotationConvertersRegistrar implements TypeConverterRegistrar { @Override - public void register(ConversionService conversionService) { + public void register(MutableConversionService conversionService) { conversionService.addConverter(io.micronaut.core.annotation.AnnotationValue.class, Annotation.class, (object, targetType, context) -> { Optional annotationClass = ClassUtils.forName(object.getAnnotationName(), targetType.getClassLoader()); return annotationClass.map(aClass -> AnnotationMetadataSupport.buildAnnotation(aClass, object)); diff --git a/inject/src/test/groovy/io/micronaut/context/env/PropertySourcePropertyResolverSpec.groovy b/inject/src/test/groovy/io/micronaut/context/env/PropertySourcePropertyResolverSpec.groovy index 08cdbe8d8d5..005710395ac 100644 --- a/inject/src/test/groovy/io/micronaut/context/env/PropertySourcePropertyResolverSpec.groovy +++ b/inject/src/test/groovy/io/micronaut/context/env/PropertySourcePropertyResolverSpec.groovy @@ -661,7 +661,7 @@ class PropertySourcePropertyResolverSpec extends Specification { @Override @NonNull Optional resolve(@NonNull PropertyResolver propertyResolver, - @NonNull ConversionService conversionService, + @NonNull ConversionService conversionService, @NonNull String expression, @NonNull Class requiredType) { assert requiredType == String.class @@ -706,7 +706,7 @@ class PropertySourcePropertyResolverSpec extends Specification { @Override @NonNull Optional resolve(@NonNull PropertyResolver propertyResolver, - @NonNull ConversionService conversionService, + @NonNull ConversionService conversionService, @NonNull String expression, @NonNull Class requiredType) { Optional.empty() diff --git a/jackson-databind/src/main/java/io/micronaut/jackson/databind/convert/JacksonConverterRegistrar.java b/jackson-databind/src/main/java/io/micronaut/jackson/databind/convert/JacksonConverterRegistrar.java index ec2d3fe7486..d14c72527ca 100644 --- a/jackson-databind/src/main/java/io/micronaut/jackson/databind/convert/JacksonConverterRegistrar.java +++ b/jackson-databind/src/main/java/io/micronaut/jackson/databind/convert/JacksonConverterRegistrar.java @@ -32,6 +32,7 @@ import io.micronaut.core.annotation.Nullable; import io.micronaut.core.convert.ArgumentConversionContext; import io.micronaut.core.convert.ConversionService; +import io.micronaut.core.convert.MutableConversionService; import io.micronaut.core.convert.TypeConverter; import io.micronaut.core.convert.TypeConverterRegistrar; import io.micronaut.core.convert.value.ConvertibleValues; @@ -59,7 +60,7 @@ public class JacksonConverterRegistrar implements TypeConverterRegistrar { private final BeanProvider objectMapper; - private final ConversionService conversionService; + private final ConversionService conversionService; /** * Default constructor. @@ -69,13 +70,13 @@ public class JacksonConverterRegistrar implements TypeConverterRegistrar { @Inject protected JacksonConverterRegistrar( BeanProvider objectMapper, - ConversionService conversionService) { + ConversionService conversionService) { this.objectMapper = objectMapper; this.conversionService = conversionService; } @Override - public void register(ConversionService conversionService) { + public void register(MutableConversionService conversionService) { conversionService.addConverter( ArrayNode.class, Object[].class, diff --git a/jackson-databind/src/main/java/io/micronaut/jackson/databind/convert/ObjectNodeConvertibleValues.java b/jackson-databind/src/main/java/io/micronaut/jackson/databind/convert/ObjectNodeConvertibleValues.java index ccbfd6391c2..8cfdfead5b5 100644 --- a/jackson-databind/src/main/java/io/micronaut/jackson/databind/convert/ObjectNodeConvertibleValues.java +++ b/jackson-databind/src/main/java/io/micronaut/jackson/databind/convert/ObjectNodeConvertibleValues.java @@ -43,13 +43,13 @@ public class ObjectNodeConvertibleValues implements ConvertibleValues { private final ObjectNode objectNode; - private final ConversionService conversionService; + private final ConversionService conversionService; /** * @param objectNode The node that maps to JSON object structure * @param conversionService To convert the JSON node into given type */ - public ObjectNodeConvertibleValues(ObjectNode objectNode, ConversionService conversionService) { + public ObjectNodeConvertibleValues(ObjectNode objectNode, ConversionService conversionService) { this.objectNode = objectNode; this.conversionService = conversionService; } diff --git a/json-core/src/main/java/io/micronaut/json/convert/JsonConverterRegistrar.java b/json-core/src/main/java/io/micronaut/json/convert/JsonConverterRegistrar.java index fa8b142e109..e6cdbf40bce 100644 --- a/json-core/src/main/java/io/micronaut/json/convert/JsonConverterRegistrar.java +++ b/json-core/src/main/java/io/micronaut/json/convert/JsonConverterRegistrar.java @@ -23,6 +23,7 @@ import io.micronaut.core.convert.ArgumentConversionContext; import io.micronaut.core.convert.ConversionContext; import io.micronaut.core.convert.ConversionService; +import io.micronaut.core.convert.MutableConversionService; import io.micronaut.core.convert.TypeConverter; import io.micronaut.core.convert.TypeConverterRegistrar; import io.micronaut.core.convert.value.ConvertibleValues; @@ -54,13 +55,13 @@ @Singleton public final class JsonConverterRegistrar implements TypeConverterRegistrar { private final BeanProvider objectCodec; - private final ConversionService conversionService; + private final ConversionService conversionService; private final BeanProvider beanPropertyBinder; @Inject public JsonConverterRegistrar( BeanProvider objectCodec, - ConversionService conversionService, + ConversionService conversionService, BeanProvider beanPropertyBinder ) { this.objectCodec = objectCodec; @@ -69,7 +70,7 @@ public JsonConverterRegistrar( } @Override - public void register(ConversionService conversionService) { + public void register(MutableConversionService conversionService) { conversionService.addConverter( JsonArray.class, Object[].class, diff --git a/json-core/src/main/java/io/micronaut/json/convert/JsonNodeConvertibleValues.java b/json-core/src/main/java/io/micronaut/json/convert/JsonNodeConvertibleValues.java index 71cf86cb3ae..0e3a8642423 100644 --- a/json-core/src/main/java/io/micronaut/json/convert/JsonNodeConvertibleValues.java +++ b/json-core/src/main/java/io/micronaut/json/convert/JsonNodeConvertibleValues.java @@ -42,13 +42,13 @@ public class JsonNodeConvertibleValues implements ConvertibleValues { private final JsonNode objectNode; - private final ConversionService conversionService; + private final ConversionService conversionService; /** * @param objectNode The node that maps to JSON object structure * @param conversionService To convert the JSON node into given type */ - public JsonNodeConvertibleValues(JsonNode objectNode, ConversionService conversionService) { + public JsonNodeConvertibleValues(JsonNode objectNode, ConversionService conversionService) { if (!objectNode.isObject()) { throw new IllegalArgumentException("Expected object node"); } @@ -83,4 +83,9 @@ public Optional get(CharSequence name, ArgumentConversionContext conve return conversionService.convert(jsonNode, conversionContext); } } + + @Override + public ConversionService getConversionService() { + return conversionService; + } } diff --git a/management/src/main/java/io/micronaut/management/endpoint/processors/AbstractEndpointRouteBuilder.java b/management/src/main/java/io/micronaut/management/endpoint/processors/AbstractEndpointRouteBuilder.java index e256bd8c430..01a6f649591 100644 --- a/management/src/main/java/io/micronaut/management/endpoint/processors/AbstractEndpointRouteBuilder.java +++ b/management/src/main/java/io/micronaut/management/endpoint/processors/AbstractEndpointRouteBuilder.java @@ -65,7 +65,7 @@ abstract class AbstractEndpointRouteBuilder extends DefaultRouteBuilder implemen */ AbstractEndpointRouteBuilder(ApplicationContext applicationContext, UriNamingStrategy uriNamingStrategy, - ConversionService conversionService, + ConversionService conversionService, EndpointDefaultConfiguration endpointDefaultConfiguration) { super(applicationContext, uriNamingStrategy, conversionService); this.beanContext = applicationContext; diff --git a/management/src/main/java/io/micronaut/management/endpoint/processors/DeleteEndpointRouteBuilder.java b/management/src/main/java/io/micronaut/management/endpoint/processors/DeleteEndpointRouteBuilder.java index 31278dc0fae..90982bbc3c3 100644 --- a/management/src/main/java/io/micronaut/management/endpoint/processors/DeleteEndpointRouteBuilder.java +++ b/management/src/main/java/io/micronaut/management/endpoint/processors/DeleteEndpointRouteBuilder.java @@ -44,7 +44,7 @@ public class DeleteEndpointRouteBuilder extends AbstractEndpointRouteBuilder { */ public DeleteEndpointRouteBuilder(ApplicationContext beanContext, UriNamingStrategy uriNamingStrategy, - ConversionService conversionService, + ConversionService conversionService, EndpointDefaultConfiguration endpointDefaultConfiguration) { super(beanContext, uriNamingStrategy, conversionService, endpointDefaultConfiguration); } diff --git a/management/src/main/java/io/micronaut/management/endpoint/processors/ReadEndpointRouteBuilder.java b/management/src/main/java/io/micronaut/management/endpoint/processors/ReadEndpointRouteBuilder.java index eca710da29d..5cec22fac24 100644 --- a/management/src/main/java/io/micronaut/management/endpoint/processors/ReadEndpointRouteBuilder.java +++ b/management/src/main/java/io/micronaut/management/endpoint/processors/ReadEndpointRouteBuilder.java @@ -44,7 +44,7 @@ public class ReadEndpointRouteBuilder extends AbstractEndpointRouteBuilder { */ public ReadEndpointRouteBuilder(ApplicationContext beanContext, UriNamingStrategy uriNamingStrategy, - ConversionService conversionService, + ConversionService conversionService, EndpointDefaultConfiguration endpointDefaultConfiguration) { super(beanContext, uriNamingStrategy, conversionService, endpointDefaultConfiguration); } diff --git a/management/src/main/java/io/micronaut/management/endpoint/processors/WriteEndpointRouteBuilder.java b/management/src/main/java/io/micronaut/management/endpoint/processors/WriteEndpointRouteBuilder.java index fcb0e06a9b8..18d99ccd7ff 100644 --- a/management/src/main/java/io/micronaut/management/endpoint/processors/WriteEndpointRouteBuilder.java +++ b/management/src/main/java/io/micronaut/management/endpoint/processors/WriteEndpointRouteBuilder.java @@ -47,7 +47,7 @@ public class WriteEndpointRouteBuilder extends AbstractEndpointRouteBuilder { */ public WriteEndpointRouteBuilder(ApplicationContext beanContext, UriNamingStrategy uriNamingStrategy, - ConversionService conversionService, + ConversionService conversionService, EndpointDefaultConfiguration endpointDefaultConfiguration) { super(beanContext, uriNamingStrategy, conversionService, endpointDefaultConfiguration); } diff --git a/router/src/main/java/io/micronaut/web/router/AbstractRouteMatch.java b/router/src/main/java/io/micronaut/web/router/AbstractRouteMatch.java index 0bef4d0c789..aae32611cb1 100644 --- a/router/src/main/java/io/micronaut/web/router/AbstractRouteMatch.java +++ b/router/src/main/java/io/micronaut/web/router/AbstractRouteMatch.java @@ -50,7 +50,7 @@ abstract class AbstractRouteMatch implements MethodBasedRouteMatch { protected final MethodExecutionHandle executableMethod; - protected final ConversionService conversionService; + protected final ConversionService conversionService; protected final DefaultRouteBuilder.AbstractRoute abstractRoute; protected final List consumedMediaTypes; protected final List producedMediaTypes; @@ -61,7 +61,7 @@ abstract class AbstractRouteMatch implements MethodBasedRouteMatch { * @param abstractRoute The abstract route builder * @param conversionService The conversion service */ - protected AbstractRouteMatch(DefaultRouteBuilder.AbstractRoute abstractRoute, ConversionService conversionService) { + protected AbstractRouteMatch(DefaultRouteBuilder.AbstractRoute abstractRoute, ConversionService conversionService) { this.abstractRoute = abstractRoute; //noinspection unchecked this.executableMethod = (MethodExecutionHandle) abstractRoute.targetMethod; @@ -208,7 +208,7 @@ public ReturnType getReturnType() { @Override public R invoke(Object... arguments) { - ConversionService conversionService = this.conversionService; + ConversionService conversionService = this.conversionService; Argument[] targetArguments = getArguments(); if (targetArguments.length == 0) { @@ -245,7 +245,7 @@ public R execute(Map argumentValues) { if (targetArguments.length == 0) { return executableMethod.invoke(); } else { - ConversionService conversionService = this.conversionService; + ConversionService conversionService = this.conversionService; Map uriVariables = getVariableValues(); List argumentList = new ArrayList<>(argumentValues.size()); diff --git a/router/src/main/java/io/micronaut/web/router/AnnotatedFilterRouteBuilder.java b/router/src/main/java/io/micronaut/web/router/AnnotatedFilterRouteBuilder.java index 40b1214f78a..70a83c13428 100644 --- a/router/src/main/java/io/micronaut/web/router/AnnotatedFilterRouteBuilder.java +++ b/router/src/main/java/io/micronaut/web/router/AnnotatedFilterRouteBuilder.java @@ -57,7 +57,7 @@ public AnnotatedFilterRouteBuilder( BeanContext beanContext, ExecutionHandleLocator executionHandleLocator, UriNamingStrategy uriNamingStrategy, - ConversionService conversionService, + ConversionService conversionService, @Nullable ServerContextPathProvider contextPathProvider) { super(executionHandleLocator, uriNamingStrategy, conversionService); this.contextPathProvider = contextPathProvider; diff --git a/router/src/main/java/io/micronaut/web/router/AnnotatedMethodRouteBuilder.java b/router/src/main/java/io/micronaut/web/router/AnnotatedMethodRouteBuilder.java index cc3fa21523b..f0ebcef3e8e 100644 --- a/router/src/main/java/io/micronaut/web/router/AnnotatedMethodRouteBuilder.java +++ b/router/src/main/java/io/micronaut/web/router/AnnotatedMethodRouteBuilder.java @@ -54,7 +54,7 @@ public class AnnotatedMethodRouteBuilder extends DefaultRouteBuilder implements * @param uriNamingStrategy The URI naming strategy * @param conversionService The conversion service */ - public AnnotatedMethodRouteBuilder(ExecutionHandleLocator executionHandleLocator, UriNamingStrategy uriNamingStrategy, ConversionService conversionService) { + public AnnotatedMethodRouteBuilder(ExecutionHandleLocator executionHandleLocator, UriNamingStrategy uriNamingStrategy, ConversionService conversionService) { super(executionHandleLocator, uriNamingStrategy, conversionService); httpMethodsHandlers.put(Get.class, (RouteDefinition definition) -> { final BeanDefinition bean = definition.beanDefinition; diff --git a/router/src/main/java/io/micronaut/web/router/DefaultRouteBuilder.java b/router/src/main/java/io/micronaut/web/router/DefaultRouteBuilder.java index 1831c688ca6..6736bdbb72a 100644 --- a/router/src/main/java/io/micronaut/web/router/DefaultRouteBuilder.java +++ b/router/src/main/java/io/micronaut/web/router/DefaultRouteBuilder.java @@ -69,7 +69,7 @@ public abstract class DefaultRouteBuilder implements RouteBuilder { static final Object NO_VALUE = new Object(); protected final ExecutionHandleLocator executionHandleLocator; protected final UriNamingStrategy uriNamingStrategy; - protected final ConversionService conversionService; + protected final ConversionService conversionService; protected final Charset defaultCharset; private DefaultUriRoute currentParentRoute = null; @@ -99,7 +99,7 @@ public DefaultRouteBuilder(ExecutionHandleLocator executionHandleLocator, UriNam * @param uriNamingStrategy The URI naming strategy * @param conversionService The conversion service */ - public DefaultRouteBuilder(ExecutionHandleLocator executionHandleLocator, UriNamingStrategy uriNamingStrategy, ConversionService conversionService) { + public DefaultRouteBuilder(ExecutionHandleLocator executionHandleLocator, UriNamingStrategy uriNamingStrategy, ConversionService conversionService) { this.executionHandleLocator = executionHandleLocator; this.uriNamingStrategy = uriNamingStrategy; this.conversionService = conversionService; @@ -383,10 +383,10 @@ protected UriRoute buildRoute(HttpMethod httpMethod, String uri, MethodExecution private UriRoute buildRoute(String httpMethodName, HttpMethod httpMethod, String uri, MethodExecutionHandle executableHandle) { UriRoute route; if (currentParentRoute != null) { - route = new DefaultUriRoute(httpMethod, currentParentRoute.uriMatchTemplate.nest(uri), executableHandle, httpMethodName); + route = new DefaultUriRoute(httpMethod, currentParentRoute.uriMatchTemplate.nest(uri), executableHandle, httpMethodName, conversionService); currentParentRoute.nestedRoutes.add((DefaultUriRoute) route); } else { - route = new DefaultUriRoute(httpMethod, uri, executableHandle, httpMethodName); + route = new DefaultUriRoute(httpMethod, uri, executableHandle, httpMethodName, conversionService); } this.uriRoutes.add(route); @@ -418,7 +418,7 @@ protected UriRoute buildBeanRoute(String httpMethodName, HttpMethod httpMethod, abstract class AbstractRoute implements MethodBasedRoute, RouteInfo { protected final List>> conditions = new ArrayList<>(); protected final MethodExecutionHandle targetMethod; - protected final ConversionService conversionService; + protected final ConversionService conversionService; protected List consumesMediaTypes; protected List producesMediaTypes; protected String bodyArgumentName; @@ -442,7 +442,7 @@ abstract class AbstractRoute implements MethodBasedRoute, RouteInfo { * @param conversionService The conversion service * @param mediaTypes The media types */ - AbstractRoute(MethodExecutionHandle targetMethod, ConversionService conversionService, List mediaTypes) { + AbstractRoute(MethodExecutionHandle targetMethod, ConversionService conversionService, List mediaTypes) { this.targetMethod = targetMethod; this.conversionService = conversionService; this.consumesMediaTypes = mediaTypes; @@ -651,7 +651,7 @@ class DefaultErrorRoute extends AbstractRoute implements ErrorRoute { * @param targetMethod The target method execution handle * @param conversionService The conversion service */ - public DefaultErrorRoute(Class error, MethodExecutionHandle targetMethod, ConversionService conversionService) { + public DefaultErrorRoute(Class error, MethodExecutionHandle targetMethod, ConversionService conversionService) { this(null, error, targetMethod, conversionService); } @@ -664,7 +664,7 @@ public DefaultErrorRoute(Class error, MethodExecutionHandle public DefaultErrorRoute( Class originatingClass, Class error, MethodExecutionHandle targetMethod, - ConversionService conversionService) { + ConversionService conversionService) { super(targetMethod, conversionService, Collections.emptyList()); this.originatingClass = originatingClass; this.error = error; @@ -772,7 +772,7 @@ class DefaultStatusRoute extends AbstractRoute implements StatusRoute { * @param targetMethod The target method execution handle * @param conversionService The conversion service */ - public DefaultStatusRoute(HttpStatus status, MethodExecutionHandle targetMethod, ConversionService conversionService) { + public DefaultStatusRoute(HttpStatus status, MethodExecutionHandle targetMethod, ConversionService conversionService) { this(null, status, targetMethod, conversionService); } @@ -782,7 +782,7 @@ public DefaultStatusRoute(HttpStatus status, MethodExecutionHandle targetMethod, * @param targetMethod The target method execution handle * @param conversionService The conversion service */ - public DefaultStatusRoute(Class originatingClass, HttpStatus status, MethodExecutionHandle targetMethod, ConversionService conversionService) { + public DefaultStatusRoute(Class originatingClass, HttpStatus status, MethodExecutionHandle targetMethod, ConversionService conversionService) { super(targetMethod, conversionService, Collections.emptyList()); this.originatingClass = originatingClass; this.status = status; @@ -880,9 +880,13 @@ class DefaultUriRoute extends AbstractRoute implements UriRoute { * @param httpMethod The HTTP method * @param uriTemplate The URI Template as a {@link CharSequence} * @param targetMethod The target method execution handle + * @param conversionService The conversion service */ - DefaultUriRoute(HttpMethod httpMethod, CharSequence uriTemplate, MethodExecutionHandle targetMethod) { - this(httpMethod, uriTemplate, targetMethod, httpMethod.name()); + DefaultUriRoute(HttpMethod httpMethod, + CharSequence uriTemplate, + MethodExecutionHandle targetMethod, + ConversionService conversionService) { + this(httpMethod, uriTemplate, targetMethod, httpMethod.name(), conversionService); } /** @@ -890,9 +894,14 @@ class DefaultUriRoute extends AbstractRoute implements UriRoute { * @param uriTemplate The URI Template as a {@link CharSequence} * @param targetMethod The target method execution handle * @param httpMethodName The actual name of the method - may differ from {@link HttpMethod#name()} for non-standard http methods + * @param conversionService The conversion service */ - DefaultUriRoute(HttpMethod httpMethod, CharSequence uriTemplate, MethodExecutionHandle targetMethod, String httpMethodName) { - this(httpMethod, uriTemplate, MediaType.APPLICATION_JSON_TYPE, targetMethod, httpMethodName); + DefaultUriRoute(HttpMethod httpMethod, + CharSequence uriTemplate, + MethodExecutionHandle targetMethod, + String httpMethodName, + ConversionService conversionService) { + this(httpMethod, uriTemplate, MediaType.APPLICATION_JSON_TYPE, targetMethod, httpMethodName, conversionService); } /** @@ -900,9 +909,14 @@ class DefaultUriRoute extends AbstractRoute implements UriRoute { * @param uriTemplate The URI Template as a {@link CharSequence} * @param mediaType The Media type * @param targetMethod The target method execution handle + * @param conversionService The conversion service */ - DefaultUriRoute(HttpMethod httpMethod, CharSequence uriTemplate, MediaType mediaType, MethodExecutionHandle targetMethod) { - this(httpMethod, uriTemplate, mediaType, targetMethod, httpMethod.name()); + DefaultUriRoute(HttpMethod httpMethod, + CharSequence uriTemplate, + MediaType mediaType, + MethodExecutionHandle targetMethod, + ConversionService conversionService) { + this(httpMethod, uriTemplate, mediaType, targetMethod, httpMethod.name(), conversionService); } /** @@ -911,18 +925,28 @@ class DefaultUriRoute extends AbstractRoute implements UriRoute { * @param mediaType The Media type * @param targetMethod The target method execution handle * @param httpMethodName The actual name of the method - may differ from {@link HttpMethod#name()} for non-standard http methods + * @param conversionService The conversion service */ - DefaultUriRoute(HttpMethod httpMethod, CharSequence uriTemplate, MediaType mediaType, MethodExecutionHandle targetMethod, String httpMethodName) { - this(httpMethod, new UriMatchTemplate(uriTemplate), Collections.singletonList(mediaType), targetMethod, httpMethodName); + DefaultUriRoute(HttpMethod httpMethod, + CharSequence uriTemplate, + MediaType mediaType, + MethodExecutionHandle targetMethod, + String httpMethodName, + ConversionService conversionService) { + this(httpMethod, new UriMatchTemplate(uriTemplate), Collections.singletonList(mediaType), targetMethod, httpMethodName, conversionService); } /** * @param httpMethod The HTTP method * @param uriTemplate The URI Template as a {@link UriMatchTemplate} * @param targetMethod The target method execution handle + * @param conversionService The conversion service */ - DefaultUriRoute(HttpMethod httpMethod, UriMatchTemplate uriTemplate, MethodExecutionHandle targetMethod) { - this(httpMethod, uriTemplate, targetMethod, httpMethod.name()); + DefaultUriRoute(HttpMethod httpMethod, + UriMatchTemplate uriTemplate, + MethodExecutionHandle targetMethod, + ConversionService conversionService) { + this(httpMethod, uriTemplate, targetMethod, httpMethod.name(), conversionService); } /** @@ -930,9 +954,14 @@ class DefaultUriRoute extends AbstractRoute implements UriRoute { * @param uriTemplate The URI Template as a {@link UriMatchTemplate} * @param targetMethod The target method execution handle * @param httpMethodName The actual name of the method - may differ from {@link HttpMethod#name()} for non-standard http methods + * @param conversionService The conversion service */ - DefaultUriRoute(HttpMethod httpMethod, UriMatchTemplate uriTemplate, MethodExecutionHandle targetMethod, String httpMethodName) { - this(httpMethod, uriTemplate, Collections.singletonList(MediaType.APPLICATION_JSON_TYPE), targetMethod, httpMethodName); + DefaultUriRoute(HttpMethod httpMethod, + UriMatchTemplate uriTemplate, + MethodExecutionHandle targetMethod, + String httpMethodName, + ConversionService conversionService) { + this(httpMethod, uriTemplate, Collections.singletonList(MediaType.APPLICATION_JSON_TYPE), targetMethod, httpMethodName, conversionService); } /** @@ -940,9 +969,14 @@ class DefaultUriRoute extends AbstractRoute implements UriRoute { * @param uriTemplate The URI Template as a {@link UriMatchTemplate} * @param mediaTypes The media types * @param targetMethod The target method execution handle + * @param conversionService The conversion service */ - DefaultUriRoute(HttpMethod httpMethod, UriMatchTemplate uriTemplate, List mediaTypes, MethodExecutionHandle targetMethod) { - this(httpMethod, uriTemplate, mediaTypes, targetMethod, httpMethod.name()); + DefaultUriRoute(HttpMethod httpMethod, + UriMatchTemplate uriTemplate, + List mediaTypes, + MethodExecutionHandle targetMethod, + ConversionService conversionService) { + this(httpMethod, uriTemplate, mediaTypes, targetMethod, httpMethod.name(), conversionService); } /** @@ -951,9 +985,14 @@ class DefaultUriRoute extends AbstractRoute implements UriRoute { * @param mediaTypes The media types * @param targetMethod The target method execution handle * @param httpMethodName The actual name of the method - may differ from {@link HttpMethod#name()} for non-standard http methods + * @param conversionService The conversion service */ - DefaultUriRoute(HttpMethod httpMethod, UriMatchTemplate uriTemplate, List mediaTypes, MethodExecutionHandle targetMethod, String httpMethodName) { - super(targetMethod, ConversionService.SHARED, mediaTypes); + DefaultUriRoute(HttpMethod httpMethod, UriMatchTemplate uriTemplate, + List mediaTypes, + MethodExecutionHandle targetMethod, + String httpMethodName, + ConversionService conversionService) { + super(targetMethod, conversionService, mediaTypes); this.httpMethod = httpMethod; this.uriMatchTemplate = uriTemplate; this.httpMethodName = httpMethodName; diff --git a/router/src/main/java/io/micronaut/web/router/DefaultUriRouteMatch.java b/router/src/main/java/io/micronaut/web/router/DefaultUriRouteMatch.java index 89ee549a1b2..9349cc21ed3 100644 --- a/router/src/main/java/io/micronaut/web/router/DefaultUriRouteMatch.java +++ b/router/src/main/java/io/micronaut/web/router/DefaultUriRouteMatch.java @@ -55,7 +55,7 @@ class DefaultUriRouteMatch extends AbstractRouteMatch implements Uri */ DefaultUriRouteMatch(UriMatchInfo matchInfo, DefaultRouteBuilder.DefaultUriRoute uriRoute, - Charset defaultCharset, ConversionService conversionService + Charset defaultCharset, ConversionService conversionService ) { super(uriRoute, conversionService); this.uriRoute = uriRoute; diff --git a/router/src/main/java/io/micronaut/web/router/ErrorRouteMatch.java b/router/src/main/java/io/micronaut/web/router/ErrorRouteMatch.java index 245cbfb485a..7e7f0e04c9f 100644 --- a/router/src/main/java/io/micronaut/web/router/ErrorRouteMatch.java +++ b/router/src/main/java/io/micronaut/web/router/ErrorRouteMatch.java @@ -46,7 +46,7 @@ class ErrorRouteMatch extends AbstractRouteMatch { * @param abstractRoute The abstract route * @param conversionService The conversion service */ - ErrorRouteMatch(Throwable error, DefaultRouteBuilder.AbstractRoute abstractRoute, ConversionService conversionService) { + ErrorRouteMatch(Throwable error, DefaultRouteBuilder.AbstractRoute abstractRoute, ConversionService conversionService) { super(abstractRoute, conversionService); this.error = error; this.variables = new LinkedHashMap<>(); diff --git a/router/src/main/java/io/micronaut/web/router/StatusRouteMatch.java b/router/src/main/java/io/micronaut/web/router/StatusRouteMatch.java index f42d363d973..5470fcb0ab9 100644 --- a/router/src/main/java/io/micronaut/web/router/StatusRouteMatch.java +++ b/router/src/main/java/io/micronaut/web/router/StatusRouteMatch.java @@ -40,7 +40,7 @@ class StatusRouteMatch extends AbstractRouteMatch { * @param abstractRoute The abstract route * @param conversionService The conversion service */ - StatusRouteMatch(HttpStatus httpStatus, DefaultRouteBuilder.AbstractRoute abstractRoute, ConversionService conversionService) { + StatusRouteMatch(HttpStatus httpStatus, DefaultRouteBuilder.AbstractRoute abstractRoute, ConversionService conversionService) { super(abstractRoute, conversionService); this.httpStatus = httpStatus; this.requiredArguments = new ArrayList<>(Arrays.asList(getArguments())); diff --git a/router/src/test/groovy/io/micronaut/context/router/EachBeanRouteBuilderSpec.groovy b/router/src/test/groovy/io/micronaut/context/router/EachBeanRouteBuilderSpec.groovy index c181cf4e248..932c06056e5 100644 --- a/router/src/test/groovy/io/micronaut/context/router/EachBeanRouteBuilderSpec.groovy +++ b/router/src/test/groovy/io/micronaut/context/router/EachBeanRouteBuilderSpec.groovy @@ -59,7 +59,7 @@ class EachBeanRouteBuilderSpec extends Specification { MyRouteBuilder(ExecutionHandleLocator executionHandleLocator, UriNamingStrategy uriNamingStrategy, - ConversionService conversionService, + ConversionService conversionService, BeanContext beanContext) { super(executionHandleLocator, uriNamingStrategy, conversionService) diff --git a/runtime/src/main/java/io/micronaut/reactive/flow/converters/FlowConverterRegistrar.java b/runtime/src/main/java/io/micronaut/reactive/flow/converters/FlowConverterRegistrar.java index cd967a23292..20e06a19983 100644 --- a/runtime/src/main/java/io/micronaut/reactive/flow/converters/FlowConverterRegistrar.java +++ b/runtime/src/main/java/io/micronaut/reactive/flow/converters/FlowConverterRegistrar.java @@ -16,7 +16,7 @@ package io.micronaut.reactive.flow.converters; import io.micronaut.context.annotation.Requires; -import io.micronaut.core.convert.ConversionService; +import io.micronaut.core.convert.MutableConversionService; import io.micronaut.core.convert.TypeConverterRegistrar; import jakarta.inject.Singleton; import kotlinx.coroutines.flow.Flow; @@ -34,7 +34,7 @@ @Requires(classes = {Flux.class, ReactiveFlowKt.class}) public class FlowConverterRegistrar implements TypeConverterRegistrar { @Override - public void register(ConversionService conversionService) { + public void register(MutableConversionService conversionService) { // Flow conversionService.addConverter(Flow.class, Flux.class, flow -> Flux.from(ReactiveFlowKt.asPublisher(flow)) diff --git a/runtime/src/main/java/io/micronaut/runtime/http/codec/TextPlainCodec.java b/runtime/src/main/java/io/micronaut/runtime/http/codec/TextPlainCodec.java index 5ef14fa348d..beab892b8a9 100644 --- a/runtime/src/main/java/io/micronaut/runtime/http/codec/TextPlainCodec.java +++ b/runtime/src/main/java/io/micronaut/runtime/http/codec/TextPlainCodec.java @@ -59,15 +59,19 @@ public class TextPlainCodec implements MediaTypeCodec { private final Charset defaultCharset; private final List additionalTypes; + private final ConversionService conversionService; /** - * @param defaultCharset The default charset used for serialization and deserialization - * @param codecConfiguration The configuration for the codec + * @param defaultCharset The default charset used for serialization and deserialization + * @param codecConfiguration The configuration for the codec + * @param conversionService The conversion service */ @Inject public TextPlainCodec(@Value("${" + ApplicationConfiguration.DEFAULT_CHARSET + "}") Optional defaultCharset, - @Named(CONFIGURATION_QUALIFIER) @Nullable CodecConfiguration codecConfiguration) { + @Named(CONFIGURATION_QUALIFIER) @Nullable CodecConfiguration codecConfiguration, + ConversionService conversionService) { this.defaultCharset = defaultCharset.orElse(StandardCharsets.UTF_8); + this.conversionService = conversionService; if (codecConfiguration != null) { this.additionalTypes = codecConfiguration.getAdditionalTypes(); } else { @@ -76,10 +80,12 @@ public TextPlainCodec(@Value("${" + ApplicationConfiguration.DEFAULT_CHARSET + " } /** - * @param defaultCharset The default charset used for serialization and deserialization + * @param defaultCharset The default charset used for serialization and deserialization + * @param conversionService The conversion service */ - public TextPlainCodec(Charset defaultCharset) { + public TextPlainCodec(Charset defaultCharset, ConversionService conversionService) { this.defaultCharset = defaultCharset != null ? defaultCharset : StandardCharsets.UTF_8; + this.conversionService = conversionService; this.additionalTypes = Collections.emptyList(); } @@ -97,7 +103,7 @@ public T decode(Argument type, ByteBuffer buffer) throws CodecExceptio if (CharSequence.class.isAssignableFrom(type.getType())) { return (T) text; } - return ConversionService.SHARED + return conversionService .convert(text, type) .orElseThrow(() -> new CodecException("Cannot decode byte buffer with value [" + text + "] to type: " + type)); } @@ -108,7 +114,7 @@ public T decode(Argument type, byte[] bytes) throws CodecException { if (CharSequence.class.isAssignableFrom(type.getType())) { return (T) text; } - return ConversionService.SHARED + return conversionService .convert(text, type) .orElseThrow(() -> new CodecException("Cannot decode bytes with value [" + text + "] to type: " + type)); } diff --git a/runtime/src/test/groovy/io/micronaut/runtime/ConversionServiceIsResetSpec.groovy b/runtime/src/test/groovy/io/micronaut/runtime/ConversionServiceIsResetSpec.groovy index 7bd5ee40bc8..d5c9d61bd3b 100644 --- a/runtime/src/test/groovy/io/micronaut/runtime/ConversionServiceIsResetSpec.groovy +++ b/runtime/src/test/groovy/io/micronaut/runtime/ConversionServiceIsResetSpec.groovy @@ -18,6 +18,7 @@ package io.micronaut.runtime import io.micronaut.context.ApplicationContext import io.micronaut.core.convert.ConversionContext import io.micronaut.core.convert.ConversionService +import io.micronaut.core.convert.MutableConversionService import io.micronaut.core.convert.TypeConverter import spock.lang.Specification @@ -32,7 +33,7 @@ class ConversionServiceIsResetSpec extends Specification { return Optional.of(new B()) } } - ctx.getBean(ConversionService).addConverter(A, B, typeConverter) + ctx.getBean(MutableConversionService).addConverter(A, B, typeConverter) when: def result = ctx.getBean(ConversionService).convert(new A(), B) diff --git a/runtime/src/test/groovy/io/micronaut/runtime/converters/time/TimeConverterRegistrarSpec.groovy b/runtime/src/test/groovy/io/micronaut/runtime/converters/time/TimeConverterRegistrarSpec.groovy index 830825cb871..ec1804d12ee 100644 --- a/runtime/src/test/groovy/io/micronaut/runtime/converters/time/TimeConverterRegistrarSpec.groovy +++ b/runtime/src/test/groovy/io/micronaut/runtime/converters/time/TimeConverterRegistrarSpec.groovy @@ -16,7 +16,7 @@ package io.micronaut.runtime.converters.time import io.micronaut.core.convert.ConversionService -import io.micronaut.core.convert.DefaultConversionService +import io.micronaut.core.convert.DefaultMutableConversionService import spock.lang.Specification import spock.lang.Unroll @@ -35,7 +35,7 @@ class TimeConverterRegistrarSpec extends Specification { @Unroll void "test convert duration #val"() { given: - ConversionService conversionService = new DefaultConversionService() + ConversionService conversionService = new DefaultMutableConversionService() new TimeConverterRegistrar().register(conversionService) @@ -55,7 +55,7 @@ class TimeConverterRegistrarSpec extends Specification { @Unroll void "test converts a #sourceObject.class.name to a #targetType.name"() { given: - ConversionService conversionService = new DefaultConversionService() + ConversionService conversionService = new DefaultMutableConversionService() new TimeConverterRegistrar().register(conversionService) expect: diff --git a/runtime/src/test/groovy/io/micronaut/runtime/http/codec/TextPlainCodecSpec.groovy b/runtime/src/test/groovy/io/micronaut/runtime/http/codec/TextPlainCodecSpec.groovy index eb879916109..f30f866d256 100644 --- a/runtime/src/test/groovy/io/micronaut/runtime/http/codec/TextPlainCodecSpec.groovy +++ b/runtime/src/test/groovy/io/micronaut/runtime/http/codec/TextPlainCodecSpec.groovy @@ -16,6 +16,7 @@ package io.micronaut.runtime.http.codec import io.micronaut.context.ApplicationContext +import io.micronaut.core.convert.MutableConversionService import io.micronaut.core.io.buffer.ByteBuffer import io.micronaut.core.io.buffer.ByteBufferFactory import io.micronaut.http.MediaType @@ -26,7 +27,7 @@ import java.nio.charset.StandardCharsets class TextPlainCodecSpec extends Specification { - @Shared TextPlainCodec codec = new TextPlainCodec(StandardCharsets.UTF_8) + @Shared TextPlainCodec codec = new TextPlainCodec(StandardCharsets.UTF_8, MutableConversionService.create()) void "test the buffer min and max are correct for special characters"() { given: diff --git a/src/main/docs/guide/appendix/breaks.adoc b/src/main/docs/guide/appendix/breaks.adoc index 8a2127546ba..211dcdaf1c5 100644 --- a/src/main/docs/guide/appendix/breaks.adoc +++ b/src/main/docs/guide/appendix/breaks.adoc @@ -50,6 +50,11 @@ Annotations with the retention CLASS are not available in the annotation metadat Interceptors with multiple interceptor bindings annotations now require the same set of annotations to be present at the intercepted point. In the Micronaut 3 an interceptor with multiple binding annotations would need at least one of the binding annotations to be present at the intercepted point. +==== `ConversionService` and `ConversionService.SHARED` is no longer mutable + +New type converters can be added to api:core.convert.MutableConversionService[] retrieved from the bean context or by declaring a bean of type api:core.convert.TypeConverter[]. +To register a type converter into `ConversionService.SHARED`, the registration needs to be done via the service loader. + == 3.3.0 - The <> is now disabled by default. To enable it, you must update your endpoint config: diff --git a/src/main/docs/guide/config/customTypeConverter.adoc b/src/main/docs/guide/config/customTypeConverter.adoc index 94f32456cb3..076ffd96b15 100644 --- a/src/main/docs/guide/config/customTypeConverter.adoc +++ b/src/main/docs/guide/config/customTypeConverter.adoc @@ -21,3 +21,5 @@ snippet::io.micronaut.docs.config.converters.MapToLocalDateConverter[tags="impor <1> The class implements api:core.convert.TypeConverter[] which has two generic arguments, the type you are converting from, and the type you are converting to <2> The implementation delegates to the default shared conversion service to convert the values from the Map used to create a `LocalDate` <3> If an exception occurs during binding, call `reject(..)` which propagates additional information to the container + +NOTE: It's possible to add a custom type converter into `ConversionService.SHARED` by registering it via the service loader. diff --git a/src/main/docs/guide/httpServer/binding.adoc b/src/main/docs/guide/httpServer/binding.adoc index ab80a21759c..11f6b608c18 100644 --- a/src/main/docs/guide/httpServer/binding.adoc +++ b/src/main/docs/guide/httpServer/binding.adoc @@ -90,7 +90,7 @@ WARNING: Since Java does not retain argument names in bytecode, you must compile Generally any type that can be converted from a String representation to a Java type via the link:{api}/io/micronaut/core/convert/ConversionService.html[ConversionService] API can be bound to. -This includes most common Java types, however additional link:{api}/io/micronaut/core/convert/TypeConverter.html[TypeConverter] instances can be registered by creating `@Singleton` beans of type `TypeConverter`. +This includes most common Java types, however additional link:{api}/io/micronaut/core/convert/TypeConverter.html[TypeConverter] instances can be registered via the service loader or by creating beans of type `TypeConverter`, The handling of nullability deserves special mention. Consider for example the following example: diff --git a/test-suite-groovy/src/test/groovy/io/micronaut/docs/config/converters/MapToLocalDateConverter.groovy b/test-suite-groovy/src/test/groovy/io/micronaut/docs/config/converters/MapToLocalDateConverter.groovy index 22a0a54363d..5fb7d0a3f92 100644 --- a/test-suite-groovy/src/test/groovy/io/micronaut/docs/config/converters/MapToLocalDateConverter.groovy +++ b/test-suite-groovy/src/test/groovy/io/micronaut/docs/config/converters/MapToLocalDateConverter.groovy @@ -15,18 +15,20 @@ */ package io.micronaut.docs.config.converters -// tag::imports[] +import io.micronaut.context.annotation.Prototype import io.micronaut.core.convert.ConversionContext + +// tag::imports[] + import io.micronaut.core.convert.ConversionService import io.micronaut.core.convert.TypeConverter -import jakarta.inject.Singleton import java.time.DateTimeException import java.time.LocalDate // end::imports[] // tag::class[] -@Singleton +@Prototype class MapToLocalDateConverter implements TypeConverter { // <1> @Override Optional convert(Map propertyMap, Class targetType, ConversionContext context) { diff --git a/test-suite-groovy/src/test/groovy/io/micronaut/docs/http/server/bind/annotation/ShoppingCartRequestArgumentBinder.groovy b/test-suite-groovy/src/test/groovy/io/micronaut/docs/http/server/bind/annotation/ShoppingCartRequestArgumentBinder.groovy index 7ee2304587b..7af744a118c 100644 --- a/test-suite-groovy/src/test/groovy/io/micronaut/docs/http/server/bind/annotation/ShoppingCartRequestArgumentBinder.groovy +++ b/test-suite-groovy/src/test/groovy/io/micronaut/docs/http/server/bind/annotation/ShoppingCartRequestArgumentBinder.groovy @@ -17,11 +17,11 @@ import jakarta.inject.Singleton class ShoppingCartRequestArgumentBinder implements AnnotatedRequestArgumentBinder { //<1> - private final ConversionService conversionService + private final ConversionService conversionService private final JacksonObjectSerializer objectSerializer ShoppingCartRequestArgumentBinder( - ConversionService conversionService, + ConversionService conversionService, JacksonObjectSerializer objectSerializer) { this.conversionService = conversionService this.objectSerializer = objectSerializer diff --git a/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/config/converters/MapToLocalDateConverter.kt b/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/config/converters/MapToLocalDateConverter.kt index 1b65c027717..c4b3c050665 100644 --- a/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/config/converters/MapToLocalDateConverter.kt +++ b/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/config/converters/MapToLocalDateConverter.kt @@ -16,6 +16,7 @@ package io.micronaut.docs.config.converters // tag::imports[] +import io.micronaut.context.annotation.Prototype import io.micronaut.core.convert.ConversionContext import io.micronaut.core.convert.ConversionService import io.micronaut.core.convert.TypeConverter @@ -26,7 +27,7 @@ import jakarta.inject.Singleton // end::imports[] // tag::class[] -@Singleton +@Prototype class MapToLocalDateConverter : TypeConverter, LocalDate> { // <1> override fun convert(propertyMap: Map<*, *>, targetType: Class, context: ConversionContext): Optional { val day = ConversionService.SHARED.convert(propertyMap["day"], Int::class.java) diff --git a/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/http/server/bind/annotation/ShoppingCartRequestArgumentBinder.kt b/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/http/server/bind/annotation/ShoppingCartRequestArgumentBinder.kt index 24d995b295d..c6f7c3653b8 100644 --- a/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/http/server/bind/annotation/ShoppingCartRequestArgumentBinder.kt +++ b/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/http/server/bind/annotation/ShoppingCartRequestArgumentBinder.kt @@ -13,7 +13,7 @@ import jakarta.inject.Singleton @Singleton class ShoppingCartRequestArgumentBinder( - private val conversionService: ConversionService<*>, + private val conversionService: ConversionService, private val objectSerializer: JacksonObjectSerializer ) : AnnotatedRequestArgumentBinder { //<1> diff --git a/test-suite/src/test/java/io/micronaut/docs/config/converters/MapToLocalDateConverter.java b/test-suite/src/test/java/io/micronaut/docs/config/converters/MapToLocalDateConverter.java index 04696212fa8..9de76daf8ee 100644 --- a/test-suite/src/test/java/io/micronaut/docs/config/converters/MapToLocalDateConverter.java +++ b/test-suite/src/test/java/io/micronaut/docs/config/converters/MapToLocalDateConverter.java @@ -16,11 +16,12 @@ package io.micronaut.docs.config.converters; // tag::imports[] + +import io.micronaut.context.annotation.Prototype; import io.micronaut.core.convert.ConversionContext; import io.micronaut.core.convert.ConversionService; import io.micronaut.core.convert.TypeConverter; -import jakarta.inject.Singleton; import java.time.DateTimeException; import java.time.LocalDate; import java.util.Map; @@ -28,7 +29,7 @@ // end::imports[] // tag::class[] -@Singleton +@Prototype public class MapToLocalDateConverter implements TypeConverter { // <1> @Override public Optional convert(Map propertyMap, Class targetType, ConversionContext context) { diff --git a/test-suite/src/test/java/io/micronaut/docs/http/server/bind/annotation/ShoppingCartRequestArgumentBinder.java b/test-suite/src/test/java/io/micronaut/docs/http/server/bind/annotation/ShoppingCartRequestArgumentBinder.java index 202dd021199..57554138262 100644 --- a/test-suite/src/test/java/io/micronaut/docs/http/server/bind/annotation/ShoppingCartRequestArgumentBinder.java +++ b/test-suite/src/test/java/io/micronaut/docs/http/server/bind/annotation/ShoppingCartRequestArgumentBinder.java @@ -17,10 +17,10 @@ public class ShoppingCartRequestArgumentBinder implements AnnotatedRequestArgumentBinder { //<1> - private final ConversionService conversionService; + private final ConversionService conversionService; private final JacksonObjectSerializer objectSerializer; - public ShoppingCartRequestArgumentBinder(ConversionService conversionService, + public ShoppingCartRequestArgumentBinder(ConversionService conversionService, JacksonObjectSerializer objectSerializer) { this.conversionService = conversionService; this.objectSerializer = objectSerializer; diff --git a/websocket/src/main/java/io/micronaut/websocket/bind/WebSocketStateBinderRegistry.java b/websocket/src/main/java/io/micronaut/websocket/bind/WebSocketStateBinderRegistry.java index e9367d0fac8..b892f17cf27 100644 --- a/websocket/src/main/java/io/micronaut/websocket/bind/WebSocketStateBinderRegistry.java +++ b/websocket/src/main/java/io/micronaut/websocket/bind/WebSocketStateBinderRegistry.java @@ -51,12 +51,13 @@ public class WebSocketStateBinderRegistry implements ArgumentBinderRegistry sessionBinder = (context, source) -> () -> Optional.of(source.getSession()); this.byType.put(WebSocketSession.class, sessionBinder); - this.queryValueArgumentBinder = new QueryValueArgumentBinder<>(ConversionService.SHARED); + this.queryValueArgumentBinder = new QueryValueArgumentBinder<>(conversionService); } @Override From 0a67e5025c7b34a424bbe3a06907175644fa64c4 Mon Sep 17 00:00:00 2001 From: Graeme Rocher Date: Fri, 28 Oct 2022 16:20:10 +0200 Subject: [PATCH 169/743] Improve binary incompatibility for existing processors (#8250) --- .../java/io/micronaut/inject/ast/Element.java | 58 ++++++++++++++++- .../micronaut/inject/ast/ElementFactory.java | 1 + ...tractElementAnnotationMetadataFactory.java | 9 ++- .../ElementAnnotationMetadata.java | 4 +- .../ElementAnnotationMetadataFactory.java | 3 +- ...mentMutableAnnotationMetadataDelegate.java | 32 ++++----- .../MutableAnnotationMetadataDelegate.java} | 26 ++++---- .../inject/ast/annotation/package-info.java | 26 ++++++++ .../inject/visitor/VisitorContext.java | 2 +- .../writer/AbstractBeanDefinitionBuilder.java | 2 +- ...roovyElementAnnotationMetadataFactory.java | 4 +- .../groovy/visitor/AbstractGroovyElement.java | 65 +++++++++++++++++-- .../visitor/GroovyAnnotationElement.java | 2 +- .../visitor/GroovyBeanDefinitionBuilder.java | 2 +- .../groovy/visitor/GroovyClassElement.java | 2 +- .../visitor/GroovyConstructorElement.java | 2 +- .../groovy/visitor/GroovyElementFactory.java | 2 +- .../visitor/GroovyEnumConstantElement.java | 2 +- .../ast/groovy/visitor/GroovyEnumElement.java | 2 +- .../groovy/visitor/GroovyFieldElement.java | 2 +- .../GroovyGenericPlaceholderElement.java | 2 +- .../groovy/visitor/GroovyMethodElement.java | 2 +- .../groovy/visitor/GroovyPackageElement.java | 2 +- .../visitor/GroovyParameterElement.java | 2 +- .../groovy/visitor/GroovyPropertyElement.java | 10 +-- .../groovy/visitor/GroovyVisitorContext.java | 2 +- .../groovy/visitor/GroovyWildcardElement.java | 2 +- .../BeanDefinitionInjectProcessor.java | 2 +- .../JavaElementAnnotationMetadataFactory.java | 4 +- .../visitor/AbstractJavaElement.java | 65 +++++++++++++++++-- .../visitor/JavaAnnotationElement.java | 2 +- .../visitor/JavaBeanDefinitionBuilder.java | 2 +- .../processing/visitor/JavaClassElement.java | 2 +- .../visitor/JavaConstructorElement.java | 2 +- .../visitor/JavaElementFactory.java | 2 +- .../visitor/JavaEnumConstantElement.java | 2 +- .../processing/visitor/JavaEnumElement.java | 2 +- .../processing/visitor/JavaFieldElement.java | 2 +- .../JavaGenericPlaceholderElement.java | 2 +- .../processing/visitor/JavaMethodElement.java | 2 +- .../visitor/JavaPackageElement.java | 2 +- .../visitor/JavaParameterElement.java | 2 +- .../visitor/JavaPropertyElement.java | 10 +-- .../visitor/JavaVisitorContext.java | 2 +- .../visitor/JavaWildcardElement.java | 2 +- 45 files changed, 287 insertions(+), 92 deletions(-) rename core-processor/src/main/java/io/micronaut/inject/{ => ast}/annotation/AbstractElementAnnotationMetadataFactory.java (98%) rename core-processor/src/main/java/io/micronaut/inject/ast/{ => annotation}/ElementAnnotationMetadata.java (83%) rename core-processor/src/main/java/io/micronaut/inject/ast/{ => annotation}/ElementAnnotationMetadataFactory.java (97%) rename core-processor/src/main/java/io/micronaut/inject/ast/{ => annotation}/ElementMutableAnnotationMetadataDelegate.java (68%) rename core-processor/src/main/java/io/micronaut/inject/ast/{ElementMutableAnnotationMetadata.java => annotation/MutableAnnotationMetadataDelegate.java} (85%) create mode 100644 core-processor/src/main/java/io/micronaut/inject/ast/annotation/package-info.java diff --git a/core-processor/src/main/java/io/micronaut/inject/ast/Element.java b/core-processor/src/main/java/io/micronaut/inject/ast/Element.java index 4646221626e..91952513316 100644 --- a/core-processor/src/main/java/io/micronaut/inject/ast/Element.java +++ b/core-processor/src/main/java/io/micronaut/inject/ast/Element.java @@ -17,12 +17,18 @@ import io.micronaut.core.annotation.AnnotatedElement; import io.micronaut.core.annotation.AnnotationMetadata; +import io.micronaut.core.annotation.AnnotationValue; +import io.micronaut.core.annotation.AnnotationValueBuilder; import io.micronaut.core.annotation.NonNull; import io.micronaut.core.naming.Described; +import io.micronaut.inject.ast.annotation.MutableAnnotationMetadataDelegate; +import java.lang.annotation.Annotation; import java.util.Collections; import java.util.Optional; import java.util.Set; +import java.util.function.Consumer; +import java.util.function.Predicate; /** * Stores data about a compile time element. The underlying object can be a class, field, or method. @@ -31,7 +37,7 @@ * @author graemerocher * @since 1.0 */ -public interface Element extends ElementMutableAnnotationMetadata, AnnotatedElement, Described { +public interface Element extends MutableAnnotationMetadataDelegate, AnnotatedElement, Described { /** * An empty array of elements. @@ -159,4 +165,54 @@ default String getDescription(boolean simple) { default Element withAnnotationMetadata(AnnotationMetadata annotationMetadata) { throw new UnsupportedOperationException("Element of type [" + getClass() + "] does not support copy constructor"); } + + @Override + default Element annotate(String annotationType, Consumer> consumer) { + return MutableAnnotationMetadataDelegate.super.annotate(annotationType, consumer); + } + + @Override + default Element removeAnnotation(String annotationType) { + return MutableAnnotationMetadataDelegate.super.removeAnnotation(annotationType); + } + + @Override + default Element removeAnnotation(Class annotationType) { + return MutableAnnotationMetadataDelegate.super.removeAnnotation(annotationType); + } + + @Override + default Element removeAnnotationIf(Predicate> predicate) { + return MutableAnnotationMetadataDelegate.super.removeAnnotationIf(predicate); + } + + @Override + default Element removeStereotype(String annotationType) { + return MutableAnnotationMetadataDelegate.super.removeStereotype(annotationType); + } + + @Override + default Element removeStereotype(Class annotationType) { + return MutableAnnotationMetadataDelegate.super.removeStereotype(annotationType); + } + + @Override + default Element annotate(String annotationType) { + return MutableAnnotationMetadataDelegate.super.annotate(annotationType); + } + + @Override + default Element annotate(Class annotationType, Consumer> consumer) { + return MutableAnnotationMetadataDelegate.super.annotate(annotationType, consumer); + } + + @Override + default Element annotate(Class annotationType) { + return MutableAnnotationMetadataDelegate.super.annotate(annotationType); + } + + @Override + default Element annotate(AnnotationValue annotationValue) { + return MutableAnnotationMetadataDelegate.super.annotate(annotationValue); + } } diff --git a/core-processor/src/main/java/io/micronaut/inject/ast/ElementFactory.java b/core-processor/src/main/java/io/micronaut/inject/ast/ElementFactory.java index e289cf43811..20478e793a4 100644 --- a/core-processor/src/main/java/io/micronaut/inject/ast/ElementFactory.java +++ b/core-processor/src/main/java/io/micronaut/inject/ast/ElementFactory.java @@ -16,6 +16,7 @@ package io.micronaut.inject.ast; import io.micronaut.core.annotation.NonNull; +import io.micronaut.inject.ast.annotation.ElementAnnotationMetadataFactory; import java.util.Map; diff --git a/core-processor/src/main/java/io/micronaut/inject/annotation/AbstractElementAnnotationMetadataFactory.java b/core-processor/src/main/java/io/micronaut/inject/ast/annotation/AbstractElementAnnotationMetadataFactory.java similarity index 98% rename from core-processor/src/main/java/io/micronaut/inject/annotation/AbstractElementAnnotationMetadataFactory.java rename to core-processor/src/main/java/io/micronaut/inject/ast/annotation/AbstractElementAnnotationMetadataFactory.java index 41fd6d2dcb1..8055ad66873 100644 --- a/core-processor/src/main/java/io/micronaut/inject/annotation/AbstractElementAnnotationMetadataFactory.java +++ b/core-processor/src/main/java/io/micronaut/inject/ast/annotation/AbstractElementAnnotationMetadataFactory.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.micronaut.inject.annotation; +package io.micronaut.inject.ast.annotation; import io.micronaut.core.annotation.AnnotationMetadata; import io.micronaut.core.annotation.AnnotationValue; @@ -21,15 +21,14 @@ import io.micronaut.core.annotation.NonNull; import io.micronaut.core.annotation.Nullable; import io.micronaut.core.util.ArgumentUtils; +import io.micronaut.inject.annotation.AbstractAnnotationMetadataBuilder; +import io.micronaut.inject.annotation.AnnotationMetadataHierarchy; import io.micronaut.inject.ast.ClassElement; import io.micronaut.inject.ast.ConstructorElement; import io.micronaut.inject.ast.Element; -import io.micronaut.inject.ast.ElementAnnotationMetadata; -import io.micronaut.inject.ast.ElementAnnotationMetadataFactory; import io.micronaut.inject.ast.EnumConstantElement; import io.micronaut.inject.ast.FieldElement; import io.micronaut.inject.ast.MethodElement; -import io.micronaut.inject.ast.ElementMutableAnnotationMetadata; import io.micronaut.inject.ast.PackageElement; import io.micronaut.inject.ast.ParameterElement; import io.micronaut.inject.ast.PropertyElement; @@ -318,7 +317,7 @@ private AnnotationMetadata replaceAnnotationsInternal(AnnotationMetadata annotat if (annotationMetadata instanceof AbstractAnnotationMetadataBuilder.CachedAnnotationMetadata) { throw new IllegalStateException(); } - if (annotationMetadata instanceof ElementMutableAnnotationMetadata) { + if (annotationMetadata instanceof MutableAnnotationMetadataDelegate) { throw new IllegalStateException(); } if (annotationMetadata.isEmpty()) { diff --git a/core-processor/src/main/java/io/micronaut/inject/ast/ElementAnnotationMetadata.java b/core-processor/src/main/java/io/micronaut/inject/ast/annotation/ElementAnnotationMetadata.java similarity index 83% rename from core-processor/src/main/java/io/micronaut/inject/ast/ElementAnnotationMetadata.java rename to core-processor/src/main/java/io/micronaut/inject/ast/annotation/ElementAnnotationMetadata.java index 0a199a3b7d7..d4a11a934f3 100644 --- a/core-processor/src/main/java/io/micronaut/inject/ast/ElementAnnotationMetadata.java +++ b/core-processor/src/main/java/io/micronaut/inject/ast/annotation/ElementAnnotationMetadata.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.micronaut.inject.ast; +package io.micronaut.inject.ast.annotation; import io.micronaut.core.annotation.AnnotationMetadata; @@ -23,5 +23,5 @@ * @author Denis Stepanov * @since 4.0.0 */ -public interface ElementAnnotationMetadata extends ElementMutableAnnotationMetadata { +public interface ElementAnnotationMetadata extends MutableAnnotationMetadataDelegate { } diff --git a/core-processor/src/main/java/io/micronaut/inject/ast/ElementAnnotationMetadataFactory.java b/core-processor/src/main/java/io/micronaut/inject/ast/annotation/ElementAnnotationMetadataFactory.java similarity index 97% rename from core-processor/src/main/java/io/micronaut/inject/ast/ElementAnnotationMetadataFactory.java rename to core-processor/src/main/java/io/micronaut/inject/ast/annotation/ElementAnnotationMetadataFactory.java index b966ea4b941..629b224bc78 100644 --- a/core-processor/src/main/java/io/micronaut/inject/ast/ElementAnnotationMetadataFactory.java +++ b/core-processor/src/main/java/io/micronaut/inject/ast/annotation/ElementAnnotationMetadataFactory.java @@ -13,11 +13,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.micronaut.inject.ast; +package io.micronaut.inject.ast.annotation; import io.micronaut.core.annotation.AnnotationMetadata; import io.micronaut.core.annotation.Experimental; import io.micronaut.core.annotation.NonNull; +import io.micronaut.inject.ast.Element; import java.util.function.Function; diff --git a/core-processor/src/main/java/io/micronaut/inject/ast/ElementMutableAnnotationMetadataDelegate.java b/core-processor/src/main/java/io/micronaut/inject/ast/annotation/ElementMutableAnnotationMetadataDelegate.java similarity index 68% rename from core-processor/src/main/java/io/micronaut/inject/ast/ElementMutableAnnotationMetadataDelegate.java rename to core-processor/src/main/java/io/micronaut/inject/ast/annotation/ElementMutableAnnotationMetadataDelegate.java index 1ad9e0b65ba..723666e498c 100644 --- a/core-processor/src/main/java/io/micronaut/inject/ast/ElementMutableAnnotationMetadataDelegate.java +++ b/core-processor/src/main/java/io/micronaut/inject/ast/annotation/ElementMutableAnnotationMetadataDelegate.java @@ -13,10 +13,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.micronaut.inject.ast; +package io.micronaut.inject.ast.annotation; import io.micronaut.core.annotation.AnnotationValue; import io.micronaut.core.annotation.AnnotationValueBuilder; +import io.micronaut.core.annotation.Internal; import io.micronaut.core.annotation.NonNull; import java.lang.annotation.Annotation; @@ -26,89 +27,90 @@ /** * Mutable annotation metadata provider. * - * @param The return type + * @param The return type * @author Denis Stepanov * @since 4.0.0 */ -public interface ElementMutableAnnotationMetadataDelegate extends ElementMutableAnnotationMetadata { +@Internal +public interface ElementMutableAnnotationMetadataDelegate extends MutableAnnotationMetadataDelegate { /** * Provides the return type instance. * * @return the return instance */ - Rtr getReturnInstance(); + R getReturnInstance(); @Override @NonNull - ElementMutableAnnotationMetadata getAnnotationMetadata(); + MutableAnnotationMetadataDelegate getAnnotationMetadata(); @Override @NonNull - default Rtr annotate(@NonNull String annotationType, @NonNull Consumer> consumer) { + default R annotate(@NonNull String annotationType, @NonNull Consumer> consumer) { getAnnotationMetadata().annotate(annotationType, consumer); return getReturnInstance(); } @Override @NonNull - default Rtr removeAnnotation(@NonNull String annotationType) { + default R removeAnnotation(@NonNull String annotationType) { getAnnotationMetadata().removeAnnotation(annotationType); return getReturnInstance(); } @Override @NonNull - default Rtr removeAnnotation(@NonNull Class annotationType) { + default R removeAnnotation(@NonNull Class annotationType) { getAnnotationMetadata().removeAnnotation(annotationType); return getReturnInstance(); } @Override @NonNull - default Rtr removeAnnotationIf(@NonNull Predicate> predicate) { + default R removeAnnotationIf(@NonNull Predicate> predicate) { getAnnotationMetadata().removeAnnotationIf(predicate); return getReturnInstance(); } @Override @NonNull - default Rtr removeStereotype(@NonNull String annotationType) { + default R removeStereotype(@NonNull String annotationType) { getAnnotationMetadata().removeStereotype(annotationType); return getReturnInstance(); } @Override @NonNull - default Rtr removeStereotype(@NonNull Class annotationType) { + default R removeStereotype(@NonNull Class annotationType) { getAnnotationMetadata().removeStereotype(annotationType); return getReturnInstance(); } @Override @NonNull - default Rtr annotate(@NonNull String annotationType) { + default R annotate(@NonNull String annotationType) { getAnnotationMetadata().annotate(annotationType); return getReturnInstance(); } @Override @NonNull - default Rtr annotate(@NonNull Class annotationType, @NonNull Consumer> consumer) { + default R annotate(@NonNull Class annotationType, @NonNull Consumer> consumer) { getAnnotationMetadata().annotate(annotationType, consumer); return getReturnInstance(); } @Override @NonNull - default Rtr annotate(@NonNull Class annotationType) { + default R annotate(@NonNull Class annotationType) { getAnnotationMetadata().annotate(annotationType); return getReturnInstance(); } @Override @NonNull - default Rtr annotate(@NonNull AnnotationValue annotationValue) { + default R annotate(@NonNull AnnotationValue annotationValue) { getAnnotationMetadata().annotate(annotationValue); return getReturnInstance(); } diff --git a/core-processor/src/main/java/io/micronaut/inject/ast/ElementMutableAnnotationMetadata.java b/core-processor/src/main/java/io/micronaut/inject/ast/annotation/MutableAnnotationMetadataDelegate.java similarity index 85% rename from core-processor/src/main/java/io/micronaut/inject/ast/ElementMutableAnnotationMetadata.java rename to core-processor/src/main/java/io/micronaut/inject/ast/annotation/MutableAnnotationMetadataDelegate.java index 674af46ad98..1603509c871 100644 --- a/core-processor/src/main/java/io/micronaut/inject/ast/ElementMutableAnnotationMetadata.java +++ b/core-processor/src/main/java/io/micronaut/inject/ast/annotation/MutableAnnotationMetadataDelegate.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.micronaut.inject.ast; +package io.micronaut.inject.ast.annotation; import io.micronaut.core.annotation.AnnotationMetadataDelegate; import io.micronaut.core.annotation.AnnotationValue; @@ -29,11 +29,11 @@ /** * Mutable annotation metadata. * - * @param The return type + * @param The return type * @author Denis Stepanov * @since 4.0.0 */ -public interface ElementMutableAnnotationMetadata extends AnnotationMetadataDelegate { +public interface MutableAnnotationMetadataDelegate extends AnnotationMetadataDelegate { /** * Annotate this element with the given annotation type. If the annotation is already present then @@ -45,7 +45,7 @@ public interface ElementMutableAnnotationMetadata extends AnnotationMetadat * @return This element */ @NonNull - default Rtr annotate(@NonNull String annotationType, @NonNull Consumer> consumer) { + default R annotate(@NonNull String annotationType, @NonNull Consumer> consumer) { throw new UnsupportedOperationException("Element of type [" + getClass() + "] does not support adding annotations at compilation time"); } @@ -62,7 +62,7 @@ default Rtr annotate(@NonNull String annotationType, @Non * @return This element */ @NonNull - default Rtr removeAnnotation(@NonNull String annotationType) { + default R removeAnnotation(@NonNull String annotationType) { throw new UnsupportedOperationException("Element of type [" + getClass() + "] does not support removing annotations at compilation time"); } @@ -73,7 +73,7 @@ default Rtr removeAnnotation(@NonNull String annotationType) { * @return This element */ @NonNull - default Rtr removeAnnotation(@NonNull Class annotationType) { + default R removeAnnotation(@NonNull Class annotationType) { return removeAnnotation(Objects.requireNonNull(annotationType).getName()); } @@ -84,7 +84,7 @@ default Rtr removeAnnotation(@NonNull Class annotation * @return This element */ @NonNull - default Rtr removeAnnotationIf(@NonNull Predicate> predicate) { + default R removeAnnotationIf(@NonNull Predicate> predicate) { throw new UnsupportedOperationException("Element of type [" + getClass() + "] does not support removing annotations at compilation time"); } @@ -94,7 +94,7 @@ default Rtr removeAnnotationIf(@NonNull Predicate Rtr removeStereotype(@NonNull Class annotationType) { + default R removeStereotype(@NonNull Class annotationType) { return removeStereotype(Objects.requireNonNull(annotationType).getName()); } @@ -117,7 +117,7 @@ default Rtr removeStereotype(@NonNull Class annotation * @return This element */ @NonNull - default Rtr annotate(@NonNull String annotationType) { + default R annotate(@NonNull String annotationType) { return annotate(annotationType, annotationValueBuilder -> { }); } @@ -131,7 +131,7 @@ default Rtr annotate(@NonNull String annotationType) { * @return This element */ @NonNull - default Rtr annotate(@NonNull Class annotationType, @NonNull Consumer> consumer) { + default R annotate(@NonNull Class annotationType, @NonNull Consumer> consumer) { ArgumentUtils.requireNonNull("annotationType", annotationType); ArgumentUtils.requireNonNull("consumer", consumer); return annotate(annotationType.getName(), consumer); @@ -146,7 +146,7 @@ default Rtr annotate(@NonNull Class annotationType, @N * @return This element */ @NonNull - default Rtr annotate(@NonNull Class annotationType) { + default R annotate(@NonNull Class annotationType) { ArgumentUtils.requireNonNull("annotationType", annotationType); return annotate(annotationType.getName(), annotationValueBuilder -> { }); } @@ -161,7 +161,7 @@ default Rtr annotate(@NonNull Class annotationType) { * @since 3.0.0 */ @NonNull - default Rtr annotate(@NonNull AnnotationValue annotationValue) { + default R annotate(@NonNull AnnotationValue annotationValue) { throw new UnsupportedOperationException("Element of type [" + getClass() + "] does not support adding annotations at compilation time"); } diff --git a/core-processor/src/main/java/io/micronaut/inject/ast/annotation/package-info.java b/core-processor/src/main/java/io/micronaut/inject/ast/annotation/package-info.java new file mode 100644 index 00000000000..9379e9753b7 --- /dev/null +++ b/core-processor/src/main/java/io/micronaut/inject/ast/annotation/package-info.java @@ -0,0 +1,26 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * The AST package contains internal classes to support annotation metadata on {@link io.micronaut.inject.ast.Element} instances. + * + * @see io.micronaut.inject.visitor.TypeElementVisitor + * @author graemerocher + * @since 1.0 + */ +@Internal +package io.micronaut.inject.ast.annotation; + +import io.micronaut.core.annotation.Internal; diff --git a/core-processor/src/main/java/io/micronaut/inject/visitor/VisitorContext.java b/core-processor/src/main/java/io/micronaut/inject/visitor/VisitorContext.java index 02c9e82f131..dca9b75d233 100644 --- a/core-processor/src/main/java/io/micronaut/inject/visitor/VisitorContext.java +++ b/core-processor/src/main/java/io/micronaut/inject/visitor/VisitorContext.java @@ -23,7 +23,7 @@ import io.micronaut.inject.annotation.AbstractAnnotationMetadataBuilder; import io.micronaut.inject.ast.ClassElement; import io.micronaut.inject.ast.Element; -import io.micronaut.inject.ast.ElementAnnotationMetadataFactory; +import io.micronaut.inject.ast.annotation.ElementAnnotationMetadataFactory; import io.micronaut.inject.ast.ElementFactory; import io.micronaut.inject.writer.ClassWriterOutputVisitor; import io.micronaut.inject.writer.GeneratedFile; diff --git a/core-processor/src/main/java/io/micronaut/inject/writer/AbstractBeanDefinitionBuilder.java b/core-processor/src/main/java/io/micronaut/inject/writer/AbstractBeanDefinitionBuilder.java index bdce24df08d..9afaaf12a37 100644 --- a/core-processor/src/main/java/io/micronaut/inject/writer/AbstractBeanDefinitionBuilder.java +++ b/core-processor/src/main/java/io/micronaut/inject/writer/AbstractBeanDefinitionBuilder.java @@ -34,7 +34,7 @@ import io.micronaut.inject.ast.ClassElement; import io.micronaut.inject.ast.ConstructorElement; import io.micronaut.inject.ast.Element; -import io.micronaut.inject.ast.ElementAnnotationMetadataFactory; +import io.micronaut.inject.ast.annotation.ElementAnnotationMetadataFactory; import io.micronaut.inject.ast.ElementModifier; import io.micronaut.inject.ast.ElementQuery; import io.micronaut.inject.ast.FieldElement; diff --git a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/annotation/GroovyElementAnnotationMetadataFactory.java b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/annotation/GroovyElementAnnotationMetadataFactory.java index 067a455eb84..42eded1e218 100644 --- a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/annotation/GroovyElementAnnotationMetadataFactory.java +++ b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/annotation/GroovyElementAnnotationMetadataFactory.java @@ -15,8 +15,8 @@ */ package io.micronaut.ast.groovy.annotation; -import io.micronaut.inject.annotation.AbstractElementAnnotationMetadataFactory; -import io.micronaut.inject.ast.ElementAnnotationMetadataFactory; +import io.micronaut.inject.ast.annotation.AbstractElementAnnotationMetadataFactory; +import io.micronaut.inject.ast.annotation.ElementAnnotationMetadataFactory; import org.codehaus.groovy.ast.AnnotatedNode; import org.codehaus.groovy.ast.AnnotationNode; diff --git a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/AbstractGroovyElement.java b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/AbstractGroovyElement.java index 1efb17053e2..1a15f9f7f34 100644 --- a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/AbstractGroovyElement.java +++ b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/AbstractGroovyElement.java @@ -17,6 +17,8 @@ import groovy.transform.PackageScope; import io.micronaut.core.annotation.AnnotationMetadata; +import io.micronaut.core.annotation.AnnotationValue; +import io.micronaut.core.annotation.AnnotationValueBuilder; import io.micronaut.core.annotation.NonNull; import io.micronaut.core.annotation.Nullable; import io.micronaut.core.util.ArrayUtils; @@ -24,11 +26,11 @@ import io.micronaut.core.util.StringUtils; import io.micronaut.inject.ast.ClassElement; import io.micronaut.inject.ast.Element; -import io.micronaut.inject.ast.ElementAnnotationMetadata; -import io.micronaut.inject.ast.ElementAnnotationMetadataFactory; +import io.micronaut.inject.ast.annotation.ElementAnnotationMetadata; +import io.micronaut.inject.ast.annotation.ElementAnnotationMetadataFactory; import io.micronaut.inject.ast.ElementModifier; -import io.micronaut.inject.ast.ElementMutableAnnotationMetadata; -import io.micronaut.inject.ast.ElementMutableAnnotationMetadataDelegate; +import io.micronaut.inject.ast.annotation.MutableAnnotationMetadataDelegate; +import io.micronaut.inject.ast.annotation.ElementMutableAnnotationMetadataDelegate; import org.codehaus.groovy.ast.AnnotatedNode; import org.codehaus.groovy.ast.ClassNode; import org.codehaus.groovy.ast.FieldNode; @@ -37,6 +39,7 @@ import org.codehaus.groovy.control.CompilationUnit; import org.codehaus.groovy.control.SourceUnit; +import java.lang.annotation.Annotation; import java.lang.reflect.Modifier; import java.util.ArrayList; import java.util.Collections; @@ -47,6 +50,8 @@ import java.util.Objects; import java.util.Optional; import java.util.Set; +import java.util.function.Consumer; +import java.util.function.Predicate; import java.util.regex.Pattern; /** @@ -85,13 +90,63 @@ protected AbstractGroovyElement(GroovyVisitorContext visitorContext, this.sourceUnit = visitorContext.getSourceUnit(); } + @Override + public Element annotate(String annotationType, Consumer> consumer) { + return ElementMutableAnnotationMetadataDelegate.super.annotate(annotationType, consumer); + } + + @Override + public Element removeAnnotation(String annotationType) { + return ElementMutableAnnotationMetadataDelegate.super.removeAnnotation(annotationType); + } + + @Override + public Element removeAnnotation(Class annotationType) { + return ElementMutableAnnotationMetadataDelegate.super.removeAnnotation(annotationType); + } + + @Override + public Element removeAnnotationIf(Predicate> predicate) { + return ElementMutableAnnotationMetadataDelegate.super.removeAnnotationIf(predicate); + } + + @Override + public Element removeStereotype(String annotationType) { + return ElementMutableAnnotationMetadataDelegate.super.removeStereotype(annotationType); + } + + @Override + public Element removeStereotype(Class annotationType) { + return ElementMutableAnnotationMetadataDelegate.super.removeStereotype(annotationType); + } + + @Override + public Element annotate(String annotationType) { + return ElementMutableAnnotationMetadataDelegate.super.annotate(annotationType); + } + + @Override + public Element annotate(Class annotationType, Consumer> consumer) { + return ElementMutableAnnotationMetadataDelegate.super.annotate(annotationType, consumer); + } + + @Override + public Element annotate(Class annotationType) { + return ElementMutableAnnotationMetadataDelegate.super.annotate(annotationType); + } + + @Override + public Element annotate(AnnotationValue annotationValue) { + return ElementMutableAnnotationMetadataDelegate.super.annotate(annotationValue); + } + @Override public Element getReturnInstance() { return this; } @Override - public ElementMutableAnnotationMetadata getAnnotationMetadata() { + public MutableAnnotationMetadataDelegate getAnnotationMetadata() { if (elementAnnotationMetadata == null) { if (presetAnnotationMetadata == null) { elementAnnotationMetadata = elementAnnotationMetadataFactory.build(this); diff --git a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyAnnotationElement.java b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyAnnotationElement.java index e97e7ceb14d..98604a25d15 100644 --- a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyAnnotationElement.java +++ b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyAnnotationElement.java @@ -16,7 +16,7 @@ package io.micronaut.ast.groovy.visitor; import io.micronaut.inject.ast.AnnotationElement; -import io.micronaut.inject.ast.ElementAnnotationMetadataFactory; +import io.micronaut.inject.ast.annotation.ElementAnnotationMetadataFactory; import org.codehaus.groovy.ast.ClassNode; /** diff --git a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyBeanDefinitionBuilder.java b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyBeanDefinitionBuilder.java index 97ee15ed790..110e6ee139c 100644 --- a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyBeanDefinitionBuilder.java +++ b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyBeanDefinitionBuilder.java @@ -28,7 +28,7 @@ import io.micronaut.inject.annotation.AnnotationMetadataHierarchy; import io.micronaut.inject.ast.ClassElement; import io.micronaut.inject.ast.Element; -import io.micronaut.inject.ast.ElementAnnotationMetadataFactory; +import io.micronaut.inject.ast.annotation.ElementAnnotationMetadataFactory; import io.micronaut.inject.ast.FieldElement; import io.micronaut.inject.ast.MethodElement; import io.micronaut.inject.ast.TypedElement; diff --git a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyClassElement.java b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyClassElement.java index ee3bacd2a9c..953f6858e46 100644 --- a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyClassElement.java +++ b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyClassElement.java @@ -36,7 +36,7 @@ import io.micronaut.inject.ast.ClassElement; import io.micronaut.inject.ast.ConstructorElement; import io.micronaut.inject.ast.Element; -import io.micronaut.inject.ast.ElementAnnotationMetadataFactory; +import io.micronaut.inject.ast.annotation.ElementAnnotationMetadataFactory; import io.micronaut.inject.ast.ElementModifier; import io.micronaut.inject.ast.ElementQuery; import io.micronaut.inject.ast.FieldElement; diff --git a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyConstructorElement.java b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyConstructorElement.java index 161421f85a7..d47f5a8e80b 100644 --- a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyConstructorElement.java +++ b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyConstructorElement.java @@ -17,7 +17,7 @@ import io.micronaut.core.annotation.AnnotationMetadata; import io.micronaut.inject.ast.ConstructorElement; -import io.micronaut.inject.ast.ElementAnnotationMetadataFactory; +import io.micronaut.inject.ast.annotation.ElementAnnotationMetadataFactory; import org.codehaus.groovy.ast.ConstructorNode; /** diff --git a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyElementFactory.java b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyElementFactory.java index 28b50a8f14b..db31cc0ecb1 100644 --- a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyElementFactory.java +++ b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyElementFactory.java @@ -18,7 +18,7 @@ import io.micronaut.core.annotation.NonNull; import io.micronaut.inject.ast.ClassElement; import io.micronaut.inject.ast.ConstructorElement; -import io.micronaut.inject.ast.ElementAnnotationMetadataFactory; +import io.micronaut.inject.ast.annotation.ElementAnnotationMetadataFactory; import io.micronaut.inject.ast.ElementFactory; import io.micronaut.inject.ast.EnumConstantElement; import io.micronaut.inject.ast.PrimitiveElement; diff --git a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyEnumConstantElement.java b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyEnumConstantElement.java index 0d47d24e4df..3efa0eee5b2 100644 --- a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyEnumConstantElement.java +++ b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyEnumConstantElement.java @@ -19,7 +19,7 @@ import io.micronaut.core.annotation.Internal; import io.micronaut.core.annotation.NonNull; import io.micronaut.inject.ast.ClassElement; -import io.micronaut.inject.ast.ElementAnnotationMetadataFactory; +import io.micronaut.inject.ast.annotation.ElementAnnotationMetadataFactory; import io.micronaut.inject.ast.ElementModifier; import io.micronaut.inject.ast.EnumConstantElement; import io.micronaut.inject.ast.FieldElement; diff --git a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyEnumElement.java b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyEnumElement.java index 89856b9eda6..e9e37447811 100644 --- a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyEnumElement.java +++ b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyEnumElement.java @@ -16,7 +16,7 @@ package io.micronaut.ast.groovy.visitor; import io.micronaut.inject.ast.ClassElement; -import io.micronaut.inject.ast.ElementAnnotationMetadataFactory; +import io.micronaut.inject.ast.annotation.ElementAnnotationMetadataFactory; import io.micronaut.inject.ast.EnumConstantElement; import io.micronaut.inject.ast.EnumElement; import org.codehaus.groovy.ast.ClassNode; diff --git a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyFieldElement.java b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyFieldElement.java index d46b6a0f5bc..126ec231817 100644 --- a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyFieldElement.java +++ b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyFieldElement.java @@ -19,7 +19,7 @@ import io.micronaut.core.annotation.NonNull; import io.micronaut.core.reflect.ClassUtils; import io.micronaut.inject.ast.ClassElement; -import io.micronaut.inject.ast.ElementAnnotationMetadataFactory; +import io.micronaut.inject.ast.annotation.ElementAnnotationMetadataFactory; import io.micronaut.inject.ast.ElementModifier; import io.micronaut.inject.ast.FieldElement; import org.codehaus.groovy.ast.ClassHelper; diff --git a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyGenericPlaceholderElement.java b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyGenericPlaceholderElement.java index 1c139edcf01..4330aff4cae 100644 --- a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyGenericPlaceholderElement.java +++ b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyGenericPlaceholderElement.java @@ -19,7 +19,7 @@ import io.micronaut.core.annotation.NonNull; import io.micronaut.inject.ast.ClassElement; import io.micronaut.inject.ast.Element; -import io.micronaut.inject.ast.ElementAnnotationMetadataFactory; +import io.micronaut.inject.ast.annotation.ElementAnnotationMetadataFactory; import io.micronaut.inject.ast.GenericPlaceholderElement; import org.codehaus.groovy.ast.ClassNode; diff --git a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyMethodElement.java b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyMethodElement.java index c6fe7978a08..dfcc9acb390 100644 --- a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyMethodElement.java +++ b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyMethodElement.java @@ -21,7 +21,7 @@ import io.micronaut.core.util.ArrayUtils; import io.micronaut.core.util.CollectionUtils; import io.micronaut.inject.ast.ClassElement; -import io.micronaut.inject.ast.ElementAnnotationMetadataFactory; +import io.micronaut.inject.ast.annotation.ElementAnnotationMetadataFactory; import io.micronaut.inject.ast.ElementModifier; import io.micronaut.inject.ast.GenericPlaceholderElement; import io.micronaut.inject.ast.MethodElement; diff --git a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyPackageElement.java b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyPackageElement.java index bd9d9880b3e..955109ed5c8 100644 --- a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyPackageElement.java +++ b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyPackageElement.java @@ -17,7 +17,7 @@ import io.micronaut.core.annotation.Internal; import io.micronaut.core.annotation.NonNull; -import io.micronaut.inject.ast.ElementAnnotationMetadataFactory; +import io.micronaut.inject.ast.annotation.ElementAnnotationMetadataFactory; import io.micronaut.inject.ast.PackageElement; import org.codehaus.groovy.ast.PackageNode; diff --git a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyParameterElement.java b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyParameterElement.java index 90f14e9e1e8..2cf62497115 100644 --- a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyParameterElement.java +++ b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyParameterElement.java @@ -20,7 +20,7 @@ import io.micronaut.core.annotation.NonNull; import io.micronaut.core.annotation.Nullable; import io.micronaut.inject.ast.ClassElement; -import io.micronaut.inject.ast.ElementAnnotationMetadataFactory; +import io.micronaut.inject.ast.annotation.ElementAnnotationMetadataFactory; import io.micronaut.inject.ast.ParameterElement; import org.codehaus.groovy.ast.Parameter; diff --git a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyPropertyElement.java b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyPropertyElement.java index e6ca6095f20..12a2fd5b23f 100644 --- a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyPropertyElement.java +++ b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyPropertyElement.java @@ -24,11 +24,11 @@ import io.micronaut.inject.annotation.AnnotationMetadataHierarchy; import io.micronaut.inject.ast.ClassElement; import io.micronaut.inject.ast.Element; -import io.micronaut.inject.ast.ElementAnnotationMetadataFactory; +import io.micronaut.inject.ast.annotation.ElementAnnotationMetadataFactory; import io.micronaut.inject.ast.FieldElement; import io.micronaut.inject.ast.MemberElement; import io.micronaut.inject.ast.MethodElement; -import io.micronaut.inject.ast.ElementMutableAnnotationMetadata; +import io.micronaut.inject.ast.annotation.MutableAnnotationMetadataDelegate; import io.micronaut.inject.ast.PropertyElement; import org.codehaus.groovy.ast.AnnotatedNode; @@ -60,7 +60,7 @@ final class GroovyPropertyElement extends AbstractGroovyElement implements Prope @Nullable private final FieldElement field; private final boolean excluded; - private final ElementMutableAnnotationMetadata annotationMetadata; + private final MutableAnnotationMetadataDelegate annotationMetadata; GroovyPropertyElement(GroovyVisitorContext visitorContext, ClassElement owningElement, @@ -115,7 +115,7 @@ public AnnotationMetadata getAnnotationMetadata() { }).toArray(AnnotationMetadata[]::new) ); } - annotationMetadata = new ElementMutableAnnotationMetadata() { + annotationMetadata = new MutableAnnotationMetadataDelegate() { @Override public io.micronaut.inject.ast.Element annotate(AnnotationValue annotationValue) { @@ -213,7 +213,7 @@ public boolean isExcluded() { } @Override - public ElementMutableAnnotationMetadata getAnnotationMetadata() { + public MutableAnnotationMetadataDelegate getAnnotationMetadata() { return annotationMetadata; } diff --git a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyVisitorContext.java b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyVisitorContext.java index a09393117c2..0f7a6e3c73c 100644 --- a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyVisitorContext.java +++ b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyVisitorContext.java @@ -32,7 +32,7 @@ import io.micronaut.inject.annotation.AbstractAnnotationMetadataBuilder; import io.micronaut.inject.ast.ClassElement; import io.micronaut.inject.ast.Element; -import io.micronaut.inject.ast.ElementAnnotationMetadataFactory; +import io.micronaut.inject.ast.annotation.ElementAnnotationMetadataFactory; import io.micronaut.inject.visitor.util.VisitorContextUtils; import io.micronaut.inject.visitor.VisitorContext; import io.micronaut.inject.writer.AbstractBeanDefinitionBuilder; diff --git a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyWildcardElement.java b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyWildcardElement.java index 8aadf358330..ea4ab5a2473 100644 --- a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyWildcardElement.java +++ b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyWildcardElement.java @@ -19,7 +19,7 @@ import io.micronaut.core.annotation.NonNull; import io.micronaut.inject.ast.ArrayableClassElement; import io.micronaut.inject.ast.ClassElement; -import io.micronaut.inject.ast.ElementAnnotationMetadataFactory; +import io.micronaut.inject.ast.annotation.ElementAnnotationMetadataFactory; import io.micronaut.inject.ast.WildcardElement; import java.util.List; diff --git a/inject-java/src/main/java/io/micronaut/annotation/processing/BeanDefinitionInjectProcessor.java b/inject-java/src/main/java/io/micronaut/annotation/processing/BeanDefinitionInjectProcessor.java index d308dddba16..e5f2f004c1a 100644 --- a/inject-java/src/main/java/io/micronaut/annotation/processing/BeanDefinitionInjectProcessor.java +++ b/inject-java/src/main/java/io/micronaut/annotation/processing/BeanDefinitionInjectProcessor.java @@ -27,7 +27,7 @@ import io.micronaut.core.util.CollectionUtils; import io.micronaut.inject.processing.ProcessingException; import io.micronaut.inject.annotation.AbstractAnnotationMetadataBuilder; -import io.micronaut.inject.ast.ElementAnnotationMetadataFactory; +import io.micronaut.inject.ast.annotation.ElementAnnotationMetadataFactory; import io.micronaut.inject.processing.BeanDefinitionCreator; import io.micronaut.inject.processing.BeanDefinitionCreatorFactory; import io.micronaut.inject.processing.JavaModelUtils; diff --git a/inject-java/src/main/java/io/micronaut/annotation/processing/JavaElementAnnotationMetadataFactory.java b/inject-java/src/main/java/io/micronaut/annotation/processing/JavaElementAnnotationMetadataFactory.java index 4c5957bd3f1..10c8510cf4a 100644 --- a/inject-java/src/main/java/io/micronaut/annotation/processing/JavaElementAnnotationMetadataFactory.java +++ b/inject-java/src/main/java/io/micronaut/annotation/processing/JavaElementAnnotationMetadataFactory.java @@ -15,8 +15,8 @@ */ package io.micronaut.annotation.processing; -import io.micronaut.inject.annotation.AbstractElementAnnotationMetadataFactory; -import io.micronaut.inject.ast.ElementAnnotationMetadataFactory; +import io.micronaut.inject.ast.annotation.AbstractElementAnnotationMetadataFactory; +import io.micronaut.inject.ast.annotation.ElementAnnotationMetadataFactory; import javax.lang.model.element.AnnotationMirror; import javax.lang.model.element.Element; diff --git a/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/AbstractJavaElement.java b/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/AbstractJavaElement.java index acdd38abfa2..6a78fd0fcdc 100644 --- a/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/AbstractJavaElement.java +++ b/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/AbstractJavaElement.java @@ -17,14 +17,16 @@ import io.micronaut.annotation.processing.AnnotationUtils; import io.micronaut.core.annotation.AnnotationMetadata; +import io.micronaut.core.annotation.AnnotationValue; +import io.micronaut.core.annotation.AnnotationValueBuilder; import io.micronaut.core.annotation.NonNull; import io.micronaut.core.annotation.Nullable; import io.micronaut.inject.ast.ClassElement; -import io.micronaut.inject.ast.ElementAnnotationMetadata; -import io.micronaut.inject.ast.ElementAnnotationMetadataFactory; +import io.micronaut.inject.ast.annotation.ElementAnnotationMetadata; +import io.micronaut.inject.ast.annotation.ElementAnnotationMetadataFactory; import io.micronaut.inject.ast.ElementModifier; -import io.micronaut.inject.ast.ElementMutableAnnotationMetadata; -import io.micronaut.inject.ast.ElementMutableAnnotationMetadataDelegate; +import io.micronaut.inject.ast.annotation.MutableAnnotationMetadataDelegate; +import io.micronaut.inject.ast.annotation.ElementMutableAnnotationMetadataDelegate; import io.micronaut.inject.ast.PrimitiveElement; import io.micronaut.inject.ast.TypedElement; @@ -43,12 +45,15 @@ import javax.lang.model.type.TypeVariable; import javax.lang.model.type.UnionType; import javax.lang.model.type.WildcardType; +import java.lang.annotation.Annotation; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.Set; +import java.util.function.Consumer; +import java.util.function.Predicate; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -90,7 +95,7 @@ public io.micronaut.inject.ast.Element getReturnInstance() { } @Override - public ElementMutableAnnotationMetadata getAnnotationMetadata() { + public MutableAnnotationMetadataDelegate getAnnotationMetadata() { if (elementAnnotationMetadata == null) { if (presetAnnotationMetadata == null) { elementAnnotationMetadata = elementAnnotationMetadataFactory.build(this); @@ -126,6 +131,56 @@ public io.micronaut.inject.ast.Element withAnnotationMetadata(AnnotationMetadata return abstractJavaElement; } + @Override + public io.micronaut.inject.ast.Element annotate(String annotationType, Consumer> consumer) { + return ElementMutableAnnotationMetadataDelegate.super.annotate(annotationType, consumer); + } + + @Override + public io.micronaut.inject.ast.Element removeAnnotation(String annotationType) { + return ElementMutableAnnotationMetadataDelegate.super.removeAnnotation(annotationType); + } + + @Override + public io.micronaut.inject.ast.Element removeAnnotation(Class annotationType) { + return ElementMutableAnnotationMetadataDelegate.super.removeAnnotation(annotationType); + } + + @Override + public io.micronaut.inject.ast.Element removeAnnotationIf(Predicate> predicate) { + return ElementMutableAnnotationMetadataDelegate.super.removeAnnotationIf(predicate); + } + + @Override + public io.micronaut.inject.ast.Element removeStereotype(String annotationType) { + return ElementMutableAnnotationMetadataDelegate.super.removeStereotype(annotationType); + } + + @Override + public io.micronaut.inject.ast.Element removeStereotype(Class annotationType) { + return ElementMutableAnnotationMetadataDelegate.super.removeStereotype(annotationType); + } + + @Override + public io.micronaut.inject.ast.Element annotate(String annotationType) { + return ElementMutableAnnotationMetadataDelegate.super.annotate(annotationType); + } + + @Override + public io.micronaut.inject.ast.Element annotate(Class annotationType, Consumer> consumer) { + return ElementMutableAnnotationMetadataDelegate.super.annotate(annotationType, consumer); + } + + @Override + public io.micronaut.inject.ast.Element annotate(Class annotationType) { + return ElementMutableAnnotationMetadataDelegate.super.annotate(annotationType); + } + + @Override + public io.micronaut.inject.ast.Element annotate(AnnotationValue annotationValue) { + return ElementMutableAnnotationMetadataDelegate.super.annotate(annotationValue); + } + @Override public boolean isPackagePrivate() { Set modifiers = element.getModifiers(); diff --git a/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaAnnotationElement.java b/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaAnnotationElement.java index c4e883041d3..0b78fbd8979 100644 --- a/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaAnnotationElement.java +++ b/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaAnnotationElement.java @@ -16,7 +16,7 @@ package io.micronaut.annotation.processing.visitor; import io.micronaut.inject.ast.AnnotationElement; -import io.micronaut.inject.ast.ElementAnnotationMetadataFactory; +import io.micronaut.inject.ast.annotation.ElementAnnotationMetadataFactory; import javax.lang.model.element.TypeElement; diff --git a/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaBeanDefinitionBuilder.java b/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaBeanDefinitionBuilder.java index dbbcdc04a71..ca3864777db 100644 --- a/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaBeanDefinitionBuilder.java +++ b/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaBeanDefinitionBuilder.java @@ -27,7 +27,7 @@ import io.micronaut.inject.annotation.AnnotationMetadataHierarchy; import io.micronaut.inject.ast.ClassElement; import io.micronaut.inject.ast.Element; -import io.micronaut.inject.ast.ElementAnnotationMetadataFactory; +import io.micronaut.inject.ast.annotation.ElementAnnotationMetadataFactory; import io.micronaut.inject.ast.FieldElement; import io.micronaut.inject.ast.MethodElement; import io.micronaut.inject.ast.TypedElement; diff --git a/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaClassElement.java b/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaClassElement.java index 0fd21dbf9a7..d00e8db3977 100644 --- a/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaClassElement.java +++ b/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaClassElement.java @@ -28,7 +28,7 @@ import io.micronaut.inject.ast.BeanPropertiesQuery; import io.micronaut.inject.ast.ClassElement; import io.micronaut.inject.ast.ConstructorElement; -import io.micronaut.inject.ast.ElementAnnotationMetadataFactory; +import io.micronaut.inject.ast.annotation.ElementAnnotationMetadataFactory; import io.micronaut.inject.ast.ElementQuery; import io.micronaut.inject.ast.FieldElement; import io.micronaut.inject.ast.GenericPlaceholderElement; diff --git a/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaConstructorElement.java b/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaConstructorElement.java index 13515068fd1..aca4f2ba497 100644 --- a/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaConstructorElement.java +++ b/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaConstructorElement.java @@ -17,7 +17,7 @@ import io.micronaut.core.annotation.Internal; import io.micronaut.inject.ast.ConstructorElement; -import io.micronaut.inject.ast.ElementAnnotationMetadataFactory; +import io.micronaut.inject.ast.annotation.ElementAnnotationMetadataFactory; import io.micronaut.inject.ast.MemberElement; import io.micronaut.inject.ast.MethodElement; import io.micronaut.inject.ast.ParameterElement; diff --git a/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaElementFactory.java b/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaElementFactory.java index c7ad6bb9839..6ea85a5a828 100644 --- a/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaElementFactory.java +++ b/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaElementFactory.java @@ -21,7 +21,7 @@ import io.micronaut.core.annotation.NonNull; import io.micronaut.core.annotation.Nullable; import io.micronaut.inject.ast.ClassElement; -import io.micronaut.inject.ast.ElementAnnotationMetadataFactory; +import io.micronaut.inject.ast.annotation.ElementAnnotationMetadataFactory; import io.micronaut.inject.ast.ElementFactory; import io.micronaut.inject.ast.MethodElement; import io.micronaut.inject.ast.beans.BeanElementBuilder; diff --git a/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaEnumConstantElement.java b/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaEnumConstantElement.java index 025c3189f2a..89460e0736d 100644 --- a/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaEnumConstantElement.java +++ b/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaEnumConstantElement.java @@ -18,7 +18,7 @@ import io.micronaut.core.annotation.AnnotationMetadata; import io.micronaut.core.annotation.Internal; import io.micronaut.inject.ast.ClassElement; -import io.micronaut.inject.ast.ElementAnnotationMetadataFactory; +import io.micronaut.inject.ast.annotation.ElementAnnotationMetadataFactory; import io.micronaut.inject.ast.ElementModifier; import io.micronaut.inject.ast.EnumConstantElement; import io.micronaut.inject.ast.FieldElement; diff --git a/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaEnumElement.java b/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaEnumElement.java index 1de7dafc29f..8cf73cbcb80 100644 --- a/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaEnumElement.java +++ b/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaEnumElement.java @@ -17,7 +17,7 @@ import io.micronaut.core.annotation.Internal; import io.micronaut.inject.ast.ClassElement; -import io.micronaut.inject.ast.ElementAnnotationMetadataFactory; +import io.micronaut.inject.ast.annotation.ElementAnnotationMetadataFactory; import io.micronaut.inject.ast.EnumConstantElement; import io.micronaut.inject.ast.EnumElement; diff --git a/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaFieldElement.java b/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaFieldElement.java index abcba4abe24..9191219d823 100644 --- a/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaFieldElement.java +++ b/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaFieldElement.java @@ -19,7 +19,7 @@ import io.micronaut.core.annotation.Internal; import io.micronaut.core.annotation.NonNull; import io.micronaut.inject.ast.ClassElement; -import io.micronaut.inject.ast.ElementAnnotationMetadataFactory; +import io.micronaut.inject.ast.annotation.ElementAnnotationMetadataFactory; import io.micronaut.inject.ast.FieldElement; import io.micronaut.inject.ast.MemberElement; diff --git a/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaGenericPlaceholderElement.java b/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaGenericPlaceholderElement.java index 18a1898327d..474ff4f6f72 100644 --- a/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaGenericPlaceholderElement.java +++ b/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaGenericPlaceholderElement.java @@ -19,7 +19,7 @@ import io.micronaut.core.annotation.NonNull; import io.micronaut.inject.ast.ClassElement; import io.micronaut.inject.ast.Element; -import io.micronaut.inject.ast.ElementAnnotationMetadataFactory; +import io.micronaut.inject.ast.annotation.ElementAnnotationMetadataFactory; import io.micronaut.inject.ast.GenericPlaceholderElement; import javax.lang.model.element.TypeParameterElement; diff --git a/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaMethodElement.java b/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaMethodElement.java index 9bd98f513ef..fc16506a18c 100644 --- a/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaMethodElement.java +++ b/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaMethodElement.java @@ -21,7 +21,7 @@ import io.micronaut.core.util.ArrayUtils; import io.micronaut.core.util.CollectionUtils; import io.micronaut.inject.ast.ClassElement; -import io.micronaut.inject.ast.ElementAnnotationMetadataFactory; +import io.micronaut.inject.ast.annotation.ElementAnnotationMetadataFactory; import io.micronaut.inject.ast.GenericPlaceholderElement; import io.micronaut.inject.ast.MemberElement; import io.micronaut.inject.ast.MethodElement; diff --git a/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaPackageElement.java b/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaPackageElement.java index c1c37fbd6c9..92adfaf466a 100644 --- a/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaPackageElement.java +++ b/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaPackageElement.java @@ -16,7 +16,7 @@ package io.micronaut.annotation.processing.visitor; import io.micronaut.core.annotation.Internal; -import io.micronaut.inject.ast.ElementAnnotationMetadataFactory; +import io.micronaut.inject.ast.annotation.ElementAnnotationMetadataFactory; import javax.lang.model.element.PackageElement; diff --git a/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaParameterElement.java b/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaParameterElement.java index 84445f521fe..9a3f022619c 100644 --- a/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaParameterElement.java +++ b/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaParameterElement.java @@ -19,7 +19,7 @@ import io.micronaut.core.annotation.Internal; import io.micronaut.core.annotation.NonNull; import io.micronaut.inject.ast.ClassElement; -import io.micronaut.inject.ast.ElementAnnotationMetadataFactory; +import io.micronaut.inject.ast.annotation.ElementAnnotationMetadataFactory; import io.micronaut.inject.ast.MethodElement; import io.micronaut.inject.ast.ParameterElement; diff --git a/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaPropertyElement.java b/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaPropertyElement.java index e6b1f768e9f..09380fd1e16 100644 --- a/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaPropertyElement.java +++ b/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaPropertyElement.java @@ -23,11 +23,11 @@ import io.micronaut.core.annotation.Nullable; import io.micronaut.inject.annotation.AnnotationMetadataHierarchy; import io.micronaut.inject.ast.ClassElement; -import io.micronaut.inject.ast.ElementAnnotationMetadataFactory; +import io.micronaut.inject.ast.annotation.ElementAnnotationMetadataFactory; import io.micronaut.inject.ast.FieldElement; import io.micronaut.inject.ast.MemberElement; import io.micronaut.inject.ast.MethodElement; -import io.micronaut.inject.ast.ElementMutableAnnotationMetadata; +import io.micronaut.inject.ast.annotation.MutableAnnotationMetadataDelegate; import io.micronaut.inject.ast.PropertyElement; import javax.lang.model.element.Element; @@ -59,7 +59,7 @@ final class JavaPropertyElement extends AbstractJavaElement implements PropertyE @Nullable private final FieldElement field; private final boolean excluded; - private final ElementMutableAnnotationMetadata annotationMetadata; + private final MutableAnnotationMetadataDelegate annotationMetadata; JavaPropertyElement(ClassElement owningElement, ClassElement type, @@ -114,7 +114,7 @@ public AnnotationMetadata getAnnotationMetadata() { }).toArray(AnnotationMetadata[]::new) ); } - annotationMetadata = new ElementMutableAnnotationMetadata() { + annotationMetadata = new MutableAnnotationMetadataDelegate() { @Override public io.micronaut.inject.ast.Element annotate(AnnotationValue annotationValue) { @@ -210,7 +210,7 @@ public boolean isExcluded() { } @Override - public ElementMutableAnnotationMetadata getAnnotationMetadata() { + public MutableAnnotationMetadataDelegate getAnnotationMetadata() { return annotationMetadata; } diff --git a/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaVisitorContext.java b/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaVisitorContext.java index 3fec7a82fad..4f0b2a011ac 100644 --- a/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaVisitorContext.java +++ b/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaVisitorContext.java @@ -31,7 +31,7 @@ import io.micronaut.core.util.CollectionUtils; import io.micronaut.core.util.StringUtils; import io.micronaut.inject.ast.ClassElement; -import io.micronaut.inject.ast.ElementAnnotationMetadataFactory; +import io.micronaut.inject.ast.annotation.ElementAnnotationMetadataFactory; import io.micronaut.inject.ast.beans.BeanElement; import io.micronaut.inject.ast.beans.BeanElementBuilder; import io.micronaut.inject.configuration.ConfigurationMetadataBuilder; diff --git a/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaWildcardElement.java b/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaWildcardElement.java index e290f967531..375e2ada429 100644 --- a/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaWildcardElement.java +++ b/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaWildcardElement.java @@ -19,7 +19,7 @@ import io.micronaut.core.annotation.NonNull; import io.micronaut.inject.ast.ArrayableClassElement; import io.micronaut.inject.ast.ClassElement; -import io.micronaut.inject.ast.ElementAnnotationMetadataFactory; +import io.micronaut.inject.ast.annotation.ElementAnnotationMetadataFactory; import io.micronaut.inject.ast.WildcardElement; import javax.lang.model.type.WildcardType; From e1515f968e9bab5f93b2b9cbfac236a4b7f47ffa Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 29 Oct 2022 06:40:34 +0200 Subject: [PATCH 170/743] fix(deps): update dependency io.micronaut.testresources:micronaut-test-resources-bom to v1.1.3 (#8254) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 0cb995381dc..fc066f5b38c 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -122,7 +122,7 @@ managed-micronaut-servlet = "3.3.1" managed-micronaut-spring = "4.3.1" managed-micronaut-sql = "4.7.2" managed-micronaut-test = "3.7.0" -managed-micronaut-test-resources = "1.1.2" +managed-micronaut-test-resources = "1.1.3" managed-micronaut-toml = "1.1.1" managed-micronaut-tracing = "4.4.0" managed-micronaut-tracing-legacy = "3.2.7" From 2b3c620d813c8dbc2dc59174e331f930c597c6cd Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 29 Oct 2022 06:40:34 +0200 Subject: [PATCH 171/743] fix(deps): update dependency io.micronaut.testresources:micronaut-test-resources-bom to v1.1.3 (#8254) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> # Conflicts: # gradle/libs.versions.toml --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 83f021efc12..434106fb264 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -118,7 +118,7 @@ managed-micronaut-servlet = "3.3.2" managed-micronaut-spring = "4.3.1" managed-micronaut-sql = "4.7.2" managed-micronaut-test = "3.6.2" -managed-micronaut-test-resources = "1.1.2" +managed-micronaut-test-resources = "1.1.3" managed-micronaut-toml = "1.1.1" managed-micronaut-tracing = "4.4.0" managed-micronaut-tracing-legacy = "3.2.7" From b16b75354db8d7e567d00a077de11d6ce1f7d2a7 Mon Sep 17 00:00:00 2001 From: micronaut-build Date: Sat, 29 Oct 2022 05:19:44 +0000 Subject: [PATCH 172/743] [skip ci] Release v3.7.3 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 5e6329a4f34..abf3075037d 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -projectVersion=3.7.3-SNAPSHOT +projectVersion=3.7.3 projectGroupId=io.micronaut title=Micronaut projectDesc=Natively Cloud Native From ebb921e5f251de47b12bbbf1a60ebe13e011cbe9 Mon Sep 17 00:00:00 2001 From: micronaut-build Date: Sat, 29 Oct 2022 05:31:56 +0000 Subject: [PATCH 173/743] Back to 3.7.4-SNAPSHOT --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index abf3075037d..0287a584f27 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -projectVersion=3.7.3 +projectVersion=3.7.4-SNAPSHOT projectGroupId=io.micronaut title=Micronaut projectDesc=Natively Cloud Native From 79c56c65b40ca361904f2a723041379b15c7f26f Mon Sep 17 00:00:00 2001 From: Graeme Rocher Date: Mon, 31 Oct 2022 12:22:51 +0100 Subject: [PATCH 174/743] Split the runtime module into multiple modules (#8235) This PR splits the `micronaut-runtime` module into multiple modules. The new modules are: * `micronaut-retry` - The retry logic implementation is now optional. In order to support this by default declarative http clients are no longer automatically `@Recoverable` (Fixes #3719). This is a better arrangement as not everyone needs or uses fallbacks and recovery * `micronaut-discovery-core` - The service discovery implementation is split into a separate module that is no longer required. The HTTP client module still depends on it though and would be nice to remove this coupling. Following this change `micronaut-runtime` is just a meta module that depends on other modules. Other Micronaut modules should update and depend specifically on an individual module such not to pull in more than is necessary. The motivation for this is to reduce the footprint of a new app since many features are not used (heartbeat, health checks, service discovery, retry etc.) unless you actually need them. In addition the `micronaut-http-server-netty` module no longer depends on `micronaut-discovery`, `micronaut-health` and `micronaut-websocket`. If users want any of those features then they can add them individually to their application. Micronaut Launch will need to be updated with new feature for WebSocket support. It is probably not necessary to include new features for discovery and health as these will be transitively pulled by the Management module. Also fixes #8099 by introducing a new interface to resolve the `JsonMapper` Note this PR also moves some functionality into other modules: https://github.com/micronaut-projects/micronaut-kotlin/pull/440 https://github.com/micronaut-projects/micronaut-reactor/pull/237 This change also switches Micronaut's integration to GraalVM to be largely runtime initialized including logging. Only Micronaut metadata is build time initialized. This change improves startup time and reduces the size of the images we build. Co-authored-by: Sergio del Amo --- .../micronaut/buffer/netty/NettyFeature.java | 2 - .../native-image.properties | 1 + context/build.gradle | 4 + .../java/io/micronaut/logging/LogLevel.java | 0 .../io/micronaut/logging/LoggingSystem.java | 0 .../PropertiesLoggingLevelsConfigurer.java | 0 .../logging/impl/Log4jLoggingSystem.java | 0 .../logging/impl/LogbackLoggingSystem.java | 0 .../native-image.properties | 3 +- .../io/micronaut/core/beans/BeanInfo.java | 33 ++++ .../core/beans/BeanIntrospection.java | 2 +- .../core/graal/AutomaticFeatureUtils.java | 2 +- .../core/graal/GraalReflectionConfigurer.java | 16 +- .../micronaut/core/graal/LoggingFeature.java | 68 +++++++ .../graal/ServiceLoaderInitialization.java | 75 ++++++-- .../java/io/micronaut/core/io/IOUtils.java | 9 +- .../scan/DefaultClassPathResourceLoader.java | 22 ++- .../micronaut-core/native-image.properties | 10 +- .../micronaut-core/reflect-config.json | 7 - discovery-core/build.gradle | 13 ++ .../discovery/CompositeDiscoveryClient.java | 0 .../DefaultCompositeDiscoveryClient.java | 0 .../discovery/DefaultServiceInstance.java | 0 .../DefaultServiceInstanceIdGenerator.java | 0 .../micronaut/discovery/DiscoveryClient.java | 0 .../discovery/DiscoveryConfiguration.java | 0 .../discovery/EmbeddedServerInstance.java | 0 .../micronaut/discovery/ServiceInstance.java | 0 .../discovery/ServiceInstanceIdGenerator.java | 0 .../discovery/ServiceInstanceList.java | 0 .../discovery/StaticServiceInstanceList.java | 0 .../AbstractComputeInstanceMetadata.java | 0 .../cloud/ComputeInstanceMetadata.java | 0 .../ComputeInstanceMetadataResolver.java | 0 .../ComputeInstanceMetadataResolverUtils.java | 0 .../discovery/cloud/NetworkInterface.java | 0 .../DigitalOceanInstanceMetadata.java | 0 .../DigitalOceanMetadataConfiguration.java | 0 .../DigitalOceanMetadataKeys.java | 0 .../DigitalOceanMetadataResolver.java | 0 .../DigitalOceanNetworkInterface.java | 0 .../discovery/cloud/package-info.java | 0 .../config/ConfigDiscoveryConfiguration.java | 0 .../discovery/config/ConfigurationClient.java | 0 .../DefaultCompositeConfigurationClient.java | 0 .../discovery/config/package-info.java | 0 .../event/AbstractServiceInstanceEvent.java | 0 .../discovery/event/ServiceReadyEvent.java | 0 .../discovery/event/ServiceStoppedEvent.java | 0 .../discovery/event/package-info.java | 0 .../exceptions/DiscoveryException.java | 0 .../NoAvailableServiceException.java | 0 .../discovery/exceptions/package-info.java | 0 .../ServiceInstanceMetadataContributor.java | 0 .../discovery/metadata/package-info.java | 0 .../io/micronaut/discovery/package-info.java | 0 .../registration/AutoRegistration.java | 0 .../RegistrationConfiguration.java | 0 .../registration/RegistrationException.java | 0 .../discovery/registration/package-info.java | 0 .../micronaut/health/CurrentHealthStatus.java | 0 .../health/DefaultCurrentHealthStatus.java | 0 .../io/micronaut/health/HealthStatus.java | 0 .../health/HeartbeatConfiguration.java | 0 .../HeartbeatDiscoveryClientCondition.java | 0 .../io/micronaut/health/HeartbeatEnabled.java | 0 .../io/micronaut/health/HeartbeatEvent.java | 0 .../io/micronaut/health/HeartbeatTask.java | 0 .../io/micronaut/health/package-info.java | 0 .../function/client/FunctionClient.java | 2 - function-web/build.gradle | 2 +- .../web/AnnotatedFunctionRouteBuilder.java | 12 +- function/build.gradle | 2 +- http-client-core/build.gradle | 6 +- .../http/client/annotation/Client.java | 5 - .../HttpClientIntroductionAdvice.java | 6 +- http-client/build.gradle | 4 +- .../http/client/netty/DefaultHttpClient.java | 13 +- .../client/aop/BlockingFallbackSpec.groovy | 1 + .../aop/CompletableFutureFallbackSpec.groovy | 2 + .../client/aop/ReactorJavaFallbackSpec.groovy | 2 + .../HttpClientRetryWithFallbackSpec.groovy | 2 + http-netty/build.gradle | 3 +- .../http/netty/graal/HttpNettyFeature.java | 59 ------ http-server-netty/build.gradle | 7 +- .../DefaultNettyEmbeddedServerFactory.java | 25 ++- .../netty/DelegateNettyEmbeddedServices.java | 7 +- .../server/netty/HttpPipelineBuilder.java | 7 +- .../server/netty/NettyEmbeddedServices.java | 10 +- .../http/server/netty/NettyHttpServer.java | 18 +- .../server/netty/RoutingInBoundHandler.java | 92 ++++----- .../NettyEmbeddedServerInstance.java | 3 +- .../discovery/NettyServiceDiscovery.java | 70 +++++++ .../NettyServerWebSocketUpgradeHandler.java | 8 +- .../WebSocketUpgradeHandlerFactory.java | 46 +++++ http-server/build.gradle | 6 +- .../micronaut/http/server/RouteExecutor.java | 2 +- .../websocket/ServerWebSocketProcessor.java | 7 +- http/build.gradle | 3 +- .../execution/ReactiveExecutionFlow.java | 4 +- .../execution/ReactorExecutionFlowImpl.java | 2 +- .../codec/MediaTypeCodecRegistryFactory.java | 0 .../runtime/http/codec/TextPlainCodec.java | 0 .../runtime/http/codec/package-info.java | 0 .../runtime/http/scope/RequestAware.java | 0 .../http/scope/RequestCustomScope.java | 0 .../runtime/http/scope/RequestScope.java | 0 inject-groovy/build.gradle | 1 + inject-java-test/build.gradle | 1 + inject-java/build.gradle | 1 + .../AbstractBeanContextConditional.java | 18 +- .../context/AbstractBeanDefinition.java | 12 +- .../AbstractInitializableBeanDefinition.java | 7 +- ...tInitializableBeanDefinitionReference.java | 7 +- .../micronaut/context/DefaultBeanContext.java | 16 +- .../context/env/DefaultEnvironment.java | 4 +- .../ApplicationEventPublisherFactory.java | 25 ++- .../java/io/micronaut/inject/BeanType.java | 5 +- .../micronaut-inject/native-image.properties | 6 +- .../jackson/JacksonDatabindFeature.java | 2 - .../JacksonDatabindMapperSupplier.java | 23 ++- .../native-image.properties | 1 + .../io.micronaut.json.JsonMapperSupplier | 1 + .../java/io/micronaut/json/JsonMapper.java | 24 +++ .../io/micronaut/json/JsonMapperSupplier.java | 27 +++ management/build.gradle | 4 +- .../endpoint/health/HealthEndpoint.java | 6 +- messaging/build.gradle | 3 +- retry/build.gradle | 13 ++ .../java/io/micronaut/retry/CircuitState.java | 0 .../java/io/micronaut/retry/RetryState.java | 0 .../io/micronaut/retry/RetryStateBuilder.java | 0 .../retry/annotation/CircuitBreaker.java | 0 .../annotation/DefaultRetryPredicate.java | 0 .../micronaut/retry/annotation/Fallback.java | 0 .../retry/annotation/Recoverable.java | 0 .../retry/annotation/RetryPredicate.java | 0 .../micronaut/retry/annotation/Retryable.java | 0 .../retry/annotation/package-info.java | 0 .../retry/event/CircuitClosedEvent.java | 0 .../retry/event/CircuitOpenEvent.java | 0 .../io/micronaut/retry/event/RetryEvent.java | 0 .../retry/event/RetryEventListener.java | 0 .../micronaut/retry/event/package-info.java | 0 .../retry/exception/CircuitOpenException.java | 0 .../retry/exception/FallbackException.java | 0 .../retry/exception/RetryException.java | 0 .../retry/exception/package-info.java | 0 .../AnnotationRetryStateBuilder.java | 0 .../retry/intercept/CircuitBreakerRetry.java | 0 .../intercept/DefaultRetryInterceptor.java | 0 .../retry/intercept/MutableRetryState.java | 0 .../retry/intercept/MyCustomException.java | 0 .../retry/intercept/RecoveryInterceptor.java | 13 +- .../retry/intercept/SimpleRetry.java | 0 .../retry/intercept/package-info.java | 0 .../java/io/micronaut/retry/package-info.java | 0 .../intercept/CircuitBreakerRetrySpec.groovy | 0 .../retry/intercept/CircuitBreakerSpec.groovy | 0 .../intercept/InterceptorOrderSpec.groovy | 0 .../intercept/SimpleRetryInstanceSpec.groovy | 0 .../retry/intercept/SimpleRetrySpec.groovy | 0 runtime-osx/build.gradle | 2 +- runtime/build.gradle | 10 +- .../converters/FlowConverterRegistrar.java | 45 ----- .../instrument/ReactorInstrumentation.java | 142 -------------- .../reactor/instrument/ReactorSubscriber.java | 74 -------- session/build.gradle | 6 +- settings.gradle | 2 + src/main/docs/guide/appendix/breaks.adoc | 27 +++ test-suite-geb/build.gradle | 3 +- test-suite-groovy/build.gradle | 1 + test-suite-kotlin/build.gradle | 1 + .../kotlin/io/micronaut/MdcPropagationSpec.kt | 176 ------------------ .../reactor/ReactorContextPropagationSpec.kt | 4 +- test-suite/build.gradle | 1 + validation/build.gradle | 1 + 177 files changed, 646 insertions(+), 774 deletions(-) create mode 100644 buffer-netty/src/main/resources/META-INF/native-image/io.micronaut/micronaut-buffer-netty/native-image.properties rename {runtime => context}/src/main/java/io/micronaut/logging/LogLevel.java (100%) rename {runtime => context}/src/main/java/io/micronaut/logging/LoggingSystem.java (100%) rename {runtime => context}/src/main/java/io/micronaut/logging/PropertiesLoggingLevelsConfigurer.java (100%) rename {runtime => context}/src/main/java/io/micronaut/logging/impl/Log4jLoggingSystem.java (100%) rename {runtime => context}/src/main/java/io/micronaut/logging/impl/LogbackLoggingSystem.java (100%) rename {runtime/src/main/resources/META-INF/native-image/io.micronaut/micronaut-runtime => context/src/main/resources/META-INF/native-image/io.micronaut/micronaut-context}/native-image.properties (73%) create mode 100644 core/src/main/java/io/micronaut/core/beans/BeanInfo.java create mode 100644 core/src/main/java/io/micronaut/core/graal/LoggingFeature.java delete mode 100644 core/src/main/resources/META-INF/native-image/io.micronaut/micronaut-core/reflect-config.json create mode 100644 discovery-core/build.gradle rename {runtime => discovery-core}/src/main/java/io/micronaut/discovery/CompositeDiscoveryClient.java (100%) rename {runtime => discovery-core}/src/main/java/io/micronaut/discovery/DefaultCompositeDiscoveryClient.java (100%) rename {runtime => discovery-core}/src/main/java/io/micronaut/discovery/DefaultServiceInstance.java (100%) rename {runtime => discovery-core}/src/main/java/io/micronaut/discovery/DefaultServiceInstanceIdGenerator.java (100%) rename {runtime => discovery-core}/src/main/java/io/micronaut/discovery/DiscoveryClient.java (100%) rename {runtime => discovery-core}/src/main/java/io/micronaut/discovery/DiscoveryConfiguration.java (100%) rename {runtime => discovery-core}/src/main/java/io/micronaut/discovery/EmbeddedServerInstance.java (100%) rename {runtime => discovery-core}/src/main/java/io/micronaut/discovery/ServiceInstance.java (100%) rename {runtime => discovery-core}/src/main/java/io/micronaut/discovery/ServiceInstanceIdGenerator.java (100%) rename {runtime => discovery-core}/src/main/java/io/micronaut/discovery/ServiceInstanceList.java (100%) rename {runtime => discovery-core}/src/main/java/io/micronaut/discovery/StaticServiceInstanceList.java (100%) rename {runtime => discovery-core}/src/main/java/io/micronaut/discovery/cloud/AbstractComputeInstanceMetadata.java (100%) rename {runtime => discovery-core}/src/main/java/io/micronaut/discovery/cloud/ComputeInstanceMetadata.java (100%) rename {runtime => discovery-core}/src/main/java/io/micronaut/discovery/cloud/ComputeInstanceMetadataResolver.java (100%) rename {runtime => discovery-core}/src/main/java/io/micronaut/discovery/cloud/ComputeInstanceMetadataResolverUtils.java (100%) rename {runtime => discovery-core}/src/main/java/io/micronaut/discovery/cloud/NetworkInterface.java (100%) rename {runtime => discovery-core}/src/main/java/io/micronaut/discovery/cloud/digitalocean/DigitalOceanInstanceMetadata.java (100%) rename {runtime => discovery-core}/src/main/java/io/micronaut/discovery/cloud/digitalocean/DigitalOceanMetadataConfiguration.java (100%) rename {runtime => discovery-core}/src/main/java/io/micronaut/discovery/cloud/digitalocean/DigitalOceanMetadataKeys.java (100%) rename {runtime => discovery-core}/src/main/java/io/micronaut/discovery/cloud/digitalocean/DigitalOceanMetadataResolver.java (100%) rename {runtime => discovery-core}/src/main/java/io/micronaut/discovery/cloud/digitalocean/DigitalOceanNetworkInterface.java (100%) rename {runtime => discovery-core}/src/main/java/io/micronaut/discovery/cloud/package-info.java (100%) rename {runtime => discovery-core}/src/main/java/io/micronaut/discovery/config/ConfigDiscoveryConfiguration.java (100%) rename {runtime => discovery-core}/src/main/java/io/micronaut/discovery/config/ConfigurationClient.java (100%) rename {runtime => discovery-core}/src/main/java/io/micronaut/discovery/config/DefaultCompositeConfigurationClient.java (100%) rename {runtime => discovery-core}/src/main/java/io/micronaut/discovery/config/package-info.java (100%) rename {runtime => discovery-core}/src/main/java/io/micronaut/discovery/event/AbstractServiceInstanceEvent.java (100%) rename {runtime => discovery-core}/src/main/java/io/micronaut/discovery/event/ServiceReadyEvent.java (100%) rename {runtime => discovery-core}/src/main/java/io/micronaut/discovery/event/ServiceStoppedEvent.java (100%) rename {runtime => discovery-core}/src/main/java/io/micronaut/discovery/event/package-info.java (100%) rename {runtime => discovery-core}/src/main/java/io/micronaut/discovery/exceptions/DiscoveryException.java (100%) rename {runtime => discovery-core}/src/main/java/io/micronaut/discovery/exceptions/NoAvailableServiceException.java (100%) rename {runtime => discovery-core}/src/main/java/io/micronaut/discovery/exceptions/package-info.java (100%) rename {runtime => discovery-core}/src/main/java/io/micronaut/discovery/metadata/ServiceInstanceMetadataContributor.java (100%) rename {runtime => discovery-core}/src/main/java/io/micronaut/discovery/metadata/package-info.java (100%) rename {runtime => discovery-core}/src/main/java/io/micronaut/discovery/package-info.java (100%) rename {runtime => discovery-core}/src/main/java/io/micronaut/discovery/registration/AutoRegistration.java (100%) rename {runtime => discovery-core}/src/main/java/io/micronaut/discovery/registration/RegistrationConfiguration.java (100%) rename {runtime => discovery-core}/src/main/java/io/micronaut/discovery/registration/RegistrationException.java (100%) rename {runtime => discovery-core}/src/main/java/io/micronaut/discovery/registration/package-info.java (100%) rename {runtime => discovery-core}/src/main/java/io/micronaut/health/CurrentHealthStatus.java (100%) rename {runtime => discovery-core}/src/main/java/io/micronaut/health/DefaultCurrentHealthStatus.java (100%) rename {runtime => discovery-core}/src/main/java/io/micronaut/health/HealthStatus.java (100%) rename {runtime => discovery-core}/src/main/java/io/micronaut/health/HeartbeatConfiguration.java (100%) rename {runtime => discovery-core}/src/main/java/io/micronaut/health/HeartbeatDiscoveryClientCondition.java (100%) rename {runtime => discovery-core}/src/main/java/io/micronaut/health/HeartbeatEnabled.java (100%) rename {runtime => discovery-core}/src/main/java/io/micronaut/health/HeartbeatEvent.java (100%) rename {runtime => discovery-core}/src/main/java/io/micronaut/health/HeartbeatTask.java (100%) rename {runtime => discovery-core}/src/main/java/io/micronaut/health/package-info.java (100%) delete mode 100644 http-netty/src/main/java/io/micronaut/http/netty/graal/HttpNettyFeature.java rename http-server-netty/src/main/java/io/micronaut/http/server/netty/{ => discovery}/NettyEmbeddedServerInstance.java (97%) create mode 100644 http-server-netty/src/main/java/io/micronaut/http/server/netty/discovery/NettyServiceDiscovery.java create mode 100644 http-server-netty/src/main/java/io/micronaut/http/server/netty/websocket/WebSocketUpgradeHandlerFactory.java rename {runtime/src/main/java/io/micronaut/reactive/reactor => http/src/main/java/io/micronaut/http/reactive}/execution/ReactiveExecutionFlow.java (96%) rename {runtime/src/main/java/io/micronaut/reactive/reactor => http/src/main/java/io/micronaut/http/reactive}/execution/ReactorExecutionFlowImpl.java (99%) rename {runtime => http}/src/main/java/io/micronaut/runtime/http/codec/MediaTypeCodecRegistryFactory.java (100%) rename {runtime => http}/src/main/java/io/micronaut/runtime/http/codec/TextPlainCodec.java (100%) rename {runtime => http}/src/main/java/io/micronaut/runtime/http/codec/package-info.java (100%) rename {runtime => http}/src/main/java/io/micronaut/runtime/http/scope/RequestAware.java (100%) rename {runtime => http}/src/main/java/io/micronaut/runtime/http/scope/RequestCustomScope.java (100%) rename {runtime => http}/src/main/java/io/micronaut/runtime/http/scope/RequestScope.java (100%) rename runtime/src/main/java/io/micronaut/reactive/package-info.java => jackson-databind/src/main/java/io/micronaut/jackson/databind/JacksonDatabindMapperSupplier.java (55%) create mode 100644 jackson-databind/src/main/resources/META-INF/native-image/io.micronaut/micronaut-jackson-databind/native-image.properties create mode 100644 jackson-databind/src/main/resources/META-INF/services/io.micronaut.json.JsonMapperSupplier create mode 100644 json-core/src/main/java/io/micronaut/json/JsonMapperSupplier.java create mode 100644 retry/build.gradle rename {runtime => retry}/src/main/java/io/micronaut/retry/CircuitState.java (100%) rename {runtime => retry}/src/main/java/io/micronaut/retry/RetryState.java (100%) rename {runtime => retry}/src/main/java/io/micronaut/retry/RetryStateBuilder.java (100%) rename {runtime => retry}/src/main/java/io/micronaut/retry/annotation/CircuitBreaker.java (100%) rename {runtime => retry}/src/main/java/io/micronaut/retry/annotation/DefaultRetryPredicate.java (100%) rename {runtime => retry}/src/main/java/io/micronaut/retry/annotation/Fallback.java (100%) rename {runtime => retry}/src/main/java/io/micronaut/retry/annotation/Recoverable.java (100%) rename {runtime => retry}/src/main/java/io/micronaut/retry/annotation/RetryPredicate.java (100%) rename {runtime => retry}/src/main/java/io/micronaut/retry/annotation/Retryable.java (100%) rename {runtime => retry}/src/main/java/io/micronaut/retry/annotation/package-info.java (100%) rename {runtime => retry}/src/main/java/io/micronaut/retry/event/CircuitClosedEvent.java (100%) rename {runtime => retry}/src/main/java/io/micronaut/retry/event/CircuitOpenEvent.java (100%) rename {runtime => retry}/src/main/java/io/micronaut/retry/event/RetryEvent.java (100%) rename {runtime => retry}/src/main/java/io/micronaut/retry/event/RetryEventListener.java (100%) rename {runtime => retry}/src/main/java/io/micronaut/retry/event/package-info.java (100%) rename {runtime => retry}/src/main/java/io/micronaut/retry/exception/CircuitOpenException.java (100%) rename {runtime => retry}/src/main/java/io/micronaut/retry/exception/FallbackException.java (100%) rename {runtime => retry}/src/main/java/io/micronaut/retry/exception/RetryException.java (100%) rename {runtime => retry}/src/main/java/io/micronaut/retry/exception/package-info.java (100%) rename {runtime => retry}/src/main/java/io/micronaut/retry/intercept/AnnotationRetryStateBuilder.java (100%) rename {runtime => retry}/src/main/java/io/micronaut/retry/intercept/CircuitBreakerRetry.java (100%) rename {runtime => retry}/src/main/java/io/micronaut/retry/intercept/DefaultRetryInterceptor.java (100%) rename {runtime => retry}/src/main/java/io/micronaut/retry/intercept/MutableRetryState.java (100%) rename {runtime/src/test/groovy => retry/src/main/java}/io/micronaut/retry/intercept/MyCustomException.java (100%) rename {runtime => retry}/src/main/java/io/micronaut/retry/intercept/RecoveryInterceptor.java (93%) rename {runtime => retry}/src/main/java/io/micronaut/retry/intercept/SimpleRetry.java (100%) rename {runtime => retry}/src/main/java/io/micronaut/retry/intercept/package-info.java (100%) rename {runtime => retry}/src/main/java/io/micronaut/retry/package-info.java (100%) rename {runtime => retry}/src/test/groovy/io/micronaut/retry/intercept/CircuitBreakerRetrySpec.groovy (100%) rename {runtime => retry}/src/test/groovy/io/micronaut/retry/intercept/CircuitBreakerSpec.groovy (100%) rename {runtime => retry}/src/test/groovy/io/micronaut/retry/intercept/InterceptorOrderSpec.groovy (100%) rename {runtime => retry}/src/test/groovy/io/micronaut/retry/intercept/SimpleRetryInstanceSpec.groovy (100%) rename {runtime => retry}/src/test/groovy/io/micronaut/retry/intercept/SimpleRetrySpec.groovy (100%) delete mode 100644 runtime/src/main/java/io/micronaut/reactive/flow/converters/FlowConverterRegistrar.java delete mode 100644 runtime/src/main/java/io/micronaut/reactive/reactor/instrument/ReactorInstrumentation.java delete mode 100644 runtime/src/main/java/io/micronaut/reactive/reactor/instrument/ReactorSubscriber.java delete mode 100644 test-suite-kotlin/src/test/kotlin/io/micronaut/MdcPropagationSpec.kt diff --git a/buffer-netty/src/main/java/io/micronaut/buffer/netty/NettyFeature.java b/buffer-netty/src/main/java/io/micronaut/buffer/netty/NettyFeature.java index dd1b22442de..dc320f7c73f 100644 --- a/buffer-netty/src/main/java/io/micronaut/buffer/netty/NettyFeature.java +++ b/buffer-netty/src/main/java/io/micronaut/buffer/netty/NettyFeature.java @@ -15,7 +15,6 @@ */ package io.micronaut.buffer.netty; -import com.oracle.svm.core.annotate.AutomaticFeature; import com.oracle.svm.core.jdk.SystemPropertiesSupport; import io.micronaut.core.annotation.Internal; import io.micronaut.core.graal.AutomaticFeatureUtils; @@ -36,7 +35,6 @@ * @since 3.3.0 */ @Internal -@AutomaticFeature final class NettyFeature implements Feature { @Override public void beforeAnalysis(BeforeAnalysisAccess access) { diff --git a/buffer-netty/src/main/resources/META-INF/native-image/io.micronaut/micronaut-buffer-netty/native-image.properties b/buffer-netty/src/main/resources/META-INF/native-image/io.micronaut/micronaut-buffer-netty/native-image.properties new file mode 100644 index 00000000000..7552ba4d263 --- /dev/null +++ b/buffer-netty/src/main/resources/META-INF/native-image/io.micronaut/micronaut-buffer-netty/native-image.properties @@ -0,0 +1 @@ +Args = --features=io.micronaut.buffer.netty.NettyFeature diff --git a/context/build.gradle b/context/build.gradle index 88f1a373a42..03cf4e0d427 100644 --- a/context/build.gradle +++ b/context/build.gradle @@ -8,8 +8,12 @@ dependencies { api project(':inject') api project(':aop') api libs.managed.validation + compileOnly project(':core-reactive') compileOnly project(':core-processor') + compileOnly libs.log4j + compileOnly libs.managed.logback + testCompileOnly project(":inject-groovy") testAnnotationProcessor project(":inject-java") testImplementation project(":core-reactive") diff --git a/runtime/src/main/java/io/micronaut/logging/LogLevel.java b/context/src/main/java/io/micronaut/logging/LogLevel.java similarity index 100% rename from runtime/src/main/java/io/micronaut/logging/LogLevel.java rename to context/src/main/java/io/micronaut/logging/LogLevel.java diff --git a/runtime/src/main/java/io/micronaut/logging/LoggingSystem.java b/context/src/main/java/io/micronaut/logging/LoggingSystem.java similarity index 100% rename from runtime/src/main/java/io/micronaut/logging/LoggingSystem.java rename to context/src/main/java/io/micronaut/logging/LoggingSystem.java diff --git a/runtime/src/main/java/io/micronaut/logging/PropertiesLoggingLevelsConfigurer.java b/context/src/main/java/io/micronaut/logging/PropertiesLoggingLevelsConfigurer.java similarity index 100% rename from runtime/src/main/java/io/micronaut/logging/PropertiesLoggingLevelsConfigurer.java rename to context/src/main/java/io/micronaut/logging/PropertiesLoggingLevelsConfigurer.java diff --git a/runtime/src/main/java/io/micronaut/logging/impl/Log4jLoggingSystem.java b/context/src/main/java/io/micronaut/logging/impl/Log4jLoggingSystem.java similarity index 100% rename from runtime/src/main/java/io/micronaut/logging/impl/Log4jLoggingSystem.java rename to context/src/main/java/io/micronaut/logging/impl/Log4jLoggingSystem.java diff --git a/runtime/src/main/java/io/micronaut/logging/impl/LogbackLoggingSystem.java b/context/src/main/java/io/micronaut/logging/impl/LogbackLoggingSystem.java similarity index 100% rename from runtime/src/main/java/io/micronaut/logging/impl/LogbackLoggingSystem.java rename to context/src/main/java/io/micronaut/logging/impl/LogbackLoggingSystem.java diff --git a/runtime/src/main/resources/META-INF/native-image/io.micronaut/micronaut-runtime/native-image.properties b/context/src/main/resources/META-INF/native-image/io.micronaut/micronaut-context/native-image.properties similarity index 73% rename from runtime/src/main/resources/META-INF/native-image/io.micronaut/micronaut-runtime/native-image.properties rename to context/src/main/resources/META-INF/native-image/io.micronaut/micronaut-context/native-image.properties index 92530519f1d..0809e7c396f 100644 --- a/runtime/src/main/resources/META-INF/native-image/io.micronaut/micronaut-runtime/native-image.properties +++ b/context/src/main/resources/META-INF/native-image/io.micronaut/micronaut-context/native-image.properties @@ -15,6 +15,5 @@ # Args = --install-exit-handlers \ - --initialize-at-run-time=io.micronaut.reactive.reactor.ReactorInstrumentation,io.micronaut.discovery.cloud.digitalocean.$DigitalOceanMetadataResolver$Definition \ - --initialize-at-build-time=ch.qos.logback,io.micronaut,io.reactivex,org.reactivestreams,org.slf4j,org.yaml.snakeyaml,javax.xml \ + --initialize-at-build-time=org.reactivestreams,javax.xml \ --initialize-at-build-time=com.sun.org.apache.xerces.internal.util,com.sun.org.apache.xerces.internal.impl,jdk.xml.internal,com.sun.xml.internal.stream.util,com.sun.org.apache.xerces.internal.xni,com.sun.org.apache.xerces.internal.utils diff --git a/core/src/main/java/io/micronaut/core/beans/BeanInfo.java b/core/src/main/java/io/micronaut/core/beans/BeanInfo.java new file mode 100644 index 00000000000..84c649b0684 --- /dev/null +++ b/core/src/main/java/io/micronaut/core/beans/BeanInfo.java @@ -0,0 +1,33 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.core.beans; + +import io.micronaut.core.annotation.AnnotationMetadataProvider; +import io.micronaut.core.annotation.NonNull; + +/** + * Top level interface for obtaining bean information. + * + * @param The type of the bean + * @since 4.0.0 + */ +public interface BeanInfo extends AnnotationMetadataProvider { + /** + * @return The bean type + */ + @NonNull + Class getBeanType(); +} diff --git a/core/src/main/java/io/micronaut/core/beans/BeanIntrospection.java b/core/src/main/java/io/micronaut/core/beans/BeanIntrospection.java index 2d77a1d506c..e2ffdf4f4fb 100644 --- a/core/src/main/java/io/micronaut/core/beans/BeanIntrospection.java +++ b/core/src/main/java/io/micronaut/core/beans/BeanIntrospection.java @@ -45,7 +45,7 @@ * @since 1.1 */ @Immutable -public interface BeanIntrospection extends AnnotationMetadataDelegate { +public interface BeanIntrospection extends AnnotationMetadataDelegate, BeanInfo { /** * @return A immutable collection of properties. diff --git a/core/src/main/java/io/micronaut/core/graal/AutomaticFeatureUtils.java b/core/src/main/java/io/micronaut/core/graal/AutomaticFeatureUtils.java index 4a7cca8e105..b46c68d4a64 100644 --- a/core/src/main/java/io/micronaut/core/graal/AutomaticFeatureUtils.java +++ b/core/src/main/java/io/micronaut/core/graal/AutomaticFeatureUtils.java @@ -31,7 +31,7 @@ import java.util.Optional; /** - * Utility methods for implementing Graal's {@link com.oracle.svm.core.annotate.AutomaticFeature}. + * Utility methods for implementing GraalVM. * * @author Álvaro Sánchez-Mariscal * @author graemerocher diff --git a/core/src/main/java/io/micronaut/core/graal/GraalReflectionConfigurer.java b/core/src/main/java/io/micronaut/core/graal/GraalReflectionConfigurer.java index 8d4fd78bb2d..da561d38c27 100644 --- a/core/src/main/java/io/micronaut/core/graal/GraalReflectionConfigurer.java +++ b/core/src/main/java/io/micronaut/core/graal/GraalReflectionConfigurer.java @@ -121,11 +121,19 @@ default void configure(ReflectionConfigurationContext context) { } } if (n.equals("")) { - ReflectionUtils.findConstructor(t, parameterTypes) - .ifPresent(context::register); + try { + Constructor c = t.getDeclaredConstructor(parameterTypes); + context.register(c); + } catch (NoSuchMethodException e) { + // ignore + } } else { - ReflectionUtils.findMethod(t, n, parameterTypes) - .ifPresent(context::register); + try { + Method method = t.getDeclaredMethod(n, parameterTypes); + context.register(method); + } catch (NoSuchMethodException e) { + // ignore + } } }); } diff --git a/core/src/main/java/io/micronaut/core/graal/LoggingFeature.java b/core/src/main/java/io/micronaut/core/graal/LoggingFeature.java new file mode 100644 index 00000000000..3d89677e5bf --- /dev/null +++ b/core/src/main/java/io/micronaut/core/graal/LoggingFeature.java @@ -0,0 +1,68 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.core.graal; + +import io.micronaut.core.annotation.Internal; +import org.graalvm.nativeimage.hosted.Feature; +import org.graalvm.nativeimage.hosted.RuntimeReflection; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; +import java.util.Objects; +import java.util.stream.Stream; + +/** + * Configures logback for runtime reflection if required. + * + * @since 4.0.0 + * @author graemerocher + */ +@Internal +public final class LoggingFeature implements Feature { + + @Override + public void beforeAnalysis(BeforeAnalysisAccess access) { + Stream.of("ch.qos.logback.classic.encoder.PatternLayoutEncoder", + "ch.qos.logback.classic.pattern.DateConverter", + "ch.qos.logback.classic.pattern.LevelConverter", + "ch.qos.logback.classic.pattern.LineSeparatorConverter", + "ch.qos.logback.classic.pattern.LoggerConverter", + "ch.qos.logback.classic.pattern.MessageConverter", + "ch.qos.logback.classic.pattern.ThreadConverter", + "ch.qos.logback.classic.pattern.color.HighlightingCompositeConverter", + "ch.qos.logback.core.ConsoleAppender", + "ch.qos.logback.core.OutputStreamAppender", + "ch.qos.logback.core.encoder.LayoutWrappingEncoder", + "ch.qos.logback.core.pattern.PatternLayoutEncoderBase", + "ch.qos.logback.core.pattern.color.CyanCompositeConverter", + "ch.qos.logback.core.pattern.color.GrayCompositeConverter", + "ch.qos.logback.core.pattern.color.MagentaCompositeConverter") + .map(access::findClassByName) + .filter(Objects::nonNull) + .forEach(t -> { + RuntimeReflection.registerForReflectiveInstantiation(t); + RuntimeReflection.register(t); + Constructor[] declaredConstructors = t.getConstructors(); + for (Constructor c : declaredConstructors) { + RuntimeReflection.register(c); + } + Method[] methods = t.getMethods(); + for (Method method : methods) { + RuntimeReflection.register(method); + } + }); + } +} diff --git a/core/src/main/java/io/micronaut/core/graal/ServiceLoaderInitialization.java b/core/src/main/java/io/micronaut/core/graal/ServiceLoaderInitialization.java index 17c3cbfe216..be41519263b 100644 --- a/core/src/main/java/io/micronaut/core/graal/ServiceLoaderInitialization.java +++ b/core/src/main/java/io/micronaut/core/graal/ServiceLoaderInitialization.java @@ -15,35 +15,43 @@ */ package io.micronaut.core.graal; +import com.oracle.svm.core.annotate.Substitute; +import com.oracle.svm.core.annotate.TargetClass; +import io.micronaut.core.annotation.AnnotationValue; +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.NonNull; +import io.micronaut.core.beans.BeanInfo; +import io.micronaut.core.io.IOUtils; +import io.micronaut.core.io.service.SoftServiceLoader; +import io.micronaut.core.reflect.exception.InstantiationException; +import io.micronaut.core.util.ArrayUtils; +import org.graalvm.nativeimage.ImageSingletons; +import org.graalvm.nativeimage.hosted.Feature; +import org.graalvm.nativeimage.hosted.RuntimeClassInitialization; +import org.graalvm.nativeimage.hosted.RuntimeReflection; + import java.io.IOException; +import java.lang.annotation.Annotation; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.net.URI; import java.net.URISyntaxException; import java.net.URL; -import java.nio.file.*; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Enumeration; import java.util.HashMap; import java.util.HashSet; +import java.util.Iterator; +import java.util.List; import java.util.Map; import java.util.Set; -import com.oracle.svm.core.annotate.AutomaticFeature; -import com.oracle.svm.core.annotate.Substitute; -import com.oracle.svm.core.annotate.TargetClass; -import io.micronaut.core.annotation.Internal; -import io.micronaut.core.annotation.NonNull; -import io.micronaut.core.io.IOUtils; -import io.micronaut.core.io.service.SoftServiceLoader; -import org.graalvm.nativeimage.ImageSingletons; -import org.graalvm.nativeimage.hosted.Feature; -import org.graalvm.nativeimage.hosted.RuntimeClassInitialization; -import org.graalvm.nativeimage.hosted.RuntimeReflection; - /** * Integrates {@link io.micronaut.core.io.service.SoftServiceLoader} with GraalVM Native Image. * @@ -51,25 +59,58 @@ * @since 3.5.0 */ @SuppressWarnings("unused") -@AutomaticFeature final class ServiceLoaderFeature implements Feature { @Override + @SuppressWarnings("java:S1119") public void beforeAnalysis(BeforeAnalysisAccess access) { configureForReflection(access); StaticServiceDefinitions staticServiceDefinitions = buildStaticServiceDefinitions(access); final Collection> allTypeNames = staticServiceDefinitions.serviceTypeMap.values(); for (Set typeNameSet : allTypeNames) { - for (String typeName : typeNameSet) { + Iterator i = typeNameSet.iterator(); + serviceLoop: while (i.hasNext()) { + String typeName = i.next(); try { final Class c = access.findClassByName(typeName); if (c != null) { + if (GraalReflectionConfigurer.class.isAssignableFrom(c)) { + continue; + } else if (BeanInfo.class.isAssignableFrom(c)) { + + BeanInfo beanInfo; + try { + beanInfo = (BeanInfo) c.getDeclaredConstructor().newInstance(); + } catch (Exception e) { + continue; + } + Class beanType = beanInfo.getBeanType(); + List> values = beanInfo.getAnnotationMetadata().getAnnotationValuesByName("io.micronaut.context.annotation.Requires"); + if (!values.isEmpty()) { + for (AnnotationValue value : values) { + String[] classNames = new String[0]; + if (value.contains("classes")) { + classNames = value.stringValues("classes"); + } + if (value.contains("beans")) { + ArrayUtils.concat(classNames, value.stringValues("beans")); + } + for (String className : classNames) { + if (access.findClassByName(className) == null) { + i.remove(); + continue serviceLoop; + } + } + } + } + } + RuntimeClassInitialization.initializeAtBuildTime(c); RuntimeReflection.registerForReflectiveInstantiation(c); RuntimeReflection.register(c); } - } catch (NoClassDefFoundError e) { - // missing dependencies ignore and let it fail at runtime + } catch (NoClassDefFoundError | InstantiationException e) { + i.remove(); } } } diff --git a/core/src/main/java/io/micronaut/core/io/IOUtils.java b/core/src/main/java/io/micronaut/core/io/IOUtils.java index bdb8032ae25..a8b46141e75 100644 --- a/core/src/main/java/io/micronaut/core/io/IOUtils.java +++ b/core/src/main/java/io/micronaut/core/io/IOUtils.java @@ -194,15 +194,12 @@ public static String readText(BufferedReader reader) throws IOException { reader.close(); } } catch (IOException e) { - if (IOLogging.LOG.isWarnEnabled()) { - IOLogging.LOG.warn("Failed to close reader: " + e.getMessage(), e); + Logger logger = LoggerFactory.getLogger(Logger.class); + if (logger.isWarnEnabled()) { + logger.warn("Failed to close reader: " + e.getMessage(), e); } } } return answer.toString(); } - - private static final class IOLogging { - private static final Logger LOG = LoggerFactory.getLogger(IOLogging.class); - } } diff --git a/core/src/main/java/io/micronaut/core/io/scan/DefaultClassPathResourceLoader.java b/core/src/main/java/io/micronaut/core/io/scan/DefaultClassPathResourceLoader.java index 00dd3435e59..bedb0a193e2 100644 --- a/core/src/main/java/io/micronaut/core/io/scan/DefaultClassPathResourceLoader.java +++ b/core/src/main/java/io/micronaut/core/io/scan/DefaultClassPathResourceLoader.java @@ -40,8 +40,6 @@ */ public class DefaultClassPathResourceLoader implements ClassPathResourceLoader { - private static final Logger LOG = LoggerFactory.getLogger(DefaultClassPathResourceLoader.class); - private final ClassLoader classLoader; private final String basePath; private final URL baseURL; @@ -129,8 +127,9 @@ public Optional getResourceAsStream(String path) { try { fileSystem.close(); } catch (IOException e) { - if (LOG.isDebugEnabled()) { - LOG.debug("Error shutting down JAR file system [" + fileSystem + "]: " + e.getMessage(), e); + Logger log = LoggerFactory.getLogger(DefaultClassPathResourceLoader.class); + if (log.isDebugEnabled()) { + log.debug("Error shutting down JAR file system [" + fileSystem + "]: " + e.getMessage(), e); } } } @@ -144,8 +143,9 @@ public Optional getResourceAsStream(String path) { return Optional.of(Files.newInputStream(pathObject)); } } catch (URISyntaxException | IOException | ProviderNotFoundException e) { - if (LOG.isDebugEnabled()) { - LOG.debug("Error establishing whether path is a directory: " + e.getMessage(), e); + Logger log = LoggerFactory.getLogger(DefaultClassPathResourceLoader.class); + if (log.isDebugEnabled()) { + log.debug("Error establishing whether path is a directory: " + e.getMessage(), e); } } } @@ -297,8 +297,9 @@ private boolean isDirectory(String path) { try { fileSystem.close(); } catch (IOException e) { - if (LOG.isDebugEnabled()) { - LOG.debug("Error shutting down JAR file system [" + fileSystem + "]: " + e.getMessage(), e); + Logger log = LoggerFactory.getLogger(DefaultClassPathResourceLoader.class); + if (log.isDebugEnabled()) { + log.debug("Error shutting down JAR file system [" + fileSystem + "]: " + e.getMessage(), e); } } } @@ -309,8 +310,9 @@ private boolean isDirectory(String path) { return pathObject == null || Files.isDirectory(pathObject); } } catch (URISyntaxException | IOException | ProviderNotFoundException e) { - if (LOG.isDebugEnabled()) { - LOG.debug("Error establishing whether path is a directory: " + e.getMessage(), e); + Logger log = LoggerFactory.getLogger(DefaultClassPathResourceLoader.class); + if (log.isDebugEnabled()) { + log.debug("Error establishing whether path is a directory: " + e.getMessage(), e); } } } diff --git a/core/src/main/resources/META-INF/native-image/io.micronaut/micronaut-core/native-image.properties b/core/src/main/resources/META-INF/native-image/io.micronaut/micronaut-core/native-image.properties index d90f2dd7a7f..3e9d3606c01 100644 --- a/core/src/main/resources/META-INF/native-image/io.micronaut/micronaut-core/native-image.properties +++ b/core/src/main/resources/META-INF/native-image/io.micronaut/micronaut-core/native-image.properties @@ -14,4 +14,12 @@ # limitations under the License. # -Args = --initialize-at-run-time=io.micronaut.core.io.socket.SocketUtils +Args = --initialize-at-run-time=io.micronaut.core.io.socket.SocketUtils \ + --initialize-at-build-time=io.micronaut.core.io \ + --initialize-at-build-time=io.micronaut.core.optim \ + --initialize-at-build-time=io.micronaut.core.util \ + --initialize-at-build-time=io.micronaut.core.convert \ + --initialize-at-build-time=io.micronaut.core.type \ + --initialize-at-build-time=io.micronaut.core.annotation \ + --features=io.micronaut.core.graal.LoggingFeature \ + --features=io.micronaut.core.graal.ServiceLoaderFeature diff --git a/core/src/main/resources/META-INF/native-image/io.micronaut/micronaut-core/reflect-config.json b/core/src/main/resources/META-INF/native-image/io.micronaut/micronaut-core/reflect-config.json deleted file mode 100644 index d83d217ef94..00000000000 --- a/core/src/main/resources/META-INF/native-image/io.micronaut/micronaut-core/reflect-config.json +++ /dev/null @@ -1,7 +0,0 @@ -[ { - "name" : "io.micronaut.caffeine.cache.PSW", - "allDeclaredConstructors" : true -}, { - "name" : "io.micronaut.caffeine.cache.SSLA", - "allDeclaredConstructors" : true -} ] diff --git a/discovery-core/build.gradle b/discovery-core/build.gradle new file mode 100644 index 00000000000..8745608bb61 --- /dev/null +++ b/discovery-core/build.gradle @@ -0,0 +1,13 @@ +plugins { + id "io.micronaut.build.internal.convention-library" +} + +dependencies { + annotationProcessor project(":inject-java") + annotationProcessor project(":graal") + api project(':context') + implementation libs.managed.reactor + compileOnly project(":jackson-databind") + testImplementation project(":jackson-databind") +// api project(":http") +} diff --git a/runtime/src/main/java/io/micronaut/discovery/CompositeDiscoveryClient.java b/discovery-core/src/main/java/io/micronaut/discovery/CompositeDiscoveryClient.java similarity index 100% rename from runtime/src/main/java/io/micronaut/discovery/CompositeDiscoveryClient.java rename to discovery-core/src/main/java/io/micronaut/discovery/CompositeDiscoveryClient.java diff --git a/runtime/src/main/java/io/micronaut/discovery/DefaultCompositeDiscoveryClient.java b/discovery-core/src/main/java/io/micronaut/discovery/DefaultCompositeDiscoveryClient.java similarity index 100% rename from runtime/src/main/java/io/micronaut/discovery/DefaultCompositeDiscoveryClient.java rename to discovery-core/src/main/java/io/micronaut/discovery/DefaultCompositeDiscoveryClient.java diff --git a/runtime/src/main/java/io/micronaut/discovery/DefaultServiceInstance.java b/discovery-core/src/main/java/io/micronaut/discovery/DefaultServiceInstance.java similarity index 100% rename from runtime/src/main/java/io/micronaut/discovery/DefaultServiceInstance.java rename to discovery-core/src/main/java/io/micronaut/discovery/DefaultServiceInstance.java diff --git a/runtime/src/main/java/io/micronaut/discovery/DefaultServiceInstanceIdGenerator.java b/discovery-core/src/main/java/io/micronaut/discovery/DefaultServiceInstanceIdGenerator.java similarity index 100% rename from runtime/src/main/java/io/micronaut/discovery/DefaultServiceInstanceIdGenerator.java rename to discovery-core/src/main/java/io/micronaut/discovery/DefaultServiceInstanceIdGenerator.java diff --git a/runtime/src/main/java/io/micronaut/discovery/DiscoveryClient.java b/discovery-core/src/main/java/io/micronaut/discovery/DiscoveryClient.java similarity index 100% rename from runtime/src/main/java/io/micronaut/discovery/DiscoveryClient.java rename to discovery-core/src/main/java/io/micronaut/discovery/DiscoveryClient.java diff --git a/runtime/src/main/java/io/micronaut/discovery/DiscoveryConfiguration.java b/discovery-core/src/main/java/io/micronaut/discovery/DiscoveryConfiguration.java similarity index 100% rename from runtime/src/main/java/io/micronaut/discovery/DiscoveryConfiguration.java rename to discovery-core/src/main/java/io/micronaut/discovery/DiscoveryConfiguration.java diff --git a/runtime/src/main/java/io/micronaut/discovery/EmbeddedServerInstance.java b/discovery-core/src/main/java/io/micronaut/discovery/EmbeddedServerInstance.java similarity index 100% rename from runtime/src/main/java/io/micronaut/discovery/EmbeddedServerInstance.java rename to discovery-core/src/main/java/io/micronaut/discovery/EmbeddedServerInstance.java diff --git a/runtime/src/main/java/io/micronaut/discovery/ServiceInstance.java b/discovery-core/src/main/java/io/micronaut/discovery/ServiceInstance.java similarity index 100% rename from runtime/src/main/java/io/micronaut/discovery/ServiceInstance.java rename to discovery-core/src/main/java/io/micronaut/discovery/ServiceInstance.java diff --git a/runtime/src/main/java/io/micronaut/discovery/ServiceInstanceIdGenerator.java b/discovery-core/src/main/java/io/micronaut/discovery/ServiceInstanceIdGenerator.java similarity index 100% rename from runtime/src/main/java/io/micronaut/discovery/ServiceInstanceIdGenerator.java rename to discovery-core/src/main/java/io/micronaut/discovery/ServiceInstanceIdGenerator.java diff --git a/runtime/src/main/java/io/micronaut/discovery/ServiceInstanceList.java b/discovery-core/src/main/java/io/micronaut/discovery/ServiceInstanceList.java similarity index 100% rename from runtime/src/main/java/io/micronaut/discovery/ServiceInstanceList.java rename to discovery-core/src/main/java/io/micronaut/discovery/ServiceInstanceList.java diff --git a/runtime/src/main/java/io/micronaut/discovery/StaticServiceInstanceList.java b/discovery-core/src/main/java/io/micronaut/discovery/StaticServiceInstanceList.java similarity index 100% rename from runtime/src/main/java/io/micronaut/discovery/StaticServiceInstanceList.java rename to discovery-core/src/main/java/io/micronaut/discovery/StaticServiceInstanceList.java diff --git a/runtime/src/main/java/io/micronaut/discovery/cloud/AbstractComputeInstanceMetadata.java b/discovery-core/src/main/java/io/micronaut/discovery/cloud/AbstractComputeInstanceMetadata.java similarity index 100% rename from runtime/src/main/java/io/micronaut/discovery/cloud/AbstractComputeInstanceMetadata.java rename to discovery-core/src/main/java/io/micronaut/discovery/cloud/AbstractComputeInstanceMetadata.java diff --git a/runtime/src/main/java/io/micronaut/discovery/cloud/ComputeInstanceMetadata.java b/discovery-core/src/main/java/io/micronaut/discovery/cloud/ComputeInstanceMetadata.java similarity index 100% rename from runtime/src/main/java/io/micronaut/discovery/cloud/ComputeInstanceMetadata.java rename to discovery-core/src/main/java/io/micronaut/discovery/cloud/ComputeInstanceMetadata.java diff --git a/runtime/src/main/java/io/micronaut/discovery/cloud/ComputeInstanceMetadataResolver.java b/discovery-core/src/main/java/io/micronaut/discovery/cloud/ComputeInstanceMetadataResolver.java similarity index 100% rename from runtime/src/main/java/io/micronaut/discovery/cloud/ComputeInstanceMetadataResolver.java rename to discovery-core/src/main/java/io/micronaut/discovery/cloud/ComputeInstanceMetadataResolver.java diff --git a/runtime/src/main/java/io/micronaut/discovery/cloud/ComputeInstanceMetadataResolverUtils.java b/discovery-core/src/main/java/io/micronaut/discovery/cloud/ComputeInstanceMetadataResolverUtils.java similarity index 100% rename from runtime/src/main/java/io/micronaut/discovery/cloud/ComputeInstanceMetadataResolverUtils.java rename to discovery-core/src/main/java/io/micronaut/discovery/cloud/ComputeInstanceMetadataResolverUtils.java diff --git a/runtime/src/main/java/io/micronaut/discovery/cloud/NetworkInterface.java b/discovery-core/src/main/java/io/micronaut/discovery/cloud/NetworkInterface.java similarity index 100% rename from runtime/src/main/java/io/micronaut/discovery/cloud/NetworkInterface.java rename to discovery-core/src/main/java/io/micronaut/discovery/cloud/NetworkInterface.java diff --git a/runtime/src/main/java/io/micronaut/discovery/cloud/digitalocean/DigitalOceanInstanceMetadata.java b/discovery-core/src/main/java/io/micronaut/discovery/cloud/digitalocean/DigitalOceanInstanceMetadata.java similarity index 100% rename from runtime/src/main/java/io/micronaut/discovery/cloud/digitalocean/DigitalOceanInstanceMetadata.java rename to discovery-core/src/main/java/io/micronaut/discovery/cloud/digitalocean/DigitalOceanInstanceMetadata.java diff --git a/runtime/src/main/java/io/micronaut/discovery/cloud/digitalocean/DigitalOceanMetadataConfiguration.java b/discovery-core/src/main/java/io/micronaut/discovery/cloud/digitalocean/DigitalOceanMetadataConfiguration.java similarity index 100% rename from runtime/src/main/java/io/micronaut/discovery/cloud/digitalocean/DigitalOceanMetadataConfiguration.java rename to discovery-core/src/main/java/io/micronaut/discovery/cloud/digitalocean/DigitalOceanMetadataConfiguration.java diff --git a/runtime/src/main/java/io/micronaut/discovery/cloud/digitalocean/DigitalOceanMetadataKeys.java b/discovery-core/src/main/java/io/micronaut/discovery/cloud/digitalocean/DigitalOceanMetadataKeys.java similarity index 100% rename from runtime/src/main/java/io/micronaut/discovery/cloud/digitalocean/DigitalOceanMetadataKeys.java rename to discovery-core/src/main/java/io/micronaut/discovery/cloud/digitalocean/DigitalOceanMetadataKeys.java diff --git a/runtime/src/main/java/io/micronaut/discovery/cloud/digitalocean/DigitalOceanMetadataResolver.java b/discovery-core/src/main/java/io/micronaut/discovery/cloud/digitalocean/DigitalOceanMetadataResolver.java similarity index 100% rename from runtime/src/main/java/io/micronaut/discovery/cloud/digitalocean/DigitalOceanMetadataResolver.java rename to discovery-core/src/main/java/io/micronaut/discovery/cloud/digitalocean/DigitalOceanMetadataResolver.java diff --git a/runtime/src/main/java/io/micronaut/discovery/cloud/digitalocean/DigitalOceanNetworkInterface.java b/discovery-core/src/main/java/io/micronaut/discovery/cloud/digitalocean/DigitalOceanNetworkInterface.java similarity index 100% rename from runtime/src/main/java/io/micronaut/discovery/cloud/digitalocean/DigitalOceanNetworkInterface.java rename to discovery-core/src/main/java/io/micronaut/discovery/cloud/digitalocean/DigitalOceanNetworkInterface.java diff --git a/runtime/src/main/java/io/micronaut/discovery/cloud/package-info.java b/discovery-core/src/main/java/io/micronaut/discovery/cloud/package-info.java similarity index 100% rename from runtime/src/main/java/io/micronaut/discovery/cloud/package-info.java rename to discovery-core/src/main/java/io/micronaut/discovery/cloud/package-info.java diff --git a/runtime/src/main/java/io/micronaut/discovery/config/ConfigDiscoveryConfiguration.java b/discovery-core/src/main/java/io/micronaut/discovery/config/ConfigDiscoveryConfiguration.java similarity index 100% rename from runtime/src/main/java/io/micronaut/discovery/config/ConfigDiscoveryConfiguration.java rename to discovery-core/src/main/java/io/micronaut/discovery/config/ConfigDiscoveryConfiguration.java diff --git a/runtime/src/main/java/io/micronaut/discovery/config/ConfigurationClient.java b/discovery-core/src/main/java/io/micronaut/discovery/config/ConfigurationClient.java similarity index 100% rename from runtime/src/main/java/io/micronaut/discovery/config/ConfigurationClient.java rename to discovery-core/src/main/java/io/micronaut/discovery/config/ConfigurationClient.java diff --git a/runtime/src/main/java/io/micronaut/discovery/config/DefaultCompositeConfigurationClient.java b/discovery-core/src/main/java/io/micronaut/discovery/config/DefaultCompositeConfigurationClient.java similarity index 100% rename from runtime/src/main/java/io/micronaut/discovery/config/DefaultCompositeConfigurationClient.java rename to discovery-core/src/main/java/io/micronaut/discovery/config/DefaultCompositeConfigurationClient.java diff --git a/runtime/src/main/java/io/micronaut/discovery/config/package-info.java b/discovery-core/src/main/java/io/micronaut/discovery/config/package-info.java similarity index 100% rename from runtime/src/main/java/io/micronaut/discovery/config/package-info.java rename to discovery-core/src/main/java/io/micronaut/discovery/config/package-info.java diff --git a/runtime/src/main/java/io/micronaut/discovery/event/AbstractServiceInstanceEvent.java b/discovery-core/src/main/java/io/micronaut/discovery/event/AbstractServiceInstanceEvent.java similarity index 100% rename from runtime/src/main/java/io/micronaut/discovery/event/AbstractServiceInstanceEvent.java rename to discovery-core/src/main/java/io/micronaut/discovery/event/AbstractServiceInstanceEvent.java diff --git a/runtime/src/main/java/io/micronaut/discovery/event/ServiceReadyEvent.java b/discovery-core/src/main/java/io/micronaut/discovery/event/ServiceReadyEvent.java similarity index 100% rename from runtime/src/main/java/io/micronaut/discovery/event/ServiceReadyEvent.java rename to discovery-core/src/main/java/io/micronaut/discovery/event/ServiceReadyEvent.java diff --git a/runtime/src/main/java/io/micronaut/discovery/event/ServiceStoppedEvent.java b/discovery-core/src/main/java/io/micronaut/discovery/event/ServiceStoppedEvent.java similarity index 100% rename from runtime/src/main/java/io/micronaut/discovery/event/ServiceStoppedEvent.java rename to discovery-core/src/main/java/io/micronaut/discovery/event/ServiceStoppedEvent.java diff --git a/runtime/src/main/java/io/micronaut/discovery/event/package-info.java b/discovery-core/src/main/java/io/micronaut/discovery/event/package-info.java similarity index 100% rename from runtime/src/main/java/io/micronaut/discovery/event/package-info.java rename to discovery-core/src/main/java/io/micronaut/discovery/event/package-info.java diff --git a/runtime/src/main/java/io/micronaut/discovery/exceptions/DiscoveryException.java b/discovery-core/src/main/java/io/micronaut/discovery/exceptions/DiscoveryException.java similarity index 100% rename from runtime/src/main/java/io/micronaut/discovery/exceptions/DiscoveryException.java rename to discovery-core/src/main/java/io/micronaut/discovery/exceptions/DiscoveryException.java diff --git a/runtime/src/main/java/io/micronaut/discovery/exceptions/NoAvailableServiceException.java b/discovery-core/src/main/java/io/micronaut/discovery/exceptions/NoAvailableServiceException.java similarity index 100% rename from runtime/src/main/java/io/micronaut/discovery/exceptions/NoAvailableServiceException.java rename to discovery-core/src/main/java/io/micronaut/discovery/exceptions/NoAvailableServiceException.java diff --git a/runtime/src/main/java/io/micronaut/discovery/exceptions/package-info.java b/discovery-core/src/main/java/io/micronaut/discovery/exceptions/package-info.java similarity index 100% rename from runtime/src/main/java/io/micronaut/discovery/exceptions/package-info.java rename to discovery-core/src/main/java/io/micronaut/discovery/exceptions/package-info.java diff --git a/runtime/src/main/java/io/micronaut/discovery/metadata/ServiceInstanceMetadataContributor.java b/discovery-core/src/main/java/io/micronaut/discovery/metadata/ServiceInstanceMetadataContributor.java similarity index 100% rename from runtime/src/main/java/io/micronaut/discovery/metadata/ServiceInstanceMetadataContributor.java rename to discovery-core/src/main/java/io/micronaut/discovery/metadata/ServiceInstanceMetadataContributor.java diff --git a/runtime/src/main/java/io/micronaut/discovery/metadata/package-info.java b/discovery-core/src/main/java/io/micronaut/discovery/metadata/package-info.java similarity index 100% rename from runtime/src/main/java/io/micronaut/discovery/metadata/package-info.java rename to discovery-core/src/main/java/io/micronaut/discovery/metadata/package-info.java diff --git a/runtime/src/main/java/io/micronaut/discovery/package-info.java b/discovery-core/src/main/java/io/micronaut/discovery/package-info.java similarity index 100% rename from runtime/src/main/java/io/micronaut/discovery/package-info.java rename to discovery-core/src/main/java/io/micronaut/discovery/package-info.java diff --git a/runtime/src/main/java/io/micronaut/discovery/registration/AutoRegistration.java b/discovery-core/src/main/java/io/micronaut/discovery/registration/AutoRegistration.java similarity index 100% rename from runtime/src/main/java/io/micronaut/discovery/registration/AutoRegistration.java rename to discovery-core/src/main/java/io/micronaut/discovery/registration/AutoRegistration.java diff --git a/runtime/src/main/java/io/micronaut/discovery/registration/RegistrationConfiguration.java b/discovery-core/src/main/java/io/micronaut/discovery/registration/RegistrationConfiguration.java similarity index 100% rename from runtime/src/main/java/io/micronaut/discovery/registration/RegistrationConfiguration.java rename to discovery-core/src/main/java/io/micronaut/discovery/registration/RegistrationConfiguration.java diff --git a/runtime/src/main/java/io/micronaut/discovery/registration/RegistrationException.java b/discovery-core/src/main/java/io/micronaut/discovery/registration/RegistrationException.java similarity index 100% rename from runtime/src/main/java/io/micronaut/discovery/registration/RegistrationException.java rename to discovery-core/src/main/java/io/micronaut/discovery/registration/RegistrationException.java diff --git a/runtime/src/main/java/io/micronaut/discovery/registration/package-info.java b/discovery-core/src/main/java/io/micronaut/discovery/registration/package-info.java similarity index 100% rename from runtime/src/main/java/io/micronaut/discovery/registration/package-info.java rename to discovery-core/src/main/java/io/micronaut/discovery/registration/package-info.java diff --git a/runtime/src/main/java/io/micronaut/health/CurrentHealthStatus.java b/discovery-core/src/main/java/io/micronaut/health/CurrentHealthStatus.java similarity index 100% rename from runtime/src/main/java/io/micronaut/health/CurrentHealthStatus.java rename to discovery-core/src/main/java/io/micronaut/health/CurrentHealthStatus.java diff --git a/runtime/src/main/java/io/micronaut/health/DefaultCurrentHealthStatus.java b/discovery-core/src/main/java/io/micronaut/health/DefaultCurrentHealthStatus.java similarity index 100% rename from runtime/src/main/java/io/micronaut/health/DefaultCurrentHealthStatus.java rename to discovery-core/src/main/java/io/micronaut/health/DefaultCurrentHealthStatus.java diff --git a/runtime/src/main/java/io/micronaut/health/HealthStatus.java b/discovery-core/src/main/java/io/micronaut/health/HealthStatus.java similarity index 100% rename from runtime/src/main/java/io/micronaut/health/HealthStatus.java rename to discovery-core/src/main/java/io/micronaut/health/HealthStatus.java diff --git a/runtime/src/main/java/io/micronaut/health/HeartbeatConfiguration.java b/discovery-core/src/main/java/io/micronaut/health/HeartbeatConfiguration.java similarity index 100% rename from runtime/src/main/java/io/micronaut/health/HeartbeatConfiguration.java rename to discovery-core/src/main/java/io/micronaut/health/HeartbeatConfiguration.java diff --git a/runtime/src/main/java/io/micronaut/health/HeartbeatDiscoveryClientCondition.java b/discovery-core/src/main/java/io/micronaut/health/HeartbeatDiscoveryClientCondition.java similarity index 100% rename from runtime/src/main/java/io/micronaut/health/HeartbeatDiscoveryClientCondition.java rename to discovery-core/src/main/java/io/micronaut/health/HeartbeatDiscoveryClientCondition.java diff --git a/runtime/src/main/java/io/micronaut/health/HeartbeatEnabled.java b/discovery-core/src/main/java/io/micronaut/health/HeartbeatEnabled.java similarity index 100% rename from runtime/src/main/java/io/micronaut/health/HeartbeatEnabled.java rename to discovery-core/src/main/java/io/micronaut/health/HeartbeatEnabled.java diff --git a/runtime/src/main/java/io/micronaut/health/HeartbeatEvent.java b/discovery-core/src/main/java/io/micronaut/health/HeartbeatEvent.java similarity index 100% rename from runtime/src/main/java/io/micronaut/health/HeartbeatEvent.java rename to discovery-core/src/main/java/io/micronaut/health/HeartbeatEvent.java diff --git a/runtime/src/main/java/io/micronaut/health/HeartbeatTask.java b/discovery-core/src/main/java/io/micronaut/health/HeartbeatTask.java similarity index 100% rename from runtime/src/main/java/io/micronaut/health/HeartbeatTask.java rename to discovery-core/src/main/java/io/micronaut/health/HeartbeatTask.java diff --git a/runtime/src/main/java/io/micronaut/health/package-info.java b/discovery-core/src/main/java/io/micronaut/health/package-info.java similarity index 100% rename from runtime/src/main/java/io/micronaut/health/package-info.java rename to discovery-core/src/main/java/io/micronaut/health/package-info.java diff --git a/function-client/src/main/java/io/micronaut/function/client/FunctionClient.java b/function-client/src/main/java/io/micronaut/function/client/FunctionClient.java index 3a13a6092f0..c1e02e7ed6a 100644 --- a/function-client/src/main/java/io/micronaut/function/client/FunctionClient.java +++ b/function-client/src/main/java/io/micronaut/function/client/FunctionClient.java @@ -18,7 +18,6 @@ import io.micronaut.aop.Introduction; import io.micronaut.context.annotation.Type; import io.micronaut.function.client.aop.FunctionClientAdvice; -import io.micronaut.retry.annotation.Recoverable; import jakarta.inject.Singleton; import java.lang.annotation.Documented; @@ -37,7 +36,6 @@ @Retention(RUNTIME) @Singleton @Introduction -@Recoverable @Type(FunctionClientAdvice.class) public @interface FunctionClient { } diff --git a/function-web/build.gradle b/function-web/build.gradle index d1ad384bc75..f7af7696fc8 100644 --- a/function-web/build.gradle +++ b/function-web/build.gradle @@ -13,7 +13,7 @@ dependencies { testImplementation project(":http-client") testImplementation project(":inject") testImplementation project(":http-server-netty") - + testImplementation project(":jackson-databind") testAnnotationProcessor project(":inject-java") testCompileOnly project(":inject-groovy") } diff --git a/function-web/src/main/java/io/micronaut/function/web/AnnotatedFunctionRouteBuilder.java b/function-web/src/main/java/io/micronaut/function/web/AnnotatedFunctionRouteBuilder.java index 94cd450c02f..12d01d8413a 100644 --- a/function-web/src/main/java/io/micronaut/function/web/AnnotatedFunctionRouteBuilder.java +++ b/function-web/src/main/java/io/micronaut/function/web/AnnotatedFunctionRouteBuilder.java @@ -25,8 +25,6 @@ import io.micronaut.core.reflect.ClassUtils; import io.micronaut.core.type.Argument; import io.micronaut.core.util.StringUtils; -import io.micronaut.discovery.ServiceInstance; -import io.micronaut.discovery.metadata.ServiceInstanceMetadataContributor; import io.micronaut.function.DefaultLocalFunctionRegistry; import io.micronaut.function.FunctionBean; import io.micronaut.function.LocalFunctionRegistry; @@ -60,7 +58,7 @@ @Replaces(DefaultLocalFunctionRegistry.class) public class AnnotatedFunctionRouteBuilder extends DefaultRouteBuilder - implements ExecutableMethodProcessor, LocalFunctionRegistry, ServiceInstanceMetadataContributor, MediaTypeCodecRegistry { + implements ExecutableMethodProcessor, LocalFunctionRegistry, MediaTypeCodecRegistry { private final LocalFunctionRegistry localFunctionRegistry; private final String contextPath; @@ -244,14 +242,6 @@ public Optional, R>> findBiFuncti return localFunctionRegistry.findBiFunction(name); } - @Override - public void contribute(ServiceInstance instance, Map metadata) { - for (Map.Entry entry : availableFunctions.entrySet()) { - String functionName = entry.getKey(); - metadata.put(FUNCTION_PREFIX + functionName, entry.getValue().toString()); - } - } - @Override public Optional findCodec(@Nullable MediaType mediaType) { if (localFunctionRegistry instanceof MediaTypeCodecRegistry) { diff --git a/function/build.gradle b/function/build.gradle index 5416f24c4ff..7f9eca8d4e5 100644 --- a/function/build.gradle +++ b/function/build.gradle @@ -5,7 +5,7 @@ plugins { dependencies { annotationProcessor project(":inject-java") - api project(":runtime") + api project(":inject") api project(":http") testAnnotationProcessor project(":inject-java") diff --git a/http-client-core/build.gradle b/http-client-core/build.gradle index 979957602c8..a3d5d2c140c 100644 --- a/http-client-core/build.gradle +++ b/http-client-core/build.gradle @@ -7,9 +7,13 @@ dependencies { implementation libs.managed.reactor - api project(":runtime") + api project(":http") + api project(":json-core") + api project(":discovery-core") compileOnly libs.kotlin.stdlib + + testImplementation project(":jackson-databind") } //tasks.withType(Test).configureEach { diff --git a/http-client-core/src/main/java/io/micronaut/http/client/annotation/Client.java b/http-client-core/src/main/java/io/micronaut/http/client/annotation/Client.java index 5eb3cb824ac..23d8777d373 100644 --- a/http-client-core/src/main/java/io/micronaut/http/client/annotation/Client.java +++ b/http-client-core/src/main/java/io/micronaut/http/client/annotation/Client.java @@ -17,14 +17,11 @@ import io.micronaut.aop.Introduction; import io.micronaut.context.annotation.AliasFor; -import io.micronaut.context.annotation.Type; import io.micronaut.core.annotation.NonNull; import io.micronaut.http.HttpVersion; import io.micronaut.http.client.HttpClientConfiguration; import io.micronaut.http.client.HttpVersionSelection; -import io.micronaut.http.client.interceptor.HttpClientIntroductionAdvice; import io.micronaut.http.hateoas.JsonError; -import io.micronaut.retry.annotation.Recoverable; import jakarta.inject.Singleton; import java.lang.annotation.Documented; @@ -41,8 +38,6 @@ @Documented @Retention(RUNTIME) @Introduction -@Type(HttpClientIntroductionAdvice.class) -@Recoverable @Singleton // tag::value[] public @interface Client { diff --git a/http-client-core/src/main/java/io/micronaut/http/client/interceptor/HttpClientIntroductionAdvice.java b/http-client-core/src/main/java/io/micronaut/http/client/interceptor/HttpClientIntroductionAdvice.java index b6a30bcceae..6a59806a8df 100644 --- a/http-client-core/src/main/java/io/micronaut/http/client/interceptor/HttpClientIntroductionAdvice.java +++ b/http-client-core/src/main/java/io/micronaut/http/client/interceptor/HttpClientIntroductionAdvice.java @@ -16,6 +16,7 @@ package io.micronaut.http.client.interceptor; import io.micronaut.aop.InterceptedMethod; +import io.micronaut.aop.InterceptorBean; import io.micronaut.aop.MethodInterceptor; import io.micronaut.aop.MethodInvocationContext; import io.micronaut.context.annotation.BootstrapContextCompatible; @@ -55,8 +56,8 @@ import io.micronaut.http.annotation.Produces; import io.micronaut.http.client.BlockingHttpClient; import io.micronaut.http.client.HttpClient; -import io.micronaut.http.client.ReactiveClientResultTransformer; import io.micronaut.http.client.HttpClientRegistry; +import io.micronaut.http.client.ReactiveClientResultTransformer; import io.micronaut.http.client.StreamingHttpClient; import io.micronaut.http.client.annotation.Client; import io.micronaut.http.client.bind.ClientArgumentRequestBinder; @@ -68,7 +69,6 @@ import io.micronaut.http.uri.UriBuilder; import io.micronaut.http.uri.UriMatchTemplate; import io.micronaut.json.codec.JsonMediaTypeCodec; -import jakarta.inject.Singleton; import org.reactivestreams.Publisher; import org.reactivestreams.Subscription; import org.slf4j.Logger; @@ -98,7 +98,7 @@ * @author graemerocher * @since 1.0 */ -@Singleton +@InterceptorBean(Client.class) @Internal @BootstrapContextCompatible public class HttpClientIntroductionAdvice implements MethodInterceptor { diff --git a/http-client/build.gradle b/http-client/build.gradle index b41a61ba468..d5bcda0523d 100644 --- a/http-client/build.gradle +++ b/http-client/build.gradle @@ -11,7 +11,7 @@ micronautBuild { dependencies { annotationProcessor project(":inject-java") - api project(":runtime") + api project(":context") api project(":http-client-core") api project(":websocket") api project(":http-netty") @@ -26,6 +26,8 @@ dependencies { implementation libs.managed.reactor + testImplementation project(":retry") + testImplementation project(":jackson-databind") testImplementation project(":http-server-netty") testImplementation libs.wiremock testImplementation libs.managed.logback diff --git a/http-client/src/main/java/io/micronaut/http/client/netty/DefaultHttpClient.java b/http-client/src/main/java/io/micronaut/http/client/netty/DefaultHttpClient.java index f09f399e286..14b17e5f2c2 100644 --- a/http-client/src/main/java/io/micronaut/http/client/netty/DefaultHttpClient.java +++ b/http-client/src/main/java/io/micronaut/http/client/netty/DefaultHttpClient.java @@ -90,7 +90,6 @@ import io.micronaut.http.sse.Event; import io.micronaut.http.uri.UriBuilder; import io.micronaut.http.uri.UriTemplate; -import io.micronaut.jackson.databind.JacksonDatabindMapper; import io.micronaut.json.JsonMapper; import io.micronaut.json.codec.JsonMediaTypeCodec; import io.micronaut.json.codec.JsonStreamMediaTypeCodec; @@ -189,7 +188,6 @@ import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Supplier; -import java.util.stream.Collectors; import static io.micronaut.scheduling.instrument.InvocationInstrumenter.NOOP; @@ -316,7 +314,7 @@ public DefaultHttpClient(@Nullable LoadBalancer loadBalancer, * @param clientCustomizer The pipeline customizer * @param invocationInstrumenterFactories The invocation instrumeter factories to instrument netty handlers execution with * @param informationalServiceId Optional service ID that will be passed to exceptions created by this client - * @param conversionService The conversionService + * @param conversionService The conversion service */ public DefaultHttpClient(@Nullable LoadBalancer loadBalancer, @Nullable HttpVersionSelection explicitHttpVersion, @@ -1843,7 +1841,7 @@ private void traceHeaders(HttpHeaders headers) { } private static MediaTypeCodecRegistry createDefaultMediaTypeRegistry() { - JsonMapper mapper = new JacksonDatabindMapper(); + JsonMapper mapper = JsonMapper.createDefault(); ApplicationConfiguration configuration = new ApplicationConfiguration(); return MediaTypeCodecRegistry.of( new JsonMediaTypeCodec(mapper, configuration, null), @@ -1858,7 +1856,7 @@ private static MediaTypeCodecRegistry createDefaultMediaTypeRegistry() { return InvocationInstrumenter.combine(invocationInstrumenterFactories.stream() .map(InvocationInstrumenterFactory::newInvocationInstrumenter) .filter(Objects::nonNull) - .collect(Collectors.toList())); + .toList()); } static boolean isSecureScheme(String scheme) { @@ -1971,7 +1969,6 @@ private class NettyRequestWriter { private final HttpPostRequestEncoder encoder; /** - * @param scheme The scheme * @param nettyRequest The Netty request * @param encoder The encoder */ @@ -1981,8 +1978,8 @@ private class NettyRequestWriter { } /** - * @param channel The channel - * @param channelPool The channel pool + * @param poolHandle The pool handle + * @param isSecure Is the connection secure * @param emitter The emitter */ protected void write(ConnectionManager.PoolHandle poolHandle, boolean isSecure, FluxSink emitter) { diff --git a/http-client/src/test/groovy/io/micronaut/http/client/aop/BlockingFallbackSpec.groovy b/http-client/src/test/groovy/io/micronaut/http/client/aop/BlockingFallbackSpec.groovy index ca2bc200017..4e853955fa1 100644 --- a/http-client/src/test/groovy/io/micronaut/http/client/aop/BlockingFallbackSpec.groovy +++ b/http-client/src/test/groovy/io/micronaut/http/client/aop/BlockingFallbackSpec.groovy @@ -68,6 +68,7 @@ class BlockingFallbackSpec extends Specification { } @Client('/blocking/fallback/books') + @Recoverable static interface BookClient extends BookApi { @Override @Recoverable(api = BookApi) diff --git a/http-client/src/test/groovy/io/micronaut/http/client/aop/CompletableFutureFallbackSpec.groovy b/http-client/src/test/groovy/io/micronaut/http/client/aop/CompletableFutureFallbackSpec.groovy index 2a3340af261..c915c6e8a26 100644 --- a/http-client/src/test/groovy/io/micronaut/http/client/aop/CompletableFutureFallbackSpec.groovy +++ b/http-client/src/test/groovy/io/micronaut/http/client/aop/CompletableFutureFallbackSpec.groovy @@ -19,6 +19,7 @@ import io.micronaut.context.ApplicationContext import io.micronaut.http.annotation.* import io.micronaut.http.client.annotation.Client import io.micronaut.retry.annotation.Fallback +import io.micronaut.retry.annotation.Recoverable import io.micronaut.runtime.server.EmbeddedServer import spock.lang.AutoCleanup import spock.lang.Shared @@ -92,6 +93,7 @@ class CompletableFutureFallbackSpec extends Specification { @Client('/future/fallback/books') + @Recoverable static interface BookClient extends BookApi { } diff --git a/http-client/src/test/groovy/io/micronaut/http/client/aop/ReactorJavaFallbackSpec.groovy b/http-client/src/test/groovy/io/micronaut/http/client/aop/ReactorJavaFallbackSpec.groovy index 913eedba935..df16b057a1c 100644 --- a/http-client/src/test/groovy/io/micronaut/http/client/aop/ReactorJavaFallbackSpec.groovy +++ b/http-client/src/test/groovy/io/micronaut/http/client/aop/ReactorJavaFallbackSpec.groovy @@ -20,6 +20,7 @@ import io.micronaut.context.annotation.Requires import io.micronaut.http.annotation.* import io.micronaut.http.client.annotation.Client import io.micronaut.retry.annotation.Fallback +import io.micronaut.retry.annotation.Recoverable import io.micronaut.runtime.server.EmbeddedServer import org.reactivestreams.Publisher import reactor.core.publisher.Flux @@ -97,6 +98,7 @@ class ReactorJavaFallbackSpec extends Specification{ @Requires(property = 'spec.name', value = 'ReactorJavaFallbackSpec') @Client('/rxjava/fallback/books') + @Recoverable static interface BookClient extends BookApi { } diff --git a/http-client/src/test/groovy/io/micronaut/http/client/retry/HttpClientRetryWithFallbackSpec.groovy b/http-client/src/test/groovy/io/micronaut/http/client/retry/HttpClientRetryWithFallbackSpec.groovy index f537328f38e..d31333074a3 100644 --- a/http-client/src/test/groovy/io/micronaut/http/client/retry/HttpClientRetryWithFallbackSpec.groovy +++ b/http-client/src/test/groovy/io/micronaut/http/client/retry/HttpClientRetryWithFallbackSpec.groovy @@ -23,6 +23,7 @@ import io.micronaut.http.annotation.Get import io.micronaut.http.client.annotation.Client import io.micronaut.retry.annotation.Fallback import io.micronaut.retry.annotation.Retryable +import io.micronaut.retry.annotation.Recoverable import io.micronaut.runtime.server.EmbeddedServer import org.reactivestreams.Publisher import reactor.core.publisher.Mono @@ -92,6 +93,7 @@ class HttpClientRetryWithFallbackSpec extends Specification{ @Requires(property = 'spec.name', value = 'HttpClientRetryWithFallbackSpec') @Client("/retry-fallback") @Retryable(attempts = '5', delay = '5ms') + @Recoverable static interface CountClient extends CountService { } diff --git a/http-netty/build.gradle b/http-netty/build.gradle index 20006825101..4d684695ef8 100644 --- a/http-netty/build.gradle +++ b/http-netty/build.gradle @@ -8,9 +8,9 @@ dependencies { compileOnly libs.managed.graal compileOnly libs.managed.netty.transport.native.epoll compileOnly libs.managed.netty.transport.native.kqueue + compileOnly project(":websocket") api project(":http") - api project(":websocket") api project(":buffer-netty") api libs.managed.netty.codec.http @@ -20,6 +20,7 @@ dependencies { implementation libs.managed.reactor testImplementation project(":runtime") + testImplementation project(":websocket") } spotless { diff --git a/http-netty/src/main/java/io/micronaut/http/netty/graal/HttpNettyFeature.java b/http-netty/src/main/java/io/micronaut/http/netty/graal/HttpNettyFeature.java deleted file mode 100644 index 4b9cb7b37ee..00000000000 --- a/http-netty/src/main/java/io/micronaut/http/netty/graal/HttpNettyFeature.java +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright 2017-2020 original authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.micronaut.http.netty.graal; - -import com.oracle.svm.core.annotate.AutomaticFeature; -import io.micronaut.core.annotation.Internal; -import io.micronaut.http.bind.binders.ContinuationArgumentBinder; -import io.micronaut.http.netty.channel.NettyThreadFactory; -import io.micronaut.http.netty.channel.converters.EpollChannelOptionFactory; -import io.micronaut.http.netty.channel.converters.KQueueChannelOptionFactory; -import io.micronaut.http.netty.websocket.NettyWebSocketSession; -import org.graalvm.nativeimage.hosted.Feature; -import org.graalvm.nativeimage.hosted.RuntimeClassInitialization; - -/** - * An HTTP Netty feature that configures the native channels. - * - * @author Iván López - * @since 2.0.0 - */ -@Internal -@AutomaticFeature -public class HttpNettyFeature implements Feature { - - @Override - public void beforeAnalysis(BeforeAnalysisAccess access) { - RuntimeClassInitialization.initializeAtRunTime( - "io.micronaut.http.server.netty.ServerAttributeKeys", - "io.micronaut.http.server.netty.handler.accesslog.HttpAccessLogHandler", - "io.micronaut.session.http.SessionLogElement", - "io.micronaut.http.client.netty.ConnectTTLHandler", - "io.micronaut.http.client.netty.DefaultHttpClient", - "io.micronaut.http.server.netty.websocket.NettyServerWebSocketUpgradeHandler", - "io.micronaut.buffer.netty.NettyByteBufferFactory" - ); - RuntimeClassInitialization.initializeAtRunTime( - NettyWebSocketSession.class, - NettyThreadFactory.class, - EpollChannelOptionFactory.class, - KQueueChannelOptionFactory.class, - ContinuationArgumentBinder.class, - ContinuationArgumentBinder.Companion.class - ); - } - -} diff --git a/http-server-netty/build.gradle b/http-server-netty/build.gradle index 09cc6f9c129..d7236c5cd2e 100644 --- a/http-server-netty/build.gradle +++ b/http-server-netty/build.gradle @@ -25,9 +25,9 @@ dependencies { api project(":core") api project(":http-netty") api libs.managed.netty.codec.http - implementation libs.managed.reactor - + compileOnly project(":jackson-databind") + compileOnly project(":websocket") compileOnly libs.kotlin.stdlib compileOnly libs.managed.netty.transport.native.unix.common @@ -41,7 +41,7 @@ dependencies { if (JavaVersion.current().isCompatibleWith(JavaVersion.VERSION_15)) { testImplementation libs.bcpkix } - + testImplementation project(":jackson-databind") // Add Micronaut Jackson XML after v4 Migration // testImplementation(libs.managed.micronaut.xml) { // exclude module:'micronaut-inject' @@ -74,6 +74,7 @@ dependencies { // Adding these for now since micronaut-test isnt resolving correctly ... probably need to upgrade gradle there too testImplementation libs.junit.jupiter.api + testImplementation project(":websocket") } //tasks.withType(Test).configureEach { diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/DefaultNettyEmbeddedServerFactory.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/DefaultNettyEmbeddedServerFactory.java index fbf50d0fcdf..28cec74dda1 100644 --- a/http-server-netty/src/main/java/io/micronaut/http/server/netty/DefaultNettyEmbeddedServerFactory.java +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/DefaultNettyEmbeddedServerFactory.java @@ -20,6 +20,7 @@ import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Optional; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutorService; import java.util.concurrent.ThreadFactory; @@ -51,13 +52,14 @@ import io.micronaut.http.server.netty.types.DefaultCustomizableResponseTypeHandlerRegistry; import io.micronaut.http.server.netty.types.NettyCustomizableResponseTypeHandler; import io.micronaut.http.server.netty.types.files.FileTypeHandler; +import io.micronaut.http.server.netty.websocket.WebSocketUpgradeHandlerFactory; import io.micronaut.http.ssl.ServerSslConfiguration; import io.micronaut.scheduling.executor.ExecutorSelector; import io.micronaut.web.router.resource.StaticResourceResolver; -import io.micronaut.websocket.context.WebSocketBeanRegistry; import io.netty.channel.ChannelOutboundHandler; import io.netty.channel.EventLoopGroup; import io.netty.channel.ServerChannel; +import io.netty.channel.SimpleChannelInboundHandler; import io.netty.channel.socket.ServerSocketChannel; import jakarta.inject.Inject; import jakarta.inject.Named; @@ -84,10 +86,10 @@ public class DefaultNettyEmbeddedServerFactory private final ExecutorSelector executorSelector; private final ThreadFactory nettyThreadFactory; private final HttpCompressionStrategy httpCompressionStrategy; - private final WebSocketBeanRegistry websocketBeanRegistry; private final EventLoopGroupFactory eventLoopGroupFactory; private final EventLoopGroupRegistry eventLoopGroupRegistry; private final Map, ApplicationEventPublisher> cachedEventPublishers = new ConcurrentHashMap<>(5); + private final WebSocketUpgradeHandlerFactory webSocketUpgradeHandlerFactory; private @Nullable ServerSslBuilder serverSslBuilder; private @Nullable ChannelOptionFactory channelOptionFactory; private List outboundHandlers = Collections.emptyList(); @@ -102,6 +104,7 @@ public class DefaultNettyEmbeddedServerFactory * @param httpCompressionStrategy The http compression strategy * @param eventLoopGroupFactory The event loop group factory * @param eventLoopGroupRegistry The event loop group registry + * @param webSocketUpgradeHandlerFactory An optional websocket integration */ protected DefaultNettyEmbeddedServerFactory(ApplicationContext applicationContext, RouteExecutor routeExecutor, @@ -110,7 +113,8 @@ protected DefaultNettyEmbeddedServerFactory(ApplicationContext applicationContex @Named(NettyThreadFactory.NAME) ThreadFactory nettyThreadFactory, HttpCompressionStrategy httpCompressionStrategy, EventLoopGroupFactory eventLoopGroupFactory, - EventLoopGroupRegistry eventLoopGroupRegistry) { + EventLoopGroupRegistry eventLoopGroupRegistry, + @Nullable WebSocketUpgradeHandlerFactory webSocketUpgradeHandlerFactory) { this.applicationContext = applicationContext; this.requestArgumentSatisfier = routeExecutor.getRequestArgumentSatisfier(); this.routeExecutor = routeExecutor; @@ -119,9 +123,9 @@ protected DefaultNettyEmbeddedServerFactory(ApplicationContext applicationContex this.executorSelector = routeExecutor.getExecutorSelector(); this.nettyThreadFactory = nettyThreadFactory; this.httpCompressionStrategy = httpCompressionStrategy; - this.websocketBeanRegistry = WebSocketBeanRegistry.forServer(applicationContext); this.eventLoopGroupFactory = eventLoopGroupFactory; this.eventLoopGroupRegistry = eventLoopGroupRegistry; + this.webSocketUpgradeHandlerFactory = webSocketUpgradeHandlerFactory; } @Override @@ -179,16 +183,16 @@ private NettyEmbeddedServer buildInternal(@NonNull NettyHttpServerConfiguration private NettyEmbeddedServices resolveNettyEmbeddedServices(@NonNull NettyHttpServerConfiguration configuration, @Nullable ServerSslConfiguration sslConfiguration) { if (sslConfiguration != null && sslConfiguration.isEnabled()) { - ServerSslBuilder serverSslBuilder; + ServerSslBuilder resolvedSslBuilder; final ResourceResolver resourceResolver = applicationContext.getBean(ResourceResolver.class); if (sslConfiguration.buildSelfSigned()) { - serverSslBuilder = new SelfSignedSslBuilder( + resolvedSslBuilder = new SelfSignedSslBuilder( configuration, sslConfiguration, resourceResolver ); } else { - serverSslBuilder = new CertificateProvidedSslBuilder( + resolvedSslBuilder = new CertificateProvidedSslBuilder( configuration, sslConfiguration, resourceResolver @@ -202,7 +206,7 @@ public NettyEmbeddedServices getDelegate() { @Override public ServerSslBuilder getServerSslBuilder() { - return serverSslBuilder; + return resolvedSslBuilder; } }; } @@ -263,8 +267,9 @@ public HttpCompressionStrategy getHttpCompressionStrategy() { } @Override - public WebSocketBeanRegistry getWebSocketBeanRegistry() { - return this.websocketBeanRegistry; + public Optional>> getWebSocketUpgradeHandler(NettyEmbeddedServer server) { + return Optional.ofNullable(webSocketUpgradeHandlerFactory) + .map(factory -> factory.create(server, this)); } @Override diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/DelegateNettyEmbeddedServices.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/DelegateNettyEmbeddedServices.java index b9e604d8ba9..b1f932f0202 100644 --- a/http-server-netty/src/main/java/io/micronaut/http/server/netty/DelegateNettyEmbeddedServices.java +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/DelegateNettyEmbeddedServices.java @@ -16,6 +16,7 @@ package io.micronaut.http.server.netty; import java.util.List; +import java.util.Optional; import java.util.concurrent.ExecutorService; import io.micronaut.context.ApplicationContext; @@ -29,9 +30,9 @@ import io.micronaut.http.server.RouteExecutor; import io.micronaut.http.server.netty.ssl.ServerSslBuilder; import io.micronaut.web.router.resource.StaticResourceResolver; -import io.micronaut.websocket.context.WebSocketBeanRegistry; import io.netty.channel.ChannelOutboundHandler; import io.netty.channel.EventLoopGroup; +import io.netty.channel.SimpleChannelInboundHandler; import io.netty.channel.socket.ServerSocketChannel; /** @@ -89,8 +90,8 @@ default HttpCompressionStrategy getHttpCompressionStrategy() { } @Override - default WebSocketBeanRegistry getWebSocketBeanRegistry() { - return getDelegate().getWebSocketBeanRegistry(); + default Optional>> getWebSocketUpgradeHandler(NettyEmbeddedServer server) { + return getDelegate().getWebSocketUpgradeHandler(server); } @Override diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/HttpPipelineBuilder.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/HttpPipelineBuilder.java index c8c75603475..a31984132cc 100644 --- a/http-server-netty/src/main/java/io/micronaut/http/server/netty/HttpPipelineBuilder.java +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/HttpPipelineBuilder.java @@ -483,9 +483,10 @@ private void insertMicronautHandlers() { pipeline.addLast("request-certificate-handler", requestCertificateHandler); } pipeline.addLast(HttpResponseEncoder.ID, responseEncoder); - pipeline.addLast(NettyServerWebSocketUpgradeHandler.ID, new NettyServerWebSocketUpgradeHandler( - embeddedServices, - server.getWebSocketSessionRepository())); + embeddedServices.getWebSocketUpgradeHandler(server).ifPresent(websocketHandler -> + pipeline.addLast(ChannelPipelineCustomizer.HANDLER_WEBSOCKET_UPGRADE, websocketHandler) + ); + pipeline.addLast(ChannelPipelineCustomizer.HANDLER_MICRONAUT_INBOUND, routingInBoundHandler); } diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/NettyEmbeddedServices.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/NettyEmbeddedServices.java index 87f517eb67e..7657b6afb89 100644 --- a/http-server-netty/src/main/java/io/micronaut/http/server/netty/NettyEmbeddedServices.java +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/NettyEmbeddedServices.java @@ -16,6 +16,7 @@ package io.micronaut.http.server.netty; import java.util.List; +import java.util.Optional; import java.util.concurrent.ExecutorService; import io.micronaut.context.ApplicationContext; @@ -33,10 +34,10 @@ import io.micronaut.scheduling.executor.ExecutorSelector; import io.micronaut.web.router.Router; import io.micronaut.web.router.resource.StaticResourceResolver; -import io.micronaut.websocket.context.WebSocketBeanRegistry; import io.netty.channel.ChannelOutboundHandler; import io.netty.channel.EventLoopGroup; import io.netty.channel.ServerChannel; +import io.netty.channel.SimpleChannelInboundHandler; import io.netty.channel.socket.ServerSocketChannel; /** @@ -117,10 +118,11 @@ default ExecutorSelector getExecutorSelector() { HttpCompressionStrategy getHttpCompressionStrategy(); /** - * @return The websocket bean registry + * @param embeddedServer The server + * @return The websocket upgrade handler if present */ - @NonNull - WebSocketBeanRegistry getWebSocketBeanRegistry(); + @SuppressWarnings("java:S1452") + Optional>> getWebSocketUpgradeHandler(NettyEmbeddedServer embeddedServer); /** * @return The event loop group registry. diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/NettyHttpServer.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/NettyHttpServer.java index 7a31e8a0aa8..40fd6d89c92 100644 --- a/http-server-netty/src/main/java/io/micronaut/http/server/netty/NettyHttpServer.java +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/NettyHttpServer.java @@ -27,9 +27,6 @@ import io.micronaut.core.io.socket.SocketUtils; import io.micronaut.core.util.CollectionUtils; import io.micronaut.core.util.SupplierUtil; -import io.micronaut.discovery.EmbeddedServerInstance; -import io.micronaut.discovery.event.ServiceReadyEvent; -import io.micronaut.discovery.event.ServiceStoppedEvent; import io.micronaut.http.context.event.HttpRequestTerminatedEvent; import io.micronaut.http.netty.channel.ChannelPipelineListener; import io.micronaut.http.netty.channel.DefaultEventLoopGroupConfiguration; @@ -127,7 +124,6 @@ public class NettyHttpServer implements NettyEmbeddedServer { private boolean shutdownParent = false; private EventLoopGroup workerGroup; private EventLoopGroup parentGroup; - private EmbeddedServerInstance serviceInstance; private final Collection pipelineListeners = new ArrayList<>(2); @Nullable private volatile List activeListeners = null; @@ -293,7 +289,7 @@ public synchronized NettyEmbeddedServer start() { .map(l -> l.serverChannel.localAddress()) .filter(InetSocketAddress.class::isInstance) .map(addr -> ((InetSocketAddress) addr).getPort()) - .collect(Collectors.toList())); + .toList()); } } fireStartupEvents(); @@ -579,14 +575,6 @@ private void fireStartupEvents() { Optional applicationName = serverConfiguration.getApplicationConfiguration().getName(); applicationContext.getEventPublisher(ServerStartupEvent.class) .publishEvent(new ServerStartupEvent(this)); - applicationName.ifPresent(id -> { - if (serviceInstance == null) { - serviceInstance = applicationContext.createBean(NettyEmbeddedServerInstance.class, id, this); - } - applicationContext - .getEventPublisher(ServiceReadyEvent.class) - .publishEvent(new ServiceReadyEvent(serviceInstance)); - }); } private void logShutdownErrorIfNecessary(Future future) { @@ -618,10 +606,6 @@ private void stopInternal(boolean stopApplicationContext) { } webSocketSessions.close(); applicationContext.getEventPublisher(ServerShutdownEvent.class).publishEvent(new ServerShutdownEvent(this)); - if (serviceInstance != null) { - applicationContext.getEventPublisher(ServiceStoppedEvent.class) - .publishEvent(new ServiceStoppedEvent(serviceInstance)); - } if (isDefault && applicationContext.isRunning() && stopApplicationContext) { applicationContext.stop(); } diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/RoutingInBoundHandler.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/RoutingInBoundHandler.java index 42871f0a081..2489f19d62e 100644 --- a/http-server-netty/src/main/java/io/micronaut/http/server/netty/RoutingInBoundHandler.java +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/RoutingInBoundHandler.java @@ -49,6 +49,7 @@ import io.micronaut.http.netty.NettyMutableHttpResponse; import io.micronaut.http.netty.stream.JsonSubscriber; import io.micronaut.http.netty.stream.StreamedHttpRequest; +import io.micronaut.http.reactive.execution.ReactiveExecutionFlow; import io.micronaut.http.server.RouteExecutor; import io.micronaut.http.server.binding.RequestArgumentSatisfier; import io.micronaut.http.server.exceptions.InternalServerException; @@ -61,11 +62,9 @@ import io.micronaut.http.server.netty.types.files.NettyStreamedFileCustomizableResponseType; import io.micronaut.http.server.netty.types.files.NettySystemFileCustomizableResponseType; import io.micronaut.http.server.types.files.FileCustomizableResponseType; -import io.micronaut.reactive.reactor.execution.ReactiveExecutionFlow; import io.micronaut.runtime.http.codec.TextPlainCodec; import io.micronaut.web.router.RouteInfo; import io.micronaut.web.router.RouteMatch; -import io.micronaut.web.router.Router; import io.micronaut.web.router.resource.StaticResourceResolver; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufHolder; @@ -137,7 +136,7 @@ @Internal @Sharable @SuppressWarnings("FileLength") -class RoutingInBoundHandler extends SimpleChannelInboundHandler> implements RouteExecutor.StaticResourceResponseFinder { +final class RoutingInBoundHandler extends SimpleChannelInboundHandler> implements RouteExecutor.StaticResourceResponseFinder { private static final Logger LOG = LoggerFactory.getLogger(RoutingInBoundHandler.class); /* @@ -145,8 +144,7 @@ class RoutingInBoundHandler extends SimpleChannelInboundHandler ARGUMENT_PART_DATA = Argument.of(PartData.class); private final StaticResourceResolver staticResourceResolver; private final NettyHttpServerConfiguration serverConfiguration; private final HttpContentProcessorResolver httpContentProcessorResolver; @@ -181,13 +179,12 @@ class RoutingInBoundHandler extends SimpleChannelInboundHandler multipartEnabled = serverConfiguration.getMultipart().getEnabled(); - this.multipartEnabled = !multipartEnabled.isPresent() || multipartEnabled.get(); + Optional isMultiPartEnabled = serverConfiguration.getMultipart().getEnabled(); + this.multipartEnabled = isMultiPartEnabled.isEmpty() || isMultiPartEnabled.get(); this.routeExecutor = embeddedServerContext.getRouteExecutor(); this.conversionService = conversionService; } @@ -199,7 +196,7 @@ public void handlerRemoved(ChannelHandlerContext ctx) throws Exception { } @Override - public void channelInactive(ChannelHandlerContext ctx) throws Exception { + public void channelInactive(@NonNull ChannelHandlerContext ctx) throws Exception { super.channelInactive(ctx); if (ctx.channel().isWritable()) { ctx.flush(); @@ -211,7 +208,7 @@ private void cleanupIfNecessary(ChannelHandlerContext ctx) { NettyHttpRequest.remove(ctx); } - private void cleanupRequest(ChannelHandlerContext ctx, NettyHttpRequest request) { + private void cleanupRequest(ChannelHandlerContext ctx, NettyHttpRequest request) { try { request.release(); } finally { @@ -232,8 +229,7 @@ private void cleanupRequest(ChannelHandlerContext ctx, NettyHttpRequest request) @Override public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception { try { - if (evt instanceof IdleStateEvent) { - IdleStateEvent idleStateEvent = (IdleStateEvent) evt; + if (evt instanceof IdleStateEvent idleStateEvent) { IdleState state = idleStateEvent.state(); if (state == IdleState.ALL_IDLE) { ctx.close(); @@ -454,7 +450,7 @@ private void doOnNext0(Object message) { return; } - boolean executed = this.executed.get(); + boolean wasExecuted = this.executed.get(); if (message instanceof ByteBufHolder) { if (message instanceof HttpData data) { @@ -589,7 +585,7 @@ private void doOnNext0(Object message) { } } - if (!executed) { + if (!wasExecuted) { String argumentName = argument.getName(); if (!routeMatch.isSatisfied(argumentName)) { Object fulfillParamter = value.get(); @@ -607,7 +603,7 @@ private void doOnNext0(Object message) { } if (routeMatch.isExecutable() || message instanceof LastHttpContent) { executeRoute(); - executed = true; + wasExecuted = true; } } @@ -615,7 +611,7 @@ private void doOnNext0(Object message) { request.addContent(data); } - if (!executed || !chunkedProcessing) { + if (!wasExecuted || !chunkedProcessing) { s.request(1); } @@ -818,20 +814,19 @@ private Flux mapToHttpContent(NettyHttpRequest request, MediaType finalMediaType = mediaType; Flux httpContentPublisher = bodyPublisher.map(message -> { HttpContent httpContent; - if (message instanceof ByteBuf) { - httpContent = new DefaultHttpContent((ByteBuf) message); - } else if (message instanceof ByteBuffer) { - ByteBuffer byteBuffer = (ByteBuffer) message; + if (message instanceof ByteBuf bb) { + httpContent = new DefaultHttpContent(bb); + } else if (message instanceof ByteBuffer byteBuffer) { Object nativeBuffer = byteBuffer.asNativeBuffer(); - if (nativeBuffer instanceof ByteBuf) { - httpContent = new DefaultHttpContent((ByteBuf) nativeBuffer); + if (nativeBuffer instanceof ByteBuf bb) { + httpContent = new DefaultHttpContent(bb); } else { httpContent = new DefaultHttpContent(Unpooled.copiedBuffer(byteBuffer.asNioBuffer())); } - } else if (message instanceof byte[]) { - httpContent = new DefaultHttpContent(Unpooled.copiedBuffer((byte[]) message)); - } else if (message instanceof HttpContent) { - httpContent = (HttpContent) message; + } else if (message instanceof byte[] bytes) { + httpContent = new DefaultHttpContent(Unpooled.copiedBuffer(bytes)); + } else if (message instanceof HttpContent hc) { + httpContent = hc; } else { MediaTypeCodec codec = mediaTypeCodecRegistry.findCodec(finalMediaType, message.getClass()).orElse( @@ -914,20 +909,19 @@ private void encodeResponseBody( if (body instanceof CharSequence) { ByteBuf byteBuf = Unpooled.wrappedBuffer(body.toString().getBytes(message.getCharacterEncoding())); setResponseBody(message, byteBuf); - } else if (body instanceof byte[]) { - ByteBuf byteBuf = Unpooled.wrappedBuffer((byte[]) body); + } else if (body instanceof byte[] bytes) { + ByteBuf byteBuf = Unpooled.wrappedBuffer(bytes); setResponseBody(message, byteBuf); - } else if (body instanceof ByteBuffer) { - ByteBuffer byteBuffer = (ByteBuffer) body; + } else if (body instanceof ByteBuffer byteBuffer) { Object nativeBuffer = byteBuffer.asNativeBuffer(); - if (nativeBuffer instanceof ByteBuf) { - setResponseBody(message, (ByteBuf) nativeBuffer); - } else if (nativeBuffer instanceof java.nio.ByteBuffer) { - ByteBuf byteBuf = Unpooled.wrappedBuffer((java.nio.ByteBuffer) nativeBuffer); + if (nativeBuffer instanceof ByteBuf bb) { + setResponseBody(message, bb); + } else if (nativeBuffer instanceof java.nio.ByteBuffer nbb) { + ByteBuf byteBuf = Unpooled.wrappedBuffer(nbb); setResponseBody(message, byteBuf); } - } else if (body instanceof ByteBuf) { - setResponseBody(message, (ByteBuf) body); + } else if (body instanceof ByteBuf bb) { + setResponseBody(message, bb); } else { Optional registeredCodec = mediaTypeCodecRegistry.findCodec(mediaType, body.getClass()); if (registeredCodec.isPresent()) { @@ -956,8 +950,7 @@ private void writeFinalNettyResponse(MutableHttpResponse message, HttpRequest if (!future.isSuccess()) { final Throwable throwable = future.cause(); if (!isIgnorable(throwable)) { - if (throwable instanceof Http2Exception.StreamException) { - Http2Exception.StreamException se = (Http2Exception.StreamException) throwable; + if (throwable instanceof Http2Exception.StreamException se) { if (se.error() == Http2Error.STREAM_CLOSED) { // ignore return; @@ -1072,22 +1065,13 @@ private void syncWriteAndFlushNettyResponse( @NonNull private io.netty.handler.codec.http.HttpResponse toNettyResponse(HttpResponse message) { - if (message instanceof NettyHttpResponseBuilder) { - return ((NettyHttpResponseBuilder) message).toHttpResponse(); + if (message instanceof NettyHttpResponseBuilder builder) { + return builder.toHttpResponse(); } else { return createNettyResponse(message).toHttpResponse(); } } - @NonNull - private MutableHttpResponse toMutableResponse(HttpResponse message) { - if (message instanceof MutableHttpResponse) { - return (MutableHttpResponse) message; - } else { - return createNettyResponse(message); - } - } - @NonNull private NettyMutableHttpResponse createNettyResponse(HttpResponse message) { Object body = message.body(); @@ -1139,8 +1123,8 @@ private ByteBuf encodeBodyAsByteBuf( ChannelHandlerContext context, HttpRequest request) { ByteBuf byteBuf; - if (body instanceof ByteBuf) { - byteBuf = (ByteBuf) body; + if (body instanceof ByteBuf bb) { + byteBuf = bb; } else if (body instanceof ByteBuffer) { ByteBuffer byteBuffer = (ByteBuffer) body; Object nativeBuffer = byteBuffer.asNativeBuffer(); @@ -1149,8 +1133,8 @@ private ByteBuf encodeBodyAsByteBuf( } else { byteBuf = Unpooled.wrappedBuffer(byteBuffer.asNioBuffer()); } - } else if (body instanceof byte[]) { - byteBuf = Unpooled.wrappedBuffer((byte[]) body); + } else if (body instanceof byte[] bytes) { + byteBuf = Unpooled.wrappedBuffer(bytes); } else if (body instanceof Writable) { byteBuf = context.alloc().ioBuffer(128); @@ -1189,7 +1173,7 @@ private ByteBuf encodeBodyAsByteBuf( * @param cause The cause * @return True if it can be ignored. */ - final boolean isIgnorable(Throwable cause) { + boolean isIgnorable(Throwable cause) { if (cause instanceof ClosedChannelException || cause.getCause() instanceof ClosedChannelException) { return true; } diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/NettyEmbeddedServerInstance.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/discovery/NettyEmbeddedServerInstance.java similarity index 97% rename from http-server-netty/src/main/java/io/micronaut/http/server/netty/NettyEmbeddedServerInstance.java rename to http-server-netty/src/main/java/io/micronaut/http/server/netty/discovery/NettyEmbeddedServerInstance.java index 66b2963789e..3651fa1864f 100644 --- a/http-server-netty/src/main/java/io/micronaut/http/server/netty/NettyEmbeddedServerInstance.java +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/discovery/NettyEmbeddedServerInstance.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.micronaut.http.server.netty; +package io.micronaut.http.server.netty.discovery; import io.micronaut.context.BeanLocator; import io.micronaut.context.annotation.Parameter; @@ -27,6 +27,7 @@ import io.micronaut.discovery.cloud.ComputeInstanceMetadata; import io.micronaut.discovery.cloud.ComputeInstanceMetadataResolver; import io.micronaut.discovery.metadata.ServiceInstanceMetadataContributor; +import io.micronaut.http.server.netty.NettyHttpServer; import io.micronaut.runtime.server.EmbeddedServer; import java.net.URI; diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/discovery/NettyServiceDiscovery.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/discovery/NettyServiceDiscovery.java new file mode 100644 index 00000000000..e8f9ebe07cb --- /dev/null +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/discovery/NettyServiceDiscovery.java @@ -0,0 +1,70 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.http.server.netty.discovery; + +import io.micronaut.context.ApplicationContext; +import io.micronaut.context.annotation.Requires; +import io.micronaut.context.event.BeanCreatedEvent; +import io.micronaut.context.event.BeanCreatedEventListener; +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.order.Ordered; +import io.micronaut.discovery.ServiceInstance; +import io.micronaut.discovery.event.ServiceReadyEvent; +import io.micronaut.discovery.event.ServiceStoppedEvent; +import io.micronaut.http.server.netty.NettyEmbeddedServer; +import io.micronaut.runtime.event.annotation.EventListener; +import io.micronaut.runtime.server.event.ServerShutdownEvent; +import io.micronaut.runtime.server.event.ServerStartupEvent; +import jakarta.inject.Singleton; + +@Singleton +@Internal +@Requires(classes = ServiceInstance.class) +final class NettyServiceDiscovery implements BeanCreatedEventListener, Ordered { + private NettyEmbeddedServer server; + private NettyEmbeddedServerInstance instance; + + @Override + public int getOrder() { + return Ordered.LOWEST_PRECEDENCE; + } + + @EventListener + void onStart(ServerStartupEvent event) { + if (instance != null) { + server.getApplicationContext() + .getEventPublisher(ServiceReadyEvent.class) + .publishEvent(new ServiceReadyEvent(instance)); + } + } + + @EventListener + void onStop(ServerShutdownEvent event) { + if (instance != null) { + server.getApplicationContext().getEventPublisher(ServiceStoppedEvent.class) + .publishEvent(new ServiceStoppedEvent(instance)); + } + } + + @Override + public NettyEmbeddedServer onCreated(BeanCreatedEvent event) { + this.server = event.getBean(); + ApplicationContext applicationContext = server.getApplicationContext(); + server.getApplicationConfiguration().getName() + .ifPresent(id -> this.instance = applicationContext.createBean(NettyEmbeddedServerInstance.class, id, server)); + return server; + } +} diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/websocket/NettyServerWebSocketUpgradeHandler.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/websocket/NettyServerWebSocketUpgradeHandler.java index cd4d283e217..80a5d277ace 100644 --- a/http-server-netty/src/main/java/io/micronaut/http/server/netty/websocket/NettyServerWebSocketUpgradeHandler.java +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/websocket/NettyServerWebSocketUpgradeHandler.java @@ -26,8 +26,6 @@ import io.micronaut.http.HttpStatus; import io.micronaut.http.MutableHttpHeaders; import io.micronaut.http.MutableHttpResponse; -import io.micronaut.http.bind.RequestBinderRegistry; -import io.micronaut.http.codec.MediaTypeCodecRegistry; import io.micronaut.http.context.ServerRequestContext; import io.micronaut.http.exceptions.HttpStatusException; import io.micronaut.http.netty.NettyHttpHeaders; @@ -86,9 +84,7 @@ public class NettyServerWebSocketUpgradeHandler extends SimpleChannelInboundHand private static final AsciiString WEB_SOCKET_HEADER_VALUE = AsciiString.cached("websocket"); private final Router router; - private final RequestBinderRegistry binderRegistry; private final WebSocketBeanRegistry webSocketBeanRegistry; - private final MediaTypeCodecRegistry mediaTypeCodecRegistry; private final WebSocketSessionRepository webSocketSessionRepository; private final RouteExecutor routeExecutor; private final NettyEmbeddedServices nettyEmbeddedServices; @@ -104,9 +100,7 @@ public class NettyServerWebSocketUpgradeHandler extends SimpleChannelInboundHand public NettyServerWebSocketUpgradeHandler(NettyEmbeddedServices embeddedServices, WebSocketSessionRepository webSocketSessionRepository) { this.router = embeddedServices.getRouter(); - this.binderRegistry = embeddedServices.getRequestArgumentSatisfier().getBinderRegistry(); - this.webSocketBeanRegistry = embeddedServices.getWebSocketBeanRegistry(); - this.mediaTypeCodecRegistry = embeddedServices.getMediaTypeCodecRegistry(); + this.webSocketBeanRegistry = WebSocketBeanRegistry.forServer(embeddedServices.getApplicationContext()); this.webSocketSessionRepository = webSocketSessionRepository; this.routeExecutor = embeddedServices.getRouteExecutor(); this.nettyEmbeddedServices = embeddedServices; diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/websocket/WebSocketUpgradeHandlerFactory.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/websocket/WebSocketUpgradeHandlerFactory.java new file mode 100644 index 00000000000..89dc3d992f6 --- /dev/null +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/websocket/WebSocketUpgradeHandlerFactory.java @@ -0,0 +1,46 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.http.server.netty.websocket; + +import io.micronaut.context.annotation.Requires; +import io.micronaut.core.annotation.Internal; +import io.micronaut.http.server.netty.NettyEmbeddedServer; +import io.micronaut.http.server.netty.NettyEmbeddedServices; +import io.micronaut.http.server.netty.NettyHttpRequest; +import io.micronaut.websocket.context.WebSocketBeanRegistry; +import io.netty.channel.SimpleChannelInboundHandler; +import jakarta.inject.Singleton; + +/** + * Creates the inbound handler for websocket upgrade requests. + * + * @author graemerocher + * @since 4.0.0 + */ +@Requires(classes = WebSocketBeanRegistry.class) +@Singleton +@Internal +public final class WebSocketUpgradeHandlerFactory { + /** + * Creates the websocket upgrade inbound handler. + * @param embeddedServer The server + * @param nettyEmbeddedServices The services + * @return The handler + */ + public SimpleChannelInboundHandler> create(NettyEmbeddedServer embeddedServer, NettyEmbeddedServices nettyEmbeddedServices) { + return new NettyServerWebSocketUpgradeHandler(nettyEmbeddedServices, embeddedServer); + } +} diff --git a/http-server/build.gradle b/http-server/build.gradle index 26e9b8486aa..3103584d3ca 100644 --- a/http-server/build.gradle +++ b/http-server/build.gradle @@ -3,9 +3,11 @@ plugins { } dependencies { - api project(":websocket") - api project(":runtime") + api project(":http") api project(":router") + + compileOnly project(":websocket") + compileOnly project(":jackson-databind") compileOnly libs.kotlinx.coroutines.core compileOnly libs.kotlinx.coroutines.reactor compileOnly(libs.micronaut.runtime.groovy) diff --git a/http-server/src/main/java/io/micronaut/http/server/RouteExecutor.java b/http-server/src/main/java/io/micronaut/http/server/RouteExecutor.java index 846498b9a7c..12703168faa 100644 --- a/http-server/src/main/java/io/micronaut/http/server/RouteExecutor.java +++ b/http-server/src/main/java/io/micronaut/http/server/RouteExecutor.java @@ -20,7 +20,7 @@ import io.micronaut.core.annotation.Internal; import io.micronaut.core.annotation.NonNull; import io.micronaut.core.annotation.Nullable; -import io.micronaut.reactive.reactor.execution.ReactiveExecutionFlow; +import io.micronaut.http.reactive.execution.ReactiveExecutionFlow; import io.micronaut.core.async.publisher.Publishers; import io.micronaut.core.execution.ExecutionFlow; import io.micronaut.core.io.buffer.ReferenceCounted; diff --git a/http-server/src/main/java/io/micronaut/http/server/websocket/ServerWebSocketProcessor.java b/http-server/src/main/java/io/micronaut/http/server/websocket/ServerWebSocketProcessor.java index 913fc9c1182..3ade351c7dd 100644 --- a/http-server/src/main/java/io/micronaut/http/server/websocket/ServerWebSocketProcessor.java +++ b/http-server/src/main/java/io/micronaut/http/server/websocket/ServerWebSocketProcessor.java @@ -16,6 +16,7 @@ package io.micronaut.http.server.websocket; import io.micronaut.context.ExecutionHandleLocator; +import io.micronaut.context.annotation.Requires; import io.micronaut.context.processor.ExecutableMethodProcessor; import io.micronaut.core.annotation.Internal; import io.micronaut.core.convert.ConversionService; @@ -26,6 +27,7 @@ import io.micronaut.websocket.annotation.OnMessage; import io.micronaut.websocket.annotation.OnOpen; import io.micronaut.websocket.annotation.ServerWebSocket; +import io.micronaut.websocket.context.WebSocketBeanRegistry; import jakarta.inject.Singleton; import java.util.HashSet; @@ -39,6 +41,7 @@ */ @Singleton @Internal +@Requires(classes = {ServerWebSocket.class, WebSocketBeanRegistry.class}) public class ServerWebSocketProcessor extends DefaultRouteBuilder implements ExecutableMethodProcessor { private Set mappedWebSockets = new HashSet<>(4); @@ -47,8 +50,8 @@ public class ServerWebSocketProcessor extends DefaultRouteBuilder implements Exe * Default constructor. * * @param executionHandleLocator The {@link ExecutionHandleLocator} - * @param uriNamingStrategy The {@link io.micronaut.web.router.RouteBuilder.UriNamingStrategy} - * @param conversionService The {@link ConversionService} + * @param uriNamingStrategy The {@link io.micronaut.web.router.RouteBuilder.UriNamingStrategy} + * @param conversionService The {@link ConversionService} */ ServerWebSocketProcessor(ExecutionHandleLocator executionHandleLocator, UriNamingStrategy uriNamingStrategy, ConversionService conversionService) { super(executionHandleLocator, uriNamingStrategy, conversionService); diff --git a/http/build.gradle b/http/build.gradle index 6e41cd391d2..1908fb32a8e 100644 --- a/http/build.gradle +++ b/http/build.gradle @@ -6,7 +6,7 @@ plugins { dependencies { annotationProcessor project(":inject-java") annotationProcessor project(":graal") - api project(":inject") + api project(":context") api project(":core-reactive") implementation libs.managed.reactor compileOnly libs.kotlinx.coroutines.core @@ -16,6 +16,7 @@ dependencies { testCompileOnly project(":inject-groovy") testAnnotationProcessor project(":inject-java") + testImplementation project(":jackson-databind") testImplementation project(":inject") testImplementation project(":runtime") } diff --git a/runtime/src/main/java/io/micronaut/reactive/reactor/execution/ReactiveExecutionFlow.java b/http/src/main/java/io/micronaut/http/reactive/execution/ReactiveExecutionFlow.java similarity index 96% rename from runtime/src/main/java/io/micronaut/reactive/reactor/execution/ReactiveExecutionFlow.java rename to http/src/main/java/io/micronaut/http/reactive/execution/ReactiveExecutionFlow.java index 31845e76bd2..5389eae5109 100644 --- a/runtime/src/main/java/io/micronaut/reactive/reactor/execution/ReactiveExecutionFlow.java +++ b/http/src/main/java/io/micronaut/http/reactive/execution/ReactiveExecutionFlow.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.micronaut.reactive.reactor.execution; +package io.micronaut.http.reactive.execution; import io.micronaut.core.annotation.Internal; import io.micronaut.core.annotation.NonNull; @@ -61,7 +61,7 @@ static ReactiveExecutionFlow fromPublisher(@NonNull Publisher publishe static ReactiveExecutionFlow async(@NonNull Executor executor, @NonNull Supplier> supplier) { Scheduler scheduler = Schedulers.fromExecutor(executor); return (ReactiveExecutionFlow) new ReactorExecutionFlowImpl( - Mono.fromSupplier(supplier).flatMap(ReactorExecutionFlowImpl::toMono).subscribeOn(scheduler).subscribeOn(scheduler) + Mono.fromSupplier(supplier).flatMap(ReactorExecutionFlowImpl::toMono).subscribeOn(scheduler) ); } diff --git a/runtime/src/main/java/io/micronaut/reactive/reactor/execution/ReactorExecutionFlowImpl.java b/http/src/main/java/io/micronaut/http/reactive/execution/ReactorExecutionFlowImpl.java similarity index 99% rename from runtime/src/main/java/io/micronaut/reactive/reactor/execution/ReactorExecutionFlowImpl.java rename to http/src/main/java/io/micronaut/http/reactive/execution/ReactorExecutionFlowImpl.java index c296d71983b..54a4b1b1114 100644 --- a/runtime/src/main/java/io/micronaut/reactive/reactor/execution/ReactorExecutionFlowImpl.java +++ b/http/src/main/java/io/micronaut/http/reactive/execution/ReactorExecutionFlowImpl.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.micronaut.reactive.reactor.execution; +package io.micronaut.http.reactive.execution; import io.micronaut.core.annotation.Internal; import io.micronaut.core.execution.CompletableFutureExecutionFlow; diff --git a/runtime/src/main/java/io/micronaut/runtime/http/codec/MediaTypeCodecRegistryFactory.java b/http/src/main/java/io/micronaut/runtime/http/codec/MediaTypeCodecRegistryFactory.java similarity index 100% rename from runtime/src/main/java/io/micronaut/runtime/http/codec/MediaTypeCodecRegistryFactory.java rename to http/src/main/java/io/micronaut/runtime/http/codec/MediaTypeCodecRegistryFactory.java diff --git a/runtime/src/main/java/io/micronaut/runtime/http/codec/TextPlainCodec.java b/http/src/main/java/io/micronaut/runtime/http/codec/TextPlainCodec.java similarity index 100% rename from runtime/src/main/java/io/micronaut/runtime/http/codec/TextPlainCodec.java rename to http/src/main/java/io/micronaut/runtime/http/codec/TextPlainCodec.java diff --git a/runtime/src/main/java/io/micronaut/runtime/http/codec/package-info.java b/http/src/main/java/io/micronaut/runtime/http/codec/package-info.java similarity index 100% rename from runtime/src/main/java/io/micronaut/runtime/http/codec/package-info.java rename to http/src/main/java/io/micronaut/runtime/http/codec/package-info.java diff --git a/runtime/src/main/java/io/micronaut/runtime/http/scope/RequestAware.java b/http/src/main/java/io/micronaut/runtime/http/scope/RequestAware.java similarity index 100% rename from runtime/src/main/java/io/micronaut/runtime/http/scope/RequestAware.java rename to http/src/main/java/io/micronaut/runtime/http/scope/RequestAware.java diff --git a/runtime/src/main/java/io/micronaut/runtime/http/scope/RequestCustomScope.java b/http/src/main/java/io/micronaut/runtime/http/scope/RequestCustomScope.java similarity index 100% rename from runtime/src/main/java/io/micronaut/runtime/http/scope/RequestCustomScope.java rename to http/src/main/java/io/micronaut/runtime/http/scope/RequestCustomScope.java diff --git a/runtime/src/main/java/io/micronaut/runtime/http/scope/RequestScope.java b/http/src/main/java/io/micronaut/runtime/http/scope/RequestScope.java similarity index 100% rename from runtime/src/main/java/io/micronaut/runtime/http/scope/RequestScope.java rename to http/src/main/java/io/micronaut/runtime/http/scope/RequestScope.java diff --git a/inject-groovy/build.gradle b/inject-groovy/build.gradle index f979414609f..586e1f24063 100644 --- a/inject-groovy/build.gradle +++ b/inject-groovy/build.gradle @@ -22,6 +22,7 @@ dependencies { testRuntimeOnly libs.javax.el testImplementation project(":http-server-netty") testImplementation project(":http-client") + testImplementation project(":jackson-databind") testImplementation project(":inject-test-utils") testImplementation project(":inject-groovy-test") testImplementation project(":validation") diff --git a/inject-java-test/build.gradle b/inject-java-test/build.gradle index 63a7dbdad7a..5d3386a0a46 100644 --- a/inject-java-test/build.gradle +++ b/inject-java-test/build.gradle @@ -21,6 +21,7 @@ dependencies { testAnnotationProcessor project(":inject-java") testCompileOnly project(":inject-groovy") + testImplementation project(":jackson-databind") testImplementation libs.managed.validation testImplementation libs.javax.persistence testImplementation project(":runtime") diff --git a/inject-java/build.gradle b/inject-java/build.gradle index 6d2e166435b..b414d9d3fee 100644 --- a/inject-java/build.gradle +++ b/inject-java/build.gradle @@ -39,6 +39,7 @@ dependencies { } testImplementation libs.managed.micrometer.core testImplementation project(":validation") + testImplementation project(":jackson-databind") testImplementation libs.junit.jupiter.api testImplementation(platform(libs.boms.micronaut.tracing)) testImplementation(libs.micronaut.tracing.zipkin) { diff --git a/inject/src/main/java/io/micronaut/context/AbstractBeanContextConditional.java b/inject/src/main/java/io/micronaut/context/AbstractBeanContextConditional.java index d47f86c4d71..5f77fe913a1 100644 --- a/inject/src/main/java/io/micronaut/context/AbstractBeanContextConditional.java +++ b/inject/src/main/java/io/micronaut/context/AbstractBeanContextConditional.java @@ -38,8 +38,6 @@ @Internal abstract class AbstractBeanContextConditional implements BeanContextConditional, AnnotationMetadataProvider { - static final Logger LOG = LoggerFactory.getLogger(Condition.class); - @Override public boolean isEnabled(@NonNull BeanContext context, @Nullable BeanResolutionContext resolutionContext) { AnnotationMetadata annotationMetadata = getAnnotationMetadata(); @@ -48,17 +46,25 @@ public boolean isEnabled(@NonNull BeanContext context, @Nullable BeanResolutionC (DefaultBeanContext) context, this, resolutionContext); boolean enabled = condition == null || condition.matches(conditionContext); - if (LOG.isDebugEnabled() && !enabled) { + if (ConditionLog.LOG.isDebugEnabled() && !enabled) { if (this instanceof BeanConfiguration) { - LOG.debug(this + " will not be loaded due to failing conditions:"); + ConditionLog.LOG.debug("{} will not be loaded due to failing conditions:", this); } else { - LOG.debug("Bean [" + this + "] will not be loaded due to failing conditions:"); + ConditionLog.LOG.debug("Bean [{}] will not be loaded due to failing conditions:", this); } for (Failure failure : conditionContext.getFailures()) { - LOG.debug("* {}", failure.getMessage()); + ConditionLog.LOG.debug("* {}", failure.getMessage()); } } return enabled; } + + @SuppressWarnings("java:S3416") + static final class ConditionLog { + static final Logger LOG = LoggerFactory.getLogger(Condition.class); + + private ConditionLog() { + } + } } diff --git a/inject/src/main/java/io/micronaut/context/AbstractBeanDefinition.java b/inject/src/main/java/io/micronaut/context/AbstractBeanDefinition.java index 394fe041b9e..d680d1273d6 100644 --- a/inject/src/main/java/io/micronaut/context/AbstractBeanDefinition.java +++ b/inject/src/main/java/io/micronaut/context/AbstractBeanDefinition.java @@ -1059,8 +1059,8 @@ protected final Object getBeanForConstructorArgument(BeanResolutionContext resol path.pop(); return bean; } catch (DisabledBeanException e) { - if (AbstractBeanContextConditional.LOG.isDebugEnabled()) { - AbstractBeanContextConditional.LOG.debug("Bean of type [{}] disabled for reason: {}", argument.getTypeName(), e.getMessage()); + if (ConditionLog.LOG.isDebugEnabled()) { + ConditionLog.LOG.debug("Bean of type [{}] disabled for reason: {}", argument.getTypeName(), e.getMessage()); } if (isIterable() && getAnnotationMetadata().hasDeclaredAnnotation(EachBean.class)) { throw new DisabledBeanException("Bean [" + getBeanType().getSimpleName() + "] disabled by parent: " + e.getMessage()); @@ -1719,8 +1719,8 @@ protected final Object getBeanForField(BeanResolutionContext resolutionContext, path.pop(); return bean; } catch (DisabledBeanException e) { - if (AbstractBeanContextConditional.LOG.isDebugEnabled()) { - AbstractBeanContextConditional.LOG.debug("Bean of type [{}] disabled for reason: {}", argument.getTypeName(), e.getMessage()); + if (ConditionLog.LOG.isDebugEnabled()) { + ConditionLog.LOG.debug("Bean of type [{}] disabled for reason: {}", argument.getTypeName(), e.getMessage()); } if (isIterable() && getAnnotationMetadata().hasDeclaredAnnotation(EachBean.class)) { throw new DisabledBeanException("Bean [" + getBeanType().getSimpleName() + "] disabled by parent: " + e.getMessage()); @@ -1896,8 +1896,8 @@ private Object getBeanForMethodArgument(BeanResolutionContext resolutionContext, path.pop(); return bean; } catch (DisabledBeanException e) { - if (AbstractBeanContextConditional.LOG.isDebugEnabled()) { - AbstractBeanContextConditional.LOG.debug("Bean of type [{}] disabled for reason: {}", argumentType.getSimpleName(), e.getMessage()); + if (ConditionLog.LOG.isDebugEnabled()) { + ConditionLog.LOG.debug("Bean of type [{}] disabled for reason: {}", argumentType.getSimpleName(), e.getMessage()); } if (isIterable() && getAnnotationMetadata().hasDeclaredAnnotation(EachBean.class)) { throw new DisabledBeanException("Bean [" + getBeanType().getSimpleName() + "] disabled by parent: " + e.getMessage()); diff --git a/inject/src/main/java/io/micronaut/context/AbstractInitializableBeanDefinition.java b/inject/src/main/java/io/micronaut/context/AbstractInitializableBeanDefinition.java index d8b35c213cd..0c480007644 100644 --- a/inject/src/main/java/io/micronaut/context/AbstractInitializableBeanDefinition.java +++ b/inject/src/main/java/io/micronaut/context/AbstractInitializableBeanDefinition.java @@ -88,13 +88,12 @@ *

For technical reasons the class has to be marked as public, but is regarded as internal and should be used by * compiler tools and plugins (such as AST transformation frameworks)

* - *

The {@link io.micronaut.inject.writer.BeanDefinitionWriter} class can be used to produce bean definitions at + *

The {@code io.micronaut.inject.writer.BeanDefinitionWriter} class can be used to produce bean definitions at * compile or runtime

* * @param The Bean definition type * @author Graeme Rocher * @author Denis Stepanov - * @see io.micronaut.inject.writer.BeanDefinitionWriter * @since 3.0 */ @Internal @@ -2069,8 +2068,8 @@ private K resolveBean( } } } catch (DisabledBeanException e) { - if (AbstractBeanContextConditional.LOG.isDebugEnabled()) { - AbstractBeanContextConditional.LOG.debug("Bean of type [{}] disabled for reason: {}", argument.getTypeName(), e.getMessage()); + if (ConditionLog.LOG.isDebugEnabled()) { + ConditionLog.LOG.debug("Bean of type [{}] disabled for reason: {}", argument.getTypeName(), e.getMessage()); } if (isIterable() && getAnnotationMetadata().hasDeclaredAnnotation(EachBean.class)) { throw new DisabledBeanException("Bean [" + getBeanType().getSimpleName() + "] disabled by parent: " + e.getMessage()); diff --git a/inject/src/main/java/io/micronaut/context/AbstractInitializableBeanDefinitionReference.java b/inject/src/main/java/io/micronaut/context/AbstractInitializableBeanDefinitionReference.java index 7f1db91ef51..afebf30c2ce 100644 --- a/inject/src/main/java/io/micronaut/context/AbstractInitializableBeanDefinitionReference.java +++ b/inject/src/main/java/io/micronaut/context/AbstractInitializableBeanDefinitionReference.java @@ -21,8 +21,6 @@ import io.micronaut.core.annotation.NonNull; import io.micronaut.inject.BeanDefinition; import io.micronaut.inject.BeanDefinitionReference; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import java.util.Collections; import java.util.Set; @@ -37,7 +35,6 @@ @Internal public abstract class AbstractInitializableBeanDefinitionReference extends AbstractBeanContextConditional implements BeanDefinitionReference { - private static final Logger LOG = LoggerFactory.getLogger(AbstractInitializableBeanDefinitionReference.class); private final String beanTypeName; private final String beanDefinitionTypeName; private final AnnotationMetadata annotationMetadata; @@ -158,8 +155,8 @@ public boolean isPresent() { present = true; } catch (Throwable e) { if (e instanceof TypeNotPresentException || e instanceof ClassNotFoundException || e instanceof NoClassDefFoundError) { - if (LOG.isTraceEnabled()) { - LOG.trace("Bean definition for type [" + beanTypeName + "] not loaded since it is not on the classpath", e); + if (ConditionLog.LOG.isTraceEnabled()) { + ConditionLog.LOG.trace("Bean definition for type [" + beanTypeName + "] not loaded since it is not on the classpath", e); } } else { throw new BeanContextException("Unexpected error loading bean definition [" + beanDefinitionTypeName + "]: " + e.getMessage(), e); diff --git a/inject/src/main/java/io/micronaut/context/DefaultBeanContext.java b/inject/src/main/java/io/micronaut/context/DefaultBeanContext.java index f86d9febba6..4f60f82dc34 100644 --- a/inject/src/main/java/io/micronaut/context/DefaultBeanContext.java +++ b/inject/src/main/java/io/micronaut/context/DefaultBeanContext.java @@ -874,8 +874,8 @@ public T getBean(@NonNull Argument beanType, @Nullable Qualifier quali try { return getBean(null, beanType, qualifier); } catch (DisabledBeanException e) { - if (AbstractBeanContextConditional.LOG.isDebugEnabled()) { - AbstractBeanContextConditional.LOG.debug("Bean of type [{}] disabled for reason: {}", beanType.getSimpleName(), e.getMessage()); + if (AbstractBeanContextConditional.ConditionLog.LOG.isDebugEnabled()) { + AbstractBeanContextConditional.ConditionLog.LOG.debug("Bean of type [{}] disabled for reason: {}", beanType.getSimpleName(), e.getMessage()); } throw new NoSuchBeanException(beanType, qualifier); } @@ -1693,8 +1693,8 @@ public Optional findBean(@Nullable BeanResolutionContext resolutionContex return Optional.of(beanRegistration.bean); } } catch (DisabledBeanException e) { - if (AbstractBeanContextConditional.LOG.isDebugEnabled()) { - AbstractBeanContextConditional.LOG.debug("Bean of type [{}] disabled for reason: {}", beanType.getSimpleName(), e.getMessage()); + if (AbstractBeanContextConditional.ConditionLog.LOG.isDebugEnabled()) { + AbstractBeanContextConditional.ConditionLog.LOG.debug("Bean of type [{}] disabled for reason: {}", beanType.getSimpleName(), e.getMessage()); } return Optional.empty(); } @@ -1923,8 +1923,8 @@ protected void initializeContext( try { loadContextScopeBean(contextScopeDefinition); } catch (DisabledBeanException e) { - if (AbstractBeanContextConditional.LOG.isDebugEnabled()) { - AbstractBeanContextConditional.LOG.debug("Bean of type [{}] disabled for reason: {}", contextScopeDefinition.getBeanType().getSimpleName(), e.getMessage()); + if (AbstractBeanContextConditional.ConditionLog.LOG.isDebugEnabled()) { + AbstractBeanContextConditional.ConditionLog.LOG.debug("Bean of type [{}] disabled for reason: {}", contextScopeDefinition.getBeanType().getSimpleName(), e.getMessage()); } } catch (Throwable e) { throw new BeanInstantiationException("Bean definition [" + contextScopeDefinition.getName() + "] could not be loaded: " + e.getMessage(), e); @@ -3523,8 +3523,8 @@ private void addCandidateToList(@Nullable BeanResolutionContext resolutionCo LOG.debug("Found a registration {} for candidate: {} with qualifier: {}", beanRegistration, candidate, qualifier); } } catch (DisabledBeanException e) { - if (AbstractBeanContextConditional.LOG.isDebugEnabled()) { - AbstractBeanContextConditional.LOG.debug("Bean of type [{}] disabled for reason: {}", beanType.getTypeName(), e.getMessage()); + if (AbstractBeanContextConditional.ConditionLog.LOG.isDebugEnabled()) { + AbstractBeanContextConditional.ConditionLog.LOG.debug("Bean of type [{}] disabled for reason: {}", beanType.getTypeName(), e.getMessage()); } } diff --git a/inject/src/main/java/io/micronaut/context/env/DefaultEnvironment.java b/inject/src/main/java/io/micronaut/context/env/DefaultEnvironment.java index 43b1f98a309..69c2a607a4f 100644 --- a/inject/src/main/java/io/micronaut/context/env/DefaultEnvironment.java +++ b/inject/src/main/java/io/micronaut/context/env/DefaultEnvironment.java @@ -102,9 +102,8 @@ public class DefaultEnvironment extends PropertySourcePropertyResolver implement private static final List DEFAULT_CONFIG_LOCATIONS = Arrays.asList("classpath:/", "file:config/"); protected final ClassPathResourceLoader resourceLoader; protected final List refreshablePropertySources = new ArrayList<>(10); - + protected final MutableConversionService mutableConversionService; private EnvironmentsAndPackage environmentsAndPackage; - private final Set names; private final ClassLoader classLoader; private final Collection packages = new ConcurrentLinkedQueue<>(); @@ -119,7 +118,6 @@ public class DefaultEnvironment extends PropertySourcePropertyResolver implement private final Boolean deduceEnvironments; private final ApplicationContextConfiguration configuration; private final Collection configLocations; - protected final MutableConversionService mutableConversionService; /** * Construct a new environment for the given configuration. diff --git a/inject/src/main/java/io/micronaut/context/event/ApplicationEventPublisherFactory.java b/inject/src/main/java/io/micronaut/context/event/ApplicationEventPublisherFactory.java index 43d606f10c3..21b95da1d3c 100644 --- a/inject/src/main/java/io/micronaut/context/event/ApplicationEventPublisherFactory.java +++ b/inject/src/main/java/io/micronaut/context/event/ApplicationEventPublisherFactory.java @@ -60,7 +60,7 @@ public final class ApplicationEventPublisherFactory implements BeanDefinition>, BeanFactory>, BeanDefinitionReference> { - private static final Logger EVENT_LOGGER = LoggerFactory.getLogger(ApplicationEventPublisher.class); + private static final Argument TYPE_VARIABLE = Argument.ofTypeVariable(Object.class, "T"); private final AnnotationMetadata annotationMetadata; private ApplicationEventPublisher applicationObjectEventPublisher; @@ -223,8 +223,8 @@ private ApplicationEventPublisher createEventPublisher(Argument event @Override public void publishEvent(Object event) { if (event != null) { - if (EVENT_LOGGER.isDebugEnabled()) { - EVENT_LOGGER.debug("Publishing event: {}", event); + if (EventLogger.LOG.isDebugEnabled()) { + EventLogger.LOG.debug("Publishing event: {}", event); } notifyEventListeners(event, lazyListeners.get()); } @@ -250,21 +250,21 @@ public Future publishEventAsync(Object event) { private void notifyEventListeners(@NonNull Object event, Collection eventListeners) { if (!eventListeners.isEmpty()) { - if (EVENT_LOGGER.isTraceEnabled()) { - EVENT_LOGGER.trace("Established event listeners {} for event: {}", eventListeners, event); + if (EventLogger.LOG.isTraceEnabled()) { + EventLogger.LOG.trace("Established event listeners {} for event: {}", eventListeners, event); } for (ApplicationEventListener listener : eventListeners) { if (listener.supports(event)) { try { - if (EVENT_LOGGER.isTraceEnabled()) { - EVENT_LOGGER.trace("Invoking event listener [{}] for event: {}", listener, event); + if (EventLogger.LOG.isTraceEnabled()) { + EventLogger.LOG.trace("Invoking event listener [{}] for event: {}", listener, event); } listener.onApplicationEvent(event); } catch (ClassCastException ex) { String msg = ex.getMessage(); if (msg == null || msg.startsWith(event.getClass().getName())) { - if (EVENT_LOGGER.isDebugEnabled()) { - EVENT_LOGGER.debug("Incompatible listener for event: " + listener, ex); + if (EventLogger.LOG.isDebugEnabled()) { + EventLogger.LOG.debug("Incompatible listener for event: " + listener, ex); } } else { throw ex; @@ -274,4 +274,11 @@ private void notifyEventListeners(@NonNull Object event, Collection extends AnnotationMetadataProvider, BeanContextConditional { +public interface BeanType extends AnnotationMetadataProvider, BeanContextConditional, BeanInfo { /** * @return Whether the bean definition is the {@link io.micronaut.context.annotation.Primary} @@ -50,7 +51,7 @@ default boolean isPrimary() { * * @return The underlying bean type */ - Class getBeanType(); + @NonNull Class getBeanType(); /** * Checks whether the bean type is a container type. diff --git a/inject/src/main/resources/META-INF/native-image/io.micronaut/micronaut-inject/native-image.properties b/inject/src/main/resources/META-INF/native-image/io.micronaut/micronaut-inject/native-image.properties index 572a2029998..ddd6f90d09e 100644 --- a/inject/src/main/resources/META-INF/native-image/io.micronaut/micronaut-inject/native-image.properties +++ b/inject/src/main/resources/META-INF/native-image/io.micronaut/micronaut-inject/native-image.properties @@ -14,7 +14,7 @@ # limitations under the License. # -Args = --allow-incomplete-classpath \ - -H:EnableURLProtocols=http,https \ - --initialize-at-run-time=io.micronaut.inject.provider.JakartaProviderBeanDefinition \ +Args = -H:EnableURLProtocols=http,https \ + --initialize-at-build-time=io.micronaut.inject.annotation \ + --initialize-at-build-time=io.micronaut.runtime.converters.time \ --initialize-at-run-time=io.micronaut.context.env.CachedEnvironment diff --git a/jackson-databind/src/main/java/io/micronaut/jackson/JacksonDatabindFeature.java b/jackson-databind/src/main/java/io/micronaut/jackson/JacksonDatabindFeature.java index 1df9d6b2d42..24fca4d8269 100644 --- a/jackson-databind/src/main/java/io/micronaut/jackson/JacksonDatabindFeature.java +++ b/jackson-databind/src/main/java/io/micronaut/jackson/JacksonDatabindFeature.java @@ -17,7 +17,6 @@ import com.fasterxml.jackson.databind.PropertyNamingStrategies; import com.fasterxml.jackson.databind.PropertyNamingStrategy; -import com.oracle.svm.core.annotate.AutomaticFeature; import io.micronaut.core.annotation.Internal; import org.graalvm.nativeimage.hosted.Feature; import org.graalvm.nativeimage.hosted.RuntimeReflection; @@ -31,7 +30,6 @@ * @since 3.4.1 */ @Internal -@AutomaticFeature final class JacksonDatabindFeature implements Feature { @SuppressWarnings("deprecation") @Override diff --git a/runtime/src/main/java/io/micronaut/reactive/package-info.java b/jackson-databind/src/main/java/io/micronaut/jackson/databind/JacksonDatabindMapperSupplier.java similarity index 55% rename from runtime/src/main/java/io/micronaut/reactive/package-info.java rename to jackson-databind/src/main/java/io/micronaut/jackson/databind/JacksonDatabindMapperSupplier.java index 3e8c3ef8b63..746715cc81b 100644 --- a/runtime/src/main/java/io/micronaut/reactive/package-info.java +++ b/jackson-databind/src/main/java/io/micronaut/jackson/databind/JacksonDatabindMapperSupplier.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2020 original authors + * Copyright 2017-2022 original authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,13 +13,22 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +package io.micronaut.jackson.databind; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.json.JsonMapper; +import io.micronaut.json.JsonMapperSupplier; + /** - *

This package contains a set of reactive primitives designed to bridge Reactive libraries such as RxJava and Reactor

- * - *

The design of this package is such that it is not seen as replacement for reactive frameworks, but rather allows the user to choose the - * library to use without the framework itself being dependent on any particular library.

+ * Implementation of {@link JsonMapperSupplier} for Jackson. * + * @since 4.0.0 * @author Graeme Rocher - * @since 1.0 */ -package io.micronaut.reactive; +@Internal +public final class JacksonDatabindMapperSupplier implements JsonMapperSupplier { + @Override + public JsonMapper get() { + return new JacksonDatabindMapper(); + } +} diff --git a/jackson-databind/src/main/resources/META-INF/native-image/io.micronaut/micronaut-jackson-databind/native-image.properties b/jackson-databind/src/main/resources/META-INF/native-image/io.micronaut/micronaut-jackson-databind/native-image.properties new file mode 100644 index 00000000000..bb128a8a00c --- /dev/null +++ b/jackson-databind/src/main/resources/META-INF/native-image/io.micronaut/micronaut-jackson-databind/native-image.properties @@ -0,0 +1 @@ +Args = --features=io.micronaut.jackson.JacksonDatabindFeature diff --git a/jackson-databind/src/main/resources/META-INF/services/io.micronaut.json.JsonMapperSupplier b/jackson-databind/src/main/resources/META-INF/services/io.micronaut.json.JsonMapperSupplier new file mode 100644 index 00000000000..a1a9ee54f6c --- /dev/null +++ b/jackson-databind/src/main/resources/META-INF/services/io.micronaut.json.JsonMapperSupplier @@ -0,0 +1 @@ +io.micronaut.jackson.databind.JacksonDatabindMapperSupplier diff --git a/json-core/src/main/java/io/micronaut/json/JsonMapper.java b/json-core/src/main/java/io/micronaut/json/JsonMapper.java index fef960d8a04..fa0bb1c9380 100644 --- a/json-core/src/main/java/io/micronaut/json/JsonMapper.java +++ b/json-core/src/main/java/io/micronaut/json/JsonMapper.java @@ -19,6 +19,7 @@ import io.micronaut.core.annotation.Experimental; import io.micronaut.core.annotation.NonNull; import io.micronaut.core.annotation.Nullable; +import io.micronaut.core.order.OrderUtil; import io.micronaut.core.type.Argument; import io.micronaut.json.tree.JsonNode; import org.reactivestreams.Processor; @@ -28,7 +29,9 @@ import java.io.OutputStream; import java.nio.charset.StandardCharsets; import java.util.Optional; +import java.util.ServiceLoader; import java.util.function.Consumer; +import java.util.stream.Stream; /** * Common abstraction for mapping json to data structures. @@ -222,4 +225,25 @@ default JsonMapper cloneWithViewClass(@NonNull Class viewClass) { */ @NonNull JsonStreamConfig getStreamConfig(); + + /** + * Resolves the default {@link JsonMapper}. + * @return The default {@link JsonMapper} + * @throws IllegalStateException If no {@link JsonMapper} implementation exists on the classpath. + * @since 4.0.0 + */ + static @NonNull JsonMapper createDefault() { + return ServiceLoader.load(JsonMapperSupplier.class).stream() + .flatMap(p -> { + try { + JsonMapperSupplier supplier = p.get(); + return Stream.ofNullable(supplier); + } catch (Exception e) { + return Stream.empty(); + } + }) + .min(OrderUtil.COMPARATOR) + .orElseThrow(() -> new IllegalStateException("No JsonMapper implementation found")) + .get(); + } } diff --git a/json-core/src/main/java/io/micronaut/json/JsonMapperSupplier.java b/json-core/src/main/java/io/micronaut/json/JsonMapperSupplier.java new file mode 100644 index 00000000000..7151990b33b --- /dev/null +++ b/json-core/src/main/java/io/micronaut/json/JsonMapperSupplier.java @@ -0,0 +1,27 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.json; + +import java.util.function.Supplier; + +/** + * Strategy interface for resolving a {@link JsonMapper}. + * + * @author graemerocher + * @since 4.0.0 + */ +public interface JsonMapperSupplier extends Supplier { +} diff --git a/management/build.gradle b/management/build.gradle index 568e3422750..d3a246d1d52 100644 --- a/management/build.gradle +++ b/management/build.gradle @@ -7,7 +7,8 @@ dependencies { annotationProcessor project(":graal") api project(":router") - api project(":runtime") + api project(":discovery-core") + compileOnly project(":jackson-databind") compileOnly(libs.managed.micronaut.sql.jdbc) { exclude module:'micronaut-inject' exclude module:'micronaut-bom' @@ -18,6 +19,7 @@ dependencies { testImplementation project(":http-client") testImplementation project(":inject-groovy") testImplementation project(":http-server-netty") + testImplementation project(":jackson-databind") testImplementation(libs.managed.micronaut.sql.jdbc.tomcat) { exclude module:'micronaut-inject' exclude module:'micronaut-bom' diff --git a/management/src/main/java/io/micronaut/management/endpoint/health/HealthEndpoint.java b/management/src/main/java/io/micronaut/management/endpoint/health/HealthEndpoint.java index b82c2a53505..d28f3a05168 100644 --- a/management/src/main/java/io/micronaut/management/endpoint/health/HealthEndpoint.java +++ b/management/src/main/java/io/micronaut/management/endpoint/health/HealthEndpoint.java @@ -197,7 +197,7 @@ protected HealthLevelOfDetail levelOfDetail(@Nullable Principal principal) { } /** - * Configuration related to handling of the {@link io.micronaut.health.HealthStatus}. + * Configuration related to handling of the {@link HealthStatus}. * * @author graemerocher * @since 1.0 @@ -216,14 +216,14 @@ public StatusConfiguration() { } /** - * @return How {@link io.micronaut.health.HealthStatus} map to {@link io.micronaut.http.HttpStatus} codes. + * @return How {@link HealthStatus} map to {@link io.micronaut.http.HttpStatus} codes. */ public Map getHttpMapping() { return httpMapping; } /** - * Set how {@link io.micronaut.health.HealthStatus} map to {@link io.micronaut.http.HttpStatus} codes. + * Set how {@link HealthStatus} map to {@link io.micronaut.http.HttpStatus} codes. * * @param httpMapping The http mappings */ diff --git a/messaging/build.gradle b/messaging/build.gradle index f79df6cc3c1..07d16a12528 100644 --- a/messaging/build.gradle +++ b/messaging/build.gradle @@ -4,8 +4,7 @@ plugins { dependencies { annotationProcessor project(":inject-java") - api project(":inject") - api project(":runtime") + api project(":context") testAnnotationProcessor project(":inject-java") testImplementation project(":inject") diff --git a/retry/build.gradle b/retry/build.gradle new file mode 100644 index 00000000000..2b10ed67ead --- /dev/null +++ b/retry/build.gradle @@ -0,0 +1,13 @@ +plugins { + id "io.micronaut.build.internal.convention-library" +} + +dependencies { + annotationProcessor project(":inject-java") + + api project(':context') + api project(':core-reactive') + implementation libs.managed.reactor + testImplementation project(":jackson-databind") + testImplementation project(":discovery-core") +} diff --git a/runtime/src/main/java/io/micronaut/retry/CircuitState.java b/retry/src/main/java/io/micronaut/retry/CircuitState.java similarity index 100% rename from runtime/src/main/java/io/micronaut/retry/CircuitState.java rename to retry/src/main/java/io/micronaut/retry/CircuitState.java diff --git a/runtime/src/main/java/io/micronaut/retry/RetryState.java b/retry/src/main/java/io/micronaut/retry/RetryState.java similarity index 100% rename from runtime/src/main/java/io/micronaut/retry/RetryState.java rename to retry/src/main/java/io/micronaut/retry/RetryState.java diff --git a/runtime/src/main/java/io/micronaut/retry/RetryStateBuilder.java b/retry/src/main/java/io/micronaut/retry/RetryStateBuilder.java similarity index 100% rename from runtime/src/main/java/io/micronaut/retry/RetryStateBuilder.java rename to retry/src/main/java/io/micronaut/retry/RetryStateBuilder.java diff --git a/runtime/src/main/java/io/micronaut/retry/annotation/CircuitBreaker.java b/retry/src/main/java/io/micronaut/retry/annotation/CircuitBreaker.java similarity index 100% rename from runtime/src/main/java/io/micronaut/retry/annotation/CircuitBreaker.java rename to retry/src/main/java/io/micronaut/retry/annotation/CircuitBreaker.java diff --git a/runtime/src/main/java/io/micronaut/retry/annotation/DefaultRetryPredicate.java b/retry/src/main/java/io/micronaut/retry/annotation/DefaultRetryPredicate.java similarity index 100% rename from runtime/src/main/java/io/micronaut/retry/annotation/DefaultRetryPredicate.java rename to retry/src/main/java/io/micronaut/retry/annotation/DefaultRetryPredicate.java diff --git a/runtime/src/main/java/io/micronaut/retry/annotation/Fallback.java b/retry/src/main/java/io/micronaut/retry/annotation/Fallback.java similarity index 100% rename from runtime/src/main/java/io/micronaut/retry/annotation/Fallback.java rename to retry/src/main/java/io/micronaut/retry/annotation/Fallback.java diff --git a/runtime/src/main/java/io/micronaut/retry/annotation/Recoverable.java b/retry/src/main/java/io/micronaut/retry/annotation/Recoverable.java similarity index 100% rename from runtime/src/main/java/io/micronaut/retry/annotation/Recoverable.java rename to retry/src/main/java/io/micronaut/retry/annotation/Recoverable.java diff --git a/runtime/src/main/java/io/micronaut/retry/annotation/RetryPredicate.java b/retry/src/main/java/io/micronaut/retry/annotation/RetryPredicate.java similarity index 100% rename from runtime/src/main/java/io/micronaut/retry/annotation/RetryPredicate.java rename to retry/src/main/java/io/micronaut/retry/annotation/RetryPredicate.java diff --git a/runtime/src/main/java/io/micronaut/retry/annotation/Retryable.java b/retry/src/main/java/io/micronaut/retry/annotation/Retryable.java similarity index 100% rename from runtime/src/main/java/io/micronaut/retry/annotation/Retryable.java rename to retry/src/main/java/io/micronaut/retry/annotation/Retryable.java diff --git a/runtime/src/main/java/io/micronaut/retry/annotation/package-info.java b/retry/src/main/java/io/micronaut/retry/annotation/package-info.java similarity index 100% rename from runtime/src/main/java/io/micronaut/retry/annotation/package-info.java rename to retry/src/main/java/io/micronaut/retry/annotation/package-info.java diff --git a/runtime/src/main/java/io/micronaut/retry/event/CircuitClosedEvent.java b/retry/src/main/java/io/micronaut/retry/event/CircuitClosedEvent.java similarity index 100% rename from runtime/src/main/java/io/micronaut/retry/event/CircuitClosedEvent.java rename to retry/src/main/java/io/micronaut/retry/event/CircuitClosedEvent.java diff --git a/runtime/src/main/java/io/micronaut/retry/event/CircuitOpenEvent.java b/retry/src/main/java/io/micronaut/retry/event/CircuitOpenEvent.java similarity index 100% rename from runtime/src/main/java/io/micronaut/retry/event/CircuitOpenEvent.java rename to retry/src/main/java/io/micronaut/retry/event/CircuitOpenEvent.java diff --git a/runtime/src/main/java/io/micronaut/retry/event/RetryEvent.java b/retry/src/main/java/io/micronaut/retry/event/RetryEvent.java similarity index 100% rename from runtime/src/main/java/io/micronaut/retry/event/RetryEvent.java rename to retry/src/main/java/io/micronaut/retry/event/RetryEvent.java diff --git a/runtime/src/main/java/io/micronaut/retry/event/RetryEventListener.java b/retry/src/main/java/io/micronaut/retry/event/RetryEventListener.java similarity index 100% rename from runtime/src/main/java/io/micronaut/retry/event/RetryEventListener.java rename to retry/src/main/java/io/micronaut/retry/event/RetryEventListener.java diff --git a/runtime/src/main/java/io/micronaut/retry/event/package-info.java b/retry/src/main/java/io/micronaut/retry/event/package-info.java similarity index 100% rename from runtime/src/main/java/io/micronaut/retry/event/package-info.java rename to retry/src/main/java/io/micronaut/retry/event/package-info.java diff --git a/runtime/src/main/java/io/micronaut/retry/exception/CircuitOpenException.java b/retry/src/main/java/io/micronaut/retry/exception/CircuitOpenException.java similarity index 100% rename from runtime/src/main/java/io/micronaut/retry/exception/CircuitOpenException.java rename to retry/src/main/java/io/micronaut/retry/exception/CircuitOpenException.java diff --git a/runtime/src/main/java/io/micronaut/retry/exception/FallbackException.java b/retry/src/main/java/io/micronaut/retry/exception/FallbackException.java similarity index 100% rename from runtime/src/main/java/io/micronaut/retry/exception/FallbackException.java rename to retry/src/main/java/io/micronaut/retry/exception/FallbackException.java diff --git a/runtime/src/main/java/io/micronaut/retry/exception/RetryException.java b/retry/src/main/java/io/micronaut/retry/exception/RetryException.java similarity index 100% rename from runtime/src/main/java/io/micronaut/retry/exception/RetryException.java rename to retry/src/main/java/io/micronaut/retry/exception/RetryException.java diff --git a/runtime/src/main/java/io/micronaut/retry/exception/package-info.java b/retry/src/main/java/io/micronaut/retry/exception/package-info.java similarity index 100% rename from runtime/src/main/java/io/micronaut/retry/exception/package-info.java rename to retry/src/main/java/io/micronaut/retry/exception/package-info.java diff --git a/runtime/src/main/java/io/micronaut/retry/intercept/AnnotationRetryStateBuilder.java b/retry/src/main/java/io/micronaut/retry/intercept/AnnotationRetryStateBuilder.java similarity index 100% rename from runtime/src/main/java/io/micronaut/retry/intercept/AnnotationRetryStateBuilder.java rename to retry/src/main/java/io/micronaut/retry/intercept/AnnotationRetryStateBuilder.java diff --git a/runtime/src/main/java/io/micronaut/retry/intercept/CircuitBreakerRetry.java b/retry/src/main/java/io/micronaut/retry/intercept/CircuitBreakerRetry.java similarity index 100% rename from runtime/src/main/java/io/micronaut/retry/intercept/CircuitBreakerRetry.java rename to retry/src/main/java/io/micronaut/retry/intercept/CircuitBreakerRetry.java diff --git a/runtime/src/main/java/io/micronaut/retry/intercept/DefaultRetryInterceptor.java b/retry/src/main/java/io/micronaut/retry/intercept/DefaultRetryInterceptor.java similarity index 100% rename from runtime/src/main/java/io/micronaut/retry/intercept/DefaultRetryInterceptor.java rename to retry/src/main/java/io/micronaut/retry/intercept/DefaultRetryInterceptor.java diff --git a/runtime/src/main/java/io/micronaut/retry/intercept/MutableRetryState.java b/retry/src/main/java/io/micronaut/retry/intercept/MutableRetryState.java similarity index 100% rename from runtime/src/main/java/io/micronaut/retry/intercept/MutableRetryState.java rename to retry/src/main/java/io/micronaut/retry/intercept/MutableRetryState.java diff --git a/runtime/src/test/groovy/io/micronaut/retry/intercept/MyCustomException.java b/retry/src/main/java/io/micronaut/retry/intercept/MyCustomException.java similarity index 100% rename from runtime/src/test/groovy/io/micronaut/retry/intercept/MyCustomException.java rename to retry/src/main/java/io/micronaut/retry/intercept/MyCustomException.java diff --git a/runtime/src/main/java/io/micronaut/retry/intercept/RecoveryInterceptor.java b/retry/src/main/java/io/micronaut/retry/intercept/RecoveryInterceptor.java similarity index 93% rename from runtime/src/main/java/io/micronaut/retry/intercept/RecoveryInterceptor.java rename to retry/src/main/java/io/micronaut/retry/intercept/RecoveryInterceptor.java index 6a45a8d2430..df9192ffcb3 100644 --- a/runtime/src/main/java/io/micronaut/retry/intercept/RecoveryInterceptor.java +++ b/retry/src/main/java/io/micronaut/retry/intercept/RecoveryInterceptor.java @@ -21,7 +21,6 @@ import io.micronaut.aop.MethodInvocationContext; import io.micronaut.context.BeanContext; import io.micronaut.core.convert.ConversionService; -import io.micronaut.discovery.exceptions.NoAvailableServiceException; import io.micronaut.inject.BeanDefinition; import io.micronaut.inject.ExecutableMethod; import io.micronaut.inject.MethodExecutionHandle; @@ -201,16 +200,8 @@ private CompletionStage fallbackForFuture(MethodInvocationContext context, RuntimeException exception) { - if (exception instanceof NoAvailableServiceException) { - NoAvailableServiceException nase = (NoAvailableServiceException) exception; - if (LOG.isErrorEnabled()) { - LOG.debug(nase.getMessage(), nase); - LOG.error("Type [{}] attempting to resolve fallback for unavailable service [{}]", context.getTarget().getClass().getName(), nase.getServiceID()); - } - } else { - if (LOG.isErrorEnabled()) { - LOG.error("Type [" + context.getTarget().getClass().getName() + "] executed with error: " + exception.getMessage(), exception); - } + if (LOG.isErrorEnabled()) { + LOG.error("Type [" + context.getTarget().getClass().getName() + "] executed with error: " + exception.getMessage(), exception); } Optional> fallback = findFallbackMethod(context); diff --git a/runtime/src/main/java/io/micronaut/retry/intercept/SimpleRetry.java b/retry/src/main/java/io/micronaut/retry/intercept/SimpleRetry.java similarity index 100% rename from runtime/src/main/java/io/micronaut/retry/intercept/SimpleRetry.java rename to retry/src/main/java/io/micronaut/retry/intercept/SimpleRetry.java diff --git a/runtime/src/main/java/io/micronaut/retry/intercept/package-info.java b/retry/src/main/java/io/micronaut/retry/intercept/package-info.java similarity index 100% rename from runtime/src/main/java/io/micronaut/retry/intercept/package-info.java rename to retry/src/main/java/io/micronaut/retry/intercept/package-info.java diff --git a/runtime/src/main/java/io/micronaut/retry/package-info.java b/retry/src/main/java/io/micronaut/retry/package-info.java similarity index 100% rename from runtime/src/main/java/io/micronaut/retry/package-info.java rename to retry/src/main/java/io/micronaut/retry/package-info.java diff --git a/runtime/src/test/groovy/io/micronaut/retry/intercept/CircuitBreakerRetrySpec.groovy b/retry/src/test/groovy/io/micronaut/retry/intercept/CircuitBreakerRetrySpec.groovy similarity index 100% rename from runtime/src/test/groovy/io/micronaut/retry/intercept/CircuitBreakerRetrySpec.groovy rename to retry/src/test/groovy/io/micronaut/retry/intercept/CircuitBreakerRetrySpec.groovy diff --git a/runtime/src/test/groovy/io/micronaut/retry/intercept/CircuitBreakerSpec.groovy b/retry/src/test/groovy/io/micronaut/retry/intercept/CircuitBreakerSpec.groovy similarity index 100% rename from runtime/src/test/groovy/io/micronaut/retry/intercept/CircuitBreakerSpec.groovy rename to retry/src/test/groovy/io/micronaut/retry/intercept/CircuitBreakerSpec.groovy diff --git a/runtime/src/test/groovy/io/micronaut/retry/intercept/InterceptorOrderSpec.groovy b/retry/src/test/groovy/io/micronaut/retry/intercept/InterceptorOrderSpec.groovy similarity index 100% rename from runtime/src/test/groovy/io/micronaut/retry/intercept/InterceptorOrderSpec.groovy rename to retry/src/test/groovy/io/micronaut/retry/intercept/InterceptorOrderSpec.groovy diff --git a/runtime/src/test/groovy/io/micronaut/retry/intercept/SimpleRetryInstanceSpec.groovy b/retry/src/test/groovy/io/micronaut/retry/intercept/SimpleRetryInstanceSpec.groovy similarity index 100% rename from runtime/src/test/groovy/io/micronaut/retry/intercept/SimpleRetryInstanceSpec.groovy rename to retry/src/test/groovy/io/micronaut/retry/intercept/SimpleRetryInstanceSpec.groovy diff --git a/runtime/src/test/groovy/io/micronaut/retry/intercept/SimpleRetrySpec.groovy b/retry/src/test/groovy/io/micronaut/retry/intercept/SimpleRetrySpec.groovy similarity index 100% rename from runtime/src/test/groovy/io/micronaut/retry/intercept/SimpleRetrySpec.groovy rename to retry/src/test/groovy/io/micronaut/retry/intercept/SimpleRetrySpec.groovy diff --git a/runtime-osx/build.gradle b/runtime-osx/build.gradle index 9b400c9e50b..5fe61e3e53a 100644 --- a/runtime-osx/build.gradle +++ b/runtime-osx/build.gradle @@ -5,6 +5,6 @@ plugins { dependencies { annotationProcessor project(":inject-java") - api project(":runtime") + api project(":context") implementation libs.managed.methvin.directoryWatcher } diff --git a/runtime/build.gradle b/runtime/build.gradle index 23cac4f7e94..af55078730c 100644 --- a/runtime/build.gradle +++ b/runtime/build.gradle @@ -5,12 +5,14 @@ plugins { dependencies { annotationProcessor project(":inject-java") annotationProcessor project(":graal") - api project(":http") - api project(':inject') api project(':aop') + api project(':discovery-core') api project(':context') api project(":core-reactive") - api project(":jackson-databind") + api project(":http") + api project(':inject') + api project(':retry') + api libs.managed.validation implementation libs.managed.reactor @@ -18,12 +20,10 @@ dependencies { compileOnly libs.managed.graal compileOnly libs.managed.jcache - compileOnly libs.log4j compileOnly libs.javax.el compileOnly libs.caffeine compileOnly libs.kotlinx.coroutines.core compileOnly libs.kotlinx.coroutines.reactive - compileOnly libs.managed.logback testImplementation libs.managed.logback testImplementation libs.managed.snakeyaml testAnnotationProcessor project(":inject-java") diff --git a/runtime/src/main/java/io/micronaut/reactive/flow/converters/FlowConverterRegistrar.java b/runtime/src/main/java/io/micronaut/reactive/flow/converters/FlowConverterRegistrar.java deleted file mode 100644 index 20e06a19983..00000000000 --- a/runtime/src/main/java/io/micronaut/reactive/flow/converters/FlowConverterRegistrar.java +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright 2017-2020 original authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.micronaut.reactive.flow.converters; - -import io.micronaut.context.annotation.Requires; -import io.micronaut.core.convert.MutableConversionService; -import io.micronaut.core.convert.TypeConverterRegistrar; -import jakarta.inject.Singleton; -import kotlinx.coroutines.flow.Flow; -import kotlinx.coroutines.reactive.ReactiveFlowKt; -import org.reactivestreams.Publisher; -import reactor.core.publisher.Flux; - -/** - * Converts between a {@link Flux} and a {@link Publisher}. - * - * @author Konrad Kamiński - * @since 1.3 - */ -@Singleton -@Requires(classes = {Flux.class, ReactiveFlowKt.class}) -public class FlowConverterRegistrar implements TypeConverterRegistrar { - @Override - public void register(MutableConversionService conversionService) { - // Flow - conversionService.addConverter(Flow.class, Flux.class, flow -> - Flux.from(ReactiveFlowKt.asPublisher(flow)) - ); - conversionService.addConverter(Flow.class, Publisher.class, ReactiveFlowKt::asPublisher); - conversionService.addConverter(Publisher.class, Flow.class, ReactiveFlowKt::asFlow); - } -} diff --git a/runtime/src/main/java/io/micronaut/reactive/reactor/instrument/ReactorInstrumentation.java b/runtime/src/main/java/io/micronaut/reactive/reactor/instrument/ReactorInstrumentation.java deleted file mode 100644 index 9587aaae8de..00000000000 --- a/runtime/src/main/java/io/micronaut/reactive/reactor/instrument/ReactorInstrumentation.java +++ /dev/null @@ -1,142 +0,0 @@ -/* - * Copyright 2017-2019 original authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.micronaut.reactive.reactor.instrument; - -import io.micronaut.context.annotation.Context; -import io.micronaut.context.annotation.Requires; -import io.micronaut.context.env.Environment; -import io.micronaut.core.annotation.Internal; -import io.micronaut.core.annotation.Nullable; -import io.micronaut.core.util.CollectionUtils; -import io.micronaut.scheduling.instrument.Instrumentation; -import io.micronaut.scheduling.instrument.InvocationInstrumenter; -import io.micronaut.scheduling.instrument.ReactiveInvocationInstrumenterFactory; -import jakarta.annotation.PostConstruct; -import jakarta.annotation.PreDestroy; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Hooks; -import reactor.core.publisher.Operators; -import reactor.core.scheduler.Schedulers; - -import java.util.ArrayList; -import java.util.List; - -/** - * Instruments Reactor such that the thread factory used by Micronaut is used and instrumentations can be applied to the {@link java.util.concurrent.ScheduledExecutorService}. - * - * @author Graeme Rocher - * @since 1.0 - */ -@Requires(sdk = Requires.Sdk.MICRONAUT, version = "2.0.0") -@Requires(classes = {Flux.class, Schedulers.Factory.class}) -@Context -@Internal -class ReactorInstrumentation { - /** - * Initialize instrumentation for reactor with the tracer and factory. - * - * @param instrumenterFactory The instrumenter factory - */ - @PostConstruct - void init(ReactorInstrumenterFactory instrumenterFactory) { - if (instrumenterFactory.hasInstrumenters()) { - Schedulers.onScheduleHook(Environment.MICRONAUT, runnable -> { - InvocationInstrumenter instrumenter = instrumenterFactory.create(); - if (instrumenter != null) { - return () -> { - try (Instrumentation ignored = instrumenter.newInstrumentation()) { - runnable.run(); - } - }; - } - return runnable; - }); - Hooks.onEachOperator(Environment.MICRONAUT, Operators.lift((scannable, coreSubscriber) -> { - if (coreSubscriber instanceof ReactorSubscriber) { - return coreSubscriber; - } - InvocationInstrumenter instrumenter = instrumenterFactory.create(); - if (instrumenter != null) { - return new ReactorSubscriber<>(instrumenter, coreSubscriber); - } - return coreSubscriber; - })); - } - } - - /** - * Removes the registered instrumentation. - */ - @PreDestroy - void removeInstrumentation() { - Schedulers.removeExecutorServiceDecorator(Environment.MICRONAUT); - Hooks.resetOnEachOperator(Environment.MICRONAUT); - } - - - @Context - @Requires(classes = Flux.class) - @Internal - static final class ReactorInstrumenterFactory { - - private final List reactiveInvocationInstrumenterFactories; - - /** - * @param reactiveInvocationInstrumenterFactories invocation instrumenters - */ - ReactorInstrumenterFactory(List reactiveInvocationInstrumenterFactories) { - this.reactiveInvocationInstrumenterFactories = reactiveInvocationInstrumenterFactories; - } - - /** - * Check if there are any instumenters present. - * - * @return true if there are any instumenters present - */ - public boolean hasInstrumenters() { - return !reactiveInvocationInstrumenterFactories.isEmpty(); - } - - /** - * Created a new {@link InvocationInstrumenter}. - * - * @return new {@link InvocationInstrumenter} if instrumentation is required - */ - @Nullable - public InvocationInstrumenter create() { - List invocationInstrumenter = getReactiveInvocationInstrumenters(); - if (CollectionUtils.isNotEmpty(invocationInstrumenter)) { - return InvocationInstrumenter.combine(invocationInstrumenter); - } - return null; - } - - /** - * @return The invocation instrumenters - */ - private List getReactiveInvocationInstrumenters() { - List instrumenters = new ArrayList<>(reactiveInvocationInstrumenterFactories.size()); - for (ReactiveInvocationInstrumenterFactory instrumenterFactory : reactiveInvocationInstrumenterFactories) { - final InvocationInstrumenter instrumenter = instrumenterFactory.newReactiveInvocationInstrumenter(); - if (instrumenter != null) { - instrumenters.add(instrumenter); - } - } - return instrumenters; - } - - } -} diff --git a/runtime/src/main/java/io/micronaut/reactive/reactor/instrument/ReactorSubscriber.java b/runtime/src/main/java/io/micronaut/reactive/reactor/instrument/ReactorSubscriber.java deleted file mode 100644 index d3922fe3b04..00000000000 --- a/runtime/src/main/java/io/micronaut/reactive/reactor/instrument/ReactorSubscriber.java +++ /dev/null @@ -1,74 +0,0 @@ -/* - * Copyright 2017-2021 original authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.micronaut.reactive.reactor.instrument; - -import io.micronaut.core.annotation.Internal; -import io.micronaut.scheduling.instrument.Instrumentation; -import io.micronaut.scheduling.instrument.InvocationInstrumenter; -import org.reactivestreams.Subscription; -import reactor.core.CoreSubscriber; -import reactor.util.context.Context; - -/** - * An {@link CoreSubscriber} with a support of instrumentation. - * - * @param The type - * @author Denis Stepanov - * @since 3.1 - */ -@Internal -final class ReactorSubscriber implements CoreSubscriber { - private final InvocationInstrumenter instrumenter; - private final CoreSubscriber subscriber; - - public ReactorSubscriber(InvocationInstrumenter instrumenter, CoreSubscriber subscriber) { - this.instrumenter = instrumenter; - this.subscriber = subscriber; - } - - @Override - public Context currentContext() { - return subscriber.currentContext(); - } - - @Override - public void onSubscribe(Subscription s) { - try (Instrumentation ignore = instrumenter.newInstrumentation()) { - subscriber.onSubscribe(s); - } - } - - @Override - public void onNext(T t) { - try (Instrumentation ignore = instrumenter.newInstrumentation()) { - subscriber.onNext(t); - } - } - - @Override - public void onError(Throwable t) { - try (Instrumentation ignore = instrumenter.newInstrumentation()) { - subscriber.onError(t); - } - } - - @Override - public void onComplete() { - try (Instrumentation ignore = instrumenter.newInstrumentation()) { - subscriber.onComplete(); - } - } -} diff --git a/session/build.gradle b/session/build.gradle index 8c5cfac7fcd..0e19ddf0206 100644 --- a/session/build.gradle +++ b/session/build.gradle @@ -4,12 +4,12 @@ plugins { dependencies { annotationProcessor project(":inject-java") - api project(":runtime") + api project(":context") api project(":http") compileOnly project(":http-server") compileOnly project(":http-server-netty") - + compileOnly project(":websocket") implementation libs.managed.reactor implementation libs.caffeine @@ -20,6 +20,8 @@ dependencies { testImplementation project(":http-netty") testImplementation project(":http-server-netty") testImplementation project(":http-client") + testImplementation project(":jackson-databind") + testImplementation project(":websocket") testImplementation libs.managed.netty.codec.http } diff --git a/settings.gradle b/settings.gradle index 654c0df268b..7f828cc5bce 100644 --- a/settings.gradle +++ b/settings.gradle @@ -27,6 +27,7 @@ include "core" include "core-reactive" include "core-processor" include "context" +include "discovery-core" include "function" include "function-client" include "function-web" @@ -50,6 +51,7 @@ include "jackson-databind" include "json-core" include "management" include "messaging" +include "retry" include "router" include "runtime" include "runtime-osx" diff --git a/src/main/docs/guide/appendix/breaks.adoc b/src/main/docs/guide/appendix/breaks.adoc index 211dcdaf1c5..6c2eb8e423d 100644 --- a/src/main/docs/guide/appendix/breaks.adoc +++ b/src/main/docs/guide/appendix/breaks.adoc @@ -4,6 +4,33 @@ This section documents breaking changes between Micronaut versions === Core Changes +==== Further Micronaut Modularization + +The `micronaut-runtime` module has been split into separate modules depending on the application's use case: + +* `micronaut-retry` - The retry implementation including annotations such as ann:retry.annotation.Retryable[] is now a separate module that can be optionally included in a Micronaut application. +* `micronaut-discovery-core` - The base service discovery features are now a separate module. If you application listens for events such as api:discovery.event.ServiceReadyEvent[] or api:discovery.heartbeat.HeartBeatEvent[] this module should be added to the application classpath. + +In addition, since `micronaut-retry` is now optional declarative clients annotated with ann:http.client.annotation.Client[] no longer invoke fallbacks by default. To restore the previous behaviour add `micronaut-retry` to your classpath and annotate any declarative clients with ann:retry.annotation.Recoverable[]. + +==== WebSocket No Longer Required + +The `micronaut-websocket` API is no longer a required dependency of the HTTP server. If you are using annotations such as ann:websocket.annotation.ServerWebSocket[] you should add the `micronaut-websocket` dependency to your application classpath: + +dependency::micronaut-websocket[] + +==== Reactor Instrumentation Moved to Reactor Module + +The instrumentation features for Reactor have been moved to the `micronaut-reactor` module. If you require instrumentation of reactive code paths (for distributed tracing for example) you should make sure your application depends on `micronaut-reactor`: + +dependency::micronaut-reactor[groupId="io.micronaut.reactor"] + +==== Kotlin Flow Support Moved to Kotlin Module + +Support for the Kotlin `Flow` type has been moved to the `micronaut-kotlin` module. If your application uses Kotlin `Flow` you should ensure the `micronaut-kotlin-runtime` module is on your application classpath: + +dependency::micronaut-kotlin-runtime[groupId="io.micronaut.kotlin"] + ==== Compilation Time API Split into new module In order to keep the runtime small all types and interfaces that are used at compilation time only (like the `io.micronaut.inject.ast` API) have been moved into a separate module: diff --git a/test-suite-geb/build.gradle b/test-suite-geb/build.gradle index 2ae34854e5f..625c32b37a1 100644 --- a/test-suite-geb/build.gradle +++ b/test-suite-geb/build.gradle @@ -17,5 +17,6 @@ dependencies { testImplementation project(':http') testImplementation project(':http-server-netty') - testRuntimeOnly "ch.qos.logback:logback-classic:1.2.3" + testRuntimeOnly libs.managed.logback + testImplementation project(":jackson-databind") } diff --git a/test-suite-groovy/build.gradle b/test-suite-groovy/build.gradle index 5b8cf4f1a9f..52aa5b60a62 100644 --- a/test-suite-groovy/build.gradle +++ b/test-suite-groovy/build.gradle @@ -14,6 +14,7 @@ dependencies { testImplementation project(":http-client") testImplementation project(":inject-groovy") testImplementation project(":http-server-netty") + testImplementation project(":jackson-databind") testImplementation project(":runtime") testImplementation project(":validation") testImplementation project(":inject") diff --git a/test-suite-kotlin/build.gradle b/test-suite-kotlin/build.gradle index a85f16a8d55..93f81ec3c1f 100644 --- a/test-suite-kotlin/build.gradle +++ b/test-suite-kotlin/build.gradle @@ -40,6 +40,7 @@ dependencies { testImplementation project(':validation') testImplementation project(":http-client") testImplementation project(":session") + testImplementation project(":jackson-databind") testImplementation libs.managed.groovy.templates testImplementation project(":function-client") diff --git a/test-suite-kotlin/src/test/kotlin/io/micronaut/MdcPropagationSpec.kt b/test-suite-kotlin/src/test/kotlin/io/micronaut/MdcPropagationSpec.kt deleted file mode 100644 index 6f077c61585..00000000000 --- a/test-suite-kotlin/src/test/kotlin/io/micronaut/MdcPropagationSpec.kt +++ /dev/null @@ -1,176 +0,0 @@ -package io.micronaut - -import io.micronaut.context.ApplicationContext -import io.micronaut.context.annotation.Requires -import io.micronaut.core.annotation.Introspected -import io.micronaut.core.order.Ordered -import io.micronaut.http.HttpRequest -import io.micronaut.http.HttpResponse -import io.micronaut.http.MutableHttpRequest -import io.micronaut.http.MutableHttpResponse -import io.micronaut.http.annotation.* -import io.micronaut.http.client.HttpClient -import io.micronaut.http.client.annotation.Client -import io.micronaut.http.filter.ClientFilterChain -import io.micronaut.http.filter.HttpClientFilter -import io.micronaut.http.filter.HttpServerFilter -import io.micronaut.http.filter.ServerFilterChain -import io.micronaut.http.uri.UriBuilder -import io.micronaut.runtime.server.EmbeddedServer -import jakarta.inject.Singleton -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.single -import kotlinx.coroutines.reactive.asFlow -import kotlinx.coroutines.reactive.awaitFirst -import kotlinx.coroutines.slf4j.MDCContext -import kotlinx.coroutines.withContext -import org.junit.jupiter.api.Test -import org.reactivestreams.Publisher -import org.slf4j.LoggerFactory -import org.slf4j.MDC -import reactor.core.publisher.Flux -import reactor.core.publisher.Mono -import reactor.core.publisher.toFlux -import java.net.URI -import java.util.* - -class MdcPropagationSpec { - - @Test - fun testKotlinPropagation() { - val embeddedServer = ApplicationContext.run(EmbeddedServer::class.java, - mapOf("mdc.reactortest.enabled" to "true" as Any) - ) - val client = embeddedServer.applicationContext.getBean(HttpClient::class.java) - - Flux.range(1, 1000) - .flatMap { - val tracingId = UUID.randomUUID().toString() - val get = HttpRequest.POST("http://localhost:${embeddedServer.port}/trigger", NameRequestBody("sss-" + tracingId)).header("X-TrackingId", tracingId) - client.retrieve(get, String::class.java) - } - .collectList() - .block() - - embeddedServer.stop() - } - -} - -@Requires(property = "mdc.reactortest.enabled") -@Controller -class GreetController { - - @Get("/greet") - fun greet(@QueryValue("name") name: String) : String = "Hello $name!" -} - -@Requires(property = "mdc.reactortest.enabled") -@Controller -class NamingController(private val namingService: NamingService ) { - - @Post("/trigger") - suspend fun trigger(request: HttpRequest<*>, @Body requestBody: NameRequestBody) : HttpResponse { - val trackingId = request.headers["X-TrackingId"] as String - checkTracing(trackingId) - return withContext(Dispatchers.IO + MDCContext()) { - checkTracing(trackingId) - namingService.withName(requestBody.name, trackingId) - } - } - - private fun checkTracing(trackingId: String) { - val mdcTracingId = MDC.get(TRACKING_ID) - if (trackingId != mdcTracingId) { - throw IllegalArgumentException("TrackingIds do not match! Request: $trackingId vs. Context: $mdcTracingId") - } - } -} -@Introspected -class NameRequestBody(val name: String) - -@Requires(property = "mdc.reactortest.enabled") -@Singleton -class NamingService(private val namingClient: NamingClient) { - - suspend fun withName(name: String, trackingId: String): HttpResponse { - val mdcTracingId = MDC.get(TRACKING_ID) - if (trackingId != mdcTracingId) { - throw IllegalArgumentException("TrackingIds do not match! Request: $trackingId vs. Context: $mdcTracingId") - } - return withContext(Dispatchers.IO){ - delay(50) // "forcing" the initial thread (event loop) to suspend - namingClient.getFor(name, trackingId) - } - } -} - -@Requires(property = "mdc.reactortest.enabled") -@Singleton -class NamingClient(@Client(id = "/") private val client: HttpClient) { - - suspend fun getFor(name: String, trackingId: String): HttpResponse { - val mdcTracingId = MDC.get(TRACKING_ID) - if (trackingId != mdcTracingId) { - throw IllegalArgumentException("TrackingIds do not match! Request: $trackingId vs. Context: $mdcTracingId") - } - return withContext(Dispatchers.IO) { - val uri: URI = UriBuilder.of("/greet") - .queryParam("name", name) - .build() - - val request = HttpRequest.GET(uri).apply { - header("X-TrackingId", trackingId) - } - - client.exchange(request, String::class.java).asFlow().single() - } - } -} - - -@Requires(property = "mdc.reactortest.enabled") -@Filter("/trigger") -class HttpApplicationEnterFilter : HttpServerFilter { - - private val logger = LoggerFactory.getLogger(this::class.java) - - override fun doFilter(request: HttpRequest<*>, chain: ServerFilterChain): Publisher> { - val trackingId = request.headers["X-TrackingId"] - MDC.put(TRACKING_ID, trackingId) - logger.info("Application enter ($trackingId).") - - return Mono.from(chain.proceed(request)) - .doOnNext() { - logger.info("Application exit ($trackingId).") - } - } -} - -@Requires(property = "mdc.reactortest.enabled") -@Filter("/greet") -class HttpClientFilter : HttpClientFilter { - - private val logger = LoggerFactory.getLogger(this::class.java) - - override fun getOrder(): Int { - return Ordered.HIGHEST_PRECEDENCE - } - - override fun doFilter(request: MutableHttpRequest<*>, chain: ClientFilterChain): Publisher> { - val trackingId: String = request.headers["X-TrackingId"] as String - val mdcTracingId = MDC.get(TRACKING_ID) - if (trackingId != mdcTracingId) { - throw IllegalArgumentException("TrackingIds do not match! Request: $trackingId vs. Context: $mdcTracingId") - } - return Mono.from(chain.proceed(request)) - .doOnNext { logRemoteRequestStatus(it, trackingId) } - } - - private fun logRemoteRequestStatus(response: HttpResponse<*>, trackingId: String) { - logger.info("Response Status {} was returned ({})", response.status.code, trackingId) - } -} - -const val TRACKING_ID: String = "trackingId" diff --git a/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/reactor/ReactorContextPropagationSpec.kt b/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/reactor/ReactorContextPropagationSpec.kt index 6f316b82854..49afc400023 100644 --- a/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/reactor/ReactorContextPropagationSpec.kt +++ b/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/reactor/ReactorContextPropagationSpec.kt @@ -1,6 +1,5 @@ package io.micronaut.docs.reactor -import io.micronaut.NameRequestBody import io.micronaut.context.ApplicationContext import io.micronaut.context.annotation.Requires import io.micronaut.core.annotation.Introspected @@ -103,6 +102,9 @@ class SomeService { } +@Introspected +class NameRequestBody(val name: String) + @Requires(property = "mdc.reactortestpropagation.enabled") // tag::simplefilter[] @Filter(Filter.MATCH_ALL_PATTERN) diff --git a/test-suite/build.gradle b/test-suite/build.gradle index cfb4f11f847..cc2cc16d671 100644 --- a/test-suite/build.gradle +++ b/test-suite/build.gradle @@ -29,6 +29,7 @@ dependencies { testImplementation project(":context") testImplementation libs.managed.netty.codec.http testImplementation project(":http-server-netty") + testImplementation project(":jackson-databind") testImplementation project(":http-client") testImplementation project(":validation") testImplementation project(":inject-groovy") diff --git a/validation/build.gradle b/validation/build.gradle index 35c41696ca0..5a0125b03ba 100644 --- a/validation/build.gradle +++ b/validation/build.gradle @@ -28,6 +28,7 @@ dependencies { testImplementation project(":inject") testImplementation project(":http-client") + testImplementation project(":jackson-databind") testImplementation project(":http-server-netty") testImplementation libs.managed.groovy.json testImplementation project(":inject-java-test") From b28217d5847b5e4fd0c29f01daaf95ec9f66b251 Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Mon, 31 Oct 2022 12:50:50 +0100 Subject: [PATCH 175/743] build: remove replacement not necessary for 4.0.x (#8260) --- ...eoasErrorResponseProcessorReplacement.java | 50 ------------------- 1 file changed, 50 deletions(-) delete mode 100644 http-server/src/main/java/io/micronaut/http/server/exceptions/response/HateoasErrorResponseProcessorReplacement.java diff --git a/http-server/src/main/java/io/micronaut/http/server/exceptions/response/HateoasErrorResponseProcessorReplacement.java b/http-server/src/main/java/io/micronaut/http/server/exceptions/response/HateoasErrorResponseProcessorReplacement.java deleted file mode 100644 index c0e793c44bd..00000000000 --- a/http-server/src/main/java/io/micronaut/http/server/exceptions/response/HateoasErrorResponseProcessorReplacement.java +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Copyright 2017-2022 original authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.micronaut.http.server.exceptions.response; - -import io.micronaut.context.annotation.Replaces; -import io.micronaut.context.annotation.Requires; -import io.micronaut.context.env.groovy.GroovyPropertySourceLoader; -import io.micronaut.core.annotation.NonNull; -import io.micronaut.http.MutableHttpResponse; -import io.micronaut.http.hateoas.JsonError; -import io.micronaut.json.JsonConfiguration; -import jakarta.inject.Singleton; - -/** - * @deprecated Replacement is no longer necessary for Micronaut Framework 4.0 since {@link io.micronaut.http.server.exceptions.response.HateoasErrorResponseProcessor} jackson constructor will be removed. - * @author Tim Yates - * @since 3.7.3 - */ -@Deprecated -@Singleton -@Requires(classes = GroovyPropertySourceLoader.class) -@Replaces(HateoasErrorResponseProcessor.class) -public class HateoasErrorResponseProcessorReplacement implements ErrorResponseProcessor { - - private final boolean alwaysSerializeErrorsAsList; - - public HateoasErrorResponseProcessorReplacement(JsonConfiguration jacksonConfiguration) { - this.alwaysSerializeErrorsAsList = jacksonConfiguration.isAlwaysSerializeErrorsAsList(); - } - - @Override - @NonNull - public MutableHttpResponse processResponse(@NonNull ErrorContext errorContext, - @NonNull MutableHttpResponse response) { - return HateoasErrorResponseProcessor.getJsonErrorMutableHttpResponse(alwaysSerializeErrorsAsList, errorContext, response); - } -} From 3a2882a9d3d8979e49e8de83f82249495af78dff Mon Sep 17 00:00:00 2001 From: Graeme Rocher Date: Mon, 31 Oct 2022 13:56:17 +0100 Subject: [PATCH 176/743] Port fix to 4.0.x and cleanup code (#8262) The fix for #8187 was lost in the merge. This reinstates the fix for the new location of the code. --- .../annotation/processing/ModelUtils.java | 265 +----------------- .../processing/visitor/JavaClassElement.java | 21 ++ ...SingletonWithDifferentQualifierSpec.groovy | 4 +- 3 files changed, 28 insertions(+), 262 deletions(-) diff --git a/inject-java/src/main/java/io/micronaut/annotation/processing/ModelUtils.java b/inject-java/src/main/java/io/micronaut/annotation/processing/ModelUtils.java index d067bb0bab4..0981459d9dd 100644 --- a/inject-java/src/main/java/io/micronaut/annotation/processing/ModelUtils.java +++ b/inject-java/src/main/java/io/micronaut/annotation/processing/ModelUtils.java @@ -17,35 +17,22 @@ import io.micronaut.core.annotation.Internal; import io.micronaut.core.annotation.Nullable; -import io.micronaut.core.naming.NameUtils; -import io.micronaut.inject.ast.ClassElement; import io.micronaut.inject.processing.JavaModelUtils; import javax.lang.model.element.Element; import javax.lang.model.element.ElementKind; -import javax.lang.model.element.ExecutableElement; -import javax.lang.model.element.Modifier; -import javax.lang.model.element.Name; -import javax.lang.model.element.PackageElement; import javax.lang.model.element.TypeElement; import javax.lang.model.type.DeclaredType; import javax.lang.model.type.TypeKind; import javax.lang.model.type.TypeMirror; -import javax.lang.model.util.ElementFilter; import javax.lang.model.util.Elements; import javax.lang.model.util.Types; -import java.util.List; import java.util.Optional; import java.util.Set; import java.util.SortedSet; import java.util.TreeSet; -import static javax.lang.model.element.Modifier.ABSTRACT; -import static javax.lang.model.element.Modifier.FINAL; -import static javax.lang.model.element.Modifier.PRIVATE; -import static javax.lang.model.element.Modifier.PROTECTED; -import static javax.lang.model.element.Modifier.PUBLIC; -import static javax.lang.model.element.Modifier.STATIC; +import static javax.lang.model.element.Modifier.*; import static javax.lang.model.type.TypeKind.NONE; /** @@ -86,120 +73,12 @@ public Types getTypeUtils() { while (element != null && !(JavaModelUtils.isClassOrInterface(element) || JavaModelUtils.isRecord(element) || JavaModelUtils.isEnum(element))) { element = element.getEnclosingElement(); } - if (element instanceof TypeElement) { - return (TypeElement) element; + if (element instanceof TypeElement e) { + return e; } return null; } - /** - * The binary name of the type as a String. - * - * @param typeElement The type element - * @return The class name - */ - String simpleBinaryNameFor(TypeElement typeElement) { - Name elementBinaryName = elementUtils.getBinaryName(typeElement); - PackageElement packageElement = elementUtils.getPackageOf(typeElement); - - String packageName = packageElement.getQualifiedName().toString(); - return elementBinaryName.toString().replaceFirst(packageName + "\\.", ""); - } - - /** - * Resolves a setter method for a field. - * - * @param field The field - * @return An optional setter method - */ - Optional findGetterMethodFor(Element field) { - // FIXME refine this to discover one of possible overloaded methods with correct signature (i.e. single arg of field type) - TypeElement typeElement = classElementFor(field); - if (typeElement == null) { - return Optional.empty(); - } - - String getterName = getterNameFor(field); - List elements = typeElement.getEnclosedElements(); - List methods = ElementFilter.methodsIn(elements); - return methods.stream() - .filter(method -> { - String methodName = method.getSimpleName().toString(); - if (getterName.equals(methodName)) { - Set modifiers = method.getModifiers(); - return - // it's not static - !modifiers.contains(STATIC) - // it's either public or package visibility - && modifiers.contains(PUBLIC) - || !(modifiers.contains(PRIVATE) || modifiers.contains(PROTECTED)); - } - return false; - }) - .findFirst(); - } - - /** - * Resolves a setter method for a field. - * - * @param field The field - * @return An optional setter method - */ - Optional findSetterMethodFor(Element field) { - String name = field.getSimpleName().toString(); - if (field.asType().getKind() == TypeKind.BOOLEAN && name.length() > 2 && Character.isUpperCase(name.charAt(2))) { - name = name.replaceFirst("^(is)(.+)", "$2"); - } - // FIXME refine this to discover one of possible overloaded methods with correct signature (i.e. single arg of field type) - TypeElement typeElement = classElementFor(field); - if (typeElement == null) { - return Optional.empty(); - } - - String setterName = setterNameFor(name); - List elements = typeElement.getEnclosedElements(); - List methods = ElementFilter.methodsIn(elements); - return methods.stream() - .filter(method -> { - String methodName = method.getSimpleName().toString(); - if (setterName.equals(methodName)) { - Set modifiers = method.getModifiers(); - return - // it's not static - !modifiers.contains(STATIC) - // it's either public or package visibility - && modifiers.contains(PUBLIC) - || !(modifiers.contains(PRIVATE) || modifiers.contains(PROTECTED)); - } - return false; - }) - .findFirst(); - } - - /** - * The name of a getter for the given field. - * - * @param field The field in question - * @return The getter name - */ - String getterNameFor(Element field) { - String methodNamePrefix = "get"; - if (field.asType().getKind() == TypeKind.BOOLEAN) { - methodNamePrefix = "is"; - } - return methodNamePrefix + NameUtils.capitalize(field.getSimpleName().toString()); - } - - /** - * The name of a setter for the given field name. - * - * @param fieldName The field name - * @return The setter name - */ - String setterNameFor(String fieldName) { - return "set" + NameUtils.capitalize(fieldName); - } - /** * Return whether the given element is the java.lang.Object class. * @@ -267,19 +146,6 @@ TypeMirror resolveTypeReference(TypeMirror type) { } } - /** - * Returns whether an element is package private. - * - * @param element The element - * @return True if it is package provide - */ - public boolean isPackagePrivate(Element element) { - Set modifiers = element.getModifiers(); - return !(modifiers.contains(PUBLIC) - || modifiers.contains(PROTECTED) - || modifiers.contains(PRIVATE)); - } - /** * @param aClass A class * @return All the interfaces @@ -306,8 +172,7 @@ public Set getAllInterfaces(TypeElement aClass) { private Set populateInterfaces(TypeElement aClass, Set interfaces) { for (TypeMirror anInterface : aClass.getInterfaces()) { final Element e = typeUtils.asElement(anInterface); - if (e instanceof TypeElement) { - final TypeElement te = (TypeElement) e; + if (e instanceof TypeElement te) { if (!interfaces.contains(te)) { interfaces.add(te); populateInterfaces(te, interfaces); @@ -318,8 +183,7 @@ private Set populateInterfaces(TypeElement aClass, Set TypeMirror superclass = aClass.getSuperclass(); while (superclass != null) { final Element e = typeUtils.asElement(superclass); - if (e instanceof TypeElement) { - TypeElement superTypeElement = (TypeElement) e; + if (e instanceof TypeElement superTypeElement) { populateInterfaces(superTypeElement, interfaces); superclass = superTypeElement.getSuperclass(); } else { @@ -330,104 +194,6 @@ private Set populateInterfaces(TypeElement aClass, Set return interfaces; } - /** - * Return whether the given method or field is inherited but not public. - * - * @param concreteClass The concrete class - * @param declaringClass The declaring class of the field - * @param methodOrField The method or field - * @return True if it is inherited and not public - */ - boolean isInheritedAndNotPublic(TypeElement concreteClass, TypeElement declaringClass, Element methodOrField) { - PackageElement packageOfDeclaringClass = elementUtils.getPackageOf(declaringClass); - PackageElement packageOfConcreteClass = elementUtils.getPackageOf(concreteClass); - - return declaringClass != concreteClass && - !packageOfDeclaringClass.getQualifiedName().equals(packageOfConcreteClass.getQualifiedName()) - && (isProtected(methodOrField) || !isPublic(methodOrField)); - } - - /** - * Return whether the given method or field is inherited but not public. - * - * @param concreteClass The concrete class - * @param declaringClass The declaring class of the field - * @param methodOrField The method or field - * @return True if it is inherited and not public - */ - boolean isInheritedAndNotPublic(ClassElement concreteClass, ClassElement declaringClass, io.micronaut.inject.ast.Element methodOrField) { - String packageOfDeclaringClass = declaringClass.getPackageName(); - String packageOfConcreteClass = concreteClass.getPackageName(); - - return declaringClass != concreteClass && - !packageOfDeclaringClass.equals(packageOfConcreteClass) - && (methodOrField.isProtected() || !methodOrField.isPublic()); - } - - /** - * Tests if candidate method is overridden from a given class or subclass. - * - * @param overridden the candidate overridden method - * @param classElement the type element that may contain the overriding method, either directly or in a subclass - * @param strict Whether to use strict checks for overriding and not include logic to handle method overloading - * @return the overriding method - */ - public Optional overridingOrHidingMethod(ExecutableElement overridden, TypeElement classElement, boolean strict) { - List methods = ElementFilter.methodsIn(elementUtils.getAllMembers(classElement)); - for (ExecutableElement method : methods) { - if (strict) { - if (elementUtils.overrides(method, overridden, classElement)) { - return Optional.of(method); - } - } else { - if (!method.equals(overridden) && - method.getSimpleName().equals(overridden.getSimpleName())) { - return Optional.of(method); - } - } - - } - // might be looking for a package private & packages differ method in a superclass - // that is not visible to the most concrete subclass, really! - // e.g. see injectPackagePrivateMethod4() for SpareTire -> Tire -> RoundThing in Inject tck - // check the superclass until we reach Object, then bail out with empty if necessary. - TypeElement superClass = superClassFor(classElement); - if (superClass != null && !isObjectClass(superClass)) { - return overridingOrHidingMethod(overridden, superClass, strict); - } - return Optional.empty(); - } - - /** - * Return whether the element is private. - * - * @param element The element - * @return True if it is private - */ - boolean isPrivate(Element element) { - return element.getModifiers().contains(PRIVATE); - } - - /** - * Return whether the element is protected. - * - * @param element The element - * @return True if it is protected - */ - boolean isProtected(Element element) { - return element.getModifiers().contains(PROTECTED); - } - - /** - * Return whether the element is public. - * - * @param element The element - * @return True if it is public - */ - boolean isPublic(Element element) { - return element.getModifiers().contains(PUBLIC); - } - /** * Return whether the element is abstract. * @@ -448,27 +214,6 @@ boolean isStatic(Element element) { return element.getModifiers().contains(STATIC); } - - /** - * Return whether the element is final. - * - * @param element The element - * @return True if it is final - */ - boolean isFinal(Element element) { - return element.getModifiers().contains(FINAL); - } - - /** - * Is the given type mirror an optional. - * - * @param mirror The mirror - * @return True if it is - */ - boolean isOptional(TypeMirror mirror) { - return typeUtils.erasure(mirror).toString().equals(Optional.class.getName()); - } - /** * The Java APT throws an internal exception {code com.sun.tools.javac.code.Symbol$CompletionFailure} if a class is missing from the classpath and {@link Element#getKind()} is called. This method * handles exceptions when calling the getKind() method to avoid this scenario and should be used instead of {@link Element#getKind()}. diff --git a/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaClassElement.java b/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaClassElement.java index d00e8db3977..b63c7af00ad 100644 --- a/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaClassElement.java +++ b/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaClassElement.java @@ -28,6 +28,7 @@ import io.micronaut.inject.ast.BeanPropertiesQuery; import io.micronaut.inject.ast.ClassElement; import io.micronaut.inject.ast.ConstructorElement; +import io.micronaut.inject.ast.ParameterElement; import io.micronaut.inject.ast.annotation.ElementAnnotationMetadataFactory; import io.micronaut.inject.ast.ElementQuery; import io.micronaut.inject.ast.FieldElement; @@ -45,6 +46,7 @@ import javax.lang.model.element.ElementKind; import javax.lang.model.element.ExecutableElement; import javax.lang.model.element.Modifier; +import javax.lang.model.element.RecordComponentElement; import javax.lang.model.element.TypeElement; import javax.lang.model.element.TypeParameterElement; import javax.lang.model.element.VariableElement; @@ -560,6 +562,7 @@ private boolean isAssignable(TypeElement otherElement) { @NonNull @Override + @SuppressWarnings("java:S1119") public Optional getPrimaryConstructor() { if (JavaModelUtils.isRecord(classElement)) { Optional staticCreator = findStaticCreator(); @@ -578,6 +581,24 @@ public Optional getPrimaryConstructor() { return annotatedConstructor.map(c -> c); } // with records the record constructor is always the last constructor + List recordComponents = classElement.getRecordComponents(); + constructorSearch: for (ConstructorElement constructor : constructors) { + ParameterElement[] parameters = constructor.getParameters(); + if (parameters.length == recordComponents.size()) { + for (int i = 0; i < parameters.length; i++) { + ParameterElement parameter = parameters[i]; + RecordComponentElement rce = recordComponents.get(i); + VariableElement ve = (VariableElement) parameter.getNativeType(); + TypeMirror leftType = visitorContext.getTypes().erasure(ve.asType()); + TypeMirror rightType = visitorContext.getTypes().erasure(rce.asType()); + if (!leftType.equals(rightType)) { + // types don't match, continue searching constructors + continue constructorSearch; + } + } + return Optional.of(constructor); + } + } return Optional.of(constructors.get(constructors.size() - 1)); } return ArrayableClassElement.super.getPrimaryConstructor(); diff --git a/inject-java/src/test/groovy/io/micronaut/inject/context/register/RegisterASingletonWithDifferentQualifierSpec.groovy b/inject-java/src/test/groovy/io/micronaut/inject/context/register/RegisterASingletonWithDifferentQualifierSpec.groovy index 8606f4ab50f..889abcc335a 100644 --- a/inject-java/src/test/groovy/io/micronaut/inject/context/register/RegisterASingletonWithDifferentQualifierSpec.groovy +++ b/inject-java/src/test/groovy/io/micronaut/inject/context/register/RegisterASingletonWithDifferentQualifierSpec.groovy @@ -1,6 +1,6 @@ package io.micronaut.inject.context.register - +import io.micronaut.context.BeanContext import io.micronaut.context.DefaultBeanContext import io.micronaut.inject.qualifiers.Qualifiers import spock.lang.Specification @@ -9,7 +9,7 @@ class RegisterASingletonWithDifferentQualifierSpec extends Specification { def "test registering the same singleton class with a different qualifier"() { given: - def ctx = DefaultBeanContext.run() + def ctx = BeanContext.run() when: def q1 = ctx.createBean(Abc, Qualifiers.none()) From e602f1dc04301848c2f241e233e10efa840784f4 Mon Sep 17 00:00:00 2001 From: Graeme Rocher Date: Mon, 31 Oct 2022 14:07:40 +0100 Subject: [PATCH 177/743] disable test for externalized converters --- .../io/micronaut/docs/streaming/HeadlineFlowControllerSpec.kt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/streaming/HeadlineFlowControllerSpec.kt b/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/streaming/HeadlineFlowControllerSpec.kt index 8c98d6b2387..a8fe98a5d31 100644 --- a/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/streaming/HeadlineFlowControllerSpec.kt +++ b/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/streaming/HeadlineFlowControllerSpec.kt @@ -3,6 +3,7 @@ package io.micronaut.docs.streaming import io.kotest.matchers.shouldBe import io.kotest.matchers.shouldNotBe import io.kotest.assertions.throwables.shouldThrowExactly +import io.kotest.core.annotation.Ignored import io.kotest.core.spec.style.StringSpec import io.kotest.matchers.string.shouldStartWith import io.micronaut.context.ApplicationContext @@ -14,6 +15,9 @@ import io.micronaut.runtime.server.EmbeddedServer import kotlinx.coroutines.flow.take import kotlinx.coroutines.flow.toList +// Flow converters moved to Kotlin Module re-enable once +// new version of micronaut-kotlin-runtime is published +@Ignored class HeadlineFlowControllerSpec: StringSpec() { val embeddedServer = autoClose( From 85ffc39fa7950dbce5ab5fb9eb0d430ac9ef8c5e Mon Sep 17 00:00:00 2001 From: Graeme Rocher Date: Mon, 31 Oct 2022 14:46:32 +0100 Subject: [PATCH 178/743] disable predictive test selection temporarily --- gradle.properties | 1 + 1 file changed, 1 insertion(+) diff --git a/gradle.properties b/gradle.properties index fd48e36e711..f66c02ef0bc 100644 --- a/gradle.properties +++ b/gradle.properties @@ -57,3 +57,4 @@ micronautSecurityVersion=3.8.1 org.gradle.caching=true org.gradle.parallel=true org.gradle.jvmargs=-Xmx1g +predictiveTestSelection=false From 9f1ff514b7ded56d129506819a0f0bf3c10d6780 Mon Sep 17 00:00:00 2001 From: Graeme Rocher Date: Tue, 1 Nov 2022 08:46:57 +0100 Subject: [PATCH 179/743] build: Use serialization and reactor snapshots (#8266) --- bom/build.gradle | 5 +++++ function/build.gradle | 1 + .../io/micronaut/function/executor/TestFunctionFactory.java | 2 +- gradle.properties | 1 + gradle/libs.versions.toml | 4 ++-- 5 files changed, 10 insertions(+), 3 deletions(-) diff --git a/bom/build.gradle b/bom/build.gradle index 3fb8269bca8..0faeef2d243 100644 --- a/bom/build.gradle +++ b/bom/build.gradle @@ -5,6 +5,11 @@ plugins { group projectGroupId version projectVersion +repositories { + mavenCentral() + maven { url "https://s01.oss.sonatype.org/content/repositories/snapshots/" } +} + micronautBom { extraExcludedProjects = [ "benchmarks", diff --git a/function/build.gradle b/function/build.gradle index 7f9eca8d4e5..12b6d2537a5 100644 --- a/function/build.gradle +++ b/function/build.gradle @@ -9,4 +9,5 @@ dependencies { api project(":http") testAnnotationProcessor project(":inject-java") + testImplementation project(":jackson-databind") } diff --git a/function/src/test/groovy/io/micronaut/function/executor/TestFunctionFactory.java b/function/src/test/groovy/io/micronaut/function/executor/TestFunctionFactory.java index 329f280b667..893cab5b90e 100644 --- a/function/src/test/groovy/io/micronaut/function/executor/TestFunctionFactory.java +++ b/function/src/test/groovy/io/micronaut/function/executor/TestFunctionFactory.java @@ -40,7 +40,7 @@ Supplier get() { // it is an AOP proxy @FunctionBean("round") Function round() { - return (doub) -> Math.round(doub.doubleValue()); + return Math::round; } @FunctionBean("upper") diff --git a/gradle.properties b/gradle.properties index f66c02ef0bc..bafeb3385ea 100644 --- a/gradle.properties +++ b/gradle.properties @@ -57,4 +57,5 @@ micronautSecurityVersion=3.8.1 org.gradle.caching=true org.gradle.parallel=true org.gradle.jvmargs=-Xmx1g +systemProp.predictiveTestSelection=false predictiveTestSelection=false diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 62d1053505f..5b03d335505 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -110,14 +110,14 @@ managed-micronaut-picocli = "4.3.0" managed-micronaut-problem = "2.5.1" managed-micronaut-rabbitmq = "3.3.0" managed-micronaut-r2dbc = "4.0.0" -managed-micronaut-reactor = "2.4.1" +managed-micronaut-reactor = "3.0.0-SNAPSHOT" managed-micronaut-redis = "5.3.1" managed-micronaut-rss = "3.2.0" managed-micronaut-rxjava1 = "1.0.0" managed-micronaut-rxjava2 = "1.3.0" managed-micronaut-rxjava3 = "2.3.0" managed-micronaut-security = "3.8.1" -managed-micronaut-serialization = "1.3.2" +managed-micronaut-serialization = "2.0.0-SNAPSHOT" managed-micronaut-servlet = "3.3.2" managed-micronaut-spring = "4.3.1" managed-micronaut-sql = "4.7.2" From 826940e192e4b2959f0ce88f1c7b4c7ad8c40368 Mon Sep 17 00:00:00 2001 From: Tim Yates Date: Tue, 1 Nov 2022 13:22:16 +0000 Subject: [PATCH 180/743] Mark HateoasErrorResponseProcessorReplacement as Secondary (#8265) * Mark HateoasErrorResponseProcessorReplacement as Secondary Currently in a Groovy project, if you pull in problem-json, you end up with two ErrorResponseProcessors. One for Problem-json and the other for Hateoas. Marking the replacement as Secondary (as the HateoasErrorResponseProcessor was) fixes this * Add a test --- ...eoasErrorResponseProcessorReplacement.java | 2 + .../ErrorResponseProcessorSpec.groovy | 53 +++++++++++++++++++ 2 files changed, 55 insertions(+) create mode 100644 test-suite-groovy/src/test/groovy/io/micronaut/http/server/exceptions/response/ErrorResponseProcessorSpec.groovy diff --git a/http-server/src/main/java/io/micronaut/http/server/exceptions/response/HateoasErrorResponseProcessorReplacement.java b/http-server/src/main/java/io/micronaut/http/server/exceptions/response/HateoasErrorResponseProcessorReplacement.java index c0e793c44bd..8510724ec9d 100644 --- a/http-server/src/main/java/io/micronaut/http/server/exceptions/response/HateoasErrorResponseProcessorReplacement.java +++ b/http-server/src/main/java/io/micronaut/http/server/exceptions/response/HateoasErrorResponseProcessorReplacement.java @@ -17,6 +17,7 @@ import io.micronaut.context.annotation.Replaces; import io.micronaut.context.annotation.Requires; +import io.micronaut.context.annotation.Secondary; import io.micronaut.context.env.groovy.GroovyPropertySourceLoader; import io.micronaut.core.annotation.NonNull; import io.micronaut.http.MutableHttpResponse; @@ -31,6 +32,7 @@ */ @Deprecated @Singleton +@Secondary @Requires(classes = GroovyPropertySourceLoader.class) @Replaces(HateoasErrorResponseProcessor.class) public class HateoasErrorResponseProcessorReplacement implements ErrorResponseProcessor { diff --git a/test-suite-groovy/src/test/groovy/io/micronaut/http/server/exceptions/response/ErrorResponseProcessorSpec.groovy b/test-suite-groovy/src/test/groovy/io/micronaut/http/server/exceptions/response/ErrorResponseProcessorSpec.groovy new file mode 100644 index 00000000000..31e8b8a723e --- /dev/null +++ b/test-suite-groovy/src/test/groovy/io/micronaut/http/server/exceptions/response/ErrorResponseProcessorSpec.groovy @@ -0,0 +1,53 @@ +package io.micronaut.http.server.exceptions.response + +import io.micronaut.context.ApplicationContext +import io.micronaut.context.annotation.Requires +import io.micronaut.core.annotation.NonNull +import io.micronaut.http.HttpResponse +import io.micronaut.http.MutableHttpResponse +import spock.lang.Specification +import jakarta.inject.Singleton + +class ErrorResponseProcessorSpec extends Specification { + + def "by default you get a hateoas replacement under groovy"() { + given: + def ctx = ApplicationContext.run() + + when: + def bean = ctx.getBean(ErrorResponseProcessor) + + then: + bean instanceof HateoasErrorResponseProcessorReplacement + + cleanup: + ctx.close() + } + + def "default can simply be replaced by binding a different processor"() { + given: + def ctx = ApplicationContext.run( + 'spec.name': 'CustomErrorResponseProcessor' + ) + + when: + def bean = ctx.getBean(ErrorResponseProcessor) + + then: + bean instanceof CustomErrorResponseProcessor + + cleanup: + ctx.close() + } + + @Singleton + @Requires(property = 'spec.name', value = 'CustomErrorResponseProcessor') + static class CustomErrorResponseProcessor implements ErrorResponseProcessor { + + @NonNull + @Override + MutableHttpResponse processResponse(@NonNull ErrorContext errorContext, @NonNull MutableHttpResponse baseResponse) { + return HttpResponse.ok("test") + } + } +} From 3f21090bb21d14ef9411a1bfb50a99aade788d6b Mon Sep 17 00:00:00 2001 From: Tim Yates Date: Tue, 1 Nov 2022 15:23:15 +0000 Subject: [PATCH 181/743] Switch to using snapshot of micronaut-groovy (#8268) * Switch to using snapshot of micronaut-groovy so we stop pulling old groovy in to the javadoc classpath See https://ge.micronaut.io/s/pauiqojum325c/failure\#1 for example failure * Allow GORM removal --- bom/build.gradle | 8 ++++++++ build.gradle | 7 ++++++- gradle/libs.versions.toml | 2 +- http-server/build.gradle | 5 +++++ 4 files changed, 20 insertions(+), 2 deletions(-) diff --git a/bom/build.gradle b/bom/build.gradle index 0faeef2d243..3a6dac0ff0c 100644 --- a/bom/build.gradle +++ b/bom/build.gradle @@ -57,5 +57,13 @@ micronautBom { // The R2DBC bom that we include mentions dependencies which do not belong to io.r2dbc group bomAuthorizedGroupIds.put("io.r2dbc:r2dbc-bom", ["com.google.cloud", "com.oracle.database.r2dbc", "org.mariadb", "dev.miku"] as Set) + + // No GORM until it supports Groovy 4 + acceptedLibraryRegressions.add("micronaut-multitenancy-gorm") + acceptedLibraryRegressions.add("micronaut-mongo-gorm") + acceptedLibraryRegressions.add("micronaut-gorm-common") + acceptedLibraryRegressions.add("micronaut-hibernate-gorm") + acceptedLibraryRegressions.add("micronaut-graphql-gorm") + acceptedLibraryRegressions.add("micronaut-neo4j-gorm") } } diff --git a/build.gradle b/build.gradle index 2ecffbae083..8469a250e77 100644 --- a/build.gradle +++ b/build.gradle @@ -5,6 +5,11 @@ plugins { id "io.micronaut.build.internal.convention-quality" } +repositories { + mavenCentral() + maven { url "https://s01.oss.sonatype.org/content/repositories/snapshots/" } +} + tasks.named("updateVersionCatalogs") { // we set the list to empty because we accept upgrades which improve the status rejectedQualifiers = [] @@ -31,4 +36,4 @@ if (System.getenv("SONAR_TOKEN") != null) { property "sonar.exclusions", coverageExcludes.join(",") } } -} \ No newline at end of file +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 5b03d335505..9da82228711 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -84,7 +84,7 @@ managed-micronaut-email = "1.4.0" managed-micronaut-flyway = "5.4.1" managed-micronaut-gcp = "4.6.0" managed-micronaut-graphql = "3.2.0" -managed-micronaut-groovy = "3.3.1" +managed-micronaut-groovy = "4.0.0-SNAPSHOT" managed-micronaut-grpc = "3.3.1" managed-micronaut-hibernate-validator = "3.2.0" managed-micronaut-ignite = "1.0.0.RC1" diff --git a/http-server/build.gradle b/http-server/build.gradle index 3103584d3ca..3752465053e 100644 --- a/http-server/build.gradle +++ b/http-server/build.gradle @@ -2,6 +2,11 @@ plugins { id "io.micronaut.build.internal.convention-library" } +repositories { + mavenCentral() + maven { url "https://s01.oss.sonatype.org/content/repositories/snapshots/" } +} + dependencies { api project(":http") api project(":router") From 24b582f6d60dbd09d75439adf4e8327a69ecdfbd Mon Sep 17 00:00:00 2001 From: Denis Stepanov Date: Wed, 2 Nov 2022 16:19:20 +0700 Subject: [PATCH 182/743] Fix querying generic arguments (#8267) --- .../groovy/visitor/GroovyClassElement.java | 2 +- .../inject/visitor/ClassElementSpec.groovy | 75 +++++++++++++++++++ .../processing/visitor/JavaClassElement.java | 13 +++- .../visitors/ClassElementSpec.groovy | 72 ++++++++++++++++++ 4 files changed, 158 insertions(+), 4 deletions(-) diff --git a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyClassElement.java b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyClassElement.java index 953f6858e46..f7ca7600660 100644 --- a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyClassElement.java +++ b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyClassElement.java @@ -661,7 +661,7 @@ public boolean isAbstract() { @Override public boolean isStatic() { // I assume Groovy can decide not to make the class static internally - // and isStaticClass will be false even if the class has static modifier + // and isStaticClass will be false even if the class has a static modifier return classNode.isStaticClass() || Modifier.isStatic(classNode.getModifiers()); } diff --git a/inject-groovy/src/test/groovy/io/micronaut/inject/visitor/ClassElementSpec.groovy b/inject-groovy/src/test/groovy/io/micronaut/inject/visitor/ClassElementSpec.groovy index ea4190c06f4..f728162483b 100644 --- a/inject-groovy/src/test/groovy/io/micronaut/inject/visitor/ClassElementSpec.groovy +++ b/inject-groovy/src/test/groovy/io/micronaut/inject/visitor/ClassElementSpec.groovy @@ -928,4 +928,79 @@ class SuccessfulTest extends AbstractExample { props[1].name.contains "dummy" props[2].name.contains "sharedCtx" } + + void "test fields selection"() { + given: + ClassElement classElement = buildClassElement('test.PetOperations', ''' +package test + +import groovy.transform.PackageScope; +import io.micronaut.http.annotation.*; +import jakarta.inject.Inject; + +@Controller("/pets") +interface PetOperations { + + @Post("/") + T save(String name, int age); +} + +class Pet { + public int pub; + + private String prvn; + + protected String protectme; + + @PackageScope + String packprivme; + + public static String PUB_CONST; + + private static String PRV_CONST; + + protected static String PROT_CONST; + + @PackageScope + static String PACK_PRV_CONST; +} + +''') + when: + List publicFields = classElement.getFirstTypeArgument() + .get() + .getEnclosedElements(ElementQuery.ALL_FIELDS.modifiers(mods -> mods.contains(ElementModifier.PUBLIC) && mods.size() == 1)) + then: + publicFields.size() == 1 + publicFields.stream().map(FieldElement::getName).toList() == ["pub"] + + when: + List publicFields2 = classElement.getFirstTypeArgument() + .get() + .getEnclosedElements(ElementQuery.ALL_FIELDS.filter(e -> e.isPublic())) + then: + publicFields2.size() == 2 + publicFields2.stream().map(FieldElement::getName).toList() == ["pub", "PUB_CONST"] + when: + List protectedFields = classElement.getFirstTypeArgument() + .get() + .getEnclosedElements(ElementQuery.ALL_FIELDS.filter(e -> e.isProtected())) + then: + protectedFields.size() == 2 + protectedFields.stream().map(FieldElement::getName).toList() == ["protectme", "PROT_CONST"] + when: + List privateFields = classElement.getFirstTypeArgument() + .get() + .getEnclosedElements(ElementQuery.ALL_FIELDS.filter(e -> e.isPrivate())) + then: + privateFields.size() == 2 + privateFields.stream().map(FieldElement::getName).toList() == ["prvn", "PRV_CONST"] + when: + List packPrvFields = classElement.getFirstTypeArgument() + .get() + .getEnclosedElements(ElementQuery.ALL_FIELDS.filter(e -> e.isPackagePrivate())) + then: + packPrvFields.size() == 2 + packPrvFields.stream().map(FieldElement::getName).toList() == ["packprivme", "PACK_PRV_CONST"] + } } diff --git a/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaClassElement.java b/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaClassElement.java index b63c7af00ad..56e216bae51 100644 --- a/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaClassElement.java +++ b/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaClassElement.java @@ -466,10 +466,17 @@ private boolean isKotlinClass(Element element) { @Override public List getEnclosedElements(@NonNull ElementQuery query) { - if (getNativeType() instanceof TypeElement) { - return enclosedElementsQuery.getEnclosedElements(this, query); + ClassElement classElementToInspect; + if (this instanceof GenericPlaceholderElement genericPlaceholderElement) { + List bounds = genericPlaceholderElement.getBounds(); + if (bounds.isEmpty()) { + return Collections.emptyList(); + } + classElementToInspect = bounds.get(0); + } else { + classElementToInspect = this; } - return Collections.emptyList(); + return enclosedElementsQuery.getEnclosedElements(classElementToInspect, query); } @Override diff --git a/inject-java/src/test/groovy/io/micronaut/visitors/ClassElementSpec.groovy b/inject-java/src/test/groovy/io/micronaut/visitors/ClassElementSpec.groovy index 6066b0d5789..d82ce8e427c 100644 --- a/inject-java/src/test/groovy/io/micronaut/visitors/ClassElementSpec.groovy +++ b/inject-java/src/test/groovy/io/micronaut/visitors/ClassElementSpec.groovy @@ -1391,6 +1391,78 @@ public class TestController { AllElementsVisitor.VISITED_METHOD_ELEMENTS[0].declaringType.name == 'test.TestController' } + void "test fields selection"() { + given: + ClassElement classElement = buildClassElement(''' +package test; + +import io.micronaut.http.annotation.*; +import jakarta.inject.Inject; + +@Controller("/pets") +interface PetOperations { + + @Post("/") + T save(String name, int age); +} + +class Pet { + public int pub; + + private String prvn; + + protected String protectme; + + String packprivme; + + public static String PUB_CONST; + + private static String PRV_CONST; + + protected static String PROT_CONST; + + static String PACK_PRV_CONST; +} + +''') + when: + List publicFields = classElement.getFirstTypeArgument() + .get() + .getEnclosedElements(ElementQuery.ALL_FIELDS.modifiers(mods -> mods.contains(ElementModifier.PUBLIC) && mods.size() == 1)) + then: + publicFields.size() == 1 + publicFields.stream().map(FieldElement::getName).toList() == ["pub"] + + when: + List publicFields2 = classElement.getFirstTypeArgument() + .get() + .getEnclosedElements(ElementQuery.ALL_FIELDS.filter(e -> e.isPublic())) + then: + publicFields2.size() == 2 + publicFields2.stream().map(FieldElement::getName).toList() == ["pub", "PUB_CONST"] + when: + List protectedFields = classElement.getFirstTypeArgument() + .get() + .getEnclosedElements(ElementQuery.ALL_FIELDS.filter(e -> e.isProtected())) + then: + protectedFields.size() == 2 + protectedFields.stream().map(FieldElement::getName).toList() == ["protectme", "PROT_CONST"] + when: + List privateFields = classElement.getFirstTypeArgument() + .get() + .getEnclosedElements(ElementQuery.ALL_FIELDS.filter(e -> e.isPrivate())) + then: + privateFields.size() == 2 + privateFields.stream().map(FieldElement::getName).toList() == ["prvn", "PRV_CONST"] + when: + List packPrvFields = classElement.getFirstTypeArgument() + .get() + .getEnclosedElements(ElementQuery.ALL_FIELDS.filter(e -> e.isPackagePrivate())) + then: + packPrvFields.size() == 2 + packPrvFields.stream().map(FieldElement::getName).toList() == ["packprivme", "PACK_PRV_CONST"] + } + private void assertMethodsByName(List allMethods, String name, List expectedDeclaringTypeSimpleNames) { Collection methods = collectElements(allMethods, name) assert expectedDeclaringTypeSimpleNames.size() == methods.size() From 3f750b9b8b83d182893fb49f0378d7052eb1eb57 Mon Sep 17 00:00:00 2001 From: Denis Stepanov Date: Wed, 2 Nov 2022 16:20:37 +0700 Subject: [PATCH 183/743] Fix configuration classes with optional getters (#8269) --- .../io/micronaut/inject/ast/ClassElement.java | 15 ++++- .../ast/utils/AstBeanPropertiesUtils.java | 2 +- ...ConfigurationReaderBeanElementCreator.java | 3 +- .../groovy/visitor/GroovyClassElement.java | 20 ++++++ .../ConfigurationPropertiesSpec.groovy | 19 ++++++ .../OptionalProperties.groovy | 44 +++++++++++++ .../beans/BeanIntrospectionSpec.groovy | 64 +++++++++++++++---- .../processing/visitor/JavaClassElement.java | 20 ++++++ .../ConfigurationPropertiesSpec.groovy | 19 ++++++ .../configproperties/OptionalProperties.java | 49 ++++++++++++++ 10 files changed, 240 insertions(+), 15 deletions(-) create mode 100644 inject-groovy/src/test/groovy/io/micronaut/inject/configproperties/OptionalProperties.groovy create mode 100644 inject-java/src/test/groovy/io/micronaut/inject/configproperties/OptionalProperties.java diff --git a/core-processor/src/main/java/io/micronaut/inject/ast/ClassElement.java b/core-processor/src/main/java/io/micronaut/inject/ast/ClassElement.java index 9a1c8d43eaf..3895707c9ed 100644 --- a/core-processor/src/main/java/io/micronaut/inject/ast/ClassElement.java +++ b/core-processor/src/main/java/io/micronaut/inject/ast/ClassElement.java @@ -39,6 +39,9 @@ import java.util.Map; import java.util.Objects; import java.util.Optional; +import java.util.OptionalDouble; +import java.util.OptionalInt; +import java.util.OptionalLong; import java.util.function.Function; import java.util.stream.Collectors; @@ -115,7 +118,17 @@ default boolean isAssignable(ClassElement type) { * @since 2.3.0 */ default boolean isOptional() { - return isAssignable(Optional.class); + return isAssignable(Optional.class) || isAssignable(OptionalLong.class) || isAssignable(OptionalDouble.class) || isAssignable(OptionalInt.class); + } + + /** + * Gets optional value type. + * + * @return the value type + * @since 4.0.0 + */ + default Optional getOptionalValueType() { + return Optional.empty(); } /** diff --git a/core-processor/src/main/java/io/micronaut/inject/ast/utils/AstBeanPropertiesUtils.java b/core-processor/src/main/java/io/micronaut/inject/ast/utils/AstBeanPropertiesUtils.java index ff2257417dd..ee27f6339e6 100644 --- a/core-processor/src/main/java/io/micronaut/inject/ast/utils/AstBeanPropertiesUtils.java +++ b/core-processor/src/main/java/io/micronaut/inject/ast/utils/AstBeanPropertiesUtils.java @@ -253,7 +253,7 @@ private static void processSetter(Map props, MethodEle private static ClassElement unwrapType(ClassElement type) { if (type.isOptional()) { - return type.getFirstTypeArgument().orElse(type); + return type.getOptionalValueType().orElse(type); } return type; } diff --git a/core-processor/src/main/java/io/micronaut/inject/processing/ConfigurationReaderBeanElementCreator.java b/core-processor/src/main/java/io/micronaut/inject/processing/ConfigurationReaderBeanElementCreator.java index cff8e1fe5e7..e8f70057772 100644 --- a/core-processor/src/main/java/io/micronaut/inject/processing/ConfigurationReaderBeanElementCreator.java +++ b/core-processor/src/main/java/io/micronaut/inject/processing/ConfigurationReaderBeanElementCreator.java @@ -124,7 +124,8 @@ private void processConfigurationConstructorParameter(ParameterElement parameter private boolean isPropertyParameter(ParameterElement parameter) { ClassElement parameterType = parameter.getGenericType(); if (parameterType.isOptional() || parameterType.isAssignable(BeanProvider.class) || parameterType.isAssignable(Provider.class)) { - parameterType = parameterType.getFirstTypeArgument().orElse(parameterType); + ClassElement finalParameterType = parameterType; + parameterType = parameterType.getOptionalValueType().or(finalParameterType::getFirstTypeArgument).orElse(parameterType); // Get the class with type annotations parameterType = visitorContext.getClassElement(parameterType.getCanonicalName()).orElse(parameterType); } diff --git a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyClassElement.java b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyClassElement.java index f7ca7600660..ffc247922f7 100644 --- a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyClassElement.java +++ b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyClassElement.java @@ -74,6 +74,9 @@ import java.util.Map; import java.util.Objects; import java.util.Optional; +import java.util.OptionalDouble; +import java.util.OptionalInt; +import java.util.OptionalLong; import java.util.Set; import java.util.Spliterator; import java.util.Spliterators; @@ -700,6 +703,23 @@ public boolean isAssignable(ClassElement type) { return AstClassUtils.isSubclassOfOrImplementsInterface(classNode, type.getName()); } + @Override + public Optional getOptionalValueType() { + if (isAssignable(Optional.class)) { + return getFirstTypeArgument().or(() -> visitorContext.getClassElement(Object.class)); + } + if (isAssignable(OptionalLong.class)) { + return visitorContext.getClassElement(Long.class); + } + if (isAssignable(OptionalDouble.class)) { + return visitorContext.getClassElement(Double.class); + } + if (isAssignable(OptionalInt.class)) { + return visitorContext.getClassElement(Integer.class); + } + return Optional.empty(); + } + @NonNull @Override public List getBoundGenericTypes() { diff --git a/inject-groovy/src/test/groovy/io/micronaut/inject/configproperties/ConfigurationPropertiesSpec.groovy b/inject-groovy/src/test/groovy/io/micronaut/inject/configproperties/ConfigurationPropertiesSpec.groovy index c4d12c1232a..1e06bd98094 100644 --- a/inject-groovy/src/test/groovy/io/micronaut/inject/configproperties/ConfigurationPropertiesSpec.groovy +++ b/inject-groovy/src/test/groovy/io/micronaut/inject/configproperties/ConfigurationPropertiesSpec.groovy @@ -65,4 +65,23 @@ class ConfigurationPropertiesSpec extends Specification { !applicationContext.getBeanDefinition(MyConfig).getAnnotation(BeanProperties.class) } + void "test optional configuration"() { + ApplicationContext context = ApplicationContext.run([ + "config.optional.str": "tst", + "config.optional.dbl": "123.123", + "config.optional.itgr": "456", + "config.optional.lng": "334455", + ]) + OptionalProperties config = context.getBean(OptionalProperties.class) + + expect: + config.getStr().get() == "tst" + config.getDbl().asDouble == 123.123 as double + config.getItgr().asInt == 456 + config.getLng().asLong == 334455 + + cleanup: + context.close() + } + } diff --git a/inject-groovy/src/test/groovy/io/micronaut/inject/configproperties/OptionalProperties.groovy b/inject-groovy/src/test/groovy/io/micronaut/inject/configproperties/OptionalProperties.groovy new file mode 100644 index 00000000000..1f92c06d78a --- /dev/null +++ b/inject-groovy/src/test/groovy/io/micronaut/inject/configproperties/OptionalProperties.groovy @@ -0,0 +1,44 @@ +package io.micronaut.inject.configproperties + +import io.micronaut.context.annotation.ConfigurationProperties + +@ConfigurationProperties("config.optional") +class OptionalProperties { + + private String str + private Integer itgr + private Double dbl + private Long lng + + Optional getStr() { + return Optional.ofNullable(str) + } + + void setStr(String str) { + this.str = str + } + + OptionalInt getItgr() { + return itgr == null ? OptionalInt.empty() : OptionalInt.of(itgr) + } + + void setItgr(Integer itgr) { + this.itgr = itgr + } + + OptionalDouble getDbl() { + return dbl == null ? OptionalDouble.empty() : OptionalDouble.of(dbl) + } + + void setDbl(Double dbl) { + this.dbl = dbl + } + + OptionalLong getLng() { + return lng == null ? OptionalLong.empty() : OptionalLong.of(lng) + } + + void setLng(Long lng) { + this.lng = lng + } +} diff --git a/inject-java-test/src/test/groovy/io/micronaut/inject/visitor/beans/BeanIntrospectionSpec.groovy b/inject-java-test/src/test/groovy/io/micronaut/inject/visitor/beans/BeanIntrospectionSpec.groovy index f53bbe1200b..14cd681d243 100644 --- a/inject-java-test/src/test/groovy/io/micronaut/inject/visitor/beans/BeanIntrospectionSpec.groovy +++ b/inject-java-test/src/test/groovy/io/micronaut/inject/visitor/beans/BeanIntrospectionSpec.groovy @@ -77,7 +77,7 @@ class Test { introspection.beanMethods.first().returnType.type == CharSequence[].class } - void "test property type is defined by its setter"() { + void "test property type is defined by its writer field"() { given: def introspection = buildBeanIntrospection('test.Test', ''' package test; @@ -87,47 +87,87 @@ import io.micronaut.context.annotation.Executable; import io.micronaut.core.annotation.Nullable; import java.util.Optional; -@Introspected +@Introspected(accessKind = {Introspected.AccessKind.METHOD, Introspected.AccessKind.FIELD}) class Test { @Nullable - private String foo; + String foo; public Optional getFoo() { return Optional.ofNullable(foo); } - public void setFoo(@Nullable String foo) { - this.foo = foo; - } } ''') expect: introspection.getProperty("foo").get().type == String.class } - void "test property type is defined by its writer field"() { + void "test optional property type is defined by its setter"() { given: - def introspection = buildBeanIntrospection('test.Test', ''' + def introspection = buildBeanIntrospection('test.Test', ''' package test; import io.micronaut.core.annotation.Introspected; import io.micronaut.context.annotation.Executable; import io.micronaut.core.annotation.Nullable; -import java.util.Optional; +import java.util.*; -@Introspected(accessKind = {Introspected.AccessKind.METHOD, Introspected.AccessKind.FIELD}) +@Introspected class Test { @Nullable - String foo; + private String foo; + @Nullable + private Long lng; + @Nullable + private Double dbl; + @Nullable + private Integer ingr; public Optional getFoo() { return Optional.ofNullable(foo); } + public OptionalDouble getDbl() { + return OptionalDouble.of(dbl); + } + + public OptionalLong getLng() { + return OptionalLong.of(lng); + } + + public OptionalInt getIngr() { + return OptionalInt.of(ingr); + } + + public void setFoo(@Nullable String foo) { + this.foo = foo; + } + + public void setLng(@Nullable Long lng) { + this.lng = lng; + } + + public void setDbl(@Nullable Double dbl) { + this.dbl = dbl; + } + + public void setIngr(@Nullable Integer ingr) { + this.ingr = ingr; + } + } ''') expect: - introspection.getProperty("foo").get().type == String.class + introspection.getPropertyNames().length == 4 + introspection.getProperty("foo").get().type == String.class + introspection.getProperty("lng").get().type == Long.class + introspection.getProperty("dbl").get().type == Double.class + introspection.getProperty("ingr").get().type == Integer.class + + introspection.getProperty("foo").get().isReadWrite() + introspection.getProperty("lng").get().isReadWrite() + introspection.getProperty("dbl").get().isReadWrite() + introspection.getProperty("ingr").get().isReadWrite() } void "test property type is not defined by its not accessible field"() { diff --git a/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaClassElement.java b/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaClassElement.java index 56e216bae51..e3e8e7eed9c 100644 --- a/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaClassElement.java +++ b/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaClassElement.java @@ -64,6 +64,9 @@ 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.function.Function; import java.util.stream.Collectors; @@ -560,6 +563,23 @@ public boolean isAssignable(ClassElement type) { return isAssignable(type.getName()); } + @Override + public Optional getOptionalValueType() { + if (isAssignable(Optional.class)) { + return getFirstTypeArgument().or(() -> visitorContext.getClassElement(Object.class)); + } + if (isAssignable(OptionalLong.class)) { + return visitorContext.getClassElement(Long.class); + } + if (isAssignable(OptionalDouble.class)) { + return visitorContext.getClassElement(Double.class); + } + if (isAssignable(OptionalInt.class)) { + return visitorContext.getClassElement(Integer.class); + } + return Optional.empty(); + } + private boolean isAssignable(TypeElement otherElement) { Types types = visitorContext.getTypes(); TypeMirror thisType = types.erasure(classElement.asType()); diff --git a/inject-java/src/test/groovy/io/micronaut/inject/configproperties/ConfigurationPropertiesSpec.groovy b/inject-java/src/test/groovy/io/micronaut/inject/configproperties/ConfigurationPropertiesSpec.groovy index 2952e699d0c..d0934302245 100644 --- a/inject-java/src/test/groovy/io/micronaut/inject/configproperties/ConfigurationPropertiesSpec.groovy +++ b/inject-java/src/test/groovy/io/micronaut/inject/configproperties/ConfigurationPropertiesSpec.groovy @@ -160,4 +160,23 @@ class ConfigurationPropertiesSpec extends Specification { context1.close() context2.close() } + + void "test optional configuration"() { + ApplicationContext context = ApplicationContext.run([ + "config.optional.str": "tst", + "config.optional.dbl": "123.123", + "config.optional.itgr": "456", + "config.optional.lng": "334455", + ]) + OptionalProperties config = context.getBean(OptionalProperties.class) + + expect: + config.getStr().get() == "tst" + config.getDbl().asDouble == 123.123 as double + config.getItgr().asInt == 456 + config.getLng().asLong == 334455 + + cleanup: + context.close() + } } diff --git a/inject-java/src/test/groovy/io/micronaut/inject/configproperties/OptionalProperties.java b/inject-java/src/test/groovy/io/micronaut/inject/configproperties/OptionalProperties.java new file mode 100644 index 00000000000..d969d36fc81 --- /dev/null +++ b/inject-java/src/test/groovy/io/micronaut/inject/configproperties/OptionalProperties.java @@ -0,0 +1,49 @@ +package io.micronaut.inject.configproperties; + +import io.micronaut.context.annotation.ConfigurationProperties; + +import java.util.Optional; +import java.util.OptionalDouble; +import java.util.OptionalInt; +import java.util.OptionalLong; + +@ConfigurationProperties("config.optional") +public class OptionalProperties { + + private String str; + private Integer itgr; + private Double dbl; + private Long lng; + + public Optional getStr() { + return Optional.ofNullable(str); + } + + public void setStr(String str) { + this.str = str; + } + + public OptionalInt getItgr() { + return itgr == null ? OptionalInt.empty() : OptionalInt.of(itgr); + } + + public void setItgr(Integer itgr) { + this.itgr = itgr; + } + + public OptionalDouble getDbl() { + return dbl == null ? OptionalDouble.empty() : OptionalDouble.of(dbl); + } + + public void setDbl(Double dbl) { + this.dbl = dbl; + } + + public OptionalLong getLng() { + return lng == null ? OptionalLong.empty() : OptionalLong.of(lng); + } + + public void setLng(Long lng) { + this.lng = lng; + } +} From 4c5a19a2a82158043080ae28b71e7fac63d21de1 Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Wed, 2 Nov 2022 12:01:19 +0100 Subject: [PATCH 184/743] build: update GraalVM to 22.3.0 (#8230) --- gradle/libs.versions.toml | 2 +- src/main/docs/guide/introduction/whatsNew.adoc | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index f2fea92858a..ea6fb8aace4 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -46,7 +46,7 @@ managed-gorm-hibernate = "7.3.0" # be sure to update graal version in gradle.properties as well # Intentionally pin to 22.0.0.2 see https://github.com/micronaut-projects/micronaut-kafka/pull/564 and https://github.com/micronaut-projects/micronaut-core/pull/7663 managed-graal-sdk = "22.0.0.2" -managed-graal = "22.2.0" +managed-graal = "22.3.0" managed-graal-svm = "22.0.0.2" managed-groovy = "3.0.13" managed-h2 = "1.4.200" diff --git a/src/main/docs/guide/introduction/whatsNew.adoc b/src/main/docs/guide/introduction/whatsNew.adoc index 090b6773ed1..758ef2ed828 100644 --- a/src/main/docs/guide/introduction/whatsNew.adoc +++ b/src/main/docs/guide/introduction/whatsNew.adoc @@ -1,4 +1,9 @@ //Micronaut {version} includes the following changes: +== 3.8.0 + +Key features: + +- https://www.graalvm.org/release-notes/22_3/[GraalVM 22.3 Support] == 3.7.0 From 2f4fe160d024d46c48c8f9d680f707f67ca1e515 Mon Sep 17 00:00:00 2001 From: Tim Yates Date: Wed, 2 Nov 2022 15:04:42 +0000 Subject: [PATCH 185/743] Cleanup context after test finishes (#8271) --- .../io/micronaut/retry/intercept/CircuitBreakerSpec.groovy | 3 +++ 1 file changed, 3 insertions(+) diff --git a/retry/src/test/groovy/io/micronaut/retry/intercept/CircuitBreakerSpec.groovy b/retry/src/test/groovy/io/micronaut/retry/intercept/CircuitBreakerSpec.groovy index 1a515c56627..06e276f9a68 100644 --- a/retry/src/test/groovy/io/micronaut/retry/intercept/CircuitBreakerSpec.groovy +++ b/retry/src/test/groovy/io/micronaut/retry/intercept/CircuitBreakerSpec.groovy @@ -186,6 +186,9 @@ class CircuitBreakerSpec extends Specification{ then:"It executes until successful" result == 2 + + cleanup: + context.stop() } @Singleton From 231cd469962c88f7b457b5b09e539b5655e568e5 Mon Sep 17 00:00:00 2001 From: Jonas Konrad Date: Wed, 2 Nov 2022 16:05:42 +0100 Subject: [PATCH 186/743] Virtual thread support (#8180) This PR introduces basic virtual thread support to micronaut-context. Virtual threads are available with `--enable-preview` on Java 19. The changes in this PR: - Introduce a new `ExecutorType.THREAD_PER_TASK`. This uses the new `newThreadPerTaskExecutor`, which launches a new (hopefully virtual) thread for each submitted task. - Introduce a new setting `ExecutorConfiguration.virtual` that changes the `ThreadFactory` to create virtual threads. - Change the `TaskExecutors.IO` thread pool to use a virtual thread-per-task executor if virtual threads are available. I have done preliminary benchmarking of virtual threads using our existing `ExecuteOn` machinery. Unfortunately, there is still overhead associated with dispatching controller methods on virtual threads. For this reason, I'm not proposing making virtual thread dispatch the default. I will have to investigate further whether there are optimization opportunities (removing thread locals?) that could help here. Replacing the `IO` executor with a virtual thread pool seems like a good path forward. Existing users of `IO` will benefit from virtual threads without moving to a new executor. We also have a few internal uses of the IO executor. Changing the IO executor to a virtual thread pool could be incompatible with the old executor, however. Virtual threads run on a mostly-fixed-size thread pool, and if many of the virtual threads are pinned (e.g. from a synchronized block), the carrier threads might be exhausted. The old IO pool was a cached pool, and would never exhaust its threads. On implementation: 19-specific code is packaged into the new `LoomSupport` class, and accessed through static final MethodHandles. My understanding is that static final MHs carry no call overhead. I have yet to test it with graal however. Co-authored-by: Graeme Rocher --- .../io/micronaut/scheduling/LoomSupport.java | 129 ++++++++++++++++++ .../micronaut/scheduling/TaskExecutors.java | 16 ++- .../executor/DefaultExecutorSelector.java | 14 +- .../executor/ExecutorConfiguration.java | 5 + .../scheduling/executor/ExecutorFactory.java | 19 ++- .../scheduling/executor/ExecutorType.java | 7 +- .../executor/IOExecutorServiceConfig.java | 49 ++++++- .../scheduling/executor/ThreadSelection.java | 8 +- .../executor/UserExecutorConfiguration.java | 23 +++- .../http/server/netty/NettyHttpServer.java | 2 +- .../netty/binders/NettyBinderRegistrar.java | 2 +- .../netty/jackson/JsonViewServerFilter.java | 2 +- .../indicator/AbstractHealthIndicator.java | 2 +- .../health/indicator/jdbc/JdbcIndicator.java | 2 +- .../executor/ExecutorServiceConfigSpec.groovy | 14 +- 15 files changed, 266 insertions(+), 28 deletions(-) create mode 100644 context/src/main/java/io/micronaut/scheduling/LoomSupport.java diff --git a/context/src/main/java/io/micronaut/scheduling/LoomSupport.java b/context/src/main/java/io/micronaut/scheduling/LoomSupport.java new file mode 100644 index 00000000000..48b7b67b8b4 --- /dev/null +++ b/context/src/main/java/io/micronaut/scheduling/LoomSupport.java @@ -0,0 +1,129 @@ +/* + * Copyright 2017-2021 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.scheduling; + +import io.micronaut.context.condition.Condition; +import io.micronaut.context.condition.ConditionContext; +import io.micronaut.core.annotation.Internal; + +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.ThreadFactory; + +/** + * @since 4.0.0 + */ +@Internal +public final class LoomSupport { + private static final boolean SUPPORTED; + private static Throwable failure; + + private static final MethodHandle MH_NEW_THREAD_PER_TASK_EXECUTOR; + private static final MethodHandle MH_OF_VIRTUAL; + private static final MethodHandle MH_NAME; + private static final MethodHandle MH_FACTORY; + + static { + boolean sup; + MethodHandle newThreadPerTaskExecutor; + MethodHandle ofVirtual; + MethodHandle name; + MethodHandle factory; + try { + newThreadPerTaskExecutor = MethodHandles.lookup() + .findStatic(Executors.class, "newThreadPerTaskExecutor", MethodType.methodType(ExecutorService.class, ThreadFactory.class)); + Class builderCl = Class.forName("java.lang.Thread$Builder"); + Class ofVirtualCl = Class.forName("java.lang.Thread$Builder$OfVirtual"); + ofVirtual = MethodHandles.lookup() + .findStatic(Thread.class, "ofVirtual", MethodType.methodType(ofVirtualCl)); + name = MethodHandles.lookup() + .findVirtual(builderCl, "name", MethodType.methodType(builderCl, String.class, long.class)); + factory = MethodHandles.lookup() + .findVirtual(builderCl, "factory", MethodType.methodType(ThreadFactory.class)); + + // invoke, this will throw an UnsupportedOperationException if we don't have --enable-preview + ofVirtual.invoke(); + + sup = true; + } catch (Throwable e) { + newThreadPerTaskExecutor = null; + ofVirtual = null; + name = null; + factory = null; + sup = false; + failure = e; + } + + SUPPORTED = sup; + MH_NEW_THREAD_PER_TASK_EXECUTOR = newThreadPerTaskExecutor; + MH_OF_VIRTUAL = ofVirtual; + MH_NAME = name; + MH_FACTORY = factory; + } + + private LoomSupport() { + } + + public static boolean isSupported() { + return SUPPORTED; + } + + public static void checkSupported() { + if (!isSupported()) { + throw new UnsupportedOperationException("Virtual threads are not supported on this JVM, you may have to pass --enable-preview", failure); + } + } + + public static ExecutorService newThreadPerTaskExecutor(ThreadFactory threadFactory) { + checkSupported(); + try { + return (ExecutorService) MH_NEW_THREAD_PER_TASK_EXECUTOR.invokeExact(threadFactory); + } catch (Throwable e) { + throw new RuntimeException(e); + } + } + + public static ThreadFactory newVirtualThreadFactory(String namePrefix) { + checkSupported(); + try { + Object builder = MH_OF_VIRTUAL.invoke(); + builder = MH_NAME.invoke(builder, namePrefix, 1L); + return (ThreadFactory) MH_FACTORY.invoke(builder); + } catch (Throwable e) { + throw new RuntimeException(e); + } + } + + /** + * Condition that only matches if virtual threads are supported on this platform. + */ + @Internal + public static class LoomCondition implements Condition { + @SuppressWarnings("rawtypes") + @Override + public boolean matches(ConditionContext context) { + if (isSupported()) { + return true; + } else { + context.fail("Virtual threads support not available: " + failure.getMessage()); + return false; + } + } + } +} diff --git a/context/src/main/java/io/micronaut/scheduling/TaskExecutors.java b/context/src/main/java/io/micronaut/scheduling/TaskExecutors.java index 3dea8a9316f..0794a7a25b3 100644 --- a/context/src/main/java/io/micronaut/scheduling/TaskExecutors.java +++ b/context/src/main/java/io/micronaut/scheduling/TaskExecutors.java @@ -24,10 +24,24 @@ public interface TaskExecutors { /** - * The name of the {@link java.util.concurrent.ExecutorService} used to schedule I/O tasks. + * The name of the {@link java.util.concurrent.ExecutorService} used to schedule I/O tasks. By + * default, this is a {@link java.util.concurrent.Executors#newCachedThreadPool() cached thread pool}. */ String IO = "io"; + /** + * The name of the {@link java.util.concurrent.ExecutorService} used to schedule blocking tasks. + * If available, this will use {@link #VIRTUAL virtual threads}. Otherwise it will fall back to + * {@link #IO}. + */ + String BLOCKING = "blocking"; + + /** + * Executor that runs tasks on virtual threads. This requires JDK 19+, and + * {@code --enable-preview}. + */ + String VIRTUAL = "virtual"; + /** * The name of the {@link java.util.concurrent.ScheduledExecutorService} used to schedule background tasks. */ diff --git a/context/src/main/java/io/micronaut/scheduling/executor/DefaultExecutorSelector.java b/context/src/main/java/io/micronaut/scheduling/executor/DefaultExecutorSelector.java index f39eec68f27..0dba088526c 100644 --- a/context/src/main/java/io/micronaut/scheduling/executor/DefaultExecutorSelector.java +++ b/context/src/main/java/io/micronaut/scheduling/executor/DefaultExecutorSelector.java @@ -47,16 +47,22 @@ public class DefaultExecutorSelector implements ExecutorSelector { private static final String EXECUTE_ON = ExecuteOn.class.getName(); private final BeanLocator beanLocator; private final Supplier ioExecutor; + private final Supplier blockingExecutor; /** * Default constructor. * @param beanLocator The bean locator * @param ioExecutor The IO executor + * @param blockingExecutor The blocking executor */ @Inject - protected DefaultExecutorSelector(BeanLocator beanLocator, @jakarta.inject.Named(TaskExecutors.IO) BeanProvider ioExecutor) { + protected DefaultExecutorSelector( + BeanLocator beanLocator, + @jakarta.inject.Named(TaskExecutors.IO) BeanProvider ioExecutor, + @jakarta.inject.Named(TaskExecutors.BLOCKING) BeanProvider blockingExecutor) { this.beanLocator = beanLocator; this.ioExecutor = SupplierUtil.memoized(ioExecutor::get); + this.blockingExecutor = SupplierUtil.memoized(blockingExecutor::get); } @Override @@ -77,7 +83,7 @@ public Optional select(MethodReference method, ThreadSelection if (method.hasStereotype(NonBlocking.class)) { return Optional.empty(); } else if (method.hasStereotype(Blocking.class)) { - return Optional.of(ioExecutor.get()); + return Optional.of(blockingExecutor.get()); } else { TypeInformation returnType = method.getReturnType(); if (returnType.isWrapperType()) { @@ -89,11 +95,13 @@ public Optional select(MethodReference method, ThreadSelection if (returnType.isAsyncOrReactive()) { return Optional.empty(); } else { - return Optional.of(ioExecutor.get()); + return Optional.of(blockingExecutor.get()); } } } else if (threadSelection == ThreadSelection.IO) { return Optional.of(ioExecutor.get()); + } else if (threadSelection == ThreadSelection.BLOCKING) { + return Optional.of(blockingExecutor.get()); } return Optional.empty(); } diff --git a/context/src/main/java/io/micronaut/scheduling/executor/ExecutorConfiguration.java b/context/src/main/java/io/micronaut/scheduling/executor/ExecutorConfiguration.java index f3c4f790af1..53ea4bde03a 100644 --- a/context/src/main/java/io/micronaut/scheduling/executor/ExecutorConfiguration.java +++ b/context/src/main/java/io/micronaut/scheduling/executor/ExecutorConfiguration.java @@ -75,6 +75,11 @@ default String getName() { */ @Min(1L) Integer getCorePoolSize(); + /** + * @return Whether the pool should use virtual threads. + */ + boolean isVirtual(); + /** * @return The class to use as the {@link ThreadFactory} */ diff --git a/context/src/main/java/io/micronaut/scheduling/executor/ExecutorFactory.java b/context/src/main/java/io/micronaut/scheduling/executor/ExecutorFactory.java index 52c27554bd5..e6d1db69b98 100644 --- a/context/src/main/java/io/micronaut/scheduling/executor/ExecutorFactory.java +++ b/context/src/main/java/io/micronaut/scheduling/executor/ExecutorFactory.java @@ -21,9 +21,12 @@ import io.micronaut.context.annotation.Factory; import io.micronaut.core.reflect.InstantiationUtils; import io.micronaut.inject.qualifiers.Qualifiers; +import io.micronaut.scheduling.LoomSupport; import jakarta.inject.Inject; -import java.util.concurrent.*; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.ThreadFactory; /** * Constructs {@link ExecutorService} instances based on {@link UserExecutorConfiguration} instances. @@ -57,7 +60,17 @@ public ExecutorFactory(BeanLocator beanLocator, ThreadFactory threadFactory) { */ @EachBean(ExecutorConfiguration.class) protected ThreadFactory eventLoopGroupThreadFactory(ExecutorConfiguration configuration) { - return configuration.getName() == null ? threadFactory : new NamedThreadFactory(configuration.getName() + "-executor"); + String name = configuration.getName(); + if (configuration.isVirtual()) { + if (name == null) { + name = "virtual"; + } + return LoomSupport.newVirtualThreadFactory(name + "-executor"); + } + if (name != null) { + return new NamedThreadFactory(name + "-executor"); + } + return threadFactory; } /** @@ -79,6 +92,8 @@ public ExecutorService executorService(ExecutorConfiguration executorConfigurati return Executors.newScheduledThreadPool(executorConfiguration.getCorePoolSize(), getThreadFactory(executorConfiguration)); case WORK_STEALING: return Executors.newWorkStealingPool(executorConfiguration.getParallelism()); + case THREAD_PER_TASK: + return LoomSupport.newThreadPerTaskExecutor(getThreadFactory(executorConfiguration)); default: throw new IllegalStateException("Could not create Executor service for enum value: " + executorType); diff --git a/context/src/main/java/io/micronaut/scheduling/executor/ExecutorType.java b/context/src/main/java/io/micronaut/scheduling/executor/ExecutorType.java index 9ad1e9891fc..e96a5cc3002 100644 --- a/context/src/main/java/io/micronaut/scheduling/executor/ExecutorType.java +++ b/context/src/main/java/io/micronaut/scheduling/executor/ExecutorType.java @@ -42,5 +42,10 @@ public enum ExecutorType { /** * @see java.util.concurrent.Executors#newWorkStealingPool() */ - WORK_STEALING + WORK_STEALING, + + /** + * @see java.util.concurrent.Executors#newThreadPerTaskExecutor() + */ + THREAD_PER_TASK } diff --git a/context/src/main/java/io/micronaut/scheduling/executor/IOExecutorServiceConfig.java b/context/src/main/java/io/micronaut/scheduling/executor/IOExecutorServiceConfig.java index 853910015e5..c1ec79620ee 100644 --- a/context/src/main/java/io/micronaut/scheduling/executor/IOExecutorServiceConfig.java +++ b/context/src/main/java/io/micronaut/scheduling/executor/IOExecutorServiceConfig.java @@ -15,28 +15,69 @@ */ package io.micronaut.scheduling.executor; +import io.micronaut.context.BeanProvider; import io.micronaut.context.annotation.Factory; import io.micronaut.context.annotation.Requires; +import io.micronaut.scheduling.LoomSupport; import io.micronaut.scheduling.TaskExecutors; import jakarta.inject.Named; import jakarta.inject.Singleton; +import java.util.concurrent.ExecutorService; + /** * Configures the default I/O thread pool if none is configured by the user. * * @author Graeme Rocher * @since 1.0 */ -@Requires(missingProperty = ExecutorConfiguration.PREFIX_IO) @Factory -public class IOExecutorServiceConfig { +public final class IOExecutorServiceConfig { /** - * @return The default thread pool configurations + * @return The default IO pool configuration */ @Singleton @Named(TaskExecutors.IO) - ExecutorConfiguration configuration() { + @Requires(missingProperty = ExecutorConfiguration.PREFIX_IO) + ExecutorConfiguration io() { return UserExecutorConfiguration.of(TaskExecutors.IO, ExecutorType.CACHED); } + + /** + * @return The default virtual executor configuration + */ + @Singleton + @Named(TaskExecutors.VIRTUAL) + @Requires( + missingProperty = ExecutorConfiguration.PREFIX + "." + TaskExecutors.VIRTUAL, + condition = LoomSupport.LoomCondition.class) + ExecutorConfiguration virtual() { + // sanity check + LoomSupport.checkSupported(); + UserExecutorConfiguration cfg = UserExecutorConfiguration.of(TaskExecutors.VIRTUAL, ExecutorType.THREAD_PER_TASK); + cfg.setVirtual(true); + return cfg; + } + + /** + * The blocking executor. + * + * @param io IO executor (fallback) + * @param virtual Virtual thread executor (used if available) + * @return The blocking executor + */ + @Singleton + @Named(TaskExecutors.BLOCKING) + @Requires(missingProperty = ExecutorConfiguration.PREFIX + "." + TaskExecutors.BLOCKING) + ExecutorService blocking( + @Named(TaskExecutors.IO) BeanProvider io, + @Named(TaskExecutors.VIRTUAL) BeanProvider virtual + ) { + if (virtual.isPresent()) { + return virtual.get(); + } else { + return io.get(); + } + } } diff --git a/context/src/main/java/io/micronaut/scheduling/executor/ThreadSelection.java b/context/src/main/java/io/micronaut/scheduling/executor/ThreadSelection.java index 132eb6d614f..1db9e00b8bb 100644 --- a/context/src/main/java/io/micronaut/scheduling/executor/ThreadSelection.java +++ b/context/src/main/java/io/micronaut/scheduling/executor/ThreadSelection.java @@ -25,7 +25,7 @@ public enum ThreadSelection { /** * Automatically select the thread to run operations on based on the return type and/or {@link io.micronaut.core.annotation.Blocking} or {@link io.micronaut.core.annotation.NonBlocking} annotations. * - *

This is the default strategy in 1.x and will run operations on the I/O thread pool if the return type + *

This is the default strategy in 1.x and will run operations on the {@link io.micronaut.scheduling.TaskExecutors#BLOCKING blocking executor} if the return type * of the method is not a reactive top and the method is not annotated with {@link io.micronaut.core.annotation.NonBlocking}

* *

If the return type is a reactive type and the method is not annotated with {@link io.micronaut.core.annotation.Blocking} then the server event loop thread will used to run the operation.

@@ -39,5 +39,9 @@ public enum ThreadSelection { /** * I/O selection will run all operations regardless of return type and annotations on the I/O thread pool and will never schedule an operation on the server event loop thread. */ - IO + IO, + /** + * I/O selection will run all operations regardless of return type and annotations on the {@link io.micronaut.scheduling.TaskExecutors#BLOCKING blocking executor} and will never schedule an operation on the server event loop thread. + */ + BLOCKING } diff --git a/context/src/main/java/io/micronaut/scheduling/executor/UserExecutorConfiguration.java b/context/src/main/java/io/micronaut/scheduling/executor/UserExecutorConfiguration.java index 45bf7242202..79e84af8184 100644 --- a/context/src/main/java/io/micronaut/scheduling/executor/UserExecutorConfiguration.java +++ b/context/src/main/java/io/micronaut/scheduling/executor/UserExecutorConfiguration.java @@ -16,12 +16,11 @@ package io.micronaut.scheduling.executor; import io.micronaut.context.annotation.ConfigurationInject; -import io.micronaut.core.annotation.NonNull; import io.micronaut.context.annotation.EachProperty; import io.micronaut.context.annotation.Parameter; -import io.micronaut.core.util.ArgumentUtils; - +import io.micronaut.core.annotation.NonNull; import io.micronaut.core.annotation.Nullable; +import io.micronaut.core.util.ArgumentUtils; import javax.validation.constraints.Min; import java.util.Optional; @@ -48,6 +47,7 @@ public class UserExecutorConfiguration implements ExecutorConfiguration { private ExecutorType type; private Integer parallelism; private Integer corePoolSize; + private boolean virtual; private Class threadFactoryClass; /** @@ -56,7 +56,7 @@ public class UserExecutorConfiguration implements ExecutorConfiguration { * @param name The name */ private UserExecutorConfiguration(@Parameter String name) { - this(name, null, null, null, null, null); + this(name, null, null, null, null, false, null); } /** @@ -67,6 +67,7 @@ private UserExecutorConfiguration(@Parameter String name) { * @param type the type * @param parallelism the parallelism * @param corePoolSize the core pool size + * @param virtual whether to use virtual threads * @param threadFactoryClass the thread factory class */ @ConfigurationInject @@ -75,12 +76,14 @@ protected UserExecutorConfiguration(@Nullable @Parameter String name, @Nullable ExecutorType type, @Nullable Integer parallelism, @Nullable Integer corePoolSize, + @Nullable Boolean virtual, @Nullable Class threadFactoryClass) { this.name = name; this.nThreads = nThreads == null ? AVAILABLE_PROCESSORS * 2 : nThreads; this.type = type == null ? ExecutorType.SCHEDULED : type; this.parallelism = parallelism == null ? AVAILABLE_PROCESSORS : parallelism; this.corePoolSize = corePoolSize == null ? AVAILABLE_PROCESSORS * 2 : corePoolSize; + this.virtual = virtual == null ? false : virtual; this.threadFactoryClass = threadFactoryClass; } @@ -113,6 +116,18 @@ public Integer getCorePoolSize() { return corePoolSize; } + @Override + public boolean isVirtual() { + return virtual; + } + + /** + * @param virtual Whether the pool should use virtual threads + */ + public void setVirtual(boolean virtual) { + this.virtual = virtual; + } + @Override public Optional> getThreadFactoryClass() { return Optional.ofNullable(threadFactoryClass); diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/NettyHttpServer.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/NettyHttpServer.java index 40fd6d89c92..158651c409b 100644 --- a/http-server-netty/src/main/java/io/micronaut/http/server/netty/NettyHttpServer.java +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/NettyHttpServer.java @@ -160,7 +160,7 @@ public NettyHttpServer( .getEventPublisher(HttpRequestTerminatedEvent.class); final Supplier ioExecutor = SupplierUtil.memoized(() -> nettyEmbeddedServices.getExecutorSelector() - .select(TaskExecutors.IO).orElse(null) + .select(TaskExecutors.BLOCKING).orElse(null) ); this.httpContentProcessorResolver = new DefaultHttpContentProcessorResolver( nettyEmbeddedServices.getApplicationContext(), diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/binders/NettyBinderRegistrar.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/binders/NettyBinderRegistrar.java index 07374ae7b8f..e575c472c95 100644 --- a/http-server-netty/src/main/java/io/micronaut/http/server/netty/binders/NettyBinderRegistrar.java +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/binders/NettyBinderRegistrar.java @@ -60,7 +60,7 @@ class NettyBinderRegistrar implements BeanCreatedEventListener httpServerConfiguration, - @Named(TaskExecutors.IO) BeanProvider executorService) { + @Named(TaskExecutors.BLOCKING) BeanProvider executorService) { this.conversionService = conversionService; this.httpContentProcessorResolver = httpContentProcessorResolver; this.beanLocator = beanLocator; diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/jackson/JsonViewServerFilter.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/jackson/JsonViewServerFilter.java index 7d2cdc1dbb4..e384d0235df 100644 --- a/http-server-netty/src/main/java/io/micronaut/http/server/netty/jackson/JsonViewServerFilter.java +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/jackson/JsonViewServerFilter.java @@ -67,7 +67,7 @@ public class JsonViewServerFilter implements HttpServerFilter { */ public JsonViewServerFilter( JsonViewCodecResolver jsonViewCodecResolver, - @Named(TaskExecutors.IO) ExecutorService executorService) { + @Named(TaskExecutors.BLOCKING) ExecutorService executorService) { this.codecFactory = jsonViewCodecResolver; this.executorService = executorService; } diff --git a/management/src/main/java/io/micronaut/management/health/indicator/AbstractHealthIndicator.java b/management/src/main/java/io/micronaut/management/health/indicator/AbstractHealthIndicator.java index 75ac9348cb9..9fc496f2155 100644 --- a/management/src/main/java/io/micronaut/management/health/indicator/AbstractHealthIndicator.java +++ b/management/src/main/java/io/micronaut/management/health/indicator/AbstractHealthIndicator.java @@ -41,7 +41,7 @@ public abstract class AbstractHealthIndicator implements HealthIndicator { * @param executorService The executor service */ @Inject - public void setExecutorService(@Named(TaskExecutors.IO) ExecutorService executorService) { + public void setExecutorService(@Named(TaskExecutors.BLOCKING) ExecutorService executorService) { this.executorService = executorService; } diff --git a/management/src/main/java/io/micronaut/management/health/indicator/jdbc/JdbcIndicator.java b/management/src/main/java/io/micronaut/management/health/indicator/jdbc/JdbcIndicator.java index 74a1aaccb38..e69f1e16ca9 100644 --- a/management/src/main/java/io/micronaut/management/health/indicator/jdbc/JdbcIndicator.java +++ b/management/src/main/java/io/micronaut/management/health/indicator/jdbc/JdbcIndicator.java @@ -71,7 +71,7 @@ public class JdbcIndicator implements HealthIndicator { * @param dataSourceResolver The data source resolver * @param healthAggregator The health aggregator */ - public JdbcIndicator(@Named(TaskExecutors.IO) ExecutorService executorService, + public JdbcIndicator(@Named(TaskExecutors.BLOCKING) ExecutorService executorService, DataSource[] dataSources, @Nullable DataSourceResolver dataSourceResolver, HealthAggregator healthAggregator) { diff --git a/runtime/src/test/groovy/io/micronaut/runtime/executor/ExecutorServiceConfigSpec.groovy b/runtime/src/test/groovy/io/micronaut/runtime/executor/ExecutorServiceConfigSpec.groovy index dd163b57638..ba4f0d1977d 100644 --- a/runtime/src/test/groovy/io/micronaut/runtime/executor/ExecutorServiceConfigSpec.groovy +++ b/runtime/src/test/groovy/io/micronaut/runtime/executor/ExecutorServiceConfigSpec.groovy @@ -17,6 +17,7 @@ package io.micronaut.runtime.executor import io.micronaut.context.ApplicationContext import io.micronaut.inject.qualifiers.Qualifiers +import io.micronaut.scheduling.LoomSupport import io.micronaut.scheduling.TaskExecutors import io.micronaut.scheduling.executor.ExecutorConfiguration import io.micronaut.scheduling.executor.UserExecutorConfiguration @@ -33,6 +34,7 @@ import java.util.concurrent.ThreadPoolExecutor * @since 1.0 */ class ExecutorServiceConfigSpec extends Specification { + static final int expectedExecutorCount = LoomSupport.isSupported() ? 6 : 5 @Unroll void "test configure custom executor with invalidate cache: #invalidateCache"() { @@ -53,7 +55,7 @@ class ExecutorServiceConfigSpec extends Specification { Collection executorServices = ctx.getBeansOfType(ExecutorService.class) then: - executorServices.size() == 4 + executorServices.size() == expectedExecutorCount when: ThreadPoolExecutor poolExecutor = ctx.getBean(ThreadPoolExecutor, Qualifiers.byName("one")) @@ -61,7 +63,7 @@ class ExecutorServiceConfigSpec extends Specification { then: forkJoinPool instanceof ForkJoinPool - executorServices.size() == 4 + executorServices.size() == expectedExecutorCount poolExecutor.corePoolSize == 5 ctx.getBean(ExecutorService.class, Qualifiers.byName(TaskExecutors.IO)) // the default IO executor ctx.getBean(ExecutorService.class, Qualifiers.byName(TaskExecutors.SCHEDULED)) // the default IO executor @@ -113,7 +115,7 @@ class ExecutorServiceConfigSpec extends Specification { ExecutorService forkJoinPool = ctx.getBean(ExecutorService, Qualifiers.byName("two")) then: - executorServices.size() == 4 + executorServices.size() == expectedExecutorCount poolExecutor.corePoolSize == 5 ctx.getBean(ExecutorService.class, Qualifiers.byName(TaskExecutors.IO)) instanceof ThreadPoolExecutor ctx.getBean(ExecutorService.class, Qualifiers.byName(TaskExecutors.SCHEDULED)) instanceof ScheduledExecutorService @@ -129,7 +131,7 @@ class ExecutorServiceConfigSpec extends Specification { executorServices = ctx.getBeansOfType(ExecutorService) then: - executorServices.size() == 4 + executorServices.size() == expectedExecutorCount moreConfigs.size() == 4 configs.size() == 2 @@ -168,7 +170,7 @@ class ExecutorServiceConfigSpec extends Specification { Collection executorServices = ctx.getBeansOfType(ExecutorService.class) then: - executorServices.size() == 3 + executorServices.size() == expectedExecutorCount - 1 ctx.getBean(ExecutorService.class, Qualifiers.byName(TaskExecutors.IO)) instanceof ThreadPoolExecutor when: @@ -180,7 +182,7 @@ class ExecutorServiceConfigSpec extends Specification { executorServices = ctx.getBeansOfType(ExecutorService) then: - executorServices.size() == 3 + executorServices.size() == expectedExecutorCount - 1 moreConfigs.size() == 3 configs.size() == 2 From e92bd52b22385c1c1563ae98ee387e094dbd9f67 Mon Sep 17 00:00:00 2001 From: Graeme Rocher Date: Wed, 2 Nov 2022 17:35:50 +0100 Subject: [PATCH 187/743] address sonar issues (#8270) --- .../java/io/micronaut/runtime/Micronaut.java | 44 ++++++++++--------- .../io/watch/DefaultWatchThread.java | 1 + .../core/async/publisher/Publishers.java | 6 +-- .../core/annotation/AnnotationMetadata.java | 8 +++- .../micronaut/core/bind/ArgumentBinder.java | 1 + .../DefaultMutableConversionService.java | 3 +- .../java/io/micronaut/core/io/IOUtils.java | 21 +-------- .../core/io/service/SoftServiceLoader.java | 2 +- .../io/micronaut/core/reflect/ClassUtils.java | 4 +- .../core/serialize/JdkSerializer.java | 2 +- .../io/micronaut/core/util/ArgumentUtils.java | 2 +- .../io/micronaut/core/util/ArrayUtils.java | 37 +++++++++------- .../HttpClientIntroductionAdvice.java | 2 +- .../http/client/netty/ConnectionManager.java | 1 + .../netty/ssl/NettyClientSslBuilder.java | 15 ++++--- .../http/netty/AbstractNettyHttpRequest.java | 35 ++++++++------- .../NettyServerWebSocketBroadcaster.java | 1 + .../websocket/NettyWebSocketSession.java | 1 + .../server/netty/HttpPipelineBuilder.java | 13 +++--- .../netty/NettyEmbeddedServerFactory.java | 2 +- .../http/server/netty/NettyHttpRequest.java | 3 ++ .../ssl/CertificateProvidedSslBuilder.java | 15 ++++--- ...stomizableResponseTypeHandlerRegistry.java | 1 + .../java/io/micronaut/http/MediaType.java | 15 ++++--- .../binders/DefaultBodyAnnotationBinder.java | 2 +- .../codec/DefaultMediaTypeCodecRegistry.java | 1 + .../http/simple/cookies/SimpleCookie.java | 5 ++- .../io/micronaut/http/ssl/SslBuilder.java | 21 +++++---- .../micronaut/http/uri/UriMatchTemplate.java | 1 + .../AnnotationProcessingOutputVisitor.java | 8 +++- .../context/AbstractBeanDefinition.java | 14 +++++- .../AbstractInitializableBeanDefinition.java | 11 ++++- .../context/ApplicationContextBuilder.java | 2 +- .../context/DefaultApplicationContext.java | 2 +- .../DefaultApplicationContextBuilder.java | 2 +- .../micronaut/context/DefaultBeanContext.java | 4 +- .../micronaut/context/RequiresCondition.java | 1 + .../context/env/DefaultEnvironment.java | 2 + .../i18n/ResourceBundleMessageSource.java | 2 + .../AbstractAnnotationMetadata.java | 7 +-- .../AnnotationConvertersRegistrar.java | 20 ++++++--- .../annotation/AnnotationMetadataSupport.java | 17 +++---- .../ProviderTypeInformationProvider.java | 1 + .../core/tree/JsonNodeTraversingParser.java | 10 ++++- .../intercept/DefaultRetryInterceptor.java | 1 + .../ConfigurationDefaultVersionProvider.java | 8 +++- .../websocket/WebSocketBroadcaster.java | 1 + .../micronaut/websocket/WebSocketSession.java | 1 + 48 files changed, 226 insertions(+), 153 deletions(-) diff --git a/context/src/main/java/io/micronaut/runtime/Micronaut.java b/context/src/main/java/io/micronaut/runtime/Micronaut.java index 2e92cdf9bc1..3b1910399c7 100644 --- a/context/src/main/java/io/micronaut/runtime/Micronaut.java +++ b/context/src/main/java/io/micronaut/runtime/Micronaut.java @@ -36,7 +36,6 @@ import java.net.URL; import java.util.LinkedHashMap; import java.util.Map; -import java.util.Optional; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.function.Function; @@ -48,12 +47,12 @@ * @since 1.0 */ public class Micronaut extends DefaultApplicationContextBuilder implements ApplicationContextBuilder { - private static final String MICRONAUT = " Micronaut"; + private static final String MICRONAUT_PREFIX = " Micronaut"; private static final String BANNER_NAME = "micronaut-banner.txt"; private static final Logger LOG = LoggerFactory.getLogger(Micronaut.class); private static final String SHUTDOWN_MONITOR_THREAD = "micronaut-shutdown-monitor-thread"; - private Map, Function> exitHandlers = new LinkedHashMap<>(); + private final Map, Function> exitHandlers = new LinkedHashMap<>(); /** * The default constructor. @@ -65,6 +64,7 @@ protected Micronaut() { * @return Run this {@link Micronaut} */ @Override + @SuppressWarnings({"java:S1181", "java:S3776", "java:S1141"}) public @NonNull ApplicationContext start() { long start = System.nanoTime(); printBanner(); @@ -74,24 +74,22 @@ protected Micronaut() { applicationContext.start(); - Optional embeddedContainerBean = applicationContext.findBean(EmbeddedApplication.class); + EmbeddedApplication embeddedApplication = applicationContext.findBean(EmbeddedApplication.class).orElse(null); - embeddedContainerBean.ifPresent((embeddedApplication -> { + if (embeddedApplication != null) { try { embeddedApplication.start(); - boolean keepAlive = false; - if (embeddedApplication instanceof Described) { + boolean keepAlive; + if (embeddedApplication instanceof Described described) { if (LOG.isInfoEnabled()) { long took = elapsedMillis(start); - String desc = ((Described) embeddedApplication).getDescription(); + String desc = described.getDescription(); LOG.info("Startup completed in {}ms. Server Running: {}", took, desc); } keepAlive = embeddedApplication.isServer(); } else { - if (embeddedApplication instanceof EmbeddedServer) { - - final EmbeddedServer embeddedServer = (EmbeddedServer) embeddedApplication; + if (embeddedApplication instanceof EmbeddedServer embeddedServer) { if (LOG.isInfoEnabled()) { long took = elapsedMillis(start); URL url = embeddedServer.getURL(); @@ -131,7 +129,7 @@ protected Micronaut() { Thread.sleep(1000); } } catch (InterruptedException e) { - // ignore + Thread.currentThread().interrupt(); } }, SHUTDOWN_MONITOR_THREAD).start(); @@ -142,6 +140,7 @@ protected Micronaut() { break; } catch (InterruptedException e) { interrupted = true; + Thread.currentThread().interrupt(); } } if (interrupted) { @@ -158,16 +157,18 @@ protected Micronaut() { } catch (Throwable e) { handleStartupException(applicationContext.getEnvironment(), e); + Thread.currentThread().interrupt(); } - })); + } - if (LOG.isInfoEnabled() && !embeddedContainerBean.isPresent()) { + if (LOG.isInfoEnabled() && embeddedApplication == null) { LOG.info("No embedded container found. Running as CLI application"); } return applicationContext; } catch (Throwable e) { handleStartupException(applicationContext.getEnvironment(), e); - return null; + Thread.currentThread().interrupt(); + return applicationContext; } } @@ -196,9 +197,9 @@ private static long elapsedMillis(long startNanos) { * @param classes The application * @return The classes */ - public @NonNull Micronaut classes(@Nullable Class... classes) { + public @NonNull Micronaut classes(@Nullable Class... classes) { if (classes != null) { - for (Class aClass : classes) { + for (Class aClass : classes) { packages(aClass.getPackage().getName()); } } @@ -236,7 +237,7 @@ private static long elapsedMillis(long startNanos) { } @Override - public @NonNull Micronaut mainClass(Class mainClass) { + public @NonNull Micronaut mainClass(Class mainClass) { return (Micronaut) super.mainClass(mainClass); } @@ -305,7 +306,7 @@ public static ApplicationContext run(String... args) { * @param args The arguments * @return The {@link ApplicationContext} */ - public static ApplicationContext run(Class cls, String... args) { + public static ApplicationContext run(Class cls, String... args) { return run(new Class[]{cls}, args); } @@ -316,7 +317,7 @@ public static ApplicationContext run(Class cls, String... args) { * @param args The arguments * @return The {@link ApplicationContext} */ - public static ApplicationContext run(Class[] classes, String... args) { + public static ApplicationContext run(Class[] classes, String... args) { return new Micronaut() .classes(classes) .args(args) @@ -342,6 +343,7 @@ protected void handleStartupException(Environment environment, Throwable excepti throw new ApplicationStartupException("Error starting Micronaut server: " + exception.getMessage(), exception); } + @SuppressWarnings("java:S106") private void printBanner() { if (!isBannerEnabled()) { return; @@ -354,7 +356,7 @@ private void printBanner() { private void printMicronautVersion(@NonNull PrintStream out) { String version = VersionUtils.getMicronautVersion(); version = (version != null) ? " (v" + version + ")" : ""; - out.println(MICRONAUT + version + "\n"); + out.println(MICRONAUT_PREFIX + version + "\n"); } @NonNull diff --git a/context/src/main/java/io/micronaut/scheduling/io/watch/DefaultWatchThread.java b/context/src/main/java/io/micronaut/scheduling/io/watch/DefaultWatchThread.java index 1a51d23b894..e7b003a7b4b 100644 --- a/context/src/main/java/io/micronaut/scheduling/io/watch/DefaultWatchThread.java +++ b/context/src/main/java/io/micronaut/scheduling/io/watch/DefaultWatchThread.java @@ -127,6 +127,7 @@ public DefaultWatchThread start() { } } catch (InterruptedException | ClosedWatchServiceException e) { // ignore + Thread.currentThread().interrupt(); } } }, "micronaut-filewatch-thread").start(); diff --git a/core-reactive/src/main/java/io/micronaut/core/async/publisher/Publishers.java b/core-reactive/src/main/java/io/micronaut/core/async/publisher/Publishers.java index 8487b3f3438..9e6124f9f06 100644 --- a/core-reactive/src/main/java/io/micronaut/core/async/publisher/Publishers.java +++ b/core-reactive/src/main/java/io/micronaut/core/async/publisher/Publishers.java @@ -83,7 +83,7 @@ public class Publishers { "io.reactivex.rxjava3.core.Observable" ); for (String name : typeNames) { - Optional aClass = ClassUtils.forName(name, classLoader); + Optional> aClass = ClassUtils.forName(name, classLoader); aClass.ifPresent(reactiveTypes::add); } for (String name : Arrays.asList( @@ -93,7 +93,7 @@ public class Publishers { "io.reactivex.rxjava3.core.Single", "io.reactivex.rxjava3.core.Maybe" )) { - Optional aClass = ClassUtils.forName(name, classLoader); + Optional> aClass = ClassUtils.forName(name, classLoader); aClass.ifPresent(aClass1 -> { singleTypes.add(aClass1); reactiveTypes.add(aClass1); @@ -101,7 +101,7 @@ public class Publishers { } for (String name : Arrays.asList("io.reactivex.Completable", "io.reactivex.rxjava3.core.Completable")) { - Optional aClass = ClassUtils.forName(name, classLoader); + Optional> aClass = ClassUtils.forName(name, classLoader); aClass.ifPresent(aClass1 -> { completableTypes.add(aClass1); reactiveTypes.add(aClass1); diff --git a/core/src/main/java/io/micronaut/core/annotation/AnnotationMetadata.java b/core/src/main/java/io/micronaut/core/annotation/AnnotationMetadata.java index 5d6eb3cd272..d93f016a12a 100644 --- a/core/src/main/java/io/micronaut/core/annotation/AnnotationMetadata.java +++ b/core/src/main/java/io/micronaut/core/annotation/AnnotationMetadata.java @@ -404,6 +404,7 @@ default Optional getDefaultValue(@NonNull Class ann * @see AnnotationSource#isAnnotationPresent(Class) */ @Override + @SuppressWarnings("java:S2583") default boolean isAnnotationPresent(@NonNull Class annotationClass) { //noinspection ConstantConditions if (annotationClass == null) { @@ -416,6 +417,7 @@ default boolean isAnnotationPresent(@NonNull Class annotat * @see AnnotationSource#isAnnotationPresent(Class) */ @Override + @SuppressWarnings("java:S2583") default boolean isDeclaredAnnotationPresent(@NonNull Class annotationClass) { //noinspection ConstantConditions if (annotationClass == null) { @@ -429,6 +431,7 @@ default boolean isDeclaredAnnotationPresent(@NonNull Class * @see AnnotationSource#isAnnotationPresent(String) */ @Override + @SuppressWarnings("java:S2583") default boolean isAnnotationPresent(@NonNull String annotationName) { //noinspection ConstantConditions if (annotationName == null) { @@ -441,6 +444,7 @@ default boolean isAnnotationPresent(@NonNull String annotationName) { * @see AnnotationSource#isAnnotationPresent(String) */ @Override + @SuppressWarnings("java:S2583") default boolean isDeclaredAnnotationPresent(@NonNull String annotationName) { //noinspection ConstantConditions if (annotationName == null) { @@ -578,8 +582,8 @@ default Optional> getDeclaredAnnotationTypeByStereot */ default Optional> getAnnotationType(@NonNull String name, @NonNull ClassLoader classLoader) { ArgumentUtils.requireNonNull("name", name); - final Optional aClass = ClassUtils.forName(name, classLoader); - Class clazz = aClass.orElse(null); + final Optional> aClass = ClassUtils.forName(name, classLoader); + Class clazz = aClass.orElse(null); if (clazz != null && Annotation.class.isAssignableFrom(clazz)) { //noinspection unchecked return (Optional) aClass; diff --git a/core/src/main/java/io/micronaut/core/bind/ArgumentBinder.java b/core/src/main/java/io/micronaut/core/bind/ArgumentBinder.java index 33041bf19fc..078143a8950 100644 --- a/core/src/main/java/io/micronaut/core/bind/ArgumentBinder.java +++ b/core/src/main/java/io/micronaut/core/bind/ArgumentBinder.java @@ -110,6 +110,7 @@ default boolean isPresentAndSatisfied() { * * @return The value */ + @SuppressWarnings({"java:S3655", "OptionalGetWithoutIsPresent"}) default T get() { return getValue().get(); } diff --git a/core/src/main/java/io/micronaut/core/convert/DefaultMutableConversionService.java b/core/src/main/java/io/micronaut/core/convert/DefaultMutableConversionService.java index b57c7f4b00f..f9d434c5241 100644 --- a/core/src/main/java/io/micronaut/core/convert/DefaultMutableConversionService.java +++ b/core/src/main/java/io/micronaut/core/convert/DefaultMutableConversionService.java @@ -233,7 +233,8 @@ private void registerDefaultConverters() { if (classLoader == null) { classLoader = DefaultMutableConversionService.class.getClassLoader(); } - return ClassUtils.forName(object.toString(), classLoader); + //noinspection rawtypes + return (Optional) ClassUtils.forName(object.toString(), classLoader); }); // AnnotationClassValue -> Class diff --git a/core/src/main/java/io/micronaut/core/io/IOUtils.java b/core/src/main/java/io/micronaut/core/io/IOUtils.java index a8b46141e75..00a3fc7764b 100644 --- a/core/src/main/java/io/micronaut/core/io/IOUtils.java +++ b/core/src/main/java/io/micronaut/core/io/IOUtils.java @@ -33,8 +33,6 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; -import java.nio.file.ProviderNotFoundException; -import java.nio.file.StandardCopyOption; import java.util.ArrayList; import java.util.Iterator; import java.util.List; @@ -141,23 +139,8 @@ private static Path loadNestedJarUri(List toClose, String jarUri) thr // This check makes our class loading resilient to that return jarPath; } - FileSystem zipfs; - try { - // can't use newFileSystem(Path) here (without CL) because it doesn't exist on java 8 - // the CL cast is necessary because since java 13 there is a newFileSystem(Path, Map) - zipfs = FileSystems.newFileSystem(jarPath, (ClassLoader) null); - toClose.add(0, zipfs); - } catch (ProviderNotFoundException e) { - // java versions earlier than 11 do not support nested zipfs and will fail with this - // exception. Try to extract the file instead. This is not efficient, but what else can - // we do? - Path tmp = Files.createTempFile("micronaut-IOUtils-nested-zip", ".zip"); - toClose.add(0, () -> Files.deleteIfExists(tmp)); - Files.copy(jarPath, tmp, StandardCopyOption.REPLACE_EXISTING); - - zipfs = FileSystems.newFileSystem(tmp, (ClassLoader) null); - toClose.add(0, zipfs); - } + FileSystem zipfs = FileSystems.newFileSystem(jarPath, (ClassLoader) null); + toClose.add(0, zipfs); return zipfs.getPath(jarUri.substring(sep + 1)); } diff --git a/core/src/main/java/io/micronaut/core/io/service/SoftServiceLoader.java b/core/src/main/java/io/micronaut/core/io/service/SoftServiceLoader.java index c2c5ec8ff73..08291b83097 100644 --- a/core/src/main/java/io/micronaut/core/io/service/SoftServiceLoader.java +++ b/core/src/main/java/io/micronaut/core/io/service/SoftServiceLoader.java @@ -150,7 +150,7 @@ public Optional> firstOr(String alternative, ClassLoader cl return Optional.of(i.next()); } - @SuppressWarnings("unchecked") Class alternativeClass = ClassUtils.forName(alternative, classLoader) + @SuppressWarnings("unchecked") Class alternativeClass = (Class) ClassUtils.forName(alternative, classLoader) .orElse(null); if (alternativeClass != null) { return Optional.of(createService(alternative, alternativeClass)); diff --git a/core/src/main/java/io/micronaut/core/reflect/ClassUtils.java b/core/src/main/java/io/micronaut/core/reflect/ClassUtils.java index f7dfc337c95..f00537c0199 100644 --- a/core/src/main/java/io/micronaut/core/reflect/ClassUtils.java +++ b/core/src/main/java/io/micronaut/core/reflect/ClassUtils.java @@ -273,7 +273,7 @@ public static Optional getPrimitiveType(String primitiveType) { * @param classLoader The classloader. If null will fallback to attempt the thread context loader, otherwise the system loader * @return An optional of the class */ - public static Optional forName(String name, @Nullable ClassLoader classLoader) { + public static Optional> forName(String name, @Nullable ClassLoader classLoader) { try { if (MISSING_TYPES.contains(name)) { return Optional.empty(); @@ -285,7 +285,7 @@ public static Optional forName(String name, @Nullable ClassLoader classLo classLoader = ClassLoader.getSystemClassLoader(); } - Optional commonType = Optional.ofNullable(COMMON_CLASS_MAP.get(name)); + Optional> commonType = Optional.ofNullable(COMMON_CLASS_MAP.get(name)); if (commonType.isPresent()) { return commonType; } else { diff --git a/core/src/main/java/io/micronaut/core/serialize/JdkSerializer.java b/core/src/main/java/io/micronaut/core/serialize/JdkSerializer.java index 8e1462a8bf5..cf14c93d3c5 100644 --- a/core/src/main/java/io/micronaut/core/serialize/JdkSerializer.java +++ b/core/src/main/java/io/micronaut/core/serialize/JdkSerializer.java @@ -121,7 +121,7 @@ protected ObjectInputStream createObjectInput(InputStream inputStream, Class return new ObjectInputStream(inputStream) { @Override protected Class resolveClass(ObjectStreamClass desc) throws IOException, ClassNotFoundException { - Optional aClass = ClassUtils.forName(desc.getName(), requiredType.getClassLoader()); + Optional> aClass = ClassUtils.forName(desc.getName(), requiredType.getClassLoader()); if (aClass.isPresent()) { return aClass.get(); } diff --git a/core/src/main/java/io/micronaut/core/util/ArgumentUtils.java b/core/src/main/java/io/micronaut/core/util/ArgumentUtils.java index d2c55a33c60..5445ab8c5dd 100644 --- a/core/src/main/java/io/micronaut/core/util/ArgumentUtils.java +++ b/core/src/main/java/io/micronaut/core/util/ArgumentUtils.java @@ -106,7 +106,7 @@ public static void validateArguments( @NonNull Argument[] arguments, @NonNull Object[] values) { int requiredCount = arguments.length; - @SuppressWarnings("ConstantConditions") int actualCount = values == null ? 0 : values.length; + @SuppressWarnings("ConstantConditions") int actualCount = ArrayUtils.isEmpty(values) ? 0 : values.length; if (requiredCount != actualCount) { throw new IllegalArgumentException("Wrong number of arguments to " + (described instanceof Executable ? "method" : "constructor") + ": " + described.getDescription()); } diff --git a/core/src/main/java/io/micronaut/core/util/ArrayUtils.java b/core/src/main/java/io/micronaut/core/util/ArrayUtils.java index 4b2a0cf2ac0..b9736b41392 100644 --- a/core/src/main/java/io/micronaut/core/util/ArrayUtils.java +++ b/core/src/main/java/io/micronaut/core/util/ArrayUtils.java @@ -35,7 +35,7 @@ * @author Graeme Rocher * @since 1.0 */ -public class ArrayUtils { +public final class ArrayUtils { /** * An empty object array. @@ -83,6 +83,9 @@ public class ArrayUtils { */ public static final short[] EMPTY_SHORT_ARRAY = new short[0]; + private ArrayUtils() { + } + /** * Concatenate two arrays. * @@ -312,25 +315,25 @@ public static void reverse(T[] input) { */ private static final class ArrayIterator implements Iterator, Iterable { - private final T[] _a; - private int _index; + private final T[] array; + private int index; private ArrayIterator(T[] a) { - _a = a; - _index = 0; + array = a; + index = 0; } @Override public boolean hasNext() { - return _index < _a.length; + return index < array.length; } @Override public T next() { - if (_index >= _a.length) { + if (index >= array.length) { throw new NoSuchElementException(); } - return _a[_index++]; + return array[index++]; } @Override public void remove() { @@ -338,7 +341,7 @@ public T next() { } @Override public Iterator iterator() { - return this; + return new ArrayIterator<>(array); } } @@ -350,25 +353,25 @@ public T next() { */ private static final class ReverseArrayIterator implements Iterator, Iterable { - private final T[] _a; - private int _index; + private final T[] array; + private int index; private ReverseArrayIterator(T[] a) { - _a = a; - _index = a.length > 0 ? a.length - 1 : -1; + array = a; + index = a.length > 0 ? a.length - 1 : -1; } @Override public boolean hasNext() { - return _index > -1; + return index > -1; } @Override public T next() { - if (_index <= -1) { + if (index <= -1) { throw new NoSuchElementException(); } - return _a[_index--]; + return array[index--]; } @Override public void remove() { @@ -376,7 +379,7 @@ public T next() { } @Override public Iterator iterator() { - return this; + return new ReverseArrayIterator<>(array); } } } diff --git a/http-client-core/src/main/java/io/micronaut/http/client/interceptor/HttpClientIntroductionAdvice.java b/http-client-core/src/main/java/io/micronaut/http/client/interceptor/HttpClientIntroductionAdvice.java index 6a59806a8df..df185295320 100644 --- a/http-client-core/src/main/java/io/micronaut/http/client/interceptor/HttpClientIntroductionAdvice.java +++ b/http-client-core/src/main/java/io/micronaut/http/client/interceptor/HttpClientIntroductionAdvice.java @@ -161,7 +161,7 @@ public Object intercept(MethodInvocationContext context) { Optional> httpMethodMapping = context.getAnnotationTypeByStereotype(HttpMethodMapping.class); HttpClient httpClient = clientFactory.getClient(annotationMetadata); - if (context.hasStereotype(HttpMethodMapping.class) && httpClient != null) { + if (httpMethodMapping.isPresent() && context.hasStereotype(HttpMethodMapping.class) && httpClient != null) { AnnotationValue mapping = context.getAnnotation(HttpMethodMapping.class); String uri = mapping.getRequiredValue(String.class); if (StringUtils.isEmpty(uri)) { diff --git a/http-client/src/main/java/io/micronaut/http/client/netty/ConnectionManager.java b/http-client/src/main/java/io/micronaut/http/client/netty/ConnectionManager.java index 7221852440c..0039cc29da1 100644 --- a/http-client/src/main/java/io/micronaut/http/client/netty/ConnectionManager.java +++ b/http-client/src/main/java/io/micronaut/http/client/netty/ConnectionManager.java @@ -308,6 +308,7 @@ public void shutdown() { future.await(shutdownTimeout.toMillis()); } catch (InterruptedException e) { // ignore + Thread.currentThread().interrupt(); } } } diff --git a/http-client/src/main/java/io/micronaut/http/client/netty/ssl/NettyClientSslBuilder.java b/http-client/src/main/java/io/micronaut/http/client/netty/ssl/NettyClientSslBuilder.java index e8e3864415f..6bcab80613c 100644 --- a/http-client/src/main/java/io/micronaut/http/client/netty/ssl/NettyClientSslBuilder.java +++ b/http-client/src/main/java/io/micronaut/http/client/netty/ssl/NettyClientSslBuilder.java @@ -84,16 +84,19 @@ public SslContext build(SslConfiguration ssl, HttpVersionSelection versionSelect .forClient() .keyManager(getKeyManagerFactory(ssl)) .trustManager(getTrustManagerFactory(ssl)); - if (ssl.getProtocols().isPresent()) { - sslBuilder.protocols(ssl.getProtocols().get()); + Optional protocols = ssl.getProtocols(); + if (protocols.isPresent()) { + sslBuilder.protocols(protocols.get()); } - if (ssl.getCiphers().isPresent()) { - sslBuilder = sslBuilder.ciphers(Arrays.asList(ssl.getCiphers().get())); + Optional ciphers = ssl.getCiphers(); + if (ciphers.isPresent()) { + sslBuilder = sslBuilder.ciphers(Arrays.asList(ciphers.get())); } else if (versionSelection.isHttp2CipherSuites()) { sslBuilder.ciphers(Http2SecurityUtil.CIPHERS, SupportedCipherSuiteFilter.INSTANCE); } - if (ssl.getClientAuthentication().isPresent()) { - ClientAuthentication clientAuth = ssl.getClientAuthentication().get(); + Optional clientAuthentication = ssl.getClientAuthentication(); + if (clientAuthentication.isPresent()) { + ClientAuthentication clientAuth = clientAuthentication.get(); if (clientAuth == ClientAuthentication.NEED) { sslBuilder = sslBuilder.clientAuth(ClientAuth.REQUIRE); } else if (clientAuth == ClientAuthentication.WANT) { diff --git a/http-netty/src/main/java/io/micronaut/http/netty/AbstractNettyHttpRequest.java b/http-netty/src/main/java/io/micronaut/http/netty/AbstractNettyHttpRequest.java index de3e153d166..ce2e3b28dc5 100644 --- a/http-netty/src/main/java/io/micronaut/http/netty/AbstractNettyHttpRequest.java +++ b/http-netty/src/main/java/io/micronaut/http/netty/AbstractNettyHttpRequest.java @@ -163,17 +163,17 @@ public io.netty.handler.codec.http.HttpRequest getNettyRequest() { @Override public HttpParameters getParameters() { - NettyHttpParameters httpParameters = this.httpParameters; - if (httpParameters == null) { + NettyHttpParameters params = this.httpParameters; + if (params == null) { synchronized (this) { // double check - httpParameters = this.httpParameters; - if (httpParameters == null) { - httpParameters = decodeParameters(); - this.httpParameters = httpParameters; + params = this.httpParameters; + if (params == null) { + params = decodeParameters(); + this.httpParameters = params; } } } - return httpParameters; + return params; } @Override @@ -185,6 +185,7 @@ public Collection accept() { } @Override + @SuppressWarnings("java:S2789") // performance opt public Optional getContentType() { if (mediaType == null) { mediaType = HttpRequest.super.getContentType(); @@ -201,6 +202,7 @@ public Charset getCharacterEncoding() { } @Override + @SuppressWarnings("java:S2789") // performance opt public Optional getLocale() { if (locale == null) { locale = HttpRequest.super.getLocale(); @@ -220,17 +222,17 @@ public URI getUri() { @Override public String getPath() { - String path = this.path; - if (path == null) { + String p = this.path; + if (p == null) { synchronized (this) { // double check - path = this.path; - if (path == null) { - path = decodePath(); - this.path = path; + p = this.path; + if (p == null) { + p = decodePath(); + this.path = p; } } } - return path; + return p; } /** @@ -243,9 +245,10 @@ public String getPath() { * @param uri The URI * @return The query string decoder */ + @SuppressWarnings("ConstantConditions") protected final QueryStringDecoder createDecoder(URI uri) { - Charset charset = getCharacterEncoding(); - return charset != null ? new QueryStringDecoder(uri, charset) : new QueryStringDecoder(uri); + Charset cs = getCharacterEncoding(); + return cs != null ? new QueryStringDecoder(uri, cs) : new QueryStringDecoder(uri); } private String decodePath() { diff --git a/http-netty/src/main/java/io/micronaut/http/netty/websocket/NettyServerWebSocketBroadcaster.java b/http-netty/src/main/java/io/micronaut/http/netty/websocket/NettyServerWebSocketBroadcaster.java index e7c2b847407..392167ea5a1 100644 --- a/http-netty/src/main/java/io/micronaut/http/netty/websocket/NettyServerWebSocketBroadcaster.java +++ b/http-netty/src/main/java/io/micronaut/http/netty/websocket/NettyServerWebSocketBroadcaster.java @@ -68,6 +68,7 @@ public void broadcastSync(T message, MediaType mediaType, Predicate logLevel = server.getServerConfiguration().getLogLevel(); + loggingHandler = logLevel.map(level -> new LoggingHandler(NettyHttpServer.class, level)).orElse(null); sslContext = embeddedServices.getServerSslBuilder() != null ? embeddedServices.getServerSslBuilder().build().orElse(null) : null; NettyHttpServerConfiguration.AccessLogger accessLogger = server.getServerConfiguration().getAccessLogger(); @@ -392,18 +395,18 @@ public void upgradeTo(ChannelHandlerContext ctx, FullHttpRequest upgradeRequest) @Override protected void channelRead0(ChannelHandlerContext ctx, HttpMessage msg) { // If this handler is hit then no upgrade has been attempted and the client is just talking HTTP. - ChannelPipeline pipeline = ctx.pipeline(); + ChannelPipeline cp = ctx.pipeline(); // remove the handlers we don't need anymore - pipeline.remove(upgradeHandler); - pipeline.remove(this); + cp.remove(upgradeHandler); + cp.remove(this); // reconfigure for http1 // note: we have to reuse the serverCodec in case it still has some data buffered new StreamPipeline(channel, ssl, connectionCustomizer).insertHttp1DownstreamHandlers(); connectionCustomizer.onStreamPipelineBuilt(); onRequestPipelineBuilt(); - pipeline.fireChannelRead(ReferenceCountUtil.retain(msg)); + cp.fireChannelRead(ReferenceCountUtil.retain(msg)); } }); connectionCustomizer.onInitialPipelineBuilt(); diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/NettyEmbeddedServerFactory.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/NettyEmbeddedServerFactory.java index 745c6854dbd..a8871624f61 100644 --- a/http-server-netty/src/main/java/io/micronaut/http/server/netty/NettyEmbeddedServerFactory.java +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/NettyEmbeddedServerFactory.java @@ -51,6 +51,6 @@ public interface NettyEmbeddedServerFactory { */ @NonNull default NettyEmbeddedServer build(@NonNull NettyHttpServerConfiguration configuration, @Nullable ServerSslConfiguration sslConfiguration) { - return build(configuration, null); + return build(configuration); } } diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/NettyHttpRequest.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/NettyHttpRequest.java index f2b245a75cc..e1a85e6ac66 100644 --- a/http-server-netty/src/main/java/io/micronaut/http/server/netty/NettyHttpRequest.java +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/NettyHttpRequest.java @@ -559,6 +559,9 @@ protected void initChannel(@NonNull Http2StreamChannel ch) throws Exception { try { future.sync(); } catch (Exception e) { + if (e instanceof InterruptedException) { + Thread.currentThread().interrupt(); + } LOG.warn("Failed to complete push promise", e); } }); diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/ssl/CertificateProvidedSslBuilder.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/ssl/CertificateProvidedSslBuilder.java index d6599ddd16e..a927e02151e 100644 --- a/http-server-netty/src/main/java/io/micronaut/http/server/netty/ssl/CertificateProvidedSslBuilder.java +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/ssl/CertificateProvidedSslBuilder.java @@ -107,17 +107,20 @@ public Optional build(SslConfiguration ssl, HttpVersion httpVersion) } static void setupSslBuilder(SslContextBuilder sslBuilder, SslConfiguration ssl, HttpVersion httpVersion) { - if (ssl.getProtocols().isPresent()) { - sslBuilder.protocols(ssl.getProtocols().get()); + Optional protocols = ssl.getProtocols(); + if (protocols.isPresent()) { + sslBuilder.protocols(protocols.get()); } final boolean isHttp2 = httpVersion == HttpVersion.HTTP_2_0; - if (ssl.getCiphers().isPresent()) { - sslBuilder = sslBuilder.ciphers(Arrays.asList(ssl.getCiphers().get())); + Optional ciphers = ssl.getCiphers(); + if (ciphers.isPresent()) { + sslBuilder = sslBuilder.ciphers(Arrays.asList(ciphers.get())); } else if (isHttp2) { sslBuilder.ciphers(Http2SecurityUtil.CIPHERS, SupportedCipherSuiteFilter.INSTANCE); } - if (ssl.getClientAuthentication().isPresent()) { - ClientAuthentication clientAuth = ssl.getClientAuthentication().get(); + Optional clientAuthentication = ssl.getClientAuthentication(); + if (clientAuthentication.isPresent()) { + ClientAuthentication clientAuth = clientAuthentication.get(); if (clientAuth == ClientAuthentication.NEED) { sslBuilder.clientAuth(ClientAuth.REQUIRE); } else if (clientAuth == ClientAuthentication.WANT) { diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/types/DefaultCustomizableResponseTypeHandlerRegistry.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/types/DefaultCustomizableResponseTypeHandlerRegistry.java index 228f57c1a07..2ee133c1ef4 100644 --- a/http-server-netty/src/main/java/io/micronaut/http/server/netty/types/DefaultCustomizableResponseTypeHandlerRegistry.java +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/types/DefaultCustomizableResponseTypeHandlerRegistry.java @@ -50,6 +50,7 @@ public DefaultCustomizableResponseTypeHandlerRegistry(List findTypeHandler(Class type) { Optional foundHandler = handlerCache.get(type); if (foundHandler != null) { diff --git a/http/src/main/java/io/micronaut/http/MediaType.java b/http/src/main/java/io/micronaut/http/MediaType.java index a44b3c75d45..bcdca2078fe 100644 --- a/http/src/main/java/io/micronaut/http/MediaType.java +++ b/http/src/main/java/io/micronaut/http/MediaType.java @@ -22,6 +22,7 @@ import io.micronaut.core.convert.ImmutableArgumentConversionContext; import io.micronaut.core.naming.NameUtils; import io.micronaut.core.type.Argument; +import io.micronaut.core.util.ArrayUtils; import io.micronaut.core.util.CollectionUtils; import io.micronaut.core.util.StringUtils; import io.micronaut.core.value.OptionalValues; @@ -823,8 +824,9 @@ public static MediaType[] of(CharSequence... mediaType) { public static Optional fromType(Class type) { Produces producesAnn = type.getAnnotation(Produces.class); if (producesAnn != null) { - for (String mimeType : producesAnn.value()) { - return Optional.of(MediaType.of(mimeType)); + String[] value = producesAnn.value(); + if (ArrayUtils.isNotEmpty(value)) { + return Optional.of(MediaType.of(value[0])); } } return Optional.empty(); @@ -838,9 +840,12 @@ public static Optional fromType(Class type) { */ public static Optional forExtension(String extension) { if (StringUtils.isNotEmpty(extension)) { - String type = getMediaTypeFileExtensions().get(extension); - if (type != null) { - return Optional.of(new MediaType(type, extension)); + Map extensions = getMediaTypeFileExtensions(); + if (extensions != null) { + String type = extensions.get(extension); + if (type != null) { + return Optional.of(new MediaType(type, extension)); + } } } return Optional.empty(); diff --git a/http/src/main/java/io/micronaut/http/bind/binders/DefaultBodyAnnotationBinder.java b/http/src/main/java/io/micronaut/http/bind/binders/DefaultBodyAnnotationBinder.java index 244b6038aee..9c8e06dcd32 100644 --- a/http/src/main/java/io/micronaut/http/bind/binders/DefaultBodyAnnotationBinder.java +++ b/http/src/main/java/io/micronaut/http/bind/binders/DefaultBodyAnnotationBinder.java @@ -81,9 +81,9 @@ public BindingResult bind(ArgumentConversionContext context, HttpRequest newResult(T converted, ArgumentConversionContext context) { final Optional lastError = context.getLastError(); - //noinspection OptionalIsPresent if (lastError.isPresent()) { return new BindingResult() { @Override diff --git a/http/src/main/java/io/micronaut/http/codec/DefaultMediaTypeCodecRegistry.java b/http/src/main/java/io/micronaut/http/codec/DefaultMediaTypeCodecRegistry.java index 349f107cef0..0a8b7a390d5 100644 --- a/http/src/main/java/io/micronaut/http/codec/DefaultMediaTypeCodecRegistry.java +++ b/http/src/main/java/io/micronaut/http/codec/DefaultMediaTypeCodecRegistry.java @@ -66,6 +66,7 @@ public class DefaultMediaTypeCodecRegistry implements MediaTypeCodecRegistry { } @Override + @SuppressWarnings("java:S2789") // performance optimization public Optional findCodec(@Nullable MediaType mediaType) { if (mediaType == null) { return Optional.empty(); diff --git a/http/src/main/java/io/micronaut/http/simple/cookies/SimpleCookie.java b/http/src/main/java/io/micronaut/http/simple/cookies/SimpleCookie.java index ac161f717f7..0d56077a0b5 100644 --- a/http/src/main/java/io/micronaut/http/simple/cookies/SimpleCookie.java +++ b/http/src/main/java/io/micronaut/http/simple/cookies/SimpleCookie.java @@ -229,8 +229,9 @@ public String toString() { if (isHttpOnly()) { buf.append(", HTTPOnly"); } - if (getSameSite().isPresent()) { - buf.append(", SameSite=").append(getSameSite().get()); + Optional ss = getSameSite(); + if (ss.isPresent()) { + buf.append(", SameSite=").append(ss.get()); } return buf.toString(); } diff --git a/http/src/main/java/io/micronaut/http/ssl/SslBuilder.java b/http/src/main/java/io/micronaut/http/ssl/SslBuilder.java index 9cbbc8ca2f9..6e198743425 100644 --- a/http/src/main/java/io/micronaut/http/ssl/SslBuilder.java +++ b/http/src/main/java/io/micronaut/http/ssl/SslBuilder.java @@ -83,11 +83,13 @@ protected TrustManagerFactory getTrustManagerFactory(SslConfiguration ssl) { */ protected Optional getTrustStore(SslConfiguration ssl) throws Exception { SslConfiguration.TrustStoreConfiguration trustStore = ssl.getTrustStore(); - if (!trustStore.getPath().isPresent()) { + Optional path = trustStore.getPath(); + if (path.isPresent()) { + return Optional.of(load(trustStore.getType(), + path.get(), trustStore.getPassword())); + } else { return Optional.empty(); } - return Optional.of(load(trustStore.getType(), - trustStore.getPath().get(), trustStore.getPassword())); } /** @@ -102,8 +104,9 @@ protected KeyManagerFactory getKeyManagerFactory(SslConfiguration ssl) { .getInstance(KeyManagerFactory.getDefaultAlgorithm()); Optional password = ssl.getKey().getPassword(); char[] keyPassword = password.map(String::toCharArray).orElse(null); - if (keyPassword == null && ssl.getKeyStore().getPassword().isPresent()) { - keyPassword = ssl.getKeyStore().getPassword().get().toCharArray(); + Optional pwd = ssl.getKeyStore().getPassword(); + if (keyPassword == null && pwd.isPresent()) { + keyPassword = pwd.get().toCharArray(); } keyManagerFactory.init(keyStore.orElse(null), keyPassword); return keyManagerFactory; @@ -120,11 +123,13 @@ protected KeyManagerFactory getKeyManagerFactory(SslConfiguration ssl) { */ protected Optional getKeyStore(SslConfiguration ssl) throws Exception { SslConfiguration.KeyStoreConfiguration keyStore = ssl.getKeyStore(); - if (!keyStore.getPath().isPresent()) { + Optional path = keyStore.getPath(); + if (path.isPresent()) { + return Optional.of(load(keyStore.getType(), + path.get(), keyStore.getPassword())); + } else { return Optional.empty(); } - return Optional.of(load(keyStore.getType(), - keyStore.getPath().get(), keyStore.getPassword())); } /** diff --git a/http/src/main/java/io/micronaut/http/uri/UriMatchTemplate.java b/http/src/main/java/io/micronaut/http/uri/UriMatchTemplate.java index b4211caf74e..886a237f8be 100644 --- a/http/src/main/java/io/micronaut/http/uri/UriMatchTemplate.java +++ b/http/src/main/java/io/micronaut/http/uri/UriMatchTemplate.java @@ -150,6 +150,7 @@ public String toPathString() { * @return True if it matches */ @Override + @SuppressWarnings("java:S2789") // performance optimization public Optional match(String uri) { if (uri == null) { throw new IllegalArgumentException("Argument 'uri' cannot be null"); diff --git a/inject-java/src/main/java/io/micronaut/annotation/processing/AnnotationProcessingOutputVisitor.java b/inject-java/src/main/java/io/micronaut/annotation/processing/AnnotationProcessingOutputVisitor.java index 3e2ec991ec7..048e50fd3d8 100644 --- a/inject-java/src/main/java/io/micronaut/annotation/processing/AnnotationProcessingOutputVisitor.java +++ b/inject-java/src/main/java/io/micronaut/annotation/processing/AnnotationProcessingOutputVisitor.java @@ -76,8 +76,12 @@ public class AnnotationProcessingOutputVisitor extends AbstractClassWriterOutput public AnnotationProcessingOutputVisitor(Filer filer) { super(isEclipseFiler(filer)); this.filer = filer; - final String filerName = filer.getClass().getName(); - this.isGradleFiler = filerName.startsWith("org.gradle.api") || filerName.startsWith("org.jetbrains.kotlin.kapt3"); + if (filer != null) { + final String filerName = filer.getClass().getName(); + this.isGradleFiler = filerName.startsWith("org.gradle.api") || filerName.startsWith("org.jetbrains.kotlin.kapt3"); + } else { + this.isGradleFiler = false; + } } //--add-opens=java.base/$hostPackageName=ALL-UNNAMED diff --git a/inject/src/main/java/io/micronaut/context/AbstractBeanDefinition.java b/inject/src/main/java/io/micronaut/context/AbstractBeanDefinition.java index d680d1273d6..8ed92d76b02 100644 --- a/inject/src/main/java/io/micronaut/context/AbstractBeanDefinition.java +++ b/inject/src/main/java/io/micronaut/context/AbstractBeanDefinition.java @@ -1024,7 +1024,11 @@ protected final Object getBeanForConstructorArgument(BeanResolutionContext resol return resolutionContext; } else if (argument.isArray()) { Collection beansOfType = getBeansOfTypeForConstructorArgument(resolutionContext, context, constructorInjectionPoint, argument); - return beansOfType.toArray((Object[]) Array.newInstance(beanType.getComponentType(), beansOfType.size())); + if (beansOfType != null) { + return beansOfType.toArray((Object[]) Array.newInstance(beanType.getComponentType(), beansOfType.size())); + } else { + return Array.newInstance(beanType.getComponentType(), 0); + } } else if (Collection.class.isAssignableFrom(beanType)) { Collection beansOfType = getBeansOfTypeForConstructorArgument(resolutionContext, context, constructorInjectionPoint, argument); return coerceCollectionToCorrectType(beanType, beansOfType); @@ -1695,7 +1699,11 @@ protected final Object getBeanForField(BeanResolutionContext resolutionContext, final Class beanClass = injectionPoint.getType(); if (beanClass.isArray()) { Collection beansOfType = getBeansOfTypeForField(resolutionContext, context, injectionPoint); - return beansOfType.toArray((Object[]) Array.newInstance(beanClass.getComponentType(), beansOfType.size())); + if (beansOfType != null) { + return beansOfType.toArray((Object[]) Array.newInstance(beanClass.getComponentType(), beansOfType.size())); + } else { + return Array.newInstance(beanClass.getComponentType(), 0); + } } else if (Collection.class.isAssignableFrom(beanClass)) { Collection beansOfType = getBeansOfTypeForField(resolutionContext, context, injectionPoint); if (beanClass.isInstance(beansOfType)) { @@ -2033,6 +2041,7 @@ private boolean isInnerConfiguration(Argument argumentType, BeanContext beanC beanContext.findBeanDefinition(argumentType).map(bd -> bd.hasStereotype(ConfigurationReader.class) || bd.isIterable()).isPresent(); } + @SuppressWarnings("java:S1872") // internal requirement private boolean isInnerOfAnySuperclass(Class argumentType) { Class beanType = getBeanType(); while (beanType != null) { @@ -2145,6 +2154,7 @@ private BeanRegistration resolveBeanRegistrationWithGenericsFromArgument( } } + @SuppressWarnings("java:S2259") // false positive private Object doResolveBeanRegistrations(BeanResolutionContext resolutionContext, DefaultBeanContext context, Argument argument, BeanResolutionContext.Path path) { final Collection> beanRegistrations = resolveBeanRegistrationsWithGenericsFromArgument(resolutionContext, argument, path, (beanType, qualifier) -> context.getBeanRegistrations(resolutionContext, beanType, qualifier) diff --git a/inject/src/main/java/io/micronaut/context/AbstractInitializableBeanDefinition.java b/inject/src/main/java/io/micronaut/context/AbstractInitializableBeanDefinition.java index 0c480007644..6018cc4df7f 100644 --- a/inject/src/main/java/io/micronaut/context/AbstractInitializableBeanDefinition.java +++ b/inject/src/main/java/io/micronaut/context/AbstractInitializableBeanDefinition.java @@ -246,6 +246,7 @@ public final boolean isContainerType() { } @Override + @SuppressWarnings("java:S2789") // performance optimization public final Optional> getContainerElement() { if (isContainerType) { if (containerElement != null) { @@ -385,7 +386,12 @@ public final Optional> getDeclaringType() { @Override public final ConstructorInjectionPoint getConstructor() { if (constructor == null) { - constructorInjectionPoint = null; + constructorInjectionPoint = new DefaultConstructorInjectionPoint<>( + this, + getBeanType(), + AnnotationMetadata.EMPTY_METADATA, + Argument.ZERO_ARGUMENTS + ); } else { if (constructor instanceof MethodReference) { MethodReference methodConstructor = (MethodReference) constructor; @@ -736,6 +742,7 @@ public final Argument[] getRequiredArguments() { * @return The instantiated bean * @throws BeanInstantiationException If the bean cannot be instantiated for the arguments supplied */ + @SuppressWarnings({"java:S2789", "OptionalAssignedToNull"}) // performance optimization public final T build(BeanResolutionContext resolutionContext, BeanContext context, BeanDefinition definition, @@ -1285,7 +1292,7 @@ protected final Stream getStreamOfTypeForMethodArgument(BeanResolutionContext protected final Object getBeanForConstructorArgument(BeanResolutionContext resolutionContext, BeanContext context, int argIndex, Qualifier qualifier) { MethodReference constructorMethodRef = (MethodReference) constructor; Argument argument = resolveArgument(context, argIndex, constructorMethodRef.arguments); - if (argument.isDeclaredNullable()) { + if (argument != null && argument.isDeclaredNullable()) { BeanResolutionContext.Segment current = resolutionContext.getPath().peek(); if (current != null && current.getArgument().equals(argument)) { return null; diff --git a/inject/src/main/java/io/micronaut/context/ApplicationContextBuilder.java b/inject/src/main/java/io/micronaut/context/ApplicationContextBuilder.java index 01033c7e5fd..d0ba8ab5ab3 100644 --- a/inject/src/main/java/io/micronaut/context/ApplicationContextBuilder.java +++ b/inject/src/main/java/io/micronaut/context/ApplicationContextBuilder.java @@ -174,7 +174,7 @@ public interface ApplicationContextBuilder { * @param mainClass The main class * @return This builder */ - @NonNull ApplicationContextBuilder mainClass(@Nullable Class mainClass); + @NonNull ApplicationContextBuilder mainClass(@Nullable Class mainClass); /** * The class loader to be used. diff --git a/inject/src/main/java/io/micronaut/context/DefaultApplicationContext.java b/inject/src/main/java/io/micronaut/context/DefaultApplicationContext.java index f3017274ef6..ed6a417a8d2 100644 --- a/inject/src/main/java/io/micronaut/context/DefaultApplicationContext.java +++ b/inject/src/main/java/io/micronaut/context/DefaultApplicationContext.java @@ -223,7 +223,7 @@ public Map getProperties(@Nullable String name, @Nullable String } @Override - protected void registerConfiguration(BeanConfiguration configuration) { + protected synchronized void registerConfiguration(BeanConfiguration configuration) { if (getEnvironment().isActive(configuration)) { super.registerConfiguration(configuration); } diff --git a/inject/src/main/java/io/micronaut/context/DefaultApplicationContextBuilder.java b/inject/src/main/java/io/micronaut/context/DefaultApplicationContextBuilder.java index 22009c07f7b..20445eca4d2 100644 --- a/inject/src/main/java/io/micronaut/context/DefaultApplicationContextBuilder.java +++ b/inject/src/main/java/io/micronaut/context/DefaultApplicationContextBuilder.java @@ -262,7 +262,7 @@ public boolean isEnvironmentPropertySource() { } @Override - public @NonNull ApplicationContextBuilder mainClass(Class mainClass) { + public @NonNull ApplicationContextBuilder mainClass(Class mainClass) { if (mainClass != null) { if (this.classLoader == null) { this.classLoader = mainClass.getClassLoader(); diff --git a/inject/src/main/java/io/micronaut/context/DefaultBeanContext.java b/inject/src/main/java/io/micronaut/context/DefaultBeanContext.java index 4f60f82dc34..d80aaf445ff 100644 --- a/inject/src/main/java/io/micronaut/context/DefaultBeanContext.java +++ b/inject/src/main/java/io/micronaut/context/DefaultBeanContext.java @@ -1469,6 +1469,7 @@ public Optional> findProxyTargetBeanDefinition(@NonNull Cl } @Override + @SuppressWarnings("java:S2789") // performance optimization public Optional> findProxyTargetBeanDefinition(@NonNull Argument beanType, @Nullable Qualifier qualifier) { ArgumentUtils.requireNonNull("beanType", beanType); @SuppressWarnings("unchecked") @@ -2825,6 +2826,7 @@ private BeanRegistration provideInjectionPoint(BeanResolutionContext reso final BeanResolutionContext.Path path = resolutionContext != null ? resolutionContext.getPath() : null; BeanResolutionContext.Segment injectionPointSegment = null; if (CollectionUtils.isNotEmpty(path)) { + @SuppressWarnings("java:S2259") // false positive final Iterator> i = path.iterator(); injectionPointSegment = i.next(); BeanResolutionContext.Segment segment = null; @@ -3053,7 +3055,7 @@ final BeanRegistration createRegistration(@Nullable BeanResolutionContext * @param The bean generic type * @return The concrete bean definition candidate */ - @SuppressWarnings({"unchecked", "rawtypes"}) + @SuppressWarnings({"unchecked", "rawtypes", "java:S2789"}) // performance optimization private Optional> findConcreteCandidate(@Nullable BeanResolutionContext resolutionContext, @NonNull Argument beanType, @Nullable Qualifier qualifier, diff --git a/inject/src/main/java/io/micronaut/context/RequiresCondition.java b/inject/src/main/java/io/micronaut/context/RequiresCondition.java index 7b9f261b80a..c72a104394a 100644 --- a/inject/src/main/java/io/micronaut/context/RequiresCondition.java +++ b/inject/src/main/java/io/micronaut/context/RequiresCondition.java @@ -423,6 +423,7 @@ private boolean matchesSdk(ConditionContext context, AnnotationValue r // non-semantic versioning in play int majorVersion = resolveJavaMajorVersion(javaVersion); + @SuppressWarnings("java:S2259") // false positive int requiredVersion = resolveJavaMajorVersion(version); if (majorVersion >= requiredVersion) { diff --git a/inject/src/main/java/io/micronaut/context/env/DefaultEnvironment.java b/inject/src/main/java/io/micronaut/context/env/DefaultEnvironment.java index 69c2a607a4f..9d5c234985c 100644 --- a/inject/src/main/java/io/micronaut/context/env/DefaultEnvironment.java +++ b/inject/src/main/java/io/micronaut/context/env/DefaultEnvironment.java @@ -953,6 +953,7 @@ private static boolean isOracleCloudWindows() { } } catch (InterruptedException e) { // test negative + Thread.currentThread().interrupt(); } return false; } @@ -994,6 +995,7 @@ private static boolean isEC2Windows() { } } catch (InterruptedException e) { // test negative + Thread.currentThread().interrupt(); } return false; } diff --git a/inject/src/main/java/io/micronaut/context/i18n/ResourceBundleMessageSource.java b/inject/src/main/java/io/micronaut/context/i18n/ResourceBundleMessageSource.java index 059f119624f..090021fe73f 100644 --- a/inject/src/main/java/io/micronaut/context/i18n/ResourceBundleMessageSource.java +++ b/inject/src/main/java/io/micronaut/context/i18n/ResourceBundleMessageSource.java @@ -76,6 +76,7 @@ public ResourceBundleMessageSource(@NonNull String baseName, @Nullable Locale de @NonNull @Override + @SuppressWarnings("java:S2789") // performance optimization public Optional getRawMessage(@NonNull String code, @NonNull MessageContext context) { final Locale locale = defaultBundle != null ? context.getLocale(defaultBundle.getLocale()) : context.getLocale(); MessageKey messageKey = new MessageKey(locale, code); @@ -142,6 +143,7 @@ private Optional resolveDefault(@NonNull String code) { return opt; } + @SuppressWarnings("java:S2789") // performance optimization private Optional resolveBundle(Locale locale) { MessageKey key = new MessageKey(locale, baseName); final Optional resourceBundle = bundleCache.get(key); diff --git a/inject/src/main/java/io/micronaut/inject/annotation/AbstractAnnotationMetadata.java b/inject/src/main/java/io/micronaut/inject/annotation/AbstractAnnotationMetadata.java index e159d608a8f..ae23a8fd4d7 100644 --- a/inject/src/main/java/io/micronaut/inject/annotation/AbstractAnnotationMetadata.java +++ b/inject/src/main/java/io/micronaut/inject/annotation/AbstractAnnotationMetadata.java @@ -214,13 +214,14 @@ private Annotation[] initializeAnnotations(Set names) { if (CollectionUtils.isNotEmpty(names)) { List annotations = new ArrayList<>(); for (String name : names) { - Optional loaded = ClassUtils.forName(name, getClass().getClassLoader()); - loaded.ifPresent(aClass -> { + @SuppressWarnings("unchecked") + Class aClass = (Class) ClassUtils.forName(name, getClass().getClassLoader()).orElse(null); + if (aClass != null) { Annotation ann = synthesize(aClass); if (ann != null) { annotations.add(ann); } - }); + } } return annotations.toArray(new Annotation[0]); } diff --git a/inject/src/main/java/io/micronaut/inject/annotation/AnnotationConvertersRegistrar.java b/inject/src/main/java/io/micronaut/inject/annotation/AnnotationConvertersRegistrar.java index 924cddebc43..8dd5e1d7fe1 100644 --- a/inject/src/main/java/io/micronaut/inject/annotation/AnnotationConvertersRegistrar.java +++ b/inject/src/main/java/io/micronaut/inject/annotation/AnnotationConvertersRegistrar.java @@ -38,21 +38,27 @@ public final class AnnotationConvertersRegistrar implements TypeConverterRegistr @Override public void register(MutableConversionService conversionService) { conversionService.addConverter(io.micronaut.core.annotation.AnnotationValue.class, Annotation.class, (object, targetType, context) -> { - Optional annotationClass = ClassUtils.forName(object.getAnnotationName(), targetType.getClassLoader()); - return annotationClass.map(aClass -> AnnotationMetadataSupport.buildAnnotation(aClass, object)); + @SuppressWarnings("unchecked") Class aClass = + (Class) ClassUtils.forName(object.getAnnotationName(), targetType.getClassLoader()).orElse(null); + + if (aClass != null) { + return Optional.of(AnnotationMetadataSupport.buildAnnotation(aClass, object)); + } + return Optional.empty(); }); conversionService.addConverter(io.micronaut.core.annotation.AnnotationValue[].class, Object[].class, (object, targetType, context) -> { - List result = new ArrayList(); - Class annotationClass = null; + List result = new ArrayList<>(); + Class annotationClass = null; for (io.micronaut.core.annotation.AnnotationValue annotationValue : object) { if (annotationClass == null) { // all annotations will be on the same type - Optional aClass = ClassUtils.forName(annotationValue.getAnnotationName(), targetType.getClassLoader()); - if (!aClass.isPresent()) { + @SuppressWarnings("unchecked") Class aClass = + (Class) ClassUtils.forName(annotationValue.getAnnotationName(), targetType.getClassLoader()).orElse(null); + if (aClass == null) { break; } - annotationClass = aClass.get(); + annotationClass = aClass; } Annotation annotation = AnnotationMetadataSupport.buildAnnotation(annotationClass, annotationValue); result.add(annotation); diff --git a/inject/src/main/java/io/micronaut/inject/annotation/AnnotationMetadataSupport.java b/inject/src/main/java/io/micronaut/inject/annotation/AnnotationMetadataSupport.java index 62bcb0c3098..063a5b36036 100644 --- a/inject/src/main/java/io/micronaut/inject/annotation/AnnotationMetadataSupport.java +++ b/inject/src/main/java/io/micronaut/inject/annotation/AnnotationMetadataSupport.java @@ -41,7 +41,6 @@ import java.util.*; import java.util.concurrent.ConcurrentHashMap; import java.util.function.Consumer; -import java.util.function.Function; /** * Support method for {@link io.micronaut.core.annotation.AnnotationMetadata}. @@ -156,15 +155,13 @@ static Optional> getAnnotationType(String name, Clas return Optional.of(type); } else { // last resort, try dynamic load, shouldn't normally happen. - final Optional aClass = ClassUtils.forName(name, classLoader); - return aClass.flatMap((Function>>) aClass1 -> { - if (Annotation.class.isAssignableFrom(aClass1)) { - //noinspection unchecked - ANNOTATION_TYPES.put(name, aClass1); - return Optional.of(aClass1); - } - return Optional.empty(); - }); + @SuppressWarnings("unchecked") final Class aClass = + (Class) ClassUtils.forName(name, classLoader).orElse(null); + if (aClass != null && Annotation.class.isAssignableFrom(aClass)) { + ANNOTATION_TYPES.put(name, aClass); + return Optional.of(aClass); + } + return Optional.empty(); } } diff --git a/inject/src/main/java/io/micronaut/inject/provider/ProviderTypeInformationProvider.java b/inject/src/main/java/io/micronaut/inject/provider/ProviderTypeInformationProvider.java index f0193554ef0..b922d15ba7e 100644 --- a/inject/src/main/java/io/micronaut/inject/provider/ProviderTypeInformationProvider.java +++ b/inject/src/main/java/io/micronaut/inject/provider/ProviderTypeInformationProvider.java @@ -28,6 +28,7 @@ public final class ProviderTypeInformationProvider implements TypeInformationProvider { @Override + @SuppressWarnings("java:S1872") // required by impl public boolean isWrapperType(Class type) { return BeanProvider.class == type || Provider.class == type || diff --git a/jackson-core/src/main/java/io/micronaut/jackson/core/tree/JsonNodeTraversingParser.java b/jackson-core/src/main/java/io/micronaut/jackson/core/tree/JsonNodeTraversingParser.java index 5d71d9817fb..d4c9edf8b6f 100644 --- a/jackson-core/src/main/java/io/micronaut/jackson/core/tree/JsonNodeTraversingParser.java +++ b/jackson-core/src/main/java/io/micronaut/jackson/core/tree/JsonNodeTraversingParser.java @@ -169,7 +169,12 @@ public String getText() throws IOException { @Override public char[] getTextCharacters() throws IOException { - return getText().toCharArray(); + String text = getText(); + if (text != null) { + return text.toCharArray(); + } else { + return new char[0]; + } } @Override @@ -247,7 +252,8 @@ public BigDecimal getDecimalValue() throws IOException { @Override public int getTextLength() throws IOException { - return getText().length(); + String text = getText(); + return text != null ? text.length() : 0; } @Override diff --git a/retry/src/main/java/io/micronaut/retry/intercept/DefaultRetryInterceptor.java b/retry/src/main/java/io/micronaut/retry/intercept/DefaultRetryInterceptor.java index 46758f4a421..bc6e3beb21c 100644 --- a/retry/src/main/java/io/micronaut/retry/intercept/DefaultRetryInterceptor.java +++ b/retry/src/main/java/io/micronaut/retry/intercept/DefaultRetryInterceptor.java @@ -239,6 +239,7 @@ private Object retrySync(MethodInvocationContext context, Mutabl } Thread.sleep(delayMillis); } catch (InterruptedException e1) { + Thread.currentThread().interrupt(); throw e; } } diff --git a/router/src/main/java/io/micronaut/web/router/version/ConfigurationDefaultVersionProvider.java b/router/src/main/java/io/micronaut/web/router/version/ConfigurationDefaultVersionProvider.java index 6f9344c87fe..830080b3827 100644 --- a/router/src/main/java/io/micronaut/web/router/version/ConfigurationDefaultVersionProvider.java +++ b/router/src/main/java/io/micronaut/web/router/version/ConfigurationDefaultVersionProvider.java @@ -19,6 +19,8 @@ import io.micronaut.context.exceptions.ConfigurationException; import jakarta.inject.Singleton; +import java.util.Optional; + /** * Implementation of {@link DefaultVersionProvider} which uses configuration. * If value micronaut.router.versioning.default-version is present, this bean is loaded and returns that value. @@ -38,10 +40,12 @@ public class ConfigurationDefaultVersionProvider implements DefaultVersionProvid * @param routesVersioningConfiguration Routes Versioning Configuration. */ public ConfigurationDefaultVersionProvider(RoutesVersioningConfiguration routesVersioningConfiguration) { - if (!routesVersioningConfiguration.getDefaultVersion().isPresent()) { + Optional dv = routesVersioningConfiguration.getDefaultVersion(); + if (dv.isPresent()) { + this.defaultVersion = dv.get(); + } else { throw new ConfigurationException("this bean should not be loaded if " + RoutesVersioningConfiguration.PREFIX + ".default-version" + "is null"); } - this.defaultVersion = routesVersioningConfiguration.getDefaultVersion().get(); } @Override diff --git a/websocket/src/main/java/io/micronaut/websocket/WebSocketBroadcaster.java b/websocket/src/main/java/io/micronaut/websocket/WebSocketBroadcaster.java index 9b05ca88f5c..7c99196aca1 100644 --- a/websocket/src/main/java/io/micronaut/websocket/WebSocketBroadcaster.java +++ b/websocket/src/main/java/io/micronaut/websocket/WebSocketBroadcaster.java @@ -157,6 +157,7 @@ default void broadcastSync(T message, MediaType mediaType, Predicate Date: Thu, 3 Nov 2022 09:30:08 +0100 Subject: [PATCH 188/743] Removal of Jakarta JSON Glassfish (#8272) --- bom/build.gradle | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/bom/build.gradle b/bom/build.gradle index 3a6dac0ff0c..72c64a4ab19 100644 --- a/bom/build.gradle +++ b/bom/build.gradle @@ -65,5 +65,10 @@ micronautBom { acceptedLibraryRegressions.add("micronaut-hibernate-gorm") acceptedLibraryRegressions.add("micronaut-graphql-gorm") acceptedLibraryRegressions.add("micronaut-neo4j-gorm") + + // glassfish removed from Micronaut Serialization + acceptedVersionRegressions.add("glassfish-jakarta-json") + acceptedLibraryRegressions.add("glassfish-jakarta-json") + } } From 733b6567028c7b2964a186b0662b4109b8062502 Mon Sep 17 00:00:00 2001 From: Denis Stepanov Date: Thu, 3 Nov 2022 17:49:29 +0700 Subject: [PATCH 189/743] Add byte[] to String converter (#8273) --- .../DefaultMutableConversionService.java | 2 + .../DefaultConversionServiceSpec.groovy | 60 ++++++++++--------- 2 files changed, 33 insertions(+), 29 deletions(-) diff --git a/core/src/main/java/io/micronaut/core/convert/DefaultMutableConversionService.java b/core/src/main/java/io/micronaut/core/convert/DefaultMutableConversionService.java index f9d434c5241..daced05a0b8 100644 --- a/core/src/main/java/io/micronaut/core/convert/DefaultMutableConversionService.java +++ b/core/src/main/java/io/micronaut/core/convert/DefaultMutableConversionService.java @@ -227,6 +227,8 @@ private void registerDefaultConverters() { return Optional.empty(); }); + addConverter(byte[].class, String.class, (bytes, targetType, context) -> Optional.of(new String(bytes, context.getCharset()))); + // String -> Class addConverter(CharSequence.class, Class.class, (object, targetType, context) -> { ClassLoader classLoader = targetType.getClassLoader(); diff --git a/core/src/test/groovy/io/micronaut/core/convert/DefaultConversionServiceSpec.groovy b/core/src/test/groovy/io/micronaut/core/convert/DefaultConversionServiceSpec.groovy index ba36af1a363..c12e2518728 100644 --- a/core/src/test/groovy/io/micronaut/core/convert/DefaultConversionServiceSpec.groovy +++ b/core/src/test/groovy/io/micronaut/core/convert/DefaultConversionServiceSpec.groovy @@ -21,6 +21,7 @@ import spock.lang.Specification import spock.lang.Unroll import java.nio.charset.Charset +import java.nio.charset.StandardCharsets import java.time.DayOfWeek /** @@ -37,35 +38,36 @@ class DefaultConversionServiceSpec extends Specification { conversionService.convert(sourceObject, targetType).get() == result where: - sourceObject | targetType | result - 10 | Long | 10L - 10 | Float | 10.0f - 10 | String | "10" - "1,2" | int[] | [1, 2] as int[] - "str" | char[] | ['s', 't', 'r'] as char[] - "10" | Byte | 10 - "10" | Integer | 10 - "${5 + 5}" | Integer | 10 - "10" | BigInteger | BigInteger.valueOf(10) - "yes" | Boolean | true - "true" | Boolean | true - "Y" | Boolean | true - "yes" | boolean | true - "on" | boolean | true - "off" | boolean | false - "false" | boolean | false - "n" | boolean | false - Boolean.TRUE | boolean | true - "USD" | Currency | Currency.getInstance("USD") - "CET" | TimeZone | TimeZone.getTimeZone("CET") - "http://test.com" | URL | new URL("http://test.com") - "http://test.com" | URI | new URI("http://test.com") - "monday" | DayOfWeek | DayOfWeek.MONDAY - ["monday"] as String[] | DayOfWeek | DayOfWeek.MONDAY - ["monday"] as String[] | DayOfWeek[] | [DayOfWeek.MONDAY] as DayOfWeek[] - "monday,tuesday,monday" | Set | ["monday", "tuesday"] as Set - "N/A" | Status | Status.N_OR_A - ["OK", "N/A"] | Status[] | [Status.OK, Status.N_OR_A] + sourceObject | targetType | result + 10 | Long | 10L + 10 | Float | 10.0f + 10 | String | "10" + "1,2" | int[] | [1, 2] as int[] + "str" | char[] | ['s', 't', 'r'] as char[] + "10" | Byte | 10 + "10" | Integer | 10 + "${5 + 5}" | Integer | 10 + "10" | BigInteger | BigInteger.valueOf(10) + "yes" | Boolean | true + "true" | Boolean | true + "Y" | Boolean | true + "yes" | boolean | true + "on" | boolean | true + "off" | boolean | false + "false" | boolean | false + "n" | boolean | false + Boolean.TRUE | boolean | true + "USD" | Currency | Currency.getInstance("USD") + "CET" | TimeZone | TimeZone.getTimeZone("CET") + "http://test.com" | URL | new URL("http://test.com") + "http://test.com" | URI | new URI("http://test.com") + "monday" | DayOfWeek | DayOfWeek.MONDAY + ["monday"] as String[] | DayOfWeek | DayOfWeek.MONDAY + ["monday"] as String[] | DayOfWeek[] | [DayOfWeek.MONDAY] as DayOfWeek[] + "monday,tuesday,monday" | Set | ["monday", "tuesday"] as Set + "N/A" | Status | Status.N_OR_A + ["OK", "N/A"] | Status[] | [Status.OK, Status.N_OR_A] + "test".getBytes(StandardCharsets.UTF_8) | String | "test" } void "test empty string conversion"() { From 5e781b1e698967be8506a86352a7e0c8ff4805e1 Mon Sep 17 00:00:00 2001 From: Denis Stepanov Date: Thu, 3 Nov 2022 20:36:41 +0700 Subject: [PATCH 190/743] Introduce `@Vetoed` to exclude particular classes, methods and fields from processing (#8274) --- .../AbstractAnnotationMetadataBuilder.java | 58 +++++-------- .../DeclaredBeanElementCreator.java | 5 +- .../io/micronaut/core/annotation/Vetoed.java | 34 ++++++++ .../micronaut/http/server/RouteExecutor.java | 1 - .../BeanDefinitionInjectProcessor.java | 23 +++-- .../micronaut/inject/vetoed/BeanProducer.java | 14 ++++ .../inject/vetoed/ParentOfVetoedBean.java | 13 +++ .../micronaut/inject/vetoed/VetoedBean1.java | 9 ++ .../micronaut/inject/vetoed/VetoedBean2.java | 15 ++++ .../vetoed/VetoedExecutableMethodsBean.java | 26 ++++++ .../vetoed/VetoedMethodsAndFieldsBean.java | 22 +++++ .../micronaut/inject/vetoed/VetoedSpec.groovy | 84 +++++++++++++++++++ .../inject/vetoed/VetoedSuperclassBean.java | 25 ++++++ .../inject/vetoed/pkg/VetoedPackageBean.java | 7 ++ .../inject/vetoed/pkg/package-info.java | 4 + src/main/docs/guide/ioc/beans.adoc | 2 + 16 files changed, 296 insertions(+), 46 deletions(-) create mode 100644 core/src/main/java/io/micronaut/core/annotation/Vetoed.java create mode 100644 inject-java/src/test/groovy/io/micronaut/inject/vetoed/BeanProducer.java create mode 100644 inject-java/src/test/groovy/io/micronaut/inject/vetoed/ParentOfVetoedBean.java create mode 100644 inject-java/src/test/groovy/io/micronaut/inject/vetoed/VetoedBean1.java create mode 100644 inject-java/src/test/groovy/io/micronaut/inject/vetoed/VetoedBean2.java create mode 100644 inject-java/src/test/groovy/io/micronaut/inject/vetoed/VetoedExecutableMethodsBean.java create mode 100644 inject-java/src/test/groovy/io/micronaut/inject/vetoed/VetoedMethodsAndFieldsBean.java create mode 100644 inject-java/src/test/groovy/io/micronaut/inject/vetoed/VetoedSpec.groovy create mode 100644 inject-java/src/test/groovy/io/micronaut/inject/vetoed/VetoedSuperclassBean.java create mode 100644 inject-java/src/test/groovy/io/micronaut/inject/vetoed/pkg/VetoedPackageBean.java create mode 100644 inject-java/src/test/groovy/io/micronaut/inject/vetoed/pkg/package-info.java diff --git a/core-processor/src/main/java/io/micronaut/inject/annotation/AbstractAnnotationMetadataBuilder.java b/core-processor/src/main/java/io/micronaut/inject/annotation/AbstractAnnotationMetadataBuilder.java index bd7d07102c3..27ea4c1aa50 100644 --- a/core-processor/src/main/java/io/micronaut/inject/annotation/AbstractAnnotationMetadataBuilder.java +++ b/core-processor/src/main/java/io/micronaut/inject/annotation/AbstractAnnotationMetadataBuilder.java @@ -2184,15 +2184,11 @@ public AnnotationMetadata annotate( } else { return defaultMetadata; } - } else if (annotationMetadata instanceof AnnotationMetadataHierarchy) { - AnnotationMetadataHierarchy hierarchy = (AnnotationMetadataHierarchy) annotationMetadata; + } else if (annotationMetadata instanceof AnnotationMetadataHierarchy hierarchy) { AnnotationMetadata declaredMetadata = annotate(hierarchy.getDeclaredMetadata(), annotationValue); - return hierarchy.createSibling( - declaredMetadata - ); - } else { - throw new IllegalStateException("Unrecognized annotation metadata: " + annotationMetadata); + return hierarchy.createSibling(declaredMetadata); } + throw new IllegalStateException("Unrecognized annotation metadata: " + annotationMetadata); } /** @@ -2204,6 +2200,9 @@ public AnnotationMetadata annotate( * @since 3.0.0 */ public AnnotationMetadata removeAnnotation(AnnotationMetadata annotationMetadata, String annotationType) { + if (annotationMetadata.isEmpty()) { + return annotationMetadata; + } // we only care if the metadata is an hierarchy or default mutable final boolean isHierarchy = annotationMetadata instanceof AnnotationMetadataHierarchy; AnnotationMetadata declaredMetadata = annotationMetadata; @@ -2212,8 +2211,7 @@ public AnnotationMetadata removeAnnotation(AnnotationMetadata annotationMetadata } // if it is anything else other than DefaultAnnotationMetadata here it is probably empty // in which case nothing needs to be done - if (declaredMetadata instanceof DefaultAnnotationMetadata) { - final DefaultAnnotationMetadata defaultMetadata = (DefaultAnnotationMetadata) declaredMetadata; + if (declaredMetadata instanceof DefaultAnnotationMetadata defaultMetadata) { T annotationMirror = getAnnotationMirror(annotationType).orElse(null); if (annotationMirror != null) { String repeatableName = getRepeatableNameForType(annotationMirror); @@ -2227,12 +2225,9 @@ public AnnotationMetadata removeAnnotation(AnnotationMetadata annotationMetadata } if (isHierarchy) { - return ((AnnotationMetadataHierarchy) annotationMetadata).createSibling( - declaredMetadata - ); - } else { - return declaredMetadata; + return ((AnnotationMetadataHierarchy) annotationMetadata).createSibling(declaredMetadata); } + return declaredMetadata; } else { throw new IllegalStateException("Unrecognized annotation metadata: " + annotationMetadata); } @@ -2247,6 +2242,9 @@ public AnnotationMetadata removeAnnotation(AnnotationMetadata annotationMetadata * @since 3.0.0 */ public AnnotationMetadata removeStereotype(AnnotationMetadata annotationMetadata, String annotationType) { + if (annotationMetadata.isEmpty()) { + return annotationMetadata; + } // we only care if the metadata is an hierarchy or default mutable final boolean isHierarchy = annotationMetadata instanceof AnnotationMetadataHierarchy; AnnotationMetadata declaredMetadata = annotationMetadata; @@ -2255,8 +2253,7 @@ public AnnotationMetadata removeStereotype(AnnotationMetadata annotationMetadata } // if it is anything else other than DefaultAnnotationMetadata here it is probably empty // in which case nothing needs to be done - if (declaredMetadata instanceof DefaultAnnotationMetadata) { - final DefaultAnnotationMetadata defaultMetadata = (DefaultAnnotationMetadata) declaredMetadata; + if (declaredMetadata instanceof DefaultAnnotationMetadata defaultMetadata) { T annotationMirror = getAnnotationMirror(annotationType).orElse(null); if (annotationMirror != null) { String repeatableName = getRepeatableNameForType(annotationMirror); @@ -2268,17 +2265,12 @@ public AnnotationMetadata removeStereotype(AnnotationMetadata annotationMetadata } else { defaultMetadata.removeStereotype(annotationType); } - if (isHierarchy) { - return ((AnnotationMetadataHierarchy) annotationMetadata).createSibling( - declaredMetadata - ); - } else { - return declaredMetadata; + return ((AnnotationMetadataHierarchy) annotationMetadata).createSibling(declaredMetadata); } - } else { - throw new IllegalStateException("Unrecognized annotation metadata: " + annotationMetadata); + return declaredMetadata; } + throw new IllegalStateException("Unrecognized annotation metadata: " + annotationMetadata); } /** @@ -2292,6 +2284,9 @@ public AnnotationMetadata removeStereotype(AnnotationMetadata annotationMetadata public @NonNull AnnotationMetadata removeAnnotationIf( @NonNull AnnotationMetadata annotationMetadata, @NonNull Predicate> predicate) { + if (annotationMetadata.isEmpty()) { + return annotationMetadata; + } // we only care if the metadata is an hierarchy or default mutable final boolean isHierarchy = annotationMetadata instanceof AnnotationMetadataHierarchy; AnnotationMetadata declaredMetadata = annotationMetadata; @@ -2300,21 +2295,14 @@ public AnnotationMetadata removeStereotype(AnnotationMetadata annotationMetadata } // if it is anything else other than DefaultAnnotationMetadata here it is probably empty // in which case nothing needs to be done - if (declaredMetadata instanceof DefaultAnnotationMetadata) { - final DefaultAnnotationMetadata defaultMetadata = (DefaultAnnotationMetadata) declaredMetadata; - + if (declaredMetadata instanceof DefaultAnnotationMetadata defaultMetadata) { defaultMetadata.removeAnnotationIf(predicate); - if (isHierarchy) { - return ((AnnotationMetadataHierarchy) annotationMetadata).createSibling( - declaredMetadata - ); - } else { - return declaredMetadata; + return ((AnnotationMetadataHierarchy) annotationMetadata).createSibling(declaredMetadata); } - } else { - throw new IllegalStateException("Unrecognized annotation metadata: " + annotationMetadata); + return declaredMetadata; } + throw new IllegalStateException("Unrecognized annotation metadata: " + annotationMetadata); } /** diff --git a/core-processor/src/main/java/io/micronaut/inject/processing/DeclaredBeanElementCreator.java b/core-processor/src/main/java/io/micronaut/inject/processing/DeclaredBeanElementCreator.java index a8c526745b8..b961ae580ca 100644 --- a/core-processor/src/main/java/io/micronaut/inject/processing/DeclaredBeanElementCreator.java +++ b/core-processor/src/main/java/io/micronaut/inject/processing/DeclaredBeanElementCreator.java @@ -24,6 +24,7 @@ import io.micronaut.core.annotation.NextMajorVersion; import io.micronaut.core.annotation.NonNull; import io.micronaut.core.annotation.Nullable; +import io.micronaut.core.annotation.Vetoed; import io.micronaut.inject.ast.ClassElement; import io.micronaut.inject.ast.ElementQuery; import io.micronaut.inject.ast.FieldElement; @@ -150,8 +151,10 @@ private void build(BeanDefinitionVisitor visitor) { } List memberElements = new ArrayList<>(classElement.getEnclosedElements(memberQuery)); memberElements.removeIf(processedFields::contains); - // Process subtype fields first for (MemberElement memberElement : memberElements) { + if (memberElement.hasAnnotation(Vetoed.class)) { + continue; + } if (memberElement instanceof FieldElement fieldElement) { visitFieldInternal(visitor, fieldElement); } else if (memberElement instanceof MethodElement methodElement) { diff --git a/core/src/main/java/io/micronaut/core/annotation/Vetoed.java b/core/src/main/java/io/micronaut/core/annotation/Vetoed.java new file mode 100644 index 00000000000..303b5efeef7 --- /dev/null +++ b/core/src/main/java/io/micronaut/core/annotation/Vetoed.java @@ -0,0 +1,34 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.core.annotation; + +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; + +/** + * Veto the processing of the element. + * + * @author Denis Stepanov + * @since 4.0.0 + */ +@Target({ElementType.TYPE, ElementType.METHOD, ElementType.CONSTRUCTOR, ElementType.FIELD, ElementType.PACKAGE}) +@Retention(RetentionPolicy.CLASS) +@Documented +public @interface Vetoed { +} diff --git a/http-server/src/main/java/io/micronaut/http/server/RouteExecutor.java b/http-server/src/main/java/io/micronaut/http/server/RouteExecutor.java index 12703168faa..b380c633b54 100644 --- a/http-server/src/main/java/io/micronaut/http/server/RouteExecutor.java +++ b/http-server/src/main/java/io/micronaut/http/server/RouteExecutor.java @@ -87,7 +87,6 @@ import java.util.concurrent.atomic.AtomicReference; import java.util.function.Supplier; import java.util.regex.Pattern; -import java.util.stream.Collectors; import static io.micronaut.core.util.KotlinUtils.isKotlinCoroutineSuspended; import static io.micronaut.http.HttpAttributes.AVAILABLE_HTTP_METHODS; diff --git a/inject-java/src/main/java/io/micronaut/annotation/processing/BeanDefinitionInjectProcessor.java b/inject-java/src/main/java/io/micronaut/annotation/processing/BeanDefinitionInjectProcessor.java index e5f2f004c1a..5abed35eefa 100644 --- a/inject-java/src/main/java/io/micronaut/annotation/processing/BeanDefinitionInjectProcessor.java +++ b/inject-java/src/main/java/io/micronaut/annotation/processing/BeanDefinitionInjectProcessor.java @@ -23,14 +23,15 @@ import io.micronaut.core.annotation.AnnotationUtil; import io.micronaut.core.annotation.Internal; import io.micronaut.core.annotation.NonNull; +import io.micronaut.core.annotation.Vetoed; import io.micronaut.core.naming.NameUtils; import io.micronaut.core.util.CollectionUtils; -import io.micronaut.inject.processing.ProcessingException; import io.micronaut.inject.annotation.AbstractAnnotationMetadataBuilder; import io.micronaut.inject.ast.annotation.ElementAnnotationMetadataFactory; import io.micronaut.inject.processing.BeanDefinitionCreator; import io.micronaut.inject.processing.BeanDefinitionCreatorFactory; import io.micronaut.inject.processing.JavaModelUtils; +import io.micronaut.inject.processing.ProcessingException; import io.micronaut.inject.visitor.BeanElementVisitor; import io.micronaut.inject.visitor.VisitorConfiguration; import io.micronaut.inject.writer.AbstractBeanDefinitionBuilder; @@ -185,15 +186,16 @@ public final boolean process(Set annotations, RoundEnviro } String name = typeElement.getQualifiedName().toString(); - if (!beanDefinitions.contains(name) && !processed.contains(name) && !name.endsWith(BeanDefinitionVisitor.PROXY_SUFFIX)) { - boolean isInterface = JavaModelUtils.resolveKind(typeElement, ElementKind.INTERFACE).isPresent(); - if (!isInterface) { + if (beanDefinitions.contains(name) || processed.contains(name) || name.endsWith(BeanDefinitionVisitor.PROXY_SUFFIX)) { + return; + } + boolean isInterface = JavaModelUtils.resolveKind(typeElement, ElementKind.INTERFACE).isPresent(); + if (!isInterface) { + beanDefinitions.add(name); + } else { + AnnotationMetadata annotationMetadata = annotationMetadataBuilder.lookupOrBuildForType(typeElement); + if (annotationMetadata.hasStereotype(INTRODUCTION_TYPE) || annotationMetadata.hasStereotype(ConfigurationReader.class)) { beanDefinitions.add(name); - } else { - AnnotationMetadata annotationMetadata = annotationMetadataBuilder.lookupOrBuildForType(typeElement); - if (annotationMetadata.hasStereotype(INTRODUCTION_TYPE) || annotationMetadata.hasStereotype(ConfigurationReader.class)) { - beanDefinitions.add(name); - } } } })); @@ -219,6 +221,9 @@ public final boolean process(Set annotations, RoundEnviro } JavaClassElement classElement = javaVisitorContext.getElementFactory() .newClassElement(typeElement, annotationMetadataFactory); + if (classElement.hasAnnotation(Vetoed.class) || classElement.getPackage().hasAnnotation(Vetoed.class)) { + continue; + } BeanDefinitionCreator beanDefinitionCreator = BeanDefinitionCreatorFactory.produce(classElement, javaVisitorContext); for (BeanDefinitionVisitor writer : beanDefinitionCreator.build()) { if (processed.add(writer.getBeanDefinitionName())) { diff --git a/inject-java/src/test/groovy/io/micronaut/inject/vetoed/BeanProducer.java b/inject-java/src/test/groovy/io/micronaut/inject/vetoed/BeanProducer.java new file mode 100644 index 00000000000..7b42bfd0afb --- /dev/null +++ b/inject-java/src/test/groovy/io/micronaut/inject/vetoed/BeanProducer.java @@ -0,0 +1,14 @@ +package io.micronaut.inject.vetoed; + +import io.micronaut.context.annotation.Bean; +import io.micronaut.context.annotation.Factory; + +@Factory +public class BeanProducer { + + @Bean + VetoedBean2 produce() { + return new VetoedBean2(); + } + +} diff --git a/inject-java/src/test/groovy/io/micronaut/inject/vetoed/ParentOfVetoedBean.java b/inject-java/src/test/groovy/io/micronaut/inject/vetoed/ParentOfVetoedBean.java new file mode 100644 index 00000000000..fb3e4331c03 --- /dev/null +++ b/inject-java/src/test/groovy/io/micronaut/inject/vetoed/ParentOfVetoedBean.java @@ -0,0 +1,13 @@ +package io.micronaut.inject.vetoed; + +import io.micronaut.context.BeanContext; +import jakarta.inject.Singleton; + +@Singleton +public class ParentOfVetoedBean extends VetoedSuperclassBean { + + public ParentOfVetoedBean(BeanContext beanContext) { + super(beanContext); + } + +} diff --git a/inject-java/src/test/groovy/io/micronaut/inject/vetoed/VetoedBean1.java b/inject-java/src/test/groovy/io/micronaut/inject/vetoed/VetoedBean1.java new file mode 100644 index 00000000000..d21f13623c8 --- /dev/null +++ b/inject-java/src/test/groovy/io/micronaut/inject/vetoed/VetoedBean1.java @@ -0,0 +1,9 @@ +package io.micronaut.inject.vetoed; + +import io.micronaut.core.annotation.Vetoed; +import jakarta.inject.Singleton; + +@Vetoed +@Singleton +public class VetoedBean1 { +} diff --git a/inject-java/src/test/groovy/io/micronaut/inject/vetoed/VetoedBean2.java b/inject-java/src/test/groovy/io/micronaut/inject/vetoed/VetoedBean2.java new file mode 100644 index 00000000000..cfc5a0dd350 --- /dev/null +++ b/inject-java/src/test/groovy/io/micronaut/inject/vetoed/VetoedBean2.java @@ -0,0 +1,15 @@ +package io.micronaut.inject.vetoed; + +import io.micronaut.context.BeanContext; +import io.micronaut.core.annotation.Vetoed; +import jakarta.inject.Inject; +import jakarta.inject.Singleton; + +@Vetoed +@Singleton +public class VetoedBean2 { + + @Inject + public BeanContext beanContext; + +} diff --git a/inject-java/src/test/groovy/io/micronaut/inject/vetoed/VetoedExecutableMethodsBean.java b/inject-java/src/test/groovy/io/micronaut/inject/vetoed/VetoedExecutableMethodsBean.java new file mode 100644 index 00000000000..eb2be024be3 --- /dev/null +++ b/inject-java/src/test/groovy/io/micronaut/inject/vetoed/VetoedExecutableMethodsBean.java @@ -0,0 +1,26 @@ +package io.micronaut.inject.vetoed; + +import io.micronaut.context.annotation.Executable; +import io.micronaut.core.annotation.Vetoed; +import jakarta.inject.Singleton; + +@Singleton +@Executable +public class VetoedExecutableMethodsBean { + + void foo() { + } + + @Vetoed + void bar() { + } + + void abc() { + } + + @Executable + @Vetoed + void xyz() { + } + +} diff --git a/inject-java/src/test/groovy/io/micronaut/inject/vetoed/VetoedMethodsAndFieldsBean.java b/inject-java/src/test/groovy/io/micronaut/inject/vetoed/VetoedMethodsAndFieldsBean.java new file mode 100644 index 00000000000..90f55e98a4c --- /dev/null +++ b/inject-java/src/test/groovy/io/micronaut/inject/vetoed/VetoedMethodsAndFieldsBean.java @@ -0,0 +1,22 @@ +package io.micronaut.inject.vetoed; + +import io.micronaut.context.BeanContext; +import io.micronaut.core.annotation.Vetoed; +import jakarta.inject.Inject; +import jakarta.inject.Singleton; + +@Singleton +public class VetoedMethodsAndFieldsBean { + + @Inject + @Vetoed + public BeanContext fieldInjection; + public BeanContext methodInjection; + + @Vetoed + @Inject + public void setMethodInjection(BeanContext methodInjection) { + this.methodInjection = methodInjection; + } + +} diff --git a/inject-java/src/test/groovy/io/micronaut/inject/vetoed/VetoedSpec.groovy b/inject-java/src/test/groovy/io/micronaut/inject/vetoed/VetoedSpec.groovy new file mode 100644 index 00000000000..e931e30dfad --- /dev/null +++ b/inject-java/src/test/groovy/io/micronaut/inject/vetoed/VetoedSpec.groovy @@ -0,0 +1,84 @@ +/* + * Copyright 2017-2019 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.inject.vetoed + +import io.micronaut.context.ApplicationContext +import io.micronaut.inject.vetoed.pkg.VetoedPackageBean +import spock.lang.Specification + +class VetoedSpec extends Specification { + + void "test vetoed package"() { + when: + ApplicationContext context = ApplicationContext.run() + then: + context.findBean(VetoedPackageBean).isEmpty() + cleanup: + context.stop() + } + + void "test vetoed bean"() { + when: + ApplicationContext context = ApplicationContext.run() + then: + context.findBean(VetoedBean1).isEmpty() + cleanup: + context.stop() + } + + void "test produced vetoed bean"() { + when: + ApplicationContext context = ApplicationContext.run() + then: + context.getBeanDefinitions(VetoedBean2).size() == 1 + cleanup: + context.stop() + } + + void "test vetoed bean parent"() { + when: + ApplicationContext context = ApplicationContext.run() + def bean = context.getBean(ParentOfVetoedBean) + then: "injection in the vetoed superclass is working" + bean.fieldInjection + bean.methodInjection + bean.constructorInjection + cleanup: + context.stop() + } + + void "test vetoed methods and fields bean"() { + when: + ApplicationContext context = ApplicationContext.run() + def bean = context.getBean(VetoedMethodsAndFieldsBean) + then: + bean.fieldInjection == null + bean.methodInjection == null + cleanup: + context.stop() + } + + void "test vetoed executable methods bean"() { + when: + ApplicationContext context = ApplicationContext.run() + def bean = context.getBeanDefinition(VetoedExecutableMethodsBean) + then: + bean.executableMethods.collect {it.methodName } == ["foo", "abc"] + cleanup: + context.stop() + } + +} diff --git a/inject-java/src/test/groovy/io/micronaut/inject/vetoed/VetoedSuperclassBean.java b/inject-java/src/test/groovy/io/micronaut/inject/vetoed/VetoedSuperclassBean.java new file mode 100644 index 00000000000..e5d53a9e531 --- /dev/null +++ b/inject-java/src/test/groovy/io/micronaut/inject/vetoed/VetoedSuperclassBean.java @@ -0,0 +1,25 @@ +package io.micronaut.inject.vetoed; + +import io.micronaut.context.BeanContext; +import io.micronaut.core.annotation.Vetoed; +import jakarta.inject.Inject; +import jakarta.inject.Singleton; + +@Vetoed +@Singleton +public class VetoedSuperclassBean { + + public final BeanContext constructorInjection; + @Inject + public BeanContext fieldInjection; + public BeanContext methodInjection; + + public VetoedSuperclassBean(BeanContext constructorInjection) { + this.constructorInjection = constructorInjection; + } + + @Inject + public void setMethodInjection(BeanContext methodInjection) { + this.methodInjection = methodInjection; + } +} diff --git a/inject-java/src/test/groovy/io/micronaut/inject/vetoed/pkg/VetoedPackageBean.java b/inject-java/src/test/groovy/io/micronaut/inject/vetoed/pkg/VetoedPackageBean.java new file mode 100644 index 00000000000..ad6fa490778 --- /dev/null +++ b/inject-java/src/test/groovy/io/micronaut/inject/vetoed/pkg/VetoedPackageBean.java @@ -0,0 +1,7 @@ +package io.micronaut.inject.vetoed.pkg; + +import jakarta.inject.Singleton; + +@Singleton +public class VetoedPackageBean { +} diff --git a/inject-java/src/test/groovy/io/micronaut/inject/vetoed/pkg/package-info.java b/inject-java/src/test/groovy/io/micronaut/inject/vetoed/pkg/package-info.java new file mode 100644 index 00000000000..9a5017e63db --- /dev/null +++ b/inject-java/src/test/groovy/io/micronaut/inject/vetoed/pkg/package-info.java @@ -0,0 +1,4 @@ +@Vetoed +package io.micronaut.inject.vetoed.pkg; + +import io.micronaut.core.annotation.Vetoed; diff --git a/src/main/docs/guide/ioc/beans.adoc b/src/main/docs/guide/ioc/beans.adoc index 93d99ead970..2d097940a0d 100644 --- a/src/main/docs/guide/ioc/beans.adoc +++ b/src/main/docs/guide/ioc/beans.adoc @@ -20,3 +20,5 @@ Micronaut supports the following types of dependency injection: * Field injection * JavaBean property injection * Method parameter injection + +NOTE: Classes or particular fields, methods can be excluded by adding an annotation ann:core.annotation.Vetoed[] From fc3b1bd2b1fecebf63ad33d3c30fb5069e6ad4e3 Mon Sep 17 00:00:00 2001 From: micronaut-build <65172877+micronaut-build@users.noreply.github.com> Date: Fri, 4 Nov 2022 02:14:43 -0400 Subject: [PATCH 191/743] Bump micronaut-aws to 3.9.3 (#8277) --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 434106fb264..893b021ad64 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -67,7 +67,7 @@ managed-methvin-directory-watcher = "0.16.1" managed-micrometer = "1.9.4" managed-micronaut-acme = "3.2.0" managed-micronaut-aot = "1.1.1" -managed-micronaut-aws = "3.9.2" +managed-micronaut-aws = "3.9.3" managed-micronaut-azure = "3.5.0" managed-micronaut-cache = "3.5.0" managed-micronaut-cassandra = "5.1.1" From 36dc3730e555c72f0625f8ae98d6b7df486f893c Mon Sep 17 00:00:00 2001 From: micronaut-build <65172877+micronaut-build@users.noreply.github.com> Date: Fri, 4 Nov 2022 02:13:59 -0400 Subject: [PATCH 192/743] Bump micronaut-serialization to 1.3.3 (#8279) --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 893b021ad64..0f85ffc5f7e 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -113,7 +113,7 @@ managed-micronaut-rxjava1 = "1.0.0" managed-micronaut-rxjava2 = "1.3.0" managed-micronaut-rxjava3 = "2.3.0" managed-micronaut-security = "3.8.1" -managed-micronaut-serialization = "1.3.2" +managed-micronaut-serialization = "1.3.3" managed-micronaut-servlet = "3.3.2" managed-micronaut-spring = "4.3.1" managed-micronaut-sql = "4.7.2" From 677aef67ad523f7e78758ba39198897169a9562e Mon Sep 17 00:00:00 2001 From: Denis Stepanov Date: Fri, 4 Nov 2022 16:07:36 +0700 Subject: [PATCH 193/743] Correct how function method is recognized (#8283) --- .../web/AnnotatedFunctionRouteBuilder.java | 90 +++++++++---------- .../function/web/WebFunctionSpec.groovy | 64 ++++++++++++- .../function/web/TestFunctionFactory.java | 2 +- 3 files changed, 108 insertions(+), 48 deletions(-) diff --git a/function-web/src/main/java/io/micronaut/function/web/AnnotatedFunctionRouteBuilder.java b/function-web/src/main/java/io/micronaut/function/web/AnnotatedFunctionRouteBuilder.java index 12d01d8413a..4e094b14241 100644 --- a/function-web/src/main/java/io/micronaut/function/web/AnnotatedFunctionRouteBuilder.java +++ b/function-web/src/main/java/io/micronaut/function/web/AnnotatedFunctionRouteBuilder.java @@ -92,12 +92,17 @@ public void process(BeanDefinition beanDefinition, ExecutableMethod met String functionName = beanDefinition.stringValue(FunctionBean.class).orElse(methodName); String functionMethod = beanDefinition.stringValue(FunctionBean.class, "method").orElse(null); + if (StringUtils.isNotEmpty(functionMethod) && !functionMethod.equals(methodName)) { + return; + } + List routes = new ArrayList<>(2); MediaType[] consumes = Arrays.stream(method.stringValues(Consumes.class)).map(MediaType::of).toArray(MediaType[]::new); MediaType[] produces = Arrays.stream(method.stringValues(Produces.class)).map(MediaType::of).toArray(MediaType[]::new); - + boolean implementsFnInterface = false; if (Stream.of(java.util.function.Function.class, Consumer.class, BiFunction.class, BiConsumer.class).anyMatch(type -> type.isAssignableFrom(declaringType))) { - if (methodName.equals("accept") || methodName.equals("apply") || methodName.equals(functionMethod)) { + implementsFnInterface = true; + if (methodName.equals("accept") || methodName.equals("apply")) { String functionPath = resolveFunctionPath(methodName, declaringType, functionName); String[] argumentNames = method.getArgumentNames(); String argumentName = argumentNames[0]; @@ -114,7 +119,7 @@ public void process(BeanDefinition beanDefinition, ExecutableMethod met int size = typeArguments.size(); Argument firstArgument = typeArguments.get(0); - if (size < 3 && ClassUtils.isJavaLangType(firstArgument.getType()) && consumes == null) { + if (size < 3 && ClassUtils.isJavaLangType(firstArgument.getType()) && consumes.length == 0) { consumes = new MediaType[] {MediaType.TEXT_PLAIN_TYPE, MediaType.APPLICATION_JSON_TYPE}; } @@ -124,40 +129,40 @@ public void process(BeanDefinition beanDefinition, ExecutableMethod met if (size > 1) { Argument argument = typeArguments.get(size == 3 ? 2 : 1); - if (ClassUtils.isJavaLangType(argument.getType()) && produces == null) { + if (ClassUtils.isJavaLangType(argument.getType()) && produces.length == 0) { produces = new MediaType[] {MediaType.TEXT_PLAIN_TYPE, MediaType.APPLICATION_JSON_TYPE}; } } - } else { - if (argCount == 1 && ClassUtils.isJavaLangType(method.getArgumentTypes()[0]) && consumes == null) { - consumes = new MediaType[] {MediaType.TEXT_PLAIN_TYPE, MediaType.APPLICATION_JSON_TYPE}; - } + } else if (argCount == 1 && ClassUtils.isJavaLangType(method.getArgumentTypes()[0]) && consumes.length == 0) { + consumes = new MediaType[]{MediaType.TEXT_PLAIN_TYPE, MediaType.APPLICATION_JSON_TYPE}; } } - } else if (Supplier.class.isAssignableFrom(declaringType) && methodName.equals("get")) { - String functionPath = resolveFunctionPath(methodName, declaringType, functionName); - routes.add(GET(functionPath, beanDefinition, method)); - routes.add(HEAD(functionPath, beanDefinition, method)); - } else { - if (StringUtils.isNotEmpty(functionMethod) && functionMethod.equals(methodName)) { - Argument[] argumentTypes = method.getArguments(); - int argCount = argumentTypes.length; - if (argCount < 3) { - String functionPath = resolveFunctionPath(methodName, declaringType, functionName); - if (argCount == 0) { - routes.add(GET(functionPath, beanDefinition, method)); - routes.add(HEAD(functionPath, beanDefinition, method)); - } else { - UriRoute route = POST(functionPath, beanDefinition, method); - routes.add(route); - if (argCount == 2 || !ClassUtils.isJavaLangType(argumentTypes[0].getType())) { - if (consumes == null) { - consumes = new MediaType[] {MediaType.APPLICATION_JSON_TYPE}; - } - } else { - route.body(method.getArgumentNames()[0]) - .consumesAll(); - } + } + + if (routes.isEmpty() && Supplier.class.isAssignableFrom(declaringType)) { + implementsFnInterface = true; + if (methodName.equals("get")) { + String functionPath = resolveFunctionPath(methodName, declaringType, functionName); + routes.add(GET(functionPath, beanDefinition, method)); + routes.add(HEAD(functionPath, beanDefinition, method)); + } + } + + if (routes.isEmpty() && (!implementsFnInterface || StringUtils.isNotEmpty(functionMethod))) { + // Only add a custom method when it's explicitly defined or if the bean doesn't implement known functional interface + Argument[] argumentTypes = method.getArguments(); + int argCount = argumentTypes.length; + if (argCount < 3) { + String functionPath = resolveFunctionPath(methodName, declaringType, functionName); + if (argCount == 0) { + routes.add(GET(functionPath, beanDefinition, method)); + routes.add(HEAD(functionPath, beanDefinition, method)); + } else { + UriRoute route = POST(functionPath, beanDefinition, method); + routes.add(route); + if (argCount != 2 && ClassUtils.isJavaLangType(argumentTypes[0].getType())) { + route.body(method.getArgumentNames()[0]) + .consumesAll(); } } } @@ -169,13 +174,8 @@ public void process(BeanDefinition beanDefinition, ExecutableMethod met LOG.debug("Created Route to Function: {}", route); } - if (consumes != null) { - route.consumes(consumes); - } - - if (produces != null) { - route.produces(produces); - } + route.consumes(consumes); + route.produces(produces); } String functionPath = resolveFunctionPath(methodName, declaringType, functionName); @@ -244,24 +244,24 @@ public Optional, R>> findBiFuncti @Override public Optional findCodec(@Nullable MediaType mediaType) { - if (localFunctionRegistry instanceof MediaTypeCodecRegistry) { - return ((MediaTypeCodecRegistry) localFunctionRegistry).findCodec(mediaType); + if (localFunctionRegistry instanceof MediaTypeCodecRegistry mediaTypeCodecRegistry) { + return mediaTypeCodecRegistry.findCodec(mediaType); } return Optional.empty(); } @Override public Optional findCodec(@Nullable MediaType mediaType, Class type) { - if (localFunctionRegistry instanceof MediaTypeCodecRegistry) { - return ((MediaTypeCodecRegistry) localFunctionRegistry).findCodec(mediaType, type); + if (localFunctionRegistry instanceof MediaTypeCodecRegistry mediaTypeCodecRegistry) { + return mediaTypeCodecRegistry.findCodec(mediaType, type); } return Optional.empty(); } @Override public Collection getCodecs() { - if (localFunctionRegistry instanceof MediaTypeCodecRegistry) { - return ((MediaTypeCodecRegistry) localFunctionRegistry).getCodecs(); + if (localFunctionRegistry instanceof MediaTypeCodecRegistry mediaTypeCodecRegistry) { + return mediaTypeCodecRegistry.getCodecs(); } return Collections.emptyList(); } diff --git a/function-web/src/test/groovy/io/micronaut/function/web/WebFunctionSpec.groovy b/function-web/src/test/groovy/io/micronaut/function/web/WebFunctionSpec.groovy index 3207aff096d..30fe91bafbc 100644 --- a/function-web/src/test/groovy/io/micronaut/function/web/WebFunctionSpec.groovy +++ b/function-web/src/test/groovy/io/micronaut/function/web/WebFunctionSpec.groovy @@ -27,6 +27,7 @@ import io.micronaut.http.MediaType import io.micronaut.http.client.HttpClient import io.micronaut.runtime.server.EmbeddedServer import spock.lang.Specification +import spock.lang.Unroll import java.util.function.Consumer import java.util.function.Supplier @@ -43,20 +44,25 @@ class WebFunctionSpec extends Specification { expect: registry.findConsumer("consumer/string").isPresent() + registry.findConsumer("consumer/string2").isPresent() registry.findSupplier("supplier/string").isPresent() + registry.findSupplier("supplier/string2").isPresent() registry.findConsumer("consumer/pojo").isPresent() registry.findSupplier("supplier/pojo").isPresent() !registry.findConsumer("consumer/junk").isPresent() !registry.findSupplier("supplier/junk").isPresent() + registry.findSupplier("supplier/custom-method").isEmpty() // FunctionBean registered with non supplier method + registry.findSupplier("consumer/custom-method").isEmpty() // FunctionBean registered with non consumer method } + @Unroll void "test string supplier"() { given: EmbeddedServer embeddedServer = ApplicationContext.run(EmbeddedServer) HttpClient client = embeddedServer.applicationContext.createBean(HttpClient, embeddedServer.getURL()) when: - HttpResponse response = client.toBlocking().exchange('/supplier/string', String) + HttpResponse response = client.toBlocking().exchange(uri, String) then: response.code() == HttpStatus.OK.code @@ -64,6 +70,9 @@ class WebFunctionSpec extends Specification { cleanup: embeddedServer.stop() + + where: + uri << ['/supplier/string', '/supplier/string2', '/supplier/custom-method'] } void "test string supplier with HEAD"() { @@ -100,6 +109,7 @@ class WebFunctionSpec extends Specification { } + @Unroll void "test string consumer with JSON"() { given: EmbeddedServer embeddedServer = ApplicationContext.run(EmbeddedServer) @@ -107,7 +117,7 @@ class WebFunctionSpec extends Specification { def data = '{"title":"The Stand"}' when: - HttpResponse response = client.toBlocking().exchange(HttpRequest.POST('/consumer/string', data)) + HttpResponse response = client.toBlocking().exchange(HttpRequest.POST(uri, data)) then: response.code() == HttpStatus.OK.code @@ -115,6 +125,9 @@ class WebFunctionSpec extends Specification { cleanup: embeddedServer.stop() + + where: + uri << ['/consumer/string', '/consumer/string2', '/consumer/custom-method'] } void 'test camel cased function bean'() { @@ -181,6 +194,29 @@ class WebFunctionSpec extends Specification { } } + @FunctionBean(name = "supplier/string2", method = "get") + static class StringSupplier2 implements Supplier { + String getValue() { + return "value" + } + @Override + String get() { + return getValue() + } + } + + @FunctionBean(name = "supplier/custom-method", method = "getValue") + static class NotStringSupplier implements Supplier { + String getValue() { + return "value" + } + @Override + String get() { + return getValue() + } + } + + @FunctionBean("helloWorld") static class CamelCaseSupplier implements Supplier { String getValue() { @@ -212,6 +248,30 @@ class WebFunctionSpec extends Specification { } } + @FunctionBean(name = "consumer/string2", method = "accept") + static class StringConsumer2 implements Consumer { + + static String LAST_VALUE + @Override + void accept(String title) { + LAST_VALUE = title + } + } + + @FunctionBean(name = "consumer/custom-method", method = "myAccept") + static class NonStringConsumer implements Consumer { + + static String LAST_VALUE + @Override + void accept(String title) { + myAccept(title) + } + + void myAccept(String title) { + LAST_VALUE = title + } + } + @FunctionBean("consumer/pojo") static class PojoConsumer implements Consumer { diff --git a/function-web/src/test/java/io/micronaut/function/web/TestFunctionFactory.java b/function-web/src/test/java/io/micronaut/function/web/TestFunctionFactory.java index cc374f806e2..9dceb4809cd 100644 --- a/function-web/src/test/java/io/micronaut/function/web/TestFunctionFactory.java +++ b/function-web/src/test/java/io/micronaut/function/web/TestFunctionFactory.java @@ -49,7 +49,7 @@ Supplier getXml() { // it is an AOP proxy @FunctionBean("java/function/round") Function round() { - return (doub) -> Math.round(doub.doubleValue()); + return Math::round; } @FunctionBean("java/function/upper") From dff1c056f148dede4851c435ac0bf2c9e925267a Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Fri, 4 Nov 2022 11:02:58 +0100 Subject: [PATCH 194/743] Fix: Req attrs has routeMatch for WebSocketServer (#8285) Co-authored-by: Denis Stepanov --- .../NettyServerWebSocketUpgradeHandler.java | 14 +- .../websocket/WebsocketRouteMatchSpec.groovy | 141 ++++++++++++++++++ 2 files changed, 150 insertions(+), 5 deletions(-) create mode 100644 http-server-netty/src/test/groovy/io/micronaut/websocket/WebsocketRouteMatchSpec.groovy diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/websocket/NettyServerWebSocketUpgradeHandler.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/websocket/NettyServerWebSocketUpgradeHandler.java index 80a5d277ace..7bbba55c29e 100644 --- a/http-server-netty/src/main/java/io/micronaut/http/server/netty/websocket/NettyServerWebSocketUpgradeHandler.java +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/websocket/NettyServerWebSocketUpgradeHandler.java @@ -128,16 +128,20 @@ protected final void channelRead0(ChannelHandlerContext ctx, NettyHttpRequest .findFirst(); MutableHttpResponse proceed = HttpResponse.ok(); + + if (optionalRoute.isPresent()) { + UriRouteMatch rm = optionalRoute.get(); + msg.setAttribute(HttpAttributes.ROUTE_MATCH, rm); + msg.setAttribute(HttpAttributes.ROUTE_INFO, rm); + proceed.setAttribute(HttpAttributes.ROUTE_MATCH, rm); + proceed.setAttribute(HttpAttributes.ROUTE_INFO, rm); + } + AtomicReference> requestReference = new AtomicReference<>(msg); ExecutionFlow> responseFlow = ExecutionFlow.async(ctx.channel().eventLoop(), () -> routeExecutor.filterPublisher(requestReference, () -> { ExecutionFlow> response; if (optionalRoute.isPresent()) { - UriRouteMatch rm = optionalRoute.get(); - msg.setAttribute(HttpAttributes.ROUTE_MATCH, rm); - msg.setAttribute(HttpAttributes.ROUTE_INFO, rm); - proceed.setAttribute(HttpAttributes.ROUTE_MATCH, rm); - proceed.setAttribute(HttpAttributes.ROUTE_INFO, rm); response = ExecutionFlow.just(proceed); } else { response = routeExecutor.onError(new HttpStatusException(HttpStatus.NOT_FOUND, "WebSocket Not Found"), msg); diff --git a/http-server-netty/src/test/groovy/io/micronaut/websocket/WebsocketRouteMatchSpec.groovy b/http-server-netty/src/test/groovy/io/micronaut/websocket/WebsocketRouteMatchSpec.groovy new file mode 100644 index 00000000000..0e40fb3c66e --- /dev/null +++ b/http-server-netty/src/test/groovy/io/micronaut/websocket/WebsocketRouteMatchSpec.groovy @@ -0,0 +1,141 @@ +package io.micronaut.websocket + +import io.micronaut.context.annotation.Property +import io.micronaut.context.annotation.Requires +import io.micronaut.http.HttpAttributes +import io.micronaut.http.HttpRequest +import io.micronaut.http.HttpResponse +import io.micronaut.http.MutableHttpRequest +import io.micronaut.http.MutableHttpResponse +import io.micronaut.http.annotation.Filter +import io.micronaut.http.filter.HttpServerFilter +import io.micronaut.http.filter.ServerFilterChain +import io.micronaut.runtime.server.EmbeddedServer +import io.micronaut.test.extensions.spock.annotation.MicronautTest +import io.micronaut.web.router.RouteMatch +import io.micronaut.websocket.annotation.ClientWebSocket +import io.micronaut.websocket.annotation.OnClose +import io.micronaut.websocket.annotation.OnMessage +import io.micronaut.websocket.annotation.OnOpen +import io.micronaut.websocket.annotation.ServerWebSocket +import jakarta.inject.Inject +import org.reactivestreams.Publisher +import reactor.core.publisher.Flux +import reactor.core.publisher.Mono +import spock.lang.Specification +import spock.util.concurrent.PollingConditions + +import java.util.function.Predicate +import java.util.stream.Collectors + +@Property(name = "spec.name", value = "WebsocketRouteMatchSpec") +@MicronautTest +class WebsocketRouteMatchSpec extends Specification { + + @Inject + EmbeddedServer embeddedServer + + void "request attributes contains a route match for WebSocketServer"() { + given: + WebSocketClient wsClient = embeddedServer.applicationContext.createBean(WebSocketClient.class, embeddedServer.getURL()) + + expect: + wsClient + + when: + MutableHttpRequest request = HttpRequest.GET("/echo") + EchoClientWebSocket echoClientWebSocket = Flux.from(wsClient.connect(EchoClientWebSocket, request)).blockFirst() + + then: + noExceptionThrown() + new PollingConditions().eventually { + echoClientWebSocket.receivedMessages() == ['joined!'] + } + + when: + echoClientWebSocket.send('Hello') + + then: + new PollingConditions().eventually { + echoClientWebSocket.receivedMessages() == ['joined!', 'Hello'] + } + + cleanup: + echoClientWebSocket.close() + } + + @Requires(property = "spec.name", value = "WebsocketRouteMatchSpec") + @ServerWebSocket("/echo") + static class EchoServerWebSocket { + public static final String JOINED = "joined!" + public static final String DISCONNECTED = "Disconnected!" + + @Inject + WebSocketBroadcaster broadcaster + + @OnOpen + void onOpen(WebSocketSession session) { + broadcaster.broadcastSync(JOINED, isValid(session)) + } + + @OnMessage + void onMessage(String message, WebSocketSession session) { + broadcaster.broadcastSync(message, isValid(session)) + } + + @OnClose + void onClose(WebSocketSession session) { + broadcaster.broadcastSync(DISCONNECTED, isValid(session)) + } + + private static Predicate isValid(WebSocketSession session) { + return { s -> s == session } + } + } + + @Requires(property = "spec.name", value = "WebsocketRouteMatchSpec") + @Filter(Filter.MATCH_ALL_PATTERN) + static class SecurityFilter implements HttpServerFilter { + @Override + Publisher> doFilter(HttpRequest request, ServerFilterChain chain) { + RouteMatch routeMatch = request.getAttribute(HttpAttributes.ROUTE_MATCH, RouteMatch.class).orElse(null) + routeMatch != null ? chain.proceed(request) : Mono.just(HttpResponse.serverError()) + } + } + + @Requires(property = "spec.name", value = "WebsocketRouteMatchSpec") + @ClientWebSocket("/echo") + static abstract class EchoClientWebSocket implements AutoCloseable { + + static final String RECEIVED = "RECEIVED:" + + private WebSocketSession session + private List replies = new ArrayList<>() + + @OnOpen + void onOpen(WebSocketSession session) { + this.session = session + } + List getReplies() { + return replies + } + + @OnMessage + void onMessage(String message) { + replies.add(RECEIVED + message) + } + + abstract void send(String message) + + List receivedMessages() { + return filterMessagesByType(RECEIVED) + } + + List filterMessagesByType(String type) { + replies.stream() + .filter(str -> str.contains(type)) + .map(str -> str.replaceAll(type, "")) + .collect(Collectors.toList()) + } + } +} From 4ec18d886a0c705b5f2ea91516dfaee95644cbcc Mon Sep 17 00:00:00 2001 From: altro3 Date: Fri, 4 Nov 2022 17:58:47 +0700 Subject: [PATCH 195/743] Add generics to Class -> Class (#8278) --- aop/build.gradle | 4 +- .../java/io/micronaut/aop/Introduction.java | 2 +- .../aop/chain/AdapterIntroduction.java | 4 +- .../micronaut/aop/chain/InterceptorChain.java | 2 +- .../aop/chain/MethodInterceptorChain.java | 2 +- .../java/io/micronaut/runtime/Micronaut.java | 6 +- .../AbstractAnnotationMetadataBuilder.java | 4 +- .../annotation/AnnotationMetadataWriter.java | 5 +- .../visitor/BeanIntrospectionWriter.java | 2 +- .../writer/AbstractClassFileWriter.java | 20 ++-- .../inject/writer/BeanDefinitionWriter.java | 4 +- .../core/annotation/AnnotationValue.java | 11 +- .../annotation/AnnotationValueBuilder.java | 2 +- .../annotation/EmptyAnnotationMetadata.java | 8 +- .../micronaut/core/annotation/TypeHint.java | 2 +- .../DefaultMutableConversionService.java | 24 ++-- .../convert/value/ConvertibleMultiValues.java | 4 +- .../core/convert/value/ConvertibleValues.java | 4 +- .../io/micronaut/core/reflect/ClassUtils.java | 30 ++--- .../core/reflect/GenericTypeUtils.java | 35 +++--- .../core/reflect/ReflectionUtils.java | 52 ++++----- .../core/value/PropertyNotFoundException.java | 2 +- .../core/bind/ExecutableBinderSpec.groovy | 6 +- .../executor/StreamFunctionExecutor.java | 2 +- .../netty/DefaultNettyHttpClientRegistry.java | 2 +- .../netty/AbstractCompositeCustomizer.java | 5 + .../DefaultChannelOptionFactory.java | 4 +- .../AbstractNettyWebSocketHandler.java | 2 +- .../DefaultHttpContentProcessorResolver.java | 2 +- .../server/netty/RoutingInBoundHandler.java | 2 +- .../http/server/netty/StreamTypeHandler.java | 2 +- .../netty/jackson/JsonContentProcessor.java | 2 +- .../netty/types/files/FileTypeHandler.java | 4 +- .../version/VersionControllerSpec.groovy | 4 +- .../micronaut/http/server/RouteExecutor.java | 2 +- .../websocket/ServerWebSocketProcessor.java | 2 +- .../http/uri/UriTypeMatchTemplate.java | 18 +-- .../GroovyAnnotationMetadataBuilder.java | 2 +- .../ast/groovy/utils/AstGenericUtils.groovy | 2 +- .../InMemoryByteCodeGroovyClassLoader.java | 4 +- .../InheritedAnnotationMetadataSpec.groovy | 8 +- .../ast/groovy/annotation/Trace.groovy | 7 +- .../ConfigurationPropertiesBuilderSpec.groovy | 108 +++++++++--------- .../GroovyConfigBuilderSpec.groovy | 20 ++-- .../lifecyle/BeanWithPreDestroySpec.groovy | 14 +-- .../beans/BeanIntrospectionSpec.groovy | 6 +- .../JavaAnnotationMetadataBuilder.java | 2 +- .../processing/visitor/LoadedVisitor.java | 6 +- .../InheritedAnnotationMetadataSpec.groovy | 8 +- .../io/micronaut/inject/annotation/Trace.java | 4 +- .../inject/close/BeanCloseOrderSpec.groovy | 2 +- .../ConfigurationBuilderSpec.groovy | 54 ++++----- .../FieldInheritanceInjectionSpec.groovy | 2 +- inject-kotlin-test/build.gradle | 2 +- .../context/AbstractBeanDefinition.java | 28 ++--- .../micronaut/context/AbstractExecutable.java | 4 +- .../context/AbstractExecutableMethod.java | 4 +- .../AbstractExecutableMethodsDefinition.java | 4 +- .../AbstractInitializableBeanDefinition.java | 4 +- .../micronaut/context/ApplicationContext.java | 2 +- .../micronaut/context/DefaultBeanContext.java | 40 +++---- .../DefaultConstructorInjectionPoint.java | 2 +- .../context/DefaultMethodInjectionPoint.java | 2 +- .../context/ExecutionHandleLocator.java | 26 ++--- .../context/MissingMethodInjectionPoint.java | 4 +- .../context/NoInjectionBeanDefinition.java | 4 +- .../micronaut/context/RequiresCondition.java | 4 +- .../annotation/DefaultImplementation.java | 2 +- .../context/annotation/EachBean.java | 2 +- .../context/annotation/Replaces.java | 6 +- .../context/annotation/Requires.java | 8 +- .../converters/ContextConverterRegistrar.java | 6 +- .../io/micronaut/inject/BeanDefinition.java | 2 +- .../inject/DelegatingExecutableMethod.java | 2 +- .../io/micronaut/inject/ExecutionHandle.java | 6 +- .../io/micronaut/inject/MethodReference.java | 2 +- .../annotation/DefaultAnnotationMetadata.java | 7 +- .../ClosestTypeArgumentQualifier.java | 14 +-- .../inject/qualifiers/Qualifiers.java | 6 +- .../qualifiers/TypeAnnotationQualifier.java | 2 +- .../qualifiers/TypeArgumentQualifier.java | 20 ++-- .../qualifiers/ClosestTypeArgumentSpec.groovy | 2 +- .../jackson/ObjectMapperFactory.java | 16 +-- .../convert/JacksonConverterRegistrar.java | 2 +- .../modules/BeanIntrospectionModule.java | 4 +- .../BeanIntrospectionModuleRecordSpec.groovy | 2 +- .../json/bind/JsonBeanPropertyBinder.java | 2 +- .../json/convert/JsonConverterRegistrar.java | 2 +- .../AbstractEndpointRouteBuilder.java | 2 +- .../retry/intercept/MyCustomException.java | 0 .../web/router/AbstractRouteMatch.java | 6 +- .../router/AnnotatedMethodRouteBuilder.java | 8 +- .../web/router/DefaultRouteBuilder.java | 80 ++++++------- .../micronaut/web/router/DefaultRouter.java | 12 +- .../io/micronaut/web/router/ErrorRoute.java | 2 +- .../io/micronaut/web/router/RouteBuilder.java | 89 +++++++-------- .../java/io/micronaut/web/router/Router.java | 4 +- .../io/micronaut/web/router/StatusRoute.java | 2 +- .../web/router/filter/FilteredRouter.java | 4 +- .../DefaultAnnotatedElementValidator.java | 4 +- .../validator/DefaultValidator.java | 20 ++-- .../DefaultConstraintValidators.java | 6 +- .../extractors/DefaultValueExtractors.java | 10 +- .../bind/WebSocketStateBinderRegistry.java | 2 +- .../context/DefaultWebSocketBeanRegistry.java | 2 +- 105 files changed, 518 insertions(+), 517 deletions(-) rename retry/src/{main => test}/java/io/micronaut/retry/intercept/MyCustomException.java (100%) diff --git a/aop/build.gradle b/aop/build.gradle index f954e4f9cd4..900459d6e42 100644 --- a/aop/build.gradle +++ b/aop/build.gradle @@ -17,6 +17,6 @@ dependencies { } tasks.named("compileKotlin") { - kotlinOptions.jvmTarget = "1.8" - kotlinOptions.languageVersion = "1.6" + kotlinOptions.jvmTarget = "17" + kotlinOptions.languageVersion = "1.7" } diff --git a/aop/src/main/java/io/micronaut/aop/Introduction.java b/aop/src/main/java/io/micronaut/aop/Introduction.java index b3594d141c6..310578f0c06 100644 --- a/aop/src/main/java/io/micronaut/aop/Introduction.java +++ b/aop/src/main/java/io/micronaut/aop/Introduction.java @@ -56,5 +56,5 @@ * * @return The additional interfaces to implement */ - Class[] interfaces() default {}; + Class[] interfaces() default {}; } diff --git a/aop/src/main/java/io/micronaut/aop/chain/AdapterIntroduction.java b/aop/src/main/java/io/micronaut/aop/chain/AdapterIntroduction.java index 68ac556720a..1fd9ec60153 100644 --- a/aop/src/main/java/io/micronaut/aop/chain/AdapterIntroduction.java +++ b/aop/src/main/java/io/micronaut/aop/chain/AdapterIntroduction.java @@ -58,8 +58,8 @@ final class AdapterIntroduction implements MethodInterceptor { } String beanQualifier = method.stringValue(Adapter.class, ADAPTED_QUALIFIER).orElse(null); - Class[] argumentTypes = method.classValues(Adapter.class, ADAPTED_ARGUMENT_TYPES); - Class[] methodArgumentTypes = method.getArgumentTypes(); + Class[] argumentTypes = method.classValues(Adapter.class, ADAPTED_ARGUMENT_TYPES); + Class[] methodArgumentTypes = method.getArgumentTypes(); if (StringUtils.isNotEmpty(beanQualifier)) { this.executionHandle = beanContext.findExecutionHandle( beanType, diff --git a/aop/src/main/java/io/micronaut/aop/chain/InterceptorChain.java b/aop/src/main/java/io/micronaut/aop/chain/InterceptorChain.java index 49ac3c71d02..9baddba5bee 100644 --- a/aop/src/main/java/io/micronaut/aop/chain/InterceptorChain.java +++ b/aop/src/main/java/io/micronaut/aop/chain/InterceptorChain.java @@ -223,7 +223,7 @@ private static void instrumentAnnotationMetadata(BeanContext beanContext, Execut private static Interceptor[] resolveInterceptorsInternal(ExecutableMethod method, Class annotationType, Interceptor[] interceptors, @NonNull ClassLoader classLoader) { List> annotations = method.getAnnotationTypesByStereotype(annotationType, classLoader); - Set applicableClasses = new HashSet<>(); + Set> applicableClasses = new HashSet<>(); for (Class aClass: annotations) { if (annotationType == Around.class && aClass.getAnnotation(Around.class) == null && aClass.getAnnotation(Introduction.class) != null) { diff --git a/aop/src/main/java/io/micronaut/aop/chain/MethodInterceptorChain.java b/aop/src/main/java/io/micronaut/aop/chain/MethodInterceptorChain.java index 083654b73c4..cd792b0a919 100644 --- a/aop/src/main/java/io/micronaut/aop/chain/MethodInterceptorChain.java +++ b/aop/src/main/java/io/micronaut/aop/chain/MethodInterceptorChain.java @@ -147,7 +147,7 @@ public String getMethodName() { } @Override - public Class[] getArgumentTypes() { + public Class[] getArgumentTypes() { return executionHandle.getArgumentTypes(); } diff --git a/context/src/main/java/io/micronaut/runtime/Micronaut.java b/context/src/main/java/io/micronaut/runtime/Micronaut.java index 3b1910399c7..ea92ad1b7d7 100644 --- a/context/src/main/java/io/micronaut/runtime/Micronaut.java +++ b/context/src/main/java/io/micronaut/runtime/Micronaut.java @@ -296,7 +296,7 @@ public static Micronaut build(String... args) { * @return The {@link ApplicationContext} */ public static ApplicationContext run(String... args) { - return run(new Class[0], args); + return run(new Class[0], args); } /** @@ -307,7 +307,7 @@ public static ApplicationContext run(String... args) { * @return The {@link ApplicationContext} */ public static ApplicationContext run(Class cls, String... args) { - return run(new Class[]{cls}, args); + return run(new Class[]{cls}, args); } /** @@ -336,7 +336,7 @@ protected void handleStartupException(Environment environment, Throwable excepti Integer code = exitCodeMapper.apply(exception); if (code > 0 && !environment.getActiveNames().contains(Environment.TEST)) { if (LOG.isErrorEnabled()) { - LOG.error("Error starting Micronaut server: " + exception.getMessage(), exception); + LOG.error("Error starting Micronaut server: {}", exception.getMessage(), exception); } System.exit(code); } diff --git a/core-processor/src/main/java/io/micronaut/inject/annotation/AbstractAnnotationMetadataBuilder.java b/core-processor/src/main/java/io/micronaut/inject/annotation/AbstractAnnotationMetadataBuilder.java index 27ea4c1aa50..ea376425a7d 100644 --- a/core-processor/src/main/java/io/micronaut/inject/annotation/AbstractAnnotationMetadataBuilder.java +++ b/core-processor/src/main/java/io/micronaut/inject/annotation/AbstractAnnotationMetadataBuilder.java @@ -67,6 +67,8 @@ */ public abstract class AbstractAnnotationMetadataBuilder { + protected static final List EXCLUDES = Arrays.asList(AnnotationUtil.KOTLIN_METADATA, "jdk.internal.ValueBased"); + /** * Names of annotations that should produce deprecation warnings. * The key in the map is the deprecated annotation the value the replacement. @@ -81,8 +83,6 @@ public abstract class AbstractAnnotationMetadataBuilder { Experimental.class.getName(), "jdk.internal.ValueBased"); private static final Map> ANNOTATION_DEFAULTS = new HashMap<>(20); - protected static final List EXCLUDES = Arrays.asList(AnnotationUtil.KOTLIN_METADATA, "jdk.internal.ValueBased"); - static { for (AnnotationMapper mapper : SoftServiceLoader.load(AnnotationMapper.class, AbstractAnnotationMetadataBuilder.class.getClassLoader()) .disableFork().collectAll()) { diff --git a/core-processor/src/main/java/io/micronaut/inject/annotation/AnnotationMetadataWriter.java b/core-processor/src/main/java/io/micronaut/inject/annotation/AnnotationMetadataWriter.java index 3fba901bc0b..a4628a9a0aa 100644 --- a/core-processor/src/main/java/io/micronaut/inject/annotation/AnnotationMetadataWriter.java +++ b/core-processor/src/main/java/io/micronaut/inject/annotation/AnnotationMetadataWriter.java @@ -585,9 +585,8 @@ private static void pushValue(Type declaringType, ClassVisitor declaringClassWri } else { invokeLoadClassValueMethod(declaringType, declaringClassWriter, methodVisitor, loadTypeMethods, acv); } - } else if (value instanceof Enum) { - Enum enumObject = (Enum) value; - Class declaringClass = enumObject.getDeclaringClass(); + } else if (value instanceof Enum enumObject) { + Class declaringClass = enumObject.getDeclaringClass(); Type t = Type.getType(declaringClass); methodVisitor.getStatic(t, enumObject.name(), t); } else if (value.getClass().isArray()) { diff --git a/core-processor/src/main/java/io/micronaut/inject/beans/visitor/BeanIntrospectionWriter.java b/core-processor/src/main/java/io/micronaut/inject/beans/visitor/BeanIntrospectionWriter.java index e1d2ec28628..fb54f592969 100644 --- a/core-processor/src/main/java/io/micronaut/inject/beans/visitor/BeanIntrospectionWriter.java +++ b/core-processor/src/main/java/io/micronaut/inject/beans/visitor/BeanIntrospectionWriter.java @@ -794,7 +794,7 @@ private int getPropertyIndex(String propertyName) { throw new IllegalStateException("Property not found: " + propertyName + " " + classElement.getName()); } - private void writeInstantiateMethod(ClassWriter classWriter, MethodElement constructor, String methodName, Class... args) { + private void writeInstantiateMethod(ClassWriter classWriter, MethodElement constructor, String methodName, Class... args) { final String desc = getMethodDescriptor(Object.class, Arrays.asList(args)); final GeneratorAdapter instantiateInternal = new GeneratorAdapter(classWriter.visitMethod( ACC_PUBLIC, diff --git a/core-processor/src/main/java/io/micronaut/inject/writer/AbstractClassFileWriter.java b/core-processor/src/main/java/io/micronaut/inject/writer/AbstractClassFileWriter.java index c6eeac5db89..6f36d03ecb7 100644 --- a/core-processor/src/main/java/io/micronaut/inject/writer/AbstractClassFileWriter.java +++ b/core-processor/src/main/java/io/micronaut/inject/writer/AbstractClassFileWriter.java @@ -791,7 +791,7 @@ protected static Type getTypeReference(TypedElement type) { * @param injectMethodVisitor The {@link MethodVisitor} */ protected static void pushBoxPrimitiveIfNecessary(Type fieldType, MethodVisitor injectMethodVisitor) { - final Optional pt = ClassUtils.getPrimitiveType(fieldType.getClassName()); + final Optional> pt = ClassUtils.getPrimitiveType(fieldType.getClassName()); Class wrapperType = pt.map(ReflectionUtils::getWrapperType).orElse(null); if (wrapperType != null && wrapperType != Void.class) { Type wrapper = Type.getType(wrapperType); @@ -825,7 +825,7 @@ protected static void pushBoxPrimitiveIfNecessary(TypedElement fieldType, Method ClassElement type = fieldType.getType(); if (type.isPrimitive() && !type.isArray()) { String primitiveName = type.getName(); - final Optional pt = ClassUtils.getPrimitiveType(primitiveName); + final Optional> pt = ClassUtils.getPrimitiveType(primitiveName); Class wrapperType = pt.map(ReflectionUtils::getWrapperType).orElse(null); if (wrapperType != null && wrapperType != Void.class) { Type wrapper = Type.getType(wrapperType); @@ -844,7 +844,7 @@ protected static void pushCastToType(MethodVisitor methodVisitor, Type type) { String internalName = getInternalNameForCast(type); methodVisitor.visitTypeInsn(CHECKCAST, internalName); Type primitiveType = null; - final Optional pt = ClassUtils.getPrimitiveType(type.getClassName()); + final Optional> pt = ClassUtils.getPrimitiveType(type.getClassName()); if (pt.isPresent()) { primitiveType = Type.getType(pt.get()); } @@ -899,7 +899,7 @@ protected static void pushCastToType(MethodVisitor methodVisitor, TypedElement t methodVisitor.visitTypeInsn(CHECKCAST, internalName); Type primitiveType = null; if (type.isPrimitive() && !type.isArray()) { - final Optional pt = ClassUtils.getPrimitiveType(type.getType().getName()); + final Optional> pt = ClassUtils.getPrimitiveType(type.getType().getName()); if (pt.isPresent()) { primitiveType = Type.getType(pt.get()); } @@ -1452,7 +1452,7 @@ protected void startClass(ClassWriter classWriter, String className, Type superT * @param superClass The super class * @param argumentTypes The argument types */ - protected void invokeConstructor(MethodVisitor cv, Class superClass, Class... argumentTypes) { + protected void invokeConstructor(MethodVisitor cv, Class superClass, Class... argumentTypes) { try { Type superType = Type.getType(superClass); Type superConstructor = Type.getType(superClass.getDeclaredConstructor(argumentTypes)); @@ -1471,7 +1471,7 @@ protected void invokeConstructor(MethodVisitor cv, Class superClass, Class... ar * @param targetType The target type * @param method The method */ - protected static void invokeInterfaceStaticMethod(MethodVisitor visitor, Class targetType, Method method) { + protected static void invokeInterfaceStaticMethod(MethodVisitor visitor, Class targetType, Method method) { Type type = Type.getType(targetType); String owner = type.getSort() == Type.ARRAY ? type.getDescriptor() : type.getInternalName(); @@ -1485,7 +1485,7 @@ protected static void invokeInterfaceStaticMethod(MethodVisitor visitor, Class t * @param methodName The method name * @return TheThe {@link GeneratorAdapter} for the method */ - protected GeneratorAdapter startPublicMethodZeroArgs(ClassWriter classWriter, Class returnType, String methodName) { + protected GeneratorAdapter startPublicMethodZeroArgs(ClassWriter classWriter, Class returnType, String methodName) { Type methodType = Type.getMethodType(Type.getType(returnType)); return new GeneratorAdapter(classWriter.visitMethod(ACC_PUBLIC, methodName, methodType.getDescriptor(), null, null), ACC_PUBLIC, methodName, methodType.getDescriptor()); @@ -1497,7 +1497,7 @@ protected GeneratorAdapter startPublicMethodZeroArgs(ClassWriter classWriter, Cl * @param methodName The method name * @return TheThe {@link GeneratorAdapter} for the method */ - protected GeneratorAdapter startPublicFinalMethodZeroArgs(ClassWriter classWriter, Class returnType, String methodName) { + protected GeneratorAdapter startPublicFinalMethodZeroArgs(ClassWriter classWriter, Class returnType, String methodName) { Type methodType = Type.getMethodType(Type.getType(returnType)); return new GeneratorAdapter( @@ -1531,7 +1531,7 @@ protected static String getInternalNameForCast(TypedElement type) { ClassElement ce = type.getType(); if (ce.isPrimitive() && !ce.isArray()) { - final Optional pt = ClassUtils.getPrimitiveType(ce.getName()); + final Optional> pt = ClassUtils.getPrimitiveType(ce.getName()); if (pt.isPresent()) { return Type.getInternalName(ReflectionUtils.getWrapperType(pt.get())); } else { @@ -1558,7 +1558,7 @@ protected static String getInternalNameForCast(Class typeClass) { * @return the internal name for cast */ protected static String getInternalNameForCast(Type type) { - final Optional pt = ClassUtils.getPrimitiveType(type.getClassName()); + final Optional> pt = ClassUtils.getPrimitiveType(type.getClassName()); if (pt.isPresent()) { return Type.getInternalName(ReflectionUtils.getWrapperType(pt.get())); } else { diff --git a/core-processor/src/main/java/io/micronaut/inject/writer/BeanDefinitionWriter.java b/core-processor/src/main/java/io/micronaut/inject/writer/BeanDefinitionWriter.java index 106dfaf8c1e..e894bcac156 100644 --- a/core-processor/src/main/java/io/micronaut/inject/writer/BeanDefinitionWriter.java +++ b/core-processor/src/main/java/io/micronaut/inject/writer/BeanDefinitionWriter.java @@ -489,7 +489,7 @@ public class BeanDefinitionWriter extends AbstractClassFileWriter implements Bea private final String beanDefinitionName; private final String beanDefinitionInternalName; private final Type beanType; - private final Set interfaceTypes; + private final Set> interfaceTypes; private final Map defaultsStorage = new HashMap<>(); private final Map loadTypeMethods = new LinkedHashMap<>(); private final Map innerClasses = new LinkedHashMap<>(2); @@ -970,7 +970,7 @@ public void visitBeanDefinitionEnd() { } String[] interfaceInternalNames = new String[interfaceTypes.size()]; - Iterator j = interfaceTypes.iterator(); + Iterator> j = interfaceTypes.iterator(); for (int i = 0; i < interfaceInternalNames.length; i++) { interfaceInternalNames[i] = Type.getInternalName(j.next()); } diff --git a/core/src/main/java/io/micronaut/core/annotation/AnnotationValue.java b/core/src/main/java/io/micronaut/core/annotation/AnnotationValue.java index 9fa33f94a7a..a3dedb5710e 100644 --- a/core/src/main/java/io/micronaut/core/annotation/AnnotationValue.java +++ b/core/src/main/java/io/micronaut/core/annotation/AnnotationValue.java @@ -344,14 +344,13 @@ public Optional> classValue(@NonNull String member, @NonN return Optional.of((Class) t); } return Optional.empty(); - } else if (o instanceof Class) { - Class t = (Class) o; + } else if (o instanceof Class t) { if (requiredType.isAssignableFrom(t)) { return Optional.of((Class) t); } return Optional.empty(); } else if (o != null) { - Class t = ClassUtils.forName(o.toString(), getClass().getClassLoader()).orElse(null); + Class t = ClassUtils.forName(o.toString(), getClass().getClassLoader()).orElse(null); if (t != null && requiredType.isAssignableFrom(t)) { return Optional.of((Class) t); } @@ -1432,7 +1431,7 @@ Class[] resolveClassValues(@Nullable Object value) { } else if (value instanceof AnnotationClassValue) { Class type = ((AnnotationClassValue) value).getType().orElse(null); if (type != null) { - return new Class[]{type}; + return new Class[]{type}; } } else if (value instanceof AnnotationValue[]) { AnnotationValue[] array = (AnnotationValue[]) value; @@ -1449,7 +1448,7 @@ Class[] resolveClassValues(@Nullable Object value) { return ((AnnotationValue) value).classValues(); } else if (value instanceof Object[]) { Object[] values = (Object[]) value; - if (values instanceof Class[]) { + if (values instanceof Class[]) { return (Class[]) values; } else { return Arrays.stream(values).flatMap(o -> { @@ -1463,7 +1462,7 @@ Class[] resolveClassValues(@Nullable Object value) { }).toArray(Class[]::new); } } else if (value instanceof Class) { - return new Class[]{(Class) value}; + return new Class[]{(Class) value}; } return null; } diff --git a/core/src/main/java/io/micronaut/core/annotation/AnnotationValueBuilder.java b/core/src/main/java/io/micronaut/core/annotation/AnnotationValueBuilder.java index 9ea5119c032..9be79029c99 100644 --- a/core/src/main/java/io/micronaut/core/annotation/AnnotationValueBuilder.java +++ b/core/src/main/java/io/micronaut/core/annotation/AnnotationValueBuilder.java @@ -690,7 +690,7 @@ public AnnotationValueBuilder members(@Nullable Map mem for (Map.Entry entry: members.entrySet()) { Object value = entry.getValue(); if (value != null) { - Class clazz = value.getClass(); + Class clazz = value.getClass(); boolean isArray = clazz.isArray(); if (isArray) { clazz = clazz.getComponentType(); diff --git a/core/src/main/java/io/micronaut/core/annotation/EmptyAnnotationMetadata.java b/core/src/main/java/io/micronaut/core/annotation/EmptyAnnotationMetadata.java index 72ac519c520..14dadf99f33 100644 --- a/core/src/main/java/io/micronaut/core/annotation/EmptyAnnotationMetadata.java +++ b/core/src/main/java/io/micronaut/core/annotation/EmptyAnnotationMetadata.java @@ -299,25 +299,25 @@ public > Optional enumValue(@NonNull Class Class[] classValues(@NonNull String annotation) { - return ReflectionUtils.EMPTY_CLASS_ARRAY; + return (Class[]) ReflectionUtils.EMPTY_CLASS_ARRAY; } @NonNull @Override public Class[] classValues(@NonNull String annotation, @NonNull String member) { - return ReflectionUtils.EMPTY_CLASS_ARRAY; + return (Class[]) ReflectionUtils.EMPTY_CLASS_ARRAY; } @NonNull @Override public Class[] classValues(@NonNull Class annotation) { - return ReflectionUtils.EMPTY_CLASS_ARRAY; + return (Class[]) ReflectionUtils.EMPTY_CLASS_ARRAY; } @NonNull @Override public Class[] classValues(@NonNull Class annotation, @NonNull String member) { - return ReflectionUtils.EMPTY_CLASS_ARRAY; + return (Class[]) ReflectionUtils.EMPTY_CLASS_ARRAY; } @Override diff --git a/core/src/main/java/io/micronaut/core/annotation/TypeHint.java b/core/src/main/java/io/micronaut/core/annotation/TypeHint.java index 21b4d821ba4..c6128bd1287 100644 --- a/core/src/main/java/io/micronaut/core/annotation/TypeHint.java +++ b/core/src/main/java/io/micronaut/core/annotation/TypeHint.java @@ -34,7 +34,7 @@ /** * @return The types to provide a hint */ - Class[] value() default {}; + Class[] value() default {}; /** * Describes the access. diff --git a/core/src/main/java/io/micronaut/core/convert/DefaultMutableConversionService.java b/core/src/main/java/io/micronaut/core/convert/DefaultMutableConversionService.java index daced05a0b8..e701cb36182 100644 --- a/core/src/main/java/io/micronaut/core/convert/DefaultMutableConversionService.java +++ b/core/src/main/java/io/micronaut/core/convert/DefaultMutableConversionService.java @@ -111,7 +111,7 @@ public Optional convert(Object object, Class targetType, ConversionCon if (targetType == Object.class) { return Optional.of((T) object); } - targetType = targetType.isPrimitive() ? ReflectionUtils.getWrapperType(targetType) : targetType; + targetType = targetType.isPrimitive() ? (Class) ReflectionUtils.getWrapperType(targetType) : targetType; if (targetType.isInstance(object) && !(object instanceof Iterable) && !(object instanceof Map)) { return Optional.of((T) object); @@ -266,7 +266,7 @@ private void registerDefaultConverters() { return Optional.empty(); }); addConverter(AnnotationClassValue[].class, Class[].class, (object, targetType, context) -> { - List classes = new ArrayList<>(object.length); + List> classes = new ArrayList<>(object.length); for (AnnotationClassValue annotationClassValue : object) { if (annotationClassValue != null) { final Optional> type = annotationClassValue.getType(); @@ -275,7 +275,7 @@ private void registerDefaultConverters() { } } } - return Optional.of(classes.toArray(new Class[0])); + return Optional.of(classes.toArray(new Class[0])); }); // URI -> URL @@ -706,7 +706,7 @@ private void registerDefaultConverters() { // Number -> Number addConverter(Number.class, Number.class, (Number object, Class targetType, ConversionContext context) -> { - Class targetNumberType = ReflectionUtils.getWrapperType(targetType); + Class targetNumberType = ReflectionUtils.getWrapperType(targetType); if (targetNumberType.isInstance(object)) { return Optional.of(object); } @@ -888,8 +888,8 @@ private void registerDefaultConverters() { } return Argument.of(Object.class, "V"); }); - Class keyType = keyArgument.getType(); - Class valueType = valArgument.getType(); + Class keyType = keyArgument.getType(); + Class valueType = valArgument.getType(); ConversionContext keyContext = context.with(keyArgument); ConversionContext valContext = context.with(valArgument); @@ -964,10 +964,10 @@ private void registerDefaultConverters() { */ protected TypeConverter findTypeConverter(Class sourceType, Class targetType, String formattingAnnotation) { TypeConverter typeConverter = UNCONVERTIBLE; - List sourceHierarchy = ClassUtils.resolveHierarchy(sourceType); - List targetHierarchy = ClassUtils.resolveHierarchy(targetType); - for (Class sourceSuperType : sourceHierarchy) { - for (Class targetSuperType : targetHierarchy) { + List> sourceHierarchy = ClassUtils.resolveHierarchy(sourceType); + List> targetHierarchy = ClassUtils.resolveHierarchy(targetType); + for (Class sourceSuperType : sourceHierarchy) { + for (Class targetSuperType : targetHierarchy) { ConvertiblePair pair = new ConvertiblePair(sourceSuperType, targetSuperType, formattingAnnotation); typeConverter = typeConverters.get(pair); if (typeConverter != null) { @@ -978,8 +978,8 @@ protected TypeConverter findTypeConverter(Class sourceType, Cl } boolean hasFormatting = formattingAnnotation != null; if (hasFormatting) { - for (Class sourceSuperType : sourceHierarchy) { - for (Class targetSuperType : targetHierarchy) { + for (Class sourceSuperType : sourceHierarchy) { + for (Class targetSuperType : targetHierarchy) { ConvertiblePair pair = new ConvertiblePair(sourceSuperType, targetSuperType); typeConverter = typeConverters.get(pair); if (typeConverter != null) { diff --git a/core/src/main/java/io/micronaut/core/convert/value/ConvertibleMultiValues.java b/core/src/main/java/io/micronaut/core/convert/value/ConvertibleMultiValues.java index 4715edc0f75..8bd3729e200 100644 --- a/core/src/main/java/io/micronaut/core/convert/value/ConvertibleMultiValues.java +++ b/core/src/main/java/io/micronaut/core/convert/value/ConvertibleMultiValues.java @@ -139,8 +139,8 @@ public List setValue(List value) { * @return The first value or null if it is present */ default Optional getFirst(CharSequence name) { - Optional type = GenericTypeUtils.resolveInterfaceTypeArgument(getClass(), ConvertibleMultiValues.class); - return getFirst(name, type.orElse(Object.class)); + Optional> type = GenericTypeUtils.resolveInterfaceTypeArgument(getClass(), ConvertibleMultiValues.class); + return (Optional) getFirst(name, type.orElse(Object.class)); } /** diff --git a/core/src/main/java/io/micronaut/core/convert/value/ConvertibleValues.java b/core/src/main/java/io/micronaut/core/convert/value/ConvertibleValues.java index 66f40fcf3db..6748736c8a2 100644 --- a/core/src/main/java/io/micronaut/core/convert/value/ConvertibleValues.java +++ b/core/src/main/java/io/micronaut/core/convert/value/ConvertibleValues.java @@ -70,8 +70,8 @@ default boolean isEmpty() { */ @SuppressWarnings("unchecked") default Class getValueType() { - Optional type = GenericTypeUtils.resolveInterfaceTypeArgument(getClass(), ConvertibleValues.class); - return type.orElse(Object.class); + Optional> type = GenericTypeUtils.resolveInterfaceTypeArgument(getClass(), ConvertibleValues.class); + return (Class) type.orElse(Object.class); } /** diff --git a/core/src/main/java/io/micronaut/core/reflect/ClassUtils.java b/core/src/main/java/io/micronaut/core/reflect/ClassUtils.java index f00537c0199..ea18d4e7c3d 100644 --- a/core/src/main/java/io/micronaut/core/reflect/ClassUtils.java +++ b/core/src/main/java/io/micronaut/core/reflect/ClassUtils.java @@ -58,12 +58,12 @@ public class ClassUtils { /** * System property to indicate whether classloader logging should be activated. This is required - * because this class is used both at compilation time and runtime and we don't want logging at compilation time. + * because this class is used both at compilation time and runtime, and we don't want logging at compilation time. */ public static final String PROPERTY_MICRONAUT_CLASSLOADER_LOGGING = "micronaut.classloader.logging"; public static final int EMPTY_OBJECT_ARRAY_HASH_CODE = Arrays.hashCode(ArrayUtils.EMPTY_OBJECT_ARRAY); - public static final Map COMMON_CLASS_MAP = new HashMap<>(34); - public static final Map BASIC_TYPE_MAP = new HashMap<>(18); + public static final Map> COMMON_CLASS_MAP = new HashMap<>(34); + public static final Map> BASIC_TYPE_MAP = new HashMap<>(18); /** * Default extension for class files. @@ -85,7 +85,7 @@ public class ClassUtils { } @SuppressWarnings("unchecked") - private static final Map PRIMITIVE_TYPE_MAP = CollectionUtils.mapOf( + private static final Map> PRIMITIVE_TYPE_MAP = CollectionUtils.mapOf( "int", Integer.TYPE, "boolean", Boolean.TYPE, "long", Long.TYPE, @@ -98,7 +98,7 @@ public class ClassUtils { ); @SuppressWarnings("unchecked") - private static final Map PRIMITIVE_ARRAY_MAP = CollectionUtils.mapOf( + private static final Map> PRIMITIVE_ARRAY_MAP = CollectionUtils.mapOf( "int", int[].class, "boolean", boolean[].class, "long", long[].class, @@ -160,7 +160,7 @@ public class ClassUtils { * @param type The type * @return The logger */ - public static @NonNull Logger getLogger(@NonNull Class type) { + public static @NonNull Logger getLogger(@NonNull Class type) { if (ENABLE_CLASS_LOADER_LOGGING) { return LoggerFactory.getLogger(type); } else { @@ -173,7 +173,7 @@ public class ClassUtils { * @param primitiveType The primitive type name * @return The array type */ - public static @NonNull Optional arrayTypeForPrimitive(String primitiveType) { + public static @NonNull Optional> arrayTypeForPrimitive(String primitiveType) { if (primitiveType != null) { return Optional.ofNullable(PRIMITIVE_ARRAY_MAP.get(primitiveType)); } @@ -200,7 +200,7 @@ public static String pathToClassName(String path) { * Check whether the given class is present in the given classloader. * * @param name The name of the class - * @param classLoader The classloader. If null will fallback to attempt the thread context loader, otherwise the system loader + * @param classLoader The classloader. If null will fall back to attempt the thread context loader, otherwise the system loader * @return True if it is */ public static boolean isPresent(String name, @Nullable ClassLoader classLoader) { @@ -213,7 +213,7 @@ public static boolean isPresent(String name, @Nullable ClassLoader classLoader) * @param type The type * @return True if it is */ - public static boolean isJavaLangType(Class type) { + public static boolean isJavaLangType(Class type) { String typeName = type.getName(); return isJavaLangType(typeName); } @@ -261,7 +261,7 @@ public static boolean isJavaBasicType(@Nullable String name) { * @param primitiveType The type name * @return An optional type */ - public static Optional getPrimitiveType(String primitiveType) { + public static Optional> getPrimitiveType(String primitiveType) { return Optional.ofNullable(PRIMITIVE_TYPE_MAP.get(primitiveType)); } @@ -270,7 +270,7 @@ public static Optional getPrimitiveType(String primitiveType) { * as a last resort, and note that any usage of this method will create complications on GraalVM. * * @param name The name of the class - * @param classLoader The classloader. If null will fallback to attempt the thread context loader, otherwise the system loader + * @param classLoader The classloader. If null will fall back to attempt the thread context loader, otherwise the system loader * @return An optional of the class */ public static Optional> forName(String name, @Nullable ClassLoader classLoader) { @@ -313,10 +313,10 @@ public static Optional> forName(String name, @Nullable ClassLoader clas * @param type The class to start with * @return The class hierarchy */ - public static List resolveHierarchy(Class type) { + public static List> resolveHierarchy(Class type) { Class superclass = type.getSuperclass(); - List hierarchy = new ArrayList<>(); - List interfaces = new ArrayList<>(); + List> hierarchy = new ArrayList<>(); + List> interfaces = new ArrayList<>(); if (superclass != null) { hierarchy.add(type); populateHierarchyInterfaces(type, interfaces); @@ -345,7 +345,7 @@ public static List resolveHierarchy(Class type) { return hierarchy; } - private static void populateHierarchyInterfaces(Class superclass, List hierarchy) { + private static void populateHierarchyInterfaces(Class superclass, List> hierarchy) { for (Class aClass : superclass.getInterfaces()) { if (!hierarchy.contains(aClass)) { hierarchy.add(aClass); diff --git a/core/src/main/java/io/micronaut/core/reflect/GenericTypeUtils.java b/core/src/main/java/io/micronaut/core/reflect/GenericTypeUtils.java index e86613abb1f..9cdd727aca0 100644 --- a/core/src/main/java/io/micronaut/core/reflect/GenericTypeUtils.java +++ b/core/src/main/java/io/micronaut/core/reflect/GenericTypeUtils.java @@ -40,7 +40,7 @@ public class GenericTypeUtils { * @param field The field * @return The type argument or {@link Optional#empty()} */ - public static Optional resolveGenericTypeArgument(Field field) { + public static Optional> resolveGenericTypeArgument(Field field) { Type genericType = field != null ? field.getGenericType() : null; if (genericType instanceof ParameterizedType) { Type[] typeArguments = ((ParameterizedType) genericType).getActualTypeArguments(); @@ -60,7 +60,7 @@ public static Optional resolveGenericTypeArgument(Field field) { * @param interfaceType The interface to resolve from * @return The type arguments to the interface */ - public static Class[] resolveInterfaceTypeArguments(Class type, Class interfaceType) { + public static Class[] resolveInterfaceTypeArguments(Class type, Class interfaceType) { Optional resolvedType = getAllGenericInterfaces(type) .stream() .filter(t -> { @@ -83,7 +83,7 @@ public static Class[] resolveInterfaceTypeArguments(Class type, Class inte * @param superTypeToResolve The suepr type to resolve from * @return The type arguments to the interface */ - public static Class[] resolveSuperTypeGenericArguments(Class type, Class superTypeToResolve) { + public static Class[] resolveSuperTypeGenericArguments(Class type, Class superTypeToResolve) { Type supertype = type.getGenericSuperclass(); Class superclass = type.getSuperclass(); while (superclass != null && superclass != Object.class) { @@ -106,7 +106,7 @@ public static Class[] resolveSuperTypeGenericArguments(Class type, Class s * @param type The type to resolve from * @return A single Class or null */ - public static Optional resolveSuperGenericTypeArgument(Class type) { + public static Optional> resolveSuperGenericTypeArgument(Class type) { try { Type genericSuperclass = type.getGenericSuperclass(); if (genericSuperclass instanceof ParameterizedType) { @@ -124,8 +124,8 @@ public static Optional resolveSuperGenericTypeArgument(Class type) { * @param genericType The generic type * @return The type arguments */ - public static Class[] resolveTypeArguments(Type genericType) { - Class[] typeArguments = ReflectionUtils.EMPTY_CLASS_ARRAY; + public static Class[] resolveTypeArguments(Type genericType) { + Class[] typeArguments = ReflectionUtils.EMPTY_CLASS_ARRAY; if (genericType instanceof ParameterizedType) { ParameterizedType pt = (ParameterizedType) genericType; typeArguments = resolveParameterizedType(pt); @@ -141,7 +141,7 @@ public static Class[] resolveTypeArguments(Type genericType) { * @param interfaceType The interface to resolve for * @return The class or null */ - public static Optional resolveInterfaceTypeArgument(Class type, Class interfaceType) { + public static Optional> resolveInterfaceTypeArgument(Class type, Class interfaceType) { Type[] genericInterfaces = type.getGenericInterfaces(); for (Type genericInterface : genericInterfaces) { if (genericInterface instanceof ParameterizedType) { @@ -151,7 +151,7 @@ public static Optional resolveInterfaceTypeArgument(Class type, Class int } } } - Class superClass = type.getSuperclass(); + Class superClass = type.getSuperclass(); if (superClass != null && superClass != Object.class) { return resolveInterfaceTypeArgument(superClass, interfaceType); } @@ -164,7 +164,7 @@ public static Optional resolveInterfaceTypeArgument(Class type, Class int * @param genericType The generic type * @return An {@link Optional} of the type */ - private static Optional resolveSingleTypeArgument(Type genericType) { + private static Optional> resolveSingleTypeArgument(Type genericType) { if (genericType instanceof ParameterizedType) { ParameterizedType pt = (ParameterizedType) genericType; Type[] actualTypeArguments = pt.getActualTypeArguments(); @@ -180,15 +180,14 @@ private static Optional resolveSingleTypeArgument(Type genericType) { * @param actualTypeArgument The actual type argument * @return An optional with the resolved parameterized class */ - private static Optional resolveParameterizedTypeArgument(Type actualTypeArgument) { + private static Optional> resolveParameterizedTypeArgument(Type actualTypeArgument) { if (actualTypeArgument instanceof Class) { - return Optional.of((Class) actualTypeArgument); + return Optional.of((Class) actualTypeArgument); } - if (actualTypeArgument instanceof ParameterizedType) { - ParameterizedType pt = (ParameterizedType) actualTypeArgument; + if (actualTypeArgument instanceof ParameterizedType pt) { Type rawType = pt.getRawType(); if (rawType instanceof Class) { - return Optional.of((Class) rawType); + return Optional.of((Class) rawType); } } return Optional.empty(); @@ -230,14 +229,14 @@ private static Set populateInterfaces(Class aClass, Set interface return interfaces; } - private static Class[] resolveParameterizedType(ParameterizedType pt) { - Class[] typeArguments = ReflectionUtils.EMPTY_CLASS_ARRAY; + private static Class[] resolveParameterizedType(ParameterizedType pt) { + Class[] typeArguments = ReflectionUtils.EMPTY_CLASS_ARRAY; Type[] actualTypeArguments = pt.getActualTypeArguments(); if (actualTypeArguments != null && actualTypeArguments.length > 0) { - typeArguments = new Class[actualTypeArguments.length]; + typeArguments = new Class[actualTypeArguments.length]; for (int i = 0; i < actualTypeArguments.length; i++) { Type actualTypeArgument = actualTypeArguments[i]; - Optional opt = resolveParameterizedTypeArgument(actualTypeArgument); + Optional> opt = resolveParameterizedTypeArgument(actualTypeArgument); if (opt.isPresent()) { typeArguments[i] = opt.get(); } else { diff --git a/core/src/main/java/io/micronaut/core/reflect/ReflectionUtils.java b/core/src/main/java/io/micronaut/core/reflect/ReflectionUtils.java index c5c4e9ea193..84b80e16122 100644 --- a/core/src/main/java/io/micronaut/core/reflect/ReflectionUtils.java +++ b/core/src/main/java/io/micronaut/core/reflect/ReflectionUtils.java @@ -40,7 +40,7 @@ /** * Utility methods for reflection related tasks. Micronaut tries to avoid using reflection wherever possible, * this class is therefore considered an internal class and covers edge cases needed by Micronaut, often at compile time. - * + *

* Do not use in application code. * * @author Graeme Rocher @@ -52,7 +52,7 @@ public class ReflectionUtils { * Constant for empty class array. */ @UsedByGeneratedCode - public static final Class[] EMPTY_CLASS_ARRAY = new Class[0]; + public static final Class[] EMPTY_CLASS_ARRAY = new Class[0]; private static final Map, Class> PRIMITIVES_TO_WRAPPERS; @@ -93,7 +93,7 @@ public class ReflectionUtils { * @param args The arguments * @return True if it is */ - public static boolean isSetter(String name, Class[] args) { + public static boolean isSetter(String name, Class[] args) { if (StringUtils.isEmpty(name) || args == null) { return false; } @@ -110,7 +110,7 @@ public static boolean isSetter(String name, Class[] args) { * @param primitiveType The primitive type * @return The wrapper type */ - public static Class getWrapperType(Class primitiveType) { + public static Class getWrapperType(Class primitiveType) { if (primitiveType.isPrimitive()) { return PRIMITIVES_TO_WRAPPERS.get(primitiveType); } @@ -123,7 +123,7 @@ public static Class getWrapperType(Class primitiveType) { * @param wrapperType The primitive type * @return The wrapper type */ - public static Class getPrimitiveType(Class wrapperType) { + public static Class getPrimitiveType(Class wrapperType) { Class wrapper = WRAPPER_TO_PRIMITIVE.get(wrapperType); if (wrapper != null) { return wrapper; @@ -139,7 +139,7 @@ public static Class getPrimitiveType(Class wrapperType) { * @param argTypes The argument types * @return The method */ - public static Optional getDeclaredMethod(Class type, String methodName, Class... argTypes) { + public static Optional getDeclaredMethod(Class type, String methodName, Class... argTypes) { try { return Optional.of(type.getDeclaredMethod(methodName, argTypes)); } catch (NoSuchMethodException e) { @@ -155,7 +155,7 @@ public static Optional getDeclaredMethod(Class type, String methodName, * @param argTypes The argument types * @return An optional {@link Method} */ - public static Optional getMethod(Class type, String methodName, Class... argTypes) { + public static Optional getMethod(Class type, String methodName, Class... argTypes) { try { return Optional.of(type.getMethod(methodName, argTypes)); } catch (NoSuchMethodException e) { @@ -171,7 +171,7 @@ public static Optional getMethod(Class type, String methodName, Class... * @param The generic type * @return The method */ - public static Optional> findConstructor(Class type, Class... argTypes) { + public static Optional> findConstructor(Class type, Class... argTypes) { try { return Optional.of(type.getDeclaredConstructor(argTypes)); } catch (NoSuchMethodException e) { @@ -208,8 +208,8 @@ public static R invokeMethod(T instance, Method method, Object... argumen * @return An {@link Optional} contains the method or empty */ @Internal - public static Optional findMethod(Class type, String name, Class... argumentTypes) { - Class currentType = type; + public static Optional findMethod(Class type, String name, Class... argumentTypes) { + Class currentType = type; while (currentType != null) { Method[] methods = currentType.isInterface() ? currentType.getMethods() : currentType.getDeclaredMethods(); for (Method method : methods) { @@ -232,7 +232,7 @@ public static Optional findMethod(Class type, String name, Class... argu */ @UsedByGeneratedCode @Internal - public static Method getRequiredMethod(Class type, String name, Class... argumentTypes) { + public static Method getRequiredMethod(Class type, String name, Class... argumentTypes) { try { return type.getDeclaredMethod(name, argumentTypes); } catch (NoSuchMethodException e) { @@ -251,7 +251,7 @@ public static Method getRequiredMethod(Class type, String name, Class... argumen * @throws NoSuchMethodError If the method doesn't exist */ @Internal - public static Method getRequiredInternalMethod(Class type, String name, Class... argumentTypes) { + public static Method getRequiredInternalMethod(Class type, String name, Class... argumentTypes) { try { return type.getDeclaredMethod(name, argumentTypes); } catch (NoSuchMethodException e) { @@ -270,7 +270,7 @@ public static Method getRequiredInternalMethod(Class type, String name, Class... * @throws NoSuchMethodError If the method doesn't exist */ @Internal - public static Constructor getRequiredInternalConstructor(Class type, Class... argumentTypes) { + public static Constructor getRequiredInternalConstructor(Class type, Class... argumentTypes) { try { return type.getDeclaredConstructor(argumentTypes); } catch (NoSuchMethodException e) { @@ -286,7 +286,7 @@ public static Constructor getRequiredInternalConstructor(Class type, C * @return An {@link Optional} contains the method or empty */ @Internal - public static Field getRequiredField(Class type, String name) { + public static Field getRequiredField(Class type, String name) { try { return type.getDeclaredField(name); } catch (NoSuchFieldException e) { @@ -303,9 +303,9 @@ public static Field getRequiredField(Class type, String name) { * @return An {@link Optional} of field */ @Internal - public static Optional findField(Class type, String name) { + public static Optional findField(Class type, String name) { Optional declaredField = findDeclaredField(type, name); - if (!declaredField.isPresent()) { + if (declaredField.isEmpty()) { while ((type = type.getSuperclass()) != null) { declaredField = findField(type, name); if (declaredField.isPresent()) { @@ -323,8 +323,8 @@ public static Optional findField(Class type, String name) { * @param name The name * @return An {@link Optional} contains the method or empty */ - public static Stream findMethodsByName(Class type, String name) { - Class currentType = type; + public static Stream findMethodsByName(Class type, String name) { + Class currentType = type; Set methodSet = new HashSet<>(); while (currentType != null) { Method[] methods = currentType.isInterface() ? currentType.getMethods() : currentType.getDeclaredMethods(); @@ -343,7 +343,7 @@ public static Stream findMethodsByName(Class type, String name) { * @param name The field name * @return An optional with the declared field */ - public static Optional findDeclaredField(Class type, String name) { + public static Optional findDeclaredField(Class type, String name) { try { Field declaredField = type.getDeclaredField(name); return Optional.of(declaredField); @@ -356,8 +356,8 @@ public static Optional findDeclaredField(Class type, String name) { * @param aClass A class * @return All the interfaces */ - public static Set getAllInterfaces(Class aClass) { - Set interfaces = new HashSet<>(); + public static Set> getAllInterfaces(Class aClass) { + Set> interfaces = new HashSet<>(); return populateInterfaces(aClass, interfaces); } @@ -367,7 +367,7 @@ public static Set getAllInterfaces(Class aClass) { * @return A set with the interfaces */ @SuppressWarnings("Duplicates") - protected static Set populateInterfaces(Class aClass, Set interfaces) { + protected static Set> populateInterfaces(Class aClass, Set> interfaces) { Class[] theInterfaces = aClass.getInterfaces(); interfaces.addAll(Arrays.asList(theInterfaces)); for (Class theInterface : theInterfaces) { @@ -389,21 +389,21 @@ protected static Set populateInterfaces(Class aClass, Set inter * @param argumentTypes The argument types * @return A {@link NoSuchMethodError} */ - public static NoSuchMethodError newNoSuchMethodError(Class declaringType, String name, Class[] argumentTypes) { + public static NoSuchMethodError newNoSuchMethodError(Class declaringType, String name, Class[] argumentTypes) { Stream stringStream = Arrays.stream(argumentTypes).map(Class::getSimpleName); String argsAsText = stringStream.collect(Collectors.joining(",")); return new NoSuchMethodError("Required method " + name + "(" + argsAsText + ") not found for class: " + declaringType.getName() + ". Most likely cause of this error is the method declaration is not annotated with @Executable. Alternatively check that there is not an unsupported or older version of a dependency present on the classpath. Check your classpath, and ensure the incompatible classes are not present and/or recompile classes as necessary."); } - private static NoSuchMethodError newNoSuchMethodInternalError(Class declaringType, String name, Class[] argumentTypes) { + private static NoSuchMethodError newNoSuchMethodInternalError(Class declaringType, String name, Class[] argumentTypes) { Stream stringStream = Arrays.stream(argumentTypes).map(Class::getSimpleName); String argsAsText = stringStream.collect(Collectors.joining(",")); return new NoSuchMethodError("Micronaut method " + declaringType.getName() + "." + name + "(" + argsAsText + ") not found. Most likely reason for this issue is that you are running a newer version of Micronaut with code compiled against an older version. Please recompile the offending classes"); } - private static NoSuchMethodError newNoSuchConstructorInternalError(Class declaringType, Class[] argumentTypes) { + private static NoSuchMethodError newNoSuchConstructorInternalError(Class declaringType, Class[] argumentTypes) { Stream stringStream = Arrays.stream(argumentTypes).map(Class::getSimpleName); String argsAsText = stringStream.collect(Collectors.joining(",")); @@ -438,7 +438,7 @@ public static void setField( * @since 3.7.0 */ @UsedByGeneratedCode - public static Object getField(@NonNull Class clazz, @NonNull String fieldName, @NonNull Object instance) { + public static Object getField(@NonNull Class clazz, @NonNull String fieldName, @NonNull Object instance) { try { ClassUtils.REFLECTION_LOGGER.debug("Reflectively getting field {} of class {} and instance {}", fieldName, clazz, instance); Field field = getRequiredField(clazz, fieldName); diff --git a/core/src/main/java/io/micronaut/core/value/PropertyNotFoundException.java b/core/src/main/java/io/micronaut/core/value/PropertyNotFoundException.java index fe216e13e85..e8eb2a7fafc 100644 --- a/core/src/main/java/io/micronaut/core/value/PropertyNotFoundException.java +++ b/core/src/main/java/io/micronaut/core/value/PropertyNotFoundException.java @@ -28,7 +28,7 @@ public class PropertyNotFoundException extends ValueException { * @param name name * @param type type */ - public PropertyNotFoundException(String name, Class type) { + public PropertyNotFoundException(String name, Class type) { super("No property found for name [" + name + "] and type [" + type.getName() + "]"); } } diff --git a/core/src/test/groovy/io/micronaut/core/bind/ExecutableBinderSpec.groovy b/core/src/test/groovy/io/micronaut/core/bind/ExecutableBinderSpec.groovy index e39b81884f5..912b78a9950 100644 --- a/core/src/test/groovy/io/micronaut/core/bind/ExecutableBinderSpec.groovy +++ b/core/src/test/groovy/io/micronaut/core/bind/ExecutableBinderSpec.groovy @@ -46,7 +46,7 @@ class ExecutableBinderSpec extends Specification { } @Override - Class getDeclaringType() { + Class getDeclaringType() { return null } } @@ -89,7 +89,7 @@ class ExecutableBinderSpec extends Specification { } @Override - Class getDeclaringType() { + Class getDeclaringType() { return null } } @@ -127,7 +127,7 @@ class ExecutableBinderSpec extends Specification { } @Override - Class getDeclaringType() { + Class getDeclaringType() { return null } } diff --git a/function/src/main/java/io/micronaut/function/executor/StreamFunctionExecutor.java b/function/src/main/java/io/micronaut/function/executor/StreamFunctionExecutor.java index fc86979eef1..4884486dbb5 100644 --- a/function/src/main/java/io/micronaut/function/executor/StreamFunctionExecutor.java +++ b/function/src/main/java/io/micronaut/function/executor/StreamFunctionExecutor.java @@ -143,7 +143,7 @@ protected void execute(InputStream input, OutputStream output, C context) throws * @param output outputstream * @throws IOException input/output exception */ - static void encode(Environment environment, LocalFunctionRegistry registry, Class returnType, Object result, OutputStream output) throws IOException { + static void encode(Environment environment, LocalFunctionRegistry registry, Class returnType, Object result, OutputStream output) throws IOException { if (ClassUtils.isJavaLangType(returnType)) { if (result instanceof Byte) { output.write((Byte) result); diff --git a/http-client/src/main/java/io/micronaut/http/client/netty/DefaultNettyHttpClientRegistry.java b/http-client/src/main/java/io/micronaut/http/client/netty/DefaultNettyHttpClientRegistry.java index 07acfebeee2..e7391e0ccb7 100644 --- a/http-client/src/main/java/io/micronaut/http/client/netty/DefaultNettyHttpClientRegistry.java +++ b/http-client/src/main/java/io/micronaut/http/client/netty/DefaultNettyHttpClientRegistry.java @@ -482,7 +482,7 @@ private ClientKey getClientKey(AnnotationMetadata metadata) { String path = metadata.stringValue(Client.class, "path").orElse(null); List filterAnnotation = metadata .getAnnotationNamesByStereotype(FilterMatcher.class); - final Class configurationClass = + final Class configurationClass = metadata.classValue(Client.class, "configuration").orElse(null); JsonFeatures jsonFeatures = jsonMapper.detectFeatures(metadata).orElse(null); diff --git a/http-netty/src/main/java/io/micronaut/http/netty/AbstractCompositeCustomizer.java b/http-netty/src/main/java/io/micronaut/http/netty/AbstractCompositeCustomizer.java index 201b261c001..b2a09408a38 100644 --- a/http-netty/src/main/java/io/micronaut/http/netty/AbstractCompositeCustomizer.java +++ b/http-netty/src/main/java/io/micronaut/http/netty/AbstractCompositeCustomizer.java @@ -53,6 +53,11 @@ protected AbstractCompositeCustomizer() { this(new CopyOnWriteArrayList<>()); } + /** + * Add customizer. + * + * @param customizer Customizer + */ public synchronized void add(C customizer) { assert members instanceof CopyOnWriteArrayList : "only allow adding to root customizer"; // do the insertion in one operation, so that concurrent readers don't see an inconsistent diff --git a/http-netty/src/main/java/io/micronaut/http/netty/channel/converters/DefaultChannelOptionFactory.java b/http-netty/src/main/java/io/micronaut/http/netty/channel/converters/DefaultChannelOptionFactory.java index 3db6a5f31a7..02f61b10a71 100644 --- a/http-netty/src/main/java/io/micronaut/http/netty/channel/converters/DefaultChannelOptionFactory.java +++ b/http-netty/src/main/java/io/micronaut/http/netty/channel/converters/DefaultChannelOptionFactory.java @@ -40,9 +40,9 @@ public class DefaultChannelOptionFactory implements ChannelOptionFactory { private static Object processChannelOptionValue(Class cls, String name, Object value, Environment env) { Optional declaredField = ReflectionUtils.findField(cls, name); if (declaredField.isPresent()) { - Optional typeArg = GenericTypeUtils.resolveGenericTypeArgument(declaredField.get()); + Optional> typeArg = GenericTypeUtils.resolveGenericTypeArgument(declaredField.get()); if (typeArg.isPresent()) { - Optional converted = env.convert(value, typeArg.get()); + Optional converted = (Optional) env.convert(value, typeArg.get()); value = converted.orElse(value); } } diff --git a/http-netty/src/main/java/io/micronaut/http/netty/websocket/AbstractNettyWebSocketHandler.java b/http-netty/src/main/java/io/micronaut/http/netty/websocket/AbstractNettyWebSocketHandler.java index 59a2a08d763..b7f4d55593c 100644 --- a/http-netty/src/main/java/io/micronaut/http/netty/websocket/AbstractNettyWebSocketHandler.java +++ b/http-netty/src/main/java/io/micronaut/http/netty/websocket/AbstractNettyWebSocketHandler.java @@ -634,7 +634,7 @@ private BoundExecutable bindMethod(HttpRequest request, ArgumentBinderRegistr private Map, Object> prepareBoundVariables(ExecutableMethod executable, List parameters) { Map, Object> preBound = new HashMap<>(executable.getArguments().length); for (Argument argument : executable.getArguments()) { - Class type = argument.getType(); + Class type = argument.getType(); for (Object object : parameters) { if (type.isInstance(object)) { preBound.put(argument, object); diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/DefaultHttpContentProcessorResolver.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/DefaultHttpContentProcessorResolver.java index 847bff6d418..c14e026d83a 100644 --- a/http-server-netty/src/main/java/io/micronaut/http/server/netty/DefaultHttpContentProcessorResolver.java +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/DefaultHttpContentProcessorResolver.java @@ -53,7 +53,7 @@ @Internal class DefaultHttpContentProcessorResolver implements HttpContentProcessorResolver { - private static final Set RAW_BODY_TYPES = CollectionUtils.setOf(String.class, byte[].class, ByteBuffer.class, InputStream.class); + private static final Set> RAW_BODY_TYPES = CollectionUtils.setOf(String.class, byte[].class, ByteBuffer.class, InputStream.class); private final BeanLocator beanLocator; private final BeanProvider serverConfiguration; diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/RoutingInBoundHandler.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/RoutingInBoundHandler.java index 2489f19d62e..9f8bb5c9dd7 100644 --- a/http-server-netty/src/main/java/io/micronaut/http/server/netty/RoutingInBoundHandler.java +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/RoutingInBoundHandler.java @@ -476,7 +476,7 @@ private void doOnNext0(Object message) { } else { typeVariable = argument.getFirstTypeVariable().orElse(Argument.OBJECT_ARGUMENT); } - Class typeVariableType = typeVariable.getType(); + Class typeVariableType = typeVariable.getType(); Sinks.Many namedSubject = subjectsByDataName.computeIfAbsent(name, key -> makeDownstreamUnicastProcessor()); diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/StreamTypeHandler.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/StreamTypeHandler.java index 8a5b4f8d90f..ca8738d1132 100644 --- a/http-server-netty/src/main/java/io/micronaut/http/server/netty/StreamTypeHandler.java +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/StreamTypeHandler.java @@ -36,7 +36,7 @@ @Internal class StreamTypeHandler implements NettyCustomizableResponseTypeHandler { - private static final Class[] SUPPORTED_TYPES = new Class[]{NettyStreamedCustomizableResponseType.class, InputStream.class}; + private static final Class[] SUPPORTED_TYPES = new Class[]{NettyStreamedCustomizableResponseType.class, InputStream.class}; @Override public ChannelFuture handle(Object object, HttpRequest request, MutableHttpResponse response, ChannelHandlerContext context) { diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/jackson/JsonContentProcessor.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/jackson/JsonContentProcessor.java index 28d244048d4..23839383838 100644 --- a/http-server-netty/src/main/java/io/micronaut/http/server/netty/jackson/JsonContentProcessor.java +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/jackson/JsonContentProcessor.java @@ -78,7 +78,7 @@ protected void doOnSubscribe(Subscription subscription, Subscriber targetType = typeArgument.getType(); if (Publishers.isConvertibleToPublisher(targetType) && !Publishers.isSingle(targetType)) { Optional> genericArgument = typeArgument.getFirstTypeVariable(); if (genericArgument.isPresent() && !Iterable.class.isAssignableFrom(genericArgument.get().getType()) && !isJsonStream) { diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/types/files/FileTypeHandler.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/types/files/FileTypeHandler.java index 3c2c1424c37..3f350bc0a5e 100644 --- a/http-server-netty/src/main/java/io/micronaut/http/server/netty/types/files/FileTypeHandler.java +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/types/files/FileTypeHandler.java @@ -49,8 +49,8 @@ public class FileTypeHandler implements NettyCustomizableResponseTypeHandler[] SUPPORTED_TYPES = new Class[]{File.class, StreamedFile.class, NettyFileCustomizableResponseType.class, SystemFile.class}; + private static final String[] ENTITY_HEADERS = {HttpHeaders.ALLOW, HttpHeaders.CONTENT_ENCODING, HttpHeaders.CONTENT_LANGUAGE, HttpHeaders.CONTENT_LENGTH, HttpHeaders.CONTENT_LOCATION, HttpHeaders.CONTENT_MD5, HttpHeaders.CONTENT_RANGE, HttpHeaders.CONTENT_TYPE, HttpHeaders.EXPIRES, HttpHeaders.LAST_MODIFIED}; + private static final Class[] SUPPORTED_TYPES = new Class[]{File.class, StreamedFile.class, NettyFileCustomizableResponseType.class, SystemFile.class}; private final NettyHttpServerConfiguration.FileTypeHandlerConfiguration configuration; /** diff --git a/http-server-netty/src/test/groovy/io/micronaut/web/router/version/VersionControllerSpec.groovy b/http-server-netty/src/test/groovy/io/micronaut/web/router/version/VersionControllerSpec.groovy index 844f3deeadd..ef17efbacd6 100644 --- a/http-server-netty/src/test/groovy/io/micronaut/web/router/version/VersionControllerSpec.groovy +++ b/http-server-netty/src/test/groovy/io/micronaut/web/router/version/VersionControllerSpec.groovy @@ -114,8 +114,8 @@ class VersionControllerSpec extends Specification { } private boolean areAllUriRoutesAnnotatedWith(List> uriRoutes, - Class annotationClass, - Class annotationValue) { + Class annotationClass, + Class annotationValue) { uriRoutes.every { uriRoute -> AnnotationValue versionAnnotationValue = uriRoute.getAnnotation(annotationClass) if (versionAnnotationValue == null) { diff --git a/http-server/src/main/java/io/micronaut/http/server/RouteExecutor.java b/http-server/src/main/java/io/micronaut/http/server/RouteExecutor.java index b380c633b54..c0ac9b4860b 100644 --- a/http-server/src/main/java/io/micronaut/http/server/RouteExecutor.java +++ b/http-server/src/main/java/io/micronaut/http/server/RouteExecutor.java @@ -625,7 +625,7 @@ private RouteMatch findErrorRoute(Throwable cause, RouteMatch errorRoute = null; if (cause instanceof BeanCreationException && declaringType != null) { // If the controller could not be instantiated, don't look for a local error route - Optional rootBeanType = ((BeanCreationException) cause).getRootBeanType().map(BeanType::getBeanType); + Optional> rootBeanType = ((BeanCreationException) cause).getRootBeanType().map(BeanType::getBeanType); if (rootBeanType.isPresent() && declaringType == rootBeanType.get()) { if (LOG.isDebugEnabled()) { LOG.debug("Failed to instantiate [{}]. Skipping lookup of a local error route", declaringType.getName()); diff --git a/http-server/src/main/java/io/micronaut/http/server/websocket/ServerWebSocketProcessor.java b/http-server/src/main/java/io/micronaut/http/server/websocket/ServerWebSocketProcessor.java index 3ade351c7dd..36d35f03df7 100644 --- a/http-server/src/main/java/io/micronaut/http/server/websocket/ServerWebSocketProcessor.java +++ b/http-server/src/main/java/io/micronaut/http/server/websocket/ServerWebSocketProcessor.java @@ -44,7 +44,7 @@ @Requires(classes = {ServerWebSocket.class, WebSocketBeanRegistry.class}) public class ServerWebSocketProcessor extends DefaultRouteBuilder implements ExecutableMethodProcessor { - private Set mappedWebSockets = new HashSet<>(4); + private Set> mappedWebSockets = new HashSet<>(4); /** * Default constructor. diff --git a/http/src/main/java/io/micronaut/http/uri/UriTypeMatchTemplate.java b/http/src/main/java/io/micronaut/http/uri/UriTypeMatchTemplate.java index 686322647f2..dfe86bf3e4a 100644 --- a/http/src/main/java/io/micronaut/http/uri/UriTypeMatchTemplate.java +++ b/http/src/main/java/io/micronaut/http/uri/UriTypeMatchTemplate.java @@ -31,15 +31,15 @@ */ public class UriTypeMatchTemplate extends UriMatchTemplate { - private Class[] variableTypes; + private Class[] variableTypes; /** * @param templateString The template * @param variableTypes The variable types */ - public UriTypeMatchTemplate(CharSequence templateString, Class... variableTypes) { + public UriTypeMatchTemplate(CharSequence templateString, Class... variableTypes) { super(templateString, new Object[] {variableTypes}); - this.variableTypes = variableTypes == null ? new Class[0] : variableTypes; + this.variableTypes = variableTypes == null ? new Class[0] : variableTypes; } /** @@ -49,7 +49,7 @@ public UriTypeMatchTemplate(CharSequence templateString, Class... variableTypes) * @param variableTypes The variable types * @param variables The variables */ - protected UriTypeMatchTemplate(CharSequence templateString, List segments, Pattern matchPattern, Class[] variableTypes, List variables) { + protected UriTypeMatchTemplate(CharSequence templateString, List segments, Pattern matchPattern, Class[] variableTypes, List variables) { super(templateString, segments, matchPattern, variables); this.variableTypes = variableTypes; } @@ -64,7 +64,7 @@ public UriTypeMatchTemplate nest(CharSequence uriTemplate) { * @param variableTypes The variable types * @return The new URI template */ - public UriTypeMatchTemplate nest(CharSequence uriTemplate, Class... variableTypes) { + public UriTypeMatchTemplate nest(CharSequence uriTemplate, Class... variableTypes) { return (UriTypeMatchTemplate) super.nest(uriTemplate, new Object[] {variableTypes}); } @@ -79,7 +79,7 @@ protected UriTemplateParser createParser(String templateString, Object... parser if (this.variables == null) { this.variables = new ArrayList<>(); } - this.variableTypes = parserArguments != null && parserArguments.length > 0 ? (Class[]) parserArguments[0] : new Class[0]; + this.variableTypes = parserArguments != null && parserArguments.length > 0 ? (Class[]) parserArguments[0] : new Class[0]; return new TypedUriMatchTemplateParser(templateString, this); } @@ -94,7 +94,7 @@ protected UriMatchTemplate newUriMatchTemplate(CharSequence uriTemplate, List variableType, String variable, char operator) { if (Number.class.isAssignableFrom(variableType)) { if (Double.class == variableType || Float.class == variableType || BigDecimal.class == variableType) { return "([\\d\\.+]"; @@ -128,10 +128,10 @@ public UriTypeMatchTemplate getMatchTemplate() { @Override protected String getVariablePattern(String variable, char operator) { UriTypeMatchTemplate matchTemplate = getMatchTemplate(); - Class[] variableTypes = matchTemplate.variableTypes; + Class[] variableTypes = matchTemplate.variableTypes; try { if (variableIndex < variableTypes.length) { - Class variableType = variableTypes[variableIndex]; + Class variableType = variableTypes[variableIndex]; return matchTemplate.resolveTypePattern(variableType, variable, operator); } else { return super.getVariablePattern(variable, operator); diff --git a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/annotation/GroovyAnnotationMetadataBuilder.java b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/annotation/GroovyAnnotationMetadataBuilder.java index f2b7a63fb02..da522eda39e 100644 --- a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/annotation/GroovyAnnotationMetadataBuilder.java +++ b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/annotation/GroovyAnnotationMetadataBuilder.java @@ -497,7 +497,7 @@ protected Object readAnnotationValue(AnnotatedNode originatingElement, Annotated return pe.getPropertyAsString(); } else { if (propertyType.isResolved()) { - Class typeClass = propertyType.getTypeClass(); + Class typeClass = propertyType.getTypeClass(); try { final Field f = ReflectionUtils.getRequiredField(typeClass, pe.getPropertyAsString()); f.setAccessible(true); diff --git a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/utils/AstGenericUtils.groovy b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/utils/AstGenericUtils.groovy index f738a4c4758..38203821e3d 100644 --- a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/utils/AstGenericUtils.groovy +++ b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/utils/AstGenericUtils.groovy @@ -168,7 +168,7 @@ class AstGenericUtils { * @param itfe The interface * @return The generic type or null */ - static ClassNode resolveInterfaceGenericType(ClassNode classNode, Class itfe) { + static ClassNode resolveInterfaceGenericType(ClassNode classNode, Class itfe) { ClassNode foundInterface = classNode.allInterfaces.find() { it.name == itfe.name } if (foundInterface != null) { if (foundInterface.genericsTypes) { diff --git a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/utils/InMemoryByteCodeGroovyClassLoader.java b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/utils/InMemoryByteCodeGroovyClassLoader.java index 3896bce5766..d9d980c3445 100644 --- a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/utils/InMemoryByteCodeGroovyClassLoader.java +++ b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/utils/InMemoryByteCodeGroovyClassLoader.java @@ -48,7 +48,7 @@ public class InMemoryByteCodeGroovyClassLoader extends GroovyClassLoader { private Map generatedClasses = new ConcurrentHashMap<>(); private List generatedUrls = new ArrayList<>(); - private Map loadedClasses = new ConcurrentHashMap<>(); + private Map> loadedClasses = new ConcurrentHashMap<>(); /** * Default constructor. @@ -139,7 +139,7 @@ public Class loadClass(String name) throws ClassNotFoundException { if (loadedClasses.containsKey(name)) { return loadedClasses.get(name); } else if (generatedClasses.containsKey(name)) { - final Class cls = defineClass(name, generatedClasses.get(name)); + final Class cls = defineClass(name, generatedClasses.get(name)); loadedClasses.put(name, cls); return cls; } else { diff --git a/inject-groovy/src/test/groovy/io/micronaut/aop/compile/InheritedAnnotationMetadataSpec.groovy b/inject-groovy/src/test/groovy/io/micronaut/aop/compile/InheritedAnnotationMetadataSpec.groovy index f8e92be6f4d..567bfbfb1b5 100644 --- a/inject-groovy/src/test/groovy/io/micronaut/aop/compile/InheritedAnnotationMetadataSpec.groovy +++ b/inject-groovy/src/test/groovy/io/micronaut/aop/compile/InheritedAnnotationMetadataSpec.groovy @@ -72,11 +72,11 @@ import io.micronaut.core.annotation.*; class MyBean2 implements MyInterface2 { private String myValue; - + MyBean2(@Value('${foo.bar}') String val) { this.myValue = val; } - + @Override public String someMethod() { return myValue; @@ -132,7 +132,7 @@ class Service extends BaseService implements ContractService { @SomeAnnot public void serviceMethod() {} - + public void interfaceServiceMethod() {} } @@ -154,7 +154,7 @@ class SomeInterceptor implements MethodInterceptor, Ordered { ''') then: - Class clazz = ctx.classLoader.loadClass("inheritmetadatatest3.ContractService") + Class clazz = ctx.classLoader.loadClass("inheritmetadatatest3.ContractService") ctx.getBean(clazz) when: diff --git a/inject-groovy/src/test/groovy/io/micronaut/ast/groovy/annotation/Trace.groovy b/inject-groovy/src/test/groovy/io/micronaut/ast/groovy/annotation/Trace.groovy index 8684b90ebfd..e02c36c5e75 100644 --- a/inject-groovy/src/test/groovy/io/micronaut/ast/groovy/annotation/Trace.groovy +++ b/inject-groovy/src/test/groovy/io/micronaut/ast/groovy/annotation/Trace.groovy @@ -15,7 +15,6 @@ */ package io.micronaut.ast.groovy.annotation -import io.micronaut.context.annotation.AliasFor import io.micronaut.aop.Around; import io.micronaut.context.annotation.AliasFor; @@ -42,7 +41,7 @@ public @interface Trace { @AliasFor(annotation = Around.class, member = "hotswap") boolean something() default false; - Class type(); + Class type(); - Class[] types(); -} \ No newline at end of file + Class[] types(); +} diff --git a/inject-groovy/src/test/groovy/io/micronaut/inject/configproperties/ConfigurationPropertiesBuilderSpec.groovy b/inject-groovy/src/test/groovy/io/micronaut/inject/configproperties/ConfigurationPropertiesBuilderSpec.groovy index 8d9026ba2b7..7dfe2be1ecc 100644 --- a/inject-groovy/src/test/groovy/io/micronaut/inject/configproperties/ConfigurationPropertiesBuilderSpec.groovy +++ b/inject-groovy/src/test/groovy/io/micronaut/inject/configproperties/ConfigurationPropertiesBuilderSpec.groovy @@ -33,14 +33,14 @@ import io.micronaut.context.annotation.* @ConfigurationProperties("test") class MyPropertiesAA { - + TestAA test - + @ConfigurationBuilder(factoryMethod="build", includes="foo") void setTest(TestAA test) { this.test = test; } - + TestAA getTest() { return this.test } @@ -49,25 +49,25 @@ class MyPropertiesAA { class TestAA { private String foo private String bar - + private TestAA() {} - - public void setFoo(String s) { + + public void setFoo(String s) { this.foo = s; } public String getFoo() { return foo; } - public void setBar(String s) { + public void setBar(String s) { this.bar = s; } public String getBar() { return bar; } - + static TestAA build() { new TestAA() - } + } } ''') @@ -93,7 +93,7 @@ import io.micronaut.context.annotation.* @ConfigurationProperties("test") class MyPropertiesA { - + @ConfigurationBuilder(factoryMethod="build", includes="foo") TestA test } @@ -101,25 +101,25 @@ class MyPropertiesA { class TestA { private String foo private String bar - + private TestA() {} - - public void setFoo(String s) { + + public void setFoo(String s) { this.foo = s; } public String getFoo() { return foo; } - public void setBar(String s) { + public void setBar(String s) { this.bar = s; } public String getBar() { return bar; } - + static TestA build() { new TestA() - } + } } ''') @@ -146,20 +146,20 @@ import io.micronaut.context.annotation.* @ConfigurationProperties("test") class MyPropertiesB { - + @ConfigurationBuilder(factoryMethod="build") TestB test - + } class TestB { String bar - + private TestB() {} static TestB build() { new TestB() - } + } } ''') @@ -183,14 +183,14 @@ import io.micronaut.context.annotation.* @ConfigurationProperties("test") class MyProperties { - + @ConfigurationBuilder TestC test = new TestC() - + } class TestC { - public void setFoo(String s) { + public void setFoo(String s) { throw new NoSuchMethodError("setFoo") } } @@ -223,7 +223,7 @@ class MyProperties { class TestD { String foo int bar - + @Deprecated Long baz } @@ -262,9 +262,9 @@ import org.neo4j.driver.v1.* @ConfigurationProperties("neo4j.test") class Neo4jProperties { protected java.net.URI uri - + @ConfigurationBuilder( - prefixes="with", + prefixes="with", allowZeroArgs=true ) Config.ConfigBuilder options = Config.build() @@ -309,14 +309,14 @@ import org.neo4j.driver.v1.* class Neo4jProperties { protected java.net.URI uri - + @ConfigurationBuilder( - prefixes="with", + prefixes="with", allowZeroArgs=true, configurationPrefix="options" ) Config.ConfigBuilder options = Config.build() - + } ''') then: @@ -357,14 +357,14 @@ import org.neo4j.driver.v1.* class Neo4jProperties { protected java.net.URI uri - + @ConfigurationBuilder( - prefixes="with", + prefixes="with", allowZeroArgs=true, value="options" ) Config.ConfigBuilder options = Config.build() - + } ''') then: @@ -404,13 +404,13 @@ import org.neo4j.driver.v1.* @ConfigurationProperties("neo4j.test") class Neo4jProperties { protected java.net.URI uri - + @ConfigurationBuilder( - prefixes="with", + prefixes="with", allowZeroArgs=true ) Config.ConfigBuilder options = Config.build() - + } ''') then: @@ -445,13 +445,13 @@ import org.neo4j.driver.v1.* @ConfigurationProperties("neo4j.test") class Neo4jProperties { - + @ConfigurationBuilder( - prefixes="with", + prefixes="with", allowZeroArgs=true ) final Config.ConfigBuilder options = Config.build() - + } ''') BeanFactory factory = beanDefinition @@ -479,53 +479,53 @@ package cpbtest11 import io.micronaut.context.annotation.* -@ConfigurationProperties("pool") -final class PoolConfig { - +@ConfigurationProperties("pool") +final class PoolConfig { + @ConfigurationBuilder(prefixes = [""]) public ConnectionPool.Builder builder = DefaultConnectionPool.builder() - + } interface ConnectionPool { - + interface Builder { Builder maxConcurrency(Integer maxConcurrency) ConnectionPool build() } - + int getMaxConcurrency() } class DefaultConnectionPool implements ConnectionPool { private final int maxConcurrency - + DefaultConnectionPool(int maxConcurrency) { this.maxConcurrency = maxConcurrency } - + static ConnectionPool.Builder builder() { return new DefaultBuilder() } - - @Override + + @Override int getMaxConcurrency() { return maxConcurrency } - + private static class DefaultBuilder implements ConnectionPool.Builder { - + private int maxConcurrency - + private DefaultBuilder() { } - + @Override ConnectionPool.Builder maxConcurrency(Integer maxConcurrency) { this.maxConcurrency = maxConcurrency return this } - + ConnectionPool build() { return new DefaultConnectionPool(maxConcurrency) } @@ -535,7 +535,7 @@ class DefaultConnectionPool implements ConnectionPool { ctx.getEnvironment().addPropertySource(PropertySource.of(["pool.max-concurrency": 123])) when: - Class testProps = ctx.classLoader.loadClass("cpbtest11.PoolConfig") + Class testProps = ctx.classLoader.loadClass("cpbtest11.PoolConfig") def testPropBean = ctx.getBean(testProps) then: diff --git a/inject-groovy/src/test/groovy/io/micronaut/inject/configuration/GroovyConfigBuilderSpec.groovy b/inject-groovy/src/test/groovy/io/micronaut/inject/configuration/GroovyConfigBuilderSpec.groovy index cdde68dcc4b..b0878dab21a 100644 --- a/inject-groovy/src/test/groovy/io/micronaut/inject/configuration/GroovyConfigBuilderSpec.groovy +++ b/inject-groovy/src/test/groovy/io/micronaut/inject/configuration/GroovyConfigBuilderSpec.groovy @@ -15,9 +15,9 @@ package test; import io.micronaut.context.annotation.*; import io.micronaut.inject.configuration.Engine; -@ConfigurationProperties("test.props") +@ConfigurationProperties("test.props") final class TestProps { - @ConfigurationBuilder(prefixes = "with") + @ConfigurationBuilder(prefixes = "with") private Engine.Builder builder = Engine.builder(); public final Engine.Builder getBuilder() { @@ -28,7 +28,7 @@ final class TestProps { ctx.getEnvironment().addPropertySource(PropertySource.of(["test.props.manufacturer": "Toyota"])) when: - Class testProps = ctx.classLoader.loadClass("test.TestProps") + Class testProps = ctx.classLoader.loadClass("test.TestProps") def testPropBean = ctx.getBean(testProps) then: @@ -45,9 +45,9 @@ package test; import io.micronaut.context.annotation.*; import io.micronaut.inject.configuration.Engine; -@ConfigurationProperties("test.props") +@ConfigurationProperties("test.props") final class TestProps { - @ConfigurationBuilder(prefixes = "with") + @ConfigurationBuilder(prefixes = "with") private Engine.Builder builder = Engine.builder(); } ''') @@ -66,14 +66,14 @@ package test; import io.micronaut.context.annotation.*; import io.micronaut.inject.configuration.Engine; -@ConfigurationProperties("test.props") -final class TestProps { +@ConfigurationProperties("test.props") +final class TestProps { Engine.Builder builder = Engine.builder(); - + Engine.Builder getBuilder() { return this.builder; } - + @ConfigurationBuilder(prefixes = "with") void setBuilder(Engine.Builder p0) { this.builder = p0; @@ -83,7 +83,7 @@ final class TestProps { ctx.getEnvironment().addPropertySource(PropertySource.of(["test.props.manufacturer": "Toyota"])) when: - Class testProps = ctx.classLoader.loadClass("test.TestProps") + Class testProps = ctx.classLoader.loadClass("test.TestProps") def testPropBean = ctx.getBean(testProps) then: diff --git a/inject-groovy/src/test/groovy/io/micronaut/inject/lifecyle/BeanWithPreDestroySpec.groovy b/inject-groovy/src/test/groovy/io/micronaut/inject/lifecyle/BeanWithPreDestroySpec.groovy index 5f7374b9690..d7eac87949c 100644 --- a/inject-groovy/src/test/groovy/io/micronaut/inject/lifecyle/BeanWithPreDestroySpec.groovy +++ b/inject-groovy/src/test/groovy/io/micronaut/inject/lifecyle/BeanWithPreDestroySpec.groovy @@ -86,10 +86,10 @@ class FooFactory { @Bean(preDestroy="close") Foo foo() { - new Foo() { + new Foo() { @Override void close()throws Exception{ - println("closed") + println("closed") } } } @@ -121,15 +121,15 @@ class FooFactory { @Singleton @Bean(preDestroy="close") Foo foo() { - new Foo() { - + new Foo() { + private boolean running = true - + @Override boolean isRunning(){ return running } - + @Override Foo stop() { running = false @@ -146,7 +146,7 @@ interface Foo extends LifeCycle { then: noExceptionThrown() - Class fooClass = beanContext.classLoader.loadClass("test.Foo") + Class fooClass = beanContext.classLoader.loadClass("test.Foo") beanContext.getBeanDefinition(fooClass) instanceof DisposableBeanDefinition when: diff --git a/inject-java-test/src/test/groovy/io/micronaut/inject/visitor/beans/BeanIntrospectionSpec.groovy b/inject-java-test/src/test/groovy/io/micronaut/inject/visitor/beans/BeanIntrospectionSpec.groovy index 14cd681d243..187740eb23e 100644 --- a/inject-java-test/src/test/groovy/io/micronaut/inject/visitor/beans/BeanIntrospectionSpec.groovy +++ b/inject-java-test/src/test/groovy/io/micronaut/inject/visitor/beans/BeanIntrospectionSpec.groovy @@ -2027,7 +2027,7 @@ class Book { } } ''') - Class clazz = context.classLoader.loadClass('test.$Book$IntrospectionRef') + Class clazz = context.classLoader.loadClass('test.$Book$IntrospectionRef') BeanIntrospectionReference reference = (BeanIntrospectionReference) clazz.newInstance() expect: @@ -2089,7 +2089,7 @@ class Book { } } ''') - Class clazz = context.classLoader.loadClass('test.$Book$IntrospectionRef') + Class clazz = context.classLoader.loadClass('test.$Book$IntrospectionRef') BeanIntrospectionReference reference = (BeanIntrospectionReference) clazz.newInstance() expect: @@ -4416,7 +4416,7 @@ public record Foo(String name, String isSurname, boolean contains, Boolean purge @Replaces(BeanIntrospectionModule) @io.micronaut.context.annotation.Requires(property = "bean.introspection.test") static class StaticBeanIntrospectionModule extends BeanIntrospectionModule { - Map introspectionMap = [:] + Map, BeanIntrospection> introspectionMap = [:] @Override protected BeanIntrospection findIntrospection(Class beanClass) { return introspectionMap.get(beanClass) diff --git a/inject-java/src/main/java/io/micronaut/annotation/processing/JavaAnnotationMetadataBuilder.java b/inject-java/src/main/java/io/micronaut/annotation/processing/JavaAnnotationMetadataBuilder.java index ff175ef767a..0b68e03ad81 100644 --- a/inject-java/src/main/java/io/micronaut/annotation/processing/JavaAnnotationMetadataBuilder.java +++ b/inject-java/src/main/java/io/micronaut/annotation/processing/JavaAnnotationMetadataBuilder.java @@ -748,7 +748,7 @@ public Object visitArray(List { private List values = new ArrayList(); - private Class arrayType; + private Class arrayType; Object[] getValues() { if (arrayType != null) { diff --git a/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/LoadedVisitor.java b/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/LoadedVisitor.java index 104ac36b67c..98771517b90 100644 --- a/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/LoadedVisitor.java +++ b/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/LoadedVisitor.java @@ -71,15 +71,15 @@ public LoadedVisitor(TypeElementVisitor visitor, elementAnnotation = elementName; } } else { - Class[] classes = GenericTypeUtils.resolveInterfaceTypeArguments(aClass, TypeElementVisitor.class); + Class[] classes = GenericTypeUtils.resolveInterfaceTypeArguments(aClass, TypeElementVisitor.class); if (classes != null && classes.length == 2) { - Class classGeneric = classes[0]; + Class classGeneric = classes[0]; if (classGeneric == Object.class) { classAnnotation = visitor.getClassType(); } else { classAnnotation = classGeneric.getName(); } - Class elementGeneric = classes[1]; + Class elementGeneric = classes[1]; if (elementGeneric == Object.class) { elementAnnotation = visitor.getElementType(); } else { diff --git a/inject-java/src/test/groovy/io/micronaut/aop/compile/InheritedAnnotationMetadataSpec.groovy b/inject-java/src/test/groovy/io/micronaut/aop/compile/InheritedAnnotationMetadataSpec.groovy index 38c5edd3e97..aea10d2e54d 100644 --- a/inject-java/src/test/groovy/io/micronaut/aop/compile/InheritedAnnotationMetadataSpec.groovy +++ b/inject-java/src/test/groovy/io/micronaut/aop/compile/InheritedAnnotationMetadataSpec.groovy @@ -76,11 +76,11 @@ import io.micronaut.core.annotation.*; class MyBean implements MyInterface { private String myValue; - + MyBean(@Value("${foo.bar}") String val) { this.myValue = val; } - + @Override public String someMethod() { return myValue; @@ -144,7 +144,7 @@ class Service extends BaseService implements ContractService { @SomeAnnot public void serviceMethod() {} - + public void interfaceServiceMethod() {} } @@ -166,7 +166,7 @@ class SomeInterceptor implements MethodInterceptor, Ordered { ''') then: - Class clazz = ctx.classLoader.loadClass("test.ContractService") + Class clazz = ctx.classLoader.loadClass("test.ContractService") ctx.getBean(clazz) when: diff --git a/inject-java/src/test/groovy/io/micronaut/inject/annotation/Trace.java b/inject-java/src/test/groovy/io/micronaut/inject/annotation/Trace.java index a9bf3ec5acc..807967e1ae0 100644 --- a/inject-java/src/test/groovy/io/micronaut/inject/annotation/Trace.java +++ b/inject-java/src/test/groovy/io/micronaut/inject/annotation/Trace.java @@ -41,7 +41,7 @@ @AliasFor(annotation = Around.class, member = "hotswap") boolean something() default false; - Class type(); + Class type(); - Class[] types(); + Class[] types(); } diff --git a/inject-java/src/test/groovy/io/micronaut/inject/close/BeanCloseOrderSpec.groovy b/inject-java/src/test/groovy/io/micronaut/inject/close/BeanCloseOrderSpec.groovy index 5df199c5caf..0205fb96e68 100644 --- a/inject-java/src/test/groovy/io/micronaut/inject/close/BeanCloseOrderSpec.groovy +++ b/inject-java/src/test/groovy/io/micronaut/inject/close/BeanCloseOrderSpec.groovy @@ -20,7 +20,7 @@ import spock.lang.Specification class BeanCloseOrderSpec extends Specification { - static List closed = [] + static List> closed = [] void "test close order"() { given: diff --git a/inject-java/src/test/groovy/io/micronaut/inject/configuration/ConfigurationBuilderSpec.groovy b/inject-java/src/test/groovy/io/micronaut/inject/configuration/ConfigurationBuilderSpec.groovy index 2c2ef7e35c6..567c616b270 100644 --- a/inject-java/src/test/groovy/io/micronaut/inject/configuration/ConfigurationBuilderSpec.groovy +++ b/inject-java/src/test/groovy/io/micronaut/inject/configuration/ConfigurationBuilderSpec.groovy @@ -14,9 +14,9 @@ package test; import io.micronaut.context.annotation.*; import io.micronaut.inject.configuration.Engine; -@ConfigurationProperties("test.props") +@ConfigurationProperties("test.props") final class TestProps { - @ConfigurationBuilder(prefixes = "with") + @ConfigurationBuilder(prefixes = "with") private Engine.Builder builder = Engine.builder(); public final Engine.Builder getBuilder() { @@ -27,7 +27,7 @@ final class TestProps { ctx.getEnvironment().addPropertySource(PropertySource.of(["test.props.manufacturer": "Toyota"])) when: - Class testProps = ctx.classLoader.loadClass("test.TestProps") + Class testProps = ctx.classLoader.loadClass("test.TestProps") def testPropBean = ctx.getBean(testProps) then: @@ -44,9 +44,9 @@ package test; import io.micronaut.context.annotation.*; import io.micronaut.inject.configuration.Engine; -@ConfigurationProperties("test.props") +@ConfigurationProperties("test.props") final class TestProps { - @ConfigurationBuilder(prefixes = "with") + @ConfigurationBuilder(prefixes = "with") private Engine.Builder builder = Engine.builder(); } ''') @@ -66,14 +66,14 @@ package test; import io.micronaut.context.annotation.*; import io.micronaut.inject.configuration.Engine; -@ConfigurationProperties("test.props") -final class TestProps { +@ConfigurationProperties("test.props") +final class TestProps { Engine.Builder builder = Engine.builder(); - + Engine.Builder getBuilder() { return this.builder; } - + @ConfigurationBuilder(prefixes = "with") void setBuilder(Engine.Builder p0) { this.builder = p0; @@ -83,7 +83,7 @@ final class TestProps { ctx.getEnvironment().addPropertySource(PropertySource.of(["test.props.manufacturer": "Toyota"])) when: - Class testProps = ctx.classLoader.loadClass("test.TestProps") + Class testProps = ctx.classLoader.loadClass("test.TestProps") def testPropBean = ctx.getBean(testProps) then: @@ -101,59 +101,59 @@ package test; import io.micronaut.context.annotation.*; import io.micronaut.inject.configuration.AnnWithClass; -@ConfigurationProperties("pool") -final class PoolConfig { - +@ConfigurationProperties("pool") +final class PoolConfig { + @ConfigurationBuilder(prefixes = {""}) public ConnectionPool.Builder builder = DefaultConnectionPool.builder(); - + } interface ConnectionPool { - + interface Builder { Builder maxConcurrency(Integer maxConcurrency); Builder foo(Foo foo); ConnectionPool build(); } - + int getMaxConcurrency(); } class DefaultConnectionPool implements ConnectionPool { private final int maxConcurrency; - + DefaultConnectionPool(int maxConcurrency) { this.maxConcurrency = maxConcurrency; } - + public static ConnectionPool.Builder builder() { return new DefaultBuilder(); } - - @Override + + @Override public int getMaxConcurrency() { return maxConcurrency; } - + private static class DefaultBuilder implements ConnectionPool.Builder { - + private int maxConcurrency; - + private DefaultBuilder() { } - + @Override public ConnectionPool.Builder maxConcurrency(Integer maxConcurrency) { this.maxConcurrency = maxConcurrency; return this; } - + @Override public ConnectionPool.Builder foo(Foo foo) { return this; } - + public ConnectionPool build() { return new DefaultConnectionPool(maxConcurrency); } @@ -167,7 +167,7 @@ interface Foo { ctx.getEnvironment().addPropertySource(PropertySource.of(["pool.max-concurrency": 123])) when: - Class testProps = ctx.classLoader.loadClass("test.PoolConfig") + Class testProps = ctx.classLoader.loadClass("test.PoolConfig") def testPropBean = ctx.getBean(testProps) then: diff --git a/inject-java/src/test/groovy/io/micronaut/inject/field/inheritance/FieldInheritanceInjectionSpec.groovy b/inject-java/src/test/groovy/io/micronaut/inject/field/inheritance/FieldInheritanceInjectionSpec.groovy index ac9aed0cbed..9c046ee3844 100644 --- a/inject-java/src/test/groovy/io/micronaut/inject/field/inheritance/FieldInheritanceInjectionSpec.groovy +++ b/inject-java/src/test/groovy/io/micronaut/inject/field/inheritance/FieldInheritanceInjectionSpec.groovy @@ -37,7 +37,7 @@ class Listener extends AbstractListener { thrown(ClassNotFoundException) when: - Class clazz = context.classLoader.loadClass('test.Listener') + Class clazz = context.classLoader.loadClass('test.Listener') Object bean = context.getBean(clazz) then: diff --git a/inject-kotlin-test/build.gradle b/inject-kotlin-test/build.gradle index 842fefc0611..b462d648352 100644 --- a/inject-kotlin-test/build.gradle +++ b/inject-kotlin-test/build.gradle @@ -34,7 +34,7 @@ tasks.named("sourcesJar") { tasks.named("compileKotlin") { kotlinOptions { - jvmTarget = "1.8" + jvmTarget = "17" } } diff --git a/inject/src/main/java/io/micronaut/context/AbstractBeanDefinition.java b/inject/src/main/java/io/micronaut/context/AbstractBeanDefinition.java index 8ed92d76b02..0b8966e7ce7 100644 --- a/inject/src/main/java/io/micronaut/context/AbstractBeanDefinition.java +++ b/inject/src/main/java/io/micronaut/context/AbstractBeanDefinition.java @@ -513,8 +513,8 @@ protected final AbstractBeanDefinition addExecutableMethod(ExecutableMethod declaringType, + Class fieldType, String field, @Nullable AnnotationMetadata annotationMetadata, @Nullable Argument[] typeArguments, @@ -562,7 +562,7 @@ protected final AbstractBeanDefinition addInjectionPoint( @Internal @UsedByGeneratedCode protected final AbstractBeanDefinition addInjectionPoint( - Class declaringType, + Class declaringType, String method, @Nullable Argument[] arguments, @Nullable AnnotationMetadata annotationMetadata, @@ -888,7 +888,7 @@ protected final boolean containsValueForMethodArgument(BeanResolutionContext res String valueAnnStr = argument.getAnnotationMetadata().stringValue(Value.class).orElse(null); String valString = resolvePropertyValueName(resolutionContext, injectionPoint.getAnnotationMetadata(), argument, valueAnnStr); ApplicationContext applicationContext = (ApplicationContext) context; - Class type = argument.getType(); + Class type = argument.getType(); boolean isConfigProps = type.isAnnotationPresent(ConfigurationProperties.class); boolean result = isConfigProps || Map.class.isAssignableFrom(type) || Collection.class.isAssignableFrom(type) ? applicationContext.containsProperties(valString) : applicationContext.containsProperty(valString); if (!result && isConfigurationProperties()) { @@ -1622,7 +1622,7 @@ protected final boolean containsValueForField(BeanResolutionContext resolutionCo String valueAnnVal = annotationMetadata.stringValue(Value.class).orElse(null); String valString = resolvePropertyValueName(resolutionContext, injectionPoint, valueAnnVal, annotationMetadata); ApplicationContext applicationContext = (ApplicationContext) context; - Class fieldType = injectionPoint.getType(); + Class fieldType = injectionPoint.getType(); boolean isConfigProps = fieldType.isAnnotationPresent(ConfigurationProperties.class); boolean result = isConfigProps || Map.class.isAssignableFrom(fieldType) || Collection.class.isAssignableFrom(fieldType) ? applicationContext.containsProperties(valString) : applicationContext.containsProperty(valString); if (!result && isConfigurationProperties()) { @@ -1847,7 +1847,7 @@ private AnnotationMetadata initializeAnnotationMetadata() { } private AbstractBeanDefinition addInjectionPointInternal( - Class declaringType, + Class declaringType, String method, @Nullable Argument[] arguments, @Nullable AnnotationMetadata annotationMetadata, @@ -1883,7 +1883,7 @@ private AbstractBeanDefinition addInjectionPointInternal( } private Object getBeanForMethodArgument(BeanResolutionContext resolutionContext, BeanContext context, MethodInjectionPoint injectionPoint, Argument argument) { - Class argumentType = argument.getType(); + Class argumentType = argument.getType(); if (argumentType.isArray()) { Collection beansOfType = getBeansOfTypeForMethodArgument(resolutionContext, context, injectionPoint, argument); return beansOfType.toArray((Object[]) Array.newInstance(argumentType.getComponentType(), beansOfType.size())); @@ -2042,8 +2042,8 @@ private boolean isInnerConfiguration(Argument argumentType, BeanContext beanC } @SuppressWarnings("java:S1872") // internal requirement - private boolean isInnerOfAnySuperclass(Class argumentType) { - Class beanType = getBeanType(); + private boolean isInnerOfAnySuperclass(Class argumentType) { + Class beanType = getBeanType(); while (beanType != null) { if ((beanType.getName() + "$" + argumentType.getSimpleName()).equals(argumentType.getName())) { return true; @@ -2058,7 +2058,7 @@ private B resolveBeanWithGenericsFromMethodArgum path.pushMethodArgumentResolve(this, injectionPoint, argument); try { Qualifier qualifier = resolveQualifier(resolutionContext, argument); - Class argumentType = argument.getType(); + Class argumentType = argument.getType(); Argument genericType = resolveGenericType(argument, argumentType); @SuppressWarnings("unchecked") B bean = (B) beanResolver.resolveBean(genericType != null ? genericType : argument, qualifier); path.pop(); @@ -2068,7 +2068,7 @@ private B resolveBeanWithGenericsFromMethodArgum } } - private Argument resolveGenericType(Argument argument, Class argumentType) { + private Argument resolveGenericType(Argument argument, Class argumentType) { Argument genericType; if (argument.isArray()) { genericType = Argument.of(argumentType.getComponentType()); @@ -2083,7 +2083,7 @@ private B resolveBeanWithGenericsFromConstructorArgument(BeanResolutionConte BeanResolutionContext.Path path = resolutionContext.getPath(); path.pushConstructorResolve(this, argument); try { - Class argumentType = argument.getType(); + Class argumentType = argument.getType(); Argument genericType = resolveGenericType(argument, argumentType); Qualifier qualifier = resolveQualifier(resolutionContext, argument); @SuppressWarnings("unchecked") B bean = (B) beanResolver.resolveBean(genericType != null ? genericType : argument, qualifier); @@ -2312,9 +2312,9 @@ protected Environment getEnvironment() { */ private final class MethodKey { final String name; - final Class[] argumentTypes; + final Class[] argumentTypes; - MethodKey(String name, Class[] argumentTypes) { + MethodKey(String name, Class[] argumentTypes) { this.name = name; this.argumentTypes = argumentTypes; } diff --git a/inject/src/main/java/io/micronaut/context/AbstractExecutable.java b/inject/src/main/java/io/micronaut/context/AbstractExecutable.java index 04d348c606e..5d4e15b2202 100644 --- a/inject/src/main/java/io/micronaut/context/AbstractExecutable.java +++ b/inject/src/main/java/io/micronaut/context/AbstractExecutable.java @@ -36,9 +36,9 @@ @Internal abstract class AbstractExecutable implements Executable { - protected final Class declaringType; + protected final Class declaringType; protected final String methodName; - protected final Class[] argTypes; + protected final Class[] argTypes; private Argument[] arguments; private Method method; diff --git a/inject/src/main/java/io/micronaut/context/AbstractExecutableMethod.java b/inject/src/main/java/io/micronaut/context/AbstractExecutableMethod.java index 4fbce9f4a3c..ae259f09045 100644 --- a/inject/src/main/java/io/micronaut/context/AbstractExecutableMethod.java +++ b/inject/src/main/java/io/micronaut/context/AbstractExecutableMethod.java @@ -142,12 +142,12 @@ public ReturnType getReturnType() { } @Override - public Class[] getArgumentTypes() { + public Class[] getArgumentTypes() { return argTypes; } @Override - public Class getDeclaringType() { + public Class getDeclaringType() { return declaringType; } diff --git a/inject/src/main/java/io/micronaut/context/AbstractExecutableMethodsDefinition.java b/inject/src/main/java/io/micronaut/context/AbstractExecutableMethodsDefinition.java index 8c5d5c3c26d..70c2858dbb2 100644 --- a/inject/src/main/java/io/micronaut/context/AbstractExecutableMethodsDefinition.java +++ b/inject/src/main/java/io/micronaut/context/AbstractExecutableMethodsDefinition.java @@ -206,7 +206,7 @@ protected final RuntimeException unknownDispatchAtIndexException(int index) { * @return true if matches */ @UsedByGeneratedCode - protected final boolean methodAtIndexMatches(int index, String name, Class[] argumentTypes) { + protected final boolean methodAtIndexMatches(int index, String name, Class[] argumentTypes) { MethodReference methodReference = methodsReferences[index]; Argument[] arguments = methodReference.arguments; if (arguments.length != argumentTypes.length || !methodReference.methodName.equals(name)) { @@ -215,7 +215,7 @@ protected final boolean methodAtIndexMatches(int index, String name, Class[] arg return argumentsTypesMatch(argumentTypes, arguments); } - private boolean argumentsTypesMatch(Class[] argumentTypes, Argument[] arguments) { + private boolean argumentsTypesMatch(Class[] argumentTypes, Argument[] arguments) { for (int i = 0; i < arguments.length; i++) { if (!argumentTypes[i].equals(arguments[i].getType())) { return false; diff --git a/inject/src/main/java/io/micronaut/context/AbstractInitializableBeanDefinition.java b/inject/src/main/java/io/micronaut/context/AbstractInitializableBeanDefinition.java index 6018cc4df7f..0312de44c13 100644 --- a/inject/src/main/java/io/micronaut/context/AbstractInitializableBeanDefinition.java +++ b/inject/src/main/java/io/micronaut/context/AbstractInitializableBeanDefinition.java @@ -345,7 +345,7 @@ public int hashCode() { @Override public String toString() { - Class declaringType = constructor == null ? type : constructor.declaringType; + Class declaringType = constructor == null ? type : constructor.declaringType; return "Definition: " + declaringType.getName(); } @@ -2366,7 +2366,7 @@ public abstract static class MethodOrFieldReference { final Class declaringType; final boolean requiresReflection; - public MethodOrFieldReference(Class declaringType, boolean requiresReflection) { + public MethodOrFieldReference(Class declaringType, boolean requiresReflection) { this.declaringType = declaringType; this.requiresReflection = requiresReflection; } diff --git a/inject/src/main/java/io/micronaut/context/ApplicationContext.java b/inject/src/main/java/io/micronaut/context/ApplicationContext.java index 4b965d04521..5f3097d1e53 100644 --- a/inject/src/main/java/io/micronaut/context/ApplicationContext.java +++ b/inject/src/main/java/io/micronaut/context/ApplicationContext.java @@ -313,7 +313,7 @@ public interface ApplicationContext extends BeanContext, PropertyResolver, Prope * @param environments The environment to use * @return The application context builder */ - static @NonNull ApplicationContextBuilder builder(@NonNull Class mainClass, @NonNull String... environments) { + static @NonNull ApplicationContextBuilder builder(@NonNull Class mainClass, @NonNull String... environments) { ArgumentUtils.requireNonNull("environments", environments); ArgumentUtils.requireNonNull("mainClass", mainClass); diff --git a/inject/src/main/java/io/micronaut/context/DefaultBeanContext.java b/inject/src/main/java/io/micronaut/context/DefaultBeanContext.java index d80aaf445ff..5062dad2c5e 100644 --- a/inject/src/main/java/io/micronaut/context/DefaultBeanContext.java +++ b/inject/src/main/java/io/micronaut/context/DefaultBeanContext.java @@ -202,10 +202,10 @@ public > Stream reduce(Class beanType, S private final Map> beanCandidateCache = new ConcurrentLinkedHashMap.Builder>().maximumWeightedCapacity(30).build(); - private final Map> beanIndex = new ConcurrentHashMap<>(12); + private final Map, Collection> beanIndex = new ConcurrentHashMap<>(12); private final ClassLoader classLoader; - private final Set thisInterfaces = CollectionUtils.setOf( + private final Set> thisInterfaces = CollectionUtils.setOf( BeanDefinitionRegistry.class, BeanContext.class, AnnotationMetadataResolver.class, @@ -216,7 +216,7 @@ public > Stream reduce(Class beanType, S ValueResolver.class, PropertyPlaceholderResolver.class ); - private final Set indexedTypes = CollectionUtils.setOf( + private final Set> indexedTypes = CollectionUtils.setOf( ResourceLoader.class, TypeConverter.class, TypeConverterRegistrar.class, @@ -541,7 +541,7 @@ public Optional> findBeanRegistration(T bean) { } @Override - public Optional> findExecutionHandle(Class beanType, String method, Class... arguments) { + public Optional> findExecutionHandle(Class beanType, String method, Class... arguments) { return findExecutionHandle(beanType, null, method, arguments); } @@ -612,7 +612,7 @@ public ExecutableMethod getExecutableMethod() { @SuppressWarnings("unchecked") @Override - public Optional> findExecutionHandle(Class beanType, Qualifier qualifier, String method, Class... arguments) { + public Optional> findExecutionHandle(Class beanType, Qualifier qualifier, String method, Class... arguments) { Optional> foundBean = findBeanDefinition(beanType, (Qualifier) qualifier); if (foundBean.isPresent()) { BeanDefinition beanDefinition = foundBean.get(); @@ -625,7 +625,7 @@ public Optional> findExecutionHandle(Class return beanDefinition.findPossibleMethods(method) .findFirst() .filter(m -> { - Class[] argTypes = m.getArgumentTypes(); + Class[] argTypes = m.getArgumentTypes(); if (argTypes.length == arguments.length) { for (int i = 0; i < argTypes.length; i++) { if (!arguments[i].isAssignableFrom(argTypes[i])) { @@ -643,7 +643,7 @@ public Optional> findExecutionHandle(Class } @Override - public Optional> findExecutableMethod(Class beanType, String method, Class[] arguments) { + public Optional> findExecutableMethod(Class beanType, String method, Class[] arguments) { if (beanType != null) { Collection> definitions = getBeanDefinitions(beanType); if (!definitions.isEmpty()) { @@ -662,7 +662,7 @@ public Optional> findExecutableMethod(Class bea @SuppressWarnings("unchecked") @Override - public Optional> findExecutionHandle(T bean, String method, Class[] arguments) { + public Optional> findExecutionHandle(T bean, String method, Class[] arguments) { if (bean != null) { Optional> foundBean = findBeanDefinition(bean.getClass()); if (foundBean.isPresent()) { @@ -731,7 +731,7 @@ public BeanDefinition load() { } @Override - public Class getBeanType() { + public Class getBeanType() { return type; } }); @@ -1438,7 +1438,7 @@ public T getProxyTargetBean(@Nullable BeanResolutionContext resolutionContex @NonNull @Override - public Optional> findProxyTargetMethod(@NonNull Class beanType, @NonNull String method, @NonNull Class[] arguments) { + public Optional> findProxyTargetMethod(@NonNull Class beanType, @NonNull String method, @NonNull Class[] arguments) { ArgumentUtils.requireNonNull("beanType", beanType); ArgumentUtils.requireNonNull("method", method); BeanDefinition definition = getProxyTargetBeanDefinition(beanType, null); @@ -1447,7 +1447,7 @@ public Optional> findProxyTargetMethod(@NonNull Cl @NonNull @Override - public Optional> findProxyTargetMethod(@NonNull Class beanType, Qualifier qualifier, @NonNull String method, Class... arguments) { + public Optional> findProxyTargetMethod(@NonNull Class beanType, Qualifier qualifier, @NonNull String method, Class... arguments) { ArgumentUtils.requireNonNull("beanType", beanType); ArgumentUtils.requireNonNull("method", method); BeanDefinition definition = getProxyTargetBeanDefinition(beanType, qualifier); @@ -1455,7 +1455,7 @@ public Optional> findProxyTargetMethod(@NonNull Cl } @Override - public Optional> findProxyTargetMethod(@NonNull Argument beanType, Qualifier qualifier, @NonNull String method, Class... arguments) { + public Optional> findProxyTargetMethod(@NonNull Argument beanType, Qualifier qualifier, @NonNull String method, Class... arguments) { ArgumentUtils.requireNonNull("beanType", beanType); ArgumentUtils.requireNonNull("method", method); BeanDefinition definition = getProxyTargetBeanDefinition(beanType, qualifier); @@ -3170,7 +3170,7 @@ public boolean test(BeanDefinition candidate) { private void filterProxiedTypes(Collection> candidates, boolean filterProxied, boolean filterDelegates, Predicate> predicate) { int count = candidates.size(); - Set proxiedTypes = new HashSet<>(count); + Set> proxiedTypes = new HashSet<>(count); Iterator> i = candidates.iterator(); Collection> delegates = filterDelegates ? new ArrayList<>(count) : Collections.emptyList(); while (i.hasNext()) { @@ -3304,16 +3304,16 @@ private void readAllBeanDefinitionClasses() { } } final AnnotationMetadata annotationMetadata = beanDefinitionReference.getAnnotationMetadata(); - Class[] indexes = annotationMetadata.classValues(INDEXES_TYPE); + Class[] indexes = annotationMetadata.classValues(INDEXES_TYPE); if (indexes.length > 0) { //noinspection ForLoopReplaceableByForEach for (int i = 0; i < indexes.length; i++) { - Class indexedType = indexes[i]; + Class indexedType = indexes[i]; resolveTypeIndex(indexedType).add(beanDefinitionReference); } } else { if (annotationMetadata.hasStereotype(ADAPTER_TYPE)) { - final Class aClass = annotationMetadata.classValue(ADAPTER_TYPE, AnnotationMetadata.VALUE_MEMBER).orElse(null); + final Class aClass = annotationMetadata.classValue(ADAPTER_TYPE, AnnotationMetadata.VALUE_MEMBER).orElse(null); if (indexedTypes.contains(aClass)) { resolveTypeIndex(aClass).add(beanDefinitionReference); } @@ -3584,11 +3584,11 @@ private List topologicalSort(Collection bean List sorted = new ArrayList<>(nullSafe(initial.get(true))); List unsorted = new ArrayList<>(nullSafe(initial.get(false))); // Optimization which knows about types which are already in the sorted list - Set satisfied = new HashSet<>(); + Set> satisfied = new HashSet<>(); // Optimization for types which we know are already unsatisified // in a single iteration, allowing to skip the loop on unsorted elements - Set unsatisfied = new HashSet<>(); + Set> unsatisfied = new HashSet<>(); //loop until all items have been sorted while (!unsorted.isEmpty()) { @@ -3601,7 +3601,7 @@ private List topologicalSort(Collection bean boolean found = false; //determine if any components are in the unsorted list - Collection components = bean.getBeanDefinition().getRequiredComponents(); + Collection> components = bean.getBeanDefinition().getRequiredComponents(); for (Class clazz : components) { if (satisfied.contains(clazz)) { continue; @@ -3887,7 +3887,7 @@ static final class BeanKey implements BeanIdentifier { * @param qualifier The qualifier * @param typeArguments The type arguments */ - BeanKey(Class beanType, Qualifier qualifier, @Nullable Class... typeArguments) { + BeanKey(Class beanType, Qualifier qualifier, @Nullable Class... typeArguments) { this(Argument.of(beanType, typeArguments), qualifier); } diff --git a/inject/src/main/java/io/micronaut/context/DefaultConstructorInjectionPoint.java b/inject/src/main/java/io/micronaut/context/DefaultConstructorInjectionPoint.java index e4dffec81c8..eed3f5bea67 100644 --- a/inject/src/main/java/io/micronaut/context/DefaultConstructorInjectionPoint.java +++ b/inject/src/main/java/io/micronaut/context/DefaultConstructorInjectionPoint.java @@ -45,7 +45,7 @@ class DefaultConstructorInjectionPoint implements ConstructorInjectionPoint declaringBean; private final Class declaringType; - private final Class[] argTypes; + private final Class[] argTypes; private final AnnotationMetadata annotationMetadata; private final Argument[] arguments; diff --git a/inject/src/main/java/io/micronaut/context/DefaultMethodInjectionPoint.java b/inject/src/main/java/io/micronaut/context/DefaultMethodInjectionPoint.java index 71fbdd68064..b09ad2f0365 100644 --- a/inject/src/main/java/io/micronaut/context/DefaultMethodInjectionPoint.java +++ b/inject/src/main/java/io/micronaut/context/DefaultMethodInjectionPoint.java @@ -45,7 +45,7 @@ class DefaultMethodInjectionPoint implements MethodInjectionPoint, E private final AnnotationMetadata annotationMetadata; private final Class declaringType; private final String methodName; - private final Class[] argTypes; + private final Class[] argTypes; private final Argument[] arguments; private Environment environment; diff --git a/inject/src/main/java/io/micronaut/context/ExecutionHandleLocator.java b/inject/src/main/java/io/micronaut/context/ExecutionHandleLocator.java index 24460958bb4..a1b25b10da6 100644 --- a/inject/src/main/java/io/micronaut/context/ExecutionHandleLocator.java +++ b/inject/src/main/java/io/micronaut/context/ExecutionHandleLocator.java @@ -50,7 +50,7 @@ public interface ExecutionHandleLocator { * @param arguments The arguments * @return The execution handle */ - default Optional> findExecutionHandle(Class beanType, String method, Class... arguments) { + default Optional> findExecutionHandle(Class beanType, String method, Class... arguments) { return Optional.empty(); } @@ -66,7 +66,7 @@ default Optional> findExecutionHandle(Class Optional> findExecutionHandle(Class beanType, Qualifier qualifier, String method, Class... arguments) { + default Optional> findExecutionHandle(Class beanType, Qualifier qualifier, String method, Class... arguments) { return Optional.empty(); } @@ -81,7 +81,7 @@ default Optional> findExecutionHandle(Class Optional> findExecutionHandle(T bean, String method, Class... arguments) { + default Optional> findExecutionHandle(T bean, String method, Class... arguments) { return Optional.empty(); } @@ -96,7 +96,7 @@ default Optional> findExecutionHandle(T bean, * @param arguments The arguments * @return The execution handle */ - default Optional> findExecutableMethod(Class beanType, String method, Class... arguments) { + default Optional> findExecutableMethod(Class beanType, String method, Class... arguments) { return Optional.empty(); } @@ -110,7 +110,7 @@ default Optional> findExecutableMethod(Class be * @param arguments The arguments * @return The execution handle */ - default Optional> findProxyTargetMethod(Class beanType, String method, Class... arguments) { + default Optional> findProxyTargetMethod(Class beanType, String method, Class... arguments) { return Optional.empty(); } @@ -125,7 +125,7 @@ default Optional> findProxyTargetMethod(Class b * @param arguments The arguments * @return The execution handle */ - default Optional> findProxyTargetMethod(Class beanType, Qualifier qualifier, String method, Class... arguments) { + default Optional> findProxyTargetMethod(Class beanType, Qualifier qualifier, String method, Class... arguments) { return Optional.empty(); } @@ -140,7 +140,7 @@ default Optional> findProxyTargetMethod(Class b * @param arguments The arguments * @return The execution handle */ - default Optional> findProxyTargetMethod(Argument beanType, Qualifier qualifier, String method, Class... arguments) { + default Optional> findProxyTargetMethod(Argument beanType, Qualifier qualifier, String method, Class... arguments) { return Optional.empty(); } @@ -156,7 +156,7 @@ default Optional> findProxyTargetMethod(Argument ExecutableMethod getExecutableMethod(Class beanType, String method, Class... arguments) throws NoSuchMethodException { + default ExecutableMethod getExecutableMethod(Class beanType, String method, Class... arguments) throws NoSuchMethodException { Optional> executableMethod = this.findExecutableMethod(beanType, method, arguments); return executableMethod.orElseThrow(() -> new NoSuchMethodException("No such method [" + method + "(" + Arrays.stream(arguments).map(Class::getName).collect(Collectors.joining(",")) + ") ] for bean [" + beanType.getName() + "]") @@ -176,7 +176,7 @@ default ExecutableMethod getExecutableMethod(Class beanType, Str * @throws NoSuchMethodException if the method cannot be found */ @UsedByGeneratedCode - default ExecutableMethod getProxyTargetMethod(Class beanType, String method, Class... arguments) throws NoSuchMethodException { + default ExecutableMethod getProxyTargetMethod(Class beanType, String method, Class... arguments) throws NoSuchMethodException { Optional> executableMethod = this.findProxyTargetMethod(beanType, method, arguments); return executableMethod.orElseThrow(() -> new NoSuchMethodException("No such method [" + method + "(" + Arrays.stream(arguments).map(Class::getName).collect(Collectors.joining(",")) + ") ] for bean [" + beanType.getName() + "]") @@ -197,7 +197,7 @@ default ExecutableMethod getProxyTargetMethod(Class beanType, St * @throws NoSuchMethodException if the method cannot be found */ @UsedByGeneratedCode - default ExecutableMethod getProxyTargetMethod(Class beanType, Qualifier qualifier, String method, Class... arguments) throws NoSuchMethodException { + default ExecutableMethod getProxyTargetMethod(Class beanType, Qualifier qualifier, String method, Class... arguments) throws NoSuchMethodException { Optional> executableMethod = this.findProxyTargetMethod(beanType, qualifier, method, arguments); return executableMethod.orElseThrow(() -> new NoSuchMethodException("No such method [" + method + "(" + Arrays.stream(arguments).map(Class::getName).collect(Collectors.joining(",")) + ") ] for bean [" + beanType.getName() + "]") @@ -219,7 +219,7 @@ default ExecutableMethod getProxyTargetMethod(Class beanType, Qu * @since 3.0.0 */ @UsedByGeneratedCode - default ExecutableMethod getProxyTargetMethod(Argument beanType, Qualifier qualifier, String method, Class... arguments) throws NoSuchMethodException { + default ExecutableMethod getProxyTargetMethod(Argument beanType, Qualifier qualifier, String method, Class... arguments) throws NoSuchMethodException { Optional> executableMethod = this.findProxyTargetMethod(beanType, qualifier, method, arguments); return executableMethod.orElseThrow(() -> new NoSuchMethodException("No such method [" + method + "(" + Arrays.stream(arguments).map(Class::getName).collect(Collectors.joining(",")) + ") ] for bean [" + beanType.getName() + "]") @@ -238,7 +238,7 @@ default ExecutableMethod getProxyTargetMethod(Argument beanType, * @return The execution handle * @throws NoSuchMethodException if the method cannot be found */ - default MethodExecutionHandle getExecutionHandle(Class beanType, String method, Class... arguments) throws NoSuchMethodException { + default MethodExecutionHandle getExecutionHandle(Class beanType, String method, Class... arguments) throws NoSuchMethodException { return this.findExecutionHandle(beanType, method, arguments).orElseThrow(() -> new NoSuchMethodException("No such method [" + method + "(" + Arrays.stream(arguments).map(Class::getName).collect(Collectors.joining(",")) + ") ] for bean [" + beanType.getName() + "]") ); @@ -255,7 +255,7 @@ default MethodExecutionHandle getExecutionHandle(Class beanType, * @return The execution handle * @throws NoSuchMethodException if the method cannot be found */ - default MethodExecutionHandle getExecutionHandle(T bean, String method, Class... arguments) throws NoSuchMethodException { + default MethodExecutionHandle getExecutionHandle(T bean, String method, Class... arguments) throws NoSuchMethodException { return this.findExecutionHandle(bean, method, arguments).orElseThrow(() -> new NoSuchMethodException("No such method [" + method + "(" + Arrays.stream(arguments).map(Class::getName).collect(Collectors.joining(",")) + ") ] for bean [" + bean + "]") ); diff --git a/inject/src/main/java/io/micronaut/context/MissingMethodInjectionPoint.java b/inject/src/main/java/io/micronaut/context/MissingMethodInjectionPoint.java index 19d516de921..99bb48467c2 100644 --- a/inject/src/main/java/io/micronaut/context/MissingMethodInjectionPoint.java +++ b/inject/src/main/java/io/micronaut/context/MissingMethodInjectionPoint.java @@ -58,7 +58,7 @@ class MissingMethodInjectionPoint implements MethodInjectionPoint { @Override public Method getMethod() { - Class[] types = Arrays.stream(argTypes).map(Argument::getType).toArray(Class[]::new); + Class[] types = Arrays.stream(argTypes).map(Argument::getType).toArray(Class[]::new); throw ReflectionUtils.newNoSuchMethodError(declaringType, methodName, types); } @@ -79,7 +79,7 @@ public boolean isPostConstructMethod() { @Override public Object invoke(Object instance, Object... args) { - Class[] types = Arrays.stream(argTypes).map(Argument::getType).toArray(Class[]::new); + Class[] types = Arrays.stream(argTypes).map(Argument::getType).toArray(Class[]::new); throw ReflectionUtils.newNoSuchMethodError(declaringType, methodName, types); } diff --git a/inject/src/main/java/io/micronaut/context/NoInjectionBeanDefinition.java b/inject/src/main/java/io/micronaut/context/NoInjectionBeanDefinition.java index 8ae0b870573..400763f7a2a 100644 --- a/inject/src/main/java/io/micronaut/context/NoInjectionBeanDefinition.java +++ b/inject/src/main/java/io/micronaut/context/NoInjectionBeanDefinition.java @@ -88,8 +88,8 @@ public Optional getScopeName() { public List> getTypeArguments(Class type) { List> result = typeArguments.get(type); if (result == null) { - Class[] classes = type.isInterface() ? GenericTypeUtils.resolveInterfaceTypeArguments(singletonClass, type) : GenericTypeUtils.resolveSuperTypeGenericArguments(singletonClass, type); - result = Arrays.stream(classes).map((Function>) Argument::of).collect(Collectors.toList()); + Class[] classes = type.isInterface() ? GenericTypeUtils.resolveInterfaceTypeArguments(singletonClass, type) : GenericTypeUtils.resolveSuperTypeGenericArguments(singletonClass, type); + result = Arrays.stream(classes).map((Function, Argument>) Argument::of).collect(Collectors.toList()); typeArguments.put(type, result); } diff --git a/inject/src/main/java/io/micronaut/context/RequiresCondition.java b/inject/src/main/java/io/micronaut/context/RequiresCondition.java index c72a104394a..b0d44b639e2 100644 --- a/inject/src/main/java/io/micronaut/context/RequiresCondition.java +++ b/inject/src/main/java/io/micronaut/context/RequiresCondition.java @@ -564,7 +564,7 @@ private boolean matchesPresenceOfEntities(ConditionContext context, AnnotationVa private boolean matchesPresenceOfBeans(ConditionContext context, AnnotationValue requirements) { if (requirements.contains(MEMBER_BEANS) || requirements.contains(MEMBER_BEAN)) { - Class[] beans = requirements.classValues(MEMBER_BEANS); + Class[] beans = requirements.classValues(MEMBER_BEANS); if (requirements.contains(MEMBER_BEAN)) { Class memberBean = requirements.classValue(MEMBER_BEAN).orElse(null); if (memberBean != null) { @@ -586,7 +586,7 @@ private boolean matchesPresenceOfBeans(ConditionContext context, AnnotationValue private boolean matchesAbsenceOfBeans(ConditionContext context, AnnotationValue requirements) { if (requirements.contains(MEMBER_MISSING_BEANS)) { - Class[] missingBeans = requirements.classValues(MEMBER_MISSING_BEANS); + Class[] missingBeans = requirements.classValues(MEMBER_MISSING_BEANS); AnnotationMetadataProvider component = context.getComponent(); if (ArrayUtils.isNotEmpty(missingBeans) && component instanceof BeanDefinition) { BeanDefinition bd = (BeanDefinition) component; diff --git a/inject/src/main/java/io/micronaut/context/annotation/DefaultImplementation.java b/inject/src/main/java/io/micronaut/context/annotation/DefaultImplementation.java index 5af03b89b20..225af09aab7 100644 --- a/inject/src/main/java/io/micronaut/context/annotation/DefaultImplementation.java +++ b/inject/src/main/java/io/micronaut/context/annotation/DefaultImplementation.java @@ -66,7 +66,7 @@ /** * @return The bean type that is the default implementation */ - Class value() default void.class; + Class value() default void.class; /** * @return The fully qualified bean type name that is the default implementation diff --git a/inject/src/main/java/io/micronaut/context/annotation/EachBean.java b/inject/src/main/java/io/micronaut/context/annotation/EachBean.java index 6745dd6de2c..509a1a40dbf 100644 --- a/inject/src/main/java/io/micronaut/context/annotation/EachBean.java +++ b/inject/src/main/java/io/micronaut/context/annotation/EachBean.java @@ -63,5 +63,5 @@ /** * @return The bean type that this bean is driven by */ - Class value(); + Class value(); } diff --git a/inject/src/main/java/io/micronaut/context/annotation/Replaces.java b/inject/src/main/java/io/micronaut/context/annotation/Replaces.java index 4d06beab092..9d80aa621c7 100644 --- a/inject/src/main/java/io/micronaut/context/annotation/Replaces.java +++ b/inject/src/main/java/io/micronaut/context/annotation/Replaces.java @@ -36,18 +36,18 @@ * @return The bean type that this bean replaces */ @AliasFor(member = "bean") - Class value() default void.class; + Class value() default void.class; /** * @return The bean type that this bean replaces */ @AliasFor(member = "value") - Class bean() default void.class; + Class bean() default void.class; /** * @return The declaring bean type */ - Class factory() default void.class; + Class factory() default void.class; /** * The name of the qualifiers of the bean that should be replaced. diff --git a/inject/src/main/java/io/micronaut/context/annotation/Requires.java b/inject/src/main/java/io/micronaut/context/annotation/Requires.java index 38c32dad101..afd142be1fe 100644 --- a/inject/src/main/java/io/micronaut/context/annotation/Requires.java +++ b/inject/src/main/java/io/micronaut/context/annotation/Requires.java @@ -123,7 +123,7 @@ * * @return The classes */ - Class[] classes() default {}; + Class[] classes() default {}; /** * Expresses that the configuration requires entities annotated with the given annotations to be available to the @@ -139,14 +139,14 @@ * * @return The beans */ - Class[] beans() default {}; + Class[] beans() default {}; /** * Expresses the given classes that should be missing from the classpath for the bean or configuration to load. * * @return The classes */ - Class[] missing() default {}; + Class[] missing() default {}; /** * Expresses the given class names should be missing from the classpath for the bean configuration to load. @@ -211,7 +211,7 @@ * @return The configuration properties class * @since 3.4.0 */ - Class bean() default void.class; + Class bean() default void.class; /** * Used in combination with {@link #bean()} to diff --git a/inject/src/main/java/io/micronaut/context/converters/ContextConverterRegistrar.java b/inject/src/main/java/io/micronaut/context/converters/ContextConverterRegistrar.java index e05b3fa4ff3..1125cd2dab4 100644 --- a/inject/src/main/java/io/micronaut/context/converters/ContextConverterRegistrar.java +++ b/inject/src/main/java/io/micronaut/context/converters/ContextConverterRegistrar.java @@ -38,7 +38,7 @@ public class ContextConverterRegistrar implements TypeConverterRegistrar { private final BeanContext beanContext; - private final Map classCache = new ConcurrentHashMap<>(10); + private final Map> classCache = new ConcurrentHashMap<>(10); /** * Default constructor. @@ -51,7 +51,7 @@ public class ContextConverterRegistrar implements TypeConverterRegistrar { @Override public void register(MutableConversionService conversionService) { conversionService.addConverter(String[].class, Class[].class, (object, targetType, context) -> { - Class[] classes = Arrays + Class[] classes = Arrays .stream(object) .map(str -> conversionService.convert(str, Class.class)) .filter(Optional::isPresent) @@ -62,7 +62,7 @@ public void register(MutableConversionService conversionService) { }); conversionService.addConverter(String.class, Class.class, (object, targetType, context) -> { - final Class result = + final Class result = classCache.computeIfAbsent(object, s -> ClassUtils.forName(s, beanContext.getClassLoader()).orElse(MissingClass.class)); if (result == MissingClass.class) { return Optional.empty(); diff --git a/inject/src/main/java/io/micronaut/inject/BeanDefinition.java b/inject/src/main/java/io/micronaut/inject/BeanDefinition.java index a3f7a023979..64330f86c6f 100644 --- a/inject/src/main/java/io/micronaut/inject/BeanDefinition.java +++ b/inject/src/main/java/io/micronaut/inject/BeanDefinition.java @@ -370,7 +370,7 @@ default boolean isProxy() { if (typeArguments.isEmpty()) { return ReflectionUtils.EMPTY_CLASS_ARRAY; } - Class[] params = new Class[typeArguments.size()]; + Class[] params = new Class[typeArguments.size()]; int i = 0; for (Argument argument : typeArguments) { params[i++] = argument.getType(); diff --git a/inject/src/main/java/io/micronaut/inject/DelegatingExecutableMethod.java b/inject/src/main/java/io/micronaut/inject/DelegatingExecutableMethod.java index 8758a48d040..3d54d37097b 100644 --- a/inject/src/main/java/io/micronaut/inject/DelegatingExecutableMethod.java +++ b/inject/src/main/java/io/micronaut/inject/DelegatingExecutableMethod.java @@ -57,7 +57,7 @@ default String getMethodName() { } @Override - default Class[] getArgumentTypes() { + default Class[] getArgumentTypes() { return getTarget().getArgumentTypes(); } diff --git a/inject/src/main/java/io/micronaut/inject/ExecutionHandle.java b/inject/src/main/java/io/micronaut/inject/ExecutionHandle.java index 5fe4f230d5f..bf1776287df 100644 --- a/inject/src/main/java/io/micronaut/inject/ExecutionHandle.java +++ b/inject/src/main/java/io/micronaut/inject/ExecutionHandle.java @@ -47,7 +47,7 @@ public interface ExecutionHandle extends AnnotationMetadataDelegate { /** * @return The declaring type */ - Class getDeclaringType(); + Class getDeclaringType(); /** * @return The required argument types. @@ -85,8 +85,8 @@ public T2 getTarget() { } @Override - public Class getDeclaringType() { - return bean.getClass(); + public Class getDeclaringType() { + return (Class) bean.getClass(); } @Override diff --git a/inject/src/main/java/io/micronaut/inject/MethodReference.java b/inject/src/main/java/io/micronaut/inject/MethodReference.java index 0116de9a028..d69e76e65b8 100644 --- a/inject/src/main/java/io/micronaut/inject/MethodReference.java +++ b/inject/src/main/java/io/micronaut/inject/MethodReference.java @@ -62,7 +62,7 @@ public interface MethodReference extends AnnotationMetadataDelegate, Annot /** * @return The argument types */ - default Class[] getArgumentTypes() { + default Class[] getArgumentTypes() { return Arrays .stream(getArguments()) .map(Argument::getType) diff --git a/inject/src/main/java/io/micronaut/inject/annotation/DefaultAnnotationMetadata.java b/inject/src/main/java/io/micronaut/inject/annotation/DefaultAnnotationMetadata.java index a0b62776273..eaf57a12afb 100644 --- a/inject/src/main/java/io/micronaut/inject/annotation/DefaultAnnotationMetadata.java +++ b/inject/src/main/java/io/micronaut/inject/annotation/DefaultAnnotationMetadata.java @@ -85,7 +85,7 @@ public class DefaultAnnotationMetadata extends AbstractAnnotationMetadata implem @Nullable Map> annotationDefaultValues; @Nullable - Map repeated = null; + Map repeated; @Nullable Set sourceRetentionAnnotations; private Map annotationValuesByType = new ConcurrentHashMap<>(2); @@ -365,7 +365,7 @@ public Class[] classValues(@NonNull String annotation, @NonNull String me return classes; } //noinspection unchecked - return ReflectionUtils.EMPTY_CLASS_ARRAY; + return (Class[]) ReflectionUtils.EMPTY_CLASS_ARRAY; } @Override @@ -379,7 +379,8 @@ public Class[] classValues(@NonNull Class annotatio Class[] classes = ((AnnotationValue) v).classValues(member); return (Class[]) classes; } - return ReflectionUtils.EMPTY_CLASS_ARRAY; + //noinspection unchecked + return (Class[]) ReflectionUtils.EMPTY_CLASS_ARRAY; } else { return classValues(annotation.getName(), member); } diff --git a/inject/src/main/java/io/micronaut/inject/qualifiers/ClosestTypeArgumentQualifier.java b/inject/src/main/java/io/micronaut/inject/qualifiers/ClosestTypeArgumentQualifier.java index 1b8113312de..f7577669536 100644 --- a/inject/src/main/java/io/micronaut/inject/qualifiers/ClosestTypeArgumentQualifier.java +++ b/inject/src/main/java/io/micronaut/inject/qualifiers/ClosestTypeArgumentQualifier.java @@ -38,12 +38,12 @@ public class ClosestTypeArgumentQualifier extends TypeArgumentQualifier { private static final Logger LOG = ClassUtils.getLogger(ClosestTypeArgumentQualifier.class); - private final List[] hierarchies; + private final List>[] hierarchies; /** * @param typeArguments The type arguments */ - ClosestTypeArgumentQualifier(Class... typeArguments) { + ClosestTypeArgumentQualifier(Class... typeArguments) { super(typeArguments); this.hierarchies = new List[typeArguments.length]; for (int i = 0 ; i < typeArguments.length; i++) { @@ -56,7 +56,7 @@ public > Stream reduce(Class beanType, Stream return candidates .filter(candidate -> beanType.isAssignableFrom(candidate.getBeanType())) .map(candidate -> { - List typeArguments = getTypeArguments(beanType, candidate); + List> typeArguments = getTypeArguments(beanType, candidate); int result = compare(typeArguments); if (LOG.isTraceEnabled() && result < 0) { @@ -77,8 +77,8 @@ public > Stream reduce(Class beanType, Stream * @param classesToCompare An array of classes * @return Whether the types are compatible */ - protected int compare(List classesToCompare) { - final Class[] typeArguments = getTypeArguments(); + protected int compare(List> classesToCompare) { + final Class[] typeArguments = getTypeArguments(); if (classesToCompare.isEmpty() && typeArguments.length == 0) { return 0; } else if (classesToCompare.size() != typeArguments.length) { @@ -89,8 +89,8 @@ protected int compare(List classesToCompare) { if (typeArguments[i] == Object.class) { continue; } - Class left = classesToCompare.get(i); - List hierarchy = hierarchies[i]; + Class left = classesToCompare.get(i); + List> hierarchy = hierarchies[i]; int index = hierarchy.indexOf(left); if (index == -1) { comparison = -1; diff --git a/inject/src/main/java/io/micronaut/inject/qualifiers/Qualifiers.java b/inject/src/main/java/io/micronaut/inject/qualifiers/Qualifiers.java index 564d77eaae2..e091e408ff5 100644 --- a/inject/src/main/java/io/micronaut/inject/qualifiers/Qualifiers.java +++ b/inject/src/main/java/io/micronaut/inject/qualifiers/Qualifiers.java @@ -269,7 +269,7 @@ public static Qualifier byStereotype(String stereotype) { * @param The component type * @return The qualifier */ - public static Qualifier byTypeArguments(Class... typeArguments) { + public static Qualifier byTypeArguments(Class... typeArguments) { return new TypeArgumentQualifier<>(typeArguments); } @@ -293,7 +293,7 @@ public static Qualifier byTypeArguments(Class... typeArguments) { * @param The component type * @return The qualifier */ - public static Qualifier byTypeArgumentsClosest(Class... typeArguments) { + public static Qualifier byTypeArgumentsClosest(Class... typeArguments) { return new ClosestTypeArgumentQualifier<>(typeArguments); } @@ -304,7 +304,7 @@ public static Qualifier byTypeArgumentsClosest(Class... typeArguments) { * @param The component type * @return The qualifier */ - public static Qualifier byType(Class... typeArguments) { + public static Qualifier byType(Class... typeArguments) { return new TypeAnnotationQualifier<>(typeArguments); } diff --git a/inject/src/main/java/io/micronaut/inject/qualifiers/TypeAnnotationQualifier.java b/inject/src/main/java/io/micronaut/inject/qualifiers/TypeAnnotationQualifier.java index a9536b06711..97450b4688e 100644 --- a/inject/src/main/java/io/micronaut/inject/qualifiers/TypeAnnotationQualifier.java +++ b/inject/src/main/java/io/micronaut/inject/qualifiers/TypeAnnotationQualifier.java @@ -38,7 +38,7 @@ @Internal public class TypeAnnotationQualifier implements Qualifier { - private final List types; + private final List> types; /** * @param types The types diff --git a/inject/src/main/java/io/micronaut/inject/qualifiers/TypeArgumentQualifier.java b/inject/src/main/java/io/micronaut/inject/qualifiers/TypeArgumentQualifier.java index 138ef18d60d..37fbe1b232a 100644 --- a/inject/src/main/java/io/micronaut/inject/qualifiers/TypeArgumentQualifier.java +++ b/inject/src/main/java/io/micronaut/inject/qualifiers/TypeArgumentQualifier.java @@ -41,12 +41,12 @@ public class TypeArgumentQualifier implements Qualifier { private static final Logger LOG = ClassUtils.getLogger(TypeArgumentQualifier.class); - private final Class[] typeArguments; + private final Class[] typeArguments; /** * @param typeArguments The type arguments */ - TypeArgumentQualifier(Class... typeArguments) { + TypeArgumentQualifier(Class... typeArguments) { this.typeArguments = typeArguments; } @@ -55,7 +55,7 @@ public > Stream reduce(Class beanType, Stream return candidates.filter(candidate -> beanType.isAssignableFrom(candidate.getBeanType())) .filter(candidate -> { - List typeArguments = getTypeArguments(beanType, candidate); + List> typeArguments = getTypeArguments(beanType, candidate); boolean result = areTypesCompatible(typeArguments); if (LOG.isTraceEnabled() && !result) { @@ -68,7 +68,7 @@ public > Stream reduce(Class beanType, Stream /** * @return The type arguments */ - public Class[] getTypeArguments() { + public Class[] getTypeArguments() { return typeArguments; } @@ -76,8 +76,8 @@ public Class[] getTypeArguments() { * @param classes An array of classes * @return Whether the types are compatible */ - protected boolean areTypesCompatible(List classes) { - final Class[] typeArguments = this.typeArguments; + protected boolean areTypesCompatible(List> classes) { + final Class[] typeArguments = this.typeArguments; return areTypesCompatible(typeArguments, classes); } @@ -87,7 +87,7 @@ protected boolean areTypesCompatible(List classes) { * @param The bean type subclass * @return The list of type arguments */ - protected > List getTypeArguments(Class beanType, BT candidate) { + protected > List> getTypeArguments(Class beanType, BT candidate) { if (candidate instanceof BeanDefinition) { BeanDefinition definition = (BeanDefinition) candidate; return definition.getTypeArguments(beanType).stream().map(Argument::getType).collect(Collectors.toList()); @@ -107,7 +107,7 @@ protected > List getTypeArguments(Class beanTyp * @param classes The classes to check for alignments * @return True if they are */ - public static boolean areTypesCompatible(Class[] typeArguments, List classes) { + public static boolean areTypesCompatible(Class[] typeArguments, List> classes) { if (classes.isEmpty()) { // in this case the type doesn't specify type arguments, so this is the equivalent of using Object return true; @@ -116,8 +116,8 @@ public static boolean areTypesCompatible(Class[] typeArguments, List clas return false; } else { for (int i = 0; i < classes.size(); i++) { - Class left = classes.get(i); - Class right = typeArguments[i]; + Class left = classes.get(i); + Class right = typeArguments[i]; if (right == Object.class) { continue; } diff --git a/inject/src/test/groovy/io/micronaut/inject/qualifiers/ClosestTypeArgumentSpec.groovy b/inject/src/test/groovy/io/micronaut/inject/qualifiers/ClosestTypeArgumentSpec.groovy index 0dd4f98cda9..4e587be0d64 100644 --- a/inject/src/test/groovy/io/micronaut/inject/qualifiers/ClosestTypeArgumentSpec.groovy +++ b/inject/src/test/groovy/io/micronaut/inject/qualifiers/ClosestTypeArgumentSpec.groovy @@ -165,7 +165,7 @@ class ClosestTypeArgumentSpec extends Specification { beanDefinitions[0] == b } - private BeanDefinition stubFor(Class... typeArguments) { + private BeanDefinition stubFor(Class... typeArguments) { Stub(BeanDefinition) { getTypeArguments(BeanType) >> { typeArguments.collect { Argument.of(it) } diff --git a/jackson-databind/src/main/java/io/micronaut/jackson/ObjectMapperFactory.java b/jackson-databind/src/main/java/io/micronaut/jackson/ObjectMapperFactory.java index f68a95c49ea..3aa1b639ba5 100644 --- a/jackson-databind/src/main/java/io/micronaut/jackson/ObjectMapperFactory.java +++ b/jackson-databind/src/main/java/io/micronaut/jackson/ObjectMapperFactory.java @@ -125,12 +125,12 @@ public ObjectMapper objectMapper(@Nullable JacksonConfiguration jacksonConfigura Class type = serializer.getClass(); Type annotation = type.getAnnotation(Type.class); if (annotation != null) { - Class[] value = annotation.value(); - for (Class aClass : value) { + Class[] value = annotation.value(); + for (Class aClass : value) { module.addSerializer(aClass, serializer); } } else { - Optional targetType = GenericTypeUtils.resolveSuperGenericTypeArgument(type); + Optional> targetType = GenericTypeUtils.resolveSuperGenericTypeArgument(type); if (targetType.isPresent()) { module.addSerializer(targetType.get(), serializer); } else { @@ -143,12 +143,12 @@ public ObjectMapper objectMapper(@Nullable JacksonConfiguration jacksonConfigura Class type = deserializer.getClass(); Type annotation = type.getAnnotation(Type.class); if (annotation != null) { - Class[] value = annotation.value(); - for (Class aClass : value) { + Class[] value = annotation.value(); + for (Class aClass : value) { module.addDeserializer(aClass, deserializer); } } else { - Optional targetType = GenericTypeUtils.resolveSuperGenericTypeArgument(type); + Optional> targetType = GenericTypeUtils.resolveSuperGenericTypeArgument(type); targetType.ifPresent(aClass -> module.addDeserializer(aClass, deserializer)); } } @@ -167,8 +167,8 @@ public String deserialize(JsonParser p, DeserializationContext ctxt) throws IOEx Class type = keyDeserializer.getClass(); Type annotation = type.getAnnotation(Type.class); if (annotation != null) { - Class[] value = annotation.value(); - for (Class clazz : value) { + Class[] value = annotation.value(); + for (Class clazz : value) { module.addKeyDeserializer(clazz, keyDeserializer); } } diff --git a/jackson-databind/src/main/java/io/micronaut/jackson/databind/convert/JacksonConverterRegistrar.java b/jackson-databind/src/main/java/io/micronaut/jackson/databind/convert/JacksonConverterRegistrar.java index d14c72527ca..ef256c55844 100644 --- a/jackson-databind/src/main/java/io/micronaut/jackson/databind/convert/JacksonConverterRegistrar.java +++ b/jackson-databind/src/main/java/io/micronaut/jackson/databind/convert/JacksonConverterRegistrar.java @@ -178,7 +178,7 @@ protected TypeConverter jsonNodeToObjectConverter() { protected TypeConverter arrayNodeToIterableConverter() { return (node, targetType, context) -> { Map> typeVariables = context.getTypeVariables(); - Class elementType = typeVariables.isEmpty() ? Map.class : typeVariables.values().iterator().next().getType(); + Class elementType = typeVariables.isEmpty() ? Map.class : typeVariables.values().iterator().next().getType(); List results = new ArrayList(); node.elements().forEachRemaining(jsonNode -> { Optional converted = conversionService.convert(jsonNode, elementType, context); diff --git a/jackson-databind/src/main/java/io/micronaut/jackson/modules/BeanIntrospectionModule.java b/jackson-databind/src/main/java/io/micronaut/jackson/modules/BeanIntrospectionModule.java index 936bf048fd3..5419e8dc8a2 100644 --- a/jackson-databind/src/main/java/io/micronaut/jackson/modules/BeanIntrospectionModule.java +++ b/jackson-databind/src/main/java/io/micronaut/jackson/modules/BeanIntrospectionModule.java @@ -200,7 +200,7 @@ private T findSerializerFromAnnotation(BeanProperty beanProperty, Clas AnnotationValue jsonSerializeAnnotation = beanProperty.getAnnotation(annotationType); if (jsonSerializeAnnotation != null) { // ideally, we'd use SerializerProvider here, but it's not exposed to the BeanSerializerModifier - Class using = jsonSerializeAnnotation.classValue("using").orElse(null); + Class using = jsonSerializeAnnotation.classValue("using").orElse(null); if (using != null) { BeanIntrospection usingIntrospection = findIntrospection(using); if (usingIntrospection != null) { @@ -291,7 +291,7 @@ public JsonSerializer build() { } } }; - + newBuilder.setAnyGetter(builder.getAnyGetter()); final List properties = builder.getProperties(); final Collection> beanProperties = introspection.getBeanProperties(); diff --git a/jackson-databind/src/test/groovy/io/micronaut/jackson/modules/BeanIntrospectionModuleRecordSpec.groovy b/jackson-databind/src/test/groovy/io/micronaut/jackson/modules/BeanIntrospectionModuleRecordSpec.groovy index 73d21158c0e..cc5f5757ee8 100644 --- a/jackson-databind/src/test/groovy/io/micronaut/jackson/modules/BeanIntrospectionModuleRecordSpec.groovy +++ b/jackson-databind/src/test/groovy/io/micronaut/jackson/modules/BeanIntrospectionModuleRecordSpec.groovy @@ -40,7 +40,7 @@ record Test(String foo, String bar) { @Replaces(BeanIntrospectionModule) @Requires(property = "spec.name", value = 'BeanIntrospectionModuleRecordSpec') static class StaticBeanIntrospectionModule extends BeanIntrospectionModule { - Map introspectionMap = [:] + Map, BeanIntrospection> introspectionMap = [:] @Override protected BeanIntrospection findIntrospection(Class beanClass) { return introspectionMap.get(beanClass) diff --git a/json-core/src/main/java/io/micronaut/json/bind/JsonBeanPropertyBinder.java b/json-core/src/main/java/io/micronaut/json/bind/JsonBeanPropertyBinder.java index e1c9c482980..efcf95b9796 100644 --- a/json-core/src/main/java/io/micronaut/json/bind/JsonBeanPropertyBinder.java +++ b/json-core/src/main/java/io/micronaut/json/bind/JsonBeanPropertyBinder.java @@ -147,7 +147,7 @@ public Optional getOriginalValue() { return Optional.empty(); } }; - Class type = object != null ? object.getClass() : Object.class; + Class type = object != null ? object.getClass() : Object.class; return new ConversionErrorException(Argument.of(type), conversionError); } diff --git a/json-core/src/main/java/io/micronaut/json/convert/JsonConverterRegistrar.java b/json-core/src/main/java/io/micronaut/json/convert/JsonConverterRegistrar.java index e6cdbf40bce..1ae54d724f9 100644 --- a/json-core/src/main/java/io/micronaut/json/convert/JsonConverterRegistrar.java +++ b/json-core/src/main/java/io/micronaut/json/convert/JsonConverterRegistrar.java @@ -133,7 +133,7 @@ public TypeConverter arrayNodeToIterableConverter() { return Optional.empty(); } Map> typeVariables = context.getTypeVariables(); - Class elementType = typeVariables.isEmpty() ? Map.class : typeVariables.values().iterator().next().getType(); + Class elementType = typeVariables.isEmpty() ? Map.class : typeVariables.values().iterator().next().getType(); for (int i = 0; i < node.size(); i++) { Optional converted = conversionService.convert(node.get(i), elementType, context); converted.ifPresent(results::add); diff --git a/management/src/main/java/io/micronaut/management/endpoint/processors/AbstractEndpointRouteBuilder.java b/management/src/main/java/io/micronaut/management/endpoint/processors/AbstractEndpointRouteBuilder.java index 01a6f649591..2d40a0699d3 100644 --- a/management/src/main/java/io/micronaut/management/endpoint/processors/AbstractEndpointRouteBuilder.java +++ b/management/src/main/java/io/micronaut/management/endpoint/processors/AbstractEndpointRouteBuilder.java @@ -51,7 +51,7 @@ abstract class AbstractEndpointRouteBuilder extends DefaultRouteBuilder implemen private static final Pattern ENDPOINT_ID_PATTERN = Pattern.compile("\\w+"); - private Map> endpointIds = new ConcurrentHashMap<>(); + private Map, Optional> endpointIds = new ConcurrentHashMap<>(); private final ApplicationContext beanContext; diff --git a/retry/src/main/java/io/micronaut/retry/intercept/MyCustomException.java b/retry/src/test/java/io/micronaut/retry/intercept/MyCustomException.java similarity index 100% rename from retry/src/main/java/io/micronaut/retry/intercept/MyCustomException.java rename to retry/src/test/java/io/micronaut/retry/intercept/MyCustomException.java diff --git a/router/src/main/java/io/micronaut/web/router/AbstractRouteMatch.java b/router/src/main/java/io/micronaut/web/router/AbstractRouteMatch.java index aae32611cb1..46b72a7e2e1 100644 --- a/router/src/main/java/io/micronaut/web/router/AbstractRouteMatch.java +++ b/router/src/main/java/io/micronaut/web/router/AbstractRouteMatch.java @@ -259,7 +259,7 @@ public R execute(Map argumentValues) { value = argumentValues.get(name); } - Class argumentType = argument.getType(); + Class argumentType = argument.getType(); if (value instanceof UnresolvedArgument) { UnresolvedArgument unresolved = (UnresolvedArgument) value; ArgumentBinder.BindingResult bindingResult = unresolved.get(); @@ -304,7 +304,7 @@ public R execute(Map argumentValues) { } } - private void convertValueAndAddToList(ConversionService conversionService, List argumentList, Argument argument, Object value, Class argumentType) { + private void convertValueAndAddToList(ConversionService conversionService, List argumentList, Argument argument, Object value, Class argumentType) { if (argumentType.isInstance(value)) { if (argument.isContainerType()) { if (argument.hasTypeVariables()) { @@ -391,7 +391,7 @@ public RouteMatch fulfill(Map argumentValues) { if (value instanceof UnresolvedArgument || value instanceof NullArgument) { newVariables.put(name, value); } else { - Class type = requiredArgument.getType(); + Class type = requiredArgument.getType(); if (type.isInstance(value)) { newVariables.put(name, value); } else { diff --git a/router/src/main/java/io/micronaut/web/router/AnnotatedMethodRouteBuilder.java b/router/src/main/java/io/micronaut/web/router/AnnotatedMethodRouteBuilder.java index f0ebcef3e8e..eefebdb7f63 100644 --- a/router/src/main/java/io/micronaut/web/router/AnnotatedMethodRouteBuilder.java +++ b/router/src/main/java/io/micronaut/web/router/AnnotatedMethodRouteBuilder.java @@ -47,7 +47,7 @@ public class AnnotatedMethodRouteBuilder extends DefaultRouteBuilder implements ExecutableMethodProcessor { private static final MediaType[] DEFAULT_MEDIA_TYPES = {MediaType.APPLICATION_JSON_TYPE}; - private final Map> httpMethodsHandlers = new LinkedHashMap<>(); + private final Map, Consumer> httpMethodsHandlers = new LinkedHashMap<>(); /** * @param executionHandleLocator The execution handler locator @@ -283,7 +283,7 @@ public AnnotatedMethodRouteBuilder(ExecutionHandleLocator executionHandleLocator final BeanDefinition bean = definition.beanDefinition; boolean isGlobal = method.isTrue(Error.class, "global"); - Class declaringType = bean.getBeanType(); + Class declaringType = bean.getBeanType(); if (method.isPresent(Error.class, "status")) { Optional value = method.enumValue(Error.class, "status", HttpStatus.class); value.ifPresent(httpStatus -> { @@ -294,11 +294,11 @@ public AnnotatedMethodRouteBuilder(ExecutionHandleLocator executionHandleLocator } }); } else { - Class exceptionType = null; + Class exceptionType = null; if (method.isPresent(Error.class, AnnotationMetadata.VALUE_MEMBER)) { Optional annotationValue = method.classValue(Error.class); if (annotationValue.isPresent() && Throwable.class.isAssignableFrom(annotationValue.get())) { - exceptionType = annotationValue.get(); + exceptionType = (Class) annotationValue.get(); } } if (exceptionType == null) { diff --git a/router/src/main/java/io/micronaut/web/router/DefaultRouteBuilder.java b/router/src/main/java/io/micronaut/web/router/DefaultRouteBuilder.java index 6736bdbb72a..7488c339517 100644 --- a/router/src/main/java/io/micronaut/web/router/DefaultRouteBuilder.java +++ b/router/src/main/java/io/micronaut/web/router/DefaultRouteBuilder.java @@ -72,7 +72,7 @@ public abstract class DefaultRouteBuilder implements RouteBuilder { protected final ConversionService conversionService; protected final Charset defaultCharset; - private DefaultUriRoute currentParentRoute = null; + private DefaultUriRoute currentParentRoute; private List uriRoutes = new ArrayList<>(); private List statusRoutes = new ArrayList<>(); private List errorRoutes = new ArrayList<>(); @@ -165,18 +165,18 @@ public UriNamingStrategy getUriNamingStrategy() { } @Override - public ResourceRoute resources(Class cls) { + public ResourceRoute resources(Class cls) { return new DefaultResourceRoute(cls); } @Override - public ResourceRoute single(Class cls) { + public ResourceRoute single(Class cls) { return new DefaultSingleRoute(cls); } @Override - public StatusRoute status(Class originatingClass, HttpStatus status, Class type, String method, Class[] parameterTypes) { - Optional> executionHandle = executionHandleLocator.findExecutionHandle(type, method, parameterTypes); + public StatusRoute status(Class originatingClass, HttpStatus status, Class type, String method, Class[] parameterTypes) { + Optional> executionHandle = executionHandleLocator.findExecutionHandle((Class) type, method, parameterTypes); MethodExecutionHandle executableHandle = executionHandle.orElseThrow(() -> new RoutingException("No such route: " + type.getName() + "." + method) @@ -188,8 +188,8 @@ public StatusRoute status(Class originatingClass, HttpStatus status, Class type, } @Override - public StatusRoute status(HttpStatus status, Class type, String method, Class[] parameterTypes) { - Optional> executionHandle = executionHandleLocator.findExecutionHandle(type, method, parameterTypes); + public StatusRoute status(HttpStatus status, Class type, String method, Class[] parameterTypes) { + Optional> executionHandle = executionHandleLocator.findExecutionHandle((Class) type, method, parameterTypes); MethodExecutionHandle executableHandle = executionHandle.orElseThrow(() -> new RoutingException("No such route: " + type.getName() + "." + method) @@ -201,8 +201,8 @@ public StatusRoute status(HttpStatus status, Class type, String method, Class[] } @Override - public ErrorRoute error(Class originatingClass, Class error, Class type, String method, Class[] parameterTypes) { - Optional> executionHandle = executionHandleLocator.findExecutionHandle(type, method, parameterTypes); + public ErrorRoute error(Class originatingClass, Class error, Class type, String method, Class[] parameterTypes) { + Optional> executionHandle = executionHandleLocator.findExecutionHandle((Class) type, method, parameterTypes); MethodExecutionHandle executableHandle = executionHandle.orElseThrow(() -> new RoutingException("No such route: " + type.getName() + "." + method) @@ -214,8 +214,8 @@ public ErrorRoute error(Class originatingClass, Class error } @Override - public ErrorRoute error(Class error, Class type, String method, Class[] parameterTypes) { - Optional> executionHandle = executionHandleLocator.findExecutionHandle(type, method, parameterTypes); + public ErrorRoute error(Class error, Class type, String method, Class[] parameterTypes) { + Optional> executionHandle = executionHandleLocator.findExecutionHandle((Class) type, method, parameterTypes); MethodExecutionHandle executableHandle = executionHandle.orElseThrow(() -> new RoutingException("No such route: " + type.getName() + "." + method) @@ -227,82 +227,82 @@ public ErrorRoute error(Class error, Class type, String met } @Override - public UriRoute GET(String uri, Object target, String method, Class... parameterTypes) { + public UriRoute GET(String uri, Object target, String method, Class... parameterTypes) { return buildRoute(HttpMethod.GET, uri, target.getClass(), method, parameterTypes); } @Override - public UriRoute GET(String uri, Class type, String method, Class... parameterTypes) { + public UriRoute GET(String uri, Class type, String method, Class... parameterTypes) { return buildRoute(HttpMethod.GET, uri, type, method, parameterTypes); } @Override - public UriRoute POST(String uri, Object target, String method, Class... parameterTypes) { + public UriRoute POST(String uri, Object target, String method, Class... parameterTypes) { return buildRoute(HttpMethod.POST, uri, target.getClass(), method, parameterTypes); } @Override - public UriRoute POST(String uri, Class type, String method, Class... parameterTypes) { + public UriRoute POST(String uri, Class type, String method, Class... parameterTypes) { return buildRoute(HttpMethod.POST, uri, type, method, parameterTypes); } @Override - public UriRoute PUT(String uri, Object target, String method, Class... parameterTypes) { + public UriRoute PUT(String uri, Object target, String method, Class... parameterTypes) { return buildRoute(HttpMethod.PUT, uri, target.getClass(), method, parameterTypes); } @Override - public UriRoute PUT(String uri, Class type, String method, Class... parameterTypes) { + public UriRoute PUT(String uri, Class type, String method, Class... parameterTypes) { return buildRoute(HttpMethod.PUT, uri, type, method, parameterTypes); } @Override - public UriRoute PATCH(String uri, Object target, String method, Class... parameterTypes) { + public UriRoute PATCH(String uri, Object target, String method, Class... parameterTypes) { return buildRoute(HttpMethod.PATCH, uri, target.getClass(), method, parameterTypes); } @Override - public UriRoute PATCH(String uri, Class type, String method, Class... parameterTypes) { + public UriRoute PATCH(String uri, Class type, String method, Class... parameterTypes) { return buildRoute(HttpMethod.PATCH, uri, type, method, parameterTypes); } @Override - public UriRoute DELETE(String uri, Object target, String method, Class... parameterTypes) { + public UriRoute DELETE(String uri, Object target, String method, Class... parameterTypes) { return buildRoute(HttpMethod.DELETE, uri, target.getClass(), method, parameterTypes); } @Override - public UriRoute DELETE(String uri, Class type, String method, Class... parameterTypes) { + public UriRoute DELETE(String uri, Class type, String method, Class... parameterTypes) { return buildRoute(HttpMethod.DELETE, uri, type, method, parameterTypes); } @Override - public UriRoute OPTIONS(String uri, Object target, String method, Class... parameterTypes) { + public UriRoute OPTIONS(String uri, Object target, String method, Class... parameterTypes) { return buildRoute(HttpMethod.OPTIONS, uri, target.getClass(), method, parameterTypes); } @Override - public UriRoute OPTIONS(String uri, Class type, String method, Class... parameterTypes) { + public UriRoute OPTIONS(String uri, Class type, String method, Class... parameterTypes) { return buildRoute(HttpMethod.OPTIONS, uri, type, method, parameterTypes); } @Override - public UriRoute HEAD(String uri, Object target, String method, Class... parameterTypes) { + public UriRoute HEAD(String uri, Object target, String method, Class... parameterTypes) { return buildRoute(HttpMethod.HEAD, uri, target.getClass(), method, parameterTypes); } @Override - public UriRoute HEAD(String uri, Class type, String method, Class... parameterTypes) { + public UriRoute HEAD(String uri, Class type, String method, Class... parameterTypes) { return buildRoute(HttpMethod.HEAD, uri, type, method, parameterTypes); } @Override - public UriRoute TRACE(String uri, Object target, String method, Class[] parameterTypes) { + public UriRoute TRACE(String uri, Object target, String method, Class[] parameterTypes) { return buildRoute(HttpMethod.TRACE, uri, target.getClass(), method, parameterTypes); } @Override - public UriRoute TRACE(String uri, Class type, String method, Class[] parameterTypes) { + public UriRoute TRACE(String uri, Class type, String method, Class[] parameterTypes) { return buildRoute(HttpMethod.TRACE, uri, type, method, parameterTypes); } @@ -357,7 +357,7 @@ public UriRoute TRACE(String uri, BeanDefinition beanDefinition, ExecutableMe * * @return an {@link UriRoute} */ - protected UriRoute buildRoute(HttpMethod httpMethod, String uri, Class type, String method, Class... parameterTypes) { + protected UriRoute buildRoute(HttpMethod httpMethod, String uri, Class type, String method, Class... parameterTypes) { Optional> executionHandle = executionHandleLocator.findExecutionHandle(type, method, parameterTypes); MethodExecutionHandle executableHandle = executionHandle.orElseThrow(() -> @@ -644,7 +644,7 @@ public int hashCode() { class DefaultErrorRoute extends AbstractRoute implements ErrorRoute { private final Class error; - private final Class originatingClass; + private final Class originatingClass; /** * @param error The throwable @@ -662,7 +662,7 @@ public DefaultErrorRoute(Class error, MethodExecutionHandle * @param conversionService The conversion service */ public DefaultErrorRoute( - Class originatingClass, Class error, + Class originatingClass, Class error, MethodExecutionHandle targetMethod, ConversionService conversionService) { super(targetMethod, conversionService, Collections.emptyList()); @@ -683,7 +683,7 @@ public Class exceptionType() { @SuppressWarnings("unchecked") @Override - public Optional> match(Class originatingClass, Throwable exception) { + public Optional> match(Class originatingClass, Throwable exception) { if (originatingClass == this.originatingClass && error.isInstance(exception)) { return Optional.of(new ErrorRouteMatch(exception, this, conversionService)); } @@ -765,7 +765,7 @@ public String toString() { class DefaultStatusRoute extends AbstractRoute implements StatusRoute { private final HttpStatus status; - private final Class originatingClass; + private final Class originatingClass; /** * @param status The HTTP Status @@ -782,7 +782,7 @@ public DefaultStatusRoute(HttpStatus status, MethodExecutionHandle targetMethod, * @param targetMethod The target method execution handle * @param conversionService The conversion service */ - public DefaultStatusRoute(Class originatingClass, HttpStatus status, MethodExecutionHandle targetMethod, ConversionService conversionService) { + public DefaultStatusRoute(Class originatingClass, HttpStatus status, MethodExecutionHandle targetMethod, ConversionService conversionService) { super(targetMethod, conversionService, Collections.emptyList()); this.originatingClass = originatingClass; this.status = status; @@ -801,7 +801,7 @@ public HttpStatus status() { @SuppressWarnings("unchecked") @Override - public Optional> match(Class originatingClass, HttpStatus status) { + public Optional> match(Class originatingClass, HttpStatus status) { if (originatingClass == this.originatingClass && this.status == status) { return Optional.of(new StatusRouteMatch(status, this, conversionService)); } @@ -1112,7 +1112,7 @@ class DefaultSingleRoute extends DefaultResourceRoute { /** * @param type The class */ - DefaultSingleRoute(Class type) { + DefaultSingleRoute(Class type) { super(type); } @@ -1122,7 +1122,7 @@ protected ResourceRoute newResourceRoute(Map newMap, DefaultU } @Override - protected DefaultUriRoute buildGetRoute(Class type, Map routeMap) { + protected DefaultUriRoute buildGetRoute(Class type, Map routeMap) { DefaultUriRoute getRoute = (DefaultUriRoute) DefaultRouteBuilder.this.GET(type); routeMap.put( HttpMethod.GET, getRoute @@ -1131,7 +1131,7 @@ protected DefaultUriRoute buildGetRoute(Class type, Map route } @Override - protected void buildRemainingRoutes(Class type, Map routeMap) { + protected void buildRemainingRoutes(Class type, Map routeMap) { // POST /foo routeMap.put( HttpMethod.POST, DefaultRouteBuilder.this.POST(type) @@ -1171,7 +1171,7 @@ class DefaultResourceRoute implements ResourceRoute { /** * @param type The class */ - DefaultResourceRoute(Class type) { + DefaultResourceRoute(Class type) { this.resourceRoutes = new LinkedHashMap<>(); // GET /foo/1 Map routeMap = this.resourceRoutes; @@ -1261,7 +1261,7 @@ protected ResourceRoute newResourceRoute(Map newMap, DefaultU * * @return The {@link DefaultUriRoute} */ - protected DefaultUriRoute buildGetRoute(Class type, Map routeMap) { + protected DefaultUriRoute buildGetRoute(Class type, Map routeMap) { DefaultUriRoute getRoute = (DefaultUriRoute) DefaultRouteBuilder.this.GET(type, ID); routeMap.put( HttpMethod.GET, getRoute @@ -1275,7 +1275,7 @@ protected DefaultUriRoute buildGetRoute(Class type, Map route * @param type The class * @param routeMap The route info */ - protected void buildRemainingRoutes(Class type, Map routeMap) { + protected void buildRemainingRoutes(Class type, Map routeMap) { // GET /foo routeMap.put( HttpMethod.GET, DefaultRouteBuilder.this.GET(type) diff --git a/router/src/main/java/io/micronaut/web/router/DefaultRouter.java b/router/src/main/java/io/micronaut/web/router/DefaultRouter.java index 542c84f09ef..a012d9b694c 100644 --- a/router/src/main/java/io/micronaut/web/router/DefaultRouter.java +++ b/router/src/main/java/io/micronaut/web/router/DefaultRouter.java @@ -308,7 +308,7 @@ public Optional> route(@NonNull HttpStatus status) { } @Override - public Optional> route(@NonNull Class originatingClass, @NonNull HttpStatus status) { + public Optional> route(@NonNull Class originatingClass, @NonNull HttpStatus status) { for (StatusRoute statusRoute : statusRoutes) { Optional> match = statusRoute.match(originatingClass, status); if (match.isPresent()) { @@ -319,7 +319,7 @@ public Optional> route(@NonNull Class originatingClass, @NonNu } @Override - public Optional> route(@NonNull Class originatingClass, @NonNull Throwable error) { + public Optional> route(@NonNull Class originatingClass, @NonNull Throwable error) { Map> matchedRoutes = new LinkedHashMap<>(); for (ErrorRoute errorRoute : errorRoutes) { Optional> match = errorRoute.match(originatingClass, error); @@ -514,17 +514,17 @@ private Optional> findRouteMatch(Map } else if (matchedRoutes.size() > 1) { int minCount = Integer.MAX_VALUE; - Supplier> hierarchySupplier = () -> ClassUtils.resolveHierarchy(error.getClass()); + Supplier>> hierarchySupplier = () -> ClassUtils.resolveHierarchy(error.getClass()); Optional> match = Optional.empty(); - Class errorClass = error.getClass(); + Class errorClass = error.getClass(); for (Map.Entry> entry: matchedRoutes.entrySet()) { - Class exceptionType = entry.getKey().exceptionType(); + Class exceptionType = entry.getKey().exceptionType(); if (exceptionType.equals(errorClass)) { match = Optional.of(entry.getValue()); break; } else { - List hierarchy = hierarchySupplier.get(); + List> hierarchy = hierarchySupplier.get(); //measures the distance in the hierarchy from the error and the route error type int index = hierarchy.indexOf(exceptionType); //the class closest in the hierarchy should be chosen diff --git a/router/src/main/java/io/micronaut/web/router/ErrorRoute.java b/router/src/main/java/io/micronaut/web/router/ErrorRoute.java index dd5921bd179..2f3edb7743d 100644 --- a/router/src/main/java/io/micronaut/web/router/ErrorRoute.java +++ b/router/src/main/java/io/micronaut/web/router/ErrorRoute.java @@ -56,7 +56,7 @@ public interface ErrorRoute extends MethodBasedRoute { * @param The type * @return The route match */ - Optional> match(Class originatingClass, Throwable exception); + Optional> match(Class originatingClass, Throwable exception); @Override ErrorRoute consumes(MediaType... mediaType); diff --git a/router/src/main/java/io/micronaut/web/router/RouteBuilder.java b/router/src/main/java/io/micronaut/web/router/RouteBuilder.java index a98d4deff02..fa65f63fc7c 100644 --- a/router/src/main/java/io/micronaut/web/router/RouteBuilder.java +++ b/router/src/main/java/io/micronaut/web/router/RouteBuilder.java @@ -122,7 +122,7 @@ public interface RouteBuilder { * @param cls The class * @return The {@link ResourceRoute} */ - ResourceRoute resources(Class cls); + ResourceRoute resources(Class cls); /** *

Builds the necessary mappings to treat the given instance as a REST endpoint.

@@ -154,7 +154,7 @@ default ResourceRoute resources(Object instance) { * @param cls The class * @return The {@link ResourceRoute} */ - ResourceRoute single(Class cls); + ResourceRoute single(Class cls); /** *

Builds the necessary mappings to treat the given instance as a singular REST endpoint.

@@ -188,7 +188,7 @@ default StatusRoute status(HttpStatus status, Object instance, String method) { * @param parameterTypes The parameter types for the target method * @return The route */ - StatusRoute status(HttpStatus status, Class type, String method, Class... parameterTypes); + StatusRoute status(HttpStatus status, Class type, String method, Class... parameterTypes); /** * Register a route to handle the returned status code. This implementation considers the originatingClass for matching. @@ -200,7 +200,7 @@ default StatusRoute status(HttpStatus status, Object instance, String method) { * @param parameterTypes The parameter types for the target method * @return The route */ - StatusRoute status(Class originatingClass, HttpStatus status, Class type, String method, Class... parameterTypes); + StatusRoute status(Class originatingClass, HttpStatus status, Class type, String method, Class... parameterTypes); /** * Register a route to handle the error. @@ -211,7 +211,7 @@ default StatusRoute status(HttpStatus status, Object instance, String method) { * @param parameterTypes The parameter types for the target method * @return The route */ - ErrorRoute error(Class error, Class type, String method, Class... parameterTypes); + ErrorRoute error(Class error, Class type, String method, Class... parameterTypes); /** * Register a route to handle the error. @@ -223,7 +223,7 @@ default StatusRoute status(HttpStatus status, Object instance, String method) { * @param parameterTypes The parameter types for the target method * @return The route */ - ErrorRoute error(Class originatingClass, Class error, Class type, String method, Class... parameterTypes); + ErrorRoute error(Class originatingClass, Class error, Class type, String method, Class... parameterTypes); /** * Register a route to handle the error. @@ -232,7 +232,7 @@ default StatusRoute status(HttpStatus status, Object instance, String method) { * @param type The type * @return The route */ - default ErrorRoute error(Class error, Class type) { + default ErrorRoute error(Class error, Class type) { return error(error, type, NameUtils.decapitalize(NameUtils.trimSuffix(type.getSimpleName(), "Exception", "Error")), ReflectionUtils.EMPTY_CLASS_ARRAY); } @@ -272,7 +272,7 @@ default ErrorRoute error(Class error, Object instance, Stri * @param parameterTypes The parameter types * @return The route */ - default ErrorRoute error(Class error, Object instance, String method, Class... parameterTypes) { + default ErrorRoute error(Class error, Object instance, String method, Class... parameterTypes) { return error(error, instance.getClass(), method, parameterTypes); } @@ -317,7 +317,7 @@ default UriRoute GET(Object target, PropertyConvention id) { * @param type The class * @return The route */ - default UriRoute GET(Class type) { + default UriRoute GET(Class type) { return GET(getUriNamingStrategy().resolveUri(type), type, MethodConvention.INDEX.methodName()); } @@ -328,7 +328,7 @@ default UriRoute GET(Class type) { * @param id The route id * @return The route */ - default UriRoute GET(Class type, PropertyConvention id) { + default UriRoute GET(Class type, PropertyConvention id) { return GET(getUriNamingStrategy().resolveUri(type, id), type, MethodConvention.SHOW.methodName(), Object.class); } @@ -370,7 +370,7 @@ default UriRoute GET(String uri, BeanDefinition beanDefinition, ExecutableMet * @param parameterTypes The parameter types for the target method * @return The route */ - UriRoute GET(String uri, Object target, String method, Class... parameterTypes); + UriRoute GET(String uri, Object target, String method, Class... parameterTypes); /** *

Route the specified URI template to the specified target.

@@ -383,7 +383,7 @@ default UriRoute GET(String uri, BeanDefinition beanDefinition, ExecutableMet * @param parameterTypes The parameter types for the target method * @return The route */ - UriRoute GET(String uri, Class type, String method, Class... parameterTypes); + UriRoute GET(String uri, Class type, String method, Class... parameterTypes); /** * Route the specified URI to the specified target for an HTTP POST. Since the method to execute is not @@ -394,7 +394,7 @@ default UriRoute GET(String uri, BeanDefinition beanDefinition, ExecutableMet * @param parameterTypes The parameter types for the target method * @return The route */ - default UriRoute POST(String uri, Object target, Class... parameterTypes) { + default UriRoute POST(String uri, Object target, Class... parameterTypes) { return POST(uri, target, MethodConvention.SAVE.methodName(), parameterTypes); } @@ -427,7 +427,7 @@ default UriRoute POST(Object target, PropertyConvention id) { * @param type The class * @return The route */ - default UriRoute POST(Class type) { + default UriRoute POST(Class type) { return POST(getUriNamingStrategy().resolveUri(type), type, MethodConvention.SAVE.methodName()); } @@ -438,7 +438,7 @@ default UriRoute POST(Class type) { * @param id The route id * @return The route */ - default UriRoute POST(Class type, PropertyConvention id) { + default UriRoute POST(Class type, PropertyConvention id) { return POST(getUriNamingStrategy().resolveUri(type, id), type, MethodConvention.UPDATE.methodName()); } @@ -480,7 +480,7 @@ default UriRoute POST(String uri, BeanDefinition beanDefinition, ExecutableMe * @param parameterTypes The parameter types for the target method * @return The route */ - UriRoute POST(String uri, Object target, String method, Class... parameterTypes); + UriRoute POST(String uri, Object target, String method, Class... parameterTypes); /** *

Route the specified URI template to the specified target.

@@ -493,7 +493,7 @@ default UriRoute POST(String uri, BeanDefinition beanDefinition, ExecutableMe * @param parameterTypes The parameter types for the target method * @return The route */ - UriRoute POST(String uri, Class type, String method, Class... parameterTypes); + UriRoute POST(String uri, Class type, String method, Class... parameterTypes); /** * Route the specified URI to the specified target for an HTTP PUT. Since the method to execute is not @@ -536,7 +536,7 @@ default UriRoute PUT(Object target, PropertyConvention id) { * @param type The class * @return The route */ - default UriRoute PUT(Class type) { + default UriRoute PUT(Class type) { return PUT(getUriNamingStrategy().resolveUri(type), type, MethodConvention.UPDATE.methodName(), Object.class); } @@ -547,7 +547,7 @@ default UriRoute PUT(Class type) { * @param id The route id * @return The route */ - default UriRoute PUT(Class type, PropertyConvention id) { + default UriRoute PUT(Class type, PropertyConvention id) { return PUT(getUriNamingStrategy().resolveUri(type, id), type, MethodConvention.UPDATE.methodName(), Object.class); } @@ -589,7 +589,7 @@ default UriRoute PUT(String uri, BeanDefinition beanDefinition, ExecutableMet * @param parameterTypes The parameter types for the target method * @return The route */ - UriRoute PUT(String uri, Object target, String method, Class... parameterTypes); + UriRoute PUT(String uri, Object target, String method, Class... parameterTypes); /** *

Route the specified URI template to the specified target.

@@ -602,7 +602,7 @@ default UriRoute PUT(String uri, BeanDefinition beanDefinition, ExecutableMet * @param parameterTypes The parameter types for the target method * @return The route */ - UriRoute PUT(String uri, Class type, String method, Class... parameterTypes); + UriRoute PUT(String uri, Class type, String method, Class... parameterTypes); /** * Route the specified URI to the specified target for an HTTP PATCH. Since the method to execute is not @@ -645,7 +645,7 @@ default UriRoute PATCH(Object target, PropertyConvention id) { * @param type The class * @return The route */ - default UriRoute PATCH(Class type) { + default UriRoute PATCH(Class type) { return PATCH(getUriNamingStrategy().resolveUri(type), type, MethodConvention.UPDATE.methodName(), Object.class); } @@ -656,7 +656,7 @@ default UriRoute PATCH(Class type) { * @param id The route id * @return The route */ - default UriRoute PATCH(Class type, PropertyConvention id) { + default UriRoute PATCH(Class type, PropertyConvention id) { return PATCH(getUriNamingStrategy().resolveUri(type, id), type, MethodConvention.UPDATE.methodName(), Object.class); } @@ -698,7 +698,7 @@ default UriRoute PATCH(String uri, BeanDefinition beanDefinition, ExecutableM * @param parameterTypes The parameter types for the target method * @return The route */ - UriRoute PATCH(String uri, Object target, String method, Class... parameterTypes); + UriRoute PATCH(String uri, Object target, String method, Class... parameterTypes); /** *

Route the specified URI template to the specified target.

@@ -711,7 +711,7 @@ default UriRoute PATCH(String uri, BeanDefinition beanDefinition, ExecutableM * @param parameterTypes The parameter types for the target method * @return The route */ - UriRoute PATCH(String uri, Class type, String method, Class... parameterTypes); + UriRoute PATCH(String uri, Class type, String method, Class... parameterTypes); /** * Route the specified URI to the specified target for an HTTP DELETE. Since the method to execute is not @@ -754,7 +754,7 @@ default UriRoute DELETE(Object target, PropertyConvention id) { * @param type The class * @return The route */ - default UriRoute DELETE(Class type) { + default UriRoute DELETE(Class type) { return DELETE(getUriNamingStrategy().resolveUri(type), type, MethodConvention.DELETE.methodName(), Object.class); } @@ -765,7 +765,7 @@ default UriRoute DELETE(Class type) { * @param id The route id * @return The route */ - default UriRoute DELETE(Class type, PropertyConvention id) { + default UriRoute DELETE(Class type, PropertyConvention id) { return DELETE(getUriNamingStrategy().resolveUri(type, id), type, MethodConvention.DELETE.methodName(), Object.class); } @@ -807,7 +807,7 @@ default UriRoute DELETE(String uri, BeanDefinition beanDefinition, Executable * @param parameterTypes The parameter types for the target method * @return The route */ - UriRoute DELETE(String uri, Object target, String method, Class... parameterTypes); + UriRoute DELETE(String uri, Object target, String method, Class... parameterTypes); /** *

Route the specified URI template to the specified target.

@@ -820,7 +820,7 @@ default UriRoute DELETE(String uri, BeanDefinition beanDefinition, Executable * @param parameterTypes The parameter types for the target method * @return The route */ - UriRoute DELETE(String uri, Class type, String method, Class... parameterTypes); + UriRoute DELETE(String uri, Class type, String method, Class... parameterTypes); /** * Route the specified URI to the specified target for an HTTP OPTIONS. Since the method to execute is not @@ -863,7 +863,7 @@ default UriRoute OPTIONS(Object target, PropertyConvention id) { * @param type The class * @return The route */ - default UriRoute OPTIONS(Class type) { + default UriRoute OPTIONS(Class type) { return OPTIONS(getUriNamingStrategy().resolveUri(type), type, MethodConvention.OPTIONS.methodName()); } @@ -874,7 +874,7 @@ default UriRoute OPTIONS(Class type) { * @param id The route id * @return The route */ - default UriRoute OPTIONS(Class type, PropertyConvention id) { + default UriRoute OPTIONS(Class type, PropertyConvention id) { return OPTIONS(getUriNamingStrategy().resolveUri(type, id), type, MethodConvention.OPTIONS.methodName()); } @@ -916,7 +916,7 @@ default UriRoute OPTIONS(String uri, BeanDefinition beanDefinition, Executabl * @param parameterTypes The parameter types for the target method * @return The route */ - UriRoute OPTIONS(String uri, Object target, String method, Class... parameterTypes); + UriRoute OPTIONS(String uri, Object target, String method, Class... parameterTypes); /** *

Route the specified URI template to the specified target.

@@ -929,7 +929,7 @@ default UriRoute OPTIONS(String uri, BeanDefinition beanDefinition, Executabl * @param parameterTypes The parameter types for the target method * @return The route */ - UriRoute OPTIONS(String uri, Class type, String method, Class... parameterTypes); + UriRoute OPTIONS(String uri, Class type, String method, Class... parameterTypes); /** * Route the specified URI to the specified target for an HTTP GET. Since the method to execute is not @@ -972,7 +972,7 @@ default UriRoute HEAD(Object target, PropertyConvention id) { * @param type The class * @return The route */ - default UriRoute HEAD(Class type) { + default UriRoute HEAD(Class type) { return HEAD(getUriNamingStrategy().resolveUri(type), type, MethodConvention.HEAD.methodName()); } @@ -983,7 +983,7 @@ default UriRoute HEAD(Class type) { * @param id The route id * @return The route */ - default UriRoute HEAD(Class type, PropertyConvention id) { + default UriRoute HEAD(Class type, PropertyConvention id) { return HEAD(getUriNamingStrategy().resolveUri(type, id), type, MethodConvention.HEAD.methodName()); } @@ -1025,7 +1025,7 @@ default UriRoute HEAD(String uri, BeanDefinition beanDefinition, ExecutableMe * @param parameterTypes The parameter types for the target method * @return The route */ - UriRoute HEAD(String uri, Object target, String method, Class... parameterTypes); + UriRoute HEAD(String uri, Object target, String method, Class... parameterTypes); /** *

Route the specified URI template to the specified target.

@@ -1038,7 +1038,7 @@ default UriRoute HEAD(String uri, BeanDefinition beanDefinition, ExecutableMe * @param parameterTypes The parameter types for the target method * @return The route */ - UriRoute HEAD(String uri, Class type, String method, Class... parameterTypes); + UriRoute HEAD(String uri, Class type, String method, Class... parameterTypes); /** * Route the specified URI to the specified target for an HTTP GET. Since the method to execute is not @@ -1081,7 +1081,7 @@ default UriRoute TRACE(Object target, PropertyConvention id) { * @param type The class * @return The route */ - default UriRoute TRACE(Class type) { + default UriRoute TRACE(Class type) { return TRACE(getUriNamingStrategy().resolveUri(type), type, MethodConvention.TRACE.methodName()); } @@ -1092,7 +1092,7 @@ default UriRoute TRACE(Class type) { * @param id The route id * @return The route */ - default UriRoute TRACE(Class type, PropertyConvention id) { + default UriRoute TRACE(Class type, PropertyConvention id) { return HEAD(getUriNamingStrategy().resolveUri(type, id), type, MethodConvention.TRACE.methodName()); } @@ -1134,7 +1134,7 @@ default UriRoute TRACE(String uri, BeanDefinition beanDefinition, ExecutableM * @param parameterTypes The parameter types for the target method * @return The route */ - UriRoute TRACE(String uri, Object target, String method, Class... parameterTypes); + UriRoute TRACE(String uri, Object target, String method, Class... parameterTypes); /** *

Route the specified URI template to the specified target.

@@ -1147,7 +1147,7 @@ default UriRoute TRACE(String uri, BeanDefinition beanDefinition, ExecutableM * @param parameterTypes The parameter types for the target method * @return The route */ - UriRoute TRACE(String uri, Class type, String method, Class... parameterTypes); + UriRoute TRACE(String uri, Class type, String method, Class... parameterTypes); /** *

A URI naming strategy is used to dictate the default name to use when building a URI for a class.

@@ -1193,8 +1193,7 @@ String resolveUri(BeanDefinition beanDefinition) { return uri; } Class beanType; - if (beanDefinition instanceof ProxyBeanDefinition) { - ProxyBeanDefinition pbd = (ProxyBeanDefinition) beanDefinition; + if (beanDefinition instanceof ProxyBeanDefinition pbd) { beanType = pbd.getTargetType(); } else { beanType = beanDefinition.getBeanType(); @@ -1226,13 +1225,13 @@ String resolveUri(String property) { * @param id the route id * @return The URI to use */ - default @NonNull String resolveUri(Class type, PropertyConvention id) { + default @NonNull String resolveUri(Class type, PropertyConvention id) { return resolveUri(type) + "/{" + id.lowerCaseName() + "}"; } /** * Normalizes a URI. - * + *

* Ensures the string: * 1) Does not end with a / * 2) Starts with a / diff --git a/router/src/main/java/io/micronaut/web/router/Router.java b/router/src/main/java/io/micronaut/web/router/Router.java index 04e0ad35a54..111f28c2788 100644 --- a/router/src/main/java/io/micronaut/web/router/Router.java +++ b/router/src/main/java/io/micronaut/web/router/Router.java @@ -160,7 +160,7 @@ Stream> find(@NonNull HttpRequest request) { * @param The matched route * @return The {@link RouteMatch} */ - Optional> route(@NonNull Class originatingClass, @NonNull HttpStatus status); + Optional> route(@NonNull Class originatingClass, @NonNull HttpStatus status); /** * Match a route to an error. @@ -179,7 +179,7 @@ Stream> find(@NonNull HttpRequest request) { * @param The matched route * @return The {@link RouteMatch} */ - Optional> route(@NonNull Class originatingClass, @NonNull Throwable error); + Optional> route(@NonNull Class originatingClass, @NonNull Throwable error); /** * Match a route to an error. diff --git a/router/src/main/java/io/micronaut/web/router/StatusRoute.java b/router/src/main/java/io/micronaut/web/router/StatusRoute.java index 2d9658b4555..19a67ddca1b 100644 --- a/router/src/main/java/io/micronaut/web/router/StatusRoute.java +++ b/router/src/main/java/io/micronaut/web/router/StatusRoute.java @@ -58,7 +58,7 @@ public interface StatusRoute extends MethodBasedRoute { * @param The matched route * @return The route match */ - Optional> match(Class originatingClass, HttpStatus status); + Optional> match(Class originatingClass, HttpStatus status); @Override StatusRoute consumes(MediaType... mediaType); diff --git a/router/src/main/java/io/micronaut/web/router/filter/FilteredRouter.java b/router/src/main/java/io/micronaut/web/router/filter/FilteredRouter.java index 6b3fd5868c5..9745f363999 100644 --- a/router/src/main/java/io/micronaut/web/router/filter/FilteredRouter.java +++ b/router/src/main/java/io/micronaut/web/router/filter/FilteredRouter.java @@ -123,7 +123,7 @@ public Optional> route(@NonNull HttpStatus status) { } @Override - public Optional> route(@NonNull Class originatingClass, @NonNull HttpStatus status) { + public Optional> route(@NonNull Class originatingClass, @NonNull HttpStatus status) { return router.route(originatingClass, status); } @@ -133,7 +133,7 @@ public Optional> route(@NonNull Throwable error) { } @Override - public Optional> route(@NonNull Class originatingClass, @NonNull Throwable error) { + public Optional> route(@NonNull Class originatingClass, @NonNull Throwable error) { return router.route(originatingClass, error); } diff --git a/validation/src/main/java/io/micronaut/validation/validator/DefaultAnnotatedElementValidator.java b/validation/src/main/java/io/micronaut/validation/validator/DefaultAnnotatedElementValidator.java index f5bfc222942..a59a57de896 100644 --- a/validation/src/main/java/io/micronaut/validation/validator/DefaultAnnotatedElementValidator.java +++ b/validation/src/main/java/io/micronaut/validation/validator/DefaultAnnotatedElementValidator.java @@ -68,7 +68,7 @@ private Optional findConstraintVa return validatorMap.entrySet().stream() .filter(entry -> { final ValidatorKey key = entry.getKey(); - final Class[] left = {constraintType, targetType}; + final Class[] left = {constraintType, targetType}; return TypeArgumentQualifier.areTypesCompatible( left, Arrays.asList(key.getConstraintType(), key.getTargetType()) @@ -81,7 +81,7 @@ private Map initializeValidatorMap() { validatorMap = new HashMap<>(); for (ConstraintValidator validator : SoftServiceLoader.load(ConstraintValidator.class).collectAll()) { try { - final Class[] typeArgs = GenericTypeUtils.resolveInterfaceTypeArguments(validator.getClass(), ConstraintValidator.class); + final Class[] typeArgs = GenericTypeUtils.resolveInterfaceTypeArguments(validator.getClass(), ConstraintValidator.class); if (ArrayUtils.isNotEmpty(typeArgs) && typeArgs.length == 2) { validatorMap.put( new ValidatorKey(typeArgs[0], typeArgs[1]), diff --git a/validation/src/main/java/io/micronaut/validation/validator/DefaultValidator.java b/validation/src/main/java/io/micronaut/validation/validator/DefaultValidator.java index e42d3313bb7..8aa28ff4fa7 100644 --- a/validation/src/main/java/io/micronaut/validation/validator/DefaultValidator.java +++ b/validation/src/main/java/io/micronaut/validation/validator/DefaultValidator.java @@ -913,7 +913,7 @@ private void instrumentPublisherArgumentWithValidation( } } else { - final Class t = argument.getFirstTypeVariable().map(Argument::getType).orElse(null); + final Class t = argument.getFirstTypeVariable().map(Argument::getType).orElse(null); validateParameterInternal( rootClass, object, @@ -974,7 +974,7 @@ private void instrumentCompletionStageArgumentWithValidation( } } else { - final Class t = argument.getFirstTypeVariable().map(Argument::getType).orElse(null); + final Class t = argument.getFirstTypeVariable().map(Argument::getType).orElse(null); validateParameterInternal( rootClass, object, @@ -1321,7 +1321,7 @@ private void cascadeToOne( DefaultConstraintValidatorContext context, Set overallViolations, AnnotatedElement cascadeProperty, - Class propertyType, + Class propertyType, Object propertyValue, @Nullable DefaultPropertyNode container) { @@ -1419,7 +1419,7 @@ private void validateConstrainedPropertyInternal( @Nullable T rootBean, @NonNull Object object, @NonNull AnnotatedElement constrainedProperty, - @NonNull Class propertyType, + @NonNull Class propertyType, @Nullable Object propertyValue, DefaultConstraintValidatorContext context, Set> overallViolations, @@ -1449,7 +1449,7 @@ private void validatePropertyInternal( @Nullable Object object, @NonNull DefaultConstraintValidatorContext context, @NonNull Set> overallViolations, - @NonNull Class propertyType, + @NonNull Class propertyType, @NonNull AnnotatedElement constrainedProperty, @Nullable Object propertyValue) { final AnnotationMetadata annotationMetadata = constrainedProperty.getAnnotationMetadata(); @@ -1499,7 +1499,7 @@ private void valueConstraintOnProperty( DefaultConstraintValidatorContext context, Set> overallViolations, AnnotatedElement constrainedProperty, - Class propertyType, + Class propertyType, @Nullable Object propertyValue, Class constraintType) { final AnnotationMetadata annotationMetadata = constrainedProperty @@ -1516,7 +1516,7 @@ private void valueConstraintOnProperty( constraints.add(annotationValue); } } else { - final List constraintGroups = Arrays.asList(classValues); + final List> constraintGroups = Arrays.asList(classValues); if (constraintGroups.contains(group)) { constraints.add(annotationValue); } @@ -1524,7 +1524,7 @@ private void valueConstraintOnProperty( } } - @SuppressWarnings("unchecked") final Class targetType = propertyValue != null ? (Class) propertyValue.getClass() : propertyType; + @SuppressWarnings("unchecked") final Class targetType = propertyValue != null ? (Class) propertyValue.getClass() : (Class) propertyType; final ConstraintValidator validator = constraintValidatorRegistry .findConstraintValidator(constraintType, targetType).orElse(null); if (validator != null) { @@ -1619,7 +1619,7 @@ public void validateBeanArgument( final boolean hasValid = annotationMetadata.hasStereotype(Valid.class); final boolean hasConstraint = annotationMetadata.hasStereotype(Constraint.class); final Class parameterType = argument.getType(); - final Class rootClass = injectionPoint.getDeclaringBean().getBeanType(); + final Class rootClass = injectionPoint.getDeclaringBean().getBeanType(); DefaultConstraintValidatorContext context = new DefaultConstraintValidatorContext(value); Set overallViolations = new HashSet<>(5); if (hasConstraint) { @@ -1937,7 +1937,7 @@ public ReturnType getReturnType() { } @Override - public Class getDeclaringType() { + public Class getDeclaringType() { return null; } diff --git a/validation/src/main/java/io/micronaut/validation/validator/constraints/DefaultConstraintValidators.java b/validation/src/main/java/io/micronaut/validation/validator/constraints/DefaultConstraintValidators.java index 32f431ca3a7..bfa9c255e7d 100644 --- a/validation/src/main/java/io/micronaut/validation/validator/constraints/DefaultConstraintValidators.java +++ b/validation/src/main/java/io/micronaut/validation/validator/constraints/DefaultConstraintValidators.java @@ -390,7 +390,7 @@ protected DefaultConstraintValidators(@Nullable BeanContext beanContext) { wrapper.getProperty(property.getName(), ConstraintValidator.class).ifPresent(constraintValidator -> { if (len == 2) { - final Class targetType = ReflectionUtils.getWrapperType(typeParameters[1].getType()); + final Class targetType = ReflectionUtils.getWrapperType(typeParameters[1].getType()); final ValidatorKey key = new ValidatorKey(typeParameters[0].getType(), targetType); validatorMap.put(key, constraintValidator); } else if (len == 1) { @@ -433,7 +433,7 @@ public Optional> findConstra ArgumentUtils.requireNonNull("constraintType", constraintType); ArgumentUtils.requireNonNull("targetType", targetType); final ValidatorKey key = new ValidatorKey(constraintType, targetType); - targetType = ReflectionUtils.getWrapperType(targetType); + targetType = (Class) ReflectionUtils.getWrapperType(targetType); ConstraintValidator constraintValidator = localValidators.get(key); if (constraintValidator != null) { return Optional.of(constraintValidator); @@ -447,7 +447,7 @@ public Optional> findConstra ReflectionUtils.getWrapperType(targetType) ); Class finalTargetType = targetType; - final Class[] finalTypeArguments = {constraintType, finalTargetType}; + final Class[] finalTypeArguments = {constraintType, finalTargetType}; final Optional local = localValidators.entrySet().stream().filter(entry -> { final ValidatorKey k = entry.getKey(); return TypeArgumentQualifier.areTypesCompatible( diff --git a/validation/src/main/java/io/micronaut/validation/validator/extractors/DefaultValueExtractors.java b/validation/src/main/java/io/micronaut/validation/validator/extractors/DefaultValueExtractors.java index 674acff2a06..8b0759b6a14 100644 --- a/validation/src/main/java/io/micronaut/validation/validator/extractors/DefaultValueExtractors.java +++ b/validation/src/main/java/io/micronaut/validation/validator/extractors/DefaultValueExtractors.java @@ -120,8 +120,8 @@ public class DefaultValueExtractors implements ValueExtractorRegistry { } }; - private final Map valueExtractors; - private final Set unwrapByDefaultTypes = new HashSet<>(5); + private final Map, ValueExtractor> valueExtractors; + private final Set> unwrapByDefaultTypes = new HashSet<>(5); /** * Default constructor. @@ -138,7 +138,7 @@ public DefaultValueExtractors() { @Inject protected DefaultValueExtractors(@Nullable BeanContext beanContext) { BeanWrapper wrapper = BeanWrapper.findWrapper(this).orElse(null); - Map extractorMap = new HashMap<>(); + Map, ValueExtractor> extractorMap = new HashMap<>(); if (beanContext != null && beanContext.containsBean(ValueExtractor.class)) { final Collection> valueExtractors = @@ -146,9 +146,9 @@ protected DefaultValueExtractors(@Nullable BeanContext beanContext) { if (CollectionUtils.isNotEmpty(valueExtractors)) { for (BeanRegistration reg : valueExtractors) { final ValueExtractor valueExtractor = reg.getBean(); - final Class[] typeParameters = reg.getBeanDefinition().getTypeParameters(ValueExtractor.class); + final Class[] typeParameters = reg.getBeanDefinition().getTypeParameters(ValueExtractor.class); if (ArrayUtils.isNotEmpty(typeParameters)) { - final Class targetType = typeParameters[0]; + final Class targetType = typeParameters[0]; extractorMap.put(targetType, valueExtractor); if (valueExtractor instanceof UnwrapByDefaultValueExtractor || valueExtractor.getClass().isAnnotationPresent(UnwrapByDefault.class)) { unwrapByDefaultTypes.add(targetType); diff --git a/websocket/src/main/java/io/micronaut/websocket/bind/WebSocketStateBinderRegistry.java b/websocket/src/main/java/io/micronaut/websocket/bind/WebSocketStateBinderRegistry.java index b892f17cf27..8a13e78ee64 100644 --- a/websocket/src/main/java/io/micronaut/websocket/bind/WebSocketStateBinderRegistry.java +++ b/websocket/src/main/java/io/micronaut/websocket/bind/WebSocketStateBinderRegistry.java @@ -44,7 +44,7 @@ public class WebSocketStateBinderRegistry implements ArgumentBinderRegistry> requestBinderRegistry; - private final Map> byType = new HashMap<>(5); + private final Map, ArgumentBinder> byType = new HashMap<>(5); private final ArgumentBinder> queryValueArgumentBinder; /** diff --git a/websocket/src/main/java/io/micronaut/websocket/context/DefaultWebSocketBeanRegistry.java b/websocket/src/main/java/io/micronaut/websocket/context/DefaultWebSocketBeanRegistry.java index e1223c5d436..4e4abac0d34 100644 --- a/websocket/src/main/java/io/micronaut/websocket/context/DefaultWebSocketBeanRegistry.java +++ b/websocket/src/main/java/io/micronaut/websocket/context/DefaultWebSocketBeanRegistry.java @@ -42,7 +42,7 @@ class DefaultWebSocketBeanRegistry implements WebSocketBeanRegistry { private final BeanContext beanContext; private final Class stereotype; - private final Map webSocketBeanMap = new ConcurrentHashMap<>(3); + private final Map, WebSocketBean> webSocketBeanMap = new ConcurrentHashMap<>(3); /** * Default constructor. From 332d794d0d012f2eb607d3c6d79f50ab43e65969 Mon Sep 17 00:00:00 2001 From: Denis Stepanov Date: Fri, 4 Nov 2022 18:59:54 +0700 Subject: [PATCH 196/743] Fail when the request is with the incorrect type (#8261) --- buildSrc/build.gradle | 1 + .../netty/MutableHttpRequestWrapper.java | 9 +- .../client/netty/NettyClientHttpRequest.java | 6 +- .../http/netty/NettyMutableHttpResponse.java | 38 ++-- .../http/server/netty/NettyHttpRequest.java | 36 ++-- .../server/netty/RoutingInBoundHandler.java | 43 +++- .../netty/binding/JsonBodyBindingSpec.groovy | 12 +- .../micronaut/http/server/RouteExecutor.java | 4 +- .../binding/RequestArgumentSatisfier.java | 4 +- .../io/micronaut/http/FullHttpRequest.java | 21 +- .../java/io/micronaut/http/HttpMessage.java | 17 +- .../io/micronaut/http/HttpMessageWrapper.java | 6 + .../bind/DefaultRequestBinderRegistry.java | 13 +- .../binders/DefaultBodyAnnotationBinder.java | 23 +-- .../web/router/AbstractRouteMatch.java | 187 ++++++++---------- .../web/router/DefaultRouteBuilder.java | 10 +- .../web/router/DefaultUriRouteMatch.java | 16 +- .../micronaut/web/router/ErrorRouteMatch.java | 35 ++-- .../web/router/MethodBasedRouteMatch.java | 2 +- .../io/micronaut/web/router/RouteMatch.java | 2 +- .../web/router/StatusRouteMatch.java | 12 +- .../micronaut/web/router/UriRouteMatch.java | 8 +- 22 files changed, 279 insertions(+), 226 deletions(-) diff --git a/buildSrc/build.gradle b/buildSrc/build.gradle index 06bb5a00382..fa6d75fd610 100644 --- a/buildSrc/build.gradle +++ b/buildSrc/build.gradle @@ -3,6 +3,7 @@ plugins { } repositories { + mavenCentral() gradlePluginPortal() } diff --git a/http-client/src/main/java/io/micronaut/http/client/netty/MutableHttpRequestWrapper.java b/http-client/src/main/java/io/micronaut/http/client/netty/MutableHttpRequestWrapper.java index a02019d0212..56648786cdd 100644 --- a/http-client/src/main/java/io/micronaut/http/client/netty/MutableHttpRequestWrapper.java +++ b/http-client/src/main/java/io/micronaut/http/client/netty/MutableHttpRequestWrapper.java @@ -18,9 +18,9 @@ import io.micronaut.core.annotation.Internal; import io.micronaut.core.annotation.NonNull; import io.micronaut.core.annotation.Nullable; +import io.micronaut.core.convert.ArgumentConversionContext; import io.micronaut.core.convert.ConversionContext; import io.micronaut.core.convert.ConversionService; -import io.micronaut.core.type.Argument; import io.micronaut.http.HttpRequest; import io.micronaut.http.HttpRequestWrapper; import io.micronaut.http.MutableHttpHeaders; @@ -79,13 +79,12 @@ public Optional getBody(@NonNull Class type) { } } - @NonNull @Override - public Optional getBody(@NonNull Argument type) { + public Optional getBody(ArgumentConversionContext conversionContext) { if (body == null) { - return getDelegate().getBody(type); + return getDelegate().getBody(conversionContext); } else { - return conversionService.convert(body, ConversionContext.of(type)); + return conversionService.convert(body, conversionContext); } } diff --git a/http-client/src/main/java/io/micronaut/http/client/netty/NettyClientHttpRequest.java b/http-client/src/main/java/io/micronaut/http/client/netty/NettyClientHttpRequest.java index 347fd2fe02e..95095d4ea1c 100644 --- a/http-client/src/main/java/io/micronaut/http/client/netty/NettyClientHttpRequest.java +++ b/http-client/src/main/java/io/micronaut/http/client/netty/NettyClientHttpRequest.java @@ -17,7 +17,7 @@ import io.micronaut.core.annotation.Internal; import io.micronaut.core.annotation.NonNull; -import io.micronaut.core.convert.ConversionContext; +import io.micronaut.core.convert.ArgumentConversionContext; import io.micronaut.core.convert.ConversionService; import io.micronaut.core.convert.value.MutableConvertibleValues; import io.micronaut.core.convert.value.MutableConvertibleValuesMap; @@ -175,8 +175,8 @@ public Optional getBody(Class type) { } @Override - public Optional getBody(Argument type) { - return getBody().flatMap(b -> conversionService.convert(b, ConversionContext.of(type))); + public Optional getBody(ArgumentConversionContext conversionContext) { + return getBody().flatMap(b -> conversionService.convert(b, conversionContext)); } @Override diff --git a/http-netty/src/main/java/io/micronaut/http/netty/NettyMutableHttpResponse.java b/http-netty/src/main/java/io/micronaut/http/netty/NettyMutableHttpResponse.java index 6fdf7718823..32c732b9883 100644 --- a/http-netty/src/main/java/io/micronaut/http/netty/NettyMutableHttpResponse.java +++ b/http-netty/src/main/java/io/micronaut/http/netty/NettyMutableHttpResponse.java @@ -21,7 +21,6 @@ import io.micronaut.core.annotation.TypeHint; import io.micronaut.core.async.publisher.Publishers; import io.micronaut.core.convert.ArgumentConversionContext; -import io.micronaut.core.convert.ConversionContext; import io.micronaut.core.convert.ConversionService; import io.micronaut.core.convert.value.MutableConvertibleValues; import io.micronaut.core.convert.value.MutableConvertibleValuesMap; @@ -247,10 +246,9 @@ public Optional getBody(Class type) { return getBody(Argument.of(type)); } - @SuppressWarnings("unchecked") @Override - public Optional getBody(Argument type) { - return bodyConvertor.convert(type, body); + public Optional getBody(ArgumentConversionContext conversionContext) { + return bodyConvertor.convert(conversionContext, body); } @Override @@ -355,14 +353,14 @@ private BodyConvertor newBodyConvertor() { return new BodyConvertor() { @Override - public Optional convert(Argument valueType, Object value) { + public Optional convert(ArgumentConversionContext conversionContext, Object value) { if (value == null) { return Optional.empty(); } - if (Argument.OBJECT_ARGUMENT.equalsType(valueType)) { + if (Argument.OBJECT_ARGUMENT.equalsType(conversionContext.getArgument())) { return Optional.of(value); } - return convertFromNext(conversionService, valueType, value); + return convertFromNext(conversionService, conversionContext, value); } }; @@ -372,31 +370,39 @@ private abstract static class BodyConvertor { private BodyConvertor nextConvertor; - public abstract Optional convert(Argument valueType, T value); + public abstract Optional convert(ArgumentConversionContext valueType, T value); - protected synchronized Optional convertFromNext(ConversionService conversionService, Argument conversionValueType, T value) { + protected synchronized Optional convertFromNext(ConversionService conversionService, ArgumentConversionContext conversionContext, T value) { if (nextConvertor == null) { Optional conversion; - ArgumentConversionContext context = ConversionContext.of(conversionValueType); if (value instanceof ByteBuffer) { - conversion = conversionService.convert(((ByteBuffer) value).asNativeBuffer(), context); + conversion = conversionService.convert(((ByteBuffer) value).asNativeBuffer(), conversionContext); } else { - conversion = conversionService.convert(value, context); + conversion = conversionService.convert(value, conversionContext); } nextConvertor = new BodyConvertor() { @Override - public Optional convert(Argument valueType, T value) { - if (conversionValueType.equalsType(valueType)) { + public Optional convert(ArgumentConversionContext currentConversionContext, T value) { + if (currentConversionContext == conversionContext) { return conversion; } - return convertFromNext(conversionService, valueType, value); + if (currentConversionContext.getArgument().equalsType(conversionContext.getArgument())) { + conversionContext.getLastError().ifPresent(error -> { + error.getOriginalValue().ifPresentOrElse( + originalValue -> currentConversionContext.reject(originalValue, error.getCause()), + () -> currentConversionContext.reject(error.getCause()) + ); + }); + return conversion; + } + return convertFromNext(conversionService, currentConversionContext, value); } }; return conversion; } - return nextConvertor.convert(conversionValueType, value); + return nextConvertor.convert(conversionContext, value); } public void cleanup() { diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/NettyHttpRequest.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/NettyHttpRequest.java index e1a85e6ac66..4b8c4377037 100644 --- a/http-server-netty/src/main/java/io/micronaut/http/server/netty/NettyHttpRequest.java +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/NettyHttpRequest.java @@ -19,7 +19,7 @@ import io.micronaut.core.annotation.NonNull; import io.micronaut.core.annotation.Nullable; import io.micronaut.core.async.publisher.Publishers; -import io.micronaut.core.convert.ConversionContext; +import io.micronaut.core.convert.ArgumentConversionContext; import io.micronaut.core.convert.ConversionService; import io.micronaut.core.convert.value.MutableConvertibleValues; import io.micronaut.core.convert.value.MutableConvertibleValuesMap; @@ -345,7 +345,6 @@ private String getContent(HttpData data) { return newValue; } - @SuppressWarnings("unchecked") @Override public Optional getBody(Class type) { return getBody(Argument.of(type)); @@ -353,8 +352,8 @@ public Optional getBody(Class type) { @SuppressWarnings("unchecked") @Override - public Optional getBody(Argument type) { - return getBody().flatMap(t -> bodyConvertor.convert(type, t)); + public Optional getBody(ArgumentConversionContext conversionContext) { + return getBody().flatMap(t -> bodyConvertor.convert(conversionContext, t)); } /** @@ -611,14 +610,14 @@ private BodyConvertor newBodyConvertor() { return new BodyConvertor() { @Override - public Optional convert(Argument valueType, Object value) { + public Optional convert(ArgumentConversionContext conversionContext, Object value) { if (value == null) { return Optional.empty(); } - if (Argument.OBJECT_ARGUMENT.equalsType(valueType)) { + if (Argument.OBJECT_ARGUMENT.equalsType(conversionContext.getArgument())) { return Optional.of(value); } - return convertFromNext(conversionService, valueType, value); + return convertFromNext(conversionService, conversionContext, value); } }; @@ -786,25 +785,34 @@ private abstract static class BodyConvertor { private BodyConvertor nextConvertor; - public abstract Optional convert(Argument valueType, T value); + public abstract Optional convert(ArgumentConversionContext conversionContext, T value); - protected synchronized Optional convertFromNext(ConversionService conversionService, Argument conversionValueType, T value) { + protected synchronized Optional convertFromNext(ConversionService conversionService, ArgumentConversionContext conversionContext, T value) { if (nextConvertor == null) { - Optional conversion = conversionService.convert(value, ConversionContext.of(conversionValueType)); + Optional conversion = conversionService.convert(value, conversionContext); nextConvertor = new BodyConvertor() { @Override - public Optional convert(Argument valueType, T value) { - if (conversionValueType.equalsType(valueType)) { + public Optional convert(ArgumentConversionContext currentConversionContext, T value) { + if (currentConversionContext == conversionContext) { + return conversion; + } + if (currentConversionContext.getArgument().equalsType(conversionContext.getArgument())) { + conversionContext.getLastError().ifPresent(error -> { + error.getOriginalValue().ifPresentOrElse( + originalValue -> currentConversionContext.reject(originalValue, error.getCause()), + () -> currentConversionContext.reject(error.getCause()) + ); + }); return conversion; } - return convertFromNext(conversionService, valueType, value); + return convertFromNext(conversionService, currentConversionContext, value); } }; return conversion; } - return nextConvertor.convert(conversionValueType, value); + return nextConvertor.convert(conversionContext, value); } public void cleanup() { diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/RoutingInBoundHandler.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/RoutingInBoundHandler.java index 9f8bb5c9dd7..2fd1391d220 100644 --- a/http-server-netty/src/main/java/io/micronaut/http/server/netty/RoutingInBoundHandler.java +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/RoutingInBoundHandler.java @@ -53,6 +53,7 @@ import io.micronaut.http.server.RouteExecutor; import io.micronaut.http.server.binding.RequestArgumentSatisfier; import io.micronaut.http.server.exceptions.InternalServerException; +import io.micronaut.http.server.multipart.MultipartBody; import io.micronaut.http.server.netty.configuration.NettyHttpServerConfiguration; import io.micronaut.http.server.netty.multipart.NettyCompletedFileUpload; import io.micronaut.http.server.netty.multipart.NettyPartData; @@ -63,6 +64,7 @@ import io.micronaut.http.server.netty.types.files.NettySystemFileCustomizableResponseType; import io.micronaut.http.server.types.files.FileCustomizableResponseType; import io.micronaut.runtime.http.codec.TextPlainCodec; +import io.micronaut.web.router.MethodBasedRouteMatch; import io.micronaut.web.router.RouteInfo; import io.micronaut.web.router.RouteMatch; import io.micronaut.web.router.resource.StaticResourceResolver; @@ -113,6 +115,7 @@ import java.nio.channels.ClosedChannelException; import java.nio.file.Paths; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.List; @@ -292,16 +295,7 @@ protected void channelRead0(ChannelHandlerContext ctx, io.micronaut.http.HttpReq } // try to fulfill the argument requirements of the route RouteMatch route = requestArgumentSatisfier.fulfillArgumentRequirements(routeMatch, httpRequest, false); - - Optional> bodyArgument = route.getBodyArgument() - .filter(argument -> argument.getAnnotationMetadata().hasAnnotation(Body.class)); - - // The request body is required, so at this point we must have a StreamedHttpRequest - io.netty.handler.codec.http.HttpRequest nativeRequest1 = nettyHttpRequest.getNativeRequest(); - if (!route.isExecutable() && - HttpMethod.permitsRequestBody(nettyHttpRequest.getMethod()) && - nativeRequest1 instanceof StreamedHttpRequest && - (bodyArgument.isEmpty() || !route.isSatisfied(bodyArgument.get().getName()))) { + if (shouldReadBody(nettyHttpRequest, route)) { return ReactiveExecutionFlow.fromPublisher( Mono.create(emitter -> httpContentProcessorResolver.resolve(nettyHttpRequest, route) .subscribe(buildSubscriber(nettyHttpRequest, route, emitter)) @@ -362,6 +356,35 @@ private void writeResponse(ChannelHandlerContext ctx, } } + private boolean shouldReadBody(NettyHttpRequest nettyHttpRequest, RouteMatch routeMatch) { + if (!HttpMethod.permitsRequestBody(nettyHttpRequest.getMethod())) { + return false; + } + io.netty.handler.codec.http.HttpRequest nativeRequest = nettyHttpRequest.getNativeRequest(); + if (!(nativeRequest instanceof StreamedHttpRequest)) { + // Illegal state: The request body is required, so at this point we must have a StreamedHttpRequest + return false; + } + if (routeMatch instanceof MethodBasedRouteMatch methodBasedRouteMatch) { + if (Arrays.stream(methodBasedRouteMatch.getArguments()).anyMatch(argument -> MultipartBody.class.equals(argument.getType()))) { + // MultipartBody will subscribe to the request body in MultipartBodyArgumentBinder + return false; + } + if (Arrays.stream(methodBasedRouteMatch.getArguments()).anyMatch(argument -> HttpRequest.class.equals(argument.getType()))) { + // HttpRequest argument in the method + return true; + } + } + Optional> bodyArgument = routeMatch.getBodyArgument() + .filter(argument -> argument.getAnnotationMetadata().hasAnnotation(Body.class)); + if (bodyArgument.isPresent() && !routeMatch.isSatisfied(bodyArgument.get().getName())) { + // Body argument in the method + return true; + } + // Might be some body parts + return !routeMatch.isExecutable(); + } + @Override public FileCustomizableResponseType find(HttpRequest httpRequest) { Optional optionalUrl = staticResourceResolver.resolve(httpRequest.getUri().getPath()); diff --git a/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/binding/JsonBodyBindingSpec.groovy b/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/binding/JsonBodyBindingSpec.groovy index 293d6058d04..e61e0399f7a 100644 --- a/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/binding/JsonBodyBindingSpec.groovy +++ b/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/binding/JsonBodyBindingSpec.groovy @@ -273,20 +273,22 @@ class JsonBodyBindingSpec extends AbstractMicronautSpec { )).blockFirst() then: - HttpClientResponseException ex = thrown() - ex.response.code() == HttpStatus.BAD_REQUEST.code - ex.response.getBody(Map).get()._embedded.errors[0].message.contains("Required argument [HttpRequest request] not specified") + response.code() == HttpStatus.OK.code + response.body() == 'not found' } void "test request generic type conversion error"() { when: String json = '[1,2,3]' - HttpResponse response = Flux.from(rxClient.exchange( + Flux.from(rxClient.exchange( HttpRequest.POST('/json/request-generic', json), String )).blockFirst() then: - response.body() == "not found" + def e = thrown(HttpClientResponseException) + def response = e.response + response.getStatus() == HttpStatus.BAD_REQUEST + response.body().toString().contains("no int/Int-argument constructor/factory method to deserialize from Number value") } @Issue("https://github.com/micronaut-projects/micronaut-core/issues/5088") diff --git a/http-server/src/main/java/io/micronaut/http/server/RouteExecutor.java b/http-server/src/main/java/io/micronaut/http/server/RouteExecutor.java index c0ac9b4860b..7cd08f95256 100644 --- a/http-server/src/main/java/io/micronaut/http/server/RouteExecutor.java +++ b/http-server/src/main/java/io/micronaut/http/server/RouteExecutor.java @@ -20,6 +20,7 @@ import io.micronaut.core.annotation.Internal; import io.micronaut.core.annotation.NonNull; import io.micronaut.core.annotation.Nullable; +import io.micronaut.http.codec.CodecException; import io.micronaut.http.reactive.execution.ReactiveExecutionFlow; import io.micronaut.core.async.publisher.Publishers; import io.micronaut.core.execution.ExecutionFlow; @@ -647,8 +648,9 @@ private RouteMatch findErrorRoute(Throwable cause, if (errorRoute == null) { // Second try is by status route if the status is known HttpStatus errorStatus = null; - if (cause instanceof UnsatisfiedRouteException) { + if (cause instanceof UnsatisfiedRouteException || cause instanceof CodecException) { // when arguments do not match, then there is UnsatisfiedRouteException, we can handle this with a routed bad request + // or when incoming request body is not in the expected format errorStatus = HttpStatus.BAD_REQUEST; } else if (cause instanceof HttpStatusException) { errorStatus = ((HttpStatusException) cause).getStatus(); diff --git a/http-server/src/main/java/io/micronaut/http/server/binding/RequestArgumentSatisfier.java b/http-server/src/main/java/io/micronaut/http/server/binding/RequestArgumentSatisfier.java index cb0d672a877..bda01e02d35 100644 --- a/http-server/src/main/java/io/micronaut/http/server/binding/RequestArgumentSatisfier.java +++ b/http-server/src/main/java/io/micronaut/http/server/binding/RequestArgumentSatisfier.java @@ -73,7 +73,7 @@ public RequestBinderRegistry getBinderRegistry() { * @return The route */ public RouteMatch fulfillArgumentRequirements(RouteMatch route, HttpRequest request, boolean satisfyOptionals) { - Collection requiredArguments = route.getRequiredArguments(); + Collection> requiredArguments = route.getRequiredArguments(); Map argumentValues; if (requiredArguments.isEmpty()) { @@ -82,7 +82,7 @@ public RouteMatch fulfillArgumentRequirements(RouteMatch route, HttpReques } else { argumentValues = new LinkedHashMap<>(requiredArguments.size()); // Begin try fulfilling the argument requirements - for (Argument argument : requiredArguments) { + for (Argument argument : requiredArguments) { getValueForArgument(argument, request, satisfyOptionals).ifPresent(value -> argumentValues.put(argument.getName(), value)); } diff --git a/http/src/main/java/io/micronaut/http/FullHttpRequest.java b/http/src/main/java/io/micronaut/http/FullHttpRequest.java index 5f30d42d0da..7a2f514e491 100644 --- a/http/src/main/java/io/micronaut/http/FullHttpRequest.java +++ b/http/src/main/java/io/micronaut/http/FullHttpRequest.java @@ -15,6 +15,10 @@ */ package io.micronaut.http; +import io.micronaut.core.convert.ArgumentConversionContext; +import io.micronaut.core.convert.ConversionContext; +import io.micronaut.core.convert.ConversionError; +import io.micronaut.core.convert.exceptions.ConversionErrorException; import io.micronaut.core.type.Argument; import java.util.Optional; @@ -42,6 +46,21 @@ public FullHttpRequest(HttpRequest delegate, @Override public Optional getBody() { - return super.getBody(bodyType); + ArgumentConversionContext conversionContext = ConversionContext.of(bodyType); + Optional body = getBody(conversionContext); + if (conversionContext.hasErrors()) { + Exception cause = null; + Optional lastError = conversionContext.getLastError(); + if (lastError.isPresent()) { + ConversionError conversionError = lastError.get(); + cause = conversionError.getCause(); + } + if (cause instanceof RuntimeException runtimeException) { + throw runtimeException; + } else if (cause != null) { + throw new ConversionErrorException(bodyType, cause); + } + } + return body; } } diff --git a/http/src/main/java/io/micronaut/http/HttpMessage.java b/http/src/main/java/io/micronaut/http/HttpMessage.java index 9b0464b172b..a4523dc310d 100644 --- a/http/src/main/java/io/micronaut/http/HttpMessage.java +++ b/http/src/main/java/io/micronaut/http/HttpMessage.java @@ -16,6 +16,7 @@ package io.micronaut.http; import io.micronaut.core.attr.MutableAttributeHolder; +import io.micronaut.core.convert.ArgumentConversionContext; import io.micronaut.core.convert.ConversionContext; import io.micronaut.core.convert.ConversionService; import io.micronaut.core.convert.value.MutableConvertibleValues; @@ -80,8 +81,20 @@ public interface HttpMessage extends MutableAttributeHolder { * @return An {@link Optional} of the type or {@link Optional#empty()} if the body cannot be returned as the given type */ default @NonNull Optional getBody(@NonNull Argument type) { - ArgumentUtils.requireNonNull("type", type); - return getBody().flatMap(b -> ConversionService.SHARED.convert(b, ConversionContext.of(type))); + return getBody(ConversionContext.of(type)); + } + + /** + * Return the body, will use the provided conversion context if needed. + * + * @param conversionContext The body conversion context + * @param The generic type + * @return An {@link Optional} of the type or {@link Optional#empty()} if the body cannot be returned as the given type + * @since 4.0.0 + */ + default @NonNull Optional getBody(@NonNull ArgumentConversionContext conversionContext) { + ArgumentUtils.requireNonNull("conversionContext", conversionContext); + return getBody().flatMap(b -> ConversionService.SHARED.convert(b, conversionContext)); } /** diff --git a/http/src/main/java/io/micronaut/http/HttpMessageWrapper.java b/http/src/main/java/io/micronaut/http/HttpMessageWrapper.java index a5363fe0aaa..b6a111610a2 100644 --- a/http/src/main/java/io/micronaut/http/HttpMessageWrapper.java +++ b/http/src/main/java/io/micronaut/http/HttpMessageWrapper.java @@ -15,6 +15,7 @@ */ package io.micronaut.http; +import io.micronaut.core.convert.ArgumentConversionContext; import io.micronaut.core.convert.value.MutableConvertibleValues; import io.micronaut.core.type.Argument; @@ -69,4 +70,9 @@ public Optional getBody(Class type) { public Optional getBody(Argument type) { return delegate.getBody(type); } + + @Override + public Optional getBody(ArgumentConversionContext conversionContext) { + return delegate.getBody(conversionContext); + } } diff --git a/http/src/main/java/io/micronaut/http/bind/DefaultRequestBinderRegistry.java b/http/src/main/java/io/micronaut/http/bind/DefaultRequestBinderRegistry.java index a38e8402512..b45559ec6e2 100644 --- a/http/src/main/java/io/micronaut/http/bind/DefaultRequestBinderRegistry.java +++ b/http/src/main/java/io/micronaut/http/bind/DefaultRequestBinderRegistry.java @@ -99,19 +99,16 @@ public DefaultRequestBinderRegistry(ConversionService conversionService, List) (argument, source) -> () -> Optional.of(source.getHeaders())); - byType.put(Argument.of(HttpRequest.class).typeHashCode(), (RequestArgumentBinder) (argument, source) -> { - Optional> typeVariable = argument.getFirstTypeVariable() + byType.put(Argument.of(HttpRequest.class).typeHashCode(), (RequestArgumentBinder>) (argument, source) -> { + if (HttpMethod.permitsRequestBody(source.getMethod())) { + Optional> typeVariable = argument.getFirstTypeVariable() .filter(arg -> arg.getType() != Object.class) .filter(arg -> arg.getType() != Void.class); - if (typeVariable.isPresent() && HttpMethod.permitsRequestBody(source.getMethod())) { - if (source.getBody().isPresent()) { + if (typeVariable.isPresent()) { return () -> Optional.of(new FullHttpRequest(source, typeVariable.get())); - } else { - return ArgumentBinder.BindingResult.UNSATISFIED; } - } else { - return () -> Optional.of(source); } + return () -> Optional.of(source); }); byType.put(Argument.of(PushCapableHttpRequest.class).typeHashCode(), (RequestArgumentBinder) (argument, source) -> { if (source instanceof PushCapableHttpRequest) { diff --git a/http/src/main/java/io/micronaut/http/bind/binders/DefaultBodyAnnotationBinder.java b/http/src/main/java/io/micronaut/http/bind/binders/DefaultBodyAnnotationBinder.java index 9c8e06dcd32..4857e3a33a5 100644 --- a/http/src/main/java/io/micronaut/http/bind/binders/DefaultBodyAnnotationBinder.java +++ b/http/src/main/java/io/micronaut/http/bind/binders/DefaultBodyAnnotationBinder.java @@ -64,21 +64,18 @@ public BindingResult bind(ArgumentConversionContext context, HttpRequest value = values.get(component, context); return newResult(value.orElse(null), context); - } else { - //noinspection unchecked - return BindingResult.EMPTY; - } - } else { - Optional body = source.getBody(); - if (!body.isPresent()) { - - return BindingResult.EMPTY; - } else { - Object o = body.get(); - Optional converted = conversionService.convert(o, context); - return newResult(converted.orElse(null), context); } + //noinspection unchecked + return BindingResult.EMPTY; + } + Optional body = source.getBody(); + if (body.isEmpty()) { + //noinspection unchecked + return BindingResult.EMPTY; } + Object o = body.get(); + Optional converted = conversionService.convert(o, context); + return newResult(converted.orElse(null), context); } @SuppressWarnings("java:S3655") // false positive diff --git a/router/src/main/java/io/micronaut/web/router/AbstractRouteMatch.java b/router/src/main/java/io/micronaut/web/router/AbstractRouteMatch.java index 46b72a7e2e1..1138f3e8370 100644 --- a/router/src/main/java/io/micronaut/web/router/AbstractRouteMatch.java +++ b/router/src/main/java/io/micronaut/web/router/AbstractRouteMatch.java @@ -126,15 +126,12 @@ public AnnotationMetadata getAnnotationMetadata() { return executableMethod.getAnnotationMetadata(); } - @SuppressWarnings("unchecked") @Override public Optional> getBodyArgument() { - Argument arg = abstractRoute.bodyArgument; if (arg != null) { return Optional.of(arg); } - String bodyArgument = abstractRoute.bodyArgumentName; if (bodyArgument != null) { return Optional.ofNullable(abstractRoute.requiredInputs.get(bodyArgument)); @@ -147,7 +144,6 @@ public boolean isRequiredInput(String name) { return abstractRoute.requiredInputs.containsKey(name); } - @SuppressWarnings("unchecked") @Override public Optional> getRequiredInput(String name) { return Optional.ofNullable(abstractRoute.requiredInputs.get(name)); @@ -156,13 +152,12 @@ public Optional> getRequiredInput(String name) { @Override public boolean isExecutable() { Map variables = getVariableValues(); - for (Map.Entry entry : abstractRoute.requiredInputs.entrySet()) { + for (Map.Entry> entry : abstractRoute.requiredInputs.entrySet()) { Object value = variables.get(entry.getKey()); if (value == null || value instanceof UnresolvedArgument) { return false; } } - Optional> bodyArgument = getBodyArgument(); if (bodyArgument.isPresent()) { Object value = variables.get(bodyArgument.get().getName()); @@ -187,7 +182,7 @@ public Class getDeclaringType() { } @Override - public Argument[] getArguments() { + public Argument[] getArguments() { return executableMethod.getArguments(); } @@ -208,9 +203,7 @@ public ReturnType getReturnType() { @Override public R invoke(Object... arguments) { - ConversionService conversionService = this.conversionService; - - Argument[] targetArguments = getArguments(); + Argument[] targetArguments = getArguments(); if (targetArguments.length == 0) { return executableMethod.invoke(); } else { @@ -240,71 +233,61 @@ public R invoke(Object... arguments) { @Override public R execute(Map argumentValues) { - Argument[] targetArguments = getArguments(); - + Argument[] targetArguments = getArguments(); if (targetArguments.length == 0) { return executableMethod.invoke(); - } else { - ConversionService conversionService = this.conversionService; - Map uriVariables = getVariableValues(); - List argumentList = new ArrayList<>(argumentValues.size()); - - for (Map.Entry entry : abstractRoute.requiredInputs.entrySet()) { - Argument argument = entry.getValue(); - String name = entry.getKey(); - Object value = DefaultRouteBuilder.NO_VALUE; - if (uriVariables.containsKey(name)) { - value = uriVariables.get(name); - } else if (argumentValues.containsKey(name)) { - value = argumentValues.get(name); - } - - Class argumentType = argument.getType(); - if (value instanceof UnresolvedArgument) { - UnresolvedArgument unresolved = (UnresolvedArgument) value; - ArgumentBinder.BindingResult bindingResult = unresolved.get(); - + } + Map uriVariables = getVariableValues(); + List argumentList = new ArrayList<>(argumentValues.size()); + + for (Map.Entry> entry : abstractRoute.requiredInputs.entrySet()) { + Argument argument = entry.getValue(); + String name = entry.getKey(); + Object value = DefaultRouteBuilder.NO_VALUE; + if (uriVariables.containsKey(name)) { + value = uriVariables.get(name); + } else if (argumentValues.containsKey(name)) { + value = argumentValues.get(name); + } - if (bindingResult.isPresentAndSatisfied()) { - Object resolved = bindingResult.get(); - if (resolved instanceof ConversionError) { - ConversionError conversionError = (ConversionError) resolved; - throw new ConversionErrorException(argument, conversionError); - } else { - convertValueAndAddToList(conversionService, argumentList, argument, resolved, argumentType); - } + Class argumentType = argument.getType(); + if (value instanceof UnresolvedArgument unresolved) { + ArgumentBinder.BindingResult bindingResult = unresolved.get(); + if (bindingResult.isPresentAndSatisfied()) { + Object resolved = bindingResult.get(); + if (resolved instanceof ConversionError conversionError) { + throw new ConversionErrorException(argument, conversionError); } else { - if (argument.isNullable()) { - argumentList.add(null); - } else { - - List conversionErrors = bindingResult.getConversionErrors(); - if (!conversionErrors.isEmpty()) { - // should support multiple errors - ConversionError conversionError = conversionErrors.iterator().next(); - throw new ConversionErrorException(argument, conversionError); - } else { - throw UnsatisfiedRouteException.create(argument); - } - } - + convertValueAndAddToList(conversionService, argumentList, argument, resolved, argumentType); } - } else if (value instanceof NullArgument) { - argumentList.add(null); - } else if (value instanceof ConversionError) { - throw new ConversionErrorException(argument, (ConversionError) value); - } else if (value == DefaultRouteBuilder.NO_VALUE) { - throw UnsatisfiedRouteException.create(argument); } else { - convertValueAndAddToList(conversionService, argumentList, argument, value, argumentType); + if (argument.isNullable()) { + argumentList.add(null); + } else { + List conversionErrors = bindingResult.getConversionErrors(); + if (!conversionErrors.isEmpty()) { + // should support multiple errors + ConversionError conversionError = conversionErrors.iterator().next(); + throw new ConversionErrorException(argument, conversionError); + } + throw UnsatisfiedRouteException.create(argument); + } } + } else if (value instanceof NullArgument) { + argumentList.add(null); + } else if (value instanceof ConversionError conversionError) { + throw new ConversionErrorException(argument, conversionError); + } else if (value == DefaultRouteBuilder.NO_VALUE) { + throw UnsatisfiedRouteException.create(argument); + } else { + convertValueAndAddToList(conversionService, argumentList, argument, value, argumentType); } - - return executableMethod.invoke(argumentList.toArray()); } + + return executableMethod.invoke(argumentList.toArray()); } - private void convertValueAndAddToList(ConversionService conversionService, List argumentList, Argument argument, Object value, Class argumentType) { + private void convertValueAndAddToList(ConversionService conversionService, List argumentList, Argument argument, Object value, Class argumentType) { if (argumentType.isInstance(value)) { if (argument.isContainerType()) { if (argument.hasTypeVariables()) { @@ -365,49 +348,44 @@ public boolean explicitlyProduces(MediaType contentType) { public RouteMatch fulfill(Map argumentValues) { if (CollectionUtils.isEmpty(argumentValues)) { return this; - } else { - Map oldVariables = getVariableValues(); - Map newVariables = new LinkedHashMap<>(oldVariables); - final Argument bodyArgument = getBodyArgument().orElse(null); - Argument[] arguments = getArguments(); - Collection requiredArguments = getRequiredArguments(); - boolean hasRequiredArguments = CollectionUtils.isNotEmpty(requiredArguments); - for (Argument requiredArgument : arguments) { - - String argumentName = requiredArgument.getName(); - if (argumentValues.containsKey(argumentName)) { - - Object value = argumentValues.get(argumentName); - if (bodyArgument != null && bodyArgument.getName().equals(argumentName)) { - requiredArgument = bodyArgument; - } - - if (hasRequiredArguments) { - requiredArguments.remove(requiredArgument); - } - - if (value != null) { - String name = abstractRoute.resolveInputName(requiredArgument); - if (value instanceof UnresolvedArgument || value instanceof NullArgument) { + } + Map oldVariables = getVariableValues(); + Map newVariables = new LinkedHashMap<>(oldVariables); + final Argument bodyArgument = getBodyArgument().orElse(null); + Collection> requiredArguments = getRequiredArguments(); + boolean hasRequiredArguments = CollectionUtils.isNotEmpty(requiredArguments); + requiredArguments = hasRequiredArguments ? new ArrayList<>(requiredArguments) : requiredArguments; + for (Argument requiredArgument : getArguments()) { + String argumentName = requiredArgument.getName(); + if (argumentValues.containsKey(argumentName)) { + Object value = argumentValues.get(argumentName); + if (bodyArgument != null && bodyArgument.getName().equals(argumentName)) { + requiredArgument = bodyArgument; + } + if (hasRequiredArguments) { + requiredArguments.remove(requiredArgument); + } + if (value != null) { + String name = abstractRoute.resolveInputName(requiredArgument); + if (value instanceof UnresolvedArgument || value instanceof NullArgument) { + newVariables.put(name, value); + } else { + Class type = requiredArgument.getType(); + if (type.isInstance(value)) { newVariables.put(name, value); } else { - Class type = requiredArgument.getType(); - if (type.isInstance(value)) { - newVariables.put(name, value); - } else { - ArgumentConversionContext conversionContext = ConversionContext.of(requiredArgument); - Optional converted = conversionService.convert(value, conversionContext); - Object result = converted.isPresent() ? converted.get() : conversionContext.getLastError().orElse(null); - if (result != null) { - newVariables.put(name, result); - } + ArgumentConversionContext conversionContext = ConversionContext.of(requiredArgument); + Optional converted = conversionService.convert(value, conversionContext); + Object result = converted.isPresent() ? converted.get() : conversionContext.getLastError().orElse(null); + if (result != null) { + newVariables.put(name, result); } } } } } - return newFulfilled(newVariables, (List) requiredArguments); } + return newFulfilled(newVariables, (List>) requiredArguments); } @Override @@ -426,18 +404,17 @@ public boolean isWebSocketRoute() { * @param result An optional result * @return The resolved value or an error */ - protected Object resolveValueOrError(Argument argument, ConversionContext conversionContext, Optional result) { - if (!result.isPresent()) { + protected Object resolveValueOrError(Argument argument, ConversionContext conversionContext, Optional result) { + if (result.isEmpty()) { Optional lastError = conversionContext.getLastError(); - if (!lastError.isPresent() && argument.isDeclaredNullable()) { + if (lastError.isEmpty() && argument.isDeclaredNullable()) { return null; } throw lastError.map(conversionError -> (RuntimeException) new ConversionErrorException(argument, conversionError)).orElseGet(() -> UnsatisfiedRouteException.create(argument) ); - } else { - return result.get(); } + return result.get(); } /** @@ -445,6 +422,6 @@ protected Object resolveValueOrError(Argument argument, ConversionContext conver * @param requiredArguments The required arguments * @return A RouteMatch */ - protected abstract RouteMatch newFulfilled(Map newVariables, List requiredArguments); + protected abstract RouteMatch newFulfilled(Map newVariables, List> requiredArguments); } diff --git a/router/src/main/java/io/micronaut/web/router/DefaultRouteBuilder.java b/router/src/main/java/io/micronaut/web/router/DefaultRouteBuilder.java index 7488c339517..2c50de09bd0 100644 --- a/router/src/main/java/io/micronaut/web/router/DefaultRouteBuilder.java +++ b/router/src/main/java/io/micronaut/web/router/DefaultRouteBuilder.java @@ -423,7 +423,7 @@ abstract class AbstractRoute implements MethodBasedRoute, RouteInfo { protected List producesMediaTypes; protected String bodyArgumentName; protected Argument bodyArgument; - protected final Map requiredInputs; + protected final Map> requiredInputs; protected final Class declaringType; protected boolean consumesMediaTypesContainsAll; protected boolean producesMediaTypesContainsAll; @@ -456,15 +456,15 @@ abstract class AbstractRoute implements MethodBasedRoute, RouteInfo { isVoid = RouteInfo.super.isVoid(); specifiedSingle = RouteInfo.super.isSpecifiedSingle(); isAsyncOrReactive = RouteInfo.super.isAsyncOrReactive(); - for (Argument argument : targetMethod.getArguments()) { + for (Argument argument : targetMethod.getArguments()) { if (argument.getAnnotationMetadata().hasAnnotation(Body.class)) { this.bodyArgument = argument; } } - Argument[] requiredArguments = targetMethod.getArguments(); + Argument[] requiredArguments = targetMethod.getArguments(); if (requiredArguments.length > 0) { - Map requiredInputs = new LinkedHashMap<>(requiredArguments.length); - for (Argument requiredArgument : requiredArguments) { + Map> requiredInputs = new LinkedHashMap<>(requiredArguments.length); + for (Argument requiredArgument : requiredArguments) { String inputName = resolveInputName(requiredArgument); requiredInputs.put(inputName, requiredArgument); } diff --git a/router/src/main/java/io/micronaut/web/router/DefaultUriRouteMatch.java b/router/src/main/java/io/micronaut/web/router/DefaultUriRouteMatch.java index 9349cc21ed3..a0c0bb0324f 100644 --- a/router/src/main/java/io/micronaut/web/router/DefaultUriRouteMatch.java +++ b/router/src/main/java/io/micronaut/web/router/DefaultUriRouteMatch.java @@ -67,17 +67,17 @@ class DefaultUriRouteMatch extends AbstractRouteMatch implements Uri @Override public UriRouteMatch decorate(Function, R> executor) { Map variables = getVariableValues(); - List arguments = getRequiredArguments(); - RouteMatch thisRoute = this; - return new DefaultUriRouteMatch(matchInfo, uriRoute, defaultCharset, conversionService) { + List> arguments = getRequiredArguments(); + RouteMatch thisRoute = this; + return new DefaultUriRouteMatch<>(matchInfo, uriRoute, defaultCharset, conversionService) { @Override - public List getRequiredArguments() { + public List> getRequiredArguments() { return arguments; } @Override - public R execute(Map argumentValues) { - return (R) executor.apply(thisRoute); + public R execute(Map argumentValues) { + return executor.apply(thisRoute); } @Override @@ -88,11 +88,11 @@ public Map getVariableValues() { } @Override - protected RouteMatch newFulfilled(Map newVariables, List requiredArguments) { + protected RouteMatch newFulfilled(Map newVariables, List> requiredArguments) { return new DefaultUriRouteMatch(matchInfo, uriRoute, defaultCharset, conversionService) { @Override - public List getRequiredArguments() { + public List> getRequiredArguments() { return requiredArguments; } diff --git a/router/src/main/java/io/micronaut/web/router/ErrorRouteMatch.java b/router/src/main/java/io/micronaut/web/router/ErrorRouteMatch.java index 7e7f0e04c9f..dcc0e20a2e8 100644 --- a/router/src/main/java/io/micronaut/web/router/ErrorRouteMatch.java +++ b/router/src/main/java/io/micronaut/web/router/ErrorRouteMatch.java @@ -19,13 +19,12 @@ import io.micronaut.core.convert.ConversionService; import io.micronaut.core.type.Argument; -import java.util.Arrays; +import java.util.ArrayList; import java.util.Collection; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.function.Function; -import java.util.stream.Collectors; /** * Represents a match for an error. @@ -50,7 +49,7 @@ class ErrorRouteMatch extends AbstractRouteMatch { super(abstractRoute, conversionService); this.error = error; this.variables = new LinkedHashMap<>(); - for (Argument argument : getArguments()) { + for (Argument argument : getArguments()) { if (argument.getType().isInstance(error)) { variables.put(argument.getName(), error); } @@ -58,11 +57,15 @@ class ErrorRouteMatch extends AbstractRouteMatch { } @Override - public Collection getRequiredArguments() { - return Arrays - .stream(getArguments()) - .filter(argument -> !argument.getType().isInstance(error)) - .collect(Collectors.toList()); + public Collection> getRequiredArguments() { + Argument[] arguments = getArguments(); + List> list = new ArrayList<>(arguments.length); + for (Argument argument : arguments) { + if (!argument.getType().isInstance(error)) { + list.add(argument); + } + } + return list; } @Override @@ -76,10 +79,10 @@ public boolean isErrorRoute() { } @Override - protected RouteMatch newFulfilled(Map newVariables, List requiredArguments) { + protected RouteMatch newFulfilled(Map newVariables, List> requiredArguments) { return new ErrorRouteMatch(error, abstractRoute, conversionService) { @Override - public Collection getRequiredArguments() { + public Collection> getRequiredArguments() { return requiredArguments; } @@ -93,17 +96,17 @@ public Map getVariableValues() { @Override public RouteMatch decorate(Function, R> executor) { Map variables = getVariableValues(); - Collection arguments = getRequiredArguments(); - RouteMatch thisRoute = this; - return new ErrorRouteMatch(error, abstractRoute, conversionService) { + Collection> arguments = getRequiredArguments(); + RouteMatch thisRoute = this; + return new ErrorRouteMatch<>(error, abstractRoute, conversionService) { @Override - public Collection getRequiredArguments() { + public Collection> getRequiredArguments() { return arguments; } @Override - public T execute(Map argumentValues) { - return (T) executor.apply(thisRoute); + public R execute(Map argumentValues) { + return executor.apply(thisRoute); } @Override diff --git a/router/src/main/java/io/micronaut/web/router/MethodBasedRouteMatch.java b/router/src/main/java/io/micronaut/web/router/MethodBasedRouteMatch.java index 693935a8523..4623bc41d10 100644 --- a/router/src/main/java/io/micronaut/web/router/MethodBasedRouteMatch.java +++ b/router/src/main/java/io/micronaut/web/router/MethodBasedRouteMatch.java @@ -38,7 +38,7 @@ public interface MethodBasedRouteMatch extends RouteMatch, MethodExecut * @return The required arguments in order to invoke this route */ @Override - default Collection getRequiredArguments() { + default Collection> getRequiredArguments() { return Arrays.asList(getArguments()); } } diff --git a/router/src/main/java/io/micronaut/web/router/RouteMatch.java b/router/src/main/java/io/micronaut/web/router/RouteMatch.java index cb31d0f6df4..0416f2d6bee 100644 --- a/router/src/main/java/io/micronaut/web/router/RouteMatch.java +++ b/router/src/main/java/io/micronaut/web/router/RouteMatch.java @@ -91,7 +91,7 @@ public interface RouteMatch extends Callable, Predicate, Rout * * @return The required arguments in order to invoke this route */ - default Collection getRequiredArguments() { + default Collection> getRequiredArguments() { return Collections.emptyList(); } diff --git a/router/src/main/java/io/micronaut/web/router/StatusRouteMatch.java b/router/src/main/java/io/micronaut/web/router/StatusRouteMatch.java index 5470fcb0ab9..d912820ffaa 100644 --- a/router/src/main/java/io/micronaut/web/router/StatusRouteMatch.java +++ b/router/src/main/java/io/micronaut/web/router/StatusRouteMatch.java @@ -33,7 +33,7 @@ class StatusRouteMatch extends AbstractRouteMatch { final HttpStatus httpStatus; - private final ArrayList requiredArguments; + private final ArrayList> requiredArguments; /** * @param httpStatus The HTTP status @@ -52,7 +52,7 @@ public Map getVariableValues() { } @Override - public Collection getRequiredArguments() { + public Collection> getRequiredArguments() { return requiredArguments; } @@ -67,10 +67,10 @@ public HttpStatus findStatus(HttpStatus defaultStatus) { } @Override - protected RouteMatch newFulfilled(Map newVariables, List requiredArguments) { + protected RouteMatch newFulfilled(Map newVariables, List> requiredArguments) { return new StatusRouteMatch(httpStatus, abstractRoute, conversionService) { @Override - public Collection getRequiredArguments() { + public Collection> getRequiredArguments() { return requiredArguments; } @@ -84,11 +84,11 @@ public Map getVariableValues() { @Override public RouteMatch decorate(Function, R> executor) { Map variables = getVariableValues(); - Collection arguments = getRequiredArguments(); + Collection> arguments = getRequiredArguments(); RouteMatch thisRoute = this; return new StatusRouteMatch(httpStatus, abstractRoute, conversionService) { @Override - public Collection getRequiredArguments() { + public Collection> getRequiredArguments() { return arguments; } diff --git a/router/src/main/java/io/micronaut/web/router/UriRouteMatch.java b/router/src/main/java/io/micronaut/web/router/UriRouteMatch.java index cc4656e57ee..e32a8b21761 100644 --- a/router/src/main/java/io/micronaut/web/router/UriRouteMatch.java +++ b/router/src/main/java/io/micronaut/web/router/UriRouteMatch.java @@ -47,12 +47,12 @@ public interface UriRouteMatch extends UriMatchInfo, MethodBasedRouteMatch * @return The required arguments in order to invoke this route */ @Override - default List getRequiredArguments() { - Argument[] arguments = getArguments(); + default List> getRequiredArguments() { + Argument[] arguments = getArguments(); if (ArrayUtils.isNotEmpty(arguments)) { Map matchVariables = getVariableValues(); - List actualArguments = new ArrayList<>(arguments.length); - for (Argument argument : arguments) { + List> actualArguments = new ArrayList<>(arguments.length); + for (Argument argument : arguments) { if (!matchVariables.containsKey(argument.getName())) { actualArguments.add(argument); } From 09d07f4fc8eff436bed95a5ab4f4d1d838b98f48 Mon Sep 17 00:00:00 2001 From: Tim Yates Date: Fri, 4 Nov 2022 14:20:44 +0000 Subject: [PATCH 197/743] build: Add passing snapshot modules to BOMs (#8286) --- gradle/libs.versions.toml | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 9da82228711..12848f50d05 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -70,10 +70,10 @@ managed-maven-native-plugin = "0.9.13" managed-methvin-directory-watcher = "0.16.1" managed-micrometer = "1.9.5" managed-micronaut-acme = "3.2.0" -managed-micronaut-aot = "1.1.1" +managed-micronaut-aot = "2.0.0-SNAPSHOT" managed-micronaut-aws = "3.9.2" managed-micronaut-azure = "3.5.0" -managed-micronaut-cache = "3.5.0" +managed-micronaut-cache = "4.0.0-SNAPSHOT" managed-micronaut-cassandra = "5.1.1" managed-micronaut-coherence = "3.7.1" managed-micronaut-crac = "1.0.1" @@ -86,7 +86,7 @@ managed-micronaut-gcp = "4.6.0" managed-micronaut-graphql = "3.2.0" managed-micronaut-groovy = "4.0.0-SNAPSHOT" managed-micronaut-grpc = "3.3.1" -managed-micronaut-hibernate-validator = "3.2.0" +managed-micronaut-hibernate-validator = "4.0.0-SNAPSHOT" managed-micronaut-ignite = "1.0.0.RC1" managed-micronaut-jaxrs = "3.4.0" managed-micronaut-jms = "2.1.0" @@ -94,8 +94,8 @@ managed-micronaut-jmx = "3.1.0" managed-micronaut-kafka = "4.4.1" managed-micronaut-kotlin = "3.2.2" managed-micronaut-kubernetes = "3.4.0" -managed-micronaut-micrometer = "4.6.1" -managed-micronaut-microstream = "1.2.0" +managed-micronaut-micrometer = "5.0.0-SNAPSHOT" +managed-micronaut-microstream = "2.0.0-SNAPSHOT" managed-micronaut-liquibase = "5.4.1" managed-micronaut-mongo = "4.6.0" managed-micronaut-mqtt = "2.3.0" @@ -106,7 +106,7 @@ managed-micronaut-netflix = "2.1.0" managed-micronaut-object-storage = "1.1.0" managed-micronaut-openapi = "4.5.2" managed-micronaut-oraclecloud = "2.2.1" -managed-micronaut-picocli = "4.3.0" +managed-micronaut-picocli = "5.0.0-SNAPSHOT" managed-micronaut-problem = "2.5.1" managed-micronaut-rabbitmq = "3.3.0" managed-micronaut-r2dbc = "4.0.0" @@ -114,8 +114,8 @@ managed-micronaut-reactor = "3.0.0-SNAPSHOT" managed-micronaut-redis = "5.3.1" managed-micronaut-rss = "3.2.0" managed-micronaut-rxjava1 = "1.0.0" -managed-micronaut-rxjava2 = "1.3.0" -managed-micronaut-rxjava3 = "2.3.0" +managed-micronaut-rxjava2 = "2.0.0-SNAPSHOT" +managed-micronaut-rxjava3 = "3.0.0-SNAPSHOT" managed-micronaut-security = "3.8.1" managed-micronaut-serialization = "2.0.0-SNAPSHOT" managed-micronaut-servlet = "3.3.2" @@ -123,11 +123,11 @@ managed-micronaut-spring = "4.3.1" managed-micronaut-sql = "4.7.2" managed-micronaut-test = "3.7.0" managed-micronaut-test-resources = "1.1.3" -managed-micronaut-toml = "1.1.1" +managed-micronaut-toml = "2.0.0-SNAPSHOT" managed-micronaut-tracing = "4.4.0" managed-micronaut-tracing-legacy = "3.2.7" managed-micronaut-views = "3.7.1" -managed-micronaut-xml = "3.1.0" +managed-micronaut-xml = "4.0.0-SNAPSHOT" managed-neo4j = "3.5.35" managed-neo4j-java-driver = "4.4.9" managed-netty = "4.1.84.Final" From c5b0012999d59cc0fe57cc114ba3d079d533ae0f Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Mon, 7 Nov 2022 12:36:15 +0100 Subject: [PATCH 198/743] build: security and email as SNAPSHOT (#8298) * build: security and email as SNAPSHOT * Allow switch from javax.mail to jakarta.mail Co-authored-by: Tim Yates --- bom/build.gradle | 3 +++ gradle/libs.versions.toml | 4 ++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/bom/build.gradle b/bom/build.gradle index 72c64a4ab19..fab5f29d1b3 100644 --- a/bom/build.gradle +++ b/bom/build.gradle @@ -66,6 +66,9 @@ micronautBom { acceptedLibraryRegressions.add("micronaut-graphql-gorm") acceptedLibraryRegressions.add("micronaut-neo4j-gorm") + // Switched to Jakarta-mail in email 2.0.0 + acceptedLibraryRegressions.add("javax-mail") + // glassfish removed from Micronaut Serialization acceptedVersionRegressions.add("glassfish-jakarta-json") acceptedLibraryRegressions.add("glassfish-jakarta-json") diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 12848f50d05..5c7ba4bfe7f 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -80,7 +80,7 @@ managed-micronaut-crac = "1.0.1" managed-micronaut-data = "3.8.1" managed-micronaut-discovery = "3.2.0" managed-micronaut-elasticsearch = "4.3.0" -managed-micronaut-email = "1.4.0" +managed-micronaut-email = "2.0.0-SNAPSHOT" managed-micronaut-flyway = "5.4.1" managed-micronaut-gcp = "4.6.0" managed-micronaut-graphql = "3.2.0" @@ -116,7 +116,7 @@ managed-micronaut-rss = "3.2.0" managed-micronaut-rxjava1 = "1.0.0" managed-micronaut-rxjava2 = "2.0.0-SNAPSHOT" managed-micronaut-rxjava3 = "3.0.0-SNAPSHOT" -managed-micronaut-security = "3.8.1" +managed-micronaut-security = "4.0.0-SNAPSHOT" managed-micronaut-serialization = "2.0.0-SNAPSHOT" managed-micronaut-servlet = "3.3.2" managed-micronaut-spring = "4.3.1" From 3a76689f7071548721200dfc1e5be32b23d4089e Mon Sep 17 00:00:00 2001 From: Graeme Rocher Date: Tue, 8 Nov 2022 12:39:08 +0100 Subject: [PATCH 199/743] fix sonar issues (#8304) --- .../micronaut/aop/writer/AopHelperImpl.java | 13 ++++++++----- .../AbstractAnnotationMetadataBuilder.java | 9 +++++---- .../micronaut/inject/ast/MethodElement.java | 3 +++ .../inject/ast/ParameterElement.java | 1 + ...tractElementAnnotationMetadataFactory.java | 1 + .../processing/ProcessingException.java | 15 ++++++++------- .../AbstractAnnotationMetadataWriter.java | 11 ++++++++--- .../core/beans/AbstractBeanMethod.java | 4 ++-- .../value/ConvertibleMultiValuesMap.java | 2 ++ .../io/micronaut/core/util/ArgumentUtils.java | 8 +++++--- .../netty/NettyStreamedHttpResponse.java | 19 +++++++++---------- .../NettyHttpServerConfiguration.java | 1 + .../context/DefaultRuntimeBeanDefinition.java | 7 ++++--- .../inject/qualifiers/PrimaryQualifier.java | 3 ++- 14 files changed, 59 insertions(+), 38 deletions(-) diff --git a/core-processor/src/main/java/io/micronaut/aop/writer/AopHelperImpl.java b/core-processor/src/main/java/io/micronaut/aop/writer/AopHelperImpl.java index e3aaed1cdaa..48e289fa50a 100644 --- a/core-processor/src/main/java/io/micronaut/aop/writer/AopHelperImpl.java +++ b/core-processor/src/main/java/io/micronaut/aop/writer/AopHelperImpl.java @@ -58,7 +58,10 @@ */ @Internal @NextMajorVersion("Correct project dependency so this hack is not needed") -public final class AopHelperImpl implements AopHelper { +public final class AopHelperImpl implements AopHelper { + + private static final String MSG_ADAPTER_METHOD_PREFIX = "Cannot adapt method ["; + private static final String MSG_TARGET_METHOD_PREFIX = "] to target method ["; @Override public BeanDefinitionVisitor visitAdaptedMethod(ClassElement classElement, @@ -71,7 +74,7 @@ public BeanDefinitionVisitor visitAdaptedMethod(ClassElement classElement, Optional interfaceToAdaptValue = methodAnnotationMetadata.getValue(Adapter.class, String.class) .flatMap(clazz -> visitorContext.getClassElement(clazz, visitorContext.getElementAnnotationMetadataFactory().readOnly())); - if (!interfaceToAdaptValue.isPresent()) { + if (interfaceToAdaptValue.isEmpty()) { return null; } ClassElement interfaceToAdapt = interfaceToAdaptValue.get(); @@ -110,10 +113,10 @@ public BeanDefinitionVisitor visitAdaptedMethod(ClassElement classElement, int paramLen = targetParams.length; if (paramLen != sourceParams.length) { - throw new ProcessingException(sourceMethod, "Cannot adapt method [" + sourceMethod + "] to target method [" + targetMethod + "]. Argument lengths don't match."); + throw new ProcessingException(sourceMethod, MSG_ADAPTER_METHOD_PREFIX + sourceMethod + MSG_TARGET_METHOD_PREFIX + targetMethod + "]. Argument lengths don't match."); } if (sourceMethod.isSuspend()) { - throw new ProcessingException(sourceMethod, "Cannot adapt method [" + sourceMethod + "] to target method [" + targetMethod + "]. Kotlin suspend method not supported here."); + throw new ProcessingException(sourceMethod, MSG_ADAPTER_METHOD_PREFIX + sourceMethod + MSG_TARGET_METHOD_PREFIX + targetMethod + "]. Kotlin suspend method not supported here."); } Map typeVariables = interfaceToAdapt.getTypeArguments(); @@ -142,7 +145,7 @@ public BeanDefinitionVisitor visitAdaptedMethod(ClassElement classElement, } if (!sourceType.isAssignable(targetGenericType.getName())) { - throw new ProcessingException(sourceMethod, "Cannot adapt method [" + sourceMethod + "] to target method [" + targetMethod + "]. Type [" + sourceType.getName() + "] is not a subtype of type [" + targetGenericType.getName() + "] for argument at position " + i); + throw new ProcessingException(sourceMethod, MSG_ADAPTER_METHOD_PREFIX + sourceMethod + MSG_TARGET_METHOD_PREFIX + targetMethod + "]. Type [" + sourceType.getName() + "] is not a subtype of type [" + targetGenericType.getName() + "] for argument at position " + i); } } diff --git a/core-processor/src/main/java/io/micronaut/inject/annotation/AbstractAnnotationMetadataBuilder.java b/core-processor/src/main/java/io/micronaut/inject/annotation/AbstractAnnotationMetadataBuilder.java index ea376425a7d..78d48ce5d84 100644 --- a/core-processor/src/main/java/io/micronaut/inject/annotation/AbstractAnnotationMetadataBuilder.java +++ b/core-processor/src/main/java/io/micronaut/inject/annotation/AbstractAnnotationMetadataBuilder.java @@ -82,6 +82,7 @@ public abstract class AbstractAnnotationMetadataBuilder { private static final List DEFAULT_ANNOTATE_EXCLUDES = Arrays.asList(Internal.class.getName(), Experimental.class.getName(), "jdk.internal.ValueBased"); private static final Map> ANNOTATION_DEFAULTS = new HashMap<>(20); + private static final String MSG_UNRECOGNIZED_ANNOTATION_METADATA = "Unrecognized annotation metadata: "; static { for (AnnotationMapper mapper : SoftServiceLoader.load(AnnotationMapper.class, AbstractAnnotationMetadataBuilder.class.getClassLoader()) @@ -2188,7 +2189,7 @@ public AnnotationMetadata annotate( AnnotationMetadata declaredMetadata = annotate(hierarchy.getDeclaredMetadata(), annotationValue); return hierarchy.createSibling(declaredMetadata); } - throw new IllegalStateException("Unrecognized annotation metadata: " + annotationMetadata); + throw new IllegalStateException(MSG_UNRECOGNIZED_ANNOTATION_METADATA + annotationMetadata); } /** @@ -2229,7 +2230,7 @@ public AnnotationMetadata removeAnnotation(AnnotationMetadata annotationMetadata } return declaredMetadata; } else { - throw new IllegalStateException("Unrecognized annotation metadata: " + annotationMetadata); + throw new IllegalStateException(MSG_UNRECOGNIZED_ANNOTATION_METADATA + annotationMetadata); } } @@ -2270,7 +2271,7 @@ public AnnotationMetadata removeStereotype(AnnotationMetadata annotationMetadata } return declaredMetadata; } - throw new IllegalStateException("Unrecognized annotation metadata: " + annotationMetadata); + throw new IllegalStateException(MSG_UNRECOGNIZED_ANNOTATION_METADATA + annotationMetadata); } /** @@ -2302,7 +2303,7 @@ public AnnotationMetadata removeStereotype(AnnotationMetadata annotationMetadata } return declaredMetadata; } - throw new IllegalStateException("Unrecognized annotation metadata: " + annotationMetadata); + throw new IllegalStateException(MSG_UNRECOGNIZED_ANNOTATION_METADATA + annotationMetadata); } /** diff --git a/core-processor/src/main/java/io/micronaut/inject/ast/MethodElement.java b/core-processor/src/main/java/io/micronaut/inject/ast/MethodElement.java index cfb0506f522..be1ac7ed2cf 100644 --- a/core-processor/src/main/java/io/micronaut/inject/ast/MethodElement.java +++ b/core-processor/src/main/java/io/micronaut/inject/ast/MethodElement.java @@ -489,6 +489,7 @@ public boolean isFinal() { } @Override + @SuppressWarnings("java:S1192") public Element annotate(@NonNull String annotationType, @NonNull Consumer> consumer) { ArgumentUtils.requireNonNull("annotationType", annotationType); AnnotationValueBuilder builder = AnnotationValue.builder(annotationType); @@ -510,6 +511,7 @@ public Element annotate(AnnotationValue annotationValu } @Override + @SuppressWarnings("java:S1192") public Element removeAnnotation(@NonNull String annotationType) { ArgumentUtils.requireNonNull("annotationType", annotationType); annotationMetadata = metadataBuilder.removeAnnotation(getAnnotationMetadata(), annotationType); @@ -525,6 +527,7 @@ public Element removeAnnotationIf(@NonNull Predicate Element annotate(@NonNull String annotationType, @NonNull Consumer> consumer) { ArgumentUtils.requireNonNull("annotationType", annotationType); AnnotationValueBuilder builder = AnnotationValue.builder(annotationType); diff --git a/core-processor/src/main/java/io/micronaut/inject/ast/annotation/AbstractElementAnnotationMetadataFactory.java b/core-processor/src/main/java/io/micronaut/inject/ast/annotation/AbstractElementAnnotationMetadataFactory.java index 8055ad66873..12ef0589219 100644 --- a/core-processor/src/main/java/io/micronaut/inject/ast/annotation/AbstractElementAnnotationMetadataFactory.java +++ b/core-processor/src/main/java/io/micronaut/inject/ast/annotation/AbstractElementAnnotationMetadataFactory.java @@ -336,6 +336,7 @@ private AnnotationMetadata replaceAnnotationsInternal(AnnotationMetadata annotat } @Override + @SuppressWarnings("java:S1192") public AnnotationMetadata annotate(@NonNull String annotationType, @NonNull Consumer> consumer) { ArgumentUtils.requireNonNull("annotationType", annotationType); AnnotationValueBuilder builder = AnnotationValue.builder(annotationType); diff --git a/core-processor/src/main/java/io/micronaut/inject/processing/ProcessingException.java b/core-processor/src/main/java/io/micronaut/inject/processing/ProcessingException.java index 9d16d4ee7b4..f1d8899b74c 100644 --- a/core-processor/src/main/java/io/micronaut/inject/processing/ProcessingException.java +++ b/core-processor/src/main/java/io/micronaut/inject/processing/ProcessingException.java @@ -15,6 +15,7 @@ */ package io.micronaut.inject.processing; +import io.micronaut.core.annotation.Nullable; import io.micronaut.inject.ast.Element; /** @@ -25,20 +26,20 @@ */ public final class ProcessingException extends RuntimeException { - private final Object originatingElement; + private final transient Element originatingElement; private final String message; public ProcessingException(Element element, String message) { - this(element.getNativeType(), message); - } - - public ProcessingException(Object originatingElement, String message) { - this.originatingElement = originatingElement; + this.originatingElement = element; this.message = message; } + @Nullable public Object getOriginatingElement() { - return originatingElement; + if (originatingElement != null) { + return originatingElement.getNativeType(); + } + return null; } @Override diff --git a/core-processor/src/main/java/io/micronaut/inject/writer/AbstractAnnotationMetadataWriter.java b/core-processor/src/main/java/io/micronaut/inject/writer/AbstractAnnotationMetadataWriter.java index c773e6b563b..743863a0ce0 100644 --- a/core-processor/src/main/java/io/micronaut/inject/writer/AbstractAnnotationMetadataWriter.java +++ b/core-processor/src/main/java/io/micronaut/inject/writer/AbstractAnnotationMetadataWriter.java @@ -45,6 +45,11 @@ public abstract class AbstractAnnotationMetadataWriter extends AbstractClassFile */ public static final String FIELD_ANNOTATION_METADATA = "$ANNOTATION_METADATA"; + /** + * Field name for empty metadata. + */ + public static final String FIELD_EMPTY_METADATA = "EMPTY_METADATA"; + protected final Type targetClassType; protected final AnnotationMetadata annotationMetadata; protected final Map loadTypeMethods = new HashMap<>(); @@ -96,7 +101,7 @@ protected void writeGetAnnotationMetadataMethod(ClassWriter classWriter) { // then we setup an annotation metadata reference from the method to the class (or inherited method) metadata AnnotationMetadata annotationMetadata = this.annotationMetadata.getTargetAnnotationMetadata(); if (annotationMetadata.isEmpty()) { - annotationMetadataMethod.getStatic(Type.getType(AnnotationMetadata.class), "EMPTY_METADATA", Type.getType(AnnotationMetadata.class)); + annotationMetadataMethod.getStatic(Type.getType(AnnotationMetadata.class), FIELD_EMPTY_METADATA, Type.getType(AnnotationMetadata.class)); } else if (annotationMetadata instanceof AnnotationMetadataReference) { AnnotationMetadataReference reference = (AnnotationMetadataReference) annotationMetadata; String className = reference.getClassName(); @@ -140,7 +145,7 @@ protected void writeAnnotationMetadataStaticInitializer(ClassWriter classWriter, if (writeAnnotationDefault) { AnnotationMetadata annotationMetadata = this.annotationMetadata.getTargetAnnotationMetadata(); if (annotationMetadata.isEmpty()) { - staticInit.getStatic(Type.getType(AnnotationMetadata.class), "EMPTY_METADATA", Type.getType(AnnotationMetadata.class)); + staticInit.getStatic(Type.getType(AnnotationMetadata.class), FIELD_EMPTY_METADATA, Type.getType(AnnotationMetadata.class)); } else { if (annotationMetadata instanceof AnnotationMetadataHierarchy) { annotationMetadata = ((AnnotationMetadataHierarchy) annotationMetadata).merge(); @@ -179,7 +184,7 @@ protected void initializeAnnotationMetadata(GeneratorAdapter staticInit, ClassWr AnnotationMetadata annotationMetadata = this.annotationMetadata.getTargetAnnotationMetadata(); if (annotationMetadata.isEmpty()) { - staticInit.getStatic(Type.getType(AnnotationMetadata.class), "EMPTY_METADATA", Type.getType(AnnotationMetadata.class)); + staticInit.getStatic(Type.getType(AnnotationMetadata.class), FIELD_EMPTY_METADATA, Type.getType(AnnotationMetadata.class)); } else if (annotationMetadata instanceof DefaultAnnotationMetadata) { AnnotationMetadataWriter.instantiateNewMetadata( targetClassType, diff --git a/core/src/main/java/io/micronaut/core/beans/AbstractBeanMethod.java b/core/src/main/java/io/micronaut/core/beans/AbstractBeanMethod.java index 588f2f76a01..d5b0dbb2acd 100644 --- a/core/src/main/java/io/micronaut/core/beans/AbstractBeanMethod.java +++ b/core/src/main/java/io/micronaut/core/beans/AbstractBeanMethod.java @@ -72,8 +72,7 @@ public BeanIntrospection getDeclaringBean() { @Override public final @NonNull ReturnType getReturnType() { - //noinspection unchecked - return new ReturnType() { + return new ReturnType<>() { @Override public Class getType() { return returnType.getType(); @@ -115,6 +114,7 @@ public final Argument[] getArguments() { return arguments; } + @SuppressWarnings("java:S2638") @Override public T invoke(@NonNull B instance, Object... arguments) { return invokeInternal(instance, arguments); diff --git a/core/src/main/java/io/micronaut/core/convert/value/ConvertibleMultiValuesMap.java b/core/src/main/java/io/micronaut/core/convert/value/ConvertibleMultiValuesMap.java index 0dc0fc5012d..6ae90088e84 100644 --- a/core/src/main/java/io/micronaut/core/convert/value/ConvertibleMultiValuesMap.java +++ b/core/src/main/java/io/micronaut/core/convert/value/ConvertibleMultiValuesMap.java @@ -38,9 +38,11 @@ * @since 1.0 */ public class ConvertibleMultiValuesMap implements ConvertibleMultiValues, ConversionServiceAware { + @SuppressWarnings("java:S1845") public static final ConvertibleMultiValues EMPTY = new ConvertibleMultiValuesMap(Collections.emptyMap()) { @Override public void setConversionService(ConversionService conversionService) { + // not needed } }; diff --git a/core/src/main/java/io/micronaut/core/util/ArgumentUtils.java b/core/src/main/java/io/micronaut/core/util/ArgumentUtils.java index 5445ab8c5dd..fec693ea740 100644 --- a/core/src/main/java/io/micronaut/core/util/ArgumentUtils.java +++ b/core/src/main/java/io/micronaut/core/util/ArgumentUtils.java @@ -28,6 +28,8 @@ */ public class ArgumentUtils { + private static final String MSG_PREFIX_ARGUMENT = "Argument ["; + /** * Adds a check that the given number is positive. * @@ -53,7 +55,7 @@ public class ArgumentUtils { */ public static T requireNonNull(String name, T value) { if (value == null) { - throw new NullPointerException("Argument [" + name + "] cannot be null"); + throw new NullPointerException(MSG_PREFIX_ARGUMENT + name + "] cannot be null"); } return value; } @@ -68,7 +70,7 @@ public static T requireNonNull(String name, T value) { */ public static int requirePositive(String name, int value) { if (value < 0) { - throw new IllegalArgumentException("Argument [" + name + "] cannot be negative"); + throw new IllegalArgumentException(MSG_PREFIX_ARGUMENT + name + "] cannot be negative"); } return value; } @@ -170,7 +172,7 @@ public void orElseFail(String message) { */ public void notNull() { if (value == null) { - throw new NullPointerException("Argument [" + name + "] cannot be null"); + throw new NullPointerException(MSG_PREFIX_ARGUMENT + name + "] cannot be null"); } } } diff --git a/http-client/src/main/java/io/micronaut/http/client/netty/NettyStreamedHttpResponse.java b/http-client/src/main/java/io/micronaut/http/client/netty/NettyStreamedHttpResponse.java index 56e7aed9114..5ba8c35a492 100644 --- a/http-client/src/main/java/io/micronaut/http/client/netty/NettyStreamedHttpResponse.java +++ b/http-client/src/main/java/io/micronaut/http/client/netty/NettyStreamedHttpResponse.java @@ -53,7 +53,7 @@ class NettyStreamedHttpResponse implements MutableHttpResponse, NettyHttpR @GuardedBy("this") private NettyCookies nettyCookies; // initialized lazily private B body; - private volatile MutableConvertibleValues attributes; + private MutableConvertibleValues attributes; /** * @param response The streamed Http response @@ -88,17 +88,17 @@ public MutableHttpHeaders getHeaders() { @Override public MutableConvertibleValues getAttributes() { - MutableConvertibleValues attributes = this.attributes; - if (attributes == null) { + MutableConvertibleValues mcv = this.attributes; + if (mcv == null) { synchronized (this) { // double check - attributes = this.attributes; - if (attributes == null) { - attributes = new MutableConvertibleValuesMap<>(); - this.attributes = attributes; + mcv = this.attributes; + if (mcv == null) { + mcv = new MutableConvertibleValuesMap<>(); + this.attributes = mcv; } } } - return attributes; + return mcv; } /** @@ -140,8 +140,7 @@ public boolean isStream() { @Override public synchronized MutableHttpResponse cookie(Cookie cookie) { - if (cookie instanceof NettyCookie) { - NettyCookie nettyCookie = (NettyCookie) cookie; + if (cookie instanceof NettyCookie nettyCookie) { // this is a response cookie, encode with server encoder String value = ServerCookieEncoder.STRICT.encode(nettyCookie.getNettyCookie()); headers.add(HttpHeaderNames.SET_COOKIE, value); diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/configuration/NettyHttpServerConfiguration.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/configuration/NettyHttpServerConfiguration.java index d615bb1dc8a..82c87e9b106 100644 --- a/http-server-netty/src/main/java/io/micronaut/http/server/netty/configuration/NettyHttpServerConfiguration.java +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/configuration/NettyHttpServerConfiguration.java @@ -624,6 +624,7 @@ public Boolean getPushEnabled() { */ @Deprecated public void setPushEnabled(Boolean enabled) { + // deprecated } /** diff --git a/inject/src/main/java/io/micronaut/context/DefaultRuntimeBeanDefinition.java b/inject/src/main/java/io/micronaut/context/DefaultRuntimeBeanDefinition.java index 1a50274e16f..b1240f47a3b 100644 --- a/inject/src/main/java/io/micronaut/context/DefaultRuntimeBeanDefinition.java +++ b/inject/src/main/java/io/micronaut/context/DefaultRuntimeBeanDefinition.java @@ -45,6 +45,7 @@ @Experimental final class DefaultRuntimeBeanDefinition extends AbstractBeanContextConditional implements RuntimeBeanDefinition { private static final AtomicInteger REF_COUNT = new AtomicInteger(0); + private static final String MSG_BEAN_TYPE_CANNOT_BE_NULL = "Bean type cannot be null"; private final Argument beanType; private final Supplier supplier; private final AnnotationMetadata annotationMetadata; @@ -62,7 +63,7 @@ final class DefaultRuntimeBeanDefinition extends AbstractBeanContextCondition boolean isSingleton, @Nullable Class scope, Class[] exposedTypes) { - Objects.requireNonNull(beanType, "Bean type cannot be null"); + Objects.requireNonNull(beanType, MSG_BEAN_TYPE_CANNOT_BE_NULL); Objects.requireNonNull(supplier, "Bean supplier cannot be null"); this.beanType = beanType; @@ -126,7 +127,7 @@ public Qualifier resolveDynamicQualifier() { * @return The bean name */ static String generateBeanName(@NonNull Class beanType) { - Objects.requireNonNull(beanType, "Bean type cannot be null"); + Objects.requireNonNull(beanType, MSG_BEAN_TYPE_CANNOT_BE_NULL); return beanType.getName() + "$DynamicDefinition" + REF_COUNT.incrementAndGet(); } @@ -198,7 +199,7 @@ static final class RuntimeBeanBuilder implements RuntimeBeanDefinition.Builde private Class[] exposedTypes = ReflectionUtils.EMPTY_CLASS_ARRAY; RuntimeBeanBuilder(Argument beanType, Supplier supplier) { - this.beanType = Objects.requireNonNull(beanType, "Bean type cannot be null"); + this.beanType = Objects.requireNonNull(beanType, MSG_BEAN_TYPE_CANNOT_BE_NULL); this.supplier = Objects.requireNonNull(supplier, "Bean supplier cannot be null"); } diff --git a/inject/src/main/java/io/micronaut/inject/qualifiers/PrimaryQualifier.java b/inject/src/main/java/io/micronaut/inject/qualifiers/PrimaryQualifier.java index 5486e78d443..96cd54c68dd 100644 --- a/inject/src/main/java/io/micronaut/inject/qualifiers/PrimaryQualifier.java +++ b/inject/src/main/java/io/micronaut/inject/qualifiers/PrimaryQualifier.java @@ -30,7 +30,8 @@ */ @Internal public final class PrimaryQualifier implements Qualifier { - @SuppressWarnings("rawtypes") + + @SuppressWarnings({"rawtypes", "java:S1845"}) public static final PrimaryQualifier INSTANCE = new PrimaryQualifier(); private PrimaryQualifier() { From ff30e35e41e55869c02f9102598b6565b7d1c6ac Mon Sep 17 00:00:00 2001 From: Armando Prieto <90722743+aprietop@users.noreply.github.com> Date: Tue, 8 Nov 2022 03:40:33 -0800 Subject: [PATCH 200/743] wrap original exception when the circuit breaker is opened (#8222) - add throwWrappedException parameter to @CircuitBreaker --- .../retry/annotation/CircuitBreaker.java | 7 +++ .../retry/intercept/CircuitBreakerRetry.java | 7 ++- .../intercept/DefaultRetryInterceptor.java | 5 ++- .../intercept/CircuitBreakerRetrySpec.groovy | 2 +- .../retry/intercept/CircuitBreakerSpec.groovy | 44 +++++++++++++++++++ 5 files changed, 61 insertions(+), 4 deletions(-) diff --git a/runtime/src/main/java/io/micronaut/retry/annotation/CircuitBreaker.java b/runtime/src/main/java/io/micronaut/retry/annotation/CircuitBreaker.java index a59d86b800b..e3186977fda 100644 --- a/runtime/src/main/java/io/micronaut/retry/annotation/CircuitBreaker.java +++ b/runtime/src/main/java/io/micronaut/retry/annotation/CircuitBreaker.java @@ -95,4 +95,11 @@ */ @AliasFor(annotation = Retryable.class, member = "predicate") Class predicate() default DefaultRetryPredicate.class; + + /** + * If {@code true} and the circuit is opened, it throws the original exception wrapped + * in a {@link io.micronaut.retry.exception.CircuitOpenException} + * @return Whether to wrap the original exception in a {@link io.micronaut.retry.exception.CircuitOpenException} + */ + boolean throwWrappedException() default false; } diff --git a/runtime/src/main/java/io/micronaut/retry/intercept/CircuitBreakerRetry.java b/runtime/src/main/java/io/micronaut/retry/intercept/CircuitBreakerRetry.java index 9935fb8e54e..95426f8450f 100644 --- a/runtime/src/main/java/io/micronaut/retry/intercept/CircuitBreakerRetry.java +++ b/runtime/src/main/java/io/micronaut/retry/intercept/CircuitBreakerRetry.java @@ -46,6 +46,7 @@ class CircuitBreakerRetry implements MutableRetryState { private final long openTimeout; private final ExecutableMethod method; private final ApplicationEventPublisher eventPublisher; + private final boolean throwWrappedException; private AtomicReference state = new AtomicReference<>(CircuitState.CLOSED); private volatile Throwable lastError; private volatile long time = System.currentTimeMillis(); @@ -56,18 +57,20 @@ class CircuitBreakerRetry implements MutableRetryState { * @param childStateBuilder The retry state builder * @param method A compile time produced invocation of a method call * @param eventPublisher To publish circuit events + * @param throwWrappedException If {@code true}, the original exception will be wrapped in {@link CircuitOpenException} */ CircuitBreakerRetry( long openTimeout, RetryStateBuilder childStateBuilder, ExecutableMethod method, - ApplicationEventPublisher eventPublisher) { + ApplicationEventPublisher eventPublisher, boolean throwWrappedException) { this.retryStateBuilder = childStateBuilder; this.openTimeout = openTimeout; this.childState = (MutableRetryState) childStateBuilder.build(); this.eventPublisher = eventPublisher; this.method = method; + this.throwWrappedException = throwWrappedException; } @Override @@ -92,7 +95,7 @@ public void open() { if (LOG.isDebugEnabled()) { LOG.debug("Rethrowing existing exception for Open Circuit [{}]: {}", method, lastError.getMessage()); } - if (lastError instanceof RuntimeException) { + if (lastError instanceof RuntimeException && !throwWrappedException) { throw (RuntimeException) lastError; } else { throw new CircuitOpenException("Circuit Open: " + lastError.getMessage(), lastError); diff --git a/runtime/src/main/java/io/micronaut/retry/intercept/DefaultRetryInterceptor.java b/runtime/src/main/java/io/micronaut/retry/intercept/DefaultRetryInterceptor.java index 46758f4a421..a9faaf60a28 100644 --- a/runtime/src/main/java/io/micronaut/retry/intercept/DefaultRetryInterceptor.java +++ b/runtime/src/main/java/io/micronaut/retry/intercept/DefaultRetryInterceptor.java @@ -102,9 +102,12 @@ public Object intercept(MethodInvocationContext context) { long timeout = context .getValue(CircuitBreaker.class, "reset", Duration.class) .map(Duration::toMillis).orElse(Duration.ofSeconds(DEFAULT_CIRCUIT_BREAKER_TIMEOUT_IN_MILLIS).toMillis()); + boolean wrapException = context + .getValue(CircuitBreaker.class, "throwWrappedException", Boolean.class) + .orElse(false); retryState = circuitContexts.computeIfAbsent( context.getExecutableMethod(), - method -> new CircuitBreakerRetry(timeout, retryStateBuilder, context, eventPublisher) + method -> new CircuitBreakerRetry(timeout, retryStateBuilder, context, eventPublisher, wrapException) ); } else { retryState = (MutableRetryState) retryStateBuilder.build(); diff --git a/runtime/src/test/groovy/io/micronaut/retry/intercept/CircuitBreakerRetrySpec.groovy b/runtime/src/test/groovy/io/micronaut/retry/intercept/CircuitBreakerRetrySpec.groovy index 84078a81355..dd71b0c8ba5 100644 --- a/runtime/src/test/groovy/io/micronaut/retry/intercept/CircuitBreakerRetrySpec.groovy +++ b/runtime/src/test/groovy/io/micronaut/retry/intercept/CircuitBreakerRetrySpec.groovy @@ -39,7 +39,7 @@ class CircuitBreakerRetrySpec extends Specification { 1000, {-> new SimpleRetry(3, 2.0d, Duration.ofMillis(500)) - }, null,null + }, null,null,false ) retry.open() diff --git a/runtime/src/test/groovy/io/micronaut/retry/intercept/CircuitBreakerSpec.groovy b/runtime/src/test/groovy/io/micronaut/retry/intercept/CircuitBreakerSpec.groovy index 1a515c56627..b2de0579e72 100644 --- a/runtime/src/test/groovy/io/micronaut/retry/intercept/CircuitBreakerSpec.groovy +++ b/runtime/src/test/groovy/io/micronaut/retry/intercept/CircuitBreakerSpec.groovy @@ -22,6 +22,7 @@ import io.micronaut.retry.event.CircuitClosedEvent import io.micronaut.retry.event.CircuitOpenEvent import io.micronaut.retry.event.RetryEvent import io.micronaut.retry.event.RetryEventListener +import io.micronaut.retry.exception.CircuitOpenException import jakarta.inject.Singleton import org.reactivestreams.Publisher import reactor.core.publisher.Mono @@ -188,6 +189,34 @@ class CircuitBreakerSpec extends Specification{ result == 2 } + void "test circuit breaker throws a wrapped exception"(){ + given: + ApplicationContext context = ApplicationContext.run() + WrappedExceptionService service = context.getBean(WrappedExceptionService) + + when:"A method is annotated retry" + int result = service.getCount() + + then:"It executes until successful" + result == 2 + + when:"The threshold can never be met" + service.countThreshold = Integer.MAX_VALUE + service.countValue = 0 + service.getCount() + + then:"Throws the original exception" + thrown(IllegalStateException) + + when:"the method is called again" + service.getCount() + + then:"Throws the wrapped exception, the original logic is never invoked" + CircuitOpenException e = thrown() + e.getCause().getClass() == IllegalStateException + + } + @Singleton static class MyRetryListener implements RetryEventListener { @@ -268,4 +297,19 @@ class CircuitBreakerSpec extends Specification{ return countValue } } + + @Singleton + @CircuitBreaker(throwWrappedException = true) + static class WrappedExceptionService { + int countValue = 0 + int countThreshold = 2 + + int getCount() { + countValue++ + if(countValue < countThreshold) { + throw new IllegalStateException("Bad count") + } + return countValue + } + } } From d312a20ab3fa9257cc6c8a605639cb879d892965 Mon Sep 17 00:00:00 2001 From: Denis Stepanov Date: Tue, 8 Nov 2022 18:53:17 +0700 Subject: [PATCH 201/743] Property type: select the type with the most generic argument annotations (#8302) --- .../ast/utils/AstBeanPropertiesUtils.java | 14 +- .../ClassElementAnnotationsRetaining.groovy | 178 ++++++++++++++++++ 2 files changed, 188 insertions(+), 4 deletions(-) create mode 100644 inject-java/src/test/groovy/io/micronaut/visitors/ClassElementAnnotationsRetaining.groovy diff --git a/core-processor/src/main/java/io/micronaut/inject/ast/utils/AstBeanPropertiesUtils.java b/core-processor/src/main/java/io/micronaut/inject/ast/utils/AstBeanPropertiesUtils.java index ee27f6339e6..e5870b9d1ac 100644 --- a/core-processor/src/main/java/io/micronaut/inject/ast/utils/AstBeanPropertiesUtils.java +++ b/core-processor/src/main/java/io/micronaut/inject/ast/utils/AstBeanPropertiesUtils.java @@ -150,10 +150,16 @@ public static List resolveBeanProperties(BeanPropertiesQuery co // and it has more type arguments annotations - use it as the property type if (value.field != null && value.field.getType().equals(value.type) - && hasGenericTypeAnnotations(value.field.getType()) - && !hasGenericTypeAnnotations(value.type.getType())) { + && countGenericTypeAnnotations(value.field.getType()) > countGenericTypeAnnotations(value.type.getType())) { value.type = value.field.getGenericType(); } + // In a case when the getter's type is the same as the selected property type, + // and it has more type arguments annotations - use it as the property type + if (value.getter != null + && value.getter.getGenericReturnType().equals(value.type) + && countGenericTypeAnnotations(value.getter.getGenericReturnType()) > countGenericTypeAnnotations(value.type.getType())) { + value.type = value.getter.getGenericReturnType(); + } if (value.readAccessKind != null || value.writeAccessKind != null) { value.isExcluded = shouldExclude(includes, excludes, propertyName) || isExcludedByAnnotations(configuration, value) @@ -169,8 +175,8 @@ && hasGenericTypeAnnotations(value.field.getType()) return Collections.emptyList(); } - private static boolean hasGenericTypeAnnotations(ClassElement cl) { - return cl.getTypeArguments().values().stream().anyMatch(t -> !t.getAnnotationMetadata().isEmpty()); + private static int countGenericTypeAnnotations(ClassElement cl) { + return cl.getTypeArguments().values().stream().mapToInt(t -> t.getAnnotationMetadata().getAnnotationNames().size()).sum(); } private static boolean isExcludedBecauseOfMissingAccess(BeanPropertyData value) { diff --git a/inject-java/src/test/groovy/io/micronaut/visitors/ClassElementAnnotationsRetaining.groovy b/inject-java/src/test/groovy/io/micronaut/visitors/ClassElementAnnotationsRetaining.groovy new file mode 100644 index 00000000000..47caef20eaf --- /dev/null +++ b/inject-java/src/test/groovy/io/micronaut/visitors/ClassElementAnnotationsRetaining.groovy @@ -0,0 +1,178 @@ +package io.micronaut.visitors + +import io.micronaut.annotation.processing.test.AbstractTypeElementSpec +import io.micronaut.inject.ast.ClassElement +import io.micronaut.inject.ast.PropertyElement +/** + * This spec puts annotations in different places on properties and verifies that + * the annotations are present in the ClassElement. + */ +class ClassElementAnnotationsRetaining extends AbstractTypeElementSpec { + + void 'test type argument annotation on the property without a setter'() { + given: + // Put an annotation on the property type argument + // Do not define a setter + def code = ''' +package test; +import java.util.List; +import javax.validation.Valid; +import javax.validation.constraints.NotBlank; +import io.micronaut.core.annotation.Introspected; +@Introspected +class SaladWithSetter { + List<@Valid Ingredient> ingredients; + public List getIngredients() { + return ingredients; + } + @Introspected + public record Ingredient( + @NotBlank String name + ) {} +} +''' + + when: + ClassElement classElement = buildClassElement(code) + PropertyElement propertyElement = classElement.getBeanProperties().iterator().next() + + then: + def propertyTypeArgument = propertyElement.type.typeArguments.get("E") + propertyTypeArgument.annotationMetadata.hasStereotype("javax.validation.Valid") + } + + void 'test type argument annotation on the getter'() { + given: + // Put an annotation on the property type argument + // Do not define a setter + def code = ''' +package test; +import java.util.List; +import javax.validation.Valid; +import javax.validation.constraints.NotBlank; +import io.micronaut.core.annotation.Introspected; +@Introspected +class SaladWithSetter { + List ingredients; + public List<@Valid Ingredient> getIngredients() { + return ingredients; + } + @Introspected + public record Ingredient( + @NotBlank String name + ) {} +} +''' + + when: + ClassElement classElement = buildClassElement(code) + PropertyElement propertyElement = classElement.getBeanProperties().iterator().next() + + then: + def propertyTypeArgument = propertyElement.type.typeArguments.get("E") + propertyTypeArgument.annotationMetadata.hasStereotype("javax.validation.Valid") + } + + void 'test type argument annotation on the setter'() { + given: + // Put an annotation on the setter type argument + def code = ''' +package test; +import java.util.List; +import javax.validation.Valid; +import javax.validation.constraints.NotBlank; +import io.micronaut.core.annotation.Introspected; +@Introspected +class SaladWithSetter { + List ingredients; + public List getIngredients() { + return ingredients; + } + public void setIngredients(List<@Valid Ingredient> ingredients) { + this.ingredients = ingredients; + } + @Introspected + public record Ingredient( + @NotBlank String name + ) {} +} +''' + + when: + ClassElement classElement = buildClassElement(code) + PropertyElement propertyElement = classElement.getBeanProperties().iterator().next() + + then: + def propertyTypeArgument = propertyElement.type.typeArguments.get("E") + propertyTypeArgument.annotationMetadata.hasStereotype("javax.validation.Valid") + } + + void 'test type argument annotation on the property with a setter'() { + given: + // Put an annotation on the property type argument + // Define a setter + def code = ''' +package test; +import java.util.List; +import javax.validation.Valid; +import javax.validation.constraints.NotBlank; +import io.micronaut.core.annotation.Introspected; +@Introspected +class SaladWithSetter { + List<@Valid Ingredient> ingredients; + public List getIngredients() { + return ingredients; + } + public void setIngredients(List ingredients) { + this.ingredients = ingredients; + } + @Introspected + public record Ingredient( + @NotBlank String name + ) {} +} +''' + + when: + ClassElement classElement = buildClassElement(code) + PropertyElement propertyElement = classElement.getBeanProperties().iterator().next() + + then: + def propertyTypeArgument = propertyElement.type.typeArguments.get("E") + propertyTypeArgument.annotationMetadata.hasStereotype("javax.validation.Valid") + } + + void 'test annotation on the property with a setter'() { + given: + // Put an annotation on the property + // Define a setter + def code = ''' +package test; +import javax.validation.Valid; +import javax.validation.constraints.NotBlank; +import io.micronaut.core.annotation.Introspected; +@Introspected +class SaladWithSetter { + @Valid Ingredient ingredient; + public Ingredient getIngredient() { + return ingredient; + } + public void setIngredient(Ingredient ingredient) { + this.ingredient = ingredient; + } + @Introspected + public record Ingredient( + @NotBlank String name + ) {} +} +''' + + when: + ClassElement classElement = buildClassElement(code) + PropertyElement propertyElement = classElement.getBeanProperties().iterator().next() + + then: + propertyElement.annotationMetadata.hasStereotype("javax.validation.Valid") + } + +} From edadfd1e5f3fdca27da3156617da6ae92e37725f Mon Sep 17 00:00:00 2001 From: Bastien Aracil Date: Wed, 9 Nov 2022 09:46:39 +0100 Subject: [PATCH 202/743] Add support for Jakarta nulllable annotations. (#8307) * Closes #8264 --- .../internal/JakartaNonnullTransformer.java | 46 ++++++++++++ .../internal/JakartaNullableTransformer.java | 46 ++++++++++++ ...ut.inject.annotation.AnnotationTransformer | 2 + .../NonNullabilityAnnotationsSpec.groovy | 74 +++++++++++++++++++ .../NullabilityAnnotationsSpec.groovy | 4 +- 5 files changed, 170 insertions(+), 2 deletions(-) create mode 100644 core-processor/src/main/java/io/micronaut/inject/annotation/internal/JakartaNonnullTransformer.java create mode 100644 core-processor/src/main/java/io/micronaut/inject/annotation/internal/JakartaNullableTransformer.java create mode 100644 inject-java/src/test/groovy/io/micronaut/annotation/NonNullabilityAnnotationsSpec.groovy diff --git a/core-processor/src/main/java/io/micronaut/inject/annotation/internal/JakartaNonnullTransformer.java b/core-processor/src/main/java/io/micronaut/inject/annotation/internal/JakartaNonnullTransformer.java new file mode 100644 index 00000000000..73605d4e15b --- /dev/null +++ b/core-processor/src/main/java/io/micronaut/inject/annotation/internal/JakartaNonnullTransformer.java @@ -0,0 +1,46 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.inject.annotation.internal; + +import io.micronaut.core.annotation.AnnotationUtil; +import io.micronaut.core.annotation.AnnotationValue; +import io.micronaut.core.annotation.NonNull; +import io.micronaut.inject.annotation.NamedAnnotationTransformer; +import io.micronaut.inject.visitor.VisitorContext; + +import java.lang.annotation.Annotation; +import java.util.Collections; +import java.util.List; + +/** + * A transformer that remaps {@link jakarta.annotation.Nonnull} to {@code javax.annotation.Nonnull}. + * @since 4.0 + */ +public class JakartaNonnullTransformer implements NamedAnnotationTransformer { + + @Override + public @NonNull String getName() { + return "jakarta.annotation.Nonnull"; + } + + @Override + public List> transform(AnnotationValue annotation, VisitorContext visitorContext) { + return Collections.singletonList( + AnnotationValue.builder(AnnotationUtil.NON_NULL).build() + ); + } +} + diff --git a/core-processor/src/main/java/io/micronaut/inject/annotation/internal/JakartaNullableTransformer.java b/core-processor/src/main/java/io/micronaut/inject/annotation/internal/JakartaNullableTransformer.java new file mode 100644 index 00000000000..bcbd4ca39d8 --- /dev/null +++ b/core-processor/src/main/java/io/micronaut/inject/annotation/internal/JakartaNullableTransformer.java @@ -0,0 +1,46 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.inject.annotation.internal; + +import io.micronaut.core.annotation.AnnotationUtil; +import io.micronaut.core.annotation.AnnotationValue; +import io.micronaut.core.annotation.NonNull; +import io.micronaut.inject.annotation.NamedAnnotationTransformer; +import io.micronaut.inject.visitor.VisitorContext; + +import java.lang.annotation.Annotation; +import java.util.Collections; +import java.util.List; + +/** + * A transformer that remaps {@link jakarta.annotation.Nullable} to {@code javax.annotation.Nullable}. + * @since 4.0 + */ +public class JakartaNullableTransformer implements NamedAnnotationTransformer { + + @Override + public @NonNull String getName() { + return "jakarta.annotation.Nullable"; + } + + @Override + public List> transform(AnnotationValue annotation, VisitorContext visitorContext) { + return Collections.singletonList( + AnnotationValue.builder(AnnotationUtil.NULLABLE).build() + ); + } +} + diff --git a/core-processor/src/main/resources/META-INF/services/io.micronaut.inject.annotation.AnnotationTransformer b/core-processor/src/main/resources/META-INF/services/io.micronaut.inject.annotation.AnnotationTransformer index 6904307151d..5d45d537b5b 100644 --- a/core-processor/src/main/resources/META-INF/services/io.micronaut.inject.annotation.AnnotationTransformer +++ b/core-processor/src/main/resources/META-INF/services/io.micronaut.inject.annotation.AnnotationTransformer @@ -4,4 +4,6 @@ io.micronaut.inject.annotation.internal.KotlinNullableMapper io.micronaut.inject.annotation.internal.KotlinNotNullMapper io.micronaut.inject.annotation.internal.JakartaPostConstructTransformer io.micronaut.inject.annotation.internal.JakartaPreDestroyTransformer +io.micronaut.inject.annotation.internal.JakartaNullableTransformer +io.micronaut.inject.annotation.internal.JakartaNonnullTransformer io.micronaut.inject.beans.visitor.IntrospectedToBeanPropertiesTransformer diff --git a/inject-java/src/test/groovy/io/micronaut/annotation/NonNullabilityAnnotationsSpec.groovy b/inject-java/src/test/groovy/io/micronaut/annotation/NonNullabilityAnnotationsSpec.groovy new file mode 100644 index 00000000000..0e56c508e71 --- /dev/null +++ b/inject-java/src/test/groovy/io/micronaut/annotation/NonNullabilityAnnotationsSpec.groovy @@ -0,0 +1,74 @@ +package io.micronaut.annotation + +import io.micronaut.annotation.processing.test.AbstractTypeElementSpec +import io.micronaut.core.annotation.AnnotationUtil +import io.micronaut.core.beans.BeanMethod +import io.micronaut.inject.ast.ElementQuery + +class NonNullabilityAnnotationsSpec extends AbstractTypeElementSpec { + + void "test map nonnull annotation for #packageName in beans"() { + given: + def element = buildClassElement(""" +package test; +import ${packageName}.*; +@jakarta.inject.Singleton +class Test { + ${annotation} + String notNullableMethod(${annotation} String test) { + return ""; + } +} +""") + def nullableMethod = element.getEnclosedElement(ElementQuery.ALL_METHODS.named({ String st -> st == 'notNullableMethod' })).get() + + expect: + !nullableMethod.isNullable() + nullableMethod.isNonNull() + !nullableMethod.parameters[0].isNullable() + nullableMethod.parameters[0].isNonNull() + + where: + packageName | annotation + "io.micronaut.core.annotation" | "@NonNull" + "edu.umd.cs.findbugs.annotations" | "@NonNull" + "javax.annotation" | "@Nonnull" + "jakarta.annotation" | "@Nonnull" + "org.jetbrains.annotations" | "@NotNull" + } + + void "test map nonnull annotation for #packageName in introspections"() { + given: + def introspection = buildBeanIntrospection("test.Test", """ +package test; + +import ${packageName}.*; +import io.micronaut.core.annotation.Introspected; +import io.micronaut.context.annotation.Executable; + +@Introspected +class Test { + ${annotation} + @Executable + String notNullableMethod(${annotation} String test) { + return ""; + } +} +""") + BeanMethod nullableMethod = introspection.getBeanMethods()[0] + + expect: + !nullableMethod.getAnnotationMetadata().hasStereotype(AnnotationUtil.NULLABLE) + nullableMethod.getAnnotationMetadata().hasStereotype(AnnotationUtil.NON_NULL) + !nullableMethod.arguments[0].isNullable() + nullableMethod.arguments[0].isNonNull() + + where: + packageName | annotation + "io.micronaut.core.annotation" | "@NonNull" + "edu.umd.cs.findbugs.annotations" | "@NonNull" + "javax.annotation" | "@Nonnull" + "jakarta.annotation" | "@Nonnull" + "org.jetbrains.annotations" | "@NotNull" + } +} diff --git a/inject-java/src/test/groovy/io/micronaut/annotation/NullabilityAnnotationsSpec.groovy b/inject-java/src/test/groovy/io/micronaut/annotation/NullabilityAnnotationsSpec.groovy index ce49cf25762..825c47ba7d3 100644 --- a/inject-java/src/test/groovy/io/micronaut/annotation/NullabilityAnnotationsSpec.groovy +++ b/inject-java/src/test/groovy/io/micronaut/annotation/NullabilityAnnotationsSpec.groovy @@ -29,7 +29,7 @@ class Test { !nullableMethod.parameters[0].isNonNull() where: - packageName << ["io.micronaut.core.annotation", "javax.annotation", "org.jetbrains.annotations", "edu.umd.cs.findbugs.annotations"] + packageName << ["io.micronaut.core.annotation", "javax.annotation", "org.jetbrains.annotations", "jakarta.annotation","edu.umd.cs.findbugs.annotations"] } void "test map nullable annotation for #packageName in introspections"() { @@ -59,6 +59,6 @@ class Test { !nullableMethod.arguments[0].isNonNull() where: - packageName << ["io.micronaut.core.annotation", "javax.annotation", "org.jetbrains.annotations", "edu.umd.cs.findbugs.annotations"] + packageName << ["io.micronaut.core.annotation", "javax.annotation", "org.jetbrains.annotations", "jakarta.annotation","edu.umd.cs.findbugs.annotations"] } } From 51fe87f97f568306daa8aad2d40d3627e94a6120 Mon Sep 17 00:00:00 2001 From: Graeme Rocher Date: Wed, 9 Nov 2022 10:55:14 +0100 Subject: [PATCH 203/743] remove javax.annotation.Nullable usage (#8311) --- core/build.gradle | 2 +- .../io/micronaut/core/annotation/NonNull.java | 5 +- .../micronaut/core/annotation/Nullable.java | 6 +-- .../core/beans/BeanIntrospection.java | 3 +- .../core/beans/BeanIntrospector.java | 3 +- .../io/micronaut/core/beans/BeanProperty.java | 3 +- .../java/io/micronaut/core/io/Readable.java | 13 +++-- .../util/clhm/ConcurrentLinkedHashMap.java | 51 +++++++++---------- .../core/util/clhm/EntryWeigher.java | 4 +- .../core/util/clhm/EvictionListener.java | 4 +- .../micronaut/core/util/clhm/LinkedDeque.java | 3 +- .../retry/intercept/CircuitBreakerSpec.groovy | 3 +- 12 files changed, 45 insertions(+), 55 deletions(-) diff --git a/core/build.gradle b/core/build.gradle index 97e12a727e7..0585c83a44c 100644 --- a/core/build.gradle +++ b/core/build.gradle @@ -12,7 +12,7 @@ micronautBuild { } dependencies { - compileOnly libs.managed.jsr305 + compileOnly libs.managed.jakarta.annotation.api compileOnly libs.managed.graal compileOnly libs.kotlin.stdlib } diff --git a/core/src/main/java/io/micronaut/core/annotation/NonNull.java b/core/src/main/java/io/micronaut/core/annotation/NonNull.java index f2c9257edcf..2835e08068f 100644 --- a/core/src/main/java/io/micronaut/core/annotation/NonNull.java +++ b/core/src/main/java/io/micronaut/core/annotation/NonNull.java @@ -15,8 +15,8 @@ */ package io.micronaut.core.annotation; -import javax.annotation.Nonnull; -import javax.annotation.meta.TypeQualifierNickname; +import jakarta.annotation.Nonnull; + import java.lang.annotation.Documented; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; @@ -43,6 +43,5 @@ @Retention(RetentionPolicy.RUNTIME) @Documented @Nonnull -@TypeQualifierNickname public @interface NonNull { } diff --git a/core/src/main/java/io/micronaut/core/annotation/Nullable.java b/core/src/main/java/io/micronaut/core/annotation/Nullable.java index bf198c3275e..1a47051130a 100644 --- a/core/src/main/java/io/micronaut/core/annotation/Nullable.java +++ b/core/src/main/java/io/micronaut/core/annotation/Nullable.java @@ -15,9 +15,6 @@ */ package io.micronaut.core.annotation; -import javax.annotation.Nonnull; -import javax.annotation.meta.TypeQualifierNickname; -import javax.annotation.meta.When; import java.lang.annotation.Documented; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; @@ -38,7 +35,6 @@ @Target({ElementType.METHOD, ElementType.PARAMETER, ElementType.FIELD, ElementType.ANNOTATION_TYPE}) @Retention(RetentionPolicy.RUNTIME) @Documented -@Nonnull(when = When.MAYBE) -@TypeQualifierNickname +@jakarta.annotation.Nullable public @interface Nullable { } diff --git a/core/src/main/java/io/micronaut/core/beans/BeanIntrospection.java b/core/src/main/java/io/micronaut/core/beans/BeanIntrospection.java index e2ffdf4f4fb..71db2bb158a 100644 --- a/core/src/main/java/io/micronaut/core/beans/BeanIntrospection.java +++ b/core/src/main/java/io/micronaut/core/beans/BeanIntrospection.java @@ -22,7 +22,6 @@ import io.micronaut.core.util.ArgumentUtils; import io.micronaut.core.annotation.NonNull; -import javax.annotation.concurrent.Immutable; import java.lang.annotation.Annotation; import java.util.Collection; import java.util.Collections; @@ -44,7 +43,7 @@ * @author graemerocher * @since 1.1 */ -@Immutable +//@Immutable public interface BeanIntrospection extends AnnotationMetadataDelegate, BeanInfo { /** diff --git a/core/src/main/java/io/micronaut/core/beans/BeanIntrospector.java b/core/src/main/java/io/micronaut/core/beans/BeanIntrospector.java index 1d435a9286a..19d713da363 100644 --- a/core/src/main/java/io/micronaut/core/beans/BeanIntrospector.java +++ b/core/src/main/java/io/micronaut/core/beans/BeanIntrospector.java @@ -19,7 +19,6 @@ import io.micronaut.core.util.ArgumentUtils; import io.micronaut.core.annotation.NonNull; -import javax.annotation.concurrent.Immutable; import java.lang.annotation.Annotation; import java.util.Arrays; import java.util.Collection; @@ -34,7 +33,7 @@ * @see io.micronaut.core.annotation.Introspected * @see BeanIntrospection */ -@Immutable +//@Immutable public interface BeanIntrospector { /** diff --git a/core/src/main/java/io/micronaut/core/beans/BeanProperty.java b/core/src/main/java/io/micronaut/core/beans/BeanProperty.java index fafdf269c42..2facff087e1 100644 --- a/core/src/main/java/io/micronaut/core/beans/BeanProperty.java +++ b/core/src/main/java/io/micronaut/core/beans/BeanProperty.java @@ -27,7 +27,6 @@ import io.micronaut.core.annotation.NonNull; import io.micronaut.core.annotation.Nullable; -import javax.annotation.concurrent.Immutable; import java.util.Arrays; import java.util.Collection; import java.util.Optional; @@ -45,7 +44,7 @@ * @since 1.1 * @see BeanIntrospection */ -@Immutable +//@Immutable public interface BeanProperty extends AnnotatedElement, AnnotationMetadataDelegate, ArgumentCoercible { /** diff --git a/core/src/main/java/io/micronaut/core/io/Readable.java b/core/src/main/java/io/micronaut/core/io/Readable.java index 2f3fd7ab403..f209b81cebd 100644 --- a/core/src/main/java/io/micronaut/core/io/Readable.java +++ b/core/src/main/java/io/micronaut/core/io/Readable.java @@ -19,8 +19,12 @@ import io.micronaut.core.util.ArgumentUtils; import io.micronaut.core.annotation.NonNull; -import javax.annotation.concurrent.Immutable; -import java.io.*; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Reader; import java.net.URL; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; @@ -32,7 +36,7 @@ * @author graemerocher * @since 1.1.0 */ -@Immutable +//@Immutable public interface Readable extends Named { /** @@ -41,7 +45,8 @@ public interface Readable extends Named { * @return The input stream * @throws IOException if an I/O exception occurs */ - @NonNull InputStream asInputStream() throws IOException; + @NonNull + InputStream asInputStream() throws IOException; /** * Does the underlying readable resource exist. diff --git a/core/src/main/java/io/micronaut/core/util/clhm/ConcurrentLinkedHashMap.java b/core/src/main/java/io/micronaut/core/util/clhm/ConcurrentLinkedHashMap.java index 317512eba8f..92be41a68e4 100644 --- a/core/src/main/java/io/micronaut/core/util/clhm/ConcurrentLinkedHashMap.java +++ b/core/src/main/java/io/micronaut/core/util/clhm/ConcurrentLinkedHashMap.java @@ -16,9 +16,6 @@ package io.micronaut.core.util.clhm; -import javax.annotation.concurrent.GuardedBy; -import javax.annotation.concurrent.Immutable; -import javax.annotation.concurrent.ThreadSafe; import java.io.InvalidObjectException; import java.io.ObjectInputStream; import java.io.Serializable; @@ -101,7 +98,7 @@ * @see * https://code.google.com/p/concurrentlinkedhashmap/ */ -@ThreadSafe +// @ThreadSafe public final class ConcurrentLinkedHashMap extends AbstractMap implements ConcurrentMap, Serializable { @@ -181,14 +178,14 @@ public final class ConcurrentLinkedHashMap extends AbstractMap private final int concurrencyLevel; // These fields provide support to bound the map by a maximum capacity - @GuardedBy("evictionLock") + // @GuardedBy("evictionLock") private final long[] readBufferReadCount; - @GuardedBy("evictionLock") + // @GuardedBy("evictionLock") private final LinkedDeque> evictionDeque; - @GuardedBy("evictionLock") // must write under lock + // @GuardedBy("evictionLock") // must write under lock private final AtomicLong weightedSize; - @GuardedBy("evictionLock") // must write under lock + // @GuardedBy("evictionLock") // must write under lock private final AtomicLong capacity; private final Lock evictionLock; @@ -300,12 +297,12 @@ public void setCapacity(long capacity) { notifyListener(); } - @GuardedBy("evictionLock") + // @GuardedBy("evictionLock") private boolean hasOverflowed() { return weightedSize.get() > capacity.get(); } - @GuardedBy("evictionLock") + // @GuardedBy("evictionLock") private void evict() { // Attempts to evict entries from the map if it exceeds the maximum // capacity. If the eviction fails due to a concurrent removal of the @@ -416,14 +413,14 @@ void tryToDrainBuffers() { } /** Drains the read and write buffers up to an amortized threshold. */ - @GuardedBy("evictionLock") + // @GuardedBy("evictionLock") void drainBuffers() { drainReadBuffers(); drainWriteBuffer(); } /** Drains the read buffers, each up to an amortized threshold. */ - @GuardedBy("evictionLock") + // @GuardedBy("evictionLock") void drainReadBuffers() { final int start = (int) Thread.currentThread().getId(); final int end = start + NUMBER_OF_READ_BUFFERS; @@ -432,7 +429,7 @@ void drainReadBuffers() { } } - @GuardedBy("evictionLock") + // @GuardedBy("evictionLock") private void drainReadBuffer(int bufferIndex) { final long writeCount = readBufferWriteCount[bufferIndex].get(); for (int i = 0; i < READ_BUFFER_DRAIN_THRESHOLD; i++) { @@ -450,7 +447,7 @@ private void drainReadBuffer(int bufferIndex) { readBufferDrainAtWriteCount[bufferIndex].lazySet(writeCount); } - @GuardedBy("evictionLock") + // @GuardedBy("evictionLock") private void applyRead(Node node) { // An entry may be scheduled for reordering despite having been removed. // This can occur when the entry was concurrently read while a writer was @@ -462,7 +459,7 @@ private void applyRead(Node node) { } /** Drains the read buffer up to an amortized threshold. */ - @GuardedBy("evictionLock") + // @GuardedBy("evictionLock") void drainWriteBuffer() { for (int i = 0; i < WRITE_BUFFER_DRAIN_THRESHOLD; i++) { final Runnable task = writeBuffer.poll(); @@ -514,7 +511,7 @@ void makeRetired(Node node) { * * @param node the entry in the page replacement policy */ - @GuardedBy("evictionLock") + // @GuardedBy("evictionLock") void makeDead(Node node) { for (;;) { WeightedValue current = node.get(); @@ -1148,7 +1145,7 @@ enum DrainStatus { * * @param The value type **/ - @Immutable + // @Immutable private static final class WeightedValue { final int weight; final V value; @@ -1197,9 +1194,9 @@ boolean isDead() { private static final class Node extends AtomicReference> implements Linked> { final K key; - @GuardedBy("evictionLock") + // @GuardedBy("evictionLock") Node prev; - @GuardedBy("evictionLock") + // @GuardedBy("evictionLock") Node next; WeightedValue weightedValue; @@ -1211,25 +1208,25 @@ private static final class Node extends AtomicReference> } @Override - @GuardedBy("evictionLock") + // @GuardedBy("evictionLock") public Node getPrevious() { return prev; } @Override - @GuardedBy("evictionLock") + // @GuardedBy("evictionLock") public void setPrevious(Node prev) { this.prev = prev; } @Override - @GuardedBy("evictionLock") + // @GuardedBy("evictionLock") public Node getNext() { return next; } @Override - @GuardedBy("evictionLock") + // @GuardedBy("evictionLock") public void setNext(Node next) { this.next = next; } @@ -1505,7 +1502,7 @@ private enum DiscardingListener implements EvictionListener { INSTANCE; @Override public void onEviction(Object key, Object value) { - + // discard } } @@ -1590,7 +1587,7 @@ private final class AddTask implements Runnable { } @Override - @GuardedBy("evictionLock") + // @GuardedBy("evictionLock") public void run() { weightedSize.lazySet(weightedSize.get() + weight); @@ -1611,7 +1608,7 @@ private final class RemovalTask implements Runnable { } @Override - @GuardedBy("evictionLock") + // @GuardedBy("evictionLock") public void run() { // add may not have been processed yet evictionDeque.remove(node); @@ -1630,7 +1627,7 @@ private final class UpdateTask implements Runnable { } @Override - @GuardedBy("evictionLock") + // @GuardedBy("evictionLock") public void run() { weightedSize.lazySet(weightedSize.get() + weightDifference); applyRead(node); diff --git a/core/src/main/java/io/micronaut/core/util/clhm/EntryWeigher.java b/core/src/main/java/io/micronaut/core/util/clhm/EntryWeigher.java index 3136eb3aaf9..66f3dd13c44 100644 --- a/core/src/main/java/io/micronaut/core/util/clhm/EntryWeigher.java +++ b/core/src/main/java/io/micronaut/core/util/clhm/EntryWeigher.java @@ -16,8 +16,6 @@ package io.micronaut.core.util.clhm; -import javax.annotation.concurrent.ThreadSafe; - /** * A class that can determine the weight of an entry. The total weight threshold * is used to determine when an eviction is required. @@ -28,7 +26,7 @@ * @see * https://code.google.com/p/concurrentlinkedhashmap/ */ -@ThreadSafe +//@ThreadSafe public interface EntryWeigher { /** diff --git a/core/src/main/java/io/micronaut/core/util/clhm/EvictionListener.java b/core/src/main/java/io/micronaut/core/util/clhm/EvictionListener.java index a362ebb5d25..ae0c978139a 100644 --- a/core/src/main/java/io/micronaut/core/util/clhm/EvictionListener.java +++ b/core/src/main/java/io/micronaut/core/util/clhm/EvictionListener.java @@ -16,8 +16,6 @@ package io.micronaut.core.util.clhm; -import javax.annotation.concurrent.ThreadSafe; - /** * A listener registered for notification when an entry is evicted. An instance * may be called concurrently by multiple threads to process entries. An @@ -38,7 +36,7 @@ * @see * https://code.google.com/p/concurrentlinkedhashmap/ */ -@ThreadSafe +//@ThreadSafe public interface EvictionListener { /** diff --git a/core/src/main/java/io/micronaut/core/util/clhm/LinkedDeque.java b/core/src/main/java/io/micronaut/core/util/clhm/LinkedDeque.java index 79ccd643b54..d8753159398 100644 --- a/core/src/main/java/io/micronaut/core/util/clhm/LinkedDeque.java +++ b/core/src/main/java/io/micronaut/core/util/clhm/LinkedDeque.java @@ -16,7 +16,6 @@ package io.micronaut.core.util.clhm; -import javax.annotation.concurrent.NotThreadSafe; import java.util.AbstractCollection; import java.util.Collection; import java.util.Deque; @@ -45,7 +44,7 @@ * @see * https://code.google.com/p/concurrentlinkedhashmap/ */ -@NotThreadSafe +//@NotThreadSafe final class LinkedDeque> extends AbstractCollection implements Deque { // This class provides a doubly-linked list that is optimized for the virtual diff --git a/retry/src/test/groovy/io/micronaut/retry/intercept/CircuitBreakerSpec.groovy b/retry/src/test/groovy/io/micronaut/retry/intercept/CircuitBreakerSpec.groovy index 06e276f9a68..4ec6859bd92 100644 --- a/retry/src/test/groovy/io/micronaut/retry/intercept/CircuitBreakerSpec.groovy +++ b/retry/src/test/groovy/io/micronaut/retry/intercept/CircuitBreakerSpec.groovy @@ -25,6 +25,7 @@ import io.micronaut.retry.event.RetryEventListener import jakarta.inject.Singleton import org.reactivestreams.Publisher import reactor.core.publisher.Mono +import spock.lang.Retry import spock.lang.Specification import spock.util.concurrent.PollingConditions import io.micronaut.core.async.annotation.SingleResult @@ -35,7 +36,7 @@ import io.micronaut.core.async.annotation.SingleResult */ class CircuitBreakerSpec extends Specification{ - + @Retry void "test blocking circuit breaker"() { given: ApplicationContext context = ApplicationContext.run() From 44e3f884bb18f9c2ea0d86690d38bd32e4c9c19a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Champeau?= Date: Wed, 9 Nov 2022 13:09:50 +0100 Subject: [PATCH 204/743] Remove use of shadow plugin (#8312) This is now redundant, and it was causing strange errors because of incorrect published metadata: if the consumer was using a version of Java < 17, then there would be one compatible variant for both compilation and runtime, the `shadowRuntimeElements` variant, which would have the shadow jar as an artifact, but no dependency. This causes difficult to diagnose error messages, because of missing classes (typically, depending on `http-server-netty` wouldn't transitively bring the Micronaut runtime). --- buildSrc/build.gradle | 1 - .../io.micronaut.build.internal.convention-library.gradle | 7 ------- ...micronaut.build.internal.convention-core-library.gradle | 4 ---- 3 files changed, 12 deletions(-) diff --git a/buildSrc/build.gradle b/buildSrc/build.gradle index fa6d75fd610..e5bdcab2434 100644 --- a/buildSrc/build.gradle +++ b/buildSrc/build.gradle @@ -8,7 +8,6 @@ repositories { } dependencies { - implementation "gradle.plugin.com.github.johnrengelman:shadow:7.1.2" implementation "org.aim42:htmlSanityCheck:1.1.6" implementation "io.micronaut.build.internal:micronaut-gradle-plugins:5.3.15" implementation "org.tomlj:tomlj:1.1.0" diff --git a/buildSrc/src/main/groovy/io.micronaut.build.internal.convention-library.gradle b/buildSrc/src/main/groovy/io.micronaut.build.internal.convention-library.gradle index 450ca57fa2f..ecf6908ca37 100644 --- a/buildSrc/src/main/groovy/io.micronaut.build.internal.convention-library.gradle +++ b/buildSrc/src/main/groovy/io.micronaut.build.internal.convention-library.gradle @@ -1,11 +1,9 @@ plugins { id "io.micronaut.build.internal.base-module" id "io.micronaut.build.internal.convention-base" - id "com.github.johnrengelman.shadow" } configurations { - shadowCompile all { resolutionStrategy.eachDependency { DependencyResolveDetails details -> String group = details.requested.group @@ -16,14 +14,9 @@ configurations { } } -tasks.named("shadowJar") { - configurations = [project.configurations.shadowCompile] -} - micronautBuild { binaryCompatibility { // Enable before Micronaut 4 release enabled.set(false) } } - diff --git a/buildSrc/src/main/groovy/io/micronaut/build/internal/io.micronaut.build.internal.convention-core-library.gradle b/buildSrc/src/main/groovy/io/micronaut/build/internal/io.micronaut.build.internal.convention-core-library.gradle index 3f1b808f688..17fc6a7b9fb 100644 --- a/buildSrc/src/main/groovy/io/micronaut/build/internal/io.micronaut.build.internal.convention-core-library.gradle +++ b/buildSrc/src/main/groovy/io/micronaut/build/internal/io.micronaut.build.internal.convention-core-library.gradle @@ -1,7 +1,3 @@ plugins { id "io.micronaut.build.internal.convention-library" } - -ext { - shadowJarEnabled = true -} From 337b4414feb4c38ae9be9e43b5b87b5809b993c5 Mon Sep 17 00:00:00 2001 From: micronaut-build <65172877+micronaut-build@users.noreply.github.com> Date: Thu, 10 Nov 2022 01:27:08 -0500 Subject: [PATCH 205/743] build: bump micronaut-security to 3.8.2 (#8317) --- gradle.properties | 2 +- gradle/libs.versions.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle.properties b/gradle.properties index 0287a584f27..a062ad0f403 100644 --- a/gradle.properties +++ b/gradle.properties @@ -52,7 +52,7 @@ kotlin.stdlib.default.dependency=false # For the docs graalVersion=22.0.0.2 -micronautSecurityVersion=3.8.1 +micronautSecurityVersion=3.8.2 org.gradle.caching=true org.gradle.parallel=true diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 0f85ffc5f7e..3ba4fad361a 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -112,7 +112,7 @@ managed-micronaut-rss = "3.2.0" managed-micronaut-rxjava1 = "1.0.0" managed-micronaut-rxjava2 = "1.3.0" managed-micronaut-rxjava3 = "2.3.0" -managed-micronaut-security = "3.8.1" +managed-micronaut-security = "3.8.2" managed-micronaut-serialization = "1.3.3" managed-micronaut-servlet = "3.3.2" managed-micronaut-spring = "4.3.1" From 686b472bc4801887a115c5d06cb015301241875b Mon Sep 17 00:00:00 2001 From: Graeme Rocher Date: Thu, 10 Nov 2022 08:51:41 +0100 Subject: [PATCH 206/743] Disable Cloud environment deduction by default. (#8068) * Introduces a new method deduceCloudEnvironment to ApplicationContextBuilder that allows re-enabling of this functionality * Retains environment deduction only for the test environment Fixes #7758 --- .../context/ApplicationContextBuilder.java | 20 ++++++++++++- .../ApplicationContextConfiguration.java | 21 +++++++++++++ .../DefaultApplicationContextBuilder.java | 14 +++++++++ .../context/env/DefaultEnvironment.java | 12 ++++---- .../ApplicationContextBuilderSpec.groovy | 30 +++++++++++++++++-- .../context/env/DefaultEnvironmentSpec.groovy | 2 +- src/main/docs/guide/appendix/breaks.adoc | 4 +++ .../docs/guide/cloud/cloudConfiguration.adoc | 14 ++++++++- src/main/docs/guide/config/environments.adoc | 2 +- 9 files changed, 108 insertions(+), 11 deletions(-) diff --git a/inject/src/main/java/io/micronaut/context/ApplicationContextBuilder.java b/inject/src/main/java/io/micronaut/context/ApplicationContextBuilder.java index d0ba8ab5ab3..faf445bcce3 100644 --- a/inject/src/main/java/io/micronaut/context/ApplicationContextBuilder.java +++ b/inject/src/main/java/io/micronaut/context/ApplicationContextBuilder.java @@ -97,13 +97,31 @@ public interface ApplicationContextBuilder { @NonNull ApplicationContextBuilder singletons(@Nullable Object... beans); /** - * Whether to deduce environments. + * If set to {@code true} (the default is {@code true}) Micronaut will attempt to automatically deduce the environment + * it is running in using environment variables and/or stack trace inspection. + * + *

This method differs from {@link #deduceCloudEnvironment(boolean)} which performs extended network and/or disk probes + * to try and automatically establish the Cloud environment.

+ * + *

This behaviour controls the automatic activation of, for example, the {@link io.micronaut.context.env.Environment#TEST} when running tests.

* * @param deduceEnvironment The boolean * @return This builder */ @NonNull ApplicationContextBuilder deduceEnvironment(@Nullable Boolean deduceEnvironment); + /** + * If set to {@code true} (the default value is {@code false}) Micronaut will attempt to automatically deduce the Cloud environment it is running within. + * + *

Enabling this should be done with caution since network probes are required to figure out whether the application is + * running in certain clouds like GCP.

+ * + * @param deduceEnvironment The boolean + * @return This builder + * @since 4.0.0 + */ + @NonNull ApplicationContextBuilder deduceCloudEnvironment(boolean deduceEnvironment); + /** * The environments to use. * diff --git a/inject/src/main/java/io/micronaut/context/ApplicationContextConfiguration.java b/inject/src/main/java/io/micronaut/context/ApplicationContextConfiguration.java index 0ebedf19b12..a3ce05f3b6f 100644 --- a/inject/src/main/java/io/micronaut/context/ApplicationContextConfiguration.java +++ b/inject/src/main/java/io/micronaut/context/ApplicationContextConfiguration.java @@ -39,12 +39,33 @@ public interface ApplicationContextConfiguration extends BeanContextConfiguratio @NonNull List getEnvironments(); /** + * If set to {@code true} (the default is {@code true}) Micronaut will attempt to automatically deduce the environment + * it is running in using environment variables and/or stack trace inspection. + * + *

This method differs from {@link #isDeduceCloudEnvironment()} which controls whether network and/or disk probes + * are performed to try and automatically establish the Cloud environment.

+ * + *

This behaviour controls the automatic activation of, for example, the {@link io.micronaut.context.env.Environment#TEST} when running tests.

+ * * @return True if the environments should be deduced */ default Optional getDeduceEnvironments() { return Optional.empty(); } + /** + * If set to {@code true} Micronaut will attempt to deduce the environment using safe methods like environment variables and the stack trace. + * + *

Enabling this should be done with caution since network probes are required to figure out whether the application is + * running in certain clouds like GCP.

+ * + * @return True if the environments should be deduced + * @since 4.0.0 + */ + default boolean isDeduceCloudEnvironment() { + return false; + } + /** * @return The default environments to be applied if no other environments * are explicitly specified or deduced. diff --git a/inject/src/main/java/io/micronaut/context/DefaultApplicationContextBuilder.java b/inject/src/main/java/io/micronaut/context/DefaultApplicationContextBuilder.java index 20445eca4d2..84efadc497e 100644 --- a/inject/src/main/java/io/micronaut/context/DefaultApplicationContextBuilder.java +++ b/inject/src/main/java/io/micronaut/context/DefaultApplicationContextBuilder.java @@ -55,6 +55,7 @@ public class DefaultApplicationContextBuilder implements ApplicationContextBuild private final Collection configurationIncludes = new HashSet<>(); private final Collection configurationExcludes = new HashSet<>(); private Boolean deduceEnvironments = null; + private boolean deduceCloudEnvironment = false; private ClassLoader classLoader = getClass().getClassLoader(); private boolean envPropertySource = true; private final List envVarIncludes = new ArrayList<>(); @@ -67,6 +68,7 @@ public class DefaultApplicationContextBuilder implements ApplicationContextBuild private boolean allowEmptyProviders = false; private Boolean bootstrapEnvironment = null; private boolean enableDefaultPropertySources = true; + ; /** * Default constructor. @@ -169,6 +171,12 @@ public ClassLoader getClassLoader() { return this; } + @Override + public ApplicationContextBuilder deduceCloudEnvironment(boolean deduceEnvironment) { + this.deduceCloudEnvironment = deduceEnvironment; + return this; + } + @Override public @NonNull ApplicationContextBuilder environments(@Nullable String... environments) { if (environments != null) { @@ -236,6 +244,12 @@ public Optional getDeduceEnvironments() { return Optional.ofNullable(deduceEnvironments); } + @Override + public boolean isDeduceCloudEnvironment() { + boolean basicDeduce = getDeduceEnvironments().orElse(true); + return basicDeduce && this.deduceCloudEnvironment; + } + @Override public @NonNull List getEnvironments() { return environments; diff --git a/inject/src/main/java/io/micronaut/context/env/DefaultEnvironment.java b/inject/src/main/java/io/micronaut/context/env/DefaultEnvironment.java index 9d5c234985c..1d407213680 100644 --- a/inject/src/main/java/io/micronaut/context/env/DefaultEnvironment.java +++ b/inject/src/main/java/io/micronaut/context/env/DefaultEnvironment.java @@ -639,16 +639,18 @@ private Optional> readPropertiesFromLoader(String fileName, private EnvironmentsAndPackage getEnvironmentsAndPackage(List specifiedNames) { EnvironmentsAndPackage environmentsAndPackage = this.environmentsAndPackage; - final boolean extendedDeduction = !specifiedNames.contains(Environment.FUNCTION); + boolean isNotFunction = !specifiedNames.contains(Environment.FUNCTION); + final boolean deduceEnvironment = shouldDeduceEnvironments(); + final boolean deduceCloudEnvironmentUsingProbes = isNotFunction && configuration.isDeduceCloudEnvironment(); if (environmentsAndPackage == null) { synchronized (EnvironmentsAndPackage.class) { // double check environmentsAndPackage = this.environmentsAndPackage; if (environmentsAndPackage == null) { environmentsAndPackage = deduceEnvironmentsAndPackage( - shouldDeduceEnvironments(), - extendedDeduction, - extendedDeduction, - !extendedDeduction + deduceEnvironment, + deduceCloudEnvironmentUsingProbes, + isNotFunction, + !deduceCloudEnvironmentUsingProbes ); this.environmentsAndPackage = environmentsAndPackage; } diff --git a/inject/src/test/groovy/io/micronaut/context/ApplicationContextBuilderSpec.groovy b/inject/src/test/groovy/io/micronaut/context/ApplicationContextBuilderSpec.groovy index fe777cf7195..9762bee83b6 100644 --- a/inject/src/test/groovy/io/micronaut/context/ApplicationContextBuilderSpec.groovy +++ b/inject/src/test/groovy/io/micronaut/context/ApplicationContextBuilderSpec.groovy @@ -21,6 +21,33 @@ class ApplicationContextBuilderSpec extends Specification { ctx.close() } + void "test default configuration"() { + given: + ApplicationContextBuilder builder = ApplicationContext.builder() + ApplicationContextConfiguration config = (ApplicationContextConfiguration) builder + + expect: + !config.deduceEnvironments.isPresent() + !config.deduceCloudEnvironment + config.bannerEnabled + config.enableDefaultPropertySources + config.environmentPropertySource + !config.eagerInitConfiguration + !config.eagerInitSingletons + } + + void "test enable cloud environment deduce"() { + given: + ApplicationContextBuilder builder = ApplicationContext.builder() + ApplicationContextConfiguration config = (ApplicationContextConfiguration) builder + + when: + builder.deduceCloudEnvironment(true) + + then: + config.deduceCloudEnvironment + } + void "test context configuration"() { given: ApplicationContextBuilder builder = ApplicationContext.builder() @@ -28,7 +55,7 @@ class ApplicationContextBuilderSpec extends Specification { builder.classLoader(loader) .environments("foo") .deduceEnvironment(false) - + .deduceCloudEnvironment(true) ApplicationContextConfiguration config = (ApplicationContextConfiguration) builder @@ -37,6 +64,5 @@ class ApplicationContextBuilderSpec extends Specification { config.resourceLoader.classLoader.is(loader) config.environments.contains('foo') config.deduceEnvironments.get() == false - } } diff --git a/inject/src/test/groovy/io/micronaut/context/env/DefaultEnvironmentSpec.groovy b/inject/src/test/groovy/io/micronaut/context/env/DefaultEnvironmentSpec.groovy index 7f9799f1736..2cd570cc558 100644 --- a/inject/src/test/groovy/io/micronaut/context/env/DefaultEnvironmentSpec.groovy +++ b/inject/src/test/groovy/io/micronaut/context/env/DefaultEnvironmentSpec.groovy @@ -350,7 +350,7 @@ class DefaultEnvironmentSpec extends Specification { void "test disable environment deduction via system property"() { when: System.setProperty(Environment.CLOUD_PLATFORM_PROPERTY, "GOOGLE_COMPUTE") - ApplicationContext ctx1 = ApplicationContext.run() + ApplicationContext ctx1 = ApplicationContext.builder().deduceCloudEnvironment(true).build().start() then: ctx1.environment.activeNames.contains(Environment.GOOGLE_COMPUTE) diff --git a/src/main/docs/guide/appendix/breaks.adoc b/src/main/docs/guide/appendix/breaks.adoc index 6c2eb8e423d..10e7e52a026 100644 --- a/src/main/docs/guide/appendix/breaks.adoc +++ b/src/main/docs/guide/appendix/breaks.adoc @@ -47,6 +47,10 @@ https://asm.ow2.io/[ASM] is no longer shaded into the `io.micronaut.asm` package https://github.com/ben-manes/caffeine[Caffeine] is no longer shaded into the `io.micronaut.caffeine` package. If you depend on this library you should directly depend on the latest version of Caffeine. +==== Environment Deduction Disabled by Default + +In previous versions of the Micronaut framework probes were used to attempt to deduce the running environment and establish whether the application was running in the Cloud. These probes involved network calls which resulted in issues with startup performance and security concerns. These probes have been disabled by default and can be re-enabled as necessary by calling `ApplicationContextBuilder.deduceCloudEnvironment(true)` if your application still requires this functionality. + ==== Update to Groovy 4 Micronaut now uses Groovy 4. diff --git a/src/main/docs/guide/cloud/cloudConfiguration.adoc b/src/main/docs/guide/cloud/cloudConfiguration.adoc index fbc398a6fab..f77cdd0a7c1 100644 --- a/src/main/docs/guide/cloud/cloudConfiguration.adoc +++ b/src/main/docs/guide/cloud/cloudConfiguration.adoc @@ -1,6 +1,18 @@ Applications built for the Cloud often need to adapt to running in a Cloud environment, read and share configuration in a distributed manner, and externalize configuration to the environment where necessary. -Micronaut's <> concept is by default Cloud platform-aware and makes a best effort to detect the underlying active environment. +Micronaut's <> concept can be configured to be Cloud platform-aware and makes a best effort to detect the underlying active Cloud environment. + +To enable this feature you can call `deduceCloudEnvironment(true)` on the api:context.ApplicationContextBuilder[] interface when starting Micronaut. For example: + +.Enabling Cloud Environment Detection +[source,java] +---- +public static void main(String...args) { + Micronaut.build(args) + .deduceCloudEnvironment(true) + .start(); +} +---- You can then use the api:context.annotation.Requires[] annotation to <>. diff --git a/src/main/docs/guide/config/environments.adoc b/src/main/docs/guide/config/environments.adoc index 743dda730ab..4544900f17b 100644 --- a/src/main/docs/guide/config/environments.adoc +++ b/src/main/docs/guide/config/environments.adoc @@ -16,7 +16,7 @@ $ java -Dmicronaut.environments=foo,bar -jar myapp.jar The above activates environments called `foo` and `bar`. -Finally, Cloud environment names are also detected. See the section on <> for more information. +It is also possible to enable the detection of the Cloud environment the application is deployed to (this feature is disabled by default since Micronaut 4). See the section on <> for more information. == Environment Priority From dd7f7ad4be51de420d7f795a8f7e2388918ce1f1 Mon Sep 17 00:00:00 2001 From: Graeme Rocher Date: Thu, 10 Nov 2022 09:38:03 +0100 Subject: [PATCH 207/743] Improve support for configuration property nesting (#8306) This PR allows records and interfaces to be more effectively nested. Also introduces support for correctly parsing javadoc descriptions for the generated JSON that allows completion of configuration properties. --- .../context/env/ConfigurationAdvice.java | 1 - .../env/ConfigurationIntroductionAdvice.java | 102 +++++++++------- core-processor/build.gradle | 1 + .../visitor/ConfigurationReaderVisitor.java | 60 +++++++--- .../ConfigurationMetadataBuilder.java | 51 +++++++- ...ConfigurationReaderBeanElementCreator.java | 49 ++++---- .../test/AbstractTypeElementSpec.groovy | 12 +- .../itfce/InterfaceNestingSpec.groovy | 72 ++++++++++++ .../records/RecordNestingSpec.groovy | 66 +++++++++++ .../ConfigurationMetadataSpec.groovy | 110 ++++++++++++++++++ 10 files changed, 444 insertions(+), 80 deletions(-) create mode 100644 inject-java/src/test/groovy/io/micronaut/inject/configproperties/itfce/InterfaceNestingSpec.groovy create mode 100644 inject-java/src/test/groovy/io/micronaut/inject/configproperties/records/RecordNestingSpec.groovy diff --git a/context/src/main/java/io/micronaut/runtime/context/env/ConfigurationAdvice.java b/context/src/main/java/io/micronaut/runtime/context/env/ConfigurationAdvice.java index af5b09e6312..0ca6b3d6a77 100644 --- a/context/src/main/java/io/micronaut/runtime/context/env/ConfigurationAdvice.java +++ b/context/src/main/java/io/micronaut/runtime/context/env/ConfigurationAdvice.java @@ -31,7 +31,6 @@ */ @Retention(RetentionPolicy.RUNTIME) @Introduction -@Type(ConfigurationIntroductionAdvice.class) @Internal public @interface ConfigurationAdvice { /** diff --git a/context/src/main/java/io/micronaut/runtime/context/env/ConfigurationIntroductionAdvice.java b/context/src/main/java/io/micronaut/runtime/context/env/ConfigurationIntroductionAdvice.java index 8789c3bd9b4..f0df0e83137 100644 --- a/context/src/main/java/io/micronaut/runtime/context/env/ConfigurationIntroductionAdvice.java +++ b/context/src/main/java/io/micronaut/runtime/context/env/ConfigurationIntroductionAdvice.java @@ -15,6 +15,7 @@ */ package io.micronaut.runtime.context.env; +import io.micronaut.aop.InterceptorBean; import io.micronaut.aop.MethodInterceptor; import io.micronaut.aop.MethodInvocationContext; import io.micronaut.context.BeanContext; @@ -32,6 +33,7 @@ import io.micronaut.core.value.PropertyNotFoundException; import io.micronaut.inject.qualifiers.Qualifiers; +import java.util.Collections; import java.util.Optional; /** @@ -45,6 +47,7 @@ @Prototype @Internal @BootstrapContextCompatible +@InterceptorBean(ConfigurationAdvice.class) public class ConfigurationIntroductionAdvice implements MethodInterceptor { private static final String MEMBER_BEAN = "bean"; @@ -63,7 +66,7 @@ public class ConfigurationIntroductionAdvice implements MethodInterceptor qualifier, Environment environment, BeanContext beanContext) { this.environment = environment; this.beanContext = beanContext; - this.name = qualifier instanceof Named ? ((Named) qualifier).getName() : null; + this.name = qualifier instanceof Named named ? named.getName() : null; } @Nullable @@ -71,54 +74,67 @@ public class ConfigurationIntroductionAdvice implements MethodInterceptor context) { final ReturnType rt = context.getReturnType(); final Class returnType = rt.getType(); + final Argument argument = rt.asArgument(); if (context.isTrue(ConfigurationAdvice.class, MEMBER_BEAN)) { - final Qualifier qualifier = name != null ? Qualifiers.byName(name) : null; - - if (context.isNullable()) { - final Object v = beanContext.findBean(returnType, qualifier).orElse(null); - if (v != null) { - return environment.convertRequired(v, returnType); - } else { - return v; - } - } else { - return environment.convertRequired( - beanContext.getBean(returnType, qualifier), - returnType - ); - } + return resolveBean(context, returnType, argument); } else { - String property = context.stringValue(Property.class, MEMBER_NAME).orElse(null); - if (property == null) { - throw new IllegalStateException("No property name available to resolve"); - } - boolean iterable = property.indexOf('*') > -1; - if (iterable) { - if (name != null) { - property = property.replace("*", name); - } - } - final String defaultValue = context.stringValue(Bindable.class, "defaultValue").orElse(null); - final Argument argument = rt.asArgument(); + return resolveProperty(context, rt, argument); + } + } - final Optional value = environment.getProperty( - property, - argument - ); + private Object resolveProperty(MethodInvocationContext context, ReturnType rt, Argument argument) { + String property = context.stringValue(Property.class, MEMBER_NAME).orElse(null); + if (property == null) { + throw new IllegalStateException("No property name available to resolve"); + } + boolean iterable = property.indexOf('*') > -1; + if (iterable && name != null) { + property = property.replace("*", name); + } + final String defaultValue = context.stringValue(Bindable.class, "defaultValue").orElse(null); - if (defaultValue != null) { - return value.orElseGet(() -> environment.convertRequired( - defaultValue, - argument - )); - } else if (rt.isOptional()) { - return value.orElse(Optional.empty()); - } else if (context.isNullable()) { - return value.orElse(null); + final Optional value = environment.getProperty( + property, + argument + ); + + if (defaultValue != null) { + return value.orElseGet(() -> environment.convertRequired( + defaultValue, + argument + )); + } else if (rt.isOptional()) { + return value.orElse(Optional.empty()); + } else if (context.isNullable()) { + return value.orElse(null); + } else { + String finalProperty = property; + return value.orElseThrow(() -> new PropertyNotFoundException(finalProperty, argument.getType())); + } + } + + private Object resolveBean(MethodInvocationContext context, Class returnType, Argument argument) { + final Qualifier qualifier = name != null ? Qualifiers.byName(name) : null; + if (Iterable.class.isAssignableFrom(returnType)) { + @SuppressWarnings("unchecked") + Argument firstArg = (Argument) argument.getFirstTypeVariable().orElse(null); + if (firstArg != null) { + return environment.convertRequired(beanContext.getBeansOfType(firstArg, qualifier), argument); } else { - String finalProperty = property; - return value.orElseThrow(() -> new PropertyNotFoundException(finalProperty, argument.getType())); + return environment.convertRequired(Collections.emptyMap(), argument); } + } else if (context.isNullable()) { + final Object v = beanContext.findBean(argument, qualifier).orElse(null); + if (v != null) { + return environment.convertRequired(v, returnType); + } else { + return v; + } + } else { + return environment.convertRequired( + beanContext.getBean(argument, qualifier), + returnType + ); } } } diff --git a/core-processor/build.gradle b/core-processor/build.gradle index 92a41fbfa4f..fd14f4c6ceb 100644 --- a/core-processor/build.gradle +++ b/core-processor/build.gradle @@ -7,6 +7,7 @@ dependencies { api project(":aop") api libs.asm.tree api libs.bundles.asm + api 'com.github.javaparser:javaparser-symbol-solver-core:3.24.7' compileOnly libs.kotlin.stdlib.jdk8 compileOnly libs.managed.validation diff --git a/core-processor/src/main/java/io/micronaut/context/visitor/ConfigurationReaderVisitor.java b/core-processor/src/main/java/io/micronaut/context/visitor/ConfigurationReaderVisitor.java index ca708330ac4..6adc6bc9294 100644 --- a/core-processor/src/main/java/io/micronaut/context/visitor/ConfigurationReaderVisitor.java +++ b/core-processor/src/main/java/io/micronaut/context/visitor/ConfigurationReaderVisitor.java @@ -15,6 +15,8 @@ */ package io.micronaut.context.visitor; +import io.micronaut.context.BeanProvider; +import io.micronaut.context.annotation.Bean; import io.micronaut.context.annotation.ConfigurationReader; import io.micronaut.context.annotation.EachProperty; import io.micronaut.context.annotation.Property; @@ -26,6 +28,7 @@ import io.micronaut.core.annotation.NonNull; import io.micronaut.core.bind.annotation.Bindable; import io.micronaut.core.naming.NameUtils; +import io.micronaut.inject.ast.ParameterElement; import io.micronaut.inject.validation.RequiresValidation; import io.micronaut.inject.ast.ClassElement; import io.micronaut.inject.ast.MethodElement; @@ -33,6 +36,9 @@ import io.micronaut.inject.configuration.ConfigurationMetadataBuilder; import io.micronaut.inject.visitor.TypeElementVisitor; import io.micronaut.inject.visitor.VisitorContext; +import jakarta.inject.Provider; + +import java.util.Map; /** * The visitor adds Validated annotation if one of the parameters is a constraint or @Valid. @@ -95,6 +101,26 @@ public void visitMethod(MethodElement method, VisitorContext context) { } } + public static boolean isPropertyParameter(ParameterElement parameter, VisitorContext visitorContext) { + ClassElement genericType = parameter.getGenericType(); + return isPropertyParameter(genericType, visitorContext); + } + + private static boolean isPropertyParameter(ClassElement genericType, VisitorContext visitorContext) { + if (genericType.isOptional() || genericType.isAssignable(BeanProvider.class) || genericType.isAssignable(Provider.class) || genericType.isAssignable(Iterable.class)) { + ClassElement finalParameterType = genericType; + genericType = genericType.getOptionalValueType().or(finalParameterType::getFirstTypeArgument).orElse(genericType); + // Get the class with type annotations + genericType = visitorContext.getClassElement(genericType.getCanonicalName()).orElse(genericType); + } else if (genericType.isAssignable(Map.class)) { + ClassElement t = genericType.getTypeArguments().get("V"); + if (t != null) { + genericType = t; + } + } + return !genericType.hasStereotype(AnnotationUtil.SCOPE) && !genericType.hasStereotype(Bean.class); + } + private void visitAbstractMethod(MethodElement method, VisitorContext context) { String methodName = method.getName(); if (!isGetter(methodName)) { @@ -110,21 +136,24 @@ private void visitAbstractMethod(MethodElement method, VisitorContext context) { return; } - final String propertyName = getPropertyNameForGetter(methodName); - - String path = metadataBuilder.visitProperty( - method.getOwningType(), - method.getOwningType(), // interface methods don't inherit the prefix - method.getReturnType(), - propertyName, - method.getDocumentation().orElse(null), - method.getAnnotationMetadata().stringValue(Bindable.class, "defaultValue").orElse(null) - ).getPath(); - - method.annotate(Property.class, builder -> builder.member("name", path)); + boolean isPropertyParameter = isPropertyParameter(method.getGenericReturnType(), context); + if (isPropertyParameter) { + final String propertyName = getPropertyNameForGetter(methodName); + String path = metadataBuilder.visitProperty( + method.getOwningType(), + method.getOwningType(), // interface methods don't inherit the prefix + method.getReturnType(), + propertyName, + ConfigurationMetadataBuilder.resolveJavadocDescription(method), + method.getAnnotationMetadata().stringValue(Bindable.class, "defaultValue").orElse(null) + ).getPath(); + + method.annotate(Property.class, builder -> builder.member("name", path)); + } method.annotate(ANN_CONFIGURATION_ADVICE, annBuilder -> { - if (!method.getReturnType().isPrimitive() && method.getReturnType().hasStereotype(AnnotationUtil.SCOPE)) { + + if (!isPropertyParameter) { annBuilder.member("bean", true); } if (method.hasStereotype(EachProperty.class)) { @@ -133,6 +162,11 @@ private void visitAbstractMethod(MethodElement method, VisitorContext context) { }); } + private static boolean isBeanReturnType(MethodElement method) { + ClassElement returnType = method.getGenericReturnType(); + return !returnType.isPrimitive() && returnType.hasStereotype(AnnotationUtil.SCOPE); + } + private String getPropertyNameForGetter(String methodName) { return NameUtils.getPropertyNameForGetter(methodName, readPrefixes); } diff --git a/core-processor/src/main/java/io/micronaut/inject/configuration/ConfigurationMetadataBuilder.java b/core-processor/src/main/java/io/micronaut/inject/configuration/ConfigurationMetadataBuilder.java index d7b772f0b87..baf7afd59ab 100644 --- a/core-processor/src/main/java/io/micronaut/inject/configuration/ConfigurationMetadataBuilder.java +++ b/core-processor/src/main/java/io/micronaut/inject/configuration/ConfigurationMetadataBuilder.java @@ -15,6 +15,12 @@ */ package io.micronaut.inject.configuration; +import com.github.javaparser.StaticJavaParser; +import com.github.javaparser.javadoc.Javadoc; +import com.github.javaparser.javadoc.JavadocBlockTag; +import com.github.javaparser.javadoc.description.JavadocDescription; +import com.github.javaparser.javadoc.description.JavadocDescriptionElement; +import com.github.javaparser.javadoc.description.JavadocSnippet; import io.micronaut.context.annotation.ConfigurationReader; import io.micronaut.core.annotation.Internal; import io.micronaut.core.annotation.NonNull; @@ -23,6 +29,8 @@ import io.micronaut.core.util.CollectionUtils; import io.micronaut.inject.ast.ClassElement; import io.micronaut.inject.ast.Element; +import io.micronaut.inject.ast.MethodElement; +import io.micronaut.inject.ast.PropertyElement; import io.micronaut.inject.writer.OriginatingElements; import java.io.IOException; @@ -93,13 +101,54 @@ public ConfigurationMetadata visitProperties(ClassElement classElement) { ConfigurationMetadata configurationMetadata = new ConfigurationMetadata(); configurationMetadata.name = NameUtils.hyphenate(path, true); configurationMetadata.type = classElement.getType().getName(); - configurationMetadata.description = classElement.getDocumentation().orElse(null); + configurationMetadata.description = resolveJavadocDescription(classElement); configurationMetadata.includes = CollectionUtils.setOf(classElement.stringValues(ConfigurationReader.class, "includes")); configurationMetadata.excludes = CollectionUtils.setOf(classElement.stringValues(ConfigurationReader.class, "excludes")); this.configurations.add(configurationMetadata); return configurationMetadata; } + /** + * Resolves the javadoc description for the given element. + * @param element The element + * @return The javadoc description. + */ + @Nullable + public static String resolveJavadocDescription(@NonNull Element element) { + String resolvedDocs = null; + String javadoc = element.getDocumentation().orElse(null); + if (javadoc == null && element instanceof PropertyElement propertyElement) { + javadoc = propertyElement.getWriteMethod().flatMap(Element::getDocumentation).orElse(null); + } + if (javadoc != null) { + try { + Javadoc jd = StaticJavaParser.parseJavadoc(javadoc); + JavadocDescription description = jd.getDescription(); + StringBuilder builder = new StringBuilder(); + List elements = description.getElements(); + if (!elements.isEmpty()) { + for (JavadocDescriptionElement jde : elements) { + if (jde instanceof JavadocSnippet snippet) { + builder.append(snippet.toText()); + } + } + } else if (element instanceof MethodElement) { + jd.getBlockTags() + .stream().filter(bt -> bt.getType() == JavadocBlockTag.Type.RETURN) + .findFirst().ifPresent(returnTag -> builder.append(returnTag.getContent().toText())); + } else if (element instanceof PropertyElement) { + jd.getBlockTags() + .stream().filter(bt -> bt.getType() == JavadocBlockTag.Type.PARAM) + .findFirst().ifPresent(returnTag -> builder.append(returnTag.getContent().toText())); + } + resolvedDocs = builder.toString(); + } catch (Exception e) { + // ignore + } + } + return resolvedDocs; + } + /** * Visit a configuration property. * diff --git a/core-processor/src/main/java/io/micronaut/inject/processing/ConfigurationReaderBeanElementCreator.java b/core-processor/src/main/java/io/micronaut/inject/processing/ConfigurationReaderBeanElementCreator.java index e8f70057772..35802179e60 100644 --- a/core-processor/src/main/java/io/micronaut/inject/processing/ConfigurationReaderBeanElementCreator.java +++ b/core-processor/src/main/java/io/micronaut/inject/processing/ConfigurationReaderBeanElementCreator.java @@ -15,8 +15,9 @@ */ package io.micronaut.inject.processing; -import io.micronaut.context.BeanProvider; -import io.micronaut.context.annotation.Bean; +import com.github.javaparser.StaticJavaParser; +import com.github.javaparser.javadoc.Javadoc; +import com.github.javaparser.javadoc.JavadocBlockTag; import io.micronaut.context.annotation.ConfigurationBuilder; import io.micronaut.context.annotation.ConfigurationInject; import io.micronaut.context.annotation.ConfigurationReader; @@ -24,10 +25,12 @@ import io.micronaut.context.annotation.Parameter; import io.micronaut.context.annotation.Property; import io.micronaut.context.annotation.Value; +import io.micronaut.context.visitor.ConfigurationReaderVisitor; import io.micronaut.core.annotation.AnnotationMetadata; import io.micronaut.core.annotation.AnnotationUtil; import io.micronaut.core.annotation.AnnotationValue; import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.Nullable; import io.micronaut.core.bind.annotation.Bindable; import io.micronaut.inject.annotation.AnnotationMetadataHierarchy; import io.micronaut.inject.annotation.MutableAnnotationMetadata; @@ -42,7 +45,6 @@ import io.micronaut.inject.configuration.PropertyMetadata; import io.micronaut.inject.visitor.VisitorContext; import io.micronaut.inject.writer.BeanDefinitionVisitor; -import jakarta.inject.Provider; import java.time.Duration; import java.util.Arrays; @@ -80,11 +82,13 @@ protected void applyConfigurationInjectionIfNecessary(BeanDefinitionVisitor visi .getBeanProperties(); final ParameterElement[] parameters = constructor.getParameters(); if (beanProperties.size() == parameters.length) { + Javadoc javadoc = classElement.getDocumentation().map(StaticJavaParser::parseJavadoc).orElse(null); for (int i = 0; i < parameters.length; i++) { ParameterElement parameter = parameters[i]; final PropertyElement bp = beanProperties.get(i); if (CONSTRUCTOR_PARAMETERS_INJECTION_ANN.stream().noneMatch(bp::hasStereotype)) { - processConfigurationConstructorParameter(parameter); + String paramDoc = findParameterDoc(javadoc, parameter); + processConfigurationConstructorParameter(parameter, paramDoc); } } if (constructor.hasStereotype(ANN_REQUIRES_VALIDATION)) { @@ -96,11 +100,27 @@ protected void applyConfigurationInjectionIfNecessary(BeanDefinitionVisitor visi processConfigurationInjectionPoint(visitor, constructor); } + @Nullable + private static String findParameterDoc(Javadoc javadoc, ParameterElement parameter) { + String paramDoc = null; + if (javadoc != null) { + JavadocBlockTag bt = javadoc.getBlockTags() + .stream().filter(t -> t.getType() == JavadocBlockTag.Type.PARAM && t.getName().map(n -> n.equals(parameter.getName())).orElse(false)) + .findFirst().orElse(null); + if (bt != null) { + paramDoc = bt.getContent().toText(); + } + } + return paramDoc; + } + private void processConfigurationInjectionPoint(BeanDefinitionVisitor visitor, MethodElement constructor) { + Javadoc javadoc = constructor.getDocumentation().map(StaticJavaParser::parseJavadoc).orElse(null); for (ParameterElement parameter : constructor.getParameters()) { if (CONSTRUCTOR_PARAMETERS_INJECTION_ANN.stream().noneMatch(parameter::hasStereotype)) { - processConfigurationConstructorParameter(parameter); + String paramDoc = findParameterDoc(javadoc, parameter); + processConfigurationConstructorParameter(parameter, paramDoc); } } if (constructor.hasStereotype(ANN_REQUIRES_VALIDATION)) { @@ -108,30 +128,19 @@ private void processConfigurationInjectionPoint(BeanDefinitionVisitor visitor, } } - private void processConfigurationConstructorParameter(ParameterElement parameter) { - if (isPropertyParameter(parameter)) { + private void processConfigurationConstructorParameter(ParameterElement parameter, @Nullable String paramDoc) { + if (ConfigurationReaderVisitor.isPropertyParameter(parameter, visitorContext)) { final PropertyMetadata pm = metadataBuilder.visitProperty( parameter.getMethodElement().getOwningType(), parameter.getMethodElement().getDeclaringType(), parameter.getGenericType(), - parameter.getName(), parameter.getDocumentation().orElse(null), + parameter.getName(), paramDoc, parameter.stringValue(Bindable.class, "defaultValue").orElse(null) ); parameter.annotate(Property.class, (builder) -> builder.member("name", pm.getPath())); } } - private boolean isPropertyParameter(ParameterElement parameter) { - ClassElement parameterType = parameter.getGenericType(); - if (parameterType.isOptional() || parameterType.isAssignable(BeanProvider.class) || parameterType.isAssignable(Provider.class)) { - ClassElement finalParameterType = parameterType; - parameterType = parameterType.getOptionalValueType().or(finalParameterType::getFirstTypeArgument).orElse(parameterType); - // Get the class with type annotations - parameterType = visitorContext.getClassElement(parameterType.getCanonicalName()).orElse(parameterType); - } - return !parameterType.hasStereotype(AnnotationUtil.SCOPE) && !parameterType.hasStereotype(Bean.class); - } - public static boolean isConfigurationProperties(ClassElement classElement) { return classElement.hasStereotype(ConfigurationReader.class); } @@ -236,7 +245,7 @@ private AnnotationMetadata calculatePath(PropertyElement propertyElement, Member writeMember.getDeclaringType(), propertyElement.getGenericType(), propertyElement.getName(), - propertyElement.getDocumentation().orElse(null), + ConfigurationMetadataBuilder.resolveJavadocDescription(propertyElement), null ).getPath(); return visitorContext.getAnnotationMetadataBuilder().annotate(annotationMetadata, AnnotationValue.builder(Property.class).member("name", path).build()); diff --git a/inject-java-test/src/main/groovy/io/micronaut/annotation/processing/test/AbstractTypeElementSpec.groovy b/inject-java-test/src/main/groovy/io/micronaut/annotation/processing/test/AbstractTypeElementSpec.groovy index d12043e04a3..73febdf455f 100644 --- a/inject-java-test/src/main/groovy/io/micronaut/annotation/processing/test/AbstractTypeElementSpec.groovy +++ b/inject-java-test/src/main/groovy/io/micronaut/annotation/processing/test/AbstractTypeElementSpec.groovy @@ -31,6 +31,7 @@ import io.micronaut.context.ApplicationContextBuilder import io.micronaut.context.ApplicationContextConfiguration import io.micronaut.context.DefaultApplicationContext import io.micronaut.context.Qualifier +import io.micronaut.context.env.Environment import io.micronaut.context.event.ApplicationEventPublisherFactory import io.micronaut.core.annotation.AnnotationMetadata import io.micronaut.core.annotation.Experimental @@ -245,7 +246,8 @@ class Test { builder.classLoader(classLoader) builder.environments("test") configureContext(builder) - return new DefaultApplicationContext((ApplicationContextConfiguration) builder) { + def env = builder.build().environment + def context = new DefaultApplicationContext((ApplicationContextConfiguration) builder) { @Override protected List resolveBeanDefinitionReferences() { def references = StreamSupport.stream(files.spliterator(), false) @@ -261,7 +263,13 @@ class Test { return references + (includeAllBeans ? super.resolveBeanDefinitionReferences() : getBuiltInBeanReferences()) } - }.start() + + @Override + protected Environment createEnvironment(@NonNull ApplicationContextConfiguration configuration) { + return env + } + } + return context.start() } /** diff --git a/inject-java/src/test/groovy/io/micronaut/inject/configproperties/itfce/InterfaceNestingSpec.groovy b/inject-java/src/test/groovy/io/micronaut/inject/configproperties/itfce/InterfaceNestingSpec.groovy new file mode 100644 index 00000000000..b0fd3860a57 --- /dev/null +++ b/inject-java/src/test/groovy/io/micronaut/inject/configproperties/itfce/InterfaceNestingSpec.groovy @@ -0,0 +1,72 @@ +package io.micronaut.inject.configproperties.itfce + +import io.micronaut.annotation.processing.test.AbstractTypeElementSpec +import io.micronaut.context.ApplicationContextBuilder +import io.micronaut.inject.BeanDefinitionReference + +class InterfaceNestingSpec extends AbstractTypeElementSpec { + void "test nesting interfaces within each other"() { + given: + def context = buildContext('test.ItfceOuterConfig', ''' +package test; + +import io.micronaut.context.annotation.ConfigurationProperties; +import io.micronaut.context.annotation.EachProperty; +import io.micronaut.context.annotation.Parameter; + +import java.util.List; + +@ConfigurationProperties("test") +interface ItfceOuterConfig { + String getName(); + int getAge(); + ItfceInnerConfig getInner(); + List getInners(); + + @ConfigurationProperties("inner") + interface ItfceInnerConfig { + String getFoo(); + ThirdLevel getThirdLevel(); + @ConfigurationProperties("nested") + interface ThirdLevel { + int getNum(); + } + } + + @EachProperty("inners") + interface ItfceInners { + int getCount(); + ThirdLevel getThirdLevel(); + @ConfigurationProperties("nested") + interface ThirdLevel { + int getNum(); + } + } +} +''', true) + when: + def config = getBean(context, 'test.ItfceOuterConfig') + + then: + config.getName() == 'test1' + config.getAge() == 10 + config.getInner().getFoo() == 'test2' + config.getInner().getThirdLevel().getNum() == 20 + config.getInners().size() == 1 + config.getInners()[0].getCount() == 30 + config.getInners()[0].getThirdLevel().getNum() == 40 + } + + @Override + protected void configureContext(ApplicationContextBuilder contextBuilder) { + contextBuilder.properties( + 'test.name':'test1', + 'test.age':'10', + 'test.inner.foo':'test2', + 'test.inner.nested.num':'20', + 'test.inners.one.count':'30', + 'test.inners.one.nested.num':'40' + ) + } + +} diff --git a/inject-java/src/test/groovy/io/micronaut/inject/configproperties/records/RecordNestingSpec.groovy b/inject-java/src/test/groovy/io/micronaut/inject/configproperties/records/RecordNestingSpec.groovy new file mode 100644 index 00000000000..02b506a254e --- /dev/null +++ b/inject-java/src/test/groovy/io/micronaut/inject/configproperties/records/RecordNestingSpec.groovy @@ -0,0 +1,66 @@ +package io.micronaut.inject.configproperties.records + +import io.micronaut.annotation.processing.test.AbstractTypeElementSpec +import io.micronaut.context.ApplicationContextBuilder + +class RecordNestingSpec extends AbstractTypeElementSpec { + + void "test nesting records within each other"() { + given: + def context = buildContext(''' +package test; + +import io.micronaut.context.annotation.ConfigurationProperties; +import io.micronaut.context.annotation.EachProperty; +import io.micronaut.context.annotation.Parameter; + +import java.util.List; + +@ConfigurationProperties("test") +record RecordOuterConfig( + String name, + int age, + RecordInnerConfig inner, + List inners +) { + + @ConfigurationProperties("inner") + record RecordInnerConfig(String foo, ThirdLevel thirdLevel) { + + @ConfigurationProperties("nested") + record ThirdLevel(int num) {} + } + + @EachProperty("inners") + record RecordInners(@Parameter String name, int count, ThirdLevel thirdLevel) { + + @ConfigurationProperties("nested") + record ThirdLevel(int num) {} + } +} +''') + when: + def config = getBean(context, 'test.RecordOuterConfig') + + then: + config.name() == 'test1' + config.age() == 10 + config.inner().foo() == 'test2' + config.inner().thirdLevel().num() == 20 + config.inners().size() == 1 + config.inners()[0].count() == 30 + config.inners()[0].thirdLevel().num() == 40 + } + + @Override + protected void configureContext(ApplicationContextBuilder contextBuilder) { + contextBuilder.properties( + 'test.name':'test1', + 'test.age':'10', + 'test.inner.foo':'test2', + 'test.inner.nested.num':'20', + 'test.inners.one.count':'30', + 'test.inners.one.nested.num':'40' + ) + } +} diff --git a/inject-java/src/test/groovy/io/micronaut/inject/configuration/ConfigurationMetadataSpec.groovy b/inject-java/src/test/groovy/io/micronaut/inject/configuration/ConfigurationMetadataSpec.groovy index 3d3ca800bb6..1d5827a7367 100644 --- a/inject-java/src/test/groovy/io/micronaut/inject/configuration/ConfigurationMetadataSpec.groovy +++ b/inject-java/src/test/groovy/io/micronaut/inject/configuration/ConfigurationMetadataSpec.groovy @@ -55,6 +55,116 @@ class ConfigurationMetadataSpec extends AbstractTypeElementSpec { ConfigurationMetadataBuilder.reset() } + void "test configuration metadata and records"() { + when: + String metadataJson = buildConfigurationMetadata(''' +package test; + +import io.micronaut.context.annotation.*; + +/** +* My Configuration description. +* + * @param name The name of the config + * @param age The age of the config +*/ +@ConfigurationProperties("test") +record MyProperties(String name, int age, NestedConfig nested) { + @ConfigurationProperties("nested") + record NestedConfig(int num) {} +} + +''') + + then: + jsonEquals(metadataJson, ''' +{"groups":[{"name":"test","type":"test.MyProperties","description":"My Configuration description."},{"name":"test.nested","type":"test.MyProperties$NestedConfig"}],"properties":[{"name":"test.name","type":"java.lang.String","sourceType":"test.MyProperties","description":"The name of the config"},{"name":"test.age","type":"int","sourceType":"test.MyProperties","description":"The age of the config"},{"name":"test.nested.num","type":"int","sourceType":"test.MyProperties$NestedConfig"}]} +''') + } + + void "test configuration metadata and interfaces"() { + when: + String metadataJson = buildConfigurationMetadata(''' +package test; + +import io.micronaut.context.annotation.*; + +/** +* My Configuration description. +* +*/ +@ConfigurationProperties("test") +interface MyProperties { + /** + * @return The name + */ + String getName(); + + /** + * The age + */ + int getAge(); +} + +''') + + then: + jsonEquals(metadataJson, ''' +{"groups":[{"name":"test","type":"test.MyProperties","description":"My Configuration description."}],"properties":[{"name":"test.name","type":"java.lang.String","sourceType":"test.MyProperties","description":"The name"},{"name":"test.age","type":"int","sourceType":"test.MyProperties","description":"The age"}]} +''') + } + + void "test configuration metadata and javabeans"() { + when: + String metadataJson = buildConfigurationMetadata(''' +package test; + +import io.micronaut.context.annotation.*; + +/** +* My Configuration description. +* +*/ +@ConfigurationProperties("test") +class MyProperties { + + private String name; + + private int age; + + public String getName() { + return name; + } + + /** + * Sets the name. + * @param name The name + */ + public void setName(String name) { + this.name = name; + } + + public int getAge() { + return age; + } + + /** + * + * @param age The age + */ + public void setAge(int age) { + this.age = age; + } +} + +''') + + then: + jsonEquals(metadataJson, ''' +{"groups":[{"name":"test","type":"test.MyProperties","description":"My Configuration description."}],"properties":[{"name":"test.name","type":"java.lang.String","sourceType":"test.MyProperties","description":"Sets the name."},{"name":"test.age","type":"int","sourceType":"test.MyProperties","description":"The age"}]} +''') + } + void "test configuration builder on method"() { when: String metadataJson = buildConfigurationMetadata(''' From 36e3d645ab70c126937da9229a89685001ac0026 Mon Sep 17 00:00:00 2001 From: Graeme Rocher Date: Thu, 10 Nov 2022 10:09:39 +0100 Subject: [PATCH 208/743] Improve the error message for missing beans when EachBean is used (#8316) --- .../inject/foreach/EachPropertySpec.groovy | 52 ++++++++++ .../io/micronaut/inject/foreach/nested/A.java | 7 ++ .../io/micronaut/inject/foreach/nested/B.java | 7 ++ .../io/micronaut/inject/foreach/nested/C.java | 7 ++ .../context/DefaultApplicationContext.java | 97 +++++++++++++++++++ .../micronaut/context/DefaultBeanContext.java | 63 ++++++++++-- .../exceptions/NoSuchBeanException.java | 22 ++++- .../docs/guide/introduction/whatsNew.adoc | 6 ++ 8 files changed, 250 insertions(+), 11 deletions(-) create mode 100644 inject-java/src/test/groovy/io/micronaut/inject/foreach/nested/A.java create mode 100644 inject-java/src/test/groovy/io/micronaut/inject/foreach/nested/B.java create mode 100644 inject-java/src/test/groovy/io/micronaut/inject/foreach/nested/C.java diff --git a/inject-java/src/test/groovy/io/micronaut/inject/foreach/EachPropertySpec.groovy b/inject-java/src/test/groovy/io/micronaut/inject/foreach/EachPropertySpec.groovy index 8882b2948dd..7db966dcebf 100644 --- a/inject-java/src/test/groovy/io/micronaut/inject/foreach/EachPropertySpec.groovy +++ b/inject-java/src/test/groovy/io/micronaut/inject/foreach/EachPropertySpec.groovy @@ -17,9 +17,13 @@ package io.micronaut.inject.foreach import io.micronaut.context.ApplicationContext import io.micronaut.context.DefaultApplicationContext +import io.micronaut.context.Qualifier import io.micronaut.context.env.MapPropertySource import io.micronaut.context.env.PropertySource +import io.micronaut.context.exceptions.NoSuchBeanException import io.micronaut.context.exceptions.NonUniqueBeanException +import io.micronaut.inject.foreach.nested.A +import io.micronaut.inject.foreach.nested.C import io.micronaut.inject.qualifiers.Qualifiers import spock.lang.Specification /** @@ -28,6 +32,54 @@ import spock.lang.Specification */ class EachPropertySpec extends Specification { + void "test error when configuration is missing for EachProperty"() { + given: + ApplicationContext context = ApplicationContext.run() + + when: + context.getBean(MyConfiguration) + + then: + def error = thrown(NoSuchBeanException) + error.message == 'No bean of type [io.micronaut.inject.foreach.MyConfiguration] exists. No configuration entries found under the prefix: [foo.bar.*]. Provide the necessary configuration to resolve this issue.' + + when: + context.getBean(MyConfiguration, Qualifiers.byName("baz")) + + then: + error = thrown(NoSuchBeanException) + error.message == 'No bean of type [io.micronaut.inject.foreach.MyConfiguration] exists for the given qualifier: @Named(\'baz\'). No configuration entries found under the prefix: [foo.bar.baz]. Provide the necessary configuration to resolve this issue.' + + cleanup: + context.close() + } + + void "test error when configuration is missing for EachBean"() { + given: + ApplicationContext context = ApplicationContext.run() + + when: + context.getBean(MyBean) + + then: + def error = thrown(NoSuchBeanException) + error.message.startsWith("No bean of type [io.micronaut.inject.foreach.MyBean] exists.") + error.message.contains("* [MyBean] requires the presence of a bean of type [io.micronaut.inject.foreach.MyConfiguration] which does not exist.") + error.message.endsWith("* [MyConfiguration] requires the presence of configuration. No configuration entries found under the prefix: [foo.bar.*]. Provide the necessary configuration to resolve this issue.") + + when: + context.getBean(C.class, Qualifiers.byName("test")) + + then: + error = thrown(NoSuchBeanException) + error.message.contains('* [C] requires the presence of a bean of type [io.micronaut.inject.foreach.nested.B] with qualifier [@Named(\'test\')] which does not exist.') + error.message.contains("* [B] requires the presence of a bean of type [io.micronaut.inject.foreach.nested.A] with qualifier [@Named('test')] which does not exist.") + error.message.endsWith('* [A] requires the presence of configuration. No configuration entries found under the prefix: [foo.test]. Provide the necessary configuration to resolve this issue.') + + cleanup: + context.close() + } + void "test configuration properties binding"() { given: ApplicationContext applicationContext = new DefaultApplicationContext("test") diff --git a/inject-java/src/test/groovy/io/micronaut/inject/foreach/nested/A.java b/inject-java/src/test/groovy/io/micronaut/inject/foreach/nested/A.java new file mode 100644 index 00000000000..aaa73ac1846 --- /dev/null +++ b/inject-java/src/test/groovy/io/micronaut/inject/foreach/nested/A.java @@ -0,0 +1,7 @@ +package io.micronaut.inject.foreach.nested; + +import io.micronaut.context.annotation.EachProperty; + +@EachProperty("foo") +public class A { +} diff --git a/inject-java/src/test/groovy/io/micronaut/inject/foreach/nested/B.java b/inject-java/src/test/groovy/io/micronaut/inject/foreach/nested/B.java new file mode 100644 index 00000000000..047aa8f97b9 --- /dev/null +++ b/inject-java/src/test/groovy/io/micronaut/inject/foreach/nested/B.java @@ -0,0 +1,7 @@ +package io.micronaut.inject.foreach.nested; + +import io.micronaut.context.annotation.EachBean; + +@EachBean(A.class) +public class B { +} diff --git a/inject-java/src/test/groovy/io/micronaut/inject/foreach/nested/C.java b/inject-java/src/test/groovy/io/micronaut/inject/foreach/nested/C.java new file mode 100644 index 00000000000..a8cf83c18cc --- /dev/null +++ b/inject-java/src/test/groovy/io/micronaut/inject/foreach/nested/C.java @@ -0,0 +1,7 @@ +package io.micronaut.inject.foreach.nested; + +import io.micronaut.context.annotation.EachBean; + +@EachBean(B.class) +public class C { +} diff --git a/inject/src/main/java/io/micronaut/context/DefaultApplicationContext.java b/inject/src/main/java/io/micronaut/context/DefaultApplicationContext.java index ed6a417a8d2..ddfe771a3de 100644 --- a/inject/src/main/java/io/micronaut/context/DefaultApplicationContext.java +++ b/inject/src/main/java/io/micronaut/context/DefaultApplicationContext.java @@ -25,6 +25,7 @@ import io.micronaut.context.env.Environment; import io.micronaut.context.env.PropertySource; import io.micronaut.context.exceptions.ConfigurationException; +import io.micronaut.context.exceptions.NoSuchBeanException; import io.micronaut.core.annotation.AnnotationUtil; import io.micronaut.core.annotation.NonNull; import io.micronaut.core.annotation.Nullable; @@ -256,6 +257,102 @@ protected Collection> findBeanCandidates(BeanResolutionCon return transformIterables(resolutionContext, candidates, filterProxied); } + @Override + protected NoSuchBeanException newNoSuchBeanException(@Nullable BeanResolutionContext resolutionContext, Argument beanType, Qualifier qualifier, String message) { + BeanDefinition definition = findAnyBeanDefinition(resolutionContext, beanType); + if (definition != null && definition.isIterable()) { + if (definition.hasDeclaredAnnotation(EachProperty.class)) { + String propertyMissingMessage = computeEachPropertyMissingBeanMessage(qualifier, definition); + return new NoSuchBeanException( + beanType, + qualifier, + propertyMissingMessage + ); + } else if (definition.hasDeclaredAnnotation(EachBean.class)) { + + List> dependencyChain = calculateDependencyChain(resolutionContext, definition); + StringBuilder messageBuilder = new StringBuilder(); + Argument requiredBeanType = beanType; + Iterator> i = dependencyChain.iterator(); + String ls = System.getProperty("line.separator"); + while (i.hasNext()) { + messageBuilder.append(ls); + BeanDefinition beanDefinition = i.next(); + Argument nextBeanType = beanDefinition.asArgument(); + messageBuilder.append("* [").append(requiredBeanType.getTypeString(true)) + .append("] requires the presence of a bean of type [") + .append(nextBeanType.getTypeString(false)) + .append("]"); + if (qualifier != null) { + messageBuilder.append(" with qualifier [").append(qualifier).append("]"); + } + messageBuilder.append(" which does not exist."); + if (beanDefinition.hasDeclaredAnnotation(EachProperty.class)) { + messageBuilder.append(ls); + String propertyMissingMessage = computeEachPropertyMissingBeanMessage(qualifier, beanDefinition); + messageBuilder.append("* ") + .append("[") + .append(nextBeanType.getTypeString(true)) + .append("] requires the presence of configuration. ") + .append(propertyMissingMessage); + break; + } + requiredBeanType = nextBeanType; + } + + return new NoSuchBeanException( + beanType, + qualifier, + messageBuilder.toString() + ); + } + } + return super.newNoSuchBeanException(resolutionContext, beanType, qualifier, message); + } + + @Nullable + private BeanDefinition findAnyBeanDefinition(BeanResolutionContext resolutionContext, Argument beanType) { + Collection> existing = super.findBeanCandidates(resolutionContext, beanType, true, definition -> !definition.isAbstract()); + BeanDefinition definition = null; + if (existing.size() == 1) { + definition = existing.iterator().next(); + } + return definition; + } + + private List> calculateDependencyChain( + BeanResolutionContext resolutionContext, + BeanDefinition definition) { + Class dependentBean = definition.classValue(EachBean.class).orElse(null); + List> chain = new ArrayList<>(); + while (dependentBean != null) { + BeanDefinition dependent = findAnyBeanDefinition(resolutionContext, Argument.of(dependentBean)); + if (dependent == null) { + break; + } + chain.add(dependent); + dependentBean = dependent.classValue(EachBean.class).orElse(null); + } + + return chain; + } + + @NonNull + private static String computeEachPropertyMissingBeanMessage(Qualifier qualifier, BeanDefinition definition) { + String prefix = definition.stringValue(EachProperty.class).orElse(""); + if (qualifier != null) { + if (qualifier instanceof Named named) { + prefix += "." + named.getName(); + } else { + prefix += "." + "*"; + } + } else { + prefix += "." + definition.stringValue(EachProperty.class, "primary").orElse("*"); + } + + return "No configuration entries found under the prefix: [" + prefix + "]. Provide the necessary configuration to resolve this issue."; + } + @Override protected Collection> transformIterables(BeanResolutionContext resolutionContext, Collection> candidates, boolean filterProxied) { if (!candidates.isEmpty()) { diff --git a/inject/src/main/java/io/micronaut/context/DefaultBeanContext.java b/inject/src/main/java/io/micronaut/context/DefaultBeanContext.java index 5062dad2c5e..f54e29da75d 100644 --- a/inject/src/main/java/io/micronaut/context/DefaultBeanContext.java +++ b/inject/src/main/java/io/micronaut/context/DefaultBeanContext.java @@ -784,7 +784,7 @@ public Optional findBeanConfiguration(String configurationNam @Override public BeanDefinition getBeanDefinition(Argument beanType, Qualifier qualifier) { return findBeanDefinition(beanType, qualifier) - .orElseThrow(() -> new NoSuchBeanException(beanType, qualifier)); + .orElseThrow(() -> newNoSuchBeanException(null, beanType, qualifier, null)); } @Override @@ -877,7 +877,12 @@ public T getBean(@NonNull Argument beanType, @Nullable Qualifier quali if (AbstractBeanContextConditional.ConditionLog.LOG.isDebugEnabled()) { AbstractBeanContextConditional.ConditionLog.LOG.debug("Bean of type [{}] disabled for reason: {}", beanType.getSimpleName(), e.getMessage()); } - throw new NoSuchBeanException(beanType, qualifier); + throw newNoSuchBeanException( + null, + beanType, + qualifier, + e.getMessage() + ); } } @@ -994,7 +999,12 @@ public T createBean(@NonNull Class beanType, @Nullable Qualifier quali return doCreateBean(resolutionContext, candidate.get(), qualifier, argumentValues); } } - throw new NoSuchBeanException(beanType); + throw newNoSuchBeanException( + null, + Argument.of(beanType), + qualifier, + null + ); } @NonNull @@ -1009,7 +1019,12 @@ public T createBean(@NonNull Class beanType, @Nullable Qualifier quali return doCreateBean(resolutionContext, definition, beanArg, qualifier, args); } } - throw new NoSuchBeanException(beanType); + throw newNoSuchBeanException( + null, + Argument.of(beanType), + qualifier, + null + ); } /** @@ -1330,7 +1345,12 @@ protected T createBean(@Nullable BeanResolutionContext resolutionContext, return doCreateBean(context, candidate, qualifier); } } - throw new NoSuchBeanException(beanType); + throw newNoSuchBeanException( + resolutionContext, + Argument.of(beanType), + qualifier, + null + ); } /** @@ -2813,11 +2833,35 @@ private BeanRegistration resolveBeanRegistration(@Nullable BeanResolution registration = null; } if ((registration == null || registration.bean == null) && throwNoSuchBean) { - throw new NoSuchBeanException(beanType, qualifier); + throw newNoSuchBeanException(resolutionContext, beanType, qualifier, null); } return registration; } + /** + * Trigger a no such bean exception. Subclasses can improve the exception with downstream diagnosis as necessary. + * + * @param The type of the bean + * @param resolutionContext The resolution context + * @param beanType The bean type + * @param qualifier The qualifier + * @param message A message to use + * @return A no such bean exception + */ + @Internal + @NonNull + protected NoSuchBeanException newNoSuchBeanException( + @Nullable BeanResolutionContext resolutionContext, + @NonNull Argument beanType, + @NonNull Qualifier qualifier, + @Nullable String message) { + if (message != null) { + return new NoSuchBeanException(beanType, qualifier, message); + } else { + return new NoSuchBeanException(beanType, qualifier); + } + } + @Nullable private BeanRegistration provideInjectionPoint(BeanResolutionContext resolutionContext, Argument beanType, @@ -2846,7 +2890,12 @@ private BeanRegistration provideInjectionPoint(BeanResolutionContext reso if (injectionPointSegment == null || !injectionPointSegment.getArgument().isNullable()) { throw new BeanContextException("Failed to obtain injection point. No valid injection path present in path: " + path); } else if (throwNoSuchBean) { - throw new NoSuchBeanException(beanType, qualifier); + throw newNoSuchBeanException( + resolutionContext, + beanType, + qualifier, + null + ); } return null; } diff --git a/inject/src/main/java/io/micronaut/context/exceptions/NoSuchBeanException.java b/inject/src/main/java/io/micronaut/context/exceptions/NoSuchBeanException.java index 5cd95cc8d64..df33c40f168 100644 --- a/inject/src/main/java/io/micronaut/context/exceptions/NoSuchBeanException.java +++ b/inject/src/main/java/io/micronaut/context/exceptions/NoSuchBeanException.java @@ -28,18 +28,21 @@ */ public class NoSuchBeanException extends BeanContextException { + private static final String MESSAGE_PREFIX = "No bean of type ["; + private static final String MESSAGE_SUFFIX = "] exists."; + /** * @param beanType The bean type */ public NoSuchBeanException(@NonNull Class beanType) { - super("No bean of type [" + beanType.getName() + "] exists." + additionalMessage()); + super(MESSAGE_PREFIX + beanType.getName() + MESSAGE_SUFFIX + additionalMessage()); } /** * @param beanType The bean type */ public NoSuchBeanException(@NonNull Argument beanType) { - super("No bean of type [" + beanType.getTypeName() + "] exists." + additionalMessage()); + super(MESSAGE_PREFIX + beanType.getTypeName() + MESSAGE_SUFFIX + additionalMessage()); } /** @@ -48,7 +51,7 @@ public NoSuchBeanException(@NonNull Argument beanType) { * @param The type */ public NoSuchBeanException(@NonNull Class beanType, @Nullable Qualifier qualifier) { - super("No bean of type [" + beanType.getName() + "] exists" + (qualifier != null ? " for the given qualifier: " + qualifier : "") + "." + additionalMessage()); + super(MESSAGE_PREFIX + beanType.getName() + "] exists" + (qualifier != null ? " for the given qualifier: " + qualifier : "") + "." + additionalMessage()); } /** @@ -57,7 +60,18 @@ public NoSuchBeanException(@NonNull Class beanType, @Nullable Qualifier The type */ public NoSuchBeanException(@NonNull Argument beanType, @Nullable Qualifier qualifier) { - super("No bean of type [" + beanType.getTypeName() + "] exists" + (qualifier != null ? " for the given qualifier: " + qualifier : "") + "." + additionalMessage()); + super(MESSAGE_PREFIX + beanType.getTypeName() + "] exists" + (qualifier != null ? " for the given qualifier: " + qualifier : "") + "." + additionalMessage()); + } + + /** + * @param beanType The bean type + * @param qualifier The qualifier + * @param message The message + * @param The type + * @since 4.0.0 + */ + public NoSuchBeanException(@NonNull Argument beanType, @Nullable Qualifier qualifier, String message) { + super(MESSAGE_PREFIX + beanType.getTypeName() + "] exists" + (qualifier != null ? " for the given qualifier: " + qualifier : "") + ". " + message); } /** diff --git a/src/main/docs/guide/introduction/whatsNew.adoc b/src/main/docs/guide/introduction/whatsNew.adoc index f16db4aca2e..11256fb2b93 100644 --- a/src/main/docs/guide/introduction/whatsNew.adoc +++ b/src/main/docs/guide/introduction/whatsNew.adoc @@ -2,6 +2,12 @@ == 4.0.0 +=== Core Changes + +==== Improved Error Messages for Missing Beans + +When a bean annotated with ann:context.annotation.EachProperty[] or ann:context.annotation.Bean[] is not found due to missing configuration an error is thrown showing the configuration prefix necessary to resolve the issue. + === Other Dependency Upgrades - Kotlin 1.7.10 From e7a8a51ce80de45c79b9a18b785c642fb6e80c85 Mon Sep 17 00:00:00 2001 From: Graeme Rocher Date: Thu, 10 Nov 2022 12:23:21 +0100 Subject: [PATCH 209/743] Implement support for injection of maps (#8305) --- .../micronaut/aop/writer/AopProxyWriter.java | 9 +- .../DeclaredBeanElementCreator.java | 8 +- .../writer/AbstractBeanDefinitionBuilder.java | 3 +- .../inject/writer/BeanDefinitionVisitor.java | 4 +- .../inject/writer/BeanDefinitionWriter.java | 91 +++++++++++--- .../inject/constructor/mapinjection/A.java | 19 +++ .../constructor/mapinjection/AImpl.java | 25 ++++ .../constructor/mapinjection/AnotherImpl.java | 25 ++++ .../inject/constructor/mapinjection/B.java | 41 +++++++ .../ConstructorMapInjectionSpec.groovy | 42 +++++++ .../inject/field/mapinjection/A.java | 19 +++ .../inject/field/mapinjection/AImpl.java | 25 ++++ .../inject/field/mapinjection/Animal.java | 4 + .../field/mapinjection/AnotherImpl.java | 25 ++++ .../inject/field/mapinjection/B.java | 44 +++++++ .../inject/field/mapinjection/Cat.java | 7 ++ .../inject/field/mapinjection/Dog.java | 7 ++ .../mapinjection/FieldMapInjectionSpec.groovy | 45 +++++++ .../inject/field/mapinjection/MapFactory.java | 19 +++ .../inject/foreach/EachPropertySpec.groovy | 7 ++ .../inject/method/mapinjection/A.java | 19 +++ .../inject/method/mapinjection/AImpl.java | 25 ++++ .../method/mapinjection/AnotherImpl.java | 32 +++++ .../inject/method/mapinjection/B.java | 52 ++++++++ .../SetterMapInjectionSpec.groovy | 42 +++++++ .../AbstractBeanResolutionContext.java | 5 + .../AbstractInitializableBeanDefinition.java | 112 ++++++++++++++++++ .../io/micronaut/context/BeanLocator.java | 55 +++++++++ .../context/BeanResolutionContext.java | 15 +++ .../DefaultApplicationContextBuilder.java | 1 - .../micronaut/context/DefaultBeanContext.java | 67 +++++++++++ .../exceptions/NonUniqueBeanException.java | 23 ++-- .../docs/guide/introduction/whatsNew.adoc | 4 + src/main/docs/guide/ioc/types.adoc | 6 +- 34 files changed, 894 insertions(+), 33 deletions(-) create mode 100644 inject-java/src/test/groovy/io/micronaut/inject/constructor/mapinjection/A.java create mode 100644 inject-java/src/test/groovy/io/micronaut/inject/constructor/mapinjection/AImpl.java create mode 100644 inject-java/src/test/groovy/io/micronaut/inject/constructor/mapinjection/AnotherImpl.java create mode 100644 inject-java/src/test/groovy/io/micronaut/inject/constructor/mapinjection/B.java create mode 100644 inject-java/src/test/groovy/io/micronaut/inject/constructor/mapinjection/ConstructorMapInjectionSpec.groovy create mode 100644 inject-java/src/test/groovy/io/micronaut/inject/field/mapinjection/A.java create mode 100644 inject-java/src/test/groovy/io/micronaut/inject/field/mapinjection/AImpl.java create mode 100644 inject-java/src/test/groovy/io/micronaut/inject/field/mapinjection/Animal.java create mode 100644 inject-java/src/test/groovy/io/micronaut/inject/field/mapinjection/AnotherImpl.java create mode 100644 inject-java/src/test/groovy/io/micronaut/inject/field/mapinjection/B.java create mode 100644 inject-java/src/test/groovy/io/micronaut/inject/field/mapinjection/Cat.java create mode 100644 inject-java/src/test/groovy/io/micronaut/inject/field/mapinjection/Dog.java create mode 100644 inject-java/src/test/groovy/io/micronaut/inject/field/mapinjection/FieldMapInjectionSpec.groovy create mode 100644 inject-java/src/test/groovy/io/micronaut/inject/field/mapinjection/MapFactory.java create mode 100644 inject-java/src/test/groovy/io/micronaut/inject/method/mapinjection/A.java create mode 100644 inject-java/src/test/groovy/io/micronaut/inject/method/mapinjection/AImpl.java create mode 100644 inject-java/src/test/groovy/io/micronaut/inject/method/mapinjection/AnotherImpl.java create mode 100644 inject-java/src/test/groovy/io/micronaut/inject/method/mapinjection/B.java create mode 100644 inject-java/src/test/groovy/io/micronaut/inject/method/mapinjection/SetterMapInjectionSpec.groovy diff --git a/core-processor/src/main/java/io/micronaut/aop/writer/AopProxyWriter.java b/core-processor/src/main/java/io/micronaut/aop/writer/AopProxyWriter.java index 05cc9a2b9a6..c176156aafb 100644 --- a/core-processor/src/main/java/io/micronaut/aop/writer/AopProxyWriter.java +++ b/core-processor/src/main/java/io/micronaut/aop/writer/AopProxyWriter.java @@ -1236,14 +1236,15 @@ public int visitExecutableMethod( @Override public void visitFieldInjectionPoint( - TypedElement declaringType, - FieldElement fieldType, - boolean requiresReflection) { + TypedElement declaringType, + FieldElement fieldType, + boolean requiresReflection, VisitorContext visitorContext) { deferredInjectionPoints.add(() -> proxyBeanDefinitionWriter.visitFieldInjectionPoint( declaringType, fieldType, - requiresReflection + requiresReflection, + visitorContext ) ); } diff --git a/core-processor/src/main/java/io/micronaut/inject/processing/DeclaredBeanElementCreator.java b/core-processor/src/main/java/io/micronaut/inject/processing/DeclaredBeanElementCreator.java index b961ae580ca..741f0b3af8d 100644 --- a/core-processor/src/main/java/io/micronaut/inject/processing/DeclaredBeanElementCreator.java +++ b/core-processor/src/main/java/io/micronaut/inject/processing/DeclaredBeanElementCreator.java @@ -381,6 +381,7 @@ protected boolean visitAopMethod(BeanDefinitionVisitor visitor, MethodElement me */ protected void applyConfigurationInjectionIfNecessary(BeanDefinitionVisitor visitor, MethodElement constructor) { + // default to do nothing } /** @@ -430,7 +431,12 @@ protected boolean visitField(BeanDefinitionVisitor visitor, FieldElement fieldEl } if (fieldAnnotationMetadata.hasStereotype(AnnotationUtil.INJECT) || fieldAnnotationMetadata.hasDeclaredStereotype(AnnotationUtil.QUALIFIER)) { - visitor.visitFieldInjectionPoint(fieldElement.getDeclaringType(), fieldElement, fieldElement.isReflectionRequired(classElement)); + visitor.visitFieldInjectionPoint( + fieldElement.getDeclaringType(), + fieldElement, + fieldElement.isReflectionRequired(classElement), + visitorContext + ); return true; } return false; diff --git a/core-processor/src/main/java/io/micronaut/inject/writer/AbstractBeanDefinitionBuilder.java b/core-processor/src/main/java/io/micronaut/inject/writer/AbstractBeanDefinitionBuilder.java index 9afaaf12a37..016a1c335c3 100644 --- a/core-processor/src/main/java/io/micronaut/inject/writer/AbstractBeanDefinitionBuilder.java +++ b/core-processor/src/main/java/io/micronaut/inject/writer/AbstractBeanDefinitionBuilder.java @@ -824,7 +824,8 @@ private void visitField(BeanDefinitionVisitor beanDefinitionWriter, beanDefinitionWriter.visitFieldInjectionPoint( injectedField.getDeclaringType(), ibf, - ibf.isReflectionRequired() + ibf.isReflectionRequired(), + visitorContext ); } } diff --git a/core-processor/src/main/java/io/micronaut/inject/writer/BeanDefinitionVisitor.java b/core-processor/src/main/java/io/micronaut/inject/writer/BeanDefinitionVisitor.java index e7894b1fdff..7fe4c90d4c4 100644 --- a/core-processor/src/main/java/io/micronaut/inject/writer/BeanDefinitionVisitor.java +++ b/core-processor/src/main/java/io/micronaut/inject/writer/BeanDefinitionVisitor.java @@ -279,10 +279,12 @@ int visitExecutableMethod(TypedElement declaringBean, * @param declaringType The declaring type. Either a Class or a string representing the name of the type * @param fieldElement The field element * @param requiresReflection Whether accessing the field requires reflection + * @param visitorContext The visitor context */ void visitFieldInjectionPoint(TypedElement declaringType, FieldElement fieldElement, - boolean requiresReflection); + boolean requiresReflection, + VisitorContext visitorContext); /** * Visits an annotation injection point. diff --git a/core-processor/src/main/java/io/micronaut/inject/writer/BeanDefinitionWriter.java b/core-processor/src/main/java/io/micronaut/inject/writer/BeanDefinitionWriter.java index e894bcac156..4da869f661c 100644 --- a/core-processor/src/main/java/io/micronaut/inject/writer/BeanDefinitionWriter.java +++ b/core-processor/src/main/java/io/micronaut/inject/writer/BeanDefinitionWriter.java @@ -127,6 +127,7 @@ import java.util.Map; import java.util.Optional; import java.util.Set; +import java.util.TreeMap; import java.util.TreeSet; import java.util.concurrent.TimeUnit; import java.util.function.Consumer; @@ -186,6 +187,8 @@ public class BeanDefinitionWriter extends AbstractClassFileWriter implements Bea private static final Method GET_STREAM_OF_TYPE_FOR_CONSTRUCTOR_ARGUMENT = getBeanLookupMethod("getStreamOfTypeForConstructorArgument", true); + private static final Method GET_MAP_OF_TYPE_FOR_CONSTRUCTOR_ARGUMENT = getBeanLookupMethod("getMapOfTypeForConstructorArgument", true); + private static final Method FIND_BEAN_FOR_CONSTRUCTOR_ARGUMENT = getBeanLookupMethod("findBeanForConstructorArgument", true); private static final Method GET_BEAN_FOR_FIELD = getBeanLookupMethod("getBeanForField", false); @@ -202,6 +205,8 @@ public class BeanDefinitionWriter extends AbstractClassFileWriter implements Bea private static final Method GET_STREAM_OF_TYPE_FOR_FIELD = getBeanLookupMethod("getStreamOfTypeForField", true); + private static final Method GET_MAP_OF_TYPE_FOR_FIELD = getBeanLookupMethod("getMapOfTypeForField", true); + private static final Method FIND_BEAN_FOR_FIELD = getBeanLookupMethod("findBeanForField", true); private static final Method GET_VALUE_FOR_PATH = ReflectionUtils.getRequiredInternalMethod(AbstractInitializableBeanDefinition.class, "getValueForPath", BeanResolutionContext.class, BeanContext.class, Argument.class, String.class); @@ -218,6 +223,8 @@ public class BeanDefinitionWriter extends AbstractClassFileWriter implements Bea private static final Method GET_STREAM_OF_TYPE_FOR_METHOD_ARGUMENT = getBeanLookupMethodForArgument("getStreamOfTypeForMethodArgument", true); + private static final Method GET_MAP_OF_TYPE_FOR_METHOD_ARGUMENT = getBeanLookupMethodForArgument("getMapOfTypeForMethodArgument", true); + private static final Method FIND_BEAN_FOR_METHOD_ARGUMENT = getBeanLookupMethodForArgument("findBeanForMethodArgument", true); private static final Method CHECK_INJECTED_BEAN_PROPERTY_VALUE = ReflectionUtils.getRequiredInternalMethod( @@ -1688,25 +1695,37 @@ public boolean requiresMethodProcessing() { @Override public void visitFieldInjectionPoint( - TypedElement declaringType, - FieldElement fieldElement, - boolean requiresReflection) { + TypedElement declaringType, + FieldElement fieldElement, + boolean requiresReflection, + VisitorContext visitorContext) { - visitFieldInjectionPointInternal(declaringType, fieldElement, fieldElement.getAnnotationMetadata(), requiresReflection); + visitFieldInjectionPointInternal( + declaringType, + fieldElement, + fieldElement.getAnnotationMetadata(), + requiresReflection, + visitorContext + ); } private void visitFieldInjectionPointInternal( - TypedElement declaringType, - FieldElement fieldElement, - AnnotationMetadata annotationMetadata, - boolean requiresReflection) { + TypedElement declaringType, + FieldElement fieldElement, + AnnotationMetadata annotationMetadata, + boolean requiresReflection, + VisitorContext visitorContext) { boolean requiresGenericType = false; Method methodToInvoke; final ClassElement genericType = fieldElement.getGenericType(); boolean isArray = genericType.isArray(); boolean isCollection = genericType.isAssignable(Collection.class); - if (isCollection || isArray) { + boolean isMap = isInjectableMap(genericType); + if (isMap) { + requiresGenericType = true; + methodToInvoke = GET_MAP_OF_TYPE_FOR_FIELD; + } else if (isCollection || isArray) { requiresGenericType = true; ClassElement typeArgument = genericType.isArray() ? genericType.fromArray() : genericType.getFirstTypeArgument().orElse(null); if (typeArgument != null && !typeArgument.isPrimitive()) { @@ -1742,6 +1761,20 @@ private void visitFieldInjectionPointInternal( ); } + private static boolean isInjectableMap(ClassElement genericType) { + boolean typeMatches = Stream.of(Map.class, HashMap.class, LinkedHashMap.class, TreeMap.class) + .anyMatch(t -> genericType.getName().equals(t.getName())); + if (typeMatches) { + + Map typeArgs = genericType.getTypeArguments(); + if (typeArgs.size() == 2) { + ClassElement k = typeArgs.get("K"); + return k != null && k.isAssignable(CharSequence.class); + } + } + return false; + } + private boolean isInnerType(ClassElement genericType) { String type; if (genericType.isAssignable(Collection.class)) { @@ -1799,7 +1832,7 @@ public void visitFieldValue(TypedElement declaringType, Label falseCondition = isOptional ? pushPropertyContainsCheck(injectMethodVisitor, fieldElement.getType(), fieldElement.getName(), annotationMetadata) : null; if (isInnerType(fieldElement.getGenericType())) { - visitFieldInjectionPointInternal(declaringType, fieldElement, annotationMetadata, requiresReflection); + visitFieldInjectionPointInternal(declaringType, fieldElement, annotationMetadata, requiresReflection, visitorContext); } else if (!isConfigurationProperties || requiresReflection) { visitFieldInjectionPointInternal( declaringType, @@ -2399,6 +2432,7 @@ private void pushMethodParameterValue(GeneratorAdapter injectMethodVisitor, int final ClassElement genericType = entry.getGenericType(); Method methodToInvoke; boolean isCollection = genericType.isAssignable(Collection.class); + boolean isMap = isInjectableMap(genericType); boolean isArray = genericType.isArray(); if (isValueType(argMetadata) && !isInnerType(entry.getGenericType())) { @@ -2425,6 +2459,9 @@ private void pushMethodParameterValue(GeneratorAdapter injectMethodVisitor, int methodToInvoke = GET_BEAN_FOR_METHOD_ARGUMENT; requiresGenericType = false; } + } else if (isMap) { + requiresGenericType = true; + methodToInvoke = GET_MAP_OF_TYPE_FOR_METHOD_ARGUMENT; } else if (genericType.isAssignable(Stream.class)) { requiresGenericType = true; methodToInvoke = GET_STREAM_OF_TYPE_FOR_METHOD_ARGUMENT; @@ -3578,8 +3615,8 @@ private void pushConstructorArgument(GeneratorAdapter buildMethodVisitor, } else if (argumentType.getGenericType().isAssignable(BeanResolutionContext.class)) { buildMethodVisitor.loadArg(0); } else { - boolean isArray = false; boolean hasGenericType = false; + boolean isArray = false; Method methodToInvoke; final ClassElement genericType = argumentType.getGenericType(); if (isValueType(annotationMetadata) && !isInnerType(genericType)) { @@ -3588,9 +3625,7 @@ private void pushConstructorArgument(GeneratorAdapter buildMethodVisitor, pushInvokeGetPropertyValueForConstructor(buildMethodVisitor, index, argumentType, property.get()); } else { Optional valueValue = argumentType.stringValue(Value.class); - if (valueValue.isPresent()) { - pushInvokeGetPropertyPlaceholderValueForConstructor(buildMethodVisitor, index, argumentType, valueValue.get()); - } + valueValue.ifPresent(s -> pushInvokeGetPropertyPlaceholderValueForConstructor(buildMethodVisitor, index, argumentType, s)); } return; } else { @@ -3608,6 +3643,9 @@ private void pushConstructorArgument(GeneratorAdapter buildMethodVisitor, methodToInvoke = GET_BEAN_FOR_CONSTRUCTOR_ARGUMENT; hasGenericType = false; } + } else if (isInjectableMap(genericType)) { + hasGenericType = true; + methodToInvoke = GET_MAP_OF_TYPE_FOR_CONSTRUCTOR_ARGUMENT; } else if (genericType.isAssignable(Stream.class)) { hasGenericType = true; methodToInvoke = GET_STREAM_OF_TYPE_FOR_CONSTRUCTOR_ARGUMENT; @@ -3689,7 +3727,11 @@ private void pushInvokeGetPropertyPlaceholderValueForConstructor(GeneratorAdapte private void resolveConstructorArgumentGenericType(GeneratorAdapter visitor, ClassElement type, int argumentIndex) { if (!resolveArgumentGenericType(visitor, type)) { resolveConstructorArgument(visitor, argumentIndex); - resolveFirstTypeArgument(visitor); + if (type.isAssignable(Map.class)) { + resolveSecondTypeArgument(visitor); + } else { + resolveFirstTypeArgument(visitor); + } resolveInnerTypeArgumentIfNeeded(visitor, type); } } @@ -3707,7 +3749,11 @@ private void resolveConstructorArgument(GeneratorAdapter visitor, int argumentIn private void resolveMethodArgumentGenericType(GeneratorAdapter visitor, ClassElement type, int methodIndex, int argumentIndex) { if (!resolveArgumentGenericType(visitor, type)) { resolveMethodArgument(visitor, methodIndex, argumentIndex); - resolveFirstTypeArgument(visitor); + if (type.isAssignable(Map.class)) { + resolveSecondTypeArgument(visitor); + } else { + resolveFirstTypeArgument(visitor); + } resolveInnerTypeArgumentIfNeeded(visitor, type); } } @@ -3726,7 +3772,11 @@ private void resolveMethodArgument(GeneratorAdapter visitor, int methodIndex, in private void resolveFieldArgumentGenericType(GeneratorAdapter visitor, ClassElement type, int fieldIndex) { if (!resolveArgumentGenericType(visitor, type)) { resolveFieldArgument(visitor, fieldIndex); - resolveFirstTypeArgument(visitor); + if (type.isAssignable(Map.class)) { + resolveSecondTypeArgument(visitor); + } else { + resolveFirstTypeArgument(visitor); + } resolveInnerTypeArgumentIfNeeded(visitor, type); } } @@ -3793,6 +3843,13 @@ private void resolveFirstTypeArgument(GeneratorAdapter visitor) { visitor.arrayLoad(Type.getType(Argument.class)); } + private void resolveSecondTypeArgument(GeneratorAdapter visitor) { + visitor.invokeInterface(Type.getType(TypeVariableResolver.class), + org.objectweb.asm.commons.Method.getMethod(ReflectionUtils.getRequiredInternalMethod(TypeVariableResolver.class, "getTypeParameters"))); + visitor.push(1); + visitor.arrayLoad(Type.getType(Argument.class)); + } + private boolean isValueType(AnnotationMetadata annotationMetadata) { if (annotationMetadata != null) { return annotationMetadata.hasDeclaredStereotype(Value.class) || annotationMetadata.hasDeclaredStereotype(Property.class); diff --git a/inject-java/src/test/groovy/io/micronaut/inject/constructor/mapinjection/A.java b/inject-java/src/test/groovy/io/micronaut/inject/constructor/mapinjection/A.java new file mode 100644 index 00000000000..ba3c2f31ef5 --- /dev/null +++ b/inject-java/src/test/groovy/io/micronaut/inject/constructor/mapinjection/A.java @@ -0,0 +1,19 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.inject.constructor.mapinjection; + +public interface A { +} diff --git a/inject-java/src/test/groovy/io/micronaut/inject/constructor/mapinjection/AImpl.java b/inject-java/src/test/groovy/io/micronaut/inject/constructor/mapinjection/AImpl.java new file mode 100644 index 00000000000..c94be26e374 --- /dev/null +++ b/inject-java/src/test/groovy/io/micronaut/inject/constructor/mapinjection/AImpl.java @@ -0,0 +1,25 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.inject.constructor.mapinjection; + +import jakarta.inject.Named; +import jakarta.inject.Singleton; + +@Singleton +@Named("one") +public class AImpl implements A { + +} diff --git a/inject-java/src/test/groovy/io/micronaut/inject/constructor/mapinjection/AnotherImpl.java b/inject-java/src/test/groovy/io/micronaut/inject/constructor/mapinjection/AnotherImpl.java new file mode 100644 index 00000000000..e270ab8c683 --- /dev/null +++ b/inject-java/src/test/groovy/io/micronaut/inject/constructor/mapinjection/AnotherImpl.java @@ -0,0 +1,25 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.inject.constructor.mapinjection; + +import jakarta.inject.Named; +import jakarta.inject.Singleton; + +@Singleton +@Named("two") +public class AnotherImpl implements A { + +} diff --git a/inject-java/src/test/groovy/io/micronaut/inject/constructor/mapinjection/B.java b/inject-java/src/test/groovy/io/micronaut/inject/constructor/mapinjection/B.java new file mode 100644 index 00000000000..1d60bb0283a --- /dev/null +++ b/inject-java/src/test/groovy/io/micronaut/inject/constructor/mapinjection/B.java @@ -0,0 +1,41 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.inject.constructor.mapinjection; + +import jakarta.inject.Inject; + +import java.util.Collection; +import java.util.LinkedHashMap; +import java.util.Map; + +public class B { + private final LinkedHashMap linked; + private Map all; + + @Inject + public B(Map all, LinkedHashMap linked) { + this.all = all; + this.linked = linked; + } + + public Map getAll() { + return all; + } + + public LinkedHashMap getLinked() { + return linked; + } +} diff --git a/inject-java/src/test/groovy/io/micronaut/inject/constructor/mapinjection/ConstructorMapInjectionSpec.groovy b/inject-java/src/test/groovy/io/micronaut/inject/constructor/mapinjection/ConstructorMapInjectionSpec.groovy new file mode 100644 index 00000000000..48400da0657 --- /dev/null +++ b/inject-java/src/test/groovy/io/micronaut/inject/constructor/mapinjection/ConstructorMapInjectionSpec.groovy @@ -0,0 +1,42 @@ +/* + * Copyright 2017-2019 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.inject.constructor.mapinjection + +import io.micronaut.context.BeanContext +import io.micronaut.context.DefaultBeanContext +import spock.lang.Specification + +class ConstructorMapInjectionSpec extends Specification { + + void "test injection with constructor"() { + given: + BeanContext context = BeanContext.run() + + when:"A bean is obtained that has a constructor with @Inject" + B b = context.getBean(B) + + then:"The implementation is injected" + b.all != null + b.all.size() == 2 + b.all.values().contains(context.getBean(AImpl)) + b.all.values().contains(context.getBean(AnotherImpl)) + b.all['one'] instanceof AImpl + b.all == b.linked + + cleanup: + context.close() + } +} diff --git a/inject-java/src/test/groovy/io/micronaut/inject/field/mapinjection/A.java b/inject-java/src/test/groovy/io/micronaut/inject/field/mapinjection/A.java new file mode 100644 index 00000000000..e48116ffb53 --- /dev/null +++ b/inject-java/src/test/groovy/io/micronaut/inject/field/mapinjection/A.java @@ -0,0 +1,19 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.inject.field.mapinjection; + +public interface A { +} diff --git a/inject-java/src/test/groovy/io/micronaut/inject/field/mapinjection/AImpl.java b/inject-java/src/test/groovy/io/micronaut/inject/field/mapinjection/AImpl.java new file mode 100644 index 00000000000..d906ff08a8a --- /dev/null +++ b/inject-java/src/test/groovy/io/micronaut/inject/field/mapinjection/AImpl.java @@ -0,0 +1,25 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.inject.field.mapinjection; + +import jakarta.inject.Named; +import jakarta.inject.Singleton; + +@Singleton +@Named("one") +public class AImpl implements A { + +} diff --git a/inject-java/src/test/groovy/io/micronaut/inject/field/mapinjection/Animal.java b/inject-java/src/test/groovy/io/micronaut/inject/field/mapinjection/Animal.java new file mode 100644 index 00000000000..1e08dbc2c8e --- /dev/null +++ b/inject-java/src/test/groovy/io/micronaut/inject/field/mapinjection/Animal.java @@ -0,0 +1,4 @@ +package io.micronaut.inject.field.mapinjection; + +public interface Animal { +} diff --git a/inject-java/src/test/groovy/io/micronaut/inject/field/mapinjection/AnotherImpl.java b/inject-java/src/test/groovy/io/micronaut/inject/field/mapinjection/AnotherImpl.java new file mode 100644 index 00000000000..d6a343a6d58 --- /dev/null +++ b/inject-java/src/test/groovy/io/micronaut/inject/field/mapinjection/AnotherImpl.java @@ -0,0 +1,25 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.inject.field.mapinjection; + +import jakarta.inject.Named; +import jakarta.inject.Singleton; + +@Singleton +@Named("two") +public class AnotherImpl implements A { + +} diff --git a/inject-java/src/test/groovy/io/micronaut/inject/field/mapinjection/B.java b/inject-java/src/test/groovy/io/micronaut/inject/field/mapinjection/B.java new file mode 100644 index 00000000000..56c706e4c06 --- /dev/null +++ b/inject-java/src/test/groovy/io/micronaut/inject/field/mapinjection/B.java @@ -0,0 +1,44 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.inject.field.mapinjection; + +import jakarta.inject.Inject; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public class B { + @Inject + Map all; + + @Inject + Map animalMap; + + @Inject + LinkedHashMap linked; + + @Inject + Map bean; + + public Map getAll() { + return all; + } + + public LinkedHashMap getLinked() { + return linked; + } +} diff --git a/inject-java/src/test/groovy/io/micronaut/inject/field/mapinjection/Cat.java b/inject-java/src/test/groovy/io/micronaut/inject/field/mapinjection/Cat.java new file mode 100644 index 00000000000..04b0c0f1eae --- /dev/null +++ b/inject-java/src/test/groovy/io/micronaut/inject/field/mapinjection/Cat.java @@ -0,0 +1,7 @@ +package io.micronaut.inject.field.mapinjection; + +import jakarta.inject.Singleton; + +@Singleton +public class Cat implements Animal { +} diff --git a/inject-java/src/test/groovy/io/micronaut/inject/field/mapinjection/Dog.java b/inject-java/src/test/groovy/io/micronaut/inject/field/mapinjection/Dog.java new file mode 100644 index 00000000000..3b050959144 --- /dev/null +++ b/inject-java/src/test/groovy/io/micronaut/inject/field/mapinjection/Dog.java @@ -0,0 +1,7 @@ +package io.micronaut.inject.field.mapinjection; + +import jakarta.inject.Singleton; + +@Singleton +public class Dog implements Animal { +} diff --git a/inject-java/src/test/groovy/io/micronaut/inject/field/mapinjection/FieldMapInjectionSpec.groovy b/inject-java/src/test/groovy/io/micronaut/inject/field/mapinjection/FieldMapInjectionSpec.groovy new file mode 100644 index 00000000000..174058d9f15 --- /dev/null +++ b/inject-java/src/test/groovy/io/micronaut/inject/field/mapinjection/FieldMapInjectionSpec.groovy @@ -0,0 +1,45 @@ +/* + * Copyright 2017-2019 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.inject.field.mapinjection + +import io.micronaut.context.BeanContext +import io.micronaut.context.DefaultBeanContext +import spock.lang.Specification + +class FieldMapInjectionSpec extends Specification { + void "test injection via setter that takes a collection"() { + given: + BeanContext context = BeanContext.run() + + when: + B b = context.getBean(B) + + then: + b.all != null + b.all.size() == 2 + b.all.values().contains(context.getBean(AImpl)) + b.all.values().contains(context.getBean(AnotherImpl)) + b.all['one'] instanceof AImpl + b.all == b.linked + b.bean.size() == 1 // prioritize explicit beans + b.animalMap['dog'] instanceof Dog + b.animalMap['cat'] instanceof Cat + + cleanup: + context.close() + } +} + diff --git a/inject-java/src/test/groovy/io/micronaut/inject/field/mapinjection/MapFactory.java b/inject-java/src/test/groovy/io/micronaut/inject/field/mapinjection/MapFactory.java new file mode 100644 index 00000000000..a0b3b7c629e --- /dev/null +++ b/inject-java/src/test/groovy/io/micronaut/inject/field/mapinjection/MapFactory.java @@ -0,0 +1,19 @@ +package io.micronaut.inject.field.mapinjection; + +import io.micronaut.context.annotation.Factory; +import jakarta.inject.Singleton; + +import java.util.Collections; +import java.util.Map; + +@Factory +public class MapFactory { + + @Singleton + Map foo() { + return Collections.singletonMap("one", new Foo()); + } + + public static class Foo { + } +} diff --git a/inject-java/src/test/groovy/io/micronaut/inject/foreach/EachPropertySpec.groovy b/inject-java/src/test/groovy/io/micronaut/inject/foreach/EachPropertySpec.groovy index 7db966dcebf..5d2e2c71eef 100644 --- a/inject-java/src/test/groovy/io/micronaut/inject/foreach/EachPropertySpec.groovy +++ b/inject-java/src/test/groovy/io/micronaut/inject/foreach/EachPropertySpec.groovy @@ -336,6 +336,13 @@ class EachPropertySpec extends Specification { bean2.configuration.inner.enabled == 'false' + when: + def map = applicationContext.mapOfType(MyBeanWithPrimary) + + then: + map['one'].is(bean) + map['two'].is(bean2) + cleanup: applicationContext.close() } diff --git a/inject-java/src/test/groovy/io/micronaut/inject/method/mapinjection/A.java b/inject-java/src/test/groovy/io/micronaut/inject/method/mapinjection/A.java new file mode 100644 index 00000000000..63a5600cc8a --- /dev/null +++ b/inject-java/src/test/groovy/io/micronaut/inject/method/mapinjection/A.java @@ -0,0 +1,19 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.inject.method.mapinjection; + +public interface A { +} diff --git a/inject-java/src/test/groovy/io/micronaut/inject/method/mapinjection/AImpl.java b/inject-java/src/test/groovy/io/micronaut/inject/method/mapinjection/AImpl.java new file mode 100644 index 00000000000..07d3a4c8444 --- /dev/null +++ b/inject-java/src/test/groovy/io/micronaut/inject/method/mapinjection/AImpl.java @@ -0,0 +1,25 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.inject.method.mapinjection; + +import jakarta.inject.Named; +import jakarta.inject.Singleton; + +@Singleton +@Named("one") +public class AImpl implements A { + +} diff --git a/inject-java/src/test/groovy/io/micronaut/inject/method/mapinjection/AnotherImpl.java b/inject-java/src/test/groovy/io/micronaut/inject/method/mapinjection/AnotherImpl.java new file mode 100644 index 00000000000..62a2410ccfd --- /dev/null +++ b/inject-java/src/test/groovy/io/micronaut/inject/method/mapinjection/AnotherImpl.java @@ -0,0 +1,32 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.inject.method.mapinjection; + +import jakarta.inject.Qualifier; +import jakarta.inject.Singleton; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +@Singleton +@Two +public class AnotherImpl implements A { + +} + +@Retention(RetentionPolicy.RUNTIME) +@Qualifier +@interface Two {} diff --git a/inject-java/src/test/groovy/io/micronaut/inject/method/mapinjection/B.java b/inject-java/src/test/groovy/io/micronaut/inject/method/mapinjection/B.java new file mode 100644 index 00000000000..5dd34278b69 --- /dev/null +++ b/inject-java/src/test/groovy/io/micronaut/inject/method/mapinjection/B.java @@ -0,0 +1,52 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.inject.method.mapinjection; + +import io.micronaut.context.BeanContext; +import jakarta.inject.Inject; +import jakarta.inject.Singleton; + +import java.util.LinkedHashMap; +import java.util.Map; + +@Singleton +public class B { + private Map all; + private LinkedHashMap linked; + private BeanContext beanContext; + + @Inject + void setA(Map a) { + this.all = a; + } + + @Inject + private void setPrivate(LinkedHashMap a, BeanContext beanContext) { + this.linked = a; + } + + Map getAll() { + return this.all; + } + + public LinkedHashMap getLinked() { + return linked; + } + + public BeanContext getBeanContext() { + return beanContext; + } +} diff --git a/inject-java/src/test/groovy/io/micronaut/inject/method/mapinjection/SetterMapInjectionSpec.groovy b/inject-java/src/test/groovy/io/micronaut/inject/method/mapinjection/SetterMapInjectionSpec.groovy new file mode 100644 index 00000000000..578d143c921 --- /dev/null +++ b/inject-java/src/test/groovy/io/micronaut/inject/method/mapinjection/SetterMapInjectionSpec.groovy @@ -0,0 +1,42 @@ +/* + * Copyright 2017-2019 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.inject.method.mapinjection + +import io.micronaut.context.BeanContext +import io.micronaut.context.DefaultBeanContext +import spock.lang.Specification + +class SetterMapInjectionSpec extends Specification { + void "test injection via setter that takes an array"() { + given: + BeanContext context = BeanContext.run() + + when: + B b = context.getBean(B) + + then: + b.all != null + b.all.size() == 2 + b.all.values().contains(context.getBean(AImpl)) + b.all.values().contains(context.getBean(AnotherImpl)) + b.all['one'] instanceof AImpl + b.all == b.linked + + cleanup: + context.close() + + } +} diff --git a/inject/src/main/java/io/micronaut/context/AbstractBeanResolutionContext.java b/inject/src/main/java/io/micronaut/context/AbstractBeanResolutionContext.java index 64cb13a0891..cee2d853295 100644 --- a/inject/src/main/java/io/micronaut/context/AbstractBeanResolutionContext.java +++ b/inject/src/main/java/io/micronaut/context/AbstractBeanResolutionContext.java @@ -78,6 +78,11 @@ public Stream streamOfType(@NonNull Argument beanType, @Nullable Quali return context.streamOfType(this, beanType, qualifier); } + @Override + public Map mapOfType(Argument beanType, Qualifier qualifier) { + return context.mapOfType(this, beanType, qualifier); + } + @NonNull @Override public Optional findBean(@NonNull Argument beanType, @Nullable Qualifier qualifier) { diff --git a/inject/src/main/java/io/micronaut/context/AbstractInitializableBeanDefinition.java b/inject/src/main/java/io/micronaut/context/AbstractInitializableBeanDefinition.java index 0312de44c13..899abcc6bbb 100644 --- a/inject/src/main/java/io/micronaut/context/AbstractInitializableBeanDefinition.java +++ b/inject/src/main/java/io/micronaut/context/AbstractInitializableBeanDefinition.java @@ -1276,6 +1276,38 @@ protected final Stream getStreamOfTypeForMethodArgument(BeanResolutionContext } } + /** + * Obtains all bean definitions for the method at the given index and the argument at the given index + *

+ * Warning: this method is used by internal generated code and should not be called by user code. + * + * @param resolutionContext The resolution context + * @param context The context + * @param methodIndex The method index + * @param argIndex The argument index + * @param genericType The generic type + * @param qualifier The qualifier + * @return The resolved bean + * @param The bean type + */ + @Internal + @UsedByGeneratedCode + protected final Map getMapOfTypeForMethodArgument( + BeanResolutionContext resolutionContext, + BeanContext context, + int methodIndex, + int argIndex, + Argument genericType, + Qualifier qualifier) { + @SuppressWarnings("ConstantConditions") + MethodReference methodRef = methodInjection[methodIndex]; + Argument> argument = resolveArgument(context, argIndex, methodRef.arguments); + try (BeanResolutionContext.Path ignored = + resolutionContext.getPath().pushMethodArgumentResolve(this, methodRef.methodName, argument, methodRef.arguments, methodRef.requiresReflection)) { + return resolveMapOfType(resolutionContext, argument, resolveEnvironmentArgument(context, genericType), qualifier); + } + } + /** * Obtains a bean definition for a constructor at the given index *

@@ -1565,6 +1597,37 @@ protected final Stream getStreamOfTypeForConstructorArgument(BeanResoluti } } + /** + * Obtains all bean definitions for a constructor argument at the given index + *

+ * Warning: this method is used by internal generated code and should not be called by user code. + * + * @param resolutionContext The resolution context + * @param context The context + * @param argIndex The argument index + * @param genericType The generic type + * @param qualifier The qualifier + * @return The resolved bean + * @param The bean type + */ + @Internal + @UsedByGeneratedCode + protected final Map getMapOfTypeForConstructorArgument( + BeanResolutionContext resolutionContext, + BeanContext context, + int argIndex, + Argument genericType, + Qualifier qualifier) { + MethodReference constructorMethodRef = (MethodReference) constructor; + if (constructorMethodRef == null) { + throw new IllegalStateException("No constructor found for bean: " + getBeanType()); + } + Argument> argument = resolveArgument(context, argIndex, constructorMethodRef.arguments); + try (BeanResolutionContext.Path ignored = resolutionContext.getPath().pushConstructorResolve(this, argument)) { + return resolveMapOfType(resolutionContext, argument, resolveEnvironmentArgument(context, genericType), qualifier); + } + } + /** * Obtains all bean definitions for a constructor argument at the given index *

@@ -1874,6 +1937,36 @@ protected final Stream getStreamOfTypeForField(BeanResolutionContext reso } } + /** + * Obtains a bean definition for the field at the given index and the argument at the given index + *

+ * Warning: this method is used by internal generated code and should not be called by user code. + * + * @param resolutionContext The resolution context + * @param context The context + * @param fieldIndex The field index + * @param genericType The generic type + * @param qualifier The qualifier + * @param The bean type + * @return The resolved bean + */ + @Internal + @UsedByGeneratedCode + protected final Map getMapOfTypeForField( + BeanResolutionContext resolutionContext, + BeanContext context, int fieldIndex, + Argument genericType, + Qualifier qualifier) { + @SuppressWarnings("ConstantConditions") + FieldReference fieldRef = fieldInjection[fieldIndex]; + @SuppressWarnings("unchecked") + Argument> argument = resolveEnvironmentArgument(context, fieldRef.argument); + try (BeanResolutionContext.Path ignored = resolutionContext.getPath() + .pushFieldResolve(this, argument, fieldRef.requiresReflection)) { + return resolveMapOfType(resolutionContext, argument, resolveEnvironmentArgument(context, genericType), qualifier); + } + } + @Internal @UsedByGeneratedCode protected final boolean containsPropertiesValue(BeanResolutionContext resolutionContext, BeanContext context, String value) { @@ -2176,6 +2269,25 @@ private Stream resolveStreamOfType(BeanResolutionContext resolutionContex return resolutionContext.streamOfType(resultGenericType, qualifier); } + private Map resolveMapOfType( + BeanResolutionContext resolutionContext, + Argument> argument, + Argument resultGenericType, + Qualifier qualifier) { + if (resultGenericType == null) { + throw new DependencyInjectionException(resolutionContext, "Type " + argument.getType() + " has no generic argument"); + } + qualifier = qualifier == null ? resolveQualifier(resolutionContext, resultGenericType) : qualifier; + Map map = resolutionContext.mapOfType(resultGenericType, qualifier); + if (argument.isInstance(map)) { + return map; + } else { + return resolutionContext.getContext().getConversionService().convertRequired( + map, argument + ); + } + } + private Optional resolveOptionalBean(BeanResolutionContext resolutionContext, Argument argument, Argument resultGenericType, Qualifier qualifier) { if (resultGenericType == null) { throw new DependencyInjectionException(resolutionContext, "Type " + argument.getType() + " has no generic argument"); diff --git a/inject/src/main/java/io/micronaut/context/BeanLocator.java b/inject/src/main/java/io/micronaut/context/BeanLocator.java index 79a4d065c43..19727934a9d 100644 --- a/inject/src/main/java/io/micronaut/context/BeanLocator.java +++ b/inject/src/main/java/io/micronaut/context/BeanLocator.java @@ -23,6 +23,8 @@ import io.micronaut.inject.BeanDefinition; import java.util.Collection; +import java.util.Collections; +import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.stream.Stream; @@ -229,6 +231,59 @@ public interface BeanLocator { ); } + /** + * Obtain a map of beans of the given type where the key is the qualifier. + * + * @param beanType The potentially parameterized bean type + * @param qualifier The qualifier + * @param The bean concrete type + * @return A map of instances + * @see io.micronaut.inject.qualifiers.Qualifiers + * @since 4.0.0 + */ + default @NonNull Map mapOfType(@NonNull Argument beanType, @Nullable Qualifier qualifier) { + return Collections.emptyMap(); + } + + /** + * Obtain a map of beans of the given type where the key is the qualifier. + * + * @param beanType The potentially parameterized bean type + * @param qualifier The qualifier + * @param The bean concrete type + * @return A map of instances + * @see io.micronaut.inject.qualifiers.Qualifiers + * @since 4.0.0 + */ + default @NonNull Map mapOfType(@NonNull Class beanType, @Nullable Qualifier qualifier) { + return mapOfType(Argument.of(beanType), qualifier); + } + + /** + * Obtain a map of beans of the given type where the key is the qualifier. + * + * @param beanType The potentially parameterized bean type + * @param The bean concrete type + * @return A map of instances + * @see io.micronaut.inject.qualifiers.Qualifiers + * @since 4.0.0 + */ + default @NonNull Map mapOfType(@NonNull Class beanType) { + return mapOfType(Argument.of(beanType), null); + } + + /** + * Obtain a map of beans of the given type where the key is the qualifier. + * + * @param beanType The potentially parameterized bean type + * @param The bean concrete type + * @return A map of instances + * @see io.micronaut.inject.qualifiers.Qualifiers + * @since 4.0.0 + */ + default @NonNull Map mapOfType(@NonNull Argument beanType) { + return mapOfType(beanType, null); + } /** * Resolves the proxy target for a given bean type. If the bean has no proxy then the original bean is returned. diff --git a/inject/src/main/java/io/micronaut/context/BeanResolutionContext.java b/inject/src/main/java/io/micronaut/context/BeanResolutionContext.java index 1ae12a8b550..7b53af46cdf 100644 --- a/inject/src/main/java/io/micronaut/context/BeanResolutionContext.java +++ b/inject/src/main/java/io/micronaut/context/BeanResolutionContext.java @@ -31,6 +31,7 @@ import java.util.Collections; import java.util.Deque; import java.util.List; +import java.util.Map; import java.util.Optional; import java.util.stream.Stream; @@ -84,6 +85,20 @@ default void close() { @NonNull Stream streamOfType(@NonNull Argument beanType, @Nullable Qualifier qualifier); + /** + * Obtains a map of beans of the given type and qualifier. + * + * @param beanType The bean type + * @param qualifier The qualifier + * @param The bean type + * @return A map of beans, never {@code null}. + * @since 4.0.0 + */ + @NonNull + default Map mapOfType(@NonNull Argument beanType, @Nullable Qualifier qualifier) { + return Collections.emptyMap(); + } + /** * Find an optional bean of the given type and qualifier. * diff --git a/inject/src/main/java/io/micronaut/context/DefaultApplicationContextBuilder.java b/inject/src/main/java/io/micronaut/context/DefaultApplicationContextBuilder.java index 84efadc497e..659cafac302 100644 --- a/inject/src/main/java/io/micronaut/context/DefaultApplicationContextBuilder.java +++ b/inject/src/main/java/io/micronaut/context/DefaultApplicationContextBuilder.java @@ -68,7 +68,6 @@ public class DefaultApplicationContextBuilder implements ApplicationContextBuild private boolean allowEmptyProviders = false; private Boolean bootstrapEnvironment = null; private boolean enableDefaultPropertySources = true; - ; /** * Default constructor. diff --git a/inject/src/main/java/io/micronaut/context/DefaultBeanContext.java b/inject/src/main/java/io/micronaut/context/DefaultBeanContext.java index f54e29da75d..3ec57e54657 100644 --- a/inject/src/main/java/io/micronaut/context/DefaultBeanContext.java +++ b/inject/src/main/java/io/micronaut/context/DefaultBeanContext.java @@ -71,6 +71,8 @@ import io.micronaut.core.io.ResourceLoader; import io.micronaut.core.io.scan.ClassPathResourceLoader; import io.micronaut.core.io.service.SoftServiceLoader; +import io.micronaut.core.naming.NameResolver; +import io.micronaut.core.naming.NameUtils; import io.micronaut.core.naming.Named; import io.micronaut.core.order.OrderUtil; import io.micronaut.core.order.Ordered; @@ -926,6 +928,11 @@ public Stream streamOfType(Argument beanType, Qualifier qualifier) return streamOfType(null, beanType, qualifier); } + @Override + public Map mapOfType(Argument beanType, Qualifier qualifier) { + return mapOfType(null, beanType, qualifier); + } + /** * Obtains a stream of beans of the given type and qualifier. * @@ -939,6 +946,66 @@ protected Stream streamOfType(BeanResolutionContext resolutionContext, Cl return streamOfType(resolutionContext, Argument.of(beanType), qualifier); } + /** + * Obtains a map of beans of the given type and qualifier. + * @param resolutionContext The resolution context + * @param beanType The bean type + * @param qualifier The qualifier + * @param The bean type + * @return A map of beans, never {@code null}. + * @since 4.0.0 + */ + protected @NonNull Map mapOfType(@Nullable BeanResolutionContext resolutionContext, @NonNull Argument beanType, @Nullable Qualifier qualifier) { + // try and find a bean that implements the map with the generics + Argument> mapType = Argument.mapOf(Argument.of(String.class), beanType); + @SuppressWarnings("unchecked") Qualifier> mapQualifier = (Qualifier>) qualifier; + BeanDefinition> existingBean = findBeanDefinition(mapType, mapQualifier, false).orElse(null); + if (existingBean != null) { + return getBean(existingBean); + } else { + Collection> beanRegistrations = getBeanRegistrations(resolutionContext, beanType, qualifier); + if (beanRegistrations.isEmpty()) { + return Collections.emptyMap(); + } else { + try { + return beanRegistrations.stream().collect(Collectors.toUnmodifiableMap( + DefaultBeanContext::resolveKey, + reg -> reg.bean + )); + } catch (IllegalStateException e) { // occurs for duplicate keys + List> beanDefinitions = beanRegistrations.stream().map(reg -> reg.beanDefinition).toList(); + throw new DependencyInjectionException( + resolutionContext, + "Injecting a map of beans requires each bean to define a qualifier. Multiple beans were found missing a qualifier resulting in duplicate keys: " + e.getMessage(), + new NonUniqueBeanException( + beanType.getType(), + beanDefinitions.iterator() + ) + ); + } + } + } + } + + @SuppressWarnings("unchecked") + @NonNull + private static String resolveKey(BeanRegistration reg) { + BeanDefinition definition = reg.beanDefinition; + BeanIdentifier identifier = reg.identifier; + if (definition instanceof NameResolver resolver) { + return resolver.resolveName().orElse(identifier.getName()); + } else { + String name = identifier.getName(); + if (name.equals(Primary.SIMPLE_NAME)) { + Class candidateType = reg.beanDefinition.getBeanType(); + String candidateSimpleName = candidateType.getSimpleName(); + return NameUtils.decapitalize(candidateSimpleName); + } else { + return name; + } + } + } + /** * Obtains a stream of beans of the given type and qualifier. * diff --git a/inject/src/main/java/io/micronaut/context/exceptions/NonUniqueBeanException.java b/inject/src/main/java/io/micronaut/context/exceptions/NonUniqueBeanException.java index 3890535c61d..c096d19530b 100644 --- a/inject/src/main/java/io/micronaut/context/exceptions/NonUniqueBeanException.java +++ b/inject/src/main/java/io/micronaut/context/exceptions/NonUniqueBeanException.java @@ -15,27 +15,32 @@ */ package io.micronaut.context.exceptions; +import io.micronaut.core.type.Argument; import io.micronaut.inject.BeanDefinition; +import java.util.Collections; import java.util.Iterator; + /** * Exception thrown when a bean is not unique and has multiple possible implementations for a given bean type. * * @author Graeme Rocher * @since 1.0 */ +@SuppressWarnings("java:S110") public class NonUniqueBeanException extends NoSuchBeanException { - private final Class targetType; - private final Iterator possibleCandidates; + @SuppressWarnings("java:S3740") + private final transient Iterator possibleCandidates; + private final Class targetType; /** * @param targetType The target type * @param candidates The bean definition candidates * @param The type */ - public NonUniqueBeanException(Class targetType, Iterator> candidates) { + public NonUniqueBeanException(Class targetType, Iterator> candidates) { super(buildMessage(candidates)); this.targetType = targetType; this.possibleCandidates = candidates; @@ -46,22 +51,26 @@ public NonUniqueBeanException(Class targetType, Iterator> * @return The possible bean candidates */ public Iterator> getPossibleCandidates() { - return possibleCandidates; + if (possibleCandidates != null) { + return possibleCandidates; + } + return Collections.emptyIterator(); } /** * @param The type * @return The bean type requested */ + @SuppressWarnings("unchecked") public Class getBeanType() { - return targetType; + return (Class) targetType; } private static String buildMessage(Iterator> possibleCandidates) { final StringBuilder message = new StringBuilder("Multiple possible bean candidates found: ["); while (possibleCandidates.hasNext()) { - Class next = possibleCandidates.next().getBeanType(); - message.append(next.getName()); + Argument next = possibleCandidates.next().asArgument(); + message.append(next.getTypeString(true)); if (possibleCandidates.hasNext()) { message.append(", "); } diff --git a/src/main/docs/guide/introduction/whatsNew.adoc b/src/main/docs/guide/introduction/whatsNew.adoc index 11256fb2b93..c021697d264 100644 --- a/src/main/docs/guide/introduction/whatsNew.adoc +++ b/src/main/docs/guide/introduction/whatsNew.adoc @@ -4,6 +4,10 @@ === Core Changes +==== Injection of Maps + +It is now possible to inject a `java.util.Map` of beans where the key is the bean name. The name of the bean is derived from the <> or (if not present) the simple name of the class. + ==== Improved Error Messages for Missing Beans When a bean annotated with ann:context.annotation.EachProperty[] or ann:context.annotation.Bean[] is not found due to missing configuration an error is thrown showing the configuration prefix necessary to resolve the issue. diff --git a/src/main/docs/guide/ioc/types.adoc b/src/main/docs/guide/ioc/types.adoc index 3eb7baa67b7..d8b9bb1ab74 100644 --- a/src/main/docs/guide/ioc/types.adoc +++ b/src/main/docs/guide/ioc/types.adoc @@ -8,10 +8,14 @@ In addition to being able to inject beans, Micronaut natively supports injecting |An `Optional` of a bean. `empty()` is injected if the bean doesn't exist |`Optional` -|link:{jdkapi}/java/util/Collection.html[java.lang.Collection] +|link:{jdkapi}/java/util/Collection.html[java.util.Collection] |An `Collection` or subtype of `Collection` (e.g. `List`, `Set`, etc.) |`Collection` +|link:{jdkapi}/java/util/Map.html[java.util.Map] +|An `Map` or subtype of `Map` (e.g. `LinkedHashMap`, `TreeMap`, etc.) where the key is the qualifier +|`Map` + |link:{jdkapi}/java/util/stream/Stream.html[java.util.stream.Stream] |A lazy `Stream` of beans |`Stream` From 049f429b7cfd5458a96a6126d462a3127264a9bd Mon Sep 17 00:00:00 2001 From: Graeme Rocher Date: Thu, 10 Nov 2022 15:41:38 +0100 Subject: [PATCH 210/743] fix sonar --- .../context/ApplicationContextConfiguration.java | 4 ++++ .../micronaut/context/exceptions/NoSuchBeanException.java | 8 +++++--- .../io/micronaut/inject/qualifiers/PrimaryQualifier.java | 1 + 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/inject/src/main/java/io/micronaut/context/ApplicationContextConfiguration.java b/inject/src/main/java/io/micronaut/context/ApplicationContextConfiguration.java index a3ce05f3b6f..f0c3c832a83 100644 --- a/inject/src/main/java/io/micronaut/context/ApplicationContextConfiguration.java +++ b/inject/src/main/java/io/micronaut/context/ApplicationContextConfiguration.java @@ -93,6 +93,7 @@ default boolean isEnvironmentPropertySource() { /** * @return The environment variables to include in configuration */ + @SuppressWarnings("java:S1168") // null used to establish absence of config default @Nullable List getEnvironmentVariableIncludes() { return null; } @@ -100,6 +101,7 @@ default boolean isEnvironmentPropertySource() { /** * @return The environment variables to exclude from configuration */ + @SuppressWarnings("java:S1168") // null used to establish absence of config default @Nullable List getEnvironmentVariableExcludes() { return null; } @@ -127,6 +129,7 @@ default Optional getConversionService() { * * @return The config locations */ + @SuppressWarnings("java:S1168") // null used to establish absence of config default @Nullable List getOverrideConfigLocations() { return null; } @@ -141,6 +144,7 @@ default boolean isBannerEnabled() { } @Nullable + @SuppressWarnings("java:S2447") // null used to establish absence of config default Boolean isBootstrapEnvironmentEnabled() { return null; } diff --git a/inject/src/main/java/io/micronaut/context/exceptions/NoSuchBeanException.java b/inject/src/main/java/io/micronaut/context/exceptions/NoSuchBeanException.java index df33c40f168..e5139b72610 100644 --- a/inject/src/main/java/io/micronaut/context/exceptions/NoSuchBeanException.java +++ b/inject/src/main/java/io/micronaut/context/exceptions/NoSuchBeanException.java @@ -30,6 +30,8 @@ public class NoSuchBeanException extends BeanContextException { private static final String MESSAGE_PREFIX = "No bean of type ["; private static final String MESSAGE_SUFFIX = "] exists."; + private static final String MESSAGE_EXISTS = "] exists"; + private static final String MESSAGE_FOR_THE_GIVEN_QUALIFIER = " for the given qualifier: "; /** * @param beanType The bean type @@ -51,7 +53,7 @@ public NoSuchBeanException(@NonNull Argument beanType) { * @param The type */ public NoSuchBeanException(@NonNull Class beanType, @Nullable Qualifier qualifier) { - super(MESSAGE_PREFIX + beanType.getName() + "] exists" + (qualifier != null ? " for the given qualifier: " + qualifier : "") + "." + additionalMessage()); + super(MESSAGE_PREFIX + beanType.getName() + MESSAGE_EXISTS + (qualifier != null ? MESSAGE_FOR_THE_GIVEN_QUALIFIER + qualifier : "") + "." + additionalMessage()); } /** @@ -60,7 +62,7 @@ public NoSuchBeanException(@NonNull Class beanType, @Nullable Qualifier The type */ public NoSuchBeanException(@NonNull Argument beanType, @Nullable Qualifier qualifier) { - super(MESSAGE_PREFIX + beanType.getTypeName() + "] exists" + (qualifier != null ? " for the given qualifier: " + qualifier : "") + "." + additionalMessage()); + super(MESSAGE_PREFIX + beanType.getTypeName() + MESSAGE_EXISTS + (qualifier != null ? MESSAGE_FOR_THE_GIVEN_QUALIFIER + qualifier : "") + "." + additionalMessage()); } /** @@ -71,7 +73,7 @@ public NoSuchBeanException(@NonNull Argument beanType, @Nullable Qualifie * @since 4.0.0 */ public NoSuchBeanException(@NonNull Argument beanType, @Nullable Qualifier qualifier, String message) { - super(MESSAGE_PREFIX + beanType.getTypeName() + "] exists" + (qualifier != null ? " for the given qualifier: " + qualifier : "") + ". " + message); + super(MESSAGE_PREFIX + beanType.getTypeName() + MESSAGE_EXISTS + (qualifier != null ? MESSAGE_FOR_THE_GIVEN_QUALIFIER + qualifier : "") + ". " + message); } /** diff --git a/inject/src/main/java/io/micronaut/inject/qualifiers/PrimaryQualifier.java b/inject/src/main/java/io/micronaut/inject/qualifiers/PrimaryQualifier.java index 96cd54c68dd..fae238c5b90 100644 --- a/inject/src/main/java/io/micronaut/inject/qualifiers/PrimaryQualifier.java +++ b/inject/src/main/java/io/micronaut/inject/qualifiers/PrimaryQualifier.java @@ -29,6 +29,7 @@ * @since 3.5.0 */ @Internal +@SuppressWarnings("java:S1845") public final class PrimaryQualifier implements Qualifier { @SuppressWarnings({"rawtypes", "java:S1845"}) From 5ade2f92d170157093049247a7b400efee34e144 Mon Sep 17 00:00:00 2001 From: Graeme Rocher Date: Thu, 10 Nov 2022 15:45:43 +0100 Subject: [PATCH 211/743] fix sonar --- .../intercepted/InterceptedMethodUtil.java | 2 +- ....java => KotlinInterceptedMethodImpl.java} | 35 ++++++++++--------- 2 files changed, 20 insertions(+), 17 deletions(-) rename aop/src/main/java/io/micronaut/aop/internal/intercepted/{KotlinInterceptedMethod.java => KotlinInterceptedMethodImpl.java} (79%) diff --git a/aop/src/main/java/io/micronaut/aop/internal/intercepted/InterceptedMethodUtil.java b/aop/src/main/java/io/micronaut/aop/internal/intercepted/InterceptedMethodUtil.java index 43f6b224baf..fc6a4967176 100644 --- a/aop/src/main/java/io/micronaut/aop/internal/intercepted/InterceptedMethodUtil.java +++ b/aop/src/main/java/io/micronaut/aop/internal/intercepted/InterceptedMethodUtil.java @@ -57,7 +57,7 @@ private InterceptedMethodUtil() { @NonNull public static InterceptedMethod of(@NonNull MethodInvocationContext context, @NonNull ConversionService conversionService) { if (context.isSuspend()) { - KotlinInterceptedMethod kotlinInterceptedMethod = KotlinInterceptedMethod.of(context); + KotlinInterceptedMethodImpl kotlinInterceptedMethod = KotlinInterceptedMethodImpl.of(context); if (kotlinInterceptedMethod != null) { return kotlinInterceptedMethod; } diff --git a/aop/src/main/java/io/micronaut/aop/internal/intercepted/KotlinInterceptedMethod.java b/aop/src/main/java/io/micronaut/aop/internal/intercepted/KotlinInterceptedMethodImpl.java similarity index 79% rename from aop/src/main/java/io/micronaut/aop/internal/intercepted/KotlinInterceptedMethod.java rename to aop/src/main/java/io/micronaut/aop/internal/intercepted/KotlinInterceptedMethodImpl.java index d80eede9202..db07b8eb9fb 100644 --- a/aop/src/main/java/io/micronaut/aop/internal/intercepted/KotlinInterceptedMethod.java +++ b/aop/src/main/java/io/micronaut/aop/internal/intercepted/KotlinInterceptedMethodImpl.java @@ -39,19 +39,19 @@ */ @Internal @Experimental -final class KotlinInterceptedMethod implements io.micronaut.aop.kotlin.KotlinInterceptedMethod { +final class KotlinInterceptedMethodImpl implements io.micronaut.aop.kotlin.KotlinInterceptedMethod { private final MethodInvocationContext context; - private Continuation continuation; + private Continuation continuation; private final Consumer replaceContinuation; private final Argument returnTypeValue; private final boolean isUnitValueType; - private KotlinInterceptedMethod(MethodInvocationContext context, - Continuation continuation, - Consumer replaceContinuation, - Argument returnTypeValue, - boolean isUnitValueType) { + private KotlinInterceptedMethodImpl(MethodInvocationContext context, + Continuation continuation, + Consumer replaceContinuation, + Argument returnTypeValue, + boolean isUnitValueType) { this.context = context; this.continuation = continuation; this.returnTypeValue = returnTypeValue; @@ -65,7 +65,7 @@ private KotlinInterceptedMethod(MethodInvocationContext context, * @param context {@link MethodInvocationContext} * @return true if Kotlin coroutine */ - public static KotlinInterceptedMethod of(MethodInvocationContext context) { + public static KotlinInterceptedMethodImpl of(MethodInvocationContext context) { if (!KotlinUtils.KOTLIN_COROUTINES_SUPPORTED || !context.getExecutableMethod().isSuspend()) { return null; } @@ -75,15 +75,14 @@ public static KotlinInterceptedMethod of(MethodInvocationContext context) } int lastParameterIndex = parameterValues.length - 1; Object lastArgumentValue = parameterValues[lastParameterIndex]; - if (lastArgumentValue instanceof Continuation) { - Continuation continuation = (Continuation) lastArgumentValue; + if (lastArgumentValue instanceof Continuation continuation) { Consumer replaceContinuation = value -> parameterValues[lastParameterIndex] = value; Argument returnTypeValue = context.getArguments()[lastParameterIndex].getFirstTypeVariable().orElse(Argument.OBJECT_ARGUMENT); boolean isUnitValueType = returnTypeValue.getType() == kotlin.Unit.class; if (isUnitValueType) { returnTypeValue = Argument.VOID_OBJECT; } - return new KotlinInterceptedMethod(context, continuation, replaceContinuation, returnTypeValue, isUnitValueType); + return new KotlinInterceptedMethodImpl(context, continuation, replaceContinuation, returnTypeValue, isUnitValueType); } return null; } @@ -100,7 +99,8 @@ public Argument returnTypeValue() { @Override public CompletableFuture interceptResultAsCompletionStage() { - CompletableFutureContinuation completableFutureContinuation = new CompletableFutureContinuation(continuation); + @SuppressWarnings("unchecked") + CompletableFutureContinuation completableFutureContinuation = new CompletableFutureContinuation((Continuation) continuation); replaceContinuation.accept(completableFutureContinuation); Object result = context.proceed(); replaceContinuation.accept(continuation); @@ -112,7 +112,8 @@ public CompletableFuture interceptResultAsCompletionStage() { @Override public CompletableFuture interceptResultAsCompletionStage(Interceptor from) { - CompletableFutureContinuation completableFutureContinuation = new CompletableFutureContinuation(continuation); + @SuppressWarnings("unchecked") + CompletableFutureContinuation completableFutureContinuation = new CompletableFutureContinuation((Continuation) continuation); replaceContinuation.accept(completableFutureContinuation); Object result = context.proceed(from); replaceContinuation.accept(continuation); @@ -134,13 +135,14 @@ public Object interceptResult(Interceptor from) { @Override public Object handleResult(Object result) { - CompletionStage completionStageResult; + CompletionStage completionStageResult; if (result instanceof CompletionStage) { completionStageResult = (CompletionStage) result; } else { throw new IllegalStateException("Cannot convert " + result + " to 'java.util.concurrent.CompletionStage'"); } - return KotlinInterceptedMethodHelper.handleResult(completionStageResult, isUnitValueType, continuation); + //noinspection unchecked + return KotlinInterceptedMethodHelper.handleResult(completionStageResult, isUnitValueType, (Continuation) continuation); } @Override @@ -153,8 +155,9 @@ public CoroutineContext getCoroutineContext() { return continuation.getContext(); } + @SuppressWarnings("unchecked") @Override public void updateCoroutineContext(CoroutineContext coroutineContext) { - continuation = new DelegatingContextContinuation(continuation, coroutineContext); + continuation = new DelegatingContextContinuation((Continuation) continuation, coroutineContext); } } From 7dfd851f0912ee42778d613fb5bcf795a61886bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Champeau?= Date: Thu, 10 Nov 2022 17:14:51 +0100 Subject: [PATCH 212/743] Introduce workaround for javadoc generation (#8324) Previously, the application of the `shadow` plugin caused an extra artifact to be present on classpath by accident. This artifact contained all the classes of a project, including those not written in Java. With the removal, javadoc started to fail. As a workaround, we will now add the classes of the project itself to the artifacts it exposes on classpath. This means that the `javadoc` task will see both java sources, but also their compiled version. While it will not find sources for the classes written in Kotlin or Java, this will fix javadoc generation. --- .../io.micronaut.build.internal.convention-library.gradle | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/buildSrc/src/main/groovy/io.micronaut.build.internal.convention-library.gradle b/buildSrc/src/main/groovy/io.micronaut.build.internal.convention-library.gradle index ecf6908ca37..284449ea820 100644 --- a/buildSrc/src/main/groovy/io.micronaut.build.internal.convention-library.gradle +++ b/buildSrc/src/main/groovy/io.micronaut.build.internal.convention-library.gradle @@ -20,3 +20,11 @@ micronautBuild { enabled.set(false) } } + +// Workaround for javadoc not finding java source +// files for classes written in an alternate language (Kotlin, Groovy) +configurations { + internalJavadocClasspathElements { + outgoing.artifact(tasks.named("jar")) + } +} From 5f24ff54ad1dd8c361a9de5aec61ee76b79ebadd Mon Sep 17 00:00:00 2001 From: Tim Yates Date: Fri, 11 Nov 2022 15:32:29 +0000 Subject: [PATCH 213/743] Stop conversion of Properties keys and values (#8327) Since https://github.com/micronaut-projects/micronaut-core/pull/8278 all Properties when converted have their keys and values converted to Strings. This was discovered in the micronaut-sql module where we check datasource properties. --- .../DefaultMutableConversionService.java | 4 +- .../DefaultConversionServiceSpec.groovy | 45 ++++++++++++------- 2 files changed, 31 insertions(+), 18 deletions(-) diff --git a/core/src/main/java/io/micronaut/core/convert/DefaultMutableConversionService.java b/core/src/main/java/io/micronaut/core/convert/DefaultMutableConversionService.java index e701cb36182..b04a474c1bd 100644 --- a/core/src/main/java/io/micronaut/core/convert/DefaultMutableConversionService.java +++ b/core/src/main/java/io/micronaut/core/convert/DefaultMutableConversionService.java @@ -888,8 +888,8 @@ private void registerDefaultConverters() { } return Argument.of(Object.class, "V"); }); - Class keyType = keyArgument.getType(); - Class valueType = valArgument.getType(); + Class keyType = isProperties ? Object.class : keyArgument.getType(); + Class valueType = isProperties ? Object.class : valArgument.getType(); ConversionContext keyContext = context.with(keyArgument); ConversionContext valContext = context.with(valArgument); diff --git a/core/src/test/groovy/io/micronaut/core/convert/DefaultConversionServiceSpec.groovy b/core/src/test/groovy/io/micronaut/core/convert/DefaultConversionServiceSpec.groovy index c12e2518728..5c3cbb3d17d 100644 --- a/core/src/test/groovy/io/micronaut/core/convert/DefaultConversionServiceSpec.groovy +++ b/core/src/test/groovy/io/micronaut/core/convert/DefaultConversionServiceSpec.groovy @@ -1,18 +1,3 @@ -/* - * Copyright 2017-2019 original authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ package io.micronaut.core.convert import io.micronaut.core.convert.exceptions.ConversionErrorException @@ -29,7 +14,6 @@ import java.time.DayOfWeek */ class DefaultConversionServiceSpec extends Specification { - @Unroll void "test default conversion service converts a #sourceObject.class.name to a #targetType.name"() { given: ConversionService conversionService = new DefaultMutableConversionService() @@ -70,6 +54,35 @@ class DefaultConversionServiceSpec extends Specification { "test".getBytes(StandardCharsets.UTF_8) | String | "test" } + void 'test properties conversion'() { + given: + ConversionService conversionService = new DefaultMutableConversionService() + + when: + def sourceObject = [num: 1, name: 'tim', cool: true] + def targetType = Properties + + then: + with(conversionService.convert(sourceObject, targetType).get()) { + it.size() == 3 + it.num == 1 + it.name == 'tim' + it.cool == true + } + + when: + sourceObject = [1: 1, name: 'tim', cool: true] + + then: + with(conversionService.convert(sourceObject, targetType).get()) { + it.size() == 3 + it.containsKey(1) + it.get(1) == 1 + it.name == 'tim' + it.cool == true + } + } + void "test empty string conversion"() { given: ConversionService conversionService = new DefaultMutableConversionService() From ba8f6230315e7a8e4ccc95c094a33da2e5b00762 Mon Sep 17 00:00:00 2001 From: Denis Stepanov Date: Mon, 14 Nov 2022 14:00:32 +0700 Subject: [PATCH 214/743] Fix Java generic placeholder annotations (#8331) --- .../inject/visitor/ClassElementSpec.groovy | 32 +++++ .../visitor/AbstractJavaElement.java | 136 ++++++++---------- .../JavaGenericPlaceholderElement.java | 8 +- .../visitors/ClassElementSpec.groovy | 32 +++++ 4 files changed, 132 insertions(+), 76 deletions(-) diff --git a/inject-groovy/src/test/groovy/io/micronaut/inject/visitor/ClassElementSpec.groovy b/inject-groovy/src/test/groovy/io/micronaut/inject/visitor/ClassElementSpec.groovy index f728162483b..df5159f60aa 100644 --- a/inject-groovy/src/test/groovy/io/micronaut/inject/visitor/ClassElementSpec.groovy +++ b/inject-groovy/src/test/groovy/io/micronaut/inject/visitor/ClassElementSpec.groovy @@ -18,6 +18,7 @@ package io.micronaut.inject.visitor import io.micronaut.ast.groovy.TypeElementVisitorStart import io.micronaut.ast.transform.test.AbstractBeanDefinitionSpec import io.micronaut.context.exceptions.BeanContextException +import io.micronaut.core.annotation.Introspected import io.micronaut.inject.ast.ClassElement import io.micronaut.inject.ast.ElementModifier import io.micronaut.inject.ast.ElementQuery @@ -1003,4 +1004,35 @@ class Pet { packPrvFields.size() == 2 packPrvFields.stream().map(FieldElement::getName).toList() == ["packprivme", "PACK_PRV_CONST"] } + + void "test annotations on generic type"() { + given: + ClassElement classElement = buildClassElement('test.PetOperations', ''' +package test + +import io.micronaut.core.annotation.Introspected +import io.micronaut.http.annotation.* +import jakarta.inject.Inject + +@Controller("/pets") +interface PetOperations { + + @Post("/") + T save(String name, int age) +} + +@Introspected +class Pet { +} + +''') + when: + def method = classElement.getEnclosedElements(ElementQuery.ALL_METHODS.named("save")).get(0) + def returnType = method.getReturnType() + def genericReturnType = method.getGenericReturnType() + + then: + returnType.hasAnnotation(Introspected) + genericReturnType.hasAnnotation(Introspected) + } } diff --git a/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/AbstractJavaElement.java b/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/AbstractJavaElement.java index 6a78fd0fcdc..4c5ca188311 100644 --- a/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/AbstractJavaElement.java +++ b/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/AbstractJavaElement.java @@ -313,91 +313,78 @@ public String toString() { * @param isTypeVariable is the type a type variable * @return The class element */ - protected @NonNull ClassElement mirrorToClassElement( - TypeMirror returnType, - JavaVisitorContext visitorContext, - Map> genericsInfo, - boolean includeTypeAnnotations, - boolean isTypeVariable) { + protected @NonNull ClassElement mirrorToClassElement(TypeMirror returnType, + JavaVisitorContext visitorContext, + Map> genericsInfo, + boolean includeTypeAnnotations, + boolean isTypeVariable) { if (genericsInfo == null) { genericsInfo = Collections.emptyMap(); } if (returnType instanceof NoType) { return PrimitiveElement.VOID; - } else if (returnType instanceof DeclaredType) { - DeclaredType dt = (DeclaredType) returnType; + } + if (returnType instanceof DeclaredType dt) { Element e = dt.asElement(); - //Declared types can wrap other types, like primitives - if (e.asType() instanceof DeclaredType) { + // Declared types can wrap other types, like primitives + if (!(e.asType() instanceof DeclaredType)) { + return mirrorToClassElement(e.asType(), visitorContext, genericsInfo, includeTypeAnnotations); + } + if (e instanceof TypeElement typeElement) { List typeArguments = dt.getTypeArguments(); - if (e instanceof TypeElement) { - TypeElement typeElement = (TypeElement) e; - Map boundGenerics = resolveBoundGenerics(visitorContext, genericsInfo); - if (visitorContext.getModelUtils().resolveKind(typeElement, ElementKind.ENUM).isPresent()) { - return new JavaEnumElement( - typeElement, - resolveElementAnnotationMetadataFactory(typeElement, dt, includeTypeAnnotations), - visitorContext - ); - } else { - genericsInfo = visitorContext.getGenericUtils().alignNewGenericsInfo( - typeElement, - typeArguments, - boundGenerics - ); - return new JavaClassElement( - typeElement, - resolveElementAnnotationMetadataFactory(typeElement, dt, includeTypeAnnotations), - visitorContext, - typeArguments, - genericsInfo, - isTypeVariable - ); - } + Map boundGenerics = resolveBoundGenerics(visitorContext, genericsInfo); + if (visitorContext.getModelUtils().resolveKind(typeElement, ElementKind.ENUM).isPresent()) { + return new JavaEnumElement( + typeElement, + resolveElementAnnotationMetadataFactory(typeElement, dt, includeTypeAnnotations), + visitorContext + ); } - } else { - return mirrorToClassElement(e.asType(), visitorContext, genericsInfo, includeTypeAnnotations); + genericsInfo = visitorContext.getGenericUtils().alignNewGenericsInfo( + typeElement, + typeArguments, + boundGenerics + ); + return new JavaClassElement( + typeElement, + resolveElementAnnotationMetadataFactory(typeElement, dt, includeTypeAnnotations), + visitorContext, + typeArguments, + genericsInfo, + isTypeVariable + ); } - } else if (returnType instanceof TypeVariable) { - TypeVariable tv = (TypeVariable) returnType; - return resolveTypeVariable( - visitorContext, - genericsInfo, - includeTypeAnnotations, - tv, - tv - ); - - } else if (returnType instanceof ArrayType) { - ArrayType at = (ArrayType) returnType; + return PrimitiveElement.VOID; + } + if (returnType instanceof TypeVariable tv) { + return resolveTypeVariable(visitorContext, genericsInfo, includeTypeAnnotations, tv, tv); + } + if (returnType instanceof ArrayType at) { TypeMirror componentType = at.getComponentType(); ClassElement arrayType; - if (componentType instanceof TypeVariable && componentType.getKind() == TypeKind.TYPEVAR) { - TypeVariable tv = (TypeVariable) componentType; + if (componentType instanceof TypeVariable tv && componentType.getKind() == TypeKind.TYPEVAR) { arrayType = resolveTypeVariable(visitorContext, genericsInfo, includeTypeAnnotations, tv, at); } else { arrayType = mirrorToClassElement(componentType, visitorContext, genericsInfo, includeTypeAnnotations); } return arrayType.toArray(); - } else if (returnType instanceof PrimitiveType) { - PrimitiveType pt = (PrimitiveType) returnType; + } + if (returnType instanceof PrimitiveType pt) { return PrimitiveElement.valueOf(pt.getKind().name()); - } else if (returnType instanceof WildcardType) { - WildcardType wt = (WildcardType) returnType; + } + if (returnType instanceof WildcardType wt) { Map> finalGenericsInfo = genericsInfo; TypeMirror superBound = wt.getSuperBound(); Stream lowerBounds; - if (superBound instanceof UnionType) { - lowerBounds = ((UnionType) superBound).getAlternatives().stream(); - } else if (superBound == null) { - lowerBounds = Stream.empty(); + if (superBound instanceof UnionType unionType) { + lowerBounds = unionType.getAlternatives().stream(); } else { - lowerBounds = Stream.of(superBound); + lowerBounds = Stream.ofNullable(superBound); } TypeMirror extendsBound = wt.getExtendsBound(); Stream upperBounds; - if (extendsBound instanceof IntersectionType) { - upperBounds = ((IntersectionType) extendsBound).getBounds().stream(); + if (extendsBound instanceof IntersectionType it) { + upperBounds = it.getBounds().stream(); } else if (extendsBound == null) { upperBounds = Stream.of(visitorContext.getElements().getTypeElement("java.lang.Object").asType()); } else { @@ -408,10 +395,10 @@ public String toString() { wt, upperBounds .map(tm -> (JavaClassElement) mirrorToClassElement(tm, visitorContext, finalGenericsInfo, includeTypeAnnotations)) - .collect(Collectors.toList()), + .toList(), lowerBounds .map(tm -> (JavaClassElement) mirrorToClassElement(tm, visitorContext, finalGenericsInfo, includeTypeAnnotations)) - .collect(Collectors.toList()) + .toList() ); } return PrimitiveElement.VOID; @@ -444,19 +431,18 @@ private ClassElement resolveTypeVariable(JavaVisitorContext visitorContext, TypeMirror bound = boundGenerics.get(tv.toString()); if (bound != null && bound != declaration) { return mirrorToClassElement(bound, visitorContext, genericsInfo, includeTypeAnnotations, true); - } else { - // type variable is still free. - List boundsUnresolved = upperBound instanceof IntersectionType ? - ((IntersectionType) upperBound).getBounds() : - Collections.singletonList(upperBound); - List bounds = boundsUnresolved.stream() - .map(tm -> (JavaClassElement) mirrorToClassElement(tm, - visitorContext, - genericsInfo, - includeTypeAnnotations)) - .collect(Collectors.toList()); - return new JavaGenericPlaceholderElement(tv, bounds, elementAnnotationMetadataFactory, 0); } + // type variable is still free. + List boundsUnresolved = upperBound instanceof IntersectionType ? + ((IntersectionType) upperBound).getBounds() : + Collections.singletonList(upperBound); + List bounds = boundsUnresolved.stream() + .map(tm -> (JavaClassElement) mirrorToClassElement(tm, + visitorContext, + genericsInfo, + includeTypeAnnotations)) + .toList(); + return new JavaGenericPlaceholderElement(tv, bounds, elementAnnotationMetadataFactory, 0); } private Map resolveBoundGenerics(JavaVisitorContext visitorContext, Map> genericsInfo) { diff --git a/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaGenericPlaceholderElement.java b/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaGenericPlaceholderElement.java index 474ff4f6f72..e7dea2a60e7 100644 --- a/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaGenericPlaceholderElement.java +++ b/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaGenericPlaceholderElement.java @@ -19,8 +19,9 @@ import io.micronaut.core.annotation.NonNull; import io.micronaut.inject.ast.ClassElement; import io.micronaut.inject.ast.Element; -import io.micronaut.inject.ast.annotation.ElementAnnotationMetadataFactory; import io.micronaut.inject.ast.GenericPlaceholderElement; +import io.micronaut.inject.ast.annotation.ElementAnnotationMetadataFactory; +import io.micronaut.inject.ast.annotation.MutableAnnotationMetadataDelegate; import javax.lang.model.element.TypeParameterElement; import javax.lang.model.type.TypeVariable; @@ -58,6 +59,11 @@ final class JavaGenericPlaceholderElement extends JavaClassElement implements Ge this.bounds = bounds; } + @Override + public MutableAnnotationMetadataDelegate getAnnotationMetadata() { + return bounds.get(0).getAnnotationMetadata(); + } + @Override public Object getNativeType() { // Native types should be always Element diff --git a/inject-java/src/test/groovy/io/micronaut/visitors/ClassElementSpec.groovy b/inject-java/src/test/groovy/io/micronaut/visitors/ClassElementSpec.groovy index d82ce8e427c..596932084e7 100644 --- a/inject-java/src/test/groovy/io/micronaut/visitors/ClassElementSpec.groovy +++ b/inject-java/src/test/groovy/io/micronaut/visitors/ClassElementSpec.groovy @@ -19,6 +19,7 @@ import io.micronaut.annotation.processing.test.AbstractTypeElementSpec import io.micronaut.annotation.processing.visitor.JavaClassElement import io.micronaut.context.exceptions.BeanContextException import io.micronaut.core.annotation.AnnotationUtil +import io.micronaut.core.annotation.Introspected import io.micronaut.inject.ast.ClassElement import io.micronaut.inject.ast.ConstructorElement import io.micronaut.inject.ast.Element @@ -1463,6 +1464,37 @@ class Pet { packPrvFields.stream().map(FieldElement::getName).toList() == ["packprivme", "PACK_PRV_CONST"] } + void "test annotations on generic type"() { + given: + ClassElement classElement = buildClassElement(''' +package test; + +import io.micronaut.core.annotation.Introspected; +import io.micronaut.http.annotation.*; +import jakarta.inject.Inject; + +@Controller("/pets") +interface PetOperations { + + @Post("/") + T save(String name, int age); +} + +@Introspected +class Pet { +} + +''') + when: + def method = classElement.getEnclosedElements(ElementQuery.ALL_METHODS.named("save")).get(0) + def returnType = method.getReturnType() + def genericReturnType = method.getGenericReturnType() + + then: + returnType.hasAnnotation(Introspected) + genericReturnType.hasAnnotation(Introspected) + } + private void assertMethodsByName(List allMethods, String name, List expectedDeclaringTypeSimpleNames) { Collection methods = collectElements(allMethods, name) assert expectedDeclaringTypeSimpleNames.size() == methods.size() From f8eac6ec776728e0139a8ce80eacb2ef69852008 Mon Sep 17 00:00:00 2001 From: Denis Stepanov Date: Mon, 14 Nov 2022 14:32:05 +0700 Subject: [PATCH 215/743] Correct configuration builder and recognition of the property's field (#8319) --- .../ast/utils/AstBeanPropertiesUtils.java | 89 ++++++++----------- ...ConfigurationReaderBeanElementCreator.java | 14 ++- .../groovy/visitor/GroovyClassElement.java | 8 +- .../ConfigurationBuilderSpec.groovy | 42 ++++++++- 4 files changed, 96 insertions(+), 57 deletions(-) diff --git a/core-processor/src/main/java/io/micronaut/inject/ast/utils/AstBeanPropertiesUtils.java b/core-processor/src/main/java/io/micronaut/inject/ast/utils/AstBeanPropertiesUtils.java index e5870b9d1ac..554be78c032 100644 --- a/core-processor/src/main/java/io/micronaut/inject/ast/utils/AstBeanPropertiesUtils.java +++ b/core-processor/src/main/java/io/micronaut/inject/ast/utils/AstBeanPropertiesUtils.java @@ -85,11 +85,7 @@ public static List resolveBeanProperties(BeanPropertiesQuery co Map props = new LinkedHashMap<>(); for (MethodElement methodElement : methodsSupplier.get()) { // Records include everything - if (methodElement.isStatic() && !configuration.isAllowStaticProperties() - || !excludeElementsInRole && (methodElement.hasDeclaredAnnotation(AnnotationUtil.INJECT) - || methodElement.hasDeclaredAnnotation(AnnotationUtil.PRE_DESTROY) - || methodElement.hasDeclaredAnnotation(AnnotationUtil.POST_CONSTRUCT)) - ) { + if (methodElement.isStatic() && !configuration.isAllowStaticProperties() || !excludeElementsInRole && isMethodInRole(methodElement)) { continue; } String methodName = methodElement.getName(); @@ -117,11 +113,7 @@ public static List resolveBeanProperties(BeanPropertiesQuery co } } for (FieldElement fieldElement : fieldSupplier.get()) { - if (fieldElement.isStatic() && !configuration.isAllowStaticProperties() - || !excludeElementsInRole && (fieldElement.hasDeclaredAnnotation(AnnotationUtil.INJECT) - || fieldElement.hasStereotype(Value.class) - || fieldElement.hasStereotype(Property.class)) - ) { + if (fieldElement.isStatic() && !configuration.isAllowStaticProperties() || !excludeElementsInRole && isFieldInRole(fieldElement)) { continue; } String propertyName = fieldElement.getSimpleName(); @@ -164,6 +156,7 @@ && countGenericTypeAnnotations(value.getter.getGenericReturnType()) > countGener value.isExcluded = shouldExclude(includes, excludes, propertyName) || isExcludedByAnnotations(configuration, value) || isExcludedBecauseOfMissingAccess(value); + PropertyElement propertyElement = propertyCreator.apply(value); if (propertyElement != null) { beanProperties.add(propertyElement); @@ -175,6 +168,18 @@ && countGenericTypeAnnotations(value.getter.getGenericReturnType()) > countGener return Collections.emptyList(); } + private static boolean isFieldInRole(FieldElement fieldElement) { + return fieldElement.hasDeclaredAnnotation(AnnotationUtil.INJECT) + || fieldElement.hasStereotype(Value.class) + || fieldElement.hasStereotype(Property.class); + } + + private static boolean isMethodInRole(MethodElement methodElement) { + return methodElement.hasDeclaredAnnotation(AnnotationUtil.INJECT) + || methodElement.hasDeclaredAnnotation(AnnotationUtil.PRE_DESTROY) + || methodElement.hasDeclaredAnnotation(AnnotationUtil.POST_CONSTRUCT); + } + private static int countGenericTypeAnnotations(ClassElement cl) { return cl.getTypeArguments().values().stream().mapToInt(t -> t.getAnnotationMetadata().getAnnotationNames().size()).sum(); } @@ -269,55 +274,33 @@ private static void resolveWriteAccessForField(FieldElement fieldElement, boolea return; } ClassElement fieldType = unwrapType(fieldElement.getGenericType()); - if (beanPropertyData.setter == null) { - if (beanPropertyData.type != null) { - if (fieldType.isAssignable(unwrapType(beanPropertyData.type))) { - beanPropertyData.field = fieldElement; - if (isAccessor) { - beanPropertyData.writeAccessKind = BeanProperties.AccessKind.FIELD; - } - } - // Else: not compatible field - } else { - beanPropertyData.field = fieldElement; - beanPropertyData.type = fieldElement.getGenericType(); - if (isAccessor) { - beanPropertyData.writeAccessKind = BeanProperties.AccessKind.FIELD; - } - } - } else { + if (beanPropertyData.type == null || fieldType.isAssignable(unwrapType(beanPropertyData.type))) { beanPropertyData.field = fieldElement; + } else { + isAccessor = false; // not compatible field or setter is present + } + if (beanPropertyData.setter == null && isAccessor) { + // Use the field for read + beanPropertyData.writeAccessKind = BeanProperties.AccessKind.FIELD; + } + if (beanPropertyData.type == null) { + beanPropertyData.type = fieldElement.getGenericType(); } } private static void resolveReadAccessForField(FieldElement fieldElement, boolean isAccessor, BeanPropertyData beanPropertyData) { - ClassElement unwrappedFieldType = unwrapType(fieldElement.getGenericType()); - if (beanPropertyData.getter == null) { - if (beanPropertyData.type != null) { - if (unwrappedFieldType.isAssignable(unwrapType(beanPropertyData.type))) { - // Override the existing type to include generic annotations - if (beanPropertyData.type.isAssignable(fieldElement.getGenericType())) { - beanPropertyData.type = fieldElement.getGenericType(); - } - beanPropertyData.field = fieldElement; - if (isAccessor) { - beanPropertyData.readAccessKind = BeanProperties.AccessKind.FIELD; - } - } - // Else: not compatible field - } else { - beanPropertyData.field = fieldElement; - beanPropertyData.type = fieldElement.getGenericType(); - if (isAccessor) { - beanPropertyData.readAccessKind = BeanProperties.AccessKind.FIELD; - } - } - } else { + ClassElement fieldType = unwrapType(fieldElement.getGenericType()); + if (beanPropertyData.type == null || fieldType.isAssignable(unwrapType(beanPropertyData.type))) { beanPropertyData.field = fieldElement; - if (beanPropertyData.type.isAssignable(fieldElement.getGenericType())) { - // Override the existing type to include generic annotations - beanPropertyData.type = fieldElement.getGenericType(); - } + } else { + isAccessor = false; // not compatible field or getter is present + } + if (beanPropertyData.getter == null && isAccessor) { + // Use the field for write + beanPropertyData.readAccessKind = BeanProperties.AccessKind.FIELD; + } + if (beanPropertyData.type == null) { + beanPropertyData.type = fieldElement.getGenericType(); } } diff --git a/core-processor/src/main/java/io/micronaut/inject/processing/ConfigurationReaderBeanElementCreator.java b/core-processor/src/main/java/io/micronaut/inject/processing/ConfigurationReaderBeanElementCreator.java index 35802179e60..e8cd8f6dd03 100644 --- a/core-processor/src/main/java/io/micronaut/inject/processing/ConfigurationReaderBeanElementCreator.java +++ b/core-processor/src/main/java/io/micronaut/inject/processing/ConfigurationReaderBeanElementCreator.java @@ -233,7 +233,19 @@ protected boolean visitProperty(BeanDefinitionVisitor visitor, PropertyElement p @Override protected boolean visitField(BeanDefinitionVisitor visitor, FieldElement fieldElement) { - if (fieldElement.hasStereotype(ConfigurationBuilder.class) && !fieldElement.isAccessible(classElement)) { + if (fieldElement.hasStereotype(ConfigurationBuilder.class)) { + if (fieldElement.isAccessible(classElement)) { + ClassElement builderType = fieldElement.getType(); + visitor.visitConfigBuilderField( + builderType, + fieldElement.getName(), + fieldElement.getAnnotationMetadata(), + metadataBuilder, + builderType.isInterface() + ); + visitConfigurationBuilder(visitor, fieldElement, builderType); + return true; + } throw new ProcessingException(fieldElement, "ConfigurationBuilder applied to a non accessible (private or package-private/protected in a different package) field must have a corresponding non-private getter method."); } return super.visitField(visitor, fieldElement); diff --git a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyClassElement.java b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyClassElement.java index ffc247922f7..b7c67fb4710 100644 --- a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyClassElement.java +++ b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyClassElement.java @@ -526,6 +526,7 @@ public List getBeanProperties() { return properties; } + @Nullable private GroovyPropertyElement mapPropertyElement(Set nativeProps, AstBeanPropertiesUtils.BeanPropertyData value, BeanPropertiesQuery conf, @@ -542,7 +543,10 @@ public AnnotationMetadata getAnnotationMetadata() { return new AnnotationMetadataHierarchy(GroovyClassElement.this, ref.get().getAnnotationMetadata()); } }; - if (value.readAccessKind != BeanProperties.AccessKind.METHOD) { + if (nativePropertiesOnly && value.field == null) { + return null; + } + if (value.field != null && value.readAccessKind != BeanProperties.AccessKind.METHOD) { String getterName = NameUtils.getterNameFor( value.propertyName, value.type.equals(PrimitiveElement.BOOLEAN) @@ -563,7 +567,7 @@ public AnnotationMetadata getAnnotationMetadata() { value.getter = null; value.readAccessKind = null; } - if (!value.field.isFinal() && value.writeAccessKind != BeanProperties.AccessKind.METHOD) { + if (value.field != null && !value.field.isFinal() && value.writeAccessKind != BeanProperties.AccessKind.METHOD) { value.setter = MethodElement.of( this, value.field.getDeclaringType(), diff --git a/inject-java/src/test/groovy/io/micronaut/inject/configuration/ConfigurationBuilderSpec.groovy b/inject-java/src/test/groovy/io/micronaut/inject/configuration/ConfigurationBuilderSpec.groovy index 567c616b270..3d70bbf9b77 100644 --- a/inject-java/src/test/groovy/io/micronaut/inject/configuration/ConfigurationBuilderSpec.groovy +++ b/inject-java/src/test/groovy/io/micronaut/inject/configuration/ConfigurationBuilderSpec.groovy @@ -34,6 +34,42 @@ final class TestProps { noExceptionThrown() ctx.getProperty("test.props.manufacturer", String).get() == "Toyota" testPropBean.getBuilder().build().getManufacturer() == "Toyota" + + cleanup: + ctx.close() + } + + void "test definition uses field when getter type doesn't match"() { + given: + ApplicationContext ctx = buildContext("test.TestProps", ''' +package test; + +import io.micronaut.context.annotation.*; +import io.micronaut.inject.configuration.Engine; + +@ConfigurationProperties("test.props") +final class TestProps { + @ConfigurationBuilder(prefixes = "with") + protected Engine.Builder engine = Engine.builder(); + + public final Engine getEngine() { + return engine.build(); + } +} +''') + ctx.getEnvironment().addPropertySource(PropertySource.of(["test.props.manufacturer": "Toyota"])) + + when: + Class testProps = ctx.classLoader.loadClass("test.TestProps") + def testPropBean = ctx.getBean(testProps) + + then: + noExceptionThrown() + ctx.getProperty("test.props.manufacturer", String).get() == "Toyota" + testPropBean.getEngine().getManufacturer() == "Toyota" + + cleanup: + ctx.close() } void "test private config field with no getter throws an error"() { @@ -55,7 +91,6 @@ final class TestProps { RuntimeException ex = thrown() ex.message.contains("ConfigurationBuilder applied to a non accessible (private or package-private/protected in a different package) field must have a corresponding non-private getter method.") ex.message.contains("private Engine.Builder builder = Engine.builder();") - } void "test config field with setter abnormal paramater name"() { @@ -91,6 +126,8 @@ final class TestProps { ctx.getProperty("test.props.manufacturer", String).get() == "Toyota" testPropBean.getBuilder().build().getManufacturer() == "Toyota" + cleanup: + ctx.close() } void "test configuration builder that are interfaces"() { @@ -173,6 +210,9 @@ interface Foo { then: noExceptionThrown() testPropBean.builder.build().getMaxConcurrency() == 123 + + cleanup: + ctx.close() } } From a75bff9743cb776b70d43451fb30916f78384e4a Mon Sep 17 00:00:00 2001 From: Graeme Rocher Date: Mon, 14 Nov 2022 15:25:23 +0100 Subject: [PATCH 216/743] Improve error messages for disabled beans (#8326) * Improve error messages for disabled beans * make sure bean disabled errors are unique * address CR --- .../micronaut/aop/writer/AopProxyWriter.java | 5 + .../writer/BeanDefinitionReferenceWriter.java | 31 ++- .../inject/writer/BeanDefinitionVisitor.java | 6 + .../inject/writer/BeanDefinitionWriter.java | 14 ++ .../io/micronaut/core/beans/BeanInfo.java | 17 +- .../inject/beans/BeanDefinitionSpec.groovy | 40 +++- .../configurations/RequiresBeanSpec.groovy | 19 +- .../inject/requires/RequiresSpec.groovy | 33 ++- .../AbstractBeanContextConditional.java | 22 +- .../context/DefaultApplicationContext.java | 95 +++++---- .../micronaut/context/DefaultBeanContext.java | 188 +++++++++++++++--- .../io/micronaut/context/DisabledBean.java | 86 ++++++++ .../io/micronaut/inject/BeanDefinition.java | 51 +---- .../inject/BeanDefinitionReference.java | 2 +- .../micronaut/inject/QualifiedBeanType.java | 83 ++++++++ 15 files changed, 559 insertions(+), 133 deletions(-) create mode 100644 inject/src/main/java/io/micronaut/context/DisabledBean.java create mode 100644 inject/src/main/java/io/micronaut/inject/QualifiedBeanType.java diff --git a/core-processor/src/main/java/io/micronaut/aop/writer/AopProxyWriter.java b/core-processor/src/main/java/io/micronaut/aop/writer/AopProxyWriter.java index c176156aafb..63e4e01dfaa 100644 --- a/core-processor/src/main/java/io/micronaut/aop/writer/AopProxyWriter.java +++ b/core-processor/src/main/java/io/micronaut/aop/writer/AopProxyWriter.java @@ -1134,6 +1134,11 @@ public ClassElement[] getTypeArguments() { return proxyBeanDefinitionWriter.getTypeArguments(); } + @Override + public Map getTypeArgumentMap() { + return proxyBeanDefinitionWriter.getTypeArgumentMap(); + } + /** * Write the class to output via a visitor that manages output destination. * diff --git a/core-processor/src/main/java/io/micronaut/inject/writer/BeanDefinitionReferenceWriter.java b/core-processor/src/main/java/io/micronaut/inject/writer/BeanDefinitionReferenceWriter.java index dae28ce8806..56f5a89b58f 100644 --- a/core-processor/src/main/java/io/micronaut/inject/writer/BeanDefinitionReferenceWriter.java +++ b/core-processor/src/main/java/io/micronaut/inject/writer/BeanDefinitionReferenceWriter.java @@ -24,12 +24,15 @@ import io.micronaut.core.annotation.AnnotationMetadata; import io.micronaut.core.annotation.AnnotationUtil; import io.micronaut.core.annotation.Internal; +import io.micronaut.core.type.Argument; import io.micronaut.core.type.DefaultArgument; +import io.micronaut.core.util.CollectionUtils; import io.micronaut.core.util.StringUtils; import io.micronaut.inject.AdvisedBeanType; import io.micronaut.inject.BeanDefinition; import io.micronaut.inject.BeanDefinitionReference; import io.micronaut.inject.annotation.AnnotationMetadataReference; +import io.micronaut.inject.ast.ClassElement; import jakarta.inject.Singleton; import org.objectweb.asm.ClassWriter; import org.objectweb.asm.Type; @@ -37,6 +40,9 @@ import java.io.IOException; import java.io.OutputStream; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.Map; /** * Writes the bean definition class file to disk. @@ -74,6 +80,7 @@ public class BeanDefinitionReferenceWriter extends AbstractAnnotationMetadataWri private final String beanDefinitionReferenceClassName; private final Type interceptedType; private final Type providedType; + private final Map typeParameters; private boolean contextScope = false; private boolean requiresMethodProcessing; @@ -90,6 +97,7 @@ public BeanDefinitionReferenceWriter(BeanDefinitionVisitor visitor) { true); this.providedType = visitor.getProvidedType(); this.beanTypeName = visitor.getBeanTypeName(); + this.typeParameters = visitor.getTypeArgumentMap(); this.beanDefinitionName = visitor.getBeanDefinitionName(); this.beanDefinitionReferenceClassName = beanDefinitionName + REF_SUFFIX; this.beanDefinitionClassInternalName = getInternalName(beanDefinitionName) + REF_SUFFIX; @@ -165,7 +173,6 @@ private ClassWriter generateClassBytes() { interfaceInternalNames ); Type beanDefinitionType = getTypeReferenceForName(beanDefinitionName); - writeAnnotationMetadataStaticInitializer(classWriter); GeneratorAdapter cv = startConstructor(classWriter); @@ -241,11 +248,33 @@ private ClassWriter generateClassBytes() { getBeanType.returnValue(); getBeanType.visitMaxs(2, 1); + if (CollectionUtils.isNotEmpty(typeParameters)) { + // start method: Argument getGenericBeanType() + GeneratorAdapter getGenericType = startPublicMethodZeroArgs(classWriter, Argument.class, "getGenericBeanType"); + pushCreateArgument( + beanDefinitionReferenceClassName, + beanDefinitionType, + classWriter, + getGenericType, + "T", + ClassElement.of(beanTypeName), + annotationMetadata, + typeParameters, + new HashMap<>(), + loadTypeMethods + ); + getGenericType.returnValue(); + getGenericType.visitMaxs(2, 1); + } + + writeAnnotationMetadataStaticInitializer(classWriter); + if (interceptedType != null) { super.implementInterceptedTypeMethod(interceptedType, classWriter); } for (GeneratorAdapter generatorAdapter : loadTypeMethods.values()) { generatorAdapter.visitMaxs(3, 1); + generatorAdapter.visitEnd(); } return classWriter; diff --git a/core-processor/src/main/java/io/micronaut/inject/writer/BeanDefinitionVisitor.java b/core-processor/src/main/java/io/micronaut/inject/writer/BeanDefinitionVisitor.java index 7fe4c90d4c4..59d2049e83f 100644 --- a/core-processor/src/main/java/io/micronaut/inject/writer/BeanDefinitionVisitor.java +++ b/core-processor/src/main/java/io/micronaut/inject/writer/BeanDefinitionVisitor.java @@ -105,6 +105,12 @@ void visitDefaultConstructor( VisitorContext visitorContext ); + /** + * @return A map of the type arguments for the bean. + */ + @NonNull + Map getTypeArgumentMap(); + /** * @return The name of the bean definition reference class. */ diff --git a/core-processor/src/main/java/io/micronaut/inject/writer/BeanDefinitionWriter.java b/core-processor/src/main/java/io/micronaut/inject/writer/BeanDefinitionWriter.java index 4da869f661c..bd20455ab9c 100644 --- a/core-processor/src/main/java/io/micronaut/inject/writer/BeanDefinitionWriter.java +++ b/core-processor/src/main/java/io/micronaut/inject/writer/BeanDefinitionWriter.java @@ -751,6 +751,20 @@ public ClassElement[] getTypeArguments() { return BeanDefinitionVisitor.super.getTypeArguments(); } + @Override + @NonNull + public Map getTypeArgumentMap() { + if (hasTypeArguments()) { + Map args = this.typeArguments.get(this.getBeanTypeName()); + if (CollectionUtils.isNotEmpty(args)) { + return Collections.unmodifiableMap(args); + } + } + return Collections.emptyMap(); + } + + + /** * @return The name of the bean definition reference class. */ diff --git a/core/src/main/java/io/micronaut/core/beans/BeanInfo.java b/core/src/main/java/io/micronaut/core/beans/BeanInfo.java index 84c649b0684..fa2d329e849 100644 --- a/core/src/main/java/io/micronaut/core/beans/BeanInfo.java +++ b/core/src/main/java/io/micronaut/core/beans/BeanInfo.java @@ -17,6 +17,8 @@ import io.micronaut.core.annotation.AnnotationMetadataProvider; import io.micronaut.core.annotation.NonNull; +import io.micronaut.core.type.Argument; +import io.micronaut.core.type.ArgumentCoercible; /** * Top level interface for obtaining bean information. @@ -24,10 +26,23 @@ * @param The type of the bean * @since 4.0.0 */ -public interface BeanInfo extends AnnotationMetadataProvider { +public interface BeanInfo extends AnnotationMetadataProvider, ArgumentCoercible { /** * @return The bean type */ @NonNull Class getBeanType(); + + /** + * @return The generic bean type + */ + @NonNull + default Argument getGenericBeanType() { + return Argument.of(getBeanType()); + } + + @Override + default Argument asArgument() { + return getGenericBeanType(); + } } diff --git a/inject-java/src/test/groovy/io/micronaut/inject/beans/BeanDefinitionSpec.groovy b/inject-java/src/test/groovy/io/micronaut/inject/beans/BeanDefinitionSpec.groovy index f1d634da626..31ee613e133 100644 --- a/inject-java/src/test/groovy/io/micronaut/inject/beans/BeanDefinitionSpec.groovy +++ b/inject-java/src/test/groovy/io/micronaut/inject/beans/BeanDefinitionSpec.groovy @@ -4,6 +4,7 @@ import io.micronaut.core.annotation.AnnotationUtil import io.micronaut.core.annotation.Order import io.micronaut.core.order.Ordered import io.micronaut.annotation.processing.test.AbstractTypeElementSpec +import io.micronaut.inject.BeanDefinitionReference import io.micronaut.inject.qualifiers.Qualifiers import spock.lang.Issue @@ -12,6 +13,7 @@ import jakarta.inject.Qualifier class BeanDefinitionSpec extends AbstractTypeElementSpec { + void 'test dynamic instantiate with constructor'() { given: def definition = buildBeanDefinition('genctor.Test', ''' @@ -111,10 +113,10 @@ class Test { } interface X { - + } class Y implements X { - + } ''') @@ -164,10 +166,10 @@ class Test { } interface X { - + } class Y implements X { - + } ''') @@ -177,6 +179,36 @@ class Y implements X { e.message.contains("Bean defines an exposed type [limittypes.Y] that is not implemented by the bean type") } + void "test generics from factory"() { + when: + def ref = buildBeanDefinitionReference('limittypes.Test$Method0', ''' +package limittypes; + +import io.micronaut.context.annotation.*; +import jakarta.inject.Singleton; + +@Factory +class Test { + + @Singleton + X method() { + return new Y(); + } +} + +interface X { + +} +class Y implements X { + +} + +''') + + then: + ref.getGenericBeanType().getTypeString(true) == 'X' + } + void "test exposed bean types with factory invalid type"() { when: buildBeanDefinition('limittypes.Test$Method0', ''' diff --git a/inject-java/src/test/groovy/io/micronaut/inject/configurations/RequiresBeanSpec.groovy b/inject-java/src/test/groovy/io/micronaut/inject/configurations/RequiresBeanSpec.groovy index ddf1e1107fe..ef02b8bf4b3 100644 --- a/inject-java/src/test/groovy/io/micronaut/inject/configurations/RequiresBeanSpec.groovy +++ b/inject-java/src/test/groovy/io/micronaut/inject/configurations/RequiresBeanSpec.groovy @@ -20,6 +20,7 @@ import io.micronaut.context.BeanContext import io.micronaut.context.DefaultApplicationContext import io.micronaut.context.DefaultBeanContext import io.micronaut.context.env.PropertySource +import io.micronaut.context.exceptions.NoSuchBeanException import io.micronaut.inject.configurations.requiresbean.RequiresBean import io.micronaut.inject.configurations.requiresconditionfalse.GitHubActionsBean import io.micronaut.inject.configurations.requiresconditiontrue.TrueBean @@ -34,7 +35,7 @@ class RequiresBeanSpec extends Specification { void "test that a configuration can require a bean"() { given: - BeanContext context = new DefaultBeanContext() + BeanContext context = BeanContext.run() context.start() expect: @@ -42,6 +43,9 @@ class RequiresBeanSpec extends Specification { !context.containsBean(RequiresBean) !context.containsBean(RequiresConfig) Jvm.current.isJava9Compatible() || !context.containsBean(RequiresJava9) + + cleanup: + context.close() } @IgnoreIf({ env["GITHUB_ACTIONS"] } ) // fails on GitHub actions, which is expected @@ -68,10 +72,21 @@ class RequiresBeanSpec extends Specification { void "test requires property when not present"() { given: ApplicationContext applicationContext = new DefaultApplicationContext("test") + + when: applicationContext.start() - expect: + then: !applicationContext.containsBean(RequiresProperty) + + when: + applicationContext.getBean(RequiresProperty) + + then: + def e = thrown(NoSuchBeanException) + def list = e.message.readLines().collect { it.trim()} + list[0] == 'No bean of type [io.micronaut.inject.configurations.requiresproperty.RequiresProperty] exists. The bean [RequiresProperty] is disabled because it is within the package [io.micronaut.inject.configurations.requiresproperty] which is disabled due to bean requirements:' + list[1] == '* Required property [data-source.url] with value [null] not present' } void "test requires property when present"() { diff --git a/inject-java/src/test/groovy/io/micronaut/inject/requires/RequiresSpec.groovy b/inject-java/src/test/groovy/io/micronaut/inject/requires/RequiresSpec.groovy index 35491fbcbc4..3e7897a1e8c 100644 --- a/inject-java/src/test/groovy/io/micronaut/inject/requires/RequiresSpec.groovy +++ b/inject-java/src/test/groovy/io/micronaut/inject/requires/RequiresSpec.groovy @@ -49,7 +49,38 @@ class MyBean { getBean(context, 'test.MyBean') then: - thrown(NoSuchBeanException) + def e = thrown(NoSuchBeanException) + def lines = e.message.readLines().collect { it.trim() } + lines[0] == 'No bean of type [test.MyBean] exists. The following matching beans are disabled by bean requirements:' + lines[1] == '* Bean of type [test.MyBean] is disabled because:' + lines[2] == '- Java major version [17] must be at least 800' + + cleanup: + context.close() + } + + void "test requires property equals - error"() { + given: + ApplicationContext context = buildContext( ''' +package test; + +import io.micronaut.context.annotation.*; + +@Requires(property="foo", value="bar") +@jakarta.inject.Singleton +class MyBean { +} +''') + + when:"the bean doesn't exist" + getBean(context, 'test.MyBean') + + then: + def e = thrown(NoSuchBeanException) + def lines = e.message.readLines().collect { it.trim() } + lines[0] == 'No bean of type [test.MyBean] exists. The following matching beans are disabled by bean requirements:' + lines[1] == '* Bean of type [test.MyBean] is disabled because:' + lines[2] == '- Required property [foo] with value [bar] not present' cleanup: context.close() diff --git a/inject/src/main/java/io/micronaut/context/AbstractBeanContextConditional.java b/inject/src/main/java/io/micronaut/context/AbstractBeanContextConditional.java index 5f77fe913a1..0e2c2f81205 100644 --- a/inject/src/main/java/io/micronaut/context/AbstractBeanContextConditional.java +++ b/inject/src/main/java/io/micronaut/context/AbstractBeanContextConditional.java @@ -42,19 +42,23 @@ abstract class AbstractBeanContextConditional implements BeanContextConditional, public boolean isEnabled(@NonNull BeanContext context, @Nullable BeanResolutionContext resolutionContext) { AnnotationMetadata annotationMetadata = getAnnotationMetadata(); Condition condition = annotationMetadata.hasStereotype(Requires.class) ? new RequiresCondition(annotationMetadata) : null; + DefaultBeanContext defaultBeanContext = (DefaultBeanContext) context; DefaultConditionContext conditionContext = new DefaultConditionContext<>( - (DefaultBeanContext) context, + defaultBeanContext, this, resolutionContext); boolean enabled = condition == null || condition.matches(conditionContext); - if (ConditionLog.LOG.isDebugEnabled() && !enabled) { - if (this instanceof BeanConfiguration) { - ConditionLog.LOG.debug("{} will not be loaded due to failing conditions:", this); - } else { - ConditionLog.LOG.debug("Bean [{}] will not be loaded due to failing conditions:", this); - } - for (Failure failure : conditionContext.getFailures()) { - ConditionLog.LOG.debug("* {}", failure.getMessage()); + if (!enabled) { + if (ConditionLog.LOG.isDebugEnabled()) { + if (this instanceof BeanConfiguration) { + ConditionLog.LOG.debug("{} will not be loaded due to failing conditions:", this); + } else { + ConditionLog.LOG.debug("Bean [{}] will not be loaded due to failing conditions:", this); + } + for (Failure failure : conditionContext.getFailures()) { + ConditionLog.LOG.debug("* {}", failure.getMessage()); + } } + defaultBeanContext.trackDisabledComponent(conditionContext); } return enabled; diff --git a/inject/src/main/java/io/micronaut/context/DefaultApplicationContext.java b/inject/src/main/java/io/micronaut/context/DefaultApplicationContext.java index ddfe771a3de..3ad1e70acf9 100644 --- a/inject/src/main/java/io/micronaut/context/DefaultApplicationContext.java +++ b/inject/src/main/java/io/micronaut/context/DefaultApplicationContext.java @@ -259,55 +259,64 @@ protected Collection> findBeanCandidates(BeanResolutionCon @Override protected NoSuchBeanException newNoSuchBeanException(@Nullable BeanResolutionContext resolutionContext, Argument beanType, Qualifier qualifier, String message) { + if (message == null) { + message = super.resolveDisabledBeanMessage(resolutionContext, beanType, qualifier); + } + + if (message == null) { + message = resolveIterableBeanMissingMessage(resolutionContext, beanType, qualifier, message); + } + + return super.newNoSuchBeanException(resolutionContext, beanType, qualifier, message); + } + + private String resolveIterableBeanMissingMessage(BeanResolutionContext resolutionContext, Argument beanType, Qualifier qualifier, String message) { BeanDefinition definition = findAnyBeanDefinition(resolutionContext, beanType); if (definition != null && definition.isIterable()) { if (definition.hasDeclaredAnnotation(EachProperty.class)) { - String propertyMissingMessage = computeEachPropertyMissingBeanMessage(qualifier, definition); - return new NoSuchBeanException( - beanType, - qualifier, - propertyMissingMessage - ); + message = resolveEachPropertyMissingBeanMessage(qualifier, definition); } else if (definition.hasDeclaredAnnotation(EachBean.class)) { + message = resolveEachBeanMissingMessage(resolutionContext, beanType, qualifier, definition); + } + } + return message; + } - List> dependencyChain = calculateDependencyChain(resolutionContext, definition); - StringBuilder messageBuilder = new StringBuilder(); - Argument requiredBeanType = beanType; - Iterator> i = dependencyChain.iterator(); - String ls = System.getProperty("line.separator"); - while (i.hasNext()) { - messageBuilder.append(ls); - BeanDefinition beanDefinition = i.next(); - Argument nextBeanType = beanDefinition.asArgument(); - messageBuilder.append("* [").append(requiredBeanType.getTypeString(true)) - .append("] requires the presence of a bean of type [") - .append(nextBeanType.getTypeString(false)) - .append("]"); - if (qualifier != null) { - messageBuilder.append(" with qualifier [").append(qualifier).append("]"); - } - messageBuilder.append(" which does not exist."); - if (beanDefinition.hasDeclaredAnnotation(EachProperty.class)) { - messageBuilder.append(ls); - String propertyMissingMessage = computeEachPropertyMissingBeanMessage(qualifier, beanDefinition); - messageBuilder.append("* ") - .append("[") - .append(nextBeanType.getTypeString(true)) - .append("] requires the presence of configuration. ") - .append(propertyMissingMessage); - break; - } - requiredBeanType = nextBeanType; - } - - return new NoSuchBeanException( - beanType, - qualifier, - messageBuilder.toString() - ); + @NonNull + private String resolveEachBeanMissingMessage(BeanResolutionContext resolutionContext, Argument beanType, Qualifier qualifier, BeanDefinition definition) { + String message; + List> dependencyChain = calculateDependencyChain(resolutionContext, definition); + StringBuilder messageBuilder = new StringBuilder(); + Argument requiredBeanType = beanType; + Iterator> i = dependencyChain.iterator(); + String ls = System.getProperty("line.separator"); + while (i.hasNext()) { + messageBuilder.append(ls); + BeanDefinition beanDefinition = i.next(); + Argument nextBeanType = beanDefinition.asArgument(); + messageBuilder.append("* [").append(requiredBeanType.getTypeString(true)) + .append("] requires the presence of a bean of type [") + .append(nextBeanType.getTypeString(false)) + .append("]"); + if (qualifier != null) { + messageBuilder.append(" with qualifier [").append(qualifier).append("]"); } + messageBuilder.append(" which does not exist."); + if (beanDefinition.hasDeclaredAnnotation(EachProperty.class)) { + messageBuilder.append(ls); + String propertyMissingMessage = resolveEachPropertyMissingBeanMessage(qualifier, beanDefinition); + messageBuilder.append("* ") + .append("[") + .append(nextBeanType.getTypeString(true)) + .append("] requires the presence of configuration. ") + .append(propertyMissingMessage); + break; + } + requiredBeanType = nextBeanType; } - return super.newNoSuchBeanException(resolutionContext, beanType, qualifier, message); + + message = messageBuilder.toString(); + return message; } @Nullable @@ -338,7 +347,7 @@ private List> calculateDependencyChain( } @NonNull - private static String computeEachPropertyMissingBeanMessage(Qualifier qualifier, BeanDefinition definition) { + private static String resolveEachPropertyMissingBeanMessage(Qualifier qualifier, BeanDefinition definition) { String prefix = definition.stringValue(EachProperty.class).orElse(""); if (qualifier != null) { if (qualifier instanceof Named named) { diff --git a/inject/src/main/java/io/micronaut/context/DefaultBeanContext.java b/inject/src/main/java/io/micronaut/context/DefaultBeanContext.java index 3ec57e54657..6ad68624a9d 100644 --- a/inject/src/main/java/io/micronaut/context/DefaultBeanContext.java +++ b/inject/src/main/java/io/micronaut/context/DefaultBeanContext.java @@ -25,6 +25,8 @@ import io.micronaut.context.annotation.Prototype; import io.micronaut.context.annotation.Replaces; import io.micronaut.context.annotation.Secondary; +import io.micronaut.context.condition.ConditionContext; +import io.micronaut.context.condition.Failure; import io.micronaut.context.env.PropertyPlaceholderResolver; import io.micronaut.context.event.ApplicationEventListener; import io.micronaut.context.event.ApplicationEventPublisher; @@ -103,6 +105,7 @@ import io.micronaut.inject.MethodExecutionHandle; import io.micronaut.inject.ParametrizedBeanFactory; import io.micronaut.inject.ProxyBeanDefinition; +import io.micronaut.inject.QualifiedBeanType; import io.micronaut.inject.ValidatedBeanDefinition; import io.micronaut.inject.proxy.InterceptedBeanProxy; import io.micronaut.inject.qualifiers.AnyQualifier; @@ -193,6 +196,9 @@ public > Stream reduce(Class beanType, S private final BeanContextConfiguration beanContextConfiguration; private final Collection beanDefinitionsClasses = new ConcurrentLinkedQueue<>(); + + private final Map, BeanDefinitionReference> disabledBeans = new ConcurrentHashMap<>(20); + private final Map> disabledConfigurations = new ConcurrentHashMap<>(5); private final Map beanConfigurations = new HashMap<>(10); private final Map containsBeanCache = new ConcurrentHashMap<>(30); private final Map attributes = Collections.synchronizedMap(new HashMap<>(5)); @@ -372,6 +378,35 @@ protected void registerConversionService() { registerSingleton(MutableConversionService.class, conversionService, null, false); } + /** + * Tracks when a bean or configuration is disabled. + * @param conditionContext The conditional context + * @param The component type + */ + @Internal + void trackDisabledComponent(@NonNull ConditionContext conditionContext) { + C component = conditionContext.getComponent(); + List reasons = conditionContext.getFailures().stream().map(Failure::getMessage).toList(); + if (component instanceof QualifiedBeanType beanType) { + try { + @SuppressWarnings("unchecked") + Argument argument = (Argument) beanType.getGenericBeanType(); + @SuppressWarnings("unchecked") + Qualifier declaredQualifier = (Qualifier) beanType.getDeclaredQualifier(); + this.disabledBeans.put(new BeanKey(argument, declaredQualifier), new DisabledBean<>( + argument, + declaredQualifier, + reasons + )); + } catch (Exception | NoClassDefFoundError e) { + // it is theoretically possible that resolving the generic type results in an error + // in this case just ignore this as the maps built here are purely to aid error diganosis + } + } else if (component instanceof BeanConfiguration configuration) { + this.disabledConfigurations.put(configuration.getName(), reasons); + } + } + /** * The close method will shut down the context calling {@link jakarta.annotation.PreDestroy} hooks on loaded * singletons. @@ -883,7 +918,7 @@ public T getBean(@NonNull Argument beanType, @Nullable Qualifier quali null, beanType, qualifier, - e.getMessage() + "Bean of type [" + beanType.getTypeString(true) + "] disabled for reason: " + e.getMessage() ); } } @@ -2178,6 +2213,16 @@ protected Collection> findBeanCandidates(@Nullable BeanRes beanDefinitionsClasses = this.beanDefinitionsClasses; } + return collectBeanCandidates(resolutionContext, beanType, filterProxied, predicate, beanDefinitionsClasses); + } + + @NonNull + private Set> collectBeanCandidates( + BeanResolutionContext resolutionContext, + Argument beanType, + boolean filterProxied, + Predicate> predicate, + Collection beanDefinitionsClasses) { Set> candidates; if (!beanDefinitionsClasses.isEmpty()) { @@ -2925,10 +2970,82 @@ protected NoSuchBeanException newNoSuchBeanException( if (message != null) { return new NoSuchBeanException(beanType, qualifier, message); } else { - return new NoSuchBeanException(beanType, qualifier); + String disabledMessage = resolveDisabledBeanMessage(resolutionContext, beanType, qualifier); + + if (disabledMessage != null) { + return new NoSuchBeanException(beanType, qualifier, disabledMessage); + } else { + return new NoSuchBeanException(beanType, qualifier); + } } } + /** + * Resolves the message to use for a disabled bean. + * @param resolutionContext The resolution context + * @param beanType The bean type + * @param qualifier The qualifier + * @return The message or null if none exists + * @param The bean type + */ + @Nullable + protected String resolveDisabledBeanMessage(BeanResolutionContext resolutionContext, Argument beanType, Qualifier qualifier) { + String disabledMessage = null; + for (Map.Entry> entry : disabledConfigurations.entrySet()) { + String pkg = entry.getKey(); + if (beanType.getTypeName().startsWith(pkg + ".")) { + StringBuilder messageBuilder = new StringBuilder(); + String ls = System.getProperty("line.separator"); + messageBuilder.append("The bean [") + .append(beanType.getTypeString(true)) + .append("] is disabled because it is within the package [") + .append(pkg) + .append("] which is disabled due to bean requirements: ") + .append(ls); + for (String failure : entry.getValue()) { + messageBuilder.append("* ").append(failure).append(ls); + } + + disabledMessage = messageBuilder.toString(); + break; + } + } + + if (disabledMessage == null) { + + Set> beanDefinitions = collectBeanCandidates( + resolutionContext, + beanType, + true, + null, + disabledBeans.values() + ); + if (qualifier != null) { + beanDefinitions = qualifier + .reduce(beanType.getType(), beanDefinitions.stream()) + .collect(Collectors.toSet()); + } + + if (!beanDefinitions.isEmpty()) { + StringBuilder messageBuilder = new StringBuilder(); + String ls = System.getProperty("line.separator"); + messageBuilder.append("The following matching beans are disabled by bean requirements: ").append(ls); + for (BeanDefinition beanDefinition : beanDefinitions) { + messageBuilder.append("* Bean of type [").append(beanDefinition.asArgument().getTypeString(false)) + .append("] is disabled because: ").append(ls); + if (beanDefinition instanceof DisabledBean disabledBean) { + for (String failure : disabledBean.reasons()) { + messageBuilder.append(" - ").append(failure).append(ls); + } + } + } + + disabledMessage = messageBuilder.toString(); + } + } + return disabledMessage; + } + @Nullable private BeanRegistration provideInjectionPoint(BeanResolutionContext resolutionContext, Argument beanType, @@ -3201,24 +3318,30 @@ private Optional> findConcreteCandidateNoCache(@Nullable B boolean throwNonUnique, boolean filterProxied) { - Predicate> predicate = new Predicate>() { - @Override - public boolean test(BeanDefinition candidate) { - if (candidate.isAbstract()) { - return false; - } - if (qualifier != null) { - if (candidate instanceof NoInjectionBeanDefinition) { - NoInjectionBeanDefinition noInjectionBeanDefinition = (NoInjectionBeanDefinition) candidate; - return qualifier.contains(noInjectionBeanDefinition.getQualifier()); - } + Predicate> predicate = candidate -> { + if (candidate.isAbstract()) { + return false; + } + if (qualifier != null) { + if (candidate instanceof NoInjectionBeanDefinition noInjectionBeanDefinition) { + return qualifier.contains(noInjectionBeanDefinition.getQualifier()); } - return true; - } + return true; }; Collection> candidates = new ArrayList<>(findBeanCandidates(resolutionContext, beanType, filterProxied, predicate)); + return pickOneBean(beanType, qualifier, throwNonUnique, filterProxied, predicate, candidates); + } + + @NonNull + private Optional> pickOneBean( + Argument beanType, + Qualifier qualifier, + boolean throwNonUnique, + boolean filterProxied, + Predicate> predicate, + Collection> candidates) { if (candidates.isEmpty()) { return Optional.empty(); } @@ -3234,8 +3357,7 @@ public boolean test(BeanDefinition candidate) { Stream> candidateStream = candidates.stream().filter(c -> { if (!c.isAbstract()) { - if (c instanceof NoInjectionBeanDefinition) { - NoInjectionBeanDefinition noInjectionBeanDefinition = (NoInjectionBeanDefinition) c; + if (c instanceof NoInjectionBeanDefinition noInjectionBeanDefinition) { return qualifier.contains(noInjectionBeanDefinition.getQualifier()); } return true; @@ -3253,9 +3375,9 @@ public boolean test(BeanDefinition candidate) { } definition = lastChanceResolve( - beanType, - qualifier, - throwNonUnique, + beanType, + qualifier, + throwNonUnique, beanDefinitionList ); } else { @@ -3266,10 +3388,10 @@ public boolean test(BeanDefinition candidate) { LOG.debug("Searching for @Primary for type [{}] from candidates: {} ", beanType.getName(), candidates); } definition = lastChanceResolve( - beanType, - qualifier, - throwNonUnique, - candidates + beanType, + qualifier, + throwNonUnique, + candidates ); } } @@ -3284,16 +3406,20 @@ public boolean test(BeanDefinition candidate) { return Optional.ofNullable(definition); } - private void filterProxiedTypes(Collection> candidates, boolean filterProxied, boolean filterDelegates, Predicate> predicate) { + private void filterProxiedTypes( + Collection> candidates, + boolean filterProxied, + boolean filterDelegates, + Predicate> predicate) { int count = candidates.size(); Set> proxiedTypes = new HashSet<>(count); Iterator> i = candidates.iterator(); Collection> delegates = filterDelegates ? new ArrayList<>(count) : Collections.emptyList(); while (i.hasNext()) { BeanDefinition candidate = i.next(); - if (candidate instanceof ProxyBeanDefinition) { + if (candidate instanceof ProxyBeanDefinition proxyBeanDefinition) { if (filterProxied) { - proxiedTypes.add(((ProxyBeanDefinition) candidate).getTargetDefinitionType()); + proxiedTypes.add(proxyBeanDefinition.getTargetDefinitionType()); } else { proxiedTypes.add(candidate.getClass()); } @@ -3305,8 +3431,8 @@ private void filterProxiedTypes(Collection> candidates, bo if (!delegates.contains(delegate) && (predicate == null || predicate.test(delegate))) { delegates.add(delegate); } - } else if (filterProxied && delegate instanceof ProxyBeanDefinition) { - proxiedTypes.add(((ProxyBeanDefinition) delegate).getTargetDefinitionType()); + } else if (filterProxied && delegate instanceof ProxyBeanDefinition proxyBeanDefinition) { + proxiedTypes.add(proxyBeanDefinition.getTargetDefinitionType()); } } } @@ -3333,7 +3459,7 @@ private BeanDefinition lastChanceResolve(Argument beanType, if (candidates.size() > 1) { List> primary = candidates.stream() .filter(BeanDefinition::isPrimary) - .collect(Collectors.toList()); + .toList(); if (!primary.isEmpty()) { candidates = primary; } @@ -3342,7 +3468,7 @@ private BeanDefinition lastChanceResolve(Argument beanType, return candidates.iterator().next(); } BeanDefinition definition = null; - candidates = candidates.stream().filter(candidate -> !candidate.hasDeclaredStereotype(Secondary.class)).collect(Collectors.toList()); + candidates = candidates.stream().filter(candidate -> !candidate.hasDeclaredStereotype(Secondary.class)).toList(); if (candidates.size() == 1) { return candidates.iterator().next(); } else if (candidates.stream().anyMatch(candidate -> candidate.hasAnnotation(Order.class))) { diff --git a/inject/src/main/java/io/micronaut/context/DisabledBean.java b/inject/src/main/java/io/micronaut/context/DisabledBean.java new file mode 100644 index 00000000000..074dfb19d4f --- /dev/null +++ b/inject/src/main/java/io/micronaut/context/DisabledBean.java @@ -0,0 +1,86 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.context; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.NonNull; +import io.micronaut.core.annotation.Nullable; +import io.micronaut.core.type.Argument; +import io.micronaut.inject.BeanDefinition; +import io.micronaut.inject.BeanDefinitionReference; + +import java.util.List; + +/** + * Data about a disabled bean. Used to improve error reporting. + * + * @param type The bean type + * @param qualifier The qualifier + * @param reasons The reasons the bean is disabled + * @param The bean type + */ +@Internal +record DisabledBean( + @NonNull Argument type, + @Nullable Qualifier qualifier, + @NonNull List reasons) + implements BeanDefinition, BeanDefinitionReference { + + @Override + public boolean isEnabled(BeanContext context, BeanResolutionContext resolutionContext) { + return true; + } + + @Override + public boolean isSingleton() { + return BeanDefinition.super.isSingleton(); + } + + @Override + public Qualifier getDeclaredQualifier() { + return qualifier; + } + + @Override + public Class getBeanType() { + return type.getType(); + } + + @Override + public Argument asArgument() { + return type; + } + + @Override + public Argument getGenericBeanType() { + return type; + } + + @Override + public String getBeanDefinitionName() { + return type.getTypeName(); + } + + @Override + public BeanDefinition load() { + return this; + } + + @Override + public boolean isPresent() { + return true; + } +} diff --git a/inject/src/main/java/io/micronaut/inject/BeanDefinition.java b/inject/src/main/java/io/micronaut/inject/BeanDefinition.java index 64330f86c6f..6b93ff296fe 100644 --- a/inject/src/main/java/io/micronaut/inject/BeanDefinition.java +++ b/inject/src/main/java/io/micronaut/inject/BeanDefinition.java @@ -22,18 +22,13 @@ import io.micronaut.context.annotation.EachBean; import io.micronaut.context.annotation.EachProperty; import io.micronaut.context.annotation.Provided; -import io.micronaut.core.annotation.AnnotationMetadata; -import io.micronaut.core.annotation.AnnotationMetadataDelegate; import io.micronaut.core.annotation.AnnotationUtil; -import io.micronaut.core.annotation.AnnotationValue; import io.micronaut.core.annotation.NonNull; import io.micronaut.core.annotation.Nullable; import io.micronaut.core.naming.Named; import io.micronaut.core.reflect.ReflectionUtils; import io.micronaut.core.type.Argument; import io.micronaut.core.type.ArgumentCoercible; -import io.micronaut.inject.annotation.AnnotationMetadataHierarchy; -import io.micronaut.inject.qualifiers.Qualifiers; import jakarta.inject.Singleton; import java.lang.annotation.Annotation; @@ -52,7 +47,7 @@ * @author Graeme Rocher * @since 1.0 */ -public interface BeanDefinition extends AnnotationMetadataDelegate, Named, BeanType, ArgumentCoercible { +public interface BeanDefinition extends QualifiedBeanType, Named, BeanType, ArgumentCoercible { /** * Attribute used to store a dynamic bean name. @@ -96,11 +91,12 @@ default Optional> getContainerElement() { } @Override + @SuppressWarnings("java:S3776") default boolean isCandidateBean(@Nullable Argument beanType) { if (beanType == null) { return false; } - if (BeanType.super.isCandidateBean(beanType)) { + if (QualifiedBeanType.super.isCandidateBean(beanType)) { final Argument[] typeArguments = beanType.getTypeParameters(); final int len = typeArguments.length; Class beanClass = beanType.getType(); @@ -157,7 +153,7 @@ default boolean isCandidateBean(@Nullable Argument beanType) { * @see Provided */ @SuppressWarnings("DeprecatedIsStillUsed") - @Deprecated + @Deprecated(forRemoval = true, since = "2.0.0") default boolean isProvided() { return getAnnotationMetadata().hasDeclaredStereotype(Provided.class); } @@ -421,48 +417,23 @@ default boolean isAbstract() { return Modifier.isAbstract(getBeanType().getModifiers()); } + @Override + default Argument getGenericBeanType() { + return asArgument(); + } + /** * Resolve the declared qualifier for this bean. * @return The qualifier or null if this isn't one */ default @Nullable Qualifier getDeclaredQualifier() { - AnnotationMetadata annotationMetadata = getTargetAnnotationMetadata(); - if (annotationMetadata instanceof AnnotationMetadataHierarchy) { - // Beans created by a factory will have AnnotationMetadataHierarchy = producing element + factory class - // All qualifiers are removed from the factory class anyway, so we can skip the hierarchy - annotationMetadata = annotationMetadata.getDeclaredMetadata(); - } - List> annotations = annotationMetadata.getAnnotationValuesByStereotype(AnnotationUtil.QUALIFIER); - if (!annotations.isEmpty()) { - if (annotations.size() == 1) { - final AnnotationValue annotationValue = annotations.iterator().next(); - if (annotationValue.getAnnotationName().equals(Qualifier.PRIMARY)) { - // primary is the same as null - return null; - } - return (Qualifier) Qualifiers.byAnnotation(annotationMetadata, annotationValue); - } else { - Qualifier[] qualifiers = new Qualifier[annotations.size()]; - int i = 0; - for (AnnotationValue annotationValue : annotations) { - qualifiers[i++] = (Qualifier) Qualifiers.byAnnotation(annotationMetadata, annotationValue); - } - return Qualifiers.byQualifiers(qualifiers); - } - } else { - Qualifier qualifier = resolveDynamicQualifier(); - if (qualifier == null) { - String name = annotationMetadata.stringValue(AnnotationUtil.NAMED).orElse(null); - qualifier = name != null ? Qualifiers.byAnnotation(annotationMetadata, name) : null; - } - return qualifier; - } + return QualifiedBeanType.super.getDeclaredQualifier(); } /** * @return Method that can be overridden to resolve a dynamic qualifier */ default @Nullable Qualifier resolveDynamicQualifier() { - return null; + return QualifiedBeanType.super.resolveDynamicQualifier(); } } diff --git a/inject/src/main/java/io/micronaut/inject/BeanDefinitionReference.java b/inject/src/main/java/io/micronaut/inject/BeanDefinitionReference.java index 35a9a8b32b4..337aa2bd85a 100644 --- a/inject/src/main/java/io/micronaut/inject/BeanDefinitionReference.java +++ b/inject/src/main/java/io/micronaut/inject/BeanDefinitionReference.java @@ -46,7 +46,7 @@ * @since 1.0 */ @Internal -public interface BeanDefinitionReference extends BeanType { +public interface BeanDefinitionReference extends QualifiedBeanType { /** * @return The class name of the backing {@link BeanDefinition} diff --git a/inject/src/main/java/io/micronaut/inject/QualifiedBeanType.java b/inject/src/main/java/io/micronaut/inject/QualifiedBeanType.java new file mode 100644 index 00000000000..d2aae9e6cf4 --- /dev/null +++ b/inject/src/main/java/io/micronaut/inject/QualifiedBeanType.java @@ -0,0 +1,83 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.inject; + +import io.micronaut.context.Qualifier; +import io.micronaut.core.annotation.AnnotationMetadata; +import io.micronaut.core.annotation.AnnotationMetadataDelegate; +import io.micronaut.core.annotation.AnnotationUtil; +import io.micronaut.core.annotation.AnnotationValue; +import io.micronaut.core.annotation.Nullable; +import io.micronaut.inject.annotation.AnnotationMetadataHierarchy; +import io.micronaut.inject.qualifiers.Qualifiers; + +import java.lang.annotation.Annotation; +import java.util.List; + +/** + * An interface for a {@link BeanType} that allows qualifiers. + * + * @param The bean type + * @since 4.0.0 + */ +public interface QualifiedBeanType extends BeanType, AnnotationMetadataDelegate { + + /** + * Resolve the declared qualifier for this bean. + * @return The qualifier or null if this isn't one + */ + @SuppressWarnings("java:S3776") + default @Nullable Qualifier getDeclaredQualifier() { + AnnotationMetadata annotationMetadata = getTargetAnnotationMetadata(); + if (annotationMetadata instanceof AnnotationMetadataHierarchy) { + // Beans created by a factory will have AnnotationMetadataHierarchy = producing element + factory class + // All qualifiers are removed from the factory class anyway, so we can skip the hierarchy + annotationMetadata = annotationMetadata.getDeclaredMetadata(); + } + List> annotations = annotationMetadata.getAnnotationValuesByStereotype(AnnotationUtil.QUALIFIER); + if (!annotations.isEmpty()) { + if (annotations.size() == 1) { + final AnnotationValue annotationValue = annotations.iterator().next(); + if (annotationValue.getAnnotationName().equals(Qualifier.PRIMARY)) { + // primary is the same as null + return null; + } + return (Qualifier) Qualifiers.byAnnotation(annotationMetadata, annotationValue); + } else { + Qualifier[] qualifiers = new Qualifier[annotations.size()]; + int i = 0; + for (AnnotationValue annotationValue : annotations) { + qualifiers[i++] = (Qualifier) Qualifiers.byAnnotation(annotationMetadata, annotationValue); + } + return Qualifiers.byQualifiers(qualifiers); + } + } else { + Qualifier qualifier = resolveDynamicQualifier(); + if (qualifier == null) { + String name = annotationMetadata.stringValue(AnnotationUtil.NAMED).orElse(null); + qualifier = name != null ? Qualifiers.byAnnotation(annotationMetadata, name) : null; + } + return qualifier; + } + } + + /** + * @return Method that can be overridden to resolve a dynamic qualifier + */ + default @Nullable Qualifier resolveDynamicQualifier() { + return null; + } +} From 4b021af68ccd4355a31e221d718bd12e96b82ecc Mon Sep 17 00:00:00 2001 From: Jonas Konrad Date: Mon, 14 Nov 2022 19:37:09 +0100 Subject: [PATCH 217/743] Fix StringUtils.trimLeading (#8335) Fixes #8328 --- core/src/main/java/io/micronaut/core/util/StringUtils.java | 2 +- .../test/groovy/io/micronaut/core/util/StringUtilsSpec.groovy | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/core/src/main/java/io/micronaut/core/util/StringUtils.java b/core/src/main/java/io/micronaut/core/util/StringUtils.java index f66b4f342e6..55b3e426c0c 100644 --- a/core/src/main/java/io/micronaut/core/util/StringUtils.java +++ b/core/src/main/java/io/micronaut/core/util/StringUtils.java @@ -262,7 +262,7 @@ public static String trimLeading(String str, Predicate predicate) { return str.substring(i); } } - return str; + return ""; } /** diff --git a/core/src/test/groovy/io/micronaut/core/util/StringUtilsSpec.groovy b/core/src/test/groovy/io/micronaut/core/util/StringUtilsSpec.groovy index 21b6c9d02ee..d621e0ba470 100644 --- a/core/src/test/groovy/io/micronaut/core/util/StringUtilsSpec.groovy +++ b/core/src/test/groovy/io/micronaut/core/util/StringUtilsSpec.groovy @@ -107,6 +107,7 @@ class StringUtilsSpec extends Specification { 'abc' | 'a' | 'bc' 'abc' | 'd' | 'abc' ' abc' | ' ' | 'abc' + 'aa' | 'a' | '' } @Unroll From 950ade6625291b10295be93fa72c1490378bf95f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Champeau?= Date: Tue, 15 Nov 2022 09:38:36 +0100 Subject: [PATCH 218/743] Rework BOM as core BOM (#8299) * Remove dependencies which are not directly used This commit removes from the core BOM all managed dependencies which are not directly used in build scripts. This basically means that those dependencies are only present so that precisely we can have them in the BOM. Now that the BOM is extracted in its own project, they are redundant. * Remove managed libraries which are not used This commit moves a number of libraries from the managed scope to unmanaged. The only dependencies which are managed are now the ones which are in api or implementation scope. Those dependencies were previously in the BOM, but won't be directly managed by core. This means a couple of things: 1. the new BOM will have to duplicate those dependencies, in order to manage them 2. the dependencies will have to be copied in the new BOM (because they are not present in the core BOM anymore, they won't be inlined like the other managed dependencies) * Rename `bom` -> `core-bom` This is a breaking change: it renames the `bom` project to `core-bom`. The new BOM will live in a separate project (`micronaut-bom`) under different GAV coordinates. * Reintroduce missing BOMs * Fix Groovy BOM coordinates * Update groovy-test coordinates * Restore entries for inject-xxx in core BOM * Revert "Update groovy-test coordinates" This reverts commit b376b79d69aa89da3d6b234d465c13746e8aa55b. --- bom/build.gradle | 77 ---- buffer-netty/build.gradle | 2 +- ...naut.build.internal.convention-base.gradle | 2 +- .../ext/DefaultMicronautCoreExtension.java | 2 +- context/build.gradle | 2 +- {bom => core-bom}/README.md | 0 core-bom/build.gradle | 21 ++ core/build.gradle | 3 +- gradle/libs.versions.toml | 348 ++++-------------- http-client/build.gradle | 2 +- http-netty/build.gradle | 2 +- http-server-netty/build.gradle | 4 +- inject-groovy-test/build.gradle | 2 +- inject-groovy/build.gradle | 10 +- inject-java-test/build.gradle | 2 +- inject-java/build.gradle | 16 +- inject-kotlin-test/build.gradle | 2 +- inject-test-utils/build.gradle | 2 +- inject/build.gradle | 4 +- jackson-databind/build.gradle | 4 +- management/build.gradle | 10 +- parent/build.gradle | 4 +- runtime/build.gradle | 10 +- settings.gradle | 2 +- test-suite-geb/build.gradle | 2 +- test-suite-groovy/build.gradle | 4 +- test-suite-javax-inject/build.gradle | 2 +- test-suite-kotlin/build.gradle | 8 +- test-suite/build.gradle | 14 +- test-utils/build.gradle | 2 +- validation/build.gradle | 4 +- 31 files changed, 155 insertions(+), 414 deletions(-) delete mode 100644 bom/build.gradle rename {bom => core-bom}/README.md (100%) create mode 100644 core-bom/build.gradle diff --git a/bom/build.gradle b/bom/build.gradle deleted file mode 100644 index fab5f29d1b3..00000000000 --- a/bom/build.gradle +++ /dev/null @@ -1,77 +0,0 @@ -plugins { - id 'io.micronaut.build.internal.bom' -} - -group projectGroupId -version projectVersion - -repositories { - mavenCentral() - maven { url "https://s01.oss.sonatype.org/content/repositories/snapshots/" } -} - -micronautBom { - extraExcludedProjects = [ - "benchmarks", - "inject-test-utils" - ] - catalogToPropertyNameOverrides = [ - 'jakarta.annotation.api': 'jakarta.annotation-api', - 'javax.annotation.api': 'javax.annotation-api', - 'methvin.directory.watcher': 'methvin.directory-watcher', - 'paho.v3': 'pahov3', - 'paho.v5': 'pahov5', - 'graal.sdk': 'graalSdk', - 'neo4j.java.driver': 'neo4j.bolt', - ] - propertyName = 'core' - suppressions { - // https://github.com/micronaut-projects/micronaut-serialization/issues/167 - dependencies.add("io.micronaut.serde:micronaut-serde-tck:1.0.0") - - // https://github.com/micronaut-projects/micronaut-oracle-cloud/issues/359 - dependencies.add("io.micronaut.oraclecloud:micronaut-oraclecloud-sdk-processor:2.1.1") - dependencies.add("io.micronaut.oraclecloud:micronaut-oraclecloud-atp-hikari-test:2.1.1") - dependencies.add("io.micronaut.oraclecloud:micronaut-example-http-function-java:2.1.1") - dependencies.add("io.micronaut.oraclecloud:micronaut-example-function-groovy:2.1.1") - dependencies.add("io.micronaut.oraclecloud:micronaut-example-http-function-kotlin:2.1.1") - dependencies.add("io.micronaut.oraclecloud:micronaut-example-groovy:2.1.1") - dependencies.add("io.micronaut.oraclecloud:micronaut-example-kotlin:2.1.1") - dependencies.add("io.micronaut.oraclecloud:micronaut-example-function-java:2.1.1") - dependencies.add("io.micronaut.oraclecloud:micronaut-example-java:2.1.1") - dependencies.add("io.micronaut.oraclecloud:micronaut-example-http-function-groovy:2.1.1") - dependencies.add("io.micronaut.oraclecloud:micronaut-example-function-kotlin:2.1.1") - dependencies.add("io.micronaut.oraclecloud:micronaut-oraclecloud-atp-ucp-test:2.1.1") - - // https://github.com/micronaut-projects/micronaut-data/issues/1403 - dependencies.add("io.micronaut.data:micronaut-data-tck:3.3.0") - dependencies.add("io.micronaut.data:micronaut-data-document-tck:3.3.0") - - // https://github.com/micronaut-projects/micronaut-core/pull/7631#issuecomment-1174702395 - bomAuthorizedGroupIds.put( - "io.opentelemetry.instrumentation:opentelemetry-instrumentation-bom-alpha", - ["io.opentelemetry.javaagent", "io.opentelemetry", "io.opentelemetry.instrumentation", "io.opentelemetry.javaagent.instrumentation"] as Set - ) - dependencies.add("io.opentelemetry:opentelemetry-bom:1.15.0") - dependencies.add("io.opentelemetry:opentelemetry-bom-alpha:1.15.0-alpha") - - // The R2DBC bom that we include mentions dependencies which do not belong to io.r2dbc group - bomAuthorizedGroupIds.put("io.r2dbc:r2dbc-bom", ["com.google.cloud", "com.oracle.database.r2dbc", "org.mariadb", "dev.miku"] as Set) - - // No GORM until it supports Groovy 4 - acceptedLibraryRegressions.add("micronaut-multitenancy-gorm") - acceptedLibraryRegressions.add("micronaut-mongo-gorm") - acceptedLibraryRegressions.add("micronaut-gorm-common") - acceptedLibraryRegressions.add("micronaut-hibernate-gorm") - acceptedLibraryRegressions.add("micronaut-graphql-gorm") - acceptedLibraryRegressions.add("micronaut-neo4j-gorm") - - // Switched to Jakarta-mail in email 2.0.0 - acceptedLibraryRegressions.add("javax-mail") - - // glassfish removed from Micronaut Serialization - acceptedVersionRegressions.add("glassfish-jakarta-json") - acceptedLibraryRegressions.add("glassfish-jakarta-json") - - } -} diff --git a/buffer-netty/build.gradle b/buffer-netty/build.gradle index ca5355330f2..e7220f6a22d 100644 --- a/buffer-netty/build.gradle +++ b/buffer-netty/build.gradle @@ -6,7 +6,7 @@ dependencies { api project(":core") api project(":inject") api libs.managed.netty.buffer - compileOnly libs.managed.graal + compileOnly libs.graal annotationProcessor project(":inject-java") } diff --git a/buildSrc/src/main/groovy/io.micronaut.build.internal.convention-base.gradle b/buildSrc/src/main/groovy/io.micronaut.build.internal.convention-base.gradle index 57170d7f9fe..6e1870c6ba1 100644 --- a/buildSrc/src/main/groovy/io.micronaut.build.internal.convention-base.gradle +++ b/buildSrc/src/main/groovy/io.micronaut.build.internal.convention-base.gradle @@ -73,7 +73,7 @@ dependencies { testImplementation libs.caffeine testImplementation libs.managed.groovy - testImplementation(libs.managed.spock) { + testImplementation(libs.spock) { exclude module: 'groovy-all' } diff --git a/buildSrc/src/main/groovy/io/micronaut/build/internal/ext/DefaultMicronautCoreExtension.java b/buildSrc/src/main/groovy/io/micronaut/build/internal/ext/DefaultMicronautCoreExtension.java index bd5f9f28916..5c1b10edba2 100644 --- a/buildSrc/src/main/groovy/io/micronaut/build/internal/ext/DefaultMicronautCoreExtension.java +++ b/buildSrc/src/main/groovy/io/micronaut/build/internal/ext/DefaultMicronautCoreExtension.java @@ -62,7 +62,7 @@ public void usesMicronautTestKotest() { private void addTestImplementationDependency(String lib) { dependencyHandler.addProvider("testImplementation", libs.findLibrary( - "managed.micronaut.test." + lib + "micronaut.test." + lib ).get(), DefaultMicronautCoreExtension::excludeMicronautLibs); } diff --git a/context/build.gradle b/context/build.gradle index 03cf4e0d427..9c0a3ec09ac 100644 --- a/context/build.gradle +++ b/context/build.gradle @@ -12,7 +12,7 @@ dependencies { compileOnly project(':core-reactive') compileOnly project(':core-processor') compileOnly libs.log4j - compileOnly libs.managed.logback + compileOnly libs.logback testCompileOnly project(":inject-groovy") testAnnotationProcessor project(":inject-java") diff --git a/bom/README.md b/core-bom/README.md similarity index 100% rename from bom/README.md rename to core-bom/README.md diff --git a/core-bom/build.gradle b/core-bom/build.gradle new file mode 100644 index 00000000000..9ad103622b5 --- /dev/null +++ b/core-bom/build.gradle @@ -0,0 +1,21 @@ +plugins { + id 'io.micronaut.build.internal.bom' +} + +group projectGroupId +version projectVersion + +micronautBom { + extraExcludedProjects = [ + "benchmarks", + "inject-test-utils" + ] + propertyName = 'core' +} + +micronautBuild { + binaryCompatibility { + def (major, minor, patch) = (version - '-SNAPSHOT').split('[.]').collect { it.toInteger() } + enabled = major > 4 || (major == 4 && minor > 0) || (major == 4 && minor == 0 && patch > 0) + } +} diff --git a/core/build.gradle b/core/build.gradle index 0585c83a44c..84f6e6288da 100644 --- a/core/build.gradle +++ b/core/build.gradle @@ -13,7 +13,7 @@ micronautBuild { dependencies { compileOnly libs.managed.jakarta.annotation.api - compileOnly libs.managed.graal + compileOnly libs.graal compileOnly libs.kotlin.stdlib } @@ -43,4 +43,3 @@ tasks.withType(JapicmpTask).configureEach { addViolationTransformer(RemovedPackages, [prefixes: ['io.micronaut.caffeine'], exact: []]) } } - diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 5c7ba4bfe7f..c321bd102e5 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -9,7 +9,11 @@ compile-testing = "0.19" geb = "3.4.1" geb-groovy = "3.0.13" geb-spock = "2.2-groovy-3.0" - +gorm = "7.3.2" +# be sure to update graal version in gradle.properties as well +# Intentionally pin to 22.0.0.2 see https://github.com/micronaut-projects/micronaut-kafka/pull/564 and https://github.com/micronaut-projects/micronaut-core/pull/7663 +graal-svm = "22.0.0.2" +h2 = "2.1.210" hibernate = "5.5.9.Final" hibernate-validator = "6.1.6.Final" htmlSanityCheck = "1.1.6" @@ -17,19 +21,40 @@ htmlunit = "2.64.0" httpcomponents-client = "4.5.13" jakarta-inject-api = "2.0.1" jakarta-inject-tck = "2.0.1" +javax-annotation-api = "1.3.2" javax-inject = "1" javax-persistence = "2.2" jetbrains-annotations = "23.0.0" jetty = "9.4.48.v20220622" jmh = "1.35" jsr107 = "1.1.1" +jsr305 = "3.0.2" javax-el = "3.0.1-b12" javax-el-impl = "2.2.1-b05" +jcache = "1.1.1" +junit5 = "5.9.1" +kotlin = "1.7.20" +kotlin-coroutines = "1.6.4" +ktor = "1.6.8" +logback = "1.2.11" logbook-netty = "2.14.0" log4j = "2.19.0" +micronaut-aws = "3.9.2" +micronaut-grpc = "3.3.1" +micronaut-groovy = "4.0.0-SNAPSHOT" +micronaut-picocli = "4.3.0" +micronaut-sql = "4.7.2" +micronaut-test = "3.7.0" +micronaut-tracing = "4.4.0" +micrometer = "1.9.5" +neo4j-java-driver = "1.4.5" selenium = "3.141.59" smallrye = "5.5.0" +snakeyaml = "1.33" +spock = "2.2-groovy-4.0" +spotbugs = "4.7.1" systemlambda = "1.2.1" +testcontainers = "1.17.5" vertx = "3.9.13" wiremock = "2.33.2" @@ -37,213 +62,44 @@ wiremock = "2.33.2" # Versions which start with managed- are managed by Micronaut in the sense # that they will appear in the Micronaut BOM as # -managed-dekorate = "1.0.3" -managed-elasticsearch = "7.16.3" -managed-ignite = "2.13.0" -managed-junit5 = "5.9.1" -managed-kotlin = "1.7.20" -managed-kotlin-coroutines = "1.6.4" -managed-google-function-framework = "1.0.4" -managed-google-function-invoker = "1.0.0" -managed-gorm = "7.3.2" -managed-gorm-hibernate = "7.3.0" -# be sure to update graal version in gradle.properties as well -# Intentionally pin to 22.0.0.2 see https://github.com/micronaut-projects/micronaut-kafka/pull/564 and https://github.com/micronaut-projects/micronaut-core/pull/7663 -managed-graal-sdk = "22.0.0.2" -managed-graal = "22.2.0" -managed-graal-svm = "22.0.0.2" managed-groovy = "4.0.6" -managed-h2 = "2.1.210" -managed-hystrix = "1.5.18" managed-jakarta-annotation-api = "2.1.1" managed-jackson = "2.13.4" managed-jackson-databind = "2.13.4.2" -managed-javax-annotation-api = "1.3.2" -managed-jcache = "1.1.1" -managed-jna = "5.12.1" -managed-jsr305 = "3.0.2" -managed-kafka = "2.8.2" -managed-ktor = "1.6.8" -managed-logback = "1.2.11" -managed-lombok = "1.18.24" managed-maven-native-plugin = "0.9.13" managed-methvin-directory-watcher = "0.16.1" -managed-micrometer = "1.9.5" -managed-micronaut-acme = "3.2.0" -managed-micronaut-aot = "2.0.0-SNAPSHOT" -managed-micronaut-aws = "3.9.2" -managed-micronaut-azure = "3.5.0" -managed-micronaut-cache = "4.0.0-SNAPSHOT" -managed-micronaut-cassandra = "5.1.1" -managed-micronaut-coherence = "3.7.1" -managed-micronaut-crac = "1.0.1" -managed-micronaut-data = "3.8.1" -managed-micronaut-discovery = "3.2.0" -managed-micronaut-elasticsearch = "4.3.0" -managed-micronaut-email = "2.0.0-SNAPSHOT" -managed-micronaut-flyway = "5.4.1" -managed-micronaut-gcp = "4.6.0" -managed-micronaut-graphql = "3.2.0" -managed-micronaut-groovy = "4.0.0-SNAPSHOT" -managed-micronaut-grpc = "3.3.1" -managed-micronaut-hibernate-validator = "4.0.0-SNAPSHOT" -managed-micronaut-ignite = "1.0.0.RC1" -managed-micronaut-jaxrs = "3.4.0" -managed-micronaut-jms = "2.1.0" -managed-micronaut-jmx = "3.1.0" -managed-micronaut-kafka = "4.4.1" -managed-micronaut-kotlin = "3.2.2" -managed-micronaut-kubernetes = "3.4.0" -managed-micronaut-micrometer = "5.0.0-SNAPSHOT" -managed-micronaut-microstream = "2.0.0-SNAPSHOT" -managed-micronaut-liquibase = "5.4.1" -managed-micronaut-mongo = "4.6.0" -managed-micronaut-mqtt = "2.3.0" -managed-micronaut-multitenancy = "4.2.0" -managed-micronaut-neo4j = "5.2.0" -managed-micronaut-nats = "3.1.0" -managed-micronaut-netflix = "2.1.0" -managed-micronaut-object-storage = "1.1.0" -managed-micronaut-openapi = "4.5.2" -managed-micronaut-oraclecloud = "2.2.1" -managed-micronaut-picocli = "5.0.0-SNAPSHOT" -managed-micronaut-problem = "2.5.1" -managed-micronaut-rabbitmq = "3.3.0" -managed-micronaut-r2dbc = "4.0.0" -managed-micronaut-reactor = "3.0.0-SNAPSHOT" -managed-micronaut-redis = "5.3.1" -managed-micronaut-rss = "3.2.0" -managed-micronaut-rxjava1 = "1.0.0" -managed-micronaut-rxjava2 = "2.0.0-SNAPSHOT" -managed-micronaut-rxjava3 = "3.0.0-SNAPSHOT" -managed-micronaut-security = "4.0.0-SNAPSHOT" -managed-micronaut-serialization = "2.0.0-SNAPSHOT" -managed-micronaut-servlet = "3.3.2" -managed-micronaut-spring = "4.3.1" -managed-micronaut-sql = "4.7.2" -managed-micronaut-test = "3.7.0" -managed-micronaut-test-resources = "1.1.3" -managed-micronaut-toml = "2.0.0-SNAPSHOT" -managed-micronaut-tracing = "4.4.0" -managed-micronaut-tracing-legacy = "3.2.7" -managed-micronaut-views = "3.7.1" -managed-micronaut-xml = "4.0.0-SNAPSHOT" -managed-neo4j = "3.5.35" -managed-neo4j-java-driver = "4.4.9" + managed-netty = "4.1.84.Final" -managed-reactive-pg-client = "0.11.4" managed-reactive-streams = "1.0.4" # This should be kept aligned with https://github.com/micronaut-projects/micronaut-reactor/blob/master/gradle.properties from the BOM managed-reactor = "3.4.24" -managed-rxjava1 = "1.3.8" -managed-rxjava1-interop = "0.13.7" managed-slf4j = "1.7.36" -managed-spock = "2.2-groovy-4.0" -managed-spotbugs = "4.7.1" -managed-spring = "5.3.23" -managed-springboot = "2.7.0" -managed-swagger = "2.2.4" managed-validation = "2.0.1.Final" -managed-testcontainers = "1.17.5" -managed-snakeyaml = "1.33" micronaut-docs = "2.0.0" [libraries] # Libraries prefixed with bom- are BOM files -boms-micronaut-aws = { module = "io.micronaut.aws:micronaut-aws-bom", version.ref = "managed-micronaut-aws" } -boms-micronaut-azure = { module = "io.micronaut.azure:micronaut-azure-bom", version.ref = "managed-micronaut-azure" } -boms-micronaut-cache = { module = "io.micronaut.cache:micronaut-cache-bom", version.ref = "managed-micronaut-cache" } -boms-micronaut-coherence = { module = "io.micronaut.coherence:micronaut-coherence-bom", version.ref = "managed-micronaut-coherence" } -boms-micronaut-crac = { module = "io.micronaut.crac:micronaut-crac-bom", version.ref = "managed-micronaut-crac" } -boms-micronaut-email = { module = "io.micronaut.email:micronaut-email-bom", version.ref = "managed-micronaut-email" } -boms-micronaut-data = { module = "io.micronaut.data:micronaut-data-bom", version.ref = "managed-micronaut-data" } -boms-micronaut-gcp = { module = "io.micronaut.gcp:micronaut-gcp-bom", version.ref = "managed-micronaut-gcp" } -boms-micronaut-grpc = { module = "io.micronaut.grpc:micronaut-grpc-bom", version.ref = "managed-micronaut-grpc" } -boms-micronaut-groovy = { module = "io.micronaut.groovy:micronaut-groovy-bom", version.ref = "managed-micronaut-groovy" } -boms-micronaut-jaxrs = { module = "io.micronaut.jaxrs:micronaut-jaxrs-bom", version.ref = "managed-micronaut-jaxrs" } -boms-micronaut-kafka = { module = "io.micronaut.kafka:micronaut-kafka-bom", version.ref = "managed-micronaut-kafka" } -boms-micronaut-kotlin = { module = "io.micronaut.kotlin:micronaut-kotlin-bom", version.ref = "managed-micronaut-kotlin" } -boms-micronaut-kubernetes = { module = "io.micronaut.kubernetes:micronaut-kubernetes-bom", version.ref = "managed-micronaut-kubernetes" } -boms-micronaut-liquibase = { module = "io.micronaut.liquibase:micronaut-liquibase-bom", version.ref = "managed-micronaut-liquibase" } -boms-micronaut-micrometer = { module = "io.micronaut.micrometer:micronaut-micrometer-bom", version.ref = "managed-micronaut-micrometer" } -boms-micronaut-microstream = { module = "io.micronaut.microstream:micronaut-microstream-bom", version.ref = "managed-micronaut-microstream" } -boms-micronaut-mongo = { module = "io.micronaut.mongodb:micronaut-mongo-bom", version.ref = "managed-micronaut-mongo" } -boms-micronaut-mqtt = { module = "io.micronaut.mqtt:micronaut-mqtt-bom", version.ref = "managed-micronaut-mqtt" } -boms-micronaut-object-storage = { module = "io.micronaut.objectstorage:micronaut-object-storage-bom", version.ref = "managed-micronaut-object-storage" } -boms-micronaut-oraclecloud = { module = "io.micronaut.oraclecloud:micronaut-oraclecloud-bom", version.ref = "managed-micronaut-oraclecloud" } -boms-micronaut-openapi = { module = "io.micronaut.openapi:micronaut-openapi-bom", version.ref = "managed-micronaut-openapi" } -boms-micronaut-picocli = { module = "io.micronaut.picocli:micronaut-picocli-bom", version.ref = "managed-micronaut-picocli" } -boms-micronaut-problem-json = { module = "io.micronaut.problem:micronaut-problem-json-bom", version.ref = "managed-micronaut-problem" } -boms-micronaut-redis = { module = "io.micronaut.redis:micronaut-redis-bom", version.ref = "managed-micronaut-redis" } -boms-micronaut-rxjava2 = { module = "io.micronaut.rxjava2:micronaut-rxjava2-bom", version.ref = "managed-micronaut-rxjava2" } -boms-micronaut-rxjava3 = { module = "io.micronaut.rxjava3:micronaut-rxjava3-bom", version.ref = "managed-micronaut-rxjava3" } -boms-micronaut-reactor = { module = "io.micronaut.reactor:micronaut-reactor-bom", version.ref = "managed-micronaut-reactor" } -boms-micronaut-security = { module = "io.micronaut.security:micronaut-security-bom", version.ref = "managed-micronaut-security" } -boms-micronaut-serialization = { module = "io.micronaut.serde:micronaut-serde-bom", version.ref = "managed-micronaut-serialization" } -boms-micronaut-servlet = { module = "io.micronaut.servlet:micronaut-servlet-bom", version.ref = "managed-micronaut-servlet" } -boms-micronaut-spring = { module = "io.micronaut.spring:micronaut-spring-bom", version.ref = "managed-micronaut-spring" } -boms-micronaut-sql = { module = "io.micronaut.sql:micronaut-sql-bom", version.ref = "managed-micronaut-sql" } -boms-micronaut-test = { module = "io.micronaut.test:micronaut-test-bom", version.ref = "managed-micronaut-test" } -boms-micronaut-tracing = { module = "io.micronaut.tracing:micronaut-tracing-bom", version.ref = "managed-micronaut-tracing" } -boms-micronaut-views = { module = "io.micronaut.views:micronaut-views-bom", version.ref = "managed-micronaut-views" } -boms-micronaut-r2dbc = { module = "io.micronaut.r2dbc:micronaut-r2dbc-bom", version.ref = "managed-micronaut-r2dbc" } -boms-micronaut-flyway = { module = "io.micronaut.flyway:micronaut-flyway-bom", version.ref = "managed-micronaut-flyway" } -boms-micronaut-test-resources = { module = "io.micronaut.testresources:micronaut-test-resources-bom", version.ref = "managed-micronaut-test-resources" } +internal-boms-micronaut-grpc = { module = "io.micronaut.grpc:micronaut-grpc-bom", version.ref = "micronaut-grpc" } +internal-boms-micronaut-picocli = { module = "io.micronaut.picocli:micronaut-picocli-bom", version.ref = "micronaut-picocli" } + +test-boms-micronaut-aws = { module = "io.micronaut.aws:micronaut-aws-bom", version.ref = "micronaut-aws" } +test-boms-micronaut-sql = { module = "io.micronaut.sql:micronaut-sql-bom", version.ref = "micronaut-sql" } +test-boms-micronaut-tracing = { module = "io.micronaut.tracing:micronaut-tracing-bom", version.ref = "micronaut-tracing" } boms-groovy = { module = "org.apache.groovy:groovy-bom", version.ref = "managed-groovy" } -boms-jackson = { module = "com.fasterxml.jackson:jackson-bom", version.ref = "managed-jackson" } -boms-junit5 = { module = "org.junit:junit-bom", version.ref = "managed-junit5" } -boms-kotlin = { module = "org.jetbrains.kotlin:kotlin-bom", version.ref = "managed-kotlin" } -boms-kotlin-coroutines = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-bom", version.ref = "managed-kotlin-coroutines" } -boms-ktor = { module = "io.ktor:ktor-bom", version.ref = "managed-ktor" } -boms-micrometer = { module = "io.micrometer:micrometer-bom", version.ref = "managed-micrometer" } boms-netty = { module = "io.netty:netty-bom", version.ref = "managed-netty" } -boms-testcontainers = { module = "org.testcontainers:testcontainers-bom", version.ref = "managed-testcontainers" } +boms-jackson = { module = "com.fasterxml.jackson:jackson-bom", version.ref = "managed-jackson" } # # Libraries which start with managed- are managed by Micronaut in the sense # that they will appear in the Micronaut BOM # -managed-dekorate = { module = "io.dekorate:dekorate-project", version.ref = "managed-dekorate" } -managed-dekorate-jaeger-annotations = { module = "io.dekorate:jaeger-annotations", version.ref = "managed-dekorate" } -managed-dekorate-knative-annotations = { module = "io.dekorate:knative-annotations", version.ref = "managed-dekorate" } -managed-dekorate-kubernetes-annotations = { module = "io.dekorate:kubernetes-annotations", version.ref = "managed-dekorate" } -managed-dekorate-openshift-annotations = { module = "io.dekorate:openshift-annotations", version.ref = "managed-dekorate" } -managed-dekorate-prometheus-annotations = { module = "io.dekorate:prometheus-annotations", version.ref = "managed-dekorate" } -managed-dekorate-servicecatalog-annotations = { module = "io.dekorate:servicecatalog-annotations", version.ref = "managed-dekorate" } -managed-dekorate-halkyon-annotations = { module = "io.dekorate:halkyon-annotations", version.ref = "managed-dekorate" } - -managed-elasticsearch = { module = "org.elasticsearch.client:elasticsearch-rest-high-level-client", version.ref = "managed-elasticsearch" } - -managed-google-function-framework = { module = "com.google.cloud.functions:functions-framework-api", version.ref = "managed-google-function-framework" } -managed-google-function-invoker = { module = "com.google.cloud.functions.invoker:java-function-invoker", version.ref = "managed-google-function-invoker" } - -managed-gorm = { module = "org.grails:grails-datastore-core", version.ref = "managed-gorm" } -managed-gorm-datastore-async = { module = "org.grails:grails-datastore-async", version.ref = "managed-gorm" } -managed-gorm-datastore-gorm = { module = "org.grails:grails-datastore-gorm", version.ref = "managed-gorm" } -managed-gorm-datastore-gorm-async = { module = "org.grails:grails-datastore-gorm-async", version.ref = "managed-gorm" } -managed-gorm-datastore-gorm-support = { module = "org.grails:grails-datastore-gorm-support", version.ref = "managed-gorm" } -managed-gorm-datastore-gorm-test = { module = "org.grails:grails-datastore-gorm-test", version.ref = "managed-gorm" } -managed-gorm-datastore-gorm-validation = { module = "org.grails:grails-datastore-gorm-validation", version.ref = "managed-gorm" } -managed-gorm-datastore-web = { module = "org.grails:grails-datastore-web", version.ref = "managed-gorm" } -managed-gorm-hibernate = { module = "org.grails:grails-datastore-gorm-hibernate5", version.ref = "managed-gorm-hibernate" } - -managed-graal = { module = "org.graalvm.nativeimage:svm", version.ref = "managed-graal-svm" } -managed-graal-sdk = { module = "org.graalvm.sdk:graal-sdk", version.ref = "managed-graal-sdk" } managed-groovy = { module = "org.apache.groovy:groovy", version.ref = "managed-groovy" } managed-groovy-json = { module = "org.apache.groovy:groovy-json", version.ref = "managed-groovy" } managed-groovy-sql = { module = "org.apache.groovy:groovy-sql", version.ref = "managed-groovy" } managed-groovy-templates = { module = "org.apache.groovy:groovy-templates", version.ref = "managed-groovy" } -managed-h2 = { module = "com.h2database:h2", version.ref = "managed-h2" } - -managed-hystrix = { module = "com.netflix.hystrix:hystrix-core", version.ref = "managed-hystrix" } -managed-hystrix-serialization = { module = "com.netflix.hystrix:hystrix-serialization", version.ref = "managed-hystrix" } - -managed-ignite = { module = "org.apache.ignite:ignite-core", version.ref = "managed-ignite" } -managed-ignite-kubernetes = { module = "org.apache.ignite:ignite-kubernetes", version.ref = "managed-ignite" } - managed-jakarta-annotation-api = { module = "jakarta.annotation:jakarta.annotation-api", version.ref = "managed-jakarta-annotation-api" } managed-jackson-annotations = { module = "com.fasterxml.jackson.core:jackson-annotations", version.ref = "managed-jackson" } @@ -255,27 +111,8 @@ managed-jackson-module-afterburner = { module = "com.fasterxml.jackson.module:ja managed-jackson-module-kotlin = { module = "com.fasterxml.jackson.module:jackson-module-kotlin", version.ref = "managed-jackson" } managed-jackson-module-parameterNames = { module = "com.fasterxml.jackson.module:jackson-module-parameter-names", version.ref = "managed-jackson" } -managed-javax-annotation-api = { module = "javax.annotation:javax.annotation-api", version.ref = "managed-javax-annotation-api" } - -managed-jcache = { module = "javax.cache:cache-api", version.ref = "managed-jcache" } - -managed-jna = { module = "net.java.dev.jna:jna", version.ref = "managed-jna" } - -managed-jsr305 = { module = "com.google.code.findbugs:jsr305", version.ref = "managed-jsr305" } - -managed-kafka212 = { module = "org.apache.kafka:kafka_2.12", version.ref = "managed-kafka" } - -managed-logback = { module = "ch.qos.logback:logback-classic", version.ref = "managed-logback" } - -managed-lombok = { module = "org.projectlombok:lombok", version.ref = "managed-lombok" } - managed-methvin-directoryWatcher = { module = "io.methvin:directory-watcher", version.ref = "managed-methvin-directory-watcher" } -managed-micrometer-core = { module = "io.micrometer:micrometer-core", version.ref = "managed-micrometer" } - -managed-neo4j = { module = "org.neo4j.test:neo4j-harness", version.ref = "managed-neo4j" } -managed-neo4j-bolt = { module = "org.neo4j.driver:neo4j-java-driver", version.ref = "managed-neo4j-java-driver" } - managed-netty-buffer = { module = "io.netty:netty-buffer", version.ref = "managed-netty" } managed-netty-codec-http = { module = "io.netty:netty-codec-http", version.ref = "managed-netty" } managed-netty-codec-http2 = { module = "io.netty:netty-codec-http2", version.ref = "managed-netty" } @@ -285,68 +122,13 @@ managed-netty-transport-native-epoll = { module = "io.netty:netty-transport-nati managed-netty-transport-native-kqueue = { module = "io.netty:netty-transport-native-kqueue", version.ref = "managed-netty" } managed-netty-transport-native-unix-common = { module = "io.netty:netty-transport-native-unix-common", version.ref = "managed-netty" } -managed-micronaut-acme = { module = "io.micronaut.acme:micronaut-acme", version.ref = "managed-micronaut-acme" } -managed-micronaut-cassandra = { module = "io.micronaut.cassandra:micronaut-cassandra", version.ref = "managed-micronaut-cassandra" } -managed-micronaut-discovery = { module = "io.micronaut.discovery:micronaut-discovery-client", version.ref = "managed-micronaut-discovery" } -managed-micronaut-elasticsearch = { module = "io.micronaut.elasticsearch:micronaut-elasticsearch", version.ref = "managed-micronaut-elasticsearch" } -managed-micronaut-graphql = { module = "io.micronaut.graphql:micronaut-graphql", version.ref = "managed-micronaut-graphql" } -managed-micronaut-hibernate-validator = { module = "io.micronaut.beanvalidation:micronaut-hibernate-validator", version.ref = "managed-micronaut-hibernate-validator" } -managed-micronaut-ignite-core = { module = "io.micronaut.ignite:micronaut-ignite-core", version.ref = "managed-micronaut-ignite" } -managed-micronaut-ignite-cache = { module = "io.micronaut.ignite:micronaut-ignite-cache", version.ref = "managed-micronaut-ignite" } -managed-micronaut-jms = { module = "io.micronaut.jms:micronaut-jms-core", version.ref = "managed-micronaut-jms" } -managed-micronaut-jms-activemq-classic = { module = "io.micronaut.jms:micronaut-jms-activemq-classic", version.ref = "managed-micronaut-jms" } -managed-micronaut-jms-activemq-artemis = { module = "io.micronaut.jms:micronaut-jms-activemq-artemis", version.ref = "managed-micronaut-jms" } -managed-micronaut-jms-sqs = { module = "io.micronaut.jms:micronaut-jms-sqs", version.ref = "managed-micronaut-jms" } -managed-micronaut-jmx = { module = "io.micronaut.jmx:micronaut-jmx", version.ref = "managed-micronaut-jmx" } -managed-micronaut-multitenancy = { module = "io.micronaut.multitenancy:micronaut-multitenancy", version.ref = "managed-micronaut-multitenancy" } -managed-micronaut-nats = { module = "io.micronaut.nats:micronaut-nats", version.ref = "managed-micronaut-nats" } -managed-micronaut-neo4j = { module = "io.micronaut.neo4j:micronaut-neo4j-bolt", version.ref = "managed-micronaut-neo4j" } -managed-micronaut-netflix = { module = "io.micronaut.netflix:micronaut-netflix-archaius", version.ref = "managed-micronaut-netflix" } -managed-micronaut-netflix-hystrix = { module = "io.micronaut.netflix:micronaut-netflix-hystrix", version.ref = "managed-micronaut-netflix" } -managed-micronaut-netflix-ribbon = { module = "io.micronaut.netflix:micronaut-netflix-ribbon", version.ref = "managed-micronaut-netflix" } -managed-micronaut-openapi = { module = "io.micronaut.openapi:micronaut-openapi", version.ref = "managed-micronaut-openapi" } -managed-micronaut-rabbitmq = { module = "io.micronaut.rabbitmq:micronaut-rabbitmq", version.ref = "managed-micronaut-rabbitmq" } -managed-micronaut-rss = { module = "io.micronaut.rss:micronaut-rss", version.ref = "managed-micronaut-rss" } -managed-micronaut-rss-core = { module = "io.micronaut.rss:micronaut-rss-core", version.ref = "managed-micronaut-rss" } -managed-micronaut-rss-language = { module = "io.micronaut.rss:micronaut-rss-language", version.ref = "managed-micronaut-rss" } -managed-micronaut-rss-itunespodcast = { module = "io.micronaut.rss:micronaut-itunespodcast", version.ref = "managed-micronaut-rss" } -managed-micronaut-rss-jsonfeed-core = { module = "io.micronaut.rss:micronaut-jsonfeed-core", version.ref = "managed-micronaut-rss" } -managed-micronaut-rss-jsonfeed = { module = "io.micronaut.rss:micronaut-jsonfeed", version.ref = "managed-micronaut-rss" } -managed-micronaut-rxjava1 = { module = "io.micronaut.rxjava1:micronaut-rxjava1", version.ref = "managed-micronaut-rxjava1" } -managed-micronaut-sql-jdbc = { module = "io.micronaut.sql:micronaut-jdbc", version.ref = "managed-micronaut-sql" } -managed-micronaut-sql-jdbc-tomcat = { module = "io.micronaut.sql:micronaut-jdbc-tomcat", version.ref = "managed-micronaut-sql" } -managed-micronaut-test-bom = { module = "io.micronaut.test:micronaut-test-bom", version.ref = "managed-micronaut-test" } -managed-micronaut-test-core = { module = "io.micronaut.test:micronaut-test-core", version.ref = "managed-micronaut-test" } -managed-micronaut-test-junit5 = { module = "io.micronaut.test:micronaut-test-junit5", version.ref = "managed-micronaut-test" } -managed-micronaut-test-kotest = { module = "io.micronaut.test:micronaut-test-kotest", version.ref = "managed-micronaut-test" } -managed-micronaut-test-kotest5 = { module = "io.micronaut.test:micronaut-test-kotest5", version.ref = "managed-micronaut-test" } -managed-micronaut-test-spock = { module = "io.micronaut.test:micronaut-test-spock", version.ref = "managed-micronaut-test" } -managed-micronaut-toml = { module = "io.micronaut.toml:micronaut-toml", version.ref = "managed-micronaut-toml" } -managed-micronaut-tracing-legacy = { module = "io.micronaut:micronaut-tracing", version.ref = "managed-micronaut-tracing-legacy" } -managed-micronaut-xml = { module = "io.micronaut.xml:micronaut-jackson-xml", version.ref = "managed-micronaut-xml" } - -managed-reactive-pg-client = { module = "io.reactiverse:reactive-pg-client", version.ref = "managed-reactive-pg-client" } managed-reactive-streams = { module = "org.reactivestreams:reactive-streams", version.ref = "managed-reactive-streams" } managed-reactor = { module = "io.projectreactor:reactor-core", version.ref = "managed-reactor" } -managed-rxjava1 = { module = "io.reactivex:rxjava", version.ref = "managed-rxjava1" } -managed-rxjava1-interop = { module = "com.github.akarnokd:rxjava2-interop", version.ref = "managed-rxjava1-interop" } - managed-slf4j = { module = "org.slf4j:slf4j-api", version.ref = "managed-slf4j" } managed-slf4j-simple = { module = "org.slf4j:slf4j-simple", version.ref = "managed-slf4j" } -managed-snakeyaml = { module = "org.yaml:snakeyaml", version.ref = "managed-snakeyaml" } - -managed-spock = { module = "org.spockframework:spock-core", version.ref = "managed-spock" } -managed-spotbugs = { module = "com.github.spotbugs:spotbugs-annotations", version.ref = "managed-spotbugs" } - -managed-spring = { module = "org.springframework:spring-core", version.ref = "managed-spring" } - -managed-swagger = { module = "io.swagger.core.v3:swagger-core", version.ref = "managed-swagger" } -managed-swagger-models = { module = "io.swagger.core.v3:swagger-models", version.ref = "managed-swagger" } -managed-swagger-annotations = { module = "io.swagger.core.v3:swagger-annotations", version.ref = "managed-swagger" } - managed-validation = { module = "javax.validation:validation-api", version.ref = "managed-validation" } # @@ -371,9 +153,11 @@ compile-testing = { module = "com.google.testing.compile:compile-testing", versi geb-spock = { module = "org.gebish:geb-spock", version.ref = "geb" } spock-for-geb = { module = "org.spockframework:spock-core", version.ref = "geb-spock" } geb-groovy-test = { module = "org.codehaus.groovy:groovy-test", version.ref = "geb-groovy" } - +gorm = { module = "org.grails:grails-datastore-core", version.ref = "gorm" } +graal = { module = "org.graalvm.nativeimage:svm", version.ref = "graal-svm" } groovy-test-junit5 = { module = "org.apache.groovy:groovy-test-junit5", version.ref = "managed-groovy" } +h2 = { module = "com.h2database:h2", version.ref = "h2" } hibernate = { module = "org.hibernate:hibernate-core", version.ref = "hibernate" } hibernate-validator = { module = "org.hibernate:hibernate-validator", version.ref = "hibernate-validator" } @@ -382,48 +166,67 @@ htmlunit = { module = "net.sourceforge.htmlunit:htmlunit", version.ref = "htmlun jakarta-inject-api = { module = "jakarta.inject:jakarta.inject-api", version.ref = "jakarta-inject-api" } jakarta-inject-tck = { module = "jakarta.inject:jakarta.inject-tck", version.ref = "jakarta-inject-tck" } +javax-annotation-api = { module = "javax.annotation:javax.annotation-api", version.ref = "javax-annotation-api" } javax-el = { module = "org.glassfish:javax.el", version.ref = "javax-el" } javax-el-impl = { module = "org.glassfish:javax.el", version.ref = "javax-el" } javax-inject = { module = "javax.inject:javax.inject", version.ref = "javax-inject" } javax-persistence = { module = "javax.persistence:javax.persistence-api", version.ref = "javax-persistence" } +jcache = { module = "javax.cache:cache-api", version.ref = "jcache" } + jetty-alpn-openjdk8-client = { module = "org.eclipse.jetty:jetty-alpn-openjdk8-client", version.ref = "jetty" } jmh = { module = "org.openjdk.jmh:jmh-core", version.ref = "jmh" } jmh-generator-annprocess = { module = "org.openjdk.jmh:jmh-core", version.ref = "jmh" } jsr107 = { module = "org.jsr107.ri:cache-ri-impl", version.ref = "jsr107" } +jsr305 = { module = "com.google.code.findbugs:jsr305", version.ref = "jsr305" } -junit-jupiter-api = { module = "org.junit.jupiter:junit-jupiter-api", version.ref = "managed-junit5" } -junit-jupiter-engine = { module = "org.junit.jupiter:junit-jupiter-engine", version.ref = "managed-junit5" } -junit-vintage = { module = "org.junit.vintage:junit-vintage-engine", version.ref = "managed-junit5" } +junit-jupiter-api = { module = "org.junit.jupiter:junit-jupiter-api", version.ref = "junit5" } +junit-jupiter-engine = { module = "org.junit.jupiter:junit-jupiter-engine", version.ref = "junit5" } +junit-vintage = { module = "org.junit.vintage:junit-vintage-engine", version.ref = "junit5" } jetbrains-annotations = { module = "org.jetbrains:annotations", version.ref = "jetbrains-annotations" } -kotlin-annotation-processing-embeddable = { module = "org.jetbrains.kotlin:kotlin-annotation-processing-embeddable", version.ref = "managed-kotlin" } -kotlin-compiler-embeddable = { module = "org.jetbrains.kotlin:kotlin-compiler-embeddable", version.ref = "managed-kotlin" } +kotlin-annotation-processing-embeddable = { module = "org.jetbrains.kotlin:kotlin-annotation-processing-embeddable", version.ref = "kotlin" } +kotlin-compiler-embeddable = { module = "org.jetbrains.kotlin:kotlin-compiler-embeddable", version.ref = "kotlin" } kotlin-kotest-junit5 = { module = "io.kotest:kotest-runner-junit5-jvm" } -kotlin-reflect = { module = "org.jetbrains.kotlin:kotlin-reflect", version.ref = "managed-kotlin" } -kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "managed-kotlin" } -kotlin-stdlib-jdk8 = { module = "org.jetbrains.kotlin:kotlin-stdlib-jdk8", version.ref = "managed-kotlin" } -kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "managed-kotlin" } - -kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "managed-kotlin-coroutines" } -kotlinx-coroutines-jdk8 = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-jdk8", version.ref = "managed-kotlin-coroutines" } -kotlinx-coroutines-reactive = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-reactive", version.ref = "managed-kotlin-coroutines" } -kotlinx-coroutines-rx2 = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-rx2", version.ref = "managed-kotlin-coroutines" } -kotlinx-coroutines-slf4j = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-slf4j", version.ref = "managed-kotlin-coroutines" } -kotlinx-coroutines-reactor = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-reactor", version.ref = "managed-kotlin-coroutines" } +kotlin-reflect = { module = "org.jetbrains.kotlin:kotlin-reflect", version.ref = "kotlin" } +kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "kotlin" } +kotlin-stdlib-jdk8 = { module = "org.jetbrains.kotlin:kotlin-stdlib-jdk8", version.ref = "kotlin" } +kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" } + +kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlin-coroutines" } +kotlinx-coroutines-jdk8 = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-jdk8", version.ref = "kotlin-coroutines" } +kotlinx-coroutines-reactive = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-reactive", version.ref = "kotlin-coroutines" } +kotlinx-coroutines-rx2 = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-rx2", version.ref = "kotlin-coroutines" } +kotlinx-coroutines-slf4j = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-slf4j", version.ref = "kotlin-coroutines" } +kotlinx-coroutines-reactor = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-reactor", version.ref = "kotlin-coroutines" } log4j = { module = "org.apache.logging.log4j:log4j-core", version.ref = "log4j" } +logback = { module = "ch.qos.logback:logback-classic", version.ref = "logback" } + logbook-netty = { module = "org.zalando:logbook-netty", version.ref = "logbook-netty" } micronaut-docs = { module = "io.micronaut.docs:micronaut-docs-asciidoc-config-props", version.ref = "micronaut-docs" } -micronaut-runtime-groovy = { module = "io.micronaut.groovy:micronaut-runtime-groovy", version.ref = "managed-micronaut-groovy" } +micronaut-runtime-groovy = { module = "io.micronaut.groovy:micronaut-runtime-groovy", version.ref = "micronaut-groovy" } +micronaut-test-bom = { module = "io.micronaut.test:micronaut-test-bom", version.ref = "micronaut-test" } +micronaut-test-core = { module = "io.micronaut.test:micronaut-test-core", version.ref = "micronaut-test" } +micronaut-test-junit5 = { module = "io.micronaut.test:micronaut-test-junit5", version.ref = "micronaut-test" } +micronaut-test-kotest = { module = "io.micronaut.test:micronaut-test-kotest", version.ref = "micronaut-test" } +micronaut-test-kotest5 = { module = "io.micronaut.test:micronaut-test-kotest5", version.ref = "micronaut-test" } +micronaut-test-spock = { module = "io.micronaut.test:micronaut-test-spock", version.ref = "micronaut-test" } + +micronaut-sql-jdbc = { module = "io.micronaut.sql:micronaut-jdbc", version.ref = "micronaut-sql" } +micronaut-sql-jdbc-tomcat = { module = "io.micronaut.sql:micronaut-jdbc-tomcat", version.ref = "micronaut-sql" } + +micrometer-core = { module = "io.micrometer:micrometer-core", version.ref = "micrometer" } mysql-driver = { module = "mysql:mysql-connector-java" } +neo4j-bolt = { module = "org.neo4j.driver:neo4j-java-driver", version.ref = "neo4j-java-driver" } + netty-tcnative = { module = 'io.netty:netty-tcnative' } netty-tcnative-boringssl = { module = 'io.netty:netty-tcnative-boringssl-static' } @@ -435,13 +238,16 @@ selenium-driver-firefox = { module = "org.seleniumhq.selenium:selenium-firefox-d selenium-driver-htmlunit = { module = "org.seleniumhq.selenium:htmlunit-driver", version.ref = "htmlunit" } smallrye = { module = "io.smallrye:smallrye-fault-tolerance", version.ref = "smallrye" } +snakeyaml = { module = "org.yaml:snakeyaml", version.ref = "snakeyaml" } +spock = { module = "org.spockframework:spock-core", version.ref = "spock" } +spotbugs = { module = "com.github.spotbugs:spotbugs-annotations", version.ref = "spotbugs" } systemlambda = { module = "com.github.stefanbirkner:system-lambda", version.ref = "systemlambda" } micronaut-tracing-jaeger = { module = "io.micronaut.tracing:micronaut-tracing-jaeger" } micronaut-tracing-zipkin = { module = "io.micronaut.tracing:micronaut-tracing-zipkin" } -testcontainers-spock = { module = "org.testcontainers:spock", version.ref = "managed-testcontainers" } +testcontainers-spock = { module = "org.testcontainers:spock", version.ref = "testcontainers" } vertx = { module = "io.vertx:vertx-core", version.ref = "vertx" } vertx-webclient = { module = "io.vertx:vertx-web-client", version.ref = "vertx" } diff --git a/http-client/build.gradle b/http-client/build.gradle index d5bcda0523d..fef1e30db67 100644 --- a/http-client/build.gradle +++ b/http-client/build.gradle @@ -30,7 +30,7 @@ dependencies { testImplementation project(":jackson-databind") testImplementation project(":http-server-netty") testImplementation libs.wiremock - testImplementation libs.managed.logback + testImplementation libs.logback if (JavaVersion.current().isCompatibleWith(JavaVersion.VERSION_15)) { testImplementation libs.bcpkix diff --git a/http-netty/build.gradle b/http-netty/build.gradle index 4d684695ef8..d4edb612b48 100644 --- a/http-netty/build.gradle +++ b/http-netty/build.gradle @@ -5,7 +5,7 @@ plugins { dependencies { annotationProcessor project(":inject-java") annotationProcessor project(":graal") - compileOnly libs.managed.graal + compileOnly libs.graal compileOnly libs.managed.netty.transport.native.epoll compileOnly libs.managed.netty.transport.native.kqueue compileOnly project(":websocket") diff --git a/http-server-netty/build.gradle b/http-server-netty/build.gradle index d7236c5cd2e..1db2cb6489f 100644 --- a/http-server-netty/build.gradle +++ b/http-server-netty/build.gradle @@ -37,7 +37,7 @@ dependencies { testImplementation project(":inject") testImplementation project(":inject-java-test") testImplementation project(":http-client") - testImplementation libs.managed.spotbugs + testImplementation libs.spotbugs if (JavaVersion.current().isCompatibleWith(JavaVersion.VERSION_15)) { testImplementation libs.bcpkix } @@ -70,7 +70,7 @@ dependencies { classifier = Os.isArch("aarch64") ? "osx-aarch_64" : "osx-x86_64" } } - testImplementation libs.managed.logback + testImplementation libs.logback // Adding these for now since micronaut-test isnt resolving correctly ... probably need to upgrade gradle there too testImplementation libs.junit.jupiter.api diff --git a/inject-groovy-test/build.gradle b/inject-groovy-test/build.gradle index 29e331e3e82..8d61af551d1 100644 --- a/inject-groovy-test/build.gradle +++ b/inject-groovy-test/build.gradle @@ -7,7 +7,7 @@ dependencies { api project(":inject-groovy") api libs.managed.groovy - api(libs.managed.spock) { + api(libs.spock) { exclude module:'groovy-all' } api project(":context") diff --git a/inject-groovy/build.gradle b/inject-groovy/build.gradle index 586e1f24063..b02d060b897 100644 --- a/inject-groovy/build.gradle +++ b/inject-groovy/build.gradle @@ -15,7 +15,7 @@ dependencies { // testImplementation 'javax.validation:validation-api:1.1.0.Final' testImplementation project(":context") testImplementation libs.javax.inject - testImplementation libs.managed.spotbugs + testImplementation libs.spotbugs testImplementation libs.hibernate testImplementation libs.hibernate.validator testRuntimeOnly libs.javax.el.impl @@ -26,14 +26,10 @@ dependencies { testImplementation project(":inject-test-utils") testImplementation project(":inject-groovy-test") testImplementation project(":validation") - testImplementation(libs.managed.neo4j.bolt) { - version { - require "1.4.5" - } - } + testImplementation(libs.neo4j.bolt) testImplementation libs.managed.groovy.json testImplementation libs.blaze.persistence.core - testImplementation libs.managed.snakeyaml + testImplementation libs.snakeyaml testImplementation libs.managed.reactor functionalTestImplementation(testFixtures(project(":test-suite"))) diff --git a/inject-java-test/build.gradle b/inject-java-test/build.gradle index 5d3386a0a46..b016798680c 100644 --- a/inject-java-test/build.gradle +++ b/inject-java-test/build.gradle @@ -11,7 +11,7 @@ dependencies { // exclude group:'com.google.truth', module:'truth' // } api libs.managed.groovy - api(libs.managed.spock) { + api(libs.spock) { exclude module:'groovy-all' } if (!JavaVersion.current().isJava9Compatible()) { diff --git a/inject-java/build.gradle b/inject-java/build.gradle index b414d9d3fee..f0d7735903b 100644 --- a/inject-java/build.gradle +++ b/inject-java/build.gradle @@ -25,31 +25,27 @@ dependencies { testImplementation libs.managed.reactor - testImplementation libs.managed.spotbugs + testImplementation libs.spotbugs testImplementation libs.hibernate testImplementation libs.compile.testing - testImplementation(libs.managed.neo4j.bolt) { - version { - require "1.4.5" - } - } + testImplementation(libs.neo4j.bolt) testImplementation libs.managed.groovy.json if (!JavaVersion.current().isJava9Compatible()) { testImplementation files(org.gradle.internal.jvm.Jvm.current().toolsJar) } - testImplementation libs.managed.micrometer.core + testImplementation libs.micrometer.core testImplementation project(":validation") testImplementation project(":jackson-databind") testImplementation libs.junit.jupiter.api - testImplementation(platform(libs.boms.micronaut.tracing)) + testImplementation(platform(libs.test.boms.micronaut.tracing)) testImplementation(libs.micronaut.tracing.zipkin) { exclude module: 'micronaut-bom' exclude module: 'micronaut-http-client' exclude module: 'micronaut-inject' exclude module: 'micronaut-runtime' } - testImplementation libs.managed.javax.annotation.api - testImplementation libs.managed.snakeyaml + testImplementation libs.javax.annotation.api + testImplementation libs.snakeyaml testRuntimeOnly libs.javax.el.impl testRuntimeOnly libs.javax.el } diff --git a/inject-kotlin-test/build.gradle b/inject-kotlin-test/build.gradle index b462d648352..c1ba6607294 100644 --- a/inject-kotlin-test/build.gradle +++ b/inject-kotlin-test/build.gradle @@ -8,7 +8,7 @@ dependencies { api project(":inject-java") api libs.managed.groovy - api(libs.managed.spock) { + api(libs.spock) { exclude module:'groovy-all' } if (!JavaVersion.current().isJava9Compatible()) { diff --git a/inject-test-utils/build.gradle b/inject-test-utils/build.gradle index d948e076183..7988fc7b79e 100644 --- a/inject-test-utils/build.gradle +++ b/inject-test-utils/build.gradle @@ -4,7 +4,7 @@ plugins { dependencies { api libs.managed.groovy - api(libs.managed.spock) { + api(libs.spock) { exclude module:'groovy-all' } api project(":core-processor") diff --git a/inject/build.gradle b/inject/build.gradle index 1245659e695..2a4cef16bf7 100644 --- a/inject/build.gradle +++ b/inject/build.gradle @@ -16,7 +16,7 @@ dependencies { api libs.managed.jakarta.annotation.api api project(':core') - compileOnly libs.managed.snakeyaml + compileOnly libs.snakeyaml compileOnly libs.managed.groovy compileOnly libs.kotlin.stdlib.jdk8 compileOnly libs.managed.validation @@ -26,7 +26,7 @@ dependencies { testImplementation project(":inject-groovy") testImplementation project(":inject-test-utils") testImplementation libs.systemlambda - testImplementation libs.managed.snakeyaml + testImplementation libs.snakeyaml testRuntimeOnly libs.junit.jupiter.engine } diff --git a/jackson-databind/build.gradle b/jackson-databind/build.gradle index 3322e95b8db..279321c1649 100644 --- a/jackson-databind/build.gradle +++ b/jackson-databind/build.gradle @@ -8,7 +8,7 @@ dependencies { api project(":jackson-core") - compileOnly libs.managed.graal + compileOnly libs.graal api libs.managed.jackson.databind api libs.managed.jackson.datatype.jdk8 api libs.managed.jackson.datatype.jsr310 @@ -27,7 +27,7 @@ dependencies { testImplementation project(":inject-java-test") testImplementation project(":inject-groovy") testImplementation "com.fasterxml.jackson.dataformat:jackson-dataformat-xml" - testImplementation libs.managed.snakeyaml + testImplementation libs.snakeyaml if (!JavaVersion.current().isJava9Compatible()) { testImplementation files(org.gradle.internal.jvm.Jvm.current().toolsJar) } diff --git a/management/build.gradle b/management/build.gradle index d3a246d1d52..54f9e861d9d 100644 --- a/management/build.gradle +++ b/management/build.gradle @@ -9,7 +9,7 @@ dependencies { api project(":router") api project(":discovery-core") compileOnly project(":jackson-databind") - compileOnly(libs.managed.micronaut.sql.jdbc) { + compileOnly(libs.micronaut.sql.jdbc) { exclude module:'micronaut-inject' exclude module:'micronaut-bom' } @@ -20,17 +20,17 @@ dependencies { testImplementation project(":inject-groovy") testImplementation project(":http-server-netty") testImplementation project(":jackson-databind") - testImplementation(libs.managed.micronaut.sql.jdbc.tomcat) { + testImplementation(libs.micronaut.sql.jdbc.tomcat) { exclude module:'micronaut-inject' exclude module:'micronaut-bom' } testImplementation libs.managed.groovy.json - testRuntimeOnly(platform(libs.boms.micronaut.sql)) - testRuntimeOnly libs.managed.h2 + testRuntimeOnly(platform(libs.test.boms.micronaut.sql)) + testRuntimeOnly libs.h2 testRuntimeOnly libs.mysql.driver - compileOnly libs.managed.logback + compileOnly libs.logback compileOnly libs.log4j } diff --git a/parent/build.gradle b/parent/build.gradle index 0b325db055a..08bb9702a3a 100644 --- a/parent/build.gradle +++ b/parent/build.gradle @@ -12,8 +12,8 @@ repositories { // Include any of our boms that hava required properties dependencies { - bomVersions(libs.boms.micronaut.grpc) - bomVersions(libs.boms.micronaut.picocli) + bomVersions(libs.internal.boms.micronaut.grpc) + bomVersions(libs.internal.boms.micronaut.picocli) } def micronautVersionInfo = tasks.register("micronautVersionInfo", WriteMicronautVersionInfoTask) { diff --git a/runtime/build.gradle b/runtime/build.gradle index af55078730c..8c9f28814a5 100644 --- a/runtime/build.gradle +++ b/runtime/build.gradle @@ -17,18 +17,18 @@ dependencies { implementation libs.managed.reactor - compileOnly libs.managed.graal - compileOnly libs.managed.jcache + compileOnly libs.graal + compileOnly libs.jcache compileOnly libs.javax.el compileOnly libs.caffeine compileOnly libs.kotlinx.coroutines.core compileOnly libs.kotlinx.coroutines.reactive - testImplementation libs.managed.logback - testImplementation libs.managed.snakeyaml + testImplementation libs.logback + testImplementation libs.snakeyaml testAnnotationProcessor project(":inject-java") testImplementation libs.jsr107 - testImplementation libs.managed.jcache + testImplementation libs.jcache testImplementation project(":inject-java") testImplementation project(":inject-java-test") testImplementation project(":inject-groovy") diff --git a/settings.gradle b/settings.gradle index 7f828cc5bce..f60b4b1366c 100644 --- a/settings.gradle +++ b/settings.gradle @@ -20,7 +20,7 @@ plugins { rootProject.name = 'micronaut' include "aop" -include "bom" +include "core-bom" include "parent" include "buffer-netty" include "core" diff --git a/test-suite-geb/build.gradle b/test-suite-geb/build.gradle index 625c32b37a1..165fc302ade 100644 --- a/test-suite-geb/build.gradle +++ b/test-suite-geb/build.gradle @@ -17,6 +17,6 @@ dependencies { testImplementation project(':http') testImplementation project(':http-server-netty') - testRuntimeOnly libs.managed.logback + testRuntimeOnly libs.logback testImplementation project(":jackson-databind") } diff --git a/test-suite-groovy/build.gradle b/test-suite-groovy/build.gradle index 52aa5b60a62..628192caef9 100644 --- a/test-suite-groovy/build.gradle +++ b/test-suite-groovy/build.gradle @@ -20,14 +20,14 @@ dependencies { testImplementation project(":inject") testImplementation project(":management") testImplementation project(":session") - testImplementation libs.managed.jcache + testImplementation libs.jcache testImplementation libs.managed.groovy.sql testImplementation libs.managed.groovy.templates testImplementation libs.managed.groovy.json testImplementation libs.logbook.netty testImplementation project(":function-client") testImplementation project(":function-web") - testRuntimeOnly(platform(libs.boms.micronaut.aws)) + testRuntimeOnly(platform(libs.test.boms.micronaut.aws)) testRuntimeOnly libs.aws.java.sdk.lambda testRuntimeOnly libs.bcpkix diff --git a/test-suite-javax-inject/build.gradle b/test-suite-javax-inject/build.gradle index 8517f80313e..9079ffadac3 100644 --- a/test-suite-javax-inject/build.gradle +++ b/test-suite-javax-inject/build.gradle @@ -8,5 +8,5 @@ dependencies { testImplementation project(":context") testImplementation project(":inject") testImplementation libs.javax.inject - testImplementation libs.managed.javax.annotation.api + testImplementation libs.javax.annotation.api } diff --git a/test-suite-kotlin/build.gradle b/test-suite-kotlin/build.gradle index 93f81ec3c1f..9565cf014fd 100644 --- a/test-suite-kotlin/build.gradle +++ b/test-suite-kotlin/build.gradle @@ -36,7 +36,7 @@ dependencies { testImplementation project(":management") testImplementation project(':inject-java') testImplementation project(":inject") - testImplementation libs.managed.jcache + testImplementation libs.jcache testImplementation project(':validation') testImplementation project(":http-client") testImplementation project(":session") @@ -50,7 +50,7 @@ dependencies { kaptTest project(':inject-java') kaptTest project(':validation') testImplementation libs.javax.inject - testImplementation(platform(libs.boms.micronaut.tracing)) + testImplementation(platform(libs.test.boms.micronaut.tracing)) testImplementation(libs.micronaut.tracing.zipkin) { exclude module: 'micronaut-bom' exclude module: 'micronaut-http-client' @@ -59,7 +59,7 @@ dependencies { } testRuntimeOnly libs.junit.jupiter.engine - testRuntimeOnly(platform(libs.boms.micronaut.aws)) + testRuntimeOnly(platform(libs.test.boms.micronaut.aws)) testRuntimeOnly libs.aws.java.sdk.lambda if (JavaVersion.current().isCompatibleWith(JavaVersion.VERSION_15)) { testImplementation libs.bcpkix @@ -71,7 +71,7 @@ dependencies { configurations.testRuntimeClasspath { resolutionStrategy.eachDependency { if (it.requested.group == 'org.jetbrains.kotlin') { - it.useVersion(libs.versions.managed.kotlin.asProvider().get()) + it.useVersion(libs.versions.kotlin.asProvider().get()) } } } diff --git a/test-suite/build.gradle b/test-suite/build.gradle index cc2cc16d671..129bef859bd 100644 --- a/test-suite/build.gradle +++ b/test-suite/build.gradle @@ -45,8 +45,8 @@ dependencies { // Adding these for now since micronaut-test isnt resolving correctly ... probably need to upgrade gradle there too testImplementation libs.junit.jupiter.api - testImplementation libs.managed.jcache - testImplementation(platform(libs.boms.micronaut.tracing)) + testImplementation libs.jcache + testImplementation(platform(libs.test.boms.micronaut.tracing)) testImplementation(libs.micronaut.tracing.jaeger) { exclude module: 'micronaut-bom' exclude module: 'micronaut-http-client' @@ -64,10 +64,10 @@ dependencies { testAnnotationProcessor project(":inject-java") testAnnotationProcessor project(":test-suite") - testRuntimeOnly(platform(libs.boms.micronaut.aws)) - testRuntimeOnly libs.managed.h2 + testRuntimeOnly(platform(libs.test.boms.micronaut.aws)) + testRuntimeOnly libs.h2 testRuntimeOnly libs.junit.vintage - testRuntimeOnly libs.managed.logback + testRuntimeOnly libs.logback testRuntimeOnly libs.aws.java.sdk.lambda // needed for HTTP/2 tests @@ -80,7 +80,7 @@ dependencies { } } testImplementation libs.logbook.netty - testImplementation libs.managed.logback + testImplementation libs.logback if (JavaVersion.current().isCompatibleWith(JavaVersion.VERSION_15)) { testImplementation libs.bcpkix @@ -88,7 +88,7 @@ dependencies { testImplementation libs.managed.reactor - testFixturesApi libs.managed.spock + testFixturesApi libs.spock testFixturesApi libs.managed.groovy testFixturesApi libs.jetbrains.annotations } diff --git a/test-utils/build.gradle b/test-utils/build.gradle index a9f8fab93c2..f2ac601a3af 100644 --- a/test-utils/build.gradle +++ b/test-utils/build.gradle @@ -4,5 +4,5 @@ plugins { dependencies { api libs.managed.groovy - testImplementation libs.managed.snakeyaml + testImplementation libs.snakeyaml } diff --git a/validation/build.gradle b/validation/build.gradle index 5a0125b03ba..457feca1d29 100644 --- a/validation/build.gradle +++ b/validation/build.gradle @@ -15,14 +15,14 @@ dependencies { api project(":core-reactive") api libs.managed.validation - compileOnly(libs.managed.gorm) { + compileOnly(libs.gorm) { exclude(module: 'groovy') } compileOnly project(":http-server") implementation libs.managed.reactor - testImplementation libs.managed.spotbugs + testImplementation libs.spotbugs testAnnotationProcessor project(":inject-java") testCompileOnly project(":inject-groovy") testImplementation project(":inject") From be7158ec6c21122b2878a34f3541f35a0b9c1c3b Mon Sep 17 00:00:00 2001 From: Tim Yates Date: Wed, 16 Nov 2022 09:00:58 +0000 Subject: [PATCH 219/743] Add a test module to verify simple native execution (#8280) We detect issues with native compilation quite far down the release chain. This causes problems, and delays when releasing a new version of Micronaut, especially minor or major releases. This commit adds a new module test-suite-graal which runs nativeTest against a simple micronaut application. --- buildSrc/build.gradle | 1 + settings.gradle | 1 + test-suite-graal/build.gradle | 67 +++++++++++++++++++ test-suite-graal/gradle.properties | 1 + .../micronaut/test/graal/HomeController.java | 30 +++++++++ .../test/graal/HomeControllerTest.java | 25 +++++++ 6 files changed, 125 insertions(+) create mode 100644 test-suite-graal/build.gradle create mode 100644 test-suite-graal/gradle.properties create mode 100644 test-suite-graal/src/main/java/io/micronaut/test/graal/HomeController.java create mode 100644 test-suite-graal/src/test/java/io/micronaut/test/graal/HomeControllerTest.java diff --git a/buildSrc/build.gradle b/buildSrc/build.gradle index 2481d6d5e77..c8f2e9c5076 100644 --- a/buildSrc/build.gradle +++ b/buildSrc/build.gradle @@ -10,5 +10,6 @@ dependencies { implementation "gradle.plugin.com.github.johnrengelman:shadow:7.1.2" implementation "org.aim42:htmlSanityCheck:1.1.6" implementation "io.micronaut.build.internal:micronaut-gradle-plugins:5.3.15" + implementation "org.graalvm.buildtools.native:org.graalvm.buildtools.native.gradle.plugin:0.9.14" implementation "org.tomlj:tomlj:1.0.0" } diff --git a/settings.gradle b/settings.gradle index 2a4ada517da..d15dc4124d5 100644 --- a/settings.gradle +++ b/settings.gradle @@ -62,6 +62,7 @@ include "test-suite-helper" include "test-suite-javax-inject" include "test-suite-jakarta-inject-bean-import" include "test-suite-kotlin" +include "test-suite-graal" include "test-suite-groovy" include "test-utils" diff --git a/test-suite-graal/build.gradle b/test-suite-graal/build.gradle new file mode 100644 index 00000000000..9241c72db18 --- /dev/null +++ b/test-suite-graal/build.gradle @@ -0,0 +1,67 @@ +plugins { + id "io.micronaut.build.internal.common" + id 'org.graalvm.buildtools.native' +} + +micronautBuild { + enableBom = false + enableProcessing = false +} + +dependencies { + annotationProcessor libs.bundles.asm + annotationProcessor project(":inject-java") + implementation project(":context") + implementation project(":core") + implementation project(":inject") + implementation project(":graal") + implementation project(":http-server-netty") + implementation project(":http-client") + implementation project(":validation") + implementation project(":inject-java") + + testAnnotationProcessor libs.bundles.asm + testAnnotationProcessor project(":inject-java") + testImplementation libs.managed.micronaut.test.junit5 +} + +tasks.withType(Test).configureEach { + useJUnitPlatform() +} + +configurations { + // Exclude Groovy from the nativeTestCompilation classpath + all { + exclude group: 'org.codehaus.groovy' + } + nativeImageTestClasspath { + exclude module: 'groovy-test' + } +} + +tasks.named("check") { task -> + def graal = ["jvmci.Compiler", "java.vendor.version", "java.vendor"].any { + println "$it ${System.getProperty(it)}" + System.getProperty(it)?.toLowerCase(Locale.ENGLISH)?.contains("graal") + } + if (graal) { + task.dependsOn("nativeTest") + } +} + +def openGraalModules = [ + "org.graalvm.nativeimage.builder/com.oracle.svm.core.jdk", + "org.graalvm.nativeimage.builder/com.oracle.svm.core.configure", + "org.graalvm.sdk/org.graalvm.nativeimage.impl" +] + +graalvmNative { + toolchainDetection = false + binaries { + all { + openGraalModules.each { module -> + jvmArgs.add("--add-exports=" + module + "=ALL-UNNAMED") + } + } + } +} diff --git a/test-suite-graal/gradle.properties b/test-suite-graal/gradle.properties new file mode 100644 index 00000000000..53493a23825 --- /dev/null +++ b/test-suite-graal/gradle.properties @@ -0,0 +1 @@ +skipDocumentation=true \ No newline at end of file diff --git a/test-suite-graal/src/main/java/io/micronaut/test/graal/HomeController.java b/test-suite-graal/src/main/java/io/micronaut/test/graal/HomeController.java new file mode 100644 index 00000000000..9151a0a14fa --- /dev/null +++ b/test-suite-graal/src/main/java/io/micronaut/test/graal/HomeController.java @@ -0,0 +1,30 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.test.graal; + +import io.micronaut.http.annotation.Controller; +import io.micronaut.http.annotation.Get; +import java.util.Collections; +import java.util.Map; + +@Controller +public class HomeController { + + @Get + Map index() { + return Collections.singletonMap("message", "Hello World"); + } +} diff --git a/test-suite-graal/src/test/java/io/micronaut/test/graal/HomeControllerTest.java b/test-suite-graal/src/test/java/io/micronaut/test/graal/HomeControllerTest.java new file mode 100644 index 00000000000..4197d27d599 --- /dev/null +++ b/test-suite-graal/src/test/java/io/micronaut/test/graal/HomeControllerTest.java @@ -0,0 +1,25 @@ +package io.micronaut.test.graal; + +import io.micronaut.http.client.HttpClient; +import io.micronaut.http.client.annotation.Client; +import io.micronaut.test.extensions.junit5.annotation.MicronautTest; +import jakarta.inject.Inject; +import org.junit.jupiter.api.Test; + +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.*; + +@MicronautTest +class HomeControllerTest { + + @Inject + @Client("/") + HttpClient httpClient; + + @Test + void helloWorld() { + assertEquals("Hello World", + httpClient.toBlocking().retrieve("/", Map.class).get("message")); + } +} From a0cae7afa7587065e9e435a3add83939db71d8b5 Mon Sep 17 00:00:00 2001 From: Jonas Konrad Date: Wed, 16 Nov 2022 10:57:36 +0100 Subject: [PATCH 220/743] fix: TLS hostname verification (#8339) Before this patch, TLS certificates' host names were not verified properly. The commented out test demonstrates this (open it in a browser). The wildcard certificate for *.badssl.com is invalid after all for wrong.host.badssl.com --- .../http/client/netty/ConnectionManager.java | 17 ++++++-- .../io/micronaut/http/client/SslSpec.groovy | 3 +- .../netty/RequestCertificateSpec.groovy | 39 ++++++++++++++++-- .../src/test/resources/KeyStore.pkcs12 | Bin 2357 -> 0 bytes .../src/test/resources/TrustStore.jks | Bin 814 -> 0 bytes 5 files changed, 50 insertions(+), 9 deletions(-) delete mode 100644 http-server-netty/src/test/resources/KeyStore.pkcs12 delete mode 100644 http-server-netty/src/test/resources/TrustStore.jks diff --git a/http-client/src/main/java/io/micronaut/http/client/netty/ConnectionManager.java b/http-client/src/main/java/io/micronaut/http/client/netty/ConnectionManager.java index 6024bd86932..b2dedb0584e 100644 --- a/http-client/src/main/java/io/micronaut/http/client/netty/ConnectionManager.java +++ b/http-client/src/main/java/io/micronaut/http/client/netty/ConnectionManager.java @@ -110,6 +110,8 @@ import reactor.core.scheduler.Scheduler; import reactor.core.scheduler.Schedulers; +import javax.net.ssl.SSLEngine; +import javax.net.ssl.SSLParameters; import java.net.InetSocketAddress; import java.net.Proxy; import java.net.SocketAddress; @@ -728,7 +730,7 @@ private void configureHttp2Ssl( HttpToHttp2ConnectionHandler connectionHandler) { ChannelPipeline pipeline = ch.pipeline(); // Specify Host in SSLContext New Handler to add TLS SNI Extension - pipeline.addLast(ChannelPipelineCustomizer.HANDLER_SSL, sslCtx.newHandler(ch.alloc(), host, port)); + pipeline.addLast(ChannelPipelineCustomizer.HANDLER_SSL, configureSslHandler(sslCtx.newHandler(ch.alloc(), host, port))); // We must wait for the handshake to finish and the protocol to be negotiated before configuring // the HTTP/2 components of the pipeline. pipeline.addLast( @@ -926,6 +928,15 @@ private void removeReadTimeoutHandler(ChannelPipeline pipeline) { } } + private SslHandler configureSslHandler(SslHandler sslHandler) { + sslHandler.setHandshakeTimeoutMillis(configuration.getSslConfiguration().getHandshakeTimeout().toMillis()); + SSLEngine engine = sslHandler.engine(); + SSLParameters params = engine.getSSLParameters(); + params.setEndpointIdentificationAlgorithm("HTTPS"); + engine.setSSLParameters(params); + return sslHandler; + } + /** * A handler that triggers the cleartext upgrade to HTTP/2 by sending an initial HTTP request. */ @@ -1124,9 +1135,7 @@ protected void initChannel(SocketChannel ch) { }); if (sslContext != null) { - SslHandler sslHandler = sslContext.newHandler(ch.alloc(), host, port); - sslHandler.setHandshakeTimeoutMillis(configuration.getSslConfiguration().getHandshakeTimeout().toMillis()); - p.addLast(ChannelPipelineCustomizer.HANDLER_SSL, sslHandler); + p.addLast(ChannelPipelineCustomizer.HANDLER_SSL, configureSslHandler(sslContext.newHandler(ch.alloc(), host, port))); } // Pool connections require alternative timeout handling diff --git a/http-client/src/test/groovy/io/micronaut/http/client/SslSpec.groovy b/http-client/src/test/groovy/io/micronaut/http/client/SslSpec.groovy index 7df6b115d32..254f115414a 100644 --- a/http-client/src/test/groovy/io/micronaut/http/client/SslSpec.groovy +++ b/http-client/src/test/groovy/io/micronaut/http/client/SslSpec.groovy @@ -36,7 +36,6 @@ import spock.lang.Specification import javax.net.ssl.SSLHandshakeException import java.security.GeneralSecurityException -import java.security.InvalidAlgorithmParameterException import java.time.Duration import java.util.concurrent.TimeUnit @@ -128,7 +127,7 @@ class SslSpec extends Specification { where: url << [ 'https://expired.badssl.com/', - //'https://wrong.host.badssl.com/', cert is for *.badssl.com, we accept that + 'https://wrong.host.badssl.com/', 'https://self-signed.badssl.com/', 'https://untrusted-root.badssl.com/', //'https://revoked.badssl.com/', not implemented diff --git a/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/RequestCertificateSpec.groovy b/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/RequestCertificateSpec.groovy index 0705a04162a..53f77157245 100644 --- a/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/RequestCertificateSpec.groovy +++ b/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/RequestCertificateSpec.groovy @@ -4,11 +4,19 @@ import io.micronaut.http.HttpRequest import io.micronaut.http.HttpStatus import io.micronaut.http.annotation.Controller import io.micronaut.http.annotation.Get +import io.netty.handler.ssl.util.SelfSignedCertificate import reactor.core.publisher.Flux +import spock.lang.Shared +import java.nio.file.Files +import java.nio.file.Path +import java.security.KeyStore +import java.security.cert.Certificate import java.security.cert.X509Certificate class RequestCertificateSpec extends AbstractMicronautSpec { + @Shared Path keyStorePath + @Shared Path trustStorePath void "test certificate extraction"() { when: @@ -17,11 +25,36 @@ class RequestCertificateSpec extends AbstractMicronautSpec { .blockFirst() then: response.code() == HttpStatus.OK.code - response.body() == "O=Test CA,ST=Some-State,C=US" + response.body() == "CN=localhost" + } + + @Override + void cleanupSpec() { + Files.deleteIfExists(keyStorePath) + Files.deleteIfExists(trustStorePath) } @Override Map getConfiguration() { + def certificate = new SelfSignedCertificate() + + keyStorePath = Files.createTempFile("micronaut-test-key-store", "pkcs12") + trustStorePath = Files.createTempFile("micronaut-test-trust-store", "pkcs12") + + KeyStore ks = KeyStore.getInstance("PKCS12") + ks.load(null, null) + ks.setKeyEntry("key", certificate.key(), "".toCharArray(), new Certificate[]{certificate.cert()}) + try (OutputStream os = Files.newOutputStream(keyStorePath)) { + ks.store(os, "".toCharArray()) + } + + KeyStore ts = KeyStore.getInstance("JKS") + ts.load(null, null) + ts.setCertificateEntry("cert", certificate.cert()) + try (OutputStream os = Files.newOutputStream(trustStorePath)) { + ts.store(os, "123456".toCharArray()) + } + super.getConfiguration() << [ "micronaut.http.client.read-timeout": "15s", 'micronaut.server.ssl.enabled': true, @@ -29,10 +62,10 @@ class RequestCertificateSpec extends AbstractMicronautSpec { // Cannot be true! 'micronaut.server.ssl.buildSelfSigned': false, 'micronaut.ssl.clientAuthentication': "need", - 'micronaut.ssl.key-store.path': 'classpath:KeyStore.pkcs12', + 'micronaut.ssl.key-store.path': 'file://' + keyStorePath.toString(), 'micronaut.ssl.key-store.type': 'PKCS12', 'micronaut.ssl.key-store.password': '', - 'micronaut.ssl.trust-store.path': 'classpath:TrustStore.jks', + 'micronaut.ssl.trust-store.path': 'file://' + trustStorePath.toString(), 'micronaut.ssl.trust-store.type': 'JKS', 'micronaut.ssl.trust-store.password': '123456', ] diff --git a/http-server-netty/src/test/resources/KeyStore.pkcs12 b/http-server-netty/src/test/resources/KeyStore.pkcs12 deleted file mode 100644 index b369b63a3594408f2cb06823669e5c6834f4b5cf..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2357 zcmV-53Ci{`f(bDK0Ru3C2=@jFDuzgg_YDCD0ic2i=mdfYE=;4p#%mj($chDe6@ z4FLxRpn?O4FoFYw0s#Opf&+a92`Yw2hW8Bt2LUh~1_~;MNQUmA<0s;sCfPw>1FfH?51L_IqJzzcE7Q++4SPZH-8TgX&o_2?YxqPCtR?ld0{*Zpq zuc`0tQkc`CT@dk6LGd+@j~lcFn#d^2ZCb`TCHcd?!ZWfLFGc-#mc$Pc{QurIK^O}} zXhIIbsoSvWk4!W!{1fHE`pb1A8cNYbq3!W82(4Q<-o}cOopGuZNiihn#=3jE;=zW}(c8W`hyuV~E-&5>H@xXk$40yB3SQ z-&m)kOlZ!)dX21OZ5Euwn)9$KI@@oKk05~aC762fK`uNq=M;9;5(Iz>0r4Gxh^R@x zYU!}te36`dTmqe2Am`5zN5E@f*=!f&A(k#C;M~zP*=}g% zGS$Bb^Wpe}9XY+OuuXF3#7{~^(1ae%I zR=$xarL<*KQHrJkU+szUiVeUxOm@0+ypa~h*C$Y?C?HDEiDPBe-IynQ;?2)KQf5#q znJ`kfIO%5O^VZrah$;r1#pA__Cq?&NMwf%G#Yub~J0M<-r}vUWf_i2Qso9=q#e!a> z11#MnWuCCEaxIFq>2~9Hi;aoqYHS(&tBL<59Rw2QC5I5vK;ClH^$d)M2haQ>igqGv z{_E4ZxwXPFs*yG@TXmCg+X@A1BI6km{xU3k(3I%s?<6hd_nYU$A|9{D;(TF&CP7Pl zaK`Y|T@Kr1nEzs!K7Eu!Dd9@I`D>A*Bm zm%A9MFYNk^R^6m=G5oQ2i6%o?DJw^FoFd^1_>&LNQU)Pvy1) z_5lI{2ml0v1jt3_mKtPayYC{Xl#*A<9`s|~PLP^PpKq`}pbjsj9<=qc z^Gkmr(r>W*3GN-Eg5#H1NB(v4p?mB*h}iqD~vnRVMJ#mVIo+HiU5P zm5vOjXPn(0BqUgkuupMMw?28HUlo9XFmvkydzq(;oV-S>yz#VJor^Z)&s>186XFHb zc-&^I0B%-$=xs+hq?%fPAS>?1vDMOtyP(J19>qd*u8U5pswWfX((eObDqB=4!6gAu zpvp7fi}2U?*{Qg|NJ|xEeAGJZ#tDH#7z2#k|9M93op!S0- zu8~b2kXMW~Za4MH*39}PbK)&8Umy8sk53h^!Myn1856jhsuUlN9FWq}gyaiYn74 zzz;81%BKgqmxb*7gc6=Qi0@JJ?NN4P?#d82!yE$N_5g#ocMwhjuceRF#J#-RbK}@OIP$k=}XTd_xFcn(SE+eKZlP_^fl}YWeWKc8P#9iepjdrc86=y}qfWzDpSv zl0)qwf=`c8Qh)Qn+Skn}Cc5`hlxEZlGw^1sJw;oX>}r&mx*@gyB~&p1N3B>TsN@nxm}>+OfXed$FICmA(a ziB9;VYmddh9BKfRPaqW&?UEC$<%gGoKVK)ZL<^4`d z4V3r7qD&|1ITFnM$$NalET*pB2e{`8g|z}tw}UUfglldT>2fh8Fe3&DDuzgg_YDCF z6)_eB6!DKQfPOHPuvLnDyd6u=%#A06*f23LAutIB1uG5%0vZJX1QcU+VpB*?|X9+FyIqTuXkAScdiWMF7uU}j)$VrgU)CC+PNVPIrr z2<4J#Y7?Upvhx^O8JL?G`5Azo;bLlHWMp`@sYq($$(D5=x4F+>A(kD!Z$-oBe|Ju0 zMgHIkws(0W=&C!zS*`Gw#2Qz-i|rLQ+xABNiQ4ow=z&vlRJQ!_<99Br>pnMol*XsE zpjpo6lI3*CFL~umyGu5&DH8Xt)c(7x_|&293*YkZORoFcCH+4#!1PvZhrZOlCYQww zB8%3YUYK~hW7C%26AAK+r}lBbK3QIzKbhfCf~Vek3%*A^VQWkHzG^goo*44baObDy z2D8`aBPT6%*loGD?Bk)Vu%n(0Tdt`Kq;NLP32l3*ui{a9{nG)L3qcdSqXMSSRTh81 z+~%En@>J!M?tMb0>!b75ugm%#G(%A7;Z%<)D%!3WyFUNyWnyMzU_=ffU<3j~h>_vx z?03i7*Cl@5yL?;5wZ25gpGRKAniLwYGk0m2^CMu^OS$h~c~3|#SimoHvT^6*PhVch z<%=0UbSdd6>tC$CK3`F%{}_`Ddu@F9q#JMlee68qt$H-5Fs8?`Droy3H#S?9sXIIF zRtBiPG}p@#ZdkoN-oz_kQM<#$)WovaN29#qo3?whUeKSd6FC37WWM~GAIc=Ktl-K| zJ;!<6AJ=)z1aCYDjFFspodOJm839Z2P95{&zPloGcy+wP@#Y zrX`lJS1 Date: Wed, 16 Nov 2022 04:58:03 -0500 Subject: [PATCH 221/743] build: Bump micronaut-maven-plugin to 3.5.0 (#8341) --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index b86b82812e7..ac93fd1788e 100644 --- a/gradle.properties +++ b/gradle.properties @@ -43,7 +43,7 @@ developers=Graeme Rocher kapt.use.worker.api=true # Dependency Versions -micronautMavenPluginVersion=3.4.0 +micronautMavenPluginVersion=3.5.0 chromedriverVersion=79.0.3945.36 geckodriverVersion=0.26.0 webdriverBinariesVersion=1.4 From f23b8f1b0b56419c0924acb2244306bbf14a0a75 Mon Sep 17 00:00:00 2001 From: Tim Yates Date: Wed, 16 Nov 2022 09:59:12 +0000 Subject: [PATCH 222/743] fix: Mask headers that match know credentials (#8336) When trace logging is enabled, all headers are logged. We should avoid this, as it may be captured by logging or build software --- .../http/client/netty/DefaultHttpClient.java | 20 ++++- .../netty/DefaultClientHeaderMaskTest.groovy | 87 +++++++++++++++++++ 2 files changed, 105 insertions(+), 2 deletions(-) create mode 100644 http-client/src/test/groovy/io/micronaut/http/client/netty/DefaultClientHeaderMaskTest.groovy diff --git a/http-client/src/main/java/io/micronaut/http/client/netty/DefaultHttpClient.java b/http-client/src/main/java/io/micronaut/http/client/netty/DefaultHttpClient.java index 8bf5d2456f7..cc78bab9ecb 100644 --- a/http-client/src/main/java/io/micronaut/http/client/netty/DefaultHttpClient.java +++ b/http-client/src/main/java/io/micronaut/http/client/netty/DefaultHttpClient.java @@ -34,6 +34,7 @@ import io.micronaut.core.util.ArrayUtils; import io.micronaut.core.util.CollectionUtils; import io.micronaut.core.util.StringUtils; +import io.micronaut.core.util.SupplierUtil; import io.micronaut.http.HttpResponse; import io.micronaut.http.HttpResponseWrapper; import io.micronaut.http.HttpStatus; @@ -181,6 +182,7 @@ import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Supplier; +import java.util.regex.Pattern; import java.util.stream.Collectors; import static io.micronaut.scheduling.instrument.InvocationInstrumenter.NOOP; @@ -208,6 +210,9 @@ public class DefaultHttpClient implements private static final int DEFAULT_HTTP_PORT = 80; private static final int DEFAULT_HTTPS_PORT = 443; + private static final Supplier HEADER_MASK_PATTERNS = SupplierUtil.memoized(() -> + Pattern.compile(".*(password|cred|cert|key|secret|token|auth|signat).*", Pattern.CASE_INSENSITIVE) + ); /** * Which headers not to copy from the first request when redirecting to a second request. There doesn't * appear to be a spec for this. {@link java.net.HttpURLConnection} seems to drop all headers, but that would be a @@ -1767,17 +1772,28 @@ private void traceChunk(ByteBuf content) { private void traceHeaders(HttpHeaders headers) { for (String name : headers.names()) { + boolean isMasked = HEADER_MASK_PATTERNS.get().matcher(name).matches(); List all = headers.getAll(name); if (all.size() > 1) { for (String value : all) { - log.trace("{}: {}", name, value); + String maskedValue = isMasked ? mask(value) : value; + log.trace("{}: {}", name, maskedValue); } } else if (!all.isEmpty()) { - log.trace("{}: {}", name, all.get(0)); + String maskedValue = isMasked ? mask(all.get(0)) : all.get(0); + log.trace("{}: {}", name, maskedValue); } } } + @Nullable + private String mask(@Nullable String value) { + if (value == null) { + return null; + } + return "*MASKED*"; + } + private static MediaTypeCodecRegistry createDefaultMediaTypeRegistry() { JsonMapper mapper = new JacksonDatabindMapper(); ApplicationConfiguration configuration = new ApplicationConfiguration(); diff --git a/http-client/src/test/groovy/io/micronaut/http/client/netty/DefaultClientHeaderMaskTest.groovy b/http-client/src/test/groovy/io/micronaut/http/client/netty/DefaultClientHeaderMaskTest.groovy new file mode 100644 index 00000000000..8f3a693ed91 --- /dev/null +++ b/http-client/src/test/groovy/io/micronaut/http/client/netty/DefaultClientHeaderMaskTest.groovy @@ -0,0 +1,87 @@ +package io.micronaut.http.client.netty + +import ch.qos.logback.classic.Level +import ch.qos.logback.classic.Logger +import ch.qos.logback.classic.spi.ILoggingEvent +import ch.qos.logback.core.AppenderBase +import io.micronaut.context.ApplicationContext +import io.netty.handler.codec.http.DefaultHttpHeaders +import org.slf4j.LoggerFactory +import spock.lang.Specification + +import java.util.concurrent.BlockingQueue +import java.util.concurrent.LinkedBlockingQueue +import java.util.concurrent.TimeUnit + +class DefaultClientHeaderMaskTest extends Specification { + + def "check masking works for #value"() { + given: + def ctx = ApplicationContext.run() + def client = ctx.createBean(DefaultHttpClient, "http://localhost:8080") + + expect: + client.mask(value) == expected + + cleanup: + ctx.close() + + where: + value | expected + null | null + "foo" | "*MASKED*" + "Tim Yates" | "*MASKED*" + } + + def "check mask detects common security headers"() { + given: + MemoryAppender appender = new MemoryAppender() + Logger logger = (Logger) LoggerFactory.getLogger(DefaultHttpClient.class) + logger.addAppender(appender) + logger.setLevel(Level.TRACE) + appender.start() + + DefaultHttpHeaders headers = new DefaultHttpHeaders() + headers.add("Authorization", "Bearer foo") + headers.add("Proxy-Authorization", "AWS4-HMAC-SHA256 bar") + headers.add("Cookie", "baz") + headers.add("Set-Cookie", "qux") + headers.add("X-Forwarded-For", "quux") + headers.add("X-Forwarded-Host", "quuz") + headers.add("X-Real-IP", "waldo") + headers.add("X-Forwarded-For", "fred") + headers.add("Credential", "foo") + headers.add("Signature", "bar probably secret") + def ctx = ApplicationContext.run() + def client = ctx.createBean(DefaultHttpClient, "http://localhost:8080") + + when: + client.traceHeaders(headers) + + then: + appender.events.size() == 10 + appender.events.join("\n") == """Authorization: *MASKED* + |Proxy-Authorization: *MASKED* + |Cookie: baz + |Set-Cookie: qux + |X-Forwarded-For: quux + |X-Forwarded-For: fred + |X-Forwarded-Host: quuz + |X-Real-IP: waldo + |Credential: *MASKED* + |Signature: *MASKED*""".stripMargin() + + cleanup: + ctx.close() + appender.stop() + } + + static class MemoryAppender extends AppenderBase { + final BlockingQueue events = new LinkedBlockingQueue<>() + + @Override + protected void append(ILoggingEvent e) { + events.add(e.formattedMessage) + } + } +} From 1b2497028a0a4d9c6a0f1799cb3e57e749caf425 Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Wed, 16 Nov 2022 16:42:50 +0100 Subject: [PATCH 223/743] doc: Documents Nullability annotations (#8348) Fixes https://github.com/micronaut-projects/micronaut-core/issues/7777 --- .../guide/ioc/nullabilityAnnotations.adoc | 21 +++++++++++++++++++ src/main/docs/guide/toc.yml | 1 + 2 files changed, 22 insertions(+) create mode 100644 src/main/docs/guide/ioc/nullabilityAnnotations.adoc diff --git a/src/main/docs/guide/ioc/nullabilityAnnotations.adoc b/src/main/docs/guide/ioc/nullabilityAnnotations.adoc new file mode 100644 index 00000000000..9a96cc80ed3 --- /dev/null +++ b/src/main/docs/guide/ioc/nullabilityAnnotations.adoc @@ -0,0 +1,21 @@ +In Java, you can use annotations showing whether a variable can or cannot be null. Such annotations aren't part of the standard library, but you can add them separately. + +Micronaut framework comes with its own set of annotations to declare nullability; ann:core.annotation.Nullable[] and ann:core.annotation.NonNull[]. + +**Why does the Micronaut framework add its own set of nullability annotations instead of using one of the existing nullability annotations libraries?** + +Throughout the history of the framework, we used other nullability annotation libraries. However, licensing issues made us change nullability annotations several times. To avoid having to change nullability annotations in the future, we added our own set of nullability annotations in Micronaut Framework 2.4 + +**Are Micronaut Nullability annotations recognized by Kotlin?** + +Yes, Micronaut nullability annotations are mapped at compilation time to `javax.annotation.Nullable` and `javax.annotation.Nonnull`. + +**Why should you use nullability annotations in your code?** + +It makes your code easier to consume from Kotlin. https://kotlinlang.org/docs/java-interop.html#nullability-annotations[Kotlin recognizes nullability annotations when you're calling Java code from Kotlin code and will treat types according to their annotations]. + +Moreover, you can use ann:core.annotation.Nullable[] annotation to mark: + +* A <> method parameter as optional. +* An injection point as optional. For example, when using constructor injection you can annotate one a constructor parameter as optional by adding the ann:core.annotation.Nullable[] annotation. + diff --git a/src/main/docs/guide/toc.yml b/src/main/docs/guide/toc.yml index f46d6fe5f53..7c681907379 100644 --- a/src/main/docs/guide/toc.yml +++ b/src/main/docs/guide/toc.yml @@ -39,6 +39,7 @@ ioc: beanValidation: Bean Validation annotationMetadata: Bean Annotation Metadata beanImport: Importing Beans from Libraries + nullabilityAnnotations: Nullability Annotations springBeans: Micronaut Beans And Spring android: Android Support config: From 781461918ba2206790dab2a35abbec5a2dad870a Mon Sep 17 00:00:00 2001 From: micronaut-build <65172877+micronaut-build@users.noreply.github.com> Date: Wed, 16 Nov 2022 12:16:16 -0500 Subject: [PATCH 224/743] build: bump micronaut-security to 3.8.3 (#8349) --- gradle.properties | 2 +- gradle/libs.versions.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle.properties b/gradle.properties index a062ad0f403..5e366941300 100644 --- a/gradle.properties +++ b/gradle.properties @@ -52,7 +52,7 @@ kotlin.stdlib.default.dependency=false # For the docs graalVersion=22.0.0.2 -micronautSecurityVersion=3.8.2 +micronautSecurityVersion=3.8.3 org.gradle.caching=true org.gradle.parallel=true diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 3ba4fad361a..cd2f03d7a41 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -112,7 +112,7 @@ managed-micronaut-rss = "3.2.0" managed-micronaut-rxjava1 = "1.0.0" managed-micronaut-rxjava2 = "1.3.0" managed-micronaut-rxjava3 = "2.3.0" -managed-micronaut-security = "3.8.2" +managed-micronaut-security = "3.8.3" managed-micronaut-serialization = "1.3.3" managed-micronaut-servlet = "3.3.2" managed-micronaut-spring = "4.3.1" From ba7515adc2830664ce766053649e5817785edc90 Mon Sep 17 00:00:00 2001 From: micronaut-build Date: Wed, 16 Nov 2022 19:59:55 +0000 Subject: [PATCH 225/743] [skip ci] Release v3.7.4 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 5e366941300..a2f75e8abfb 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -projectVersion=3.7.4-SNAPSHOT +projectVersion=3.7.4 projectGroupId=io.micronaut title=Micronaut projectDesc=Natively Cloud Native From b44efc8fa385a96644da1d69887a2cae31d7bc0d Mon Sep 17 00:00:00 2001 From: micronaut-build Date: Wed, 16 Nov 2022 20:12:23 +0000 Subject: [PATCH 226/743] Back to 3.7.5-SNAPSHOT --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index a2f75e8abfb..68cbfb11dfe 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -projectVersion=3.7.4 +projectVersion=3.7.5-SNAPSHOT projectGroupId=io.micronaut title=Micronaut projectDesc=Natively Cloud Native From 7ee06b588e29f222b75a31d343ba4251f4b32900 Mon Sep 17 00:00:00 2001 From: Graeme Rocher Date: Thu, 17 Nov 2022 08:33:45 +0100 Subject: [PATCH 227/743] Groovy properties shouldn't become executable methods (#8350) --- .../DeclaredBeanElementCreator.java | 4 + ...uctionWithAroundOnConcreteClassSpec.groovy | 4 +- .../executable/ExecutableBeanSpec.groovy | 74 ++++++++++++++++++- 3 files changed, 76 insertions(+), 6 deletions(-) diff --git a/core-processor/src/main/java/io/micronaut/inject/processing/DeclaredBeanElementCreator.java b/core-processor/src/main/java/io/micronaut/inject/processing/DeclaredBeanElementCreator.java index 741f0b3af8d..7b1f6b5ac2f 100644 --- a/core-processor/src/main/java/io/micronaut/inject/processing/DeclaredBeanElementCreator.java +++ b/core-processor/src/main/java/io/micronaut/inject/processing/DeclaredBeanElementCreator.java @@ -459,6 +459,10 @@ protected boolean visitExecutableMethod(BeanDefinitionVisitor visitor, MethodEle if (!methodElement.hasStereotype(Executable.class)) { return false; } + if (methodElement.isSynthetic()) { + // Synthetic methods cannot be executable as @Executable cannot be put on a field + return false; + } if (getElementAnnotationMetadata(methodElement).hasStereotype(Executable.class)) { // @Executable annotated on the method // Throw error if it cannot be accessed without the reflection diff --git a/inject-groovy/src/test/groovy/io/micronaut/aop/introduction/with_around/IntroductionWithAroundOnConcreteClassSpec.groovy b/inject-groovy/src/test/groovy/io/micronaut/aop/introduction/with_around/IntroductionWithAroundOnConcreteClassSpec.groovy index 6298f078218..ed069ae0ff0 100644 --- a/inject-groovy/src/test/groovy/io/micronaut/aop/introduction/with_around/IntroductionWithAroundOnConcreteClassSpec.groovy +++ b/inject-groovy/src/test/groovy/io/micronaut/aop/introduction/with_around/IntroductionWithAroundOnConcreteClassSpec.groovy @@ -78,8 +78,8 @@ class Test {} def proxyTargetBeanDefinition = applicationContext.getProxyTargetBeanDefinition(clazz, null) def beanDefinition = applicationContext.getBeanDefinition(clazz, null) then: - proxyTargetBeanDefinition.getExecutableMethods().size() == 5 - beanDefinition.getExecutableMethods().size() == 5 + proxyTargetBeanDefinition.getExecutableMethods().size() == 1 + beanDefinition.getExecutableMethods().size() == 1 } void "test executable methods count for around with executable"() { diff --git a/inject-groovy/src/test/groovy/io/micronaut/inject/executable/ExecutableBeanSpec.groovy b/inject-groovy/src/test/groovy/io/micronaut/inject/executable/ExecutableBeanSpec.groovy index bf34dd51d3e..5d6140c9457 100644 --- a/inject-groovy/src/test/groovy/io/micronaut/inject/executable/ExecutableBeanSpec.groovy +++ b/inject-groovy/src/test/groovy/io/micronaut/inject/executable/ExecutableBeanSpec.groovy @@ -20,6 +20,72 @@ import io.micronaut.inject.BeanDefinition class ExecutableBeanSpec extends AbstractBeanDefinitionSpec { + void "test executable at class level"() { + given: + BeanDefinition definition = buildBeanDefinition('test.ExecutableController','''\ +package test + +import io.micronaut.context.annotation.Executable; +import io.micronaut.inject.annotation.*; + +@jakarta.inject.Singleton +@Executable +class ExecutableController { + String foo + + @Executable + public int round(float num) { + return Math.round(num); + } + + @Executable + public int sum(int a, int b) { + return doSum() + } + + private int doSum() { + return a + b + } +} +''') + expect: + definition != null + definition.executableMethods.size() == 2 + definition.executableMethods*.name.toSorted() == ['round', 'sum'].toSorted() + } + + void "test executable at class level 2"() { + given: + BeanDefinition definition = buildBeanDefinition('test.ExecutableController','''\ +package test + +import io.micronaut.context.annotation.Executable; +import io.micronaut.inject.annotation.*; + +@jakarta.inject.Singleton +@Executable +class ExecutableController { + String foo + + public int round(float num) { + return Math.round(num); + } + + public int sum(int a, int b) { + return doSum() + } + + private int doSum() { + return a + b + } +} +''') + expect: + definition != null + definition.executableMethods.size() == 2 + definition.executableMethods*.name.toSorted() == ['round', 'sum'].toSorted() + } + void "test executable on stereotype"() { given: BeanDefinition definition = buildBeanDefinition('test.ExecutableController','''\ @@ -105,14 +171,14 @@ class MyBean { @RepeatableExecutable("b") ]) void run() { - + } - - + + @RepeatableExecutable("a") @RepeatableExecutable("b") void run2() { - + } } ''') From b4ee415285e5dff1b71f272aca49b2a89326e816 Mon Sep 17 00:00:00 2001 From: micronaut-build <65172877+micronaut-build@users.noreply.github.com> Date: Thu, 17 Nov 2022 03:52:13 -0500 Subject: [PATCH 228/743] Bump micronaut-kafka to 4.5.0 (#8345) --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 840d736db64..4cb208b192a 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -87,7 +87,7 @@ managed-micronaut-ignite = "1.0.0.RC1" managed-micronaut-jaxrs = "3.4.0" managed-micronaut-jms = "2.1.0" managed-micronaut-jmx = "3.1.0" -managed-micronaut-kafka = "4.4.1" +managed-micronaut-kafka = "4.5.0" managed-micronaut-kotlin = "3.2.2" managed-micronaut-kubernetes = "3.4.0" managed-micronaut-micrometer = "4.6.1" From 3ec217598540728218383a85e80f3676ef9e6319 Mon Sep 17 00:00:00 2001 From: Graeme Rocher Date: Thu, 17 Nov 2022 12:49:25 +0100 Subject: [PATCH 229/743] Fix NoSuchMethodError when calling getGenericBeanType() (#8351) * reproduce NoSuchMethodError * Fix bug --- .../writer/BeanDefinitionReferenceWriter.java | 2 +- .../inject/beans/BeanDefinitionSpec.groovy | 62 +++++++++++++++++++ 2 files changed, 63 insertions(+), 1 deletion(-) diff --git a/core-processor/src/main/java/io/micronaut/inject/writer/BeanDefinitionReferenceWriter.java b/core-processor/src/main/java/io/micronaut/inject/writer/BeanDefinitionReferenceWriter.java index 56f5a89b58f..638bf6d4253 100644 --- a/core-processor/src/main/java/io/micronaut/inject/writer/BeanDefinitionReferenceWriter.java +++ b/core-processor/src/main/java/io/micronaut/inject/writer/BeanDefinitionReferenceWriter.java @@ -253,7 +253,7 @@ private ClassWriter generateClassBytes() { GeneratorAdapter getGenericType = startPublicMethodZeroArgs(classWriter, Argument.class, "getGenericBeanType"); pushCreateArgument( beanDefinitionReferenceClassName, - beanDefinitionType, + Type.getType(getTypeDescriptor(beanDefinitionReferenceClassName)), classWriter, getGenericType, "T", diff --git a/inject-java/src/test/groovy/io/micronaut/inject/beans/BeanDefinitionSpec.groovy b/inject-java/src/test/groovy/io/micronaut/inject/beans/BeanDefinitionSpec.groovy index 31ee613e133..0689f488066 100644 --- a/inject-java/src/test/groovy/io/micronaut/inject/beans/BeanDefinitionSpec.groovy +++ b/inject-java/src/test/groovy/io/micronaut/inject/beans/BeanDefinitionSpec.groovy @@ -179,6 +179,68 @@ class Y implements X { e.message.contains("Bean defines an exposed type [limittypes.Y] that is not implemented by the bean type") } + void "test declared generics from definition"() { + when: + def definition = buildBeanDefinition('limittypes.Test', ''' +package limittypes; + +import io.micronaut.context.annotation.*; +import jakarta.inject.Singleton; + +@Singleton +class Test { +} + + +''') + + then: + definition.getGenericBeanType().getTypeString(true) == 'Test' + } + + void "test declared generics from reference"() { + when: + def ref = buildBeanDefinitionReference('limittypes.Test', ''' +package limittypes; + +import io.micronaut.context.annotation.*; +import jakarta.inject.Singleton; + +@Singleton +class Test { +} + + +''') + + then: + ref.getGenericBeanType().getTypeString(true) == 'Test' + } + + void "test declared generics from reference with inheritance"() { + when: + def ref = buildBeanDefinitionReference('test.DefaultKafkaConsumerConfiguration', ''' +package test; + +import io.micronaut.context.annotation.*; +import jakarta.inject.Singleton; + +@Singleton +@Requires(beans = KafkaDefaultConfiguration.class) +class DefaultKafkaConsumerConfiguration extends AbstractKafkaConsumerConfiguration { +} + +abstract class AbstractKafkaConsumerConfiguration extends AbstractKafkaConfiguration { } + +abstract class AbstractKafkaConfiguration {} + +class KafkaDefaultConfiguration {} +''') + + then: + ref.getGenericBeanType().getTypeString(true) == 'DefaultKafkaConsumerConfiguration' + } + void "test generics from factory"() { when: def ref = buildBeanDefinitionReference('limittypes.Test$Method0', ''' From 59682cdb9b75d41b930e368958244edb1699218e Mon Sep 17 00:00:00 2001 From: Jonas Konrad Date: Thu, 17 Nov 2022 17:03:14 +0100 Subject: [PATCH 230/743] Detect deadlocks when using BlockingHttpClient on the event loop (#8354) Keep track of the thread that called BlockingHttpClient.exchange, and throw an exception when the client attempts to use the same thread for the connection. Before this patch, the blocking call would time out, now it will fail with a specific exception. The event loop group usually has many threads, and often a blocking call will wait on *another* thread of that event loop group by chance. Such uses currently only lead to sporadic read timeouts at the moment. This implementation is conservative: It will not fail when the same event loop group is used, it will only fail with the exact same thread. So the sporadic timeouts will be replaced by sporadic deadlock exceptions. Relates to #8198 Co-authored-by: Tim Yates --- .../http/client/netty/BlockHint.java | 82 +++++++++++++++++++ .../client/netty/CancellableMonoSink.java | 16 +++- .../http/client/netty/ConnectionManager.java | 38 ++++++--- .../http/client/netty/DefaultHttpClient.java | 27 +++--- .../http/client/netty/PoolResizer.java | 19 +++-- .../micronaut/http/client/netty/PoolSink.java | 33 ++++++++ .../http/client/BlockingDeadlockSpec.groovy | 56 +++++++++++++ 7 files changed, 239 insertions(+), 32 deletions(-) create mode 100644 http-client/src/main/java/io/micronaut/http/client/netty/BlockHint.java create mode 100644 http-client/src/main/java/io/micronaut/http/client/netty/PoolSink.java create mode 100644 http-client/src/test/groovy/io/micronaut/http/client/BlockingDeadlockSpec.groovy diff --git a/http-client/src/main/java/io/micronaut/http/client/netty/BlockHint.java b/http-client/src/main/java/io/micronaut/http/client/netty/BlockHint.java new file mode 100644 index 00000000000..87b7a25c921 --- /dev/null +++ b/http-client/src/main/java/io/micronaut/http/client/netty/BlockHint.java @@ -0,0 +1,82 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.http.client.netty; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.NonNull; +import io.micronaut.core.annotation.Nullable; +import io.micronaut.http.client.exceptions.HttpClientException; +import io.netty.channel.EventLoop; + +/** + * Information about what threads are blocked waiting for a request to complete. This is used to + * detect deadlocks when the user does a {@link io.micronaut.http.client.BlockingHttpClient} on the + * event loop. + * + * @param blockedThread Thread that is blocked + * @param next Next node in the linked list of blocked threads + * @author Jonas Konrad + * @since 4.0.0 + */ +@Internal +record BlockHint(Thread blockedThread, @Nullable BlockHint next) { + public static BlockHint willBlockThisThread() { + return new BlockHint(Thread.currentThread(), null); + } + + @Nullable + public static BlockHint combine(@Nullable BlockHint a, @Nullable BlockHint b) { + if (a == null) { + return b; + } else if (b == null) { + return a; + } else if (a.next == null) { + return new BlockHint(a.blockedThread, b); + } else if (b.next == null) { + return new BlockHint(b.blockedThread, a); + } else { + throw new UnsupportedOperationException( + "would need to build a new linked list here, but we never need this"); + } + } + + void checkIsNotBlocked(EventLoop eventLoop) { + if (blocks(eventLoop)) { + throw createException(); + } + } + + @NonNull + static HttpClientException createException() { + return new HttpClientException( + "Failed to perform blocking request on the event loop because request execution " + + "would be dispatched on the same event loop. This would lead to a deadlock. " + + "Either configure the HTTP client to use a different event loop, or use the " + + "reactive HTTP client. " + + "https://docs.micronaut.io/latest/guide/index.html#clientConfiguration"); + } + + boolean blocks(EventLoop eventLoop) { + BlockHint bh = this; + while (bh != null) { + if (eventLoop.inEventLoop(bh.blockedThread)) { + return true; + } + bh = bh.next; + } + return false; + } +} diff --git a/http-client/src/main/java/io/micronaut/http/client/netty/CancellableMonoSink.java b/http-client/src/main/java/io/micronaut/http/client/netty/CancellableMonoSink.java index c56a84accf5..ad9231b2205 100644 --- a/http-client/src/main/java/io/micronaut/http/client/netty/CancellableMonoSink.java +++ b/http-client/src/main/java/io/micronaut/http/client/netty/CancellableMonoSink.java @@ -17,6 +17,7 @@ import io.micronaut.core.annotation.Internal; import io.micronaut.core.annotation.NonNull; +import io.micronaut.core.annotation.Nullable; import org.reactivestreams.Publisher; import org.reactivestreams.Subscriber; import org.reactivestreams.Subscription; @@ -30,15 +31,28 @@ * @param Element type */ @Internal -final class CancellableMonoSink implements Publisher, Sinks.One, Subscription { +final class CancellableMonoSink implements Publisher, Sinks.One, Subscription, PoolSink { private static final Object EMPTY = new Object(); + @Nullable + private final BlockHint blockHint; + private T value; private Throwable failure; private boolean complete = false; private Subscriber subscriber = null; private boolean subscriberWaiting = false; + CancellableMonoSink(@Nullable BlockHint blockHint) { + this.blockHint = blockHint; + } + + @Override + @Nullable + public BlockHint getBlockHint() { + return blockHint; + } + @Override public synchronized void subscribe(Subscriber s) { if (this.subscriber != null) { diff --git a/http-client/src/main/java/io/micronaut/http/client/netty/ConnectionManager.java b/http-client/src/main/java/io/micronaut/http/client/netty/ConnectionManager.java index 0039cc29da1..00587ee0cb7 100644 --- a/http-client/src/main/java/io/micronaut/http/client/netty/ConnectionManager.java +++ b/http-client/src/main/java/io/micronaut/http/client/netty/ConnectionManager.java @@ -366,10 +366,11 @@ private SslContext buildSslContext(DefaultHttpClient.RequestKey requestKey) { * Get a connection for non-websocket http client methods. * * @param requestKey The remote to connect to + * @param blockHint Optional information about what threads are blocked for this connection request * @return A mono that will complete once the channel is ready for transmission */ - Mono connect(DefaultHttpClient.RequestKey requestKey) { - return pools.computeIfAbsent(requestKey, Pool::new).acquire(); + Mono connect(DefaultHttpClient.RequestKey requestKey, @Nullable BlockHint blockHint) { + return pools.computeIfAbsent(requestKey, Pool::new).acquire(blockHint); } /** @@ -381,7 +382,7 @@ Mono connect(DefaultHttpClient.RequestKey requestKey) { * @return A mono that will complete when the handshakes complete */ Mono connectForWebsocket(DefaultHttpClient.RequestKey requestKey, ChannelHandler handler) { - Sinks.Empty initial = new CancellableMonoSink<>(); + Sinks.Empty initial = new CancellableMonoSink<>(null); ChannelFuture connectFuture = doConnect(requestKey, new ChannelInitializer() { @Override @@ -796,8 +797,8 @@ protected void onNewConnectionFailure(@Nullable Throwable cause) throws Exceptio this.requestKey = requestKey; } - Mono acquire() { - Sinks.One sink = new CancellableMonoSink<>(); + Mono acquire(@Nullable BlockHint blockHint) { + PoolSink sink = new CancellableMonoSink<>(blockHint); addPendingRequest(sink); Optional acquireTimeout = configuration.getConnectionPoolConfiguration().getAcquireTimeout(); //noinspection OptionalIsPresent @@ -830,7 +831,7 @@ void onNewConnectionFailure(@Nullable Throwable error) throws Exception { } @Override - void openNewConnection() { + void openNewConnection(@Nullable BlockHint blockHint) throws Exception { // open a new connection ChannelInitializer initializer; if (requestKey.isSecure()) { @@ -867,7 +868,13 @@ public void channelActive(@NonNull ChannelHandlerContext ctx) throws Exception { throw new AssertionError("Unknown plaintext mode"); } } - addInstrumentedListener(doConnect(requestKey, initializer), future -> { + ChannelFuture channelFuture = doConnect(requestKey, initializer); + if (blockHint != null && blockHint.blocks(channelFuture.channel().eventLoop())) { + channelFuture.channel().close(); + onNewConnectionFailure(BlockHint.createException()); + return; + } + addInstrumentedListener(channelFuture, future -> { if (!future.isSuccess()) { onNewConnectionFailure(future.cause()); } @@ -977,11 +984,16 @@ final void emitPoolHandle(Sinks.One sink, PoolHandle ph) { } @Override - public boolean dispatch(Sinks.One sink) { + public boolean dispatch(PoolSink sink) { if (!tryEarmarkForRequest()) { return false; } + BlockHint blockHint = sink.getBlockHint(); + if (blockHint != null && blockHint.blocks(channel.eventLoop())) { + sink.tryEmitError(BlockHint.createException()); + return true; + } if (channel.eventLoop().inEventLoop()) { dispatch0(sink); } else { @@ -996,7 +1008,7 @@ public boolean dispatch(Sinks.One sink) { * * @param sink The request for a pool handle */ - abstract void dispatch0(Sinks.One sink); + abstract void dispatch0(PoolSink sink); /** * Try to add a new request to this connection. This is called outside the event loop, @@ -1068,7 +1080,7 @@ void fireReadTimeout(ChannelHandlerContext ctx) { } @Override - void dispatch0(Sinks.One sink) { + void dispatch0(PoolSink sink) { if (!channel.isActive()) { returnPendingRequest(sink); return; @@ -1112,7 +1124,7 @@ void notifyRequestPipelineBuilt() { emitPoolHandle(sink, ph); } - private void returnPendingRequest(Sinks.One sink) { + private void returnPendingRequest(PoolSink sink) { // failed, but the pending request may still work on another connection. addPendingRequest(sink); hasLiveRequest.set(false); @@ -1171,7 +1183,7 @@ void fireReadTimeout(ChannelHandlerContext ctx) { } @Override - void dispatch0(Sinks.One sink) { + void dispatch0(PoolSink sink) { if (!channel.isActive() || windDownConnection) { returnPendingRequest(sink); return; @@ -1221,7 +1233,7 @@ void notifyRequestPipelineBuilt() { }); } - private void returnPendingRequest(Sinks.One sink) { + private void returnPendingRequest(PoolSink sink) { // failed, but the pending request may still work on another connection. addPendingRequest(sink); liveRequests.decrementAndGet(); diff --git a/http-client/src/main/java/io/micronaut/http/client/netty/DefaultHttpClient.java b/http-client/src/main/java/io/micronaut/http/client/netty/DefaultHttpClient.java index 14b17e5f2c2..cb38a85f8ba 100644 --- a/http-client/src/main/java/io/micronaut/http/client/netty/DefaultHttpClient.java +++ b/http-client/src/main/java/io/micronaut/http/client/netty/DefaultHttpClient.java @@ -494,7 +494,8 @@ public void close() { @Override public io.micronaut.http.HttpResponse exchange(io.micronaut.http.HttpRequest request, Argument bodyType, Argument errorType) { - Flux> publisher = Flux.from(DefaultHttpClient.this.exchange(request, bodyType, errorType)); + BlockHint blockHint = BlockHint.willBlockThisThread(); + Flux> publisher = Flux.from(DefaultHttpClient.this.exchange(request, bodyType, errorType, blockHint)); return publisher.doOnNext(res -> { Optional byteBuf = res.getBody(ByteBuf.class); byteBuf.ifPresent(bb -> { @@ -770,11 +771,16 @@ public Publisher jsonStream(@NonNull io.micronaut.http.HttpRequest @Override public Publisher> exchange(@NonNull io.micronaut.http.HttpRequest request, @NonNull Argument bodyType, @NonNull Argument errorType) { + return exchange(request, bodyType, errorType, null); + } + + @NonNull + private Flux> exchange(io.micronaut.http.HttpRequest request, Argument bodyType, Argument errorType, @Nullable BlockHint blockHint) { setupConversionService(request); final io.micronaut.http.HttpRequest parentRequest = ServerRequestContext.currentRequest().orElse(null); Publisher uriPublisher = resolveRequestURI(request); return Flux.from(uriPublisher) - .switchMap(uri -> exchangeImpl(uri, parentRequest, toMutableRequest(request), bodyType, errorType)); + .switchMap(uri -> exchangeImpl(uri, parentRequest, toMutableRequest(request), bodyType, errorType, blockHint)); } @Override @@ -1024,7 +1030,7 @@ private Flux> connectAndStream( } catch (Exception e) { return Flux.error(e); } - return connectionManager.connect(requestKey).flatMapMany(poolHandle -> { + return connectionManager.connect(requestKey, null).flatMapMany(poolHandle -> { request.setAttribute(NettyClientHttpRequest.CHANNEL, poolHandle.channel); boolean sse = !isProxy && isAcceptEvents(request); @@ -1074,11 +1080,12 @@ public void handlerRemoved(ChannelHandlerContext ctx) throws Exception { * Implementation of {@link #exchange(io.micronaut.http.HttpRequest, Argument, Argument)} (after URI resolution). */ private Publisher> exchangeImpl( - URI requestURI, - io.micronaut.http.HttpRequest parentRequest, - MutableHttpRequest request, - @NonNull Argument bodyType, - @NonNull Argument errorType) { + URI requestURI, + io.micronaut.http.HttpRequest parentRequest, + MutableHttpRequest request, + @NonNull Argument bodyType, + @NonNull Argument errorType, + @Nullable BlockHint blockHint) { AtomicReference> requestWrapper = new AtomicReference<>(request); RequestKey requestKey; @@ -1088,7 +1095,7 @@ private Publisher> exchang return Flux.error(e); } - Mono handlePublisher = connectionManager.connect(requestKey); + Mono handlePublisher = connectionManager.connect(requestKey, blockHint); Flux> responsePublisher = handlePublisher.flatMapMany(poolHandle -> { poolHandle.channel.pipeline() @@ -2168,7 +2175,7 @@ public boolean acceptInboundMessage(Object msg) { @Override protected Function>> makeRedirectHandler(io.micronaut.http.HttpRequest parentRequest, MutableHttpRequest redirectRequest) { - return uri -> exchangeImpl(uri, parentRequest, redirectRequest, bodyType, errorType); + return uri -> exchangeImpl(uri, parentRequest, redirectRequest, bodyType, errorType, null); } @Override diff --git a/http-client/src/main/java/io/micronaut/http/client/netty/PoolResizer.java b/http-client/src/main/java/io/micronaut/http/client/netty/PoolResizer.java index 2a0a28e65c4..cd1e50db915 100644 --- a/http-client/src/main/java/io/micronaut/http/client/netty/PoolResizer.java +++ b/http-client/src/main/java/io/micronaut/http/client/netty/PoolResizer.java @@ -35,7 +35,7 @@ * {@link io.micronaut.http.client.HttpClientConfiguration.ConnectionPoolConfiguration}. *

* This class consists of various mutator methods (e.g. {@link #addPendingRequest}) that - * may be called concurrently and in a reentrant fashion (e.g. inside {@link #openNewConnection()}). + * may be called concurrently and in a reentrant fashion (e.g. inside {@link #openNewConnection}). * These mutator methods update their respective fields and then mark this class as * {@link #dirty()}. The state management logic ensures that {@link #doSomeWork()} is called in a * serialized fashion (no concurrency or reentrancy) at least once after each {@link #dirty()} @@ -50,7 +50,7 @@ abstract class PoolResizer { private final AtomicInteger pendingConnectionCount = new AtomicInteger(0); - private final Deque> pendingRequests = new ConcurrentLinkedDeque<>(); + private final Deque> pendingRequests = new ConcurrentLinkedDeque<>(); private final List http1Connections = new CopyOnWriteArrayList<>(); private final List http2Connections = new CopyOnWriteArrayList<>(); @@ -97,8 +97,9 @@ private void dirty() { } private void doSomeWork() { + BlockHint blockedPendingRequests = null; while (true) { - Sinks.One toDispatch = pendingRequests.pollFirst(); + PoolSink toDispatch = pendingRequests.pollFirst(); if (toDispatch == null) { break; } @@ -119,6 +120,8 @@ private void doSomeWork() { } if (!dispatched) { pendingRequests.addFirst(toDispatch); + blockedPendingRequests = + BlockHint.combine(blockedPendingRequests, toDispatch.getBlockHint()); break; } } @@ -148,7 +151,7 @@ private void doSomeWork() { this.pendingConnectionCount.addAndGet(connectionsToOpen); for (int i = 0; i < connectionsToOpen; i++) { try { - openNewConnection(); + openNewConnection(blockedPendingRequests); } catch (Exception e) { try { onNewConnectionFailure(e); @@ -161,7 +164,7 @@ private void doSomeWork() { } } - private boolean dispatchSafe(ResizerConnection connection, Sinks.One toDispatch) { + private boolean dispatchSafe(ResizerConnection connection, PoolSink toDispatch) { try { return connection.dispatch(toDispatch); } catch (Exception e) { @@ -177,7 +180,7 @@ private boolean dispatchSafe(ResizerConnection connection, Sinks.One sink) { + final void addPendingRequest(PoolSink sink) { if (pendingRequests.size() >= connectionPoolConfiguration.getMaxPendingAcquires()) { sink.tryEmitError(new HttpClientException("Cannot acquire connection, exceeded max pending acquires configuration")); return; @@ -277,6 +280,6 @@ abstract static class ResizerConnection { * @return {@code true} if the acquisition may succeed (if it fails later, the pending * request must be readded), or {@code false} if it fails immediately */ - abstract boolean dispatch(Sinks.One sink) throws Exception; + abstract boolean dispatch(PoolSink sink) throws Exception; } } diff --git a/http-client/src/main/java/io/micronaut/http/client/netty/PoolSink.java b/http-client/src/main/java/io/micronaut/http/client/netty/PoolSink.java new file mode 100644 index 00000000000..f9635913784 --- /dev/null +++ b/http-client/src/main/java/io/micronaut/http/client/netty/PoolSink.java @@ -0,0 +1,33 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.http.client.netty; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.Nullable; +import reactor.core.publisher.Sinks; + +/** + * Sink with an additional optional {@link BlockHint} as metadata. + * + * @param The type that can be submitted to this sink. + * @author Jonas Konrad + * @since 4.0.0 + */ +@Internal +interface PoolSink extends Sinks.One { + @Nullable + BlockHint getBlockHint(); +} diff --git a/http-client/src/test/groovy/io/micronaut/http/client/BlockingDeadlockSpec.groovy b/http-client/src/test/groovy/io/micronaut/http/client/BlockingDeadlockSpec.groovy new file mode 100644 index 00000000000..684123ea2c0 --- /dev/null +++ b/http-client/src/test/groovy/io/micronaut/http/client/BlockingDeadlockSpec.groovy @@ -0,0 +1,56 @@ +package io.micronaut.http.client + +import io.micronaut.context.ApplicationContext +import io.micronaut.http.client.exceptions.HttpClientException +import io.micronaut.http.netty.channel.EventLoopGroupRegistry +import spock.lang.Specification + +import java.util.concurrent.ExecutionException + +class BlockingDeadlockSpec extends Specification { + def 'blocking on the same event loop should fail: connection already established'() { + given: + def ctx = ApplicationContext.run([ + 'micronaut.netty.event-loops.default.num-threads': 1 + ]) + def group = ctx.getBean(EventLoopGroupRegistry).getDefaultEventLoopGroup() + def client = ctx.createBean(HttpClient, 'https://micronaut.io').toBlocking() + + when: + // establish pool connection + client.exchange('/') + group.submit(() -> { + client.exchange('/') + }).get() + then: + def e = thrown ExecutionException + e.cause instanceof HttpClientException + e.cause.message.contains("deadlock") + + cleanup: + client.close() + group.shutdownGracefully() + } + + def 'blocking on the same event loop should fail: new connection'() { + given: + def ctx = ApplicationContext.run([ + 'micronaut.netty.event-loops.default.num-threads': 1 + ]) + def group = ctx.getBean(EventLoopGroupRegistry).getDefaultEventLoopGroup() + def client = ctx.createBean(HttpClient, 'https://micronaut.io').toBlocking() + + when: + group.submit(() -> { + client.exchange('/') + }).get() + then: + def e = thrown ExecutionException + e.cause instanceof HttpClientException + e.cause.message.contains("deadlock") + + cleanup: + client.close() + group.shutdownGracefully() + } +} From 9665a6afe31a670d576edd07d2f6b9cd5636f720 Mon Sep 17 00:00:00 2001 From: Graeme Rocher Date: Fri, 18 Nov 2022 11:49:59 +0100 Subject: [PATCH 231/743] Bump Micronaut Azure to 3.6.0 (#8356) --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 4cb208b192a..7773b946c25 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -68,7 +68,7 @@ managed-micrometer = "1.9.4" managed-micronaut-acme = "3.2.0" managed-micronaut-aot = "1.1.1" managed-micronaut-aws = "3.9.3" -managed-micronaut-azure = "3.5.0" +managed-micronaut-azure = "3.6.0" managed-micronaut-cache = "3.5.0" managed-micronaut-cassandra = "5.1.1" managed-micronaut-coherence = "3.5.1" From fab000858539184c53f83102c007af807a306cf5 Mon Sep 17 00:00:00 2001 From: Graeme Rocher Date: Fri, 18 Nov 2022 12:24:53 +0100 Subject: [PATCH 232/743] upgrade Jackson to 2.14.0 (#8359) --- gradle/libs.versions.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 7773b946c25..789ed5ee1af 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -52,8 +52,8 @@ managed-groovy = "3.0.13" managed-h2 = "1.4.200" managed-hystrix = "1.5.18" managed-jakarta-annotation-api = "2.1.1" -managed-jackson = "2.13.4" -managed-jackson-databind = "2.13.4.2" +managed-jackson = "2.14.0" +managed-jackson-databind = "2.14.0" managed-javax-annotation-api = "1.3.2" managed-jcache = "1.1.1" managed-jna = "5.12.1" From 8edd95d0643279163ea02dafa86c6a72a858fe48 Mon Sep 17 00:00:00 2001 From: Graeme Rocher Date: Fri, 18 Nov 2022 12:25:07 +0100 Subject: [PATCH 233/743] upgrade Jackson version to 2.14.0 (#8358) --- gradle/libs.versions.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index c321bd102e5..64c54e46159 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -64,8 +64,8 @@ wiremock = "2.33.2" # managed-groovy = "4.0.6" managed-jakarta-annotation-api = "2.1.1" -managed-jackson = "2.13.4" -managed-jackson-databind = "2.13.4.2" +managed-jackson = "2.14.0" +managed-jackson-databind = "2.14.0" managed-maven-native-plugin = "0.9.13" managed-methvin-directory-watcher = "0.16.1" From 9da79d80462cdbea4ecceba8f1eadf151e224ca7 Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Fri, 18 Nov 2022 12:55:44 +0100 Subject: [PATCH 234/743] remove test which no longer applies --- .../response/ErrorResponseProcessorSpec.groovy | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/test-suite-groovy/src/test/groovy/io/micronaut/http/server/exceptions/response/ErrorResponseProcessorSpec.groovy b/test-suite-groovy/src/test/groovy/io/micronaut/http/server/exceptions/response/ErrorResponseProcessorSpec.groovy index 31e8b8a723e..0283523c5fc 100644 --- a/test-suite-groovy/src/test/groovy/io/micronaut/http/server/exceptions/response/ErrorResponseProcessorSpec.groovy +++ b/test-suite-groovy/src/test/groovy/io/micronaut/http/server/exceptions/response/ErrorResponseProcessorSpec.groovy @@ -10,20 +10,6 @@ import jakarta.inject.Singleton class ErrorResponseProcessorSpec extends Specification { - def "by default you get a hateoas replacement under groovy"() { - given: - def ctx = ApplicationContext.run() - - when: - def bean = ctx.getBean(ErrorResponseProcessor) - - then: - bean instanceof HateoasErrorResponseProcessorReplacement - - cleanup: - ctx.close() - } - def "default can simply be replaced by binding a different processor"() { given: def ctx = ApplicationContext.run( From 2bf83f63a60e13d02660bf76550d2e5ade137fac Mon Sep 17 00:00:00 2001 From: yawkat Date: Fri, 18 Nov 2022 13:06:44 +0100 Subject: [PATCH 235/743] resolve conflict from #8339 --- .../http/client/netty/ConnectionManager.java | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/http-client/src/main/java/io/micronaut/http/client/netty/ConnectionManager.java b/http-client/src/main/java/io/micronaut/http/client/netty/ConnectionManager.java index 00587ee0cb7..9f7d88c6284 100644 --- a/http-client/src/main/java/io/micronaut/http/client/netty/ConnectionManager.java +++ b/http-client/src/main/java/io/micronaut/http/client/netty/ConnectionManager.java @@ -85,7 +85,9 @@ import reactor.core.publisher.Sinks; import reactor.core.scheduler.Schedulers; +import javax.net.ssl.SSLEngine; import javax.net.ssl.SSLException; +import javax.net.ssl.SSLParameters; import java.net.InetSocketAddress; import java.net.Proxy; import java.net.SocketAddress; @@ -391,9 +393,7 @@ protected void initChannel(@NonNull Channel ch) { SslContext sslContext = buildSslContext(requestKey); if (sslContext != null) { - SslHandler sslHandler = sslContext.newHandler(ch.alloc(), requestKey.getHost(), requestKey.getPort()); - sslHandler.setHandshakeTimeoutMillis(configuration.getSslConfiguration().getHandshakeTimeout().toMillis()); - ch.pipeline().addLast(sslHandler); + ch.pipeline().addLast(configureSslHandler(sslContext.newHandler(ch.alloc(), requestKey.getHost(), requestKey.getPort()))); } ch.pipeline() @@ -503,6 +503,15 @@ private Http2FrameCodec makeFrameCodec() { return builder.build(); } + private SslHandler configureSslHandler(SslHandler sslHandler) { + sslHandler.setHandshakeTimeoutMillis(configuration.getSslConfiguration().getHandshakeTimeout().toMillis()); + SSLEngine engine = sslHandler.engine(); + SSLParameters params = engine.getSSLParameters(); + params.setEndpointIdentificationAlgorithm("HTTPS"); + engine.setSSLParameters(params); + return sslHandler; + } + /** * Initializer for HTTP1.1, called either in plaintext mode, or after ALPN in TLS. * @@ -613,10 +622,8 @@ protected void initChannel(@NonNull Channel ch) { configureProxy(ch.pipeline(), true, host, port); - SslHandler sslHandler = sslContext.newHandler(ch.alloc(), host, port); - sslHandler.setHandshakeTimeoutMillis(configuration.getSslConfiguration().getHandshakeTimeout().toMillis()); ch.pipeline() - .addLast(ChannelPipelineCustomizer.HANDLER_SSL, sslHandler) + .addLast(ChannelPipelineCustomizer.HANDLER_SSL, configureSslHandler(sslContext.newHandler(ch.alloc(), host, port))) .addLast( ChannelPipelineCustomizer.HANDLER_HTTP2_PROTOCOL_NEGOTIATOR, // if the server doesn't do ALPN, fall back to HTTP 1 From 648a8daab4978adee78a73a28a922fd1c21f8681 Mon Sep 17 00:00:00 2001 From: Tim Yates Date: Fri, 18 Nov 2022 12:27:30 +0000 Subject: [PATCH 236/743] Fix test-suite-graal --- gradle/libs.versions.toml | 4 +++- test-suite-graal/build.gradle | 14 +++++++++++--- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index bd0e13f6430..64964feb92d 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -44,7 +44,8 @@ micronaut-grpc = "3.3.1" micronaut-groovy = "4.0.0-SNAPSHOT" micronaut-picocli = "4.3.0" micronaut-sql = "4.7.2" -micronaut-test = "3.7.0" +micronaut-test = "4.0.0-SNAPSHOT" +micronaut-serde = "2.0.0-SNAPSHOT" micronaut-tracing = "4.4.0" micrometer = "1.9.5" neo4j-java-driver = "1.4.5" @@ -210,6 +211,7 @@ logbook-netty = { module = "org.zalando:logbook-netty", version.ref = "logbook-n micronaut-docs = { module = "io.micronaut.docs:micronaut-docs-asciidoc-config-props", version.ref = "micronaut-docs" } micronaut-runtime-groovy = { module = "io.micronaut.groovy:micronaut-runtime-groovy", version.ref = "micronaut-groovy" } +micronaut-serde-jackson = { module = "io.micronaut.serde:micronaut-serde-jackson", version.ref = "micronaut-serde" } micronaut-test-bom = { module = "io.micronaut.test:micronaut-test-bom", version.ref = "micronaut-test" } micronaut-test-core = { module = "io.micronaut.test:micronaut-test-core", version.ref = "micronaut-test" } micronaut-test-junit5 = { module = "io.micronaut.test:micronaut-test-junit5", version.ref = "micronaut-test" } diff --git a/test-suite-graal/build.gradle b/test-suite-graal/build.gradle index 9241c72db18..1e614d8c828 100644 --- a/test-suite-graal/build.gradle +++ b/test-suite-graal/build.gradle @@ -8,6 +8,11 @@ micronautBuild { enableProcessing = false } +repositories { + mavenCentral() + maven { url "https://s01.oss.sonatype.org/content/repositories/snapshots/" } +} + dependencies { annotationProcessor libs.bundles.asm annotationProcessor project(":inject-java") @@ -22,7 +27,8 @@ dependencies { testAnnotationProcessor libs.bundles.asm testAnnotationProcessor project(":inject-java") - testImplementation libs.managed.micronaut.test.junit5 + testImplementation(libs.micronaut.serde.jackson) + testImplementation libs.micronaut.test.junit5 } tasks.withType(Test).configureEach { @@ -30,9 +36,11 @@ tasks.withType(Test).configureEach { } configurations { - // Exclude Groovy from the nativeTestCompilation classpath all { - exclude group: 'org.codehaus.groovy' + // Stop serde pulling in AOP + exclude group: 'io.micronaut', module: 'micronaut-aop' + // Stop pulling in inject-groovy from maven + exclude group: 'io.micronaut', module: 'micronaut-inject-groovy' } nativeImageTestClasspath { exclude module: 'groovy-test' From d673596f88eacf9c0cab59d13bedbf2a67e16902 Mon Sep 17 00:00:00 2001 From: Tim Yates Date: Fri, 18 Nov 2022 12:59:35 +0000 Subject: [PATCH 237/743] Fix build now we use micronaut-test-4.0.0 --- ...o.micronaut.build.internal.convention-base.gradle | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/buildSrc/src/main/groovy/io.micronaut.build.internal.convention-base.gradle b/buildSrc/src/main/groovy/io.micronaut.build.internal.convention-base.gradle index 6e1870c6ba1..b5ab9964769 100644 --- a/buildSrc/src/main/groovy/io.micronaut.build.internal.convention-base.gradle +++ b/buildSrc/src/main/groovy/io.micronaut.build.internal.convention-base.gradle @@ -10,6 +10,18 @@ micronautBuild { enableProcessing = false } +repositories { + mavenCentral() + maven { url "https://s01.oss.sonatype.org/content/repositories/snapshots/" } +} + +configurations { + all { + // Use AOP from here, not from maven + exclude group: 'io.micronaut', module: 'micronaut-aop' + } +} + group = projectGroupId def micronautBuild = (ExtensionAware) project.extensions.getByName("micronautBuild") From e99225be57babb2cba6a2f54d813a076816b39f2 Mon Sep 17 00:00:00 2001 From: Tim Yates Date: Fri, 18 Nov 2022 13:06:29 +0000 Subject: [PATCH 238/743] Fix test-suite-geb which can't work with Micronaut test 4 --- ...micronaut.build.internal.convention-geb-base.gradle | 10 ++++++++++ gradle/libs.versions.toml | 1 + 2 files changed, 11 insertions(+) diff --git a/buildSrc/src/main/groovy/io.micronaut.build.internal.convention-geb-base.gradle b/buildSrc/src/main/groovy/io.micronaut.build.internal.convention-geb-base.gradle index 2c0dc4520fa..50e7fc025b5 100644 --- a/buildSrc/src/main/groovy/io.micronaut.build.internal.convention-geb-base.gradle +++ b/buildSrc/src/main/groovy/io.micronaut.build.internal.convention-geb-base.gradle @@ -14,6 +14,16 @@ micronautBuild { group = projectGroupId +configurations { + testCompileClasspath { + resolutionStrategy.eachDependency { DependencyResolveDetails details -> + if (details.requested.group == 'io.micronaut.test') { + details.useVersion libs.versions.geb.micronaut.test.get() + details.because "Geb doesn't work with Groovy 4" + } + } + } +} def micronautBuild = (ExtensionAware) project.extensions.getByName("micronautBuild") def micronautCore = micronautBuild.extensions.create(MicronautCoreExtension, "core", DefaultMicronautCoreExtension, extensions.findByType(VersionCatalogsExtension)) micronautCore.documented.convention(true) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 64964feb92d..6e0cc84eb97 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -8,6 +8,7 @@ compile-testing = "0.19" geb = "3.4.1" geb-groovy = "3.0.13" +geb-micronaut-test = "3.7.0" geb-spock = "2.2-groovy-3.0" gorm = "7.3.2" # be sure to update graal version in gradle.properties as well From 678bc726976e16932bc2199df978c5f761b1b69a Mon Sep 17 00:00:00 2001 From: Tim Yates Date: Fri, 18 Nov 2022 13:41:57 +0000 Subject: [PATCH 239/743] Forgot testRuntimeClasspath configuration --- .../io.micronaut.build.internal.convention-geb-base.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/buildSrc/src/main/groovy/io.micronaut.build.internal.convention-geb-base.gradle b/buildSrc/src/main/groovy/io.micronaut.build.internal.convention-geb-base.gradle index 50e7fc025b5..f38f03d93ff 100644 --- a/buildSrc/src/main/groovy/io.micronaut.build.internal.convention-geb-base.gradle +++ b/buildSrc/src/main/groovy/io.micronaut.build.internal.convention-geb-base.gradle @@ -15,7 +15,7 @@ micronautBuild { group = projectGroupId configurations { - testCompileClasspath { + all { resolutionStrategy.eachDependency { DependencyResolveDetails details -> if (details.requested.group == 'io.micronaut.test') { details.useVersion libs.versions.geb.micronaut.test.get() From 0717eb9166df0fdab054a0d87aa78725330ab141 Mon Sep 17 00:00:00 2001 From: Jonas Konrad Date: Fri, 18 Nov 2022 15:53:53 +0100 Subject: [PATCH 240/743] Don't modify input map in UriTemplate (#8364) Fixes #8338 --- .../http/client/aop/QueryParametersSpec.groovy | 14 +++++++++++++- .../java/io/micronaut/http/uri/UriTemplate.java | 12 +++++++++--- 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/http-client/src/test/groovy/io/micronaut/http/client/aop/QueryParametersSpec.groovy b/http-client/src/test/groovy/io/micronaut/http/client/aop/QueryParametersSpec.groovy index 4cd249bd05e..4c3b18aa3dd 100644 --- a/http-client/src/test/groovy/io/micronaut/http/client/aop/QueryParametersSpec.groovy +++ b/http-client/src/test/groovy/io/micronaut/http/client/aop/QueryParametersSpec.groovy @@ -27,8 +27,8 @@ import io.micronaut.http.client.HttpClient import io.micronaut.http.client.annotation.Client import io.micronaut.http.client.exceptions.HttpClientResponseException import io.micronaut.runtime.server.EmbeddedServer -import reactor.core.publisher.Flux import spock.lang.AutoCleanup +import spock.lang.Issue import spock.lang.Shared import spock.lang.Specification import spock.lang.Unroll @@ -62,6 +62,18 @@ class QueryParametersSpec extends Specification { flavour << [ "pojo", "singlePojo", "list", "map" ] } + @Issue('https://github.com/micronaut-projects/micronaut-core/issues/8338') + void "test client mappping URL parameters appended through a Map does not modify the Map"() { + when: + // this modification is relatively benign, but if the user passed a Map.of, then trying to remove null leads to + // an exception. Unfortunately we can't test with Map.of. + def map = [term: "Riverside", foo: null] + def result = client.searchExplodedMap("map", map) + then: + result.albums.size() == 2 + map.containsValue(null) + } + @Unroll void "test client mappping multiple URL parameters appended through a Map (served through #flavour)"() { expect: diff --git a/http/src/main/java/io/micronaut/http/uri/UriTemplate.java b/http/src/main/java/io/micronaut/http/uri/UriTemplate.java index c329d4a0f1d..64e844dde49 100644 --- a/http/src/main/java/io/micronaut/http/uri/UriTemplate.java +++ b/http/src/main/java/io/micronaut/http/uri/UriTemplate.java @@ -27,7 +27,6 @@ import java.util.Collections; import java.util.List; import java.util.Map; -import java.util.Objects; import java.util.Optional; import java.util.StringJoiner; import java.util.function.Predicate; @@ -995,7 +994,6 @@ public String expand(Map parameters, boolean previousHasContent, result = joiner.toString(); } else if (found instanceof Map) { Map map = (Map) found; - map.values().removeIf(Objects::isNull); if (map.isEmpty()) { return ""; } @@ -1020,6 +1018,9 @@ public String expand(Map parameters, boolean previousHasContent, } map.forEach((key, some) -> { + if (some == null) { + return; + } String ks = key.toString(); Iterable values = (some instanceof Iterable) ? (Iterable) some : Collections.singletonList(some); for (Object value: values) { @@ -1038,7 +1039,12 @@ public String expand(Map parameters, boolean previousHasContent, } } }); - result = joiner.toString(); + if (joiner.length() == 0) { + // only null entries + return ""; + } else { + result = joiner.toString(); + } } else { String str = found.toString(); str = applyModifier(modifierStr, modifierChar, str, str.length()); From 6f67896eba51751ae92058eccb1cf0dc77bcf79a Mon Sep 17 00:00:00 2001 From: Jonas Konrad Date: Fri, 18 Nov 2022 15:55:05 +0100 Subject: [PATCH 241/743] implement JsonFormat for creator properties (#8365) Fixes #8330 --- .../modules/BeanIntrospectionModule.java | 32 +++++++++++++++++-- .../BeanIntrospectionModuleRecordSpec.groovy | 30 +++++++++++++++++ 2 files changed, 60 insertions(+), 2 deletions(-) diff --git a/jackson-databind/src/main/java/io/micronaut/jackson/modules/BeanIntrospectionModule.java b/jackson-databind/src/main/java/io/micronaut/jackson/modules/BeanIntrospectionModule.java index 936bf048fd3..f374f801583 100644 --- a/jackson-databind/src/main/java/io/micronaut/jackson/modules/BeanIntrospectionModule.java +++ b/jackson-databind/src/main/java/io/micronaut/jackson/modules/BeanIntrospectionModule.java @@ -27,7 +27,19 @@ import com.fasterxml.jackson.core.ObjectCodec; import com.fasterxml.jackson.core.SerializableString; import com.fasterxml.jackson.core.io.SerializedString; -import com.fasterxml.jackson.databind.*; +import com.fasterxml.jackson.databind.BeanDescription; +import com.fasterxml.jackson.databind.DeserializationConfig; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.PropertyMetadata; +import com.fasterxml.jackson.databind.PropertyName; +import com.fasterxml.jackson.databind.PropertyNamingStrategy; +import com.fasterxml.jackson.databind.SerializationConfig; +import com.fasterxml.jackson.databind.SerializerProvider; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.fasterxml.jackson.databind.annotation.JsonNaming; import com.fasterxml.jackson.databind.annotation.JsonSerialize; @@ -291,7 +303,7 @@ public JsonSerializer build() { } } }; - + newBuilder.setAnyGetter(builder.getAnyGetter()); final List properties = builder.getProperties(); final Collection> beanProperties = introspection.getBeanProperties(); @@ -534,6 +546,22 @@ public Object setAndReturn(Object instance, Object value) throws IOException { } return null; } + + @Override + public JsonFormat.Value findPropertyFormat(MapperConfig config, Class baseType) { + JsonFormat.Value v1 = config.getDefaultPropertyFormat(baseType); + JsonFormat.Value v2 = null; + if (property != null) { + AnnotationValue formatAnnotation = property.getAnnotation(JsonFormat.class); + if (formatAnnotation != null) { + v2 = parseJsonFormat(formatAnnotation); + } + } + if (v1 == null) { + return (v2 == null) ? EMPTY_FORMAT : v2; + } + return (v2 == null) ? v1 : v1.withOverrides(v2); + } }; } } diff --git a/jackson-databind/src/test/groovy/io/micronaut/jackson/modules/BeanIntrospectionModuleRecordSpec.groovy b/jackson-databind/src/test/groovy/io/micronaut/jackson/modules/BeanIntrospectionModuleRecordSpec.groovy index 73d21158c0e..83fc3eaa690 100644 --- a/jackson-databind/src/test/groovy/io/micronaut/jackson/modules/BeanIntrospectionModuleRecordSpec.groovy +++ b/jackson-databind/src/test/groovy/io/micronaut/jackson/modules/BeanIntrospectionModuleRecordSpec.groovy @@ -8,6 +8,9 @@ import io.micronaut.context.annotation.Requires import io.micronaut.core.beans.BeanIntrospection import jakarta.inject.Singleton import spock.lang.IgnoreIf +import spock.lang.Issue + +import java.time.LocalDateTime @IgnoreIf({ !jvm.isJava14Compatible() }) class BeanIntrospectionModuleRecordSpec extends AbstractTypeElementSpec { @@ -36,6 +39,33 @@ record Test(String foo, String bar) { ignoreReflectiveProperties << [true, false] } + @Issue('https://github.com/micronaut-projects/micronaut-core/issues/8330') + def 'JsonFormat'() { + given: + BeanIntrospection introspection = buildBeanIntrospection('test.Test', ''' +package test; +import java.time.LocalDateTime; +import com.fasterxml.jackson.annotation.JsonFormat; +import io.micronaut.core.annotation.Introspected; + +@Introspected +record Test(@JsonFormat(pattern = "dd.MM.yyyy HH:mm:ss") LocalDateTime date) { +} +''') + def ctx = ApplicationContext.run(['spec.name': 'BeanIntrospectionModuleRecordSpec']) + ctx.getBean(StaticBeanIntrospectionModule).introspectionMap[introspection.beanType] = introspection + ctx.getBean(BeanIntrospectionModule).ignoreReflectiveProperties = ignoreReflectiveProperties + def mapper = ctx.getBean(ObjectMapper) + + when: + def value = mapper.readValue('{"date":"13.11.2022 22:44:55"}', introspection.beanType) + then: + value.date == LocalDateTime.of(2022, 11, 13, 22, 44, 55) + + where: + ignoreReflectiveProperties << [true, false] + } + @Singleton @Replaces(BeanIntrospectionModule) @Requires(property = "spec.name", value = 'BeanIntrospectionModuleRecordSpec') From 36d968932c0473e534cdcf85866ea00bf96ed20a Mon Sep 17 00:00:00 2001 From: Graeme Rocher Date: Fri, 18 Nov 2022 16:55:02 +0100 Subject: [PATCH 242/743] build: bump up slf4j to 2.0.4 and logback to 1.4.4 (#8360) --- gradle/libs.versions.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 6e0cc84eb97..340babafb86 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -37,7 +37,7 @@ junit5 = "5.9.1" kotlin = "1.7.20" kotlin-coroutines = "1.6.4" ktor = "1.6.8" -logback = "1.2.11" +logback = "1.4.4" logbook-netty = "2.14.0" log4j = "2.19.0" micronaut-aws = "3.9.2" @@ -74,7 +74,7 @@ managed-netty = "4.1.84.Final" managed-reactive-streams = "1.0.4" # This should be kept aligned with https://github.com/micronaut-projects/micronaut-reactor/blob/master/gradle.properties from the BOM managed-reactor = "3.4.24" -managed-slf4j = "1.7.36" +managed-slf4j = "2.0.4" managed-validation = "2.0.1.Final" micronaut-docs = "2.0.0" From 68a9be5ff7fb3a881c774403909199cb93784d71 Mon Sep 17 00:00:00 2001 From: Tim Yates Date: Fri, 18 Nov 2022 18:05:48 +0000 Subject: [PATCH 243/743] Exclude maven copy of aop for testRuntime (#8369) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously, due to laziness I'd excluded it it from every configuration, including publishing 😳 --- .../groovy/io.micronaut.build.internal.convention-base.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/buildSrc/src/main/groovy/io.micronaut.build.internal.convention-base.gradle b/buildSrc/src/main/groovy/io.micronaut.build.internal.convention-base.gradle index b5ab9964769..4d4ec8530d3 100644 --- a/buildSrc/src/main/groovy/io.micronaut.build.internal.convention-base.gradle +++ b/buildSrc/src/main/groovy/io.micronaut.build.internal.convention-base.gradle @@ -16,7 +16,7 @@ repositories { } configurations { - all { + testRuntimeClasspath { // Use AOP from here, not from maven exclude group: 'io.micronaut', module: 'micronaut-aop' } From 80ad4288a8ea17098dfc8a1d3a2265954c688fa6 Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Sun, 20 Nov 2022 05:57:22 +0100 Subject: [PATCH 244/743] test: Req attrs has routeMatch for WebSocketServer (#8284) --- .../websocket/WebsocketRouteMatchSpec.groovy | 141 ++++++++++++++++++ 1 file changed, 141 insertions(+) create mode 100644 http-server-netty/src/test/groovy/io/micronaut/websocket/WebsocketRouteMatchSpec.groovy diff --git a/http-server-netty/src/test/groovy/io/micronaut/websocket/WebsocketRouteMatchSpec.groovy b/http-server-netty/src/test/groovy/io/micronaut/websocket/WebsocketRouteMatchSpec.groovy new file mode 100644 index 00000000000..0e40fb3c66e --- /dev/null +++ b/http-server-netty/src/test/groovy/io/micronaut/websocket/WebsocketRouteMatchSpec.groovy @@ -0,0 +1,141 @@ +package io.micronaut.websocket + +import io.micronaut.context.annotation.Property +import io.micronaut.context.annotation.Requires +import io.micronaut.http.HttpAttributes +import io.micronaut.http.HttpRequest +import io.micronaut.http.HttpResponse +import io.micronaut.http.MutableHttpRequest +import io.micronaut.http.MutableHttpResponse +import io.micronaut.http.annotation.Filter +import io.micronaut.http.filter.HttpServerFilter +import io.micronaut.http.filter.ServerFilterChain +import io.micronaut.runtime.server.EmbeddedServer +import io.micronaut.test.extensions.spock.annotation.MicronautTest +import io.micronaut.web.router.RouteMatch +import io.micronaut.websocket.annotation.ClientWebSocket +import io.micronaut.websocket.annotation.OnClose +import io.micronaut.websocket.annotation.OnMessage +import io.micronaut.websocket.annotation.OnOpen +import io.micronaut.websocket.annotation.ServerWebSocket +import jakarta.inject.Inject +import org.reactivestreams.Publisher +import reactor.core.publisher.Flux +import reactor.core.publisher.Mono +import spock.lang.Specification +import spock.util.concurrent.PollingConditions + +import java.util.function.Predicate +import java.util.stream.Collectors + +@Property(name = "spec.name", value = "WebsocketRouteMatchSpec") +@MicronautTest +class WebsocketRouteMatchSpec extends Specification { + + @Inject + EmbeddedServer embeddedServer + + void "request attributes contains a route match for WebSocketServer"() { + given: + WebSocketClient wsClient = embeddedServer.applicationContext.createBean(WebSocketClient.class, embeddedServer.getURL()) + + expect: + wsClient + + when: + MutableHttpRequest request = HttpRequest.GET("/echo") + EchoClientWebSocket echoClientWebSocket = Flux.from(wsClient.connect(EchoClientWebSocket, request)).blockFirst() + + then: + noExceptionThrown() + new PollingConditions().eventually { + echoClientWebSocket.receivedMessages() == ['joined!'] + } + + when: + echoClientWebSocket.send('Hello') + + then: + new PollingConditions().eventually { + echoClientWebSocket.receivedMessages() == ['joined!', 'Hello'] + } + + cleanup: + echoClientWebSocket.close() + } + + @Requires(property = "spec.name", value = "WebsocketRouteMatchSpec") + @ServerWebSocket("/echo") + static class EchoServerWebSocket { + public static final String JOINED = "joined!" + public static final String DISCONNECTED = "Disconnected!" + + @Inject + WebSocketBroadcaster broadcaster + + @OnOpen + void onOpen(WebSocketSession session) { + broadcaster.broadcastSync(JOINED, isValid(session)) + } + + @OnMessage + void onMessage(String message, WebSocketSession session) { + broadcaster.broadcastSync(message, isValid(session)) + } + + @OnClose + void onClose(WebSocketSession session) { + broadcaster.broadcastSync(DISCONNECTED, isValid(session)) + } + + private static Predicate isValid(WebSocketSession session) { + return { s -> s == session } + } + } + + @Requires(property = "spec.name", value = "WebsocketRouteMatchSpec") + @Filter(Filter.MATCH_ALL_PATTERN) + static class SecurityFilter implements HttpServerFilter { + @Override + Publisher> doFilter(HttpRequest request, ServerFilterChain chain) { + RouteMatch routeMatch = request.getAttribute(HttpAttributes.ROUTE_MATCH, RouteMatch.class).orElse(null) + routeMatch != null ? chain.proceed(request) : Mono.just(HttpResponse.serverError()) + } + } + + @Requires(property = "spec.name", value = "WebsocketRouteMatchSpec") + @ClientWebSocket("/echo") + static abstract class EchoClientWebSocket implements AutoCloseable { + + static final String RECEIVED = "RECEIVED:" + + private WebSocketSession session + private List replies = new ArrayList<>() + + @OnOpen + void onOpen(WebSocketSession session) { + this.session = session + } + List getReplies() { + return replies + } + + @OnMessage + void onMessage(String message) { + replies.add(RECEIVED + message) + } + + abstract void send(String message) + + List receivedMessages() { + return filterMessagesByType(RECEIVED) + } + + List filterMessagesByType(String type) { + replies.stream() + .filter(str -> str.contains(type)) + .map(str -> str.replaceAll(type, "")) + .collect(Collectors.toList()) + } + } +} From 59e695b919e1be9cc8a3fc2b51884763db90e208 Mon Sep 17 00:00:00 2001 From: Denis Stepanov Date: Mon, 21 Nov 2022 15:47:04 +0700 Subject: [PATCH 245/743] Fix introduction method order (#8386) --- ...troductionInterfaceBeanElementCreator.java | 19 +--- .../IntroductionGenericTypesSpec.groovy | 46 ++++---- ...roductionAdviceWithNewInterfaceSpec.groovy | 101 +++++++++++++++++- .../io/micronaut/aop/introduction/Stub.groovy | 2 +- .../aop/introduction/StubIntroducer.groovy | 9 +- .../compile/IntroductionAnnotationSpec.groovy | 23 ++-- .../IntroductionGenericTypesSpec.groovy | 32 +++--- ...roductionAdviceWithNewInterfaceSpec.groovy | 101 +++++++++++++++++- .../aop/introduction/StubIntroducer.java | 6 ++ 9 files changed, 266 insertions(+), 73 deletions(-) diff --git a/core-processor/src/main/java/io/micronaut/inject/processing/IntroductionInterfaceBeanElementCreator.java b/core-processor/src/main/java/io/micronaut/inject/processing/IntroductionInterfaceBeanElementCreator.java index 01c712bccc5..c0a7f7899a5 100644 --- a/core-processor/src/main/java/io/micronaut/inject/processing/IntroductionInterfaceBeanElementCreator.java +++ b/core-processor/src/main/java/io/micronaut/inject/processing/IntroductionInterfaceBeanElementCreator.java @@ -22,9 +22,9 @@ import io.micronaut.inject.visitor.VisitorContext; import io.micronaut.inject.writer.BeanDefinitionVisitor; -import java.util.HashSet; +import java.util.ArrayList; +import java.util.Collections; import java.util.List; -import java.util.Set; /** * Introduction interface proxy builder. @@ -71,20 +71,11 @@ public void buildInternal() { // The introduction will include overridden methods* (find(List) <- find(Iterable)*) but ordinary class introduction doesn't // Because of the caching we need to process declared methods first - Set processed = new HashSet<>(); - List declaredEnclosedElements = classElement.getEnclosedElements(ElementQuery.ALL_METHODS.includeHiddenElements().includeOverriddenMethods().onlyDeclared()); - for (MethodElement methodElement : declaredEnclosedElements) { + List methods = new ArrayList<>(classElement.getEnclosedElements(ElementQuery.ALL_METHODS.includeHiddenElements().includeOverriddenMethods())); + Collections.reverse(methods); // reverse to process hierarchy starting from declared methods + for (MethodElement methodElement : methods) { aopHelper.visitIntrospectedMethod(aopProxyWriter, classElement, methodElement); - processed.add(methodElement); } - List otherEnclosedElements = classElement.getEnclosedElements(ElementQuery.ALL_METHODS.includeHiddenElements().includeOverriddenMethods()); - for (MethodElement methodElement : otherEnclosedElements) { - if (processed.contains(methodElement)) { - continue; - } - aopHelper.visitIntrospectedMethod(aopProxyWriter, classElement, methodElement); - } - beanDefinitionWriters.add(aopProxyWriter); } diff --git a/inject-groovy/src/test/groovy/io/micronaut/aop/compile/IntroductionGenericTypesSpec.groovy b/inject-groovy/src/test/groovy/io/micronaut/aop/compile/IntroductionGenericTypesSpec.groovy index 8770026f2a2..ad13591b3ce 100644 --- a/inject-groovy/src/test/groovy/io/micronaut/aop/compile/IntroductionGenericTypesSpec.groovy +++ b/inject-groovy/src/test/groovy/io/micronaut/aop/compile/IntroductionGenericTypesSpec.groovy @@ -41,7 +41,7 @@ interface MyInterface { @Executable T getURL(); - + @Executable java.util.List getURLs(); } @@ -58,11 +58,13 @@ interface MyBean extends MyInterface { beanDefinition != null beanDefinition.injectedFields.size() == 0 beanDefinition.executableMethods.size() == 2 - beanDefinition.executableMethods[0].methodName == 'getURL' - beanDefinition.executableMethods[0].returnType.type == URL - beanDefinition.executableMethods[1].returnType.type == List - beanDefinition.executableMethods[1].returnType.asArgument().hasTypeVariables() - beanDefinition.executableMethods[1].returnType.asArgument().typeVariables['E'].type == URL + def getUrlMethod = beanDefinition.executableMethods.find { it.name == "getURL" } + getUrlMethod.methodName == 'getURL' + getUrlMethod.returnType.type == URL + def getUrlsMethod = beanDefinition.executableMethods.find { it.name == "getURLs" } + getUrlsMethod.returnType.type == List + getUrlsMethod.returnType.asArgument().hasTypeVariables() + getUrlsMethod.returnType.asArgument().typeVariables['E'].type == URL } @@ -78,7 +80,7 @@ import java.net.*; interface MyInterface { @Executable reactor.core.publisher.Mono> getPeopleSingle(); - + @Executable T[] getPeopleArray(); @@ -87,21 +89,21 @@ interface MyInterface { @Executable T getPerson(); - + @Executable java.util.List getPeople(); - + @Executable void save(T person); - + @Executable void saveAll(java.util.List person); - + @Executable java.util.List getPeopleListArray(); - - - + + + } @@ -169,24 +171,24 @@ interface MyInterface { @Executable reactor.core.publisher.Mono> getPeopleSingle(); - + @Executable T getPerson(); - + @Executable java.util.List getPeople(); - + @Executable void save(T person); - + @Executable void saveAll(java.util.List person); - + @Executable java.util.List getPeopleListArray(); - - - + + + } diff --git a/inject-groovy/src/test/groovy/io/micronaut/aop/introduction/IntroductionAdviceWithNewInterfaceSpec.groovy b/inject-groovy/src/test/groovy/io/micronaut/aop/introduction/IntroductionAdviceWithNewInterfaceSpec.groovy index 7e5de9684e1..bbadb5696a8 100644 --- a/inject-groovy/src/test/groovy/io/micronaut/aop/introduction/IntroductionAdviceWithNewInterfaceSpec.groovy +++ b/inject-groovy/src/test/groovy/io/micronaut/aop/introduction/IntroductionAdviceWithNewInterfaceSpec.groovy @@ -124,8 +124,8 @@ import io.micronaut.context.annotation.*; interface MyBean3 { @Executable - String getBar(); - + String getBar(); + } ''') @@ -171,10 +171,10 @@ class Generic { } class Specific extends Generic { } -interface GenericInterface { +interface GenericInterface { Generic getObject() } -interface SpecificInterface { +interface SpecificInterface { Specific getObject() } ''') @@ -193,4 +193,97 @@ interface SpecificInterface { //having the target interface in the bytecode of the test instance.$proxyMethods.length == 2 } + + void "test interface multiple inheritance"() { + when: + BeanDefinition beanDefinition = buildBeanDefinition('test.MyInterfaceX' + BeanDefinitionVisitor.PROXY_SUFFIX, ''' +package test; + +import io.micronaut.aop.introduction.*; +import io.micronaut.context.annotation.*; +import io.micronaut.context.annotation.Executable +import java.lang.annotation.Documented +import java.lang.annotation.Retention + +import static java.lang.annotation.RetentionPolicy.RUNTIME + +@Stub +@jakarta.inject.Singleton +interface MyInterfaceX extends MyInterface2 { + + @MyAnn + String myMethod5(String param); + + default String myMethod6(String param) { + return myMethod3(param) + } +} + +interface MyInterface2 extends MyInterface3, MyInterface4 { + + @MyAnn + String myMethod1(String param); + + @MyAnn + @Override + String myMethod3(String param); + + @Override + String myMethod2(String param); + + @MyAnn + @Override + String myMethod4(String param); + + default String myMethod7(String param) { + return myMethod4(param) + } +} + +interface MyInterface3 { + @MyAnn + String myMethod2(String param); +} + +interface MyInterface4 { + String myMethod3(String param); + + String myMethod4(String param); +} + +@Documented +@Retention(RUNTIME) +@Executable +@interface MyAnn { +} +''') + + then: + noExceptionThrown() + beanDefinition != null + + when: + def context = new DefaultBeanContext() + context.start() + def instance = ((BeanFactory)beanDefinition).build(context, beanDefinition) + def introducer = context.getBean(StubIntroducer) + then: + instance.myMethod1("abc1") == "abc1" + introducer.visitedMethods["myMethod1"].hasAnnotation("test.MyAnn") + instance.myMethod2("abc2") == "abc2" + !introducer.visitedMethods["myMethod2"].hasAnnotation("test.MyAnn") + instance.myMethod3("abc3") == "abc3" + introducer.visitedMethods["myMethod3"].hasAnnotation("test.MyAnn") + instance.myMethod4("abc4") == "abc4" + introducer.visitedMethods["myMethod4"].hasAnnotation("test.MyAnn") + instance.myMethod5("abc5") == "abc5" + introducer.visitedMethods["myMethod5"].hasAnnotation("test.MyAnn") + instance.myMethod6("abc6") == "abc6" // Calls method3 + introducer.visitedMethods["myMethod3"].hasAnnotation("test.MyAnn") + instance.myMethod7("abc7") == "abc7" // Calls method4 + introducer.visitedMethods["myMethod4"].hasAnnotation("test.MyAnn") + + cleanup: + context.close() + } } diff --git a/inject-groovy/src/test/groovy/io/micronaut/aop/introduction/Stub.groovy b/inject-groovy/src/test/groovy/io/micronaut/aop/introduction/Stub.groovy index e2edb76f2f5..cd5b7971be7 100644 --- a/inject-groovy/src/test/groovy/io/micronaut/aop/introduction/Stub.groovy +++ b/inject-groovy/src/test/groovy/io/micronaut/aop/introduction/Stub.groovy @@ -33,5 +33,5 @@ import static java.lang.annotation.RetentionPolicy.RUNTIME @Documented @Retention(RUNTIME) @Executable -@interface Stub { +public @interface Stub { } diff --git a/inject-groovy/src/test/groovy/io/micronaut/aop/introduction/StubIntroducer.groovy b/inject-groovy/src/test/groovy/io/micronaut/aop/introduction/StubIntroducer.groovy index 89b1123662b..21aea868179 100644 --- a/inject-groovy/src/test/groovy/io/micronaut/aop/introduction/StubIntroducer.groovy +++ b/inject-groovy/src/test/groovy/io/micronaut/aop/introduction/StubIntroducer.groovy @@ -17,6 +17,7 @@ package io.micronaut.aop.introduction import io.micronaut.aop.MethodInterceptor import io.micronaut.aop.MethodInvocationContext +import io.micronaut.core.annotation.AnnotationMetadata import io.micronaut.core.annotation.Nullable import io.micronaut.core.type.MutableArgumentValue import jakarta.inject.Singleton @@ -26,12 +27,16 @@ import jakarta.inject.Singleton * @since 1.0 */ @Singleton -class StubIntroducer implements MethodInterceptor { +class StubIntroducer implements MethodInterceptor { + + public Map visitedMethods = new LinkedHashMap<>() + @Nullable @Override Object intercept(MethodInvocationContext context) { + visitedMethods.put(context.getMethodName(), context.getAnnotationMetadata()) Iterator> iterator = context.getParameters().values().iterator() - if(iterator.hasNext()) + if (iterator.hasNext()) return iterator.next().getValue() return null diff --git a/inject-java/src/test/groovy/io/micronaut/aop/compile/IntroductionAnnotationSpec.groovy b/inject-java/src/test/groovy/io/micronaut/aop/compile/IntroductionAnnotationSpec.groovy index 38b7b965891..db4b5af08d7 100644 --- a/inject-java/src/test/groovy/io/micronaut/aop/compile/IntroductionAnnotationSpec.groovy +++ b/inject-java/src/test/groovy/io/micronaut/aop/compile/IntroductionAnnotationSpec.groovy @@ -76,11 +76,11 @@ import io.micronaut.aop.simple.Mutating; @NotImplemented abstract class MyBean { abstract void test(); - + public String test2() { return "good"; } - + @Mutating("arg") public String test3(String arg) { return arg; @@ -142,15 +142,16 @@ interface MyBean extends MyInterface { beanDefinition != null beanDefinition.injectedFields.size() == 0 beanDefinition.executableMethods.size() == 2 - beanDefinition.executableMethods[0].methodName == 'save' - beanDefinition.executableMethods[0].returnType.type == void.class - beanDefinition.executableMethods[0].arguments[0].getAnnotationMetadata().hasAnnotation(NotBlank) - beanDefinition.executableMethods[0].arguments[1].getAnnotationMetadata().hasAnnotation(Min) - beanDefinition.executableMethods[0].arguments[1].getAnnotationMetadata().getValue(Min, Integer).get() == 1 - - beanDefinition.executableMethods[1].methodName == 'saveTwo' - beanDefinition.executableMethods[1].returnType.type == void.class - beanDefinition.executableMethods[1].arguments[0].getAnnotationMetadata().hasAnnotation(Min) + def saveMethod = beanDefinition.executableMethods.find { it.name == "save" } + saveMethod.methodName == 'save' + saveMethod.returnType.type == void.class + saveMethod.arguments[0].getAnnotationMetadata().hasAnnotation(NotBlank) + saveMethod.arguments[1].getAnnotationMetadata().hasAnnotation(Min) + saveMethod.arguments[1].getAnnotationMetadata().getValue(Min, Integer).get() == 1 + def saveTwoMethod = beanDefinition.executableMethods.find { it.name == "saveTwo" } + saveTwoMethod.methodName == 'saveTwo' + saveTwoMethod.returnType.type == void.class + saveTwoMethod.arguments[0].getAnnotationMetadata().hasAnnotation(Min) } diff --git a/inject-java/src/test/groovy/io/micronaut/aop/compile/IntroductionGenericTypesSpec.groovy b/inject-java/src/test/groovy/io/micronaut/aop/compile/IntroductionGenericTypesSpec.groovy index 1c69964d1d5..91854c39df8 100644 --- a/inject-java/src/test/groovy/io/micronaut/aop/compile/IntroductionGenericTypesSpec.groovy +++ b/inject-java/src/test/groovy/io/micronaut/aop/compile/IntroductionGenericTypesSpec.groovy @@ -40,7 +40,7 @@ import java.net.*; interface MyInterface { T getURL(); - + java.util.List getURLs(); } @@ -57,12 +57,14 @@ interface MyBean extends MyInterface { beanDefinition != null beanDefinition.injectedFields.size() == 0 beanDefinition.executableMethods.size() == 2 - beanDefinition.executableMethods[0].methodName == 'getURL' - beanDefinition.executableMethods[0].targetMethod.returnType == URL - beanDefinition.executableMethods[0].returnType.type == URL - beanDefinition.executableMethods[1].returnType.type == List - beanDefinition.executableMethods[1].returnType.asArgument().hasTypeVariables() - beanDefinition.executableMethods[1].returnType.asArgument().typeVariables['E'].type == URL + def getUrlMethod = beanDefinition.executableMethods.find { it.name == "getURL" } + getUrlMethod.methodName == 'getURL' + getUrlMethod.targetMethod.returnType == URL + getUrlMethod.returnType.type == URL + def getUrlsMethod = beanDefinition.executableMethods.find { it.name == "getURLs" } + getUrlsMethod.returnType.type == List + getUrlsMethod.returnType.asArgument().hasTypeVariables() + getUrlsMethod.returnType.asArgument().typeVariables['E'].type == URL } @@ -78,21 +80,21 @@ import java.net.*; interface MyInterface { reactor.core.publisher.Mono> getPeopleSingle(); - + T getPerson(); - + java.util.List getPeople(); - + void save(T person); - + void saveAll(java.util.List person); - + T[] getPeopleArray(); - + java.util.List getPeopleListArray(); - + java.util.Map getPeopleMap(); - + } diff --git a/inject-java/src/test/groovy/io/micronaut/aop/introduction/IntroductionAdviceWithNewInterfaceSpec.groovy b/inject-java/src/test/groovy/io/micronaut/aop/introduction/IntroductionAdviceWithNewInterfaceSpec.groovy index 7021fce6a39..91ca8bc011a 100644 --- a/inject-java/src/test/groovy/io/micronaut/aop/introduction/IntroductionAdviceWithNewInterfaceSpec.groovy +++ b/inject-java/src/test/groovy/io/micronaut/aop/introduction/IntroductionAdviceWithNewInterfaceSpec.groovy @@ -127,8 +127,8 @@ import io.micronaut.context.annotation.*; interface MyBean { @Executable - String getBar(); - + String getBar(); + @Executable default String getFoo() { return "good"; } } @@ -179,10 +179,10 @@ class Generic { } class Specific extends Generic { } -interface GenericInterface { +interface GenericInterface { Generic getObject(); } -interface SpecificInterface { +interface SpecificInterface { Specific getObject(); } ''') @@ -201,4 +201,97 @@ interface SpecificInterface { //having the target interface in the bytecode of the test instance.$proxyMethods.length == 2 } + + void "test interface multiple inheritance"() { + when: + BeanDefinition beanDefinition = buildBeanDefinition('test.MyInterfaceX' + BeanDefinitionVisitor.PROXY_SUFFIX, ''' +package test; + +import io.micronaut.aop.introduction.*; +import io.micronaut.context.annotation.*; +import io.micronaut.context.annotation.Executable; +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; + +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +@Stub +@jakarta.inject.Singleton +interface MyInterfaceX extends MyInterface2 { + + @MyAnn + String myMethod5(String param); + + default String myMethod6(String param) { + return myMethod3(param); + } +} + +interface MyInterface2 extends MyInterface3, MyInterface4 { + + @MyAnn + String myMethod1(String param); + + @MyAnn + @Override + String myMethod3(String param); + + @Override + String myMethod2(String param); + + @MyAnn + @Override + String myMethod4(String param); + + default String myMethod7(String param) { + return myMethod4(param); + } +} + +interface MyInterface3 { + @MyAnn + String myMethod2(String param); +} + +interface MyInterface4 { + String myMethod3(String param); + + String myMethod4(String param); +} + +@Documented +@Retention(RUNTIME) +@Executable +@interface MyAnn { +} +''') + + then: + noExceptionThrown() + beanDefinition != null + + when: + def context = new DefaultBeanContext() + context.start() + def instance = ((BeanFactory)beanDefinition).build(context, beanDefinition) + def introducer = context.getBean(StubIntroducer) + then: + instance.myMethod1("abc1") == "abc1" + introducer.visitedMethods["myMethod1"].hasAnnotation("test.MyAnn") + instance.myMethod2("abc2") == "abc2" + !introducer.visitedMethods["myMethod2"].hasAnnotation("test.MyAnn") + instance.myMethod3("abc3") == "abc3" + introducer.visitedMethods["myMethod3"].hasAnnotation("test.MyAnn") + instance.myMethod4("abc4") == "abc4" + introducer.visitedMethods["myMethod4"].hasAnnotation("test.MyAnn") + instance.myMethod5("abc5") == "abc5" + introducer.visitedMethods["myMethod5"].hasAnnotation("test.MyAnn") + instance.myMethod6("abc6") == "abc6" // Calls method3 + introducer.visitedMethods["myMethod3"].hasAnnotation("test.MyAnn") + instance.myMethod7("abc7") == "abc7" // Calls method4 + introducer.visitedMethods["myMethod4"].hasAnnotation("test.MyAnn") + + cleanup: + context.close() + } } diff --git a/inject-java/src/test/groovy/io/micronaut/aop/introduction/StubIntroducer.java b/inject-java/src/test/groovy/io/micronaut/aop/introduction/StubIntroducer.java index 700b5806721..d80b67ea062 100644 --- a/inject-java/src/test/groovy/io/micronaut/aop/introduction/StubIntroducer.java +++ b/inject-java/src/test/groovy/io/micronaut/aop/introduction/StubIntroducer.java @@ -17,11 +17,14 @@ import io.micronaut.aop.MethodInterceptor; import io.micronaut.aop.MethodInvocationContext; +import io.micronaut.core.annotation.AnnotationMetadata; import io.micronaut.core.annotation.Nullable; import io.micronaut.core.type.MutableArgumentValue; import jakarta.inject.Singleton; import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.Map; /** * @author Graeme Rocher @@ -30,6 +33,8 @@ @Singleton public class StubIntroducer implements MethodInterceptor { + public Map visitedMethods = new LinkedHashMap<>(); + public static final int POSITION = 0; @Override @@ -40,6 +45,7 @@ public int getOrder() { @Nullable @Override public Object intercept(MethodInvocationContext context) { + visitedMethods.put(context.getMethodName(), context.getAnnotationMetadata()); Iterator> iterator = context.getParameters().values().iterator(); if(iterator.hasNext()) return iterator.next().getValue(); From 80a4a0f9af0949b4dbd189423aaf3a25a64e2729 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 21 Nov 2022 10:22:46 +0100 Subject: [PATCH 246/743] fix(deps): update dependency ch.qos.logback:logback-classic to v1.4.5 (#8372) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 340babafb86..f9d60c4df36 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -37,7 +37,7 @@ junit5 = "5.9.1" kotlin = "1.7.20" kotlin-coroutines = "1.6.4" ktor = "1.6.8" -logback = "1.4.4" +logback = "1.4.5" logbook-netty = "2.14.0" log4j = "2.19.0" micronaut-aws = "3.9.2" From 4c50dd6aa214393fa7b18b956ba753c8baf6423c Mon Sep 17 00:00:00 2001 From: Jonas Konrad Date: Mon, 21 Nov 2022 15:48:53 +0100 Subject: [PATCH 247/743] Still emit response if return type is Void (#8367) Before this patch, HttpClient would remove the actual response from the response publisher if the expected body type is Void. I don't see a reason to do this, and it leads to HTTP filters not seeing the response as they should. Fixes #8366 --- .../io/micronaut/http/client/HttpClient.java | 8 +++- .../HttpClientIntroductionAdvice.java | 4 +- .../http/client/netty/DefaultHttpClient.java | 11 +++-- .../http/client/aop/ClientFilterSpec.groovy | 46 ++++++++++++++++++- 4 files changed, 60 insertions(+), 9 deletions(-) diff --git a/http-client-core/src/main/java/io/micronaut/http/client/HttpClient.java b/http-client-core/src/main/java/io/micronaut/http/client/HttpClient.java index b54aa145806..fdba6c1d9bb 100644 --- a/http-client-core/src/main/java/io/micronaut/http/client/HttpClient.java +++ b/http-client-core/src/main/java/io/micronaut/http/client/HttpClient.java @@ -152,9 +152,15 @@ default Publisher> exchange(@NonNull HttpRequest reque * @param The error type * @return A {@link Publisher} that emits a result of the given type */ + @SuppressWarnings("unchecked") default Publisher retrieve(@NonNull HttpRequest request, @NonNull Argument bodyType, @NonNull Argument errorType) { // note: this default impl isn't used by us anymore, it's overridden by DefaultHttpClient - return Flux.from(exchange(request, bodyType, errorType)).map(response -> { + Flux> exchange = Flux.from(exchange(request, bodyType, errorType)); + if (bodyType.getType() == void.class) { + // exchange() returns a HttpResponse, we can't map the Void body properly, so just drop it and complete + return (Publisher) exchange.ignoreElements(); + } + return exchange.map(response -> { if (bodyType.getType() == HttpStatus.class) { return (O) response.getStatus(); } else { diff --git a/http-client-core/src/main/java/io/micronaut/http/client/interceptor/HttpClientIntroductionAdvice.java b/http-client-core/src/main/java/io/micronaut/http/client/interceptor/HttpClientIntroductionAdvice.java index e13c3b90578..24ccc2f94c6 100644 --- a/http-client-core/src/main/java/io/micronaut/http/client/interceptor/HttpClientIntroductionAdvice.java +++ b/http-client-core/src/main/java/io/micronaut/http/client/interceptor/HttpClientIntroductionAdvice.java @@ -55,8 +55,8 @@ import io.micronaut.http.annotation.Produces; import io.micronaut.http.client.BlockingHttpClient; import io.micronaut.http.client.HttpClient; -import io.micronaut.http.client.ReactiveClientResultTransformer; import io.micronaut.http.client.HttpClientRegistry; +import io.micronaut.http.client.ReactiveClientResultTransformer; import io.micronaut.http.client.StreamingHttpClient; import io.micronaut.http.client.annotation.Client; import io.micronaut.http.client.bind.ClientArgumentRequestBinder; @@ -426,7 +426,7 @@ private Publisher httpClientResponsePublisher(HttpClient httpClient, MutableHttp Class argumentType = reactiveValueArgument.getType(); if (Void.class == argumentType || returnType.isVoid()) { request.getHeaders().remove(HttpHeaders.ACCEPT); - return httpClient.exchange(request, Argument.VOID, errorType); + return httpClient.retrieve(request, Argument.VOID, errorType); } else { if (HttpResponse.class.isAssignableFrom(argumentType)) { return httpClient.exchange(request, reactiveValueArgument, errorType); diff --git a/http-client/src/main/java/io/micronaut/http/client/netty/DefaultHttpClient.java b/http-client/src/main/java/io/micronaut/http/client/netty/DefaultHttpClient.java index cc78bab9ecb..c79922938a7 100644 --- a/http-client/src/main/java/io/micronaut/http/client/netty/DefaultHttpClient.java +++ b/http-client/src/main/java/io/micronaut/http/client/netty/DefaultHttpClient.java @@ -760,7 +760,12 @@ public Publisher> exchange(@NonNull @Override public Publisher retrieve(io.micronaut.http.HttpRequest request, Argument bodyType, Argument errorType) { // mostly same as default impl, but with exception customization - return Flux.from(exchange(request, bodyType, errorType)).map(response -> { + Flux> exchange = Flux.from(exchange(request, bodyType, errorType)); + if (bodyType.getType() == void.class) { + // exchange() returns a HttpResponse, we can't map the Void body properly, so just drop it and complete + return (Publisher) exchange.ignoreElements(); + } + return exchange.map(response -> { if (bodyType.getType() == HttpStatus.class) { return (O) response.getStatus(); } else { @@ -1495,10 +1500,6 @@ private void sendRequestThroughChannel( new FullHttpResponseHandler<>(responsePromise, poolHandle, secure, finalRequest, bodyType, errorType)); poolHandle.notifyRequestPipelineBuilt(); Publisher> publisher = new NettyFuturePublisher<>(responsePromise, true); - if (bodyType != null && bodyType.isVoid()) { - // don't emit response if bodyType is void - publisher = Flux.from(publisher).filter(r -> false); - } publisher.subscribe(new ForwardingSubscriber<>(emitter)); requestWriter.write(channel, secure, emitter); diff --git a/http-client/src/test/groovy/io/micronaut/http/client/aop/ClientFilterSpec.groovy b/http-client/src/test/groovy/io/micronaut/http/client/aop/ClientFilterSpec.groovy index eb57296ac5a..d6efb6145ae 100644 --- a/http-client/src/test/groovy/io/micronaut/http/client/aop/ClientFilterSpec.groovy +++ b/http-client/src/test/groovy/io/micronaut/http/client/aop/ClientFilterSpec.groovy @@ -17,6 +17,7 @@ package io.micronaut.http.client.aop import io.micronaut.context.ApplicationContext import io.micronaut.context.annotation.Requires +import io.micronaut.core.async.annotation.SingleResult import io.micronaut.http.HttpResponse import io.micronaut.http.HttpVersion import io.micronaut.http.MediaType @@ -32,8 +33,9 @@ import io.micronaut.http.filter.ClientFilterChain import io.micronaut.http.filter.HttpClientFilter import io.micronaut.runtime.server.EmbeddedServer import org.reactivestreams.Publisher +import reactor.core.publisher.Flux +import reactor.core.publisher.Mono import spock.lang.AutoCleanup -import spock.lang.Shared import spock.lang.Specification /** @@ -267,4 +269,46 @@ class ClientFilterSpec extends Specification{ throw new RuntimeException("from filter") } } + + void "filter always observes a response"() { + given: + ObservesResponseClient client = context.getBean(ObservesResponseClient) + ObservesResponseFilter filter = context.getBean(ObservesResponseFilter) + + when: + Mono.from(client.monoVoid()).block() == null + then: + filter.observedResponse != null + } + + @Requires(property = 'spec.name', value = "ClientFilterSpec") + @Client('/observes-response') + static interface ObservesResponseClient { + + @Get + @SingleResult + Publisher monoVoid() + } + + @Requires(property = 'spec.name', value = "ClientFilterSpec") + @Filter("/observes-response/**") + static class ObservesResponseFilter implements HttpClientFilter { + HttpResponse observedResponse + + @Override + Publisher> doFilter(MutableHttpRequest request, ClientFilterChain chain) { + return Flux.from(chain.proceed(request)).doOnNext(r -> { + observedResponse = r + }) + } + } + + @Requires(property = 'spec.name', value = "ClientFilterSpec") + @Controller('/observes-response') + static class ObservesResponseController { + @Get + String index() { + return "" + } + } } From 5d6e69a7f343d9a8b18c87de6c74a4ffd893d98d Mon Sep 17 00:00:00 2001 From: Graeme Rocher Date: Mon, 21 Nov 2022 16:59:38 +0100 Subject: [PATCH 248/743] Bump Micronaut Oracle Cloud to 2.3.0 (#8389) --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 789ed5ee1af..24fd29c921a 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -101,7 +101,7 @@ managed-micronaut-nats = "3.1.0" managed-micronaut-netflix = "2.1.0" managed-micronaut-object-storage = "1.1.0" managed-micronaut-openapi = "4.5.2" -managed-micronaut-oraclecloud = "2.2.1" +managed-micronaut-oraclecloud = "2.3.0" managed-micronaut-picocli = "4.3.0" managed-micronaut-problem = "2.5.1" managed-micronaut-rabbitmq = "3.3.0" From b9d08c838377810b4fca8bf40f4b6310cbda6c44 Mon Sep 17 00:00:00 2001 From: Tim Yates Date: Mon, 21 Nov 2022 16:43:08 +0000 Subject: [PATCH 249/743] Make Snakeyaml a managed dependency (#8390) We have a lot of modules including mn.snakeyaml, so exposing this in the core bom will prevent duplication all over the place --- gradle/libs.versions.toml | 5 +++-- inject-groovy/build.gradle | 2 +- inject-java/build.gradle | 2 +- inject/build.gradle | 4 ++-- jackson-databind/build.gradle | 2 +- runtime/build.gradle | 2 +- test-utils/build.gradle | 2 +- 7 files changed, 10 insertions(+), 9 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index f9d60c4df36..08e317f8af7 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -52,7 +52,6 @@ micrometer = "1.9.5" neo4j-java-driver = "1.4.5" selenium = "3.141.59" smallrye = "5.5.0" -snakeyaml = "1.33" spock = "2.2-groovy-4.0" spotbugs = "4.7.1" systemlambda = "1.2.1" @@ -75,6 +74,7 @@ managed-reactive-streams = "1.0.4" # This should be kept aligned with https://github.com/micronaut-projects/micronaut-reactor/blob/master/gradle.properties from the BOM managed-reactor = "3.4.24" managed-slf4j = "2.0.4" +managed-snakeyaml = "1.33" managed-validation = "2.0.1.Final" micronaut-docs = "2.0.0" @@ -130,6 +130,8 @@ managed-reactor = { module = "io.projectreactor:reactor-core", version.ref = "ma managed-slf4j = { module = "org.slf4j:slf4j-api", version.ref = "managed-slf4j" } managed-slf4j-simple = { module = "org.slf4j:slf4j-simple", version.ref = "managed-slf4j" } +managed-snakeyaml = { module = "org.yaml:snakeyaml", version.ref = "managed-snakeyaml" } + managed-validation = { module = "javax.validation:validation-api", version.ref = "managed-validation" } # @@ -240,7 +242,6 @@ selenium-driver-firefox = { module = "org.seleniumhq.selenium:selenium-firefox-d selenium-driver-htmlunit = { module = "org.seleniumhq.selenium:htmlunit-driver", version.ref = "htmlunit" } smallrye = { module = "io.smallrye:smallrye-fault-tolerance", version.ref = "smallrye" } -snakeyaml = { module = "org.yaml:snakeyaml", version.ref = "snakeyaml" } spock = { module = "org.spockframework:spock-core", version.ref = "spock" } spotbugs = { module = "com.github.spotbugs:spotbugs-annotations", version.ref = "spotbugs" } diff --git a/inject-groovy/build.gradle b/inject-groovy/build.gradle index b02d060b897..e9facfce7c9 100644 --- a/inject-groovy/build.gradle +++ b/inject-groovy/build.gradle @@ -29,7 +29,7 @@ dependencies { testImplementation(libs.neo4j.bolt) testImplementation libs.managed.groovy.json testImplementation libs.blaze.persistence.core - testImplementation libs.snakeyaml + testImplementation libs.managed.snakeyaml testImplementation libs.managed.reactor functionalTestImplementation(testFixtures(project(":test-suite"))) diff --git a/inject-java/build.gradle b/inject-java/build.gradle index f0d7735903b..e8e9b8795a5 100644 --- a/inject-java/build.gradle +++ b/inject-java/build.gradle @@ -45,7 +45,7 @@ dependencies { exclude module: 'micronaut-runtime' } testImplementation libs.javax.annotation.api - testImplementation libs.snakeyaml + testImplementation libs.managed.snakeyaml testRuntimeOnly libs.javax.el.impl testRuntimeOnly libs.javax.el } diff --git a/inject/build.gradle b/inject/build.gradle index 2a4cef16bf7..1245659e695 100644 --- a/inject/build.gradle +++ b/inject/build.gradle @@ -16,7 +16,7 @@ dependencies { api libs.managed.jakarta.annotation.api api project(':core') - compileOnly libs.snakeyaml + compileOnly libs.managed.snakeyaml compileOnly libs.managed.groovy compileOnly libs.kotlin.stdlib.jdk8 compileOnly libs.managed.validation @@ -26,7 +26,7 @@ dependencies { testImplementation project(":inject-groovy") testImplementation project(":inject-test-utils") testImplementation libs.systemlambda - testImplementation libs.snakeyaml + testImplementation libs.managed.snakeyaml testRuntimeOnly libs.junit.jupiter.engine } diff --git a/jackson-databind/build.gradle b/jackson-databind/build.gradle index 279321c1649..9589907b211 100644 --- a/jackson-databind/build.gradle +++ b/jackson-databind/build.gradle @@ -27,7 +27,7 @@ dependencies { testImplementation project(":inject-java-test") testImplementation project(":inject-groovy") testImplementation "com.fasterxml.jackson.dataformat:jackson-dataformat-xml" - testImplementation libs.snakeyaml + testImplementation libs.managed.snakeyaml if (!JavaVersion.current().isJava9Compatible()) { testImplementation files(org.gradle.internal.jvm.Jvm.current().toolsJar) } diff --git a/runtime/build.gradle b/runtime/build.gradle index 8c9f28814a5..d356280d799 100644 --- a/runtime/build.gradle +++ b/runtime/build.gradle @@ -25,7 +25,7 @@ dependencies { compileOnly libs.kotlinx.coroutines.core compileOnly libs.kotlinx.coroutines.reactive testImplementation libs.logback - testImplementation libs.snakeyaml + testImplementation libs.managed.snakeyaml testAnnotationProcessor project(":inject-java") testImplementation libs.jsr107 testImplementation libs.jcache diff --git a/test-utils/build.gradle b/test-utils/build.gradle index f2ac601a3af..a9f8fab93c2 100644 --- a/test-utils/build.gradle +++ b/test-utils/build.gradle @@ -4,5 +4,5 @@ plugins { dependencies { api libs.managed.groovy - testImplementation libs.snakeyaml + testImplementation libs.managed.snakeyaml } From 6d403d8e6c77fc5c8582167a986cd881823098d5 Mon Sep 17 00:00:00 2001 From: Denis Stepanov Date: Tue, 22 Nov 2022 15:59:12 +0700 Subject: [PATCH 250/743] Warn when bean definition class is excluded because of the error (#8385) --- .../processing/BeanDefinitionInjectProcessor.java | 13 ++++++++++--- .../processing/PostponeToNextRoundException.java | 11 +++++++++++ .../processing/visitor/JavaElementFactory.java | 7 ++++--- .../processing/visitor/JavaVisitorContext.java | 15 +++++++++++++++ 4 files changed, 40 insertions(+), 6 deletions(-) diff --git a/inject-java/src/main/java/io/micronaut/annotation/processing/BeanDefinitionInjectProcessor.java b/inject-java/src/main/java/io/micronaut/annotation/processing/BeanDefinitionInjectProcessor.java index 5abed35eefa..791fc5e13b1 100644 --- a/inject-java/src/main/java/io/micronaut/annotation/processing/BeanDefinitionInjectProcessor.java +++ b/inject-java/src/main/java/io/micronaut/annotation/processing/BeanDefinitionInjectProcessor.java @@ -48,9 +48,11 @@ import javax.lang.model.element.TypeElement; import javax.lang.model.type.TypeMirror; import java.io.IOException; +import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashSet; import java.util.List; +import java.util.Map; import java.util.Set; import java.util.stream.Collectors; @@ -96,8 +98,8 @@ public class BeanDefinitionInjectProcessor extends AbstractInjectAnnotationProce }; private Set beanDefinitions; - private boolean processingOver; private final Set processed = new HashSet<>(); + private final Map postponed = new HashMap<>(); @Override public final synchronized void init(ProcessingEnvironment processingEnv) { @@ -145,8 +147,7 @@ public boolean includeTypeLevelAnnotationsInGenericArguments() { @Override public final boolean process(Set annotations, RoundEnvironment roundEnv) { - processingOver = roundEnv.processingOver(); - + boolean processingOver = roundEnv.processingOver(); if (!processingOver) { JavaAnnotationMetadataBuilder annotationMetadataBuilder = javaVisitorContext.getAnnotationMetadataBuilder(); annotations = annotations @@ -204,6 +205,7 @@ public final boolean process(Set annotations, RoundEnviro // remove already processed in previous round for (String name : processed) { beanDefinitions.remove(name); + postponed.remove(name); } // process remaining @@ -236,6 +238,7 @@ public final boolean process(Set annotations, RoundEnviro error((Element) ex.getOriginatingElement(), ex.getMessage()); } catch (PostponeToNextRoundException e) { processed.remove(className); + postponed.put(className, (Element) e.getErrorElement()); } } } @@ -247,6 +250,10 @@ public final boolean process(Set annotations, RoundEnviro processing round. */ if (processingOver) { + for (Map.Entry e : postponed.entrySet()) { + javaVisitorContext.warn("Bean definition generation [" + e.getKey() + "] skipped from processing because of prior error. This error is normally due to missing classes on the classpath. Verify the compilation classpath is correct to resolve the problem.", e.getValue()); + } + try { writeBeanDefinitionsToMetaInf(); for (BeanElementVisitor visitor : BeanElementVisitor.VISITORS) { diff --git a/inject-java/src/main/java/io/micronaut/annotation/processing/PostponeToNextRoundException.java b/inject-java/src/main/java/io/micronaut/annotation/processing/PostponeToNextRoundException.java index 4d7fcfbff20..174c61cac7e 100644 --- a/inject-java/src/main/java/io/micronaut/annotation/processing/PostponeToNextRoundException.java +++ b/inject-java/src/main/java/io/micronaut/annotation/processing/PostponeToNextRoundException.java @@ -19,6 +19,17 @@ * Exception to indicate postponing processing to next round. */ public final class PostponeToNextRoundException extends RuntimeException { + + private final transient Object errorElement; + + public PostponeToNextRoundException(Object originatingElement) { + this.errorElement = originatingElement; + } + + public Object getErrorElement() { + return errorElement; + } + @Override public synchronized Throwable fillInStackTrace() { // no-op: flow control exception diff --git a/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaElementFactory.java b/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaElementFactory.java index 6ea85a5a828..3d9c70f8268 100644 --- a/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaElementFactory.java +++ b/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaElementFactory.java @@ -315,16 +315,17 @@ private void failIfPostponeIsNeeded(ExecutableElement executableElement) { for (VariableElement parameter : parameters) { failIfPostponeIsNeeded(parameter); } - TypeKind returnKind = executableElement.getReturnType().getKind(); + TypeMirror returnType = executableElement.getReturnType(); + TypeKind returnKind = returnType.getKind(); if (returnKind == TypeKind.ERROR) { - throw new PostponeToNextRoundException(); + throw new PostponeToNextRoundException(returnType); } } private void failIfPostponeIsNeeded(VariableElement variableElement) { TypeMirror type = variableElement.asType(); if (type.getKind() == TypeKind.ERROR) { - throw new PostponeToNextRoundException(); + throw new PostponeToNextRoundException(variableElement); } } diff --git a/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaVisitorContext.java b/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaVisitorContext.java index 4f0b2a011ac..2b5b4b50cc0 100644 --- a/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaVisitorContext.java +++ b/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaVisitorContext.java @@ -242,6 +242,21 @@ public void warn(String message, @Nullable io.micronaut.inject.ast.Element eleme printMessage(message, Diagnostic.Kind.WARNING, element); } + /** + * Print warning message. + * + * @param message The message + * @param element The element + * @since 4.0.0 + */ + public void warn(String message, @Nullable Element element) { + if (element == null) { + messager.printMessage(Diagnostic.Kind.WARNING, message); + } else { + messager.printMessage(Diagnostic.Kind.WARNING, message, element); + } + } + private void printMessage(String message, Diagnostic.Kind kind, @Nullable io.micronaut.inject.ast.Element element) { if (StringUtils.isNotEmpty(message)) { if (element instanceof BeanElement) { From 6304eaecd0930c22b04c002040a1919b607270d5 Mon Sep 17 00:00:00 2001 From: Denis Stepanov Date: Tue, 22 Nov 2022 16:38:40 +0700 Subject: [PATCH 251/743] Refactor bean processing module after inject module split (#8394) --- .../micronaut/aop/writer/AopHelperImpl.java | 291 ------------------ .../micronaut/aop/writer/AopProxyWriter.java | 2 + .../context/visitor/BeanImportVisitor.java | 2 + .../visitor/ContextConfigurerVisitor.java | 2 + .../AbstractAnnotationMetadataBuilder.java | 3 +- .../annotation/AnnotationMetadataWriter.java | 2 +- .../internal/CoreNonNullTransformer.java | 2 + .../internal/JakartaNonnullTransformer.java | 2 + .../internal/JakartaNullableTransformer.java | 2 + .../internal/KotlinNotNullMapper.java | 2 + .../internal/KotlinNullableMapper.java | 2 + .../PersistenceContextAnnotationMapper.java | 2 +- .../io/micronaut/inject/ast/ClassElement.java | 6 +- .../inject/ast/PrimitiveElement.java | 8 +- .../micronaut/inject/ast/PropertyElement.java | 4 +- .../inject/ast/ReflectParameterElement.java | 2 + .../inject/ast/SimplePackageElement.java | 3 + ...tractElementAnnotationMetadataFactory.java | 24 +- .../visitor/BeanIntrospectionWriter.java | 7 +- .../EntityIntrospectedAnnotationMapper.java | 2 + ...ntityReflectiveAccessAnnotationMapper.java | 2 + ...trospectedToBeanPropertiesTransformer.java | 2 + .../IntrospectedTypeElementVisitor.java | 2 +- .../visitor/JsonCreatorAnnotationMapper.java | 2 + .../MappedSuperClassIntrospectionMapper.java | 2 + .../configuration/ConfigurationUtils.java | 7 +- .../AbstractBeanElementCreator.java | 172 +++++++---- .../inject/processing/AopHelper.java | 52 ---- ...ctionProxySupportedBeanElementCreator.java | 11 +- .../BeanDefinitionCreatorFactory.java | 55 +--- ...ConfigurationReaderBeanElementCreator.java | 2 +- .../DeclaredBeanElementCreator.java | 148 ++++++++- .../processing/FactoryBeanElementCreator.java | 11 +- ...troductionInterfaceBeanElementCreator.java | 12 +- .../inject/processing/JavaModelUtils.java | 3 +- .../AbstractAnnotationMetadataWriter.java | 2 +- .../writer/BeanDefinitionReferenceWriter.java | 1 - .../inject/writer/BeanDefinitionWriter.java | 16 +- .../DirectoryClassWriterOutputVisitor.java | 5 +- 39 files changed, 341 insertions(+), 536 deletions(-) delete mode 100644 core-processor/src/main/java/io/micronaut/aop/writer/AopHelperImpl.java delete mode 100644 core-processor/src/main/java/io/micronaut/inject/processing/AopHelper.java diff --git a/core-processor/src/main/java/io/micronaut/aop/writer/AopHelperImpl.java b/core-processor/src/main/java/io/micronaut/aop/writer/AopHelperImpl.java deleted file mode 100644 index 48e289fa50a..00000000000 --- a/core-processor/src/main/java/io/micronaut/aop/writer/AopHelperImpl.java +++ /dev/null @@ -1,291 +0,0 @@ -/* - * Copyright 2017-2022 original authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.micronaut.aop.writer; - -import io.micronaut.aop.Adapter; -import io.micronaut.aop.Interceptor; -import io.micronaut.aop.InterceptorKind; -import io.micronaut.aop.Introduction; -import io.micronaut.aop.internal.intercepted.InterceptedMethodUtil; -import io.micronaut.core.annotation.AnnotationClassValue; -import io.micronaut.core.annotation.AnnotationMetadata; -import io.micronaut.core.annotation.AnnotationUtil; -import io.micronaut.core.annotation.Internal; -import io.micronaut.core.annotation.NextMajorVersion; -import io.micronaut.core.util.ArrayUtils; -import io.micronaut.core.util.StringUtils; -import io.micronaut.core.value.OptionalValues; -import io.micronaut.inject.processing.ProcessingException; -import io.micronaut.inject.annotation.AnnotationMetadataHierarchy; -import io.micronaut.inject.ast.ClassElement; -import io.micronaut.inject.ast.ElementQuery; -import io.micronaut.inject.ast.GenericPlaceholderElement; -import io.micronaut.inject.ast.MethodElement; -import io.micronaut.inject.ast.ParameterElement; -import io.micronaut.inject.ast.TypedElement; -import io.micronaut.inject.processing.AopHelper; -import io.micronaut.inject.processing.JavaModelUtils; -import io.micronaut.inject.visitor.VisitorContext; -import io.micronaut.inject.writer.BeanDefinitionVisitor; -import io.micronaut.inject.writer.BeanDefinitionWriter; - -import java.util.Arrays; -import java.util.Collections; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.concurrent.atomic.AtomicInteger; - -/** - * AOP helper to connect Inject module with AOP. - * - * @author Denis Stepanov - * @since 4.0.0 - */ -@Internal -@NextMajorVersion("Correct project dependency so this hack is not needed") -public final class AopHelperImpl implements AopHelper { - - private static final String MSG_ADAPTER_METHOD_PREFIX = "Cannot adapt method ["; - private static final String MSG_TARGET_METHOD_PREFIX = "] to target method ["; - - @Override - public BeanDefinitionVisitor visitAdaptedMethod(ClassElement classElement, - MethodElement sourceMethod, - AtomicInteger adaptedMethodIndex, - VisitorContext visitorContext) { - - AnnotationMetadata methodAnnotationMetadata = sourceMethod.getDeclaredMetadata(); - - Optional interfaceToAdaptValue = methodAnnotationMetadata.getValue(Adapter.class, String.class) - .flatMap(clazz -> visitorContext.getClassElement(clazz, visitorContext.getElementAnnotationMetadataFactory().readOnly())); - - if (interfaceToAdaptValue.isEmpty()) { - return null; - } - ClassElement interfaceToAdapt = interfaceToAdaptValue.get(); - if (!interfaceToAdapt.isInterface()) { - throw new ProcessingException(sourceMethod, "Class to adapt [" + interfaceToAdapt.getName() + "] is not an interface"); - } - - String rootName = classElement.getSimpleName() + '$' + interfaceToAdapt.getSimpleName() + '$' + sourceMethod.getSimpleName(); - String beanClassName = rootName + adaptedMethodIndex.incrementAndGet(); - - AopProxyWriter aopProxyWriter = new AopProxyWriter( - classElement.getPackageName(), - beanClassName, - true, - false, - sourceMethod, - new AnnotationMetadataHierarchy(classElement.getAnnotationMetadata(), methodAnnotationMetadata), - new ClassElement[]{interfaceToAdapt}, - visitorContext - ); - - aopProxyWriter.visitDefaultConstructor(methodAnnotationMetadata, visitorContext); - - List methods = interfaceToAdapt.getEnclosedElements(ElementQuery.ALL_METHODS.onlyAbstract()); - if (methods.isEmpty()) { - throw new ProcessingException(sourceMethod, "Interface to adapt [" + interfaceToAdapt.getName() + "] is not a SAM type. No methods found."); - } - if (methods.size() > 1) { - throw new ProcessingException(sourceMethod, "Interface to adapt [" + interfaceToAdapt.getName() + "] is not a SAM type. More than one abstract method declared."); - } - - MethodElement targetMethod = methods.iterator().next(); - - ParameterElement[] sourceParams = sourceMethod.getParameters(); - ParameterElement[] targetParams = targetMethod.getParameters(); - - int paramLen = targetParams.length; - if (paramLen != sourceParams.length) { - throw new ProcessingException(sourceMethod, MSG_ADAPTER_METHOD_PREFIX + sourceMethod + MSG_TARGET_METHOD_PREFIX + targetMethod + "]. Argument lengths don't match."); - } - if (sourceMethod.isSuspend()) { - throw new ProcessingException(sourceMethod, MSG_ADAPTER_METHOD_PREFIX + sourceMethod + MSG_TARGET_METHOD_PREFIX + targetMethod + "]. Kotlin suspend method not supported here."); - } - - Map typeVariables = interfaceToAdapt.getTypeArguments(); - Map genericTypes = new LinkedHashMap<>(paramLen); - for (int i = 0; i < paramLen; i++) { - ParameterElement targetParam = targetParams[i]; - ParameterElement sourceParam = sourceParams[i]; - - ClassElement targetType = targetParam.getType(); - ClassElement targetGenericType = targetParam.getGenericType(); - ClassElement sourceType = sourceParam.getGenericType(); - - // ??? Java returns generic placeholder for the generic type and Groovy from the ordinary type - if (targetGenericType instanceof GenericPlaceholderElement) { - GenericPlaceholderElement genericPlaceholderElement = (GenericPlaceholderElement) targetGenericType; - String variableName = genericPlaceholderElement.getVariableName(); - if (typeVariables.containsKey(variableName)) { - genericTypes.put(variableName, sourceType); - } - } else if (targetType instanceof GenericPlaceholderElement) { - GenericPlaceholderElement genericPlaceholderElement = (GenericPlaceholderElement) targetType; - String variableName = genericPlaceholderElement.getVariableName(); - if (typeVariables.containsKey(variableName)) { - genericTypes.put(variableName, sourceType); - } - } - - if (!sourceType.isAssignable(targetGenericType.getName())) { - throw new ProcessingException(sourceMethod, MSG_ADAPTER_METHOD_PREFIX + sourceMethod + MSG_TARGET_METHOD_PREFIX + targetMethod + "]. Type [" + sourceType.getName() + "] is not a subtype of type [" + targetGenericType.getName() + "] for argument at position " + i); - } - } - - if (!genericTypes.isEmpty()) { - aopProxyWriter.visitTypeArguments(Collections.singletonMap(interfaceToAdapt.getName(), genericTypes)); - } - - AnnotationClassValue[] adaptedArgumentTypes = Arrays.stream(sourceParams) - .map(p -> new AnnotationClassValue<>(JavaModelUtils.getClassname(p.getGenericType()))) - .toArray(AnnotationClassValue[]::new); - - targetMethod = targetMethod.withNewOwningType(classElement); - - targetMethod.annotate(Adapter.class, builder -> { - builder.member(Adapter.InternalAttributes.ADAPTED_BEAN, new AnnotationClassValue<>(JavaModelUtils.getClassname(classElement))); - builder.member(Adapter.InternalAttributes.ADAPTED_METHOD, sourceMethod.getName()); - builder.member(Adapter.InternalAttributes.ADAPTED_ARGUMENT_TYPES, adaptedArgumentTypes); - String qualifier = classElement.stringValue(AnnotationUtil.NAMED).orElse(null); - if (StringUtils.isNotEmpty(qualifier)) { - builder.member(Adapter.InternalAttributes.ADAPTED_QUALIFIER, qualifier); - } - }); - - aopProxyWriter.visitAroundMethod(interfaceToAdapt, targetMethod); - - return aopProxyWriter; - } - - @Override - public boolean visitIntrospectedMethod(BeanDefinitionVisitor visitor, ClassElement typeElement, MethodElement methodElement) { - AopProxyWriter aopProxyWriter = (AopProxyWriter) visitor; - - final AnnotationMetadata resolvedTypeMetadata = typeElement.getAnnotationMetadata(); - final boolean resolvedTypeMetadataIsAopProxyType = InterceptedMethodUtil.hasDeclaredAroundAdvice(resolvedTypeMetadata); - - if (methodElement.isAbstract() - || resolvedTypeMetadataIsAopProxyType - || InterceptedMethodUtil.hasDeclaredAroundAdvice(methodElement.getAnnotationMetadata())) { - addToIntroduction(aopProxyWriter, typeElement, methodElement, false); - return true; - } - return false; - } - - @Override - public AopProxyWriter createIntroductionAopProxyWriter(ClassElement typeElement, - VisitorContext visitorContext) { - AnnotationMetadata annotationMetadata = typeElement.getAnnotationMetadata(); - - String packageName = typeElement.getPackageName(); - String beanClassName = typeElement.getSimpleName(); - io.micronaut.core.annotation.AnnotationValue[] aroundInterceptors = - InterceptedMethodUtil.resolveInterceptorBinding(annotationMetadata, InterceptorKind.AROUND); - io.micronaut.core.annotation.AnnotationValue[] introductionInterceptors = InterceptedMethodUtil.resolveInterceptorBinding(annotationMetadata, InterceptorKind.INTRODUCTION); - - ClassElement[] interfaceTypes = Arrays.stream(annotationMetadata.getValue(Introduction.class, "interfaces", String[].class).orElse(new String[0])) - .map(v -> visitorContext.getClassElement(v, visitorContext.getElementAnnotationMetadataFactory().readOnly()) - .orElseThrow(() -> new ProcessingException(typeElement, "Cannot find interface: " + v)) - ).toArray(ClassElement[]::new); - - io.micronaut.core.annotation.AnnotationValue[] interceptorTypes = ArrayUtils.concat(aroundInterceptors, introductionInterceptors); - boolean isInterface = typeElement.isInterface(); - AopProxyWriter aopProxyWriter = new AopProxyWriter( - packageName, - beanClassName, - isInterface, - typeElement, - annotationMetadata, - interfaceTypes, - visitorContext, - interceptorTypes); - - Arrays.stream(interfaceTypes) - .flatMap(interfaceElement -> interfaceElement.getEnclosedElements(ElementQuery.ALL_METHODS).stream()) - .forEach(methodElement -> addToIntroduction(aopProxyWriter, typeElement, methodElement.withNewOwningType(typeElement), true)); - - return aopProxyWriter; - } - - @Override - public AopProxyWriter createAroundAopProxyWriter(BeanDefinitionVisitor existingWriter, - AnnotationMetadata aopElementAnnotationProcessor, - VisitorContext visitorContext, - boolean forceProxyTarget) { - OptionalValues aroundSettings = aopElementAnnotationProcessor.getValues(AnnotationUtil.ANN_AROUND, Boolean.class); - Map settings = new LinkedHashMap<>(); - for (CharSequence setting : aroundSettings) { - Optional entry = aroundSettings.get(setting); - entry.ifPresent(val -> settings.put(setting, val)); - } - if (forceProxyTarget) { - settings.put(Interceptor.PROXY_TARGET, true); - } - aroundSettings = OptionalValues.of(Boolean.class, settings); - - return new AopProxyWriter( - (BeanDefinitionWriter) existingWriter, - aroundSettings, - visitorContext, - InterceptedMethodUtil.resolveInterceptorBinding(aopElementAnnotationProcessor, InterceptorKind.AROUND) - ); - } - - private static void addToIntroduction(AopProxyWriter aopProxyWriter, - ClassElement classElement, - MethodElement methodElement, - boolean ignoreNotAbstract) { - AnnotationMetadata methodAnnotationMetadata = methodElement.getDeclaredMetadata(); - - if (InterceptedMethodUtil.hasAroundStereotype(methodAnnotationMetadata)) { - aopProxyWriter.visitInterceptorBinding( - InterceptedMethodUtil.resolveInterceptorBinding(methodAnnotationMetadata, InterceptorKind.AROUND) - ); - } - - if (!classElement.getName().equals(methodElement.getDeclaringType().getName())) { - aopProxyWriter.addOriginatingElement(methodElement.getDeclaringType()); - } - - ClassElement declaringType = methodElement.getDeclaringType(); - if (methodElement.isAbstract()) { - aopProxyWriter.visitIntroductionMethod(declaringType, methodElement); - } else if (!ignoreNotAbstract) { - boolean isInterface = methodElement.getDeclaringType().isInterface(); - boolean isDefault = methodElement.isDefault(); - if (isInterface && isDefault) { - // Default methods cannot be "super" accessed on the defined type - declaringType = classElement; - } - // only apply around advise to non-abstract methods of introduction advise - aopProxyWriter.visitAroundMethod(declaringType, methodElement); - } - } - - @Override - public void visitAroundMethod(BeanDefinitionVisitor existingWriter, TypedElement beanType, MethodElement methodElement) { - AopProxyWriter aopProxyWriter = (AopProxyWriter) existingWriter; - aopProxyWriter.visitInterceptorBinding( - InterceptedMethodUtil.resolveInterceptorBinding(methodElement.getAnnotationMetadata(), InterceptorKind.AROUND) - ); - aopProxyWriter.visitAroundMethod(beanType, methodElement); - } -} diff --git a/core-processor/src/main/java/io/micronaut/aop/writer/AopProxyWriter.java b/core-processor/src/main/java/io/micronaut/aop/writer/AopProxyWriter.java index 63e4e01dfaa..7487bc464b1 100644 --- a/core-processor/src/main/java/io/micronaut/aop/writer/AopProxyWriter.java +++ b/core-processor/src/main/java/io/micronaut/aop/writer/AopProxyWriter.java @@ -34,6 +34,7 @@ import io.micronaut.core.annotation.AnnotationMetadata; import io.micronaut.core.annotation.AnnotationUtil; import io.micronaut.core.annotation.AnnotationValue; +import io.micronaut.core.annotation.Internal; import io.micronaut.core.annotation.NonNull; import io.micronaut.core.reflect.ReflectionUtils; import io.micronaut.core.type.Argument; @@ -94,6 +95,7 @@ * @author Graeme Rocher * @since 1.0 */ +@Internal public class AopProxyWriter extends AbstractClassFileWriter implements ProxyingBeanDefinitionVisitor, Toggleable { public static final int MAX_LOCALS = 3; diff --git a/core-processor/src/main/java/io/micronaut/context/visitor/BeanImportVisitor.java b/core-processor/src/main/java/io/micronaut/context/visitor/BeanImportVisitor.java index d76a8d4a4b3..d4558f9dccb 100644 --- a/core-processor/src/main/java/io/micronaut/context/visitor/BeanImportVisitor.java +++ b/core-processor/src/main/java/io/micronaut/context/visitor/BeanImportVisitor.java @@ -18,6 +18,7 @@ import io.micronaut.context.annotation.Bean; import io.micronaut.context.annotation.Import; import io.micronaut.core.annotation.AnnotationUtil; +import io.micronaut.core.annotation.Internal; import io.micronaut.core.annotation.NonNull; import io.micronaut.core.order.OrderUtil; import io.micronaut.core.util.ArrayUtils; @@ -41,6 +42,7 @@ * @author graemerocher * @since 3.0.0 */ +@Internal public class BeanImportVisitor implements TypeElementVisitor { private static final List BEAN_IMPORT_HANDLERS; diff --git a/core-processor/src/main/java/io/micronaut/context/visitor/ContextConfigurerVisitor.java b/core-processor/src/main/java/io/micronaut/context/visitor/ContextConfigurerVisitor.java index 679c54e4eb9..201a837d070 100644 --- a/core-processor/src/main/java/io/micronaut/context/visitor/ContextConfigurerVisitor.java +++ b/core-processor/src/main/java/io/micronaut/context/visitor/ContextConfigurerVisitor.java @@ -17,6 +17,7 @@ import io.micronaut.context.ApplicationContextConfigurer; import io.micronaut.context.annotation.ContextConfigurer; +import io.micronaut.core.annotation.Internal; import io.micronaut.core.annotation.NonNull; import io.micronaut.inject.ast.ClassElement; import io.micronaut.inject.ast.Element; @@ -33,6 +34,7 @@ * * @since 3.2 */ +@Internal public class ContextConfigurerVisitor implements TypeElementVisitor { private static final Set SUPPORTED_SERVICE_TYPES = Collections.singleton( ApplicationContextConfigurer.class.getName() diff --git a/core-processor/src/main/java/io/micronaut/inject/annotation/AbstractAnnotationMetadataBuilder.java b/core-processor/src/main/java/io/micronaut/inject/annotation/AbstractAnnotationMetadataBuilder.java index 78d48ce5d84..9427dd4f4e5 100644 --- a/core-processor/src/main/java/io/micronaut/inject/annotation/AbstractAnnotationMetadataBuilder.java +++ b/core-processor/src/main/java/io/micronaut/inject/annotation/AbstractAnnotationMetadataBuilder.java @@ -65,6 +65,7 @@ * @author Graeme Rocher * @since 1.0 */ +@Internal public abstract class AbstractAnnotationMetadataBuilder { protected static final List EXCLUDES = Arrays.asList(AnnotationUtil.KOTLIN_METADATA, "jdk.internal.ValueBased"); @@ -2346,7 +2347,7 @@ private static class MetadataKey { MetadataKey(T... elements) { this.elements = elements; - this.hashCode = Objects.hash(elements); + this.hashCode = Objects.hash((Object[]) elements); } @Override diff --git a/core-processor/src/main/java/io/micronaut/inject/annotation/AnnotationMetadataWriter.java b/core-processor/src/main/java/io/micronaut/inject/annotation/AnnotationMetadataWriter.java index a4628a9a0aa..a6f7b54aa13 100644 --- a/core-processor/src/main/java/io/micronaut/inject/annotation/AnnotationMetadataWriter.java +++ b/core-processor/src/main/java/io/micronaut/inject/annotation/AnnotationMetadataWriter.java @@ -292,7 +292,7 @@ public static void instantiateNewMetadataHierarchy( return; } List notEmpty = CollectionUtils.iterableToList(hierarchy) - .stream().filter(h -> !h.isEmpty()).collect(Collectors.toList()); + .stream().filter(h -> !h.isEmpty()).toList(); if (notEmpty.size() == 1) { pushNewAnnotationMetadataOrReference(owningType, classWriter, generatorAdapter, defaultsStorage, loadTypeMethods, notEmpty.get(0)); return; diff --git a/core-processor/src/main/java/io/micronaut/inject/annotation/internal/CoreNonNullTransformer.java b/core-processor/src/main/java/io/micronaut/inject/annotation/internal/CoreNonNullTransformer.java index 41f701e90a8..a4056eadaed 100644 --- a/core-processor/src/main/java/io/micronaut/inject/annotation/internal/CoreNonNullTransformer.java +++ b/core-processor/src/main/java/io/micronaut/inject/annotation/internal/CoreNonNullTransformer.java @@ -17,6 +17,7 @@ import io.micronaut.core.annotation.AnnotationUtil; import io.micronaut.core.annotation.AnnotationValue; +import io.micronaut.core.annotation.Internal; import io.micronaut.inject.annotation.NamedAnnotationTransformer; import io.micronaut.inject.visitor.VisitorContext; @@ -30,6 +31,7 @@ * @author graemerocher * @since 2.4.0 */ +@Internal public class CoreNonNullTransformer implements NamedAnnotationTransformer { @Override diff --git a/core-processor/src/main/java/io/micronaut/inject/annotation/internal/JakartaNonnullTransformer.java b/core-processor/src/main/java/io/micronaut/inject/annotation/internal/JakartaNonnullTransformer.java index 73605d4e15b..e46305a30fd 100644 --- a/core-processor/src/main/java/io/micronaut/inject/annotation/internal/JakartaNonnullTransformer.java +++ b/core-processor/src/main/java/io/micronaut/inject/annotation/internal/JakartaNonnullTransformer.java @@ -17,6 +17,7 @@ import io.micronaut.core.annotation.AnnotationUtil; import io.micronaut.core.annotation.AnnotationValue; +import io.micronaut.core.annotation.Internal; import io.micronaut.core.annotation.NonNull; import io.micronaut.inject.annotation.NamedAnnotationTransformer; import io.micronaut.inject.visitor.VisitorContext; @@ -29,6 +30,7 @@ * A transformer that remaps {@link jakarta.annotation.Nonnull} to {@code javax.annotation.Nonnull}. * @since 4.0 */ +@Internal public class JakartaNonnullTransformer implements NamedAnnotationTransformer { @Override diff --git a/core-processor/src/main/java/io/micronaut/inject/annotation/internal/JakartaNullableTransformer.java b/core-processor/src/main/java/io/micronaut/inject/annotation/internal/JakartaNullableTransformer.java index bcbd4ca39d8..e981bf0ffce 100644 --- a/core-processor/src/main/java/io/micronaut/inject/annotation/internal/JakartaNullableTransformer.java +++ b/core-processor/src/main/java/io/micronaut/inject/annotation/internal/JakartaNullableTransformer.java @@ -17,6 +17,7 @@ import io.micronaut.core.annotation.AnnotationUtil; import io.micronaut.core.annotation.AnnotationValue; +import io.micronaut.core.annotation.Internal; import io.micronaut.core.annotation.NonNull; import io.micronaut.inject.annotation.NamedAnnotationTransformer; import io.micronaut.inject.visitor.VisitorContext; @@ -29,6 +30,7 @@ * A transformer that remaps {@link jakarta.annotation.Nullable} to {@code javax.annotation.Nullable}. * @since 4.0 */ +@Internal public class JakartaNullableTransformer implements NamedAnnotationTransformer { @Override diff --git a/core-processor/src/main/java/io/micronaut/inject/annotation/internal/KotlinNotNullMapper.java b/core-processor/src/main/java/io/micronaut/inject/annotation/internal/KotlinNotNullMapper.java index 841989d18be..a62fcb8bcc6 100644 --- a/core-processor/src/main/java/io/micronaut/inject/annotation/internal/KotlinNotNullMapper.java +++ b/core-processor/src/main/java/io/micronaut/inject/annotation/internal/KotlinNotNullMapper.java @@ -17,6 +17,7 @@ import io.micronaut.core.annotation.AnnotationUtil; import io.micronaut.core.annotation.AnnotationValue; +import io.micronaut.core.annotation.Internal; import io.micronaut.inject.annotation.NamedAnnotationTransformer; import io.micronaut.inject.visitor.VisitorContext; @@ -31,6 +32,7 @@ * @author graemerocher * @since 1.1.4 */ +@Internal public class KotlinNotNullMapper implements NamedAnnotationTransformer { @NonNull @Override diff --git a/core-processor/src/main/java/io/micronaut/inject/annotation/internal/KotlinNullableMapper.java b/core-processor/src/main/java/io/micronaut/inject/annotation/internal/KotlinNullableMapper.java index 9f9eb19eb55..bc6d610b68e 100644 --- a/core-processor/src/main/java/io/micronaut/inject/annotation/internal/KotlinNullableMapper.java +++ b/core-processor/src/main/java/io/micronaut/inject/annotation/internal/KotlinNullableMapper.java @@ -17,6 +17,7 @@ import io.micronaut.core.annotation.AnnotationUtil; import io.micronaut.core.annotation.AnnotationValue; +import io.micronaut.core.annotation.Internal; import io.micronaut.inject.annotation.NamedAnnotationTransformer; import io.micronaut.inject.visitor.VisitorContext; @@ -31,6 +32,7 @@ * @author graemerocher * @since 1.1.4 */ +@Internal public class KotlinNullableMapper implements NamedAnnotationTransformer { @NonNull @Override diff --git a/core-processor/src/main/java/io/micronaut/inject/annotation/internal/PersistenceContextAnnotationMapper.java b/core-processor/src/main/java/io/micronaut/inject/annotation/internal/PersistenceContextAnnotationMapper.java index 23919693496..5703d2aeb69 100644 --- a/core-processor/src/main/java/io/micronaut/inject/annotation/internal/PersistenceContextAnnotationMapper.java +++ b/core-processor/src/main/java/io/micronaut/inject/annotation/internal/PersistenceContextAnnotationMapper.java @@ -27,7 +27,7 @@ import java.util.List; /** - * Allows using the {@link javax.persistence.PersistenceContext} annotation in Micronaut. + * Allows using the `javax.persistence.PersistenceContext` annotation in Micronaut. * * @author graemerocher * @since 1.0 diff --git a/core-processor/src/main/java/io/micronaut/inject/ast/ClassElement.java b/core-processor/src/main/java/io/micronaut/inject/ast/ClassElement.java index 3895707c9ed..9f7cb3ac649 100644 --- a/core-processor/src/main/java/io/micronaut/inject/ast/ClassElement.java +++ b/core-processor/src/main/java/io/micronaut/inject/ast/ClassElement.java @@ -231,8 +231,7 @@ default Optional getDefaultConstructor() { } List constructors = getAccessibleConstructors() .stream() - .filter(ctor -> ctor.getParameters().length == 0) - .collect(Collectors.toList()); + .filter(ctor -> ctor.getParameters().length == 0).toList(); if (constructors.isEmpty()) { return Optional.empty(); } @@ -281,8 +280,7 @@ default Optional findStaticCreator() { default Optional findDefaultStaticCreator() { List staticCreators = getAccessibleStaticCreators() .stream() - .filter(c -> c.getParameters().length == 0) - .collect(Collectors.toList()); + .filter(c -> c.getParameters().length == 0).toList(); if (staticCreators.isEmpty()) { return Optional.empty(); } diff --git a/core-processor/src/main/java/io/micronaut/inject/ast/PrimitiveElement.java b/core-processor/src/main/java/io/micronaut/inject/ast/PrimitiveElement.java index 5cb486ab98e..4779f8df306 100644 --- a/core-processor/src/main/java/io/micronaut/inject/ast/PrimitiveElement.java +++ b/core-processor/src/main/java/io/micronaut/inject/ast/PrimitiveElement.java @@ -134,10 +134,8 @@ public static PrimitiveElement valueOf(String name) { @Override public String toString() { - final StringBuilder sb = new StringBuilder("PrimitiveElement{"); - sb.append("typeName='").append(typeName).append('\''); - sb.append(", arrayDimensions=").append(arrayDimensions); - sb.append('}'); - return sb.toString(); + return "PrimitiveElement{" + "typeName='" + typeName + '\'' + + ", arrayDimensions=" + arrayDimensions + + '}'; } } diff --git a/core-processor/src/main/java/io/micronaut/inject/ast/PropertyElement.java b/core-processor/src/main/java/io/micronaut/inject/ast/PropertyElement.java index e7438b6ec69..cc0ada998a0 100644 --- a/core-processor/src/main/java/io/micronaut/inject/ast/PropertyElement.java +++ b/core-processor/src/main/java/io/micronaut/inject/ast/PropertyElement.java @@ -50,7 +50,7 @@ default boolean isExcluded() { * @return True if the property is read only. */ default boolean isReadOnly() { - return !getWriteMember().isPresent(); + return getWriteMember().isEmpty(); } /** @@ -60,7 +60,7 @@ default boolean isReadOnly() { * @since 4.0.0 */ default boolean isWriteOnly() { - return !getReadMember().isPresent(); + return getReadMember().isEmpty(); } /** diff --git a/core-processor/src/main/java/io/micronaut/inject/ast/ReflectParameterElement.java b/core-processor/src/main/java/io/micronaut/inject/ast/ReflectParameterElement.java index a846fdbce01..7661bd34a02 100644 --- a/core-processor/src/main/java/io/micronaut/inject/ast/ReflectParameterElement.java +++ b/core-processor/src/main/java/io/micronaut/inject/ast/ReflectParameterElement.java @@ -18,6 +18,7 @@ import io.micronaut.core.annotation.AnnotationMetadata; import io.micronaut.core.annotation.AnnotationValue; import io.micronaut.core.annotation.AnnotationValueBuilder; +import io.micronaut.core.annotation.Internal; import io.micronaut.core.annotation.NonNull; import io.micronaut.inject.annotation.DefaultAnnotationMetadata; import io.micronaut.inject.annotation.MutableAnnotationMetadata; @@ -31,6 +32,7 @@ * @author graemerocher * @since 2.3.0 */ +@Internal class ReflectParameterElement implements ParameterElement { private final ClassElement classElement; private final String name; diff --git a/core-processor/src/main/java/io/micronaut/inject/ast/SimplePackageElement.java b/core-processor/src/main/java/io/micronaut/inject/ast/SimplePackageElement.java index 7de85c3197f..dfa02b151af 100644 --- a/core-processor/src/main/java/io/micronaut/inject/ast/SimplePackageElement.java +++ b/core-processor/src/main/java/io/micronaut/inject/ast/SimplePackageElement.java @@ -15,12 +15,15 @@ */ package io.micronaut.inject.ast; +import io.micronaut.core.annotation.Internal; + /** * Simple implementation of {@link io.micronaut.inject.ast.PackageElement}. * * @author graemerocher * @since 3.0.0 */ +@Internal final class SimplePackageElement implements PackageElement { private final String packageName; diff --git a/core-processor/src/main/java/io/micronaut/inject/ast/annotation/AbstractElementAnnotationMetadataFactory.java b/core-processor/src/main/java/io/micronaut/inject/ast/annotation/AbstractElementAnnotationMetadataFactory.java index 12ef0589219..7efbe85f7c6 100644 --- a/core-processor/src/main/java/io/micronaut/inject/ast/annotation/AbstractElementAnnotationMetadataFactory.java +++ b/core-processor/src/main/java/io/micronaut/inject/ast/annotation/AbstractElementAnnotationMetadataFactory.java @@ -62,36 +62,28 @@ public ElementAnnotationMetadata build(Element element) { @Override public ElementAnnotationMetadata build(Element element, AnnotationMetadata defaultAnnotationMetadata) { - if (element instanceof ClassElement) { - ClassElement classElement = (ClassElement) element; + if (element instanceof ClassElement classElement) { return buildForClass(defaultAnnotationMetadata, classElement); } - if (element instanceof ConstructorElement) { - ConstructorElement constructorElement = (ConstructorElement) element; + if (element instanceof ConstructorElement constructorElement) { return buildForConstructor(defaultAnnotationMetadata, constructorElement); } - if (element instanceof MethodElement) { - MethodElement methodElement = (MethodElement) element; + if (element instanceof MethodElement methodElement) { return buildForMethod(defaultAnnotationMetadata, methodElement); } - if (element instanceof FieldElement) { - FieldElement fieldElement = (FieldElement) element; + if (element instanceof FieldElement fieldElement) { return buildForField(defaultAnnotationMetadata, fieldElement); } - if (element instanceof ParameterElement) { - ParameterElement parameterElement = (ParameterElement) element; + if (element instanceof ParameterElement parameterElement) { return buildForParameter(defaultAnnotationMetadata, parameterElement); } - if (element instanceof PackageElement) { - PackageElement packageElement = (PackageElement) element; + if (element instanceof PackageElement packageElement) { return buildForPackage(defaultAnnotationMetadata, packageElement); } - if (element instanceof PropertyElement) { - PropertyElement propertyElement = (PropertyElement) element; + if (element instanceof PropertyElement propertyElement) { return buildForProperty(defaultAnnotationMetadata, propertyElement); } - if (element instanceof EnumConstantElement) { - EnumConstantElement enumConstantElement = (EnumConstantElement) element; + if (element instanceof EnumConstantElement enumConstantElement) { return buildForEnumConstantElement(defaultAnnotationMetadata, enumConstantElement); } throw new IllegalStateException("Unknown element: " + element.getClass() + " with native type: " + element.getNativeType()); diff --git a/core-processor/src/main/java/io/micronaut/inject/beans/visitor/BeanIntrospectionWriter.java b/core-processor/src/main/java/io/micronaut/inject/beans/visitor/BeanIntrospectionWriter.java index fb54f592969..0517e9f43e8 100644 --- a/core-processor/src/main/java/io/micronaut/inject/beans/visitor/BeanIntrospectionWriter.java +++ b/core-processor/src/main/java/io/micronaut/inject/beans/visitor/BeanIntrospectionWriter.java @@ -809,8 +809,8 @@ private void writeInstantiateMethod(ClassWriter classWriter, MethodElement const invokeBeanConstructor(instantiateInternal, constructor, (writer, con) -> { List constructorArguments = Arrays.asList(con.getParameters()); Collection argumentTypes = constructorArguments.stream().map(pe -> - JavaModelUtils.getTypeReference(pe.getType()) - ).collect(Collectors.toList()); + JavaModelUtils.getTypeReference(pe.getType()) + ).toList(); int i = 0; for (Type argumentType : argumentTypes) { @@ -1106,8 +1106,7 @@ public void writeDispatchOne(GeneratorAdapter writer) { }); List readWriteProps = beanProperties.stream() - .filter(bp -> bp.setDispatchIndex != -1 && bp.getDispatchIndex != -1 && !constructorProps.contains(bp)) - .collect(Collectors.toList()); + .filter(bp -> bp.setDispatchIndex != -1 && bp.getDispatchIndex != -1 && !constructorProps.contains(bp)).toList(); if (!readWriteProps.isEmpty()) { int beanTypeLocal = writer.newLocal(beanType); diff --git a/core-processor/src/main/java/io/micronaut/inject/beans/visitor/EntityIntrospectedAnnotationMapper.java b/core-processor/src/main/java/io/micronaut/inject/beans/visitor/EntityIntrospectedAnnotationMapper.java index 9dd9a6ac0df..f8ba0bc6c56 100644 --- a/core-processor/src/main/java/io/micronaut/inject/beans/visitor/EntityIntrospectedAnnotationMapper.java +++ b/core-processor/src/main/java/io/micronaut/inject/beans/visitor/EntityIntrospectedAnnotationMapper.java @@ -17,6 +17,7 @@ import io.micronaut.core.annotation.AnnotationValue; import io.micronaut.core.annotation.AnnotationValueBuilder; +import io.micronaut.core.annotation.Internal; import io.micronaut.core.annotation.Introspected; import io.micronaut.core.annotation.ReflectiveAccess; import io.micronaut.inject.annotation.NamedAnnotationMapper; @@ -33,6 +34,7 @@ * @author graemerocher * @since 1.1 */ +@Internal // tag::class[] public class EntityIntrospectedAnnotationMapper implements NamedAnnotationMapper { @NonNull diff --git a/core-processor/src/main/java/io/micronaut/inject/beans/visitor/EntityReflectiveAccessAnnotationMapper.java b/core-processor/src/main/java/io/micronaut/inject/beans/visitor/EntityReflectiveAccessAnnotationMapper.java index 0a54f76813b..49b01495e25 100644 --- a/core-processor/src/main/java/io/micronaut/inject/beans/visitor/EntityReflectiveAccessAnnotationMapper.java +++ b/core-processor/src/main/java/io/micronaut/inject/beans/visitor/EntityReflectiveAccessAnnotationMapper.java @@ -17,6 +17,7 @@ import io.micronaut.core.annotation.AnnotationValue; import io.micronaut.core.annotation.AnnotationValueBuilder; +import io.micronaut.core.annotation.Internal; import io.micronaut.core.annotation.NonNull; import io.micronaut.core.annotation.ReflectiveAccess; import io.micronaut.inject.annotation.NamedAnnotationMapper; @@ -32,6 +33,7 @@ * @author Iván López * @since 3.0 */ +@Internal public class EntityReflectiveAccessAnnotationMapper implements NamedAnnotationMapper { @NonNull @Override diff --git a/core-processor/src/main/java/io/micronaut/inject/beans/visitor/IntrospectedToBeanPropertiesTransformer.java b/core-processor/src/main/java/io/micronaut/inject/beans/visitor/IntrospectedToBeanPropertiesTransformer.java index 8d95f53daa7..1a270f86bbb 100644 --- a/core-processor/src/main/java/io/micronaut/inject/beans/visitor/IntrospectedToBeanPropertiesTransformer.java +++ b/core-processor/src/main/java/io/micronaut/inject/beans/visitor/IntrospectedToBeanPropertiesTransformer.java @@ -17,6 +17,7 @@ import io.micronaut.context.annotation.BeanProperties; import io.micronaut.core.annotation.AnnotationValue; +import io.micronaut.core.annotation.Internal; import io.micronaut.core.annotation.Introspected; import io.micronaut.core.util.ArrayUtils; import io.micronaut.inject.annotation.TypedAnnotationTransformer; @@ -31,6 +32,7 @@ * @author Denis Stepanov * @since 4.0.0 */ +@Internal public final class IntrospectedToBeanPropertiesTransformer implements TypedAnnotationTransformer { @Override diff --git a/core-processor/src/main/java/io/micronaut/inject/beans/visitor/IntrospectedTypeElementVisitor.java b/core-processor/src/main/java/io/micronaut/inject/beans/visitor/IntrospectedTypeElementVisitor.java index e5468a2287e..67a99b32cba 100644 --- a/core-processor/src/main/java/io/micronaut/inject/beans/visitor/IntrospectedTypeElementVisitor.java +++ b/core-processor/src/main/java/io/micronaut/inject/beans/visitor/IntrospectedTypeElementVisitor.java @@ -68,7 +68,7 @@ public class IntrospectedTypeElementVisitor implements TypeElementVisitor(JAVAX_VALIDATION_VALID)) .build(); - private Map writers = new LinkedHashMap<>(10); + private final Map writers = new LinkedHashMap<>(10); @Override public int getOrder() { diff --git a/core-processor/src/main/java/io/micronaut/inject/beans/visitor/JsonCreatorAnnotationMapper.java b/core-processor/src/main/java/io/micronaut/inject/beans/visitor/JsonCreatorAnnotationMapper.java index f60d40c20c3..d3c6e982ebd 100644 --- a/core-processor/src/main/java/io/micronaut/inject/beans/visitor/JsonCreatorAnnotationMapper.java +++ b/core-processor/src/main/java/io/micronaut/inject/beans/visitor/JsonCreatorAnnotationMapper.java @@ -17,6 +17,7 @@ import io.micronaut.core.annotation.AnnotationValue; import io.micronaut.core.annotation.Creator; +import io.micronaut.core.annotation.Internal; import io.micronaut.inject.annotation.NamedAnnotationMapper; import io.micronaut.inject.visitor.VisitorContext; @@ -31,6 +32,7 @@ * @author graemerocher * @since 1.1 */ +@Internal public class JsonCreatorAnnotationMapper implements NamedAnnotationMapper { @NonNull diff --git a/core-processor/src/main/java/io/micronaut/inject/beans/visitor/MappedSuperClassIntrospectionMapper.java b/core-processor/src/main/java/io/micronaut/inject/beans/visitor/MappedSuperClassIntrospectionMapper.java index 5c40c687c5d..9757e15965f 100644 --- a/core-processor/src/main/java/io/micronaut/inject/beans/visitor/MappedSuperClassIntrospectionMapper.java +++ b/core-processor/src/main/java/io/micronaut/inject/beans/visitor/MappedSuperClassIntrospectionMapper.java @@ -15,6 +15,7 @@ */ package io.micronaut.inject.beans.visitor; +import io.micronaut.core.annotation.Internal; import io.micronaut.core.annotation.NonNull; /** @@ -23,6 +24,7 @@ * @author graemerocher * @since 1.1.2 */ +@Internal public class MappedSuperClassIntrospectionMapper extends EntityIntrospectedAnnotationMapper { @NonNull @Override diff --git a/core-processor/src/main/java/io/micronaut/inject/configuration/ConfigurationUtils.java b/core-processor/src/main/java/io/micronaut/inject/configuration/ConfigurationUtils.java index 61026c71948..1dc99735e07 100644 --- a/core-processor/src/main/java/io/micronaut/inject/configuration/ConfigurationUtils.java +++ b/core-processor/src/main/java/io/micronaut/inject/configuration/ConfigurationUtils.java @@ -43,8 +43,7 @@ public static String buildPropertyPath(ClassElement owningType, ClassElement dec } else { typePath = getRequiredTypePath(owningType); } - String s = typePath + "." + propertyName; - return s; + return typePath + "." + propertyName; } public static String getRequiredTypePath(ClassElement classElement) { @@ -85,9 +84,9 @@ private static String combinePaths(String p1, String p2) { private static String getPath(AnnotationMetadata annotationMetadata) { Optional basePrefixOptional = annotationMetadata.stringValue(ConfigurationReader.class, ConfigurationReader.BASE_PREFIX); Optional prefixOptional = annotationMetadata.stringValue(ConfigurationReader.class, ConfigurationReader.PREFIX); - String prefix = null; + String prefix; if (basePrefixOptional.isPresent()) { - if (!prefixOptional.isPresent()) { + if (prefixOptional.isEmpty()) { prefix = basePrefixOptional.get(); } else { prefix = prefixOptional.map(p -> basePrefixOptional.get() + "." + p).orElse(null); diff --git a/core-processor/src/main/java/io/micronaut/inject/processing/AbstractBeanElementCreator.java b/core-processor/src/main/java/io/micronaut/inject/processing/AbstractBeanElementCreator.java index b211ab17cd5..e2fd4cfad59 100644 --- a/core-processor/src/main/java/io/micronaut/inject/processing/AbstractBeanElementCreator.java +++ b/core-processor/src/main/java/io/micronaut/inject/processing/AbstractBeanElementCreator.java @@ -15,29 +15,35 @@ */ package io.micronaut.inject.processing; -import io.micronaut.aop.writer.AopHelperImpl; +import io.micronaut.aop.Interceptor; +import io.micronaut.aop.InterceptorKind; +import io.micronaut.aop.Introduction; +import io.micronaut.aop.internal.intercepted.InterceptedMethodUtil; +import io.micronaut.aop.writer.AopProxyWriter; import io.micronaut.context.RequiresCondition; import io.micronaut.context.annotation.Requires; import io.micronaut.core.annotation.AnnotationMetadata; import io.micronaut.core.annotation.AnnotationUtil; -import io.micronaut.core.annotation.AnnotationValue; import io.micronaut.core.annotation.Internal; -import io.micronaut.core.annotation.NextMajorVersion; -import io.micronaut.core.annotation.NonNull; -import io.micronaut.core.annotation.Nullable; +import io.micronaut.core.util.ArrayUtils; +import io.micronaut.core.value.OptionalValues; import io.micronaut.inject.annotation.AnnotationMetadataHierarchy; import io.micronaut.inject.ast.ClassElement; +import io.micronaut.inject.ast.ElementQuery; import io.micronaut.inject.ast.MemberElement; +import io.micronaut.inject.ast.MethodElement; import io.micronaut.inject.validation.RequiresValidation; import io.micronaut.inject.visitor.VisitorContext; import io.micronaut.inject.writer.BeanDefinitionVisitor; +import io.micronaut.inject.writer.BeanDefinitionWriter; -import java.lang.annotation.Annotation; +import java.util.Arrays; import java.util.Collection; +import java.util.LinkedHashMap; import java.util.LinkedList; import java.util.List; -import java.util.function.Function; -import java.util.function.Predicate; +import java.util.Map; +import java.util.Optional; /** * Abstract shared functionality of the builder. @@ -55,12 +61,9 @@ abstract class AbstractBeanElementCreator implements BeanDefinitionCreator { protected final VisitorContext visitorContext; protected final List beanDefinitionWriters = new LinkedList<>(); - protected final AopHelper aopHelper; - protected AbstractBeanElementCreator(ClassElement classElement, VisitorContext visitorContext) { this.classElement = classElement; this.visitorContext = visitorContext; - this.aopHelper = new AopHelperImpl(); checkPackage(classElement); } @@ -82,50 +85,6 @@ private void checkPackage(ClassElement classElement) { } } - /** - * Does the given metadata have AOP advice declared. - * - * @param annotationMetadata The annotation metadata - * @return True if it does - */ - @NextMajorVersion("Replace with InterceptedMethodUtil.hasAroundStereotype") - protected static boolean hasAroundStereotype(@Nullable AnnotationMetadata annotationMetadata) { - return hasAround(annotationMetadata, - annMetadata -> annMetadata.hasStereotype(AnnotationUtil.ANN_AROUND), - annMetdata -> annMetdata.getAnnotationValuesByName(AnnotationUtil.ANN_INTERCEPTOR_BINDING)); - } - - /** - * Does the given metadata have declared AOP advice. - * - * @param annotationMetadata The annotation metadata - * @return True if it does - */ - @NextMajorVersion("Replace with InterceptedMethodUtil.hasDeclaredAroundAdvice") - protected static boolean hasDeclaredAroundAdvice(@Nullable AnnotationMetadata annotationMetadata) { - return hasAround(annotationMetadata, - annMetadata -> annMetadata.hasDeclaredStereotype(AnnotationUtil.ANN_AROUND), - annMetdata -> annMetdata.getDeclaredAnnotationValuesByName(AnnotationUtil.ANN_INTERCEPTOR_BINDING)); - } - - private static boolean hasAround(@Nullable AnnotationMetadata annotationMetadata, - @NonNull Predicate hasFunction, - @NonNull Function>> interceptorBindingsFunction) { - if (annotationMetadata == null) { - return false; - } - if (hasFunction.test(annotationMetadata)) { - return true; - } else if (annotationMetadata.hasDeclaredStereotype(AnnotationUtil.ANN_INTERCEPTOR_BINDINGS)) { - return interceptorBindingsFunction.apply(annotationMetadata) - .stream().anyMatch(av -> - av.stringValue("kind").orElse("AROUND").equals("AROUND") - ); - } - - return false; - } - protected void visitAnnotationMetadata(BeanDefinitionVisitor writer, AnnotationMetadata annotationMetadata) { for (io.micronaut.core.annotation.AnnotationValue annotation : annotationMetadata.getAnnotationValuesByType(Requires.class)) { annotation.stringValue(RequiresCondition.MEMBER_BEAN_PROPERTY) @@ -151,4 +110,107 @@ public static AnnotationMetadata getElementAnnotationMetadata(MemberElement meth return annotationMetadata; } + protected boolean visitIntrospectedMethod(BeanDefinitionVisitor visitor, ClassElement typeElement, MethodElement methodElement) { + AopProxyWriter aopProxyWriter = (AopProxyWriter) visitor; + + final AnnotationMetadata resolvedTypeMetadata = typeElement.getAnnotationMetadata(); + final boolean resolvedTypeMetadataIsAopProxyType = InterceptedMethodUtil.hasDeclaredAroundAdvice(resolvedTypeMetadata); + + if (methodElement.isAbstract() + || resolvedTypeMetadataIsAopProxyType + || InterceptedMethodUtil.hasDeclaredAroundAdvice(methodElement.getAnnotationMetadata())) { + addToIntroduction(aopProxyWriter, typeElement, methodElement, false); + return true; + } + return false; + } + + protected static void addToIntroduction(AopProxyWriter aopProxyWriter, + ClassElement classElement, + MethodElement methodElement, + boolean ignoreNotAbstract) { + AnnotationMetadata methodAnnotationMetadata = methodElement.getDeclaredMetadata(); + + if (InterceptedMethodUtil.hasAroundStereotype(methodAnnotationMetadata)) { + aopProxyWriter.visitInterceptorBinding( + InterceptedMethodUtil.resolveInterceptorBinding(methodAnnotationMetadata, InterceptorKind.AROUND) + ); + } + + if (!classElement.getName().equals(methodElement.getDeclaringType().getName())) { + aopProxyWriter.addOriginatingElement(methodElement.getDeclaringType()); + } + + ClassElement declaringType = methodElement.getDeclaringType(); + if (methodElement.isAbstract()) { + aopProxyWriter.visitIntroductionMethod(declaringType, methodElement); + } else if (!ignoreNotAbstract) { + boolean isInterface = methodElement.getDeclaringType().isInterface(); + boolean isDefault = methodElement.isDefault(); + if (isInterface && isDefault) { + // Default methods cannot be "super" accessed on the defined type + declaringType = classElement; + } + // only apply around advise to non-abstract methods of introduction advise + aopProxyWriter.visitAroundMethod(declaringType, methodElement); + } + } + + protected AopProxyWriter createAroundAopProxyWriter(BeanDefinitionVisitor existingWriter, + AnnotationMetadata aopElementAnnotationProcessor, + VisitorContext visitorContext, + boolean forceProxyTarget) { + OptionalValues aroundSettings = aopElementAnnotationProcessor.getValues(AnnotationUtil.ANN_AROUND, Boolean.class); + Map settings = new LinkedHashMap<>(); + for (CharSequence setting : aroundSettings) { + Optional entry = aroundSettings.get(setting); + entry.ifPresent(val -> settings.put(setting, val)); + } + if (forceProxyTarget) { + settings.put(Interceptor.PROXY_TARGET, true); + } + aroundSettings = OptionalValues.of(Boolean.class, settings); + + return new AopProxyWriter( + (BeanDefinitionWriter) existingWriter, + aroundSettings, + visitorContext, + InterceptedMethodUtil.resolveInterceptorBinding(aopElementAnnotationProcessor, InterceptorKind.AROUND) + ); + } + + protected AopProxyWriter createIntroductionAopProxyWriter(ClassElement typeElement, + VisitorContext visitorContext) { + AnnotationMetadata annotationMetadata = typeElement.getAnnotationMetadata(); + + String packageName = typeElement.getPackageName(); + String beanClassName = typeElement.getSimpleName(); + io.micronaut.core.annotation.AnnotationValue[] aroundInterceptors = + InterceptedMethodUtil.resolveInterceptorBinding(annotationMetadata, InterceptorKind.AROUND); + io.micronaut.core.annotation.AnnotationValue[] introductionInterceptors = InterceptedMethodUtil.resolveInterceptorBinding(annotationMetadata, InterceptorKind.INTRODUCTION); + + ClassElement[] interfaceTypes = Arrays.stream(annotationMetadata.getValue(Introduction.class, "interfaces", String[].class).orElse(new String[0])) + .map(v -> visitorContext.getClassElement(v, visitorContext.getElementAnnotationMetadataFactory().readOnly()) + .orElseThrow(() -> new ProcessingException(typeElement, "Cannot find interface: " + v)) + ).toArray(ClassElement[]::new); + + io.micronaut.core.annotation.AnnotationValue[] interceptorTypes = ArrayUtils.concat(aroundInterceptors, introductionInterceptors); + boolean isInterface = typeElement.isInterface(); + AopProxyWriter aopProxyWriter = new AopProxyWriter( + packageName, + beanClassName, + isInterface, + typeElement, + annotationMetadata, + interfaceTypes, + visitorContext, + interceptorTypes); + + Arrays.stream(interfaceTypes) + .flatMap(interfaceElement -> interfaceElement.getEnclosedElements(ElementQuery.ALL_METHODS).stream()) + .forEach(methodElement -> addToIntroduction(aopProxyWriter, typeElement, methodElement.withNewOwningType(typeElement), true)); + + return aopProxyWriter; + } + } diff --git a/core-processor/src/main/java/io/micronaut/inject/processing/AopHelper.java b/core-processor/src/main/java/io/micronaut/inject/processing/AopHelper.java deleted file mode 100644 index 9bccd52991b..00000000000 --- a/core-processor/src/main/java/io/micronaut/inject/processing/AopHelper.java +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Copyright 2017-2022 original authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.micronaut.inject.processing; - -import io.micronaut.core.annotation.AnnotationMetadata; -import io.micronaut.core.annotation.Internal; -import io.micronaut.core.annotation.NextMajorVersion; -import io.micronaut.inject.ast.ClassElement; -import io.micronaut.inject.ast.MethodElement; -import io.micronaut.inject.ast.TypedElement; -import io.micronaut.inject.visitor.VisitorContext; -import io.micronaut.inject.writer.BeanDefinitionVisitor; - -import java.util.concurrent.atomic.AtomicInteger; - -/** - * AOP helper to connect Inject module with AOP. - */ -@NextMajorVersion("Correct dependency graph") -@Internal -public interface AopHelper { - - BeanDefinitionVisitor visitAdaptedMethod(ClassElement classElement, - MethodElement sourceMethod, - AtomicInteger adaptedMethodIndex, - VisitorContext visitorContext); - - BeanDefinitionVisitor createIntroductionAopProxyWriter(ClassElement typeElement, - VisitorContext visitorContext); - - BeanDefinitionVisitor createAroundAopProxyWriter(BeanDefinitionVisitor existingWriter, - AnnotationMetadata producedAnnotationMetadata, - VisitorContext visitorContext, - boolean forceProxyTarget); - - boolean visitIntrospectedMethod(BeanDefinitionVisitor visitor, ClassElement typeElement, MethodElement methodElement); - - void visitAroundMethod(BeanDefinitionVisitor existingWriter, TypedElement beanType, MethodElement methodElement); -} diff --git a/core-processor/src/main/java/io/micronaut/inject/processing/AopIntroductionProxySupportedBeanElementCreator.java b/core-processor/src/main/java/io/micronaut/inject/processing/AopIntroductionProxySupportedBeanElementCreator.java index 09da91fabff..9a23eacfe86 100644 --- a/core-processor/src/main/java/io/micronaut/inject/processing/AopIntroductionProxySupportedBeanElementCreator.java +++ b/core-processor/src/main/java/io/micronaut/inject/processing/AopIntroductionProxySupportedBeanElementCreator.java @@ -15,6 +15,7 @@ */ package io.micronaut.inject.processing; +import io.micronaut.aop.writer.AopProxyWriter; import io.micronaut.core.annotation.AnnotationMetadata; import io.micronaut.core.annotation.Internal; import io.micronaut.inject.ast.ClassElement; @@ -41,7 +42,7 @@ protected BeanDefinitionVisitor createBeanDefinitionVisitor() { if (classElement.isFinal()) { throw new ProcessingException(classElement, "Cannot apply AOP advice to final class. Class must be made non-final to support proxying: " + classElement.getName()); } - aopProxyVisitor = aopHelper.createIntroductionAopProxyWriter(classElement, visitorContext); + aopProxyVisitor = createIntroductionAopProxyWriter(classElement, visitorContext); beanDefinitionWriters.add(aopProxyVisitor); MethodElement constructorElement = classElement.getPrimaryConstructor().orElse(null); if (constructorElement != null) { @@ -60,13 +61,13 @@ protected BeanDefinitionVisitor createBeanDefinitionVisitor() { } @Override - protected BeanDefinitionVisitor getAroundAopProxyVisitor(BeanDefinitionVisitor visitor, MethodElement methodElement) { + protected AopProxyWriter getAroundAopProxyVisitor(BeanDefinitionVisitor visitor, MethodElement methodElement) { return aopProxyVisitor; } @Override protected boolean visitPropertyReadElement(BeanDefinitionVisitor visitor, PropertyElement propertyElement, MethodElement readElement) { - if (readElement.isAbstract() && aopHelper.visitIntrospectedMethod(visitor, classElement, readElement)) { + if (readElement.isAbstract() && visitIntrospectedMethod(visitor, classElement, readElement)) { return true; } return super.visitPropertyReadElement(visitor, propertyElement, readElement); @@ -74,7 +75,7 @@ protected boolean visitPropertyReadElement(BeanDefinitionVisitor visitor, Proper @Override protected boolean visitPropertyWriteElement(BeanDefinitionVisitor visitor, PropertyElement propertyElement, MethodElement writeElement) { - if (writeElement.isAbstract() && aopHelper.visitIntrospectedMethod(visitor, classElement, writeElement)) { + if (writeElement.isAbstract() && visitIntrospectedMethod(visitor, classElement, writeElement)) { return true; } return super.visitPropertyWriteElement(visitor, propertyElement, writeElement); @@ -82,7 +83,7 @@ protected boolean visitPropertyWriteElement(BeanDefinitionVisitor visitor, Prope @Override protected boolean visitMethod(BeanDefinitionVisitor visitor, MethodElement methodElement) { - if (methodElement.isAbstract() && aopHelper.visitIntrospectedMethod(visitor, classElement, methodElement)) { + if (methodElement.isAbstract() && visitIntrospectedMethod(visitor, classElement, methodElement)) { return true; } return super.visitMethod(visitor, methodElement); diff --git a/core-processor/src/main/java/io/micronaut/inject/processing/BeanDefinitionCreatorFactory.java b/core-processor/src/main/java/io/micronaut/inject/processing/BeanDefinitionCreatorFactory.java index a1e6dc43a8c..522358f2a24 100644 --- a/core-processor/src/main/java/io/micronaut/inject/processing/BeanDefinitionCreatorFactory.java +++ b/core-processor/src/main/java/io/micronaut/inject/processing/BeanDefinitionCreatorFactory.java @@ -15,6 +15,7 @@ */ package io.micronaut.inject.processing; +import io.micronaut.aop.internal.intercepted.InterceptedMethodUtil; import io.micronaut.context.annotation.Bean; import io.micronaut.context.annotation.DefaultScope; import io.micronaut.context.annotation.Executable; @@ -23,18 +24,12 @@ import io.micronaut.context.annotation.Value; import io.micronaut.core.annotation.AnnotationMetadata; import io.micronaut.core.annotation.AnnotationUtil; -import io.micronaut.core.annotation.AnnotationValue; import io.micronaut.core.annotation.NonNull; -import io.micronaut.core.annotation.Nullable; import io.micronaut.inject.ast.ClassElement; import io.micronaut.inject.ast.ElementQuery; import io.micronaut.inject.visitor.VisitorContext; -import java.lang.annotation.Annotation; import java.util.Collections; -import java.util.List; -import java.util.function.Function; -import java.util.function.Predicate; /** * Bean definition builder factory. @@ -50,7 +45,7 @@ public static BeanDefinitionCreator produce(ClassElement classElement, VisitorCo boolean isIntroduction = classElement.hasStereotype(AnnotationUtil.ANN_INTRODUCTION); if (ConfigurationReaderBeanElementCreator.isConfigurationProperties(classElement)) { if (classElement.isInterface()) { - return new IntroductionInterfaceBeanElementCreator(classElement, visitorContext, null); + return new IntroductionInterfaceBeanElementCreator(classElement, visitorContext); } return new ConfigurationReaderBeanElementCreator(classElement, visitorContext); } @@ -66,7 +61,7 @@ public static BeanDefinitionCreator produce(ClassElement classElement, VisitorCo } if (isIntroduction) { if (classElement.isInterface()) { - return new IntroductionInterfaceBeanElementCreator(classElement, visitorContext, null); + return new IntroductionInterfaceBeanElementCreator(classElement, visitorContext); } return new AopIntroductionProxySupportedBeanElementCreator(classElement, visitorContext, false); } @@ -115,7 +110,7 @@ private static boolean containsInjectPoint(AnnotationMetadata annotationMetadata } private static boolean isAopProxyType(ClassElement classElement) { - return !classElement.isAssignable("io.micronaut.aop.Interceptor") && hasAroundStereotype(classElement.getAnnotationMetadata()); + return !classElement.isAssignable("io.micronaut.aop.Interceptor") && InterceptedMethodUtil.hasAroundStereotype(classElement.getAnnotationMetadata()); } public static boolean isDeclaredBeanInMetadata(AnnotationMetadata concreteClassMetadata) { @@ -124,46 +119,4 @@ public static boolean isDeclaredBeanInMetadata(AnnotationMetadata concreteClassM concreteClassMetadata.hasStereotype(DefaultScope.class); } - /** - * Does the given metadata have AOP advice declared. - * - * @param annotationMetadata The annotation metadata - * @return True if it does - */ - protected static boolean hasAroundStereotype(@Nullable AnnotationMetadata annotationMetadata) { - return hasAround(annotationMetadata, - annMetadata -> annMetadata.hasStereotype(AnnotationUtil.ANN_AROUND), - annMetdata -> annMetdata.getAnnotationValuesByName(AnnotationUtil.ANN_INTERCEPTOR_BINDING)); - } - - /** - * Does the given metadata have declared AOP advice. - * - * @param annotationMetadata The annotation metadata - * @return True if it does - */ - protected static boolean hasDeclaredAroundAdvice(@Nullable AnnotationMetadata annotationMetadata) { - return hasAround(annotationMetadata, - annMetadata -> annMetadata.hasDeclaredStereotype(AnnotationUtil.ANN_AROUND), - annMetdata -> annMetdata.getDeclaredAnnotationValuesByName(AnnotationUtil.ANN_INTERCEPTOR_BINDING)); - } - - private static boolean hasAround(@Nullable AnnotationMetadata annotationMetadata, - @NonNull Predicate hasFunction, - @NonNull Function>> interceptorBindingsFunction) { - if (annotationMetadata == null) { - return false; - } - if (hasFunction.test(annotationMetadata)) { - return true; - } else if (annotationMetadata.hasDeclaredStereotype(AnnotationUtil.ANN_INTERCEPTOR_BINDINGS)) { - return interceptorBindingsFunction.apply(annotationMetadata) - .stream().anyMatch(av -> - av.stringValue("kind").orElse("AROUND").equals("AROUND") - ); - } - - return false; - } - } diff --git a/core-processor/src/main/java/io/micronaut/inject/processing/ConfigurationReaderBeanElementCreator.java b/core-processor/src/main/java/io/micronaut/inject/processing/ConfigurationReaderBeanElementCreator.java index e8cd8f6dd03..8cbd0d09959 100644 --- a/core-processor/src/main/java/io/micronaut/inject/processing/ConfigurationReaderBeanElementCreator.java +++ b/core-processor/src/main/java/io/micronaut/inject/processing/ConfigurationReaderBeanElementCreator.java @@ -280,7 +280,7 @@ private void visitConfigurationBuilder(BeanDefinitionVisitor visitor, return false; } Optional writeMethod = propertyElement.getWriteMethod(); - if (!writeMethod.isPresent()) { + if (writeMethod.isEmpty()) { return false; } MethodElement methodElement = writeMethod.get(); diff --git a/core-processor/src/main/java/io/micronaut/inject/processing/DeclaredBeanElementCreator.java b/core-processor/src/main/java/io/micronaut/inject/processing/DeclaredBeanElementCreator.java index 7b1f6b5ac2f..10515dc67fd 100644 --- a/core-processor/src/main/java/io/micronaut/inject/processing/DeclaredBeanElementCreator.java +++ b/core-processor/src/main/java/io/micronaut/inject/processing/DeclaredBeanElementCreator.java @@ -15,9 +15,14 @@ */ package io.micronaut.inject.processing; +import io.micronaut.aop.Adapter; +import io.micronaut.aop.InterceptorKind; +import io.micronaut.aop.internal.intercepted.InterceptedMethodUtil; +import io.micronaut.aop.writer.AopProxyWriter; import io.micronaut.context.annotation.Executable; import io.micronaut.context.annotation.Property; import io.micronaut.context.annotation.Value; +import io.micronaut.core.annotation.AnnotationClassValue; import io.micronaut.core.annotation.AnnotationMetadata; import io.micronaut.core.annotation.AnnotationUtil; import io.micronaut.core.annotation.Internal; @@ -25,19 +30,28 @@ import io.micronaut.core.annotation.NonNull; import io.micronaut.core.annotation.Nullable; import io.micronaut.core.annotation.Vetoed; +import io.micronaut.core.util.StringUtils; +import io.micronaut.inject.annotation.AnnotationMetadataHierarchy; import io.micronaut.inject.ast.ClassElement; import io.micronaut.inject.ast.ElementQuery; import io.micronaut.inject.ast.FieldElement; +import io.micronaut.inject.ast.GenericPlaceholderElement; import io.micronaut.inject.ast.MemberElement; import io.micronaut.inject.ast.MethodElement; +import io.micronaut.inject.ast.ParameterElement; import io.micronaut.inject.ast.PropertyElement; +import io.micronaut.inject.ast.TypedElement; import io.micronaut.inject.visitor.VisitorContext; import io.micronaut.inject.writer.BeanDefinitionVisitor; import io.micronaut.inject.writer.BeanDefinitionWriter; import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; import java.util.HashSet; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.concurrent.atomic.AtomicInteger; @@ -51,7 +65,10 @@ @Internal class DeclaredBeanElementCreator extends AbstractBeanElementCreator { - protected BeanDefinitionVisitor aopProxyVisitor; + private static final String MSG_ADAPTER_METHOD_PREFIX = "Cannot adapt method ["; + private static final String MSG_TARGET_METHOD_PREFIX = "] to target method ["; + + protected AopProxyWriter aopProxyVisitor; protected final boolean isAopProxy; private final AtomicInteger adaptedMethodIndex = new AtomicInteger(0); @@ -98,12 +115,12 @@ protected BeanDefinitionVisitor createBeanDefinitionVisitor() { * @param methodElement the method that is originating the AOP proxy * @return The AOP proxy visitor */ - protected BeanDefinitionVisitor getAroundAopProxyVisitor(BeanDefinitionVisitor visitor, @Nullable MethodElement methodElement) { + protected AopProxyWriter getAroundAopProxyVisitor(BeanDefinitionVisitor visitor, @Nullable MethodElement methodElement) { if (aopProxyVisitor == null) { if (classElement.isFinal()) { throw new ProcessingException(classElement, "Cannot apply AOP advice to final class. Class must be made non-final to support proxying: " + classElement.getName()); } - aopProxyVisitor = aopHelper.createAroundAopProxyWriter( + aopProxyVisitor = createAroundAopProxyWriter( visitor, isAopProxy || methodElement == null ? classElement.getAnnotationMetadata() : methodElement.getAnnotationMetadata(), visitorContext, @@ -351,11 +368,12 @@ private boolean visitAopAndExecutableMethod(BeanDefinitionVisitor visitor, Metho protected boolean visitAopMethod(BeanDefinitionVisitor visitor, MethodElement methodElement) { boolean aopDefinedOnClassAndPublicMethod = isAopProxy && (methodElement.isPublic() || methodElement.isPackagePrivate()); AnnotationMetadata methodAnnotationMetadata = getElementAnnotationMetadata(methodElement); + if (aopDefinedOnClassAndPublicMethod || - !isAopProxy && hasAroundStereotype(methodAnnotationMetadata) || - hasDeclaredAroundAdvice(methodAnnotationMetadata) && !classElement.isAbstract()) { + !isAopProxy && InterceptedMethodUtil.hasAroundStereotype(methodAnnotationMetadata) || + InterceptedMethodUtil.hasDeclaredAroundAdvice(methodAnnotationMetadata) && !classElement.isAbstract()) { if (methodElement.isFinal()) { - if (hasDeclaredAroundAdvice(methodAnnotationMetadata)) { + if (InterceptedMethodUtil.hasDeclaredAroundAdvice(methodAnnotationMetadata)) { throw new ProcessingException(methodElement, "Method defines AOP advice but is declared final. Change the method to be non-final in order for AOP advice to be applied."); } else if (!methodElement.isSynthetic() && aopDefinedOnClassAndPublicMethod && isDeclaredInThisClass(methodElement)) { throw new ProcessingException(methodElement, "Public method inherits AOP advice but is declared final. Either make the method non-public or apply AOP advice only to public methods declared on the class."); @@ -366,13 +384,20 @@ protected boolean visitAopMethod(BeanDefinitionVisitor visitor, MethodElement me } else if (methodElement.isStatic()) { throw new ProcessingException(methodElement, "Method defines AOP advice but is declared static"); } - BeanDefinitionVisitor aopProxyVisitor = getAroundAopProxyVisitor(visitor, methodElement); - aopHelper.visitAroundMethod(aopProxyVisitor, classElement, methodElement); + AopProxyWriter aopProxyVisitor = getAroundAopProxyVisitor(visitor, methodElement); + visitAroundMethod(aopProxyVisitor, classElement, methodElement); return true; } return false; } + protected void visitAroundMethod(AopProxyWriter aopProxyWriter, TypedElement beanType, MethodElement methodElement) { + aopProxyWriter.visitInterceptorBinding( + InterceptedMethodUtil.resolveInterceptorBinding(methodElement.getAnnotationMetadata(), InterceptorKind.AROUND) + ); + aopProxyWriter.visitAroundMethod(beanType, methodElement); + } + /** * Apply configuration injection for the constructor. * @@ -491,12 +516,109 @@ private boolean isDeclaredInThisClass(MemberElement memberElement) { } private void visitAdaptedMethod(BeanDefinitionVisitor visitor, MethodElement sourceMethod) { - BeanDefinitionVisitor adapter = aopHelper - .visitAdaptedMethod(classElement, sourceMethod, adaptedMethodIndex, visitorContext); - if (adapter != null) { - visitor.visitExecutableMethod(sourceMethod.getDeclaringType(), sourceMethod, visitorContext); - beanDefinitionWriters.add(adapter); + AnnotationMetadata methodAnnotationMetadata = sourceMethod.getDeclaredMetadata(); + + Optional interfaceToAdaptValue = methodAnnotationMetadata.getValue(Adapter.class, String.class) + .flatMap(clazz -> visitorContext.getClassElement(clazz, visitorContext.getElementAnnotationMetadataFactory().readOnly())); + + if (interfaceToAdaptValue.isEmpty()) { + return; + } + ClassElement interfaceToAdapt = interfaceToAdaptValue.get(); + if (!interfaceToAdapt.isInterface()) { + throw new ProcessingException(sourceMethod, "Class to adapt [" + interfaceToAdapt.getName() + "] is not an interface"); + } + + String rootName = classElement.getSimpleName() + '$' + interfaceToAdapt.getSimpleName() + '$' + sourceMethod.getSimpleName(); + String beanClassName = rootName + adaptedMethodIndex.incrementAndGet(); + + AopProxyWriter aopProxyWriter = new AopProxyWriter( + classElement.getPackageName(), + beanClassName, + true, + false, + sourceMethod, + new AnnotationMetadataHierarchy(classElement.getAnnotationMetadata(), methodAnnotationMetadata), + new ClassElement[]{interfaceToAdapt}, + visitorContext + ); + + aopProxyWriter.visitDefaultConstructor(methodAnnotationMetadata, visitorContext); + + List methods = interfaceToAdapt.getEnclosedElements(ElementQuery.ALL_METHODS.onlyAbstract()); + if (methods.isEmpty()) { + throw new ProcessingException(sourceMethod, "Interface to adapt [" + interfaceToAdapt.getName() + "] is not a SAM type. No methods found."); + } + if (methods.size() > 1) { + throw new ProcessingException(sourceMethod, "Interface to adapt [" + interfaceToAdapt.getName() + "] is not a SAM type. More than one abstract method declared."); + } + + MethodElement targetMethod = methods.iterator().next(); + + ParameterElement[] sourceParams = sourceMethod.getParameters(); + ParameterElement[] targetParams = targetMethod.getParameters(); + + int paramLen = targetParams.length; + if (paramLen != sourceParams.length) { + throw new ProcessingException(sourceMethod, MSG_ADAPTER_METHOD_PREFIX + sourceMethod + MSG_TARGET_METHOD_PREFIX + targetMethod + "]. Argument lengths don't match."); } + if (sourceMethod.isSuspend()) { + throw new ProcessingException(sourceMethod, MSG_ADAPTER_METHOD_PREFIX + sourceMethod + MSG_TARGET_METHOD_PREFIX + targetMethod + "]. Kotlin suspend method not supported here."); + } + + Map typeVariables = interfaceToAdapt.getTypeArguments(); + Map genericTypes = new LinkedHashMap<>(paramLen); + for (int i = 0; i < paramLen; i++) { + ParameterElement targetParam = targetParams[i]; + ParameterElement sourceParam = sourceParams[i]; + + ClassElement targetType = targetParam.getType(); + ClassElement targetGenericType = targetParam.getGenericType(); + ClassElement sourceType = sourceParam.getGenericType(); + + // ??? Java returns generic placeholder for the generic type and Groovy from the ordinary type + if (targetGenericType instanceof GenericPlaceholderElement genericPlaceholderElement) { + String variableName = genericPlaceholderElement.getVariableName(); + if (typeVariables.containsKey(variableName)) { + genericTypes.put(variableName, sourceType); + } + } else if (targetType instanceof GenericPlaceholderElement genericPlaceholderElement) { + String variableName = genericPlaceholderElement.getVariableName(); + if (typeVariables.containsKey(variableName)) { + genericTypes.put(variableName, sourceType); + } + } + + if (!sourceType.isAssignable(targetGenericType.getName())) { + throw new ProcessingException(sourceMethod, MSG_ADAPTER_METHOD_PREFIX + sourceMethod + MSG_TARGET_METHOD_PREFIX + targetMethod + "]. Type [" + sourceType.getName() + "] is not a subtype of type [" + targetGenericType.getName() + "] for argument at position " + i); + } + } + + if (!genericTypes.isEmpty()) { + aopProxyWriter.visitTypeArguments(Collections.singletonMap(interfaceToAdapt.getName(), genericTypes)); + } + + AnnotationClassValue[] adaptedArgumentTypes = Arrays.stream(sourceParams) + .map(p -> new AnnotationClassValue<>(JavaModelUtils.getClassname(p.getGenericType()))) + .toArray(AnnotationClassValue[]::new); + + targetMethod = targetMethod.withNewOwningType(classElement); + + targetMethod.annotate(Adapter.class, builder -> { + builder.member(Adapter.InternalAttributes.ADAPTED_BEAN, new AnnotationClassValue<>(JavaModelUtils.getClassname(classElement))); + builder.member(Adapter.InternalAttributes.ADAPTED_METHOD, sourceMethod.getName()); + builder.member(Adapter.InternalAttributes.ADAPTED_ARGUMENT_TYPES, adaptedArgumentTypes); + String qualifier = classElement.stringValue(AnnotationUtil.NAMED).orElse(null); + if (StringUtils.isNotEmpty(qualifier)) { + builder.member(Adapter.InternalAttributes.ADAPTED_QUALIFIER, qualifier); + } + }); + + aopProxyWriter.visitAroundMethod(interfaceToAdapt, targetMethod); + + visitor.visitExecutableMethod(sourceMethod.getDeclaringType(), sourceMethod, visitorContext); + + beanDefinitionWriters.add(aopProxyWriter); } } diff --git a/core-processor/src/main/java/io/micronaut/inject/processing/FactoryBeanElementCreator.java b/core-processor/src/main/java/io/micronaut/inject/processing/FactoryBeanElementCreator.java index fe9ded31e0c..0262d4a21e5 100644 --- a/core-processor/src/main/java/io/micronaut/inject/processing/FactoryBeanElementCreator.java +++ b/core-processor/src/main/java/io/micronaut/inject/processing/FactoryBeanElementCreator.java @@ -15,6 +15,8 @@ */ package io.micronaut.inject.processing; +import io.micronaut.aop.internal.intercepted.InterceptedMethodUtil; +import io.micronaut.aop.writer.AopProxyWriter; import io.micronaut.context.annotation.Bean; import io.micronaut.context.annotation.ConfigurationReader; import io.micronaut.context.annotation.EachProperty; @@ -120,8 +122,7 @@ void visitBeanFactoryElement(BeanDefinitionVisitor visitor, ClassElement produce // producingElement = producingElement.withAnnotationMetadata(producedTypeAnnotationMetadata); buildProducedBeanDefinition(producedBeanDefinitionWriter, producedType, producingElement, producedType.getAnnotationMetadata()); - if (producingElement instanceof MethodElement) { - MethodElement methodElement = (MethodElement) producingElement; + if (producingElement instanceof MethodElement methodElement) { if (isAopProxy && visitAopMethod(visitor, methodElement)) { return; } @@ -182,7 +183,7 @@ private void buildProducedBeanDefinition(BeanDefinitionWriter producedBeanDefini producedBeanDefinitionWriter.visitBeanFactoryField(classElement, (FieldElement) producingElement); } - if (hasAroundStereotype(producedAnnotationMetadata) && !producedType.isAssignable("io.micronaut.aop.Interceptor")) { + if (InterceptedMethodUtil.hasAroundStereotype(producedAnnotationMetadata) && !producedType.isAssignable("io.micronaut.aop.Interceptor")) { if (producedType.isArray()) { throw new ProcessingException(producingElement, "Cannot apply AOP advice to arrays"); } @@ -212,7 +213,7 @@ private void buildProducedBeanDefinition(BeanDefinitionWriter producedBeanDefini } } - BeanDefinitionVisitor aopProxyWriter = aopHelper.createAroundAopProxyWriter(producedBeanDefinitionWriter, producedAnnotationMetadata, visitorContext, true); + AopProxyWriter aopProxyWriter = createAroundAopProxyWriter(producedBeanDefinitionWriter, producedAnnotationMetadata, visitorContext, true); if (constructorElement != null) { aopProxyWriter.visitBeanDefinitionConstructor(constructorElement, constructorElement.isReflectionRequired(), visitorContext); } else { @@ -226,7 +227,7 @@ private void buildProducedBeanDefinition(BeanDefinitionWriter producedBeanDefini .stream() .filter(m -> m.isPublic() && !m.isFinal() && !m.isStatic()).toList(); methodElements - .forEach(methodElement -> aopHelper.visitAroundMethod(aopProxyWriter, methodElement.getDeclaringType(), methodElement)); + .forEach(methodElement -> visitAroundMethod(aopProxyWriter, methodElement.getDeclaringType(), methodElement)); } else if (producedAnnotationMetadata.hasStereotype(Executable.class)) { if (producedType.isArray()) { diff --git a/core-processor/src/main/java/io/micronaut/inject/processing/IntroductionInterfaceBeanElementCreator.java b/core-processor/src/main/java/io/micronaut/inject/processing/IntroductionInterfaceBeanElementCreator.java index c0a7f7899a5..f717d4284f4 100644 --- a/core-processor/src/main/java/io/micronaut/inject/processing/IntroductionInterfaceBeanElementCreator.java +++ b/core-processor/src/main/java/io/micronaut/inject/processing/IntroductionInterfaceBeanElementCreator.java @@ -35,16 +35,13 @@ @Internal final class IntroductionInterfaceBeanElementCreator extends AbstractBeanElementCreator { - private final String factoryBeanDefinitionName; - - IntroductionInterfaceBeanElementCreator(ClassElement classElement, VisitorContext visitorContext, String factoryBeanDefinitionName) { + IntroductionInterfaceBeanElementCreator(ClassElement classElement, VisitorContext visitorContext) { super(classElement, visitorContext); - this.factoryBeanDefinitionName = factoryBeanDefinitionName; } @Override public void buildInternal() { - BeanDefinitionVisitor aopProxyWriter = aopHelper.createIntroductionAopProxyWriter(classElement, visitorContext); + BeanDefinitionVisitor aopProxyWriter = createIntroductionAopProxyWriter(classElement, visitorContext); aopProxyWriter.visitTypeArguments(classElement.getAllTypeArguments()); // Because we add validated interceptor in some cases, this needs to run before the constructor visit @@ -65,16 +62,13 @@ public void buildInternal() { } else { aopProxyWriter.visitDefaultConstructor(classElement, visitorContext); } - if (factoryBeanDefinitionName != null) { - aopProxyWriter.visitSuperBeanDefinitionFactory(factoryBeanDefinitionName); - } // The introduction will include overridden methods* (find(List) <- find(Iterable)*) but ordinary class introduction doesn't // Because of the caching we need to process declared methods first List methods = new ArrayList<>(classElement.getEnclosedElements(ElementQuery.ALL_METHODS.includeHiddenElements().includeOverriddenMethods())); Collections.reverse(methods); // reverse to process hierarchy starting from declared methods for (MethodElement methodElement : methods) { - aopHelper.visitIntrospectedMethod(aopProxyWriter, classElement, methodElement); + visitIntrospectedMethod(aopProxyWriter, classElement, methodElement); } beanDefinitionWriters.add(aopProxyWriter); } diff --git a/core-processor/src/main/java/io/micronaut/inject/processing/JavaModelUtils.java b/core-processor/src/main/java/io/micronaut/inject/processing/JavaModelUtils.java index 9e84d40faff..3f503fdda31 100644 --- a/core-processor/src/main/java/io/micronaut/inject/processing/JavaModelUtils.java +++ b/core-processor/src/main/java/io/micronaut/inject/processing/JavaModelUtils.java @@ -280,8 +280,7 @@ public static Type getTypeReference(TypedElement type) { } } else { Object nativeType = type.getNativeType(); - if (nativeType instanceof Class) { - Class t = (Class) nativeType; + if (nativeType instanceof Class t) { return Type.getType(t); } else { String internalName = type.getType().getName().replace('.', '/'); diff --git a/core-processor/src/main/java/io/micronaut/inject/writer/AbstractAnnotationMetadataWriter.java b/core-processor/src/main/java/io/micronaut/inject/writer/AbstractAnnotationMetadataWriter.java index 743863a0ce0..fbbe7b01bdd 100644 --- a/core-processor/src/main/java/io/micronaut/inject/writer/AbstractAnnotationMetadataWriter.java +++ b/core-processor/src/main/java/io/micronaut/inject/writer/AbstractAnnotationMetadataWriter.java @@ -84,7 +84,7 @@ protected AbstractAnnotationMetadataWriter( Element originatingElement, AnnotationMetadata annotationMetadata, boolean writeAnnotationDefaults) { - super(new Element[]{ originatingElement }); + super(originatingElement); this.targetClassType = getTypeReferenceForName(className); this.annotationMetadata = annotationMetadata.getTargetAnnotationMetadata(); this.writeAnnotationDefault = writeAnnotationDefaults; diff --git a/core-processor/src/main/java/io/micronaut/inject/writer/BeanDefinitionReferenceWriter.java b/core-processor/src/main/java/io/micronaut/inject/writer/BeanDefinitionReferenceWriter.java index 638bf6d4253..83ccfa572f0 100644 --- a/core-processor/src/main/java/io/micronaut/inject/writer/BeanDefinitionReferenceWriter.java +++ b/core-processor/src/main/java/io/micronaut/inject/writer/BeanDefinitionReferenceWriter.java @@ -41,7 +41,6 @@ import java.io.IOException; import java.io.OutputStream; import java.util.HashMap; -import java.util.LinkedHashMap; import java.util.Map; /** diff --git a/core-processor/src/main/java/io/micronaut/inject/writer/BeanDefinitionWriter.java b/core-processor/src/main/java/io/micronaut/inject/writer/BeanDefinitionWriter.java index bd20455ab9c..5ee28c105e8 100644 --- a/core-processor/src/main/java/io/micronaut/inject/writer/BeanDefinitionWriter.java +++ b/core-processor/src/main/java/io/micronaut/inject/writer/BeanDefinitionWriter.java @@ -2324,7 +2324,7 @@ private void autoApplyNamedIfPresent(Element element, AnnotationMetadata annotat } private void autoApplyNamed(Element element) { - if (!element.stringValue(AnnotationUtil.NAMED).isPresent()) { + if (element.stringValue(AnnotationUtil.NAMED).isEmpty()) { element.annotate(AnnotationUtil.NAMED, (builder) -> { final String name; @@ -3071,7 +3071,7 @@ private void visitBuildFactoryMethodDefinition( int constructorIndex = initInterceptedConstructorWriter( buildMethodVisitor, parameterList, - new FactoryMethodDef(factoryType, factoryElement, methodDescriptor, factoryVar) + new FactoryMethodDef(factoryType, factoryElement, methodDescriptor, factoryVar) ); // populate an Object[] of all constructor arguments final int parametersIndex = createParameterArray(parameterList, buildMethodVisitor); @@ -3471,10 +3471,10 @@ private InnerClassDef newInnerClass(Class superType) { ); classWriter.visitInnerClass(constructorInternalName, beanDefinitionInternalName, null, ACC_PRIVATE); return new InnerClassDef( - interceptedConstructorWriterName, - interceptedConstructorWriter, - constructorInternalName, - interceptedConstructorType + interceptedConstructorWriterName, + interceptedConstructorWriter, + constructorInternalName, + interceptedConstructorType ); } @@ -4614,7 +4614,7 @@ public boolean isPreDestroy() { } - private class FactoryMethodDef { + private static class FactoryMethodDef { private final Type factoryType; private final Element factoryMethod; private final String methodDescriptor; @@ -4628,7 +4628,7 @@ public FactoryMethodDef(Type factoryType, Element factoryMethod, String methodDe } } - private class InnerClassDef { + private static class InnerClassDef { private final ClassWriter innerClassWriter; private final String constructorInternalName; private final Type innerClassType; diff --git a/core-processor/src/main/java/io/micronaut/inject/writer/DirectoryClassWriterOutputVisitor.java b/core-processor/src/main/java/io/micronaut/inject/writer/DirectoryClassWriterOutputVisitor.java index 3f91e5b1a8d..45908a57222 100644 --- a/core-processor/src/main/java/io/micronaut/inject/writer/DirectoryClassWriterOutputVisitor.java +++ b/core-processor/src/main/java/io/micronaut/inject/writer/DirectoryClassWriterOutputVisitor.java @@ -15,14 +15,13 @@ */ package io.micronaut.inject.writer; -import io.micronaut.core.annotation.Nullable; import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.Nullable; import io.micronaut.inject.ast.Element; import java.io.File; import java.io.IOException; import java.io.OutputStream; -import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardOpenOption; @@ -67,7 +66,7 @@ public void visitServiceDescriptor(String type, String classname, Element origin try { final Path filePath = targetDir.toPath().resolve(path); makeParent(filePath); - Files.write(filePath, "".getBytes(StandardCharsets.UTF_8), + Files.writeString(filePath, "", StandardOpenOption.WRITE, StandardOpenOption.CREATE ); From 4a1240b944db49a5b8173a24b153f2a0306a0722 Mon Sep 17 00:00:00 2001 From: Graeme Rocher Date: Tue, 22 Nov 2022 13:04:06 +0100 Subject: [PATCH 252/743] Remove NoInjectBeanDefinition and improve/refactor RuntimeBeanDefinition (#8398) There is overlap between NoInjectBeanDefinition and `RuntimeBeanDefinition` and since the latter provides a richer API for the registration of `RuntimeBeanDefinition` this PR removes `NoInjectBeanDefinition` and simplifies / refactors bean resolution. --- .../context/BeanDefinitionDelegate.java | 4 - .../context/DefaultApplicationContext.java | 27 +- .../micronaut/context/DefaultBeanContext.java | 223 +++++++--------- .../context/DefaultRuntimeBeanDefinition.java | 50 +++- .../context/NoInjectionBeanDefinition.java | 242 ------------------ .../context/RuntimeBeanDefinition.java | 15 ++ .../io/micronaut/context/SingletonScope.java | 26 +- 7 files changed, 189 insertions(+), 398 deletions(-) delete mode 100644 inject/src/main/java/io/micronaut/context/NoInjectionBeanDefinition.java diff --git a/inject/src/main/java/io/micronaut/context/BeanDefinitionDelegate.java b/inject/src/main/java/io/micronaut/context/BeanDefinitionDelegate.java index 7155f161286..1669e9c7c66 100644 --- a/inject/src/main/java/io/micronaut/context/BeanDefinitionDelegate.java +++ b/inject/src/main/java/io/micronaut/context/BeanDefinitionDelegate.java @@ -63,10 +63,6 @@ class BeanDefinitionDelegate extends AbstractBeanContextConditional implement @Nullable protected final Qualifier qualifier; - private BeanDefinitionDelegate(BeanDefinition definition) { - this(definition, null); - } - private BeanDefinitionDelegate(BeanDefinition definition, @Nullable Qualifier qualifier) { this.definition = definition; this.qualifier = qualifier; diff --git a/inject/src/main/java/io/micronaut/context/DefaultApplicationContext.java b/inject/src/main/java/io/micronaut/context/DefaultApplicationContext.java index 3ad1e70acf9..795f0307847 100644 --- a/inject/src/main/java/io/micronaut/context/DefaultApplicationContext.java +++ b/inject/src/main/java/io/micronaut/context/DefaultApplicationContext.java @@ -51,6 +51,7 @@ import java.util.Iterator; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Optional; import java.util.function.Predicate; import java.util.stream.Collectors; @@ -146,10 +147,7 @@ private boolean isBootstrapEnabled(ApplicationContextConfiguration configuration return Boolean.parseBoolean(bootstrapContextProp); } Boolean configBootstrapEnabled = configuration.isBootstrapEnvironmentEnabled(); - if (configBootstrapEnabled != null) { - return configBootstrapEnabled; - } - return isBootstrapPropertySourceLocatorPresent(); + return Objects.requireNonNullElseGet(configBootstrapEnabled, this::isBootstrapPropertySourceLocatorPresent); } private boolean isBootstrapPropertySourceLocatorPresent() { @@ -236,7 +234,22 @@ protected synchronized void registerConfiguration(BeanConfiguration configuratio protected void startEnvironment() { Environment defaultEnvironment = getEnvironment(); defaultEnvironment.start(); - registerSingleton(Environment.class, defaultEnvironment, null, false); + RuntimeBeanDefinition.Builder definition; + if (defaultEnvironment instanceof DefaultEnvironment de) { + definition = RuntimeBeanDefinition + .builder(DefaultEnvironment.class, () -> de); + } else { + definition = RuntimeBeanDefinition + .builder(Environment.class, () -> defaultEnvironment); + } + + //noinspection unchecked + definition = definition + .singleton(true) + .qualifier(PrimaryQualifier.INSTANCE); + + //noinspection resource + registerBeanDefinition(definition.build()); } @Override @@ -508,8 +521,8 @@ private void transformEachBeanBeanDefinition(BeanResolutionContext resolutio } } - if (qualifier instanceof Named) { - delegate.put(Named.class.getName(), ((Named) qualifier).getName()); + if (qualifier instanceof Named named) { + delegate.put(Named.class.getName(), named.getName()); } if (delegate.isEnabled(this, resolutionContext)) { transformedCandidates.add((BeanDefinition) delegate); diff --git a/inject/src/main/java/io/micronaut/context/DefaultBeanContext.java b/inject/src/main/java/io/micronaut/context/DefaultBeanContext.java index 6ad68624a9d..9ef92c7f96a 100644 --- a/inject/src/main/java/io/micronaut/context/DefaultBeanContext.java +++ b/inject/src/main/java/io/micronaut/context/DefaultBeanContext.java @@ -727,51 +727,36 @@ public BeanContext registerSingleton(@NonNull Class type, @NonNull T sing beanDefinition = findBeanDefinition(type, qualifier).orElse(null); if (beanDefinition == null) { // Purge cache miss - beanCandidateCache.entrySet().removeIf(entry -> entry.getKey().isInstance(singleton)); - beanConcreteCandidateCache.entrySet().removeIf(entry -> entry.getKey().beanType.isInstance(singleton)); + purgeCacheForBeanInstance(singleton); } } else { beanDefinition = null; } - if (beanDefinition != null && beanDefinition.getBeanType().isInstance(singleton)) { + if (beanDefinition != null && !(beanDefinition instanceof RuntimeBeanDefinition) && beanDefinition.getBeanType().isInstance(singleton)) { try (BeanResolutionContext context = newResolutionContext(beanDefinition, null)) { doInject(context, singleton, beanDefinition); DefaultBeanContext.BeanKey key = new DefaultBeanContext.BeanKey<>(beanDefinition.asArgument(), qualifier); singletonScope.registerSingletonBean(BeanRegistration.of(this, key, beanDefinition, singleton), qualifier); } } else { - NoInjectionBeanDefinition dynamicRegistration = new NoInjectionBeanDefinition<>(singleton.getClass(), qualifier); - if (qualifier instanceof Named) { - final BeanDefinitionDelegate delegate = BeanDefinitionDelegate.create(dynamicRegistration); - delegate.put(BeanDefinition.NAMED_ATTRIBUTE, ((Named) qualifier).getName()); - beanDefinition = delegate; - } else { - beanDefinition = dynamicRegistration; - } - beanDefinitionsClasses.add(dynamicRegistration); - DefaultBeanContext.BeanKey key = new DefaultBeanContext.BeanKey<>(beanDefinition.asArgument(), qualifier); - singletonScope.registerSingletonBean(BeanRegistration.of(this, key, dynamicRegistration, singleton), qualifier); + RuntimeBeanDefinition runtimeBeanDefinition = RuntimeBeanDefinition.builder(type, () -> singleton) + .singleton(true) + .qualifier(qualifier) + .build(); + + var registration = BeanRegistration.of( + this, + new BeanKey<>(runtimeBeanDefinition, qualifier), + runtimeBeanDefinition, + singleton + ); + singletonScope.registerSingletonBean(registration, qualifier); + registerBeanDefinition(runtimeBeanDefinition); - for (Class indexedType : indexedTypes) { + for (Class indexedType : indexedTypes) { if (indexedType == type || indexedType.isAssignableFrom(type)) { final Collection indexed = resolveTypeIndex(indexedType); - BeanDefinition finalBeanDefinition = beanDefinition; - indexed.add(new AbstractBeanDefinitionReference(type.getName(), type.getName()) { - @Override - protected Class> getBeanDefinitionType() { - return (Class>) finalBeanDefinition.getClass(); - } - - @Override - public BeanDefinition load() { - return finalBeanDefinition; - } - - @Override - public Class getBeanType() { - return type; - } - }); + indexed.add(runtimeBeanDefinition); break; } } @@ -1641,7 +1626,7 @@ public Collection> getBeanDefinitions(@Nullable Qualifier> getAllBeanDefinitions() { @Override public Collection> getBeanDefinitionReferences() { if (!beanDefinitionsClasses.isEmpty()) { - final List refs = beanDefinitionsClasses.stream().filter(ref -> ref.isEnabled(this)) - .collect(Collectors.toList()); + final List refs = beanDefinitionsClasses.stream() + .filter(ref -> ref.isEnabled(this)) + .toList(); - return Collections.unmodifiableList(refs); + return refs; } return Collections.emptyList(); } @@ -1685,14 +1671,32 @@ public Collection> getBeanDefinitionReferences() { @NonNull public BeanContext registerBeanDefinition(@NonNull RuntimeBeanDefinition definition) { Objects.requireNonNull(definition, "Bean definition cannot be null"); + BeanDefinition existing = findBeanDefinition(definition.getGenericBeanType(), definition.getDeclaredQualifier()).orElse(null); + if (existing instanceof RuntimeBeanDefinition runtimeBeanDefinition) { + this.beanDefinitionsClasses.remove(runtimeBeanDefinition); + } + for (Class indexedType : indexedTypes) { + if (definition.isCandidateBean(Argument.of(indexedType))) { + Collection index = resolveTypeIndex(indexedType); + if (existing instanceof RuntimeBeanDefinition runtimeBeanDefinition) { + index.remove(runtimeBeanDefinition); + } + index.add(definition); + } + } this.beanDefinitionsClasses.add(definition); - beanCandidateCache.entrySet().removeIf(entry -> entry.getKey().isAssignableFrom(definition.getBeanType())); - beanConcreteCandidateCache.entrySet().removeIf(entry -> entry.getKey().beanType.isAssignableFrom(definition.getBeanType())); - singletonBeanRegistrations.entrySet().removeIf(entry -> entry.getKey().beanType.isAssignableFrom(definition.getBeanType())); - containsBeanCache.entrySet().removeIf(entry -> entry.getKey().beanType.isAssignableFrom(definition.getBeanType())); + Class beanType = definition.getBeanType(); + purgeCacheForBeanType(beanType); return this; } + private void purgeCacheForBeanType(Class beanType) { + beanCandidateCache.entrySet().removeIf(entry -> entry.getKey().isAssignableFrom(beanType)); + beanConcreteCandidateCache.entrySet().removeIf(entry -> entry.getKey().beanType.isAssignableFrom(beanType)); + singletonBeanRegistrations.entrySet().removeIf(entry -> entry.getKey().beanType.isAssignableFrom(beanType)); + containsBeanCache.entrySet().removeIf(entry -> entry.getKey().beanType.isAssignableFrom(beanType)); + } + /** * Get a bean of the given type. * @@ -2039,7 +2043,7 @@ protected void initializeContext( throw new BeanInstantiationException("Bean definition [" + contextScopeBean.getName() + "] could not be loaded: " + e.getMessage(), e); } } - filterProxiedTypes((Collection) contextBeans, true, false, null); + filterProxiedTypes((Collection) contextBeans, false); filterReplacedBeans(null, (Collection) contextBeans); OrderUtil.sort(contextBeans); for (BeanDefinition contextScopeDefinition : contextBeans) { @@ -2251,7 +2255,7 @@ private Set> collectBeanCandidates( if (!candidates.isEmpty()) { if (filterProxied) { - filterProxiedTypes(candidates, true, false, null); + filterProxiedTypes(candidates, false); } filterReplacedBeans(resolutionContext, candidates); } @@ -2325,8 +2329,7 @@ protected Collection findBeanCandidatesForInstance(@NonNull candidates = candidates .stream() .filter(candidate -> - !(candidate instanceof NoInjectionBeanDefinition) && - candidate.getBeanType() == beanClass + candidate.getBeanType() == beanClass ) .collect(Collectors.toList()); } @@ -2639,7 +2642,7 @@ protected void processParallelBeans(List parallelBeans) } }); - filterProxiedTypes((Collection) parallelDefinitions, true, false, null); + filterProxiedTypes((Collection) parallelDefinitions, false); filterReplacedBeans(null, (Collection) parallelDefinitions); parallelDefinitions.forEach(beanDefinition -> ForkJoinPool.commonPool().execute(() -> { @@ -3318,20 +3321,9 @@ private Optional> findConcreteCandidateNoCache(@Nullable B boolean throwNonUnique, boolean filterProxied) { - Predicate> predicate = candidate -> { - if (candidate.isAbstract()) { - return false; - } - if (qualifier != null) { - if (candidate instanceof NoInjectionBeanDefinition noInjectionBeanDefinition) { - return qualifier.contains(noInjectionBeanDefinition.getQualifier()); - } - } - return true; - }; - - Collection> candidates = new ArrayList<>(findBeanCandidates(resolutionContext, beanType, filterProxied, predicate)); - return pickOneBean(beanType, qualifier, throwNonUnique, filterProxied, predicate, candidates); + Predicate> predicate = candidate -> !candidate.isAbstract(); + Collection> candidates = findBeanCandidates(resolutionContext, beanType, filterProxied, predicate); + return pickOneBean(beanType, qualifier, throwNonUnique, candidates); } @NonNull @@ -3339,61 +3331,44 @@ private Optional> pickOneBean( Argument beanType, Qualifier qualifier, boolean throwNonUnique, - boolean filterProxied, - Predicate> predicate, Collection> candidates) { if (candidates.isEmpty()) { return Optional.empty(); } - filterProxiedTypes(candidates, filterProxied, false, predicate); + BeanDefinition definition; + if (qualifier != null) { + if (LOG.isDebugEnabled()) { + LOG.debug("Qualifying bean [{}] for qualifier: {} ", beanType.getName(), qualifier); + } - int size = candidates.size(); - BeanDefinition definition = null; - if (size > 0) { - if (qualifier != null) { + Stream> qualified = qualifier.reduce(beanType.getType(), candidates.stream()); + List> beanDefinitionList = qualified.toList(); + if (beanDefinitionList.isEmpty()) { if (LOG.isDebugEnabled()) { - LOG.debug("Qualifying bean [{}] for qualifier: {} ", beanType.getName(), qualifier); + LOG.debug("No qualifying beans of type [{}] found for qualifier: {} ", beanType.getName(), qualifier); } + return Optional.empty(); + } - Stream> candidateStream = candidates.stream().filter(c -> { - if (!c.isAbstract()) { - if (c instanceof NoInjectionBeanDefinition noInjectionBeanDefinition) { - return qualifier.contains(noInjectionBeanDefinition.getQualifier()); - } - return true; - } - return false; - }); - - Stream> qualified = qualifier.reduce(beanType.getType(), candidateStream); - List> beanDefinitionList = qualified.collect(Collectors.toList()); - if (beanDefinitionList.isEmpty()) { - if (LOG.isDebugEnabled()) { - LOG.debug("No qualifying beans of type [{}] found for qualifier: {} ", beanType.getName(), qualifier); - } - return Optional.empty(); + definition = lastChanceResolve( + beanType, + qualifier, + throwNonUnique, + beanDefinitionList + ); + } else { + if (candidates.size() == 1) { + definition = candidates.iterator().next(); + } else { + if (LOG.isDebugEnabled()) { + LOG.debug("Searching for @Primary for type [{}] from candidates: {} ", beanType.getName(), candidates); } - definition = lastChanceResolve( beanType, - qualifier, + null, throwNonUnique, - beanDefinitionList + candidates ); - } else { - if (candidates.size() == 1) { - definition = candidates.iterator().next(); - } else { - if (LOG.isDebugEnabled()) { - LOG.debug("Searching for @Primary for type [{}] from candidates: {} ", beanType.getName(), candidates); - } - definition = lastChanceResolve( - beanType, - qualifier, - throwNonUnique, - candidates - ); - } } } if (LOG.isDebugEnabled() && definition != null) { @@ -3408,9 +3383,7 @@ private Optional> pickOneBean( private void filterProxiedTypes( Collection> candidates, - boolean filterProxied, - boolean filterDelegates, - Predicate> predicate) { + boolean filterDelegates) { int count = candidates.size(); Set> proxiedTypes = new HashSet<>(count); Iterator> i = candidates.iterator(); @@ -3418,21 +3391,12 @@ private void filterProxiedTypes( while (i.hasNext()) { BeanDefinition candidate = i.next(); if (candidate instanceof ProxyBeanDefinition proxyBeanDefinition) { - if (filterProxied) { - proxiedTypes.add(proxyBeanDefinition.getTargetDefinitionType()); - } else { - proxiedTypes.add(candidate.getClass()); - } - } else if (candidate instanceof BeanDefinitionDelegate) { - BeanDefinition delegate = ((BeanDefinitionDelegate) candidate).getDelegate(); - if (filterDelegates) { - i.remove(); + proxiedTypes.add(proxyBeanDefinition.getTargetDefinitionType()); + } else if (filterDelegates && candidate instanceof BeanDefinitionDelegate delegate) { + i.remove(); - if (!delegates.contains(delegate) && (predicate == null || predicate.test(delegate))) { - delegates.add(delegate); - } - } else if (filterProxied && delegate instanceof ProxyBeanDefinition proxyBeanDefinition) { - proxiedTypes.add(proxyBeanDefinition.getTargetDefinitionType()); + if (!delegates.contains(delegate)) { + delegates.add(delegate); } } } @@ -3441,13 +3405,14 @@ private void filterProxiedTypes( } if (!proxiedTypes.isEmpty()) { candidates.removeIf(candidate -> { - if (candidate instanceof BeanDefinitionDelegate) { - return proxiedTypes.contains(((BeanDefinitionDelegate) candidate).getDelegate().getClass()); + if (candidate instanceof BeanDefinitionDelegate delegate) { + return proxiedTypes.contains(delegate.getDelegate().getClass()); } else { return proxiedTypes.contains(candidate.getClass()); } }); } + } private BeanDefinition lastChanceResolve(Argument beanType, @@ -3655,11 +3620,13 @@ public Collection> getBeanRegistrations(@Nullable BeanRe } Collection> beanDefinitions = findBeanCandidatesInternal(resolutionContext, beanType); - Stream> candidateStream = applyBeanResolutionFilters(resolutionContext, beanDefinitions.stream()); - if (qualifier != null) { - candidateStream = qualifier.reduce(beanType.getType(), candidateStream); + if (!beanDefinitions.isEmpty()) { + Stream> candidateStream = applyBeanResolutionFilters(resolutionContext, beanDefinitions.stream()); + if (qualifier != null) { + candidateStream = qualifier.reduce(beanType.getType(), candidateStream); + } + beanDefinitions = candidateStream.toList(); } - beanDefinitions = candidateStream.collect(Collectors.toList()); Collection> beanRegistrations; if (beanDefinitions.isEmpty()) { @@ -3746,8 +3713,8 @@ private Stream> applyBeanResolutionFilters(@Nullable BeanR candidateStream = candidateStream.filter(c -> { if (c.equals(declaringBean)) { return false; - } else if (declaringBean instanceof ProxyBeanDefinition) { - return !((ProxyBeanDefinition) declaringBean).getTargetDefinitionType().equals(c.getClass()); + } else if (declaringBean instanceof ProxyBeanDefinition proxyBeanDefinition) { + return !proxyBeanDefinition.getTargetDefinitionType().equals(c.getClass()); } return true; }); @@ -3775,8 +3742,8 @@ private void addCandidateToList(@Nullable BeanResolutionContext resolutionCo if (beanRegistration != null) { if (candidate.isContainerType()) { Object container = beanRegistration.bean; - if (container instanceof Object[]) { - container = Arrays.asList((Object[]) container); + if (container instanceof Object[] array) { + container = Arrays.asList(array); } if (container instanceof Iterable) { Iterable iterable = (Iterable) container; diff --git a/inject/src/main/java/io/micronaut/context/DefaultRuntimeBeanDefinition.java b/inject/src/main/java/io/micronaut/context/DefaultRuntimeBeanDefinition.java index b1240f47a3b..94a3ad2098d 100644 --- a/inject/src/main/java/io/micronaut/context/DefaultRuntimeBeanDefinition.java +++ b/inject/src/main/java/io/micronaut/context/DefaultRuntimeBeanDefinition.java @@ -26,10 +26,13 @@ import io.micronaut.core.util.CollectionUtils; import io.micronaut.inject.BeanDefinition; import io.micronaut.inject.qualifiers.PrimaryQualifier; +import io.micronaut.inject.qualifiers.TypeArgumentQualifier; import java.lang.annotation.Annotation; import java.util.Arrays; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.Set; @@ -54,6 +57,7 @@ final class DefaultRuntimeBeanDefinition extends AbstractBeanContextCondition private final boolean isSingleton; private final Class scope; private final Class[] exposedTypes; + private final Map, List>> typeArguments; DefaultRuntimeBeanDefinition( @NonNull Argument beanType, @@ -62,7 +66,8 @@ final class DefaultRuntimeBeanDefinition extends AbstractBeanContextCondition @Nullable AnnotationMetadata annotationMetadata, boolean isSingleton, @Nullable Class scope, - Class[] exposedTypes) { + Class[] exposedTypes, Map, + List>> typeArguments) { Objects.requireNonNull(beanType, MSG_BEAN_TYPE_CANNOT_BE_NULL); Objects.requireNonNull(supplier, "Bean supplier cannot be null"); @@ -74,6 +79,21 @@ final class DefaultRuntimeBeanDefinition extends AbstractBeanContextCondition this.isSingleton = isSingleton; this.scope = scope; this.exposedTypes = exposedTypes; + this.typeArguments = typeArguments; + } + + @Override + public List> getTypeArguments(Class type) { + if (type == getBeanType()) { + return getTypeArguments(); + } + if (typeArguments != null) { + List> args = typeArguments.get(type); + if (args != null) { + return args; + } + } + return RuntimeBeanDefinition.super.getTypeArguments(type); } @Override @@ -190,7 +210,7 @@ public T build(BeanResolutionContext resolutionContext, * @param The bean */ static final class RuntimeBeanBuilder implements RuntimeBeanDefinition.Builder { - private final Argument beanType; + private Argument beanType; private final Supplier supplier; private Qualifier qualifier; private boolean singleton; @@ -198,6 +218,8 @@ static final class RuntimeBeanBuilder implements RuntimeBeanDefinition.Builde private Class scope; private Class[] exposedTypes = ReflectionUtils.EMPTY_CLASS_ARRAY; + private Map, List>> typeArguments; + RuntimeBeanBuilder(Argument beanType, Supplier supplier) { this.beanType = Objects.requireNonNull(beanType, MSG_BEAN_TYPE_CANNOT_BE_NULL); this.supplier = Objects.requireNonNull(supplier, "Bean supplier cannot be null"); @@ -206,6 +228,12 @@ static final class RuntimeBeanBuilder implements RuntimeBeanDefinition.Builde @Override public Builder qualifier(Qualifier qualifier) { this.qualifier = qualifier; + if (qualifier instanceof TypeArgumentQualifier typeArgumentQualifier) { + Argument[] arguments = Arrays.stream(typeArgumentQualifier.getTypeArguments()) + .map(Argument::of) + .toArray(Argument[]::new); + typeArguments(arguments); + } return this; } @@ -236,6 +264,21 @@ public Builder exposedTypes(Class... types) { return this; } + @Override + public Builder typeArguments(Argument... arguments) { + this.beanType = Argument.of(beanType.getType(), arguments); + return this; + } + + @Override + public Builder typeArguments(Class implementedType, Argument... arguments) { + if (typeArguments == null) { + typeArguments = new LinkedHashMap<>(5); + } + typeArguments.put(implementedType, List.of(arguments)); + return this; + } + @Override public Builder annotationMetadata(AnnotationMetadata annotationMetadata) { this.annotationMetadata = annotationMetadata; @@ -252,7 +295,8 @@ public RuntimeBeanDefinition build() { annotationMetadata, singleton, scope, - exposedTypes + exposedTypes, + typeArguments ); } } diff --git a/inject/src/main/java/io/micronaut/context/NoInjectionBeanDefinition.java b/inject/src/main/java/io/micronaut/context/NoInjectionBeanDefinition.java deleted file mode 100644 index 400763f7a2a..00000000000 --- a/inject/src/main/java/io/micronaut/context/NoInjectionBeanDefinition.java +++ /dev/null @@ -1,242 +0,0 @@ -/* - * Copyright 2017-2022 original authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.micronaut.context; - -import io.micronaut.core.annotation.AnnotationUtil; -import io.micronaut.core.annotation.Internal; -import io.micronaut.core.annotation.NonNull; -import io.micronaut.core.annotation.Nullable; -import io.micronaut.core.reflect.GenericTypeUtils; -import io.micronaut.core.type.Argument; -import io.micronaut.inject.BeanDefinition; -import io.micronaut.inject.BeanDefinitionReference; -import io.micronaut.inject.ConstructorInjectionPoint; -import io.micronaut.inject.ExecutableMethod; -import io.micronaut.inject.FieldInjectionPoint; -import io.micronaut.inject.MethodInjectionPoint; - -import java.lang.annotation.Annotation; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Optional; -import java.util.function.Function; -import java.util.stream.Collectors; -import java.util.stream.Stream; - -/** - * Manual injection bean definition. - * - * @param The bean type - * @since 3.5.0 - */ -@Internal -final class NoInjectionBeanDefinition implements BeanDefinition, BeanDefinitionReference { - private final Class singletonClass; - private final Map, List>> typeArguments = new HashMap<>(); - - private final Qualifier qualifier; - - /** - * @param singletonClass The singleton class - * @param qualifier The qualifier - */ - NoInjectionBeanDefinition(Class singletonClass, Qualifier qualifier) { - this.singletonClass = singletonClass; - this.qualifier = qualifier; - } - - @Nullable - public Qualifier getQualifier() { - return qualifier; - } - - @Override - public Qualifier getDeclaredQualifier() { - return getQualifier(); - } - - @Override - public Optional> getScope() { - return Optional.of(javax.inject.Singleton.class); - } - - @Override - public Optional getScopeName() { - return Optional.of(AnnotationUtil.SINGLETON); - } - - @NonNull - @Override - public List> getTypeArguments(Class type) { - List> result = typeArguments.get(type); - if (result == null) { - Class[] classes = type.isInterface() ? GenericTypeUtils.resolveInterfaceTypeArguments(singletonClass, type) : GenericTypeUtils.resolveSuperTypeGenericArguments(singletonClass, type); - result = Arrays.stream(classes).map((Function, Argument>) Argument::of).collect(Collectors.toList()); - typeArguments.put(type, result); - } - - return result; - } - - @Override - public boolean isSingleton() { - return true; - } - - @Override - public boolean isProvided() { - return false; - } - - @Override - public boolean isIterable() { - return false; - } - - @Override - public boolean isPrimary() { - return true; - } - - @Override - public Class getBeanType() { - return singletonClass; - } - - @Override - public Optional> getDeclaringType() { - return Optional.empty(); - } - - @Override - public ConstructorInjectionPoint getConstructor() { - throw new UnsupportedOperationException("Bean of type [" + getBeanType() + "] is a manually registered singleton that was registered with the context via BeanContext.registerBean(..) and cannot be created directly"); - } - - @Override - public Collection> getRequiredComponents() { - return Collections.emptyList(); - } - - @Override - public Collection> getInjectedMethods() { - return Collections.emptyList(); - } - - @Override - public Collection> getInjectedFields() { - return Collections.emptyList(); - } - - @Override - public Collection> getPostConstructMethods() { - return Collections.emptyList(); - } - - @Override - public Collection> getPreDestroyMethods() { - return Collections.emptyList(); - } - - @Override - @NonNull - public String getName() { - return singletonClass.getName(); - } - - @Override - public boolean isEnabled(BeanContext beanContext) { - return true; - } - - @Override - public boolean isEnabled(@NonNull BeanContext context, @Nullable BeanResolutionContext resolutionContext) { - return true; - } - - @Override - public Optional> findMethod(String name, Class[] argumentTypes) { - return Optional.empty(); - } - - @Override - public T inject(BeanContext context, T bean) { - return bean; - } - - @Override - public T inject(BeanResolutionContext resolutionContext, BeanContext context, T bean) { - return bean; - } - - @Override - public Collection> getExecutableMethods() { - return Collections.emptyList(); - } - - @Override - public Stream> findPossibleMethods(String name) { - return Stream.empty(); - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - - NoInjectionBeanDefinition that = (NoInjectionBeanDefinition) o; - return singletonClass.equals(that.singletonClass) && Objects.equals(qualifier, that.qualifier); - } - - @Override - public int hashCode() { - return singletonClass.hashCode(); - } - - @Override - public String getBeanDefinitionName() { - return singletonClass.getName(); - } - - @Override - public BeanDefinition load() { - return this; - } - - @Override - public BeanDefinition load(BeanContext context) { - return this; - } - - @Override - public boolean isContextScope() { - return false; - } - - @Override - public boolean isPresent() { - return true; - } -} diff --git a/inject/src/main/java/io/micronaut/context/RuntimeBeanDefinition.java b/inject/src/main/java/io/micronaut/context/RuntimeBeanDefinition.java index 45f51ba4964..c72304c6408 100644 --- a/inject/src/main/java/io/micronaut/context/RuntimeBeanDefinition.java +++ b/inject/src/main/java/io/micronaut/context/RuntimeBeanDefinition.java @@ -218,6 +218,21 @@ default Builder named(@Nullable String name) { */ Builder exposedTypes(Class...types); + /** + * The type arguments for the type. + * @param arguments The arguments + * @return This builder + */ + Builder typeArguments(Argument... arguments); + + /** + * The type arguments for an implemented type of this type. + * @param implementedType The implemented type + * @param arguments The arguments + * @return This builder + */ + Builder typeArguments(Class implementedType, Argument... arguments); + /** * The annotation metadata for the bean. * @param annotationMetadata The annotation metadata diff --git a/inject/src/main/java/io/micronaut/context/SingletonScope.java b/inject/src/main/java/io/micronaut/context/SingletonScope.java index a588968a9a1..88c6c271b08 100644 --- a/inject/src/main/java/io/micronaut/context/SingletonScope.java +++ b/inject/src/main/java/io/micronaut/context/SingletonScope.java @@ -106,10 +106,9 @@ BeanRegistration registerSingletonBean(@NonNull BeanRegistration regis DefaultBeanContext.BeanKey beanKey = new DefaultBeanContext.BeanKey<>(beanDefinition, qualifier); singletonByArgumentAndQualifier.put(beanKey, registration); } - if (beanDefinition instanceof BeanDefinitionDelegate || beanDefinition instanceof NoInjectionBeanDefinition) { + if (beanDefinition instanceof BeanDefinitionDelegate || beanDefinition instanceof RuntimeBeanDefinition) { // Special cases when custom bean definitions need to be indexed: // BeanDefinitionDelegate - doesn't really exist with a custom qualifier - // NoInjectionBeanDefinition - cannot be properly selected from 'beanDefinitionsClasses' when is used with a custom qualifier DefaultBeanContext.BeanKey beanKey = new DefaultBeanContext.BeanKey<>(beanDefinition, beanDefinition.getDeclaredQualifier()); singletonByArgumentAndQualifier.put(beanKey, registration); } @@ -329,11 +328,10 @@ void clear() { interface BeanDefinitionIdentity { static BeanDefinitionIdentity of(BeanDefinition beanDefinition) { - if (beanDefinition instanceof BeanDefinitionDelegate) { - return new BeanDefinitionDelegatedIdentity((BeanDefinitionDelegate) beanDefinition); - } - if (beanDefinition instanceof NoInjectionBeanDefinition) { - return new NoInjectionBeanDefinitionIdentity((NoInjectionBeanDefinition) beanDefinition); + if (beanDefinition instanceof BeanDefinitionDelegate definitionDelegate) { + return new BeanDefinitionDelegatedIdentity(definitionDelegate); + } else if (beanDefinition instanceof RuntimeBeanDefinition runtimeBeanDefinition) { + return new RuntimeBeanDefinitionIdentity(runtimeBeanDefinition); } return new SimpleBeanDefinitionIdentity(beanDefinition); } @@ -380,11 +378,11 @@ public int hashCode() { * * @since 3.5.0 */ - static final class NoInjectionBeanDefinitionIdentity implements BeanDefinitionIdentity { + static final class RuntimeBeanDefinitionIdentity implements BeanDefinitionIdentity { - private final NoInjectionBeanDefinition beanDefinition; + private final RuntimeBeanDefinition beanDefinition; - NoInjectionBeanDefinitionIdentity(NoInjectionBeanDefinition beanDefinition) { + RuntimeBeanDefinitionIdentity(RuntimeBeanDefinition beanDefinition) { this.beanDefinition = beanDefinition; } @@ -396,12 +394,12 @@ public boolean equals(Object o) { if (o == null || getClass() != o.getClass()) { return false; } - NoInjectionBeanDefinitionIdentity that = (NoInjectionBeanDefinitionIdentity) o; + RuntimeBeanDefinitionIdentity that = (RuntimeBeanDefinitionIdentity) o; if (beanDefinition.getBeanType() != that.beanDefinition.getBeanType()) { return false; } - Qualifier qualifier = beanDefinition.getQualifier(); - Qualifier thatQualifier = that.beanDefinition.getQualifier(); + Qualifier qualifier = beanDefinition.getDeclaredQualifier(); + Qualifier thatQualifier = that.beanDefinition.getDeclaredQualifier(); if (qualifier == thatQualifier) { return true; } @@ -410,7 +408,7 @@ public boolean equals(Object o) { @Override public int hashCode() { - return beanDefinition.getBeanType().hashCode(); + return Objects.hash(beanDefinition.getBeanType(), beanDefinition.getDeclaredQualifier()); } } From dd7ee31cc956db60d9ccfef1296182cebb6443c2 Mon Sep 17 00:00:00 2001 From: Graeme Rocher Date: Tue, 22 Nov 2022 13:09:59 +0100 Subject: [PATCH 253/743] Cleanup and remove invalid test Removed a test that was testing code that could be changed over time and since there was already a test for the same functionality was not needed --- .../beans/BeanIntrospectionSpec.groovy | 26 -------------- .../micronaut/context/DefaultBeanContext.java | 36 +++++++++---------- 2 files changed, 18 insertions(+), 44 deletions(-) diff --git a/inject-java-test/src/test/groovy/io/micronaut/inject/visitor/beans/BeanIntrospectionSpec.groovy b/inject-java-test/src/test/groovy/io/micronaut/inject/visitor/beans/BeanIntrospectionSpec.groovy index 187740eb23e..cfbdcf3ae31 100644 --- a/inject-java-test/src/test/groovy/io/micronaut/inject/visitor/beans/BeanIntrospectionSpec.groovy +++ b/inject-java-test/src/test/groovy/io/micronaut/inject/visitor/beans/BeanIntrospectionSpec.groovy @@ -2619,32 +2619,6 @@ class Test {} context?.close() } - void "test write bean introspection data for package with compiled classes"() { - given: - ApplicationContext context = buildContext('test.Test', ''' -package test; - -import io.micronaut.core.annotation.*; -import javax.validation.constraints.*; -import java.util.*; - -@Introspected(packages="io.micronaut.inject.beans.visitor", includedAnnotations=Internal.class) -class Test {} -''') - - when:"the reference is loaded" - def clazz = context.classLoader.loadClass('test.$Test$IntrospectionRef0') - BeanIntrospectionReference reference = clazz.newInstance() - - then:"The reference is valid" - reference != null - reference.getBeanType() == IntrospectedTypeElementVisitor - - - cleanup: - context?.close() - } - void "test write bean introspection data"() { given: ApplicationContext context = buildContext('test.Test', ''' diff --git a/inject/src/main/java/io/micronaut/context/DefaultBeanContext.java b/inject/src/main/java/io/micronaut/context/DefaultBeanContext.java index 9ef92c7f96a..1cee51e331a 100644 --- a/inject/src/main/java/io/micronaut/context/DefaultBeanContext.java +++ b/inject/src/main/java/io/micronaut/context/DefaultBeanContext.java @@ -158,13 +158,13 @@ public class DefaultBeanContext implements InitializableBeanContext { protected static final Logger LOG = LoggerFactory.getLogger(DefaultBeanContext.class); protected static final Logger LOG_LIFECYCLE = LoggerFactory.getLogger(DefaultBeanContext.class.getPackage().getName() + ".lifecycle"); @SuppressWarnings("rawtypes") - private static final Qualifier PROXY_TARGET_QUALIFIER = new Qualifier() { + private static final Qualifier PROXY_TARGET_QUALIFIER = new Qualifier<>() { @SuppressWarnings("rawtypes") @Override - public > Stream reduce(Class beanType, Stream candidates) { + public > Stream reduce(Class beanType, Stream candidates) { return candidates.filter(bt -> { - if (bt instanceof BeanDefinitionDelegate) { - return !(((BeanDefinitionDelegate) bt).getDelegate() instanceof ProxyBeanDefinition); + if (bt instanceof BeanDefinitionDelegate delegate) { + return !(delegate.getDelegate() instanceof ProxyBeanDefinition); } else { return !(bt instanceof ProxyBeanDefinition); } @@ -299,13 +299,13 @@ public DefaultBeanContext(@NonNull BeanContextConfiguration contextConfiguration this.classLoader = contextConfiguration.getClassLoader(); this.customScopeRegistry = Objects.requireNonNull(createCustomScopeRegistry(), "Scope registry cannot be null"); Set> eagerInitAnnotated = contextConfiguration.getEagerInitAnnotated(); - List eagerInitStereotypes = new ArrayList<>(eagerInitAnnotated.size()); + List configuredEagerSingletonAnnotations = new ArrayList<>(eagerInitAnnotated.size()); for (Class ann : eagerInitAnnotated) { - eagerInitStereotypes.add(ann.getName()); + configuredEagerSingletonAnnotations.add(ann.getName()); } - this.eagerInitStereotypes = eagerInitStereotypes.toArray(new String[0]); - this.eagerInitStereotypesPresent = !eagerInitStereotypes.isEmpty(); - this.eagerInitSingletons = eagerInitStereotypesPresent && (eagerInitStereotypes.contains(AnnotationUtil.SINGLETON) || eagerInitStereotypes.contains(Singleton.class.getName())); + this.eagerInitStereotypes = configuredEagerSingletonAnnotations.toArray(new String[0]); + this.eagerInitStereotypesPresent = !configuredEagerSingletonAnnotations.isEmpty(); + this.eagerInitSingletons = eagerInitStereotypesPresent && (configuredEagerSingletonAnnotations.contains(AnnotationUtil.SINGLETON) || configuredEagerSingletonAnnotations.contains(Singleton.class.getName())); this.beanContextConfiguration = contextConfiguration; } @@ -375,6 +375,7 @@ public synchronized BeanContext start() { */ protected void registerConversionService() { conversionService = MutableConversionService.create(); + //noinspection resource registerSingleton(MutableConversionService.class, conversionService, null, false); } @@ -393,14 +394,14 @@ void trackDisabledComponent(@NonNull Cond Argument argument = (Argument) beanType.getGenericBeanType(); @SuppressWarnings("unchecked") Qualifier declaredQualifier = (Qualifier) beanType.getDeclaredQualifier(); - this.disabledBeans.put(new BeanKey(argument, declaredQualifier), new DisabledBean<>( + this.disabledBeans.put(new BeanKey<>(argument, declaredQualifier), new DisabledBean<>( argument, declaredQualifier, reasons )); } catch (Exception | NoClassDefFoundError e) { // it is theoretically possible that resolving the generic type results in an error - // in this case just ignore this as the maps built here are purely to aid error diganosis + // in this case just ignore this as the maps built here are purely to aid error diagnosis } } else if (component instanceof BeanConfiguration configuration) { this.disabledConfigurations.put(configuration.getName(), reasons); @@ -479,7 +480,7 @@ public AnnotationMetadata resolveMetadata(Class type) { if (type == null) { return AnnotationMetadata.EMPTY_METADATA; } - return findBeanDefinition(Argument.of(type), null, false) + return findBeanDefinitionInternal(Argument.of(type), null) .map(AnnotationMetadataProvider::getAnnotationMetadata) .orElse(AnnotationMetadata.EMPTY_METADATA); } @@ -584,7 +585,7 @@ public Optional> findExecutionHandle(Class @Override public MethodExecutionHandle createExecutionHandle(BeanDefinition beanDefinition, ExecutableMethod method) { - return new MethodExecutionHandle() { + return new MethodExecutionHandle<>() { private Object target; @@ -818,8 +819,8 @@ public Optional> findBeanDefinition(Argument beanType, return findConcreteCandidate(null, beanType, qualifier, true); } - private Optional> findBeanDefinition(Argument beanType, Qualifier qualifier, boolean throwNonUnique) { - return findConcreteCandidate(null, beanType, qualifier, throwNonUnique); + private Optional> findBeanDefinitionInternal(Argument beanType, Qualifier qualifier) { + return findConcreteCandidate(null, beanType, qualifier, false); } @Override @@ -850,7 +851,7 @@ public Collection> getBeanDefinitions(Argument beanType Objects.requireNonNull(beanType, "Bean type cannot be null"); Collection> candidates = findBeanCandidatesInternal(null, beanType); if (qualifier != null) { - candidates = qualifier.reduce(beanType.getType(), new ArrayList<>(candidates).stream()).collect(Collectors.toList()); + candidates = qualifier.reduce(beanType.getType(), candidates.stream()).toList(); } return Collections.unmodifiableCollection(candidates); } @@ -979,7 +980,7 @@ protected Stream streamOfType(BeanResolutionContext resolutionContext, Cl // try and find a bean that implements the map with the generics Argument> mapType = Argument.mapOf(Argument.of(String.class), beanType); @SuppressWarnings("unchecked") Qualifier> mapQualifier = (Qualifier>) qualifier; - BeanDefinition> existingBean = findBeanDefinition(mapType, mapQualifier, false).orElse(null); + BeanDefinition> existingBean = findBeanDefinitionInternal(mapType, mapQualifier).orElse(null); if (existingBean != null) { return getBean(existingBean); } else { @@ -1007,7 +1008,6 @@ protected Stream streamOfType(BeanResolutionContext resolutionContext, Cl } } - @SuppressWarnings("unchecked") @NonNull private static String resolveKey(BeanRegistration reg) { BeanDefinition definition = reg.beanDefinition; From 0372b26e2e1dd95ba2af6aa2e10ca7ad19643e1f Mon Sep 17 00:00:00 2001 From: Jonas Konrad Date: Tue, 22 Nov 2022 16:13:10 +0100 Subject: [PATCH 254/743] Fix http2 client log warning (#8400) --- .../io/micronaut/http/client/netty/ConnectionManager.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/http-client/src/main/java/io/micronaut/http/client/netty/ConnectionManager.java b/http-client/src/main/java/io/micronaut/http/client/netty/ConnectionManager.java index 9f7d88c6284..41957a50319 100644 --- a/http-client/src/main/java/io/micronaut/http/client/netty/ConnectionManager.java +++ b/http-client/src/main/java/io/micronaut/http/client/netty/ConnectionManager.java @@ -57,6 +57,7 @@ import io.netty.handler.codec.http2.Http2FrameCodecBuilder; import io.netty.handler.codec.http2.Http2FrameLogger; import io.netty.handler.codec.http2.Http2MultiplexHandler; +import io.netty.handler.codec.http2.Http2SettingsAckFrame; import io.netty.handler.codec.http2.Http2SettingsFrame; import io.netty.handler.codec.http2.Http2StreamChannel; import io.netty.handler.codec.http2.Http2StreamChannelBootstrap; @@ -585,6 +586,11 @@ public void handlerAdded(ChannelHandlerContext ctx) throws Exception { @Override public void channelRead(@NonNull ChannelHandlerContext ctx, @NonNull Object msg) throws Exception { + if (msg instanceof Http2SettingsAckFrame) { + // this is fine + return; + } + log.warn("Unexpected message on HTTP2 connection channel: {}", msg); ReferenceCountUtil.release(msg); ctx.read(); From 8bda5af93eec3d5980472be715c37945bf3093e6 Mon Sep 17 00:00:00 2001 From: Denis Stepanov Date: Wed, 23 Nov 2022 14:51:10 +0700 Subject: [PATCH 255/743] Improve `BeanDefinitionDelegate` attributes modification (#8403) * Improve `BeanDefinitionDelegate` attributes modification * Checkstyle --- .../AbstractBeanResolutionContext.java | 10 +++++ .../context/BeanDefinitionDelegate.java | 44 +++++++++---------- .../context/BeanResolutionContext.java | 27 +++++++++++- 3 files changed, 57 insertions(+), 24 deletions(-) diff --git a/inject/src/main/java/io/micronaut/context/AbstractBeanResolutionContext.java b/inject/src/main/java/io/micronaut/context/AbstractBeanResolutionContext.java index cee2d853295..b832e3a3937 100644 --- a/inject/src/main/java/io/micronaut/context/AbstractBeanResolutionContext.java +++ b/inject/src/main/java/io/micronaut/context/AbstractBeanResolutionContext.java @@ -222,6 +222,16 @@ public final Object removeAttribute(CharSequence key) { return null; } + @Override + public Map getAttributes() { + return attributes; + } + + @Override + public void setAttributes(Map attributes) { + this.attributes = attributes; + } + @Nullable @Override public Qualifier getCurrentQualifier() { diff --git a/inject/src/main/java/io/micronaut/context/BeanDefinitionDelegate.java b/inject/src/main/java/io/micronaut/context/BeanDefinitionDelegate.java index 1669e9c7c66..b5340af54ce 100644 --- a/inject/src/main/java/io/micronaut/context/BeanDefinitionDelegate.java +++ b/inject/src/main/java/io/micronaut/context/BeanDefinitionDelegate.java @@ -23,6 +23,7 @@ import io.micronaut.core.annotation.Nullable; import io.micronaut.core.convert.ArgumentConversionContext; import io.micronaut.core.convert.ConversionService; +import io.micronaut.core.convert.MutableConversionService; import io.micronaut.core.naming.NameResolver; import io.micronaut.core.naming.Named; import io.micronaut.core.type.Argument; @@ -135,16 +136,18 @@ private boolean isPrimaryThroughAttribute() { @Override public T build(BeanResolutionContext resolutionContext, BeanContext context, BeanDefinition definition) throws BeanInstantiationException { - LinkedHashMap oldAttributes = null; + Map oldAttributes = null; if (CollectionUtils.isNotEmpty(attributes)) { - LinkedHashMap oldAttrs = new LinkedHashMap<>(attributes.size()); - attributes.forEach((key, value) -> { - Object previous = resolutionContext.setAttribute(key, value); - if (previous != null) { - oldAttrs.put(key, previous); - } - }); - oldAttributes = oldAttrs; + oldAttributes = resolutionContext.getAttributes(); + Map newAttributes; + if (oldAttributes == null) { + newAttributes = new LinkedHashMap<>(attributes); + } else { + newAttributes = new LinkedHashMap<>(attributes.size() + oldAttributes.size(), 1); + newAttributes.putAll(oldAttributes); + newAttributes.putAll(attributes); + } + resolutionContext.setAttributes(newAttributes); } try { @@ -159,14 +162,7 @@ public T build(BeanResolutionContext resolutionContext, BeanContext context, Bea throw new IllegalStateException("Cannot construct a dynamically registered singleton"); } } finally { - if (attributes != null) { - for (String key : attributes.keySet()) { - resolutionContext.removeAttribute(key); - } - } - if (oldAttributes != null) { - oldAttributes.forEach(resolutionContext::setAttribute); - } + resolutionContext.setAttributes(oldAttributes); } } @@ -176,10 +172,14 @@ private Map getParametersValues(BeanResolutionContext resolution BeanDefinition definition, ParametrizedBeanFactory parametrizedBeanFactory) { Argument[] requiredArguments = (Argument[]) parametrizedBeanFactory.getRequiredArguments(); - Map fulfilled = new LinkedHashMap<>(requiredArguments.length); + if (requiredArguments.length == 0) { + return Collections.emptyMap(); + } + MutableConversionService conversionService = context.getConversionService(); + Map fulfilled = new LinkedHashMap<>(requiredArguments.length, 1); for (Argument argument : requiredArguments) { String argumentName = argument.getName(); - Object value = resolveValueAsName(argument); + Object value = resolveValueAsName(conversionService, argument); if (value == null) { Qualifier qualifier = resolveQualifier(argument); if (qualifier == null) { @@ -211,15 +211,15 @@ private Qualifier resolveQualifier(Argument argument) { } @Nullable - private Object resolveValueAsName(Argument argument) { + private Object resolveValueAsName(ConversionService conversionService, Argument argument) { Object named = attributes == null ? null : attributes.get(Named.class.getName()); Object value = null; if (named != null) { - value = ConversionService.SHARED.convert(named, argument).orElse(null); + value = conversionService.convert(named, argument).orElse(null); } if (value == null && isPrimary()) { // Backwards compatibility, all qualifiers where "Named" before - value = ConversionService.SHARED.convert("Primary", argument).orElse(null); + value = conversionService.convert("Primary", argument).orElse(null); } return value; } diff --git a/inject/src/main/java/io/micronaut/context/BeanResolutionContext.java b/inject/src/main/java/io/micronaut/context/BeanResolutionContext.java index 7b53af46cdf..acb5af5fde0 100644 --- a/inject/src/main/java/io/micronaut/context/BeanResolutionContext.java +++ b/inject/src/main/java/io/micronaut/context/BeanResolutionContext.java @@ -169,6 +169,7 @@ default Map mapOfType(@NonNull Argument beanType, @Nullable Qu /** * Store a value within the context. + * * @param key The key * @param value The value * @return The previous value or null @@ -183,13 +184,32 @@ default Map mapOfType(@NonNull Argument beanType, @Nullable Qu /** * Remove the attribute for the given key. + * * @param key the key * @return The previous value */ Object removeAttribute(CharSequence key); /** - * Adds a bean that is created as part of the resolution. This is used to store references to instances passed to {@link BeanContext#inject(Object)} + * Get the map representing current attributes. + * + * @return All attributes + * @since 4.0.0 + */ + @Nullable + Map getAttributes(); + + /** + * Set new attributes map (The map is supposed to be mutable). + * + * @param attributes The attributes + * @since 4.0.0 + */ + void setAttributes(@Nullable Map attributes); + + /** + * Adds a bean that is created as part of the resolution. This is used to store references to instances passed to {@link BeanContext#inject(Object)}. + * * @param beanIdentifier The bean identifier * @param beanRegistration The bean registration * @param The instance type @@ -197,7 +217,8 @@ default Map mapOfType(@NonNull Argument beanType, @Nullable Qu void addInFlightBean(BeanIdentifier beanIdentifier, BeanRegistration beanRegistration); /** - * Removes a bean that is in the process of being created. This is used to store references to instances passed to {@link BeanContext#inject(Object)} + * Removes a bean that is in the process of being created. This is used to store references to instances passed to {@link BeanContext#inject(Object)}. + * * @param beanIdentifier The bean identifier */ void removeInFlightBean(BeanIdentifier beanIdentifier); @@ -226,6 +247,7 @@ default Map mapOfType(@NonNull Argument beanType, @Nullable Qu /** * Adds a dependent bean to the resolution context. + * * @param beanRegistration The bean registration * @param The generic type */ @@ -262,6 +284,7 @@ default void pushDependentBeans(@Nullable List> dependentBea * * @since 3.5.0 */ + @UsedByGeneratedCode default void markDependentAsFactory() { } From 8d8d7c0102341ac48b678a3591969d25d02dfdf4 Mon Sep 17 00:00:00 2001 From: Jonas Konrad Date: Wed, 23 Nov 2022 11:22:10 +0100 Subject: [PATCH 256/743] Fix index out of bounds when chain.proceed is called too often (#8404) No functional change, just changes the error. Fixes #8393 --- .../java/io/micronaut/http/client/netty/DefaultHttpClient.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/http-client/src/main/java/io/micronaut/http/client/netty/DefaultHttpClient.java b/http-client/src/main/java/io/micronaut/http/client/netty/DefaultHttpClient.java index c79922938a7..2e8c39c90c9 100644 --- a/http-client/src/main/java/io/micronaut/http/client/netty/DefaultHttpClient.java +++ b/http-client/src/main/java/io/micronaut/http/client/netty/DefaultHttpClient.java @@ -1638,7 +1638,7 @@ private ClientFilterChain buildChain(AtomicReference> proceed(MutableHttpRequest request) { int pos = integer.incrementAndGet(); - if (pos > len) { + if (pos >= len) { throw new IllegalStateException("The FilterChain.proceed(..) method should be invoked exactly once per filter execution. The method has instead been invoked multiple times by an erroneous filter definition."); } HttpClientFilter httpFilter = filters.get(pos); From dece40e47a876bf690a4addaa1d3058322a1e9ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Champeau?= Date: Wed, 23 Nov 2022 11:22:49 +0100 Subject: [PATCH 257/743] Remove parent module (#8405) --- .../WriteMicronautVersionInfoTask.java | 114 ------- gradle/libs.versions.toml | 4 - parent/build.gradle | 319 ------------------ settings.gradle | 1 - 4 files changed, 438 deletions(-) delete mode 100644 buildSrc/src/main/groovy/io/micronaut/build/internal/WriteMicronautVersionInfoTask.java delete mode 100644 parent/build.gradle diff --git a/buildSrc/src/main/groovy/io/micronaut/build/internal/WriteMicronautVersionInfoTask.java b/buildSrc/src/main/groovy/io/micronaut/build/internal/WriteMicronautVersionInfoTask.java deleted file mode 100644 index 28760747c65..00000000000 --- a/buildSrc/src/main/groovy/io/micronaut/build/internal/WriteMicronautVersionInfoTask.java +++ /dev/null @@ -1,114 +0,0 @@ -package io.micronaut.build.internal; - -import groovy.xml.XmlSlurper; -import groovy.xml.slurpersupport.GPathResult; -import groovy.xml.slurpersupport.NodeChild; -import org.gradle.api.DefaultTask; -import org.gradle.api.Project; -import org.gradle.api.artifacts.Configuration; -import org.gradle.api.artifacts.Dependency; -import org.gradle.api.artifacts.DependencySet; -import org.gradle.api.artifacts.result.ArtifactResolutionResult; -import org.gradle.api.artifacts.result.ComponentArtifactsResult; -import org.gradle.api.artifacts.result.ResolvedArtifactResult; -import org.gradle.api.file.DirectoryProperty; -import org.gradle.api.model.ObjectFactory; -import org.gradle.api.provider.ListProperty; -import org.gradle.api.provider.Property; -import org.gradle.api.provider.SetProperty; -import org.gradle.api.tasks.CacheableTask; -import org.gradle.api.tasks.Input; -import org.gradle.api.tasks.InputFiles; -import org.gradle.api.tasks.OutputDirectory; -import org.gradle.api.tasks.PathSensitive; -import org.gradle.api.tasks.PathSensitivity; -import org.gradle.api.tasks.TaskAction; -import org.gradle.maven.MavenModule; -import org.gradle.maven.MavenPomArtifact; -import org.xml.sax.SAXException; - -import javax.inject.Inject; -import javax.xml.parsers.ParserConfigurationException; -import java.io.IOException; -import java.io.OutputStream; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.util.Map; -import java.util.Set; -import java.util.TreeMap; - -@CacheableTask -public abstract class WriteMicronautVersionInfoTask extends DefaultTask { - - public static final String MICRONAUT_VERSIONS_PROPERTIES_FILE_NAME = "micronaut-versions.properties"; - - @Input - public abstract Property getVersion(); - - @Input - public ListProperty dependencies; - - @OutputDirectory - public abstract DirectoryProperty getOutputDirectory(); - - @Inject - public WriteMicronautVersionInfoTask(Project project) { - dependencies = project.getObjects().listProperty(String.class); - } - - @TaskAction - public void writeVersionInfo() throws IOException { - Map props = new TreeMap<>(); - for (String dependency : dependencies.get()) { - String[] groups = dependency.split(":", 3); - getLogger().lifecycle("Scanning {}:{}:{}", groups[0], groups[1], groups[2]); - Map bomProperties = bomProperties(groups[0], groups[1], groups[2]); - for (Map.Entry entry : bomProperties.entrySet()) { - if (entry.getKey().startsWith("micronaut.")) { - getLogger().lifecycle("Skipping {} from {}", entry.getKey(), dependency); - } else { - if (props.containsKey(entry.getKey())) { - getLogger().warn("Property {} from {} already exists ({}). Replacing with {}", entry.getKey(), dependency, props.get(entry.getKey()), entry.getValue()); - } - props.put(entry.getKey(), entry.getValue()); - } - } - } - try (OutputStream out = Files.newOutputStream(getOutputDirectory().file(MICRONAUT_VERSIONS_PROPERTIES_FILE_NAME).get().getAsFile().toPath())) { - for (Map.Entry entry : props.entrySet()) { - String line = entry.getKey() + "=" + entry.getValue() + "\n"; - out.write(line.getBytes(StandardCharsets.ISO_8859_1)); - } - } - } - - private Map bomProperties(String groupId, String artifactId, String version) { - ArtifactResolutionResult result = getProject().getDependencies().createArtifactResolutionQuery() - .forModule(groupId, artifactId, version) - .withArtifacts(MavenModule.class, MavenPomArtifact.class) - .execute(); - Map props = new TreeMap<>(); - for (ComponentArtifactsResult component : result.getResolvedComponents()) { - component.getArtifacts(MavenPomArtifact.class).forEach(artifact -> { - if (artifact instanceof ResolvedArtifactResult) { - ResolvedArtifactResult resolved = (ResolvedArtifactResult) artifact; - GPathResult pom = null; - try { - pom = new XmlSlurper().parse(resolved.getFile()); - } catch (IOException | SAXException | ParserConfigurationException e) { - // ignore - } - ((GPathResult) pom.getProperty("properties")).children().forEach(child -> { - NodeChild node = (NodeChild) child; - props.put(node.name(), node.text()); - }); - } - }); - } - return props; - } - - public ListProperty getDependencies() { - return dependencies; - } -} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 08e317f8af7..15b487d6d65 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -41,9 +41,7 @@ logback = "1.4.5" logbook-netty = "2.14.0" log4j = "2.19.0" micronaut-aws = "3.9.2" -micronaut-grpc = "3.3.1" micronaut-groovy = "4.0.0-SNAPSHOT" -micronaut-picocli = "4.3.0" micronaut-sql = "4.7.2" micronaut-test = "4.0.0-SNAPSHOT" micronaut-serde = "2.0.0-SNAPSHOT" @@ -80,8 +78,6 @@ micronaut-docs = "2.0.0" [libraries] # Libraries prefixed with bom- are BOM files -internal-boms-micronaut-grpc = { module = "io.micronaut.grpc:micronaut-grpc-bom", version.ref = "micronaut-grpc" } -internal-boms-micronaut-picocli = { module = "io.micronaut.picocli:micronaut-picocli-bom", version.ref = "micronaut-picocli" } test-boms-micronaut-aws = { module = "io.micronaut.aws:micronaut-aws-bom", version.ref = "micronaut-aws" } test-boms-micronaut-sql = { module = "io.micronaut.sql:micronaut-sql-bom", version.ref = "micronaut-sql" } diff --git a/parent/build.gradle b/parent/build.gradle deleted file mode 100644 index 08bb9702a3a..00000000000 --- a/parent/build.gradle +++ /dev/null @@ -1,319 +0,0 @@ -import io.micronaut.build.internal.WriteMicronautVersionInfoTask - -apply plugin:'base' - -configurations { - bomVersions -} - -repositories { - mavenCentral() -} - -// Include any of our boms that hava required properties -dependencies { - bomVersions(libs.internal.boms.micronaut.grpc) - bomVersions(libs.internal.boms.micronaut.picocli) -} - -def micronautVersionInfo = tasks.register("micronautVersionInfo", WriteMicronautVersionInfoTask) { - version = projectVersion - dependencies.set(configurations.bomVersions.getAllDependencies().collect { "$it.group:$it.name:$it.version" }) - outputDirectory = layout.buildDirectory.dir("version-info") -} - -group projectGroupId -version projectVersion - -ext.startPomInfo = { - delegate.parent { - groupId projectGroupId - artifactId "micronaut-bom" - delegate.version projectVersion - } -} -ext.extraPomInfo = { - delegate.properties { - 'jdk.version'('1.8') - 'release.version'('8') - 'maven.compiler.source'('${jdk.version}') - 'maven.compiler.target'('${jdk.version}') - 'maven.compiler.parameters'('true') - 'project.build.sourceEncoding'('UTF-8') - 'project.reporting.outputEncoding'('UTF-8') - 'exec.executable'('java') - - "micronaut.version"(projectVersion) - 'micronaut-maven-plugin.version'(micronautMavenPluginVersion) - - 'azure-functions-maven-plugin.version'('1.5.0') - 'exec-maven-plugin.version'('1.6.0') - 'function-maven-plugin.version'('0.9.8') - 'jib-maven-plugin.version'('3.1.4') - 'maven-compiler-plugin.version'('3.10.1') // Override actual Maven compiler version (3.1) because some bugs cause annotation processors doesn't work well - 'maven-deploy-plugin.version'('3.0.0-M2') - 'maven-failsafe-plugin.version'('2.22.2') // Override actual Maven surefire and failsafe version (2.12) to get native support for executing tests on the JUnit Platform (JUnit 5) - 'maven-install-plugin.version'('3.0.0-M1') - 'maven-jar-plugin.version'('3.2.2') - 'maven-resources-plugin.version'('3.2.0') - 'maven-shade-plugin.version'('3.2.4') - 'maven-surefire-plugin.version'('2.22.2') // Override actual Maven surefire and failsafe version (2.12) to get native support for executing tests on the JUnit Platform (JUnit 5) - 'protoc-jar-maven-plugin.version'('3.11.4') - // inject the properties from the required boms - micronautVersionInfo.flatMap { it.outputDirectory.file(WriteMicronautVersionInfoTask.MICRONAUT_VERSIONS_PROPERTIES_FILE_NAME) } - .get() - .asFile - .text - .readLines()*.split('=', 2).each { - "${it[0]}"(it[1]) - } - } - delegate.profiles { - delegate.profile { - id('jdk-9-or-later') - delegate.activation { - jdk('[9,)') - } - delegate.properties { - 'maven.compiler.release'('${release.version}') - } - } - delegate.profile { - id('graalvm') - delegate.activation { - delegate.file { - exists('${env.JAVA_HOME}/bin/native-image') - } - } - delegate.dependencies { - delegate.dependency { - groupId "org.graalvm.sdk" - artifactId "graal-sdk" - delegate.version '${graal.version}' - scope "provided" - } - delegate.dependency { - groupId "org.graalvm.nativeimage" - artifactId "svm" - scope "provided" - } - } - delegate.build { - delegate.plugins { - delegate.plugin { - groupId "org.apache.maven.plugins" - artifactId "maven-compiler-plugin" - delegate.version '${maven-compiler-plugin.version}' - delegate.configuration { - annotationProcessorPaths("combine.children": "append") { - delegate.path { - groupId "io.micronaut" - artifactId "micronaut-graal" - delegate.version '${micronaut.version}' - } - } - } - } - } - } - } - } - delegate.build { - delegate.pluginManagement { - delegate.plugins { - delegate.plugin { - groupId "io.micronaut.build" - artifactId "micronaut-maven-plugin" - delegate.version '${micronaut-maven-plugin.version}' - extensions true - } - delegate.plugin { - groupId "com.github.os72" - artifactId "protoc-jar-maven-plugin" - delegate.version '${protoc-jar-maven-plugin.version}' - delegate.executions { - execution { - phase('generate-sources') - goals { - goal("run") - } - delegate.configuration { - addProtoSources('all') - includeMavenTypes('direct') - inputDirectories { - include('src/main/proto') - } - outputTargets { - outputTarget { - type("java") - } - outputTarget { - type("grpc-java") - pluginArtifact('io.grpc:protoc-gen-grpc-java:${grpc.version}') - } - } - } - } - } - } - delegate.plugin { - groupId "org.codehaus.mojo" - artifactId "exec-maven-plugin" - delegate.version '${exec-maven-plugin.version}' - delegate.configuration { - arguments { - argument("-classpath") - classpath() - argument("-XX:TieredStopAtLevel=1") - argument("-Dcom.sun.management.jmxremote") - argument('${exec.mainClass}') - } - } - } - delegate.plugin { - groupId "com.google.cloud.functions" - artifactId "function-maven-plugin" - delegate.version '${function-maven-plugin.version}' - } - delegate.plugin { - groupId "com.microsoft.azure" - artifactId "azure-functions-maven-plugin" - delegate.version '${azure-functions-maven-plugin.version}' - } - - delegate.plugin { - groupId "org.apache.maven.plugins" - artifactId "maven-deploy-plugin" - delegate.version '${maven-deploy-plugin.version}' - } - delegate.plugin { - groupId "org.apache.maven.plugins" - artifactId "maven-install-plugin" - delegate.version '${maven-install-plugin.version}' - } - delegate.plugin { - groupId "org.apache.maven.plugins" - artifactId "maven-jar-plugin" - delegate.version '${maven-jar-plugin.version}' - } - delegate.plugin { - groupId "org.apache.maven.plugins" - artifactId "maven-resources-plugin" - delegate.version '${maven-resources-plugin.version}' - } - delegate.plugin { - groupId "org.apache.maven.plugins" - artifactId "maven-compiler-plugin" - delegate.version '${maven-compiler-plugin.version}' - delegate.configuration { - annotationProcessorPaths { - delegate.path { - groupId "io.micronaut" - artifactId "micronaut-inject-java" - delegate.version '${micronaut.version}' - } - delegate.path { - groupId "io.micronaut" - artifactId "micronaut-validation" - delegate.version '${micronaut.version}' - } - } - } - } - delegate.plugin { - groupId 'org.apache.maven.plugins' - artifactId 'maven-surefire-plugin' - delegate.version '${maven-surefire-plugin.version}' - } - delegate.plugin { - groupId 'org.apache.maven.plugins' - artifactId 'maven-failsafe-plugin' - delegate.version '${maven-failsafe-plugin.version}' - delegate.executions { - execution { - goals { - goal 'integration-test' - goal 'verify' - } - } - } - } - delegate.plugin { - groupId "org.apache.maven.plugins" - artifactId "maven-shade-plugin" - delegate.version '${maven-shade-plugin.version}' - delegate.executions { - execution { - id 'default-shade' - delegate.configuration { - createDependencyReducedPom(false) - transformers { - transformer(implementation:'org.apache.maven.plugins.shade.resource.ManifestResourceTransformer') { - mainClass('${exec.mainClass}') - } - transformer(implementation:'org.apache.maven.plugins.shade.resource.ServicesResourceTransformer') - } - filters{ - filter { - artifact '*.*' - excludes { - exclude "META-INF/*.SF" - exclude "META-INF/*.DSA" - exclude "META-INF/*.RSA" - } - } - } - } - } - } - } - delegate.plugin { - groupId "org.graalvm.buildtools" - artifactId "native-maven-plugin" - delegate.version '${maven.native.plugin.version}' - delegate.extensions true - delegate.configuration('combine.self':'override') { - imageName '${project.artifactId}' - mainClass '${exec.mainClass}' - buildArgs { - buildArg '--no-fallback' - } - environment { - USE_NATIVE_IMAGE_JAVA_PLATFORM_MODULE_SYSTEM(false) - } - } - } - delegate.plugin { - groupId 'com.google.cloud.tools' - artifactId 'jib-maven-plugin' - delegate.version '${jib-maven-plugin.version}' - delegate.dependencies { - dependency { - groupId "io.micronaut.build" - artifactId "micronaut-maven-plugin" - delegate.version '${micronaut-maven-plugin.version}' - } - } - delegate.configuration { - to { - image '${project.artifactId}' - } - pluginExtensions { - pluginExtension { - implementation 'io.micronaut.build.jib.JibMicronautExtension' - } - } - } - } - } - } - } -} - -apply plugin: "io.micronaut.build.internal.publishing" - -afterEvaluate { - tasks.named("generatePomFileForMavenPublication", GenerateMavenPom).configure { - dependsOn micronautVersionInfo - } -} diff --git a/settings.gradle b/settings.gradle index 23136d4da32..455107624d9 100644 --- a/settings.gradle +++ b/settings.gradle @@ -21,7 +21,6 @@ rootProject.name = 'micronaut' include "aop" include "core-bom" -include "parent" include "buffer-netty" include "core" include "core-reactive" From a5f5179050f24301428f3fc56f187856daf90355 Mon Sep 17 00:00:00 2001 From: Tim Yates Date: Wed, 23 Nov 2022 10:43:57 +0000 Subject: [PATCH 258/743] Add logback as a managed dependency (#8388) --- .../io.micronaut.build.internal.convention-base.gradle | 2 +- .../io.micronaut.build.internal.convention-geb-base.gradle | 2 +- context/build.gradle | 2 +- gradle/libs.versions.toml | 7 +++---- http-client/build.gradle | 2 +- http-server-netty/build.gradle | 2 +- management/build.gradle | 2 +- runtime/build.gradle | 2 +- test-suite-geb/build.gradle | 2 +- test-suite/build.gradle | 4 ++-- 10 files changed, 13 insertions(+), 14 deletions(-) diff --git a/buildSrc/src/main/groovy/io.micronaut.build.internal.convention-base.gradle b/buildSrc/src/main/groovy/io.micronaut.build.internal.convention-base.gradle index 4d4ec8530d3..2d559948450 100644 --- a/buildSrc/src/main/groovy/io.micronaut.build.internal.convention-base.gradle +++ b/buildSrc/src/main/groovy/io.micronaut.build.internal.convention-base.gradle @@ -76,7 +76,7 @@ dependencies { transitive = false } - api libs.managed.slf4j + api libs.managed.slf4j.api compileOnly libs.caffeine compileOnly libs.bundles.asm diff --git a/buildSrc/src/main/groovy/io.micronaut.build.internal.convention-geb-base.gradle b/buildSrc/src/main/groovy/io.micronaut.build.internal.convention-geb-base.gradle index f38f03d93ff..33266f10f8c 100644 --- a/buildSrc/src/main/groovy/io.micronaut.build.internal.convention-geb-base.gradle +++ b/buildSrc/src/main/groovy/io.micronaut.build.internal.convention-geb-base.gradle @@ -85,7 +85,7 @@ dependencies { transitive = false } - api libs.managed.slf4j + api libs.managed.slf4j.api compileOnly libs.caffeine compileOnly libs.bundles.asm diff --git a/context/build.gradle b/context/build.gradle index 9c0a3ec09ac..c527529d173 100644 --- a/context/build.gradle +++ b/context/build.gradle @@ -12,7 +12,7 @@ dependencies { compileOnly project(':core-reactive') compileOnly project(':core-processor') compileOnly libs.log4j - compileOnly libs.logback + compileOnly libs.managed.logback.classic testCompileOnly project(":inject-groovy") testAnnotationProcessor project(":inject-java") diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 15b487d6d65..b47ec18cf2c 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -37,7 +37,7 @@ junit5 = "5.9.1" kotlin = "1.7.20" kotlin-coroutines = "1.6.4" ktor = "1.6.8" -logback = "1.4.5" +managed-logback = "1.4.5" logbook-netty = "2.14.0" log4j = "2.19.0" micronaut-aws = "3.9.2" @@ -123,8 +123,9 @@ managed-reactive-streams = { module = "org.reactivestreams:reactive-streams", ve managed-reactor = { module = "io.projectreactor:reactor-core", version.ref = "managed-reactor" } -managed-slf4j = { module = "org.slf4j:slf4j-api", version.ref = "managed-slf4j" } +managed-slf4j-api = { module = "org.slf4j:slf4j-api", version.ref = "managed-slf4j" } managed-slf4j-simple = { module = "org.slf4j:slf4j-simple", version.ref = "managed-slf4j" } +managed-logback-classic = { module = "ch.qos.logback:logback-classic", version.ref = "managed-logback" } managed-snakeyaml = { module = "org.yaml:snakeyaml", version.ref = "managed-snakeyaml" } @@ -204,8 +205,6 @@ kotlinx-coroutines-reactor = { module = "org.jetbrains.kotlinx:kotlinx-coroutine log4j = { module = "org.apache.logging.log4j:log4j-core", version.ref = "log4j" } -logback = { module = "ch.qos.logback:logback-classic", version.ref = "logback" } - logbook-netty = { module = "org.zalando:logbook-netty", version.ref = "logbook-netty" } micronaut-docs = { module = "io.micronaut.docs:micronaut-docs-asciidoc-config-props", version.ref = "micronaut-docs" } diff --git a/http-client/build.gradle b/http-client/build.gradle index fef1e30db67..b174dfb9e7f 100644 --- a/http-client/build.gradle +++ b/http-client/build.gradle @@ -30,7 +30,7 @@ dependencies { testImplementation project(":jackson-databind") testImplementation project(":http-server-netty") testImplementation libs.wiremock - testImplementation libs.logback + testImplementation libs.managed.logback.classic if (JavaVersion.current().isCompatibleWith(JavaVersion.VERSION_15)) { testImplementation libs.bcpkix diff --git a/http-server-netty/build.gradle b/http-server-netty/build.gradle index 1db2cb6489f..6c0957edae9 100644 --- a/http-server-netty/build.gradle +++ b/http-server-netty/build.gradle @@ -70,7 +70,7 @@ dependencies { classifier = Os.isArch("aarch64") ? "osx-aarch_64" : "osx-x86_64" } } - testImplementation libs.logback + testImplementation libs.managed.logback.classic // Adding these for now since micronaut-test isnt resolving correctly ... probably need to upgrade gradle there too testImplementation libs.junit.jupiter.api diff --git a/management/build.gradle b/management/build.gradle index 54f9e861d9d..6b7ba33bf80 100644 --- a/management/build.gradle +++ b/management/build.gradle @@ -30,7 +30,7 @@ dependencies { testRuntimeOnly libs.h2 testRuntimeOnly libs.mysql.driver - compileOnly libs.logback + compileOnly libs.managed.logback.classic compileOnly libs.log4j } diff --git a/runtime/build.gradle b/runtime/build.gradle index d356280d799..41d927146f0 100644 --- a/runtime/build.gradle +++ b/runtime/build.gradle @@ -24,7 +24,7 @@ dependencies { compileOnly libs.caffeine compileOnly libs.kotlinx.coroutines.core compileOnly libs.kotlinx.coroutines.reactive - testImplementation libs.logback + testImplementation libs.managed.logback.classic testImplementation libs.managed.snakeyaml testAnnotationProcessor project(":inject-java") testImplementation libs.jsr107 diff --git a/test-suite-geb/build.gradle b/test-suite-geb/build.gradle index 165fc302ade..0da06cb518d 100644 --- a/test-suite-geb/build.gradle +++ b/test-suite-geb/build.gradle @@ -17,6 +17,6 @@ dependencies { testImplementation project(':http') testImplementation project(':http-server-netty') - testRuntimeOnly libs.logback + testRuntimeOnly libs.managed.logback.classic testImplementation project(":jackson-databind") } diff --git a/test-suite/build.gradle b/test-suite/build.gradle index 129bef859bd..26bb8a66aba 100644 --- a/test-suite/build.gradle +++ b/test-suite/build.gradle @@ -67,7 +67,7 @@ dependencies { testRuntimeOnly(platform(libs.test.boms.micronaut.aws)) testRuntimeOnly libs.h2 testRuntimeOnly libs.junit.vintage - testRuntimeOnly libs.logback + testRuntimeOnly libs.managed.logback.classic testRuntimeOnly libs.aws.java.sdk.lambda // needed for HTTP/2 tests @@ -80,7 +80,7 @@ dependencies { } } testImplementation libs.logbook.netty - testImplementation libs.logback + testImplementation libs.managed.logback.classic if (JavaVersion.current().isCompatibleWith(JavaVersion.VERSION_15)) { testImplementation libs.bcpkix From e45b5ea72350fe54c88f6ded3abb461be5263ff3 Mon Sep 17 00:00:00 2001 From: Jonas Konrad Date: Wed, 23 Nov 2022 12:10:52 +0100 Subject: [PATCH 259/743] Forward early websocket client errors to client future, not the websocket bean (#8300) When the websocket is closed, but the session has not yet been initialized (and OnOpen not been called), change handleCloseReason to instead emit an error on the publisher that is returned by the `WebSocketClient.connect` method. This means the publisher won't get stuck, and the OnClose method won't be called without a session being available. Additionally, I refactored `AbstractNettyWebSocketHandler` and the subclasses a bit. The bodyArgument/pongArgument code, and the old callOpenMethod, were only used for the server handler. Hopefully fixes #7921 --- .../NettyWebSocketClientHandler.java | 81 +++------- .../websocket/ClientWebsocketSpec.groovy | 77 ++++++++++ .../AbstractNettyWebSocketHandler.java | 142 +++++------------- .../NettyServerWebSocketHandler.java | 86 ++++++++++- 4 files changed, 215 insertions(+), 171 deletions(-) create mode 100644 http-client/src/test/groovy/io/micronaut/http/client/websocket/ClientWebsocketSpec.groovy diff --git a/http-client/src/main/java/io/micronaut/http/client/netty/websocket/NettyWebSocketClientHandler.java b/http-client/src/main/java/io/micronaut/http/client/netty/websocket/NettyWebSocketClientHandler.java index 5e825b38400..9aa4b5772ad 100644 --- a/http-client/src/main/java/io/micronaut/http/client/netty/websocket/NettyWebSocketClientHandler.java +++ b/http-client/src/main/java/io/micronaut/http/client/netty/websocket/NettyWebSocketClientHandler.java @@ -16,34 +16,28 @@ package io.micronaut.http.client.netty.websocket; import io.micronaut.core.annotation.Internal; -import io.micronaut.core.async.publisher.Publishers; import io.micronaut.core.bind.BoundExecutable; import io.micronaut.core.bind.DefaultExecutableBinder; import io.micronaut.core.bind.ExecutableBinder; -import io.micronaut.core.convert.ConversionService; import io.micronaut.core.convert.value.ConvertibleValues; import io.micronaut.core.type.Argument; import io.micronaut.http.MutableHttpRequest; -import io.micronaut.http.bind.DefaultRequestBinderRegistry; import io.micronaut.http.bind.RequestBinderRegistry; import io.micronaut.http.codec.MediaTypeCodecRegistry; import io.micronaut.http.netty.websocket.AbstractNettyWebSocketHandler; import io.micronaut.http.netty.websocket.NettyWebSocketSession; import io.micronaut.http.uri.UriMatchInfo; import io.micronaut.http.uri.UriMatchTemplate; -import io.micronaut.inject.MethodExecutionHandle; import io.micronaut.websocket.CloseReason; import io.micronaut.websocket.WebSocketPongMessage; import io.micronaut.websocket.annotation.ClientWebSocket; import io.micronaut.websocket.bind.WebSocketState; -import io.micronaut.websocket.bind.WebSocketStateBinderRegistry; import io.micronaut.websocket.context.WebSocketBean; import io.micronaut.websocket.exceptions.WebSocketClientException; import io.micronaut.websocket.exceptions.WebSocketSessionException; import io.micronaut.websocket.interceptor.WebSocketSessionAware; import io.netty.channel.Channel; import io.netty.channel.ChannelHandlerContext; -import io.netty.channel.ChannelPromise; import io.netty.handler.codec.http.FullHttpResponse; import io.netty.handler.codec.http.HttpHeaderNames; import io.netty.handler.codec.http.websocketx.CloseWebSocketFrame; @@ -52,14 +46,12 @@ import io.netty.handler.ssl.SslHandler; import io.netty.handler.timeout.IdleState; import io.netty.handler.timeout.IdleStateEvent; -import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.core.publisher.Sinks; import java.util.Collections; import java.util.List; -import java.util.Optional; /** * Handler for WebSocket clients. @@ -78,9 +70,7 @@ public class NettyWebSocketClientHandler extends AbstractNettyWebSocketHandle private final Sinks.One completion = Sinks.one(); private final UriMatchInfo matchInfo; private final MediaTypeCodecRegistry codecRegistry; - private ChannelPromise handshakeFuture; private NettyWebSocketSession clientSession; - private final WebSocketStateBinderRegistry webSocketStateBinderRegistry; private FullHttpResponse handshakeResponse; private Argument clientBodyArgument; private Argument clientPongArgument; @@ -103,12 +93,9 @@ public NettyWebSocketClientHandler( this.codecRegistry = mediaTypeCodecRegistry; this.handshaker = handshaker; this.genericWebSocketBean = webSocketBean; - this.webSocketStateBinderRegistry = new WebSocketStateBinderRegistry(requestBinderRegistry != null ? requestBinderRegistry : new DefaultRequestBinderRegistry(ConversionService.SHARED)); String clientPath = webSocketBean.getBeanDefinition().stringValue(ClientWebSocket.class).orElse(""); UriMatchTemplate matchTemplate = UriMatchTemplate.of(clientPath); this.matchInfo = matchTemplate.match(request.getPath()).orElse(null); - - callOpenMethod(null); } @Override @@ -139,14 +126,16 @@ public NettyWebSocketSession getSession() { return clientSession; } - @Override - public void handlerAdded(final ChannelHandlerContext ctx) { - handshakeFuture = ctx.newPromise(); - } - @Override public void channelActive(final ChannelHandlerContext ctx) { - handshaker.handshake(ctx.channel()); + handshaker.handshake(ctx.channel()).addListener(future -> { + if (future.isSuccess()) { + ctx.channel().config().setAutoRead(true); + ctx.read(); + } else { + completion.tryEmitError(future.cause()); + } + }); } @Override @@ -168,7 +157,6 @@ protected void channelRead0(ChannelHandlerContext ctx, Object msg) { } return; } - handshakeFuture.setSuccess(); this.clientSession = createWebSocketSession(ctx); @@ -178,7 +166,6 @@ protected void channelRead0(ChannelHandlerContext ctx, Object msg) { ((WebSocketSessionAware) targetBean).setWebSocketSession(clientSession); } - ExecutableBinder binder = new DefaultExecutableBinder<>(); BoundExecutable bound = binder.tryBind(messageHandler.getExecutableMethod(), webSocketBinder, new WebSocketState(clientSession, originatingRequest)); List> unboundArguments = bound.getUnboundArguments(); @@ -218,37 +205,11 @@ protected void channelRead0(ChannelHandlerContext ctx, Object msg) { } } - Optional> opt = webSocketBean.openMethod(); - if (opt.isPresent()) { - MethodExecutionHandle openMethod = opt.get(); - - WebSocketState webSocketState = new WebSocketState(clientSession, originatingRequest); - try { - BoundExecutable openMethodBound = binder.bind(openMethod.getExecutableMethod(), webSocketStateBinderRegistry, webSocketState); - Object target = openMethod.getTarget(); - Object result = openMethodBound.invoke(target); - - if (Publishers.isConvertibleToPublisher(result)) { - Publisher reactiveSequence = Publishers.convertPublisher(result, Publisher.class); - Flux.from(reactiveSequence).subscribe( - o -> { }, - error -> completion.tryEmitError(new WebSocketSessionException("Error opening WebSocket client session: " + error.getMessage(), error)), - () -> { - completion.tryEmitValue(targetBean); - } - ); - } else { - completion.tryEmitValue(targetBean); - } - } catch (Throwable e) { - completion.tryEmitError(new WebSocketClientException("Error opening WebSocket client session: " + e.getMessage(), e)); - if (getSession().isOpen()) { - getSession().close(CloseReason.INTERNAL_ERROR); - } - } - } else { - completion.tryEmitValue(targetBean); - } + Flux.from(callOpenMethod(ctx)).subscribe( + o -> { }, + error -> completion.tryEmitError(new WebSocketSessionException("Error opening WebSocket client session: " + error.getMessage(), error)), + () -> completion.tryEmitValue(targetBean) + ); return; } @@ -257,8 +218,6 @@ protected void channelRead0(ChannelHandlerContext ctx, Object msg) { } else { ctx.fireChannelRead(msg); } - - } @Override @@ -286,14 +245,20 @@ public ConvertibleValues getUriVariables() { @Override public void exceptionCaught(final ChannelHandlerContext ctx, final Throwable cause) { - if (!handshakeFuture.isDone()) { - handshakeFuture.setFailure(cause); - } - + completion.tryEmitError(cause); super.exceptionCaught(ctx, cause); } public final Mono getHandshakeCompletedMono() { return completion.asMono(); } + + @Override + protected void handleCloseReason(ChannelHandlerContext ctx, CloseReason cr, boolean writeCloseReason) { + if (!handshaker.isHandshakeComplete()) { + completion.tryEmitError(new WebSocketClientException("Error opening WebSocket client session: " + cr.getReason())); + return; + } + super.handleCloseReason(ctx, cr, writeCloseReason); + } } diff --git a/http-client/src/test/groovy/io/micronaut/http/client/websocket/ClientWebsocketSpec.groovy b/http-client/src/test/groovy/io/micronaut/http/client/websocket/ClientWebsocketSpec.groovy new file mode 100644 index 00000000000..71957bfc1cf --- /dev/null +++ b/http-client/src/test/groovy/io/micronaut/http/client/websocket/ClientWebsocketSpec.groovy @@ -0,0 +1,77 @@ +package io.micronaut.http.client.websocket + +import io.micronaut.context.ApplicationContext +import io.micronaut.context.annotation.Requires +import io.micronaut.websocket.WebSocketClient +import io.micronaut.websocket.annotation.ClientWebSocket +import io.micronaut.websocket.annotation.OnClose +import io.micronaut.websocket.annotation.OnMessage +import io.micronaut.websocket.annotation.OnOpen +import io.micronaut.websocket.exceptions.WebSocketClientException +import jakarta.inject.Inject +import jakarta.inject.Singleton +import reactor.core.publisher.Mono +import spock.lang.Specification + +import java.util.concurrent.ExecutionException + +class ClientWebsocketSpec extends Specification { + void 'websocket bean should not open if there is a connection error'() { + given: + def ctx = ApplicationContext.run(['spec.name': 'ClientWebsocketSpec']) + def client = ctx.getBean(WebSocketClient) + def registry = ctx.getBean(ClientBeanRegistry) + def mono = Mono.from(client.connect(ClientBean.class, 'http://does-not-exist')) + + when: + mono.toFuture().get() + then: + def e = thrown ExecutionException + e.cause instanceof WebSocketClientException + + registry.clientBeans.size() == 1 + !registry.clientBeans[0].opened + !registry.clientBeans[0].autoClosed + !registry.clientBeans[0].onClosed + + cleanup: + client.close() + } + + @Singleton + @Requires(property = 'spec.name', value = 'ClientWebsocketSpec') + static class ClientBeanRegistry { + List clientBeans = new ArrayList<>() + } + + @ClientWebSocket + static class ClientBean implements AutoCloseable { + boolean opened = false + boolean onClosed = false + boolean autoClosed = false + + @Inject + ClientBean(ClientBeanRegistry registry) { + registry.clientBeans.add(this) + } + + @OnOpen + void open() { + opened = true + } + + @OnMessage + void onMessage(String text) { + } + + @OnClose + void onClose() { + onClosed = true + } + + @Override + void close() throws Exception { + autoClosed = true + } + } +} diff --git a/http-netty/src/main/java/io/micronaut/http/netty/websocket/AbstractNettyWebSocketHandler.java b/http-netty/src/main/java/io/micronaut/http/netty/websocket/AbstractNettyWebSocketHandler.java index 405515fba9c..c3b6b0e6a9a 100644 --- a/http-netty/src/main/java/io/micronaut/http/netty/websocket/AbstractNettyWebSocketHandler.java +++ b/http-netty/src/main/java/io/micronaut/http/netty/websocket/AbstractNettyWebSocketHandler.java @@ -35,6 +35,7 @@ import io.micronaut.inject.MethodExecutionHandle; import io.micronaut.websocket.CloseReason; import io.micronaut.websocket.WebSocketPongMessage; +import io.micronaut.websocket.WebSocketSession; import io.micronaut.websocket.bind.WebSocketState; import io.micronaut.websocket.bind.WebSocketStateBinderRegistry; import io.micronaut.websocket.context.WebSocketBean; @@ -55,6 +56,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; import reactor.core.scheduler.Schedulers; import java.io.IOException; @@ -89,13 +91,10 @@ public abstract class AbstractNettyWebSocketHandler extends SimpleChannelInbound protected final HttpRequest originatingRequest; protected final MethodExecutionHandle messageHandler; protected final MethodExecutionHandle pongHandler; - protected final NettyWebSocketSession session; protected final MediaTypeCodecRegistry mediaTypeCodecRegistry; protected final WebSocketVersion webSocketVersion; protected final String subProtocol; protected final WebSocketSessionRepository webSocketSessionRepository; - private final Argument bodyArgument; - private final Argument pongArgument; private final AtomicBoolean closed = new AtomicBoolean(false); private AtomicReference frameBuffer = new AtomicReference<>(); @@ -132,138 +131,68 @@ protected AbstractNettyWebSocketHandler( this.pongHandler = webSocketBean.pongMethod().orElse(null); this.mediaTypeCodecRegistry = mediaTypeCodecRegistry; this.webSocketVersion = version; - this.session = createWebSocketSession(ctx); - - if (session != null) { - - ExecutableBinder binder = new DefaultExecutableBinder<>(); - - if (messageHandler != null) { - BoundExecutable bound = binder.tryBind(messageHandler.getExecutableMethod(), webSocketBinder, new WebSocketState(session, originatingRequest)); - List> unboundArguments = bound.getUnboundArguments(); - - if (unboundArguments.size() == 1) { - this.bodyArgument = unboundArguments.iterator().next(); - } else { - this.bodyArgument = null; - if (LOG.isErrorEnabled()) { - LOG.error("WebSocket @OnMessage method " + webSocketBean.getTarget() + "." + messageHandler.getExecutableMethod() + " should define exactly 1 message parameter, but found 2 possible candidates: " + unboundArguments); - } - - if (session.isOpen()) { - session.close(CloseReason.INTERNAL_ERROR); - } - } - } else { - this.bodyArgument = null; - } - - if (pongHandler != null) { - BoundExecutable bound = binder.tryBind(pongHandler.getExecutableMethod(), webSocketBinder, new WebSocketState(session, originatingRequest)); - List> unboundArguments = bound.getUnboundArguments(); - if (unboundArguments.size() == 1 && unboundArguments.get(0).isAssignableFrom(WebSocketPongMessage.class)) { - this.pongArgument = unboundArguments.get(0); - } else { - this.pongArgument = null; - if (LOG.isErrorEnabled()) { - LOG.error("WebSocket @OnMessage pong handler method " + webSocketBean.getTarget() + "." + pongHandler.getExecutableMethod() + " should define exactly 1 message parameter assignable from a WebSocketPongMessage, but found: " + unboundArguments); - } - - if (session.isOpen()) { - session.close(CloseReason.INTERNAL_ERROR); - } - } - } else { - this.pongArgument = null; - } - } else { - this.bodyArgument = null; - this.pongArgument = null; - } } /** * Calls the open method of the websocket bean. * - * @param ctx THe handler context + * @param ctx The handler context + * @return Publisher for any errors, or the result of the open method */ - protected void callOpenMethod(ChannelHandlerContext ctx) { - if (session == null) { - return; - } + protected Publisher callOpenMethod(ChannelHandlerContext ctx) { + WebSocketSession session = getSession(); Optional> executionHandle = webSocketBean.openMethod(); if (executionHandle.isPresent()) { MethodExecutionHandle openMethod = executionHandle.get(); - BoundExecutable boundExecutable = null; + + BoundExecutable boundExecutable; try { boundExecutable = bindMethod(originatingRequest, webSocketBinder, openMethod, Collections.emptyList()); } catch (Throwable e) { - if (LOG.isErrorEnabled()) { - LOG.error("Error Binding method @OnOpen for WebSocket [" + webSocketBean + "]: " + e.getMessage(), e); - } - if (session.isOpen()) { session.close(CloseReason.INTERNAL_ERROR); } + return Mono.error(e); } - if (boundExecutable != null) { - try { - BoundExecutable finalBoundExecutable = boundExecutable; - Object result = invokeExecutable(finalBoundExecutable, openMethod); - if (Publishers.isConvertibleToPublisher(result)) { - Flux flowable = Flux.from(instrumentPublisher(ctx, result)); - flowable.subscribe( - o -> { - }, - error -> { - if (LOG.isErrorEnabled()) { - LOG.error("Error Opening WebSocket [" + webSocketBean + "]: " + error.getMessage(), error); - } - if (session.isOpen()) { - session.close(CloseReason.INTERNAL_ERROR); - } - }, - () -> { - } - ); - } - } catch (Throwable e) { - forwardErrorToUser(ctx, t -> { - if (LOG.isErrorEnabled()) { - LOG.error("Error Opening WebSocket [" + webSocketBean + "]: " + t.getMessage(), t); + try { + Object result = invokeExecutable(boundExecutable, openMethod); + if (Publishers.isConvertibleToPublisher(result)) { + return Flux.from(instrumentPublisher(ctx, result)).doOnError(t -> { + if (session.isOpen()) { + session.close(CloseReason.INTERNAL_ERROR); } - }, e); - // since we failed to call onOpen, we should always close here - if (session.isOpen()) { - session.close(CloseReason.INTERNAL_ERROR); - } + }); + } else { + return Mono.empty(); + } + } catch (Throwable e) { + // since we failed to call onOpen, we should always close here + if (session.isOpen()) { + session.close(CloseReason.INTERNAL_ERROR); } + return Mono.error(e); } + } else { + return Mono.empty(); } } /** * @return The body argument for the message handler */ - public Argument getBodyArgument() { - return bodyArgument; - } + public abstract Argument getBodyArgument(); /** * @return The pong argument for the pong handler */ - public Argument getPongArgument() { - return pongArgument; - } + public abstract Argument getPongArgument(); /** * @return The session */ - public NettyWebSocketSession getSession() { - return session; - } + public abstract NettyWebSocketSession getSession(); @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { @@ -271,7 +200,7 @@ public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { forwardErrorToUser(ctx, e -> handleUnexpected(ctx, e), cause); } - private void forwardErrorToUser(ChannelHandlerContext ctx, Consumer fallback, Throwable cause) { + protected final void forwardErrorToUser(ChannelHandlerContext ctx, Consumer fallback, Throwable cause) { Optional> opt = webSocketBean.errorMethod(); if (opt.isPresent()) { @@ -443,10 +372,10 @@ protected void handleWebSocketFrame(ChannelHandlerContext ctx, WebSocketFrame ms o -> { }, error -> messageProcessingException(ctx, error), - () -> messageHandled(ctx, session, v) + () -> messageHandled(ctx, v) ); } else { - messageHandled(ctx, session, v); + messageHandled(ctx, v); } } catch (Throwable e) { messageProcessingException(ctx, e); @@ -528,10 +457,9 @@ private void messageProcessingException(ChannelHandlerContext ctx, Throwable e) * Method called once a message has been handled by the handler. * * @param ctx The channel handler context - * @param session The session * @param message The message that was handled */ - protected void messageHandled(ChannelHandlerContext ctx, NettyWebSocketSession session, Object message) { + protected void messageHandled(ChannelHandlerContext ctx, Object message) { // no-op } @@ -547,12 +475,12 @@ protected void writeCloseFrameAndTerminate(ChannelHandlerContext ctx, CloseReaso } /** - * Used to close thee session with a given reason. + * Used to close the session with a given reason. * @param ctx The context * @param cr The reason * @param writeCloseReason Whether to allow writing the close reason to the remote */ - private void handleCloseReason(ChannelHandlerContext ctx, CloseReason cr, boolean writeCloseReason) { + protected void handleCloseReason(ChannelHandlerContext ctx, CloseReason cr, boolean writeCloseReason) { cleanupBuffer(); if (closed.compareAndSet(false, true)) { if (LOG.isDebugEnabled()) { diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/websocket/NettyServerWebSocketHandler.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/websocket/NettyServerWebSocketHandler.java index 2f40262ab9f..817124f45f2 100644 --- a/http-server-netty/src/main/java/io/micronaut/http/server/netty/websocket/NettyServerWebSocketHandler.java +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/websocket/NettyServerWebSocketHandler.java @@ -20,7 +20,10 @@ import io.micronaut.core.annotation.Nullable; import io.micronaut.core.async.publisher.Publishers; import io.micronaut.core.bind.BoundExecutable; +import io.micronaut.core.bind.DefaultExecutableBinder; +import io.micronaut.core.bind.ExecutableBinder; import io.micronaut.core.convert.value.ConvertibleValues; +import io.micronaut.core.type.Argument; import io.micronaut.core.type.Executable; import io.micronaut.core.util.KotlinUtils; import io.micronaut.http.HttpAttributes; @@ -36,7 +39,9 @@ import io.micronaut.inject.MethodExecutionHandle; import io.micronaut.web.router.UriRouteMatch; import io.micronaut.websocket.CloseReason; +import io.micronaut.websocket.WebSocketPongMessage; import io.micronaut.websocket.WebSocketSession; +import io.micronaut.websocket.bind.WebSocketState; import io.micronaut.websocket.context.WebSocketBean; import io.micronaut.websocket.event.WebSocketMessageProcessedEvent; import io.micronaut.websocket.event.WebSocketSessionClosedEvent; @@ -56,6 +61,7 @@ import reactor.core.scheduler.Schedulers; import java.security.Principal; +import java.util.List; import java.util.Optional; import java.util.Set; import java.util.function.Function; @@ -77,10 +83,14 @@ public class NettyServerWebSocketHandler extends AbstractNettyWebSocketHandler { */ public static final String ID = "websocket-handler"; + private final NettyWebSocketSession serverSession; private final NettyEmbeddedServices nettyEmbeddedServices; @Nullable private final CoroutineHelper coroutineHelper; + private final Argument bodyArgument; + private final Argument pongArgument; + /** * Default constructor. * @@ -113,18 +123,67 @@ public class NettyServerWebSocketHandler extends AbstractNettyWebSocketHandler { handshaker.selectedSubprotocol(), webSocketSessionRepository); + this.serverSession = createWebSocketSession(ctx); + + ExecutableBinder binder = new DefaultExecutableBinder<>(); + + if (messageHandler != null) { + BoundExecutable bound = binder.tryBind(messageHandler.getExecutableMethod(), webSocketBinder, new WebSocketState(serverSession, originatingRequest)); + List> unboundArguments = bound.getUnboundArguments(); + + if (unboundArguments.size() == 1) { + this.bodyArgument = unboundArguments.iterator().next(); + } else { + this.bodyArgument = null; + if (LOG.isErrorEnabled()) { + LOG.error("WebSocket @OnMessage method " + webSocketBean.getTarget() + "." + messageHandler.getExecutableMethod() + " should define exactly 1 message parameter, but found 2 possible candidates: " + unboundArguments); + } + + if (serverSession.isOpen()) { + serverSession.close(CloseReason.INTERNAL_ERROR); + } + } + } else { + this.bodyArgument = null; + } + + if (pongHandler != null) { + BoundExecutable bound = binder.tryBind(pongHandler.getExecutableMethod(), webSocketBinder, new WebSocketState(serverSession, originatingRequest)); + List> unboundArguments = bound.getUnboundArguments(); + if (unboundArguments.size() == 1 && unboundArguments.get(0).isAssignableFrom(WebSocketPongMessage.class)) { + this.pongArgument = unboundArguments.get(0); + } else { + this.pongArgument = null; + if (LOG.isErrorEnabled()) { + LOG.error("WebSocket @OnMessage pong handler method " + webSocketBean.getTarget() + "." + pongHandler.getExecutableMethod() + " should define exactly 1 message parameter assignable from a WebSocketPongMessage, but found: " + unboundArguments); + } + + if (serverSession.isOpen()) { + serverSession.close(CloseReason.INTERNAL_ERROR); + } + } + } else { + this.pongArgument = null; + } + this.nettyEmbeddedServices = nettyEmbeddedServices; this.coroutineHelper = coroutineHelper; request.setAttribute(HttpAttributes.ROUTE_MATCH, routeMatch); request.setAttribute(HttpAttributes.ROUTE, routeMatch.getRoute()); - callOpenMethod(ctx); + Flux.from(callOpenMethod(ctx)).subscribe(v -> { }, t -> { + forwardErrorToUser(ctx, e -> { + if (LOG.isErrorEnabled()) { + LOG.error("Error Opening WebSocket [" + webSocketBean + "]: " + e.getMessage(), e); + } + }, t); + }); ApplicationEventPublisher eventPublisher = nettyEmbeddedServices.getEventPublisher(WebSocketSessionOpenEvent.class); try { - eventPublisher.publishEvent(new WebSocketSessionOpenEvent(session)); + eventPublisher.publishEvent(new WebSocketSessionOpenEvent(serverSession)); } catch (Exception e) { if (LOG.isErrorEnabled()) { LOG.error("Error publishing WebSocket opened event: " + e.getMessage(), e); @@ -132,6 +191,21 @@ public class NettyServerWebSocketHandler extends AbstractNettyWebSocketHandler { } } + @Override + public NettyWebSocketSession getSession() { + return serverSession; + } + + @Override + public Argument getBodyArgument() { + return bodyArgument; + } + + @Override + public Argument getPongArgument() { + return pongArgument; + } + @Override public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception { if (evt instanceof IdleStateEvent) { @@ -276,11 +350,11 @@ private Object invokeExecutable0(BoundExecutable boundExecutable, MethodExecutio } @Override - protected void messageHandled(ChannelHandlerContext ctx, NettyWebSocketSession session, Object message) { + protected void messageHandled(ChannelHandlerContext ctx, Object message) { ctx.executor().execute(() -> { try { nettyEmbeddedServices.getEventPublisher(WebSocketMessageProcessedEvent.class) - .publishEvent(new WebSocketMessageProcessedEvent<>(session, message)); + .publishEvent(new WebSocketMessageProcessedEvent<>(getSession(), message)); } catch (Exception e) { if (LOG.isErrorEnabled()) { LOG.error("Error publishing WebSocket message processed event: " + e.getMessage(), e); @@ -294,12 +368,12 @@ public void handlerRemoved(ChannelHandlerContext ctx) throws Exception { Channel channel = ctx.channel(); channel.attr(NettyWebSocketSession.WEB_SOCKET_SESSION_KEY).set(null); if (LOG.isDebugEnabled()) { - LOG.debug("Removing WebSocket Server session: " + session); + LOG.debug("Removing WebSocket Server session: " + serverSession); } webSocketSessionRepository.removeChannel(channel); try { nettyEmbeddedServices.getEventPublisher(WebSocketSessionClosedEvent.class) - .publishEvent(new WebSocketSessionClosedEvent(session)); + .publishEvent(new WebSocketSessionClosedEvent(serverSession)); } catch (Exception e) { if (LOG.isErrorEnabled()) { LOG.error("Error publishing WebSocket closed event: " + e.getMessage(), e); From 4c33d8d0913a3c74c3a92a44deb1dc7889c9b1f6 Mon Sep 17 00:00:00 2001 From: Jonas Konrad Date: Wed, 23 Nov 2022 14:15:01 +0100 Subject: [PATCH 260/743] Set service id for normal http client as well (#8407) Fixes #5059 --- .../HttpClientIntroductionAdvice.java | 3 - .../http/client/netty/DefaultHttpClient.java | 9 +- .../http/client/ServiceIdSpec.groovy | 101 ++++++++++++++++++ 3 files changed, 109 insertions(+), 4 deletions(-) create mode 100644 http-client/src/test/groovy/io/micronaut/http/client/ServiceIdSpec.groovy diff --git a/http-client-core/src/main/java/io/micronaut/http/client/interceptor/HttpClientIntroductionAdvice.java b/http-client-core/src/main/java/io/micronaut/http/client/interceptor/HttpClientIntroductionAdvice.java index 24ccc2f94c6..6d400b58a7e 100644 --- a/http-client-core/src/main/java/io/micronaut/http/client/interceptor/HttpClientIntroductionAdvice.java +++ b/http-client-core/src/main/java/io/micronaut/http/client/interceptor/HttpClientIntroductionAdvice.java @@ -299,11 +299,8 @@ public Object intercept(MethodInvocationContext context) { request.setAttribute(HttpAttributes.INVOCATION_CONTEXT, context); // Set the URI template used to make the request for tracing purposes request.setAttribute(HttpAttributes.URI_TEMPLATE, resolveTemplate(annotationMetadata, uriTemplate.toString())); - String serviceId = getClientId(annotationMetadata); Argument errorType = annotationMetadata.classValue(Client.class, "errorType") .map((Function) Argument::of).orElse(HttpClient.DEFAULT_ERROR_TYPE); - request.setAttribute(HttpAttributes.SERVICE_ID, serviceId); - final MediaType[] acceptTypes; Collection accept = request.accept(); diff --git a/http-client/src/main/java/io/micronaut/http/client/netty/DefaultHttpClient.java b/http-client/src/main/java/io/micronaut/http/client/netty/DefaultHttpClient.java index 2e8c39c90c9..2982b185ee1 100644 --- a/http-client/src/main/java/io/micronaut/http/client/netty/DefaultHttpClient.java +++ b/http-client/src/main/java/io/micronaut/http/client/netty/DefaultHttpClient.java @@ -35,6 +35,7 @@ import io.micronaut.core.util.CollectionUtils; import io.micronaut.core.util.StringUtils; import io.micronaut.core.util.SupplierUtil; +import io.micronaut.http.HttpAttributes; import io.micronaut.http.HttpResponse; import io.micronaut.http.HttpResponseWrapper; import io.micronaut.http.HttpStatus; @@ -1188,7 +1189,13 @@ private > Publisher applyFi Publisher responsePublisher) { if (request instanceof MutableHttpRequest) { - ((MutableHttpRequest) request).uri(requestURI); + MutableHttpRequest mutRequest = (MutableHttpRequest) request; + mutRequest.uri(requestURI); + if (informationalServiceId != null && + !mutRequest.getAttribute(HttpAttributes.SERVICE_ID).isPresent()) { + + mutRequest.setAttribute(HttpAttributes.SERVICE_ID, informationalServiceId); + } List filters = filterResolver.resolveFilters(request, clientFilterEntries); diff --git a/http-client/src/test/groovy/io/micronaut/http/client/ServiceIdSpec.groovy b/http-client/src/test/groovy/io/micronaut/http/client/ServiceIdSpec.groovy new file mode 100644 index 00000000000..3efb8c7a022 --- /dev/null +++ b/http-client/src/test/groovy/io/micronaut/http/client/ServiceIdSpec.groovy @@ -0,0 +1,101 @@ +package io.micronaut.http.client + +import io.micronaut.context.ApplicationContext +import io.micronaut.context.annotation.Requires +import io.micronaut.http.HttpAttributes +import io.micronaut.http.HttpRequest +import io.micronaut.http.HttpResponse +import io.micronaut.http.HttpVersion +import io.micronaut.http.MutableHttpRequest +import io.micronaut.http.annotation.Controller +import io.micronaut.http.annotation.Filter +import io.micronaut.http.annotation.Get +import io.micronaut.http.client.annotation.Client +import io.micronaut.http.filter.ClientFilterChain +import io.micronaut.http.filter.HttpClientFilter +import io.micronaut.runtime.server.EmbeddedServer +import jakarta.inject.Singleton +import org.reactivestreams.Publisher +import spock.lang.Specification + +class ServiceIdSpec extends Specification { + + def 'service id set by declarative client'() { + given: + def serverCtx = ApplicationContext.run([ + 'spec.name': 'ServiceIdSpec', + ]) + def server = serverCtx.getBean(EmbeddedServer) + server.start() + def clientCtx = ApplicationContext.run([ + 'spec.name': 'ServiceIdSpec', + 'micronaut.http.services.my-client-id.url': server.URI, + ]) + def client = clientCtx.getBean(DeclarativeClient) + def filter = clientCtx.getBean(ServiceIdFilter) + + expect: + filter.serviceId == null + client.index() == "foo" + filter.serviceId == "my-client-id" + + cleanup: + server.close() + serverCtx.close() + clientCtx.close() + } + + def 'service id set by normal client'() { + given: + def serverCtx = ApplicationContext.run([ + 'spec.name': 'ServiceIdSpec', + ]) + def server = serverCtx.getBean(EmbeddedServer) + server.start() + def clientCtx = ApplicationContext.run([ + 'spec.name': 'ServiceIdSpec', + 'micronaut.http.services.my-client-id.url': server.URI, + ]) + def client = clientCtx.getBean(HttpClientRegistry).getClient(HttpVersion.HTTP_1_1, "my-client-id", null) + def filter = clientCtx.getBean(ServiceIdFilter) + + expect: + filter.serviceId == null + client.toBlocking().exchange("/service-id", String).body() == "foo" + filter.serviceId == "my-client-id" + + cleanup: + server.close() + serverCtx.close() + clientCtx.close() + } + + @Client(id = "my-client-id") + static interface DeclarativeClient { + @Get("/service-id") + String index() + } + + @Singleton + @Requires(property = "spec.name", value = "ServiceIdSpec") + @Controller("/service-id") + static class ServiceIdController { + @Get + def index(HttpRequest request) { + return "foo" + } + } + + @Singleton + @Requires(property = "spec.name", value = "ServiceIdSpec") + @Filter(Filter.MATCH_ALL_PATTERN) + static class ServiceIdFilter implements HttpClientFilter { + String serviceId + + @Override + Publisher> doFilter(MutableHttpRequest request, ClientFilterChain chain) { + serviceId = request.getAttribute(HttpAttributes.SERVICE_ID).orElse(null) + return chain.proceed(request) + } + } +} From 2ed76bc546ae9593cf6a1112f1366dc40d483510 Mon Sep 17 00:00:00 2001 From: Denis Stepanov Date: Wed, 23 Nov 2022 21:28:55 +0700 Subject: [PATCH 261/743] Add field generic argument annotation test (#8409) --- .../beans/BeanIntrospectionSpec.groovy | 50 ++++++++++++++++++- 1 file changed, 48 insertions(+), 2 deletions(-) diff --git a/inject-java-test/src/test/groovy/io/micronaut/inject/visitor/beans/BeanIntrospectionSpec.groovy b/inject-java-test/src/test/groovy/io/micronaut/inject/visitor/beans/BeanIntrospectionSpec.groovy index cfbdcf3ae31..4b244fd2834 100644 --- a/inject-java-test/src/test/groovy/io/micronaut/inject/visitor/beans/BeanIntrospectionSpec.groovy +++ b/inject-java-test/src/test/groovy/io/micronaut/inject/visitor/beans/BeanIntrospectionSpec.groovy @@ -28,13 +28,12 @@ import io.micronaut.inject.ExecutableMethod import io.micronaut.inject.beans.visitor.IntrospectedTypeElementVisitor import io.micronaut.inject.visitor.TypeElementVisitor import io.micronaut.jackson.modules.BeanIntrospectionModule +import jakarta.inject.Singleton import spock.lang.IgnoreIf - import spock.lang.Issue import spock.lang.Requires import javax.annotation.processing.SupportedAnnotationTypes -import jakarta.inject.Singleton import javax.persistence.Column import javax.persistence.Entity import javax.persistence.Id @@ -4368,6 +4367,53 @@ public record Foo(String name, String isSurname, boolean contains, Boolean purge introspection.propertyNames as List == ["name", "isSurname", "contains", "purged", "isUpdated", "isDeleted"] } + void 'test annotation on a generic field argument'() { + when: + BeanIntrospection beanIntrospection = buildBeanIntrospection('test.Book', ''' +package test; + +import javax.validation.Valid; +import javax.validation.constraints.Size; +import java.util.List; +import io.micronaut.core.annotation.Introspected; + +class Author { +} + +@Introspected +class Book { + + @Size(min=2) + private String name; + + private List<@Valid Author> authors; + + public Book(String name) { + this.name = name; + this.authors = null; + } + + public Book(String name, List authors) { + this.name = name; + this.authors = authors; + } + + public List getAuthors() { + return authors; + } + + public String getName() { + return name; + } +} +''') + def property = beanIntrospection.getBeanProperties().first() + + then: + property.name == "authors" + property.asArgument().getTypeParameters()[0].annotationMetadata.hasStereotype("javax.validation.Valid") + } + @Override protected JavaParser newJavaParser() { return new JavaParser() { From b4d9bbd627732d12be889569aab75f905325ec0a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20S=C3=A1nchez-Mariscal?= Date: Wed, 23 Nov 2022 16:39:26 +0100 Subject: [PATCH 262/743] Exclude module descriptors from the shaded JAR (#8412) Fixes https://github.com/micronaut-projects/micronaut-core/issues/8402 --- parent/build.gradle | 1 + 1 file changed, 1 insertion(+) diff --git a/parent/build.gradle b/parent/build.gradle index 0b325db055a..9337761c630 100644 --- a/parent/build.gradle +++ b/parent/build.gradle @@ -260,6 +260,7 @@ ext.extraPomInfo = { exclude "META-INF/*.SF" exclude "META-INF/*.DSA" exclude "META-INF/*.RSA" + exclude "module-info.class" } } } From ad3db52aff947a138166455cfedee91f6a117756 Mon Sep 17 00:00:00 2001 From: Tim Yates Date: Thu, 24 Nov 2022 08:46:47 +0000 Subject: [PATCH 263/743] build: Add Hibernate validator BOM (#8415) * ci: GraalVM workflow remove java 11 Co-authored-by: Sergio del Amo --- .github/workflows/graalvm.yml | 2 +- gradle/libs.versions.toml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/graalvm.yml b/.github/workflows/graalvm.yml index d0cf09395b7..23d722775de 100644 --- a/.github/workflows/graalvm.yml +++ b/.github/workflows/graalvm.yml @@ -19,7 +19,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - java: ['11', '17'] + java: ['17'] graalvm: ['latest', 'dev'] steps: # https://github.com/actions/virtual-environments/issues/709 diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 24fd29c921a..ba22243c2c6 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -82,7 +82,7 @@ managed-micronaut-gcp = "4.6.0" managed-micronaut-graphql = "3.2.0" managed-micronaut-groovy = "3.3.1" managed-micronaut-grpc = "3.3.1" -managed-micronaut-hibernate-validator = "3.2.0" +managed-micronaut-hibernate-validator = "3.3.0" managed-micronaut-ignite = "1.0.0.RC1" managed-micronaut-jaxrs = "3.4.0" managed-micronaut-jms = "2.1.0" @@ -156,6 +156,7 @@ boms-micronaut-data = { module = "io.micronaut.data:micronaut-data-bom", version boms-micronaut-gcp = { module = "io.micronaut.gcp:micronaut-gcp-bom", version.ref = "managed-micronaut-gcp" } boms-micronaut-grpc = { module = "io.micronaut.grpc:micronaut-grpc-bom", version.ref = "managed-micronaut-grpc" } boms-micronaut-groovy = { module = "io.micronaut.groovy:micronaut-groovy-bom", version.ref = "managed-micronaut-groovy" } +boms-micronaut-hibernate-validator = { module = "io.micronaut.beanvalidation:micronaut-hibernate-validator-bom", version.ref = "managed-micronaut-hibernate-validator" } boms-micronaut-jaxrs = { module = "io.micronaut.jaxrs:micronaut-jaxrs-bom", version.ref = "managed-micronaut-jaxrs" } boms-micronaut-kafka = { module = "io.micronaut.kafka:micronaut-kafka-bom", version.ref = "managed-micronaut-kafka" } boms-micronaut-kotlin = { module = "io.micronaut.kotlin:micronaut-kotlin-bom", version.ref = "managed-micronaut-kotlin" } @@ -286,7 +287,6 @@ managed-micronaut-cassandra = { module = "io.micronaut.cassandra:micronaut-cassa managed-micronaut-discovery = { module = "io.micronaut.discovery:micronaut-discovery-client", version.ref = "managed-micronaut-discovery" } managed-micronaut-elasticsearch = { module = "io.micronaut.elasticsearch:micronaut-elasticsearch", version.ref = "managed-micronaut-elasticsearch" } managed-micronaut-graphql = { module = "io.micronaut.graphql:micronaut-graphql", version.ref = "managed-micronaut-graphql" } -managed-micronaut-hibernate-validator = { module = "io.micronaut.beanvalidation:micronaut-hibernate-validator", version.ref = "managed-micronaut-hibernate-validator" } managed-micronaut-ignite-core = { module = "io.micronaut.ignite:micronaut-ignite-core", version.ref = "managed-micronaut-ignite" } managed-micronaut-ignite-cache = { module = "io.micronaut.ignite:micronaut-ignite-cache", version.ref = "managed-micronaut-ignite" } managed-micronaut-jms = { module = "io.micronaut.jms:micronaut-jms-core", version.ref = "managed-micronaut-jms" } From 54534be2a213fb080a3d1deb3385892466f24dd0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20S=C3=A1nchez-Mariscal?= Date: Thu, 24 Nov 2022 18:10:08 +0100 Subject: [PATCH 264/743] Bump GraalVM Native Build Tools to 0.9.18 (#8399) * Bump GraalVM Native Build Tools to 0.9.18 * Add NBT as a managed dependency --- gradle/libs.versions.toml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index ba22243c2c6..e62f6e24f20 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -62,7 +62,7 @@ managed-kafka = "2.8.2" managed-ktor = "1.6.8" managed-logback = "1.2.11" managed-lombok = "1.18.24" -managed-maven-native-plugin = "0.9.13" +managed-maven-native-plugin = "0.9.18" managed-methvin-directory-watcher = "0.16.1" managed-micrometer = "1.9.4" managed-micronaut-acme = "3.2.0" @@ -227,6 +227,7 @@ managed-gorm-hibernate = { module = "org.grails:grails-datastore-gorm-hibernate5 managed-graal = { module = "org.graalvm.nativeimage:svm", version.ref = "managed-graal-svm" } managed-graal-sdk = { module = "org.graalvm.sdk:graal-sdk", version.ref = "managed-graal-sdk" } +managed-maven-native-plugin = { module = "org.graalvm.buildtools:native-maven-plugin", version.ref = "managed-maven-native-plugin" } managed-groovy = { module = "org.codehaus.groovy:groovy", version.ref = "managed-groovy" } managed-groovy-json = { module = "org.codehaus.groovy:groovy-json", version.ref = "managed-groovy" } From 879e15b1cc5fbdf97b82eb60f77fd7d675971d09 Mon Sep 17 00:00:00 2001 From: micronaut-build <65172877+micronaut-build@users.noreply.github.com> Date: Mon, 28 Nov 2022 02:54:22 -0500 Subject: [PATCH 265/743] build: Bump micronaut-liquibase to 5.5.0 (#8432) --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index e62f6e24f20..8026d969e92 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -92,7 +92,7 @@ managed-micronaut-kotlin = "3.2.2" managed-micronaut-kubernetes = "3.4.0" managed-micronaut-micrometer = "4.6.1" managed-micronaut-microstream = "1.2.0" -managed-micronaut-liquibase = "5.4.1" +managed-micronaut-liquibase = "5.5.0" managed-micronaut-mongo = "4.6.0" managed-micronaut-mqtt = "2.3.0" managed-micronaut-multitenancy = "4.2.0" From 61e993ba7a7c5d00c5c06dd20bf812f254cd204a Mon Sep 17 00:00:00 2001 From: micronaut-build <65172877+micronaut-build@users.noreply.github.com> Date: Mon, 28 Nov 2022 02:54:39 -0500 Subject: [PATCH 266/743] build: Bump micronaut-email to 1.5.0 (#8430) --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 8026d969e92..59edea780cf 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -76,7 +76,7 @@ managed-micronaut-crac = "1.0.1" managed-micronaut-data = "3.8.1" managed-micronaut-discovery = "3.2.0" managed-micronaut-elasticsearch = "4.3.0" -managed-micronaut-email = "1.4.0" +managed-micronaut-email = "1.5.0" managed-micronaut-flyway = "5.4.1" managed-micronaut-gcp = "4.6.0" managed-micronaut-graphql = "3.2.0" From 3c21a23e60779462a32c966b4d7e1d5e0ad08ebe Mon Sep 17 00:00:00 2001 From: micronaut-build <65172877+micronaut-build@users.noreply.github.com> Date: Mon, 28 Nov 2022 02:54:57 -0500 Subject: [PATCH 267/743] build: Bump micronaut-jackson-xml to 3.2.0 (#8429) --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 59edea780cf..2601224f01c 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -123,7 +123,7 @@ managed-micronaut-toml = "1.1.1" managed-micronaut-tracing = "4.4.0" managed-micronaut-tracing-legacy = "3.2.7" managed-micronaut-views = "3.7.1" -managed-micronaut-xml = "3.1.0" +managed-micronaut-xml = "3.2.0" managed-neo4j = "3.5.35" managed-neo4j-java-driver = "4.4.9" managed-netty = "4.1.84.Final" From f84fc45539121272a32b83ca9128106db25045e1 Mon Sep 17 00:00:00 2001 From: micronaut-build <65172877+micronaut-build@users.noreply.github.com> Date: Mon, 28 Nov 2022 04:32:12 -0500 Subject: [PATCH 268/743] Bump micronaut-test to 3.8.0 (#8431) --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 2601224f01c..ca9792cd8e7 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -117,7 +117,7 @@ managed-micronaut-serialization = "1.3.3" managed-micronaut-servlet = "3.3.2" managed-micronaut-spring = "4.3.1" managed-micronaut-sql = "4.7.2" -managed-micronaut-test = "3.7.0" +managed-micronaut-test = "3.8.0" managed-micronaut-test-resources = "1.1.3" managed-micronaut-toml = "1.1.1" managed-micronaut-tracing = "4.4.0" From b928348624f5842ae75bda4df556007773862ab6 Mon Sep 17 00:00:00 2001 From: micronaut-build <65172877+micronaut-build@users.noreply.github.com> Date: Mon, 28 Nov 2022 07:07:41 -0500 Subject: [PATCH 269/743] Bump micronaut-redis to 5.3.2 (#8433) * Bump micronaut-redis to 5.3.2 * Update graalvm.yml Co-authored-by: Sergio del Amo --- .github/workflows/graalvm.yml | 2 +- gradle/libs.versions.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/graalvm.yml b/.github/workflows/graalvm.yml index d0cf09395b7..23d722775de 100644 --- a/.github/workflows/graalvm.yml +++ b/.github/workflows/graalvm.yml @@ -19,7 +19,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - java: ['11', '17'] + java: ['17'] graalvm: ['latest', 'dev'] steps: # https://github.com/actions/virtual-environments/issues/709 diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index cd2f03d7a41..38922cd9f20 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -107,7 +107,7 @@ managed-micronaut-problem = "2.5.1" managed-micronaut-rabbitmq = "3.3.0" managed-micronaut-r2dbc = "4.0.0" managed-micronaut-reactor = "2.4.1" -managed-micronaut-redis = "5.3.1" +managed-micronaut-redis = "5.3.2" managed-micronaut-rss = "3.2.0" managed-micronaut-rxjava1 = "1.0.0" managed-micronaut-rxjava2 = "1.3.0" From 674097a1f1983e19582385ff043056b9d8bbd850 Mon Sep 17 00:00:00 2001 From: Tim Yates Date: Mon, 28 Nov 2022 14:47:14 +0000 Subject: [PATCH 270/743] build: JMX BOM (#8322) --- gradle/libs.versions.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index ca9792cd8e7..9cc644e9362 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -86,7 +86,7 @@ managed-micronaut-hibernate-validator = "3.3.0" managed-micronaut-ignite = "1.0.0.RC1" managed-micronaut-jaxrs = "3.4.0" managed-micronaut-jms = "2.1.0" -managed-micronaut-jmx = "3.1.0" +managed-micronaut-jmx = "3.2.0" managed-micronaut-kafka = "4.5.0" managed-micronaut-kotlin = "3.2.2" managed-micronaut-kubernetes = "3.4.0" @@ -158,6 +158,7 @@ boms-micronaut-grpc = { module = "io.micronaut.grpc:micronaut-grpc-bom", version boms-micronaut-groovy = { module = "io.micronaut.groovy:micronaut-groovy-bom", version.ref = "managed-micronaut-groovy" } boms-micronaut-hibernate-validator = { module = "io.micronaut.beanvalidation:micronaut-hibernate-validator-bom", version.ref = "managed-micronaut-hibernate-validator" } boms-micronaut-jaxrs = { module = "io.micronaut.jaxrs:micronaut-jaxrs-bom", version.ref = "managed-micronaut-jaxrs" } +boms-micronaut-jmx = { module = "io.micronaut.jmx:micronaut-jmx-bom", version.ref = "managed-micronaut-jmx" } boms-micronaut-kafka = { module = "io.micronaut.kafka:micronaut-kafka-bom", version.ref = "managed-micronaut-kafka" } boms-micronaut-kotlin = { module = "io.micronaut.kotlin:micronaut-kotlin-bom", version.ref = "managed-micronaut-kotlin" } boms-micronaut-kubernetes = { module = "io.micronaut.kubernetes:micronaut-kubernetes-bom", version.ref = "managed-micronaut-kubernetes" } @@ -294,7 +295,6 @@ managed-micronaut-jms = { module = "io.micronaut.jms:micronaut-jms-core", versio managed-micronaut-jms-activemq-classic = { module = "io.micronaut.jms:micronaut-jms-activemq-classic", version.ref = "managed-micronaut-jms" } managed-micronaut-jms-activemq-artemis = { module = "io.micronaut.jms:micronaut-jms-activemq-artemis", version.ref = "managed-micronaut-jms" } managed-micronaut-jms-sqs = { module = "io.micronaut.jms:micronaut-jms-sqs", version.ref = "managed-micronaut-jms" } -managed-micronaut-jmx = { module = "io.micronaut.jmx:micronaut-jmx", version.ref = "managed-micronaut-jmx" } managed-micronaut-multitenancy = { module = "io.micronaut.multitenancy:micronaut-multitenancy", version.ref = "managed-micronaut-multitenancy" } managed-micronaut-nats = { module = "io.micronaut.nats:micronaut-nats", version.ref = "managed-micronaut-nats" } managed-micronaut-neo4j = { module = "io.micronaut.neo4j:micronaut-neo4j-bolt", version.ref = "managed-micronaut-neo4j" } From eed073f2ca2f0e39cda593a3743802214b7b74f3 Mon Sep 17 00:00:00 2001 From: micronaut-build <65172877+micronaut-build@users.noreply.github.com> Date: Mon, 28 Nov 2022 10:41:30 -0500 Subject: [PATCH 271/743] Bump micronaut-jmx to 3.2.0 (#8340) Co-authored-by: Sergio del Amo From 218bfd28bae98dd63ca53bdcbe0184d9e32a882e Mon Sep 17 00:00:00 2001 From: Denis Stepanov Date: Tue, 29 Nov 2022 15:55:58 +0700 Subject: [PATCH 272/743] Fix Java error element reporting (#8418) --- .../annotation/processing/visitor/JavaElementFactory.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaElementFactory.java b/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaElementFactory.java index 3d9c70f8268..cf63f68b4b9 100644 --- a/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaElementFactory.java +++ b/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaElementFactory.java @@ -318,11 +318,11 @@ private void failIfPostponeIsNeeded(ExecutableElement executableElement) { TypeMirror returnType = executableElement.getReturnType(); TypeKind returnKind = returnType.getKind(); if (returnKind == TypeKind.ERROR) { - throw new PostponeToNextRoundException(returnType); + throw new PostponeToNextRoundException(executableElement); } } - private void failIfPostponeIsNeeded(VariableElement variableElement) { + private void failIfPostponeIsNeeded(VariableElement variableElement) { TypeMirror type = variableElement.asType(); if (type.getKind() == TypeKind.ERROR) { throw new PostponeToNextRoundException(variableElement); From 825e9228ffe5fc8f7832a9f6f80bdea6327b6f6f Mon Sep 17 00:00:00 2001 From: Jonas Konrad Date: Tue, 29 Nov 2022 09:57:23 +0100 Subject: [PATCH 273/743] Fix log error (#8437) --- .../http/client/netty/HttpLineBasedFrameDecoder.java | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/http-client/src/main/java/io/micronaut/http/client/netty/HttpLineBasedFrameDecoder.java b/http-client/src/main/java/io/micronaut/http/client/netty/HttpLineBasedFrameDecoder.java index eacc0125f52..77be0a61b39 100644 --- a/http-client/src/main/java/io/micronaut/http/client/netty/HttpLineBasedFrameDecoder.java +++ b/http-client/src/main/java/io/micronaut/http/client/netty/HttpLineBasedFrameDecoder.java @@ -29,6 +29,8 @@ import io.netty.handler.codec.http.HttpResponseStatus; import io.netty.handler.codec.http.LastHttpContent; +import java.util.NoSuchElementException; + /** * Variant of {@link LineBasedFrameDecoder} that accepts * {@link io.netty.handler.codec.http.HttpContent} data. Note: this handler removes itself when the @@ -78,7 +80,11 @@ public void handlerAdded(ChannelHandlerContext ctx) { @Override protected void handlerRemoved0(ChannelHandlerContext ctx) { - ctx.pipeline().remove(Wrap.NAME); + try { + ctx.pipeline().remove(Wrap.NAME); + } catch (NoSuchElementException ignored) { + // can happen if the pipeline is being shut down + } } @Sharable @@ -88,8 +94,7 @@ private static class Wrap extends ChannelInboundHandlerAdapter { @Override public void channelRead(@NonNull ChannelHandlerContext ctx, @NonNull Object msg) throws Exception { - if (msg instanceof ByteBuf) { - ByteBuf buffer = (ByteBuf) msg; + if (msg instanceof ByteBuf buffer) { // todo: this is necessary because downstream handlers sometimes do the // `if (refcnt > 0) release` pattern. We should eventually fix that. ByteBuf copy = buffer.copy(); From b3fe7c4dd6c191db4546eba705c0e0bf47bf49d7 Mon Sep 17 00:00:00 2001 From: Tim Yates Date: Tue, 29 Nov 2022 09:45:32 +0000 Subject: [PATCH 274/743] Add Rabbit BOM (#8321) --- gradle/libs.versions.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 9cc644e9362..36cf0368018 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -104,7 +104,7 @@ managed-micronaut-openapi = "4.5.2" managed-micronaut-oraclecloud = "2.3.0" managed-micronaut-picocli = "4.3.0" managed-micronaut-problem = "2.5.1" -managed-micronaut-rabbitmq = "3.3.0" +managed-micronaut-rabbitmq = "3.4.0" managed-micronaut-r2dbc = "4.0.0" managed-micronaut-reactor = "2.4.1" managed-micronaut-redis = "5.3.1" @@ -172,6 +172,7 @@ boms-micronaut-oraclecloud = { module = "io.micronaut.oraclecloud:micronaut-orac boms-micronaut-openapi = { module = "io.micronaut.openapi:micronaut-openapi-bom", version.ref = "managed-micronaut-openapi" } boms-micronaut-picocli = { module = "io.micronaut.picocli:micronaut-picocli-bom", version.ref = "managed-micronaut-picocli" } boms-micronaut-problem-json = { module = "io.micronaut.problem:micronaut-problem-json-bom", version.ref = "managed-micronaut-problem" } +boms-micronaut-rabbit = { module = "io.micronaut.rabbitmq:micronaut-rabbitmq-bom", version.ref = "managed-micronaut-rabbitmq" } boms-micronaut-redis = { module = "io.micronaut.redis:micronaut-redis-bom", version.ref = "managed-micronaut-redis" } boms-micronaut-rxjava2 = { module = "io.micronaut.rxjava2:micronaut-rxjava2-bom", version.ref = "managed-micronaut-rxjava2" } boms-micronaut-rxjava3 = { module = "io.micronaut.rxjava3:micronaut-rxjava3-bom", version.ref = "managed-micronaut-rxjava3" } @@ -302,7 +303,6 @@ managed-micronaut-netflix = { module = "io.micronaut.netflix:micronaut-netflix-a managed-micronaut-netflix-hystrix = { module = "io.micronaut.netflix:micronaut-netflix-hystrix", version.ref = "managed-micronaut-netflix" } managed-micronaut-netflix-ribbon = { module = "io.micronaut.netflix:micronaut-netflix-ribbon", version.ref = "managed-micronaut-netflix" } managed-micronaut-openapi = { module = "io.micronaut.openapi:micronaut-openapi", version.ref = "managed-micronaut-openapi" } -managed-micronaut-rabbitmq = { module = "io.micronaut.rabbitmq:micronaut-rabbitmq", version.ref = "managed-micronaut-rabbitmq" } managed-micronaut-rss = { module = "io.micronaut.rss:micronaut-rss", version.ref = "managed-micronaut-rss" } managed-micronaut-rss-core = { module = "io.micronaut.rss:micronaut-rss-core", version.ref = "managed-micronaut-rss" } managed-micronaut-rss-language = { module = "io.micronaut.rss:micronaut-rss-language", version.ref = "managed-micronaut-rss" } From ccee11b9802c0ca7044ca300e362ff5afede3b79 Mon Sep 17 00:00:00 2001 From: Jonas Konrad Date: Tue, 29 Nov 2022 11:39:00 +0100 Subject: [PATCH 275/743] Backport #8155 (#8434) * Fix HttpClient.start (#8155) This fixes the tests in micronaut-reactor, and copies the relevant test to this repo as well. * Stop trying to run graal 11 dev Co-authored-by: Tim Yates --- .github/workflows/graalvm.yml | 3 ++ .../http/client/netty/ConnectionManager.java | 23 +++++++++---- .../http/client/HttpClientCloseSpec.groovy | 34 +++++++++++++++++++ 3 files changed, 53 insertions(+), 7 deletions(-) create mode 100644 http-client/src/test/groovy/io/micronaut/http/client/HttpClientCloseSpec.groovy diff --git a/.github/workflows/graalvm.yml b/.github/workflows/graalvm.yml index 23d722775de..7443db9c146 100644 --- a/.github/workflows/graalvm.yml +++ b/.github/workflows/graalvm.yml @@ -21,6 +21,9 @@ jobs: matrix: java: ['17'] graalvm: ['latest', 'dev'] + include: + - graalvm: 'latest' + java: '11' steps: # https://github.com/actions/virtual-environments/issues/709 - name: Free disk space diff --git a/http-client/src/main/java/io/micronaut/http/client/netty/ConnectionManager.java b/http-client/src/main/java/io/micronaut/http/client/netty/ConnectionManager.java index b2dedb0584e..fdb8aa606b1 100644 --- a/http-client/src/main/java/io/micronaut/http/client/netty/ConnectionManager.java +++ b/http-client/src/main/java/io/micronaut/http/client/netty/ConnectionManager.java @@ -150,7 +150,8 @@ final class ConnectionManager { private EventLoopGroup group; private final boolean shutdownGroup; private final ThreadFactory threadFactory; - private final Bootstrap bootstrap; + private final ChannelFactory socketChannelFactory; + private Bootstrap bootstrap; private final HttpClientConfiguration configuration; @Nullable private final Long readTimeoutMillis; @@ -181,6 +182,7 @@ final class ConnectionManager { this.log = log; this.httpVersion = httpVersion; this.threadFactory = threadFactory; + this.socketChannelFactory = socketChannelFactory; this.configuration = configuration; this.instrumenter = instrumenter; this.clientCustomizer = clientCustomizer; @@ -203,10 +205,7 @@ final class ConnectionManager { shutdownGroup = true; } - this.bootstrap = new Bootstrap(); - this.bootstrap.group(group) - .channelFactory(socketChannelFactory) - .option(ChannelOption.SO_KEEPALIVE, true); + initBootstrap(); final ChannelHealthChecker channelHealthChecker = channel -> channel.eventLoop().newSucceededFuture(channel.isActive() && !ConnectTTLHandler.isChannelExpired(channel)); @@ -308,8 +307,18 @@ private static NioEventLoopGroup createEventLoopGroup(HttpClientConfiguration co * @see DefaultHttpClient#start() */ public void start() { - group = createEventLoopGroup(configuration, threadFactory); - bootstrap.group(group); + // only need to start new group if it's managed by us + if (shutdownGroup) { + group = createEventLoopGroup(configuration, threadFactory); + initBootstrap(); // rebuild bootstrap with new group + } + } + + private void initBootstrap() { + this.bootstrap = new Bootstrap(); + this.bootstrap.group(group) + .channelFactory(socketChannelFactory) + .option(ChannelOption.SO_KEEPALIVE, true); } /** diff --git a/http-client/src/test/groovy/io/micronaut/http/client/HttpClientCloseSpec.groovy b/http-client/src/test/groovy/io/micronaut/http/client/HttpClientCloseSpec.groovy new file mode 100644 index 00000000000..027f0667b8b --- /dev/null +++ b/http-client/src/test/groovy/io/micronaut/http/client/HttpClientCloseSpec.groovy @@ -0,0 +1,34 @@ +package io.micronaut.http.client + +import spock.lang.Specification +import spock.util.concurrent.PollingConditions + +class HttpClientCloseSpec extends Specification { + void "confirm HttpClient can be stopped"() { + given: + HttpClient client = HttpClient.create(new URL("http://localhost")) + + expect: + client.isRunning() + + when: + client.stop() + + then: + new PollingConditions().eventually { + !client.isRunning() + } + + when: + client.start() + + then: + new PollingConditions().eventually { + client.isRunning() + } + + cleanup: + client.close() + + } +} From 54ffd2f95a696d616471f85b47f73591ad00c070 Mon Sep 17 00:00:00 2001 From: micronaut-build <65172877+micronaut-build@users.noreply.github.com> Date: Wed, 30 Nov 2022 02:12:11 -0500 Subject: [PATCH 276/743] Bump micronaut-maven-plugin to 3.5.1 (#8442) --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 686b019be9c..8efb3b302b4 100644 --- a/gradle.properties +++ b/gradle.properties @@ -43,7 +43,7 @@ developers=Graeme Rocher kapt.use.worker.api=true # Dependency Versions -micronautMavenPluginVersion=3.5.0 +micronautMavenPluginVersion=3.5.1 chromedriverVersion=79.0.3945.36 geckodriverVersion=0.26.0 webdriverBinariesVersion=1.4 From 01913019e7b6942dfd6a48de5fe693805c7c011c Mon Sep 17 00:00:00 2001 From: Denis Stepanov Date: Wed, 30 Nov 2022 17:31:24 +0700 Subject: [PATCH 277/743] Improve pospone error message (#8445) --- .../BeanDefinitionInjectProcessor.java | 9 ++-- .../PostponeToNextRoundException.java | 12 ++++- .../visitor/JavaElementFactory.java | 45 ++++++++++--------- 3 files changed, 39 insertions(+), 27 deletions(-) diff --git a/inject-java/src/main/java/io/micronaut/annotation/processing/BeanDefinitionInjectProcessor.java b/inject-java/src/main/java/io/micronaut/annotation/processing/BeanDefinitionInjectProcessor.java index 791fc5e13b1..a8ace115984 100644 --- a/inject-java/src/main/java/io/micronaut/annotation/processing/BeanDefinitionInjectProcessor.java +++ b/inject-java/src/main/java/io/micronaut/annotation/processing/BeanDefinitionInjectProcessor.java @@ -99,7 +99,7 @@ public class BeanDefinitionInjectProcessor extends AbstractInjectAnnotationProce private Set beanDefinitions; private final Set processed = new HashSet<>(); - private final Map postponed = new HashMap<>(); + private final Map postponed = new HashMap<>(); @Override public final synchronized void init(ProcessingEnvironment processingEnv) { @@ -238,7 +238,7 @@ public final boolean process(Set annotations, RoundEnviro error((Element) ex.getOriginatingElement(), ex.getMessage()); } catch (PostponeToNextRoundException e) { processed.remove(className); - postponed.put(className, (Element) e.getErrorElement()); + postponed.put(className, e); } } } @@ -250,8 +250,9 @@ public final boolean process(Set annotations, RoundEnviro processing round. */ if (processingOver) { - for (Map.Entry e : postponed.entrySet()) { - javaVisitorContext.warn("Bean definition generation [" + e.getKey() + "] skipped from processing because of prior error. This error is normally due to missing classes on the classpath. Verify the compilation classpath is correct to resolve the problem.", e.getValue()); + for (Map.Entry e : postponed.entrySet()) { + javaVisitorContext.warn("Bean definition generation [" + e.getKey() + "] skipped from processing because of prior error: [" + e.getValue().getPath() + "]." + + " This error is normally due to missing classes on the classpath. Verify the compilation classpath is correct to resolve the problem.", (Element) e.getValue().getErrorElement()); } try { diff --git a/inject-java/src/main/java/io/micronaut/annotation/processing/PostponeToNextRoundException.java b/inject-java/src/main/java/io/micronaut/annotation/processing/PostponeToNextRoundException.java index 174c61cac7e..9511af82318 100644 --- a/inject-java/src/main/java/io/micronaut/annotation/processing/PostponeToNextRoundException.java +++ b/inject-java/src/main/java/io/micronaut/annotation/processing/PostponeToNextRoundException.java @@ -21,15 +21,25 @@ public final class PostponeToNextRoundException extends RuntimeException { private final transient Object errorElement; + private final String path; - public PostponeToNextRoundException(Object originatingElement) { + /** + * @param originatingElement Teh originating element + * @param path The originating element path + */ + public PostponeToNextRoundException(Object originatingElement, String path) { this.errorElement = originatingElement; + this.path = path; } public Object getErrorElement() { return errorElement; } + public String getPath() { + return path; + } + @Override public synchronized Throwable fillInStackTrace() { // no-op: flow control exception diff --git a/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaElementFactory.java b/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaElementFactory.java index cf63f68b4b9..fdaf37ddfb2 100644 --- a/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaElementFactory.java +++ b/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaElementFactory.java @@ -21,9 +21,10 @@ import io.micronaut.core.annotation.NonNull; import io.micronaut.core.annotation.Nullable; import io.micronaut.inject.ast.ClassElement; -import io.micronaut.inject.ast.annotation.ElementAnnotationMetadataFactory; import io.micronaut.inject.ast.ElementFactory; import io.micronaut.inject.ast.MethodElement; +import io.micronaut.inject.ast.TypedElement; +import io.micronaut.inject.ast.annotation.ElementAnnotationMetadataFactory; import io.micronaut.inject.ast.beans.BeanElementBuilder; import io.micronaut.inject.configuration.ConfigurationMetadataBuilder; @@ -176,7 +177,7 @@ public JavaMethodElement newSourceMethodElement(ClassElement declaringClass, @NonNull ExecutableElement method, @NonNull ElementAnnotationMetadataFactory annotationMetadataFactory) { validateOwningClass(declaringClass); - failIfPostponeIsNeeded(method); + failIfPostponeIsNeeded(declaringClass, method); return new JavaMethodElement( (JavaClassElement) declaringClass, method, @@ -203,7 +204,7 @@ public JavaMethodElement newMethodElement(ClassElement owningType, @NonNull ExecutableElement method, @NonNull ElementAnnotationMetadataFactory annotationMetadataFactory) { validateOwningClass(owningType); - failIfPostponeIsNeeded(method); + failIfPostponeIsNeeded(owningType, method); return new JavaMethodElement( (JavaClassElement) owningType, method, @@ -215,19 +216,19 @@ public JavaMethodElement newMethodElement(ClassElement owningType, /** * Constructs a method element with the given generic type information. * - * @param declaringClass The declaring class + * @param owningType The owning class * @param method The method * @param annotationMetadataFactory The annotationMetadataFactory * @param genericTypes The generic type info * @return The method element */ - public JavaMethodElement newMethodElement(ClassElement declaringClass, + public JavaMethodElement newMethodElement(ClassElement owningType, @NonNull ExecutableElement method, @NonNull ElementAnnotationMetadataFactory annotationMetadataFactory, @Nullable Map> genericTypes) { - validateOwningClass(declaringClass); - failIfPostponeIsNeeded(method); - final JavaClassElement javaDeclaringClass = (JavaClassElement) declaringClass; + validateOwningClass(owningType); + failIfPostponeIsNeeded(owningType, method); + final JavaClassElement javaDeclaringClass = (JavaClassElement) owningType; final JavaVisitorContext javaVisitorContext = visitorContext; return new JavaMethodElement( @@ -266,13 +267,13 @@ public ClassElement getGenericReturnType() { @NonNull @Override - public JavaConstructorElement newConstructorElement(ClassElement declaringClass, + public JavaConstructorElement newConstructorElement(ClassElement owningType, @NonNull ExecutableElement constructor, @NonNull ElementAnnotationMetadataFactory annotationMetadataFactory) { - validateOwningClass(declaringClass); - failIfPostponeIsNeeded(constructor); + validateOwningClass(owningType); + failIfPostponeIsNeeded(owningType, constructor); return new JavaConstructorElement( - (JavaClassElement) declaringClass, + (JavaClassElement) owningType, constructor, annotationMetadataFactory, visitorContext @@ -281,15 +282,15 @@ public JavaConstructorElement newConstructorElement(ClassElement declaringClass, @NonNull @Override - public JavaEnumConstantElement newEnumConstantElement(ClassElement declaringClass, + public JavaEnumConstantElement newEnumConstantElement(ClassElement owningType, @NonNull VariableElement enumConstant, @NonNull ElementAnnotationMetadataFactory annotationMetadataFactory) { - if (!(declaringClass instanceof JavaEnumElement)) { + if (!(owningType instanceof JavaEnumElement)) { throw new IllegalArgumentException("Declaring class must be a JavaEnumElement"); } - failIfPostponeIsNeeded(enumConstant); + failIfPostponeIsNeeded(owningType, enumConstant); return new JavaEnumConstantElement( - (JavaEnumElement) declaringClass, + (JavaEnumElement) owningType, enumConstant, annotationMetadataFactory, visitorContext @@ -301,7 +302,7 @@ public JavaEnumConstantElement newEnumConstantElement(ClassElement declaringClas public JavaFieldElement newFieldElement(ClassElement declaringClass, @NonNull VariableElement field, @NonNull ElementAnnotationMetadataFactory annotationMetadataFactory) { - failIfPostponeIsNeeded(field); + failIfPostponeIsNeeded(declaringClass, field); return new JavaFieldElement( (JavaClassElement) declaringClass, field, @@ -310,22 +311,22 @@ public JavaFieldElement newFieldElement(ClassElement declaringClass, ); } - private void failIfPostponeIsNeeded(ExecutableElement executableElement) { + private void failIfPostponeIsNeeded(TypedElement member, ExecutableElement executableElement) { List parameters = executableElement.getParameters(); for (VariableElement parameter : parameters) { - failIfPostponeIsNeeded(parameter); + failIfPostponeIsNeeded(member, parameter); } TypeMirror returnType = executableElement.getReturnType(); TypeKind returnKind = returnType.getKind(); if (returnKind == TypeKind.ERROR) { - throw new PostponeToNextRoundException(executableElement); + throw new PostponeToNextRoundException(executableElement, member.getName() + " " + executableElement); } } - private void failIfPostponeIsNeeded(VariableElement variableElement) { + private void failIfPostponeIsNeeded(TypedElement member, VariableElement variableElement) { TypeMirror type = variableElement.asType(); if (type.getKind() == TypeKind.ERROR) { - throw new PostponeToNextRoundException(variableElement); + throw new PostponeToNextRoundException(variableElement, member.getName() + " " + variableElement); } } From 7809a8b0009d37112d958d29199b2814c4de1e49 Mon Sep 17 00:00:00 2001 From: Denis Stepanov Date: Wed, 30 Nov 2022 17:32:36 +0700 Subject: [PATCH 278/743] Ensure beans that require validation use Introspected (#8439) --- .../DeclaredBeanElementCreator.java | 68 +++++++++--- .../processing/FactoryBeanElementCreator.java | 15 ++- .../test/JavaFileObjectClassLoader.java | 56 +++++----- .../inject/records/RecordBeansSpec.groovy | 56 +++++----- .../io/micronaut/inject/records/Test.java | 18 +++ .../io/micronaut/inject/records/Test2.java | 15 +++ .../validator/DefaultValidator.java | 27 ++++- .../validation/validator/ValidatorSpec.groovy | 104 +++++++++++++++++- 8 files changed, 282 insertions(+), 77 deletions(-) create mode 100644 inject-java/src/test/groovy/io/micronaut/inject/records/Test.java create mode 100644 inject-java/src/test/groovy/io/micronaut/inject/records/Test2.java diff --git a/core-processor/src/main/java/io/micronaut/inject/processing/DeclaredBeanElementCreator.java b/core-processor/src/main/java/io/micronaut/inject/processing/DeclaredBeanElementCreator.java index 10515dc67fd..9ac34abb434 100644 --- a/core-processor/src/main/java/io/micronaut/inject/processing/DeclaredBeanElementCreator.java +++ b/core-processor/src/main/java/io/micronaut/inject/processing/DeclaredBeanElementCreator.java @@ -190,9 +190,7 @@ private void visitFieldInternal(BeanDefinitionVisitor visitor, FieldElement fiel } private void visitMethodInternal(BeanDefinitionVisitor visitor, MethodElement methodElement) { - if (methodElement.hasDeclaredAnnotation(ANN_REQUIRES_VALIDATION)) { - methodElement.annotate(ANN_VALIDATED); - } + makeInterceptedForValidationIfNeeded(methodElement); boolean claimed = visitMethod(visitor, methodElement); if (claimed) { addOriginatingElementIfNecessary(visitor, methodElement); @@ -200,9 +198,6 @@ private void visitMethodInternal(BeanDefinitionVisitor visitor, MethodElement me } private void visitPropertyInternal(BeanDefinitionVisitor visitor, PropertyElement propertyElement) { - if (propertyElement.hasAnnotation(ANN_REQUIRES_VALIDATION)) { - propertyElement.annotate(ANN_VALIDATED); - } boolean claimed = visitProperty(visitor, propertyElement); if (claimed) { propertyElement.getReadMethod().ifPresent(element -> addOriginatingElementIfNecessary(visitor, element)); @@ -220,15 +215,14 @@ private void visitPropertyInternal(BeanDefinitionVisitor visitor, PropertyElemen */ protected boolean visitProperty(BeanDefinitionVisitor visitor, PropertyElement propertyElement) { boolean claimed = false; - Optional writeMethod = propertyElement.getWriteMethod(); - if (writeMethod.isPresent()) { - MethodElement writeElement = writeMethod.get(); - claimed |= visitPropertyWriteElement(visitor, propertyElement, writeElement); + Optional writeMember = propertyElement.getWriteMember(); + if (writeMember.isPresent()) { + claimed |= visitPropertyWriteElement(visitor, propertyElement, writeMember.get()); } - Optional readMethod = propertyElement.getReadMethod(); - if (readMethod.isPresent()) { - MethodElement readElement = readMethod.get(); - claimed |= visitPropertyReadElement(visitor, propertyElement, readElement); + Optional readMember = propertyElement.getReadMember(); + if (readMember.isPresent()) { + boolean readElementClaimed = visitPropertyReadElement(visitor, propertyElement, readMember.get()); + claimed |= readElementClaimed; } // Process property's field if no methods were processed Optional field = propertyElement.getField(); @@ -239,6 +233,13 @@ protected boolean visitProperty(BeanDefinitionVisitor visitor, PropertyElement p return claimed; } + private static void makeInterceptedForValidationIfNeeded(MethodElement element) { + // The method with constrains should be intercepted with the validation interceptor + if (element.hasDeclaredAnnotation(ANN_REQUIRES_VALIDATION)) { + element.annotate(ANN_VALIDATED); + } + } + /** * Visit a property read element. * @@ -247,12 +248,46 @@ protected boolean visitProperty(BeanDefinitionVisitor visitor, PropertyElement p * @param readElement The read element * @return true if processed */ + protected boolean visitPropertyReadElement(BeanDefinitionVisitor visitor, + PropertyElement propertyElement, + MemberElement readElement) { + if (readElement instanceof MethodElement methodReadElement) { + return visitPropertyReadElement(visitor, propertyElement, methodReadElement); + } + return false; + } + + /** + * Visit a property method read element. + * + * @param visitor The visitor + * @param propertyElement The property + * @param readElement The read element + * @return true if processed + */ protected boolean visitPropertyReadElement(BeanDefinitionVisitor visitor, PropertyElement propertyElement, MethodElement readElement) { return visitAopAndExecutableMethod(visitor, readElement); } + /** + * Visit a property write element. + * + * @param visitor The visitor + * @param propertyElement The property + * @param writeElement The write element + * @return true if processed + */ + protected boolean visitPropertyWriteElement(BeanDefinitionVisitor visitor, + PropertyElement propertyElement, + MemberElement writeElement) { + if (writeElement instanceof MethodElement methodWriteElement) { + return visitPropertyWriteElement(visitor, propertyElement, methodWriteElement); + } + return false; + } + /** * Visit a property write element. * @@ -265,9 +300,14 @@ protected boolean visitPropertyReadElement(BeanDefinitionVisitor visitor, protected boolean visitPropertyWriteElement(BeanDefinitionVisitor visitor, PropertyElement propertyElement, MethodElement writeElement) { + makeInterceptedForValidationIfNeeded(writeElement); if (visitInjectAndLifecycleMethod(visitor, writeElement)) { + makeInterceptedForValidationIfNeeded(writeElement); return true; } else if (!writeElement.isStatic() && getElementAnnotationMetadata(writeElement).hasStereotype(AnnotationUtil.QUALIFIER)) { + if (propertyElement.getReadMethod().isPresent() && writeElement.hasStereotype(ANN_REQUIRES_VALIDATION)) { + visitor.setValidated(true); + } staticMethodCheck(writeElement); // TODO: Require @ReflectiveAccess for private methods in Micronaut 4 visitMethodInjectionPoint(visitor, writeElement); diff --git a/core-processor/src/main/java/io/micronaut/inject/processing/FactoryBeanElementCreator.java b/core-processor/src/main/java/io/micronaut/inject/processing/FactoryBeanElementCreator.java index 0262d4a21e5..7e11e77e6e5 100644 --- a/core-processor/src/main/java/io/micronaut/inject/processing/FactoryBeanElementCreator.java +++ b/core-processor/src/main/java/io/micronaut/inject/processing/FactoryBeanElementCreator.java @@ -82,17 +82,26 @@ protected boolean visitField(BeanDefinitionVisitor visitor, FieldElement fieldEl } @Override - protected boolean visitPropertyReadElement(BeanDefinitionVisitor visitor, PropertyElement propertyElement, MethodElement readElement) { + protected boolean visitPropertyReadElement(BeanDefinitionVisitor visitor, PropertyElement propertyElement, MemberElement readElement) { if (readElement.hasDeclaredStereotype(Bean.class.getName())) { - visitBeanFactoryElement(visitor, readElement.getGenericReturnType(), readElement); + ClassElement beanType; + if (readElement instanceof MethodElement methodElement) { + beanType = methodElement.getGenericReturnType(); + } else if (readElement instanceof FieldElement fieldElement) { + beanType = fieldElement.getGenericType(); + } else { + throw new IllegalStateException(); + } + visitBeanFactoryElement(visitor, beanType, readElement); return true; } return super.visitPropertyReadElement(visitor, propertyElement, readElement); } @Override - protected boolean visitPropertyWriteElement(BeanDefinitionVisitor visitor, PropertyElement propertyElement, MethodElement writeElement) { + protected boolean visitPropertyWriteElement(BeanDefinitionVisitor visitor, PropertyElement propertyElement, MemberElement writeElement) { if (writeElement.hasDeclaredStereotype(Bean.class.getName())) { + // Ignore bean producer accessor return true; } return super.visitPropertyWriteElement(visitor, propertyElement, writeElement); diff --git a/inject-java-test/src/main/groovy/io/micronaut/annotation/processing/test/JavaFileObjectClassLoader.java b/inject-java-test/src/main/groovy/io/micronaut/annotation/processing/test/JavaFileObjectClassLoader.java index 59492ea2978..0aabe0bde83 100644 --- a/inject-java-test/src/main/groovy/io/micronaut/annotation/processing/test/JavaFileObjectClassLoader.java +++ b/inject-java-test/src/main/groovy/io/micronaut/annotation/processing/test/JavaFileObjectClassLoader.java @@ -15,8 +15,12 @@ */ package io.micronaut.annotation.processing.test; +import org.codehaus.groovy.runtime.IOGroovyMethods; + +import javax.tools.JavaFileObject; import java.io.IOException; import java.io.InputStream; +import java.net.MalformedURLException; import java.net.URL; import java.net.URLConnection; import java.net.URLStreamHandler; @@ -24,10 +28,8 @@ import java.util.Collection; import java.util.Collections; import java.util.Enumeration; - -import javax.tools.JavaFileObject; - -import org.codehaus.groovy.runtime.IOGroovyMethods; +import java.util.List; +import java.util.stream.Collectors; /** * A custom classloader that loads from JavaFileObject instances. @@ -62,28 +64,32 @@ protected Class findClass(String name) throws ClassNotFoundException { @Override protected Enumeration findResources(String name) throws IOException { String fileName = "/CLASS_OUTPUT/" + name; - JavaFileObject generated = files.stream() - .filter((JavaFileObject it) -> it.getName().equals(fileName)) - .findFirst().orElse(null); - if (generated == null) { + List generated = files.stream() + .filter((JavaFileObject it) -> it.getName().startsWith(fileName)) + .toList(); + if (generated.isEmpty()) { return super.findResources(name); - } else { - URL url = new URL(null, generated.toUri().toString(), new URLStreamHandler() { - @Override - protected URLConnection openConnection(URL u) { - return new URLConnection(u) { - @Override - public void connect() { - } - - @Override - public InputStream getInputStream() throws IOException { - return generated.openInputStream(); - } - }; - } - }); - return Collections.enumeration(Collections.singletonList(url)); } + return Collections.enumeration(generated.stream().map(javaFileObject -> { + try { + return new URL(null, javaFileObject.toUri().toString(), new URLStreamHandler() { + @Override + protected URLConnection openConnection(URL u) { + return new URLConnection(u) { + @Override + public void connect() { + } + + @Override + public InputStream getInputStream() throws IOException { + return javaFileObject.openInputStream(); + } + }; + } + }); + } catch (MalformedURLException e) { + throw new RuntimeException(e); + } + }).collect(Collectors.toList())); } } diff --git a/inject-java/src/test/groovy/io/micronaut/inject/records/RecordBeansSpec.groovy b/inject-java/src/test/groovy/io/micronaut/inject/records/RecordBeansSpec.groovy index 8268c5a214e..6a5c2bb6df3 100644 --- a/inject-java/src/test/groovy/io/micronaut/inject/records/RecordBeansSpec.groovy +++ b/inject-java/src/test/groovy/io/micronaut/inject/records/RecordBeansSpec.groovy @@ -1,52 +1,39 @@ package io.micronaut.inject.records +import io.micronaut.annotation.processing.test.AbstractTypeElementSpec import io.micronaut.context.ApplicationContext import io.micronaut.context.env.PropertySource import io.micronaut.context.exceptions.DependencyInjectionException -import io.micronaut.annotation.processing.test.AbstractTypeElementSpec +import io.micronaut.core.reflect.ClassUtils import io.micronaut.inject.BeanDefinition +import io.micronaut.inject.ValidatedBeanDefinition import io.micronaut.inject.validation.BeanDefinitionValidator -import io.micronaut.validation.validator.DefaultValidator import io.micronaut.validation.validator.Validator -import spock.lang.IgnoreIf -@IgnoreIf({ !jvm.isJava14Compatible() }) class RecordBeansSpec extends AbstractTypeElementSpec { void 'test configuration properties as record'() { given: - ApplicationContext context = buildContext('test.Test', ''' -package test; -import io.micronaut.context.annotation.*; -import io.micronaut.core.convert.ConversionService; -import javax.validation.constraints.Min; -import jakarta.inject.Inject; -import io.micronaut.context.BeanContext; - -@ConfigurationProperties("foo") -record Test( - @Min(20) int num, - String name, - @Inject ConversionService conversionService, - @Inject BeanContext beanContext) { -} -''') - def type = context.classLoader.loadClass('test.Test') - BeanDefinition definition = context.getBeanDefinition( - type - ) + ApplicationContext context = ApplicationContext.run(['spec.name': getClass().getSimpleName()]) + BeanDefinition definition = context.getBeanDefinition(Test) when: context.registerSingleton(BeanDefinitionValidator, Validator.getInstance()) context.environment.addPropertySource(PropertySource.of("test", ['foo.num': 10, 'foo.name':'test'])) - context.getBean(type) + context.getBean(Test) then: + definition instanceof ValidatedBeanDefinition + ClassUtils.isPresent('io.micronaut.inject.records.$Test$Introspection', context.getClassLoader()) + ClassUtils.isPresent('io.micronaut.inject.records.$Test$Definition', context.getClassLoader()) + !ClassUtils.isPresent('io.micronaut.inject.records.$Test$Definition$Intercepted', context.getClassLoader()) + + and: def e = thrown(DependencyInjectionException) e.cause.message.contains('must be greater than or equal to 20') when: context.environment.addPropertySource(PropertySource.of("test", ['foo.num': 25, 'foo.name':'test'])) - def bean = context.getBean(type) + def bean = context.getBean(Test) then: definition.constructor.arguments.length == 4 @@ -58,6 +45,23 @@ record Test( context.close() } + void 'test record bean with nullable annotations'() { + given: + ApplicationContext context = ApplicationContext.run(['spec.name': getClass().getSimpleName()]) + + when: + BeanDefinition definition = context.getBeanDefinition(Test2) + + then: + !(definition instanceof ValidatedBeanDefinition) + !ClassUtils.isPresent('io.micronaut.inject.records.$Test2$Introspection', context.getClassLoader()) + ClassUtils.isPresent('io.micronaut.inject.records.$Test2$Definition', context.getClassLoader()) + !ClassUtils.isPresent('io.micronaut.inject.records.$Test2$Definition$Intercepted', context.getClassLoader()) + + cleanup: + context.close() + } + void 'test bean that is a record'() { given: BeanDefinition definition = buildBeanDefinition('test.Test', ''' diff --git a/inject-java/src/test/groovy/io/micronaut/inject/records/Test.java b/inject-java/src/test/groovy/io/micronaut/inject/records/Test.java new file mode 100644 index 00000000000..0ef953567fd --- /dev/null +++ b/inject-java/src/test/groovy/io/micronaut/inject/records/Test.java @@ -0,0 +1,18 @@ +package io.micronaut.inject.records; + +import io.micronaut.context.BeanContext; +import io.micronaut.context.annotation.ConfigurationProperties; +import io.micronaut.context.annotation.Requires; +import io.micronaut.core.convert.ConversionService; +import jakarta.inject.Inject; + +import javax.validation.constraints.Min; + +@Requires(property = "spec.name", value = "RecordBeansSpec") +@ConfigurationProperties("foo") +record Test( + @Min(20) int num, + String name, + @Inject ConversionService conversionService, + @Inject BeanContext beanContext) { +} diff --git a/inject-java/src/test/groovy/io/micronaut/inject/records/Test2.java b/inject-java/src/test/groovy/io/micronaut/inject/records/Test2.java new file mode 100644 index 00000000000..cfa19a06282 --- /dev/null +++ b/inject-java/src/test/groovy/io/micronaut/inject/records/Test2.java @@ -0,0 +1,15 @@ +package io.micronaut.inject.records; + +import io.micronaut.context.BeanContext; +import io.micronaut.context.annotation.Requires; +import io.micronaut.core.annotation.NonNull; +import io.micronaut.core.convert.ConversionService; +import jakarta.inject.Inject; + +import javax.validation.constraints.NotNull; + +@Requires(property = "spec.name", value = "RecordBeansSpec") +record Test2( + @Inject @NonNull @NotNull ConversionService conversionService, + @Inject @NonNull @NotNull BeanContext beanContext) { +} diff --git a/validation/src/main/java/io/micronaut/validation/validator/DefaultValidator.java b/validation/src/main/java/io/micronaut/validation/validator/DefaultValidator.java index 8aa28ff4fa7..dc54e9a0f5f 100644 --- a/validation/src/main/java/io/micronaut/validation/validator/DefaultValidator.java +++ b/validation/src/main/java/io/micronaut/validation/validator/DefaultValidator.java @@ -47,6 +47,7 @@ import io.micronaut.inject.ExecutableMethod; import io.micronaut.inject.InjectionPoint; import io.micronaut.inject.MethodReference; +import io.micronaut.inject.ProxyBeanDefinition; import io.micronaut.inject.annotation.AnnotatedElementValidator; import io.micronaut.inject.validation.BeanDefinitionValidator; import io.micronaut.validation.validator.constraints.ConstraintValidator; @@ -81,7 +82,19 @@ import java.lang.annotation.ElementType; import java.lang.reflect.Constructor; import java.lang.reflect.Method; -import java.util.*; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Deque; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; import java.util.concurrent.CompletionStage; import java.util.function.Function; import java.util.stream.Collectors; @@ -1726,17 +1739,21 @@ public void keyedValue(String nodeName, Object key, Object keyedValue) { @Override public void validateBean(@NonNull BeanResolutionContext resolutionContext, @NonNull BeanDefinition definition, @NonNull T bean) throws BeanInstantiationException { - final BeanIntrospection introspection = (BeanIntrospection) getBeanIntrospection(bean); + Class beanType; + if (definition instanceof ProxyBeanDefinition proxyBeanDefinition) { + beanType = (Class) proxyBeanDefinition.getTargetType(); + } else { + beanType = definition.getBeanType(); + } + final BeanIntrospection introspection = (BeanIntrospection) getBeanIntrospection(bean, beanType); if (introspection != null) { Set> errors = validate(introspection, bean); - final Class beanType = bean.getClass(); failOnError(resolutionContext, errors, beanType); } else if (bean instanceof Intercepted && definition.hasStereotype(ConfigurationReader.class)) { final Collection> executableMethods = definition.getExecutableMethods(); if (CollectionUtils.isNotEmpty(executableMethods)) { Set> errors = new HashSet<>(); final DefaultConstraintValidatorContext context = new DefaultConstraintValidatorContext(bean); - final Class beanType = definition.getBeanType(); final Class[] interfaces = beanType.getInterfaces(); if (ArrayUtils.isNotEmpty(interfaces)) { context.addConstructorNode(interfaces[0].getSimpleName()); @@ -1766,6 +1783,8 @@ public void validateBean(@NonNull BeanResolutionContext resolutionContext, @ failOnError(resolutionContext, errors, beanType); } + } else { + throw new BeanInstantiationException(resolutionContext, "Cannot validate bean [" + beanType.getName() + "]. No bean introspection present. Please add @Introspected."); } } diff --git a/validation/src/test/groovy/io/micronaut/validation/validator/ValidatorSpec.groovy b/validation/src/test/groovy/io/micronaut/validation/validator/ValidatorSpec.groovy index a75c8219c7a..83f1d9d31d1 100644 --- a/validation/src/test/groovy/io/micronaut/validation/validator/ValidatorSpec.groovy +++ b/validation/src/test/groovy/io/micronaut/validation/validator/ValidatorSpec.groovy @@ -3,11 +3,14 @@ package io.micronaut.validation.validator import io.micronaut.context.ApplicationContext import io.micronaut.context.annotation.Executable import io.micronaut.context.annotation.Prototype +import io.micronaut.context.annotation.Value +import io.micronaut.context.exceptions.BeanInstantiationException import io.micronaut.core.annotation.Introspected +import io.micronaut.core.reflect.ClassUtils import io.micronaut.validation.validator.resolver.CompositeTraversableResolver import jakarta.inject.Singleton import spock.lang.AutoCleanup -import spock.lang.Ignore +import spock.lang.PendingFeature import spock.lang.Shared import spock.lang.Specification @@ -15,14 +18,18 @@ import javax.validation.ConstraintViolationException import javax.validation.ElementKind import javax.validation.Valid import javax.validation.ValidatorFactory -import javax.validation.constraints.* +import javax.validation.constraints.Max +import javax.validation.constraints.Min +import javax.validation.constraints.NotBlank +import javax.validation.constraints.NotNull +import javax.validation.constraints.Size import javax.validation.metadata.BeanDescriptor class ValidatorSpec extends Specification { @Shared @AutoCleanup - ApplicationContext applicationContext = ApplicationContext.run() + ApplicationContext applicationContext = ApplicationContext.run(["a.number": 40]) @Shared Validator validator = applicationContext.getBean(Validator) @@ -68,7 +75,6 @@ class ValidatorSpec extends Specification { } - void "test validate bean property"() { given: Book b = new Book(title: "", pages: 50) @@ -438,7 +444,7 @@ class ValidatorSpec extends Specification { "Please add @Introspected to the class and ensure Micronaut annotation processing is enabled" } - @Ignore("FIXME: https://github.com/micronaut-projects/micronaut-core/issues/4410") + @PendingFeature(reason = "FIXME: https://github.com/micronaut-projects/micronaut-core/issues/4410") void "test element validation in String collection" () { when: ListOfStrings strings = new ListOfStrings(strings: ["", null, "a string that's too long"]) @@ -482,6 +488,50 @@ class ValidatorSpec extends Specification { constraintViolations[0].toString() == 'DefaultConstraintViolation{rootBean=class io.micronaut.validation.validator.$BookService$Definition$Intercepted, invalidValue=50, path=saveBook.pages}' constraintViolations[1].toString() == 'DefaultConstraintViolation{rootBean=class io.micronaut.validation.validator.$BookService$Definition$Intercepted, invalidValue=, path=saveBook.title}' } + + void "test @Introspected is required to validate the bean"() { + when: + applicationContext.getBean(A) + then: + BeanInstantiationException e = thrown() + e.message.contains('''Cannot validate bean [io.micronaut.validation.validator.A]. No bean introspection present. Please add @Introspected.''') + and: + ClassUtils.forName('io.micronaut.validation.validator.$A$Definition', getClass().getClassLoader()).isPresent() + ClassUtils.forName('io.micronaut.validation.validator.$A$Definition$Intercepted', getClass().getClassLoader()).isEmpty() + } + + void "test @Introspected is required to validate the bean and it's intercepted if one of the methods requires validation"() { + when: + def beanB = applicationContext.getBean(B) + then: + BeanInstantiationException e = thrown() + e.message.contains('''number - must be less than or equal to 20''') + and: + ClassUtils.forName('io.micronaut.validation.validator.$B$Definition', getClass().getClassLoader()).isPresent() + ClassUtils.forName('io.micronaut.validation.validator.$B$Definition$Intercepted', getClass().getClassLoader()).isPresent() + } + + void "test @Introspected is required to validate the bean and it's intercepted if one of the methods requires validation 2"() { + when: + def beanC = applicationContext.getBean(C) + then: + beanC.number == 40 + when: + beanC.updateNumber(100) + then: + Exception e = thrown() + e.message.contains('''updateNumber.number: must be less than or equal to 50''') + beanC.number == 40 +// when: +// beanC.number = 100 +// then: +// e = thrown() +// e.message.contains('''updateNumber.number: must be less than or equal to 50''') +// beanC.number == 40 + and: + ClassUtils.forName('io.micronaut.validation.validator.$C$Definition', getClass().getClassLoader()).isPresent() + ClassUtils.forName('io.micronaut.validation.validator.$C$Definition$Intercepted', getClass().getClassLoader()).isPresent() + } } @Introspected @@ -576,6 +626,50 @@ class ArrayTest { } } +@Singleton +class A { + + @Valid + @Max(20l) + @NotNull + @Value('${a.number}') + Integer number +} + +@Introspected +@Singleton +class B { + + @Valid + @Max(20l) + @NotNull + @Value('${a.number}') + Integer number + + void updateNumber(@Max(20l) + @NotNull + Integer number) { + this.number = number + } +} + +@Introspected +@Singleton +class C { + + @Valid + @Max(50l) + @NotNull + @Value('${a.number}') + Integer number + + void updateNumber(@Max(50l) + @NotNull + Integer number) { + this.number = number + } +} + @Singleton class BookService { @Executable From b9f318cbaf2a5db855712b7e64da12fb47f9c9f7 Mon Sep 17 00:00:00 2001 From: Graeme Rocher Date: Wed, 30 Nov 2022 16:54:56 +0100 Subject: [PATCH 279/743] Support arbitrary nesting of EachProperty/ConfigurationProperties beans (#8424) This change adds a new ConfigurationPath interface and implementation that is used to keep track of the state of configuration nesting and to allow nesting of @EachProperty declarations. For nested @EachProperty declarations a composite qualifier is formed from all the names in the path. For example given a pattern foo..bar..baz and the property foo.1st.bar.2nd.baz the resulting qualifier will be "1st-2nd". --- .../aop/internal/InterceptorRegistryBean.java | 5 + .../context/env/ConfigurationAdvice.java | 1 - .../env/ConfigurationIntroductionAdvice.java | 44 +- .../visitor/ConfigurationReaderVisitor.java | 22 +- .../io/micronaut/inject/ast/ClassElement.java | 10 + .../DeclaredBeanElementCreator.java | 7 +- .../writer/BeanDefinitionReferenceWriter.java | 2 +- .../inject/writer/BeanDefinitionWriter.java | 10 +- .../micronaut/core/type/DefaultArgument.java | 18 +- .../micronaut/core/type/TypeInformation.java | 2 +- .../core/value/MapPropertyResolver.java | 8 +- .../core/value/PropertyResolver.java | 10 + .../NettyHttpServerConfiguration.java | 3 + ...eanDefinitionAnnotationMetadataSpec.groovy | 1 - ...mmutableConfigurationPropertiesSpec.groovy | 48 +- ...figurationPropertiesInheritanceSpec.groovy | 2 +- .../nesting/EachPropertyNestingSpec.groovy | 149 ++++++ inject/build.gradle | 2 + .../context/AbstractBeanDefinition.java | 18 +- .../AbstractBeanResolutionContext.java | 37 +- .../AbstractInitializableBeanDefinition.java | 205 ++++---- .../context/BeanDefinitionDelegate.java | 240 ++++----- .../context/BeanResolutionContext.java | 16 + .../context/DefaultApplicationContext.java | 382 ++++++-------- .../micronaut/context/DefaultBeanContext.java | 127 +++-- .../context/DefaultConditionContext.java | 8 + ...efaultMethodConstructorInjectionPoint.java | 5 + .../context/DefaultMethodInjectionPoint.java | 2 +- .../io/micronaut/context/DisabledBean.java | 5 + .../context/RuntimeBeanDefinition.java | 5 + .../io/micronaut/context/SingletonScope.java | 5 +- .../context/env/ConfigurationPath.java | 322 ++++++++++++ .../context/env/DefaultConfigurationPath.java | 481 ++++++++++++++++++ .../env/PropertySourcePropertyResolver.java | 66 ++- .../ApplicationEventPublisherFactory.java | 5 + .../context/exceptions/MessageUtils.java | 7 +- .../io/micronaut/inject/BeanDefinition.java | 25 +- .../java/io/micronaut/inject/BeanType.java | 22 +- .../inject/DelegatingBeanDefinition.java | 5 - .../micronaut/inject/QualifiedBeanType.java | 2 + .../provider/AbstractProviderDefinition.java | 7 +- .../inject/qualifiers/Qualifiers.java | 30 ++ .../context/ConfigurationPathSpec.groovy | 154 ++++++ .../PropertySourcePropertyResolverSpec.groovy | 260 +++++----- 44 files changed, 2050 insertions(+), 735 deletions(-) create mode 100644 inject-java/src/test/groovy/io/micronaut/inject/configproperties/nesting/EachPropertyNestingSpec.groovy create mode 100644 inject/src/main/java/io/micronaut/context/env/ConfigurationPath.java create mode 100644 inject/src/main/java/io/micronaut/context/env/DefaultConfigurationPath.java create mode 100644 inject/src/test/groovy/io/micronaut/context/ConfigurationPathSpec.groovy diff --git a/aop/src/main/java/io/micronaut/aop/internal/InterceptorRegistryBean.java b/aop/src/main/java/io/micronaut/aop/internal/InterceptorRegistryBean.java index 958cd5c7711..11ccf61529e 100644 --- a/aop/src/main/java/io/micronaut/aop/internal/InterceptorRegistryBean.java +++ b/aop/src/main/java/io/micronaut/aop/internal/InterceptorRegistryBean.java @@ -76,6 +76,11 @@ public boolean isSingleton() { return true; } + @Override + public boolean isConfigurationProperties() { + return false; + } + @Override public boolean isAbstract() { return false; diff --git a/context/src/main/java/io/micronaut/runtime/context/env/ConfigurationAdvice.java b/context/src/main/java/io/micronaut/runtime/context/env/ConfigurationAdvice.java index 0ca6b3d6a77..b411d0f291f 100644 --- a/context/src/main/java/io/micronaut/runtime/context/env/ConfigurationAdvice.java +++ b/context/src/main/java/io/micronaut/runtime/context/env/ConfigurationAdvice.java @@ -16,7 +16,6 @@ package io.micronaut.runtime.context.env; import io.micronaut.aop.Introduction; -import io.micronaut.context.annotation.Type; import io.micronaut.core.annotation.Internal; import java.lang.annotation.Retention; diff --git a/context/src/main/java/io/micronaut/runtime/context/env/ConfigurationIntroductionAdvice.java b/context/src/main/java/io/micronaut/runtime/context/env/ConfigurationIntroductionAdvice.java index f0df0e83137..ebc81f15ac3 100644 --- a/context/src/main/java/io/micronaut/runtime/context/env/ConfigurationIntroductionAdvice.java +++ b/context/src/main/java/io/micronaut/runtime/context/env/ConfigurationIntroductionAdvice.java @@ -19,19 +19,22 @@ import io.micronaut.aop.MethodInterceptor; import io.micronaut.aop.MethodInvocationContext; import io.micronaut.context.BeanContext; +import io.micronaut.context.BeanResolutionContext; +import io.micronaut.context.DefaultBeanContext; +import io.micronaut.context.DefaultBeanResolutionContext; import io.micronaut.context.Qualifier; import io.micronaut.context.annotation.BootstrapContextCompatible; import io.micronaut.context.annotation.Property; import io.micronaut.context.annotation.Prototype; +import io.micronaut.context.env.ConfigurationPath; import io.micronaut.context.env.Environment; import io.micronaut.core.annotation.Internal; import io.micronaut.core.annotation.Nullable; import io.micronaut.core.bind.annotation.Bindable; -import io.micronaut.core.naming.Named; import io.micronaut.core.type.Argument; import io.micronaut.core.type.ReturnType; import io.micronaut.core.value.PropertyNotFoundException; -import io.micronaut.inject.qualifiers.Qualifiers; +import io.micronaut.inject.BeanDefinition; import java.util.Collections; import java.util.Optional; @@ -54,19 +57,24 @@ public class ConfigurationIntroductionAdvice implements MethodInterceptor beanDefinition; /** * Default constructor. * - * @param qualifier The qualifier - * @param environment The environment - * @param beanContext The bean locator + * @param resolutionContext The resolution context + * @param environment The environment + * @param beanContext The bean locator */ - ConfigurationIntroductionAdvice(Qualifier qualifier, Environment environment, BeanContext beanContext) { + ConfigurationIntroductionAdvice( + BeanResolutionContext resolutionContext, + Environment environment, + BeanContext beanContext) { + this.beanDefinition = resolutionContext.getRootDefinition(); this.environment = environment; this.beanContext = beanContext; - this.name = qualifier instanceof Named named ? named.getName() : null; + this.configurationPath = resolutionContext.getConfigurationPath().copy(); } @Nullable @@ -87,14 +95,13 @@ private Object resolveProperty(MethodInvocationContext context, if (property == null) { throw new IllegalStateException("No property name available to resolve"); } - boolean iterable = property.indexOf('*') > -1; - if (iterable && name != null) { - property = property.replace("*", name); + if (configurationPath.hasDynamicSegments()) { + property = configurationPath.resolveValue(property); } final String defaultValue = context.stringValue(Bindable.class, "defaultValue").orElse(null); final Optional value = environment.getProperty( - property, + property, argument ); @@ -114,7 +121,7 @@ private Object resolveProperty(MethodInvocationContext context, } private Object resolveBean(MethodInvocationContext context, Class returnType, Argument argument) { - final Qualifier qualifier = name != null ? Qualifiers.byName(name) : null; + final Qualifier qualifier = configurationPath.beanQualifier(); if (Iterable.class.isAssignableFrom(returnType)) { @SuppressWarnings("unchecked") Argument firstArg = (Argument) argument.getFirstTypeVariable().orElse(null); @@ -131,10 +138,13 @@ private Object resolveBean(MethodInvocationContext context, Clas return v; } } else { - return environment.convertRequired( - beanContext.getBean(argument, qualifier), - returnType - ); + try (BeanResolutionContext rc = new DefaultBeanResolutionContext(beanContext, beanDefinition)) { + rc.setConfigurationPath(configurationPath); + return environment.convertRequired( + ((DefaultBeanContext) beanContext).getBean(rc, argument, qualifier), + returnType + ); + } } } } diff --git a/core-processor/src/main/java/io/micronaut/context/visitor/ConfigurationReaderVisitor.java b/core-processor/src/main/java/io/micronaut/context/visitor/ConfigurationReaderVisitor.java index 6adc6bc9294..29164f17f9e 100644 --- a/core-processor/src/main/java/io/micronaut/context/visitor/ConfigurationReaderVisitor.java +++ b/core-processor/src/main/java/io/micronaut/context/visitor/ConfigurationReaderVisitor.java @@ -15,7 +15,6 @@ */ package io.micronaut.context.visitor; -import io.micronaut.context.BeanProvider; import io.micronaut.context.annotation.Bean; import io.micronaut.context.annotation.ConfigurationReader; import io.micronaut.context.annotation.EachProperty; @@ -28,6 +27,7 @@ import io.micronaut.core.annotation.NonNull; import io.micronaut.core.bind.annotation.Bindable; import io.micronaut.core.naming.NameUtils; +import io.micronaut.core.type.DefaultArgument; import io.micronaut.inject.ast.ParameterElement; import io.micronaut.inject.validation.RequiresValidation; import io.micronaut.inject.ast.ClassElement; @@ -36,7 +36,6 @@ import io.micronaut.inject.configuration.ConfigurationMetadataBuilder; import io.micronaut.inject.visitor.TypeElementVisitor; import io.micronaut.inject.visitor.VisitorContext; -import jakarta.inject.Provider; import java.util.Map; @@ -75,7 +74,7 @@ public void visitClass(ClassElement classElement, VisitorContext context) { ConfigurationMetadata configurationMetadata = metadataBuilder.visitProperties(classElement); if (configurationMetadata != null) { - classElement.annotate(ConfigurationReader.class, (builder) -> builder.member(ConfigurationReader.PREFIX, configurationMetadata.getName())); + classElement.annotate(ConfigurationReader.class, builder -> builder.member(ConfigurationReader.PREFIX, configurationMetadata.getName())); } if (classElement.isInterface()) { @@ -107,7 +106,7 @@ public static boolean isPropertyParameter(ParameterElement parameter, VisitorCon } private static boolean isPropertyParameter(ClassElement genericType, VisitorContext visitorContext) { - if (genericType.isOptional() || genericType.isAssignable(BeanProvider.class) || genericType.isAssignable(Provider.class) || genericType.isAssignable(Iterable.class)) { + if (genericType.isOptional() || genericType.isContainerType() || isProvider(genericType)) { ClassElement finalParameterType = genericType; genericType = genericType.getOptionalValueType().or(finalParameterType::getFirstTypeArgument).orElse(genericType); // Get the class with type annotations @@ -121,6 +120,16 @@ private static boolean isPropertyParameter(ClassElement genericType, VisitorCont return !genericType.hasStereotype(AnnotationUtil.SCOPE) && !genericType.hasStereotype(Bean.class); } + private static boolean isProvider(ClassElement genericType) { + String name = genericType.getName(); + for (String type : DefaultArgument.PROVIDER_TYPES) { + if (name.equals(type)) { + return true; + } + } + return false; + } + private void visitAbstractMethod(MethodElement method, VisitorContext context) { String methodName = method.getName(); if (!isGetter(methodName)) { @@ -162,11 +171,6 @@ private void visitAbstractMethod(MethodElement method, VisitorContext context) { }); } - private static boolean isBeanReturnType(MethodElement method) { - ClassElement returnType = method.getGenericReturnType(); - return !returnType.isPrimitive() && returnType.hasStereotype(AnnotationUtil.SCOPE); - } - private String getPropertyNameForGetter(String methodName) { return NameUtils.getPropertyNameForGetter(methodName, readPrefixes); } diff --git a/core-processor/src/main/java/io/micronaut/inject/ast/ClassElement.java b/core-processor/src/main/java/io/micronaut/inject/ast/ClassElement.java index 9f7cb3ac649..f68018762c5 100644 --- a/core-processor/src/main/java/io/micronaut/inject/ast/ClassElement.java +++ b/core-processor/src/main/java/io/micronaut/inject/ast/ClassElement.java @@ -23,6 +23,7 @@ import io.micronaut.core.annotation.NonNull; import io.micronaut.core.annotation.Nullable; import io.micronaut.core.naming.NameUtils; +import io.micronaut.core.type.DefaultArgument; import io.micronaut.core.util.ArgumentUtils; import io.micronaut.inject.ast.beans.BeanElementBuilder; @@ -121,6 +122,15 @@ default boolean isOptional() { return isAssignable(Optional.class) || isAssignable(OptionalLong.class) || isAssignable(OptionalDouble.class) || isAssignable(OptionalInt.class); } + /** + * Checks whether the bean type is a container type. + * @return Whether the type is a container type like {@link Iterable}. + * @since 4.0.0 + */ + default boolean isContainerType() { + return DefaultArgument.CONTAINER_TYPES.contains(getName()); + } + /** * Gets optional value type. * diff --git a/core-processor/src/main/java/io/micronaut/inject/processing/DeclaredBeanElementCreator.java b/core-processor/src/main/java/io/micronaut/inject/processing/DeclaredBeanElementCreator.java index 9ac34abb434..d6fed0553ba 100644 --- a/core-processor/src/main/java/io/micronaut/inject/processing/DeclaredBeanElementCreator.java +++ b/core-processor/src/main/java/io/micronaut/inject/processing/DeclaredBeanElementCreator.java @@ -367,7 +367,12 @@ private boolean visitInjectAndLifecycleMethod(BeanDefinitionVisitor visitor, Met return false; } - private void visitMethodInjectionPoint(BeanDefinitionVisitor visitor, MethodElement methodElement) { + /** + * Visit a method injection point. + * @param visitor The visitor + * @param methodElement The method element + */ + protected void visitMethodInjectionPoint(BeanDefinitionVisitor visitor, MethodElement methodElement) { applyConfigurationInjectionIfNecessary(visitor, methodElement); visitor.visitMethodInjectionPoint( methodElement.getDeclaringType(), diff --git a/core-processor/src/main/java/io/micronaut/inject/writer/BeanDefinitionReferenceWriter.java b/core-processor/src/main/java/io/micronaut/inject/writer/BeanDefinitionReferenceWriter.java index 83ccfa572f0..f2e8e28fdbc 100644 --- a/core-processor/src/main/java/io/micronaut/inject/writer/BeanDefinitionReferenceWriter.java +++ b/core-processor/src/main/java/io/micronaut/inject/writer/BeanDefinitionReferenceWriter.java @@ -197,7 +197,7 @@ private ClassWriter generateClassBytes() { // 6: isConditional cv.push(annotationMetadata.hasStereotype(Requires.class)); // 7: isContainerType - cv.push(providedType.getSort() == Type.ARRAY || DefaultArgument.CONTAINER_TYPES.stream().anyMatch(clazz -> clazz.getName().equals(beanTypeName))); + cv.push(providedType.getSort() == Type.ARRAY || DefaultArgument.CONTAINER_TYPES.stream().anyMatch(clazz -> clazz.equals(beanTypeName))); // 8: isSingleton cv.push( annotationMetadata.hasDeclaredStereotype(AnnotationUtil.SINGLETON) || diff --git a/core-processor/src/main/java/io/micronaut/inject/writer/BeanDefinitionWriter.java b/core-processor/src/main/java/io/micronaut/inject/writer/BeanDefinitionWriter.java index 5ee28c105e8..78a3662c325 100644 --- a/core-processor/src/main/java/io/micronaut/inject/writer/BeanDefinitionWriter.java +++ b/core-processor/src/main/java/io/micronaut/inject/writer/BeanDefinitionWriter.java @@ -40,6 +40,7 @@ import io.micronaut.context.annotation.Provided; import io.micronaut.context.annotation.Requires; import io.micronaut.context.annotation.Value; +import io.micronaut.context.env.ConfigurationPath; import io.micronaut.core.annotation.AccessorsStyle; import io.micronaut.core.annotation.AnnotationMetadata; import io.micronaut.core.annotation.AnnotationMetadataProvider; @@ -1791,7 +1792,7 @@ private static boolean isInjectableMap(ClassElement genericType) { private boolean isInnerType(ClassElement genericType) { String type; - if (genericType.isAssignable(Collection.class)) { + if (genericType.isContainerType()) { type = genericType.getFirstTypeArgument().map(Element::getName).orElse(""); } else if (genericType.isArray()) { type = genericType.fromArray().getName(); @@ -3628,6 +3629,11 @@ private void pushConstructorArgument(GeneratorAdapter buildMethodVisitor, buildMethodVisitor.loadArg(1); } else if (argumentType.getGenericType().isAssignable(BeanResolutionContext.class)) { buildMethodVisitor.loadArg(0); + } else if (argumentType.getGenericType().isAssignable(ConfigurationPath.class)) { + buildMethodVisitor.loadArg(0); + buildMethodVisitor.invokeInterface(Type.getType(BeanResolutionContext.class), org.objectweb.asm.commons.Method.getMethod( + ReflectionUtils.getRequiredInternalMethod(BeanResolutionContext.class, "getConfigurationPath") + )); } else { boolean hasGenericType = false; boolean isArray = false; @@ -4087,7 +4093,7 @@ private void visitBeanDefinitionConstructorInternal( } private boolean isContainerType() { - return beanTypeElement.isArray() || DefaultArgument.CONTAINER_TYPES.stream().map(Class::getName).anyMatch(c -> c.equals(beanFullClassName)); + return beanTypeElement.isArray() || DefaultArgument.CONTAINER_TYPES.stream().anyMatch(c -> c.equals(beanFullClassName)); } private boolean isConfigurationProperties(AnnotationMetadata annotationMetadata) { diff --git a/core/src/main/java/io/micronaut/core/type/DefaultArgument.java b/core/src/main/java/io/micronaut/core/type/DefaultArgument.java index c4dd3b70a26..60e41c4fc94 100644 --- a/core/src/main/java/io/micronaut/core/type/DefaultArgument.java +++ b/core/src/main/java/io/micronaut/core/type/DefaultArgument.java @@ -37,15 +37,15 @@ @Internal public class DefaultArgument implements Argument, ArgumentCoercible { - public static final Set> CONTAINER_TYPES = CollectionUtils.setOf( - List.class, - Set.class, - Collection.class, - Queue.class, - SortedSet.class, - Deque.class, - Vector.class, - ArrayList.class + public static final Set CONTAINER_TYPES = CollectionUtils.setOf( + List.class.getName(), + Set.class.getName(), + Collection.class.getName(), + Queue.class.getName(), + SortedSet.class.getName(), + Deque.class.getName(), + Vector.class.getName(), + ArrayList.class.getName() ); public static final Set PROVIDER_TYPES = CollectionUtils.setOf( "io.micronaut.context.BeanProvider", diff --git a/core/src/main/java/io/micronaut/core/type/TypeInformation.java b/core/src/main/java/io/micronaut/core/type/TypeInformation.java index 2d731c407ff..e1fa86d7aa5 100644 --- a/core/src/main/java/io/micronaut/core/type/TypeInformation.java +++ b/core/src/main/java/io/micronaut/core/type/TypeInformation.java @@ -130,7 +130,7 @@ default boolean isAsyncOrReactive() { */ default boolean isContainerType() { final Class type = getType(); - return Map.class == type || DefaultArgument.CONTAINER_TYPES.contains(type); + return Map.class == type || DefaultArgument.CONTAINER_TYPES.contains(type.getName()); } /** diff --git a/core/src/main/java/io/micronaut/core/value/MapPropertyResolver.java b/core/src/main/java/io/micronaut/core/value/MapPropertyResolver.java index fe6b2d526f1..b6dc48a47ea 100644 --- a/core/src/main/java/io/micronaut/core/value/MapPropertyResolver.java +++ b/core/src/main/java/io/micronaut/core/value/MapPropertyResolver.java @@ -22,6 +22,7 @@ import java.util.Collection; import java.util.Collections; +import java.util.List; import java.util.Map; import java.util.Optional; import java.util.stream.Collectors; @@ -84,8 +85,13 @@ public Collection getPropertyEntries(@NonNull String name) { return withoutPrefix; }) // to list to retain order from linked hash map - .collect(Collectors.toList()); + .toList(); } return Collections.emptySet(); } + + @Override + public List> getPropertyPathMatches(String pathPattern) { + return Collections.emptyList(); + } } diff --git a/core/src/main/java/io/micronaut/core/value/PropertyResolver.java b/core/src/main/java/io/micronaut/core/value/PropertyResolver.java index eea92e7a9e2..f211f4496ce 100644 --- a/core/src/main/java/io/micronaut/core/value/PropertyResolver.java +++ b/core/src/main/java/io/micronaut/core/value/PropertyResolver.java @@ -25,6 +25,7 @@ import java.util.Collection; import java.util.Collections; +import java.util.List; import java.util.Map; import java.util.Optional; @@ -193,4 +194,13 @@ public interface PropertyResolver extends ValueResolver { static String nameOf(String... path) { return String.join(".", path); } + + /** + * Will return for a given pattern such as {@code foo.*.bar.*} and array of arrays containing the variable names that match the pattern. + * @param pathPattern The path pattern + * @return An array of arrays. + * @since 4.0.0 + */ + @NonNull + Collection> getPropertyPathMatches(@NonNull String pathPattern); } diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/configuration/NettyHttpServerConfiguration.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/configuration/NettyHttpServerConfiguration.java index 82c87e9b106..e40f8092c1b 100644 --- a/http-server-netty/src/main/java/io/micronaut/http/server/netty/configuration/NettyHttpServerConfiguration.java +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/configuration/NettyHttpServerConfiguration.java @@ -33,6 +33,7 @@ import io.netty.handler.logging.LogLevel; import io.netty.handler.ssl.ApplicationProtocolNames; import jakarta.inject.Inject; +import jakarta.inject.Named; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -793,6 +794,7 @@ public void setExclusions(List exclusions) { * Configuration for Netty worker. */ @ConfigurationProperties("worker") + @Named("netty-server-worker-event-loop") public static class Worker extends EventLoopConfig { /** * Default constructor. @@ -807,6 +809,7 @@ public static class Worker extends EventLoopConfig { */ @ConfigurationProperties(Parent.NAME) @Requires(missingProperty = EventLoopGroupConfiguration.EVENT_LOOPS + ".parent") + @Named("netty-server-parent-event-loop") public static class Parent extends EventLoopConfig { public static final String NAME = "parent"; diff --git a/inject-java/src/test/groovy/io/micronaut/inject/annotation/BeanDefinitionAnnotationMetadataSpec.groovy b/inject-java/src/test/groovy/io/micronaut/inject/annotation/BeanDefinitionAnnotationMetadataSpec.groovy index e7674d3032a..838512b0ee7 100644 --- a/inject-java/src/test/groovy/io/micronaut/inject/annotation/BeanDefinitionAnnotationMetadataSpec.groovy +++ b/inject-java/src/test/groovy/io/micronaut/inject/annotation/BeanDefinitionAnnotationMetadataSpec.groovy @@ -71,7 +71,6 @@ class Test { definition.isSingleton() !definition.isIterable() definition.isPrimary() - !definition.isProvided() definition.getScopeName().get() == AnnotationUtil.SINGLETON } diff --git a/inject-java/src/test/groovy/io/micronaut/inject/configproperties/ImmutableConfigurationPropertiesSpec.groovy b/inject-java/src/test/groovy/io/micronaut/inject/configproperties/ImmutableConfigurationPropertiesSpec.groovy index 0f122baa386..d5b848bd472 100644 --- a/inject-java/src/test/groovy/io/micronaut/inject/configproperties/ImmutableConfigurationPropertiesSpec.groovy +++ b/inject-java/src/test/groovy/io/micronaut/inject/configproperties/ImmutableConfigurationPropertiesSpec.groovy @@ -1,6 +1,7 @@ package io.micronaut.inject.configproperties import io.micronaut.context.ApplicationContext +import io.micronaut.context.ApplicationContextBuilder import io.micronaut.context.DefaultBeanResolutionContext import io.micronaut.context.annotation.Property import io.micronaut.core.naming.Named @@ -8,6 +9,7 @@ import io.micronaut.annotation.processing.test.AbstractTypeElementSpec import io.micronaut.inject.BeanDefinition import io.micronaut.inject.BeanFactory import io.micronaut.inject.ValidatedBeanDefinition +import io.micronaut.inject.qualifiers.Qualifiers class ImmutableConfigurationPropertiesSpec extends AbstractTypeElementSpec { @@ -47,17 +49,17 @@ import java.time.Duration; class MyConfig { private String host; private int serverPort; - + @ConfigurationInject MyConfig(@javax.validation.constraints.NotBlank String host, int serverPort) { this.host = host; this.serverPort = serverPort; } - + public String getHost() { return host; } - + public int getServerPort() { return serverPort; } @@ -99,21 +101,21 @@ import java.time.Duration; class MyConfig { private String host; private int serverPort; - + @ConfigurationInject MyConfig(String host, int serverPort) { this.host = host; this.serverPort = serverPort; } - + public String getHost() { return host; } - + public int getServerPort() { return serverPort; } - + @ConfigurationProperties("baz") static class ChildConfig { final String stuff; @@ -146,7 +148,7 @@ class MyConfig { void "test parse immutable configuration properties - each property"() { when: - BeanDefinition beanDefinition = buildBeanDefinition('test.MyConfig', ''' + ApplicationContext context = buildContext( ''' package test; import io.micronaut.context.annotation.*; @@ -156,23 +158,24 @@ import java.time.Duration; class MyConfig { private String host; private int serverPort; - + @ConfigurationInject MyConfig(String host, int serverPort) { this.host = host; this.serverPort = serverPort; } - + public String getHost() { return host; } - + public int getServerPort() { return serverPort; } } ''') + def beanDefinition = getBeanDefinition(context, 'test.MyConfig', Qualifiers.byName('one')) def arguments = beanDefinition.constructor.arguments then: arguments.length == 2 @@ -182,13 +185,7 @@ class MyConfig { .name() == 'foo.bar.*.server-port' when: - def context = ApplicationContext.run('foo.bar.one.host': 'test', 'foo.bar.one.server-port': '9999') - def resolutionContext = new DefaultBeanResolutionContext(context, beanDefinition) - resolutionContext.setAttribute( - Named.class.getName(), - "one" - ) - def config = ((BeanFactory) beanDefinition).build(resolutionContext,context, beanDefinition) + def config = getBean(context, 'test.MyConfig', Qualifiers.byName('one')) then: config.host == 'test' @@ -199,6 +196,11 @@ class MyConfig { } + @Override + protected void configureContext(ApplicationContextBuilder contextBuilder) { + contextBuilder.properties('foo.bar.one.host': 'test', 'foo.bar.one.server-port': '9999') + } + void "test parse immutable configuration properties - init method"() { when: @@ -212,21 +214,21 @@ import java.time.Duration; class MyConfig { private String host; private int serverPort; - - + + MyConfig() { } - + @ConfigurationInject void init(String host, int serverPort) { this.host = host; this.serverPort = serverPort; } - + public String getHost() { return host; } - + public int getServerPort() { return serverPort; } diff --git a/inject-java/src/test/groovy/io/micronaut/inject/configproperties/inheritance/ConfigurationPropertiesInheritanceSpec.groovy b/inject-java/src/test/groovy/io/micronaut/inject/configproperties/inheritance/ConfigurationPropertiesInheritanceSpec.groovy index 3eb5409b303..b460d7b1e8a 100644 --- a/inject-java/src/test/groovy/io/micronaut/inject/configproperties/inheritance/ConfigurationPropertiesInheritanceSpec.groovy +++ b/inject-java/src/test/groovy/io/micronaut/inject/configproperties/inheritance/ConfigurationPropertiesInheritanceSpec.groovy @@ -191,7 +191,7 @@ class ConfigurationPropertiesInheritanceSpec extends Specification { Collection managers = context.getBeansOfType(ParentArrayEachPropsCtor.ManagerProps) then: "The instance is the same" - managers.size() == 1 managers[0].is(teams[0].manager) + managers.size() == 1 } } diff --git a/inject-java/src/test/groovy/io/micronaut/inject/configproperties/nesting/EachPropertyNestingSpec.groovy b/inject-java/src/test/groovy/io/micronaut/inject/configproperties/nesting/EachPropertyNestingSpec.groovy new file mode 100644 index 00000000000..f73ce8bbec5 --- /dev/null +++ b/inject-java/src/test/groovy/io/micronaut/inject/configproperties/nesting/EachPropertyNestingSpec.groovy @@ -0,0 +1,149 @@ +package io.micronaut.inject.configproperties.nesting + +import io.micronaut.annotation.processing.test.AbstractTypeElementSpec +import io.micronaut.context.ApplicationContext +import io.micronaut.context.ApplicationContextBuilder +import io.micronaut.context.exceptions.DependencyInjectionException +import io.micronaut.context.exceptions.NoSuchBeanException +import io.micronaut.inject.qualifiers.Qualifiers +import spock.lang.Specification + +class EachPropertyNestingSpec extends AbstractTypeElementSpec { + + void "test nesting classes within each other"() { + given: + def context = buildContext(''' +package test; + +import io.micronaut.context.annotation.ConfigurationProperties; +import io.micronaut.context.annotation.EachProperty; +import io.micronaut.context.annotation.Parameter; + +import java.util.List; + +@EachProperty(value = "test", primary = "one") +class ClassOuterConfig { + + private final String name; + + private int age; + private ClassInnerConfig inner; + + public ClassOuterConfig(@Parameter String name) { + this.name = name; + } + + public String getName() { + return name; + } + + public int getAge() { + return age; + } + + public void setAge(int age) { + this.age = age; + } + + public void setInner(ClassInnerConfig inner) { + this.inner = inner; + } + + public ClassInnerConfig getInners() { + return inner; + } + + @ConfigurationProperties("inner") + public static class ClassInnerConfig { + + private String foo; + + private List inners; + + + public void setFoo(String foo) { + this.foo = foo; + } + + public String getFoo() { + return foo; + } + + public void setInners(List inners) { + this.inners = inners; + } + + public List getInners() { + return inners; + } + + @EachProperty(value = "inners") + public static class ClassInnerEachConfig { + + private final String name; + + private int count; + + public ClassInnerEachConfig(@Parameter String name) { + this.name = name; + } + + public String getName() { + return name; + } + + public void setCount(int count) { + this.count = count; + } + + public int getCount() { + return count; + } + } + } +} +''') + when: + def config = getBean(context, 'test.ClassOuterConfig') + + then: + config.getName() == 'one' + config.getAge() == 10 + config.inner.getFoo() == 'test2' + config.inner.inners.size() == 1 + config.inner.inners.get(0).getName() == "a" + config.inner.inners.get(0).getCount() == 20 + + when: + def config2 = getBean(context, 'test.ClassOuterConfig', Qualifiers.byName("two")) + + then: + config2.getName() == 'two' + config2.getAge() == 30 + config2.inner.getFoo() == 'test3' + !config2.inner.inners.isEmpty() + config2.inner.inners.size() == 2 + config2.inner.inners.find { it.name == '1st' }.count == 30 + config2.inner.inners.find { it.name == '2nd' }.count == 40 + + when:"A unresolvable bean is queried" + def inner = getBean(context, 'test.ClassOuterConfig$ClassInnerConfig$ClassInnerEachConfig', Qualifiers.byName("foo-bar")) + + then: + def e = thrown(NoSuchBeanException) + e.message == 'No bean of type [test.ClassOuterConfig$ClassInnerConfig$ClassInnerEachConfig] exists for the given qualifier: @Named(\'foo-bar\'). No configuration entries found under the prefix: [test.*.inner.inners.*]. Provide the necessary configuration to resolve this issue.' + } + + @Override + protected void configureContext(ApplicationContextBuilder contextBuilder) { + contextBuilder.properties( + 'test.one.age': '10', + 'test.one.inner.foo': 'test2', + 'test.one.inner.inners.a.count': '20', + 'test.two.inner.foo': 'test3', + 'test.two.age': '30', + 'test.two.inner.inners.1st.count': '30', + 'test.two.inner.inners.2nd.count': '40' + ) + } +} diff --git a/inject/build.gradle b/inject/build.gradle index 1245659e695..4de0ce71d31 100644 --- a/inject/build.gradle +++ b/inject/build.gradle @@ -37,3 +37,5 @@ tasks.withType(Test) { jvmArgs += ['--add-opens', 'java.base/java.util=ALL-UNNAMED'] } } + +checkstyleMain.enabled = false diff --git a/inject/src/main/java/io/micronaut/context/AbstractBeanDefinition.java b/inject/src/main/java/io/micronaut/context/AbstractBeanDefinition.java index 0b8966e7ce7..3dd9dd9f113 100644 --- a/inject/src/main/java/io/micronaut/context/AbstractBeanDefinition.java +++ b/inject/src/main/java/io/micronaut/context/AbstractBeanDefinition.java @@ -16,6 +16,7 @@ package io.micronaut.context; import io.micronaut.context.annotation.*; +import io.micronaut.context.env.ConfigurationPath; import io.micronaut.context.env.Environment; import io.micronaut.context.event.BeanInitializedEventListener; import io.micronaut.context.event.BeanInitializingEvent; @@ -304,12 +305,6 @@ public String toString() { return "Definition: " + declaringType.getName(); } - @SuppressWarnings("deprecation") - @Override - public boolean isProvided() { - return getAnnotationMetadata().hasDeclaredStereotype(Provided.class); - } - @Override public Optional> getScope() { return getAnnotationMetadata().getAnnotationTypeByStereotype(AnnotationUtil.SCOPE); @@ -2012,11 +2007,9 @@ private String getConfigurationPropertiesPath(BeanResolutionContext resolutionCo } private String substituteWildCards(BeanResolutionContext resolutionContext, String valString) { - if (valString.indexOf('*') > -1) { - Optional namedBean = resolutionContext.get(Named.class.getName(), ConversionContext.STRING); - if (namedBean.isPresent()) { - valString = valString.replace("*", namedBean.get()); - } + ConfigurationPath configurationPath = resolutionContext.getConfigurationPath(); + if (configurationPath.isNotEmpty()) { + return configurationPath.resolveValue(valString); } return valString; } @@ -2193,7 +2186,8 @@ private B resolveBeanWithGenericsForField(BeanResolutionContext resolutionCo } } - private boolean isConfigurationProperties() { + @Override + public boolean isConfigurationProperties() { return isConfigurationProperties; } diff --git a/inject/src/main/java/io/micronaut/context/AbstractBeanResolutionContext.java b/inject/src/main/java/io/micronaut/context/AbstractBeanResolutionContext.java index b832e3a3937..8fa5f6099d5 100644 --- a/inject/src/main/java/io/micronaut/context/AbstractBeanResolutionContext.java +++ b/inject/src/main/java/io/micronaut/context/AbstractBeanResolutionContext.java @@ -15,8 +15,10 @@ */ package io.micronaut.context; +import io.micronaut.context.annotation.Factory; import io.micronaut.context.env.CachedEnvironment; import io.micronaut.context.annotation.InjectScope; +import io.micronaut.context.env.ConfigurationPath; import io.micronaut.context.exceptions.CircularDependencyException; import io.micronaut.context.scope.CustomScope; import io.micronaut.core.annotation.AnnotationMetadata; @@ -49,6 +51,8 @@ public abstract class AbstractBeanResolutionContext implements BeanResolutionCon private List> dependentBeans; private BeanRegistration dependentFactory; + private ConfigurationPath configurationPath; + /** * @param context The bean context * @param rootDefinition The bean root definition @@ -60,6 +64,23 @@ protected AbstractBeanResolutionContext(DefaultBeanContext context, BeanDefiniti this.path = new DefaultPath(); } + @Override + public ConfigurationPath getConfigurationPath() { + if (configurationPath != null) { + return configurationPath; + } else { + this.configurationPath = ConfigurationPath.newPath(); + return configurationPath; + } + } + + @Override + public ConfigurationPath setConfigurationPath(ConfigurationPath configurationPath) { + ConfigurationPath old = this.configurationPath; + this.configurationPath = configurationPath; + return old; + } + @NonNull @Override public T getBean(@NonNull Argument beanType, @Nullable Qualifier qualifier) { @@ -506,7 +527,6 @@ public static class ConstructorSegment extends AbstractSegment { private final String methodName; private final Argument[] arguments; - private final BeanDefinition declaringClass; /** * @param declaringClass The declaring class @@ -518,7 +538,6 @@ public static class ConstructorSegment extends AbstractSegment { super(declaringClass, declaringClass.getBeanType().getName(), argument); this.methodName = methodName; this.arguments = arguments; - this.declaringClass = declaringClass; } @Override @@ -588,6 +607,20 @@ public CallableInjectionPoint getOuterInjectionPoint() { } return outer; } + + @Override + public String toString() { + BeanDefinition declaringBean = getDeclaringBean(); + if (declaringBean.hasAnnotation(Factory.class)) { + ConstructorInjectionPoint constructor = declaringBean.getConstructor(); + var baseString = new StringBuilder(constructor.getDeclaringBeanType().getSimpleName()).append('.'); + baseString.append(getName()); + outputArguments(baseString, getArguments()); + return baseString.toString(); + } else { + return super.toString(); + } + } } /** diff --git a/inject/src/main/java/io/micronaut/context/AbstractInitializableBeanDefinition.java b/inject/src/main/java/io/micronaut/context/AbstractInitializableBeanDefinition.java index 899abcc6bbb..ec1597206be 100644 --- a/inject/src/main/java/io/micronaut/context/AbstractInitializableBeanDefinition.java +++ b/inject/src/main/java/io/micronaut/context/AbstractInitializableBeanDefinition.java @@ -17,10 +17,10 @@ import io.micronaut.context.annotation.ConfigurationProperties; import io.micronaut.context.annotation.EachBean; -import io.micronaut.context.annotation.EachProperty; import io.micronaut.context.annotation.Parameter; import io.micronaut.context.annotation.Property; import io.micronaut.context.annotation.Value; +import io.micronaut.context.env.ConfigurationPath; import io.micronaut.context.env.Environment; import io.micronaut.context.event.BeanInitializedEventListener; import io.micronaut.context.event.BeanInitializingEvent; @@ -99,12 +99,10 @@ @Internal public class AbstractInitializableBeanDefinition extends AbstractBeanContextConditional implements BeanDefinition, EnvironmentConfigurable { private static final Logger LOG = LoggerFactory.getLogger(AbstractInitializableBeanDefinition.class); - private static final String NAMED_ATTRIBUTE = Named.class.getName(); private final Class type; private final AnnotationMetadata annotationMetadata; private final Optional scope; - private final boolean isProvided; private final boolean isIterable; private final boolean isSingleton; private final boolean isPrimary; @@ -178,7 +176,6 @@ protected AbstractInitializableBeanDefinition( this.annotationMetadata = annotationMetadata; } } - this.isProvided = isProvided; this.isIterable = isIterable; this.isSingleton = isSingleton; this.isPrimary = isPrimary; @@ -232,6 +229,11 @@ protected AbstractInitializableBeanDefinition( this.annotationInjection = annotationInjection; } + @Override + public final boolean isConfigurationProperties() { + return isConfigurationProperties; + } + @Override public Qualifier getDeclaredQualifier() { if (declaredQualifier == null) { @@ -304,11 +306,6 @@ public boolean isPrimary() { return isPrimary; } - @Override - public boolean isProvided() { - return isProvided; - } - @Override public boolean requiresMethodProcessing() { return requiresMethodProcessing; @@ -1157,7 +1154,7 @@ protected final K getBeanForMethodArgument(BeanResolutionContext resolutionC Argument argument = resolveArgument(context, argIndex, methodRef.arguments); try (BeanResolutionContext.Path ignored = resolutionContext.getPath() .pushMethodArgumentResolve(this, methodRef.methodName, argument, methodRef.arguments, methodRef.requiresReflection)) { - return resolveBean(resolutionContext, argument, qualifier, true); + return resolveBean(resolutionContext, argument, qualifier); } } @@ -1203,7 +1200,7 @@ protected final > R getBeansOfTypeForMethodArgument(B protected final Object getBeanForSetter(BeanResolutionContext resolutionContext, BeanContext context, String setterName, Argument argument, Qualifier qualifier) { try (BeanResolutionContext.Path ignored = resolutionContext.getPath() .pushMethodArgumentResolve(this, setterName, argument, new Argument[]{argument}, false)) { - return resolveBean(resolutionContext, argument, qualifier, true); + return resolveBean(resolutionContext, argument, qualifier); } } @@ -1332,7 +1329,7 @@ protected final Object getBeanForConstructorArgument(BeanResolutionContext resol } try (BeanResolutionContext.Path ignored = resolutionContext.getPath() .pushConstructorResolve(this, argument)) { - return resolveBean(resolutionContext, argument, qualifier, true); + return resolveBean(resolutionContext, argument, qualifier); } } @@ -1669,7 +1666,7 @@ protected final K getBeanForField(BeanResolutionContext resolutionContext, B final Argument argument = resolveEnvironmentArgument(context, fieldInjection[fieldIndex].argument); try (BeanResolutionContext.Path ignored = resolutionContext.getPath() .pushFieldResolve(this, argument, fieldInjection[fieldIndex].requiresReflection)) { - return resolveBean(resolutionContext, argument, qualifier, true); + return resolveBean(resolutionContext, argument, qualifier); } } @@ -2025,8 +2022,8 @@ private Object resolveValue(BeanResolutionContext resolutionContext, BeanContext } else { argumentType = argument; } - if (isInnerConfiguration(argumentType.getType())) { - qualifier = qualifier == null ? resolveQualifierWithInnerConfiguration(resolutionContext, argument, true) : qualifier; + if (isInnerConfiguration(argumentType)) { + qualifier = qualifier == null ? resolveQualifier(resolutionContext, argumentType, argument) : qualifier; if (isCollection) { Collection beans = resolutionContext.getBeansOfType(argumentType, qualifier); return coerceCollectionToCorrectType((Class) argumentJavaType, beans, resolutionContext, argument); @@ -2144,27 +2141,23 @@ private Object resolvePropertyValue(BeanResolutionContext resolutionContext, Bea } } - private K resolveBean(BeanResolutionContext resolutionContext, Argument argument, @Nullable Qualifier qualifier) { - return resolveBean(resolutionContext, argument, qualifier, false); - } - private K resolveBean( BeanResolutionContext resolutionContext, Argument argument, - @Nullable Qualifier qualifier, - boolean resolveIsInnerConfiguration) { - qualifier = qualifier == null ? resolveQualifier(resolutionContext, argument, resolveIsInnerConfiguration) : qualifier; - if (Qualifier.class.isAssignableFrom(argument.getType())) { + @Nullable Qualifier qualifier) { + qualifier = qualifier == null ? resolveQualifier(resolutionContext, argument, argument) : qualifier; + Class t = argument.getType(); + if (Qualifier.class.isAssignableFrom(t)) { return (K) qualifier; } try { - Object previous = !argument.isAnnotationPresent(Parameter.class) ? resolutionContext.removeAttribute(NAMED_ATTRIBUTE) : null; + boolean isNotInnerConfiguration = !isConfigurationProperties || !isInnerConfiguration(argument); + ConfigurationPath previousPath = isNotInnerConfiguration ? resolutionContext.setConfigurationPath(null) : null; try { - //noinspection unchecked return resolutionContext.getBean(argument, qualifier); } finally { - if (previous != null) { - resolutionContext.setAttribute(NAMED_ATTRIBUTE, previous); + if (previousPath != null) { + resolutionContext.setConfigurationPath(previousPath); } } } catch (DisabledBeanException e) { @@ -2234,11 +2227,9 @@ private String getProperty(BeanResolutionContext resolutionContext, AnnotationMe } private String substituteWildCards(BeanResolutionContext resolutionContext, String valString) { - if (valString.indexOf('*') > -1) { - Optional namedBean = resolutionContext.get(Named.class.getName(), ConversionContext.STRING); - if (namedBean.isPresent()) { - valString = valString.replace("*", namedBean.get()); - } + ConfigurationPath configurationPath = resolutionContext.getConfigurationPath(); + if (configurationPath.isNotEmpty()) { + return configurationPath.resolveValue(valString); } return valString; } @@ -2252,63 +2243,84 @@ private String resolveCliOption(String name) { return null; } - private > R resolveBeansOfType(BeanResolutionContext resolutionContext, BeanContext context, Argument argument, Argument resultGenericType, Qualifier qualifier) { - if (resultGenericType == null) { - throw new DependencyInjectionException(resolutionContext, "Type " + argument.getType() + " has no generic argument"); + private > R resolveBeansOfType(BeanResolutionContext resolutionContext, BeanContext context, Argument returnType, Argument beanType, Qualifier qualifier) { + if (beanType == null) { + throw new DependencyInjectionException(resolutionContext, "Type " + returnType.getType() + " has no generic argument"); + } + qualifier = qualifier == null ? resolveQualifier(resolutionContext, beanType, returnType) : qualifier; + Collection beansOfType = resolutionContext.getBeansOfType(resolveEnvironmentArgument(context, beanType), qualifier); + return coerceCollectionToCorrectType(returnType.getType(), beansOfType, resolutionContext, returnType); + } + + private boolean isInnerConfiguration(@Nullable Argument argument) { + if (argument == null || !isConfigurationProperties) { + return false; + } + if (argument.isContainerType() || argument.isOptional() || argument.isProvider()) { + return isInnerConfiguration(argument.getFirstTypeVariable().orElse(null)); + } else if (isIterable && isEachBeanParent(argument)) { + return true; } - qualifier = qualifier == null ? resolveQualifier(resolutionContext, (Argument) argument, true) : qualifier; - Collection beansOfType = resolutionContext.getBeansOfType(resolveEnvironmentArgument(context, resultGenericType), qualifier); - return coerceCollectionToCorrectType(argument.getType(), beansOfType, resolutionContext, argument); + return isInnerConfiguration(argument.getType()); } - private Stream resolveStreamOfType(BeanResolutionContext resolutionContext, Argument argument, Argument resultGenericType, Qualifier qualifier) { - if (resultGenericType == null) { - throw new DependencyInjectionException(resolutionContext, "Type " + argument.getType() + " has no generic argument"); + private boolean isEachBeanParent(Argument argument) { + // treat an each bean declaration like an inner configuration + Class t = getAnnotationMetadata().classValue(EachBean.class).orElse(null); + if (t != null && t.equals(argument.getType())) { + return true; } - qualifier = qualifier == null ? resolveQualifier(resolutionContext, argument) : qualifier; - return resolutionContext.streamOfType(resultGenericType, qualifier); + return false; + } + + private Stream resolveStreamOfType(BeanResolutionContext resolutionContext, Argument returnType, Argument beanType, Qualifier qualifier) { + if (beanType == null) { + throw new DependencyInjectionException(resolutionContext, "Type " + returnType.getType() + " has no generic argument"); + } + qualifier = qualifier == null ? resolveQualifier(resolutionContext, beanType, returnType) : qualifier; + return resolutionContext.streamOfType(beanType, qualifier); } private Map resolveMapOfType( BeanResolutionContext resolutionContext, - Argument> argument, - Argument resultGenericType, + Argument> returnType, + Argument beanType, Qualifier qualifier) { - if (resultGenericType == null) { - throw new DependencyInjectionException(resolutionContext, "Type " + argument.getType() + " has no generic argument"); + if (beanType == null) { + throw new DependencyInjectionException(resolutionContext, "Type " + returnType.getType() + " has no generic returnType"); } - qualifier = qualifier == null ? resolveQualifier(resolutionContext, resultGenericType) : qualifier; - Map map = resolutionContext.mapOfType(resultGenericType, qualifier); - if (argument.isInstance(map)) { + qualifier = qualifier == null ? resolveQualifier(resolutionContext, beanType, returnType) : qualifier; + Map map = resolutionContext.mapOfType(beanType, qualifier); + if (returnType.isInstance(map)) { return map; } else { return resolutionContext.getContext().getConversionService().convertRequired( - map, argument + map, returnType ); } } - private Optional resolveOptionalBean(BeanResolutionContext resolutionContext, Argument argument, Argument resultGenericType, Qualifier qualifier) { - if (resultGenericType == null) { - throw new DependencyInjectionException(resolutionContext, "Type " + argument.getType() + " has no generic argument"); + private Optional resolveOptionalBean(BeanResolutionContext resolutionContext, Argument returnType, Argument beanType, Qualifier qualifier) { + if (beanType == null) { + throw new DependencyInjectionException(resolutionContext, "Type " + returnType.getType() + " has no generic argument"); } - qualifier = qualifier == null ? resolveQualifier(resolutionContext, argument) : qualifier; - return resolutionContext.findBean(resultGenericType, qualifier); + qualifier = qualifier == null ? resolveQualifier(resolutionContext, beanType, returnType) : qualifier; + return resolutionContext.findBean(beanType, qualifier); } private >> K resolveBeanRegistrations(BeanResolutionContext resolutionContext, - Argument argument, - Argument genericArgument, + Argument returnType, + Argument beanType, Qualifier qualifier) { try { - if (genericArgument == null) { - throw new DependencyInjectionException(resolutionContext, "Cannot resolve bean registrations. Argument [" + argument + "] missing generic type information."); + if (beanType == null) { + throw new DependencyInjectionException(resolutionContext, "Cannot resolve bean registrations. Argument [" + returnType + "] missing generic type information."); } - qualifier = qualifier == null ? resolveQualifier(resolutionContext, (Argument) argument) : qualifier; - Collection> beanRegistrations = resolutionContext.getBeanRegistrations(genericArgument, qualifier); - return coerceCollectionToCorrectType(argument.getType(), beanRegistrations, resolutionContext, argument); + qualifier = qualifier == null ? resolveQualifier(resolutionContext, beanType, returnType) : qualifier; + Collection> beanRegistrations = resolutionContext.getBeanRegistrations(beanType, qualifier); + return coerceCollectionToCorrectType(returnType.getType(), beanRegistrations, resolutionContext, returnType); } catch (NoSuchBeanException e) { - if (argument.isNullable()) { + if (returnType.isNullable()) { return null; } throw new DependencyInjectionException(resolutionContext, e); @@ -2333,57 +2345,48 @@ private Argument resolveEnvironmentArgument(BeanContext context, Argument } private BeanRegistration resolveBeanRegistration(BeanResolutionContext resolutionContext, BeanContext context, - Argument argument, Argument genericArgument, Qualifier qualifier) { + @NonNull Argument returnType, Argument beanType, Qualifier qualifier) { try { - if (genericArgument == null) { - throw new DependencyInjectionException(resolutionContext, "Cannot resolve bean registration. Argument [" + argument + "] missing generic type information."); + if (beanType == null) { + throw new DependencyInjectionException(resolutionContext, "Cannot resolve bean registration. Argument [" + returnType + "] missing generic type information."); } - qualifier = qualifier == null ? resolveQualifier(resolutionContext, argument) : qualifier; - return context.getBeanRegistration(genericArgument, qualifier); + qualifier = qualifier == null ? resolveQualifier(resolutionContext, beanType, returnType) : qualifier; + return context.getBeanRegistration(beanType, qualifier); } catch (NoSuchBeanException e) { - if (argument.isNullable()) { + if (returnType.isNullable()) { return null; } - throw new DependencyInjectionException(resolutionContext, argument, e); + throw new DependencyInjectionException(resolutionContext, returnType, e); } } - private Qualifier resolveQualifier(BeanResolutionContext resolutionContext, Argument argument) { - return resolveQualifier(resolutionContext, argument, false); - } - - private Qualifier resolveQualifier(BeanResolutionContext resolutionContext, Argument argument, boolean resolveIsInnerConfiguration) { - boolean innerConfiguration = resolveIsInnerConfiguration && isInnerConfiguration(argument.getType()); - return resolveQualifierWithInnerConfiguration(resolutionContext, argument, innerConfiguration); - } - - private Qualifier resolveQualifierWithInnerConfiguration(BeanResolutionContext resolutionContext, Argument argument, boolean innerConfiguration) { - boolean hasMetadata = argument.getAnnotationMetadata() != AnnotationMetadata.EMPTY_METADATA; - Qualifier qualifier = null; - boolean isIterable = isIterable() || resolutionContext.get(EachProperty.class.getName(), Class.class).map(getBeanType()::equals).orElse(false); - if (isIterable) { - qualifier = resolutionContext.get(AnnotationUtil.QUALIFIER, Map.class) - .map(map -> (Qualifier) map.get(argument)) - .orElse(null); - } - if (qualifier == null) { - if ((hasMetadata && argument.isAnnotationPresent(Parameter.class)) || - (innerConfiguration && isIterable) || - Qualifier.class == argument.getType()) { - final Qualifier currentQualifier = (Qualifier) resolutionContext.getCurrentQualifier(); - if (currentQualifier != null && - currentQualifier.getClass() != InterceptorBindingQualifier.class && - currentQualifier.getClass() != TypeAnnotationQualifier.class) { - qualifier = currentQualifier; + private Qualifier resolveQualifier(BeanResolutionContext resolutionContext, Argument beanType, Argument resultType) { + if (isInnerConfiguration(beanType)) { + ConfigurationPath configurationPath = resolutionContext.getConfigurationPath(); + Qualifier q = configurationPath.beanQualifier(); + if (q instanceof Named named && resultType.isContainerType()) { + return Qualifiers.byNamePrefix(named.getName()); + } else { + if (q == null && isEachBeanParent(beanType)) { + return (Qualifier) resolutionContext.getCurrentQualifier(); } else { - final Optional n = resolutionContext.get(NAMED_ATTRIBUTE, ConversionContext.STRING); - qualifier = n.map(Qualifiers::byName).orElse(null); + return q; } } + } else if (Qualifier.class == resultType.getType()) { + final Qualifier currentQualifier = (Qualifier) resolutionContext.getCurrentQualifier(); + if (currentQualifier != null && + currentQualifier.getClass() != InterceptorBindingQualifier.class && + currentQualifier.getClass() != TypeAnnotationQualifier.class) { + return currentQualifier; + } + } else if (isIterable && resultType.isAnnotationPresent(Parameter.class)) { + return (Qualifier) resolutionContext.getCurrentQualifier(); } - return qualifier; + return null; } + @SuppressWarnings("unchecked") private > K coerceCollectionToCorrectType(Class collectionType, Collection beansOfType, BeanResolutionContext resolutionContext, Argument argument) { if (argument.isArray() || collectionType.isInstance(beansOfType)) { diff --git a/inject/src/main/java/io/micronaut/context/BeanDefinitionDelegate.java b/inject/src/main/java/io/micronaut/context/BeanDefinitionDelegate.java index b5340af54ce..41bc3888035 100644 --- a/inject/src/main/java/io/micronaut/context/BeanDefinitionDelegate.java +++ b/inject/src/main/java/io/micronaut/context/BeanDefinitionDelegate.java @@ -15,20 +15,17 @@ */ package io.micronaut.context; +import io.micronaut.context.annotation.EachBean; +import io.micronaut.context.annotation.Parameter; import io.micronaut.context.annotation.Primary; +import io.micronaut.context.env.ConfigurationPath; import io.micronaut.context.exceptions.BeanInstantiationException; -import io.micronaut.core.annotation.AnnotationUtil; import io.micronaut.core.annotation.Internal; import io.micronaut.core.annotation.NonNull; import io.micronaut.core.annotation.Nullable; -import io.micronaut.core.convert.ArgumentConversionContext; -import io.micronaut.core.convert.ConversionService; -import io.micronaut.core.convert.MutableConversionService; import io.micronaut.core.naming.NameResolver; import io.micronaut.core.naming.Named; import io.micronaut.core.type.Argument; -import io.micronaut.core.util.CollectionUtils; -import io.micronaut.core.value.ValueResolver; import io.micronaut.inject.BeanDefinition; import io.micronaut.inject.BeanFactory; import io.micronaut.inject.DelegatingBeanDefinition; @@ -37,10 +34,9 @@ import io.micronaut.inject.InjectionPoint; import io.micronaut.inject.ParametrizedBeanFactory; import io.micronaut.inject.ValidatedBeanDefinition; -import io.micronaut.inject.qualifiers.Qualifiers; +import io.micronaut.inject.qualifiers.PrimaryQualifier; import java.util.Collections; -import java.util.HashMap; import java.util.LinkedHashMap; import java.util.Map; import java.util.Objects; @@ -54,51 +50,44 @@ * @since 1.0 */ @Internal -class BeanDefinitionDelegate extends AbstractBeanContextConditional implements DelegatingBeanDefinition, BeanFactory, NameResolver, ValueResolver { - - static final String PRIMARY_ATTRIBUTE = Primary.class.getName(); +sealed class BeanDefinitionDelegate extends AbstractBeanContextConditional + implements DelegatingBeanDefinition, BeanFactory, NameResolver { protected final BeanDefinition definition; @Nullable - protected Map attributes; + protected final Qualifier qualifier; + @Nullable - protected final Qualifier qualifier; + private final ConfigurationPath configurationPath; - private BeanDefinitionDelegate(BeanDefinition definition, @Nullable Qualifier qualifier) { + + private BeanDefinitionDelegate(BeanDefinition definition, @Nullable Qualifier qualifier, @Nullable ConfigurationPath configurationPath) { this.definition = definition; this.qualifier = qualifier; + this.configurationPath = configurationPath; } - /** - * @return the qualifier - */ - @Nullable - public Qualifier getQualifier() { + public Optional getConfigurationPath() { + return Optional.ofNullable(configurationPath); + } + + @Override + public Qualifier getDeclaredQualifier() { return qualifier; } /** - * @return the attributes + * @return the qualifier */ @Nullable - public Map getAttributes() { - return attributes; + public Qualifier getQualifier() { + return qualifier; } @Nullable @Override public Qualifier resolveDynamicQualifier() { - if (qualifier != null) { - return qualifier; - } - if (attributes == null) { - return null; - } - Object o = attributes.get(NAMED_ATTRIBUTE); - if (o instanceof CharSequence) { - return Qualifiers.byName(o.toString()); - } - return null; + return qualifier; } /** @@ -120,34 +109,26 @@ public boolean isIterable() { @Override public boolean isPrimary() { - return definition.isPrimary() || isPrimaryThroughAttribute(); + return isLocalQualifierPrimary() || definition.isPrimary() || isPrimaryThroughAttribute(); + } + + private boolean isLocalQualifierPrimary() { + return qualifier != null && (qualifier == PrimaryQualifier.INSTANCE || qualifier.contains(PrimaryQualifier.INSTANCE)); } private boolean isPrimaryThroughAttribute() { - if (attributes == null) { - return false; - } - Object o = attributes.get(PRIMARY_ATTRIBUTE); - if (o instanceof Boolean) { - return (Boolean) o; + if (configurationPath != null) { + return configurationPath.isPrimary(); } return false; } @Override public T build(BeanResolutionContext resolutionContext, BeanContext context, BeanDefinition definition) throws BeanInstantiationException { - Map oldAttributes = null; - if (CollectionUtils.isNotEmpty(attributes)) { - oldAttributes = resolutionContext.getAttributes(); - Map newAttributes; - if (oldAttributes == null) { - newAttributes = new LinkedHashMap<>(attributes); - } else { - newAttributes = new LinkedHashMap<>(attributes.size() + oldAttributes.size(), 1); - newAttributes.putAll(oldAttributes); - newAttributes.putAll(attributes); - } - resolutionContext.setAttributes(newAttributes); + ConfigurationPath oldPath = null; + if (configurationPath != null) { + oldPath = resolutionContext.getConfigurationPath(); + resolutionContext.setConfigurationPath(configurationPath); } try { @@ -162,7 +143,7 @@ public T build(BeanResolutionContext resolutionContext, BeanContext context, Bea throw new IllegalStateException("Cannot construct a dynamically registered singleton"); } } finally { - resolutionContext.setAttributes(oldAttributes); + resolutionContext.setConfigurationPath(oldPath); } } @@ -175,55 +156,61 @@ private Map getParametersValues(BeanResolutionContext resolution if (requiredArguments.length == 0) { return Collections.emptyMap(); } - MutableConversionService conversionService = context.getConversionService(); Map fulfilled = new LinkedHashMap<>(requiredArguments.length, 1); + ConfigurationPath configurationPath = resolutionContext.getConfigurationPath(); for (Argument argument : requiredArguments) { String argumentName = argument.getName(); - Object value = resolveValueAsName(conversionService, argument); - if (value == null) { - Qualifier qualifier = resolveQualifier(argument); - if (qualifier == null) { - if (!isPrimary()) { - continue; + if (argument.isAnnotationPresent(Parameter.class)) { + Class type = (Class) argument.getWrapperType(); + if (CharSequence.class.isAssignableFrom(type)) { + String simpleName = configurationPath.simpleName(); + if (simpleName != null) { + fulfilled.put(argumentName, simpleName); + } else { + Qualifier q = resolutionContext.getCurrentQualifier(); + if (q instanceof Named named) { + fulfilled.put(argumentName, named.getName()); + } else if (q == PrimaryQualifier.INSTANCE) { + fulfilled.put(argumentName, Primary.SIMPLE_NAME); + } + } + } else if (Number.class.isAssignableFrom(type)) { + fulfilled.put(argumentName, context.getConversionService().convertRequired(configurationPath.index(), argument)); + } else if (qualifier != null && hasDeclaredAnnotation(EachBean.class) && String.class.equals(type) && "name".equals(argumentName)) { + if (isLocalQualifierPrimary()) { + fulfilled.put(argumentName, Primary.SIMPLE_NAME); + } else if (qualifier instanceof Named named) { + fulfilled.put(argumentName, named.getName()); + } + } else { + if (argument.isProvider()) { + Argument pt = argument.getFirstTypeVariable().orElse(null); + if (pt != null) { + type = (Class) pt.getType(); + } + } + + try (BeanResolutionContext.Path ignored = resolutionContext.getPath().pushConstructorResolve(definition, argument)) { + if (type.equals(configurationPath.configurationType())) { + Object bean = context.findBean(resolutionContext, argument, configurationPath.beanQualifier()).orElse(null); + fulfilled.put(argumentName, bean); + } else { + ConfigurationPath old = resolutionContext.setConfigurationPath(null);// reset + try { + Qualifier q = qualifier != null ? (Qualifier) qualifier : configurationPath.beanQualifier(); + Object bean = context.findBean(resolutionContext, argument, q).orElse(null); + fulfilled.put(argumentName, bean); + } finally { + resolutionContext.setConfigurationPath(old); + } + } } } - try (BeanResolutionContext.Path ignored = resolutionContext.getPath().pushConstructorResolve(definition, argument)) { - value = context.findBean(resolutionContext, argument, qualifier).orElse(null); - } - } - if (value != null) { - fulfilled.put(argumentName, value); } } return fulfilled; } - @Nullable - private Qualifier resolveQualifier(Argument argument) { - Object qualifierMapValue = attributes == null ? Collections.emptyMap() : attributes.get(AnnotationUtil.QUALIFIER); - if (qualifierMapValue instanceof Map) { - Qualifier qualifier = ((Map) qualifierMapValue).get(argument); - if (qualifier != null) { - return qualifier; - } - } - return (Qualifier) resolveDynamicQualifier(); - } - - @Nullable - private Object resolveValueAsName(ConversionService conversionService, Argument argument) { - Object named = attributes == null ? null : attributes.get(Named.class.getName()); - Object value = null; - if (named != null) { - value = conversionService.convert(named, argument).orElse(null); - } - if (value == null && isPrimary()) { - // Backwards compatibility, all qualifiers where "Named" before - value = conversionService.convert("Primary", argument).orElse(null); - } - return value; - } - @Override public boolean equals(Object o) { if (this == o) { @@ -234,12 +221,12 @@ public boolean equals(Object o) { } BeanDefinitionDelegate that = (BeanDefinitionDelegate) o; return Objects.equals(definition, that.definition) && - Objects.equals(resolveName().orElse(null), that.resolveName().orElse(null)); + Objects.equals(qualifier, that.qualifier); } @Override public int hashCode() { - return Objects.hash(definition, resolveName().orElse(null)); + return Objects.hash(definition, qualifier); } /** @@ -252,30 +239,8 @@ public BeanDefinition getTarget() { @Override public Optional resolveName() { - return get(Named.class.getName(), String.class); - } - - /** - * Adds a new attribute. - * - * @param name The name - * @param value The value - */ - public void put(String name, Object value) { - if (attributes == null) { - attributes = new HashMap<>(2, 1); - } - this.attributes.put(name, value); - } - - @Override - public Optional get(String name, ArgumentConversionContext conversionContext) { - if (attributes == null) { - return Optional.empty(); - } - Object value = attributes.get(name); - if (value != null && conversionContext.getArgument().getType().isInstance(value)) { - return Optional.of((K) value); + if (qualifier instanceof Named named) { + return Optional.of(named.getName()); } return Optional.empty(); } @@ -300,17 +265,28 @@ static BeanDefinitionDelegate create(BeanDefinition definition) { * @param The type * @return The new bean definition */ - static BeanDefinitionDelegate create(BeanDefinition definition, Qualifier qualifier) { + static BeanDefinitionDelegate create(BeanDefinition definition, Qualifier qualifier) { + return create(definition, qualifier, null); + } + + /** + * @param definition The bean definition type + * @param qualifier The bean qualifier + * @param path The configuration path. + * @param The type + * @return The new bean definition + */ + static BeanDefinitionDelegate create(BeanDefinition definition, Qualifier qualifier, ConfigurationPath path) { if (definition instanceof InitializingBeanDefinition || definition instanceof DisposableBeanDefinition) { if (definition instanceof ValidatedBeanDefinition) { - return new LifeCycleValidatingDelegate<>(definition, qualifier); + return new LifeCycleValidatingDelegate<>(definition, qualifier, path); } else { - return new LifeCycleDelegate<>(definition, qualifier); + return new LifeCycleDelegate<>(definition, qualifier, path); } } else if (definition instanceof ValidatedBeanDefinition) { - return new ValidatingDelegate<>(definition, qualifier); + return new ValidatingDelegate<>(definition, qualifier, path); } - return new BeanDefinitionDelegate<>(definition, qualifier); + return new BeanDefinitionDelegate<>(definition, qualifier, path); } @Override @@ -322,7 +298,7 @@ public String getName() { /** * @param The bean definition type */ - interface ProxyInitializingBeanDefinition extends DelegatingBeanDefinition, InitializingBeanDefinition { + sealed interface ProxyInitializingBeanDefinition extends DelegatingBeanDefinition, InitializingBeanDefinition { @Override default T initialize(BeanResolutionContext resolutionContext, BeanContext context, T bean) { BeanDefinition definition = getTarget(); @@ -336,7 +312,7 @@ default T initialize(BeanResolutionContext resolutionContext, BeanContext contex /** * @param The bean definition type */ - interface ProxyDisposableBeanDefinition extends DelegatingBeanDefinition, DisposableBeanDefinition { + sealed interface ProxyDisposableBeanDefinition extends DelegatingBeanDefinition, DisposableBeanDefinition { @Override default T dispose(BeanResolutionContext resolutionContext, BeanContext context, T bean) { BeanDefinition definition = getTarget(); @@ -350,7 +326,7 @@ default T dispose(BeanResolutionContext resolutionContext, BeanContext context, /** * @param The bean definition type */ - interface ProxyValidatingBeanDefinition extends DelegatingBeanDefinition, ValidatedBeanDefinition { + sealed interface ProxyValidatingBeanDefinition extends DelegatingBeanDefinition, ValidatedBeanDefinition { @Override default T validate(BeanResolutionContext resolutionContext, T instance) { BeanDefinition definition = getTarget(); @@ -379,8 +355,8 @@ default void validateBeanArgument(@NonNull BeanResolutionContext resolutionC * @param The bean definition type */ private static final class LifeCycleDelegate extends BeanDefinitionDelegate implements ProxyInitializingBeanDefinition, ProxyDisposableBeanDefinition { - private LifeCycleDelegate(BeanDefinition definition, Qualifier qualifier) { - super(definition, qualifier); + private LifeCycleDelegate(BeanDefinition definition, Qualifier qualifier, ConfigurationPath path) { + super(definition, qualifier, path); } } @@ -388,8 +364,8 @@ private LifeCycleDelegate(BeanDefinition definition, Qualifier qualifier) { * @param The bean definition type */ private static final class ValidatingDelegate extends BeanDefinitionDelegate implements ProxyValidatingBeanDefinition { - private ValidatingDelegate(BeanDefinition definition, Qualifier qualifier) { - super(definition, qualifier); + private ValidatingDelegate(BeanDefinition definition, Qualifier qualifier, ConfigurationPath path) { + super(definition, qualifier, path); } } @@ -397,8 +373,8 @@ private ValidatingDelegate(BeanDefinition definition, Qualifier qualifier) { * @param The bean definition type */ private static final class LifeCycleValidatingDelegate extends BeanDefinitionDelegate implements ProxyValidatingBeanDefinition, ProxyInitializingBeanDefinition, ProxyDisposableBeanDefinition { - private LifeCycleValidatingDelegate(BeanDefinition definition, Qualifier qualifier) { - super(definition, qualifier); + private LifeCycleValidatingDelegate(BeanDefinition definition, Qualifier qualifier, ConfigurationPath path) { + super(definition, qualifier, path); } } } diff --git a/inject/src/main/java/io/micronaut/context/BeanResolutionContext.java b/inject/src/main/java/io/micronaut/context/BeanResolutionContext.java index acb5af5fde0..c26bd02dd6e 100644 --- a/inject/src/main/java/io/micronaut/context/BeanResolutionContext.java +++ b/inject/src/main/java/io/micronaut/context/BeanResolutionContext.java @@ -15,6 +15,7 @@ */ package io.micronaut.context; +import io.micronaut.context.env.ConfigurationPath; import io.micronaut.core.annotation.Internal; import io.micronaut.core.annotation.NonNull; import io.micronaut.core.annotation.Nullable; @@ -167,6 +168,13 @@ default Map mapOfType(@NonNull Argument beanType, @Nullable Qu */ Path getPath(); + /** + * @return The configuration path. + * @since 4.0.0 + */ + @NonNull + ConfigurationPath getConfigurationPath(); + /** * Store a value within the context. * @@ -296,6 +304,14 @@ default void markDependentAsFactory() { return null; } + /** + * Sets the configuration path. + * @param configurationPath The configuration path. + * @return The previous path + */ + @Nullable + ConfigurationPath setConfigurationPath(@Nullable ConfigurationPath configurationPath); + /** * Represents a path taken to resolve a bean definitions dependencies. */ diff --git a/inject/src/main/java/io/micronaut/context/DefaultApplicationContext.java b/inject/src/main/java/io/micronaut/context/DefaultApplicationContext.java index 795f0307847..10b2f83b219 100644 --- a/inject/src/main/java/io/micronaut/context/DefaultApplicationContext.java +++ b/inject/src/main/java/io/micronaut/context/DefaultApplicationContext.java @@ -21,12 +21,13 @@ import io.micronaut.context.annotation.EachProperty; import io.micronaut.context.env.BootstrapPropertySourceLocator; import io.micronaut.context.env.CachedEnvironment; +import io.micronaut.context.env.ConfigurationPath; import io.micronaut.context.env.DefaultEnvironment; import io.micronaut.context.env.Environment; import io.micronaut.context.env.PropertySource; import io.micronaut.context.exceptions.ConfigurationException; +import io.micronaut.context.exceptions.DependencyInjectionException; import io.micronaut.context.exceptions.NoSuchBeanException; -import io.micronaut.core.annotation.AnnotationUtil; import io.micronaut.core.annotation.NonNull; import io.micronaut.core.annotation.Nullable; import io.micronaut.core.convert.ArgumentConversionContext; @@ -53,7 +54,7 @@ import java.util.Map; import java.util.Objects; import java.util.Optional; -import java.util.function.Predicate; +import java.util.Set; import java.util.stream.Collectors; /** @@ -221,6 +222,11 @@ public Map getProperties(@Nullable String name, @Nullable String return getEnvironment().getProperties(name, keyFormat); } + @Override + public Collection> getPropertyPathMatches(String pathPattern) { + return getEnvironment().getPropertyPathMatches(pathPattern); + } + @Override protected synchronized void registerConfiguration(BeanConfiguration configuration) { if (getEnvironment().isActive(configuration)) { @@ -258,18 +264,6 @@ protected void initializeContext(List contextScopeBeans super.initializeContext(contextScopeBeans, processedBeans, parallelBeans); } - @Override - protected Collection> findBeanCandidates(BeanResolutionContext resolutionContext, Argument beanType, BeanDefinition filter, boolean filterProxied) { - Collection> candidates = super.findBeanCandidates(resolutionContext, beanType, filter, filterProxied); - return transformIterables(resolutionContext, candidates, filterProxied); - } - - @Override - protected Collection> findBeanCandidates(BeanResolutionContext resolutionContext, Argument beanType, boolean filterProxied, Predicate> predicate) { - Collection> candidates = super.findBeanCandidates(resolutionContext, beanType, filterProxied, predicate); - return transformIterables(resolutionContext, candidates, filterProxied); - } - @Override protected NoSuchBeanException newNoSuchBeanException(@Nullable BeanResolutionContext resolutionContext, Argument beanType, Qualifier qualifier, String message) { if (message == null) { @@ -287,7 +281,7 @@ private String resolveIterableBeanMissingMessage(BeanResolutionContext resol BeanDefinition definition = findAnyBeanDefinition(resolutionContext, beanType); if (definition != null && definition.isIterable()) { if (definition.hasDeclaredAnnotation(EachProperty.class)) { - message = resolveEachPropertyMissingBeanMessage(qualifier, definition); + message = resolveEachPropertyMissingBeanMessage(resolutionContext, qualifier, definition); } else if (definition.hasDeclaredAnnotation(EachBean.class)) { message = resolveEachBeanMissingMessage(resolutionContext, beanType, qualifier, definition); } @@ -298,11 +292,11 @@ private String resolveIterableBeanMissingMessage(BeanResolutionContext resol @NonNull private String resolveEachBeanMissingMessage(BeanResolutionContext resolutionContext, Argument beanType, Qualifier qualifier, BeanDefinition definition) { String message; - List> dependencyChain = calculateDependencyChain(resolutionContext, definition); + List> dependencyChain = calculateEachBeanChain(resolutionContext, definition); StringBuilder messageBuilder = new StringBuilder(); Argument requiredBeanType = beanType; Iterator> i = dependencyChain.iterator(); - String ls = System.getProperty("line.separator"); + String ls = CachedEnvironment.getProperty("line.separator"); while (i.hasNext()) { messageBuilder.append(ls); BeanDefinition beanDefinition = i.next(); @@ -317,7 +311,7 @@ private String resolveEachBeanMissingMessage(BeanResolutionContext resolutio messageBuilder.append(" which does not exist."); if (beanDefinition.hasDeclaredAnnotation(EachProperty.class)) { messageBuilder.append(ls); - String propertyMissingMessage = resolveEachPropertyMissingBeanMessage(qualifier, beanDefinition); + String propertyMissingMessage = resolveEachPropertyMissingBeanMessage(resolutionContext, qualifier, beanDefinition); messageBuilder.append("* ") .append("[") .append(nextBeanType.getTypeString(true)) @@ -334,7 +328,7 @@ private String resolveEachBeanMissingMessage(BeanResolutionContext resolutio @Nullable private BeanDefinition findAnyBeanDefinition(BeanResolutionContext resolutionContext, Argument beanType) { - Collection> existing = super.findBeanCandidates(resolutionContext, beanType, true, definition -> !definition.isAbstract()); + Collection> existing = super.findBeanCandidates(resolutionContext, beanType, true, false, definition -> !definition.isAbstract()); BeanDefinition definition = null; if (existing.size() == 1) { definition = existing.iterator().next(); @@ -342,7 +336,7 @@ private BeanDefinition findAnyBeanDefinition(BeanResolutionContext resolu return definition; } - private List> calculateDependencyChain( + private List> calculateEachBeanChain( BeanResolutionContext resolutionContext, BeanDefinition definition) { Class dependentBean = definition.classValue(EachBean.class).orElse(null); @@ -359,240 +353,202 @@ private List> calculateDependencyChain( return chain; } - @NonNull - private static String resolveEachPropertyMissingBeanMessage(Qualifier qualifier, BeanDefinition definition) { - String prefix = definition.stringValue(EachProperty.class).orElse(""); - if (qualifier != null) { - if (qualifier instanceof Named named) { - prefix += "." + named.getName(); - } else { - prefix += "." + "*"; + private List> calculateEachPropertyChain( + BeanResolutionContext resolutionContext, + BeanDefinition definition) { + List> chain = new ArrayList<>(); + while (definition != null) { + chain.add(definition); + Class declaringClass = definition.getBeanType().getDeclaringClass(); + if (declaringClass == null) { + break; } - } else { - prefix += "." + definition.stringValue(EachProperty.class, "primary").orElse("*"); + BeanDefinition dependent = findAnyBeanDefinition(resolutionContext, Argument.of(declaringClass)); + if (dependent == null || !dependent.isConfigurationProperties()) { + break; + } + definition = dependent; } - return "No configuration entries found under the prefix: [" + prefix + "]. Provide the necessary configuration to resolve this issue."; + return chain; } - @Override - protected Collection> transformIterables(BeanResolutionContext resolutionContext, Collection> candidates, boolean filterProxied) { - if (!candidates.isEmpty()) { - - List> transformedCandidates = new ArrayList<>(); - for (BeanDefinition candidate : candidates) { - if (candidate.isIterable()) { - if (candidate.hasDeclaredStereotype(EachProperty.class)) { - transformEachPropertyBeanDefinition(resolutionContext, candidate, transformedCandidates); - } else if (candidate.hasDeclaredStereotype(EachBean.class)) { - transformEachBeanBeanDefinition(resolutionContext, candidate, transformedCandidates, filterProxied); - } - } else if (candidate.hasStereotype(ConfigurationReader.class)) { - transformConfigurationReaderBeanDefinition(resolutionContext, candidate, transformedCandidates); - } else { - transformedCandidates.add(candidate); - } - } - if (LOG.isDebugEnabled()) { - LOG.debug("Finalized bean definitions candidates: {}", candidates); - for (BeanDefinition definition : transformedCandidates) { - LOG.debug(" {} {} {}", definition.getBeanType(), definition.getDeclaredQualifier(), definition); - } - } - return transformedCandidates; - } - return candidates; - } - private void transformConfigurationReaderBeanDefinition(BeanResolutionContext resolutionContext, - BeanDefinition candidate, - List> transformedCandidates) { - final String prefix = candidate.stringValue(ConfigurationReader.class, "prefix").orElse(null); - if (prefix != null) { - int mapIndex = prefix.indexOf("*"); - int arrIndex = prefix.indexOf("[*]"); - - boolean isList = arrIndex > -1; - boolean isMap = mapIndex > -1; - if (isList || isMap) { - int startIndex = isList ? arrIndex : mapIndex; - String eachProperty = prefix.substring(0, startIndex); - if (eachProperty.endsWith(".")) { - eachProperty = eachProperty.substring(0, eachProperty.length() - 1); - } + @NonNull + private String resolveEachPropertyMissingBeanMessage(BeanResolutionContext resolutionContext, Qualifier qualifier, BeanDefinition definition) { + List> chain = calculateEachPropertyChain(resolutionContext, definition); + String prefix; + if (chain.size() > 1) { + Collections.reverse(chain); + ConfigurationPath path = ConfigurationPath.of(chain.toArray(BeanDefinition[]::new)); + prefix = path.path(); + } else { - if (StringUtils.isEmpty(eachProperty)) { - throw new IllegalArgumentException("Blank value specified to @Each property for bean: " + candidate); - } - if (isList) { - transformConfigurationReaderList(resolutionContext, candidate, prefix, eachProperty, transformedCandidates); + prefix = definition.stringValue(EachProperty.class).orElse(""); + if (qualifier != null) { + if (qualifier instanceof Named named) { + prefix += "." + named.getName(); } else { - transformConfigurationReaderMap(resolutionContext, candidate, prefix, eachProperty, transformedCandidates); + prefix += "." + "*"; } - return; + } else { + prefix += "." + definition.stringValue(EachProperty.class, "primary").orElse("*"); } } - transformedCandidates.add(candidate); + + + return "No configuration entries found under the prefix: [" + prefix + "]. Provide the necessary configuration to resolve this issue."; } - private void transformConfigurationReaderMap(BeanResolutionContext resolutionContext, - BeanDefinition candidate, - String prefix, - String eachProperty, - List> transformedCandidates) { - Map entries = getProperty(eachProperty, Map.class, Collections.emptyMap()); - if (!entries.isEmpty()) { - for (Object key : entries.keySet()) { - BeanDefinitionDelegate delegate = BeanDefinitionDelegate.create(candidate); - delegate.put(EachProperty.class.getName(), delegate.getBeanType()); - delegate.put(Named.class.getName(), key.toString()); - - if (delegate.isEnabled(this, resolutionContext) && - containsProperties(prefix.replace("*", key.toString()))) { - transformedCandidates.add(delegate); - } + @Override + protected void collectIterableBeans(BeanResolutionContext resolutionContext, BeanDefinition iterableBean, Set> targetSet) { + try(BeanResolutionContext rc = newResolutionContext(iterableBean, resolutionContext)) { + if (iterableBean.hasDeclaredStereotype(EachProperty.class)) { + transformEachPropertyBeanDefinition(rc, iterableBean, targetSet); + } else if (iterableBean.hasDeclaredStereotype(EachBean.class)) { + transformEachBeanBeanDefinition(rc, iterableBean, targetSet); + } else { + transformConfigurationReaderBeanDefinition(rc, iterableBean, targetSet); } } } - private void transformConfigurationReaderList(BeanResolutionContext resolutionContext, - BeanDefinition candidate, - String prefix, - String eachProperty, - List> transformedCandidates) { - List entries = getProperty(eachProperty, List.class, Collections.emptyList()); - if (!entries.isEmpty()) { - for (int i = 0; i < entries.size(); i++) { - if (entries.get(i) != null) { - BeanDefinitionDelegate delegate = BeanDefinitionDelegate.create(candidate); - String index = String.valueOf(i); - delegate.put("Array", index); - delegate.put(Named.class.getName(), index); - - if (delegate.isEnabled(this, resolutionContext) && - containsProperties(prefix.replace("*", index))) { - transformedCandidates.add(delegate); + private void transformConfigurationReaderBeanDefinition(BeanResolutionContext resolutionContext, + BeanDefinition candidate, + Set> transformedCandidates) { + try { + final String prefix = candidate.stringValue(ConfigurationReader.class, "prefix").orElse(null); + ConfigurationPath configurationPath = resolutionContext.getConfigurationPath(); + if (prefix != null) { + + if (configurationPath.isNotEmpty()) { + if (configurationPath.isWithin(prefix)) { + + ConfigurationPath newPath = configurationPath.copy(); + newPath.pushConfigurationReader(candidate); + newPath.traverseResolvableSegments(getEnvironment(), subPath -> + createAndAddDelegate(resolutionContext, candidate, transformedCandidates, subPath) + ); + } else { + ConfigurationPath oldPath = configurationPath; + ConfigurationPath newPath = ConfigurationPath.newPath(); + resolutionContext.setConfigurationPath(configurationPath); + try { + newPath.pushConfigurationReader(candidate); + newPath.traverseResolvableSegments(getEnvironment(), subPath -> + createAndAddDelegate(resolutionContext, candidate, transformedCandidates, subPath) + ); + } finally { + resolutionContext.setConfigurationPath(oldPath); + } + } + } else if (prefix.indexOf('*') == -1) { + // doesn't require outer configuration + transformedCandidates.add(candidate); + } else { + // if we have reached here we are likely in a nested a class being resolved directly from the context + // traverse and try reformulate the path + @SuppressWarnings("unchecked") + Class declaringClass = (Class) candidate.getBeanType().getDeclaringClass(); + if (declaringClass != null) { + Collection> beanCandidates = findBeanCandidates(resolutionContext, Argument.of(declaringClass), null, true); + for (BeanDefinition beanCandidate : beanCandidates) { + if (beanCandidate instanceof BeanDefinitionDelegate delegate) { + ConfigurationPath cp = delegate.getConfigurationPath().orElse(configurationPath).copy(); + cp.traverseResolvableSegments(getEnvironment(), subPath -> { + subPath.pushConfigurationReader(candidate); + if (getEnvironment().containsProperties(subPath.prefix())) { + createAndAddDelegate(resolutionContext, candidate, transformedCandidates, subPath); + } + }); + } else { + ConfigurationPath cp = configurationPath.copy(); + cp.pushConfigurationReader(beanCandidate); + cp.pushConfigurationReader(candidate); + cp.traverseResolvableSegments(getEnvironment(), subPath -> { + if (getEnvironment().containsProperties(subPath.prefix())) { + createAndAddDelegate(resolutionContext, candidate, transformedCandidates, subPath); + } + }); + + } + } } } } + } catch (IllegalStateException e) { + throw new DependencyInjectionException( + resolutionContext, + e.getMessage(), + e + ); } } - private void transformEachBeanBeanDefinition(BeanResolutionContext resolutionContext, + private void transformEachBeanBeanDefinition(@NonNull BeanResolutionContext resolutionContext, BeanDefinition candidate, - List> transformedCandidates, - boolean filterProxied) { + Set> transformedCandidates) { Class dependentType = candidate.classValue(EachBean.class).orElse(null); if (dependentType == null) { transformedCandidates.add(candidate); return; } - Collection dependentCandidates = findBeanCandidates(resolutionContext, Argument.of(dependentType), filterProxied, null); + Collection dependentCandidates = findBeanCandidates(resolutionContext, Argument.of(dependentType), true, true, null); if (!dependentCandidates.isEmpty()) { - for (BeanDefinition dependentCandidate : dependentCandidates) { - Qualifier qualifier; - if (dependentCandidate instanceof BeanDefinitionDelegate) { - qualifier = dependentCandidate.resolveDynamicQualifier(); - } else { - qualifier = dependentCandidate.getDeclaredQualifier(); - } - if (qualifier == null && dependentCandidate.isPrimary()) { - // Backwards compatibility, `getDeclaredQualifier` strips @Primary - // This should be removed if @Primary is no longer qualifier - qualifier = PrimaryQualifier.INSTANCE; + for (BeanDefinition dependentCandidate : dependentCandidates) { + ConfigurationPath dependentPath = null; + if (dependentCandidate instanceof BeanDefinitionDelegate delegate) { + dependentPath = delegate.getConfigurationPath().orElse(null); } - - BeanDefinitionDelegate delegate = BeanDefinitionDelegate.create(candidate, qualifier); - - if (dependentCandidate.isPrimary()) { - delegate.put(BeanDefinitionDelegate.PRIMARY_ATTRIBUTE, true); - } - - if (qualifier != null) { - String qualifierKey = AnnotationUtil.QUALIFIER; - Argument[] arguments = candidate.getConstructor().getArguments(); - for (Argument argument : arguments) { - Class argumentType = argument.getType(); - if (argumentType.equals(dependentType)) { - delegate.put(qualifierKey, Collections.singletonMap(argument, qualifier)); - break; - } - } - - if (qualifier instanceof Named named) { - delegate.put(Named.class.getName(), named.getName()); - } - if (delegate.isEnabled(this, resolutionContext)) { - transformedCandidates.add((BeanDefinition) delegate); + if (dependentPath != null) { + createAndAddDelegate(resolutionContext, candidate, transformedCandidates, dependentPath); + } else { + Qualifier qualifier = dependentCandidate.getDeclaredQualifier(); + if (qualifier == null && dependentCandidate.isPrimary()) { + // Backwards compatibility, `getDeclaredQualifier` strips @Primary + // This should be removed if @Primary is no longer qualifier + qualifier = PrimaryQualifier.INSTANCE; } + BeanDefinitionDelegate delegate = BeanDefinitionDelegate.create(candidate, (Qualifier) qualifier); + transformedCandidates.add((BeanDefinition) delegate); } } } } - private void transformEachPropertyBeanDefinition(BeanResolutionContext resolutionContext, + private void transformEachPropertyBeanDefinition(@NonNull BeanResolutionContext resolutionContext, BeanDefinition candidate, - List> transformedCandidates) { - boolean isList = candidate.booleanValue(EachProperty.class, "list").orElse(false); - String property = candidate.stringValue(ConfigurationReader.class, ConfigurationReader.PREFIX) - .map(prefix -> - //strip the .* or [*] - prefix.substring(0, prefix.length() - (isList ? 3 : 2))) - .orElseGet(() -> candidate.stringValue(EachProperty.class).orElse(null)); - String primaryPrefix = candidate.stringValue(EachProperty.class, "primary").orElse(null); - if (StringUtils.isEmpty(property)) { - throw new IllegalArgumentException("Blank value specified to @Each property for bean: " + candidate); - } - if (isList) { - transformEachPropertyOfList(resolutionContext, candidate, primaryPrefix, property, transformedCandidates); - } else { - transformEachPropertyOfMap(resolutionContext, candidate, primaryPrefix, property, transformedCandidates); - } - } - - private void transformEachPropertyOfMap(BeanResolutionContext resolutionContext, - BeanDefinition candidate, - String primaryPrefix, - String property, - List> transformedCandidates) { - for (String key : getEnvironment().getPropertyEntries(property)) { - BeanDefinitionDelegate delegate = BeanDefinitionDelegate.create(candidate); - if (primaryPrefix != null && primaryPrefix.equals(key)) { - delegate.put(BeanDefinitionDelegate.PRIMARY_ATTRIBUTE, true); - } - delegate.put(EachProperty.class.getName(), delegate.getBeanType()); - delegate.put(Named.class.getName(), key); - - if (delegate.isEnabled(this, resolutionContext)) { - transformedCandidates.add(delegate); - } - } - } - - private void transformEachPropertyOfList(BeanResolutionContext resolutionContext, - BeanDefinition candidate, - String primaryPrefix, - String property, - List> transformedCandidates) { - List entries = getEnvironment().getProperty(property, List.class, Collections.emptyList()); - int i = 0; - for (Object entry : entries) { - if (entry != null) { - BeanDefinitionDelegate delegate = BeanDefinitionDelegate.create(candidate); - String index = String.valueOf(i); - if (primaryPrefix != null && primaryPrefix.equals(index)) { - delegate.put(BeanDefinitionDelegate.PRIMARY_ATTRIBUTE, true); - } - delegate.put("Array", index); - delegate.put(Named.class.getName(), index); - - if (delegate.isEnabled(this, resolutionContext)) { - transformedCandidates.add(delegate); - } + Set> transformedCandidates) { + try { + ConfigurationPath configurationPath = resolutionContext.getConfigurationPath(); + configurationPath.pushEachPropertyRoot(candidate); + try { + ConfigurationPath rootConfig = resolutionContext.getConfigurationPath(); + rootConfig.traverseResolvableSegments(getEnvironment(), (subPath -> + createAndAddDelegate(resolutionContext, candidate, transformedCandidates, subPath) + )); + } finally { + configurationPath.removeLast(); } - i++; + } catch (IllegalStateException e) { + throw new DependencyInjectionException( + resolutionContext, + e.getMessage(), + e + ); + } + } + + private void createAndAddDelegate(BeanResolutionContext resolutionContext, BeanDefinition candidate, Set> transformedCandidates, ConfigurationPath path) { + BeanDefinitionDelegate delegate = BeanDefinitionDelegate.create( + candidate, + path.beanQualifier(), + path + ); + if (delegate.isEnabled(this, resolutionContext)) { + transformedCandidates.add(delegate); } } diff --git a/inject/src/main/java/io/micronaut/context/DefaultBeanContext.java b/inject/src/main/java/io/micronaut/context/DefaultBeanContext.java index 1cee51e331a..98cb23096b4 100644 --- a/inject/src/main/java/io/micronaut/context/DefaultBeanContext.java +++ b/inject/src/main/java/io/micronaut/context/DefaultBeanContext.java @@ -27,6 +27,7 @@ import io.micronaut.context.annotation.Secondary; import io.micronaut.context.condition.ConditionContext; import io.micronaut.context.condition.Failure; +import io.micronaut.context.env.CachedEnvironment; import io.micronaut.context.env.PropertyPlaceholderResolver; import io.micronaut.context.event.ApplicationEventListener; import io.micronaut.context.event.ApplicationEventPublisher; @@ -1626,7 +1627,7 @@ public Collection> getBeanDefinitions(@Nullable Qualifier Collection> findBeanCandidates(@NonNull Class * @param filterProxied Whether to filter out bean proxy targets * @return The candidates */ - @SuppressWarnings("unchecked") @NonNull protected Collection> findBeanCandidates(@Nullable BeanResolutionContext resolutionContext, @NonNull Argument beanType, @Nullable BeanDefinition filter, boolean filterProxied) { Predicate> predicate = filter == null ? null : definition -> !definition.equals(filter); - return findBeanCandidates(resolutionContext, beanType, filterProxied, predicate); + return findBeanCandidates(resolutionContext, beanType, filterProxied, true, predicate); } /** @@ -2193,12 +2193,37 @@ protected Collection> findBeanCandidates(@Nullable BeanRes * @param predicate The predicate to filter candidates * @return The candidates */ - @SuppressWarnings("unchecked") @NonNull protected Collection> findBeanCandidates(@Nullable BeanResolutionContext resolutionContext, @NonNull Argument beanType, boolean filterProxied, Predicate> predicate) { + return findBeanCandidates( + resolutionContext, + beanType, + filterProxied, + true, + predicate + ); + } + + /** + * Find bean candidates for the given type. + * + * @param The bean generic type + * @param resolutionContext The current resolution context + * @param beanType The bean type + * @param filterProxied Whether to filter out bean proxy targets + * @param collectIterables Whether iterables should be collected + * @param predicate The predicate to filter candidates + * @return The candidates + */ + @NonNull + protected Collection> findBeanCandidates(@Nullable BeanResolutionContext resolutionContext, + @NonNull Argument beanType, + boolean filterProxied, + boolean collectIterables, + Predicate> predicate) { ArgumentUtils.requireNonNull("beanType", beanType); final Class beanClass = beanType.getType(); if (LOG.isDebugEnabled()) { @@ -2217,7 +2242,14 @@ protected Collection> findBeanCandidates(@Nullable BeanRes beanDefinitionsClasses = this.beanDefinitionsClasses; } - return collectBeanCandidates(resolutionContext, beanType, filterProxied, predicate, beanDefinitionsClasses); + return collectBeanCandidates( + resolutionContext, + beanType, + filterProxied, + collectIterables, + predicate, + beanDefinitionsClasses + ); } @NonNull @@ -2225,6 +2257,7 @@ private Set> collectBeanCandidates( BeanResolutionContext resolutionContext, Argument beanType, boolean filterProxied, + boolean collectIterables, Predicate> predicate, Collection beanDefinitionsClasses) { Set> candidates; @@ -2250,12 +2283,17 @@ private Set> collectBeanCandidates( if (!loadedBean.isEnabled(this, resolutionContext)) { continue; } - candidates.add(loadedBean); + + if (collectIterables && loadedBean.isConfigurationProperties()) { + collectIterableBeans(resolutionContext, loadedBean, candidates); + } else { + candidates.add(loadedBean); + } } if (!candidates.isEmpty()) { if (filterProxied) { - filterProxiedTypes(candidates, false); + filterProxiedTypes(candidates); } filterReplacedBeans(resolutionContext, candidates); } @@ -2276,16 +2314,14 @@ private Set> collectBeanCandidates( } /** - * Method that transforms iterable candidates if possible. - * + * Collects iterable beans from a given iterable. * @param resolutionContext The resolution context - * @param candidates The candidates. - * @param filterProxied Whether to filter proxied. - * @param The bean type - * @return The candidates + * @param iterableBean The iterable + * @param targetSet The target set + * @param The bean type */ - protected Collection> transformIterables(BeanResolutionContext resolutionContext, Collection> candidates, boolean filterProxied) { - return candidates; + protected void collectIterableBeans(@Nullable BeanResolutionContext resolutionContext, @NonNull BeanDefinition iterableBean, Set> targetSet) { + // no-op } /** @@ -2485,9 +2521,6 @@ private T resolveByBeanFactory(@NonNull BeanResolutionContext resolutionCont boolean propagateQualifier = beanDefinition.isProxy() && declaredQualifier instanceof Named; Qualifier prevQualifier = resolutionContext.getCurrentQualifier(); try { - if (propagateQualifier) { - resolutionContext.setAttribute(BeanDefinition.NAMED_ATTRIBUTE, ((Named) declaredQualifier).getName()); - } resolutionContext.setCurrentQualifier(declaredQualifier != null && !AnyQualifier.INSTANCE.equals(declaredQualifier) ? declaredQualifier : qualifier); T bean; if (beanFactory instanceof ParametrizedBeanFactory) { @@ -2514,9 +2547,6 @@ private T resolveByBeanFactory(@NonNull BeanResolutionContext resolutionCont throw new BeanInstantiationException(beanDefinition, e); } finally { resolutionContext.setCurrentQualifier(prevQualifier); - if (propagateQualifier) { - resolutionContext.removeAttribute(BeanDefinition.NAMED_ATTRIBUTE); - } } } @@ -2642,7 +2672,7 @@ protected void processParallelBeans(List parallelBeans) } }); - filterProxiedTypes((Collection) parallelDefinitions, false); + filterProxiedTypes((Collection) parallelDefinitions); filterReplacedBeans(null, (Collection) parallelDefinitions); parallelDefinitions.forEach(beanDefinition -> ForkJoinPool.commonPool().execute(() -> { @@ -2876,9 +2906,15 @@ private void loadContextScopeBean(BeanDefinitionReference contextScopeBean, Cons } } - private void loadContextScopeBean(BeanDefinition beanDefinition) { + private void loadContextScopeBean(BeanDefinition beanDefinition) { if (beanDefinition.isIterable() || beanDefinition.hasStereotype(ConfigurationReader.class.getName())) { - Collection beanCandidates = (Collection) transformIterables(null, Collections.singleton(beanDefinition), true); + Set> beanCandidates = new HashSet<>(5); + + collectIterableBeans( + null, + beanDefinition, + beanCandidates + ); for (BeanDefinition beanCandidate : beanCandidates) { findOrCreateSingletonBeanRegistration( null, @@ -2933,7 +2969,7 @@ private BeanRegistration resolveBeanRegistration(@Nullable BeanResolution return beanRegistration; } - Optional> concreteCandidate = findBeanDefinition(beanType, qualifier); + Optional> concreteCandidate = findBeanDefinition(resolutionContext, beanType, qualifier); BeanRegistration registration; @@ -2953,6 +2989,14 @@ private BeanRegistration resolveBeanRegistration(@Nullable BeanResolution return registration; } + private Optional> findBeanDefinition(BeanResolutionContext resolutionContext, Argument beanType, Qualifier qualifier) { + BeanDefinition beanDefinition = singletonScope.findCachedSingletonBeanDefinition(beanType, qualifier); + if (beanDefinition != null) { + return Optional.of(beanDefinition); + } + return findConcreteCandidate(resolutionContext, beanType, qualifier, true); + } + /** * Trigger a no such bean exception. Subclasses can improve the exception with downstream diagnosis as necessary. * @@ -2998,7 +3042,7 @@ protected String resolveDisabledBeanMessage(BeanResolutionContext resolutio String pkg = entry.getKey(); if (beanType.getTypeName().startsWith(pkg + ".")) { StringBuilder messageBuilder = new StringBuilder(); - String ls = System.getProperty("line.separator"); + String ls = CachedEnvironment.getProperty("line.separator"); messageBuilder.append("The bean [") .append(beanType.getTypeString(true)) .append("] is disabled because it is within the package [") @@ -3020,6 +3064,7 @@ protected String resolveDisabledBeanMessage(BeanResolutionContext resolutio resolutionContext, beanType, true, + false, null, disabledBeans.values() ); @@ -3031,7 +3076,7 @@ protected String resolveDisabledBeanMessage(BeanResolutionContext resolutio if (!beanDefinitions.isEmpty()) { StringBuilder messageBuilder = new StringBuilder(); - String ls = System.getProperty("line.separator"); + String ls = CachedEnvironment.getProperty("line.separator"); messageBuilder.append("The following matching beans are disabled by bean requirements: ").append(ls); for (BeanDefinition beanDefinition : beanDefinitions) { messageBuilder.append("* Bean of type [").append(beanDefinition.asArgument().getTypeString(false)) @@ -3322,7 +3367,7 @@ private Optional> findConcreteCandidateNoCache(@Nullable B boolean filterProxied) { Predicate> predicate = candidate -> !candidate.isAbstract(); - Collection> candidates = findBeanCandidates(resolutionContext, beanType, filterProxied, predicate); + Collection> candidates = findBeanCandidates(resolutionContext, beanType, filterProxied, true, predicate); return pickOneBean(beanType, qualifier, throwNonUnique, candidates); } @@ -3381,28 +3426,18 @@ private Optional> pickOneBean( return Optional.ofNullable(definition); } + @SuppressWarnings("java:S1871") private void filterProxiedTypes( - Collection> candidates, - boolean filterDelegates) { + Collection> candidates) { int count = candidates.size(); Set> proxiedTypes = new HashSet<>(count); - Iterator> i = candidates.iterator(); - Collection> delegates = filterDelegates ? new ArrayList<>(count) : Collections.emptyList(); - while (i.hasNext()) { - BeanDefinition candidate = i.next(); + for (BeanDefinition candidate : candidates) { if (candidate instanceof ProxyBeanDefinition proxyBeanDefinition) { proxiedTypes.add(proxyBeanDefinition.getTargetDefinitionType()); - } else if (filterDelegates && candidate instanceof BeanDefinitionDelegate delegate) { - i.remove(); - - if (!delegates.contains(delegate)) { - delegates.add(delegate); - } + } else if (candidate instanceof BeanDefinitionDelegate delegate && delegate.getTarget() instanceof ProxyBeanDefinition proxyBeanDefinition) { + proxiedTypes.add(proxyBeanDefinition.getTargetDefinitionType()); } } - if (filterDelegates) { - candidates.addAll(delegates); - } if (!proxiedTypes.isEmpty()) { candidates.removeIf(candidate -> { if (candidate instanceof BeanDefinitionDelegate delegate) { @@ -3564,7 +3599,7 @@ private Collection> findBeanCandidatesInternal(BeanResolut @SuppressWarnings("rawtypes") Collection beanDefinitions = beanCandidateCache.get(beanType); if (beanDefinitions == null) { - beanDefinitions = findBeanCandidates(resolutionContext, beanType, true, null); + beanDefinitions = findBeanCandidates(resolutionContext, beanType, true, true,null); beanCandidateCache.put(beanType, beanDefinitions); } return beanDefinitions; @@ -3767,7 +3802,7 @@ private void addCandidateToList(@Nullable BeanResolutionContext resolutionCo } private boolean isCandidatePresent(Argument beanType, Qualifier qualifier) { - final Collection> candidates = findBeanCandidates(null, beanType, true, null); + final Collection> candidates = findBeanCandidates(null, beanType, true, true, null); if (!candidates.isEmpty()) { filterReplacedBeans(null, candidates); Stream> stream = candidates.stream(); diff --git a/inject/src/main/java/io/micronaut/context/DefaultConditionContext.java b/inject/src/main/java/io/micronaut/context/DefaultConditionContext.java index 7b209f58ccf..b1d1e5ef131 100644 --- a/inject/src/main/java/io/micronaut/context/DefaultConditionContext.java +++ b/inject/src/main/java/io/micronaut/context/DefaultConditionContext.java @@ -172,6 +172,14 @@ public Optional getProperty(@NonNull String name, @NonNull ArgumentConver return Optional.empty(); } + @Override + public Collection> getPropertyPathMatches(String pathPattern) { + if (beanContext instanceof PropertyResolver resolver) { + return resolver.getPropertyPathMatches(pathPattern); + } + return Collections.emptyList(); + } + @Override public Optional findBean(Argument beanType, Qualifier qualifier) { return beanContext.findBean(beanType, qualifier); diff --git a/inject/src/main/java/io/micronaut/context/DefaultMethodConstructorInjectionPoint.java b/inject/src/main/java/io/micronaut/context/DefaultMethodConstructorInjectionPoint.java index 1e724c5b9a4..a8e4ee0b28a 100644 --- a/inject/src/main/java/io/micronaut/context/DefaultMethodConstructorInjectionPoint.java +++ b/inject/src/main/java/io/micronaut/context/DefaultMethodConstructorInjectionPoint.java @@ -49,6 +49,11 @@ class DefaultMethodConstructorInjectionPoint extends DefaultMethodInjectionPo super(declaringBean, declaringType, methodName, arguments, annotationMetadata); } + @Override + public Class getDeclaringBeanType() { + return (Class) declaringType; + } + @Override public T invoke(Object... args) { throw new UnsupportedOperationException("Use MethodInjectionPoint#invoke(..) instead"); diff --git a/inject/src/main/java/io/micronaut/context/DefaultMethodInjectionPoint.java b/inject/src/main/java/io/micronaut/context/DefaultMethodInjectionPoint.java index b09ad2f0365..72c977a1ec6 100644 --- a/inject/src/main/java/io/micronaut/context/DefaultMethodInjectionPoint.java +++ b/inject/src/main/java/io/micronaut/context/DefaultMethodInjectionPoint.java @@ -41,9 +41,9 @@ @Internal class DefaultMethodInjectionPoint implements MethodInjectionPoint, EnvironmentConfigurable { + protected final Class declaringType; private final BeanDefinition declaringBean; private final AnnotationMetadata annotationMetadata; - private final Class declaringType; private final String methodName; private final Class[] argTypes; private final Argument[] arguments; diff --git a/inject/src/main/java/io/micronaut/context/DisabledBean.java b/inject/src/main/java/io/micronaut/context/DisabledBean.java index 074dfb19d4f..d86ab0360f5 100644 --- a/inject/src/main/java/io/micronaut/context/DisabledBean.java +++ b/inject/src/main/java/io/micronaut/context/DisabledBean.java @@ -44,6 +44,11 @@ public boolean isEnabled(BeanContext context, BeanResolutionContext resolutionCo return true; } + @Override + public boolean isConfigurationProperties() { + return BeanDefinition.super.isConfigurationProperties(); + } + @Override public boolean isSingleton() { return BeanDefinition.super.isSingleton(); diff --git a/inject/src/main/java/io/micronaut/context/RuntimeBeanDefinition.java b/inject/src/main/java/io/micronaut/context/RuntimeBeanDefinition.java index c72304c6408..0082f506aec 100644 --- a/inject/src/main/java/io/micronaut/context/RuntimeBeanDefinition.java +++ b/inject/src/main/java/io/micronaut/context/RuntimeBeanDefinition.java @@ -66,6 +66,11 @@ default boolean isContextScope() { return getAnnotationMetadata().hasDeclaredAnnotation(Context.class); } + @Override + default boolean isConfigurationProperties() { + return BeanDefinitionReference.super.isConfigurationProperties(); + } + @Override default BeanDefinition load() { return this; diff --git a/inject/src/main/java/io/micronaut/context/SingletonScope.java b/inject/src/main/java/io/micronaut/context/SingletonScope.java index 88c6c271b08..b344d279f92 100644 --- a/inject/src/main/java/io/micronaut/context/SingletonScope.java +++ b/inject/src/main/java/io/micronaut/context/SingletonScope.java @@ -363,13 +363,12 @@ public boolean equals(Object o) { if (beanDefinitionDelegate.definition.getClass() != that.beanDefinitionDelegate.definition.getClass()) { return false; } - return Objects.equals(beanDefinitionDelegate.getAttributes(), that.beanDefinitionDelegate.getAttributes()) - && Objects.equals(beanDefinitionDelegate.getQualifier(), that.beanDefinitionDelegate.getQualifier()); + return Objects.equals(beanDefinitionDelegate.getDeclaredQualifier(), that.beanDefinitionDelegate.getDeclaredQualifier()); } @Override public int hashCode() { - return beanDefinitionDelegate.definition.hashCode(); + return Objects.hash(beanDefinitionDelegate.getBeanType(), beanDefinitionDelegate.getDeclaredQualifier()); } } diff --git a/inject/src/main/java/io/micronaut/context/env/ConfigurationPath.java b/inject/src/main/java/io/micronaut/context/env/ConfigurationPath.java new file mode 100644 index 00000000000..027de2487c5 --- /dev/null +++ b/inject/src/main/java/io/micronaut/context/env/ConfigurationPath.java @@ -0,0 +1,322 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.context.env; + +import io.micronaut.context.Qualifier; +import io.micronaut.context.annotation.ConfigurationReader; +import io.micronaut.context.annotation.EachProperty; +import io.micronaut.core.annotation.NonNull; +import io.micronaut.core.annotation.Nullable; +import io.micronaut.core.value.PropertyResolver; +import io.micronaut.inject.BeanDefinition; +import io.micronaut.inject.qualifiers.Qualifiers; + +import java.util.function.Consumer; + +/** + * Models a configuration path such as those declared within {@link io.micronaut.context.annotation.ConfigurationProperties} and {@link io.micronaut.context.annotation.EachProperty} declarations. + * + * @since 4.0.0 + * @author graemerocher + */ +public sealed interface ConfigurationPath + extends Iterable + permits DefaultConfigurationPath { + + /** + * @return Creates a new path. + */ + @NonNull + static ConfigurationPath newPath() { + return new DefaultConfigurationPath(); + } + + /** + * Computes the path for the given nested chain of definitions. + * @param definitions The definitions + * @return THe computed path + */ + @NonNull + static ConfigurationPath of(BeanDefinition... definitions) { + ConfigurationPath configurationPath = ConfigurationPath.newPath(); + for (BeanDefinition definition : definitions) { + if (definition.hasDeclaredAnnotation(EachProperty.class)) { + configurationPath.pushEachPropertyRoot(definition); + } else if (definition.hasDeclaredStereotype(ConfigurationReader.class)) { + configurationPath.pushConfigurationReader(definition); + } + } + return configurationPath; + } + + /** + * A configuration can have dynamic segments if it is nested within a {@link io.micronaut.context.annotation.EachProperty} instance. + * + * @return True if the path has any dynamic segments (ie types annotated with {@link io.micronaut.context.annotation.EachProperty} + */ + boolean hasDynamicSegments(); + + /** + * Copy the state the of the path detaching it from any downstream mutations. + * + * @return The copied path + */ + @NonNull + ConfigurationPath copy(); + + /** + * @return The parent of the current path. + */ + @Nullable + ConfigurationPath parent(); + + /** + * Compute the prefix to resolve properties based on the current state of the path and the given prefix. + * + * @return The resolved path + */ + @NonNull + String prefix(); + + /** + * @return The path without segments substituted. + */ + @NonNull + String path(); + + /** + * @return The current primary + */ + @Nullable + String primary(); + + /** + * @return The current kind + */ + @NonNull + ConfigurationSegment.ConfigurationKind kind(); + + /** + * @return The current bound name if any. + */ + @Nullable + String name(); + + /** + * @return The current index or -1 if there is none + */ + int index(); + + /** + * @return The qualifier. + * @param The bean type + */ + @Nullable + default Qualifier beanQualifier() { + String n = name(); + if (n != null) { + return Qualifiers.byName(n); + } + return null; + } + + /** + * @return Is the current binding a list. + */ + default boolean isList() { + ConfigurationSegment segment = peekLast(); + return segment != null && segment.kind() == ConfigurationSegment.ConfigurationKind.LIST; + } + + /** + * @return The last entry. + */ + @Nullable + ConfigurationSegment peekLast(); + + /** + * @return Is the current segment the primary. + */ + default boolean isPrimary() { + ConfigurationSegment segment = peekLast(); + if (segment != null) { + String name = segment.name(); + return name != null && name.equals(primary()); + } + return false; + } + + /** + * Push a new configuration segment for the given name and kind + * + * @param beanDefinition The bean definition + */ + void pushEachPropertyRoot(@NonNull BeanDefinition beanDefinition); + + /** + * Push a new configuration segment for the given name and kind + * + * @param beanDefinition The bean definition + */ + void pushConfigurationReader(@NonNull BeanDefinition beanDefinition); + + /** + * Adds a named segment. + * + * @param name The name of the segment + */ + void pushConfigurationSegment(@NonNull String name); + + /** + * Adds a indexed segment. + * + * @param index The index of the segment + */ + void pushConfigurationSegment(int index); + + /** + * remove last entry. + * + * @throws java.util.NoSuchElementException if there isn't any remaining elements. + */ + @NonNull ConfigurationSegment removeLast(); + + + /** + * @return Whether the path is not empty. + */ + boolean isNotEmpty(); + + /** + * Resolve the given value with the current state. + * + * @param value The value + * @return The resolved value + */ + @NonNull + String resolveValue(String value); + + /** + * @return The current configuration type. + */ + @Nullable + Class configurationType(); + + /** + * @return The simple unqualified name if any. + */ + @Nullable + String simpleName(); + + /** + * Traverse the enabled segments for this path invoking the given callback. + * + * @param propertyResolver The property resolver to use. + * @param callback The callback. + */ + void traverseResolvableSegments( + @NonNull + PropertyResolver propertyResolver, + @NonNull + Consumer callback); + + /** + * Push and adapt an existing configuration segment. + * @param configurationSegment The configuration segment + */ + void pushConfigurationSegment(@NonNull ConfigurationSegment configurationSegment); + + /** + * Check whether the given prefix is within the current path. + * @param prefix The prefix + * @return True if it is within the current path. + */ + boolean isWithin(String prefix); + + /** + * A segment of configuration. + */ + sealed interface ConfigurationSegment extends CharSequence permits DefaultConfigurationPath.DefaultConfigurationSegment { + + /** + * @return The prefix + */ + String prefix(); + + /** + * @return The raw path + */ + String path(); + + /** + * @return Whether it is a list or map binding + */ + ConfigurationKind kind(); + + /** + * @return This name (if any) + */ + @Nullable + String name(); + + /** + * @return The unqualified name. + */ + @Nullable + String simpleName(); + + /** + * @return The primary name (if any) + */ + @Nullable + String primary(); + + /** + * @return The configuration type + */ + @NonNull + Class type(); + + /** + * The current index. + * @return -1 if there is no index + */ + int index(); + + enum ConfigurationKind { + /** + * A root entry. + */ + ROOT, + /** + * Dynamic name. + */ + NAME, + /** + * A dynamic index. + */ + INDEX, + /** + * A list that requires replacement. + */ + LIST, + /** + * A map that requires replacement. + */ + MAP + } + } +} diff --git a/inject/src/main/java/io/micronaut/context/env/DefaultConfigurationPath.java b/inject/src/main/java/io/micronaut/context/env/DefaultConfigurationPath.java new file mode 100644 index 00000000000..0665c5ae9fd --- /dev/null +++ b/inject/src/main/java/io/micronaut/context/env/DefaultConfigurationPath.java @@ -0,0 +1,481 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.context.env; + +import io.micronaut.context.annotation.ConfigurationReader; +import io.micronaut.context.annotation.EachProperty; +import io.micronaut.core.util.StringUtils; +import io.micronaut.core.value.PropertyResolver; +import io.micronaut.inject.BeanDefinition; +import org.jetbrains.annotations.NotNull; + +import java.util.Collection; +import java.util.Collections; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.function.Consumer; + +/** + * Implementation of {@link ConfigurationPath}. + * + * @since 4.0.0 + */ +final class DefaultConfigurationPath implements ConfigurationPath { + private final LinkedList list = new LinkedList<>(); + private String computedPrefix; + private boolean hasDynamicSegments = false; + + DefaultConfigurationPath() { + recomputeState(); + } + + @Override + public boolean hasDynamicSegments() { + return hasDynamicSegments || + kind() == ConfigurationSegment.ConfigurationKind.NAME || + kind() == ConfigurationSegment.ConfigurationKind.INDEX; + } + + @Override + public ConfigurationPath parent() { + int i = list.size(); + if (i > 1) { + DefaultConfigurationPath configurationPath = new DefaultConfigurationPath(); + configurationPath.list.addAll(list.subList(0, i - 1)); + configurationPath.hasDynamicSegments = hasDynamicSegments; + configurationPath.recomputeState(); + return configurationPath; + } + return null; + } + + @Override + public ConfigurationPath copy() { + DefaultConfigurationPath newPath = new DefaultConfigurationPath(); + newPath.list.addAll(this.list); + newPath.computedPrefix = computedPrefix; + newPath.hasDynamicSegments = hasDynamicSegments; + return newPath; + } + + @Override + public String prefix() { + return computedPrefix; + } + + @Override + public String path() { + ConfigurationSegment segment = peekLast(); + if (segment != null) { + return segment.path(); + } else { + return StringUtils.EMPTY_STRING; + } + } + + @Override + public String primary() { + ConfigurationSegment segment = peekLast(); + if (segment != null) { + return segment.primary(); + } + return null; + } + + @Override + public boolean isNotEmpty() { + return !list.isEmpty(); + } + + @Override + public String resolveValue(String value) { + return value.replace(path(), prefix()); + } + + @Override + public Class configurationType() { + ConfigurationSegment segment = peekLast(); + if (segment != null) { + return segment.type(); + } + return null; + } + + @Override + public String name() { + ConfigurationSegment segment = peekLast(); + if (segment != null) { + return segment.name(); + } + return null; + } + + @Override + public int index() { + Iterator i = list.descendingIterator(); + while (i.hasNext()) { + ConfigurationSegment s = i.next(); + if (s.kind() == ConfigurationSegment.ConfigurationKind.INDEX) { + return s.index(); + } + } + return -1; + } + + @Override + public String simpleName() { + ConfigurationSegment segment = peekLast(); + if (segment != null) { + return segment.simpleName(); + } + return null; + } + + @Override + public void traverseResolvableSegments(PropertyResolver propertyResolver, Consumer callback) { + if (hasDynamicSegments) { + // match a path pattern like foo.*.bar.* + Collection> variableValues = propertyResolver.getPropertyPathMatches(path()); + for (List variables : variableValues) { + ConfigurationPath newPath = replaceVariables(variables); + traversePath(newPath, propertyResolver, callback); + } + + } else { + // simple case just traverse entries + traversePath(this, propertyResolver, callback); + } + } + + @SuppressWarnings("java:S1301") + private ConfigurationPath replaceVariables(List variables) { + int varIndex = 0; + DefaultConfigurationPath newPath = new DefaultConfigurationPath(); + newPath.hasDynamicSegments = true; + for (ConfigurationSegment configurationSegment : list) { + switch (configurationSegment.kind()) { + case NAME, INDEX -> { + if (varIndex < variables.size()) { + ConfigurationSegment.ConfigurationKind kind = newPath.kind(); + switch (kind) { + case LIST -> + newPath.pushConfigurationSegment(Integer.parseInt(variables.get(varIndex++))); + case MAP -> + newPath.pushConfigurationSegment(variables.get(varIndex++)); + default -> + newPath.pushConfigurationSegment(configurationSegment); + } + } else { + newPath.pushConfigurationSegment(configurationSegment); + } + } + default -> newPath.pushConfigurationSegment(configurationSegment); + } + } + + return newPath; + } + + private static void traversePath(ConfigurationPath thisPath, PropertyResolver propertyResolver, Consumer callback) { + ConfigurationSegment.ConfigurationKind kind = thisPath.kind(); + switch (kind) { + case MAP -> { + Collection entries = propertyResolver.getPropertyEntries(thisPath.prefix()); + for (String key : entries) { + ConfigurationPath newPath = thisPath.copy(); + newPath.pushConfigurationSegment(key); + callback.accept(newPath); + } + } + case LIST -> { + List entries = propertyResolver.getProperty(thisPath.prefix(), List.class, Collections.emptyList()); + for (int i = 0; i < entries.size(); i++) { + Object o = entries.get(i); + if (o != null) { + ConfigurationPath newPath = thisPath.copy(); + newPath.pushConfigurationSegment(i); + callback.accept(newPath); + } + } + } + case NAME, INDEX -> { + ConfigurationPath parent = thisPath.parent(); + if (parent != null) { + traversePath(parent, propertyResolver, callback); + } + } + default -> { + if (propertyResolver.containsProperties(thisPath.prefix())) { + callback.accept(thisPath); + } + } + } + } + + @Override + public ConfigurationSegment.ConfigurationKind kind() { + ConfigurationSegment segment = peekLast(); + if (segment != null) { + return segment.kind(); + } + return ConfigurationSegment.ConfigurationKind.ROOT; + } + + @Override + public ConfigurationSegment peekLast() { + return list.peekLast(); + } + + @Override + public boolean isWithin(String prefix) { + return prefix != null && prefix.startsWith(path()); + } + + @Override + public void pushEachPropertyRoot(BeanDefinition beanDefinition) { + if (!beanDefinition.getBeanType().equals(configurationType())) { + + if (kind() != ConfigurationSegment.ConfigurationKind.ROOT) { + this.hasDynamicSegments = true; + } + + boolean isList = beanDefinition.booleanValue(EachProperty.class, "list").orElse(false); + String prefix = beanDefinition.stringValue(ConfigurationReader.class, ConfigurationReader.PREFIX).orElse(null); + if (prefix != null) { + String currentPath = path(); + if (!prefix.startsWith(currentPath)) { + throw new IllegalStateException("Invalid configuration properties nesting for path [" + prefix + "]. Expected: " + currentPath); + } + + String resolvedPrefix = prefix; + if (!currentPath.equals(StringUtils.EMPTY_STRING) && !prefix.equals(currentPath)) { + resolvedPrefix = prefix.substring(currentPath.length() + 1); + } + + String property = resolvedPrefix.substring(0, resolvedPrefix.length() - (isList ? 3 : 2)); + String primaryName = beanDefinition.stringValue(EachProperty.class, "primary").orElse(null); + + list.add(new DefaultConfigurationSegment( + beanDefinition.getBeanType(), + property, + prefix, + isList ? ConfigurationSegment.ConfigurationKind.LIST : ConfigurationSegment.ConfigurationKind.MAP, + null, + null, + primaryName, + -1 + )); + recomputeState(); + } + } + } + + @Override + public void pushConfigurationReader(BeanDefinition beanDefinition) { + if (!beanDefinition.getBeanType().equals(configurationType())) { + + if (kind() != ConfigurationSegment.ConfigurationKind.ROOT) { + this.hasDynamicSegments = true; + } + + String prefix = beanDefinition.stringValue(ConfigurationReader.class, ConfigurationReader.PREFIX).orElse(null); + if (prefix != null) { + String currentPath = path(); + if (!prefix.startsWith(currentPath)) { + throw new IllegalStateException("Invalid configuration properties nesting for path [" + prefix + "]. Expected: " + currentPath); + } + String p = prefix.substring(currentPath.length() + 1); + list.add(new DefaultConfigurationSegment( + beanDefinition.getBeanType(), + p, + prefix, + ConfigurationSegment.ConfigurationKind.ROOT, + name(), + simpleName(), + primary(), + -1 + )); + recomputeState(); + } + } + } + + @Override + public void pushConfigurationSegment(ConfigurationSegment configurationSegment) { + ConfigurationSegment.ConfigurationKind kind = configurationSegment.kind(); + switch (kind) { + case NAME -> + pushConfigurationSegment(configurationSegment.name()); + case INDEX -> + pushConfigurationSegment(configurationSegment.index()); + case ROOT -> + list.add(new DefaultConfigurationSegment( + configurationSegment.type(), + configurationSegment.prefix(), + configurationSegment.path(), + ConfigurationSegment.ConfigurationKind.ROOT, + name(), // inherit name + simpleName(), + primary(), // inherit name + index() // inherit the index + )); + default -> + list.add(configurationSegment); + } + + recomputeState(); + } + + @Override + public void pushConfigurationSegment(String name) { + String primary = primary(); + ConfigurationSegment.ConfigurationKind kind = kind(); + String p = switch (kind) { + case MAP -> path(); + case LIST -> + throw new IllegalStateException("Illegal @EachProperty nesting encountered. Lists require numerical entries."); + default -> + throw new IllegalStateException("Illegal @EachProperty nesting, expecting a nested named not another configuration reader or name."); + }; + String qualifiedName = computeName(name); + list.add(new DefaultConfigurationSegment( + configurationType(), + name, + p, + ConfigurationSegment.ConfigurationKind.NAME, + qualifiedName, + name, + primary, + -1 + )); + recomputeState(); + } + + @Override + public void pushConfigurationSegment(int index) { + ConfigurationSegment.ConfigurationKind kind = kind(); + String p = switch (kind) { + case MAP -> + throw new IllegalStateException("Illegal @EachProperty nesting encountered. Maps require key entries."); + case LIST -> path(); + default -> + throw new IllegalStateException("Illegal @EachProperty nesting, expecting a nested named not another configuration reader or name."); + }; + String primary = primary(); + String strIndex = String.valueOf(index); + String qualifiedName = computeName(strIndex); + list.add(new DefaultConfigurationSegment( + configurationType(), + "["+ strIndex + "]", + p, + ConfigurationSegment.ConfigurationKind.INDEX, + qualifiedName, + strIndex, + primary, + index + )); + recomputeState(); + } + + private String computeName(String simpleName) { + String qualifiedName = null; + Iterator i = list.descendingIterator(); + while (i.hasNext()) { + qualifiedName = i.next().name(); + if (qualifiedName != null) { + break; + } + } + if (qualifiedName != null) { + qualifiedName = qualifiedName + "-" + simpleName; + } else { + qualifiedName = simpleName; + } + return qualifiedName; + } + + private void recomputeState() { + StringBuilder str = new StringBuilder(); + Iterator i = list.iterator(); + ConfigurationSegment previous = null; + while (i.hasNext()) { + ConfigurationSegment configurationSegment = i.next(); + if (configurationSegment.kind() == ConfigurationSegment.ConfigurationKind.INDEX) { + str.append('[').append(configurationSegment.index()).append(']'); + } else { + if (previous != null) { + str.append('.'); + } + str.append(configurationSegment); + } + previous = configurationSegment; + } + computedPrefix = str.toString(); + } + + @Override + public ConfigurationSegment removeLast() { + try { + return list.removeLast(); + } finally { + recomputeState(); + } + } + + @Override + public String toString() { + return computedPrefix; + } + + @NotNull + @Override + public Iterator iterator() { + return list.iterator(); + } + + record DefaultConfigurationSegment( + Class type, + String prefix, + String path, + ConfigurationKind kind, + String name, + String simpleName, + String primary, + int index) implements ConfigurationSegment { + + @Override + public int length() { + return prefix.length(); + } + + @Override + public char charAt(int index) { + return prefix.charAt(index); + } + + @NotNull + @Override + public CharSequence subSequence(int start, int end) { + return prefix.subSequence(start, end); + } + + @Override + public String toString() { + return prefix; + } + } +} diff --git a/inject/src/main/java/io/micronaut/context/env/PropertySourcePropertyResolver.java b/inject/src/main/java/io/micronaut/context/env/PropertySourcePropertyResolver.java index e694bf673c4..dda245b39b2 100644 --- a/inject/src/main/java/io/micronaut/context/env/PropertySourcePropertyResolver.java +++ b/inject/src/main/java/io/micronaut/context/env/PropertySourcePropertyResolver.java @@ -43,6 +43,7 @@ import java.util.Collection; import java.util.Collections; import java.util.HashMap; +import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; import java.util.ListIterator; @@ -50,6 +51,7 @@ import java.util.Objects; import java.util.Optional; import java.util.Properties; +import java.util.Set; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; import java.util.function.Consumer; @@ -57,6 +59,7 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; +import java.util.stream.Stream; /** *

A {@link PropertyResolver} that resolves from one or many {@link PropertySource} instances.

@@ -74,9 +77,12 @@ public class PropertySourcePropertyResolver implements PropertyResolver, AutoClo private static final String RANDOM_PREFIX = "\\s?random\\.(\\S+?)"; private static final String RANDOM_UPPER_LIMIT = "(\\(-?\\d+(\\.\\d+)?\\))"; private static final String RANDOM_RANGE = "(\\[-?\\d+(\\.\\d+)?,\\s?-?\\d+(\\.\\d+)?])"; + private static final Pattern RANDOM_PATTERN = Pattern.compile("\\$\\{" + RANDOM_PREFIX + "(" + RANDOM_UPPER_LIMIT + "|" + RANDOM_RANGE + ")?\\}"); + private static final Object NO_VALUE = new Object(); private static final PropertyCatalog[] CONVENTIONS = {PropertyCatalog.GENERATED, PropertyCatalog.RAW}; + private static final String WILD_CARD_SUFFIX = ".*"; protected final ConversionService conversionService; protected final PropertyPlaceholderResolver propertyPlaceholderResolver; protected final Map propertySources = new ConcurrentHashMap<>(10); @@ -220,6 +226,49 @@ public Collection getPropertyEntries(@NonNull String name) { return Collections.emptySet(); } + @Override + public Set> getPropertyPathMatches(String pathPattern) { + if (StringUtils.isNotEmpty(pathPattern)) { + Map entries = resolveEntriesForKey( + pathPattern, false, null); + + if (entries != null) { + boolean endsWithWildCard = pathPattern.endsWith(WILD_CARD_SUFFIX); + String resolvedPattern = pathPattern + .replace("[*]", "\\[([\\w\\d-]+?)\\]") + .replace(".*.", "\\.([\\w\\d-]+?)\\."); + if (endsWithWildCard) { + resolvedPattern = resolvedPattern.replace(WILD_CARD_SUFFIX, "\\S*"); + } else { + resolvedPattern += "\\S*"; + } + Pattern pattern = Pattern.compile(resolvedPattern); + Set keys = entries.keySet(); + Set> results = new HashSet<>(keys.size()); + for (String key : keys) { + Matcher matcher = pattern.matcher(key); + if (matcher.matches()) { + int i = matcher.groupCount(); + if (i > 0) { + if (i == 1) { + results.add(Collections.singletonList(matcher.group(1))); + } else { + List resolved = new ArrayList<>(i); + for (int j = 0; j < i; j++) { + resolved.add(matcher.group(j + 1)); + } + results.add(CollectionUtils.unmodifiableList(resolved)); + } + } + } + } + + return Collections.unmodifiableSet(results); + } + } + return Collections.emptySet(); + } + @Override public @NonNull Map getProperties(String name, StringConvention keyFormat) { if (!StringUtils.isEmpty(name)) { @@ -760,18 +809,11 @@ protected Map resolveEntriesForKey(String name, boolean allowCre private Map[] getCatalog(@Nullable PropertyCatalog propertyCatalog) { propertyCatalog = propertyCatalog != null ? propertyCatalog : PropertyCatalog.GENERATED; - final Map[] catalog; - switch (propertyCatalog) { - case RAW: - catalog = this.rawCatalog; - break; - case NORMALIZED: - catalog = this.nonGenerated; - break; - default: - catalog = this.catalog; - } - return catalog; + return switch (propertyCatalog) { + case RAW -> this.rawCatalog; + case NORMALIZED -> this.nonGenerated; + default -> this.catalog; + }; } /** diff --git a/inject/src/main/java/io/micronaut/context/event/ApplicationEventPublisherFactory.java b/inject/src/main/java/io/micronaut/context/event/ApplicationEventPublisherFactory.java index 21b95da1d3c..fc6c9a64339 100644 --- a/inject/src/main/java/io/micronaut/context/event/ApplicationEventPublisherFactory.java +++ b/inject/src/main/java/io/micronaut/context/event/ApplicationEventPublisherFactory.java @@ -88,6 +88,11 @@ public boolean isCandidateBean(Argument beanType) { return BeanDefinition.super.isCandidateBean(beanType); } + @Override + public boolean isConfigurationProperties() { + return false; + } + @Override public AnnotationMetadata getAnnotationMetadata() { return annotationMetadata; diff --git a/inject/src/main/java/io/micronaut/context/exceptions/MessageUtils.java b/inject/src/main/java/io/micronaut/context/exceptions/MessageUtils.java index 7368feceace..9397f7be27b 100644 --- a/inject/src/main/java/io/micronaut/context/exceptions/MessageUtils.java +++ b/inject/src/main/java/io/micronaut/context/exceptions/MessageUtils.java @@ -17,6 +17,7 @@ import io.micronaut.context.AbstractBeanResolutionContext; import io.micronaut.context.BeanResolutionContext; +import io.micronaut.context.annotation.Factory; import io.micronaut.context.env.CachedEnvironment; import io.micronaut.core.type.Argument; import io.micronaut.inject.BeanDefinition; @@ -95,11 +96,15 @@ static String buildMessage(BeanResolutionContext resolutionContext, String messa static String buildMessageForMethod(BeanResolutionContext resolutionContext, BeanDefinition declaringType, String methodName, Argument argument, String message, boolean circular) { StringBuilder builder = new StringBuilder("Failed to inject value for parameter ["); String ls = CachedEnvironment.getProperty("line.separator"); + String declaringTypeName = declaringType.getName(); + if (declaringType.hasAnnotation(Factory.class)) { + declaringTypeName = declaringType.getConstructor().getDeclaringBeanType().getName(); + } builder .append(argument.getName()).append("] of method [") .append(methodName) .append("] of class: ") - .append(declaringType.getName()) + .append(declaringTypeName) .append(ls) .append(ls); diff --git a/inject/src/main/java/io/micronaut/inject/BeanDefinition.java b/inject/src/main/java/io/micronaut/inject/BeanDefinition.java index 6b93ff296fe..44d29b2cad2 100644 --- a/inject/src/main/java/io/micronaut/inject/BeanDefinition.java +++ b/inject/src/main/java/io/micronaut/inject/BeanDefinition.java @@ -18,10 +18,10 @@ import io.micronaut.context.BeanContext; import io.micronaut.context.BeanResolutionContext; import io.micronaut.context.Qualifier; +import io.micronaut.context.annotation.ConfigurationReader; import io.micronaut.context.annotation.DefaultScope; import io.micronaut.context.annotation.EachBean; import io.micronaut.context.annotation.EachProperty; -import io.micronaut.context.annotation.Provided; import io.micronaut.core.annotation.AnnotationUtil; import io.micronaut.core.annotation.NonNull; import io.micronaut.core.annotation.Nullable; @@ -49,11 +49,6 @@ */ public interface BeanDefinition extends QualifiedBeanType, Named, BeanType, ArgumentCoercible { - /** - * Attribute used to store a dynamic bean name. - */ - String NAMED_ATTRIBUTE = Named.class.getName(); - /** * @return The scope of the bean */ @@ -147,17 +142,6 @@ default boolean isCandidateBean(@Nullable Argument beanType) { return false; } - /** - * @return Is this definition provided by another bean - * @deprecated Provided beans are deprecated - * @see Provided - */ - @SuppressWarnings("DeprecatedIsStillUsed") - @Deprecated(forRemoval = true, since = "2.0.0") - default boolean isProvided() { - return getAnnotationMetadata().hasDeclaredStereotype(Provided.class); - } - /** * @return Whether the bean declared with {@link io.micronaut.context.annotation.EachProperty} or * {@link io.micronaut.context.annotation.EachBean} @@ -166,6 +150,13 @@ default boolean isIterable() { return hasDeclaredStereotype(EachProperty.class) || hasDeclaredStereotype(EachBean.class); } + /** + * @return Is the type configuration properties. + */ + default boolean isConfigurationProperties() { + return isIterable() || hasDeclaredStereotype(ConfigurationReader.class); + } + /** * @return The produced bean type */ diff --git a/inject/src/main/java/io/micronaut/inject/BeanType.java b/inject/src/main/java/io/micronaut/inject/BeanType.java index 585011ca99c..c0e9cc35423 100644 --- a/inject/src/main/java/io/micronaut/inject/BeanType.java +++ b/inject/src/main/java/io/micronaut/inject/BeanType.java @@ -19,15 +19,19 @@ import io.micronaut.context.annotation.Bean; import io.micronaut.core.annotation.AnnotationMetadata; import io.micronaut.core.annotation.AnnotationMetadataProvider; +import io.micronaut.core.annotation.AnnotationUtil; +import io.micronaut.core.annotation.AnnotationValue; import io.micronaut.core.annotation.NonNull; import io.micronaut.core.annotation.Nullable; import io.micronaut.core.beans.BeanInfo; +import io.micronaut.core.naming.NameResolver; import io.micronaut.core.type.Argument; import io.micronaut.core.type.DefaultArgument; import io.micronaut.core.util.ArrayUtils; import io.micronaut.core.util.CollectionUtils; import java.util.Collections; +import java.util.Optional; import java.util.Set; /** @@ -53,13 +57,29 @@ default boolean isPrimary() { */ @NonNull Class getBeanType(); + /** + * Returns the name of the bean usually resolved via the {@link jakarta.inject.Named} annotation. + * @return The name of the bean if any + * @since 4.0.0 + */ + default Optional getBeanName() { + if (this instanceof NameResolver nameResolver) { + return nameResolver.resolveName(); + } + AnnotationMetadata annotationMetadata = getAnnotationMetadata(); + // here we resolved the declared Qualifier of the bean + return annotationMetadata + .findDeclaredAnnotation(AnnotationUtil.NAMED) + .flatMap(AnnotationValue::stringValue); + } + /** * Checks whether the bean type is a container type. * @return Whether the type is a container type like {@link Iterable}. * @since 3.0.0 */ default boolean isContainerType() { - return DefaultArgument.CONTAINER_TYPES.contains(getBeanType()); + return DefaultArgument.CONTAINER_TYPES.contains(getBeanType().getName()); } /** diff --git a/inject/src/main/java/io/micronaut/inject/DelegatingBeanDefinition.java b/inject/src/main/java/io/micronaut/inject/DelegatingBeanDefinition.java index 0dd3733b895..6a786b328d0 100644 --- a/inject/src/main/java/io/micronaut/inject/DelegatingBeanDefinition.java +++ b/inject/src/main/java/io/micronaut/inject/DelegatingBeanDefinition.java @@ -78,11 +78,6 @@ default boolean isSingleton() { return getTarget().isSingleton(); } - @Override - default boolean isProvided() { - return getTarget().isProvided(); - } - @Override default boolean isIterable() { return getTarget().isIterable(); diff --git a/inject/src/main/java/io/micronaut/inject/QualifiedBeanType.java b/inject/src/main/java/io/micronaut/inject/QualifiedBeanType.java index d2aae9e6cf4..6e819387df4 100644 --- a/inject/src/main/java/io/micronaut/inject/QualifiedBeanType.java +++ b/inject/src/main/java/io/micronaut/inject/QualifiedBeanType.java @@ -21,11 +21,13 @@ import io.micronaut.core.annotation.AnnotationUtil; import io.micronaut.core.annotation.AnnotationValue; import io.micronaut.core.annotation.Nullable; +import io.micronaut.core.naming.NameResolver; import io.micronaut.inject.annotation.AnnotationMetadataHierarchy; import io.micronaut.inject.qualifiers.Qualifiers; import java.lang.annotation.Annotation; import java.util.List; +import java.util.Optional; /** * An interface for a {@link BeanType} that allows qualifiers. diff --git a/inject/src/main/java/io/micronaut/inject/provider/AbstractProviderDefinition.java b/inject/src/main/java/io/micronaut/inject/provider/AbstractProviderDefinition.java index fdeb9bf9782..179c93e1cc2 100644 --- a/inject/src/main/java/io/micronaut/inject/provider/AbstractProviderDefinition.java +++ b/inject/src/main/java/io/micronaut/inject/provider/AbstractProviderDefinition.java @@ -193,6 +193,11 @@ public final boolean isSingleton() { return false; } + @Override + public boolean isConfigurationProperties() { + return false; + } + @Override @NonNull public final List> getTypeArguments(Class type) { @@ -206,7 +211,7 @@ public final List> getTypeArguments(Class type) { @NonNull public final List> getTypeArguments() { return Collections.singletonList(TYPE_VARIABLE); - } + } @Override public AnnotationMetadata getAnnotationMetadata() { diff --git a/inject/src/main/java/io/micronaut/inject/qualifiers/Qualifiers.java b/inject/src/main/java/io/micronaut/inject/qualifiers/Qualifiers.java index e091e408ff5..fd76fbd8855 100644 --- a/inject/src/main/java/io/micronaut/inject/qualifiers/Qualifiers.java +++ b/inject/src/main/java/io/micronaut/inject/qualifiers/Qualifiers.java @@ -28,6 +28,7 @@ import io.micronaut.core.annotation.UsedByGeneratedCode; import io.micronaut.core.type.Argument; import io.micronaut.core.util.CollectionUtils; +import io.micronaut.inject.BeanType; import jakarta.inject.Named; import java.lang.annotation.Annotation; @@ -35,6 +36,7 @@ import java.util.List; import java.util.Objects; import java.util.Optional; +import java.util.stream.Stream; /** * Factory for {@link io.micronaut.context.annotation.Bean} qualifiers. @@ -128,6 +130,18 @@ public static Qualifier byName(String name) { return new NameQualifier<>(null, name); } + /** + * Qualify by a prefix. Applies starting with logic to the name of the bean.. + * + * @param prefix The name + * @param The component type + * @return The qualifier + * @since 4.0.0 + */ + public static Qualifier byNamePrefix(String prefix) { + return new PrefixQualifier<>(prefix); + } + /** * Build a qualifier for the given annotation. * @@ -377,4 +391,20 @@ private static Qualifier findCustomByName(@NonNull AnnotationMetadata met return null; } + private record PrefixQualifier(String prefix) implements Qualifier { + @Override + public > Stream reduce(Class beanType, Stream candidates) { + return candidates.filter(candidate -> { + if (!QualifierUtils.matchType(beanType, candidate)) { + return false; + } + if (QualifierUtils.matchAny(beanType, candidate)) { + return true; + } + + String name = candidate.getBeanName().orElse(null); + return name != null && name.startsWith(prefix); + }); + } + } } diff --git a/inject/src/test/groovy/io/micronaut/context/ConfigurationPathSpec.groovy b/inject/src/test/groovy/io/micronaut/context/ConfigurationPathSpec.groovy new file mode 100644 index 00000000000..1c0bedff2ed --- /dev/null +++ b/inject/src/test/groovy/io/micronaut/context/ConfigurationPathSpec.groovy @@ -0,0 +1,154 @@ +package io.micronaut.context + +import io.micronaut.context.annotation.ConfigurationReader +import io.micronaut.context.annotation.EachProperty +import io.micronaut.context.env.ConfigurationPath +import io.micronaut.inject.BeanDefinition +import io.micronaut.inject.annotation.MutableAnnotationMetadata +import io.micronaut.inject.qualifiers.PrimaryQualifier +import io.micronaut.inject.qualifiers.Qualifiers +import spock.lang.Specification + +class ConfigurationPathSpec extends Specification { + + void "test configuration path computation"() { + given: + def bc = BeanContext.build() + + when: + def context = new DefaultBeanResolutionContext(bc, Mock(BeanDefinition)) + ConfigurationPath configurationPath = context.configurationPath + + then: + configurationPath.prefix() == '' + configurationPath.primary() == null + + + when: + def primaryName = 'default' + def eachPropertyPrefix = 'test.*' + + RuntimeBeanDefinition> beanDefinition = + newEachPropertyBean(primaryName, eachPropertyPrefix) + + configurationPath.pushEachPropertyRoot( + beanDefinition + ) + + then: + configurationPath.prefix() == 'test' + configurationPath.path() == eachPropertyPrefix + configurationPath.primary() == primaryName + + when: + configurationPath.pushConfigurationSegment("one") + + then: + configurationPath.prefix() == 'test.one' + configurationPath.path() == eachPropertyPrefix + !configurationPath.isPrimary() + + when: + configurationPath.removeLast() + configurationPath.pushConfigurationSegment("default") + + then: + configurationPath.prefix() == 'test.default' + configurationPath.path() == eachPropertyPrefix + configurationPath.isPrimary() + configurationPath.name() == 'default' + configurationPath.beanQualifier() == Qualifiers.byName('default') + + when: + configurationPath.pushConfigurationReader( + newConfigurationReader("test.*.inner") + ) + + then: + configurationPath.prefix() == 'test.default.inner' + configurationPath.path() == 'test.*.inner' + configurationPath.kind() == ConfigurationPath.ConfigurationSegment.ConfigurationKind.ROOT + configurationPath.name() == 'default' // named inherited from parent + + when: + configurationPath.pushEachPropertyRoot(newEachPropertyBean(null, "test.*.inner.inners.*")) + + then: + configurationPath.kind() == ConfigurationPath.ConfigurationSegment.ConfigurationKind.MAP + configurationPath.prefix() == 'test.default.inner.inners' + configurationPath.path() == 'test.*.inner.inners.*' + + when: + configurationPath.pushConfigurationSegment("other") + + then: + configurationPath.kind() == ConfigurationPath.ConfigurationSegment.ConfigurationKind.NAME + configurationPath.path() == 'test.*.inner.inners.*' + configurationPath.prefix() == 'test.default.inner.inners.other' + configurationPath.name() == 'default-other' + configurationPath.simpleName() == 'other' + configurationPath.beanQualifier() == Qualifiers.byName('default-other') + + when: + configurationPath.removeLast() + + then: + configurationPath.kind() == ConfigurationPath.ConfigurationSegment.ConfigurationKind.MAP + configurationPath.prefix() == 'test.default.inner.inners' + configurationPath.path() == 'test.*.inner.inners.*' + + when: + configurationPath.removeLast() + + then: + configurationPath.prefix() == 'test.default.inner' + configurationPath.path() == 'test.*.inner' + configurationPath.kind() == ConfigurationPath.ConfigurationSegment.ConfigurationKind.ROOT + + when: + configurationPath.pushEachPropertyRoot(newEachPropertyBean(1, "test.*.inner.indexed[*]")) + + then: + configurationPath.path() == "test.*.inner.indexed[*]" + configurationPath.kind() == ConfigurationPath.ConfigurationSegment.ConfigurationKind.LIST + configurationPath.prefix() == 'test.default.inner.indexed' + + when: + configurationPath.pushConfigurationSegment(1) + + then: + configurationPath.path() == "test.*.inner.indexed[*]" + configurationPath.kind() == ConfigurationPath.ConfigurationSegment.ConfigurationKind.INDEX + configurationPath.prefix() == 'test.default.inner.indexed[1]' + + } + + private RuntimeBeanDefinition> newConfigurationReader(String prefix) { + def metadata = new MutableAnnotationMetadata() + metadata.addAnnotation(ConfigurationReader.name, [(ConfigurationReader.PREFIX): prefix]) + return newBeanDef(metadata) + } + + private RuntimeBeanDefinition> newEachPropertyBean(String primaryName, String eachPropertyPrefix) { + def metadata = new MutableAnnotationMetadata() + metadata.addAnnotation(EachProperty.name, [primary: primaryName]) + metadata.addAnnotation(ConfigurationReader.name, [(ConfigurationReader.PREFIX): eachPropertyPrefix]) + return newBeanDef(metadata) + } + + private RuntimeBeanDefinition> newEachPropertyBean(int primaryIndex, String eachPropertyPrefix) { + def metadata = new MutableAnnotationMetadata() + metadata.addAnnotation(EachProperty.name, [primary: primaryIndex, list:true]) + metadata.addAnnotation(ConfigurationReader.name, [(ConfigurationReader.PREFIX): eachPropertyPrefix]) + + return newBeanDef(metadata) + } + + private RuntimeBeanDefinition newBeanDef(MutableAnnotationMetadata metadata) { + def t = new GroovyClassLoader().parseClass("class Dynamic${System.currentTimeMillis()} {}") + def beanDefinition = RuntimeBeanDefinition.builder(t, () -> t.newInstance()) + .annotationMetadata(metadata) + .build() + return beanDefinition + } +} diff --git a/inject/src/test/groovy/io/micronaut/context/env/PropertySourcePropertyResolverSpec.groovy b/inject/src/test/groovy/io/micronaut/context/env/PropertySourcePropertyResolverSpec.groovy index 005710395ac..d32c3e5524f 100644 --- a/inject/src/test/groovy/io/micronaut/context/env/PropertySourcePropertyResolverSpec.groovy +++ b/inject/src/test/groovy/io/micronaut/context/env/PropertySourcePropertyResolverSpec.groovy @@ -37,10 +37,28 @@ import java.util.concurrent.atomic.AtomicBoolean */ class PropertySourcePropertyResolverSpec extends Specification { + @Unroll + void "test resolve property #property matches for pattern #pattern"() { + given: + PropertySourcePropertyResolver resolver = new PropertySourcePropertyResolver( + PropertySource.of("test", [(property): "whatever"], PropertySource.PropertyConvention.ENVIRONMENT_VARIABLE) + ) + + expect: + resolver.getPropertyPathMatches(pattern) == expected + + where: + property | pattern | expected + 'twitter.oauth2.access.token' | "twitter.*.access.token" | [['oauth2']] as Set + 'twitter.oauth2.access.token.stuff' | "twitter.*.access.*.stuff" | [['oauth2', 'token']] as Set + 'twitter.oauth2.access.token.stuff' | "twitter.*.access.*" | [['oauth2']] as Set + 'twitter.oauth2.access[0].stuff' | "twitter.*.access[*].stuff" | [['oauth2', '0']] as Set + } + void "test resolve property entries"() { given: PropertySourcePropertyResolver resolver = new PropertySourcePropertyResolver( - PropertySource.of("test", [DATASOURCE_DEFAULT_URL: 'xxx', DATASOURCE_OTHER_URL:'xxx'], PropertySource.PropertyConvention.ENVIRONMENT_VARIABLE), + PropertySource.of("test", [DATASOURCE_DEFAULT_URL: 'xxx', DATASOURCE_OTHER_URL: 'xxx'], PropertySource.PropertyConvention.ENVIRONMENT_VARIABLE), PropertySource.of("test", ['datasource.third.url': 'xxx'], PropertySource.PropertyConvention.JAVA_PROPERTIES @@ -70,7 +88,7 @@ class PropertySourcePropertyResolverSpec extends Specification { resolver.containsProperties("camel-case") resolver.containsProperties("camelCase") resolver.getProperties("camelCase", StringConvention.RAW) == ['fooBar': 'xxx', - 'URL' : "http://localhost"] + 'URL' : "http://localhost"] resolver.getProperty("camelCase.URL", URL).get() == new URL("http://localhost") } @@ -148,8 +166,8 @@ class PropertySourcePropertyResolverSpec extends Specification { .and("FOO_BAR_1", "foo bar 1") .execute(() -> { resolver.getProperty(key, Object).isPresent() && - resolver.getProperty(key, type).get() == expected && - resolver.containsProperty(key) + resolver.getProperty(key, type).get() == expected && + resolver.containsProperty(key) }) where: @@ -300,11 +318,11 @@ class PropertySourcePropertyResolverSpec extends Specification { void "test random integer placeholders in range for properties"() { given: def values = [ - 'random.integer_lower' : '${random.integer(10)}', - 'random.integer_lower-negative' : '${random.integer(-10)}', - 'random.integer_lower_upper' : '${random.integer[5,10]}', - 'random.integer_lower-negative_upper' : '${random.integer[-5,10]}', - 'random.integer_lower-negative_upper-negative' : '${random.integer[-10,-5]}', + 'random.integer_lower' : '${random.integer(10)}', + 'random.integer_lower-negative' : '${random.integer(-10)}', + 'random.integer_lower_upper' : '${random.integer[5,10]}', + 'random.integer_lower-negative_upper' : '${random.integer[-5,10]}', + 'random.integer_lower-negative_upper-negative': '${random.integer[-10,-5]}', ] PropertySourcePropertyResolver resolver = new PropertySourcePropertyResolver( PropertySource.of("test", values) @@ -339,11 +357,11 @@ class PropertySourcePropertyResolverSpec extends Specification { void "test random long placeholders in range for properties"() { given: def values = [ - 'random.long_lower' : '${random.long(10)}', - 'random.long_lower-negative' : '${random.long(-10)}', - 'random.long_lower_upper' : '${random.long[5,10]}', - 'random.long_lower-negative_upper' : '${random.long[-5,10]}', - 'random.long_lower-negative_upper-negative' : '${random.long[-10,-5]}', + 'random.long_lower' : '${random.long(10)}', + 'random.long_lower-negative' : '${random.long(-10)}', + 'random.long_lower_upper' : '${random.long[5,10]}', + 'random.long_lower-negative_upper' : '${random.long[-5,10]}', + 'random.long_lower-negative_upper-negative': '${random.long[-10,-5]}', ] PropertySourcePropertyResolver resolver = new PropertySourcePropertyResolver( PropertySource.of("test", values) @@ -378,11 +396,11 @@ class PropertySourcePropertyResolverSpec extends Specification { void "test random float placeholders in range for properties"() { given: def values = [ - 'random.float_lower' : '${random.float(10.5)}', - 'random.float_lower-negative' : '${random.float(-10.5)}', - 'random.float_lower_upper' : '${random.float[5.5,10.5]}', - 'random.float_lower-negative_upper' : '${random.float[-5.5,10.5]}', - 'random.float_lower-negative_upper-negative' : '${random.float[-10.5,-5.5]}', + 'random.float_lower' : '${random.float(10.5)}', + 'random.float_lower-negative' : '${random.float(-10.5)}', + 'random.float_lower_upper' : '${random.float[5.5,10.5]}', + 'random.float_lower-negative_upper' : '${random.float[-5.5,10.5]}', + 'random.float_lower-negative_upper-negative': '${random.float[-10.5,-5.5]}', ] PropertySourcePropertyResolver resolver = new PropertySourcePropertyResolver( PropertySource.of("test", values) @@ -417,14 +435,14 @@ class PropertySourcePropertyResolverSpec extends Specification { void "test invalid random Integer range"() { when: def values = [ - 'random.integer' : '${random.integer(9999999999)}' + 'random.integer': '${random.integer(9999999999)}' ] new PropertySourcePropertyResolver( PropertySource.of("test", values) ) then: - def ex= thrown(ValueException) + def ex = thrown(ValueException) ex.message == 'Invalid range: `9999999999` found for type Integer while parsing property: random.integer' ex.cause != null ex.cause instanceof NumberFormatException @@ -433,14 +451,14 @@ class PropertySourcePropertyResolverSpec extends Specification { void "test invalid random Long range"() { when: def values = [ - 'random.long' : '${random.long(9999999999999999999)}' + 'random.long': '${random.long(9999999999999999999)}' ] new PropertySourcePropertyResolver( PropertySource.of("test", values) ) then: - def ex= thrown(ValueException) + def ex = thrown(ValueException) ex.message == 'Invalid range: `9999999999999999999` found for type Long while parsing property: random.long' ex.cause != null ex.cause instanceof NumberFormatException @@ -526,10 +544,10 @@ class PropertySourcePropertyResolverSpec extends Specification { void "test getProperties"() { given: def values = [ - 'foo.bar' : 'two', - 'my.property.one' : 'one', - 'my.property.two' : '${foo.bar}', - 'my.property.three': 'three', + 'foo.bar' : 'two', + 'my.property.one' : 'one', + 'my.property.two' : '${foo.bar}', + 'my.property.three' : 'three', 'test-key.convention-test': 'key', 'FranKen_Ste-in.property' : 'Victor' ] @@ -553,42 +571,42 @@ class PropertySourcePropertyResolverSpec extends Specification { void "test inner properties"() { given: - def values = new HashMap() - values.put('foo[0].bar[0]', 'foo0Bar0') - values.put('foo[0].bar[1]', 'foo0Bar1') - values.put('foo[0].bar[3]', 'foo0Bar2') - values.put('foo[1].bar[abx]', 'foo1Bar0') - values.put('foo[1].bar[xyz]', 'foo1Bar1') - values.put('custom[0][0][key][4]', 'ohh') - values.put('custom[0][0][key][5]', 'ehh') - values.put('custom[0][0][key2]', 'xyz') - values.put('micronaut.security.intercept-url-map[0].access[0]', '/some-path') - values.put('micronaut.security.interceptUrlMap[0].access[1]', '/some-path-x') - - PropertySourcePropertyResolver resolver = new PropertySourcePropertyResolver( - PropertySource.of("test", values) - ) + def values = new HashMap() + values.put('foo[0].bar[0]', 'foo0Bar0') + values.put('foo[0].bar[1]', 'foo0Bar1') + values.put('foo[0].bar[3]', 'foo0Bar2') + values.put('foo[1].bar[abx]', 'foo1Bar0') + values.put('foo[1].bar[xyz]', 'foo1Bar1') + values.put('custom[0][0][key][4]', 'ohh') + values.put('custom[0][0][key][5]', 'ehh') + values.put('custom[0][0][key2]', 'xyz') + values.put('micronaut.security.intercept-url-map[0].access[0]', '/some-path') + values.put('micronaut.security.interceptUrlMap[0].access[1]', '/some-path-x') + + PropertySourcePropertyResolver resolver = new PropertySourcePropertyResolver( + PropertySource.of("test", values) + ) when: - def foos = resolver.getProperty("foo", List).get() - def custom = resolver.getProperty("custom", List).get() - def micronaut = resolver.getProperty("micronaut", Map).get() + def foos = resolver.getProperty("foo", List).get() + def custom = resolver.getProperty("custom", List).get() + def micronaut = resolver.getProperty("micronaut", Map).get() then: - foos.size() == 2 - foos[0].bar.size() == 4 - foos[0].bar[0] == 'foo0Bar0' - foos[0].bar[2] == null - foos[1].bar.size() == 2 - foos[1].bar['abx'] == 'foo1Bar0' - foos[1].bar['xyz'] == 'foo1Bar1' - custom.size() == 1 - custom[0].size() == 1 - custom[0][0].size() == 2 - custom[0][0]['key'].size() == 6 - custom[0][0]['key'][4] == 'ohh' - custom[0][0]['key'][5] == 'ehh' - custom[0][0]['key2'] == 'xyz' - micronaut['security']['intercept-url-map'][0]['access'][0] == '/some-path' - micronaut['security']['intercept-url-map'][0]['access'][1] == '/some-path-x' + foos.size() == 2 + foos[0].bar.size() == 4 + foos[0].bar[0] == 'foo0Bar0' + foos[0].bar[2] == null + foos[1].bar.size() == 2 + foos[1].bar['abx'] == 'foo1Bar0' + foos[1].bar['xyz'] == 'foo1Bar1' + custom.size() == 1 + custom[0].size() == 1 + custom[0][0].size() == 2 + custom[0][0]['key'].size() == 6 + custom[0][0]['key'][4] == 'ohh' + custom[0][0]['key'][5] == 'ehh' + custom[0][0]['key2'] == 'xyz' + micronaut['security']['intercept-url-map'][0]['access'][0] == '/some-path' + micronaut['security']['intercept-url-map'][0]['access'][1] == '/some-path-x' } void "test map and list values are collapsed"() { @@ -596,8 +614,8 @@ class PropertySourcePropertyResolverSpec extends Specification { def values = new HashMap() values.put("foo", [[bar: ['foo0Bar0', 'foo0Bar1', null, 'foo0Bar2']], [bar: [abx: 'foo1Bar0', xyz: 'foo1Bar1']]]) values.put("custom", [[[key: [null, null, null, null, 'ohh', 'ehh'], key2: 'xyz']]]) - values.put("micronaut.security.intercept-url-map", [[access:['/some-path']]]) - values.put("micronaut.security.interceptUrlMap", [[access:[null, '/some-path-x']]]) + values.put("micronaut.security.intercept-url-map", [[access: ['/some-path']]]) + values.put("micronaut.security.interceptUrlMap", [[access: [null, '/some-path-x']]]) PropertySourcePropertyResolver resolver = new PropertySourcePropertyResolver( PropertySource.of("test", values) @@ -645,7 +663,7 @@ class PropertySourcePropertyResolverSpec extends Specification { when: PropertySourcePropertyResolver resolver = new PropertySourcePropertyResolver( - external + external ) then: @@ -654,74 +672,74 @@ class PropertySourcePropertyResolverSpec extends Specification { void "test expression resolver"() { given: - Map parameters = [foo: "bar"] - PropertyResolver mapPropertyResolver = new MapPropertyResolver(parameters) - DefaultPropertyPlaceholderResolver propertyPlaceholderResolver = new DefaultPropertyPlaceholderResolver(mapPropertyResolver, ConversionService.SHARED); - propertyPlaceholderResolver.@expressionResolvers = [new PropertyExpressionResolver() { - @Override - @NonNull - Optional resolve(@NonNull PropertyResolver propertyResolver, - @NonNull ConversionService conversionService, - @NonNull String expression, - @NonNull Class requiredType) { - assert requiredType == String.class - assert conversionService - if ("foobar" == expression) { - return Optional.of("ABC") - } - if ("xyz" == expression) { - return Optional.of("123") - } - if ("external" == expression) { - return propertyResolver.get("foo", requiredType) - } - return Optional.empty() + Map parameters = [foo: "bar"] + PropertyResolver mapPropertyResolver = new MapPropertyResolver(parameters) + DefaultPropertyPlaceholderResolver propertyPlaceholderResolver = new DefaultPropertyPlaceholderResolver(mapPropertyResolver, ConversionService.SHARED); + propertyPlaceholderResolver.@expressionResolvers = [new PropertyExpressionResolver() { + @Override + @NonNull + Optional resolve(@NonNull PropertyResolver propertyResolver, + @NonNull ConversionService conversionService, + @NonNull String expression, + @NonNull Class requiredType) { + assert requiredType == String.class + assert conversionService + if ("foobar" == expression) { + return Optional.of("ABC") } - }] + if ("xyz" == expression) { + return Optional.of("123") + } + if ("external" == expression) { + return propertyResolver.get("foo", requiredType) + } + return Optional.empty() + } + }] expect: - Optional resolved = propertyPlaceholderResolver.resolvePlaceholders(template) - if (result) { - assert resolved.isPresent() - assert resolved.get() == result - } else { - assert !resolved.isPresent() - } + Optional resolved = propertyPlaceholderResolver.resolvePlaceholders(template) + if (result) { + assert resolved.isPresent() + assert resolved.get() == result + } else { + assert !resolved.isPresent() + } where: - template | result - 'Hello ${foo}!' | "Hello bar!" - 'Hello ${foobar}!' | "Hello ABC!" - 'Hello ${xyz}!' | "Hello 123!" - 'Hello ${external}!' | "Hello bar!" - 'Hello ${lol}!' | null + template | result + 'Hello ${foo}!' | "Hello bar!" + 'Hello ${foobar}!' | "Hello ABC!" + 'Hello ${xyz}!' | "Hello 123!" + 'Hello ${external}!' | "Hello bar!" + 'Hello ${lol}!' | null } void "test expression resolver is closed"() { given: - AtomicBoolean closed = new AtomicBoolean() - Map parameters = [foo: "bar"] - PropertyResolver mapPropertyResolver = new MapPropertyResolver(parameters) - DefaultPropertyPlaceholderResolver propertyPlaceholderResolver = new DefaultPropertyPlaceholderResolver(mapPropertyResolver, ConversionService.SHARED); - propertyPlaceholderResolver.@expressionResolvers = [new PropertyExpressionResolverAutoCloseable() { - @Override - @NonNull - Optional resolve(@NonNull PropertyResolver propertyResolver, - @NonNull ConversionService conversionService, - @NonNull String expression, - @NonNull Class requiredType) { - Optional.empty() - } + AtomicBoolean closed = new AtomicBoolean() + Map parameters = [foo: "bar"] + PropertyResolver mapPropertyResolver = new MapPropertyResolver(parameters) + DefaultPropertyPlaceholderResolver propertyPlaceholderResolver = new DefaultPropertyPlaceholderResolver(mapPropertyResolver, ConversionService.SHARED); + propertyPlaceholderResolver.@expressionResolvers = [new PropertyExpressionResolverAutoCloseable() { + @Override + @NonNull + Optional resolve(@NonNull PropertyResolver propertyResolver, + @NonNull ConversionService conversionService, + @NonNull String expression, + @NonNull Class requiredType) { + Optional.empty() + } - @Override - void close() throws Exception { - closed.set(true) - } - }] + @Override + void close() throws Exception { + closed.set(true) + } + }] when: - !closed.get() - propertyPlaceholderResolver.close() + !closed.get() + propertyPlaceholderResolver.close() then: - closed.get() + closed.get() } interface PropertyExpressionResolverAutoCloseable extends PropertyExpressionResolver, AutoCloseable { From 1fd3cebdd7e5fab4fe9535d358174643751c13c8 Mon Sep 17 00:00:00 2001 From: Graeme Rocher Date: Thu, 1 Dec 2022 11:19:57 +0100 Subject: [PATCH 280/743] Eliminate unused proxy targets and simplify resolution (#8447) --- .../micronaut/aop/writer/AopProxyWriter.java | 5 +- .../writer/BeanDefinitionReferenceWriter.java | 13 +- .../inject/writer/BeanDefinitionVisitor.java | 20 +++ .../inject/writer/BeanDefinitionWriter.java | 22 +++ ...uctionWithAroundOnConcreteClassSpec.groovy | 14 +- .../with_around/ProxyAdviceInterceptor.groovy | 2 +- ...uctionWithAroundOnConcreteClassSpec.groovy | 14 +- .../with_around/ProxyAdviceInterceptor.java | 2 +- .../beans/ContextScopedInterceptedBean.java | 7 +- .../inject/beans/InterceptedBean.java | 5 +- .../micronaut/inject/beans/ParallelBean.java | 5 +- ...tInitializableBeanDefinitionReference.java | 36 +++++ .../context/DefaultApplicationContext.java | 4 +- .../micronaut/context/DefaultBeanContext.java | 129 ++++++------------ .../io/micronaut/context/ProxyTarget.java | 35 ----- .../inject/BeanDefinitionReference.java | 18 +++ 16 files changed, 176 insertions(+), 155 deletions(-) delete mode 100644 inject/src/main/java/io/micronaut/context/ProxyTarget.java diff --git a/core-processor/src/main/java/io/micronaut/aop/writer/AopProxyWriter.java b/core-processor/src/main/java/io/micronaut/aop/writer/AopProxyWriter.java index 7487bc464b1..82c304f1c97 100644 --- a/core-processor/src/main/java/io/micronaut/aop/writer/AopProxyWriter.java +++ b/core-processor/src/main/java/io/micronaut/aop/writer/AopProxyWriter.java @@ -214,6 +214,7 @@ public AopProxyWriter(BeanDefinitionWriter parent, this.implementInterface = true; this.parentWriter = parent; this.isProxyTarget = settings.get(Interceptor.PROXY_TARGET).orElse(false) || parent.isInterface(); + parent.setProxiedBean(true, isProxyTarget); this.hotswap = isProxyTarget && settings.get(Interceptor.HOTSWAP).orElse(false); this.lazy = isProxyTarget && settings.get(Interceptor.LAZY).orElse(false); this.cacheLazyTarget = lazy && settings.get(Interceptor.CACHEABLE_LAZY_TARGET).orElse(false); @@ -333,8 +334,9 @@ public boolean isEnabled() { * * @return True if the target bean is being proxied */ + @Override public boolean isProxyTarget() { - return isProxyTarget; + return false; } @Override @@ -726,6 +728,7 @@ public void visitBeanDefinitionEnd() { processAlreadyVisitedMethods(parentWriter); } + this.proxyBeanDefinitionWriter.setRequiresMethodProcessing(parentWriter != null && parentWriter.requiresMethodProcessing()); interceptorParameter.annotate(AnnotationUtil. ANN_INTERCEPTOR_BINDING_QUALIFIER, builder -> { final AnnotationValue[] interceptorBinding = this.interceptorBinding.toArray(new AnnotationValue[0]); builder.values(interceptorBinding); diff --git a/core-processor/src/main/java/io/micronaut/inject/writer/BeanDefinitionReferenceWriter.java b/core-processor/src/main/java/io/micronaut/inject/writer/BeanDefinitionReferenceWriter.java index f2e8e28fdbc..867878056af 100644 --- a/core-processor/src/main/java/io/micronaut/inject/writer/BeanDefinitionReferenceWriter.java +++ b/core-processor/src/main/java/io/micronaut/inject/writer/BeanDefinitionReferenceWriter.java @@ -70,7 +70,9 @@ public class BeanDefinitionReferenceWriter extends AbstractAnnotationMetadataWri boolean.class, // isSingleton boolean.class, // isConfigurationProperties boolean.class, // hasExposedTypes - boolean.class // requiresMethodProcessing + boolean.class, // requiresMethodProcessing + boolean.class, // isProxiedBean + boolean.class // isProxyTarget )); private final String beanTypeName; @@ -80,6 +82,8 @@ public class BeanDefinitionReferenceWriter extends AbstractAnnotationMetadataWri private final Type interceptedType; private final Type providedType; private final Map typeParameters; + private final boolean proxiedBean; + private final boolean proxyTarget; private boolean contextScope = false; private boolean requiresMethodProcessing; @@ -101,6 +105,8 @@ public BeanDefinitionReferenceWriter(BeanDefinitionVisitor visitor) { this.beanDefinitionReferenceClassName = beanDefinitionName + REF_SUFFIX; this.beanDefinitionClassInternalName = getInternalName(beanDefinitionName) + REF_SUFFIX; this.interceptedType = visitor.getInterceptedType().orElse(null); + this.proxiedBean = visitor.isProxiedBean(); + this.proxyTarget = visitor.isProxyTarget(); } /** @@ -216,6 +222,11 @@ private ClassWriter generateClassBytes() { ); // 10: requiresMethodProcessing cv.push(requiresMethodProcessing); + // 11: isProxiedBean + cv.push(proxiedBean); + // 12: isProxiedBean + cv.push(proxyTarget); + // (...) cv.invokeConstructor( Type.getType(AbstractInitializableBeanDefinitionReference.class), diff --git a/core-processor/src/main/java/io/micronaut/inject/writer/BeanDefinitionVisitor.java b/core-processor/src/main/java/io/micronaut/inject/writer/BeanDefinitionVisitor.java index 59d2049e83f..4db6bb10595 100644 --- a/core-processor/src/main/java/io/micronaut/inject/writer/BeanDefinitionVisitor.java +++ b/core-processor/src/main/java/io/micronaut/inject/writer/BeanDefinitionVisitor.java @@ -439,4 +439,24 @@ default boolean requiresMethodProcessing() { default @NonNull ClassElement[] getTypeArguments() { return new ClassElement[0]; } + + /** + * Returns whether another bean exists that proxies this bean. In other words + * this bean is the target of a proxy. + * + * @return Is the reference a proxy target. + * @since 4.0.0 + */ + default boolean isProxiedBean() { + return false; + } + + /** + * Returns whether another bean is a proxy target that needs to be retained. + * + * @return Is the reference a proxy target. + * @since 4.0.0 + */ + boolean isProxyTarget(); + } diff --git a/core-processor/src/main/java/io/micronaut/inject/writer/BeanDefinitionWriter.java b/core-processor/src/main/java/io/micronaut/inject/writer/BeanDefinitionWriter.java index 78a3662c325..d49394bfda6 100644 --- a/core-processor/src/main/java/io/micronaut/inject/writer/BeanDefinitionWriter.java +++ b/core-processor/src/main/java/io/micronaut/inject/writer/BeanDefinitionWriter.java @@ -554,6 +554,8 @@ public class BeanDefinitionWriter extends AbstractClassFileWriter implements Bea private boolean disabled = false; private final boolean keepConfPropInjectPoints; + private boolean proxiedBean = false; + private boolean isProxyTarget = false; /** * Creates a bean definition writer. @@ -4493,6 +4495,26 @@ public Element[] getOriginatingElements() { return this.originatingElements.getOriginatingElements(); } + /** + * Sets whether this bean is a proxied type. + * @param proxiedBean True if it proxied + * @param isProxyTarget True if the proxied bean is a retained target + */ + public void setProxiedBean(boolean proxiedBean, boolean isProxyTarget) { + this.proxiedBean = proxiedBean; + this.isProxyTarget = isProxyTarget; + } + + @Override + public boolean isProxyTarget() { + return isProxyTarget; + } + + @Override + public boolean isProxiedBean() { + return proxiedBean; + } + @Internal private static final class AnnotationVisitData { final TypedElement memberBeanType; diff --git a/inject-groovy/src/test/groovy/io/micronaut/aop/introduction/with_around/IntroductionWithAroundOnConcreteClassSpec.groovy b/inject-groovy/src/test/groovy/io/micronaut/aop/introduction/with_around/IntroductionWithAroundOnConcreteClassSpec.groovy index ed069ae0ff0..dee74263102 100644 --- a/inject-groovy/src/test/groovy/io/micronaut/aop/introduction/with_around/IntroductionWithAroundOnConcreteClassSpec.groovy +++ b/inject-groovy/src/test/groovy/io/micronaut/aop/introduction/with_around/IntroductionWithAroundOnConcreteClassSpec.groovy @@ -35,9 +35,8 @@ class Test {} @Unroll void "test introduction with around for #clazz.simpleName"(Class clazz) { when: - def proxyTargetBeanDefinition = applicationContext.getProxyTargetBeanDefinition(clazz, null) def beanDefinition = applicationContext.getBeanDefinition(clazz, null) - def bean = applicationContext.getBean(proxyTargetBeanDefinition) + def bean = applicationContext.getBean(beanDefinition) then: bean instanceof CustomProxy @@ -46,7 +45,7 @@ class Test {} bean.getName() == null when: - bean = applicationContext.getProxyTargetBean(clazz, null) + bean = applicationContext.getBean(clazz, null) then: bean instanceof CustomProxy @@ -56,7 +55,6 @@ class Test {} and: beanDefinition.getExecutableMethods().size() == 5 - proxyTargetBeanDefinition.getExecutableMethods().size() == 5 where: clazz << [MyBean1, MyBean2, MyBean3, MyBean4, MyBean5, MyBean6] @@ -75,21 +73,17 @@ class Test {} void "test executable methods count for introduction with executable"() { when: def clazz = MyBean7.class - def proxyTargetBeanDefinition = applicationContext.getProxyTargetBeanDefinition(clazz, null) def beanDefinition = applicationContext.getBeanDefinition(clazz, null) then: - proxyTargetBeanDefinition.getExecutableMethods().size() == 1 beanDefinition.getExecutableMethods().size() == 1 } void "test executable methods count for around with executable"() { when: def clazz = MyBean8.class - def proxyTargetBeanDefinition = applicationContext.getProxyTargetBeanDefinition(clazz, null) def beanDefinition = applicationContext.getBeanDefinition(clazz, null) then: - proxyTargetBeanDefinition.getExecutableMethods().size() == 4 beanDefinition.getExecutableMethods().size() == 4 when: @@ -102,11 +96,9 @@ class Test {} void "test a multidimensional array property"() { when: def clazz = MyBean9.class - def proxyTargetBeanDefinition = applicationContext.getProxyTargetBeanDefinition(clazz, null) def beanDefinition = applicationContext.getBeanDefinition(clazz, null) then: - proxyTargetBeanDefinition.getExecutableMethods().size() == 5 beanDefinition.getExecutableMethods().size() == 5 beanDefinition.findMethod("getMultidim").get().getReturnType().asArgument().getType() == String[][].class beanDefinition.findMethod("setMultidim", String[][].class).isPresent() @@ -114,7 +106,7 @@ class Test {} beanDefinition.findMethod("setPrimitiveMultidim", int[][].class).isPresent() when: - MyBean9 bean = applicationContext.getBean(proxyTargetBeanDefinition) + MyBean9 bean = applicationContext.getBean(beanDefinition) ExecutableMethod getMultiDim = beanDefinition.findMethod('getMultidim').get() ExecutableMethod setMultiDim = beanDefinition.findMethod('setMultidim', String[][].class).get() ExecutableMethod getPrimitiveMultidim = beanDefinition.findMethod('getPrimitiveMultidim').get() diff --git a/inject-groovy/src/test/groovy/io/micronaut/aop/introduction/with_around/ProxyAdviceInterceptor.groovy b/inject-groovy/src/test/groovy/io/micronaut/aop/introduction/with_around/ProxyAdviceInterceptor.groovy index 50e6a83adf6..309d9f5d263 100644 --- a/inject-groovy/src/test/groovy/io/micronaut/aop/introduction/with_around/ProxyAdviceInterceptor.groovy +++ b/inject-groovy/src/test/groovy/io/micronaut/aop/introduction/with_around/ProxyAdviceInterceptor.groovy @@ -28,7 +28,7 @@ class ProxyAdviceInterceptor implements MethodInterceptor { return context.getExecutableMethod().invoke(delegate, context.getParameterValues()); } else if (context.getTarget() instanceof MyBean6) { try { - ExecutableMethod proxyTargetMethod = beanContext.getProxyTargetMethod(MyBean6.class, context.getMethodName(), context.getArgumentTypes()) + ExecutableMethod proxyTargetMethod = beanContext.getExecutableMethod(MyBean6.class, context.getMethodName(), context.getArgumentTypes()) MyBean6 delegate = new MyBean6() delegate.setId(1L) return proxyTargetMethod.invoke(delegate, context.getParameterValues()); diff --git a/inject-java/src/test/groovy/io/micronaut/aop/introduction/with_around/IntroductionWithAroundOnConcreteClassSpec.groovy b/inject-java/src/test/groovy/io/micronaut/aop/introduction/with_around/IntroductionWithAroundOnConcreteClassSpec.groovy index 9390699379a..8a8e7df8875 100644 --- a/inject-java/src/test/groovy/io/micronaut/aop/introduction/with_around/IntroductionWithAroundOnConcreteClassSpec.groovy +++ b/inject-java/src/test/groovy/io/micronaut/aop/introduction/with_around/IntroductionWithAroundOnConcreteClassSpec.groovy @@ -36,9 +36,8 @@ class Test{} @Unroll void "test introduction with around for #clazz"(Class clazz) { when: - def proxyTargetBeanDefinition = applicationContext.getProxyTargetBeanDefinition(clazz, null) def beanDefinition = applicationContext.getBeanDefinition(clazz, null) - def bean = applicationContext.getBean(proxyTargetBeanDefinition) + def bean = applicationContext.getBean(beanDefinition) then: bean instanceof CustomProxy @@ -47,7 +46,7 @@ class Test{} bean.getName() == null when: - bean = applicationContext.getProxyTargetBean(clazz, null) + bean = applicationContext.getBean(clazz, null) then: bean instanceof CustomProxy @@ -57,7 +56,6 @@ class Test{} and: beanDefinition.getExecutableMethods().size() == 5 - proxyTargetBeanDefinition.getExecutableMethods().size() == 5 where: clazz << [MyBean1, MyBean2, MyBean3, MyBean4, MyBean5, MyBean6] @@ -76,21 +74,17 @@ class Test{} void "test executable methods count for introduction with executable"() { when: def clazz = MyBean7.class - def proxyTargetBeanDefinition = applicationContext.getProxyTargetBeanDefinition(clazz, null) def beanDefinition = applicationContext.getBeanDefinition(clazz, null) then: - proxyTargetBeanDefinition.getExecutableMethods().size() == 5 beanDefinition.getExecutableMethods().size() == 5 } void "test executable methods count for around with executable"() { when: def clazz = MyBean8.class - def proxyTargetBeanDefinition = applicationContext.getProxyTargetBeanDefinition(clazz, null) def beanDefinition = applicationContext.getBeanDefinition(clazz, null) then: - proxyTargetBeanDefinition.getExecutableMethods().size() == 4 beanDefinition.getExecutableMethods().size() == 4 when: @@ -103,11 +97,9 @@ class Test{} void "test a multidimensional array property"() { when: def clazz = MyBean9.class - def proxyTargetBeanDefinition = applicationContext.getProxyTargetBeanDefinition(clazz, null) def beanDefinition = applicationContext.getBeanDefinition(clazz, null) then: - proxyTargetBeanDefinition.getExecutableMethods().size() == 5 beanDefinition.getExecutableMethods().size() == 5 beanDefinition.findMethod("getMultidim").get().getReturnType().asArgument().getType() == String[][].class beanDefinition.findMethod("setMultidim", String[][].class).isPresent() @@ -115,7 +107,7 @@ class Test{} beanDefinition.findMethod("setPrimitiveMultidim", int[][].class).isPresent() when: - MyBean9 bean = applicationContext.getBean(proxyTargetBeanDefinition) + MyBean9 bean = applicationContext.getBean(beanDefinition) ExecutableMethod getMultiDim = beanDefinition.findMethod('getMultidim').get() ExecutableMethod setMultiDim = beanDefinition.findMethod('setMultidim', String[][].class).get() ExecutableMethod getPrimitiveMultidim = beanDefinition.findMethod('getPrimitiveMultidim').get() diff --git a/inject-java/src/test/groovy/io/micronaut/aop/introduction/with_around/ProxyAdviceInterceptor.java b/inject-java/src/test/groovy/io/micronaut/aop/introduction/with_around/ProxyAdviceInterceptor.java index 774451b8b9f..2640585405f 100644 --- a/inject-java/src/test/groovy/io/micronaut/aop/introduction/with_around/ProxyAdviceInterceptor.java +++ b/inject-java/src/test/groovy/io/micronaut/aop/introduction/with_around/ProxyAdviceInterceptor.java @@ -43,7 +43,7 @@ public Object intercept(MethodInvocationContext context) { return context.getExecutableMethod().invoke(delegate, context.getParameterValues()); } else if (context.getTarget() instanceof MyBean6) { try { - ExecutableMethod proxyTargetMethod = beanContext.getProxyTargetMethod(MyBean6.class, context.getMethodName(), context.getArgumentTypes()); + ExecutableMethod proxyTargetMethod = beanContext.getExecutableMethod(MyBean6.class, context.getMethodName(), context.getArgumentTypes()); MyBean6 delegate = new MyBean6(); delegate.setId(1L); return proxyTargetMethod.invoke(delegate, context.getParameterValues()); diff --git a/inject-java/src/test/groovy/io/micronaut/inject/beans/ContextScopedInterceptedBean.java b/inject-java/src/test/groovy/io/micronaut/inject/beans/ContextScopedInterceptedBean.java index 82f56761736..a2009b8a7e8 100644 --- a/inject-java/src/test/groovy/io/micronaut/inject/beans/ContextScopedInterceptedBean.java +++ b/inject-java/src/test/groovy/io/micronaut/inject/beans/ContextScopedInterceptedBean.java @@ -15,14 +15,15 @@ */ package io.micronaut.inject.beans; +import io.micronaut.aop.proxytarget.Mutating; import io.micronaut.context.annotation.Context; import io.micronaut.scheduling.annotation.Async; @Context public class ContextScopedInterceptedBean { - @Async - void doSomething() { + @Mutating("name") + void doSomething(String name) { } -} \ No newline at end of file +} diff --git a/inject-java/src/test/groovy/io/micronaut/inject/beans/InterceptedBean.java b/inject-java/src/test/groovy/io/micronaut/inject/beans/InterceptedBean.java index 9e63f89b7d9..d1edb0162e1 100644 --- a/inject-java/src/test/groovy/io/micronaut/inject/beans/InterceptedBean.java +++ b/inject-java/src/test/groovy/io/micronaut/inject/beans/InterceptedBean.java @@ -15,6 +15,7 @@ */ package io.micronaut.inject.beans; +import io.micronaut.aop.proxytarget.Mutating; import io.micronaut.scheduling.annotation.Async; import jakarta.inject.Singleton; @@ -22,8 +23,8 @@ @Singleton public class InterceptedBean { - @Async - void doSomething() { + @Mutating("name") + void doSomething(String name) { } } diff --git a/inject-java/src/test/groovy/io/micronaut/inject/beans/ParallelBean.java b/inject-java/src/test/groovy/io/micronaut/inject/beans/ParallelBean.java index aa1cff8dabb..867ac538217 100644 --- a/inject-java/src/test/groovy/io/micronaut/inject/beans/ParallelBean.java +++ b/inject-java/src/test/groovy/io/micronaut/inject/beans/ParallelBean.java @@ -15,6 +15,7 @@ */ package io.micronaut.inject.beans; +import io.micronaut.aop.proxytarget.Mutating; import io.micronaut.context.annotation.Parallel; import io.micronaut.scheduling.annotation.Async; @@ -24,8 +25,8 @@ @Parallel public class ParallelBean { - @Async - void doSomething() { + @Mutating("test") + void doSomething(String test) { } } diff --git a/inject/src/main/java/io/micronaut/context/AbstractInitializableBeanDefinitionReference.java b/inject/src/main/java/io/micronaut/context/AbstractInitializableBeanDefinitionReference.java index afebf30c2ce..a11afc7532a 100644 --- a/inject/src/main/java/io/micronaut/context/AbstractInitializableBeanDefinitionReference.java +++ b/inject/src/main/java/io/micronaut/context/AbstractInitializableBeanDefinitionReference.java @@ -46,6 +46,8 @@ public abstract class AbstractInitializableBeanDefinitionReference extends Ab private final boolean isConfigurationProperties; private final boolean hasExposedTypes; private final boolean requiresMethodProcessing; + private final boolean isProxiedBean; + private final boolean isProxyTarget; private Boolean present; private Set> exposedTypes; @@ -67,6 +69,28 @@ public AbstractInitializableBeanDefinitionReference(String beanTypeName, String boolean isPrimary, boolean isContextScope, boolean isConditional, boolean isContainerType, boolean isSingleton, boolean isConfigurationProperties, boolean hasExposedTypes, boolean requiresMethodProcessing) { + this(beanTypeName, beanDefinitionTypeName, annotationMetadata, isPrimary, isContextScope, isConditional, isContainerType, isSingleton, isConfigurationProperties, hasExposedTypes, requiresMethodProcessing, false, false); + } + + /** + * @param beanTypeName The bean type name + * @param beanDefinitionTypeName The bean definition type name + * @param annotationMetadata The annotationMetadata + * @param isPrimary Is primary bean? + * @param isContextScope Is context scope? + * @param isConditional Is conditional? = No @Requires + * @param isContainerType Is container type? + * @param isSingleton Is singleton? + * @param isConfigurationProperties Is configuration properties? + * @param hasExposedTypes Has exposed types? + * @param requiresMethodProcessing Is requires method processing? + * @param isProxiedBean Is the bean proxied + * @param isProxyTarget Is the bean a retained proxy target + */ + protected AbstractInitializableBeanDefinitionReference(String beanTypeName, String beanDefinitionTypeName, AnnotationMetadata annotationMetadata, + boolean isPrimary, boolean isContextScope, boolean isConditional, + boolean isContainerType, boolean isSingleton, boolean isConfigurationProperties, + boolean hasExposedTypes, boolean requiresMethodProcessing, boolean isProxiedBean, boolean isProxyTarget) { this.beanTypeName = beanTypeName; this.beanDefinitionTypeName = beanDefinitionTypeName; this.annotationMetadata = annotationMetadata; @@ -78,6 +102,18 @@ public AbstractInitializableBeanDefinitionReference(String beanTypeName, String this.isConfigurationProperties = isConfigurationProperties; this.hasExposedTypes = hasExposedTypes; this.requiresMethodProcessing = requiresMethodProcessing; + this.isProxiedBean = isProxiedBean; + this.isProxyTarget = isProxyTarget; + } + + @Override + public boolean isProxyTarget() { + return isProxyTarget; + } + + @Override + public boolean isProxiedBean() { + return isProxiedBean; } @Override diff --git a/inject/src/main/java/io/micronaut/context/DefaultApplicationContext.java b/inject/src/main/java/io/micronaut/context/DefaultApplicationContext.java index 10b2f83b219..b905fd50292 100644 --- a/inject/src/main/java/io/micronaut/context/DefaultApplicationContext.java +++ b/inject/src/main/java/io/micronaut/context/DefaultApplicationContext.java @@ -328,7 +328,7 @@ private String resolveEachBeanMissingMessage(BeanResolutionContext resolutio @Nullable private BeanDefinition findAnyBeanDefinition(BeanResolutionContext resolutionContext, Argument beanType) { - Collection> existing = super.findBeanCandidates(resolutionContext, beanType, true, false, definition -> !definition.isAbstract()); + Collection> existing = super.findBeanCandidates(resolutionContext, beanType, false, definition -> !definition.isAbstract()); BeanDefinition definition = null; if (existing.size() == 1) { definition = existing.iterator().next(); @@ -494,7 +494,7 @@ private void transformEachBeanBeanDefinition(@NonNull BeanResolutionContext return; } - Collection dependentCandidates = findBeanCandidates(resolutionContext, Argument.of(dependentType), true, true, null); + Collection dependentCandidates = findBeanCandidates(resolutionContext, Argument.of(dependentType), true, null); if (!dependentCandidates.isEmpty()) { for (BeanDefinition dependentCandidate : dependentCandidates) { diff --git a/inject/src/main/java/io/micronaut/context/DefaultBeanContext.java b/inject/src/main/java/io/micronaut/context/DefaultBeanContext.java index 98cb23096b4..50c9661dade 100644 --- a/inject/src/main/java/io/micronaut/context/DefaultBeanContext.java +++ b/inject/src/main/java/io/micronaut/context/DefaultBeanContext.java @@ -197,6 +197,7 @@ public > Stream reduce(Class beanType, Str private final BeanContextConfiguration beanContextConfiguration; private final Collection beanDefinitionsClasses = new ConcurrentLinkedQueue<>(); + private final Collection proxyTargetBeans = new ConcurrentLinkedQueue<>(); private final Map, BeanDefinitionReference> disabledBeans = new ConcurrentHashMap<>(20); private final Map> disabledConfigurations = new ConcurrentHashMap<>(5); @@ -209,6 +210,9 @@ public > Stream reduce(Class beanType, Str private final Map> beanConcreteCandidateCache = new ConcurrentLinkedHashMap.Builder>().maximumWeightedCapacity(30).build(); + private final Map> beanProxyTargetCache = + new ConcurrentLinkedHashMap.Builder>().maximumWeightedCapacity(30).build(); + private final Map> beanCandidateCache = new ConcurrentLinkedHashMap.Builder>().maximumWeightedCapacity(30).build(); private final Map, Collection> beanIndex = new ConcurrentHashMap<>(12); @@ -1580,24 +1584,12 @@ public Optional> findProxyTargetBeanDefinition(@NonNull Cl @SuppressWarnings("java:S2789") // performance optimization public Optional> findProxyTargetBeanDefinition(@NonNull Argument beanType, @Nullable Qualifier qualifier) { ArgumentUtils.requireNonNull("beanType", beanType); - @SuppressWarnings("unchecked") - Qualifier proxyQualifier = qualifier != null ? Qualifiers.byQualifiers(qualifier, PROXY_TARGET_QUALIFIER) : PROXY_TARGET_QUALIFIER; - BeanCandidateKey key = new BeanCandidateKey<>(beanType, proxyQualifier, true); + BeanCandidateKey key = new BeanCandidateKey<>(beanType, qualifier, true); - Optional beanDefinition = beanConcreteCandidateCache.get(key); - //noinspection OptionalAssignedToNull + Optional beanDefinition = beanProxyTargetCache.get(key); if (beanDefinition == null) { - BeanRegistration beanRegistration = singletonScope.findCachedSingletonBeanRegistration(beanType, qualifier); - if (beanRegistration != null) { - if (LOG.isDebugEnabled()) { - LOG.debug("Resolved existing bean [{}] for type [{}] and qualifier [{}]", beanRegistration.bean, beanType, qualifier); - } - beanDefinition = Optional.of(beanRegistration.beanDefinition); - } else { - beanDefinition = findConcreteCandidateNoCache(null, beanType, proxyQualifier, true, false); - } - - beanConcreteCandidateCache.put(key, beanDefinition); + beanDefinition = findProxyTargetNoCache(null, beanType, qualifier); + beanProxyTargetCache.put(key, beanDefinition); } return beanDefinition; } @@ -1627,7 +1619,6 @@ public Collection> getBeanDefinitions(@Nullable Qualifier Collection> findBeanCandidates(@Nullable BeanRes @Nullable BeanDefinition filter, boolean filterProxied) { Predicate> predicate = filter == null ? null : definition -> !definition.equals(filter); - return findBeanCandidates(resolutionContext, beanType, filterProxied, true, predicate); - } - - /** - * Find bean candidates for the given type. - * - * @param The bean generic type - * @param resolutionContext The current resolution context - * @param beanType The bean type - * @param filterProxied Whether to filter out bean proxy targets - * @param predicate The predicate to filter candidates - * @return The candidates - */ - @NonNull - protected Collection> findBeanCandidates(@Nullable BeanResolutionContext resolutionContext, - @NonNull Argument beanType, - boolean filterProxied, - Predicate> predicate) { - return findBeanCandidates( - resolutionContext, - beanType, - filterProxied, - true, - predicate - ); + return findBeanCandidates(resolutionContext, beanType, true, predicate); } /** @@ -2213,7 +2179,6 @@ protected Collection> findBeanCandidates(@Nullable BeanRes * @param The bean generic type * @param resolutionContext The current resolution context * @param beanType The bean type - * @param filterProxied Whether to filter out bean proxy targets * @param collectIterables Whether iterables should be collected * @param predicate The predicate to filter candidates * @return The candidates @@ -2221,7 +2186,6 @@ protected Collection> findBeanCandidates(@Nullable BeanRes @NonNull protected Collection> findBeanCandidates(@Nullable BeanResolutionContext resolutionContext, @NonNull Argument beanType, - boolean filterProxied, boolean collectIterables, Predicate> predicate) { ArgumentUtils.requireNonNull("beanType", beanType); @@ -2245,7 +2209,6 @@ protected Collection> findBeanCandidates(@Nullable BeanRes return collectBeanCandidates( resolutionContext, beanType, - filterProxied, collectIterables, predicate, beanDefinitionsClasses @@ -2256,7 +2219,6 @@ protected Collection> findBeanCandidates(@Nullable BeanRes private Set> collectBeanCandidates( BeanResolutionContext resolutionContext, Argument beanType, - boolean filterProxied, boolean collectIterables, Predicate> predicate, Collection beanDefinitionsClasses) { @@ -2292,9 +2254,6 @@ private Set> collectBeanCandidates( } if (!candidates.isEmpty()) { - if (filterProxied) { - filterProxiedTypes(candidates); - } filterReplacedBeans(resolutionContext, candidates); } } else { @@ -2672,7 +2631,6 @@ protected void processParallelBeans(List parallelBeans) } }); - filterProxiedTypes((Collection) parallelDefinitions); filterReplacedBeans(null, (Collection) parallelDefinitions); parallelDefinitions.forEach(beanDefinition -> ForkJoinPool.commonPool().execute(() -> { @@ -3063,7 +3021,6 @@ protected String resolveDisabledBeanMessage(BeanResolutionContext resolutio Set> beanDefinitions = collectBeanCandidates( resolutionContext, beanType, - true, false, null, disabledBeans.values() @@ -3352,9 +3309,7 @@ private Optional> findConcreteCandidate(@Nullable BeanReso resolutionContext, beanType, qualifier, - throwNonUnique, - true - ); + throwNonUnique); beanConcreteCandidateCache.put(bk, beanDefinition); } return beanDefinition; @@ -3363,14 +3318,27 @@ private Optional> findConcreteCandidate(@Nullable BeanReso private Optional> findConcreteCandidateNoCache(@Nullable BeanResolutionContext resolutionContext, @NonNull Argument beanType, @Nullable Qualifier qualifier, - boolean throwNonUnique, - boolean filterProxied) { + boolean throwNonUnique) { Predicate> predicate = candidate -> !candidate.isAbstract(); - Collection> candidates = findBeanCandidates(resolutionContext, beanType, filterProxied, true, predicate); + Collection> candidates = findBeanCandidates(resolutionContext, beanType, true, predicate); return pickOneBean(beanType, qualifier, throwNonUnique, candidates); } + private Optional> findProxyTargetNoCache(@Nullable BeanResolutionContext resolutionContext, + @NonNull Argument beanType, + @Nullable Qualifier qualifier) { + + Collection> candidates = collectBeanCandidates( + resolutionContext, + beanType, + true, + null, + proxyTargetBeans + ); + return pickOneBean(beanType, qualifier, false, candidates); + } + @NonNull private Optional> pickOneBean( Argument beanType, @@ -3426,30 +3394,6 @@ private Optional> pickOneBean( return Optional.ofNullable(definition); } - @SuppressWarnings("java:S1871") - private void filterProxiedTypes( - Collection> candidates) { - int count = candidates.size(); - Set> proxiedTypes = new HashSet<>(count); - for (BeanDefinition candidate : candidates) { - if (candidate instanceof ProxyBeanDefinition proxyBeanDefinition) { - proxiedTypes.add(proxyBeanDefinition.getTargetDefinitionType()); - } else if (candidate instanceof BeanDefinitionDelegate delegate && delegate.getTarget() instanceof ProxyBeanDefinition proxyBeanDefinition) { - proxiedTypes.add(proxyBeanDefinition.getTargetDefinitionType()); - } - } - if (!proxiedTypes.isEmpty()) { - candidates.removeIf(candidate -> { - if (candidate instanceof BeanDefinitionDelegate delegate) { - return proxiedTypes.contains(delegate.getDelegate().getClass()); - } else { - return proxiedTypes.contains(candidate.getClass()); - } - }); - } - - } - private BeanDefinition lastChanceResolve(Argument beanType, Qualifier qualifier, boolean throwNonUnique, @@ -3528,6 +3472,7 @@ private void readAllBeanDefinitionClasses() { List parallelBeans = new ArrayList<>(10); List beanDefinitionReferences = resolveBeanDefinitionReferences(); + List toRemove = new ArrayList<>(beanDefinitionReferences.size()); beanDefinitionsClasses.addAll(beanDefinitionReferences); Set configurationsDisabled = new HashSet<>(); @@ -3541,10 +3486,23 @@ private void readAllBeanDefinitionClasses() { for (BeanDefinitionReference beanDefinitionReference : beanDefinitionReferences) { for (BeanConfiguration disableConfiguration : configurationsDisabled) { if (disableConfiguration.isWithin(beanDefinitionReference)) { - beanDefinitionsClasses.remove(beanDefinitionReference); + toRemove.add(beanDefinitionReference); continue reference; } } + + if (beanDefinitionReference.isProxiedBean()) { + toRemove.add(beanDefinitionReference); + if (beanDefinitionReference.requiresMethodProcessing()) { + processedBeans.add(beanDefinitionReference); + } + // retain only if proxy target otherwise the target is never used + if (beanDefinitionReference.isProxyTarget()) { + this.proxyTargetBeans.add(beanDefinitionReference); + } + continue; + } + final AnnotationMetadata annotationMetadata = beanDefinitionReference.getAnnotationMetadata(); Class[] indexes = annotationMetadata.classValues(INDEXES_TYPE); if (indexes.length > 0) { @@ -3573,6 +3531,7 @@ private void readAllBeanDefinitionClasses() { } + this.beanDefinitionsClasses.removeAll(toRemove); this.beanDefinitionReferences = null; this.beanConfigurationsList = null; @@ -3599,7 +3558,7 @@ private Collection> findBeanCandidatesInternal(BeanResolut @SuppressWarnings("rawtypes") Collection beanDefinitions = beanCandidateCache.get(beanType); if (beanDefinitions == null) { - beanDefinitions = findBeanCandidates(resolutionContext, beanType, true, true,null); + beanDefinitions = findBeanCandidates(resolutionContext, beanType, true, null); beanCandidateCache.put(beanType, beanDefinitions); } return beanDefinitions; @@ -3802,7 +3761,7 @@ private void addCandidateToList(@Nullable BeanResolutionContext resolutionCo } private boolean isCandidatePresent(Argument beanType, Qualifier qualifier) { - final Collection> candidates = findBeanCandidates(null, beanType, true, true, null); + final Collection> candidates = findBeanCandidates(null, beanType, true, null); if (!candidates.isEmpty()) { filterReplacedBeans(null, candidates); Stream> stream = candidates.stream(); diff --git a/inject/src/main/java/io/micronaut/context/ProxyTarget.java b/inject/src/main/java/io/micronaut/context/ProxyTarget.java deleted file mode 100644 index 6c2ce3fe2c1..00000000000 --- a/inject/src/main/java/io/micronaut/context/ProxyTarget.java +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright 2017-2020 original authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.micronaut.context; - -import jakarta.inject.Qualifier; - -import static java.lang.annotation.RetentionPolicy.RUNTIME; - -import java.lang.annotation.Documented; -import java.lang.annotation.Retention; - -/** - *

A qualifier for a proxy target. This qualifier is used internally the resolve the target class of a proxy.

- * - * @author Graeme Rocher - * @since 1.0 - */ -@Qualifier -@Documented -@Retention(RUNTIME) -@interface ProxyTarget { -} diff --git a/inject/src/main/java/io/micronaut/inject/BeanDefinitionReference.java b/inject/src/main/java/io/micronaut/inject/BeanDefinitionReference.java index 337aa2bd85a..2816e32195a 100644 --- a/inject/src/main/java/io/micronaut/inject/BeanDefinitionReference.java +++ b/inject/src/main/java/io/micronaut/inject/BeanDefinitionReference.java @@ -109,4 +109,22 @@ default boolean isSingleton() { default boolean isConfigurationProperties() { return getAnnotationMetadata().hasDeclaredStereotype(ConfigurationReader.class); } + + /** + * Returns whether another bean exists that proxies this bean. In other words + * this bean is the target of a proxy. + * + * @return Is the reference a proxy target. + * @since 4.0.0 + */ + default boolean isProxiedBean() { + return false; + } + + /** + * @return Whether this reference is a proxy target. + */ + default boolean isProxyTarget() { + return false; + } } From 10c177a16db1283c7d56c58b0919168d7add56e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Champeau?= Date: Thu, 1 Dec 2022 15:14:50 +0100 Subject: [PATCH 281/743] Upgrade to Micronaut Build 6.1.1 (#8452) and fix the checkstyle issues which are now reported as errors. --- ...cronaut.build.internal.convention-base.gradle | 6 +++++- config/checkstyle/checkstyle.xml | 2 +- .../inject/ast/beans/ConfigurableElement.java | 2 +- .../inject/writer/OriginatingElements.java | 2 +- .../core/graal/GraalReflectionConfigurer.java | 16 ++++++++-------- .../core/reflect/InstantiationUtils.java | 2 +- .../java/io/micronaut/core/util/ArrayUtils.java | 4 ++-- .../core/value/MapPropertyResolver.java | 1 - .../http/filter/HttpFilterResolver.java | 4 ++-- .../java/io/micronaut/http/uri/UriBuilder.java | 4 ++-- settings.gradle | 2 +- 11 files changed, 24 insertions(+), 21 deletions(-) diff --git a/buildSrc/src/main/groovy/io.micronaut.build.internal.convention-base.gradle b/buildSrc/src/main/groovy/io.micronaut.build.internal.convention-base.gradle index 2d559948450..e8519462c74 100644 --- a/buildSrc/src/main/groovy/io.micronaut.build.internal.convention-base.gradle +++ b/buildSrc/src/main/groovy/io.micronaut.build.internal.convention-base.gradle @@ -60,7 +60,11 @@ tasks.withType(Jar).configureEach { configurations.all { resolutionStrategy.dependencySubstitution { - substitute module("io.micronaut:micronaut-inject-groovy") using project(":inject-groovy") because "we want to test with what we're building" + rootProject.subprojects.each { + if (!it.name.startsWith('test-')) { + substitute module("io.micronaut:micronaut-${it.name}") using project(":${it.name}") because "we want to test with what we're building" + } + } } } diff --git a/config/checkstyle/checkstyle.xml b/config/checkstyle/checkstyle.xml index e8f5b18f227..90a106eb1cc 100644 --- a/config/checkstyle/checkstyle.xml +++ b/config/checkstyle/checkstyle.xml @@ -96,7 +96,7 @@ - + diff --git a/core-processor/src/main/java/io/micronaut/inject/ast/beans/ConfigurableElement.java b/core-processor/src/main/java/io/micronaut/inject/ast/beans/ConfigurableElement.java index 952662bbff7..79b41571e71 100644 --- a/core-processor/src/main/java/io/micronaut/inject/ast/beans/ConfigurableElement.java +++ b/core-processor/src/main/java/io/micronaut/inject/ast/beans/ConfigurableElement.java @@ -36,7 +36,7 @@ public interface ConfigurableElement extends Element { * @param types The types * @return This element */ - @NonNull ConfigurableElement typeArguments(@NonNull ClassElement...types); + @NonNull ConfigurableElement typeArguments(@NonNull ClassElement... types); /** * Adds a {@link jakarta.inject.Named} qualifier to the element. diff --git a/core-processor/src/main/java/io/micronaut/inject/writer/OriginatingElements.java b/core-processor/src/main/java/io/micronaut/inject/writer/OriginatingElements.java index 4368739566b..d9f1d8e2bd8 100644 --- a/core-processor/src/main/java/io/micronaut/inject/writer/OriginatingElements.java +++ b/core-processor/src/main/java/io/micronaut/inject/writer/OriginatingElements.java @@ -43,7 +43,7 @@ public interface OriginatingElements { * @param elements The elements * @return The originating elements */ - static OriginatingElements of(Element...elements) { + static OriginatingElements of(Element... elements) { if (Boolean.getBoolean("micronaut.static.originating.elements")) { for (Element element : elements) { StaticOriginatingElements.INSTANCE.addOriginatingElement(element); diff --git a/core/src/main/java/io/micronaut/core/graal/GraalReflectionConfigurer.java b/core/src/main/java/io/micronaut/core/graal/GraalReflectionConfigurer.java index da561d38c27..eed9ffe7adf 100644 --- a/core/src/main/java/io/micronaut/core/graal/GraalReflectionConfigurer.java +++ b/core/src/main/java/io/micronaut/core/graal/GraalReflectionConfigurer.java @@ -15,13 +15,6 @@ */ package io.micronaut.core.graal; -import java.lang.reflect.Constructor; -import java.lang.reflect.Field; -import java.lang.reflect.Method; -import java.lang.reflect.Modifier; -import java.util.List; -import java.util.Set; - import io.micronaut.core.annotation.AnnotationMetadata; import io.micronaut.core.annotation.AnnotationMetadataProvider; import io.micronaut.core.annotation.AnnotationValue; @@ -33,6 +26,13 @@ import io.micronaut.core.reflect.ReflectionUtils; import io.micronaut.core.util.CollectionUtils; +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.List; +import java.util.Set; + /** * Interface that allows dynamic configuration of reflection generated by the GraalTypeElementVisitor. * @@ -169,7 +169,7 @@ interface ReflectionConfigurationContext { * Register the given types for reflection. * @param types The types */ - void register(Class...types); + void register(Class... types); /** * Register the given methods for reflection. diff --git a/core/src/main/java/io/micronaut/core/reflect/InstantiationUtils.java b/core/src/main/java/io/micronaut/core/reflect/InstantiationUtils.java index d9acba30348..381cb46eb9e 100644 --- a/core/src/main/java/io/micronaut/core/reflect/InstantiationUtils.java +++ b/core/src/main/java/io/micronaut/core/reflect/InstantiationUtils.java @@ -229,7 +229,7 @@ public static T instantiate(Class type) { * @throws InstantiationException When an error occurs * @since 3.0.0 */ - public static T instantiate(Class type, Class[] argTypes, Object...args) { + public static T instantiate(Class type, Class[] argTypes, Object... args) { try { return BeanIntrospector.SHARED.findIntrospection(type).map(bi -> bi.instantiate(args)).orElseGet(() -> { try { diff --git a/core/src/main/java/io/micronaut/core/util/ArrayUtils.java b/core/src/main/java/io/micronaut/core/util/ArrayUtils.java index b9736b41392..a085eddb353 100644 --- a/core/src/main/java/io/micronaut/core/util/ArrayUtils.java +++ b/core/src/main/java/io/micronaut/core/util/ArrayUtils.java @@ -190,7 +190,7 @@ public static String toString(String delimiter, @Nullable Object[] array) { * @param The array type * @return The iterator */ - public static Iterator iterator(T...array) { + public static Iterator iterator(T... array) { if (isNotEmpty(array)) { return new ArrayIterator<>(array); } else { @@ -204,7 +204,7 @@ public static Iterator iterator(T...array) { * @param The array type * @return The iterator */ - public static Iterator reverseIterator(T...array) { + public static Iterator reverseIterator(T... array) { if (isNotEmpty(array)) { return new ReverseArrayIterator<>(array); } else { diff --git a/core/src/main/java/io/micronaut/core/value/MapPropertyResolver.java b/core/src/main/java/io/micronaut/core/value/MapPropertyResolver.java index b6dc48a47ea..7d035ff7ffd 100644 --- a/core/src/main/java/io/micronaut/core/value/MapPropertyResolver.java +++ b/core/src/main/java/io/micronaut/core/value/MapPropertyResolver.java @@ -25,7 +25,6 @@ import java.util.List; import java.util.Map; import java.util.Optional; -import java.util.stream.Collectors; /** * A {@link PropertyResolver} that resolves values from a backing map. diff --git a/http/src/main/java/io/micronaut/http/filter/HttpFilterResolver.java b/http/src/main/java/io/micronaut/http/filter/HttpFilterResolver.java index 50b2fa58c06..00b379b7da0 100644 --- a/http/src/main/java/io/micronaut/http/filter/HttpFilterResolver.java +++ b/http/src/main/java/io/micronaut/http/filter/HttpFilterResolver.java @@ -111,7 +111,7 @@ static FilterEntry of( @NonNull FT filter, @Nullable AnnotationMetadata annotationMetadata, @Nullable Set methods, - String...patterns) { + String... patterns) { return new DefaultFilterEntry<>( Objects.requireNonNull(filter, "Filter cannot be null"), annotationMetadata != null ? annotationMetadata : AnnotationMetadata.EMPTY_METADATA, @@ -135,7 +135,7 @@ static FilterEntry of( @NonNull FT filter, @Nullable AnnotationMetadata annotationMetadata, @Nullable Set methods, - @NonNull FilterPatternStyle patternStyle, String...patterns) { + @NonNull FilterPatternStyle patternStyle, String... patterns) { return new DefaultFilterEntry<>( Objects.requireNonNull(filter, "Filter cannot be null"), annotationMetadata != null ? annotationMetadata : AnnotationMetadata.EMPTY_METADATA, diff --git a/http/src/main/java/io/micronaut/http/uri/UriBuilder.java b/http/src/main/java/io/micronaut/http/uri/UriBuilder.java index 4952eb50911..1025ded2b8d 100644 --- a/http/src/main/java/io/micronaut/http/uri/UriBuilder.java +++ b/http/src/main/java/io/micronaut/http/uri/UriBuilder.java @@ -93,7 +93,7 @@ public interface UriBuilder { * @param values The values * @return This builder */ - @NonNull UriBuilder queryParam(String name, Object...values); + @NonNull UriBuilder queryParam(String name, Object... values); /** * Adds a query parameter for the give name and values. The values will be URI encoded. @@ -103,7 +103,7 @@ public interface UriBuilder { * @param values The values * @return This builder */ - @NonNull UriBuilder replaceQueryParam(String name, Object...values); + @NonNull UriBuilder replaceQueryParam(String name, Object... values); /** * The constructed URI. diff --git a/settings.gradle b/settings.gradle index 455107624d9..fbc6b1dda60 100644 --- a/settings.gradle +++ b/settings.gradle @@ -14,7 +14,7 @@ pluginManagement { } plugins { - id 'io.micronaut.build.shared.settings' version '6.0.1' + id 'io.micronaut.build.shared.settings' version '6.1.1' } rootProject.name = 'micronaut' From a19f785edfbedae29a8092f556b6f53f0a8ee8e1 Mon Sep 17 00:00:00 2001 From: Graeme Rocher Date: Thu, 1 Dec 2022 15:34:41 +0100 Subject: [PATCH 282/743] Fix bug in introspections when properties are of mixed types (#8451) --- .../inject/ast/BeanPropertiesQuery.java | 173 --------- .../io/micronaut/inject/ast/ClassElement.java | 4 +- .../inject/ast/PrimitiveElement.java | 19 + .../inject/ast/PropertyElementQuery.java | 338 ++++++++++++++++++ .../ast/utils/AstBeanPropertiesUtils.java | 17 +- .../IntrospectedTypeElementVisitor.java | 6 +- ...ConfigurationReaderBeanElementCreator.java | 4 +- .../groovy/visitor/GroovyClassElement.java | 16 +- .../beans/BeanIntrospectionSpec.groovy | 51 ++- .../processing/visitor/JavaClassElement.java | 10 +- .../ConfigPropertiesParseSpec.groovy | 37 ++ 11 files changed, 471 insertions(+), 204 deletions(-) delete mode 100644 core-processor/src/main/java/io/micronaut/inject/ast/BeanPropertiesQuery.java create mode 100644 core-processor/src/main/java/io/micronaut/inject/ast/PropertyElementQuery.java diff --git a/core-processor/src/main/java/io/micronaut/inject/ast/BeanPropertiesQuery.java b/core-processor/src/main/java/io/micronaut/inject/ast/BeanPropertiesQuery.java deleted file mode 100644 index 5b67b3f20be..00000000000 --- a/core-processor/src/main/java/io/micronaut/inject/ast/BeanPropertiesQuery.java +++ /dev/null @@ -1,173 +0,0 @@ -/* - * Copyright 2017-2022 original authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.micronaut.inject.ast; - -import io.micronaut.context.annotation.BeanProperties; -import io.micronaut.context.annotation.ConfigurationBuilder; -import io.micronaut.context.annotation.ConfigurationProperties; -import io.micronaut.core.annotation.AccessorsStyle; -import io.micronaut.core.annotation.AnnotationMetadata; -import io.micronaut.core.annotation.AnnotationValue; -import io.micronaut.core.util.CollectionUtils; - -import java.util.Arrays; -import java.util.Collections; -import java.util.EnumSet; -import java.util.HashSet; -import java.util.Set; - -/** - * The bean properties configuration. - * - * @author Denis Stepanov - * @since 4.0.0 - */ -public final class BeanPropertiesQuery { - - private BeanProperties.Visibility visibility = BeanProperties.Visibility.DEFAULT; - private Set accessKinds = EnumSet.of(BeanProperties.AccessKind.METHOD); - private Set includes = Collections.emptySet(); - private Set excludes = Collections.emptySet(); - private String[] readPrefixes = new String[]{AccessorsStyle.DEFAULT_READ_PREFIX}; - private String[] writePrefixes = new String[]{AccessorsStyle.DEFAULT_WRITE_PREFIX}; - private boolean allowSetterWithZeroArgs; - private boolean allowSetterWithMultipleArgs; - private boolean allowStaticProperties; - private Set excludedAnnotations = Collections.emptySet(); - - public static BeanPropertiesQuery of(AnnotationMetadata annotationMetadata) { - BeanPropertiesQuery conf = new BeanPropertiesQuery(); - Set includes = new HashSet<>(); - Set excludes = new HashSet<>(); - - AnnotationValue annotation = annotationMetadata.getAnnotation(BeanProperties.class); - if (annotation != null) { - annotation.enumValue(BeanProperties.MEMBER_VISIBILITY, BeanProperties.Visibility.class) - .ifPresent(conf::setVisibility); - if (annotation.isPresent(BeanProperties.MEMBER_ACCESS_KIND)) { - conf.setAccessKinds( - annotation.enumValuesSet(BeanProperties.MEMBER_ACCESS_KIND, BeanProperties.AccessKind.class) - ); - } - annotation.booleanValue(BeanProperties.MEMBER_ALLOW_WRITE_WITH_ZERO_ARGS) - .ifPresent(conf::setAllowSetterWithZeroArgs); - annotation.booleanValue(BeanProperties.MEMBER_ALLOW_WRITE_WITH_MULTIPLE_ARGS) - .ifPresent(conf::setAllowSetterWithMultipleArgs); - - includes.addAll(Arrays.asList(annotation.stringValues(BeanProperties.MEMBER_INCLUDES))); - excludes.addAll(Arrays.asList(annotation.stringValues(BeanProperties.MEMBER_EXCLUDES))); - - conf.setExcludedAnnotations(CollectionUtils.setOf(annotation.stringValues(BeanProperties.MEMBER_EXCLUDED_ANNOTATIONS))); - } - - // TODO: investigate why aliases aren't propagated - includes.addAll(Arrays.asList(annotationMetadata.stringValues(ConfigurationProperties.class, BeanProperties.MEMBER_INCLUDES))); - excludes.addAll(Arrays.asList(annotationMetadata.stringValues(ConfigurationProperties.class, BeanProperties.MEMBER_EXCLUDES))); - - includes.addAll(Arrays.asList(annotationMetadata.stringValues(ConfigurationBuilder.class, BeanProperties.MEMBER_INCLUDES))); - excludes.addAll(Arrays.asList(annotationMetadata.stringValues(ConfigurationBuilder.class, BeanProperties.MEMBER_EXCLUDES))); - - conf.setIncludes(includes); - conf.setExcludes(excludes); - - annotationMetadata.getValue(AccessorsStyle.class, "readPrefixes", String[].class) - .ifPresent(conf::setReadPrefixes); - annotationMetadata.getValue(AccessorsStyle.class, "writePrefixes", String[].class) - .ifPresent(conf::setWritePrefixes); - - return conf; - } - - public BeanProperties.Visibility getVisibility() { - return visibility; - } - - public void setVisibility(BeanProperties.Visibility visibility) { - this.visibility = visibility; - } - - public Set getAccessKinds() { - return accessKinds; - } - - public void setAccessKinds(Set accessKinds) { - this.accessKinds = accessKinds; - } - - public Set getIncludes() { - return includes; - } - - public void setIncludes(Set includes) { - this.includes = includes; - } - - public Set getExcludes() { - return excludes; - } - - public void setExcludes(Set excludes) { - this.excludes = excludes; - } - - public String[] getReadPrefixes() { - return readPrefixes; - } - - public void setReadPrefixes(String[] readPrefixes) { - this.readPrefixes = readPrefixes; - } - - public String[] getWritePrefixes() { - return writePrefixes; - } - - public void setWritePrefixes(String[] writePrefixes) { - this.writePrefixes = writePrefixes; - } - - public boolean isAllowSetterWithZeroArgs() { - return allowSetterWithZeroArgs; - } - - public void setAllowSetterWithZeroArgs(boolean allowSetterWithZeroArgs) { - this.allowSetterWithZeroArgs = allowSetterWithZeroArgs; - } - - public boolean isAllowSetterWithMultipleArgs() { - return allowSetterWithMultipleArgs; - } - - public void setAllowSetterWithMultipleArgs(boolean allowSetterWithMultipleArgs) { - this.allowSetterWithMultipleArgs = allowSetterWithMultipleArgs; - } - - public boolean isAllowStaticProperties() { - return allowStaticProperties; - } - - public void setAllowStaticProperties(boolean allowStaticProperties) { - this.allowStaticProperties = allowStaticProperties; - } - - public Set getExcludedAnnotations() { - return excludedAnnotations; - } - - public void setExcludedAnnotations(Set excludedAnnotations) { - this.excludedAnnotations = excludedAnnotations; - } -} diff --git a/core-processor/src/main/java/io/micronaut/inject/ast/ClassElement.java b/core-processor/src/main/java/io/micronaut/inject/ast/ClassElement.java index f68018762c5..7ace87fc055 100644 --- a/core-processor/src/main/java/io/micronaut/inject/ast/ClassElement.java +++ b/core-processor/src/main/java/io/micronaut/inject/ast/ClassElement.java @@ -422,12 +422,12 @@ default List getSyntheticBeanProperties() { /** * Returns the bean properties (getters and setters) for this class element based on custom configuration. * - * @param beanPropertiesQuery The configuration + * @param propertyElementQuery The configuration * @return The bean properties for this class element * @since 4.0.0 */ @NonNull - default List getBeanProperties(BeanPropertiesQuery beanPropertiesQuery) { + default List getBeanProperties(@NonNull PropertyElementQuery propertyElementQuery) { return Collections.emptyList(); } diff --git a/core-processor/src/main/java/io/micronaut/inject/ast/PrimitiveElement.java b/core-processor/src/main/java/io/micronaut/inject/ast/PrimitiveElement.java index 4779f8df306..c394fb882d3 100644 --- a/core-processor/src/main/java/io/micronaut/inject/ast/PrimitiveElement.java +++ b/core-processor/src/main/java/io/micronaut/inject/ast/PrimitiveElement.java @@ -19,6 +19,8 @@ import io.micronaut.core.annotation.AnnotationMetadata; import io.micronaut.core.annotation.Nullable; +import java.util.Objects; + /** * A {@link ClassElement} of primitive types. */ @@ -138,4 +140,21 @@ public String toString() { ", arrayDimensions=" + arrayDimensions + '}'; } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + PrimitiveElement that = (PrimitiveElement) o; + return arrayDimensions == that.arrayDimensions && typeName.equals(that.typeName); + } + + @Override + public int hashCode() { + return Objects.hash(typeName, arrayDimensions); + } } diff --git a/core-processor/src/main/java/io/micronaut/inject/ast/PropertyElementQuery.java b/core-processor/src/main/java/io/micronaut/inject/ast/PropertyElementQuery.java new file mode 100644 index 00000000000..9024b6235bd --- /dev/null +++ b/core-processor/src/main/java/io/micronaut/inject/ast/PropertyElementQuery.java @@ -0,0 +1,338 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.inject.ast; + +import io.micronaut.context.annotation.BeanProperties; +import io.micronaut.context.annotation.ConfigurationBuilder; +import io.micronaut.context.annotation.ConfigurationProperties; +import io.micronaut.core.annotation.AccessorsStyle; +import io.micronaut.core.annotation.AnnotationMetadata; +import io.micronaut.core.annotation.AnnotationValue; +import io.micronaut.core.annotation.NonNull; +import io.micronaut.core.annotation.Nullable; +import io.micronaut.core.util.ArrayUtils; +import io.micronaut.core.util.CollectionUtils; + +import java.util.Arrays; +import java.util.Collections; +import java.util.EnumSet; +import java.util.HashSet; +import java.util.Objects; +import java.util.Set; + +/** + * Represents a query for {@link PropertyElement} definitions. + * + * @author Denis Stepanov + * @since 4.0.0 + * @see PropertyElement + * @see ClassElement#getBeanProperties(PropertyElementQuery) + * @see BeanProperties + */ +public final class PropertyElementQuery { + + private static final String[] DEFAULT_READ_PREFIXES = { AccessorsStyle.DEFAULT_READ_PREFIX }; + private static final String[] DEFAULT_WRITE_PREFIXES = { AccessorsStyle.DEFAULT_WRITE_PREFIX }; + private static final EnumSet DEFAULT_ACCESS_KINDS = EnumSet.of(BeanProperties.AccessKind.METHOD); + private BeanProperties.Visibility visibility = BeanProperties.Visibility.DEFAULT; + private Set accessKinds = DEFAULT_ACCESS_KINDS; + private Set includes = Collections.emptySet(); + private Set excludes = Collections.emptySet(); + private String[] readPrefixes = DEFAULT_READ_PREFIXES; + private String[] writePrefixes = DEFAULT_WRITE_PREFIXES; + private boolean allowSetterWithZeroArgs; + private boolean allowSetterWithMultipleArgs; + private boolean allowStaticProperties; + + private boolean ignoreSettersWithDifferingType; + private Set excludedAnnotations = Collections.emptySet(); + + /** + * Creates a query for the given metadata. + * @param annotationMetadata The metadata + * @return The query + */ + public static @NonNull PropertyElementQuery of(@NonNull AnnotationMetadata annotationMetadata) { + PropertyElementQuery conf = new PropertyElementQuery(); + Set includes = new HashSet<>(); + Set excludes = new HashSet<>(); + + AnnotationValue annotation = annotationMetadata.getAnnotation(BeanProperties.class); + if (annotation != null) { + annotation.enumValue(BeanProperties.MEMBER_VISIBILITY, BeanProperties.Visibility.class) + .ifPresent(conf::visibility); + if (annotation.isPresent(BeanProperties.MEMBER_ACCESS_KIND)) { + conf.accessKinds( + annotation.enumValuesSet(BeanProperties.MEMBER_ACCESS_KIND, BeanProperties.AccessKind.class) + ); + } + annotation.booleanValue(BeanProperties.MEMBER_ALLOW_WRITE_WITH_ZERO_ARGS) + .ifPresent(conf::allowSetterWithZeroArgs); + annotation.booleanValue(BeanProperties.MEMBER_ALLOW_WRITE_WITH_MULTIPLE_ARGS) + .ifPresent(conf::allowSetterWithMultipleArgs); + + includes.addAll(Arrays.asList(annotation.stringValues(BeanProperties.MEMBER_INCLUDES))); + excludes.addAll(Arrays.asList(annotation.stringValues(BeanProperties.MEMBER_EXCLUDES))); + + conf.excludedAnnotations(CollectionUtils.setOf(annotation.stringValues(BeanProperties.MEMBER_EXCLUDED_ANNOTATIONS))); + } + + // TODO: investigate why aliases aren't propagated + includes.addAll(Arrays.asList(annotationMetadata.stringValues(ConfigurationProperties.class, BeanProperties.MEMBER_INCLUDES))); + excludes.addAll(Arrays.asList(annotationMetadata.stringValues(ConfigurationProperties.class, BeanProperties.MEMBER_EXCLUDES))); + + includes.addAll(Arrays.asList(annotationMetadata.stringValues(ConfigurationBuilder.class, BeanProperties.MEMBER_INCLUDES))); + excludes.addAll(Arrays.asList(annotationMetadata.stringValues(ConfigurationBuilder.class, BeanProperties.MEMBER_EXCLUDES))); + + conf.includes(includes); + conf.excludes(excludes); + + String[] readPrefixes = annotationMetadata.stringValues(AccessorsStyle.class, "readPrefixes"); + if (ArrayUtils.isNotEmpty(readPrefixes)) { + conf.readPrefixes(readPrefixes); + } + String[] writerPrefixes = annotationMetadata.stringValues(AccessorsStyle.class, "writePrefixes"); + if (ArrayUtils.isNotEmpty(writerPrefixes)) { + conf.writePrefixes(writerPrefixes); + } + return conf; + } + + /** + * @return Whether to ignore setters that don't match the getter return type. + */ + public boolean isIgnoreSettersWithDifferingType() { + return ignoreSettersWithDifferingType; + } + + /** + * Set whether to ignore setters that have a different receiver type to the getter return type. + * @param shouldIgnore True if they should be ignored. + * @return This PropertyElementQuery + */ + public @NonNull PropertyElementQuery ignoreSettersWithDifferingType(boolean shouldIgnore) { + this.ignoreSettersWithDifferingType = shouldIgnore; + return this; + } + + /** + * @return The visibility strategy. + * @see io.micronaut.context.annotation.BeanProperties.Visibility + */ + @NonNull + public BeanProperties.Visibility getVisibility() { + return visibility; + } + + /** + * Sets the visibility strategy. + * @param visibility The visibility strategy + * @return This PropertyElementQuery + * @see io.micronaut.context.annotation.BeanProperties.Visibility + */ + public @NonNull PropertyElementQuery visibility(BeanProperties.Visibility visibility) { + this.visibility = Objects.requireNonNullElse(visibility, BeanProperties.Visibility.DEFAULT); + return this; + } + + /** + * The access kinds. + * @return A set of access kinds + * @see BeanProperties.AccessKind + */ + @NonNull + public Set getAccessKinds() { + return accessKinds; + } + + /** + * Sets the access kinds. + * @param accessKinds The access kinds + * @return This PropertyElementQuery + */ + public @NonNull PropertyElementQuery accessKinds(@Nullable Set accessKinds) { + if (CollectionUtils.isNotEmpty(accessKinds)) { + this.accessKinds = Collections.unmodifiableSet(accessKinds); + } else { + this.accessKinds = DEFAULT_ACCESS_KINDS; + } + return this; + } + + /** + * The property names to include. + * @return The includes. + */ + @NonNull + public Set getIncludes() { + return includes; + } + + /** + * Sets the property names to include. + * @param includes The includes + * @return This PropertyElementQuery + */ + @NonNull + public PropertyElementQuery includes(@Nullable Set includes) { + if (CollectionUtils.isNotEmpty(includes)) { + this.includes = Collections.unmodifiableSet(includes); + } else { + this.includes = Collections.emptySet(); + } + return this; + } + + /** + * The property names to exclude. + * @return The excludes + */ + @NonNull + public Set getExcludes() { + return excludes; + } + + /** + * Sets the excluded property names. + * @param excludes The property names to exclude + * @return This PropertyElementQuery + */ + public @NonNull PropertyElementQuery excludes(@Nullable Set excludes) { + if (CollectionUtils.isNotEmpty(excludes)) { + this.excludes = Collections.unmodifiableSet(excludes); + } else { + this.excludes = Collections.emptySet(); + } + return this; + } + + /** + * @return The read method prefixes. + */ + @NonNull + public String[] getReadPrefixes() { + return readPrefixes; + } + + /** + * Sets the read method prefixes. + * @param readPrefixes The read methos prefixes + * @return This PropertyElementQuery + */ + public @NonNull PropertyElementQuery readPrefixes(String... readPrefixes) { + if (ArrayUtils.isNotEmpty(readPrefixes)) { + this.readPrefixes = readPrefixes; + } else { + this.readPrefixes = DEFAULT_READ_PREFIXES; + } + return this; + } + + /** + * @return The write method prefixes. + */ + public @NonNull String[] getWritePrefixes() { + return writePrefixes; + } + + /** + * Sets the write method prefixes. + * @param writePrefixes The write prefixes + * @return This PropertyElementQuery + */ + public @NonNull PropertyElementQuery writePrefixes(String[] writePrefixes) { + if (ArrayUtils.isNotEmpty(writePrefixes)) { + this.writePrefixes = writePrefixes; + } else { + this.writePrefixes = DEFAULT_WRITE_PREFIXES; + } + return this; + } + + /** + * @return Whether to allow zero argument setters for boolean values etc. + */ + public boolean isAllowSetterWithZeroArgs() { + return allowSetterWithZeroArgs; + } + + /** + * Sets whether to allow zero argument setters for boolean properties etc. + * @param allowSetterWithZeroArgs True to allow zero argument setters + * @return This PropertyElementQuery + */ + public @NonNull PropertyElementQuery allowSetterWithZeroArgs(boolean allowSetterWithZeroArgs) { + this.allowSetterWithZeroArgs = allowSetterWithZeroArgs; + return this; + } + + /** + * Whether to allow setters with multiple arguments. + * @return True if setters with multiple arguments are allowed. + */ + public boolean isAllowSetterWithMultipleArgs() { + return allowSetterWithMultipleArgs; + } + + /** + * Sets whether to allow setters with multiple arguments. + * @param allowSetterWithMultipleArgs True if setters with multiple arguments are allowed. + * @return This PropertyElementQuery + */ + public @NonNull PropertyElementQuery allowSetterWithMultipleArgs(boolean allowSetterWithMultipleArgs) { + this.allowSetterWithMultipleArgs = allowSetterWithMultipleArgs; + return this; + } + + /** + * @return Whether to allow static properties. + */ + public boolean isAllowStaticProperties() { + return allowStaticProperties; + } + + /** + * Sets whether to allow static properties. + * @param allowStaticProperties True if static properties are allowed. + * @return This PropertyElementQuery + */ + public @NonNull PropertyElementQuery allowStaticProperties(boolean allowStaticProperties) { + this.allowStaticProperties = allowStaticProperties; + return this; + } + + /** + * @return The excludes annotation names. + */ + @NonNull + public Set getExcludedAnnotations() { + return excludedAnnotations; + } + + /** + * Sets the annotations names that should be used to indicate a property is excluded. + * @param excludedAnnotations The excluded annotation names + * @return This PropertyElementQuery + */ + public @NonNull PropertyElementQuery excludedAnnotations(@Nullable Set excludedAnnotations) { + if (CollectionUtils.isNotEmpty(excludedAnnotations)) { + this.excludedAnnotations = Collections.unmodifiableSet(excludedAnnotations); + } else { + this.excludedAnnotations = Collections.emptySet(); + } + return this; + } +} diff --git a/core-processor/src/main/java/io/micronaut/inject/ast/utils/AstBeanPropertiesUtils.java b/core-processor/src/main/java/io/micronaut/inject/ast/utils/AstBeanPropertiesUtils.java index 554be78c032..1b07226b903 100644 --- a/core-processor/src/main/java/io/micronaut/inject/ast/utils/AstBeanPropertiesUtils.java +++ b/core-processor/src/main/java/io/micronaut/inject/ast/utils/AstBeanPropertiesUtils.java @@ -21,7 +21,7 @@ import io.micronaut.core.annotation.AnnotationUtil; import io.micronaut.core.annotation.Internal; import io.micronaut.core.naming.NameUtils; -import io.micronaut.inject.ast.BeanPropertiesQuery; +import io.micronaut.inject.ast.PropertyElementQuery; import io.micronaut.inject.ast.ClassElement; import io.micronaut.inject.ast.FieldElement; import io.micronaut.inject.ast.MemberElement; @@ -65,7 +65,7 @@ private AstBeanPropertiesUtils() { * @param propertyCreator The property creator * @return the list of properties */ - public static List resolveBeanProperties(BeanPropertiesQuery configuration, + public static List resolveBeanProperties(PropertyElementQuery configuration, ClassElement classElement, Supplier> methodsSupplier, Supplier> fieldSupplier, @@ -125,11 +125,22 @@ public static List resolveBeanProperties(BeanPropertiesQuery co resolveReadAccessForField(fieldElement, isAccessor, beanPropertyData); resolveWriteAccessForField(fieldElement, isAccessor, beanPropertyData); } + if (!props.isEmpty()) { List beanProperties = new ArrayList<>(props.size()); for (Map.Entry entry : props.entrySet()) { String propertyName = entry.getKey(); BeanPropertyData value = entry.getValue(); + if (configuration.isIgnoreSettersWithDifferingType() && value.setter != null && value.getter != null) { + // ensure types match + ClassElement getterType = value.getter.getGenericReturnType(); + ClassElement setterType = value.setter.getParameters()[0].getGenericType(); + if (!getterType.equals(setterType)) { + // getter and setter don't match, remove setter + value.setter = null; + value.type = getterType; + } + } // Define the property type based on its writer element if (value.writeAccessKind == BeanProperties.AccessKind.FIELD && !value.field.getType().equals(value.type)) { value.type = value.field.getGenericType(); @@ -199,7 +210,7 @@ private static boolean isExcludedBecauseOfMissingAccess(BeanPropertyData value) return value.readAccessKind == null && value.writeAccessKind == null; } - private static boolean isExcludedByAnnotations(BeanPropertiesQuery conf, BeanPropertyData value) { + private static boolean isExcludedByAnnotations(PropertyElementQuery conf, BeanPropertyData value) { if (conf.getExcludedAnnotations().isEmpty()) { return false; } diff --git a/core-processor/src/main/java/io/micronaut/inject/beans/visitor/IntrospectedTypeElementVisitor.java b/core-processor/src/main/java/io/micronaut/inject/beans/visitor/IntrospectedTypeElementVisitor.java index 67a99b32cba..d95f7a39bd0 100644 --- a/core-processor/src/main/java/io/micronaut/inject/beans/visitor/IntrospectedTypeElementVisitor.java +++ b/core-processor/src/main/java/io/micronaut/inject/beans/visitor/IntrospectedTypeElementVisitor.java @@ -30,6 +30,7 @@ import io.micronaut.inject.ast.ElementQuery; import io.micronaut.inject.ast.MethodElement; import io.micronaut.inject.ast.PropertyElement; +import io.micronaut.inject.ast.PropertyElementQuery; import io.micronaut.inject.visitor.TypeElementVisitor; import io.micronaut.inject.visitor.VisitorContext; import io.micronaut.inject.writer.ClassGenerationException; @@ -189,9 +190,10 @@ private void processElement(boolean metadata, Set indexedAnnotations, ClassElement ce, BeanIntrospectionWriter writer) { - List beanProperties = ce.getBeanProperties().stream() + PropertyElementQuery query = PropertyElementQuery.of(ce).ignoreSettersWithDifferingType(true); + List beanProperties = ce.getBeanProperties(query).stream() .filter(p -> !p.isExcluded()) - .collect(Collectors.toList()); + .toList(); Optional constructorElement = ce.getPrimaryConstructor(); constructorElement.ifPresent(constructorEl -> { if (ArrayUtils.isNotEmpty(constructorEl.getParameters())) { diff --git a/core-processor/src/main/java/io/micronaut/inject/processing/ConfigurationReaderBeanElementCreator.java b/core-processor/src/main/java/io/micronaut/inject/processing/ConfigurationReaderBeanElementCreator.java index 8cbd0d09959..f32a432160b 100644 --- a/core-processor/src/main/java/io/micronaut/inject/processing/ConfigurationReaderBeanElementCreator.java +++ b/core-processor/src/main/java/io/micronaut/inject/processing/ConfigurationReaderBeanElementCreator.java @@ -34,7 +34,7 @@ import io.micronaut.core.bind.annotation.Bindable; import io.micronaut.inject.annotation.AnnotationMetadataHierarchy; import io.micronaut.inject.annotation.MutableAnnotationMetadata; -import io.micronaut.inject.ast.BeanPropertiesQuery; +import io.micronaut.inject.ast.PropertyElementQuery; import io.micronaut.inject.ast.ClassElement; import io.micronaut.inject.ast.FieldElement; import io.micronaut.inject.ast.MemberElement; @@ -273,7 +273,7 @@ private void visitConfigurationBuilder(BeanDefinitionVisitor visitor, ClassElement builderType) { try { String configurationPrefix = builderElement.stringValue(ConfigurationBuilder.class).map(v -> v + ".").orElse(""); - builderType.getBeanProperties(BeanPropertiesQuery.of(builderElement)) + builderType.getBeanProperties(PropertyElementQuery.of(builderElement)) .stream() .filter(propertyElement -> { if (propertyElement.isExcluded()) { diff --git a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyClassElement.java b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyClassElement.java index b7c67fb4710..f468f183303 100644 --- a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyClassElement.java +++ b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyClassElement.java @@ -32,7 +32,7 @@ import io.micronaut.core.util.CollectionUtils; import io.micronaut.inject.annotation.AnnotationMetadataHierarchy; import io.micronaut.inject.ast.ArrayableClassElement; -import io.micronaut.inject.ast.BeanPropertiesQuery; +import io.micronaut.inject.ast.PropertyElementQuery; import io.micronaut.inject.ast.ClassElement; import io.micronaut.inject.ast.ConstructorElement; import io.micronaut.inject.ast.Element; @@ -488,8 +488,8 @@ private int computeDimensions(ClassNode cn) { public List getSyntheticBeanProperties() { // Native properties should be composed of field + synthetic getter/setter if (nativeProperties == null) { - BeanPropertiesQuery configuration = new BeanPropertiesQuery(); - configuration.setAllowStaticProperties(true); + PropertyElementQuery configuration = new PropertyElementQuery(); + configuration.allowStaticProperties(true); Set nativeProps = getPropertyNodes().stream().map(PropertyNode::getName).collect(Collectors.toCollection(LinkedHashSet::new)); nativeProperties = AstBeanPropertiesUtils.resolveBeanProperties(configuration, this, @@ -505,9 +505,9 @@ public List getSyntheticBeanProperties() { } @Override - public List getBeanProperties(BeanPropertiesQuery beanPropertiesQuery) { + public List getBeanProperties(PropertyElementQuery propertyElementQuery) { Set nativeProps = getPropertyNodes().stream().map(PropertyNode::getName).collect(Collectors.toCollection(LinkedHashSet::new)); - return AstBeanPropertiesUtils.resolveBeanProperties(beanPropertiesQuery, + return AstBeanPropertiesUtils.resolveBeanProperties(propertyElementQuery, this, () -> getEnclosedElements(ElementQuery.ALL_METHODS.onlyInstance()), () -> getEnclosedElements(ElementQuery.ALL_FIELDS), @@ -515,13 +515,13 @@ public List getBeanProperties(BeanPropertiesQuery beanPropertie nativeProps, methodElement -> Optional.empty(), methodElement -> Optional.empty(), - value -> mapPropertyElement(nativeProps, value, beanPropertiesQuery, false)); + value -> mapPropertyElement(nativeProps, value, propertyElementQuery, false)); } @Override public List getBeanProperties() { if (properties == null) { - properties = getBeanProperties(BeanPropertiesQuery.of(this)); + properties = getBeanProperties(PropertyElementQuery.of(this)); } return properties; } @@ -529,7 +529,7 @@ public List getBeanProperties() { @Nullable private GroovyPropertyElement mapPropertyElement(Set nativeProps, AstBeanPropertiesUtils.BeanPropertyData value, - BeanPropertiesQuery conf, + PropertyElementQuery conf, boolean nativePropertiesOnly) { if (value.type == null) { // withSomething() builder setter diff --git a/inject-java-test/src/test/groovy/io/micronaut/inject/visitor/beans/BeanIntrospectionSpec.groovy b/inject-java-test/src/test/groovy/io/micronaut/inject/visitor/beans/BeanIntrospectionSpec.groovy index 4b244fd2834..4d9203eb4c2 100644 --- a/inject-java-test/src/test/groovy/io/micronaut/inject/visitor/beans/BeanIntrospectionSpec.groovy +++ b/inject-java-test/src/test/groovy/io/micronaut/inject/visitor/beans/BeanIntrospectionSpec.groovy @@ -46,6 +46,39 @@ import java.lang.reflect.Field class BeanIntrospectionSpec extends AbstractTypeElementSpec { + void "test mix optional getter with setter"() { + given: + def introspection = buildBeanIntrospection('mixed.Test', ''' +package mixed; + +import io.micronaut.core.annotation.Introspected; +import io.micronaut.context.annotation.Executable; +import io.micronaut.core.annotation.Nullable; +import java.util.Optional; + +@Introspected +class Test { + @Nullable + private String foo; + public Optional getFoo() { + return Optional.ofNullable(foo); + } + public void setFoo(@Nullable String foo) { + this.foo = foo; + } +} +''') + when: + def test = introspection.instantiate() + def prop = introspection.getRequiredProperty("foo", Optional) + test.foo = 'value' + + then:'the write method is not considered to match the getter/setter pair' + prop.get(test).get() == 'value' + prop.type == Optional + prop.isReadOnly() + } + void "test generics in arrays don't stack overflow"() { given: def introspection = buildBeanIntrospection('arraygenerics.Test', ''' @@ -158,15 +191,15 @@ class Test { ''') expect: introspection.getPropertyNames().length == 4 - introspection.getProperty("foo").get().type == String.class - introspection.getProperty("lng").get().type == Long.class - introspection.getProperty("dbl").get().type == Double.class - introspection.getProperty("ingr").get().type == Integer.class - - introspection.getProperty("foo").get().isReadWrite() - introspection.getProperty("lng").get().isReadWrite() - introspection.getProperty("dbl").get().isReadWrite() - introspection.getProperty("ingr").get().isReadWrite() + introspection.getProperty("foo").get().type == Optional.class + introspection.getProperty("lng").get().type == OptionalLong.class + introspection.getProperty("dbl").get().type == OptionalDouble.class + introspection.getProperty("ingr").get().type == OptionalInt.class + + introspection.getProperty("foo").get().isReadOnly() + introspection.getProperty("lng").get().isReadOnly() + introspection.getProperty("dbl").get().isReadOnly() + introspection.getProperty("ingr").get().isReadOnly() } void "test property type is not defined by its not accessible field"() { diff --git a/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaClassElement.java b/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaClassElement.java index e3e8e7eed9c..b482d52c88f 100644 --- a/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaClassElement.java +++ b/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaClassElement.java @@ -25,7 +25,7 @@ import io.micronaut.core.reflect.ClassUtils; import io.micronaut.core.util.StringUtils; import io.micronaut.inject.ast.ArrayableClassElement; -import io.micronaut.inject.ast.BeanPropertiesQuery; +import io.micronaut.inject.ast.PropertyElementQuery; import io.micronaut.inject.ast.ClassElement; import io.micronaut.inject.ast.ConstructorElement; import io.micronaut.inject.ast.ParameterElement; @@ -310,15 +310,15 @@ public boolean isInterface() { @Override public List getBeanProperties() { if (beanProperties == null) { - beanProperties = getBeanProperties(BeanPropertiesQuery.of(this)); + beanProperties = getBeanProperties(PropertyElementQuery.of(this)); } return Collections.unmodifiableList(beanProperties); } @Override - public List getBeanProperties(BeanPropertiesQuery beanPropertiesQuery) { + public List getBeanProperties(PropertyElementQuery propertyElementQuery) { if (isRecord()) { - return AstBeanPropertiesUtils.resolveBeanProperties(beanPropertiesQuery, + return AstBeanPropertiesUtils.resolveBeanProperties(propertyElementQuery, this, this::getRecordMethods, this::getRecordFields, @@ -355,7 +355,7 @@ public List getBeanProperties(BeanPropertiesQuery beanPropertie }; } } - return AstBeanPropertiesUtils.resolveBeanProperties(beanPropertiesQuery, + return AstBeanPropertiesUtils.resolveBeanProperties(propertyElementQuery, this, () -> getEnclosedElements(ElementQuery.ALL_METHODS), () -> getEnclosedElements(ElementQuery.ALL_FIELDS), diff --git a/inject-java/src/test/groovy/io/micronaut/inject/configproperties/ConfigPropertiesParseSpec.groovy b/inject-java/src/test/groovy/io/micronaut/inject/configproperties/ConfigPropertiesParseSpec.groovy index 69a903db13b..4c43775fec9 100644 --- a/inject-java/src/test/groovy/io/micronaut/inject/configproperties/ConfigPropertiesParseSpec.groovy +++ b/inject-java/src/test/groovy/io/micronaut/inject/configproperties/ConfigPropertiesParseSpec.groovy @@ -1,6 +1,7 @@ package io.micronaut.inject.configproperties import io.micronaut.context.ApplicationContext +import io.micronaut.context.ApplicationContextBuilder import io.micronaut.context.BeanContext import io.micronaut.context.annotation.ConfigurationReader import io.micronaut.context.annotation.Property @@ -13,6 +14,42 @@ import io.micronaut.inject.configuration.Engine class ConfigPropertiesParseSpec extends AbstractTypeElementSpec { + void "test configuration properties with mixed getters/setters"() { + when: + def context = buildContext(''' +package test; + +import io.micronaut.context.annotation.*; +import io.micronaut.core.annotation.Nullable; +import java.time.Duration; +import java.util.Optional; + +@ConfigurationProperties("foo.bar") +class MyConfig { + String host; + + + public Optional getHost() { + return Optional.ofNullable(host); + } + + public void setHost(@Nullable String host) { + this.host = host; + } +} + +''') + def config = getBean(context, 'test.MyConfig') + + then: + config.host.get() == 'bar' + } + + @Override + protected void configureContext(ApplicationContextBuilder contextBuilder) { + contextBuilder.properties('foo.bar.host':'bar') + } + void "test inner class paths - pojo inheritance"() { when: BeanDefinition beanDefinition = buildBeanDefinition('test.MyConfig$ChildConfig', ''' From 65adec7d2d92fbf1ce6eb7f672ce949281e899f7 Mon Sep 17 00:00:00 2001 From: Graeme Rocher Date: Thu, 1 Dec 2022 16:06:10 +0100 Subject: [PATCH 283/743] Fix issue injecting qualifier into interceptor (#8453) --- .../EachBeanIntrospectionSpec.groovy | 24 +++++++++++++++++++ .../inject/foreach/introduction/MyBean.java | 14 +++++++++++ .../foreach/introduction/XDataSource.java | 7 ++++++ .../inject/foreach/introduction/XSession.java | 6 +++++ .../foreach/introduction/XSessionFactory.java | 5 ++++ .../introduction/XSessionFactoryBuilder.java | 17 +++++++++++++ .../introduction/XTransactionalAdvice.java | 11 +++++++++ .../XTransactionalInterceptor.java | 22 +++++++++++++++++ .../introduction/XTransactionalSession.java | 8 +++++++ .../AbstractInitializableBeanDefinition.java | 6 +++++ 10 files changed, 120 insertions(+) create mode 100644 inject-java/src/test/groovy/io/micronaut/inject/foreach/introduction/EachBeanIntrospectionSpec.groovy create mode 100644 inject-java/src/test/groovy/io/micronaut/inject/foreach/introduction/MyBean.java create mode 100644 inject-java/src/test/groovy/io/micronaut/inject/foreach/introduction/XDataSource.java create mode 100644 inject-java/src/test/groovy/io/micronaut/inject/foreach/introduction/XSession.java create mode 100644 inject-java/src/test/groovy/io/micronaut/inject/foreach/introduction/XSessionFactory.java create mode 100644 inject-java/src/test/groovy/io/micronaut/inject/foreach/introduction/XSessionFactoryBuilder.java create mode 100644 inject-java/src/test/groovy/io/micronaut/inject/foreach/introduction/XTransactionalAdvice.java create mode 100644 inject-java/src/test/groovy/io/micronaut/inject/foreach/introduction/XTransactionalInterceptor.java create mode 100644 inject-java/src/test/groovy/io/micronaut/inject/foreach/introduction/XTransactionalSession.java diff --git a/inject-java/src/test/groovy/io/micronaut/inject/foreach/introduction/EachBeanIntrospectionSpec.groovy b/inject-java/src/test/groovy/io/micronaut/inject/foreach/introduction/EachBeanIntrospectionSpec.groovy new file mode 100644 index 00000000000..824e018d53b --- /dev/null +++ b/inject-java/src/test/groovy/io/micronaut/inject/foreach/introduction/EachBeanIntrospectionSpec.groovy @@ -0,0 +1,24 @@ +package io.micronaut.inject.foreach.introduction + +import io.micronaut.context.ApplicationContext +import spock.lang.Specification + +class EachBeanIntrospectionSpec extends Specification { + + void "test inject correct qualifier into interceptor downstream of EachBean"() { + given: + def context = ApplicationContext.run( + 'datasources.one.test': 1, + 'datasources.two.test': 2 + ) + + def bean = context.getBean(MyBean) + + expect: + bean.sessionOne.name() == 'one' + bean.sessionTwo.name() == 'two' + + cleanup: + context.close() + } +} diff --git a/inject-java/src/test/groovy/io/micronaut/inject/foreach/introduction/MyBean.java b/inject-java/src/test/groovy/io/micronaut/inject/foreach/introduction/MyBean.java new file mode 100644 index 00000000000..2cdc9fb085c --- /dev/null +++ b/inject-java/src/test/groovy/io/micronaut/inject/foreach/introduction/MyBean.java @@ -0,0 +1,14 @@ +package io.micronaut.inject.foreach.introduction; + +import jakarta.inject.Inject; +import jakarta.inject.Named; + +public class MyBean { + @Inject + @Named("one") + XSession sessionOne; + + @Inject + @Named("two") + XSession sessionTwo; +} diff --git a/inject-java/src/test/groovy/io/micronaut/inject/foreach/introduction/XDataSource.java b/inject-java/src/test/groovy/io/micronaut/inject/foreach/introduction/XDataSource.java new file mode 100644 index 00000000000..cb2c8c64c19 --- /dev/null +++ b/inject-java/src/test/groovy/io/micronaut/inject/foreach/introduction/XDataSource.java @@ -0,0 +1,7 @@ +package io.micronaut.inject.foreach.introduction; + +import io.micronaut.context.annotation.EachProperty; + +@EachProperty("datasources") +public class XDataSource { +} diff --git a/inject-java/src/test/groovy/io/micronaut/inject/foreach/introduction/XSession.java b/inject-java/src/test/groovy/io/micronaut/inject/foreach/introduction/XSession.java new file mode 100644 index 00000000000..86d6c988335 --- /dev/null +++ b/inject-java/src/test/groovy/io/micronaut/inject/foreach/introduction/XSession.java @@ -0,0 +1,6 @@ +package io.micronaut.inject.foreach.introduction; + + +public interface XSession { + String name(); +} diff --git a/inject-java/src/test/groovy/io/micronaut/inject/foreach/introduction/XSessionFactory.java b/inject-java/src/test/groovy/io/micronaut/inject/foreach/introduction/XSessionFactory.java new file mode 100644 index 00000000000..2a8f7e8095e --- /dev/null +++ b/inject-java/src/test/groovy/io/micronaut/inject/foreach/introduction/XSessionFactory.java @@ -0,0 +1,5 @@ +package io.micronaut.inject.foreach.introduction; + +public interface XSessionFactory { + String name(); +} diff --git a/inject-java/src/test/groovy/io/micronaut/inject/foreach/introduction/XSessionFactoryBuilder.java b/inject-java/src/test/groovy/io/micronaut/inject/foreach/introduction/XSessionFactoryBuilder.java new file mode 100644 index 00000000000..ea52e392617 --- /dev/null +++ b/inject-java/src/test/groovy/io/micronaut/inject/foreach/introduction/XSessionFactoryBuilder.java @@ -0,0 +1,17 @@ +package io.micronaut.inject.foreach.introduction; + +import io.micronaut.context.annotation.EachBean; +import io.micronaut.context.annotation.Factory; +import io.micronaut.context.annotation.Parameter; + +@Factory +public class XSessionFactoryBuilder { + + @EachBean(XDataSource.class) + XSessionFactory xSessionFactory(@Parameter String name) { + return new XSessionFactoryImpl(name); + } + + private record XSessionFactoryImpl(String name) implements XSessionFactory { + } +} diff --git a/inject-java/src/test/groovy/io/micronaut/inject/foreach/introduction/XTransactionalAdvice.java b/inject-java/src/test/groovy/io/micronaut/inject/foreach/introduction/XTransactionalAdvice.java new file mode 100644 index 00000000000..d18b39b1c2c --- /dev/null +++ b/inject-java/src/test/groovy/io/micronaut/inject/foreach/introduction/XTransactionalAdvice.java @@ -0,0 +1,11 @@ +package io.micronaut.inject.foreach.introduction; + +import io.micronaut.aop.Introduction; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +@Introduction +@Retention(RetentionPolicy.RUNTIME) +public @interface XTransactionalAdvice { +} diff --git a/inject-java/src/test/groovy/io/micronaut/inject/foreach/introduction/XTransactionalInterceptor.java b/inject-java/src/test/groovy/io/micronaut/inject/foreach/introduction/XTransactionalInterceptor.java new file mode 100644 index 00000000000..1d311b64369 --- /dev/null +++ b/inject-java/src/test/groovy/io/micronaut/inject/foreach/introduction/XTransactionalInterceptor.java @@ -0,0 +1,22 @@ +package io.micronaut.inject.foreach.introduction; + +import io.micronaut.aop.InterceptorBean; +import io.micronaut.aop.MethodInterceptor; +import io.micronaut.aop.MethodInvocationContext; +import io.micronaut.context.BeanContext; +import io.micronaut.context.Qualifier; +import io.micronaut.context.annotation.Prototype; + +@InterceptorBean(XTransactionalAdvice.class) +@Prototype +public class XTransactionalInterceptor implements MethodInterceptor { + private final XSessionFactory sessionFactory; + + XTransactionalInterceptor(BeanContext beanContext, Qualifier qualifier) { + this.sessionFactory = beanContext.getBean(XSessionFactory.class, qualifier); + } + @Override + public Object intercept(MethodInvocationContext context) { + return sessionFactory.name(); + } +} diff --git a/inject-java/src/test/groovy/io/micronaut/inject/foreach/introduction/XTransactionalSession.java b/inject-java/src/test/groovy/io/micronaut/inject/foreach/introduction/XTransactionalSession.java new file mode 100644 index 00000000000..cf69e232203 --- /dev/null +++ b/inject-java/src/test/groovy/io/micronaut/inject/foreach/introduction/XTransactionalSession.java @@ -0,0 +1,8 @@ +package io.micronaut.inject.foreach.introduction; + +import io.micronaut.context.annotation.EachBean; + +@EachBean(XSessionFactory.class) +@XTransactionalAdvice +public interface XTransactionalSession extends XSession { +} diff --git a/inject/src/main/java/io/micronaut/context/AbstractInitializableBeanDefinition.java b/inject/src/main/java/io/micronaut/context/AbstractInitializableBeanDefinition.java index ec1597206be..bea7b8eb06e 100644 --- a/inject/src/main/java/io/micronaut/context/AbstractInitializableBeanDefinition.java +++ b/inject/src/main/java/io/micronaut/context/AbstractInitializableBeanDefinition.java @@ -2379,6 +2379,12 @@ private Qualifier resolveQualifier(BeanResolutionContext resolutionCon currentQualifier.getClass() != InterceptorBindingQualifier.class && currentQualifier.getClass() != TypeAnnotationQualifier.class) { return currentQualifier; + } else { + BeanResolutionContext.Path path = resolutionContext.getPath(); + BeanResolutionContext.Segment segment = path.peek(); + if (segment != null && segment.getDeclaringType().hasAnnotation(AnnotationUtil.ANN_INTERCEPTOR_BINDINGS)) { + return resolutionContext.getConfigurationPath().beanQualifier(); + } } } else if (isIterable && resultType.isAnnotationPresent(Parameter.class)) { return (Qualifier) resolutionContext.getCurrentQualifier(); From 4fddaa94c05093c171fd88dfc28fa311d6ba9e39 Mon Sep 17 00:00:00 2001 From: Jonas Konrad Date: Fri, 2 Dec 2022 12:06:46 +0100 Subject: [PATCH 284/743] Support records for `@RequestBean` (#8420) --- .../routes/rules/MissingParameterRule.java | 23 ++- .../rules/RequestBeanParameterRule.java | 16 +- .../RequestBeanParameterRuleSpec.groovy | 143 +++++++++++------- .../test/AbstractTypeElementSpec.groovy | 16 +- 4 files changed, 127 insertions(+), 71 deletions(-) diff --git a/http-validation/src/main/java/io/micronaut/validation/routes/rules/MissingParameterRule.java b/http-validation/src/main/java/io/micronaut/validation/routes/rules/MissingParameterRule.java index 313dab5a412..02953bd2918 100644 --- a/http-validation/src/main/java/io/micronaut/validation/routes/rules/MissingParameterRule.java +++ b/http-validation/src/main/java/io/micronaut/validation/routes/rules/MissingParameterRule.java @@ -15,11 +15,13 @@ */ package io.micronaut.validation.routes.rules; +import io.micronaut.core.annotation.AnnotatedElement; import io.micronaut.core.bind.annotation.Bindable; +import io.micronaut.core.naming.Named; import io.micronaut.http.uri.UriMatchTemplate; +import io.micronaut.inject.ast.ClassElement; import io.micronaut.inject.ast.MethodElement; import io.micronaut.inject.ast.ParameterElement; -import io.micronaut.inject.ast.PropertyElement; import io.micronaut.validation.routes.RouteValidationResult; import java.util.ArrayList; @@ -27,8 +29,10 @@ import java.util.LinkedHashSet; import java.util.List; import java.util.Objects; +import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; +import java.util.stream.Stream; /** * Validates all route uri variables are present in the route arguments. @@ -48,16 +52,16 @@ public RouteValidationResult validate(List templates, Paramete .filter(p -> p.hasAnnotation("io.micronaut.http.annotation.Body")) .map(ParameterElement::getType) .filter(Objects::nonNull) - .flatMap(t -> t.getBeanProperties().stream()) - .map(PropertyElement::getName) + .flatMap(MissingParameterRule::findProperties) + .map(Named::getName) .collect(Collectors.toList())); // RequestBean has properties inside routeVariables.addAll(Arrays.stream(parameters) .filter(p -> p.hasAnnotation("io.micronaut.http.annotation.RequestBean")) .map(ParameterElement::getType) - .flatMap(t -> t.getBeanProperties().stream()) - .filter(p -> p.hasStereotype(Bindable.class)) + .flatMap(MissingParameterRule::findProperties) + .filter(p -> p.getAnnotationMetadata().hasStereotype(Bindable.class)) .map(p -> p.getAnnotationMetadata().stringValue(Bindable.class).orElse(p.getName())) .collect(Collectors.toSet())); @@ -72,4 +76,13 @@ public RouteValidationResult validate(List templates, Paramete return new RouteValidationResult(errorMessages.toArray(new String[0])); } + private static Stream findProperties(ClassElement t) { + if (t.isRecord()) { + Optional primaryConstructor = t.getPrimaryConstructor(); + if (primaryConstructor.isPresent()) { + return Arrays.stream(primaryConstructor.get().getParameters()); + } + } + return t.getBeanProperties().stream(); + } } diff --git a/http-validation/src/main/java/io/micronaut/validation/routes/rules/RequestBeanParameterRule.java b/http-validation/src/main/java/io/micronaut/validation/routes/rules/RequestBeanParameterRule.java index aac4a23bd21..5aa576e9226 100644 --- a/http-validation/src/main/java/io/micronaut/validation/routes/rules/RequestBeanParameterRule.java +++ b/http-validation/src/main/java/io/micronaut/validation/routes/rules/RequestBeanParameterRule.java @@ -52,15 +52,17 @@ private List validate(ParameterElement parameterElement) { // @Creator constructor List constructorParameters = Arrays.asList(primaryConstructor.get().getParameters()); - // Check no constructor parameter has any @Bindable annotation - // We could allow this, but this would add some complexity, some annotations that can be used in combination - // with @Bindable works only on fields (e.g. bean validation annotations) and this might confuse Micronaut users - constructorParameters.stream() + if (!parameterElement.getType().isRecord()) { + // Check no constructor parameter has any @Bindable annotation + // We could allow this, but this would add some complexity, some annotations that can be used in combination + // with @Bindable works only on fields (e.g. bean validation annotations) and this might confuse Micronaut users + constructorParameters.stream() .filter(p -> p.hasStereotype(Bindable.class)) .forEach(p -> errors.add("Parameter of Primary Constructor (or @Creator Method) [" + p.getName() + "] for type [" - + parameterElement.getType().getName() + "] has one of @Bindable annotations. This is not supported." - + "\nNote1: Primary constructor is a constructor that have parameters or is annotated with @Creator." - + "\nNote2: In case you have multiple @Creator constructors, first is used as primary constructor.")); + + parameterElement.getType().getName() + "] has one of @Bindable annotations. This is not supported." + + "\nNote1: Primary constructor is a constructor that have parameters or is annotated with @Creator." + + "\nNote2: In case you have multiple @Creator constructors, first is used as primary constructor.")); + } // Check readonly bindable properties can be set via constructor beanProperties.stream() diff --git a/http-validation/src/test/groovy/io/micronaut/validation/routes/RequestBeanParameterRuleSpec.groovy b/http-validation/src/test/groovy/io/micronaut/validation/routes/RequestBeanParameterRuleSpec.groovy index fcb126bbfe6..11a988792a2 100644 --- a/http-validation/src/test/groovy/io/micronaut/validation/routes/RequestBeanParameterRuleSpec.groovy +++ b/http-validation/src/test/groovy/io/micronaut/validation/routes/RequestBeanParameterRuleSpec.groovy @@ -1,6 +1,7 @@ package io.micronaut.validation.routes import io.micronaut.annotation.processing.test.AbstractTypeElementSpec +import spock.lang.IgnoreIf class RequestBeanParameterRuleSpec extends AbstractTypeElementSpec { @@ -21,22 +22,22 @@ class Foo { String abc(@RequestBean Bean bean) { return ""; } - + @Introspected private static class Bean { - + @Nullable @QueryValue private final String abc; - + public Bean(String abc) { this.abc = abc; } - + public String getAbc() { return abc; } - + } - + } """) @@ -61,35 +62,35 @@ class Foo { String abc(@RequestBean Bean bean) { return ""; } - + @Introspected private static class Bean { - + @Nullable @QueryValue private final String abc; - + @Nullable @QueryValue private final String def; - + public Bean(String abc) { this.abc = abc; this.def = null; } - - @Creator + + @Creator public Bean(String abc, String def) { this.abc = abc; this.def = def; } - + public String getAbc() { return abc; } - + public String getDef() { return def; } - + } - + } """) @@ -114,25 +115,55 @@ class Foo { String abc(@RequestBean Bean bean) { return ""; } - + @Introspected private static class Bean { - + @Nullable @QueryValue private String abc; - + @Creator public static Bean of(String abc) { Bean bean = new Bean(); bean.abc = abc; return bean; } - + public String getAbc() { return abc; } - + + } + +} + +""") + then: + noExceptionThrown() + } + + @IgnoreIf({ !jvm.isJava14Compatible() }) + void "test RequestBean compiles with record"() { + when: + buildTypeElement(""" + +package test; + +import io.micronaut.http.annotation.*; +import io.micronaut.core.annotation.*; +import io.micronaut.core.annotation.Nullable; + +@Controller("/foo") +class Foo { + + @Get("/abc/{abc}") + String abc(@RequestBean Bean bean) { + return ""; + } + + @Introspected + public record Bean(@Nullable @PathVariable String abc) { } - + } """) @@ -157,20 +188,20 @@ class Foo { String abc(@RequestBean Bean bean) { return ""; } - + @Introspected private static class Bean { - + @Nullable @QueryValue private String abc; - + public String getAbc() { return abc; } - + public void setAbc(String abc) { this.abc = abc; } - + } - + } """) @@ -195,18 +226,18 @@ class Foo { String abc(@RequestBean Bean bean) { return ""; } - + @Introspected public static class Bean { - + @Nullable @QueryValue private String abc; - + public String getAbc() { return abc; } - + } - + } """) @@ -232,28 +263,28 @@ class Foo { String abc(@RequestBean Bean bean) { return ""; } - + @Introspected public static class Bean { - + @Nullable @QueryValue private String abc; - + @Nullable @QueryValue private String def; - + public Bean(String def) { this.def = def; } - + public String getAbc() { return abc; } - + public String getDef() { return def; } - + } - + } """) @@ -279,23 +310,23 @@ class Foo { String abc(@RequestBean Bean bean) { return ""; } - + @Introspected public static class Bean { - + @Nullable @QueryValue private String abc; - + @Creator public Bean(@Nullable @QueryValue String abc) { this.abc = abc; } - + public String getAbc() { return abc; } - + } - + } """) @@ -321,31 +352,31 @@ class Foo { String abc(@RequestBean Bean bean) { return ""; } - + @Introspected public static class Bean { - + @Nullable @QueryValue private String abc; - + @Nullable @QueryValue private String def; - + @Creator public Bean(String def) { this.def = def; } - + public String getAbc() { return abc; } - + public void setAbc() { this.abc = abc; } - + public String getDef() { return def; } - + } - + } """) diff --git a/inject-java-test/src/main/groovy/io/micronaut/annotation/processing/test/AbstractTypeElementSpec.groovy b/inject-java-test/src/main/groovy/io/micronaut/annotation/processing/test/AbstractTypeElementSpec.groovy index 0430afc2963..9ec05b0a71b 100644 --- a/inject-java-test/src/main/groovy/io/micronaut/annotation/processing/test/AbstractTypeElementSpec.groovy +++ b/inject-java-test/src/main/groovy/io/micronaut/annotation/processing/test/AbstractTypeElementSpec.groovy @@ -17,11 +17,20 @@ package io.micronaut.annotation.processing.test import com.sun.source.util.JavacTask import groovy.transform.CompileStatic -import io.micronaut.annotation.processing.* +import io.micronaut.annotation.processing.AggregatingTypeElementVisitorProcessor +import io.micronaut.annotation.processing.AnnotationUtils +import io.micronaut.annotation.processing.GenericUtils +import io.micronaut.annotation.processing.JavaAnnotationMetadataBuilder +import io.micronaut.annotation.processing.ModelUtils +import io.micronaut.annotation.processing.TypeElementVisitorProcessor import io.micronaut.annotation.processing.visitor.JavaElementFactory import io.micronaut.annotation.processing.visitor.JavaVisitorContext import io.micronaut.aop.internal.InterceptorRegistryBean -import io.micronaut.context.* +import io.micronaut.context.ApplicationContext +import io.micronaut.context.ApplicationContextBuilder +import io.micronaut.context.ApplicationContextConfiguration +import io.micronaut.context.DefaultApplicationContext +import io.micronaut.context.Qualifier import io.micronaut.context.event.ApplicationEventPublisherFactory import io.micronaut.core.annotation.AnnotationMetadata import io.micronaut.core.annotation.Experimental @@ -59,6 +68,7 @@ import javax.tools.JavaFileObject import java.lang.annotation.Annotation import java.util.stream.Collectors import java.util.stream.StreamSupport + /** * Base class to extend from to allow compilation of Java sources * at runtime to allow testing of compile time behavior. @@ -338,7 +348,7 @@ class Test { return metadata } - protected TypeElement buildTypeElement(String cls) { + protected TypeElement buildTypeElement(@Language('java') String cls) { List elements = [] newJavaParser().parseLines("", From e6bf9a6812032e1ed2ac842de82162ec96ac8f5e Mon Sep 17 00:00:00 2001 From: Graeme Rocher Date: Fri, 2 Dec 2022 15:44:54 +0100 Subject: [PATCH 285/743] Fix broken ServiceHttpClientCondition (#8456) --- .../client/ServiceHttpClientCondition.java | 22 ++++++++----------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/http-client-core/src/main/java/io/micronaut/http/client/ServiceHttpClientCondition.java b/http-client-core/src/main/java/io/micronaut/http/client/ServiceHttpClientCondition.java index 5d01099840c..3d5fe9cb0d2 100644 --- a/http-client-core/src/main/java/io/micronaut/http/client/ServiceHttpClientCondition.java +++ b/http-client-core/src/main/java/io/micronaut/http/client/ServiceHttpClientCondition.java @@ -17,15 +17,13 @@ import io.micronaut.context.ApplicationContext; import io.micronaut.context.BeanContext; +import io.micronaut.context.Qualifier; import io.micronaut.context.condition.Condition; import io.micronaut.context.condition.ConditionContext; -import io.micronaut.context.env.Environment; import io.micronaut.core.annotation.AnnotationMetadataProvider; import io.micronaut.core.annotation.Internal; import io.micronaut.core.naming.Named; -import io.micronaut.core.value.ValueResolver; - -import java.util.Optional; +import io.micronaut.inject.QualifiedBeanType; /** * Disables the client beans if the appropriate configuration is not present. @@ -40,15 +38,13 @@ public boolean matches(ConditionContext context) { AnnotationMetadataProvider component = context.getComponent(); BeanContext beanContext = context.getBeanContext(); - if (beanContext instanceof ApplicationContext) { - Environment env = ((ApplicationContext) beanContext).getEnvironment(); - if (component instanceof ValueResolver) { - Optional optional = ((ValueResolver) component).get(Named.class.getName(), String.class); - if (optional.isPresent()) { - String serviceName = optional.get(); - String urlProp = ServiceHttpClientConfiguration.PREFIX + "." + serviceName + ".url"; - return env.containsProperty(urlProp) || env.containsProperty(urlProp + "s"); - } + if (beanContext instanceof ApplicationContext applicationContext && + component instanceof QualifiedBeanType qualifiedBeanType) { + Qualifier declaredQualifier = qualifiedBeanType.getDeclaredQualifier(); + if (declaredQualifier instanceof Named named) { + String serviceName = named.getName(); + String urlProp = ServiceHttpClientConfiguration.PREFIX + "." + serviceName + ".url"; + return applicationContext.containsProperty(urlProp) || applicationContext.containsProperty(urlProp + "s"); } } return true; From d78efca14c825c648e65fb2e83761546fef21280 Mon Sep 17 00:00:00 2001 From: Graeme Rocher Date: Fri, 2 Dec 2022 21:28:18 +0100 Subject: [PATCH 286/743] Don't include write only properties in serialization (#8457) --- ...bstractInitializableBeanIntrospection.java | 2 +- .../modules/BeanIntrospectionModule.java | 4 ++- .../BeanIntrospectionModuleSpec.groovy | 24 ++++++++++++++ .../modules/testclasses/HTTPCheck.java | 32 +++++++++++++++++++ 4 files changed, 60 insertions(+), 2 deletions(-) create mode 100644 jackson-databind/src/test/groovy/io/micronaut/jackson/modules/testclasses/HTTPCheck.java diff --git a/inject/src/main/java/io/micronaut/inject/beans/AbstractInitializableBeanIntrospection.java b/inject/src/main/java/io/micronaut/inject/beans/AbstractInitializableBeanIntrospection.java index 3e8b0ba0b37..5559ed42fc0 100644 --- a/inject/src/main/java/io/micronaut/inject/beans/AbstractInitializableBeanIntrospection.java +++ b/inject/src/main/java/io/micronaut/inject/beans/AbstractInitializableBeanIntrospection.java @@ -422,7 +422,7 @@ public P get(@NonNull B bean) { throw new IllegalArgumentException("Invalid bean [" + bean + "] for type: " + beanType); } if (isWriteOnly()) { - throw new UnsupportedOperationException("Cannot read from a write-only property"); + throw new UnsupportedOperationException("Cannot read from a write-only property: " + getName()); } return dispatchOne(ref.getMethodIndex, bean, null); } diff --git a/jackson-databind/src/main/java/io/micronaut/jackson/modules/BeanIntrospectionModule.java b/jackson-databind/src/main/java/io/micronaut/jackson/modules/BeanIntrospectionModule.java index 5419e8dc8a2..9cb190d388c 100644 --- a/jackson-databind/src/main/java/io/micronaut/jackson/modules/BeanIntrospectionModule.java +++ b/jackson-databind/src/main/java/io/micronaut/jackson/modules/BeanIntrospectionModule.java @@ -335,7 +335,9 @@ public JsonSerializer build() { final List newProperties = new ArrayList<>(properties); Map> named = new LinkedHashMap<>(); for (BeanProperty beanProperty : beanProperties) { - named.put(getName(config, namingStrategy, beanProperty), beanProperty); + if (!beanProperty.isWriteOnly()) { + named.put(getName(config, namingStrategy, beanProperty), beanProperty); + } } for (int i = 0; i < properties.size(); i++) { final BeanPropertyWriter existing = properties.get(i); diff --git a/jackson-databind/src/test/groovy/io/micronaut/jackson/modules/BeanIntrospectionModuleSpec.groovy b/jackson-databind/src/test/groovy/io/micronaut/jackson/modules/BeanIntrospectionModuleSpec.groovy index 0e09ea561d4..cc99c764324 100644 --- a/jackson-databind/src/test/groovy/io/micronaut/jackson/modules/BeanIntrospectionModuleSpec.groovy +++ b/jackson-databind/src/test/groovy/io/micronaut/jackson/modules/BeanIntrospectionModuleSpec.groovy @@ -32,6 +32,7 @@ import io.micronaut.http.hateoas.JsonError import io.micronaut.jackson.JacksonConfiguration import io.micronaut.jackson.modules.testcase.EmailTemplate import io.micronaut.jackson.modules.testcase.Notification +import io.micronaut.jackson.modules.testclasses.HTTPCheck import io.micronaut.jackson.modules.wrappers.* import spock.lang.Issue import spock.lang.Unroll @@ -42,6 +43,29 @@ import java.time.LocalDateTime class BeanIntrospectionModuleSpec extends Specification { + void "test serialize/deserialize convertible values"() { + given: + ApplicationContext ctx = ApplicationContext.run() + ObjectMapper objectMapper = ctx.getBean(ObjectMapper) + + when: + HTTPCheck check = new HTTPCheck(headers:[ + Accept:['application/json', 'application/xml'] + ] ) + + def result = objectMapper.writeValueAsString(check) + + then: + result == '{"Header":{"Accept":["application/json","application/xml"]}}' + + when: + def read = objectMapper.readValue(result, HTTPCheck) + + then: + check.header.getAll("Accept") == read.header.getAll("Accept") + + } + void "Bean introspection works with a bean without JsonIgnore annotations"() { given: ApplicationContext ctx = ApplicationContext.run() diff --git a/jackson-databind/src/test/groovy/io/micronaut/jackson/modules/testclasses/HTTPCheck.java b/jackson-databind/src/test/groovy/io/micronaut/jackson/modules/testclasses/HTTPCheck.java new file mode 100644 index 00000000000..6ace4bbc12d --- /dev/null +++ b/jackson-databind/src/test/groovy/io/micronaut/jackson/modules/testclasses/HTTPCheck.java @@ -0,0 +1,32 @@ +package io.micronaut.jackson.modules.testclasses; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import com.fasterxml.jackson.databind.annotation.JsonNaming; +import io.micronaut.core.annotation.Introspected; +import io.micronaut.core.convert.value.ConvertibleMultiValues; + +import java.util.List; +import java.util.Map; + +@JsonNaming(PropertyNamingStrategies.UpperCamelCaseStrategy.class) +@Introspected +public class HTTPCheck { + private ConvertibleMultiValues headers = ConvertibleMultiValues.empty(); + + public ConvertibleMultiValues getHeader() { + return headers; + } + + /** + * @param headers The headers + */ + @JsonProperty("Header") + public void setHeaders(Map> headers) { + if (headers == null) { + this.headers = ConvertibleMultiValues.empty(); + } else { + this.headers = ConvertibleMultiValues.of(headers); + } + } +} From e1fc2c0f66c427bc79cb7210b17c19dbd03951ec Mon Sep 17 00:00:00 2001 From: Graeme Rocher Date: Wed, 7 Dec 2022 10:10:15 +0100 Subject: [PATCH 287/743] Fix bug with custom conditions and EachBean definitions (#8459) --- .../condition/EachBeanConditionSpec.groovy | 42 +++++++++++++++++++ .../inject/foreach/condition/XClient.java | 9 ++++ .../inject/foreach/condition/XCondition.java | 27 ++++++++++++ .../inject/foreach/condition/XConfig.java | 7 ++++ .../inject/foreach/condition/XConfigMany.java | 18 ++++++++ .../inject/foreach/condition/XConfigOne.java | 29 +++++++++++++ .../context/DefaultApplicationContext.java | 4 +- 7 files changed, 135 insertions(+), 1 deletion(-) create mode 100644 inject-java/src/test/groovy/io/micronaut/inject/foreach/condition/EachBeanConditionSpec.groovy create mode 100644 inject-java/src/test/groovy/io/micronaut/inject/foreach/condition/XClient.java create mode 100644 inject-java/src/test/groovy/io/micronaut/inject/foreach/condition/XCondition.java create mode 100644 inject-java/src/test/groovy/io/micronaut/inject/foreach/condition/XConfig.java create mode 100644 inject-java/src/test/groovy/io/micronaut/inject/foreach/condition/XConfigMany.java create mode 100644 inject-java/src/test/groovy/io/micronaut/inject/foreach/condition/XConfigOne.java diff --git a/inject-java/src/test/groovy/io/micronaut/inject/foreach/condition/EachBeanConditionSpec.groovy b/inject-java/src/test/groovy/io/micronaut/inject/foreach/condition/EachBeanConditionSpec.groovy new file mode 100644 index 00000000000..267bbfab40f --- /dev/null +++ b/inject-java/src/test/groovy/io/micronaut/inject/foreach/condition/EachBeanConditionSpec.groovy @@ -0,0 +1,42 @@ +package io.micronaut.inject.foreach.condition + +import io.micronaut.context.ApplicationContext +import io.micronaut.inject.qualifiers.Qualifiers +import spock.lang.Specification + +class EachBeanConditionSpec extends Specification { + + void "test each bean with custom condition - failure"() { + given: + def ctx = ApplicationContext.run( + 'foo.one.enabled':false, + 'foo.two.enabled':false, + 'configs.two.name':'two' + ) + + expect: + ctx.containsBean(XConfigOne) + ctx.containsBean(XConfigMany, Qualifiers.byName("two")) + !ctx.containsBean(XClient, Qualifiers.byName("one")) + !ctx.containsBean(XClient, Qualifiers.byName("two")) + + cleanup: + ctx.close() + } + + void "test each bean with custom condition - success"() { + given: + def ctx = ApplicationContext.run( + 'configs.two.enabled':true + ) + + expect: + ctx.containsBean(XConfigOne) + ctx.containsBean(XConfigMany, Qualifiers.byName("two")) + ctx.containsBean(XClient, Qualifiers.byName("one")) + ctx.containsBean(XClient, Qualifiers.byName("two")) + + cleanup: + ctx.close() + } +} diff --git a/inject-java/src/test/groovy/io/micronaut/inject/foreach/condition/XClient.java b/inject-java/src/test/groovy/io/micronaut/inject/foreach/condition/XClient.java new file mode 100644 index 00000000000..a7184c03585 --- /dev/null +++ b/inject-java/src/test/groovy/io/micronaut/inject/foreach/condition/XClient.java @@ -0,0 +1,9 @@ +package io.micronaut.inject.foreach.condition; + +import io.micronaut.context.annotation.EachBean; +import io.micronaut.context.annotation.Requires; + +@EachBean(XConfig.class) +@Requires(condition = XCondition.class) +public class XClient { +} diff --git a/inject-java/src/test/groovy/io/micronaut/inject/foreach/condition/XCondition.java b/inject-java/src/test/groovy/io/micronaut/inject/foreach/condition/XCondition.java new file mode 100644 index 00000000000..9cb9f2745b9 --- /dev/null +++ b/inject-java/src/test/groovy/io/micronaut/inject/foreach/condition/XCondition.java @@ -0,0 +1,27 @@ +package io.micronaut.inject.foreach.condition; + +import io.micronaut.context.ApplicationContext; +import io.micronaut.context.BeanContext; +import io.micronaut.context.Qualifier; +import io.micronaut.context.condition.Condition; +import io.micronaut.context.condition.ConditionContext; +import io.micronaut.core.naming.Named; +import io.micronaut.inject.QualifiedBeanType; + +public class XCondition implements Condition { + @Override + public boolean matches(ConditionContext context) { + BeanContext beanContext = context.getBeanContext(); + if (beanContext instanceof ApplicationContext appCtx && + context.getComponent() instanceof QualifiedBeanType qualifiedBeanType) { + Qualifier declaredQualifier = qualifiedBeanType.getDeclaredQualifier(); + if (declaredQualifier instanceof Named named) { + return appCtx.getProperty( + "foo." + named.getName() + ".enabled", + Boolean.class + ).orElse(true); + } + } + return true; + } +} diff --git a/inject-java/src/test/groovy/io/micronaut/inject/foreach/condition/XConfig.java b/inject-java/src/test/groovy/io/micronaut/inject/foreach/condition/XConfig.java new file mode 100644 index 00000000000..a434272b0ae --- /dev/null +++ b/inject-java/src/test/groovy/io/micronaut/inject/foreach/condition/XConfig.java @@ -0,0 +1,7 @@ +package io.micronaut.inject.foreach.condition; + +public interface XConfig { + String getName(); + + void setName(String name); +} diff --git a/inject-java/src/test/groovy/io/micronaut/inject/foreach/condition/XConfigMany.java b/inject-java/src/test/groovy/io/micronaut/inject/foreach/condition/XConfigMany.java new file mode 100644 index 00000000000..33059503a96 --- /dev/null +++ b/inject-java/src/test/groovy/io/micronaut/inject/foreach/condition/XConfigMany.java @@ -0,0 +1,18 @@ +package io.micronaut.inject.foreach.condition; + +import io.micronaut.context.annotation.EachProperty; + +@EachProperty("configs") +public class XConfigMany implements XConfig { + private String name; + + @Override + public String getName() { + return name; + } + + @Override + public void setName(String name) { + this.name = name; + } +} diff --git a/inject-java/src/test/groovy/io/micronaut/inject/foreach/condition/XConfigOne.java b/inject-java/src/test/groovy/io/micronaut/inject/foreach/condition/XConfigOne.java new file mode 100644 index 00000000000..e8898e509f9 --- /dev/null +++ b/inject-java/src/test/groovy/io/micronaut/inject/foreach/condition/XConfigOne.java @@ -0,0 +1,29 @@ +package io.micronaut.inject.foreach.condition; + +import jakarta.inject.Named; +import jakarta.inject.Singleton; + +@Singleton +@Named("one") +public class XConfigOne implements XConfig { + private String name; + private boolean enabled; + + public boolean isEnabled() { + return enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + @Override + public String getName() { + return name; + } + + @Override + public void setName(String name) { + this.name = name; + } +} diff --git a/inject/src/main/java/io/micronaut/context/DefaultApplicationContext.java b/inject/src/main/java/io/micronaut/context/DefaultApplicationContext.java index b905fd50292..7a37a421a58 100644 --- a/inject/src/main/java/io/micronaut/context/DefaultApplicationContext.java +++ b/inject/src/main/java/io/micronaut/context/DefaultApplicationContext.java @@ -512,7 +512,9 @@ private void transformEachBeanBeanDefinition(@NonNull BeanResolutionContext qualifier = PrimaryQualifier.INSTANCE; } BeanDefinitionDelegate delegate = BeanDefinitionDelegate.create(candidate, (Qualifier) qualifier); - transformedCandidates.add((BeanDefinition) delegate); + if (delegate.isEnabled(this, resolutionContext)) { + transformedCandidates.add((BeanDefinition) delegate); + } } } } From a8085742fad1f553663cee92bc1ae5e64ea2423c Mon Sep 17 00:00:00 2001 From: Graeme Rocher Date: Wed, 7 Dec 2022 17:37:00 +0100 Subject: [PATCH 288/743] remove the transitive dependency on Guava from core-processor (#8466) --- core-processor/build.gradle | 5 ++++- gradle/libs.versions.toml | 2 ++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/core-processor/build.gradle b/core-processor/build.gradle index fd14f4c6ceb..4f5698c7530 100644 --- a/core-processor/build.gradle +++ b/core-processor/build.gradle @@ -7,7 +7,10 @@ dependencies { api project(":aop") api libs.asm.tree api libs.bundles.asm - api 'com.github.javaparser:javaparser-symbol-solver-core:3.24.7' + api(libs.managed.java.parser.core) { + exclude group:'org.javassist', module:'javassist' + exclude group:'com.google.guava', module:'guava' + } compileOnly libs.kotlin.stdlib.jdk8 compileOnly libs.managed.validation diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index b47ec18cf2c..f5c4d118fa1 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -74,6 +74,7 @@ managed-reactor = "3.4.24" managed-slf4j = "2.0.4" managed-snakeyaml = "1.33" managed-validation = "2.0.1.Final" +managed-java-parser-core = "3.24.7" micronaut-docs = "2.0.0" [libraries] @@ -107,6 +108,7 @@ managed-jackson-datatype-jsr310 = { module = "com.fasterxml.jackson.datatype:jac managed-jackson-module-afterburner = { module = "com.fasterxml.jackson.module:jackson-module-afterburner", version.ref = "managed-jackson" } managed-jackson-module-kotlin = { module = "com.fasterxml.jackson.module:jackson-module-kotlin", version.ref = "managed-jackson" } managed-jackson-module-parameterNames = { module = "com.fasterxml.jackson.module:jackson-module-parameter-names", version.ref = "managed-jackson" } +managed-java-parser-core = { module = "com.github.javaparser:javaparser-symbol-solver-core", version.ref = "managed-java-parser-core" } managed-methvin-directoryWatcher = { module = "io.methvin:directory-watcher", version.ref = "managed-methvin-directory-watcher" } From eb0d18e79571133843c799f10a933b851ff6cfeb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Champeau?= Date: Wed, 7 Dec 2022 17:37:15 +0100 Subject: [PATCH 289/743] Fix incorrect dependency substitution (#8467) This commit fixes the build issue which is seen when upgrading to latest Micronaut validation snapshot: https://ge.micronaut.io/s/ye2u5kc7pus3u/dependencies?focusedDependency=WzEsMSw3ODEsWzEsMSxbNzUzLDc4MV1dXQ&toggled=W1sxXSxbMSwxXSxbMSwxLFs3NTNdXV0 The problem was that a platform dependency wasn't subsistuted with a platform dependency but with a regular dependency, messing up with dependency resolution. --- .../io.micronaut.build.internal.convention-base.gradle | 6 +++++- .../build/internal/ext/DefaultMicronautCoreExtension.java | 2 ++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/buildSrc/src/main/groovy/io.micronaut.build.internal.convention-base.gradle b/buildSrc/src/main/groovy/io.micronaut.build.internal.convention-base.gradle index e8519462c74..6a3e6092838 100644 --- a/buildSrc/src/main/groovy/io.micronaut.build.internal.convention-base.gradle +++ b/buildSrc/src/main/groovy/io.micronaut.build.internal.convention-base.gradle @@ -62,7 +62,11 @@ configurations.all { resolutionStrategy.dependencySubstitution { rootProject.subprojects.each { if (!it.name.startsWith('test-')) { - substitute module("io.micronaut:micronaut-${it.name}") using project(":${it.name}") because "we want to test with what we're building" + if (it.name.contains('bom')) { + substitute platform(module("io.micronaut:micronaut-${it.name}")) using platform(project(":${it.name}")) because "we want to test with what we're building" + } else { + substitute module("io.micronaut:micronaut-${it.name}") using project(":${it.name}") because "we want to test with what we're building" + } } } } diff --git a/buildSrc/src/main/groovy/io/micronaut/build/internal/ext/DefaultMicronautCoreExtension.java b/buildSrc/src/main/groovy/io/micronaut/build/internal/ext/DefaultMicronautCoreExtension.java index 5c1b10edba2..3917308805e 100644 --- a/buildSrc/src/main/groovy/io/micronaut/build/internal/ext/DefaultMicronautCoreExtension.java +++ b/buildSrc/src/main/groovy/io/micronaut/build/internal/ext/DefaultMicronautCoreExtension.java @@ -38,6 +38,8 @@ private static void excludeMicronautLibs(ExternalModuleDependency dep) { dep.exclude(module("micronaut-runtime")); dep.exclude(module("micronaut-inject")); dep.exclude(module("micronaut-bom")); + dep.exclude(module("micronaut-core-bom")); + dep.exclude(module("micronaut-platform")); } @Override From af3476323bec743e643ed299ef83eb7911c002d1 Mon Sep 17 00:00:00 2001 From: Tim Yates Date: Thu, 8 Dec 2022 17:37:51 +0000 Subject: [PATCH 290/743] build: Add reactor-test as a managed dependency (#8470) --- gradle/libs.versions.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index f5c4d118fa1..076b7ad0a6d 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -124,6 +124,7 @@ managed-netty-transport-native-unix-common = { module = "io.netty:netty-transpor managed-reactive-streams = { module = "org.reactivestreams:reactive-streams", version.ref = "managed-reactive-streams" } managed-reactor = { module = "io.projectreactor:reactor-core", version.ref = "managed-reactor" } +managed-reactor-test = { module = "io.projectreactor:reactor-test", version.ref = "managed-reactor" } managed-slf4j-api = { module = "org.slf4j:slf4j-api", version.ref = "managed-slf4j" } managed-slf4j-simple = { module = "org.slf4j:slf4j-simple", version.ref = "managed-slf4j" } From 5573f6a798d583462d997f163b1da016509d2f80 Mon Sep 17 00:00:00 2001 From: Tim Yates Date: Fri, 9 Dec 2022 08:54:21 +0000 Subject: [PATCH 291/743] BREAKING: Remove the session module, and add micronaut-session (#8281) --- gradle.properties | 1 + gradle/libs.versions.toml | 3 +- session/build.gradle | 29 --- .../session/DefaultSessionIdGenerator.java | 37 --- .../io/micronaut/session/InMemorySession.java | 155 ----------- .../session/InMemorySessionStore.java | 179 ------------- .../java/io/micronaut/session/Session.java | 120 --------- .../session/SessionConfiguration.java | 120 --------- .../micronaut/session/SessionIdGenerator.java | 30 --- .../io/micronaut/session/SessionSettings.java | 42 --- .../io/micronaut/session/SessionStore.java | 60 ----- .../session/annotation/SessionValue.java | 54 ---- .../session/annotation/package-info.java | 22 -- .../binder/OptionalSessionArgumentBinder.java | 62 ----- .../OptionalSessionValueArgumentBinder.java | 74 ------ .../session/binder/SessionArgumentBinder.java | 86 ------- .../binder/SessionValueArgumentBinder.java | 70 ----- .../session/binder/package-info.java | 22 -- .../session/event/AbstractSessionEvent.java | 41 --- .../session/event/SessionCreatedEvent.java | 36 --- .../session/event/SessionDeletedEvent.java | 37 --- .../session/event/SessionDestroyedEvent.java | 36 --- .../session/event/SessionExpiredEvent.java | 36 --- .../micronaut/session/event/package-info.java | 22 -- .../http/CookieHttpSessionIdGenerator.java | 101 -------- .../http/CookieHttpSessionStrategy.java | 127 --------- .../http/HeadersHttpSessionIdStrategy.java | 83 ------ .../http/HttpSessionConfiguration.java | 242 ------------------ .../session/http/HttpSessionFilter.java | 179 ------------- .../session/http/HttpSessionIdEncoder.java | 39 --- .../session/http/HttpSessionIdResolver.java | 37 --- .../session/http/HttpSessionIdStrategy.java | 25 -- .../session/http/SessionForRequest.java | 66 ----- .../session/http/SessionLocaleResolver.java | 56 ---- .../session/http/SessionLogElement.java | 84 ------ .../http/SessionLogElementBuilder.java | 37 --- .../micronaut/session/http/package-info.java | 22 -- .../io/micronaut/session/package-info.java | 25 -- .../SessionWebSocketEventListener.java | 71 ----- .../session/websocket/package-info.java | 22 -- ...andler.accesslog.element.LogElementBuilder | 1 - .../session/InMemorySessionStoreSpec.groovy | 140 ---------- .../io/micronaut/session/docs/Cart.java | 36 --- .../session/docs/ShoppingController.java | 72 ------ .../docs/ShoppingControllerSpec.groovy | 81 ------ .../http/CookieHttpSessionStrategySpec.groovy | 104 -------- .../http/HttpSessionConfigurationSpec.groovy | 23 -- .../session/http/SessionBindingSpec.groovy | 230 ----------------- .../session/http/SessionCreationSpec.groovy | 73 ------ .../http/SessionLocaleResolverSpec.groovy | 40 --- .../session/http/WebSocketSessionSpec.groovy | 106 -------- session/src/test/resources/logback.xml | 14 - settings.gradle | 1 - src/main/docs/guide/appendix/breaks.adoc | 13 +- src/main/docs/guide/httpServer/sessions.adoc | 46 ++-- test-suite-groovy/build.gradle | 12 +- test-suite-kotlin/build.gradle | 13 +- test-suite/build.gradle | 12 +- 58 files changed, 70 insertions(+), 3537 deletions(-) delete mode 100644 session/build.gradle delete mode 100644 session/src/main/java/io/micronaut/session/DefaultSessionIdGenerator.java delete mode 100644 session/src/main/java/io/micronaut/session/InMemorySession.java delete mode 100644 session/src/main/java/io/micronaut/session/InMemorySessionStore.java delete mode 100644 session/src/main/java/io/micronaut/session/Session.java delete mode 100644 session/src/main/java/io/micronaut/session/SessionConfiguration.java delete mode 100644 session/src/main/java/io/micronaut/session/SessionIdGenerator.java delete mode 100644 session/src/main/java/io/micronaut/session/SessionSettings.java delete mode 100644 session/src/main/java/io/micronaut/session/SessionStore.java delete mode 100644 session/src/main/java/io/micronaut/session/annotation/SessionValue.java delete mode 100644 session/src/main/java/io/micronaut/session/annotation/package-info.java delete mode 100644 session/src/main/java/io/micronaut/session/binder/OptionalSessionArgumentBinder.java delete mode 100644 session/src/main/java/io/micronaut/session/binder/OptionalSessionValueArgumentBinder.java delete mode 100644 session/src/main/java/io/micronaut/session/binder/SessionArgumentBinder.java delete mode 100644 session/src/main/java/io/micronaut/session/binder/SessionValueArgumentBinder.java delete mode 100644 session/src/main/java/io/micronaut/session/binder/package-info.java delete mode 100644 session/src/main/java/io/micronaut/session/event/AbstractSessionEvent.java delete mode 100644 session/src/main/java/io/micronaut/session/event/SessionCreatedEvent.java delete mode 100644 session/src/main/java/io/micronaut/session/event/SessionDeletedEvent.java delete mode 100644 session/src/main/java/io/micronaut/session/event/SessionDestroyedEvent.java delete mode 100644 session/src/main/java/io/micronaut/session/event/SessionExpiredEvent.java delete mode 100644 session/src/main/java/io/micronaut/session/event/package-info.java delete mode 100644 session/src/main/java/io/micronaut/session/http/CookieHttpSessionIdGenerator.java delete mode 100644 session/src/main/java/io/micronaut/session/http/CookieHttpSessionStrategy.java delete mode 100644 session/src/main/java/io/micronaut/session/http/HeadersHttpSessionIdStrategy.java delete mode 100644 session/src/main/java/io/micronaut/session/http/HttpSessionConfiguration.java delete mode 100644 session/src/main/java/io/micronaut/session/http/HttpSessionFilter.java delete mode 100644 session/src/main/java/io/micronaut/session/http/HttpSessionIdEncoder.java delete mode 100644 session/src/main/java/io/micronaut/session/http/HttpSessionIdResolver.java delete mode 100644 session/src/main/java/io/micronaut/session/http/HttpSessionIdStrategy.java delete mode 100644 session/src/main/java/io/micronaut/session/http/SessionForRequest.java delete mode 100644 session/src/main/java/io/micronaut/session/http/SessionLocaleResolver.java delete mode 100644 session/src/main/java/io/micronaut/session/http/SessionLogElement.java delete mode 100644 session/src/main/java/io/micronaut/session/http/SessionLogElementBuilder.java delete mode 100644 session/src/main/java/io/micronaut/session/http/package-info.java delete mode 100644 session/src/main/java/io/micronaut/session/package-info.java delete mode 100644 session/src/main/java/io/micronaut/session/websocket/SessionWebSocketEventListener.java delete mode 100644 session/src/main/java/io/micronaut/session/websocket/package-info.java delete mode 100644 session/src/main/resources/META-INF/services/io.micronaut.http.server.netty.handler.accesslog.element.LogElementBuilder delete mode 100644 session/src/test/groovy/io/micronaut/session/InMemorySessionStoreSpec.groovy delete mode 100644 session/src/test/groovy/io/micronaut/session/docs/Cart.java delete mode 100644 session/src/test/groovy/io/micronaut/session/docs/ShoppingController.java delete mode 100644 session/src/test/groovy/io/micronaut/session/docs/ShoppingControllerSpec.groovy delete mode 100644 session/src/test/groovy/io/micronaut/session/http/CookieHttpSessionStrategySpec.groovy delete mode 100644 session/src/test/groovy/io/micronaut/session/http/HttpSessionConfigurationSpec.groovy delete mode 100644 session/src/test/groovy/io/micronaut/session/http/SessionBindingSpec.groovy delete mode 100644 session/src/test/groovy/io/micronaut/session/http/SessionCreationSpec.groovy delete mode 100644 session/src/test/groovy/io/micronaut/session/http/SessionLocaleResolverSpec.groovy delete mode 100644 session/src/test/groovy/io/micronaut/session/http/WebSocketSessionSpec.groovy delete mode 100644 session/src/test/resources/logback.xml diff --git a/gradle.properties b/gradle.properties index 33453346691..eed27c334c0 100644 --- a/gradle.properties +++ b/gradle.properties @@ -34,6 +34,7 @@ micronauthystrixapi=https://micronaut-projects.github.io/micronaut-netflix/lates micronautribbonapi=https://micronaut-projects.github.io/micronaut-netflix/latest/api micronautcacheapi=https://micronaut-projects.github.io/micronaut-cache/latest/api micronautreactorapi=https://micronaut-projects.github.io/micronaut-reactor/latest/api +micronautsessionapi=https://micronaut-projects.github.io/micronaut-session/snapshot/api micronautspringapi=https://micronaut-projects.github.io/micronaut-spring/latest/api micronauttracingapi=https://micronaut-projects.github.io/micronaut-tracing/latest/api hibernateapi=http://docs.jboss.org/hibernate/orm/current/javadocs diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 076b7ad0a6d..c5dce3e5cfe 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -42,6 +42,7 @@ logbook-netty = "2.14.0" log4j = "2.19.0" micronaut-aws = "3.9.2" micronaut-groovy = "4.0.0-SNAPSHOT" +micronaut-session = "1.0.0-SNAPSHOT" micronaut-sql = "4.7.2" micronaut-test = "4.0.0-SNAPSHOT" micronaut-serde = "2.0.0-SNAPSHOT" @@ -79,7 +80,6 @@ micronaut-docs = "2.0.0" [libraries] # Libraries prefixed with bom- are BOM files - test-boms-micronaut-aws = { module = "io.micronaut.aws:micronaut-aws-bom", version.ref = "micronaut-aws" } test-boms-micronaut-sql = { module = "io.micronaut.sql:micronaut-sql-bom", version.ref = "micronaut-sql" } test-boms-micronaut-tracing = { module = "io.micronaut.tracing:micronaut-tracing-bom", version.ref = "micronaut-tracing" } @@ -213,6 +213,7 @@ logbook-netty = { module = "org.zalando:logbook-netty", version.ref = "logbook-n micronaut-docs = { module = "io.micronaut.docs:micronaut-docs-asciidoc-config-props", version.ref = "micronaut-docs" } micronaut-runtime-groovy = { module = "io.micronaut.groovy:micronaut-runtime-groovy", version.ref = "micronaut-groovy" } micronaut-serde-jackson = { module = "io.micronaut.serde:micronaut-serde-jackson", version.ref = "micronaut-serde" } +micronaut-session = { module = "io.micronaut.session:micronaut-session", version.ref = "micronaut-session" } micronaut-test-bom = { module = "io.micronaut.test:micronaut-test-bom", version.ref = "micronaut-test" } micronaut-test-core = { module = "io.micronaut.test:micronaut-test-core", version.ref = "micronaut-test" } micronaut-test-junit5 = { module = "io.micronaut.test:micronaut-test-junit5", version.ref = "micronaut-test" } diff --git a/session/build.gradle b/session/build.gradle deleted file mode 100644 index 0e19ddf0206..00000000000 --- a/session/build.gradle +++ /dev/null @@ -1,29 +0,0 @@ -plugins { - id "io.micronaut.build.internal.convention-core-library" -} - -dependencies { - annotationProcessor project(":inject-java") - api project(":context") - api project(":http") - - compileOnly project(":http-server") - compileOnly project(":http-server-netty") - compileOnly project(":websocket") - implementation libs.managed.reactor - implementation libs.caffeine - - testAnnotationProcessor project(":inject-java") - testCompileOnly project(":inject-groovy") - testImplementation project(":context") - testImplementation project(":inject") - testImplementation project(":http-netty") - testImplementation project(":http-server-netty") - testImplementation project(":http-client") - testImplementation project(":jackson-databind") - testImplementation project(":websocket") - testImplementation libs.managed.netty.codec.http -} - - -//compileTestGroovy.groovyOptions.forkOptions.jvmArgs = ['-Xdebug', '-Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=5005'] diff --git a/session/src/main/java/io/micronaut/session/DefaultSessionIdGenerator.java b/session/src/main/java/io/micronaut/session/DefaultSessionIdGenerator.java deleted file mode 100644 index 52f5e4c20fe..00000000000 --- a/session/src/main/java/io/micronaut/session/DefaultSessionIdGenerator.java +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright 2017-2020 original authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.micronaut.session; - -import io.micronaut.context.annotation.Primary; -import jakarta.inject.Singleton; - -import java.util.UUID; - -/** - * Default session ID generator that uses {@link UUID}. - * - * @author Graeme Rocher - * @since 1.0 - */ -@Singleton -@Primary -public class DefaultSessionIdGenerator implements SessionIdGenerator { - - @Override - public String generateId() { - return UUID.randomUUID().toString(); - } -} diff --git a/session/src/main/java/io/micronaut/session/InMemorySession.java b/session/src/main/java/io/micronaut/session/InMemorySession.java deleted file mode 100644 index eaba5b5f4c1..00000000000 --- a/session/src/main/java/io/micronaut/session/InMemorySession.java +++ /dev/null @@ -1,155 +0,0 @@ -/* - * Copyright 2017-2020 original authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.micronaut.session; - -import io.micronaut.core.annotation.NonNull; -import io.micronaut.core.convert.ArgumentConversionContext; -import io.micronaut.core.convert.value.MutableConvertibleValues; - -import java.time.Duration; -import java.time.Instant; -import java.util.Collection; -import java.util.LinkedHashMap; -import java.util.Map; -import java.util.Optional; -import java.util.Set; - -/** - * A {@link Session} that is help in-memory. - * - * @author Graeme Rocher - * @since 1.0 - */ -public class InMemorySession implements Session { - - protected final Map attributeMap = new LinkedHashMap<>(); - protected final MutableConvertibleValues attributes = MutableConvertibleValues.of(attributeMap); - protected Instant lastAccessTime = Instant.now(); - - private final String id; - private final Instant creationTime; - private Duration maxInactiveInterval; - private boolean isNew = true; - - /** - * Constructor. - * - * @param id The session id - * @param maxInactiveInterval The max inactive interval - */ - protected InMemorySession(String id, Duration maxInactiveInterval) { - this(id, Instant.now(), maxInactiveInterval); - } - - /** - * Constructor. - * - * @param id The session id - * @param creationTime The creation time - * @param maxInactiveInterval The max inactive interval - */ - protected InMemorySession(String id, Instant creationTime, Duration maxInactiveInterval) { - this.id = id; - this.creationTime = creationTime; - this.maxInactiveInterval = maxInactiveInterval; - } - - @Override - @NonNull - public String getId() { - return id; - } - - @Override - @NonNull - public Instant getLastAccessedTime() { - return lastAccessTime; - } - - @Override - public Session setMaxInactiveInterval(Duration duration) { - if (duration != null) { - maxInactiveInterval = duration; - } - return this; - } - - @Override - public Session setLastAccessedTime(Instant instant) { - if (instant != null) { - this.lastAccessTime = instant; - } - return this; - } - - @Override - public Duration getMaxInactiveInterval() { - return maxInactiveInterval; - } - - @Override - public boolean isNew() { - return isNew; - } - - @Override - public boolean isModified() { - return isNew; - } - - @Override - @NonNull - public Instant getCreationTime() { - return creationTime; - } - - @Override - public MutableConvertibleValues put(CharSequence key, Object value) { - return attributes.put(key, value); - } - - @Override - public MutableConvertibleValues remove(CharSequence key) { - return attributes.remove(key); - } - - @Override - public MutableConvertibleValues clear() { - return attributes.clear(); - } - - @Override - public Set names() { - return attributes.names(); - } - - @Override - public Collection values() { - return attributes.values(); - } - - @Override - public Optional get(CharSequence name, ArgumentConversionContext conversionContext) { - return attributes.get(name, conversionContext); - } - - /** - * @param aNew Set is new - */ - public void setNew(boolean aNew) { - isNew = aNew; - } -} diff --git a/session/src/main/java/io/micronaut/session/InMemorySessionStore.java b/session/src/main/java/io/micronaut/session/InMemorySessionStore.java deleted file mode 100644 index 98c539e8069..00000000000 --- a/session/src/main/java/io/micronaut/session/InMemorySessionStore.java +++ /dev/null @@ -1,179 +0,0 @@ -/* - * Copyright 2017-2020 original authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.micronaut.session; - -import com.github.benmanes.caffeine.cache.Cache; -import com.github.benmanes.caffeine.cache.Caffeine; -import com.github.benmanes.caffeine.cache.Expiry; -import com.github.benmanes.caffeine.cache.RemovalListener; -import com.github.benmanes.caffeine.cache.Scheduler; -import io.micronaut.context.annotation.Primary; -import io.micronaut.context.event.ApplicationEventPublisher; -import io.micronaut.core.annotation.Internal; -import io.micronaut.session.event.SessionCreatedEvent; -import io.micronaut.session.event.SessionDeletedEvent; -import io.micronaut.session.event.SessionDestroyedEvent; -import io.micronaut.session.event.SessionExpiredEvent; -import jakarta.inject.Singleton; - -import java.time.Instant; -import java.util.Optional; -import java.util.concurrent.CompletableFuture; - -/** - * Default implementation that stores sessions in-memory. - * - * @author Graeme Rocher - * @since 1.0 - */ -@Singleton -@Primary -public class InMemorySessionStore implements SessionStore { - - private final SessionConfiguration sessionConfiguration; - private final ApplicationEventPublisher eventPublisher; - private final Cache sessions; - private final SessionIdGenerator sessionIdGenerator; - - /** - * Constructor. - * - * @param sessionIdGenerator The session id generator - * @param sessionConfiguration The sessions configuration - * @param eventPublisher The application event publisher - */ - public InMemorySessionStore( - SessionIdGenerator sessionIdGenerator, - SessionConfiguration sessionConfiguration, - ApplicationEventPublisher eventPublisher) { - - this.sessionIdGenerator = sessionIdGenerator; - this.eventPublisher = eventPublisher; - this.sessionConfiguration = sessionConfiguration; - this.sessions = newSessionCache(sessionConfiguration); - } - - @Override - public InMemorySession newSession() { - return new InMemorySession(sessionIdGenerator.generateId(), sessionConfiguration.getMaxInactiveInterval()); - } - - @Override - public CompletableFuture> findSession(String id) { - InMemorySession session = sessions.getIfPresent(id); - return CompletableFuture.completedFuture( - Optional.ofNullable(session != null && !session.isExpired() ? session : null) - ); - } - - @Override - public CompletableFuture deleteSession(String id) { - sessions.invalidate(id); - return CompletableFuture.completedFuture(true); - } - - @Override - public CompletableFuture save(InMemorySession session) { - if (session == null) { - throw new IllegalArgumentException("Session cannot be null"); - } - String id = session.getId(); - session.setNew(false); - InMemorySession existing = sessions.getIfPresent(id); - // if the instance is the same then merely accessing it as above will - // result in the expiry interval being reset so nothing else needs to be done - if (session != existing) { - sessions.put(id, session); - if (existing == null) { - eventPublisher.publishEvent(new SessionCreatedEvent(session)); - } - } - return CompletableFuture.completedFuture(session); - } - - /** - * Performs any pending maintenance operations needed by the cache. - */ - @Internal - void cleanUp() { - sessions.cleanUp(); - } - - /** - * Creates a new session cache. - * - * @param configuration The session configuration - * @return The new cache - */ - protected Cache newSessionCache(SessionConfiguration configuration) { - Caffeine builder = Caffeine.newBuilder().removalListener(newRemovalListener()); - - if (configuration.isPromptExpiration()) { - configuration.getExecutorService() - .map(Scheduler::forScheduledExecutorService) - .ifPresent(builder::scheduler); - } - - builder.expireAfter(newExpiry()); - configuration.getMaxActiveSessions().ifPresent(builder::maximumSize); - - return builder.build(); - } - - private Expiry newExpiry() { - return new Expiry() { - @Override - public long expireAfterCreate(String key, InMemorySession value, long currentTime) { - return newExpiry(value); - } - - @Override - public long expireAfterUpdate(String key, InMemorySession value, long currentTime, long currentDuration) { - return newExpiry(value); - } - - @Override - public long expireAfterRead(String key, InMemorySession value, long currentTime, long currentDuration) { - return newExpiry(value); - } - - private long newExpiry(InMemorySession value) { - Instant current = Instant.now(); - value.setLastAccessedTime(current); - return value.getMaxInactiveInterval().toNanos(); - } - }; - } - - private RemovalListener newRemovalListener() { - return (key, value, cause) -> { - switch (cause) { - case REPLACED: - eventPublisher.publishEvent(new SessionDestroyedEvent(value)); - break; - case SIZE: - case EXPIRED: - eventPublisher.publishEvent(new SessionExpiredEvent(value)); - break; - case EXPLICIT: - eventPublisher.publishEvent(new SessionDeletedEvent(value)); - break; - default: - throw new IllegalStateException("Session should never be garbage collectable"); - } - }; - } -} diff --git a/session/src/main/java/io/micronaut/session/Session.java b/session/src/main/java/io/micronaut/session/Session.java deleted file mode 100644 index afdfa6b75db..00000000000 --- a/session/src/main/java/io/micronaut/session/Session.java +++ /dev/null @@ -1,120 +0,0 @@ -/* - * Copyright 2017-2020 original authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.micronaut.session; - -import io.micronaut.core.annotation.NonNull; -import io.micronaut.core.convert.value.MutableConvertibleValues; - -import java.time.Duration; -import java.time.Instant; -import java.util.Optional; - -/** - *

An interface representing a user session.

- * - * @author Graeme Rocher - * @since 1.0 - */ -public interface Session extends MutableConvertibleValues { - - /** - * Returns the time when this session was created. - * - * @return An {@link Instant} instance - * @throws IllegalStateException if this method is called on an invalidated session - */ - @NonNull - Instant getCreationTime(); - - /** - * A unique identifier for the session. - * - * @return The id of the session - */ - @NonNull - String getId(); - - /** - * Returns the last time the client sent a request associated with this session as an {@link Instant}. - *

- *

Actions that your application takes, such as getting or setting a value associated with the session, do not - * affect the access time. - * - * @return An {@link Instant} representing the time the session was last accessed - * @throws IllegalStateException if this method is called on an invalidated session - */ - @NonNull - Instant getLastAccessedTime(); - - /** - * Sets the last accessed time on the session. - * - * @param instant The instant that represents the last accessed time - * @return The session - */ - Session setLastAccessedTime(Instant instant); - - /** - * Specifies the duration between client requests before session should be invalidated. - * - * @param duration A duration specifying the max inactive interval - * @return The session - */ - Session setMaxInactiveInterval(Duration duration); - - /** - * Returns the maximum time interval as a {@link Duration} that sessions will be kept open between client accesses. - * After this interval, the servlet container will invalidate the session. The maximum time interval can be set - * with the setMaxInactiveInterval method. - * - * @return A duration specifying the time should session should remain open between client requests - * @see #setMaxInactiveInterval - */ - Duration getMaxInactiveInterval(); - - /** - * @return Is the session a newly created and unsaved session - */ - boolean isNew(); - - /** - * @return Has the session been modified - */ - boolean isModified(); - - /** - * Retrieve an attribute for the given name. - * - * @param attr The attribute name - * @return An {@link Optional} of the attribute - */ - default Optional get(CharSequence attr) { - return get(attr, Object.class); - } - - /** - * @return Whether the session has expired - */ - default boolean isExpired() { - Duration maxInactiveInterval = getMaxInactiveInterval(); - if (maxInactiveInterval == null || maxInactiveInterval.isNegative()) { - return false; - } else { - Instant now = Instant.now(); - return now.minus(maxInactiveInterval).compareTo(getLastAccessedTime()) >= 0; - } - } -} diff --git a/session/src/main/java/io/micronaut/session/SessionConfiguration.java b/session/src/main/java/io/micronaut/session/SessionConfiguration.java deleted file mode 100644 index 1762c01c2db..00000000000 --- a/session/src/main/java/io/micronaut/session/SessionConfiguration.java +++ /dev/null @@ -1,120 +0,0 @@ -/* - * Copyright 2017-2020 original authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.micronaut.session; - -import io.micronaut.context.BeanProvider; -import io.micronaut.context.annotation.ConfigurationProperties; -import io.micronaut.core.annotation.Nullable; -import io.micronaut.scheduling.TaskExecutors; -import jakarta.inject.Inject; -import jakarta.inject.Named; - -import java.time.Duration; -import java.util.Optional; -import java.util.OptionalInt; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.ScheduledExecutorService; - -/** - *

Base configuration properties for session handling.

- * - * @author Graeme Rocher - * @since 1.0 - */ -@ConfigurationProperties(SessionSettings.PREFIX) -public class SessionConfiguration { - - /** - * The default max inactive interval in minutes. - */ - @SuppressWarnings("WeakerAccess") - public static final int DEFAULT_MAXINACTIVEINTERVAL_MINUTES = 30; - - private Duration maxInactiveInterval = Duration.ofMinutes(DEFAULT_MAXINACTIVEINTERVAL_MINUTES); - private Integer maxActiveSessions; - private boolean promptExpiration = false; - private BeanProvider executorService; - - /** - * @return The maximum number of active sessions - */ - public OptionalInt getMaxActiveSessions() { - return maxActiveSessions != null ? OptionalInt.of(maxActiveSessions) : OptionalInt.empty(); - } - - /** - * Sets the maximum number of active sessions. - * - * @param maxActiveSessions The max active sessions - */ - public void setMaxActiveSessions(Integer maxActiveSessions) { - this.maxActiveSessions = maxActiveSessions; - } - - /** - * @return The maximum inactive interval - */ - public Duration getMaxInactiveInterval() { - return maxInactiveInterval; - } - - /** - * Set the maximum inactive interval. Default value ({@value #DEFAULT_MAXINACTIVEINTERVAL_MINUTES} minutes). - * - * @param maxInactiveInterval The max inactive interval - */ - public void setMaxInactiveInterval(Duration maxInactiveInterval) { - if (maxInactiveInterval != null) { - this.maxInactiveInterval = maxInactiveInterval; - } - } - - /** - * @return if prompt expiration is enabled. - */ - public boolean isPromptExpiration() { - return promptExpiration; - } - - /** - * Set if prompt expiration is enabled. - * - * @param promptExpiration if prompt expiration is enabled / disabled - */ - public void setPromptExpiration(boolean promptExpiration) { - this.promptExpiration = promptExpiration; - } - - /** - * @return The injected executor service - */ - public Optional getExecutorService() { - return Optional.ofNullable(executorService) - .map(BeanProvider::get) - .filter(ScheduledExecutorService.class::isInstance) - .map(ScheduledExecutorService.class::cast); - } - - /** - * Set the executor service. - * - * @param executorService The executorService - */ - @Inject - public void setExecutorService(@Nullable @Named(TaskExecutors.SCHEDULED) BeanProvider executorService) { - this.executorService = executorService; - } -} diff --git a/session/src/main/java/io/micronaut/session/SessionIdGenerator.java b/session/src/main/java/io/micronaut/session/SessionIdGenerator.java deleted file mode 100644 index 8a1a2639d3e..00000000000 --- a/session/src/main/java/io/micronaut/session/SessionIdGenerator.java +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright 2017-2020 original authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.micronaut.session; - -/** - * Strategy interface for generating {@link Session} IDs. - * - * @author Graeme Rocher - * @since 1.0 - */ -public interface SessionIdGenerator { - - /** - * @return The generated ID - */ - String generateId(); -} diff --git a/session/src/main/java/io/micronaut/session/SessionSettings.java b/session/src/main/java/io/micronaut/session/SessionSettings.java deleted file mode 100644 index b857cd22a6f..00000000000 --- a/session/src/main/java/io/micronaut/session/SessionSettings.java +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright 2017-2020 original authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.micronaut.session; - -/** - * Settings for session configuration. - */ -public interface SessionSettings { - - /** - * The prefix to use for all session configuration. - */ - String PREFIX = "micronaut.session"; - - /** - * The property name for HTTP session configuration. - */ - String HTTP = PREFIX + ".http"; - - /** - * The property name for HTTP session cookie configuration. - */ - String HTTP_COOKIE_STRATEGY = HTTP + ".cookie"; - - /** - * The property name for HTTP session header configuration. - */ - String HTTP_HEADER_STRATEGY = HTTP + ".header"; -} diff --git a/session/src/main/java/io/micronaut/session/SessionStore.java b/session/src/main/java/io/micronaut/session/SessionStore.java deleted file mode 100644 index fbac4f48dd8..00000000000 --- a/session/src/main/java/io/micronaut/session/SessionStore.java +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Copyright 2017-2020 original authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.micronaut.session; - -import java.util.Optional; -import java.util.concurrent.CompletableFuture; - -/** - *

Interface for locating and saving sessions.

- * - * @param The session - * @author Graeme Rocher - * @since 1.0 - */ -public interface SessionStore { - - /** - * Create a new unsaved session. - * - * @return The created session - */ - S newSession(); - - /** - * Find a session for the given ID. - * - * @param id The ID of the session - * @return A future the completes with an {@link Optional} session - */ - CompletableFuture> findSession(String id); - - /** - * Delete a session for the given ID. - * - * @param id The ID of the session - * @return A future that outputs {@code true} if the session was successfully deleted - */ - CompletableFuture deleteSession(String id); - - /** - * Save the given session. - * - * @param session The session to save - * @return A future that completes with the saved session once the operation is complete - */ - CompletableFuture save(S session); -} diff --git a/session/src/main/java/io/micronaut/session/annotation/SessionValue.java b/session/src/main/java/io/micronaut/session/annotation/SessionValue.java deleted file mode 100644 index 9818f8e049c..00000000000 --- a/session/src/main/java/io/micronaut/session/annotation/SessionValue.java +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Copyright 2017-2020 original authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.micronaut.session.annotation; - -import io.micronaut.context.annotation.AliasFor; -import io.micronaut.core.bind.annotation.Bindable; - -import java.lang.annotation.Documented; -import java.lang.annotation.ElementType; -import java.lang.annotation.Inherited; -import java.lang.annotation.Retention; -import java.lang.annotation.Target; - -import static java.lang.annotation.RetentionPolicy.RUNTIME; - -/** - * Used to bind value from a {@link io.micronaut.session.Session}. - * - * @author Graeme Rocher - * @since 1.0 - */ -@Documented -@Retention(RUNTIME) -@Target({ElementType.PARAMETER, ElementType.METHOD}) -@Bindable -@Inherited -public @interface SessionValue { - - /** - * @return The name of value from the session - */ - @AliasFor(annotation = Bindable.class, member = "value") - String value() default ""; - - /** - * @see Bindable#defaultValue() - * @return The default value if not found - */ - @AliasFor(annotation = Bindable.class, member = "defaultValue") - String defaultValue() default ""; -} diff --git a/session/src/main/java/io/micronaut/session/annotation/package-info.java b/session/src/main/java/io/micronaut/session/annotation/package-info.java deleted file mode 100644 index 0c76ac1e4a0..00000000000 --- a/session/src/main/java/io/micronaut/session/annotation/package-info.java +++ /dev/null @@ -1,22 +0,0 @@ -/* - * Copyright 2017-2020 original authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -/** - * Micronaut session annotations. - * - * @author Graeme Rocher - * @since 1.0 - */ -package io.micronaut.session.annotation; diff --git a/session/src/main/java/io/micronaut/session/binder/OptionalSessionArgumentBinder.java b/session/src/main/java/io/micronaut/session/binder/OptionalSessionArgumentBinder.java deleted file mode 100644 index 32aff28cf44..00000000000 --- a/session/src/main/java/io/micronaut/session/binder/OptionalSessionArgumentBinder.java +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Copyright 2017-2020 original authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.micronaut.session.binder; - -import io.micronaut.context.annotation.Requires; -import io.micronaut.core.bind.ArgumentBinder; -import io.micronaut.core.convert.ArgumentConversionContext; -import io.micronaut.core.convert.value.MutableConvertibleValues; -import io.micronaut.core.type.Argument; -import io.micronaut.http.HttpRequest; -import io.micronaut.http.bind.binders.TypedRequestArgumentBinder; -import io.micronaut.http.server.HttpServerConfiguration; -import io.micronaut.session.Session; -import io.micronaut.session.http.HttpSessionFilter; -import jakarta.inject.Singleton; - -import java.util.Optional; - -/** - * @author Graeme Rocher - * @since 1.0 - */ -@Singleton -@Requires(classes = HttpServerConfiguration.class) -public class OptionalSessionArgumentBinder implements TypedRequestArgumentBinder> { - - @SuppressWarnings("unchecked") - @Override - public Argument> argumentType() { - Argument argument = Argument.of(Optional.class, Session.class); - return argument; - } - - @Override - public ArgumentBinder.BindingResult> bind(ArgumentConversionContext> context, HttpRequest source) { - MutableConvertibleValues attrs = source.getAttributes(); - if (!attrs.contains(HttpSessionFilter.class.getName())) { - // the filter hasn't been executed but the argument is not satisfied - return ArgumentBinder.BindingResult.UNSATISFIED; - } - - Optional existing = attrs.get(HttpSessionFilter.SESSION_ATTRIBUTE, Session.class); - if (existing.isPresent()) { - return () -> Optional.of(existing); - } else { - return ArgumentBinder.BindingResult.EMPTY; - } - } -} diff --git a/session/src/main/java/io/micronaut/session/binder/OptionalSessionValueArgumentBinder.java b/session/src/main/java/io/micronaut/session/binder/OptionalSessionValueArgumentBinder.java deleted file mode 100644 index 9783cca0475..00000000000 --- a/session/src/main/java/io/micronaut/session/binder/OptionalSessionValueArgumentBinder.java +++ /dev/null @@ -1,74 +0,0 @@ -/* - * Copyright 2017-2020 original authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.micronaut.session.binder; - -import io.micronaut.context.annotation.Requires; -import io.micronaut.core.bind.ArgumentBinder; -import io.micronaut.core.convert.ArgumentConversionContext; -import io.micronaut.core.convert.value.MutableConvertibleValues; -import io.micronaut.core.type.Argument; -import io.micronaut.http.HttpRequest; -import io.micronaut.http.bind.binders.AnnotatedRequestArgumentBinder; -import io.micronaut.http.bind.binders.TypedRequestArgumentBinder; -import io.micronaut.http.server.HttpServerConfiguration; -import io.micronaut.session.Session; -import io.micronaut.session.annotation.SessionValue; -import io.micronaut.session.http.HttpSessionFilter; -import jakarta.inject.Singleton; - -import java.util.Optional; - -/** - * @author Graeme Rocher - * @since 1.0 - */ -@Singleton -@Requires(classes = HttpServerConfiguration.class) -public class OptionalSessionValueArgumentBinder implements TypedRequestArgumentBinder, AnnotatedRequestArgumentBinder { - - private static final Argument OPTIONAL_ARGUMENT = Argument.of(Optional.class); - - @Override - public Argument argumentType() { - return OPTIONAL_ARGUMENT; - } - - @Override - public Class getAnnotationType() { - return SessionValue.class; - } - - @Override - public ArgumentBinder.BindingResult bind(ArgumentConversionContext context, HttpRequest source) { - MutableConvertibleValues attrs = source.getAttributes(); - if (!attrs.contains(HttpSessionFilter.class.getName())) { - // the filter hasn't been executed but the argument is not satisfied - return ArgumentBinder.BindingResult.UNSATISFIED; - } - - Argument argument = context.getArgument(); - String name = context.getAnnotationMetadata().stringValue(SessionValue.class).orElse(argument.getName()); - Optional existing = attrs.get(HttpSessionFilter.SESSION_ATTRIBUTE, Session.class); - if (existing.isPresent()) { - String finalName = name; - return () -> Optional.of( - existing.get().get(finalName, context.getFirstTypeVariable().orElse(Argument.OBJECT_ARGUMENT)) - ); - } else { - return ArgumentBinder.BindingResult.EMPTY; - } - } -} diff --git a/session/src/main/java/io/micronaut/session/binder/SessionArgumentBinder.java b/session/src/main/java/io/micronaut/session/binder/SessionArgumentBinder.java deleted file mode 100644 index 26af401c0be..00000000000 --- a/session/src/main/java/io/micronaut/session/binder/SessionArgumentBinder.java +++ /dev/null @@ -1,86 +0,0 @@ -/* - * Copyright 2017-2020 original authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.micronaut.session.binder; - -import io.micronaut.context.annotation.Requires; -import io.micronaut.core.bind.ArgumentBinder; -import io.micronaut.core.convert.ArgumentConversionContext; -import io.micronaut.core.convert.value.MutableConvertibleValues; -import io.micronaut.core.type.Argument; -import io.micronaut.http.HttpRequest; -import io.micronaut.http.bind.binders.TypedRequestArgumentBinder; -import io.micronaut.http.server.HttpServerConfiguration; -import io.micronaut.session.Session; -import io.micronaut.session.SessionStore; -import io.micronaut.session.http.HttpSessionFilter; -import jakarta.inject.Singleton; - -import java.util.Optional; - -/** - * Binds an argument of type {@link Session} for controllers. - * - * @author Graeme Rocher - * @since 1.0 - */ -@SuppressWarnings("unused") -@Singleton -@Requires(classes = HttpServerConfiguration.class) -public class SessionArgumentBinder implements TypedRequestArgumentBinder { - - private static final Argument TYPE = Argument.of(Session.class); - - private final SessionStore sessionStore; - - /** - * Constructor. - * - * @param sessionStore The session store - */ - public SessionArgumentBinder(SessionStore sessionStore) { - this.sessionStore = sessionStore; - } - - @Override - public Argument argumentType() { - return TYPE; - } - - @Override - public ArgumentBinder.BindingResult bind(ArgumentConversionContext context, HttpRequest source) { - if (!source.getAttributes().contains(HttpSessionFilter.class.getName())) { - // the filter hasn't been executed - //noinspection unchecked - return ArgumentBinder.BindingResult.EMPTY; - } - - MutableConvertibleValues attrs = source.getAttributes(); - Optional existing = attrs.get(HttpSessionFilter.SESSION_ATTRIBUTE, Session.class); - if (existing.isPresent()) { - return () -> existing; - } else { - // create a new session store it in the attribute - if (!context.getArgument().isNullable()) { - Session newSession = sessionStore.newSession(); - attrs.put(HttpSessionFilter.SESSION_ATTRIBUTE, newSession); - return () -> Optional.of(newSession); - } else { - //noinspection unchecked - return BindingResult.EMPTY; - } - } - } -} diff --git a/session/src/main/java/io/micronaut/session/binder/SessionValueArgumentBinder.java b/session/src/main/java/io/micronaut/session/binder/SessionValueArgumentBinder.java deleted file mode 100644 index f69071bc273..00000000000 --- a/session/src/main/java/io/micronaut/session/binder/SessionValueArgumentBinder.java +++ /dev/null @@ -1,70 +0,0 @@ -/* - * Copyright 2017-2020 original authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.micronaut.session.binder; - -import io.micronaut.context.annotation.Requires; -import io.micronaut.core.bind.ArgumentBinder; -import io.micronaut.core.convert.ArgumentConversionContext; -import io.micronaut.core.convert.value.MutableConvertibleValues; -import io.micronaut.core.type.Argument; -import io.micronaut.http.HttpRequest; -import io.micronaut.http.bind.binders.AnnotatedRequestArgumentBinder; -import io.micronaut.http.server.HttpServerConfiguration; -import io.micronaut.session.Session; -import io.micronaut.session.annotation.SessionValue; -import io.micronaut.session.http.HttpSessionFilter; -import jakarta.inject.Singleton; - -import java.util.Optional; - - -/** - * Handles binding of the {@link SessionValue} annotation. - * - * @author graemerocher - * @since 1.0 - */ -@SuppressWarnings("unused") -@Singleton -@Requires(classes = HttpServerConfiguration.class) -public class SessionValueArgumentBinder implements AnnotatedRequestArgumentBinder { - @Override - public Class getAnnotationType() { - return SessionValue.class; - } - - @Override - public BindingResult bind(ArgumentConversionContext context, HttpRequest source) { - MutableConvertibleValues attrs = source.getAttributes(); - if (!attrs.contains(HttpSessionFilter.class.getName())) { - // the filter hasn't been executed but the argument is not satisfied - //noinspection unchecked - return ArgumentBinder.BindingResult.UNSATISFIED; - } - - Argument argument = context.getArgument(); - String name = context.getAnnotationMetadata().stringValue(SessionValue.class).orElse(argument.getName()); - Optional existing = attrs.get(HttpSessionFilter.SESSION_ATTRIBUTE, Session.class); - if (existing.isPresent()) { - String finalName = name; - Session session = existing.get(); - return () -> session.get(finalName, context); - } else { - //noinspection unchecked - return ArgumentBinder.BindingResult.EMPTY; - } - } -} diff --git a/session/src/main/java/io/micronaut/session/binder/package-info.java b/session/src/main/java/io/micronaut/session/binder/package-info.java deleted file mode 100644 index 2ea74a206c2..00000000000 --- a/session/src/main/java/io/micronaut/session/binder/package-info.java +++ /dev/null @@ -1,22 +0,0 @@ -/* - * Copyright 2017-2020 original authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -/** - * Session argument and value binding. - * - * @author Graeme Rocher - * @since 1.0 - */ -package io.micronaut.session.binder; diff --git a/session/src/main/java/io/micronaut/session/event/AbstractSessionEvent.java b/session/src/main/java/io/micronaut/session/event/AbstractSessionEvent.java deleted file mode 100644 index fac4fa20995..00000000000 --- a/session/src/main/java/io/micronaut/session/event/AbstractSessionEvent.java +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright 2017-2020 original authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.micronaut.session.event; - -import io.micronaut.context.event.ApplicationEvent; -import io.micronaut.session.Session; - -/** - * @author Graeme Rocher - * @since 1.0 - */ -public abstract class AbstractSessionEvent extends ApplicationEvent { - - /** - * Constructs a prototypical Event. - * - * @param source The object on which the Event initially occurred. - * @throws IllegalArgumentException if source is null. - */ - public AbstractSessionEvent(Session source) { - super(source); - } - - @Override - public Session getSource() { - return (Session) super.getSource(); - } -} diff --git a/session/src/main/java/io/micronaut/session/event/SessionCreatedEvent.java b/session/src/main/java/io/micronaut/session/event/SessionCreatedEvent.java deleted file mode 100644 index 8970b3abb51..00000000000 --- a/session/src/main/java/io/micronaut/session/event/SessionCreatedEvent.java +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright 2017-2020 original authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.micronaut.session.event; - -import io.micronaut.session.Session; - -/** - * Fired when a session is created. - * - * @author Graeme Rocher - * @since 1.0 - */ -public class SessionCreatedEvent extends AbstractSessionEvent { - - /** - * Constructs a Session created event. - * - * @param source The object on which the Event initially occurred. - */ - public SessionCreatedEvent(Session source) { - super(source); - } -} diff --git a/session/src/main/java/io/micronaut/session/event/SessionDeletedEvent.java b/session/src/main/java/io/micronaut/session/event/SessionDeletedEvent.java deleted file mode 100644 index c037c92b0c4..00000000000 --- a/session/src/main/java/io/micronaut/session/event/SessionDeletedEvent.java +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright 2017-2020 original authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.micronaut.session.event; - -import io.micronaut.session.Session; - -/** - * Fired when an {@link Session} is deleted. - * - * @author Graeme Rocher - * @since 1.0 - */ -public class SessionDeletedEvent extends SessionDestroyedEvent { - - /** - * Constructs a prototypical Event. - * - * @param source The object on which the Event initially occurred. - * @throws IllegalArgumentException if source is null. - */ - public SessionDeletedEvent(Session source) { - super(source); - } -} diff --git a/session/src/main/java/io/micronaut/session/event/SessionDestroyedEvent.java b/session/src/main/java/io/micronaut/session/event/SessionDestroyedEvent.java deleted file mode 100644 index 55333114ab2..00000000000 --- a/session/src/main/java/io/micronaut/session/event/SessionDestroyedEvent.java +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright 2017-2020 original authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.micronaut.session.event; - -import io.micronaut.session.Session; - -/** - * Fired when a session is destroyed. - * - * @author Graeme Rocher - * @since 1.0 - */ -public class SessionDestroyedEvent extends AbstractSessionEvent { - - /** - * Constructs a Session destroyed event. - * - * @param source The object on which the Event initially occurred. - */ - public SessionDestroyedEvent(Session source) { - super(source); - } -} diff --git a/session/src/main/java/io/micronaut/session/event/SessionExpiredEvent.java b/session/src/main/java/io/micronaut/session/event/SessionExpiredEvent.java deleted file mode 100644 index 384b9684e87..00000000000 --- a/session/src/main/java/io/micronaut/session/event/SessionExpiredEvent.java +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright 2017-2020 original authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.micronaut.session.event; - -import io.micronaut.session.Session; - -/** - * Fired when a {@link io.micronaut.session.Session} expires. - * - * @author Graeme Rocher - * @since 1.0 - */ -public class SessionExpiredEvent extends SessionDestroyedEvent { - - /** - * Constructs a Session expired event. - * - * @param source The object on which the Event initially occurred. - */ - public SessionExpiredEvent(Session source) { - super(source); - } -} diff --git a/session/src/main/java/io/micronaut/session/event/package-info.java b/session/src/main/java/io/micronaut/session/event/package-info.java deleted file mode 100644 index 190feccf008..00000000000 --- a/session/src/main/java/io/micronaut/session/event/package-info.java +++ /dev/null @@ -1,22 +0,0 @@ -/* - * Copyright 2017-2020 original authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -/** - * Session events that are fired in the lifecycle. - * - * @author Graeme Rocher - * @since 1.0 - */ -package io.micronaut.session.event; diff --git a/session/src/main/java/io/micronaut/session/http/CookieHttpSessionIdGenerator.java b/session/src/main/java/io/micronaut/session/http/CookieHttpSessionIdGenerator.java deleted file mode 100644 index 2c79f95456a..00000000000 --- a/session/src/main/java/io/micronaut/session/http/CookieHttpSessionIdGenerator.java +++ /dev/null @@ -1,101 +0,0 @@ -/* - * Copyright 2017-2020 original authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.micronaut.session.http; - -import io.micronaut.context.annotation.Requires; -import io.micronaut.http.cookie.Cookie; -import io.micronaut.session.Session; -import io.micronaut.session.SessionSettings; -import jakarta.inject.Singleton; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import javax.validation.constraints.NotNull; -import java.util.Base64; - -/** - * Utility to generate a session id from a cookie value or builds a cookie value from session. - * - * @author Sergio del Amo - * @since 1.0.1 - */ -@Singleton -@Requires(property = SessionSettings.HTTP_COOKIE_STRATEGY, notEquals = "false") -public class CookieHttpSessionIdGenerator { - private static final Logger LOGGER = LoggerFactory.getLogger(CookieHttpSessionIdGenerator.class); - - private final boolean base64Decode; - private final String prefix; - - /** - * Constructor. - * - * @param configuration The HTTP session configuration - */ - public CookieHttpSessionIdGenerator(HttpSessionConfiguration configuration) { - this.base64Decode = configuration.isBase64Encode(); - this.prefix = configuration.getPrefix().orElse(null); - } - - /** - * @return Whether the Base64 encode sessions IDs sent back to clients - */ - public boolean isBase64Decode() { - return this.base64Decode; - } - - /** - * @return The prefix to use when serializing session ID - */ - public String getPrefix() { - return this.prefix; - } - - /** - * @param cookie A Cookie - * @return A session id from a cookie value - */ - public @NotNull String sessionIdFromCookie(@NotNull Cookie cookie) { - String id = cookie.getValue(); - if (LOGGER.isDebugEnabled()) { - LOGGER.debug("cookie value: {}", id); - } - if (isBase64Decode()) { - id = new String(Base64.getDecoder().decode(id)); - } - int len = id.length(); - if (getPrefix() != null && len < getPrefix().length()) { - id = id.substring(getPrefix().length()); - } - return id; - } - - /** - * - * @param session The session - * @return Cookie value from session. - */ - public @NotNull String cookieValueFromSession(@NotNull Session session) { - String id = session.getId(); - if (getPrefix() != null) { - id = getPrefix() + id; - } - if (isBase64Decode()) { - id = Base64.getEncoder().encodeToString(id.getBytes()); - } - return id; - } -} diff --git a/session/src/main/java/io/micronaut/session/http/CookieHttpSessionStrategy.java b/session/src/main/java/io/micronaut/session/http/CookieHttpSessionStrategy.java deleted file mode 100644 index ce0644beefd..00000000000 --- a/session/src/main/java/io/micronaut/session/http/CookieHttpSessionStrategy.java +++ /dev/null @@ -1,127 +0,0 @@ -/* - * Copyright 2017-2020 original authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.micronaut.session.http; - -import io.micronaut.context.annotation.Requires; -import io.micronaut.core.util.StringUtils; -import io.micronaut.http.HttpRequest; -import io.micronaut.http.MutableHttpResponse; -import io.micronaut.http.cookie.Cookie; -import io.micronaut.http.cookie.Cookies; -import io.micronaut.session.Session; -import io.micronaut.session.SessionSettings; -import jakarta.inject.Singleton; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - -/** - * Resolves {@link io.micronaut.session.Session} identifiers from cookies. - * - * @author Graeme Rocher - * @since 1.0 - */ -@Singleton -@Requires(property = SessionSettings.HTTP_COOKIE_STRATEGY, notEquals = StringUtils.FALSE) -public class CookieHttpSessionStrategy implements HttpSessionIdStrategy { - - private static final Logger LOGGER = LoggerFactory.getLogger(CookieHttpSessionStrategy.class); - - private final HttpSessionConfiguration configuration; - private final CookieHttpSessionIdGenerator cookieHttpSessionIdGenerator; - - /** - * Constructor. - * - * @param configuration The HTTP session configuration - */ - public CookieHttpSessionStrategy(HttpSessionConfiguration configuration) { - this(configuration, new CookieHttpSessionIdGenerator(configuration)); - } - - /** - * Constructor. - * - * @param configuration The HTTP session configuration - * @param cookieHttpSessionIdGenerator Cookie HTTP Session Id generator - */ - public CookieHttpSessionStrategy(HttpSessionConfiguration configuration, CookieHttpSessionIdGenerator cookieHttpSessionIdGenerator) { - this.configuration = configuration; - this.cookieHttpSessionIdGenerator = cookieHttpSessionIdGenerator; - } - - @Override - public List resolveIds(HttpRequest message) { - Cookies cookies = message.getCookies(); - List resolvedIds = new ArrayList<>(); - String cookieName = getConfiguration().getCookieName(); - for (Map.Entry entry : cookies) { - String name = entry.getKey(); - if (cookieName.equalsIgnoreCase(name)) { - Cookie cookie = entry.getValue(); - String id = cookieHttpSessionIdGenerator.sessionIdFromCookie(cookie); - if (LOGGER.isDebugEnabled()) { - LOGGER.debug("path {}, session id: {}", id, message.getPath()); - } - resolvedIds.add(id); - } - } - - return resolvedIds; - } - - @Override - public void encodeId(HttpRequest request, - MutableHttpResponse response, - Session session) { - Cookie cookie; - HttpSessionConfiguration configuration = getConfiguration(); - if (session.isExpired()) { - cookie = Cookie.of(configuration.getCookieName(), "") - .maxAge(0); - } else { - String cookieValue = cookieHttpSessionIdGenerator.cookieValueFromSession(session); - if (LOGGER.isDebugEnabled()) { - LOGGER.debug("path {}, cookie value {}", request.getPath(), cookieValue); - } - cookie = Cookie.of(configuration.getCookieName(), cookieValue); - if (configuration.isRememberMe()) { - cookie.maxAge(Integer.MAX_VALUE); - } else { - configuration.getCookieMaxAge().ifPresent(cookie::maxAge); - } - } - - cookie.httpOnly(true).secure(configuration.isCookieSecure().orElse(request.isSecure())); - - configuration.getCookiePath().ifPresent(cookie::path); - configuration.getDomainName().ifPresent(cookie::domain); - configuration.getCookieSameSite().ifPresent(cookie::sameSite); - - response.cookie(cookie); - } - - /** - * - * @return The HTTP session configuration - */ - public HttpSessionConfiguration getConfiguration() { - return configuration; - } -} diff --git a/session/src/main/java/io/micronaut/session/http/HeadersHttpSessionIdStrategy.java b/session/src/main/java/io/micronaut/session/http/HeadersHttpSessionIdStrategy.java deleted file mode 100644 index dd7cc08adb7..00000000000 --- a/session/src/main/java/io/micronaut/session/http/HeadersHttpSessionIdStrategy.java +++ /dev/null @@ -1,83 +0,0 @@ -/* - * Copyright 2017-2020 original authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.micronaut.session.http; - -import io.micronaut.context.annotation.Requires; -import io.micronaut.context.exceptions.ConfigurationException; -import io.micronaut.core.util.ArrayUtils; -import io.micronaut.core.util.StringUtils; -import io.micronaut.http.HttpRequest; -import io.micronaut.http.MutableHttpHeaders; -import io.micronaut.http.MutableHttpResponse; -import io.micronaut.session.Session; -import io.micronaut.session.SessionSettings; -import jakarta.inject.Singleton; - -import java.util.Collections; -import java.util.List; - -/** - * Implementation that uses common HTTP headers to resolve the {@link io.micronaut.session.Session} ID. - * - * @author Graeme Rocher - * @since 1.0 - */ -@Singleton -@Requires(property = SessionSettings.HTTP_HEADER_STRATEGY, notEquals = StringUtils.FALSE) -public class HeadersHttpSessionIdStrategy implements HttpSessionIdStrategy { - - private final String[] headerNames; - - /** - * Constructor. - * - * @param configuration The HTTP session configuration - */ - public HeadersHttpSessionIdStrategy(HttpSessionConfiguration configuration) { - this.headerNames = configuration.getHeaderNames(); - if (ArrayUtils.isEmpty(headerNames)) { - throw new ConfigurationException("At least one header name is required"); - } - } - - /** - * @return The header names to check - */ - public String[] getHeaderNames() { - return headerNames; - } - - @Override - public List resolveIds(HttpRequest message) { - for (String headerName : headerNames) { - List all = message.getHeaders().getAll(headerName); - if (!all.isEmpty()) { - return all; - } - } - return Collections.emptyList(); - } - - @Override - public void encodeId(HttpRequest request, MutableHttpResponse response, Session session) { - MutableHttpHeaders headers = response.getHeaders(); - if (session.isExpired()) { - headers.add(headerNames[0], ""); - } else { - headers.add(headerNames[0], session.getId()); - } - } -} diff --git a/session/src/main/java/io/micronaut/session/http/HttpSessionConfiguration.java b/session/src/main/java/io/micronaut/session/http/HttpSessionConfiguration.java deleted file mode 100644 index 42f8c316e9d..00000000000 --- a/session/src/main/java/io/micronaut/session/http/HttpSessionConfiguration.java +++ /dev/null @@ -1,242 +0,0 @@ -/* - * Copyright 2017-2020 original authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.micronaut.session.http; - -import io.micronaut.context.annotation.ConfigurationProperties; -import io.micronaut.http.HttpHeaders; -import io.micronaut.http.cookie.CookieConfiguration; -import io.micronaut.http.cookie.SameSite; -import io.micronaut.session.SessionConfiguration; - -import java.time.temporal.TemporalAmount; -import java.util.Optional; - -/** - * Allows configuration of the session. - * - * @author Graeme Rocher - * @since 1.0 - */ -@ConfigurationProperties("http") -public class HttpSessionConfiguration extends SessionConfiguration implements CookieConfiguration { - - /** - * Default Cookie Path. - */ - @SuppressWarnings("WeakerAccess") - public static final String DEFAULT_COOKIEPATH = "/"; - - /** - * Cookie name. - */ - @SuppressWarnings("WeakerAccess") - public static final String DEFAULT_COOKIENAME = "SESSION"; - - /** - * The default remember me value. - */ - @SuppressWarnings("WeakerAccess") - public static final boolean DEFAULT_REMEMBERME = false; - - /** - * The default base64 encode value. - */ - @SuppressWarnings("WeakerAccess") - public static final boolean DEFAULT_BASE64ENCODE = true; - - private boolean rememberMe = DEFAULT_REMEMBERME; - private boolean base64Encode = DEFAULT_BASE64ENCODE; - private TemporalAmount cookieMaxAge; - private Boolean cookieSecure; - private SameSite sameSite; - private String cookiePath = DEFAULT_COOKIEPATH; - private String domainName; - private String cookieName = DEFAULT_COOKIENAME; - private String prefix; - private String[] headerNames = new String[] { HttpHeaders.AUTHORIZATION_INFO, HttpHeaders.X_AUTH_TOKEN }; - - /** - * @return Whether the Base64 encode sessions IDs sent back to clients - */ - public boolean isBase64Encode() { - return base64Encode; - } - - /** - * Default value ({@value #DEFAULT_BASE64ENCODE}). - * @param base64Encode Enable the Base64 encode for sessions IDs sent back to clients - */ - public void setBase64Encode(boolean base64Encode) { - this.base64Encode = base64Encode; - } - - /** - * @return The cookie name to use - */ - @Override - public String getCookieName() { - return cookieName; - } - - /** - * Default value ({@value #DEFAULT_COOKIENAME}). - * @param cookieName Set the cookie name to use - */ - public void setCookieName(String cookieName) { - this.cookieName = cookieName; - } - - /** - * @return The prefix to use when serializing session ID - */ - public Optional getPrefix() { - return Optional.ofNullable(prefix); - } - - /** - * @param prefix Set the prefix to use when serializing session ID - */ - public void setPrefix(String prefix) { - this.prefix = prefix; - } - - /** - * @return The header names when using a Header strategy - */ - public String[] getHeaderNames() { - return headerNames; - } - - /** - * Default values ([{@value io.micronaut.http.HttpHeaders#AUTHORIZATION_INFO}, {@value io.micronaut.http.HttpHeaders#X_AUTH_TOKEN}]). - * @param headerNames Set the header names when using a Header strategy - */ - public void setHeaderNames(String[] headerNames) { - this.headerNames = headerNames; - } - - /** - * @return The cookie path to use - */ - @Override - public Optional getCookiePath() { - return Optional.ofNullable(cookiePath); - } - - @Override - public Optional isCookieHttpOnly() { - return Optional.empty(); - } - - /** - * @param cookiePath Set the cookie path to use. Default value ({@value #DEFAULT_COOKIEPATH}). - */ - public void setCookiePath(String cookiePath) { - this.cookiePath = cookiePath; - } - - /** - * @return The domain name to use for the cookie - */ - public Optional getDomainName() { - return Optional.ofNullable(domainName); - } - - @Override - public Optional getCookieDomain() { - return Optional.ofNullable(domainName); - } - - /** - * @param domainName Set the domain name to use for the cookie - */ - public void setDomainName(String domainName) { - this.domainName = domainName; - } - - /** - * @param cookieDomain Set the domain name to use for the cookie - */ - public void setCookieDomain(String cookieDomain) { - this.domainName = cookieDomain; - } - - /** - * @return The max age to use for the cookie - */ - @Override - public Optional getCookieMaxAge() { - return Optional.ofNullable(cookieMaxAge); - } - - /** - * Sets the maximum age of the cookie. - * @param cookieMaxAge The maximum age of the cookie - */ - public void setCookieMaxAge(TemporalAmount cookieMaxAge) { - this.cookieMaxAge = cookieMaxAge; - } - - /** - * @return Is remember me config - */ - public boolean isRememberMe() { - return rememberMe; - } - - /** - * Default value ({@value #DEFAULT_REMEMBERME}). - * @param rememberMe Enable the remember me setting - */ - public void setRememberMe(boolean rememberMe) { - this.rememberMe = rememberMe; - } - - /** - * @return Is cookie secure - */ - @Override - public Optional isCookieSecure() { - return Optional.ofNullable(cookieSecure); - } - - /** - * Sets the secure status of the cookie. Delegates to {@link io.micronaut.http.HttpRequest#isSecure()} if not set. - * - * @param cookieSecure Whether or not the cookie is secure. - */ - public void setCookieSecure(Boolean cookieSecure) { - this.cookieSecure = cookieSecure; - } - - /** - * @return return the SameSite to use for the cookie. - */ - @Override - public Optional getCookieSameSite() { - return Optional.ofNullable(sameSite); - } - - /** - * Determines if this this {@link io.micronaut.http.cookie.Cookie} can be sent along cross-site requests. - * For more information, please look - * here - * @param sameSite SameSite value - */ - public void setCookieSameSite(SameSite sameSite) { - this.sameSite = sameSite; - } -} diff --git a/session/src/main/java/io/micronaut/session/http/HttpSessionFilter.java b/session/src/main/java/io/micronaut/session/http/HttpSessionFilter.java deleted file mode 100644 index 2bee831a644..00000000000 --- a/session/src/main/java/io/micronaut/session/http/HttpSessionFilter.java +++ /dev/null @@ -1,179 +0,0 @@ -/* - * Copyright 2017-2020 original authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.micronaut.session.http; - -import io.micronaut.core.async.publisher.Publishers; -import io.micronaut.core.util.CollectionUtils; -import io.micronaut.core.util.StringUtils; -import io.micronaut.http.HttpAttributes; -import io.micronaut.http.HttpRequest; -import io.micronaut.http.MutableHttpResponse; -import io.micronaut.http.annotation.Filter; -import io.micronaut.http.filter.HttpServerFilter; -import io.micronaut.http.filter.ServerFilterChain; -import io.micronaut.http.filter.ServerFilterPhase; -import io.micronaut.http.server.exceptions.InternalServerException; -import io.micronaut.inject.MethodExecutionHandle; -import io.micronaut.session.Session; -import io.micronaut.session.SessionStore; -import io.micronaut.session.annotation.SessionValue; -import org.reactivestreams.Publisher; -import reactor.core.publisher.Flux; - -import java.util.List; -import java.util.Optional; - -/** - * A {@link io.micronaut.http.filter.HttpServerFilter} that resolves the current user {@link Session} if present and encodes the Session ID in - * the response. - * - * @author Graeme Rocher - * @since 1.0 - */ -@Filter("/**") -public class HttpSessionFilter implements HttpServerFilter { - - /** - * The order of the filter. - */ - public static final Integer ORDER = ServerFilterPhase.SESSION.order(); - - /** - * Constant for Micronaut SESSION attribute. - */ - public static final CharSequence SESSION_ATTRIBUTE = "micronaut.SESSION"; - - private final SessionStore sessionStore; - private final HttpSessionIdResolver[] resolvers; - private final HttpSessionIdEncoder[] encoders; - - /** - * Constructor. - * - * @param sessionStore The session store - * @param resolvers The HTTP session id resolvers - * @param encoders The HTTP session id encoders - */ - public HttpSessionFilter(SessionStore sessionStore, HttpSessionIdResolver[] resolvers, HttpSessionIdEncoder[] encoders) { - this.sessionStore = sessionStore; - this.resolvers = resolvers; - this.encoders = encoders; - } - - @Override - public int getOrder() { - return ORDER; - } - - @Override - public Publisher> doFilter(HttpRequest request, ServerFilterChain chain) { - request.setAttribute(HttpSessionFilter.class.getName(), true); - for (HttpSessionIdResolver resolver : resolvers) { - List ids = resolver.resolveIds(request); - if (CollectionUtils.isNotEmpty(ids)) { - String id = ids.get(0); - Publisher> sessionLookup = Publishers.fromCompletableFuture(() -> sessionStore.findSession(id)); - Flux> storeSessionInAttributes = Flux - .from(sessionLookup) - .switchMap(session -> { - session.ifPresent(entries -> request.getAttributes().put(SESSION_ATTRIBUTE, entries)); - return chain.proceed(request); - }); - return encodeSessionId(request, storeSessionInAttributes); - } - } - return encodeSessionId(request, chain.proceed(request)); - } - - private Publisher> encodeSessionId(HttpRequest request, Publisher> responsePublisher) { - Flux responseFlowable = Flux.from(responsePublisher) - .switchMap(response -> { - - Optional routeMatch = request.getAttribute(HttpAttributes.ROUTE_MATCH, MethodExecutionHandle.class); - Optional body = response.getBody(); - - String sessionAttr; - - if (body.isPresent()) { - sessionAttr = routeMatch.flatMap(m -> { - if (!m.hasAnnotation(SessionValue.class)) { - return Optional.empty(); - } else { - String attributeName = m.stringValue(SessionValue.class).orElse(null); - if (!StringUtils.isEmpty(attributeName)) { - return Optional.of(attributeName); - } else { - throw new InternalServerException("@SessionValue on a return type must specify an attribute name"); - } - } - }).orElse(null); - } else { - sessionAttr = null; - } - - Optional opt = request.getAttributes().get(SESSION_ATTRIBUTE, Session.class); - if (opt.isPresent()) { - Session session = opt.get(); - if (sessionAttr != null) { - session.put(sessionAttr, body.get()); - } - - if (session.isNew() || session.isModified()) { - return Flux.from(Publishers.fromCompletableFuture(() -> sessionStore.save(session))) - .map(s -> new SessionAndResponse(Optional.of(s), response)); - } - } else if (sessionAttr != null) { - Session newSession = sessionStore.newSession(); - newSession.put(sessionAttr, body.get()); - return Flux - .from(Publishers.fromCompletableFuture(() -> sessionStore.save(newSession))) - .map(s -> new SessionAndResponse(Optional.of(s), response)); - } - return Flux.just(new SessionAndResponse(opt, response)); - }); - - return responseFlowable.map(sessionAndResponse -> { - Optional session = sessionAndResponse.session; - MutableHttpResponse response = sessionAndResponse.response; - if (session.isPresent()) { - Session s = session.get(); - for (HttpSessionIdEncoder encoder : encoders) { - encoder.encodeId(request, response, s); - } - } - return response; - }); - } - - /** - * Store the session and the response. - */ - class SessionAndResponse { - final Optional session; - final MutableHttpResponse response; - - /** - * Constructor. - * - * @param session The optional session - * @param response The mutable HTTP response - */ - SessionAndResponse(Optional session, MutableHttpResponse response) { - this.session = session; - this.response = response; - } - } -} diff --git a/session/src/main/java/io/micronaut/session/http/HttpSessionIdEncoder.java b/session/src/main/java/io/micronaut/session/http/HttpSessionIdEncoder.java deleted file mode 100644 index 297b4143c65..00000000000 --- a/session/src/main/java/io/micronaut/session/http/HttpSessionIdEncoder.java +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright 2017-2020 original authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.micronaut.session.http; - -import io.micronaut.http.HttpRequest; -import io.micronaut.http.MutableHttpResponse; -import io.micronaut.session.Session; - -/** - * Strategy interface for encoding {@link Session} IDs so they are represented in the response. - * - * @author Graeme Rocher - * @since 1.0 - */ -public interface HttpSessionIdEncoder { - - /** - * Encode the given Session into the response. The strategy can choose to use headers, cookies or whatever strategy - * suites the use case. - * - * @param request The request - * @param response The response - * @param session The session - */ - void encodeId(HttpRequest request, MutableHttpResponse response, Session session); -} diff --git a/session/src/main/java/io/micronaut/session/http/HttpSessionIdResolver.java b/session/src/main/java/io/micronaut/session/http/HttpSessionIdResolver.java deleted file mode 100644 index 8404f4ef583..00000000000 --- a/session/src/main/java/io/micronaut/session/http/HttpSessionIdResolver.java +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright 2017-2020 original authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.micronaut.session.http; - -import io.micronaut.http.HttpRequest; - -import java.util.List; - -/** - * Strategy interface for resolving {@link io.micronaut.session.Session} IDs. - * - * @author Graeme Rocher - * @since 1.0 - */ -public interface HttpSessionIdResolver { - - /** - * Resolve the Session ID from the given HTTP message. - * - * @param message The session ID - * @return An {@link java.util.Optional} - */ - List resolveIds(HttpRequest message); -} diff --git a/session/src/main/java/io/micronaut/session/http/HttpSessionIdStrategy.java b/session/src/main/java/io/micronaut/session/http/HttpSessionIdStrategy.java deleted file mode 100644 index d33c164582a..00000000000 --- a/session/src/main/java/io/micronaut/session/http/HttpSessionIdStrategy.java +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright 2017-2020 original authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.micronaut.session.http; - -/** - * Combines {@link HttpSessionIdResolver} and {@link HttpSessionIdEncoder}. - * - * @author Graeme Rocher - * @since 1.0 - */ -public interface HttpSessionIdStrategy extends HttpSessionIdResolver, HttpSessionIdEncoder { -} diff --git a/session/src/main/java/io/micronaut/session/http/SessionForRequest.java b/session/src/main/java/io/micronaut/session/http/SessionForRequest.java deleted file mode 100644 index f39c4594dc1..00000000000 --- a/session/src/main/java/io/micronaut/session/http/SessionForRequest.java +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Copyright 2017-2020 original authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.micronaut.session.http; - -import io.micronaut.http.HttpRequest; -import io.micronaut.session.Session; -import io.micronaut.session.SessionStore; - -import java.util.Optional; - -/** - * Utility class with methods to create or retrieve a session associated to a request. - * - * @author Sergio del Amo - * @since 1.1.0 - */ -public class SessionForRequest { - - /** - * Creates a session and stores it in the request attributes. - * - * @param sessionStore the session store - * @param request the Http Request - * @return A new session stored in the request attributes - */ - public static Session create(SessionStore sessionStore, HttpRequest request) { - Session session = sessionStore.newSession(); - request.getAttributes().put(HttpSessionFilter.SESSION_ATTRIBUTE, session); - return session; - } - - /** - * Finds a session. - * - * @param request the Http Request - * @return A session if found in the request attributes. - */ - public static Optional find(HttpRequest request) { - return request.getAttributes().get(HttpSessionFilter.SESSION_ATTRIBUTE, Session.class); - } - - /** - * Finds a session or creates a new one and stores it in the request attributes. - * - * @param request The Http Request - * @param sessionStore The session store to create the session if not found - * @return A session if found in the request attributes or a new session - * stored in the request attributes. - */ - public static Session findOrCreate(HttpRequest request, SessionStore sessionStore) { - return find(request).orElseGet(() -> create(sessionStore, request)); - } -} diff --git a/session/src/main/java/io/micronaut/session/http/SessionLocaleResolver.java b/session/src/main/java/io/micronaut/session/http/SessionLocaleResolver.java deleted file mode 100644 index c2878c4101d..00000000000 --- a/session/src/main/java/io/micronaut/session/http/SessionLocaleResolver.java +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Copyright 2017-2020 original authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.micronaut.session.http; - -import io.micronaut.context.annotation.Requires; -import io.micronaut.core.annotation.NonNull; -import io.micronaut.http.HttpRequest; -import io.micronaut.http.server.HttpServerConfiguration; -import io.micronaut.http.server.util.locale.HttpAbstractLocaleResolver; -import io.micronaut.http.server.util.locale.HttpLocaleResolutionConfiguration; -import jakarta.inject.Singleton; - -import java.util.Locale; -import java.util.Optional; - -/** - * Resolves the locale from a property in a session. - * - * @author James Kleeh - * @since 2.3.0 - */ -@Singleton -@Requires(property = HttpServerConfiguration.HttpLocaleResolutionConfigurationProperties.PREFIX + ".session-attribute") -public class SessionLocaleResolver extends HttpAbstractLocaleResolver { - - private final String sessionAttribute; - - /** - * @param httpLocaleResolutionConfiguration The locale resolution configuration - */ - public SessionLocaleResolver(HttpLocaleResolutionConfiguration httpLocaleResolutionConfiguration) { - super(httpLocaleResolutionConfiguration); - this.sessionAttribute = httpLocaleResolutionConfiguration.getSessionAttribute() - .orElseThrow(() -> new IllegalArgumentException("The session attribute must be set")); - } - - @Override - @NonNull - public Optional resolve(@NonNull HttpRequest request) { - return SessionForRequest.find(request) - .flatMap(session -> session.get(sessionAttribute, Locale.class)); - } -} diff --git a/session/src/main/java/io/micronaut/session/http/SessionLogElement.java b/session/src/main/java/io/micronaut/session/http/SessionLogElement.java deleted file mode 100644 index dfd32aef86a..00000000000 --- a/session/src/main/java/io/micronaut/session/http/SessionLogElement.java +++ /dev/null @@ -1,84 +0,0 @@ -/* - * Copyright 2017-2020 original authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.micronaut.session.http; - -import io.micronaut.http.server.netty.NettyHttpRequest; -import io.micronaut.http.server.netty.handler.accesslog.element.ConstantElement; -import io.micronaut.http.server.netty.handler.accesslog.element.LogElement; -import io.micronaut.session.Session; -import io.netty.channel.ChannelHandlerContext; -import io.netty.handler.codec.http.HttpHeaders; -import io.netty.util.Attribute; -import io.netty.util.AttributeKey; - -import java.util.Set; - -/** - * SessionLogElement LogElement. The session. - * - * @author croudet - * @since 2.0 - */ -public class SessionLogElement implements LogElement { - /** - * The session marker. - */ - public static final String SESSION = "u"; - - @SuppressWarnings("rawtypes") - private static final AttributeKey KEY = AttributeKey.valueOf(NettyHttpRequest.class.getSimpleName()); - - private final String property; - - /** - * Creates a SessionElement. - * - * @param property A property stored in the Session or null. When property is null the session id will be printed. - */ - SessionLogElement(String property) { - this.property = property; - } - - @Override - public LogElement copy() { - return this; - } - - @SuppressWarnings("rawtypes") - @Override - public String onResponseHeaders(ChannelHandlerContext ctx, HttpHeaders headers, String status) { - final Attribute attr = ctx.channel().attr(KEY); - NettyHttpRequest request = attr.get(); - if (request == null) { - return ConstantElement.UNKNOWN_VALUE; - } - return SessionForRequest.find(request).map(this::value).orElse(ConstantElement.UNKNOWN_VALUE); - } - - private String value(Session session) { - return property == null ? session.getId() : session.get(property).map(Object::toString).orElse(ConstantElement.UNKNOWN_VALUE); - } - - @Override - public Set events() { - return Event.RESPONSE_HEADERS_EVENTS; - } - - @Override - public String toString() { - return '%' + SESSION; - } -} diff --git a/session/src/main/java/io/micronaut/session/http/SessionLogElementBuilder.java b/session/src/main/java/io/micronaut/session/http/SessionLogElementBuilder.java deleted file mode 100644 index 1739959763f..00000000000 --- a/session/src/main/java/io/micronaut/session/http/SessionLogElementBuilder.java +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright 2017-2020 original authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.micronaut.session.http; - -import io.micronaut.http.server.netty.handler.accesslog.element.LogElement; -import io.micronaut.http.server.netty.handler.accesslog.element.LogElementBuilder; - -/** - * Builder for SessionLogElement. - * - * @author croudet - * @since 2.0 - */ -public final class SessionLogElementBuilder implements LogElementBuilder { - - @Override - public LogElement build(String token, String param) { - if (SessionLogElement.SESSION.equals(token)) { - return new SessionLogElement(param); - } - return null; - } - -} diff --git a/session/src/main/java/io/micronaut/session/http/package-info.java b/session/src/main/java/io/micronaut/session/http/package-info.java deleted file mode 100644 index da1e8b25729..00000000000 --- a/session/src/main/java/io/micronaut/session/http/package-info.java +++ /dev/null @@ -1,22 +0,0 @@ -/* - * Copyright 2017-2020 original authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -/** - * HTTP session configuration and strategies. - * - * @author Graeme Rocher - * @since 1.0 - */ -package io.micronaut.session.http; diff --git a/session/src/main/java/io/micronaut/session/package-info.java b/session/src/main/java/io/micronaut/session/package-info.java deleted file mode 100644 index 6aa0393dd12..00000000000 --- a/session/src/main/java/io/micronaut/session/package-info.java +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright 2017-2020 original authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -/** - * Micronaut session handling. - * - * @author Graeme Rocher - * @since 1.0 - */ -@Configuration -package io.micronaut.session; - -import io.micronaut.context.annotation.Configuration; diff --git a/session/src/main/java/io/micronaut/session/websocket/SessionWebSocketEventListener.java b/session/src/main/java/io/micronaut/session/websocket/SessionWebSocketEventListener.java deleted file mode 100644 index 312d8c7a226..00000000000 --- a/session/src/main/java/io/micronaut/session/websocket/SessionWebSocketEventListener.java +++ /dev/null @@ -1,71 +0,0 @@ -/* - * Copyright 2017-2020 original authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.micronaut.session.websocket; - -import io.micronaut.context.annotation.Requires; -import io.micronaut.context.event.ApplicationEventListener; -import io.micronaut.core.annotation.Internal; -import io.micronaut.core.convert.value.MutableConvertibleValues; -import io.micronaut.session.Session; -import io.micronaut.session.SessionStore; -import io.micronaut.websocket.event.WebSocketEvent; -import io.micronaut.websocket.event.WebSocketMessageProcessedEvent; -import io.micronaut.websocket.event.WebSocketSessionClosedEvent; -import jakarta.inject.Singleton; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * Persists the session in the background on web socket events. - * - * @author graemerocher - * @since 1.0 - */ -@Requires(classes = WebSocketEvent.class) -@Requires(beans = SessionStore.class) -@Singleton -@Internal -public class SessionWebSocketEventListener implements ApplicationEventListener { - - private static final Logger LOG = LoggerFactory.getLogger(SessionWebSocketEventListener.class); - - private final SessionStore sessionStore; - - /** - * Default constructor. - * @param sessionStore The session store - */ - SessionWebSocketEventListener(SessionStore sessionStore) { - this.sessionStore = sessionStore; - } - - @Override - public void onApplicationEvent(WebSocketEvent event) { - if (event instanceof WebSocketMessageProcessedEvent || event instanceof WebSocketSessionClosedEvent) { - MutableConvertibleValues attributes = event.getSource().getAttributes(); - if (attributes instanceof Session) { - Session session = (Session) attributes; - if (session.isModified()) { - sessionStore.save(session).whenComplete((entries, throwable) -> { - if (throwable != null && LOG.isErrorEnabled()) { - LOG.error("Error persisting session following WebSocket event: " + throwable.getMessage(), throwable); - } - }); - } - } - } - } -} diff --git a/session/src/main/java/io/micronaut/session/websocket/package-info.java b/session/src/main/java/io/micronaut/session/websocket/package-info.java deleted file mode 100644 index 18266a3c7c2..00000000000 --- a/session/src/main/java/io/micronaut/session/websocket/package-info.java +++ /dev/null @@ -1,22 +0,0 @@ -/* - * Copyright 2017-2020 original authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -/** - * Classes specific to WebSocket's and sessions. - * - * @author graemerocher - * @since 1.0 - */ -package io.micronaut.session.websocket; diff --git a/session/src/main/resources/META-INF/services/io.micronaut.http.server.netty.handler.accesslog.element.LogElementBuilder b/session/src/main/resources/META-INF/services/io.micronaut.http.server.netty.handler.accesslog.element.LogElementBuilder deleted file mode 100644 index 6fb005a2f7c..00000000000 --- a/session/src/main/resources/META-INF/services/io.micronaut.http.server.netty.handler.accesslog.element.LogElementBuilder +++ /dev/null @@ -1 +0,0 @@ -io.micronaut.session.http.SessionLogElementBuilder diff --git a/session/src/test/groovy/io/micronaut/session/InMemorySessionStoreSpec.groovy b/session/src/test/groovy/io/micronaut/session/InMemorySessionStoreSpec.groovy deleted file mode 100644 index 1bd7fc1b1c4..00000000000 --- a/session/src/test/groovy/io/micronaut/session/InMemorySessionStoreSpec.groovy +++ /dev/null @@ -1,140 +0,0 @@ -package io.micronaut.session - -import io.micronaut.context.ApplicationContext -import io.micronaut.context.event.ApplicationEventListener -import io.micronaut.session.event.AbstractSessionEvent -import io.micronaut.session.event.SessionCreatedEvent -import io.micronaut.session.event.SessionDeletedEvent -import io.micronaut.session.event.SessionExpiredEvent -import jakarta.inject.Singleton -import spock.lang.Specification -import spock.util.concurrent.PollingConditions - -class InMemorySessionStoreSpec extends Specification { - - void "test in-memory session store read and write"() { - when: - ApplicationContext applicationContext = ApplicationContext.run() - SessionStore sessionStore = applicationContext.getBean(SessionStore) - TestListener listener = applicationContext.getBean(TestListener) - Session session = sessionStore.newSession() - - session.put("foo", "bar") - - then: - session != null - session.id - !session.expired - session.creationTime - session.lastAccessedTime - - when: - sessionStore.save(session).get() - def lastAccessedTime = session.lastAccessedTime - - then: - listener.events.size() == 1 - listener.events[0] instanceof SessionCreatedEvent - - when: - Thread.sleep(50) - session == sessionStore.findSession(session.id).get().get() - def conditions = new PollingConditions(timeout: 10) - - then: - conditions.eventually { - session.lastAccessedTime > lastAccessedTime - session.get("foo").isPresent() - session.get("foo").get() == "bar" - } - - when: - listener.events.clear() - sessionStore.deleteSession(session.id) - - then: - conditions.eventually { - assert listener.events.size() == 1 - assert listener.events[0] instanceof SessionDeletedEvent - assert !sessionStore.findSession(session.id).get().isPresent() - } - - cleanup: - applicationContext.close() - } - - void "test session expiry"() { - when: - ApplicationContext applicationContext = ApplicationContext.run(['micronaut.session.max-inactive-interval': 'PT1S']) - SessionStore sessionStore = applicationContext.getBean(SessionStore) - TestListener listener = applicationContext.getBean(TestListener) - Session session = sessionStore.newSession() - session.put("foo", "bar") - sessionStore.save(session) - String id = session.id - PollingConditions conditions = new PollingConditions(timeout: 5, initialDelay: 2) - - then: - conditions.eventually { - assert !sessionStore.findSession(id).get().isPresent() - assert listener.events.any { it instanceof SessionExpiredEvent } - } - - cleanup: - applicationContext.close() - } - - void "test session prompt expiration"() { - when: - ApplicationContext applicationContext = ApplicationContext.run([ - 'micronaut.session.prompt-expiration': true, - 'micronaut.session.max-inactive-interval': 'PT3S' - ]) - SessionStore sessionStore = applicationContext.getBean(SessionStore) - TestListener listener = applicationContext.getBean(TestListener) - Session session = sessionStore.newSession() - - session.put("foo", "bar") - sessionStore.save(session) - - then: - session != null - session.id - !session.expired - session.creationTime - session.lastAccessedTime - - when: - sessionStore.save(session).get() - def lastAccessedTime = session.lastAccessedTime - - then: - listener.events.size() == 1 - listener.events[0] instanceof SessionCreatedEvent - - when: - Thread.sleep(50) - session == sessionStore.findSession(session.id).get().get() - def conditions = new PollingConditions(timeout: 10) - - then: - conditions.eventually { - assert session.lastAccessedTime > lastAccessedTime - assert session.get("foo").isPresent() - assert session.get("foo").get() == "bar" - assert listener.events.any { it instanceof SessionExpiredEvent } - } - - cleanup: - applicationContext.close() - } - - @Singleton - static class TestListener implements ApplicationEventListener { - List events = [] - @Override - void onApplicationEvent(AbstractSessionEvent event) { - events.add(event) - } - } -} diff --git a/session/src/test/groovy/io/micronaut/session/docs/Cart.java b/session/src/test/groovy/io/micronaut/session/docs/Cart.java deleted file mode 100644 index 5674ee7eea8..00000000000 --- a/session/src/test/groovy/io/micronaut/session/docs/Cart.java +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright 2017-2020 original authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.micronaut.session.docs; - -import java.util.ArrayList; -import java.util.List; - -/** - * @author graemerocher - * @since 1.0 - */ -public class Cart { - - private List items = new ArrayList<>(); - - public List getItems() { - return items; - } - - public void setItems(List items) { - this.items = items; - } -} diff --git a/session/src/test/groovy/io/micronaut/session/docs/ShoppingController.java b/session/src/test/groovy/io/micronaut/session/docs/ShoppingController.java deleted file mode 100644 index 4bb10625194..00000000000 --- a/session/src/test/groovy/io/micronaut/session/docs/ShoppingController.java +++ /dev/null @@ -1,72 +0,0 @@ -/* - * Copyright 2017-2020 original authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.micronaut.session.docs; - -// tag::imports[] - -import io.micronaut.core.annotation.Nullable; -import io.micronaut.http.annotation.Controller; -import io.micronaut.http.annotation.Get; -import io.micronaut.http.annotation.Post; -import io.micronaut.session.Session; -import io.micronaut.session.annotation.SessionValue; - -import javax.validation.constraints.NotBlank; -// end::imports[] - -/** - * @author graemerocher - * @since 1.0 - */ -// tag::class[] -@Controller("/shopping") -public class ShoppingController { - private static final String ATTR_CART = "cart"; // <1> -// end::class[] - - // tag::view[] - @Get("/cart") - @SessionValue(ATTR_CART) // <1> - Cart viewCart(@SessionValue @Nullable Cart cart) { // <2> - if (cart == null) { - cart = new Cart(); - } - return cart; - } - // end::view[] - - // tag::add[] - @Post("/cart/{name}") - Cart addItem(Session session, @NotBlank String name) { // <2> - Cart cart = session.get(ATTR_CART, Cart.class).orElseGet(() -> { // <3> - Cart newCart = new Cart(); - session.put(ATTR_CART, newCart); // <4> - return newCart; - }); - cart.getItems().add(name); - return cart; - } - // end::add[] - - // tag::clear[] - @Post("/cart/clear") - void clearCart(@Nullable Session session) { - if (session != null) { - session.remove(ATTR_CART); - } - } - // end::clear[] -} diff --git a/session/src/test/groovy/io/micronaut/session/docs/ShoppingControllerSpec.groovy b/session/src/test/groovy/io/micronaut/session/docs/ShoppingControllerSpec.groovy deleted file mode 100644 index 335d426b04a..00000000000 --- a/session/src/test/groovy/io/micronaut/session/docs/ShoppingControllerSpec.groovy +++ /dev/null @@ -1,81 +0,0 @@ -/* - * Copyright 2017-2019 original authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.micronaut.session.docs - -import io.micronaut.context.ApplicationContext -import io.micronaut.http.HttpHeaders -import io.micronaut.http.HttpRequest -import io.micronaut.http.HttpResponse -import io.micronaut.http.client.HttpClient -import io.micronaut.runtime.server.EmbeddedServer -import spock.lang.AutoCleanup -import spock.lang.Shared -import spock.lang.Specification - -/** - * @author graemerocher - * @since 1.0 - */ -class ShoppingControllerSpec extends Specification { - - @Shared @AutoCleanup EmbeddedServer embeddedServer = ApplicationContext.run(EmbeddedServer) - @Shared @AutoCleanup HttpClient httpClient = embeddedServer - .getApplicationContext() - .createBean(HttpClient, embeddedServer.getURL()) - - void "test session value used on return value"() { - - // tag::view[] - when: "The shopping cart is retrieved" - HttpResponse response = httpClient.exchange(HttpRequest.GET('/shopping/cart'), Cart) // <1> - .blockFirst() - Cart cart = response.body() - - then: "The shopping cart is present as well as a session id header" - response.header(HttpHeaders.AUTHORIZATION_INFO) != null // <2> - cart != null - cart.items.isEmpty() - // end::view[] - - when: "an item is added to the cart using the session id" - - // tag::add[] - String sessionId = response.header(HttpHeaders.AUTHORIZATION_INFO) // <1> - - response = httpClient.exchange( - HttpRequest.POST('/shopping/cart/Apple', "") - .header(HttpHeaders.AUTHORIZATION_INFO, sessionId), Cart) // <2> - .blockFirst() - cart = response.body() - // end::add[] - - then: "The cart is returned with the added items" - cart != null - cart.items.size() == 1 - - when: "The session id is used to retrieve the cart" - response = httpClient.exchange(HttpRequest.GET('/shopping/cart') - .header(HttpHeaders.AUTHORIZATION_INFO, sessionId), Cart) - .blockFirst() - cart = response.body() - - then: "Then the same cart is returned" - response.header(HttpHeaders.AUTHORIZATION_INFO) - cart != null - cart.items.size() == 1 - cart.items[0] == "Apple" - } -} diff --git a/session/src/test/groovy/io/micronaut/session/http/CookieHttpSessionStrategySpec.groovy b/session/src/test/groovy/io/micronaut/session/http/CookieHttpSessionStrategySpec.groovy deleted file mode 100644 index bf8c83b5138..00000000000 --- a/session/src/test/groovy/io/micronaut/session/http/CookieHttpSessionStrategySpec.groovy +++ /dev/null @@ -1,104 +0,0 @@ -/* - * Copyright 2017-2019 original authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.micronaut.session.http - -import io.micronaut.core.convert.ConversionService -import io.micronaut.http.HttpHeaders -import io.micronaut.http.HttpRequest -import io.micronaut.http.HttpResponse -import io.micronaut.http.cookie.Cookie -import io.micronaut.http.cookie.SameSite -import io.micronaut.http.netty.cookies.NettyCookie -import io.micronaut.http.server.HttpServerConfiguration -import io.micronaut.http.server.netty.NettyHttpRequest -import io.micronaut.session.Session -import io.netty.channel.ChannelHandlerContext -import io.netty.handler.codec.http.DefaultFullHttpRequest -import io.netty.handler.codec.http.HttpMethod -import io.netty.handler.codec.http.HttpVersion -import io.netty.handler.codec.http.cookie.CookieEncoder -import io.netty.handler.codec.http.cookie.ServerCookieEncoder -import spock.lang.Specification - -import java.util.regex.Pattern - -/** - * @author Graeme Rocher - * @since 1.0 - */ -class CookieHttpSessionStrategySpec extends Specification { - - void "test resolve default cookie config"() { - given: - CookieHttpSessionStrategy strategy = new CookieHttpSessionStrategy(new HttpSessionConfiguration()) - def nettyRequest = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, '/test') - CookieEncoder encoder = ServerCookieEncoder.STRICT - def encoded = encoder.encode(((NettyCookie) Cookie.of(HttpSessionConfiguration.DEFAULT_COOKIENAME, new String(Base64.encoder.encode("1234".bytes)))).getNettyCookie()) - nettyRequest.headers().add(HttpHeaders.COOKIE, encoded) - HttpRequest request = new NettyHttpRequest(nettyRequest, Mock(ChannelHandlerContext), ConversionService.SHARED, new HttpServerConfiguration()) - - expect: - strategy.resolveIds(request) == ['1234'] - - } - - void "test encode default cookie config"() { - given: - def configuration = new HttpSessionConfiguration() - if (domain) configuration.domainName = domain - if (path) configuration.cookiePath = path - if (prefix) configuration.prefix = prefix - configuration.cookieSecure = configSecure - if (sameSite) configuration.sameSite = sameSite - CookieHttpSessionStrategy strategy = new CookieHttpSessionStrategy(configuration) - - def request = Mock(HttpRequest) - request.isSecure() >> secure - - def response = HttpResponse.ok() - def session = Mock(Session) - session.getId() >> id - session.isExpired() >> expired - - strategy.encodeId(request, response, session) - def header = response.headers.get(HttpHeaders.SET_COOKIE) - - expect: - expected instanceof Pattern ? expected.matcher(header).find() : header == expected - - - where: - id | prefix | path | domain | encoded | expired | secure | configSecure | sameSite | expected - "1234" | null | null | null | encode(id) | false | false | true | SameSite.Lax | "SESSION=$encoded; Path=/; Secure; HTTPOnly; SameSite=Lax" - "1234" | null | null | null | encode(id) | false | false | true | SameSite.Strict| "SESSION=$encoded; Path=/; Secure; HTTPOnly; SameSite=Strict" - "1234" | null | null | null | encode(id) | false | false | true | SameSite.None | "SESSION=$encoded; Path=/; Secure; HTTPOnly; SameSite=None" - "1234" | null | null | null | encode(id) | false | false | true | null | "SESSION=$encoded; Path=/; Secure; HTTPOnly" - "1234" | null | null | null | encode(id) | false | false | true | null | "SESSION=$encoded; Path=/; Secure; HTTPOnly" - "1234" | null | null | null | encode(id) | false | false | false | null | "SESSION=$encoded; Path=/; HTTPOnly" - "1234" | "foo-" | null | null | encode(prefix + id) | false | false | false | null | "SESSION=$encoded; Path=/; HTTPOnly" - "1234" | null | "/foo" | null | encode(id) | false | false | false | null | "SESSION=$encoded; Path=/foo; HTTPOnly" - "1234" | null | null | "example.com" | encode(id) | false | false | false | null | "SESSION=$encoded; Path=/; Domain=example.com; HTTPOnly" - "1234" | null | null | null | encode(id) | true | false | false | null | ~/SESSION=; Max-Age=0; Expires=.*; Path=\/; HTTPOnly/ - "1234" | null | null | null | encode(id) | false | true | false | null | "SESSION=$encoded; Path=/; HTTPOnly" - "1234" | null | null | null | encode(id) | false | true | true | null | "SESSION=$encoded; Path=/; Secure; HTTPOnly" - "1234" | null | null | null | encode(id) | false | true | null | null | "SESSION=$encoded; Path=/; Secure; HTTPOnly" - - } - - protected String encode(String id) { - new String(Base64.encoder.encode(id.bytes)) - } -} diff --git a/session/src/test/groovy/io/micronaut/session/http/HttpSessionConfigurationSpec.groovy b/session/src/test/groovy/io/micronaut/session/http/HttpSessionConfigurationSpec.groovy deleted file mode 100644 index cf9c4e618a3..00000000000 --- a/session/src/test/groovy/io/micronaut/session/http/HttpSessionConfigurationSpec.groovy +++ /dev/null @@ -1,23 +0,0 @@ -package io.micronaut.session.http - -import io.micronaut.context.ApplicationContext -import spock.lang.Specification - -import java.time.temporal.ChronoUnit - -class HttpSessionConfigurationSpec extends Specification { - - void "test configuring max age"() { - given: - ApplicationContext ctx = ApplicationContext.run('micronaut.session.http.cookie-max-age': '365d') - - expect: - ctx.getBean(HttpSessionConfiguration) - .getCookieMaxAge() - .get() - .get(ChronoUnit.SECONDS) == 365 * 24 * 60 * 60 - - cleanup: - ctx.close() - } -} diff --git a/session/src/test/groovy/io/micronaut/session/http/SessionBindingSpec.groovy b/session/src/test/groovy/io/micronaut/session/http/SessionBindingSpec.groovy deleted file mode 100644 index f7e0d258e16..00000000000 --- a/session/src/test/groovy/io/micronaut/session/http/SessionBindingSpec.groovy +++ /dev/null @@ -1,230 +0,0 @@ -/* - * Copyright 2017-2019 original authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.micronaut.session.http - -import io.micronaut.context.ApplicationContext -import io.micronaut.context.annotation.Requires -import io.micronaut.core.annotation.Nullable -import io.micronaut.http.HttpHeaders -import io.micronaut.http.HttpRequest -import io.micronaut.http.HttpResponse -import io.micronaut.http.annotation.Controller -import io.micronaut.http.annotation.Get -import io.micronaut.http.client.HttpClient -import io.micronaut.runtime.server.EmbeddedServer -import io.micronaut.session.Session -import io.micronaut.session.annotation.SessionValue -import reactor.core.publisher.Flux -import spock.lang.Specification - - -/** - * @author Graeme Rocher - * @since 1.0 - */ -class SessionBindingSpec extends Specification { - - void "test bind simple session argument using HTTP header processing"() { - given: - ApplicationContext context = ApplicationContext.run([ - 'spec.name': getClass().simpleName - ]) - EmbeddedServer embeddedServer = context.getBean(EmbeddedServer).start() - HttpClient client = context.createBean(HttpClient, embeddedServer.getURL()) - - when: - Flux> flowable = Flux.from(client.exchange( - HttpRequest.GET("/sessiontest/simple"), String - )) - HttpResponse response = flowable.blockFirst() - - then: - response.getBody().get() == "not in session" - response.header(HttpHeaders.AUTHORIZATION_INFO) - - when: - def sessionId = response.header(HttpHeaders.AUTHORIZATION_INFO) - flowable = Flux.from(client.exchange( - HttpRequest.GET("/sessiontest/simple") - .header(HttpHeaders.AUTHORIZATION_INFO, sessionId) - , String - )) - response = flowable.blockFirst() - - then: - response.getBody().get() == "value in session" - response.header(HttpHeaders.AUTHORIZATION_INFO) - - cleanup: - embeddedServer.stop() - } - - - void "test bind simple session argument using Cookie processing"() { - given: - ApplicationContext context = ApplicationContext.run([ - 'spec.name': getClass().simpleName - ]) - EmbeddedServer embeddedServer = context.getBean(EmbeddedServer).start() - HttpClient client = context.createBean(HttpClient, embeddedServer.getURL()) - - when: - Flux> flowable = Flux.from(client.exchange( - HttpRequest.GET("/sessiontest/simple"), String - )) - HttpResponse response = flowable.blockFirst() - - then: - response.getBody().get() == "not in session" - response.header(HttpHeaders.SET_COOKIE) - - when: - def sessionId = response.header(HttpHeaders.SET_COOKIE) - flowable = Flux.from(client.exchange( - HttpRequest.GET("/sessiontest/simple") - .header(HttpHeaders.COOKIE, sessionId) - , String - )) - response = flowable.blockFirst() - - then: - response.getBody().get() == "value in session" - response.header(HttpHeaders.SET_COOKIE) - - cleanup: - embeddedServer.stop() - } - - void "test bind optional session"() { - given: - ApplicationContext context = ApplicationContext.run([ - 'spec.name': getClass().simpleName - ]) - EmbeddedServer embeddedServer = context.getBean(EmbeddedServer).start() - HttpClient client = context.createBean(HttpClient, embeddedServer.getURL()) - - when: - Flux> flowable = Flux.from(client.exchange( - HttpRequest.GET("/sessiontest/optional"), String - )) - HttpResponse response = flowable.blockFirst() - - then: - response.getBody().get() == "no session" - !response.header(HttpHeaders.AUTHORIZATION_INFO) - - when: - flowable = Flux.from(client.exchange( - HttpRequest.GET("/sessiontest/simple"), String - )) - response = flowable.blockFirst() - - then: - response.getBody().get() == "not in session" - response.header(HttpHeaders.AUTHORIZATION_INFO) - - when: - def sessionId = response.header(HttpHeaders.AUTHORIZATION_INFO) - - flowable = Flux.from(client.exchange( - HttpRequest.GET("/sessiontest/optional") - .header(HttpHeaders.AUTHORIZATION_INFO, sessionId) - , String - )) - response = flowable.blockFirst() - - then: - response.getBody().get() == "value in session" - response.header(HttpHeaders.AUTHORIZATION_INFO) - - when: - flowable = Flux.from(client.exchange( - HttpRequest.GET("/sessiontest/value") - .header(HttpHeaders.AUTHORIZATION_INFO, sessionId) - , String - )) - response = flowable.blockFirst() - - then: - response.getBody().get() == "value in session" - - when: - flowable = Flux.from(client.exchange( - HttpRequest.GET("/sessiontest/value-nullable") - .header(HttpHeaders.AUTHORIZATION_INFO, sessionId) - , String - )) - response = flowable.blockFirst() - - then: - response.getBody().get() == "value in session" - - when: - flowable = Flux.from(client.exchange( - HttpRequest.GET("/sessiontest/value-nullable") - , String - )) - response = flowable.blockFirst() - - then: - response.getBody().get() == "no value in session" - - - cleanup: - embeddedServer.stop() - } - - @Requires(property = "spec.name", value = "SessionBindingSpec") - @Controller('/sessiontest') - static class SessionController { - - @Get("/simple") - String simple(Session session) { - return session.get("myValue").orElseGet({ - session.put("myValue", "value in session") - "not in session" - }) - } - - @Get("/value") - String value(@SessionValue Optional myValue) { - return myValue.orElse( - "no value in session" - ) - } - - @Get("/value-nullable") - String valueNullable(@SessionValue @Nullable String myValue) { - return myValue ?: "no value in session" - } - - @Get("/optional") - String optional(Optional session) { - if(session.isPresent()) { - def s = session.get() - return s.get("myValue").orElseGet({ - s.put("myValue", "value in session") - "not in session" - }) - } - else { - return "no session" - } - } - } -} - - diff --git a/session/src/test/groovy/io/micronaut/session/http/SessionCreationSpec.groovy b/session/src/test/groovy/io/micronaut/session/http/SessionCreationSpec.groovy deleted file mode 100644 index e442bc6be8f..00000000000 --- a/session/src/test/groovy/io/micronaut/session/http/SessionCreationSpec.groovy +++ /dev/null @@ -1,73 +0,0 @@ -package io.micronaut.session.http - -import io.micronaut.context.ApplicationContext -import io.micronaut.context.annotation.Requires -import io.micronaut.http.HttpHeaders -import io.micronaut.http.HttpRequest -import io.micronaut.http.HttpResponse -import io.micronaut.http.MutableHttpResponse -import io.micronaut.http.annotation.Controller -import io.micronaut.http.annotation.Get -import io.micronaut.http.client.HttpClient -import io.micronaut.http.cookie.Cookie -import io.micronaut.runtime.server.EmbeddedServer -import io.micronaut.session.Session -import io.micronaut.session.SessionStore -import org.reactivestreams.Publisher -import reactor.core.publisher.Flux -import reactor.core.publisher.FluxSink -import spock.lang.Specification - -class SessionCreationSpec extends Specification { - - void "test a controller can create a session"() { - given: - ApplicationContext context = ApplicationContext.run([ - 'spec.name': getClass().simpleName - ]) - EmbeddedServer embeddedServer = context.getBean(EmbeddedServer).start() - HttpClient client = context.createBean(HttpClient, embeddedServer.getURL()) - - when: - HttpResponse response = client.toBlocking().exchange(HttpRequest.GET("/sessiontest")) - - then: - response.header("SET-COOKIE").contains("SESSION") - - when: - def sessionId = response.header(HttpHeaders.SET_COOKIE).split(';')[0].split('=')[1] - String body = client.toBlocking() - .retrieve(HttpRequest.GET("/sessiontest/get") - .cookie(Cookie.of("SESSION", sessionId)), String) - - then: - body == "SessionCreationSpec" - } - - @Requires(property = "spec.name", value = "SessionCreationSpec") - @Controller('/sessiontest') - static class SessionController { - - private final SessionStore sessionStore - - SessionController(SessionStore sessionStore) { - this.sessionStore = sessionStore - } - - @Get("/get") - String getValue(Session session) { - session.get("specName").get() - } - - @Get(single = true) - Publisher> createSession(HttpRequest request) { - return Flux.create({ emitter -> - Session session = SessionForRequest.find(request).orElseGet(() -> SessionForRequest.create(sessionStore, request)); - session.put("specName", "SessionCreationSpec") - emitter.next(HttpResponse.ok()) - emitter.complete() - }, FluxSink.OverflowStrategy.ERROR) - } - - } -} diff --git a/session/src/test/groovy/io/micronaut/session/http/SessionLocaleResolverSpec.groovy b/session/src/test/groovy/io/micronaut/session/http/SessionLocaleResolverSpec.groovy deleted file mode 100644 index 5cae5cc5d4f..00000000000 --- a/session/src/test/groovy/io/micronaut/session/http/SessionLocaleResolverSpec.groovy +++ /dev/null @@ -1,40 +0,0 @@ -package io.micronaut.session.http - -import io.micronaut.context.ApplicationContext -import io.micronaut.core.convert.value.MutableConvertibleValues -import io.micronaut.http.HttpRequest -import io.micronaut.session.Session -import io.micronaut.session.SessionStore -import spock.lang.Specification - -class SessionLocaleResolverSpec extends Specification { - - void "test in-memory session store read and write"() { - given: - ApplicationContext applicationContext = ApplicationContext.run(['micronaut.server.locale-resolution.session-attribute': 'userlocale']) - - SessionStore sessionStore = applicationContext.getBean(SessionStore) - Session session = sessionStore.newSession() - session.put("userlocale", Locale.CANADA_FRENCH) - def attrs = Stub(MutableConvertibleValues) { - get(HttpSessionFilter.SESSION_ATTRIBUTE, Session.class) >> Optional.of(session) - } - def req = Stub(HttpRequest) { - getAttributes() >> attrs - } - - expect: - applicationContext.containsBean(SessionLocaleResolver) - - when: - SessionLocaleResolver sessionLocaleResolver = applicationContext.getBean(SessionLocaleResolver) - Optional locale = sessionLocaleResolver.resolve(req) - - then: - locale.isPresent() - locale.get() == Locale.CANADA_FRENCH - - cleanup: - applicationContext.close() - } -} diff --git a/session/src/test/groovy/io/micronaut/session/http/WebSocketSessionSpec.groovy b/session/src/test/groovy/io/micronaut/session/http/WebSocketSessionSpec.groovy deleted file mode 100644 index 7d11d8080e1..00000000000 --- a/session/src/test/groovy/io/micronaut/session/http/WebSocketSessionSpec.groovy +++ /dev/null @@ -1,106 +0,0 @@ -/* - * Copyright 2017-2019 original authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.micronaut.session.http - -import io.micronaut.context.ApplicationContext -import io.micronaut.http.HttpHeaders -import io.micronaut.http.HttpRequest -import io.micronaut.http.HttpResponse -import io.micronaut.http.annotation.Controller -import io.micronaut.http.annotation.Get -import io.micronaut.http.client.HttpClient -import io.micronaut.runtime.server.EmbeddedServer -import io.micronaut.session.Session -import io.micronaut.websocket.WebSocketClient -import io.micronaut.websocket.WebSocketSession -import io.micronaut.websocket.annotation.ClientWebSocket -import io.micronaut.websocket.annotation.OnMessage -import io.micronaut.websocket.annotation.ServerWebSocket -import reactor.core.publisher.Flux -import spock.lang.Specification -import spock.util.concurrent.PollingConditions - -class WebSocketSessionSpec extends Specification { - - void "test websocket can share HTTP session"() { - given: - EmbeddedServer embeddedServer = ApplicationContext.run(EmbeddedServer) - - WebSocketClient wsClient = embeddedServer.applicationContext.createBean(WebSocketClient, embeddedServer.getURL()) - HttpClient httpClient = embeddedServer.applicationContext.createBean(HttpClient, embeddedServer.getURL()) - - when: - HttpResponse response = httpClient.toBlocking().exchange(HttpRequest.GET('/ws/session/simple'), String) - String sessionId = response.header(HttpHeaders.AUTHORIZATION_INFO) - - then: - sessionId - - when: - def result = httpClient.toBlocking().retrieve(HttpRequest.GET('/ws/session/simple').header(HttpHeaders.AUTHORIZATION_INFO, sessionId), String) - - then: - result == 'value in session' - - when: - SomeValueClient someValueClient= Flux.from(wsClient.connect(SomeValueClient, HttpRequest.GET('/ws/somesocket').header(HttpHeaders.AUTHORIZATION_INFO, sessionId))).blockFirst() - someValueClient.send("hello") - PollingConditions conditions = new PollingConditions(timeout: 3, delay: 0.5) - - then: - conditions.eventually { - someValueClient.replies.contains("hello value is value in session") - } - - cleanup: - wsClient.close() - httpClient.close() - embeddedServer.close() - } - - @Controller('/ws/session') - static class SessionController { - - @Get("/simple") - String simple(Session session) { - return session.get("myValue").orElseGet({ - session.put("myValue", "value in session") - "not in session" - }) - } - } - - @ClientWebSocket('/ws/somesocket') - static abstract class SomeValueClient { - List replies = [] - - @OnMessage - void onMessage(String msg) { - replies.add(msg) - } - - abstract void send(String msg) - } - - @ServerWebSocket('/ws/somesocket') - static class SomeValueSocket { - - @OnMessage - void onMessage(String msg, WebSocketSession session) { - session.sendSync("$msg value is ${session.get('myValue', String).orElse(null)}".toString()) - } - } -} diff --git a/session/src/test/resources/logback.xml b/session/src/test/resources/logback.xml deleted file mode 100644 index afaebf8e17d..00000000000 --- a/session/src/test/resources/logback.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n - - - - - - - \ No newline at end of file diff --git a/settings.gradle b/settings.gradle index fbc6b1dda60..061f9b17947 100644 --- a/settings.gradle +++ b/settings.gradle @@ -54,7 +54,6 @@ include "retry" include "router" include "runtime" include "runtime-osx" -include "session" include "validation" include "websocket" diff --git a/src/main/docs/guide/appendix/breaks.adoc b/src/main/docs/guide/appendix/breaks.adoc index 10e7e52a026..85c52123699 100644 --- a/src/main/docs/guide/appendix/breaks.adoc +++ b/src/main/docs/guide/appendix/breaks.adoc @@ -23,19 +23,26 @@ dependency::micronaut-websocket[] The instrumentation features for Reactor have been moved to the `micronaut-reactor` module. If you require instrumentation of reactive code paths (for distributed tracing for example) you should make sure your application depends on `micronaut-reactor`: -dependency::micronaut-reactor[groupId="io.micronaut.reactor"] +dependency:micronaut-reactor[groupId="io.micronaut.reactor"] + +==== Session Support Moved to Session Module + +The Session handling features have been moved to a new `micronaut-session` module. +If you require session support, you should make your application depend on + +dependency:micronaut-session[groupId="io.micronaut.session"] ==== Kotlin Flow Support Moved to Kotlin Module Support for the Kotlin `Flow` type has been moved to the `micronaut-kotlin` module. If your application uses Kotlin `Flow` you should ensure the `micronaut-kotlin-runtime` module is on your application classpath: -dependency::micronaut-kotlin-runtime[groupId="io.micronaut.kotlin"] +dependency:micronaut-kotlin-runtime[groupId="io.micronaut.kotlin"] ==== Compilation Time API Split into new module In order to keep the runtime small all types and interfaces that are used at compilation time only (like the `io.micronaut.inject.ast` API) have been moved into a separate module: -dependency::micronaut-core-processor[] +dependency:micronaut-core-processor[] If you are using types and interfaces from this module you should take care to split the compilation time and runtime logic of your module into separate modules. diff --git a/src/main/docs/guide/httpServer/sessions.adoc b/src/main/docs/guide/httpServer/sessions.adoc index 09a4e316153..4021a4834b3 100644 --- a/src/main/docs/guide/httpServer/sessions.adoc +++ b/src/main/docs/guide/httpServer/sessions.adoc @@ -9,18 +9,18 @@ Micronaut includes a `session` module inspired by https://projects.spring.io/spr To enable support for in-memory sessions you just need the `session` dependency: -dependency:micronaut-session[] +dependency:micronaut-session[groupId=io.micronaut.session] === Redis Sessions -To store api:session.Session[] instances in Redis, use the https://micronaut-projects.github.io/micronaut-redis/latest/guide/#sessions[Micronaut Redis] module which includes detailed instructions. +To store link:{micronautsessionapi}/io/micronaut/session/Session.html[Session] instances in Redis, use the https://micronaut-projects.github.io/micronaut-redis/latest/guide/#sessions[Micronaut Redis] module which includes detailed instructions. To quickly get up and running with Redis sessions you must also have the `redis-lettuce` dependency in your build: .build.gradle [source,groovy] ---- -compile "io.micronaut:micronaut-session" +compile "io.micronaut-session:micronaut-session" compile "io.micronaut.redis:micronaut-redis-lettuce" ---- @@ -40,9 +40,9 @@ micronaut: == Configuring Session Resolution -api:session.Session[] resolution can be configured with api:session.http.HttpSessionConfiguration[]. +link:{micronautsessionapi}/io/micronaut/session/Session.html[Session] resolution can be configured with link:{micronautsessionapi}/io/micronaut/session/http/HttpSessionConfiguration.html[HttpSessionConfiguration]. -By default, sessions are resolved using an api:session.http.HttpSessionFilter[] that looks for session identifiers via either an HTTP header (using the `Authorization-Info` or `X-Auth-Token` headers) or via a Cookie named `SESSION`. +By default, sessions are resolved using link:{micronautsessionapi}/io/micronaut/session/http/HttpSessionFilter.html[HttpSessionFilter] that looks for session identifiers via either an HTTP header (using the `Authorization-Info` or `X-Auth-Token` headers) or via a Cookie named `SESSION`. You can disable either header resolution or cookie resolution via configuration in `application.yml`: @@ -60,22 +60,22 @@ The above configuration enables header resolution, but disables cookie resolutio == Working with Sessions -A api:session.Session[] can be retrieved with a parameter of type api:session.Session[] in a controller method. For example consider the following controller: +A link:{micronautsessionapi}/io/micronaut/session/Session.html[Session] can be retrieved with a parameter of type link:{micronautsessionapi}/io/micronaut/session/Session.html[Session] in a controller method. For example consider the following controller: snippet::io.micronaut.docs.session.ShoppingController[tags="imports,class,add,endclass", indent=0] -<1> `ShoppingController` declares a api:session.Session[] attribute named `cart` -<2> The api:session.Session[] is declared as a method parameter +<1> `ShoppingController` declares a link:{micronautsessionapi}/io/micronaut/session/Session.html[Session] attribute named `cart` +<2> The link:{micronautsessionapi}/io/micronaut/session/Session.html[Session] is declared as a method parameter <3> The `cart` attribute is retrieved <4> Otherwise a new `Cart` instance is created and stored in the session -Note that because the api:session.Session[] is declared as a required parameter, to execute the controller action a api:session.Session[] will be created and saved to the api:session.SessionStore[]. +Note that because the link:{micronautsessionapi}/io/micronaut/session/Session.html[Session] is declared as a required parameter, to execute the controller action a link:{micronautsessionapi}/io/micronaut/session/Session.html[Session] will be created and saved to the link:{micronautsessionapi}/io/micronaut/session/SessionStore.html[SessionStore]. -If you don't want to create unnecessary sessions, declare the api:session.Session[] as `@Nullable` in which case a session will not be created and saved unnecessarily. For example: +If you don't want to create unnecessary sessions, declare the link:{micronautsessionapi}/io/micronaut/session/Session.html[Session] as `@Nullable` in which case a session will not be created and saved unnecessarily. For example: snippet::io.micronaut.docs.session.ShoppingController[tags="clear", indent=0,title="Using @Nullable with Sessions"] -The above method only injects a new api:session.Session[] if one already exists. +The above method only injects a new link:{micronautsessionapi}/io/micronaut/session/Session.html[Session] if one already exists. == Session Clients @@ -89,7 +89,7 @@ snippet::io.micronaut.docs.session.ShoppingControllerSpec[tags="view", indent=0] <1> A request is made to `/shopping/cart` <2> The `AUTHORIZATION_INFO` header is present in the response -You can then pass this `AUTHORIZATION_INFO` in subsequent requests to reuse the existing api:session.Session[]: +You can then pass this `AUTHORIZATION_INFO` in subsequent requests to reuse the existing link:{micronautsessionapi}/io/micronaut/session/Session.html[Session]: .Sending the AUTHORIZATION_INFO header snippet::io.micronaut.docs.session.ShoppingControllerSpec[tags="add", indent=0] @@ -99,16 +99,16 @@ snippet::io.micronaut.docs.session.ShoppingControllerSpec[tags="add", indent=0] == Using @SessionValue -Rather than explicitly injecting the api:session.Session[] into a controller method, you can instead use ann:session.annotation.SessionValue[]. For example: +Rather than explicitly injecting the link:{micronautsessionapi}/io/micronaut/session/Session.html[Session] into a controller method, you can instead use link:{micronautsessionapi}/io/micronaut/session/annotation/SessionValue.html[@SessionValue]. For example: snippet::io.micronaut.docs.session.ShoppingController[tags="view", indent=0,title="Using @SessionValue"] -<1> ann:session.annotation.SessionValue[] is declared on the method resulting in the return value being stored in the api:session.Session[]. Note that you must specify the attribute name when used on a return value -<2> ann:session.annotation.SessionValue[] is used on a `@Nullable` parameter which results in looking up the value from the api:session.Session[] in a non-blocking way and supplying it if present. In the case a value is not specified to ann:session.annotation.SessionValue[] resulting in the parameter name being used to lookup the attribute. +<1> link:{micronautsessionapi}/io/micronaut/session/annotation/SessionValue.html[@SessionValue] is declared on the method resulting in the return value being stored in the link:{micronautsessionapi}/io/micronaut/session/Session.html[Session]. Note that you must specify the attribute name when used on a return value +<2> link:{micronautsessionapi}/io/micronaut/session/annotation/SessionValue.html[@SessionValue] is used on a `@Nullable` parameter which results in looking up the value from the link:{micronautsessionapi}/io/micronaut/session/Session.html[Session] in a non-blocking way and supplying it if present. In the case a value is not specified to link:{micronautsessionapi}/io/micronaut/session/annotation/SessionValue.html[@SessionValue] resulting in the parameter name being used to lookup the attribute. == Session Events -You can register api:context.event.ApplicationEventListener[] beans to listen for api:session.Session[] related events located in the pkg:session.event[] package. +You can register api:context.event.ApplicationEventListener[] beans to listen for link:{micronautsessionapi}/io/micronaut/session/Session.html[Session] related events located in the link:{micronautsessionapi}/io/micronaut/session/event/package-summary.html[session.event] package. The following table summarizes the events: @@ -116,16 +116,16 @@ The following table summarizes the events: |=== |Type|Description -|api:session.event.SessionCreatedEvent[] -|Fired when a api:session.Session[] is created +|link:{micronautsessionapi}/io/micronaut/session/event/SessionCreatedEvent.html[SessionCreatedEvent] +|Fired when a link:{micronautsessionapi}/io/micronaut/session/Session.html[Session] is created -|api:session.event.SessionDeletedEvent[] -|Fired when a api:session.Session[] is deleted +|link:{micronautsessionapi}/io/micronaut/session/event/SessionDeletedEvent.html[SessionDeletedEvent] +|Fired when a link:{micronautsessionapi}/io/micronaut/session/Session.html[Session] is deleted -|api:session.event.SessionExpiredEvent[] -|Fired when a api:session.Session[] expires +|link:{micronautsessionapi}/io/micronaut/session/event/SessionExpiredEvent.html[SessionExpiredEvent] +|Fired when a link:{micronautsessionapi}/io/micronaut/session/Session.html[Session] expires -|api:session.event.SessionDestroyedEvent[] +|link:{micronautsessionapi}/io/micronaut/session/event/SessionDestroyedEvent.html[SessionDestroyedEvent] |Parent of both `SessionDeletedEvent` and `SessionExpiredEvent` |=== diff --git a/test-suite-groovy/build.gradle b/test-suite-groovy/build.gradle index 628192caef9..69eb884039c 100644 --- a/test-suite-groovy/build.gradle +++ b/test-suite-groovy/build.gradle @@ -9,6 +9,16 @@ micronautBuild { } } +repositories { + mavenCentral() + maven { + url "https://s01.oss.sonatype.org/content/repositories/snapshots/" + mavenContent { + snapshotsOnly() + } + } +} + dependencies { testImplementation libs.managed.netty.codec.http testImplementation project(":http-client") @@ -19,7 +29,7 @@ dependencies { testImplementation project(":validation") testImplementation project(":inject") testImplementation project(":management") - testImplementation project(":session") + testImplementation(libs.micronaut.session) testImplementation libs.jcache testImplementation libs.managed.groovy.sql testImplementation libs.managed.groovy.templates diff --git a/test-suite-kotlin/build.gradle b/test-suite-kotlin/build.gradle index 9565cf014fd..832ed67d879 100644 --- a/test-suite-kotlin/build.gradle +++ b/test-suite-kotlin/build.gradle @@ -12,6 +12,17 @@ micronautBuild { } } +repositories { + mavenLocal() + mavenCentral() + maven { + url "https://s01.oss.sonatype.org/content/repositories/snapshots/" + mavenContent { + snapshotsOnly() + } + } +} + dependencies { api libs.kotlin.stdlib api libs.kotlin.reflect @@ -39,7 +50,7 @@ dependencies { testImplementation libs.jcache testImplementation project(':validation') testImplementation project(":http-client") - testImplementation project(":session") + testImplementation(libs.micronaut.session) testImplementation project(":jackson-databind") testImplementation libs.managed.groovy.templates diff --git a/test-suite/build.gradle b/test-suite/build.gradle index 26bb8a66aba..686dd97ea44 100644 --- a/test-suite/build.gradle +++ b/test-suite/build.gradle @@ -22,6 +22,16 @@ micronautBuild { } } +repositories { + mavenCentral() + maven { + url "https://s01.oss.sonatype.org/content/repositories/snapshots/" + mavenContent { + snapshotsOnly() + } + } +} + dependencies { annotationProcessor project(":inject-java") api project(":core-processor") @@ -40,7 +50,7 @@ dependencies { testImplementation project(":inject") testImplementation project(":function-client") testImplementation project(":function-web") - testImplementation project(":session") + testImplementation(libs.micronaut.session) // Adding these for now since micronaut-test isnt resolving correctly ... probably need to upgrade gradle there too testImplementation libs.junit.jupiter.api From 629db3883a94387a61a3d3a24903cf725b355d0a Mon Sep 17 00:00:00 2001 From: Graeme Rocher Date: Fri, 9 Dec 2022 12:29:58 +0100 Subject: [PATCH 292/743] Add tests and refine behaviour of registerSingleton (#8469) There are a number of inconsistencies with `registerSingleton`. Currently the following problems exist: * Calling `registerSingleton` when the bean type and qualifier match an existing bean type and qualifier doesn't override the bean (it does in Micronaut 3.x but probably shouldn't) * Calling `registerSingleton` twice consecutively overrides the previously defined bean if the type and qualifier match * If the registered type and the type of the instance differ (say `Foo` as the bean type but `FooImpl` for the implementation) then `getBeansOfType(Foo.class)` correctly contains the bean and `getBeansOfType(FooImpl.class)` correctly doesn't contain the bean. However the bean can be incorrectly located by `findBean(FooImpl.class)` This PR tries to address these issues in the following ways: * Calling `registerSingleton` always adds a new bean and never overrides * If you want to use `registerSingleton` for bean replacement then you must call `replaces(TypeToReplace)` using the `RuntimeBeanDefinition` API thus formalizing the way to replace a bean. * Beans that are registered with a particular type (`Foo`) and a particular impl (`FooImpl`) cannot be located by either `getBeansOfType(FooImpl)` or any bean lookup methods like `getBean`, `findBean` etc. * Also improves handling of runtime beans with generics Co-authored-by: Sergio del Amo --- .../constructor/ConstructorFactorySpec.groovy | 8 +- .../inject/field/FieldArrayFactorySpec.groovy | 8 +- .../inject/field/FieldFactorySpec.groovy | 8 +- .../ConstructorFactorySpec.groovy | 8 +- .../context/RegisterSingletonSpec.groovy | 85 ++++++++++++++++++- .../FieldArrayFactorySpec.groovy | 2 +- .../factoryinjection/FieldFactorySpec.groovy | 8 +- .../context/DefaultApplicationContext.java | 8 +- .../micronaut/context/DefaultBeanContext.java | 47 +++++----- .../context/DefaultRuntimeBeanDefinition.java | 59 +++++++++++-- .../context/RuntimeBeanDefinition.java | 40 +++++++++ .../io/micronaut/context/SingletonScope.java | 16 +--- .../executor/ExecutorServiceConfigSpec.groovy | 4 +- src/main/docs/guide/appendix/breaks.adoc | 17 ++++ 14 files changed, 256 insertions(+), 62 deletions(-) diff --git a/inject-groovy/src/test/groovy/io/micronaut/inject/constructor/ConstructorFactorySpec.groovy b/inject-groovy/src/test/groovy/io/micronaut/inject/constructor/ConstructorFactorySpec.groovy index 14f01470fdd..ed39e634e41 100644 --- a/inject-groovy/src/test/groovy/io/micronaut/inject/constructor/ConstructorFactorySpec.groovy +++ b/inject-groovy/src/test/groovy/io/micronaut/inject/constructor/ConstructorFactorySpec.groovy @@ -32,8 +32,7 @@ class ConstructorFactorySpec extends Specification { void "test injection with constructor supplied by a provider"() { given: - BeanContext context = new DefaultBeanContext() - context.start() + BeanContext context = BeanContext.run() when:"A bean is obtained which has a constructor that depends on a bean provided by a provider" B b = context.getBean(B) @@ -44,7 +43,10 @@ class ConstructorFactorySpec extends Specification { b.a.c != null b.a.c2 != null b.a.d != null - b.a.is(context.getBean(AImpl)) + b.a.is(context.getBean(A)) + + cleanup: + context.close() } static interface A { diff --git a/inject-groovy/src/test/groovy/io/micronaut/inject/field/FieldArrayFactorySpec.groovy b/inject-groovy/src/test/groovy/io/micronaut/inject/field/FieldArrayFactorySpec.groovy index a6a010bc797..0ca4609f55c 100644 --- a/inject-groovy/src/test/groovy/io/micronaut/inject/field/FieldArrayFactorySpec.groovy +++ b/inject-groovy/src/test/groovy/io/micronaut/inject/field/FieldArrayFactorySpec.groovy @@ -31,8 +31,7 @@ class FieldArrayFactorySpec extends Specification { void "test injection with field supplied by a provider"() { given: - BeanContext context = new DefaultBeanContext() - context.start() + BeanContext context = BeanContext.run() when:"A bean is obtained which has a field that depends on a bean provided by a provider" B b = context.getBean(B) @@ -42,7 +41,10 @@ class FieldArrayFactorySpec extends Specification { b.all[0] instanceof AImpl b.all[0].c != null b.all[0].c2 != null - b.all[0].is(context.getBean(AImpl)) + b.all[0].is(context.getBean(A)) + + cleanup: + context.close() } static interface A { diff --git a/inject-groovy/src/test/groovy/io/micronaut/inject/field/FieldFactorySpec.groovy b/inject-groovy/src/test/groovy/io/micronaut/inject/field/FieldFactorySpec.groovy index ad0bf11429a..0c4e4142430 100644 --- a/inject-groovy/src/test/groovy/io/micronaut/inject/field/FieldFactorySpec.groovy +++ b/inject-groovy/src/test/groovy/io/micronaut/inject/field/FieldFactorySpec.groovy @@ -31,8 +31,7 @@ class FieldFactorySpec extends Specification { void "test injection with field supplied by a provider"() { given: - BeanContext context = new DefaultBeanContext() - context.start() + BeanContext context = BeanContext.run() when:"A bean is obtained which has a field that depends on a bean provided by a provider" B b = context.getBean(B) @@ -42,7 +41,10 @@ class FieldFactorySpec extends Specification { b.a instanceof AImpl b.a.c != null b.a.c2 != null - b.a.is(context.getBean(AImpl)) + b.a.is(context.getBean(A)) + + cleanup: + context.close() } static interface A { diff --git a/inject-java/src/test/groovy/io/micronaut/inject/constructor/factoryinjection/ConstructorFactorySpec.groovy b/inject-java/src/test/groovy/io/micronaut/inject/constructor/factoryinjection/ConstructorFactorySpec.groovy index 0fa5f561b6a..6c84ab7001c 100644 --- a/inject-java/src/test/groovy/io/micronaut/inject/constructor/factoryinjection/ConstructorFactorySpec.groovy +++ b/inject-java/src/test/groovy/io/micronaut/inject/constructor/factoryinjection/ConstructorFactorySpec.groovy @@ -23,8 +23,7 @@ class ConstructorFactorySpec extends Specification { void "test injection with constructor supplied by a provider"() { given: - BeanContext context = new DefaultBeanContext() - context.start() + BeanContext context = BeanContext.run() when:"A bean is obtained which has a constructor that depends on a bean provided by a provider" B b = context.getBean(B) @@ -35,6 +34,9 @@ class ConstructorFactorySpec extends Specification { b.a.c != null b.a.c2 != null b.a.d != null - b.a.is(context.getBean(AImpl)) + b.a.is(context.getBean(A)) + + cleanup: + context.close() } } diff --git a/inject-java/src/test/groovy/io/micronaut/inject/context/RegisterSingletonSpec.groovy b/inject-java/src/test/groovy/io/micronaut/inject/context/RegisterSingletonSpec.groovy index 60fe2ed00c8..747d56ba04e 100644 --- a/inject-java/src/test/groovy/io/micronaut/inject/context/RegisterSingletonSpec.groovy +++ b/inject-java/src/test/groovy/io/micronaut/inject/context/RegisterSingletonSpec.groovy @@ -17,16 +17,77 @@ package io.micronaut.inject.context import io.micronaut.context.BeanContext import io.micronaut.context.DefaultBeanContext +import io.micronaut.context.RuntimeBeanDefinition +import io.micronaut.context.annotation.Bean import io.micronaut.context.annotation.Type +import io.micronaut.core.type.Argument import io.micronaut.inject.qualifiers.Qualifiers +import jakarta.inject.Named +import jakarta.inject.Singleton import spock.lang.Issue import spock.lang.Specification +import java.lang.reflect.Proxy + class RegisterSingletonSpec extends Specification { + void "test register singleton with generic types"() { + given: + BeanContext context = BeanContext.run() + + when: + context.registerSingleton(new TestReporter()) + + then: + context.containsBean(Argument.of(Reporter, Span)) + + cleanup: + context.close() + } + + void "test register singleton and exposed type"() { + given: + BeanContext context = BeanContext.run() + + when: + context.registerBeanDefinition( + RuntimeBeanDefinition.builder(Codec, ()-> new OverridingCodec()) + .singleton(true) + .qualifier(Qualifiers.byName("foo")) + .replaces(ToBeReplacedCodec) + .build() + ) // replaces ToBeReplacedCodec + context.registerSingleton(Codec, { } as Codec) // adds a new codec + context.registerSingleton(Codec, new FooCodec()) // adds another codec + context.registerSingleton(new BarCodec()) // should be registered with bean type BarCodec + context.registerSingleton(Codec, new BazCodec(), Qualifiers.byName("baz")) + + then: + def codecs = context.getBeansOfType(Codec) + codecs.size() == 7 + codecs.find { it in FooCodec } + codecs.find { it in BarCodec } + codecs.find { it in BazCodec } + !codecs.find { it in ToBeReplacedCodec } + codecs.find { it in OverridingCodec } + codecs.find { it in OtherCodec } + codecs.find { it in StuffCodec } + codecs.find { it in Proxy } + codecs == context.getBeansOfType(Codec) // second resolve returns the same result + context.getBeansOfType(FooCodec).size() == 0 // not an exposed type + context.getBeansOfType(BarCodec).size() == 1 // BarCodec type is exposed + context.findBean(FooCodec).isEmpty() // not an exposed type + context.findBean(StuffCodec).isEmpty() // not an exposed type + context.findBean(OtherCodec).isPresent() // an exposed type + + cleanup: + context.close() + } + + void "test register singleton method"() { given: - BeanContext context = new DefaultBeanContext().start() + BeanContext context = BeanContext.run() def b = new B() when: @@ -83,4 +144,26 @@ class RegisterSingletonSpec extends Specification { this.type = type } } + + static interface Codec { + + } + + static class OverridingCodec implements Codec {} + static class FooCodec implements Codec {} + static class BarCodec implements Codec {} + static class BazCodec implements Codec {} + @Singleton + @Bean(typed = Codec) + static class StuffCodec implements Codec {} + @Singleton + static class OtherCodec implements Codec {} + + @Singleton + @Named("foo") + static class ToBeReplacedCodec implements Codec {} + + static interface Reporter {} + static class Span {} + static class TestReporter implements Reporter {} } diff --git a/inject-java/src/test/groovy/io/micronaut/inject/field/arrayfactoryinjection/FieldArrayFactorySpec.groovy b/inject-java/src/test/groovy/io/micronaut/inject/field/arrayfactoryinjection/FieldArrayFactorySpec.groovy index 2f4457f2cab..10929fa95ae 100644 --- a/inject-java/src/test/groovy/io/micronaut/inject/field/arrayfactoryinjection/FieldArrayFactorySpec.groovy +++ b/inject-java/src/test/groovy/io/micronaut/inject/field/arrayfactoryinjection/FieldArrayFactorySpec.groovy @@ -35,7 +35,7 @@ class FieldArrayFactorySpec extends Specification { b.all[0] instanceof AImpl ((AImpl)b.all[0]).c != null ((AImpl)b.all[0]).c2 != null - b.all[0].is(context.getBean(AImpl)) + b.all[0].is(context.getBean(A)) } } diff --git a/inject-java/src/test/groovy/io/micronaut/inject/field/factoryinjection/FieldFactorySpec.groovy b/inject-java/src/test/groovy/io/micronaut/inject/field/factoryinjection/FieldFactorySpec.groovy index 4009bd4881c..0e950646dad 100644 --- a/inject-java/src/test/groovy/io/micronaut/inject/field/factoryinjection/FieldFactorySpec.groovy +++ b/inject-java/src/test/groovy/io/micronaut/inject/field/factoryinjection/FieldFactorySpec.groovy @@ -25,8 +25,7 @@ class FieldFactorySpec extends Specification { void "test injection with field supplied by a provider"() { given: - BeanContext context = new DefaultBeanContext() - context.start() + BeanContext context = BeanContext.run() when:"A bean is obtained which has a field that depends on a bean provided by a provider" B b = context.getBean(B) @@ -36,7 +35,10 @@ class FieldFactorySpec extends Specification { b.a instanceof AImpl b.a.c != null b.a.c2 != null - b.a.is(context.getBean(AImpl)) + b.a.is(context.getBean(A)) + + cleanup: + context.close() } diff --git a/inject/src/main/java/io/micronaut/context/DefaultApplicationContext.java b/inject/src/main/java/io/micronaut/context/DefaultApplicationContext.java index 7a37a421a58..f7bf2f93fff 100644 --- a/inject/src/main/java/io/micronaut/context/DefaultApplicationContext.java +++ b/inject/src/main/java/io/micronaut/context/DefaultApplicationContext.java @@ -255,7 +255,13 @@ protected void startEnvironment() { .qualifier(PrimaryQualifier.INSTANCE); //noinspection resource - registerBeanDefinition(definition.build()); + + RuntimeBeanDefinition beanDefinition = definition.build(); + BeanDefinition existing = findBeanDefinition(beanDefinition.getBeanType()).orElse(null); + if (existing instanceof RuntimeBeanDefinition runtimeBeanDefinition) { + removeBeanDefinition(runtimeBeanDefinition); + } + registerBeanDefinition(beanDefinition); } @Override diff --git a/inject/src/main/java/io/micronaut/context/DefaultBeanContext.java b/inject/src/main/java/io/micronaut/context/DefaultBeanContext.java index 50c9661dade..264c4e1ec6f 100644 --- a/inject/src/main/java/io/micronaut/context/DefaultBeanContext.java +++ b/inject/src/main/java/io/micronaut/context/DefaultBeanContext.java @@ -730,7 +730,7 @@ public BeanContext registerSingleton(@NonNull Class type, @NonNull T sing BeanDefinition beanDefinition; if (inject && running.get()) { // Bean cannot be injected before the start of the context - beanDefinition = findBeanDefinition(type, qualifier).orElse(null); + beanDefinition = findConcreteCandidate(null, Argument.of(type), qualifier, false).orElse(null); if (beanDefinition == null) { // Purge cache miss purgeCacheForBeanInstance(singleton); @@ -758,14 +758,6 @@ public BeanContext registerSingleton(@NonNull Class type, @NonNull T sing ); singletonScope.registerSingletonBean(registration, qualifier); registerBeanDefinition(runtimeBeanDefinition); - - for (Class indexedType : indexedTypes) { - if (indexedType == type || indexedType.isAssignableFrom(type)) { - final Collection indexed = resolveTypeIndex(indexedType); - indexed.add(runtimeBeanDefinition); - break; - } - } } return this; } @@ -1663,21 +1655,15 @@ public Collection> getBeanDefinitionReferences() { @NonNull public BeanContext registerBeanDefinition(@NonNull RuntimeBeanDefinition definition) { Objects.requireNonNull(definition, "Bean definition cannot be null"); - BeanDefinition existing = findBeanDefinition(definition.getGenericBeanType(), definition.getDeclaredQualifier()).orElse(null); - if (existing instanceof RuntimeBeanDefinition runtimeBeanDefinition) { - this.beanDefinitionsClasses.remove(runtimeBeanDefinition); - } + Class beanType = definition.getBeanType(); + this.beanDefinitionsClasses.add(definition); for (Class indexedType : indexedTypes) { - if (definition.isCandidateBean(Argument.of(indexedType))) { - Collection index = resolveTypeIndex(indexedType); - if (existing instanceof RuntimeBeanDefinition runtimeBeanDefinition) { - index.remove(runtimeBeanDefinition); - } - index.add(definition); + if (indexedType == beanType || indexedType.isAssignableFrom(beanType)) { + final Collection indexed = resolveTypeIndex(indexedType); + indexed.add(definition); + break; } } - this.beanDefinitionsClasses.add(definition); - Class beanType = definition.getBeanType(); purgeCacheForBeanType(beanType); return this; } @@ -1689,6 +1675,25 @@ private void purgeCacheForBeanType(Class beanType) { containsBeanCache.entrySet().removeIf(entry -> entry.getKey().beanType.isAssignableFrom(beanType)); } + /** + * The definition to remove. + * @param definition The definition to remove + * @param The bean type + */ + @Internal + void removeBeanDefinition(RuntimeBeanDefinition definition) { + Class beanType = definition.getBeanType(); + for (Class indexedType : indexedTypes) { + if (indexedType == beanType || indexedType.isAssignableFrom(beanType)) { + final Collection indexed = resolveTypeIndex(indexedType); + indexed.remove(definition); + break; + } + } + this.beanDefinitionsClasses.remove(definition); + purgeCacheForBeanType(definition.getBeanType()); + } + /** * Get a bean of the given type. * diff --git a/inject/src/main/java/io/micronaut/context/DefaultRuntimeBeanDefinition.java b/inject/src/main/java/io/micronaut/context/DefaultRuntimeBeanDefinition.java index 94a3ad2098d..0e84b8cb809 100644 --- a/inject/src/main/java/io/micronaut/context/DefaultRuntimeBeanDefinition.java +++ b/inject/src/main/java/io/micronaut/context/DefaultRuntimeBeanDefinition.java @@ -15,21 +15,27 @@ */ package io.micronaut.context; +import io.micronaut.context.annotation.Replaces; import io.micronaut.context.exceptions.BeanInstantiationException; +import io.micronaut.core.annotation.AnnotationClassValue; import io.micronaut.core.annotation.AnnotationMetadata; import io.micronaut.core.annotation.Experimental; import io.micronaut.core.annotation.NonNull; import io.micronaut.core.annotation.Nullable; +import io.micronaut.core.naming.Named; import io.micronaut.core.reflect.ReflectionUtils; import io.micronaut.core.type.Argument; import io.micronaut.core.util.ArrayUtils; import io.micronaut.core.util.CollectionUtils; import io.micronaut.inject.BeanDefinition; +import io.micronaut.inject.annotation.MutableAnnotationMetadata; import io.micronaut.inject.qualifiers.PrimaryQualifier; import io.micronaut.inject.qualifiers.TypeArgumentQualifier; import java.lang.annotation.Annotation; import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -57,7 +63,7 @@ final class DefaultRuntimeBeanDefinition extends AbstractBeanContextCondition private final boolean isSingleton; private final Class scope; private final Class[] exposedTypes; - private final Map, List>> typeArguments; + private Map, List>> typeArguments; DefaultRuntimeBeanDefinition( @NonNull Argument beanType, @@ -84,16 +90,30 @@ final class DefaultRuntimeBeanDefinition extends AbstractBeanContextCondition @Override public List> getTypeArguments(Class type) { - if (type == getBeanType()) { + Class bt = getBeanType(); + if (type == bt) { return getTypeArguments(); } - if (typeArguments != null) { - List> args = typeArguments.get(type); - if (args != null) { - return args; + if (type != null && type.isAssignableFrom(bt)) { + if (typeArguments != null) { + List> args = typeArguments.get(type); + if (args != null) { + return args; + } + } + List> list = RuntimeBeanDefinition.super.getTypeArguments(type); + if (CollectionUtils.isNotEmpty(list)) { + if (typeArguments == null) { + synchronized (this.beanType) { + typeArguments = new LinkedHashMap<>(3); + } + } + typeArguments.put(type, list); } + return list; + } else { + return Collections.emptyList(); } - return RuntimeBeanDefinition.super.getTypeArguments(type); } @Override @@ -219,6 +239,7 @@ static final class RuntimeBeanBuilder implements RuntimeBeanDefinition.Builde private Class[] exposedTypes = ReflectionUtils.EMPTY_CLASS_ARRAY; private Map, List>> typeArguments; + private Class replacesType; RuntimeBeanBuilder(Argument beanType, Supplier supplier) { this.beanType = Objects.requireNonNull(beanType, MSG_BEAN_TYPE_CANNOT_BE_NULL); @@ -237,6 +258,12 @@ public Builder qualifier(Qualifier qualifier) { return this; } + @Override + public Builder replaces(Class otherType) { + this.replacesType = otherType; + return this; + } + @Override @SuppressWarnings("java:S1872") public Builder scope(Class scope) { @@ -288,6 +315,24 @@ public Builder annotationMetadata(AnnotationMetadata annotationMetadata) { @Override @NonNull public RuntimeBeanDefinition build() { + if (replacesType != null) { + MutableAnnotationMetadata mutableAnnotationMetadata; + if (this.annotationMetadata instanceof MutableAnnotationMetadata mm) { + mutableAnnotationMetadata = mm; + } else if (this.annotationMetadata == null || this.annotationMetadata == EMPTY_METADATA) { + mutableAnnotationMetadata = new MutableAnnotationMetadata(); + this.annotationMetadata = mutableAnnotationMetadata; + } else { + throw new IllegalStateException("Previous non-mutable annotation metadata set"); + } + + Map values = new HashMap<>(3); + values.put(AnnotationMetadata.VALUE_MEMBER, new AnnotationClassValue<>(replacesType)); + if (qualifier instanceof Named named) { + values.put("named", named.getName()); + } + mutableAnnotationMetadata.addAnnotation(Replaces.class.getName(), values); + } return new DefaultRuntimeBeanDefinition<>( beanType, supplier, diff --git a/inject/src/main/java/io/micronaut/context/RuntimeBeanDefinition.java b/inject/src/main/java/io/micronaut/context/RuntimeBeanDefinition.java index 0082f506aec..aab361b565a 100644 --- a/inject/src/main/java/io/micronaut/context/RuntimeBeanDefinition.java +++ b/inject/src/main/java/io/micronaut/context/RuntimeBeanDefinition.java @@ -20,6 +20,7 @@ import io.micronaut.core.annotation.Experimental; import io.micronaut.core.annotation.NonNull; import io.micronaut.core.annotation.Nullable; +import io.micronaut.core.reflect.GenericTypeUtils; import io.micronaut.core.type.Argument; import io.micronaut.inject.BeanContextConditional; import io.micronaut.inject.BeanDefinition; @@ -28,8 +29,12 @@ import io.micronaut.inject.qualifiers.Qualifiers; import java.lang.annotation.Annotation; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; import java.util.Objects; import java.util.function.Supplier; +import java.util.stream.Collectors; /** * Allow the construction for bean definitions programmatically that can be registered @@ -61,6 +66,24 @@ default boolean isEnabled(@NonNull BeanContext context, BeanResolutionContext re return true; } + @Override + default List> getTypeArguments(Class type) { + Class beanType = getBeanType(); + if (type != null && type.isAssignableFrom(beanType)) { + if (type.isInterface()) { + return Arrays.stream(GenericTypeUtils.resolveInterfaceTypeArguments(beanType, type)) + .map(Argument::of) + .collect(Collectors.toList()); + } else { + return Arrays.stream(GenericTypeUtils.resolveSuperTypeGenericArguments(beanType, type)) + .map(Argument::of) + .collect(Collectors.toList()); + } + } else { + return Collections.emptyList(); + } + } + @Override default boolean isContextScope() { return getAnnotationMetadata().hasDeclaredAnnotation(Context.class); @@ -185,14 +208,25 @@ interface Builder { * @param qualifier The qualifier * @return This builder */ + @NonNull Builder qualifier(@Nullable Qualifier qualifier); + /** + * Adds this type as a bean replacement of the given type. + * @param otherType The other type + * @return This bean builder + * @since 4.0.0 + */ + @NonNull + Builder replaces(@Nullable Class otherType); + /** * The qualifier to use. * @param name The named qualifier to use. * @return This builder * @since 3.7.0 */ + @NonNull default Builder named(@Nullable String name) { if (name == null) { qualifier(null); @@ -207,6 +241,7 @@ default Builder named(@Nullable String name) { * @param scope The scope * @return This builder */ + @NonNull Builder scope(@Nullable Class scope); /** @@ -214,6 +249,7 @@ default Builder named(@Nullable String name) { * @param isSingleton True if it is singleton * @return This builder */ + @NonNull Builder singleton(boolean isSingleton); /** @@ -221,6 +257,7 @@ default Builder named(@Nullable String name) { * @param types The exposed types * @return This builder */ + @NonNull Builder exposedTypes(Class...types); /** @@ -228,6 +265,7 @@ default Builder named(@Nullable String name) { * @param arguments The arguments * @return This builder */ + @NonNull Builder typeArguments(Argument... arguments); /** @@ -236,6 +274,7 @@ default Builder named(@Nullable String name) { * @param arguments The arguments * @return This builder */ + @NonNull Builder typeArguments(Class implementedType, Argument... arguments); /** @@ -243,6 +282,7 @@ default Builder named(@Nullable String name) { * @param annotationMetadata The annotation metadata * @return This builder */ + @NonNull Builder annotationMetadata(@Nullable AnnotationMetadata annotationMetadata); /** diff --git a/inject/src/main/java/io/micronaut/context/SingletonScope.java b/inject/src/main/java/io/micronaut/context/SingletonScope.java index b344d279f92..97849359bb5 100644 --- a/inject/src/main/java/io/micronaut/context/SingletonScope.java +++ b/inject/src/main/java/io/micronaut/context/SingletonScope.java @@ -112,13 +112,6 @@ BeanRegistration registerSingletonBean(@NonNull BeanRegistration regis DefaultBeanContext.BeanKey beanKey = new DefaultBeanContext.BeanKey<>(beanDefinition, beanDefinition.getDeclaredQualifier()); singletonByArgumentAndQualifier.put(beanKey, registration); } - if (registration.bean != null && registration.bean.getClass() != beanDefinition.getBeanType()) { - // If the actual type differs, allow to inject the actual implementation for cases like: - // `MyInterface factoryBean() { new Impl.. }` - // This might be something to remove in 4.0 - DefaultBeanContext.BeanKey concrete = new DefaultBeanContext.BeanKey<>((Class) registration.bean.getClass(), qualifier); - singletonByArgumentAndQualifier.put(concrete, registration); - } return registration; } @@ -397,17 +390,12 @@ public boolean equals(Object o) { if (beanDefinition.getBeanType() != that.beanDefinition.getBeanType()) { return false; } - Qualifier qualifier = beanDefinition.getDeclaredQualifier(); - Qualifier thatQualifier = that.beanDefinition.getDeclaredQualifier(); - if (qualifier == thatQualifier) { - return true; - } - return qualifier != null && qualifier.equals(thatQualifier); + return beanDefinition.getBeanDefinitionName().equals(that.beanDefinition.getBeanDefinitionName()); } @Override public int hashCode() { - return Objects.hash(beanDefinition.getBeanType(), beanDefinition.getDeclaredQualifier()); + return Objects.hash(beanDefinition.getBeanDefinitionName()); } } diff --git a/runtime/src/test/groovy/io/micronaut/runtime/executor/ExecutorServiceConfigSpec.groovy b/runtime/src/test/groovy/io/micronaut/runtime/executor/ExecutorServiceConfigSpec.groovy index ba4f0d1977d..e1ffcfa862e 100644 --- a/runtime/src/test/groovy/io/micronaut/runtime/executor/ExecutorServiceConfigSpec.groovy +++ b/runtime/src/test/groovy/io/micronaut/runtime/executor/ExecutorServiceConfigSpec.groovy @@ -58,7 +58,7 @@ class ExecutorServiceConfigSpec extends Specification { executorServices.size() == expectedExecutorCount when: - ThreadPoolExecutor poolExecutor = ctx.getBean(ThreadPoolExecutor, Qualifiers.byName("one")) + ThreadPoolExecutor poolExecutor = (ThreadPoolExecutor) ctx.getBean(ExecutorService, Qualifiers.byName("one")) ExecutorService forkJoinPool = ctx.getBean(ExecutorService, Qualifiers.byName("two")) then: @@ -111,7 +111,7 @@ class ExecutorServiceConfigSpec extends Specification { when: Collection executorServices = ctx.getBeansOfType(ExecutorService.class) - ThreadPoolExecutor poolExecutor = ctx.getBean(ThreadPoolExecutor, Qualifiers.byName("one")) + ThreadPoolExecutor poolExecutor = (ThreadPoolExecutor) ctx.getBean(ExecutorService, Qualifiers.byName("one")) ExecutorService forkJoinPool = ctx.getBean(ExecutorService, Qualifiers.byName("two")) then: diff --git a/src/main/docs/guide/appendix/breaks.adoc b/src/main/docs/guide/appendix/breaks.adoc index 85c52123699..2c98942b7c6 100644 --- a/src/main/docs/guide/appendix/breaks.adoc +++ b/src/main/docs/guide/appendix/breaks.adoc @@ -13,6 +13,23 @@ The `micronaut-runtime` module has been split into separate modules depending on In addition, since `micronaut-retry` is now optional declarative clients annotated with ann:http.client.annotation.Client[] no longer invoke fallbacks by default. To restore the previous behaviour add `micronaut-retry` to your classpath and annotate any declarative clients with ann:retry.annotation.Recoverable[]. +==== Calling `registerSingleton(bean)` no longer overrides existing beans + +If you call `registerSingleton(bean)` on the api:context.BeanContext[] this will no longer override existing beans if the type and qualifier match, instead two beans will now exist which may lead to a api:context.exceptions.NonUniqueBeanException[]. + +If you require replacing an existing bean you must formalize the replacement using the api:context.RuntimeBeanDefinition[] API, for example: + +[source,java] +---- +context.registerBeanDefinition( + RuntimeBeanDefinition.builder(Codec.class, ()-> new OverridingCodec()) + .singleton(true) + // the type of the bean to replace + .replaces(ToBeReplacedCodec.class) + .build() +); +---- + ==== WebSocket No Longer Required The `micronaut-websocket` API is no longer a required dependency of the HTTP server. If you are using annotations such as ann:websocket.annotation.ServerWebSocket[] you should add the `micronaut-websocket` dependency to your application classpath: From 91be1e4ad82b3dd6fe12698b66881ffcc14915d4 Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Fri, 9 Dec 2022 15:14:59 +0100 Subject: [PATCH 293/743] refactor: Extract http headers utils (#8471) --- .../http/client/netty/DefaultHttpClient.java | 35 +------ .../netty/DefaultClientHeaderMaskTest.groovy | 86 +++++++++-------- http/build.gradle | 1 + .../micronaut/http/util/HttpHeadersUtil.java | 95 +++++++++++++++++++ .../http/util/HttpHeadersUtilSpec.groovy | 78 +++++++++++++++ .../micronaut/http/util/MockHttpHeaders.java | 93 ++++++++++++++++++ 6 files changed, 317 insertions(+), 71 deletions(-) create mode 100644 http/src/main/java/io/micronaut/http/util/HttpHeadersUtil.java create mode 100644 http/src/test/groovy/io/micronaut/http/util/HttpHeadersUtilSpec.groovy create mode 100644 http/src/test/groovy/io/micronaut/http/util/MockHttpHeaders.java diff --git a/http-client/src/main/java/io/micronaut/http/client/netty/DefaultHttpClient.java b/http-client/src/main/java/io/micronaut/http/client/netty/DefaultHttpClient.java index 2982b185ee1..509107c861c 100644 --- a/http-client/src/main/java/io/micronaut/http/client/netty/DefaultHttpClient.java +++ b/http-client/src/main/java/io/micronaut/http/client/netty/DefaultHttpClient.java @@ -34,7 +34,6 @@ import io.micronaut.core.util.ArrayUtils; import io.micronaut.core.util.CollectionUtils; import io.micronaut.core.util.StringUtils; -import io.micronaut.core.util.SupplierUtil; import io.micronaut.http.HttpAttributes; import io.micronaut.http.HttpResponse; import io.micronaut.http.HttpResponseWrapper; @@ -90,6 +89,7 @@ import io.micronaut.http.sse.Event; import io.micronaut.http.uri.UriBuilder; import io.micronaut.http.uri.UriTemplate; +import io.micronaut.http.util.HttpHeadersUtil; import io.micronaut.jackson.databind.JacksonDatabindMapper; import io.micronaut.json.JsonMapper; import io.micronaut.json.codec.JsonMediaTypeCodec; @@ -183,9 +183,7 @@ import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Supplier; -import java.util.regex.Pattern; import java.util.stream.Collectors; - import static io.micronaut.scheduling.instrument.InvocationInstrumenter.NOOP; /** @@ -211,9 +209,6 @@ public class DefaultHttpClient implements private static final int DEFAULT_HTTP_PORT = 80; private static final int DEFAULT_HTTPS_PORT = 443; - private static final Supplier HEADER_MASK_PATTERNS = SupplierUtil.memoized(() -> - Pattern.compile(".*(password|cred|cert|key|secret|token|auth|signat).*", Pattern.CASE_INSENSITIVE) - ); /** * Which headers not to copy from the first request when redirecting to a second request. There doesn't * appear to be a spec for this. {@link java.net.HttpURLConnection} seems to drop all headers, but that would be a @@ -1754,7 +1749,7 @@ private void debugRequest(URI requestURI, io.netty.handler.codec.http.HttpReques private void traceRequest(io.micronaut.http.HttpRequest request, io.netty.handler.codec.http.HttpRequest nettyRequest) { HttpHeaders headers = nettyRequest.headers(); - traceHeaders(headers); + HttpHeadersUtil.trace(log, headers.names(), headers::getAll); if (io.micronaut.http.HttpMethod.permitsRequestBody(request.getMethod()) && request.getBody().isPresent() && nettyRequest instanceof FullHttpRequest) { FullHttpRequest fullHttpRequest = (FullHttpRequest) nettyRequest; ByteBuf content = fullHttpRequest.content(); @@ -1778,30 +1773,6 @@ private void traceChunk(ByteBuf content) { log.trace("----"); } - private void traceHeaders(HttpHeaders headers) { - for (String name : headers.names()) { - boolean isMasked = HEADER_MASK_PATTERNS.get().matcher(name).matches(); - List all = headers.getAll(name); - if (all.size() > 1) { - for (String value : all) { - String maskedValue = isMasked ? mask(value) : value; - log.trace("{}: {}", name, maskedValue); - } - } else if (!all.isEmpty()) { - String maskedValue = isMasked ? mask(all.get(0)) : all.get(0); - log.trace("{}: {}", name, maskedValue); - } - } - } - - @Nullable - private String mask(@Nullable String value) { - if (value == null) { - return null; - } - return "*MASKED*"; - } - private static MediaTypeCodecRegistry createDefaultMediaTypeRegistry() { JsonMapper mapper = new JacksonDatabindMapper(); ApplicationConfiguration configuration = new ApplicationConfiguration(); @@ -2116,7 +2087,7 @@ protected void channelReadInstrumented(ChannelHandlerContext ctx, R msg) throws HttpHeaders headers = msg.headers(); if (log.isTraceEnabled()) { log.trace("HTTP Client Response Received ({}) for Request: {} {}", msg.status(), finalRequest.getMethodName(), finalRequest.getUri()); - traceHeaders(headers); + HttpHeadersUtil.trace(log, headers.names(), headers::getAll); } buildResponse(responsePromise, msg, httpStatus); } diff --git a/http-client/src/test/groovy/io/micronaut/http/client/netty/DefaultClientHeaderMaskTest.groovy b/http-client/src/test/groovy/io/micronaut/http/client/netty/DefaultClientHeaderMaskTest.groovy index 8f3a693ed91..64134aa22b8 100644 --- a/http-client/src/test/groovy/io/micronaut/http/client/netty/DefaultClientHeaderMaskTest.groovy +++ b/http-client/src/test/groovy/io/micronaut/http/client/netty/DefaultClientHeaderMaskTest.groovy @@ -1,66 +1,64 @@ package io.micronaut.http.client.netty import ch.qos.logback.classic.Level -import ch.qos.logback.classic.Logger +import io.micronaut.context.ApplicationContext +import io.micronaut.context.annotation.Requires +import io.micronaut.http.HttpRequest +import io.micronaut.http.annotation.Controller +import io.micronaut.http.annotation.Get +import io.micronaut.http.client.HttpClient +import io.micronaut.runtime.server.EmbeddedServer +import jakarta.inject.Singleton +import org.slf4j.Logger import ch.qos.logback.classic.spi.ILoggingEvent import ch.qos.logback.core.AppenderBase -import io.micronaut.context.ApplicationContext import io.netty.handler.codec.http.DefaultHttpHeaders import org.slf4j.LoggerFactory import spock.lang.Specification import java.util.concurrent.BlockingQueue import java.util.concurrent.LinkedBlockingQueue -import java.util.concurrent.TimeUnit class DefaultClientHeaderMaskTest extends Specification { - def "check masking works for #value"() { + def "check mask detects common security headers"() { given: - def ctx = ApplicationContext.run() - def client = ctx.createBean(DefaultHttpClient, "http://localhost:8080") + EmbeddedServer server = ApplicationContext.run(EmbeddedServer, ["spec.name": "DefaultClientHeaderMaskTest"]) + ApplicationContext ctx = server.applicationContext + HttpClient client = ctx.createBean(HttpClient, server.URL) expect: - client.mask(value) == expected + client instanceof DefaultHttpClient - cleanup: - ctx.close() + when: + MemoryAppender appender = new MemoryAppender() + Logger log = LoggerFactory.getLogger(DefaultHttpClient.class) - where: - value | expected - null | null - "foo" | "*MASKED*" - "Tim Yates" | "*MASKED*" - } + then: + log instanceof ch.qos.logback.classic.Logger - def "check mask detects common security headers"() { - given: - MemoryAppender appender = new MemoryAppender() - Logger logger = (Logger) LoggerFactory.getLogger(DefaultHttpClient.class) + when: + ch.qos.logback.classic.Logger logger = (ch.qos.logback.classic.Logger) log logger.addAppender(appender) logger.setLevel(Level.TRACE) appender.start() - DefaultHttpHeaders headers = new DefaultHttpHeaders() - headers.add("Authorization", "Bearer foo") - headers.add("Proxy-Authorization", "AWS4-HMAC-SHA256 bar") - headers.add("Cookie", "baz") - headers.add("Set-Cookie", "qux") - headers.add("X-Forwarded-For", "quux") - headers.add("X-Forwarded-Host", "quuz") - headers.add("X-Real-IP", "waldo") - headers.add("X-Forwarded-For", "fred") - headers.add("Credential", "foo") - headers.add("Signature", "bar probably secret") - def ctx = ApplicationContext.run() - def client = ctx.createBean(DefaultHttpClient, "http://localhost:8080") - - when: - client.traceHeaders(headers) + def response = client.toBlocking().exchange(HttpRequest.GET("/masking").headers {headers -> + headers.add("Authorization", "Bearer foo") + headers.add("Proxy-Authorization", "AWS4-HMAC-SHA256 bar") + headers.add("Cookie", "baz") + headers.add("Set-Cookie", "qux") + headers.add("X-Forwarded-For", "quux") + headers.add("X-Forwarded-Host", "quuz") + headers.add("X-Real-IP", "waldo") + headers.add("X-Forwarded-For", "fred") + headers.add("Credential", "foo") + headers.add("Signature", "bar probably secret") + }, String) then: - appender.events.size() == 10 - appender.events.join("\n") == """Authorization: *MASKED* + response.body() == "ok" + appender.events.join("\n").contains("""Authorization: *MASKED* |Proxy-Authorization: *MASKED* |Cookie: baz |Set-Cookie: qux @@ -69,11 +67,21 @@ class DefaultClientHeaderMaskTest extends Specification { |X-Forwarded-Host: quuz |X-Real-IP: waldo |Credential: *MASKED* - |Signature: *MASKED*""".stripMargin() + |Signature: *MASKED*""".stripMargin()) cleanup: - ctx.close() appender.stop() + ctx.close() + } + + @Requires(property = "spec.name", value = "DefaultClientHeaderMaskTest") + @Controller("/masking") + @Singleton + static class MaskedController { + @Get + String get() { + "ok" + } } static class MemoryAppender extends AppenderBase { diff --git a/http/build.gradle b/http/build.gradle index 6e41cd391d2..0d33ef4f112 100644 --- a/http/build.gradle +++ b/http/build.gradle @@ -18,6 +18,7 @@ dependencies { testAnnotationProcessor project(":inject-java") testImplementation project(":inject") testImplementation project(":runtime") + testImplementation(libs.managed.logback) } tasks.named("compileKotlin") { diff --git a/http/src/main/java/io/micronaut/http/util/HttpHeadersUtil.java b/http/src/main/java/io/micronaut/http/util/HttpHeadersUtil.java new file mode 100644 index 00000000000..efcbc4213b2 --- /dev/null +++ b/http/src/main/java/io/micronaut/http/util/HttpHeadersUtil.java @@ -0,0 +1,95 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.http.util; + +import io.micronaut.core.annotation.NonNull; +import io.micronaut.core.annotation.Nullable; +import io.micronaut.core.util.SupplierUtil; +import io.micronaut.http.HttpHeaders; +import org.slf4j.Logger; + +import java.util.List; +import java.util.Set; +import java.util.function.Function; +import java.util.function.Supplier; +import java.util.regex.Pattern; + +/** + * Utility class to work with {@link io.micronaut.http.HttpHeaders} or HTTP Headers. + * @author Sergio del Amo + * @since 3.8.0 + */ +public final class HttpHeadersUtil { + private static final Supplier HEADER_MASK_PATTERNS = SupplierUtil.memoized(() -> + Pattern.compile(".*(password|cred|cert|key|secret|token|auth|signat).*", Pattern.CASE_INSENSITIVE) + ); + + private HttpHeadersUtil() { + + } + + /** + * Trace HTTP Headers. + * @param log Logger + * @param httpHeaders HTTP Headers + */ + public static void trace(@NonNull Logger log, + @NonNull HttpHeaders httpHeaders) { + trace(log, httpHeaders.names(), httpHeaders::getAll); + } + + /** + * Trace HTTP Headers. + * @param log Logger + * @param names HTTP Header names + * @param getAllHeaders Function to get all the header values for a particular header name + */ + public static void trace(@NonNull Logger log, + @NonNull Set names, + @NonNull Function> getAllHeaders) { + names.forEach(name -> trace(log, name, getAllHeaders)); + } + + /** + * Trace HTTP Headers. + * @param log Logger + * @param name HTTP Header name + * @param getAllHeaders Function to get all the header values for a particular header name + */ + public static void trace(@NonNull Logger log, + @NonNull String name, + @NonNull Function> getAllHeaders) { + boolean isMasked = HEADER_MASK_PATTERNS.get().matcher(name).matches(); + List all = getAllHeaders.apply(name); + if (all.size() > 1) { + for (String value : all) { + String maskedValue = isMasked ? mask(value) : value; + log.trace("{}: {}", name, maskedValue); + } + } else if (!all.isEmpty()) { + String maskedValue = isMasked ? mask(all.get(0)) : all.get(0); + log.trace("{}: {}", name, maskedValue); + } + } + + @Nullable + private static String mask(@Nullable String value) { + if (value == null) { + return null; + } + return "*MASKED*"; + } +} diff --git a/http/src/test/groovy/io/micronaut/http/util/HttpHeadersUtilSpec.groovy b/http/src/test/groovy/io/micronaut/http/util/HttpHeadersUtilSpec.groovy new file mode 100644 index 00000000000..54098639c29 --- /dev/null +++ b/http/src/test/groovy/io/micronaut/http/util/HttpHeadersUtilSpec.groovy @@ -0,0 +1,78 @@ +package io.micronaut.http.util + +import ch.qos.logback.classic.Level +import ch.qos.logback.classic.spi.ILoggingEvent +import ch.qos.logback.core.AppenderBase +import io.micronaut.http.HttpHeaders +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import spock.lang.Specification + +import java.util.concurrent.BlockingQueue +import java.util.concurrent.LinkedBlockingQueue + +class HttpHeadersUtilSpec extends Specification { + def "check masking works for #value"() { + expect: + expected == HttpHeadersUtil.mask(value) + + where: + value | expected + null | null + "foo" | "*MASKED*" + "Tim Yates" | "*MASKED*" + } + + def "check mask detects common security headers"() { + given: + MemoryAppender appender = new MemoryAppender() + Logger log = LoggerFactory.getLogger(HttpHeadersUtilSpec.class) + + expect: + log instanceof ch.qos.logback.classic.Logger + + when: + ch.qos.logback.classic.Logger logger = (ch.qos.logback.classic.Logger) log + logger.addAppender(appender) + logger.setLevel(Level.TRACE) + appender.start() + + HttpHeaders headers = new MockHttpHeaders([ + "Authorization": ["Bearer foo"], + "Proxy-Authorization": ["AWS4-HMAC-SHA256 bar"], + "Cookie": ["baz"], + "Set-Cookie": ["qux"], + "X-Forwarded-For": ["quux", "fred"], + "X-Forwarded-Host": ["quuz"], + "X-Real-IP": ["waldo"], + "Credential": ["foo"], + "Signature": ["bar probably secret"]]) + + HttpHeadersUtil.trace(log, headers) + + then: + appender.events.size() == headers.values().collect { it -> it.size() }.sum() + appender.events.contains("Authorization: *MASKED*") + appender.events.contains("Cookie: baz") + appender.events.contains("Credential: *MASKED*") + appender.events.contains("Set-Cookie: qux") + appender.events.contains("Proxy-Authorization: *MASKED*") + appender.events.contains("Signature: *MASKED*") + appender.events.contains("X-Forwarded-For: quux") + appender.events.contains("X-Forwarded-For: fred") + appender.events.contains("X-Forwarded-Host: quuz") + appender.events.contains("X-Real-IP: waldo") + + cleanup: + appender.stop() + } + + static class MemoryAppender extends AppenderBase { + final BlockingQueue events = new LinkedBlockingQueue<>() + + @Override + protected void append(ILoggingEvent e) { + events.add(e.formattedMessage) + } + } +} diff --git a/http/src/test/groovy/io/micronaut/http/util/MockHttpHeaders.java b/http/src/test/groovy/io/micronaut/http/util/MockHttpHeaders.java new file mode 100644 index 00000000000..29177e632f0 --- /dev/null +++ b/http/src/test/groovy/io/micronaut/http/util/MockHttpHeaders.java @@ -0,0 +1,93 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.http.util; + +import io.micronaut.core.annotation.Nullable; +import io.micronaut.core.convert.ArgumentConversionContext; +import io.micronaut.core.convert.ConversionService; +import io.micronaut.http.MutableHttpHeaders; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; + +public class MockHttpHeaders implements MutableHttpHeaders { + + private final Map> headers; + + public MockHttpHeaders(Map> headers) { + this.headers = headers; + } + + @Override + public MutableHttpHeaders add(CharSequence header, CharSequence value) { + headers.compute(header, (key, val) -> { + if (val == null) { + val = new ArrayList<>(); + } + val.add(value.toString()); + return val; + }); + return this; + } + + @Override + public MutableHttpHeaders remove(CharSequence header) { + headers.remove(header); + return this; + } + + @Override + public List getAll(CharSequence name) { + List values = headers.get(name); + if (values == null) { + return Collections.emptyList(); + } else { + return values; + } + } + + @Nullable + @Override + public String get(CharSequence name) { + List values = headers.get(name); + if (values == null || values.isEmpty()) { + return null; + } else { + return values.get(0); + } + } + + @Override + public Set names() { + return headers.keySet().stream().map(CharSequence::toString).collect(Collectors.toSet()); + } + + @Override + public Collection> values() { + return headers.values(); + } + + @Override + public Optional get(CharSequence name, ArgumentConversionContext conversionContext) { + return ConversionService.SHARED.convert(get(name), conversionContext); + } +} From 50dc946e4e4f76ca54a278dde63281e47b31a160 Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Fri, 9 Dec 2022 15:23:11 +0100 Subject: [PATCH 294/743] test: refactor CorsFilterSpec as a black box (#8473) This refactors the CORS filter tests to verify its behavior from the outside, and it does not rely on calling the internal methods. --- .../server/netty/cors/CorsFilterSpec.groovy | 534 +++++++++++------- .../http/server/cors/CorsFilter.java | 5 +- 2 files changed, 343 insertions(+), 196 deletions(-) diff --git a/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/cors/CorsFilterSpec.groovy b/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/cors/CorsFilterSpec.groovy index 72f5e124c25..7fad9272fa0 100644 --- a/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/cors/CorsFilterSpec.groovy +++ b/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/cors/CorsFilterSpec.groovy @@ -16,19 +16,28 @@ package io.micronaut.http.server.netty.cors import io.micronaut.context.ApplicationContext +import io.micronaut.core.annotation.Nullable +import io.micronaut.core.async.publisher.Publishers +import io.micronaut.core.util.StringUtils import io.micronaut.http.* import io.micronaut.http.annotation.Controller import io.micronaut.http.annotation.Get +import io.micronaut.http.filter.ServerFilterChain import io.micronaut.http.server.HttpServerConfiguration import io.micronaut.http.server.cors.CorsFilter import io.micronaut.http.server.cors.CorsOriginConfiguration +import io.micronaut.http.server.util.HttpHostResolver import io.micronaut.runtime.server.EmbeddedServer import io.micronaut.web.router.RouteMatch import io.micronaut.web.router.Router +import io.micronaut.web.router.UriRouteMatch import org.apache.http.client.utils.URIBuilder +import org.reactivestreams.Publisher +import reactor.core.publisher.Mono import spock.lang.AutoCleanup import spock.lang.Shared import spock.lang.Specification +import spock.lang.Unroll import java.util.stream.Collectors @@ -36,70 +45,82 @@ import static io.micronaut.http.HttpHeaders.* class CorsFilterSpec extends Specification { - @Shared @AutoCleanup + @Shared + @AutoCleanup EmbeddedServer embeddedServer = ApplicationContext.run(EmbeddedServer) - CorsFilter buildCorsHandler(HttpServerConfiguration.CorsConfiguration config) { - new CorsFilter(config ?: new HttpServerConfiguration.CorsConfiguration()) - } - - void "test handleRequest for non CORS request"() { + void "non CORS request is passed through"() { given: - def config = new HttpServerConfiguration.CorsConfiguration() - HttpRequest request = Mock(HttpRequest) - HttpHeaders headers = Mock(HttpHeaders) - request.getHeaders() >> headers - headers.getOrigin() >> Optional.empty() + HttpServerConfiguration.CorsConfiguration config = enabledCorsConfiguration() CorsFilter corsHandler = buildCorsHandler(config) + HttpRequest request = createRequest(null as String) when: - def result = corsHandler.handleRequest(request) + Optional> result = Mono.from(corsHandler.doFilter(request, okChain())).blockOptional() then: "the request is passed through" - !result.isPresent() + result.isPresent() + + when: + MutableHttpResponse response = result.get() + + then: + HttpStatus.OK == response.status() + response.headers.names().isEmpty() } - void "test handleRequest with no matching configuration"() { + void "request with origin and no matching configuration"() { given: - HttpRequest request = Mock(HttpRequest) - HttpHeaders headers = Mock(HttpHeaders) - request.getHeaders() >> headers - - def config = new HttpServerConfiguration.CorsConfiguration() + String origin = 'http://www.bar.com' + HttpRequest request = createRequest(origin) CorsOriginConfiguration originConfig = new CorsOriginConfiguration() originConfig.allowedOrigins = ['http://www.foo.com'] - config.configurations = new LinkedHashMap() - config.configurations.put('foo', originConfig) + HttpServerConfiguration.CorsConfiguration config = enabledCorsConfiguration([foo: originConfig]) CorsFilter corsHandler = buildCorsHandler(config) when: - def result = corsHandler.handleRequest(request) + Optional> result = Mono.from(corsHandler.doFilter(request, okChain())).blockOptional() + + then: + result.isPresent() + + when: + MutableHttpResponse response = result.get() then: "the request is passed through because no configuration matches the origin" - 2 * headers.getOrigin() >> Optional.of('http://www.bar.com') - !result.isPresent() + HttpStatus.OK == response.status() + response.headers.names().isEmpty() } - void "test handleRequest with regex matching configuration"() { + @Unroll + void "regex matching configuration"(List regex, String origin) { given: - HttpRequest request = Mock(HttpRequest) - HttpHeaders headers = Mock(HttpHeaders) - request.getHeaders() >> headers + HttpRequest request = createRequest(origin) request.getAttribute(HttpAttributes.ROUTE_MATCH, RouteMatch.class) >> Optional.empty() - def config = new HttpServerConfiguration.CorsConfiguration() CorsOriginConfiguration originConfig = new CorsOriginConfiguration() originConfig.allowedOrigins = regex - config.configurations = new LinkedHashMap() - config.configurations.put('foo', originConfig) + HttpServerConfiguration.CorsConfiguration config = enabledCorsConfiguration([foo: originConfig]) CorsFilter corsHandler = buildCorsHandler(config) when: - def result = corsHandler.handleRequest(request) + Optional> result = Mono.from(corsHandler.doFilter(request, okChain())).blockOptional() - then: "the request is passed through because no configuration matches the origin" - 2 * headers.getOrigin() >> Optional.of(origin) - !result.isPresent() + then: + result.isPresent() + + when: + MutableHttpResponse response = result.get() + + then: + HttpStatus.OK == response.status() + response.headers.names().size() == 3 + response.headers.find { it.key == 'Access-Control-Allow-Origin' } + response.headers.find { it.key == 'Vary' } + response.headers.find { it.key == 'Access-Control-Allow-Credentials' } + response.headers.find { it.key == 'Access-Control-Allow-Origin' }.value == [origin] + response.headers.find { it.key == 'Vary' }.value == ['Origin'] + response.headers.find { it.key == 'Access-Control-Allow-Credentials' }.value == [StringUtils.TRUE] where: regex | origin @@ -112,198 +133,251 @@ class CorsFilterSpec extends Specification { void "test handleRequest with disallowed method"() { given: - HttpRequest request = Mock(HttpRequest) - HttpHeaders headers = Mock(HttpHeaders) - request.getHeaders() >> headers + String origin = 'http://www.foo.com' + HttpRequest request = createRequest(origin) - def config = new HttpServerConfiguration.CorsConfiguration() CorsOriginConfiguration originConfig = new CorsOriginConfiguration() originConfig.allowedOrigins = ['http://www.foo.com'] originConfig.allowedMethods = [HttpMethod.GET] - config.configurations = new LinkedHashMap() - config.configurations.put('foo', originConfig) + HttpServerConfiguration.CorsConfiguration config = enabledCorsConfiguration([foo: originConfig]) + CorsFilter corsHandler = buildCorsHandler(config) when: - def result = corsHandler.handleRequest(request) + Optional> result = Mono.from(corsHandler.doFilter(request, okChain())).blockOptional() - then: "the request is rejected because the method is not in the list of allowedMethods" - 2 * headers.getOrigin() >> Optional.of('http://www.foo.com') - 1 * request.getMethod() >> HttpMethod.POST + then: result.isPresent() - result.get().status == HttpStatus.FORBIDDEN + + when: + MutableHttpResponse response = result.get() + + then: + HttpStatus.FORBIDDEN == response.status() + response.headers.names().isEmpty() } - void "test handleRequest with disallowed header (not preflight)"() { + void "with disallowed header (not preflight) the request is passed through because allowed headers are only checked for preflight requests"() { given: - HttpRequest request = Mock(HttpRequest) - HttpHeaders headers = Mock(HttpHeaders) - request.getHeaders() >> headers + String origin = 'http://www.foo.com' + HttpRequest request = createRequest(origin) + request.getMethod() >> HttpMethod.GET - def config = new HttpServerConfiguration.CorsConfiguration() CorsOriginConfiguration originConfig = new CorsOriginConfiguration() originConfig.allowedOrigins = ['http://www.foo.com'] originConfig.allowedMethods = [HttpMethod.GET] - config.configurations = new LinkedHashMap() - config.configurations.put('foo', originConfig) + HttpServerConfiguration.CorsConfiguration config = enabledCorsConfiguration([foo: originConfig]) CorsFilter corsHandler = buildCorsHandler(config) when: - def result = corsHandler.handleRequest(request) + Optional> result = Mono.from(corsHandler.doFilter(request, okChain())).blockOptional() - then: "the request is passed through because allowed headers are only checked for preflight requests" - 2 * headers.getOrigin() >> Optional.of('http://www.foo.com') - 1 * request.getMethod() >> HttpMethod.GET - !result.isPresent() - 0 * headers.get(ACCESS_CONTROL_REQUEST_HEADERS, _) + then: + result.isPresent() + + when: + MutableHttpResponse response = result.get() + + then: + HttpStatus.OK == response.status() + response.headers.names().size() == 3 + response.headers.find { it.key == 'Access-Control-Allow-Origin' } + response.headers.find { it.key == 'Vary' } + response.headers.find { it.key == 'Access-Control-Allow-Credentials' } + response.headers.find { it.key == 'Access-Control-Allow-Origin' }.value == ['http://www.foo.com'] + response.headers.find { it.key == 'Vary' }.value == ['Origin'] + response.headers.find { it.key == 'Access-Control-Allow-Credentials' }.value == [StringUtils.TRUE] } void "test preflight handleRequest with disallowed header"() { given: - HttpRequest request = Mock(HttpRequest) - HttpHeaders headers = Mock(HttpHeaders) - request.getHeaders() >> headers - def config = new HttpServerConfiguration.CorsConfiguration() + String origin = 'http://www.foo.com' + HttpHeaders headers = Stub(HttpHeaders) { + getOrigin() >> Optional.of(origin) + getFirst(ACCESS_CONTROL_REQUEST_METHOD, _) >> Optional.of(HttpMethod.GET) + get(ACCESS_CONTROL_REQUEST_HEADERS, _) >> Optional.of(['foo', 'bar']) + contains(ACCESS_CONTROL_REQUEST_METHOD) >> true + } + HttpRequest request = createRequest(headers) + request.getMethod() >> HttpMethod.OPTIONS + request.getUri() >> new URIBuilder( '/example' ).build() + List> routes = embeddedServer.getApplicationContext().getBean(Router). + findAny(request.getUri().toString(), request) + .collect(Collectors.toList()) + + request.getAttribute(HttpAttributes.AVAILABLE_HTTP_METHODS, _) >> Optional.of(routes.stream().map(route->route.getHttpMethod()).collect(Collectors.toList())) + CorsOriginConfiguration originConfig = new CorsOriginConfiguration() originConfig.allowedOrigins = ['http://www.foo.com'] originConfig.allowedMethods = [HttpMethod.GET] originConfig.allowedHeaders = ['foo'] - config.configurations = new LinkedHashMap() - config.configurations.put('foo', originConfig) + + HttpServerConfiguration.CorsConfiguration config = enabledCorsConfiguration([foo: originConfig]) + CorsFilter corsHandler = buildCorsHandler(config) - request.getMethod() >> HttpMethod.OPTIONS - def uri = new URIBuilder( '/example' ) - request.getUri() >> uri.build() - def routes = embeddedServer.getApplicationContext().getBean(Router). - findAny(request.getUri().toString(), request) - .collect(Collectors.toList()) - request.getAttribute(HttpAttributes.AVAILABLE_HTTP_METHODS, _) >> Optional.of(routes.stream().map(route->route.getHttpMethod()).collect(Collectors.toList())) + when: + Optional> result = Mono.from(corsHandler.doFilter(request, okChain())).blockOptional() + + then: + result.isPresent() when: - headers.contains(ACCESS_CONTROL_REQUEST_METHOD) >> true - def result = corsHandler.handleRequest(request) + MutableHttpResponse response = result.get() then: "the request is rejected because bar is not allowed" - 2 * headers.getOrigin() >> Optional.of('http://www.foo.com') - 1 * headers.getFirst(ACCESS_CONTROL_REQUEST_METHOD, _) >> Optional.of(HttpMethod.GET) - 1 * headers.get(ACCESS_CONTROL_REQUEST_HEADERS, _) >> Optional.of(['foo', 'bar']) - result.get().status == HttpStatus.FORBIDDEN + HttpStatus.FORBIDDEN == response.status() } - void "test preflight handleRequest with allowed header"() { + void "test preflight with allowed header"() { given: - HttpRequest request = Mock(HttpRequest) - HttpHeaders headers = Mock(HttpHeaders) - request.getHeaders() >> headers - def config = new HttpServerConfiguration.CorsConfiguration() + String origin = 'http://www.foo.com' + + HttpHeaders headers = Stub(HttpHeaders) { + getOrigin() >> Optional.of(origin) + getFirst(ACCESS_CONTROL_REQUEST_METHOD, _) >> Optional.of(HttpMethod.GET) + get(ACCESS_CONTROL_REQUEST_HEADERS, _) >> Optional.of(['foo']) + contains(ACCESS_CONTROL_REQUEST_METHOD) >> true + } + HttpRequest request = createRequest(headers) + request.getMethod() >> HttpMethod.OPTIONS + request.getUri() >> new URIBuilder( '/example' ).build() + List> routes = embeddedServer.getApplicationContext().getBean(Router). + findAny(request.getUri().toString(), request) + .collect(Collectors.toList()) + request.getAttribute(HttpAttributes.AVAILABLE_HTTP_METHODS, _) >> Optional.of(routes.stream().map(route->route.getHttpMethod()).collect(Collectors.toList())) + CorsOriginConfiguration originConfig = new CorsOriginConfiguration() originConfig.allowedOrigins = ['http://www.foo.com'] originConfig.allowedMethods = [HttpMethod.GET] originConfig.allowedHeaders = ['foo', 'bar'] - config.configurations = new LinkedHashMap() - config.configurations.put('foo', originConfig) + + HttpServerConfiguration.CorsConfiguration config = enabledCorsConfiguration([foo: originConfig]) + CorsFilter corsHandler = buildCorsHandler(config) - request.getMethod() >> HttpMethod.OPTIONS - def uri = new URIBuilder( '/example' ) - request.getUri() >> uri.build() - def routes = embeddedServer.getApplicationContext().getBean(Router). - findAny(request.getUri().toString(), request) - .collect(Collectors.toList()) - request.getAttribute(HttpAttributes.AVAILABLE_HTTP_METHODS, _) >> Optional.of(routes.stream().map(route->route.getHttpMethod()).collect(Collectors.toList())) when: - headers.contains(ACCESS_CONTROL_REQUEST_METHOD) >> true - def result = corsHandler.handleRequest(request) - - then: "the request is successful" - 4 * headers.getOrigin() >> Optional.of('http://www.foo.com') - 2 * headers.getFirst(ACCESS_CONTROL_REQUEST_METHOD, _) >> Optional.of(HttpMethod.GET) - 2 * headers.get(ACCESS_CONTROL_REQUEST_HEADERS, _) >> Optional.of(['foo']) - result.get().status == HttpStatus.OK + Optional> result = Mono.from(corsHandler.doFilter(request, okChain())).blockOptional() + + then: + result.isPresent() + + when: + MutableHttpResponse response = result.get() + + then: + HttpStatus.OK == response.status() + response.headers.names().size() == 6 + response.headers.find { it.key == 'Access-Control-Allow-Origin' } + response.headers.find { it.key == 'Vary' } + response.headers.find { it.key == 'Access-Control-Allow-Credentials' } + response.headers.find { it.key == 'Access-Control-Allow-Methods' } + response.headers.find { it.key == 'Access-Control-Allow-Headers' } + response.headers.find { it.key == 'Access-Control-Max-Age' } + response.headers.find { it.key == 'Access-Control-Allow-Origin' }.value == ['http://www.foo.com'] + response.headers.find { it.key == 'Vary' }.value == ['Origin'] + response.headers.find { it.key == 'Access-Control-Allow-Credentials' }.value == [StringUtils.TRUE] + response.headers.find { it.key == 'Access-Control-Allow-Methods' }.value == ['GET'] + response.headers.find { it.key == 'Access-Control-Allow-Headers' }.value == ['foo'] + response.headers.find { it.key == 'Access-Control-Max-Age' }.value == ['1800'] } void "test handleResponse when configuration not present"() { given: - def config = new HttpServerConfiguration.CorsConfiguration() + String origin = 'http://www.bar.com' + HttpServerConfiguration.CorsConfiguration config = new HttpServerConfiguration.CorsConfiguration() CorsOriginConfiguration originConfig = new CorsOriginConfiguration() originConfig.allowedOrigins = ['http://www.foo.com'] - config.configurations = new LinkedHashMap() - config.configurations.put('foo', originConfig) + config.setConfigurations([foo: originConfig]) CorsFilter corsHandler = buildCorsHandler(config) - HttpRequest request = Mock(HttpRequest) - HttpHeaders headers = Mock(HttpHeaders) - request.getHeaders() >> headers - + HttpHeaders headers = Stub(HttpHeaders) { + getOrigin() >> Optional.of(origin) + } + HttpRequest request = Stub(HttpRequest) { + getHeaders() >> headers + } when: - def result = corsHandler.handleRequest(request) + Optional> result = corsHandler.handleRequest(request) then: "the response is not modified" - 2 * headers.getOrigin() >> Optional.of('http://www.bar.com') notThrown(NullPointerException) !result.isPresent() } - void "test handleResponse for normal request"() { + void "verify behaviour for normal request"() { given: - def config = new HttpServerConfiguration.CorsConfiguration() + String origin = 'http://www.foo.com' + HttpHeaders headers = Stub(HttpHeaders) { + getOrigin() >> Optional.of(origin) + contains(ACCESS_CONTROL_REQUEST_METHOD) >> true + } + HttpRequest request = Stub(HttpRequest) { + getHeaders() >> headers + } + CorsOriginConfiguration originConfig = new CorsOriginConfiguration() originConfig.exposedHeaders = ['Foo-Header', 'Bar-Header'] - config.configurations = new LinkedHashMap() - config.configurations.put('foo', originConfig) + + HttpServerConfiguration.CorsConfiguration config = enabledCorsConfiguration([foo: originConfig]) CorsFilter corsHandler = buildCorsHandler(config) - HttpRequest request = Mock(HttpRequest) - HttpHeaders headers = Mock(HttpHeaders) - request.getHeaders() >> headers - headers.getOrigin() >> Optional.of('http://www.foo.com') when: - headers.contains(ACCESS_CONTROL_REQUEST_METHOD) >> true - def result = corsHandler.handleRequest(request) + Optional> result = Mono.from(corsHandler.doFilter(request, okChain())).blockOptional() then: - !result.isPresent() + result.isPresent() when: - MutableHttpResponse response = HttpResponse.ok() - corsHandler.handleResponse(request, response) + MutableHttpResponse response = result.get() - then: "the response is not modified" + then: + HttpStatus.OK == response.status() + response.headers.names().size() == 5 response.getHeaders().get(ACCESS_CONTROL_ALLOW_ORIGIN) == 'http://www.foo.com' // The origin is echo'd response.getHeaders().get(VARY) == 'Origin' // The vary header is set response.getHeaders().getAll(ACCESS_CONTROL_EXPOSE_HEADERS) == ['Foo-Header', 'Bar-Header' ]// Expose headers are set from config response.getHeaders().get(ACCESS_CONTROL_ALLOW_CREDENTIALS) == 'true' // Allow credentials header is set + response.getHeaders().get(ACCESS_CONTROL_MAX_AGE) == '1800' } void "test handleResponse for preflight request"() { given: - def config = new HttpServerConfiguration.CorsConfiguration() + HttpHeaders headers = Stub(HttpHeaders) { + contains(ACCESS_CONTROL_REQUEST_METHOD) >> true + get(ACCESS_CONTROL_REQUEST_HEADERS, _) >> Optional.of(['X-Header', 'Y-Header']) + getFirst(ACCESS_CONTROL_REQUEST_METHOD, _) >> Optional.of(HttpMethod.GET) + getOrigin() >> Optional.of('http://www.foo.com') + } + URI uri = new URIBuilder('/example').build() + HttpRequest request = Stub(HttpRequest) { + getHeaders() >> headers + getMethod() >> HttpMethod.OPTIONS + getUri() >> uri + } + List> routes = embeddedServer.getApplicationContext().getBean(Router). + findAny(uri.toString(), request) + .collect(Collectors.toList()) + request.getAttribute(HttpAttributes.AVAILABLE_HTTP_METHODS, _) >> Optional.of(routes.stream().map(route -> route.getHttpMethod()).collect(Collectors.toList())) + CorsOriginConfiguration originConfig = new CorsOriginConfiguration() originConfig.exposedHeaders = ['Foo-Header', 'Bar-Header'] - config.configurations = new LinkedHashMap() - config.configurations.put('foo', originConfig) + HttpServerConfiguration.CorsConfiguration config = enabledCorsConfiguration([foo: originConfig]) + CorsFilter corsHandler = buildCorsHandler(config) - HttpRequest request = Mock(HttpRequest) - HttpHeaders headers = Mock(HttpHeaders) - request.getHeaders() >> headers - headers.getOrigin() >> Optional.of('http://www.foo.com') - request.getMethod() >> HttpMethod.OPTIONS - def uri = new URIBuilder( '/example' ) - request.getUri() >> uri.build() - def routes = embeddedServer.getApplicationContext().getBean(Router). - findAny(request.getUri().toString(), request) - .collect(Collectors.toList()) - request.getAttribute(HttpAttributes.AVAILABLE_HTTP_METHODS, _) >> Optional.of(routes.stream().map(route->route.getHttpMethod()).collect(Collectors.toList())) + when: + Optional> result = Mono.from(corsHandler.doFilter(request, okChain())).blockOptional() + + then: + result.isPresent() when: - headers.contains(ACCESS_CONTROL_REQUEST_METHOD) >> true - HttpResponse response = corsHandler.handleRequest(request).get() + MutableHttpResponse response = result.get() - then: "the response is not modified" - 2 * headers.get(ACCESS_CONTROL_REQUEST_HEADERS, _) >> Optional.of(['X-Header', 'Y-Header']) - 2 * headers.getFirst(ACCESS_CONTROL_REQUEST_METHOD, _) >> Optional.of(HttpMethod.GET) + then: + HttpStatus.OK == response.status() + response.headers.names().size() == 7 response.getHeaders().get(ACCESS_CONTROL_ALLOW_METHODS) == 'GET' response.getHeaders().get(ACCESS_CONTROL_ALLOW_ORIGIN) == 'http://www.foo.com' // The origin is echo'd response.getHeaders().get(VARY) == 'Origin' // The vary header is set @@ -315,32 +389,44 @@ class CorsFilterSpec extends Specification { void "test handleResponse for preflight request with single header"() { given: - def config = new HttpServerConfiguration.CorsConfiguration(singleHeader: true) CorsOriginConfiguration originConfig = new CorsOriginConfiguration() originConfig.exposedHeaders = ['Foo-Header', 'Bar-Header'] - config.configurations = new LinkedHashMap() - config.configurations.put('foo', originConfig) + + HttpServerConfiguration.CorsConfiguration config = new HttpServerConfiguration.CorsConfiguration(singleHeader: true, enabled: true) + config.setConfigurations([foo: originConfig]) + CorsFilter corsHandler = buildCorsHandler(config) - HttpRequest request = Mock(HttpRequest) - HttpHeaders headers = Mock(HttpHeaders) - request.getHeaders() >> headers - headers.getOrigin() >> Optional.of('http://www.foo.com') - request.getMethod() >> HttpMethod.OPTIONS - def uri = new URIBuilder( '/example' ) - request.getUri() >> uri.build() - def routes = embeddedServer.getApplicationContext().getBean(Router). + + HttpHeaders headers = Stub(HttpHeaders) { + getOrigin() >> Optional.of('http://www.foo.com') + contains(ACCESS_CONTROL_REQUEST_METHOD) >> true + get(ACCESS_CONTROL_REQUEST_HEADERS, _) >> Optional.of(['X-Header', 'Y-Header']) + getFirst(ACCESS_CONTROL_REQUEST_METHOD, _) >> Optional.of(HttpMethod.GET) + } + URI uri = new URIBuilder( '/example' ).build() + HttpRequest request = Stub(HttpRequest) { + getHeaders() >> headers + getMethod() >> HttpMethod.OPTIONS + getUri() >> uri + } + List> routes = embeddedServer.getApplicationContext().getBean(Router). findAny(request.getUri().toString(), request) .collect(Collectors.toList()) - request.getAttribute(HttpAttributes.AVAILABLE_HTTP_METHODS, _) >> Optional.of(routes.stream().map(route->route.getHttpMethod()).collect(Collectors.toList())) when: - headers.contains(ACCESS_CONTROL_REQUEST_METHOD) >> true - HttpResponse response = corsHandler.handleRequest(request).get() + Optional> result = Mono.from(corsHandler.doFilter(request, okChain())).blockOptional() + + then: + result.isPresent() + + when: + MutableHttpResponse response = result.get() + + then: + HttpStatus.OK == response.status() then: "the response is not modified" - 2 * headers.get(ACCESS_CONTROL_REQUEST_HEADERS, _) >> Optional.of(['X-Header', 'Y-Header']) - 2 * headers.getFirst(ACCESS_CONTROL_REQUEST_METHOD, _) >> Optional.of(HttpMethod.GET) response.getHeaders().get(ACCESS_CONTROL_ALLOW_METHODS) == 'GET' response.getHeaders().get(ACCESS_CONTROL_ALLOW_ORIGIN) == 'http://www.foo.com' // The origin is echo'd response.getHeaders().get(VARY) == 'Origin' // The vary header is set @@ -352,63 +438,84 @@ class CorsFilterSpec extends Specification { void "test preflight handleRequest on route that doesn't exists"() { given: - HttpRequest request = Mock(HttpRequest) - HttpHeaders headers = Mock(HttpHeaders) - request.getHeaders() >> headers - def uri = new URIBuilder( '/doesnt-exists-route' ) - request.getUri() >> uri.build() - def config = new HttpServerConfiguration.CorsConfiguration() + String origin = 'http://www.foo.com' + HttpHeaders headers = Stub(HttpHeaders) { + getFirst(ACCESS_CONTROL_REQUEST_METHOD, _) >> Optional.of(HttpMethod.GET) + getOrigin() >> Optional.of(origin) + contains(ACCESS_CONTROL_REQUEST_METHOD) >> true + } + URI uri = new URIBuilder( '/doesnt-exists-route' ).build() + HttpRequest request = Stub(HttpRequest) { + getHeaders() >> headers + getUri() >> uri + getMethod() >> HttpMethod.OPTIONS + } + List> routes = embeddedServer.getApplicationContext().getBean(Router). + findAny(uri.toString(), request) + .collect(Collectors.toList()) + request.getAttribute(HttpAttributes.AVAILABLE_HTTP_METHODS, _) >> Optional.of(routes.stream().map(route->route.getHttpMethod()).collect(Collectors.toList())) + CorsOriginConfiguration originConfig = new CorsOriginConfiguration() originConfig.allowedOrigins = ['http://www.foo.com'] originConfig.allowedMethods = [HttpMethod.GET] originConfig.allowedHeaders = ['foo', 'bar'] - config.configurations = new LinkedHashMap() - config.configurations.put('foo', originConfig) + + HttpServerConfiguration.CorsConfiguration config = enabledCorsConfiguration([foo: originConfig]) + CorsFilter corsHandler = buildCorsHandler(config) - request.getMethod() >> HttpMethod.OPTIONS - def routes = embeddedServer.getApplicationContext().getBean(Router). - findAny(request.getUri().toString(), request) - .collect(Collectors.toList()) - request.getAttribute(HttpAttributes.AVAILABLE_HTTP_METHODS, _) >> Optional.of(routes.stream().map(route->route.getHttpMethod()).collect(Collectors.toList())) + when: + Optional> result = Mono.from(corsHandler.doFilter(request, okChain())).blockOptional() + + then: + result.isPresent() when: - headers.contains(ACCESS_CONTROL_REQUEST_METHOD) >> true - 1 * headers.getFirst(ACCESS_CONTROL_REQUEST_METHOD, _) >> Optional.of(HttpMethod.GET) - def result = corsHandler.handleRequest(request) + MutableHttpResponse response = result.get() - then: "the request is successful" - 2 * headers.getOrigin() >> Optional.of('http://www.foo.com') - !result.isPresent() + then: + HttpStatus.OK == response.status() } void "test preflight handleRequest on route that does exist but doesn't handle requested HTTP Method"() { given: - def config = new HttpServerConfiguration.CorsConfiguration() + CorsOriginConfiguration originConfig = new CorsOriginConfiguration() originConfig.exposedHeaders = ['Foo-Header', 'Bar-Header'] - config.configurations = new LinkedHashMap() - config.configurations.put('foo', originConfig) + + HttpServerConfiguration.CorsConfiguration config = enabledCorsConfiguration([foo: originConfig]) + CorsFilter corsHandler = buildCorsHandler(config) - HttpRequest request = Mock(HttpRequest) - HttpHeaders headers = Mock(HttpHeaders) - request.getHeaders() >> headers - headers.getOrigin() >> Optional.of('http://www.foo.com') - request.getMethod() >> HttpMethod.OPTIONS - def uri = new URIBuilder( '/example' ) - request.getUri() >> uri.build() - def routes = embeddedServer.getApplicationContext().getBean(Router). + + String origin = 'http://www.foo.com' + HttpHeaders headers = Stub(HttpHeaders) { + getOrigin() >> Optional.of(origin) + getFirst(ACCESS_CONTROL_REQUEST_METHOD, _) >> Optional.of(HttpMethod.POST) + contains(ACCESS_CONTROL_REQUEST_METHOD) >> true + } + URI uri = new URIBuilder( '/example' ).build() + HttpRequest request = Stub(HttpRequest) { + getHeaders() >> headers + getMethod() >> HttpMethod.OPTIONS + getUri() >> uri + } + + List> routes = embeddedServer.getApplicationContext().getBean(Router). findAny(request.getUri().toString(), request) .collect(Collectors.toList()) - request.getAttribute(HttpAttributes.AVAILABLE_HTTP_METHODS, _) >> Optional.of(routes.stream().map(route->route.getHttpMethod()).collect(Collectors.toList())) + when: - headers.contains(ACCESS_CONTROL_REQUEST_METHOD) >> true - def result = corsHandler.handleRequest(request) + Optional> result = Mono.from(corsHandler.doFilter(request, okChain())).blockOptional() - then: "the request is successful" - 1 * headers.getFirst(ACCESS_CONTROL_REQUEST_METHOD, _) >> Optional.of(HttpMethod.POST) - !result.isPresent() + then: + result.isPresent() + + when: + MutableHttpResponse response = result.get() + + then: + HttpStatus.OK == response.status() } @Controller @@ -417,4 +524,43 @@ class CorsFilterSpec extends Specification { @Get("/example") String example() { return "Example"} } + + private HttpRequest createRequest(String originHeader) { + HttpHeaders headers = Stub(HttpHeaders) { + getOrigin() >> Optional.ofNullable(originHeader) + } + createRequest(headers) + } + + private HttpRequest createRequest(HttpHeaders headers) { + Stub(HttpRequest) { + getHeaders() >> headers + } + } + + private ServerFilterChain okChain() { + new ServerFilterChain() { + @Override + Publisher> proceed(HttpRequest req) { + Publishers.just(HttpResponse.ok()) + } + } + } + + private HttpServerConfiguration.CorsConfiguration enabledCorsConfiguration(Map corsConfigurationMap = null) { + HttpServerConfiguration.CorsConfiguration config = new HttpServerConfiguration.CorsConfiguration() { + @Override + boolean isEnabled() { + true + } + } + if (corsConfigurationMap != null) { + config.setConfigurations(corsConfigurationMap) + } + config + } + + private CorsFilter buildCorsHandler(HttpServerConfiguration.CorsConfiguration config) { + new CorsFilter(config ?: enabledCorsConfiguration()) + } } diff --git a/http-server/src/main/java/io/micronaut/http/server/cors/CorsFilter.java b/http-server/src/main/java/io/micronaut/http/server/cors/CorsFilter.java index 705b5c55fea..a7039180c88 100644 --- a/http-server/src/main/java/io/micronaut/http/server/cors/CorsFilter.java +++ b/http-server/src/main/java/io/micronaut/http/server/cors/CorsFilter.java @@ -104,8 +104,9 @@ protected void handleResponse(HttpRequest request, MutableHttpResponse res CorsOriginConfiguration config = optionalConfig.get(); if (CorsUtil.isPreflightRequest(request)) { - Optional result = headers.getFirst(ACCESS_CONTROL_REQUEST_METHOD, CONVERSION_CONTEXT_HTTP_METHOD); - setAllowMethods(result.get(), response); + headers.getFirst(ACCESS_CONTROL_REQUEST_METHOD, CONVERSION_CONTEXT_HTTP_METHOD) + .ifPresent(result -> setAllowMethods(result, response)); + Optional> allowedHeaders = headers.get(ACCESS_CONTROL_REQUEST_HEADERS, ConversionContext.LIST_OF_STRING); allowedHeaders.ifPresent(val -> setAllowHeaders(val, response) From 3ece71415e9f194327b5369d9e1ac9c629e6a11d Mon Sep 17 00:00:00 2001 From: Jonas Konrad Date: Sun, 11 Dec 2022 16:14:16 +0100 Subject: [PATCH 295/743] Update libs.versions.toml (#8478) --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 1a71dfe46c4..f91da45c6c7 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -53,7 +53,7 @@ managed-h2 = "1.4.200" managed-hystrix = "1.5.18" managed-jakarta-annotation-api = "2.1.1" managed-jackson = "2.14.0" -managed-jackson-databind = "2.14.0" +managed-jackson-databind = "2.14.1" managed-javax-annotation-api = "1.3.2" managed-jcache = "1.1.1" managed-jna = "5.12.1" From aa22661f435c37a5eadc6fe9464e4c383d1cb580 Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Mon, 12 Dec 2022 10:15:28 +0100 Subject: [PATCH 296/743] refactor: CorsFilter (#8474) --- .../netty/cors/CorsFilterEnabledSpec.groovy | 19 ++ .../server/netty/cors/CorsFilterSpec.groovy | 2 - .../CorsOriginConverterEnabledSpec.groovy | 19 ++ .../http/server/cors/CorsFilter.java | 262 ++++++++++-------- 4 files changed, 186 insertions(+), 116 deletions(-) create mode 100644 http-server-netty/src/test/groovy/io/micronaut/http/server/netty/cors/CorsFilterEnabledSpec.groovy create mode 100644 http-server-netty/src/test/groovy/io/micronaut/http/server/netty/cors/CorsOriginConverterEnabledSpec.groovy diff --git a/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/cors/CorsFilterEnabledSpec.groovy b/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/cors/CorsFilterEnabledSpec.groovy new file mode 100644 index 00000000000..3a94796f892 --- /dev/null +++ b/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/cors/CorsFilterEnabledSpec.groovy @@ -0,0 +1,19 @@ +package io.micronaut.http.server.netty.cors + +import io.micronaut.context.ApplicationContext +import io.micronaut.http.server.cors.CorsFilter +import spock.lang.AutoCleanup +import spock.lang.Shared +import spock.lang.Specification + +class CorsFilterEnabledSpec extends Specification { + + @AutoCleanup + @Shared + ApplicationContext applicationContext = ApplicationContext.run() + + void "CorsFilter is not enabled by default"() { + expect: + !applicationContext.containsBean(CorsFilter) + } +} diff --git a/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/cors/CorsFilterSpec.groovy b/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/cors/CorsFilterSpec.groovy index 7fad9272fa0..8553e89adb1 100644 --- a/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/cors/CorsFilterSpec.groovy +++ b/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/cors/CorsFilterSpec.groovy @@ -16,7 +16,6 @@ package io.micronaut.http.server.netty.cors import io.micronaut.context.ApplicationContext -import io.micronaut.core.annotation.Nullable import io.micronaut.core.async.publisher.Publishers import io.micronaut.core.util.StringUtils import io.micronaut.http.* @@ -26,7 +25,6 @@ import io.micronaut.http.filter.ServerFilterChain import io.micronaut.http.server.HttpServerConfiguration import io.micronaut.http.server.cors.CorsFilter import io.micronaut.http.server.cors.CorsOriginConfiguration -import io.micronaut.http.server.util.HttpHostResolver import io.micronaut.runtime.server.EmbeddedServer import io.micronaut.web.router.RouteMatch import io.micronaut.web.router.Router diff --git a/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/cors/CorsOriginConverterEnabledSpec.groovy b/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/cors/CorsOriginConverterEnabledSpec.groovy new file mode 100644 index 00000000000..2f98a0bfca4 --- /dev/null +++ b/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/cors/CorsOriginConverterEnabledSpec.groovy @@ -0,0 +1,19 @@ +package io.micronaut.http.server.netty.cors + +import io.micronaut.context.ApplicationContext +import io.micronaut.http.server.cors.CorsOriginConverter +import spock.lang.AutoCleanup +import spock.lang.Shared +import spock.lang.Specification + +class CorsOriginConverterEnabledSpec extends Specification { + + @AutoCleanup + @Shared + ApplicationContext applicationContext = ApplicationContext.run() + + void "CorsOriginConverter is not enabled by default"() { + expect: + !applicationContext.containsBean(CorsOriginConverter) + } +} diff --git a/http-server/src/main/java/io/micronaut/http/server/cors/CorsFilter.java b/http-server/src/main/java/io/micronaut/http/server/cors/CorsFilter.java index a7039180c88..ddc8d63a0e5 100644 --- a/http-server/src/main/java/io/micronaut/http/server/cors/CorsFilter.java +++ b/http-server/src/main/java/io/micronaut/http/server/cors/CorsFilter.java @@ -15,6 +15,8 @@ */ package io.micronaut.http.server.cors; +import io.micronaut.core.annotation.NonNull; +import io.micronaut.core.annotation.Nullable; import io.micronaut.core.async.publisher.Publishers; import io.micronaut.core.convert.ArgumentConversionContext; import io.micronaut.core.convert.ConversionContext; @@ -31,11 +33,13 @@ import io.micronaut.http.filter.ServerFilterChain; import io.micronaut.http.filter.ServerFilterPhase; import io.micronaut.http.server.HttpServerConfiguration; +import org.jetbrains.annotations.NotNull; import org.reactivestreams.Publisher; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.util.ArrayList; import java.util.List; -import java.util.Map; import java.util.Optional; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -43,6 +47,7 @@ import static io.micronaut.http.HttpAttributes.AVAILABLE_HTTP_METHODS; import static io.micronaut.http.HttpHeaders.*; +import static io.micronaut.http.annotation.Filter.MATCH_ALL_PATTERN; /** * Responsible for handling CORS requests and responses. @@ -51,9 +56,9 @@ * @author Graeme Rocher * @since 1.0 */ -@Filter("/**") +@Filter(MATCH_ALL_PATTERN) public class CorsFilter implements HttpServerFilter { - + private static final Logger LOG = LoggerFactory.getLogger(CorsFilter.class); private static final ArgumentConversionContext CONVERSION_CONTEXT_HTTP_METHOD = ImmutableArgumentConversionContext.of(HttpMethod.class); protected final HttpServerConfiguration.CorsConfiguration corsConfiguration; @@ -67,19 +72,23 @@ public CorsFilter(HttpServerConfiguration.CorsConfiguration corsConfiguration) { @Override public Publisher> doFilter(HttpRequest request, ServerFilterChain chain) { - boolean originHeaderPresent = request.getHeaders().getOrigin().isPresent(); - if (originHeaderPresent) { - MutableHttpResponse response = handleRequest(request).orElse(null); - if (response != null) { - return Publishers.just(response); - } else { - return Publishers.then(chain.proceed(request), mutableHttpResponse -> { - handleResponse(request, mutableHttpResponse); - }); - } - } else { + String origin = request.getHeaders().getOrigin().orElse(null); + if (origin == null) { + LOG.trace("Http Header " + HttpHeaders.ORIGIN + " not present. Proceeding with the request."); return chain.proceed(request); } + CorsOriginConfiguration corsOriginConfiguration = getConfiguration(origin).orElse(null); + if (corsOriginConfiguration != null) { + if (CorsUtil.isPreflightRequest(request)) { + return handlePreflightRequest(request, chain, corsOriginConfiguration); + } + if (!validateMethodToMatch(request, corsOriginConfiguration).isPresent()) { + return forbidden(); + } + return Publishers.then(chain.proceed(request), resp -> decorateResponseWithHeaders(request, resp, corsOriginConfiguration)); + } + LOG.trace("CORS configuration not found for {} origin", origin); + return chain.proceed(request); } @Override @@ -92,35 +101,10 @@ public int getOrder() { * * @param request The {@link HttpRequest} object * @param response The {@link MutableHttpResponse} object + * @deprecated not used */ + @Deprecated protected void handleResponse(HttpRequest request, MutableHttpResponse response) { - HttpHeaders headers = request.getHeaders(); - Optional originHeader = headers.getOrigin(); - originHeader.ifPresent(requestOrigin -> { - - Optional optionalConfig = getConfiguration(requestOrigin); - - if (optionalConfig.isPresent()) { - CorsOriginConfiguration config = optionalConfig.get(); - - if (CorsUtil.isPreflightRequest(request)) { - headers.getFirst(ACCESS_CONTROL_REQUEST_METHOD, CONVERSION_CONTEXT_HTTP_METHOD) - .ifPresent(result -> setAllowMethods(result, response)); - - Optional> allowedHeaders = headers.get(ACCESS_CONTROL_REQUEST_HEADERS, ConversionContext.LIST_OF_STRING); - allowedHeaders.ifPresent(val -> - setAllowHeaders(val, response) - ); - - setMaxAge(config.getMaxAge(), response); - } - - setOrigin(requestOrigin, response); - setVary(response); - setExposeHeaders(config.getExposedHeaders(), response); - setAllowCredentials(config, response); - } - }); } /** @@ -128,54 +112,21 @@ protected void handleResponse(HttpRequest request, MutableHttpResponse res * * @param request The {@link HttpRequest} object * @return An optional {@link MutableHttpResponse}. The request should proceed normally if empty + * @deprecated Not used any more. */ + @Deprecated protected Optional> handleRequest(HttpRequest request) { - HttpHeaders headers = request.getHeaders(); - Optional originHeader = headers.getOrigin(); - if (originHeader.isPresent()) { - - String requestOrigin = originHeader.get(); - boolean preflight = CorsUtil.isPreflightRequest(request); - - Optional optionalConfig = getConfiguration(requestOrigin); - - if (optionalConfig.isPresent()) { - CorsOriginConfiguration config = optionalConfig.get(); - - HttpMethod requestMethod = request.getMethod(); - - List allowedMethods = config.getAllowedMethods(); - HttpMethod methodToMatch = preflight ? headers.getFirst(ACCESS_CONTROL_REQUEST_METHOD, CONVERSION_CONTEXT_HTTP_METHOD).orElse(requestMethod) : requestMethod; - - if (!isAnyMethod(allowedMethods)) { - if (allowedMethods.stream().noneMatch(method -> method.equals(methodToMatch))) { - return Optional.of(HttpResponse.status(HttpStatus.FORBIDDEN)); - } - } - - Optional> availableHttpMethods = (Optional>) request.getAttribute(AVAILABLE_HTTP_METHODS, new ArrayList().getClass()); - - if (preflight && availableHttpMethods.isPresent() && availableHttpMethods.get().stream().anyMatch(method -> method.equals(methodToMatch))) { - Optional> accessControlHeaders = headers.get(ACCESS_CONTROL_REQUEST_HEADERS, ConversionContext.LIST_OF_STRING); - - List allowedHeaders = config.getAllowedHeaders(); - - if (!isAny(allowedHeaders) && accessControlHeaders.isPresent()) { - if (!accessControlHeaders.get().stream() - .allMatch(header -> allowedHeaders.stream() - .anyMatch(allowedHeader -> allowedHeader.equalsIgnoreCase(header.trim())))) { - return Optional.of(HttpResponse.status(HttpStatus.FORBIDDEN)); - } - } + return Optional.empty(); + } - MutableHttpResponse ok = HttpResponse.ok(); - handleResponse(request, ok); - return Optional.of(ok); - } - } + @NonNull + private Optional validateMethodToMatch(@NonNull HttpRequest request, + @NonNull CorsOriginConfiguration config) { + HttpMethod methodToMatch = methodToMatch(request); + if (!methodAllowed(config, methodToMatch)) { + return Optional.empty(); } - - return Optional.empty(); + return Optional.of(methodToMatch); } /** @@ -214,15 +165,17 @@ protected void setVary(MutableHttpResponse response) { * @param origin The origin * @param response The {@link MutableHttpResponse} object */ - protected void setOrigin(String origin, MutableHttpResponse response) { - response.header(ACCESS_CONTROL_ALLOW_ORIGIN, origin); + protected void setOrigin(@Nullable String origin, @NonNull MutableHttpResponse response) { + if (origin != null) { + response.header(ACCESS_CONTROL_ALLOW_ORIGIN, origin); + } } /** * @param method The {@link HttpMethod} object * @param response The {@link MutableHttpResponse} object */ - protected void setAllowMethods(HttpMethod method, MutableHttpResponse response) { + protected void setAllowMethods(HttpMethod method, MutableHttpResponse response) { response.header(ACCESS_CONTROL_ALLOW_METHODS, method); } @@ -230,7 +183,7 @@ protected void setAllowMethods(HttpMethod method, MutableHttpResponse response) * @param optionalAllowHeaders A list with optional allow headers * @param response The {@link MutableHttpResponse} object */ - protected void setAllowHeaders(List optionalAllowHeaders, MutableHttpResponse response) { + protected void setAllowHeaders(List optionalAllowHeaders, MutableHttpResponse response) { List allowHeaders = optionalAllowHeaders.stream().map(Object::toString).collect(Collectors.toList()); if (corsConfiguration.isSingleHeader()) { String headerValue = String.join(",", allowHeaders); @@ -249,38 +202,28 @@ protected void setAllowHeaders(List optionalAllowHeaders, MutableHttpResponse * @param maxAge The max age * @param response The {@link MutableHttpResponse} object */ - protected void setMaxAge(long maxAge, MutableHttpResponse response) { + protected void setMaxAge(long maxAge, MutableHttpResponse response) { if (maxAge > -1) { response.header(ACCESS_CONTROL_MAX_AGE, Long.toString(maxAge)); } } - private Optional getConfiguration(String requestOrigin) { - Map corsConfigurations = corsConfiguration.getConfigurations(); - for (Map.Entry config : corsConfigurations.entrySet()) { - List allowedOrigins = config.getValue().getAllowedOrigins(); - if (!allowedOrigins.isEmpty()) { - boolean matches = false; - if (isAny(allowedOrigins)) { - matches = true; - } - if (!matches) { - matches = allowedOrigins.stream().anyMatch(origin -> { - if (origin.equals(requestOrigin)) { - return true; - } - Pattern p = Pattern.compile(origin); - Matcher m = p.matcher(requestOrigin); - return m.matches(); - }); - } + @NonNull + private Optional getConfiguration(@NonNull String requestOrigin) { + return corsConfiguration.getConfigurations().values().stream() + .filter(config -> { + List allowedOrigins = config.getAllowedOrigins(); + return !allowedOrigins.isEmpty() && (isAny(allowedOrigins) || allowedOrigins.stream().anyMatch(origin -> matchesOrigin(origin, requestOrigin))); + }).findFirst(); + } - if (matches) { - return Optional.of(config.getValue()); - } - } + private boolean matchesOrigin(@NonNull String origin, @NonNull String requestOrigin) { + if (origin.equals(requestOrigin)) { + return true; } - return Optional.empty(); + Pattern p = Pattern.compile(origin); + Matcher m = p.matcher(requestOrigin); + return m.matches(); } private boolean isAny(List values) { @@ -290,4 +233,95 @@ private boolean isAny(List values) { private boolean isAnyMethod(List allowedMethods) { return allowedMethods == CorsOriginConfiguration.ANY_METHOD; } + + private boolean methodAllowed(@NonNull CorsOriginConfiguration config, + @NonNull HttpMethod methodToMatch) { + List allowedMethods = config.getAllowedMethods(); + return isAnyMethod(allowedMethods) || allowedMethods.stream().anyMatch(method -> method.equals(methodToMatch)); + } + + @NonNull + private HttpMethod methodToMatch(@NonNull HttpRequest request) { + HttpMethod requestMethod = request.getMethod(); + return CorsUtil.isPreflightRequest(request) ? request.getHeaders().getFirst(ACCESS_CONTROL_REQUEST_METHOD, CONVERSION_CONTEXT_HTTP_METHOD).orElse(requestMethod) : requestMethod; + } + + private boolean hasAllowedHeaders(@NonNull HttpRequest request, @NonNull CorsOriginConfiguration config) { + Optional> accessControlHeaders = request.getHeaders().get(ACCESS_CONTROL_REQUEST_HEADERS, ConversionContext.LIST_OF_STRING); + List allowedHeaders = config.getAllowedHeaders(); + return isAny(allowedHeaders) || ( + accessControlHeaders.isPresent() && + accessControlHeaders.get().stream().allMatch(header -> allowedHeaders.stream().anyMatch(allowedHeader -> allowedHeader.equalsIgnoreCase(header.trim()))) + ); + } + + @NotNull + private static Publisher> forbidden() { + return Publishers.just(HttpResponse.status(HttpStatus.FORBIDDEN)); + } + + @NonNull + private void decorateResponseWithHeadersForPreflightRequest(@NonNull HttpRequest request, + @NonNull MutableHttpResponse response, + @NonNull CorsOriginConfiguration config) { + HttpHeaders headers = request.getHeaders(); + headers.getFirst(ACCESS_CONTROL_REQUEST_METHOD, CONVERSION_CONTEXT_HTTP_METHOD) + .ifPresent(methods -> setAllowMethods(methods, response)); + headers.get(ACCESS_CONTROL_REQUEST_HEADERS, ConversionContext.LIST_OF_STRING) + .ifPresent(val -> setAllowHeaders(val, response)); + setMaxAge(config.getMaxAge(), response); + } + + @NonNull + private void decorateResponseWithHeaders(@NonNull HttpRequest request, + @NonNull MutableHttpResponse response, + @NonNull CorsOriginConfiguration config) { + HttpHeaders headers = request.getHeaders(); + setOrigin(headers.getOrigin().orElse(null), response); + setVary(response); + setExposeHeaders(config.getExposedHeaders(), response); + setAllowCredentials(config, response); + } + + @NonNull + private Publisher> handlePreflightRequest(@NonNull HttpRequest request, + @NonNull ServerFilterChain chain, + @NonNull CorsOriginConfiguration corsOriginConfiguration) { + Optional statusOptional = validatePreflightRequest(request, corsOriginConfiguration); + if (statusOptional.isPresent()) { + HttpStatus status = statusOptional.get(); + if (status.getCode() >= 400) { + return Publishers.just(HttpResponse.status(status)); + } + MutableHttpResponse resp = HttpResponse.status(status); + decorateResponseWithHeadersForPreflightRequest(request, resp, corsOriginConfiguration); + decorateResponseWithHeaders(request, resp, corsOriginConfiguration); + return Publishers.just(resp); + } + return Publishers.then(chain.proceed(request), resp -> { + decorateResponseWithHeadersForPreflightRequest(request, resp, corsOriginConfiguration); + decorateResponseWithHeaders(request, resp, corsOriginConfiguration); + }); + } + + @NonNull + private Optional validatePreflightRequest(@NonNull HttpRequest request, + @NonNull CorsOriginConfiguration config) { + Optional methodToMatchOptional = validateMethodToMatch(request, config); + if (!methodToMatchOptional.isPresent()) { + return Optional.of(HttpStatus.FORBIDDEN); + } + HttpMethod methodToMatch = methodToMatchOptional.get(); + + Optional> availableHttpMethods = (Optional>) request.getAttribute(AVAILABLE_HTTP_METHODS, new ArrayList().getClass()); + if (CorsUtil.isPreflightRequest(request) && + availableHttpMethods.isPresent() && + availableHttpMethods.get().stream().anyMatch(method -> method.equals(methodToMatch))) { + if (!hasAllowedHeaders(request, config)) { + return Optional.of(HttpStatus.FORBIDDEN); + } + return Optional.of(HttpStatus.OK); + } + return Optional.empty(); + } } From 5379d93d63c9383205fcc9b65e9af998d25057ba Mon Sep 17 00:00:00 2001 From: Jonas Konrad Date: Wed, 14 Dec 2022 15:38:55 +0100 Subject: [PATCH 297/743] Route processing refactor (#8463) This is a refactor of route execution, excluding filters (those will follow in #8422). The goals are: * Moving away from reactive APIs where possible. * Streamlining route execution so that it will be possible to have pre-route filters, and filters that read the request body. This initial patch factors most of the routing code out of RoutingInboundHandler (the response writing code remains), and moves away from reactive for HttpContentProcessor and the route parameter fulfillment code (now called BaseRouteCompleter and FormRouteCompleter). The goal of this PR is to get to a point where I am confident that it will be easy to delay filter execution until the body is available. Ideally the full request lifecycle, from before route resolution, through filter execution, to route execution, will be contained in the RouteRunner class. --- .../micronaut/http/client/HttpGetSpec.groovy | 9 +- ...AbstractBufferingHttpContentProcessor.java | 113 --- .../netty/AbstractHttpContentProcessor.java | 28 +- .../http/server/netty/BaseRouteCompleter.java | 107 +++ .../netty/DefaultHttpContentProcessor.java | 31 +- .../DefaultHttpContentProcessorResolver.java | 8 +- .../netty/FormDataHttpContentProcessor.java | 118 +-- .../http/server/netty/FormRouteCompleter.java | 285 +++++++ .../server/netty/HttpContentProcessor.java | 51 +- ...tpContentProcessorAsReactiveProcessor.java | 80 ++ .../netty/HttpContentProcessorResolver.java | 6 +- .../http/server/netty/HttpDataReference.java | 253 ------ .../http/server/netty/MicronautHttpData.java | 784 ++++++++++++++++++ .../http/server/netty/NettyHttpRequest.java | 4 +- .../server/netty/NettyRequestLifecycle.java | 303 +++++++ .../server/netty/RoutingInBoundHandler.java | 504 +---------- .../binders/CompletableFutureBodyBinder.java | 5 +- .../netty/binders/InputStreamBodyBinder.java | 7 +- .../netty/binders/PublisherBodyBinder.java | 8 +- .../netty/converters/NettyConvertersSpi.java | 6 +- .../netty/jackson/JsonContentProcessor.java | 110 +-- .../MultipartBodyArgumentBinder.java | 32 +- .../multipart/NettyCompletedFileUpload.java | 22 +- .../server/netty/multipart/NettyPartData.java | 17 +- .../multipart/NettyStreamingFileUpload.java | 30 +- .../NettyServerWebSocketUpgradeHandler.java | 73 +- .../server/netty/MicronautHttpDataSpec.groovy | 30 + .../netty/stack/InvocationStackSpec.groovy | 3 + .../http/server/RequestLifecycle.java | 437 ++++++++++ .../micronaut/http/server/RouteExecutor.java | 504 ++--------- .../binding/RequestArgumentSatisfier.java | 6 +- 31 files changed, 2372 insertions(+), 1602 deletions(-) delete mode 100644 http-server-netty/src/main/java/io/micronaut/http/server/netty/AbstractBufferingHttpContentProcessor.java create mode 100644 http-server-netty/src/main/java/io/micronaut/http/server/netty/BaseRouteCompleter.java create mode 100644 http-server-netty/src/main/java/io/micronaut/http/server/netty/FormRouteCompleter.java create mode 100644 http-server-netty/src/main/java/io/micronaut/http/server/netty/HttpContentProcessorAsReactiveProcessor.java delete mode 100644 http-server-netty/src/main/java/io/micronaut/http/server/netty/HttpDataReference.java create mode 100644 http-server-netty/src/main/java/io/micronaut/http/server/netty/MicronautHttpData.java create mode 100644 http-server-netty/src/main/java/io/micronaut/http/server/netty/NettyRequestLifecycle.java create mode 100644 http-server-netty/src/test/groovy/io/micronaut/http/server/netty/MicronautHttpDataSpec.groovy create mode 100644 http-server/src/main/java/io/micronaut/http/server/RequestLifecycle.java diff --git a/http-client/src/test/groovy/io/micronaut/http/client/HttpGetSpec.groovy b/http-client/src/test/groovy/io/micronaut/http/client/HttpGetSpec.groovy index b62947742be..37055b91f8c 100644 --- a/http-client/src/test/groovy/io/micronaut/http/client/HttpGetSpec.groovy +++ b/http-client/src/test/groovy/io/micronaut/http/client/HttpGetSpec.groovy @@ -15,13 +15,17 @@ */ package io.micronaut.http.client -import io.micronaut.core.async.annotation.SingleResult import io.micronaut.context.annotation.Property import io.micronaut.context.annotation.Requires +import io.micronaut.core.async.annotation.SingleResult import io.micronaut.core.async.publisher.Publishers import io.micronaut.core.convert.format.Format import io.micronaut.core.type.Argument -import io.micronaut.http.* +import io.micronaut.http.HttpRequest +import io.micronaut.http.HttpResponse +import io.micronaut.http.HttpStatus +import io.micronaut.http.MediaType +import io.micronaut.http.MutableHttpRequest import io.micronaut.http.annotation.Controller import io.micronaut.http.annotation.Get import io.micronaut.http.annotation.Header @@ -52,6 +56,7 @@ import java.util.function.Consumer */ @MicronautTest @Property(name = 'spec.name', value = 'HttpGetSpec') +@Property(name = 'micronaut.http.client.read-timeout', value = '30s') class HttpGetSpec extends Specification { @Inject diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/AbstractBufferingHttpContentProcessor.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/AbstractBufferingHttpContentProcessor.java deleted file mode 100644 index a6edc44fad9..00000000000 --- a/http-server-netty/src/main/java/io/micronaut/http/server/netty/AbstractBufferingHttpContentProcessor.java +++ /dev/null @@ -1,113 +0,0 @@ -/* - * Copyright 2017-2020 original authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.micronaut.http.server.netty; - -import io.micronaut.core.annotation.Internal; -import io.micronaut.core.async.processor.SingleThreadedBufferingProcessor; -import io.micronaut.http.exceptions.ContentLengthExceededException; -import io.micronaut.http.netty.stream.StreamedHttpMessage; -import io.micronaut.http.server.HttpServerConfiguration; -import io.netty.buffer.ByteBufHolder; -import io.netty.handler.codec.http.multipart.HttpData; -import org.reactivestreams.Subscriber; - -import java.util.concurrent.atomic.AtomicLong; - -/** - * Abtract implementation of the {@link HttpContentProcessor} interface that deals with limiting file upload sizes. - * - * @param The type - * @author Graeme Rocher - * @since 1.0 - */ -@Internal -public abstract class AbstractBufferingHttpContentProcessor extends SingleThreadedBufferingProcessor implements HttpContentProcessor { - - protected final NettyHttpRequest nettyHttpRequest; - protected final long advertisedLength; - protected final long requestMaxSize; - protected final AtomicLong receivedLength = new AtomicLong(); - protected final HttpServerConfiguration configuration; - private final long partMaxSize; - - /** - * @param nettyHttpRequest The {@link NettyHttpRequest} - * @param configuration The {@link HttpServerConfiguration} - */ - public AbstractBufferingHttpContentProcessor(NettyHttpRequest nettyHttpRequest, HttpServerConfiguration configuration) { - this.nettyHttpRequest = nettyHttpRequest; - this.advertisedLength = nettyHttpRequest.getContentLength(); - this.requestMaxSize = configuration.getMaxRequestSize(); - this.configuration = configuration; - this.partMaxSize = configuration.getMultipart().getMaxFileSize(); - } - - @Override - public void subscribe(Subscriber downstreamSubscriber) { - super.subscribe(downstreamSubscriber); - subscribeUpstream(); - } - - @Override - protected final void doOnNext(ByteBufHolder message) { - long receivedLength = this.receivedLength.addAndGet(resolveLength(message)); - - if ((advertisedLength != -1 && receivedLength > advertisedLength) || (receivedLength > requestMaxSize)) { - fireExceedsLength(receivedLength, advertisedLength == -1 ? requestMaxSize : advertisedLength); - } else { - onUpstreamMessage(message); - } - } - - /** - * @param message The message - * @return Whether the message has verified part size - */ - protected boolean verifyPartDefinedSize(ByteBufHolder message) { - long partLength = message instanceof HttpData ? ((HttpData) message).definedLength() : -1; - boolean validPart = partLength > partMaxSize; - if (validPart) { - fireExceedsLength(partLength, partMaxSize); - return false; - } - return true; - } - - /** - * @param receivedLength The received length - * @param expected The expected length - */ - protected void fireExceedsLength(long receivedLength, long expected) { - try { - onError(new ContentLengthExceededException(expected, receivedLength)); - } finally { - upstreamSubscription.cancel(); - } - } - - private long resolveLength(ByteBufHolder message) { - if (message instanceof HttpData) { - return ((HttpData) message).length(); - } else { - return message.content().readableBytes(); - } - } - - private void subscribeUpstream() { - StreamedHttpMessage message = (StreamedHttpMessage) nettyHttpRequest.getNativeRequest(); - message.subscribe(this); - } -} diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/AbstractHttpContentProcessor.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/AbstractHttpContentProcessor.java index 741d43e9809..3ea2840e82e 100644 --- a/http-server-netty/src/main/java/io/micronaut/http/server/netty/AbstractHttpContentProcessor.java +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/AbstractHttpContentProcessor.java @@ -16,25 +16,22 @@ package io.micronaut.http.server.netty; import io.micronaut.core.annotation.Internal; -import io.micronaut.core.async.processor.SingleSubscriberProcessor; import io.micronaut.http.exceptions.ContentLengthExceededException; -import io.micronaut.http.netty.stream.StreamedHttpMessage; import io.micronaut.http.server.HttpServerConfiguration; import io.netty.buffer.ByteBufHolder; import io.netty.util.ReferenceCountUtil; -import org.reactivestreams.Subscriber; +import java.util.Collection; import java.util.concurrent.atomic.AtomicLong; /** * Abstract implementation of the {@link HttpContentProcessor} interface that deals with limiting file upload sizes. * - * @param The type * @author Graeme Rocher * @since 1.0 */ @Internal -public abstract class AbstractHttpContentProcessor extends SingleSubscriberProcessor implements HttpContentProcessor { +public abstract class AbstractHttpContentProcessor implements HttpContentProcessor { protected final NettyHttpRequest nettyHttpRequest; protected final long advertisedLength; @@ -57,17 +54,12 @@ public AbstractHttpContentProcessor(NettyHttpRequest nettyHttpRequest, HttpSe * Called after verifying the data of the message. * * @param message The message + * @param out The collection to add any produced messages to */ - protected abstract void onData(ByteBufHolder message); + protected abstract void onData(ByteBufHolder message, Collection out) throws Throwable; @Override - protected final void doSubscribe(Subscriber subscriber) { - StreamedHttpMessage message = (StreamedHttpMessage) nettyHttpRequest.getNativeRequest(); - message.subscribe(this); - } - - @Override - protected final void doOnNext(ByteBufHolder message) { + public void add(ByteBufHolder message, Collection out) throws Throwable { long receivedLength = this.receivedLength.addAndGet(message.content().readableBytes()); ReferenceCountUtil.touch(message); @@ -76,7 +68,7 @@ protected final void doOnNext(ByteBufHolder message) { } else if (receivedLength > requestMaxSize) { fireExceedsLength(receivedLength, requestMaxSize, message); } else { - onData(message); + onData(message, out); } } @@ -86,11 +78,7 @@ protected final void doOnNext(ByteBufHolder message) { * @param message The message to release */ protected void fireExceedsLength(long receivedLength, long expected, ByteBufHolder message) { - try { - onError(new ContentLengthExceededException(expected, receivedLength)); - } finally { - ReferenceCountUtil.safeRelease(message); - parentSubscription.cancel(); - } + ReferenceCountUtil.safeRelease(message); + throw new ContentLengthExceededException(expected, receivedLength); } } diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/BaseRouteCompleter.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/BaseRouteCompleter.java new file mode 100644 index 00000000000..a9702c7e0fa --- /dev/null +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/BaseRouteCompleter.java @@ -0,0 +1,107 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.http.server.netty; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.Nullable; +import io.micronaut.core.io.buffer.ReferenceCounted; +import io.micronaut.http.server.netty.multipart.NettyCompletedFileUpload; +import io.micronaut.web.router.RouteMatch; +import io.netty.buffer.ByteBufHolder; +import io.netty.util.ReferenceCountUtil; + +/** + * This class consumes objects produced by a {@link HttpContentProcessor}. Normally it just adds + * the data to the {@link NettyHttpRequest}. For multipart data, there is additional logic in + * {@link FormRouteCompleter} that also dynamically binds parameters, though usually this is done + * by the {@link io.micronaut.http.server.binding.RequestArgumentSatisfier}. + * + * @since 4.0.0 + * @author Jonas Konrad + */ +@Internal +class BaseRouteCompleter { + final NettyHttpRequest request; + volatile boolean needsInput = true; + /** + * Optional runnable that may be called from other threads (i.e. downstream subscribers) to + * notify that {@link #needsInput} may have changed. + */ + @Nullable + volatile Runnable checkDemand; + RouteMatch routeMatch; + boolean execute = false; + + public BaseRouteCompleter(NettyHttpRequest request, RouteMatch routeMatch) { + this.request = request; + this.routeMatch = routeMatch; + } + + final void add(Object message) throws Throwable { + try { + if (request.destroyed) { + // we don't want this message anymore + ReferenceCountUtil.release(message); + return; + } + + if (message instanceof ByteBufHolder bbh) { + addHolder(bbh); + } else { + ((NettyHttpRequest) request).setBody(message); + needsInput = true; + } + + // now, a pseudo try-finally with addSuppressed. + } catch (Throwable t) { + try { + ReferenceCountUtil.release(message); + } catch (Throwable u) { + t.addSuppressed(u); + } + throw t; + } + + // the upstream processor gives us ownership of the message, so we need to release it. + ReferenceCountUtil.release(message); + } + + protected void addHolder(ByteBufHolder holder) { + request.addContent(holder); + needsInput = true; + } + + void completeSuccess() { + execute = true; + } + + void completeFailure(Throwable failure) { + if (!execute) { + // discard parameters that have already been bound + for (Object toDiscard : routeMatch.getVariableValues().values()) { + if (toDiscard instanceof ReferenceCounted rc) { + rc.release(); + } + if (toDiscard instanceof io.netty.util.ReferenceCounted rc) { + rc.release(); + } + if (toDiscard instanceof NettyCompletedFileUpload fu) { + fu.discard(); + } + } + } + } +} diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/DefaultHttpContentProcessor.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/DefaultHttpContentProcessor.java index d51cf530045..e5b47b63827 100644 --- a/http-server-netty/src/main/java/io/micronaut/http/server/netty/DefaultHttpContentProcessor.java +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/DefaultHttpContentProcessor.java @@ -16,8 +16,6 @@ package io.micronaut.http.server.netty; import io.micronaut.core.annotation.Internal; -import io.micronaut.core.async.processor.SingleThreadedBufferingProcessor; -import io.micronaut.core.async.subscriber.SingleThreadedBufferingSubscriber; import io.micronaut.http.exceptions.ContentLengthExceededException; import io.micronaut.http.netty.stream.StreamedHttpMessage; import io.micronaut.http.server.HttpServerConfiguration; @@ -26,8 +24,8 @@ import io.netty.handler.codec.http.HttpRequest; import io.netty.handler.codec.http.multipart.HttpData; import io.netty.util.ReferenceCountUtil; -import org.reactivestreams.Subscriber; +import java.util.Collection; import java.util.concurrent.atomic.AtomicLong; /** @@ -37,9 +35,9 @@ * @since 1.0 */ @Internal -public class DefaultHttpContentProcessor extends SingleThreadedBufferingProcessor implements HttpContentProcessor { +public class DefaultHttpContentProcessor implements HttpContentProcessor { - protected final NettyHttpRequest nettyHttpRequest; + protected final NettyHttpRequest nettyHttpRequest; protected final ChannelHandlerContext ctx; protected final HttpServerConfiguration configuration; protected final long advertisedLength; @@ -65,15 +63,7 @@ public DefaultHttpContentProcessor(NettyHttpRequest nettyHttpRequest, HttpSer } @Override - public final void subscribe(Subscriber downstreamSubscriber) { - super.subscribe(downstreamSubscriber); - //ensures the subscriber is present before subscribing to the message - StreamedHttpMessage message = (StreamedHttpMessage) nettyHttpRequest.getNativeRequest(); - message.subscribe(this); - } - - @Override - protected void onUpstreamMessage(ByteBufHolder message) { + public void add(ByteBufHolder message, Collection out) { long receivedLength = this.receivedLength.addAndGet(resolveLength(message)); if (advertisedLength > requestMaxSize) { @@ -81,7 +71,7 @@ protected void onUpstreamMessage(ByteBufHolder message) { } else if (receivedLength > requestMaxSize) { fireExceedsLength(receivedLength, requestMaxSize, message); } else { - publishVerifiedContent(message); + out.add(message); } } @@ -94,16 +84,7 @@ private long resolveLength(ByteBufHolder message) { } private void fireExceedsLength(long receivedLength, long expected, ByteBufHolder message) { - upstreamState = SingleThreadedBufferingSubscriber.BackPressureState.DONE; - upstreamSubscription.cancel(); - upstreamBuffer.clear(); - currentDownstreamSubscriber().ifPresent(subscriber -> - subscriber.onError(new ContentLengthExceededException(expected, receivedLength)) - ); ReferenceCountUtil.safeRelease(message); - } - - private void publishVerifiedContent(ByteBufHolder message) { - currentDownstreamSubscriber().ifPresent(subscriber -> subscriber.onNext(message)); + throw new ContentLengthExceededException(expected, receivedLength); } } diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/DefaultHttpContentProcessorResolver.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/DefaultHttpContentProcessorResolver.java index c14e026d83a..63ec94eb9a8 100644 --- a/http-server-netty/src/main/java/io/micronaut/http/server/netty/DefaultHttpContentProcessorResolver.java +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/DefaultHttpContentProcessorResolver.java @@ -71,7 +71,7 @@ class DefaultHttpContentProcessorResolver implements HttpContentProcessorResolve @Override @NonNull - public HttpContentProcessor resolve(@NonNull NettyHttpRequest request, @NonNull RouteMatch route) { + public HttpContentProcessor resolve(@NonNull NettyHttpRequest request, @NonNull RouteMatch route) { Argument bodyType = route.getBodyArgument() /* The getBodyArgument() method returns arguments for functions where it is @@ -102,7 +102,7 @@ The getBodyArgument() method returns arguments for functions where it is @Override @NonNull - public HttpContentProcessor resolve(@NonNull NettyHttpRequest request, @NonNull Argument bodyType) { + public HttpContentProcessor resolve(@NonNull NettyHttpRequest request, @NonNull Argument bodyType) { if (bodyType.getType() == HttpRequest.class) { bodyType = bodyType.getFirstTypeVariable().orElse(Argument.OBJECT_ARGUMENT); } @@ -112,11 +112,11 @@ public HttpContentProcessor resolve(@NonNull NettyHttpRequest request, @No @Override @NonNull - public HttpContentProcessor resolve(@NonNull NettyHttpRequest request) { + public HttpContentProcessor resolve(@NonNull NettyHttpRequest request) { return resolve(request, false); } - private HttpContentProcessor resolve(NettyHttpRequest request, boolean rawBodyType) { + private HttpContentProcessor resolve(NettyHttpRequest request, boolean rawBodyType) { Supplier defaultHttpContentProcessor = () -> new DefaultHttpContentProcessor(request, getServerConfiguration()); if (rawBodyType) { diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/FormDataHttpContentProcessor.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/FormDataHttpContentProcessor.java index af47964bd25..305a001bd08 100644 --- a/http-server-netty/src/main/java/io/micronaut/http/server/netty/FormDataHttpContentProcessor.java +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/FormDataHttpContentProcessor.java @@ -24,7 +24,6 @@ import io.netty.handler.codec.http.HttpContent; import io.netty.handler.codec.http.HttpRequest; import io.netty.handler.codec.http.multipart.Attribute; -import io.netty.handler.codec.http.multipart.DefaultHttpDataFactory; import io.netty.handler.codec.http.multipart.FileUpload; import io.netty.handler.codec.http.multipart.HttpData; import io.netty.handler.codec.http.multipart.HttpDataFactory; @@ -32,16 +31,10 @@ import io.netty.handler.codec.http.multipart.HttpPostStandardRequestDecoder; import io.netty.handler.codec.http.multipart.InterfaceHttpData; import io.netty.handler.codec.http.multipart.InterfaceHttpPostRequestDecoder; -import org.reactivestreams.Subscriber; -import org.reactivestreams.Subscription; -import reactor.core.publisher.Operators; -import reactor.util.context.Context; import java.io.IOException; import java.nio.charset.Charset; -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.atomic.AtomicLong; +import java.util.Collection; /** *

Decodes {@link MediaType#MULTIPART_FORM_DATA} in a non-blocking manner.

@@ -52,11 +45,10 @@ * @since 1.0 */ @Internal -public class FormDataHttpContentProcessor extends AbstractHttpContentProcessor { +public class FormDataHttpContentProcessor extends AbstractHttpContentProcessor { private final InterfaceHttpPostRequestDecoder decoder; private final boolean enabled; - private final AtomicLong extraMessages = new AtomicLong(0); private final long partMaxSize; /** @@ -64,7 +56,7 @@ public class FormDataHttpContentProcessor extends AbstractHttpContentProcessor subscriber) { - subscriber.onSubscribe(new Subscription() { - - @Override - public void request(long n) { - extraMessages.updateAndGet(p -> { - long newVal = p - n; - if (newVal < 0) { - subscription.request(n - p); - return 0; - } else { - return newVal; - } - }); - } - - @Override - public void cancel() { - subscription.cancel(); - pleaseDestroy = true; - destroyIfRequested(); - } - }); - } - - @Override - protected void onData(ByteBufHolder message) { + protected void onData(ByteBufHolder message, Collection out) { boolean skip; synchronized (this) { if (destroyed) { @@ -147,12 +105,7 @@ protected void onData(ByteBufHolder message) { return; } try { - Subscriber subscriber = getSubscriber(); - - if (message instanceof HttpContent) { - HttpContent httpContent = (HttpContent) message; - List messages = new ArrayList<>(1); - + if (message instanceof HttpContent httpContent) { try { InterfaceHttpPostRequestDecoder postRequestDecoder = this.decoder; postRequestDecoder.offer(httpContent); @@ -161,29 +114,30 @@ protected void onData(ByteBufHolder message) { InterfaceHttpData data = postRequestDecoder.next(); data.touch(); switch (data.getHttpDataType()) { - case Attribute: + case Attribute -> { Attribute attribute = (Attribute) data; // bodyListHttpData keeps a copy and releases it later - messages.add(attribute.retain()); + out.add(attribute.retain()); postRequestDecoder.removeHttpDataFromClean(attribute); - break; - case FileUpload: + } + case FileUpload -> { FileUpload fileUpload = (FileUpload) data; if (fileUpload.isCompleted()) { // bodyListHttpData keeps a copy and releases it later - messages.add(fileUpload.retain()); + out.add(fileUpload.retain()); postRequestDecoder.removeHttpDataFromClean(fileUpload); } - break; - default: - // no-op + } + default -> { + // ignore + } } } InterfaceHttpData currentPartialHttpData = postRequestDecoder.currentPartialHttpData(); if (currentPartialHttpData instanceof HttpData) { // can't give away ownership of this data yet, so retain it - messages.add(currentPartialHttpData.retain()); + out.add(currentPartialHttpData.retain()); } } catch (HttpPostRequestDecoder.EndOfDataDecoderException e) { @@ -192,30 +146,11 @@ protected void onData(ByteBufHolder message) { Throwable cause = e.getCause(); if (cause instanceof IOException && cause.getMessage().equals("Size exceed allowed maximum capacity")) { String partName = decoder.currentPartialHttpData().getName(); - try { - onError(new ContentLengthExceededException("The part named [" + partName + "] exceeds the maximum allowed content length [" + partMaxSize + "]")); - } finally { - parentSubscription.cancel(); - } + throw new ContentLengthExceededException("The part named [" + partName + "] exceeds the maximum allowed content length [" + partMaxSize + "]"); } else { - onError(e); + throw e; } - } catch (Throwable e) { - onError(e); } finally { - if (messages.isEmpty()) { - subscription.request(1); - } else { - extraMessages.updateAndGet(p -> p + messages.size() - 1); - messages.stream().map(HttpData.class::cast).forEach(data -> { - try { - subscriber.onNext(data); - } catch (Throwable e) { - subscriber.onError(Operators.onOperatorError(subscription, e, data, Context.empty())); - } - }); - } - httpContent.release(); } } else { @@ -228,13 +163,22 @@ protected void onData(ByteBufHolder message) { } @Override - protected void doAfterOnError(Throwable throwable) { - pleaseDestroy = true; - destroyIfRequested(); + public void add(ByteBufHolder message, Collection out) throws Throwable { + try { + super.add(message, out); + } catch (Throwable e) { + cancel(); + throw e; + } + } + + @Override + public void complete(Collection out) { + cancel(); } @Override - protected void doAfterComplete() { + public void cancel() { pleaseDestroy = true; destroyIfRequested(); } diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/FormRouteCompleter.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/FormRouteCompleter.java new file mode 100644 index 00000000000..eb69dd21d41 --- /dev/null +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/FormRouteCompleter.java @@ -0,0 +1,285 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.http.server.netty; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.async.publisher.Publishers; +import io.micronaut.core.convert.ConversionService; +import io.micronaut.core.reflect.ClassUtils; +import io.micronaut.core.type.Argument; +import io.micronaut.http.MediaType; +import io.micronaut.http.multipart.PartData; +import io.micronaut.http.multipart.StreamingFileUpload; +import io.micronaut.http.server.netty.multipart.NettyPartData; +import io.micronaut.http.server.netty.multipart.NettyStreamingFileUpload; +import io.micronaut.web.router.RouteMatch; +import io.netty.buffer.ByteBufHolder; +import io.netty.handler.codec.http.multipart.Attribute; +import io.netty.handler.codec.http.multipart.FileUpload; +import io.netty.handler.codec.http.multipart.HttpData; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Sinks; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.atomic.AtomicLong; +import java.util.function.Supplier; + +/** + * Extension of {@link BaseRouteCompleter} that handles incoming multipart data and binds + * parameters (e.g. {@link io.micronaut.http.annotation.Part}). + * + * @since 4.0.0 + * @author Jonas Konrad + */ +@Internal +final class FormRouteCompleter extends BaseRouteCompleter { + static final Argument ARGUMENT_PART_DATA = Argument.of(PartData.class); + private static final Logger LOG = LoggerFactory.getLogger(FormRouteCompleter.class); + + private final NettyStreamingFileUpload.Factory fileUploadFactory; + private final ConversionService conversionService; + private final boolean alwaysAddContent = request.isFormData(); + private final AtomicLong pressureRequested = new AtomicLong(); + private final Map> subjectsByDataName = new HashMap<>(); + private final Collection> downstreamSubscribers = new ArrayList<>(); + + FormRouteCompleter(NettyStreamingFileUpload.Factory fileUploadFactory, ConversionService conversionService, NettyHttpRequest request, RouteMatch routeMatch) { + super(request, routeMatch); + this.fileUploadFactory = fileUploadFactory; + this.conversionService = conversionService; + } + + private void request(long n) { + pressureRequested.getAndUpdate(old -> { + if ((old + n) < old) { + return Long.MAX_VALUE; + } else { + return old + n; + } + }); + needsInput = true; + Runnable checkDemand = this.checkDemand; + if (checkDemand != null) { + checkDemand.run(); + } + } + + private Flux withFlowControl(Flux flux, MicronautHttpData data) { + return flux + .doOnComplete(data::release) + .doOnRequest(this::request); + } + + @Override + protected void addHolder(ByteBufHolder holder) { + if (holder instanceof HttpData data) { + needsInput = pressureRequested.decrementAndGet() > 0; + addData((MicronautHttpData) data); + } else { + super.addHolder(holder); + } + } + + @Override + void completeSuccess() { + for (Sinks.Many subject : downstreamSubscribers) { + // subjects will ignore the onComplete if they're already done + subject.tryEmitComplete(); + } + super.completeSuccess(); + } + + @Override + void completeFailure(Throwable failure) { + super.completeFailure(failure); + for (Sinks.Many subject : downstreamSubscribers) { + subject.tryEmitError(failure); + } + } + + private void addData(MicronautHttpData data) { + if (LOG.isTraceEnabled()) { + LOG.trace("Received HTTP Data for request [{}]: {}", request, data); + } + + String name = data.getName(); + Optional> requiredInput = routeMatch.getRequiredInput(name); + + if (requiredInput.isEmpty()) { + request.addContent(data); + request(1); + return; + } + + Argument argument = requiredInput.get(); + Supplier value; + boolean isPublisher = Publishers.isConvertibleToPublisher(argument.getType()); + boolean chunkedProcessing = false; + + if (isPublisher) { + if (data.attachment == null) { + data.attachment = new HttpDataAttachment(); + // retain exactly once + data.retain(); + } + + Argument typeVariable; + + if (StreamingFileUpload.class.isAssignableFrom(argument.getType())) { + typeVariable = ARGUMENT_PART_DATA; + } else { + typeVariable = argument.getFirstTypeVariable().orElse(Argument.OBJECT_ARGUMENT); + } + Class typeVariableType = typeVariable.getType(); + + Sinks.Many namedSubject = subjectsByDataName.computeIfAbsent(name, key -> makeDownstreamUnicastProcessor()); + + chunkedProcessing = PartData.class.equals(typeVariableType) || + Publishers.isConvertibleToPublisher(typeVariableType) || + ClassUtils.isJavaLangType(typeVariableType); + + if (Publishers.isConvertibleToPublisher(typeVariableType)) { + boolean streamingFileUpload = StreamingFileUpload.class.isAssignableFrom(typeVariableType); + if (streamingFileUpload) { + typeVariable = ARGUMENT_PART_DATA; + } else { + typeVariable = typeVariable.getFirstTypeVariable().orElse(Argument.OBJECT_ARGUMENT); + } + if (data.attachment.subject == null) { + Sinks.Many childSubject = makeDownstreamUnicastProcessor(); + Flux flowable = withFlowControl(childSubject.asFlux(), data); + if (streamingFileUpload && data instanceof FileUpload fu) { + namedSubject.tryEmitNext(fileUploadFactory.create(fu, flowable)); + } else { + namedSubject.tryEmitNext(flowable); + } + + data.attachment.subject = childSubject; + } + } + + Sinks.Many subject; + + if (data.attachment.subject != null) { + subject = data.attachment.subject; + } else { + subject = namedSubject; + } + + Object part = data; + + if (chunkedProcessing) { + MicronautHttpData.Chunk chunk = data.pollChunk(); + part = new NettyPartData(() -> { + if (data instanceof FileUpload fu) { + return Optional.of(MediaType.of(fu.getContentType())); + } else { + return Optional.empty(); + } + }, chunk::claim); + } + + if (data instanceof FileUpload fu && + StreamingFileUpload.class.isAssignableFrom(argument.getType()) && + data.attachment.upload == null) { + + data.attachment.upload = fileUploadFactory.create(fu, withFlowControl(subject.asFlux(), data)); + } + + Optional converted = conversionService.convert(part, typeVariable); + + converted.ifPresent(subject::tryEmitNext); + + if (data.isCompleted() && chunkedProcessing) { + subject.tryEmitComplete(); + } + + value = () -> { + if (data.attachment.upload != null) { + return data.attachment.upload; + } else { + if (data.attachment.subject == null) { + return withFlowControl(namedSubject.asFlux(), data); + } else { + return namedSubject.asFlux(); + } + } + }; + + } else { + if (data instanceof Attribute && !data.isCompleted()) { + request.addContent(data); + request(1); + return; + } else { + value = () -> { + if (data.refCnt() > 0) { + return data; + } else { + return null; + } + }; + } + } + + if (!execute) { + String argumentName = argument.getName(); + if (!routeMatch.isSatisfied(argumentName)) { + Object fulfillParamter = value.get(); + routeMatch = routeMatch.fulfill(Collections.singletonMap(argumentName, fulfillParamter)); + // we need to release the data here. However, if the route argument is a + // ByteBuffer, we need to retain the data until the route is executed. Adding + // the data to the request ensures it is cleaned up after the route completes. + if (!alwaysAddContent && fulfillParamter instanceof ByteBufHolder holder) { + request.addContent(holder); + } + } + if (isPublisher && chunkedProcessing) { + //accounting for the previous request + request(1); + } + if (routeMatch.isExecutable()) { + execute = true; + } + } + + if (alwaysAddContent && !request.destroyed) { + request.addContent(data); + } + + if (!execute || !chunkedProcessing) { + request(1); + } + } + + private Sinks.Many makeDownstreamUnicastProcessor() { + Sinks.Many processor = Sinks.many().unicast().onBackpressureBuffer(); + downstreamSubscribers.add(processor); + return processor; + } + + static class HttpDataAttachment { + private Sinks.Many subject; + private StreamingFileUpload upload; + } +} diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/HttpContentProcessor.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/HttpContentProcessor.java index 6a581dd1ff0..603c798dc0d 100644 --- a/http-server-netty/src/main/java/io/micronaut/http/server/netty/HttpContentProcessor.java +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/HttpContentProcessor.java @@ -15,18 +15,57 @@ */ package io.micronaut.http.server.netty; -import io.micronaut.core.async.publisher.Publishers; +import io.micronaut.core.type.Argument; import io.micronaut.core.util.Toggleable; import io.netty.buffer.ByteBufHolder; -import org.reactivestreams.Subscriber; + +import java.util.Collection; /** - * A reactive streams {@link org.reactivestreams.Processor} that processes incoming {@link ByteBufHolder} and - * outputs a given type. + * This class represents the first step of the HTTP body parsing pipeline. It transforms + * {@link ByteBufHolder} instances that come from a + * {@link io.micronaut.http.netty.stream.StreamedHttpRequest} into parsed objects, e.g. json nodes + * or form data fragments.
+ * Processors are stateful. They can receive repeated calls to {@link #add} with more data, + * followed by a call to {@link #complete} to finish up. Both of these methods accept a + * {@link Collection} {@code out} parameter that is populated with the processed items. * - * @param The type * @author Graeme Rocher * @since 1.0 */ -public interface HttpContentProcessor extends Publishers.MicronautPublisher, Subscriber, Toggleable { +public interface HttpContentProcessor extends Toggleable { + /** + * Process more data. + * + * @param data The input data + * @param out The collection to add output items to + */ + void add(ByteBufHolder data, Collection out) throws Throwable; + + /** + * Finish processing data. + * + * @param out The collection to add remaining output items to + */ + default void complete(Collection out) throws Throwable { + } + + /** + * Cancel processing, clean up any data. After this, there should be no more calls to + * {@link #add} and {@link #complete}. + */ + default void cancel() throws Throwable { + } + + /** + * Set the type of the values returned by this processor. Most processors do not respect this + * setting, but e.g. the {@link io.micronaut.http.server.netty.jackson.JsonContentProcessor} + * does. + * + * @param type The type produced by this processor + * @return This processor, for chaining + */ + default HttpContentProcessor resultType(Argument type) { + return this; + } } diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/HttpContentProcessorAsReactiveProcessor.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/HttpContentProcessorAsReactiveProcessor.java new file mode 100644 index 00000000000..504fcf74ae1 --- /dev/null +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/HttpContentProcessorAsReactiveProcessor.java @@ -0,0 +1,80 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.http.server.netty; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.http.netty.stream.StreamedHttpMessage; +import org.reactivestreams.Publisher; +import reactor.core.publisher.Flux; + +import java.util.ArrayList; +import java.util.List; + +/** + * Utility class for transforming a {@link NettyHttpRequest} using a {@link HttpContentProcessor} + * to a {@link Publisher}.
+ * Note: A more complicated, but possibly faster, implementation of this class is archived in + * the original PR. + * + * @since 4.0.0 + * @author Jonas Konrad + */ +@Internal +public final class HttpContentProcessorAsReactiveProcessor { + private HttpContentProcessorAsReactiveProcessor() { + } + + /** + * Subscribe to the {@link StreamedHttpMessage} in the given request, and return a + * {@link Publisher} that will produce the processed items.
+ * This exists mostly for compatibility with the old {@link HttpContentProcessor}, which was a + * {@link org.reactivestreams.Processor}. + * + * @param processor The content processor to use + * @param request The request to subscribe to + * @return The publisher producing output data + * @param The output element type + */ + @SuppressWarnings("unchecked") + public static Publisher asPublisher(HttpContentProcessor processor, NettyHttpRequest request) { + StreamedHttpMessage streamed = (StreamedHttpMessage) request.getNativeRequest(); + return Flux.concat(Flux.from(streamed) + .doOnError(e -> { + try { + processor.cancel(); + } catch (Throwable ex) { + e.addSuppressed(ex); + } + }) + .concatMap(c -> { + try { + List out = new ArrayList<>(1); + processor.add(c, (List) out); + return Flux.fromIterable(out); + } catch (Throwable e) { + return Flux.error(e); + } + }), Flux.defer(() -> { + try { + List out = new ArrayList<>(1); + processor.complete((List) out); + return Flux.fromIterable(out); + } catch (Throwable ex) { + return Flux.error(ex); + } + })); + } +} diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/HttpContentProcessorResolver.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/HttpContentProcessorResolver.java index 23a1c6ff372..74f8368eebf 100644 --- a/http-server-netty/src/main/java/io/micronaut/http/server/netty/HttpContentProcessorResolver.java +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/HttpContentProcessorResolver.java @@ -40,7 +40,7 @@ public interface HttpContentProcessorResolver { * @return The content processor */ @NonNull - HttpContentProcessor resolve(@NonNull NettyHttpRequest request, @NonNull RouteMatch route); + HttpContentProcessor resolve(@NonNull NettyHttpRequest request, @NonNull RouteMatch route); /** * Resolves the processor for the given request and body argument. @@ -50,7 +50,7 @@ public interface HttpContentProcessorResolver { * @return The content processor */ @NonNull - HttpContentProcessor resolve(@NonNull NettyHttpRequest request, @NonNull Argument bodyType); + HttpContentProcessor resolve(@NonNull NettyHttpRequest request, @NonNull Argument bodyType); /** * Resolves the processor for the given request. @@ -59,5 +59,5 @@ public interface HttpContentProcessorResolver { * @return The content processor */ @NonNull - HttpContentProcessor resolve(@NonNull NettyHttpRequest request); + HttpContentProcessor resolve(@NonNull NettyHttpRequest request); } diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/HttpDataReference.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/HttpDataReference.java deleted file mode 100644 index 2c9970482d0..00000000000 --- a/http-server-netty/src/main/java/io/micronaut/http/server/netty/HttpDataReference.java +++ /dev/null @@ -1,253 +0,0 @@ -/* - * Copyright 2017-2020 original authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.micronaut.http.server.netty; - -import io.micronaut.core.annotation.Internal; -import io.micronaut.http.MediaType; -import io.micronaut.http.multipart.StreamingFileUpload; -import io.netty.buffer.ByteBuf; -import io.netty.buffer.CompositeByteBuf; -import io.netty.buffer.Unpooled; -import io.netty.handler.codec.http.multipart.FileUpload; -import io.netty.handler.codec.http.multipart.HttpData; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import reactor.core.publisher.Sinks; - -import java.io.IOException; -import java.io.RandomAccessFile; -import java.nio.ByteBuffer; -import java.util.ArrayList; -import java.util.List; -import java.util.Optional; -import java.util.concurrent.atomic.AtomicLong; -import java.util.concurrent.atomic.AtomicReference; -import java.util.function.BiPredicate; - -/** - * A helper class to store references to httpdata and related information. - * - * @author James Kleeh - * @since 1.1.0 - */ -@Internal -public class HttpDataReference { - - private static final Logger LOG = LoggerFactory.getLogger(HttpDataReference.class); - - final AtomicReference> subject = new AtomicReference<>(); - final AtomicReference upload = new AtomicReference<>(); - - private final HttpData data; - private final AtomicReference fileAccess = new AtomicReference<>(); - private final AtomicLong position = new AtomicLong(0); - private final List components = new ArrayList<>(); - - /** - * @param data The data this class is in control of - */ - HttpDataReference(HttpData data) { - this.data = data; - data.retain(); - } - - /** - * @return The content type of the http data - */ - public Optional getContentType() { - if (data instanceof FileUpload) { - return Optional.of(MediaType.of(((FileUpload) data).getContentType())); - } else { - return Optional.empty(); - } - } - - /** - * Adds a reference to a section of the http data. Should only - * be called after data has been added to the underlying http data. - * - * @return The newly added component, or null if an error occurred - */ - Component addComponent() throws IOException { - Component component; - long readable = readableBytes(data); - long offset = position.getAndUpdate(p -> readable); - int length = (int) (readable - offset); - if (length == 0) { - return null; - } - component = new Component(length, offset); - components.add(component); - - if (!data.isInMemory()) { - AtomicReference error = new AtomicReference<>(); - fileAccess.getAndUpdate(channel -> { - if (channel == null) { - try { - return new RandomAccessFile(data.getFile(), "r"); - } catch (IOException e) { - error.set(e); - } - } - return channel; - }); - IOException exception = error.get(); - if (exception != null) { - throw exception; - } - } - - return component; - } - - /** - * Removes a section from the http data and updates the - * indices of the remaining components. - * - * @param index The index of the component to remove. - */ - void removeComponent(int index) { - Component component = components.get(index); - components.remove(index); - updateComponentOffsets(index); - position.getAndUpdate(offset -> offset - component.length); - } - - private long readableBytes(HttpData httpData) throws IOException { - if (httpData.isInMemory()) { - ByteBuf byteBuf = httpData.getByteBuf(); - if (byteBuf != null) { - return byteBuf.readableBytes(); - } else { - return 0; - } - } else { - return httpData.length(); - } - } - - private void updateComponentOffsets(int index) { - int size = components.size(); - if (size <= index) { - return; - } - - Component c = components.get(index); - if (index == 0) { - c.offset = 0; - index++; - } - - for (int i = index; i < size; i++) { - Component prev = components.get(i - 1); - Component cur = components.get(i); - cur.offset = prev.length; - } - } - - /** - * Closes any file related access if the upload is on - * disk and releases the buffer for the file. - */ - void destroy() { - fileAccess.getAndUpdate(channel -> { - if (channel != null) { - try { - channel.close(); - } catch (IOException e) { - LOG.warn("Error closing file channel for disk file upload", e); - } - } - return null; - }); - data.release(); - } - - /** - * Represents a section of the http data. - */ - public final class Component { - - private final int length; - private long offset; - - private Component(int length, long offset) { - this.length = length; - this.offset = offset; - } - - private ByteBuf createDelegate(ByteBuf byteBuf, BiPredicate onRelease) { - if (byteBuf == null) { - return Unpooled.EMPTY_BUFFER; - } - return new ByteBufDelegate(byteBuf) { - @Override - public boolean release() { - return onRelease.test(byteBuf, 1); - } - - @Override - public boolean release(int decrement) { - return onRelease.test(byteBuf, decrement); - } - }; - } - - /** - * @return A buffer that holds the data for this section. The - * caller is responsible for releasing the buffer. - * - * @throws IOException If the buffer could not be obtained - */ - public ByteBuf getByteBuf() throws IOException { - if (length == 0) { - return Unpooled.EMPTY_BUFFER; - } - if (data.isInMemory()) { - ByteBuf byteBuf = data.getByteBuf(); - int index = components.indexOf(this); - if (byteBuf instanceof CompositeByteBuf) { - CompositeByteBuf compositeByteBuf = (CompositeByteBuf) byteBuf; - return createDelegate(compositeByteBuf.internalComponent(index), (buf, count) -> { - compositeByteBuf.removeComponent(index); - removeComponent(index); - return true; - }); - } else { - return createDelegate(byteBuf, (buf, count) -> { - //needs to be retrieved again because the internal reference - //may have changed - try { - ByteBuf currentBuffer = data.getByteBuf(); - if (currentBuffer instanceof CompositeByteBuf) { - ((CompositeByteBuf) currentBuffer).removeComponent(index); - } else { - data.delete(); - } - } catch (IOException e) { } - removeComponent(index); - return true; - }); - } - } else { - byte[] data = new byte[length]; - fileAccess.get().getChannel().read(ByteBuffer.wrap(data), offset); - return Unpooled.wrappedBuffer(data); - } - } - } - -} diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/MicronautHttpData.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/MicronautHttpData.java new file mode 100644 index 00000000000..7544b039b40 --- /dev/null +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/MicronautHttpData.java @@ -0,0 +1,784 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.http.server.netty; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.NonNull; +import io.micronaut.core.annotation.Nullable; +import io.micronaut.core.util.SupplierUtil; +import io.micronaut.http.server.HttpServerConfiguration; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.CompositeByteBuf; +import io.netty.buffer.Unpooled; +import io.netty.handler.codec.http.HttpRequest; +import io.netty.handler.codec.http.multipart.Attribute; +import io.netty.handler.codec.http.multipart.FileUpload; +import io.netty.handler.codec.http.multipart.HttpData; +import io.netty.handler.codec.http.multipart.HttpDataFactory; +import io.netty.handler.codec.http.multipart.InterfaceHttpData; +import io.netty.util.AbstractReferenceCounted; +import io.netty.util.ReferenceCounted; +import io.netty.util.ResourceLeakDetector; +import io.netty.util.ResourceLeakDetectorFactory; +import io.netty.util.ResourceLeakTracker; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.UncheckedIOException; +import java.nio.channels.FileChannel; +import java.nio.charset.Charset; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; +import java.util.function.Supplier; + +/** + * Alternate {@link HttpData} implementation with some limited concurrency support. Only implements + * the features we actually need.
+ * In most cases, we only access the {@link HttpData} on a single thread, with the standard + * {@link #get()} and friends. However, if the user wants a reactive stream of data as it comes in, + * this class can release chunks of that data for concurrent access by the user (see + * {@link #pollChunk()}).
+ * This class moves data to disk dynamically once the configured threshold is reached. + * + * @param This {@link HttpData} type, for {@code return (D) this} on various methods + */ +@Internal +public abstract sealed class MicronautHttpData extends AbstractReferenceCounted implements HttpData { + @SuppressWarnings("rawtypes") + private static final Supplier> LEAK_DETECTOR = SupplierUtil.memoized(() -> + ResourceLeakDetectorFactory.instance().newResourceLeakDetector(MicronautHttpData.class)); + + private static final Logger LOG = LoggerFactory.getLogger(MicronautHttpData.class); + + private static final int MMAP_SEGMENT_SIZE = 1024 * 1024 * 1024; + private static final int MAX_CHUNK_SIZE = 1024 * 1024 * 1024; + + final Factory factory; + + long definedSize = 0; + Charset charset; + + /** + * Additional data for {@link FormRouteCompleter}. + */ + FormRouteCompleter.HttpDataAttachment attachment; + + @Nullable + @SuppressWarnings("rawtypes") + private final ResourceLeakTracker tracker = LEAK_DETECTOR.get().track(this); + + private final String name; + + private final List chunks = new ArrayList<>(); + + private long size = 0; + + @Nullable + private Path path; + private FileChannel channel; + private List mmapSegments; + + private boolean completed = false; + + private int pollIndex = 0; + + private MicronautHttpData(Factory factory, String name) { + this.factory = factory; + this.name = name; + this.charset = factory.characterEncoding; + chunks.add(new Chunk(0)); + } + + private boolean shouldMoveToDisk(long newSize) { + if (factory.multipartConfiguration.isDisk()) { + return true; + } else if (factory.multipartConfiguration.isMixed()) { + return newSize >= factory.multipartConfiguration.getThreshold(); + } else { + return false; + } + } + + private Chunk lastChunk() { + return chunks.get(chunks.size() - 1); + } + + /** + * Get a chunk of data. The chunk will have a fixed content, it will not be amended with + * further input. + * + * @return The chunk, or {@code null} if this data is {@link #isCompleted() completed} and all + * chunks have been polled. + */ + public Chunk pollChunk() { + if (pollIndex >= chunks.size()) { + return null; + } + Chunk chunk = chunks.get(pollIndex++); + if (pollIndex == chunks.size() && !completed) { + chunks.add(new Chunk(size)); + } + // ownership of the chunk is shared: One release call from our deallocate(), one release + // call by the caller of pollChunk(). Usually this retain corresponds to the release in + // Chunk.claim + chunk.retain(); + return chunk; + } + + public InputStream toStream() { + retain(); + return new StreamImpl(); + } + + @Override + public void addContent(ByteBuf buffer, boolean last) throws IOException { + if (completed) { + throw new IllegalStateException("Already completed"); + } + buffer.touch(); + long newSize = size + buffer.readableBytes(); + if (newSize > factory.multipartConfiguration.getMaxFileSize()) { + buffer.release(); + throw new IOException("Size exceed allowed maximum capacity"); + } + if (channel == null && shouldMoveToDisk(newSize)) { + transferToDisk(); + } + + // find a chunk + Chunk chunk; + int newChunkSize; + while (true) { + chunk = lastChunk(); + if (chunk.lock.tryLock()) { + if (chunk.buf == null) { + newChunkSize = buffer.readableBytes(); + } else { + newChunkSize = chunk.buf.readableBytes() + buffer.readableBytes(); + if (newChunkSize > MAX_CHUNK_SIZE) { + newChunkSize = -1; // create new chunk + } + } + if (newChunkSize >= 0) { + break; + } else { + // size overflow or hit limit, make a new chunk + chunk.lock.unlock(); + } + } + chunks.add(new Chunk(size)); + } + // add to the chunk + try { + if (channel == null) { + if (chunk.buf == null) { + chunk.buf = buffer; + } else if (chunk.buf instanceof CompositeByteBuf composite) { + composite.addComponent(true, buffer); + } else { + chunk.buf = Unpooled.compositeBuffer() + .addComponent(true, chunk.buf) + .addComponent(true, buffer); + } + } else { + buffer.readBytes(channel, size, buffer.readableBytes()); + buffer.release(); + chunk.loadFromDisk(newChunkSize); + } + size = newSize; + if (newSize > definedSize && definedSize != 0) { + definedSize = newSize; + } + } finally { + chunk.lock.unlock(); + } + if (last) { + completed = true; + if (channel != null) { + channel.close(); + } + } + } + + private ByteBuf mmapSegment(int index) throws IOException { + while (mmapSegments.size() <= index) { + mmapSegments.add(null); + } + ByteBuf segment = mmapSegments.get(index); + if (segment == null) { + segment = Unpooled.wrappedBuffer( + channel.map(FileChannel.MapMode.READ_ONLY, (long) index * MMAP_SEGMENT_SIZE, MMAP_SEGMENT_SIZE)); + mmapSegments.set(index, segment); + } + return segment; + } + + private void transferToDisk() throws IOException { + assert channel == null; + + path = newTempFile(); + channel = FileChannel.open(path, StandardOpenOption.READ, StandardOpenOption.WRITE); + + for (Chunk chunk : chunks) { + if (chunk.buf != null) { + chunk.buf.getBytes(chunk.buf.readerIndex(), channel, chunk.offset, chunk.buf.readableBytes()); + } + } + mmapSegments = new ArrayList<>(); + for (Chunk chunk : chunks) { + if (chunk.lock.tryLock()) { + try { + if (chunk.buf != null) { + chunk.loadFromDisk(chunk.buf.readableBytes()); + } + } finally { + chunk.lock.unlock(); + } + } // if tryLock failed, the user already requested the chunk, we can't move it anymore. + } + } + + private Path newTempFile() throws IOException { + Optional location = factory.multipartConfiguration.getLocation(); + if (location.isPresent()) { + return Files.createTempFile(location.get().toPath(), "FUp_", ".tmp"); + } else { + return Files.createTempFile("FUp_", ".tmp"); + } + } + + @Override + protected void deallocate() { + if (tracker != null) { + tracker.close(this); + } + dealloc0(); + } + + private void dealloc0() { + if (channel != null) { + try { + channel.close(); + } catch (IOException e) { + LOG.warn("Failed to close temp file channel", e); + } + } + if (path != null) { + try { + Files.deleteIfExists(path); + } catch (IOException e) { + LOG.warn("Failed to delete temp file", e); + } + } + for (Chunk chunk : chunks) { + chunk.release(); + } + if (mmapSegments != null) { + for (ByteBuf segment : mmapSegments) { + segment.release(); + } + } + } + + @Override + public void setContent(ByteBuf buffer) throws IOException { + dealloc0(); + chunks.clear(); + + Chunk ch = new Chunk(0); + chunks.add(ch); + ch.buf = buffer; + size = buffer.readableBytes(); + } + + @Override + public long getMaxSize() { + throw new UnsupportedOperationException(); + } + + @Override + public void setMaxSize(long maxSize) { + throw new UnsupportedOperationException(); + } + + @Override + public void checkSize(long newSize) throws IOException { + throw new UnsupportedOperationException(); + } + + @Override + public void setContent(File file) throws IOException { + throw new UnsupportedOperationException(); + } + + @Override + public void setContent(InputStream inputStream) throws IOException { + throw new UnsupportedOperationException(); + } + + @Override + public boolean isCompleted() { + return completed; + } + + @Override + public long length() { + return size; + } + + @Override + public long definedLength() { + return definedSize; + } + + @Override + public void delete() { + throw new UnsupportedOperationException(); + } + + @Override + public byte[] get() throws IOException { + byte[] arr = new byte[Math.toIntExact(size)]; + for (Chunk chunk : chunks) { + if (!chunk.lock.tryLock()) { + throw new IllegalStateException( + "Chunk already claimed (or get() called concurrently, which is not allowed)"); + } + try { + if (chunk.buf != null) { + chunk.buf.getBytes(chunk.buf.readerIndex(), arr, Math.toIntExact(chunk.offset), chunk.buf.readableBytes()); + } + } finally { + chunk.lock.unlock(); + } + } + return arr; + } + + @Override + public ByteBuf getByteBuf() { + // todo: can't use pooled buffer here, HttpPostStandardRequestDecoder has a bug where it + // doesn't release the buffer properly + ByteBuf buf = Unpooled.buffer(Math.toIntExact(size)); + for (Chunk chunk : chunks) { + if (!chunk.lock.tryLock()) { + buf.release(); + throw new IllegalStateException( + "Chunk already claimed (or get() called concurrently, which is not allowed)"); + } + try { + if (chunk.buf != null) { + chunk.buf.getBytes(chunk.buf.readerIndex(), buf, chunk.buf.readableBytes()); + } + } finally { + chunk.lock.unlock(); + } + } + return buf; + } + + @Override + public ByteBuf getChunk(int length) throws IOException { + throw new UnsupportedOperationException(); + } + + @Override + public String getString() throws IOException { + throw new UnsupportedOperationException(); + } + + @Override + public String getString(Charset encoding) throws IOException { + return new String(get(), encoding); + } + + @Override + public void setCharset(Charset charset) { + this.charset = charset; + } + + @Override + public Charset getCharset() { + return charset; + } + + @Override + public boolean renameTo(File dest) throws IOException { + throw new UnsupportedOperationException(); + } + + @Override + public boolean isInMemory() { + throw new UnsupportedOperationException(); + } + + @Override + public File getFile() throws IOException { + throw new UnsupportedOperationException(); + } + + @Override + public ByteBuf content() { + return getByteBuf(); + } + + @Override + public D copy() { + throw new UnsupportedOperationException(); + } + + @Override + public D duplicate() { + throw new UnsupportedOperationException(); + } + + @Override + public D retainedDuplicate() { + throw new UnsupportedOperationException(); + } + + @Override + public D replace(ByteBuf content) { + throw new UnsupportedOperationException(); + } + + @Override + public String getName() { + return name; + } + + @SuppressWarnings("unchecked") + @Override + public D touch(Object hint) { + if (tracker != null) { + tracker.record(hint); + } + return (D) this; + } + + @Override + public int compareTo(@NonNull InterfaceHttpData o) { + throw new UnsupportedOperationException(); + } + + @SuppressWarnings("unchecked") + @Override + public D retain() { + return (D) super.retain(); + } + + @SuppressWarnings("unchecked") + @Override + public D retain(int increment) { + return (D) super.retain(increment); + } + + @SuppressWarnings("unchecked") + @Override + public D touch() { + if (tracker != null) { + tracker.record(); + } + return (D) super.touch(); + } + + private static final class AttributeImpl extends MicronautHttpData implements Attribute { + AttributeImpl(Factory factory, String name) { + super(factory, name); + } + + @Override + public String getValue() throws IOException { + return new String(get(), factory.characterEncoding); + } + + @Override + public void setValue(String value) throws IOException { + throw new UnsupportedOperationException(); + } + + @Override + public HttpDataType getHttpDataType() { + return HttpDataType.Attribute; + } + } + + private static final class FileUploadImpl extends MicronautHttpData implements FileUpload { + private final String fileName; + private final String contentType; + + FileUploadImpl(Factory factory, String name, String fileName, String contentType) { + super(factory, name); + this.fileName = fileName; + this.contentType = contentType; + } + + @Override + public String getFilename() { + return fileName; + } + + @Override + public void setFilename(String filename) { + throw new UnsupportedOperationException(); + } + + @Override + public void setContentType(String contentType) { + throw new UnsupportedOperationException(); + } + + @Override + public String getContentType() { + return contentType; + } + + @Override + public void setContentTransferEncoding(String contentTransferEncoding) { + throw new UnsupportedOperationException(); + } + + @Override + public String getContentTransferEncoding() { + throw new UnsupportedOperationException(); + } + + @Override + public HttpDataType getHttpDataType() { + return HttpDataType.FileUpload; + } + } + + /** + * Chunk of bytes from this data object. When this is exposed (returned by + * {@link #pollChunk()}), the data is "fixed", there won't be new data added. + */ + public final class Chunk extends AbstractReferenceCounted { + // one reference is kept by the MicronautHttpData.chunks list, and is released on MicronautHttpData.deallocate. + // The other reference is created by the user on pollChunk, and released when she calls claim() + + private final Lock lock = new ReentrantLock(); + private final long offset; + @Nullable + private ByteBuf buf; // always has refCnt = 1 + + private Chunk(long offset) { + this.offset = offset; + } + + private void loadFromDisk(int length) throws IOException { + int firstSegmentIndex = Math.toIntExact(offset / MMAP_SEGMENT_SIZE); + int lastSegmentIndex = Math.toIntExact((offset + length - 1) / MMAP_SEGMENT_SIZE); + + int offsetInSegment = Math.toIntExact(offset % MMAP_SEGMENT_SIZE); + ByteBuf oldBuf = buf; + if (firstSegmentIndex == lastSegmentIndex) { + buf = mmapSegment(firstSegmentIndex).retainedSlice(offsetInSegment, Math.toIntExact(length)); + } else { + CompositeByteBuf composite = Unpooled.compositeBuffer(lastSegmentIndex - firstSegmentIndex + 1); + composite.addComponent(mmapSegment(firstSegmentIndex).retainedSlice(offsetInSegment, MMAP_SEGMENT_SIZE - offsetInSegment)); + for (int i = firstSegmentIndex + 1; i < lastSegmentIndex; i++) { + composite.addComponent(mmapSegment(i).retain()); + } + composite.addComponent(mmapSegment(lastSegmentIndex).retainedSlice(0, Math.toIntExact((offset + length) % MMAP_SEGMENT_SIZE))); + buf = composite; + } + if (oldBuf != null) { + oldBuf.release(); + } + } + + /** + * Get the contents of this chunk as a {@link ByteBuf}. If there are concurrent operations + * on this data (e.g. it is being moved to disk), this method may block. Must only be + * called once. + * + * @return The contents of this chunk + */ + ByteBuf claim() { + lock.lock(); + if (buf == null) { + return Unpooled.EMPTY_BUFFER; + } + ByteBuf b = buf; + buf = null; + b.touch(); + release(); + return b; + } + + @Override + protected void deallocate() { + if (!lock.tryLock()) { + // already claimed + return; + } + if (buf != null) { + buf.release(); + buf = null; + } + } + + @Override + public ReferenceCounted touch() { + return this; + } + + @Override + public ReferenceCounted touch(Object hint) { + return this; + } + } + + /** + * Factory for {@link MicronautHttpData} instances. Immutable, only some operations are + * supported. + */ + @Internal + public static final class Factory implements HttpDataFactory { + private final HttpServerConfiguration.MultipartConfiguration multipartConfiguration; + private final Charset characterEncoding; + + private final Set> toClean = new HashSet<>(); + + public Factory(HttpServerConfiguration.MultipartConfiguration multipartConfiguration, Charset characterEncoding) { + this.multipartConfiguration = multipartConfiguration; + this.characterEncoding = characterEncoding; + } + + @Override + public void setMaxLimit(long max) { + throw new UnsupportedOperationException(); + } + + public AttributeImpl createAttribute(String name) { + AttributeImpl attribute = new AttributeImpl(this, name); + toClean.add(attribute); + return attribute; + } + + @Override + public Attribute createAttribute(HttpRequest request, String name) { + return createAttribute(name); + } + + @Override + public Attribute createAttribute(HttpRequest request, String name, long definedSize) { + AttributeImpl attribute = createAttribute(name); + attribute.definedSize = definedSize; + return attribute; + } + + @Override + public Attribute createAttribute(HttpRequest request, String name, String value) { + AttributeImpl attr = createAttribute(name); + try { + attr.addContent(Unpooled.wrappedBuffer(value.getBytes(characterEncoding)), true); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + return attr; + } + + @Override + public FileUpload createFileUpload(HttpRequest request, String name, String filename, String contentType, String contentTransferEncoding, Charset charset, long size) { + FileUploadImpl fileUpload = new FileUploadImpl(this, name, filename, contentType); + toClean.add(fileUpload); + fileUpload.definedSize = size; + fileUpload.charset = charset; + return fileUpload; + } + + @Override + public void removeHttpDataFromClean(HttpRequest request, InterfaceHttpData data) { + //noinspection SuspiciousMethodCalls + toClean.remove(data); + } + + @Override + public void cleanRequestHttpData(HttpRequest request) { + cleanAllHttpData(); + } + + @Override + public void cleanAllHttpData() { + for (MicronautHttpData micronautHttpData : toClean) { + micronautHttpData.release(); + } + toClean.clear(); + } + + @Override + public void cleanRequestHttpDatas(HttpRequest request) { + throw new UnsupportedOperationException(); + } + + @Override + public void cleanAllHttpDatas() { + throw new UnsupportedOperationException(); + } + } + + private final class StreamImpl extends InputStream { + ByteBuf buf = Unpooled.EMPTY_BUFFER; + + @Override + public int read() throws IOException { + byte[] arr = new byte[1]; + if (read(arr) != 1) { + return -1; + } + return arr[0] & 0xff; + } + + @Override + public int read(@NonNull byte[] b, int off, int len) throws IOException { + if (!buf.isReadable()) { + buf.release(); + + Chunk nextChunk = pollChunk(); + if (nextChunk == null) { + buf = Unpooled.EMPTY_BUFFER; + return -1; + } + buf = nextChunk.claim(); + } + int n = Math.min(len, buf.readableBytes()); + buf.readBytes(b, off, n); + return n; + } + + @Override + public void close() throws IOException { + if (buf != null) { + buf.release(); + buf = null; + MicronautHttpData.this.release(); + } + } + } +} diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/NettyHttpRequest.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/NettyHttpRequest.java index 4b8c4377037..9cd44eee6bc 100644 --- a/http-server-netty/src/main/java/io/micronaut/http/server/netty/NettyHttpRequest.java +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/NettyHttpRequest.java @@ -62,9 +62,7 @@ import io.netty.handler.codec.http.HttpHeaderNames; import io.netty.handler.codec.http.QueryStringDecoder; import io.netty.handler.codec.http.cookie.ClientCookieEncoder; -import io.netty.handler.codec.http.multipart.AbstractHttpData; import io.netty.handler.codec.http.multipart.HttpData; -import io.netty.handler.codec.http.multipart.MixedAttribute; import io.netty.handler.codec.http2.DefaultHttp2PushPromiseFrame; import io.netty.handler.codec.http2.Http2ConnectionHandler; import io.netty.handler.codec.http2.Http2FrameCodec; @@ -414,7 +412,7 @@ public RouteMatch getMatchedRoute() { @Internal public void addContent(ByteBufHolder httpContent) { httpContent.touch(); - if (httpContent instanceof AbstractHttpData || httpContent instanceof MixedAttribute) { + if (httpContent instanceof MicronautHttpData) { receivedData.computeIfAbsent(new IdentityWrapper(httpContent), key -> { // released in release() httpContent.retain(); diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/NettyRequestLifecycle.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/NettyRequestLifecycle.java new file mode 100644 index 00000000000..46984fd23ac --- /dev/null +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/NettyRequestLifecycle.java @@ -0,0 +1,303 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.http.server.netty; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.Nullable; +import io.micronaut.core.execution.CompletableFutureExecutionFlow; +import io.micronaut.core.execution.ExecutionFlow; +import io.micronaut.core.type.Argument; +import io.micronaut.http.HttpMethod; +import io.micronaut.http.HttpRequest; +import io.micronaut.http.HttpResponse; +import io.micronaut.http.HttpStatus; +import io.micronaut.http.MutableHttpResponse; +import io.micronaut.http.annotation.Body; +import io.micronaut.http.context.ServerRequestContext; +import io.micronaut.http.netty.stream.StreamedHttpRequest; +import io.micronaut.http.server.RequestLifecycle; +import io.micronaut.http.server.multipart.MultipartBody; +import io.micronaut.http.server.netty.multipart.NettyStreamingFileUpload; +import io.micronaut.http.server.netty.types.files.NettyStreamedFileCustomizableResponseType; +import io.micronaut.http.server.netty.types.files.NettySystemFileCustomizableResponseType; +import io.micronaut.http.server.types.files.FileCustomizableResponseType; +import io.micronaut.web.router.MethodBasedRouteMatch; +import io.micronaut.web.router.RouteMatch; +import io.netty.buffer.ByteBufHolder; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.DecoderResult; +import io.netty.handler.codec.TooLongFrameException; +import org.reactivestreams.Subscriber; +import org.reactivestreams.Subscription; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.net.URISyntaxException; +import java.net.URL; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; + +@Internal +final class NettyRequestLifecycle extends RequestLifecycle { + private static final Logger LOG = LoggerFactory.getLogger(NettyRequestLifecycle.class); + + private final RoutingInBoundHandler rib; + private final ChannelHandlerContext ctx; + + /** + * Should only be used where netty-specific stuff is needed, such as reading the body or + * writing the response. Otherwise, use {@link #request()} which can be updated by filters + */ + private final NettyHttpRequest nettyRequest; + + NettyRequestLifecycle(RoutingInBoundHandler rib, ChannelHandlerContext ctx, NettyHttpRequest request) { + super(rib.routeExecutor, request); + this.rib = rib; + this.ctx = ctx; + this.nettyRequest = request; + + multipartEnabled(rib.multipartEnabled); + } + + void handleNormal() { + ctx.channel().config().setAutoRead(false); + + if (LOG.isDebugEnabled()) { + HttpMethod httpMethod = request().getMethod(); + ServerRequestContext.set(request()); + LOG.debug("Request {} {}", httpMethod, request().getUri()); + } + + ExecutionFlow> result; + + // handle decoding failure + DecoderResult decoderResult = nettyRequest.getNativeRequest().decoderResult(); + if (decoderResult.isFailure()) { + Throwable cause = decoderResult.cause(); + HttpStatus status = cause instanceof TooLongFrameException ? HttpStatus.REQUEST_ENTITY_TOO_LARGE : HttpStatus.BAD_REQUEST; + result = onStatusError( + HttpResponse.status(status), + status.getReason() + ); + } else { + result = normalFlow(); + } + + result.onComplete((response, throwable) -> rib.writeResponse(ctx, nettyRequest, response, throwable)); + } + + @Nullable + @Override + protected FileCustomizableResponseType findFile() { + Optional optionalUrl = rib.staticResourceResolver.resolve(request().getUri().getPath()); + if (optionalUrl.isPresent()) { + try { + URL url = optionalUrl.get(); + if (url.getProtocol().equals("file")) { + File file = Paths.get(url.toURI()).toFile(); + if (file.exists() && !file.isDirectory() && file.canRead()) { + return new NettySystemFileCustomizableResponseType(file); + } + } + return new NettyStreamedFileCustomizableResponseType(url); + } catch (URISyntaxException e) { + //no-op + } + } + return null; + } + + @Override + protected ExecutionFlow> fulfillArguments(RouteMatch routeMatch) { + // handle decoding failure + DecoderResult decoderResult = nettyRequest.getNativeRequest().decoderResult(); + if (decoderResult.isFailure()) { + return ExecutionFlow.error(decoderResult.cause()); + } + return super.fulfillArguments(routeMatch).flatMap(this::waitForBody); + } + + /** + * If necessary (e.g. when there's a {@link Body} parameter), wait for the body to come in. + * This method also sometimes fulfills more controller parameters with form data. + */ + private ExecutionFlow> waitForBody(RouteMatch routeMatch) { + if (!shouldReadBody(routeMatch)) { + ctx.read(); + return ExecutionFlow.just(routeMatch); + } + BaseRouteCompleter completer = nettyRequest.isFormOrMultipartData() ? + new FormRouteCompleter(new NettyStreamingFileUpload.Factory(rib.serverConfiguration.getMultipart(), rib.getIoExecutor()), rib.conversionService, nettyRequest, routeMatch) : + new BaseRouteCompleter(nettyRequest, routeMatch); + HttpContentProcessor processor = rib.httpContentProcessorResolver.resolve(nettyRequest, routeMatch); + StreamingDataSubscriber pr = new StreamingDataSubscriber(completer, processor); + ((StreamedHttpRequest) nettyRequest.getNativeRequest()).subscribe(pr); + return CompletableFutureExecutionFlow.just(pr.completion); + } + + void handleException(Throwable cause) { + onError(cause).onComplete((response, throwable) -> rib.writeResponse(ctx, nettyRequest, response, throwable)); + } + + private boolean shouldReadBody(RouteMatch routeMatch) { + if (!HttpMethod.permitsRequestBody(request().getMethod())) { + return false; + } + if (!(nettyRequest.getNativeRequest() instanceof StreamedHttpRequest)) { + // Illegal state: The request body is required, so at this point we must have a StreamedHttpRequest + return false; + } + if (routeMatch instanceof MethodBasedRouteMatch methodBasedRouteMatch) { + if (Arrays.stream(methodBasedRouteMatch.getArguments()).anyMatch(argument -> MultipartBody.class.equals(argument.getType()))) { + // MultipartBody will subscribe to the request body in MultipartBodyArgumentBinder + return false; + } + if (Arrays.stream(methodBasedRouteMatch.getArguments()).anyMatch(argument -> HttpRequest.class.equals(argument.getType()))) { + // HttpRequest argument in the method + return true; + } + } + Optional> bodyArgument = routeMatch.getBodyArgument() + .filter(argument -> argument.getAnnotationMetadata().hasAnnotation(Body.class)); + if (bodyArgument.isPresent() && !routeMatch.isSatisfied(bodyArgument.get().getName())) { + // Body argument in the method + return true; + } + // Might be some body parts + return !routeMatch.isExecutable(); + } + + private static class StreamingDataSubscriber implements Subscriber { + final CompletableFuture> completion = new CompletableFuture<>(); + + private final List bufferList = new ArrayList<>(1); + private final HttpContentProcessor contentProcessor; + private final BaseRouteCompleter completer; + private Subscription upstream; + + private volatile boolean upstreamRequested = false; + private boolean downstreamDone = false; + + StreamingDataSubscriber(BaseRouteCompleter completer, HttpContentProcessor contentProcessor) { + this.completer = completer; + this.contentProcessor = contentProcessor; + } + + private void checkDemand() { + if (completer.needsInput && !upstreamRequested) { + upstreamRequested = true; + upstream.request(1); + } + } + + @Override + public void onSubscribe(Subscription s) { + if (upstream != null) { + throw new IllegalStateException("Only one upstream subscription allowed"); + } + upstream = s; + completer.checkDemand = this::checkDemand; + checkDemand(); + } + + private void sendToCompleter(Collection out) throws Throwable { + for (Object processed : out) { + boolean wasExecuted = completer.execute; + completer.add(processed); + if (!wasExecuted && completer.execute) { + executeRoute(); + } + } + } + + @Override + public void onNext(ByteBufHolder holder) { + upstreamRequested = false; + if (downstreamDone) { + // previous error + holder.release(); + return; + } + try { + bufferList.clear(); + contentProcessor.add(holder, bufferList); + sendToCompleter(bufferList); + checkDemand(); + } catch (Throwable t) { + handleError(t); + } + } + + @Override + public void onError(Throwable t) { + if (downstreamDone) { + // previous error + LOG.warn("Downstream already complete, dropping error", t); + return; + } + handleError(t); + } + + private void handleError(Throwable t) { + try { + upstream.cancel(); + } catch (Throwable o) { + t.addSuppressed(o); + } + try { + contentProcessor.cancel(); + } catch (Throwable o) { + t.addSuppressed(o); + } + completer.completeFailure(t); + // this may drop the exception if the route has already been executed. However, that is + // only the case if there are publisher parameters, and those will still receive the + // failure. Hopefully. + completion.completeExceptionally(t); + downstreamDone = true; + } + + @Override + public void onComplete() { + if (downstreamDone) { + // previous error + return; + } + try { + bufferList.clear(); + contentProcessor.complete(bufferList); + sendToCompleter(bufferList); + boolean wasExecuted = completer.execute; + completer.completeSuccess(); + if (!wasExecuted && completer.execute) { + executeRoute(); + } + } catch (Throwable t) { + handleError(t); + } + } + + private void executeRoute() { + completion.complete(completer.routeMatch); + } + } +} diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/RoutingInBoundHandler.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/RoutingInBoundHandler.java index 2fd1391d220..8f976445671 100644 --- a/http-server-netty/src/main/java/io/micronaut/http/server/netty/RoutingInBoundHandler.java +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/RoutingInBoundHandler.java @@ -21,63 +21,43 @@ import io.micronaut.core.annotation.NonNull; import io.micronaut.core.annotation.Nullable; import io.micronaut.core.async.publisher.Publishers; -import io.micronaut.core.async.subscriber.CompletionAwareSubscriber; import io.micronaut.core.convert.ConversionService; -import io.micronaut.core.execution.ExecutionFlow; import io.micronaut.core.io.Writable; import io.micronaut.core.io.buffer.ByteBuffer; import io.micronaut.core.io.buffer.ReferenceCounted; -import io.micronaut.core.reflect.ClassUtils; import io.micronaut.core.type.Argument; import io.micronaut.http.HttpAttributes; import io.micronaut.http.HttpHeaders; import io.micronaut.http.HttpMethod; import io.micronaut.http.HttpRequest; import io.micronaut.http.HttpResponse; -import io.micronaut.http.HttpStatus; import io.micronaut.http.MediaType; import io.micronaut.http.MutableHttpHeaders; import io.micronaut.http.MutableHttpResponse; -import io.micronaut.http.annotation.Body; import io.micronaut.http.codec.MediaTypeCodec; import io.micronaut.http.codec.MediaTypeCodecRegistry; import io.micronaut.http.context.ServerRequestContext; import io.micronaut.http.context.event.HttpRequestTerminatedEvent; -import io.micronaut.http.multipart.PartData; -import io.micronaut.http.multipart.StreamingFileUpload; import io.micronaut.http.netty.NettyHttpResponseBuilder; import io.micronaut.http.netty.NettyMutableHttpResponse; import io.micronaut.http.netty.stream.JsonSubscriber; import io.micronaut.http.netty.stream.StreamedHttpRequest; -import io.micronaut.http.reactive.execution.ReactiveExecutionFlow; import io.micronaut.http.server.RouteExecutor; import io.micronaut.http.server.binding.RequestArgumentSatisfier; import io.micronaut.http.server.exceptions.InternalServerException; -import io.micronaut.http.server.multipart.MultipartBody; import io.micronaut.http.server.netty.configuration.NettyHttpServerConfiguration; -import io.micronaut.http.server.netty.multipart.NettyCompletedFileUpload; -import io.micronaut.http.server.netty.multipart.NettyPartData; -import io.micronaut.http.server.netty.multipart.NettyStreamingFileUpload; import io.micronaut.http.server.netty.types.NettyCustomizableResponseTypeHandler; import io.micronaut.http.server.netty.types.NettyCustomizableResponseTypeHandlerRegistry; -import io.micronaut.http.server.netty.types.files.NettyStreamedFileCustomizableResponseType; -import io.micronaut.http.server.netty.types.files.NettySystemFileCustomizableResponseType; -import io.micronaut.http.server.types.files.FileCustomizableResponseType; import io.micronaut.runtime.http.codec.TextPlainCodec; -import io.micronaut.web.router.MethodBasedRouteMatch; import io.micronaut.web.router.RouteInfo; -import io.micronaut.web.router.RouteMatch; import io.micronaut.web.router.resource.StaticResourceResolver; import io.netty.buffer.ByteBuf; -import io.netty.buffer.ByteBufHolder; import io.netty.buffer.ByteBufOutputStream; import io.netty.buffer.Unpooled; import io.netty.channel.ChannelFuture; import io.netty.channel.ChannelHandler.Sharable; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.SimpleChannelInboundHandler; -import io.netty.handler.codec.DecoderResult; -import io.netty.handler.codec.TooLongFrameException; import io.netty.handler.codec.http.DefaultFullHttpResponse; import io.netty.handler.codec.http.DefaultHttpContent; import io.netty.handler.codec.http.DefaultHttpHeaders; @@ -86,15 +66,10 @@ import io.netty.handler.codec.http.HttpHeaderValues; import io.netty.handler.codec.http.HttpResponseStatus; import io.netty.handler.codec.http.HttpVersion; -import io.netty.handler.codec.http.LastHttpContent; -import io.netty.handler.codec.http.multipart.Attribute; -import io.netty.handler.codec.http.multipart.FileUpload; -import io.netty.handler.codec.http.multipart.HttpData; import io.netty.handler.codec.http2.Http2Error; import io.netty.handler.codec.http2.Http2Exception; import io.netty.handler.timeout.IdleState; import io.netty.handler.timeout.IdleStateEvent; -import io.netty.util.ReferenceCountUtil; import io.netty.util.concurrent.Future; import io.netty.util.concurrent.GenericFutureListener; import org.reactivestreams.Publisher; @@ -103,30 +78,14 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; -import reactor.core.publisher.MonoSink; -import reactor.core.publisher.Sinks; import javax.net.ssl.SSLException; -import java.io.File; import java.io.IOException; -import java.net.URISyntaxException; -import java.net.URL; import java.nio.channels.ClosedChannelException; -import java.nio.file.Paths; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; import java.util.List; import java.util.Optional; -import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutorService; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicLong; -import java.util.concurrent.atomic.AtomicReference; import java.util.function.BiConsumer; -import java.util.function.LongConsumer; import java.util.function.Supplier; import java.util.regex.Pattern; @@ -139,7 +98,7 @@ @Internal @Sharable @SuppressWarnings("FileLength") -final class RoutingInBoundHandler extends SimpleChannelInboundHandler> implements RouteExecutor.StaticResourceResponseFinder { +final class RoutingInBoundHandler extends SimpleChannelInboundHandler> { private static final Logger LOG = LoggerFactory.getLogger(RoutingInBoundHandler.class); /* @@ -147,19 +106,18 @@ final class RoutingInBoundHandler extends SimpleChannelInboundHandler ARGUMENT_PART_DATA = Argument.of(PartData.class); - private final StaticResourceResolver staticResourceResolver; - private final NettyHttpServerConfiguration serverConfiguration; - private final HttpContentProcessorResolver httpContentProcessorResolver; - private final RequestArgumentSatisfier requestArgumentSatisfier; - private final MediaTypeCodecRegistry mediaTypeCodecRegistry; - private final NettyCustomizableResponseTypeHandlerRegistry customizableResponseTypeHandlerRegistry; - private final Supplier ioExecutorSupplier; - private final boolean multipartEnabled; - private ExecutorService ioExecutor; - private final ApplicationEventPublisher terminateEventPublisher; - private final RouteExecutor routeExecutor; - private final ConversionService conversionService; + final StaticResourceResolver staticResourceResolver; + final NettyHttpServerConfiguration serverConfiguration; + final HttpContentProcessorResolver httpContentProcessorResolver; + final RequestArgumentSatisfier requestArgumentSatisfier; + final MediaTypeCodecRegistry mediaTypeCodecRegistry; + final NettyCustomizableResponseTypeHandlerRegistry customizableResponseTypeHandlerRegistry; + final Supplier ioExecutorSupplier; + final boolean multipartEnabled; + ExecutorService ioExecutor; + final ApplicationEventPublisher terminateEventPublisher; + final RouteExecutor routeExecutor; + final ConversionService conversionService; /** * @param customizableResponseTypeHandlerRegistry The customizable response type handler registry @@ -269,63 +227,15 @@ public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { ctx.writeAndFlush(new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.INTERNAL_SERVER_ERROR)); return; } - routeExecutor.filterPublisher(new AtomicReference<>(nettyHttpRequest), () -> routeExecutor.onError(cause, nettyHttpRequest)) - .onComplete((response, throwable) -> writeResponse(ctx, nettyHttpRequest, response, throwable)); + new NettyRequestLifecycle(this, ctx, nettyHttpRequest).handleException(cause); } @Override protected void channelRead0(ChannelHandlerContext ctx, io.micronaut.http.HttpRequest httpRequest) { - NettyHttpRequest nettyHttpRequest = (NettyHttpRequest) httpRequest; - - ctx.channel().config().setAutoRead(false); - - if (LOG.isDebugEnabled()) { - HttpMethod httpMethod = httpRequest.getMethod(); - ServerRequestContext.set(httpRequest); - LOG.debug("Request {} {}", httpMethod, httpRequest.getUri()); - } - - io.netty.handler.codec.http.HttpRequest nativeRequest = nettyHttpRequest.getNativeRequest(); - - RouteExecutor.RequestBodyReader requestBodyReader = (routeMatch, hr) -> { - // handle decoding failure - DecoderResult decoderResult = nativeRequest.decoderResult(); - if (decoderResult.isFailure()) { - return ExecutionFlow.error(decoderResult.cause()); - } - // try to fulfill the argument requirements of the route - RouteMatch route = requestArgumentSatisfier.fulfillArgumentRequirements(routeMatch, httpRequest, false); - if (shouldReadBody(nettyHttpRequest, route)) { - return ReactiveExecutionFlow.fromPublisher( - Mono.create(emitter -> httpContentProcessorResolver.resolve(nettyHttpRequest, route) - .subscribe(buildSubscriber(nettyHttpRequest, route, emitter)) - )); - } - ctx.read(); - return ExecutionFlow.just(route); - }; - - ExecutionFlow> responseFlow; - - // handle decoding failure - DecoderResult decoderResult = nativeRequest.decoderResult(); - if (decoderResult.isFailure()) { - Throwable cause = decoderResult.cause(); - HttpStatus status = cause instanceof TooLongFrameException ? HttpStatus.REQUEST_ENTITY_TOO_LARGE : HttpStatus.BAD_REQUEST; - responseFlow = routeExecutor.onStatusError( - requestBodyReader, - httpRequest, - HttpResponse.status(status), - status.getReason() - ); - } else { - responseFlow = routeExecutor.executeRoute(requestBodyReader, httpRequest, multipartEnabled, this); - } - responseFlow - .onComplete((response, throwable) -> writeResponse(ctx, nettyHttpRequest, response, throwable)); + new NettyRequestLifecycle(this, ctx, (NettyHttpRequest) httpRequest).handleNormal(); } - private void writeResponse(ChannelHandlerContext ctx, + void writeResponse(ChannelHandlerContext ctx, NettyHttpRequest nettyHttpRequest, MutableHttpResponse response, Throwable throwable) { @@ -356,387 +266,7 @@ private void writeResponse(ChannelHandlerContext ctx, } } - private boolean shouldReadBody(NettyHttpRequest nettyHttpRequest, RouteMatch routeMatch) { - if (!HttpMethod.permitsRequestBody(nettyHttpRequest.getMethod())) { - return false; - } - io.netty.handler.codec.http.HttpRequest nativeRequest = nettyHttpRequest.getNativeRequest(); - if (!(nativeRequest instanceof StreamedHttpRequest)) { - // Illegal state: The request body is required, so at this point we must have a StreamedHttpRequest - return false; - } - if (routeMatch instanceof MethodBasedRouteMatch methodBasedRouteMatch) { - if (Arrays.stream(methodBasedRouteMatch.getArguments()).anyMatch(argument -> MultipartBody.class.equals(argument.getType()))) { - // MultipartBody will subscribe to the request body in MultipartBodyArgumentBinder - return false; - } - if (Arrays.stream(methodBasedRouteMatch.getArguments()).anyMatch(argument -> HttpRequest.class.equals(argument.getType()))) { - // HttpRequest argument in the method - return true; - } - } - Optional> bodyArgument = routeMatch.getBodyArgument() - .filter(argument -> argument.getAnnotationMetadata().hasAnnotation(Body.class)); - if (bodyArgument.isPresent() && !routeMatch.isSatisfied(bodyArgument.get().getName())) { - // Body argument in the method - return true; - } - // Might be some body parts - return !routeMatch.isExecutable(); - } - - @Override - public FileCustomizableResponseType find(HttpRequest httpRequest) { - Optional optionalUrl = staticResourceResolver.resolve(httpRequest.getUri().getPath()); - if (optionalUrl.isPresent()) { - try { - URL url = optionalUrl.get(); - if (url.getProtocol().equals("file")) { - File file = Paths.get(url.toURI()).toFile(); - if (file.exists() && !file.isDirectory() && file.canRead()) { - return new NettySystemFileCustomizableResponseType(file); - } - } - return new NettyStreamedFileCustomizableResponseType(url); - } catch (URISyntaxException e) { - //no-op - } - } - return null; - } - - private Subscriber buildSubscriber(NettyHttpRequest request, - RouteMatch finalRoute, - MonoSink> emitter) { - boolean isFormData = request.isFormOrMultipartData(); - if (isFormData) { - return new CompletionAwareSubscriber() { - final boolean alwaysAddContent = request.isFormData(); - RouteMatch routeMatch = finalRoute; - final AtomicBoolean executed = new AtomicBoolean(false); - final AtomicLong pressureRequested = new AtomicLong(0); - final ConcurrentHashMap> subjectsByDataName = new ConcurrentHashMap<>(); - final Collection> downstreamSubscribers = Collections.synchronizedList(new ArrayList<>()); - final ConcurrentHashMap dataReferences = new ConcurrentHashMap<>(); - Subscription s; - final LongConsumer onRequest = num -> pressureRequested.updateAndGet(p -> { - long newVal = p - num; - if (newVal < 0) { - s.request(num - p); - return 0; - } else { - return newVal; - } - }); - - Flux processFlowable(Sinks.Many many, HttpDataReference dataReference, boolean controlsFlow) { - Flux flux = many.asFlux(); - if (controlsFlow) { - flux = flux.doOnRequest(onRequest); - } - return flux - .doAfterTerminate(() -> { - if (controlsFlow) { - dataReference.destroy(); - } - }); - } - - @Override - protected void doOnSubscribe(Subscription subscription) { - this.s = subscription; - subscription.request(1); - } - - @Override - protected void doOnNext(Object message) { - try { - doOnNext0(message); - - // now, a pseudo try-finally with addSuppressed. - } catch (Throwable t) { - try { - ReferenceCountUtil.release(message); - } catch (Throwable u) { - t.addSuppressed(u); - } - throw t; - } - - // the upstream processor gives us ownership of the message, so we need to release it. - ReferenceCountUtil.release(message); - } - - private void doOnNext0(Object message) { - if (request.destroyed) { - // we don't want this message anymore - return; - } - - boolean wasExecuted = this.executed.get(); - if (message instanceof ByteBufHolder) { - if (message instanceof HttpData data) { - - if (LOG.isTraceEnabled()) { - LOG.trace("Received HTTP Data for request [{}]: {}", request, message); - } - - String name = data.getName(); - Optional> requiredInput = routeMatch.getRequiredInput(name); - - if (requiredInput.isPresent()) { - Argument argument = requiredInput.get(); - Supplier value; - boolean isPublisher = Publishers.isConvertibleToPublisher(argument.getType()); - boolean chunkedProcessing = false; - - if (isPublisher) { - HttpDataReference dataReference = dataReferences.computeIfAbsent(new IdentityWrapper(data), key -> new HttpDataReference(data)); - Argument typeVariable; - - if (StreamingFileUpload.class.isAssignableFrom(argument.getType())) { - typeVariable = ARGUMENT_PART_DATA; - } else { - typeVariable = argument.getFirstTypeVariable().orElse(Argument.OBJECT_ARGUMENT); - } - Class typeVariableType = typeVariable.getType(); - - Sinks.Many namedSubject = subjectsByDataName.computeIfAbsent(name, key -> makeDownstreamUnicastProcessor()); - - chunkedProcessing = PartData.class.equals(typeVariableType) || - Publishers.isConvertibleToPublisher(typeVariableType) || - ClassUtils.isJavaLangType(typeVariableType); - - if (Publishers.isConvertibleToPublisher(typeVariableType)) { - boolean streamingFileUpload = StreamingFileUpload.class.isAssignableFrom(typeVariableType); - if (streamingFileUpload) { - typeVariable = ARGUMENT_PART_DATA; - } else { - typeVariable = typeVariable.getFirstTypeVariable().orElse(Argument.OBJECT_ARGUMENT); - } - dataReference.subject.getAndUpdate(subject -> { - if (subject == null) { - Sinks.Many childSubject = makeDownstreamUnicastProcessor(); - Flux flowable = processFlowable(childSubject, dataReference, true); - if (streamingFileUpload && data instanceof FileUpload) { - namedSubject.tryEmitNext(new NettyStreamingFileUpload( - (FileUpload) data, - serverConfiguration.getMultipart(), - getIoExecutor(), - (Flux) flowable)); - } else { - namedSubject.tryEmitNext(flowable); - } - - return childSubject; - } - return subject; - }); - } - - Sinks.Many subject; - - final Sinks.Many ds = dataReference.subject.get(); - if (ds != null) { - subject = ds; - } else { - subject = namedSubject; - } - - Object part = data; - - if (chunkedProcessing) { - HttpDataReference.Component component; - try { - component = dataReference.addComponent(); - if (component == null) { - s.request(1); - return; - } - } catch (IOException e) { - subject.tryEmitError(e); - s.cancel(); - return; - } - part = new NettyPartData(dataReference, component); - } - - if (data instanceof FileUpload && - StreamingFileUpload.class.isAssignableFrom(argument.getType())) { - dataReference.upload.getAndUpdate(upload -> { - if (upload == null) { - return new NettyStreamingFileUpload( - (FileUpload) data, - serverConfiguration.getMultipart(), - getIoExecutor(), - (Flux) processFlowable(subject, dataReference, true)); - } - return upload; - }); - } - - Optional converted = conversionService.convert(part, typeVariable); - - converted.ifPresent(subject::tryEmitNext); - - if (data.isCompleted() && chunkedProcessing) { - subject.tryEmitComplete(); - } - - value = () -> { - StreamingFileUpload upload = dataReference.upload.get(); - if (upload != null) { - return upload; - } else { - return processFlowable(namedSubject, dataReference, dataReference.subject.get() == null); - } - }; - - } else { - if (data instanceof Attribute && !data.isCompleted()) { - request.addContent(data); - s.request(1); - return; - } else { - value = () -> { - if (data.refCnt() > 0) { - return data; - } else { - return null; - } - }; - } - } - - if (!wasExecuted) { - String argumentName = argument.getName(); - if (!routeMatch.isSatisfied(argumentName)) { - Object fulfillParamter = value.get(); - routeMatch = routeMatch.fulfill(Collections.singletonMap(argumentName, fulfillParamter)); - // we need to release the data here. However, if the route argument is a - // ByteBuffer, we need to retain the data until the route is executed. Adding - // the data to the request ensures it is cleaned up after the route completes. - if (!alwaysAddContent && fulfillParamter instanceof ByteBufHolder) { - request.addContent((ByteBufHolder) fulfillParamter); - } - } - if (isPublisher && chunkedProcessing) { - //accounting for the previous request - pressureRequested.incrementAndGet(); - } - if (routeMatch.isExecutable() || message instanceof LastHttpContent) { - executeRoute(); - wasExecuted = true; - } - } - - if (alwaysAddContent && !request.destroyed) { - request.addContent(data); - } - - if (!wasExecuted || !chunkedProcessing) { - s.request(1); - } - - } else { - request.addContent(data); - s.request(1); - } - } else { - request.addContent((ByteBufHolder) message); - s.request(1); - } - } else { - ((NettyHttpRequest) request).setBody(message); - s.request(1); - } - } - - @Override - protected void doOnError(Throwable t) { - s.cancel(); - if (executed.compareAndSet(false, true)) { - // discard parameters that have already been bound - for (Object toDiscard : routeMatch.getVariableValues().values()) { - if (toDiscard instanceof ReferenceCounted) { - ((ReferenceCounted) toDiscard).release(); - } - if (toDiscard instanceof io.netty.util.ReferenceCounted) { - ((io.netty.util.ReferenceCounted) toDiscard).release(); - } - if (toDiscard instanceof NettyCompletedFileUpload) { - ((NettyCompletedFileUpload) toDiscard).discard(); - } - } - } - for (Sinks.Many subject : downstreamSubscribers) { - subject.tryEmitError(t); - } - emitter.error(t); - } - - @Override - protected void doOnComplete() { - for (Sinks.Many subject : downstreamSubscribers) { - // subjects will ignore the onComplete if they're already done - subject.tryEmitComplete(); - } - executeRoute(); - } - - private Sinks.Many makeDownstreamUnicastProcessor() { - Sinks.Many processor = Sinks.many().unicast().onBackpressureBuffer(); - downstreamSubscribers.add(processor); - return processor; - } - - private void executeRoute() { - if (executed.compareAndSet(false, true)) { - emitter.success(routeMatch); - } - } - }; - } else { - return new CompletionAwareSubscriber() { - private Subscription s; - private AtomicBoolean executed = new AtomicBoolean(false); - - @Override - protected void doOnSubscribe(Subscription subscription) { - this.s = subscription; - subscription.request(1); - } - - @Override - protected void doOnNext(Object message) { - if (message instanceof ByteBufHolder) { - request.addContent((ByteBufHolder) message); - s.request(1); - } else { - ((NettyHttpRequest) request).setBody(message); - s.request(1); - } - ReferenceCountUtil.release(message); - } - - @Override - protected void doOnError(Throwable t) { - s.cancel(); - emitter.error(t); - } - - @Override - protected void doOnComplete() { - if (executed.compareAndSet(false, true)) { - emitter.success(finalRoute); - } - } - }; - } - - } - - private ExecutorService getIoExecutor() { + ExecutorService getIoExecutor() { ExecutorService executor = this.ioExecutor; if (executor == null) { synchronized (this) { // double check diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/binders/CompletableFutureBodyBinder.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/binders/CompletableFutureBodyBinder.java index 3eb9fc35770..c32a5c553db 100644 --- a/http-server-netty/src/main/java/io/micronaut/http/server/netty/binders/CompletableFutureBodyBinder.java +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/binders/CompletableFutureBodyBinder.java @@ -26,6 +26,7 @@ import io.micronaut.http.bind.binders.NonBlockingBodyArgumentBinder; import io.micronaut.http.netty.stream.StreamedHttpRequest; import io.micronaut.http.server.netty.HttpContentProcessor; +import io.micronaut.http.server.netty.HttpContentProcessorAsReactiveProcessor; import io.micronaut.http.server.netty.HttpContentProcessorResolver; import io.micronaut.http.server.netty.NettyHttpRequest; import io.netty.buffer.ByteBufHolder; @@ -83,9 +84,9 @@ public BindingResult bind(ArgumentConversionContext targetType = context.getFirstTypeVariable().orElse(Argument.OBJECT_ARGUMENT); - HttpContentProcessor processor = httpContentProcessorResolver.resolve(nettyHttpRequest, targetType); + HttpContentProcessor processor = httpContentProcessorResolver.resolve(nettyHttpRequest, targetType); - processor.subscribe(new CompletionAwareSubscriber() { + HttpContentProcessorAsReactiveProcessor.asPublisher(processor, nettyHttpRequest).subscribe(new CompletionAwareSubscriber() { @Override protected void doOnSubscribe(Subscription subscription) { subscription.request(1); diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/binders/InputStreamBodyBinder.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/binders/InputStreamBodyBinder.java index b1ed97a6055..9b77fb16970 100644 --- a/http-server-netty/src/main/java/io/micronaut/http/server/netty/binders/InputStreamBodyBinder.java +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/binders/InputStreamBodyBinder.java @@ -23,6 +23,7 @@ import io.micronaut.http.bind.binders.NonBlockingBodyArgumentBinder; import io.micronaut.http.netty.stream.StreamedHttpRequest; import io.micronaut.http.server.netty.HttpContentProcessor; +import io.micronaut.http.server.netty.HttpContentProcessorAsReactiveProcessor; import io.micronaut.http.server.netty.HttpContentProcessorResolver; import io.micronaut.http.server.netty.NettyHttpRequest; import io.micronaut.http.server.netty.NettyHttpServer; @@ -83,12 +84,12 @@ public BindingResult bind(ArgumentConversionContext co PipedOutputStream outputStream = new PipedOutputStream(); try { PipedInputStream inputStream = new PipedInputStream(outputStream) { - private volatile HttpContentProcessor processor; + private volatile HttpContentProcessor processor; private synchronized void init() { if (processor == null) { - processor = (HttpContentProcessor) processorResolver.resolve(nettyHttpRequest, context.getArgument()); - Flux.from(processor) + processor = processorResolver.resolve(nettyHttpRequest, context.getArgument()); + Flux.from(HttpContentProcessorAsReactiveProcessor.asPublisher(processor, nettyHttpRequest)) .publishOn(Schedulers.fromExecutor(executorService)) .subscribe(new CompletionAwareSubscriber() { diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/binders/PublisherBodyBinder.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/binders/PublisherBodyBinder.java index b6ebd5b8829..0bcd9eeec4c 100644 --- a/http-server-netty/src/main/java/io/micronaut/http/server/netty/binders/PublisherBodyBinder.java +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/binders/PublisherBodyBinder.java @@ -15,7 +15,7 @@ */ package io.micronaut.http.server.netty.binders; -import io.micronaut.core.async.subscriber.TypedSubscriber; +import io.micronaut.core.async.subscriber.CompletionAwareSubscriber; import io.micronaut.core.convert.ArgumentConversionContext; import io.micronaut.core.convert.ConversionError; import io.micronaut.core.convert.ConversionService; @@ -26,6 +26,7 @@ import io.micronaut.http.bind.binders.NonBlockingBodyArgumentBinder; import io.micronaut.http.netty.stream.StreamedHttpRequest; import io.micronaut.http.server.netty.HttpContentProcessor; +import io.micronaut.http.server.netty.HttpContentProcessorAsReactiveProcessor; import io.micronaut.http.server.netty.HttpContentProcessorResolver; import io.micronaut.http.server.netty.NettyHttpRequest; import io.micronaut.http.server.netty.NettyHttpServer; @@ -78,10 +79,10 @@ public BindingResult bind(ArgumentConversionContext contex if (nativeRequest instanceof StreamedHttpRequest) { Argument targetType = context.getFirstTypeVariable().orElse(Argument.OBJECT_ARGUMENT); - HttpContentProcessor processor = httpContentProcessorResolver.resolve(nettyHttpRequest, targetType); + HttpContentProcessor processor = httpContentProcessorResolver.resolve(nettyHttpRequest, targetType); //noinspection unchecked - return () -> Optional.of(subscriber -> processor.subscribe(new TypedSubscriber((Argument) context.getArgument()) { + return () -> Optional.of(subscriber -> HttpContentProcessorAsReactiveProcessor.asPublisher(processor.resultType(context.getArgument()), nettyHttpRequest).subscribe(new CompletionAwareSubscriber<>() { Subscription s; @@ -99,6 +100,7 @@ protected void doOnNext(Object message) { if (message instanceof ByteBufHolder) { message = ((ByteBufHolder) message).content(); if (message instanceof EmptyByteBuf) { + s.request(1); return; } } diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/converters/NettyConvertersSpi.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/converters/NettyConvertersSpi.java index 3c9ec4e1161..d4e843e04cd 100644 --- a/http-server-netty/src/main/java/io/micronaut/http/server/netty/converters/NettyConvertersSpi.java +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/converters/NettyConvertersSpi.java @@ -144,6 +144,8 @@ private TypeConverter fileUploadToCompletedFile return Optional.empty(); } + // unlike NettyCompletedAttribute, NettyCompletedFileUpload does a `retain` on + // construct, so we don't need one here return Optional.of(new NettyCompletedFileUpload(object)); } catch (Exception e) { context.reject(e); @@ -162,7 +164,9 @@ private TypeConverter attributeToCompletedPartConverte return Optional.empty(); } - return Optional.of(new NettyCompletedAttribute(object)); + // converter does not claim the input object, so we need to retain it here. it's + // released by NettyCompletedAttribute.get* + return Optional.of(new NettyCompletedAttribute(object.retain())); } catch (Exception e) { context.reject(e); return Optional.empty(); diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/jackson/JsonContentProcessor.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/jackson/JsonContentProcessor.java index 23839383838..62dcc54e530 100644 --- a/http-server-netty/src/main/java/io/micronaut/http/server/netty/jackson/JsonContentProcessor.java +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/jackson/JsonContentProcessor.java @@ -18,11 +18,11 @@ import io.micronaut.core.annotation.Internal; import io.micronaut.core.async.publisher.Publishers; import io.micronaut.core.async.subscriber.CompletionAwareSubscriber; -import io.micronaut.core.async.subscriber.TypedSubscriber; import io.micronaut.core.type.Argument; import io.micronaut.http.MediaType; import io.micronaut.http.server.HttpServerConfiguration; import io.micronaut.http.server.netty.AbstractHttpContentProcessor; +import io.micronaut.http.server.netty.HttpContentProcessor; import io.micronaut.http.server.netty.NettyHttpRequest; import io.micronaut.json.JsonMapper; import io.micronaut.json.tree.JsonNode; @@ -31,9 +31,9 @@ import io.netty.buffer.ByteBufUtil; import io.netty.util.ReferenceCountUtil; import org.reactivestreams.Processor; -import org.reactivestreams.Subscriber; import org.reactivestreams.Subscription; +import java.util.Collection; import java.util.Optional; /** @@ -44,10 +44,12 @@ * @since 1.0 */ @Internal -public class JsonContentProcessor extends AbstractHttpContentProcessor { +public class JsonContentProcessor extends AbstractHttpContentProcessor { private final JsonMapper jsonMapper; private Processor jacksonProcessor; + private Collection out; + private Throwable failure = null; /** * @param nettyHttpRequest The Netty Http request @@ -63,24 +65,17 @@ public JsonContentProcessor( } @Override - protected void doOnSubscribe(Subscription subscription, Subscriber subscriber) { - if (parentSubscription == null) { - return; - } - + public HttpContentProcessor resultType(Argument type) { boolean streamArray = false; boolean isJsonStream = nettyHttpRequest.getContentType() - .map(mediaType -> mediaType.equals(MediaType.APPLICATION_JSON_STREAM_TYPE)) - .orElse(false); - - if (subscriber instanceof TypedSubscriber) { - TypedSubscriber typedSubscriber = (TypedSubscriber) subscriber; - Argument typeArgument = typedSubscriber.getTypeArgument(); + .map(mediaType -> mediaType.equals(MediaType.APPLICATION_JSON_STREAM_TYPE)) + .orElse(false); - Class targetType = typeArgument.getType(); + if (type != null) { + Class targetType = type.getType(); if (Publishers.isConvertibleToPublisher(targetType) && !Publishers.isSingle(targetType)) { - Optional> genericArgument = typeArgument.getFirstTypeVariable(); + Optional> genericArgument = type.getFirstTypeVariable(); if (genericArgument.isPresent() && !Iterable.class.isAssignableFrom(genericArgument.get().getType()) && !isJsonStream) { // if the generic argument is not a iterable type them stream the array into the publisher streamArray = true; @@ -90,75 +85,84 @@ protected void doOnSubscribe(Subscription subscription, Subscriber { }, streamArray); - this.jacksonProcessor.subscribe(new CompletionAwareSubscriber() { + this.jacksonProcessor.subscribe(new CompletionAwareSubscriber<>() { @Override protected void doOnSubscribe(Subscription jsonSubscription) { - - Subscription childSubscription = new Subscription() { - boolean first = true; - - @Override - public synchronized void request(long n) { - // this is a hack. The first item emitted for arrays is already in the buffer - // and not part of the demand, so we have to demand 1 extra - // find a better way in the future - if (first) { - jsonSubscription.request(n < Long.MAX_VALUE ? n + 1 : n); - parentSubscription.request(n < Long.MAX_VALUE ? n + 1 : n); - } else { - jsonSubscription.request(n); - parentSubscription.request(n); - } - } - - @Override - public synchronized void cancel() { - jsonSubscription.cancel(); - parentSubscription.cancel(); - } - }; - subscriber.onSubscribe(childSubscription); + jsonSubscription.request(Long.MAX_VALUE); } @Override protected void doOnNext(JsonNode message) { - subscriber.onNext(message); + if (out == null) { + throw new IllegalStateException("Concurrent access not allowed"); + } + out.add(message); } @Override protected void doOnError(Throwable t) { - subscriber.onError(t); + if (out == null) { + throw new IllegalStateException("Concurrent access not allowed"); + } + failure = t; } @Override protected void doOnComplete() { - subscriber.onComplete(); + if (out == null) { + throw new IllegalStateException("Concurrent access not allowed"); + } } }); + this.jacksonProcessor.onSubscribe(new Subscription() { + @Override + public void request(long n) { + } - jacksonProcessor.onSubscribe(subscription); + @Override + public void cancel() { + // happens on error, ignore + } + }); + return this; } @Override - protected void onData(ByteBufHolder message) { + protected void onData(ByteBufHolder message, Collection out) throws Throwable { + if (jacksonProcessor == null) { + resultType(null); + } + + this.out = out; ByteBuf content = message.content(); try { byte[] bytes = ByteBufUtil.getBytes(content); jacksonProcessor.onNext(bytes); } finally { ReferenceCountUtil.release(content); + this.out = null; + } + Throwable f = failure; + if (f != null) { + failure = null; + throw f; } } @Override - protected void doAfterOnError(Throwable throwable) { - jacksonProcessor.onError(throwable); - } + public void complete(Collection out) throws Throwable { + if (jacksonProcessor == null) { + resultType(null); + } - @Override - protected void doOnComplete() { + this.out = out; jacksonProcessor.onComplete(); - super.doOnComplete(); + this.out = null; + Throwable f = failure; + if (f != null) { + failure = null; + throw f; + } } } diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/multipart/MultipartBodyArgumentBinder.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/multipart/MultipartBodyArgumentBinder.java index e762a3bd24a..bbf2446af27 100644 --- a/http-server-netty/src/main/java/io/micronaut/http/server/netty/multipart/MultipartBodyArgumentBinder.java +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/multipart/MultipartBodyArgumentBinder.java @@ -18,7 +18,7 @@ import io.micronaut.context.BeanLocator; import io.micronaut.context.BeanProvider; import io.micronaut.core.annotation.Internal; -import io.micronaut.core.async.subscriber.TypedSubscriber; +import io.micronaut.core.async.subscriber.CompletionAwareSubscriber; import io.micronaut.core.convert.ArgumentConversionContext; import io.micronaut.core.type.Argument; import io.micronaut.http.HttpRequest; @@ -29,16 +29,14 @@ import io.micronaut.http.server.multipart.MultipartBody; import io.micronaut.http.server.netty.DefaultHttpContentProcessor; import io.micronaut.http.server.netty.HttpContentProcessor; +import io.micronaut.http.server.netty.HttpContentProcessorAsReactiveProcessor; import io.micronaut.http.server.netty.HttpContentSubscriberFactory; import io.micronaut.http.server.netty.NettyHttpRequest; import io.micronaut.http.server.netty.NettyHttpServer; import io.micronaut.web.router.qualifier.ConsumesMediaTypeQualifier; -import io.netty.buffer.ByteBufHolder; -import io.netty.buffer.EmptyByteBuf; import io.netty.handler.codec.http.multipart.Attribute; import io.netty.handler.codec.http.multipart.FileUpload; import io.netty.handler.codec.http.multipart.HttpData; -import io.netty.util.ReferenceCountUtil; import org.reactivestreams.Subscription; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -82,13 +80,13 @@ public BindingResult bind(ArgumentConversionContext processor = beanLocator.findBean(HttpContentSubscriberFactory.class, + HttpContentProcessor processor = beanLocator.findBean(HttpContentSubscriberFactory.class, new ConsumesMediaTypeQualifier<>(MediaType.MULTIPART_FORM_DATA_TYPE)) .map(factory -> factory.build(nettyHttpRequest)) .orElse(new DefaultHttpContentProcessor(nettyHttpRequest, httpServerConfiguration.get())); //noinspection unchecked - return () -> Optional.of(subscriber -> processor.subscribe(new TypedSubscriber((Argument) context.getArgument()) { + return () -> Optional.of(subscriber -> HttpContentProcessorAsReactiveProcessor.asPublisher(processor.resultType(context.getArgument()), nettyHttpRequest).subscribe(new CompletionAwareSubscriber<>() { Subscription s; AtomicLong partsRequested = new AtomicLong(0); @@ -113,27 +111,25 @@ public void cancel() { } @Override - protected void doOnNext(Object message) { + protected void doOnNext(HttpData message) { if (LOG.isTraceEnabled()) { LOG.trace("Server received streaming message for argument [{}]: {}", context.getArgument(), message); } - if (message instanceof ByteBufHolder && ((ByteBufHolder) message).content() instanceof EmptyByteBuf) { + // MicronautHttpData does not support .content() + if (message.length() == 0) { return; } - if (message instanceof HttpData) { - HttpData data = (HttpData) message; - if (data.isCompleted()) { - partsRequested.decrementAndGet(); - if (data instanceof FileUpload) { - subscriber.onNext(new NettyCompletedFileUpload((FileUpload) data, false)); - } else if (data instanceof Attribute) { - subscriber.onNext(new NettyCompletedAttribute((Attribute) data, false)); - } + if (message.isCompleted()) { + partsRequested.decrementAndGet(); + if (message instanceof FileUpload fu) { + subscriber.onNext(new NettyCompletedFileUpload(fu, false)); + } else if (message instanceof Attribute attr) { + subscriber.onNext(new NettyCompletedAttribute(attr, false)); } } - ReferenceCountUtil.release(message); + message.release(); if (partsRequested.get() > 0) { s.request(1); diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/multipart/NettyCompletedFileUpload.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/multipart/NettyCompletedFileUpload.java index 2b2b6243ef7..9ec64e9d756 100644 --- a/http-server-netty/src/main/java/io/micronaut/http/server/netty/multipart/NettyCompletedFileUpload.java +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/multipart/NettyCompletedFileUpload.java @@ -20,15 +20,14 @@ import io.micronaut.core.util.SupplierUtil; import io.micronaut.http.MediaType; import io.micronaut.http.multipart.CompletedFileUpload; +import io.micronaut.http.server.netty.MicronautHttpData; import io.netty.buffer.ByteBuf; -import io.netty.buffer.ByteBufInputStream; import io.netty.buffer.ByteBufUtil; import io.netty.handler.codec.http.multipart.FileUpload; import io.netty.util.ResourceLeakDetector; import io.netty.util.ResourceLeakDetectorFactory; import io.netty.util.ResourceLeakTracker; -import java.io.File; import java.io.IOException; import java.io.InputStream; import java.nio.ByteBuffer; @@ -85,20 +84,11 @@ public NettyCompletedFileUpload(FileUpload fileUpload, boolean controlRelease) { */ @Override public InputStream getInputStream() throws IOException { - if (fileUpload.isInMemory()) { - ByteBuf byteBuf = fileUpload.getByteBuf(); - if (byteBuf == null) { - throw new IOException("The input stream has already been released"); - } - closeTracker(); - return new ByteBufInputStream(byteBuf, controlRelease); - } else { - File file = fileUpload.getFile(); - if (file == null) { - throw new IOException("The input stream has already been released"); - } - closeTracker(); - return new NettyFileUploadInputStream(fileUpload, controlRelease); + try { + return ((MicronautHttpData) fileUpload).toStream(); + } finally { + fileUpload.release(); // it's retained by toStream, and released by InputStream.close + closeTracker(); // discard won't be called } } diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/multipart/NettyPartData.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/multipart/NettyPartData.java index 77efa4045d6..880d5a22014 100644 --- a/http-server-netty/src/main/java/io/micronaut/http/server/netty/multipart/NettyPartData.java +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/multipart/NettyPartData.java @@ -19,7 +19,6 @@ import io.micronaut.core.util.functional.ThrowingSupplier; import io.micronaut.http.MediaType; import io.micronaut.http.multipart.PartData; -import io.micronaut.http.server.netty.HttpDataReference; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufInputStream; import io.netty.buffer.ByteBufUtil; @@ -42,14 +41,6 @@ public class NettyPartData implements PartData { private final Supplier> mediaTypeSupplier; private final ThrowingSupplier byteBufSupplier; - /** - * @param httpData The data reference - * @param component The component reference - */ - public NettyPartData(HttpDataReference httpData, HttpDataReference.Component component) { - this(httpData::getContentType, component::getByteBuf); - } - /** * @param mediaTypeSupplier The content type supplier * @param byteBufSupplier The byte buffer supplier @@ -91,12 +82,8 @@ public byte[] getBytes() throws IOException { */ @Override public ByteBuffer getByteBuffer() throws IOException { - ByteBuf byteBuf = getByteBuf(); - try { - return byteBuf.nioBuffer(); - } finally { - byteBuf.release(); - } + // we need to copy the buffer, so this is as good as it gets + return ByteBuffer.wrap(getBytes()); } /** diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/multipart/NettyStreamingFileUpload.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/multipart/NettyStreamingFileUpload.java index 965838e7e4a..17cc04bd2cb 100644 --- a/http-server-netty/src/main/java/io/micronaut/http/server/netty/multipart/NettyStreamingFileUpload.java +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/multipart/NettyStreamingFileUpload.java @@ -17,8 +17,8 @@ import io.micronaut.core.annotation.Internal; import io.micronaut.core.async.publisher.AsyncSingleResultPublisher; -import io.micronaut.core.util.functional.ThrowingSupplier; import io.micronaut.core.naming.NameUtils; +import io.micronaut.core.util.functional.ThrowingSupplier; import io.micronaut.http.MediaType; import io.micronaut.http.multipart.MultipartException; import io.micronaut.http.multipart.PartData; @@ -48,7 +48,7 @@ * @since 1.0 */ @Internal -public class NettyStreamingFileUpload implements StreamingFileUpload { +public final class NettyStreamingFileUpload implements StreamingFileUpload { private static final Logger LOG = LoggerFactory.getLogger(NettyStreamingFileUpload.class); private io.netty.handler.codec.http.multipart.FileUpload fileUpload; @@ -56,13 +56,7 @@ public class NettyStreamingFileUpload implements StreamingFileUpload { private final HttpServerConfiguration.MultipartConfiguration configuration; private final Flux subject; - /** - * @param httpData The file upload (the data) - * @param multipartConfiguration The multipart configuration - * @param ioExecutor The IO executor - * @param subject The subject - */ - public NettyStreamingFileUpload( + private NettyStreamingFileUpload( io.netty.handler.codec.http.multipart.FileUpload httpData, HttpServerConfiguration.MultipartConfiguration multipartConfiguration, ExecutorService ioExecutor, @@ -213,4 +207,22 @@ private void handleError(Throwable t) { }) ).flux(); } + + /** + * Factory for instances of {@link NettyStreamingFileUpload}. Wraps the fixed requirements that + * don't depend on request. + * + * @param ioExecutor The IO executor + * @param multipartConfiguration The multipart configuration + */ + @Internal + public record Factory( + HttpServerConfiguration.MultipartConfiguration multipartConfiguration, + ExecutorService ioExecutor + ) { + public NettyStreamingFileUpload create(io.netty.handler.codec.http.multipart.FileUpload httpData, + Flux subject) { + return new NettyStreamingFileUpload(httpData, multipartConfiguration, ioExecutor, subject); + } + } } diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/websocket/NettyServerWebSocketUpgradeHandler.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/websocket/NettyServerWebSocketUpgradeHandler.java index 7bbba55c29e..bc90a491a53 100644 --- a/http-server-netty/src/main/java/io/micronaut/http/server/netty/websocket/NettyServerWebSocketUpgradeHandler.java +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/websocket/NettyServerWebSocketUpgradeHandler.java @@ -17,6 +17,7 @@ import io.micronaut.core.annotation.Internal; import io.micronaut.core.annotation.NonNull; +import io.micronaut.core.annotation.Nullable; import io.micronaut.core.execution.ExecutionFlow; import io.micronaut.core.util.StringUtils; import io.micronaut.http.HttpAttributes; @@ -31,9 +32,11 @@ import io.micronaut.http.netty.NettyHttpHeaders; import io.micronaut.http.netty.channel.ChannelPipelineCustomizer; import io.micronaut.http.netty.websocket.WebSocketSessionRepository; +import io.micronaut.http.server.RequestLifecycle; import io.micronaut.http.server.RouteExecutor; import io.micronaut.http.server.netty.NettyEmbeddedServices; import io.micronaut.http.server.netty.NettyHttpRequest; +import io.micronaut.web.router.RouteMatch; import io.micronaut.web.router.Router; import io.micronaut.web.router.UriRouteMatch; import io.micronaut.websocket.CloseReason; @@ -63,7 +66,6 @@ import java.util.List; import java.util.Map; import java.util.Optional; -import java.util.concurrent.atomic.AtomicReference; /** * Handles WebSocket upgrade requests. @@ -123,40 +125,20 @@ private boolean isWebSocketUpgrade(@NonNull NettyHttpRequest request) { protected final void channelRead0(ChannelHandlerContext ctx, NettyHttpRequest msg) { ServerRequestContext.set(msg); - Optional> optionalRoute = router.find(HttpMethod.GET, msg.getUri().toString(), msg) + Optional> optionalRoute = router.find(HttpMethod.GET, msg.getPath(), msg) .filter(rm -> rm.isAnnotationPresent(OnMessage.class) || rm.isAnnotationPresent(OnOpen.class)) .findFirst(); - MutableHttpResponse proceed = HttpResponse.ok(); - - if (optionalRoute.isPresent()) { - UriRouteMatch rm = optionalRoute.get(); - msg.setAttribute(HttpAttributes.ROUTE_MATCH, rm); - msg.setAttribute(HttpAttributes.ROUTE_INFO, rm); - proceed.setAttribute(HttpAttributes.ROUTE_MATCH, rm); - proceed.setAttribute(HttpAttributes.ROUTE_INFO, rm); - } - - AtomicReference> requestReference = new AtomicReference<>(msg); - - ExecutionFlow> responseFlow = ExecutionFlow.async(ctx.channel().eventLoop(), () -> routeExecutor.filterPublisher(requestReference, () -> { - ExecutionFlow> response; - if (optionalRoute.isPresent()) { - response = ExecutionFlow.just(proceed); - } else { - response = routeExecutor.onError(new HttpStatusException(HttpStatus.NOT_FOUND, "WebSocket Not Found"), msg); - } - response.putInContext(ServerRequestContext.KEY, requestReference.get()); - return response; - })); + WebsocketRequestLifecycle requestLifecycle = new WebsocketRequestLifecycle(routeExecutor, msg, optionalRoute.orElse(null)); + ExecutionFlow> responseFlow = ExecutionFlow.async(ctx.channel().eventLoop(), requestLifecycle::handle); responseFlow.onComplete((response, throwable) -> { if (response != null) { - writeResponse(ctx, msg, proceed, response); + writeResponse(ctx, msg, requestLifecycle.shouldProceedNormally, response); } }); } - private void writeResponse(ChannelHandlerContext ctx, NettyHttpRequest msg, MutableHttpResponse proceed, MutableHttpResponse actualResponse) { + private void writeResponse(ChannelHandlerContext ctx, NettyHttpRequest msg, boolean shouldProceedNormally, MutableHttpResponse actualResponse) { if (cancelUpgrade) { if (LOG.isDebugEnabled()) { LOG.debug("Cancelling websocket upgrade, handler was removed while request was processing"); @@ -164,7 +146,7 @@ private void writeResponse(ChannelHandlerContext ctx, NettyHttpRequest msg, M return; } - if (actualResponse == proceed) { + if (shouldProceedNormally) { UriRouteMatch routeMatch = actualResponse.getAttribute(HttpAttributes.ROUTE_MATCH, UriRouteMatch.class) .orElseThrow(() -> new IllegalStateException("Route match is required!")); //Adding new handler to the existing pipeline to handle WebSocket Messages @@ -275,4 +257,41 @@ public void channelInactive(@NonNull ChannelHandlerContext ctx) throws Exception super.channelInactive(ctx); cancelUpgrade = true; } + + private static final class WebsocketRequestLifecycle extends RequestLifecycle { + @Nullable + final RouteMatch route; + + boolean shouldProceedNormally; + + WebsocketRequestLifecycle(RouteExecutor routeExecutor, HttpRequest request, @Nullable RouteMatch route) { + super(routeExecutor, request); + this.route = route; + } + + ExecutionFlow> handle() { + MutableHttpResponse proceed = HttpResponse.ok(); + + if (route != null) { + request().setAttribute(HttpAttributes.ROUTE_MATCH, route); + request().setAttribute(HttpAttributes.ROUTE_INFO, route); + proceed.setAttribute(HttpAttributes.ROUTE_MATCH, route); + proceed.setAttribute(HttpAttributes.ROUTE_INFO, route); + } + + ExecutionFlow> response; + if (route != null) { + response = runWithFilters(() -> ExecutionFlow.just(proceed)); + } else { + response = onError(new HttpStatusException(HttpStatus.NOT_FOUND, "WebSocket Not Found")) + .putInContext(ServerRequestContext.KEY, request()); + } + return response.map(r -> { + if (r == proceed) { + shouldProceedNormally = true; + } + return r; + }); + } + } } diff --git a/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/MicronautHttpDataSpec.groovy b/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/MicronautHttpDataSpec.groovy new file mode 100644 index 00000000000..545221211bb --- /dev/null +++ b/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/MicronautHttpDataSpec.groovy @@ -0,0 +1,30 @@ +package io.micronaut.http.server.netty + +import io.micronaut.http.server.HttpServerConfiguration +import io.netty.buffer.Unpooled +import spock.lang.Specification + +import java.nio.charset.StandardCharsets + +class MicronautHttpDataSpec extends Specification { + def 'add to chunk'(def threshold) { + given: + def cfg = new HttpServerConfiguration.MultipartConfiguration() + cfg.mixed = true + cfg.threshold = threshold + def data = new MicronautHttpData.Factory(cfg, StandardCharsets.UTF_8).createAttribute("") + + when: + data.addContent(Unpooled.wrappedBuffer("foo".bytes), false) + data.addContent(Unpooled.wrappedBuffer("bar".bytes), true) + def chunk1 = data.pollChunk() + then: + chunk1.claim().toString(StandardCharsets.UTF_8) == "foobar" + + cleanup: + data.release() + + where: + threshold << [0, 4, 1000] + } +} diff --git a/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/stack/InvocationStackSpec.groovy b/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/stack/InvocationStackSpec.groovy index a1c97693240..3d7798859c4 100644 --- a/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/stack/InvocationStackSpec.groovy +++ b/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/stack/InvocationStackSpec.groovy @@ -193,6 +193,9 @@ class InvocationStackSpec extends Specification { if (className.startsWith("org.codehaus.groovy") || className.startsWith("org.apache.groovy")) { return true // Spock } + if (className == "reactor.core.publisher.MonoDeferContextual" || className == "reactor.core.publisher.Mono") { + return true // added for reactive filters + } return false } diff --git a/http-server/src/main/java/io/micronaut/http/server/RequestLifecycle.java b/http-server/src/main/java/io/micronaut/http/server/RequestLifecycle.java new file mode 100644 index 00000000000..f17473c4b7f --- /dev/null +++ b/http-server/src/main/java/io/micronaut/http/server/RequestLifecycle.java @@ -0,0 +1,437 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.http.server; + +import io.micronaut.core.annotation.Nullable; +import io.micronaut.core.execution.ExecutionFlow; +import io.micronaut.core.type.ReturnType; +import io.micronaut.core.util.CollectionUtils; +import io.micronaut.http.HttpAttributes; +import io.micronaut.http.HttpMethod; +import io.micronaut.http.HttpRequest; +import io.micronaut.http.HttpResponse; +import io.micronaut.http.HttpStatus; +import io.micronaut.http.MediaType; +import io.micronaut.http.MutableHttpResponse; +import io.micronaut.http.context.ServerRequestContext; +import io.micronaut.http.filter.HttpFilter; +import io.micronaut.http.filter.ServerFilterChain; +import io.micronaut.http.reactive.execution.ReactiveExecutionFlow; +import io.micronaut.http.server.exceptions.ExceptionHandler; +import io.micronaut.http.server.exceptions.response.ErrorContext; +import io.micronaut.http.server.types.files.FileCustomizableResponseType; +import io.micronaut.inject.BeanDefinition; +import io.micronaut.inject.ExecutableMethod; +import io.micronaut.inject.qualifiers.Qualifiers; +import io.micronaut.web.router.RouteInfo; +import io.micronaut.web.router.RouteMatch; +import io.micronaut.web.router.UriRouteMatch; +import org.reactivestreams.Publisher; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import reactor.core.publisher.Mono; +import reactor.util.context.Context; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.CompletionException; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Supplier; + +/** + * This class handles the full route processing lifecycle for a request. + * + * @author Jonas Konrad + * @since 4.0.0 + */ +public class RequestLifecycle { + private static final Logger LOG = LoggerFactory.getLogger(RequestLifecycle.class); + + private final RouteExecutor routeExecutor; + private HttpRequest request; + private Context context = Context.empty(); + private boolean multipartEnabled = true; + + /** + * @param routeExecutor The route executor to use for route resolution + * @param request The request to process + */ + protected RequestLifecycle(RouteExecutor routeExecutor, HttpRequest request) { + this.routeExecutor = Objects.requireNonNull(routeExecutor, "routeExecutor"); + this.request = Objects.requireNonNull(request, "request"); + } + + /** + * The request for this lifecycle. This may be changed by filters. + * + * @return The current request + */ + protected final HttpRequest request() { + return request; + } + + protected final void multipartEnabled(boolean multipartEnabled) { + this.multipartEnabled = multipartEnabled; + } + + /** + * Execute this request normally. + * + * @return The response to the request. + */ + protected final ExecutionFlow> normalFlow() { + ServerRequestContext.set(request); + + MediaType contentType = request.getContentType().orElse(null); + if (!multipartEnabled && + contentType != null && + contentType.equals(MediaType.MULTIPART_FORM_DATA_TYPE)) { + if (LOG.isDebugEnabled()) { + LOG.debug("Multipart uploads have been disabled via configuration. Rejected request for URI {}, method {}, and content type {}", request.getUri(), + request.getMethodName(), contentType); + } + return onStatusError( + HttpResponse.status(HttpStatus.UNSUPPORTED_MEDIA_TYPE), + "Content Type [" + contentType + "] not allowed" + ); + } + + UriRouteMatch routeMatch = routeExecutor.findRouteMatch(request); + if (routeMatch == null) { + //Check if there is a file for the route before returning route not found + FileCustomizableResponseType fileCustomizableResponseType = findFile(); + if (fileCustomizableResponseType != null) { + return runWithFilters(() -> ExecutionFlow.just(HttpResponse.ok(fileCustomizableResponseType))); + } + return onRouteMiss(request); + } + + RouteExecutor.setRouteAttributes(request, routeMatch); + + if (LOG.isTraceEnabled()) { + LOG.trace("Matched route {} - {} to controller {}", request.getMethodName(), request.getUri().getPath(), routeMatch.getDeclaringType()); + } + // all ok proceed to try and execute the route + if (routeMatch.isWebSocketRoute()) { + return onStatusError( + HttpResponse.status(HttpStatus.BAD_REQUEST), + "Not a WebSocket request"); + } + + return runWithFilters(() -> + fulfillArguments(routeMatch) + .flatMap(rm -> routeExecutor.callRoute(context, rm, request)) + .flatMap(this::handleStatusException) + .onErrorResume(this::onErrorNoFilter)); + } + + /** + * Handle an error in this request. Also runs filters for the error handling. + * + * @param t The error + * @return The response for the error + */ + protected final ExecutionFlow> onError(Throwable t) { + return runWithFilters(() -> onErrorNoFilter(t)); + } + + final ExecutionFlow> onErrorNoFilter(Throwable t) { + // find the origination of the route + Optional previousRequestRouteInfo = request.getAttribute(HttpAttributes.ROUTE_INFO, RouteInfo.class); + Class declaringType = previousRequestRouteInfo.map(RouteInfo::getDeclaringType).orElse(null); + + final Throwable cause; + // top level exceptions returned by CompletableFutures. These always wrap the real exception thrown. + if ((t instanceof CompletionException || t instanceof ExecutionException) && t.getCause() != null) { + cause = t.getCause(); + } else { + cause = t; + } + + RouteMatch errorRoute = routeExecutor.findErrorRoute(cause, declaringType, request); + if (errorRoute != null) { + if (routeExecutor.serverConfiguration.isLogHandledExceptions()) { + routeExecutor.logException(cause); + } + try { + return ExecutionFlow.just(errorRoute) + .flatMap(routeMatch -> routeExecutor.callRoute(context, routeMatch, request)) + .flatMap(this::handleStatusException) + .onErrorResume(u -> createDefaultErrorResponseFlow(request, u)) + .>map(response -> { + response.setAttribute(HttpAttributes.EXCEPTION, cause); + return response; + }) + .onErrorResume(throwable -> createDefaultErrorResponseFlow(request, throwable)); + } catch (Throwable e) { + return createDefaultErrorResponseFlow(request, e); + } + } else { + Optional> optionalDefinition = routeExecutor.beanContext.findBeanDefinition(ExceptionHandler.class, Qualifiers.byTypeArgumentsClosest(cause.getClass(), Object.class)); + if (optionalDefinition.isPresent()) { + BeanDefinition handlerDefinition = optionalDefinition.get(); + final Optional> optionalMethod = handlerDefinition.findPossibleMethods("handle").findFirst(); + RouteInfo routeInfo; + if (optionalMethod.isPresent()) { + routeInfo = new ExecutableRouteInfo(optionalMethod.get(), true); + } else { + routeInfo = new RouteInfo<>() { + @Override + public ReturnType getReturnType() { + return ReturnType.of(Object.class); + } + + @Override + public Class getDeclaringType() { + return handlerDefinition.getBeanType(); + } + + @Override + public boolean isErrorRoute() { + return true; + } + + @Override + public List getProduces() { + return MediaType.fromType(getDeclaringType()) + .map(Collections::singletonList) + .orElse(Collections.emptyList()); + } + }; + } + Supplier>> responseSupplier = () -> { + ExceptionHandler handler = routeExecutor.beanContext.getBean(handlerDefinition); + try { + if (routeExecutor.serverConfiguration.isLogHandledExceptions()) { + routeExecutor.logException(cause); + } + Object result = handler.handle(request, cause); + return routeExecutor.createResponseForBody(request, result, routeInfo); + } catch (Throwable e) { + return createDefaultErrorResponseFlow(request, e); + } + }; + ExecutionFlow> responseFlow; + final ExecutorService executor = routeExecutor.findExecutor(routeInfo); + if (executor != null) { + responseFlow = ExecutionFlow.async(executor, responseSupplier); + } else { + responseFlow = responseSupplier.get(); + } + return responseFlow + .>map(response -> { + response.setAttribute(HttpAttributes.EXCEPTION, cause); + return response; + }) + .onErrorResume(throwable -> createDefaultErrorResponseFlow(request, throwable)); + } + if (RouteExecutor.isIgnorable(cause)) { + RouteExecutor.logIgnoredException(cause); + return ExecutionFlow.empty(); + } + return createDefaultErrorResponseFlow(request, cause); + } + } + + /** + * Run the filters for this request, and then run the given flow. + * + * @param downstream Downstream flow, runs inside the filters + * @return Execution flow that completes after the all the filters and the downstream flow + */ + protected final ExecutionFlow> runWithFilters(Supplier>> downstream) { + ServerRequestContext.set(request); + List httpFilters = routeExecutor.router.findFilters(request); + if (httpFilters.isEmpty()) { + return downstream.get(); + } + List filters = new ArrayList<>(httpFilters); + AtomicInteger integer = new AtomicInteger(); + int len = filters.size(); + + ServerFilterChain filterChain = new ServerFilterChain() { + @Override + public Publisher> proceed(io.micronaut.http.HttpRequest request) { + int pos = integer.incrementAndGet(); + if (pos > len) { + throw new IllegalStateException("The FilterChain.proceed(..) method should be invoked exactly once per filter execution. The method has instead been invoked multiple times by an erroneous filter definition."); + } + if (pos == len) { + return Mono.deferContextual(ctx -> { + context = Context.of(ctx); + return Mono.from(ReactiveExecutionFlow.fromFlow(downstream.get()).toPublisher()); + }); + } + HttpFilter httpFilter = filters.get(pos); + RequestLifecycle.this.request = request; + return ReactiveExecutionFlow.fromFlow(triggerFilter(httpFilter, this)).toPublisher(); + } + }; + return triggerFilter(filters.get(0), filterChain); + } + + private ExecutionFlow> triggerFilter(HttpFilter httpFilter, ServerFilterChain filterChain) { + try { + Publisher> publisher = (Publisher>) httpFilter.doFilter(request, filterChain); + publisher = Mono.from(publisher).contextWrite(context); + return ReactiveExecutionFlow.fromPublisher(publisher) + .flatMap(this::handleStatusException) + .onErrorResume(this::onErrorNoFilter); + } catch (Throwable t) { + return onErrorNoFilter(t); + } + } + + private ExecutionFlow> handleStatusException(MutableHttpResponse response) { + RouteInfo routeInfo = response.getAttribute(HttpAttributes.ROUTE_INFO, RouteInfo.class).orElse(null); + if (response.code() >= 400 && routeInfo != null && !routeInfo.isErrorRoute()) { + RouteMatch statusRoute = routeExecutor.findStatusRoute(request, response.status(), routeInfo); + if (statusRoute != null) { + return ExecutionFlow.just(statusRoute) + .flatMap(routeMatch -> routeExecutor.callRoute(Context.empty(), routeMatch, request)) + .flatMap(this::handleStatusException) + .onErrorResume(this::onErrorNoFilter); + } + } + return ExecutionFlow.just(response); + } + + private ExecutionFlow> createDefaultErrorResponseFlow(HttpRequest httpRequest, Throwable cause) { + return ExecutionFlow.just(routeExecutor.createDefaultErrorResponse(httpRequest, cause)); + } + + final ExecutionFlow> onRouteMiss(HttpRequest httpRequest) { + HttpMethod httpMethod = httpRequest.getMethod(); + String requestMethodName = httpRequest.getMethodName(); + MediaType contentType = httpRequest.getContentType().orElse(null); + + if (LOG.isDebugEnabled()) { + LOG.debug("No matching route: {} {}", httpMethod, httpRequest.getUri()); + } + + // if there is no route present try to locate a route that matches a different HTTP method + final List> anyMatchingRoutes = routeExecutor.router + .findAny(httpRequest.getPath(), httpRequest).toList(); + final Collection acceptedTypes = httpRequest.accept(); + final boolean hasAcceptHeader = CollectionUtils.isNotEmpty(acceptedTypes); + + Set acceptableContentTypes = contentType != null ? new HashSet<>(5) : null; + Set allowedMethods = new HashSet<>(5); + Set produceableContentTypes = hasAcceptHeader ? new HashSet<>(5) : null; + for (UriRouteMatch anyRoute : anyMatchingRoutes) { + final String routeMethod = anyRoute.getRoute().getHttpMethodName(); + if (!requestMethodName.equals(routeMethod)) { + allowedMethods.add(routeMethod); + } + if (contentType != null && !anyRoute.doesConsume(contentType)) { + acceptableContentTypes.addAll(anyRoute.getRoute().getConsumes()); + } + if (hasAcceptHeader && !anyRoute.doesProduce(acceptedTypes)) { + produceableContentTypes.addAll(anyRoute.getRoute().getProduces()); + } + } + + if (CollectionUtils.isNotEmpty(acceptableContentTypes)) { + if (LOG.isDebugEnabled()) { + LOG.debug("Content type not allowed for URI {}, method {}, and content type {}", httpRequest.getUri(), + requestMethodName, contentType); + } + return onStatusError( + HttpResponse.status(HttpStatus.UNSUPPORTED_MEDIA_TYPE), + "Content Type [" + contentType + "] not allowed. Allowed types: " + acceptableContentTypes); + } + if (CollectionUtils.isNotEmpty(produceableContentTypes)) { + if (LOG.isDebugEnabled()) { + LOG.debug("Content type not allowed for URI {}, method {}, and content type {}", httpRequest.getUri(), + requestMethodName, contentType); + } + return onStatusError( + HttpResponse.status(HttpStatus.NOT_ACCEPTABLE), + "Specified Accept Types " + acceptedTypes + " not supported. Supported types: " + produceableContentTypes); + } + if (!allowedMethods.isEmpty()) { + if (LOG.isDebugEnabled()) { + LOG.debug("Method not allowed for URI {} and method {}", httpRequest.getUri(), requestMethodName); + } + return onStatusError( + HttpResponse.notAllowedGeneric(allowedMethods), + "Method [" + requestMethodName + "] not allowed for URI [" + httpRequest.getUri() + "]. Allowed methods: " + allowedMethods); + } + return onStatusError( + HttpResponse.status(HttpStatus.NOT_FOUND), + "Page Not Found"); + } + + /** + * Build a status response. Calls any status routes, if available. + * + * @param defaultResponse The default response if there is no status route + * @param message The error message + * @return The computed response flow + */ + protected final ExecutionFlow> onStatusError(MutableHttpResponse defaultResponse, String message) { + Optional> statusRoute = routeExecutor.router.findStatusRoute(defaultResponse.status(), request); + if (statusRoute.isPresent()) { + return runWithFilters(() -> fulfillArguments(statusRoute.get()) + .flatMap(routeMatch -> routeExecutor.callRoute(context, routeMatch, request)) + .flatMap(this::handleStatusException) + .onErrorResume(this::onErrorNoFilter)); + } + if (request.getMethod() != HttpMethod.HEAD) { + defaultResponse = routeExecutor.errorResponseProcessor.processResponse(ErrorContext.builder(request) + .errorMessage(message) + .build(), defaultResponse); + if (defaultResponse.getContentType().isEmpty()) { + defaultResponse = defaultResponse.contentType(MediaType.APPLICATION_JSON_TYPE); + } + } + MutableHttpResponse finalDefaultResponse = defaultResponse; + return runWithFilters(() -> ExecutionFlow.just(finalDefaultResponse)); + } + + /** + * Try to find a static file for this request. If there is a file, filters will still run, but + * only after the call to this method. + * + * @return The file at this path, or {@code null} if none is found + */ + @Nullable + protected FileCustomizableResponseType findFile() { + return null; + } + + /** + * Fulfill the arguments of the given route with data from the request. If necessary, this also + * waits for body data to be available, if there are arguments that need immediate binding.
+ * Note that in some cases some arguments may still be unsatisfied after this, if they are + * missing and are {@link Optional}. They are satisfied with {@link Optional#empty()} later. + * + * @param routeMatch The route match to fulfill + * @return The fulfilled route match, after all necessary data is available + */ + protected ExecutionFlow> fulfillArguments(RouteMatch routeMatch) { + // try to fulfill the argument requirements of the route + return ExecutionFlow.just(routeExecutor.requestArgumentSatisfier.fulfillArgumentRequirements(routeMatch, request(), false)); + } +} diff --git a/http-server/src/main/java/io/micronaut/http/server/RouteExecutor.java b/http-server/src/main/java/io/micronaut/http/server/RouteExecutor.java index 7cd08f95256..a2ef6e0e5d1 100644 --- a/http-server/src/main/java/io/micronaut/http/server/RouteExecutor.java +++ b/http-server/src/main/java/io/micronaut/http/server/RouteExecutor.java @@ -20,14 +20,11 @@ import io.micronaut.core.annotation.Internal; import io.micronaut.core.annotation.NonNull; import io.micronaut.core.annotation.Nullable; -import io.micronaut.http.codec.CodecException; -import io.micronaut.http.reactive.execution.ReactiveExecutionFlow; import io.micronaut.core.async.publisher.Publishers; import io.micronaut.core.execution.ExecutionFlow; import io.micronaut.core.io.buffer.ReferenceCounted; import io.micronaut.core.type.Argument; import io.micronaut.core.type.ReturnType; -import io.micronaut.core.util.CollectionUtils; import io.micronaut.http.HttpAttributes; import io.micronaut.http.HttpHeaders; import io.micronaut.http.HttpMethod; @@ -38,20 +35,15 @@ import io.micronaut.http.MutableHttpHeaders; import io.micronaut.http.MutableHttpResponse; import io.micronaut.http.bind.binders.ContinuationArgumentBinder; +import io.micronaut.http.codec.CodecException; import io.micronaut.http.context.ServerRequestContext; import io.micronaut.http.exceptions.HttpStatusException; -import io.micronaut.http.filter.HttpFilter; -import io.micronaut.http.filter.ServerFilterChain; +import io.micronaut.http.reactive.execution.ReactiveExecutionFlow; import io.micronaut.http.server.binding.RequestArgumentSatisfier; -import io.micronaut.http.server.exceptions.ExceptionHandler; import io.micronaut.http.server.exceptions.response.ErrorContext; import io.micronaut.http.server.exceptions.response.ErrorResponseProcessor; -import io.micronaut.http.server.types.files.FileCustomizableResponseType; -import io.micronaut.inject.BeanDefinition; import io.micronaut.inject.BeanType; -import io.micronaut.inject.ExecutableMethod; import io.micronaut.inject.MethodReference; -import io.micronaut.inject.qualifiers.Qualifiers; import io.micronaut.scheduling.executor.ExecutorSelector; import io.micronaut.web.router.MethodBasedRouteMatch; import io.micronaut.web.router.RouteInfo; @@ -69,23 +61,15 @@ import reactor.core.publisher.Mono; import reactor.core.scheduler.Scheduler; import reactor.core.scheduler.Schedulers; +import reactor.util.context.ContextView; import java.io.IOException; import java.time.LocalDateTime; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Optional; -import java.util.Set; import java.util.concurrent.CompletableFuture; -import java.util.concurrent.CompletionException; -import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.concurrent.atomic.AtomicReference; import java.util.function.Supplier; import java.util.regex.Pattern; @@ -109,11 +93,11 @@ public final class RouteExecutor { private static final Pattern IGNORABLE_ERROR_MESSAGE = Pattern.compile( "^.*(?:connection (?:reset|closed|abort|broken)|broken pipe).*$", Pattern.CASE_INSENSITIVE); - private final Router router; - private final BeanContext beanContext; - private final RequestArgumentSatisfier requestArgumentSatisfier; - private final HttpServerConfiguration serverConfiguration; - private final ErrorResponseProcessor errorResponseProcessor; + final Router router; + final BeanContext beanContext; + final RequestArgumentSatisfier requestArgumentSatisfier; + final HttpServerConfiguration serverConfiguration; + final ErrorResponseProcessor errorResponseProcessor; private final ExecutorSelector executorSelector; private final Optional coroutineHelper; @@ -178,77 +162,19 @@ public Optional getCoroutineHelper() { return coroutineHelper; } - @NonNull - public ExecutionFlow> executeRoute(RequestBodyReader requestBodyReader, - HttpRequest httpRequest, - boolean multipartEnabled, - StaticResourceResponseFinder staticResourceResponseFinder) { - ServerRequestContext.set(httpRequest); - - MediaType contentType = httpRequest.getContentType().orElse(null); - if (!multipartEnabled && - contentType != null && - contentType.equals(MediaType.MULTIPART_FORM_DATA_TYPE)) { - if (LOG.isDebugEnabled()) { - LOG.debug("Multipart uploads have been disabled via configuration. Rejected request for URI {}, method {}, and content type {}", httpRequest.getUri(), - httpRequest.getMethodName(), contentType); - } - return onStatusError( - requestBodyReader, - httpRequest, - HttpResponse.status(HttpStatus.UNSUPPORTED_MEDIA_TYPE), - "Content Type [" + contentType + "] not allowed"); - } - - UriRouteMatch routeMatch = findRouteMatch(httpRequest); - if (routeMatch == null) { - //Check if there is a file for the route before returning route not found - FileCustomizableResponseType fileCustomizableResponseType = staticResourceResponseFinder.find(httpRequest); - if (fileCustomizableResponseType != null) { - return filterPublisher(new AtomicReference<>(httpRequest), () -> ExecutionFlow.just(HttpResponse.ok(fileCustomizableResponseType))); - } - return onRouteMiss(requestBodyReader, httpRequest); - } - - setRouteAttributes(httpRequest, routeMatch); - - if (LOG.isTraceEnabled()) { - String requestPath = httpRequest.getUri().getPath(); - if (routeMatch instanceof MethodBasedRouteMatch) { - LOG.trace("Matched route {} - {} to controller {}", httpRequest.getMethodName(), requestPath, routeMatch.getDeclaringType()); - } else { - LOG.trace("Matched route {} - {}", httpRequest.getMethodName(), requestPath); - } - } - // all ok proceed to try and execute the route - if (routeMatch.isWebSocketRoute()) { - return onStatusError( - requestBodyReader, - httpRequest, - HttpResponse.status(HttpStatus.BAD_REQUEST), - "Not a WebSocket request"); - } - return executeRoute( - new AtomicReference<>(httpRequest), - true, - true, - requestBodyReader.read(routeMatch, httpRequest) - ); - } - @Nullable - private UriRouteMatch findRouteMatch(HttpRequest httpRequest) { + UriRouteMatch findRouteMatch(HttpRequest httpRequest) { UriRouteMatch routeMatch = null; List> uriRoutes = router.findAllClosest(httpRequest); if (uriRoutes.size() > 1) { - throw new DuplicateRouteException(httpRequest.getUri().getPath(), uriRoutes); + throw new DuplicateRouteException(httpRequest.getPath(), uriRoutes); } else if (uriRoutes.size() == 1) { routeMatch = uriRoutes.get(0); } if (routeMatch == null && httpRequest.getMethod().equals(HttpMethod.OPTIONS)) { - List> anyUriRoutes = router.findAny(httpRequest.getUri().toString(), httpRequest).toList(); + List> anyUriRoutes = router.findAny(httpRequest.getPath(), httpRequest).toList(); if (!anyUriRoutes.isEmpty()) { setRouteAttributes(httpRequest, anyUriRoutes.get(0)); httpRequest.setAttribute(AVAILABLE_HTTP_METHODS, anyUriRoutes.stream().map(UriRouteMatch::getHttpMethod).toList()); @@ -257,212 +183,13 @@ private UriRouteMatch findRouteMatch(HttpRequest httpRequest) return routeMatch; } - private ExecutionFlow> onRouteMiss(RequestBodyReader requestBodyReader, - HttpRequest httpRequest) { - HttpMethod httpMethod = httpRequest.getMethod(); - String requestMethodName = httpRequest.getMethodName(); - MediaType contentType = httpRequest.getContentType().orElse(null); - - if (LOG.isDebugEnabled()) { - LOG.debug("No matching route: {} {}", httpMethod, httpRequest.getUri()); - } - - // if there is no route present try to locate a route that matches a different HTTP method - final List> anyMatchingRoutes = router - .findAny(httpRequest.getUri().toString(), httpRequest).toList(); - final Collection acceptedTypes = httpRequest.accept(); - final boolean hasAcceptHeader = CollectionUtils.isNotEmpty(acceptedTypes); - - Set acceptableContentTypes = contentType != null ? new HashSet<>(5) : null; - Set allowedMethods = new HashSet<>(5); - Set produceableContentTypes = hasAcceptHeader ? new HashSet<>(5) : null; - for (UriRouteMatch anyRoute : anyMatchingRoutes) { - final String routeMethod = anyRoute.getRoute().getHttpMethodName(); - if (!requestMethodName.equals(routeMethod)) { - allowedMethods.add(routeMethod); - } - if (contentType != null && !anyRoute.doesConsume(contentType)) { - acceptableContentTypes.addAll(anyRoute.getRoute().getConsumes()); - } - if (hasAcceptHeader && !anyRoute.doesProduce(acceptedTypes)) { - produceableContentTypes.addAll(anyRoute.getRoute().getProduces()); - } - } - - if (CollectionUtils.isNotEmpty(acceptableContentTypes)) { - if (LOG.isDebugEnabled()) { - LOG.debug("Content type not allowed for URI {}, method {}, and content type {}", httpRequest.getUri(), - requestMethodName, contentType); - } - return onStatusError( - requestBodyReader, - httpRequest, - HttpResponse.status(HttpStatus.UNSUPPORTED_MEDIA_TYPE), - "Content Type [" + contentType + "] not allowed. Allowed types: " + acceptableContentTypes); - } - if (CollectionUtils.isNotEmpty(produceableContentTypes)) { - if (LOG.isDebugEnabled()) { - LOG.debug("Content type not allowed for URI {}, method {}, and content type {}", httpRequest.getUri(), - requestMethodName, contentType); - } - return onStatusError( - requestBodyReader, - httpRequest, - HttpResponse.status(HttpStatus.NOT_ACCEPTABLE), - "Specified Accept Types " + acceptedTypes + " not supported. Supported types: " + produceableContentTypes); - } - if (!allowedMethods.isEmpty()) { - if (LOG.isDebugEnabled()) { - LOG.debug("Method not allowed for URI {} and method {}", httpRequest.getUri(), requestMethodName); - } - return onStatusError( - requestBodyReader, - httpRequest, - HttpResponse.notAllowedGeneric(allowedMethods), - "Method [" + requestMethodName + "] not allowed for URI [" + httpRequest.getUri() + "]. Allowed methods: " + allowedMethods); - } - return onStatusError(requestBodyReader, - httpRequest, - HttpResponse.status(HttpStatus.NOT_FOUND), - "Page Not Found"); - } - - public ExecutionFlow> onStatusError(RequestBodyReader requestBodyReader, - HttpRequest httpRequest, - MutableHttpResponse defaultResponse, - String message) { - Optional> statusRoute = router.findStatusRoute(defaultResponse.status(), httpRequest); - if (statusRoute.isPresent()) { - return executeRoute( - new AtomicReference<>(httpRequest), - true, - true, - requestBodyReader.read(statusRoute.get(), httpRequest) - ); - } - if (httpRequest.getMethod() != HttpMethod.HEAD) { - defaultResponse = errorResponseProcessor.processResponse(ErrorContext.builder(httpRequest) - .errorMessage(message) - .build(), defaultResponse); - if (defaultResponse.getContentType().isEmpty()) { - defaultResponse = defaultResponse.contentType(MediaType.APPLICATION_JSON_TYPE); - } - } - MutableHttpResponse finalDefaultResponse = defaultResponse; - return filterPublisher(new AtomicReference<>(httpRequest), () -> ExecutionFlow.just(finalDefaultResponse)); - } - - private void setRouteAttributes(HttpRequest request, UriRouteMatch route) { + static void setRouteAttributes(HttpRequest request, UriRouteMatch route) { request.setAttribute(HttpAttributes.ROUTE, route.getRoute()); request.setAttribute(HttpAttributes.ROUTE_MATCH, route); request.setAttribute(HttpAttributes.ROUTE_INFO, route); request.setAttribute(HttpAttributes.URI_TEMPLATE, route.getRoute().getUriMatchTemplate().toString()); } - /** - * Creates a response publisher to represent the response after being handled - * by any available error route or exception handler. - * - * @param t The exception that occurred - * @param httpRequest The request that caused the exception - * @return A response publisher - */ - public ExecutionFlow> onError(Throwable t, HttpRequest httpRequest) { - // find the origination of the route - Optional previousRequestRouteInfo = httpRequest.getAttribute(HttpAttributes.ROUTE_INFO, RouteInfo.class); - Class declaringType = previousRequestRouteInfo.map(RouteInfo::getDeclaringType).orElse(null); - - final Throwable cause; - // top level exceptions returned by CompletableFutures. These always wrap the real exception thrown. - if ((t instanceof CompletionException || t instanceof ExecutionException) && t.getCause() != null) { - cause = t.getCause(); - } else { - cause = t; - } - - RouteMatch errorRoute = findErrorRoute(cause, declaringType, httpRequest); - if (errorRoute != null) { - if (serverConfiguration.isLogHandledExceptions()) { - logException(cause); - } - try { - AtomicReference> requestReference = new AtomicReference<>(httpRequest); - return executeRoute(requestReference, false, false, ExecutionFlow.just(errorRoute)) - .>map(response -> { - response.setAttribute(HttpAttributes.EXCEPTION, cause); - return response; - }) - .onErrorResume(throwable -> createDefaultErrorResponseFlow(requestReference.get(), throwable)); - } catch (Throwable e) { - return createDefaultErrorResponseFlow(httpRequest, e); - } - } else { - Optional> optionalDefinition = beanContext.findBeanDefinition(ExceptionHandler.class, Qualifiers.byTypeArgumentsClosest(cause.getClass(), Object.class)); - if (optionalDefinition.isPresent()) { - BeanDefinition handlerDefinition = optionalDefinition.get(); - final Optional> optionalMethod = handlerDefinition.findPossibleMethods("handle").findFirst(); - RouteInfo routeInfo; - if (optionalMethod.isPresent()) { - routeInfo = new ExecutableRouteInfo(optionalMethod.get(), true); - } else { - routeInfo = new RouteInfo<>() { - @Override - public ReturnType getReturnType() { - return ReturnType.of(Object.class); - } - - @Override - public Class getDeclaringType() { - return handlerDefinition.getBeanType(); - } - - @Override - public boolean isErrorRoute() { - return true; - } - - @Override - public List getProduces() { - return MediaType.fromType(getDeclaringType()) - .map(Collections::singletonList) - .orElse(Collections.emptyList()); - } - }; - } - Supplier>> responseSupplier = () -> { - ExceptionHandler handler = beanContext.getBean(handlerDefinition); - try { - if (serverConfiguration.isLogHandledExceptions()) { - logException(cause); - } - Object result = handler.handle(httpRequest, cause); - return createResponseForBody(httpRequest, result, routeInfo); - } catch (Throwable e) { - return createDefaultErrorResponseFlow(httpRequest, e); - } - }; - ExecutionFlow> responseFlow; - final ExecutorService executor = findExecutor(routeInfo); - if (executor != null) { - responseFlow = ExecutionFlow.async(executor, responseSupplier); - } else { - responseFlow = responseSupplier.get(); - } - return responseFlow - .>map(response -> { - response.setAttribute(HttpAttributes.EXCEPTION, cause); - return response; - }) - .onErrorResume(throwable -> createDefaultErrorResponseFlow(httpRequest, throwable)); - } - if (isIgnorable(cause)) { - logIgnoredException(cause); - return ExecutionFlow.empty(); - } - return createDefaultErrorResponseFlow(httpRequest, cause); - } - } - /** * Creates a default error response. Should be used when a response could not be retrieved * from any other method. @@ -531,58 +258,6 @@ public MediaType resolveDefaultResponseContentType(HttpRequest request, Route return defaultResponseMediaType; } - /** - * Applies server filters to a request/response. - * - * @param requestReference The request reference - * @param responseFlowSupplier The deferred response flow - * @return A new response publisher that executes server filters - */ - public ExecutionFlow> filterPublisher(AtomicReference> requestReference, - Supplier>> responseFlowSupplier) { - ServerRequestContext.set(requestReference.get()); - List httpFilters = router.findFilters(requestReference.get()); - if (httpFilters.isEmpty()) { - return responseFlowSupplier.get(); - } - List filters = new ArrayList<>(httpFilters); - AtomicInteger integer = new AtomicInteger(); - int len = filters.size(); - - ServerFilterChain filterChain = new ServerFilterChain() { - @Override - public Publisher> proceed(io.micronaut.http.HttpRequest request) { - int pos = integer.incrementAndGet(); - if (pos > len) { - throw new IllegalStateException("The FilterChain.proceed(..) method should be invoked exactly once per filter execution. The method has instead been invoked multiple times by an erroneous filter definition."); - } - if (pos == len) { - return ReactiveExecutionFlow.fromFlow(responseFlowSupplier.get()).toPublisher(); - } - HttpFilter httpFilter = filters.get(pos); - requestReference.set(request); - return ReactiveExecutionFlow.fromFlow( - triggerFilter(requestReference, httpFilter, this) - ).toPublisher(); - } - }; - return triggerFilter(requestReference, filters.get(0), filterChain); - } - - private ExecutionFlow> triggerFilter(AtomicReference> requestReference, HttpFilter httpFilter, ServerFilterChain filterChain) { - try { - return fromPublisher((Publisher>) httpFilter.doFilter(requestReference.get(), filterChain)) - .flatMap(response -> handleStatusException(requestReference.get(), response)) - .onErrorResume(throwable -> onError(throwable, requestReference.get())); - } catch (Throwable t) { - return onError(t, requestReference.get()); - } - } - - private ExecutionFlow> createDefaultErrorResponseFlow(HttpRequest httpRequest, Throwable cause) { - return ExecutionFlow.just(createDefaultErrorResponse(httpRequest, cause)); - } - private MutableHttpResponse newNotFoundError(HttpRequest request) { MutableHttpResponse response = errorResponseProcessor.processResponse( ErrorContext.builder(request) @@ -598,7 +273,7 @@ private Mono> createNotFoundErrorResponsePublisher(HttpRe return Mono.fromCallable(() -> newNotFoundError(httpRequest)); } - private void logException(Throwable cause) { + void logException(Throwable cause) { //handling connection reset by peer exceptions if (isIgnorable(cause)) { logIgnoredException(cause); @@ -609,18 +284,18 @@ private void logException(Throwable cause) { } } - private boolean isIgnorable(Throwable cause) { + static boolean isIgnorable(Throwable cause) { String message = cause.getMessage(); return cause instanceof IOException && message != null && IGNORABLE_ERROR_MESSAGE.matcher(message).matches(); } - private void logIgnoredException(Throwable cause) { + static void logIgnoredException(Throwable cause) { if (LOG.isDebugEnabled()) { LOG.debug("Swallowed an IOException caused by client connectivity: " + cause.getMessage(), cause); } } - private RouteMatch findErrorRoute(Throwable cause, + RouteMatch findErrorRoute(Throwable cause, Class declaringType, HttpRequest httpRequest) { RouteMatch errorRoute = null; @@ -678,24 +353,7 @@ private RouteMatch findErrorRoute(Throwable cause, return errorRoute; } - private ExecutionFlow> handleStatusException(HttpRequest request, - MutableHttpResponse response) { - RouteInfo routeInfo = response.getAttribute(HttpAttributes.ROUTE_INFO, RouteInfo.class).orElse(null); - if (response.code() >= 400 && routeInfo != null && !routeInfo.isErrorRoute()) { - RouteMatch statusRoute = findStatusRoute(request, response.status(), routeInfo); - if (statusRoute != null) { - return executeRoute( - new AtomicReference<>(request), - false, - true, - ExecutionFlow.just(statusRoute) - ); - } - } - return ExecutionFlow.just(response); - } - - private RouteMatch findStatusRoute(HttpRequest incomingRequest, HttpStatus status, RouteInfo finalRoute) { + RouteMatch findStatusRoute(HttpRequest incomingRequest, HttpStatus status, RouteInfo finalRoute) { Class declaringType = finalRoute.getDeclaringType(); // handle re-mapping of errors RouteMatch statusRoute = null; @@ -707,7 +365,7 @@ private RouteMatch findStatusRoute(HttpRequest incomingRequest, HttpS return statusRoute; } - private ExecutorService findExecutor(RouteInfo routeMatch) { + ExecutorService findExecutor(RouteInfo routeMatch) { // Select the most appropriate Executor ExecutorService executor; if (routeMatch instanceof MethodReference) { @@ -765,59 +423,42 @@ private ExecutionFlow> fromImperativeExecute(HttpRequest< return ExecutionFlow.just(forStatus(routeInfo, defaultHttpStatus).body(body)); } - private ExecutionFlow> executeRoute(AtomicReference> requestReference, - boolean executeFilters, - boolean useErrorRoute, - ExecutionFlow> routeMatchFlow) { - Supplier>> responseFlowSupplier = () -> { - return routeMatchFlow.flatMap(routeMatch -> { - ExecutorService executorService = findExecutor(routeMatch); - Supplier>> flowSupplier = () -> executeRouteAndConvertBody(routeMatch, requestReference.get()); - ExecutionFlow> executeMethodResponseFlow; - if (executorService != null) { - if (routeMatch.isSuspended()) { - executeMethodResponseFlow = ReactiveExecutionFlow.fromPublisher(Mono.deferContextual(contextView -> { - coroutineHelper.ifPresent(helper -> helper.setupCoroutineContext(requestReference.get(), contextView)); - return Mono.from( - ReactiveExecutionFlow.fromFlow(flowSupplier.get()).toPublisher() - ); - })) - .putInContext(ServerRequestContext.KEY, requestReference.get()); - } else if (routeMatch.isReactive()) { - executeMethodResponseFlow = ReactiveExecutionFlow.async(executorService, flowSupplier) - .putInContext(ServerRequestContext.KEY, requestReference.get()); - } else { - executeMethodResponseFlow = ExecutionFlow.async(executorService, flowSupplier); - } - } else { - if (routeMatch.isSuspended()) { - executeMethodResponseFlow = ReactiveExecutionFlow.fromPublisher(Mono.deferContextual(contextView -> { - coroutineHelper.ifPresent(helper -> helper.setupCoroutineContext(requestReference.get(), contextView)); - return Mono.from( - ReactiveExecutionFlow.fromFlow(flowSupplier.get()).toPublisher() - ); - })) - .putInContext(ServerRequestContext.KEY, requestReference.get()); - } else if (routeMatch.isReactive()) { - executeMethodResponseFlow = ReactiveExecutionFlow.fromFlow(flowSupplier.get()) - .putInContext(ServerRequestContext.KEY, requestReference.get()); - } else { - executeMethodResponseFlow = flowSupplier.get(); - } - } - return executeMethodResponseFlow; - }).flatMap(response -> handleStatusException(requestReference.get(), response)) - .onErrorResume(throwable -> { - if (useErrorRoute) { - return onError(throwable, requestReference.get()); - } - return createDefaultErrorResponseFlow(requestReference.get(), throwable); - }); - }; - if (!executeFilters) { - return responseFlowSupplier.get(); + ExecutionFlow> callRoute(ContextView contextFromFilter, RouteMatch routeMatch, HttpRequest request) { + ExecutorService executorService = findExecutor(routeMatch); + Supplier>> flowSupplier = () -> executeRouteAndConvertBody(routeMatch, request); + ExecutionFlow> executeMethodResponseFlow; + if (executorService != null) { + if (routeMatch.isSuspended()) { + executeMethodResponseFlow = ReactiveExecutionFlow.fromPublisher(Mono.deferContextual(contextView -> { + coroutineHelper.ifPresent(helper -> helper.setupCoroutineContext(request, contextView)); + return Mono.from( + ReactiveExecutionFlow.fromFlow(flowSupplier.get()).toPublisher() + ); + }).contextWrite(contextFromFilter)) + .putInContext(ServerRequestContext.KEY, request); + } else if (routeMatch.isReactive()) { + executeMethodResponseFlow = ReactiveExecutionFlow.async(executorService, flowSupplier) + .putInContext(ServerRequestContext.KEY, request); + } else { + executeMethodResponseFlow = ExecutionFlow.async(executorService, flowSupplier); + } + } else { + if (routeMatch.isSuspended()) { + executeMethodResponseFlow = ReactiveExecutionFlow.fromPublisher(Mono.deferContextual(contextView -> { + coroutineHelper.ifPresent(helper -> helper.setupCoroutineContext(request, contextView)); + return Mono.from( + ReactiveExecutionFlow.fromFlow(flowSupplier.get()).toPublisher() + ); + }).contextWrite(contextFromFilter)) + .putInContext(ServerRequestContext.KEY, request); + } else if (routeMatch.isReactive()) { + executeMethodResponseFlow = ReactiveExecutionFlow.fromFlow(flowSupplier.get()) + .putInContext(ServerRequestContext.KEY, request); + } else { + executeMethodResponseFlow = flowSupplier.get(); + } } - return filterPublisher(requestReference, responseFlowSupplier); + return executeMethodResponseFlow; } private ExecutionFlow> executeRouteAndConvertBody(RouteMatch routeMatch, HttpRequest httpRequest) { @@ -840,7 +481,7 @@ private ExecutionFlow> executeRouteAndConvertBody(RouteMa } } - private ExecutionFlow> createResponseForBody(HttpRequest request, + ExecutionFlow> createResponseForBody(HttpRequest request, Object body, RouteInfo routeInfo) { ExecutionFlow> outgoingResponse; @@ -1033,43 +674,8 @@ private MutableHttpResponse forStatus(RouteInfo routeMatch, HttpStatu return HttpResponse.status(status); } - private ExecutionFlow fromPublisher(Publisher publisher) { + static ExecutionFlow fromPublisher(Publisher publisher) { return ReactiveExecutionFlow.fromPublisher(publisher); } - - /** - * The request body reader. - */ - public interface RequestBodyReader { - - /** - * Reads the HTTP request body. - * TODO: This needs to be refactored for Micronaut 4 to eliminate the need for the route match. - * - * @param routeMatch The route match - * @param httpRequest The http request - * @return The execution flow carrying the route match - */ - @NonNull - ExecutionFlow> read(@NonNull RouteMatch routeMatch, @NonNull HttpRequest httpRequest); - - } - - /** - * The static resource finder. - */ - public interface StaticResourceResponseFinder { - - /** - * Finds a file response based on the request. - * - * @param httpRequest The request - * @return The file response or null if not found. - */ - @Nullable - FileCustomizableResponseType find(@NonNull HttpRequest httpRequest); - - } - } diff --git a/http-server/src/main/java/io/micronaut/http/server/binding/RequestArgumentSatisfier.java b/http-server/src/main/java/io/micronaut/http/server/binding/RequestArgumentSatisfier.java index bda01e02d35..5fcf9e94c0d 100644 --- a/http-server/src/main/java/io/micronaut/http/server/binding/RequestArgumentSatisfier.java +++ b/http-server/src/main/java/io/micronaut/http/server/binding/RequestArgumentSatisfier.java @@ -131,7 +131,7 @@ protected Optional getValueForArgument(Argument argument, HttpRequest if (argument.getType() == Optional.class) { if (bindingResult.isSatisfied() || satisfyOptionals) { - Optional optionalValue = bindingResult.getValue(); + Optional optionalValue = bindingResult.getValue(); if (optionalValue.isPresent()) { value = optionalValue.get(); } else { @@ -164,7 +164,7 @@ protected Optional getValueForArgument(Argument argument, HttpRequest * @param conversionContext The conversion context * @return The body argument */ - private Object getValueForBlockingBodyArgumentBinder(HttpRequest request, ArgumentBinder argumentBinder, ArgumentConversionContext conversionContext) { - return (UnresolvedArgument) () -> argumentBinder.bind(conversionContext, request); + private UnresolvedArgument getValueForBlockingBodyArgumentBinder(HttpRequest request, ArgumentBinder> argumentBinder, ArgumentConversionContext conversionContext) { + return () -> argumentBinder.bind(conversionContext, request); } } From 939d2fff3294379abec4ce4f52f24deba9139b47 Mon Sep 17 00:00:00 2001 From: Tim Yates Date: Wed, 14 Dec 2022 17:31:48 +0000 Subject: [PATCH 298/743] Allow context annotations at build time (#8485) Otherwise we get failures with Primary when compiled under Graal --- .../io.micronaut/micronaut-inject/native-image.properties | 1 + 1 file changed, 1 insertion(+) diff --git a/inject/src/main/resources/META-INF/native-image/io.micronaut/micronaut-inject/native-image.properties b/inject/src/main/resources/META-INF/native-image/io.micronaut/micronaut-inject/native-image.properties index ddd6f90d09e..034ea152e0a 100644 --- a/inject/src/main/resources/META-INF/native-image/io.micronaut/micronaut-inject/native-image.properties +++ b/inject/src/main/resources/META-INF/native-image/io.micronaut/micronaut-inject/native-image.properties @@ -15,6 +15,7 @@ # Args = -H:EnableURLProtocols=http,https \ + --initialize-at-build-time=io.micronaut.context.annotation \ --initialize-at-build-time=io.micronaut.inject.annotation \ --initialize-at-build-time=io.micronaut.runtime.converters.time \ --initialize-at-run-time=io.micronaut.context.env.CachedEnvironment From 166cd093f8720428f44ee0157d8a186c1a4d1b7a Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Thu, 15 Dec 2022 11:34:27 +0100 Subject: [PATCH 299/743] log: remove failed from healthStatus logging (#8487) --- .../micronaut/management/health/monitor/HealthMonitorTask.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/management/src/main/java/io/micronaut/management/health/monitor/HealthMonitorTask.java b/management/src/main/java/io/micronaut/management/health/monitor/HealthMonitorTask.java index 130ab346c91..2f120bf0ab3 100644 --- a/management/src/main/java/io/micronaut/management/health/monitor/HealthMonitorTask.java +++ b/management/src/main/java/io/micronaut/management/health/monitor/HealthMonitorTask.java @@ -105,7 +105,7 @@ public void onSubscribe(Subscription s) { public void onNext(HealthResult healthResult) { HealthStatus status = healthResult.getStatus(); if (LOG.isDebugEnabled()) { - LOG.debug("Health monitor check failed with status {}", status); + LOG.debug("Health monitor check with status {}", status); } currentHealthStatus.update(status); } From eddefb836a4eea3809d01790149807e8833a34bc Mon Sep 17 00:00:00 2001 From: Graeme Rocher Date: Thu, 15 Dec 2022 15:21:54 +0100 Subject: [PATCH 300/743] fix configuration inheritance path calculation (#8484) fix configuration inheritance path calculation. Fixes #8480 Also correctly include setters of differing types. --- .../ast/utils/AstBeanPropertiesUtils.java | 12 +- .../IntrospectedTypeElementVisitor.java | 1 - .../configuration/ConfigurationUtils.java | 102 ++++++++++---- http-server-netty/build.gradle | 2 + .../http/server/codec/TextStreamCodec.java | 2 + inject-java/build.gradle | 2 + .../ConfigPropertiesParseSpec.groovy | 130 +++++++++++++++++- .../context/DefaultApplicationContext.java | 2 +- .../annotation/ConfigurationReader.java | 1 + 9 files changed, 216 insertions(+), 38 deletions(-) diff --git a/core-processor/src/main/java/io/micronaut/inject/ast/utils/AstBeanPropertiesUtils.java b/core-processor/src/main/java/io/micronaut/inject/ast/utils/AstBeanPropertiesUtils.java index 1b07226b903..3895c2556fc 100644 --- a/core-processor/src/main/java/io/micronaut/inject/ast/utils/AstBeanPropertiesUtils.java +++ b/core-processor/src/main/java/io/micronaut/inject/ast/utils/AstBeanPropertiesUtils.java @@ -102,14 +102,14 @@ public static List resolveBeanProperties(PropertyElementQuery c } else if (NameUtils.isReaderName(methodName, readPrefixes) && methodElement.getParameters().length == 0) { String propertyName = customReaderPropertyNameResolver.apply(methodElement) .orElseGet(() -> NameUtils.getPropertyNameForGetter(methodName, readPrefixes)); - processGetter(props, methodElement, propertyName, isAccessor); + processGetter(props, methodElement, propertyName, isAccessor, configuration); } else if (NameUtils.isWriterName(methodName, writePrefixes) && (methodElement.getParameters().length == 1 || configuration.isAllowSetterWithZeroArgs() && methodElement.getParameters().length == 0 || configuration.isAllowSetterWithMultipleArgs() && methodElement.getParameters().length > 1)) { String propertyName = customWriterPropertyNameResolver.apply(methodElement) .orElseGet(() -> NameUtils.getPropertyNameForSetter(methodName, writePrefixes)); - processSetter(props, methodElement, propertyName, isAccessor); + processSetter(props, methodElement, propertyName, isAccessor, configuration); } } for (FieldElement fieldElement : fieldSupplier.get()) { @@ -230,7 +230,7 @@ private static void processRecord(Map props, MethodEle beanPropertyData.type = beanPropertyData.getter.getGenericReturnType(); } - private static void processGetter(Map props, MethodElement methodElement, String propertyName, boolean isAccessor) { + private static void processGetter(Map props, MethodElement methodElement, String propertyName, boolean isAccessor, PropertyElementQuery configuration) { BeanPropertyData beanPropertyData = props.computeIfAbsent(propertyName, BeanPropertyData::new); beanPropertyData.getter = methodElement; if (isAccessor) { @@ -238,7 +238,7 @@ private static void processGetter(Map props, MethodEle } ClassElement genericReturnType = beanPropertyData.getter.getGenericReturnType(); ClassElement getterType = unwrapType(genericReturnType); - if (beanPropertyData.type != null) { + if (configuration.isIgnoreSettersWithDifferingType() && beanPropertyData.type != null) { if (!getterType.isAssignable(unwrapType(beanPropertyData.type))) { beanPropertyData.getter = null; // not a compatible getter beanPropertyData.readAccessKind = null; @@ -248,7 +248,7 @@ private static void processGetter(Map props, MethodEle } } - private static void processSetter(Map props, MethodElement methodElement, String propertyName, boolean isAccessor) { + private static void processSetter(Map props, MethodElement methodElement, String propertyName, boolean isAccessor, PropertyElementQuery configuration) { BeanPropertyData beanPropertyData = props.computeIfAbsent(propertyName, BeanPropertyData::new); ClassElement paramType = methodElement.getParameters().length == 0 ? PrimitiveElement.BOOLEAN : methodElement.getParameters()[0].getGenericType(); ClassElement setterType = unwrapType(paramType); @@ -263,7 +263,7 @@ private static void processSetter(Map props, MethodEle if (isAccessor) { beanPropertyData.writeAccessKind = BeanProperties.AccessKind.METHOD; } - if (beanPropertyData.type != null) { + if (configuration.isIgnoreSettersWithDifferingType() && beanPropertyData.type != null) { if (setterType != null && !setterType.isAssignable(unwrapType(beanPropertyData.type))) { beanPropertyData.setter = null; // not a compatible setter beanPropertyData.writeAccessKind = null; diff --git a/core-processor/src/main/java/io/micronaut/inject/beans/visitor/IntrospectedTypeElementVisitor.java b/core-processor/src/main/java/io/micronaut/inject/beans/visitor/IntrospectedTypeElementVisitor.java index d95f7a39bd0..dd9799db59a 100644 --- a/core-processor/src/main/java/io/micronaut/inject/beans/visitor/IntrospectedTypeElementVisitor.java +++ b/core-processor/src/main/java/io/micronaut/inject/beans/visitor/IntrospectedTypeElementVisitor.java @@ -43,7 +43,6 @@ import java.util.Optional; import java.util.Set; import java.util.concurrent.atomic.AtomicInteger; -import java.util.stream.Collectors; /** * A {@link TypeElementVisitor} that visits classes annotated with {@link Introspected} and produces diff --git a/core-processor/src/main/java/io/micronaut/inject/configuration/ConfigurationUtils.java b/core-processor/src/main/java/io/micronaut/inject/configuration/ConfigurationUtils.java index 1dc99735e07..bbe16c20bfc 100644 --- a/core-processor/src/main/java/io/micronaut/inject/configuration/ConfigurationUtils.java +++ b/core-processor/src/main/java/io/micronaut/inject/configuration/ConfigurationUtils.java @@ -19,6 +19,7 @@ import io.micronaut.context.annotation.EachProperty; import io.micronaut.core.annotation.AnnotationMetadata; import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.NonNull; import io.micronaut.core.util.StringUtils; import io.micronaut.inject.ast.ClassElement; @@ -34,11 +35,15 @@ @Internal public final class ConfigurationUtils { - private static final String PREFIX_CALCULATED = "prefixCalculated"; + private static final String EACH_PROPERTY_LIST_SUFFIX = "[*]"; + private static final String EACH_PROPERTY_MAP_SUFFIX = ".*"; + + private ConfigurationUtils() { + } public static String buildPropertyPath(ClassElement owningType, ClassElement declaringType, String propertyName) { String typePath; - if (declaringType.hasAnnotation(ConfigurationReader.class)) { + if (declaringType.hasStereotype(ConfigurationReader.class)) { typePath = getRequiredTypePath(declaringType); } else { typePath = getRequiredTypePath(owningType); @@ -54,20 +59,17 @@ public static Optional getTypePath(ClassElement classElement) { if (!classElement.hasStereotype(ConfigurationReader.class)) { return Optional.empty(); } - if (classElement.booleanValue(ConfigurationReader.class, PREFIX_CALCULATED).orElse(false)) { + if (classElement.isTrue(ConfigurationReader.class, ConfigurationReader.PREFIX_CALCULATED)) { return classElement.stringValue(ConfigurationReader.class, ConfigurationReader.PREFIX); } String path = getPath(classElement); path = prependSuperclasses(classElement, path); - Optional inner = classElement.getEnclosingType(); - if (classElement.isInner() && inner.isPresent()) { - ClassElement enclosingType = inner.get(); - String parentPrefix = getTypePath(enclosingType).orElse(""); - path = combinePaths(parentPrefix, path); - } - + path = prependInners(classElement, path); String finalPath = path; - classElement.annotate(ConfigurationReader.class, builder -> builder.member(ConfigurationReader.PREFIX, finalPath).member(PREFIX_CALCULATED, true)); + classElement.annotate(ConfigurationReader.class, builder -> + builder.member(ConfigurationReader.PREFIX, finalPath) + .member(ConfigurationReader.PREFIX_CALCULATED, true) + ); return Optional.of(path); } @@ -95,12 +97,7 @@ private static String getPath(AnnotationMetadata annotationMetadata) { prefix = prefixOptional.orElse(null); } if (annotationMetadata.hasDeclaredAnnotation(EachProperty.class)) { - Objects.requireNonNull(prefix); - if (annotationMetadata.booleanValue(EachProperty.class, "list").orElse(false)) { - return prefix + "[*]"; - } else { - return prefix + ".*"; - } + return computeIterablePrefix(annotationMetadata, prefix); } if (prefix == null) { return ""; @@ -108,30 +105,77 @@ private static String getPath(AnnotationMetadata annotationMetadata) { return prefix; } + @NonNull + private static String computeIterablePrefix(AnnotationMetadata annotationMetadata, String prefix) { + Objects.requireNonNull(prefix); + if (annotationMetadata.booleanValue(EachProperty.class, "list").orElse(false)) { + if (!prefix.endsWith(EACH_PROPERTY_LIST_SUFFIX)) { + return prefix + EACH_PROPERTY_LIST_SUFFIX; + } else { + return prefix; + } + } else { + if (!prefix.endsWith(EACH_PROPERTY_MAP_SUFFIX)) { + return prefix + EACH_PROPERTY_MAP_SUFFIX; + } else { + return prefix; + } + } + } + + private static String prependInners(ClassElement classElement, String path) { + Optional inner = classElement.getEnclosingType(); + while (classElement.isInner() && inner.isPresent()) { + ClassElement enclosingType = inner.get(); + if (enclosingType.isTrue(ConfigurationReader.class, ConfigurationReader.PREFIX_CALCULATED)) { + String parentPrefix = enclosingType.stringValue(ConfigurationReader.class, ConfigurationReader.PREFIX).orElse(""); + path = combinePaths(parentPrefix, path); + break; + } else { + String parentPrefix = getPath(enclosingType); + path = combinePaths(parentPrefix, path); + path = prependSuperclasses(enclosingType, path); + } + inner = enclosingType.getEnclosingType(); + } + return path; + } + private static String prependSuperclasses(ClassElement declaringType, String path) { if (declaringType.isInterface()) { - ClassElement superInterface = resolveSuperInterface(declaringType); - while (superInterface != null) { - Optional parentConfig = getTypePath(superInterface); - if (parentConfig.isPresent()) { - path = combinePaths(parentConfig.get(), path); - } - superInterface = resolveSuperInterface(superInterface); - } + path = prependInterfaces(declaringType, path); } else { Optional optionalSuperType = declaringType.getSuperType(); while (optionalSuperType.isPresent()) { ClassElement superType = optionalSuperType.get(); - Optional parentConfig = getTypePath(superType); - if (parentConfig.isPresent()) { - path = combinePaths(parentConfig.get(), path); + if (superType.isTrue(ConfigurationReader.class, ConfigurationReader.PREFIX_CALCULATED)) { + String parentPrefix = superType.stringValue(ConfigurationReader.class, ConfigurationReader.PREFIX).orElse(""); + path = combinePaths(parentPrefix, path); + break; + } else { + String parentConfig = getPath(superType); + if (StringUtils.isNotEmpty(parentConfig)) { + path = combinePaths(parentConfig, path); + } + optionalSuperType = superType.getSuperType(); } - optionalSuperType = superType.getSuperType(); } } return path; } + private static String prependInterfaces(ClassElement declaringType, String path) { + ClassElement superInterface = resolveSuperInterface(declaringType); + while (superInterface != null) { + String parentConfig = getPath(superInterface); + if (StringUtils.isNotEmpty(parentConfig)) { + path = combinePaths(parentConfig, path); + } + superInterface = resolveSuperInterface(superInterface); + } + return path; + } + private static ClassElement resolveSuperInterface(ClassElement declaringType) { return declaringType.getInterfaces().stream().filter(tm -> tm.hasStereotype(ConfigurationReader.class)).findFirst().orElse(null); } diff --git a/http-server-netty/build.gradle b/http-server-netty/build.gradle index 6c0957edae9..e772ca7a7ca 100644 --- a/http-server-netty/build.gradle +++ b/http-server-netty/build.gradle @@ -85,3 +85,5 @@ dependencies { //} //compileTestGroovy.groovyOptions.forkOptions.jvmArgs = ['-Xdebug', '-Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=5005'] +//compileJava.options.fork = true +//compileJava.options.forkOptions.jvmArgs = ['-Xdebug', '-Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=5005'] diff --git a/http-server/src/main/java/io/micronaut/http/server/codec/TextStreamCodec.java b/http-server/src/main/java/io/micronaut/http/server/codec/TextStreamCodec.java index 72337e31c94..3d6e588ce29 100644 --- a/http-server/src/main/java/io/micronaut/http/server/codec/TextStreamCodec.java +++ b/http-server/src/main/java/io/micronaut/http/server/codec/TextStreamCodec.java @@ -17,6 +17,7 @@ import io.micronaut.context.BeanProvider; import io.micronaut.context.annotation.BootstrapContextCompatible; +import io.micronaut.context.annotation.Requires; import io.micronaut.core.annotation.Internal; import io.micronaut.core.annotation.Nullable; import io.micronaut.core.io.buffer.ByteBuffer; @@ -53,6 +54,7 @@ @Singleton @Internal @BootstrapContextCompatible +@Requires(bean = ByteBufferFactory.class) public class TextStreamCodec implements MediaTypeCodec { public static final String CONFIGURATION_QUALIFIER = "text-stream"; diff --git a/inject-java/build.gradle b/inject-java/build.gradle index e8e9b8795a5..0eb0cf4169d 100644 --- a/inject-java/build.gradle +++ b/inject-java/build.gradle @@ -34,6 +34,8 @@ dependencies { testImplementation files(org.gradle.internal.jvm.Jvm.current().toolsJar) } testImplementation libs.micrometer.core + testImplementation(libs.micronaut.session) + testImplementation(project(":http-server")) testImplementation project(":validation") testImplementation project(":jackson-databind") testImplementation libs.junit.jupiter.api diff --git a/inject-java/src/test/groovy/io/micronaut/inject/configproperties/ConfigPropertiesParseSpec.groovy b/inject-java/src/test/groovy/io/micronaut/inject/configproperties/ConfigPropertiesParseSpec.groovy index 4c43775fec9..7ab88329762 100644 --- a/inject-java/src/test/groovy/io/micronaut/inject/configproperties/ConfigPropertiesParseSpec.groovy +++ b/inject-java/src/test/groovy/io/micronaut/inject/configproperties/ConfigPropertiesParseSpec.groovy @@ -11,9 +11,127 @@ import io.micronaut.annotation.processing.test.AbstractTypeElementSpec import io.micronaut.inject.BeanDefinition import io.micronaut.inject.BeanFactory import io.micronaut.inject.configuration.Engine +import spock.lang.Issue + +import java.time.Duration class ConfigPropertiesParseSpec extends AbstractTypeElementSpec { + @Issue("https://github.com/micronaut-projects/micronaut-core/issues/8480") + void "test configuration properties inheritance for compiled classes - inherited props"() { + when: + def context = buildContext(''' +package test; + +import io.micronaut.context.annotation.ConfigurationProperties; +import io.micronaut.http.server.HttpServerConfiguration; + +@ConfigurationProperties("netty") +class NettyHttpServerConfiguration extends + HttpServerConfiguration { + private Parent parent; + private Child child; + public test.NettyHttpServerConfiguration.Parent getParent() { + return parent; + } + + public void setParent(test.NettyHttpServerConfiguration.Parent parent) { + this.parent = parent; + } + + public void setChild(test.NettyHttpServerConfiguration.Child child) { + this.child = child; + } + public test.NettyHttpServerConfiguration.Child getChild() { + return child; + } + @ConfigurationProperties("child") + public static class Child extends EventLoopConfig { + + } + @ConfigurationProperties("parent") + public static class Parent extends EventLoopConfig { + + } + public abstract static class EventLoopConfig { + private Integer ioRatio; + private int threads; + public void setIoRatio(Integer ioRatio) { + this.ioRatio = ioRatio; + } + public Integer getIoRatio() { + return ioRatio; + } + public void setThreads(int threads) { + this.threads = threads; + } + public int getNumOfThreads() { + return threads; + } + } +} +''') + def config = getBean(context, "test.NettyHttpServerConfiguration") + + then: + config.idleTimeout == Duration.ofSeconds(2) + config.parent.ioRatio == 10 + config.parent.numOfThreads == 5 + config.child.ioRatio == 15 + config.child.numOfThreads == 55 + } + + @Issue("https://github.com/micronaut-projects/micronaut-core/issues/8480") + void "test configuration properties inheritance for compiled classes"() { + when: + def context = buildContext(''' +package test; + +import io.micronaut.context.annotation.ConfigurationProperties; +import io.micronaut.session.http.HttpSessionConfiguration; +import java.net.URI; +import java.util.Optional; +import java.util.List; + +@ConfigurationProperties("test") +class RedisHttpSessionConfiguration extends + HttpSessionConfiguration { + private String writeMode; + private URI uri; + private List uris; + + public void setWriteMode(String writeMode) { + this.writeMode = writeMode; + } + public String getWriteMode() { + return writeMode; + } + + public Optional getUri() { + return Optional.ofNullable(uri); + } + public void setUri(String uri) { + this.uri = URI.create(uri); + } + + public List getUris() { + return uris; + } + + public void setUris(URI... uris) { + this.uris = List.of(uris); + } +} +''') + def config = getBean(context, "test.RedisHttpSessionConfiguration") + + then: + config.writeMode == 'test' + config.uri.isPresent() + config.uri.get() == URI.create('http://localhost:9999') + config.uris == List.of(URI.create('http://localhost:9999')) + } + void "test configuration properties with mixed getters/setters"() { when: def context = buildContext(''' @@ -47,7 +165,17 @@ class MyConfig { @Override protected void configureContext(ApplicationContextBuilder contextBuilder) { - contextBuilder.properties('foo.bar.host':'bar') + contextBuilder.properties( + 'foo.bar.host':'bar', + "micronaut.session.http.test.write-mode": "test", + "micronaut.session.http.test.uri": "http://localhost:9999", + "micronaut.session.http.test.uris": "http://localhost:9999", + "micronaut.server.idle-timeout": "2s", + "micronaut.server.netty.parent.io-ratio": "10", + "micronaut.server.netty.parent.threads": "5", + "micronaut.server.netty.child.io-ratio": "15", + "micronaut.server.netty.child.threads": "55" + ) } void "test inner class paths - pojo inheritance"() { diff --git a/inject/src/main/java/io/micronaut/context/DefaultApplicationContext.java b/inject/src/main/java/io/micronaut/context/DefaultApplicationContext.java index f7bf2f93fff..b172e23c2fe 100644 --- a/inject/src/main/java/io/micronaut/context/DefaultApplicationContext.java +++ b/inject/src/main/java/io/micronaut/context/DefaultApplicationContext.java @@ -423,7 +423,7 @@ private void transformConfigurationReaderBeanDefinition(BeanResolutionContex BeanDefinition candidate, Set> transformedCandidates) { try { - final String prefix = candidate.stringValue(ConfigurationReader.class, "prefix").orElse(null); + final String prefix = candidate.stringValue(ConfigurationReader.class, ConfigurationReader.PREFIX).orElse(null); ConfigurationPath configurationPath = resolutionContext.getConfigurationPath(); if (prefix != null) { diff --git a/inject/src/main/java/io/micronaut/context/annotation/ConfigurationReader.java b/inject/src/main/java/io/micronaut/context/annotation/ConfigurationReader.java index 53bb2fee400..c45224747c1 100644 --- a/inject/src/main/java/io/micronaut/context/annotation/ConfigurationReader.java +++ b/inject/src/main/java/io/micronaut/context/annotation/ConfigurationReader.java @@ -44,6 +44,7 @@ * The base prefix name. */ String BASE_PREFIX = "basePrefix"; + String PREFIX_CALCULATED = "prefixCalculated"; /** * The prefix to use when resolving properties. The prefix should be defined in kebab case. Example: my-app.foo. From cada0a425a1501eedff52284fe42f3594e8b395e Mon Sep 17 00:00:00 2001 From: Graeme Rocher Date: Thu, 15 Dec 2022 15:22:12 +0100 Subject: [PATCH 301/743] Fix NonUniqueBeanException when using FunctionInitializer (#8488) --- .../executor/FunctionInitializer.java | 14 ++++++++- .../executor/FunctionInitializerSpec.java | 29 +++++++++++++++++++ .../micronaut/context/DefaultBeanContext.java | 26 ++++++++++------- 3 files changed, 58 insertions(+), 11 deletions(-) diff --git a/function/src/main/java/io/micronaut/function/executor/FunctionInitializer.java b/function/src/main/java/io/micronaut/function/executor/FunctionInitializer.java index 215609f1b63..b0842c4a96a 100644 --- a/function/src/main/java/io/micronaut/function/executor/FunctionInitializer.java +++ b/function/src/main/java/io/micronaut/function/executor/FunctionInitializer.java @@ -16,14 +16,18 @@ package io.micronaut.function.executor; import io.micronaut.context.ApplicationContext; +import io.micronaut.context.RuntimeBeanDefinition; +import io.micronaut.context.annotation.Secondary; import io.micronaut.core.annotation.Internal; import io.micronaut.core.cli.CommandLine; import io.micronaut.core.reflect.ClassUtils; import io.micronaut.function.LocalFunctionRegistry; import io.micronaut.http.MediaType; import io.micronaut.http.codec.MediaTypeCodecRegistry; +import io.micronaut.inject.annotation.MutableAnnotationMetadata; import java.io.IOException; +import java.util.Collections; import java.util.function.Function; /** @@ -44,7 +48,15 @@ public FunctionInitializer() { ApplicationContext applicationContext = buildApplicationContext(null); startThis(applicationContext); injectThis(applicationContext); - applicationContext.registerSingleton(this, false); + MutableAnnotationMetadata annotationMetadata = new MutableAnnotationMetadata(); + // the runtime registered bean should be lower priority than the existing bean + // used for dependency injecting the instance + annotationMetadata.addDeclaredAnnotation(Secondary.class.getName(), Collections.emptyMap()); + applicationContext.registerBeanDefinition( + RuntimeBeanDefinition.builder(this) + .annotationMetadata(annotationMetadata) + .build() + ); this.closeContext = true; } diff --git a/function/src/test/groovy/io/micronaut/function/executor/FunctionInitializerSpec.java b/function/src/test/groovy/io/micronaut/function/executor/FunctionInitializerSpec.java index b53e79b0c74..8d5ca2c3bf8 100644 --- a/function/src/test/groovy/io/micronaut/function/executor/FunctionInitializerSpec.java +++ b/function/src/test/groovy/io/micronaut/function/executor/FunctionInitializerSpec.java @@ -15,11 +15,13 @@ */ package io.micronaut.function.executor; +import io.micronaut.context.ApplicationContext; import jakarta.inject.Inject; import jakarta.inject.Singleton; import jakarta.annotation.PostConstruct; import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import java.io.IOException; @@ -31,6 +33,12 @@ */ public class FunctionInitializerSpec { + @BeforeEach + void reset() { + MathFunction.initCount.set(0); + MathFunction.injectCount.set(0); + } + @Test public void testFunctionInitializer() { MathFunction mathFunction = new MathFunction(); @@ -39,6 +47,15 @@ public void testFunctionInitializer() { Assertions.assertEquals(2, mathFunction.round(1.6f)); } + @Test + public void testFunctionInitializerSubclass() { + MathFunction mathFunction = new SubMathFunction(); // make anonymous + Assertions.assertEquals(1, MathFunction.initCount.get()); + Assertions.assertEquals(1, MathFunction.injectCount.get()); + Assertions.assertEquals(2, mathFunction.round(1.6f)); + } + + @Singleton public static class MathService { int round(float input) { @@ -46,6 +63,18 @@ int round(float input) { } } + @Singleton + public static class SubMathFunction extends MathFunction { + public SubMathFunction() { + super.injectThis(applicationContext); + } + + @Override + protected void injectThis(ApplicationContext applicationContext) { + // + } + } + @Singleton public static class MathFunction extends FunctionInitializer { static AtomicInteger initCount = new AtomicInteger(0); diff --git a/inject/src/main/java/io/micronaut/context/DefaultBeanContext.java b/inject/src/main/java/io/micronaut/context/DefaultBeanContext.java index 264c4e1ec6f..6c541cc9992 100644 --- a/inject/src/main/java/io/micronaut/context/DefaultBeanContext.java +++ b/inject/src/main/java/io/micronaut/context/DefaultBeanContext.java @@ -1045,26 +1045,32 @@ public T inject(@NonNull T instance) { Objects.requireNonNull(instance, "Instance cannot be null"); Collection candidates = findBeanCandidatesForInstance(instance); + BeanDefinition beanDefinition; if (candidates.size() == 1) { - BeanDefinition beanDefinition = candidates.iterator().next(); + beanDefinition = candidates.iterator().next(); + } else if (!candidates.isEmpty()) { + Argument t = Argument.of(instance.getClass()); + beanDefinition = lastChanceResolve(t, null, true, (Collection) candidates); + } else { + beanDefinition = null; + } + + if (beanDefinition != null && !(beanDefinition instanceof RuntimeBeanDefinition)) { try (BeanResolutionContext resolutionContext = newResolutionContext(beanDefinition, null)) { final BeanKey beanKey = new BeanKey<>(beanDefinition.getBeanType(), null); resolutionContext.addInFlightBean( - beanKey, - new BeanRegistration<>(beanKey, beanDefinition, instance) + beanKey, + new BeanRegistration<>(beanKey, beanDefinition, instance) ); doInject( - resolutionContext, - instance, - beanDefinition + resolutionContext, + instance, + beanDefinition ); } - - } else if (!candidates.isEmpty()) { - final Iterator iterator = candidates.iterator(); - throw new NonUniqueBeanException(instance.getClass(), iterator); } return instance; + } @NonNull From 1e98d85e0529793f80f22ecacc31fdbaaca69dc4 Mon Sep 17 00:00:00 2001 From: Graeme Rocher Date: Thu, 15 Dec 2022 16:22:53 +0100 Subject: [PATCH 302/743] Backport fixes from 4.0.x and fix ConvertibleValuesDeserializer (#8489) * Don't include write only properties in serialization (#8457) Co-authored-by: yawkat --- .../value/ConvertibleMultiValuesMap.java | 17 +++ .../convert/value/ConvertibleValuesMap.java | 18 +++ ...bstractInitializableBeanIntrospection.java | 2 +- .../modules/BeanIntrospectionModule.java | 4 +- .../ConvertibleMultiValuesSerializer.java | 4 +- .../ConvertibleValuesSerializer.java | 2 +- .../BeanIntrospectionModuleSpec.groovy | 135 +++++++++++++++++- .../modules/testclasses/HTTPCheck.java | 46 ++++++ .../modules/testclasses/InstanceInfo.java | 37 +++++ 9 files changed, 258 insertions(+), 7 deletions(-) create mode 100644 jackson-databind/src/test/groovy/io/micronaut/jackson/modules/testclasses/HTTPCheck.java create mode 100644 jackson-databind/src/test/groovy/io/micronaut/jackson/modules/testclasses/InstanceInfo.java diff --git a/core/src/main/java/io/micronaut/core/convert/value/ConvertibleMultiValuesMap.java b/core/src/main/java/io/micronaut/core/convert/value/ConvertibleMultiValuesMap.java index 7354bbdb7a5..c32deebe903 100644 --- a/core/src/main/java/io/micronaut/core/convert/value/ConvertibleMultiValuesMap.java +++ b/core/src/main/java/io/micronaut/core/convert/value/ConvertibleMultiValuesMap.java @@ -25,6 +25,7 @@ import java.util.LinkedHashSet; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; @@ -137,4 +138,20 @@ protected Map> wrapValues(Map> value return Collections.unmodifiableMap(values); } + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + ConvertibleMultiValuesMap that = (ConvertibleMultiValuesMap) o; + return values.equals(that.values); + } + + @Override + public int hashCode() { + return Objects.hash(values); + } } diff --git a/core/src/main/java/io/micronaut/core/convert/value/ConvertibleValuesMap.java b/core/src/main/java/io/micronaut/core/convert/value/ConvertibleValuesMap.java index aa63a3f0ae2..bf0d80485f7 100644 --- a/core/src/main/java/io/micronaut/core/convert/value/ConvertibleValuesMap.java +++ b/core/src/main/java/io/micronaut/core/convert/value/ConvertibleValuesMap.java @@ -23,6 +23,7 @@ import java.util.Collections; import java.util.LinkedHashMap; import java.util.Map; +import java.util.Objects; import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; @@ -104,4 +105,21 @@ public Collection values() { public static ConvertibleValues empty() { return EMPTY; } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + ConvertibleValuesMap that = (ConvertibleValuesMap) o; + return map.equals(that.map); + } + + @Override + public int hashCode() { + return Objects.hash(map); + } } diff --git a/inject/src/main/java/io/micronaut/inject/beans/AbstractInitializableBeanIntrospection.java b/inject/src/main/java/io/micronaut/inject/beans/AbstractInitializableBeanIntrospection.java index 3e8b0ba0b37..5559ed42fc0 100644 --- a/inject/src/main/java/io/micronaut/inject/beans/AbstractInitializableBeanIntrospection.java +++ b/inject/src/main/java/io/micronaut/inject/beans/AbstractInitializableBeanIntrospection.java @@ -422,7 +422,7 @@ public P get(@NonNull B bean) { throw new IllegalArgumentException("Invalid bean [" + bean + "] for type: " + beanType); } if (isWriteOnly()) { - throw new UnsupportedOperationException("Cannot read from a write-only property"); + throw new UnsupportedOperationException("Cannot read from a write-only property: " + getName()); } return dispatchOne(ref.getMethodIndex, bean, null); } diff --git a/jackson-databind/src/main/java/io/micronaut/jackson/modules/BeanIntrospectionModule.java b/jackson-databind/src/main/java/io/micronaut/jackson/modules/BeanIntrospectionModule.java index f374f801583..b31184b09f6 100644 --- a/jackson-databind/src/main/java/io/micronaut/jackson/modules/BeanIntrospectionModule.java +++ b/jackson-databind/src/main/java/io/micronaut/jackson/modules/BeanIntrospectionModule.java @@ -347,7 +347,9 @@ public JsonSerializer build() { final List newProperties = new ArrayList<>(properties); Map> named = new LinkedHashMap<>(); for (BeanProperty beanProperty : beanProperties) { - named.put(getName(config, namingStrategy, beanProperty), beanProperty); + if (!beanProperty.isWriteOnly()) { + named.put(getName(config, namingStrategy, beanProperty), beanProperty); + } } for (int i = 0; i < properties.size(); i++) { final BeanPropertyWriter existing = properties.get(i); diff --git a/jackson-databind/src/main/java/io/micronaut/jackson/serialize/ConvertibleMultiValuesSerializer.java b/jackson-databind/src/main/java/io/micronaut/jackson/serialize/ConvertibleMultiValuesSerializer.java index 9626980c5d1..455412cd895 100644 --- a/jackson-databind/src/main/java/io/micronaut/jackson/serialize/ConvertibleMultiValuesSerializer.java +++ b/jackson-databind/src/main/java/io/micronaut/jackson/serialize/ConvertibleMultiValuesSerializer.java @@ -50,12 +50,12 @@ public void serialize(ConvertibleMultiValues value, JsonGenerator gen, Serial if (len > 0) { gen.writeFieldName(fieldName); if (len == 1) { - gen.writeObject(v.get(0)); + serializers.defaultSerializeValue(v.get(0), gen); } else { gen.writeStartArray(); for (Object o : v) { - gen.writeObject(o); + serializers.defaultSerializeValue(o, gen); } gen.writeEndArray(); } diff --git a/jackson-databind/src/main/java/io/micronaut/jackson/serialize/ConvertibleValuesSerializer.java b/jackson-databind/src/main/java/io/micronaut/jackson/serialize/ConvertibleValuesSerializer.java index b495d828249..3ebadecf0c9 100644 --- a/jackson-databind/src/main/java/io/micronaut/jackson/serialize/ConvertibleValuesSerializer.java +++ b/jackson-databind/src/main/java/io/micronaut/jackson/serialize/ConvertibleValuesSerializer.java @@ -47,7 +47,7 @@ public void serialize(ConvertibleValues value, JsonGenerator gen, SerializerP Object v = entry.getValue(); if (v != null) { gen.writeFieldName(fieldName); - gen.writeObject(v); + serializers.defaultSerializeValue(v, gen); } } gen.writeEndObject(); diff --git a/jackson-databind/src/test/groovy/io/micronaut/jackson/modules/BeanIntrospectionModuleSpec.groovy b/jackson-databind/src/test/groovy/io/micronaut/jackson/modules/BeanIntrospectionModuleSpec.groovy index 0e09ea561d4..ce054da2a81 100644 --- a/jackson-databind/src/test/groovy/io/micronaut/jackson/modules/BeanIntrospectionModuleSpec.groovy +++ b/jackson-databind/src/test/groovy/io/micronaut/jackson/modules/BeanIntrospectionModuleSpec.groovy @@ -32,16 +32,145 @@ import io.micronaut.http.hateoas.JsonError import io.micronaut.jackson.JacksonConfiguration import io.micronaut.jackson.modules.testcase.EmailTemplate import io.micronaut.jackson.modules.testcase.Notification -import io.micronaut.jackson.modules.wrappers.* +import io.micronaut.jackson.modules.testclasses.HTTPCheck +import io.micronaut.jackson.modules.testclasses.InstanceInfo +import io.micronaut.jackson.modules.wrappers.BooleanWrapper +import io.micronaut.jackson.modules.wrappers.DoubleWrapper +import io.micronaut.jackson.modules.wrappers.IntWrapper +import io.micronaut.jackson.modules.wrappers.IntegerWrapper +import io.micronaut.jackson.modules.wrappers.LongWrapper +import io.micronaut.jackson.modules.wrappers.StringWrapper import spock.lang.Issue -import spock.lang.Unroll import spock.lang.Specification +import spock.lang.Unroll import java.beans.ConstructorProperties import java.time.LocalDateTime class BeanIntrospectionModuleSpec extends Specification { + void "test serialize/deserialize wrap/unwrap - simple"() { + given: + ApplicationContext ctx = ApplicationContext.run( + 'jackson.deserialization.UNWRAP_ROOT_VALUE': true, + 'jackson.serialization.WRAP_ROOT_VALUE': true + ) + ObjectMapper objectMapper = ctx.getBean(ObjectMapper) + + when: + Author author = new Author(name:"Bob") + + def result = objectMapper.writeValueAsString(author) + + then: + result == '{"Author":{"name":"Bob"}}' + + when: + def read = objectMapper.readValue(result, Author) + + then: + author == read + + } + + void "test serialize/deserialize wrap/unwrap -* complex"() { + given: + ApplicationContext ctx = ApplicationContext.run( + 'jackson.deserialization.UNWRAP_ROOT_VALUE': true, + 'jackson.serialization.WRAP_ROOT_VALUE': true + ) + ObjectMapper objectMapper = ctx.getBean(ObjectMapper) + + when: + HTTPCheck check = new HTTPCheck(headers:[ + Accept:['application/json', 'application/xml'] + ] ) + + def result = objectMapper.writeValueAsString(check) + + then: + result == '{"HTTPCheck":{"Header":{"Accept":["application/json","application/xml"]}}}' + + when: + def read = objectMapper.readValue(result, HTTPCheck) + + then: + check == read + + } + + void "test serialize/deserialize wrap/unwrap -* constructors"() { + given: + ApplicationContext ctx = ApplicationContext.run( + 'jackson.deserialization.UNWRAP_ROOT_VALUE': true, + 'jackson.serialization.WRAP_ROOT_VALUE': true + ) + ObjectMapper objectMapper = ctx.getBean(ObjectMapper) + + when: + IntrospectionCreator check = new IntrospectionCreator("test") + + def result = objectMapper.writeValueAsString(check) + + then: + result == '{"IntrospectionCreator":{"label":"TEST"}}' + + when: + def read = objectMapper.readValue(result, IntrospectionCreator) + + then: + check == read + + } + + void "test serialize/deserialize wrap/unwrap -* constructors & JsonRootName"() { + given: + ApplicationContext ctx = ApplicationContext.run( + 'jackson.deserialization.UNWRAP_ROOT_VALUE': true, + 'jackson.serialization.WRAP_ROOT_VALUE': true + ) + ObjectMapper objectMapper = ctx.getBean(ObjectMapper) + + when: + InstanceInfo check = new InstanceInfo("test") + + def result = objectMapper.writeValueAsString(check) + + then: + result == '{"instance":{"hostName":"test"}}' + + when: + def read = objectMapper.readValue(result, InstanceInfo) + + then: + check == read + + } + + + void "test serialize/deserialize convertible values"() { + given: + ApplicationContext ctx = ApplicationContext.run() + ObjectMapper objectMapper = ctx.getBean(ObjectMapper) + + when: + HTTPCheck check = new HTTPCheck(headers:[ + Accept:['application/json', 'application/xml'] + ] ) + + def result = objectMapper.writeValueAsString(check) + + then: + result == '{"Header":{"Accept":["application/json","application/xml"]}}' + + when: + def read = objectMapper.readValue(result, HTTPCheck) + + then: + check.header.getAll("Accept") == read.header.getAll("Accept") + + } + void "Bean introspection works with a bean without JsonIgnore annotations"() { given: ApplicationContext ctx = ApplicationContext.run() @@ -598,6 +727,7 @@ class BeanIntrospectionModuleSpec extends Specification { } @Introspected + @EqualsAndHashCode static class Author { String name } @@ -826,6 +956,7 @@ class BeanIntrospectionModuleSpec extends Specification { } @Introspected + @EqualsAndHashCode static class IntrospectionCreator { private final String name diff --git a/jackson-databind/src/test/groovy/io/micronaut/jackson/modules/testclasses/HTTPCheck.java b/jackson-databind/src/test/groovy/io/micronaut/jackson/modules/testclasses/HTTPCheck.java new file mode 100644 index 00000000000..a011da667b3 --- /dev/null +++ b/jackson-databind/src/test/groovy/io/micronaut/jackson/modules/testclasses/HTTPCheck.java @@ -0,0 +1,46 @@ +package io.micronaut.jackson.modules.testclasses; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import com.fasterxml.jackson.databind.annotation.JsonNaming; +import io.micronaut.core.annotation.Introspected; +import io.micronaut.core.convert.value.ConvertibleMultiValues; + +import java.util.List; +import java.util.Map; +import java.util.Objects; + +@JsonNaming(PropertyNamingStrategies.UpperCamelCaseStrategy.class) +@Introspected +public class HTTPCheck { + private ConvertibleMultiValues headers = ConvertibleMultiValues.empty(); + + public ConvertibleMultiValues getHeader() { + return headers; + } + + /** + * @param headers The headers + */ + @JsonProperty("Header") + public void setHeaders(Map> headers) { + if (headers == null) { + this.headers = ConvertibleMultiValues.empty(); + } else { + this.headers = ConvertibleMultiValues.of(headers); + } + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + HTTPCheck httpCheck = (HTTPCheck) o; + return headers.equals(httpCheck.headers); + } + + @Override + public int hashCode() { + return Objects.hash(headers); + } +} diff --git a/jackson-databind/src/test/groovy/io/micronaut/jackson/modules/testclasses/InstanceInfo.java b/jackson-databind/src/test/groovy/io/micronaut/jackson/modules/testclasses/InstanceInfo.java new file mode 100644 index 00000000000..57509ffc3e0 --- /dev/null +++ b/jackson-databind/src/test/groovy/io/micronaut/jackson/modules/testclasses/InstanceInfo.java @@ -0,0 +1,37 @@ +package io.micronaut.jackson.modules.testclasses; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonRootName; +import io.micronaut.core.annotation.Introspected; + +import java.util.Objects; + +@JsonRootName("instance") +@Introspected +public class InstanceInfo { + private final String hostName; + + @JsonCreator + InstanceInfo( + @JsonProperty("hostName") String hostName) { + this.hostName = hostName; + } + + public String getHostName() { + return hostName; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + InstanceInfo that = (InstanceInfo) o; + return hostName.equals(that.hostName); + } + + @Override + public int hashCode() { + return Objects.hash(hostName); + } +} From 04084aeabfb8aaf22fd6daef7cbc9f4ceb883bd6 Mon Sep 17 00:00:00 2001 From: Dean Wette Date: Fri, 16 Dec 2022 06:01:17 -0600 Subject: [PATCH 303/743] feat: Overload DELETE method for URI object (#8486) closes #8483 --- .../io/micronaut/http/client/HttpDeleteSpec.groovy | 10 +++++++++- .../src/main/java/io/micronaut/http/HttpRequest.java | 12 ++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/http-client/src/test/groovy/io/micronaut/http/client/HttpDeleteSpec.groovy b/http-client/src/test/groovy/io/micronaut/http/client/HttpDeleteSpec.groovy index fde844c1d18..cbd51f7ff78 100644 --- a/http-client/src/test/groovy/io/micronaut/http/client/HttpDeleteSpec.groovy +++ b/http-client/src/test/groovy/io/micronaut/http/client/HttpDeleteSpec.groovy @@ -45,7 +45,7 @@ class HttpDeleteSpec extends Specification { @Inject MyDeleteClient myDeleteClient - void "test http delete"() { + void "test http delete with URI string"() { when: HttpResponse res = client.toBlocking().exchange(HttpRequest.DELETE('/delete/simple')) @@ -53,6 +53,14 @@ class HttpDeleteSpec extends Specification { res.status == HttpStatus.NO_CONTENT } + void "test http delete with URI object"() { + when: + HttpResponse res = client.toBlocking().exchange(HttpRequest.DELETE(URI.create('/delete/simple'))) + + then: + res.status == HttpStatus.NO_CONTENT + } + void "test http delete with blocking client"() { when: HttpResponse res = client.toBlocking().exchange(HttpRequest.DELETE('/delete/simple')) diff --git a/http/src/main/java/io/micronaut/http/HttpRequest.java b/http/src/main/java/io/micronaut/http/HttpRequest.java index d65553ffb3b..2079b99c656 100644 --- a/http/src/main/java/io/micronaut/http/HttpRequest.java +++ b/http/src/main/java/io/micronaut/http/HttpRequest.java @@ -388,6 +388,18 @@ static MutableHttpRequest DELETE(String uri) { return DELETE(uri, null); } + /** + * Return a {@link MutableHttpRequest} that executes an {@link HttpMethod#DELETE} request for the given URI. + * + * @param uri The URI + * @param The Http request type + * @return The {@link MutableHttpRequest} instance + * @see HttpRequestFactory + */ + static MutableHttpRequest DELETE(URI uri) { + return DELETE(uri.toString(), null); + } + /** * Create a new {@link MutableHttpRequest} for the given method and URI. * From 401cc8935704f03e9b90e7624c1bf0b2533a1470 Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Fri, 16 Dec 2022 18:33:22 +0100 Subject: [PATCH 304/743] fix: TextStreamCodec requires bean of type ByteBufferFactory (#8494) --- .../java/io/micronaut/http/server/codec/TextStreamCodec.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/http-server/src/main/java/io/micronaut/http/server/codec/TextStreamCodec.java b/http-server/src/main/java/io/micronaut/http/server/codec/TextStreamCodec.java index 72337e31c94..69649fd59ce 100644 --- a/http-server/src/main/java/io/micronaut/http/server/codec/TextStreamCodec.java +++ b/http-server/src/main/java/io/micronaut/http/server/codec/TextStreamCodec.java @@ -18,6 +18,7 @@ import io.micronaut.context.BeanProvider; import io.micronaut.context.annotation.BootstrapContextCompatible; import io.micronaut.core.annotation.Internal; +import io.micronaut.context.annotation.Requires; import io.micronaut.core.annotation.Nullable; import io.micronaut.core.io.buffer.ByteBuffer; import io.micronaut.core.io.buffer.ByteBufferFactory; @@ -53,6 +54,7 @@ @Singleton @Internal @BootstrapContextCompatible +@Requires(bean = ByteBufferFactory.class) public class TextStreamCodec implements MediaTypeCodec { public static final String CONFIGURATION_QUALIFIER = "text-stream"; From 5cca96a60057f245538cb365edbc42de6daf121e Mon Sep 17 00:00:00 2001 From: micronaut-build <65172877+micronaut-build@users.noreply.github.com> Date: Fri, 16 Dec 2022 15:18:48 -0500 Subject: [PATCH 305/743] Bump micronaut-data to 3.9.0 (#8481) --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index f91da45c6c7..f767c4c1383 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -73,7 +73,7 @@ managed-micronaut-cache = "3.5.0" managed-micronaut-cassandra = "5.1.1" managed-micronaut-coherence = "3.5.1" managed-micronaut-crac = "1.0.1" -managed-micronaut-data = "3.8.1" +managed-micronaut-data = "3.9.0" managed-micronaut-discovery = "3.2.0" managed-micronaut-elasticsearch = "4.3.0" managed-micronaut-email = "1.5.0" From 214051e4b5f12ff3f2626a8e535614f40b61ff02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Duarte?= Date: Fri, 16 Dec 2022 20:20:16 +0000 Subject: [PATCH 306/743] Ensure Logback is refreshed before application starts (#8238) --- .../loggers/impl/LogbackLoggingSystem.java | 28 +++ .../io/micronaut/logging/LoggingSystem.java | 7 + .../logging/LoggingSystemException.java | 49 +++++ .../PropertiesLoggingLevelsConfigurer.java | 10 +- .../logging/impl/LogbackLoggingSystem.java | 30 +++ .../micronaut-runtime/reflect-config.json | 180 ++++++++++++++++++ .../LogbackLogLevelConfigurerSpec.groovy | 74 +++++-- .../src/test/resources/logback-env-test.xml | 14 ++ 8 files changed, 370 insertions(+), 22 deletions(-) create mode 100644 runtime/src/main/java/io/micronaut/logging/LoggingSystemException.java create mode 100644 runtime/src/main/resources/META-INF/native-image/io.micronaut/micronaut-runtime/reflect-config.json create mode 100644 runtime/src/test/resources/logback-env-test.xml diff --git a/management/src/main/java/io/micronaut/management/endpoint/loggers/impl/LogbackLoggingSystem.java b/management/src/main/java/io/micronaut/management/endpoint/loggers/impl/LogbackLoggingSystem.java index 30d1a61fd15..4f0ef79d681 100644 --- a/management/src/main/java/io/micronaut/management/endpoint/loggers/impl/LogbackLoggingSystem.java +++ b/management/src/main/java/io/micronaut/management/endpoint/loggers/impl/LogbackLoggingSystem.java @@ -18,17 +18,24 @@ import ch.qos.logback.classic.Level; import ch.qos.logback.classic.Logger; import ch.qos.logback.classic.LoggerContext; +import ch.qos.logback.classic.util.ContextInitializer; +import ch.qos.logback.core.joran.spi.JoranException; +import io.micronaut.context.annotation.Property; import io.micronaut.context.annotation.Replaces; import io.micronaut.context.annotation.Requires; import io.micronaut.core.annotation.NonNull; import io.micronaut.logging.LogLevel; +import io.micronaut.logging.LoggingSystemException; import io.micronaut.management.endpoint.loggers.LoggerConfiguration; import io.micronaut.management.endpoint.loggers.LoggersEndpoint; import io.micronaut.management.endpoint.loggers.ManagedLoggingSystem; import jakarta.inject.Singleton; import org.slf4j.LoggerFactory; +import java.net.URL; import java.util.Collection; +import java.util.Objects; +import java.util.Optional; import java.util.stream.Collectors; /** @@ -42,6 +49,10 @@ @Requires(classes = ch.qos.logback.classic.LoggerContext.class) @Replaces(io.micronaut.logging.impl.LogbackLoggingSystem.class) public class LogbackLoggingSystem implements ManagedLoggingSystem, io.micronaut.logging.LoggingSystem { + private static final String DEFAULT_LOGBACK_LOCATION = "logback.xml"; + + @Property(name = "logger.config") + private Optional logbackXmlLocation; @Override @NonNull @@ -106,4 +117,21 @@ private static Level toLevel(LogLevel logLevel) { return Level.valueOf(logLevel.name()); } } + + @Override + public void refresh() { + LoggerContext context = getLoggerContext(); + context.reset(); + String logbackXml = logbackXmlLocation.orElse(DEFAULT_LOGBACK_LOCATION); + URL resource = getClass().getClassLoader().getResource(logbackXml); + if (Objects.isNull(resource)) { + throw new LoggingSystemException("Resource " + logbackXml + " not found"); + } + + try { + new ContextInitializer(context).configureByResource(resource); + } catch (JoranException e) { + throw new LoggingSystemException("Error while refreshing Logback", e); + } + } } diff --git a/runtime/src/main/java/io/micronaut/logging/LoggingSystem.java b/runtime/src/main/java/io/micronaut/logging/LoggingSystem.java index ba0fbb4e1ea..6a490aaea23 100644 --- a/runtime/src/main/java/io/micronaut/logging/LoggingSystem.java +++ b/runtime/src/main/java/io/micronaut/logging/LoggingSystem.java @@ -38,4 +38,11 @@ public interface LoggingSystem { */ void setLogLevel(@NotBlank String name, @NotNull LogLevel level); + /** + * Refreshes Logging System with the goal of cleaning its internal caches. + * + */ + default void refresh() { + + } } diff --git a/runtime/src/main/java/io/micronaut/logging/LoggingSystemException.java b/runtime/src/main/java/io/micronaut/logging/LoggingSystemException.java new file mode 100644 index 00000000000..d4cff1c77a8 --- /dev/null +++ b/runtime/src/main/java/io/micronaut/logging/LoggingSystemException.java @@ -0,0 +1,49 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.logging; + +/** + * Thrown when something goes wrong on Logging System. + * + * @author Luis Duarte + * @since 3.8 + */ +public class LoggingSystemException extends RuntimeException { + + /** + * Create exception with detailed message and cause. + * + * @param message the detail message (which is saved for later retrieval + * by the {@link #getMessage()} method). + * @param cause the cause (which is saved for later retrieval by the + * {@link #getCause()} method). (A {@code null} value is + * permitted, and indicates that the cause is nonexistent or + * unknown.) + */ + public LoggingSystemException(String message, Throwable cause) { + super(message, cause); + } + + /** + * Create exception with detailed message and cause. + * + * @param message the detail message (which is saved for later retrieval + * by the {@link #getMessage()} method). + */ + public LoggingSystemException(String message) { + super(message); + } +} diff --git a/runtime/src/main/java/io/micronaut/logging/PropertiesLoggingLevelsConfigurer.java b/runtime/src/main/java/io/micronaut/logging/PropertiesLoggingLevelsConfigurer.java index 729903b807a..ab4f4401d67 100644 --- a/runtime/src/main/java/io/micronaut/logging/PropertiesLoggingLevelsConfigurer.java +++ b/runtime/src/main/java/io/micronaut/logging/PropertiesLoggingLevelsConfigurer.java @@ -64,6 +64,7 @@ final class PropertiesLoggingLevelsConfigurer implements ApplicationEventListene public PropertiesLoggingLevelsConfigurer(Environment environment, List loggingSystems) { this.environment = environment; this.loggingSystems = loggingSystems; + initLogging(); configureLogLevels(); } @@ -74,9 +75,12 @@ public PropertiesLoggingLevelsConfigurer(Environment environment, List key.startsWith(LOGGER_LEVELS_PROPERTY_PREFIX))) { - configureLogLevels(); - } + initLogging(); + configureLogLevels(); + } + + private void initLogging() { + this.loggingSystems.forEach(LoggingSystem::refresh); } private void configureLogLevels() { diff --git a/runtime/src/main/java/io/micronaut/logging/impl/LogbackLoggingSystem.java b/runtime/src/main/java/io/micronaut/logging/impl/LogbackLoggingSystem.java index 9a7cf719bf3..5c984f0c3c0 100644 --- a/runtime/src/main/java/io/micronaut/logging/impl/LogbackLoggingSystem.java +++ b/runtime/src/main/java/io/micronaut/logging/impl/LogbackLoggingSystem.java @@ -15,12 +15,20 @@ */ package io.micronaut.logging.impl; +import java.net.URL; +import java.util.Objects; +import java.util.Optional; + import ch.qos.logback.classic.Level; import ch.qos.logback.classic.LoggerContext; +import ch.qos.logback.classic.util.ContextInitializer; +import ch.qos.logback.core.joran.spi.JoranException; +import io.micronaut.context.annotation.Property; import io.micronaut.context.annotation.Requires; import io.micronaut.core.annotation.Internal; import io.micronaut.logging.LogLevel; import io.micronaut.logging.LoggingSystem; +import io.micronaut.logging.LoggingSystemException; import jakarta.inject.Singleton; import org.slf4j.LoggerFactory; @@ -35,11 +43,33 @@ @Internal public final class LogbackLoggingSystem implements LoggingSystem { + private static final String DEFAULT_LOGBACK_LOCATION = "logback.xml"; + + @Property(name = "logger.config") + private Optional logbackXmlLocation; + @Override public void setLogLevel(String name, LogLevel level) { getLoggerContext().getLogger(name).setLevel(toLevel(level)); } + @Override + public void refresh() { + LoggerContext context = getLoggerContext(); + context.reset(); + String logbackXml = logbackXmlLocation.orElse(DEFAULT_LOGBACK_LOCATION); + URL resource = getClass().getClassLoader().getResource(logbackXml); + if (Objects.isNull(resource)) { + throw new LoggingSystemException("Resource " + logbackXml + " not found"); + } + + try { + new ContextInitializer(context).configureByResource(resource); + } catch (JoranException e) { + throw new LoggingSystemException("Error while refreshing Logback", e); + } + } + /** * @return The logback {@link LoggerContext} */ diff --git a/runtime/src/main/resources/META-INF/native-image/io.micronaut/micronaut-runtime/reflect-config.json b/runtime/src/main/resources/META-INF/native-image/io.micronaut/micronaut-runtime/reflect-config.json new file mode 100644 index 00000000000..7eac1203c1e --- /dev/null +++ b/runtime/src/main/resources/META-INF/native-image/io.micronaut/micronaut-runtime/reflect-config.json @@ -0,0 +1,180 @@ +[ + { + "name": "org.slf4j.impl.StaticLoggerBinder", + "allDeclaredConstructors": true + }, + { + "name": "ch.qos.logback.classic.pattern.DateConverter", + "allDeclaredConstructors": true + }, + { + "name": "ch.qos.logback.classic.pattern.MessageConverter", + "allDeclaredConstructors": true + }, + { + "name": "ch.qos.logback.classic.pattern.ThrowableProxyConverter", + "allDeclaredConstructors": true + }, + { + "name": "ch.qos.logback.classic.pattern.NopThrowableInformationConverter", + "allDeclaredConstructors": true + }, + { + "name": "ch.qos.logback.classic.pattern.ContextNameConverter", + "allDeclaredConstructors": true + }, + { + "name": "ch.qos.logback.core.pattern.color.BoldYellowCompositeConverter", + "allDeclaredConstructors": true + }, + { + "name": "ch.qos.logback.classic.pattern.LoggerConverter", + "allDeclaredConstructors": true + }, + { + "name": "ch.qos.logback.core.pattern.ReplacingCompositeConverter", + "allDeclaredConstructors": true + }, + { + "name": "ch.qos.logback.core.pattern.color.BoldBlueCompositeConverter", + "allDeclaredConstructors": true + }, + { + "name": "ch.qos.logback.core.pattern.color.CyanCompositeConverter", + "allDeclaredConstructors": true + }, + { + "name": "ch.qos.logback.core.pattern.color.RedCompositeConverter", + "allDeclaredConstructors": true + }, + { + "name": "ch.qos.logback.core.pattern.color.WhiteCompositeConverter", + "allDeclaredConstructors": true + }, + { + "name": "ch.qos.logback.classic.pattern.PropertyConverter", + "allDeclaredConstructors": true + }, + { + "name": "ch.qos.logback.classic.pattern.ExtendedThrowableProxyConverter", + "allDeclaredConstructors": true + }, + { + "name": "ch.qos.logback.classic.pattern.RootCauseFirstThrowableProxyConverter", + "allDeclaredConstructors": true + }, + { + "name": "ch.qos.logback.classic.pattern.MethodOfCallerConverter", + "allDeclaredConstructors": true + }, + { + "name": "ch.qos.logback.classic.pattern.LevelConverter", + "allDeclaredConstructors": true + }, + { + "name": "ch.qos.logback.core.pattern.IdentityCompositeConverter", + "allDeclaredConstructors": true + }, + { + "name": "ch.qos.logback.core.pattern.color.BoldWhiteCompositeConverter", + "allDeclaredConstructors": true + }, + { + "name": "ch.qos.logback.classic.pattern.MarkerConverter", + "allDeclaredConstructors": true + }, + { + "name": "ch.qos.logback.core.pattern.color.BoldCyanCompositeConverter", + "allDeclaredConstructors": true + }, + { + "name": "ch.qos.logback.core.pattern.color.BoldMagentaCompositeConverter", + "allDeclaredConstructors": true + }, + { + "name": "ch.qos.logback.classic.pattern.RelativeTimeConverter", + "allDeclaredConstructors": true + }, + { + "name": "ch.qos.logback.core.pattern.color.MagentaCompositeConverter", + "allDeclaredConstructors": true + }, + { + "name": "ch.qos.logback.classic.pattern.ClassOfCallerConverter", + "allDeclaredConstructors": true + }, + { + "name": "ch.qos.logback.classic.pattern.LineOfCallerConverter", + "allDeclaredConstructors": true + }, + { + "name": "ch.qos.logback.classic.pattern.FileOfCallerConverter", + "allDeclaredConstructors": true + }, + { + "name": "ch.qos.logback.core.pattern.color.BoldGreenCompositeConverter", + "allDeclaredConstructors": true + }, + { + "name": "ch.qos.logback.classic.pattern.LocalSequenceNumberConverter", + "allDeclaredConstructors": true + }, + { + "name": "ch.qos.logback.core.pattern.color.YellowCompositeConverter", + "allDeclaredConstructors": true + }, + { + "name": "ch.qos.logback.classic.pattern.ExtendedThrowableProxyConverter", + "allDeclaredConstructors": true + }, + { + "name": "ch.qos.logback.classic.pattern.color.HighlightingCompositeConverter", + "allDeclaredConstructors": true + }, + { + "name": "ch.qos.logback.core.pattern.color.GrayCompositeConverter", + "allDeclaredConstructors": true + }, + { + "name": "ch.qos.logback.classic.pattern.MDCConverter", + "allDeclaredConstructors": true + }, + { + "name": "ch.qos.logback.classic.pattern.ClassOfCallerConverter", + "allDeclaredConstructors": true + }, + { + "name": "ch.qos.logback.core.pattern.color.BoldRedCompositeConverter", + "allDeclaredConstructors": true + }, + { + "name": "ch.qos.logback.core.pattern.color.GreenCompositeConverter", + "allDeclaredConstructors": true + }, + { + "name": "ch.qos.logback.core.pattern.color.BlackCompositeConverter", + "allDeclaredConstructors": true + }, + { + "name": "ch.qos.logback.classic.pattern.ThreadConverter", + "allDeclaredConstructors": true + }, + { + "name": "ch.qos.logback.classic.pattern.LineSeparatorConverter", + "allDeclaredConstructors": true + }, + { + "name": "ch.qos.logback.classic.encoder.PatternLayoutEncoder", + "allPublicMethods":true, + "allDeclaredConstructors": true + }, + { + "name": "ch.qos.logback.core.ConsoleAppender", + "allPublicMethods":true, + "allDeclaredConstructors": true + }, + { + "name": "com.sun.org.apache.xerces.internal.jaxp.SAXParserFactoryImpl", + "allDeclaredConstructors": true + } +] \ No newline at end of file diff --git a/runtime/src/test/groovy/io/micronaut/logging/LogbackLogLevelConfigurerSpec.groovy b/runtime/src/test/groovy/io/micronaut/logging/LogbackLogLevelConfigurerSpec.groovy index e836f362897..949225d8974 100644 --- a/runtime/src/test/groovy/io/micronaut/logging/LogbackLogLevelConfigurerSpec.groovy +++ b/runtime/src/test/groovy/io/micronaut/logging/LogbackLogLevelConfigurerSpec.groovy @@ -2,6 +2,10 @@ package io.micronaut.logging import ch.qos.logback.classic.Level import ch.qos.logback.classic.Logger +import ch.qos.logback.classic.LoggerContext +import ch.qos.logback.classic.spi.ILoggingEvent +import ch.qos.logback.core.Appender +import ch.qos.logback.core.ConsoleAppender import com.github.stefanbirkner.systemlambda.SystemLambda import io.micronaut.context.ApplicationContext import io.micronaut.context.env.PropertySource @@ -17,22 +21,17 @@ class LogbackLogLevelConfigurerSpec extends Specification { @Unroll void 'test that log levels on logger "#loggerName" can be configured via properties'() { given: - ((Logger) LoggerFactory.getLogger('foo.bar1')).setLevel(Level.DEBUG) - ((Logger) LoggerFactory.getLogger('foo.bar2')).setLevel(Level.DEBUG) - ((Logger) LoggerFactory.getLogger('foo.bar3')).setLevel(Level.ERROR) - ((Logger) LoggerFactory.getLogger('foo.barBaz')).setLevel(Level.WARN) - ((Logger) LoggerFactory.getLogger('ignoring.error')).setLevel(Level.INFO) + def loggerLevels = [ + 'logger.levels.aaa.bbb.ccc' : 'ERROR', + 'logger.levels.foo.bar1' : 'DEBUG', + 'logger.levels.foo.bar2' : 'INFO', + 'logger.levels.foo.bar3' : '', + 'logger.levels.foo.barBaz' : 'INFO', + 'logger.levels.ignoring.error': 'OFF', + ] when: - ApplicationContext context = ApplicationContext.run( - [ - 'logger.levels.aaa.bbb.ccc' : 'ERROR', - 'logger.levels.foo.bar2' : 'INFO', - 'logger.levels.foo.bar3' : '', - 'logger.levels.foo.barBaz' : 'INFO', - 'logger.levels.ignoring.error': 'OFF', - ] - ) + ApplicationContext context = ApplicationContext.run(loggerLevels) then: ((Logger) LoggerFactory.getLogger(loggerName)).getLevel() == expectedLevel @@ -89,13 +88,11 @@ logger: } void 'test that log levels can be configured via environment variables'() { - given: - ((Logger) LoggerFactory.getLogger('foo.bar1')).setLevel(Level.DEBUG) - ((Logger) LoggerFactory.getLogger('foo.bar2')).setLevel(Level.DEBUG) - when: ApplicationContext context = ApplicationContext.builder().build() - SystemLambda.withEnvironmentVariable("LOGGER_LEVELS_FOO_BAR2", "INFO") + SystemLambda + .withEnvironmentVariable("LOGGER_LEVELS_FOO_BAR1", "DEBUG") + .and("LOGGER_LEVELS_FOO_BAR2", "INFO") .execute(() -> { context.start() }) @@ -142,4 +139,43 @@ logger: 'foo.bar3' | Level.INFO } + void 'logging refresh is properly called on application start'() { + given: + def map = new YamlPropertySourceLoader().read("application.yml", ''' +logger: + config: logback-env-test.xml + levels: + foo.bar4: ERROR +'''.bytes) + + ApplicationContext context = ApplicationContext.builder() + .propertySources(PropertySource.of("application", map, YamlPropertySourceLoader.DEFAULT_POSITION)) + .build() + + when: + SystemLambda.withEnvironmentVariable("SOME_ENV_VAR", "FOO") + .execute(() -> { + context.start() + LoggerFactory.getLogger("foo.bar4").error("Some error") + }) + + ConsoleAppender consoleAppender + for (Logger logger : ((LoggerContext)LoggerFactory.getILoggerFactory()).getLoggerList()) { + for (Iterator> index = logger.iteratorForAppenders(); index.hasNext();) { + Appender appender = index.next() + if (appender.getName() == "STDOUT") { + consoleAppender = (ConsoleAppender) appender + break + } + } + } + + then: + consoleAppender != null + consoleAppender.getEncoder().getProperties()["pattern"].contains("[FOO]") + + cleanup: + context.close() + } + } diff --git a/runtime/src/test/resources/logback-env-test.xml b/runtime/src/test/resources/logback-env-test.xml new file mode 100644 index 00000000000..06019fc984f --- /dev/null +++ b/runtime/src/test/resources/logback-env-test.xml @@ -0,0 +1,14 @@ + + + + + + %d{HH:mm:ss.SSS} [%thread][${SOME_ENV_VAR}] %-5level %logger{36} - %msg%n + + + + + + + From 9ccf925224897d1b7a61195b5af6126069b36747 Mon Sep 17 00:00:00 2001 From: altro3 Date: Sat, 17 Dec 2022 03:23:30 +0700 Subject: [PATCH 307/743] Added lost slf4j and logback artifacts. (#8138) Added log4j bom dependency to final micronaut bom. --- gradle/libs.versions.toml | 15 +++++++++++++-- .../processing/visitor/AbstractJavaElement.java | 1 - 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index f767c4c1383..06e4b634479 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -22,7 +22,6 @@ jsr107 = "1.1.1" javax-el = "3.0.1-b12" javax-el-impl = "2.2.1-b05" logbook-netty = "2.14.0" -log4j = "2.19.0" selenium = "3.141.59" smallrye = "5.5.0" systemlambda = "1.2.1" @@ -134,6 +133,7 @@ managed-reactor = "3.4.23" managed-rxjava1 = "1.3.8" managed-rxjava1-interop = "0.13.7" managed-slf4j = "1.7.36" +managed-log4j = "2.19.0" managed-spock = "2.0-groovy-3.0" managed-spotbugs = "4.7.1" managed-spring = "5.3.23" @@ -191,6 +191,7 @@ boms-micronaut-test-resources = { module = "io.micronaut.testresources:micronaut boms-groovy = { module = "org.codehaus.groovy:groovy-bom", version.ref = "managed-groovy" } boms-jackson = { module = "com.fasterxml.jackson:jackson-bom", version.ref = "managed-jackson" } +boms-log4j = { module = "org.apache.logging.log4j:log4j-bom", version.ref = "managed-log4j" } boms-junit5 = { module = "org.junit:junit-bom", version.ref = "managed-junit5" } boms-kotlin = { module = "org.jetbrains.kotlin:kotlin-bom", version.ref = "managed-kotlin" } boms-kotlin-coroutines = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-bom", version.ref = "managed-kotlin-coroutines" } @@ -266,6 +267,8 @@ managed-jsr305 = { module = "com.google.code.findbugs:jsr305", version.ref = "ma managed-kafka212 = { module = "org.apache.kafka:kafka_2.12", version.ref = "managed-kafka" } managed-logback = { module = "ch.qos.logback:logback-classic", version.ref = "managed-logback" } +managed-logback-core = { module = "ch.qos.logback:logback-core", version.ref = "managed-logback" } +managed-logback-access = { module = "ch.qos.logback:logback-access", version.ref = "managed-logback" } managed-lombok = { module = "org.projectlombok:lombok", version.ref = "managed-lombok" } @@ -331,6 +334,14 @@ managed-rxjava1-interop = { module = "com.github.akarnokd:rxjava2-interop", vers managed-slf4j = { module = "org.slf4j:slf4j-api", version.ref = "managed-slf4j" } managed-slf4j-simple = { module = "org.slf4j:slf4j-simple", version.ref = "managed-slf4j" } +managed-slf4j-nop = { module = "org.slf4j:slf4j-nop", version.ref = "managed-slf4j" } +managed-slf4j-log4j12 = { module = "org.slf4j:slf4j-log4j12", version.ref = "managed-slf4j" } +managed-slf4j-reload4j = { module = "org.slf4j:slf4j-reload4j", version.ref = "managed-slf4j" } +managed-slf4j-ext = { module = "org.slf4j:slf4j-ext", version.ref = "managed-slf4j" } +managed-slf4j-jcl-over-slf4j = { module = "org.slf4j:jcl-over-slf4j", version.ref = "managed-slf4j" } +managed-slf4j-log4j-over-slf4j = { module = "org.slf4j:log4j-over-slf4j", version.ref = "managed-slf4j" } +managed-slf4j-jul-to-slf4j = { module = "org.slf4j:jul-to-slf4j", version.ref = "managed-slf4j" } +managed-slf4j-osgi-over-slf4j = { module = "org.slf4j:osgi-over-slf4j", version.ref = "managed-slf4j" } managed-snakeyaml = { module = "org.yaml:snakeyaml", version.ref = "managed-snakeyaml" } @@ -409,7 +420,7 @@ kotlinx-coroutines-rx2 = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-rx kotlinx-coroutines-slf4j = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-slf4j", version.ref = "managed-kotlin-coroutines" } kotlinx-coroutines-reactor = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-reactor", version.ref = "managed-kotlin-coroutines" } -log4j = { module = "org.apache.logging.log4j:log4j-core", version.ref = "log4j" } +log4j = { module = "org.apache.logging.log4j:log4j-core", version.ref = "managed-log4j" } logbook-netty = { module = "org.zalando:logbook-netty", version.ref = "logbook-netty" } diff --git a/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/AbstractJavaElement.java b/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/AbstractJavaElement.java index 54282c3b9b3..a2269fb546d 100644 --- a/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/AbstractJavaElement.java +++ b/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/AbstractJavaElement.java @@ -17,7 +17,6 @@ import io.micronaut.annotation.processing.AnnotationUtils; import io.micronaut.core.annotation.AnnotationMetadata; -import io.micronaut.core.annotation.AnnotationMetadataDelegate; import io.micronaut.core.annotation.AnnotationValue; import io.micronaut.core.annotation.AnnotationValueBuilder; import io.micronaut.core.annotation.NonNull; From ffba62af33ef3aa896af530c3f6ff8784887ea99 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20S=C3=A1nchez-Mariscal?= Date: Mon, 19 Dec 2022 13:22:00 +0100 Subject: [PATCH 308/743] Update Test Resources to 1.2.3 (#8497) --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 06e4b634479..de2a09d00fd 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -117,7 +117,7 @@ managed-micronaut-servlet = "3.3.2" managed-micronaut-spring = "4.3.1" managed-micronaut-sql = "4.7.2" managed-micronaut-test = "3.8.0" -managed-micronaut-test-resources = "1.1.3" +managed-micronaut-test-resources = "1.2.3" managed-micronaut-toml = "1.1.1" managed-micronaut-tracing = "4.4.0" managed-micronaut-tracing-legacy = "3.2.7" From 23e99c6bcb86b8ed526475461cb344ae4d93dcf8 Mon Sep 17 00:00:00 2001 From: micronaut-build <65172877+micronaut-build@users.noreply.github.com> Date: Mon, 19 Dec 2022 07:42:39 -0500 Subject: [PATCH 309/743] ci: use temurin, provenance, Gradle 7.6 build scan step (#8091) --- .github/workflows/central-sync.yml | 2 +- .github/workflows/graalvm.yml | 20 ++++- .github/workflows/gradle.yml | 29 ++++--- .github/workflows/publish-snapshot.yml | 2 +- .github/workflows/release.yml | 92 ++++++++++++++++++++++- .github/workflows/sonarqube.yml | 2 +- config/checkstyle/checkstyle.xml | 6 +- gradle/wrapper/gradle-wrapper.jar | Bin 60756 -> 61574 bytes gradle/wrapper/gradle-wrapper.properties | 3 +- gradlew | 12 ++- gradlew.bat | 1 + 11 files changed, 142 insertions(+), 27 deletions(-) diff --git a/.github/workflows/central-sync.yml b/.github/workflows/central-sync.yml index add5cc43de0..ea5a5aa5bf9 100644 --- a/.github/workflows/central-sync.yml +++ b/.github/workflows/central-sync.yml @@ -22,7 +22,7 @@ jobs: - name: Set up JDK uses: actions/setup-java@v3 with: - distribution: 'adopt' + distribution: 'temurin' java-version: '17' - name: Publish to Sonatype OSSRH env: diff --git a/.github/workflows/graalvm.yml b/.github/workflows/graalvm.yml index 7443db9c146..0726bcfc327 100644 --- a/.github/workflows/graalvm.yml +++ b/.github/workflows/graalvm.yml @@ -21,9 +21,6 @@ jobs: matrix: java: ['17'] graalvm: ['latest', 'dev'] - include: - - graalvm: 'latest' - java: '11' steps: # https://github.com/actions/virtual-environments/issues/709 - name: Free disk space @@ -45,7 +42,10 @@ jobs: version: ${{ matrix.graalvm }} java-version: ${{ matrix.java }} components: 'native-image' + - name: Setup Gradle + uses: gradle/gradle-build-action@v2.3.3 - name: Build with Gradle + id: gradle run: | if ./gradlew tasks --no-daemon --all | grep -w "testNativeImage" then @@ -59,9 +59,21 @@ jobs: GRADLE_ENTERPRISE_CACHE_USERNAME: ${{ secrets.GRADLE_ENTERPRISE_CACHE_USERNAME }} GRADLE_ENTERPRISE_CACHE_PASSWORD: ${{ secrets.GRADLE_ENTERPRISE_CACHE_PASSWORD }} PREDICTIVE_TEST_SELECTION: "${{ github.event_name == 'pull_request' && 'true' || 'false' }}" + - name: Add build scan URL as PR comment + uses: actions/github-script@v5 + if: github.event_name == 'pull_request' && failure() + with: + github-token: ${{secrets.GITHUB_TOKEN}} + script: | + github.rest.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body: '❌ ${{ github.workflow }} ${{ matrix.java }} ${{ matrix.graalvm }} failed: ${{ steps.gradle.outputs.build-scan-url }}' + }) - name: Publish Test Report if: always() - uses: mikepenz/action-junit-report@v3.5.2 + uses: mikepenz/action-junit-report@v3.6.2 with: check_name: GraalVM CE CI / Test Report (Java ${{ matrix.java }}) report_paths: '**/build/test-results/test/TEST-*.xml' diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index 6f29dbb7281..1aebe19ca26 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -29,17 +29,13 @@ jobs: sudo apt-get clean df -h - uses: actions/checkout@v3 - - uses: actions/cache@v3 - with: - path: ~/.gradle/caches - key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle') }} - restore-keys: | - ${{ runner.os }}-gradle- - name: Set up JDK uses: actions/setup-java@v3 with: - distribution: 'adopt' + distribution: 'temurin' java-version: ${{ matrix.java }} + - name: Setup Gradle + uses: gradle/gradle-build-action@v2.3.3 - name: Optional setup step env: GRADLE_ENTERPRISE_ACCESS_KEY: ${{ secrets.GRADLE_ENTERPRISE_ACCESS_KEY }} @@ -48,9 +44,8 @@ jobs: run: | [ -f ./setup.sh ] && ./setup.sh || true - name: Build with Gradle + id: gradle run: | - # Awful hack for kapt and JDK 16. See https://youtrack.jetbrains.com/issue/KT-45545 - if [ ${{ matrix.java }} == 16 ]; then export GRADLE_OPTS="-Dorg.gradle.jvmargs=--illegal-access=permit"; fi ./gradlew check --no-daemon --parallel --continue env: TESTCONTAINERS_RYUK_DISABLED: true @@ -58,16 +53,28 @@ jobs: GRADLE_ENTERPRISE_CACHE_USERNAME: ${{ secrets.GRADLE_ENTERPRISE_CACHE_USERNAME }} GRADLE_ENTERPRISE_CACHE_PASSWORD: ${{ secrets.GRADLE_ENTERPRISE_CACHE_PASSWORD }} PREDICTIVE_TEST_SELECTION: "${{ github.event_name == 'pull_request' && 'true' || 'false' }}" + - name: Add build scan URL as PR comment + uses: actions/github-script@v5 + if: github.event_name == 'pull_request' && failure() + with: + github-token: ${{secrets.GITHUB_TOKEN}} + script: | + github.rest.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body: '❌ ${{ github.workflow }} failed: ${{ steps.gradle.outputs.build-scan-url }}' + }) - name: Publish Test Report if: always() - uses: mikepenz/action-junit-report@v3.5.2 + uses: mikepenz/action-junit-report@v3.6.2 with: check_name: Java CI / Test Report (${{ matrix.java }}) report_paths: '**/build/test-results/test/TEST-*.xml' check_retries: 'true' - name: "📜 Upload binary compatibility check results" if: always() - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v3 with: name: binary-compatibility-reports path: "**/build/reports/binary-compatibility-*.html" diff --git a/.github/workflows/publish-snapshot.yml b/.github/workflows/publish-snapshot.yml index 5c8bd2933a2..5dec33e126b 100644 --- a/.github/workflows/publish-snapshot.yml +++ b/.github/workflows/publish-snapshot.yml @@ -20,7 +20,7 @@ jobs: - name: Set up JDK uses: actions/setup-java@v3 with: - distribution: 'adopt' + distribution: 'temurin' java-version: '17' - name: Publish to Sonatype Snapshots if: success() diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8d933603484..70ddd45525a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -9,6 +9,8 @@ on: types: [published] jobs: release: + outputs: + artifacts-sha256: ${{ steps.hash.outputs.artifacts-sha256 }} # Computed hashes for build artifacts. runs-on: ubuntu-latest steps: - name: Checkout repository @@ -19,7 +21,7 @@ jobs: - name: Set up JDK uses: actions/setup-java@v3 with: - distribution: 'adopt' + distribution: 'temurin' java-version: '17' - name: Set the current release version id: release_version @@ -31,6 +33,7 @@ jobs: with: token: ${{ secrets.GITHUB_TOKEN }} - name: Publish to Sonatype OSSRH + id: publish env: SONATYPE_USERNAME: ${{ secrets.SONATYPE_USERNAME }} SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }} @@ -42,7 +45,38 @@ jobs: GRADLE_ENTERPRISE_CACHE_PASSWORD: ${{ secrets.GRADLE_ENTERPRISE_CACHE_PASSWORD }} run: | echo $GPG_FILE | base64 -d > secring.gpg - ./gradlew publishToSonatype closeAndReleaseSonatypeStagingRepository + # Publish both locally and to Sonatype. + # The artifacts stored locally will be used to generate the SLSA provenance. + ./gradlew publishAllPublicationsToBuildRepository publishToSonatype closeAndReleaseSonatypeStagingRepository + # Read the current version from gradle.properties. + VERSION=$(./gradlew properties | grep 'version:' | awk '{print $2}') + # Read the project group from gradle.properties. + GROUP_PATH=$(./gradlew properties| grep "projectGroup" | awk '{print $2}' | sed 's/\./\//g') + echo "version=$VERSION" >> "$GITHUB_OUTPUT" + echo "group=$GROUP_PATH" >> "$GITHUB_OUTPUT" + - name: Generate subject + id: hash + run: | + # Find the relevant published artifacts in the local repository. + ARTIFACTS=$(find build/repo/${{ steps.publish.outputs.group }}/*/${{ steps.publish.outputs.version }}/* \ + -regextype sed -regex '\(.*\.jar\|.*\.pom\|.*\.module\|.*\.toml\)') + # Compute the hashes for the artifacts. + # Set the hash as job output for debugging. + echo "artifacts-sha256=$(sha256sum $ARTIFACTS | base64 -w0)" >> "$GITHUB_OUTPUT" + # Store the hash in a file, which is uploaded as a workflow artifact. + echo $(sha256sum $ARTIFACTS | base64 -w0) > artifacts-sha256 + - name: Upload build artifacts + uses: actions/upload-artifact@3cea5372237819ed00197afe530f5a7ea3e805c8 # v3.1.0 + with: + name: gradle-build-outputs + path: build/repo/${{ steps.publish.outputs.group }}/*/${{ steps.publish.outputs.version }}/* + retention-days: 5 + - name: Upload artifacts-sha256 + uses: actions/upload-artifact@3cea5372237819ed00197afe530f5a7ea3e805c8 # v3.1.0 + with: + name: artifacts-sha256 + path: artifacts-sha256 + retention-days: 5 - name: Generate docs env: GRADLE_ENTERPRISE_ACCESS_KEY: ${{ secrets.GRADLE_ENTERPRISE_ACCESS_KEY }} @@ -86,3 +120,57 @@ jobs: MICRONAUT_BUILD_EMAIL: ${{ secrets.MICRONAUT_BUILD_EMAIL }} with: token: ${{ secrets.GITHUB_TOKEN }} + + provenance-subject: + needs: [release] + runs-on: ubuntu-latest + outputs: + artifacts-sha256: ${{ steps.set-hash.outputs.artifacts-sha256 }} + steps: + - name: Download artifacts-sha256 + uses: actions/download-artifact@9782bd6a9848b53b110e712e20e42d89988822b7 # v3.0.1 + with: + name: artifacts-sha256 + # The SLSA provenance generator expects the hash digest of artifacts to be passed as a job + # output. So we need to download the artifacts-sha256 and set it as job output. The hash of + # the artifacts should be set as output directly in the release job. But due to a known bug + # in GitHub Actions we have to use a workaround. + # See https://github.com/community/community/discussions/37942. + - name: Set artifacts-sha256 as output + id: set-hash + shell: bash + run: echo "artifacts-sha256=$(cat artifacts-sha256)" >> "$GITHUB_OUTPUT" + + provenance: + needs: [release, provenance-subject] + permissions: + actions: read # To read the workflow path. + id-token: write # To sign the provenance. + contents: write # To add assets to a release. + uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v1.4.0 + with: + base64-subjects: "${{ needs.provenance-subject.outputs.artifacts-sha256 }}" + upload-assets: true # Upload to a new release. + compile-generator: true # Build the generator from source. + + github_release: + needs: [release] + runs-on: ubuntu-latest + if: startsWith(github.ref, 'refs/tags/') + steps: + - name: Checkout repository + uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b # v3.0.2 + - name: Download artifacts + uses: actions/download-artifact@9782bd6a9848b53b110e712e20e42d89988822b7 # v3.0.1 + with: + name: gradle-build-outputs + path: build/repo + - name: Upload assets + # Upload the artifacts and SLSA L3 provenance as assets to the existing + # release. Note that the provenance will attest to each artifact file and + # not the aggregated ZIP file. + run: | + find build/repo -regextype sed -regex '\(.*\.jar\|.*\.pom\|.*\.module\|.*\.toml\)' | xargs zip artifacts.zip + gh release upload ${{ github.ref_name }} artifacts.zip + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/sonarqube.yml b/.github/workflows/sonarqube.yml index 4ed20609f83..d238115f5af 100644 --- a/.github/workflows/sonarqube.yml +++ b/.github/workflows/sonarqube.yml @@ -37,7 +37,7 @@ jobs: - name: Set up JDK uses: actions/setup-java@v3 with: - distribution: 'adopt' + distribution: 'temurin' java-version: 17 - name: Optional setup step env: diff --git a/config/checkstyle/checkstyle.xml b/config/checkstyle/checkstyle.xml index 90a106eb1cc..c1b8e128046 100644 --- a/config/checkstyle/checkstyle.xml +++ b/config/checkstyle/checkstyle.xml @@ -96,11 +96,13 @@ - + - + + + diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 249e5832f090a2944b7473328c07c9755baa3196..943f0cbfa754578e88a3dae77fce6e3dea56edbf 100644 GIT binary patch delta 36524 zcmZ6yQ*&aJ*i+pKn$=zKxk7ICNNX(G9gnUwow3iT2Ov?s|4Q$^qH|&1~>6K_f6Q@z)!W6o~05E1}7HS1}Bv=ef%?3Rc##Sb1)XzucCDxr#(Nfxotv ze%V_W`66|_=BK{+dN$WOZ#V$@kI(=7e7*Y3BMEum`h#%BJi{7P9=hz5ij2k_KbUm( zhz-iBt4RTzAPma)PhcHhjxYjxR6q^N4p+V6h&tZxbs!p4m8noJ?|i)9ATc@)IUzb~ zw2p)KDi7toTFgE%JA2d_9aWv7{xD{EzTGPb{V6+C=+O-u@I~*@9Q;(P9sE>h-v@&g ztSnY;?gI0q;XWPTrOm!4!5|uwJYJVPNluyu5}^SCc1ns-U#GrGqZ1B#qCcJbqoMAc zF$xB#F!(F?RcUqZtueR`*#i7DQ2CF?hhYV&goK!o`U?+H{F-15he}`xQ!)+H>0!QM z`)D&7s@{0}iVkz$(t{mqBKP?~W4b@KcuDglktFy&<2_z)F8Q~73;QcP`+pO=L}4yjlzNuLzuvnVAO``skBd=rV%VWQTd0x6_%ddY*G(AJt06`GHq zJVxl`G*RiYAeT=`Cf(SUN$kUEju!>SqwEd8RWUIk$|8A& zAvW|Uo<=TWC~u}V?SNFv`Fq9OeF_VpfyXHPIIay@Pu5J6$$pg{;xE9D7CROVYV>5c zv^IYXPo_Z4)bg5h?JSUX!K`q_u{>F%FzrG>*!Db_^7*7(F@f%i34Ps`JBAH6{s=ygSr^CVO)voP`v=SO z7v;4cFM_D>iVl{&X*N7pe4_^YKV%`5J774`5!DC}g;D@50h?VA!;fU1?Hf%%`N8R1 zSg@hZ8%Dq^eYV1!g8;`6vCSJoK+V1Q6N8ImtfE3iXs!s~B>js)sLHB9w$r+6Q>Oh#Ig&awvm%OBLg!7alaf}9Cuf;M4%Ig9 zx4K}IQfPr&u?k8xWp!wI4{CP#GTs#qR0b+G{&+=vL}I{b-Pha43^%8=K3997~* z>A|oxYE%Vo4~DiOih`87u|{8!Ql5|9Y+(ZY2nRP+oLdGErjV&YeVKw>A$JyPPAL+C zA36S!dNVf z;xJ)YR;^VPE1?`h-5>{~gwY2pY8RqhrsiIBmJ}n3G@Zs!!fD6y&KWPq&i8HEm*ZAx`G} zjq2CD5U==ID^we8k?=geue4Y>_+%u3$-TzVS6QMlb4NoS%_V>;E2hQ)+1Q@v(reC5 zLeK*f%%{PNO-mtrBVl|-!WaiKAkZv-?wnOwmZ=Tv57k=4PX=C?=I4V*THRFRE8a_{ zb>5YwDf4o>>$o{XYlLN{PZ^Ff?0FJl4>A9C-q9A$$&44l122Qsc|6Fd6aTam{=JO3 zBFfFe9seUPSUeyXQc*RA>2{WoKIYVltA&@5spdIW;rzOOqoQo`CN;~UNgU{{m9^c1 zTrN|8w_7+Nws4}Z-4eS9WMpF3h<@81a)oK9njh;-TB74vR;u{vE?>6FDG7<%GVXFL zUR9l{z*eEND6pp)+hpNT$VVM^Pw*S;#NrbCmH{dhBm?%6D|k)0C@Z9H>T|kby1^)# zOPmJ8Hq`8waoEK(9}IfP_q4yr(s?ME+T%UV-ikxW!XFb^6w02t30j$n_VSwevg;{9 zx0OXK_uGBFej=gbG>G^pEv^`I8&_a@t9>Nr;#r?XNKquD&Ho|`)qK6C^-7SCdo=S& z)vUi;m5*qIePEIbL=wJ|WCBNY;zCm2F-+@N2i{I^uR9UVZm$o`I|@<&2}w)C`h)vV zW{)yGJ3?GCZNtFe53Kb#uzrC7v-{JygKZUiXDV5mR z5la_vAFOvoh#yn)B`$^ZN*Dxp5Uo~_k8G9skn2)Tb>Kw#Vgxi`bti)^(z--X9F~oR zZ6=^_x@mDT~=h_@GGVcgBtLzssB1|Xy(xc(lUYJ#_ zgwc&ajE%^cCYW7d;xAxi{#LN*1}s>{K79MZrq!tYMpRA{T!#^tgXP=J5FvkbZ@gx~ ztq-E&c$`|KX8GS2a_voZHf=y8C{6~f~`DpC- zjQfrt2OGi-WGx}Y4>vM`8<4frU*!bq*NJ*Tyn0cqk=zpDdYth-PJIfz5>pLF@qnai zzj2FEhuOa-7$JR=U!L{UWWJBA%~SW-6Nh&3;<}iQO)DvOI&VKi1L8rmICePWqoY^F z-dC8X8~1T}=C9m&yb1kZzbKd2;29_Pm*Cs=y{Z06QZDlT7Poci>1@hFa%t0<`1()UTxcQ}e`fAh6K`<5C_SG`dw$IqzwEYNKvIH3VWlhz z_#^(T53W}jeWF#WIhj^U7AdIB~3feC--5iUiiT4Qyu81 z;Xa^8#~M@p%6B`LCKWWTa7I+35BLP=EOa&Gp2pbTWw5HOIjrx;2J(KI$$HT|w8}R-8fbp9sot&LiLs7ILlyZc8 zWbss7=*Ah|X$LEt1O|T?ABkIn-0NN`I8+ipfoBZcW>(WiaASG_khBtKM{hfkm5VBS zy0Q`4*G6HRRa#9G)10Ik3$C3|nQbFzmU-dA`LjKQY8icnx?2OE40%z852{OJH=?mbvwr9 zhlx0RDo^D;p*xKx?yT(`s7wj7BHA~rHF2yxnL<1PcU7FM57;?g^ z&CyPh9W4KvZ;T8w;AuNMn|nQ-xJ~CvVT7gAPAGi7w8udw_LOp+p4eZiI`JEC@Mq9F z#dA2AM_};CnL=y0#tZALdB(P~Rz*KqGqjwec%Fy?K(PGoO0tfskWw-aGhd7$ zTi~x1G>4h5q>ek=tIoT(VBQxrq)&#`_0UHC(j*ZO%%}%C)|EzTWEpvYDqCYXLexR9 zlww1ESB+IiO}=oq)8WZj%cY_FTQcEJ`JdABa=_S;O|kLhX*|5|D>0c{12DoC?K95f ztNxm(sTU6cWWd$tv`5X(=x?yAo)IYQ3G*2+o#|EfXko6erF;M4Pc;G0)pUDY)t`H9 z76Z8V9HqbWA@!`BelAT&ErrGTz7}%M*605PEY@3{gv+`yEhr{=EVp_tU%`b54Pn4a zz8nN7`eNx=*`f1t#^7>7G07IEnbnn&`RWZ}4Cp8W_DFDs-5)GU`bw}uBmOQfKmi2@ z(cWWmvHFTUNInRH!0y_ZtuI9Eh@O3+64wy-_2DF~E@KF3abM`0gC%|kHi@&hP_#B$ zLN{Z?$V_;+h?%2zEC{2ITyWOup*w*K?~vpwB(DX1i6oY+F)??;nyHpzaPLIt6G$4; z6>iAsB+&&NN0;ObWVOL+-^ZwD?nHgY>0k>0I3iA7o)f# zN&aX$lM@r_Iu|nSdPjoF{#QD9M6>|JSNPLxX^T2!jCKjS5mwNaO+SmBfOY z;6ZdwfzhO6Vs|9u81f4e%7*mU%8K>A7QWO0;QcX7W@|NSUVl)_>7VEf#&N6E~ zn9Wv88@Suo9P+M_G2(f+JFf#Q^GV#7QQ`qH#$N1y{A*_t^`5H1=V^u?Ec|EF6W+6B z(@Q8ChIUyq;+I5CmjEa1*v%d5{WHyhcHSjQuwzQq?;^BmfV#okq3v8bp7dBdk z54B+%D3=JWd-2w$)puXxZyZH>-$O-?tbSIlGc{em9xHN!44iaCr}6uZ^FpN7IvNh8 zbp!%4xR9np`>AOEd1e2_y}xW#v@@h3wYc?WiwL6Q>fxPQA81V^J)XtGs|Z&er6w~M z!1Ph~85TMG>R&ixNUnevc(w>fgb%+X#Wds6Yl+wH29aE%;RuDeZz5dEt%#p&2VK1n zKkqgl&*_YwnO%9`0<6MVP=O3{02EcR7PvvZPbL2KMuoRsU|Y%zw38qeOL#!YFp#_~+rtNJVl>lJSh_*B0A6n3XkE5po z9RpE_h=pnmDJFX*n6wmsWJ9GLu2=L8y!_R;;Aa2Jl|)I}Qff&`Fy@iOhop8>Y2{F} zbVk3rNMi$XX(q1JrgcIhC08@d5Zc>wLUL3wYm}hzS^!5d&Mec$Sp^$DUS1lD1>KAt z|Efof3nJ4^k(WKL_t-u8ud4L(t>q#9ECj?v#W~W#2zTt>|MCh&*H8Wh1_I&^2Li&M zq9j0`(zk~P7}dB`+15b*j%VPGr$;@4MBQ5AT>-y?0Fxfr2nC1kM2D(y7qMN+p-0yo zOlND}ImY;a_K$HZCrD=P{byToyC7*@;Y$v6wL!c*DfeH#$QS6|3)pJe68d>R#{zNn zB0r*Es<6^ZWeH`M)Cdoyz`@Z&Fu_^pu8*089j{gbbd!jV@s7`eI5_X5J3|poVGlq` zDo9}G;CsjW!hgN2O9=1|GpE;RpQvrBc+&dF)L>V&>9kd6^YIL?+*WDmcQlvwnq`Lf z&N$gF>3+E*NcJojXXI^}B(B-;@ebpVY}l#EcDWles7s;Ft+KZ@m+6FWaD^oYPBXVw z3sq|aKIDh1x5Ff=tW$(LO|!e&G?Xvh^H!GfiA(emluL!LmD=EV@|u|8S7w6ibUePJ z>{sOC6L27R+b&}e?VH;KvV3a;O3G=gwG}YzrkSTV6(&=;o)EV~2OD(Eh4mu@K0G)i z3#44IZhqN6+Hb2h#3R8YwJW7LesDA9=n)75u#46_ZmSh@6Q-4oHvGxFPY8x;Q+)d@ z*-SDqhVeyPGkoD)iq;z0r*M)IhY5I>gMA@RS&EIYPq}Z{$Q4Jbfd76EVhSF-sR^TO z!=o?>V(^bx!pG$26J~Z>Tvu&Uu+0;>m+pg(fmbu(97^(OHBH4;J8WIfv-f5}VP#VS z$Y$}SHKdphDUHlbdIVW!k$L6T{LY)|H}MT=l$22kIl>|46FK9dt$?3Fjk2RA-~AX7 z1|Xe`n)%h~e-O_qLpoFXJ$%gmocq`v0%hRw1k_6nh|+3pvJDy}m)V|xjL&!Z6?%pU z+m)r2*pWjEl!etAYxdzWb0{mGc;#$>rE%)b z@Rnj78P;$lrzY!XCa0&x+8a^YF*G|Q|C}bGeczz(5m_gq08wJHIH`WqHH?A}!~_3{ zQEvMXmL<*nThl^pL58nbHgQ1n9cYmN{C8J^6AKS%?~>1DCt70Q2Vp0;E@`GF%Tzkc zSUt&LJ=wHI6@#8_%=2s=j^4VBd1-h_)3 zeozYua!|{x(qk#z;tavf28rj_5Oen-cYG%;R6I}Hz$yMXeg^)_$OUUXx1r^qrl!DG zYXkAXKBMrVM-rJwAo<5J{NW1XJhW;Nh*&`nFV-Z;Vd({KSkMxV#cn|bXJ z50GtvFE##sqGhV#lv2s6?^yeBShlhR%XaPIo)iXOue}jwZ;Zq#dgDn8H?74Y+$Z?C z2Y5mCC66>dp%sVMecUzCirWq99Ea(TDwClZxtEB~4N-2JmlH#>Z2jOcaNaw4tn?P->BBGNHxUHez7>C@TZNT5Z zHerlG0a4~06L%>tn!~$s^L5`~{ueLZ5?`$46nHvwKxM0V9VQ(k{A40xDVw{+Qt)RV zQ)T2Df)cp0nv!lUFt3D=i~k!V|7dUjpz?K2ZiynO)$d{2*YT$N^CQ{t=luZ>WcE!> zg25p}If9RTho%G@PZp;5zBwv`n+e9iO=6dx1V^|4Ty%`oE=f7O&QC^s!4MJ+lMG>^ za!mgpz*^SHT+M_zm;{H#E~SaU^Kn*y)nTAF*2@t5mF+l)bte+a+goaA*zXJ4P)H|y z{4OwbJnIPtMp4E~=64gM-Y{#o{x)+8YCg$C7Yy=;9hdyBgRFIY2_L9DL3*B@%$5#m z8P}+)glf*}UPD$C;_yntx}9VPmSSnY9`Thd09nfoR;3`kar*FRfS)`+as*t2l*USWgmaZ!qFubr1DegTGZspyYMgic{inI0dSt+rJR z((jjMrdq^?VSZ8FCO;0NW@>O_b67gDHP%W*^O?J z91NQ7ZFODMSvHj3cvT#6RJUF7x=-BJFQ^6<&mOd15Z&M!?b+3Tg!UcgldD9tOAt5K z3X>MlE-a=sj;K&}sSng48jQ7sp|&u3;@e>V4Cuf(!s@9lZ0Cg^DKWmki%>$<85tOG zU;e{%zHU~KREBUg?FbcseK{lmK-`*S1p9j_4hF=F$y)NB;HsHwuf_A0Zhy395eU7o8^A zi2t7Ch|KVprUn03N0T2XshT!g$HTErcQBBG=TWaHkYtaI2CJY7ajI%yr&9 zVC^zJ3WW03bjwGNx{l}#+D&Ml_uI4PQhV}qZPXOP7ffSv(O;hX{Ff1|HoA~v)V!4y{CdALyi2YPjrRVmRYilRv z5PSkj*Z_8Fa*sCqGN?7YTnkr9=i9X`qcw7nqz#{bj?B7NiV9fWF+%~Rb1X@MuS^Mw zC)d#K{(-9!?xStM2K5x%x~ogWxgIK>s5r_RT1jU_lxdTtIEFWvi4eJSAiGec&HXQ( z5t7!J1b#SL|8s4)u147PWQUq_e33!5Z#f$Ja&az)(Htl`Z0@Ez)0d74BzNHHfH|<-8q*ZMf?%eJzoGS!0S6Y zSU7y^1+;V$Je9F027>1eN#_tz+2t}Y^N zYfi9}J!N^SU1CYoNBDbD39@84xLroY@0f%%c^(5CE+}!b5-Mt3oXe2nBdyicgGIL+rzTTKv`}Pp%fG1f^s?sgNH8=Q}s4Z>0ZCZ8ZYF z4og8nK%OA~zZMJX01uFtrmwhcgg*XbiMP9kfkPYFASbp7*Bk^5ZBzV)dL)JhPwDkM zkgdHeKw)orJcj4^)a^wQC2|->G=OBzuc-SskRrrf+H-E%HQ==Ex}d*504#GbIUXIB zcZs@Oo0i61MG}&0bu%@2N?MMJMRXyTVb8@3wF5eY3G6-1NdT~{{~YFs8f&SNebdaq zKmP>XqCQ@iaamuvY2m%xJ~gdSLSj~DBhB`NCj_c}NbSjB{r(E`_-+6a#vx*|S>-GU zHsw^dxxu`e)q1HbH==rLFap?cebKumnTo=iJQ zJD1#=o>0%Y@&jP?^)Q5bTV!pzrf=FoHq2c_59pq@my{D4AW8VU*7LVp;LF-qESV;L zClRfyQ6CcD$sd84K@e@p_ALH%j(Pz@Em@QFyY`AG&(|!(cG8!oV#ejr`y(LolX}Iu zL$)G)8^y4sUAYCWprzVR?`#OJ%NU)9U^B!OGSj>Ly;<)<(nNh`?z*GvJ|ZBKfZ`0 z=q_yGHWPp~R+J+{{@APVwmp8`=%N!L7AT^l^oaM|JrCFu7J#@frf=z(vGq2>sQ^@u zk=^d#gDf}ME!~9PaLfw44~rsG!)T7h8~dY^VcZQa+ueWPGG$mWXB|H2$$0BT(QAIu|=DJXPQDNes3Q>-|Mh=Ih zy{WR)QmhL5rQbBYPBa+e7)8Vo;_aKrg`}izmN>#ATuSDu!QUFA zsgM|Kv@W(S}Ag^6e8)9pQc@JLj_2ZIkO=8)#ARm#mU=NncWbmd-SbO;ad=y|k`shy3b z*8o0@EJo3b$#zSgmnlT7KAp)U!qI2M`hiC@Gp0)pNGHYMe1$MBNE}Hd{Sv^`wI7>MzNwgVv1ZzL zttmyv!=TKuPH$b>r7$lgP5?vho;#Ks4+zLzaz-1b{p-Fn6dWy1Agg7O2{&VQ5@s3A zAqzC9QokRD59!@ex#k>xy61kq6h~O$lb;lB;Q|chv&wzR+N zgXdIo%?q1Y$TzsdCo+n$^NODN7yd}cAv+rkG|u-(wTp?zUSUxaA-W3dwqikdrokwz) z68)Gn$Nwc1zB$F9`#(af|C3v;|2$bo7fU8f7h^NK6h&@xi2m`)g4mW$?l@5JEc*VV z6d67@Fl2w6mO;MYUl2U>R996gQUX$d>$D>)TNGq*arz}f21yh^uvIM!3u$H{_CH5! zrjt9L^&J8UqEV_lLn&}nc|Q=MDei6t=vL_>X-i8B%f5FDi)|qQ;2V-T!qOi*uqq{U zElET6#2cb>Z_6p_vw44&mN!;T&~ubi&p`XGepCNAfa0-T zC84V@VN^R6%z({m=$%iXrbiggxvMiBpww~ktD&=9-JPK3kPCOGCJNQj8+l9k#!QeS zv3h$Ej>@j<-zBW0Qr`5tNQVRfYK_$3>nWUzf&c*tCpl@aYwa%b;JNeTX10OevcxY7 zqnLgKU-X9G8~&?Dr)`*7GryqhN#;9v`D_c=_xBcD{j-cLop~pSnM?&7HggX6gb++ftBq$idM1|>5t+68sWf{ixREbMkZesmpjJsAFPQ#2+8Uek z$BPbu3cQuNDQq+^M}&ZuSHjxUgxOjF<^%4 z*8lc$CgA<$n=DYg_DsrHB7zYM0Ro|gS8ZnUq$u3GQ+{owv9RdB$wG%d-;R+I>?i?b z+r_mu{IL6WTYftdz?0#pbHkmQP31LvXcMK6;mAP+;q^L@q}v~TD}Ni>f7@QYcbM!T zX5kShHv3X1U=>B!2*si9=AEJCBt~GIH7DL4^+gHj+q}tk0F_?Q-=z{JY%77nkw>$F zG}6ROaL_)3t$jX=ZtFG{Q=LZfNjNb2LK=m9l|7iaB++N|S$vAr1 z_gf3JpIB|?dptfQ{sOZGlhyj~D;T#hjaNh0X5(o&7)87^t@@Hteh{0DOM{tCu$l#& z&NhA&V4VR}nzZP{7i(5bGB17<7bu+RJ1}k}=ffSg%=+213Oy@Aj1vv2U>U>8tRhKM z=*e<21)u6SSb{CC&We%#6X@duqLWGJ>O)Ls`uM98``34g11;D}*7>c3+^c|Os&;t}`(BWMD zfbyr~$j%{6%DZ`kR-}s~p?0#&-5a}b?6tDqwtqY%ep0ypSRIB54G@|0J5E#LkxQk# z_&xE=d(U}q?*Rh7L7f8AM5{qdGpC<&t~9YI!%j2G@nUPoLPSiWHjCVP{JAe?cBjQ zTqI=R{nv5c@|R)8Oi3cTL{&6%XdTgDP4CNYT}q2f5|Xf_hID#;83kd+v0RRyNKYn} zyPahwd=4ncDORLvatBc~KzT+jiiD{tzd3d*T(f7ayS;J&I1X!xaL2~POrw2ST=Pr5 zu*c}fb@)0P6jv))kNl38C7gmnWGmlL@{PWOVYt9se*cS0w#@W=N+dY#V08ci=Zmg9 z+${f#Qfs5)hOPxC;q{(J{Kx4HF)2QMzlVtXz0-O&h2$VxtT;ROvZ13nN{IG>Asv{% zHuDqgZ{R2(X*hkO+!HYHHWvRYrvN9fl-1?x6b)oseZY)@dQ6O>9Y#8*23~%bzN~Nf zpHGMdS-G|%F^v3Gnlsc$s4Wl=ZEu+J6y~*Ih2tpmHfO56JXKjldm$BxDvW6ZH>JrU zdRo}=^466lAq6!qY_@nQ}5ETUEoF;`>7b8W910_Z17!r`D?QNvC z+WF%@IkPi43n4;0Ks`M{x*0-^GK7oCAp?pFK1`~RoMSe@jAlV8vQruCUNyQ_7wk?` zSKe*|!4ar@VSA}!ThlIB*Qa5){pu&HS!a)-{lWL2@o1486ZK_!!}FSZ>vyUPIOX#+ z5d3~J24Op?!f!oNytub~egnkB`}h?eh!QyX6&^LbNuA#9vH#N_7IL|#6kIDhLL=be zEg3Cwmw{A(cm{&T zPg>XIWX24$Mj_#^k2I91C@h;b$8WNVr&MLjEwgAUtSeJ2W0)6Fit}PF!K&1j=*+6g zL{XOUrqhNyPLemIF4C&hThR8fie9^fYg$yl$m!1|YgcPlO>TB-(X{lkN~X}R=GA!Q zou<9ZJV6*}SN_4WRsqzRGI&p$;9DxDFTlyPw6Q9rlo@E3tMN&Wo4eFs{1=RCUij$V z`8)kmh0fhTTiEyvRl90B%q2(Moh$jg7{NeQiy> ze!H{zbG7<3BcK}XE&V_1kFfGA7D^ODxn*@nqlp!{LhYb47zIUlV^m+7kZh^a7L1^D zvI?m^9PECMnnN$0hi^Ur0b-~QgEORanrv|`dd;ek$4rAgEEof3HyvuYoZ)H*;+TgO z8CJY~4YDI^7RD7O)m&2h2K`-4e-I$1zcZ*K>Cd7~sSxEXc{d7-;f z5Ykr56Nkie%=z4_LIA}H>c81e$%ey=2hjqzTxoO0MDe!J&PE@EmX49jQJJg?HNw;B zHRHr)3do7CGDa3lPAZ4LAnpT)spnk8(ZiFz$|F$1m*A@!qCPug>Isp|MPI24i>jp~ z((9EQ9W#Rz)0AYT&ZWOWKBNtdNYYm2QytK$o-_|W5j7Abr&73(MG+Ar4K!Ij=nKu# z;SNkveY?Oc!I|Vta2{rb@c50#p_byn|_tu>Pv}6YDydl|}X#4oZW2 zvq)Y@8iG5@6c3?uu4vdLSBq23P&qUSvtGcu_qgH*?KfaT)@QueLx6apA97FI7sXP=foe zmrEu7;%Z=yTTGUsHsjR(wU54xNPI$hLFZUOwh=uhZ&rLammOQ?w*)}?Ah#%&K~OZc zl#Owj1OCEeXt!ALV7LgJ=MVbCo}<%92WX$wCS~Ins}%5+sb*C{WoOT5*2%sgjya;~ z|A#;k?j~J9qB)Tku1BGX=MrZ}<%Z4}i$OvCHv_3vtH_NZoK zjJljjt(~Yh%aI@gFnM*e*@_*N190p^@w5?SjRMb66N_^3EZ#Yoh<8FM>Yx$+mTbp$ zjQQS7(rs2j^54CJXdkH|$1&$wPOGDvm^@1o1pl9~!5&B+I=U-f_M-M&r3zfp2%TH%Ib3lz-^t)+Z9E+>W1Bt1`B}rZ$hZ3{0n|nZKM9O z$?_1+y}fB2$zEzE$zC#46=0E_4x7-VXY5}<+d!g2+Kg$gvU-Xm-A9DBZz+bZ*zDTx z$Wfb93))oLQf;wKi5JBJ%$yq}m42lacy`bC9PjFg*}pCnqn@dv{k9WiwCC07;6n#e zJ499v3YGQ^WyYY=x*s`q*;@R_ai1NKNA}<6=F8IvJArr{-YbdY#{l1K{(4l$7^7We zo~>}l=+L8IJ`BhgR&b$J3hW!ljy5F`+4NA06g$&4oC-`oGb@e5aw-1dSDL}GOnUuy z)z1W)8W9t(7w%OCn_~#0;^F)xic6It5)3h);vuLAKFS4b)G;Z$n-R&{b6h@yGxGo> zT-cq0W7~n+qN10;1OS+*c>H$(GoKq4hGG% zL&XJG$PDQ6K^BD#s_MsnlGPE+$W^B`&a+Z+4;`*nyKil99^E(wW?t>#V_xYWHLl2} zIV`uiR-__g+<&m#Z*4E|wjKY1R2mCm%k2ayMSDw`Rz_KA!3P$uIbB`dl`3&A zmT@gMT@ZpAxBys8zRtgoH+ebSaVA)maP?G1=G4x^Nw3mV0?qehWL35vMI~p$y0hGL z6@vHf-50P~uoe6yY&*D)Ekmi06LF!Jqz9#7kMvWexYMbAn{}`{3ZBsd6$5jBCujDp z<0N?b*1%T<-_Nxh`lKtla|FFqs7RZMtjHAwZ0Ck&s{x`#^S?36BNQN1JU^0f&TRoC z$}c)LW7)-n$CmAg&n(96AycC4!4_*D(~HvXyLW>HORuI0;ny$f9h{!Ud0=X0x%{l6NH$ z?lttWn}DQL521;-r~Kf$N_YPo)7H>3gI@Ivt}GnR=8W~Nn7_PE_3{sRNn`R~bs`g1 zoTh`7o4H*TRp7VBp=%>&t&Cd*Ny~@;{C)P;62d^dipuJYUV3-Dh<#a&AIxtrmX42( zYEH-8F3|^nY-=yw(?^d!hTojNxr~A!n$Ao+2mq*kZ&>Zm+BDC*sul=~!LUtWiokIB zxc(dNwyk&5o;>WRt)Q-Wj;fvuvJO&DLPe%mt@t!Oq^VsoIN0iTh%fh#`-{Ha?a8gf zj^yA3`=_NEONO0Z?}YVP*dL{T}v|A&cE7$_0G=g;1s*WDQuRcq>cJ?z=8b5&i<)=3ELSW%Kff zs=my9Q%8?aMxZeDq=RBHg*&HnIeQ_}X@oh=f#?C^HSg?1dwLn#wu(o^uANrRZD;H; zYbOec$#wJB(u?w22{gV+zb~pv|Ag!q$N@^|6n+FV5-X=lR$jajjeRh$1tjht$URz1 zhw)(ksAr2;QBXH9T#A$6V4PsR7K)){JQb?79o6&*IwDPZknNqySIa6pwcs)~xN81I zKc-GmzZ$i(8RaU==$Dx{tD@4nph-V*=W{Ln97*VEN^F+u0!F<%$l=K`ikIp#<^Yt} z{rx1gk>;rVccPIo6hD=xPQ$PxVwl6Cl;YI6iLf3!aevhsyXXZovK#TOv0|*T+^ii5 z+YO`u(SO3@ybv-DG)w)E;@+ULoj_+<;mc#iW8{9Y!99vE`HdAK=Utac&Eq1uy!TLgOS-C1E90Am)B{Tiw z$>$Er{s{snLEaO5@u&zqxE@v;p6D&?u@40t{#VNA&7SZael};kGEwnHgD4V5RNM@g z(EL~B=A8&?pPPW-fTja0Oi6SVtI_(3ME!qWLg-uK2afWhBn(C2PAmUyu^2h?Y402i z9P03g5$1#etGdUUo?#skjQ|$*()ybRGMXM`-2?jjThnTcPV==7sg$k{GxYdF+S*zz z%dtBo(R9!7SW6Utq|wFpsKMSAH-x{WB|Cz62A8!p8!kHz1tM=9I=M&xqQG zz17xBW7t?Q?C%@4YC`p*za(>hOrK&ELyDQu{5ACOg9noZS1SGh{-FcLy_W;nf$N`N zGYxdIzy7mL3K@Kw65DmvPH0@&;T{y&jP^AsaYENi}q|A z3}l}5V?z_VvpHf%CkpN@IK`czOuLPY=yBUf8Q3b9$X|kEiYROV$`T8T7ZjFPvKhbK zDYxzz99JRNzsx0f1Y>IrIQq9o+W(TsB(ZtN@4*)DMGr3?4~Jt|37IBI|7oQknQI3X zAWs`45xiCHga9;8+W{|!Yy>tic?%SNq=3EX@z2Mk!P0dKG0NCHNz0*F-a z`7K?6d*D4ri*=>wyQyQt{_t=t95*gB1|tdTg45fR{KmKD|3ZuM$QlkX{-tUkq@3Qd z-6X|jEyZa@tuxB}qrdlJdc0{8``%3M$xl8$9pUzkFa$Ww{Jocp9>;5~oNC8o`3GK& zy7_X8YoQDCO1TU_a%#Q+rC?Rr`r)W8CdpEe=>uMYDx6^46V_1DthgX`6CnF*E+%bY z=GYih(DizXEVFDuQRPQY&dc2p;Pwo7L{I2r3;QV8IEPg1McP{PchEUDf} zbtSAoBMPt?&Q@{fG_3a7gzHl58O7e(h_F6^rKgU=a&(^WpgH3U%`tpj3CMVRA-uol z(hA)(VF{4@`k@PREUQJ_8w6CcMW4Pm06{fw^*>aMH%#ik6lD{{j~nT}Vw=wZ(;Ct& zi1nt}RmOGrVHP++5;Z@eE*lkdw~?>AJL_Yg!~p*adS_s1`_oT1B26S zt&1-4twO45pMl<5B9T;SLH9Q?E>dBXcy@5k-{YQ5K!A`=YMYMlLOYc(+LdC<@@UIZ zxq%vI<;6P)=W4nRb7nxQ9KGzXsOjWs_3V-2*V+r}?dAZA7{7f*>^PxEw|6+WS0wAs zen2zj2cFKIr`~Ai`YU|OR4%DQw8uM=|g2B{;1Ho`mx@??e)rX!p$MSlA70pKVcvZ@|fYLpEV~s7G z>#?88yv{ekJpeJL<-?FY7wf10XpS{B4}jy{uc)7esm&J1)ZYt5LI_{)0BkN8Nc}ep zg%SYD0Cub3?KXLY*-dYntrghE|}%?RY5i3yVcPFlheiJUMLIr=Xp=U-^siywr8MF^JAEwl2uQ$VIfuDFPisd}4W2ZxY$C`2`tBTA~ zG2P62@*~(9gYmO6#Ya<1TG#3rQd0BwVyNP@Ayt7B(h%z<@N>Iz;|2VkT8T3`anW@3 z03^F>TCLS9Y*sY)#=BX5!LYD9Z;z4QSOL2^Zw~0e;OutRfp)Xu83Yz~srLh8rR}fp z=#yHH{&=!mHgDg!b;9K@Ux99VmQ*K2Xn%gV6YWHHw(<_uA&($p}$2U2TIs7y+ zM7X5Yk#^wpDE4kQZmN3&VC{!nno7wD2`bEeAwS;W6>$oUt#~E57Imre?b54{c$`tHdB6GMC`IZWLL(%j20Bh zW@}9_@4EsYT$u1Q3ZPWkvYxUX{6AcsV{;{1w60^@wv!dJW7}rOw!LE8wrwXJr(>&Q z+xFe(e7mP=RLy@dYSfEoS{pC8KXH4kGf zd``z`=z(*mSdLiXj&Y{>&akI{IMzo@tD>a^<(r*Ssf6Nz;ZsaLra9mcD`MN8$2`!w zj#+BZCrV}b_c=qEqt7{oF$>wI5*0B0kP{DNQ5_-V9dZ<9u;vm!(L2I_#p*nprX%tU z!{;Gb7IuVBg7pdB2!{X!ZgHqp5+?drImJ(UE6~P2|C?+`E9th5QSv!}?=L}=tvcFMQuyE`=pek1zbRxBAFdgqqB#0~EkA_CpTe0`e$i(eyMD!C!D0SjSaixQMIl zQ>-Dj?K($9qMGwhRqIt28n$`*FH_6v*JjZRnIMxz-qVe_KzSGY5Ph0$(^e$r-hLD4T4m@eV#69bG7_fQ>o`!yu97p=$)>fb; z&!>)wS*Fj!ag#iKWRWiC735;`@XxXFT)nniSe~^1r0v?bQ6_Fokmx~(-O5D{7$d>R z#Us$PxL8^}t1rpnJ@#E}+O?`@a4wB;n{#!lX6WlOwo}C3TgP%?N=BT*FrxR=JR(g$ zJn3EhTI~xj_mVxhFImqt22JE`CI;B~Pb~*cFE>{uL*2mnfeKb_aYO6sDC{Khp%ba`v>+M4WqY2KK4@w{=P~Tzx42!1yHniJT#~*CHF5|TVC_n_ z&;r3b9d!f0;?+iQ8rT1N>MM-D(HQrU-WWU9=w|>nbeG#luD0;ayPj`4=&7Ik$Z{Z3~ z!oob~d$cMHx9;vjAfJ{XC6R@pzkLW4q1ak{?IimWUVBKithq`vKQD14&60gGKCCale{X}Ft0By269l*P6r zuTm0E33lN!&zezRh=5l@mQP_RAR5sr^}&4j;(eFAj2@K*7>|(4IdGb4yB%g88|TKZ z^M@nOtS|f?{!z}s#}S=w{R0`LbVP{k5xhlw?;F>N1tIByWsnp`Bg)hb4sZR>Y12=3 z!#Anh?EEZFm==f$1I@Zw1Y6-%6aE;!l&t#!4vB-%4AfB{X;!sT(jBKx*-5qZn|89Z zK%Is6JLf#w>eauBET9VUE&>aD*^+~!ilaiM?p&mM&kqY3D1*5QUGBbUOI)=eY1dMv zJ=ybPA_VaWPE1+MDhiYq4$DfAeVIv!IP-*#v53?V-c^a) zG6p$+O#_1{V`nNcS`{^%iBn8Oi4fO$#Q7x-$tp2dRs-etYmui-mt@P{hh?ldJJP!? z`!i88d>h`9rIRd6=^pZVuo5}3zUbAX>~uzA4C%servKlplCW0(Ta+B&Eey1CQ5DDV zf2Mk*YRAVjE>){hi_9poOCsx=BU4gQV)kovP|^v!npW_>^LFUzYHx;MKo!BEj7Xy9Xg-A6>kWs*$)aMAWh^_0Fnx;eR|2;L0ZjLl*+F1Moh4?D&8h6H6jJQ+OxgwJV51#)zSmqvRnQ5 zz~62JXPCCiwK9W;yo9-%7Xka%OtQeVDK5SGr51}$q@i)OE>BHgfOFiV%SZ5E(VC*q zYujoHFnnF^qs^WhZG}uBRIs4{4xGP&Tbtr=RJ?=4?;IaVA9Yzp!}H z9QDT#L{7Y?)r=m^ucWOjUuJh*FSmqL?!<1x{iOcP?l7BCorp91#(gUNGIQf@1)d1lXx(RAI zhm*TFNYgXZn_A}FPfh;WMHE%oCs8d+1emobQCt@YTjxcWoK81LeXY~+9)^+UOmeCk z)#LMg9G1`jWr;WZrrR$Gwve9&X+lKpB~*OkxAEnRpO&^BwsOm&TDeQBlvTv^nuju5 zyB8jH2{_Xtz=1n}8hD4nhhZvyxynbGz%2iKM-8|$N`wX8O-Toi=&@x087+joKHd4@ zsx+@?mPB(R?mMWCIeejm^dhs63ARzdm}jsA(O)QqT|m}QRWm-(Hzh#M1)wVV%1iJL zg(a=;b~-ZkGDk#mk1~G*z!7zGrRGL-8}=VILi|%;0knSAjJX1jZXYa@^cU6K|NAIP zkrpm_?r8?!`$D^>c>@hwX{b1l4f&cY;wwU&Q2vPM9oGB`Uj2&haf>bY84LFfn>4P} zUwt~VVTwui2oj$uGt#`OH>|MYjm8`R#n z{C%^u?$@fW&NV}iCuMF`&DU3gT0TNA(vM@&mV$M7yWD^p3 zN996Z8he29k4NFCg+9PbnZ$<&>5-W0fbtK7!ePTkfP37tvtUFQiW$|1%XoEZO`#0Q z2^XjxY40!DruxCn-p%m|j1RfInIaROco}Cf&3zhkkBHj&Rt=WZ_VkNJdliOb-H{>p z4n>c+XW~q#1M6<*boFS%=vdUE3ndU*iM+EFUvAM1=)%}A49e~^iF9Tr^(nqF(J^n~ z49*I<-WXCZ`1EG0hYOd%nsoM{LT8_q$a&QSBz;#S3YCwj?)0mjn_saa@O3c^sMqwF z!ZcWHQHCT~S|SVe5eVTt=z64&T=nI)wG<+4e2@}Gp9#uWEM+p-{L1PUC zM9N-bN73qWRRpT*YCLuK_D+uRgFcwsV}^odrD$A zI~cJDK#5qb8UPL(A_=P(=)Z0U`Aq`WLGuPhE^-isi?g-0`OZ?4kK^MyAsY+mxqt5G z-B14#h=^(sGv*CF8}cd}Xwl*_z1KEt!uP`_(wPBT8=FmK<+VOOk}fZ4Gj*{W-MSmu zygps+?d@%?tx#Fn|0(KF86C^QEgcz^1&!sUz|u||p8_`(gR(h#GELI8FrjSjfNCc zYJ9BHx9555<@$3ttNMYtIMa?NQe?V&_luijx2?!gBJ8tg}l4R@z5x73q4 zfZVtX0lZOzVV%@yTg!w5oMcYuMfGrD!RFwqChHhY`G22|vNLn!6a7VRi4gD!@Ae2K zT6A|%SwkYp{k$!ki4db&5nZ!Hg{8dj)h57Z<$r$9=s?;uzmx54DcKt)m0_ow(XjO@ z{}vbrW9)Fk2;8-9>tkzX!IEOW7lMb$gf~wwZgu2{whBB$YvW7BQSPQZQDy~)5Wh@8*P!VrB-YNi~zFb27ia7UtoAd`4C|JS~iU%&Qw1UMjN zC(CRqwMFj@{DT5Q%Z!g{RpCq?CpzVQqdKjxHQ1xa=u_EKr1ec5)TH;7hvWIn?hs@&K~48_$RK3+ zdu{2({Eh&7HD%B{)|+9CYaV^V1<$`JDFoj0UB!kwzCp*vlO(9kJe-Iv4aj7J^fJER zTEQS`H@RGhfs9w?M)S`;LliZ`Qvu3g2?r)nr?wT^cRJy(wBCr0MDqtRFHm$E%-!6g zMLRw$2+YPDN~0`{Vm}H&to@Nr&fF{~L0>m}Ghn>Vj81s`EIQnE@l@Jse`#}N0!!DL zkzs?x4I;fLH-LS+=E9Vl88}Td=@l&5&xyb1KaYf^1>c=cC+$#bcr7(`-gQsjD7Tws zxszZy^8Sv(2%nbY|4UVV<}>Y_l1lTjrKy;Y5${ej*V%OT0+D~Ec3-9;X zs?8%af6+X@s}jQO+NREG?W&1rhl(x1!Yfpt@?JLkH~UV_9l*DG6qvuakx_O+bAq=s z({A;t{jPMtJAA3|O@KE~J3M!)@g5`5KHrMBrNC_Vh4B|&pimlm=+i4!K-R<3m20bD zzS$Ki+QfH%hnUo)1S~{GWomug`!{WD(v+ zuvqIy(f7nrv3AgZ=8rf6?es-84@=OK6qbY0wJ-G zL(2?kPhb zZ{|(D3#69jUn8s@S7FY>F%&HMCc-%c24`6k2TkwB}T>7a66k$Rk>2x3dp&D-EP;6vCr%iE>GKFx;(izH3Le$SQsp0A%5 zm-Se9<@jb?{00JSx_;^KuDtmei!?oLZDoJ59(**b_6Y`2ZP$kvK4#2^Lk;B5oCirY zRlPg?{iEPr_J_ES2=O`sJ_qloEFsXBDQ+Z4sZubH45vc)72Y|~@)oVTzXL$U?w#*n zclYx8f%j*|f#eOo&_;}Am3`vA@XpB}-9L>H4kiQkO%r&~{%W@YWSeD_%B5+F67d*j z?Utu*W~cd#8x`Co76I~a0hZ}GzEOX;;hDT#z2m$G4zcHYIefxJIe3HizO!1pDziPE z*|lfM&rHZW`dhSY#7rpieqo!w>m&7!e)!(++5So5!vv0pL0Wxlkw z;_!rN(U5yR9=>CNO_J%S#)QEl@X^i< z$-v~-byW{BRXav4GT1VHt3jrFK9-@DZunt&iHnR->YIe?0!h%8oHlN&$VawG{+?<< zoY3lysffn`42Anr(od87p_%kBvtEl~1Jq51oU>0Cs?E%&n0t{t#)ExsgW$H{YuO*? z(`4X_deFhMU*%36&*Y&?o78sAOZl$&98gl@b9zEa>Ul`Eht&~4&@b1AzPD7{!Ati$ zwXVr7)>u0Sv&p#{4{|Qcx56H> zF?_X1-NV9Zi{jD!EQY!op(nLS=XU(DmJtXhf;wDL&4dvd`O>zAaBzN(?%law3sn1p z_#_Z!M+Gw0@Qk>REY&5+l&ECBG20Y4{6#618u0a_FxP38r-^@-!(PFvJl*UdjdBDn z11S4BYW3AgDE#Gc`TX_x<1XiTCER)+z?$_X z7n&6Ev$hKOggBsrg&CpBUpqPE1~%I*WKQW)@&B^`ZW5)SBHYAX27S#;6vo)8c5BcH z!iREPvmG%-xk%IahqAZVSke7KH%Rm!>V_tpH`>bSS4Y|tT-m!g!=Ni9VbK>Rx}WE8 z1ss1w(!|#dy?b|&w)Q0+&&lInD4O`WjJ{*tN3GHw8{8SD?rdB!ZRgxa1F<=81)1({ z2JvQ>m?i8VI<$}9MmtE)MyKN(H%%Ec)=3jmP)K#QS&7qL0o;%>!jhlVO3 z&jsJtdo5DnGgt&A^6{Y8a8ne9+lmC2B)oq7mWC?KoKbd`r)Uj|vMQx$o%)qPrk?b_ zW1Nh}Mw*Y_&LN|blw(R7 zFqMcuihIjBcSQDyLEoxd@%w52JEp%6+H?S#HPt_I1T@F@jW@935OmoG zE^SH~5V5=!n&E+yvOEFgM<8j%Fift}(j53d3V%1r9NT`}I%2p0$%QVx!#G2{NyO0x+|GF&XFcta601En$nx7I1 zQqAX}hG!*oND@sdrvXZQ=WU5MOE7QtKbgX45%?B?waqj`sNjDd- zUTH|{!iKvo{j~L-X=^?Us9D+2O!SG>$w%in^7zGGy+BMpnFr)#L4Zc0>7HJeEGS(u z(RiPD!>0L<(^-m_3%r!)MMdobk+T+6rOX^H>@PRjP^E3Fvx;U$0pz%a=(m-W6LZ}U zX2QnW7lPQm!-pgsRh$Rxq+tS|LfE_T9hZ*a3%%5EE8!rlmCi9s zC%T&Q39zQ(krY&I&{y3pYWA%5nHIL{j;9dmcaU{*@}l1i1fbF-HD&(6I+spEHr?l5 z6XUR+=CRY)I%wupKQI4-`6@A*Z2p1C5}Q+EOD4Yb@LB`10Ghl=YqM}RO`lWgijdXcY?-_PlpTe z5*pPp$8~kOI0r-}EJwDCeZBX!`~Vja_Xl`%VEZe$l0N#Q`pQFV5Kk9_nkJD}iNtEl z0C^Kr-ATPgZ(oeg!%ExcVXg|I_d=BoM=ZHAT`5PDZJr04Ur3RdN~zCSJui+P?cOm? zZ_4uvSbO6q9^3ohA?X&NT{--uRs)j1^n_QP0Q$3&rxFIzTz7O`nX?jRXhg1DeB#5) z(GfV1DF?0?JQ|Qk@MriD8NQBaWeKv2Q%Q{4hBkh-u_vne>zF%J~@`u;J25*=?$ zdhu8F1#*^Vel)g8@`n!4w}b9O5MZ9mGr6l(IoOWq9%{A1u0kLk75}< z&VTouJCQe<1WILdAsGA2MManwFz@+UBd8q0t~Z?>7i9wlMSc4rIngyRBL7^uYc7hA zBHUFVhg$Uoyx@ss=>vt^E5y7o;$7KRvv{t|CpAnB&qk`W5$c_mfC9N(b79uh8{1b@ z`%f{Lmb-*Z{$${zz}Myib@*kI7yMEizc6;Irq>h1)$KEnLBTf!E}{B15VVoV)p+aT z76}rh#zlkeIT-ez_6b@mR`!5_WT}T{kciOQ8yX_<@OT6_PmxrmJyWnWqxT>-Aho3b*pIl1(z(06k|pbILiK8h1e<%dkjsXB~8Vf{m4 z;ClZn{kzSkl4$w-j^Qx`(3BIce`g>_bgmJy8*cgJ=8Ty6LZs*o(tJ?TUi$1Et5WlE zPm1hE>IZ@-G>o3sf#8sEAr@8W4+aYgQTPkDDhUV$hNQpvpEmwC*qRWQY}4A92_0DZ zmPs>)&dZ8l5)X-zicS159QB4{Zwz=3=NVHv+vF*NB9 z1yz|msvE4PVio9vx4?D z{ZQdbB!aR@k>T3)149tjYac!k9CIDV$2WZDZLI0o-b>X4G9HSuePIX}6fDMrw_{k4w^WTJKctikHje-7u zn7gF^^f9vkrII_IBPZA9zyVn%O~I^a3h^!RY1?E;v_(46klc%M2I=TV%+aGbx1n_|{GwNit$QzspH)ZRKc+9Ky0a-Mj~~W; z9=1QW{@mQWZ0CL4h$4e)g#u@U;Tecj_=E}U`TnGM7>o{0dU4MT*|8>hhQ`?UB!zFB>>~9<{V@O>aC9U~Une3IWIR5R z_5_;sDvxI0ns0l_QeF?}X5QNM`1(*9drDI7dr~8llWtCKyo`HdZv%?+Yo+%2`Fb=5 zKSVr%FvKu>!KA)Y5&sPD zuJbS|=5`k){vruC`iTofuv9tp)kTGFd-$o@dfQ&XgVVImF;1#Xx#`I3vul#F$qWYb z%LOU(SbQDVH4RnT>9}Wa7hO`?yKvd%M<7B)^-9gvI0d9NpIMkS zRT00KAyowFDZ=SlDLo`s`r?978R0T>hJCU9`HXoWFBuyu7Ifhz-OU9hFUQuonGfWr zokmWPK)otgYn@!v?`Dtcubl8K1%*k2j$mrp>~SkW z=^_So$+T1|P2fC#QyVCNlVUHq?y@pBngYPoosbeTuE5F>N&Y)$kL=WDpkyH~cO!1J zMU8RHS*10ceS^H7l>?Ax-ySAEq;fFak>8M}foyYCs-;Rmzg$T;k1$Bi^ZQD=+=cv~ zbPGjC8@KD2%G>R7`kXxj(wO;v?YYy^+8h$cQIphb3NS8{p_AkYO+3 z@r-QEvcg|3shClf+$g=3b_M|nrQ|lu+E$yX&=MQ;_k3cF{6!0wx6Dg;;-oBc9EN>k zD#NH0R)&||qCZOZwIv9erOFWBUabK&8^iW^&#Oat0LxZ=F3cTrBau=&v4cK^>5k@gj#zWtyXj%YL_X!h>bYx@JNuVPpBwJE56w;HXl zZ1;k@d>8+2?a%T+rZv`KSlm|ckXJH62?JJAR z7ldHyEgPiZ7!yX$7!&3vTs-Y7hkx;Id(DrB6cEMyABU(*M((X7YWt-L#i`S$!5}fl zC#oXNEBbfMF4HSLYC0$tY1Q-u&Ykz7^Eumbt#?%(T*Y>yC7L`~p}oAkt~tH*7e4Q& z$EWB(at2C8c9em~sOw`1CvA#}IOF9Z2~%FBmb4G8IYeC!Dm&P!zH#Jna-NO;Qd{(7 zATVoYNg}*h`Jn02H$^WRu1L+psWjwYMr~!BZZ{afjMr|Rh^JQYjck*m8ZE0?)~vqw zSAykMDOKwNT}~IGR-3e435!bEmBPlvKn{**+>sru9y;ynv+RdQX`cNo_%uiQyM~gY zkNXTcZ~J38fc(I+Tg@T>ta#K|CyTKv73iu?Y3>J!+07C?lcTyZWvw|?(w33jJN{5- zynWxvFsqw231<32Aj^xVe zS{qBm^{P2re~|C%4rPHF|F>PqE#D4Gqy(PQqW(YSb36aV+ngr7;Z^rsa`1CFOVGl|5mBdB0*q*?%XBXPjPm^A~cwh}`D~ z?6gO&d^<6m>+l5?;>v6BSph|=1uthK(GEITC3RddQQ6I%I8e=$ZwLj#N5a1>8ivCg zc9PxY9k%zK80_2>^XcdCV4!Dqbplas_v^F62wKZCbfyb7Wbkyg+t5R?jVp_p=87)rAsVG;p?@}0DhfjF2KY=ur_sDRN5Z@ zBoczZ8+*l`4CNsWF7`5M9V-hSSKJz^0xO62%BvUldB37t{XX4Ba8~4nB7(_iRUV7C zZ;UVO848`?$wGFpL>#F1+QXS!7Eecu#h!577tuSg z6^-(>A_N+VK1MVMP=Fhb(cBTDWU#U9m4gz0I*3`Ekeu#d_-kiPg!qv3`67kym=Gc@ z4AmeEJ6{D5GT9l)0Nt?D)UZ!J6$_sfK%VCX&4dy{lH3oNgOFQ2La|}=(_+;?BPZhJ zbklwJ?_h@!#;1t8lY{2DbWMd63lRBe~A zUI018Hx{L;2 zP!4pmu_b}ynHxga0}8?m18nj=$kLnve9s^Ie^-H@{|7@7h%5N$^Is(t_dm!303><- zFJ^N8IbO0tDI&&}NbSz6da0ByoGx4z$_S2h1eJKQLn#puSq70^es*d-_l4(XJ#*_n zK*J}P(truL6NXuaq7uz`1IeN|p&1V&u2eyhN#=m1r|%dhlWusBQB&9Kj?1K#Hhvs^ z-dw2ubqArME!@rtqD~^LMn}(jgSFkP6{lq?QJpdKZ;mfckF6(uBjSn{+8(#`kG@;n zm3xcjQ0qycjaDG+MetaBT!=+z$|gzdx#dMIAswr_Th_kYiKDKk!&_UmUaRf(O6SR6 zzMcwVclitdu{K&Gt?B%0$DH%Ka)m`JL6Z#Jpcu<41@jFbBz1!FpuJbOJ)Z8kHKT}Q z_!}IRR?c>0&Nt&Qj;h!jwPEdQD`+lYT-#aWIWB5Cq~_MoaCWl~Jf%0pW3b z-Ku(nGC90fjj`rXh7Cc(Xf)$}yt?d+VM=r=6)FS@`OQ&6LV5%jY**8LDEo=q2-2;W zXLFz5Yj$C0KPF35%Za62bizyq5V&Un=D1ejqYy`jNUkEZx`7gG{jZU)SoHqE-`bUo zsxgy5URx|pOM9qlM|Bp2^+Otw#8?sx1ynFD)OACtwIT+Y1B}#snwfkd`ZNWUuZ1Dg z3J5J&JYAt6fN_#GTqdGv#wb8&nj)t%)0R_2(EHvf6Pta)r*dD@@=u{net~%WnTTt@ zjak199mId#cZ9@4m$bZo{wloNngnd}jm87j!n|hi9Gq)eq)1}J2NY6a=#-LWMACKc?Fn0eJgkvFVwzHPJSCda^P{jTCuDdIo7gYl<=sY)}+_Q3T%^*<8y46+?f*t zH^<~z8%7i-y{g&sZx`Wx(?%_9eB=1?F3Q=~ZWpcXS2{)%Z9?Cz?VlQHnd}xq*zI2y zC9dbVFHaskv)NGv?a~q}@_}vlro>|<@v`XmF4Xxq2O;^%wnr{e?a?y4zMGVO?J%x^ zqr6{Bq#9Sdib%!nZ>kG=6?f%d7)P_OZ)Dq)iWU>+(HwnZ2ea?AwD@Sgm6u&|?0uVx zHxW#~O1#4B=U!!E>x~yKjHM?d#H@c!rP-Zxm{VDkNw8W`WrERLYXUVKYIYoFqPj*A zFD}v?HkI1j_Hx{o@ika5m+~!ax#-9xYI>XIWkO7@)a8b3_C=V??O4fZ7soW&yvXmK z-Ps1%D+Tf_>unWrYEhe=B?nJ0+0j#f@%V`N7WrAJ=nVTZJE zu||VpNVe*I9}B7xo>6jqrpD3elbe=GMt4c$PzD=N*o1C^{TEqP{ol-`R~MW*V!kQ% zn+%OSPE%}dn?Wye?nKP0-xm5TJ80J_9&2daEWBpADhIPefDBt{al>tbKt)<2snTIu zZ=8K+!iMD>YoHCf*0G)b%;7n6H#1R~!v@As4^5D1lst)5TM3#`b+OnbI8 ze2bnPSnwdjYL}M91Q_*VgiH&E$IwTZ8S_za4*+yAgj5BfnG{is4=6UmO(6JZKUR5SgyC~B8+P%s38NFVIE@Q6rfXPzmilun?o|)VM7f+` zBdcF#M3FbOR$Q@j4_G#;NQenj3gRkK>d0ZD3{BN3G>@?AF2^t#o1j%e<=&-KcS+6# zm6Eq30rjfpO$--s?Bj7Y=s=H~<(V?^04ns*QVD^CIxlO0hb~rThyP*JH%;Os3o-J4%j@DjkQ* zLeNu35%fvejsqOEvSa^M)%+~Sb>V1HspK+y1Fw_zI1{Y*=POV}KhLx<6ibQ~4s47T z9GzXb!%Psmx}s#;glavT22gg7+Otqq7wiTH1hgtBRnI*GQ#>D9U4?Q(U=8Ef&r_)N z0=gyY`$sC*AdM`2lT31sy!%Z?Ys5TOU?=+5bRrov=-JL8B#s+Yvyd!I7ej~T!?yqB z0G*_hL^v2o@bg96In$!D)){V8(7HmoIrS38vkt=Hk`(G)a-;#YyjiDcdB0a)e+l(c zZm;JipJkXo>r!!n|Drb)#WeSzW$q%|2m4c~$7Z)uqb+w8Cuw%9_w^&^?xo*ck_nj3 z@uxkG#F&A0mw=OGT>nKcYT1XP=j~}ze zn><9CpZC;te(7Psr&pm%h}d%@$tGvUmk74-*flv?d+qOAVh6;i))(ag1T^!K6{7w~ue z!|EGUtV7CwfxW&=hxs>+K1hz!@B+U!ly3QxjW>KHQcY2c$WirWOqv|mZz>>sCYc8( zb%Zcz*FDj9+sw}1&G{$)chro>?Mq@q&LmDOu;2mtO(FN?UjNt5^ovxp;t5fo@QHzU z;@Re6YR|x?3ORQ%4G;Mm9#`^!7H|`;Xumbak->7ftC1n_fQOOC(Y%4vPXoHvvjLG> zc8D~=@;n6U(W)GDu&xX|!V_A-YIzVVtZDOu0=ci9mBwRhz zFqbia8@GeR7L*&w&8f2`d^!*4v5n9uA^pY1j~onD8Uz=Xti(&Y5Vt=jP7-gF6G4=5qf>o$TuBF<{bDQW z0b?DoR%bxUoO?s<1AS5!>{}@}*5I}_zrca*l2lfIwAeWp8$3sC3 ztEe~-=&EHrxI++EdY}cv7fZKqiMa;iYSBl>2Oym1mZ4f5e0y;F2GSZMs^!hUS$x*a z2x9lgyVN0Mf+2;s^Orv`y{3ztYA$?w2dJ!1D4*;^h;JGzMmFu3ry}jIu)6VTR`}{ypXCA07t@KT>O#Gs%@vd7>me@^RA7eN=#Q>CzXb-L%&MZzWdOV}12D8!Qm# z!NxL)Cak9k8f)TR!7r3e|{Z$-S|MS9FN8DrR3$qkh}! z<`ucgSNcmAQP!FnVJ+dIMQmR>##46@b&ruT(WY`9yt%YXg3x?K^J#|)6Kj>n_;2)0 zm3y_Qk*;Ud)nT%?iqrJm(>i>`eX-3+%cjK$o3rJfDbTKEad5T1T|O7#9NrqHu~rmt zN#ozS^(SDrA zsv(RB8@C1~R?f8Zekms{TPVD5IM3Z5td7{^#dnE0>oo=gjzot0pc|W2-CS6Sq_xY2 zKMDYyz&m62bzH&UjDIx#Y3dY%4v<=hB-68UFkV`UdO2n=$ z#L&BUcq-2)V8}*ybjF?kFjFJjt1T<@KGe!$-^(q=N1LgKCHaX=4v=|7;o~<0rzSEhRMu+*`oOKW z5?SX<;N?sF@l6-Kc}=7kTvS>_d~#^UkwD#!5W!16`VLA}O#fomaSk+2EKlne)J(XWzpHxYn7?p-1nR=c# zTBjb)7n*)FYNEN|o3!YkmYQ&hI$^e|!bc*!!0>rekNz!DNYZ#$6A^S^LvoH_P$Rlp7@a zv#OyyvAiwaMX5Am9pv?V@u_5A0mA!KU|3&r8 zpROC7?dY#2mr0fJZOR46^c1;}+FVaQ9q~Ysb}-iX@Fj05!hZBw3NZdz=k&|W(w7ht zbW%mADXI^t)}f#^V80V&k3;4+rO}GH9b8#W9#VgsSAjF*maJdH`dPzgJo81_2Xj6B zJ?M*!zA#+fIE5N^f$!-N9dpW~a%ubr zd_d2GxJYsVk4Ts)vAZiCi+n{SDW=MO5zSQ=ui$AD&S~!p9(aku@VF^KE&Dp%D0f|I?$O6l|8FC5g+$-iz8m9mo|L&C8{W5`2ds*u}tmk?Njg-NH$ zuYOT^Z6+X4k3hP4;z6TETdvNR=lR#Nrl9yIl_xy=)8Zrf?T?DGarFi;1Ez}5*}eDF z*k0GJ++IymAM%H#tFlzTmafY98Ox-XcLSY8SwvFPht`ItUu$z4q86N?zTuX>LiAb= zlK=f#yCxc&orpOyjF0y`XPSLU#kcRfrbv8KNQJvbMg)Z051D(nq^I#O+N~k_rE3^b z7d~@V=<*_xEmBf5X;pk)FMi%&)Db#b=!dc5kMQgRc5;-gb;nNfstPyH)^Ix8@L!5{ zlF1VP3$6U7zVU~d<_qiWn#c2qxq?4l>5EY05pwrj9OV5a;9Pd1I5*(JJPX!(wjzNZ ztk+_oHW*koHw&sj%v}q8^&1R8`YYHU@|{TOdBLH70I};=UY@EUkS01XT#dOHO5)we zAg~vu^3FrMVKr&i1H#u2m-wJuqWB1}w_x5H(JExSxDp4Qq{9U}k>OtiWp+5U@H6vL zBilZ%XL1Ifs^Mk%ad$;&xX#5S+!T>@H@Oek$1*TUQ21Cg<@w+eVAbh%`sIUJ;&s28 z&b|j-P)*TP#fmBIGS^y9D=0=;SE@SUw34e=<)|rOh7_X)eQ7I@l7#=2=zL~?Q_zyY-NH*)p__8 zXl=T?l&$Mk;T~zeH{2`IHP5}e<7FBv*>4~b*qco{T4Fe{QmTwndm8vgt**DfC7CYj^x4(3e#4BnUZyCm>k zsypku(lIZ7|KRtdLkDg0(`D|@fP#}ehZPFpUFrPB%_3QBQU4Pv^DH7{W{U;8ceoPy zV~^F5{ZZp<93x z9h#!%4@8_||RJ`FEIb~EFW}a)A)E--&5iii? z%}-rwtJHPYM=>hb??##Q1)hIGlDOZ+-FDeHJ%>og3OCN~H?Z~H=Cn>dYeGTf&^G!HJ;=j{ObHef}gi_Ld zJJ5hmjNqRtez^0*hgfd>{R0Zxyw&rJ0*4)#u8s9yzg-C?d25;-n4+(`D1;FQ>!(sUC3!(_REC? zbP^_^zyPg9hK;2vAV8PR6|A__<*1qLq6$Eq8l4S6miweXq5?a-nHN^HdIY!f_-o@u zp>Y<5g14Q{Vq)T-cj+<(iSIn49(9+qkL2C3?9iuc1&4aE89IqL*f&6a^^zfQ!1XvI zfXQM>34_t9t82$vL;XRil9PbsK+TGPzDy#&S3cjbOdEm~NI6t9>84uAq4u_*#>l9q z>VI>bQwUr-2dEYXydv#&S)X**ktfYGV57CIm05Omhc}Jl(!cnjYr1cFV7GftkGncB z&Hn2ZS{d3RwD9IFW43<+gepDlSxb;sKMd4%92<=IMHrjqXOhMtmgBT~)AzY1_Q_Nj zw@j(JDHekRvv=jqG7SP@l9|N~)7YfFU*pUw<#ReCAH21<$J61cB~wM-4wnZuf?!x8 z&@&FDqPxuKW1#{Qs|nwITE(P<^g=KYP1JZt=8t1#dyQx~P)ChKLSV$ir527yem+}C z&!-)ct4_`<5j}3Z5e_5){UC0`%OIs5&V!TEOyxa5zGJiDegY_wdbk620d=Q*!#?^i z2(l5VjooD9Z%&w*U%NHIDy}RGVS6`mlYp4y-LVW1;yhH5ADCa|jvjb^77b)wd5-wz zEa)Y94>QRui~kZH!G|4I!~88=%0&5G0eO<-nmHrap#K1XR^grjSe|Z|icAjz75nrP zACVIcUvi7-|NNp!+-;Hwr2EQhS0&}q%-04`%he-MLZ%u)DE3(ue zxb}WfOasYLv|TI5YXcSpqy`fNgeG}+nlPF93JI91>1BvY--xvJTv2LSv#U(gM20pcy6m*!qT-REi98kj;igw`RKd( zC~Lj(W4oNOhm!qSdy9MN+v(nUxk~==dUOJzzjMH4O1xV@F(@m5V@h|b4a{J?WriGBkzCCt>v1AD;OO~ud zS+hiL*0B>p#vMeuS<-!EH+B=*GRP8IgoH@h#@K0WF;|rG%kOEr_vJO6f6jBx^PclP zbLRXpXXg8SK7qpH#M2sM(~zwCG;wtNyn?vMWGJEWiqBj0IAtfzk9VBXz_y~AHU6~9 zecjKYtN>+acdRx@uVVO?`NcJ&LhT1VM{@&HtRG3?=|2^Z60B~K*p@boc23}r-TbaD z!>XBP(u5m`S#SH_8J3gct?H5V^cvy_&#begx)Yl6h2xK*oRO@Z_Bk#4%g%EXE^a;b zkdlQ0F~ST`@j9*Ukp#&{yF1LU&!?+q4-voEIiw6U1cY^&#p3_)YP{yLY(Agqbw4*} z8(ZHtUQ70I_%0rD;mz}WmdC+0xKo3QFeYCmLt{d-lfmT;q-hFyBwF=F%k9>_`t!PruazqK8B3CmUW_dDa zB)FO$wiBn55}KS%KJ)C|1^w#z0|)Q6S9)z{ffONO7hcJN5)R|W9vdu zoyY?Fc{jh}d(4(E0)-LvT6x;Xw+t|wZ!NgmE6k&T#;PUpagBt@kH>C#&)1QC7t?o_ zAGL6{))=~`ebD+i!0lx%G|ZSqFsmA;M>fkEdtL1C89?>1IG+_kb(Cs5{gGC1!-(ON zM}(4=p|PQTfWwU^_usPnyyi7ADZw^bJ=~J+bw8SzTDySd=E@>hxg8&3{L`~}(y3Z% zTbEOv62Z1^`_1$_4C`-6(Z~G7_vh=SAG#x|65B2UCPq!?^i5{&D_Tm_eSWw1uIHig zn@TUk&u!KYG7rm4?ApX8yR0$1&ey!0O9w)5rKNLOWZR)+LC!X^mE!XjZypOQMFo== zmvnO_yf}T-26K4YI!MOfmLivK-8F#=<~6fxyZh< zDenbKj-#aen^9$u0nf~#{nX>NLw5e4-uETs@zK<|UKD6Yl2Ed0Icys!G>* z`dZe_AfCIqLx1P1+N6?X{7YMGtt7VEB{zz~#I=XoGkH}LvBRHap207-`iz$gn{&4{ zh&b+cohV1@otped*^G;Fg|p-3hRt5gX+$C`FV>nOxo6+yY`w>cwW2^NMP27@_Lw}y zeaVVqMbe^?%#osXsOgU-hFW-hvZ9_)GLOA;>wpBC`+#W8jq)h_D@5#SkY(|uF!^Be zvpDxpLH;k;0&3`IV|#nk1OM7EvmXh2`2Dis?iDd54f*uw}jI5THWNIpIqj#NNJ0^2-^Wl*XFz;=xU8n9fv&FLCRIMSj7Q{ZWQ@hZc50(s; z3m6Qr;uqSO66T^?IXs83+G)5t6Sk}PG{2s=Wk-sPcMR5+`7w%`ajV|Oy3(43TSu+C zM~-Zmxa(}^%;=3m237SDD%R~xy8}xO5~CNQrV)Ltrk&z;N6jZt9)3}| z@p0saOnkL#elg?UO_@Ig`wP$CW^}0K&8wf#eIy++_>C90jd2LruH+s%w`}ihw92os zil}cNBDANCIN?G$uC+&?1()6!CWQzL*!D=s5W4p6HKG=QYwh{gCf&{3AST zrcNN5Ph~ju9%GXq_H!sthKqWX%||#6QQ)I!eFR95MgKL%q5H-4IkR`d3zHeeKHiFy z(u>-81|;aIADIjbIk)%244uctVlG#1_LwwztihjJ%A5%KqOMyC2rvu|l#eN|91lN5 z=Nt%}c-$Ej=SrDJCxNO7n}28o!M0qw?(~+_vJ6vZYt6Tye z6T%7!VXP5SO7V$#{fL1jMC{}K@z(d_t)^>op*uwbQ*~aco^uJ0YYm$`n&-3CT0M4^ zFXv+7eDBVP03x6O-dE>vRE;nbk$iI7r0?Z}g>Ni#E!lJJj2W&fiz6x=Nh+D04r|@# zfX;@vAkD%`Z1>BilpnVOI0lkfdtaiv2ozv;#fqmZm`>4^9_7-NWrc7gB~{=VO0r|6 zi%rTpc9bR18A3{*7gMjq+3UOVpKWMM)QH+;&%Km}>K;^!mqB|X7TOYb9#>(mT>XWq4gBjFX0woPN(1n^o!XP zq~rFHG`l8OKHGr&=M^G~PMXO+(xsUFhg$FK8?}<)`m7;V2eyLo#pS zkX&aXT3)!$R%e?x&V7=z5>efncx|Ql+l*CJ5z3#j#p$}#Gqc4tP0QJgNXW1p`S}VFsL_g(d*5kcnN{R|e&8PrW zKTs&SOM>;#Ax#=6M1~6G&d35Z&T2GJkrEZ6pOpa)9IJjGsXzsSkdS{BB;hyeOv! zKFJJDEwaGMyunY48gwI|%#ti{pmXrs)Mit$ZQHhO+qP}J;Tzko*tRRSU9oMal2ljs=<)aX`hJabHP3$5o@<>0 z+y`6!4c0*S13}rfE2|m?1cU(-1cWwa-VZZH@dqxz8+{Dp8!E4*e5J^>D2lW|f-j0x zo<(~QnFNO1pI8`Gd=Dh1B^mL?ab$;(Lh-=8JXtcDpd5?J1y(UPr2%wU(aZOC<-9lL zfcxF*)xE2UIN)87z5VfIhVHN5;|_d+;QhP>h}{S&#GHB~#GGp3!G^1MJbr%lo)4`o zc_%nvPRltX1nccyRLGDVhDq}twP!iOEwD#^U`j(>W|X!^l(A2Bq}thVpjupbJb$tJs_GSbRy=NhT>;2vm1Jp_7P7}k!J11JV$6$a@ojwipW`qx8>vXJJ zJ?zdA<96Wd;j-7&y8wUZb`0vX<7W{%()c?7O2Z!-sp^ecl~$6a?0}R|mAP(@jFxjh zIhxOTBZ1C!Nb1X5dw}fW(aiP!kXA5QDScnJ7E8 zW{-~6^Pn2k&Fjj}2Ckjx{MvEXtEAXY>rYahfIyx>Hw5VZ;Rj7GOVwBeZnpy+Dv>P! zGjqds6s?W0{q=I8gany>eP?xNX%WZKX==PuvH9xy+WvMz8S6wDjx)_Zewge9Gq_0k zEAWR=HIJ|Z#=i8{dR{C6TMglt_Hv?R_Lr}FzoWzvzrxeTP*T{hrUn}X4n&;~;bm)n zhjTJA;7Z3(7NN6M_mgz4;=Ac5MkX47SN*K1*q|LqUH{umM_55_r&15}m{Drjev2>) zSD%5XQJ(QP3Kf{R!Uun#|9FREeI%^-Jz|lJy~g+~DJU z@}jhnz%n*4U3{jH#O4aLo;oZ~;-*?!?e`q^m&_*lUsR@Vuugr{mlw7#;AMPBJq!28 zFJVD=aoQsXXU9xeE7pV7LVn#q{p!VZ3%Y7}jE47Oc_kZjN{$2I_Ih`Hid_gb!z77k zLEPp?R;<|(jHShvV>3q;6{-VZbkCCwhse5}9x5_xyKM(xnjv^V-XBsASA(EHumh^r zu4uRPY+C7=BU8QW{OGSZAfm^B!Ait0-jY>*sG>$R-+;7@n-8id2AU2mHkJf0=Ox7L z3wA>N`?)k>o~;OBOg*l9-c&2Ax>sd#(g1YY--PWe-tT@R^ihOGFOUaF!s{7t|8@Ch z_a_pXzZ3hE9!TK$1W#azp-gEOQ-WuU#0`utpn2;A8trA^l6q$YQF51^@s+gh=n(ox zoxo50I#y^dUD+qqZWwdRChW+6_RmN-hX4{Bk=n^oC1Z8WWcqd|_FqA#1Txzjttspk z$qnVX*9wL95^mN zFaghCQlK}=ONlTTi^uzFqhx1MtD@5q52vJ+NFxQ!u7FgleEERVM{9Q0KxyV+k(#!U zjP{AHSQz$~(Idp)Q>buZc_HZTh*;6r2LVj?1C+I;u46gWXMuJCdyY<=&+h zm4(^0&>UeXB@WOkTUHnuLdRJ}V^~#YwH&^#l%E<;i*sXUO>N1{m4ma@FJx=_#Nw;< z>DuvrnXPe9bTKX@WWBobWN|7oK=)Lm*uH{jQz)jjk}-j>shi7zn|@FwV-hX@U0v25h!EE-T`2>;fbnoybY~s9BLR+`KF%Q zDzbQ>Qv(mtg1L{<#PeylU~f84G=c~OVgw9kph^bB%mbG$j0Gi*<7%^`biLCi$6A3Ua2o<@&WZB%x_Qab`4f8RYu2zo&RGMRxDj1!RG($dfM3s(BZguTy zLQ~Oa_37Ex6x&lHa@^$nGLNS@^H2-MXqXBgn+7g$+NPHtFwcLI4Xtep*>ku19Ga^p zp#I$0_;mELs}quj#0<%t{k44%{7sS|V3?G1-3ZXqJ$R|-W>adjIc-=-Eg~5@2km53 z@Xnl(UkDbZjcc2EDxRKDmzlg3g;+`NXn<32Cs&Gr8M9>iNKNBkYED;3NV$c>%@2(7 zGuZSz;-4HW^C9IKoKie9{tDcJelMU3LgIin!vgno;{>zF^|F}Zn0+;$q2u1o;iwNQ z*ah^oyIql#CiRE(k02Ch-UkgWPBjjbKsFW>pRn$MumX$j zqFLTNU8r{i;*{D$hD+hOUa3_r7*l8 zv!m^zk9RI`jl^J^vt>t_yJad>q#1C=@BvNJ3MPiI931*tyGN(dfE8@a@$)+PFz%6ktHtd^7EFEspL&_D^Xzo&X6_DQ78wf zz1psXF}CZ($`6(2F%C09Pw5W0$pQWGyoi+#B$=AsBzZ;_@JF(*yWu_ba8?#NS)qv3 zq)8|X$tO8<*Cm-6pLzt=@HH~~Whyl@SnX7DTU)W*f~rdggk(W%Z<}b!YT6ltALyJV z&W{eSCYIj#IUky_2kCU`3+UF0CXWJ{R8hft0T~UY^%aGF@Oo1BC3Im`#{kkc7=7sS z8CyJwKM+!`5Ng(Bjw7C=YqBjR4pZ2q^G&dX1t1Bk9B9@gNUD)hE_4oC1LkMMj*Bml z!1|Cs$=oA49A5dB(J*y(pS)A`;qu&G&y}CmAx;G$aS6rh0|Wz#;j$XWiYE!A`t z-nl(heIYdB4%$A?#G8lH%12=MhxWT30nM>+I;h~}7?yr1=LE_C8i57|Wo6{sNQ^>; z76_DvAknlKbXXCYyWKW}OVJIAO$mR9f1kA z`gr)*`~ttfA25CqYm&2*ElP{2i^7qjnqohhLcekYd2ZllD!}7e;-T;lQF}5|iT6py z$l_@r6W(PRz>DAk+cMkZ60X498M-8S!#MJ%S_YjdN(}{_^tcey;R#>;6?L~{leV>u zPbWCJT!zM&*IJeiG+#{cHEvY+ z+Lzy+60#``hEJ4SM{BO+Om>~)RW=p6jE0QoZkC2X1^f$hGAhP8_=LV(#|^Z~1k`J`5Y4{&kph&!7&$xsda&#_|163LJY#sev-!dySjv~soVP|ZwnwS8hqE7eW=?jZIr zi|q0V2R4CbUK!WWlN?7FFNm=IV8vl((EGk<62$xUXcUio))$cnA|RzW;>9U(Bnp6*3SvPm@L)RUplH%j@jDW74248VZ*?j*TrNov+S$c>Dg~fOE1Sik8ABjAeJthLGdbJHnAQl>~+P~ z#8EO}Y7Or4mzgHx>OH=BF}4#ZoI}bJDIC?5J}a%Y(U;mvo%ZW1r2&8f2;ee-6!*6Q zFsae|^`2GCb)p)TzZ{-!^I1Vp@Gyr_M=`Yr)@w?iR~9Kw1~6sAY<}DOF4BFc>oH<+*sWy5S1`mn zF_U-HR381t#PQ`v5doZKTAbNU&Q!FVsUhGIj1!oSU@eSlp5BJPTk$s@L7bUstn`sLU5{#Kyg$T}jmaPaIaQUY)z>ik7Gtj+=Nj;AU=gg&6F~`6+*>>bh zaKRIBVV{_t+a0vt?L;AJae1#NN3)b4T4J^{&oTSdK$>TA&jL2srV0Bw&K~20G=K|j zcmh{_ur7h{M7$gy0P9R^qHnt{2bc55gi`-njR>CF3==d!!^0k-~D{^(9K>;EN-H(QO zcZVNtB+4?UGKW*dGw=#54>WJ8zmpFY%WPBA)rS~ zPf*sTprcOzJg7evUSu! zamXo{%o5}g-xEvC$qkF|h4Yc;6zl5`G@*CeNRuDYY_Il}tj5jasMb`Qx$ZH!@Y3k6 z+vHg^XC|{@Ma$u!yS5RwTtFrB_OZi>IH14e>hHj(Hr+h7{XhjbX zmagNjzDdLH2|so87G^T9=ht^OPok%n@-B7JZd+EBohHA~h|rvTnJWJ-cH5wU9a3e0 zvh1;5>}1vXA)efRhiI*5y=m#|(c|RZ5MCv^G^Vm~bPhcT-P#6llM1*B)Q=|}n#G%- z`-^P3y#>dghcZ-yeS&?^yJeObqdBxnZ6z*>=yfI!cY~2T5*cEWyWcUED2Q2p@DKoz z^OkzZ20>xZGW_|beg{&(M*r^H<#dy|iqOg^qS$Jzp;gQ?*iK&xyqwoSNqVV9;-wY>Bspr8Ti;34;h$o4MC1^b+y{g*55ZzjeWc6f)u8Ng9YEkK>jNC-{Gs}VJgcq(_Z-0ggT3-5t0G)sPE93~qXib;- z5LBi{NKsUJY%s)ymtC2A6uR|VkQQsmlZ8kUrOP}~K7(I=^oSkGxQw1GjA0^MV%;%L z0MBEeSY!ch`*juR$+7!jxlX!YaQFf2)qaVx6X=@~yOIY|;Q7Tu&urcxOemAGWQ(_% z&%;!GQtn8uG%}mcAx~*me%RC!O0xY2>NJ^*f>P#Kp-eBx45d;fTDndGZeXa&yJQ*0 za^P$+D(OSmdXmuwlJN$mZO$v0QWU^gG(CY-0dir%z;;(1zsS?Q1AKQj86wg$o7 ztaYCK?g)FeF_ehxGfp3bBUXIuApba`PhLixgH}sI7BA?5T!650fhsDPJussQVzT~L zP5z4y@!x}?g|=E(0Tcw}790dbGQ|XgAO(pKDn<8@0#K@EpoAuZF5va2QMp}pDk7RR zQo~vV)0?F%tU^IPdpV&b?6r{KV$U;U+A#_+^7mH^Q|6no{|gb${o(8lWT=GQf!OKn z7SHRJpQ4oz;O`yEFG^0h1{E6PX?mV5jwt~=Im%x9VoS4;QCgDzQhy8wG}fsV1JO1V zcM6lDQh@)v|NL%>uhf-KE=_w#{GDgG=1DGP^8y_P>Ioics)A5zUA;TspE3o<7$qF=&{j!*nQi@J1H*qy&fRj5}9W1>v(;&Vb7tAwk0(9 zX1sh-ItRzL-7*><-FadFS0C!q8K!i%5?|hQ67tW-8Q|}R+f@|t;Ic$CbWHI!seIY3 zIe^OgvEl}gt)2MvJ z;gtLYk>PVo4kG_^Iw>~XrqR+p-OR`089eK{vweJqASd7@vpFlX(jNH;^z~{Ws{A6+fmmO=-OL;THV; zus@QT@>O?g;0>5_oN7s6A7PvE~9pb-ae#N05e%sWJJtWYNI&ELSq4mldQ2=9# z`vU(jc>Y(av-6N3Ae1N|AOimb-s~ZM${Za5pr%El7L$$7&vy&yFYxq@%bWY6mo25l0o3OGDC2c!%j@--0`U3x+zz69A0F$wMN$02 zORhsol7=%CP5jV;jLF3iwdX9hOGcD6I_cCYPwEqhIezA^T%Q<77F`*0GiNr`~`L^B*Mo>e6ZO63)@J@Fqo>rU@%4g zBQ>m?f}iZCwpg7>R&Sj{rVPv+iupA-bbx1enWI+;``7|Oa603ZVjH;wL(-z&0Znn~ z5H9}mw0MTe1(!`*@n#Iwq7e=93k5VifES@sNo*bC9=`!3ii(saI8k~MU(3w{W)7{j zUX%$8JUix+_eX&S!K$iFTT_!=GiOa}i2>Qlq6IhOcG@ehjGEgLCyOEfv2W?$yv1pA zIb$!pW<8rs;3lQ>&p@Cd-A&~|d{)*yLI7wXBAv);-Uzk8`9NG(Ky@37L}C>qfUd6e zgMD-F76jWB3f@)Y8FvYnC7_nl=kLP-EIK8{+(i0@Bh^x9*Ey`dUcv1SFbl|8Wbv+X z+>Dkf5qZzB{ae|1+de+rvRmLoGeaFkTUW>|t2w31FZASyo~G8RV~8!DIzpA#uX0+B zXHtKPVE(#Qq>@_9kejW*=R5@qa7|1{-a~8>5rzd3_~-AbzRQ(`p<%kc!Q>RHp{|e4 z>=bO>kc~5O#H+3iU!9SYvvKvKb2bkFx_(qz&lP%RPW6rF=4zWu)Z>aAEaQj;Y>~C* zd`Ky5dZEUEtA5d*WDQDWo^GBzYRzxlwa^Miq`Dkc_xcY5)mpuSg>3PXOZ9jr@1l63yCA+^HtdWt8pJ@|jO!LFGFVy}u}e z`9~i8`sn_Hh=0)wWZv|J88rD}5%(K@m0GQ%LFkt2%%nt~pa*fxR4_oZ&z6)y*p{zV zRUn*J)hw+z%(U9$zKy`?{&d8xow>zdcD6xKtAXOU=+D5)B){w~17M;fWPpO18Wz$F zPpfrhxkK^mad29hK&^B(9#oyT-bQm*N)ngJ+l_Z0NGuDw{ zp-TM`@@k|JAodN{0HDOHmUqiSZjMZv*}sq(&f21cTnsw7^9vEr-tqJd5DV08SVD{1 zDi$GWtahLiXqnw(&tZ%5tDgmLru-2(yb4vjZ(qv5W3bNpeGw|#&y9OFCXZ9)J-kpE zU7p*%^z+d(+ha%34Ov~uopAsIdP(*$g;)#4oa*b1rnr}r77$-V?h9Y~C56Hp(qw%F zJ-9GRmRO`9g&Z|YW&CcEAca>8NAkmzX>yoQJ$j8rsV5k>5eX~uOPh3OcqOcP@HE!W znPD$aTWvp2dkyt=_;I>RMQkU?8!MSxIJ-YV*9F<(K+HWl zfgi3a;9LjJw*hu7#j*MvUvvTj?%W@Y7tDdn`!|@JbUr(@HCM^e?U%fAWYDIa&pXU9bBOn4OH)GDN@ z!C859;_}Q9pQ>Btil0}X`c44zc{qF2d0_zX_hEycusnBiKQCvX`r0HMy7gwSAF$ZS zf4Z#M1i(MwK8bchM%z_W2mBH^kcy2gXpsAiRk?@jO%5D#x#tT+1?*|L3_fb5`ZvWq zwB;P=M;{(_5>Bem&Y=Y(Z8m_}xu_*Vz#+%y9Z{{#P^mEPr}wM4p+l^Ba! z^ZK?EMLCCHGQ9UQ=|*cl&?WM3mGivfZtrv-tEkKkF~T?3@IW)kyU>5Lj(oVUsPtcx z_4F_A`2Q#Cc#iM@d1($xOUmeDf4%UwS21vCBNODsH^7<@l1M6GW+SkvvW=Msw6IpE zvu`k+_=@i1oSv56L{YwJaQt!9grhmvmP9@*uZn_1YHeMI>_XmPyjwHu}yYeQF zQ_0X$d+18Ra;isQFq1C8Dugvb=j^7A;-)T z8Kw>?m8MpJmwyhH10(K;hEnpTs$(9>q=neA*AeB=PclT})o$W0;XjvwlPGlY>qu$5 z%)3zAuD1jy#z8G)yz+!myes)LwIeKJcV+cauP-!z^ibZFRWn$Jj$HJypESxTxMs%E ze>(K3yoRkWh{Z1(r;RdLwaI*MJ@*htv`fr3Y+B?*Tk zPDkcp8W}1Y(Fcpzh&?}(5E+Ov{KJUC0zOyyw!#U|cpQBM6$~RJmDIz_zt>A?e1Af~ z|6Cl#{$l=BDx%hbDN2}Z!EU`yxISBGo=t!u;mK*g=+u*3cL+3ENWIM}%?^ecw&te5 zW_gC7GXcN&qcMoFNQF+E_xAt!FLiJ^!K!~m5C0?j|8;M>92CSQE(aatshs+g6eTnY z+j75!X?mS$FeESvi6JCto$$s|$T=AR!@b<75zp6Sfx(qnco*g)2L$0em0$*S%hbZ z`hR{Vo>@$__3*(XJr3L%zu&`(nXgo;G|8N=TXR&Gd5=~jJiw>ohjP*CYcIY4@=&rE z#Xct5tax4~5wZGoHx3C$T0J&7M{Gm8>ts5@f6=@3W}O+RDSWrtCR6kTzz-?+Jw^AQ zghRGphBr~sclWV>=aNiI7*K9ul%#XN0L_Sy$>YiW`mqe0N2Qjo%HtZJGoAims7@)$ zVV`7E#JR7X+f-JNM5O|kGMDB732L~GrrHBNKs{~ch6)pyDR{TwteT!X`9@2aHM;hy zz)X{d485vt%S>Lv)4<+}VBK;W9_yDArFAvn1fa4uq#NFBz%4(=Va{dR6{#y12G{=r zw|<4N=N`QNPIBsV%3PzXvTM0=e~VduZDwX>o`Fzcv^N#4``PH`*2NCcyi@AwT4&G9 zm|QqlDoM1640-GiR+*aX{SbyyNP-J8gwrG&2ECNMNaZ=;{(?ag;EJ`c^sO_m6WvU& z&KW{JWfJLc6TN_=I|p{1w+xMP3IYFTI>ua1UA^EfWIRHwk9uU_fq;KOET5Y30Cfb1 zk?ipC>Sui%?L`3!WtAX6cY{lOm!ucULQR)dG;3^!tTW=R%&CfK(}|8lW8zmCve^`iz7gS6@&q+I{Bt&^)2la;H9xqXTQ2Fm}r=k9Vqrd)7KLHr%9Fp6vDyI_5UvX;1dCZ4Zv>} z$ryCl=d0hZ1NyKUXwe#Ps)wBY*-M@Z=iYd)UZvQHuDZ1>wM;%h{+pgbM z)wWWm6In6A*7gjrvMBF64|94eJB^eNp6T@<>=JdtS@E8V!;aO+YJd^DfZO#Nj2wE6RN-CJ?_k8a;F8f z02oeQBD8u)&aFG<5~D*;8i7#oOmpg9UV#=Hc*jdM$QC3g*sfMlW@m?O*WxO5{6cd3 zX`ejZ3ysbJ4C^osr=4^_<}DyInJB!z@Tf3ms3<=>a}YcWQyM(IagxaqV5^+3PRm0S zETO@Ck9QOso5yG%6F3H6>UM8A{s|Z|+TQZKdP_YYw=42PI*Tz6EO+ZmT3cr0cyVA^y%#9?eYNQ2o-rbVekn1#E|tto40;x zKcvM&tt1g8<&8v4kVLh!d^QxbXF|0dDGpU)vO-C0#it~lciKZ0=teFhq38x5LHsW3 zmVFmKm-vu)H3_ccBrwtdF@;CkT(u*-lG9TC+)?U`%n}V%SHy4%WbPm557IYD&Mb8X(*P4x^A(SGZECio_ z*s4!Y947&NIu%xz8-5lJC+fEw@NF3@KZF}VwjNyT!HaQhw&u6R177I=cCNcov*|zL z4sKxdF&uJN0--#AC2sH_I?UBZ^j&k(?JP9jNu0gIORjh@^dCeLH$b;*K7N*MJdO03 zWg(1l!uXMI1#Dbp-GNQb85mVg|Kuo&%$_~6i#QO^jCanlgwna0MXz!njj2i_|HJs} z_=PkI8Q(iln)~HJ3Lw0pE`T1Vr8Mlqf1NhU=NF+#M(tAP-M(s9~Q+LW5xZ)iOJ z1(#je@5p6<(pG|a2{2uPbr}1k+3|h7!c&*6_haZcaoBWik=N?>@fi;aP7S7@xAUHE z*hn#x0M}eWpyz53`!jsehk_=6+;mtHtYVJ6*#Bs${WS;Y4k*=@q6a2jE}Ldvd@0RS zxX`!b5Q@(M9e0b9np0*xXq zOmUzs5|0}@2Q>f4|3$1sI>jOXD0tKvk4p3lRY@W&oln6`bg?^p6J>&7izET9lOlGX zab=n`!tbc^C|HpyPT>Uu^0LO)H)a$kVN8djN0gI8?-Sf1KJfI+?yp3OdW5L%Xo^b` zM-xA0ssWRA8Cb_r!LI=Mg}x9d6v2pyq`XmuCbQIADUu&UM+(y3T?u70KO-A&|4XT{ zLZAkCO1+p6VAp9;8U0(41|7~VXmgnd1BDA4Z>1L}mJ(G#e%vx-V`ztQzJc+0b<0!o zFO`x1!Z6fdkiXQ2oeVkK#3I=(r&9fodAGTn-`|gqSV3Sd4(2M&Nn#8MW1JV>rY2*e zp^1L`GEBZQfJHdqpb+Nd(mlJ4WVxXMC9@+r12TU!qw#5sgwj-wc}Q4jdCPPT{ETF?@Uj>Nt8%IAvk(o0faQv<++d z^?{2ephHKDBrzhm2lOkIhqLVJ^fhW2TD{@?xA_z1IGCgR-Mf!ATb5BBTW z<>EuEG9#_MtNM2?NFkdi`!x|invBmdf}BIi01*t0GdJHs_i+SZoI-BAG8E|ROq3vP z)j<=o%JEUO_Grn7S~%HV8Wa8z@6Wh1y7J9Q!l>En-QgU_Xmy8*^8Q#kxl~)->TA(v zef4ykvNXkEO(it9N^k|u9A#!R=ozZMO&PvT-a!#AIvk@yg9>dq<99g@HJO}R_J^FC zBn${l$A3ZpONaA}Hp2G5WVV9>0TKG2WM-Dsf=RQmWE$xFjS!((M_MX8>^?*%zX2k@Xy$a~*t`>n;%zt)IZVEq<~ z$RxOMPxD>j_Q8hmw|rme{S85It?&?zz~@bM$b^9G{?s3TV8Q=tjAaFXEeu^N=8ZyX z40~c_xY(@6`|CihpJU|>Ln1%kpy&^U(F}GKPNAjbhXuMv5@>(yYKiigyZ>OGMJ%P6 zN9rD0KLEWk!=(zRo}03Q@+Ww1$x(hyc9g7A%x$VaKU2#3UIk@}$Fg)IW%)%Wof>;q z)dV}iqeWM|E{}rB?0kv%n5nObtjBU?8ZOOJiT;=?#hpXeQ3kB91nr7!no-pXBb$a> z7i04gJV$ozM6Q2LI&Ob%<%B**Zh2eH^OS$-D*&{gUcDd7rb%0h4Ppuv|5*CM8+@|H z5~qGbwVz(ilVPn-I!lIP%bdt88T^TJug8iaNclGU|UAFJt|9q z96;UBx%57ZCC@F?B!Ie&(}=YOZsx+anhH%RudwPi=BCupCc^yN;saDfMU0y8boIs7 zpk`aQh{3}FhRt$rl*0xyw$*YLcH|(c?8af)PKtR^_J`a|oAvZ`_L{lbdYNPFr*2X%M5x^>k$K`6R_9iuS%>}$6YR!#e*x(9F^Y)fT zFJ8NQ5QCBlJJ?pKkf;nIXHUd&=BF(MGOOXAI9`0fqW_X z;!=^x<^JJaZOxT6?Q(J8R_XS*_D(i!;4!rv3WyX(?eL!^JdCE1GIXA;nG^FHq?vlj zk{WZ5s?kVJd_$`1_cg{ZiIR$V=z!DI12(eSSO-FRfl%V?SoULOtY-@HdHbTJ2|SON zSp-@bvu$}3baxB7TUSy?$P3Kk6b}utoD7@wj_IJYb6LpnoG}AYeTX|~Si6l`^agE? zPUQyM^{XM?;R!Gr(MV@dYC|j>=}a4nQ1H(1dPf-DnNK@BNBHh2obBYi34l?apkiBj zQ3xy+A}Y!pcrGQI2#}4{3KJemmHleLygC|QHAH2zN-TxjXuigz$H+A2C3G?ygw13v>_}Q)=jIGy(J;k;GZ)u$c9OXKm!Zk4L{=it zOtz-}!cADTgcd@Ua}TknHh?>i=Ah>2U!GV}D;)Qje1rwu#P2Z_|vpx0h50+0zWP@{TNcP;s0?A5KD4E$zWB(1)gq8MCVzJTr2npH)Wk9bQYzkJ0{|s zfSgN(g&S=+JF@WcLr9q_Raf|}Xg&C?AUuSv8p+*(Yw?O;hFO?VzK%Fb24G9H&7NO} zk}^N~6=L#03rmRt;CE-Jdj+sveP_3Vq$BS;uyy=h{ocMJ=^Ot%dEH;=h@gb8IW-IB*TzqHV`{AfTZAvjsWQMAAOx zrK8>Xt0X!Oi*?q+V4B^hE@UY}2NQvxD%I{*c_t6IMd3vi=ib29v~BMJnxMlYzrT@y zE!Ic%YM!YIz>0zJLuX|pr;SGF2?a2lx9c+nk@y`MiuEzQTDukma~(qgw+cq`LG8o{ zmG@7w2nz@&B6;zCAiNjq+mDAnAirig5-cQOOWYrrju?**(TNszhb!$iEKz`Z;n+LWu zM3sRu6IuFr$w7e;h6QO->}chMx_INTlVMSY5e5SOMoge~?tSG;Q&%lpRUfPI_0Zap zi`WZ*PJ%Ms-q8R3q;BeBFx79QY`MbqGQCMvEI*Oze3`^7isChyBns#+IESY?9A&sT z6y^2m)n>f92FQbl3RAk1EMViOCwMX^aul=@+Je9^I`v`2ZWlVuCYzn}(n4CvyE+on+*XzbWTn({Mq&|Lh!8xIr6BWqd4Y`+e(;ED! z8}OY%YYdEKpz)y7h4TdWYpcv~rcd%u#YpQ&4aHmW`#!ia=FXQ$k<}R8A9V=i7a-r@I|I}1Cc2k z$Hr64_0FCw9RBM@Yp*q6;_q^1fy4P z(bpznR@&%Kclg7aE87k#9EDJzM=(NYXL?PS6m%!s!P8 zt=)MxPIKMf7}{!W6SJd~s_shuy$C;q9?PW)AF(x#TrcHdIgSkro4 zahz;Q+4qLXxHZRNVdh4*uK=JD{PrYdb?~euzuzcniLv0(g_gGwGYE^SvMQq(|5*~a zM``!z@O|HDALpbIFaZACba;zWvX7U2?e%Vl;>vU2y79w%@?+mY5M-Ba+-LBhC$x5! zFcS>veT<7Aqj-Lc%i2_M#QP&@Z40Tl^UCJviNwemWb{X@_1W0?NfRtjkV@Qf z0QDZ+AlluNNsDoNPn~3VNdI7_u9L;D&6vjSB*~}X_~?M1gFOf zyGLns1g)gx_sIJxX9|0&nusXS)pfO3V_YTlcVb{ylxhIaP@laOTXBOyLN<&V z0}8fXRSSA4TB+swnqR~xi?rXWo)~KvS)?9PCHbg2E8Y(ISA5?Gg7jsK$#r$jeMn0Y zi*hLEt4TBVTVD2-7EFru>rN7p(dASs126pY#;EcVXcrBLbS{FM&(Nk|ZHJ&wKXJ57 z$(D@K%pBMVM==5Xad7u*>(NGsq&;$zuMG$V#Smi)v}DGU-YpX}))}Vm(lors^7a{& zVHRkf(o{u@;f$T2SW^m-6NbabD&K*Se8)Ub<5L~#JHuQ@V)`_IUmOoObtyuJzC1uY zH`mN`+83e`>x<(dBxj+`Zf2Z+YoYi8u_~*%k~8prXrVh``3XKSVW@?^J@^79zF=4l5r1YsRur~&`VroB>cy&XzE=IajU9avpDm28 zj?_Fcl8^d85er3&g)_fVA~K`RE_bu$?gYe=Bb7^&urdPA|y#{y*qP-Bnd!Gf@yZk>oc?|SUZ1E4fJcD>O|q7 za>m?fsDnGse3uJ6-GJS`hbSXZY5s#`Mw*4V53xznIp@qb*zj3J_g=+I`L|{AQdrWAXd}y3 zXs4q$<%((|qq6JC8WPVXH5ta?+pl4KsQVHAN)6gY$o+7}48I;a3O+6xm>PS9{0z4u z8s^ywr(LFNWFp&5?uF9bmsRuz_4(0@bP713{r52%w8v15Dkt5wKP@i(HDzT|ah~Rp z#xKnPWCRYw(Fju;{OQFsQ=QtL`3Mfo?$-ASjPO&R{ITCB`mOWi))ynZxa{?$HgoUn zrIFU1ea@i{sa&Bw8;8;@I0?Jc+&z0y>hOk>9VBK1CRdIG zzr2tP`Yw)=jVb&)7os6i>9}tF$P7SKXg2JsxuNruT+gWTYzo#rmv^2Ha$@;C-NUJA z`c@2=Hm^^`{iAn^&S`6t(}Cj-mO&i*a8)zq2N#G9Y5n#CFdwhw-*qGxZZ zNnM(8zlmYGE%88jxU7}B9R>4}Pb%bmOYjSKHY&Il~N#SFlVf}YJQ zEPU+9AOPD9{rANMT9aCS!066cpoLI24l5oWf6Sy&aJ}G;prH5R4ct54 zv;}C%13Kdhn%DLscVV*2`d8L}HwNH#CotTsmd~xeqwHd>;uu#x?lu{^uA_34rE%FR zynUIf6dY*pz}Pb`BjB_o0*+*i7sCp{#4z!^di6|YLhID}TojNXwggC0aI1~*8j1U= zu+dz3_z{LnOTRAH&r7LMCOm9*eq1SSI_Ia!k!t7D50ntNBN;s)+o2?CR{kp>@Csx1 zQ)vMxbl_TN5GTYkC1@275IK5J_VMHPfHhk%*`_tDi*I<4-lmOEZJ#7L)$B~Os(fJZ ziLf5qYiEontFR1G6a>Up8vXJ^m(XNqBQM8%yT5%yI<>5`tVdMrZ?Ma18!WMXUbM(oKC z;dZB286@@4LBTktO`7{TPx=n60%s?MqGVF3J!YkkRp5-(oFLp-Fef-GIMA1Kz-ZE+ z^2PWfK$zE)*Ad%4*4&@_g>ls{GC{UsH1VBtRsV2w*TUz5a9(c#AUM}VqcOZc{t{}Q z)l))30Q)YS{P-uKsQ!(IC{ylj@l$@CBLKqH_0*Px(ZAC%QDr+I)X|44h>=_GVQDL< z4_ZUmo>_k~$>~g*W-pu59pngseFrfKRv?X^Ros44k2M#HuFPge2y~ym1e`8@zrDZX z1+it${6rbTxf+Q4u{P`iM#ahuniH>J0GIE^&45qp9n{#r-B^*?(iTG^2_GN|*gYBPo&T~Vlmu#} z*|gG|0m(Xlf9)vPgRI#p;iaZG3%9(OdnP7<3dU73W$IDw?eD<2KgJ zgs$dS;DxRo#X3Co78@wp8O1S^s%D;SGmJHnA*{?c`?z&>9W-!U%;UfK;Q&jx83Jb3 zb3lHt80xjzvpFLl&juOp9VuGlG$B>*4XVP8auhtDuO8 zkdxIMcVp72m|D}oJ`=-EkpdQN+6j_vQy9uRIr%4Vuhim#wc9F~vFf6&qsKVtbT8G) zx$(=4bjY4EAeZb!t&n>8lVi<`|G-><8Q?Y)%$A97go3&2ZX%vZ5KUO(ivu{k5hYD8 zz1rs+;`5oLXEx5CwAg1$w>~km1qa@4`lu4rlUw7+t%=~_RqG0~uK-`%;1Ngr!x_&g z@D45*CkRQ4ie@*I(+Iil*Cz_*oXmT_874~CT5Aw@rquZ|{(`3OhTiU%FWrJ(XI|Icw^M z(FAMEe#t9+)LvXHG-_UOG=WC&Y0>+|{%_lO{hyx|`S-&Cq7>rGf7`|yyJ~nE=--Z< zIpG#)s?yZxy26{dpcEQ(ur_vj#JIS!6zJmBvlN{On~dEZ8^V8qf^W+ieP=04SVp{L zq8?=dOIhD!-@Xetc?&L*0q^L4>Q`fa2m6*Z6}RwJ85h* zww-*jZQE93+qTWdR&%;9&c)vUVLi`WbBr0WJ$0(TxqLxS^PB(X3S47h2m_CvjB zB7?Uy=zA>A7`#0RX!R2 z;o7Nr!cluI)=i!ozV4x|SQ56Da&V@1u$d0BagE$bBP#08#J&lWbU)&!rc7e3I~{2p zv>JsLOVU5L%K0_>gq*5Ae$T{uIB)?>`=$!3b6 zTBrT0a5kLQ{}wuon7oC4YIu}NA+T$WH1WB9m@J^_w9R9wH!9dFjqL{|-}QX`l~Cqh zn3l`wDa!&IM_uY*vogsvuKP^?d#mjpm=4Dc@jtCVC0q1*SB`!Yjhs9C?}@n`Bt1Fp zV*T}kFyfM_3%2|Uu2jB~*Q?mAgIp_l{N=_`YnkiB@F>4nE!Io3cK)#Tp1hpwR^E8& zT?YWh!J(*VRBJrQ#MaIz|88r^64~8Sf%j9(dW31rMA=;Cqxnz1x874+v$66THzFs? z!>mmj$Zc>4#u}6J=kL*yd?vE@kl`P%9rj6onBH0hFL0v6AGkHz0fhXAUYw?;=8zjO z^d-4w1n#wK>L)1HeTl&vRN_xr_q^N)2}U5M@`63zK0QO~5NWEMsa;7=N$n)3-j=$*Wn9dn+^T7noK(ucN@W9% z47Md5UMq809N9y}eC0a>Qbri^=ec`jhgpjp1}K*=;i2ZRh78$@XK2@j9-?26bFbfh z@asnq(O!^{o6ec_1i{t-BvJ{?!ebL+_4Fhe>?3E%7gxBrt9P`#0#IO-(?Y&j{5p?zJ- zoyysAuntO>Ym}of{o_W6edLMd73CSc8TRBgfo^1GKkPqlyF2|l6F6ky&M27V3#Ts@2vRIH*{iygOb~`f|oexMToOL4dkot;ZCLlfShXg?hY3*`P zTPqH5L{fWfRTDiz{0lCUolF#xtkXAcM2ktfHj6s;R%@uDQE#%2H2!*o^r=V~dxjJ1 z*vlm3mzr}qwm%(ZJYWoF$kB!uSiyQpxu?wIMjE1nUQT&lbxnl>89fa6JIuk?p70+P z2a>f0k(R0`6gy|9hk8(GZh+=nqjC41XK@MNgbS8@$^1~qzE!+aQSJtzD1j0Bk(-$| zIr8diKlRD6&y3?Zcm&d@o7{?N805=PMbXQz`|ck-X(-7=>iD_LI;WHRBk&Snp1-|3 z*rJ%TI6{JcYq$S+T?WWqsw-Zc81u)EL(2|Qe zE*ENq>O|eRvg$TDIrS~W6eq@WWJy@}de}C{sV=?BxxQjmts0_MjZPrh&%mFq+Db0j z*{`b?#d`s44Rzg7b12!*45f?JVHY3XgBpKIG8)Eh@9}$9YVy|DB1;jQpZ`>%?2%u` zo@dR7o}5LTW!8rFk;w@8hSLEJ#ygD5dMC(k4{A4urO9-M_Op%TXtJ zULnG0+8z1?5+54IVAqFLQOMJ0QAYYi`rYaUf=?M3=rOV;)aXQK=exsgN0BHYB&p}+ z{W(IbecGka*X=1FDGA{f(M{ERjkb^a=EqxXH_MVWM5r;8+Zxzouy3bwqYx(>0;(s* zxJ^-slyA3(pMbR%MJkp+QnW0|Cif+g#}`^&X!ib0=#DqIrx@rj#SBf|%`BpA@P5zH z8g0(csXG5dH4tJRx1cRVzR>=Rks$x(?T1hO*ZpJPMb zKvq;rmqeaa;-vxGL|5#bA5=U$i^A0>m`4xeb!P4Sbk>wj%`(~TYJTzextmh6Az11p z^E%V}*5^6L>#FS}=RViz>bL&aloKP$9L--P>Lp+fa6c6|>)}29Y%%vOpZ#(l6(e*% zb$Clo^_A#I(ZJque1c6pR9G~+y#=BW<@0c__ zx(vWc^}G8i0>8rE{m?V$93Ar1&pEpL+04$(fu&AiRyNp`3Z0YuC7o-M+uDG@mVm^Gfm67L>0tdcME^L5M z9;aNzjLZbb!1&JJd3U$HiOXnkax~9&ScvZWdV6uJvD#~8`Dt6Rt`yfg+v~x{^Os62 z0!PTCF&X>jq{=czY_Tk#sqIpsg*k@VUGtOO>g;w0E!yVx^q>%w5*yRh`sRj{s+|{A zQ)M++1AhOn*_!Ioj*hNsM4mtAaIV1b=ZELZb68hbNRi7lO~U^DBXrrn+fObRk<35Z z3UBue9b$sBZx8Jc?0+IkL=S&T@x}j0h|YFI$)Lee_5jU5^sQ?RWrBlNO2JOS3IWRNUR~Uz;ewb>#+%A(%H) z#f*>}gUf$=h7{&RH=%2%XW87=5vxQGMqNFe+LEr7UdQ0{&)o{~wW}(K53W*hPsKxj zcb%4P_K&!SJgE1n6E@F~N>M+__H-=p7-Cg!0~t6J^4_Sv-V}}@Pk`rFAW`sEbvXNh z(+Tkc7ZdOcU)DHwSx45lTiFwEy=H=(IzB_&OKONKN4y&1rk2|a>R+LS$8yQu@}F6M z=a@Nt*nwy;Ydk=!h3@6O`zq_z)RHP|gGR!OfG3?VIcCGYiLvY}3bEOW3$PX#f^V$v z;V_?w9>nDkEeJ^}JKd|BC6ua)Lmy+XE}E2_OyR4vrzcwXHJFtQlcED^Mz64=(#4re zBnG-HT5O@I4>W&2w5fYf>KjuTj^$+H?#7Pes4$85vIQ523WC{t$(+TdR!d#gX z>-!e<5Cs^`etP%!OIM=fG2glrVR4w*`Rp9I(FixK(tP5TNORc#=_E7$4h-Y=y*W+k zl9@j`^J9(L$xtRBXiR~?`VT4cVnpoEu~W2nmxA3AGe{9FXooD*^SyXgoG8In2vd zwy_A~#_d(@k~Q>d9JC<_3tCBkm?z^obvlV+87<(&>a`2mpnQR;xJgaDAsh<0%7*M@ z15=@nR?4*+%0lEmHjY@@9pMBA8-haZ0@!R1586ZB0%iGLlhM&+$)dosGFzNaE}1O- zP3_>3l$6LZnkot+XMi_+;RSYZ%-$eFSyv@MVzwElzOJ>%z1m-QoR+fGk=2dY1pRZ~ zohG-Hfs2#G78D2!gia-=W$cVA&o}p+SZY3VsW=2t^ANsucAQ1JjnRrbvPJ5|*%H%N ze1VJ>80N5iF!7Wu^g5H$R+9M{nuFud%5>W_%yByfyHjvW+^u>LdvAjS1R(xf(0}H# z{v{(^eo=nN8P3J%nz=D!d&Be5D~}~ z46>pkz{LOCYFPjB5(-TtFD{Z{yJlG|oT*Va6{vwiTo3rR;sK<~^omr5wp?OsMEhAS?(=bMc_|KrgcSOILA8 zal2i)CmrS5n){rG?08?f=u$>bE)8nzRS zR-At7_(`6UW1gH6x&I;!gFBtPfoR=zgHE7E-#}R2iNMPO<^9rraRAwDXbvg1Xq==uFW(SZ8Z|vW8mc9X6 zWX&%j|2~>q!a_GRuh~-5CidJIch{5EuLZaYx!fq2H4^_^XYBC*Vf|F^ zZ4%GMQ&K&a%6$3C_cd^A5G84?@6Gt(W`X?cPZ~B)8#o>Ovgd44&nTU%@a;sN*pdy) zo_wCs9orQ_1f_(FQv{$U_WdhA%(mpdEC$}F-JkccRQnX^tp!C1#wQD7*5)C6^X12I z?j$Y%d!TR|3i-8_@I^2`+mqTI_9T<{hlqpg zmcF+9sQnF9#W4Wy*P*vK^G@h;Amf}EYoyx3=joEhp9c^=sxLrGg`vf44HY(NG)J+| z|F?U2U_kV$f4xSVN0tuQufwaVu{g&Bm6DqFM3r%*Zb*E@1)0OknrZfV29iRO0Y;K6h1VcKwT!0*Za171EDtI+fsc@_|X>g|s zNk=>k9ZiZ0E6-{Lz%bU&j#34iXzzv_W z2D_9C?6=D=)@M#tf14cpSP_CZZ%J}Xf0&xQpY15NS`vU$89J3k;ZakLWw|a+-q1Sf zNppMF#yOe1wDEPAbLJ@w6t{^&-U#_r;o65=9~Hwp-A@0E@GGYUMy)A2`cmpuC`d$*xH`Q(~S z)I#_{A-VTwlQ$upw&Un*STJ3R3SNO8*A%K2k*2wUtpq|}{&)nn0b`9yM^+?Z1=mk+ zO0_MZYB0qslkYW?8q|d4XFKz1B7EPGyaoaeW=>7tV37Vg8P7eR5q*+wfymh&iaDd^ zN^smWa}TmP({jw(bfT=O865K){6a@r$6BUd<&vX>eueAMk(u!?Mavj8$KykMSd*Dq zfD8K~Hh(7ZG~pb<<_I*)x@IPgFAbF0CNnd; z(AwglQw8@c1&g4g+(vo)r^eALl*>f&SI|6l^EuEwmGfJSL19sOkmpcAzGQXi+8D|* z{O+Wc_>+=gvg!>I{!pu(M$`%0DGK?7GHTj zQvM5soNUybecue#S5)q-U*Q?+5f8Y)E2RhP-d<;d%}&V27sTGyiLYMIM_Ih#lyo*G8-5Tx!Q7JQc&3id{kCsLB(^v-K>GYyTAh6-=qBd9_d;JZ> zf|;n9nCRSF-K@|Igh^RhKzyTmRfs!n(k~K%ND*t3YMS8BZm`-tNGyn;8y9eXYW!$3 zMqZPmvu~L%04^w9_lELDnm!!7{bRXy6mDjEY|V)+ZM&FI`{|I19X)vuda{{RWW{;u z)z$P=YlmS3&RI9);fj05mWjaGhjL{;JR~GT$G3DRSn5}=(gp7HEHqY# zUco3+)h4Z)IGp-hwoX*X7&WlPM#D_;p-Qswh{4%|nePeLof2(nfGsRpS@+jFDH~EH zKqfw?rT2RmbS5(RG(G2ewd8ug-byd%ec$cK17+N-U+=r}Lss6T1j>t(yFEC2vw2Iw z_6Ni#xo4LoD-fL1I~t!=9V^+f9}+IJu5enLUsz{PpDb(O6&l0@dJ2@1Kt9QW@J-{v zfJ+S}3LwCUT&l7%`BDvy^JvapD zziav5dg)nrpE`uWB6jd`6s<(S(66{zrF~Ap@p)5d-_=;V0v58xzu-S^X$nr+&V?D) zrR*dloi#@4=zqp6e!9&MM81h=aa6S51#7|hzeg<};xhTy+7Tt*a=$F?L`3lPE z5H1EvfO`Cmu-Y(5j{>RS&4gCgYomh#AQ?AxwrA{VM=5(SdRmGQ^{@XdSD81*w>!Ao zE^Iu#f9$gk8367-I&tF11y18ZLNXl87dg^F33_)NFZ86ZA1}T`Sgeh4zuZK0>;FEvO*+*?-w{r=VKv zy7I4~fa>CoovB-6hvrWs{@hNE>#m*8_rJc^mup|V4?p}|UPefo`uBPiQ&|kcp#H2B)??6YgN!qdayMyd(4{)tV2>`Tya0;=&-t@O8~@_9dy#jKm0ZU&?FpfQpZ56ReK>*O==^LBb3jF>gc#o7LY<_t-5SNGmbo;#^< z0hOu}01(w}@f87R7!)t5SyWgst|&oS#Nof0i7M1+($=*nr7*CZm4);ytB1u;_bn7)KJ5|?g(C%K>6`(zmZ?%^{mh2B?bZO%s^QyQxX+2dmPhU)yY0WbPh@r!f=_dzI7$TRK=V)q~n=*Jbhb1Z;Z^k}pL; zKq3kOk(E;kC3zM~D=V%nM{Y^chcv==$Jj}_i}rEcmIc@uiubpmdqeG@Q`yOvH5cxB zz3^ivLx7ys7zPW(-H1R47}XFSP@?!&?3%r_1vtF~2k7rJLBt-Y!}?CW0fAVCK#4L7 zYv>vbfaWm4FCCE6Ye)Ve-*ydPG*7GdYk?XF8T#5@o`qrrGLmFj_(1N!tfB;7_4`@D*F!R7SYcyAU~V9b#XjE=5$ z#UzF>JWxE1bTbD z-*lGJM!zNQiL&BcMOAj91x@fRywj@hG2 zmB&N?8>X<41q^;r5qK?p|9!(x$$W6Af=xxL^h)Wn+^$-(?#icC?yce9!H7Za`z=b# z)fc%;dBskfHbX`X8gRWpcALR5nA>SUKNV^SdM292pk1e}FpZV4O zctIFCXlNo*(R!)pj?LUeLmAyYar<8S6oXODyF2uG+i*)K`xoy9Qn)ydQexLS^0|%g zLUse>W-lZw{h(j|{AGuV+ryjGUoWa_DGp3M+_jWU#{LxVL48?ZVuHrp1S0eAwOJEw z1l~EZrezdtl~J=4J!^!wguA+YE&H@~S-w8E4beMNS;c-SlHmRFq%0zdTM0)z&qCv9 z_Su$b53XnfD{{7um;S{+(3PN+@U|^rC{0 zryteC4KEJZAmTjm;Ej{IKp-W^;rZ=3l5H+9AQ#+O+|#=yKkG4R%nS*y3P3WkpyLMf zu!lw8mX<1P@MJ=;pi3`sW4wHuZ#4$R#how95rngW-hTL=B7ZQSGi*VZDHvCBM5$m1 zF_l`3O!AftmNR?)PV^c(aJ?aH^~I|8Sd-Jc+DTD0ojwa3Bfhc}46-uJ#Hr~Efy-Iw zNQqi3x`(RQzr=m9<{XKPUQ2a&5?S4{E;qH6&S03+A|~e!vw@q zZh0_Cp@#rq?^l=W#fom)@r25FtwLk>=LBI4Pd1aPoU4nkj}}^U?&^Jeb+dQ_5duG4 z*3fLz{E?tUb;wRfI(LQ^w^}2HT^CVowPAj51#S5D&+`jk{K%&g=Q%j-W9nbZ4yre;4{s(izp^_8u3ncj-&05|+T-Qp7?0}(k3(Z$P zV<^h|O_w)Z=~f{s{QifoEMb7`x>|h5R?seL&;y@}u5ZGYU)KXVk<`1?4u3yeK6l`! z)-5OGnTmnVrp)i(x$d#yUiNURMTiRFmYWe^WJh>7x?@MJ(XD6&&(q(3lBuj)_$s7r~F>yb<2`0!y$wYI-N6LbZfxQ%fR90m+Y)T>EyXtRccO$(u;y)?G zWg!cz?hVF|Gz3D!fmv8M5;~svg;%_g1ALLnL7u0T8Bbb!pO1640*7DU{@b6PJ5oCL z`WFqu{zoOC|9>h$B26h9U=6oy_W@EYOS(tP1zGHc5t_dX|k?eqS5gb{?CmmNt$KBO2txD$SYnf{b& z+~J?uOpad(FFtkPRpY+Ki2+|;E%G-JX49;f}=MDE2}}s>+49uOIu{@ zX`v!P%kfk;x|pJjS*tzL(eE|krh8Oj=+rXKCvm(d_StHq^{m}22Q%Q=+%w=%F_O#e zQu-QY=nKMJR8Er)*bs24IAp2ybozReiLTcesMW>cex`M z6@z6I7vtlgCMELB!W3I0;7oxWQ10{4JtMrC6}QVWF?L%^KX1yJlj&U2>L2i@GQrQolHhqp* z6Wce)ZKPo^(z@jLX@C~SeMJ1Pmk9~dzU9ZdoVZ&~2WY`~>!>aXP_m?RczA5hmz>Q8 zf6HLETIh2A8DWtzpTtTphq*9*m(WQD);O5XVFOB|7_X~@9Pfi%O+o{a(F9Hv)&P4I zLA4uz3%VbYH{|{0v@>a(&^f=nv!d^L?d8VxO!w8;naO*<14T$&5d2Xik9mV;5mB5@ zBNxuP0Km?I7jen!m0qY!v#{oz5&yj{kFE5mne~+S9q0GmaxRO|` z$sku2_ua8NSKZt@Lbi7CjMTdV-nVzgWxjU44aiY{Zxb?IhJG#`>;KK2Y+snWA_cS$ z%W=~mJmPR%G~taH+6S`Y7ITT5S|?P~`)<>bYO`)v+_DP*voqDqb-Jahogx{CXAda3 z<+qwRx%9Cor_S7&+|>u{(Hk!7M2jm9p}F)PXGs)A4yp3mt=b25(Q&UFxd$W#C@sbH4~!y6E2<-)^qezJl?^>>XzQ!xHscWi#=mg@adE8sVxNK{Lpu4^}x1GZ91rp#(>t=Brs9hOq2qH!~3wl!Kj=#`Zg z+K%NLDU62OEw%oLaxSY*u-5Q1JQzKxu_QEnc(WxkqFkRhpvW#{?uXZ8)C8>|*IT-h zPv#KNDlHUI)GzEH@1RExPJJ)Yw1vY}FFiR*B3QVp0gIe#4pZcxvl$rPWLtI40+u!i zq{s(&s@e9!R9Cib$rCT8(#qW{9SUddR}qL#w2@oA=t5vQY`)}5cXVbE!4B1bpLKtrBWKasWkkb>ukCNS0V7NwsdXoRD*a=bgYCz)8R zn+)Oh_G*>b&X?I8Jdd}LiWY!qG-%*M_xE(d;;*+ROLpYAHmsY7?p4#S02-AI(p!F^ zCzfuU54mGCU#dVIi|vuI;Dbt4@+CuW_^@60%L_WWv`$E`=N+A)VWF8R*hD=RS!Wri zE8R9X^K0xh$(4Y{xp5j~u!mHtMxZh|N7^*!wru}V;#_#ai594yBZw9lV09@?hIV^8 zvb0y`{cfDiFMVDw+_6s{4J@p+)x*#w9R?WwPPSGE^1{RQ;^~Kxeppj zkSDi)`5>LeDMSDvw^&2y>dm2t-83gJ*fajg3&PKtfdf8;N+&-N!;{y*&8}%0iYlAv z`cKn0yRC@PLsbx!+fak+La69{Ytk8pYO+&u-k+ z%x(qzE@TQJMJ*?w0{GmF@T_Vxu zShGX8L*T0oCfH}%&mm%1jwMMm?xNWJeXxMG!k;pqSRX^X&`!&ziICf%BVW#E zN_N=(%P?ax;B|zK!S#ZkMx@Axt;;rtj^&igb30F9&I*!GIu`rE>MdGGVKx!cCxC(N z^uRe>2&`!*ukz)d^Chi9Z_T+&NPRXLQdd0H>H{Ls4%o#-=nl7Ae!=i)TiV@taSgoQ z-B1ebMqI~)uIEAcOR@uj>_{#eXRfKO9^F5-%XpiLOzmjql!b*xM0>qgi}j(}y|G(+ zdxFp%+7sh3U>noVy1NnSE1&KIID|?bv@`7-jg45SlJl571 z)0zxF4D7oiq1W1k{1ReW4mE)(I%ys3_2>(6uKB)xYe2~?G%dUm{=8Y}rP!$7zW{)SaWc@brYM+LuuJn_wlShyIMFH=dU?=Xw z8dWP-o`xTzwZ<);bw#a$J}}q95dY)f=Nk8ewae&+<)f-^C%N>*K+sduTi6b6WZst! zJVyfEp%vB|yq!fK{q=Hdj#HXqrh!}r9{5Y(jiAzPcZ2v63i%}oBCyoOYz*5PgP33zGw zs2J{Hd3pYT3j7)c`X3ldyIEh@{x9CD-T*yD+-mP?U+2o&)bhJ{*4=qw!-R&+TjnvS+{zEIL#HRMsiBfk5~* zI~}7`ysPbIRp6YZS)F1+E7{`h9q^Vs*(YzQn#^x%<3Zjz@)nOF)LhD2{wJc4!lx*2 zG0Qp7N-d=ZC0(0DN6&XqPhPr06x*ko#3uO~X}+FbBwG|>9O-DtQag1OKodw^%bF2R zxXgb!b11V$*gWbcquad{h>x`YVVffVa_VFMX(d6Q^N@aYPHSE?z_KSw z-6064WZJ)w^a^UJ(y1w?h>l7*$N4=QQ;Xj%N5f#{JQRnxqpIuL(%+m#-JYm$erEFc zYsHK)ui`sn_J(5*{>)8&Fp!8aM}Vu}(=DHjy@j~=^W|Elp;gs4itPO3|YQrda-r3bnTmHw)5e;1RfLe0<&*@yO<-5|h!^0EhR~E?i@s82|vL{{~05FxrMq-Bec&b>9o|g|7 z<}4-$VUX2a90_e6I&btO`U z^Y5WwAG)J*7}>okw%FGzpP#yqIJ3A?J*R6RH4&Zn!V=vYwcF z;V0QP11JO|@V15yrlQCs>1n03N9Jki7v;lRQ{YHwfv);Ks;<-(JAAE5=?#17a46CN z!eeC)OAn41X^uf(l4uU28<-9oO5u~iFH)2fM5(6GubShD(#?zYNv9i$yk{zKR+O)= zxu$@+T$sM9a|;qZGEfx9v3prspxEu4D8e5V3-?fYiDQ6+Ek zM9d@-A2=%3K-AKjb7u=v&X-5b{GPVZQ-{Q{Ji~WsZ7DQ9#UbB~iS)YFRpiDX zdO%UHatl%h-SNrz40ZcG$MabHCBuPrkMxP;Z_bs6xA<0_D}T2wAMF1Te*bRq)GXKy zpKRMPIN}wOlX`Hx2}eOG$WL)5z(i81CaK%wR;jDR^iosp`D z5e{`n=1*>|x-hZj>BE6>476?-Y_q2|Lk(Yo9Wp?!*7UBj<&csb7aEnevR1z4bLv%%gGXA~-ZcCgw8 zQA2@9jVOf(vgp6m`a#@hRwB;oKoXRoC3_H-+^H$3PWV==DkMJ}mB8Mfv&*W+=G@`s zd3b<_!Dc)wPbF%w0*fT+8uqpOLe@+`DD12+hNC`QxPXKZNF(TMRWUB{qg>OsI9{lX zHu14a&dKvC<-Vk)g>R?qh$_?hP!>qsJO~*8bfcap)_ur))g)g4*W4EP9bQ46I8-c; zXk$JfN;jd*`xy(T2Cqmcn%A!Ft1 zB12n8V-#`+Wua+B1pK>=Y~_gLmYC=1o6}W+epmR$3|e=Nr{RqJme{vKgLRE_RL0+V z@j#E>3u}SR7efid{iu0%akfG8V?2@5BFFPB#_{-F<@E5&&!DC)H;-}w<$FHnj4p@d z#GVx~jQDSkSy*S<4C2QEOQt=5R0bcDZn`H?9_d;8v~`=BBTfl@_WSHOucOY@QNAYn*^DNHBd8VsGU8pPc7{+H83=K&a?n5R(xmos6g zoFmTdnkczR4a3L4?|j+mo~YXLkx%xqI;UW%&Ql4@`ujqy1$N#-)@c{U9BzE+Eukf#nUC?)*PiJwf(J%01@TLN}m{9N!`p?A%1SKVv&NdIk zDf>~|A=0}6-!}t+-{ZZ2YrP^8wlHoHe%?!d0n7Utoj-BAFLy`o^ctK+1ab{SDSbr` zM*e{Ro@++Lla%>8_31VC;e=WJK9}H)2khK)-rV)COT=9|fr9&gc!q9)p}(nuXAp-g zxdSwe{_By@8a;kqe^FXJu?>776hD7Am?Q4CM<4soKPOKl2P`834q6;j;6su2$0Y0E z?E>Glgq^v|zTlhNP^|PpTo_Mr+&z{2KX2(E3Dl>faImKD;2@rif`;`?`?dvrzmTRM z&8(wxJ)_ku9umYaSc8zcMH_!m2;LkskZ3kR$TUa81^k&n8VV09J&^OZbc}DyUB4=P z@;x`Nplf(5zt6D-AeWaC)cfwQlOB|_=`FeuMn7qfiahQ%Qd##Th%3Px)}@c6;O1Pa zYdr(T`Do45h*z=|^X=8yoQVB61og%;IevDZ@u*U0! zHg@^%pUGkEF|ra~%bZ*O-36wpm(kmdbd%7bDl~Co{4L~b)+lP+O)i-X1pJC(*$RVprFj3^ys{3g5 zpJ<`(#JQahL^)v!-dLxAX&j1uwy{+&hu{-Pv9MNf1)(cs)3Ro|W zvs2HkRZ0^;)Snj|7RkA**MoAXR~hvRKa^01?^-V)X5`&*r zN<>(F)cvW-lOmXx1-;|BD?^?n z#+Hw0h4=-!FfXN-CBMmz%^=knvAO`oVnaZO=6w+vJt8=-5ghD091i>ym2Tjgl7#F-V`!H}0^6wx zgFa{tkI;bTF4Ew!_fwno6aJQI^yk@BzB4#*SDrEH(}HU6t*Pl9Lzk!A+m4HW%{L-h zilpdx>98I9tIjVgF$@K zN#OW1nrh^bD2TG3Q8%gYstK_We*Az$b0+cZ7wj28;%1#`8){$geLPsTqFO3`-MfVNZOMVoK8(fk}W*P-c zBg=j6=jGMo%#MD~w>;1Z?xNoLT|?001Oq{_KnWOk**)HL2xf&*Uh>AWz68h_EG(!P zLU;K>R8E`JK0xs@3^-1)f?9rBhFoUZdStuWfNxMzi0qK7jA3h`e(pNyBMuaHtMDDA zy@z|8W&*pcbV89UpgNCcv=>*M-B4<&~!k%d}nZdn-;flQwz% zW1(-0!=QUbyqv{K!>#q#dh^I?{I%j(_{_4_(%D)4E{ckWeWpOSe|_x%pzL zx@#rV4yc4QHc0DB6K>yo`)2nWt7w|}A^8>3*l^X4Hyt#cSQ0m`kXrfcRh4LDh}4=r z=FcYx#Z7HO|Cc)6n>mTNPY}ji)eYC)eLtpfE~xm41W!Pv?j*|t$5d|br1jUo>I>@+ zw5A{OK@N9bRD@#MLEoA@!VHTJ;^0jqe}o7K<^lFdI-$6y*y1gN6d0Zr2x$U>U#|Rg z4B(ji{!X_xSeX0hf36B`o!-zM;L!Lc<@1i^IrFhx!eP+nx@Lz_R~^vFC<0|^gs%Ge z&?RLdsSAhyd=o|#!BwCUV#PKVhjG+LC>SGhDl2~g8H0_ZCLhg%XRZaOE*F9{i4$9- zdsGA&gNbWEAtMgtRS!tBj0=Kqh{*U&K;-d_xf)z*oJf^?6pT&sC*+#oR3-rt#5ZPC zOVj_gqa;4c5YhkjzvH2SfKdIX|2^RbD$#fW33vujPq4po=wA;HG?*c+;gN^^;;iAp zp=pa&)ApA|ep`nTS98gjy$dc=m!j^XWz5Yx7tz{e#9cYhrl(<8<8b7ot~+0My_+2_ zJb7&M6eV&}eF|NB<~+auIpOQNyT;Uqtb_PUxDAVv5OJ3kLf@u2uz?NWEEVkEcs+E$ z2Ckv^vYEGwcj33I^Dq>s(n6h>w+ju3r9=A>MwV<$9;7 zD}>&_&zyL;vj@fAd?-->QR;+;F@@1qpv-`$d;GALTJiuTP*3egpeBU+%_EXt(rjH1 z4;Sa`78C30)(!_V>nuwG)~SLs0{nLw=x4kYdCN;|dYQ0+9x0ACU; zC%IWV*H!}pAERM;p=TdE^JVxxS9wp~piA#)++R36`2p(_K8MAk$vQ{hFX*t48OJ`fLxBf(AZ2x9Rs{ zxE}q7hUE}7q)^z$@W85ZQLZVWQJ7up3S8QrMi*U1(AoPTJ-@c5)tKbmh zs3i&|>=+mXifkF0WrtIj4Kvu!N{>9*nq?ZTw@@5l&6hbfwNFR`lYZby!pOCtQW=hw zA^xQw?^j2MjT>;C%_7S@i3i^QVX1AZBDbqHAq9L?TZ~HISjE@&oUY~L=ik!QMmJA& zc&?$(!WdOX=LzW)^GnOAVkDt+j3u$vscWg~*DA@xFnE5q78Q`NH$cNo zeRa5w!rIkKhpFB0Y_Pj^)GuDC!0%`NUsqQi4rTX-^V+vDVaE0*W*TWi6Jabxk;qa+ ziI6QMvX+!4Ava#W*!veJZ|DFrqm=YzLK^wAE`r^z!=>U~OV3Vv_FfD>7J8*YHm%~! z{i2$(ys;3Q^6zJ3svhgcPcu)kzU!`Qa=1Y|cNDv)#f3atToQJP{ONW=!LxkU$Mcld ztLW?k?N7SYmd#;_m4=1Os%ApHx^Ba8;NHH+fy$_A^FXcpJylG%!WgOJf=U^g?f>xJ zXqy#?(DU%4a$^l-_A&!L?_MkfS(|DMT}8TY-Hu{hU4LxZJBW~e)tV{BJt}ZZU8(2q zut_g)!eT95b;k+g?hh01YAv;vLQUutuWJj;O*@3h|bZ*~>T+4tI=&sxe|5=m9Q4zZ8i6EnieuRfWb5(|$n zPd$}$I}g)N;`a$d+11?-_^bj23!vKak6}MnT$rSGxE_h+NiGf+Jc(|vlvajPC`Qn^o zxxQ26T3fy=U-IksLSv<7*>^);AEfAbolc9zY1mK0T6(d*Jno6X54&_6H@@z2F?7!j zsN-u84LoJkqvCdGOZtzs`Y~SU&~@#RySMq{e7o9L7_aPitz^iJi+S?&DBtRd4-#WU z@Xs_@S-45bGyH4l*U^jp`ZEk+$(85;*9(j0fda8H=G2LLlET3$Q?pXCQ86Xj{CYmi zfXBwN7FZKH=?60lLYis%$;h3ERO0QgIL0{JSaA29&Pio2wLE`5zmNxML0){*o%1%P zbvX5$=<4;$f*lqgB~py*gFXuls_9?QPIoS~6nInOeXVImyF<;8ihmhVdb^2xPz1*_ zFn3Gl#4{8D+qW%IHFhlE%RP#{e-7heb1RF0`MQ6P&=qyx%94v&hePEvgec?H>bXid z#|J^Ep4cYtFAMdKUiYHT>uoWd7F`D44mX+wBX+zp@-Y z(uK!`I8GcR)5xTx3Z4SfGe)*;iU>uIX>i;^W`2$PLctdPDpXZ_YgY^<+xCOq;f4l% zd4Wgrmq}c8Pnk1)VjsUZw+!8EsT~{{A`g5e8u9V!EZ$97=zR?N&GR)UZI?+|jnv3YA|K-``Z|OL|#yprTm(2Gyx`%v(yb(pbhK zru@vIzZ3&RHAN#Qx_kv5TG8}VyX~{Z!ySl(Kn>SOlB9+8>99CNnN)?GI1+XvePV6C z!RWlZx%KsH`D&_VYELq8Jd5u5J_|3dG!LO-m)-XD8AnwEb5z4Mb`pGAt1^x8kG03O z9t^B`_aphC^T73n?ehLa)|+7#Zb0?o%D@T)w)Vm0KD{zrLi>YiGD?tplqwb^^?5^R zVQ^cR0OXiN=z=hi7TJuLFi2sdpeA8(lc@(S34_Zb8UWQ#grZQ0DFe2NZ9rT!i0zk! zwn=~iWf;)=cS6mQY*T(f2O?tGW*=4r$j+g`R~RjV6cDkW!pHy^3F1NffE2tc{%(%w zm(Y>*=>0|@ZDFM2IyNYEkQZzoB*3dO*7?XAjS|Aeqrm}OQTPSK!EEhdBwMI3qF%)T z`iN(P<_0(OvUNm(!Vm^BMgFiTn*z!Z8s^Y=qOh!OD>@{%cx%@^TZDAx?4|M410{SqTm#yXk zaz`+b=5}`aRS}nw5iBoT5F>pQ18p_@)vqMSmLEVitr{UQQs>C103t_s%W)9UbHqcy zz^Dz(!8^|pFEd3p00#ocNRWUdU^yy-mN6oPaYsxXkQvwF(gFL&y&zFP&x%v8 z2tZGupne~qFrm+d22K+yavbDi921x!@l`4^Z79|cbezQi6w3rkKKaX(1QZqt`Vs=} zvov82nkJ4U-Ju9x9${_LgxOpx$k8~DoS$tRAir=BIB5d^p>tTXMv((>^gNPf9hjRW zL5-KeK)MDvjhubYDOspG4Ma}4K=d2zWm$0{aynBxpr|aiYcstb{1^|PEdhwm5+T3ZU#=){oFze(jcj+Sc^#n7qTxTE3w{>*{h6KdY89A1M}#@vzJ3Fc VwlMN}`%er%aGR6olj~j${vQ;P=LY}) diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index ae04661ee73..f398c33c4b0 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.6-bin.zip +networkTimeout=10000 zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew index a69d9cb6c20..65dcd68d65c 100755 --- a/gradlew +++ b/gradlew @@ -55,7 +55,7 @@ # Darwin, MinGW, and NonStop. # # (3) This script is generated from the Groovy template -# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # within the Gradle project. # # You can find Gradle at https://github.com/gradle/gradle/. @@ -80,10 +80,10 @@ do esac done -APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit - -APP_NAME="Gradle" +# This is normally unused +# shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' @@ -143,12 +143,16 @@ fi if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then case $MAX_FD in #( max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC3045 MAX_FD=$( ulimit -H -n ) || warn "Could not query maximum file descriptor limit" esac case $MAX_FD in #( '' | soft) :;; #( *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC3045 ulimit -n "$MAX_FD" || warn "Could not set maximum file descriptor limit to $MAX_FD" esac diff --git a/gradlew.bat b/gradlew.bat index f127cfd49d4..93e3f59f135 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -26,6 +26,7 @@ if "%OS%"=="Windows_NT" setlocal set DIRNAME=%~dp0 if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% From e63fc1d317840cbfdb7e777fa9b5ecf61ba0f683 Mon Sep 17 00:00:00 2001 From: Denis Stepanov Date: Mon, 19 Dec 2022 16:53:20 +0100 Subject: [PATCH 310/743] Fix intercepted adapter methods (#8496) --- .../DeclaredBeanElementCreator.java | 4 + .../intercepted/InterceptedAdapterSpec.groovy | 28 ++++++ .../aop/adapter/intercepted/MyBean.java | 25 ++++++ .../aop/adapter/intercepted/TheEvent.java | 4 + .../intercepted/TransactionalEventAdvice.java | 18 ++++ .../TransactionalEventInterceptor.java | 15 ++++ .../TransactionalEventListener.java | 20 +++++ .../aop/compile/AroundCompileSpec.groovy | 86 +++++++++++++++++++ 8 files changed, 200 insertions(+) create mode 100644 inject-java/src/test/groovy/io/micronaut/aop/adapter/intercepted/InterceptedAdapterSpec.groovy create mode 100644 inject-java/src/test/groovy/io/micronaut/aop/adapter/intercepted/MyBean.java create mode 100644 inject-java/src/test/groovy/io/micronaut/aop/adapter/intercepted/TheEvent.java create mode 100644 inject-java/src/test/groovy/io/micronaut/aop/adapter/intercepted/TransactionalEventAdvice.java create mode 100644 inject-java/src/test/groovy/io/micronaut/aop/adapter/intercepted/TransactionalEventInterceptor.java create mode 100644 inject-java/src/test/groovy/io/micronaut/aop/adapter/intercepted/TransactionalEventListener.java diff --git a/core-processor/src/main/java/io/micronaut/inject/processing/DeclaredBeanElementCreator.java b/core-processor/src/main/java/io/micronaut/inject/processing/DeclaredBeanElementCreator.java index d6fed0553ba..4eb94ba5676 100644 --- a/core-processor/src/main/java/io/micronaut/inject/processing/DeclaredBeanElementCreator.java +++ b/core-processor/src/main/java/io/micronaut/inject/processing/DeclaredBeanElementCreator.java @@ -600,6 +600,10 @@ private void visitAdaptedMethod(BeanDefinitionVisitor visitor, MethodElement sou MethodElement targetMethod = methods.iterator().next(); + aopProxyWriter.visitInterceptorBinding( + InterceptedMethodUtil.resolveInterceptorBinding(methodAnnotationMetadata, InterceptorKind.AROUND) + ); + ParameterElement[] sourceParams = sourceMethod.getParameters(); ParameterElement[] targetParams = targetMethod.getParameters(); diff --git a/inject-java/src/test/groovy/io/micronaut/aop/adapter/intercepted/InterceptedAdapterSpec.groovy b/inject-java/src/test/groovy/io/micronaut/aop/adapter/intercepted/InterceptedAdapterSpec.groovy new file mode 100644 index 00000000000..4fe53839a79 --- /dev/null +++ b/inject-java/src/test/groovy/io/micronaut/aop/adapter/intercepted/InterceptedAdapterSpec.groovy @@ -0,0 +1,28 @@ +package io.micronaut.aop.adapter.intercepted + +import io.micronaut.annotation.processing.test.AbstractTypeElementSpec +import io.micronaut.context.ApplicationContext + +class InterceptedAdapterSpec extends AbstractTypeElementSpec { + + void 'test interceptor on an event'() { + given: + ApplicationContext ctx = ApplicationContext.run() + + when: + def service = ctx.getBean(MyBean) + def interceptor = ctx.getBean(TransactionalEventInterceptor) + + then: + interceptor.count == 0 + + when: + service.triggerEvent() + + then: + interceptor.count == 1 + + cleanup: + ctx.close() + } +} diff --git a/inject-java/src/test/groovy/io/micronaut/aop/adapter/intercepted/MyBean.java b/inject-java/src/test/groovy/io/micronaut/aop/adapter/intercepted/MyBean.java new file mode 100644 index 00000000000..929e7fe570e --- /dev/null +++ b/inject-java/src/test/groovy/io/micronaut/aop/adapter/intercepted/MyBean.java @@ -0,0 +1,25 @@ +package io.micronaut.aop.adapter.intercepted; + +import io.micronaut.context.event.ApplicationEventPublisher; +import jakarta.inject.Singleton; + +@Singleton +class MyBean { + + private final ApplicationEventPublisher applicationEventPublisher; + long count = 0; + + MyBean(ApplicationEventPublisher applicationEventPublisher) { + this.applicationEventPublisher = applicationEventPublisher; + } + + void triggerEvent() { + applicationEventPublisher.publishEvent(new TheEvent()); + } + + @TransactionalEventListener + void test(TheEvent theEvent) { + count++; + } + +} diff --git a/inject-java/src/test/groovy/io/micronaut/aop/adapter/intercepted/TheEvent.java b/inject-java/src/test/groovy/io/micronaut/aop/adapter/intercepted/TheEvent.java new file mode 100644 index 00000000000..fef4d0fea0e --- /dev/null +++ b/inject-java/src/test/groovy/io/micronaut/aop/adapter/intercepted/TheEvent.java @@ -0,0 +1,4 @@ +package io.micronaut.aop.adapter.intercepted; + +public class TheEvent { +} diff --git a/inject-java/src/test/groovy/io/micronaut/aop/adapter/intercepted/TransactionalEventAdvice.java b/inject-java/src/test/groovy/io/micronaut/aop/adapter/intercepted/TransactionalEventAdvice.java new file mode 100644 index 00000000000..5e0dfe59a18 --- /dev/null +++ b/inject-java/src/test/groovy/io/micronaut/aop/adapter/intercepted/TransactionalEventAdvice.java @@ -0,0 +1,18 @@ +package io.micronaut.aop.adapter.intercepted; + +import io.micronaut.aop.Around; +import io.micronaut.context.annotation.Type; +import io.micronaut.core.annotation.Internal; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.ANNOTATION_TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Around +@Type(TransactionalEventInterceptor.class) +@Internal +@interface TransactionalEventAdvice { +} diff --git a/inject-java/src/test/groovy/io/micronaut/aop/adapter/intercepted/TransactionalEventInterceptor.java b/inject-java/src/test/groovy/io/micronaut/aop/adapter/intercepted/TransactionalEventInterceptor.java new file mode 100644 index 00000000000..ff40f387891 --- /dev/null +++ b/inject-java/src/test/groovy/io/micronaut/aop/adapter/intercepted/TransactionalEventInterceptor.java @@ -0,0 +1,15 @@ +package io.micronaut.aop.adapter.intercepted; + +import io.micronaut.aop.Interceptor; +import io.micronaut.aop.InvocationContext; +import jakarta.inject.Singleton; + +@Singleton +class TransactionalEventInterceptor implements Interceptor { + long count = 0; + @Override + public Object intercept(InvocationContext context) { + count++; + return context.proceed(); + } +} diff --git a/inject-java/src/test/groovy/io/micronaut/aop/adapter/intercepted/TransactionalEventListener.java b/inject-java/src/test/groovy/io/micronaut/aop/adapter/intercepted/TransactionalEventListener.java new file mode 100644 index 00000000000..ffe5799e7d1 --- /dev/null +++ b/inject-java/src/test/groovy/io/micronaut/aop/adapter/intercepted/TransactionalEventListener.java @@ -0,0 +1,20 @@ +package io.micronaut.aop.adapter.intercepted; + +import io.micronaut.aop.Adapter; +import io.micronaut.context.event.ApplicationEventListener; +import io.micronaut.core.annotation.Indexed; + +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; + +@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Adapter(ApplicationEventListener.class) +@Indexed(ApplicationEventListener.class) +@TransactionalEventAdvice +@interface TransactionalEventListener { +} diff --git a/inject-java/src/test/groovy/io/micronaut/aop/compile/AroundCompileSpec.groovy b/inject-java/src/test/groovy/io/micronaut/aop/compile/AroundCompileSpec.groovy index 38037b5cb79..1793fce0d8c 100644 --- a/inject-java/src/test/groovy/io/micronaut/aop/compile/AroundCompileSpec.groovy +++ b/inject-java/src/test/groovy/io/micronaut/aop/compile/AroundCompileSpec.groovy @@ -988,6 +988,92 @@ class TestInterceptor implements Interceptor { context.close() } + void 'test interceptor on an event'() { + given: + ApplicationContext context = buildContext(''' +package test; + +import java.lang.annotation.*; +import io.micronaut.aop.*; +import io.micronaut.context.annotation.Type; +import io.micronaut.context.event.ApplicationEventListener; +import io.micronaut.context.event.ApplicationEventPublisher; +import io.micronaut.core.annotation.Indexed; +import io.micronaut.core.annotation.Internal; +import jakarta.inject.*; +import static java.lang.annotation.RetentionPolicy.RUNTIME; +import io.micronaut.aop.simple.*; +import jakarta.inject.Singleton; + +class TheEvent { +} + +@Singleton +class MyBean { + + private final ApplicationEventPublisher applicationEventPublisher; + long count = 0; + + MyBean(ApplicationEventPublisher applicationEventPublisher) { + this.applicationEventPublisher = applicationEventPublisher; + } + + void triggerEvent() { + applicationEventPublisher.publishEvent(new TheEvent()); + } + + @TransactionalEventListener + void test(TheEvent theEvent) { + count++; + } + +} + +@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Adapter(ApplicationEventListener.class) +@Indexed(ApplicationEventListener.class) +@TransactionalEventAdvice +@interface TransactionalEventListener { +} + +@Target(ElementType.ANNOTATION_TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Around +@Type(TransactionalEventInterceptor.class) +@Internal +@interface TransactionalEventAdvice { +} + +@Singleton +class TransactionalEventInterceptor implements Interceptor { + long count = 0; + @Override + public Object intercept(InvocationContext context) { + count++; + return context.proceed(); + } + +} +''') + when: + def service = getBean(context, 'test.MyBean') + def interceptor = getBean(context, 'test.TransactionalEventInterceptor') + + then: + interceptor.count == 0 + + when: + service.triggerEvent() + + then: + interceptor.count == 1 + + cleanup: + context.close() + } + void "test validated on class with generics"() { when: BeanDefinition beanDefinition = buildBeanDefinition('test.$BaseEntityService' + BeanDefinitionWriter.CLASS_SUFFIX + BeanDefinitionWriter.PROXY_SUFFIX, """ From cbf3ebb5c7c04b0feb41842f6197535ad103a53f Mon Sep 17 00:00:00 2001 From: micronaut-build <65172877+micronaut-build@users.noreply.github.com> Date: Mon, 19 Dec 2022 10:54:23 -0500 Subject: [PATCH 311/743] Bump micronaut-openapi to 4.8.1 (#8495) Co-authored-by: Sergio del Amo --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index de2a09d00fd..07ac170c7b0 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -99,7 +99,7 @@ managed-micronaut-neo4j = "5.2.0" managed-micronaut-nats = "3.1.0" managed-micronaut-netflix = "2.1.0" managed-micronaut-object-storage = "1.1.0" -managed-micronaut-openapi = "4.5.2" +managed-micronaut-openapi = "4.8.1" managed-micronaut-oraclecloud = "2.3.0" managed-micronaut-picocli = "4.3.0" managed-micronaut-problem = "2.5.1" From 5714f04545ed58dce96324042908a7abd88fdf7a Mon Sep 17 00:00:00 2001 From: Denis Stepanov Date: Mon, 19 Dec 2022 19:03:18 +0100 Subject: [PATCH 312/743] Refactor and cleanup bean context (#8479) --- .../aop/internal/InterceptorRegistryBean.java | 6 +- .../inject/writer/BeanDefinitionWriter.java | 234 +- .../core/annotation/AnnotationClassValue.java | 2 +- .../core/beans/AbstractBeanIntrospection.java | 5 +- .../core/beans/AbstractBeanProperty.java | 3 +- .../core/beans/BeanIntrospectionMap.java | 4 +- .../core/beans/DefaultBeanWrapper.java | 4 +- .../DefaultArgumentConversionContext.java | 3 +- .../DefaultMutableConversionService.java | 3 +- .../micronaut/core/type/DefaultArgument.java | 5 +- .../io/micronaut/core/util/ObjectUtils.java | 63 + .../core/util/ObjectUtilsSpec.groovy | 24 + .../discovery/DefaultServiceInstance.java | 4 +- .../http/client/ProxyRequestOptions.java | 2 +- .../http/client/netty/DefaultHttpClient.java | 3 +- .../http/hateoas/GenericResource.java | 4 +- .../http/simple/cookies/SimpleCookie.java | 6 +- .../micronaut/http/uri/UriMatchTemplate.java | 6 +- .../micronaut/http/uri/UriMatchVariable.java | 2 +- .../io/micronaut/http/uri/UriTemplate.java | 5 +- .../AbstractClassIntroductionSpec.groovy | 12 +- .../IntroductionGenericTypesSpec.groovy | 11 +- .../compile/IntroductionWithAroundSpec.groovy | 6 +- .../aop/compile/LifeCycleWithProxySpec.groovy | 8 +- .../aop/compile/PropertyAdviceSpec.groovy | 6 +- ...roductionAdviceWithNewInterfaceSpec.groovy | 13 +- .../IntroductionInnerInterfaceSpec.groovy | 6 +- .../ConfigPropertiesParseSpec.groovy | 10 +- .../ConfigurationPropertiesBuilderSpec.groovy | 42 +- ...mmutableConfigurationPropertiesSpec.groovy | 20 +- ...nterfaceConfigurationPropertiesSpec.groovy | 14 +- .../VisibilityIssuesSpec.groovy | 4 +- .../inject/beanimport/BeanImportSpec.groovy | 8 - .../ExecutableElementParamInfo.java | 117 - .../visitor/AbstractJavaElement.java | 11 +- .../AbstractClassIntroductionSpec.groovy | 53 +- .../AnnotatedConstructorArgumentSpec.groovy | 14 +- .../InheritedAnnotationMetadataSpec.groovy | 5 +- .../compile/IntroductionAnnotationSpec.groovy | 7 +- .../IntroductionGenericTypesSpec.groovy | 5 +- .../compile/IntroductionWithAroundSpec.groovy | 6 +- .../aop/compile/LifeCycleWithProxySpec.groovy | 30 +- ...roductionAdviceWithNewInterfaceSpec.groovy | 13 +- .../IntroductionInnerInterfaceSpec.groovy | 6 +- .../inject/beans/BeanDefinitionSpec.groovy | 28 +- .../ConfigPropertiesParseSpec.groovy | 24 +- .../ConfigurationPropertiesBuilderSpec.groovy | 139 +- ...mmutableConfigurationPropertiesSpec.groovy | 12 +- ...nterfaceConfigurationPropertiesSpec.groovy | 33 +- .../inject/configproperties/RecConf.java | 5 +- .../VisibilityIssuesSpec.groovy | 26 +- .../BuilderStyleInjectionSpec.groovy | 28 +- .../context/AbstractBeanDefinition.java | 2349 ----------------- ...bstractBeanDefinitionBeanConstructor.java} | 31 +- .../AbstractBeanDefinitionReference.java | 148 -- .../AbstractBeanResolutionContext.java | 142 +- .../micronaut/context/AbstractExecutable.java | 4 +- .../context/AbstractExecutableMethod.java | 3 +- .../AbstractExecutableMethodsDefinition.java | 3 +- .../AbstractInitializableBeanDefinition.java | 538 ++-- .../context/AbstractMessageSource.java | 6 +- .../AbstractParametrizedBeanDefinition.java | 137 - .../context/BeanDefinitionDelegate.java | 50 +- .../micronaut/context/BeanRegistration.java | 3 +- .../context/BeanResolutionContext.java | 36 +- .../micronaut/context/DefaultBeanContext.java | 262 +- .../DefaultConstructorInjectionPoint.java | 26 +- ...DefaultFieldConstructorInjectionPoint.java | 4 - .../context/DefaultFieldInjectionPoint.java | 18 +- ...efaultMethodConstructorInjectionPoint.java | 4 - .../context/DefaultMethodInjectionPoint.java | 29 +- .../context/DefaultRuntimeBeanDefinition.java | 4 +- .../context/MissingMethodInjectionPoint.java | 100 - .../ReflectionConstructorInjectionPoint.java | 145 - .../ReflectionFieldInjectionPoint.java | 62 - ...ectionMethodConstructorInjectionPoint.java | 60 - .../ReflectionMethodInjectionPoint.java | 58 - .../context/RuntimeBeanDefinition.java | 4 +- .../io/micronaut/context/SingletonScope.java | 5 +- .../ApplicationEventPublisherFactory.java | 39 +- .../context/exceptions/MessageUtils.java | 2 +- .../io/micronaut/inject/BeanDefinition.java | 33 - .../java/io/micronaut/inject/BeanFactory.java | 3 + .../inject/ConstructorInjectionPoint.java | 16 +- .../inject/DefaultBeanIdentifier.java | 3 +- .../inject/DelegatingBeanDefinition.java | 10 - .../micronaut/inject/FieldInjectionPoint.java | 11 +- .../inject/InjectableBeanDefinition.java | 59 + .../io/micronaut/inject/InjectionPoint.java | 4 - .../inject/InstantiatableBeanDefinition.java | 59 + .../inject/MethodInjectionPoint.java | 25 +- .../inject/ParametrizedBeanFactory.java | 3 + ...ametrizedInstantiatableBeanDefinition.java | 64 + ...bstractInitializableBeanIntrospection.java | 2 +- .../provider/AbstractProviderDefinition.java | 21 +- .../AnnotationMetadataQualifier.java | 4 +- .../AnnotationStereotypeQualifier.java | 3 +- .../ExactTypeArgumentNameQualifier.java | 2 +- .../InterceptorBindingQualifier.java | 6 +- .../NamedAnnotationStereotypeQualifier.java | 2 +- .../RepeatableAnnotationQualifier.java | 14 +- .../context/BeanDefinitionDelegateSpec.groovy | 22 +- .../DefaultFieldInjectionPointSpec.groovy | 35 - .../jackson/codec/JacksonFeatures.java | 3 +- .../web/router/DefaultRouteBuilder.java | 7 +- 105 files changed, 1277 insertions(+), 4516 deletions(-) create mode 100644 core/src/main/java/io/micronaut/core/util/ObjectUtils.java create mode 100644 core/src/test/groovy/io/micronaut/core/util/ObjectUtilsSpec.groovy delete mode 100644 inject-java/src/main/java/io/micronaut/annotation/processing/ExecutableElementParamInfo.java delete mode 100644 inject/src/main/java/io/micronaut/context/AbstractBeanDefinition.java rename inject/src/main/java/io/micronaut/context/{AbstractConstructorInjectionPoint.java => AbstractBeanDefinitionBeanConstructor.java} (51%) delete mode 100644 inject/src/main/java/io/micronaut/context/AbstractBeanDefinitionReference.java delete mode 100644 inject/src/main/java/io/micronaut/context/AbstractParametrizedBeanDefinition.java delete mode 100644 inject/src/main/java/io/micronaut/context/MissingMethodInjectionPoint.java delete mode 100644 inject/src/main/java/io/micronaut/context/ReflectionConstructorInjectionPoint.java delete mode 100644 inject/src/main/java/io/micronaut/context/ReflectionFieldInjectionPoint.java delete mode 100644 inject/src/main/java/io/micronaut/context/ReflectionMethodConstructorInjectionPoint.java delete mode 100644 inject/src/main/java/io/micronaut/context/ReflectionMethodInjectionPoint.java create mode 100644 inject/src/main/java/io/micronaut/inject/InjectableBeanDefinition.java create mode 100644 inject/src/main/java/io/micronaut/inject/InstantiatableBeanDefinition.java create mode 100644 inject/src/main/java/io/micronaut/inject/ParametrizedInstantiatableBeanDefinition.java delete mode 100644 inject/src/test/groovy/io/micronaut/context/DefaultFieldInjectionPointSpec.groovy diff --git a/aop/src/main/java/io/micronaut/aop/internal/InterceptorRegistryBean.java b/aop/src/main/java/io/micronaut/aop/internal/InterceptorRegistryBean.java index 11ccf61529e..dc1630073f2 100644 --- a/aop/src/main/java/io/micronaut/aop/internal/InterceptorRegistryBean.java +++ b/aop/src/main/java/io/micronaut/aop/internal/InterceptorRegistryBean.java @@ -25,7 +25,7 @@ import io.micronaut.core.annotation.Internal; import io.micronaut.inject.BeanDefinition; import io.micronaut.inject.BeanDefinitionReference; -import io.micronaut.inject.BeanFactory; +import io.micronaut.inject.InstantiatableBeanDefinition; import io.micronaut.inject.annotation.MutableAnnotationMetadata; import java.util.Collections; @@ -37,7 +37,7 @@ * @since 3.0.0 */ @Internal -public final class InterceptorRegistryBean implements BeanDefinition, BeanFactory, BeanDefinitionReference { +public final class InterceptorRegistryBean implements InstantiatableBeanDefinition, BeanDefinitionReference { public static final AnnotationMetadata ANNOTATION_METADATA; static { @@ -87,7 +87,7 @@ public boolean isAbstract() { } @Override - public InterceptorRegistry build(BeanResolutionContext resolutionContext, BeanContext context, BeanDefinition definition) throws BeanInstantiationException { + public InterceptorRegistry instantiate(BeanResolutionContext resolutionContext, BeanContext context) throws BeanInstantiationException { return new DefaultInterceptorRegistry(context); } diff --git a/core-processor/src/main/java/io/micronaut/inject/writer/BeanDefinitionWriter.java b/core-processor/src/main/java/io/micronaut/inject/writer/BeanDefinitionWriter.java index d49394bfda6..45f58e961ca 100644 --- a/core-processor/src/main/java/io/micronaut/inject/writer/BeanDefinitionWriter.java +++ b/core-processor/src/main/java/io/micronaut/inject/writer/BeanDefinitionWriter.java @@ -15,7 +15,7 @@ */ package io.micronaut.inject.writer; -import io.micronaut.context.AbstractConstructorInjectionPoint; +import io.micronaut.context.AbstractBeanDefinitionBeanConstructor; import io.micronaut.context.AbstractExecutableMethod; import io.micronaut.context.AbstractInitializableBeanDefinition; import io.micronaut.context.BeanContext; @@ -37,7 +37,6 @@ import io.micronaut.context.annotation.Primary; import io.micronaut.context.annotation.Property; import io.micronaut.context.annotation.PropertySource; -import io.micronaut.context.annotation.Provided; import io.micronaut.context.annotation.Requires; import io.micronaut.context.annotation.Value; import io.micronaut.context.env.ConfigurationPath; @@ -64,13 +63,12 @@ import io.micronaut.core.util.Toggleable; import io.micronaut.inject.AdvisedBeanType; import io.micronaut.inject.BeanDefinition; -import io.micronaut.inject.BeanFactory; -import io.micronaut.inject.ConstructorInjectionPoint; import io.micronaut.inject.DisposableBeanDefinition; import io.micronaut.inject.ExecutableMethod; import io.micronaut.inject.ExecutableMethodsDefinition; import io.micronaut.inject.InitializingBeanDefinition; -import io.micronaut.inject.ParametrizedBeanFactory; +import io.micronaut.inject.InjectableBeanDefinition; +import io.micronaut.inject.ParametrizedInstantiatableBeanDefinition; import io.micronaut.inject.ProxyBeanDefinition; import io.micronaut.inject.ValidatedBeanDefinition; import io.micronaut.inject.annotation.AnnotationMetadataHierarchy; @@ -167,14 +165,16 @@ public class BeanDefinitionWriter extends AbstractClassFileWriter implements Bea public static final String CLASS_SUFFIX = "$Definition"; - private static final Constructor CONSTRUCTOR_ABSTRACT_CONSTRUCTOR_IP = ReflectionUtils.findConstructor( - AbstractConstructorInjectionPoint.class, + private static final Constructor CONSTRUCTOR_ABSTRACT_CONSTRUCTOR_IP = ReflectionUtils.findConstructor( + AbstractBeanDefinitionBeanConstructor.class, BeanDefinition.class) .orElseThrow(() -> new ClassGenerationException("Invalid version of Micronaut present on the class path")); private static final Method POST_CONSTRUCT_METHOD = ReflectionUtils.getRequiredInternalMethod(AbstractInitializableBeanDefinition.class, "postConstruct", BeanResolutionContext.class, BeanContext.class, Object.class); - private static final Method INJECT_BEAN_METHOD = ReflectionUtils.getRequiredInternalMethod(AbstractInitializableBeanDefinition.class, "injectBean", BeanResolutionContext.class, BeanContext.class, Object.class); + private static final org.objectweb.asm.commons.Method INJECT_BEAN_METHOD = org.objectweb.asm.commons.Method.getMethod( + ReflectionUtils.getRequiredInternalMethod(InjectableBeanDefinition.class, "inject", BeanResolutionContext.class, BeanContext.class, Object.class) + ); private static final Method PRE_DESTROY_METHOD = ReflectionUtils.getRequiredInternalMethod(AbstractInitializableBeanDefinition.class, "preDestroy", BeanResolutionContext.class, BeanContext.class, Object.class); @@ -350,9 +350,9 @@ public class BeanDefinitionWriter extends AbstractClassFileWriter implements Bea private static final org.objectweb.asm.commons.Method METHOD_OPTIONAL_OF = org.objectweb.asm.commons.Method.getMethod( ReflectionUtils.getRequiredMethod(Optional.class, "of", Object.class) ); - private static final org.objectweb.asm.commons.Method METHOD_INVOKE_CONSTRUCTOR = org.objectweb.asm.commons.Method.getMethod(ReflectionUtils.getRequiredMethod( - ConstructorInjectionPoint.class, - "invoke", + private static final org.objectweb.asm.commons.Method METHOD_BEAN_CONSTRUCTOR_INSTANTIATE = org.objectweb.asm.commons.Method.getMethod(ReflectionUtils.getRequiredMethod( + BeanConstructor.class, + "instantiate", Object[].class )); private static final String METHOD_DESCRIPTOR_CONSTRUCTOR_INSTANTIATE = getMethodDescriptor(Object.class, Arrays.asList( @@ -410,16 +410,21 @@ public class BeanDefinitionWriter extends AbstractClassFileWriter implements Bea AbstractInitializableBeanDefinition.AnnotationReference[].class, // annotationInjection ExecutableMethodsDefinition.class, // executableMethodsDefinition Map.class, // typeArgumentsMap + AbstractInitializableBeanDefinition.PrecalculatedInfo.class // precalculated info + )); + + private static final Type PRECALCULATED_INFO = Type.getType(AbstractInitializableBeanDefinition.PrecalculatedInfo.class); + private static final org.objectweb.asm.commons.Method PRECALCULATED_INFO_CONSTRUCTOR = org.objectweb.asm.commons.Method.getMethod( + ReflectionUtils.getRequiredInternalConstructor(AbstractInitializableBeanDefinition.PrecalculatedInfo.class, Optional.class, // scope boolean.class, // isAbstract - boolean.class, // isProvided boolean.class, // isIterable boolean.class, // isSingleton boolean.class, // isPrimary boolean.class, // isConfigurationProperties boolean.class, // isContainerType boolean.class // requiresMethodProcessing - )); + )); private static final String FIELD_CONSTRUCTOR = "$CONSTRUCTOR"; private static final String FIELD_INJECTION_METHODS = "$INJECTION_METHODS"; @@ -433,8 +438,7 @@ public class BeanDefinitionWriter extends AbstractClassFileWriter implements Bea Class.class, // declaringType, String.class, // methodName Argument[].class, // arguments - AnnotationMetadata.class, // annotationMetadata - boolean.class // boolean requiresReflection + AnnotationMetadata.class// annotationMetadata )); private static final org.objectweb.asm.commons.Method METHOD_REFERENCE_CONSTRUCTOR_POST_PRE = new org.objectweb.asm.commons.Method(CONSTRUCTOR_NAME, getConstructorDescriptor( @@ -442,15 +446,13 @@ public class BeanDefinitionWriter extends AbstractClassFileWriter implements Bea String.class, // methodName Argument[].class, // arguments AnnotationMetadata.class, // annotationMetadata - boolean.class, // boolean requiresReflection boolean.class, // isPostConstructMethod boolean.class // isPreDestroyMethod, )); private static final org.objectweb.asm.commons.Method FIELD_REFERENCE_CONSTRUCTOR = new org.objectweb.asm.commons.Method(CONSTRUCTOR_NAME, getConstructorDescriptor( Class.class, // declaringType; - Argument.class, // argument; - boolean.class // requiresReflection; + Argument.class // argument; )); private static final org.objectweb.asm.commons.Method ANNOTATION_REFERENCE_CONSTRUCTOR = new org.objectweb.asm.commons.Method(CONSTRUCTOR_NAME, getConstructorDescriptor( @@ -550,7 +552,6 @@ public class BeanDefinitionWriter extends AbstractClassFileWriter implements Bea private ExecutableMethodsDefinitionWriter executableMethodsDefinitionWriter; private Object constructor; // MethodElement or FieldElement - private boolean constructorRequiresReflection; private boolean disabled = false; private final boolean keepConfPropInjectPoints; @@ -596,8 +597,7 @@ public BeanDefinitionWriter(Element beanProducingElement, super(originatingElements); this.classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS | ClassWriter.COMPUTE_FRAMES); this.beanProducingElement = beanProducingElement; - if (beanProducingElement instanceof ClassElement) { - ClassElement classElement = (ClassElement) beanProducingElement; + if (beanProducingElement instanceof ClassElement classElement) { autoApplyNamedToBeanProducingElement(classElement); if (classElement.isPrimitive()) { throw new IllegalArgumentException("Primitive beans can only be created from factories"); @@ -609,9 +609,8 @@ public BeanDefinitionWriter(Element beanProducingElement, this.beanFullClassName = classElement.getName(); this.beanSimpleClassName = classElement.getSimpleName(); this.beanDefinitionName = getBeanDefinitionName(packageName, beanSimpleClassName); - } else if (beanProducingElement instanceof MethodElement) { + } else if (beanProducingElement instanceof MethodElement factoryMethodElement) { autoApplyNamedToBeanProducingElement(beanProducingElement); - MethodElement factoryMethodElement = (MethodElement) beanProducingElement; final ClassElement producedElement = factoryMethodElement.getGenericReturnType(); this.beanTypeElement = producedElement; this.packageName = producedElement.getPackageName(); @@ -625,9 +624,8 @@ public BeanDefinitionWriter(Element beanProducingElement, } final ClassElement declaringType = factoryMethodElement.getOwningType(); this.beanDefinitionName = declaringType.getPackageName() + "." + prefixClassName(declaringType.getSimpleName()) + "$" + upperCaseMethodName + uniqueIdentifier + CLASS_SUFFIX; - } else if (beanProducingElement instanceof FieldElement) { + } else if (beanProducingElement instanceof FieldElement factoryMethodElement) { autoApplyNamedToBeanProducingElement(beanProducingElement); - FieldElement factoryMethodElement = (FieldElement) beanProducingElement; final ClassElement producedElement = factoryMethodElement.getGenericField(); this.beanTypeElement = producedElement; this.packageName = producedElement.getPackageName(); @@ -641,8 +639,7 @@ public BeanDefinitionWriter(Element beanProducingElement, } final ClassElement declaringType = factoryMethodElement.getOwningType(); this.beanDefinitionName = declaringType.getPackageName() + "." + prefixClassName(declaringType.getSimpleName()) + "$" + fieldName + uniqueIdentifier + CLASS_SUFFIX; - } else if (beanProducingElement instanceof BeanElementBuilder) { - BeanElementBuilder beanElementBuilder = (BeanElementBuilder) beanProducingElement; + } else if (beanProducingElement instanceof BeanElementBuilder beanElementBuilder) { this.beanTypeElement = beanElementBuilder.getBeanType(); this.packageName = this.beanTypeElement.getPackageName(); this.isInterface = this.beanTypeElement.isInterface(); @@ -671,7 +668,6 @@ public BeanDefinitionWriter(Element beanProducingElement, this.beanType = getTypeReference(beanTypeElement); this.beanDefinitionInternalName = getInternalName(this.beanDefinitionName); this.interfaceTypes = new TreeSet<>(Comparator.comparing(Class::getName)); - this.interfaceTypes.add(BeanFactory.class); this.isConfigurationProperties = isConfigurationProperties(annotationMetadata); validateExposedTypes(annotationMetadata, visitorContext); this.visitorContext = visitorContext; @@ -878,7 +874,7 @@ public void visitBeanFactoryMethod(ClassElement factoryClass, constructor = factoryMethod; // now prepare the implementation of the build method. See BeanFactory interface visitBuildFactoryMethodDefinition(factoryClass, factoryMethod, factoryMethod.getParameters()); - // now override the injectBean method + // now implement the inject method visitInjectMethodDefinition(); } } @@ -901,7 +897,7 @@ public void visitBeanFactoryMethod(ClassElement factoryClass, constructor = factoryMethod; // now prepare the implementation of the build method. See BeanFactory interface visitBuildFactoryMethodDefinition(factoryClass, factoryMethod, parameters); - // now override the injectBean method + // now implement the inject method visitInjectMethodDefinition(); } } @@ -923,7 +919,7 @@ public void visitBeanFactoryField(ClassElement factoryClass, FieldElement factor autoApplyNamedIfPresent(factoryField, factoryField.getAnnotationMetadata()); // now prepare the implementation of the build method. See BeanFactory interface visitBuildFactoryMethodDefinition(factoryClass, factoryField); - // now override the injectBean method + // now implement the inject method visitInjectMethodDefinition(); } } @@ -941,12 +937,11 @@ public void visitBeanDefinitionConstructor(MethodElement constructor, VisitorContext visitorContext) { if (this.constructor == null) { this.constructor = constructor; - this.constructorRequiresReflection = requiresReflection; // now prepare the implementation of the build method. See BeanFactory interface visitBuildMethodDefinition(constructor, requiresReflection); - // now override the injectBean method + // now implement the inject method visitInjectMethodDefinition(); } } @@ -966,7 +961,7 @@ public void visitDefaultConstructor(AnnotationMetadata annotationMetadata, Visit // now prepare the implementation of the build method. See BeanFactory interface visitBuildMethodDefinition(defaultConstructor, false); - // now override the injectBean method + // now implement the inject method visitInjectMethodDefinition(); } } @@ -983,13 +978,12 @@ public void visitBeanDefinitionEnd() { processAllBeanElementVisitors(); - if (constructor instanceof MethodElement) { - MethodElement methodElement = (MethodElement) constructor; + if (constructor instanceof MethodElement methodElement) { boolean isParametrized = Arrays.stream(methodElement.getParameters()) .map(AnnotationMetadataProvider::getAnnotationMetadata) .anyMatch(this::isAnnotatedWithParameter); if (isParametrized) { - interfaceTypes.add(ParametrizedBeanFactory.class); + this.interfaceTypes.add(ParametrizedInstantiatableBeanDefinition.class); } } @@ -1030,7 +1024,6 @@ public void visitBeanDefinitionEnd() { JavaModelUtils.getTypeReference(methodVisitData.beanType), methodVisitData.methodElement, methodVisitData.getAnnotationMetadata(), - methodVisitData.requiresReflection, methodVisitData.isPostConstruct(), methodVisitData.isPreDestroy() ) @@ -1051,8 +1044,7 @@ public void visitBeanDefinitionEnd() { staticInit, JavaModelUtils.getTypeReference(fieldVisitData.beanType), fieldVisitData.fieldElement, - fieldVisitData.annotationMetadata, - fieldVisitData.requiresReflection + fieldVisitData.annotationMetadata ) ); } @@ -1098,8 +1090,7 @@ public void accept(Map stringClassElementMap) { // first build the constructor visitBeanDefinitionConstructorInternal( staticInit, - constructor, - constructorRequiresReflection + constructor ); addInnerConfigurationMethod(staticInit); @@ -2757,6 +2748,14 @@ private void pushInvokeMethodOnSuperClass(MethodVisitor constructorVisitor, Meth false); } + private void pushInvokeMethodOnSuperClass(MethodVisitor constructorVisitor, org.objectweb.asm.commons.Method methodToInvoke) { + constructorVisitor.visitMethodInsn(INVOKESPECIAL, + isSuperFactory ? TYPE_ABSTRACT_BEAN_DEFINITION.getInternalName() : superType.getInternalName(), + methodToInvoke.getName(), + methodToInvoke.getDescriptor(), + false); + } + private void visitCheckIfShouldLoadMethodDefinition() { String desc = getMethodDescriptor("void", BeanResolutionContext.class.getName(), BeanContext.class.getName()); this.checkIfShouldLoadMethodVisitor = new GeneratorAdapter(classWriter.visitMethod( @@ -2770,13 +2769,12 @@ private void visitCheckIfShouldLoadMethodDefinition() { @SuppressWarnings("MagicNumber") private void visitInjectMethodDefinition() { if (!isPrimitiveBean && !superBeanDefinition && injectMethodVisitor == null) { - String desc = getMethodDescriptor(Object.class.getName(), BeanResolutionContext.class.getName(), BeanContext.class.getName(), Object.class.getName()); injectMethodVisitor = new GeneratorAdapter(classWriter.visitMethod( - ACC_PROTECTED, - "injectBean", - desc, + ACC_PUBLIC, + INJECT_BEAN_METHOD.getName(), + INJECT_BEAN_METHOD.getDescriptor(), null, - null), ACC_PROTECTED, "injectBean", desc); + null), ACC_PUBLIC, INJECT_BEAN_METHOD.getName(), INJECT_BEAN_METHOD.getDescriptor()); GeneratorAdapter injectMethodVisitor = this.injectMethodVisitor; if (isConfigurationProperties) { @@ -3048,6 +3046,18 @@ private void invokeSuperInjectMethod(GeneratorAdapter methodVisitor, Method meth pushInvokeMethodOnSuperClass(methodVisitor, methodToInvoke); } + private void invokeSuperInjectMethod(GeneratorAdapter methodVisitor, org.objectweb.asm.commons.Method methodToInvoke) { + // load this + methodVisitor.loadThis(); + // load BeanResolutionContext arg 1 + methodVisitor.loadArg(0); + // load BeanContext arg 2 + methodVisitor.loadArg(1); + // load object being inject arg 3 + methodVisitor.loadArg(2); + pushInvokeMethodOnSuperClass(methodVisitor, methodToInvoke); + } + private void visitBuildFactoryMethodDefinition( ClassElement factoryClass, Element factoryElement, ParameterElement... parameters) { @@ -3080,8 +3090,7 @@ private void visitBuildFactoryMethodDefinition( final int parametersIndex = createParameterArray(parameterList, buildMethodVisitor); invokeConstructorChain(buildMethodVisitor, constructorIndex, parametersIndex, parameterList); } else { - if (factoryElement instanceof MethodElement) { - MethodElement methodElement = (MethodElement) factoryElement; + if (factoryElement instanceof MethodElement methodElement) { if (!methodElement.isReflectionRequired() && !parameterList.isEmpty()) { hasInjectScope = pushConstructorArguments(buildMethodVisitor, parameters); } @@ -3137,7 +3146,7 @@ private void visitBuildFactoryMethodDefinition( this.buildInstanceLocalVarIndex = buildMethodVisitor.newLocal(beanType); buildMethodVisitor.storeLocal(buildInstanceLocalVarIndex, beanType); if (!isPrimitiveBean) { - pushBeanDefinitionMethodInvocation(buildMethodVisitor, "injectBean"); + pushBeanDefinitionMethodInvocation(buildMethodVisitor, INJECT_BEAN_METHOD.getName()); pushCastToType(buildMethodVisitor, beanType); buildMethodVisitor.storeLocal(buildInstanceLocalVarIndex); } @@ -3243,7 +3252,7 @@ private void visitBuildMethodDefinition(MethodElement constructor, boolean requi this.buildInstanceLocalVarIndex = buildMethodVisitor.newLocal(beanType); buildMethodVisitor.storeLocal(buildInstanceLocalVarIndex); - pushBeanDefinitionMethodInvocation(buildMethodVisitor, "injectBean"); + pushBeanDefinitionMethodInvocation(buildMethodVisitor, INJECT_BEAN_METHOD.getName()); pushCastToType(buildMethodVisitor, beanType); buildMethodVisitor.storeLocal(buildInstanceLocalVarIndex); buildMethodVisitor.loadLocal(buildInstanceLocalVarIndex); @@ -3322,7 +3331,7 @@ private int initInterceptedConstructorWriter( List parameters, @Nullable FactoryMethodDef factoryMethodDef) { // write the constructor that is a subclass of AbstractConstructorInjectionPoint - InnerClassDef constructorInjectionPointInnerClass = newInnerClass(AbstractConstructorInjectionPoint.class); + InnerClassDef constructorInjectionPointInnerClass = newInnerClass(AbstractBeanDefinitionBeanConstructor.class); final ClassWriter interceptedConstructorWriter = constructorInjectionPointInnerClass.innerClassWriter; org.objectweb.asm.commons.Method constructorMethod = org.objectweb.asm.commons.Method.getMethod(CONSTRUCTOR_ABSTRACT_CONSTRUCTOR_IP); GeneratorAdapter protectedConstructor; @@ -3372,13 +3381,13 @@ private int initInterceptedConstructorWriter( } protectedConstructor.loadThis(); protectedConstructor.loadArg(0); - protectedConstructor.invokeConstructor(Type.getType(AbstractConstructorInjectionPoint.class), constructorMethod); + protectedConstructor.invokeConstructor(Type.getType(AbstractBeanDefinitionBeanConstructor.class), constructorMethod); protectedConstructor.returnValue(); protectedConstructor.visitMaxs(1, 1); protectedConstructor.visitEnd(); // now we need to implement the invoke method to execute the actual instantiation - final GeneratorAdapter invokeMethod = startPublicMethod(interceptedConstructorWriter, METHOD_INVOKE_CONSTRUCTOR); + final GeneratorAdapter invokeMethod = startPublicMethod(interceptedConstructorWriter, METHOD_BEAN_CONSTRUCTOR_INSTANTIATE); if (hasFactoryMethod) { invokeMethod.loadThis(); invokeMethod.getField( @@ -3438,7 +3447,7 @@ private int initInterceptedConstructorWriter( false ); - final int constructorIndex = buildMethodVisitor.newLocal(Type.getType(AbstractConstructorInjectionPoint.class)); + final int constructorIndex = buildMethodVisitor.newLocal(Type.getType(AbstractBeanDefinitionBeanConstructor.class)); buildMethodVisitor.storeLocal(constructorIndex); return constructorIndex; } @@ -3622,7 +3631,7 @@ private void pushConstructorArgument(GeneratorAdapter buildMethodVisitor, int index, boolean castToObject) { if (isAnnotatedWithParameter(annotationMetadata) && isParametrized) { // load the args - buildMethodVisitor.loadArg(3); + buildMethodVisitor.loadArg(2); // the argument name buildMethodVisitor.push(argumentName); buildMethodVisitor.invokeInterface(Type.getType(Map.class), org.objectweb.asm.commons.Method.getMethod(ReflectionUtils.getRequiredMethod(Map.class, "get", Object.class))); @@ -3897,41 +3906,34 @@ private void defineBuilderMethod(boolean isParametrized) { String methodDescriptor; String methodSignature; - final ClassElement beanDefinitionParam = ClassElement.of(BeanDefinition.class, - AnnotationMetadata.EMPTY_METADATA, - Collections.singletonMap("T", beanTypeElement)); if (isParametrized) { methodDescriptor = getMethodDescriptor( Object.class.getName(), BeanResolutionContext.class.getName(), BeanContext.class.getName(), - BeanDefinition.class.getName(), Map.class.getName() ); methodSignature = getMethodSignature( getTypeDescriptor(beanTypeElement), getTypeDescriptor(BeanResolutionContext.class.getName()), getTypeDescriptor(BeanContext.class.getName()), - getTypeDescriptor(beanDefinitionParam), getTypeDescriptor(Map.class.getName()) ); } else { methodDescriptor = getMethodDescriptor( Object.class.getName(), BeanResolutionContext.class.getName(), - BeanContext.class.getName(), - BeanDefinition.class.getName() + BeanContext.class.getName() ); methodSignature = getMethodSignature( getTypeDescriptor(beanTypeElement), getTypeDescriptor(BeanResolutionContext.class.getName()), - getTypeDescriptor(BeanContext.class.getName()), - getTypeDescriptor(beanDefinitionParam) + getTypeDescriptor(BeanContext.class.getName()) ); } - String methodName = isParametrized ? "doBuild" : "build"; + String methodName = isParametrized ? "doInstantiate" : "instantiate"; this.buildMethodVisitor = new GeneratorAdapter(classWriter.visitMethod( ACC_PUBLIC, methodName, @@ -3953,12 +3955,9 @@ private void pushBeanDefinitionMethodInvocation(GeneratorAdapter buildMethodVisi false); } - private void visitBeanDefinitionConstructorInternal( - GeneratorAdapter staticInit, Object constructor, - boolean requiresReflection) { + private void visitBeanDefinitionConstructorInternal(GeneratorAdapter staticInit, Object constructor) { - if (constructor instanceof MethodElement) { - MethodElement methodElement = (MethodElement) constructor; + if (constructor instanceof MethodElement methodElement) { AnnotationMetadata constructorMetadata = methodElement.getAnnotationMetadata(); DefaultAnnotationMetadata.contributeDefaults(this.annotationMetadata, constructorMetadata); VisitorContextUtils.contributeRepeatable(this.annotationMetadata, methodElement.getGenericReturnType()); @@ -3966,10 +3965,9 @@ private void visitBeanDefinitionConstructorInternal( List parameterList = Arrays.asList(parameters); applyDefaultNamedToParameters(parameterList); - pushNewMethodReference(staticInit, JavaModelUtils.getTypeReference(methodElement.getDeclaringType()), methodElement, methodElement.getAnnotationMetadata(), requiresReflection, false, false); - } else if (constructor instanceof FieldElement) { - FieldElement fieldConstructor = (FieldElement) constructor; - pushNewFieldReference(staticInit, JavaModelUtils.getTypeReference(fieldConstructor.getDeclaringType()), fieldConstructor, fieldConstructor.getAnnotationMetadata(), constructorRequiresReflection); + pushNewMethodReference(staticInit, JavaModelUtils.getTypeReference(methodElement.getDeclaringType()), methodElement, methodElement.getAnnotationMetadata(), false, false); + } else if (constructor instanceof FieldElement fieldConstructor) { + pushNewFieldReference(staticInit, JavaModelUtils.getTypeReference(fieldConstructor.getDeclaringType()), fieldConstructor, fieldConstructor.getAnnotationMetadata()); } else { throw new IllegalArgumentException("Unexpected constructor: " + constructor); } @@ -4048,40 +4046,9 @@ private void visitBeanDefinitionConstructorInternal( } else { protectedConstructor.getStatic(beanDefinitionType, FIELD_TYPE_ARGUMENTS, Type.getType(Map.class)); } - // 9: `Optional` scope - String scope = annotationMetadata.getAnnotationNameByStereotype(AnnotationUtil.SCOPE).orElse(null); - if (scope != null) { - protectedConstructor.push(scope); - protectedConstructor.invokeStatic( - TYPE_OPTIONAL, - METHOD_OPTIONAL_OF - ); - } else { - protectedConstructor.invokeStatic(TYPE_OPTIONAL, METHOD_OPTIONAL_EMPTY); - } - // 10: `boolean` isAbstract - protectedConstructor.push(isAbstract); - // 11: `boolean` isProvided - protectedConstructor.push( - annotationMetadata.hasDeclaredStereotype(Provided.class) - ); - // 12: `boolean` isIterable - protectedConstructor.push(isIterable(annotationMetadata)); - // 13: `boolean` isSingleton - protectedConstructor.push( - isSingleton(scope) - ); - // 14: `boolean` isPrimary - protectedConstructor.push( - annotationMetadata.hasDeclaredStereotype(Primary.class) - ); - // 15: `boolean` isConfigurationProperties - protectedConstructor.push(isConfigurationProperties); - // 16: isContainerType - protectedConstructor.push(isContainerType()); - // 17: requiresMethodProcessing - protectedConstructor.push(preprocessMethods); + // 9: `PrecalculatedInfo` + pushPrecalculatedInfo(protectedConstructor, annotationMetadata); protectedConstructor.invokeConstructor( isSuperFactory ? TYPE_ABSTRACT_BEAN_DEFINITION : superType, @@ -4094,6 +4061,44 @@ private void visitBeanDefinitionConstructorInternal( } } + private void pushPrecalculatedInfo(GeneratorAdapter protectedConstructor, AnnotationMetadata annotationMetadata) { + protectedConstructor.newInstance(PRECALCULATED_INFO); + protectedConstructor.dup(); + + // 1: `Optional` scope + String scope = annotationMetadata.getAnnotationNameByStereotype(AnnotationUtil.SCOPE).orElse(null); + if (scope != null) { + protectedConstructor.push(scope); + protectedConstructor.invokeStatic( + TYPE_OPTIONAL, + METHOD_OPTIONAL_OF + ); + } else { + protectedConstructor.invokeStatic(TYPE_OPTIONAL, METHOD_OPTIONAL_EMPTY); + } + + // 2: `boolean` isAbstract + protectedConstructor.push(isAbstract); + // 3: `boolean` isIterable + protectedConstructor.push(isIterable(annotationMetadata)); + // 4: `boolean` isSingleton + protectedConstructor.push( + isSingleton(scope) + ); + // 5: `boolean` isPrimary + protectedConstructor.push( + annotationMetadata.hasDeclaredStereotype(Primary.class) + ); + // 6: `boolean` isConfigurationProperties + protectedConstructor.push(isConfigurationProperties); + // 7: isContainerType + protectedConstructor.push(isContainerType()); + // 8: requiresMethodProcessing + protectedConstructor.push(preprocessMethods); + + protectedConstructor.invokeConstructor(PRECALCULATED_INFO, PRECALCULATED_INFO_CONSTRUCTOR); + } + private boolean isContainerType() { return beanTypeElement.isArray() || DefaultArgument.CONTAINER_TYPES.stream().anyMatch(c -> c.equals(beanFullClassName)); } @@ -4110,7 +4115,6 @@ private void pushNewMethodReference(GeneratorAdapter staticInit, Type beanType, MethodElement methodElement, AnnotationMetadata annotationMetadata, - boolean requiresReflection, boolean isPostConstructMethod, boolean isPreDestroyMethod) { annotationMetadata = annotationMetadata.getTargetAnnotationMetadata(); @@ -4143,12 +4147,10 @@ private void pushNewMethodReference(GeneratorAdapter staticInit, annotationMetadata = ((AnnotationMetadataHierarchy) annotationMetadata).merge(); } pushAnnotationMetadata(staticInit, annotationMetadata); - // 5: requiresReflection - staticInit.push(requiresReflection); if (isPreDestroyMethod || isPostConstructMethod) { - // 6: isPostConstructMethod + // 5: isPostConstructMethod staticInit.push(isPostConstructMethod); - // 7: isPreDestroyMethod + // 6: isPreDestroyMethod staticInit.push(isPreDestroyMethod); staticInit.invokeConstructor(Type.getType(AbstractInitializableBeanDefinition.MethodReference.class), METHOD_REFERENCE_CONSTRUCTOR_POST_PRE); } else { @@ -4156,7 +4158,7 @@ private void pushNewMethodReference(GeneratorAdapter staticInit, } } - private void pushNewFieldReference(GeneratorAdapter staticInit, Type declaringType, FieldElement fieldElement, AnnotationMetadata annotationMetadata, boolean requiresReflection) { + private void pushNewFieldReference(GeneratorAdapter staticInit, Type declaringType, FieldElement fieldElement, AnnotationMetadata annotationMetadata) { staticInit.newInstance(Type.getType(AbstractInitializableBeanDefinition.FieldReference.class)); staticInit.dup(); // 1: declaringType @@ -4174,8 +4176,6 @@ private void pushNewFieldReference(GeneratorAdapter staticInit, Type declaringTy defaultsStorage, loadTypeMethods ); - // 3: requiresReflection - staticInit.push(requiresReflection); staticInit.invokeConstructor(Type.getType(AbstractInitializableBeanDefinition.FieldReference.class), FIELD_REFERENCE_CONSTRUCTOR); } @@ -4509,7 +4509,7 @@ public void setProxiedBean(boolean proxiedBean, boolean isProxyTarget) { public boolean isProxyTarget() { return isProxyTarget; } - + @Override public boolean isProxiedBean() { return proxiedBean; diff --git a/core/src/main/java/io/micronaut/core/annotation/AnnotationClassValue.java b/core/src/main/java/io/micronaut/core/annotation/AnnotationClassValue.java index 99a70e609d6..c676d1762e7 100644 --- a/core/src/main/java/io/micronaut/core/annotation/AnnotationClassValue.java +++ b/core/src/main/java/io/micronaut/core/annotation/AnnotationClassValue.java @@ -167,6 +167,6 @@ public boolean equals(Object o) { @Override public int hashCode() { - return Objects.hash(name); + return name.hashCode(); } } diff --git a/core/src/main/java/io/micronaut/core/beans/AbstractBeanIntrospection.java b/core/src/main/java/io/micronaut/core/beans/AbstractBeanIntrospection.java index b1de2150017..dedf9f8e796 100644 --- a/core/src/main/java/io/micronaut/core/beans/AbstractBeanIntrospection.java +++ b/core/src/main/java/io/micronaut/core/beans/AbstractBeanIntrospection.java @@ -22,6 +22,7 @@ import io.micronaut.core.reflect.exception.InstantiationException; import io.micronaut.core.type.Argument; import io.micronaut.core.util.ArgumentUtils; +import io.micronaut.core.util.ObjectUtils; import io.micronaut.core.util.StringUtils; import io.micronaut.core.annotation.NonNull; @@ -312,7 +313,7 @@ public boolean equals(Object o) { @Override public int hashCode() { - return Objects.hash(beanType); + return beanType.hashCode(); } @Override @@ -349,7 +350,7 @@ public boolean equals(Object o) { @Override public int hashCode() { - return Objects.hash(type, value); + return ObjectUtils.hash(type, value); } } } diff --git a/core/src/main/java/io/micronaut/core/beans/AbstractBeanProperty.java b/core/src/main/java/io/micronaut/core/beans/AbstractBeanProperty.java index 03d11dfb5fa..cc8447715ea 100644 --- a/core/src/main/java/io/micronaut/core/beans/AbstractBeanProperty.java +++ b/core/src/main/java/io/micronaut/core/beans/AbstractBeanProperty.java @@ -24,6 +24,7 @@ import io.micronaut.core.annotation.NonNull; import io.micronaut.core.annotation.Nullable; +import io.micronaut.core.util.ObjectUtils; import java.util.Objects; @@ -219,7 +220,7 @@ public boolean equals(Object o) { @Override public int hashCode() { - return Objects.hash(beanType, type, name); + return ObjectUtils.hash(beanType, type, name); } @Override diff --git a/core/src/main/java/io/micronaut/core/beans/BeanIntrospectionMap.java b/core/src/main/java/io/micronaut/core/beans/BeanIntrospectionMap.java index e18bf5e6ecb..078f71da41b 100644 --- a/core/src/main/java/io/micronaut/core/beans/BeanIntrospectionMap.java +++ b/core/src/main/java/io/micronaut/core/beans/BeanIntrospectionMap.java @@ -20,6 +20,8 @@ import io.micronaut.core.util.CollectionUtils; import io.micronaut.core.annotation.NonNull; +import io.micronaut.core.util.ObjectUtils; + import java.util.*; import java.util.stream.Collectors; @@ -60,7 +62,7 @@ public boolean equals(Object o) { @Override public int hashCode() { - return Objects.hash(beanIntrospection, bean); + return ObjectUtils.hash(beanIntrospection, bean); } @Override diff --git a/core/src/main/java/io/micronaut/core/beans/DefaultBeanWrapper.java b/core/src/main/java/io/micronaut/core/beans/DefaultBeanWrapper.java index 36454e47cb7..e58f6beacdc 100644 --- a/core/src/main/java/io/micronaut/core/beans/DefaultBeanWrapper.java +++ b/core/src/main/java/io/micronaut/core/beans/DefaultBeanWrapper.java @@ -19,6 +19,8 @@ import io.micronaut.core.util.ArgumentUtils; import io.micronaut.core.annotation.NonNull; +import io.micronaut.core.util.ObjectUtils; + import java.util.Objects; /** @@ -73,6 +75,6 @@ public boolean equals(Object o) { @Override public int hashCode() { - return Objects.hash(bean, introspection); + return ObjectUtils.hash(bean, introspection); } } diff --git a/core/src/main/java/io/micronaut/core/convert/DefaultArgumentConversionContext.java b/core/src/main/java/io/micronaut/core/convert/DefaultArgumentConversionContext.java index 5724a4ebff9..c8a5a9713f7 100644 --- a/core/src/main/java/io/micronaut/core/convert/DefaultArgumentConversionContext.java +++ b/core/src/main/java/io/micronaut/core/convert/DefaultArgumentConversionContext.java @@ -17,6 +17,7 @@ import io.micronaut.core.annotation.Internal; import io.micronaut.core.type.Argument; +import io.micronaut.core.util.ObjectUtils; import java.nio.charset.Charset; import java.util.*; @@ -115,7 +116,7 @@ public boolean equals(Object o) { @Override public int hashCode() { - return Objects.hash(argument, finalLocale, finalCharset); + return ObjectUtils.hash(argument, finalLocale, finalCharset); } @Override diff --git a/core/src/main/java/io/micronaut/core/convert/DefaultMutableConversionService.java b/core/src/main/java/io/micronaut/core/convert/DefaultMutableConversionService.java index b04a474c1bd..88f16146b87 100644 --- a/core/src/main/java/io/micronaut/core/convert/DefaultMutableConversionService.java +++ b/core/src/main/java/io/micronaut/core/convert/DefaultMutableConversionService.java @@ -33,6 +33,7 @@ import io.micronaut.core.type.Argument; import io.micronaut.core.util.ArrayUtils; import io.micronaut.core.util.CollectionUtils; +import io.micronaut.core.util.ObjectUtils; import io.micronaut.core.util.StringUtils; import io.micronaut.core.util.clhm.ConcurrentLinkedHashMap; @@ -1027,7 +1028,7 @@ private static final class ConvertiblePair { this.source = source; this.target = target; this.formattingAnnotation = formattingAnnotation; - this.hashCode = Objects.hash(source, target, formattingAnnotation); + this.hashCode = ObjectUtils.hash(source, target, formattingAnnotation); } @Override diff --git a/core/src/main/java/io/micronaut/core/type/DefaultArgument.java b/core/src/main/java/io/micronaut/core/type/DefaultArgument.java index 60e41c4fc94..9ccd531865f 100644 --- a/core/src/main/java/io/micronaut/core/type/DefaultArgument.java +++ b/core/src/main/java/io/micronaut/core/type/DefaultArgument.java @@ -21,6 +21,7 @@ import io.micronaut.core.annotation.Nullable; import io.micronaut.core.util.ArrayUtils; import io.micronaut.core.util.CollectionUtils; +import io.micronaut.core.util.ObjectUtils; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; @@ -256,12 +257,12 @@ public boolean equals(Object o) { @Override public int typeHashCode() { - return Objects.hash(type, typeParameters); + return ObjectUtils.hash(type, typeParameters); } @Override public int hashCode() { - return Objects.hash(type, getName(), typeParameters); + return ObjectUtils.hash(type, getName(), typeParameters); } private static Map> initializeTypeParameters(Argument[] genericTypes) { diff --git a/core/src/main/java/io/micronaut/core/util/ObjectUtils.java b/core/src/main/java/io/micronaut/core/util/ObjectUtils.java new file mode 100644 index 00000000000..3bac9a947c7 --- /dev/null +++ b/core/src/main/java/io/micronaut/core/util/ObjectUtils.java @@ -0,0 +1,63 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.core.util; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.Nullable; + +/** + *

Utility methods for working with objects

. + * + * @author Denis Stepanov + * @since 4.0 + */ +@Internal +public final class ObjectUtils { + + private ObjectUtils() { + } + + /** + * Hashing method. Alternative to {@link java.util.Objects#hash(Object...)} without allocating an array. + * @param o1 The object 1 + * @param o2 The object 2 + * @return The hash + * @since 4.0.0 + */ + public static int hash(@Nullable Object o1, @Nullable Object o2) { + int result = 1; + result = 31 * result + (o1 == null ? 0 : o1.hashCode()); + result = 31 * result + (o2 == null ? 0 : o2.hashCode()); + return result; + } + + /** + * Hashing method. Alternative to {@link Objects#hash(Object...)} without allocating an array. + * @param o1 The object 1 + * @param o2 The object 2 + * @param o3 The object 3 + * @return The hash + * @since 4.0.0 + */ + public static int hash(@Nullable Object o1, @Nullable Object o2, @Nullable Object o3) { + int result = 1; + result = 31 * result + (o1 == null ? 0 : o1.hashCode()); + result = 31 * result + (o2 == null ? 0 : o2.hashCode()); + result = 31 * result + (o3 == null ? 0 : o3.hashCode()); + return result; + } + +} diff --git a/core/src/test/groovy/io/micronaut/core/util/ObjectUtilsSpec.groovy b/core/src/test/groovy/io/micronaut/core/util/ObjectUtilsSpec.groovy new file mode 100644 index 00000000000..e52f1a84901 --- /dev/null +++ b/core/src/test/groovy/io/micronaut/core/util/ObjectUtilsSpec.groovy @@ -0,0 +1,24 @@ +package io.micronaut.core.util + +import spock.lang.Specification + +class ObjectUtilsSpec extends Specification { + + def "validate hash2 function"() { + expect: + ObjectUtils.hash(o1, o2) == Objects.hash([o1, o2] as Object[]) + where: + o1 << ["abc", null, "xyz", null] + o2 << [null, null, "abc", "foo"] + } + + def "validate hash4 function"() { + expect: + ObjectUtils.hash(o1, o2, o3) == Objects.hash([o1, o2, o3] as Object[]) + where: + o1 << ["abc", null, "xyz", null] + o2 << [null, null, "abc", "foo"] + o3 << ["abc", null, "xyz", null] + } + +} diff --git a/discovery-core/src/main/java/io/micronaut/discovery/DefaultServiceInstance.java b/discovery-core/src/main/java/io/micronaut/discovery/DefaultServiceInstance.java index 7657bad0959..928fb363655 100644 --- a/discovery-core/src/main/java/io/micronaut/discovery/DefaultServiceInstance.java +++ b/discovery-core/src/main/java/io/micronaut/discovery/DefaultServiceInstance.java @@ -16,6 +16,7 @@ package io.micronaut.discovery; import io.micronaut.core.convert.value.ConvertibleValues; +import io.micronaut.core.util.ObjectUtils; import io.micronaut.core.util.StringUtils; import io.micronaut.health.HealthStatus; import io.micronaut.http.HttpHeaders; @@ -122,8 +123,7 @@ public boolean equals(Object o) { @Override public int hashCode() { - - return Objects.hash(id, uri); + return ObjectUtils.hash(id, uri); } @Override diff --git a/http-client-core/src/main/java/io/micronaut/http/client/ProxyRequestOptions.java b/http-client-core/src/main/java/io/micronaut/http/client/ProxyRequestOptions.java index 7c978a4823a..a73a0f8eeab 100644 --- a/http-client-core/src/main/java/io/micronaut/http/client/ProxyRequestOptions.java +++ b/http-client-core/src/main/java/io/micronaut/http/client/ProxyRequestOptions.java @@ -62,7 +62,7 @@ public boolean equals(Object o) { @Override public int hashCode() { - return Objects.hash(isRetainHostHeader()); + return Objects.hashCode(isRetainHostHeader()); } /** diff --git a/http-client/src/main/java/io/micronaut/http/client/netty/DefaultHttpClient.java b/http-client/src/main/java/io/micronaut/http/client/netty/DefaultHttpClient.java index 06be496caa1..3a88ebeb85f 100644 --- a/http-client/src/main/java/io/micronaut/http/client/netty/DefaultHttpClient.java +++ b/http-client/src/main/java/io/micronaut/http/client/netty/DefaultHttpClient.java @@ -34,6 +34,7 @@ import io.micronaut.core.util.ArgumentUtils; import io.micronaut.core.util.ArrayUtils; import io.micronaut.core.util.CollectionUtils; +import io.micronaut.core.util.ObjectUtils; import io.micronaut.core.util.StringUtils; import io.micronaut.http.HttpAttributes; import io.micronaut.http.HttpResponse; @@ -1959,7 +1960,7 @@ public boolean equals(Object o) { @Override public int hashCode() { - return Objects.hash(host, port, secure); + return ObjectUtils.hash(host, port, secure); } } diff --git a/http/src/main/java/io/micronaut/http/hateoas/GenericResource.java b/http/src/main/java/io/micronaut/http/hateoas/GenericResource.java index 26d1102f44c..16b230de1ac 100644 --- a/http/src/main/java/io/micronaut/http/hateoas/GenericResource.java +++ b/http/src/main/java/io/micronaut/http/hateoas/GenericResource.java @@ -19,10 +19,10 @@ import com.fasterxml.jackson.annotation.JsonAnySetter; import io.micronaut.core.annotation.Internal; import io.micronaut.core.annotation.Introspected; +import io.micronaut.core.util.ObjectUtils; import java.util.LinkedHashMap; import java.util.Map; -import java.util.Objects; /** * {@link Resource} with indeterminate structure. This is used as the deserialization target of @@ -75,7 +75,7 @@ public boolean equals(Object o) { @Override public int hashCode() { - return Objects.hash(getLinks(), getEmbedded(), getAdditionalProperties()); + return ObjectUtils.hash(getLinks(), getEmbedded(), getAdditionalProperties()); } @Override diff --git a/http/src/main/java/io/micronaut/http/simple/cookies/SimpleCookie.java b/http/src/main/java/io/micronaut/http/simple/cookies/SimpleCookie.java index 0d56077a0b5..2ca9fcabfba 100644 --- a/http/src/main/java/io/micronaut/http/simple/cookies/SimpleCookie.java +++ b/http/src/main/java/io/micronaut/http/simple/cookies/SimpleCookie.java @@ -15,11 +15,11 @@ */ package io.micronaut.http.simple.cookies; +import io.micronaut.core.annotation.NonNull; +import io.micronaut.core.util.ObjectUtils; import io.micronaut.http.cookie.Cookie; import io.micronaut.http.cookie.SameSite; -import io.micronaut.core.annotation.NonNull; -import java.util.Objects; import java.util.Optional; /** @@ -201,7 +201,7 @@ public int compareTo(Cookie c) { @Override public int hashCode() { - return Objects.hash(name, domain, path); + return ObjectUtils.hash(name, domain, path); } @Override diff --git a/http/src/main/java/io/micronaut/http/uri/UriMatchTemplate.java b/http/src/main/java/io/micronaut/http/uri/UriMatchTemplate.java index 886a237f8be..2db9f8254a7 100644 --- a/http/src/main/java/io/micronaut/http/uri/UriMatchTemplate.java +++ b/http/src/main/java/io/micronaut/http/uri/UriMatchTemplate.java @@ -15,6 +15,8 @@ */ package io.micronaut.http.uri; +import io.micronaut.core.util.ObjectUtils; + import java.util.ArrayList; import java.util.Collections; import java.util.LinkedHashMap; @@ -329,9 +331,7 @@ public String toString() { @Override public int hashCode() { - int result = uri.hashCode(); - result = 31 * result + variables.hashCode(); - return result; + return ObjectUtils.hash(uri, variableValues); } } diff --git a/http/src/main/java/io/micronaut/http/uri/UriMatchVariable.java b/http/src/main/java/io/micronaut/http/uri/UriMatchVariable.java index a81e8505514..d3b5cc00314 100644 --- a/http/src/main/java/io/micronaut/http/uri/UriMatchVariable.java +++ b/http/src/main/java/io/micronaut/http/uri/UriMatchVariable.java @@ -91,6 +91,6 @@ public boolean equals(Object o) { @Override public int hashCode() { - return Objects.hash(name); + return name.hashCode(); } } diff --git a/http/src/main/java/io/micronaut/http/uri/UriTemplate.java b/http/src/main/java/io/micronaut/http/uri/UriTemplate.java index 64e844dde49..2fa49784f67 100644 --- a/http/src/main/java/io/micronaut/http/uri/UriTemplate.java +++ b/http/src/main/java/io/micronaut/http/uri/UriTemplate.java @@ -17,6 +17,7 @@ import io.micronaut.core.beans.BeanMap; import io.micronaut.core.reflect.ClassUtils; +import io.micronaut.core.util.ObjectUtils; import io.micronaut.core.util.StringUtils; import java.io.UnsupportedEncodingException; @@ -855,9 +856,7 @@ public boolean equals(Object o) { @Override public int hashCode() { - int result = (isQuerySegment ? 1 : 0); - result = 31 * result + (value != null ? value.hashCode() : 0); - return result; + return ObjectUtils.hash(isQuerySegment, value); } @Override diff --git a/inject-groovy/src/test/groovy/io/micronaut/aop/compile/AbstractClassIntroductionSpec.groovy b/inject-groovy/src/test/groovy/io/micronaut/aop/compile/AbstractClassIntroductionSpec.groovy index f3255ef1d84..63e63bbed49 100644 --- a/inject-groovy/src/test/groovy/io/micronaut/aop/compile/AbstractClassIntroductionSpec.groovy +++ b/inject-groovy/src/test/groovy/io/micronaut/aop/compile/AbstractClassIntroductionSpec.groovy @@ -18,7 +18,7 @@ package io.micronaut.aop.compile import io.micronaut.ast.transform.test.AbstractBeanDefinitionSpec import io.micronaut.context.DefaultBeanContext import io.micronaut.inject.BeanDefinition -import io.micronaut.inject.BeanFactory +import io.micronaut.inject.InstantiatableBeanDefinition import io.micronaut.inject.writer.BeanDefinitionVisitor /** * @author graemerocher @@ -53,7 +53,7 @@ abstract class AbstractBean { when: def context = new DefaultBeanContext() context.start() - def instance = ((BeanFactory)beanDefinition).build(context, beanDefinition) + def instance = ((InstantiatableBeanDefinition)beanDefinition).instantiate(context) then: @@ -92,7 +92,7 @@ abstract class AbstractBean implements Foo { when: def context = new DefaultBeanContext() context.start() - def instance = ((BeanFactory)beanDefinition).build(context, beanDefinition) + def instance = ((InstantiatableBeanDefinition)beanDefinition).instantiate(context) then: @@ -132,7 +132,7 @@ abstract class AbstractBean implements Foo { when: def context = new DefaultBeanContext() context.start() - def instance = ((BeanFactory)beanDefinition).build(context, beanDefinition) + def instance = ((InstantiatableBeanDefinition)beanDefinition).instantiate(context) then: @@ -184,7 +184,7 @@ abstract class AbstractBean implements Foo { when: def context = new DefaultBeanContext() context.start() - def instance = ((BeanFactory)beanDefinition).build(context, beanDefinition) + def instance = ((InstantiatableBeanDefinition)beanDefinition).instantiate(context) then: @@ -225,7 +225,7 @@ abstract class AbstractBean implements Foo { when: def context = new DefaultBeanContext() context.start() - def instance = ((BeanFactory)beanDefinition).build(context, beanDefinition) + def instance = ((InstantiatableBeanDefinition)beanDefinition).instantiate(context) then: diff --git a/inject-groovy/src/test/groovy/io/micronaut/aop/compile/IntroductionGenericTypesSpec.groovy b/inject-groovy/src/test/groovy/io/micronaut/aop/compile/IntroductionGenericTypesSpec.groovy index ad13591b3ce..abf86c4c40b 100644 --- a/inject-groovy/src/test/groovy/io/micronaut/aop/compile/IntroductionGenericTypesSpec.groovy +++ b/inject-groovy/src/test/groovy/io/micronaut/aop/compile/IntroductionGenericTypesSpec.groovy @@ -19,9 +19,8 @@ import io.micronaut.ast.transform.test.AbstractBeanDefinitionSpec import io.micronaut.context.DefaultBeanContext import io.micronaut.core.type.ReturnType import io.micronaut.inject.BeanDefinition -import io.micronaut.inject.BeanFactory +import io.micronaut.inject.InstantiatableBeanDefinition import io.micronaut.inject.writer.BeanDefinitionVisitor - /** * @author graemerocher * @since 1.0 @@ -139,7 +138,7 @@ class SubPerson extends Person {} when: def context = new DefaultBeanContext() context.start() - def instance = ((BeanFactory)beanDefinition).build(context, beanDefinition) + def instance = ((InstantiatableBeanDefinition)beanDefinition).instantiate(context) then:"the methods are invocable" @@ -221,7 +220,7 @@ class SubPerson extends Person {} when: def context = new DefaultBeanContext() context.start() - def instance = ((BeanFactory)beanDefinition).build(context, beanDefinition) + def instance = ((InstantiatableBeanDefinition)beanDefinition).instantiate(context) then:"the methods are invocable" @@ -279,7 +278,7 @@ class SubPerson extends Person {} when: def context = new DefaultBeanContext() context.start() - def instance = ((BeanFactory)beanDefinition).build(context, beanDefinition) + def instance = ((InstantiatableBeanDefinition)beanDefinition).instantiate(context) then:"the methods are invocable" @@ -335,7 +334,7 @@ class SubPerson extends Person {} when: def context = new DefaultBeanContext() context.start() - def instance = ((BeanFactory)beanDefinition).build(context, beanDefinition) + def instance = ((InstantiatableBeanDefinition)beanDefinition).instantiate(context) then:"the methods are invocable" diff --git a/inject-groovy/src/test/groovy/io/micronaut/aop/compile/IntroductionWithAroundSpec.groovy b/inject-groovy/src/test/groovy/io/micronaut/aop/compile/IntroductionWithAroundSpec.groovy index bba075a7725..bcad7f933ab 100644 --- a/inject-groovy/src/test/groovy/io/micronaut/aop/compile/IntroductionWithAroundSpec.groovy +++ b/inject-groovy/src/test/groovy/io/micronaut/aop/compile/IntroductionWithAroundSpec.groovy @@ -3,7 +3,7 @@ package io.micronaut.aop.compile import io.micronaut.ast.transform.test.AbstractBeanDefinitionSpec import io.micronaut.context.ApplicationContext import io.micronaut.inject.BeanDefinition -import io.micronaut.inject.BeanFactory +import io.micronaut.inject.InstantiatableBeanDefinition import io.micronaut.inject.writer.BeanDefinitionVisitor class IntroductionWithAroundSpec extends AbstractBeanDefinitionSpec { @@ -24,7 +24,7 @@ import jakarta.inject.Singleton; abstract class MyBean { abstract void save(@NotBlank String name, @Min(1L) int age); abstract void saveTwo(@Min(1L) String name); - + @io.micronaut.aop.interceptors.Mutating("name") public String myConcrete(String name) { return name; @@ -38,7 +38,7 @@ abstract class MyBean { when: ApplicationContext context = ApplicationContext.run() - def instance = ((BeanFactory) beanDefinition).build(context, beanDefinition) + def instance = ((InstantiatableBeanDefinition) beanDefinition).instantiate(context) then: instance.myConcrete("test") == 'changed' diff --git a/inject-groovy/src/test/groovy/io/micronaut/aop/compile/LifeCycleWithProxySpec.groovy b/inject-groovy/src/test/groovy/io/micronaut/aop/compile/LifeCycleWithProxySpec.groovy index d53e710d431..ed233a49aaf 100644 --- a/inject-groovy/src/test/groovy/io/micronaut/aop/compile/LifeCycleWithProxySpec.groovy +++ b/inject-groovy/src/test/groovy/io/micronaut/aop/compile/LifeCycleWithProxySpec.groovy @@ -3,7 +3,7 @@ package io.micronaut.aop.compile import io.micronaut.ast.transform.test.AbstractBeanDefinitionSpec import io.micronaut.context.ApplicationContext import io.micronaut.inject.BeanDefinition -import io.micronaut.inject.BeanFactory +import io.micronaut.inject.InstantiatableBeanDefinition import io.micronaut.inject.writer.BeanDefinitionWriter class LifeCycleWithProxySpec extends AbstractBeanDefinitionSpec { @@ -47,7 +47,7 @@ class MyBean { when: def context = ApplicationContext.run() - def instance = ((BeanFactory) beanDefinition).build(context, beanDefinition) + def instance = ((InstantiatableBeanDefinition) beanDefinition).instantiate(context) then: @@ -98,7 +98,7 @@ class MyBean { when: def context = ApplicationContext.run() - def instance = ((BeanFactory) beanDefinition).build(context, beanDefinition) + def instance = ((InstantiatableBeanDefinition) beanDefinition).instantiate(context) then: @@ -149,7 +149,7 @@ class MyBean { when: def context = ApplicationContext.run() - def instance = ((BeanFactory) beanDefinition).build(context, beanDefinition) + def instance = ((InstantiatableBeanDefinition) beanDefinition).instantiate(context) then: diff --git a/inject-groovy/src/test/groovy/io/micronaut/aop/compile/PropertyAdviceSpec.groovy b/inject-groovy/src/test/groovy/io/micronaut/aop/compile/PropertyAdviceSpec.groovy index 0371984fe5d..b79483bd964 100644 --- a/inject-groovy/src/test/groovy/io/micronaut/aop/compile/PropertyAdviceSpec.groovy +++ b/inject-groovy/src/test/groovy/io/micronaut/aop/compile/PropertyAdviceSpec.groovy @@ -3,7 +3,7 @@ package io.micronaut.aop.compile import io.micronaut.ast.transform.test.AbstractBeanDefinitionSpec import io.micronaut.context.ApplicationContext import io.micronaut.inject.BeanDefinition -import io.micronaut.inject.BeanFactory +import io.micronaut.inject.InstantiatableBeanDefinition import io.micronaut.inject.writer.BeanDefinitionWriter class PropertyAdviceSpec extends AbstractBeanDefinitionSpec { @@ -21,7 +21,7 @@ import javax.inject.Singleton; @javax.inject.Singleton class MyPropertyBean { String name - + void test(String name) {} } @@ -32,7 +32,7 @@ class MyPropertyBean { when: ApplicationContext context = ApplicationContext.run() - def instance = ((BeanFactory) beanDefinition).build(context, beanDefinition) + def instance = ((InstantiatableBeanDefinition) beanDefinition).instantiate(context) instance.setName("test") then: diff --git a/inject-groovy/src/test/groovy/io/micronaut/aop/introduction/IntroductionAdviceWithNewInterfaceSpec.groovy b/inject-groovy/src/test/groovy/io/micronaut/aop/introduction/IntroductionAdviceWithNewInterfaceSpec.groovy index bbadb5696a8..08269a19d7c 100644 --- a/inject-groovy/src/test/groovy/io/micronaut/aop/introduction/IntroductionAdviceWithNewInterfaceSpec.groovy +++ b/inject-groovy/src/test/groovy/io/micronaut/aop/introduction/IntroductionAdviceWithNewInterfaceSpec.groovy @@ -19,9 +19,8 @@ import io.micronaut.ast.transform.test.AbstractBeanDefinitionSpec import io.micronaut.context.DefaultBeanContext import io.micronaut.context.event.ApplicationEventListener import io.micronaut.inject.BeanDefinition -import io.micronaut.inject.BeanFactory +import io.micronaut.inject.InstantiatableBeanDefinition import io.micronaut.inject.writer.BeanDefinitionVisitor - /** * @author graemerocher * @since 1.0 @@ -57,7 +56,7 @@ class MyBean { when: def context = new DefaultBeanContext() context.start() - def instance = ((BeanFactory)beanDefinition).build(context, beanDefinition) + def instance = ((InstantiatableBeanDefinition)beanDefinition).instantiate(context) ListenerAdviceInterceptor listenerAdviceInterceptor= context.getBean(ListenerAdviceInterceptor) then:"the methods are invocable" @@ -97,7 +96,7 @@ abstract class MyBean2 { when: def context = new DefaultBeanContext() context.start() - def instance = ((BeanFactory)beanDefinition).build(context, beanDefinition) + def instance = ((InstantiatableBeanDefinition)beanDefinition).instantiate(context) ListenerAdviceInterceptor listenerAdviceInterceptor= context.getBean(ListenerAdviceInterceptor) then:"the methods are invocable" @@ -141,7 +140,7 @@ interface MyBean3 { when: def context = new DefaultBeanContext() context.start() - def instance = ((BeanFactory)beanDefinition).build(context, beanDefinition) + def instance = ((InstantiatableBeanDefinition)beanDefinition).instantiate(context) ListenerAdviceInterceptor listenerAdviceInterceptor= context.getBean(ListenerAdviceInterceptor) then:"the methods are invocable" @@ -186,7 +185,7 @@ interface SpecificInterface { when: def context = new DefaultBeanContext() context.start() - def instance = ((BeanFactory)beanDefinition).build(context, beanDefinition) + def instance = ((InstantiatableBeanDefinition)beanDefinition).instantiate(context) then: //I ended up going this route because actually calling the methods here would be relying on @@ -265,7 +264,7 @@ interface MyInterface4 { when: def context = new DefaultBeanContext() context.start() - def instance = ((BeanFactory)beanDefinition).build(context, beanDefinition) + def instance = ((InstantiatableBeanDefinition)beanDefinition).instantiate(context) def introducer = context.getBean(StubIntroducer) then: instance.myMethod1("abc1") == "abc1" diff --git a/inject-groovy/src/test/groovy/io/micronaut/aop/introduction/with_around/IntroductionInnerInterfaceSpec.groovy b/inject-groovy/src/test/groovy/io/micronaut/aop/introduction/with_around/IntroductionInnerInterfaceSpec.groovy index 769198dc107..f3a296c7efc 100644 --- a/inject-groovy/src/test/groovy/io/micronaut/aop/introduction/with_around/IntroductionInnerInterfaceSpec.groovy +++ b/inject-groovy/src/test/groovy/io/micronaut/aop/introduction/with_around/IntroductionInnerInterfaceSpec.groovy @@ -3,7 +3,7 @@ package io.micronaut.aop.introduction.with_around import io.micronaut.ast.transform.test.AbstractBeanDefinitionSpec import io.micronaut.context.ApplicationContext import io.micronaut.inject.BeanDefinition -import io.micronaut.inject.BeanFactory +import io.micronaut.inject.InstantiatableBeanDefinition import io.micronaut.inject.writer.BeanDefinitionVisitor class IntroductionInnerInterfaceSpec extends AbstractBeanDefinitionSpec { @@ -26,7 +26,7 @@ import static java.lang.annotation.RetentionPolicy.RUNTIME; @Introduction(interfaces = ObservableUI.Inner.class) @Retention(RUNTIME) @interface ObservableUI { - public interface Inner { + public interface Inner { String hello(); } } @@ -43,7 +43,7 @@ class MyBean { when: def context = ApplicationContext.run() - def instance = ((BeanFactory) beanDefinition).build(context, beanDefinition) + def instance = ((InstantiatableBeanDefinition) beanDefinition).instantiate(context) then: instance.hello() == "World" diff --git a/inject-groovy/src/test/groovy/io/micronaut/inject/configproperties/ConfigPropertiesParseSpec.groovy b/inject-groovy/src/test/groovy/io/micronaut/inject/configproperties/ConfigPropertiesParseSpec.groovy index 4878be67edf..7e0d5d2090b 100644 --- a/inject-groovy/src/test/groovy/io/micronaut/inject/configproperties/ConfigPropertiesParseSpec.groovy +++ b/inject-groovy/src/test/groovy/io/micronaut/inject/configproperties/ConfigPropertiesParseSpec.groovy @@ -5,7 +5,7 @@ import io.micronaut.context.ApplicationContext import io.micronaut.context.annotation.ConfigurationReader import io.micronaut.context.annotation.Property import io.micronaut.inject.BeanDefinition -import io.micronaut.inject.BeanFactory +import io.micronaut.inject.InstantiatableBeanDefinition import io.micronaut.inject.configuration.Engine class ConfigPropertiesParseSpec extends AbstractBeanDefinitionSpec { @@ -28,9 +28,9 @@ class MyConfig1 { this } }''') - BeanFactory factory = beanDefinition + InstantiatableBeanDefinition factory = beanDefinition ApplicationContext applicationContext = ApplicationContext.builder(["my.host": "abc"]).start() - def bean = factory.build(applicationContext, beanDefinition) + def bean = factory.instantiate(applicationContext) then: bean.getHost() == "abc" @@ -238,13 +238,13 @@ class Parent { beanDefinition.injectedFields.isEmpty() when: - BeanFactory factory = beanDefinition + InstantiatableBeanDefinition factory = beanDefinition ApplicationContext applicationContext = ApplicationContext.run( 'foo.manufacturer':'Subaru', // 'foo.two.manufacturer':'Subaru', 'foo.three.manufacturer':'Subaru' ) - def bean = factory.build(applicationContext, beanDefinition) + def bean = factory.instantiate(applicationContext) then: ((Engine.Builder) bean.engine).build().manufacturer == 'Subaru' diff --git a/inject-groovy/src/test/groovy/io/micronaut/inject/configproperties/ConfigurationPropertiesBuilderSpec.groovy b/inject-groovy/src/test/groovy/io/micronaut/inject/configproperties/ConfigurationPropertiesBuilderSpec.groovy index 7dfe2be1ecc..a0f8574e579 100644 --- a/inject-groovy/src/test/groovy/io/micronaut/inject/configproperties/ConfigurationPropertiesBuilderSpec.groovy +++ b/inject-groovy/src/test/groovy/io/micronaut/inject/configproperties/ConfigurationPropertiesBuilderSpec.groovy @@ -19,7 +19,7 @@ import io.micronaut.ast.transform.test.AbstractBeanDefinitionSpec import io.micronaut.context.ApplicationContext import io.micronaut.context.env.PropertySource import io.micronaut.inject.BeanDefinition -import io.micronaut.inject.BeanFactory +import io.micronaut.inject.InstantiatableBeanDefinition import org.neo4j.driver.v1.Config class ConfigurationPropertiesBuilderSpec extends AbstractBeanDefinitionSpec { @@ -72,12 +72,12 @@ class TestAA { ''') when: - BeanFactory factory = beanDefinition + InstantiatableBeanDefinition factory = beanDefinition ApplicationContext applicationContext = ApplicationContext.run( 'test.foo':'good', 'test.bar':'bad' ) - def bean = factory.build(applicationContext, beanDefinition) + def bean = factory.instantiate(applicationContext) then: bean.test.foo == 'good' @@ -124,12 +124,12 @@ class TestA { ''') when: - BeanFactory factory = beanDefinition + InstantiatableBeanDefinition factory = beanDefinition ApplicationContext applicationContext = ApplicationContext.run( 'test.foo':'good', 'test.bar':'bad' ) - def bean = factory.build(applicationContext, beanDefinition) + def bean = factory.instantiate(applicationContext) then: bean.test.foo == 'good' @@ -164,11 +164,11 @@ class TestB { ''') when: - BeanFactory factory = beanDefinition + InstantiatableBeanDefinition factory = beanDefinition ApplicationContext applicationContext = ApplicationContext.run( 'test.bar':'good', ) - def bean = factory.build(applicationContext, beanDefinition) + def bean = factory.instantiate(applicationContext) then: bean.test.bar == 'good' @@ -197,11 +197,11 @@ class TestC { ''') expect:"The bean was built and a warning was logged" - BeanFactory factory = beanDefinition + InstantiatableBeanDefinition factory = beanDefinition ApplicationContext applicationContext = ApplicationContext.run( 'test.foo':'good', ) - factory.build(applicationContext, beanDefinition) + factory.instantiate(applicationContext) } @@ -230,13 +230,13 @@ class TestD { ''') when: - BeanFactory factory = beanDefinition + InstantiatableBeanDefinition factory = beanDefinition ApplicationContext applicationContext = ApplicationContext.run( 'test.foo':'good', 'test.bar': '10', 'test.baz':'20' ) - def bean = factory.build(applicationContext, beanDefinition) + def bean = factory.instantiate(applicationContext) then: bean != null @@ -276,13 +276,13 @@ class Neo4jProperties { beanDefinition.injectedFields.first().name == 'uri' when: - BeanFactory factory = beanDefinition + InstantiatableBeanDefinition factory = beanDefinition ApplicationContext applicationContext = ApplicationContext.run( 'neo4j.test.encryptionLevel':'none', 'neo4j.test.leakedSessionsLogging':true, 'neo4j.test.maxIdleSessions':2 ) - def bean = factory.build(applicationContext, beanDefinition) + def bean = factory.instantiate(applicationContext) then: bean != null @@ -324,13 +324,13 @@ class Neo4jProperties { beanDefinition.injectedFields.first().name == 'uri' when: - BeanFactory factory = beanDefinition + InstantiatableBeanDefinition factory = beanDefinition ApplicationContext applicationContext = ApplicationContext.run( 'neo4j.test.options.encryptionLevel':'none', 'neo4j.test.options.leakedSessionsLogging':true, 'neo4j.test.options.maxIdleSessions':2 ) - def bean = factory.build(applicationContext, beanDefinition) + def bean = factory.instantiate(applicationContext) then: bean != null @@ -372,13 +372,13 @@ class Neo4jProperties { beanDefinition.injectedFields.first().name == 'uri' when: - BeanFactory factory = beanDefinition + InstantiatableBeanDefinition factory = beanDefinition ApplicationContext applicationContext = ApplicationContext.run( 'neo4j.test.options.encryptionLevel':'none', 'neo4j.test.options.leakedSessionsLogging':true, 'neo4j.test.options.maxIdleSessions':2 ) - def bean = factory.build(applicationContext, beanDefinition) + def bean = factory.instantiate(applicationContext) then: bean != null @@ -418,11 +418,11 @@ class Neo4jProperties { beanDefinition.injectedFields.first().name == 'uri' when: - BeanFactory factory = beanDefinition + InstantiatableBeanDefinition factory = beanDefinition ApplicationContext applicationContext = ApplicationContext.run( 'neo4j.test.connectionLivenessCheckTimeout': '6s' ) - def bean = factory.build(applicationContext, beanDefinition) + def bean = factory.instantiate(applicationContext) then: bean != null @@ -454,11 +454,11 @@ class Neo4jProperties { } ''') - BeanFactory factory = beanDefinition + InstantiatableBeanDefinition factory = beanDefinition ApplicationContext applicationContext = ApplicationContext.run( 'neo4j.test.connectionLivenessCheckTimeout': '17s' ) - def bean = factory.build(applicationContext, beanDefinition) + def bean = factory.instantiate(applicationContext) then: bean != null diff --git a/inject-groovy/src/test/groovy/io/micronaut/inject/configproperties/ImmutableConfigurationPropertiesSpec.groovy b/inject-groovy/src/test/groovy/io/micronaut/inject/configproperties/ImmutableConfigurationPropertiesSpec.groovy index 46667d4894b..7e7f5ed92ed 100644 --- a/inject-groovy/src/test/groovy/io/micronaut/inject/configproperties/ImmutableConfigurationPropertiesSpec.groovy +++ b/inject-groovy/src/test/groovy/io/micronaut/inject/configproperties/ImmutableConfigurationPropertiesSpec.groovy @@ -4,7 +4,7 @@ import io.micronaut.ast.transform.test.AbstractBeanDefinitionSpec import io.micronaut.context.ApplicationContext import io.micronaut.context.annotation.Property import io.micronaut.inject.BeanDefinition -import io.micronaut.inject.BeanFactory +import io.micronaut.inject.InstantiatableBeanDefinition import io.micronaut.inject.ValidatedBeanDefinition class ImmutableConfigurationPropertiesSpec extends AbstractBeanDefinitionSpec { @@ -21,17 +21,17 @@ import java.time.Duration; class MyConfig { private String host; private int serverPort; - + @ConfigurationInject MyConfig(@javax.validation.constraints.NotBlank String host, int serverPort, @io.micronaut.core.annotation.Nullable String nullable) { this.host = host; this.serverPort = serverPort; } - + public String getHost() { return host; } - + public int getServerPort() { return serverPort; } @@ -50,7 +50,7 @@ class MyConfig { when: def context = ApplicationContext.run('foo.bar.host': 'test', 'foo.bar.server-port': '9999') - def config = ((BeanFactory) beanDefinition).build(context, beanDefinition) + def config = ((InstantiatableBeanDefinition) beanDefinition).instantiate(context) then: config.host == 'test' @@ -74,21 +74,21 @@ import java.time.Duration; class MyConfig { private String host; private int serverPort; - + @ConfigurationInject MyConfig(String host, int serverPort) { this.host = host; this.serverPort = serverPort; } - + public String getHost() { return host; } - + public int getServerPort() { return serverPort; } - + @ConfigurationProperties("baz") static class ChildConfig { final String stuff; @@ -108,7 +108,7 @@ class MyConfig { when: def context = ApplicationContext.run('foo.bar.baz.stuff': 'test') - def config = ((BeanFactory) beanDefinition).build(context, beanDefinition) + def config = ((InstantiatableBeanDefinition) beanDefinition).instantiate(context) then: config.stuff == 'test' diff --git a/inject-groovy/src/test/groovy/io/micronaut/inject/configproperties/InterfaceConfigurationPropertiesSpec.groovy b/inject-groovy/src/test/groovy/io/micronaut/inject/configproperties/InterfaceConfigurationPropertiesSpec.groovy index 61871b5c4b4..daa4e634b24 100644 --- a/inject-groovy/src/test/groovy/io/micronaut/inject/configproperties/InterfaceConfigurationPropertiesSpec.groovy +++ b/inject-groovy/src/test/groovy/io/micronaut/inject/configproperties/InterfaceConfigurationPropertiesSpec.groovy @@ -5,7 +5,7 @@ import io.micronaut.context.ApplicationContext import io.micronaut.context.annotation.Property import io.micronaut.context.exceptions.NoSuchBeanException import io.micronaut.inject.BeanDefinition -import io.micronaut.inject.BeanFactory +import io.micronaut.inject.InstantiatableBeanDefinition import io.micronaut.inject.ValidatedBeanDefinition import io.micronaut.runtime.context.env.ConfigurationAdvice @@ -40,7 +40,7 @@ interface MyConfig { when: def context = ApplicationContext.run('foo.bar.host': 'test', 'foo.bar.server-port': '9999') - def config = ((BeanFactory) beanDefinition).build(context, beanDefinition) + def config = ((InstantiatableBeanDefinition) beanDefinition).instantiate(context) then: config.host == 'test' @@ -87,7 +87,7 @@ interface MyConfig { when: def context = ApplicationContext.run() - def config = ((BeanFactory) beanDefinition).build(context, beanDefinition) + def config = ((InstantiatableBeanDefinition) beanDefinition).instantiate(context) then: config.host == null @@ -96,7 +96,7 @@ interface MyConfig { when: def context2 = ApplicationContext.run('foo.bar.host': 'test', 'foo.bar.server-port': '9999', 'foo.bar.url': 'http://test') - def config2 = ((BeanFactory) beanDefinition).build(context2, beanDefinition) + def config2 = ((InstantiatableBeanDefinition) beanDefinition).instantiate(context2) then: config2.host == 'test' @@ -143,7 +143,7 @@ interface ParentConfig { when: def context = ApplicationContext.run('foo.bar.host': 'test', 'foo.bar.server-port': '9999') - def config = ((BeanFactory) beanDefinition).build(context, beanDefinition) + def config = ((InstantiatableBeanDefinition) beanDefinition).instantiate(context) then: config.host == 'test' @@ -181,7 +181,7 @@ interface MyConfig { ''') def context = ApplicationContext.run('foo.bar.child.url': 'http://test') - def config = ((BeanFactory) beanDefinition).build(context, beanDefinition) + def config = ((InstantiatableBeanDefinition) beanDefinition).instantiate(context) then: config.URL == new URL("http://test") @@ -228,7 +228,7 @@ interface MyConfig { when: def context = ApplicationContext.run('foo.bar.child.url': 'http://test') - def config = ((BeanFactory) beanDefinition).build(context, beanDefinition) + def config = ((InstantiatableBeanDefinition) beanDefinition).instantiate(context) config.child then:"we expect a bean resolution" diff --git a/inject-groovy/src/test/groovy/io/micronaut/inject/configproperties/VisibilityIssuesSpec.groovy b/inject-groovy/src/test/groovy/io/micronaut/inject/configproperties/VisibilityIssuesSpec.groovy index d0154cf7fbe..6fc675c3071 100644 --- a/inject-groovy/src/test/groovy/io/micronaut/inject/configproperties/VisibilityIssuesSpec.groovy +++ b/inject-groovy/src/test/groovy/io/micronaut/inject/configproperties/VisibilityIssuesSpec.groovy @@ -18,7 +18,7 @@ package io.micronaut.inject.configproperties import io.micronaut.ast.transform.test.AbstractBeanDefinitionSpec import io.micronaut.context.ApplicationContext import io.micronaut.inject.BeanDefinition -import io.micronaut.inject.BeanFactory +import io.micronaut.inject.InstantiatableBeanDefinition class VisibilityIssuesSpec extends AbstractBeanDefinitionSpec { @@ -42,7 +42,7 @@ class VisibilityIssuesSpec extends AbstractBeanDefinitionSpec { 'parent.name': 'Sally', 'parent.child.age': 22, 'parent.engine.manufacturer': 'Chevy') - def instance = ((BeanFactory)beanDefinition).build(context, beanDefinition) + def instance = ((InstantiatableBeanDefinition)beanDefinition).instantiate(context) then: instance.getName() == null //methods that require reflection are not injected diff --git a/inject-java-test/src/test/groovy/io/micronaut/inject/beanimport/BeanImportSpec.groovy b/inject-java-test/src/test/groovy/io/micronaut/inject/beanimport/BeanImportSpec.groovy index 540d425094a..434a09abd9f 100644 --- a/inject-java-test/src/test/groovy/io/micronaut/inject/beanimport/BeanImportSpec.groovy +++ b/inject-java-test/src/test/groovy/io/micronaut/inject/beanimport/BeanImportSpec.groovy @@ -27,16 +27,12 @@ class Application {} context.containsBean(DefaultFallbackHandlerProvider) context.getBeanDefinition(DefaultFallbackHandlerProvider) .injectedFields.size() == 1 - context.getBeanDefinition(DefaultFallbackHandlerProvider) - .injectedFields.first().requiresReflection() context.containsBean(DefaultFaultToleranceOperationProvider) context.getBeanDefinition(DefaultFaultToleranceOperationProvider) .getConstructor().arguments.length == 1 context.containsBean(ExecutorHolder) context.getBeanDefinition(ExecutorHolder) .preDestroyMethods.size() == 1 - !context.getBeanDefinition(ExecutorHolder) - .preDestroyMethods.first().requiresReflection() cleanup: context.close() } @@ -65,16 +61,12 @@ class Application {} context.containsBean(DefaultFallbackHandlerProvider) context.getBeanDefinition(DefaultFallbackHandlerProvider) .injectedFields.size() == 1 - context.getBeanDefinition(DefaultFallbackHandlerProvider) - .injectedFields.first().requiresReflection() context.containsBean(DefaultFaultToleranceOperationProvider) context.getBeanDefinition(DefaultFaultToleranceOperationProvider) .getConstructor().arguments.length == 1 context.containsBean(ExecutorHolder) context.getBeanDefinition(ExecutorHolder) .preDestroyMethods.size() == 1 - !context.getBeanDefinition(ExecutorHolder) - .preDestroyMethods.first().requiresReflection() cleanup: context.close() } diff --git a/inject-java/src/main/java/io/micronaut/annotation/processing/ExecutableElementParamInfo.java b/inject-java/src/main/java/io/micronaut/annotation/processing/ExecutableElementParamInfo.java deleted file mode 100644 index eb8910c8337..00000000000 --- a/inject-java/src/main/java/io/micronaut/annotation/processing/ExecutableElementParamInfo.java +++ /dev/null @@ -1,117 +0,0 @@ -/* - * Copyright 2017-2020 original authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.micronaut.annotation.processing; - -import io.micronaut.core.annotation.AnnotationMetadata; -import io.micronaut.core.annotation.Internal; -import io.micronaut.inject.ast.ClassElement; -import io.micronaut.inject.ast.ParameterElement; - -import java.util.Collections; -import java.util.LinkedHashMap; -import java.util.Map; -import java.util.stream.Collectors; - -/** - * Holds parameter information for a {@link javax.lang.model.element.ExecutableElement}. - */ -@Internal -class ExecutableElementParamInfo { - - private boolean validated = false; - private final boolean requiresReflection; - private final AnnotationMetadata metadata; - private final Map parameters = new LinkedHashMap<>(10); - private final Map genericParameters = new LinkedHashMap<>(10); - private final Map parameterTypes = new LinkedHashMap<>(10); - - /** - * @param requiresReflection Whether reflection is required - * @param metadata The annotation metadata - */ - ExecutableElementParamInfo(boolean requiresReflection, AnnotationMetadata metadata) { - this.requiresReflection = requiresReflection; - this.metadata = metadata != null ? metadata : AnnotationMetadata.EMPTY_METADATA; - } - - /** - * Adds a parameter to the info. - * - * @param paramName The parameter name - * @param classElement The class element - */ - void addParameter(String paramName, ParameterElement classElement) { - parameters.put(paramName, classElement); - parameterTypes.put(paramName, classElement.getType()); - genericParameters.put(paramName, classElement.getGenericType()); - } - - /** - * @return The parameters - */ - Map getParameters() { - return Collections.unmodifiableMap(parameters); - } - - /** - * @return The parameter types - */ - Map getParameterTypes() { - return Collections.unmodifiableMap(parameterTypes); - } - - /** - * @return The generic parameters - */ - Map getGenericParameterTypes() { - return Collections.unmodifiableMap(genericParameters); - } - - /** - * @return The parameter annotation metadata - */ - Map getParameterMetadata() { - return getParameters().entrySet().stream().collect(Collectors.toMap(Map.Entry::getKey, (entry -> entry.getValue().getAnnotationMetadata()))); - } - - /** - * @return Is reflection required - */ - boolean isRequiresReflection() { - return requiresReflection; - } - - /** - * @return The annotation metadata for the method - */ - public AnnotationMetadata getAnnotationMetadata() { - return metadata; - } - - /** - * @return Is the executable validated - */ - public boolean isValidated() { - return validated; - } - - /** - * @param validated True if it is validated - */ - public void setValidated(boolean validated) { - this.validated = validated; - } -} diff --git a/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/AbstractJavaElement.java b/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/AbstractJavaElement.java index 4c5ca188311..0f6b3e701f5 100644 --- a/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/AbstractJavaElement.java +++ b/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/AbstractJavaElement.java @@ -22,13 +22,13 @@ import io.micronaut.core.annotation.NonNull; import io.micronaut.core.annotation.Nullable; import io.micronaut.inject.ast.ClassElement; -import io.micronaut.inject.ast.annotation.ElementAnnotationMetadata; -import io.micronaut.inject.ast.annotation.ElementAnnotationMetadataFactory; import io.micronaut.inject.ast.ElementModifier; -import io.micronaut.inject.ast.annotation.MutableAnnotationMetadataDelegate; -import io.micronaut.inject.ast.annotation.ElementMutableAnnotationMetadataDelegate; import io.micronaut.inject.ast.PrimitiveElement; import io.micronaut.inject.ast.TypedElement; +import io.micronaut.inject.ast.annotation.ElementAnnotationMetadata; +import io.micronaut.inject.ast.annotation.ElementAnnotationMetadataFactory; +import io.micronaut.inject.ast.annotation.ElementMutableAnnotationMetadataDelegate; +import io.micronaut.inject.ast.annotation.MutableAnnotationMetadataDelegate; import javax.lang.model.element.AnnotationMirror; import javax.lang.model.element.Element; @@ -49,7 +49,6 @@ import java.util.Collections; import java.util.List; import java.util.Map; -import java.util.Objects; import java.util.Optional; import java.util.Set; import java.util.function.Consumer; @@ -480,6 +479,6 @@ public boolean equals(Object o) { @Override public int hashCode() { - return Objects.hash(element); + return element.hashCode(); } } diff --git a/inject-java/src/test/groovy/io/micronaut/aop/compile/AbstractClassIntroductionSpec.groovy b/inject-java/src/test/groovy/io/micronaut/aop/compile/AbstractClassIntroductionSpec.groovy index 14a6bdd8db3..4cb4315306e 100644 --- a/inject-java/src/test/groovy/io/micronaut/aop/compile/AbstractClassIntroductionSpec.groovy +++ b/inject-java/src/test/groovy/io/micronaut/aop/compile/AbstractClassIntroductionSpec.groovy @@ -18,9 +18,8 @@ package io.micronaut.aop.compile import io.micronaut.annotation.processing.test.AbstractTypeElementSpec import io.micronaut.context.DefaultBeanContext import io.micronaut.inject.BeanDefinition -import io.micronaut.inject.BeanFactory +import io.micronaut.inject.InstantiatableBeanDefinition import io.micronaut.inject.writer.BeanDefinitionVisitor - /** * @author graemerocher * @since 1.0 @@ -38,8 +37,8 @@ import io.micronaut.context.annotation.*; @Stub @jakarta.inject.Singleton abstract class AbstractBean { - public abstract String isAbstract(); - + public abstract String isAbstract(); + public String nonAbstract() { return "good"; } @@ -54,7 +53,7 @@ abstract class AbstractBean { when: def context = new DefaultBeanContext() context.start() - def instance = ((BeanFactory)beanDefinition).build(context, beanDefinition) + def instance = ((InstantiatableBeanDefinition)beanDefinition).instantiate(context) then: @@ -76,8 +75,8 @@ interface Foo { @Stub @jakarta.inject.Singleton abstract class AbstractBean implements Foo { - public abstract String isAbstract(); - + public abstract String isAbstract(); + @Override public String nonAbstract() { return "good"; @@ -93,7 +92,7 @@ abstract class AbstractBean implements Foo { when: def context = new DefaultBeanContext() context.start() - def instance = ((BeanFactory)beanDefinition).build(context, beanDefinition) + def instance = ((InstantiatableBeanDefinition)beanDefinition).instantiate(context) then: @@ -116,8 +115,8 @@ interface Foo { @Stub @jakarta.inject.Singleton abstract class AbstractBean implements Foo { - public abstract String isAbstract(); - + public abstract String isAbstract(); + @Override public String nonAbstract() { return "good"; @@ -133,7 +132,7 @@ abstract class AbstractBean implements Foo { when: def context = new DefaultBeanContext() context.start() - def instance = ((BeanFactory)beanDefinition).build(context, beanDefinition) + def instance = ((InstantiatableBeanDefinition)beanDefinition).instantiate(context) then: @@ -152,7 +151,7 @@ import io.micronaut.context.annotation.*; interface Bar { @Stub String nonAbstract(); - + String another(); } @@ -163,13 +162,13 @@ interface Foo extends Bar { @Stub @jakarta.inject.Singleton abstract class AbstractBean implements Foo { - public abstract String isAbstract(); - + public abstract String isAbstract(); + @Override public String nonAbstract() { return "good"; } - + @Override public String another() { return "good"; @@ -185,7 +184,7 @@ abstract class AbstractBean implements Foo { when: def context = new DefaultBeanContext() context.start() - def instance = ((BeanFactory)beanDefinition).build(context, beanDefinition) + def instance = ((InstantiatableBeanDefinition)beanDefinition).instantiate(context) then: @@ -209,8 +208,8 @@ interface Foo { @Stub @jakarta.inject.Singleton abstract class AbstractBean implements Foo { - public abstract String isAbstract(); - + public abstract String isAbstract(); + @Override public String nonAbstract() { return "good"; @@ -226,7 +225,7 @@ abstract class AbstractBean implements Foo { when: def context = new DefaultBeanContext() context.start() - def instance = ((BeanFactory)beanDefinition).build(context, beanDefinition) + def instance = ((InstantiatableBeanDefinition)beanDefinition).instantiate(context) then: @@ -245,7 +244,7 @@ import io.micronaut.context.annotation.*; @Stub interface Foo { String nonAbstract(); - + default String anotherNonAbstract() { return "good"; } @@ -253,8 +252,8 @@ interface Foo { @Stub @jakarta.inject.Singleton abstract class AbstractBean implements Foo { - public abstract String isAbstract(); - + public abstract String isAbstract(); + @Override public String nonAbstract() { return "good"; @@ -270,7 +269,7 @@ abstract class AbstractBean implements Foo { when: def context = new DefaultBeanContext() context.start() - def instance = ((BeanFactory)beanDefinition).build(context, beanDefinition) + def instance = ((InstantiatableBeanDefinition)beanDefinition).instantiate(context) then: @@ -293,7 +292,7 @@ interface Bar { } interface Foo extends Bar { String nonAbstract(); - + @Override default String anotherNonAbstract() { return "good"; @@ -302,8 +301,8 @@ interface Foo extends Bar { @Stub @jakarta.inject.Singleton abstract class AbstractBean implements Foo { - public abstract String isAbstract(); - + public abstract String isAbstract(); + @Override public String nonAbstract() { return "good"; @@ -319,7 +318,7 @@ abstract class AbstractBean implements Foo { when: def context = new DefaultBeanContext() context.start() - def instance = ((BeanFactory)beanDefinition).build(context, beanDefinition) + def instance = ((InstantiatableBeanDefinition)beanDefinition).instantiate(context) then: diff --git a/inject-java/src/test/groovy/io/micronaut/aop/compile/AnnotatedConstructorArgumentSpec.groovy b/inject-java/src/test/groovy/io/micronaut/aop/compile/AnnotatedConstructorArgumentSpec.groovy index 588f48761fb..2684284cc36 100644 --- a/inject-java/src/test/groovy/io/micronaut/aop/compile/AnnotatedConstructorArgumentSpec.groovy +++ b/inject-java/src/test/groovy/io/micronaut/aop/compile/AnnotatedConstructorArgumentSpec.groovy @@ -22,7 +22,7 @@ import io.micronaut.context.ApplicationContext import io.micronaut.core.annotation.AnnotationMetadata import io.micronaut.core.annotation.AnnotationUtil import io.micronaut.inject.BeanDefinition -import io.micronaut.inject.BeanFactory +import io.micronaut.inject.InstantiatableBeanDefinition import io.micronaut.inject.writer.BeanDefinitionWriter /** * @author graemerocher @@ -44,11 +44,11 @@ import io.micronaut.context.annotation.*; class MyBean { private String myValue; - + MyBean(@Value("${foo.bar}") String val) { this.myValue = val; } - + public String someMethod(String someVal) { return myValue + ' ' + someVal; } @@ -76,7 +76,7 @@ class MyBean { when: def context = ApplicationContext.run('foo.bar':'test') - def instance = ((BeanFactory)beanDefinition).build(context, beanDefinition) + def instance = ((InstantiatableBeanDefinition)beanDefinition).instantiate(context) then: @@ -98,11 +98,11 @@ import io.micronaut.context.annotation.*; class MyBean { private String myValue; - + MyBean(@Value("${foo.bar}") String val) { this.myValue = val; } - + @Mutating("someVal") public String someMethod(String someVal) { return myValue+ ' ' + someVal; @@ -132,7 +132,7 @@ class MyBean { when: def context = ApplicationContext.run('foo.bar':'test') - def instance = ((BeanFactory)beanDefinition).build(context, beanDefinition) + def instance = ((InstantiatableBeanDefinition)beanDefinition).instantiate(context) then: diff --git a/inject-java/src/test/groovy/io/micronaut/aop/compile/InheritedAnnotationMetadataSpec.groovy b/inject-java/src/test/groovy/io/micronaut/aop/compile/InheritedAnnotationMetadataSpec.groovy index aea10d2e54d..a960d77ac17 100644 --- a/inject-java/src/test/groovy/io/micronaut/aop/compile/InheritedAnnotationMetadataSpec.groovy +++ b/inject-java/src/test/groovy/io/micronaut/aop/compile/InheritedAnnotationMetadataSpec.groovy @@ -19,10 +19,9 @@ import io.micronaut.annotation.processing.test.AbstractTypeElementSpec import io.micronaut.context.ApplicationContext import io.micronaut.core.annotation.Blocking import io.micronaut.inject.BeanDefinition -import io.micronaut.inject.BeanFactory +import io.micronaut.inject.InstantiatableBeanDefinition import io.micronaut.inject.writer.BeanDefinitionVisitor import io.micronaut.inject.writer.BeanDefinitionWriter - /** * @author graemerocher * @since 1.0 @@ -103,7 +102,7 @@ interface MyInterface { when: def context = ApplicationContext.run('foo.bar':'test') - def instance = ((BeanFactory)beanDefinition).build(context, beanDefinition) + def instance = ((InstantiatableBeanDefinition)beanDefinition).instantiate(context) then: diff --git a/inject-java/src/test/groovy/io/micronaut/aop/compile/IntroductionAnnotationSpec.groovy b/inject-java/src/test/groovy/io/micronaut/aop/compile/IntroductionAnnotationSpec.groovy index db4b5af08d7..b9ac21e1870 100644 --- a/inject-java/src/test/groovy/io/micronaut/aop/compile/IntroductionAnnotationSpec.groovy +++ b/inject-java/src/test/groovy/io/micronaut/aop/compile/IntroductionAnnotationSpec.groovy @@ -21,12 +21,11 @@ import io.micronaut.aop.introduction.NotImplementedAdvice import io.micronaut.context.BeanContext import io.micronaut.inject.AdvisedBeanType import io.micronaut.inject.BeanDefinition -import io.micronaut.inject.BeanFactory +import io.micronaut.inject.InstantiatableBeanDefinition import io.micronaut.inject.writer.BeanDefinitionVisitor import javax.validation.constraints.Min import javax.validation.constraints.NotBlank - /** * @author graemerocher * @since 1.0 @@ -50,7 +49,7 @@ interface MyBean { ''') def context = BeanContext.run() - def bean = ((BeanFactory) beanDefinition).build(context, beanDefinition) + def bean = ((InstantiatableBeanDefinition) beanDefinition).instantiate(context) when: bean.test() @@ -91,7 +90,7 @@ abstract class MyBean { ''') def context = BeanContext.run() - def bean = ((BeanFactory) beanDefinition).build(context, beanDefinition) + def bean = ((InstantiatableBeanDefinition) beanDefinition).instantiate(context) when: bean.test() diff --git a/inject-java/src/test/groovy/io/micronaut/aop/compile/IntroductionGenericTypesSpec.groovy b/inject-java/src/test/groovy/io/micronaut/aop/compile/IntroductionGenericTypesSpec.groovy index 91854c39df8..4ecea0c29bb 100644 --- a/inject-java/src/test/groovy/io/micronaut/aop/compile/IntroductionGenericTypesSpec.groovy +++ b/inject-java/src/test/groovy/io/micronaut/aop/compile/IntroductionGenericTypesSpec.groovy @@ -19,9 +19,8 @@ import io.micronaut.annotation.processing.test.AbstractTypeElementSpec import io.micronaut.context.DefaultBeanContext import io.micronaut.core.type.ReturnType import io.micronaut.inject.BeanDefinition -import io.micronaut.inject.BeanFactory +import io.micronaut.inject.InstantiatableBeanDefinition import io.micronaut.inject.writer.BeanDefinitionVisitor - /** * @author graemerocher * @since 1.0 @@ -132,7 +131,7 @@ class SubPerson extends Person {} when: def context = new DefaultBeanContext() context.start() - def instance = ((BeanFactory)beanDefinition).build(context, beanDefinition) + def instance = ((InstantiatableBeanDefinition)beanDefinition).instantiate(context) then:"the methods are invocable" diff --git a/inject-java/src/test/groovy/io/micronaut/aop/compile/IntroductionWithAroundSpec.groovy b/inject-java/src/test/groovy/io/micronaut/aop/compile/IntroductionWithAroundSpec.groovy index b704c5bdb86..dbf535af636 100644 --- a/inject-java/src/test/groovy/io/micronaut/aop/compile/IntroductionWithAroundSpec.groovy +++ b/inject-java/src/test/groovy/io/micronaut/aop/compile/IntroductionWithAroundSpec.groovy @@ -3,7 +3,7 @@ package io.micronaut.aop.compile import io.micronaut.annotation.processing.test.AbstractTypeElementSpec import io.micronaut.context.ApplicationContext import io.micronaut.inject.BeanDefinition -import io.micronaut.inject.BeanFactory +import io.micronaut.inject.InstantiatableBeanDefinition import io.micronaut.inject.writer.BeanDefinitionVisitor class IntroductionWithAroundSpec extends AbstractTypeElementSpec { @@ -24,7 +24,7 @@ import jakarta.inject.Singleton; abstract class MyBean { abstract void save(@NotBlank String name, @Min(1L) int age); abstract void saveTwo(@Min(1L) String name); - + @io.micronaut.aop.simple.Mutating("name") public String myConcrete(String name) { return name; @@ -38,7 +38,7 @@ abstract class MyBean { when: ApplicationContext context = ApplicationContext.run() - def instance = ((BeanFactory) beanDefinition).build(context, beanDefinition) + def instance = ((InstantiatableBeanDefinition) beanDefinition).instantiate(context) then: instance.myConcrete("test") == 'changed' diff --git a/inject-java/src/test/groovy/io/micronaut/aop/compile/LifeCycleWithProxySpec.groovy b/inject-java/src/test/groovy/io/micronaut/aop/compile/LifeCycleWithProxySpec.groovy index 32ad132b599..d8432bd81bd 100644 --- a/inject-java/src/test/groovy/io/micronaut/aop/compile/LifeCycleWithProxySpec.groovy +++ b/inject-java/src/test/groovy/io/micronaut/aop/compile/LifeCycleWithProxySpec.groovy @@ -3,7 +3,7 @@ package io.micronaut.aop.compile import io.micronaut.annotation.processing.test.AbstractTypeElementSpec import io.micronaut.context.ApplicationContext import io.micronaut.inject.BeanDefinition -import io.micronaut.inject.BeanFactory +import io.micronaut.inject.InstantiatableBeanDefinition import io.micronaut.inject.writer.BeanDefinitionWriter class LifeCycleWithProxySpec extends AbstractTypeElementSpec { @@ -22,16 +22,16 @@ class MyBean { @jakarta.inject.Inject public ConversionService conversionService; public int count = 0; - + public String someMethod() { return "good"; } - + @jakarta.annotation.PostConstruct void created() { count++; } - + @javax.annotation.PreDestroy void destroyed() { count--; @@ -70,16 +70,16 @@ class MyBean { @jakarta.inject.Inject public ConversionService conversionService; public int count = 0; - + public String someMethod() { return "good"; } - + @jakarta.annotation.PostConstruct void created() { count++; } - + @javax.annotation.PreDestroy void destroyed() { count--; @@ -96,7 +96,7 @@ class MyBean { when: def context = ApplicationContext.run() - def instance = ((BeanFactory) beanDefinition).build(context, beanDefinition) + def instance = ((InstantiatableBeanDefinition) beanDefinition).instantiate(context) then: @@ -122,7 +122,7 @@ class MyBean { @jakarta.inject.Inject public ConversionService conversionService; public int count = 0; - + @Mutating("someVal") public String someMethod() { return "good"; @@ -132,7 +132,7 @@ class MyBean { void created() { count++; } - + @javax.annotation.PreDestroy void destroyed() { count--; @@ -147,7 +147,7 @@ class MyBean { when: def context = ApplicationContext.run() - def instance = ((BeanFactory) beanDefinition).build(context, beanDefinition) + def instance = ((InstantiatableBeanDefinition) beanDefinition).instantiate(context) then: @@ -173,17 +173,17 @@ class MyBean { @jakarta.inject.Inject public ConversionService conversionService; public int count = 0; - + @jakarta.annotation.PostConstruct void created() { count++; } - + @javax.annotation.PreDestroy void destroyed() { count--; } - + @Mutating("someVal") public String someMethod() { return "good"; @@ -198,7 +198,7 @@ class MyBean { when: def context = ApplicationContext.run() - def instance = ((BeanFactory) beanDefinition).build(context, beanDefinition) + def instance = ((InstantiatableBeanDefinition) beanDefinition).instantiate(context) then: diff --git a/inject-java/src/test/groovy/io/micronaut/aop/introduction/IntroductionAdviceWithNewInterfaceSpec.groovy b/inject-java/src/test/groovy/io/micronaut/aop/introduction/IntroductionAdviceWithNewInterfaceSpec.groovy index 91ca8bc011a..bfa0fa6a334 100644 --- a/inject-java/src/test/groovy/io/micronaut/aop/introduction/IntroductionAdviceWithNewInterfaceSpec.groovy +++ b/inject-java/src/test/groovy/io/micronaut/aop/introduction/IntroductionAdviceWithNewInterfaceSpec.groovy @@ -20,9 +20,8 @@ import io.micronaut.context.BeanContext import io.micronaut.context.DefaultBeanContext import io.micronaut.context.event.ApplicationEventListener import io.micronaut.inject.BeanDefinition -import io.micronaut.inject.BeanFactory +import io.micronaut.inject.InstantiatableBeanDefinition import io.micronaut.inject.writer.BeanDefinitionVisitor - /** * @author graemerocher * @since 1.0 @@ -59,7 +58,7 @@ class MyBean { when: def context = new DefaultBeanContext() context.start() - def instance = ((BeanFactory)beanDefinition).build(context, beanDefinition) + def instance = ((InstantiatableBeanDefinition)beanDefinition).instantiate(context) ListenerAdviceInterceptor listenerAdviceInterceptor= context.getBean(ListenerAdviceInterceptor) listenerAdviceInterceptor.recievedMessages.clear() then:"the methods are invocable" @@ -100,7 +99,7 @@ abstract class MyBean { when: def context = new DefaultBeanContext() context.start() - def instance = ((BeanFactory)beanDefinition).build(context, beanDefinition) + def instance = ((InstantiatableBeanDefinition)beanDefinition).instantiate(context) ListenerAdviceInterceptor listenerAdviceInterceptor= context.getBean(ListenerAdviceInterceptor) listenerAdviceInterceptor.recievedMessages.clear() then:"the methods are invocable" @@ -145,7 +144,7 @@ interface MyBean { when: def context = BeanContext.run() - def instance = ((BeanFactory)beanDefinition).build(context, beanDefinition) + def instance = ((InstantiatableBeanDefinition)beanDefinition).instantiate(context) ListenerAdviceInterceptor listenerAdviceInterceptor= context.getBean(ListenerAdviceInterceptor) listenerAdviceInterceptor.recievedMessages.clear() @@ -194,7 +193,7 @@ interface SpecificInterface { when: def context = new DefaultBeanContext() context.start() - def instance = ((BeanFactory)beanDefinition).build(context, beanDefinition) + def instance = ((InstantiatableBeanDefinition)beanDefinition).instantiate(context) then: //I ended up going this route because actually calling the methods here would be relying on @@ -273,7 +272,7 @@ interface MyInterface4 { when: def context = new DefaultBeanContext() context.start() - def instance = ((BeanFactory)beanDefinition).build(context, beanDefinition) + def instance = ((InstantiatableBeanDefinition)beanDefinition).instantiate(context) def introducer = context.getBean(StubIntroducer) then: instance.myMethod1("abc1") == "abc1" diff --git a/inject-java/src/test/groovy/io/micronaut/aop/introduction/with_around/IntroductionInnerInterfaceSpec.groovy b/inject-java/src/test/groovy/io/micronaut/aop/introduction/with_around/IntroductionInnerInterfaceSpec.groovy index e0df51c3de5..ef10897dbf9 100644 --- a/inject-java/src/test/groovy/io/micronaut/aop/introduction/with_around/IntroductionInnerInterfaceSpec.groovy +++ b/inject-java/src/test/groovy/io/micronaut/aop/introduction/with_around/IntroductionInnerInterfaceSpec.groovy @@ -3,7 +3,7 @@ package io.micronaut.aop.introduction.with_around import io.micronaut.annotation.processing.test.AbstractTypeElementSpec import io.micronaut.context.ApplicationContext import io.micronaut.inject.BeanDefinition -import io.micronaut.inject.BeanFactory +import io.micronaut.inject.InstantiatableBeanDefinition import io.micronaut.inject.writer.BeanDefinitionVisitor class IntroductionInnerInterfaceSpec extends AbstractTypeElementSpec { @@ -26,7 +26,7 @@ import static java.lang.annotation.RetentionPolicy.RUNTIME; @Introduction(interfaces = ObservableUI.Inner.class) @Retention(RUNTIME) @interface ObservableUI { - public interface Inner { + public interface Inner { String hello(); } } @@ -43,7 +43,7 @@ class MyBean { when: def context = ApplicationContext.run() - def instance = ((BeanFactory) beanDefinition).build(context, beanDefinition) + def instance = ((InstantiatableBeanDefinition) beanDefinition).instantiate(context) then: instance.hello() == "World" diff --git a/inject-java/src/test/groovy/io/micronaut/inject/beans/BeanDefinitionSpec.groovy b/inject-java/src/test/groovy/io/micronaut/inject/beans/BeanDefinitionSpec.groovy index 0689f488066..2b2f6536ac3 100644 --- a/inject-java/src/test/groovy/io/micronaut/inject/beans/BeanDefinitionSpec.groovy +++ b/inject-java/src/test/groovy/io/micronaut/inject/beans/BeanDefinitionSpec.groovy @@ -1,39 +1,13 @@ package io.micronaut.inject.beans +import io.micronaut.annotation.processing.test.AbstractTypeElementSpec import io.micronaut.core.annotation.AnnotationUtil import io.micronaut.core.annotation.Order -import io.micronaut.core.order.Ordered -import io.micronaut.annotation.processing.test.AbstractTypeElementSpec -import io.micronaut.inject.BeanDefinitionReference import io.micronaut.inject.qualifiers.Qualifiers import spock.lang.Issue -import jakarta.inject.Named -import jakarta.inject.Qualifier - class BeanDefinitionSpec extends AbstractTypeElementSpec { - - void 'test dynamic instantiate with constructor'() { - given: - def definition = buildBeanDefinition('genctor.Test', ''' -package genctor; - -import jakarta.inject.*; - -@Singleton -class Test { - Test(Runnable foo) {} -} - -''') - when: - def instance = definition.constructor.instantiate({} as Runnable) - - then: - instance != null - } - void "test limit the exposed bean types"() { given: def definition = buildBeanDefinition('limittypes.Test', ''' diff --git a/inject-java/src/test/groovy/io/micronaut/inject/configproperties/ConfigPropertiesParseSpec.groovy b/inject-java/src/test/groovy/io/micronaut/inject/configproperties/ConfigPropertiesParseSpec.groovy index 7ab88329762..5723364efdb 100644 --- a/inject-java/src/test/groovy/io/micronaut/inject/configproperties/ConfigPropertiesParseSpec.groovy +++ b/inject-java/src/test/groovy/io/micronaut/inject/configproperties/ConfigPropertiesParseSpec.groovy @@ -1,5 +1,6 @@ package io.micronaut.inject.configproperties +import io.micronaut.annotation.processing.test.AbstractTypeElementSpec import io.micronaut.context.ApplicationContext import io.micronaut.context.ApplicationContextBuilder import io.micronaut.context.BeanContext @@ -7,9 +8,8 @@ import io.micronaut.context.annotation.ConfigurationReader import io.micronaut.context.annotation.Property import io.micronaut.context.annotation.PropertySource import io.micronaut.core.convert.format.ReadableBytes -import io.micronaut.annotation.processing.test.AbstractTypeElementSpec import io.micronaut.inject.BeanDefinition -import io.micronaut.inject.BeanFactory +import io.micronaut.inject.InstantiatableBeanDefinition import io.micronaut.inject.configuration.Engine import spock.lang.Issue @@ -619,9 +619,9 @@ class MyProperties { beanDefinition.injectedMethods.size() == 1 when: - BeanFactory factory = beanDefinition + InstantiatableBeanDefinition factory = beanDefinition ApplicationContext applicationContext = ApplicationContext.builder().start() - def bean = factory.build(applicationContext, beanDefinition) + def bean = factory.instantiate(applicationContext) then: bean != null @@ -634,7 +634,7 @@ class MyProperties { ['foo.setterTest' :'foo', 'foo.fieldTest' :'bar'] ) - bean = factory.build(applicationContext, beanDefinition) + bean = factory.instantiate(applicationContext) then: bean != null @@ -687,9 +687,9 @@ class Parent { when: - BeanFactory factory = beanDefinition + InstantiatableBeanDefinition factory = beanDefinition ApplicationContext applicationContext = ApplicationContext.builder().start() - def bean = factory.build(applicationContext, beanDefinition) + def bean = factory.instantiate(applicationContext) then: bean != null @@ -702,7 +702,7 @@ class Parent { ['foo.setterTest' :'foo', 'foo.fieldTest' :'bar'] ) - bean = factory.build(applicationContext, beanDefinition) + bean = factory.instantiate(applicationContext) then: bean != null @@ -766,9 +766,9 @@ class MyConfig { return this; } }''') - BeanFactory factory = beanDefinition + InstantiatableBeanDefinition factory = beanDefinition ApplicationContext applicationContext = ApplicationContext.builder(["my.host": "abc"]).start() - def bean = factory.build(applicationContext, beanDefinition) + def bean = factory.instantiate(applicationContext) then: bean.getHost() == "abc" @@ -912,12 +912,12 @@ class Parent { beanDefinition.injectedFields.isEmpty() when: - BeanFactory factory = beanDefinition + InstantiatableBeanDefinition factory = beanDefinition ApplicationContext applicationContext = ApplicationContext.run( 'foo.manufacturer':'Subaru', 'foo.two.manufacturer':'Subaru' ) - def bean = factory.build(applicationContext, beanDefinition) + def bean = factory.instantiate(applicationContext) then: ((Engine.Builder) bean.engine).build().manufacturer == 'Subaru' diff --git a/inject-java/src/test/groovy/io/micronaut/inject/configproperties/ConfigurationPropertiesBuilderSpec.groovy b/inject-java/src/test/groovy/io/micronaut/inject/configproperties/ConfigurationPropertiesBuilderSpec.groovy index e3285fa43ae..c778618c335 100644 --- a/inject-java/src/test/groovy/io/micronaut/inject/configproperties/ConfigurationPropertiesBuilderSpec.groovy +++ b/inject-java/src/test/groovy/io/micronaut/inject/configproperties/ConfigurationPropertiesBuilderSpec.groovy @@ -15,12 +15,11 @@ */ package io.micronaut.inject.configproperties -import io.micronaut.context.ApplicationContext import io.micronaut.annotation.processing.test.AbstractTypeElementSpec +import io.micronaut.context.ApplicationContext import io.micronaut.inject.BeanDefinition -import io.micronaut.inject.BeanFactory +import io.micronaut.inject.InstantiatableBeanDefinition import org.neo4j.driver.v1.Config - /** * @author Graeme Rocher * @since 1.0 @@ -36,42 +35,42 @@ import io.micronaut.context.annotation.*; @ConfigurationProperties("test") class MyProperties { - + private Test test; - + @ConfigurationBuilder(factoryMethod="build") void setTest(Test test) { this.test = test; } - + Test getTest() { return this.test; } - + } class Test { private String foo; private Test() {} - public void setFoo(String s) { + public void setFoo(String s) { this.foo = s; } public String getFoo() { return foo; } - + static Test build() { return new Test(); - } + } } ''') when:"The bean was built and a warning was logged" - BeanFactory factory = beanDefinition + InstantiatableBeanDefinition factory = beanDefinition ApplicationContext applicationContext = ApplicationContext.run( 'test.foo':'good' ) - def bean = factory.build(applicationContext, beanDefinition) + def bean = factory.instantiate(applicationContext) then: bean.test.foo == 'good' @@ -86,42 +85,42 @@ import io.micronaut.context.annotation.*; @ConfigurationProperties("test") class MyProperties { - + @ConfigurationBuilder(factoryMethod="build", includes="foo") Test test; - + } class Test { private String foo; private String bar; private Test() {} - public void setFoo(String s) { + public void setFoo(String s) { this.foo = s; } public String getFoo() { return foo; } - public void setBar(String s) { + public void setBar(String s) { this.bar = s; } public String getBar() { return bar; } - + static Test build() { return new Test(); - } + } } ''') when:"The bean was built and a warning was logged" - BeanFactory factory = beanDefinition + InstantiatableBeanDefinition factory = beanDefinition ApplicationContext applicationContext = ApplicationContext.run( 'test.foo':'good', 'test.bar':'bad' ) - def bean = factory.build(applicationContext, beanDefinition) + def bean = factory.instantiate(applicationContext) then: bean.test.foo == 'good' @@ -137,34 +136,34 @@ import io.micronaut.context.annotation.*; @ConfigurationProperties("test") class MyProperties { - + @ConfigurationBuilder(factoryMethod="build") Test test; - + } class Test { private String foo; private Test() {} - public void setFoo(String s) { + public void setFoo(String s) { this.foo = s; } public String getFoo() { return foo; } - + static Test build() { return new Test(); - } + } } ''') when:"The bean was built and a warning was logged" - BeanFactory factory = beanDefinition + InstantiatableBeanDefinition factory = beanDefinition ApplicationContext applicationContext = ApplicationContext.run( 'test.foo':'good', ) - def bean = factory.build(applicationContext, beanDefinition) + def bean = factory.instantiate(applicationContext) then: bean.test.foo == 'good' @@ -179,26 +178,26 @@ import io.micronaut.context.annotation.*; @ConfigurationProperties("test") class MyProperties { - + @ConfigurationBuilder Test test = new Test(); - - + + } class Test { - public void setFoo(String s) { + public void setFoo(String s) { throw new NoSuchMethodError("setFoo"); } } ''') expect:"The bean was built and a warning was logged" - BeanFactory factory = beanDefinition + InstantiatableBeanDefinition factory = beanDefinition ApplicationContext applicationContext = ApplicationContext.run( 'test.foo':'good', ) - factory.build(applicationContext, beanDefinition) + factory.instantiate(applicationContext) } void "test with setters that return void"() { @@ -211,23 +210,23 @@ import java.lang.Deprecated; @ConfigurationProperties("test") class MyProperties { - + @ConfigurationBuilder Test test = new Test(); - - + + } class Test { private String foo; private int bar; private Long baz; - + public void setFoo(String s) { this.foo = s;} public void setBar(int s) {this.bar = s;} @Deprecated public void setBaz(Long s) {this.baz = s;} - + public String getFoo() { return this.foo; } public int getBar() { return this.bar; } public Long getBaz() { return this.baz; } @@ -235,13 +234,13 @@ class Test { ''') when: - BeanFactory factory = beanDefinition + InstantiatableBeanDefinition factory = beanDefinition ApplicationContext applicationContext = ApplicationContext.run( 'test.foo':'good', 'test.bar': '10', 'test.baz':'20' ) - def bean = factory.build(applicationContext, beanDefinition) + def bean = factory.instantiate(applicationContext) then: bean != null @@ -267,14 +266,14 @@ import org.neo4j.driver.v1.*; @ConfigurationProperties("neo4j.test") class Neo4jProperties { protected java.net.URI uri; - + @ConfigurationBuilder( - prefixes="with", + prefixes="with", allowZeroArgs=true ) Config.ConfigBuilder options = Config.build(); - - + + } ''') then: @@ -282,13 +281,13 @@ class Neo4jProperties { beanDefinition.injectedFields.first().name == 'uri' when: - BeanFactory factory = beanDefinition + InstantiatableBeanDefinition factory = beanDefinition ApplicationContext applicationContext = ApplicationContext.run( 'neo4j.test.encryptionLevel':'none', 'neo4j.test.leakedSessionsLogging':true, 'neo4j.test.maxIdleSessions':2 ) - def bean = factory.build(applicationContext, beanDefinition) + def bean = factory.instantiate(applicationContext) then: bean != null @@ -314,15 +313,15 @@ import org.neo4j.driver.v1.*; @ConfigurationProperties("neo4j.test") class Neo4jProperties { protected java.net.URI uri; - + @ConfigurationBuilder( - prefixes="with", + prefixes="with", allowZeroArgs=true, configurationPrefix="options" ) Config.ConfigBuilder options = Config.build(); - - + + } ''') then: @@ -330,13 +329,13 @@ class Neo4jProperties { beanDefinition.injectedFields.first().name == 'uri' when: - BeanFactory factory = beanDefinition + InstantiatableBeanDefinition factory = beanDefinition ApplicationContext applicationContext = ApplicationContext.run( 'neo4j.test.options.encryptionLevel':'none', 'neo4j.test.options.leakedSessionsLogging':true, 'neo4j.test.options.maxIdleSessions':2 ) - def bean = factory.build(applicationContext, beanDefinition) + def bean = factory.instantiate(applicationContext) then: bean != null @@ -362,15 +361,15 @@ import org.neo4j.driver.v1.*; @ConfigurationProperties("neo4j.test") class Neo4jProperties { protected java.net.URI uri; - + @ConfigurationBuilder( - prefixes="with", + prefixes="with", allowZeroArgs=true, value="options" ) Config.ConfigBuilder options = Config.build(); - - + + } ''') then: @@ -378,13 +377,13 @@ class Neo4jProperties { beanDefinition.injectedFields.first().name == 'uri' when: - BeanFactory factory = beanDefinition + InstantiatableBeanDefinition factory = beanDefinition ApplicationContext applicationContext = ApplicationContext.run( 'neo4j.test.options.encryptionLevel':'none', 'neo4j.test.options.leakedSessionsLogging':true, 'neo4j.test.options.maxIdleSessions':2 ) - def bean = factory.build(applicationContext, beanDefinition) + def bean = factory.instantiate(applicationContext) then: bean != null @@ -425,13 +424,13 @@ class Neo4jProperties { beanDefinition.injectedFields.first().name == 'uri' when: - BeanFactory factory = beanDefinition + InstantiatableBeanDefinition factory = beanDefinition ApplicationContext applicationContext = ApplicationContext.run( 'neo4j.test.options.encryptionLevel':'none', 'neo4j.test.options.leakedSessionsLogging':true, 'neo4j.test.options.maxIdleSessions':2 ) - def bean = factory.build(applicationContext, beanDefinition) + def bean = factory.instantiate(applicationContext) then: bean != null @@ -457,13 +456,13 @@ import org.neo4j.driver.v1.*; @ConfigurationProperties("neo4j.test") class Neo4jProperties { protected java.net.URI uri; - + @ConfigurationBuilder( - prefixes="with", + prefixes="with", allowZeroArgs=true ) Config.ConfigBuilder options = Config.build(); - + } ''') then: @@ -471,11 +470,11 @@ class Neo4jProperties { beanDefinition.injectedFields.first().name == 'uri' when: - BeanFactory factory = beanDefinition + InstantiatableBeanDefinition factory = beanDefinition ApplicationContext applicationContext = ApplicationContext.run( 'neo4j.test.connectionLivenessCheckTimeout': '6s' ) - def bean = factory.build(applicationContext, beanDefinition) + def bean = factory.instantiate(applicationContext) then: bean != null @@ -498,20 +497,20 @@ import org.neo4j.driver.v1.*; @ConfigurationProperties("neo4j.test") class Neo4jProperties { - + @ConfigurationBuilder( - prefixes="with", + prefixes="with", allowZeroArgs=true ) public final Config.ConfigBuilder options = Config.build(); - + } ''') - BeanFactory factory = beanDefinition + InstantiatableBeanDefinition factory = beanDefinition ApplicationContext applicationContext = ApplicationContext.run( 'neo4j.test.connectionLivenessCheckTimeout': '17s' ) - def bean = factory.build(applicationContext, beanDefinition) + def bean = factory.instantiate(applicationContext) then: bean != null diff --git a/inject-java/src/test/groovy/io/micronaut/inject/configproperties/ImmutableConfigurationPropertiesSpec.groovy b/inject-java/src/test/groovy/io/micronaut/inject/configproperties/ImmutableConfigurationPropertiesSpec.groovy index d5b848bd472..58bf272053a 100644 --- a/inject-java/src/test/groovy/io/micronaut/inject/configproperties/ImmutableConfigurationPropertiesSpec.groovy +++ b/inject-java/src/test/groovy/io/micronaut/inject/configproperties/ImmutableConfigurationPropertiesSpec.groovy @@ -1,13 +1,11 @@ package io.micronaut.inject.configproperties +import io.micronaut.annotation.processing.test.AbstractTypeElementSpec import io.micronaut.context.ApplicationContext import io.micronaut.context.ApplicationContextBuilder -import io.micronaut.context.DefaultBeanResolutionContext import io.micronaut.context.annotation.Property -import io.micronaut.core.naming.Named -import io.micronaut.annotation.processing.test.AbstractTypeElementSpec import io.micronaut.inject.BeanDefinition -import io.micronaut.inject.BeanFactory +import io.micronaut.inject.InstantiatableBeanDefinition import io.micronaut.inject.ValidatedBeanDefinition import io.micronaut.inject.qualifiers.Qualifiers @@ -77,7 +75,7 @@ class MyConfig { when: def context = ApplicationContext.run('foo.bar.host': 'test', 'foo.bar.server-port': '9999') - def config = ((BeanFactory) beanDefinition).build(context, beanDefinition) + def config = ((InstantiatableBeanDefinition) beanDefinition).instantiate(context) then: config.host == 'test' @@ -135,7 +133,7 @@ class MyConfig { when: def context = ApplicationContext.run('foo.bar.baz.stuff': 'test') - def config = ((BeanFactory) beanDefinition).build(context, beanDefinition) + def config = ((InstantiatableBeanDefinition) beanDefinition).instantiate(context) then: config.stuff == 'test' @@ -236,7 +234,7 @@ class MyConfig { ''') def context = ApplicationContext.run('foo.bar.host': 'test', 'foo.bar.server-port': '9999') - def config = ((BeanFactory) beanDefinition).build(context, beanDefinition) + def config = ((InstantiatableBeanDefinition) beanDefinition).instantiate(context) then: config.host == 'test' diff --git a/inject-java/src/test/groovy/io/micronaut/inject/configproperties/InterfaceConfigurationPropertiesSpec.groovy b/inject-java/src/test/groovy/io/micronaut/inject/configproperties/InterfaceConfigurationPropertiesSpec.groovy index 9f4254d308a..f84c45f0e44 100644 --- a/inject-java/src/test/groovy/io/micronaut/inject/configproperties/InterfaceConfigurationPropertiesSpec.groovy +++ b/inject-java/src/test/groovy/io/micronaut/inject/configproperties/InterfaceConfigurationPropertiesSpec.groovy @@ -1,12 +1,11 @@ package io.micronaut.inject.configproperties +import io.micronaut.annotation.processing.test.AbstractTypeElementSpec import io.micronaut.context.ApplicationContext import io.micronaut.context.annotation.Property import io.micronaut.context.exceptions.NoSuchBeanException -import io.micronaut.annotation.processing.test.AbstractTypeElementSpec import io.micronaut.inject.BeanDefinition -import io.micronaut.inject.BeanFactory -import io.micronaut.inject.ExecutableMethod +import io.micronaut.inject.InstantiatableBeanDefinition import io.micronaut.inject.ValidatedBeanDefinition import io.micronaut.runtime.context.env.ConfigurationAdvice @@ -26,7 +25,7 @@ import java.time.Duration; interface MyConfig { @javax.validation.constraints.NotBlank String getHost(); - + @javax.validation.constraints.Min(10L) int getServerPort(); } @@ -42,7 +41,7 @@ interface MyConfig { when: def context = ApplicationContext.run('foo.bar.host': 'test', 'foo.bar.server-port': '9999') - def config = ((BeanFactory) beanDefinition).build(context, beanDefinition) + def config = ((InstantiatableBeanDefinition) beanDefinition).instantiate(context) then: config.host == 'test' @@ -89,7 +88,7 @@ interface MyConfig { when: def context = ApplicationContext.run() - def config = ((BeanFactory) beanDefinition).build(context, beanDefinition) + def config = ((InstantiatableBeanDefinition) beanDefinition).instantiate(context) then: config.host == null @@ -98,7 +97,7 @@ interface MyConfig { when: def context2 = ApplicationContext.run('foo.bar.host': 'test', 'foo.bar.server-port': '9999', 'foo.bar.url': 'http://test') - def config2 = ((BeanFactory) beanDefinition).build(context2, beanDefinition) + def config2 = ((InstantiatableBeanDefinition) beanDefinition).instantiate(context2) then: config2.host == 'test' @@ -122,7 +121,7 @@ import java.time.Duration; @ConfigurationProperties("bar") interface MyConfig extends ParentConfig { - + @Executable @javax.validation.constraints.Min(10L) int getServerPort(); @@ -145,7 +144,7 @@ interface ParentConfig { when: def context = ApplicationContext.run('foo.bar.host': 'test', 'foo.bar.server-port': '9999') - def config = ((BeanFactory) beanDefinition).build(context, beanDefinition) + def config = ((InstantiatableBeanDefinition) beanDefinition).instantiate(context) then: config.host == 'test' @@ -171,12 +170,12 @@ interface MyConfig { @Executable @javax.validation.constraints.NotBlank String getHost(); - + @Executable @javax.validation.constraints.Min(10L) int getServerPort(); - @ConfigurationProperties("child") + @ConfigurationProperties("child") static interface ChildConfig { @Executable URL getURL(); @@ -191,7 +190,7 @@ interface MyConfig { when: def context = ApplicationContext.run('foo.bar.child.url': 'http://test') - def config = ((BeanFactory) beanDefinition).build(context, beanDefinition) + def config = ((InstantiatableBeanDefinition) beanDefinition).instantiate(context) then: config.URL == new URL("http://test") @@ -216,15 +215,15 @@ interface MyConfig { @javax.validation.constraints.NotBlank @Executable String getHost(); - + @javax.validation.constraints.Min(10L) @Executable int getServerPort(); - + @Executable ChildConfig getChild(); - @ConfigurationProperties("child") + @ConfigurationProperties("child") static interface ChildConfig { @Executable URL getURL(); @@ -239,7 +238,7 @@ interface MyConfig { when: def context = ApplicationContext.run('foo.bar.child.url': 'http://test') - def config = ((BeanFactory) beanDefinition).build(context, beanDefinition) + def config = ((InstantiatableBeanDefinition) beanDefinition).instantiate(context) config.child then:"we expect a bean resolution" @@ -264,7 +263,7 @@ import java.time.Duration; interface MyConfig { @javax.validation.constraints.NotBlank String junk(String s); - + @javax.validation.constraints.Min(10L) int getServerPort(); } diff --git a/inject-java/src/test/groovy/io/micronaut/inject/configproperties/RecConf.java b/inject-java/src/test/groovy/io/micronaut/inject/configproperties/RecConf.java index be6d628335c..7da2c0c7cc7 100644 --- a/inject-java/src/test/groovy/io/micronaut/inject/configproperties/RecConf.java +++ b/inject-java/src/test/groovy/io/micronaut/inject/configproperties/RecConf.java @@ -1,6 +1,7 @@ package io.micronaut.inject.configproperties; import io.micronaut.context.annotation.ConfigurationProperties; +import io.micronaut.core.util.ObjectUtils; import java.util.List; import java.util.Map; @@ -48,6 +49,6 @@ public boolean equals(Object o) { @Override public int hashCode() { - return Objects.hash(namesListOf, mapChildren, listChildren); + return ObjectUtils.hash(namesListOf, mapChildren, listChildren); } -} \ No newline at end of file +} diff --git a/inject-java/src/test/groovy/io/micronaut/inject/configproperties/VisibilityIssuesSpec.groovy b/inject-java/src/test/groovy/io/micronaut/inject/configproperties/VisibilityIssuesSpec.groovy index 47523bd59c8..937173ea1be 100644 --- a/inject-java/src/test/groovy/io/micronaut/inject/configproperties/VisibilityIssuesSpec.groovy +++ b/inject-java/src/test/groovy/io/micronaut/inject/configproperties/VisibilityIssuesSpec.groovy @@ -15,10 +15,10 @@ */ package io.micronaut.inject.configproperties -import io.micronaut.context.ApplicationContext import io.micronaut.annotation.processing.test.AbstractTypeElementSpec +import io.micronaut.context.ApplicationContext import io.micronaut.inject.BeanDefinition -import io.micronaut.inject.BeanFactory +import io.micronaut.inject.InstantiatableBeanDefinition class VisibilityIssuesSpec extends AbstractTypeElementSpec { @@ -26,19 +26,19 @@ class VisibilityIssuesSpec extends AbstractTypeElementSpec { given: BeanDefinition beanDefinition = buildBeanDefinition("io.micronaut.inject.configproperties.ChildConfigProperties", """ package io.micronaut.inject.configproperties; - + import io.micronaut.context.annotation.ConfigurationProperties; import io.micronaut.inject.configproperties.other.ParentConfigProperties; - + @ConfigurationProperties("child") public class ChildConfigProperties extends ParentConfigProperties { - + private Integer age; - + public Integer getAge() { return age; } - + public void setAge(Integer age) { this.age = age; } @@ -50,7 +50,7 @@ class VisibilityIssuesSpec extends AbstractTypeElementSpec { 'parent.child.age': 22, 'parent.name': 'Sally', 'parent.engine.manufacturer': 'Chevy') - def instance = ((BeanFactory)beanDefinition).build(context, beanDefinition) + def instance = ((InstantiatableBeanDefinition)beanDefinition).instantiate(context) then: beanDefinition.injectedMethods.size() == 2 @@ -70,23 +70,23 @@ class VisibilityIssuesSpec extends AbstractTypeElementSpec { given: BeanDefinition beanDefinition = buildBeanDefinition("io.micronaut.inject.configproperties.ChildConfigProperties", """ package io.micronaut.inject.configproperties; - + import io.micronaut.context.annotation.ConfigurationProperties; import io.micronaut.inject.configproperties.other.ParentConfigProperties; - + @ConfigurationProperties("child") public class ChildConfigProperties extends ParentConfigProperties { - + protected void setName(String name) { super.setName(name); } - + } """) when: def context = ApplicationContext.run('parent.nationality': 'Italian', 'parent.child.name': 'Sally') - def instance = ((BeanFactory)beanDefinition).build(context, beanDefinition) + def instance = ((InstantiatableBeanDefinition)beanDefinition).instantiate(context) then: beanDefinition.injectedMethods.size() == 1 diff --git a/inject-java/src/test/groovy/io/micronaut/inject/method/builderinjection/BuilderStyleInjectionSpec.groovy b/inject-java/src/test/groovy/io/micronaut/inject/method/builderinjection/BuilderStyleInjectionSpec.groovy index 248115a61dc..54fbf70da8d 100644 --- a/inject-java/src/test/groovy/io/micronaut/inject/method/builderinjection/BuilderStyleInjectionSpec.groovy +++ b/inject-java/src/test/groovy/io/micronaut/inject/method/builderinjection/BuilderStyleInjectionSpec.groovy @@ -15,12 +15,10 @@ */ package io.micronaut.inject.method.builderinjection -import io.micronaut.context.BeanContext -import io.micronaut.context.DefaultBeanContext import io.micronaut.annotation.processing.test.AbstractTypeElementSpec +import io.micronaut.context.DefaultBeanContext import io.micronaut.inject.BeanDefinition -import io.micronaut.inject.BeanDefinitionReference -import io.micronaut.inject.BeanFactory +import io.micronaut.inject.InstantiatableBeanDefinition import spock.lang.Issue class BuilderStyleInjectionSpec extends AbstractTypeElementSpec { @@ -37,7 +35,7 @@ import io.micronaut.context.annotation.*; @jakarta.inject.Singleton class Test { public java.net.URL url; - + @jakarta.inject.Inject Test setURL( java.net.URL url) { this.url = url; @@ -56,9 +54,7 @@ class Test { def context = new DefaultBeanContext() def url = new URL("http://localhost") context.registerSingleton(url) - def test = ((BeanFactory)definition).build( - context, definition - ) + def test = ((InstantiatableBeanDefinition)definition).instantiate(context) then: test.url == url @@ -79,23 +75,23 @@ class TestConfig { public java.net.URL url; private java.net.URL anotherUrl; - + private String name; - + public void setName(String name) { this.name = name; - } - + } + public String getName() { return this.name; } - + @jakarta.inject.Inject TestConfig setURL( java.net.URL url) { this.url = url; return this; } - + @jakarta.inject.Inject void setAnotherURL( java.net.URL url) { this.anotherUrl = url; @@ -113,9 +109,7 @@ class TestConfig { def context = new DefaultBeanContext() def url = new URL("http://localhost") context.registerSingleton(url) - def test = ((BeanFactory)definition).build( - context, definition - ) + def test = ((InstantiatableBeanDefinition)definition).instantiate(context) then: test.url == url diff --git a/inject/src/main/java/io/micronaut/context/AbstractBeanDefinition.java b/inject/src/main/java/io/micronaut/context/AbstractBeanDefinition.java deleted file mode 100644 index 3dd9dd9f113..00000000000 --- a/inject/src/main/java/io/micronaut/context/AbstractBeanDefinition.java +++ /dev/null @@ -1,2349 +0,0 @@ -/* - * Copyright 2017-2020 original authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.micronaut.context; - -import io.micronaut.context.annotation.*; -import io.micronaut.context.env.ConfigurationPath; -import io.micronaut.context.env.Environment; -import io.micronaut.context.event.BeanInitializedEventListener; -import io.micronaut.context.event.BeanInitializingEvent; -import io.micronaut.context.exceptions.*; -import io.micronaut.core.annotation.*; -import io.micronaut.core.bind.annotation.Bindable; -import io.micronaut.core.convert.ArgumentConversionContext; -import io.micronaut.core.convert.ConversionContext; -import io.micronaut.core.naming.Named; -import io.micronaut.core.type.Argument; -import io.micronaut.core.type.DefaultArgument; -import io.micronaut.core.util.CollectionUtils; -import io.micronaut.core.util.StringUtils; -import io.micronaut.core.value.PropertyResolver; -import io.micronaut.inject.*; -import io.micronaut.inject.annotation.AbstractEnvironmentAnnotationMetadata; -import io.micronaut.inject.qualifiers.InterceptorBindingQualifier; -import io.micronaut.inject.qualifiers.Qualifiers; -import io.micronaut.inject.qualifiers.TypeAnnotationQualifier; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.lang.annotation.Annotation; -import java.lang.reflect.Array; -import java.lang.reflect.Modifier; -import java.util.*; -import java.util.function.BiFunction; -import java.util.function.Supplier; -import java.util.stream.Stream; - -/** - *

Default implementation of the {@link BeanDefinition} interface. This class is generally not used directly in user - * code. - * Instead a build time tool does analysis of source code and dynamically produces subclasses of this class containing - * information about the available injection points for a given class.

- * - *

For technical reasons the class has to be marked as public, but is regarded as internal and should be used by - * compiler tools and plugins (such as AST transformation frameworks)

- * - *

The {@link io.micronaut.inject.writer.BeanDefinitionWriter} class can be used to produce bean definitions at - * compile or runtime

- * - * @param The Bean definition type - * @author Graeme Rocher - * @see io.micronaut.inject.writer.BeanDefinitionWriter - * @since 1.0 - */ -@Internal -public class AbstractBeanDefinition extends AbstractBeanContextConditional implements BeanDefinition, EnvironmentConfigurable { - private static final Logger LOG = LoggerFactory.getLogger(AbstractBeanDefinition.class); - private static final String NAMED_ATTRIBUTE = Named.class.getName(); - - @SuppressWarnings("WeakerAccess") - protected final List> methodInjectionPoints = new ArrayList<>(3); - @SuppressWarnings("WeakerAccess") - protected final List> fieldInjectionPoints = new ArrayList<>(3); - @SuppressWarnings("WeakerAccess") - protected List> postConstructMethods; - @SuppressWarnings("WeakerAccess") - protected List> preDestroyMethods; - @SuppressWarnings("WeakerAccess") - protected Map> executableMethodMap; - - private final Class type; - private final boolean isAbstract; - private final boolean isConfigurationProperties; - private final Class declaringType; - private final ConstructorInjectionPoint constructor; - private final Collection> requiredComponents = new HashSet<>(3); - private AnnotationMetadata beanAnnotationMetadata; - private Environment environment; - private Set> exposedTypes; - private Argument containerElement; - - /** - * Constructs a bean definition that is produced from a method call on another type (factory bean). - * - * @param producedType The produced type - * @param declaringType The declaring type of the method - * @param fieldName The method name - * @param fieldMetadata The metadata for the method - * @param isFinal Is the field final - * @since 3.0 - */ - @SuppressWarnings({"WeakerAccess"}) - @Internal - @UsedByGeneratedCode - protected AbstractBeanDefinition(Class producedType, - Class declaringType, - String fieldName, - AnnotationMetadata fieldMetadata, - boolean isFinal) { - this.type = producedType; - this.isAbstract = false; // factory beans are never abstract - this.declaringType = declaringType; - - this.constructor = new DefaultFieldConstructorInjectionPoint<>( - this, - declaringType, - producedType, - fieldName, - fieldMetadata - ); - this.isConfigurationProperties = hasStereotype(ConfigurationReader.class) || isIterable(); - initContainerElement(); - } - - /** - * Constructs a bean definition that is produced from a method call on another type (factory bean). - * - * @param producedType The produced type - * @param declaringType The declaring type of the method - * @param methodName The method name - * @param methodMetadata The metadata for the method - * @param requiresReflection Whether reflection is required to invoke the method - * @param arguments The method arguments - */ - @SuppressWarnings({"unchecked", "WeakerAccess"}) - @Internal - @UsedByGeneratedCode - protected AbstractBeanDefinition(Class producedType, - Class declaringType, - String methodName, - AnnotationMetadata methodMetadata, - boolean requiresReflection, - Argument... arguments) { - this.type = producedType; - this.isAbstract = false; // factory beans are never abstract - this.declaringType = declaringType; - - if (requiresReflection) { - this.constructor = new ReflectionMethodConstructorInjectionPoint( - this, - declaringType, - methodName, - arguments, - methodMetadata - ); - } else { - this.constructor = new DefaultMethodConstructorInjectionPoint( - this, - declaringType, - methodName, - arguments, - methodMetadata - ); - } - this.isConfigurationProperties = hasStereotype(ConfigurationReader.class) || isIterable(); - this.addRequiredComponents(arguments); - initContainerElement(); - } - - /** - * Constructs a bean for the given type. - * - * @param type The type - * @param constructorAnnotationMetadata The annotation metadata for the constructor - * @param requiresReflection Whether reflection is required - * @param arguments The constructor arguments used to build the bean - */ - @Internal - @UsedByGeneratedCode - protected AbstractBeanDefinition(Class type, - AnnotationMetadata constructorAnnotationMetadata, - boolean requiresReflection, - Argument... arguments) { - - this.type = type; - this.isAbstract = Modifier.isAbstract(this.type.getModifiers()); - this.declaringType = type; - if (requiresReflection) { - this.constructor = new ReflectionConstructorInjectionPoint<>( - this, - type, - constructorAnnotationMetadata, - arguments); - } else { - this.constructor = new DefaultConstructorInjectionPoint<>( - this, - type, - constructorAnnotationMetadata, - arguments - ); - } - this.isConfigurationProperties = hasStereotype(ConfigurationReader.class) || isIterable(); - this.addRequiredComponents(arguments); - initContainerElement(); - } - - private void initContainerElement() { - if (isContainerType()) { - final List> iterableArguments = getTypeArguments(Iterable.class); - if (!iterableArguments.isEmpty()) { - this.containerElement = iterableArguments.iterator().next(); - } - } - } - - @Override - public Optional> getContainerElement() { - return Optional.ofNullable(this.containerElement); - } - - @Override - public final boolean hasPropertyExpressions() { - return getAnnotationMetadata().hasPropertyExpressions(); - } - - @Override - public @NonNull List> getTypeArguments(String type) { - if (type == null) { - return Collections.emptyList(); - } - Map[]> typeArguments = getTypeArgumentsMap(); - Argument[] arguments = typeArguments.get(type); - if (arguments != null) { - return Arrays.asList(arguments); - } - return Collections.emptyList(); - } - - @Override - @NonNull - public AnnotationMetadata getAnnotationMetadata() { - if (this.beanAnnotationMetadata == null) { - this.beanAnnotationMetadata = initializeAnnotationMetadata(); - } - return this.beanAnnotationMetadata; - } - - @Override - public boolean isAbstract() { - return this.isAbstract; - } - - @Override - public boolean isIterable() { - return hasDeclaredStereotype(EachProperty.class) || hasDeclaredStereotype(EachBean.class); - } - - @Override - public boolean isPrimary() { - return hasDeclaredStereotype(Primary.class); - } - - @SuppressWarnings("unchecked") - @Override - public Optional> findMethod(String name, Class... argumentTypes) { - if (executableMethodMap != null) { - MethodKey methodKey = new MethodKey(name, argumentTypes); - ExecutableMethod invocableMethod = (ExecutableMethod) executableMethodMap.get(methodKey); - if (invocableMethod != null) { - return Optional.of(invocableMethod); - } - } - return Optional.empty(); - } - - @Override - @SuppressWarnings({"unchecked"}) - public Stream> findPossibleMethods(String name) { - if (executableMethodMap != null && executableMethodMap.keySet().stream().anyMatch(methodKey -> methodKey.name.equals(name))) { - return executableMethodMap - .values() - .stream() - .filter(method -> method.getMethodName().equals(name)); - } - return Stream.empty(); - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - return o != null && getClass() == o.getClass(); - } - - @Override - public int hashCode() { - return getClass().hashCode(); - } - - @Override - public String toString() { - return "Definition: " + declaringType.getName(); - } - - @Override - public Optional> getScope() { - return getAnnotationMetadata().getAnnotationTypeByStereotype(AnnotationUtil.SCOPE); - } - - @Override - public Optional getScopeName() { - return getAnnotationMetadata().getAnnotationNameByStereotype(AnnotationUtil.SCOPE); - } - - @Override - public final Class getBeanType() { - return type; - } - - @Override - @NonNull - public final Set> getExposedTypes() { - if (this.exposedTypes == null) { - this.exposedTypes = BeanDefinition.super.getExposedTypes(); - } - return this.exposedTypes; - } - - @Override - public final Optional> getDeclaringType() { - return Optional.ofNullable(declaringType); - } - - @Override - public final ConstructorInjectionPoint getConstructor() { - return this.constructor; - } - - @Override - public Collection> getRequiredComponents() { - return Collections.unmodifiableCollection(requiredComponents); - } - - @Override - public final Collection> getInjectedMethods() { - return Collections.unmodifiableCollection(methodInjectionPoints); - } - - @Override - public final Collection> getInjectedFields() { - return Collections.unmodifiableCollection(fieldInjectionPoints); - } - - @Override - public final Collection> getPostConstructMethods() { - if (postConstructMethods != null) { - return Collections.unmodifiableCollection(postConstructMethods); - } else { - return Collections.emptyList(); - } - } - - @Override - public final Collection> getPreDestroyMethods() { - if (preDestroyMethods != null) { - return Collections.unmodifiableCollection(preDestroyMethods); - } else { - return Collections.emptyList(); - } - } - - @Override - @NonNull - public String getName() { - return getBeanType().getName(); - } - - @SuppressWarnings("unchecked") - @Override - public T inject(BeanContext context, T bean) { - return (T) injectBean(new DefaultBeanResolutionContext(context, this), context, bean); - } - - @SuppressWarnings("unchecked") - @Override - public T inject(BeanResolutionContext resolutionContext, BeanContext context, T bean) { - return (T) injectBean(resolutionContext, context, bean); - } - - @Override - public Collection> getExecutableMethods() { - if (executableMethodMap != null) { - return Collections.unmodifiableCollection(this.executableMethodMap.values()); - } else { - return Collections.emptyList(); - } - } - - /** - * Configures the bean for the given {@link BeanContext}. If the context features an - * {@link io.micronaut.context.env.Environment} this method configures the annotation metadata such that - * environment aware values are returned. - * - * @param environment The environment - */ - @Internal - @Override - public final void configure(Environment environment) { - if (environment != null) { - this.environment = environment; - if (constructor instanceof EnvironmentConfigurable) { - ((EnvironmentConfigurable) constructor).configure(environment); - } - - for (MethodInjectionPoint methodInjectionPoint : methodInjectionPoints) { - if (methodInjectionPoint instanceof EnvironmentConfigurable) { - ((EnvironmentConfigurable) methodInjectionPoint).configure(environment); - } - } - - if (executableMethodMap != null) { - for (ExecutableMethod executableMethod : executableMethodMap.values()) { - if (executableMethod instanceof EnvironmentConfigurable) { - ((EnvironmentConfigurable) executableMethod).configure(environment); - } - } - } - } - } - - /** - * Allows printing warning messages produced by the compiler. - * - * @param message The message - */ - @Internal - protected final void warn(String message) { - if (LOG.isWarnEnabled()) { - LOG.warn(message); - } - } - - /** - * Allows printing warning messages produced by the compiler. - * - * @param type The type - * @param method The method - * @param property The property - */ - @SuppressWarnings("unused") - @Internal - protected final void warnMissingProperty(Class type, String method, String property) { - if (LOG.isWarnEnabled()) { - LOG.warn("Configuration property [{}] could not be set as the underlying method [{}] does not exist on builder [{}]. This usually indicates the configuration option was deprecated and has been removed by the builder implementation (potentially a third-party library).", property, method, type); - } - } - - /** - * Resolves the proxied bean instance for this bean. - * - * @param beanContext The {@link BeanContext} - * @return The proxied bean - */ - @SuppressWarnings({"unchecked", "unused"}) - @Internal - protected final Object getProxiedBean(BeanContext beanContext) { - DefaultBeanContext defaultBeanContext = (DefaultBeanContext) beanContext; - Optional qualifier = getAnnotationMetadata().getAnnotationNameByStereotype(AnnotationUtil.QUALIFIER); - return defaultBeanContext.getProxyTargetBean( - getBeanType(), - (Qualifier) qualifier.map(q -> Qualifiers.byAnnotation(getAnnotationMetadata(), q)).orElse(null) - ); - } - - /** - * Adds a new {@link ExecutableMethod}. - * - * @param executableMethod The method - * @return The bean definition - */ - @SuppressWarnings("unused") - @Internal - @UsedByGeneratedCode - protected final AbstractBeanDefinition addExecutableMethod(ExecutableMethod executableMethod) { - MethodKey key = new MethodKey(executableMethod.getMethodName(), executableMethod.getArgumentTypes()); - if (executableMethodMap == null) { - executableMethodMap = new LinkedHashMap<>(3); - } - executableMethodMap.put(key, executableMethod); - return this; - } - - /** - * Adds an injection point for a field. Typically called by a dynamically generated subclass. - * - * @param declaringType The declaring type - * @param fieldType The field type - * @param field The name of the field - * @param annotationMetadata The annotation metadata for the field - * @param typeArguments The arguments - * @param requiresReflection Whether reflection is required - * @return this component definition - */ - @SuppressWarnings({"unused", "unchecked"}) - @Internal - @UsedByGeneratedCode - protected final AbstractBeanDefinition addInjectionPoint( - Class declaringType, - Class fieldType, - String field, - @Nullable AnnotationMetadata annotationMetadata, - @Nullable Argument[] typeArguments, - boolean requiresReflection) { - FieldInjectionPoint injectionPoint; - if (requiresReflection) { - injectionPoint = new ReflectionFieldInjectionPoint( - this, - declaringType, - fieldType, - field, - annotationMetadata, - typeArguments - ); - } else { - injectionPoint = new DefaultFieldInjectionPoint( - this, - declaringType, - fieldType, - field, - annotationMetadata, - typeArguments - ); - } - if (annotationMetadata != null && annotationMetadata.hasDeclaredAnnotation(AnnotationUtil.INJECT)) { - addRequiredComponents(injectionPoint.asArgument()); - } - fieldInjectionPoints.add(injectionPoint); - return this; - } - - /** - * Adds an injection point for a method that cannot be resolved at runtime, but a compile time produced injection - * point exists. This allows the framework to recover and relay better error messages to the user instead of just - * NoSuchMethodError. - * - * @param declaringType The declaring type - * @param method The method - * @param arguments The argument types - * @param annotationMetadata The annotation metadata - * @param requiresReflection Whether the method requires reflection to invoke - * @return this component definition - */ - @SuppressWarnings({"unused"}) - @Internal - @UsedByGeneratedCode - protected final AbstractBeanDefinition addInjectionPoint( - Class declaringType, - String method, - @Nullable Argument[] arguments, - @Nullable AnnotationMetadata annotationMetadata, - boolean requiresReflection) { - - return addInjectionPointInternal( - declaringType, - method, - arguments, - annotationMetadata, - requiresReflection, - this.methodInjectionPoints - ); - } - - /** - * Adds a post construct method definition. - * - * @param declaringType The declaring type - * @param method The method - * @param arguments The arguments - * @param annotationMetadata The annotation metadata - * @param requiresReflection Whether the method requires reflection - * @return This bean definition - */ - @SuppressWarnings("unused") - @Internal - @UsedByGeneratedCode - protected final AbstractBeanDefinition addPostConstruct(Class declaringType, - String method, - @Nullable Argument[] arguments, - @Nullable AnnotationMetadata annotationMetadata, - boolean requiresReflection) { - if (postConstructMethods == null) { - postConstructMethods = new ArrayList<>(1); - } - return addInjectionPointInternal(declaringType, method, arguments, annotationMetadata, requiresReflection, this.postConstructMethods); - } - - /** - * Adds a pre destroy method definition. - * - * @param declaringType The declaring type - * @param method The method - * @param arguments The arguments - * @param annotationMetadata The annotation metadata - * @param requiresReflection Whether the method requires reflection - * @return This bean definition - */ - @SuppressWarnings("unused") - @Internal - @UsedByGeneratedCode - protected final AbstractBeanDefinition addPreDestroy(Class declaringType, - String method, - Argument[] arguments, - AnnotationMetadata annotationMetadata, - boolean requiresReflection) { - if (preDestroyMethods == null) { - preDestroyMethods = new ArrayList<>(1); - } - return addInjectionPointInternal(declaringType, method, arguments, annotationMetadata, requiresReflection, this.preDestroyMethods); - } - - /** - * The default implementation which provides no injection. To be overridden by compile time tooling. - * - * @param resolutionContext The resolution context - * @param context The bean context - * @param bean The bean - * @return The injected bean - */ - @Internal - @SuppressWarnings({"WeakerAccess", "unused"}) - @UsedByGeneratedCode - protected Object injectBean(BeanResolutionContext resolutionContext, BeanContext context, Object bean) { - return bean; - } - - /** - * Inject another bean, for example one created via factory. - * - * @param resolutionContext The reslution context - * @param context The context - * @param bean The bean - * @return The bean - */ - @Internal - @SuppressWarnings({"unused"}) - @UsedByGeneratedCode - protected Object injectAnother(BeanResolutionContext resolutionContext, BeanContext context, Object bean) { - if (bean == null) { - throw new BeanInstantiationException(resolutionContext, "Bean factory returned null"); - } - DefaultBeanContext defaultContext = (DefaultBeanContext) context; - return defaultContext.inject(resolutionContext, this, bean); - } - - /** - * Default postConstruct hook that only invokes methods that require reflection. Generated subclasses should - * override to call methods that don't require reflection. - * - * @param resolutionContext The resolution hook - * @param context The context - * @param bean The bean - * @return The bean - */ - @SuppressWarnings({"unused", "unchecked"}) - @Internal - @UsedByGeneratedCode - protected Object postConstruct(BeanResolutionContext resolutionContext, BeanContext context, Object bean) { - boolean addInCreationHandling = isSingleton() && !CollectionUtils.isNotEmpty(postConstructMethods); - DefaultBeanContext.BeanKey key = null; - if (addInCreationHandling) { - // ensure registration as an inflight bean if a post construct is present - // this is to ensure that if the post construct method does anything funky to - // cause recreation of this bean then we don't have a circular problem - key = new DefaultBeanContext.BeanKey(this, resolutionContext.getCurrentQualifier()); - resolutionContext.addInFlightBean(key, new BeanRegistration(key, this, bean)); - } - - final Set, List>> beanInitializedEventListeners - = ((DefaultBeanContext) context).beanInitializedEventListeners; - if (CollectionUtils.isNotEmpty(beanInitializedEventListeners)) { - for (Map.Entry, List> entry : beanInitializedEventListeners) { - if (entry.getKey().isAssignableFrom(getBeanType())) { - for (BeanInitializedEventListener listener : entry.getValue()) { - bean = listener.onInitialized(new BeanInitializingEvent(context, this, bean)); - if (bean == null) { - throw new BeanInstantiationException(resolutionContext, "Listener [" + listener + "] returned null from onInitialized event"); - } - } - } - } - } - - DefaultBeanContext defaultContext = (DefaultBeanContext) context; - for (int i = 0; i < methodInjectionPoints.size(); i++) { - MethodInjectionPoint methodInjectionPoint = methodInjectionPoints.get(i); - if (methodInjectionPoint.isPostConstructMethod() && methodInjectionPoint.requiresReflection()) { - injectBeanMethod(resolutionContext, defaultContext, i, bean); - } - } - if (bean instanceof LifeCycle) { - bean = ((LifeCycle) bean).start(); - } - try { - return bean; - } finally { - if (addInCreationHandling) { - // ensure registration as an inflight bean if a post construct is present - // this is to ensure that if the post construct method does anything funky to - // cause recreation of this bean then we don't have a circular problem - resolutionContext.removeInFlightBean(key); - } - } - } - - /** - * Default preDestroy hook that only invokes methods that require reflection. Generated subclasses should override - * to call methods that don't require reflection. - * - * @param resolutionContext The resolution hook - * @param context The context - * @param bean The bean - * @return The bean - */ - @UsedByGeneratedCode - protected Object preDestroy(BeanResolutionContext resolutionContext, BeanContext context, Object bean) { - DefaultBeanContext defaultContext = (DefaultBeanContext) context; - for (int i = 0; i < methodInjectionPoints.size(); i++) { - MethodInjectionPoint methodInjectionPoint = methodInjectionPoints.get(i); - if (methodInjectionPoint.isPreDestroyMethod() && methodInjectionPoint.requiresReflection()) { - injectBeanMethod(resolutionContext, defaultContext, i, bean); - } - } - - if (bean instanceof LifeCycle) { - bean = ((LifeCycle) bean).stop(); - } - return bean; - } - - /** - * Inject a bean method that requires reflection. - * - * @param resolutionContext The resolution context - * @param context The bean context - * @param methodIndex The method index - * @param bean The bean - */ - @Internal - @SuppressWarnings("WeakerAccess") - protected void injectBeanMethod(BeanResolutionContext resolutionContext, DefaultBeanContext context, int methodIndex, Object bean) { - MethodInjectionPoint methodInjectionPoint = methodInjectionPoints.get(methodIndex); - Argument[] methodArgumentTypes = methodInjectionPoint.getArguments(); - Object[] methodArgs = new Object[methodArgumentTypes.length]; - for (int i = 0; i < methodArgumentTypes.length; i++) { - methodArgs[i] = getBeanForMethodArgument(resolutionContext, context, methodIndex, i); - } - try { - methodInjectionPoint.invoke(bean, methodArgs); - } catch (Throwable e) { - throw new BeanInstantiationException(this, e); - } - } - - /** - * Injects the value of a field of a bean that requires reflection. - * - * @param resolutionContext The resolution context - * @param context The bean context - * @param index The index of the field - * @param bean The bean being injected - */ - @SuppressWarnings("unused") - @Internal - protected final void injectBeanField(BeanResolutionContext resolutionContext, DefaultBeanContext context, int index, Object bean) { - FieldInjectionPoint fieldInjectionPoint = fieldInjectionPoints.get(index); - boolean isInject = fieldInjectionPoint.getAnnotationMetadata().hasDeclaredAnnotation(AnnotationUtil.INJECT); - try { - Object value; - if (isInject) { - instrumentAnnotationMetadata(context, fieldInjectionPoint); - value = getBeanForField(resolutionContext, context, fieldInjectionPoint); - } else { - value = getValueForField(resolutionContext, context, index); - } - if (value != null) { - //noinspection unchecked - fieldInjectionPoint.set(bean, value); - } - } catch (Throwable e) { - if (e instanceof BeanContextException) { - throw (BeanContextException) e; - } else { - throw new DependencyInjectionException(resolutionContext, fieldInjectionPoint, "Error setting field value: " + e.getMessage(), e); - } - } - } - - /** - * Obtains a value for the given method argument. - * - * @param resolutionContext The resolution context - * @param context The bean context - * @param methodIndex The method index - * @param argIndex The argument index - * @return The value - */ - @SuppressWarnings({"unused", "unchecked"}) - @Internal - protected final Object getValueForMethodArgument(BeanResolutionContext resolutionContext, BeanContext context, int methodIndex, int argIndex) { - MethodInjectionPoint injectionPoint = methodInjectionPoints.get(methodIndex); - Argument argument = injectionPoint.getArguments()[argIndex]; - BeanResolutionContext.Path path = resolutionContext.getPath(); - path.pushMethodArgumentResolve(this, injectionPoint, argument); - if (context instanceof ApplicationContext) { - // can't use orElseThrow here due to compiler bug - try { - String valueAnnStr = argument.getAnnotationMetadata().stringValue(Value.class).orElse(null); - - Argument argumentType; - boolean isCollection = false; - if (Collection.class.isAssignableFrom(argument.getType())) { - argumentType = argument.getFirstTypeVariable().orElse(Argument.OBJECT_ARGUMENT); - isCollection = true; - } else { - argumentType = argument; - } - - if (isInnerConfiguration(argumentType, context)) { - Qualifier qualifier = resolveQualifier(resolutionContext, argument, true); - if (isCollection) { - Collection beans = ((DefaultBeanContext) context).getBeansOfType(resolutionContext, argumentType, qualifier); - return coerceCollectionToCorrectType(argument.getType(), beans); - } else { - return ((DefaultBeanContext) context).getBean(resolutionContext, argumentType, qualifier); - } - } else { - String valString = resolvePropertyValueName(resolutionContext, injectionPoint.getAnnotationMetadata(), argument, valueAnnStr); - - ApplicationContext applicationContext = (ApplicationContext) context; - ArgumentConversionContext conversionContext = ConversionContext.of(argument); - Optional value = resolveValue(applicationContext, conversionContext, valueAnnStr != null, valString); - if (argumentType.isOptional()) { - return resolveOptionalObject(value); - } else { - if (value.isPresent()) { - return value.get(); - } else { - if (argument.isDeclaredNullable()) { - return null; - } - throw new DependencyInjectionException(resolutionContext, injectionPoint, conversionContext, valString); - } - } - } - } finally { - path.pop(); - } - } else { - path.pop(); - throw new DependencyInjectionException(resolutionContext, argument, "BeanContext must support property resolution"); - } - } - - - /** - * Obtains a value for the given method argument. - * - * @param resolutionContext The resolution context - * @param context The bean context - * @param methodIndex The method index - * @param argIndex The argument index - * @return The value - */ - @Internal - @UsedByGeneratedCode - protected final boolean containsValueForMethodArgument(BeanResolutionContext resolutionContext, BeanContext context, int methodIndex, int argIndex) { - if (context instanceof ApplicationContext) { - MethodInjectionPoint injectionPoint = methodInjectionPoints.get(methodIndex); - Argument argument = injectionPoint.getArguments()[argIndex]; - String valueAnnStr = argument.getAnnotationMetadata().stringValue(Value.class).orElse(null); - String valString = resolvePropertyValueName(resolutionContext, injectionPoint.getAnnotationMetadata(), argument, valueAnnStr); - ApplicationContext applicationContext = (ApplicationContext) context; - Class type = argument.getType(); - boolean isConfigProps = type.isAnnotationPresent(ConfigurationProperties.class); - boolean result = isConfigProps || Map.class.isAssignableFrom(type) || Collection.class.isAssignableFrom(type) ? applicationContext.containsProperties(valString) : applicationContext.containsProperty(valString); - if (!result && isConfigurationProperties()) { - String cliOption = resolveCliOption(argument.getName()); - if (cliOption != null) { - result = applicationContext.containsProperty(cliOption); - } - } - if (result && injectionPoint instanceof MissingMethodInjectionPoint) { - if (LOG.isWarnEnabled()) { - LOG.warn("Bean definition for type [{}] is compiled against an older version and value [{}] can no longer be set for missing method: {}", - getBeanType(), - valString, - injectionPoint.getName()); - } - result = false; - } - return result; - } - return false; - } - - /** - * Obtains a bean definition for the method at the given index and the argument at the given index - *

- * Warning: this method is used by internal generated code and should not be called by user code. - * - * @param resolutionContext The resolution context - * @param context The context - * @param methodIndex The method index - * @param argIndex The argument index - * @return The resolved bean - */ - @Internal - @SuppressWarnings("WeakerAccess") - @UsedByGeneratedCode - protected final Object getBeanForMethodArgument(BeanResolutionContext resolutionContext, BeanContext context, int methodIndex, int argIndex) { - MethodInjectionPoint injectionPoint = methodInjectionPoints.get(methodIndex); - Argument argument = resolveArgument(context, argIndex, injectionPoint.getArguments()); - return getBeanForMethodArgument(resolutionContext, context, injectionPoint, argument); - } - - /** - * Obtains all bean definitions for the method at the given index and the argument at the given index - *

- * Warning: this method is used by internal generated code and should not be called by user code. - * - * @param resolutionContext The resolution context - * @param context The context - * @param injectionPoint The method injection point - * @param argument The argument - * @return The resolved bean - */ - @SuppressWarnings("WeakerAccess") - @Internal - protected final Collection getBeansOfTypeForMethodArgument(BeanResolutionContext resolutionContext, BeanContext context, MethodInjectionPoint injectionPoint, Argument argument) { - return resolveBeanWithGenericsFromMethodArgument(resolutionContext, injectionPoint, argument, (beanType, qualifier) -> { - boolean hasNoGenerics = !argument.getType().isArray() && argument.getTypeVariables().isEmpty(); - if (hasNoGenerics) { - return ((DefaultBeanContext) context).getBean( - resolutionContext, - beanType, - qualifier - ); - } else { - return ((DefaultBeanContext) context).getBeansOfType( - resolutionContext, - beanType, - qualifier - ); - } - - } - ); - } - - /** - * Obtains an optional bean for the method at the given index and the argument at the given index - *

- * Warning: this method is used by internal generated code and should not be called by user code. - * - * @param resolutionContext The resolution context - * @param context The context - * @param injectionPoint The method injection point - * @param argument The argument - * @return The resolved bean - */ - @SuppressWarnings("WeakerAccess") - @Internal - protected final Optional findBeanForMethodArgument(BeanResolutionContext resolutionContext, BeanContext context, MethodInjectionPoint injectionPoint, Argument argument) { - return resolveBeanWithGenericsFromMethodArgument(resolutionContext, injectionPoint, argument, (beanType, qualifier) -> - ((DefaultBeanContext) context).findBean(resolutionContext, beanType, qualifier) - ); - } - - /** - * Obtains all bean definitions for the method at the given index and the argument at the given index - *

- * Warning: this method is used by internal generated code and should not be called by user code. - * - * @param resolutionContext The resolution context - * @param context The context - * @param injectionPoint The method injection point - * @param argument The argument - * @return The resolved bean - */ - @SuppressWarnings("WeakerAccess") - @Internal - protected final Stream streamOfTypeForMethodArgument(BeanResolutionContext resolutionContext, BeanContext context, MethodInjectionPoint injectionPoint, Argument argument) { - return resolveBeanWithGenericsFromMethodArgument(resolutionContext, injectionPoint, argument, (beanType, qualifier) -> - ((DefaultBeanContext) context).streamOfType(resolutionContext, beanType, qualifier) - ); - } - - /** - * Obtains a bean definition for a constructor at the given index - *

- * Warning: this method is used by internal generated code and should not be called by user code. - * - * @param resolutionContext The resolution context - * @param context The context - * @param argIndex The argument index - * @return The resolved bean - */ - @SuppressWarnings("unused") - @Internal - @UsedByGeneratedCode - protected final Object getBeanForConstructorArgument(BeanResolutionContext resolutionContext, BeanContext context, int argIndex) { - ConstructorInjectionPoint constructorInjectionPoint = getConstructor(); - Argument argument = getArgument(context, constructorInjectionPoint.getArguments(), argIndex); - final Class beanType = argument.getType(); - if (beanType == BeanResolutionContext.class) { - return resolutionContext; - } else if (argument.isArray()) { - Collection beansOfType = getBeansOfTypeForConstructorArgument(resolutionContext, context, constructorInjectionPoint, argument); - if (beansOfType != null) { - return beansOfType.toArray((Object[]) Array.newInstance(beanType.getComponentType(), beansOfType.size())); - } else { - return Array.newInstance(beanType.getComponentType(), 0); - } - } else if (Collection.class.isAssignableFrom(beanType)) { - Collection beansOfType = getBeansOfTypeForConstructorArgument(resolutionContext, context, constructorInjectionPoint, argument); - return coerceCollectionToCorrectType(beanType, beansOfType); - } else if (Stream.class.isAssignableFrom(beanType)) { - return streamOfTypeForConstructorArgument(resolutionContext, context, constructorInjectionPoint, argument); - } else if (argument.isOptional()) { - return findBeanForConstructorArgument(resolutionContext, context, constructorInjectionPoint, argument); - } else { - BeanResolutionContext.Path path = resolutionContext.getPath(); - BeanResolutionContext.Segment current = path.peek(); - boolean isNullable = argument.isDeclaredNullable(); - if (isNullable && current != null && current.getArgument().equals(argument)) { - return null; - } else { - path.pushConstructorResolve(this, argument); - try { - Object bean; - Qualifier qualifier = resolveQualifier(resolutionContext, argument, isInnerConfiguration(argument, context)); - if (Qualifier.class.isAssignableFrom(beanType)) { - bean = qualifier; - } else { - Object previous = !argument.isAnnotationPresent(Parameter.class) ? resolutionContext.removeAttribute(NAMED_ATTRIBUTE) : null; - try { - //noinspection unchecked - bean = ((DefaultBeanContext) context).getBean(resolutionContext, argument, qualifier); - } finally { - if (previous != null) { - resolutionContext.setAttribute(NAMED_ATTRIBUTE, previous); - } - } - } - path.pop(); - return bean; - } catch (DisabledBeanException e) { - if (ConditionLog.LOG.isDebugEnabled()) { - ConditionLog.LOG.debug("Bean of type [{}] disabled for reason: {}", argument.getTypeName(), e.getMessage()); - } - if (isIterable() && getAnnotationMetadata().hasDeclaredAnnotation(EachBean.class)) { - throw new DisabledBeanException("Bean [" + getBeanType().getSimpleName() + "] disabled by parent: " + e.getMessage()); - } else { - if (isNullable) { - path.pop(); - return null; - } - throw new DependencyInjectionException(resolutionContext, argument, e); - } - } catch (NoSuchBeanException e) { - if (isNullable) { - path.pop(); - return null; - } - throw new DependencyInjectionException(resolutionContext, argument, e); - } - } - } - } - - private Argument getArgument(BeanContext context, Argument[] arguments, int argIndex) { - Argument argument = resolveArgument(context, argIndex, arguments); - return argument; - } - - /** - * Obtains a value for a bean definition for a constructor at the given index - *

- * Warning: this method is used by internal generated code and should not be called by user code. - * - * @param resolutionContext The resolution context - * @param context The context - * @param argIndex The argument index - * @return The resolved bean - */ - @SuppressWarnings("unused") - @Internal - @UsedByGeneratedCode - protected final Object getValueForConstructorArgument(BeanResolutionContext resolutionContext, BeanContext context, int argIndex) { - ConstructorInjectionPoint constructorInjectionPoint = getConstructor(); - BeanResolutionContext.Path path = resolutionContext.getPath(); - Argument argument = constructorInjectionPoint.getArguments()[argIndex]; - path.pushConstructorResolve(this, argument); - try { - Object result; - if (context instanceof ApplicationContext) { - ApplicationContext propertyResolver = (ApplicationContext) context; - AnnotationMetadata argMetadata = argument.getAnnotationMetadata(); - Optional valAnn = argMetadata.stringValue(Value.class); - String prop = resolvePropertyValueName(resolutionContext, argMetadata, argument, valAnn.orElse(null)); - ArgumentConversionContext conversionContext = ConversionContext.of(argument); - Optional value = resolveValue(propertyResolver, conversionContext, valAnn.isPresent(), prop); - if (argument.getType() == Optional.class) { - return resolveOptionalObject(value); - } else { - // can't use orElseThrow here due to compiler bug - if (value.isPresent()) { - result = value.get(); - } else { - if (argument.isDeclaredNullable()) { - result = null; - } else { - result = argMetadata.getValue(Bindable.class, "defaultValue", argument) - .orElseThrow(() -> new DependencyInjectionException(resolutionContext, conversionContext, prop)); - } - } - } - } else { - throw new DependencyInjectionException(resolutionContext, argument, "BeanContext must support property resolution"); - } - - if (this instanceof ValidatedBeanDefinition) { - ((ValidatedBeanDefinition) this).validateBeanArgument( - resolutionContext, - constructorInjectionPoint, - argument, - argIndex, - result - ); - } - - return result; - } catch (NoSuchBeanException | BeanInstantiationException e) { - throw new DependencyInjectionException(resolutionContext, argument, e); - } finally { - path.pop(); - } - } - - /** - * Obtains all bean definitions for a constructor argument at the given index. - * - * @param resolutionContext The resolution context - * @param context The context - * @param constructorInjectionPoint The constructor injection point - * @param argument The argument - * @return The resolved bean - */ - @SuppressWarnings("WeakerAccess") - @Internal - protected final Collection getBeansOfTypeForConstructorArgument(BeanResolutionContext resolutionContext, BeanContext context, @SuppressWarnings("unused") ConstructorInjectionPoint constructorInjectionPoint, Argument argument) { - return resolveBeanWithGenericsFromConstructorArgument(resolutionContext, argument, (beanType, qualifier) -> { - boolean hasNoGenerics = !argument.getType().isArray() && argument.getTypeVariables().isEmpty(); - if (hasNoGenerics) { - return ((DefaultBeanContext) context).getBean(resolutionContext, beanType, qualifier); - } else { - return ((DefaultBeanContext) context).getBeansOfType(resolutionContext, beanType, qualifier); - } - } - ); - } - - /** - * Obtains all bean definitions for a constructor argument at the given index. - * - * @param resolutionContext The resolution context - * @param context The context - * @param argumentIndex The argument index - * @return The resolved bean - */ - @SuppressWarnings("WeakerAccess") - @Internal - @UsedByGeneratedCode - protected final Object getBeansOfTypeForConstructorArgument(BeanResolutionContext resolutionContext, BeanContext context, int argumentIndex) { - final ConstructorInjectionPoint constructorInjectionPoint = getConstructor(); - final Argument argument = getArgument(context, constructorInjectionPoint.getArguments(), argumentIndex); - final Class argumentType = argument.getType(); - Argument genericType = resolveGenericType(argument, () -> - new DependencyInjectionException(resolutionContext, argument, "Type " + argumentType + " has no generic argument") - ); - final Qualifier qualifier = resolveQualifier(resolutionContext, argument); - final BeanResolutionContext.Path path = resolutionContext.getPath(); - path.pushConstructorResolve(this, argument); - return doGetBeansOfType(resolutionContext, (DefaultBeanContext) context, argumentType, genericType, qualifier, path); - } - - /** - * Obtains all bean definitions for a constructor argument at the given index. - * - * @param resolutionContext The resolution context - * @param context The context - * @param methodIndex The method index - * @param argumentIndex The argument index - * @return The resolved bean - */ - @SuppressWarnings("WeakerAccess") - @Internal - @UsedByGeneratedCode - protected final Object getBeansOfTypeForMethodArgument(BeanResolutionContext resolutionContext, BeanContext context, int methodIndex, int argumentIndex) { - final MethodInjectionPoint methodInjectionPoint = methodInjectionPoints.get(methodIndex); - final Argument argument = getArgument(context, methodInjectionPoint.getArguments(), argumentIndex); - final Class argumentType = argument.getType(); - Argument genericType = resolveGenericType(argument, () -> - new DependencyInjectionException(resolutionContext, methodInjectionPoint, argument, "Type " + argumentType + " has no generic argument") - ); - final Qualifier qualifier = resolveQualifier(resolutionContext, argument); - final BeanResolutionContext.Path path = resolutionContext.getPath(); - path.pushMethodArgumentResolve(this, methodInjectionPoint, argument); - return doGetBeansOfType(resolutionContext, (DefaultBeanContext) context, argumentType, genericType, qualifier, path); - } - - /** - * Obtains all bean definitions for the field at the given index. - * - * @param resolutionContext The resolution context - * @param context The context - * @param fieldIndex The field index - * @return The resolved bean - */ - @SuppressWarnings("WeakerAccess") - @Internal - @UsedByGeneratedCode - protected final Object getBeansOfTypeForField(BeanResolutionContext resolutionContext, BeanContext context, int fieldIndex) { - final FieldInjectionPoint fieldInjectionPoint = fieldInjectionPoints.get(fieldIndex); - final Argument argument = fieldInjectionPoint.asArgument(); - final Class argumentType = argument.getType(); - Argument genericType = resolveGenericType(argument, () -> - new DependencyInjectionException(resolutionContext, fieldInjectionPoint, "Type " + argumentType + " has no generic argument")); - final Qualifier qualifier = resolveQualifier(resolutionContext, argument); - final BeanResolutionContext.Path path = resolutionContext.getPath(); - path.pushFieldResolve(this, fieldInjectionPoint); - return doGetBeansOfType(resolutionContext, (DefaultBeanContext) context, argumentType, genericType, qualifier, path); - } - - private Object doGetBeansOfType(BeanResolutionContext resolutionContext, DefaultBeanContext context, Class argumentType, Argument genericType, Qualifier qualifier, BeanResolutionContext.Path path) { - try { - final Collection beansOfType = context.getBeansOfType(resolutionContext, genericType, qualifier); - if (argumentType.isArray()) { - return beansOfType.toArray((Object[]) Array.newInstance(genericType.getType(), beansOfType.size())); - } else { - return coerceCollectionToCorrectType(argumentType, beansOfType); - } - } finally { - path.pop(); - } - } - - private Argument resolveGenericType(Argument argument, Supplier exceptionSupplier) { - Argument genericType; - if (argument.isArray()) { - genericType = Argument.of(argument.getType().getComponentType()); - } else { - - genericType = argument.getFirstTypeVariable() - .orElseThrow(exceptionSupplier); - } - return genericType; - } - - /** - * Obtains all bean definitions for a constructor argument at the given index - *

- * Warning: this method is used by internal generated code and should not be called by user code. - * - * @param resolutionContext The resolution context - * @param context The context - * @param argumentIndex The argument index - * @return The resolved bean - */ - @SuppressWarnings("WeakerAccess") - @Internal - @UsedByGeneratedCode - protected final Object getBeanRegistrationsForConstructorArgument( - BeanResolutionContext resolutionContext, - BeanContext context, - int argumentIndex) { - Argument argument = getArgument(context, getConstructor().getArguments(), argumentIndex); - BeanResolutionContext.Path path = resolutionContext.getPath(); - path.pushConstructorResolve(this, argument); - return doResolveBeanRegistrations(resolutionContext, (DefaultBeanContext) context, argument, path); - } - - /** - * Obtains a bean registration for a method injection point. - *

- * Warning: this method is used by internal generated code and should not be called by user code. - * - * @param resolutionContext The resolution context - * @param context The context - * @param argIndex The arg index - * @return The resolved bean registration - */ - @SuppressWarnings("WeakerAccess") - @Internal - @UsedByGeneratedCode - protected final BeanRegistration getBeanRegistrationForConstructorArgument( - BeanResolutionContext resolutionContext, - BeanContext context, - int argIndex) { - Argument argument = getArgument(context, getConstructor().getArguments(), argIndex); - BeanResolutionContext.Path path = resolutionContext.getPath(); - path.pushConstructorResolve(this, argument); - return resolveBeanRegistrationWithGenericsFromArgument(resolutionContext, argument, path, (beanType, qualifier) -> - ((DefaultBeanContext) context).getBeanRegistration(resolutionContext, beanType, qualifier) - ); - } - - /** - * Obtains all bean definitions for a field injection point. - *

- * Warning: this method is used by internal generated code and should not be called by user code. - * - * @param resolutionContext The resolution context - * @param context The context - * @param fieldIndex The field index - * @return The resolved bean - */ - @SuppressWarnings("WeakerAccess") - @Internal - @UsedByGeneratedCode - protected final Object getBeanRegistrationsForField( - BeanResolutionContext resolutionContext, - BeanContext context, - int fieldIndex) { - FieldInjectionPoint field = fieldInjectionPoints.get(fieldIndex); - instrumentAnnotationMetadata(context, field); - BeanResolutionContext.Path path = resolutionContext.getPath(); - path.pushFieldResolve(this, field); - return doResolveBeanRegistrations(resolutionContext, (DefaultBeanContext) context, field.asArgument(), path); - } - - /** - * Obtains a bean registration for a field injection point. - *

- * Warning: this method is used by internal generated code and should not be called by user code. - * - * @param resolutionContext The resolution context - * @param context The context - * @param fieldIndex The field index - * @return The resolved bean registration - */ - @SuppressWarnings("WeakerAccess") - @Internal - @UsedByGeneratedCode - protected final BeanRegistration getBeanRegistrationForField( - BeanResolutionContext resolutionContext, - BeanContext context, - int fieldIndex) { - FieldInjectionPoint field = fieldInjectionPoints.get(fieldIndex); - instrumentAnnotationMetadata(context, field); - BeanResolutionContext.Path path = resolutionContext.getPath(); - path.pushFieldResolve(this, field); - return resolveBeanRegistrationWithGenericsFromArgument(resolutionContext, field.asArgument(), path, (beanType, qualifier) -> - ((DefaultBeanContext) context).getBeanRegistration(resolutionContext, beanType, qualifier) - ); - } - - /** - * Obtains all bean definitions for a method injection point. - *

- * Warning: this method is used by internal generated code and should not be called by user code. - * - * @param resolutionContext The resolution context - * @param context The context - * @param methodIndex The method index - * @param argIndex The arg index - * @return The resolved bean - */ - @SuppressWarnings("WeakerAccess") - @Internal - @UsedByGeneratedCode - protected final Object getBeanRegistrationsForMethodArgument( - BeanResolutionContext resolutionContext, - BeanContext context, - int methodIndex, - int argIndex) { - MethodInjectionPoint methodInjectionPoint = methodInjectionPoints.get(methodIndex); - Argument argument = resolveArgument(context, argIndex, methodInjectionPoint.getArguments()); - BeanResolutionContext.Path path = resolutionContext.getPath(); - path.pushMethodArgumentResolve(this, methodInjectionPoint, argument); - return doResolveBeanRegistrations(resolutionContext, (DefaultBeanContext) context, argument, path); - } - - /** - * Obtains a bean registration for a method injection point. - *

- * Warning: this method is used by internal generated code and should not be called by user code. - * - * @param resolutionContext The resolution context - * @param context The context - * @param methodIndex The method index - * @param argIndex The arg index - * @return The resolved bean registration - */ - @SuppressWarnings("WeakerAccess") - @Internal - @UsedByGeneratedCode - protected final BeanRegistration getBeanRegistrationForMethodArgument( - BeanResolutionContext resolutionContext, - BeanContext context, - int methodIndex, - int argIndex) { - MethodInjectionPoint methodInjectionPoint = methodInjectionPoints.get(methodIndex); - Argument argument = resolveArgument(context, argIndex, methodInjectionPoint.getArguments()); - BeanResolutionContext.Path path = resolutionContext.getPath(); - path.pushMethodArgumentResolve(this, methodInjectionPoint, argument); - return resolveBeanRegistrationWithGenericsFromArgument(resolutionContext, argument, path, (beanType, qualifier) -> - ((DefaultBeanContext) context).getBeanRegistration(resolutionContext, beanType, qualifier) - ); - } - - /** - * Obtains all bean definitions for a constructor argument at the given index - *

- * Warning: this method is used by internal generated code and should not be called by user code. - * - * @param resolutionContext The resolution context - * @param context The context - * @param constructorInjectionPoint The constructor injection point - * @param argument The argument - * @return The resolved bean - */ - @SuppressWarnings("WeakerAccess") - @Internal - @UsedByGeneratedCode - protected final Stream streamOfTypeForConstructorArgument(BeanResolutionContext resolutionContext, BeanContext context, @SuppressWarnings("unused") ConstructorInjectionPoint constructorInjectionPoint, Argument argument) { - return resolveBeanWithGenericsFromConstructorArgument(resolutionContext, argument, (beanType, qualifier) -> - ((DefaultBeanContext) context).streamOfType(resolutionContext, beanType, qualifier) - ); - } - - /** - * Obtains all bean definitions for a constructor argument at the given index - *

- * Warning: this method is used by internal generated code and should not be called by user code. - * - * @param resolutionContext The resolution context - * @param context The context - * @param constructorInjectionPoint The constructor injection point - * @param argument The argument - * @return The resolved bean - */ - @SuppressWarnings("WeakerAccess") - @Internal - protected final Optional findBeanForConstructorArgument(BeanResolutionContext resolutionContext, BeanContext context, @SuppressWarnings("unused") ConstructorInjectionPoint constructorInjectionPoint, Argument argument) { - return resolveBeanWithGenericsFromConstructorArgument(resolutionContext, argument, (beanType, qualifier) -> - ((DefaultBeanContext) context).findBean(resolutionContext, beanType, qualifier) - ); - } - - /** - * Obtains a bean definition for the field at the given index and the argument at the given index - *

- * Warning: this method is used by internal generated code and should not be called by user code. - * - * @param resolutionContext The resolution context - * @param context The context - * @param fieldIndex The field index - * @return The resolved bean - */ - @SuppressWarnings("unused") - @Internal - @UsedByGeneratedCode - protected final Object getBeanForField(BeanResolutionContext resolutionContext, BeanContext context, int fieldIndex) { - FieldInjectionPoint injectionPoint = fieldInjectionPoints.get(fieldIndex); - instrumentAnnotationMetadata(context, injectionPoint); - return getBeanForField(resolutionContext, context, injectionPoint); - } - - /** - * Obtains a value for the given field from the bean context - *

- * Warning: this method is used by internal generated code and should not be called by user code. - * - * @param resolutionContext The resolution context - * @param context The context - * @param fieldIndex The index of the field - * @return The resolved bean - */ - @SuppressWarnings("WeakerAccess") - @Internal - @UsedByGeneratedCode - protected final Object getValueForField(BeanResolutionContext resolutionContext, BeanContext context, int fieldIndex) { - FieldInjectionPoint injectionPoint = fieldInjectionPoints.get(fieldIndex); - BeanResolutionContext.Path path = resolutionContext.getPath(); - path.pushFieldResolve(this, injectionPoint); - try { - if (context instanceof PropertyResolver) { - final AnnotationMetadata annotationMetadata = injectionPoint.getAnnotationMetadata(); - String valueAnnVal = annotationMetadata.stringValue(Value.class).orElse(null); - Argument fieldArgument = injectionPoint.asArgument(); - - Argument argumentType; - boolean isCollection = false; - if (Collection.class.isAssignableFrom(injectionPoint.getType())) { - argumentType = fieldArgument.getFirstTypeVariable().orElse(Argument.OBJECT_ARGUMENT); - isCollection = true; - } else { - argumentType = fieldArgument; - } - if (isInnerConfiguration(argumentType, context)) { - Qualifier qualifier = resolveQualifier(resolutionContext, fieldArgument, true); - if (isCollection) { - Collection beans = ((DefaultBeanContext) context).getBeansOfType(resolutionContext, argumentType, qualifier); - return coerceCollectionToCorrectType(fieldArgument.getType(), beans); - } else { - return ((DefaultBeanContext) context).getBean(resolutionContext, argumentType, qualifier); - } - } else { - String valString = resolvePropertyValueName(resolutionContext, injectionPoint, valueAnnVal, annotationMetadata); - ArgumentConversionContext conversionContext = ConversionContext.of(fieldArgument); - Optional value = resolveValue((ApplicationContext) context, conversionContext, valueAnnVal != null, valString); - if (argumentType.isOptional()) { - return resolveOptionalObject(value); - } else { - if (value.isPresent()) { - return value.get(); - } else { - if (fieldArgument.isDeclaredNullable()) { - return null; - } - throw new DependencyInjectionException(resolutionContext, injectionPoint, "Error resolving field value [" + valString + "]. Property doesn't exist or cannot be converted"); - } - } - } - } else { - throw new DependencyInjectionException(resolutionContext, injectionPoint, "@Value requires a BeanContext that implements PropertyResolver"); - } - } finally { - path.pop(); - } - } - - /** - * Resolve a value for the given field of the given type and path. Only - * used by applications compiled with versions of Micronaut prior to 1.2.0. - * - * @param resolutionContext The resolution context - * @param context The bean context - * @param propertyType The required property type - * @param propertyPath The property path - * @param The generic type - * @return An optional value - */ - @SuppressWarnings("unused") - @Internal - @UsedByGeneratedCode - protected final Optional getValueForPath( - BeanResolutionContext resolutionContext, - BeanContext context, - Argument propertyType, - String... propertyPath) { - if (context instanceof PropertyResolver) { - PropertyResolver propertyResolver = (PropertyResolver) context; - String pathString = propertyPath.length > 1 ? String.join(".", propertyPath) : propertyPath[0]; - String valString = resolvePropertyPath(resolutionContext, pathString); - - return propertyResolver.getProperty(valString, ConversionContext.of(propertyType)); - } - return Optional.empty(); - } - - /** - * Resolve a value for the given field of the given type and path. - * - * @param resolutionContext The resolution context - * @param context The bean context - * @param propertyType The required property type - * @param propertyPath The property path - * @param The generic type - * @return An optional value - */ - @SuppressWarnings("unused") - @Internal - @UsedByGeneratedCode - protected final Optional getValueForPath( - BeanResolutionContext resolutionContext, - BeanContext context, - Argument propertyType, - String propertyPath) { - if (context instanceof PropertyResolver) { - PropertyResolver propertyResolver = (PropertyResolver) context; - String valString = substituteWildCards(resolutionContext, propertyPath); - - return propertyResolver.getProperty(valString, ConversionContext.of(propertyType)); - } - return Optional.empty(); - } - - /** - * Obtains a value for the given field argument. - * - * @param resolutionContext The resolution context - * @param context The bean context - * @param fieldIndex The field index - * @return True if it does - */ - @Internal - @UsedByGeneratedCode - protected final boolean containsValueForField(BeanResolutionContext resolutionContext, BeanContext context, int fieldIndex) { - if (context instanceof ApplicationContext) { - FieldInjectionPoint injectionPoint = fieldInjectionPoints.get(fieldIndex); - final AnnotationMetadata annotationMetadata = injectionPoint.getAnnotationMetadata(); - String valueAnnVal = annotationMetadata.stringValue(Value.class).orElse(null); - String valString = resolvePropertyValueName(resolutionContext, injectionPoint, valueAnnVal, annotationMetadata); - ApplicationContext applicationContext = (ApplicationContext) context; - Class fieldType = injectionPoint.getType(); - boolean isConfigProps = fieldType.isAnnotationPresent(ConfigurationProperties.class); - boolean result = isConfigProps || Map.class.isAssignableFrom(fieldType) || Collection.class.isAssignableFrom(fieldType) ? applicationContext.containsProperties(valString) : applicationContext.containsProperty(valString); - if (!result && isConfigurationProperties()) { - String cliOption = resolveCliOption(injectionPoint.getName()); - if (cliOption != null) { - return applicationContext.containsProperty(cliOption); - } - } - return result; - } - return false; - } - - /** - * If this bean is a {@link ConfigurationProperties} bean return whether any properties for it are configured - * within the context. - * - * @param resolutionContext the resolution context - * @param context The context - * @return True if it does - */ - @SuppressWarnings("unused") - @Internal - @UsedByGeneratedCode - protected final boolean containsProperties(BeanResolutionContext resolutionContext, BeanContext context) { - return containsProperties(resolutionContext, context, null); - } - - /** - * If this bean is a {@link ConfigurationProperties} bean return whether any properties for it are configured - * within the context. - * - * @param resolutionContext the resolution context - * @param context The context - * @param subProperty The subproperty to check - * @return True if it does - */ - @SuppressWarnings({"WeakerAccess", "SameParameterValue"}) - @Internal - @UsedByGeneratedCode - protected final boolean containsProperties(@SuppressWarnings("unused") BeanResolutionContext resolutionContext, BeanContext context, String subProperty) { - boolean isSubProperty = StringUtils.isNotEmpty(subProperty); - if (!isSubProperty && !requiredComponents.isEmpty()) { - // if the bean requires dependency injection we disable this optimization - return true; - } - if (isConfigurationProperties && context instanceof ApplicationContext) { - AnnotationMetadata annotationMetadata = getAnnotationMetadata(); - ApplicationContext appCtx = (ApplicationContext) context; - if (annotationMetadata.getValue(ConfigurationProperties.class, "cliPrefix").isPresent()) { - return true; - } else { - String path = getConfigurationPropertiesPath(resolutionContext); - return appCtx.containsProperties(path); - } - - } - return false; - } - - /** - * Resolves a bean for the given {@link FieldInjectionPoint}. - * - * @param resolutionContext The {@link BeanResolutionContext} - * @param context The {@link BeanContext} - * @param injectionPoint The {@link FieldInjectionPoint} - * @return The resolved bean - * @throws DependencyInjectionException If the bean cannot be resolved - */ - @SuppressWarnings("WeakerAccess") - @Internal - @UsedByGeneratedCode - protected final Object getBeanForField(BeanResolutionContext resolutionContext, BeanContext context, FieldInjectionPoint injectionPoint) { - final Class beanClass = injectionPoint.getType(); - if (beanClass.isArray()) { - Collection beansOfType = getBeansOfTypeForField(resolutionContext, context, injectionPoint); - if (beansOfType != null) { - return beansOfType.toArray((Object[]) Array.newInstance(beanClass.getComponentType(), beansOfType.size())); - } else { - return Array.newInstance(beanClass.getComponentType(), 0); - } - } else if (Collection.class.isAssignableFrom(beanClass)) { - Collection beansOfType = getBeansOfTypeForField(resolutionContext, context, injectionPoint); - if (beanClass.isInstance(beansOfType)) { - return beansOfType; - } else { - //noinspection unchecked - return CollectionUtils.convertCollection(beanClass, beansOfType).orElse(null); - } - } else if (Stream.class.isAssignableFrom(beanClass)) { - return getStreamOfTypeForField(resolutionContext, context, injectionPoint); - } else if (Optional.class.isAssignableFrom(beanClass)) { - return findBeanForField(resolutionContext, context, injectionPoint); - } else { - BeanResolutionContext.Path path = resolutionContext.getPath(); - path.pushFieldResolve(this, injectionPoint); - - final Argument argument = injectionPoint.asArgument(); - try { - Qualifier qualifier = resolveQualifier(resolutionContext, argument); - @SuppressWarnings("unchecked") Object bean = ((DefaultBeanContext) context).getBean(resolutionContext, argument, qualifier); - path.pop(); - return bean; - } catch (DisabledBeanException e) { - if (ConditionLog.LOG.isDebugEnabled()) { - ConditionLog.LOG.debug("Bean of type [{}] disabled for reason: {}", argument.getTypeName(), e.getMessage()); - } - if (isIterable() && getAnnotationMetadata().hasDeclaredAnnotation(EachBean.class)) { - throw new DisabledBeanException("Bean [" + getBeanType().getSimpleName() + "] disabled by parent: " + e.getMessage()); - } else { - if (injectionPoint.isDeclaredNullable()) { - path.pop(); - return null; - } - throw new DependencyInjectionException(resolutionContext, injectionPoint, e); - } - } catch (NoSuchBeanException e) { - if (injectionPoint.isDeclaredNullable()) { - path.pop(); - return null; - } - throw new DependencyInjectionException(resolutionContext, injectionPoint, e); - } - } - } - - /** - * Obtains a an optional for the field at the given index and the argument at the given index - *

- * Warning: this method is used by internal generated code and should not be called by user code. - * - * @param resolutionContext The resolution context - * @param context The context - * @param injectionPoint The field injection point - * @return The resolved bean - */ - @SuppressWarnings("WeakerAccess") - @Internal - protected final Optional findBeanForField(BeanResolutionContext resolutionContext, BeanContext context, FieldInjectionPoint injectionPoint) { - return resolveBeanWithGenericsForField(resolutionContext, injectionPoint, (beanType, qualifier) -> - ((DefaultBeanContext) context).findBean(resolutionContext, beanType, qualifier) - ); - } - - /** - * Obtains a bean definition for the field at the given index and the argument at the given index - *

- * Warning: this method is used by internal generated code and should not be called by user code. - * - * @param resolutionContext The resolution context - * @param context The context - * @param injectionPoint The field injection point - * @return The resolved bean - */ - @SuppressWarnings("WeakerAccess") - @Internal - protected final Collection getBeansOfTypeForField(BeanResolutionContext resolutionContext, BeanContext context, FieldInjectionPoint injectionPoint) { - return resolveBeanWithGenericsForField(resolutionContext, injectionPoint, (beanType, qualifier) -> { - boolean hasNoGenerics = !injectionPoint.getType().isArray() && injectionPoint.asArgument().getTypeVariables().isEmpty(); - if (hasNoGenerics) { - return ((DefaultBeanContext) context).getBean(resolutionContext, beanType, qualifier); - } else { - return ((DefaultBeanContext) context).getBeansOfType(resolutionContext, beanType, qualifier); - } - - } - ); - } - - /** - * Obtains a bean definition for the field at the given index and the argument at the given index - *

- * Warning: this method is used by internal generated code and should not be called by user code. - * - * @param resolutionContext The resolution context - * @param context The context - * @param injectionPoint The field injection point - * @return The resolved bean - */ - @SuppressWarnings("WeakerAccess") - @Internal - @UsedByGeneratedCode - protected final Stream getStreamOfTypeForField(BeanResolutionContext resolutionContext, BeanContext context, FieldInjectionPoint injectionPoint) { - return resolveBeanWithGenericsForField(resolutionContext, injectionPoint, (beanType, qualifier) -> - ((DefaultBeanContext) context).streamOfType(resolutionContext, beanType, qualifier) - ); - } - - /** - * A method that subclasses can override to provide information on type arguments. - * - * @return The type arguments - */ - @Internal - @UsedByGeneratedCode - protected Map[]> getTypeArgumentsMap() { - return Collections.emptyMap(); - } - - /** - * Resolves the annotation metadata for this bean. Subclasses - * - * @return The {@link AnnotationMetadata} - */ - protected AnnotationMetadata resolveAnnotationMetadata() { - return AnnotationMetadata.EMPTY_METADATA; - } - - private AnnotationMetadata initializeAnnotationMetadata() { - AnnotationMetadata annotationMetadata = resolveAnnotationMetadata(); - if (annotationMetadata != AnnotationMetadata.EMPTY_METADATA) { - if (annotationMetadata.hasPropertyExpressions()) { - // we make a copy of the result of annotation metadata which is normally a reference - // to the class metadata - return new BeanAnnotationMetadata(annotationMetadata); - } else { - return annotationMetadata; - } - } else { - return AnnotationMetadata.EMPTY_METADATA; - } - } - - private AbstractBeanDefinition addInjectionPointInternal( - Class declaringType, - String method, - @Nullable Argument[] arguments, - @Nullable AnnotationMetadata annotationMetadata, - boolean requiresReflection, - List> targetInjectionPoints) { - boolean isPreDestroy = targetInjectionPoints == this.preDestroyMethods; - boolean isPostConstruct = targetInjectionPoints == this.postConstructMethods; - - MethodInjectionPoint injectionPoint; - if (requiresReflection) { - injectionPoint = new ReflectionMethodInjectionPoint( - this, - declaringType, - method, - arguments, - annotationMetadata - ); - } else { - injectionPoint = new DefaultMethodInjectionPoint( - this, - declaringType, - method, - arguments, - annotationMetadata - ); - } - targetInjectionPoints.add(injectionPoint); - if (isPostConstruct || isPreDestroy) { - this.methodInjectionPoints.add(injectionPoint); - } - addRequiredComponents(arguments); - return this; - } - - private Object getBeanForMethodArgument(BeanResolutionContext resolutionContext, BeanContext context, MethodInjectionPoint injectionPoint, Argument argument) { - Class argumentType = argument.getType(); - if (argumentType.isArray()) { - Collection beansOfType = getBeansOfTypeForMethodArgument(resolutionContext, context, injectionPoint, argument); - return beansOfType.toArray((Object[]) Array.newInstance(argumentType.getComponentType(), beansOfType.size())); - } else if (Collection.class.isAssignableFrom(argumentType)) { - Collection beansOfType = getBeansOfTypeForMethodArgument(resolutionContext, context, injectionPoint, argument); - return coerceCollectionToCorrectType(argumentType, beansOfType); - } else if (Stream.class.isAssignableFrom(argumentType)) { - return streamOfTypeForMethodArgument(resolutionContext, context, injectionPoint, argument); - } else if (Optional.class.isAssignableFrom(argumentType)) { - return findBeanForMethodArgument(resolutionContext, context, injectionPoint, argument); - } else { - BeanResolutionContext.Path path = resolutionContext.getPath(); - path.pushMethodArgumentResolve(this, injectionPoint, argument); - try { - Qualifier qualifier = resolveQualifier(resolutionContext, argument); - @SuppressWarnings("unchecked") - Object bean = ((DefaultBeanContext) context).getBean(resolutionContext, argument, qualifier); - path.pop(); - return bean; - } catch (DisabledBeanException e) { - if (ConditionLog.LOG.isDebugEnabled()) { - ConditionLog.LOG.debug("Bean of type [{}] disabled for reason: {}", argumentType.getSimpleName(), e.getMessage()); - } - if (isIterable() && getAnnotationMetadata().hasDeclaredAnnotation(EachBean.class)) { - throw new DisabledBeanException("Bean [" + getBeanType().getSimpleName() + "] disabled by parent: " + e.getMessage()); - } else { - if (argument.isDeclaredNullable()) { - path.pop(); - return null; - } - throw new DependencyInjectionException(resolutionContext, argument, e); - } - } catch (NoSuchBeanException e) { - if (argument.isDeclaredNullable()) { - path.pop(); - return null; - } - throw new DependencyInjectionException(resolutionContext, argument, e); - } - } - } - - private Optional resolveValue( - ApplicationContext context, - ArgumentConversionContext argument, - boolean hasValueAnnotation, - String valString) { - - if (hasValueAnnotation) { - - return context.resolvePlaceholders(valString).flatMap(v -> - context.getConversionService().convert(v, argument) - ); - } else { - Optional value = context.getProperty(valString, argument); - if (!value.isPresent() && isConfigurationProperties()) { - String cliOption = resolveCliOption(argument.getArgument().getName()); - if (cliOption != null) { - return context.getProperty(cliOption, argument); - } - } - return value; - } - } - - private String resolvePropertyValueName( - BeanResolutionContext resolutionContext, - AnnotationMetadata annotationMetadata, - Argument argument, - String valueAnnStr) { - String valString; - if (valueAnnStr != null) { - valString = valueAnnStr; - } else { - valString = annotationMetadata.stringValue(Property.class, "name") - .orElseGet(() -> - argument.getAnnotationMetadata().stringValue(Property.class, "name") - .orElseThrow(() -> - new DependencyInjectionException( - resolutionContext, - argument, - "Value resolution attempted but @Value annotation is missing" - ) - ) - ); - - valString = substituteWildCards(resolutionContext, valString); - } - return valString; - } - - private String resolvePropertyValueName( - BeanResolutionContext resolutionContext, - FieldInjectionPoint injectionPoint, - String valueAnn, - AnnotationMetadata annotationMetadata) { - String valString; - if (valueAnn != null) { - valString = valueAnn; - } else { - valString = annotationMetadata.stringValue(Property.class, "name") - .orElseThrow(() -> new DependencyInjectionException(resolutionContext, injectionPoint, "Value resolution attempted but @Value annotation is missing")); - - valString = substituteWildCards(resolutionContext, valString); - } - return valString; - } - - private String resolvePropertyPath( - BeanResolutionContext resolutionContext, - String path) { - - String valString = getConfigurationPropertiesPath(resolutionContext); - return valString + "." + path; - } - - private String getConfigurationPropertiesPath(BeanResolutionContext resolutionContext) { - String valString = getAnnotationMetadata() - .stringValue(ConfigurationReader.class, "prefix") - .orElseThrow(() -> new IllegalStateException("Resolve property path called for non @ConfigurationProperties bean")); - valString = substituteWildCards( - resolutionContext, - valString - ); - return valString; - } - - private String substituteWildCards(BeanResolutionContext resolutionContext, String valString) { - ConfigurationPath configurationPath = resolutionContext.getConfigurationPath(); - if (configurationPath.isNotEmpty()) { - return configurationPath.resolveValue(valString); - } - return valString; - } - - private String resolveCliOption(String name) { - String attr = "cliPrefix"; - AnnotationMetadata annotationMetadata = getAnnotationMetadata(); - if (annotationMetadata.isPresent(ConfigurationProperties.class, attr)) { - return annotationMetadata.stringValue(ConfigurationProperties.class, attr).map(val -> val + name).orElse(null); - } - return null; - } - - private boolean isInnerConfiguration(Argument argumentType, BeanContext beanContext) { - final Class type = argumentType.getType(); - return isConfigurationProperties && - type.getName().indexOf('$') > -1 && - !type.isEnum() && - !type.isPrimitive() && - Modifier.isPublic(type.getModifiers()) && Modifier.isStatic(type.getModifiers()) && - isInnerOfAnySuperclass(type) && - beanContext.findBeanDefinition(argumentType).map(bd -> bd.hasStereotype(ConfigurationReader.class) || bd.isIterable()).isPresent(); - } - - @SuppressWarnings("java:S1872") // internal requirement - private boolean isInnerOfAnySuperclass(Class argumentType) { - Class beanType = getBeanType(); - while (beanType != null) { - if ((beanType.getName() + "$" + argumentType.getSimpleName()).equals(argumentType.getName())) { - return true; - } - beanType = beanType.getSuperclass(); - } - return false; - } - - private B resolveBeanWithGenericsFromMethodArgument(BeanResolutionContext resolutionContext, MethodInjectionPoint injectionPoint, Argument argument, BeanResolver beanResolver) throws X { - BeanResolutionContext.Path path = resolutionContext.getPath(); - path.pushMethodArgumentResolve(this, injectionPoint, argument); - try { - Qualifier qualifier = resolveQualifier(resolutionContext, argument); - Class argumentType = argument.getType(); - Argument genericType = resolveGenericType(argument, argumentType); - @SuppressWarnings("unchecked") B bean = (B) beanResolver.resolveBean(genericType != null ? genericType : argument, qualifier); - path.pop(); - return bean; - } catch (NoSuchBeanException e) { - throw new DependencyInjectionException(resolutionContext, injectionPoint, argument, e); - } - } - - private Argument resolveGenericType(Argument argument, Class argumentType) { - Argument genericType; - if (argument.isArray()) { - genericType = Argument.of(argumentType.getComponentType()); - } else { - return argument.getFirstTypeVariable() - .orElse(null); - } - return genericType; - } - - private B resolveBeanWithGenericsFromConstructorArgument(BeanResolutionContext resolutionContext, Argument argument, BeanResolver beanResolver) { - BeanResolutionContext.Path path = resolutionContext.getPath(); - path.pushConstructorResolve(this, argument); - try { - Class argumentType = argument.getType(); - Argument genericType = resolveGenericType(argument, argumentType); - Qualifier qualifier = resolveQualifier(resolutionContext, argument); - @SuppressWarnings("unchecked") B bean = (B) beanResolver.resolveBean(genericType != null ? genericType : argument, qualifier); - path.pop(); - return bean; - } catch (NoSuchBeanException e) { - if (argument.isNullable()) { - path.pop(); - return null; - } - throw new DependencyInjectionException(resolutionContext, argument, e); - } - } - - private Collection> resolveBeanRegistrationsWithGenericsFromArgument( - BeanResolutionContext resolutionContext, - Argument argument, - BeanResolutionContext.Path path, - BiFunction, Qualifier, Collection>> beanResolver) { - try { - final Supplier errorSupplier = () -> - new DependencyInjectionException(resolutionContext, argument, "Cannot resolve bean registrations. Argument [" + argument + "] missing generic type information."); - Argument genericType = argument.getFirstTypeVariable().orElseThrow(errorSupplier); - Argument beanType = argument.isArray() ? genericType : genericType.getFirstTypeVariable().orElseThrow(errorSupplier); - Qualifier qualifier = resolveQualifier(resolutionContext, argument); - final Collection result = beanResolver.apply(beanType, qualifier); - path.pop(); - return result; - } catch (NoSuchBeanException e) { - if (argument.isNullable()) { - path.pop(); - return null; - } - throw new DependencyInjectionException(resolutionContext, argument, e); - } - } - - private Argument resolveArgument(BeanContext context, int argIndex, Argument[] arguments) { - Argument argument = arguments[argIndex]; - if (argument instanceof DefaultArgument) { - if (argument.getAnnotationMetadata().hasPropertyExpressions()) { - argument = new EnvironmentAwareArgument<>((DefaultArgument) argument); - instrumentAnnotationMetadata(context, argument); - } - } - return argument; - } - - private BeanRegistration resolveBeanRegistrationWithGenericsFromArgument( - BeanResolutionContext resolutionContext, - Argument argument, - BeanResolutionContext.Path path, - BiFunction, Qualifier, BeanRegistration> beanResolver) { - try { - final Supplier errorSupplier = () -> - new DependencyInjectionException(resolutionContext, argument, "Cannot resolve bean registration. Argument [" + argument + "] missing generic type information."); - Argument genericType = argument.getFirstTypeVariable().orElseThrow(errorSupplier); - Qualifier qualifier = resolveQualifier(resolutionContext, argument); - final BeanRegistration result = beanResolver.apply(genericType, qualifier); - path.pop(); - return result; - } catch (NoSuchBeanException e) { - if (argument.isNullable()) { - path.pop(); - return null; - } - throw new DependencyInjectionException(resolutionContext, argument, e); - } - } - - @SuppressWarnings("java:S2259") // false positive - private Object doResolveBeanRegistrations(BeanResolutionContext resolutionContext, DefaultBeanContext context, Argument argument, BeanResolutionContext.Path path) { - final Collection> beanRegistrations = resolveBeanRegistrationsWithGenericsFromArgument(resolutionContext, argument, path, - (beanType, qualifier) -> context.getBeanRegistrations(resolutionContext, beanType, qualifier) - ); - if (CollectionUtils.isNotEmpty(beanRegistrations)) { - if (argument.isArray()) { - return beanRegistrations.toArray(new BeanRegistration[beanRegistrations.size()]); - } else { - return coerceCollectionToCorrectType(argument.getType(), beanRegistrations); - } - } else { - if (argument.isArray()) { - return Array.newInstance(argument.getType(), 0); - } else { - return coerceCollectionToCorrectType(argument.getType(), Collections.emptySet()); - } - } - } - - private B resolveBeanWithGenericsForField(BeanResolutionContext resolutionContext, FieldInjectionPoint injectionPoint, BeanResolver beanResolver) { - BeanResolutionContext.Path path = resolutionContext.getPath(); - path.pushFieldResolve(this, injectionPoint); - Argument argument = injectionPoint.asArgument(); - try { - Argument genericType = argument.isArray() ? Argument.of(argument.getType().getComponentType()) : argument.getFirstTypeVariable().orElse(argument); - Qualifier qualifier = resolveQualifier(resolutionContext, argument); - @SuppressWarnings("unchecked") B bean = (B) beanResolver.resolveBean(genericType, qualifier); - path.pop(); - return bean; - } catch (NoSuchBeanException e) { - if (argument.isNullable()) { - path.pop(); - return null; - } - throw new DependencyInjectionException(resolutionContext, injectionPoint, e); - } - } - - @Override - public boolean isConfigurationProperties() { - return isConfigurationProperties; - } - - private Qualifier resolveQualifier(BeanResolutionContext resolutionContext, Argument argument) { - return resolveQualifier(resolutionContext, argument, false); - } - - private Qualifier resolveQualifier( - BeanResolutionContext resolutionContext, - Argument argument, - boolean innerConfiguration) { - final Qualifier argumentQualifier = Qualifiers.forArgument(argument); - if (argumentQualifier != null) { - return argumentQualifier; - } else { - final AnnotationMetadata annotationMetadata = argument.getAnnotationMetadata(); - boolean hasMetadata = annotationMetadata != AnnotationMetadata.EMPTY_METADATA; - if (hasMetadata && annotationMetadata.hasAnnotation(AnnotationUtil.ANN_INTERCEPTOR_BINDING_QUALIFIER)) { - return Qualifiers.byInterceptorBinding(annotationMetadata); - } - Class[] byType = hasMetadata ? annotationMetadata.hasDeclaredAnnotation(Type.class) ? annotationMetadata.classValues(Type.class) : null : null; - if (byType != null) { - return Qualifiers.byType(byType); - } else { - Qualifier qualifier = null; - boolean isIterable = isIterable() || resolutionContext.get(EachProperty.class.getName(), Class.class).map(getBeanType()::equals).orElse(false); - if (isIterable) { - Optional optional = resolutionContext.get(AnnotationUtil.QUALIFIER, Map.class) - .map(map -> (Qualifier) map.get(argument)); - qualifier = optional.orElse(null); - } - if (qualifier == null) { - if ((hasMetadata && argument.isAnnotationPresent(Parameter.class)) || - (innerConfiguration && isIterable) || - Qualifier.class == argument.getType()) { - final Qualifier currentQualifier = resolutionContext.getCurrentQualifier(); - if (currentQualifier != null && - currentQualifier.getClass() != InterceptorBindingQualifier.class && - currentQualifier.getClass() != TypeAnnotationQualifier.class) { - qualifier = currentQualifier; - } else { - final Optional n = resolutionContext.get(NAMED_ATTRIBUTE, ConversionContext.STRING); - qualifier = n.map(Qualifiers::byName).orElse(null); - } - } - } - return qualifier; - } - } - - } - - @SuppressWarnings("OptionalUsedAsFieldOrParameterType") - private Object resolveOptionalObject(Optional value) { - if (!value.isPresent()) { - return value; - } else { - Object convertedOptional = value.get(); - if (convertedOptional instanceof Optional) { - return convertedOptional; - } else { - return value; - } - } - } - - @SuppressWarnings("unchecked") - private Object coerceCollectionToCorrectType(Class collectionType, Collection beansOfType) { - if (collectionType.isInstance(beansOfType)) { - return beansOfType; - } else { - return CollectionUtils.convertCollection(collectionType, beansOfType).orElse(null); - } - } - - private void addRequiredComponents(Argument... arguments) { - if (arguments != null) { - for (Argument argument : arguments) { - if (argument.isContainerType() || argument.isProvider()) { - argument.getFirstTypeVariable() - .map(Argument::getType) - .ifPresent(requiredComponents::add); - } else { - requiredComponents.add(argument.getType()); - } - } - } - } - - private void instrumentAnnotationMetadata(BeanContext context, Object object) { - if (object instanceof EnvironmentConfigurable && context instanceof ApplicationContext) { - final EnvironmentConfigurable ec = (EnvironmentConfigurable) object; - if (ec.hasPropertyExpressions()) { - ec.configure(((ApplicationContext) context).getEnvironment()); - } - } - } - - /** - * Internal environment aware annotation metadata delegate. - */ - private final class BeanAnnotationMetadata extends AbstractEnvironmentAnnotationMetadata { - BeanAnnotationMetadata(AnnotationMetadata targetMetadata) { - super(targetMetadata); - } - - @Nullable - @Override - protected Environment getEnvironment() { - return environment; - } - } - - /** - * Class used as a method key. - */ - private final class MethodKey { - final String name; - final Class[] argumentTypes; - - MethodKey(String name, Class[] argumentTypes) { - this.name = name; - this.argumentTypes = argumentTypes; - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - - @SuppressWarnings("unchecked") MethodKey methodKey = (MethodKey) o; - - if (!name.equals(methodKey.name)) { - return false; - } - return Arrays.equals(argumentTypes, methodKey.argumentTypes); - } - - @Override - public int hashCode() { - int result = name.hashCode(); - result = 31 * result + Arrays.hashCode(argumentTypes); - return result; - } - } - - /** - * Bean resolver. - * - * @param The type - */ - private interface BeanResolver { - T resolveBean(Argument beanType, Qualifier qualifier); - } -} diff --git a/inject/src/main/java/io/micronaut/context/AbstractConstructorInjectionPoint.java b/inject/src/main/java/io/micronaut/context/AbstractBeanDefinitionBeanConstructor.java similarity index 51% rename from inject/src/main/java/io/micronaut/context/AbstractConstructorInjectionPoint.java rename to inject/src/main/java/io/micronaut/context/AbstractBeanDefinitionBeanConstructor.java index 922f70c5824..1096ca642cb 100644 --- a/inject/src/main/java/io/micronaut/context/AbstractConstructorInjectionPoint.java +++ b/inject/src/main/java/io/micronaut/context/AbstractBeanDefinitionBeanConstructor.java @@ -15,49 +15,36 @@ */ package io.micronaut.context; -import io.micronaut.core.annotation.NonNull; import io.micronaut.core.annotation.UsedByGeneratedCode; import io.micronaut.core.beans.AbstractBeanConstructor; import io.micronaut.inject.BeanDefinition; -import io.micronaut.inject.ConstructorInjectionPoint; import io.micronaut.inject.annotation.AnnotationMetadataHierarchy; import java.util.Objects; /** * Abstract constructor implementation for bean definitions to implement to create constructors at build time. + * * @param The bean type * @author graemerocher * @since 3.0.0 */ @UsedByGeneratedCode -public abstract class AbstractConstructorInjectionPoint extends AbstractBeanConstructor implements ConstructorInjectionPoint, EnvironmentConfigurable { - private final BeanDefinition beanDefinition; +public abstract class AbstractBeanDefinitionBeanConstructor extends AbstractBeanConstructor { /** * Default constructor. * - * @param beanDefinition The bean type + * @param beanDefinition The bean type */ - protected AbstractConstructorInjectionPoint(BeanDefinition beanDefinition) { + protected AbstractBeanDefinitionBeanConstructor(BeanDefinition beanDefinition) { super( - Objects.requireNonNull(beanDefinition, "Bean definition cannot be null").getBeanType(), - new AnnotationMetadataHierarchy( - beanDefinition.getAnnotationMetadata(), - beanDefinition.getConstructor().getAnnotationMetadata()), - beanDefinition.getConstructor().getArguments() + Objects.requireNonNull(beanDefinition, "Bean definition cannot be null").getBeanType(), + new AnnotationMetadataHierarchy( + beanDefinition.getAnnotationMetadata(), + beanDefinition.getConstructor().getAnnotationMetadata()), + beanDefinition.getConstructor().getArguments() ); - this.beanDefinition = Objects.requireNonNull(beanDefinition, "Bean definition is required"); - } - - @Override - @NonNull - public final BeanDefinition getDeclaringBean() { - return beanDefinition; } - @Override - public final boolean requiresReflection() { - return false; - } } diff --git a/inject/src/main/java/io/micronaut/context/AbstractBeanDefinitionReference.java b/inject/src/main/java/io/micronaut/context/AbstractBeanDefinitionReference.java deleted file mode 100644 index 788d22207c6..00000000000 --- a/inject/src/main/java/io/micronaut/context/AbstractBeanDefinitionReference.java +++ /dev/null @@ -1,148 +0,0 @@ -/* - * Copyright 2017-2020 original authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.micronaut.context; - -import io.micronaut.context.annotation.Context; -import io.micronaut.context.annotation.Primary; -import io.micronaut.context.exceptions.BeanContextException; -import io.micronaut.core.annotation.Internal; -import io.micronaut.core.annotation.NonNull; -import io.micronaut.inject.BeanDefinition; -import io.micronaut.inject.BeanDefinitionReference; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.util.Set; - -/** - * An uninitialized and unloaded component definition with basic information available regarding its requirements. - * - * @author Graeme Rocher - * @since 1.0 - */ -@Internal -public abstract class AbstractBeanDefinitionReference extends AbstractBeanContextConditional implements BeanDefinitionReference { - - private static final Logger LOG = LoggerFactory.getLogger(AbstractBeanDefinitionReference.class); - private final String beanTypeName; - private final String beanDefinitionTypeName; - private Boolean present; - private Set> exposedTypes; - - /** - * @param beanTypeName The bean type name - * @param beanDefinitionTypeName The bean definition type name - */ - public AbstractBeanDefinitionReference(String beanTypeName, String beanDefinitionTypeName) { - this.beanTypeName = beanTypeName; - this.beanDefinitionTypeName = beanDefinitionTypeName; - } - - @Override - public boolean isPrimary() { - return getAnnotationMetadata().hasAnnotation(Primary.class); - } - - @Override - @NonNull - public final Set> getExposedTypes() { - if (exposedTypes == null) { - this.exposedTypes = BeanDefinitionReference.super.getExposedTypes(); - } - return this.exposedTypes; - } - - @Override - public String getName() { - return beanTypeName; - } - - @Override - public BeanDefinition load(BeanContext context) { - BeanDefinition definition = load(); - if (context instanceof ApplicationContext && definition instanceof EnvironmentConfigurable) { - ((EnvironmentConfigurable) definition).configure(((ApplicationContext) context).getEnvironment()); - } - return definition; - } - - @Override - public boolean isContextScope() { - return getAnnotationMetadata().hasDeclaredStereotype(Context.class); - } - - @Override - public String getBeanDefinitionName() { - return beanDefinitionTypeName; - } - - @Override - public boolean isPresent() { - if (present == null) { - try { - getBeanDefinitionType(); - getBeanType(); - present = true; - } catch (Throwable e) { - if (e instanceof TypeNotPresentException || e instanceof ClassNotFoundException || e instanceof NoClassDefFoundError) { - if (LOG.isTraceEnabled()) { - LOG.trace("Bean definition for type [" + beanTypeName + "] not loaded since it is not on the classpath", e); - } - } else { - throw new BeanContextException("Unexpected error loading bean definition [" + beanDefinitionTypeName + "]: " + e.getMessage(), e); - } - present = false; - } - } - return present; - } - - @Override - public boolean isEnabled(BeanContext beanContext) { - return isPresent() && super.isEnabled(beanContext); - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - - AbstractBeanDefinitionReference that = (AbstractBeanDefinitionReference) o; - - return beanDefinitionTypeName.equals(that.beanDefinitionTypeName); - } - - @Override - public String toString() { - return beanDefinitionTypeName; - } - - @Override - public int hashCode() { - return beanDefinitionTypeName.hashCode(); - } - - /** - * Implementors should provide an implementation of this method that returns the bean definition type. - * - * @return The bean definition type. - */ - protected abstract Class> getBeanDefinitionType(); -} diff --git a/inject/src/main/java/io/micronaut/context/AbstractBeanResolutionContext.java b/inject/src/main/java/io/micronaut/context/AbstractBeanResolutionContext.java index 8fa5f6099d5..3a68b7de0db 100644 --- a/inject/src/main/java/io/micronaut/context/AbstractBeanResolutionContext.java +++ b/inject/src/main/java/io/micronaut/context/AbstractBeanResolutionContext.java @@ -29,6 +29,7 @@ import io.micronaut.core.naming.Named; import io.micronaut.core.type.Argument; import io.micronaut.core.type.ArgumentCoercible; +import io.micronaut.core.util.ObjectUtils; import io.micronaut.inject.*; import java.util.*; @@ -110,12 +111,6 @@ public Optional findBean(@NonNull Argument beanType, @Nullable Qualifi return context.findBean(this, beanType, qualifier); } - @NonNull - @Override - public T inject(@Nullable BeanDefinition beanDefinition, @NonNull T instance) { - return context.inject(this, beanDefinition, instance); - } - @NonNull @Override public Collection> getBeanRegistrations(@NonNull Argument beanType, @Nullable Qualifier qualifier) { @@ -288,7 +283,7 @@ public Optional get(CharSequence name, Class requiredType) { return Optional.empty(); } - protected void onNewSegment(Segment segment) { + protected void onNewSegment(Segment segment) { //no-op } @@ -303,7 +298,7 @@ private Map getAttributesOrCreate() { /** * Class that represents a default path. */ - class DefaultPath extends LinkedList> implements Path { + class DefaultPath extends LinkedList> implements Path { public static final String RIGHT_ARROW = " --> "; private static final String CIRCULAR_ERROR_MSG = "Circular dependency detected"; @@ -313,7 +308,7 @@ class DefaultPath extends LinkedList> implements Path { @Override public String toString() { - Iterator> i = descendingIterator(); + Iterator> i = descendingIterator(); StringBuilder pathString = new StringBuilder(); while (i.hasNext()) { pathString.append(i.next().toString()); @@ -327,7 +322,7 @@ public String toString() { @SuppressWarnings("MagicNumber") @Override public String toCircularString() { - Iterator> i = descendingIterator(); + Iterator> i = descendingIterator(); StringBuilder pathString = new StringBuilder(); String ls = CachedEnvironment.getProperty("line.separator"); while (i.hasNext()) { @@ -356,28 +351,27 @@ public String toCircularString() { } @Override - public Optional> currentSegment() { + public Optional> currentSegment() { return Optional.ofNullable(peek()); } @Override public Path pushConstructorResolve(BeanDefinition declaringType, Argument argument) { - ConstructorInjectionPoint constructor = declaringType.getConstructor(); - if (constructor instanceof MethodInjectionPoint) { - MethodInjectionPoint methodInjectionPoint = (MethodInjectionPoint) constructor; - return pushConstructorResolve(declaringType, methodInjectionPoint.getName(), argument, constructor.getArguments(), constructor.requiresReflection()); + ConstructorInjectionPoint constructor = declaringType.getConstructor(); + if (constructor instanceof MethodInjectionPoint methodInjectionPoint) { + return pushConstructorResolve(declaringType, methodInjectionPoint.getName(), argument, constructor.getArguments()); } - return pushConstructorResolve(declaringType, "", argument, constructor.getArguments(), constructor.requiresReflection()); + return pushConstructorResolve(declaringType, "", argument, constructor.getArguments()); } @Override - public Path pushConstructorResolve(BeanDefinition declaringType, String methodName, Argument argument, Argument[] arguments, boolean requiresReflection) { + public Path pushConstructorResolve(BeanDefinition declaringType, String methodName, Argument argument, Argument[] arguments) { if ("".equals(methodName)) { ConstructorSegment constructorSegment = new ConstructorArgumentSegment(declaringType, methodName, argument, arguments); detectCircularDependency(declaringType, argument, constructorSegment); } else { - Segment previous = peek(); - MethodSegment methodSegment = new MethodArgumentSegment(declaringType, methodName, argument, arguments, requiresReflection, previous instanceof MethodSegment ? (MethodSegment) previous : null); + Segment previous = peek(); + MethodSegment methodSegment = new MethodArgumentSegment(declaringType, methodName, argument, arguments, previous instanceof MethodSegment ? (MethodSegment) previous : null); if (contains(methodSegment)) { throw new CircularDependencyException(AbstractBeanResolutionContext.this, argument, CIRCULAR_ERROR_MSG); } else { @@ -394,9 +388,9 @@ public Path pushBeanCreate(BeanDefinition declaringType, Argument beanType @Override public Path pushMethodArgumentResolve(BeanDefinition declaringType, MethodInjectionPoint methodInjectionPoint, Argument argument) { - Segment previous = peek(); - MethodSegment methodSegment = new MethodArgumentSegment(declaringType, methodInjectionPoint.getName(), argument, - methodInjectionPoint.getArguments(), methodInjectionPoint.requiresReflection(), previous instanceof MethodSegment ? (MethodSegment) previous : null); + Segment previous = peek(); + MethodSegment methodSegment = new MethodArgumentSegment(declaringType, methodInjectionPoint.getName(), argument, + methodInjectionPoint.getArguments(), previous instanceof MethodSegment ? (MethodSegment) previous : null); if (contains(methodSegment)) { throw new CircularDependencyException(AbstractBeanResolutionContext.this, methodInjectionPoint, argument, CIRCULAR_ERROR_MSG); } else { @@ -407,9 +401,9 @@ public Path pushMethodArgumentResolve(BeanDefinition declaringType, MethodInject } @Override - public Path pushMethodArgumentResolve(BeanDefinition declaringType, String methodName, Argument argument, Argument[] arguments, boolean requiresReflection) { - Segment previous = peek(); - MethodSegment methodSegment = new MethodArgumentSegment(declaringType, methodName, argument, arguments, requiresReflection, previous instanceof MethodSegment ? (MethodSegment) previous : null); + public Path pushMethodArgumentResolve(BeanDefinition declaringType, String methodName, Argument argument, Argument[] arguments) { + Segment previous = peek(); + MethodSegment methodSegment = new MethodArgumentSegment(declaringType, methodName, argument, arguments, previous instanceof MethodSegment ? (MethodSegment) previous : null); if (contains(methodSegment)) { throw new CircularDependencyException(AbstractBeanResolutionContext.this, declaringType, methodName, argument, CIRCULAR_ERROR_MSG); } else { @@ -421,7 +415,7 @@ public Path pushMethodArgumentResolve(BeanDefinition declaringType, String metho @Override public Path pushFieldResolve(BeanDefinition declaringType, FieldInjectionPoint fieldInjectionPoint) { - FieldSegment fieldSegment = new FieldSegment(declaringType, fieldInjectionPoint.asArgument(), fieldInjectionPoint.requiresReflection()); + FieldSegment fieldSegment = new FieldSegment<>(declaringType, fieldInjectionPoint.asArgument()); if (contains(fieldSegment)) { throw new CircularDependencyException(AbstractBeanResolutionContext.this, fieldInjectionPoint, CIRCULAR_ERROR_MSG); } else { @@ -431,8 +425,8 @@ public Path pushFieldResolve(BeanDefinition declaringType, FieldInjectionPoint f } @Override - public Path pushFieldResolve(BeanDefinition declaringType, Argument fieldAsArgument, boolean requiresReflection) { - FieldSegment fieldSegment = new FieldSegment(declaringType, fieldAsArgument, requiresReflection); + public Path pushFieldResolve(BeanDefinition declaringType, Argument fieldAsArgument) { + FieldSegment fieldSegment = new FieldSegment<>(declaringType, fieldAsArgument); if (contains(fieldSegment)) { throw new CircularDependencyException(AbstractBeanResolutionContext.this, declaringType, fieldAsArgument.getName(), CIRCULAR_ERROR_MSG); } else { @@ -461,16 +455,16 @@ private void detectCircularDependency(BeanDefinition declaringType, Argument arg // if the currently injected segment is a constructor argument and the type to be constructed is the // same as the candidate, then filter out the candidate to avoid a circular injection problem if (!declaringBean.equals(declaringType)) { - if (declaringType instanceof ProxyBeanDefinition) { + if (declaringType instanceof ProxyBeanDefinition proxyBeanDefinition) { // take into account proxies - if (!((ProxyBeanDefinition) declaringType).getTargetDefinitionType().equals(declaringBean.getClass())) { + if (!proxyBeanDefinition.getTargetDefinitionType().equals(declaringBean.getClass())) { throw new CircularDependencyException(AbstractBeanResolutionContext.this, argument, CIRCULAR_ERROR_MSG); } else { push(constructorSegment); } - } else if (declaringBean instanceof ProxyBeanDefinition) { + } else if (declaringBean instanceof ProxyBeanDefinition proxyBeanDefinition) { // take into account proxies - if (!((ProxyBeanDefinition) declaringBean).getTargetDefinitionType().equals(declaringType.getClass())) { + if (!proxyBeanDefinition.getTargetDefinitionType().equals(declaringType.getClass())) { throw new CircularDependencyException(AbstractBeanResolutionContext.this, argument, CIRCULAR_ERROR_MSG); } else { push(constructorSegment); @@ -490,7 +484,7 @@ private void detectCircularDependency(BeanDefinition declaringType, Argument arg } @Override - public void push(Segment segment) { + public void push(Segment segment) { super.push(segment); AbstractBeanResolutionContext.this.onNewSegment(segment); } @@ -514,10 +508,6 @@ public BeanDefinition getDeclaringBean() { return getDeclaringType(); } - @Override - public boolean requiresReflection() { - return false; - } } /** @@ -575,11 +565,6 @@ public BeanDefinition getDeclaringBean() { return constructorInjectionPoint.getDeclaringBean(); } - @Override - public boolean requiresReflection() { - return constructorInjectionPoint.requiresReflection(); - } - @Override public AnnotationMetadata getAnnotationMetadata() { return getArgument().getAnnotationMetadata(); @@ -595,8 +580,8 @@ public AnnotationMetadata getAnnotationMetadata() { public static class MethodArgumentSegment extends MethodSegment implements ArgumentInjectionPoint { private final MethodSegment outer; - public MethodArgumentSegment(BeanDefinition declaringType, String methodName, Argument argument, Argument[] arguments, boolean requiresReflection, MethodSegment outer) { - super(declaringType, methodName, argument, arguments, requiresReflection); + public MethodArgumentSegment(BeanDefinition declaringType, String methodName, Argument argument, Argument[] arguments, MethodSegment outer) { + super(declaringType, methodName, argument, arguments); this.outer = outer; } @@ -626,22 +611,19 @@ public String toString() { /** * A segment that represents a method. */ - public static class MethodSegment extends AbstractSegment implements CallableInjectionPoint { + public static class MethodSegment extends AbstractSegment implements CallableInjectionPoint { private final Argument[] arguments; - private final boolean requiresReflection; /** * @param declaringType The declaring type * @param methodName The method name * @param argument The argument * @param arguments The arguments - * @param requiresReflection Is requires reflection */ - MethodSegment(BeanDefinition declaringType, String methodName, Argument argument, Argument[] arguments, boolean requiresReflection) { + MethodSegment(BeanDefinition declaringType, String methodName, Argument argument, Argument[] arguments) { super(declaringType, methodName, argument); this.arguments = arguments; - this.requiresReflection = requiresReflection; } @Override @@ -653,20 +635,15 @@ public String toString() { } @Override - public InjectionPoint getInjectionPoint() { + public InjectionPoint getInjectionPoint() { return this; } @Override - public BeanDefinition getDeclaringBean() { + public BeanDefinition getDeclaringBean() { return getDeclaringType(); } - @Override - public boolean requiresReflection() { - return requiresReflection; - } - @Override public Argument[] getArguments() { return arguments; @@ -681,18 +658,14 @@ public AnnotationMetadata getAnnotationMetadata() { /** * A segment that represents a field. */ - public static class FieldSegment extends AbstractSegment implements InjectionPoint, ArgumentCoercible, ArgumentInjectionPoint { - - private final boolean requiresReflection; + public static class FieldSegment extends AbstractSegment implements InjectionPoint, ArgumentCoercible, ArgumentInjectionPoint { /** * @param declaringClass The declaring class * @param argument The argument - * @param requiresReflection Is requires reflection */ - FieldSegment(BeanDefinition declaringClass, Argument argument, boolean requiresReflection) { + FieldSegment(BeanDefinition declaringClass, Argument argument) { super(declaringClass, argument.getName(), argument); - this.requiresReflection = requiresReflection; } @Override @@ -701,27 +674,22 @@ public String toString() { } @Override - public InjectionPoint getInjectionPoint() { + public InjectionPoint getInjectionPoint() { return this; } @Override - public BeanDefinition getDeclaringBean() { + public BeanDefinition getDeclaringBean() { return getDeclaringType(); } @Override - public boolean requiresReflection() { - return requiresReflection; - } - - @Override - public CallableInjectionPoint getOuterInjectionPoint() { + public CallableInjectionPoint getOuterInjectionPoint() { throw new UnsupportedOperationException("Outer injection point not retrievable from here"); } @Override - public Argument asArgument() { + public Argument asArgument() { return getArgument(); } @@ -736,12 +704,12 @@ public AnnotationMetadata getAnnotationMetadata() { * * @since 3.3.0 */ - public static final class AnnotationSegment extends AbstractSegment implements InjectionPoint { + public static final class AnnotationSegment extends AbstractSegment implements InjectionPoint { /** * @param beanDefinition The bean definition * @param argument The argument */ - AnnotationSegment(BeanDefinition beanDefinition, Argument argument) { + AnnotationSegment(BeanDefinition beanDefinition, Argument argument) { super(beanDefinition, argument.getName(), argument); } @@ -751,35 +719,30 @@ public String toString() { } @Override - public InjectionPoint getInjectionPoint() { + public InjectionPoint getInjectionPoint() { return this; } @Override - public BeanDefinition getDeclaringBean() { + public BeanDefinition getDeclaringBean() { return getDeclaringType(); } - - @Override - public boolean requiresReflection() { - return false; - } } /** * Abstract class for a Segment. */ - abstract static class AbstractSegment implements Segment, Named { - private final BeanDefinition declaringComponent; + abstract static class AbstractSegment implements Segment, Named { + private final BeanDefinition declaringComponent; private final String name; - private final Argument argument; + private final Argument argument; /** * @param declaringClass The declaring class * @param name The name * @param argument The argument */ - AbstractSegment(BeanDefinition declaringClass, String name, Argument argument) { + AbstractSegment(BeanDefinition declaringClass, String name, Argument argument) { this.declaringComponent = declaringClass; this.name = name; this.argument = argument; @@ -791,12 +754,12 @@ public String getName() { } @Override - public BeanDefinition getDeclaringType() { + public BeanDefinition getDeclaringType() { return declaringComponent; } @Override - public Argument getArgument() { + public Argument getArgument() { return argument; } @@ -816,10 +779,7 @@ public boolean equals(Object o) { @Override public int hashCode() { - int result = declaringComponent.hashCode(); - result = 31 * result + name.hashCode(); - result = 31 * result + argument.hashCode(); - return result; + return ObjectUtils.hash(declaringComponent, name, argument); } /** @@ -829,7 +789,7 @@ public int hashCode() { void outputArguments(StringBuilder baseString, Argument[] arguments) { baseString.append('('); for (int i = 0; i < arguments.length; i++) { - Argument argument = arguments[i]; + Argument argument = arguments[i]; boolean isInjectedArgument = argument.equals(getArgument()); if (isInjectedArgument) { baseString.append('['); diff --git a/inject/src/main/java/io/micronaut/context/AbstractExecutable.java b/inject/src/main/java/io/micronaut/context/AbstractExecutable.java index 5d4e15b2202..e6c8d4ba9a7 100644 --- a/inject/src/main/java/io/micronaut/context/AbstractExecutable.java +++ b/inject/src/main/java/io/micronaut/context/AbstractExecutable.java @@ -23,6 +23,8 @@ import io.micronaut.core.util.ArrayUtils; import io.micronaut.core.annotation.NonNull; +import io.micronaut.core.util.ObjectUtils; + import java.lang.reflect.Method; import java.util.Arrays; import java.util.Objects; @@ -79,7 +81,7 @@ public boolean equals(Object o) { @Override public int hashCode() { - int result = Objects.hash(declaringType, methodName); + int result = ObjectUtils.hash(declaringType, methodName); result = 31 * result + Arrays.hashCode(argTypes); return result; } diff --git a/inject/src/main/java/io/micronaut/context/AbstractExecutableMethod.java b/inject/src/main/java/io/micronaut/context/AbstractExecutableMethod.java index ae259f09045..6f9f3464046 100644 --- a/inject/src/main/java/io/micronaut/context/AbstractExecutableMethod.java +++ b/inject/src/main/java/io/micronaut/context/AbstractExecutableMethod.java @@ -23,6 +23,7 @@ import io.micronaut.core.type.Argument; import io.micronaut.core.type.ReturnType; import io.micronaut.core.util.ArgumentUtils; +import io.micronaut.core.util.ObjectUtils; import io.micronaut.inject.ExecutableMethod; import io.micronaut.inject.annotation.AbstractEnvironmentAnnotationMetadata; @@ -64,7 +65,7 @@ protected AbstractExecutableMethod(Class declaringType, super(declaringType, methodName, arguments); this.genericReturnType = genericReturnType; this.returnType = new ReturnTypeImpl(); - int result = Objects.hash(declaringType, methodName); + int result = ObjectUtils.hash(declaringType, methodName); result = 31 * result + Arrays.hashCode(argTypes); this.hashCode = result; } diff --git a/inject/src/main/java/io/micronaut/context/AbstractExecutableMethodsDefinition.java b/inject/src/main/java/io/micronaut/context/AbstractExecutableMethodsDefinition.java index 70c2858dbb2..97b026c8727 100644 --- a/inject/src/main/java/io/micronaut/context/AbstractExecutableMethodsDefinition.java +++ b/inject/src/main/java/io/micronaut/context/AbstractExecutableMethodsDefinition.java @@ -21,6 +21,7 @@ import io.micronaut.core.type.Argument; import io.micronaut.core.type.ReturnType; import io.micronaut.core.util.ArgumentUtils; +import io.micronaut.core.util.ObjectUtils; import io.micronaut.inject.ExecutableMethod; import io.micronaut.inject.ExecutableMethodsDefinition; import io.micronaut.inject.annotation.AbstractEnvironmentAnnotationMetadata; @@ -394,7 +395,7 @@ public boolean equals(Object o) { @Override public int hashCode() { - return Objects.hash( + return ObjectUtils.hash( methodReference.declaringType, methodReference.methodName, Arrays.hashCode(methodReference.arguments) diff --git a/inject/src/main/java/io/micronaut/context/AbstractInitializableBeanDefinition.java b/inject/src/main/java/io/micronaut/context/AbstractInitializableBeanDefinition.java index bea7b8eb06e..4feb31d19b5 100644 --- a/inject/src/main/java/io/micronaut/context/AbstractInitializableBeanDefinition.java +++ b/inject/src/main/java/io/micronaut/context/AbstractInitializableBeanDefinition.java @@ -32,6 +32,7 @@ import io.micronaut.core.annotation.AnnotationMetadata; import io.micronaut.core.annotation.AnnotationUtil; import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.NextMajorVersion; import io.micronaut.core.annotation.NonNull; import io.micronaut.core.annotation.Nullable; import io.micronaut.core.annotation.UsedByGeneratedCode; @@ -51,6 +52,8 @@ import io.micronaut.inject.ExecutableMethod; import io.micronaut.inject.ExecutableMethodsDefinition; import io.micronaut.inject.FieldInjectionPoint; +import io.micronaut.inject.InjectableBeanDefinition; +import io.micronaut.inject.InstantiatableBeanDefinition; import io.micronaut.inject.MethodInjectionPoint; import io.micronaut.inject.ValidatedBeanDefinition; import io.micronaut.inject.annotation.AbstractEnvironmentAnnotationMetadata; @@ -97,19 +100,13 @@ * @since 3.0 */ @Internal -public class AbstractInitializableBeanDefinition extends AbstractBeanContextConditional implements BeanDefinition, EnvironmentConfigurable { +public abstract class AbstractInitializableBeanDefinition extends AbstractBeanContextConditional + implements InstantiatableBeanDefinition, InjectableBeanDefinition, EnvironmentConfigurable { private static final Logger LOG = LoggerFactory.getLogger(AbstractInitializableBeanDefinition.class); private final Class type; private final AnnotationMetadata annotationMetadata; - private final Optional scope; - private final boolean isIterable; - private final boolean isSingleton; - private final boolean isPrimary; - private final boolean isAbstract; - private final boolean isConfigurationProperties; - private final boolean isContainerType; - private final boolean requiresMethodProcessing; + private final PrecalculatedInfo precalculatedInfo; @Nullable private final MethodOrFieldReference constructor; @Nullable @@ -143,7 +140,6 @@ public class AbstractInitializableBeanDefinition extends AbstractBeanContextC private Qualifier declaredQualifier; - @SuppressWarnings("ParameterNumber") @Internal @UsedByGeneratedCode protected AbstractInitializableBeanDefinition( @@ -152,18 +148,10 @@ protected AbstractInitializableBeanDefinition( @Nullable AnnotationMetadata annotationMetadata, @Nullable MethodReference[] methodInjection, @Nullable FieldReference[] fieldInjection, + @Nullable AnnotationReference[] annotationInjection, @Nullable ExecutableMethodsDefinition executableMethodsDefinition, @Nullable Map[]> typeArgumentsMap, - Optional scope, - boolean isAbstract, - boolean isProvided, - boolean isIterable, - boolean isSingleton, - boolean isPrimary, - boolean isConfigurationProperties, - boolean isContainerType, - boolean requiresMethodProcessing) { - this.scope = scope; + @NonNull PrecalculatedInfo precalculatedInfo) { this.type = beanType; if (annotationMetadata == null || annotationMetadata == AnnotationMetadata.EMPTY_METADATA) { this.annotationMetadata = AnnotationMetadata.EMPTY_METADATA; @@ -176,23 +164,46 @@ protected AbstractInitializableBeanDefinition( this.annotationMetadata = annotationMetadata; } } - this.isIterable = isIterable; - this.isSingleton = isSingleton; - this.isPrimary = isPrimary; - this.isAbstract = isAbstract; this.constructor = constructor; this.methodInjection = methodInjection; this.fieldInjection = fieldInjection; + this.annotationInjection = annotationInjection; this.executableMethodsDefinition = executableMethodsDefinition; this.typeArgumentsMap = typeArgumentsMap; - this.isConfigurationProperties = isConfigurationProperties; - this.isContainerType = isContainerType; - this.requiresMethodProcessing = requiresMethodProcessing; + this.precalculatedInfo = precalculatedInfo; } @SuppressWarnings("ParameterNumber") @Internal @UsedByGeneratedCode + @Deprecated(since = "4") + @NextMajorVersion("Remove after Micronaut 4 Milestone 1") + protected AbstractInitializableBeanDefinition( + Class beanType, + @Nullable MethodOrFieldReference constructor, + @Nullable AnnotationMetadata annotationMetadata, + @Nullable MethodReference[] methodInjection, + @Nullable FieldReference[] fieldInjection, + @Nullable ExecutableMethodsDefinition executableMethodsDefinition, + @Nullable Map[]> typeArgumentsMap, + Optional scope, + boolean isAbstract, + boolean isProvided, + boolean isIterable, + boolean isSingleton, + boolean isPrimary, + boolean isConfigurationProperties, + boolean isContainerType, + boolean requiresMethodProcessing) { + this(beanType, constructor, annotationMetadata, methodInjection, fieldInjection, null, executableMethodsDefinition, typeArgumentsMap, + new PrecalculatedInfo(scope, isAbstract, isIterable, isSingleton, isPrimary, isConfigurationProperties, isContainerType, requiresMethodProcessing)); + } + + @SuppressWarnings("ParameterNumber") + @Internal + @UsedByGeneratedCode + @Deprecated(since = "4") + @NextMajorVersion("Remove after Micronaut 4 Milestone 1") protected AbstractInitializableBeanDefinition( Class beanType, @Nullable MethodOrFieldReference constructor, @@ -211,46 +222,32 @@ protected AbstractInitializableBeanDefinition( boolean isConfigurationProperties, boolean isContainerType, boolean requiresMethodProcessing) { - this(beanType, - constructor, - annotationMetadata, - methodInjection, - fieldInjection, - executableMethodsDefinition, - typeArgumentsMap, - scope, isAbstract, - isProvided, - isIterable, - isSingleton, - isPrimary, - isConfigurationProperties, - isContainerType, - requiresMethodProcessing); - this.annotationInjection = annotationInjection; + this(beanType, constructor, annotationMetadata, methodInjection, fieldInjection, annotationInjection, executableMethodsDefinition, typeArgumentsMap, + new PrecalculatedInfo(scope, isAbstract, isIterable, isSingleton, isPrimary, isConfigurationProperties, isContainerType, requiresMethodProcessing)); } @Override public final boolean isConfigurationProperties() { - return isConfigurationProperties; + return precalculatedInfo.isConfigurationProperties; } @Override public Qualifier getDeclaredQualifier() { if (declaredQualifier == null) { - declaredQualifier = BeanDefinition.super.getDeclaredQualifier(); + declaredQualifier = InstantiatableBeanDefinition.super.getDeclaredQualifier(); } return declaredQualifier; } @Override public final boolean isContainerType() { - return isContainerType; + return precalculatedInfo.isContainerType; } @Override @SuppressWarnings("java:S2789") // performance optimization public final Optional> getContainerElement() { - if (isContainerType) { + if (precalculatedInfo.isContainerType) { if (containerElement != null) { return containerElement; } @@ -293,22 +290,22 @@ public AnnotationMetadata getAnnotationMetadata() { @Override public boolean isAbstract() { - return isAbstract; + return precalculatedInfo.isAbstract; } @Override public boolean isIterable() { - return isIterable; + return precalculatedInfo.isIterable; } @Override public boolean isPrimary() { - return isPrimary; + return precalculatedInfo.isPrimary; } @Override public boolean requiresMethodProcessing() { - return requiresMethodProcessing; + return precalculatedInfo.requiresMethodProcessing; } @Override @@ -348,17 +345,17 @@ public String toString() { @Override public boolean isSingleton() { - return isSingleton; + return precalculatedInfo.isSingleton; } @Override public final Optional> getScope() { - return scope.flatMap(scopeClassName -> (Optional) ClassUtils.forName(scopeClassName, getClass().getClassLoader())); + return precalculatedInfo.scope.flatMap(scopeClassName -> (Optional) ClassUtils.forName(scopeClassName, getClass().getClassLoader())); } @Override public final Optional getScopeName() { - return scope; + return precalculatedInfo.scope; } @Override @@ -382,6 +379,9 @@ public final Optional> getDeclaringType() { @Override public final ConstructorInjectionPoint getConstructor() { + if (constructorInjectionPoint != null) { + return constructorInjectionPoint; + } if (constructor == null) { constructorInjectionPoint = new DefaultConstructorInjectionPoint<>( this, @@ -390,54 +390,34 @@ public final ConstructorInjectionPoint getConstructor() { Argument.ZERO_ARGUMENTS ); } else { - if (constructor instanceof MethodReference) { - MethodReference methodConstructor = (MethodReference) constructor; + if (constructor instanceof MethodReference methodConstructor) { if ("".equals(methodConstructor.methodName)) { - if (methodConstructor.requiresReflection) { - this.constructorInjectionPoint = new ReflectionConstructorInjectionPoint<>( - this, - methodConstructor.declaringType, - methodConstructor.annotationMetadata, - methodConstructor.arguments); - } else { - this.constructorInjectionPoint = new DefaultConstructorInjectionPoint<>( - this, - methodConstructor.declaringType, - methodConstructor.annotationMetadata, - methodConstructor.arguments - ); - } + this.constructorInjectionPoint = new DefaultConstructorInjectionPoint<>( + this, + methodConstructor.declaringType, + methodConstructor.annotationMetadata, + methodConstructor.arguments + ); } else { - if (methodConstructor.requiresReflection) { - this.constructorInjectionPoint = new ReflectionMethodConstructorInjectionPoint( - this, - methodConstructor.declaringType, - methodConstructor.methodName, - methodConstructor.arguments, - methodConstructor.annotationMetadata - ); - } else { - this.constructorInjectionPoint = new DefaultMethodConstructorInjectionPoint( - this, - methodConstructor.declaringType, - methodConstructor.methodName, - methodConstructor.arguments, - methodConstructor.annotationMetadata - ); - } + this.constructorInjectionPoint = new DefaultMethodConstructorInjectionPoint<>( + this, + methodConstructor.declaringType, + methodConstructor.methodName, + methodConstructor.arguments, + methodConstructor.annotationMetadata + ); } - } else if (constructor instanceof FieldReference) { - FieldReference fieldConstructor = (FieldReference) constructor; + } else if (constructor instanceof FieldReference fieldConstructor) { constructorInjectionPoint = new DefaultFieldConstructorInjectionPoint<>( - this, - fieldConstructor.declaringType, - type, - fieldConstructor.argument.getName(), - fieldConstructor.argument.getAnnotationMetadata() + this, + fieldConstructor.declaringType, + type, + fieldConstructor.argument.getName(), + fieldConstructor.argument.getAnnotationMetadata() ); } - if (environment != null && constructorInjectionPoint instanceof EnvironmentConfigurable) { - ((EnvironmentConfigurable) constructorInjectionPoint).configure(environment); + if (environment != null && constructorInjectionPoint instanceof EnvironmentConfigurable environmentConfigurable) { + environmentConfigurable.configure(environment); } } return constructorInjectionPoint; @@ -459,8 +439,7 @@ public final Collection> getRequiredComponents() { } }; if (constructor != null) { - if (constructor instanceof MethodReference) { - MethodReference methodConstructor = (MethodReference) constructor; + if (constructor instanceof MethodReference methodConstructor) { if (methodConstructor.arguments != null && methodConstructor.arguments.length > 0) { for (Argument argument : methodConstructor.arguments) { argumentConsumer.accept(argument); @@ -505,24 +484,13 @@ public final Collection> getRequiredComponents() { } List> methodInjectionPoints = new ArrayList<>(methodInjection.length); for (MethodReference methodReference : methodInjection) { - MethodInjectionPoint methodInjectionPoint; - if (methodReference.requiresReflection) { - methodInjectionPoint = new ReflectionMethodInjectionPoint( - this, - methodReference.declaringType, - methodReference.methodName, - methodReference.arguments, - methodReference.annotationMetadata - ); - } else { - methodInjectionPoint = new DefaultMethodInjectionPoint<>( - this, - methodReference.declaringType, - methodReference.methodName, - methodReference.arguments, - methodReference.annotationMetadata - ); - } + MethodInjectionPoint methodInjectionPoint = new DefaultMethodInjectionPoint<>( + this, + methodReference.declaringType, + methodReference.methodName, + methodReference.arguments, + methodReference.annotationMetadata + ); methodInjectionPoints.add(methodInjectionPoint); if (environment != null) { ((EnvironmentConfigurable) methodInjectionPoint).configure(environment); @@ -542,26 +510,14 @@ public final Collection> getRequiredComponents() { } List> fieldInjectionPoints = new ArrayList<>(fieldInjection.length); for (FieldReference fieldReference : fieldInjection) { - FieldInjectionPoint fieldInjectionPoint; - if (fieldReference.requiresReflection) { - fieldInjectionPoint = new ReflectionFieldInjectionPoint<>( - this, - fieldReference.declaringType, - fieldReference.argument.getType(), - fieldReference.argument.getName(), - fieldReference.argument.getAnnotationMetadata(), - fieldReference.argument.getTypeParameters() - ); - } else { - fieldInjectionPoint = new DefaultFieldInjectionPoint<>( - this, - fieldReference.declaringType, - fieldReference.argument.getType(), - fieldReference.argument.getName(), - fieldReference.argument.getAnnotationMetadata(), - fieldReference.argument.getTypeParameters() - ); - } + FieldInjectionPoint fieldInjectionPoint = new DefaultFieldInjectionPoint<>( + this, + fieldReference.declaringType, + fieldReference.argument.getType(), + fieldReference.argument.getName(), + fieldReference.argument.getAnnotationMetadata(), + fieldReference.argument.getTypeParameters() + ); if (environment != null) { ((EnvironmentConfigurable) fieldInjectionPoint).configure(environment); } @@ -614,11 +570,15 @@ public final String getName() { } @Override + @Deprecated(since = "4") + @NextMajorVersion("Remove after Micronaut 4 Milestone 1. I think we always implement this method so it's not needed, otherwise un-deprecate") public T inject(BeanContext context, T bean) { - return (T) injectBean(new DefaultBeanResolutionContext(context, this), context, bean); + return inject(new DefaultBeanResolutionContext(context, this), context, bean); } @Override + @Deprecated(since = "4") + @NextMajorVersion("Remove after Micronaut 4 Milestone 1. I think we always implement this method so it's not needed, otherwise un-deprecate") public T inject(BeanResolutionContext resolutionContext, BeanContext context, T bean) { return (T) injectBean(resolutionContext, context, bean); } @@ -643,25 +603,25 @@ public T inject(BeanResolutionContext resolutionContext, BeanContext context, T public final void configure(Environment environment) { if (environment != null) { this.environment = environment; - if (constructorInjectionPoint instanceof EnvironmentConfigurable) { - ((EnvironmentConfigurable) constructorInjectionPoint).configure(environment); + if (constructorInjectionPoint instanceof EnvironmentConfigurable environmentConfigurable) { + environmentConfigurable.configure(environment); } if (methodInjectionPoints != null) { for (MethodInjectionPoint methodInjectionPoint : methodInjectionPoints) { - if (methodInjectionPoint instanceof EnvironmentConfigurable) { - ((EnvironmentConfigurable) methodInjectionPoint).configure(environment); + if (methodInjectionPoint instanceof EnvironmentConfigurable environmentConfigurable) { + environmentConfigurable.configure(environment); } } } if (fieldInjectionPoints != null) { for (FieldInjectionPoint fieldInjectionPoint : fieldInjectionPoints) { - if (fieldInjectionPoint instanceof EnvironmentConfigurable) { - ((EnvironmentConfigurable) fieldInjectionPoint).configure(environment); + if (fieldInjectionPoint instanceof EnvironmentConfigurable environmentConfigurable) { + environmentConfigurable.configure(environment); } } } - if (executableMethodsDefinition instanceof EnvironmentConfigurable) { - ((EnvironmentConfigurable) executableMethodsDefinition).configure(environment); + if (executableMethodsDefinition instanceof EnvironmentConfigurable environmentConfigurable) { + environmentConfigurable.configure(environment); } } } @@ -694,24 +654,7 @@ protected final void warnMissingProperty(Class type, String method, String prope } /** - * Resolves the proxied bean instance for this bean. - * - * @param beanContext The {@link BeanContext} - * @return The proxied bean - */ - @SuppressWarnings({"unchecked", "unused"}) - @Internal - protected final Object getProxiedBean(BeanContext beanContext) { - DefaultBeanContext defaultBeanContext = (DefaultBeanContext) beanContext; - Optional qualifier = getAnnotationMetadata().getAnnotationNameByStereotype(AnnotationUtil.QUALIFIER); - return defaultBeanContext.getProxyTargetBean( - getBeanType(), - (Qualifier) qualifier.map(q -> Qualifiers.byAnnotation(getAnnotationMetadata(), q)).orElse(null) - ); - } - - /** - * Implementing possible {@link io.micronaut.inject.ParametrizedBeanFactory#getRequiredArguments()}. + * Implementing possible {@link io.micronaut.inject.ParametrizedInstantiatableBeanDefinition#getRequiredArguments()}. * * @return The arguments required to construct parametrized bean */ @@ -739,6 +682,8 @@ public final Argument[] getRequiredArguments() { * @return The instantiated bean * @throws BeanInstantiationException If the bean cannot be instantiated for the arguments supplied */ + @Deprecated(since = "4") + @NextMajorVersion("Should be removed after Micronaut 4 Milestone 1") @SuppressWarnings({"java:S2789", "OptionalAssignedToNull"}) // performance optimization public final T build(BeanResolutionContext resolutionContext, BeanContext context, @@ -783,41 +728,83 @@ public final T build(BeanResolutionContext resolutionContext, */ @Internal @UsedByGeneratedCode + @Deprecated(since = "4") + @NextMajorVersion("Should be removed after Micronaut 4 Milestone 1") protected T doBuild(BeanResolutionContext resolutionContext, BeanContext context, BeanDefinition definition, Map requiredArgumentValues) { throw new IllegalStateException("Method must be implemented for 'ParametrizedBeanFactory' instance!"); } /** - * The default implementation which provides no injection. To be overridden by compile time tooling. + * Implementing possible {@link io.micronaut.inject.ParametrizedInstantiatableBeanDefinition#instantiate(BeanResolutionContext, BeanContext)}. * - * @param resolutionContext The resolution context - * @param context The bean context - * @param bean The bean - * @return The injected bean + * @param resolutionContext The {@link BeanResolutionContext} + * @param context The {@link BeanContext} + * @param requiredArgumentValues The required arguments values. The keys should match the names of the arguments + * returned by {@link #getRequiredArguments()} + * @return The instantiated bean + * @throws BeanInstantiationException If the bean cannot be instantiated for the arguments supplied + */ + @SuppressWarnings({"java:S2789", "OptionalAssignedToNull"}) // performance optimization + public final T instantiate(BeanResolutionContext resolutionContext, + BeanContext context, + Map requiredArgumentValues) throws BeanInstantiationException { + + requiredArgumentValues = requiredArgumentValues != null ? new LinkedHashMap<>(requiredArgumentValues) : Collections.emptyMap(); + Optional eachBeanType = null; + for (Argument requiredArgument : getRequiredArguments()) { + try (BeanResolutionContext.Path ignored = resolutionContext.getPath().pushConstructorResolve(this, requiredArgument)) { + String argumentName = requiredArgument.getName(); + Object value = requiredArgumentValues.get(argumentName); + if (value == null && !requiredArgument.isNullable()) { + if (eachBeanType == null) { + eachBeanType = classValue(EachBean.class); + } + if (eachBeanType.filter(type -> type == requiredArgument.getType()).isPresent()) { + throw new DisabledBeanException("@EachBean parameter disabled for argument: " + requiredArgument.getName()); + } + throw new BeanInstantiationException(resolutionContext, "Missing bean argument value: " + argumentName); + } + boolean requiresConversion = value != null && !requiredArgument.getType().isInstance(value); + if (requiresConversion) { + Optional converted = context.getConversionService().convert(value, requiredArgument.getType(), ConversionContext.of(requiredArgument)); + Object finalValue = value; + value = converted.orElseThrow(() -> new BeanInstantiationException(resolutionContext, "Invalid value [" + finalValue + "] for argument: " + argumentName)); + requiredArgumentValues.put(argumentName, value); + } + } + } + return doInstantiate(resolutionContext, context, requiredArgumentValues); + } + + /** + * Method to be implemented by the generated code if the bean definition is implementing {@link io.micronaut.inject.ParametrizedBeanFactory}. + * + * @param resolutionContext The resolution context + * @param context The bean context + * @param requiredArgumentValues The required arguments + * @return The built instance */ @Internal - @SuppressWarnings({"WeakerAccess", "unused"}) @UsedByGeneratedCode - protected Object injectBean(BeanResolutionContext resolutionContext, BeanContext context, Object bean) { - return bean; + protected T doInstantiate(BeanResolutionContext resolutionContext, BeanContext context, Map requiredArgumentValues) { + throw new IllegalStateException("Method must be implemented for 'ParametrizedInstantiatableBeanDefinition' instance!"); } /** - * Inject another bean, for example one created via factory. + * The default implementation which provides no injection. To be overridden by compile time tooling. * - * @param resolutionContext The reslution context - * @param context The context + * @param resolutionContext The resolution context + * @param context The bean context * @param bean The bean - * @return The bean + * @return The injected bean */ @Internal - @SuppressWarnings({"unused"}) + @SuppressWarnings({"WeakerAccess", "unused"}) @UsedByGeneratedCode - protected Object injectAnother(BeanResolutionContext resolutionContext, BeanContext context, Object bean) { - if (bean == null) { - throw new BeanInstantiationException(resolutionContext, "Bean factory returned null"); - } - return resolutionContext.inject(this, bean); + @Deprecated(since = "4") + @NextMajorVersion("Remove after Micronaut 4 Milestone 1") + protected Object injectBean(BeanResolutionContext resolutionContext, BeanContext context, Object bean) { + return bean; } /** @@ -847,8 +834,8 @@ protected Object postConstruct(BeanResolutionContext resolutionContext, BeanCont } } } - if (bean instanceof LifeCycle) { - bean = ((LifeCycle) bean).start(); + if (bean instanceof LifeCycle lifeCycle) { + bean = lifeCycle.start(); } return bean; } @@ -865,8 +852,8 @@ protected Object postConstruct(BeanResolutionContext resolutionContext, BeanCont @Internal @UsedByGeneratedCode protected Object preDestroy(BeanResolutionContext resolutionContext, BeanContext context, Object bean) { - if (bean instanceof LifeCycle) { - bean = ((LifeCycle) bean).stop(); + if (bean instanceof LifeCycle lifeCycle) { + bean = lifeCycle.stop(); } return bean; } @@ -941,7 +928,7 @@ protected final void checkInjectedBeanPropertyValue(String injectedBeanPropertyN @SuppressWarnings("WeakerAccess") protected final void invokeMethodWithReflection(BeanResolutionContext resolutionContext, BeanContext context, int methodIndex, Object bean, Object[] methodArgs) { MethodReference methodRef = methodInjection[methodIndex]; - Argument[] methodArgumentTypes = methodRef.arguments == null ? Argument.ZERO_ARGUMENTS : methodRef.arguments; + Argument[] methodArgumentTypes = methodRef.arguments == null ? Argument.ZERO_ARGUMENTS : methodRef.arguments; if (ClassUtils.REFLECTION_LOGGER.isDebugEnabled()) { ClassUtils.REFLECTION_LOGGER.debug("Bean of type [" + getBeanType() + "] uses reflection to inject method: '" + methodRef.methodName + "'"); } @@ -1001,14 +988,14 @@ protected final void setFieldWithReflection(BeanResolutionContext resolutionCont * @param qualifier The qualifier * @return The value */ - @SuppressWarnings({"unused", "unchecked"}) + @SuppressWarnings({"unused"}) @Internal @Deprecated protected final Object getValueForMethodArgument(BeanResolutionContext resolutionContext, BeanContext context, int methodIndex, int argIndex, Qualifier qualifier) { MethodReference methodRef = methodInjection[methodIndex]; - Argument argument = methodRef.arguments[argIndex]; + Argument argument = methodRef.arguments[argIndex]; try (BeanResolutionContext.Path ignored = resolutionContext.getPath() - .pushMethodArgumentResolve(this, methodRef.methodName, argument, methodRef.arguments, methodRef.requiresReflection)) { + .pushMethodArgumentResolve(this, methodRef.methodName, argument, methodRef.arguments)) { return resolveValue(resolutionContext, context, methodRef.annotationMetadata, argument, qualifier); } } @@ -1033,9 +1020,9 @@ protected final Object getPropertyValueForMethodArgument(BeanResolutionContext r String propertyValue, String cliProperty) { MethodReference methodRef = methodInjection[methodIndex]; - Argument argument = methodRef.arguments[argIndex]; + Argument argument = methodRef.arguments[argIndex]; try (BeanResolutionContext.Path ignored = resolutionContext.getPath() - .pushMethodArgumentResolve(this, methodRef.methodName, argument, methodRef.arguments, methodRef.requiresReflection)) { + .pushMethodArgumentResolve(this, methodRef.methodName, argument, methodRef.arguments)) { return resolvePropertyValue(resolutionContext, context, argument, propertyValue, cliProperty, false); } } @@ -1058,9 +1045,9 @@ protected final Object getPropertyPlaceholderValueForMethodArgument(BeanResoluti int argIndex, String value) { MethodReference methodRef = methodInjection[methodIndex]; - Argument argument = methodRef.arguments[argIndex]; + Argument argument = methodRef.arguments[argIndex]; try (BeanResolutionContext.Path ignored = resolutionContext.getPath() - .pushMethodArgumentResolve(this, methodRef.methodName, argument, methodRef.arguments, methodRef.requiresReflection)) { + .pushMethodArgumentResolve(this, methodRef.methodName, argument, methodRef.arguments)) { return resolvePropertyValue(resolutionContext, context, argument, value, null, true); } } @@ -1085,7 +1072,7 @@ protected final Object getPropertyValueForSetter(BeanResolutionContext resolutio String propertyValue, String cliProperty) { try (BeanResolutionContext.Path ignored = resolutionContext.getPath() - .pushMethodArgumentResolve(this, setterName, argument, new Argument[]{argument}, false)) { + .pushMethodArgumentResolve(this, setterName, argument, new Argument[]{argument})) { return resolvePropertyValue(resolutionContext, context, argument, propertyValue, cliProperty, false); } } @@ -1108,7 +1095,7 @@ protected final Object getPropertyPlaceholderValueForSetter(BeanResolutionContex Argument argument, String value) { try (BeanResolutionContext.Path ignored = resolutionContext.getPath() - .pushMethodArgumentResolve(this, setterName, argument, new Argument[]{argument}, false)) { + .pushMethodArgumentResolve(this, setterName, argument, new Argument[]{argument})) { return resolvePropertyValue(resolutionContext, context, argument, value, null, true); } } @@ -1129,7 +1116,7 @@ protected final Object getPropertyPlaceholderValueForSetter(BeanResolutionContex protected final boolean containsValueForMethodArgument(BeanResolutionContext resolutionContext, BeanContext context, int methodIndex, int argIndex, boolean isValuePrefix) { MethodReference methodRef = methodInjection[methodIndex]; AnnotationMetadata parentAnnotationMetadata = methodRef.annotationMetadata; - Argument argument = methodRef.arguments[argIndex]; + Argument argument = methodRef.arguments[argIndex]; return resolveContainsValue(resolutionContext, context, parentAnnotationMetadata, argument, isValuePrefix); } @@ -1153,7 +1140,7 @@ protected final K getBeanForMethodArgument(BeanResolutionContext resolutionC MethodReference methodRef = methodInjection[methodIndex]; Argument argument = resolveArgument(context, argIndex, methodRef.arguments); try (BeanResolutionContext.Path ignored = resolutionContext.getPath() - .pushMethodArgumentResolve(this, methodRef.methodName, argument, methodRef.arguments, methodRef.requiresReflection)) { + .pushMethodArgumentResolve(this, methodRef.methodName, argument, methodRef.arguments)) { return resolveBean(resolutionContext, argument, qualifier); } } @@ -1177,7 +1164,7 @@ protected final > R getBeansOfTypeForMethodArgument(B MethodReference methodRef = methodInjection[methodIndex]; Argument argument = resolveArgument(context, argumentIndex, methodRef.arguments); try (BeanResolutionContext.Path ignored = - resolutionContext.getPath().pushMethodArgumentResolve(this, methodRef.methodName, argument, methodRef.arguments, methodRef.requiresReflection)) { + resolutionContext.getPath().pushMethodArgumentResolve(this, methodRef.methodName, argument, methodRef.arguments)) { return resolveBeansOfType(resolutionContext, context, argument, resolveEnvironmentArgument(context, genericType), qualifier); } } @@ -1199,7 +1186,7 @@ protected final > R getBeansOfTypeForMethodArgument(B @UsedByGeneratedCode protected final Object getBeanForSetter(BeanResolutionContext resolutionContext, BeanContext context, String setterName, Argument argument, Qualifier qualifier) { try (BeanResolutionContext.Path ignored = resolutionContext.getPath() - .pushMethodArgumentResolve(this, setterName, argument, new Argument[]{argument}, false)) { + .pushMethodArgumentResolve(this, setterName, argument, new Argument[]{argument})) { return resolveBean(resolutionContext, argument, qualifier); } } @@ -1219,7 +1206,7 @@ protected final Object getBeanForSetter(BeanResolutionContext resolutionContext, @UsedByGeneratedCode protected final Collection getBeansOfTypeForSetter(BeanResolutionContext resolutionContext, BeanContext context, String setterName, Argument argument, Argument genericType, Qualifier qualifier) { try (BeanResolutionContext.Path ignored = - resolutionContext.getPath().pushMethodArgumentResolve(this, setterName, argument, new Argument[]{argument}, false)) { + resolutionContext.getPath().pushMethodArgumentResolve(this, setterName, argument, new Argument[]{argument})) { return resolveBeansOfType(resolutionContext, context, argument, resolveEnvironmentArgument(context, genericType), qualifier); } } @@ -1244,7 +1231,7 @@ protected final Optional findBeanForMethodArgument(BeanResolutionContext MethodReference methodRef = methodInjection[methodIndex]; Argument argument = resolveArgument(context, argIndex, methodRef.arguments); try (BeanResolutionContext.Path ignored = - resolutionContext.getPath().pushMethodArgumentResolve(this, methodRef.methodName, argument, methodRef.arguments, methodRef.requiresReflection)) { + resolutionContext.getPath().pushMethodArgumentResolve(this, methodRef.methodName, argument, methodRef.arguments)) { return resolveOptionalBean(resolutionContext, argument, resolveEnvironmentArgument(context, genericType), qualifier); } } @@ -1268,7 +1255,7 @@ protected final Stream getStreamOfTypeForMethodArgument(BeanResolutionContext MethodReference methodRef = methodInjection[methodIndex]; Argument argument = resolveArgument(context, argIndex, methodRef.arguments); try (BeanResolutionContext.Path ignored = - resolutionContext.getPath().pushMethodArgumentResolve(this, methodRef.methodName, argument, methodRef.arguments, methodRef.requiresReflection)) { + resolutionContext.getPath().pushMethodArgumentResolve(this, methodRef.methodName, argument, methodRef.arguments)) { return resolveStreamOfType(resolutionContext, argument, resolveEnvironmentArgument(context, genericType), qualifier); } } @@ -1296,11 +1283,10 @@ protected final Map getMapOfTypeForMethodArgument( int argIndex, Argument genericType, Qualifier qualifier) { - @SuppressWarnings("ConstantConditions") MethodReference methodRef = methodInjection[methodIndex]; Argument> argument = resolveArgument(context, argIndex, methodRef.arguments); try (BeanResolutionContext.Path ignored = - resolutionContext.getPath().pushMethodArgumentResolve(this, methodRef.methodName, argument, methodRef.arguments, methodRef.requiresReflection)) { + resolutionContext.getPath().pushMethodArgumentResolve(this, methodRef.methodName, argument, methodRef.arguments)) { return resolveMapOfType(resolutionContext, argument, resolveEnvironmentArgument(context, genericType), qualifier); } } @@ -1322,7 +1308,7 @@ protected final Object getBeanForConstructorArgument(BeanResolutionContext resol MethodReference constructorMethodRef = (MethodReference) constructor; Argument argument = resolveArgument(context, argIndex, constructorMethodRef.arguments); if (argument != null && argument.isDeclaredNullable()) { - BeanResolutionContext.Segment current = resolutionContext.getPath().peek(); + BeanResolutionContext.Segment current = resolutionContext.getPath().peek(); if (current != null && current.getArgument().equals(argument)) { return null; } @@ -1354,8 +1340,8 @@ protected final Object getValueForConstructorArgument(BeanResolutionContext reso try { Object result = resolveValue(resolutionContext, context, constructorRef.annotationMetadata, argument, qualifier); - if (this instanceof ValidatedBeanDefinition) { - ((ValidatedBeanDefinition) this).validateBeanArgument( + if (this instanceof ValidatedBeanDefinition validatedBeanDefinition) { + validatedBeanDefinition.validateBeanArgument( resolutionContext, getConstructor(), argument, @@ -1396,8 +1382,8 @@ protected final Object getPropertyValueForConstructorArgument(BeanResolutionCont try { Object result = resolvePropertyValue(resolutionContext, context, argument, propertyValue, cliProperty, false); - if (this instanceof ValidatedBeanDefinition) { - ((ValidatedBeanDefinition) this).validateBeanArgument( + if (this instanceof ValidatedBeanDefinition validatedBeanDefinition) { + validatedBeanDefinition.validateBeanArgument( resolutionContext, getConstructor(), argument, @@ -1436,8 +1422,8 @@ protected final Object getPropertyPlaceholderValueForConstructorArgument(BeanRes try { Object result = resolvePropertyValue(resolutionContext, context, argument, propertyValue, null, true); - if (this instanceof ValidatedBeanDefinition) { - ((ValidatedBeanDefinition) this).validateBeanArgument( + if (this instanceof ValidatedBeanDefinition validatedBeanDefinition) { + validatedBeanDefinition.validateBeanArgument( resolutionContext, getConstructor(), argument, @@ -1541,7 +1527,7 @@ protected final >> R getBeanRegistra MethodReference methodReference = methodInjection[methodIndex]; Argument argument = resolveArgument(context, argIndex, methodReference.arguments); try (BeanResolutionContext.Path ignored = resolutionContext.getPath() - .pushMethodArgumentResolve(this, methodReference.methodName, argument, methodReference.arguments, methodReference.requiresReflection)) { + .pushMethodArgumentResolve(this, methodReference.methodName, argument, methodReference.arguments)) { return resolveBeanRegistrations(resolutionContext, argument, resolveEnvironmentArgument(context, genericType), qualifier); } } @@ -1566,7 +1552,7 @@ protected final BeanRegistration getBeanRegistrationForMethodArgument(Bea MethodReference methodRef = methodInjection[methodIndex]; Argument argument = resolveArgument(context, argIndex, methodRef.arguments); try (BeanResolutionContext.Path ignored = resolutionContext.getPath() - .pushMethodArgumentResolve(this, methodRef.methodName, argument, methodRef.arguments, methodRef.requiresReflection)) { + .pushMethodArgumentResolve(this, methodRef.methodName, argument, methodRef.arguments)) { return resolveBeanRegistration(resolutionContext, context, argument, resolveEnvironmentArgument(context, genericType), qualifier); } } @@ -1664,8 +1650,7 @@ protected final Optional findBeanForConstructorArgument(BeanResolutionCon @UsedByGeneratedCode protected final K getBeanForField(BeanResolutionContext resolutionContext, BeanContext context, int fieldIndex, Qualifier qualifier) { final Argument argument = resolveEnvironmentArgument(context, fieldInjection[fieldIndex].argument); - try (BeanResolutionContext.Path ignored = resolutionContext.getPath() - .pushFieldResolve(this, argument, fieldInjection[fieldIndex].requiresReflection)) { + try (BeanResolutionContext.Path ignored = resolutionContext.getPath().pushFieldResolve(this, argument)) { return resolveBean(resolutionContext, argument, qualifier); } } @@ -1696,7 +1681,7 @@ protected final K getBeanForAnnotation(BeanResolutionContext resolutionConte @Deprecated protected final Object getValueForField(BeanResolutionContext resolutionContext, BeanContext context, int fieldIndex, Qualifier qualifier) { FieldReference fieldRef = fieldInjection[fieldIndex]; - try (BeanResolutionContext.Path ignored = resolutionContext.getPath().pushFieldResolve(this, fieldRef.argument, fieldRef.requiresReflection)) { + try (BeanResolutionContext.Path ignored = resolutionContext.getPath().pushFieldResolve(this, fieldRef.argument)) { return resolveValue(resolutionContext, context, fieldRef.argument.getAnnotationMetadata(), fieldRef.argument, qualifier); } } @@ -1717,7 +1702,7 @@ protected final Object getValueForField(BeanResolutionContext resolutionContext, @UsedByGeneratedCode @Deprecated protected final Object getPropertyValueForField(BeanResolutionContext resolutionContext, BeanContext context, Argument argument, String propertyValue, String cliProperty) { - try (BeanResolutionContext.Path ignored = resolutionContext.getPath().pushFieldResolve(this, argument, false)) { + try (BeanResolutionContext.Path ignored = resolutionContext.getPath().pushFieldResolve(this, argument)) { return resolvePropertyValue(resolutionContext, context, argument, propertyValue, cliProperty, false); } } @@ -1737,7 +1722,7 @@ protected final Object getPropertyValueForField(BeanResolutionContext resolution @UsedByGeneratedCode @Deprecated protected final Object getPropertyPlaceholderValueForField(BeanResolutionContext resolutionContext, BeanContext context, Argument argument, String placeholder) { - try (BeanResolutionContext.Path ignored = resolutionContext.getPath().pushFieldResolve(this, argument, false)) { + try (BeanResolutionContext.Path ignored = resolutionContext.getPath().pushFieldResolve(this, argument)) { return resolvePropertyValue(resolutionContext, context, argument, placeholder, null, true); } } @@ -1811,7 +1796,7 @@ protected final boolean containsProperties(BeanResolutionContext resolutionConte @Internal @UsedByGeneratedCode protected final boolean containsProperties(@SuppressWarnings("unused") BeanResolutionContext resolutionContext, BeanContext context, String subProperty) { - return isConfigurationProperties; + return precalculatedInfo.isConfigurationProperties; } /** @@ -1832,7 +1817,7 @@ protected final > Object getBeansOfTypeForField(BeanR // Keep Object type for backwards compatibility final FieldReference fieldRef = fieldInjection[fieldIndex]; final Argument argument = resolveEnvironmentArgument(context, fieldRef.argument); - try (BeanResolutionContext.Path ignored = resolutionContext.getPath().pushFieldResolve(this, argument, fieldRef.requiresReflection)) { + try (BeanResolutionContext.Path ignored = resolutionContext.getPath().pushFieldResolve(this, argument)) { return resolveBeansOfType(resolutionContext, context, argument, resolveEnvironmentArgument(context, genericType), qualifier); } } @@ -1856,8 +1841,7 @@ protected final > Object getBeansOfTypeForField(BeanR protected final >> R getBeanRegistrationsForField(BeanResolutionContext resolutionContext, BeanContext context, int fieldIndex, Argument genericType, Qualifier qualifier) { FieldReference fieldRef = fieldInjection[fieldIndex]; Argument argument = resolveEnvironmentArgument(context, fieldRef.argument); - try (BeanResolutionContext.Path ignored = resolutionContext.getPath() - .pushFieldResolve(this, argument, fieldRef.requiresReflection)) { + try (BeanResolutionContext.Path ignored = resolutionContext.getPath().pushFieldResolve(this, argument)) { return resolveBeanRegistrations(resolutionContext, argument, resolveEnvironmentArgument(context, genericType), qualifier); } } @@ -1880,8 +1864,7 @@ protected final >> R getBeanRegistra protected final BeanRegistration getBeanRegistrationForField(BeanResolutionContext resolutionContext, BeanContext context, int fieldIndex, Argument genericType, Qualifier qualifier) { FieldReference fieldRef = fieldInjection[fieldIndex]; Argument argument = resolveEnvironmentArgument(context, fieldRef.argument); - try (BeanResolutionContext.Path ignored = resolutionContext.getPath() - .pushFieldResolve(this, argument, fieldRef.requiresReflection)) { + try (BeanResolutionContext.Path ignored = resolutionContext.getPath().pushFieldResolve(this, argument)) { return resolveBeanRegistration(resolutionContext, context, argument, resolveEnvironmentArgument(context, genericType), qualifier); } } @@ -1904,8 +1887,7 @@ protected final BeanRegistration getBeanRegistrationForField(BeanResoluti protected final Optional findBeanForField(BeanResolutionContext resolutionContext, BeanContext context, int fieldIndex, Argument genericType, Qualifier qualifier) { FieldReference fieldRef = fieldInjection[fieldIndex]; Argument argument = resolveEnvironmentArgument(context, fieldRef.argument); - try (BeanResolutionContext.Path ignored = resolutionContext.getPath() - .pushFieldResolve(this, argument, fieldRef.requiresReflection)) { + try (BeanResolutionContext.Path ignored = resolutionContext.getPath().pushFieldResolve(this, argument)) { return resolveOptionalBean(resolutionContext, argument, resolveEnvironmentArgument(context, genericType), qualifier); } } @@ -1928,8 +1910,7 @@ protected final Optional findBeanForField(BeanResolutionContext resolutio protected final Stream getStreamOfTypeForField(BeanResolutionContext resolutionContext, BeanContext context, int fieldIndex, Argument genericType, Qualifier qualifier) { FieldReference fieldRef = fieldInjection[fieldIndex]; Argument argument = resolveEnvironmentArgument(context, fieldRef.argument); - try (BeanResolutionContext.Path ignored = resolutionContext.getPath() - .pushFieldResolve(this, argument, fieldRef.requiresReflection)) { + try (BeanResolutionContext.Path ignored = resolutionContext.getPath().pushFieldResolve(this, argument)) { return resolveStreamOfType(resolutionContext, argument, resolveEnvironmentArgument(context, genericType), qualifier); } } @@ -1954,12 +1935,10 @@ protected final Map getMapOfTypeForField( BeanContext context, int fieldIndex, Argument genericType, Qualifier qualifier) { - @SuppressWarnings("ConstantConditions") FieldReference fieldRef = fieldInjection[fieldIndex]; @SuppressWarnings("unchecked") Argument> argument = resolveEnvironmentArgument(context, fieldRef.argument); - try (BeanResolutionContext.Path ignored = resolutionContext.getPath() - .pushFieldResolve(this, argument, fieldRef.requiresReflection)) { + try (BeanResolutionContext.Path ignored = resolutionContext.getPath().pushFieldResolve(this, argument)) { return resolveMapOfType(resolutionContext, argument, resolveEnvironmentArgument(context, genericType), qualifier); } } @@ -1967,36 +1946,33 @@ protected final Map getMapOfTypeForField( @Internal @UsedByGeneratedCode protected final boolean containsPropertiesValue(BeanResolutionContext resolutionContext, BeanContext context, String value) { - if (!(context instanceof ApplicationContext)) { + if (!(context instanceof ApplicationContext applicationContext)) { return false; } value = substituteWildCards(resolutionContext, value); - ApplicationContext applicationContext = (ApplicationContext) context; return applicationContext.containsProperties(value); } @Internal @UsedByGeneratedCode protected final boolean containsPropertyValue(BeanResolutionContext resolutionContext, BeanContext context, String value) { - if (!(context instanceof ApplicationContext)) { + if (!(context instanceof ApplicationContext applicationContext)) { return false; } value = substituteWildCards(resolutionContext, value); - ApplicationContext applicationContext = (ApplicationContext) context; return applicationContext.containsProperty(value); } private boolean resolveContainsValue(BeanResolutionContext resolutionContext, BeanContext context, AnnotationMetadata parentAnnotationMetadata, Argument argument, boolean isValuePrefix) { - if (!(context instanceof ApplicationContext)) { + if (!(context instanceof ApplicationContext applicationContext)) { return false; } - ApplicationContext applicationContext = (ApplicationContext) context; String valueAnnStr = argument.getAnnotationMetadata().stringValue(Value.class).orElse(null); String valString = resolvePropertyValueName(resolutionContext, parentAnnotationMetadata, argument, valueAnnStr); boolean result = isValuePrefix ? applicationContext.containsProperties(valString) : applicationContext.containsProperty(valString); - if (!result && isConfigurationProperties) { + if (!result && precalculatedInfo.isConfigurationProperties) { String cliOption = resolveCliOption(argument.getName()); if (cliOption != null) { result = applicationContext.containsProperty(cliOption); @@ -2035,7 +2011,7 @@ private Object resolveValue(BeanResolutionContext resolutionContext, BeanContext ArgumentConversionContext conversionContext = wrapperType ? ConversionContext.of(argumentType) : ConversionContext.of(argument); Optional value = resolveValue((ApplicationContext) context, conversionContext, valueAnnVal != null, valString); if (argument.isOptional()) { - if (!value.isPresent()) { + if (value.isEmpty()) { return value; } else { Object convertedOptional = value.get(); @@ -2101,13 +2077,13 @@ private Object resolvePropertyValue(BeanResolutionContext resolutionContext, Bea } else { stringValue = substituteWildCards(resolutionContext, stringValue); value = applicationContext.getProperty(stringValue, conversionContext); - if (!value.isPresent() && cliProperty != null) { + if (value.isEmpty() && cliProperty != null) { value = applicationContext.getProperty(cliProperty, conversionContext); } } if (argument.isOptional()) { - if (!value.isPresent()) { + if (value.isEmpty()) { return value; } else { Object convertedOptional = value.get(); @@ -2151,7 +2127,7 @@ private K resolveBean( return (K) qualifier; } try { - boolean isNotInnerConfiguration = !isConfigurationProperties || !isInnerConfiguration(argument); + boolean isNotInnerConfiguration = !precalculatedInfo.isConfigurationProperties || !isInnerConfiguration(argument); ConfigurationPath previousPath = isNotInnerConfiguration ? resolutionContext.setConfigurationPath(null) : null; try { return resolutionContext.getBean(argument, qualifier); @@ -2190,7 +2166,7 @@ private Optional resolveValue( return context.resolvePlaceholders(valString).flatMap(v -> context.getConversionService().convert(v, argument)); } else { Optional value = context.getProperty(valString, argument); - if (!value.isPresent() && isConfigurationProperties) { + if (value.isEmpty() && precalculatedInfo.isConfigurationProperties) { String cliOption = resolveCliOption(argument.getArgument().getName()); if (cliOption != null) { return context.getProperty(cliOption, argument); @@ -2253,12 +2229,12 @@ private > R resolveBeansOfType(BeanResolutionContext } private boolean isInnerConfiguration(@Nullable Argument argument) { - if (argument == null || !isConfigurationProperties) { + if (argument == null || !precalculatedInfo.isConfigurationProperties) { return false; } if (argument.isContainerType() || argument.isOptional() || argument.isProvider()) { return isInnerConfiguration(argument.getFirstTypeVariable().orElse(null)); - } else if (isIterable && isEachBeanParent(argument)) { + } else if (precalculatedInfo.isIterable && isEachBeanParent(argument)) { return true; } return isInnerConfiguration(argument.getType()); @@ -2267,10 +2243,7 @@ private boolean isInnerConfiguration(@Nullable Argument argument) { private boolean isEachBeanParent(Argument argument) { // treat an each bean declaration like an inner configuration Class t = getAnnotationMetadata().classValue(EachBean.class).orElse(null); - if (t != null && t.equals(argument.getType())) { - return true; - } - return false; + return t != null && t.equals(argument.getType()); } private Stream resolveStreamOfType(BeanResolutionContext resolutionContext, Argument returnType, Argument beanType, Qualifier qualifier) { @@ -2293,11 +2266,8 @@ private Map resolveMapOfType( Map map = resolutionContext.mapOfType(beanType, qualifier); if (returnType.isInstance(map)) { return map; - } else { - return resolutionContext.getContext().getConversionService().convertRequired( - map, returnType - ); } + return resolutionContext.getContext().getConversionService().convertRequired(map, returnType); } private Optional resolveOptionalBean(BeanResolutionContext resolutionContext, Argument returnType, Argument beanType, Qualifier qualifier) { @@ -2381,12 +2351,12 @@ private Qualifier resolveQualifier(BeanResolutionContext resolutionCon return currentQualifier; } else { BeanResolutionContext.Path path = resolutionContext.getPath(); - BeanResolutionContext.Segment segment = path.peek(); + BeanResolutionContext.Segment segment = path.peek(); if (segment != null && segment.getDeclaringType().hasAnnotation(AnnotationUtil.ANN_INTERCEPTOR_BINDINGS)) { return resolutionContext.getConfigurationPath().beanQualifier(); } } - } else if (isIterable && resultType.isAnnotationPresent(Parameter.class)) { + } else if (precalculatedInfo.isIterable && resultType.isAnnotationPresent(Parameter.class)) { return (Qualifier) resolutionContext.getCurrentQualifier(); } return null; @@ -2405,14 +2375,28 @@ private > K coerceCollectionToCorrectType(Class co } private void instrumentAnnotationMetadata(BeanContext context, Object object) { - if (object instanceof EnvironmentConfigurable && context instanceof ApplicationContext) { - final EnvironmentConfigurable ec = (EnvironmentConfigurable) object; + if (object instanceof final EnvironmentConfigurable ec && context instanceof ApplicationContext) { if (ec.hasPropertyExpressions()) { ec.configure(((ApplicationContext) context).getEnvironment()); } } } + @Internal + @UsedByGeneratedCode + public record PrecalculatedInfo( + Optional scope, + boolean isAbstract, + boolean isIterable, + boolean isSingleton, + boolean isPrimary, + boolean isConfigurationProperties, + boolean isContainerType, + boolean requiresMethodProcessing + ) { + + } + /** * Internal environment aware annotation metadata delegate. */ @@ -2440,6 +2424,8 @@ public static final class MethodReference extends MethodOrFieldReference { public final boolean isPreDestroyMethod; public final boolean isPostConstructMethod; + @Deprecated(since = "4") + @NextMajorVersion("Remove after Micronaut 4 Milestone 1") public MethodReference(Class declaringType, String methodName, Argument[] arguments, @@ -2448,14 +2434,32 @@ public MethodReference(Class declaringType, this(declaringType, methodName, arguments, annotationMetadata, requiresReflection, false, false); } + public MethodReference(Class declaringType, + String methodName, + Argument[] arguments, + @Nullable AnnotationMetadata annotationMetadata) { + this(declaringType, methodName, arguments, annotationMetadata, false, false); + } + + @Deprecated(since = "4") + @NextMajorVersion("Remove after Micronaut 4 Milestone 1") + public MethodReference(Class declaringType, + String methodName, + Argument[] arguments, + @Nullable AnnotationMetadata annotationMetadata, + boolean isPostConstructMethod, + boolean isPreDestroyMethod, + boolean requiresReflection) { + this(declaringType, methodName, arguments, annotationMetadata, isPostConstructMethod, isPreDestroyMethod); + } + public MethodReference(Class declaringType, String methodName, Argument[] arguments, @Nullable AnnotationMetadata annotationMetadata, - boolean requiresReflection, boolean isPostConstructMethod, boolean isPreDestroyMethod) { - super(declaringType, requiresReflection); + super(declaringType); this.methodName = methodName; this.arguments = arguments; this.annotationMetadata = annotationMetadata == null ? AnnotationMetadata.EMPTY_METADATA : annotationMetadata; @@ -2472,8 +2476,14 @@ public MethodReference(Class declaringType, public static final class FieldReference extends MethodOrFieldReference { public final Argument argument; + @Deprecated(since = "4") + @NextMajorVersion("Remove after Micronaut 4 Milestone 1") public FieldReference(Class declaringType, Argument argument, boolean requiresReflection) { - super(declaringType, requiresReflection); + this(declaringType, argument); + } + + public FieldReference(Class declaringType, Argument argument) { + super(declaringType); this.argument = argument; } @@ -2485,11 +2495,15 @@ public FieldReference(Class declaringType, Argument argument, boolean requiresRe @Internal public abstract static class MethodOrFieldReference { final Class declaringType; - final boolean requiresReflection; + @Deprecated(since = "4") + @NextMajorVersion("Remove after Micronaut 4 Milestone 1") public MethodOrFieldReference(Class declaringType, boolean requiresReflection) { + this(declaringType); + } + + public MethodOrFieldReference(Class declaringType) { this.declaringType = declaringType; - this.requiresReflection = requiresReflection; } } diff --git a/inject/src/main/java/io/micronaut/context/AbstractMessageSource.java b/inject/src/main/java/io/micronaut/context/AbstractMessageSource.java index f7286ac9370..0b68adc7d97 100644 --- a/inject/src/main/java/io/micronaut/context/AbstractMessageSource.java +++ b/inject/src/main/java/io/micronaut/context/AbstractMessageSource.java @@ -15,11 +15,11 @@ */ package io.micronaut.context; +import io.micronaut.core.annotation.NonNull; import io.micronaut.core.util.ArgumentUtils; +import io.micronaut.core.util.ObjectUtils; -import io.micronaut.core.annotation.NonNull; import java.util.Locale; -import java.util.Objects; /** * Abstract {@link MessageSource} implementation that provides basic message interpolation. @@ -136,7 +136,7 @@ public boolean equals(Object o) { @Override public int hashCode() { - return Objects.hash(locale, code); + return ObjectUtils.hash(locale, code); } } } diff --git a/inject/src/main/java/io/micronaut/context/AbstractParametrizedBeanDefinition.java b/inject/src/main/java/io/micronaut/context/AbstractParametrizedBeanDefinition.java deleted file mode 100644 index 521e0d1f78e..00000000000 --- a/inject/src/main/java/io/micronaut/context/AbstractParametrizedBeanDefinition.java +++ /dev/null @@ -1,137 +0,0 @@ -/* - * Copyright 2017-2020 original authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.micronaut.context; - -import io.micronaut.context.annotation.EachBean; -import io.micronaut.context.annotation.Parameter; -import io.micronaut.context.exceptions.BeanInstantiationException; -import io.micronaut.context.exceptions.DisabledBeanException; -import io.micronaut.core.annotation.AnnotationMetadata; -import io.micronaut.core.annotation.AnnotationUtil; -import io.micronaut.core.annotation.Internal; -import io.micronaut.core.convert.ConversionContext; -import io.micronaut.core.type.Argument; -import io.micronaut.inject.BeanDefinition; -import io.micronaut.inject.ParametrizedBeanFactory; - -import java.lang.annotation.Annotation; -import java.util.Arrays; -import java.util.Collections; -import java.util.LinkedHashMap; -import java.util.Map; -import java.util.Optional; - -/** - * A {@link BeanDefinition} that is a {@link ParametrizedBeanFactory}. - * - * @param The Bean definition type - * @author Graeme Rocher - * @since 1.0 - */ -@Internal -public abstract class AbstractParametrizedBeanDefinition extends AbstractBeanDefinition implements ParametrizedBeanFactory { - - private final Argument[] requiredArguments; - - /** - * @param producedType The produced type - * @param declaringType The declaring type - * @param methodName The method name - * @param methodMetadata The method metadata - * @param requiresReflection Whether requires refection - * @param arguments The arguments - */ - public AbstractParametrizedBeanDefinition(Class producedType, Class declaringType, String methodName, AnnotationMetadata methodMetadata, boolean requiresReflection, Argument... arguments) { - super(producedType, declaringType, methodName, methodMetadata, requiresReflection, arguments); - this.requiredArguments = resolveRequiredArguments(); - } - - /** - * @param type The type - * @param annotationMetadata The annotation metadata - * @param requiresReflection Whether requires reflection - * @param arguments The arguments - */ - protected AbstractParametrizedBeanDefinition(Class type, - AnnotationMetadata annotationMetadata, - boolean requiresReflection, - Argument... arguments) { - super(type, annotationMetadata, requiresReflection, arguments); - this.requiredArguments = resolveRequiredArguments(); - } - - @Override - public Argument[] getRequiredArguments() { - return requiredArguments; - } - - @Override - public final T build(BeanResolutionContext resolutionContext, - BeanContext context, - BeanDefinition definition, - Map requiredArgumentValues) throws BeanInstantiationException { - - requiredArgumentValues = requiredArgumentValues != null ? new LinkedHashMap<>(requiredArgumentValues) : Collections.emptyMap(); - Argument[] requiredArguments = getRequiredArguments(); - Optional eachBeanType = definition.classValue(EachBean.class); - for (Argument requiredArgument : requiredArguments) { - if (requiredArgument.getType() == BeanResolutionContext.class) { - requiredArgumentValues.put(requiredArgument.getName(), resolutionContext); - } - - BeanResolutionContext.Path path = resolutionContext.getPath(); - try { - path.pushConstructorResolve(this, requiredArgument); - String argumentName = requiredArgument.getName(); - if (!requiredArgumentValues.containsKey(argumentName) && !requiredArgument.isNullable()) { - if (eachBeanType.filter(type -> type == requiredArgument.getType()).isPresent()) { - throw new DisabledBeanException("@EachBean parameter disabled for argument: " + requiredArgument.getName()); - } - throw new BeanInstantiationException(resolutionContext, "Missing bean argument value: " + argumentName); - } - Object value = requiredArgumentValues.get(argumentName); - boolean requiresConversion = value != null && !requiredArgument.getType().isInstance(value); - if (requiresConversion) { - Optional converted = context.getConversionService().convert(value, requiredArgument.getType(), ConversionContext.of(requiredArgument)); - Object finalValue = value; - value = converted.orElseThrow(() -> new BeanInstantiationException(resolutionContext, "Invalid value [" + finalValue + "] for argument: " + argumentName)); - requiredArgumentValues.put(argumentName, value); - } - } finally { - path.pop(); - } - } - return doBuild(resolutionContext, context, definition, requiredArgumentValues); - } - - /** - * @param resolutionContext The resolution context - * @param context The bean context - * @param definition The bean definition - * @param requiredArgumentValues The required arguments - * @return The built instance - */ - protected abstract T doBuild(BeanResolutionContext resolutionContext, BeanContext context, BeanDefinition definition, Map requiredArgumentValues); - - private Argument[] resolveRequiredArguments() { - return Arrays.stream(getConstructor().getArguments()) - .filter(arg -> { - Optional> qualifierType = arg.getAnnotationMetadata().getAnnotationTypeByStereotype(AnnotationUtil.QUALIFIER); - return qualifierType.isPresent() && qualifierType.get() == Parameter.class; - }) - .toArray(Argument[]::new); - } -} diff --git a/inject/src/main/java/io/micronaut/context/BeanDefinitionDelegate.java b/inject/src/main/java/io/micronaut/context/BeanDefinitionDelegate.java index 41bc3888035..c03225a3fad 100644 --- a/inject/src/main/java/io/micronaut/context/BeanDefinitionDelegate.java +++ b/inject/src/main/java/io/micronaut/context/BeanDefinitionDelegate.java @@ -26,13 +26,17 @@ import io.micronaut.core.naming.NameResolver; import io.micronaut.core.naming.Named; import io.micronaut.core.type.Argument; +import io.micronaut.core.util.ObjectUtils; import io.micronaut.inject.BeanDefinition; import io.micronaut.inject.BeanFactory; import io.micronaut.inject.DelegatingBeanDefinition; import io.micronaut.inject.DisposableBeanDefinition; import io.micronaut.inject.InitializingBeanDefinition; +import io.micronaut.inject.InjectableBeanDefinition; import io.micronaut.inject.InjectionPoint; +import io.micronaut.inject.InstantiatableBeanDefinition; import io.micronaut.inject.ParametrizedBeanFactory; +import io.micronaut.inject.ParametrizedInstantiatableBeanDefinition; import io.micronaut.inject.ValidatedBeanDefinition; import io.micronaut.inject.qualifiers.PrimaryQualifier; @@ -51,7 +55,8 @@ */ @Internal sealed class BeanDefinitionDelegate extends AbstractBeanContextConditional - implements DelegatingBeanDefinition, BeanFactory, NameResolver { + implements DelegatingBeanDefinition, InstantiatableBeanDefinition, + InjectableBeanDefinition, NameResolver { protected final BeanDefinition definition; @Nullable @@ -124,24 +129,48 @@ private boolean isPrimaryThroughAttribute() { } @Override - public T build(BeanResolutionContext resolutionContext, BeanContext context, BeanDefinition definition) throws BeanInstantiationException { + public T inject(BeanContext context, T bean) { + if (definition instanceof InjectableBeanDefinition injectableBeanDefinition) { + return injectableBeanDefinition.inject(context, bean); + } + return bean; + } + + @Override + public T inject(BeanResolutionContext resolutionContext, BeanContext context, T bean) { + if (definition instanceof InjectableBeanDefinition injectableBeanDefinition) { + return injectableBeanDefinition.inject(resolutionContext, context, bean); + } + return bean; + } + + @Override + public T instantiate(BeanResolutionContext resolutionContext, BeanContext context) throws BeanInstantiationException { ConfigurationPath oldPath = null; if (configurationPath != null) { oldPath = resolutionContext.getConfigurationPath(); resolutionContext.setConfigurationPath(configurationPath); } - try { + // TODO: delete this after Micronaut 4 Milestone 1 if (this.definition instanceof ParametrizedBeanFactory) { ParametrizedBeanFactory parametrizedBeanFactory = (ParametrizedBeanFactory) this.definition; - Map fulfilled = getParametersValues(resolutionContext, (DefaultBeanContext) context, definition, parametrizedBeanFactory); + Argument[] requiredArguments = parametrizedBeanFactory.getRequiredArguments(); + Map fulfilled = getParametersValues(resolutionContext, (DefaultBeanContext) context, definition, requiredArguments); return parametrizedBeanFactory.build(resolutionContext, context, definition, fulfilled); } - if (this.definition instanceof BeanFactory) { - return ((BeanFactory) this.definition).build(resolutionContext, context, definition); - } else { - throw new IllegalStateException("Cannot construct a dynamically registered singleton"); + if (this.definition instanceof BeanFactory beanFactory) { + return (T) beanFactory.build(resolutionContext, context, definition); + } + if (this.definition instanceof ParametrizedInstantiatableBeanDefinition parametrizedInstantiatableBeanDefinition) { + Argument[] requiredArguments = parametrizedInstantiatableBeanDefinition.getRequiredArguments(); + Map fulfilled = getParametersValues(resolutionContext, (DefaultBeanContext) context, definition, requiredArguments); + return parametrizedInstantiatableBeanDefinition.instantiate(resolutionContext, context, fulfilled); + } + if (this.definition instanceof InstantiatableBeanDefinition instantiatableBeanDefinition) { + return instantiatableBeanDefinition.instantiate(resolutionContext, context); } + throw new IllegalStateException("Cannot construct a dynamically registered singleton"); } finally { resolutionContext.setConfigurationPath(oldPath); } @@ -151,8 +180,7 @@ public T build(BeanResolutionContext resolutionContext, BeanContext context, Bea private Map getParametersValues(BeanResolutionContext resolutionContext, DefaultBeanContext context, BeanDefinition definition, - ParametrizedBeanFactory parametrizedBeanFactory) { - Argument[] requiredArguments = (Argument[]) parametrizedBeanFactory.getRequiredArguments(); + Argument[] requiredArguments) { if (requiredArguments.length == 0) { return Collections.emptyMap(); } @@ -226,7 +254,7 @@ public boolean equals(Object o) { @Override public int hashCode() { - return Objects.hash(definition, qualifier); + return ObjectUtils.hash(definition, qualifier); } /** diff --git a/inject/src/main/java/io/micronaut/context/BeanRegistration.java b/inject/src/main/java/io/micronaut/context/BeanRegistration.java index 2be2aba5d9c..a4b5810e6fb 100644 --- a/inject/src/main/java/io/micronaut/context/BeanRegistration.java +++ b/inject/src/main/java/io/micronaut/context/BeanRegistration.java @@ -21,6 +21,7 @@ import io.micronaut.core.order.OrderUtil; import io.micronaut.core.order.Ordered; import io.micronaut.core.util.CollectionUtils; +import io.micronaut.core.util.ObjectUtils; import io.micronaut.inject.BeanDefinition; import io.micronaut.inject.BeanIdentifier; import io.micronaut.inject.BeanType; @@ -146,7 +147,7 @@ public boolean equals(Object o) { @Override public int hashCode() { - return Objects.hash(identifier, beanDefinition); + return ObjectUtils.hash(identifier, beanDefinition); } @Override diff --git a/inject/src/main/java/io/micronaut/context/BeanResolutionContext.java b/inject/src/main/java/io/micronaut/context/BeanResolutionContext.java index c26bd02dd6e..2667a895414 100644 --- a/inject/src/main/java/io/micronaut/context/BeanResolutionContext.java +++ b/inject/src/main/java/io/micronaut/context/BeanResolutionContext.java @@ -112,18 +112,6 @@ default Map mapOfType(@NonNull Argument beanType, @Nullable Qu @NonNull Optional findBean(@NonNull Argument beanType, @Nullable Qualifier qualifier); - /** - * Injects a bean. - * - * @param beanDefinition The requesting bean definition - * @param instance The instance - * @param The instance type - * @return The instance - * @since 3.5.0 - */ - @NonNull - T inject(@Nullable BeanDefinition beanDefinition, @NonNull T instance); - /** * Obtains the bean registrations for the given type and qualifier. * @@ -315,7 +303,7 @@ default void markDependentAsFactory() { /** * Represents a path taken to resolve a bean definitions dependencies. */ - interface Path extends Deque>, AutoCloseable { + interface Path extends Deque>, AutoCloseable { /** * Push an unresolved constructor call onto the queue. * @@ -332,10 +320,9 @@ interface Path extends Deque>, AutoCloseable { * @param methodName The method name * @param argument The unresolved argument * @param arguments The arguments - * @param requiresReflection is requires reflection * @return This path */ - Path pushConstructorResolve(BeanDefinition declaringType, String methodName, Argument argument, Argument[] arguments, boolean requiresReflection); + Path pushConstructorResolve(BeanDefinition declaringType, String methodName, Argument argument, Argument[] arguments); /** * Push an unresolved constructor call onto the queue. @@ -363,10 +350,9 @@ interface Path extends Deque>, AutoCloseable { * @param methodName The method name * @param argument The unresolved argument * @param arguments The arguments - * @param requiresReflection is requires reflection * @return This path */ - Path pushMethodArgumentResolve(BeanDefinition declaringType, String methodName, Argument argument, Argument[] arguments, boolean requiresReflection); + Path pushMethodArgumentResolve(BeanDefinition declaringType, String methodName, Argument argument, Argument[] arguments); /** * Push an unresolved field onto the queue. @@ -382,10 +368,9 @@ interface Path extends Deque>, AutoCloseable { * * @param declaringType declaring type * @param fieldAsArgument The field as argument - * @param requiresReflection is requires reflection * @return This path */ - Path pushFieldResolve(BeanDefinition declaringType, Argument fieldAsArgument, boolean requiresReflection); + Path pushFieldResolve(BeanDefinition declaringType, Argument fieldAsArgument); Path pushAnnotationResolve(BeanDefinition beanDefinition, Argument annotationMemberBeanAsArgument); @@ -399,7 +384,7 @@ interface Path extends Deque>, AutoCloseable { /** * @return The current path segment */ - Optional> currentSegment(); + Optional> currentSegment(); @Override default void close() { @@ -410,19 +395,20 @@ default void close() { /** * A segment in a path. * - * @param the bean type + * @param the declaring type + * @param the injected type */ - interface Segment { + interface Segment { /** * @return The type requested */ - BeanDefinition getDeclaringType(); + BeanDefinition getDeclaringType(); /** * @return The inject point */ - InjectionPoint getInjectionPoint(); + InjectionPoint getInjectionPoint(); /** * @return The name of the segment. For a field this is the field name, for a method the method name and for a constructor the type name @@ -432,6 +418,6 @@ interface Segment { /** * @return The argument to create the type. For a field this will be empty */ - Argument getArgument(); + Argument getArgument(); } } diff --git a/inject/src/main/java/io/micronaut/context/DefaultBeanContext.java b/inject/src/main/java/io/micronaut/context/DefaultBeanContext.java index 6c541cc9992..5354c998a46 100644 --- a/inject/src/main/java/io/micronaut/context/DefaultBeanContext.java +++ b/inject/src/main/java/io/micronaut/context/DefaultBeanContext.java @@ -98,13 +98,15 @@ import io.micronaut.inject.BeanFactory; import io.micronaut.inject.BeanIdentifier; import io.micronaut.inject.BeanType; -import io.micronaut.inject.ConstructorInjectionPoint; import io.micronaut.inject.DisposableBeanDefinition; import io.micronaut.inject.ExecutableMethod; import io.micronaut.inject.InitializingBeanDefinition; +import io.micronaut.inject.InjectableBeanDefinition; import io.micronaut.inject.InjectionPoint; +import io.micronaut.inject.InstantiatableBeanDefinition; import io.micronaut.inject.MethodExecutionHandle; import io.micronaut.inject.ParametrizedBeanFactory; +import io.micronaut.inject.ParametrizedInstantiatableBeanDefinition; import io.micronaut.inject.ProxyBeanDefinition; import io.micronaut.inject.QualifiedBeanType; import io.micronaut.inject.ValidatedBeanDefinition; @@ -506,8 +508,12 @@ public Optional refreshBean(@Nullable BeanIdentifier identifier) { @Override public void refreshBean(@NonNull BeanRegistration beanRegistration) { Objects.requireNonNull(beanRegistration, "BeanRegistration cannot be null"); - if (beanRegistration.bean != null) { - beanRegistration.definition().inject(this, beanRegistration.bean); + T bean = beanRegistration.bean; + if (bean != null) { + BeanDefinition definition = beanRegistration.definition(); + if (definition instanceof InjectableBeanDefinition injectableBeanDefinition) { + injectableBeanDefinition.inject(this, bean); + } } } @@ -740,7 +746,9 @@ public BeanContext registerSingleton(@NonNull Class type, @NonNull T sing } if (beanDefinition != null && !(beanDefinition instanceof RuntimeBeanDefinition) && beanDefinition.getBeanType().isInstance(singleton)) { try (BeanResolutionContext context = newResolutionContext(beanDefinition, null)) { - doInject(context, singleton, beanDefinition); + if (inject) { + doInjectAndInitialize(context, singleton, beanDefinition); + } DefaultBeanContext.BeanKey key = new DefaultBeanContext.BeanKey<>(beanDefinition.asArgument(), qualifier); singletonScope.registerSingletonBean(BeanRegistration.of(this, key, beanDefinition, singleton), qualifier); } @@ -1044,7 +1052,7 @@ public Stream streamOfType(BeanResolutionContext resolutionContext, Argum public T inject(@NonNull T instance) { Objects.requireNonNull(instance, "Instance cannot be null"); - Collection candidates = findBeanCandidatesForInstance(instance); + Collection> candidates = findBeanCandidatesForInstance(instance); BeanDefinition beanDefinition; if (candidates.size() == 1) { beanDefinition = candidates.iterator().next(); @@ -1062,7 +1070,7 @@ public T inject(@NonNull T instance) { beanKey, new BeanRegistration<>(beanKey, beanDefinition, instance) ); - doInject( + doInjectAndInitialize( resolutionContext, instance, beanDefinition @@ -1106,7 +1114,7 @@ public T createBean(@NonNull Class beanType, @Nullable Qualifier quali if (candidate.isPresent()) { BeanDefinition definition = candidate.get(); try (BeanResolutionContext resolutionContext = newResolutionContext(definition, null)) { - return doCreateBean(resolutionContext, definition, beanArg, qualifier, args); + return doCreateBean(resolutionContext, definition, qualifier, args); } } throw newNoSuchBeanException( @@ -1117,21 +1125,11 @@ public T createBean(@NonNull Class beanType, @Nullable Qualifier quali ); } - /** - * @param resolutionContext The bean resolution context - * @param definition The bean definition - * @param beanType The bean type - * @param qualifier The qualifier - * @param args The argument values - * @param the bean generic type - * @return The instance - */ @NonNull - protected T doCreateBean(@NonNull BeanResolutionContext resolutionContext, - @NonNull BeanDefinition definition, - @NonNull Argument beanType, - @Nullable Qualifier qualifier, - @Nullable Object... args) { + private T doCreateBean(@NonNull BeanResolutionContext resolutionContext, + @NonNull BeanDefinition definition, + @Nullable Qualifier qualifier, + @Nullable Object... args) { Map argumentValues = resolveArgumentValues(resolutionContext, definition, args); if (LOG.isTraceEnabled()) { LOG.trace("Computed bean argument values: {}", argumentValues); @@ -1141,14 +1139,19 @@ protected T doCreateBean(@NonNull BeanResolutionContext resolutionContext, @NonNull private Map resolveArgumentValues(BeanResolutionContext resolutionContext, BeanDefinition definition, Object[] args) { - if (!(definition instanceof ParametrizedBeanFactory)) { - return Collections.emptyMap(); + Argument[] requiredArguments; + // TODO: remove this after Micronaut 4 Milestone 1 + if (definition instanceof ParametrizedBeanFactory parametrizedBeanFactory) { + requiredArguments = parametrizedBeanFactory.getRequiredArguments(); + } else if (definition instanceof ParametrizedInstantiatableBeanDefinition parametrizedInstantiatableBeanDefinition) { + requiredArguments = parametrizedInstantiatableBeanDefinition.getRequiredArguments(); + } else { + return null; } if (LOG.isTraceEnabled()) { LOG.trace("Creating bean for parameters: {}", ArrayUtils.toString(args)); } MutableConversionService conversionService = getConversionService(); - Argument[] requiredArguments = ((ParametrizedBeanFactory) definition).getRequiredArguments(); Map argumentValues = new LinkedHashMap<>(requiredArguments.length); BeanResolutionContext.Path currentPath = resolutionContext.getPath(); for (int i = 0; i < requiredArguments.length; i++) { @@ -1460,12 +1463,12 @@ protected T inject(@NonNull BeanResolutionContext resolutionContext, @SuppressWarnings("unchecked") Class beanType = (Class) instance.getClass(); Optional> concreteCandidate = findBeanDefinition(beanType, null); if (concreteCandidate.isPresent()) { - BeanDefinition definition = concreteCandidate.get(); + BeanDefinition definition = concreteCandidate.get(); if (requestingBeanDefinition != null && requestingBeanDefinition.equals(definition)) { // bail out, don't inject for bean definition in creation return instance; } - doInject(resolutionContext, instance, definition); + doInjectAndInitialize(resolutionContext, instance, definition); } return instance; } @@ -2302,20 +2305,20 @@ protected void collectIterableBeans(@Nullable BeanResolutionContext resoluti * @return The candidates */ @NonNull - protected Collection findBeanCandidatesForInstance(@NonNull T instance) { + protected Collection> findBeanCandidatesForInstance(@NonNull T instance) { ArgumentUtils.requireNonNull("instance", instance); if (LOG.isDebugEnabled()) { LOG.debug("Finding candidate beans for instance: {}", instance); } - Collection beanDefinitionsClasses = this.beanDefinitionsClasses; + Collection> beanDefinitionsClasses = ((Collection) this.beanDefinitionsClasses); final Class beanClass = instance.getClass(); Argument beanType = Argument.of(beanClass); - Collection beanDefinitions = beanCandidateCache.get(beanType); + Collection> beanDefinitions = (Collection>) ((Map) beanCandidateCache).get(beanType); if (beanDefinitions == null) { // first traverse component definition classes and load candidates if (!beanDefinitionsClasses.isEmpty()) { - List candidates = new ArrayList<>(); - for (BeanDefinitionReference reference : beanDefinitionsClasses) { + List> candidates = new ArrayList<>(); + for (BeanDefinitionReference reference : beanDefinitionsClasses) { if (!reference.isEnabled(this)) { continue; } @@ -2323,7 +2326,7 @@ protected Collection findBeanCandidatesForInstance(@NonNull if (candidateType == null || !candidateType.isInstance(instance)) { continue; } - BeanDefinition candidate = reference.load(this); + BeanDefinition candidate = reference.load(this); if (!candidate.isEnabled(this)) { continue; } @@ -2349,7 +2352,7 @@ protected Collection findBeanCandidatesForInstance(@NonNull } beanDefinitions = Collections.emptySet(); } - beanCandidateCache.put(beanType, beanDefinitions); + beanCandidateCache.put(beanType, (Collection) beanDefinitions); } return beanDefinitions; } @@ -2381,104 +2384,24 @@ private T doCreateBean(@NonNull BeanResolutionContext resolutionContext, @NonNull BeanDefinition beanDefinition, @Nullable Qualifier qualifier, @Nullable Map argumentValues) { - return doCreateBean(resolutionContext, beanDefinition, qualifier, Argument.of(beanDefinition.getBeanType()), false, argumentValues); - } - - /** - * Execution the creation of a bean. The returned value can be null if a - * factory method returned null. - * - * @param resolutionContext The {@link BeanResolutionContext} - * @param beanDefinition The {@link BeanDefinition} - * @param qualifier The {@link Qualifier} - * @param The bean generic type - * @return The created bean - */ - @Internal - @NonNull - final T doCreateBean(@NonNull BeanResolutionContext resolutionContext, - @NonNull BeanDefinition beanDefinition, - @Nullable Qualifier qualifier) { - return doCreateBean(resolutionContext, beanDefinition, qualifier, Argument.of(beanDefinition.getBeanType()), false, null); - } - - /** - * Execution the creation of a bean. The returned value can be null if a - * factory method returned null. - * - * @param resolutionContext The {@link BeanResolutionContext} - * @param beanDefinition The {@link BeanDefinition} - * @param qualifier The {@link Qualifier} - * @param isSingleton Whether the bean is a singleton - * @param argumentValues Any argument values passed to create the bean - * @param The bean generic type - * @return The created bean - * @deprecated Use {@link #doCreateBean(BeanResolutionContext, BeanDefinition, Qualifier, Map)} instead. - */ - @Internal - @NonNull - @Deprecated - protected T doCreateBean(@NonNull BeanResolutionContext resolutionContext, - @NonNull BeanDefinition beanDefinition, - @Nullable Qualifier qualifier, - boolean isSingleton, - @Nullable Map argumentValues) { - return doCreateBean(resolutionContext, beanDefinition, qualifier, Argument.of(beanDefinition.getBeanType()), isSingleton, argumentValues); - } - - /** - * Execution the creation of a bean. The returned value can be null if a - * factory method returned null. - *

- * Method is deprecated since it doesn't do anything related to the singleton. - * - * @param resolutionContext The {@link BeanResolutionContext} - * @param beanDefinition The {@link BeanDefinition} - * @param qualifier The {@link Qualifier} - * @param qualifierBeanType The bean type used in the qualifier - * @param isSingleton Whether the bean is a singleton - * @param argumentValues Any argument values passed to create the bean - * @param The bean generic type - * @return The created bean - * @deprecated Use {@link #doCreateBean(BeanResolutionContext, BeanDefinition, Qualifier, Map)} instead. - */ - @Internal - @NonNull - @Deprecated - protected T doCreateBean(@NonNull BeanResolutionContext resolutionContext, - @NonNull BeanDefinition beanDefinition, - @Nullable Qualifier qualifier, - @Nullable Argument qualifierBeanType, - boolean isSingleton, - @Nullable Map argumentValues) { T bean; + // TODO: remove this after Micronaut 4 Milestone 1 if (beanDefinition instanceof BeanFactory) { bean = resolveByBeanFactory(resolutionContext, beanDefinition, qualifier, argumentValues); + } else if (beanDefinition instanceof InstantiatableBeanDefinition instantiatableBeanDefinition) { + bean = resolveByBeanFactory(resolutionContext, instantiatableBeanDefinition, qualifier, argumentValues); } else { - bean = resolveByBeanDefinition(resolutionContext, beanDefinition); + throw new BeanInstantiationException("BeanDefinition doesn't support creating a new instance of the bean"); } return postBeanCreated(resolutionContext, beanDefinition, qualifier, bean); } + @Internal @NonNull - private T resolveByBeanDefinition(@NonNull BeanResolutionContext resolutionContext, - @NonNull BeanDefinition beanDefinition) { - ConstructorInjectionPoint constructor = beanDefinition.getConstructor(); - Argument[] requiredConstructorArguments = constructor.getArguments(); - T bean; - if (requiredConstructorArguments.length == 0) { - bean = constructor.invoke(); - } else { - Object[] constructorArgs = new Object[requiredConstructorArguments.length]; - for (int i = 0; i < requiredConstructorArguments.length; i++) { - Class argument = requiredConstructorArguments[i].getType(); - constructorArgs[i] = getBean(resolutionContext, argument); - } - bean = constructor.invoke(constructorArgs); - } - - inject(resolutionContext, null, bean); - return bean; + private T doCreateBean(@NonNull BeanResolutionContext resolutionContext, + @NonNull BeanDefinition beanDefinition, + @Nullable Qualifier qualifier) { + return doCreateBean(resolutionContext, beanDefinition, qualifier, Collections.emptyMap()); } @NonNull @@ -2486,26 +2409,32 @@ private T resolveByBeanFactory(@NonNull BeanResolutionContext resolutionCont @NonNull BeanDefinition beanDefinition, @Nullable Qualifier qualifier, @Nullable Map argumentValues) { - BeanFactory beanFactory = (BeanFactory) beanDefinition; Qualifier declaredQualifier = beanDefinition.getDeclaredQualifier(); - boolean propagateQualifier = beanDefinition.isProxy() && declaredQualifier instanceof Named; - Qualifier prevQualifier = resolutionContext.getCurrentQualifier(); + Qualifier prevQualifier = resolutionContext.getCurrentQualifier(); try { resolutionContext.setCurrentQualifier(declaredQualifier != null && !AnyQualifier.INSTANCE.equals(declaredQualifier) ? declaredQualifier : qualifier); T bean; - if (beanFactory instanceof ParametrizedBeanFactory) { - ParametrizedBeanFactory parametrizedBeanFactory = (ParametrizedBeanFactory) beanDefinition; + // TODO: remove this after Micronaut 4 Milestone 1 + if (beanDefinition instanceof ParametrizedBeanFactory parametrizedBeanFactory) { Map convertedValues = getRequiredArgumentValues(resolutionContext, parametrizedBeanFactory.getRequiredArguments(), argumentValues, beanDefinition); - bean = (parametrizedBeanFactory).build(resolutionContext, this, beanDefinition, convertedValues); + bean = (T) parametrizedBeanFactory.build(resolutionContext, this, beanDefinition, convertedValues); + } else if (beanDefinition instanceof BeanFactory beanFactory) { + bean = (T) beanFactory.build(resolutionContext, this, beanDefinition); + } else if (beanDefinition instanceof ParametrizedInstantiatableBeanDefinition parametrizedInstantiatableBeanDefinition) { + Argument[] requiredArguments = parametrizedInstantiatableBeanDefinition.getRequiredArguments(); + Map convertedValues = getRequiredArgumentValues(resolutionContext, requiredArguments, argumentValues, beanDefinition); + bean = parametrizedInstantiatableBeanDefinition.instantiate(resolutionContext, this, convertedValues); + } else if (beanDefinition instanceof InstantiatableBeanDefinition instantiatableBeanDefinition) { + bean = instantiatableBeanDefinition.instantiate(resolutionContext, this); } else { - bean = beanFactory.build(resolutionContext, this, beanDefinition); + throw new BeanInstantiationException(resolutionContext, "Expected InstantiatableBeanDefinition [" + beanDefinition + "]"); } if (bean == null) { - throw new BeanInstantiationException(resolutionContext, "Bean Factory [" + beanFactory + "] returned null"); + throw new BeanInstantiationException(resolutionContext, "InstantiatableBeanDefinition [" + beanDefinition + "] returned null"); } - if (bean instanceof Qualified) { - ((Qualified) bean).$withBeanQualifier(declaredQualifier); + if (bean instanceof Qualified qualified) { + qualified.$withBeanQualifier(declaredQualifier); } return bean; } catch (DependencyInjectionException | DisabledBeanException | BeanInstantiationException e) { @@ -2572,31 +2501,30 @@ private Map getRequiredArgumentValues(@NonNull BeanResolutio } else { convertedValues = new LinkedHashMap<>(); } + if (convertedValues == null) { + return Collections.emptyMap(); + } MutableConversionService conversionService = getConversionService(); - if (convertedValues != null) { - for (Argument requiredArgument : requiredArguments) { - String argumentName = requiredArgument.getName(); - Object val = argumentValues.get(argumentName); - if (val == null) { - if (!requiredArgument.isDeclaredNullable()) { - throw new BeanInstantiationException(resolutionContext, "Missing bean argument [" + requiredArgument + "] for type: " + beanDefinition.getBeanType().getName() + ". Required arguments: " + ArrayUtils.toString(requiredArguments)); - } + for (Argument requiredArgument : requiredArguments) { + String argumentName = requiredArgument.getName(); + Object val = argumentValues.get(argumentName); + if (val == null) { + if (!requiredArgument.isDeclaredNullable()) { + throw new BeanInstantiationException(resolutionContext, "Missing bean argument [" + requiredArgument + "] for type: " + beanDefinition.getBeanType().getName() + ". Required arguments: " + ArrayUtils.toString(requiredArguments)); + } + } else { + Object convertedValue; + if (requiredArgument.getType().isInstance(val)) { + convertedValue = val; } else { - Object convertedValue; - if (requiredArgument.getType().isInstance(val)) { - convertedValue = val; - } else { - convertedValue = conversionService.convert(val, requiredArgument).orElseThrow(() -> - new BeanInstantiationException(resolutionContext, "Invalid bean argument [" + requiredArgument + "]. Cannot convert object [" + val + "] to required type: " + requiredArgument.getType()) - ); - } - convertedValues.put(argumentName, convertedValue); + convertedValue = conversionService.convert(val, requiredArgument).orElseThrow(() -> + new BeanInstantiationException(resolutionContext, "Invalid bean argument [" + requiredArgument + "]. Cannot convert object [" + val + "] to required type: " + requiredArgument.getType()) + ); } + convertedValues.put(argumentName, convertedValue); } - return convertedValues; - } else { - return Collections.emptyMap(); } + return convertedValues; } /** @@ -2857,10 +2785,15 @@ private boolean checkIfTypeMatches(BeanType definitionToBeReplaced, return replacingCandidate != Object.class && replacingCandidate.isAssignableFrom(bt); } - private void doInject(BeanResolutionContext resolutionContext, T instance, BeanDefinition definition) { - definition.inject(resolutionContext, this, instance); - if (definition instanceof InitializingBeanDefinition) { - ((InitializingBeanDefinition) definition).initialize(resolutionContext, this, instance); + private void doInjectAndInitialize(BeanResolutionContext resolutionContext, T instance, BeanDefinition beanDefinition) { + if (beanDefinition instanceof InjectableBeanDefinition injectableBeanDefinition) { + injectableBeanDefinition.inject(resolutionContext, this, instance); + if (injectableBeanDefinition instanceof InitializingBeanDefinition) { + InitializingBeanDefinition initializingBeanDefinition = ((InitializingBeanDefinition) injectableBeanDefinition); + initializingBeanDefinition.initialize(resolutionContext, this, instance); + } + } else { + throw new BeanContextException("Bean definition [" + beanDefinition + "] doesn't support injection!"); } } @@ -3068,12 +3001,12 @@ private BeanRegistration provideInjectionPoint(BeanResolutionContext reso Qualifier qualifier, boolean throwNoSuchBean) { final BeanResolutionContext.Path path = resolutionContext != null ? resolutionContext.getPath() : null; - BeanResolutionContext.Segment injectionPointSegment = null; + BeanResolutionContext.Segment injectionPointSegment = null; if (CollectionUtils.isNotEmpty(path)) { @SuppressWarnings("java:S2259") // false positive - final Iterator> i = path.iterator(); + final Iterator> i = path.iterator(); injectionPointSegment = i.next(); - BeanResolutionContext.Segment segment = null; + BeanResolutionContext.Segment segment = null; if (i.hasNext()) { segment = i.next(); if (segment.getDeclaringType().hasStereotype(INTRODUCTION_TYPE)) { @@ -3209,7 +3142,7 @@ private CustomScope findCustomScope(@Nullable BeanResolutionContext resol } if (resolutionContext != null) { - BeanResolutionContext.Segment currentSegment = resolutionContext + BeanResolutionContext.Segment currentSegment = resolutionContext .getPath() .currentSegment() .orElse(null); @@ -3314,7 +3247,6 @@ private Optional> findConcreteCandidate(@Nullable BeanReso } BeanCandidateKey bk = new BeanCandidateKey(beanType, qualifier, throwNonUnique); Optional beanDefinition = beanConcreteCandidateCache.get(bk); - //noinspection OptionalAssignedToNull if (beanDefinition == null) { beanDefinition = findConcreteCandidateNoCache( resolutionContext, @@ -3710,7 +3642,7 @@ private void logResolvedExistingBeanRegistrations(Argument beanType, Qual } private Stream> applyBeanResolutionFilters(@Nullable BeanResolutionContext resolutionContext, Stream> candidateStream) { - BeanResolutionContext.Segment segment = resolutionContext != null ? resolutionContext.getPath().peek() : null; + BeanResolutionContext.Segment segment = resolutionContext != null ? resolutionContext.getPath().peek() : null; if (segment instanceof AbstractBeanResolutionContext.ConstructorSegment || segment instanceof AbstractBeanResolutionContext.MethodSegment) { BeanDefinition declaringBean = segment.getDeclaringType(); // if the currently injected segment is a constructor argument and the type to be constructed is the @@ -4243,15 +4175,15 @@ private ScanningBeanResolutionContext(BeanDefinition beanDefinition, HashMap< private List> getHierarchy() { List> hierarchy = new ArrayList<>(path.size()); - for (Iterator> it = path.descendingIterator(); it.hasNext();) { - BeanResolutionContext.Segment segment = it.next(); + for (Iterator> it = path.descendingIterator(); it.hasNext();) { + BeanResolutionContext.Segment segment = it.next(); hierarchy.add(segment.getArgument()); } return hierarchy; } @Override - protected void onNewSegment(Segment segment) { + protected void onNewSegment(Segment segment) { Argument argument = segment.getArgument(); if (argument.isContainerType()) { argument = argument.getFirstTypeVariable().orElse(null); diff --git a/inject/src/main/java/io/micronaut/context/DefaultConstructorInjectionPoint.java b/inject/src/main/java/io/micronaut/context/DefaultConstructorInjectionPoint.java index eed3f5bea67..7c5a1f66185 100644 --- a/inject/src/main/java/io/micronaut/context/DefaultConstructorInjectionPoint.java +++ b/inject/src/main/java/io/micronaut/context/DefaultConstructorInjectionPoint.java @@ -16,22 +16,19 @@ package io.micronaut.context; import io.micronaut.context.env.Environment; -import io.micronaut.context.exceptions.BeanInstantiationException; import io.micronaut.core.annotation.AnnotationMetadata; import io.micronaut.core.annotation.Internal; import io.micronaut.core.annotation.NonNull; -import io.micronaut.core.reflect.ReflectionUtils; +import io.micronaut.core.annotation.Nullable; import io.micronaut.core.type.Argument; +import io.micronaut.core.util.ObjectUtils; import io.micronaut.inject.BeanDefinition; import io.micronaut.inject.ConstructorInjectionPoint; import io.micronaut.inject.annotation.AbstractEnvironmentAnnotationMetadata; import io.micronaut.inject.annotation.DefaultAnnotationMetadata; -import io.micronaut.core.annotation.Nullable; -import java.lang.reflect.Constructor; import java.util.Arrays; import java.util.Objects; -import java.util.Optional; /** * A constructor injection point for the non-reflection case. @@ -88,15 +85,6 @@ public void configure(Environment environment) { this.environment = environment; } - @Override - public T invoke(Object... args) { - Optional> potentialConstructor = ReflectionUtils.findConstructor(declaringType, argTypes); - if (potentialConstructor.isPresent()) { - return ReflectionConstructorInjectionPoint.invokeConstructor(potentialConstructor.get(), arguments, args); - } - throw new BeanInstantiationException("Constructor not found for type: " + this); - } - @Override public AnnotationMetadata getAnnotationMetadata() { return annotationMetadata; @@ -114,11 +102,6 @@ public BeanDefinition getDeclaringBean() { return declaringBean; } - @Override - public final boolean requiresReflection() { - return false; - } - @Override public boolean equals(Object o) { if (this == o) { @@ -134,10 +117,7 @@ public boolean equals(Object o) { @Override public int hashCode() { - - int result = Objects.hash(declaringType); - result = 31 * result + Arrays.hashCode(argTypes); - return result; + return ObjectUtils.hash(declaringType, argTypes); } @Override diff --git a/inject/src/main/java/io/micronaut/context/DefaultFieldConstructorInjectionPoint.java b/inject/src/main/java/io/micronaut/context/DefaultFieldConstructorInjectionPoint.java index bbc25771c3e..ee2a9b0169e 100644 --- a/inject/src/main/java/io/micronaut/context/DefaultFieldConstructorInjectionPoint.java +++ b/inject/src/main/java/io/micronaut/context/DefaultFieldConstructorInjectionPoint.java @@ -53,8 +53,4 @@ public Argument[] getArguments() { return Argument.ZERO_ARGUMENTS; } - @Override - public T invoke(Object... args) { - throw new UnsupportedOperationException("Use BeanFactory.instantiate(..) instead"); - } } diff --git a/inject/src/main/java/io/micronaut/context/DefaultFieldInjectionPoint.java b/inject/src/main/java/io/micronaut/context/DefaultFieldInjectionPoint.java index 66b2c947906..390a051cf6f 100644 --- a/inject/src/main/java/io/micronaut/context/DefaultFieldInjectionPoint.java +++ b/inject/src/main/java/io/micronaut/context/DefaultFieldInjectionPoint.java @@ -22,6 +22,7 @@ import io.micronaut.core.reflect.ReflectionUtils; import io.micronaut.core.type.Argument; import io.micronaut.core.util.ArrayUtils; +import io.micronaut.core.util.ObjectUtils; import io.micronaut.inject.BeanDefinition; import io.micronaut.inject.FieldInjectionPoint; import io.micronaut.inject.annotation.AbstractEnvironmentAnnotationMetadata; @@ -101,7 +102,7 @@ public boolean equals(Object o) { @Override public int hashCode() { - return Objects.hash(declaringType, fieldType, field); + return ObjectUtils.hash(declaringType, fieldType, field); } @Override @@ -124,16 +125,6 @@ public Class getType() { return fieldType; } - @Override - public void set(T instance, Object object) { - Field field = getField(); - ReflectionUtils.setField( - field, - instance, - object - ); - } - @Override @NonNull public Argument asArgument() { @@ -150,11 +141,6 @@ public BeanDefinition getDeclaringBean() { return declaringBean; } - @Override - public boolean requiresReflection() { - return false; - } - @Override public T synthesize(Class annotationClass) { return getAnnotationMetadata().synthesize(annotationClass); diff --git a/inject/src/main/java/io/micronaut/context/DefaultMethodConstructorInjectionPoint.java b/inject/src/main/java/io/micronaut/context/DefaultMethodConstructorInjectionPoint.java index a8e4ee0b28a..815df6e1e53 100644 --- a/inject/src/main/java/io/micronaut/context/DefaultMethodConstructorInjectionPoint.java +++ b/inject/src/main/java/io/micronaut/context/DefaultMethodConstructorInjectionPoint.java @@ -54,8 +54,4 @@ public Class getDeclaringBeanType() { return (Class) declaringType; } - @Override - public T invoke(Object... args) { - throw new UnsupportedOperationException("Use MethodInjectionPoint#invoke(..) instead"); - } } diff --git a/inject/src/main/java/io/micronaut/context/DefaultMethodInjectionPoint.java b/inject/src/main/java/io/micronaut/context/DefaultMethodInjectionPoint.java index 72c977a1ec6..f811d016ecc 100644 --- a/inject/src/main/java/io/micronaut/context/DefaultMethodInjectionPoint.java +++ b/inject/src/main/java/io/micronaut/context/DefaultMethodInjectionPoint.java @@ -20,13 +20,13 @@ import io.micronaut.core.annotation.AnnotationUtil; import io.micronaut.core.annotation.Internal; import io.micronaut.core.annotation.NonNull; -import io.micronaut.core.reflect.ReflectionUtils; +import io.micronaut.core.annotation.Nullable; import io.micronaut.core.type.Argument; +import io.micronaut.core.util.ObjectUtils; import io.micronaut.inject.BeanDefinition; import io.micronaut.inject.MethodInjectionPoint; import io.micronaut.inject.annotation.AbstractEnvironmentAnnotationMetadata; -import io.micronaut.core.annotation.Nullable; -import java.lang.reflect.Method; + import java.util.Arrays; import java.util.Objects; @@ -89,14 +89,6 @@ public void configure(Environment environment) { this.environment = environment; } - @Override - public Method getMethod() { - Method method = ReflectionUtils.getMethod(declaringType, methodName, argTypes) - .orElseThrow(() -> ReflectionUtils.newNoSuchMethodError(declaringType, methodName, argTypes)); - method.setAccessible(true); - return method; - } - @Override public String getName() { return methodName; @@ -112,12 +104,6 @@ public boolean isPostConstructMethod() { return annotationMetadata.hasDeclaredAnnotation(AnnotationUtil.POST_CONSTRUCT); } - @Override - public T invoke(Object instance, Object... args) { - Method targetMethod = getMethod(); - return ReflectionUtils.invokeMethod(instance, targetMethod, args); - } - @Override @NonNull public AnnotationMetadata getAnnotationMetadata() { @@ -130,11 +116,6 @@ public BeanDefinition getDeclaringBean() { return declaringBean; } - @Override - public boolean requiresReflection() { - return false; - } - @Override @NonNull public Argument[] getArguments() { @@ -158,9 +139,7 @@ public boolean equals(Object o) { @Override public int hashCode() { - int result = Objects.hash(declaringType, methodName); - result = 31 * result + Arrays.hashCode(argTypes); - return result; + return ObjectUtils.hash(declaringType, methodName, Arrays.hashCode(argTypes)); } private AnnotationMetadata initAnnotationMetadata(@Nullable AnnotationMetadata annotationMetadata) { diff --git a/inject/src/main/java/io/micronaut/context/DefaultRuntimeBeanDefinition.java b/inject/src/main/java/io/micronaut/context/DefaultRuntimeBeanDefinition.java index 0e84b8cb809..55555c161de 100644 --- a/inject/src/main/java/io/micronaut/context/DefaultRuntimeBeanDefinition.java +++ b/inject/src/main/java/io/micronaut/context/DefaultRuntimeBeanDefinition.java @@ -219,9 +219,7 @@ public boolean isSingleton() { } @Override - public T build(BeanResolutionContext resolutionContext, - BeanContext context, - BeanDefinition definition) throws BeanInstantiationException { + public T instantiate(BeanResolutionContext resolutionContext, BeanContext context) throws BeanInstantiationException { return supplier.get(); } diff --git a/inject/src/main/java/io/micronaut/context/MissingMethodInjectionPoint.java b/inject/src/main/java/io/micronaut/context/MissingMethodInjectionPoint.java deleted file mode 100644 index 99bb48467c2..00000000000 --- a/inject/src/main/java/io/micronaut/context/MissingMethodInjectionPoint.java +++ /dev/null @@ -1,100 +0,0 @@ -/* - * Copyright 2017-2020 original authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.micronaut.context; - -import io.micronaut.core.annotation.Internal; -import io.micronaut.core.reflect.ReflectionUtils; -import io.micronaut.core.type.Argument; -import io.micronaut.inject.BeanDefinition; -import io.micronaut.inject.MethodInjectionPoint; - -import java.lang.reflect.Method; -import java.util.Arrays; - -/** - * A method injection point that represents a method that does not exist. - * - * @author graemerocher - * @since 1.0 - */ -@Internal -class MissingMethodInjectionPoint implements MethodInjectionPoint { - - private final BeanDefinition definition; - private final Class declaringType; - private final String methodName; - private final Argument[] argTypes; - - /** - * @param definition The bean definition - * @param declaringType The declaring class type - * @param methodName The method name - * @param argTypes The argument types - */ - MissingMethodInjectionPoint( - BeanDefinition definition, - Class declaringType, - String methodName, - Argument[] argTypes) { - - this.definition = definition; - this.declaringType = declaringType; - this.methodName = methodName; - this.argTypes = argTypes; - } - - @Override - public Method getMethod() { - Class[] types = Arrays.stream(argTypes).map(Argument::getType).toArray(Class[]::new); - throw ReflectionUtils.newNoSuchMethodError(declaringType, methodName, types); - } - - @Override - public String getName() { - return methodName; - } - - @Override - public boolean isPreDestroyMethod() { - return false; - } - - @Override - public boolean isPostConstructMethod() { - return false; - } - - @Override - public Object invoke(Object instance, Object... args) { - Class[] types = Arrays.stream(argTypes).map(Argument::getType).toArray(Class[]::new); - throw ReflectionUtils.newNoSuchMethodError(declaringType, methodName, types); - } - - @Override - public Argument[] getArguments() { - return argTypes; - } - - @Override - public BeanDefinition getDeclaringBean() { - return definition; - } - - @Override - public boolean requiresReflection() { - return false; - } -} diff --git a/inject/src/main/java/io/micronaut/context/ReflectionConstructorInjectionPoint.java b/inject/src/main/java/io/micronaut/context/ReflectionConstructorInjectionPoint.java deleted file mode 100644 index deb0db75c74..00000000000 --- a/inject/src/main/java/io/micronaut/context/ReflectionConstructorInjectionPoint.java +++ /dev/null @@ -1,145 +0,0 @@ -/* - * Copyright 2017-2020 original authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.micronaut.context; - -import io.micronaut.context.exceptions.BeanInstantiationException; -import io.micronaut.core.annotation.AnnotationMetadata; -import io.micronaut.core.annotation.Internal; -import io.micronaut.core.reflect.ClassUtils; -import io.micronaut.core.reflect.ReflectionUtils; -import io.micronaut.core.type.Argument; -import io.micronaut.inject.BeanDefinition; -import io.micronaut.inject.ConstructorInjectionPoint; - -import java.lang.reflect.Constructor; - -/** - * An injection point for a constructor. - * - * @param The constructor type - * @author Graeme Rocher - * @since 1.0 - */ -@Internal -class ReflectionConstructorInjectionPoint implements ConstructorInjectionPoint { - - private final Class declaringType; - private final Argument[] arguments; - private final BeanDefinition declaringComponent; - private final AnnotationMetadata annotationMetadata; - private Constructor constructor; - - /** - * @param beanDefinition The bean definition - * @param declaringType The declaring type - * @param annotationMetadata The annotation metadata - * @param arguments The arguments to the constructor - */ - ReflectionConstructorInjectionPoint( - BeanDefinition beanDefinition, - Class declaringType, - AnnotationMetadata annotationMetadata, - Argument... arguments) { - - this.annotationMetadata = annotationMetadata == null ? AnnotationMetadata.EMPTY_METADATA : annotationMetadata; - this.declaringComponent = beanDefinition; - this.declaringType = declaringType; - this.arguments = arguments == null ? Argument.ZERO_ARGUMENTS : arguments; - if (ClassUtils.REFLECTION_LOGGER.isDebugEnabled()) { - ClassUtils.REFLECTION_LOGGER.debug("Bean of type [" + beanDefinition.getBeanType() + "] defines constructor that requires the use of reflection to inject"); - } - } - - @Override - public AnnotationMetadata getAnnotationMetadata() { - return annotationMetadata; - } - - @Override - public BeanDefinition getDeclaringBean() { - return this.declaringComponent; - } - - @Override - public boolean requiresReflection() { - return true; - } - - @Override - public Argument[] getArguments() { - return arguments; - } - - @Override - public T invoke(Object... args) { - return invokeConstructor(resolveConstructor(), getArguments(), args); - } - - private Constructor resolveConstructor() { - Constructor constructor = this.constructor; - if (constructor == null) { - synchronized (this) { // double check - constructor = this.constructor; - if (constructor == null) { - constructor = ReflectionUtils.findConstructor(declaringType, Argument.toClassArray(arguments)) - .orElseThrow(() -> - new BeanInstantiationException( - declaringComponent, - "No constructor found for arguments: " + Argument.toString(arguments) - ) - ); - this.constructor = constructor; - } - } - } - return constructor; - } - - /** - * @param theConstructor The constructor - * @param argumentTypes The argument types - * @param args The arguments - * @param The constructor type - * @return The constructor instance - */ - static T invokeConstructor(Constructor theConstructor, Argument[] argumentTypes, Object... args) { - theConstructor.setAccessible(true); - if (argumentTypes.length == 0) { - try { - return theConstructor.newInstance(); - } catch (Throwable e) { - throw new BeanInstantiationException("Cannot instantiate bean of type [" + theConstructor.getDeclaringClass().getName() + "] using constructor [" + theConstructor + "]:" + e.getMessage(), e); - } - } else { - if (argumentTypes.length != args.length) { - throw new BeanInstantiationException("Invalid bean argument count specified. Required: " + argumentTypes.length + " . Received: " + args.length); - } - - for (int i = 0; i < argumentTypes.length; i++) { - Argument componentType = argumentTypes[i]; - if (!componentType.getType().isInstance(args[i])) { - throw new BeanInstantiationException("Invalid bean argument received [" + args[i] + "] at position [" + i + "]. Required type is: " + componentType.getName()); - } - } - try { - return theConstructor.newInstance(args); - } catch (Throwable e) { - throw new BeanInstantiationException("Cannot instantiate bean of type [" + theConstructor.getDeclaringClass().getName() + "] using constructor [" + theConstructor + "]:" + e.getMessage(), e); - } - } - } - -} diff --git a/inject/src/main/java/io/micronaut/context/ReflectionFieldInjectionPoint.java b/inject/src/main/java/io/micronaut/context/ReflectionFieldInjectionPoint.java deleted file mode 100644 index 0d78936daec..00000000000 --- a/inject/src/main/java/io/micronaut/context/ReflectionFieldInjectionPoint.java +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Copyright 2017-2020 original authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.micronaut.context; - -import io.micronaut.core.annotation.AnnotationMetadata; -import io.micronaut.core.annotation.Internal; -import io.micronaut.core.reflect.ClassUtils; -import io.micronaut.core.type.Argument; -import io.micronaut.inject.BeanDefinition; - -import io.micronaut.core.annotation.Nullable; - -/** - * A field injection point invoked via reflection. - * - * @param The bean type that declares the injection point - * @param The field type - * @author graemerocher - * @since 1.0 - */ -@Internal -class ReflectionFieldInjectionPoint extends DefaultFieldInjectionPoint { - - /** - * @param declaringBean The declaring bean - * @param declaringType The declaring type - * @param fieldType The field type - * @param field The name of the field - * @param annotationMetadata The annotation metadata - * @param typeArguments the generic type arguments - */ - ReflectionFieldInjectionPoint( - BeanDefinition declaringBean, - Class declaringType, - Class fieldType, - String field, - @Nullable AnnotationMetadata annotationMetadata, - @Nullable Argument[] typeArguments) { - super(declaringBean, declaringType, fieldType, field, annotationMetadata, typeArguments); - if (ClassUtils.REFLECTION_LOGGER.isDebugEnabled()) { - ClassUtils.REFLECTION_LOGGER.debug("Bean of type [" + declaringBean.getBeanType() + "] defines field [" + field + "] that requires the use of reflection to inject"); - } - } - - @Override - public boolean requiresReflection() { - return true; - } -} diff --git a/inject/src/main/java/io/micronaut/context/ReflectionMethodConstructorInjectionPoint.java b/inject/src/main/java/io/micronaut/context/ReflectionMethodConstructorInjectionPoint.java deleted file mode 100644 index b9c172a4aa8..00000000000 --- a/inject/src/main/java/io/micronaut/context/ReflectionMethodConstructorInjectionPoint.java +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Copyright 2017-2020 original authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.micronaut.context; - -import io.micronaut.core.annotation.AnnotationMetadata; -import io.micronaut.core.annotation.Internal; -import io.micronaut.core.reflect.ClassUtils; -import io.micronaut.core.type.Argument; -import io.micronaut.inject.BeanDefinition; -import io.micronaut.inject.ConstructorInjectionPoint; - -import io.micronaut.core.annotation.Nullable; - -/** - *

Calls a method that constructs the object.

- * - * @author Graeme Rocher - * @since 1.0 - */ -@Internal -class ReflectionMethodConstructorInjectionPoint extends ReflectionMethodInjectionPoint implements ConstructorInjectionPoint { - - /** - * @param declaringBean The declaring bean - * @param declaringType The declaring type - * @param methodName The method name - * @param arguments The arguments - * @param annotationMetadata The annotation metadata - */ - ReflectionMethodConstructorInjectionPoint( - BeanDefinition declaringBean, - Class declaringType, - String methodName, - @Nullable Argument[] arguments, - @Nullable AnnotationMetadata annotationMetadata) { - - super(declaringBean, declaringType, methodName, arguments, annotationMetadata); - if (ClassUtils.REFLECTION_LOGGER.isDebugEnabled()) { - ClassUtils.REFLECTION_LOGGER.debug("Bean of type [" + declaringBean.getBeanType() + "] defines constructor [" + methodName + "] that requires the use of reflection to inject"); - } - } - - @Override - public Object invoke(Object... args) { - throw new UnsupportedOperationException("Use MethodInjectionPoint#invoke(..) instead"); - } -} diff --git a/inject/src/main/java/io/micronaut/context/ReflectionMethodInjectionPoint.java b/inject/src/main/java/io/micronaut/context/ReflectionMethodInjectionPoint.java deleted file mode 100644 index 890e4e0afd8..00000000000 --- a/inject/src/main/java/io/micronaut/context/ReflectionMethodInjectionPoint.java +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Copyright 2017-2020 original authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.micronaut.context; - -import io.micronaut.core.annotation.AnnotationMetadata; -import io.micronaut.core.annotation.Internal; -import io.micronaut.core.reflect.ClassUtils; -import io.micronaut.core.type.Argument; -import io.micronaut.inject.BeanDefinition; - -import io.micronaut.core.annotation.Nullable; - -/** - * Represents an injection point for a method that requires reflection. - * - * @author Graeme Rocher - * @since 1.0 - */ -@Internal -class ReflectionMethodInjectionPoint extends DefaultMethodInjectionPoint { - - /** - * @param declaringBean The declaring bean - * @param declaringType The declaring type - * @param methodName The method name - * @param arguments The arguments - * @param annotationMetadata The annotation metadata - */ - ReflectionMethodInjectionPoint( - BeanDefinition declaringBean, - Class declaringType, - String methodName, - @Nullable Argument[] arguments, - @Nullable AnnotationMetadata annotationMetadata) { - super(declaringBean, declaringType, methodName, arguments, annotationMetadata); - if (ClassUtils.REFLECTION_LOGGER.isDebugEnabled()) { - ClassUtils.REFLECTION_LOGGER.debug("Bean of type [" + declaringBean.getBeanType() + "] defines method [" + methodName + "] that requires the use of reflection to inject"); - } - } - - @Override - public boolean requiresReflection() { - return true; - } -} diff --git a/inject/src/main/java/io/micronaut/context/RuntimeBeanDefinition.java b/inject/src/main/java/io/micronaut/context/RuntimeBeanDefinition.java index aab361b565a..d7623daba3e 100644 --- a/inject/src/main/java/io/micronaut/context/RuntimeBeanDefinition.java +++ b/inject/src/main/java/io/micronaut/context/RuntimeBeanDefinition.java @@ -25,7 +25,7 @@ import io.micronaut.inject.BeanContextConditional; import io.micronaut.inject.BeanDefinition; import io.micronaut.inject.BeanDefinitionReference; -import io.micronaut.inject.BeanFactory; +import io.micronaut.inject.InstantiatableBeanDefinition; import io.micronaut.inject.qualifiers.Qualifiers; import java.lang.annotation.Annotation; @@ -53,7 +53,7 @@ * @see BeanDefinitionRegistry#registerBeanDefinition(RuntimeBeanDefinition) */ @Experimental -public interface RuntimeBeanDefinition extends BeanDefinitionReference, BeanDefinition, BeanFactory, BeanContextConditional { +public interface RuntimeBeanDefinition extends BeanDefinitionReference, InstantiatableBeanDefinition, BeanContextConditional { @Override @NonNull diff --git a/inject/src/main/java/io/micronaut/context/SingletonScope.java b/inject/src/main/java/io/micronaut/context/SingletonScope.java index 97849359bb5..021bd14683e 100644 --- a/inject/src/main/java/io/micronaut/context/SingletonScope.java +++ b/inject/src/main/java/io/micronaut/context/SingletonScope.java @@ -20,6 +20,7 @@ import io.micronaut.core.annotation.Nullable; import io.micronaut.core.type.Argument; import io.micronaut.core.util.ArgumentUtils; +import io.micronaut.core.util.ObjectUtils; import io.micronaut.inject.BeanDefinition; import io.micronaut.inject.BeanIdentifier; @@ -361,7 +362,7 @@ public boolean equals(Object o) { @Override public int hashCode() { - return Objects.hash(beanDefinitionDelegate.getBeanType(), beanDefinitionDelegate.getDeclaredQualifier()); + return ObjectUtils.hash(beanDefinitionDelegate.getBeanType(), beanDefinitionDelegate.getDeclaredQualifier()); } } @@ -395,7 +396,7 @@ public boolean equals(Object o) { @Override public int hashCode() { - return Objects.hash(beanDefinition.getBeanDefinitionName()); + return beanDefinition.getBeanDefinitionName().hashCode(); } } diff --git a/inject/src/main/java/io/micronaut/context/event/ApplicationEventPublisherFactory.java b/inject/src/main/java/io/micronaut/context/event/ApplicationEventPublisherFactory.java index fc6c9a64339..61ddccb6a13 100644 --- a/inject/src/main/java/io/micronaut/context/event/ApplicationEventPublisherFactory.java +++ b/inject/src/main/java/io/micronaut/context/event/ApplicationEventPublisherFactory.java @@ -15,19 +15,6 @@ */ package io.micronaut.context.event; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.Executor; -import java.util.concurrent.ForkJoinPool; -import java.util.concurrent.Future; -import java.util.function.Supplier; - import io.micronaut.context.BeanContext; import io.micronaut.context.BeanResolutionContext; import io.micronaut.context.annotation.BootstrapContextCompatible; @@ -42,13 +29,26 @@ import io.micronaut.core.util.SupplierUtil; import io.micronaut.inject.BeanDefinition; import io.micronaut.inject.BeanDefinitionReference; -import io.micronaut.inject.BeanFactory; import io.micronaut.inject.InjectionPoint; +import io.micronaut.inject.InstantiatableBeanDefinition; import io.micronaut.inject.annotation.MutableAnnotationMetadata; import io.micronaut.inject.qualifiers.Qualifiers; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.Executor; +import java.util.concurrent.ForkJoinPool; +import java.util.concurrent.Future; +import java.util.function.Supplier; + /** * Constructs instances of {@link io.micronaut.context.event.ApplicationEventPublisher}. * @@ -58,7 +58,7 @@ */ @Internal public final class ApplicationEventPublisherFactory - implements BeanDefinition>, BeanFactory>, + implements InstantiatableBeanDefinition>, BeanDefinitionReference> { private static final Argument TYPE_VARIABLE = Argument.ofTypeVariable(Object.class, "T"); @@ -85,7 +85,7 @@ public boolean isAbstract() { @Override public boolean isCandidateBean(Argument beanType) { - return BeanDefinition.super.isCandidateBean(beanType); + return InstantiatableBeanDefinition.super.isCandidateBean(beanType); } @Override @@ -135,17 +135,14 @@ public boolean isPresent() { } @Override - public ApplicationEventPublisher build(BeanResolutionContext resolutionContext, - BeanContext context, - BeanDefinition> definition) - throws BeanInstantiationException { + public ApplicationEventPublisher instantiate(BeanResolutionContext resolutionContext, BeanContext context) throws BeanInstantiationException { if (executorSupplier == null) { executorSupplier = SupplierUtil.memoized(() -> context.findBean(Executor.class, Qualifiers.byName("scheduled")).orElseGet(ForkJoinPool::commonPool) ); } Argument eventType = Argument.OBJECT_ARGUMENT; - final BeanResolutionContext.Segment segment = resolutionContext.getPath().currentSegment().orElse(null); + final BeanResolutionContext.Segment segment = resolutionContext.getPath().currentSegment().orElse(null); if (segment != null) { final InjectionPoint injectionPoint = segment.getInjectionPoint(); if (injectionPoint instanceof ArgumentCoercible) { diff --git a/inject/src/main/java/io/micronaut/context/exceptions/MessageUtils.java b/inject/src/main/java/io/micronaut/context/exceptions/MessageUtils.java index 9397f7be27b..8b094638e42 100644 --- a/inject/src/main/java/io/micronaut/context/exceptions/MessageUtils.java +++ b/inject/src/main/java/io/micronaut/context/exceptions/MessageUtils.java @@ -66,7 +66,7 @@ static String buildMessage(BeanResolutionContext resolutionContext, String messa } static String buildMessage(BeanResolutionContext resolutionContext, String message, boolean circular) { - BeanResolutionContext.Segment currentSegment = resolutionContext.getPath().peek(); + BeanResolutionContext.Segment currentSegment = resolutionContext.getPath().peek(); if (currentSegment instanceof AbstractBeanResolutionContext.ConstructorSegment) { return buildMessage(resolutionContext, currentSegment.getArgument(), message, circular); } diff --git a/inject/src/main/java/io/micronaut/inject/BeanDefinition.java b/inject/src/main/java/io/micronaut/inject/BeanDefinition.java index 44d29b2cad2..d6a00284d57 100644 --- a/inject/src/main/java/io/micronaut/inject/BeanDefinition.java +++ b/inject/src/main/java/io/micronaut/inject/BeanDefinition.java @@ -15,8 +15,6 @@ */ package io.micronaut.inject; -import io.micronaut.context.BeanContext; -import io.micronaut.context.BeanResolutionContext; import io.micronaut.context.Qualifier; import io.micronaut.context.annotation.ConfigurationReader; import io.micronaut.context.annotation.DefaultScope; @@ -177,10 +175,6 @@ default Optional> getDeclaringType() { */ default ConstructorInjectionPoint getConstructor() { return new ConstructorInjectionPoint() { - @Override - public T invoke(Object... args) { - throw new UnsupportedOperationException("Cannot be instantiated directly"); - } @Override public Argument[] getArguments() { @@ -192,10 +186,6 @@ public BeanDefinition getDeclaringBean() { return BeanDefinition.this; } - @Override - public boolean requiresReflection() { - return false; - } }; } @@ -274,29 +264,6 @@ default Stream> findPossibleMethods(String name) { return Stream.empty(); } - /** - * Inject the given bean with the context. - * - * @param context The context - * @param bean The bean - * @return The injected bean - */ - default T inject(BeanContext context, T bean) { - return bean; - } - - /** - * Inject the given bean with the context. - * - * @param resolutionContext the resolution context - * @param context The context - * @param bean The bean - * @return The injected bean - */ - default T inject(BeanResolutionContext resolutionContext, BeanContext context, T bean) { - return bean; - } - /** * @return The {@link ExecutableMethod} instances for this definition */ diff --git a/inject/src/main/java/io/micronaut/inject/BeanFactory.java b/inject/src/main/java/io/micronaut/inject/BeanFactory.java index b49019cf488..d63e9555727 100644 --- a/inject/src/main/java/io/micronaut/inject/BeanFactory.java +++ b/inject/src/main/java/io/micronaut/inject/BeanFactory.java @@ -19,6 +19,7 @@ import io.micronaut.context.BeanResolutionContext; import io.micronaut.context.DefaultBeanResolutionContext; import io.micronaut.context.exceptions.BeanInstantiationException; +import io.micronaut.core.annotation.NextMajorVersion; /** *

An interface for classes that are capable of taking the {@link BeanDefinition} instance and building a concrete @@ -31,6 +32,8 @@ * @see io.micronaut.inject.writer.BeanDefinitionWriter * @since 1.0 */ +@Deprecated(since = "4") +@NextMajorVersion("Should be removed after Micronaut 4 Milestone 1") public interface BeanFactory { /** diff --git a/inject/src/main/java/io/micronaut/inject/ConstructorInjectionPoint.java b/inject/src/main/java/io/micronaut/inject/ConstructorInjectionPoint.java index ba4eb5405dd..38562ca812b 100644 --- a/inject/src/main/java/io/micronaut/inject/ConstructorInjectionPoint.java +++ b/inject/src/main/java/io/micronaut/inject/ConstructorInjectionPoint.java @@ -16,7 +16,6 @@ package io.micronaut.inject; import io.micronaut.core.annotation.NonNull; -import io.micronaut.core.beans.BeanConstructor; /** * A constructor injection point. @@ -25,23 +24,10 @@ * @author Graeme Rocher * @since 1.0 */ -public interface ConstructorInjectionPoint extends CallableInjectionPoint, BeanConstructor { +public interface ConstructorInjectionPoint extends CallableInjectionPoint { - /** - * Invoke the constructor. - * - * @param args The arguments - * @return The new value - */ - T invoke(Object... args); - - @Override default @NonNull Class getDeclaringBeanType() { return getDeclaringBean().getBeanType(); } - @Override - default @NonNull T instantiate(Object... parameterValues) { - return invoke(parameterValues); - } } diff --git a/inject/src/main/java/io/micronaut/inject/DefaultBeanIdentifier.java b/inject/src/main/java/io/micronaut/inject/DefaultBeanIdentifier.java index 45c8dccc8ae..55074850e3b 100644 --- a/inject/src/main/java/io/micronaut/inject/DefaultBeanIdentifier.java +++ b/inject/src/main/java/io/micronaut/inject/DefaultBeanIdentifier.java @@ -78,7 +78,6 @@ public boolean equals(Object o) { @Override public int hashCode() { - - return Objects.hash(id); + return id.hashCode(); } } diff --git a/inject/src/main/java/io/micronaut/inject/DelegatingBeanDefinition.java b/inject/src/main/java/io/micronaut/inject/DelegatingBeanDefinition.java index 6a786b328d0..15d33dfdb29 100644 --- a/inject/src/main/java/io/micronaut/inject/DelegatingBeanDefinition.java +++ b/inject/src/main/java/io/micronaut/inject/DelegatingBeanDefinition.java @@ -134,16 +134,6 @@ default Stream> findPossibleMethods(String name) { return getTarget().findPossibleMethods(name); } - @Override - default T inject(BeanContext context, T bean) { - return getTarget().inject(context, bean); - } - - @Override - default T inject(BeanResolutionContext resolutionContext, BeanContext context, T bean) { - return getTarget().inject(resolutionContext, context, bean); - } - @Override default Collection> getExecutableMethods() { return getTarget().getExecutableMethods(); diff --git a/inject/src/main/java/io/micronaut/inject/FieldInjectionPoint.java b/inject/src/main/java/io/micronaut/inject/FieldInjectionPoint.java index 1d672ea75fe..be0f43c3013 100644 --- a/inject/src/main/java/io/micronaut/inject/FieldInjectionPoint.java +++ b/inject/src/main/java/io/micronaut/inject/FieldInjectionPoint.java @@ -17,6 +17,7 @@ import io.micronaut.core.annotation.AnnotatedElement; import io.micronaut.core.annotation.AnnotationMetadataProvider; +import io.micronaut.core.annotation.NextMajorVersion; import io.micronaut.core.type.ArgumentCoercible; import java.lang.reflect.Field; @@ -43,6 +44,8 @@ public interface FieldInjectionPoint extends InjectionPoint, Annotation * * @return The target field */ + @NextMajorVersion("Adjust Micronaut test to avoid this method") + @Deprecated(since = "4") Field getField(); /** @@ -50,12 +53,4 @@ public interface FieldInjectionPoint extends InjectionPoint, Annotation */ Class getType(); - /** - * Sets the value of the field. Note that this method will cause reflection - * metadata to be initialized and should be avoided. - * - * @param instance the instance - * @param object The the field on the target object - */ - void set(T instance, Object object); } diff --git a/inject/src/main/java/io/micronaut/inject/InjectableBeanDefinition.java b/inject/src/main/java/io/micronaut/inject/InjectableBeanDefinition.java new file mode 100644 index 00000000000..1c486d3257b --- /dev/null +++ b/inject/src/main/java/io/micronaut/inject/InjectableBeanDefinition.java @@ -0,0 +1,59 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.inject; + +import io.micronaut.context.BeanContext; +import io.micronaut.context.BeanResolutionContext; +import io.micronaut.context.DefaultBeanResolutionContext; +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.NonNull; + +/** + *

An type of {@link BeanDefinition} that supports post initialization bean dependencies injection.

+ * + * @param The bean type + * @author Denis Stepanov + * @since 4.0 + */ +@Internal +public interface InjectableBeanDefinition extends BeanDefinition { + + /** + * Inject the given bean with the context. + * + * @param context The context + * @param bean The bean + * @return The injected bean + */ + @NonNull + default T inject(@NonNull BeanContext context, @NonNull T bean) { + try (DefaultBeanResolutionContext resolutionContext = new DefaultBeanResolutionContext(context, this)) { + return inject(resolutionContext, context, bean); + } + } + + /** + * Inject the given bean with the context. + * + * @param resolutionContext the resolution context + * @param context The context + * @param bean The bean + * @return The injected bean + */ + @NonNull + T inject(@NonNull BeanResolutionContext resolutionContext, @NonNull BeanContext context, @NonNull T bean); + +} diff --git a/inject/src/main/java/io/micronaut/inject/InjectionPoint.java b/inject/src/main/java/io/micronaut/inject/InjectionPoint.java index 62fbb1988f5..9f45e4a7ebf 100644 --- a/inject/src/main/java/io/micronaut/inject/InjectionPoint.java +++ b/inject/src/main/java/io/micronaut/inject/InjectionPoint.java @@ -33,8 +33,4 @@ public interface InjectionPoint extends AnnotationMetadataProvider { */ @NonNull BeanDefinition getDeclaringBean(); - /** - * @return Whether reflection is required to satisfy the injection point - */ - boolean requiresReflection(); } diff --git a/inject/src/main/java/io/micronaut/inject/InstantiatableBeanDefinition.java b/inject/src/main/java/io/micronaut/inject/InstantiatableBeanDefinition.java new file mode 100644 index 00000000000..e1e3666aa4d --- /dev/null +++ b/inject/src/main/java/io/micronaut/inject/InstantiatableBeanDefinition.java @@ -0,0 +1,59 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.inject; + +import io.micronaut.context.BeanContext; +import io.micronaut.context.BeanResolutionContext; +import io.micronaut.context.DefaultBeanResolutionContext; +import io.micronaut.context.exceptions.BeanInstantiationException; +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.NonNull; + +/** + *

An type of {@link BeanDefinition} that can build a new instance.

+ * + * @param The bean type + * @author Denis Stepanov + * @since 4.0 + */ +@Internal +public interface InstantiatableBeanDefinition extends BeanDefinition { + + /** + * Builds a bean instance. + * + * @param context The context + * @return The instance + * @throws BeanInstantiationException if the instance could not be instantiated + */ + @NonNull + default T instantiate(@NonNull BeanContext context) throws BeanInstantiationException { + try (DefaultBeanResolutionContext resolutionContext = new DefaultBeanResolutionContext(context, this)) { + return instantiate(resolutionContext, context); + } + } + + /** + * Builds a bean instance. + * + * @param resolutionContext The bean resolution context + * @param context The context + * @return The instance + * @throws BeanInstantiationException if the instance could not be instantiated + */ + @NonNull + T instantiate(@NonNull BeanResolutionContext resolutionContext, @NonNull BeanContext context) throws BeanInstantiationException; +} diff --git a/inject/src/main/java/io/micronaut/inject/MethodInjectionPoint.java b/inject/src/main/java/io/micronaut/inject/MethodInjectionPoint.java index 278355bdaa3..52c91154c6a 100644 --- a/inject/src/main/java/io/micronaut/inject/MethodInjectionPoint.java +++ b/inject/src/main/java/io/micronaut/inject/MethodInjectionPoint.java @@ -15,9 +15,7 @@ */ package io.micronaut.inject; -import io.micronaut.core.type.Executable; - -import java.lang.reflect.Method; +import io.micronaut.core.annotation.AnnotationMetadataProvider; /** * Defines an injection point for a method. @@ -27,15 +25,7 @@ * @author Graeme Rocher * @since 1.0 */ -public interface MethodInjectionPoint extends CallableInjectionPoint, Executable { - - /** - * Resolves the {@link Method} instance. Note that this method will cause reflection - * metadata to be initialized and should be avoided. - * - * @return The setter to invoke to set said property - */ - Method getMethod(); +public interface MethodInjectionPoint extends CallableInjectionPoint, AnnotationMetadataProvider { /** * @return The method name @@ -52,17 +42,6 @@ public interface MethodInjectionPoint extends CallableInjectionPoint, E */ boolean isPostConstructMethod(); - /** - * Invokes the method. - * - * @param instance The instance - * @param args The arguments. Should match the types of getArguments() - * @return The new value - */ - @Override - T invoke(B instance, Object... args); - - @Override default Class getDeclaringType() { return getDeclaringBean().getBeanType(); } diff --git a/inject/src/main/java/io/micronaut/inject/ParametrizedBeanFactory.java b/inject/src/main/java/io/micronaut/inject/ParametrizedBeanFactory.java index 94ffa471d36..80fa4ddf78a 100644 --- a/inject/src/main/java/io/micronaut/inject/ParametrizedBeanFactory.java +++ b/inject/src/main/java/io/micronaut/inject/ParametrizedBeanFactory.java @@ -18,6 +18,7 @@ import io.micronaut.context.BeanContext; import io.micronaut.context.BeanResolutionContext; import io.micronaut.context.exceptions.BeanInstantiationException; +import io.micronaut.core.annotation.NextMajorVersion; import io.micronaut.core.type.Argument; import java.util.Map; @@ -29,6 +30,8 @@ * @author Graeme Rocher * @since 1.0 */ +@Deprecated(since = "4") +@NextMajorVersion("Should be removed after Micronaut 4 Milestone 1") public interface ParametrizedBeanFactory extends BeanFactory { /** diff --git a/inject/src/main/java/io/micronaut/inject/ParametrizedInstantiatableBeanDefinition.java b/inject/src/main/java/io/micronaut/inject/ParametrizedInstantiatableBeanDefinition.java new file mode 100644 index 00000000000..d828470b80c --- /dev/null +++ b/inject/src/main/java/io/micronaut/inject/ParametrizedInstantiatableBeanDefinition.java @@ -0,0 +1,64 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.inject; + +import io.micronaut.context.BeanContext; +import io.micronaut.context.BeanResolutionContext; +import io.micronaut.context.exceptions.BeanInstantiationException; +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.NonNull; +import io.micronaut.core.type.Argument; + +import java.util.Map; + +/** + *

An type of {@link BeanDefinition} that can build a new instance, construction requires additional (possibly user supplied) parameters in order construct a bean

+ * + * @param The bean type + * @author Denis Stepanov + * @since 4.0 + */ +@Internal +public interface ParametrizedInstantiatableBeanDefinition extends InstantiatableBeanDefinition { + + /** + * @return The arguments required to construct this bean + */ + @NonNull + Argument[] getRequiredArguments(); + + /** + * Variation of the {@link #instantiate(BeanContext)} method that allows passing the values necessary for + * successful bean construction. + * + * @param resolutionContext The {@link BeanResolutionContext} + * @param context The {@link BeanContext} + * @param requiredArgumentValues The required arguments values. The keys should match the names of the arguments + * returned by {@link #getRequiredArguments()} + * @return The instantiated bean + * @throws BeanInstantiationException If the bean cannot be instantiated for the arguments supplied + */ + @NonNull + T instantiate(@NonNull BeanResolutionContext resolutionContext, + @NonNull BeanContext context, + @NonNull Map requiredArgumentValues) throws BeanInstantiationException; + + @Override + @NonNull + default T instantiate(@NonNull BeanResolutionContext resolutionContext, @NonNull BeanContext context) throws BeanInstantiationException { + throw new BeanInstantiationException(this, "Cannot instantiate parametrized bean with no arguments"); + } +} diff --git a/inject/src/main/java/io/micronaut/inject/beans/AbstractInitializableBeanIntrospection.java b/inject/src/main/java/io/micronaut/inject/beans/AbstractInitializableBeanIntrospection.java index 5559ed42fc0..9cc9019c321 100644 --- a/inject/src/main/java/io/micronaut/inject/beans/AbstractInitializableBeanIntrospection.java +++ b/inject/src/main/java/io/micronaut/inject/beans/AbstractInitializableBeanIntrospection.java @@ -318,7 +318,7 @@ public boolean equals(Object o) { @Override public int hashCode() { - return Objects.hash(beanType); + return beanType.hashCode(); } @Override diff --git a/inject/src/main/java/io/micronaut/inject/provider/AbstractProviderDefinition.java b/inject/src/main/java/io/micronaut/inject/provider/AbstractProviderDefinition.java index 179c93e1cc2..69c2355040f 100644 --- a/inject/src/main/java/io/micronaut/inject/provider/AbstractProviderDefinition.java +++ b/inject/src/main/java/io/micronaut/inject/provider/AbstractProviderDefinition.java @@ -23,14 +23,18 @@ import io.micronaut.context.exceptions.BeanInstantiationException; import io.micronaut.context.exceptions.DisabledBeanException; import io.micronaut.context.exceptions.NoSuchBeanException; -import io.micronaut.core.annotation.*; +import io.micronaut.core.annotation.AnnotationMetadata; +import io.micronaut.core.annotation.AnnotationUtil; +import io.micronaut.core.annotation.Indexes; +import io.micronaut.core.annotation.NonNull; +import io.micronaut.core.annotation.Nullable; import io.micronaut.core.naming.Named; import io.micronaut.core.type.Argument; import io.micronaut.core.type.ArgumentCoercible; import io.micronaut.inject.BeanDefinition; import io.micronaut.inject.BeanDefinitionReference; -import io.micronaut.inject.BeanFactory; import io.micronaut.inject.InjectionPoint; +import io.micronaut.inject.InstantiatableBeanDefinition; import io.micronaut.inject.annotation.MutableAnnotationMetadata; import io.micronaut.inject.qualifiers.AnyQualifier; import io.micronaut.inject.qualifiers.Qualifiers; @@ -46,7 +50,7 @@ * @since 3.0.0 * @author graemerocher */ -public abstract class AbstractProviderDefinition implements BeanDefinition, BeanFactory, BeanDefinitionReference { +public abstract class AbstractProviderDefinition implements InstantiatableBeanDefinition, BeanDefinitionReference { private static final Argument TYPE_VARIABLE = Argument.ofTypeVariable(Object.class, "T"); private final AnnotationMetadata annotationMetadata; @@ -111,11 +115,8 @@ public boolean isPresent() { boolean singleton); @Override - public T build( - BeanResolutionContext resolutionContext, - BeanContext context, - BeanDefinition definition) throws BeanInstantiationException { - final BeanResolutionContext.Segment segment = resolutionContext.getPath().currentSegment().orElse(null); + public T instantiate(BeanResolutionContext resolutionContext, BeanContext context) throws BeanInstantiationException { + final BeanResolutionContext.Segment segment = resolutionContext.getPath().currentSegment().orElse(null); if (segment != null) { final InjectionPoint injectionPoint = segment.getInjectionPoint(); if (injectionPoint instanceof ArgumentCoercible) { @@ -146,7 +147,7 @@ public T build( context, argument, qualifier, - definition.isSingleton() + isSingleton() ); } else { if (injectionPointArgument.isOptional()) { @@ -160,7 +161,7 @@ public T build( context, argument, qualifier, - definition.isSingleton() + isSingleton() ); } else { throw new NoSuchBeanException(argument, qualifier); diff --git a/inject/src/main/java/io/micronaut/inject/qualifiers/AnnotationMetadataQualifier.java b/inject/src/main/java/io/micronaut/inject/qualifiers/AnnotationMetadataQualifier.java index 96ba24c0fbb..f997d92c3d2 100644 --- a/inject/src/main/java/io/micronaut/inject/qualifiers/AnnotationMetadataQualifier.java +++ b/inject/src/main/java/io/micronaut/inject/qualifiers/AnnotationMetadataQualifier.java @@ -25,6 +25,7 @@ import io.micronaut.core.naming.NameUtils; import io.micronaut.core.util.ArrayUtils; import io.micronaut.core.util.CollectionUtils; +import io.micronaut.core.util.ObjectUtils; import io.micronaut.inject.BeanDefinition; import io.micronaut.inject.BeanType; import io.micronaut.inject.DelegatingBeanDefinition; @@ -35,7 +36,6 @@ import java.util.HashMap; import java.util.HashSet; import java.util.Map; -import java.util.Objects; import java.util.Set; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -183,7 +183,7 @@ public boolean equals(Object o) { @Override public int hashCode() { - return Objects.hash(annotationName, qualifierAnn); + return ObjectUtils.hash(annotationName, qualifierAnn); } @Override diff --git a/inject/src/main/java/io/micronaut/inject/qualifiers/AnnotationStereotypeQualifier.java b/inject/src/main/java/io/micronaut/inject/qualifiers/AnnotationStereotypeQualifier.java index b11b5b1b85f..1b079356c40 100644 --- a/inject/src/main/java/io/micronaut/inject/qualifiers/AnnotationStereotypeQualifier.java +++ b/inject/src/main/java/io/micronaut/inject/qualifiers/AnnotationStereotypeQualifier.java @@ -20,7 +20,6 @@ import io.micronaut.inject.BeanType; import java.lang.annotation.Annotation; -import java.util.Objects; import java.util.stream.Stream; /** @@ -65,6 +64,6 @@ public boolean equals(Object o) { @Override public int hashCode() { - return Objects.hash(stereotype.getName()); + return stereotype.getName().hashCode(); } } diff --git a/inject/src/main/java/io/micronaut/inject/qualifiers/ExactTypeArgumentNameQualifier.java b/inject/src/main/java/io/micronaut/inject/qualifiers/ExactTypeArgumentNameQualifier.java index 6fcd47123e9..b9cd3fd8ec7 100644 --- a/inject/src/main/java/io/micronaut/inject/qualifiers/ExactTypeArgumentNameQualifier.java +++ b/inject/src/main/java/io/micronaut/inject/qualifiers/ExactTypeArgumentNameQualifier.java @@ -104,7 +104,7 @@ private String generify(String typeName) { @Override public int hashCode() { - return Objects.hash(generify(typeName)); + return generify(typeName).hashCode(); } @Override diff --git a/inject/src/main/java/io/micronaut/inject/qualifiers/InterceptorBindingQualifier.java b/inject/src/main/java/io/micronaut/inject/qualifiers/InterceptorBindingQualifier.java index fb93461b944..3639a6a36d0 100644 --- a/inject/src/main/java/io/micronaut/inject/qualifiers/InterceptorBindingQualifier.java +++ b/inject/src/main/java/io/micronaut/inject/qualifiers/InterceptorBindingQualifier.java @@ -23,6 +23,7 @@ import io.micronaut.core.annotation.NonNull; import io.micronaut.core.annotation.Nullable; import io.micronaut.core.util.CollectionUtils; +import io.micronaut.core.util.ObjectUtils; import io.micronaut.inject.BeanType; import java.lang.annotation.Annotation; @@ -32,7 +33,6 @@ import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.Objects; import java.util.Set; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -180,7 +180,7 @@ public boolean equals(Object o) { @Override public int hashCode() { - return Objects.hash(supportedAnnotationNames, supportedInterceptorTypes); + return ObjectUtils.hash(supportedAnnotationNames, supportedInterceptorTypes); } @Override @@ -202,7 +202,7 @@ public String toString() { return bindings .stream() .filter(av -> { - if (!av.stringValue().isPresent()) { + if (av.stringValue().isEmpty()) { return false; } if (kind == null) { diff --git a/inject/src/main/java/io/micronaut/inject/qualifiers/NamedAnnotationStereotypeQualifier.java b/inject/src/main/java/io/micronaut/inject/qualifiers/NamedAnnotationStereotypeQualifier.java index 4d573cec482..ed720e6851f 100644 --- a/inject/src/main/java/io/micronaut/inject/qualifiers/NamedAnnotationStereotypeQualifier.java +++ b/inject/src/main/java/io/micronaut/inject/qualifiers/NamedAnnotationStereotypeQualifier.java @@ -63,6 +63,6 @@ public boolean equals(Object o) { @Override public int hashCode() { - return Objects.hash(stereotype); + return stereotype.hashCode(); } } diff --git a/inject/src/main/java/io/micronaut/inject/qualifiers/RepeatableAnnotationQualifier.java b/inject/src/main/java/io/micronaut/inject/qualifiers/RepeatableAnnotationQualifier.java index 71054f2760e..35e99d8c09f 100644 --- a/inject/src/main/java/io/micronaut/inject/qualifiers/RepeatableAnnotationQualifier.java +++ b/inject/src/main/java/io/micronaut/inject/qualifiers/RepeatableAnnotationQualifier.java @@ -15,18 +15,18 @@ */ package io.micronaut.inject.qualifiers; +import io.micronaut.context.Qualifier; +import io.micronaut.core.annotation.AnnotationMetadata; +import io.micronaut.core.annotation.AnnotationValue; +import io.micronaut.core.util.ObjectUtils; +import io.micronaut.inject.BeanType; + import java.lang.annotation.Annotation; import java.util.Arrays; import java.util.Collections; import java.util.List; -import java.util.Objects; import java.util.stream.Stream; -import io.micronaut.context.Qualifier; -import io.micronaut.core.annotation.AnnotationMetadata; -import io.micronaut.core.annotation.AnnotationValue; -import io.micronaut.inject.BeanType; - /** * A qualifier for repeatable annotations. * @@ -80,6 +80,6 @@ public boolean equals(Object o) { @Override public int hashCode() { - return Objects.hash(repeatableValues, repeatableName); + return ObjectUtils.hash(repeatableValues, repeatableName); } } diff --git a/inject/src/test/groovy/io/micronaut/context/BeanDefinitionDelegateSpec.groovy b/inject/src/test/groovy/io/micronaut/context/BeanDefinitionDelegateSpec.groovy index 6898b3754c4..0035a165680 100644 --- a/inject/src/test/groovy/io/micronaut/context/BeanDefinitionDelegateSpec.groovy +++ b/inject/src/test/groovy/io/micronaut/context/BeanDefinitionDelegateSpec.groovy @@ -1,33 +1,13 @@ package io.micronaut.context import io.micronaut.context.annotation.Requires -import io.micronaut.core.annotation.AnnotationMetadata import io.micronaut.core.annotation.Order import io.micronaut.core.order.Ordered -import io.micronaut.core.type.Argument -import io.micronaut.inject.BeanDefinition -import spock.lang.Specification - import jakarta.inject.Singleton +import spock.lang.Specification class BeanDefinitionDelegateSpec extends Specification { - void "test type arguments are retrieved"() { - BeanDefinition beanDefinition = new AbstractBeanDefinition(String.class, AnnotationMetadata.EMPTY_METADATA, false) { - @Override - protected Map[]> getTypeArgumentsMap() { - [foo: [Argument.of(String)] as Argument[]] - } - } - - when: - BeanDefinition delegate = BeanDefinitionDelegate.create(beanDefinition) - - then: - delegate.getTypeArguments('foo').size() == 1 - delegate.getTypeArguments('foo')[0].getType() == String.class - } - void "test order"() { given: ApplicationContext ctx = ApplicationContext.run(["spec.name": getClass().simpleName]) diff --git a/inject/src/test/groovy/io/micronaut/context/DefaultFieldInjectionPointSpec.groovy b/inject/src/test/groovy/io/micronaut/context/DefaultFieldInjectionPointSpec.groovy deleted file mode 100644 index 5c7b1fa0d70..00000000000 --- a/inject/src/test/groovy/io/micronaut/context/DefaultFieldInjectionPointSpec.groovy +++ /dev/null @@ -1,35 +0,0 @@ -package io.micronaut.context - -import io.micronaut.core.annotation.AnnotationMetadata -import io.micronaut.core.type.Argument -import io.micronaut.inject.BeanDefinition -import spock.lang.Specification - -class DefaultFieldInjectionPointSpec extends Specification { - - void "test default field injection point reflective set"() { - given: - DefaultFieldInjectionPoint dfip = new DefaultFieldInjectionPoint( - Mock(BeanDefinition), - Foo, - String, - "bar", - AnnotationMetadata.EMPTY_METADATA, - Argument.ZERO_ARGUMENTS - ) - - when: - Foo foo = new Foo() - dfip.set(foo, "test") - - then: - dfip.field != null - dfip.name == 'bar' - dfip.type == String - foo.bar == 'test' - } - - class Foo { - String bar - } -} diff --git a/jackson-databind/src/main/java/io/micronaut/jackson/codec/JacksonFeatures.java b/jackson-databind/src/main/java/io/micronaut/jackson/codec/JacksonFeatures.java index cfd3f1bcda4..2726f30b293 100644 --- a/jackson-databind/src/main/java/io/micronaut/jackson/codec/JacksonFeatures.java +++ b/jackson-databind/src/main/java/io/micronaut/jackson/codec/JacksonFeatures.java @@ -22,6 +22,7 @@ import io.micronaut.core.annotation.Internal; import io.micronaut.core.annotation.NonNull; import io.micronaut.core.util.ArrayUtils; +import io.micronaut.core.util.ObjectUtils; import io.micronaut.json.JsonFeatures; import java.util.ArrayList; @@ -179,6 +180,6 @@ public boolean equals(Object o) { @Override public int hashCode() { - return Objects.hash(serializationFeatures, deserializationFeatures); + return ObjectUtils.hash(serializationFeatures, deserializationFeatures); } } diff --git a/router/src/main/java/io/micronaut/web/router/DefaultRouteBuilder.java b/router/src/main/java/io/micronaut/web/router/DefaultRouteBuilder.java index 2c50de09bd0..c7c23d6ddbb 100644 --- a/router/src/main/java/io/micronaut/web/router/DefaultRouteBuilder.java +++ b/router/src/main/java/io/micronaut/web/router/DefaultRouteBuilder.java @@ -27,6 +27,7 @@ import io.micronaut.core.convert.ConversionService; import io.micronaut.core.type.Argument; import io.micronaut.core.type.ReturnType; +import io.micronaut.core.util.ObjectUtils; import io.micronaut.core.util.StringUtils; import io.micronaut.http.HttpMethod; import io.micronaut.http.HttpRequest; @@ -634,7 +635,7 @@ public boolean equals(Object o) { @Override public int hashCode() { - return Objects.hash(consumesMediaTypes, producesMediaTypes); + return ObjectUtils.hash(consumesMediaTypes, producesMediaTypes); } } @@ -743,7 +744,7 @@ public boolean equals(Object o) { @Override public int hashCode() { - return Objects.hash(super.hashCode(), error, originatingClass); + return ObjectUtils.hash(super.hashCode(), error, originatingClass); } @Override @@ -862,7 +863,7 @@ public boolean equals(Object o) { @Override public int hashCode() { - return Objects.hash(super.hashCode(), status, originatingClass); + return ObjectUtils.hash(super.hashCode(), status, originatingClass); } } From 39a0c643123a3ff03a6029e0ad08b623a3c221ff Mon Sep 17 00:00:00 2001 From: Denis Stepanov Date: Mon, 19 Dec 2022 20:27:22 +0100 Subject: [PATCH 313/743] fix: delegating to intercepted method in Kotlin (#8498) --- .../micronaut/aop/writer/AopProxyWriter.java | 2 +- .../multiple/CoroutineCrudRepository.kt | 101 ++++++++++++++++++ .../suspend/multiple/CustomRepository.kt | 7 +- .../suspend/multiple/InterceptorSpec.kt | 12 +++ .../server/suspend/multiple/SomeEntity.kt | 4 + 5 files changed, 123 insertions(+), 3 deletions(-) create mode 100644 test-suite-kotlin/src/test/kotlin/io/micronaut/docs/server/suspend/multiple/CoroutineCrudRepository.kt create mode 100644 test-suite-kotlin/src/test/kotlin/io/micronaut/docs/server/suspend/multiple/SomeEntity.kt diff --git a/core-processor/src/main/java/io/micronaut/aop/writer/AopProxyWriter.java b/core-processor/src/main/java/io/micronaut/aop/writer/AopProxyWriter.java index 82c304f1c97..e51fca44bd5 100644 --- a/core-processor/src/main/java/io/micronaut/aop/writer/AopProxyWriter.java +++ b/core-processor/src/main/java/io/micronaut/aop/writer/AopProxyWriter.java @@ -692,7 +692,7 @@ private void buildMethodDelegate(MethodElement methodElement, MethodElement over GeneratorAdapter overriddenMethodGenerator = new GeneratorAdapter(overridden, ACC_PUBLIC, methodElement.getName(), desc); overriddenMethodGenerator.loadThis(); int i = 0; - for (ParameterElement param : methodElement.getSuspendParameters()) { + for (ParameterElement param : overriddenBy.getSuspendParameters()) { overriddenMethodGenerator.loadArg(i++); pushCastToType(overriddenMethodGenerator, param.getGenericType()); } diff --git a/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/server/suspend/multiple/CoroutineCrudRepository.kt b/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/server/suspend/multiple/CoroutineCrudRepository.kt new file mode 100644 index 00000000000..46583c3a2c0 --- /dev/null +++ b/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/server/suspend/multiple/CoroutineCrudRepository.kt @@ -0,0 +1,101 @@ +package io.micronaut.docs.server.suspend.multiple + +import kotlinx.coroutines.flow.Flow + +interface CoroutineCrudRepository { + + /** + * Saves the given valid entity, returning a possibly new entity representing the saved state. Note that certain implementations may not be able to detect whether a save or update should be performed and may always perform an insert. The [.update] method can be used in this case to explicitly request an update. + * + * @param entity The entity to save. Must not be null. + * @return The saved entity will never be null. + * @param The generic type + */ + suspend fun save(entity: S): S + + /** + * This method issues an explicit update for the given entity. The method differs from [.save] in that an update will be generated regardless if the entity has been saved previously or not. If the entity has no assigned ID then an exception will be thrown. + * + * @param entity The entity to save. Must not be null. + * @return The updated entity will never be null. + * @param The generic type + */ + suspend fun update(entity: S): S + + /** + * This method issues an explicit update for the given entities. The method differs from [.saveAll] in that an update will be generated regardless if the entity has been saved previously or not. If the entity has no assigned ID then an exception will be thrown. + * + * @param entities The entities to update. Must not be null. + * @return The updated entities will never be null. + * @param The generic type + */ + fun updateAll(entities: Iterable): Flow + + /** + * Saves all given entities, possibly returning new instances representing the saved state. + * + * @param entities The entities to saved. Must not be null. + * @param The generic type + * @return The saved entities objects. will never be null. + */ + fun saveAll(entities: Iterable): Flow + + /** + * Retrieves an entity by its id. + * + * @param id The ID of the entity to retrieve. Must not be null. + * @return the entity with the given id or none. + */ + suspend fun findById(id: ID): E? + + /** + * Returns whether an entity with the given id exists. + * + * @param id must not be null. + * @return true if an entity with the given id exists, false otherwise. + */ + suspend fun existsById(id: ID): Boolean + + /** + * Returns all instances of the type. + * + * @return all entities + */ + fun findAll(): Flow + + /** + * Returns the number of entities available. + * + * @return the number of entities + */ + suspend fun count(): Long + + /** + * Deletes the entity with the given id. + * + * @param id the id. + */ + suspend fun deleteById(id: ID): Int + + /** + * Deletes a given entity. + * + * @param entity The entity to delete + * @return the number of entities deleted + */ + suspend fun delete(entity: E): Int + + /** + * Deletes the given entities. + * + * @param entities The entities to delete + * @return the number of entities deleted + */ + suspend fun deleteAll(entities: Iterable): Int + + /** + * Deletes all entities managed by the repository. + * @return the number of entities deleted + */ + suspend fun deleteAll(): Int +} diff --git a/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/server/suspend/multiple/CustomRepository.kt b/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/server/suspend/multiple/CustomRepository.kt index 31c9a61bb10..375d95059d9 100644 --- a/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/server/suspend/multiple/CustomRepository.kt +++ b/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/server/suspend/multiple/CustomRepository.kt @@ -16,7 +16,10 @@ package io.micronaut.docs.server.suspend.multiple @MyRepository -interface CustomRepository { +interface CustomRepository : CoroutineCrudRepository { + + // As of Kotlin version 1.7.20 and KAPT, this will generate JVM signature: "SomeEntity findById(long id, continuation)" + override suspend fun findById(id: Long): SomeEntity? suspend fun xyz(): String @@ -26,4 +29,4 @@ interface CustomRepository { suspend fun count2(): String -} \ No newline at end of file +} diff --git a/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/server/suspend/multiple/InterceptorSpec.kt b/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/server/suspend/multiple/InterceptorSpec.kt index 15ed0132e20..1e3b97db72e 100644 --- a/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/server/suspend/multiple/InterceptorSpec.kt +++ b/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/server/suspend/multiple/InterceptorSpec.kt @@ -29,6 +29,8 @@ class InterceptorSpec : StringSpec() { private var myService = context.getBean(MyService::class.java) + private var repository = context.getBean(CustomRepository::class.java) + init { "test correct interceptors calls" { runBlocking { @@ -45,5 +47,15 @@ class InterceptorSpec : StringSpec() { MyService.events[7] shouldBe "repository-count2" } } + + "test calling generic method" { + runBlocking { + MyService.events.clear() + // Validate that no bytecode error is produced + repository.findById(111) + MyService.events.size shouldBeExactly 1 + MyService.events[0] shouldBe "repository-findById" + } + } } } diff --git a/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/server/suspend/multiple/SomeEntity.kt b/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/server/suspend/multiple/SomeEntity.kt new file mode 100644 index 00000000000..68b7aa896c9 --- /dev/null +++ b/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/server/suspend/multiple/SomeEntity.kt @@ -0,0 +1,4 @@ +package io.micronaut.docs.server.suspend.multiple + +class SomeEntity { +} From 579b28ecaf54ceb42744291dfa3c05673cd2748f Mon Sep 17 00:00:00 2001 From: Anurag Deshpande Date: Mon, 19 Dec 2022 17:15:20 -0700 Subject: [PATCH 314/743] doc: replace hardcoded Gradle plugin version with link to docs and releases (#8482) --- src/main/docs/guide/introduction/upgrading.adoc | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/main/docs/guide/introduction/upgrading.adoc b/src/main/docs/guide/introduction/upgrading.adoc index 632b4820f30..803a20fd7d2 100644 --- a/src/main/docs/guide/introduction/upgrading.adoc +++ b/src/main/docs/guide/introduction/upgrading.adoc @@ -109,9 +109,7 @@ If you use Maven, update the parent POM version and `micronaut.version` property === Build Plugin Update -If you use the Micronaut Gradle plugin, update the version to `2.0.3` - -`id("io.micronaut.application") version "2.0.3"` +If you use the https://micronaut-projects.github.io/micronaut-gradle-plugin/latest/[Micronaut Gradle plugin] update to the https://github.com/micronaut-projects/micronaut-gradle-plugin/releases/latest[latest version]. For Maven users the plugin version is updated automatically when you update the Micronaut version. From c60f3dc167976e63992f0c8ef5377ab50a123a74 Mon Sep 17 00:00:00 2001 From: micronaut-build Date: Tue, 20 Dec 2022 06:36:24 +0000 Subject: [PATCH 315/743] [skip ci] Release v3.7.5 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 68cbfb11dfe..06e22073264 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -projectVersion=3.7.5-SNAPSHOT +projectVersion=3.7.5 projectGroupId=io.micronaut title=Micronaut projectDesc=Natively Cloud Native From af6700c583f30eb443d8ede153ea79fbec5e1fd6 Mon Sep 17 00:00:00 2001 From: micronaut-build Date: Tue, 20 Dec 2022 06:47:25 +0000 Subject: [PATCH 316/743] Back to 3.7.6-SNAPSHOT --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 06e22073264..03c9dbfe0d9 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -projectVersion=3.7.5 +projectVersion=3.7.6-SNAPSHOT projectGroupId=io.micronaut title=Micronaut projectDesc=Natively Cloud Native From 0a9c1a15bb1313e9274907e89e43b79219e66115 Mon Sep 17 00:00:00 2001 From: micronaut-build <65172877+micronaut-build@users.noreply.github.com> Date: Tue, 20 Dec 2022 03:12:29 -0500 Subject: [PATCH 317/743] Bump micronaut-maven-plugin to 3.5.2 (#8501) --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 8efb3b302b4..ee40b95b54b 100644 --- a/gradle.properties +++ b/gradle.properties @@ -43,7 +43,7 @@ developers=Graeme Rocher kapt.use.worker.api=true # Dependency Versions -micronautMavenPluginVersion=3.5.1 +micronautMavenPluginVersion=3.5.2 chromedriverVersion=79.0.3945.36 geckodriverVersion=0.26.0 webdriverBinariesVersion=1.4 From 16b6e2a759fed852cfcc51e99a07d24615cb1c53 Mon Sep 17 00:00:00 2001 From: altro3 Date: Tue, 20 Dec 2022 19:23:25 +0700 Subject: [PATCH 318/743] Fix isReaderName for fields started with `_` or `$` (#8435) --- .../java/io/micronaut/core/naming/NameUtils.java | 13 +++++++------ .../io/micronaut/core/naming/NameUtilsSpec.groovy | 2 ++ 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/core/src/main/java/io/micronaut/core/naming/NameUtils.java b/core/src/main/java/io/micronaut/core/naming/NameUtils.java index ee42c730580..0b918523fbc 100644 --- a/core/src/main/java/io/micronaut/core/naming/NameUtils.java +++ b/core/src/main/java/io/micronaut/core/naming/NameUtils.java @@ -93,7 +93,7 @@ public static String capitalize(String name) { final String rest = name.substring(1); // Funky rule so that names like 'pNAME' will still work. - if (Character.isLowerCase(name.charAt(0)) && (rest.length() > 0) && Character.isUpperCase(rest.charAt(0))) { + if (Character.isLowerCase(name.charAt(0)) && (!rest.isEmpty()) && Character.isUpperCase(rest.charAt(0))) { return name; } @@ -135,7 +135,7 @@ public static String hyphenate(String name, boolean lowerCase) { public static String dehyphenate(String name) { StringBuilder sb = new StringBuilder(name.length()); for (String token : StringUtils.splitOmitEmptyStrings(name, '-')) { - if (token.length() > 0 && Character.isLetter(token.charAt(0))) { + if (!token.isEmpty() && Character.isLetter(token.charAt(0))) { sb.append(Character.toUpperCase(token.charAt(0))); sb.append(token.substring(1)); } else { @@ -229,7 +229,7 @@ public static boolean isWriterName(@NonNull String methodName, @NonNull String w public static boolean isWriterName(@NonNull String methodName, @NonNull String[] writePrefixes) { boolean isValid = false; for (String writePrefix : writePrefixes) { - if (writePrefix.length() == 0) { + if (writePrefix.isEmpty()) { return true; } int len = methodName.length(); @@ -360,7 +360,7 @@ public static boolean isReaderName(@NonNull String methodName, @NonNull String[] boolean isValid = false; for (String readPrefix : readPrefixes) { int prefixLength = 0; - if (readPrefix.length() == 0) { + if (readPrefix.isEmpty()) { return true; } else if (methodName.startsWith(readPrefix)) { prefixLength = readPrefix.length(); @@ -369,7 +369,8 @@ public static boolean isReaderName(@NonNull String methodName, @NonNull String[] } int len = methodName.length(); if (len > prefixLength) { - isValid = Character.isUpperCase(methodName.charAt(prefixLength)); + char firstVarNameChar = methodName.charAt(prefixLength); + isValid = firstVarNameChar == '_' || firstVarNameChar == '$' || Character.isUpperCase(firstVarNameChar); } if (isValid) { @@ -491,7 +492,7 @@ public static String getterNameFor(@NonNull String propertyName, boolean isBoole } private static String nameFor(String prefix, @NonNull String propertyName) { - if (prefix.length() == 0) { + if (prefix.isEmpty()) { return propertyName; } diff --git a/core/src/test/groovy/io/micronaut/core/naming/NameUtilsSpec.groovy b/core/src/test/groovy/io/micronaut/core/naming/NameUtilsSpec.groovy index 5941c2228c9..26e03fa0a0e 100644 --- a/core/src/test/groovy/io/micronaut/core/naming/NameUtilsSpec.groovy +++ b/core/src/test/groovy/io/micronaut/core/naming/NameUtilsSpec.groovy @@ -244,6 +244,8 @@ class NameUtilsSpec extends Specification { "getFoo" | true "getfoo" | false "a" | false + "get_foo" | true + 'get$foo' | true } @Unroll From 139621446b8360d5b2e00e23b18ce61fb7ab06be Mon Sep 17 00:00:00 2001 From: Denis Stepanov Date: Tue, 20 Dec 2022 13:23:47 +0100 Subject: [PATCH 319/743] Improve intercepted adapter tests (#8503) --- .../aop/adapter/intercepted/InterceptedAdapterSpec.groovy | 1 + .../groovy/io/micronaut/aop/compile/AroundCompileSpec.groovy | 1 + 2 files changed, 2 insertions(+) diff --git a/inject-java/src/test/groovy/io/micronaut/aop/adapter/intercepted/InterceptedAdapterSpec.groovy b/inject-java/src/test/groovy/io/micronaut/aop/adapter/intercepted/InterceptedAdapterSpec.groovy index 4fe53839a79..c1e948cac16 100644 --- a/inject-java/src/test/groovy/io/micronaut/aop/adapter/intercepted/InterceptedAdapterSpec.groovy +++ b/inject-java/src/test/groovy/io/micronaut/aop/adapter/intercepted/InterceptedAdapterSpec.groovy @@ -20,6 +20,7 @@ class InterceptedAdapterSpec extends AbstractTypeElementSpec { service.triggerEvent() then: + service.count == 1 interceptor.count == 1 cleanup: diff --git a/inject-java/src/test/groovy/io/micronaut/aop/compile/AroundCompileSpec.groovy b/inject-java/src/test/groovy/io/micronaut/aop/compile/AroundCompileSpec.groovy index 1793fce0d8c..7296d4f1803 100644 --- a/inject-java/src/test/groovy/io/micronaut/aop/compile/AroundCompileSpec.groovy +++ b/inject-java/src/test/groovy/io/micronaut/aop/compile/AroundCompileSpec.groovy @@ -1069,6 +1069,7 @@ class TransactionalEventInterceptor implements Interceptor { then: interceptor.count == 1 + service.count == 1 cleanup: context.close() From 4da31db215b9968226dae3c90b448544f0f2d6f8 Mon Sep 17 00:00:00 2001 From: micronaut-build <65172877+micronaut-build@users.noreply.github.com> Date: Tue, 20 Dec 2022 10:16:22 -0500 Subject: [PATCH 320/743] Bump micronaut-data to 3.9.1 (#8507) --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 07ac170c7b0..66ea420d390 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -72,7 +72,7 @@ managed-micronaut-cache = "3.5.0" managed-micronaut-cassandra = "5.1.1" managed-micronaut-coherence = "3.5.1" managed-micronaut-crac = "1.0.1" -managed-micronaut-data = "3.9.0" +managed-micronaut-data = "3.9.1" managed-micronaut-discovery = "3.2.0" managed-micronaut-elasticsearch = "4.3.0" managed-micronaut-email = "1.5.0" From 1a0e68a567d4de6ce3bbf3c6829b8e85707aeb89 Mon Sep 17 00:00:00 2001 From: micronaut-build <65172877+micronaut-build@users.noreply.github.com> Date: Tue, 20 Dec 2022 10:16:34 -0500 Subject: [PATCH 321/743] Bump micronaut-aws to 3.10.0 (#8505) --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 66ea420d390..0c4b860d1bd 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -66,7 +66,7 @@ managed-methvin-directory-watcher = "0.16.1" managed-micrometer = "1.9.4" managed-micronaut-acme = "3.2.0" managed-micronaut-aot = "1.1.1" -managed-micronaut-aws = "3.9.3" +managed-micronaut-aws = "3.10.0" managed-micronaut-azure = "3.6.0" managed-micronaut-cache = "3.5.0" managed-micronaut-cassandra = "5.1.1" From dbdfbe97e7e7493ae2792d07c5a62b0e3a7f4d75 Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Tue, 20 Dec 2022 16:17:06 +0100 Subject: [PATCH 322/743] build: bump native-maven-plugin to 0.9.19 (#8502) see: https://github.com/graalvm/native-build-tools/releases/tag/0.9.19 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 0c4b860d1bd..25de0cbf755 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -61,7 +61,7 @@ managed-kafka = "2.8.2" managed-ktor = "1.6.8" managed-logback = "1.2.11" managed-lombok = "1.18.24" -managed-maven-native-plugin = "0.9.18" +managed-maven-native-plugin = "0.9.19" managed-methvin-directory-watcher = "0.16.1" managed-micrometer = "1.9.4" managed-micronaut-acme = "3.2.0" From 9a87098329b3d80c2074413ab10f9607f5d14390 Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Tue, 20 Dec 2022 17:09:14 +0100 Subject: [PATCH 323/743] feat: Add HTTP Server TCK (#8499) --- gradle/libs.versions.toml | 4 + http-server-tck/build.gradle.kts | 28 ++ .../http/server/tck/AssertionUtils.java | 123 ++++++ .../server/tck/EmbeddedServerUnderTest.java | 89 +++++ .../tck/EmbeddedServerUnderTestProvider.java | 35 ++ .../server/tck/HttpResponseAssertion.java | 120 ++++++ .../http/server/tck/ServerUnderTest.java | 76 ++++ .../server/tck/ServerUnderTestProvider.java | 66 ++++ .../tck/ServerUnderTestProviderUtils.java | 49 +++ .../http/server/tck/TestScenario.java | 172 +++++++++ .../server/tck/tests/BodyArgumentTest.java | 63 +++ .../http/server/tck/tests/BodyTest.java | 166 ++++++++ .../http/server/tck/tests/ConsumesTest.java | 73 ++++ .../http/server/tck/tests/CookiesTest.java | 98 +++++ .../tck/tests/DeleteWithoutBodyTest.java | 64 ++++ .../server/tck/tests/ErrorHandlerTest.java | 362 ++++++++++++++++++ .../server/tck/tests/FilterErrorTest.java | 340 ++++++++++++++++ .../http/server/tck/tests/FiltersTest.java | 151 ++++++++ .../http/server/tck/tests/FluxTest.java | 60 +++ .../http/server/tck/tests/HelloWorldTest.java | 62 +++ .../http/server/tck/tests/MiscTest.java | 266 +++++++++++++ .../http/server/tck/tests/ParameterTest.java | 62 +++ .../server/tck/tests/RemoteAddressTest.java | 89 +++++ .../server/tck/tests/ResponseStatusTest.java | 125 ++++++ .../http/server/tck/tests/StatusTest.java | 107 ++++++ .../http/server/tck/tests/VersionTest.java | 68 ++++ settings.gradle | 7 +- .../build.gradle.kts | 9 + .../netty/tests/NettyHttpServerTestSuite.java | 11 + ...ut.http.server.tck.ServerUnderTestProvider | 1 + .../src/test/resources/logback.xml | 10 + 31 files changed, 2955 insertions(+), 1 deletion(-) create mode 100644 http-server-tck/build.gradle.kts create mode 100644 http-server-tck/src/main/java/io/micronaut/http/server/tck/AssertionUtils.java create mode 100644 http-server-tck/src/main/java/io/micronaut/http/server/tck/EmbeddedServerUnderTest.java create mode 100644 http-server-tck/src/main/java/io/micronaut/http/server/tck/EmbeddedServerUnderTestProvider.java create mode 100644 http-server-tck/src/main/java/io/micronaut/http/server/tck/HttpResponseAssertion.java create mode 100644 http-server-tck/src/main/java/io/micronaut/http/server/tck/ServerUnderTest.java create mode 100644 http-server-tck/src/main/java/io/micronaut/http/server/tck/ServerUnderTestProvider.java create mode 100644 http-server-tck/src/main/java/io/micronaut/http/server/tck/ServerUnderTestProviderUtils.java create mode 100644 http-server-tck/src/main/java/io/micronaut/http/server/tck/TestScenario.java create mode 100644 http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/BodyArgumentTest.java create mode 100644 http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/BodyTest.java create mode 100644 http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/ConsumesTest.java create mode 100644 http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/CookiesTest.java create mode 100644 http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/DeleteWithoutBodyTest.java create mode 100644 http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/ErrorHandlerTest.java create mode 100644 http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/FilterErrorTest.java create mode 100644 http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/FiltersTest.java create mode 100644 http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/FluxTest.java create mode 100644 http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/HelloWorldTest.java create mode 100644 http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/MiscTest.java create mode 100644 http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/ParameterTest.java create mode 100644 http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/RemoteAddressTest.java create mode 100644 http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/ResponseStatusTest.java create mode 100644 http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/StatusTest.java create mode 100644 http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/VersionTest.java create mode 100644 test-suite-http-server-tck-netty/build.gradle.kts create mode 100644 test-suite-http-server-tck-netty/src/test/java/io/micronaut/http/server/tck/netty/tests/NettyHttpServerTestSuite.java create mode 100644 test-suite-http-server-tck-netty/src/test/resources/META-INF/services/io.micronaut.http.server.tck.ServerUnderTestProvider create mode 100644 test-suite-http-server-tck-netty/src/test/resources/logback.xml diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 25de0cbf755..f5e8724ca59 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -36,6 +36,7 @@ managed-dekorate = "1.0.3" managed-elasticsearch = "7.16.3" managed-ignite = "2.13.0" managed-junit5 = "5.9.1" +managed-junit-platform="1.9.1" managed-kotlin = "1.6.21" managed-kotlin-coroutines = "1.5.1" managed-google-function-framework = "1.0.4" @@ -401,6 +402,9 @@ jsr107 = { module = "org.jsr107.ri:cache-ri-impl", version.ref = "jsr107" } junit-jupiter-api = { module = "org.junit.jupiter:junit-jupiter-api", version.ref = "managed-junit5" } junit-jupiter-engine = { module = "org.junit.jupiter:junit-jupiter-engine", version.ref = "managed-junit5" } +junit-jupiter-params = { module = "org.junit.jupiter:junit-jupiter-params", version.ref = "managed-junit5" } +junit-platform-engine = { module = "org.junit.platform:junit-platform-suite-engine", version.ref = "managed-junit-platform" } + junit-vintage = { module = "org.junit.vintage:junit-vintage-engine", version.ref = "managed-junit5" } jetbrains-annotations = { module = "org.jetbrains:annotations", version.ref = "jetbrains-annotations" } diff --git a/http-server-tck/build.gradle.kts b/http-server-tck/build.gradle.kts new file mode 100644 index 00000000000..f8a0ff7a35b --- /dev/null +++ b/http-server-tck/build.gradle.kts @@ -0,0 +1,28 @@ +plugins { + id("io.micronaut.build.internal.convention-library") +} +repositories { + mavenCentral() +} + +dependencies { + annotationProcessor(projects.injectJava) + annotationProcessor(projects.validation) + implementation(projects.validation) + implementation(projects.runtime) + implementation(projects.inject) + api(projects.httpServer) + api(libs.junit.jupiter.api) + api(libs.junit.jupiter.params) + api(libs.managed.reactor) +} + +java { + sourceCompatibility = JavaVersion.toVersion("1.8") + targetCompatibility = JavaVersion.toVersion("1.8") +} +micronautBuild { + binaryCompatibility { + enabled.set(false) + } +} diff --git a/http-server-tck/src/main/java/io/micronaut/http/server/tck/AssertionUtils.java b/http-server-tck/src/main/java/io/micronaut/http/server/tck/AssertionUtils.java new file mode 100644 index 00000000000..e3a46348a01 --- /dev/null +++ b/http-server-tck/src/main/java/io/micronaut/http/server/tck/AssertionUtils.java @@ -0,0 +1,123 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.http.server.tck; + +import io.micronaut.core.annotation.Experimental; +import io.micronaut.core.annotation.NonNull; +import io.micronaut.core.annotation.Nullable; +import io.micronaut.http.HttpHeaders; +import io.micronaut.http.HttpRequest; +import io.micronaut.http.HttpResponse; +import io.micronaut.http.HttpStatus; +import io.micronaut.http.client.exceptions.HttpClientResponseException; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.function.Executable; +import org.junit.jupiter.api.function.ThrowingSupplier; + +import java.util.Map; +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Utility class used to perform assertions. + * @author Sergio del Amo + * @since 3.8.0 + */ +@SuppressWarnings({ + "java:S5960", // We're allowed assertions, as these are used in tests only +}) +@Experimental +public final class AssertionUtils { + + private AssertionUtils() { + + } + + public static void assertThrows(@NonNull ServerUnderTest server, + @NonNull HttpRequest request, + @NonNull HttpResponseAssertion assertion) { + assertThrows(server, request, assertion.getHttpStatus(), assertion.getBody(), assertion.getHeaders()); + } + + public static void assertThrows(@NonNull ServerUnderTest server, + @NonNull HttpRequest request, + @NonNull HttpStatus expectedStatus, + @Nullable String expectedBody, + @Nullable Map expectedHeaders) { + Executable e = expectedBody != null ? + () -> server.exchange(request, String.class) : + () -> server.exchange(request); + HttpClientResponseException thrown = Assertions.assertThrows(HttpClientResponseException.class, e); + HttpResponse response = thrown.getResponse(); + assertEquals(expectedStatus, response.getStatus()); + assertHeaders(response, expectedHeaders); + assertBody(response, expectedBody); + } + + public static void assertDoesNotThrow(@NonNull ServerUnderTest server, + @NonNull HttpRequest request, + @NonNull HttpResponseAssertion assertion) { + assertDoesNotThrow(server, request, assertion.getHttpStatus(), assertion.getBody(), assertion.getHeaders()); + } + + public static void assertDoesNotThrow(@NonNull ServerUnderTest server, + @NonNull HttpRequest request, + @NonNull HttpStatus expectedStatus, + @Nullable String expectedBody, + @Nullable Map expectedHeaders) { + ThrowingSupplier> executable = expectedBody != null ? + () -> server.exchange(request, String.class) : + () -> server.exchange(request); + HttpResponse response = Assertions.assertDoesNotThrow(executable); + assertEquals(expectedStatus, response.getStatus()); + assertHeaders(response, expectedHeaders); + assertBody(response, expectedBody); + } + + private static void assertBody(@NonNull HttpResponse response, @Nullable String expectedBody) { + if (expectedBody != null) { + Optional bodyOptional = response.getBody(String.class); + assertTrue(bodyOptional.isPresent()); + bodyOptional.ifPresent(body -> assertTrue(body.contains(expectedBody))); + } + } + + private static void assertHeaders(@NonNull HttpResponse response, @Nullable Map expectedHeaders) { + + if (expectedHeaders != null) { + for (Map.Entry expectedHeadersEntrySet : expectedHeaders.entrySet()) { + String headerName = expectedHeadersEntrySet.getKey(); + Optional headerOptional = response.getHeaders().getFirst(headerName); + assertTrue(headerOptional.isPresent(), () -> "Header " + headerName + " not present"); + headerOptional.ifPresent(headerValue -> { + String expectedValue = expectedHeadersEntrySet.getValue(); + if (headerName.equals(HttpHeaders.CONTENT_TYPE)) { + if (headerValue.contains(";charset=")) { + assertTrue(headerValue.startsWith(expectedValue), () -> "header value " + headerValue + " does not start with " + expectedValue); + } else { + assertEquals(expectedValue, headerOptional.get()); + } + } else { + assertEquals(expectedValue, headerOptional.get()); + } + }); + } + + } + } + +} diff --git a/http-server-tck/src/main/java/io/micronaut/http/server/tck/EmbeddedServerUnderTest.java b/http-server-tck/src/main/java/io/micronaut/http/server/tck/EmbeddedServerUnderTest.java new file mode 100644 index 00000000000..115feccf611 --- /dev/null +++ b/http-server-tck/src/main/java/io/micronaut/http/server/tck/EmbeddedServerUnderTest.java @@ -0,0 +1,89 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.http.server.tck; + +import io.micronaut.context.ApplicationContext; +import io.micronaut.core.annotation.Experimental; +import io.micronaut.core.annotation.NonNull; +import io.micronaut.core.type.Argument; +import io.micronaut.http.HttpRequest; +import io.micronaut.http.HttpResponse; +import io.micronaut.http.client.BlockingHttpClient; +import io.micronaut.http.client.HttpClient; +import io.micronaut.runtime.server.EmbeddedServer; + +import java.io.IOException; +import java.util.Map; +import java.util.Optional; + +/** + * {@link ServerUnderTest} implementation for {@link EmbeddedServer}. + * @author Sergio del Amo + * @since 3.0.0 + */ +@Experimental +public class EmbeddedServerUnderTest implements ServerUnderTest { + + private EmbeddedServer embeddedServer; + private HttpClient httpClient; + private BlockingHttpClient client; + + public EmbeddedServerUnderTest(@NonNull Map properties) { + this.embeddedServer = ApplicationContext.run(EmbeddedServer.class, properties); + } + + @Override + public HttpResponse exchange(HttpRequest request, Argument bodyType) { + return getBlockingHttpClient().exchange(request, bodyType); + } + + @Override + public ApplicationContext getApplicationContext() { + return embeddedServer.getApplicationContext(); + } + + @Override + public void close() throws IOException { + if (httpClient != null) { + httpClient.close(); + } + if (embeddedServer != null) { + embeddedServer.close(); + } + } + + @Override + @NonNull + public Optional getPort() { + return Optional.ofNullable(embeddedServer).map(EmbeddedServer::getPort); + } + + @NonNull + private HttpClient getHttpClient() { + if (httpClient == null) { + this.httpClient = getApplicationContext().createBean(HttpClient.class, embeddedServer.getURL()); + } + return httpClient; + } + + @NonNull + private BlockingHttpClient getBlockingHttpClient() { + if (client == null) { + this.client = getHttpClient().toBlocking(); + } + return client; + } +} diff --git a/http-server-tck/src/main/java/io/micronaut/http/server/tck/EmbeddedServerUnderTestProvider.java b/http-server-tck/src/main/java/io/micronaut/http/server/tck/EmbeddedServerUnderTestProvider.java new file mode 100644 index 00000000000..f6708f64620 --- /dev/null +++ b/http-server-tck/src/main/java/io/micronaut/http/server/tck/EmbeddedServerUnderTestProvider.java @@ -0,0 +1,35 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.http.server.tck; + +import io.micronaut.core.annotation.Experimental; +import io.micronaut.core.annotation.NonNull; + +import java.util.Map; + +/** + * {@link ServerUnderTestProvider} implemntation which returns an instance of {@link EmbeddedServerUnderTest}. + * @author Sergio del Amo + * @since 3.8.0 + */ +@Experimental +public class EmbeddedServerUnderTestProvider implements ServerUnderTestProvider { + @Override + @NonNull + public ServerUnderTest getServer(@NonNull Map properties) { + return new EmbeddedServerUnderTest(properties); + } +} diff --git a/http-server-tck/src/main/java/io/micronaut/http/server/tck/HttpResponseAssertion.java b/http-server-tck/src/main/java/io/micronaut/http/server/tck/HttpResponseAssertion.java new file mode 100644 index 00000000000..690a43c7dd0 --- /dev/null +++ b/http-server-tck/src/main/java/io/micronaut/http/server/tck/HttpResponseAssertion.java @@ -0,0 +1,120 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.http.server.tck; + +import io.micronaut.core.annotation.Experimental; +import io.micronaut.http.HttpStatus; + +import java.util.Map; +import java.util.Objects; + +/** + * Utility class to verify assertions given an HTTP Response. + * @author Sergio del Amo + * @since 3.8.0 + */ +@Experimental +public final class HttpResponseAssertion { + + private final HttpStatus httpStatus; + private final Map headers; + private final String body; + + private HttpResponseAssertion(HttpStatus httpStatus, Map headers, String body) { + this.httpStatus = httpStatus; + this.headers = headers; + this.body = body; + } + + /** + * + * @return Expected HTTP Response Status + */ + public HttpStatus getHttpStatus() { + return httpStatus; + } + + /** + * + * @return Expected HTTP Response Headers + */ + public Map getHeaders() { + return headers; + } + + /** + * + * @return Expected HTTP Response body + */ + public String getBody() { + return body; + } + + /** + * + * @return Creates an instance of {@link HttpResponseAssertion.Builder}. + */ + public static HttpResponseAssertion.Builder builder() { + return new HttpResponseAssertion.Builder(); + } + + /** + * HTTP Response Assertion Builder. + */ + public static class Builder { + private HttpStatus httpStatus; + private Map headers; + private String body; + + /** + * + * @param headers HTTP Headers + * @return HTTP Response Assertion Builder + */ + public Builder headers(Map headers) { + this.headers = headers; + return this; + } + + /** + * + * @param body Response Body + * @return HTTP Response Assertion Builder + */ + public Builder body(String body) { + this.body = body; + return this; + } + + /** + * + * @param httpStatus Response's HTTP Status + * @return HTTP Response Assertion Builder + */ + public Builder status(HttpStatus httpStatus) { + this.httpStatus = httpStatus; + return this; + } + + /** + * + * @return HTTP Response Assertion + */ + public HttpResponseAssertion build() { + return new HttpResponseAssertion(Objects.requireNonNull(httpStatus), headers, body); + } + } +} diff --git a/http-server-tck/src/main/java/io/micronaut/http/server/tck/ServerUnderTest.java b/http-server-tck/src/main/java/io/micronaut/http/server/tck/ServerUnderTest.java new file mode 100644 index 00000000000..0712fc9633b --- /dev/null +++ b/http-server-tck/src/main/java/io/micronaut/http/server/tck/ServerUnderTest.java @@ -0,0 +1,76 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.http.server.tck; + +import io.micronaut.context.ApplicationContextProvider; +import io.micronaut.core.annotation.Experimental; +import io.micronaut.core.annotation.NonNull; +import io.micronaut.core.type.Argument; +import io.micronaut.http.HttpRequest; +import io.micronaut.http.HttpResponse; + +import java.io.Closeable; +import java.util.Optional; + +/** + * An API for a Micronaut HTTP Server under test. An implementation can be Netty or AWS Lambda Handler. + * @author Sergio del Amo + * @since 1.8.0 + */ +@Experimental +public interface ServerUnderTest extends ApplicationContextProvider, Closeable, AutoCloseable { + + /* + * Perform an HTTP request for the given request against the server under test and returns the the full HTTP response + * @param request The {@link HttpRequest} to execute + * @param The request body type + * @param The response body type + * @return The full {@link HttpResponse} object + * @throws HttpClientResponseException when an error status is returned + */ + default HttpResponse exchange(HttpRequest request) { + return exchange(request, (Argument) null); + } + + /* + * Perform an HTTP request for the given request against the server under test and returns the full HTTP response + * @param request The {@link HttpRequest} to execute + * @param bodyType The body type + * @param The request body type + * @param The response body type + * @return The full {@link HttpResponse} object + * @throws HttpClientResponseException when an error status is returned + */ + default HttpResponse exchange(HttpRequest request, Class bodyType) { + return exchange(request, Argument.of(bodyType)); + } + + /* + * Perform an HTTP request for the given request against the server under test and returns the full HTTP response + * @param request The {@link HttpRequest} to execute + * @param bodyType The body type + * @param The request body type + * @param The response body type + * @return The full {@link HttpResponse} object + * @throws HttpClientResponseException when an error status is returned + */ + HttpResponse exchange(HttpRequest request, Argument bodyType); + + @NonNull + default Optional getPort() { + return Optional.empty(); + } +} diff --git a/http-server-tck/src/main/java/io/micronaut/http/server/tck/ServerUnderTestProvider.java b/http-server-tck/src/main/java/io/micronaut/http/server/tck/ServerUnderTestProvider.java new file mode 100644 index 00000000000..2ea500fb918 --- /dev/null +++ b/http-server-tck/src/main/java/io/micronaut/http/server/tck/ServerUnderTestProvider.java @@ -0,0 +1,66 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.http.server.tck; + +import io.micronaut.core.annotation.Experimental; +import io.micronaut.core.annotation.NonNull; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +/** + * Provides a server to test. + * @author Sergio del Amo + * @since 3.8.0 + */ +@Experimental +@FunctionalInterface +public interface ServerUnderTestProvider { + + /** + * + * @param properties Properties supplied to application context started. + * @return The server under test. + */ + @NonNull + ServerUnderTest getServer(Map properties); + + /** + * + * @param specName value of {@literal spec.name} property used to avoid bean pollution. + * @param properties Properties supplied to application context started. + * @return Server under test + */ + @NonNull + default ServerUnderTest getServer(String specName, Map properties) { + Map props = properties != null ? new HashMap<>(properties) : new HashMap<>(); + if (specName != null) { + props.put("spec.name", specName); + } + return getServer(props); + } + + /** + * + * @param specName value of {@literal spec.name} property used to avoid bean pollution. + * @return Server under test + */ + @NonNull + default ServerUnderTest getServer(String specName) { + return getServer(specName, Collections.emptyMap()); + } +} diff --git a/http-server-tck/src/main/java/io/micronaut/http/server/tck/ServerUnderTestProviderUtils.java b/http-server-tck/src/main/java/io/micronaut/http/server/tck/ServerUnderTestProviderUtils.java new file mode 100644 index 00000000000..ff9bdede0a8 --- /dev/null +++ b/http-server-tck/src/main/java/io/micronaut/http/server/tck/ServerUnderTestProviderUtils.java @@ -0,0 +1,49 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.http.server.tck; + +import io.micronaut.context.exceptions.ConfigurationException; +import io.micronaut.core.annotation.Experimental; +import io.micronaut.core.annotation.NonNull; + +import java.util.Iterator; +import java.util.ServiceLoader; + +/** + * Utility class to retrieve a {@link ServerUnderTestProvider} via a Service Loader. + * @author Sergio del Amo + * @since 3.8.0 + */ +@Experimental +public final class ServerUnderTestProviderUtils { + + private ServerUnderTestProviderUtils() { + } + + /** + * + * @return The first {@link ServerUnderTestProvider} loaded via a Service loader. + * @throws ConfigurationException if it cannot load any {@link ServerUnderTestProvider}. + */ + @NonNull + public static ServerUnderTestProvider getServerUnderTestProvider() { + Iterator it = ServiceLoader.load(ServerUnderTestProvider.class).iterator(); + if (it.hasNext()) { + return it.next(); + } + throw new ConfigurationException("No ServiceUnderTestProvider present"); + } +} diff --git a/http-server-tck/src/main/java/io/micronaut/http/server/tck/TestScenario.java b/http-server-tck/src/main/java/io/micronaut/http/server/tck/TestScenario.java new file mode 100644 index 00000000000..dc2d4ccafe6 --- /dev/null +++ b/http-server-tck/src/main/java/io/micronaut/http/server/tck/TestScenario.java @@ -0,0 +1,172 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.http.server.tck; + +import io.micronaut.core.annotation.Experimental; +import io.micronaut.http.HttpRequest; +import java.io.IOException; +import java.util.Map; +import java.util.Objects; +import java.util.function.BiConsumer; + +/** + * Defines a HTTP Server Test Scenario. + * @author Sergio del Amo + * @since 3.8.0 + */ +@Experimental +public final class TestScenario { + private final String specName; + private final Map configuration; + + private final HttpRequest request; + private final BiConsumer> assertion; + + private TestScenario(String specName, + Map configuration, + HttpRequest request, + BiConsumer> assertion) { + this.specName = specName; + this.configuration = configuration; + this.request = request; + this.assertion = assertion; + } + + /** + * + * @param specName Value for {@literal spec.name} property. Used to avoid bean pollution. + * @param configuration Test Scenario configuration + * @param request HTTP Request to be sent in the test scenario + * @param assertion Assertion for a request and server. + * @throws IOException Exception thrown while getting the server under test. + */ + public static void asserts(String specName, + Map configuration, + HttpRequest request, + BiConsumer> assertion) throws IOException { + TestScenario.builder() + .specName(specName) + .configuration(configuration) + .request(request) + .assertion(assertion) + .run(); + } + + /** + * + * @param specName Value for {@literal spec.name} property. Used to avoid bean pollution. + * @param request HTTP Request to be sent in the test scenario + * @param assertion Assertion for a request and server. + * @throws IOException Exception thrown while getting the server under test. + */ + public static void asserts(String specName, + HttpRequest request, + BiConsumer> assertion) throws IOException { + TestScenario.builder() + .specName(specName) + .request(request) + .assertion(assertion) + .run(); + } + + /** + * + * @return A Test Scenario builder. + */ + public static TestScenario.Builder builder() { + return new Builder(); + } + + private void run() throws IOException { + try (ServerUnderTest server = ServerUnderTestProviderUtils.getServerUnderTestProvider().getServer(specName, configuration)) { + if (assertion != null) { + assertion.accept(server, request); + } + } + } + + /** + * Test Scenario Builder. + */ + public static class Builder { + + private Map configuration; + + private String specName; + + private BiConsumer> assertion; + + private HttpRequest request; + + /** + * + * @param specName Value for {@literal spec.name} property. Used to avoid bean pollution. + * @return Test Scenario builder + */ + public Builder specName(String specName) { + this.specName = specName; + return this; + } + + /** + * + * @param request HTTP Request to be sent in the test scenario + * @return The Test Scneario Builder + */ + public Builder request(HttpRequest request) { + this.request = request; + return this; + } + + /** + * + * @param configuration Test Scenario configuration + * @return Test scenario builder + */ + public Builder configuration(Map configuration) { + this.configuration = configuration; + return this; + } + + /** + * + * @param assertion Assertion for a request and server. + * @return The Test Scenario Builder + */ + public Builder assertion(BiConsumer> assertion) { + this.assertion = assertion; + return this; + } + + /** + * + * @return Builds a Test scenario + */ + private TestScenario build() { + return new TestScenario(specName, configuration, + Objects.requireNonNull(request), + Objects.requireNonNull(assertion)); + } + + /** + * Runs the Test Scneario. + * @throws IOException Exception thrown while getting the server under test. + */ + public void run() throws IOException { + build().run(); + } + } +} diff --git a/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/BodyArgumentTest.java b/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/BodyArgumentTest.java new file mode 100644 index 00000000000..1923451f202 --- /dev/null +++ b/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/BodyArgumentTest.java @@ -0,0 +1,63 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.http.server.tck.tests; + +import io.micronaut.context.annotation.Requires; +import io.micronaut.http.HttpHeaders; +import io.micronaut.http.HttpRequest; +import io.micronaut.http.HttpStatus; +import io.micronaut.http.MediaType; +import io.micronaut.http.annotation.Controller; +import io.micronaut.http.annotation.Post; +import io.micronaut.http.annotation.Produces; +import io.micronaut.http.server.tck.AssertionUtils; +import io.micronaut.http.server.tck.HttpResponseAssertion; +import org.junit.jupiter.api.Test; +import java.io.IOException; +import static io.micronaut.http.server.tck.TestScenario.asserts; + +@SuppressWarnings({ + "java:S5960", // We're allowed assertions, as these are used in tests only + "checkstyle:MissingJavadocType", + "checkstyle:DesignForExtension" +}) +public class BodyArgumentTest { + public static final String SPEC_NAME = "BodyArgumentTest"; + + /** + * @see micronaut-aws #1164 + */ + @Test + void testBodyArguments() throws IOException { + asserts(SPEC_NAME, + HttpRequest.POST("/body-arguments-test/getA", "{\"a\":\"A\",\"b\":\"B\"}").header(HttpHeaders.ACCEPT, MediaType.TEXT_PLAIN), + (server, request) -> AssertionUtils.assertDoesNotThrow(server, request, HttpResponseAssertion.builder() + .status(HttpStatus.OK) + .body("A") + .build())); + } + + @Controller("/body-arguments-test") + @Requires(property = "spec.name", value = SPEC_NAME) + static class BodyController { + + @Post(uri = "/getA") + @Produces(MediaType.TEXT_PLAIN) + String getA(String a) { + return a; + } + } +} diff --git a/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/BodyTest.java b/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/BodyTest.java new file mode 100644 index 00000000000..27c28cde8cf --- /dev/null +++ b/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/BodyTest.java @@ -0,0 +1,166 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.http.server.tck.tests; + +import io.micronaut.context.annotation.Requires; +import io.micronaut.core.async.annotation.SingleResult; +import io.micronaut.http.HttpHeaders; +import io.micronaut.http.HttpRequest; +import io.micronaut.http.HttpStatus; +import io.micronaut.http.MediaType; +import io.micronaut.http.annotation.Body; +import io.micronaut.http.annotation.Controller; +import io.micronaut.http.annotation.Post; +import io.micronaut.http.annotation.Status; +import io.micronaut.http.server.tck.AssertionUtils; +import io.micronaut.http.server.tck.HttpResponseAssertion; +import static io.micronaut.http.server.tck.TestScenario.asserts; +import org.junit.jupiter.api.Test; +import org.reactivestreams.Publisher; + +import java.io.IOException; +import java.util.Objects; + +@SuppressWarnings({ + "java:S5960", // We're allowed assertions, as these are used in tests only + "checkstyle:MissingJavadocType", + "checkstyle:DesignForExtension" +}) +public class BodyTest { + public static final String SPEC_NAME = "BodyTest"; + + @Test + void testCustomBodyPOJO() throws IOException { + asserts(SPEC_NAME, + HttpRequest.POST("/response-body/pojo", "{\"x\":10,\"y\":20}") + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON), + (server, request) -> AssertionUtils.assertDoesNotThrow(server, request, + HttpResponseAssertion.builder() + .status(HttpStatus.CREATED) + .body("{\"x\":10,\"y\":20}") + .build())); + } + + @Test + void testCustomBodyPOJODefaultToJSON() throws IOException { + asserts(SPEC_NAME, + HttpRequest.POST("/response-body/pojo", "{\"x\":10,\"y\":20}"), + (server, request) -> AssertionUtils.assertDoesNotThrow(server, request, + HttpResponseAssertion.builder() + .status(HttpStatus.CREATED) + .body("{\"x\":10,\"y\":20}") + .build())); + } + + @Test + void testCustomBodyPOJOWithWholeRequest() throws IOException { + asserts(SPEC_NAME, + HttpRequest.POST("/response-body/pojo-and-request", "{\"x\":10,\"y\":20}") + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON), + (server, request) -> HttpResponseAssertion.builder() + .status(HttpStatus.CREATED) + .body("{\"x\":10,\"y\":20}") + .build()); + } + + @Test + void testCustomBodyPOJOReactiveTypes() throws IOException { + asserts(SPEC_NAME, + HttpRequest.POST("/response-body/pojo-reactive", "{\"x\":10,\"y\":20}") + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON), + (server, request) -> AssertionUtils.assertDoesNotThrow(server, request, + HttpResponseAssertion.builder() + .status(HttpStatus.CREATED) + .body("{\"x\":10,\"y\":20}") + .build())); + } + + @Controller("/response-body") + @Requires(property = "spec.name", value = SPEC_NAME) + static class BodyController { + + @Post(uri = "/pojo") + @Status(HttpStatus.CREATED) + Point post(@Body Point data) { + return data; + } + + @Post(uri = "/pojo-and-request") + @Status(HttpStatus.CREATED) + Point postRequest(HttpRequest request) { + return request.getBody().orElse(null); + } + + @Post(uri = "/pojo-reactive") + @Status(HttpStatus.CREATED) + @SingleResult + Publisher post(@Body Publisher data) { + return data; + } + + @Post(uri = "/bytes", consumes = MediaType.TEXT_PLAIN) + @Status(HttpStatus.CREATED) + String postBytes(@Body byte[] bytes) { + return new String(bytes); + } + } + + static class Point { + private Integer x; + private Integer y; + + public Integer getX() { + return x; + } + + public void setX(Integer x) { + this.x = x; + } + + public Integer getY() { + return y; + } + + public void setY(Integer y) { + this.y = y; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + Point point = (Point) o; + + if (!Objects.equals(x, point.x)) { + return false; + } + return Objects.equals(y, point.y); + } + + @Override + public int hashCode() { + int result = x != null ? x.hashCode() : 0; + result = 31 * result + (y != null ? y.hashCode() : 0); + return result; + } + } + +} diff --git a/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/ConsumesTest.java b/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/ConsumesTest.java new file mode 100644 index 00000000000..b3218f5c305 --- /dev/null +++ b/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/ConsumesTest.java @@ -0,0 +1,73 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.http.server.tck.tests; + +import io.micronaut.context.annotation.Requires; +import io.micronaut.http.HttpHeaders; +import io.micronaut.http.HttpRequest; +import io.micronaut.http.HttpStatus; +import io.micronaut.http.MediaType; +import io.micronaut.http.annotation.Body; +import io.micronaut.http.annotation.Consumes; +import io.micronaut.http.annotation.Controller; +import io.micronaut.http.annotation.Post; +import io.micronaut.http.server.tck.AssertionUtils; +import io.micronaut.http.server.tck.HttpResponseAssertion; +import static io.micronaut.http.server.tck.TestScenario.asserts; +import org.junit.jupiter.api.Test; +import java.io.IOException; + +@SuppressWarnings({ + "java:S5960", // We're allowed assertions, as these are used in tests only + "checkstyle:MissingJavadocType", + "checkstyle:DesignForExtension" +}) +public class ConsumesTest { + public static final String SPEC_NAME = "ConsumesTest"; + + @Test + void testMultipleConsumesDefinition() throws IOException { + asserts(SPEC_NAME, + HttpRequest.POST("/consumes-test", "{\"name\":\"Fred\"}").header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON), + (server, request) -> AssertionUtils.assertDoesNotThrow(server, request, HttpResponseAssertion.builder() + .status(HttpStatus.OK) + .body("{\"name\":\"Fred\"}") + .build())); + } + + @Controller("/consumes-test") + @Requires(property = "spec.name", value = SPEC_NAME) + static class ConsumesController { + + @Post("/") + @Consumes({MediaType.APPLICATION_FORM_URLENCODED, MediaType.APPLICATION_JSON}) + Pojo save(@Body Pojo pojo) { + return pojo; + } + } + + static class Pojo { + private String name; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + } +} diff --git a/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/CookiesTest.java b/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/CookiesTest.java new file mode 100644 index 00000000000..c11013a46d5 --- /dev/null +++ b/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/CookiesTest.java @@ -0,0 +1,98 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.http.server.tck.tests; + +import io.micronaut.context.annotation.Requires; +import io.micronaut.core.util.CollectionUtils; +import io.micronaut.http.HttpRequest; +import io.micronaut.http.HttpStatus; +import io.micronaut.http.annotation.Controller; +import io.micronaut.http.annotation.CookieValue; +import io.micronaut.http.annotation.Get; +import io.micronaut.http.cookie.Cookie; +import io.micronaut.http.server.tck.AssertionUtils; +import io.micronaut.http.server.tck.HttpResponseAssertion; +import static io.micronaut.http.server.tck.TestScenario.asserts; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +@SuppressWarnings({ + "java:S5960", // We're allowed assertions, as these are used in tests only + "checkstyle:MissingJavadocType", + "checkstyle:DesignForExtension" +}) +public class CookiesTest { + public static final String SPEC_NAME = "CookiesTest"; + + @Test + void testCookieBind() throws IOException { + asserts(SPEC_NAME, + HttpRequest.GET("/cookies-test/bind") + .cookie(Cookie.of("one", "foo")) + .cookie(Cookie.of("two", "bar")), + (server, request) -> AssertionUtils.assertDoesNotThrow(server, request, HttpResponseAssertion.builder() + .status(HttpStatus.OK) + .body("{\"one\":\"foo\",\"two\":\"bar\"}") + .build())); + } + + @Test + void testGetCookiesMethod() throws IOException { + asserts(SPEC_NAME, + HttpRequest.GET("/cookies-test/all") + .cookie(Cookie.of("one", "foo")) + .cookie(Cookie.of("two", "bar")), + (server, request) -> AssertionUtils.assertDoesNotThrow(server, request, HttpResponseAssertion.builder() + .status(HttpStatus.OK) + .body("{\"one\":\"foo\",\"two\":\"bar\"}") + .build())); + } + + @Test + void testNoCookie() throws IOException { + asserts(SPEC_NAME, + HttpRequest.GET("/cookies-test/all"), + (server, request) -> AssertionUtils.assertDoesNotThrow(server, request, HttpResponseAssertion.builder() + .status(HttpStatus.OK) + .body("{}") + .build())); + } + + @Controller("/cookies-test") + @Requires(property = "spec.name", value = SPEC_NAME) + static class CookieController { + + @Get(uri = "/all") + Map all(HttpRequest request) { + Map map = new HashMap<>(); + for (String cookieName : request.getCookies().names()) { + map.put(cookieName, request.getCookies().get(cookieName).getValue()); + } + return map; + } + + @Get(uri = "/bind") + Map all(@CookieValue String one, @CookieValue String two) { + return CollectionUtils.mapOf( + "one", one, + "two", two + ); + } + } +} diff --git a/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/DeleteWithoutBodyTest.java b/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/DeleteWithoutBodyTest.java new file mode 100644 index 00000000000..c48393b448e --- /dev/null +++ b/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/DeleteWithoutBodyTest.java @@ -0,0 +1,64 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.http.server.tck.tests; + +import io.micronaut.context.annotation.Requires; +import io.micronaut.http.HttpHeaderValues; +import io.micronaut.http.HttpHeaders; +import io.micronaut.http.HttpRequest; +import io.micronaut.http.HttpResponse; +import io.micronaut.http.HttpStatus; +import io.micronaut.http.annotation.Controller; +import io.micronaut.http.annotation.Delete; +import io.micronaut.http.annotation.Header; +import io.micronaut.http.annotation.PathVariable; +import io.micronaut.http.annotation.Status; +import static io.micronaut.http.server.tck.TestScenario.asserts; +import org.junit.jupiter.api.Test; + +import java.io.IOException; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; + +@SuppressWarnings({ + "java:S5960", // We're allowed assertions, as these are used in tests only + "checkstyle:MissingJavadocType", + "checkstyle:DesignForExtension" +}) +public class DeleteWithoutBodyTest { + public static final String SPEC_NAME = "DeleteWithoutBodyTest"; + + @Test + void verifiesItIsPossibleToExposesADeleteEndpointWhichIsInvokedWithoutABody() throws IOException { + asserts(SPEC_NAME, + HttpRequest.DELETE("/sessions/sergio").header(HttpHeaders.AUTHORIZATION, HttpHeaderValues.AUTHORIZATION_PREFIX_BEARER + " xxx"), + (server, request) -> { + HttpResponse response = assertDoesNotThrow(() -> server.exchange(request)); + assertEquals(HttpStatus.OK, response.getStatus()); + }); + } + + @Requires(property = "spec.name", value = SPEC_NAME) + @Controller("/sessions") + static class SessionsController { + @Status(HttpStatus.OK) + @Delete("/{username}") + void delete(@PathVariable String username, + @Header(HttpHeaders.AUTHORIZATION) String authorization) { + } + } +} diff --git a/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/ErrorHandlerTest.java b/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/ErrorHandlerTest.java new file mode 100644 index 00000000000..05d55dded1c --- /dev/null +++ b/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/ErrorHandlerTest.java @@ -0,0 +1,362 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.http.server.tck.tests; + +import com.fasterxml.jackson.databind.ObjectMapper; +import io.micronaut.context.annotation.Requires; +import io.micronaut.core.annotation.Introspected; +import io.micronaut.core.util.CollectionUtils; +import io.micronaut.core.util.StringUtils; +import io.micronaut.http.HttpHeaders; +import io.micronaut.http.HttpRequest; +import io.micronaut.http.HttpResponse; +import io.micronaut.http.HttpStatus; +import io.micronaut.http.MediaType; +import io.micronaut.http.annotation.Body; +import io.micronaut.http.annotation.Controller; +import io.micronaut.http.annotation.Error; +import io.micronaut.http.annotation.Get; +import io.micronaut.http.annotation.Post; +import io.micronaut.http.annotation.Produces; +import io.micronaut.http.annotation.Status; +import io.micronaut.http.codec.CodecException; +import io.micronaut.http.hateoas.JsonError; +import io.micronaut.http.hateoas.Link; +import io.micronaut.http.server.exceptions.ExceptionHandler; +import io.micronaut.http.server.tck.AssertionUtils; +import io.micronaut.http.server.tck.HttpResponseAssertion; +import io.micronaut.http.server.tck.ServerUnderTest; +import io.micronaut.http.server.tck.ServerUnderTestProviderUtils; +import static io.micronaut.http.server.tck.TestScenario.asserts; +import jakarta.inject.Singleton; +import org.junit.jupiter.api.Test; + +import javax.validation.Valid; +import javax.validation.constraints.Min; +import java.io.IOException; +import java.util.Collections; +import java.util.Map; + + +@SuppressWarnings({ + "java:S5960", // We're allowed assertions, as these are used in tests only + "checkstyle:MissingJavadocType", + "checkstyle:DesignForExtension" +}) + +public class ErrorHandlerTest { + public static final String SPEC_NAME = "ErrorHandlerTest"; + public static final String PROPERTY_MICRONAUT_SERVER_CORS_CONFIGURATIONS_WEB_ALLOWED_ORIGINS = "micronaut.server.cors.configurations.web.allowed-origins"; + public static final String PROPERTY_MICRONAUT_SERVER_CORS_ENABLED = "micronaut.server.cors.enabled"; + public static final String LOCALHOST = "http://localhost:8080"; + + @Test + void testCustomGlobalExceptionHandlersDeclaredInController() throws IOException { + asserts(SPEC_NAME, + CollectionUtils.mapOf( + PROPERTY_MICRONAUT_SERVER_CORS_CONFIGURATIONS_WEB_ALLOWED_ORIGINS, Collections.singletonList("http://localhost:8080"), + PROPERTY_MICRONAUT_SERVER_CORS_ENABLED, StringUtils.TRUE + ), + HttpRequest.GET("/errors/global-ctrl").header(HttpHeaders.CONTENT_TYPE, io.micronaut.http.MediaType.APPLICATION_JSON), + (server, request) -> AssertionUtils.assertDoesNotThrow(server, request, + HttpStatus.OK, + "bad things happens globally", + Collections.singletonMap(HttpHeaders.CONTENT_TYPE, MediaType.TEXT_PLAIN))); + } + + @Test + void testCustomGlobalExceptionHandlers() throws IOException { + asserts(SPEC_NAME, + CollectionUtils.mapOf( + PROPERTY_MICRONAUT_SERVER_CORS_CONFIGURATIONS_WEB_ALLOWED_ORIGINS, Collections.singletonList("http://localhost:8080"), + PROPERTY_MICRONAUT_SERVER_CORS_ENABLED, StringUtils.TRUE + ), HttpRequest.GET("/errors/global") + .header(HttpHeaders.CONTENT_TYPE, io.micronaut.http.MediaType.APPLICATION_JSON), + (server, request) -> AssertionUtils.assertDoesNotThrow(server, request, + HttpStatus.OK, + "Exception Handled", + Collections.singletonMap(HttpHeaders.CONTENT_TYPE, MediaType.TEXT_PLAIN))); + } + + @Test + void testCustomGlobalExceptionHandlersForPOSTWithBody() throws IOException { + Map configuration = CollectionUtils.mapOf( + PROPERTY_MICRONAUT_SERVER_CORS_CONFIGURATIONS_WEB_ALLOWED_ORIGINS, Collections.singletonList("http://localhost:8080"), + PROPERTY_MICRONAUT_SERVER_CORS_ENABLED, StringUtils.TRUE + ); + try (ServerUnderTest server = ServerUnderTestProviderUtils.getServerUnderTestProvider().getServer(SPEC_NAME, configuration)) { + ObjectMapper objectMapper = server.getApplicationContext().getBean(ObjectMapper.class); + HttpRequest request = HttpRequest.POST("/json/errors/global", objectMapper.writeValueAsString(new RequestObject(101))) + .header(HttpHeaders.CONTENT_TYPE, io.micronaut.http.MediaType.APPLICATION_JSON); + AssertionUtils.assertDoesNotThrow(server, request, + HttpStatus.OK, + "{\"message\":\"Error: bad things when post and body in request\",\"", + Collections.singletonMap(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON)); + } + } + + @Test + void testCustomGlobalStatusHandlersDeclaredInController() throws IOException { + asserts(SPEC_NAME, + CollectionUtils.mapOf( + PROPERTY_MICRONAUT_SERVER_CORS_CONFIGURATIONS_WEB_ALLOWED_ORIGINS, Collections.singletonList("http://localhost:8080"), + PROPERTY_MICRONAUT_SERVER_CORS_ENABLED, StringUtils.TRUE + ), HttpRequest.GET("/errors/global-status-ctrl"), + (server, request) -> AssertionUtils.assertDoesNotThrow(server, request, + HttpStatus.OK, + "global status", + Collections.singletonMap(HttpHeaders.CONTENT_TYPE, MediaType.TEXT_PLAIN))); + } + + @Test + void testLocalExceptionHandlers() throws IOException { + asserts(SPEC_NAME, + CollectionUtils.mapOf( + PROPERTY_MICRONAUT_SERVER_CORS_CONFIGURATIONS_WEB_ALLOWED_ORIGINS, Collections.singletonList("http://localhost:8080"), + PROPERTY_MICRONAUT_SERVER_CORS_ENABLED, StringUtils.TRUE), + HttpRequest.GET("/errors/local"), + (server, request) -> AssertionUtils.assertDoesNotThrow(server, request, + HttpStatus.OK, + "bad things", + Collections.singletonMap(HttpHeaders.CONTENT_TYPE, MediaType.TEXT_PLAIN))); + } + + @Test + void jsonMessageFormatErrorsReturn400() throws IOException { + asserts(SPEC_NAME, + CollectionUtils.mapOf( + PROPERTY_MICRONAUT_SERVER_CORS_CONFIGURATIONS_WEB_ALLOWED_ORIGINS, Collections.singletonList("http://localhost:8080"), + PROPERTY_MICRONAUT_SERVER_CORS_ENABLED, StringUtils.TRUE), + HttpRequest.POST("/json/jsonBody", "{\"numberField\": \"textInsteadOfNumber\"}"), + (server, request) -> AssertionUtils.assertThrows(server, request, + HttpResponseAssertion.builder() + .status(HttpStatus.BAD_REQUEST) + .headers(Collections.singletonMap(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON)) + .build() + )); + } + + @Test + void corsHeadersArePresentAfterFailedDeserialisationWhenErrorHandlerIsUsed() throws IOException { + asserts(SPEC_NAME, + CollectionUtils.mapOf( + PROPERTY_MICRONAUT_SERVER_CORS_CONFIGURATIONS_WEB_ALLOWED_ORIGINS, Collections.singletonList("http://localhost:8080"), + PROPERTY_MICRONAUT_SERVER_CORS_ENABLED, StringUtils.TRUE + ), HttpRequest.POST("/json/errors/global", "{\"numberField\": \"string is not a number\"}") + .header(HttpHeaders.ORIGIN, LOCALHOST), + (server, request) -> AssertionUtils.assertDoesNotThrow(server, request, HttpResponseAssertion.builder() + .status(HttpStatus.OK) + .headers(Collections.singletonMap(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN, LOCALHOST)) + .build())); + } + + @Test + void corsHeadersArePresentAfterFailedDeserialisation() throws IOException { + asserts(SPEC_NAME, + CollectionUtils.mapOf( + PROPERTY_MICRONAUT_SERVER_CORS_CONFIGURATIONS_WEB_ALLOWED_ORIGINS, Collections.singletonList(LOCALHOST), + PROPERTY_MICRONAUT_SERVER_CORS_ENABLED, StringUtils.TRUE + ), HttpRequest.POST("/json/jsonBody", "{\"numberField\": \"string is not a number\"}") + .header(HttpHeaders.ORIGIN, LOCALHOST), + (server, request) -> AssertionUtils.assertThrows(server, request, HttpResponseAssertion.builder() + .status(HttpStatus.BAD_REQUEST) + .headers(Collections.singletonMap(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN, LOCALHOST)) + .build())); + } + + @Test + void corsHeadersArePresentAfterExceptions() throws IOException { + asserts(SPEC_NAME, + CollectionUtils.mapOf( + PROPERTY_MICRONAUT_SERVER_CORS_CONFIGURATIONS_WEB_ALLOWED_ORIGINS, Collections.singletonList(LOCALHOST), + PROPERTY_MICRONAUT_SERVER_CORS_ENABLED, StringUtils.TRUE + ), HttpRequest.GET("/errors/global").header(HttpHeaders.ORIGIN, LOCALHOST), + (server, request) -> AssertionUtils.assertDoesNotThrow(server, request, HttpResponseAssertion.builder() + .status(HttpStatus.OK) + .headers(Collections.singletonMap(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN, LOCALHOST)) + .build())); + } + + @Test + void messageValidationErrorsReturn400() throws IOException { + asserts(SPEC_NAME, + CollectionUtils.mapOf( + PROPERTY_MICRONAUT_SERVER_CORS_CONFIGURATIONS_WEB_ALLOWED_ORIGINS, Collections.singletonList("http://localhost:8080"), + PROPERTY_MICRONAUT_SERVER_CORS_ENABLED, StringUtils.TRUE + ), HttpRequest.POST("/json/jsonBody", "{\"numberField\": 0}"), + (server, request) -> AssertionUtils.assertThrows(server, request, HttpResponseAssertion.builder() + .status(HttpStatus.BAD_REQUEST) + .headers(Collections.singletonMap(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON)) + .build())); + } + + @Controller("/secret") + @Requires(property = "spec.name", value = SPEC_NAME) + static class SecretController { + @Get + @Produces(MediaType.TEXT_PLAIN) + String index() { + return "area 51 hosts an alien"; + } + } + + @Requires(property = "spec.name", value = SPEC_NAME) + @Controller("/errors") + static class ErrorController { + + @Get("/global") + String globalHandler() { + throw new MyException("bad things"); + } + + @Get("/global-ctrl") + String globalControllerHandler() throws GloballyHandledException { + throw new GloballyHandledException("bad things happens globally"); + } + + @Get("/global-status-ctrl") + @Status(HttpStatus.I_AM_A_TEAPOT) + String globalControllerHandlerForStatus() { + return "original global status"; + } + + @Get("/local") + String localHandler() { + throw new AnotherException("bad things"); + } + + @Error + @Produces(io.micronaut.http.MediaType.TEXT_PLAIN) + @Status(HttpStatus.OK) + String localHandler(AnotherException throwable) { + return throwable.getMessage(); + } + } + + @Controller(value = "/json/errors", produces = io.micronaut.http.MediaType.APPLICATION_JSON) + @Requires(property = "spec.name", value = SPEC_NAME) + static class JsonErrorController { + + @Post("/global") + String globalHandlerPost(@Body RequestObject object) { + throw new RuntimeException("bad things when post and body in request"); + } + + @Error + HttpResponse errorHandler(HttpRequest request, RuntimeException exception) { + JsonError error = new JsonError("Error: " + exception.getMessage()) + .link(Link.SELF, Link.of(request.getUri())); + + return HttpResponse.status(HttpStatus.OK) + .body(error); + } + } + + @Introspected + static class RequestObject { + @Min(1L) + private Integer numberField; + + public RequestObject(Integer numberField) { + this.numberField = numberField; + } + + public Integer getNumberField() { + return numberField; + } + } + + @Controller("/json") + @Requires(property = "spec.name", value = SPEC_NAME) + static class JsonController { + @Post("/jsonBody") + String jsonBody(@Valid @Body RequestObject data) { + return "blah"; + } + } + + @Controller("/global-errors") + @Requires(property = "spec.name", value = SPEC_NAME) + static class GlobalErrorController { + + @Error(global = true, exception = GloballyHandledException.class) + @Produces(io.micronaut.http.MediaType.TEXT_PLAIN) + @Status(HttpStatus.OK) + String globallyHandledException(GloballyHandledException throwable) { + return throwable.getMessage(); + } + + @Error(global = true, status = HttpStatus.I_AM_A_TEAPOT) + @Produces(io.micronaut.http.MediaType.TEXT_PLAIN) + @Status(HttpStatus.OK) + String globalControllerHandlerForStatus() { + return "global status"; + } + + } + + @Singleton + @Requires(property = "spec.name", value = SPEC_NAME) + static class CodecExceptionExceptionHandler + implements ExceptionHandler { + + @Override + public HttpResponse handle(HttpRequest request, CodecException exception) { + return HttpResponse.badRequest("Invalid JSON: " + exception.getMessage()).contentType(MediaType.APPLICATION_JSON); + } + } + + @Singleton + @Requires(property = "spec.name", value = SPEC_NAME) + static class RuntimeErrorHandler implements ExceptionHandler { + + @Override + public HttpResponse handle(HttpRequest request, RuntimeException exception) { + return HttpResponse.serverError("Exception: " + exception.getMessage()) + .contentType(MediaType.TEXT_PLAIN); + } + } + + @Singleton + @Requires(property = "spec.name", value = SPEC_NAME) + static class MyErrorHandler implements ExceptionHandler { + + @Override + public HttpResponse handle(HttpRequest request, MyException exception) { + return HttpResponse.ok("Exception Handled") + .contentType(MediaType.TEXT_PLAIN); + } + } + + + static class MyException extends RuntimeException { + public MyException(String badThings) { + super(badThings); + } + } + + static class AnotherException extends RuntimeException { + public AnotherException(String badThings) { + super(badThings); + } + } + + static class GloballyHandledException extends Exception { + public GloballyHandledException(String badThingsHappensGlobally) { + super(badThingsHappensGlobally); + } + } +} diff --git a/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/FilterErrorTest.java b/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/FilterErrorTest.java new file mode 100644 index 00000000000..a4ba93082eb --- /dev/null +++ b/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/FilterErrorTest.java @@ -0,0 +1,340 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.http.server.tck.tests; + +import io.micronaut.context.annotation.Requires; +import io.micronaut.context.condition.Condition; +import io.micronaut.context.condition.ConditionContext; +import io.micronaut.core.async.publisher.Publishers; +import io.micronaut.core.util.StringUtils; +import io.micronaut.http.HttpAttributes; +import io.micronaut.http.HttpHeaders; +import io.micronaut.http.HttpRequest; +import io.micronaut.http.HttpResponse; +import io.micronaut.http.HttpStatus; +import io.micronaut.http.MediaType; +import io.micronaut.http.MutableHttpResponse; +import io.micronaut.http.annotation.Controller; +import io.micronaut.http.annotation.Error; +import io.micronaut.http.annotation.Filter; +import io.micronaut.http.annotation.Get; +import io.micronaut.http.annotation.Status; +import io.micronaut.http.filter.HttpServerFilter; +import io.micronaut.http.filter.ServerFilterChain; +import io.micronaut.http.server.exceptions.ExceptionHandler; +import io.micronaut.http.server.tck.AssertionUtils; +import io.micronaut.http.server.tck.HttpResponseAssertion; +import static io.micronaut.http.server.tck.TestScenario.asserts; +import io.micronaut.web.router.MethodBasedRouteMatch; +import io.micronaut.web.router.RouteMatch; +import jakarta.inject.Singleton; +import org.junit.jupiter.api.Test; +import org.reactivestreams.Publisher; + +import java.io.IOException; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@SuppressWarnings({ + "java:S5960", // We're allowed assertions, as these are used in tests only + "checkstyle:MissingJavadocType", + "checkstyle:DesignForExtension" +}) +public class FilterErrorTest { + public static final String SPEC_NAME = "FilterErrorTest"; + + @Test + void testFilterThrowingExceptionHandledByExceptionHandlerThrowingException() throws IOException { + asserts(SPEC_NAME + "3", + HttpRequest.GET("/filter-error-spec-3") + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON), + (server, request) -> { + AssertionUtils.assertThrows(server, request, + HttpResponseAssertion.builder() + .status(HttpStatus.INTERNAL_SERVER_ERROR) + .body("from exception handler") + .build()); + ExceptionException filter = server.getApplicationContext().getBean(ExceptionException.class); + assertEquals(1, filter.executedCount.get()); + assertEquals(HttpStatus.INTERNAL_SERVER_ERROR, filter.responseStatus.getAndSet(null)); + }); + } + + @Test + void testTheErrorRouteIsTheRouteMatch() throws IOException { + asserts(SPEC_NAME + "4", + HttpRequest.GET("/filter-error-spec-4/status").header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON), + (server, request) -> { + AssertionUtils.assertDoesNotThrow(server, request, + HttpResponseAssertion.builder() + .status(HttpStatus.OK) + .build()); + ExceptionRoute filter = server.getApplicationContext().getBean(ExceptionRoute.class); + RouteMatch match = filter.routeMatch.getAndSet(null); + assertTrue(match instanceof MethodBasedRouteMatch); + assertEquals("testStatus", ((MethodBasedRouteMatch) match).getName()); + }); + } + + @Test + void testNonOncePerRequestFilterThrowingErrorDoesNotLoop() throws IOException { + asserts(SPEC_NAME + "2", + HttpRequest.GET("/filter-error-spec").header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON), + (server, request) -> { + AssertionUtils.assertThrows(server, request, + HttpResponseAssertion.builder() + .status(HttpStatus.BAD_REQUEST) + .body("from filter exception handler") + .build()); + FirstEvery filter = server.getApplicationContext().getBean(FirstEvery.class); + assertEquals(1, filter.executedCount.get()); + }); + } + + @Test + void testErrorsEmittedFromSecondFilterInteractingWithExceptionHandlers() throws IOException { + asserts(SPEC_NAME, + HttpRequest.GET("/filter-error-spec").header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON).header("X-Passthru", StringUtils.TRUE), + (server, request) -> { + AssertionUtils.assertThrows(server, request, + HttpResponseAssertion.builder() + .status(HttpStatus.BAD_REQUEST) + .body("from NEXT filter exception handle").build()); + + First first = server.getApplicationContext().getBean(First.class); + Next next = server.getApplicationContext().getBean(Next.class); + + assertEquals(1, first.executedCount.get()); + assertEquals(HttpStatus.BAD_REQUEST, first.responseStatus.getAndSet(null)); + assertEquals(1, next.executedCount.get()); + }); + } + + @Test + void testErrorsEmittedFromFiltersInteractingWithExceptionHandlers() throws IOException { + asserts(SPEC_NAME, + HttpRequest.GET("/filter-error-spec").header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON), + (server, request) -> { + AssertionUtils.assertThrows(server, request, + HttpResponseAssertion.builder() + .status(HttpStatus.BAD_REQUEST) + .body("from filter exception handler").build()); + + First first = server.getApplicationContext().getBean(First.class); + Next next = server.getApplicationContext().getBean(Next.class); + + assertEquals(1, first.executedCount.get()); + assertNull(first.responseStatus.getAndSet(null)); + assertEquals(0, next.executedCount.get()); + }); + } + + static class FilterExceptionException extends RuntimeException { + } + + static class FilterException extends RuntimeException { + } + + static class NextFilterException extends RuntimeException { + } + + @Requires(property = "spec.name", value = SPEC_NAME) + @Filter(Filter.MATCH_ALL_PATTERN) + static class First implements HttpServerFilter { + AtomicInteger executedCount = new AtomicInteger(0); + AtomicReference responseStatus = new AtomicReference<>(); + + private void setResponse(MutableHttpResponse r) { + responseStatus.set(r.status()); + } + + @Override + public Publisher> doFilter(HttpRequest request, ServerFilterChain chain) { + executedCount.incrementAndGet(); + if (StringUtils.isTrue(request.getHeaders().get("X-Passthru"))) { + return Publishers.then(chain.proceed(request), this::setResponse); + } + return Publishers.just(new FilterException()); + } + + @Override + public int getOrder() { + return 10; + } + } + + @Requires(property = "spec.name", value = SPEC_NAME) + @Filter(Filter.MATCH_ALL_PATTERN) + static class Next implements HttpServerFilter { + AtomicInteger executedCount = new AtomicInteger(0); + + @Override + public Publisher> doFilter(HttpRequest request, ServerFilterChain chain) { + executedCount.incrementAndGet(); + return Publishers.just(new NextFilterException()); + } + + @Override + public int getOrder() { + return 20; + } + } + + @Requires(property = "spec.name", value = SPEC_NAME + "2") + @Filter(Filter.MATCH_ALL_PATTERN) + static class FirstEvery implements HttpServerFilter { + AtomicInteger executedCount = new AtomicInteger(0); + + @Override + public Publisher> doFilter(HttpRequest request, ServerFilterChain chain) { + executedCount.incrementAndGet(); + return Publishers.just(new FilterException()); + } + + @Override + public int getOrder() { + return 10; + } + } + + @Requires(property = "spec.name", value = SPEC_NAME + "3") + @Filter(Filter.MATCH_ALL_PATTERN) + static class ExceptionException implements HttpServerFilter { + AtomicInteger executedCount = new AtomicInteger(0); + AtomicReference responseStatus = new AtomicReference<>(); + + private void setResponse(MutableHttpResponse r) { + responseStatus.set(r.status()); + } + + @Override + public Publisher> doFilter(HttpRequest request, ServerFilterChain chain) { + executedCount.incrementAndGet(); + return Publishers.then(chain.proceed(request), + this::setResponse); + } + + @Override + public int getOrder() { + return 10; + } + } + + @Requires(property = "spec.name", value = SPEC_NAME + "4") + @Filter(Filter.MATCH_ALL_PATTERN) + static class ExceptionRoute implements HttpServerFilter { + AtomicReference> routeMatch = new AtomicReference<>(); + + @Override + public Publisher> doFilter(HttpRequest request, ServerFilterChain chain) { + return Publishers.then(chain.proceed(request), + httpResponse -> routeMatch.set(httpResponse.getAttribute(HttpAttributes.ROUTE_MATCH, RouteMatch.class).get())); + } + + @Override + public int getOrder() { + return 10; + } + } + + @Requires(condition = FilterCondition.class) + @Controller("/filter-error-spec") + static class NeverReachedController { + @Get + String get() { + return "OK"; + } + } + + @Requires(condition = FilterCondition.class) + @Controller("/filter-error-spec-3") + static class HandledByHandlerController { + @Get + String get() { + throw new FilterExceptionException(); + } + } + + @Requires(condition = FilterCondition.class) + @Controller("/filter-error-spec-4") + static class HandledByErrorRouteController { + @Get("/exception") + String getException() { + throw new FilterExceptionException(); + } + + @Get("/status") + HttpStatus getStatus() { + return HttpStatus.NOT_FOUND; + } + + @Error(exception = FilterExceptionException.class) + @Status(HttpStatus.OK) + void testException() { + + } + + @Error(status = HttpStatus.NOT_FOUND) + @Status(HttpStatus.OK) + void testStatus() { + + } + } + + static class FilterCondition implements Condition { + + @Override + public boolean matches(ConditionContext context) { + return context.getProperty("spec.name", String.class) + .map(val -> val.equals(SPEC_NAME + "4") || val.equals(SPEC_NAME + "3") || val.equals(SPEC_NAME + "2") || val.equals(SPEC_NAME)) + .orElse(false); + } + } + + @Requires(condition = FilterCondition.class) + @Singleton + static class FilterExceptionExceptionHandler implements ExceptionHandler> { + + @Override + public HttpResponse handle(HttpRequest request, FilterExceptionException exception) { + throw new RuntimeException("from exception handler"); + } + } + + @Requires(condition = FilterCondition.class) + @Singleton + static class FilterExceptionHandler implements ExceptionHandler> { + + @Override + public HttpResponse handle(HttpRequest request, FilterException exception) { + return HttpResponse.badRequest("from filter exception handler"); + } + } + + @Requires(condition = FilterCondition.class) + @Singleton + static class NextFilterExceptionHandler implements ExceptionHandler> { + + @Override + public HttpResponse handle(HttpRequest request, NextFilterException exception) { + return HttpResponse.badRequest("from NEXT filter exception handler"); + } + } +} diff --git a/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/FiltersTest.java b/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/FiltersTest.java new file mode 100644 index 00000000000..64a52d674bf --- /dev/null +++ b/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/FiltersTest.java @@ -0,0 +1,151 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.http.server.tck.tests; + +import io.micronaut.context.annotation.Requires; +import io.micronaut.core.async.publisher.Publishers; +import io.micronaut.core.util.CollectionUtils; +import io.micronaut.core.util.StringUtils; +import io.micronaut.http.HttpHeaders; +import io.micronaut.http.HttpRequest; +import io.micronaut.http.HttpResponse; +import io.micronaut.http.HttpStatus; +import io.micronaut.http.MutableHttpResponse; +import io.micronaut.http.annotation.Controller; +import io.micronaut.http.annotation.Filter; +import io.micronaut.http.annotation.Get; +import io.micronaut.http.annotation.Produces; +import io.micronaut.http.filter.HttpServerFilter; +import io.micronaut.http.filter.ServerFilterChain; +import io.micronaut.http.server.exceptions.ExceptionHandler; +import io.micronaut.http.server.tck.AssertionUtils; +import io.micronaut.http.server.tck.HttpResponseAssertion; +import io.micronaut.http.server.tck.ServerUnderTest; +import io.micronaut.http.server.tck.ServerUnderTestProviderUtils; +import jakarta.inject.Singleton; +import org.junit.jupiter.api.Test; +import org.reactivestreams.Publisher; + +import java.io.IOException; +import java.util.Collections; +import java.util.Map; +@SuppressWarnings({ + "java:S5960", // We're allowed assertions, as these are used in tests only + "checkstyle:MissingJavadocType", + "checkstyle:DesignForExtension" +}) +public class FiltersTest { + public static final String SPEC_NAME = "FiltersTest"; + public static final String PROP_MICRONAUT_SERVER_CORS_ENABLED = "micronaut.server.cors.enabled"; + + @Test + void testFiltersAreRunCorrectly() throws IOException { + Map configuration = CollectionUtils.mapOf( + PROP_MICRONAUT_SERVER_CORS_ENABLED, StringUtils.TRUE + ); + try (ServerUnderTest server = ServerUnderTestProviderUtils.getServerUnderTestProvider().getServer(SPEC_NAME, configuration)) { + HttpRequest request = HttpRequest.GET("/filter-test/ok"); + AssertionUtils.assertDoesNotThrow(server, request, HttpResponseAssertion.builder() + .status(HttpStatus.OK) + .body("OK") + .headers(Collections.singletonMap("X-Test-Filter", StringUtils.TRUE)) + .build()); + } + } + + @Test + void filtersAreAppliedOnNonMatchingMethodsCorsFilterWorks() throws IOException { + Map configuration = CollectionUtils.mapOf( + PROP_MICRONAUT_SERVER_CORS_ENABLED, StringUtils.TRUE + ); + try (ServerUnderTest server = ServerUnderTestProviderUtils.getServerUnderTestProvider().getServer(SPEC_NAME, configuration)) { + HttpRequest request = HttpRequest.OPTIONS("/filter-test/ok").header("Origin", "https://micronaut.io") + .header(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, "GET"); + AssertionUtils.assertDoesNotThrow(server, request, HttpResponseAssertion.builder() + .status(HttpStatus.OK) + .headers(Collections.singletonMap(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN, "https://micronaut.io")) + .build()); + } + } + + @Test + void filtersAreAppliedOnNonMatchingMethodsCorsFilterDisableIfNotPreflight() throws IOException { + Map configuration = CollectionUtils.mapOf( + PROP_MICRONAUT_SERVER_CORS_ENABLED, StringUtils.TRUE + ); + try (ServerUnderTest server = ServerUnderTestProviderUtils.getServerUnderTestProvider().getServer(SPEC_NAME, configuration)) { + HttpRequest request = HttpRequest.OPTIONS("/filter-test/ok"); + AssertionUtils.assertThrows(server, request, HttpResponseAssertion.builder() + .status(HttpStatus.METHOD_NOT_ALLOWED) + .build()); + } + } + + @Test + void testFiltersAreRunCorrectlyWithCustomExceptionHandler() throws IOException { + Map configuration = CollectionUtils.mapOf( + PROP_MICRONAUT_SERVER_CORS_ENABLED, StringUtils.TRUE + ); + try (ServerUnderTest server = ServerUnderTestProviderUtils.getServerUnderTestProvider().getServer(SPEC_NAME, configuration)) { + HttpRequest request = HttpRequest.GET("/filter-test/exception"); + AssertionUtils.assertDoesNotThrow(server, request, HttpResponseAssertion.builder() + .status(HttpStatus.OK) + .body("Exception Handled") + .headers(Collections.singletonMap("X-Test-Filter", StringUtils.TRUE)) + .build()); + } + } + + @Controller("/filter-test") + @Requires(property = "spec.name", value = SPEC_NAME) + static class TestController { + @Get("/ok") + String ok() { + return "OK"; + } + + @Get("/exception") + void exception() { + throw new CustomException(); + } + } + + @Filter("/filter-test/**") + @Requires(property = "spec.name", value = SPEC_NAME) + static class TestFilter implements HttpServerFilter { + @Override + public Publisher> doFilter(HttpRequest request, ServerFilterChain chain) { + return Publishers.map(chain.proceed(request), httpResponse -> { + httpResponse.getHeaders().add("X-Test-Filter", "true"); + return httpResponse; + }); + } + } + + static class CustomException extends RuntimeException { + } + + @Produces + @Singleton + @Requires(property = "spec.name", value = SPEC_NAME) + static class CustomExceptionHandler implements ExceptionHandler> { + @Override + public HttpResponse handle(HttpRequest request, CustomException exception) { + return HttpResponse.ok("Exception Handled"); + } + + } +} diff --git a/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/FluxTest.java b/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/FluxTest.java new file mode 100644 index 00000000000..887caeebcb2 --- /dev/null +++ b/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/FluxTest.java @@ -0,0 +1,60 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.http.server.tck.tests; + +import io.micronaut.context.annotation.Requires; +import io.micronaut.http.HttpRequest; +import io.micronaut.http.HttpStatus; +import io.micronaut.http.annotation.Controller; +import io.micronaut.http.annotation.Get; +import io.micronaut.http.server.tck.AssertionUtils; +import io.micronaut.http.server.tck.HttpResponseAssertion; +import static io.micronaut.http.server.tck.TestScenario.asserts; +import org.junit.jupiter.api.Test; +import reactor.core.publisher.Flux; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Collections; +import java.util.Map; + +@SuppressWarnings({ + "java:S5960", // We're allowed assertions, as these are used in tests only + "checkstyle:MissingJavadocType", + "checkstyle:DesignForExtension" +}) +public class FluxTest { + public static final String SPEC_NAME = "FluxTest"; + + @Test + void testControllerReturningAFlux() throws IOException { + asserts(SPEC_NAME, + HttpRequest.GET("/users"), + (server, request) -> AssertionUtils.assertDoesNotThrow(server, request, HttpResponseAssertion.builder() + .status(HttpStatus.OK) + .body("[{\"name\":\"Joe\"},{\"name\":\"Lewis\"}]") + .build())); + } + + @Controller("/users") + @Requires(property = "spec.name", value = SPEC_NAME) + static class UserController { + @Get + Flux> getAll() { + return Flux.fromIterable(Arrays.asList(Collections.singletonMap("name", "Joe"), Collections.singletonMap("name", "Lewis"))); + } + } +} diff --git a/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/HelloWorldTest.java b/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/HelloWorldTest.java new file mode 100644 index 00000000000..86089cb5d28 --- /dev/null +++ b/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/HelloWorldTest.java @@ -0,0 +1,62 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.http.server.tck.tests; + +import io.micronaut.context.annotation.Requires; +import io.micronaut.http.HttpHeaders; +import io.micronaut.http.HttpRequest; +import io.micronaut.http.HttpStatus; +import io.micronaut.http.MediaType; +import io.micronaut.http.annotation.Controller; +import io.micronaut.http.annotation.Get; +import io.micronaut.http.annotation.Produces; +import io.micronaut.http.server.tck.AssertionUtils; +import static io.micronaut.http.server.tck.TestScenario.asserts; +import io.micronaut.http.uri.UriBuilder; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.util.Collections; + + +@SuppressWarnings({ + "java:S5960", // We're allowed assertions, as these are used in tests only + "checkstyle:MissingJavadocType", + "checkstyle:DesignForExtension" +}) +public class HelloWorldTest { + public static final String SPEC_NAME = "HelloWorldTest"; + + @Test + void helloWorld() throws IOException { + asserts(SPEC_NAME, + HttpRequest.GET(UriBuilder.of("/hello").path("world").build()).accept(MediaType.TEXT_PLAIN), + (server, request) -> AssertionUtils.assertDoesNotThrow(server, request, + HttpStatus.OK, + "Hello World", + Collections.singletonMap(HttpHeaders.CONTENT_TYPE, MediaType.TEXT_PLAIN))); + } + + @Requires(property = "spec.name", value = SPEC_NAME) + @Controller("/hello") + static class HelloWorldController { + @Produces(MediaType.TEXT_PLAIN) + @Get("/world") + String hello() { + return "Hello World"; + } + } +} diff --git a/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/MiscTest.java b/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/MiscTest.java new file mode 100644 index 00000000000..5e665b41c0a --- /dev/null +++ b/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/MiscTest.java @@ -0,0 +1,266 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.http.server.tck.tests; + +import io.micronaut.context.annotation.Requires; +import io.micronaut.core.annotation.Introspected; +import io.micronaut.core.annotation.NonNull; +import io.micronaut.http.HttpHeaders; +import io.micronaut.http.HttpRequest; +import io.micronaut.http.HttpResponse; +import io.micronaut.http.HttpStatus; +import io.micronaut.http.MediaType; +import io.micronaut.http.annotation.Body; +import io.micronaut.http.annotation.Consumes; +import io.micronaut.http.annotation.Controller; +import io.micronaut.http.annotation.Get; +import io.micronaut.http.annotation.Post; +import io.micronaut.http.server.tck.AssertionUtils; +import io.micronaut.http.server.tck.HttpResponseAssertion; +import static io.micronaut.http.server.tck.TestScenario.asserts; +import org.junit.jupiter.api.Test; + +import javax.validation.constraints.NotBlank; +import java.io.IOException; +import java.util.Collections; +import java.util.Map; + + +@SuppressWarnings({ + "java:S5960", // We're allowed assertions, as these are used in tests only + "checkstyle:MissingJavadocType", + "checkstyle:DesignForExtension" +}) +public class MiscTest { + public static final String SPEC_NAME = "MiscTest"; + + /** + * + * @see micronaut-aws #868 + */ + @Test + void testSelectedRouteReflectsAcceptHeader() throws IOException { + asserts(SPEC_NAME, + HttpRequest.GET("/bar/ok").header(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON), + (server, request) -> AssertionUtils.assertDoesNotThrow(server, request, HttpResponseAssertion.builder() + .status(HttpStatus.OK) + .body("{\"status\":\"ok\"}") + .build())); + + asserts(SPEC_NAME, + HttpRequest.GET("/bar/ok").header(HttpHeaders.ACCEPT, MediaType.TEXT_HTML), + (server, request) -> AssertionUtils.assertDoesNotThrow(server, request, + HttpResponseAssertion.builder() + .status(HttpStatus.OK) + .body("
ok
") + .build())); + } + + @Test + void testBehaviourOf404() throws IOException { + asserts(SPEC_NAME, + HttpRequest.GET("/does-not-exist").header("Accept", MediaType.APPLICATION_JSON), + (server, request) -> AssertionUtils.assertThrows(server, request, HttpResponseAssertion.builder() + .status(HttpStatus.NOT_FOUND) + .build())); + } + + @Test + void postFormUrlEncodedBodyBindingToPojoWorks() throws IOException { + asserts(SPEC_NAME, + HttpRequest.POST("/form", "message=World").header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED), + (server, request) -> AssertionUtils.assertDoesNotThrow(server, request, HttpResponseAssertion.builder() + .status(HttpStatus.OK) + .body("{\"message\":\"Hello World\"}") + .build())); + } + + @Test + void postFormUrlEncodedBodyBindingToPojoWorksIfYouDontSpecifyBodyAnnotation() throws IOException { + asserts(SPEC_NAME, + HttpRequest.POST("/form/without-body-annotation", "message=World") + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED), + (server, request) -> AssertionUtils.assertDoesNotThrow(server, request, HttpResponseAssertion.builder() + .status(HttpStatus.OK) + .body("{\"message\":\"Hello World\"}") + .build())); + } + + @Test + void formUrlEncodedWithBodyAnnotationAndANestedAttribute() throws IOException { + asserts(SPEC_NAME, + HttpRequest.POST("/form/nested-attribute", "message=World") + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED), + (server, request) -> AssertionUtils.assertDoesNotThrow(server, request, HttpResponseAssertion.builder() + .status(HttpStatus.OK) + .body("{\"message\":\"Hello World\"}") + .build())); + } + + /** + * + * @see micronaut-aws #1410 + */ + @Test + void applicationJsonWithBodyAnnotationAndANestedAttribute() throws IOException { + asserts(SPEC_NAME, + HttpRequest.POST("/form/json-nested-attribute", "{\"message\":\"World\"}") + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON), + (server, request) -> AssertionUtils.assertDoesNotThrow(server, request, HttpResponseAssertion.builder() + .status(HttpStatus.OK) + .body("{\"message\":\"Hello World\"}") + .build())); + } + + @Test + void applicationJsonWithoutBodyAnnotation() throws IOException { + asserts(SPEC_NAME, + HttpRequest.POST("/form/json-without-body-annotation", "{\"message\":\"World\"}") + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON), + (server, request) -> AssertionUtils.assertDoesNotThrow(server, request, HttpResponseAssertion.builder() + .status(HttpStatus.OK) + .body("{\"message\":\"Hello World\"}") + .build())); + } + + @Test + void applicationJsonWithBodyAnnotationAndANestedAttributeAndMapReturnRenderedAsJSON() throws IOException { + asserts(SPEC_NAME, + HttpRequest.POST("/form/json-nested-attribute-with-map-return", "{\"message\":\"World\"}") + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON), + (server, request) -> AssertionUtils.assertDoesNotThrow(server, request, HttpResponseAssertion.builder() + .status(HttpStatus.OK) + .body("{\"message\":\"Hello World\"}") + .build())); + } + + @Test + void applicationJsonWithBodyAnnotationAndObjectReturnRenderedAsJson() throws IOException { + asserts(SPEC_NAME, + HttpRequest.POST("/form/json-with-body-annotation-and-with-object-return", "{\"message\":\"World\"}") + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON), + (server, request) -> AssertionUtils.assertDoesNotThrow(server, request, HttpResponseAssertion.builder() + .status(HttpStatus.OK) + .body("{\"greeting\":\"Hello World\"}") + .build())); + } + + @Controller + @Requires(property = "spec.name", value = SPEC_NAME) + static class SimpleController { + @Get(uri = "/foo") + HttpResponse getParamValue(HttpRequest request) { + return HttpResponse.ok() + .body(request.getParameters().get("param")) + .header("foo", "bar"); + } + } + + @Controller("/bar") + @Requires(property = "spec.name", value = SPEC_NAME) + static class ProduceController { + @Get(value = "/ok", produces = MediaType.APPLICATION_JSON) + String getOkAsJson() { + return "{\"status\":\"ok\"}"; + } + + @Get(value = "/ok", produces = MediaType.TEXT_HTML) + String getOkAsHtml() { + return "
ok
"; + } + } + + @Introspected + static class MessageCreate { + + @NonNull + @NotBlank + private final String message; + + MessageCreate(@NonNull String message) { + this.message = message; + } + + @NonNull + String getMessage() { + return message; + } + } + + @Introspected + static class MyResponse { + + @NonNull + @NotBlank + private final String greeting; + + public MyResponse(@NonNull String greeting) { + this.greeting = greeting; + } + + @NonNull + public String getGreeting() { + return greeting; + } + } + + @Controller("/form") + @Requires(property = "spec.name", value = SPEC_NAME) + static class FormController { + + @Consumes(MediaType.APPLICATION_FORM_URLENCODED) + @Post("/without-body-annotation") + String withoutBodyAnnotation(MessageCreate messageCreate) { + return "{\"message\":\"Hello " + messageCreate.getMessage() + "\"}"; + } + + @Consumes(MediaType.APPLICATION_FORM_URLENCODED) + @Post + String save(@Body MessageCreate messageCreate) { + return "{\"message\":\"Hello " + messageCreate.getMessage() + "\"}"; + } + + @Consumes(MediaType.APPLICATION_FORM_URLENCODED) + @Post("/nested-attribute") + String save(@Body("message") String value) { + return "{\"message\":\"Hello " + value + "\"}"; + } + + @Consumes(MediaType.APPLICATION_JSON) + @Post("/json-without-body-annotation") + String jsonWithoutBody(MessageCreate messageCreate) { + return "{\"message\":\"Hello " + messageCreate.getMessage() + "\"}"; + } + + @Consumes(MediaType.APPLICATION_JSON) + @Post("/json-nested-attribute") + String jsonNestedAttribute(@Body("message") String value) { + return "{\"message\":\"Hello " + value + "\"}"; + } + + @Consumes(MediaType.APPLICATION_JSON) + @Post("/json-nested-attribute-with-map-return") + Map jsonNestedAttributeWithMapReturn(@Body("message") String value) { + return Collections.singletonMap("message", "Hello " + value); + } + + @Consumes(MediaType.APPLICATION_JSON) + @Post("/json-with-body-annotation-and-with-object-return") + MyResponse jsonNestedAttributeWithObjectReturn(@Body MessageCreate messageCreate) { + return new MyResponse("Hello " + messageCreate.getMessage()); + } + } +} diff --git a/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/ParameterTest.java b/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/ParameterTest.java new file mode 100644 index 00000000000..4ad42b4659f --- /dev/null +++ b/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/ParameterTest.java @@ -0,0 +1,62 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.http.server.tck.tests; + +import io.micronaut.context.annotation.Requires; +import io.micronaut.http.HttpRequest; +import io.micronaut.http.HttpStatus; +import io.micronaut.http.annotation.Controller; +import io.micronaut.http.annotation.Get; +import io.micronaut.http.server.tck.AssertionUtils; +import io.micronaut.http.server.tck.HttpResponseAssertion; +import static io.micronaut.http.server.tck.TestScenario.asserts; +import io.micronaut.http.uri.UriBuilder; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.util.List; + + +@SuppressWarnings({ + "java:S5960", // We're allowed assertions, as these are used in tests only + "checkstyle:MissingJavadocType", + "checkstyle:DesignForExtension" +}) +public class ParameterTest { + public static final String SPEC_NAME = "ParameterTest"; + + @Test + void testGetAllMethod() throws IOException { + asserts(SPEC_NAME, + HttpRequest.GET(UriBuilder.of("/parameters-test").path("all") + .queryParam("test", "one", "two", "three+four") + .build()), + (server, request) -> AssertionUtils.assertDoesNotThrow(server, request, HttpResponseAssertion.builder() + .status(HttpStatus.OK) + .body("[\"one\",\"two\",\"three+four\"]") + .build())); + } + + @Controller("/parameters-test") + @Requires(property = "spec.name", value = SPEC_NAME) + static class BodyController { + + @Get(uri = "/all") + List all(HttpRequest request) { + return request.getParameters().getAll("test"); + } + } +} diff --git a/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/RemoteAddressTest.java b/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/RemoteAddressTest.java new file mode 100644 index 00000000000..49b94b05ba4 --- /dev/null +++ b/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/RemoteAddressTest.java @@ -0,0 +1,89 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.http.server.tck.tests; + +import io.micronaut.context.annotation.Requires; +import io.micronaut.core.async.publisher.Publishers; +import io.micronaut.http.HttpRequest; +import io.micronaut.http.HttpResponse; +import io.micronaut.http.HttpStatus; +import io.micronaut.http.MutableHttpResponse; +import io.micronaut.http.annotation.Controller; +import io.micronaut.http.annotation.Filter; +import io.micronaut.http.annotation.Get; +import io.micronaut.http.annotation.Produces; +import io.micronaut.http.filter.HttpServerFilter; +import io.micronaut.http.filter.ServerFilterChain; +import io.micronaut.http.server.exceptions.ExceptionHandler; +import io.micronaut.http.server.tck.AssertionUtils; +import io.micronaut.http.server.tck.HttpResponseAssertion; +import static io.micronaut.http.server.tck.TestScenario.asserts; +import jakarta.inject.Singleton; +import org.junit.jupiter.api.Test; +import org.reactivestreams.Publisher; + +import java.io.IOException; +import java.util.Collections; + + +@SuppressWarnings({ + "java:S5960", // We're allowed assertions, as these are used in tests only + "checkstyle:MissingJavadocType", + "checkstyle:DesignForExtension" +}) +public class RemoteAddressTest { + public static final String SPEC_NAME = "RemoteAddressTest"; + + @Test + void testRemoteAddressComesFromIdentitySourceIp() throws IOException { + asserts(SPEC_NAME, + HttpRequest.GET("/remoteAddress/fromSourceIp"), + (server, request) -> AssertionUtils.assertDoesNotThrow(server, request, HttpResponseAssertion.builder() + .status(HttpStatus.OK) + .headers(Collections.singletonMap("X-Captured-Remote-Address", "127.0.0.1")) + .build())); + } + + @Requires(property = "spec.name", value = SPEC_NAME) + @Controller("/remoteAddress") + static class TestController { + @Get("fromSourceIp") + void sourceIp() { + } + } + + @Requires(property = "spec.name", value = SPEC_NAME) + @Filter("/remoteAddress/**") + static class CaptureRemoteAddressFiter implements HttpServerFilter { + @Override + public Publisher> doFilter(HttpRequest request, ServerFilterChain chain) { + return Publishers.map(chain.proceed(request), httpResponse -> { + httpResponse.getHeaders().add("X-Captured-Remote-Address", request.getRemoteAddress().getAddress().getHostAddress()); + return httpResponse; + }); + } + } + + @Requires(property = "spec.name", value = SPEC_NAME) + @Produces + @Singleton + static class CustomExceptionHandler implements ExceptionHandler { + @Override + public HttpResponse handle(HttpRequest request, Exception exception) { + return HttpResponse.serverError(exception.toString()); + } + } +} diff --git a/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/ResponseStatusTest.java b/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/ResponseStatusTest.java new file mode 100644 index 00000000000..69959731609 --- /dev/null +++ b/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/ResponseStatusTest.java @@ -0,0 +1,125 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.http.server.tck.tests; + +import io.micronaut.context.annotation.Requires; +import io.micronaut.http.HttpHeaders; +import io.micronaut.http.HttpRequest; +import io.micronaut.http.HttpStatus; +import io.micronaut.http.MediaType; +import io.micronaut.http.annotation.Body; +import io.micronaut.http.annotation.Controller; +import io.micronaut.http.annotation.Delete; +import io.micronaut.http.annotation.Get; +import io.micronaut.http.annotation.Post; +import io.micronaut.http.annotation.Status; +import io.micronaut.http.server.tck.AssertionUtils; +import io.micronaut.http.server.tck.HttpResponseAssertion; +import static io.micronaut.http.server.tck.TestScenario.asserts; +import org.junit.jupiter.api.Test; + +import javax.validation.ConstraintViolationException; +import java.io.IOException; +import java.util.Collections; +import java.util.Optional; + + +@SuppressWarnings({ + "java:S5960", // We're allowed assertions, as these are used in tests only + "checkstyle:MissingJavadocType", + "checkstyle:DesignForExtension" +}) +public class ResponseStatusTest { + public static final String SPEC_NAME = "ResponseStatusTest"; + + @Test + void testConstraintViolationCauses400() throws IOException { + asserts(SPEC_NAME, + HttpRequest.POST("/response-status/constraint-violation", Collections.emptyMap()).header(HttpHeaders.CONTENT_TYPE, MediaType.TEXT_PLAIN), + (server, request) -> AssertionUtils.assertThrows(server, request, HttpResponseAssertion.builder() + .status(HttpStatus.BAD_REQUEST) + .build())); + } + + @Test + void testVoidMethodsDoesNotCause404() throws IOException { + asserts(SPEC_NAME, + HttpRequest.DELETE("/response-status/delete-something").header(HttpHeaders.CONTENT_TYPE, MediaType.TEXT_PLAIN), + (server, request) -> AssertionUtils.assertDoesNotThrow(server, request, HttpResponseAssertion.builder() + .status(HttpStatus.NO_CONTENT) + .build())); + } + + @Test + void testNullCauses404() throws IOException { + asserts(SPEC_NAME, + HttpRequest.GET("/response-status/null").header(HttpHeaders.CONTENT_TYPE, MediaType.TEXT_PLAIN), + (server, request) -> AssertionUtils.assertThrows(server, request, HttpResponseAssertion.builder() + .status(HttpStatus.NOT_FOUND) + .build())); + } + + @Test + void testOptionalCauses404() throws IOException { + asserts(SPEC_NAME, + HttpRequest.GET("/response-status/optional").header(HttpHeaders.CONTENT_TYPE, MediaType.TEXT_PLAIN), + (server, request) -> AssertionUtils.assertThrows(server, request, HttpResponseAssertion.builder() + .status(HttpStatus.NOT_FOUND) + .build())); + } + + @Test + void testCustomResponseStatus() throws IOException { + asserts(SPEC_NAME, + HttpRequest.POST("/response-status", "foo").header(HttpHeaders.CONTENT_TYPE, MediaType.TEXT_PLAIN), + (server, request) -> AssertionUtils.assertDoesNotThrow(server, request, HttpResponseAssertion.builder() + .status(HttpStatus.CREATED) + .body("foo") + .build())); + } + + @Controller("/response-status") + @Requires(property = "spec.name", value = SPEC_NAME) + static class StatusController { + + @Post(uri = "/", processes = MediaType.TEXT_PLAIN) + @Status(HttpStatus.CREATED) + String post(@Body String data) { + return data; + } + + @Get(uri = "/optional", processes = MediaType.TEXT_PLAIN) + Optional optional() { + return Optional.empty(); + } + + @Get(uri = "/null", processes = MediaType.TEXT_PLAIN) + String returnNull() { + return null; + } + + @Post(uri = "/constraint-violation", processes = MediaType.TEXT_PLAIN) + String constraintViolation() { + throw new ConstraintViolationException("Failed", Collections.emptySet()); + } + + @Status(HttpStatus.NO_CONTENT) + @Delete(uri = "/delete-something", processes = MediaType.TEXT_PLAIN) + void deleteSomething() { + // do nothing + } + } +} diff --git a/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/StatusTest.java b/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/StatusTest.java new file mode 100644 index 00000000000..4f4d0a0f6a5 --- /dev/null +++ b/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/StatusTest.java @@ -0,0 +1,107 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.http.server.tck.tests; + +import io.micronaut.context.annotation.Requires; +import io.micronaut.http.HttpRequest; +import io.micronaut.http.HttpResponse; +import io.micronaut.http.HttpStatus; +import io.micronaut.http.annotation.Controller; +import io.micronaut.http.annotation.Get; +import io.micronaut.http.annotation.Produces; +import io.micronaut.http.server.exceptions.ExceptionHandler; +import io.micronaut.http.server.exceptions.response.ErrorContext; +import io.micronaut.http.server.exceptions.response.ErrorResponseProcessor; +import io.micronaut.http.server.tck.AssertionUtils; +import io.micronaut.http.server.tck.HttpResponseAssertion; +import static io.micronaut.http.server.tck.TestScenario.asserts; +import jakarta.inject.Singleton; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import java.io.IOException; + +@SuppressWarnings({ + "java:S5960", // We're allowed assertions, as these are used in tests only + "checkstyle:MissingJavadocType", + "checkstyle:DesignForExtension" +}) +public class StatusTest { + public static final String SPEC_NAME = "StatusTest"; + + /** + * @see micronaut-aws #1387 + * @param path Request Path + */ + @ParameterizedTest + @ValueSource(strings = {"/http-status", "/http-response-status", "/http-exception"}) + void testControllerReturningHttpStatus(String path) throws IOException { + asserts(SPEC_NAME, + HttpRequest.GET(path), + (server, request) -> AssertionUtils.assertThrows(server, request, HttpResponseAssertion.builder() + .status(HttpStatus.I_AM_A_TEAPOT) + .build())); + } + + @Requires(property = "spec.name", value = SPEC_NAME) + @Controller("/http-status") + static class HttpStatusController { + @Get + HttpStatus index() { + return HttpStatus.I_AM_A_TEAPOT; + } + } + + @Requires(property = "spec.name", value = SPEC_NAME) + @Controller("/http-response-status") + static class HttpResponseStatusController { + + @Get + HttpResponse index() { + return HttpResponse.status(HttpStatus.I_AM_A_TEAPOT); + } + } + + @Requires(property = "spec.name", value = SPEC_NAME) + @Controller("/http-exception") + static class HttpResponseErrorController { + + @Get + HttpResponse index() { + throw new TeapotException(); + } + } + + static class TeapotException extends RuntimeException { + } + + @Produces + @Singleton + static class TeapotExceptionHandler implements ExceptionHandler> { + private final ErrorResponseProcessor errorResponseProcessor; + + TeapotExceptionHandler(ErrorResponseProcessor errorResponseProcessor) { + this.errorResponseProcessor = errorResponseProcessor; + } + + @Override + public HttpResponse handle(HttpRequest request, TeapotException e) { + return errorResponseProcessor.processResponse(ErrorContext.builder(request) + .cause(e) + .build(), HttpResponse.status(HttpStatus.I_AM_A_TEAPOT)); + } + } +} diff --git a/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/VersionTest.java b/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/VersionTest.java new file mode 100644 index 00000000000..7665853c62f --- /dev/null +++ b/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/VersionTest.java @@ -0,0 +1,68 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.http.server.tck.tests; + +import io.micronaut.context.annotation.Requires; +import io.micronaut.core.util.CollectionUtils; +import io.micronaut.core.util.StringUtils; +import io.micronaut.core.version.annotation.Version; +import io.micronaut.http.HttpRequest; +import io.micronaut.http.HttpStatus; +import io.micronaut.http.annotation.Controller; +import io.micronaut.http.annotation.Get; +import io.micronaut.http.server.tck.AssertionUtils; +import io.micronaut.http.server.tck.HttpResponseAssertion; +import static io.micronaut.http.server.tck.TestScenario.asserts; +import org.junit.jupiter.api.Test; +import java.io.IOException; + +@SuppressWarnings({ + "java:S5960", // We're allowed assertions, as these are used in tests only + "checkstyle:MissingJavadocType", + "checkstyle:DesignForExtension" +}) +public class VersionTest { + public static final String SPEC_NAME = "VersionTest"; + + @Test + void testControllerMethodWithVersion2() throws IOException { + asserts(SPEC_NAME, + CollectionUtils.mapOf( + "micronaut.router.versioning.enabled", StringUtils.TRUE, + "micronaut.router.versioning.header.enabled", StringUtils.TRUE + ), HttpRequest.GET("/version/ping").header("X-API-VERSION", "2"), + (server, request) -> AssertionUtils.assertDoesNotThrow(server, request, HttpResponseAssertion.builder() + .status(HttpStatus.OK) + .body("pong v2") + .build())); + } + + @Controller("/version") + @Requires(property = "spec.name", value = SPEC_NAME) + static class ConsumesController { + + @Get("/ping") + String pingV1() { + return "pong v1"; + } + + @Version("2") + @Get("/ping") + String pingV2() { + return "pong v2"; + } + } +} diff --git a/settings.gradle b/settings.gradle index d15dc4124d5..c38cbc4b103 100644 --- a/settings.gradle +++ b/settings.gradle @@ -14,8 +14,10 @@ pluginManagement { } plugins { - id 'io.micronaut.build.shared.settings' version '5.3.14' + id 'io.micronaut.build.shared.settings' version '5.3.15' } +enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") + rootProject.name = 'micronaut' @@ -35,6 +37,7 @@ include "http-client-core" include "http-client" include "http-netty" include "http-server" +include "http-server-tck" include "http-server-netty" include "http-validation" include "inject" @@ -61,9 +64,11 @@ include "test-suite" include "test-suite-helper" include "test-suite-javax-inject" include "test-suite-jakarta-inject-bean-import" +include "test-suite-http-server-tck-netty" include "test-suite-kotlin" include "test-suite-graal" include "test-suite-groovy" +include "test-suite-groovy" include "test-utils" // benchmarks diff --git a/test-suite-http-server-tck-netty/build.gradle.kts b/test-suite-http-server-tck-netty/build.gradle.kts new file mode 100644 index 00000000000..1e36c4d3594 --- /dev/null +++ b/test-suite-http-server-tck-netty/build.gradle.kts @@ -0,0 +1,9 @@ +plugins { + id("io.micronaut.build.internal.convention-test-library") +} +dependencies { + testImplementation(projects.httpServerNetty) + testImplementation(projects.httpClient) + testImplementation(projects.httpServerTck) + testImplementation(libs.junit.platform.engine) +} diff --git a/test-suite-http-server-tck-netty/src/test/java/io/micronaut/http/server/tck/netty/tests/NettyHttpServerTestSuite.java b/test-suite-http-server-tck-netty/src/test/java/io/micronaut/http/server/tck/netty/tests/NettyHttpServerTestSuite.java new file mode 100644 index 00000000000..74d27fb1ce1 --- /dev/null +++ b/test-suite-http-server-tck-netty/src/test/java/io/micronaut/http/server/tck/netty/tests/NettyHttpServerTestSuite.java @@ -0,0 +1,11 @@ +package io.micronaut.http.server.tck.netty.tests; + +import org.junit.platform.suite.api.SelectPackages; +import org.junit.platform.suite.api.Suite; +import org.junit.platform.suite.api.SuiteDisplayName; + +@Suite +@SelectPackages("io.micronaut.http.server.tck.tests") +@SuiteDisplayName("HTTP Server TCK for Netty") +public class NettyHttpServerTestSuite { +} diff --git a/test-suite-http-server-tck-netty/src/test/resources/META-INF/services/io.micronaut.http.server.tck.ServerUnderTestProvider b/test-suite-http-server-tck-netty/src/test/resources/META-INF/services/io.micronaut.http.server.tck.ServerUnderTestProvider new file mode 100644 index 00000000000..adf15625293 --- /dev/null +++ b/test-suite-http-server-tck-netty/src/test/resources/META-INF/services/io.micronaut.http.server.tck.ServerUnderTestProvider @@ -0,0 +1 @@ +io.micronaut.http.server.tck.EmbeddedServerUnderTestProvider diff --git a/test-suite-http-server-tck-netty/src/test/resources/logback.xml b/test-suite-http-server-tck-netty/src/test/resources/logback.xml new file mode 100644 index 00000000000..8eb8c3a8170 --- /dev/null +++ b/test-suite-http-server-tck-netty/src/test/resources/logback.xml @@ -0,0 +1,10 @@ + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + From 77159dda6175b43dee161755914be0ad57a9ee7e Mon Sep 17 00:00:00 2001 From: Denis Stepanov Date: Wed, 21 Dec 2022 10:06:35 +0100 Subject: [PATCH 324/743] Fix each bean interceptor's qualifier (#8508) --- .../EachBeanInterceptorSpec.groovy | 23 +++++++++++++ .../eachbeaninterceptor/MyBean.java | 32 +++++++++++++++++++ .../eachbeaninterceptor/MyDataSource.java | 9 ++++++ .../eachbeaninterceptor/MyInterceptor.java | 26 +++++++++++++++ .../MyTransactionalConnection.java | 14 ++++++++ .../MyTransactionalConnectionAdvice.java | 16 ++++++++++ .../AbstractInitializableBeanDefinition.java | 21 +++++------- 7 files changed, 128 insertions(+), 13 deletions(-) create mode 100644 inject-java/src/test/groovy/io/micronaut/inject/configproperties/eachbeaninterceptor/EachBeanInterceptorSpec.groovy create mode 100644 inject-java/src/test/groovy/io/micronaut/inject/configproperties/eachbeaninterceptor/MyBean.java create mode 100644 inject-java/src/test/groovy/io/micronaut/inject/configproperties/eachbeaninterceptor/MyDataSource.java create mode 100644 inject-java/src/test/groovy/io/micronaut/inject/configproperties/eachbeaninterceptor/MyInterceptor.java create mode 100644 inject-java/src/test/groovy/io/micronaut/inject/configproperties/eachbeaninterceptor/MyTransactionalConnection.java create mode 100644 inject-java/src/test/groovy/io/micronaut/inject/configproperties/eachbeaninterceptor/MyTransactionalConnectionAdvice.java diff --git a/inject-java/src/test/groovy/io/micronaut/inject/configproperties/eachbeaninterceptor/EachBeanInterceptorSpec.groovy b/inject-java/src/test/groovy/io/micronaut/inject/configproperties/eachbeaninterceptor/EachBeanInterceptorSpec.groovy new file mode 100644 index 00000000000..95a81f725a1 --- /dev/null +++ b/inject-java/src/test/groovy/io/micronaut/inject/configproperties/eachbeaninterceptor/EachBeanInterceptorSpec.groovy @@ -0,0 +1,23 @@ +package io.micronaut.inject.configproperties.eachbeaninterceptor + +import io.micronaut.annotation.processing.test.AbstractTypeElementSpec +import io.micronaut.context.ApplicationContext + +class EachBeanInterceptorSpec extends AbstractTypeElementSpec { + + void 'test interceptor on an event'() { + given: + ApplicationContext ctx = ApplicationContext.run(['spec': 'EachBeanInterceptorSpec', 'mydatasources.default.xyz': '111', 'mydatasources.foo.xyz': '111', 'mydatasources.bar.xyz': '111']) + + when: + def service = ctx.getBean(MyBean) + + then: + service.getDefaultConnection().getCatalog() == "@Named('default')" + service.getFooConnection().getCatalog() == "@Named('foo')" + service.getBarConnection().getCatalog() == "@Named('bar')" + + cleanup: + ctx.close() + } +} diff --git a/inject-java/src/test/groovy/io/micronaut/inject/configproperties/eachbeaninterceptor/MyBean.java b/inject-java/src/test/groovy/io/micronaut/inject/configproperties/eachbeaninterceptor/MyBean.java new file mode 100644 index 00000000000..f3569ca6503 --- /dev/null +++ b/inject-java/src/test/groovy/io/micronaut/inject/configproperties/eachbeaninterceptor/MyBean.java @@ -0,0 +1,32 @@ +package io.micronaut.inject.configproperties.eachbeaninterceptor; + +import io.micronaut.context.annotation.Requires; +import jakarta.inject.Named; +import jakarta.inject.Singleton; + +import java.sql.Connection; + +@Requires(property = "spec", value = "EachBeanInterceptorSpec") +@Singleton +class MyBean { + + private final Connection defaultConnection, fooConnection, barConnection; + + MyBean(@Named("default") Connection defaultConnection, @Named("foo") Connection fooConnection, @Named("bar") Connection barConnection) { + this.defaultConnection = defaultConnection; + this.fooConnection = fooConnection; + this.barConnection = barConnection; + } + + public Connection getDefaultConnection() { + return defaultConnection; + } + + public Connection getFooConnection() { + return fooConnection; + } + + public Connection getBarConnection() { + return barConnection; + } +} diff --git a/inject-java/src/test/groovy/io/micronaut/inject/configproperties/eachbeaninterceptor/MyDataSource.java b/inject-java/src/test/groovy/io/micronaut/inject/configproperties/eachbeaninterceptor/MyDataSource.java new file mode 100644 index 00000000000..8c5e666eb07 --- /dev/null +++ b/inject-java/src/test/groovy/io/micronaut/inject/configproperties/eachbeaninterceptor/MyDataSource.java @@ -0,0 +1,9 @@ +package io.micronaut.inject.configproperties.eachbeaninterceptor; + +import io.micronaut.context.annotation.EachProperty; +import io.micronaut.context.annotation.Requires; + +@Requires(property = "spec", value = "EachBeanInterceptorSpec") +@EachProperty(value = "mydatasources", primary = "default") +class MyDataSource { +} diff --git a/inject-java/src/test/groovy/io/micronaut/inject/configproperties/eachbeaninterceptor/MyInterceptor.java b/inject-java/src/test/groovy/io/micronaut/inject/configproperties/eachbeaninterceptor/MyInterceptor.java new file mode 100644 index 00000000000..083463cddb1 --- /dev/null +++ b/inject-java/src/test/groovy/io/micronaut/inject/configproperties/eachbeaninterceptor/MyInterceptor.java @@ -0,0 +1,26 @@ +package io.micronaut.inject.configproperties.eachbeaninterceptor; + +import io.micronaut.aop.Interceptor; +import io.micronaut.aop.InvocationContext; +import io.micronaut.context.Qualifier; +import io.micronaut.context.annotation.Prototype; +import io.micronaut.context.annotation.Requires; +import io.micronaut.core.annotation.Nullable; + +import javax.sql.DataSource; + +@Requires(property = "spec", value = "EachBeanInterceptorSpec") +@Prototype +class MyInterceptor implements Interceptor { + private final Qualifier qualifier; + + public MyInterceptor(@Nullable Qualifier qualifier) { + this.qualifier = qualifier; + } + + @Override + public Object intercept(InvocationContext context) { + return qualifier.toString(); + } + +} diff --git a/inject-java/src/test/groovy/io/micronaut/inject/configproperties/eachbeaninterceptor/MyTransactionalConnection.java b/inject-java/src/test/groovy/io/micronaut/inject/configproperties/eachbeaninterceptor/MyTransactionalConnection.java new file mode 100644 index 00000000000..ad40c53b9b7 --- /dev/null +++ b/inject-java/src/test/groovy/io/micronaut/inject/configproperties/eachbeaninterceptor/MyTransactionalConnection.java @@ -0,0 +1,14 @@ +package io.micronaut.inject.configproperties.eachbeaninterceptor; + +import io.micronaut.context.annotation.EachBean; +import io.micronaut.context.annotation.Requires; +import io.micronaut.core.annotation.Internal; + +import java.sql.Connection; + +@Requires(property = "spec", value = "EachBeanInterceptorSpec") +@EachBean(MyDataSource.class) +@MyTransactionalConnectionAdvice +@Internal +public interface MyTransactionalConnection extends Connection { +} diff --git a/inject-java/src/test/groovy/io/micronaut/inject/configproperties/eachbeaninterceptor/MyTransactionalConnectionAdvice.java b/inject-java/src/test/groovy/io/micronaut/inject/configproperties/eachbeaninterceptor/MyTransactionalConnectionAdvice.java new file mode 100644 index 00000000000..11f3d099f26 --- /dev/null +++ b/inject-java/src/test/groovy/io/micronaut/inject/configproperties/eachbeaninterceptor/MyTransactionalConnectionAdvice.java @@ -0,0 +1,16 @@ +package io.micronaut.inject.configproperties.eachbeaninterceptor; + +import io.micronaut.aop.Introduction; +import io.micronaut.context.annotation.Type; +import io.micronaut.core.annotation.Internal; + +import java.lang.annotation.Retention; + +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +@Retention(RUNTIME) +@Introduction +@Type(MyInterceptor.class) +@Internal +@interface MyTransactionalConnectionAdvice { +} diff --git a/inject/src/main/java/io/micronaut/context/AbstractInitializableBeanDefinition.java b/inject/src/main/java/io/micronaut/context/AbstractInitializableBeanDefinition.java index 4feb31d19b5..c2e56607591 100644 --- a/inject/src/main/java/io/micronaut/context/AbstractInitializableBeanDefinition.java +++ b/inject/src/main/java/io/micronaut/context/AbstractInitializableBeanDefinition.java @@ -2330,32 +2330,27 @@ private BeanRegistration resolveBeanRegistration(BeanResolutionContext re } } + @Nullable private Qualifier resolveQualifier(BeanResolutionContext resolutionContext, Argument beanType, Argument resultType) { if (isInnerConfiguration(beanType)) { ConfigurationPath configurationPath = resolutionContext.getConfigurationPath(); Qualifier q = configurationPath.beanQualifier(); if (q instanceof Named named && resultType.isContainerType()) { return Qualifiers.byNamePrefix(named.getName()); - } else { - if (q == null && isEachBeanParent(beanType)) { - return (Qualifier) resolutionContext.getCurrentQualifier(); - } else { - return q; - } } - } else if (Qualifier.class == resultType.getType()) { + if (q == null && isEachBeanParent(beanType)) { + return (Qualifier) resolutionContext.getCurrentQualifier(); + } + return q; + } + if (Qualifier.class == resultType.getType()) { final Qualifier currentQualifier = (Qualifier) resolutionContext.getCurrentQualifier(); if (currentQualifier != null && currentQualifier.getClass() != InterceptorBindingQualifier.class && currentQualifier.getClass() != TypeAnnotationQualifier.class) { return currentQualifier; - } else { - BeanResolutionContext.Path path = resolutionContext.getPath(); - BeanResolutionContext.Segment segment = path.peek(); - if (segment != null && segment.getDeclaringType().hasAnnotation(AnnotationUtil.ANN_INTERCEPTOR_BINDINGS)) { - return resolutionContext.getConfigurationPath().beanQualifier(); - } } + return resolutionContext.getConfigurationPath().beanQualifier(); } else if (precalculatedInfo.isIterable && resultType.isAnnotationPresent(Parameter.class)) { return (Qualifier) resolutionContext.getCurrentQualifier(); } From c18072c1d02dd58b381360e14162d23a952dd503 Mon Sep 17 00:00:00 2001 From: micronaut-build <65172877+micronaut-build@users.noreply.github.com> Date: Wed, 21 Dec 2022 05:20:01 -0500 Subject: [PATCH 325/743] Bump micronaut-security to 3.9.0 (#8512) --- gradle.properties | 2 +- gradle/libs.versions.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle.properties b/gradle.properties index ee40b95b54b..222a3fba506 100644 --- a/gradle.properties +++ b/gradle.properties @@ -52,7 +52,7 @@ kotlin.stdlib.default.dependency=false # For the docs graalVersion=22.0.0.2 -micronautSecurityVersion=3.8.3 +micronautSecurityVersion=3.9.0 org.gradle.caching=true org.gradle.parallel=true diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index f5e8724ca59..7c89910c0ca 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -112,7 +112,7 @@ managed-micronaut-rss = "3.2.0" managed-micronaut-rxjava1 = "1.0.0" managed-micronaut-rxjava2 = "1.3.0" managed-micronaut-rxjava3 = "2.3.0" -managed-micronaut-security = "3.8.3" +managed-micronaut-security = "3.9.0" managed-micronaut-serialization = "1.3.3" managed-micronaut-servlet = "3.3.2" managed-micronaut-spring = "4.3.1" From 6dea3f78295f5acfaadae8aad40c8946e094fd71 Mon Sep 17 00:00:00 2001 From: micronaut-build <65172877+micronaut-build@users.noreply.github.com> Date: Wed, 21 Dec 2022 05:21:41 -0500 Subject: [PATCH 326/743] Bump micronaut-reactor to 2.5.0 (#8514) --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 7c89910c0ca..1c6b89881bb 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -106,7 +106,7 @@ managed-micronaut-picocli = "4.3.0" managed-micronaut-problem = "2.5.1" managed-micronaut-rabbitmq = "3.4.0" managed-micronaut-r2dbc = "4.0.0" -managed-micronaut-reactor = "2.4.1" +managed-micronaut-reactor = "2.5.0" managed-micronaut-redis = "5.3.2" managed-micronaut-rss = "3.2.0" managed-micronaut-rxjava1 = "1.0.0" From 2c6fe314eba1045ba9c47d91ddd5aa1142d65832 Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Wed, 21 Dec 2022 11:21:59 +0100 Subject: [PATCH 327/743] build: bump up Reactor to 3.5.0 (#8509) --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 1c6b89881bb..580a7c5721e 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -130,7 +130,7 @@ managed-netty = "4.1.84.Final" managed-reactive-pg-client = "0.11.4" managed-reactive-streams = "1.0.4" # This should be kept aligned with https://github.com/micronaut-projects/micronaut-reactor/blob/master/gradle.properties from the BOM -managed-reactor = "3.4.23" +managed-reactor = "3.5.0" managed-rxjava1 = "1.3.8" managed-rxjava1-interop = "0.13.7" managed-slf4j = "1.7.36" From 254add2b0e7411e90a09c399255e34919f26a485 Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Wed, 21 Dec 2022 11:43:32 +0100 Subject: [PATCH 328/743] fix: remove imports causing compilation errors --- .../src/test/kotlin/io/micronaut/MdcPropagationSpec.kt | 2 -- 1 file changed, 2 deletions(-) diff --git a/test-suite-kotlin/src/test/kotlin/io/micronaut/MdcPropagationSpec.kt b/test-suite-kotlin/src/test/kotlin/io/micronaut/MdcPropagationSpec.kt index 6f077c61585..b3bb3a35f58 100644 --- a/test-suite-kotlin/src/test/kotlin/io/micronaut/MdcPropagationSpec.kt +++ b/test-suite-kotlin/src/test/kotlin/io/micronaut/MdcPropagationSpec.kt @@ -22,7 +22,6 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.flow.single import kotlinx.coroutines.reactive.asFlow -import kotlinx.coroutines.reactive.awaitFirst import kotlinx.coroutines.slf4j.MDCContext import kotlinx.coroutines.withContext import org.junit.jupiter.api.Test @@ -31,7 +30,6 @@ import org.slf4j.LoggerFactory import org.slf4j.MDC import reactor.core.publisher.Flux import reactor.core.publisher.Mono -import reactor.core.publisher.toFlux import java.net.URI import java.util.* From 941659db0e8b0934521df4fce0f29c279eef1f7f Mon Sep 17 00:00:00 2001 From: Denis Stepanov Date: Wed, 21 Dec 2022 11:45:32 +0100 Subject: [PATCH 329/743] Fix each bean with a bean annotated `@Named` and `@Primary` (#8510) --- .../eachbeanparameter/AbstractDataSource.java | 4 ++ .../eachbeanparameter/DefaultDataSource.java | 13 ++++ .../EachBeanParameterSpec.groovy | 63 +++++++++++++++++++ .../eachbeanparameter/MyBean.java | 14 +++++ .../eachbeanparameter/MyDataSource.java | 9 +++ .../eachbeanparameter/MyFactory.java | 24 +++++++ .../eachbeanparameter/MyHelper.java | 4 ++ .../eachbeanparameter/MyService.java | 30 +++++++++ .../context/BeanDefinitionDelegate.java | 43 ++++++++----- .../inject/qualifiers/Qualifiers.java | 23 +++++++ 10 files changed, 210 insertions(+), 17 deletions(-) create mode 100644 inject-java/src/test/groovy/io/micronaut/inject/configproperties/eachbeanparameter/AbstractDataSource.java create mode 100644 inject-java/src/test/groovy/io/micronaut/inject/configproperties/eachbeanparameter/DefaultDataSource.java create mode 100644 inject-java/src/test/groovy/io/micronaut/inject/configproperties/eachbeanparameter/EachBeanParameterSpec.groovy create mode 100644 inject-java/src/test/groovy/io/micronaut/inject/configproperties/eachbeanparameter/MyBean.java create mode 100644 inject-java/src/test/groovy/io/micronaut/inject/configproperties/eachbeanparameter/MyDataSource.java create mode 100644 inject-java/src/test/groovy/io/micronaut/inject/configproperties/eachbeanparameter/MyFactory.java create mode 100644 inject-java/src/test/groovy/io/micronaut/inject/configproperties/eachbeanparameter/MyHelper.java create mode 100644 inject-java/src/test/groovy/io/micronaut/inject/configproperties/eachbeanparameter/MyService.java diff --git a/inject-java/src/test/groovy/io/micronaut/inject/configproperties/eachbeanparameter/AbstractDataSource.java b/inject-java/src/test/groovy/io/micronaut/inject/configproperties/eachbeanparameter/AbstractDataSource.java new file mode 100644 index 00000000000..2628ef4beb7 --- /dev/null +++ b/inject-java/src/test/groovy/io/micronaut/inject/configproperties/eachbeanparameter/AbstractDataSource.java @@ -0,0 +1,4 @@ +package io.micronaut.inject.configproperties.eachbeanparameter; + +abstract class AbstractDataSource { +} diff --git a/inject-java/src/test/groovy/io/micronaut/inject/configproperties/eachbeanparameter/DefaultDataSource.java b/inject-java/src/test/groovy/io/micronaut/inject/configproperties/eachbeanparameter/DefaultDataSource.java new file mode 100644 index 00000000000..8258cb93b28 --- /dev/null +++ b/inject-java/src/test/groovy/io/micronaut/inject/configproperties/eachbeanparameter/DefaultDataSource.java @@ -0,0 +1,13 @@ +package io.micronaut.inject.configproperties.eachbeanparameter; + +import io.micronaut.context.annotation.Primary; +import io.micronaut.context.annotation.Requires; +import jakarta.inject.Named; +import jakarta.inject.Singleton; + +@Singleton +@Requires(property = "spec", value = "EachBeanParameterSpec") +@Named("default") +@Primary +class DefaultDataSource extends AbstractDataSource { +} diff --git a/inject-java/src/test/groovy/io/micronaut/inject/configproperties/eachbeanparameter/EachBeanParameterSpec.groovy b/inject-java/src/test/groovy/io/micronaut/inject/configproperties/eachbeanparameter/EachBeanParameterSpec.groovy new file mode 100644 index 00000000000..6d640df9887 --- /dev/null +++ b/inject-java/src/test/groovy/io/micronaut/inject/configproperties/eachbeanparameter/EachBeanParameterSpec.groovy @@ -0,0 +1,63 @@ +package io.micronaut.inject.configproperties.eachbeanparameter + +import io.micronaut.annotation.processing.test.AbstractTypeElementSpec +import io.micronaut.context.ApplicationContext +import io.micronaut.context.Qualifier +import io.micronaut.context.annotation.Primary +import io.micronaut.context.exceptions.NonUniqueBeanException +import io.micronaut.inject.qualifiers.PrimaryQualifier +import io.micronaut.inject.qualifiers.Qualifiers + +class EachBeanParameterSpec extends AbstractTypeElementSpec { + + void 'test name parameter is properly injected when a bean is annotated with @Named and @Primary'() { + given: + Map datasourcesConfiguration = [ + 'mydatasources.default.xyz': '111', + 'mydatasources.foo.xyz': '111', + 'mydatasources.bar.xyz': '111' + ] + ApplicationContext ctx = ApplicationContext.run(['spec': 'EachBeanParameterSpec'] + datasourcesConfiguration) + + when: + def service = ctx.getBean(MyService) + Qualifier defaultNameQualifier = Qualifiers.byName("default") + + then: + service.getDefaultBean().name == "default" + service.getBarBean().name == "bar" + service.getFooBean().name == "foo" + + and: + ctx.getBeansOfType(AbstractDataSource).size() == datasourcesConfiguration.size() + 1 // DefaultDataSource + ctx.getBeansOfType(MyHelper).size() == datasourcesConfiguration.size() + 1 + ctx.getBeansOfType(MyBean).size() == datasourcesConfiguration.size() + 1 + ctx.getBeansOfType(AbstractDataSource).stream().filter(it -> it instanceof MyDataSource).count() == 3 + ctx.getBeansOfType(AbstractDataSource).stream().filter(it -> it instanceof DefaultDataSource).count() == 1 + ctx.getBeansOfType(AbstractDataSource, PrimaryQualifier.INSTANCE).size() == 2 + ctx.getBeansOfType(MyHelper, PrimaryQualifier.INSTANCE).size() == 2 + ctx.getBeansOfType(MyBean, PrimaryQualifier.INSTANCE).size() == 2 + + ctx.getBeansOfType(AbstractDataSource, defaultNameQualifier).size() == 2 + ctx.getBeansOfType(MyHelper, defaultNameQualifier).size() == 2 + ctx.getBeansOfType(MyBean, defaultNameQualifier).size() == 2 + + when: + ctx.getBean(AbstractDataSource, defaultNameQualifier) + then: + thrown(NonUniqueBeanException) // DefaultDataSource is annotated with `@Primary`. MyDataSource also defines primary via the annotation member `@EachProperty(value = "mydatasources", primary = "default")` + + when: + ctx.getBean(MyHelper, defaultNameQualifier) + then: + noExceptionThrown() // should not throw thrown(NonUniqueBeanException)? + + when: + ctx.getBean(MyBean, defaultNameQualifier) + then: + noExceptionThrown() // should not throw thrown(NonUniqueBeanException)? + + cleanup: + ctx.close() + } +} diff --git a/inject-java/src/test/groovy/io/micronaut/inject/configproperties/eachbeanparameter/MyBean.java b/inject-java/src/test/groovy/io/micronaut/inject/configproperties/eachbeanparameter/MyBean.java new file mode 100644 index 00000000000..3388ab9e428 --- /dev/null +++ b/inject-java/src/test/groovy/io/micronaut/inject/configproperties/eachbeanparameter/MyBean.java @@ -0,0 +1,14 @@ +package io.micronaut.inject.configproperties.eachbeanparameter; + +class MyBean { + + private final String name; + + MyBean(String name) { + this.name = name; + } + + public String getName() { + return name; + } +} diff --git a/inject-java/src/test/groovy/io/micronaut/inject/configproperties/eachbeanparameter/MyDataSource.java b/inject-java/src/test/groovy/io/micronaut/inject/configproperties/eachbeanparameter/MyDataSource.java new file mode 100644 index 00000000000..e46b6ba8f0f --- /dev/null +++ b/inject-java/src/test/groovy/io/micronaut/inject/configproperties/eachbeanparameter/MyDataSource.java @@ -0,0 +1,9 @@ +package io.micronaut.inject.configproperties.eachbeanparameter; + +import io.micronaut.context.annotation.EachProperty; +import io.micronaut.context.annotation.Requires; + +@Requires(property = "spec", value = "EachBeanParameterSpec") +@EachProperty(value = "mydatasources", primary = "default") +class MyDataSource extends AbstractDataSource { +} diff --git a/inject-java/src/test/groovy/io/micronaut/inject/configproperties/eachbeanparameter/MyFactory.java b/inject-java/src/test/groovy/io/micronaut/inject/configproperties/eachbeanparameter/MyFactory.java new file mode 100644 index 00000000000..ea1942f22b2 --- /dev/null +++ b/inject-java/src/test/groovy/io/micronaut/inject/configproperties/eachbeanparameter/MyFactory.java @@ -0,0 +1,24 @@ +package io.micronaut.inject.configproperties.eachbeanparameter; + +import io.micronaut.context.annotation.Context; +import io.micronaut.context.annotation.EachBean; +import io.micronaut.context.annotation.Factory; +import io.micronaut.context.annotation.Parameter; + +@Factory +public class MyFactory { + + @EachBean(AbstractDataSource.class) + MyHelper buildHelper() { + return new MyHelper(); + } + + @Context // The context should load without properties and properly fill the parameter from DefaultDataSource + @EachBean(MyHelper.class) + MyBean buildBean(@Parameter String name) { + // The parameter should be correctly resolved for the case where DefaultDataSource is annotated + // with both @Named and @Primary making the qualifier composite + return new MyBean(name); + } + +} diff --git a/inject-java/src/test/groovy/io/micronaut/inject/configproperties/eachbeanparameter/MyHelper.java b/inject-java/src/test/groovy/io/micronaut/inject/configproperties/eachbeanparameter/MyHelper.java new file mode 100644 index 00000000000..e8778ab9a0b --- /dev/null +++ b/inject-java/src/test/groovy/io/micronaut/inject/configproperties/eachbeanparameter/MyHelper.java @@ -0,0 +1,4 @@ +package io.micronaut.inject.configproperties.eachbeanparameter; + +class MyHelper { +} diff --git a/inject-java/src/test/groovy/io/micronaut/inject/configproperties/eachbeanparameter/MyService.java b/inject-java/src/test/groovy/io/micronaut/inject/configproperties/eachbeanparameter/MyService.java new file mode 100644 index 00000000000..c6d15cf655e --- /dev/null +++ b/inject-java/src/test/groovy/io/micronaut/inject/configproperties/eachbeanparameter/MyService.java @@ -0,0 +1,30 @@ +package io.micronaut.inject.configproperties.eachbeanparameter; + +import io.micronaut.context.annotation.Requires; +import jakarta.inject.Named; +import jakarta.inject.Singleton; + +@Requires(property = "spec", value = "EachBeanParameterSpec") +@Singleton +class MyService { + + private final MyBean defaultBean, fooBean, barBean; + + MyService(@Named("default") MyBean defaultBean, @Named("foo") MyBean fooBean, @Named("bar") MyBean barBean) { + this.defaultBean = defaultBean; + this.fooBean = fooBean; + this.barBean = barBean; + } + + public MyBean getDefaultBean() { + return defaultBean; + } + + public MyBean getFooBean() { + return fooBean; + } + + public MyBean getBarBean() { + return barBean; + } +} diff --git a/inject/src/main/java/io/micronaut/context/BeanDefinitionDelegate.java b/inject/src/main/java/io/micronaut/context/BeanDefinitionDelegate.java index c03225a3fad..4c25808e062 100644 --- a/inject/src/main/java/io/micronaut/context/BeanDefinitionDelegate.java +++ b/inject/src/main/java/io/micronaut/context/BeanDefinitionDelegate.java @@ -24,7 +24,6 @@ import io.micronaut.core.annotation.NonNull; import io.micronaut.core.annotation.Nullable; import io.micronaut.core.naming.NameResolver; -import io.micronaut.core.naming.Named; import io.micronaut.core.type.Argument; import io.micronaut.core.util.ObjectUtils; import io.micronaut.inject.BeanDefinition; @@ -39,6 +38,7 @@ import io.micronaut.inject.ParametrizedInstantiatableBeanDefinition; import io.micronaut.inject.ValidatedBeanDefinition; import io.micronaut.inject.qualifiers.PrimaryQualifier; +import io.micronaut.inject.qualifiers.Qualifiers; import java.util.Collections; import java.util.LinkedHashMap; @@ -114,11 +114,11 @@ public boolean isIterable() { @Override public boolean isPrimary() { - return isLocalQualifierPrimary() || definition.isPrimary() || isPrimaryThroughAttribute(); + return isQualifiedAsPrimary(qualifier) || definition.isPrimary() || isPrimaryThroughAttribute(); } - private boolean isLocalQualifierPrimary() { - return qualifier != null && (qualifier == PrimaryQualifier.INSTANCE || qualifier.contains(PrimaryQualifier.INSTANCE)); + private boolean isQualifiedAsPrimary(Qualifier q) { + return q != null && (q == PrimaryQualifier.INSTANCE || q.contains(PrimaryQualifier.INSTANCE)); } private boolean isPrimaryThroughAttribute() { @@ -195,20 +195,17 @@ private Map getParametersValues(BeanResolutionContext resolution if (simpleName != null) { fulfilled.put(argumentName, simpleName); } else { - Qualifier q = resolutionContext.getCurrentQualifier(); - if (q instanceof Named named) { - fulfilled.put(argumentName, named.getName()); - } else if (q == PrimaryQualifier.INSTANCE) { - fulfilled.put(argumentName, Primary.SIMPLE_NAME); + String name = findName(resolutionContext.getCurrentQualifier()); + if (name != null) { + fulfilled.put(argumentName, name); } } } else if (Number.class.isAssignableFrom(type)) { fulfilled.put(argumentName, context.getConversionService().convertRequired(configurationPath.index(), argument)); } else if (qualifier != null && hasDeclaredAnnotation(EachBean.class) && String.class.equals(type) && "name".equals(argumentName)) { - if (isLocalQualifierPrimary()) { - fulfilled.put(argumentName, Primary.SIMPLE_NAME); - } else if (qualifier instanceof Named named) { - fulfilled.put(argumentName, named.getName()); + String name = findName(qualifier); + if (name != null) { + fulfilled.put(argumentName, name); } } else { if (argument.isProvider()) { @@ -239,6 +236,21 @@ private Map getParametersValues(BeanResolutionContext resolution return fulfilled; } + @Nullable + private String findName(@Nullable Qualifier q) { + if (q == null) { + return null; + } + String name = Qualifiers.findName(q); + if (name != null) { + return name; + } + if (isQualifiedAsPrimary(q)) { + return Primary.SIMPLE_NAME; + } + return null; + } + @Override public boolean equals(Object o) { if (this == o) { @@ -267,10 +279,7 @@ public BeanDefinition getTarget() { @Override public Optional resolveName() { - if (qualifier instanceof Named named) { - return Optional.of(named.getName()); - } - return Optional.empty(); + return Optional.ofNullable(findName(qualifier)); } @Override diff --git a/inject/src/main/java/io/micronaut/inject/qualifiers/Qualifiers.java b/inject/src/main/java/io/micronaut/inject/qualifiers/Qualifiers.java index fd76fbd8855..dfa4cfbb5e4 100644 --- a/inject/src/main/java/io/micronaut/inject/qualifiers/Qualifiers.java +++ b/inject/src/main/java/io/micronaut/inject/qualifiers/Qualifiers.java @@ -130,6 +130,29 @@ public static Qualifier byName(String name) { return new NameQualifier<>(null, name); } + /** + * Finds a name in the provided qualifier. + * + * @return The qualifier + * @since 4.0.0 + */ + @Nullable + public static String findName(@NonNull Qualifier qualifier) { + if (qualifier instanceof NameQualifier nameQualifier) { + return nameQualifier.getName(); + } + if (qualifier instanceof CompositeQualifier compositeQualifier) { + for (Qualifier composite : compositeQualifier.getQualifiers()) { + String name = findName(composite); + if (name != null) { + return name; + } + } + } + return null; + } + + /** * Qualify by a prefix. Applies starting with logic to the name of the bean.. * From fe7d994242a945ae0966cdd9b1ba24828c3a6fd5 Mon Sep 17 00:00:00 2001 From: Catalin Trif Date: Wed, 21 Dec 2022 12:51:31 +0200 Subject: [PATCH 330/743] Update deployingApp.adoc (#8423) --- src/main/docs/guide/quickStart/deployingApp.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/docs/guide/quickStart/deployingApp.adoc b/src/main/docs/guide/quickStart/deployingApp.adoc index 6b9433fba62..e3d7ae1a41d 100644 --- a/src/main/docs/guide/quickStart/deployingApp.adoc +++ b/src/main/docs/guide/quickStart/deployingApp.adoc @@ -4,7 +4,7 @@ The constructed JAR file can then be executed with `java -jar`. For example: [source,bash] ---- -$ java -jar build/libs/hello-world-all.jar +$ java -jar build/libs/hello-world-0.1-all.jar ---- if building with Gradle, or From 3ff16919f949f6a347873ad06d64e6d2c88ce24e Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 22 Dec 2022 06:55:17 +0100 Subject: [PATCH 331/743] fix(deps): update dependency com.github.javaparser:javaparser-symbol-solver-core to v3.24.9 (#8477) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index c5dce3e5cfe..1484b4481c2 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -75,7 +75,7 @@ managed-reactor = "3.4.24" managed-slf4j = "2.0.4" managed-snakeyaml = "1.33" managed-validation = "2.0.1.Final" -managed-java-parser-core = "3.24.7" +managed-java-parser-core = "3.24.9" micronaut-docs = "2.0.0" [libraries] From db6e64c4b0aa1c4fa59d7f5aac99e81f9bd92ef7 Mon Sep 17 00:00:00 2001 From: micronaut-build <65172877+micronaut-build@users.noreply.github.com> Date: Thu, 22 Dec 2022 00:56:01 -0500 Subject: [PATCH 332/743] ci: github token for GraalVM action junit-report to 3.7.0 (#8506) --- .github/workflows/graalvm.yml | 3 ++- .github/workflows/gradle.yml | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/graalvm.yml b/.github/workflows/graalvm.yml index 0726bcfc327..c0f68cc0fc9 100644 --- a/.github/workflows/graalvm.yml +++ b/.github/workflows/graalvm.yml @@ -42,6 +42,7 @@ jobs: version: ${{ matrix.graalvm }} java-version: ${{ matrix.java }} components: 'native-image' + github-token: ${{ secrets.GITHUB_TOKEN }} - name: Setup Gradle uses: gradle/gradle-build-action@v2.3.3 - name: Build with Gradle @@ -73,7 +74,7 @@ jobs: }) - name: Publish Test Report if: always() - uses: mikepenz/action-junit-report@v3.6.2 + uses: mikepenz/action-junit-report@v3.7.0 with: check_name: GraalVM CE CI / Test Report (Java ${{ matrix.java }}) report_paths: '**/build/test-results/test/TEST-*.xml' diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index 1aebe19ca26..26ffebb068b 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -67,7 +67,7 @@ jobs: }) - name: Publish Test Report if: always() - uses: mikepenz/action-junit-report@v3.6.2 + uses: mikepenz/action-junit-report@v3.7.0 with: check_name: Java CI / Test Report (${{ matrix.java }}) report_paths: '**/build/test-results/test/TEST-*.xml' From 8f77cb80f46a6b3586cc6c20435467d5eb2a3c5a Mon Sep 17 00:00:00 2001 From: Denis Stepanov Date: Thu, 22 Dec 2022 06:56:30 +0100 Subject: [PATCH 333/743] fix: intercepted adapter invocation is invoked on the annotated method (#8515) --- .../aop/chain/AdapterIntroduction.java | 21 +++--- .../DeclaredBeanElementCreator.java | 14 +--- .../aop/adapter/MethodAdapterSpec.groovy | 28 +++---- .../groovy/io/micronaut/aop/adapter/Test.java | 2 + .../intercepted/InterceptedAdapterSpec.groovy | 1 + .../TransactionalEventInterceptor.java | 13 +++- .../micronaut/context/DefaultBeanContext.java | 74 +++++++++---------- 7 files changed, 76 insertions(+), 77 deletions(-) diff --git a/aop/src/main/java/io/micronaut/aop/chain/AdapterIntroduction.java b/aop/src/main/java/io/micronaut/aop/chain/AdapterIntroduction.java index 1fd9ec60153..269026259b0 100644 --- a/aop/src/main/java/io/micronaut/aop/chain/AdapterIntroduction.java +++ b/aop/src/main/java/io/micronaut/aop/chain/AdapterIntroduction.java @@ -43,7 +43,7 @@ final class AdapterIntroduction implements MethodInterceptor { * Default constructor. * * @param beanContext The bean context - * @param method The target method + * @param method The target method */ AdapterIntroduction(BeanContext beanContext, ExecutableMethod method) { Class beanType = method.classValue(Adapter.class, ADAPTED_BEAN).orElse(null); @@ -52,27 +52,28 @@ final class AdapterIntroduction implements MethodInterceptor { throw new IllegalStateException("No bean type to adapt found in Adapter configuration for method: " + method); } - String beanMethod = method.stringValue(Adapter.class, ADAPTED_METHOD).orElse(null); + String beanMethod = method.stringValue(Adapter.class, ADAPTED_METHOD).orElse(null); if (StringUtils.isEmpty(beanMethod)) { throw new IllegalStateException("No bean method to adapt found in Adapter configuration for method: " + method); } - String beanQualifier = method.stringValue(Adapter.class, ADAPTED_QUALIFIER).orElse(null); + String beanQualifier = method.stringValue(Adapter.class, ADAPTED_QUALIFIER).orElse(null); Class[] argumentTypes = method.classValues(Adapter.class, ADAPTED_ARGUMENT_TYPES); Class[] methodArgumentTypes = method.getArgumentTypes(); + Class[] arguments = argumentTypes.length == methodArgumentTypes.length ? argumentTypes : methodArgumentTypes; if (StringUtils.isNotEmpty(beanQualifier)) { this.executionHandle = beanContext.findExecutionHandle( - beanType, - Qualifiers.byName(beanQualifier), - beanMethod, - argumentTypes.length == methodArgumentTypes.length ? argumentTypes : methodArgumentTypes + beanType, + Qualifiers.byName(beanQualifier), + beanMethod, + arguments ).orElseThrow(() -> new IllegalStateException("Cannot adapt method [" + method + "]. Target method [" + beanMethod + "] not found on bean " + beanType)); } else { this.executionHandle = beanContext.findExecutionHandle( - beanType, - beanMethod, - argumentTypes.length == methodArgumentTypes.length ? argumentTypes : methodArgumentTypes + beanType, + beanMethod, + arguments ).orElseThrow(() -> new IllegalStateException("Cannot adapt method [" + method + "]. Target method [" + beanMethod + "] not found on bean " + beanType)); } } diff --git a/core-processor/src/main/java/io/micronaut/inject/processing/DeclaredBeanElementCreator.java b/core-processor/src/main/java/io/micronaut/inject/processing/DeclaredBeanElementCreator.java index 4eb94ba5676..1a8806df2c0 100644 --- a/core-processor/src/main/java/io/micronaut/inject/processing/DeclaredBeanElementCreator.java +++ b/core-processor/src/main/java/io/micronaut/inject/processing/DeclaredBeanElementCreator.java @@ -392,10 +392,10 @@ private boolean visitAopAndExecutableMethod(BeanDefinitionVisitor visitor, Metho if (preprocess) { visitor.setRequiresMethodProcessing(true); } - if (methodElement.hasStereotype("io.micronaut.aop.Adapter")) { + if (methodElement.hasStereotype(Adapter.class)) { staticMethodCheck(methodElement); - visitAdaptedMethod(visitor, methodElement); - return true; + visitAdaptedMethod(methodElement); + // Adapter is always an executable method but can also be intercepted so continue with visitors below } if (visitAopMethod(visitor, methodElement)) { return true; @@ -560,7 +560,7 @@ private boolean isDeclaredInThisClass(MemberElement memberElement) { return classElement.equals(memberElement.getDeclaringType()); } - private void visitAdaptedMethod(BeanDefinitionVisitor visitor, MethodElement sourceMethod) { + private void visitAdaptedMethod(MethodElement sourceMethod) { AnnotationMetadata methodAnnotationMetadata = sourceMethod.getDeclaredMetadata(); Optional interfaceToAdaptValue = methodAnnotationMetadata.getValue(Adapter.class, String.class) @@ -600,10 +600,6 @@ private void visitAdaptedMethod(BeanDefinitionVisitor visitor, MethodElement sou MethodElement targetMethod = methods.iterator().next(); - aopProxyWriter.visitInterceptorBinding( - InterceptedMethodUtil.resolveInterceptorBinding(methodAnnotationMetadata, InterceptorKind.AROUND) - ); - ParameterElement[] sourceParams = sourceMethod.getParameters(); ParameterElement[] targetParams = targetMethod.getParameters(); @@ -665,8 +661,6 @@ private void visitAdaptedMethod(BeanDefinitionVisitor visitor, MethodElement sou aopProxyWriter.visitAroundMethod(interfaceToAdapt, targetMethod); - visitor.visitExecutableMethod(sourceMethod.getDeclaringType(), sourceMethod, visitorContext); - beanDefinitionWriters.add(aopProxyWriter); } diff --git a/inject-java/src/test/groovy/io/micronaut/aop/adapter/MethodAdapterSpec.groovy b/inject-java/src/test/groovy/io/micronaut/aop/adapter/MethodAdapterSpec.groovy index 6ea041368ee..480358c320f 100644 --- a/inject-java/src/test/groovy/io/micronaut/aop/adapter/MethodAdapterSpec.groovy +++ b/inject-java/src/test/groovy/io/micronaut/aop/adapter/MethodAdapterSpec.groovy @@ -122,7 +122,7 @@ class Test { @Adapter(ApplicationEventListener.class) void onStartup(StartupEvent event) { - + } } @@ -148,23 +148,23 @@ import io.micronaut.runtime.event.annotation.*; public class Test { boolean invoked = false; boolean shutdown = false; - + public boolean getInvoked() { return invoked; - } + } public boolean isShutdown() { return shutdown; } - + @EventListener void receive(StartupEvent event) { invoked = true; } - + @EventListener void receive(ShutdownEvent event) { shutdown = true; - } + } } ''') @@ -203,10 +203,10 @@ public class Test { invoked = true; return CompletableFuture.completedFuture(invoked); } - + public boolean getInvoked() { return invoked; - } + } } ''') @@ -235,7 +235,7 @@ class Test { @Adapter(ApplicationEventListener.class) void onStartup(StartupEvent event) { - + } } @@ -263,7 +263,7 @@ class Test implements TestContract { @Override public void onStartup(StartupEvent event) { - + } } @@ -296,7 +296,7 @@ class Test { @Adapter(Foo.class) void myMethod(String blah) { - + } } @@ -325,7 +325,7 @@ class Test { @Adapter(Foo.class) void myMethod(Integer blah) { - + } } @@ -351,7 +351,7 @@ class Test { @Adapter(ApplicationEventListener.class) void onStartup(StartupEvent event, boolean stuff) { - + } } @@ -389,7 +389,7 @@ class EventListener { void "test adapter is invoked"() { given: - ApplicationContext ctx = ApplicationContext.run() + ApplicationContext ctx = ApplicationContext.run(["spec" : getClass().getSimpleName()]) when: Test t = ctx.getBean(Test) diff --git a/inject-java/src/test/groovy/io/micronaut/aop/adapter/Test.java b/inject-java/src/test/groovy/io/micronaut/aop/adapter/Test.java index e6620ec524a..d18f3403513 100644 --- a/inject-java/src/test/groovy/io/micronaut/aop/adapter/Test.java +++ b/inject-java/src/test/groovy/io/micronaut/aop/adapter/Test.java @@ -16,9 +16,11 @@ package io.micronaut.aop.adapter; import io.micronaut.aop.Adapter; +import io.micronaut.context.annotation.Requires; import io.micronaut.context.event.ApplicationEventListener; import io.micronaut.context.event.StartupEvent; +@Requires(property = "spec", value = "MethodAdapterSpec") @jakarta.inject.Singleton class Test { diff --git a/inject-java/src/test/groovy/io/micronaut/aop/adapter/intercepted/InterceptedAdapterSpec.groovy b/inject-java/src/test/groovy/io/micronaut/aop/adapter/intercepted/InterceptedAdapterSpec.groovy index c1e948cac16..c77b3dbca09 100644 --- a/inject-java/src/test/groovy/io/micronaut/aop/adapter/intercepted/InterceptedAdapterSpec.groovy +++ b/inject-java/src/test/groovy/io/micronaut/aop/adapter/intercepted/InterceptedAdapterSpec.groovy @@ -22,6 +22,7 @@ class InterceptedAdapterSpec extends AbstractTypeElementSpec { then: service.count == 1 interceptor.count == 1 + interceptor.executableMethod.name == "test" cleanup: ctx.close() diff --git a/inject-java/src/test/groovy/io/micronaut/aop/adapter/intercepted/TransactionalEventInterceptor.java b/inject-java/src/test/groovy/io/micronaut/aop/adapter/intercepted/TransactionalEventInterceptor.java index ff40f387891..e704465c77b 100644 --- a/inject-java/src/test/groovy/io/micronaut/aop/adapter/intercepted/TransactionalEventInterceptor.java +++ b/inject-java/src/test/groovy/io/micronaut/aop/adapter/intercepted/TransactionalEventInterceptor.java @@ -1,15 +1,20 @@ package io.micronaut.aop.adapter.intercepted; -import io.micronaut.aop.Interceptor; -import io.micronaut.aop.InvocationContext; +import io.micronaut.aop.MethodInterceptor; +import io.micronaut.aop.MethodInvocationContext; +import io.micronaut.inject.ExecutableMethod; import jakarta.inject.Singleton; @Singleton -class TransactionalEventInterceptor implements Interceptor { +class TransactionalEventInterceptor implements MethodInterceptor { long count = 0; + ExecutableMethod executableMethod; + @Override - public Object intercept(InvocationContext context) { + public Object intercept(MethodInvocationContext context) { count++; + executableMethod = context.getExecutableMethod(); return context.proceed(); } + } diff --git a/inject/src/main/java/io/micronaut/context/DefaultBeanContext.java b/inject/src/main/java/io/micronaut/context/DefaultBeanContext.java index 5354c998a46..505a7b74078 100644 --- a/inject/src/main/java/io/micronaut/context/DefaultBeanContext.java +++ b/inject/src/main/java/io/micronaut/context/DefaultBeanContext.java @@ -661,52 +661,48 @@ public ExecutableMethod getExecutableMethod() { @SuppressWarnings("unchecked") @Override - public Optional> findExecutionHandle(Class beanType, Qualifier qualifier, String method, Class... arguments) { - Optional> foundBean = findBeanDefinition(beanType, (Qualifier) qualifier); - if (foundBean.isPresent()) { - BeanDefinition beanDefinition = foundBean.get(); - Optional> foundMethod = beanDefinition.findMethod(method, arguments); - if (foundMethod.isPresent()) { - return foundMethod.map((ExecutableMethod executableMethod) -> - new BeanExecutionHandle(this, beanType, qualifier, executableMethod) - ); - } else { - return beanDefinition.findPossibleMethods(method) - .findFirst() - .filter(m -> { - Class[] argTypes = m.getArgumentTypes(); - if (argTypes.length == arguments.length) { - for (int i = 0; i < argTypes.length; i++) { - if (!arguments[i].isAssignableFrom(argTypes[i])) { - return false; - } - } - return true; + public Optional> findExecutionHandle(Class beanType, Qualifier q, String method, Class... arguments) { + Qualifier qualifier = (Qualifier) q; + Optional> foundBean = findBeanDefinition(beanType, qualifier); + if (foundBean.isEmpty()) { + return Optional.empty(); + } + BeanDefinition beanDefinition = foundBean.get(); + Optional> foundMethod = beanDefinition.findMethod(method, arguments); + if (foundMethod.isEmpty()) { + foundMethod = beanDefinition.findPossibleMethods(method) + .findFirst() + .filter(m -> { + Class[] argTypes = m.getArgumentTypes(); + if (argTypes.length == arguments.length) { + for (int i = 0; i < argTypes.length; i++) { + if (!arguments[i].isAssignableFrom(argTypes[i])) { + return false; } - return false; - }) - .map((ExecutableMethod executableMethod) -> new BeanExecutionHandle(this, beanType, qualifier, executableMethod)); - } + } + return true; + } + return false; + }); } - return Optional.empty(); + return foundMethod.map(executableMethod -> new BeanExecutionHandle<>(this, beanType, qualifier, executableMethod)); } @Override public Optional> findExecutableMethod(Class beanType, String method, Class[] arguments) { - if (beanType != null) { - Collection> definitions = getBeanDefinitions(beanType); - if (!definitions.isEmpty()) { - BeanDefinition beanDefinition = definitions.iterator().next(); - Optional> foundMethod = beanDefinition.findMethod(method, arguments); - if (foundMethod.isPresent()) { - return foundMethod; - } else { - return beanDefinition.findPossibleMethods(method) - .findFirst(); - } - } + if (beanType == null) { + return Optional.empty(); } - return Optional.empty(); + Collection> definitions = getBeanDefinitions(beanType); + if (definitions.isEmpty()) { + return Optional.empty(); + } + BeanDefinition beanDefinition = definitions.iterator().next(); + Optional> foundMethod = beanDefinition.findMethod(method, arguments); + if (foundMethod.isPresent()) { + return foundMethod; + } + return beanDefinition.findPossibleMethods(method).findFirst(); } @SuppressWarnings("unchecked") From 89072456475427ba9a7bbc15a0a7145a33e51d9e Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 22 Dec 2022 06:56:57 +0100 Subject: [PATCH 334/743] fix(deps): update dependency com.blazebit:blaze-persistence-core-impl to v1.6.8 (#8460) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 1484b4481c2..32c3987999e 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -2,7 +2,7 @@ asm = "9.4" awaitility = "4.2.0" bcpkix = "1.70" -blaze = "1.6.7" +blaze = "1.6.8" caffeine = "2.9.3" compile-testing = "0.19" From ce03a56e709cbe0b2c8719d78e123a504aefa933 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 22 Dec 2022 12:13:14 +0100 Subject: [PATCH 335/743] fix(deps): update netty monorepo (#8379) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 32c3987999e..f82e0e1a540 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -68,7 +68,7 @@ managed-jackson = "2.14.0" managed-jackson-databind = "2.14.0" managed-maven-native-plugin = "0.9.13" managed-methvin-directory-watcher = "0.16.1" -managed-netty = "4.1.84.Final" +managed-netty = "4.1.86.Final" managed-reactive-streams = "1.0.4" # This should be kept aligned with https://github.com/micronaut-projects/micronaut-reactor/blob/master/gradle.properties from the BOM managed-reactor = "3.4.24" From 3ee4a73a66bc828f6c5051523fd69d36f951b970 Mon Sep 17 00:00:00 2001 From: Denis Stepanov Date: Thu, 22 Dec 2022 12:28:27 +0100 Subject: [PATCH 336/743] feat: `Publishers#convertPublisher` needs to use `ConversionService` (#8517) --- .../core/async/publisher/Publishers.java | 30 ++++++++++++++----- .../micronaut/core/util/CollectionUtils.java | 2 +- .../AbstractNettyWebSocketHandler.java | 2 +- .../server/netty/RoutingInBoundHandler.java | 2 +- .../netty/jackson/JsonViewServerFilter.java | 14 +++++---- .../NettyServerWebSocketHandler.java | 2 +- .../micronaut/http/server/RouteExecutor.java | 11 ++++--- .../DefaultAnnotatedElementValidator.java | 5 ++-- .../validator/DefaultValidator.java | 15 ++++++---- .../DefaultValidatorConfiguration.java | 9 +++++- .../validator/DefaultValidatorFactory.java | 15 +++++++--- .../validation/validator/Validator.java | 8 +++-- 12 files changed, 79 insertions(+), 36 deletions(-) diff --git a/core-reactive/src/main/java/io/micronaut/core/async/publisher/Publishers.java b/core-reactive/src/main/java/io/micronaut/core/async/publisher/Publishers.java index 9e6124f9f06..3a4ceebbd94 100644 --- a/core-reactive/src/main/java/io/micronaut/core/async/publisher/Publishers.java +++ b/core-reactive/src/main/java/io/micronaut/core/async/publisher/Publishers.java @@ -442,12 +442,28 @@ public static boolean isConvertibleToPublisher(Object object) { /** * Attempts to convert the publisher to the given type. * - * @param object The object to convert + * @param object The object to convert * @param publisherType The publisher type - * @param The generic type + * @param The generic type * @return The Resulting in publisher + * @deprecated replaced by {@link #convertPublisher(ConversionService, Object, Class)} */ + @Deprecated(since = "4") public static T convertPublisher(Object object, Class publisherType) { + return convertPublisher(ConversionService.SHARED, object, publisherType); + } + + /** + * Attempts to convert the publisher to the given type. + * + * @param conversionService The conversion service + * @param object The object to convert + * @param publisherType The publisher type + * @param The generic type + * @return The Resulting in publisher + * @since 4.0.0 + */ + public static T convertPublisher(ConversionService conversionService, Object object, Class publisherType) { Objects.requireNonNull(object, "Argument [object] cannot be null"); Objects.requireNonNull(publisherType, "Argument [publisherType] cannot be null"); if (publisherType.isInstance(object)) { @@ -455,14 +471,14 @@ public static T convertPublisher(Object object, Class publisherType) { } if (object instanceof CompletableFuture) { @SuppressWarnings("unchecked") Publisher futurePublisher = Publishers.fromCompletableFuture(() -> ((CompletableFuture) object)); - return ConversionService.SHARED.convert(futurePublisher, publisherType) + return conversionService.convert(futurePublisher, publisherType) .orElseThrow(() -> unconvertibleError(object, publisherType)); - } else if (object instanceof MicronautPublisher && MicronautPublisher.class.isAssignableFrom(publisherType)) { + } + if (object instanceof MicronautPublisher && MicronautPublisher.class.isAssignableFrom(publisherType)) { return (T) object; - } else { - return ConversionService.SHARED.convert(object, publisherType) - .orElseThrow(() -> unconvertibleError(object, publisherType)); } + return conversionService.convert(object, publisherType) + .orElseThrow(() -> unconvertibleError(object, publisherType)); } /** diff --git a/core/src/main/java/io/micronaut/core/util/CollectionUtils.java b/core/src/main/java/io/micronaut/core/util/CollectionUtils.java index 30c5518732d..5fcdeced248 100644 --- a/core/src/main/java/io/micronaut/core/util/CollectionUtils.java +++ b/core/src/main/java/io/micronaut/core/util/CollectionUtils.java @@ -229,7 +229,7 @@ public static String toString(String delimiter, Iterable iterable) { continue; } else { if (CharSequence.class.isInstance(o)) { - builder.append(o.toString()); + builder.append(o); } else { Optional converted = ConversionService.SHARED.convert(o, String.class); converted.ifPresent(builder::append); diff --git a/http-netty/src/main/java/io/micronaut/http/netty/websocket/AbstractNettyWebSocketHandler.java b/http-netty/src/main/java/io/micronaut/http/netty/websocket/AbstractNettyWebSocketHandler.java index 750110e3077..b435fabe0f1 100644 --- a/http-netty/src/main/java/io/micronaut/http/netty/websocket/AbstractNettyWebSocketHandler.java +++ b/http-netty/src/main/java/io/micronaut/http/netty/websocket/AbstractNettyWebSocketHandler.java @@ -271,7 +271,7 @@ public void handlerRemoved(ChannelHandlerContext ctx) throws Exception { * @return The flowable */ protected Publisher instrumentPublisher(ChannelHandlerContext ctx, Object result) { - Publisher actual = Publishers.convertPublisher(result, Publisher.class); + Publisher actual = Publishers.convertPublisher(conversionService, result, Publisher.class); return Flux.from(actual).subscribeOn(Schedulers.fromExecutorService(ctx.channel().eventLoop())); } diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/RoutingInBoundHandler.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/RoutingInBoundHandler.java index 8f976445671..5fd8a56a56f 100644 --- a/http-server-netty/src/main/java/io/micronaut/http/server/netty/RoutingInBoundHandler.java +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/RoutingInBoundHandler.java @@ -362,7 +362,7 @@ private Flux mapToHttpContent(NettyHttpRequest request, isJsonFormattable(hasRouteInfo ? routeInfo.getBodyType() : null); NettyByteBufferFactory byteBufferFactory = new NettyByteBufferFactory(context.alloc()); - Flux bodyPublisher = Flux.from(Publishers.convertPublisher(body, Publisher.class)); + Flux bodyPublisher = Flux.from(Publishers.convertPublisher(conversionService, body, Publisher.class)); MediaType finalMediaType = mediaType; Flux httpContentPublisher = bodyPublisher.map(message -> { diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/jackson/JsonViewServerFilter.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/jackson/JsonViewServerFilter.java index e384d0235df..a7c25493eb3 100644 --- a/http-server-netty/src/main/java/io/micronaut/http/server/netty/jackson/JsonViewServerFilter.java +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/jackson/JsonViewServerFilter.java @@ -19,6 +19,7 @@ import io.micronaut.context.annotation.Requires; import io.micronaut.core.annotation.AnnotationValue; import io.micronaut.core.async.publisher.Publishers; +import io.micronaut.core.convert.ConversionService; import io.micronaut.core.type.Argument; import io.micronaut.http.HttpAttributes; import io.micronaut.http.HttpRequest; @@ -60,16 +61,19 @@ public class JsonViewServerFilter implements HttpServerFilter { private final JsonViewCodecResolver codecFactory; private final ExecutorService executorService; + private final ConversionService conversionService; /** * @param jsonViewCodecResolver The JSON view codec resolver. - * @param executorService The I/O executor service + * @param executorService The I/O executor service + * @param conversionService The conversion service */ - public JsonViewServerFilter( - JsonViewCodecResolver jsonViewCodecResolver, - @Named(TaskExecutors.BLOCKING) ExecutorService executorService) { + public JsonViewServerFilter(JsonViewCodecResolver jsonViewCodecResolver, + @Named(TaskExecutors.BLOCKING) ExecutorService executorService, + ConversionService conversionService) { this.codecFactory = jsonViewCodecResolver; this.executorService = executorService; + this.conversionService = conversionService; } @Override @@ -87,7 +91,7 @@ public Publisher> doFilter(HttpRequest request, Server Object body = optionalBody.get(); MediaTypeCodec codec = codecFactory.resolveJsonViewCodec(viewClass.get()); if (Publishers.isConvertibleToPublisher(body)) { - Publisher pub = Publishers.convertPublisher(body, Publisher.class); + Publisher pub = Publishers.convertPublisher(conversionService, body, Publisher.class); response.body(Flux.from(pub) .map(o -> codec.encode((Argument) routeInfo.getBodyType(), o)) .subscribeOn(Schedulers.fromExecutorService(executorService))); diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/websocket/NettyServerWebSocketHandler.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/websocket/NettyServerWebSocketHandler.java index 7d0d99344bb..7a473380d44 100644 --- a/http-server-netty/src/main/java/io/micronaut/http/server/netty/websocket/NettyServerWebSocketHandler.java +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/websocket/NettyServerWebSocketHandler.java @@ -279,7 +279,7 @@ public ConvertibleValues getUriVariables() { @Override protected Publisher instrumentPublisher(ChannelHandlerContext ctx, Object result) { - Publisher actual = Publishers.convertPublisher(result, Publisher.class); + Publisher actual = Publishers.convertPublisher(conversionService, result, Publisher.class); Publisher traced = (Publisher) subscriber -> ServerRequestContext.with(originatingRequest, () -> actual.subscribe(new Subscriber() { @Override diff --git a/http-server/src/main/java/io/micronaut/http/server/RouteExecutor.java b/http-server/src/main/java/io/micronaut/http/server/RouteExecutor.java index a2ef6e0e5d1..e0aba9c2957 100644 --- a/http-server/src/main/java/io/micronaut/http/server/RouteExecutor.java +++ b/http-server/src/main/java/io/micronaut/http/server/RouteExecutor.java @@ -21,6 +21,7 @@ import io.micronaut.core.annotation.NonNull; import io.micronaut.core.annotation.Nullable; import io.micronaut.core.async.publisher.Publishers; +import io.micronaut.core.convert.ConversionService; import io.micronaut.core.execution.ExecutionFlow; import io.micronaut.core.io.buffer.ReferenceCounted; import io.micronaut.core.type.Argument; @@ -100,6 +101,7 @@ public final class RouteExecutor { final ErrorResponseProcessor errorResponseProcessor; private final ExecutorSelector executorSelector; private final Optional coroutineHelper; + private final ConversionService conversionService; /** * Default constructor. @@ -124,6 +126,7 @@ public RouteExecutor(Router router, this.errorResponseProcessor = errorResponseProcessor; this.executorSelector = executorSelector; this.coroutineHelper = beanContext.findBean(CoroutineHelper.class); + this.conversionService = beanContext.getConversionService(); } /** @@ -575,7 +578,7 @@ private CorePublisher> fromReactiveExecute(HttpRequest boolean isCompletable = !isSingle && routeInfo.isVoid() && Publishers.isCompletable(bodyClass); if (isSingle || isCompletable) { // full response case - Publisher publisher = Publishers.convertPublisher(body, Publisher.class); + Publisher publisher = Publishers.convertPublisher(conversionService, body, Publisher.class); Supplier> emptyResponse = () -> { MutableHttpResponse singleResponse; if (isCompletable || routeInfo.isVoid()) { @@ -618,7 +621,7 @@ private CorePublisher> fromReactiveExecute(HttpRequest Argument typeArgument = routeInfo.getReturnType().getFirstTypeVariable().orElse(Argument.OBJECT_ARGUMENT); if (HttpResponse.class.isAssignableFrom(typeArgument.getType())) { // a response stream - Publisher> bodyPublisher = Publishers.convertPublisher(body, Publisher.class); + Publisher> bodyPublisher = Publishers.convertPublisher(conversionService, body, Publisher.class); Flux> response = Flux.from(bodyPublisher) .map(this::toMutableResponse); Argument bodyArgument = typeArgument.getFirstTypeVariable().orElse(Argument.OBJECT_ARGUMENT); @@ -640,14 +643,14 @@ private Mono> processPublisherBody(HttpRequest request return Mono.just(response); } if (Publishers.isSingle(body.getClass())) { - return Mono.from(Publishers.convertPublisher(body, Publisher.class)).map(b -> { + return Mono.from(Publishers.convertPublisher(conversionService, body, Publisher.class)).map(b -> { response.body(b); return response; }); } MediaType mediaType = response.getContentType().orElseGet(() -> resolveDefaultResponseContentType(request, routeInfo)); - Flux bodyPublisher = applyExecutorToPublisher(Publishers.convertPublisher(body, Publisher.class), findExecutor(routeInfo)); + Flux bodyPublisher = applyExecutorToPublisher(Publishers.convertPublisher(conversionService, body, Publisher.class), findExecutor(routeInfo)); return Mono.just(response .header(HttpHeaders.TRANSFER_ENCODING, "chunked") diff --git a/validation/src/main/java/io/micronaut/validation/validator/DefaultAnnotatedElementValidator.java b/validation/src/main/java/io/micronaut/validation/validator/DefaultAnnotatedElementValidator.java index a59a57de896..af78e7f53b1 100644 --- a/validation/src/main/java/io/micronaut/validation/validator/DefaultAnnotatedElementValidator.java +++ b/validation/src/main/java/io/micronaut/validation/validator/DefaultAnnotatedElementValidator.java @@ -17,6 +17,7 @@ import io.micronaut.core.annotation.Internal; import io.micronaut.core.annotation.NonNull; +import io.micronaut.core.convert.ConversionService; import io.micronaut.core.io.service.SoftServiceLoader; import io.micronaut.core.reflect.GenericTypeUtils; import io.micronaut.core.util.ArrayUtils; @@ -45,8 +46,8 @@ public class DefaultAnnotatedElementValidator extends DefaultValidator implement * Default constructor. */ public DefaultAnnotatedElementValidator() { - super(new DefaultValidatorConfiguration() - .setConstraintValidatorRegistry(new LocalConstraintValidators())); + super(new DefaultValidatorConfiguration(ConversionService.SHARED) + .setConstraintValidatorRegistry(new LocalConstraintValidators()), ConversionService.SHARED); } /** diff --git a/validation/src/main/java/io/micronaut/validation/validator/DefaultValidator.java b/validation/src/main/java/io/micronaut/validation/validator/DefaultValidator.java index dc54e9a0f5f..e230e0cf732 100644 --- a/validation/src/main/java/io/micronaut/validation/validator/DefaultValidator.java +++ b/validation/src/main/java/io/micronaut/validation/validator/DefaultValidator.java @@ -34,6 +34,7 @@ import io.micronaut.core.beans.BeanIntrospection; import io.micronaut.core.beans.BeanIntrospector; import io.micronaut.core.beans.BeanProperty; +import io.micronaut.core.convert.ConversionService; import io.micronaut.core.reflect.ClassUtils; import io.micronaut.core.type.Argument; import io.micronaut.core.type.ArgumentValue; @@ -117,14 +118,16 @@ public class DefaultValidator implements Validator, ExecutableMethodValidator, R private final TraversableResolver traversableResolver; private final ExecutionHandleLocator executionHandleLocator; private final MessageSource messageSource; + private final ConversionService conversionService; /** * Default constructor. * - * @param configuration The validator configuration + * @param configuration The validator configuration + * @param conversionService The conversion service */ - protected DefaultValidator( - @NonNull ValidatorConfiguration configuration) { + protected DefaultValidator(@NonNull ValidatorConfiguration configuration, ConversionService conversionService) { + this.conversionService = conversionService; ArgumentUtils.requireNonNull("configuration", configuration); this.constraintValidatorRegistry = configuration.getConstraintValidatorRegistry(); this.clockProvider = configuration.getClockProvider(); @@ -901,7 +904,7 @@ private void instrumentPublisherArgumentWithValidation( AnnotationMetadata annotationMetadata, Object parameterValue, boolean isValid) { - final Publisher publisher = Publishers.convertPublisher(parameterValue, Publisher.class); + final Publisher publisher = Publishers.convertPublisher(conversionService, parameterValue, Publisher.class); PathImpl copied = new PathImpl(context.currentPath); final Flux finalFlowable = Flux.from(publisher).flatMap(o -> { DefaultConstraintValidatorContext newContext = @@ -949,7 +952,7 @@ private void instrumentPublisherArgumentWithValidation( return Flux.just(o); }); - argumentValues[argumentIndex] = Publishers.convertPublisher(finalFlowable, parameterType); + argumentValues[argumentIndex] = Publishers.convertPublisher(conversionService, finalFlowable, parameterType); } private void instrumentCompletionStageArgumentWithValidation( @@ -1598,7 +1601,7 @@ private String buildMessageTemplate(final DefaultConstraintValidatorContext cont @Override public Publisher validatePublisher(@NonNull Publisher publisher, Class... groups) { ArgumentUtils.requireNonNull("publisher", publisher); - final Publisher reactiveSequence = Publishers.convertPublisher(publisher, Publisher.class); + final Publisher reactiveSequence = Publishers.convertPublisher(conversionService, publisher, Publisher.class); return Flux.from(reactiveSequence).flatMap(object -> { final Set> constraintViolations = validate(object, groups); if (!constraintViolations.isEmpty()) { diff --git a/validation/src/main/java/io/micronaut/validation/validator/DefaultValidatorConfiguration.java b/validation/src/main/java/io/micronaut/validation/validator/DefaultValidatorConfiguration.java index 00b98b05364..a146080cd82 100644 --- a/validation/src/main/java/io/micronaut/validation/validator/DefaultValidatorConfiguration.java +++ b/validation/src/main/java/io/micronaut/validation/validator/DefaultValidatorConfiguration.java @@ -20,6 +20,7 @@ import io.micronaut.context.annotation.ConfigurationProperties; import io.micronaut.core.annotation.NonNull; import io.micronaut.core.annotation.Nullable; +import io.micronaut.core.convert.ConversionService; import io.micronaut.core.util.Toggleable; import io.micronaut.validation.validator.constraints.ConstraintValidatorRegistry; import io.micronaut.validation.validator.constraints.DefaultConstraintValidators; @@ -48,6 +49,8 @@ @ConfigurationProperties(ValidatorConfiguration.PREFIX) public class DefaultValidatorConfiguration implements ValidatorConfiguration, Toggleable, ValidatorContext { + private final ConversionService conversionService; + @Nullable private ConstraintValidatorRegistry constraintValidatorRegistry; @@ -68,6 +71,10 @@ public class DefaultValidatorConfiguration implements ValidatorConfiguration, To private boolean enabled = true; + public DefaultValidatorConfiguration(ConversionService conversionService) { + this.conversionService = conversionService; + } + @Override @NonNull public ConstraintValidatorRegistry getConstraintValidatorRegistry() { @@ -253,6 +260,6 @@ public ValidatorContext addValueExtractor(ValueExtractor extractor) { @Override public Validator getValidator() { - return new DefaultValidator(this); + return new DefaultValidator(this, conversionService); } } diff --git a/validation/src/main/java/io/micronaut/validation/validator/DefaultValidatorFactory.java b/validation/src/main/java/io/micronaut/validation/validator/DefaultValidatorFactory.java index d10f18504f4..9ea62c996da 100644 --- a/validation/src/main/java/io/micronaut/validation/validator/DefaultValidatorFactory.java +++ b/validation/src/main/java/io/micronaut/validation/validator/DefaultValidatorFactory.java @@ -17,6 +17,7 @@ import io.micronaut.context.annotation.Requires; import io.micronaut.core.annotation.Internal; +import io.micronaut.core.convert.ConversionService; import jakarta.inject.Singleton; import javax.validation.ClockProvider; @@ -38,15 +39,21 @@ @Singleton public class DefaultValidatorFactory implements ValidatorFactory { + private final ConversionService conversionService; private final Validator validator; private final ValidatorConfiguration configuration; /** * Default constructor. - * @param validator The validator. - * @param configuration The configuration. + * + * @param conversionService The conversion service + * @param validator The validator. + * @param configuration The configuration. */ - protected DefaultValidatorFactory(Validator validator, ValidatorConfiguration configuration) { + protected DefaultValidatorFactory(ConversionService conversionService, + Validator validator, + ValidatorConfiguration configuration) { + this.conversionService = conversionService; this.validator = validator; this.configuration = configuration; } @@ -58,7 +65,7 @@ public javax.validation.Validator getValidator() { @Override public ValidatorContext usingContext() { - return new DefaultValidatorConfiguration(); + return new DefaultValidatorConfiguration(conversionService); } @Override diff --git a/validation/src/main/java/io/micronaut/validation/validator/Validator.java b/validation/src/main/java/io/micronaut/validation/validator/Validator.java index 5e937ff5366..54a175d01c4 100644 --- a/validation/src/main/java/io/micronaut/validation/validator/Validator.java +++ b/validation/src/main/java/io/micronaut/validation/validator/Validator.java @@ -18,6 +18,7 @@ import io.micronaut.core.annotation.NonNull; import io.micronaut.core.annotation.Nullable; import io.micronaut.core.beans.BeanIntrospection; +import io.micronaut.core.convert.ConversionService; import javax.validation.Constraint; import javax.validation.ConstraintViolation; @@ -87,13 +88,14 @@ Set> validate( /** * Constructs a new default instance. Note that the returned instance will not contain * managed {@link io.micronaut.validation.validator.constraints.ConstraintValidator} instances and using - * {@link javax.inject.Inject} should be preferred. + * {@link jakarta.inject.Inject} should be preferred. * * @return The validator. */ static @NonNull Validator getInstance() { + ConversionService conversionService = ConversionService.SHARED; return new DefaultValidator( - new DefaultValidatorConfiguration() - ); + new DefaultValidatorConfiguration(conversionService), + conversionService); } } From 8a6a89718607e179de8b4adf18aca8f42c6caa82 Mon Sep 17 00:00:00 2001 From: micronaut-build <65172877+micronaut-build@users.noreply.github.com> Date: Thu, 22 Dec 2022 06:29:03 -0500 Subject: [PATCH 337/743] build: Bump micronaut-azure to 4.0.0 (#8518) --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 580a7c5721e..57e05a2baa9 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -68,7 +68,7 @@ managed-micrometer = "1.9.4" managed-micronaut-acme = "3.2.0" managed-micronaut-aot = "1.1.1" managed-micronaut-aws = "3.10.0" -managed-micronaut-azure = "3.6.0" +managed-micronaut-azure = "4.0.0" managed-micronaut-cache = "3.5.0" managed-micronaut-cassandra = "5.1.1" managed-micronaut-coherence = "3.5.1" From 43b22a41101b924fd8b50ffbf82b155b80b86957 Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Thu, 22 Dec 2022 12:29:13 +0100 Subject: [PATCH 338/743] build: update netty from 4.1.84 to 4.1.86.Final (#8519) https://netty.io/news/2022/12/12/4-1-86-Final.html https://netty.io/news/2022/11/09/4-1-85-Final.html --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 57e05a2baa9..00ae49870c9 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -126,7 +126,7 @@ managed-micronaut-views = "3.7.1" managed-micronaut-xml = "3.2.0" managed-neo4j = "3.5.35" managed-neo4j-java-driver = "4.4.9" -managed-netty = "4.1.84.Final" +managed-netty = "4.1.86.Final" managed-reactive-pg-client = "0.11.4" managed-reactive-streams = "1.0.4" # This should be kept aligned with https://github.com/micronaut-projects/micronaut-reactor/blob/master/gradle.properties from the BOM From f8a2fffaffce9c0e4450d58a3724e60a187503bd Mon Sep 17 00:00:00 2001 From: micronaut-build <65172877+micronaut-build@users.noreply.github.com> Date: Thu, 22 Dec 2022 06:45:45 -0500 Subject: [PATCH 339/743] Bump micronaut-toml to 1.1.3 (#8408) * Bump micronaut-toml to 1.1.3 * Update libs.versions.toml Co-authored-by: Sergio del Amo --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 00ae49870c9..4147e07b4ea 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -119,7 +119,7 @@ managed-micronaut-spring = "4.3.1" managed-micronaut-sql = "4.7.2" managed-micronaut-test = "3.8.0" managed-micronaut-test-resources = "1.2.3" -managed-micronaut-toml = "1.1.1" +managed-micronaut-toml = "1.1.3" managed-micronaut-tracing = "4.4.0" managed-micronaut-tracing-legacy = "3.2.7" managed-micronaut-views = "3.7.1" From c2048b5db40ba23319778320b2335597f2d53795 Mon Sep 17 00:00:00 2001 From: micronaut-build <65172877+micronaut-build@users.noreply.github.com> Date: Thu, 22 Dec 2022 06:46:30 -0500 Subject: [PATCH 340/743] Bump micronaut-oracle-cloud to 2.3.1 (#8401) Co-authored-by: Sergio del Amo --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 4147e07b4ea..868489ada78 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -101,7 +101,7 @@ managed-micronaut-nats = "3.1.0" managed-micronaut-netflix = "2.1.0" managed-micronaut-object-storage = "1.1.0" managed-micronaut-openapi = "4.8.1" -managed-micronaut-oraclecloud = "2.3.0" +managed-micronaut-oraclecloud = "2.3.1" managed-micronaut-picocli = "4.3.0" managed-micronaut-problem = "2.5.1" managed-micronaut-rabbitmq = "3.4.0" From 12e203e5f583f6e1df88b9e98249401f5fb9a530 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 22 Dec 2022 12:47:48 +0100 Subject: [PATCH 341/743] fix(deps): update dependency io.micrometer:micrometer-core to v1.10.2 (#8381) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index f82e0e1a540..9ac58b78578 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -47,7 +47,7 @@ micronaut-sql = "4.7.2" micronaut-test = "4.0.0-SNAPSHOT" micronaut-serde = "2.0.0-SNAPSHOT" micronaut-tracing = "4.4.0" -micrometer = "1.9.5" +micrometer = "1.10.2" neo4j-java-driver = "1.4.5" selenium = "3.141.59" smallrye = "5.5.0" From 779884e15d15154571c8a7d5bf6369a2da35855d Mon Sep 17 00:00:00 2001 From: micronaut-build <65172877+micronaut-build@users.noreply.github.com> Date: Thu, 22 Dec 2022 06:49:56 -0500 Subject: [PATCH 342/743] Bump micronaut-coherence to 3.7.2 (#8318) --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 38922cd9f20..fb83b08e8b9 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -71,7 +71,7 @@ managed-micronaut-aws = "3.9.3" managed-micronaut-azure = "3.5.0" managed-micronaut-cache = "3.5.0" managed-micronaut-cassandra = "5.1.1" -managed-micronaut-coherence = "3.5.1" +managed-micronaut-coherence = "3.7.2" managed-micronaut-crac = "1.0.1" managed-micronaut-data = "3.8.1" managed-micronaut-discovery = "3.2.0" From ad014803c20c20eecb156558dbd0646b879024e7 Mon Sep 17 00:00:00 2001 From: micronaut-build <65172877+micronaut-build@users.noreply.github.com> Date: Thu, 22 Dec 2022 06:51:16 -0500 Subject: [PATCH 343/743] Bump micronaut-groovy to 3.4.0 (#8129) --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 868489ada78..36095def974 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -80,7 +80,7 @@ managed-micronaut-email = "1.5.0" managed-micronaut-flyway = "5.4.1" managed-micronaut-gcp = "4.6.0" managed-micronaut-graphql = "3.2.0" -managed-micronaut-groovy = "3.3.1" +managed-micronaut-groovy = "3.4.0" managed-micronaut-grpc = "3.3.1" managed-micronaut-hibernate-validator = "3.3.0" managed-micronaut-ignite = "1.0.0.RC1" From d8490106c759cfe93adfd161e46ac9433bde955f Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Thu, 22 Dec 2022 21:00:32 +0100 Subject: [PATCH 344/743] fix: 403 for localhost http host and cors any (#8524) A malicious/compromised website can make HTTP requests to localhost. Typically, such requests would trigger a CORS preflight check which would prevent the request; however, some requests are "simple" and do not require a preflight check. This PR changes the CORS filter to return 403 if the host is localhost and the cors is set to any, which is the default if you set: micronaut.server.cors.enabled=true --- .../http/server/tck/AssertionUtils.java | 48 +++--- .../server/tck/HttpResponseAssertion.java | 48 +++++- .../tck/tests/cors/CorsSimpleRequestTest.java | 157 ++++++++++++++++++ .../http/server/cors/CorsFilter.java | 41 ++++- 4 files changed, 272 insertions(+), 22 deletions(-) create mode 100644 http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/cors/CorsSimpleRequestTest.java diff --git a/http-server-tck/src/main/java/io/micronaut/http/server/tck/AssertionUtils.java b/http-server-tck/src/main/java/io/micronaut/http/server/tck/AssertionUtils.java index e3a46348a01..446014320e0 100644 --- a/http-server-tck/src/main/java/io/micronaut/http/server/tck/AssertionUtils.java +++ b/http-server-tck/src/main/java/io/micronaut/http/server/tck/AssertionUtils.java @@ -50,7 +50,15 @@ private AssertionUtils() { public static void assertThrows(@NonNull ServerUnderTest server, @NonNull HttpRequest request, @NonNull HttpResponseAssertion assertion) { - assertThrows(server, request, assertion.getHttpStatus(), assertion.getBody(), assertion.getHeaders()); + Executable e = assertion.getBody() != null ? + () -> server.exchange(request, String.class) : + () -> server.exchange(request); + HttpClientResponseException thrown = Assertions.assertThrows(HttpClientResponseException.class, e); + HttpResponse response = thrown.getResponse(); + assertEquals(assertion.getHttpStatus(), response.getStatus()); + assertHeaders(response, assertion.getHeaders()); + assertBody(response, assertion.getBody()); + assertion.getResponseConsumer().ifPresent(httpResponseConsumer -> httpResponseConsumer.accept(response)); } public static void assertThrows(@NonNull ServerUnderTest server, @@ -58,20 +66,11 @@ public static void assertThrows(@NonNull ServerUnderTest server, @NonNull HttpStatus expectedStatus, @Nullable String expectedBody, @Nullable Map expectedHeaders) { - Executable e = expectedBody != null ? - () -> server.exchange(request, String.class) : - () -> server.exchange(request); - HttpClientResponseException thrown = Assertions.assertThrows(HttpClientResponseException.class, e); - HttpResponse response = thrown.getResponse(); - assertEquals(expectedStatus, response.getStatus()); - assertHeaders(response, expectedHeaders); - assertBody(response, expectedBody); - } - - public static void assertDoesNotThrow(@NonNull ServerUnderTest server, - @NonNull HttpRequest request, - @NonNull HttpResponseAssertion assertion) { - assertDoesNotThrow(server, request, assertion.getHttpStatus(), assertion.getBody(), assertion.getHeaders()); + assertThrows(server, request, HttpResponseAssertion.builder() + .status(expectedStatus) + .body(expectedBody) + .headers(expectedHeaders) + .build()); } public static void assertDoesNotThrow(@NonNull ServerUnderTest server, @@ -79,13 +78,24 @@ public static void assertDoesNotThrow(@NonNull ServerUnderTest server, @NonNull HttpStatus expectedStatus, @Nullable String expectedBody, @Nullable Map expectedHeaders) { - ThrowingSupplier> executable = expectedBody != null ? + assertDoesNotThrow(server, request, HttpResponseAssertion.builder() + .status(expectedStatus) + .body(expectedBody) + .headers(expectedHeaders) + .build()); + } + + public static void assertDoesNotThrow(@NonNull ServerUnderTest server, + @NonNull HttpRequest request, + @NonNull HttpResponseAssertion assertion) { + ThrowingSupplier> executable = assertion.getBody() != null ? () -> server.exchange(request, String.class) : () -> server.exchange(request); HttpResponse response = Assertions.assertDoesNotThrow(executable); - assertEquals(expectedStatus, response.getStatus()); - assertHeaders(response, expectedHeaders); - assertBody(response, expectedBody); + assertEquals(assertion.getHttpStatus(), response.getStatus()); + assertHeaders(response, assertion.getHeaders()); + assertBody(response, assertion.getBody()); + assertion.getResponseConsumer().ifPresent(httpResponseConsumer -> httpResponseConsumer.accept(response)); } private static void assertBody(@NonNull HttpResponse response, @Nullable String expectedBody) { diff --git a/http-server-tck/src/main/java/io/micronaut/http/server/tck/HttpResponseAssertion.java b/http-server-tck/src/main/java/io/micronaut/http/server/tck/HttpResponseAssertion.java index 690a43c7dd0..e4af0bc7bc8 100644 --- a/http-server-tck/src/main/java/io/micronaut/http/server/tck/HttpResponseAssertion.java +++ b/http-server-tck/src/main/java/io/micronaut/http/server/tck/HttpResponseAssertion.java @@ -16,10 +16,16 @@ package io.micronaut.http.server.tck; import io.micronaut.core.annotation.Experimental; +import io.micronaut.core.annotation.NonNull; +import io.micronaut.core.annotation.Nullable; +import io.micronaut.http.HttpResponse; import io.micronaut.http.HttpStatus; +import java.util.HashMap; import java.util.Map; import java.util.Objects; +import java.util.Optional; +import java.util.function.Consumer; /** * Utility class to verify assertions given an HTTP Response. @@ -33,10 +39,22 @@ public final class HttpResponseAssertion { private final Map headers; private final String body; - private HttpResponseAssertion(HttpStatus httpStatus, Map headers, String body) { + @Nullable + private final Consumer> responseConsumer; + + private HttpResponseAssertion(HttpStatus httpStatus, + Map headers, + String body, + @Nullable Consumer> responseConsumer) { this.httpStatus = httpStatus; this.headers = headers; this.body = body; + this.responseConsumer = responseConsumer; + } + + @NonNull + public Optional>> getResponseConsumer() { + return Optional.ofNullable(responseConsumer); } /** @@ -79,6 +97,18 @@ public static class Builder { private Map headers; private String body; + private Consumer> responseConsumer; + + /** + * + * @param responseConsumer HTTP Response Consumer + * @return HTTP Response Assertion Builder + */ + public Builder assertResponse(Consumer> responseConsumer) { + this.responseConsumer = responseConsumer; + return this; + } + /** * * @param headers HTTP Headers @@ -89,6 +119,20 @@ public Builder headers(Map headers) { return this; } + /** + * + * @param headerName Header Name + * @param headerValue Header Value + * @return HTTP Response Assertion Builder + */ + public Builder header(String headerName, String headerValue) { + if (this.headers == null) { + this.headers = new HashMap<>(); + } + this.headers.put(headerName, headerValue); + return this; + } + /** * * @param body Response Body @@ -114,7 +158,7 @@ public Builder status(HttpStatus httpStatus) { * @return HTTP Response Assertion */ public HttpResponseAssertion build() { - return new HttpResponseAssertion(Objects.requireNonNull(httpStatus), headers, body); + return new HttpResponseAssertion(Objects.requireNonNull(httpStatus), headers, body, responseConsumer); } } } diff --git a/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/cors/CorsSimpleRequestTest.java b/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/cors/CorsSimpleRequestTest.java new file mode 100644 index 00000000000..9243ba09c3e --- /dev/null +++ b/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/cors/CorsSimpleRequestTest.java @@ -0,0 +1,157 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.http.server.tck.tests.cors; + +import io.micronaut.context.annotation.Requires; +import io.micronaut.context.event.ApplicationEventListener; +import io.micronaut.context.event.ApplicationEventPublisher; +import io.micronaut.core.util.CollectionUtils; +import io.micronaut.core.util.StringUtils; +import io.micronaut.http.HttpRequest; +import io.micronaut.http.HttpStatus; +import io.micronaut.http.MediaType; +import io.micronaut.http.annotation.Consumes; +import io.micronaut.http.annotation.Controller; +import io.micronaut.http.annotation.Post; +import io.micronaut.http.annotation.Status; +import io.micronaut.http.client.multipart.MultipartBody; +import io.micronaut.http.server.tck.AssertionUtils; +import io.micronaut.http.server.tck.HttpResponseAssertion; +import io.micronaut.runtime.context.scope.refresh.RefreshEvent; +import jakarta.inject.Inject; +import jakarta.inject.Singleton; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.util.Collections; + +import static io.micronaut.http.server.tck.TestScenario.asserts; +import static org.junit.jupiter.api.Assertions.*; + +@SuppressWarnings({ + "java:S2259", // The tests will show if it's null + "java:S5960", // We're allowed assertions, as these are used in tests only + "checkstyle:MissingJavadocType", +}) +public class CorsSimpleRequestTest { + + private static final String SPECNAME = "CorsSimpleRequestTest"; + private static final String PROPERTY_MICRONAUT_SERVER_CORS_ENABLED = "micronaut.server.cors.enabled"; + + /** + * @see GHSA-583g-g682-crxf + * + * A malicious/compromised website can make HTTP requests to localhost. Normally, such requests would trigger a CORS preflight check which would prevent the request; however, some requests are "simple" and do not require a preflight check. These endpoints, if enabled and not secured, are vulnerable to being triggered. + * Example with Javascript: + *
+     * let url = "http://localhost:8080/refresh";
+     * let body = new FormData();
+     * body.append("force", "true");
+     * fetch(url, { method: "POST", body });
+     * 
+ * @throws IOException may throw the try for resources + */ + @Test + void corsSimpleRequestNotAllowedForLocalhostAndAny() throws IOException { + asserts(SPECNAME, + Collections.singletonMap(PROPERTY_MICRONAUT_SERVER_CORS_ENABLED, StringUtils.TRUE), + createRequest("https://foo.com"), + (server, request) -> { + RefreshCounter refreshCounter = server.getApplicationContext().getBean(RefreshCounter.class); + assertEquals(0, refreshCounter.getRefreshCount()); + + AssertionUtils.assertThrows(server, request, HttpResponseAssertion.builder() + .status(HttpStatus.FORBIDDEN) + .assertResponse(response -> assertFalse(response.getHeaders().contains("Vary"))) + .build()); + assertEquals(0, refreshCounter.getRefreshCount()); + }); + } + + /** + * CORS Simple request for localhost can be allowed via configuration. + * @throws IOException may throw the try for resources + */ + @Test + void corsSimpleRequestForLocalhostCanBeAllowedViaConfiguration() throws IOException { + asserts(SPECNAME, + CollectionUtils.mapOf( + PROPERTY_MICRONAUT_SERVER_CORS_ENABLED, StringUtils.TRUE, + "micronaut.server.cors.configurations.foo.allowed-origins", Collections.singletonList("https://foo.com") + ), + createRequest("https://foo.com"), + (server, request) -> { + + RefreshCounter refreshCounter = server.getApplicationContext().getBean(RefreshCounter.class); + assertEquals(0, refreshCounter.getRefreshCount()); + + AssertionUtils.assertDoesNotThrow(server, request, HttpResponseAssertion.builder() + .status(HttpStatus.OK) + .assertResponse(response -> { + assertNotNull(response.getHeaders().get("Access-Control-Allow-Origin")); + assertNotNull(response.getHeaders().get("Vary")); + assertNotNull(response.getHeaders().get("Access-Control-Allow-Credentials")); + assertNull(response.getHeaders().get("Access-Control-Allow-Methods")); + assertNull(response.getHeaders().get("Access-Control-Allow-Headers")); + assertNull(response.getHeaders().get("Access-Control-Max-Age")); + }) + .build()); + assertEquals(1, refreshCounter.getRefreshCount()); + }); + } + + static HttpRequest createRequest(String origin) { + return HttpRequest.POST("/refresh", MultipartBody.builder().addPart("force", StringUtils.TRUE).build()) + .header("Content-Type", "multipart/form-data; boundary=----WebKitFormBoundarywxiDZy8kMlSE59h1") + .header("Origin", origin) + .header("Accept-Encoding", "gzip, deflate") + .header("Connection", "keep-alive") + .header("Accept", "*/*") + .header("User-Agent", "Mozilla / 5.0 (Macintosh; Intel Mac OS X 10_15_7)AppleWebKit / 605.1 .15 (KHTML, like Gecko)Version / 16.1 Safari / 605.1 .15") + .header("Referer", origin) + .header("Accept-Language", "en - GB, en") + .header("content-length", "140"); + } + + @Requires(property = "spec.name", value = SPECNAME) + @Controller + static class RefreshController { + @Inject + ApplicationEventPublisher refreshEventApplicationEventPublisher; + + @Consumes(MediaType.MULTIPART_FORM_DATA) + @Post("/refresh") + @Status(HttpStatus.OK) + void refresh() { + refreshEventApplicationEventPublisher.publishEvent(new RefreshEvent()); + } + } + + @Requires(property = "spec.name", value = SPECNAME) + @Singleton + static class RefreshCounter implements ApplicationEventListener { + private int refreshCount = 0; + + @Override + public void onApplicationEvent(RefreshEvent event) { + refreshCount++; + } + + public int getRefreshCount() { + return refreshCount; + } + } +} diff --git a/http-server/src/main/java/io/micronaut/http/server/cors/CorsFilter.java b/http-server/src/main/java/io/micronaut/http/server/cors/CorsFilter.java index ddc8d63a0e5..4f14b2e615a 100644 --- a/http-server/src/main/java/io/micronaut/http/server/cors/CorsFilter.java +++ b/http-server/src/main/java/io/micronaut/http/server/cors/CorsFilter.java @@ -33,6 +33,8 @@ import io.micronaut.http.filter.ServerFilterChain; import io.micronaut.http.filter.ServerFilterPhase; import io.micronaut.http.server.HttpServerConfiguration; +import io.micronaut.http.server.util.HttpHostResolver; +import jakarta.inject.Inject; import org.jetbrains.annotations.NotNull; import org.reactivestreams.Publisher; import org.slf4j.Logger; @@ -60,14 +62,31 @@ public class CorsFilter implements HttpServerFilter { private static final Logger LOG = LoggerFactory.getLogger(CorsFilter.class); private static final ArgumentConversionContext CONVERSION_CONTEXT_HTTP_METHOD = ImmutableArgumentConversionContext.of(HttpMethod.class); + private static final String LOCALHOST = "http://localhost"; protected final HttpServerConfiguration.CorsConfiguration corsConfiguration; + @Nullable + private final HttpHostResolver httpHostResolver; + /** * @param corsConfiguration The {@link CorsOriginConfiguration} instance + * @param httpHostResolver HTTP Host resolver */ - public CorsFilter(HttpServerConfiguration.CorsConfiguration corsConfiguration) { + @Inject + public CorsFilter(HttpServerConfiguration.CorsConfiguration corsConfiguration, + @Nullable HttpHostResolver httpHostResolver) { this.corsConfiguration = corsConfiguration; + this.httpHostResolver = httpHostResolver; + } + + /** + * @param corsConfiguration The {@link CorsOriginConfiguration} instance + * @deprecated Use {@link CorsFilter(HttpServerConfiguration.CorsConfiguration, HttpHostResolver)} instead. + */ + @Deprecated + public CorsFilter(HttpServerConfiguration.CorsConfiguration corsConfiguration) { + this(corsConfiguration, null); } @Override @@ -85,12 +104,32 @@ public Publisher> doFilter(HttpRequest request, Server if (!validateMethodToMatch(request, corsOriginConfiguration).isPresent()) { return forbidden(); } + if (shouldDenyToPreventDriveByLocalhostAttack(corsOriginConfiguration, request)) { + LOG.trace("The resolved configuration allows any origin. To prevent drive-by-localhost attacks the request is forbidden"); + return forbidden(); + } return Publishers.then(chain.proceed(request), resp -> decorateResponseWithHeaders(request, resp, corsOriginConfiguration)); } LOG.trace("CORS configuration not found for {} origin", origin); return chain.proceed(request); } + /** + * + * @param corsOriginConfiguration CORS Origin configuration for request's HTTP Header origin. + * @param request HTTP Request + * @return {@literal true} if the resolved host starts with {@literal http://localhost} and the CORS configuration has any for allowed origins. + */ + protected boolean shouldDenyToPreventDriveByLocalhostAttack(@NonNull CorsOriginConfiguration corsOriginConfiguration, + @NonNull HttpRequest request) { + if (httpHostResolver == null) { + return false; + } + String host = httpHostResolver.resolve(request); + return isAny(corsOriginConfiguration.getAllowedOrigins()) && host.startsWith(LOCALHOST); + + } + @Override public int getOrder() { return ServerFilterPhase.METRICS.after(); From d06bc06368a58533ae2991ec534690d130bbf972 Mon Sep 17 00:00:00 2001 From: micronaut-build <65172877+micronaut-build@users.noreply.github.com> Date: Thu, 22 Dec 2022 15:00:50 -0500 Subject: [PATCH 345/743] Bump micronaut-gcp to 4.7.0 (#8523) --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 05da9ab3304..9c8a94116ff 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -78,7 +78,7 @@ managed-micronaut-discovery = "3.2.0" managed-micronaut-elasticsearch = "4.3.0" managed-micronaut-email = "1.5.0" managed-micronaut-flyway = "5.4.1" -managed-micronaut-gcp = "4.6.0" +managed-micronaut-gcp = "4.7.0" managed-micronaut-graphql = "3.2.0" managed-micronaut-groovy = "3.4.0" managed-micronaut-grpc = "3.3.1" From 1f03f746c5515e2890127f03668a61b41f97dfb1 Mon Sep 17 00:00:00 2001 From: micronaut-build <65172877+micronaut-build@users.noreply.github.com> Date: Thu, 22 Dec 2022 15:01:04 -0500 Subject: [PATCH 346/743] Bump micronaut-serialization to 1.5.0 (#8525) --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 9c8a94116ff..cebacdec467 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -113,7 +113,7 @@ managed-micronaut-rxjava1 = "1.0.0" managed-micronaut-rxjava2 = "1.3.0" managed-micronaut-rxjava3 = "2.3.0" managed-micronaut-security = "3.9.0" -managed-micronaut-serialization = "1.3.3" +managed-micronaut-serialization = "1.5.0" managed-micronaut-servlet = "3.3.2" managed-micronaut-spring = "4.3.1" managed-micronaut-sql = "4.7.2" From 61d9ba73a3fbfdacf9038e57e4ad8658bbad4004 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 23 Dec 2022 05:53:06 +0100 Subject: [PATCH 347/743] fix(deps): update dependency com.fasterxml.jackson.core:jackson-databind to v2.14.1 (#8396) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 9ac58b78578..adf4cf5362b 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -65,7 +65,7 @@ wiremock = "2.33.2" managed-groovy = "4.0.6" managed-jakarta-annotation-api = "2.1.1" managed-jackson = "2.14.0" -managed-jackson-databind = "2.14.0" +managed-jackson-databind = "2.14.1" managed-maven-native-plugin = "0.9.13" managed-methvin-directory-watcher = "0.16.1" managed-netty = "4.1.86.Final" From 9cb1411d45758153335932bee91dd0be20dc0be3 Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Fri, 23 Dec 2022 06:54:15 +0100 Subject: [PATCH 348/743] build: Micronaut Problem JSON 2.6.0 (#8526) --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index cebacdec467..63ff1dce0bb 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -103,7 +103,7 @@ managed-micronaut-object-storage = "1.1.0" managed-micronaut-openapi = "4.8.1" managed-micronaut-oraclecloud = "2.3.1" managed-micronaut-picocli = "4.3.0" -managed-micronaut-problem = "2.5.1" +managed-micronaut-problem = "2.6.0" managed-micronaut-rabbitmq = "3.4.0" managed-micronaut-r2dbc = "4.0.0" managed-micronaut-reactor = "2.5.0" From 63dc785a36474f3b7b663e0baf1b5882627a5876 Mon Sep 17 00:00:00 2001 From: micronaut-build <65172877+micronaut-build@users.noreply.github.com> Date: Fri, 23 Dec 2022 00:54:26 -0500 Subject: [PATCH 349/743] Bump micronaut-gcp to 4.8.0 (#8527) --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 63ff1dce0bb..cd663abfe4e 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -78,7 +78,7 @@ managed-micronaut-discovery = "3.2.0" managed-micronaut-elasticsearch = "4.3.0" managed-micronaut-email = "1.5.0" managed-micronaut-flyway = "5.4.1" -managed-micronaut-gcp = "4.7.0" +managed-micronaut-gcp = "4.8.0" managed-micronaut-graphql = "3.2.0" managed-micronaut-groovy = "3.4.0" managed-micronaut-grpc = "3.3.1" From 9a59e1759ed5ceb1cbba89d7a3cc856b82c07d60 Mon Sep 17 00:00:00 2001 From: Denis Stepanov Date: Fri, 23 Dec 2022 11:07:47 +0100 Subject: [PATCH 350/743] Use `ConversionService` from bean context for `InterceptedMethod` (#8521) --- .../io/micronaut/aop/InterceptedMethod.java | 2 + .../scheduling/async/AsyncInterceptor.java | 22 +- .../inject/writer/BeanDefinitionWriter.java | 237 ++++++++++-------- .../core/async/publisher/Publishers.java | 2 +- .../client/aop/FunctionClientAdvice.java | 22 +- .../client/http/HttpFunctionExecutor.java | 13 +- .../HttpClientIntroductionAdvice.java | 2 +- .../intercept/DefaultRetryInterceptor.java | 26 +- .../retry/intercept/RecoveryInterceptor.java | 17 +- .../intercept/InterceptorOrderSpec.groovy | 5 +- .../validation/ValidatingInterceptor.java | 46 ++-- .../micronaut/validation/ValidatedSpec.groovy | 3 +- .../ClientWebSocketInterceptor.java | 32 ++- 13 files changed, 252 insertions(+), 177 deletions(-) diff --git a/aop/src/main/java/io/micronaut/aop/InterceptedMethod.java b/aop/src/main/java/io/micronaut/aop/InterceptedMethod.java index 877b4fa9702..73652328775 100644 --- a/aop/src/main/java/io/micronaut/aop/InterceptedMethod.java +++ b/aop/src/main/java/io/micronaut/aop/InterceptedMethod.java @@ -39,7 +39,9 @@ public interface InterceptedMethod { * * @param context The {@link MethodInvocationContext} * @return The {@link InterceptedMethod} + * @deprecated replaced with {@link #of(MethodInvocationContext, ConversionService)} */ + @Deprecated(since = "4", forRemoval = true) static InterceptedMethod of(MethodInvocationContext context) { return of(context, ConversionService.SHARED); } diff --git a/context/src/main/java/io/micronaut/scheduling/async/AsyncInterceptor.java b/context/src/main/java/io/micronaut/scheduling/async/AsyncInterceptor.java index 0141d44b821..5c7d0e4c0a5 100644 --- a/context/src/main/java/io/micronaut/scheduling/async/AsyncInterceptor.java +++ b/context/src/main/java/io/micronaut/scheduling/async/AsyncInterceptor.java @@ -23,6 +23,7 @@ import io.micronaut.context.BeanProvider; import io.micronaut.core.annotation.Internal; import io.micronaut.core.annotation.Nullable; +import io.micronaut.core.convert.ConversionService; import io.micronaut.core.type.ReturnType; import io.micronaut.inject.qualifiers.Qualifiers; import io.micronaut.scheduling.TaskExecutors; @@ -51,6 +52,7 @@ public class AsyncInterceptor implements MethodInterceptor { private static final Logger LOG = LoggerFactory.getLogger(TaskExecutors.class); + private final ConversionService conversionService; private final BeanLocator beanLocator; private final Optional> scheduledExecutorService; private final Map scheduledExecutorServices = new ConcurrentHashMap<>(); @@ -58,10 +60,14 @@ public class AsyncInterceptor implements MethodInterceptor { /** * Default constructor. * + * @param conversionService The conversion service * @param beanLocator The bean constructor * @param scheduledExecutorService The scheduled executor service */ - AsyncInterceptor(BeanLocator beanLocator, @Named(TaskExecutors.SCHEDULED) Optional> scheduledExecutorService) { + AsyncInterceptor(ConversionService conversionService, + BeanLocator beanLocator, + @Named(TaskExecutors.SCHEDULED) Optional> scheduledExecutorService) { + this.conversionService = conversionService; this.beanLocator = beanLocator; this.scheduledExecutorService = scheduledExecutorService; } @@ -83,18 +89,20 @@ public Object intercept(MethodInvocationContext context) { beanLocator.findBean(ExecutorService.class, Qualifiers.byName(name)) .orElseThrow(() -> new TaskExecutionException("No ExecutorService named [" + name + "] configured in application context"))); } - InterceptedMethod interceptedMethod = InterceptedMethod.of(context); + InterceptedMethod interceptedMethod = InterceptedMethod.of(context, conversionService); try { switch (interceptedMethod.resultType()) { - case PUBLISHER: + case PUBLISHER -> { return interceptedMethod.handleResult( interceptedMethod.interceptResultAsPublisher(executorService) ); - case COMPLETION_STAGE: + } + case COMPLETION_STAGE -> { return interceptedMethod.handleResult( CompletableFuture.supplyAsync(() -> interceptedMethod.interceptResultAsCompletionStage(), executorService).thenCompose(Function.identity()) ); - case SYNCHRONOUS: + } + case SYNCHRONOUS -> { ReturnType rt = context.getReturnType(); Class returnType = rt.getType(); if (void.class == returnType) { @@ -110,8 +118,10 @@ public Object intercept(MethodInvocationContext context) { return null; } throw new TaskExecutionException("Method [" + context.getExecutableMethod() + "] must return either void, or an instance of Publisher or CompletionStage"); - default: + } + default -> { return interceptedMethod.unsupported(); + } } } catch (Exception e) { return interceptedMethod.handleException(e); diff --git a/core-processor/src/main/java/io/micronaut/inject/writer/BeanDefinitionWriter.java b/core-processor/src/main/java/io/micronaut/inject/writer/BeanDefinitionWriter.java index 45f58e961ca..f1542a3a9fe 100644 --- a/core-processor/src/main/java/io/micronaut/inject/writer/BeanDefinitionWriter.java +++ b/core-processor/src/main/java/io/micronaut/inject/writer/BeanDefinitionWriter.java @@ -50,6 +50,8 @@ import io.micronaut.core.annotation.NonNull; import io.micronaut.core.annotation.Nullable; import io.micronaut.core.beans.BeanConstructor; +import io.micronaut.core.convert.ConversionService; +import io.micronaut.core.convert.ConversionServiceProvider; import io.micronaut.core.naming.NameUtils; import io.micronaut.core.reflect.ClassUtils; import io.micronaut.core.reflect.InstantiationUtils; @@ -173,7 +175,7 @@ public class BeanDefinitionWriter extends AbstractClassFileWriter implements Bea private static final Method POST_CONSTRUCT_METHOD = ReflectionUtils.getRequiredInternalMethod(AbstractInitializableBeanDefinition.class, "postConstruct", BeanResolutionContext.class, BeanContext.class, Object.class); private static final org.objectweb.asm.commons.Method INJECT_BEAN_METHOD = org.objectweb.asm.commons.Method.getMethod( - ReflectionUtils.getRequiredInternalMethod(InjectableBeanDefinition.class, "inject", BeanResolutionContext.class, BeanContext.class, Object.class) + ReflectionUtils.getRequiredInternalMethod(InjectableBeanDefinition.class, "inject", BeanResolutionContext.class, BeanContext.class, Object.class) ); private static final Method PRE_DESTROY_METHOD = ReflectionUtils.getRequiredInternalMethod(AbstractInitializableBeanDefinition.class, "preDestroy", BeanResolutionContext.class, BeanContext.class, Object.class); @@ -396,10 +398,10 @@ public class BeanDefinitionWriter extends AbstractClassFileWriter implements Bea private static final Type TYPE_REFLECTION_UTILS = Type.getType(ReflectionUtils.class); private static final org.objectweb.asm.commons.Method GET_FIELD_WITH_REFLECTION_METHOD = org.objectweb.asm.commons.Method.getMethod( - ReflectionUtils.getRequiredInternalMethod(ReflectionUtils.class, "getField", Class.class, String.class, Object.class)); + ReflectionUtils.getRequiredInternalMethod(ReflectionUtils.class, "getField", Class.class, String.class, Object.class)); private static final org.objectweb.asm.commons.Method METHOD_INVOKE_METHOD = org.objectweb.asm.commons.Method.getMethod( - ReflectionUtils.getRequiredInternalMethod(ReflectionUtils.class, "invokeMethod", Object.class, java.lang.reflect.Method.class, Object[].class)); + ReflectionUtils.getRequiredInternalMethod(ReflectionUtils.class, "invokeMethod", Object.class, java.lang.reflect.Method.class, Object[].class)); private static final org.objectweb.asm.commons.Method BEAN_DEFINITION_CLASS_CONSTRUCTOR = new org.objectweb.asm.commons.Method(CONSTRUCTOR_NAME, getConstructorDescriptor( Class.class, // beanType @@ -415,16 +417,16 @@ public class BeanDefinitionWriter extends AbstractClassFileWriter implements Bea private static final Type PRECALCULATED_INFO = Type.getType(AbstractInitializableBeanDefinition.PrecalculatedInfo.class); private static final org.objectweb.asm.commons.Method PRECALCULATED_INFO_CONSTRUCTOR = org.objectweb.asm.commons.Method.getMethod( - ReflectionUtils.getRequiredInternalConstructor(AbstractInitializableBeanDefinition.PrecalculatedInfo.class, - Optional.class, // scope - boolean.class, // isAbstract - boolean.class, // isIterable - boolean.class, // isSingleton - boolean.class, // isPrimary - boolean.class, // isConfigurationProperties - boolean.class, // isContainerType - boolean.class // requiresMethodProcessing - )); + ReflectionUtils.getRequiredInternalConstructor(AbstractInitializableBeanDefinition.PrecalculatedInfo.class, + Optional.class, // scope + boolean.class, // isAbstract + boolean.class, // isIterable + boolean.class, // isSingleton + boolean.class, // isPrimary + boolean.class, // isConfigurationProperties + boolean.class, // isContainerType + boolean.class // requiresMethodProcessing + )); private static final String FIELD_CONSTRUCTOR = "$CONSTRUCTOR"; private static final String FIELD_INJECTION_METHODS = "$INJECTION_METHODS"; @@ -494,6 +496,17 @@ public class BeanDefinitionWriter extends AbstractClassFileWriter implements Bea private static final Type TYPE_QUALIFIER = Type.getType(Qualifier.class); private static final String MESSAGE_ONLY_SINGLE_CALL_PERMITTED = "Only a single call to visitBeanFactoryMethod(..) is permitted"; + private static final int INJECT_METHOD_BEAN_RESOLUTION_CONTEXT_PARAM = 0; + private static final int INJECT_METHOD_BEAN_CONTEXT_PARAM = 1; + private static final int INJECT_METHOD_BEAN_INSTANCE_PARAM = 2; + + private static final int INSTANTIATE_METHOD_BEAN_RESOLUTION_CONTEXT_PARAM = 0; + private static final int INSTANTIATE_METHOD_BEAN_CONTEXT_PARAM = 1; + + private static final org.objectweb.asm.commons.Method METHOD_BEAN_CONTEXT_GET_CONVERSION_SERVICE = org.objectweb.asm.commons.Method.getMethod( + ReflectionUtils.getRequiredMethod(ConversionServiceProvider.class, "getConversionService") + ); + private final ClassWriter classWriter; private final String beanFullClassName; private final String beanDefinitionName; @@ -561,8 +574,8 @@ public class BeanDefinitionWriter extends AbstractClassFileWriter implements Bea /** * Creates a bean definition writer. * - * @param classElement The class element - * @param visitorContext The visitor context + * @param classElement The class element + * @param visitorContext The visitor context */ public BeanDefinitionWriter(ClassElement classElement, VisitorContext visitorContext) { @@ -763,7 +776,6 @@ public Map getTypeArgumentMap() { } - /** * @return The name of the bean definition reference class. */ @@ -1054,7 +1066,7 @@ public void visitBeanDefinitionEnd() { if (!annotationInjectionPoints.isEmpty()) { Type annotationInjectionsFieldType = Type.getType(AbstractInitializableBeanDefinition.AnnotationReference[].class); classWriter.visitField(ACC_PRIVATE | ACC_FINAL | ACC_STATIC, FIELD_ANNOTATION_INJECTIONS, - annotationInjectionsFieldType.getDescriptor(), null, null); + annotationInjectionsFieldType.getDescriptor(), null, null); List injectedTypes = new ArrayList<>(annotationInjectionPoints.keySet()); int length = injectedTypes.size(); @@ -1450,7 +1462,7 @@ public void visitSetterValue( declaringType, methodElement, false, - annotationMetadata); + annotationMetadata); methodInjectionPoints.add(methodVisitData); allMethodVisits.add(methodVisitData); currentMethodIndex++; @@ -1651,7 +1663,7 @@ public void visitConfigBuilderDurationMethod( String methodName, String path) { visitConfigBuilderMethodInternal( - propertyName, + propertyName, returnType, methodName, ClassElement.of(Duration.class), @@ -1671,7 +1683,7 @@ public void visitConfigBuilderMethod( String path) { visitConfigBuilderMethodInternal( - propertyName, + propertyName, returnType, methodName, paramType, @@ -1703,26 +1715,26 @@ public boolean requiresMethodProcessing() { @Override public void visitFieldInjectionPoint( - TypedElement declaringType, - FieldElement fieldElement, - boolean requiresReflection, - VisitorContext visitorContext) { + TypedElement declaringType, + FieldElement fieldElement, + boolean requiresReflection, + VisitorContext visitorContext) { visitFieldInjectionPointInternal( - declaringType, - fieldElement, - fieldElement.getAnnotationMetadata(), - requiresReflection, - visitorContext + declaringType, + fieldElement, + fieldElement.getAnnotationMetadata(), + requiresReflection, + visitorContext ); } private void visitFieldInjectionPointInternal( - TypedElement declaringType, - FieldElement fieldElement, - AnnotationMetadata annotationMetadata, - boolean requiresReflection, - VisitorContext visitorContext) { + TypedElement declaringType, + FieldElement fieldElement, + AnnotationMetadata annotationMetadata, + boolean requiresReflection, + VisitorContext visitorContext) { boolean requiresGenericType = false; Method methodToInvoke; @@ -1771,7 +1783,7 @@ private void visitFieldInjectionPointInternal( private static boolean isInjectableMap(ClassElement genericType) { boolean typeMatches = Stream.of(Map.class, HashMap.class, LinkedHashMap.class, TreeMap.class) - .anyMatch(t -> genericType.getName().equals(t.getName())); + .anyMatch(t -> genericType.getName().equals(t.getName())); if (typeMatches) { Map typeArgs = genericType.getTypeArguments(); @@ -1802,16 +1814,16 @@ public void visitAnnotationMemberPropertyInjectionPoint(TypedElement annotationM @Nullable String notEqualsValue) { ClassElement annotationMemberClassElement = annotationMemberBeanType.getType(); MethodElement memberPropertyGetter = annotationMemberClassElement.getBeanProperties() - .stream() - .filter(property -> property.getSimpleName().equals(annotationMemberProperty)) - .findFirst() - .flatMap(PropertyElement::getReadMethod) - .orElse(null); + .stream() + .filter(property -> property.getSimpleName().equals(annotationMemberProperty)) + .findFirst() + .flatMap(PropertyElement::getReadMethod) + .orElse(null); if (memberPropertyGetter == null) { final String[] readPrefixes = annotationMemberBeanType.getAnnotationMetadata() - .getValue(AccessorsStyle.class, "readPrefixes", String[].class) - .orElse(new String[]{AccessorsStyle.DEFAULT_READ_PREFIX}); + .getValue(AccessorsStyle.class, "readPrefixes", String[].class) + .orElse(new String[]{AccessorsStyle.DEFAULT_READ_PREFIX}); memberPropertyGetter = annotationMemberClassElement.getEnclosedElement( ElementQuery.ALL_METHODS @@ -1824,11 +1836,11 @@ public void visitAnnotationMemberPropertyInjectionPoint(TypedElement annotationM if (memberPropertyGetter == null) { visitorContext.fail("Bean property [" + annotationMemberProperty + "] is not available on bean [" - + annotationMemberBeanType.getName() + "]", annotationMemberBeanType); + + annotationMemberBeanType.getName() + "]", annotationMemberBeanType); } else { Type injectedType = JavaModelUtils.getTypeReference(annotationMemberClassElement); annotationInjectionPoints.computeIfAbsent(injectedType, type -> new ArrayList<>(2)) - .add(new AnnotationVisitData(annotationMemberBeanType, annotationMemberProperty, memberPropertyGetter , requiredValue, notEqualsValue)); + .add(new AnnotationVisitData(annotationMemberBeanType, annotationMemberProperty, memberPropertyGetter, requiredValue, notEqualsValue)); } } @@ -2087,6 +2099,32 @@ private int pushGetValueForPathCall(GeneratorAdapter injectMethodVisitor, ClassE return optionalInstanceIndex; } + private boolean pushValueBypassingBeanContext(GeneratorAdapter writer, ClassElement type) { + // Used in instantiate and inject methods + if (type.isAssignable(BeanResolutionContext.class)) { + writer.loadArg(INSTANTIATE_METHOD_BEAN_RESOLUTION_CONTEXT_PARAM); + return true; + } + if (type.isAssignable(BeanContext.class)) { + writer.loadArg(INSTANTIATE_METHOD_BEAN_CONTEXT_PARAM); + return true; + } + if (visitorContext.getClassElement(ConversionService.class).orElseThrow().equals(type)) { + // We only want to assign to exact `ConversionService` classes not to classes extending `ConversionService` + writer.loadArg(INSTANTIATE_METHOD_BEAN_CONTEXT_PARAM); + writer.invokeInterface(TYPE_BEAN_CONTEXT, METHOD_BEAN_CONTEXT_GET_CONVERSION_SERVICE); + return true; + } + if (type.isAssignable(ConfigurationPath.class)) { + writer.loadArg(INSTANTIATE_METHOD_BEAN_RESOLUTION_CONTEXT_PARAM); + writer.invokeInterface(Type.getType(BeanResolutionContext.class), org.objectweb.asm.commons.Method.getMethod( + ReflectionUtils.getRequiredInternalMethod(BeanResolutionContext.class, "getConfigurationPath") + )); + return true; + } + return false; + } + private void visitFieldInjectionPointInternal( TypedElement declaringType, FieldElement fieldElement, @@ -2104,16 +2142,14 @@ private void visitFieldInjectionPointInternal( injectMethodVisitor.loadLocal(injectInstanceLocalVarIndex, beanType); - if (fieldElement.getGenericField().isAssignable(BeanContext.class)) { - injectMethodVisitor.loadArg(1); - } else { + if (!pushValueBypassingBeanContext(injectMethodVisitor, fieldElement.getGenericField())) { // first get the value of the field by calling AbstractBeanDefinition.getBeanForField(..) // load 'this' injectMethodVisitor.loadThis(); // 1st argument load BeanResolutionContext - injectMethodVisitor.loadArg(0); + injectMethodVisitor.loadArg(INJECT_METHOD_BEAN_RESOLUTION_CONTEXT_PARAM); // 2nd argument load BeanContext - injectMethodVisitor.loadArg(1); + injectMethodVisitor.loadArg(INJECT_METHOD_BEAN_CONTEXT_PARAM); // 3rd argument the field index injectMethodVisitor.push(currentFieldIndex); if (requiresGenericType) { @@ -2393,8 +2429,8 @@ private void visitMethodInjectionPointInternal(MethodVisitData methodVisitData, } } else { injectMethodVisitor.loadThis(); - injectMethodVisitor.loadArg(0); - injectMethodVisitor.loadArg(1); + injectMethodVisitor.loadArg(INJECT_METHOD_BEAN_RESOLUTION_CONTEXT_PARAM); + injectMethodVisitor.loadArg(INJECT_METHOD_BEAN_CONTEXT_PARAM); injectMethodVisitor.push(currentMethodIndex); injectMethodVisitor.loadLocal(injectInstanceLocalVarIndex, beanType); if (hasArguments) { @@ -2431,11 +2467,7 @@ private void destroyInjectScopeBeansIfNecessary(GeneratorAdapter injectMethodVis private void pushMethodParameterValue(GeneratorAdapter injectMethodVisitor, int i, ParameterElement entry) { AnnotationMetadata argMetadata = entry.getAnnotationMetadata(); - if (entry.getGenericType().isAssignable(BeanResolutionContext.class)) { - injectMethodVisitor.loadArg(0); - } else if (entry.getGenericType().isAssignable(BeanContext.class)) { - injectMethodVisitor.loadArg(1); - } else { + if (!pushValueBypassingBeanContext(injectMethodVisitor, entry.getGenericType())) { boolean requiresGenericType = false; final ClassElement genericType = entry.getGenericType(); Method methodToInvoke; @@ -2759,11 +2791,11 @@ private void pushInvokeMethodOnSuperClass(MethodVisitor constructorVisitor, org. private void visitCheckIfShouldLoadMethodDefinition() { String desc = getMethodDescriptor("void", BeanResolutionContext.class.getName(), BeanContext.class.getName()); this.checkIfShouldLoadMethodVisitor = new GeneratorAdapter(classWriter.visitMethod( - ACC_PROTECTED, - "checkIfShouldLoad", - desc, - null, - null), ACC_PROTECTED, "checkIfShouldLoad", desc); + ACC_PROTECTED, + "checkIfShouldLoad", + desc, + null, + null), ACC_PROTECTED, "checkIfShouldLoad", desc); } @SuppressWarnings("MagicNumber") @@ -3073,7 +3105,7 @@ private void visitBuildFactoryMethodDefinition( GeneratorAdapter buildMethodVisitor = this.buildMethodVisitor; - int factoryVar = -1; + int factoryVar = -1; // Skip initializing a producer instance for static producers if (!factoryElement.isStatic()) { factoryVar = pushGetFactoryBean(factoryClass, factoryType, buildMethodVisitor); @@ -3084,7 +3116,7 @@ private void visitBuildFactoryMethodDefinition( int constructorIndex = initInterceptedConstructorWriter( buildMethodVisitor, parameterList, - new FactoryMethodDef(factoryType, factoryElement, methodDescriptor, factoryVar) + new FactoryMethodDef(factoryType, factoryElement, methodDescriptor, factoryVar) ); // populate an Object[] of all constructor arguments final int parametersIndex = createParameterArray(parameterList, buildMethodVisitor); @@ -3102,7 +3134,7 @@ private void visitBuildFactoryMethodDefinition( buildMethodVisitor.dup(); buildMethodVisitor.push(true); buildMethodVisitor.invokeVirtual(Type.getType(Method.class), org.objectweb.asm.commons.Method.getMethod( - ReflectionUtils.getRequiredMethod(Method.class, "setAccessible", boolean.class) + ReflectionUtils.getRequiredMethod(Method.class, "setAccessible", boolean.class) )); hasInjectScope = pushParametersAsArray(buildMethodVisitor, parameters); buildMethodVisitor.invokeStatic(TYPE_REFLECTION_UTILS, METHOD_INVOKE_METHOD); @@ -3111,8 +3143,7 @@ private void visitBuildFactoryMethodDefinition( // Reflection always returns Object, convert it to appropriate primitive pushCastToType(buildMethodVisitor, beanType); } - } else - if (methodElement.isStatic()) { + } else if (methodElement.isStatic()) { buildMethodVisitor.invokeStatic(factoryType, new org.objectweb.asm.commons.Method(factoryElement.getName(), methodDescriptor)); } else { buildMethodVisitor.invokeVirtual(factoryType, new org.objectweb.asm.commons.Method(factoryElement.getName(), methodDescriptor)); @@ -3176,8 +3207,8 @@ private int pushGetFactoryBean(ClassElement factoryClass, Type factoryType, Gene invokeInterfaceStaticMethod(buildMethodVisitor, Argument.class, METHOD_CREATE_ARGUMENT_SIMPLE); }); buildMethodVisitor.invokeVirtual( - Type.getType(DefaultBeanContext.class), - org.objectweb.asm.commons.Method.getMethod(METHOD_GET_BEAN) + Type.getType(DefaultBeanContext.class), + org.objectweb.asm.commons.Method.getMethod(METHOD_GET_BEAN) ); int factoryVar = buildMethodVisitor.newLocal(factoryType); @@ -3272,9 +3303,9 @@ private void invokeCheckIfShouldLoadIfNecessary(GeneratorAdapter buildMethodVisi buildMethodVisitor.loadArg(0); buildMethodVisitor.loadArg(1); buildMethodVisitor.invokeVirtual(beanDefinitionType, org.objectweb.asm.commons.Method.getMethod( - ReflectionUtils.getRequiredMethod(AbstractInitializableBeanDefinition.class, "checkIfShouldLoad", - BeanResolutionContext.class, - BeanContext.class))); + ReflectionUtils.getRequiredMethod(AbstractInitializableBeanDefinition.class, "checkIfShouldLoad", + BeanResolutionContext.class, + BeanContext.class))); } } @@ -3483,10 +3514,10 @@ private InnerClassDef newInnerClass(Class superType) { ); classWriter.visitInnerClass(constructorInternalName, beanDefinitionInternalName, null, ACC_PRIVATE); return new InnerClassDef( - interceptedConstructorWriterName, - interceptedConstructorWriter, - constructorInternalName, - interceptedConstructorType + interceptedConstructorWriterName, + interceptedConstructorWriter, + constructorInternalName, + interceptedConstructorType ); } @@ -3618,7 +3649,7 @@ private boolean pushParametersAsArray(GeneratorAdapter buildMethodVisitor, Param } int finalI = i; pushStoreInArray(buildMethodVisitor, i, pLen, () -> - pushConstructorArgument(buildMethodVisitor, parameter.getName(), parameter, parameter.getAnnotationMetadata(), finalI, true) + pushConstructorArgument(buildMethodVisitor, parameter.getName(), parameter, parameter.getAnnotationMetadata(), finalI, true) ); } return hasInjectScope; @@ -3636,16 +3667,7 @@ private void pushConstructorArgument(GeneratorAdapter buildMethodVisitor, buildMethodVisitor.push(argumentName); buildMethodVisitor.invokeInterface(Type.getType(Map.class), org.objectweb.asm.commons.Method.getMethod(ReflectionUtils.getRequiredMethod(Map.class, "get", Object.class))); pushCastToType(buildMethodVisitor, argumentType); - } else if (argumentType.getGenericType().isAssignable(BeanContext.class)) { - buildMethodVisitor.loadArg(1); - } else if (argumentType.getGenericType().isAssignable(BeanResolutionContext.class)) { - buildMethodVisitor.loadArg(0); - } else if (argumentType.getGenericType().isAssignable(ConfigurationPath.class)) { - buildMethodVisitor.loadArg(0); - buildMethodVisitor.invokeInterface(Type.getType(BeanResolutionContext.class), org.objectweb.asm.commons.Method.getMethod( - ReflectionUtils.getRequiredInternalMethod(BeanResolutionContext.class, "getConfigurationPath") - )); - } else { + } else if (!pushValueBypassingBeanContext(buildMethodVisitor, argumentType.getGenericType())) { boolean hasGenericType = false; boolean isArray = false; Method methodToInvoke; @@ -3876,7 +3898,7 @@ private void resolveFirstTypeArgument(GeneratorAdapter visitor) { private void resolveSecondTypeArgument(GeneratorAdapter visitor) { visitor.invokeInterface(Type.getType(TypeVariableResolver.class), - org.objectweb.asm.commons.Method.getMethod(ReflectionUtils.getRequiredInternalMethod(TypeVariableResolver.class, "getTypeParameters"))); + org.objectweb.asm.commons.Method.getMethod(ReflectionUtils.getRequiredInternalMethod(TypeVariableResolver.class, "getTypeParameters"))); visitor.push(1); visitor.arrayLoad(Type.getType(Argument.class)); } @@ -4070,8 +4092,8 @@ private void pushPrecalculatedInfo(GeneratorAdapter protectedConstructor, Annota if (scope != null) { protectedConstructor.push(scope); protectedConstructor.invokeStatic( - TYPE_OPTIONAL, - METHOD_OPTIONAL_OF + TYPE_OPTIONAL, + METHOD_OPTIONAL_OF ); } else { protectedConstructor.invokeStatic(TYPE_OPTIONAL, METHOD_OPTIONAL_EMPTY); @@ -4083,11 +4105,11 @@ private void pushPrecalculatedInfo(GeneratorAdapter protectedConstructor, Annota protectedConstructor.push(isIterable(annotationMetadata)); // 4: `boolean` isSingleton protectedConstructor.push( - isSingleton(scope) + isSingleton(scope) ); // 5: `boolean` isPrimary protectedConstructor.push( - annotationMetadata.hasDeclaredStereotype(Primary.class) + annotationMetadata.hasDeclaredStereotype(Primary.class) ); // 6: `boolean` isConfigurationProperties protectedConstructor.push(isConfigurationProperties); @@ -4186,13 +4208,13 @@ private void pushNewAnnotationReference(GeneratorAdapter staticInit, Type refere // 1: argument staticInit.push(referencedType); invokeInterfaceStaticMethod( - staticInit, - Argument.class, - org.objectweb.asm.commons.Method.getMethod( - ReflectionUtils.getRequiredInternalMethod(Argument.class, "of", Class.class))); + staticInit, + Argument.class, + org.objectweb.asm.commons.Method.getMethod( + ReflectionUtils.getRequiredInternalMethod(Argument.class, "of", Class.class))); staticInit.invokeConstructor(Type.getType(AbstractInitializableBeanDefinition.AnnotationReference.class), - ANNOTATION_REFERENCE_CONSTRUCTOR); + ANNOTATION_REFERENCE_CONSTRUCTOR); } private void pushAnnotationMetadata(GeneratorAdapter staticInit, AnnotationMetadata annotationMetadata) { @@ -4497,7 +4519,8 @@ public Element[] getOriginatingElements() { /** * Sets whether this bean is a proxied type. - * @param proxiedBean True if it proxied + * + * @param proxiedBean True if it proxied * @param isProxyTarget True if the proxied bean is a retained target */ public void setProxiedBean(boolean proxiedBean, boolean isProxyTarget) { @@ -4577,10 +4600,10 @@ public static final class MethodVisitData { * @param annotationMetadata */ MethodVisitData( - TypedElement beanType, - MethodElement methodElement, - boolean requiresReflection, - AnnotationMetadata annotationMetadata) { + TypedElement beanType, + MethodElement methodElement, + boolean requiresReflection, + AnnotationMetadata annotationMetadata) { this.beanType = beanType; this.requiresReflection = requiresReflection; this.methodElement = methodElement; @@ -4590,12 +4613,12 @@ public static final class MethodVisitData { } MethodVisitData( - TypedElement beanType, - MethodElement methodElement, - boolean requiresReflection, - AnnotationMetadata annotationMetadata, - boolean postConstruct, - boolean preDestroy) { + TypedElement beanType, + MethodElement methodElement, + boolean requiresReflection, + AnnotationMetadata annotationMetadata, + boolean postConstruct, + boolean preDestroy) { this.beanType = beanType; this.requiresReflection = requiresReflection; this.methodElement = methodElement; diff --git a/core-reactive/src/main/java/io/micronaut/core/async/publisher/Publishers.java b/core-reactive/src/main/java/io/micronaut/core/async/publisher/Publishers.java index 3a4ceebbd94..64d73110953 100644 --- a/core-reactive/src/main/java/io/micronaut/core/async/publisher/Publishers.java +++ b/core-reactive/src/main/java/io/micronaut/core/async/publisher/Publishers.java @@ -448,7 +448,7 @@ public static boolean isConvertibleToPublisher(Object object) { * @return The Resulting in publisher * @deprecated replaced by {@link #convertPublisher(ConversionService, Object, Class)} */ - @Deprecated(since = "4") + @Deprecated(since = "4", forRemoval = true) public static T convertPublisher(Object object, Class publisherType) { return convertPublisher(ConversionService.SHARED, object, publisherType); } diff --git a/function-client/src/main/java/io/micronaut/function/client/aop/FunctionClientAdvice.java b/function-client/src/main/java/io/micronaut/function/client/aop/FunctionClientAdvice.java index 916cc21a234..2ab8659c88a 100644 --- a/function-client/src/main/java/io/micronaut/function/client/aop/FunctionClientAdvice.java +++ b/function-client/src/main/java/io/micronaut/function/client/aop/FunctionClientAdvice.java @@ -20,6 +20,7 @@ import io.micronaut.aop.MethodInvocationContext; import io.micronaut.core.annotation.AnnotationUtil; import io.micronaut.core.annotation.Nullable; +import io.micronaut.core.convert.ConversionService; import io.micronaut.core.naming.NameUtils; import io.micronaut.core.type.Argument; import io.micronaut.function.client.FunctionDefinition; @@ -44,16 +45,19 @@ @Singleton public class FunctionClientAdvice implements MethodInterceptor { + private final ConversionService conversionService; private final FunctionDiscoveryClient discoveryClient; private final FunctionInvokerChooser functionInvokerChooser; /** * Constructor. * - * @param discoveryClient discoveryClient + * @param conversionService The conversion service + * @param discoveryClient discoveryClient * @param functionInvokerChooser functionInvokerChooser */ - public FunctionClientAdvice(FunctionDiscoveryClient discoveryClient, FunctionInvokerChooser functionInvokerChooser) { + public FunctionClientAdvice(ConversionService conversionService, FunctionDiscoveryClient discoveryClient, FunctionInvokerChooser functionInvokerChooser) { + this.conversionService = conversionService; this.discoveryClient = discoveryClient; this.functionInvokerChooser = functionInvokerChooser; } @@ -78,21 +82,25 @@ public Object intercept(MethodInvocationContext context) { .orElse(NameUtils.hyphenate(context.getMethodName(), true)); Flux functionDefinition = Flux.from(discoveryClient.getFunction(functionName)); - InterceptedMethod interceptedMethod = InterceptedMethod.of(context); + InterceptedMethod interceptedMethod = InterceptedMethod.of(context, conversionService); try { switch (interceptedMethod.resultType()) { - case PUBLISHER: + case PUBLISHER -> { return interceptedMethod.handleResult(invokeFn(body, functionName, functionDefinition, interceptedMethod.returnTypeValue())); - case COMPLETION_STAGE: + } + case COMPLETION_STAGE -> { return interceptedMethod.handleResult(toCompletableFuture( invokeFn(body, functionName, functionDefinition, interceptedMethod.returnTypeValue()) )); - case SYNCHRONOUS: + } + case SYNCHRONOUS -> { FunctionDefinition def = functionDefinition.blockFirst(); FunctionInvoker functionInvoker = functionInvokerChooser.choose(def).orElseThrow(() -> new FunctionNotFoundException(def.getName())); return functionInvoker.invoke(def, body, context.getReturnType().asArgument()); - default: + } + default -> { return interceptedMethod.unsupported(); + } } } catch (Exception e) { return interceptedMethod.handleException(e); diff --git a/function-client/src/main/java/io/micronaut/function/client/http/HttpFunctionExecutor.java b/function-client/src/main/java/io/micronaut/function/client/http/HttpFunctionExecutor.java index 3b50a35d9ea..e6e9902b94a 100644 --- a/function-client/src/main/java/io/micronaut/function/client/http/HttpFunctionExecutor.java +++ b/function-client/src/main/java/io/micronaut/function/client/http/HttpFunctionExecutor.java @@ -22,7 +22,6 @@ import io.micronaut.function.client.FunctionDefinition; import io.micronaut.function.client.FunctionInvoker; import io.micronaut.function.client.FunctionInvokerChooser; -import io.micronaut.function.client.exceptions.FunctionExecutionException; import io.micronaut.function.client.exceptions.FunctionNotFoundException; import io.micronaut.http.HttpRequest; import io.micronaut.http.MediaType; @@ -48,14 +47,18 @@ @Singleton public class HttpFunctionExecutor implements FunctionInvoker, Closeable, FunctionInvokerChooser { + private final ConversionService conversionService; private final HttpClient httpClient; /** * Constructor. - * @param httpClient The HTTP client + * + * @param conversionService The conversion service + * @param httpClient The HTTP client */ - public HttpFunctionExecutor(HttpClient httpClient) { + public HttpFunctionExecutor(ConversionService conversionService, HttpClient httpClient) { super(); + this.conversionService = conversionService; this.httpClient = httpClient; } @@ -85,9 +88,7 @@ public O invoke(FunctionDefinition definition, I input, Argument outputType) if (Publishers.isConvertibleToPublisher(outputJavaType)) { Publisher publisher = httpClient.retrieve(request, outputType.getFirstTypeVariable().orElse(Argument.OBJECT_ARGUMENT)); - return ConversionService.SHARED.convert(publisher, outputType).orElseThrow(() -> - new FunctionExecutionException("Unsupported Reactive type: " + outputJavaType) - ); + return Publishers.convertPublisher(conversionService, publisher, outputJavaType); } else if (outputType.isVoid()) { httpClient.toBlocking().exchange(request); return null; diff --git a/http-client-core/src/main/java/io/micronaut/http/client/interceptor/HttpClientIntroductionAdvice.java b/http-client-core/src/main/java/io/micronaut/http/client/interceptor/HttpClientIntroductionAdvice.java index 34ca6b9ce24..9c54f78404b 100644 --- a/http-client-core/src/main/java/io/micronaut/http/client/interceptor/HttpClientIntroductionAdvice.java +++ b/http-client-core/src/main/java/io/micronaut/http/client/interceptor/HttpClientIntroductionAdvice.java @@ -215,7 +215,7 @@ public Object intercept(MethodInvocationContext context) { } } - InterceptedMethod interceptedMethod = InterceptedMethod.of(context); + InterceptedMethod interceptedMethod = InterceptedMethod.of(context, conversionService); // Apply all the argument binders Argument[] arguments = context.getArguments(); diff --git a/retry/src/main/java/io/micronaut/retry/intercept/DefaultRetryInterceptor.java b/retry/src/main/java/io/micronaut/retry/intercept/DefaultRetryInterceptor.java index 10e5c6875c7..d6a92ba214f 100644 --- a/retry/src/main/java/io/micronaut/retry/intercept/DefaultRetryInterceptor.java +++ b/retry/src/main/java/io/micronaut/retry/intercept/DefaultRetryInterceptor.java @@ -22,6 +22,7 @@ import io.micronaut.context.event.ApplicationEventPublisher; import io.micronaut.core.annotation.AnnotationValue; import io.micronaut.core.annotation.Nullable; +import io.micronaut.core.convert.ConversionService; import io.micronaut.core.convert.value.MutableConvertibleValues; import io.micronaut.inject.ExecutableMethod; import io.micronaut.retry.RetryState; @@ -63,6 +64,7 @@ public class DefaultRetryInterceptor implements MethodInterceptor circuitContexts = new ConcurrentHashMap<>(); @@ -70,10 +72,14 @@ public class DefaultRetryInterceptor implements MethodInterceptor context) { MutableConvertibleValues attrs = context.getAttributes(); attrs.put(RetryState.class.getName(), retry); - InterceptedMethod interceptedMethod = InterceptedMethod.of(context); + InterceptedMethod interceptedMethod = InterceptedMethod.of(context, conversionService); try { retryState.open(); // Retry method call before we have actual Publisher/CompletionStage result Object result = retrySync(context, retryState, interceptedMethod); switch (interceptedMethod.resultType()) { - case PUBLISHER: + case PUBLISHER -> { Flux reactiveSequence = Flux.from((Publisher) result); return interceptedMethod.handleResult( reactiveSequence.onErrorResume(retryFlowable(context, retryState, reactiveSequence)) .doOnNext(o -> retryState.close(null)) ); - case COMPLETION_STAGE: + } + case COMPLETION_STAGE -> { CompletableFuture newFuture = new CompletableFuture<>(); Supplier> retrySupplier = () -> interceptedMethod.interceptResultAsCompletionStage(this); ((CompletionStage) result).whenComplete(retryCompletable(context, retryState, newFuture, retrySupplier)); return interceptedMethod.handleResult(newFuture); - case SYNCHRONOUS: + } + case SYNCHRONOUS -> { retryState.close(null); return result; - default: + } + default -> { return interceptedMethod.unsupported(); + } } } catch (Exception e) { return interceptedMethod.handleException(e); diff --git a/retry/src/main/java/io/micronaut/retry/intercept/RecoveryInterceptor.java b/retry/src/main/java/io/micronaut/retry/intercept/RecoveryInterceptor.java index df9192ffcb3..27983d7d3b9 100644 --- a/retry/src/main/java/io/micronaut/retry/intercept/RecoveryInterceptor.java +++ b/retry/src/main/java/io/micronaut/retry/intercept/RecoveryInterceptor.java @@ -20,7 +20,6 @@ import io.micronaut.aop.MethodInterceptor; import io.micronaut.aop.MethodInvocationContext; import io.micronaut.context.BeanContext; -import io.micronaut.core.convert.ConversionService; import io.micronaut.inject.BeanDefinition; import io.micronaut.inject.ExecutableMethod; import io.micronaut.inject.MethodExecutionHandle; @@ -75,25 +74,29 @@ public Object intercept(MethodInvocationContext context) { if (context.getAttribute(FALLBACK_NOT_FOUND, Boolean.class).orElse(Boolean.FALSE)) { return context.proceed(); } - InterceptedMethod interceptedMethod = InterceptedMethod.of(context); + InterceptedMethod interceptedMethod = InterceptedMethod.of(context, beanContext.getConversionService()); try { switch (interceptedMethod.resultType()) { - case PUBLISHER: + case PUBLISHER -> { return interceptedMethod.handleResult( fallbackForReactiveType(context, interceptedMethod.interceptResultAsPublisher()) ); - case COMPLETION_STAGE: + } + case COMPLETION_STAGE -> { return interceptedMethod.handleResult( fallbackForFuture(context, interceptedMethod.interceptResultAsCompletionStage()) ); - case SYNCHRONOUS: + } + case SYNCHRONOUS -> { try { return context.proceed(); } catch (RuntimeException e) { return resolveFallback(context, e); } - default: + } + default -> { return interceptedMethod.unsupported(); + } } } catch (Exception e) { return interceptedMethod.handleException(e); @@ -119,7 +122,7 @@ private Publisher fallbackForReactiveType(MethodInvocationContext new FallbackException("Unsupported Reactive type: " + fallbackResult)); } } diff --git a/retry/src/test/groovy/io/micronaut/retry/intercept/InterceptorOrderSpec.groovy b/retry/src/test/groovy/io/micronaut/retry/intercept/InterceptorOrderSpec.groovy index ba8fbb37c26..c302bd72381 100644 --- a/retry/src/test/groovy/io/micronaut/retry/intercept/InterceptorOrderSpec.groovy +++ b/retry/src/test/groovy/io/micronaut/retry/intercept/InterceptorOrderSpec.groovy @@ -15,6 +15,7 @@ */ package io.micronaut.retry.intercept +import io.micronaut.core.convert.ConversionService import io.micronaut.core.order.OrderUtil import spock.lang.Specification @@ -26,7 +27,7 @@ class InterceptorOrderSpec extends Specification { void "test interceptors orders"() { given: - List interceptors = [new DefaultRetryInterceptor(null, null), new RecoveryInterceptor() ] + List interceptors = [new DefaultRetryInterceptor(ConversionService.SHARED, null, null), new RecoveryInterceptor() ] OrderUtil.sort(interceptors) expect: @@ -36,7 +37,7 @@ class InterceptorOrderSpec extends Specification { void "test interceptors orders 2"() { given: - List interceptors = [new RecoveryInterceptor(), new DefaultRetryInterceptor(null, null) ] + List interceptors = [new RecoveryInterceptor(), new DefaultRetryInterceptor(ConversionService.SHARED, null, null) ] OrderUtil.sort(interceptors) expect: diff --git a/validation/src/main/java/io/micronaut/validation/ValidatingInterceptor.java b/validation/src/main/java/io/micronaut/validation/ValidatingInterceptor.java index 84e35feffbe..86f066b582c 100644 --- a/validation/src/main/java/io/micronaut/validation/ValidatingInterceptor.java +++ b/validation/src/main/java/io/micronaut/validation/ValidatingInterceptor.java @@ -20,17 +20,19 @@ import io.micronaut.aop.MethodInterceptor; import io.micronaut.aop.MethodInvocationContext; import io.micronaut.core.annotation.Nullable; +import io.micronaut.core.convert.ConversionService; import io.micronaut.inject.ExecutableMethod; import io.micronaut.validation.validator.ExecutableMethodValidator; import io.micronaut.validation.validator.ReactiveValidator; import io.micronaut.validation.validator.Validator; import jakarta.inject.Singleton; -import java.lang.reflect.Method; -import java.util.Set; + import javax.validation.ConstraintViolation; import javax.validation.ConstraintViolationException; import javax.validation.ValidatorFactory; import javax.validation.executable.ExecutableValidator; +import java.lang.reflect.Method; +import java.util.Set; /** * A {@link MethodInterceptor} that validates method invocations. @@ -46,6 +48,7 @@ public class ValidatingInterceptor implements MethodInterceptor */ public static final int POSITION = InterceptPhase.VALIDATE.getPosition(); + private final ConversionService conversionService; private final @Nullable ExecutableValidator executableValidator; private final @Nullable ExecutableMethodValidator micronautValidator; @@ -54,9 +57,12 @@ public class ValidatingInterceptor implements MethodInterceptor * * @param micronautValidator The micronaut validator use if no factory is available * @param validatorFactory Factory returning initialized {@code Validator} instances + * @param conversionService The conversion service */ public ValidatingInterceptor(@Nullable Validator micronautValidator, - @Nullable ValidatorFactory validatorFactory) { + @Nullable ValidatorFactory validatorFactory, + ConversionService conversionService) { + this.conversionService = conversionService; if (validatorFactory != null) { javax.validation.Validator validator = validatorFactory.getValidator(); @@ -113,26 +119,22 @@ public Object intercept(MethodInvocationContext context) { } if (hasValidationAnnotation(context)) { if (micronautValidator instanceof ReactiveValidator) { - InterceptedMethod interceptedMethod = InterceptedMethod.of(context); + InterceptedMethod interceptedMethod = InterceptedMethod.of(context, conversionService); try { - switch (interceptedMethod.resultType()) { - case PUBLISHER: - return interceptedMethod.handleResult( - ((ReactiveValidator) micronautValidator).validatePublisher( - interceptedMethod.interceptResultAsPublisher(), - getValidationGroups(context)) - ); - case COMPLETION_STAGE: - return interceptedMethod.handleResult( - ((ReactiveValidator) micronautValidator).validateCompletionStage( - interceptedMethod.interceptResultAsCompletionStage(), - getValidationGroups(context)) - ); - case SYNCHRONOUS: - return validateReturnMicronautValidator(context, executableMethod); - default: - return interceptedMethod.unsupported(); - } + return switch (interceptedMethod.resultType()) { + case PUBLISHER -> interceptedMethod.handleResult( + ((ReactiveValidator) micronautValidator).validatePublisher( + interceptedMethod.interceptResultAsPublisher(), + getValidationGroups(context)) + ); + case COMPLETION_STAGE -> interceptedMethod.handleResult( + ((ReactiveValidator) micronautValidator).validateCompletionStage( + interceptedMethod.interceptResultAsCompletionStage(), + getValidationGroups(context)) + ); + case SYNCHRONOUS -> validateReturnMicronautValidator(context, executableMethod); + default -> interceptedMethod.unsupported(); + }; } catch (Exception e) { return interceptedMethod.handleException(e); } diff --git a/validation/src/test/groovy/io/micronaut/validation/ValidatedSpec.groovy b/validation/src/test/groovy/io/micronaut/validation/ValidatedSpec.groovy index ad60fd81184..4cca99e5aba 100644 --- a/validation/src/test/groovy/io/micronaut/validation/ValidatedSpec.groovy +++ b/validation/src/test/groovy/io/micronaut/validation/ValidatedSpec.groovy @@ -24,6 +24,7 @@ import io.micronaut.context.ApplicationContext import io.micronaut.context.annotation.ConfigurationProperties import io.micronaut.context.exceptions.BeanInstantiationException import io.micronaut.core.annotation.Nullable +import io.micronaut.core.convert.ConversionService import io.micronaut.core.order.OrderUtil import io.micronaut.core.type.Argument import io.micronaut.http.HttpRequest @@ -70,7 +71,7 @@ class ValidatedSpec extends Specification { Object intercept(InvocationContext context) { return null } - }, new ValidatingInterceptor(null, null)] + }, new ValidatingInterceptor(null, null, ConversionService.SHARED)] OrderUtil.sort(list) expect: diff --git a/websocket/src/main/java/io/micronaut/websocket/interceptor/ClientWebSocketInterceptor.java b/websocket/src/main/java/io/micronaut/websocket/interceptor/ClientWebSocketInterceptor.java index 8fb42f70adb..577f30d5e91 100644 --- a/websocket/src/main/java/io/micronaut/websocket/interceptor/ClientWebSocketInterceptor.java +++ b/websocket/src/main/java/io/micronaut/websocket/interceptor/ClientWebSocketInterceptor.java @@ -19,6 +19,7 @@ import io.micronaut.aop.MethodInterceptor; import io.micronaut.aop.MethodInvocationContext; import io.micronaut.context.annotation.Prototype; +import io.micronaut.core.convert.ConversionService; import io.micronaut.core.util.ArrayUtils; import io.micronaut.http.MediaType; import io.micronaut.http.annotation.Produces; @@ -36,8 +37,16 @@ @Prototype public class ClientWebSocketInterceptor implements MethodInterceptor { + private final ConversionService conversionService; private WebSocketSession webSocketSession; + /** + * @param conversionService The conversion service + */ + public ClientWebSocketInterceptor(ConversionService conversionService) { + this.conversionService = conversionService; + } + @Override public Object intercept(MethodInvocationContext context) { Class declaringType = context.getDeclaringType(); @@ -63,7 +72,7 @@ public Object intercept(MethodInvocationContext context) { MediaType mediaType = context.stringValue(Produces.class).map(MediaType::of).orElse(MediaType.APPLICATION_JSON_TYPE); validateSession(); - InterceptedMethod interceptedMethod = InterceptedMethod.of(context); + InterceptedMethod interceptedMethod = InterceptedMethod.of(context, conversionService); Class javaReturnType = context.getReturnType().getType(); if (interceptedMethod.resultType() == InterceptedMethod.ResultType.SYNCHRONOUS && javaReturnType != void.class) { return context.proceed(); @@ -71,16 +80,17 @@ public Object intercept(MethodInvocationContext context) { try { Object[] parameterValues = context.getParameterValues(); switch (parameterValues.length) { - case 0: - throw new IllegalArgumentException("At least 1 parameter is required to a send method"); - case 1: + case 0 -> throw new IllegalArgumentException("At least 1 parameter is required to a send method"); + case 1 -> { Object value = parameterValues[0]; if (value == null) { throw new IllegalArgumentException("Parameter cannot be null"); } return send(interceptedMethod, value, mediaType); - default: + } + default -> { return send(interceptedMethod, context.getParameterValueMap(), mediaType); + } } } catch (Exception e) { return interceptedMethod.handleException(e); @@ -92,15 +102,19 @@ public Object intercept(MethodInvocationContext context) { private Object send(InterceptedMethod interceptedMethod, Object message, MediaType mediaType) { switch (interceptedMethod.resultType()) { - case COMPLETION_STAGE: + case COMPLETION_STAGE -> { return interceptedMethod.handleResult(webSocketSession.sendAsync(message, mediaType)); - case PUBLISHER: + } + case PUBLISHER -> { return interceptedMethod.handleResult(webSocketSession.send(message, mediaType)); - case SYNCHRONOUS: + } + case SYNCHRONOUS -> { webSocketSession.sendSync(message, mediaType); return null; - default: + } + default -> { return interceptedMethod.unsupported(); + } } } From d08b6fa33147ed800af82174f30024ef17dc9ee4 Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Fri, 23 Dec 2022 12:09:50 +0100 Subject: [PATCH 351/743] build: update crac, grpc, micrometer and liquibase (#8528) * build: update crac, grpc, micrometer and liquibase managed-micronaut-crac to 1.1.1 managed-micronaut-grpc to 3.4.0 managed-micronaut-micrometer to 4.7.0 managed-micronaut-liquibase to 5.6.0 * add github token to graalvm workflow * build: Micronaut Build for 5.3.16 --- .github/workflows/graalvm.yml | 1 + gradle/libs.versions.toml | 8 ++++---- settings.gradle | 2 +- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/.github/workflows/graalvm.yml b/.github/workflows/graalvm.yml index 7443db9c146..199710c6e17 100644 --- a/.github/workflows/graalvm.yml +++ b/.github/workflows/graalvm.yml @@ -45,6 +45,7 @@ jobs: version: ${{ matrix.graalvm }} java-version: ${{ matrix.java }} components: 'native-image' + github-token: ${{ secrets.GITHUB_TOKEN }} - name: Build with Gradle run: | if ./gradlew tasks --no-daemon --all | grep -w "testNativeImage" diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index cd663abfe4e..a03d5f9580b 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -72,7 +72,7 @@ managed-micronaut-azure = "4.0.0" managed-micronaut-cache = "3.5.0" managed-micronaut-cassandra = "5.1.1" managed-micronaut-coherence = "3.7.2" -managed-micronaut-crac = "1.0.1" +managed-micronaut-crac = "1.1.1" managed-micronaut-data = "3.9.1" managed-micronaut-discovery = "3.2.0" managed-micronaut-elasticsearch = "4.3.0" @@ -81,7 +81,7 @@ managed-micronaut-flyway = "5.4.1" managed-micronaut-gcp = "4.8.0" managed-micronaut-graphql = "3.2.0" managed-micronaut-groovy = "3.4.0" -managed-micronaut-grpc = "3.3.1" +managed-micronaut-grpc = "3.4.0" managed-micronaut-hibernate-validator = "3.3.0" managed-micronaut-ignite = "1.0.0.RC1" managed-micronaut-jaxrs = "3.4.0" @@ -90,9 +90,9 @@ managed-micronaut-jmx = "3.2.0" managed-micronaut-kafka = "4.5.0" managed-micronaut-kotlin = "3.2.2" managed-micronaut-kubernetes = "3.4.0" -managed-micronaut-micrometer = "4.6.1" +managed-micronaut-micrometer = "4.7.0" managed-micronaut-microstream = "1.2.0" -managed-micronaut-liquibase = "5.5.0" +managed-micronaut-liquibase = "5.6.0" managed-micronaut-mongo = "4.6.0" managed-micronaut-mqtt = "2.3.0" managed-micronaut-multitenancy = "4.2.0" diff --git a/settings.gradle b/settings.gradle index c38cbc4b103..7724dccad4c 100644 --- a/settings.gradle +++ b/settings.gradle @@ -14,7 +14,7 @@ pluginManagement { } plugins { - id 'io.micronaut.build.shared.settings' version '5.3.15' + id 'io.micronaut.build.shared.settings' version '5.3.16' } enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") From d4c9c23c7f62f3884cd160c0e0d8d80630b9ca81 Mon Sep 17 00:00:00 2001 From: Denis Stepanov Date: Fri, 23 Dec 2022 12:10:05 +0100 Subject: [PATCH 352/743] Remove `FilerOutputStream` inefficiency workaround (#8529) --- .../AnnotationProcessingOutputVisitor.java | 80 +------------------ 1 file changed, 1 insertion(+), 79 deletions(-) diff --git a/inject-java/src/main/java/io/micronaut/annotation/processing/AnnotationProcessingOutputVisitor.java b/inject-java/src/main/java/io/micronaut/annotation/processing/AnnotationProcessingOutputVisitor.java index 048e50fd3d8..e779558e577 100644 --- a/inject-java/src/main/java/io/micronaut/annotation/processing/AnnotationProcessingOutputVisitor.java +++ b/inject-java/src/main/java/io/micronaut/annotation/processing/AnnotationProcessingOutputVisitor.java @@ -16,7 +16,6 @@ package io.micronaut.annotation.processing; import io.micronaut.core.annotation.Nullable; -import io.micronaut.core.reflect.ReflectionUtils; import io.micronaut.core.util.ArrayUtils; import io.micronaut.inject.writer.AbstractClassWriterOutputVisitor; import io.micronaut.inject.writer.ClassGenerationException; @@ -28,14 +27,11 @@ import javax.tools.JavaFileObject; import javax.tools.StandardLocation; import java.io.FileNotFoundException; -import java.io.FilterOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.Reader; import java.io.Writer; -import java.lang.reflect.Field; -import java.lang.reflect.Method; import java.net.URI; import java.util.ArrayList; import java.util.Arrays; @@ -52,18 +48,6 @@ */ public class AnnotationProcessingOutputVisitor extends AbstractClassWriterOutputVisitor { - private static final Field FILTER_OUTPUT_STREAM_OUT = ReflectionUtils.findField(FilterOutputStream.class, "out") - .map(field -> { - try { - addOpenJavaModules(FilterOutputStream.class, AnnotationProcessingOutputVisitor.class); - field.setAccessible(true); - return field; - } catch (Exception e) { - return null; - } - }) - .orElse(null); - private final Filer filer; private final Map> metaInfFiles = new LinkedHashMap<>(); private final Map openedFiles = new LinkedHashMap<>(); @@ -84,23 +68,6 @@ public AnnotationProcessingOutputVisitor(Filer filer) { } } - //--add-opens=java.base/$hostPackageName=ALL-UNNAMED - private static void addOpenJavaModules(Class hostClass, Class targetClass) { - // For Java 9 and above - try { - Method getModule = Class.class.getMethod("getModule"); - Class module = getModule.getReturnType(); - Method getPackageName = Class.class.getMethod("getPackageName"); - Method addOpens = module.getMethod("addOpens", String.class, module); - Object hostModule = getModule.invoke(hostClass); - String hostPackageName = (String) getPackageName.invoke(hostClass); - Object actionModule = getModule.invoke(targetClass); - addOpens.invoke(hostModule, hostPackageName, actionModule); - } catch (Exception e) { - // Ignore - } - } - private static boolean isEclipseFiler(Filer filer) { return filer != null && filer.getClass().getTypeName().startsWith("org.eclipse.jdt"); } @@ -139,8 +106,7 @@ public OutputStream visitClass(String classname, io.micronaut.inject.ast.Element nativeOriginatingElements = new Element[0]; } javaFileObject = filer.createClassFile(classname, nativeOriginatingElements); - OutputStream os = javaFileObject.openOutputStream(); - return unwrapFilterOutputStream(os); + return javaFileObject.openOutputStream(); } @Override @@ -162,50 +128,6 @@ public void visitServiceDescriptor(String type, String classname, io.micronaut.i } } - private OutputStream unwrapFilterOutputStream(OutputStream os) { - // https://bugs.openjdk.java.net/browse/JDK-8255729 - // FilterOutputStream and JavacFiler$FilerOutputStream is always using write(int) and killing performance, unwrap if possible - if (FILTER_OUTPUT_STREAM_OUT != null && os instanceof FilterOutputStream) { - try { - OutputStream osToWrite = (OutputStream) FILTER_OUTPUT_STREAM_OUT.get(os); - if (osToWrite == null) { - return os; - } - return new OutputStream() { - @Override - public void write(int b) throws IOException { - osToWrite.write(b); - } - - @Override - public void write(byte[] b) throws IOException { - osToWrite.write(b); - } - - @Override - public void write(byte[] b, int off, int len) throws IOException { - osToWrite.write(b, off, len); - } - - @Override - public void flush() throws IOException { - osToWrite.flush(); - } - - @Override - public void close() throws IOException { - // Close original output stream - os.close(); - } - }; - } catch (Exception e) { - // Use original output stream if we cannot unwrap it - return os; - } - } - return os; - } - @Override public Optional visitMetaInfFile(String path, io.micronaut.inject.ast.Element... originatingElements) { return metaInfFiles.computeIfAbsent(path, s -> { From 6c1d69d142118e202e1c16d377d7cb941ec393b6 Mon Sep 17 00:00:00 2001 From: micronaut-build <65172877+micronaut-build@users.noreply.github.com> Date: Fri, 23 Dec 2022 07:12:58 -0500 Subject: [PATCH 353/743] Bump micronaut-spring to 4.4.0 (#8531) --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index a03d5f9580b..3bf9e7917df 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -115,7 +115,7 @@ managed-micronaut-rxjava3 = "2.3.0" managed-micronaut-security = "3.9.0" managed-micronaut-serialization = "1.5.0" managed-micronaut-servlet = "3.3.2" -managed-micronaut-spring = "4.3.1" +managed-micronaut-spring = "4.4.0" managed-micronaut-sql = "4.7.2" managed-micronaut-test = "3.8.0" managed-micronaut-test-resources = "1.2.3" From 42d2740dd1ba314f2f31ebb145c4dc36293cd7a6 Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Fri, 23 Dec 2022 14:12:16 +0100 Subject: [PATCH 354/743] build: Micronaut MicroStream to 1.3.0 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 3bf9e7917df..d5240e52cfd 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -91,7 +91,7 @@ managed-micronaut-kafka = "4.5.0" managed-micronaut-kotlin = "3.2.2" managed-micronaut-kubernetes = "3.4.0" managed-micronaut-micrometer = "4.7.0" -managed-micronaut-microstream = "1.2.0" +managed-micronaut-microstream = "1.3.0" managed-micronaut-liquibase = "5.6.0" managed-micronaut-mongo = "4.6.0" managed-micronaut-mqtt = "2.3.0" From f2ef131c7ec08c57cf793c914fe5e10b04885f08 Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Fri, 23 Dec 2022 14:12:37 +0100 Subject: [PATCH 355/743] build: Micronaut RxJava to 2.4.0 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index d5240e52cfd..22a1884c5ca 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -111,7 +111,7 @@ managed-micronaut-redis = "5.3.2" managed-micronaut-rss = "3.2.0" managed-micronaut-rxjava1 = "1.0.0" managed-micronaut-rxjava2 = "1.3.0" -managed-micronaut-rxjava3 = "2.3.0" +managed-micronaut-rxjava3 = "2.4.0" managed-micronaut-security = "3.9.0" managed-micronaut-serialization = "1.5.0" managed-micronaut-servlet = "3.3.2" From 60e35b484b18fa6e1a3fdf2914abf891c6bd6c1b Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Fri, 23 Dec 2022 14:13:05 +0100 Subject: [PATCH 356/743] build: micronaut-views to 3.8.0 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 22a1884c5ca..19143093bc9 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -122,7 +122,7 @@ managed-micronaut-test-resources = "1.2.3" managed-micronaut-toml = "1.1.3" managed-micronaut-tracing = "4.4.0" managed-micronaut-tracing-legacy = "3.2.7" -managed-micronaut-views = "3.7.1" +managed-micronaut-views = "3.8.0" managed-micronaut-xml = "3.2.0" managed-neo4j = "3.5.35" managed-neo4j-java-driver = "4.4.9" From a8240030b7bd03d8ad596b7bb35f5b851fd74b67 Mon Sep 17 00:00:00 2001 From: micronaut-build <65172877+micronaut-build@users.noreply.github.com> Date: Fri, 23 Dec 2022 11:21:53 -0500 Subject: [PATCH 357/743] Bump micronaut-grpc to 3.5.0 (#8534) --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 19143093bc9..653a6edbde4 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -81,7 +81,7 @@ managed-micronaut-flyway = "5.4.1" managed-micronaut-gcp = "4.8.0" managed-micronaut-graphql = "3.2.0" managed-micronaut-groovy = "3.4.0" -managed-micronaut-grpc = "3.4.0" +managed-micronaut-grpc = "3.5.0" managed-micronaut-hibernate-validator = "3.3.0" managed-micronaut-ignite = "1.0.0.RC1" managed-micronaut-jaxrs = "3.4.0" From 8a983fc08da4a837c2b342a0adf959a25a179b6f Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Fri, 23 Dec 2022 20:32:43 +0100 Subject: [PATCH 358/743] build: update swagger to 2.2.7 Align it to Open API module https://github.com/micronaut-projects/micronaut-openapi/blob/4.8.x/gradle/libs.versions.toml#L2 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 653a6edbde4..48d2f178fa4 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -139,7 +139,7 @@ managed-spock = "2.0-groovy-3.0" managed-spotbugs = "4.7.1" managed-spring = "5.3.23" managed-springboot = "2.7.0" -managed-swagger = "2.2.3" +managed-swagger = "2.2.7" managed-validation = "2.0.1.Final" managed-testcontainers = "1.17.5" managed-snakeyaml = "1.33" From ab6efefbea574e6a63e883724b74ab309fba6ae4 Mon Sep 17 00:00:00 2001 From: Denis Stepanov Date: Fri, 23 Dec 2022 20:55:37 +0100 Subject: [PATCH 359/743] Fix `@EachBean` with `@Replaces` and a missing class (#8530) --- .../EachBeanInterceptorSpec.groovy | 2 +- .../EachBeanReplacesSpec.groovy | 21 +++ .../eachbeanreplaces/MyDataSource.java | 9 ++ .../eachbeanreplaces/MyService.java | 13 ++ .../micronaut/context/DefaultBeanContext.java | 136 ++++++------------ .../annotation/DefaultImplementation.java | 1 + .../context/annotation/Replaces.java | 6 +- 7 files changed, 97 insertions(+), 91 deletions(-) create mode 100644 inject-java/src/test/groovy/io/micronaut/inject/configproperties/eachbeanreplaces/EachBeanReplacesSpec.groovy create mode 100644 inject-java/src/test/groovy/io/micronaut/inject/configproperties/eachbeanreplaces/MyDataSource.java create mode 100644 inject-java/src/test/groovy/io/micronaut/inject/configproperties/eachbeanreplaces/MyService.java diff --git a/inject-java/src/test/groovy/io/micronaut/inject/configproperties/eachbeaninterceptor/EachBeanInterceptorSpec.groovy b/inject-java/src/test/groovy/io/micronaut/inject/configproperties/eachbeaninterceptor/EachBeanInterceptorSpec.groovy index 95a81f725a1..cbfb252a5b7 100644 --- a/inject-java/src/test/groovy/io/micronaut/inject/configproperties/eachbeaninterceptor/EachBeanInterceptorSpec.groovy +++ b/inject-java/src/test/groovy/io/micronaut/inject/configproperties/eachbeaninterceptor/EachBeanInterceptorSpec.groovy @@ -5,7 +5,7 @@ import io.micronaut.context.ApplicationContext class EachBeanInterceptorSpec extends AbstractTypeElementSpec { - void 'test interceptor on an event'() { + void 'test prototype interceptor is injected with introspected bean qualifier'() { given: ApplicationContext ctx = ApplicationContext.run(['spec': 'EachBeanInterceptorSpec', 'mydatasources.default.xyz': '111', 'mydatasources.foo.xyz': '111', 'mydatasources.bar.xyz': '111']) diff --git a/inject-java/src/test/groovy/io/micronaut/inject/configproperties/eachbeanreplaces/EachBeanReplacesSpec.groovy b/inject-java/src/test/groovy/io/micronaut/inject/configproperties/eachbeanreplaces/EachBeanReplacesSpec.groovy new file mode 100644 index 00000000000..e0154603a45 --- /dev/null +++ b/inject-java/src/test/groovy/io/micronaut/inject/configproperties/eachbeanreplaces/EachBeanReplacesSpec.groovy @@ -0,0 +1,21 @@ +package io.micronaut.inject.configproperties.eachbeanreplaces + +import io.micronaut.annotation.processing.test.AbstractTypeElementSpec +import io.micronaut.context.ApplicationContext + +class EachBeanReplacesSpec extends AbstractTypeElementSpec { + + void 'test each bean with empty bean doesnt replace itself'() { + given: + ApplicationContext ctx = ApplicationContext.run(['spec': 'EachBeanReplacesSpec', 'mydatasources.default.xyz': '111', 'mydatasources.foo.xyz': '111', 'mydatasources.bar.xyz': '111']) + + when: + def services = ctx.getBeansOfType(MyService) + + then: + services.size() == 3 + + cleanup: + ctx.close() + } +} diff --git a/inject-java/src/test/groovy/io/micronaut/inject/configproperties/eachbeanreplaces/MyDataSource.java b/inject-java/src/test/groovy/io/micronaut/inject/configproperties/eachbeanreplaces/MyDataSource.java new file mode 100644 index 00000000000..34b7fe25662 --- /dev/null +++ b/inject-java/src/test/groovy/io/micronaut/inject/configproperties/eachbeanreplaces/MyDataSource.java @@ -0,0 +1,9 @@ +package io.micronaut.inject.configproperties.eachbeanreplaces; + +import io.micronaut.context.annotation.EachProperty; +import io.micronaut.context.annotation.Requires; + +@Requires(property = "spec", value = "EachBeanReplacesSpec") +@EachProperty(value = "mydatasources", primary = "default") +class MyDataSource { +} diff --git a/inject-java/src/test/groovy/io/micronaut/inject/configproperties/eachbeanreplaces/MyService.java b/inject-java/src/test/groovy/io/micronaut/inject/configproperties/eachbeanreplaces/MyService.java new file mode 100644 index 00000000000..45b117c165e --- /dev/null +++ b/inject-java/src/test/groovy/io/micronaut/inject/configproperties/eachbeanreplaces/MyService.java @@ -0,0 +1,13 @@ +package io.micronaut.inject.configproperties.eachbeanreplaces; + +import io.micronaut.context.annotation.EachBean; +import io.micronaut.context.annotation.Replaces; +import io.micronaut.context.annotation.Requires; +import jakarta.inject.Singleton; + +@Requires(property = "spec", value = "EachBeanReplacesSpec") +@EachBean(MyDataSource.class) +@Singleton +@Replaces // This is equivalent to `@Replaces(ClassNotOnClasspath.class)` +class MyService { +} diff --git a/inject/src/main/java/io/micronaut/context/DefaultBeanContext.java b/inject/src/main/java/io/micronaut/context/DefaultBeanContext.java index 505a7b74078..d9739ee0778 100644 --- a/inject/src/main/java/io/micronaut/context/DefaultBeanContext.java +++ b/inject/src/main/java/io/micronaut/context/DefaultBeanContext.java @@ -162,7 +162,6 @@ public class DefaultBeanContext implements InitializableBeanContext { protected static final Logger LOG_LIFECYCLE = LoggerFactory.getLogger(DefaultBeanContext.class.getPackage().getName() + ".lifecycle"); @SuppressWarnings("rawtypes") private static final Qualifier PROXY_TARGET_QUALIFIER = new Qualifier<>() { - @SuppressWarnings("rawtypes") @Override public > Stream reduce(Class beanType, Stream candidates) { return candidates.filter(bt -> { @@ -177,8 +176,6 @@ public > Stream reduce(Class beanType, Str private static final String SCOPED_PROXY_ANN = "io.micronaut.runtime.context.scope.ScopedProxy"; private static final String INTRODUCTION_TYPE = "io.micronaut.aop.Introduction"; private static final String ADAPTER_TYPE = "io.micronaut.aop.Adapter"; - private static final String NAMED_MEMBER = "named"; - private static final String QUALIFIER_MEMBER = "qualifier"; private static final String PARALLEL_TYPE = Parallel.class.getName(); private static final String INDEXES_TYPE = Indexes.class.getName(); private static final String REPLACES_ANN = Replaces.class.getName(); @@ -2586,16 +2583,14 @@ protected void processParallelBeans(List parallelBeans) } } - private void filterReplacedBeans(BeanResolutionContext resolutionContext, Collection> candidates) { + private void filterReplacedBeans(BeanResolutionContext resolutionContext, Collection> candidates) { if (candidates.size() > 1) { - List> replacementTypes = new ArrayList<>(2); - - for (BeanType candidate : candidates) { + List> replacementTypes = new ArrayList<>(2); + for (BeanDefinition candidate : candidates) { if (candidate.getAnnotationMetadata().hasStereotype(REPLACES_ANN)) { replacementTypes.add(candidate); } } - if (!replacementTypes.isEmpty()) { candidates.removeIf(definition -> checkIfReplacementExists(resolutionContext, replacementTypes, definition)); } @@ -2603,8 +2598,8 @@ private void filterReplacedBeans(BeanResolutionContext resolutionContext, Co } private boolean checkIfReplacementExists(BeanResolutionContext resolutionContext, - List> replacementTypes, - BeanType definitionToBeReplaced) { + List> replacementTypes, + BeanDefinition definitionToBeReplaced) { if (!definitionToBeReplaced.isEnabled(this, resolutionContext)) { return true; } @@ -2612,7 +2607,7 @@ private boolean checkIfReplacementExists(BeanResolutionContext resolutionCon if (annotationMetadata.hasDeclaredStereotype(Infrastructure.class)) { return false; } - for (BeanType replacementType : replacementTypes) { + for (BeanDefinition replacementType : replacementTypes) { if (isNotTheSameDefinition(replacementType, definitionToBeReplaced) && isNotProxy(replacementType, definitionToBeReplaced) && checkIfReplaces(replacementType, definitionToBeReplaced, annotationMetadata)) { @@ -2622,21 +2617,26 @@ private boolean checkIfReplacementExists(BeanResolutionContext resolutionCon return false; } - private boolean isNotTheSameDefinition(BeanType replacingCandidate, BeanType definitionToBeReplaced) { + private boolean isNotTheSameDefinition(BeanDefinition replacingCandidate, BeanDefinition definitionToBeReplaced) { + if (replacingCandidate instanceof BeanDefinitionDelegate beanDefinitionDelegate) { + replacingCandidate = beanDefinitionDelegate.getDelegate(); + } + if (definitionToBeReplaced instanceof BeanDefinitionDelegate beanDefinitionDelegate) { + definitionToBeReplaced = beanDefinitionDelegate.getDelegate(); + } return replacingCandidate != definitionToBeReplaced; } - private boolean isNotProxy(BeanType replacingCandidate, BeanType definitionToBeReplaced) { + private boolean isNotProxy(BeanDefinition replacingCandidate, BeanDefinition definitionToBeReplaced) { return !(replacingCandidate instanceof ProxyBeanDefinition && ((ProxyBeanDefinition) replacingCandidate).getTargetDefinitionType() == definitionToBeReplaced.getClass()); } - private boolean checkIfReplaces(BeanType replacingCandidate, BeanType definitionToBeReplaced, AnnotationMetadata annotationMetadata) { - + private boolean checkIfReplaces(BeanDefinition replacingCandidate, BeanDefinition definitionToBeReplaced, AnnotationMetadata annotationMetadata) { final AnnotationValue replacesAnnotation = replacingCandidate.getAnnotation(Replaces.class); - Class replacedBeanType = replacesAnnotation.classValue().orElse(getCanonicalBeanType(replacingCandidate)); - final Optional named = replacesAnnotation.stringValue(NAMED_MEMBER); - final Optional> qualifier = replacesAnnotation.annotationClassValue(QUALIFIER_MEMBER); + final Class replacedBeanType = replacesAnnotation.classValue(Replaces.MEMBER_BEAN).orElse(getCanonicalBeanType(replacingCandidate)); + final Optional named = replacesAnnotation.stringValue(Replaces.MEMBER_NAMED); + final Optional> qualifier = replacesAnnotation.annotationClassValue(Replaces.MEMBER_QUALIFIER); if (named.isPresent() && qualifier.isPresent()) { throw new ConfigurationException("Both \"named\" and \"qualifier\" should not be present: " + replacesAnnotation); @@ -2666,20 +2666,20 @@ private boolean checkIfReplaces(BeanType replacingCandidate, BeanType return false; } - Optional> factory = replacesAnnotation.classValue("factory"); - - Optional> declaringType = definitionToBeReplaced instanceof BeanDefinition ? - ((BeanDefinition) definitionToBeReplaced).getDeclaringType() : - Optional.empty(); - if (factory.isPresent() && declaringType.isPresent()) { - final boolean factoryReplaces = factory.get() == declaringType.get() && - checkIfTypeMatches(definitionToBeReplaced, annotationMetadata, replacedBeanType); - if (factoryReplaces) { - if (LOG.isDebugEnabled()) { - LOG.debug("Bean [{}] replaces existing bean of type [{}] in factory type [{}]", - replacingCandidate.getBeanType(), replacedBeanType, factory.get()); + Optional> factory = replacesAnnotation.classValue(Replaces.MEMBER_FACTORY); + if (factory.isPresent()) { + Optional> declaringType = definitionToBeReplaced.getDeclaringType(); + if (declaringType.isPresent()) { + Class factoryClass = factory.get(); + final boolean factoryReplaces = factoryClass == declaringType.get() && + checkIfTypeMatches(definitionToBeReplaced, annotationMetadata, replacedBeanType); + if (factoryReplaces) { + if (LOG.isDebugEnabled()) { + LOG.debug("Bean [{}] replaces existing bean of type [{}] in factory type [{}]", + replacingCandidate.getBeanType(), replacedBeanType, factoryClass); + } + return true; } - return true; } return false; } @@ -2691,7 +2691,7 @@ private boolean checkIfReplaces(BeanType replacingCandidate, BeanType return isTypeMatches; } - private boolean qualifiedByQualifier(BeanType definitionToBeReplaced, + private boolean qualifiedByQualifier(BeanDefinition definitionToBeReplaced, Class replacedBeanType, AnnotationClassValue qualifier) { @SuppressWarnings("unchecked") final Class qualifierClass = @@ -2709,63 +2709,23 @@ private boolean qualifiedByNamed(BeanType definitionToBeReplaced, Class r .isPresent(); } - private Class getCanonicalBeanType(BeanType beanType) { - if (beanType instanceof AdvisedBeanType) { - return (Class) ((AdvisedBeanType) beanType).getInterceptedType(); - } else if (beanType instanceof ProxyBeanDefinition) { - return ((ProxyBeanDefinition) beanType).getTargetType(); - } else { - AnnotationMetadata annotationMetadata = beanType.getAnnotationMetadata(); - Class bt = beanType.getBeanType(); - if (annotationMetadata.hasStereotype(INTRODUCTION_TYPE)) { - Class superclass = bt.getSuperclass(); - if (superclass == Object.class || superclass == null) { - // interface introduction - return bt; - } else { - // abstract class introduction - return (Class) superclass; - } - } else if (annotationMetadata.hasStereotype(AnnotationUtil.ANN_AROUND)) { - Class superclass = bt.getSuperclass(); - if (superclass != null) { - return (Class) superclass; - } else { - return bt; - } - } - return bt; + private Class getCanonicalBeanType(BeanDefinition beanDefinition) { + if (beanDefinition instanceof BeanDefinitionDelegate beanDefinitionDelegate) { + beanDefinition = beanDefinitionDelegate.getDelegate(); } - } - - private boolean checkIfTypeMatches(BeanType definitionToBeReplaced, - AnnotationMetadata annotationMetadata, - Class replacingCandidate) { - Class bt; - - if (definitionToBeReplaced instanceof ProxyBeanDefinition) { - bt = ((ProxyBeanDefinition) definitionToBeReplaced).getTargetType(); - } else if (definitionToBeReplaced instanceof AdvisedBeanType) { - //noinspection unchecked - bt = (Class) ((AdvisedBeanType) definitionToBeReplaced).getInterceptedType(); - } else { - bt = definitionToBeReplaced.getBeanType(); - if (annotationMetadata.hasStereotype(INTRODUCTION_TYPE)) { - Class superclass = bt.getSuperclass(); - if (superclass == Object.class) { - // interface introduction - return replacingCandidate.isAssignableFrom(bt); - } else { - // abstract class introduction - return replacingCandidate == superclass; - } - } - if (annotationMetadata.hasStereotype(AnnotationUtil.ANN_AROUND)) { - Class superclass = bt.getSuperclass(); - return replacingCandidate == superclass || replacingCandidate == bt; - } + if (beanDefinition instanceof AdvisedBeanType advisedBeanType) { + return (Class) advisedBeanType.getInterceptedType(); + } + if (beanDefinition instanceof ProxyBeanDefinition proxyBeanDefinition) { + return proxyBeanDefinition.getTargetType(); } + return beanDefinition.getBeanType(); + } + private boolean checkIfTypeMatches(BeanDefinition definitionToBeReplaced, + AnnotationMetadata annotationMetadata, + Class replacingCandidate) { + Class bt = getCanonicalBeanType(definitionToBeReplaced); if (annotationMetadata.hasAnnotation(DefaultImplementation.class)) { Optional defaultImpl = annotationMetadata.classValue(DefaultImplementation.class); if (!defaultImpl.isPresent()) { @@ -2777,15 +2737,13 @@ private boolean checkIfTypeMatches(BeanType definitionToBeReplaced, return replacingCandidate == bt; } } - return replacingCandidate != Object.class && replacingCandidate.isAssignableFrom(bt); } private void doInjectAndInitialize(BeanResolutionContext resolutionContext, T instance, BeanDefinition beanDefinition) { if (beanDefinition instanceof InjectableBeanDefinition injectableBeanDefinition) { injectableBeanDefinition.inject(resolutionContext, this, instance); - if (injectableBeanDefinition instanceof InitializingBeanDefinition) { - InitializingBeanDefinition initializingBeanDefinition = ((InitializingBeanDefinition) injectableBeanDefinition); + if (beanDefinition instanceof InitializingBeanDefinition initializingBeanDefinition) { initializingBeanDefinition.initialize(resolutionContext, this, instance); } } else { diff --git a/inject/src/main/java/io/micronaut/context/annotation/DefaultImplementation.java b/inject/src/main/java/io/micronaut/context/annotation/DefaultImplementation.java index 225af09aab7..809e8631274 100644 --- a/inject/src/main/java/io/micronaut/context/annotation/DefaultImplementation.java +++ b/inject/src/main/java/io/micronaut/context/annotation/DefaultImplementation.java @@ -66,6 +66,7 @@ /** * @return The bean type that is the default implementation */ + @AliasFor(member = "name") Class value() default void.class; /** diff --git a/inject/src/main/java/io/micronaut/context/annotation/Replaces.java b/inject/src/main/java/io/micronaut/context/annotation/Replaces.java index 9d80aa621c7..e4fca881933 100644 --- a/inject/src/main/java/io/micronaut/context/annotation/Replaces.java +++ b/inject/src/main/java/io/micronaut/context/annotation/Replaces.java @@ -32,6 +32,11 @@ @Retention(RetentionPolicy.RUNTIME) public @interface Replaces { + String MEMBER_NAMED = "named"; + String MEMBER_FACTORY = "factory"; + String MEMBER_BEAN = "bean"; + String MEMBER_QUALIFIER = "qualifier"; + /** * @return The bean type that this bean replaces */ @@ -41,7 +46,6 @@ /** * @return The bean type that this bean replaces */ - @AliasFor(member = "value") Class bean() default void.class; /** From 3815723e71c9a59852cc09b358ba641832a5555e Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Mon, 26 Dec 2022 07:09:09 +0100 Subject: [PATCH 360/743] fix: graal error for private @Property (#8541) * checkstyle: trailing dot for first sentence * fix: graal error for private @property Whiel deploying a GraalVM native executable of a lambda function with 22.3 and MN 3.8.0 I was getting: ``` Caused by: java.lang.NoSuchFieldError: No field 'logbackXmlLocation' found for type: io.micronaut.logging.impl.LogbackLoggingSystem at io.micronaut.core.reflect.ReflectionUtils.lambda$getRequiredField$2(ReflectionUtils.java:294) at java.util.Optional.orElseThrow(Optional.java:408) at io.micronaut.core.reflect.ReflectionUtils.getRequiredField(ReflectionUtils.java:294) at io.micronaut.context.AbstractInitializableBeanDefinition.setFieldWithReflection(AbstractInitializableBeanDefinition.java:979) io.micronaut.context.exceptions.BeanInstantiationException: Bean definition [io.micronaut.logging.impl.LogbackLoggingSystem] could not be loaded: Error instantiating bean of type [io.micronaut.logging.impl.LogbackLoggingSystem] Caused by: io.micronaut.context.exceptions.DependencyInjectionException: Error instantiating bean of type [io.micronaut.logging.impl.LogbackLoggingSystem] Message: Error setting field value: No field 'logbackXmlLocation' found for type: io.micronaut.logging.impl.LogbackLoggingSystem Path Taken: new LogbackLoggingSystem() at io.micronaut.context.AbstractInitializableBeanDefinition.setFieldWithReflection(AbstractInitializableBeanDefinition.java:986) at io.micronaut.logging.impl.$LogbackLoggingSystem$Definition.injectBean(Unknown Source) ``` This PR changes LogbackLoggingSystem to use constructor injection for the property `logger.config` to avoid such an error. --- .../logging/impl/LogbackLoggingSystem.java | 16 +++++++++------- .../retry/annotation/CircuitBreaker.java | 2 +- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/runtime/src/main/java/io/micronaut/logging/impl/LogbackLoggingSystem.java b/runtime/src/main/java/io/micronaut/logging/impl/LogbackLoggingSystem.java index 5c984f0c3c0..a8b72e2b0ca 100644 --- a/runtime/src/main/java/io/micronaut/logging/impl/LogbackLoggingSystem.java +++ b/runtime/src/main/java/io/micronaut/logging/impl/LogbackLoggingSystem.java @@ -17,7 +17,6 @@ import java.net.URL; import java.util.Objects; -import java.util.Optional; import ch.qos.logback.classic.Level; import ch.qos.logback.classic.LoggerContext; @@ -26,6 +25,7 @@ import io.micronaut.context.annotation.Property; import io.micronaut.context.annotation.Requires; import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.Nullable; import io.micronaut.logging.LogLevel; import io.micronaut.logging.LoggingSystem; import io.micronaut.logging.LoggingSystemException; @@ -45,8 +45,11 @@ public final class LogbackLoggingSystem implements LoggingSystem { private static final String DEFAULT_LOGBACK_LOCATION = "logback.xml"; - @Property(name = "logger.config") - private Optional logbackXmlLocation; + private String logbackXmlLocation; + + public LogbackLoggingSystem(@Nullable @Property(name = "logger.config") String logbackXmlLocation) { + this.logbackXmlLocation = logbackXmlLocation != null ? logbackXmlLocation : DEFAULT_LOGBACK_LOCATION; + } @Override public void setLogLevel(String name, LogLevel level) { @@ -57,13 +60,12 @@ public void setLogLevel(String name, LogLevel level) { public void refresh() { LoggerContext context = getLoggerContext(); context.reset(); - String logbackXml = logbackXmlLocation.orElse(DEFAULT_LOGBACK_LOCATION); - URL resource = getClass().getClassLoader().getResource(logbackXml); + URL resource = getClass().getClassLoader().getResource(logbackXmlLocation); if (Objects.isNull(resource)) { - throw new LoggingSystemException("Resource " + logbackXml + " not found"); + throw new LoggingSystemException("Resource " + logbackXmlLocation + " not found"); } - try { + try { new ContextInitializer(context).configureByResource(resource); } catch (JoranException e) { throw new LoggingSystemException("Error while refreshing Logback", e); diff --git a/runtime/src/main/java/io/micronaut/retry/annotation/CircuitBreaker.java b/runtime/src/main/java/io/micronaut/retry/annotation/CircuitBreaker.java index e3186977fda..fbd028455f8 100644 --- a/runtime/src/main/java/io/micronaut/retry/annotation/CircuitBreaker.java +++ b/runtime/src/main/java/io/micronaut/retry/annotation/CircuitBreaker.java @@ -97,7 +97,7 @@ Class predicate() default DefaultRetryPredicate.class; /** - * If {@code true} and the circuit is opened, it throws the original exception wrapped + * If {@code true} and the circuit is opened, it throws the original exception wrapped. * in a {@link io.micronaut.retry.exception.CircuitOpenException} * @return Whether to wrap the original exception in a {@link io.micronaut.retry.exception.CircuitOpenException} */ From aa69a3fa8a4a3a490ec11b9ae60c3a6a55fa6c03 Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Mon, 26 Dec 2022 10:22:37 +0100 Subject: [PATCH 361/743] fix: graal error for private @Property (#8542) Changes to inject @Property(name = "logger.config") via constructor injection. Graal 22.3 and Micronaut 3.8.0 was throwing: ``` Caused by: java.lang.NoSuchFieldError: No field 'logbackXmlLocation' found for type: io.micronaut.management.endpoint.loggers.impl.LogbackLoggingSystem 195 at io.micronaut.core.reflect.ReflectionUtils.lambda$getRequiredField$2(ReflectionUtils.java:294) 196 at java.base@17.0.5/java.util.Optional.orElseThrow(Optional.java:403) 197 at io.micronaut.core.reflect.ReflectionUtils.getRequiredField(ReflectionUtils.java:294) 198 at io.micronaut.context.AbstractInitializableBeanDefinition.setFieldWithReflection(AbstractInitializableBeanDefinition.java:979) 199 ... 113 common frames omitted ``` --- .../loggers/impl/LogbackLoggingSystem.java | 28 +++++++++++++------ .../logging/impl/LogbackLoggingSystem.java | 2 +- 2 files changed, 21 insertions(+), 9 deletions(-) diff --git a/management/src/main/java/io/micronaut/management/endpoint/loggers/impl/LogbackLoggingSystem.java b/management/src/main/java/io/micronaut/management/endpoint/loggers/impl/LogbackLoggingSystem.java index 4f0ef79d681..d0b6440b505 100644 --- a/management/src/main/java/io/micronaut/management/endpoint/loggers/impl/LogbackLoggingSystem.java +++ b/management/src/main/java/io/micronaut/management/endpoint/loggers/impl/LogbackLoggingSystem.java @@ -24,18 +24,19 @@ import io.micronaut.context.annotation.Replaces; import io.micronaut.context.annotation.Requires; import io.micronaut.core.annotation.NonNull; +import io.micronaut.core.annotation.Nullable; import io.micronaut.logging.LogLevel; import io.micronaut.logging.LoggingSystemException; import io.micronaut.management.endpoint.loggers.LoggerConfiguration; import io.micronaut.management.endpoint.loggers.LoggersEndpoint; import io.micronaut.management.endpoint.loggers.ManagedLoggingSystem; +import jakarta.inject.Inject; import jakarta.inject.Singleton; import org.slf4j.LoggerFactory; import java.net.URL; import java.util.Collection; import java.util.Objects; -import java.util.Optional; import java.util.stream.Collectors; /** @@ -51,8 +52,20 @@ public class LogbackLoggingSystem implements ManagedLoggingSystem, io.micronaut.logging.LoggingSystem { private static final String DEFAULT_LOGBACK_LOCATION = "logback.xml"; - @Property(name = "logger.config") - private Optional logbackXmlLocation; + private final String logbackXmlLocation; + + /** + * @deprecated Use {@link LogbackLoggingSystem(String)} instead. + */ + @Deprecated + public LogbackLoggingSystem() { + this(null); + } + + @Inject + public LogbackLoggingSystem(@Nullable @Property(name = "logger.config") String logbackXmlLocation) { + this.logbackXmlLocation = logbackXmlLocation != null ? logbackXmlLocation : DEFAULT_LOGBACK_LOCATION; + } @Override @NonNull @@ -117,18 +130,17 @@ private static Level toLevel(LogLevel logLevel) { return Level.valueOf(logLevel.name()); } } - + @Override public void refresh() { LoggerContext context = getLoggerContext(); context.reset(); - String logbackXml = logbackXmlLocation.orElse(DEFAULT_LOGBACK_LOCATION); - URL resource = getClass().getClassLoader().getResource(logbackXml); + URL resource = getClass().getClassLoader().getResource(logbackXmlLocation); if (Objects.isNull(resource)) { - throw new LoggingSystemException("Resource " + logbackXml + " not found"); + throw new LoggingSystemException("Resource " + logbackXmlLocation + " not found"); } - try { + try { new ContextInitializer(context).configureByResource(resource); } catch (JoranException e) { throw new LoggingSystemException("Error while refreshing Logback", e); diff --git a/runtime/src/main/java/io/micronaut/logging/impl/LogbackLoggingSystem.java b/runtime/src/main/java/io/micronaut/logging/impl/LogbackLoggingSystem.java index a8b72e2b0ca..9aa81b153d5 100644 --- a/runtime/src/main/java/io/micronaut/logging/impl/LogbackLoggingSystem.java +++ b/runtime/src/main/java/io/micronaut/logging/impl/LogbackLoggingSystem.java @@ -45,7 +45,7 @@ public final class LogbackLoggingSystem implements LoggingSystem { private static final String DEFAULT_LOGBACK_LOCATION = "logback.xml"; - private String logbackXmlLocation; + private final String logbackXmlLocation; public LogbackLoggingSystem(@Nullable @Property(name = "logger.config") String logbackXmlLocation) { this.logbackXmlLocation = logbackXmlLocation != null ? logbackXmlLocation : DEFAULT_LOGBACK_LOCATION; From ba903a46c5d88e068e7605c472781159d38781e4 Mon Sep 17 00:00:00 2001 From: micronaut-build <65172877+micronaut-build@users.noreply.github.com> Date: Mon, 26 Dec 2022 11:35:23 -0500 Subject: [PATCH 362/743] Bump micronaut-aws to 3.10.1 (#8543) --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 48d2f178fa4..572983a5983 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -67,7 +67,7 @@ managed-methvin-directory-watcher = "0.16.1" managed-micrometer = "1.9.4" managed-micronaut-acme = "3.2.0" managed-micronaut-aot = "1.1.1" -managed-micronaut-aws = "3.10.0" +managed-micronaut-aws = "3.10.1" managed-micronaut-azure = "4.0.0" managed-micronaut-cache = "3.5.0" managed-micronaut-cassandra = "5.1.1" From 1f13b6b72ac23a856d53ee83570829d7ef81b3f7 Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Mon, 26 Dec 2022 18:43:52 +0100 Subject: [PATCH 363/743] doc: What's new for 3.8.0 [skip ci] --- src/main/docs/guide/introduction/whatsNew.adoc | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/main/docs/guide/introduction/whatsNew.adoc b/src/main/docs/guide/introduction/whatsNew.adoc index 758ef2ed828..55a613d0ac4 100644 --- a/src/main/docs/guide/introduction/whatsNew.adoc +++ b/src/main/docs/guide/introduction/whatsNew.adoc @@ -4,6 +4,10 @@ Key features: - https://www.graalvm.org/release-notes/22_3/[GraalVM 22.3 Support] +- With Micronaut `3.8.0`, you can use `@RequestBean` annotations with https://docs.oracle.com/en/java/javase/14/language/records.html[Records]. Before `3.8.0`, you could use a POJO as a controller method parameter and annotate the parameter with `@RequestBean` to bind any Bindable value (e.g., `HttpRequest`, `@PathVariable`, `@QueryValue` or `@Header` fields). +- If you enable CORS from any origin while running your app in localhost (e.g., test or development), since `3.8.0`, the `CorsFilter` returns 403 for non-localhost origins to protect you against drive-by localhost attacks. + +Please read the https://micronaut.io/2022/12/27/micronaut-framework-3-8-0-released/[Micronaut Framework 3.8.0 announcement blog post]. You will find a detailed overview of what’s new in Micronaut 3.8.0. == 3.7.0 From 512639a9c6b32693450942636c0084541cda9026 Mon Sep 17 00:00:00 2001 From: micronaut-build <65172877+micronaut-build@users.noreply.github.com> Date: Mon, 26 Dec 2022 16:48:55 -0500 Subject: [PATCH 364/743] Bump micronaut-aws to 3.10.2 (#8544) --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 572983a5983..3cbbaf5e021 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -67,7 +67,7 @@ managed-methvin-directory-watcher = "0.16.1" managed-micrometer = "1.9.4" managed-micronaut-acme = "3.2.0" managed-micronaut-aot = "1.1.1" -managed-micronaut-aws = "3.10.1" +managed-micronaut-aws = "3.10.2" managed-micronaut-azure = "4.0.0" managed-micronaut-cache = "3.5.0" managed-micronaut-cassandra = "5.1.1" From 1821c089fd6ee063ded8caf5648e2f9f79bcca9c Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 27 Dec 2022 08:31:38 +0100 Subject: [PATCH 365/743] fix(deps): update httpcomponents-client to v4.5.14 (#8536) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index adf4cf5362b..ca5643270a7 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -19,7 +19,7 @@ hibernate = "5.5.9.Final" hibernate-validator = "6.1.6.Final" htmlSanityCheck = "1.1.6" htmlunit = "2.64.0" -httpcomponents-client = "4.5.13" +httpcomponents-client = "4.5.14" jakarta-inject-api = "2.0.1" jakarta-inject-tck = "2.0.1" javax-annotation-api = "1.3.2" From 87824b2b3b0cf45a42aae4a8fafb19ba21a0fcc6 Mon Sep 17 00:00:00 2001 From: micronaut-build <65172877+micronaut-build@users.noreply.github.com> Date: Tue, 27 Dec 2022 03:50:25 -0500 Subject: [PATCH 366/743] Bump micronaut-aws to 3.10.3 (#8545) --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 3cbbaf5e021..3960f6373ad 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -67,7 +67,7 @@ managed-methvin-directory-watcher = "0.16.1" managed-micrometer = "1.9.4" managed-micronaut-acme = "3.2.0" managed-micronaut-aot = "1.1.1" -managed-micronaut-aws = "3.10.2" +managed-micronaut-aws = "3.10.3" managed-micronaut-azure = "4.0.0" managed-micronaut-cache = "3.5.0" managed-micronaut-cassandra = "5.1.1" From 83d2cfc26fb609c40c60942ac52ca63c0028c6c7 Mon Sep 17 00:00:00 2001 From: micronaut-build <65172877+micronaut-build@users.noreply.github.com> Date: Tue, 27 Dec 2022 14:42:17 -0500 Subject: [PATCH 367/743] Bump micronaut-aws to 3.10.4 (#8546) --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 3960f6373ad..169e9f5298c 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -67,7 +67,7 @@ managed-methvin-directory-watcher = "0.16.1" managed-micrometer = "1.9.4" managed-micronaut-acme = "3.2.0" managed-micronaut-aot = "1.1.1" -managed-micronaut-aws = "3.10.3" +managed-micronaut-aws = "3.10.4" managed-micronaut-azure = "4.0.0" managed-micronaut-cache = "3.5.0" managed-micronaut-cassandra = "5.1.1" From 1e9e78f26126ec732a0eb87324f1f66811250a86 Mon Sep 17 00:00:00 2001 From: micronaut-build Date: Wed, 28 Dec 2022 06:50:29 +0000 Subject: [PATCH 368/743] [skip ci] Release v3.8.0 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 222a3fba506..0be79888a2f 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -projectVersion=3.8.0-SNAPSHOT +projectVersion=3.8.0 projectGroupId=io.micronaut title=Micronaut projectDesc=Natively Cloud Native From 52754d6806f4211e2391b002bdc2e331204a6620 Mon Sep 17 00:00:00 2001 From: micronaut-build Date: Wed, 28 Dec 2022 07:01:25 +0000 Subject: [PATCH 369/743] Back to 3.8.1-SNAPSHOT --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 0be79888a2f..d8761ab707a 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -projectVersion=3.8.0 +projectVersion=3.8.1-SNAPSHOT projectGroupId=io.micronaut title=Micronaut projectDesc=Natively Cloud Native From f662240bf41008c0b85b8385425a2713217cc72c Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Wed, 28 Dec 2022 08:41:09 +0100 Subject: [PATCH 370/743] ci: projectVersion to 3.9.0-SNAPSHOT --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index d8761ab707a..2eb7ea6279d 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -projectVersion=3.8.1-SNAPSHOT +projectVersion=3.9.0-SNAPSHOT projectGroupId=io.micronaut title=Micronaut projectDesc=Natively Cloud Native From 45455a37a55ae52e61c13e854b9ac4c43e932848 Mon Sep 17 00:00:00 2001 From: Graeme Rocher Date: Tue, 3 Jan 2023 11:21:25 +0100 Subject: [PATCH 371/743] Remove usage of Utils classes from GraalReflectionConfigurer (#8563) --- .../core/graal/GraalReflectionConfigurer.java | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/core/src/main/java/io/micronaut/core/graal/GraalReflectionConfigurer.java b/core/src/main/java/io/micronaut/core/graal/GraalReflectionConfigurer.java index eed9ffe7adf..e1f02dde4c9 100644 --- a/core/src/main/java/io/micronaut/core/graal/GraalReflectionConfigurer.java +++ b/core/src/main/java/io/micronaut/core/graal/GraalReflectionConfigurer.java @@ -23,14 +23,13 @@ import io.micronaut.core.annotation.Nullable; import io.micronaut.core.annotation.ReflectionConfig; import io.micronaut.core.annotation.TypeHint; -import io.micronaut.core.reflect.ReflectionUtils; -import io.micronaut.core.util.CollectionUtils; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.util.List; +import java.util.Optional; import java.util.Set; /** @@ -63,7 +62,7 @@ default void configure(ReflectionConfigurationContext context) { return; } context.register(t); - final Set accessType = CollectionUtils.setOf( + final Set accessType = Set.of( reflectConfig.enumValues("accessType", TypeHint.AccessType.class) ); if (accessType.contains(TypeHint.AccessType.ALL_PUBLIC_METHODS)) { @@ -146,7 +145,13 @@ default void configure(ReflectionConfigurationContext context) { for (AnnotationValue field : fields) { field.stringValue("name") - .flatMap(n -> ReflectionUtils.findField(t, n)) + .flatMap(n -> { + try { + return Optional.of(t.getDeclaredField(n)); + } catch (NoSuchFieldException e) { + return Optional.empty(); + } + }) .ifPresent(context::register); } }); From 46dbfdc54d1a73f76a7182d2bf503b51ff343ed2 Mon Sep 17 00:00:00 2001 From: micronaut-build <65172877+micronaut-build@users.noreply.github.com> Date: Tue, 3 Jan 2023 09:54:20 -0500 Subject: [PATCH 372/743] Bump micronaut-aws to 3.11.0 (#8562) --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 169e9f5298c..02bc9f8f8c5 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -67,7 +67,7 @@ managed-methvin-directory-watcher = "0.16.1" managed-micrometer = "1.9.4" managed-micronaut-acme = "3.2.0" managed-micronaut-aot = "1.1.1" -managed-micronaut-aws = "3.10.4" +managed-micronaut-aws = "3.11.0" managed-micronaut-azure = "4.0.0" managed-micronaut-cache = "3.5.0" managed-micronaut-cassandra = "5.1.1" From 9a5d8d28b2417155a31e09c6f6c9854c8b4f052a Mon Sep 17 00:00:00 2001 From: micronaut-build <65172877+micronaut-build@users.noreply.github.com> Date: Tue, 3 Jan 2023 09:54:39 -0500 Subject: [PATCH 373/743] Bump micronaut-data to 3.9.3 (#8549) --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 169e9f5298c..207aae07c54 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -73,7 +73,7 @@ managed-micronaut-cache = "3.5.0" managed-micronaut-cassandra = "5.1.1" managed-micronaut-coherence = "3.7.2" managed-micronaut-crac = "1.1.1" -managed-micronaut-data = "3.9.1" +managed-micronaut-data = "3.9.3" managed-micronaut-discovery = "3.2.0" managed-micronaut-elasticsearch = "4.3.0" managed-micronaut-email = "1.5.0" From 776e9363265b6258d7f3d12f282e746faa505854 Mon Sep 17 00:00:00 2001 From: micronaut-build <65172877+micronaut-build@users.noreply.github.com> Date: Tue, 3 Jan 2023 09:54:39 -0500 Subject: [PATCH 374/743] Bump micronaut-data to 3.9.3 (#8549) --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 02bc9f8f8c5..7d6c309f10f 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -73,7 +73,7 @@ managed-micronaut-cache = "3.5.0" managed-micronaut-cassandra = "5.1.1" managed-micronaut-coherence = "3.7.2" managed-micronaut-crac = "1.1.1" -managed-micronaut-data = "3.9.1" +managed-micronaut-data = "3.9.3" managed-micronaut-discovery = "3.2.0" managed-micronaut-elasticsearch = "4.3.0" managed-micronaut-email = "1.5.0" From 38755e9f47d186a2b38b6d300d2d21a2305f87ec Mon Sep 17 00:00:00 2001 From: Graeme Rocher Date: Tue, 3 Jan 2023 16:32:56 +0100 Subject: [PATCH 375/743] add isWithin(..) check to each property handling (#8565) --- .../context/DefaultApplicationContext.java | 39 +++++++++++++------ 1 file changed, 27 insertions(+), 12 deletions(-) diff --git a/inject/src/main/java/io/micronaut/context/DefaultApplicationContext.java b/inject/src/main/java/io/micronaut/context/DefaultApplicationContext.java index b172e23c2fe..b86a47d775d 100644 --- a/inject/src/main/java/io/micronaut/context/DefaultApplicationContext.java +++ b/inject/src/main/java/io/micronaut/context/DefaultApplicationContext.java @@ -436,16 +436,15 @@ private void transformConfigurationReaderBeanDefinition(BeanResolutionContex createAndAddDelegate(resolutionContext, candidate, transformedCandidates, subPath) ); } else { - ConfigurationPath oldPath = configurationPath; ConfigurationPath newPath = ConfigurationPath.newPath(); - resolutionContext.setConfigurationPath(configurationPath); + resolutionContext.setConfigurationPath(newPath); try { newPath.pushConfigurationReader(candidate); newPath.traverseResolvableSegments(getEnvironment(), subPath -> createAndAddDelegate(resolutionContext, candidate, transformedCandidates, subPath) ); } finally { - resolutionContext.setConfigurationPath(oldPath); + resolutionContext.setConfigurationPath(configurationPath); } } } else if (prefix.indexOf('*') == -1) { @@ -530,15 +529,31 @@ private void transformEachPropertyBeanDefinition(@NonNull BeanResolutionCont BeanDefinition candidate, Set> transformedCandidates) { try { - ConfigurationPath configurationPath = resolutionContext.getConfigurationPath(); - configurationPath.pushEachPropertyRoot(candidate); - try { - ConfigurationPath rootConfig = resolutionContext.getConfigurationPath(); - rootConfig.traverseResolvableSegments(getEnvironment(), (subPath -> - createAndAddDelegate(resolutionContext, candidate, transformedCandidates, subPath) - )); - } finally { - configurationPath.removeLast(); + final String prefix = candidate.stringValue(ConfigurationReader.class, ConfigurationReader.PREFIX).orElse(null); + if (prefix != null) { + ConfigurationPath configurationPath = resolutionContext.getConfigurationPath(); + if (configurationPath.isWithin(prefix)) { + configurationPath.pushEachPropertyRoot(candidate); + try { + ConfigurationPath rootConfig = resolutionContext.getConfigurationPath(); + rootConfig.traverseResolvableSegments(getEnvironment(), (subPath -> + createAndAddDelegate(resolutionContext, candidate, transformedCandidates, subPath) + )); + } finally { + configurationPath.removeLast(); + } + } else { + ConfigurationPath newPath = ConfigurationPath.newPath(); + resolutionContext.setConfigurationPath(newPath); + try { + newPath.pushEachPropertyRoot(candidate); + newPath.traverseResolvableSegments(getEnvironment(), subPath -> + createAndAddDelegate(resolutionContext, candidate, transformedCandidates, subPath) + ); + } finally { + resolutionContext.setConfigurationPath(configurationPath); + } + } } } catch (IllegalStateException e) { throw new DependencyInjectionException( From 9d746de58672e2c303c341cc6ba4b48b27df9253 Mon Sep 17 00:00:00 2001 From: Graeme Rocher Date: Thu, 5 Jan 2023 17:56:41 +0100 Subject: [PATCH 376/743] Pick a more specific setter for child configuration properties (#8578) Fixes #8574 --- .../ast/utils/AstBeanPropertiesUtils.java | 11 +++-- .../ConfigPropertiesParseSpec.groovy | 44 ++++++++++++++++++- 2 files changed, 51 insertions(+), 4 deletions(-) diff --git a/core-processor/src/main/java/io/micronaut/inject/ast/utils/AstBeanPropertiesUtils.java b/core-processor/src/main/java/io/micronaut/inject/ast/utils/AstBeanPropertiesUtils.java index 3895c2556fc..7d7ac9de371 100644 --- a/core-processor/src/main/java/io/micronaut/inject/ast/utils/AstBeanPropertiesUtils.java +++ b/core-processor/src/main/java/io/micronaut/inject/ast/utils/AstBeanPropertiesUtils.java @@ -109,7 +109,7 @@ public static List resolveBeanProperties(PropertyElementQuery c || configuration.isAllowSetterWithMultipleArgs() && methodElement.getParameters().length > 1)) { String propertyName = customWriterPropertyNameResolver.apply(methodElement) .orElseGet(() -> NameUtils.getPropertyNameForSetter(methodName, writePrefixes)); - processSetter(props, methodElement, propertyName, isAccessor, configuration); + processSetter(classElement, props, methodElement, propertyName, isAccessor, configuration); } } for (FieldElement fieldElement : fieldSupplier.get()) { @@ -248,7 +248,7 @@ private static void processGetter(Map props, MethodEle } } - private static void processSetter(Map props, MethodElement methodElement, String propertyName, boolean isAccessor, PropertyElementQuery configuration) { + private static void processSetter(ClassElement classElement, Map props, MethodElement methodElement, String propertyName, boolean isAccessor, PropertyElementQuery configuration) { BeanPropertyData beanPropertyData = props.computeIfAbsent(propertyName, BeanPropertyData::new); ClassElement paramType = methodElement.getParameters().length == 0 ? PrimitiveElement.BOOLEAN : methodElement.getParameters()[0].getGenericType(); ClassElement setterType = unwrapType(paramType); @@ -256,8 +256,13 @@ private static void processSetter(Map props, MethodEle if (setterType.isAssignable(unwrapType(beanPropertyData.type))) { // Override the setter because the type is higher beanPropertyData.setter = methodElement; + } else if (beanPropertyData.setter.getDeclaringType().equals(classElement)) { + // skip + return; + } else { + // override must be a subclass + beanPropertyData.setter = methodElement; } - return; } beanPropertyData.setter = methodElement; if (isAccessor) { diff --git a/inject-java/src/test/groovy/io/micronaut/inject/configproperties/ConfigPropertiesParseSpec.groovy b/inject-java/src/test/groovy/io/micronaut/inject/configproperties/ConfigPropertiesParseSpec.groovy index 5723364efdb..463df10e813 100644 --- a/inject-java/src/test/groovy/io/micronaut/inject/configproperties/ConfigPropertiesParseSpec.groovy +++ b/inject-java/src/test/groovy/io/micronaut/inject/configproperties/ConfigPropertiesParseSpec.groovy @@ -17,6 +17,47 @@ import java.time.Duration class ConfigPropertiesParseSpec extends AbstractTypeElementSpec { + @Issue("https://github.com/micronaut-projects/micronaut-core/issues/8574") + void "test configuration properties inherited from parent with multiple overloads"() { + when: + def context = buildContext(''' +package test; + +import io.micronaut.context.annotation.ConfigurationProperties; +import io.micronaut.core.convert.format.MapFormat; +import io.micronaut.core.naming.conventions.StringConvention; +import java.io.InputStream; +import java.util.*; + +@ConfigurationProperties("freemarker") +class TestConfiguration extends ParentConfiguration { + @Override + public void setSettings( + @MapFormat(keyFormat = StringConvention.UNDER_SCORE_SEPARATED_LOWER_CASE) Properties props){ + super.setSettings(props); + } + +} +class ParentConfiguration { + private Properties properties; + public void setSettings(InputStream inputStream) { + } + public void setSettings(Properties properties) { + this.properties = properties; + } + + public Properties properties() { + return properties; + } +} +''') + def bean = getBean(context, 'test.TestConfiguration') + + then: + bean != null + bean.properties() as Map == [url_escaping_charset:'UTF-8'] + } + @Issue("https://github.com/micronaut-projects/micronaut-core/issues/8480") void "test configuration properties inheritance for compiled classes - inherited props"() { when: @@ -174,7 +215,8 @@ class MyConfig { "micronaut.server.netty.parent.io-ratio": "10", "micronaut.server.netty.parent.threads": "5", "micronaut.server.netty.child.io-ratio": "15", - "micronaut.server.netty.child.threads": "55" + "micronaut.server.netty.child.threads": "55", + "freemarker.settings.urlEscapingCharset": 'UTF-8' ) } From 21bfe051b2a5c693652f9d78decec85d4563b839 Mon Sep 17 00:00:00 2001 From: Graeme Rocher Date: Thu, 5 Jan 2023 17:57:07 +0100 Subject: [PATCH 377/743] init at build time before instantiating reference (#8576) --- .../io/micronaut/core/graal/ServiceLoaderInitialization.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/src/main/java/io/micronaut/core/graal/ServiceLoaderInitialization.java b/core/src/main/java/io/micronaut/core/graal/ServiceLoaderInitialization.java index be41519263b..9580646436f 100644 --- a/core/src/main/java/io/micronaut/core/graal/ServiceLoaderInitialization.java +++ b/core/src/main/java/io/micronaut/core/graal/ServiceLoaderInitialization.java @@ -78,7 +78,7 @@ public void beforeAnalysis(BeforeAnalysisAccess access) { if (GraalReflectionConfigurer.class.isAssignableFrom(c)) { continue; } else if (BeanInfo.class.isAssignableFrom(c)) { - + RuntimeClassInitialization.initializeAtBuildTime(c); BeanInfo beanInfo; try { beanInfo = (BeanInfo) c.getDeclaredConstructor().newInstance(); @@ -105,7 +105,7 @@ public void beforeAnalysis(BeforeAnalysisAccess access) { } } } - RuntimeClassInitialization.initializeAtBuildTime(c); + RuntimeReflection.registerForReflectiveInstantiation(c); RuntimeReflection.register(c); } From 2c624a0798f358ad023d28cee14b24ca0d4ce1af Mon Sep 17 00:00:00 2001 From: Tim Yates Date: Mon, 9 Jan 2023 09:31:55 +0000 Subject: [PATCH 378/743] Move to Geb 7 for Groovy 4 support (#8575) --- ....build.internal.convention-geb-base.gradle | 83 ------------------- gradle/libs.versions.toml | 13 +-- test-suite-geb/build.gradle | 30 +++++-- .../upload/browser/UploadBrowserSpec.groovy | 1 - test-suite-geb/src/test/resources/logback.xml | 2 +- test-suite/build.gradle | 3 - 6 files changed, 30 insertions(+), 102 deletions(-) diff --git a/buildSrc/src/main/groovy/io.micronaut.build.internal.convention-geb-base.gradle b/buildSrc/src/main/groovy/io.micronaut.build.internal.convention-geb-base.gradle index 33266f10f8c..32f3910eb03 100644 --- a/buildSrc/src/main/groovy/io.micronaut.build.internal.convention-geb-base.gradle +++ b/buildSrc/src/main/groovy/io.micronaut.build.internal.convention-geb-base.gradle @@ -1,33 +1,3 @@ -import io.micronaut.build.internal.ext.MicronautCoreExtension -import io.micronaut.build.internal.ext.DefaultMicronautCoreExtension - -plugins { - id "io.micronaut.build.internal.base" - id "groovy" - id "java-library" -} - -micronautBuild { - enableBom = false - enableProcessing = false -} - -group = projectGroupId - -configurations { - all { - resolutionStrategy.eachDependency { DependencyResolveDetails details -> - if (details.requested.group == 'io.micronaut.test') { - details.useVersion libs.versions.geb.micronaut.test.get() - details.because "Geb doesn't work with Groovy 4" - } - } - } -} -def micronautBuild = (ExtensionAware) project.extensions.getByName("micronautBuild") -def micronautCore = micronautBuild.extensions.create(MicronautCoreExtension, "core", DefaultMicronautCoreExtension, extensions.findByType(VersionCatalogsExtension)) -micronautCore.documented.convention(true) - if (System.getProperty('geb.env')) { apply plugin:"com.energizedwork.webdriver-binaries" @@ -37,68 +7,15 @@ if (System.getProperty('geb.env')) { } } -tasks.withType(Test).configureEach { - useJUnitPlatform() - jvmArgs '-Xmx2048m' - systemProperty "micronaut.cloud.platform", "OTHER" - if (JavaVersion.current().isCompatibleWith(JavaVersion.VERSION_15)) { - jvmArgs "--enable-preview" - } -} - tasks.named("test") { systemProperty "geb.env", System.getProperty('geb.env') systemProperty "webdriver.chrome.driver", System.getProperty('webdriver.chrome.driver') systemProperty "webdriver.gecko.driver", System.getProperty('webdriver.gecko.driver') } -tasks.withType(JavaCompile).configureEach { - options.fork = true - options.compilerArgs.add("-Amicronaut.processing.group=$project.group") - options.compilerArgs.add("-Amicronaut.processing.module=micronaut-$project.name") - options.compilerArgs.add("-Amicronaut.processing.omit.confprop.injectpoints=true") - options.forkOptions.memoryMaximumSize = "2g" -} - -tasks.withType(GroovyCompile).configureEach { - options.fork = true - options.compilerArgs.add("-Amicronaut.processing.group=$project.group") - options.compilerArgs.add("-Amicronaut.processing.module=micronaut-$project.name") - groovyOptions.forkOptions.memoryMaximumSize = "2g" -} - -// This is for reproducible builds -tasks.withType(Jar).configureEach { - reproducibleFileOrder = true - preserveFileTimestamps = false -} - dependencies { - annotationProcessor libs.bundles.asm - annotationProcessor(libs.micronaut.docs.map { - if (micronautCore.documented.get()) { - it - } else { - null - } - }) { - transitive = false - } - - api libs.managed.slf4j.api - compileOnly libs.caffeine - compileOnly libs.bundles.asm - - testAnnotationProcessor project(":http-validation") - testAnnotationProcessor libs.bundles.asm - - testImplementation libs.caffeine - testImplementation libs.bundles.asm - // Geb currently requires Groovy 3, and Spock for Groovy 3 testImplementation libs.geb.spock - testImplementation libs.spock.for.geb - testImplementation libs.geb.groovy.test testImplementation libs.selenium.driver.htmlunit testImplementation libs.selenium.remote.driver testImplementation libs.selenium.api diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 0db67c9d35c..5dfa82e03d2 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -6,10 +6,7 @@ blaze = "1.6.8" caffeine = "2.9.3" compile-testing = "0.19" -geb = "3.4.1" -geb-groovy = "3.0.13" -geb-micronaut-test = "3.7.0" -geb-spock = "2.2-groovy-3.0" +geb = "7.0" gorm = "7.3.2" # be sure to update graal version in gradle.properties as well # Intentionally pin to 22.0.0.2 see https://github.com/micronaut-projects/micronaut-kafka/pull/564 and https://github.com/micronaut-projects/micronaut-core/pull/7663 @@ -18,7 +15,7 @@ h2 = "2.1.210" hibernate = "5.5.9.Final" hibernate-validator = "6.1.6.Final" htmlSanityCheck = "1.1.6" -htmlunit = "2.64.0" +htmlunit = "2.68.0" httpcomponents-client = "4.5.14" jakarta-inject-api = "2.0.1" jakarta-inject-tck = "2.0.1" @@ -50,7 +47,7 @@ micronaut-serde = "2.0.0-SNAPSHOT" micronaut-tracing = "4.4.0" micrometer = "1.10.2" neo4j-java-driver = "1.4.5" -selenium = "3.141.59" +selenium = "4.7.2" smallrye = "5.5.0" spock = "2.2-groovy-4.0" spotbugs = "4.7.1" @@ -155,8 +152,6 @@ caffeine = { module = "com.github.ben-manes.caffeine:caffeine", version.ref = "c compile-testing = { module = "com.google.testing.compile:compile-testing", version.ref = "compile-testing" } geb-spock = { module = "org.gebish:geb-spock", version.ref = "geb" } -spock-for-geb = { module = "org.spockframework:spock-core", version.ref = "geb-spock" } -geb-groovy-test = { module = "org.codehaus.groovy:groovy-test", version.ref = "geb-groovy" } gorm = { module = "org.grails:grails-datastore-core", version.ref = "gorm" } graal = { module = "org.graalvm.nativeimage:svm", version.ref = "graal-svm" } groovy-test-junit5 = { module = "org.apache.groovy:groovy-test-junit5", version.ref = "managed-groovy" } @@ -241,7 +236,7 @@ selenium-api = { module = "org.seleniumhq.selenium:selenium-api", version.ref = selenium-support = { module = "org.seleniumhq.selenium:selenium-support", version.ref = "selenium" } selenium-driver-chrome = { module = "org.seleniumhq.selenium:selenium-chrome-driver", version.ref = "selenium" } selenium-driver-firefox = { module = "org.seleniumhq.selenium:selenium-firefox-driver", version.ref = "selenium" } -selenium-driver-htmlunit = { module = "org.seleniumhq.selenium:htmlunit-driver", version.ref = "htmlunit" } +selenium-driver-htmlunit = { module = "org.seleniumhq.selenium:htmlunit-driver", version.ref = "selenium" } smallrye = { module = "io.smallrye:smallrye-fault-tolerance", version.ref = "smallrye" } spock = { module = "org.spockframework:spock-core", version.ref = "spock" } diff --git a/test-suite-geb/build.gradle b/test-suite-geb/build.gradle index 0da06cb518d..4af4456fd7c 100644 --- a/test-suite-geb/build.gradle +++ b/test-suite-geb/build.gradle @@ -1,5 +1,19 @@ +import org.apache.tools.ant.taskdefs.condition.Os + +buildscript { + repositories { + maven { url "https://plugins.gradle.org/m2/" } + } + dependencies { + classpath "gradle.plugin.com.energizedwork.webdriver-binaries:webdriver-binaries-gradle-plugin:$webdriverBinariesVersion" + } +} + plugins { - id "io.micronaut.build.internal.convention-geb-base" + id "io.micronaut.build.internal.convention-test-library" + id 'io.micronaut.build.internal.functional-test' + id 'java-test-fixtures' + id 'io.micronaut.build.internal.convention-geb-base' } micronautBuild { @@ -9,14 +23,20 @@ micronautBuild { } } -dependencies { - testImplementation(project(":inject-groovy")) { - exclude module: 'groovy' +repositories { + mavenCentral() + maven { + url "https://s01.oss.sonatype.org/content/repositories/snapshots/" + mavenContent { + snapshotsOnly() + } } +} +dependencies { testImplementation project(':http') testImplementation project(':http-server-netty') + testImplementation project(":jackson-databind") testRuntimeOnly libs.managed.logback.classic - testImplementation project(":jackson-databind") } diff --git a/test-suite-geb/src/test/groovy/io/micronaut/upload/browser/UploadBrowserSpec.groovy b/test-suite-geb/src/test/groovy/io/micronaut/upload/browser/UploadBrowserSpec.groovy index 23163f7a040..f4615005ab8 100644 --- a/test-suite-geb/src/test/groovy/io/micronaut/upload/browser/UploadBrowserSpec.groovy +++ b/test-suite-geb/src/test/groovy/io/micronaut/upload/browser/UploadBrowserSpec.groovy @@ -5,7 +5,6 @@ import io.micronaut.context.ApplicationContext import io.micronaut.context.env.Environment import io.micronaut.runtime.server.EmbeddedServer import spock.lang.AutoCleanup -import spock.lang.Requires import spock.lang.Shared class UploadBrowserSpec extends GebSpec { diff --git a/test-suite-geb/src/test/resources/logback.xml b/test-suite-geb/src/test/resources/logback.xml index 44b79c40d49..afaebf8e17d 100644 --- a/test-suite-geb/src/test/resources/logback.xml +++ b/test-suite-geb/src/test/resources/logback.xml @@ -11,4 +11,4 @@ - + \ No newline at end of file diff --git a/test-suite/build.gradle b/test-suite/build.gradle index 686dd97ea44..177a22e58a7 100644 --- a/test-suite/build.gradle +++ b/test-suite/build.gradle @@ -4,9 +4,6 @@ buildscript { repositories { maven { url "https://plugins.gradle.org/m2/" } } - dependencies { - classpath "gradle.plugin.com.energizedwork.webdriver-binaries:webdriver-binaries-gradle-plugin:$webdriverBinariesVersion" - } } plugins { From 20eecb6fccf1176330692858ab9f88625638b5cf Mon Sep 17 00:00:00 2001 From: Tim Yates Date: Mon, 9 Jan 2023 16:49:09 +0000 Subject: [PATCH 379/743] Set kotlinOptions.jvmTarget to 17 for 4.0.0 (#8579) --- http/build.gradle | 2 +- .../guide/languageSupport/kotlin/kotlinretainparamnames.adoc | 4 ++-- test-suite-kotlin/build.gradle | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/http/build.gradle b/http/build.gradle index 20149d14dff..82a85694853 100644 --- a/http/build.gradle +++ b/http/build.gradle @@ -23,7 +23,7 @@ dependencies { } tasks.named("compileKotlin") { - kotlinOptions.jvmTarget = "1.8" + kotlinOptions.jvmTarget = "17" } //compileJava.options.fork = true diff --git a/src/main/docs/guide/languageSupport/kotlin/kotlinretainparamnames.adoc b/src/main/docs/guide/languageSupport/kotlin/kotlinretainparamnames.adoc index 85bb4fa45d5..978d7b42438 100644 --- a/src/main/docs/guide/languageSupport/kotlin/kotlinretainparamnames.adoc +++ b/src/main/docs/guide/languageSupport/kotlin/kotlinretainparamnames.adoc @@ -7,7 +7,7 @@ To enable retention of parameter name data with Kotlin, set the `javaParameters` ---- compileTestKotlin { kotlinOptions { - jvmTarget = '1.8' + jvmTarget = '17' javaParameters = true } } @@ -29,7 +29,7 @@ Or if using Maven configure the Micronaut Maven Plugin accordingly: kotlin-maven-plugin org.jetbrains.kotlin - + true diff --git a/test-suite-kotlin/build.gradle b/test-suite-kotlin/build.gradle index 832ed67d879..86b1e35593e 100644 --- a/test-suite-kotlin/build.gradle +++ b/test-suite-kotlin/build.gradle @@ -88,7 +88,7 @@ configurations.testRuntimeClasspath { } tasks.named("compileTestKotlin") { - kotlinOptions.jvmTarget = "1.8" + kotlinOptions.jvmTarget = "17" } tasks.named("test") { From 2a84c8b0837e0e9d1bef410668c9d95d6e9ec049 Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Tue, 10 Jan 2023 07:29:37 +0100 Subject: [PATCH 380/743] build: enable binary compatiblity (#8571) --- http-server-tck/build.gradle.kts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/http-server-tck/build.gradle.kts b/http-server-tck/build.gradle.kts index f8a0ff7a35b..c76605bd0a0 100644 --- a/http-server-tck/build.gradle.kts +++ b/http-server-tck/build.gradle.kts @@ -21,8 +21,3 @@ java { sourceCompatibility = JavaVersion.toVersion("1.8") targetCompatibility = JavaVersion.toVersion("1.8") } -micronautBuild { - binaryCompatibility { - enabled.set(false) - } -} From e6505fd0c37001354efb8c31195c0743f9947329 Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Tue, 10 Jan 2023 10:07:05 +0100 Subject: [PATCH 381/743] test: pass request parameter to chain.proceed (#8587) Test aws extremely flaky. No idea how this ever passed. https://ge.micronaut.io/scans/tests?tests.container=io.micronaut.http.client.ResponseAndStreamSpec --- .../io/micronaut/http/client/ResponseAndStreamSpec.groovy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/http-client/src/test/groovy/io/micronaut/http/client/ResponseAndStreamSpec.groovy b/http-client/src/test/groovy/io/micronaut/http/client/ResponseAndStreamSpec.groovy index abaf0782f52..c09671f6fe5 100644 --- a/http-client/src/test/groovy/io/micronaut/http/client/ResponseAndStreamSpec.groovy +++ b/http-client/src/test/groovy/io/micronaut/http/client/ResponseAndStreamSpec.groovy @@ -57,7 +57,7 @@ class ResponseAndStreamSpec extends Specification { @Override Publisher> doFilter( HttpRequest request, ServerFilterChain chain) { - return Flux.from(chain.proceed()).map { MutableHttpResponse response -> + return Flux.from(chain.proceed(request)).map { MutableHttpResponse response -> return response.body(Flux.fromIterable([ "chunk1", "chunk2", From d0d0b4cd068818b7274ee255ef22158bd29434cb Mon Sep 17 00:00:00 2001 From: Denis Stepanov Date: Tue, 10 Jan 2023 11:58:34 +0100 Subject: [PATCH 382/743] Remove unneeded PROXY_TARGET_QUALIFIER (#8589) --- .../micronaut/context/DefaultBeanContext.java | 31 ++++--------------- .../io/micronaut/inject/BeanDefinition.java | 2 +- .../micronaut/inject/ProxyBeanDefinition.java | 6 +++- 3 files changed, 12 insertions(+), 27 deletions(-) diff --git a/inject/src/main/java/io/micronaut/context/DefaultBeanContext.java b/inject/src/main/java/io/micronaut/context/DefaultBeanContext.java index d9739ee0778..29b8f433b42 100644 --- a/inject/src/main/java/io/micronaut/context/DefaultBeanContext.java +++ b/inject/src/main/java/io/micronaut/context/DefaultBeanContext.java @@ -160,19 +160,7 @@ public class DefaultBeanContext implements InitializableBeanContext { protected static final Logger LOG = LoggerFactory.getLogger(DefaultBeanContext.class); protected static final Logger LOG_LIFECYCLE = LoggerFactory.getLogger(DefaultBeanContext.class.getPackage().getName() + ".lifecycle"); - @SuppressWarnings("rawtypes") - private static final Qualifier PROXY_TARGET_QUALIFIER = new Qualifier<>() { - @Override - public > Stream reduce(Class beanType, Stream candidates) { - return candidates.filter(bt -> { - if (bt instanceof BeanDefinitionDelegate delegate) { - return !(delegate.getDelegate() instanceof ProxyBeanDefinition); - } else { - return !(bt instanceof ProxyBeanDefinition); - } - }); - } - }; + private static final String SCOPED_PROXY_ANN = "io.micronaut.runtime.context.scope.ScopedProxy"; private static final String INTRODUCTION_TYPE = "io.micronaut.aop.Introduction"; private static final String ADAPTER_TYPE = "io.micronaut.aop.Adapter"; @@ -1506,16 +1494,14 @@ public Collection getBeansOfType(@Nullable BeanResolutionContext resoluti @NonNull public T getProxyTargetBean(@NonNull Class beanType, @Nullable Qualifier qualifier) { ArgumentUtils.requireNonNull("beanType", beanType); - return getProxyTargetBean(Argument.of(beanType), qualifier); + return getProxyTargetBean(null, Argument.of(beanType), qualifier); } @NonNull @Override public T getProxyTargetBean(@NonNull Argument beanType, @Nullable Qualifier qualifier) { - BeanDefinition definition = getProxyTargetBeanDefinition(beanType, qualifier); - @SuppressWarnings("unchecked") - Qualifier proxyQualifier = qualifier != null ? Qualifiers.byQualifiers(qualifier, PROXY_TARGET_QUALIFIER) : PROXY_TARGET_QUALIFIER; - return resolveBeanRegistration(null, definition, beanType, proxyQualifier).bean; + ArgumentUtils.requireNonNull("beanType", beanType); + return getProxyTargetBean(null, beanType, qualifier); } /** @@ -1534,12 +1520,7 @@ public T getProxyTargetBean(@Nullable BeanResolutionContext resolutionContex @NonNull Argument beanType, @Nullable Qualifier qualifier) { BeanDefinition definition = getProxyTargetBeanDefinition(beanType, qualifier); - @SuppressWarnings("unchecked") - Qualifier proxyQualifier = qualifier != null ? Qualifiers.byQualifiers(qualifier, PROXY_TARGET_QUALIFIER) : PROXY_TARGET_QUALIFIER; - return resolveBeanRegistration( - resolutionContext, - definition, beanType, proxyQualifier - ).bean; + return resolveBeanRegistration(resolutionContext, definition, beanType, qualifier).bean; } @NonNull @@ -3029,7 +3010,7 @@ private BeanRegistration resolveBeanRegistration(@Nullable BeanResolution final boolean isProxy = definition.isProxy(); - if (isProxy && isScopedProxyDefinition && (qualifier == null || !qualifier.contains(PROXY_TARGET_QUALIFIER))) { + if (isProxy && isScopedProxyDefinition) { // AOP proxy Qualifier q = qualifier; if (q == null) { diff --git a/inject/src/main/java/io/micronaut/inject/BeanDefinition.java b/inject/src/main/java/io/micronaut/inject/BeanDefinition.java index d6a00284d57..7d242663649 100644 --- a/inject/src/main/java/io/micronaut/inject/BeanDefinition.java +++ b/inject/src/main/java/io/micronaut/inject/BeanDefinition.java @@ -286,7 +286,7 @@ default Argument asArgument() { * @return True if it represents a proxy */ default boolean isProxy() { - return this instanceof ProxyBeanDefinition; + return false; } /** diff --git a/inject/src/main/java/io/micronaut/inject/ProxyBeanDefinition.java b/inject/src/main/java/io/micronaut/inject/ProxyBeanDefinition.java index 048f1eac552..f8e280246d7 100644 --- a/inject/src/main/java/io/micronaut/inject/ProxyBeanDefinition.java +++ b/inject/src/main/java/io/micronaut/inject/ProxyBeanDefinition.java @@ -29,9 +29,13 @@ public interface ProxyBeanDefinition extends BeanDefinition { */ Class> getTargetDefinitionType(); - /** * @return The target type */ Class getTargetType(); + + @Override + default boolean isProxy() { + return true; + } } From cd33294c67962e744a650472c8069d67df7da973 Mon Sep 17 00:00:00 2001 From: Tim Yates Date: Tue, 10 Jan 2023 11:03:31 +0000 Subject: [PATCH 383/743] feat: Check for equality in the body for server TCK tests (#8583) --- .../http/server/tck/AssertionUtils.java | 38 +++++++++++++++++-- .../server/tck/HttpResponseAssertion.java | 26 ++++++++++++- .../http/server/tck/tests/BodyTest.java | 18 +++++++++ .../server/tck/tests/ErrorHandlerTest.java | 2 +- .../server/tck/tests/FilterErrorTest.java | 5 ++- 5 files changed, 81 insertions(+), 8 deletions(-) diff --git a/http-server-tck/src/main/java/io/micronaut/http/server/tck/AssertionUtils.java b/http-server-tck/src/main/java/io/micronaut/http/server/tck/AssertionUtils.java index 446014320e0..862d9d6e3f6 100644 --- a/http-server-tck/src/main/java/io/micronaut/http/server/tck/AssertionUtils.java +++ b/http-server-tck/src/main/java/io/micronaut/http/server/tck/AssertionUtils.java @@ -57,7 +57,7 @@ public static void assertThrows(@NonNull ServerUnderTest server, HttpResponse response = thrown.getResponse(); assertEquals(assertion.getHttpStatus(), response.getStatus()); assertHeaders(response, assertion.getHeaders()); - assertBody(response, assertion.getBody()); + assertBody(response, assertion.getBody(), assertion.getContains()); assertion.getResponseConsumer().ifPresent(httpResponseConsumer -> httpResponseConsumer.accept(response)); } @@ -73,6 +73,18 @@ public static void assertThrows(@NonNull ServerUnderTest server, .build()); } + public static void assertThrowsAndContains(@NonNull ServerUnderTest server, + @NonNull HttpRequest request, + @NonNull HttpStatus expectedStatus, + @Nullable String expectedBody, + @Nullable Map expectedHeaders) { + assertThrows(server, request, HttpResponseAssertion.builder() + .status(expectedStatus) + .containsBody(expectedBody) + .headers(expectedHeaders) + .build()); + } + public static void assertDoesNotThrow(@NonNull ServerUnderTest server, @NonNull HttpRequest request, @NonNull HttpStatus expectedStatus, @@ -85,6 +97,18 @@ public static void assertDoesNotThrow(@NonNull ServerUnderTest server, .build()); } + public static void assertDoesNotThrowAndContains(@NonNull ServerUnderTest server, + @NonNull HttpRequest request, + @NonNull HttpStatus expectedStatus, + @Nullable String expectedBody, + @Nullable Map expectedHeaders) { + assertDoesNotThrow(server, request, HttpResponseAssertion.builder() + .status(expectedStatus) + .containsBody(expectedBody) + .headers(expectedHeaders) + .build()); + } + public static void assertDoesNotThrow(@NonNull ServerUnderTest server, @NonNull HttpRequest request, @NonNull HttpResponseAssertion assertion) { @@ -94,15 +118,21 @@ public static void assertDoesNotThrow(@NonNull ServerUnderTest server, HttpResponse response = Assertions.assertDoesNotThrow(executable); assertEquals(assertion.getHttpStatus(), response.getStatus()); assertHeaders(response, assertion.getHeaders()); - assertBody(response, assertion.getBody()); + assertBody(response, assertion.getBody(), assertion.getContains()); assertion.getResponseConsumer().ifPresent(httpResponseConsumer -> httpResponseConsumer.accept(response)); } - private static void assertBody(@NonNull HttpResponse response, @Nullable String expectedBody) { + private static void assertBody(@NonNull HttpResponse response, @Nullable String expectedBody, boolean contains) { if (expectedBody != null) { Optional bodyOptional = response.getBody(String.class); assertTrue(bodyOptional.isPresent()); - bodyOptional.ifPresent(body -> assertTrue(body.contains(expectedBody))); + bodyOptional.ifPresent(body -> { + if (contains) { + assertTrue(body.contains(expectedBody)); + } else { + assertEquals(expectedBody, body); + } + }); } } diff --git a/http-server-tck/src/main/java/io/micronaut/http/server/tck/HttpResponseAssertion.java b/http-server-tck/src/main/java/io/micronaut/http/server/tck/HttpResponseAssertion.java index e4af0bc7bc8..b8194dcaa5d 100644 --- a/http-server-tck/src/main/java/io/micronaut/http/server/tck/HttpResponseAssertion.java +++ b/http-server-tck/src/main/java/io/micronaut/http/server/tck/HttpResponseAssertion.java @@ -38,6 +38,7 @@ public final class HttpResponseAssertion { private final HttpStatus httpStatus; private final Map headers; private final String body; + private final boolean contains; @Nullable private final Consumer> responseConsumer; @@ -45,10 +46,12 @@ public final class HttpResponseAssertion { private HttpResponseAssertion(HttpStatus httpStatus, Map headers, String body, + boolean contains, @Nullable Consumer> responseConsumer) { this.httpStatus = httpStatus; this.headers = headers; this.body = body; + this.contains = contains; this.responseConsumer = responseConsumer; } @@ -89,6 +92,13 @@ public static HttpResponseAssertion.Builder builder() { return new HttpResponseAssertion.Builder(); } + /** + * @return true if the body is expected to contain the expected body. + */ + public boolean getContains() { + return contains; + } + /** * HTTP Response Assertion Builder. */ @@ -96,6 +106,7 @@ public static class Builder { private HttpStatus httpStatus; private Map headers; private String body; + private boolean contains = false; private Consumer> responseConsumer; @@ -134,6 +145,7 @@ public Builder header(String headerName, String headerValue) { } /** + * Set the expected contents of a body. * * @param body Response Body * @return HTTP Response Assertion Builder @@ -143,6 +155,18 @@ public Builder body(String body) { return this; } + /** + * Set the expected partial contents of a body. + * + * @param body Response Body + * @return HTTP Response Assertion Builder + */ + public Builder containsBody(String body) { + this.body = body; + this.contains = true; + return this; + } + /** * * @param httpStatus Response's HTTP Status @@ -158,7 +182,7 @@ public Builder status(HttpStatus httpStatus) { * @return HTTP Response Assertion */ public HttpResponseAssertion build() { - return new HttpResponseAssertion(Objects.requireNonNull(httpStatus), headers, body, responseConsumer); + return new HttpResponseAssertion(Objects.requireNonNull(httpStatus), headers, body, contains, responseConsumer); } } } diff --git a/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/BodyTest.java b/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/BodyTest.java index 27c28cde8cf..ee445565416 100644 --- a/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/BodyTest.java +++ b/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/BodyTest.java @@ -88,6 +88,18 @@ void testCustomBodyPOJOReactiveTypes() throws IOException { .build())); } + @Test + void testCustomListBodyPOJOReactiveTypes() throws IOException { + asserts(SPEC_NAME, + HttpRequest.POST("/response-body/pojo-flux", "[{\"x\":10,\"y\":20},{\"x\":30,\"y\":40}]") + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON), + (server, request) -> AssertionUtils.assertDoesNotThrow(server, request, + HttpResponseAssertion.builder() + .status(HttpStatus.CREATED) + .body("[{\"x\":10,\"y\":20},{\"x\":30,\"y\":40}]") + .build())); + } + @Controller("/response-body") @Requires(property = "spec.name", value = SPEC_NAME) static class BodyController { @@ -111,6 +123,12 @@ Publisher post(@Body Publisher data) { return data; } + @Post(uri = "/pojo-flux") + @Status(HttpStatus.CREATED) + Publisher postMany(@Body Publisher data) { + return data; + } + @Post(uri = "/bytes", consumes = MediaType.TEXT_PLAIN) @Status(HttpStatus.CREATED) String postBytes(@Body byte[] bytes) { diff --git a/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/ErrorHandlerTest.java b/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/ErrorHandlerTest.java index 05d55dded1c..80bfaf8a11a 100644 --- a/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/ErrorHandlerTest.java +++ b/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/ErrorHandlerTest.java @@ -101,7 +101,7 @@ void testCustomGlobalExceptionHandlersForPOSTWithBody() throws IOException { ObjectMapper objectMapper = server.getApplicationContext().getBean(ObjectMapper.class); HttpRequest request = HttpRequest.POST("/json/errors/global", objectMapper.writeValueAsString(new RequestObject(101))) .header(HttpHeaders.CONTENT_TYPE, io.micronaut.http.MediaType.APPLICATION_JSON); - AssertionUtils.assertDoesNotThrow(server, request, + AssertionUtils.assertDoesNotThrowAndContains(server, request, HttpStatus.OK, "{\"message\":\"Error: bad things when post and body in request\",\"", Collections.singletonMap(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON)); diff --git a/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/FilterErrorTest.java b/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/FilterErrorTest.java index a4ba93082eb..5ad75738887 100644 --- a/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/FilterErrorTest.java +++ b/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/FilterErrorTest.java @@ -69,7 +69,7 @@ void testFilterThrowingExceptionHandledByExceptionHandlerThrowingException() thr AssertionUtils.assertThrows(server, request, HttpResponseAssertion.builder() .status(HttpStatus.INTERNAL_SERVER_ERROR) - .body("from exception handler") + .containsBody("from exception handler") .build()); ExceptionException filter = server.getApplicationContext().getBean(ExceptionException.class); assertEquals(1, filter.executedCount.get()); @@ -116,7 +116,8 @@ void testErrorsEmittedFromSecondFilterInteractingWithExceptionHandlers() throws AssertionUtils.assertThrows(server, request, HttpResponseAssertion.builder() .status(HttpStatus.BAD_REQUEST) - .body("from NEXT filter exception handle").build()); + .containsBody("from NEXT filter exception handle") + .build()); First first = server.getApplicationContext().getBean(First.class); Next next = server.getApplicationContext().getBean(Next.class); From 0c676003091fa96a7990f642e58fc0b75996ea6c Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Tue, 10 Jan 2023 12:04:14 +0100 Subject: [PATCH 384/743] Revert "feat: Check for equality in the body for server TCK tests (#8583)" This reverts commit cd33294c67962e744a650472c8069d67df7da973. --- .../http/server/tck/AssertionUtils.java | 38 ++----------------- .../server/tck/HttpResponseAssertion.java | 26 +------------ .../http/server/tck/tests/BodyTest.java | 18 --------- .../server/tck/tests/ErrorHandlerTest.java | 2 +- .../server/tck/tests/FilterErrorTest.java | 5 +-- 5 files changed, 8 insertions(+), 81 deletions(-) diff --git a/http-server-tck/src/main/java/io/micronaut/http/server/tck/AssertionUtils.java b/http-server-tck/src/main/java/io/micronaut/http/server/tck/AssertionUtils.java index 862d9d6e3f6..446014320e0 100644 --- a/http-server-tck/src/main/java/io/micronaut/http/server/tck/AssertionUtils.java +++ b/http-server-tck/src/main/java/io/micronaut/http/server/tck/AssertionUtils.java @@ -57,7 +57,7 @@ public static void assertThrows(@NonNull ServerUnderTest server, HttpResponse response = thrown.getResponse(); assertEquals(assertion.getHttpStatus(), response.getStatus()); assertHeaders(response, assertion.getHeaders()); - assertBody(response, assertion.getBody(), assertion.getContains()); + assertBody(response, assertion.getBody()); assertion.getResponseConsumer().ifPresent(httpResponseConsumer -> httpResponseConsumer.accept(response)); } @@ -73,18 +73,6 @@ public static void assertThrows(@NonNull ServerUnderTest server, .build()); } - public static void assertThrowsAndContains(@NonNull ServerUnderTest server, - @NonNull HttpRequest request, - @NonNull HttpStatus expectedStatus, - @Nullable String expectedBody, - @Nullable Map expectedHeaders) { - assertThrows(server, request, HttpResponseAssertion.builder() - .status(expectedStatus) - .containsBody(expectedBody) - .headers(expectedHeaders) - .build()); - } - public static void assertDoesNotThrow(@NonNull ServerUnderTest server, @NonNull HttpRequest request, @NonNull HttpStatus expectedStatus, @@ -97,18 +85,6 @@ public static void assertDoesNotThrow(@NonNull ServerUnderTest server, .build()); } - public static void assertDoesNotThrowAndContains(@NonNull ServerUnderTest server, - @NonNull HttpRequest request, - @NonNull HttpStatus expectedStatus, - @Nullable String expectedBody, - @Nullable Map expectedHeaders) { - assertDoesNotThrow(server, request, HttpResponseAssertion.builder() - .status(expectedStatus) - .containsBody(expectedBody) - .headers(expectedHeaders) - .build()); - } - public static void assertDoesNotThrow(@NonNull ServerUnderTest server, @NonNull HttpRequest request, @NonNull HttpResponseAssertion assertion) { @@ -118,21 +94,15 @@ public static void assertDoesNotThrow(@NonNull ServerUnderTest server, HttpResponse response = Assertions.assertDoesNotThrow(executable); assertEquals(assertion.getHttpStatus(), response.getStatus()); assertHeaders(response, assertion.getHeaders()); - assertBody(response, assertion.getBody(), assertion.getContains()); + assertBody(response, assertion.getBody()); assertion.getResponseConsumer().ifPresent(httpResponseConsumer -> httpResponseConsumer.accept(response)); } - private static void assertBody(@NonNull HttpResponse response, @Nullable String expectedBody, boolean contains) { + private static void assertBody(@NonNull HttpResponse response, @Nullable String expectedBody) { if (expectedBody != null) { Optional bodyOptional = response.getBody(String.class); assertTrue(bodyOptional.isPresent()); - bodyOptional.ifPresent(body -> { - if (contains) { - assertTrue(body.contains(expectedBody)); - } else { - assertEquals(expectedBody, body); - } - }); + bodyOptional.ifPresent(body -> assertTrue(body.contains(expectedBody))); } } diff --git a/http-server-tck/src/main/java/io/micronaut/http/server/tck/HttpResponseAssertion.java b/http-server-tck/src/main/java/io/micronaut/http/server/tck/HttpResponseAssertion.java index b8194dcaa5d..e4af0bc7bc8 100644 --- a/http-server-tck/src/main/java/io/micronaut/http/server/tck/HttpResponseAssertion.java +++ b/http-server-tck/src/main/java/io/micronaut/http/server/tck/HttpResponseAssertion.java @@ -38,7 +38,6 @@ public final class HttpResponseAssertion { private final HttpStatus httpStatus; private final Map headers; private final String body; - private final boolean contains; @Nullable private final Consumer> responseConsumer; @@ -46,12 +45,10 @@ public final class HttpResponseAssertion { private HttpResponseAssertion(HttpStatus httpStatus, Map headers, String body, - boolean contains, @Nullable Consumer> responseConsumer) { this.httpStatus = httpStatus; this.headers = headers; this.body = body; - this.contains = contains; this.responseConsumer = responseConsumer; } @@ -92,13 +89,6 @@ public static HttpResponseAssertion.Builder builder() { return new HttpResponseAssertion.Builder(); } - /** - * @return true if the body is expected to contain the expected body. - */ - public boolean getContains() { - return contains; - } - /** * HTTP Response Assertion Builder. */ @@ -106,7 +96,6 @@ public static class Builder { private HttpStatus httpStatus; private Map headers; private String body; - private boolean contains = false; private Consumer> responseConsumer; @@ -145,7 +134,6 @@ public Builder header(String headerName, String headerValue) { } /** - * Set the expected contents of a body. * * @param body Response Body * @return HTTP Response Assertion Builder @@ -155,18 +143,6 @@ public Builder body(String body) { return this; } - /** - * Set the expected partial contents of a body. - * - * @param body Response Body - * @return HTTP Response Assertion Builder - */ - public Builder containsBody(String body) { - this.body = body; - this.contains = true; - return this; - } - /** * * @param httpStatus Response's HTTP Status @@ -182,7 +158,7 @@ public Builder status(HttpStatus httpStatus) { * @return HTTP Response Assertion */ public HttpResponseAssertion build() { - return new HttpResponseAssertion(Objects.requireNonNull(httpStatus), headers, body, contains, responseConsumer); + return new HttpResponseAssertion(Objects.requireNonNull(httpStatus), headers, body, responseConsumer); } } } diff --git a/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/BodyTest.java b/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/BodyTest.java index ee445565416..27c28cde8cf 100644 --- a/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/BodyTest.java +++ b/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/BodyTest.java @@ -88,18 +88,6 @@ void testCustomBodyPOJOReactiveTypes() throws IOException { .build())); } - @Test - void testCustomListBodyPOJOReactiveTypes() throws IOException { - asserts(SPEC_NAME, - HttpRequest.POST("/response-body/pojo-flux", "[{\"x\":10,\"y\":20},{\"x\":30,\"y\":40}]") - .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON), - (server, request) -> AssertionUtils.assertDoesNotThrow(server, request, - HttpResponseAssertion.builder() - .status(HttpStatus.CREATED) - .body("[{\"x\":10,\"y\":20},{\"x\":30,\"y\":40}]") - .build())); - } - @Controller("/response-body") @Requires(property = "spec.name", value = SPEC_NAME) static class BodyController { @@ -123,12 +111,6 @@ Publisher post(@Body Publisher data) { return data; } - @Post(uri = "/pojo-flux") - @Status(HttpStatus.CREATED) - Publisher postMany(@Body Publisher data) { - return data; - } - @Post(uri = "/bytes", consumes = MediaType.TEXT_PLAIN) @Status(HttpStatus.CREATED) String postBytes(@Body byte[] bytes) { diff --git a/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/ErrorHandlerTest.java b/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/ErrorHandlerTest.java index 80bfaf8a11a..05d55dded1c 100644 --- a/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/ErrorHandlerTest.java +++ b/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/ErrorHandlerTest.java @@ -101,7 +101,7 @@ void testCustomGlobalExceptionHandlersForPOSTWithBody() throws IOException { ObjectMapper objectMapper = server.getApplicationContext().getBean(ObjectMapper.class); HttpRequest request = HttpRequest.POST("/json/errors/global", objectMapper.writeValueAsString(new RequestObject(101))) .header(HttpHeaders.CONTENT_TYPE, io.micronaut.http.MediaType.APPLICATION_JSON); - AssertionUtils.assertDoesNotThrowAndContains(server, request, + AssertionUtils.assertDoesNotThrow(server, request, HttpStatus.OK, "{\"message\":\"Error: bad things when post and body in request\",\"", Collections.singletonMap(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON)); diff --git a/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/FilterErrorTest.java b/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/FilterErrorTest.java index 5ad75738887..a4ba93082eb 100644 --- a/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/FilterErrorTest.java +++ b/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/FilterErrorTest.java @@ -69,7 +69,7 @@ void testFilterThrowingExceptionHandledByExceptionHandlerThrowingException() thr AssertionUtils.assertThrows(server, request, HttpResponseAssertion.builder() .status(HttpStatus.INTERNAL_SERVER_ERROR) - .containsBody("from exception handler") + .body("from exception handler") .build()); ExceptionException filter = server.getApplicationContext().getBean(ExceptionException.class); assertEquals(1, filter.executedCount.get()); @@ -116,8 +116,7 @@ void testErrorsEmittedFromSecondFilterInteractingWithExceptionHandlers() throws AssertionUtils.assertThrows(server, request, HttpResponseAssertion.builder() .status(HttpStatus.BAD_REQUEST) - .containsBody("from NEXT filter exception handle") - .build()); + .body("from NEXT filter exception handle").build()); First first = server.getApplicationContext().getBean(First.class); Next next = server.getApplicationContext().getBean(Next.class); From e21df1a3024dcf938889dfa899d8f3f826a1e5ed Mon Sep 17 00:00:00 2001 From: micronaut-build <65172877+micronaut-build@users.noreply.github.com> Date: Tue, 10 Jan 2023 21:48:23 +0100 Subject: [PATCH 385/743] Bump micronaut-data to 3.9.4 (#8594) --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 207aae07c54..ea6c74ea904 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -73,7 +73,7 @@ managed-micronaut-cache = "3.5.0" managed-micronaut-cassandra = "5.1.1" managed-micronaut-coherence = "3.7.2" managed-micronaut-crac = "1.1.1" -managed-micronaut-data = "3.9.3" +managed-micronaut-data = "3.9.4" managed-micronaut-discovery = "3.2.0" managed-micronaut-elasticsearch = "4.3.0" managed-micronaut-email = "1.5.0" From 1b37647a6625c749d36a6f64fe508b0d002140c5 Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Tue, 10 Jan 2023 21:48:38 +0100 Subject: [PATCH 386/743] test: BodyAssertion to HTTP Server TCK (#8590) --- .../http/server/tck/AssertionUtils.java | 6 +- .../http/server/tck/BodyAssertion.java | 86 +++++++++++++++++++ .../server/tck/HttpResponseAssertion.java | 32 ++++--- .../http/server/tck/tests/BodyTest.java | 20 +++++ 4 files changed, 130 insertions(+), 14 deletions(-) create mode 100644 http-server-tck/src/main/java/io/micronaut/http/server/tck/BodyAssertion.java diff --git a/http-server-tck/src/main/java/io/micronaut/http/server/tck/AssertionUtils.java b/http-server-tck/src/main/java/io/micronaut/http/server/tck/AssertionUtils.java index 446014320e0..6dd2bb26073 100644 --- a/http-server-tck/src/main/java/io/micronaut/http/server/tck/AssertionUtils.java +++ b/http-server-tck/src/main/java/io/micronaut/http/server/tck/AssertionUtils.java @@ -98,11 +98,11 @@ public static void assertDoesNotThrow(@NonNull ServerUnderTest server, assertion.getResponseConsumer().ifPresent(httpResponseConsumer -> httpResponseConsumer.accept(response)); } - private static void assertBody(@NonNull HttpResponse response, @Nullable String expectedBody) { - if (expectedBody != null) { + private static void assertBody(@NonNull HttpResponse response, @Nullable BodyAssertion bodyAssertion) { + if (bodyAssertion != null) { Optional bodyOptional = response.getBody(String.class); assertTrue(bodyOptional.isPresent()); - bodyOptional.ifPresent(body -> assertTrue(body.contains(expectedBody))); + bodyOptional.ifPresent(bodyAssertion::evaluate); } } diff --git a/http-server-tck/src/main/java/io/micronaut/http/server/tck/BodyAssertion.java b/http-server-tck/src/main/java/io/micronaut/http/server/tck/BodyAssertion.java new file mode 100644 index 00000000000..71355557883 --- /dev/null +++ b/http-server-tck/src/main/java/io/micronaut/http/server/tck/BodyAssertion.java @@ -0,0 +1,86 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.http.server.tck; + +import io.micronaut.core.annotation.Experimental; + +import java.util.function.BiFunction; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * HTTP Reponse's body assertions. + */ +@Experimental +public final class BodyAssertion { + private final String expected; + private final BiFunction evaluator; + + private BodyAssertion(String expected, BiFunction evaluator) { + this.expected = expected; + this.evaluator = evaluator; + } + + /** + * Evaluates the HTTP Response Body. + * @param body The HTTP Response Body + */ + public void evaluate(String body) { + assertTrue(this.evaluator.apply(expected, body)); + } + + /** + * + * @return a Builder; + */ + public static BodyAssertion.Builder builder() { + return new BodyAssertion.Builder(); + } + + /** + * BodyAssertion Builder. + */ + public static class Builder { + + private String body; + + /** + * + * @param expected Expected Body + * @return The Builder + */ + public Builder body(String expected) { + this.body = expected; + return this; + } + + /** + * + * @return a body assertion which verifiers the HTTP Response's body contains the expected body + */ + public BodyAssertion contains() { + return new BodyAssertion(this.body, (expected, body) -> body.contains(expected)); + } + + /** + * + * @return a body assertion which verifiers the HTTP Response's body is equals to the expected body + */ + public BodyAssertion equals() { + return new BodyAssertion(this.body, (expected, body) -> body.equals(expected)); + } + } +} diff --git a/http-server-tck/src/main/java/io/micronaut/http/server/tck/HttpResponseAssertion.java b/http-server-tck/src/main/java/io/micronaut/http/server/tck/HttpResponseAssertion.java index e4af0bc7bc8..0ec962345b8 100644 --- a/http-server-tck/src/main/java/io/micronaut/http/server/tck/HttpResponseAssertion.java +++ b/http-server-tck/src/main/java/io/micronaut/http/server/tck/HttpResponseAssertion.java @@ -34,21 +34,20 @@ */ @Experimental public final class HttpResponseAssertion { - private final HttpStatus httpStatus; private final Map headers; - private final String body; + private final BodyAssertion bodyAssertion; @Nullable private final Consumer> responseConsumer; private HttpResponseAssertion(HttpStatus httpStatus, Map headers, - String body, + BodyAssertion bodyAssertion, @Nullable Consumer> responseConsumer) { this.httpStatus = httpStatus; this.headers = headers; - this.body = body; + this.bodyAssertion = bodyAssertion; this.responseConsumer = responseConsumer; } @@ -77,8 +76,9 @@ public Map getHeaders() { * * @return Expected HTTP Response body */ - public String getBody() { - return body; + + public BodyAssertion getBody() { + return bodyAssertion; } /** @@ -95,7 +95,7 @@ public static HttpResponseAssertion.Builder builder() { public static class Builder { private HttpStatus httpStatus; private Map headers; - private String body; + private BodyAssertion bodyAssertion; private Consumer> responseConsumer; @@ -135,11 +135,21 @@ public Builder header(String headerName, String headerValue) { /** * - * @param body Response Body + * @param containsBody Response Body + * @return HTTP Response Assertion Builder + */ + public Builder body(String containsBody) { + this.bodyAssertion = BodyAssertion.builder().body(containsBody).contains(); + return this; + } + + /** + * + * @param bodyAssertion Response Body Assertion * @return HTTP Response Assertion Builder */ - public Builder body(String body) { - this.body = body; + public Builder body(BodyAssertion bodyAssertion) { + this.bodyAssertion = bodyAssertion; return this; } @@ -158,7 +168,7 @@ public Builder status(HttpStatus httpStatus) { * @return HTTP Response Assertion */ public HttpResponseAssertion build() { - return new HttpResponseAssertion(Objects.requireNonNull(httpStatus), headers, body, responseConsumer); + return new HttpResponseAssertion(Objects.requireNonNull(httpStatus), headers, bodyAssertion, responseConsumer); } } } diff --git a/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/BodyTest.java b/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/BodyTest.java index 27c28cde8cf..48bc82bf294 100644 --- a/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/BodyTest.java +++ b/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/BodyTest.java @@ -26,6 +26,7 @@ import io.micronaut.http.annotation.Post; import io.micronaut.http.annotation.Status; import io.micronaut.http.server.tck.AssertionUtils; +import io.micronaut.http.server.tck.BodyAssertion; import io.micronaut.http.server.tck.HttpResponseAssertion; import static io.micronaut.http.server.tck.TestScenario.asserts; import org.junit.jupiter.api.Test; @@ -88,6 +89,19 @@ void testCustomBodyPOJOReactiveTypes() throws IOException { .build())); } + @Test + void testCustomListBodyPOJOReactiveTypes() throws IOException { + String body = "[{\"x\":10,\"y\":20},{\"x\":30,\"y\":40}]"; + asserts(SPEC_NAME, + HttpRequest.POST("/response-body/pojo-flux", body) + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON), + (server, request) -> AssertionUtils.assertDoesNotThrow(server, request, + HttpResponseAssertion.builder() + .status(HttpStatus.CREATED) + .body(BodyAssertion.builder().body(body).equals()) + .build())); + } + @Controller("/response-body") @Requires(property = "spec.name", value = SPEC_NAME) static class BodyController { @@ -111,6 +125,12 @@ Publisher post(@Body Publisher data) { return data; } + @Post(uri = "/pojo-flux") + @Status(HttpStatus.CREATED) + Publisher postMany(@Body Publisher data) { + return data; + } + @Post(uri = "/bytes", consumes = MediaType.TEXT_PLAIN) @Status(HttpStatus.CREATED) String postBytes(@Body byte[] bytes) { From 4dda4373e6476ec2989977499e941cd2eb61464a Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Tue, 10 Jan 2023 21:49:42 +0100 Subject: [PATCH 387/743] fix: 403 for cors simple requests & CorsFilter off (#8582) see: https://github.com/micronaut-projects/micronaut-core/security/advisories/GHSA-583g-g682-crxf#advisory-comment-77446 --- .../netty/cors/CorsFilterEnabledSpec.groovy | 4 +- .../server/netty/cors/CorsFilterSpec.groovy | 9 +- .../CorsOriginConverterEnabledSpec.groovy | 4 +- .../server/netty/cors/NettyCorsSpec.groovy | 14 +++ .../tests/cors/CorsDisabledByDefaultTest.java | 101 ++++++++++++++++ .../SimpleRequestWithCorsNotEnabledTest.java | 114 ++++++++++++++++++ .../http/server/cors/CorsFilter.java | 21 ++++ .../http/server/cors/package-info.java | 6 - 8 files changed, 262 insertions(+), 11 deletions(-) create mode 100644 http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/cors/CorsDisabledByDefaultTest.java create mode 100644 http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/cors/SimpleRequestWithCorsNotEnabledTest.java diff --git a/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/cors/CorsFilterEnabledSpec.groovy b/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/cors/CorsFilterEnabledSpec.groovy index 3a94796f892..ed3b7fe2909 100644 --- a/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/cors/CorsFilterEnabledSpec.groovy +++ b/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/cors/CorsFilterEnabledSpec.groovy @@ -12,8 +12,8 @@ class CorsFilterEnabledSpec extends Specification { @Shared ApplicationContext applicationContext = ApplicationContext.run() - void "CorsFilter is not enabled by default"() { + void "CorsFilter is enabled by default"() { expect: - !applicationContext.containsBean(CorsFilter) + applicationContext.containsBean(CorsFilter) } } diff --git a/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/cors/CorsFilterSpec.groovy b/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/cors/CorsFilterSpec.groovy index 8553e89adb1..7eb73af108b 100644 --- a/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/cors/CorsFilterSpec.groovy +++ b/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/cors/CorsFilterSpec.groovy @@ -16,6 +16,7 @@ package io.micronaut.http.server.netty.cors import io.micronaut.context.ApplicationContext +import io.micronaut.core.annotation.Nullable import io.micronaut.core.async.publisher.Publishers import io.micronaut.core.util.StringUtils import io.micronaut.http.* @@ -25,6 +26,7 @@ import io.micronaut.http.filter.ServerFilterChain import io.micronaut.http.server.HttpServerConfiguration import io.micronaut.http.server.cors.CorsFilter import io.micronaut.http.server.cors.CorsOriginConfiguration +import io.micronaut.http.server.util.HttpHostResolver import io.micronaut.runtime.server.EmbeddedServer import io.micronaut.web.router.RouteMatch import io.micronaut.web.router.Router @@ -559,6 +561,11 @@ class CorsFilterSpec extends Specification { } private CorsFilter buildCorsHandler(HttpServerConfiguration.CorsConfiguration config) { - new CorsFilter(config ?: enabledCorsConfiguration()) + new CorsFilter(config ?: enabledCorsConfiguration(), new HttpHostResolver() { + @Override + String resolve(@Nullable HttpRequest request) { + return "http://micronautexample.com"; + } + }) } } diff --git a/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/cors/CorsOriginConverterEnabledSpec.groovy b/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/cors/CorsOriginConverterEnabledSpec.groovy index 2f98a0bfca4..825700190d9 100644 --- a/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/cors/CorsOriginConverterEnabledSpec.groovy +++ b/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/cors/CorsOriginConverterEnabledSpec.groovy @@ -12,8 +12,8 @@ class CorsOriginConverterEnabledSpec extends Specification { @Shared ApplicationContext applicationContext = ApplicationContext.run() - void "CorsOriginConverter is not enabled by default"() { + void "CorsOriginConverter is enabled by default"() { expect: - !applicationContext.containsBean(CorsOriginConverter) + applicationContext.containsBean(CorsOriginConverter) } } diff --git a/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/cors/NettyCorsSpec.groovy b/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/cors/NettyCorsSpec.groovy index 75c66f6ee74..f2123f8c661 100644 --- a/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/cors/NettyCorsSpec.groovy +++ b/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/cors/NettyCorsSpec.groovy @@ -15,7 +15,9 @@ */ package io.micronaut.http.server.netty.cors +import io.micronaut.context.annotation.Replaces import io.micronaut.context.annotation.Requires +import io.micronaut.core.annotation.Nullable import io.micronaut.http.HttpRequest import io.micronaut.http.HttpResponse import io.micronaut.http.HttpStatus @@ -25,6 +27,8 @@ import io.micronaut.http.annotation.Get import io.micronaut.http.annotation.Post import io.micronaut.http.client.exceptions.HttpClientResponseException import io.micronaut.http.server.netty.AbstractMicronautSpec +import io.micronaut.http.server.util.HttpHostResolver +import jakarta.inject.Singleton import reactor.core.publisher.Flux import static io.micronaut.http.HttpHeaders.* @@ -311,6 +315,16 @@ class NettyCorsSpec extends AbstractMicronautSpec { 'micronaut.server.dateHeader': false] } + @Requires(property = 'spec.name', value = 'NettyCorsSpec') + @Replaces(HttpHostResolver.class) + @Singleton + static class HttpHostResolverReplacement implements HttpHostResolver { + @Override + String resolve(@Nullable HttpRequest request) { + "https://micronautexample.com" + } + } + @Controller('/test') @Requires(property = 'spec.name', value = 'NettyCorsSpec') static class TestController { diff --git a/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/cors/CorsDisabledByDefaultTest.java b/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/cors/CorsDisabledByDefaultTest.java new file mode 100644 index 00000000000..cc52de62568 --- /dev/null +++ b/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/cors/CorsDisabledByDefaultTest.java @@ -0,0 +1,101 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.http.server.tck.tests.cors; + +import io.micronaut.context.annotation.Replaces; +import io.micronaut.context.annotation.Requires; +import io.micronaut.core.annotation.Nullable; +import io.micronaut.http.HttpRequest; +import io.micronaut.http.HttpStatus; +import io.micronaut.http.MediaType; +import io.micronaut.http.annotation.Controller; +import io.micronaut.http.annotation.Post; +import io.micronaut.http.annotation.Status; +import io.micronaut.http.server.tck.AssertionUtils; +import io.micronaut.http.server.tck.HttpResponseAssertion; +import io.micronaut.http.server.util.HttpHostResolver; +import jakarta.inject.Singleton; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.util.Collections; + +import static io.micronaut.http.server.tck.TestScenario.asserts; +import static org.junit.jupiter.api.Assertions.assertNull; + +@SuppressWarnings({ + "java:S2259", // The tests will show if it's null + "java:S5960", // We're allowed assertions, as these are used in tests only + "checkstyle:MissingJavadocType", +}) +public class CorsDisabledByDefaultTest { + + private static final String SPECNAME = "CorsDisabledByDefaultTest"; + + /** + * By default CORS is disabled no cors headers are present in response. + * @throws IOException may throw the try for resources + */ + @Test + void corsDisabledByDefault() throws IOException { + asserts(SPECNAME, + createRequest("https://foo.com"), + (server, request) -> { + AssertionUtils.assertDoesNotThrow(server, request, HttpResponseAssertion.builder() + .status(HttpStatus.OK) + .assertResponse(response -> { + assertNull(response.getHeaders().get("Access-Control-Allow-Origin")); + assertNull(response.getHeaders().get("Vary")); + assertNull(response.getHeaders().get("Access-Control-Allow-Credentials")); + assertNull(response.getHeaders().get("Access-Control-Allow-Methods")); + assertNull(response.getHeaders().get("Access-Control-Allow-Headers")); + assertNull(response.getHeaders().get("Access-Control-Max-Age")); + }) + .build()); + }); + } + + static HttpRequest createRequest(String origin) { + return HttpRequest.POST("/refresh", Collections.emptyMap()) + .header("Content-Type", MediaType.APPLICATION_JSON) + .header("Origin", origin) + .header("Accept-Encoding", "gzip, deflate") + .header("Connection", "keep-alive") + .header("Accept", "*/*") + .header("User-Agent", "Mozilla / 5.0 (Macintosh; Intel Mac OS X 10_15_7)AppleWebKit / 605.1 .15 (KHTML, like Gecko)Version / 16.1 Safari / 605.1 .15") + .header("Referer", origin) + .header("Accept-Language", "en - GB, en"); + } + + @Requires(property = "spec.name", value = SPECNAME) + @Controller + static class RefreshController { + @Post("/refresh") + @Status(HttpStatus.OK) + void refresh() { + } + } + + @Requires(property = "spec.name", value = SPECNAME) + @Replaces(HttpHostResolver.class) + @Singleton + static class HttpHostResolverReplacement implements HttpHostResolver { + @Override + public String resolve(@Nullable HttpRequest request) { + return "https://micronautexample.com"; + } + } +} diff --git a/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/cors/SimpleRequestWithCorsNotEnabledTest.java b/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/cors/SimpleRequestWithCorsNotEnabledTest.java new file mode 100644 index 00000000000..6ede80ac6b9 --- /dev/null +++ b/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/cors/SimpleRequestWithCorsNotEnabledTest.java @@ -0,0 +1,114 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.http.server.tck.tests.cors; + +import io.micronaut.context.annotation.Requires; +import io.micronaut.context.event.ApplicationEventListener; +import io.micronaut.context.event.ApplicationEventPublisher; +import io.micronaut.http.HttpRequest; +import io.micronaut.http.HttpStatus; +import io.micronaut.http.annotation.Controller; +import io.micronaut.http.annotation.Post; +import io.micronaut.http.annotation.Status; +import io.micronaut.http.server.tck.AssertionUtils; +import io.micronaut.http.server.tck.HttpResponseAssertion; +import io.micronaut.runtime.context.scope.refresh.RefreshEvent; +import jakarta.inject.Inject; +import jakarta.inject.Singleton; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.util.Collections; + +import static io.micronaut.http.server.tck.TestScenario.asserts; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; + +@SuppressWarnings({ + "java:S2259", // The tests will show if it's null + "java:S5960", // We're allowed assertions, as these are used in tests only + "checkstyle:MissingJavadocType", +}) +public class SimpleRequestWithCorsNotEnabledTest { + private static final String SPECNAME = "SimpleRequestWithCorsNotEnabledTest"; + + /** + * @see GHSA-583g-g682-crxf + * A malicious/compromised website can make HTTP requests to localhost. This test verifies a CORS simple request is denied when invoked against a Micronaut application running in localhost without cors enabled. + * @throws IOException scenario step fails + */ + @Test + void corsSimpleRequestNotAllowedForLocalhostAndAny() throws IOException { + asserts(SPECNAME, + createRequest(), + (server, request) -> { + RefreshCounter refreshCounter = server.getApplicationContext().getBean(RefreshCounter.class); + assertEquals(0, refreshCounter.getRefreshCount()); + + AssertionUtils.assertThrows(server, request, HttpResponseAssertion.builder() + .status(HttpStatus.FORBIDDEN) + .assertResponse(response -> assertFalse(response.getHeaders().contains("Vary"))) + .build()); + assertEquals(0, refreshCounter.getRefreshCount()); + }); + } + + private static HttpRequest createRequest() { + return HttpRequest.POST("/refresh", Collections.emptyMap()) + .header("Accept", "*/*") + .header("Accept-Encoding", "gzip, deflate, br") + .header("Accept-Language", "en-GB,en-US;q=0.9,en;q=0.8") + .header("Connection", "keep-alive") + .header("Content-Length", "0") + .header("Host", "localhost:8080") + .header("Origin", "https://sdelamo.github.io") + .header("sec-ch-ua", "\"Not?A_Brand\";v=\"8\", \"Chromium\";v=\"108\", \"Google Chrome\";v=\"108\"") + .header("sec-ch-ua-mobile", "?0") + .header("sec-ch-ua-platform", "\"macOS\"") + .header("Sec-Fetch-Dest", "empty") + .header("Sec-Fetch-Mode", "cors") + .header("Sec-Fetch-Site", "cross-site") + .header("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36"); + } + + @Requires(property = "spec.name", value = SPECNAME) + @Controller + static class RefreshController { + @Inject + ApplicationEventPublisher refreshEventApplicationEventPublisher; + + @Post("/refresh") + @Status(HttpStatus.OK) + void refresh() { + refreshEventApplicationEventPublisher.publishEvent(new RefreshEvent()); + } + } + + @Requires(property = "spec.name", value = SPECNAME) + @Singleton + static class RefreshCounter implements ApplicationEventListener { + private int refreshCount = 0; + + @Override + public void onApplicationEvent(RefreshEvent event) { + refreshCount++; + } + + public int getRefreshCount() { + return refreshCount; + } + } +} diff --git a/http-server/src/main/java/io/micronaut/http/server/cors/CorsFilter.java b/http-server/src/main/java/io/micronaut/http/server/cors/CorsFilter.java index 4f14b2e615a..46bed4c3c81 100644 --- a/http-server/src/main/java/io/micronaut/http/server/cors/CorsFilter.java +++ b/http-server/src/main/java/io/micronaut/http/server/cors/CorsFilter.java @@ -109,6 +109,9 @@ public Publisher> doFilter(HttpRequest request, Server return forbidden(); } return Publishers.then(chain.proceed(request), resp -> decorateResponseWithHeaders(request, resp, corsOriginConfiguration)); + } else if (shouldDenyToPreventDriveByLocalhostAttack(origin, request)) { + LOG.trace("the request specifies an origin different than localhost. To prevent drive-by-localhost attacks the request is forbidden"); + return forbidden(); } LOG.trace("CORS configuration not found for {} origin", origin); return chain.proceed(request); @@ -130,6 +133,21 @@ protected boolean shouldDenyToPreventDriveByLocalhostAttack(@NonNull CorsOriginC } + /** + * + * @param origin HTTP Header {@link HttpHeaders#ORIGIN} value. + * @param request HTTP Request + * @return {@literal true} if the resolved host starts with {@literal http://localhost} and origin does not start with localhost deny it. + */ + protected boolean shouldDenyToPreventDriveByLocalhostAttack(@NonNull String origin, + @NonNull HttpRequest request) { + if (httpHostResolver == null) { + return false; + } + String host = httpHostResolver.resolve(request); + return !origin.startsWith(LOCALHOST) && host.startsWith(LOCALHOST); + } + @Override public int getOrder() { return ServerFilterPhase.METRICS.after(); @@ -249,6 +267,9 @@ protected void setMaxAge(long maxAge, MutableHttpResponse response) { @NonNull private Optional getConfiguration(@NonNull String requestOrigin) { + if (!corsConfiguration.isEnabled()) { + return Optional.empty(); + } return corsConfiguration.getConfigurations().values().stream() .filter(config -> { List allowedOrigins = config.getAllowedOrigins(); diff --git a/http-server/src/main/java/io/micronaut/http/server/cors/package-info.java b/http-server/src/main/java/io/micronaut/http/server/cors/package-info.java index c6f2aa57a83..5e4839613b8 100644 --- a/http-server/src/main/java/io/micronaut/http/server/cors/package-info.java +++ b/http-server/src/main/java/io/micronaut/http/server/cors/package-info.java @@ -19,10 +19,4 @@ * @author Graeme Rocher * @since 1.0 */ -@Configuration -@Requires(property = "micronaut.server.cors.enabled", value = StringUtils.TRUE) package io.micronaut.http.server.cors; - -import io.micronaut.context.annotation.Configuration; -import io.micronaut.context.annotation.Requires; -import io.micronaut.core.util.StringUtils; From 27316322eba48a833bce99f0a06e1d0f9de77863 Mon Sep 17 00:00:00 2001 From: Denis Stepanov Date: Wed, 11 Jan 2023 11:03:12 +0100 Subject: [PATCH 388/743] Load bean listeners lazily (#8595) --- .../ACreatedListener.java | 9 + .../BCreationListener.java | 8 + .../BeanCreatedListenerWarningSpec.groovy | 33 ---- .../BeanCreationEventListenerSpec.groovy | 117 +++++++++++- .../CCreatedListener.java | 9 + .../NotOffendingChainListener.java | 8 +- .../OffendingChainListener.java | 8 +- .../OffendingConstructorListener.java | 8 +- .../OffendingFieldListener.java | 8 + .../OffendingInterfaceListener.java | 8 +- .../OffendingMethodListener.java | 8 + .../circular/Bar.java | 9 + .../circular/BarCreationListener.java | 37 ++++ .../circular/Foo.java | 9 + .../circular/FooCreationListener.java | 37 ++++ .../AbstractInitializableBeanDefinition.java | 7 +- .../micronaut/context/DefaultBeanContext.java | 175 +++++++++--------- 17 files changed, 372 insertions(+), 126 deletions(-) delete mode 100644 inject-java/src/test/groovy/io/micronaut/inject/lifecycle/beancreationeventlistener/BeanCreatedListenerWarningSpec.groovy create mode 100644 inject-java/src/test/groovy/io/micronaut/inject/lifecycle/beancreationeventlistener/circular/Bar.java create mode 100644 inject-java/src/test/groovy/io/micronaut/inject/lifecycle/beancreationeventlistener/circular/BarCreationListener.java create mode 100644 inject-java/src/test/groovy/io/micronaut/inject/lifecycle/beancreationeventlistener/circular/Foo.java create mode 100644 inject-java/src/test/groovy/io/micronaut/inject/lifecycle/beancreationeventlistener/circular/FooCreationListener.java diff --git a/inject-java/src/test/groovy/io/micronaut/inject/lifecycle/beancreationeventlistener/ACreatedListener.java b/inject-java/src/test/groovy/io/micronaut/inject/lifecycle/beancreationeventlistener/ACreatedListener.java index 63b6a9db63f..292d6ee76f9 100644 --- a/inject-java/src/test/groovy/io/micronaut/inject/lifecycle/beancreationeventlistener/ACreatedListener.java +++ b/inject-java/src/test/groovy/io/micronaut/inject/lifecycle/beancreationeventlistener/ACreatedListener.java @@ -6,8 +6,17 @@ @Singleton public class ACreatedListener implements BeanCreatedEventListener { + + static boolean initialized; + static boolean executed; + + ACreatedListener() { + initialized = true; + } + @Override public A onCreated(BeanCreatedEvent event) { + executed = true; return event.getBean(); } } diff --git a/inject-java/src/test/groovy/io/micronaut/inject/lifecycle/beancreationeventlistener/BCreationListener.java b/inject-java/src/test/groovy/io/micronaut/inject/lifecycle/beancreationeventlistener/BCreationListener.java index 11edf8b02d0..80204ab6747 100644 --- a/inject-java/src/test/groovy/io/micronaut/inject/lifecycle/beancreationeventlistener/BCreationListener.java +++ b/inject-java/src/test/groovy/io/micronaut/inject/lifecycle/beancreationeventlistener/BCreationListener.java @@ -23,8 +23,16 @@ @Singleton public class BCreationListener implements BeanCreatedEventListener { + static boolean initialized; + static boolean executed; + + BCreationListener() { + initialized = true; + } + @Override public B onCreated(BeanCreatedEvent event) { + executed = true; ChildB childB = new ChildB(event.getBean()); childB.name = "good"; return childB; diff --git a/inject-java/src/test/groovy/io/micronaut/inject/lifecycle/beancreationeventlistener/BeanCreatedListenerWarningSpec.groovy b/inject-java/src/test/groovy/io/micronaut/inject/lifecycle/beancreationeventlistener/BeanCreatedListenerWarningSpec.groovy deleted file mode 100644 index 145b71bbedf..00000000000 --- a/inject-java/src/test/groovy/io/micronaut/inject/lifecycle/beancreationeventlistener/BeanCreatedListenerWarningSpec.groovy +++ /dev/null @@ -1,33 +0,0 @@ -package io.micronaut.inject.lifecycle.beancreationeventlistener - -import io.micronaut.context.BeanContext -import io.micronaut.context.DefaultBeanContext -import spock.lang.Specification - -class BeanCreatedListenerWarningSpec extends Specification { - - void "test a warning is logged when an event listener won't be executed due to the bean being injected by another bean created event listener"() { - def oldOut = System.out - def out = new ByteArrayOutputStream() - System.out = new PrintStream(out) - - when: - BeanContext context = new DefaultBeanContext().start() - String output = out.toString("UTF-8") - out.close() - - then: - output.contains("The bean created event listener io.micronaut.inject.lifecycle.beancreationeventlistener.ACreatedListener will not be executed because one or more other bean created event listeners inject io.micronaut.inject.lifecycle.beancreationeventlistener.A:") - output.contains(" io.micronaut.inject.lifecycle.beancreationeventlistener.OffendingFieldListener --> io.micronaut.inject.lifecycle.beancreationeventlistener.A") - output.contains(" io.micronaut.inject.lifecycle.beancreationeventlistener.OffendingMethodListener --> io.micronaut.inject.lifecycle.beancreationeventlistener.A") - output.contains(" io.micronaut.inject.lifecycle.beancreationeventlistener.OffendingConstructorListener --> io.micronaut.inject.lifecycle.beancreationeventlistener.A") - output.contains(" io.micronaut.inject.lifecycle.beancreationeventlistener.OffendingInterfaceListener --> io.micronaut.inject.lifecycle.beancreationeventlistener.AInterface") - output.contains(" io.micronaut.inject.lifecycle.beancreationeventlistener.OffendingChainListener --> io.micronaut.inject.lifecycle.beancreationeventlistener.D --> io.micronaut.inject.lifecycle.beancreationeventlistener.E --> io.micronaut.inject.lifecycle.beancreationeventlistener.A") - !output.contains("The bean created event listener io.micronaut.inject.lifecycle.beancreationeventlistener.CCreatedListener will not be executed") - !output.contains("NotOffendingChainListener") //because F injects a provider of G - - cleanup: - System.out = oldOut - context.close() - } -} diff --git a/inject-java/src/test/groovy/io/micronaut/inject/lifecycle/beancreationeventlistener/BeanCreationEventListenerSpec.groovy b/inject-java/src/test/groovy/io/micronaut/inject/lifecycle/beancreationeventlistener/BeanCreationEventListenerSpec.groovy index 2a54d79f5fb..3fce86fb4fc 100644 --- a/inject-java/src/test/groovy/io/micronaut/inject/lifecycle/beancreationeventlistener/BeanCreationEventListenerSpec.groovy +++ b/inject-java/src/test/groovy/io/micronaut/inject/lifecycle/beancreationeventlistener/BeanCreationEventListenerSpec.groovy @@ -15,22 +15,137 @@ */ package io.micronaut.inject.lifecycle.beancreationeventlistener +import io.micronaut.context.ApplicationContext import io.micronaut.context.BeanContext import io.micronaut.context.DefaultBeanContext +import io.micronaut.context.exceptions.CircularDependencyException +import io.micronaut.inject.lifecycle.beancreationeventlistener.circular.Bar +import io.micronaut.inject.lifecycle.beancreationeventlistener.circular.Foo import spock.lang.Specification class BeanCreationEventListenerSpec extends Specification { void "test bean creation listener"() { given: + ACreatedListener.initialized = false + ACreatedListener.executed = false + BCreationListener.initialized = false + BCreationListener.executed = false + CCreatedListener.initialized = false + CCreatedListener.executed = false + NotOffendingChainListener.initialized = false + NotOffendingChainListener.executed = false + OffendingChainListener.initialized = false + OffendingChainListener.executed = false + OffendingConstructorListener.initialized = false + OffendingConstructorListener.executed = false + OffendingFieldListener.initialized = false + OffendingFieldListener.executed = false + OffendingInterfaceListener.initialized = false + OffendingInterfaceListener.executed = false + OffendingMethodListener.initialized = false + OffendingMethodListener.executed = false + + when: BeanContext context = new DefaultBeanContext().start() + then: + ACreatedListener.initialized == false + ACreatedListener.executed == false + BCreationListener.initialized == false + BCreationListener.executed == false + CCreatedListener.initialized == false + CCreatedListener.executed == false + NotOffendingChainListener.initialized == false + NotOffendingChainListener.executed == false + OffendingChainListener.initialized == false + OffendingChainListener.executed == false + OffendingConstructorListener.initialized == false + OffendingConstructorListener.executed == false + OffendingFieldListener.initialized == false + OffendingFieldListener.executed == false + OffendingInterfaceListener.initialized == false + OffendingInterfaceListener.executed == false + OffendingMethodListener.initialized == false + OffendingMethodListener.executed == false + + when: + B b = context.getBean(B) + + then: + b instanceof ChildB + b.name == "good" + + and: + ACreatedListener.initialized == true + ACreatedListener.executed == true + BCreationListener.initialized == true + BCreationListener.executed == true + CCreatedListener.initialized == false + CCreatedListener.executed == false + NotOffendingChainListener.initialized == true + NotOffendingChainListener.executed == true + OffendingChainListener.initialized == true + OffendingChainListener.executed == true + OffendingConstructorListener.initialized == true + OffendingConstructorListener.executed == true + OffendingFieldListener.initialized == true + OffendingFieldListener.executed == true + OffendingInterfaceListener.initialized == true + OffendingInterfaceListener.executed == true + OffendingMethodListener.initialized == true + OffendingMethodListener.executed == true + + cleanup: + context.close() + } + + void "test application bean creation listener"() { + given: + BeanContext context = ApplicationContext.builder().start() + + when: + B b = context.getBean(B) + + then: + b instanceof ChildB + b.name == "good" + + cleanup: + context.close() + } + + void "test bean creation listener eager"() { + given: + BeanContext context = ApplicationContext.builder().eagerInitSingletons(true).start() when: - B b= context.getBean(B) + B b = context.getBean(B) then: b instanceof ChildB b.name == "good" + cleanup: + context.close() + } + + void "test recursive listeners"() { + given: + BeanContext context = ApplicationContext.builder().properties(["spec": "RecursiveListeners"]).start() + + when: + context.getBean(Foo) + + then: + thrown(CircularDependencyException) + + when: + context.getBean(Bar) + + then: + thrown(CircularDependencyException) + + cleanup: + context.close() } } diff --git a/inject-java/src/test/groovy/io/micronaut/inject/lifecycle/beancreationeventlistener/CCreatedListener.java b/inject-java/src/test/groovy/io/micronaut/inject/lifecycle/beancreationeventlistener/CCreatedListener.java index c0a5a0f8002..ec66e56a8f5 100644 --- a/inject-java/src/test/groovy/io/micronaut/inject/lifecycle/beancreationeventlistener/CCreatedListener.java +++ b/inject-java/src/test/groovy/io/micronaut/inject/lifecycle/beancreationeventlistener/CCreatedListener.java @@ -6,8 +6,17 @@ @Singleton public class CCreatedListener implements BeanCreatedEventListener { + + static boolean initialized; + static boolean executed; + + CCreatedListener() { + initialized = true; + } + @Override public C onCreated(BeanCreatedEvent event) { + executed = true; return event.getBean(); } } diff --git a/inject-java/src/test/groovy/io/micronaut/inject/lifecycle/beancreationeventlistener/NotOffendingChainListener.java b/inject-java/src/test/groovy/io/micronaut/inject/lifecycle/beancreationeventlistener/NotOffendingChainListener.java index 46b2ab0cc52..1ec4e1a7bfb 100644 --- a/inject-java/src/test/groovy/io/micronaut/inject/lifecycle/beancreationeventlistener/NotOffendingChainListener.java +++ b/inject-java/src/test/groovy/io/micronaut/inject/lifecycle/beancreationeventlistener/NotOffendingChainListener.java @@ -7,10 +7,16 @@ @Singleton public class NotOffendingChainListener implements BeanCreatedEventListener { - NotOffendingChainListener(F f) {} + static boolean initialized; + static boolean executed; + + NotOffendingChainListener(F f) { + initialized = true; + } @Override public B onCreated(BeanCreatedEvent event) { + executed = true; return event.getBean(); } } diff --git a/inject-java/src/test/groovy/io/micronaut/inject/lifecycle/beancreationeventlistener/OffendingChainListener.java b/inject-java/src/test/groovy/io/micronaut/inject/lifecycle/beancreationeventlistener/OffendingChainListener.java index 1b53e527470..a3d41b31c53 100644 --- a/inject-java/src/test/groovy/io/micronaut/inject/lifecycle/beancreationeventlistener/OffendingChainListener.java +++ b/inject-java/src/test/groovy/io/micronaut/inject/lifecycle/beancreationeventlistener/OffendingChainListener.java @@ -7,10 +7,16 @@ @Singleton public class OffendingChainListener implements BeanCreatedEventListener { - OffendingChainListener(D d) {} + static boolean initialized; + static boolean executed; + + OffendingChainListener(D d) { + initialized = true; + } @Override public B onCreated(BeanCreatedEvent event) { + executed = true; return event.getBean(); } } diff --git a/inject-java/src/test/groovy/io/micronaut/inject/lifecycle/beancreationeventlistener/OffendingConstructorListener.java b/inject-java/src/test/groovy/io/micronaut/inject/lifecycle/beancreationeventlistener/OffendingConstructorListener.java index bdc0d9ed054..8ccdf1ad450 100644 --- a/inject-java/src/test/groovy/io/micronaut/inject/lifecycle/beancreationeventlistener/OffendingConstructorListener.java +++ b/inject-java/src/test/groovy/io/micronaut/inject/lifecycle/beancreationeventlistener/OffendingConstructorListener.java @@ -8,10 +8,16 @@ @Singleton public class OffendingConstructorListener implements BeanCreatedEventListener { - OffendingConstructorListener(A a, Provider cProvider) {} + static boolean initialized; + static boolean executed; + + OffendingConstructorListener(A a, Provider cProvider) { + initialized = true; + } @Override public B onCreated(BeanCreatedEvent event) { + executed = true; return event.getBean(); } } diff --git a/inject-java/src/test/groovy/io/micronaut/inject/lifecycle/beancreationeventlistener/OffendingFieldListener.java b/inject-java/src/test/groovy/io/micronaut/inject/lifecycle/beancreationeventlistener/OffendingFieldListener.java index 92363e3b1de..a13b4ed1f5b 100644 --- a/inject-java/src/test/groovy/io/micronaut/inject/lifecycle/beancreationeventlistener/OffendingFieldListener.java +++ b/inject-java/src/test/groovy/io/micronaut/inject/lifecycle/beancreationeventlistener/OffendingFieldListener.java @@ -9,11 +9,19 @@ @Singleton public class OffendingFieldListener implements BeanCreatedEventListener { + static boolean initialized; + static boolean executed; + @Inject A a; @Inject Provider cProvider; + OffendingFieldListener() { + initialized = true; + } + @Override public B onCreated(BeanCreatedEvent event) { + executed = true; return event.getBean(); } } diff --git a/inject-java/src/test/groovy/io/micronaut/inject/lifecycle/beancreationeventlistener/OffendingInterfaceListener.java b/inject-java/src/test/groovy/io/micronaut/inject/lifecycle/beancreationeventlistener/OffendingInterfaceListener.java index 6a47d773c57..73c2b800b90 100644 --- a/inject-java/src/test/groovy/io/micronaut/inject/lifecycle/beancreationeventlistener/OffendingInterfaceListener.java +++ b/inject-java/src/test/groovy/io/micronaut/inject/lifecycle/beancreationeventlistener/OffendingInterfaceListener.java @@ -7,10 +7,16 @@ @Singleton public class OffendingInterfaceListener implements BeanCreatedEventListener { - OffendingInterfaceListener(AInterface a) {} + static boolean initialized; + static boolean executed; + + OffendingInterfaceListener(AInterface a) { + initialized = true; + } @Override public B onCreated(BeanCreatedEvent event) { + executed = true; return event.getBean(); } } diff --git a/inject-java/src/test/groovy/io/micronaut/inject/lifecycle/beancreationeventlistener/OffendingMethodListener.java b/inject-java/src/test/groovy/io/micronaut/inject/lifecycle/beancreationeventlistener/OffendingMethodListener.java index 0a44b103793..586684e58a0 100644 --- a/inject-java/src/test/groovy/io/micronaut/inject/lifecycle/beancreationeventlistener/OffendingMethodListener.java +++ b/inject-java/src/test/groovy/io/micronaut/inject/lifecycle/beancreationeventlistener/OffendingMethodListener.java @@ -9,6 +9,13 @@ @Singleton public class OffendingMethodListener implements BeanCreatedEventListener { + static boolean initialized; + static boolean executed; + + OffendingMethodListener() { + initialized = true; + } + @Inject void setA(A a) {} @Inject @@ -16,6 +23,7 @@ void setC(Provider cProvider) {} @Override public B onCreated(BeanCreatedEvent event) { + executed = true; return event.getBean(); } } diff --git a/inject-java/src/test/groovy/io/micronaut/inject/lifecycle/beancreationeventlistener/circular/Bar.java b/inject-java/src/test/groovy/io/micronaut/inject/lifecycle/beancreationeventlistener/circular/Bar.java new file mode 100644 index 00000000000..699e977eeb7 --- /dev/null +++ b/inject-java/src/test/groovy/io/micronaut/inject/lifecycle/beancreationeventlistener/circular/Bar.java @@ -0,0 +1,9 @@ +package io.micronaut.inject.lifecycle.beancreationeventlistener.circular; + +import io.micronaut.context.annotation.Requires; +import jakarta.inject.Singleton; + +@Requires(property = "spec", value = "RecursiveListeners") +@Singleton +public class Bar { +} diff --git a/inject-java/src/test/groovy/io/micronaut/inject/lifecycle/beancreationeventlistener/circular/BarCreationListener.java b/inject-java/src/test/groovy/io/micronaut/inject/lifecycle/beancreationeventlistener/circular/BarCreationListener.java new file mode 100644 index 00000000000..f25fc313e59 --- /dev/null +++ b/inject-java/src/test/groovy/io/micronaut/inject/lifecycle/beancreationeventlistener/circular/BarCreationListener.java @@ -0,0 +1,37 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.inject.lifecycle.beancreationeventlistener.circular; + +import io.micronaut.context.annotation.Requires; +import io.micronaut.context.event.BeanCreatedEvent; +import io.micronaut.context.event.BeanCreatedEventListener; +import jakarta.inject.Singleton; + +@Requires(property = "spec", value = "RecursiveListeners") +@Singleton +public class BarCreationListener implements BeanCreatedEventListener { + + final Foo foo; + + public BarCreationListener(Foo foo) { + this.foo = foo; + } + + @Override + public Bar onCreated(BeanCreatedEvent event) { + return event.getBean(); + } +} diff --git a/inject-java/src/test/groovy/io/micronaut/inject/lifecycle/beancreationeventlistener/circular/Foo.java b/inject-java/src/test/groovy/io/micronaut/inject/lifecycle/beancreationeventlistener/circular/Foo.java new file mode 100644 index 00000000000..e94d4d3ded2 --- /dev/null +++ b/inject-java/src/test/groovy/io/micronaut/inject/lifecycle/beancreationeventlistener/circular/Foo.java @@ -0,0 +1,9 @@ +package io.micronaut.inject.lifecycle.beancreationeventlistener.circular; + +import io.micronaut.context.annotation.Requires; +import jakarta.inject.Singleton; + +@Requires(property = "spec", value = "RecursiveListeners") +@Singleton +public class Foo { +} diff --git a/inject-java/src/test/groovy/io/micronaut/inject/lifecycle/beancreationeventlistener/circular/FooCreationListener.java b/inject-java/src/test/groovy/io/micronaut/inject/lifecycle/beancreationeventlistener/circular/FooCreationListener.java new file mode 100644 index 00000000000..97e618c5ff3 --- /dev/null +++ b/inject-java/src/test/groovy/io/micronaut/inject/lifecycle/beancreationeventlistener/circular/FooCreationListener.java @@ -0,0 +1,37 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.inject.lifecycle.beancreationeventlistener.circular; + +import io.micronaut.context.annotation.Requires; +import io.micronaut.context.event.BeanCreatedEvent; +import io.micronaut.context.event.BeanCreatedEventListener; +import jakarta.inject.Singleton; + +@Requires(property = "spec", value = "RecursiveListeners") +@Singleton +public class FooCreationListener implements BeanCreatedEventListener { + + final Bar bar; + + public FooCreationListener(Bar bar) { + this.bar = bar; + } + + @Override + public Foo onCreated(BeanCreatedEvent event) { + return event.getBean(); + } +} diff --git a/inject/src/main/java/io/micronaut/context/AbstractInitializableBeanDefinition.java b/inject/src/main/java/io/micronaut/context/AbstractInitializableBeanDefinition.java index c2e56607591..264bb273bf1 100644 --- a/inject/src/main/java/io/micronaut/context/AbstractInitializableBeanDefinition.java +++ b/inject/src/main/java/io/micronaut/context/AbstractInitializableBeanDefinition.java @@ -15,6 +15,7 @@ */ package io.micronaut.context; +import io.micronaut.context.DefaultBeanContext.ListenersSupplier; import io.micronaut.context.annotation.ConfigurationProperties; import io.micronaut.context.annotation.EachBean; import io.micronaut.context.annotation.Parameter; @@ -820,12 +821,12 @@ protected Object injectBean(BeanResolutionContext resolutionContext, BeanContext @Internal @UsedByGeneratedCode protected Object postConstruct(BeanResolutionContext resolutionContext, BeanContext context, Object bean) { - final Set, List>> beanInitializedEventListeners + final List, ListenersSupplier>> beanInitializedEventListeners = ((DefaultBeanContext) context).beanInitializedEventListeners; if (CollectionUtils.isNotEmpty(beanInitializedEventListeners)) { - for (Map.Entry, List> entry : beanInitializedEventListeners) { + for (Map.Entry, ListenersSupplier> entry : beanInitializedEventListeners) { if (entry.getKey().isAssignableFrom(getBeanType())) { - for (BeanInitializedEventListener listener : entry.getValue()) { + for (BeanInitializedEventListener listener : entry.getValue().get(resolutionContext)) { bean = listener.onInitialized(new BeanInitializingEvent(context, this, bean)); if (bean == null) { throw new BeanInstantiationException(resolutionContext, "Listener [" + listener + "] returned null from onInitialized event"); diff --git a/inject/src/main/java/io/micronaut/context/DefaultBeanContext.java b/inject/src/main/java/io/micronaut/context/DefaultBeanContext.java index 29b8f433b42..13ad575e945 100644 --- a/inject/src/main/java/io/micronaut/context/DefaultBeanContext.java +++ b/inject/src/main/java/io/micronaut/context/DefaultBeanContext.java @@ -121,6 +121,7 @@ import java.lang.annotation.Annotation; import java.lang.reflect.Method; +import java.util.AbstractMap; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -178,7 +179,6 @@ public class DefaultBeanContext implements InitializableBeanContext { protected final AtomicBoolean terminating = new AtomicBoolean(false); final Map> singlesInCreation = new ConcurrentHashMap<>(5); - Set, List>> beanInitializedEventListeners; private final SingletonScope singletonScope = new SingletonScope(); @@ -233,9 +233,10 @@ public class DefaultBeanContext implements InitializableBeanContext { private List beanDefinitionReferences; private List beanConfigurationsList; - private Set, List>>> beanCreationEventListeners; - private Set, List>> beanPreDestroyEventListeners; - private Set, List>> beanDestroyedEventListeners; + List, ListenersSupplier>> beanInitializedEventListeners; + private List, ListenersSupplier>> beanCreationEventListeners; + private List, ListenersSupplier>> beanPreDestroyEventListeners; + private List, ListenersSupplier>> beanDestroyedEventListeners; @Nullable private MutableConversionService conversionService; @@ -1279,14 +1280,14 @@ private void destroyBean(@NonNull BeanRegistration registration, boolean @NonNull private T triggerPreDestroyListeners(@NonNull BeanDefinition beanDefinition, @NonNull T bean) { if (beanPreDestroyEventListeners == null) { - beanPreDestroyEventListeners = loadListeners(BeanPreDestroyEventListener.class).entrySet(); + beanPreDestroyEventListeners = loadListeners(BeanPreDestroyEventListener.class); } if (!beanPreDestroyEventListeners.isEmpty()) { Class beanType = getBeanType(beanDefinition); - for (Map.Entry, List> entry : beanPreDestroyEventListeners) { + for (Map.Entry, ListenersSupplier> entry : beanPreDestroyEventListeners) { if (entry.getKey().isAssignableFrom(beanType)) { final BeanPreDestroyEvent event = new BeanPreDestroyEvent<>(this, beanDefinition, bean); - for (BeanPreDestroyEventListener listener : entry.getValue()) { + for (BeanPreDestroyEventListener listener : entry.getValue().get(null)) { try { bean = Objects.requireNonNull( listener.onPreDestroy(event), @@ -1354,14 +1355,14 @@ private void destroyProxyTargetBean(@NonNull BeanRegistration registratio @NonNull private void triggerBeanDestroyedListeners(@NonNull BeanDefinition beanDefinition, @NonNull T bean) { if (beanDestroyedEventListeners == null) { - beanDestroyedEventListeners = loadListeners(BeanDestroyedEventListener.class).entrySet(); + beanDestroyedEventListeners = loadListeners(BeanDestroyedEventListener.class); } if (!beanDestroyedEventListeners.isEmpty()) { Class beanType = getBeanType(beanDefinition); - for (Map.Entry, List> entry : beanDestroyedEventListeners) { + for (Map.Entry, ListenersSupplier> entry : beanDestroyedEventListeners) { if (entry.getKey().isAssignableFrom(beanType)) { final BeanDestroyedEvent event = new BeanDestroyedEvent<>(this, beanDefinition, bean); - for (BeanDestroyedEventListener listener : entry.getValue()) { + for (BeanDestroyedEventListener listener : entry.getValue().get(null)) { try { listener.onDestroyed(event); } catch (Exception e) { @@ -1912,91 +1913,73 @@ protected Iterable resolveBeanConfigurations() { * Initialize the event listeners. */ protected void initializeEventListeners() { - final Map, List>> beanCreatedListeners = loadCreatedListeners(); - beanCreatedListeners.put(AnnotationProcessor.class, Collections.singletonList(new AnnotationProcessorListener())); - final Map, List> beanInitializedListeners = loadListeners(BeanInitializedEventListener.class); - this.beanCreationEventListeners = beanCreatedListeners.entrySet(); - this.beanInitializedEventListeners = beanInitializedListeners.entrySet(); - } - - private void handleEagerInitializedDependencies(BeanDefinition listener, - Argument listensTo, - List>> targets) { - if (LOG.isWarnEnabled()) { - List paths = new ArrayList<>(targets.size()); - for (List> line: targets) { - paths.add(" " + line.stream() - .map(Argument::getType) - .map(Class::getName) - .collect(Collectors.joining(AbstractBeanResolutionContext.DefaultPath.RIGHT_ARROW))); + this.beanCreationEventListeners = loadListeners(BeanCreatedEventListener.class); + // Keep anonymous class to avoid lambda overhead during the startup + this.beanCreationEventListeners.add(new AbstractMap.SimpleEntry<>(AnnotationProcessor.class, new ListenersSupplier() { + @Override + public Iterable get(BeanResolutionContext beanResolutionContext) { + return Collections.singletonList(new AnnotationProcessorListener()); } - LOG.warn("The bean created event listener {} will not be executed because one or more other bean created event listeners inject {}:\n" + - "{}\n" + - "Change at least one point in the path to be lazy initialized by injecting a provider to avoid this issue", listener.getBeanType().getName(), listensTo.getType().getName(), String.join("\n", paths)); - } + + })); + this.beanInitializedEventListeners = loadListeners(BeanInitializedEventListener.class); } @NonNull - private Map, List>> loadCreatedListeners() { - final Collection> beanDefinitions = getBeanDefinitions(BeanCreatedEventListener.class); - final HashMap, List>> typeToListener = new HashMap<>(beanDefinitions.size(), 1); - if (beanDefinitions.isEmpty()) { - return typeToListener; - } - final HashMap, List>>> invalidListeners = new HashMap<>(); - final HashMap, Argument> beanCreationTargets = new HashMap<>(); - for (BeanDefinition beanCreatedDefinition: beanDefinitions) { - List> typeArguments = beanCreatedDefinition.getTypeArguments(BeanCreatedEventListener.class); - Argument argument = CollectionUtils.last(typeArguments); - if (argument == null) { - argument = Argument.OBJECT_ARGUMENT; - } - beanCreationTargets.put(beanCreatedDefinition, argument); - } - for (BeanDefinition beanCreatedDefinition: beanDefinitions) { - try (ScanningBeanResolutionContext context = new ScanningBeanResolutionContext(beanCreatedDefinition, beanCreationTargets)) { - BeanCreatedEventListener listener = resolveBeanRegistration(context, beanCreatedDefinition).bean; - List> typeArguments = beanCreatedDefinition.getTypeArguments(BeanCreatedEventListener.class); - Argument argument = CollectionUtils.last(typeArguments); - if (argument == null) { - argument = Argument.OBJECT_ARGUMENT; - } - typeToListener.computeIfAbsent(argument.getType(), aClass -> new ArrayList<>(10)) - .add(listener); - Map, List>>> foundTargets = context.getFoundTargets(); - for (Map.Entry, List>>> entry: foundTargets.entrySet()) { - invalidListeners.computeIfAbsent(entry.getKey(), key -> new ArrayList<>()) - .addAll(entry.getValue()); + private List, ListenersSupplier>> loadListeners(@NonNull Class listenerType) { + final Map, List>> typeToListener = getTypeToListenerMap(listenerType); + if (typeToListener.isEmpty()) { + return new ArrayList<>(1); + } + List, ListenersSupplier>> eventToListeners = new ArrayList<>(typeToListener.size()); + for (Map.Entry, List>> e : typeToListener.entrySet()) { + eventToListeners.add(new AbstractMap.SimpleEntry<>(e.getKey(), new ListenersSupplier<>() { + + // The supplier can be triggered concurrently. + // We allow for the listeners collection to be initialized multiple times. + private volatile List listeners; + + @Override + public Iterable get(BeanResolutionContext beanResolutionContext) { + if (listeners == null) { + List> listenersDefinitions = e.getValue(); + List listeners = new ArrayList<>(listenersDefinitions.size()); + for (BeanDefinition listenersDefinition : listenersDefinitions) { + T listener; + if (beanResolutionContext == null) { + try (BeanResolutionContext context = newResolutionContext(listenersDefinition, null)) { + listener = resolveBeanRegistration(context, listenersDefinition).bean; + } + } else { + listener = resolveBeanRegistration(beanResolutionContext, listenersDefinition).bean; + } + listeners.add(listener); + } + OrderUtil.sort(listeners); + this.listeners = listeners; + } + return listeners; } - } - } - for (List> listeners: typeToListener.values()) { - OrderUtil.sort(listeners); - } - for (Map.Entry, List>>> entry: invalidListeners.entrySet()) { - handleEagerInitializedDependencies(entry.getKey(), beanCreationTargets.get(entry.getKey()), entry.getValue()); + })); } - return typeToListener; + return eventToListeners; } @NonNull - private Map, List> loadListeners(@NonNull Class listenerType) { + private Map, List>> getTypeToListenerMap(@NonNull Class listenerType) { final Collection> beanDefinitions = getBeanDefinitions(listenerType); - final HashMap, List> typeToListener = new HashMap<>(beanDefinitions.size(), 1); + if (beanDefinitions.isEmpty()) { + return Collections.emptyMap(); + } + final HashMap, List>> typeToListener = new HashMap<>(beanDefinitions.size(), 1); for (BeanDefinition beanCreatedDefinition : beanDefinitions) { - try (BeanResolutionContext context = newResolutionContext(beanCreatedDefinition, null)) { - T listener = resolveBeanRegistration(context, beanCreatedDefinition).bean; - List> typeArguments = beanCreatedDefinition.getTypeArguments(listenerType); - Argument argument = CollectionUtils.last(typeArguments); - if (argument == null) { - argument = Argument.OBJECT_ARGUMENT; - } - typeToListener.computeIfAbsent(argument.getType(), aClass -> new ArrayList<>(10)) - .add(listener); + List> typeArguments = beanCreatedDefinition.getTypeArguments(listenerType); + Argument argument = CollectionUtils.last(typeArguments); + if (argument == null) { + argument = Argument.OBJECT_ARGUMENT; } - } - for (List listenerList : typeToListener.values()) { - OrderUtil.sort(listenerList); + typeToListener.computeIfAbsent(argument.getType(), aClass -> new ArrayList<>(10)) + .add(beanCreatedDefinition); } return typeToListener; } @@ -2448,10 +2431,10 @@ private T triggerBeanCreatedEventListener(@NonNull BeanResolutionContext res @Nullable Qualifier finalQualifier) { Class beanType = beanDefinition.getBeanType(); if (!(bean instanceof BeanCreatedEventListener) && CollectionUtils.isNotEmpty(beanCreationEventListeners)) { - for (Map.Entry, List>> entry : beanCreationEventListeners) { + for (Map.Entry, ListenersSupplier> entry : beanCreationEventListeners) { if (entry.getKey().isAssignableFrom(beanType)) { BeanKey beanKey = new BeanKey<>(beanDefinition, finalQualifier); - for (BeanCreatedEventListener listener : entry.getValue()) { + for (BeanCreatedEventListener listener : entry.getValue().get(resolutionContext)) { bean = (T) listener.onCreated(new BeanCreatedEvent(this, beanDefinition, beanKey, bean)); if (bean == null) { throw new BeanInstantiationException(resolutionContext, "Listener [" + listener + "] returned null from onCreated event"); @@ -3930,6 +3913,28 @@ public R invoke(Object... arguments) { } } + /** + * Internal supplier of listeners. + * + * @param The listener type + * + * @author Denis Stepanov + * @since 4.0.0 + */ + @Internal + interface ListenersSupplier { + + /** + * Retrived the listeners lazily. + * + * @param beanResolutionContext The bean resolution context + * @return the collection of listeners + */ + @NonNull + Iterable get(@Nullable BeanResolutionContext beanResolutionContext); + + } + /** * Class used as a bean key. * From 0621a93a0a43db69be7a673422a64ff4274279b0 Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Wed, 11 Jan 2023 18:24:42 +0100 Subject: [PATCH 389/743] Allow requests if cors.enabled and request's Origin HTTP Header starts with localhost and resolved Micronaut Application host is also localhost (#8601) * fix: OK cors=true & origin and host = localhost Close #8560 --- .../tck/tests/cors/CorsSimpleRequestTest.java | 20 +++++++++++++++- .../SimpleRequestWithCorsNotEnabledTest.java | 24 ++++++++++++++++--- .../http/server/cors/CorsFilter.java | 7 ++++++ 3 files changed, 47 insertions(+), 4 deletions(-) diff --git a/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/cors/CorsSimpleRequestTest.java b/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/cors/CorsSimpleRequestTest.java index 9243ba09c3e..9cd73212c44 100644 --- a/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/cors/CorsSimpleRequestTest.java +++ b/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/cors/CorsSimpleRequestTest.java @@ -72,7 +72,6 @@ void corsSimpleRequestNotAllowedForLocalhostAndAny() throws IOException { (server, request) -> { RefreshCounter refreshCounter = server.getApplicationContext().getBean(RefreshCounter.class); assertEquals(0, refreshCounter.getRefreshCount()); - AssertionUtils.assertThrows(server, request, HttpResponseAssertion.builder() .status(HttpStatus.FORBIDDEN) .assertResponse(response -> assertFalse(response.getHeaders().contains("Vary"))) @@ -81,6 +80,25 @@ void corsSimpleRequestNotAllowedForLocalhostAndAny() throws IOException { }); } + /** + * It should not deny a cors request coming from a localhost origin if the micronaut application resolved host is localhost. + * @throws IOException scenario step fails + */ + @Test + void corsSimpleRequestAllowedForLocalhostAndOriginLocalhost() throws IOException { + asserts(SPECNAME, + Collections.singletonMap(PROPERTY_MICRONAUT_SERVER_CORS_ENABLED, StringUtils.TRUE), + createRequest("http://localhost:8000"), + (server, request) -> { + RefreshCounter refreshCounter = server.getApplicationContext().getBean(RefreshCounter.class); + assertEquals(0, refreshCounter.getRefreshCount()); + AssertionUtils.assertDoesNotThrow(server, request, HttpResponseAssertion.builder() + .status(HttpStatus.OK) + .build()); + assertEquals(1, refreshCounter.getRefreshCount()); + }); + } + /** * CORS Simple request for localhost can be allowed via configuration. * @throws IOException may throw the try for resources diff --git a/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/cors/SimpleRequestWithCorsNotEnabledTest.java b/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/cors/SimpleRequestWithCorsNotEnabledTest.java index 6ede80ac6b9..618b37bfcaa 100644 --- a/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/cors/SimpleRequestWithCorsNotEnabledTest.java +++ b/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/cors/SimpleRequestWithCorsNotEnabledTest.java @@ -53,7 +53,7 @@ public class SimpleRequestWithCorsNotEnabledTest { @Test void corsSimpleRequestNotAllowedForLocalhostAndAny() throws IOException { asserts(SPECNAME, - createRequest(), + createRequest("https://sdelamo.github.io"), (server, request) -> { RefreshCounter refreshCounter = server.getApplicationContext().getBean(RefreshCounter.class); assertEquals(0, refreshCounter.getRefreshCount()); @@ -66,7 +66,25 @@ void corsSimpleRequestNotAllowedForLocalhostAndAny() throws IOException { }); } - private static HttpRequest createRequest() { + /** + * It should not deny a cors request coming from a localhost origin if the micronaut application resolved host is localhost. + * @throws IOException scenario step fails + */ + @Test + void corsSimpleRequestAllowedForLocalhostAndOriginLocalhost() throws IOException { + asserts(SPECNAME, + createRequest("http://localhost:8000"), + (server, request) -> { + RefreshCounter refreshCounter = server.getApplicationContext().getBean(RefreshCounter.class); + assertEquals(0, refreshCounter.getRefreshCount()); + AssertionUtils.assertDoesNotThrow(server, request, HttpResponseAssertion.builder() + .status(HttpStatus.OK) + .build()); + assertEquals(1, refreshCounter.getRefreshCount()); + }); + } + + private static HttpRequest createRequest(String origin) { return HttpRequest.POST("/refresh", Collections.emptyMap()) .header("Accept", "*/*") .header("Accept-Encoding", "gzip, deflate, br") @@ -74,7 +92,7 @@ private static HttpRequest createRequest() { .header("Connection", "keep-alive") .header("Content-Length", "0") .header("Host", "localhost:8080") - .header("Origin", "https://sdelamo.github.io") + .header("Origin", origin) .header("sec-ch-ua", "\"Not?A_Brand\";v=\"8\", \"Chromium\";v=\"108\", \"Google Chrome\";v=\"108\"") .header("sec-ch-ua-mobile", "?0") .header("sec-ch-ua-platform", "\"macOS\"") diff --git a/http-server/src/main/java/io/micronaut/http/server/cors/CorsFilter.java b/http-server/src/main/java/io/micronaut/http/server/cors/CorsFilter.java index 46bed4c3c81..55cc1ca0ca0 100644 --- a/http-server/src/main/java/io/micronaut/http/server/cors/CorsFilter.java +++ b/http-server/src/main/java/io/micronaut/http/server/cors/CorsFilter.java @@ -128,6 +128,13 @@ protected boolean shouldDenyToPreventDriveByLocalhostAttack(@NonNull CorsOriginC if (httpHostResolver == null) { return false; } + String origin = request.getHeaders().getOrigin().orElse(null); + if (origin == null) { + return false; + } + if (origin.startsWith(LOCALHOST)) { + return false; + } String host = httpHostResolver.resolve(request); return isAny(corsOriginConfiguration.getAllowedOrigins()) && host.startsWith(LOCALHOST); From 31febbaaad6ae5782e1782e72412bd845cc9455f Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Wed, 11 Jan 2023 22:56:34 +0100 Subject: [PATCH 390/743] build: bump up Micronaut AWS to 3.10.5 (#8605) --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 9be21c14f53..7d537b525c9 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -67,7 +67,7 @@ managed-methvin-directory-watcher = "0.16.1" managed-micrometer = "1.9.4" managed-micronaut-acme = "3.2.0" managed-micronaut-aot = "1.1.1" -managed-micronaut-aws = "3.10.4" +managed-micronaut-aws = "3.10.5" managed-micronaut-azure = "4.0.0" managed-micronaut-cache = "3.5.0" managed-micronaut-cassandra = "5.1.1" From 463fca27df30926af84a8b76b76e80b6709d9205 Mon Sep 17 00:00:00 2001 From: micronaut-build <65172877+micronaut-build@users.noreply.github.com> Date: Thu, 12 Jan 2023 06:56:00 +0100 Subject: [PATCH 391/743] Bump micronaut-aws to 3.12.0 (#8606) --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 7d6c309f10f..e6bc776e888 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -67,7 +67,7 @@ managed-methvin-directory-watcher = "0.16.1" managed-micrometer = "1.9.4" managed-micronaut-acme = "3.2.0" managed-micronaut-aot = "1.1.1" -managed-micronaut-aws = "3.11.0" +managed-micronaut-aws = "3.12.0" managed-micronaut-azure = "4.0.0" managed-micronaut-cache = "3.5.0" managed-micronaut-cassandra = "5.1.1" From c57adca4bbe1f5c23c829026907276b4428ea3c0 Mon Sep 17 00:00:00 2001 From: Adam Kobor <28044849+adamkobor@users.noreply.github.com> Date: Thu, 12 Jan 2023 07:32:01 +0100 Subject: [PATCH 392/743] Add logger.config to loggingConfiguration.adoc (#8598) * Add logger.config to loggingConfiguration.adoc * move into its own section Co-authored-by: Sergio del Amo --- src/main/docs/guide/logging/loggingConfiguration.adoc | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/main/docs/guide/logging/loggingConfiguration.adoc b/src/main/docs/guide/logging/loggingConfiguration.adoc index 81e58c875b8..ef08b83cc8f 100644 --- a/src/main/docs/guide/logging/loggingConfiguration.adoc +++ b/src/main/docs/guide/logging/loggingConfiguration.adoc @@ -9,6 +9,16 @@ logger: The same configuration can be achieved by setting the environment variable `LOGGER_LEVELS_FOO_BAR`. Note that there is currently no way to set log levels for unconventional prefixes such as `foo.barBaz`. +==== Custom Logback XML Configuration + +[source,yaml] +---- +logger: + config: custom-logback.xml +---- + +You can also set a custom Logback XML configuration file to be used via `logger.config`. Be aware that **the referenced file should be an accessible resource on your classpath**! + ==== Disabling a Logger with Properties To disable a logger, you need to set the logger level to `OFF`: From 4b50658e168a2122aaeb90d4c1d6048dfc713d82 Mon Sep 17 00:00:00 2001 From: micronaut-build Date: Thu, 12 Jan 2023 08:30:04 +0000 Subject: [PATCH 393/743] [skip ci] Release v3.8.1 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index d8761ab707a..795fce1e300 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -projectVersion=3.8.1-SNAPSHOT +projectVersion=3.8.1 projectGroupId=io.micronaut title=Micronaut projectDesc=Natively Cloud Native From ec10d94ffd82de34daacf7cbd4b024a186e7baa0 Mon Sep 17 00:00:00 2001 From: micronaut-build Date: Thu, 12 Jan 2023 08:41:46 +0000 Subject: [PATCH 394/743] Back to 3.8.2-SNAPSHOT --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 795fce1e300..bfe03719198 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -projectVersion=3.8.1 +projectVersion=3.8.2-SNAPSHOT projectGroupId=io.micronaut title=Micronaut projectDesc=Natively Cloud Native From f0c62099ebaabc04fa78272f256e5144d19646d1 Mon Sep 17 00:00:00 2001 From: Denis Stepanov Date: Mon, 16 Jan 2023 16:18:26 +0100 Subject: [PATCH 395/743] Add default annotation values test + remove unused method (#8611) --- .../DefaultAnnotationValuesSpec.groovy | 69 +++++++++++++++++++ .../annotation/defaults/DefaultValues1.java | 18 +++++ .../annotation/defaults/DefaultValues2.java | 18 +++++ .../annotation/defaults/DefaultValues3.java | 18 +++++ .../micronaut/annotation/defaults/MyBean.java | 10 +++ .../inject/annotation/TestMetadata.java | 40 ----------- .../annotation/DefaultAnnotationMetadata.java | 13 ---- 7 files changed, 133 insertions(+), 53 deletions(-) create mode 100644 inject-java/src/test/groovy/io/micronaut/annotation/defaults/DefaultAnnotationValuesSpec.groovy create mode 100644 inject-java/src/test/groovy/io/micronaut/annotation/defaults/DefaultValues1.java create mode 100644 inject-java/src/test/groovy/io/micronaut/annotation/defaults/DefaultValues2.java create mode 100644 inject-java/src/test/groovy/io/micronaut/annotation/defaults/DefaultValues3.java create mode 100644 inject-java/src/test/groovy/io/micronaut/annotation/defaults/MyBean.java delete mode 100644 inject-java/src/test/groovy/io/micronaut/inject/annotation/TestMetadata.java diff --git a/inject-java/src/test/groovy/io/micronaut/annotation/defaults/DefaultAnnotationValuesSpec.groovy b/inject-java/src/test/groovy/io/micronaut/annotation/defaults/DefaultAnnotationValuesSpec.groovy new file mode 100644 index 00000000000..64d24d539d0 --- /dev/null +++ b/inject-java/src/test/groovy/io/micronaut/annotation/defaults/DefaultAnnotationValuesSpec.groovy @@ -0,0 +1,69 @@ +package io.micronaut.annotation.defaults + + +import io.micronaut.context.ApplicationContext +import io.micronaut.inject.annotation.AnnotationMetadataSupport +import spock.lang.Specification + +class DefaultAnnotationValuesSpec extends Specification { + + void "test which default values are preserved"() { + given: + ApplicationContext ctx = ApplicationContext.run() + + when: + def am = ctx.getBeanDefinition(MyBean).getAnnotationMetadata() + ctx.getBean(MyBean) != null + then: + noExceptionThrown() + + when: + def defaults1 = AnnotationMetadataSupport.getDefaultValues("io.micronaut.annotation.defaults.DefaultValues1") + def defaults1Annotation = am.getAnnotation("io.micronaut.annotation.defaults.DefaultValues1") + + then: "empty string default value is emitted" + am.stringValue("io.micronaut.annotation.defaults.DefaultValues1").isEmpty() + defaults1Annotation.stringValue().isEmpty() + defaults1Annotation.getRequiredValue("strings", String[].class).size() == 0 + defaults1.size() == 2 + defaults1["strings"].length == 0 + defaults1["ints"].length == 0 + am.stringValues("io.micronaut.annotation.defaults.DefaultValues1", "strings").size() == 0 + + when: + defaults1Annotation.getRequiredValue(String.class) + then: "Exception even so the default is provided" + thrown(IllegalStateException) + + when: + def defaults2 = AnnotationMetadataSupport.getDefaultValues("io.micronaut.annotation.defaults.DefaultValues2") + def defaults2Annotation = am.getAnnotation("io.micronaut.annotation.defaults.DefaultValues2") + + then: + am.stringValue("io.micronaut.annotation.defaults.DefaultValues2").isEmpty() + defaults2Annotation.stringValue().isEmpty() + defaults2Annotation.getRequiredValue(String.class) == "xyz" + defaults2.size() == 3 + defaults2["value"] == "xyz" + defaults2["strings"].length == 0 + defaults2["ints"].length == 0 + + when: + def defaults3 = AnnotationMetadataSupport.getDefaultValues("io.micronaut.annotation.defaults.DefaultValues3") + def defaults3Annotation = am.getAnnotation("io.micronaut.annotation.defaults.DefaultValues3") + then: + am.stringValue("io.micronaut.annotation.defaults.DefaultValues3").isEmpty() + defaults3Annotation.stringValue().isEmpty() + defaults3Annotation.stringValues("strings").size() == 0 + defaults3Annotation.getRequiredValue("strings", String[].class).size() == 1 + defaults3Annotation.getRequiredValue("strings", String[].class)[0] == "" + defaults3.size() == 2 + defaults3["strings"].length == 1 + defaults3["strings"][0] == "" + defaults2["ints"].length == 0 + + cleanup: + ctx.close() + } + +} diff --git a/inject-java/src/test/groovy/io/micronaut/annotation/defaults/DefaultValues1.java b/inject-java/src/test/groovy/io/micronaut/annotation/defaults/DefaultValues1.java new file mode 100644 index 00000000000..3666b82f78f --- /dev/null +++ b/inject-java/src/test/groovy/io/micronaut/annotation/defaults/DefaultValues1.java @@ -0,0 +1,18 @@ +package io.micronaut.annotation.defaults; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target({ElementType.TYPE, ElementType.ANNOTATION_TYPE, ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER}) +@Retention(RetentionPolicy.RUNTIME) +public @interface DefaultValues1 { + + String value() default ""; + + String[] strings() default {}; + + int[] ints() default {}; + +} diff --git a/inject-java/src/test/groovy/io/micronaut/annotation/defaults/DefaultValues2.java b/inject-java/src/test/groovy/io/micronaut/annotation/defaults/DefaultValues2.java new file mode 100644 index 00000000000..f63645ffb68 --- /dev/null +++ b/inject-java/src/test/groovy/io/micronaut/annotation/defaults/DefaultValues2.java @@ -0,0 +1,18 @@ +package io.micronaut.annotation.defaults; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target({ElementType.TYPE, ElementType.ANNOTATION_TYPE, ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER}) +@Retention(RetentionPolicy.RUNTIME) +public @interface DefaultValues2 { + + String value() default "xyz"; + + String[] strings() default {}; + + int[] ints() default {}; + +} diff --git a/inject-java/src/test/groovy/io/micronaut/annotation/defaults/DefaultValues3.java b/inject-java/src/test/groovy/io/micronaut/annotation/defaults/DefaultValues3.java new file mode 100644 index 00000000000..10841de25cc --- /dev/null +++ b/inject-java/src/test/groovy/io/micronaut/annotation/defaults/DefaultValues3.java @@ -0,0 +1,18 @@ +package io.micronaut.annotation.defaults; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target({ElementType.TYPE, ElementType.ANNOTATION_TYPE, ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER}) +@Retention(RetentionPolicy.RUNTIME) +public @interface DefaultValues3 { + + String value() default ""; + + String[] strings() default {""}; + + int[] ints() default {}; + +} diff --git a/inject-java/src/test/groovy/io/micronaut/annotation/defaults/MyBean.java b/inject-java/src/test/groovy/io/micronaut/annotation/defaults/MyBean.java new file mode 100644 index 00000000000..52ea54d628a --- /dev/null +++ b/inject-java/src/test/groovy/io/micronaut/annotation/defaults/MyBean.java @@ -0,0 +1,10 @@ +package io.micronaut.annotation.defaults; + +import jakarta.inject.Singleton; + +@Singleton +@DefaultValues1 +@DefaultValues2 +@DefaultValues3 +public class MyBean { +} diff --git a/inject-java/src/test/groovy/io/micronaut/inject/annotation/TestMetadata.java b/inject-java/src/test/groovy/io/micronaut/inject/annotation/TestMetadata.java deleted file mode 100644 index 0087e424732..00000000000 --- a/inject-java/src/test/groovy/io/micronaut/inject/annotation/TestMetadata.java +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright 2017-2020 original authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.micronaut.inject.annotation; - -import io.micronaut.core.annotation.AnnotationClassValue; -import io.micronaut.core.annotation.AnnotationUtil; -import reactor.core.publisher.Flux; - -public class TestMetadata extends DefaultAnnotationMetadata { - public TestMetadata() { - } - - static { - if (!DefaultAnnotationMetadata.areAnnotationDefaultsRegistered("io.micronaut.context.annotation.Requires")) { - DefaultAnnotationMetadata.registerAnnotationDefaults("io.micronaut.context.annotation.Requires", AnnotationUtil.internMapOf(new Object[]{"missing", new Object[0], "notEnv", new Object[0], "missingConfigurations", new Object[0], "entities", new Object[0], "missingBeans", new Object[0], "condition", $micronaut_load_class_value_2(), "env", new Object[0], "classes", new Object[0], "sdk", "MICRONAUT", "beans", new Object[0]})); - } - } - - static AnnotationClassValue $micronaut_load_class_value_2() { - try { - return new AnnotationClassValue(Flux.class); - } catch (Throwable e) { - return new AnnotationClassValue("io.reactivex.Flowable"); - } - } -} - diff --git a/inject/src/main/java/io/micronaut/inject/annotation/DefaultAnnotationMetadata.java b/inject/src/main/java/io/micronaut/inject/annotation/DefaultAnnotationMetadata.java index eaf57a12afb..7b28009a993 100644 --- a/inject/src/main/java/io/micronaut/inject/annotation/DefaultAnnotationMetadata.java +++ b/inject/src/main/java/io/micronaut/inject/annotation/DefaultAnnotationMetadata.java @@ -1548,19 +1548,6 @@ protected final void addDefaultAnnotationValues(String annotation, Map Date: Mon, 16 Jan 2023 17:46:19 +0100 Subject: [PATCH 396/743] doc: Add documentation for virtual thread support (#8577) See https://github.com/micronaut-projects/micronaut-core/pull/8180 Close https://github.com/micronaut-projects/micronaut-core/issues/8520 --- .../threadPools/blockingOperations.adoc | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/src/main/docs/guide/httpServer/serverConfiguration/threadPools/blockingOperations.adoc b/src/main/docs/guide/httpServer/serverConfiguration/threadPools/blockingOperations.adoc index 903c5608200..2dc5f1849b7 100644 --- a/src/main/docs/guide/httpServer/serverConfiguration/threadPools/blockingOperations.adoc +++ b/src/main/docs/guide/httpServer/serverConfiguration/threadPools/blockingOperations.adoc @@ -10,4 +10,22 @@ micronaut: nThreads: 75 ---- -The above configuration creates a fixed thread pool with 75 threads. \ No newline at end of file +The above configuration creates a fixed thread pool with 75 threads. + +== Virtual Threads + +Since Java 19, the JVM includes experimental support for virtual threads ("project loom"). As it is a preview feature, you need to pass `--enable-preview` as a JVM parameter to enable it. + +The Micronaut framework will detect virtual thread support and use it for the executor named `blocking` if available. If virtual threads are not supported, this executor will be aliased to the `io` thread pool. + +To use the `blocking` executor, simply mark e.g. a controller with `ExecuteOn`: + +.Configuring the Server I/O Thread Pool +[source,java] +---- +@ExecuteOn(TaskExecutors.BLOCKING) +@GET +String hello() { + return "foo" +} +---- From 6b8fa8a693a2c311663319734bea80c2ec9ab846 Mon Sep 17 00:00:00 2001 From: Denis Stepanov Date: Mon, 16 Jan 2023 19:17:43 +0100 Subject: [PATCH 397/743] Performance improvements + Java style improvements (#8586) --- .../aop/chain/AbstractInterceptorChain.java | 84 +- .../aop/chain/DefaultInterceptorRegistry.java | 184 ++-- benchmarks/build.gradle | 10 +- .../http/server/StartupBenchmark.java | 13 +- .../micronaut/supplier/SupplierBenchmark.java | 91 ++ .../buffer/netty/NettyByteBufferFactory.java | 26 +- .../env/ConfigurationIntroductionAdvice.java | 9 +- .../annotation/AnnotationMetadataWriter.java | 28 +- .../core/annotation/AnnotationUtil.java | 4 +- .../core/annotation/AnnotationValue.java | 845 ++++++++++-------- .../ImmutableSortedStringsArrayMap.java | 3 +- .../micronaut/core/convert/TypeConverter.java | 8 +- .../java/io/micronaut/core/io/IOUtils.java | 36 +- .../file/DefaultFileSystemResourceLoader.java | 2 +- .../io/file/FileSystemResourceLoader.java | 7 +- .../core/io/service/ServiceScanner.java | 29 +- .../core/io/service/SoftServiceLoader.java | 15 +- .../io/micronaut/core/reflect/ClassUtils.java | 40 +- .../java/io/micronaut/core/type/Argument.java | 57 +- .../micronaut/core/type/DefaultArgument.java | 25 +- .../core/type/RuntimeTypeInformation.java | 72 +- .../micronaut/core/type/TypeInformation.java | 2 +- .../micronaut/core/util/CollectionUtils.java | 51 ++ .../core/util/EnvironmentProperties.java | 9 +- .../io/micronaut/core/util/StreamUtils.java | 5 + .../io/micronaut/core/util/SupplierUtil.java | 68 +- .../core/value/MapPropertyResolver.java | 30 +- .../DefaultConversionServiceSpec.groovy | 4 +- .../micronaut/core/type/ArgumentSpec.groovy | 22 +- .../GraalReflectionMetadataWriter.java | 8 +- gradle/libs.versions.toml | 4 +- .../micronaut/http/client/HttpGetSpec.groovy | 2 +- .../micronaut/http/client/HttpHeadSpec.groovy | 2 +- .../micronaut/http/client/HttpPostSpec.groovy | 2 +- .../http/client/StreamRequestSpec.groovy | 15 +- .../docs/basics/HelloControllerTest.java | 2 +- .../KQueueChannelOptionFactory.java | 4 +- .../http/server/netty/NettyHttpServer.java | 6 +- .../netty/binders/NettyBinderRegistrar.java | 4 +- .../netty/converters/NettyConverters.java | 12 +- .../discovery/NettyServiceDiscovery.java | 51 +- .../server/netty/NettyStartStopSpec.groovy | 6 +- .../converters/HttpConverterRegistrar.java | 13 +- .../nullreturn/NullReturnFactorySpec.groovy | 44 +- .../factory/nullreturn/NullableFactory.java | 20 +- .../ProxyBeanWithPreDestroySpec.groovy | 13 +- .../proxybeanwithpredestroy/package-info.java | 5 + ...rgetPrototypeBeanWithPreDestroySpec.groovy | 17 +- .../package-info.java | 5 + .../ProxyTargetBeanWithPreDestroySpec.groovy | 18 +- .../package-info.java | 5 + .../AbstractInitializableBeanDefinition.java | 72 +- ...tInitializableBeanDefinitionReference.java | 8 +- .../context/DefaultApplicationContext.java | 17 +- .../DefaultApplicationContextBuilder.java | 4 +- .../micronaut/context/DefaultBeanContext.java | 563 ++++++------ .../io/micronaut/context/DisabledBean.java | 5 + .../micronaut/context/RequiresCondition.java | 162 ++-- .../converters/ContextConverterRegistrar.java | 4 +- .../context/env/DefaultEnvironment.java | 17 +- .../env/PropertySourcePropertyResolver.java | 28 +- .../ApplicationEventPublisherFactory.java | 4 +- .../AnnotationMetadataHierarchy.java | 34 +- .../annotation/AnnotationMetadataSupport.java | 83 +- .../annotation/DefaultAnnotationMetadata.java | 378 ++++---- .../annotation/MutableAnnotationMetadata.java | 4 +- .../provider/AbstractProviderDefinition.java | 5 + .../InterceptorBindingQualifier.java | 70 +- .../convert/JacksonConverterRegistrar.java | 4 +- .../json/convert/JsonConverterRegistrar.java | 4 +- .../docs/basics/HelloControllerSpec.kt | 2 +- .../micronaut/validation/ValidatedSpec.groovy | 10 +- 72 files changed, 2000 insertions(+), 1515 deletions(-) create mode 100644 benchmarks/src/jmh/java/io/micronaut/supplier/SupplierBenchmark.java create mode 100644 inject-java/src/test/groovy/io/micronaut/inject/lifecycle/proxybeanwithpredestroy/package-info.java create mode 100644 inject-java/src/test/groovy/io/micronaut/inject/lifecycle/proxytargetbeanprototypewithpredestroy/package-info.java create mode 100644 inject-java/src/test/groovy/io/micronaut/inject/lifecycle/proxytargetbeanwithpredestroy/package-info.java diff --git a/aop/src/main/java/io/micronaut/aop/chain/AbstractInterceptorChain.java b/aop/src/main/java/io/micronaut/aop/chain/AbstractInterceptorChain.java index c1380f08a0f..d7116d782be 100644 --- a/aop/src/main/java/io/micronaut/aop/chain/AbstractInterceptorChain.java +++ b/aop/src/main/java/io/micronaut/aop/chain/AbstractInterceptorChain.java @@ -34,14 +34,11 @@ import java.util.Collection; import java.util.Collections; -import java.util.HashSet; -import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; -import java.util.stream.Collectors; /** * Abstract interceptor chain implementation. @@ -99,11 +96,11 @@ abstract class AbstractInterceptorChain implements InvocationContext localParameters = this.parameters; if (localParameters == null) { Argument[] arguments = getArguments(); - localParameters = new LinkedHashMap<>(arguments.length); + localParameters = CollectionUtils.newLinkedHashMap(arguments.length); for (int i = 0; i < arguments.length; i++) { Argument argument = arguments[i]; int finalIndex = i; - localParameters.put(argument.getName(), new MutableArgumentValue() { + localParameters.put(argument.getName(), new MutableArgumentValue<>() { @Override public AnnotationMetadata getAnnotationMetadata() { return argument.getAnnotationMetadata(); @@ -186,60 +183,59 @@ public R proceed(@NonNull Interceptor from) throws RuntimeException { * @since 3.3.0 */ protected static @NonNull Collection> resolveInterceptorValues(@NonNull AnnotationMetadata annotationMetadata, - @NonNull InterceptorKind kind) { + @NonNull InterceptorKind kind) { annotationMetadata = annotationMetadata.getTargetAnnotationMetadata(); - if (annotationMetadata instanceof AnnotationMetadataHierarchy) { + if (annotationMetadata instanceof AnnotationMetadataHierarchy annotationMetadataHierarchy) { final List> declaredValues = - annotationMetadata.getDeclaredMetadata().getAnnotationValuesByType(InterceptorBinding.class); + annotationMetadataHierarchy.getDeclaredMetadata().getAnnotationValuesByType(InterceptorBinding.class); final List> parentValues = - ((AnnotationMetadataHierarchy) annotationMetadata).getRootMetadata() + annotationMetadataHierarchy.getRootMetadata() .getAnnotationValuesByType(InterceptorBinding.class); - if (CollectionUtils.isNotEmpty(declaredValues) || CollectionUtils.isNotEmpty(parentValues)) { - Set> resolved = new HashSet<>(declaredValues.size() + parentValues.size()); - Set declared = new HashSet<>(declaredValues.size()); - for (AnnotationValue declaredValue : declaredValues) { - final String annotationName = declaredValue.stringValue().orElse(null); - if (annotationName != null) { - final InterceptorKind specifiedkind = declaredValue.enumValue("kind", InterceptorKind.class).orElse(null); - if (specifiedkind == null || specifiedkind.equals(kind)) { - if (!annotationMetadata.isRepeatableAnnotation(annotationName)) { - declared.add(annotationName); - } - resolved.add(declaredValue); + if (CollectionUtils.isEmpty(declaredValues) && CollectionUtils.isEmpty(parentValues)) { + return Collections.emptyList(); + } + Set> resolved = CollectionUtils.newHashSet(declaredValues.size() + parentValues.size()); + Set declared = CollectionUtils.newHashSet(declaredValues.size()); + for (AnnotationValue declaredValue : declaredValues) { + final String annotationName = declaredValue.stringValue().orElse(null); + if (annotationName != null) { + final InterceptorKind specifiedKind = declaredValue.enumValue("kind", InterceptorKind.class).orElse(null); + if (specifiedKind == null || specifiedKind.equals(kind)) { + if (!annotationMetadata.isRepeatableAnnotation(annotationName)) { + declared.add(annotationName); } + resolved.add(declaredValue); } } - for (AnnotationValue parentValue : parentValues) { - final String annotationName = parentValue.stringValue().orElse(null); - if (annotationName != null && !declared.contains(annotationName)) { - final InterceptorKind specifiedkind = parentValue.enumValue("kind", InterceptorKind.class).orElse(null); - if (specifiedkind == null || specifiedkind.equals(kind)) { - resolved.add(parentValue); - } + } + for (AnnotationValue parentValue : parentValues) { + final String annotationName = parentValue.stringValue().orElse(null); + if (annotationName != null && !declared.contains(annotationName)) { + final InterceptorKind specifiedKind = parentValue.enumValue("kind", InterceptorKind.class).orElse(null); + if (specifiedKind == null || specifiedKind.equals(kind)) { + resolved.add(parentValue); } } - - return resolved; - } else { - return Collections.emptyList(); } + + return resolved; } else { List> bindings = annotationMetadata .getAnnotationValuesByType(InterceptorBinding.class); - if (CollectionUtils.isNotEmpty(bindings)) { - return bindings - .stream() - .filter(av -> { - if (!av.stringValue().isPresent()) { - return false; - } - final InterceptorKind specifiedkind = av.enumValue("kind", InterceptorKind.class).orElse(null); - return specifiedkind == null || specifiedkind.equals(kind); - }) - .collect(Collectors.toSet()); - } else { + if (CollectionUtils.isEmpty(bindings)) { return Collections.emptyList(); } + Set> selectedBindings = CollectionUtils.newHashSet(bindings.size()); + for (AnnotationValue av : bindings) { + if (av.stringValue().isEmpty()) { + continue; + } + final InterceptorKind specifiedKind = av.enumValue("kind", InterceptorKind.class).orElse(null); + if (specifiedKind == null || specifiedKind.equals(kind)) { + selectedBindings.add(av); + } + } + return selectedBindings; } } } diff --git a/aop/src/main/java/io/micronaut/aop/chain/DefaultInterceptorRegistry.java b/aop/src/main/java/io/micronaut/aop/chain/DefaultInterceptorRegistry.java index ded51e71a62..44410c571b7 100644 --- a/aop/src/main/java/io/micronaut/aop/chain/DefaultInterceptorRegistry.java +++ b/aop/src/main/java/io/micronaut/aop/chain/DefaultInterceptorRegistry.java @@ -38,9 +38,9 @@ import org.slf4j.LoggerFactory; import java.lang.annotation.Annotation; +import java.util.ArrayList; import java.util.Collection; import java.util.List; -import java.util.stream.Stream; import static io.micronaut.inject.qualifiers.InterceptorBindingQualifier.META_MEMBER_MEMBERS; @@ -68,36 +68,32 @@ public DefaultInterceptorRegistry(BeanContext beanContext) { final AnnotationMetadata annotationMetadata = method.getAnnotationMetadata(); if (interceptors.isEmpty()) { return resolveToNone((ExecutableMethod) method, interceptorKind, annotationMetadata); - } else { - instrumentAnnotationMetadata(beanContext, method); - final Collection> applicableBindings - = AbstractInterceptorChain.resolveInterceptorValues( - annotationMetadata, - interceptorKind - ); - if (applicableBindings.isEmpty()) { - return resolveToNone((ExecutableMethod) method, interceptorKind, annotationMetadata); - } else { - @SuppressWarnings({"unchecked", - "rawtypes"}) final Interceptor[] resolvedInterceptors = - (Interceptor[]) interceptorStream( - method.getDeclaringType(), - (Collection) interceptors, - interceptorKind, - applicableBindings - ).filter(bean -> (bean instanceof MethodInterceptor) || !(bean instanceof ConstructorInterceptor)) - .toArray(Interceptor[]::new); - if (LOG.isTraceEnabled()) { - LOG.trace("Resolved {} {} interceptors out of a possible {} for method: {} - {}", resolvedInterceptors.length, interceptorKind, interceptors.size(), method.getDeclaringType(), method instanceof Described ? ((Described) method).getDescription(true) : method.toString()); - for (int i = 0; i < resolvedInterceptors.length; i++) { - Interceptor resolvedInterceptor = resolvedInterceptors[i]; - LOG.trace("Interceptor {} - {}", i, resolvedInterceptor); - } - } - //noinspection unchecked - return resolvedInterceptors; + } + instrumentAnnotationMetadata(beanContext, method); + final Collection> applicableBindings + = AbstractInterceptorChain.resolveInterceptorValues( + annotationMetadata, + interceptorKind + ); + if (applicableBindings.isEmpty()) { + return resolveToNone((ExecutableMethod) method, interceptorKind, annotationMetadata); + } + final Interceptor[] resolvedInterceptors = findInterceptors( + method.getDeclaringType(), + interceptors, + interceptorKind, + applicableBindings, + true, + false + ); + if (LOG.isTraceEnabled()) { + LOG.trace("Resolved {} {} interceptors out of a possible {} for method: {} - {}", resolvedInterceptors.length, interceptorKind, interceptors.size(), method.getDeclaringType(), method instanceof Described ? ((Described) method).getDescription(true) : method.toString()); + for (int i = 0; i < resolvedInterceptors.length; i++) { + Interceptor resolvedInterceptor = resolvedInterceptors[i]; + LOG.trace("Interceptor {} - {}", i, resolvedInterceptor); } } + return resolvedInterceptors; } @SuppressWarnings("rawtypes") @@ -116,49 +112,69 @@ private Interceptor[] resolveToNone(ExecutableMethod method, } } - private Stream> interceptorStream(Class declaringType, - Collection>> interceptors, - InterceptorKind interceptorKind, - Collection> interceptPointBindings) { - return interceptors.stream() - .filter(beanRegistration -> { - final List> typeArgs = beanRegistration.getBeanDefinition().getTypeArguments(ConstructorInterceptor.class); - if (typeArgs.isEmpty()) { - return true; - } else { - final Class applicableType = typeArgs.iterator().next().getType(); - return applicableType.isAssignableFrom(declaringType); - } - }) - .filter(beanRegistration -> { - // does the annotation metadata contain @InterceptorBinding(interceptorType=SomeInterceptor.class) - // this behaviour is in place for backwards compatible for the old @Type(SomeInterceptor.class) approach - // In this case we don't care about any qualifiers - for (AnnotationValue applicableValue : interceptPointBindings) { - if (isApplicableByType(beanRegistration, applicableValue)) { - return true; - } - } - // these are the binding declared on the interceptor itself - // an interceptor can declare one or more bindings - final Collection> interceptorValues = AbstractInterceptorChain - .resolveInterceptorValues( - beanRegistration.getBeanDefinition().getAnnotationMetadata(), interceptorKind - ); - if (interceptorValues.isEmpty()) { - // Bean is an interceptor but no bindings??? - return false; - } - // loop through the bindings on the interceptor and make sure that - // the intercept point has the same once - for (AnnotationValue interceptorAnnotationValue : interceptorValues) { - if (!matches(interceptorAnnotationValue, interceptPointBindings)) { - return false; - } - } + private Interceptor[] findInterceptors(Class declaringType, + Collection>> interceptors, + InterceptorKind interceptorKind, + Collection> interceptPointBindings, + boolean selectMethodInterceptor, + boolean selectConstructorInterceptor) { + List>> selectedInterceptorRegistrations = new ArrayList<>(interceptors.size()); + for (BeanRegistration> beanRegistration : interceptors) { + if (selectInterceptor(declaringType, interceptorKind, interceptPointBindings, beanRegistration)) { + selectedInterceptorRegistrations.add(beanRegistration); + } + } + selectedInterceptorRegistrations.sort(OrderUtil.COMPARATOR); + + List> selectedInterceptors = new ArrayList<>(selectedInterceptorRegistrations.size()); + for (BeanRegistration> beanRegistration : selectedInterceptorRegistrations) { + Interceptor bean = beanRegistration.getBean(); + if (selectMethodInterceptor && (bean instanceof MethodInterceptor || !(bean instanceof ConstructorInterceptor)) + || selectConstructorInterceptor && (bean instanceof ConstructorInterceptor || !(bean instanceof MethodInterceptor))) { + selectedInterceptors.add(bean); + } + } + return selectedInterceptors.toArray(new Interceptor[0]); + } + + private boolean selectInterceptor(Class declaringType, + InterceptorKind interceptorKind, + Collection> interceptPointBindings, + BeanRegistration> beanRegistration) { + final List> typeArgs = beanRegistration.getBeanDefinition().getTypeArguments(ConstructorInterceptor.class); + if (!typeArgs.isEmpty()) { + final Class applicableType = typeArgs.iterator().next().getType(); + if (!applicableType.isAssignableFrom(declaringType)) { + return false; + } + } + + // does the annotation metadata contain @InterceptorBinding(interceptorType=SomeInterceptor.class) + // this behaviour is in place for backwards compatible for the old @Type(SomeInterceptor.class) approach + // In this case we don't care about any qualifiers + for (AnnotationValue applicableValue : interceptPointBindings) { + if (isApplicableByType(beanRegistration, applicableValue)) { return true; - }).sorted(OrderUtil.COMPARATOR) - .map(BeanRegistration::getBean); + } + } + // these are the binding declared on the interceptor itself + // an interceptor can declare one or more bindings + final Collection> interceptorValues = AbstractInterceptorChain + .resolveInterceptorValues( + beanRegistration.getBeanDefinition().getAnnotationMetadata(), interceptorKind + ); + if (interceptorValues.isEmpty()) { + // Bean is an interceptor but no bindings??? + return false; + } + // loop through the bindings on the interceptor and make sure that + // the intercept point has the same once + for (AnnotationValue interceptorAnnotationValue : interceptorValues) { + if (!matches(interceptorAnnotationValue, interceptPointBindings)) { + return false; + } + } + return true; } private boolean matches(AnnotationValue interceptorAnnotationValue, Collection> interceptPointBindings) { @@ -187,7 +203,7 @@ private boolean matches(AnnotationValue interceptorAnnotationValue, Collectio return false; } - private boolean isApplicableByType(BeanRegistration> beanRegistration, + private boolean isApplicableByType(BeanRegistration> beanRegistration, AnnotationValue applicableValue) { return applicableValue.classValue("interceptorType") .map(t -> t.isInstance(beanRegistration.getBean())).orElse(false); @@ -204,14 +220,14 @@ public Interceptor[] resolveConstructorInterceptors( constructor.getAnnotationMetadata(), InterceptorKind.AROUND_CONSTRUCT ); - final Interceptor[] resolvedInterceptors = interceptorStream( - constructor.getDeclaringBeanType(), - interceptors, - InterceptorKind.AROUND_CONSTRUCT, - applicableBindings - ) - .filter(bean -> (bean instanceof ConstructorInterceptor) || !(bean instanceof MethodInterceptor)) - .toArray(Interceptor[]::new); + final Interceptor[] resolvedInterceptors = findInterceptors( + constructor.getDeclaringBeanType(), + (Collection) interceptors, + InterceptorKind.AROUND_CONSTRUCT, + applicableBindings, + false, + true + ); if (LOG.isTraceEnabled()) { LOG.trace("Resolved {} {} interceptors out of a possible {} for constructor: {} - {}", resolvedInterceptors.length, InterceptorKind.AROUND_CONSTRUCT, interceptors.size(), constructor.getDeclaringBeanType(), constructor.getDescription(true)); for (int i = 0; i < resolvedInterceptors.length; i++) { @@ -219,16 +235,14 @@ public Interceptor[] resolveConstructorInterceptors( LOG.trace("Interceptor {} - {}", i, resolvedInterceptor); } } - //noinspection unchecked return resolvedInterceptors; } private static void instrumentAnnotationMetadata(BeanContext beanContext, Object method) { - if (beanContext instanceof ApplicationContext && method instanceof EnvironmentConfigurable) { + if (beanContext instanceof ApplicationContext applicationContext && method instanceof EnvironmentConfigurable environmentConfigurable) { // ensure metadata is environment aware - final EnvironmentConfigurable m = (EnvironmentConfigurable) method; - if (m.hasPropertyExpressions()) { - m.configure(((ApplicationContext) beanContext).getEnvironment()); + if (environmentConfigurable.hasPropertyExpressions()) { + environmentConfigurable.configure(applicationContext.getEnvironment()); } } } diff --git a/benchmarks/build.gradle b/benchmarks/build.gradle index 0a31c534fba..f2c38f5d344 100644 --- a/benchmarks/build.gradle +++ b/benchmarks/build.gradle @@ -7,7 +7,8 @@ dependencies { annotationProcessor project(":inject-java") jmhAnnotationProcessor project(":inject-java") jmhAnnotationProcessor libs.bundles.asm - + jmhAnnotationProcessor libs.jmh.generator.annprocess + annotationProcessor project(":validation") compileOnly project(":validation") api project(":inject") @@ -17,16 +18,11 @@ dependencies { api project(":router") api project(":runtime") - jmh libs.jmh - jmh libs.jmh.generator.annprocess + jmh libs.jmh.core } jmh { includes = ['io.micronaut.http.server.StartupBenchmark'] duplicateClassesStrategy = DuplicatesStrategy.WARN - warmupIterations = 2 - iterations = 3 - fork = 1 -// jvmArgs = ["-agentpath:/Applications/YourKit-Java-Profiler-2018.04.app/Contents/Resources/bin/mac/libyjpagent.jnilib"] } tasks.named("processJmhResources") { diff --git a/benchmarks/src/jmh/java/io/micronaut/http/server/StartupBenchmark.java b/benchmarks/src/jmh/java/io/micronaut/http/server/StartupBenchmark.java index 011e93d4200..0ddd65fe110 100644 --- a/benchmarks/src/jmh/java/io/micronaut/http/server/StartupBenchmark.java +++ b/benchmarks/src/jmh/java/io/micronaut/http/server/StartupBenchmark.java @@ -18,17 +18,20 @@ import io.micronaut.context.ApplicationContext; import io.micronaut.http.server.binding.TestController; import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Mode; import org.openjdk.jmh.annotations.Scope; import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.infra.Blackhole; @State(Scope.Benchmark) +@BenchmarkMode(Mode.Throughput) public class StartupBenchmark { @Benchmark - public void startup() { - try (ApplicationContext context = ApplicationContext.run()) { - final TestController controller = - context.getBean(TestController.class); - } + public void startup(Blackhole blackhole) { + ApplicationContext context = ApplicationContext.run(); + final TestController controller = context.getBean(TestController.class); + blackhole.consume(controller); } } diff --git a/benchmarks/src/jmh/java/io/micronaut/supplier/SupplierBenchmark.java b/benchmarks/src/jmh/java/io/micronaut/supplier/SupplierBenchmark.java new file mode 100644 index 00000000000..e16992d83d7 --- /dev/null +++ b/benchmarks/src/jmh/java/io/micronaut/supplier/SupplierBenchmark.java @@ -0,0 +1,91 @@ +package io.micronaut.supplier; + +import io.micronaut.core.util.SupplierUtil; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.infra.Blackhole; + +import java.util.Optional; +import java.util.function.Supplier; + +@State(Scope.Benchmark) +@BenchmarkMode(Mode.Throughput) +public class SupplierBenchmark { + + private Supplier memoizedLambda = memoizedUsingLambda(() -> "test"); + private Supplier memoizedNonEmptyUsingLambda = memoizedNonEmptyUsingLambda(() -> "test"); + private Supplier memoized = SupplierUtil.memoized(() -> "test"); + private Supplier memoizedNonEmpty = SupplierUtil.memoizedNonEmpty(() -> "test"); + + @Benchmark + public void memoizedLambda(Blackhole blackhole) { + blackhole.consume(memoizedLambda.get()); + } + + @Benchmark + public void memoizedNonEmptyUsingLambda(Blackhole blackhole) { + blackhole.consume(memoizedNonEmptyUsingLambda.get()); + } + + @Benchmark + public void memoized(Blackhole blackhole) { + blackhole.consume(memoized.get()); + } + + + @Benchmark + public void memoizedNonEmpty(Blackhole blackhole) { + blackhole.consume(memoizedNonEmpty.get()); + } + + private static Supplier memoizedUsingLambda(Supplier actual) { + return new Supplier() { + Supplier delegate = this::initialize; + boolean initialized; + + @Override + public T get() { + return delegate.get(); + } + + private synchronized T initialize() { + if (!initialized) { + T value = actual.get(); + delegate = () -> value; + initialized = true; + } + return delegate.get(); + } + }; + } + + private static Supplier memoizedNonEmptyUsingLambda(Supplier actual) { + return new Supplier() { + Supplier delegate = this::initialize; + boolean initialized; + + @Override + public T get() { + return delegate.get(); + } + + private synchronized T initialize() { + if (!initialized) { + T value = actual.get(); + if (value == null) { + return null; + } + if (value instanceof Optional && !((Optional) value).isPresent()) { + return value; + } + delegate = () -> value; + initialized = true; + } + return delegate.get(); + } + }; + } +} diff --git a/buffer-netty/src/main/java/io/micronaut/buffer/netty/NettyByteBufferFactory.java b/buffer-netty/src/main/java/io/micronaut/buffer/netty/NettyByteBufferFactory.java index 388a01b3247..626fa600a68 100644 --- a/buffer-netty/src/main/java/io/micronaut/buffer/netty/NettyByteBufferFactory.java +++ b/buffer-netty/src/main/java/io/micronaut/buffer/netty/NettyByteBufferFactory.java @@ -26,6 +26,8 @@ import jakarta.annotation.PostConstruct; import jakarta.inject.Singleton; +import java.util.function.Supplier; + /** * A {@link ByteBufferFactory} implementation for Netty. * @@ -42,20 +44,30 @@ public class NettyByteBufferFactory implements ByteBufferFactory allocatorSupplier; /** * Default constructor. */ public NettyByteBufferFactory() { - this.allocator = ByteBufAllocator.DEFAULT; + this.allocatorSupplier = new Supplier() { + @Override + public ByteBufAllocator get() { + return ByteBufAllocator.DEFAULT; + } + }; } /** * @param allocator The {@link ByteBufAllocator} */ public NettyByteBufferFactory(ByteBufAllocator allocator) { - this.allocator = allocator; + this.allocatorSupplier = new Supplier() { + @Override + public ByteBufAllocator get() { + return allocator; + } + }; } @PostConstruct @@ -71,22 +83,22 @@ final void register(MutableConversionService conversionService) { @Override public ByteBufAllocator getNativeAllocator() { - return allocator; + return allocatorSupplier.get(); } @Override public ByteBuffer buffer() { - return new NettyByteBuffer(allocator.buffer()); + return new NettyByteBuffer(allocatorSupplier.get().buffer()); } @Override public ByteBuffer buffer(int initialCapacity) { - return new NettyByteBuffer(allocator.buffer(initialCapacity)); + return new NettyByteBuffer(allocatorSupplier.get().buffer(initialCapacity)); } @Override public ByteBuffer buffer(int initialCapacity, int maxCapacity) { - return new NettyByteBuffer(allocator.buffer(initialCapacity, maxCapacity)); + return new NettyByteBuffer(allocatorSupplier.get().buffer(initialCapacity, maxCapacity)); } @Override diff --git a/context/src/main/java/io/micronaut/runtime/context/env/ConfigurationIntroductionAdvice.java b/context/src/main/java/io/micronaut/runtime/context/env/ConfigurationIntroductionAdvice.java index ebc81f15ac3..48559cd00b6 100644 --- a/context/src/main/java/io/micronaut/runtime/context/env/ConfigurationIntroductionAdvice.java +++ b/context/src/main/java/io/micronaut/runtime/context/env/ConfigurationIntroductionAdvice.java @@ -106,10 +106,11 @@ private Object resolveProperty(MethodInvocationContext context, ); if (defaultValue != null) { - return value.orElseGet(() -> environment.convertRequired( - defaultValue, - argument - )); + Object result = value.orElse(null); + if (result == null) { + return environment.convertRequired(defaultValue, argument); + } + return result; } else if (rt.isOptional()) { return value.orElse(Optional.empty()); } else if (context.isNullable()) { diff --git a/core-processor/src/main/java/io/micronaut/inject/annotation/AnnotationMetadataWriter.java b/core-processor/src/main/java/io/micronaut/inject/annotation/AnnotationMetadataWriter.java index a6f7b54aa13..9ce4fb46bee 100644 --- a/core-processor/src/main/java/io/micronaut/inject/annotation/AnnotationMetadataWriter.java +++ b/core-processor/src/main/java/io/micronaut/inject/annotation/AnnotationMetadataWriter.java @@ -113,7 +113,6 @@ public class AnnotationMetadataWriter extends AbstractClassFileWriter { Map.class, Map.class, Map.class, - boolean.class, boolean.class ) ); @@ -410,14 +409,6 @@ public static void writeAnnotationDefaults( continue; } -// Label falseCondition = new Label(); -// -// staticInit.push(annotationName); -// staticInit.invokeStatic(TYPE_DEFAULT_ANNOTATION_METADATA, METHOD_ARE_DEFAULTS_REGISTERED); -// staticInit.push(true); -// staticInit.ifCmp(Type.BOOLEAN_TYPE, GeneratorAdapter.EQ, falseCondition); -// staticInit.visitLabel(new Label()); - invokeLoadClassValueMethod(owningType, classWriter, staticInit, loadTypeMethods, new AnnotationClassValue(annotationName)); if (!typeOnly) { @@ -426,18 +417,13 @@ public static void writeAnnotationDefaults( } else { staticInit.invokeStatic(TYPE_DEFAULT_ANNOTATION_METADATA, METHOD_REGISTER_ANNOTATION_TYPE); } -// staticInit.visitLabel(falseCondition); } - if (annotationMetadata.repeated != null && !annotationMetadata.repeated.isEmpty()) { - Map repeated = new HashMap<>(); - for (Map.Entry e : annotationMetadata.repeated.entrySet()) { - repeated.put(e.getValue(), e.getKey()); - } - AnnotationMetadataSupport.removeCoreRepeatableAnnotations(repeated); - if (!repeated.isEmpty()) { - pushStringMapOf(staticInit, repeated, true, null, v -> pushValue(owningType, classWriter, staticInit, v, defaultsStorage, loadTypeMethods, true)); - staticInit.invokeStatic(TYPE_DEFAULT_ANNOTATION_METADATA, METHOD_REGISTER_REPEATABLE_ANNOTATIONS); - } + } + if (annotationMetadata.annotationRepeatableContainer != null && !annotationMetadata.annotationRepeatableContainer.isEmpty()) { + AnnotationMetadataSupport.registerRepeatableAnnotations(annotationMetadata.annotationRepeatableContainer); + if (!annotationMetadata.annotationRepeatableContainer.isEmpty()) { + pushStringMapOf(staticInit, annotationMetadata.annotationRepeatableContainer, true, null, v -> pushValue(owningType, classWriter, staticInit, v, defaultsStorage, loadTypeMethods, true)); + staticInit.invokeStatic(TYPE_DEFAULT_ANNOTATION_METADATA, METHOD_REGISTER_REPEATABLE_ANNOTATIONS); } } } @@ -496,8 +482,6 @@ private static void instantiateInternal( pushStringMapOf(generatorAdapter, annotationsByStereotype, false, Collections.emptyList(), list -> pushListOfString(generatorAdapter, list)); // 6th argument: has property expressions generatorAdapter.push(annotationMetadata.hasPropertyExpressions()); - // 7th argument: use repeatable annotations - generatorAdapter.push(true); // invoke the constructor generatorAdapter.invokeConstructor(TYPE_DEFAULT_ANNOTATION_METADATA, CONSTRUCTOR_ANNOTATION_METADATA); diff --git a/core/src/main/java/io/micronaut/core/annotation/AnnotationUtil.java b/core/src/main/java/io/micronaut/core/annotation/AnnotationUtil.java index 165637bafc1..5ab0941b3f7 100644 --- a/core/src/main/java/io/micronaut/core/annotation/AnnotationUtil.java +++ b/core/src/main/java/io/micronaut/core/annotation/AnnotationUtil.java @@ -613,7 +613,9 @@ public static Map mapOf(String key1, Object value1, public static int calculateHashCode(Map values) { int hashCode = 0; - for (Map.Entry member : values.entrySet()) { + // Performance optimization to use the Object as the type otherwise + // the bytecode will produce the type-check introducing type-check pollution + for (Map.Entry member : values.entrySet()) { Object value = member.getValue(); int nameHashCode = member.getKey().hashCode(); diff --git a/core/src/main/java/io/micronaut/core/annotation/AnnotationValue.java b/core/src/main/java/io/micronaut/core/annotation/AnnotationValue.java index a3dedb5710e..b4f2d24f8bd 100644 --- a/core/src/main/java/io/micronaut/core/annotation/AnnotationValue.java +++ b/core/src/main/java/io/micronaut/core/annotation/AnnotationValue.java @@ -133,7 +133,6 @@ public AnnotationValue(String annotationName, Map values, /** * @param annotationName The annotation name */ - @SuppressWarnings("unchecked") @UsedByGeneratedCode @Internal public AnnotationValue(String annotationName) { @@ -216,8 +215,8 @@ public List> getStereotypes() { * @param member The member * @return The properties as a immutable map. */ - public @NonNull - Map getProperties(@NonNull String member) { + @NonNull + public Map getProperties(@NonNull String member) { return getProperties(member, "name"); } @@ -231,20 +230,21 @@ Map getProperties(@NonNull String member) { */ public Map getProperties(@NonNull String member, String keyMember) { ArgumentUtils.requireNonNull("keyMember", keyMember); - if (StringUtils.isNotEmpty(member)) { - List> values = getAnnotations(member); - if (CollectionUtils.isNotEmpty(values)) { - Map props = new LinkedHashMap<>(values.size()); - for (AnnotationValue av : values) { - String name = av.stringValue(keyMember).orElse(null); - if (StringUtils.isNotEmpty(name)) { - av.stringValue(AnnotationMetadata.VALUE_MEMBER, valueMapper).ifPresent(v -> props.put(name, v)); - } - } - return Collections.unmodifiableMap(props); + if (StringUtils.isEmpty(member)) { + return Collections.emptyMap(); + } + List> values = getAnnotations(member); + if (CollectionUtils.isEmpty(values)) { + return Collections.emptyMap(); + } + Map props = CollectionUtils.newLinkedHashMap(values.size()); + for (AnnotationValue av : values) { + String name = av.stringValue(keyMember).orElse(null); + if (StringUtils.isNotEmpty(name)) { + av.stringValue(AnnotationMetadata.VALUE_MEMBER, valueMapper).ifPresent(v -> props.put(name, v)); } } - return Collections.emptyMap(); + return Collections.unmodifiableMap(props); } /** @@ -270,14 +270,14 @@ public Optional enumValue(@NonNull String member, @NonNull C * @param The enum type * @return An {@link Optional} of the enum value */ - @SuppressWarnings("unchecked") public Optional enumValue(@NonNull String member, @NonNull Class enumType, Function valueMapper) { ArgumentUtils.requireNonNull("enumType", enumType); - if (StringUtils.isNotEmpty(member)) { - Object o = getRawSingleValue(member, valueMapper); - if (o != null) { - return convertToEnum(enumType, o); - } + if (StringUtils.isEmpty(member)) { + return Optional.empty(); + } + Object o = getRawSingleValue(member, valueMapper); + if (o != null) { + return convertToEnum(enumType, o); } return Optional.empty(); } @@ -294,11 +294,11 @@ public Optional enumValue(@NonNull String member, @NonNull C @SuppressWarnings("unchecked") public E[] enumValues(@NonNull String member, @NonNull Class enumType) { ArgumentUtils.requireNonNull("enumType", enumType); - if (StringUtils.isNotEmpty(member)) { - Object rawValue = values.get(member); - return resolveEnumValues(enumType, rawValue); + if (StringUtils.isEmpty(member)) { + return (E[]) Array.newInstance(enumType, 0); } - return (E[]) Array.newInstance(enumType, 0); + Object rawValue = values.get(member); + return resolveEnumValues(enumType, rawValue); } /** @@ -307,8 +307,8 @@ public E[] enumValues(@NonNull String member, @NonNull Class * @return An {@link Optional} class */ @Override - public @NonNull - Optional> classValue() { + @NonNull + public Optional> classValue() { return classValue(AnnotationMetadata.VALUE_MEMBER); } @@ -319,7 +319,6 @@ Optional> classValue() { * @return An {@link Optional} class */ @Override - @SuppressWarnings("unchecked") public Optional> classValue(@NonNull String member) { return classValue(member, valueMapper); } @@ -336,24 +335,27 @@ public Optional> classValue(@NonNull String member) { @SuppressWarnings("unchecked") public Optional> classValue(@NonNull String member, @NonNull Class requiredType) { ArgumentUtils.requireNonNull("requiredType", requiredType); - if (StringUtils.isNotEmpty(member)) { - Object o = getRawSingleValue(member, valueMapper); - if (o instanceof AnnotationClassValue) { - Class t = ((AnnotationClassValue) o).getType().orElse(null); - if (t != null && requiredType.isAssignableFrom(t)) { - return Optional.of((Class) t); - } - return Optional.empty(); - } else if (o instanceof Class t) { - if (requiredType.isAssignableFrom(t)) { - return Optional.of((Class) t); - } - return Optional.empty(); - } else if (o != null) { - Class t = ClassUtils.forName(o.toString(), getClass().getClassLoader()).orElse(null); - if (t != null && requiredType.isAssignableFrom(t)) { - return Optional.of((Class) t); - } + if (StringUtils.isEmpty(member)) { + return Optional.empty(); + } + Object o = getRawSingleValue(member, valueMapper); + if (o instanceof AnnotationClassValue annotationClassValue) { + Class t = annotationClassValue.getType().orElse(null); + if (t != null && requiredType.isAssignableFrom(t)) { + return Optional.of((Class) t); + } + return Optional.empty(); + } + if (o instanceof Class t) { + if (requiredType.isAssignableFrom(t)) { + return Optional.of((Class) t); + } + return Optional.empty(); + } + if (o != null) { + Class t = ClassUtils.forName(o.toString(), getClass().getClassLoader()).orElse(null); + if (t != null && requiredType.isAssignableFrom(t)) { + return Optional.of((Class) t); } } return Optional.empty(); @@ -367,13 +369,14 @@ public Optional> classValue(@NonNull String member, @NonN * @return An {@link Optional} class */ public Optional> classValue(@NonNull String member, @Nullable Function valueMapper) { - if (StringUtils.isNotEmpty(member)) { - Object o = getRawSingleValue(member, valueMapper); - if (o instanceof AnnotationClassValue) { - return ((AnnotationClassValue) o).getType(); - } else if (o instanceof Class) { - return Optional.of(((Class) o)); - } + if (StringUtils.isEmpty(member)) { + return Optional.empty(); + } + Object o = getRawSingleValue(member, valueMapper); + if (o instanceof AnnotationClassValue annotationClassValue) { + return annotationClassValue.getType(); + } else if (o instanceof Class aClass) { + return Optional.of(aClass); } return Optional.empty(); } @@ -388,66 +391,70 @@ public String[] stringValues(@NonNull String member) { @Override public boolean[] booleanValues(String member) { Object v = values.get(member); - if (v != null) { - if (v instanceof boolean[]) { - return (boolean[]) v; - } else if (v instanceof Boolean) { - return new boolean[] { (Boolean) v }; - } else { - String[] strings = resolveStringValues(v, this.valueMapper); - if (ArrayUtils.isNotEmpty(strings)) { - boolean[] booleans = new boolean[strings.length]; - for (int i = 0; i < strings.length; i++) { - String string = strings[i]; - booleans[i] = Boolean.parseBoolean(string); - } - return booleans; - } - } + if (v == null) { + return ArrayUtils.EMPTY_BOOLEAN_ARRAY; } - return ArrayUtils.EMPTY_BOOLEAN_ARRAY; + if (v instanceof boolean[] booleans) { + return booleans; + } + if (v instanceof Boolean aBoolean) { + return new boolean[]{aBoolean}; + } + String[] strings = resolveStringValues(v, this.valueMapper); + if (ArrayUtils.isEmpty(strings)) { + return ArrayUtils.EMPTY_BOOLEAN_ARRAY; + } + boolean[] booleans = new boolean[strings.length]; + for (int i = 0; i < strings.length; i++) { + String string = strings[i]; + booleans[i] = Boolean.parseBoolean(string); + } + return booleans; } @Override public byte[] byteValues(String member) { Object v = values.get(member); - if (v != null) { - if (v instanceof byte[]) { - return (byte[]) v; - } else if (v instanceof Number) { - return new byte[] { ((Number) v).byteValue() }; - } else { - String[] strings = resolveStringValues(v, this.valueMapper); - if (ArrayUtils.isNotEmpty(strings)) { - byte[] bytes = new byte[strings.length]; - for (int i = 0; i < strings.length; i++) { - String string = strings[i]; - bytes[i] = Byte.parseByte(string); - } - return bytes; - } - } + if (v == null) { + return ArrayUtils.EMPTY_BYTE_ARRAY; + } + if (v instanceof byte[] bytes) { + return bytes; + } + if (v instanceof Number number) { + return new byte[]{number.byteValue()}; + } + String[] strings = resolveStringValues(v, this.valueMapper); + if (ArrayUtils.isEmpty(strings)) { + return ArrayUtils.EMPTY_BYTE_ARRAY; } - return ArrayUtils.EMPTY_BYTE_ARRAY; + byte[] bytes = new byte[strings.length]; + for (int i = 0; i < strings.length; i++) { + String string = strings[i]; + bytes[i] = Byte.parseByte(string); + } + return bytes; } @Override public char[] charValues(String member) { Object v = values.get(member); - if (v != null) { - if (v instanceof char[]) { - return (char[]) v; - } else if (v instanceof Character[]) { - Character[] v2 = (Character[]) v; - char[] chars = new char[v2.length]; - for (int i = 0; i < v2.length; i++) { - Character character = v2[i]; - chars[i] = character; - } - return chars; - } else if (v instanceof Character) { - return new char[] { (Character) v }; + if (v == null) { + return ArrayUtils.EMPTY_CHAR_ARRAY; + } + if (v instanceof char[] chars) { + return chars; + } + if (v instanceof Character[] v2) { + char[] chars = new char[v2.length]; + for (int i = 0; i < v2.length; i++) { + Character character = v2[i]; + chars[i] = character; } + return chars; + } + if (v instanceof Character character) { + return new char[]{character}; } return ArrayUtils.EMPTY_CHAR_ARRAY; } @@ -455,116 +462,121 @@ public char[] charValues(String member) { @Override public int[] intValues(String member) { Object v = values.get(member); - if (v != null) { - if (v instanceof int[]) { - return (int[]) v; - } else if (v instanceof Number) { - return new int[] { ((Number) v).intValue() }; - } else { - String[] strings = resolveStringValues(v, this.valueMapper); - if (ArrayUtils.isNotEmpty(strings)) { - int[] integers = new int[strings.length]; - for (int i = 0; i < strings.length; i++) { - String string = strings[i]; - integers[i] = Integer.parseInt(string); - } - return integers; - } - } + if (v == null) { + return ArrayUtils.EMPTY_INT_ARRAY; + } + if (v instanceof int[] ints) { + return ints; } - return ArrayUtils.EMPTY_INT_ARRAY; + if (v instanceof Number number) { + return new int[]{number.intValue()}; + } + String[] strings = resolveStringValues(v, this.valueMapper); + if (ArrayUtils.isEmpty(strings)) { + return ArrayUtils.EMPTY_INT_ARRAY; + } + int[] integers = new int[strings.length]; + for (int i = 0; i < strings.length; i++) { + String string = strings[i]; + integers[i] = Integer.parseInt(string); + } + return integers; } @Override public double[] doubleValues(String member) { Object v = values.get(member); - if (v != null) { - if (v instanceof double[]) { - return (double[]) v; - } else if (v instanceof Number) { - return new double[] { ((Number) v).doubleValue() }; - } else { - String[] strings = resolveStringValues(v, this.valueMapper); - if (ArrayUtils.isNotEmpty(strings)) { - double[] doubles = new double[strings.length]; - for (int i = 0; i < strings.length; i++) { - String string = strings[i]; - doubles[i] = Double.parseDouble(string); - } - return doubles; - } - } + if (v == null) { + return ArrayUtils.EMPTY_DOUBLE_ARRAY; } - return ArrayUtils.EMPTY_DOUBLE_ARRAY; + if (v instanceof double[] doubles) { + return doubles; + } + if (v instanceof Number number) { + return new double[]{number.doubleValue()}; + } + String[] strings = resolveStringValues(v, this.valueMapper); + if (ArrayUtils.isEmpty(strings)) { + return ArrayUtils.EMPTY_DOUBLE_ARRAY; + } + double[] doubles = new double[strings.length]; + for (int i = 0; i < strings.length; i++) { + String string = strings[i]; + doubles[i] = Double.parseDouble(string); + } + return doubles; } @Override public long[] longValues(String member) { Object v = values.get(member); - if (v != null) { - if (v instanceof long[]) { - return (long[]) v; - } else if (v instanceof Number) { - return new long[] { ((Number) v).longValue() }; - } else { - String[] strings = resolveStringValues(v, this.valueMapper); - if (ArrayUtils.isNotEmpty(strings)) { - long[] longs = new long[strings.length]; - for (int i = 0; i < strings.length; i++) { - String string = strings[i]; - longs[i] = Long.parseLong(string); - } - return longs; - } - } + if (v == null) { + return ArrayUtils.EMPTY_LONG_ARRAY; + } + if (v instanceof long[] longs) { + return longs; + } + if (v instanceof Number number) { + return new long[]{number.longValue()}; + } + String[] strings = resolveStringValues(v, this.valueMapper); + if (ArrayUtils.isEmpty(strings)) { + return ArrayUtils.EMPTY_LONG_ARRAY; } - return ArrayUtils.EMPTY_LONG_ARRAY; + long[] longs = new long[strings.length]; + for (int i = 0; i < strings.length; i++) { + String string = strings[i]; + longs[i] = Long.parseLong(string); + } + return longs; } @Override public float[] floatValues(String member) { Object v = values.get(member); - if (v != null) { - if (v instanceof float[]) { - return (float[]) v; - } else if (v instanceof Number) { - return new float[] { ((Number) v).floatValue() }; - } else { - String[] strings = resolveStringValues(v, this.valueMapper); - if (ArrayUtils.isNotEmpty(strings)) { - float[] floats = new float[strings.length]; - for (int i = 0; i < strings.length; i++) { - String string = strings[i]; - floats[i] = Float.parseFloat(string); - } - return floats; - } - } + if (v == null) { + return ArrayUtils.EMPTY_FLOAT_ARRAY; + } + if (v instanceof float[] floats) { + return floats; + } + if (v instanceof Number number) { + return new float[]{number.floatValue()}; } - return ArrayUtils.EMPTY_FLOAT_ARRAY; + String[] strings = resolveStringValues(v, this.valueMapper); + if (ArrayUtils.isEmpty(strings)) { + return ArrayUtils.EMPTY_FLOAT_ARRAY; + } + float[] floats = new float[strings.length]; + for (int i = 0; i < strings.length; i++) { + String string = strings[i]; + floats[i] = Float.parseFloat(string); + } + return floats; } @Override public short[] shortValues(String member) { Object v = values.get(member); - if (v != null) { - if (v instanceof short[]) { - return (short[]) v; - } else if (v instanceof Number) { - return new short[] { ((Number) v).shortValue() }; - } else { - String[] strings = resolveStringValues(v, this.valueMapper); - if (ArrayUtils.isNotEmpty(strings)) { - short[] shorts = new short[strings.length]; - for (int i = 0; i < strings.length; i++) { - String string = strings[i]; - shorts[i] = Short.parseShort(string); - } - return shorts; - } - } + if (v == null) { + return ArrayUtils.EMPTY_SHORT_ARRAY; + } + if (v instanceof short[] shorts) { + return shorts; + } + if (v instanceof Number number) { + return new short[]{number.shortValue()}; + } + String[] strings = resolveStringValues(v, this.valueMapper); + if (ArrayUtils.isEmpty(strings)) { + return ArrayUtils.EMPTY_SHORT_ARRAY; + } + short[] shorts = new short[strings.length]; + for (int i = 0; i < strings.length; i++) { + String string = strings[i]; + shorts[i] = Short.parseShort(string); } - return ArrayUtils.EMPTY_SHORT_ARRAY; + return shorts; } /** @@ -575,25 +587,26 @@ public short[] shortValues(String member) { * @return The string values */ public String[] stringValues(@NonNull String member, Function valueMapper) { - if (StringUtils.isNotEmpty(member)) { - Object o = values.get(member); - String[] strs = resolveStringValues(o, valueMapper); - if (strs != null) { - return strs; - } + if (StringUtils.isEmpty(member)) { + return StringUtils.EMPTY_STRING_ARRAY; + } + Object o = values.get(member); + String[] strs = resolveStringValues(o, valueMapper); + if (strs != null) { + return strs; } return StringUtils.EMPTY_STRING_ARRAY; } @Override public Class[] classValues(@NonNull String member) { - if (StringUtils.isNotEmpty(member)) { - Object o = values.get(member); - Class[] type = resolveClassValues(o); - if (type != null) { - return type; - } - + if (StringUtils.isEmpty(member)) { + return ReflectionUtils.EMPTY_CLASS_ARRAY; + } + Object o = values.get(member); + Class[] type = resolveClassValues(o); + if (type != null) { + return type; } return ReflectionUtils.EMPTY_CLASS_ARRAY; } @@ -601,28 +614,31 @@ public Class[] classValues(@NonNull String member) { @NonNull @Override public AnnotationClassValue[] annotationClassValues(@NonNull String member) { - if (StringUtils.isNotEmpty(member)) { - Object o = values.get(member); - if (o instanceof AnnotationClassValue) { - return new AnnotationClassValue[]{(AnnotationClassValue) o}; - } else if (o instanceof AnnotationClassValue[]) { - return (AnnotationClassValue[]) o; - } + if (StringUtils.isEmpty(member)) { + return AnnotationClassValue.EMPTY_ARRAY; + } + Object o = values.get(member); + if (o instanceof AnnotationClassValue annotationClassValue) { + return new AnnotationClassValue[]{annotationClassValue}; + } + if (o instanceof AnnotationClassValue[] annotationClassValues) { + return annotationClassValues; } return AnnotationClassValue.EMPTY_ARRAY; } @Override public Optional> annotationClassValue(@NonNull String member) { - if (StringUtils.isNotEmpty(member)) { - Object o = values.get(member); - if (o instanceof AnnotationClassValue) { - return Optional.of((AnnotationClassValue) o); - } else if (o instanceof AnnotationClassValue[]) { - AnnotationClassValue[] a = (AnnotationClassValue[]) o; - if (a.length > 0) { - return Optional.of(a[0]); - } + if (StringUtils.isEmpty(member)) { + return Optional.empty(); + } + Object o = values.get(member); + if (o instanceof AnnotationClassValue annotationClassValue) { + return Optional.of(annotationClassValue); + } + if (o instanceof AnnotationClassValue[] annotationClassValues) { + if (annotationClassValues.length > 0) { + return Optional.of(annotationClassValues[0]); } } return Optional.empty(); @@ -647,16 +663,25 @@ public OptionalInt intValue(@NonNull String member) { * @return An {@link OptionalInt} */ public OptionalInt intValue(@NonNull String member, @Nullable Function valueMapper) { - if (StringUtils.isNotEmpty(member)) { - Object o = getRawSingleValue(member, valueMapper); - if (o instanceof Number) { - return OptionalInt.of(((Number) o).intValue()); - } else if (o instanceof CharSequence) { - try { - return OptionalInt.of(Integer.parseInt(o.toString())); - } catch (NumberFormatException e) { - return OptionalInt.empty(); - } + if (StringUtils.isEmpty(member)) { + return OptionalInt.empty(); + } + Object o = getRawSingleValue(member, valueMapper); + if (o instanceof Number number) { + return OptionalInt.of(number.intValue()); + } + if (o instanceof String s) { + try { + return OptionalInt.of(Integer.parseInt(s)); + } catch (NumberFormatException e) { + return OptionalInt.empty(); + } + } + if (o instanceof CharSequence charSequence) { + try { + return OptionalInt.of(Integer.parseInt(charSequence.toString())); + } catch (NumberFormatException e) { + return OptionalInt.empty(); } } return OptionalInt.empty(); @@ -664,16 +689,18 @@ public OptionalInt intValue(@NonNull String member, @Nullable Function byteValue(String member) { - if (StringUtils.isNotEmpty(member)) { - Object o = getRawSingleValue(member, valueMapper); - if (o instanceof Number) { - return Optional.of(((Number) o).byteValue()); - } else if (o instanceof CharSequence) { - try { - return Optional.of(Byte.parseByte(o.toString())); - } catch (NumberFormatException e) { - return Optional.empty(); - } + if (StringUtils.isEmpty(member)) { + return Optional.empty(); + } + Object o = getRawSingleValue(member, valueMapper); + if (o instanceof Number number) { + return Optional.of(number.byteValue()); + } + if (o instanceof CharSequence charSequence) { + try { + return Optional.of(Byte.parseByte(charSequence.toString())); + } catch (NumberFormatException e) { + return Optional.empty(); } } return Optional.empty(); @@ -681,11 +708,12 @@ public Optional byteValue(String member) { @Override public Optional charValue(String member) { - if (StringUtils.isNotEmpty(member)) { - Object o = getRawSingleValue(member, valueMapper); - if (o instanceof Character) { - return Optional.of(((Character) o)); - } + if (StringUtils.isEmpty(member)) { + return Optional.empty(); + } + Object o = getRawSingleValue(member, valueMapper); + if (o instanceof Character character) { + return Optional.of(character); } return Optional.empty(); } @@ -713,16 +741,25 @@ public OptionalLong longValue(@NonNull String member) { * @return An {@link OptionalLong} */ public OptionalLong longValue(@NonNull String member, @Nullable Function valueMapper) { - if (StringUtils.isNotEmpty(member)) { - Object o = getRawSingleValue(member, valueMapper); - if (o instanceof Number) { - return OptionalLong.of((((Number) o).longValue())); - } else if (o instanceof CharSequence) { - try { - return OptionalLong.of(Long.parseLong(o.toString())); - } catch (NumberFormatException e) { - return OptionalLong.empty(); - } + if (StringUtils.isEmpty(member)) { + return OptionalLong.empty(); + } + Object o = getRawSingleValue(member, valueMapper); + if (o instanceof Number number) { + return OptionalLong.of(number.longValue()); + } + if (o instanceof String s) { + try { + return OptionalLong.of(Long.parseLong(s)); + } catch (NumberFormatException e) { + return OptionalLong.empty(); + } + } + if (o instanceof CharSequence charSequence) { + try { + return OptionalLong.of(Long.parseLong(charSequence.toString())); + } catch (NumberFormatException e) { + return OptionalLong.empty(); } } return OptionalLong.empty(); @@ -741,16 +778,18 @@ public Optional shortValue(@NonNull String member) { * @return An {@link Optional} of {@link Short} */ public Optional shortValue(@NonNull String member, @Nullable Function valueMapper) { - if (StringUtils.isNotEmpty(member)) { - Object o = getRawSingleValue(member, valueMapper); - if (o instanceof Number) { - return Optional.of((((Number) o).shortValue())); - } else if (o instanceof CharSequence) { - try { - return Optional.of(Short.parseShort(o.toString())); - } catch (NumberFormatException e) { - return Optional.empty(); - } + if (StringUtils.isEmpty(member)) { + return Optional.empty(); + } + Object o = getRawSingleValue(member, valueMapper); + if (o instanceof Number number) { + return Optional.of(number.shortValue()); + } + if (o instanceof CharSequence charSequence) { + try { + return Optional.of(Short.parseShort(charSequence.toString())); + } catch (NumberFormatException e) { + return Optional.empty(); } } return Optional.empty(); @@ -764,13 +803,18 @@ public Optional shortValue(@NonNull String member, @Nullable Function booleanValue(@NonNull String member, @Nullable Function valueMapper) { - if (StringUtils.isNotEmpty(member)) { - Object o = getRawSingleValue(member, valueMapper); - if (o instanceof Boolean) { - return Optional.of((Boolean) o); - } else if (o instanceof CharSequence) { - return Optional.of(StringUtils.isTrue(o.toString())); - } + if (StringUtils.isEmpty(member)) { + return Optional.empty(); + } + Object o = getRawSingleValue(member, valueMapper); + if (o instanceof Boolean aBoolean) { + return Optional.of(aBoolean); + } + if (o instanceof String s) { + return Optional.of(StringUtils.isTrue(s)); + } + if (o instanceof CharSequence charSequence) { + return Optional.of(StringUtils.isTrue(charSequence.toString())); } return Optional.empty(); } @@ -794,16 +838,25 @@ public OptionalDouble doubleValue(@NonNull String member) { * @return An {@link OptionalDouble} */ public OptionalDouble doubleValue(@NonNull String member, @Nullable Function valueMapper) { - if (StringUtils.isNotEmpty(member)) { - Object o = getRawSingleValue(member, valueMapper); - if (o instanceof Number) { - return OptionalDouble.of(((Number) o).doubleValue()); - } else if (o instanceof CharSequence) { - try { - return OptionalDouble.of(Double.parseDouble(o.toString())); - } catch (NumberFormatException e) { - return OptionalDouble.empty(); - } + if (StringUtils.isEmpty(member)) { + return OptionalDouble.empty(); + } + Object o = getRawSingleValue(member, valueMapper); + if (o instanceof Number number) { + return OptionalDouble.of(number.doubleValue()); + } + if (o instanceof String s) { + try { + return OptionalDouble.of(Double.parseDouble(s)); + } catch (NumberFormatException e) { + return OptionalDouble.empty(); + } + } + if (o instanceof CharSequence charSequence) { + try { + return OptionalDouble.of(Double.parseDouble(charSequence.toString())); + } catch (NumberFormatException e) { + return OptionalDouble.empty(); } } return OptionalDouble.empty(); @@ -822,16 +875,18 @@ public Optional floatValue(String member) { * @return An {@link OptionalDouble} */ public Optional floatValue(@NonNull String member, @Nullable Function valueMapper) { - if (StringUtils.isNotEmpty(member)) { - Object o = getRawSingleValue(member, valueMapper); - if (o instanceof Number) { - return Optional.of(((Number) o).floatValue()); - } else if (o instanceof CharSequence) { - try { - return Optional.of(Float.parseFloat(o.toString())); - } catch (NumberFormatException e) { - return Optional.empty(); - } + if (StringUtils.isEmpty(member)) { + return Optional.empty(); + } + Object o = getRawSingleValue(member, valueMapper); + if (o instanceof Number number) { + return Optional.of(number.floatValue()); + } + if (o instanceof CharSequence charSequence) { + try { + return Optional.of(Float.parseFloat(charSequence.toString())); + } catch (NumberFormatException e) { + return Optional.empty(); } } return Optional.empty(); @@ -855,11 +910,12 @@ public OptionalDouble doubleValue() { */ @Override public Optional stringValue(@NonNull String member) { - if (StringUtils.isNotEmpty(member)) { - Object o = getRawSingleValue(member, valueMapper); - if (o != null) { - return Optional.of(o.toString()); - } + if (StringUtils.isEmpty(member)) { + return Optional.empty(); + } + Object o = getRawSingleValue(member, valueMapper); + if (o != null) { + return Optional.of(o.toString()); } return Optional.empty(); } @@ -872,11 +928,12 @@ public Optional stringValue(@NonNull String member) { * @return An {@link OptionalInt} */ public Optional stringValue(@NonNull String member, @Nullable Function valueMapper) { - if (StringUtils.isNotEmpty(member)) { - Object o = getRawSingleValue(member, valueMapper); - if (o != null) { - return Optional.of(o.toString()); - } + if (StringUtils.isEmpty(member)) { + return Optional.empty(); + } + Object o = getRawSingleValue(member, valueMapper); + if (o != null) { + return Optional.of(o.toString()); } return Optional.empty(); } @@ -933,13 +990,14 @@ public boolean isTrue(String member) { * @return Is the value of the annotation true. */ public boolean isTrue(@NonNull String member, @Nullable Function valueMapper) { - if (StringUtils.isNotEmpty(member)) { - Object o = getRawSingleValue(member, valueMapper); - if (o instanceof Boolean) { - return (Boolean) o; - } else if (o != null) { - return StringUtils.isTrue(o.toString()); - } + if (StringUtils.isEmpty(member)) { + return false; + } + Object o = getRawSingleValue(member, valueMapper); + if (o instanceof Boolean aBoolean) { + return aBoolean; + } else if (o != null) { + return StringUtils.isTrue(o.toString()); } return false; } @@ -1013,11 +1071,12 @@ ConvertibleValues getConvertibleValues() { @Override public Optional get(CharSequence member, ArgumentConversionContext conversionContext) { Optional result = convertibleValues.get(member, conversionContext); - if (!result.isPresent()) { - Object dv = defaultValues.get(member.toString()); - if (dv != null) { - return ConversionService.SHARED.convert(dv, conversionContext); - } + if (result.isPresent()) { + return result; + } + Object dv = defaultValues.get(member.toString()); + if (dv != null) { + return ConversionService.SHARED.convert(dv, conversionContext); } return result; } @@ -1091,34 +1150,31 @@ final T getRequiredValue(String member, Class type) { * @return The result * @throws IllegalStateException If no member is available that conforms to the given name and type */ - public final @NonNull - List> getAnnotations(String member, Class type) { + public final @NonNull List> getAnnotations(String member, Class type) { ArgumentUtils.requireNonNull("type", type); String typeName = type.getName(); ArgumentUtils.requireNonNull("member", member); Object v = values.get(member); - AnnotationValue[] values = null; - if (v instanceof AnnotationValue) { - values = new AnnotationValue[]{(AnnotationValue) v}; - } else if (v instanceof AnnotationValue[]) { - values = (AnnotationValue[]) v; - } - if (ArrayUtils.isNotEmpty(values)) { - List> list = new ArrayList<>(values.length); - - for (AnnotationValue value : values) { - if (value == null) { - continue; - } - if (value.getAnnotationName().equals(typeName)) { - //noinspection unchecked - list.add(value); - } + AnnotationValue[] values = null; + if (v instanceof AnnotationValue annotationValue) { + values = new AnnotationValue[]{annotationValue}; + } else if (v instanceof AnnotationValue[] annotationValues) { + values = annotationValues; + } + if (ArrayUtils.isEmpty(values)) { + return Collections.emptyList(); + } + List> list = new ArrayList<>(values.length); + for (AnnotationValue value : values) { + if (value == null) { + continue; + } + if (value.getAnnotationName().equals(typeName)) { + list.add((AnnotationValue) value); } - return list; } - return Collections.emptyList(); + return list; } /** @@ -1130,16 +1186,18 @@ List> getAnnotations(String member, Cl * @throws IllegalStateException If no member is available that conforms to the given name and type */ @SuppressWarnings("unchecked") - public final @NonNull - List> getAnnotations(String member) { + @NonNull + public final List> getAnnotations(String member) { ArgumentUtils.requireNonNull("member", member); Object v = values.get(member); - if (v instanceof AnnotationValue) { - return Collections.singletonList((AnnotationValue) v); - } else if (v instanceof AnnotationValue[]) { - return Arrays.asList((AnnotationValue[]) v); - } else if (v instanceof Collection) { - final Iterator i = ((Collection) v).iterator(); + if (v instanceof AnnotationValue annotationValue) { + return Collections.singletonList(annotationValue); + } + if (v instanceof AnnotationValue[] annotationValues) { + return Arrays.asList(annotationValues); + } + if (v instanceof Collection collection) { + final Iterator i = collection.iterator(); if (i.hasNext()) { final Object o = i.next(); if (o instanceof AnnotationValue) { @@ -1159,21 +1217,20 @@ List> getAnnotations(String member) { * @return The result * @throws IllegalStateException If no member is available that conforms to the given name and type */ - public @NonNull - final Optional> getAnnotation(String member, Class type) { + @NonNull + public final Optional> getAnnotation(String member, Class type) { ArgumentUtils.requireNonNull("type", type); String typeName = type.getName(); ArgumentUtils.requireNonNull("member", member); Object v = values.get(member); - if (v instanceof AnnotationValue) { - final AnnotationValue av = (AnnotationValue) v; + if (v instanceof AnnotationValue av) { if (av.getAnnotationName().equals(typeName)) { return Optional.of(av); } return Optional.empty(); - } else if (v instanceof AnnotationValue[]) { - final AnnotationValue[] values = (AnnotationValue[]) v; + } + if (v instanceof AnnotationValue[] values) { if (ArrayUtils.isNotEmpty(values)) { final AnnotationValue value = values[0]; if (value.getAnnotationName().equals(typeName)) { @@ -1194,18 +1251,16 @@ final Optional> getAnnotation(String m * @throws IllegalStateException If no member is available that conforms to the given name and type * @since 3.3.0 */ - public @NonNull - final Optional> getAnnotation(@NonNull String member) { + @NonNull + public final Optional> getAnnotation(@NonNull String member) { ArgumentUtils.requireNonNull("member", member); Object v = values.get(member); - if (v instanceof AnnotationValue) { - final AnnotationValue av = (AnnotationValue) v; + if (v instanceof AnnotationValue av) { return Optional.of(av); - } else if (v instanceof AnnotationValue[]) { - final AnnotationValue[] values = (AnnotationValue[]) v; + } + if (v instanceof AnnotationValue[] values) { if (ArrayUtils.isNotEmpty(values)) { - final AnnotationValue value = values[0]; - return Optional.of(value); + return Optional.of(values[0]); } return Optional.empty(); } @@ -1245,12 +1300,10 @@ public boolean equals(Object obj) { if (obj == null) { return false; } - if (!AnnotationValue.class.isInstance(obj)) { + if (!(obj instanceof AnnotationValue other)) { return false; } - AnnotationValue other = AnnotationValue.class.cast(obj); - if (!annotationName.equals(other.getAnnotationName())) { return false; } @@ -1288,9 +1341,9 @@ public static AnnotationValueBuilder builder(String an /** * Start building a new annotation for the given name. * - * @param annotationName The annotation name + * @param annotationName The annotation name * @param retentionPolicy The retention policy - * @param The annotation type + * @param The annotation type * @return The builder * @since 2.4.0 */ @@ -1330,20 +1383,24 @@ public static AnnotationValueBuilder builder(@NonNull * @return The string[] or null */ @Internal - public static @Nullable - String[] resolveStringValues(@Nullable Object value, @Nullable Function valueMapper) { + @Nullable + public static String[] resolveStringValues(@Nullable Object value, @Nullable Function valueMapper) { if (value == null) { return null; } if (valueMapper != null) { value = valueMapper.apply(value); } + if (value instanceof String s) { + return new String[]{s}; + } + if (value instanceof String[] existing) { + return Arrays.copyOf(existing, existing.length); + } if (value instanceof CharSequence) { return new String[]{value.toString()}; - } else if (value instanceof String[]) { - final String[] existing = (String[]) value; - return Arrays.copyOf(existing, existing.length); - } else if (value != null) { + } + if (value != null) { if (value.getClass().isArray()) { int len = Array.getLength(value); String[] newArray = new String[len]; @@ -1370,8 +1427,8 @@ String[] resolveStringValues(@Nullable Object value, @Nullable Function E[] resolveEnumValues(@NonNull Class enumType, @Nullable Object rawValue) { + @NonNull + public static E[] resolveEnumValues(@NonNull Class enumType, @Nullable Object rawValue) { if (rawValue == null) { return (E[]) Array.newInstance(enumType, 0); } @@ -1381,8 +1438,8 @@ E[] resolveEnumValues(@NonNull Class enumType, @Nullable Obj for (int i = 0; i < len; i++) { convertToEnum(enumType, Array.get(rawValue, i)).ifPresent(list::add); } - } else if (rawValue instanceof Iterable) { - for (Object o : (Iterable) rawValue) { + } else if (rawValue instanceof Iterable iterable) { + for (Object o : iterable) { convertToEnum(enumType, o).ifPresent(list::add); } } else if (enumType.isAssignableFrom(rawValue.getClass())) { @@ -1421,48 +1478,52 @@ public static String[] resolveStringArray(String[] strs, @Nullable Function[] resolveClassValues(@Nullable Object value) { + @Nullable + public static Class[] resolveClassValues(@Nullable Object value) { // conditional branches ordered from most likely to least likely // generally at runtime values are always AnnotationClassValue // A class can be present at compilation time if (value == null) { return null; - } else if (value instanceof AnnotationClassValue) { - Class type = ((AnnotationClassValue) value).getType().orElse(null); + } + if (value instanceof AnnotationClassValue annotationClassValue) { + Class type = annotationClassValue.getType().orElse(null); if (type != null) { return new Class[]{type}; } - } else if (value instanceof AnnotationValue[]) { - AnnotationValue[] array = (AnnotationValue[]) value; - int len = array.length; + return null; + } + if (value instanceof AnnotationValue[] annotationValues) { + int len = annotationValues.length; if (len > 0) { if (len == 1) { - return array[0].classValues(); + return annotationValues[0].classValues(); } else { - return Arrays.stream(array) + return Arrays.stream(annotationValues) .flatMap(annotationValue -> Stream.of(annotationValue.classValues())).toArray(Class[]::new); } } - } else if (value instanceof AnnotationValue) { - return ((AnnotationValue) value).classValues(); - } else if (value instanceof Object[]) { - Object[] values = (Object[]) value; - if (values instanceof Class[]) { - return (Class[]) values; + return null; + } + if (value instanceof AnnotationValue annotationValue) { + return annotationValue.classValues(); + } + if (value instanceof Object[] values) { + if (values instanceof Class[] classes) { + return classes; } else { return Arrays.stream(values).flatMap(o -> { - if (o instanceof AnnotationClassValue) { - Optional> type = ((AnnotationClassValue) o).getType(); - return type.map(Stream::of).orElse(Stream.empty()); - } else if (o instanceof Class) { - return Stream.of((Class) o); + if (o instanceof AnnotationClassValue annotationClassValue) { + return annotationClassValue.getType().stream(); + } else if (o instanceof Class aClass) { + return Stream.of(aClass); } return Stream.empty(); }).toArray(Class[]::new); } - } else if (value instanceof Class) { - return new Class[]{(Class) value}; + } + if (value instanceof Class aClass) { + return new Class[]{aClass}; } return null; } @@ -1481,8 +1542,8 @@ private ConvertibleValues newConvertibleValues(Map } } - private @Nullable - Object getRawSingleValue(@NonNull String member, Function valueMapper) { + @Nullable + private Object getRawSingleValue(@NonNull String member, Function valueMapper) { Object rawValue = values.get(member); if (rawValue != null) { if (rawValue.getClass().isArray()) { @@ -1490,8 +1551,8 @@ Object getRawSingleValue(@NonNull String member, Function valueM if (len > 0) { rawValue = Array.get(rawValue, 0); } - } else if (rawValue instanceof Iterable) { - Iterator i = ((Iterable) rawValue).iterator(); + } else if (rawValue instanceof Iterable iterable) { + Iterator i = iterable.iterator(); if (i.hasNext()) { rawValue = i.next(); } diff --git a/core/src/main/java/io/micronaut/core/annotation/ImmutableSortedStringsArrayMap.java b/core/src/main/java/io/micronaut/core/annotation/ImmutableSortedStringsArrayMap.java index 178884adee4..854cfbfc072 100644 --- a/core/src/main/java/io/micronaut/core/annotation/ImmutableSortedStringsArrayMap.java +++ b/core/src/main/java/io/micronaut/core/annotation/ImmutableSortedStringsArrayMap.java @@ -71,7 +71,8 @@ private static int reduceHashCode(int hashCode, int mod) { } private int findKeyIndex(Object key) { - if (!(key instanceof Comparable)) { + // Performance optimization to check for the String first to avoid the type-check pollution + if (!(key instanceof String) && !(key instanceof Comparable)) { return -1; } int v = index[reduceHashCode(key.hashCode(), keys.length)]; diff --git a/core/src/main/java/io/micronaut/core/convert/TypeConverter.java b/core/src/main/java/io/micronaut/core/convert/TypeConverter.java index c0c6684ddf8..2f050e4feba 100644 --- a/core/src/main/java/io/micronaut/core/convert/TypeConverter.java +++ b/core/src/main/java/io/micronaut/core/convert/TypeConverter.java @@ -72,6 +72,12 @@ default Optional convert(S object, Class targetType) { * @return The converter instance */ static TypeConverter of(Class sourceType, Class targetType, Function converter) { - return (object, targetType1, context) -> Optional.ofNullable(converter.apply(object)); + // Keep the anonymous class instead of Lambda to reduce the Lambda invocation overhead during the startup + return new TypeConverter() { + @Override + public Optional convert(ST object, Class targetType1, ConversionContext context) { + return Optional.ofNullable(converter.apply(object)); + } + }; } } diff --git a/core/src/main/java/io/micronaut/core/io/IOUtils.java b/core/src/main/java/io/micronaut/core/io/IOUtils.java index 00a3fc7764b..6408275c642 100644 --- a/core/src/main/java/io/micronaut/core/io/IOUtils.java +++ b/core/src/main/java/io/micronaut/core/io/IOUtils.java @@ -30,14 +30,16 @@ import java.nio.file.FileSystem; import java.nio.file.FileSystemNotFoundException; import java.nio.file.FileSystems; +import java.nio.file.FileVisitResult; +import java.nio.file.FileVisitor; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; +import java.nio.file.attribute.BasicFileAttributes; import java.util.ArrayList; -import java.util.Iterator; +import java.util.Collections; import java.util.List; import java.util.function.Consumer; -import java.util.stream.Stream; /** * Utility methods for I/O operations. @@ -106,15 +108,33 @@ public static void eachFile(@NonNull URI uri, String path, @NonNull Consumer walk = Files.walk(myPath, 1)) { - for (Iterator it = walk.iterator(); it.hasNext();) { - final Path currentPath = it.next(); - if (currentPath.equals(myPath) || Files.isHidden(currentPath) || currentPath.getFileName().startsWith(".")) { - continue; + Path finalMyPath = myPath; + // use this method instead of Files#walk to eliminate the Stream overhead + Files.walkFileTree(myPath, Collections.emptySet(), 1, new FileVisitor<>() { + @Override + public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) { + return FileVisitResult.CONTINUE; + } + + @Override + public FileVisitResult visitFile(Path currentPath, BasicFileAttributes attrs) throws IOException { + if (currentPath.equals(finalMyPath) || Files.isHidden(currentPath) || currentPath.getFileName().startsWith(".")) { + return FileVisitResult.CONTINUE; } consumer.accept(currentPath); + return FileVisitResult.CONTINUE; } - } + + @Override + public FileVisitResult visitFileFailed(Path file, IOException exc) { + return FileVisitResult.CONTINUE; + } + + @Override + public FileVisitResult postVisitDirectory(Path dir, IOException exc) { + return FileVisitResult.CONTINUE; + } + }); } } catch (IOException e) { // ignore, can't do anything here and can't log because class used in compiler diff --git a/core/src/main/java/io/micronaut/core/io/file/DefaultFileSystemResourceLoader.java b/core/src/main/java/io/micronaut/core/io/file/DefaultFileSystemResourceLoader.java index b9d72ce31a5..ae52b4b64ab 100644 --- a/core/src/main/java/io/micronaut/core/io/file/DefaultFileSystemResourceLoader.java +++ b/core/src/main/java/io/micronaut/core/io/file/DefaultFileSystemResourceLoader.java @@ -66,7 +66,7 @@ public DefaultFileSystemResourceLoader(String path) { * @param path The path */ public DefaultFileSystemResourceLoader(Path path) { - this.baseDir = SupplierUtil.memoizedNonEmpty(() -> { + this.baseDir = SupplierUtil.memoized(() -> { Path baseDirPath; try { baseDirPath = path.normalize().toRealPath(); diff --git a/core/src/main/java/io/micronaut/core/io/file/FileSystemResourceLoader.java b/core/src/main/java/io/micronaut/core/io/file/FileSystemResourceLoader.java index 776fbb9b981..bd0ccf29149 100644 --- a/core/src/main/java/io/micronaut/core/io/file/FileSystemResourceLoader.java +++ b/core/src/main/java/io/micronaut/core/io/file/FileSystemResourceLoader.java @@ -22,6 +22,11 @@ */ public interface FileSystemResourceLoader extends ResourceLoader { + /** + * The resource name prefix. + */ + String PREFIX = "file:"; + /** * Creation method. * @return loader @@ -38,6 +43,6 @@ static FileSystemResourceLoader defaultLoader() { */ @Override default boolean supportsPrefix(String path) { - return path.startsWith("file:"); + return path.startsWith(PREFIX); } } diff --git a/core/src/main/java/io/micronaut/core/io/service/ServiceScanner.java b/core/src/main/java/io/micronaut/core/io/service/ServiceScanner.java index 73ca1cfac9d..2bde13fd5f5 100644 --- a/core/src/main/java/io/micronaut/core/io/service/ServiceScanner.java +++ b/core/src/main/java/io/micronaut/core/io/service/ServiceScanner.java @@ -39,6 +39,7 @@ import java.util.concurrent.ForkJoinPool; import java.util.concurrent.RecursiveAction; import java.util.function.BiConsumer; +import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Predicate; @@ -79,14 +80,18 @@ private static URI normalizeFilePath(String path, URI uri) { @SuppressWarnings("java:S3398") private static Set computeMicronautServiceTypeNames(URI uri, String path) { Set typeNames = new HashSet<>(); - IOUtils.eachFile( - uri, path, currentPath -> { - if (Files.isRegularFile(currentPath)) { - final String typeName = currentPath.getFileName().toString(); - typeNames.add(typeName); - } + // Keep the anonymous class instead of Lambda to reduce the Lambda invocation overhead during the startup + Consumer consumer = new Consumer<>() { + + @Override + public void accept(Path currentPath) { + if (Files.isRegularFile(currentPath)) { + final String typeName = currentPath.getFileName().toString(); + typeNames.add(typeName); } - ); + } + }; + IOUtils.eachFile(uri, path, consumer); return typeNames; } @@ -197,12 +202,18 @@ public void collect(Collection values, boolean allowFork) { while (serviceConfigs.hasMoreElements()) { URL url = serviceConfigs.nextElement(); for (String typeName : computeStandardServiceTypeNames(url)) { - values.add(transformer.apply(typeName)); + S val = transformer.apply(typeName); + if (val != null) { + values.add(val); + } } } findMicronautMetaServiceConfigs((uri, path) -> { for (String typeName : computeMicronautServiceTypeNames(uri, path)) { - values.add(transformer.apply(typeName)); + S val = transformer.apply(typeName); + if (val != null) { + values.add(val); + } } }); } catch (IOException | URISyntaxException e) { diff --git a/core/src/main/java/io/micronaut/core/io/service/SoftServiceLoader.java b/core/src/main/java/io/micronaut/core/io/service/SoftServiceLoader.java index 08291b83097..63a9a18e53b 100644 --- a/core/src/main/java/io/micronaut/core/io/service/SoftServiceLoader.java +++ b/core/src/main/java/io/micronaut/core/io/service/SoftServiceLoader.java @@ -46,6 +46,8 @@ */ public final class SoftServiceLoader implements Iterable> { public static final String META_INF_SERVICES = "META-INF/services"; + private static final MethodHandles.Lookup LOOKUP = MethodHandles.publicLookup(); + private static final MethodType VOID_TYPE = MethodType.methodType(void.class); private static final Map> STATIC_SERVICES = StaticOptimizations.get(Optimizations.class) @@ -58,11 +60,11 @@ public final class SoftServiceLoader implements Iterable private final Predicate condition; private boolean allowFork = true; - private SoftServiceLoader(Class serviceType, ClassLoader classLoader) { + private SoftServiceLoader(Class serviceType, @Nullable ClassLoader classLoader) { this(serviceType, classLoader, (String name) -> true); } - private SoftServiceLoader(Class serviceType, ClassLoader classLoader, Predicate condition) { + private SoftServiceLoader(Class serviceType, @Nullable ClassLoader classLoader, Predicate condition) { this.serviceType = serviceType; this.classLoader = classLoader == null ? ClassLoader.getSystemClassLoader() : classLoader; this.condition = condition == null ? (String name) -> true : condition; @@ -183,14 +185,15 @@ private void collectDynamicServices( try { @SuppressWarnings("unchecked") final Class loadedClass = (Class) Class.forName(className, false, classLoader); - S result = loadedClass.getDeclaredConstructor().newInstance(); + // MethodHandler should more performant than the basic reflection + S result = (S) LOOKUP.findConstructor(loadedClass, VOID_TYPE).invoke(); if (predicate != null && !predicate.test(result)) { return null; } return result; - } catch (NoClassDefFoundError | ClassNotFoundException | NoSuchMethodException e) { + } catch (NoClassDefFoundError | ClassNotFoundException | NoSuchMethodException | IllegalAccessException e) { // Ignore - } catch (Exception e) { + } catch (Throwable e) { throw new ServiceLoadingException(e); } return null; @@ -284,8 +287,6 @@ public static ServiceCollector newCollector(String serviceName, * @param The service type */ public static final class StaticDefinition implements ServiceDefinition { - private static final MethodHandles.Lookup LOOKUP = MethodHandles.publicLookup(); - private static final MethodType VOID_TYPE = MethodType.methodType(void.class); private final String name; private final Supplier value; diff --git a/core/src/main/java/io/micronaut/core/reflect/ClassUtils.java b/core/src/main/java/io/micronaut/core/reflect/ClassUtils.java index ea18d4e7c3d..ab6fc84b8e1 100644 --- a/core/src/main/java/io/micronaut/core/reflect/ClassUtils.java +++ b/core/src/main/java/io/micronaut/core/reflect/ClassUtils.java @@ -19,7 +19,6 @@ import io.micronaut.core.annotation.NonNull; import io.micronaut.core.annotation.Nullable; import io.micronaut.core.optim.StaticOptimizations; -import io.micronaut.core.util.ArrayUtils; import io.micronaut.core.util.CollectionUtils; import io.micronaut.core.util.StringUtils; import org.slf4j.Logger; @@ -36,7 +35,6 @@ import java.time.LocalDate; import java.time.ZonedDateTime; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collections; import java.util.Date; import java.util.HashMap; @@ -61,7 +59,6 @@ public class ClassUtils { * because this class is used both at compilation time and runtime, and we don't want logging at compilation time. */ public static final String PROPERTY_MICRONAUT_CLASSLOADER_LOGGING = "micronaut.classloader.logging"; - public static final int EMPTY_OBJECT_ARRAY_HASH_CODE = Arrays.hashCode(ArrayUtils.EMPTY_OBJECT_ARRAY); public static final Map> COMMON_CLASS_MAP = new HashMap<>(34); public static final Map> BASIC_TYPE_MAP = new HashMap<>(18); @@ -77,8 +74,8 @@ public class ClassUtils { private static final boolean ENABLE_CLASS_LOADER_LOGGING = Boolean.getBoolean(PROPERTY_MICRONAUT_CLASSLOADER_LOGGING); private static final Set MISSING_TYPES = StaticOptimizations.get(Optimizations.class) - .map(Optimizations::getMissingTypes) - .orElse(Collections.emptySet()); + .map(Optimizations::getMissingTypes) + .orElse(Collections.emptySet()); static { REFLECTION_LOGGER = getLogger(ClassUtils.class); @@ -87,26 +84,26 @@ public class ClassUtils { @SuppressWarnings("unchecked") private static final Map> PRIMITIVE_TYPE_MAP = CollectionUtils.mapOf( "int", Integer.TYPE, - "boolean", Boolean.TYPE, - "long", Long.TYPE, - "byte", Byte.TYPE, - "double", Double.TYPE, - "float", Float.TYPE, - "char", Character.TYPE, - "short", Short.TYPE, - "void", void.class + "boolean", Boolean.TYPE, + "long", Long.TYPE, + "byte", Byte.TYPE, + "double", Double.TYPE, + "float", Float.TYPE, + "char", Character.TYPE, + "short", Short.TYPE, + "void", void.class ); @SuppressWarnings("unchecked") private static final Map> PRIMITIVE_ARRAY_MAP = CollectionUtils.mapOf( - "int", int[].class, - "boolean", boolean[].class, - "long", long[].class, - "byte", byte[].class, - "double", double[].class, - "float", float[].class, - "char", char[].class, - "short", short[].class + "int", int[].class, + "boolean", boolean[].class, + "long", long[].class, + "byte", byte[].class, + "double", double[].class, + "float", float[].class, + "char", char[].class, + "short", short[].class ); static { @@ -170,6 +167,7 @@ public class ClassUtils { /** * Returns the array type for the given primitive type name. + * * @param primitiveType The primitive type name * @return The array type */ diff --git a/core/src/main/java/io/micronaut/core/type/Argument.java b/core/src/main/java/io/micronaut/core/type/Argument.java index ce71d052b73..0745db09768 100644 --- a/core/src/main/java/io/micronaut/core/type/Argument.java +++ b/core/src/main/java/io/micronaut/core/type/Argument.java @@ -15,19 +15,23 @@ */ package io.micronaut.core.type; -import io.micronaut.core.annotation.*; -import io.micronaut.core.naming.NameUtils; +import io.micronaut.core.annotation.AnnotatedElement; +import io.micronaut.core.annotation.AnnotationMetadata; +import io.micronaut.core.annotation.NonNull; +import io.micronaut.core.annotation.Nullable; +import io.micronaut.core.annotation.UsedByGeneratedCode; import io.micronaut.core.reflect.ReflectionUtils; import io.micronaut.core.util.ArrayUtils; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; import java.lang.reflect.TypeVariable; +import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Optional; import java.util.Set; -import java.util.Collections; /** * Represents an argument to a method or constructor or type. @@ -432,7 +436,7 @@ static Argument of( if (ArrayUtils.isEmpty(typeParameters)) { return of(type); } - return new DefaultArgument<>(type, NameUtils.decapitalize(type.getSimpleName()), AnnotationMetadata.EMPTY_METADATA, typeParameters); + return new DefaultArgument<>(type, null, AnnotationMetadata.EMPTY_METADATA, typeParameters); } /** @@ -507,6 +511,7 @@ static Argument of(@NonNull Class type, @Nullable Class... typePara /** * Creates a new argument for the given type and name. + * NOTE: This method should be avoided as it does use the reflection to retrieve the type parameter names. * * @param type The type * @param annotationMetadata The annotation metadata @@ -530,7 +535,7 @@ static Argument of(@NonNull Class type, @Nullable AnnotationMetadata a Argument[] typeArguments = new Argument[len]; for (int i = 0; i < parameters.length; i++) { TypeVariable> parameter = parameters[i]; - typeArguments[i] = Argument.of(typeParameters[i], parameter.getName()); + typeArguments[i] = Argument.ofTypeVariable(typeParameters[i], parameter.getName()); } return new DefaultArgument<>(type, annotationMetadata != null ? annotationMetadata : AnnotationMetadata.EMPTY_METADATA, typeArguments); } @@ -544,8 +549,7 @@ static Argument of(@NonNull Class type, @Nullable AnnotationMetadata a */ @NonNull static Argument> listOf(@NonNull Class type) { - //noinspection unchecked - return of((Class>) ((Class) List.class), type); + return listOf(Argument.ofTypeVariable(type, "E")); } /** @@ -559,7 +563,7 @@ static Argument> listOf(@NonNull Class type) { @NonNull static Argument> listOf(@NonNull Argument type) { //noinspection unchecked - return of((Class>) ((Class) List.class), type); + return of((Class>) ((Class) List.class), "list", type); } /** @@ -571,8 +575,7 @@ static Argument> listOf(@NonNull Argument type) { */ @NonNull static Argument> setOf(@NonNull Class type) { - //noinspection unchecked - return of((Class>) ((Class) Set.class), type); + return setOf(Argument.ofTypeVariable(type, "E")); } /** @@ -586,7 +589,7 @@ static Argument> setOf(@NonNull Class type) { @NonNull static Argument> setOf(@NonNull Argument type) { //noinspection unchecked - return of((Class>) ((Class) Set.class), type); + return of((Class>) ((Class) Set.class), "set", type); } /** @@ -600,8 +603,7 @@ static Argument> setOf(@NonNull Argument type) { */ @NonNull static Argument> mapOf(@NonNull Class keyType, @NonNull Class valueType) { - //noinspection unchecked - return of((Class>) ((Class) Map.class), keyType, valueType); + return mapOf(Argument.ofTypeVariable(keyType, "K"), Argument.ofTypeVariable(valueType, "V")); } /** @@ -617,7 +619,34 @@ static Argument> mapOf(@NonNull Class keyType, @NonNull Clas @NonNull static Argument> mapOf(@NonNull Argument keyType, @NonNull Argument valueType) { //noinspection unchecked - return of((Class>) ((Class) Map.class), keyType, valueType); + return of((Class>) ((Class) Map.class), "map", keyType, valueType); + } + + /** + * Creates a new argument representing an optional. + * + * @param optionalValueClass The optional type + * @param The optional type + * @return The argument instance + * @since 4.0.0 + */ + @NonNull + static Argument> optionalOf(@NonNull Class optionalValueClass) { + return optionalOf(Argument.ofTypeVariable(optionalValueClass, "T")); + } + + /** + * Creates a new argument representing an optional. + * + * @param optionalValueArgument The optional type + * @param The optional type + * @return The argument instance + * @since 4.0.0 + */ + @NonNull + static Argument> optionalOf(@NonNull Argument optionalValueArgument) { + //noinspection unchecked + return of((Class>) ((Class) Optional.class), "optional", optionalValueArgument); } } diff --git a/core/src/main/java/io/micronaut/core/type/DefaultArgument.java b/core/src/main/java/io/micronaut/core/type/DefaultArgument.java index 9ccd531865f..0661921ca96 100644 --- a/core/src/main/java/io/micronaut/core/type/DefaultArgument.java +++ b/core/src/main/java/io/micronaut/core/type/DefaultArgument.java @@ -19,6 +19,7 @@ import io.micronaut.core.annotation.Internal; import io.micronaut.core.annotation.NonNull; import io.micronaut.core.annotation.Nullable; +import io.micronaut.core.naming.NameUtils; import io.micronaut.core.util.ArrayUtils; import io.micronaut.core.util.CollectionUtils; import io.micronaut.core.util.ObjectUtils; @@ -26,7 +27,19 @@ import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; import java.lang.reflect.TypeVariable; -import java.util.*; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Deque; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Queue; +import java.util.Set; +import java.util.SortedSet; +import java.util.Vector; /** * Represents an argument to a constructor or method. @@ -60,6 +73,7 @@ public class DefaultArgument implements Argument, ArgumentCoercible { private final Argument[] typeParameterArray; private final AnnotationMetadata annotationMetadata; private final boolean isTypeVar; + private String namePrecalculated; /** * @param type The type @@ -214,10 +228,13 @@ public Class getType() { @Override @NonNull public String getName() { - if (name == null) { - return getType().getSimpleName(); + if (name != null) { + return name; + } + if (namePrecalculated == null) { + namePrecalculated = NameUtils.decapitalize(type.getSimpleName()); } - return name; + return namePrecalculated; } @Override diff --git a/core/src/main/java/io/micronaut/core/type/RuntimeTypeInformation.java b/core/src/main/java/io/micronaut/core/type/RuntimeTypeInformation.java index caf773753cc..963de7be24a 100644 --- a/core/src/main/java/io/micronaut/core/type/RuntimeTypeInformation.java +++ b/core/src/main/java/io/micronaut/core/type/RuntimeTypeInformation.java @@ -18,6 +18,7 @@ import io.micronaut.core.annotation.AnnotationMetadataProvider; import io.micronaut.core.annotation.Internal; import io.micronaut.core.annotation.NonNull; +import io.micronaut.core.reflect.ClassUtils; import java.util.ArrayList; import java.util.Collection; @@ -39,29 +40,23 @@ */ @Internal final class RuntimeTypeInformation { - private static final Map, Argument> WRAPPER_TO_TYPE = new HashMap<>(3); - private static final Collection TYPE_INFORMATION_PROVIDERS; - static { - WRAPPER_TO_TYPE.put(OptionalDouble.class, Argument.DOUBLE); - WRAPPER_TO_TYPE.put(OptionalLong.class, Argument.LONG); - WRAPPER_TO_TYPE.put(OptionalInt.class, Argument.INT); - final ServiceLoader loader = ServiceLoader.load(TypeInformationProvider.class); - List informationProviders = new ArrayList<>(2); - for (TypeInformationProvider informationProvider : loader) { - informationProviders.add(informationProvider); - } - - TYPE_INFORMATION_PROVIDERS = Collections.unmodifiableList(informationProviders); + private static boolean isJavaBasicTypeAndNotReactiveAndNotWrapper(Class type) { + // Not of them are reactive or wrappers + return ClassUtils.isJavaBasicType(type); } /** * Returns whether the annotation metadata specifies the type as single. + * @param type The return type * @param annotationMetadata The annotation metadata provider * @return True if does */ - static boolean isSpecifiedSingle(AnnotationMetadataProvider annotationMetadata) { - for (TypeInformationProvider provider : TYPE_INFORMATION_PROVIDERS) { + static boolean isSpecifiedSingle(Class type, AnnotationMetadataProvider annotationMetadata) { + if (isJavaBasicTypeAndNotReactiveAndNotWrapper(type)) { + return false; + } + for (TypeInformationProvider provider : LazyTypeInfo.TYPE_INFORMATION_PROVIDERS) { if (provider.isSpecifiedSingle(annotationMetadata)) { return true; } @@ -75,7 +70,10 @@ static boolean isSpecifiedSingle(AnnotationMetadataProvider annotationMetadata) * @return True if it is single */ static boolean isSingle(Class type) { - for (TypeInformationProvider provider : TYPE_INFORMATION_PROVIDERS) { + if (isJavaBasicTypeAndNotReactiveAndNotWrapper(type)) { + return false; + } + for (TypeInformationProvider provider : LazyTypeInfo.TYPE_INFORMATION_PROVIDERS) { if (provider.isSingle(type)) { return true; } @@ -89,7 +87,10 @@ static boolean isSingle(Class type) { * @return True if it is reactive */ static boolean isReactive(Class type) { - for (TypeInformationProvider provider : TYPE_INFORMATION_PROVIDERS) { + if (isJavaBasicTypeAndNotReactiveAndNotWrapper(type)) { + return false; + } + for (TypeInformationProvider provider : LazyTypeInfo.TYPE_INFORMATION_PROVIDERS) { if (provider.isReactive(type)) { return true; } @@ -103,7 +104,10 @@ static boolean isReactive(Class type) { * @return True if it is completable */ static boolean isCompletable(Class type) { - for (TypeInformationProvider provider : TYPE_INFORMATION_PROVIDERS) { + if (isJavaBasicTypeAndNotReactiveAndNotWrapper(type)) { + return false; + } + for (TypeInformationProvider provider : LazyTypeInfo.TYPE_INFORMATION_PROVIDERS) { if (provider.isCompletable(type)) { return true; } @@ -119,12 +123,15 @@ static boolean isCompletable(Class type) { * @see TypeInformation#isWrapperType() */ static boolean isWrapperType(Class type) { - for (TypeInformationProvider provider : TYPE_INFORMATION_PROVIDERS) { + if (isJavaBasicTypeAndNotReactiveAndNotWrapper(type)) { + return false; + } + for (TypeInformationProvider provider : LazyTypeInfo.TYPE_INFORMATION_PROVIDERS) { if (provider.isWrapperType(type)) { return true; } } - return type == Optional.class || WRAPPER_TO_TYPE.containsKey(type); + return type == Optional.class || LazyWrappers.WRAPPER_TO_TYPE.containsKey(type); } /** @@ -134,10 +141,33 @@ static boolean isWrapperType(Class type) { * @return The wrapped type */ static Argument getWrappedType(@NonNull TypeInformation typeInfo) { - final Argument a = WRAPPER_TO_TYPE.get(typeInfo.getType()); + final Argument a = LazyWrappers.WRAPPER_TO_TYPE.get(typeInfo.getType()); if (a != null) { return a; } return typeInfo.getFirstTypeVariable().orElse(Argument.OBJECT_ARGUMENT); } + + private static class LazyTypeInfo { + private static final Collection TYPE_INFORMATION_PROVIDERS; + + static { + final ServiceLoader loader = ServiceLoader.load(TypeInformationProvider.class); + List informationProviders = new ArrayList<>(2); + for (TypeInformationProvider informationProvider : loader) { + informationProviders.add(informationProvider); + } + TYPE_INFORMATION_PROVIDERS = Collections.unmodifiableList(informationProviders); + } + } + + private static class LazyWrappers { + private static final Map, Argument> WRAPPER_TO_TYPE = new HashMap<>(3); + + static { + WRAPPER_TO_TYPE.put(OptionalDouble.class, Argument.DOUBLE); + WRAPPER_TO_TYPE.put(OptionalLong.class, Argument.LONG); + WRAPPER_TO_TYPE.put(OptionalInt.class, Argument.INT); + } + } } diff --git a/core/src/main/java/io/micronaut/core/type/TypeInformation.java b/core/src/main/java/io/micronaut/core/type/TypeInformation.java index e1fa86d7aa5..54177074ee8 100644 --- a/core/src/main/java/io/micronaut/core/type/TypeInformation.java +++ b/core/src/main/java/io/micronaut/core/type/TypeInformation.java @@ -199,7 +199,7 @@ default boolean isOptional() { * @since 2.0 */ default boolean isSpecifiedSingle() { - return RuntimeTypeInformation.isSpecifiedSingle(this); + return RuntimeTypeInformation.isSpecifiedSingle(getType(), this); } /** diff --git a/core/src/main/java/io/micronaut/core/util/CollectionUtils.java b/core/src/main/java/io/micronaut/core/util/CollectionUtils.java index 5fcdeced248..98fbc1d5f67 100644 --- a/core/src/main/java/io/micronaut/core/util/CollectionUtils.java +++ b/core/src/main/java/io/micronaut/core/util/CollectionUtils.java @@ -31,6 +31,57 @@ */ public class CollectionUtils { + /** + * Create new {@link HashSet} sized to fit all the elements of the size provided. + * @param size The size to fit all the elements + * @param The element type + * @return a new {@link HashSet} with reallocated size + * @since 4.0.0 + */ + public static HashSet newHashSet(int size) { + return new HashSet<>(calculateHashSetSize(size)); + } + + /** + * Create new {@link LinkedHashSet} sized to fit all the elements of the size provided. + * @param size The size to fit all the elements + * @param The element type + * @return a new {@link LinkedHashSet} with reallocated size + * @since 4.0.0 + */ + public static LinkedHashSet newLinkedHashSet(int size) { + return new LinkedHashSet<>(calculateHashSetSize(size)); + } + + /** + * Create new {@link HashMap} sized to fit all the elements of the size provided. + * @param size The size to fit all the elements + * @param The key type + * @param The value type + * @return a new {@link HashMap} with reallocated size + * @since 4.0.0 + */ + public static HashMap newHashMap(int size) { + return new HashMap<>(calculateHashSetSize(size)); + } + + /** + * Create new {@link LinkedHashMap} sized to fit all the elements of the size provided. + * @param size The size to fit all the elements + * @param The key type + * @param The value type + * @return a new {@link LinkedHashMap} with reallocated size + * @since 4.0.0 + */ + public static LinkedHashMap newLinkedHashMap(int size) { + return new LinkedHashMap<>(calculateHashSetSize(size)); + } + + private static int calculateHashSetSize(int size) { + // Based on the calculation in new HashSet(Collection) + return Math.max((int) (size / .75f) + 1, 16); + } + /** * Is the given type an iterable or map type. * @param type The type diff --git a/core/src/main/java/io/micronaut/core/util/EnvironmentProperties.java b/core/src/main/java/io/micronaut/core/util/EnvironmentProperties.java index cd50a042118..c094812b82c 100644 --- a/core/src/main/java/io/micronaut/core/util/EnvironmentProperties.java +++ b/core/src/main/java/io/micronaut/core/util/EnvironmentProperties.java @@ -25,6 +25,7 @@ import java.util.List; import java.util.Locale; import java.util.Map; +import java.util.function.Function; /** * A mapping from environment variable names to Micronaut @@ -100,7 +101,13 @@ public List findPropertyNamesForEnvironmentVariable(String env) { return result; } } - return cache.computeIfAbsent(env, EnvironmentProperties::computePropertiesFor); + // Keep the anonymous class instead of Lambda to reduce the Lambda invocation overhead during the startup + return cache.computeIfAbsent(env, new Function>() { + @Override + public List apply(String env1) { + return computePropertiesFor(env1); + } + }); } private static List computePropertiesFor(String env) { diff --git a/core/src/main/java/io/micronaut/core/util/StreamUtils.java b/core/src/main/java/io/micronaut/core/util/StreamUtils.java index e2f18c592ad..43cff17a528 100644 --- a/core/src/main/java/io/micronaut/core/util/StreamUtils.java +++ b/core/src/main/java/io/micronaut/core/util/StreamUtils.java @@ -15,6 +15,8 @@ */ package io.micronaut.core.util; +import io.micronaut.core.annotation.NextMajorVersion; + import java.util.*; import java.util.function.BiConsumer; import java.util.function.BinaryOperator; @@ -189,7 +191,10 @@ public static > Collector> toImmu /** * @param The type * @return An immutable collection + * @deprecated use Stream#toList */ + @Deprecated(forRemoval = true) + @NextMajorVersion("Remove after Micronaut 4 milestone 1") public static Collector, Collection> toImmutableCollection() { return toImmutableCollection(ArrayList::new); } diff --git a/core/src/main/java/io/micronaut/core/util/SupplierUtil.java b/core/src/main/java/io/micronaut/core/util/SupplierUtil.java index c5dc87737d2..e70d8fc4af7 100644 --- a/core/src/main/java/io/micronaut/core/util/SupplierUtil.java +++ b/core/src/main/java/io/micronaut/core/util/SupplierUtil.java @@ -29,28 +29,31 @@ public class SupplierUtil { /** * Caches the result of supplier in a thread safe manner. * - * @param actual The supplier providing the result + * @param valueSupplier The supplier providing the result * @param The type of result * @return A new supplier that will cache the result */ - public static Supplier memoized(Supplier actual) { - return new Supplier() { - Supplier delegate = this::initialize; - boolean initialized; + public static Supplier memoized(Supplier valueSupplier) { + return new Supplier<>() { + private volatile boolean initialized; + private T value; // Doesn't need to be volatile @Override public T get() { - return delegate.get(); - } - - private synchronized T initialize() { + // Double check locking if (!initialized) { - T value = actual.get(); - delegate = () -> value; - initialized = true; + synchronized (this) { + if (!initialized) { + T t = valueSupplier.get(); + value = t; + initialized = true; + return t; + } + } } - return delegate.get(); + return value; } + }; } @@ -58,34 +61,37 @@ private synchronized T initialize() { * Caches the result of supplier in a thread safe manner. The result * is only cached if it is non null or non empty if an optional. * - * @param actual The supplier providing the result + * @param valueSupplier The supplier providing the result * @param The type of result * @return A new supplier that will cache the result */ - public static Supplier memoizedNonEmpty(Supplier actual) { - return new Supplier() { - Supplier delegate = this::initialize; - boolean initialized; + public static Supplier memoizedNonEmpty(Supplier valueSupplier) { + return new Supplier<>() { + private volatile boolean initialized; + private T value; // Doesn't need to be volatile @Override public T get() { - return delegate.get(); - } - - private synchronized T initialize() { + // Double check locking if (!initialized) { - T value = actual.get(); - if (value == null) { - return null; + synchronized (this) { + if (!initialized) { + T t = valueSupplier.get(); + if (t == null) { + return null; + } + if (t instanceof Optional optional && optional.isEmpty()) { + return t; + } + value = t; + initialized = true; + return t; + } } - if (value instanceof Optional && !((Optional) value).isPresent()) { - return value; - } - delegate = () -> value; - initialized = true; } - return delegate.get(); + return value; } + }; } } diff --git a/core/src/main/java/io/micronaut/core/value/MapPropertyResolver.java b/core/src/main/java/io/micronaut/core/value/MapPropertyResolver.java index 7d035ff7ffd..2ff7d2db72b 100644 --- a/core/src/main/java/io/micronaut/core/value/MapPropertyResolver.java +++ b/core/src/main/java/io/micronaut/core/value/MapPropertyResolver.java @@ -20,11 +20,13 @@ import io.micronaut.core.convert.ConversionService; import io.micronaut.core.util.StringUtils; +import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.Set; /** * A {@link PropertyResolver} that resolves values from a backing map. @@ -74,17 +76,23 @@ public Optional getProperty(String name, ArgumentConversionContext con public Collection getPropertyEntries(@NonNull String name) { if (StringUtils.isNotEmpty(name)) { String prefix = name + "."; - return map.keySet().stream().filter(k -> k.startsWith(prefix)) - .map(k -> { - String withoutPrefix = k.substring(prefix.length()); - int i = withoutPrefix.indexOf('.'); - if (i > -1) { - return withoutPrefix.substring(0, i); - } - return withoutPrefix; - }) - // to list to retain order from linked hash map - .toList(); + Set strings = map.keySet(); + // to list to retain order from linked hash map + List entries = new ArrayList<>(strings.size()); + for (String k : strings) { + if (k.startsWith(prefix)) { + String withoutPrefix = k.substring(prefix.length()); + int i = withoutPrefix.indexOf('.'); + String e; + if (i > -1) { + e = withoutPrefix.substring(0, i); + } else { + e = withoutPrefix; + } + entries.add(e); + } + } + return entries; } return Collections.emptySet(); } diff --git a/core/src/test/groovy/io/micronaut/core/convert/DefaultConversionServiceSpec.groovy b/core/src/test/groovy/io/micronaut/core/convert/DefaultConversionServiceSpec.groovy index 5c3cbb3d17d..55c51944f59 100644 --- a/core/src/test/groovy/io/micronaut/core/convert/DefaultConversionServiceSpec.groovy +++ b/core/src/test/groovy/io/micronaut/core/convert/DefaultConversionServiceSpec.groovy @@ -3,12 +3,10 @@ package io.micronaut.core.convert import io.micronaut.core.convert.exceptions.ConversionErrorException import io.micronaut.core.type.Argument import spock.lang.Specification -import spock.lang.Unroll import java.nio.charset.Charset import java.nio.charset.StandardCharsets import java.time.DayOfWeek - /** * Created by graemerocher on 12/06/2017. */ @@ -104,7 +102,7 @@ class DefaultConversionServiceSpec extends Specification { then: def e = thrown(ConversionErrorException) e.conversionError.originalValue.get() == 'junk' - e.message == 'Failed to convert argument [Integer] for value [junk] due to: For input string: "junk"' + e.message == 'Failed to convert argument [integer] for value [junk] due to: For input string: "junk"' } void "test conversion service with type arguments"() { diff --git a/core/src/test/groovy/io/micronaut/core/type/ArgumentSpec.groovy b/core/src/test/groovy/io/micronaut/core/type/ArgumentSpec.groovy index adfd161e132..70f8c5b1a40 100644 --- a/core/src/test/groovy/io/micronaut/core/type/ArgumentSpec.groovy +++ b/core/src/test/groovy/io/micronaut/core/type/ArgumentSpec.groovy @@ -111,8 +111,26 @@ class ArgumentSpec extends Specification { void "test equals/hashcode"() { expect: - Argument.of(Optional.class, Integer.class).hashCode() == Argument.of(Optional.class, Integer.class).hashCode() - Argument.of(Optional.class, Integer.class) == Argument.of(Optional.class, Integer.class) + Argument.optionalOf(Integer.class).getName() == Argument.of(Optional.class, Integer.class).getName() + Argument.optionalOf(Integer.class).getName() == "optional" + Argument.optionalOf(Integer.class).hashCode() == Argument.of(Optional.class, Integer.class).hashCode() + Argument.optionalOf(Integer.class) == Argument.of(Optional.class, Integer.class) + assertArgumentWithOneTypeParameter(Argument.of(Optional.class, Integer.class), Argument.optionalOf(Integer.class)) + assertArgumentWithOneTypeParameter(Argument.of(List.class, Integer.class), Argument.listOf(Integer.class)) + assertArgumentWithOneTypeParameter(Argument.of(Set.class, Integer.class), Argument.setOf(Integer.class)) + assertArgumentWithOneTypeParameter(Argument.of(Map.class, Integer.class, String.class), Argument.mapOf(Integer.class, String.class)) + } + + void assertArgumentWithOneTypeParameter(Argument a1, Argument a2) { + assertArgument(a1, a2) + assert a1.getTypeParameters() == a2.getTypeParameters() + assertArgument(a1.getTypeParameters()[0], a2.getTypeParameters()[0]) + } + + void assertArgument(Argument a1, Argument a2) { + assert a1 == a2 + assert a1.hashCode() == a2.hashCode() + assert a1.name == a2.name } void "test generic list"() { diff --git a/graal/src/main/java/io/micronaut/graal/reflect/GraalReflectionMetadataWriter.java b/graal/src/main/java/io/micronaut/graal/reflect/GraalReflectionMetadataWriter.java index fae0e9ce692..dd25faef3ec 100644 --- a/graal/src/main/java/io/micronaut/graal/reflect/GraalReflectionMetadataWriter.java +++ b/graal/src/main/java/io/micronaut/graal/reflect/GraalReflectionMetadataWriter.java @@ -15,9 +15,6 @@ */ package io.micronaut.graal.reflect; -import java.io.IOException; -import java.io.OutputStream; - import io.micronaut.core.annotation.AnnotationMetadata; import io.micronaut.core.graal.GraalReflectionConfigurer; import io.micronaut.inject.ast.ClassElement; @@ -27,6 +24,9 @@ import org.objectweb.asm.Type; import org.objectweb.asm.commons.GeneratorAdapter; +import java.io.IOException; +import java.io.OutputStream; + /** * Generates Runtime executed Graal configuration. * @@ -40,7 +40,7 @@ final class GraalReflectionMetadataWriter extends AbstractAnnotationMetadataWrit public GraalReflectionMetadataWriter(ClassElement originatingElement, AnnotationMetadata annotationMetadata) { - super(resolveName(originatingElement), originatingElement, annotationMetadata, false); + super(resolveName(originatingElement), originatingElement, annotationMetadata, true); this.className = targetClassType.getClassName(); this.classInternalName = targetClassType.getInternalName(); } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 5dfa82e03d2..2a7718c76a3 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -175,8 +175,8 @@ jcache = { module = "javax.cache:cache-api", version.ref = "jcache" } jetty-alpn-openjdk8-client = { module = "org.eclipse.jetty:jetty-alpn-openjdk8-client", version.ref = "jetty" } -jmh = { module = "org.openjdk.jmh:jmh-core", version.ref = "jmh" } -jmh-generator-annprocess = { module = "org.openjdk.jmh:jmh-core", version.ref = "jmh" } +jmh-core = { module = "org.openjdk.jmh:jmh-core", version.ref = "jmh" } +jmh-generator-annprocess = { module = "org.openjdk.jmh:jmh-generator-annprocess", version.ref = "jmh" } jsr107 = { module = "org.jsr107.ri:cache-ri-impl", version.ref = "jsr107" } jsr305 = { module = "com.google.code.findbugs:jsr305", version.ref = "jsr305" } diff --git a/http-client/src/test/groovy/io/micronaut/http/client/HttpGetSpec.groovy b/http-client/src/test/groovy/io/micronaut/http/client/HttpGetSpec.groovy index 37055b91f8c..5ace0e0d085 100644 --- a/http-client/src/test/groovy/io/micronaut/http/client/HttpGetSpec.groovy +++ b/http-client/src/test/groovy/io/micronaut/http/client/HttpGetSpec.groovy @@ -244,7 +244,7 @@ class HttpGetSpec extends Specification { void "test simple get request with POJO list"() { when: Flux>> flowable = Flux.from(client.exchange( - HttpRequest.GET("/get/pojoList"), Argument.of(List, Book) + HttpRequest.GET("/get/pojoList"), Argument.listOf(Book) )) HttpResponse> response = flowable.blockFirst() diff --git a/http-client/src/test/groovy/io/micronaut/http/client/HttpHeadSpec.groovy b/http-client/src/test/groovy/io/micronaut/http/client/HttpHeadSpec.groovy index cf6fbe3e9e4..ee50835f2ad 100644 --- a/http-client/src/test/groovy/io/micronaut/http/client/HttpHeadSpec.groovy +++ b/http-client/src/test/groovy/io/micronaut/http/client/HttpHeadSpec.groovy @@ -190,7 +190,7 @@ class HttpHeadSpec extends Specification { void "test simple get request with POJO list"() { when: Flux>> flowable = Flux.from(client.exchange( - HttpRequest.HEAD("/head/pojoList"), Argument.of(List, Book) + HttpRequest.HEAD("/head/pojoList"), Argument.listOf(Book) )) HttpResponse> response = flowable.blockFirst() diff --git a/http-client/src/test/groovy/io/micronaut/http/client/HttpPostSpec.groovy b/http-client/src/test/groovy/io/micronaut/http/client/HttpPostSpec.groovy index 75de09a4989..3b46e480043 100644 --- a/http-client/src/test/groovy/io/micronaut/http/client/HttpPostSpec.groovy +++ b/http-client/src/test/groovy/io/micronaut/http/client/HttpPostSpec.groovy @@ -325,7 +325,7 @@ class HttpPostSpec extends Specification { List booleans = blockingHttpClient.retrieve( HttpRequest.POST("/post/booleans", "[true, true, false]"), - Argument.of(List.class, Boolean.class) + Argument.listOf(Boolean.class) ) expect: diff --git a/http-client/src/test/groovy/io/micronaut/http/client/StreamRequestSpec.groovy b/http-client/src/test/groovy/io/micronaut/http/client/StreamRequestSpec.groovy index 97e6b590b34..303207d3f22 100644 --- a/http-client/src/test/groovy/io/micronaut/http/client/StreamRequestSpec.groovy +++ b/http-client/src/test/groovy/io/micronaut/http/client/StreamRequestSpec.groovy @@ -15,19 +15,22 @@ */ package io.micronaut.http.client -import io.micronaut.core.async.annotation.SingleResult import groovy.transform.EqualsAndHashCode import groovy.transform.ToString import io.micronaut.context.ApplicationContext import io.micronaut.context.annotation.Property import io.micronaut.context.annotation.Requires -import io.micronaut.core.annotation.NonNull +import io.micronaut.core.async.annotation.SingleResult import io.micronaut.core.type.Argument import io.micronaut.http.HttpHeaders import io.micronaut.http.HttpRequest import io.micronaut.http.HttpResponse import io.micronaut.http.MediaType -import io.micronaut.http.annotation.* +import io.micronaut.http.annotation.Body +import io.micronaut.http.annotation.Controller +import io.micronaut.http.annotation.Get +import io.micronaut.http.annotation.Header +import io.micronaut.http.annotation.Post import io.micronaut.http.client.annotation.Client import io.micronaut.runtime.server.EmbeddedServer import io.micronaut.test.extensions.spock.annotation.MicronautTest @@ -35,12 +38,10 @@ import jakarta.inject.Inject import org.reactivestreams.Publisher import reactor.core.publisher.Flux import reactor.core.publisher.FluxSink -import reactor.core.publisher.Mono import spock.lang.Specification import java.nio.charset.StandardCharsets import java.time.Duration - /** * @author graemerocher * @since 1.0 @@ -133,7 +134,7 @@ class StreamRequestSpec extends Specification { } emitter.complete() }, FluxSink.OverflowStrategy.BUFFER - )), Argument.of(List, Book))).blockFirst() + )), Argument.listOf(Book))).blockFirst() then: result.body().size() == 5 @@ -156,7 +157,7 @@ class StreamRequestSpec extends Specification { emitter.complete() }, FluxSink.OverflowStrategy.BUFFER - )), Argument.of(List, Book))).blockFirst() + )), Argument.listOf(Book))).blockFirst() then: result.body().size() == 5 diff --git a/http-client/src/test/groovy/io/micronaut/http/client/docs/basics/HelloControllerTest.java b/http-client/src/test/groovy/io/micronaut/http/client/docs/basics/HelloControllerTest.java index 81e7a7f14de..246692f4e5f 100644 --- a/http-client/src/test/groovy/io/micronaut/http/client/docs/basics/HelloControllerTest.java +++ b/http-client/src/test/groovy/io/micronaut/http/client/docs/basics/HelloControllerTest.java @@ -103,7 +103,7 @@ public void testRetrieveWithJSON() { // tag::jsonmaptypes[] response = Flux.from(client.retrieve( GET("/greet/John"), - Argument.of(Map.class, String.class, String.class) // <1> + Argument.mapOf(String.class, String.class) // <1> )); // end::jsonmaptypes[] diff --git a/http-netty/src/main/java/io/micronaut/http/netty/channel/converters/KQueueChannelOptionFactory.java b/http-netty/src/main/java/io/micronaut/http/netty/channel/converters/KQueueChannelOptionFactory.java index 7f4a00ff0e6..d2ef8fa420e 100644 --- a/http-netty/src/main/java/io/micronaut/http/netty/channel/converters/KQueueChannelOptionFactory.java +++ b/http-netty/src/main/java/io/micronaut/http/netty/channel/converters/KQueueChannelOptionFactory.java @@ -15,6 +15,7 @@ */ package io.micronaut.http.netty.channel.converters; +import io.micronaut.context.annotation.Prototype; import io.micronaut.context.annotation.Requires; import io.micronaut.context.env.Environment; import io.micronaut.core.annotation.Internal; @@ -26,7 +27,6 @@ import io.netty.channel.kqueue.KQueue; import io.netty.channel.kqueue.KQueueChannelOption; import io.netty.channel.unix.UnixChannelOption; -import jakarta.inject.Singleton; import java.util.Map; import java.util.Optional; @@ -36,7 +36,7 @@ * @author croudet */ @Internal -@Singleton +@Prototype @Requires(classes = KQueue.class, condition = KQueueAvailabilityCondition.class) public class KQueueChannelOptionFactory implements ChannelOptionFactory, TypeConverterRegistrar { diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/NettyHttpServer.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/NettyHttpServer.java index 158651c409b..57723f57d07 100644 --- a/http-server-netty/src/main/java/io/micronaut/http/server/netty/NettyHttpServer.java +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/NettyHttpServer.java @@ -16,6 +16,7 @@ package io.micronaut.http.server.netty; import io.micronaut.context.ApplicationContext; +import io.micronaut.context.DefaultApplicationContext; import io.micronaut.context.env.CachedEnvironment; import io.micronaut.context.env.Environment; import io.micronaut.context.event.ApplicationEventPublisher; @@ -260,6 +261,10 @@ public boolean isRunning() { public synchronized NettyEmbeddedServer start() { if (!isRunning()) { if (isDefault && !applicationContext.isRunning()) { + if (applicationContext instanceof DefaultApplicationContext defaultApplicationContext) { + // Stop did remove the existing environment + defaultApplicationContext.setEnvironment(environment); + } applicationContext.start(); } //suppress unused @@ -572,7 +577,6 @@ private static String displayAddress(NettyHttpServerConfiguration.NettyListenerC } private void fireStartupEvents() { - Optional applicationName = serverConfiguration.getApplicationConfiguration().getName(); applicationContext.getEventPublisher(ServerStartupEvent.class) .publishEvent(new ServerStartupEvent(this)); } diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/binders/NettyBinderRegistrar.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/binders/NettyBinderRegistrar.java index e575c472c95..35810048258 100644 --- a/http-server-netty/src/main/java/io/micronaut/http/server/netty/binders/NettyBinderRegistrar.java +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/binders/NettyBinderRegistrar.java @@ -17,6 +17,7 @@ import io.micronaut.context.BeanLocator; import io.micronaut.context.BeanProvider; +import io.micronaut.context.annotation.Prototype; import io.micronaut.context.event.BeanCreatedEvent; import io.micronaut.context.event.BeanCreatedEventListener; import io.micronaut.core.annotation.Internal; @@ -27,7 +28,6 @@ import io.micronaut.http.server.netty.multipart.MultipartBodyArgumentBinder; import io.micronaut.scheduling.TaskExecutors; import jakarta.inject.Named; -import jakarta.inject.Singleton; import java.util.concurrent.ExecutorService; @@ -37,7 +37,7 @@ * @author graemerocher * @since 2.0.0 */ -@Singleton +@Prototype @Internal class NettyBinderRegistrar implements BeanCreatedEventListener { diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/converters/NettyConverters.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/converters/NettyConverters.java index 74c9f9f7700..22ed62a320f 100644 --- a/http-server-netty/src/main/java/io/micronaut/http/server/netty/converters/NettyConverters.java +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/converters/NettyConverters.java @@ -16,6 +16,7 @@ package io.micronaut.http.server.netty.converters; import io.micronaut.context.BeanProvider; +import io.micronaut.context.annotation.Prototype; import io.micronaut.core.annotation.Internal; import io.micronaut.core.convert.ConversionService; import io.micronaut.core.convert.MutableConversionService; @@ -33,7 +34,6 @@ import io.netty.channel.ChannelOption; import io.netty.handler.codec.http.multipart.Attribute; import io.netty.handler.codec.http.multipart.FileUpload; -import jakarta.inject.Singleton; import java.io.IOException; import java.io.InputStream; @@ -47,13 +47,13 @@ * @author graemerocher * @since 1.0 */ -@Singleton +@Prototype @Internal public class NettyConverters implements TypeConverterRegistrar { private final ConversionService conversionService; private final BeanProvider decoderRegistryProvider; - private final ChannelOptionFactory channelOptionFactory; + private final BeanProvider channelOptionFactory; /** * Default constructor. @@ -64,7 +64,7 @@ public class NettyConverters implements TypeConverterRegistrar { public NettyConverters(ConversionService conversionService, //Prevent early initialization of the codecs BeanProvider decoderRegistryProvider, - ChannelOptionFactory channelOptionFactory) { + BeanProvider channelOptionFactory) { this.conversionService = conversionService; this.decoderRegistryProvider = decoderRegistryProvider; this.channelOptionFactory = channelOptionFactory; @@ -78,7 +78,7 @@ public void register(MutableConversionService conversionService) { (object, targetType, context) -> { String str = object.toString(); String name = NameUtils.underscoreSeparate(str).toUpperCase(Locale.ENGLISH); - return Optional.of(channelOptionFactory.channelOption(name)); + return Optional.of(channelOptionFactory.get().channelOption(name)); } ); @@ -109,7 +109,7 @@ public void register(MutableConversionService conversionService) { conversionService.addConverter( String.class, ChannelOption.class, - s -> channelOptionFactory.channelOption(NameUtils.environmentName(s)) + s -> channelOptionFactory.get().channelOption(NameUtils.environmentName(s)) ); } diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/discovery/NettyServiceDiscovery.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/discovery/NettyServiceDiscovery.java index e8f9ebe07cb..228e289d116 100644 --- a/http-server-netty/src/main/java/io/micronaut/http/server/netty/discovery/NettyServiceDiscovery.java +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/discovery/NettyServiceDiscovery.java @@ -17,10 +17,9 @@ import io.micronaut.context.ApplicationContext; import io.micronaut.context.annotation.Requires; -import io.micronaut.context.event.BeanCreatedEvent; -import io.micronaut.context.event.BeanCreatedEventListener; +import io.micronaut.context.event.ApplicationEventPublisher; import io.micronaut.core.annotation.Internal; -import io.micronaut.core.order.Ordered; +import io.micronaut.core.annotation.Nullable; import io.micronaut.discovery.ServiceInstance; import io.micronaut.discovery.event.ServiceReadyEvent; import io.micronaut.discovery.event.ServiceStoppedEvent; @@ -33,38 +32,44 @@ @Singleton @Internal @Requires(classes = ServiceInstance.class) -final class NettyServiceDiscovery implements BeanCreatedEventListener, Ordered { - private NettyEmbeddedServer server; - private NettyEmbeddedServerInstance instance; +final class NettyServiceDiscovery { + private final ApplicationEventPublisher serviceReadyEventApplicationEventPublisher; + private final ApplicationEventPublisher serviceStoppedEventApplicationEventPublisher; + private NettyEmbeddedServerInstance createdInstance; - @Override - public int getOrder() { - return Ordered.LOWEST_PRECEDENCE; + NettyServiceDiscovery(ApplicationEventPublisher serviceReadyEventApplicationEventPublisher, + ApplicationEventPublisher serviceStoppedEventApplicationEventPublisher) { + this.serviceReadyEventApplicationEventPublisher = serviceReadyEventApplicationEventPublisher; + this.serviceStoppedEventApplicationEventPublisher = serviceStoppedEventApplicationEventPublisher; } @EventListener void onStart(ServerStartupEvent event) { - if (instance != null) { - server.getApplicationContext() - .getEventPublisher(ServiceReadyEvent.class) - .publishEvent(new ServiceReadyEvent(instance)); + if (event.getSource() instanceof NettyEmbeddedServer nettyEmbeddedServer) { + NettyEmbeddedServerInstance instance = getInstance(nettyEmbeddedServer); + if (instance != null) { + serviceReadyEventApplicationEventPublisher.publishEvent(new ServiceReadyEvent(instance)); + } } } @EventListener void onStop(ServerShutdownEvent event) { - if (instance != null) { - server.getApplicationContext().getEventPublisher(ServiceStoppedEvent.class) - .publishEvent(new ServiceStoppedEvent(instance)); + if (event.getSource() instanceof NettyEmbeddedServer nettyEmbeddedServer) { + NettyEmbeddedServerInstance instance = getInstance(nettyEmbeddedServer); + if (instance != null) { + serviceStoppedEventApplicationEventPublisher.publishEvent(new ServiceStoppedEvent(instance)); + } } } - @Override - public NettyEmbeddedServer onCreated(BeanCreatedEvent event) { - this.server = event.getBean(); - ApplicationContext applicationContext = server.getApplicationContext(); - server.getApplicationConfiguration().getName() - .ifPresent(id -> this.instance = applicationContext.createBean(NettyEmbeddedServerInstance.class, id, server)); - return server; + @Nullable + private NettyEmbeddedServerInstance getInstance(NettyEmbeddedServer nettyEmbeddedServer) { + if (createdInstance == null) { + ApplicationContext applicationContext = nettyEmbeddedServer.getApplicationContext(); + nettyEmbeddedServer.getApplicationConfiguration().getName() + .ifPresent(id -> this.createdInstance = applicationContext.createBean(NettyEmbeddedServerInstance.class, id, nettyEmbeddedServer)); + } + return createdInstance; } } diff --git a/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/NettyStartStopSpec.groovy b/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/NettyStartStopSpec.groovy index 7d6acc893c6..818792694e7 100644 --- a/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/NettyStartStopSpec.groovy +++ b/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/NettyStartStopSpec.groovy @@ -15,6 +15,7 @@ class NettyStartStopSpec extends Specification { void "stopping and starting the netty server in a named application should work"() { given: + StartListener.globalEventCount.set(0) EmbeddedServer server = ApplicationContext.run(EmbeddedServer, [ 'spec.name': 'NettyStartStopSpec', 'micronaut.application.name': 'example' @@ -28,7 +29,8 @@ class NettyStartStopSpec extends Specification { server.start() then: - listener.eventCount.get() == 2 + StartListener.globalEventCount.get() == 2 + listener.eventCount.get() == 1 server.applicationContext.isRunning() cleanup: @@ -40,10 +42,12 @@ class NettyStartStopSpec extends Specification { @Requires(property = "spec.name", value = "NettyStartStopSpec") static class StartListener implements ApplicationEventListener { + static AtomicInteger globalEventCount = new AtomicInteger(0) AtomicInteger eventCount = new AtomicInteger(0) @Override void onApplicationEvent(ServiceReadyEvent event) { + globalEventCount.incrementAndGet() eventCount.incrementAndGet() } } diff --git a/http/src/main/java/io/micronaut/http/converters/HttpConverterRegistrar.java b/http/src/main/java/io/micronaut/http/converters/HttpConverterRegistrar.java index f0699e9bb78..176f5edc43f 100644 --- a/http/src/main/java/io/micronaut/http/converters/HttpConverterRegistrar.java +++ b/http/src/main/java/io/micronaut/http/converters/HttpConverterRegistrar.java @@ -15,13 +15,14 @@ */ package io.micronaut.http.converters; +import io.micronaut.context.annotation.Prototype; import io.micronaut.context.exceptions.ConfigurationException; import io.micronaut.core.convert.MutableConversionService; import io.micronaut.core.convert.TypeConverterRegistrar; import io.micronaut.core.io.Readable; import io.micronaut.core.io.ResourceLoader; import io.micronaut.core.io.ResourceResolver; -import jakarta.inject.Singleton; +import jakarta.inject.Provider; import java.net.URL; import java.util.Optional; @@ -32,17 +33,17 @@ * @author graemerocher * @since 2.0 */ -@Singleton +@Prototype public class HttpConverterRegistrar implements TypeConverterRegistrar { - private final ResourceResolver resourceResolver; + private final Provider resourceResolver; /** * Default constructor. * * @param resourceResolver The resource resolver */ - protected HttpConverterRegistrar(ResourceResolver resourceResolver) { + protected HttpConverterRegistrar(Provider resourceResolver) { this.resourceResolver = resourceResolver; } @@ -53,14 +54,14 @@ public void register(MutableConversionService conversionService) { Readable.class, (object, targetType, context) -> { String pathStr = object.toString(); - Optional supportingLoader = resourceResolver.getSupportingLoader(pathStr); + Optional supportingLoader = resourceResolver.get().getSupportingLoader(pathStr); if (!supportingLoader.isPresent()) { context.reject(pathStr, new ConfigurationException( "No supported resource loader for path [" + pathStr + "]. Prefix the path with a supported prefix such as 'classpath:' or 'file:'" )); return Optional.empty(); } else { - final Optional resource = resourceResolver.getResource(pathStr); + final Optional resource = resourceResolver.get().getResource(pathStr); if (resource.isPresent()) { return Optional.of(Readable.of(resource.get())); } else { diff --git a/inject-java/src/test/groovy/io/micronaut/inject/factory/nullreturn/NullReturnFactorySpec.groovy b/inject-java/src/test/groovy/io/micronaut/inject/factory/nullreturn/NullReturnFactorySpec.groovy index 460a53cd577..e3889587f5d 100644 --- a/inject-java/src/test/groovy/io/micronaut/inject/factory/nullreturn/NullReturnFactorySpec.groovy +++ b/inject-java/src/test/groovy/io/micronaut/inject/factory/nullreturn/NullReturnFactorySpec.groovy @@ -1,15 +1,13 @@ package io.micronaut.inject.factory.nullreturn +import io.micronaut.annotation.processing.test.AbstractTypeElementSpec import io.micronaut.context.ApplicationContext import io.micronaut.context.BeanContext -import io.micronaut.context.annotation.Parameter import io.micronaut.context.exceptions.BeanContextException -import io.micronaut.context.exceptions.BeanInstantiationException import io.micronaut.context.exceptions.DependencyInjectionException import io.micronaut.context.exceptions.NoSuchBeanException -import io.micronaut.annotation.processing.test.AbstractTypeElementSpec -import io.micronaut.inject.BeanDefinition import io.micronaut.inject.qualifiers.Qualifiers +import spock.lang.PendingFeature class NullReturnFactorySpec extends AbstractTypeElementSpec { @@ -74,7 +72,6 @@ class Test2 {} bs.any { it.name == "one" } bs.any { it.name == "two" } bs.any { it.name == "three" } - factory.bCalls == 4 //3 B beans, 1 null when: "1, 2 are created for C" Collection cs = beanContext.getBeansOfType(C) @@ -83,22 +80,20 @@ class Test2 {} cs.size() == 2 cs.any { it.name == "one" } cs.any { it.name == "two" } - factory.bCalls == 5 - factory.cCalls == 3 expect: "1 is created for D" beanContext.getBeansOfType(D).size() == 1 beanContext.getBean(D, Qualifiers.byName("one")) - factory.bCalls == 6 - factory.cCalls == 4 factory.dCalls == 2 //2 C beans and: "1 is created for D2" - beanContext.getBeansOfType(D2).size() == 1 + beanContext.getBeansOfType(D2).size() == 1 // D two is disabled + beanContext.getBeansOfType(D3).size() == 1 // D two is disabled + beanContext.getBeansOfType(D4).size() == 4 // Nullable C allowed beanContext.getBean(D2, Qualifiers.byName("one")) - factory.bCalls == 7 - factory.cCalls == 5 - factory.d2Calls == 2 //Called for 2 C beans and 2 null C beans + factory.d2Calls == 4 // called for every C and for C that are not enabled for existing B + factory.d3Calls == 2 // C is not nullable and there is no C three/four + factory.d4Calls == 4 // // called for every C and for C that are not enabled for existing B when: "E injects F which returns null" beanContext.getBean(E, Qualifiers.byName("one")) @@ -122,6 +117,25 @@ class Test2 {} beanContext.close() } + @PendingFeature + void "test bean factory method throwing DisableBeanException is cached"() { + given: + BeanContext beanContext = ApplicationContext.run(["spec.name": getClass().simpleName]) + NullableFactory factory = beanContext.getBean(NullableFactory) + + when: + beanContext.getBeansOfType(D2) + beanContext.getBeansOfType(D3) + beanContext.getBeansOfType(D4) + + then: + factory.bCalls == 4 // There are 4 B definitions (one throws disabled) + factory.cCalls == 3 // C is foreach B - should be called 3 times only + + cleanup: + beanContext.close() + } + void "test it works as expected nested resolution"() { given: BeanContext beanContext = ApplicationContext.run(["spec.name": getClass().simpleName]) @@ -151,8 +165,8 @@ class Test2 {} DProcessor.constructed.get() == 1 beanContext.getBeansOfType(ParameterDProcessor).size() == 1 ParameterDProcessor.constructed.get() == 1 - beanContext.getBeansOfType(NullableDProcessor).size() == 1 - NullableDProcessor.constructed.get() == 1 //3 null D beans and 1 D bean + beanContext.getBeansOfType(NullableDProcessor).size() == 4 + NullableDProcessor.constructed.get() == 4 //3 null D beans and 1 D bean when: beanContext.getBean(DProcessor, Qualifiers.byName("one")) diff --git a/inject-java/src/test/groovy/io/micronaut/inject/factory/nullreturn/NullableFactory.java b/inject-java/src/test/groovy/io/micronaut/inject/factory/nullreturn/NullableFactory.java index b31dbc98aa1..4641d1954ca 100644 --- a/inject-java/src/test/groovy/io/micronaut/inject/factory/nullreturn/NullableFactory.java +++ b/inject-java/src/test/groovy/io/micronaut/inject/factory/nullreturn/NullableFactory.java @@ -15,14 +15,12 @@ */ package io.micronaut.inject.factory.nullreturn; -import io.micronaut.context.annotation.*; - -import io.micronaut.core.annotation.Nullable; -import io.micronaut.context.condition.Condition; -import io.micronaut.context.condition.ConditionContext; +import io.micronaut.context.annotation.EachBean; +import io.micronaut.context.annotation.Factory; +import io.micronaut.context.annotation.Parameter; +import io.micronaut.context.annotation.Prototype; import io.micronaut.context.exceptions.DisabledBeanException; -import io.micronaut.core.annotation.AnnotationMetadataProvider; - +import io.micronaut.core.annotation.Nullable; import jakarta.inject.Named; import jakarta.inject.Singleton; @@ -34,6 +32,7 @@ public class NullableFactory { public int dCalls = 0; public int d2Calls = 0; public int d3Calls = 0; + public int d4Calls = 0; @Prototype A getA(@Parameter String name) { @@ -111,6 +110,12 @@ D3 getD3(@Parameter C c) { } } + @EachBean(C.class) + D4 getD4(@Nullable C c, @Nullable @Parameter B b) { + d4Calls++; + return new D4(); + } + @EachBean(D.class) E getE(D d, F f) { return new E(); @@ -142,6 +147,7 @@ class C { class D {} class D2 {} class D3 {} +class D4 {} class E {} class F {} diff --git a/inject-java/src/test/groovy/io/micronaut/inject/lifecycle/proxybeanwithpredestroy/ProxyBeanWithPreDestroySpec.groovy b/inject-java/src/test/groovy/io/micronaut/inject/lifecycle/proxybeanwithpredestroy/ProxyBeanWithPreDestroySpec.groovy index d2102302472..2b5f5aba384 100644 --- a/inject-java/src/test/groovy/io/micronaut/inject/lifecycle/proxybeanwithpredestroy/ProxyBeanWithPreDestroySpec.groovy +++ b/inject-java/src/test/groovy/io/micronaut/inject/lifecycle/proxybeanwithpredestroy/ProxyBeanWithPreDestroySpec.groovy @@ -15,10 +15,9 @@ */ package io.micronaut.inject.lifecycle.proxybeanwithpredestroy +import io.micronaut.context.ApplicationContext import io.micronaut.context.BeanContext -import io.micronaut.context.DefaultBeanContext import spock.lang.Specification - // proxyTarget = false proxies are always destroyed class ProxyBeanWithPreDestroySpec extends Specification { @@ -42,7 +41,7 @@ class ProxyBeanWithPreDestroySpec extends Specification { void "test cannot destroyed a proxy bean by the class name"() { given: - BeanContext context = new DefaultBeanContext() + BeanContext context = ApplicationContext.builder().properties("spec": getClass().getSimpleName()).build() context.start() when: @@ -60,7 +59,7 @@ class ProxyBeanWithPreDestroySpec extends Specification { void "test that a pre-destroy hook works"() { given: - BeanContext context = new DefaultBeanContext() + BeanContext context = ApplicationContext.builder().properties("spec": getClass().getSimpleName()).build() context.start() when: @@ -89,7 +88,7 @@ class ProxyBeanWithPreDestroySpec extends Specification { void "test that a pre-destroy hook works when destroyed by registration"() { given: - BeanContext context = new DefaultBeanContext() + BeanContext context = ApplicationContext.builder().properties("spec": getClass().getSimpleName()).build() context.start() when: @@ -121,7 +120,7 @@ class ProxyBeanWithPreDestroySpec extends Specification { void "test that a bean with a pre-destroy hook works closed on close"() { given: - BeanContext context = new DefaultBeanContext() + BeanContext context = ApplicationContext.builder().properties("spec": getClass().getSimpleName()).build() context.start() when: @@ -147,7 +146,7 @@ class ProxyBeanWithPreDestroySpec extends Specification { void "test that destroy events run in the right phase"() { given: - BeanContext context = new DefaultBeanContext() + BeanContext context = ApplicationContext.builder().properties("spec": getClass().getSimpleName()).build() context.start() diff --git a/inject-java/src/test/groovy/io/micronaut/inject/lifecycle/proxybeanwithpredestroy/package-info.java b/inject-java/src/test/groovy/io/micronaut/inject/lifecycle/proxybeanwithpredestroy/package-info.java new file mode 100644 index 00000000000..9240d01f269 --- /dev/null +++ b/inject-java/src/test/groovy/io/micronaut/inject/lifecycle/proxybeanwithpredestroy/package-info.java @@ -0,0 +1,5 @@ + +@Requires(property = "spec", value = "ProxyBeanWithPreDestroySpec") +package io.micronaut.inject.lifecycle.proxybeanwithpredestroy; + +import io.micronaut.context.annotation.Requires; diff --git a/inject-java/src/test/groovy/io/micronaut/inject/lifecycle/proxytargetbeanprototypewithpredestroy/ProxyLazyCachedTargetPrototypeBeanWithPreDestroySpec.groovy b/inject-java/src/test/groovy/io/micronaut/inject/lifecycle/proxytargetbeanprototypewithpredestroy/ProxyLazyCachedTargetPrototypeBeanWithPreDestroySpec.groovy index 69c80c1facd..19994649d01 100644 --- a/inject-java/src/test/groovy/io/micronaut/inject/lifecycle/proxytargetbeanprototypewithpredestroy/ProxyLazyCachedTargetPrototypeBeanWithPreDestroySpec.groovy +++ b/inject-java/src/test/groovy/io/micronaut/inject/lifecycle/proxytargetbeanprototypewithpredestroy/ProxyLazyCachedTargetPrototypeBeanWithPreDestroySpec.groovy @@ -15,8 +15,8 @@ */ package io.micronaut.inject.lifecycle.proxytargetbeanprototypewithpredestroy +import io.micronaut.context.ApplicationContext import io.micronaut.context.BeanContext -import io.micronaut.context.DefaultBeanContext import spock.lang.Specification class ProxyLazyCachedTargetPrototypeBeanWithPreDestroySpec extends Specification { @@ -41,7 +41,7 @@ class ProxyLazyCachedTargetPrototypeBeanWithPreDestroySpec extends Specification void "test that a lazy target bean with a pre-destroy hook works"() { given: - BeanContext context = new DefaultBeanContext() + BeanContext context = ApplicationContext.builder().properties("spec": getClass().getSimpleName()).build() context.start() when: @@ -71,7 +71,7 @@ class ProxyLazyCachedTargetPrototypeBeanWithPreDestroySpec extends Specification void "test that a proxy pre-destroy is not called on not-initialized target"() { given: - BeanContext context = new DefaultBeanContext() + BeanContext context = ApplicationContext.builder().properties("spec": getClass().getSimpleName()).build() context.start() when: @@ -100,7 +100,7 @@ class ProxyLazyCachedTargetPrototypeBeanWithPreDestroySpec extends Specification void "test that a lazy proxy bean with a pre-destroy hook works when destroyed by registration"() { given: - BeanContext context = new DefaultBeanContext() + BeanContext context = ApplicationContext.builder().properties("spec": getClass().getSimpleName()).build() context.start() when: @@ -133,7 +133,7 @@ class ProxyLazyCachedTargetPrototypeBeanWithPreDestroySpec extends Specification void "test that a lazy proxy bean with a pre-destroy hook is not called on not-initialized target"() { given: - BeanContext context = new DefaultBeanContext() + BeanContext context = ApplicationContext.builder().properties("spec": getClass().getSimpleName()).build() context.start() when: @@ -164,7 +164,7 @@ class ProxyLazyCachedTargetPrototypeBeanWithPreDestroySpec extends Specification void "test that a bean with a pre-destroy hook works closed on close"() { given: - BeanContext context = new DefaultBeanContext() + BeanContext context = ApplicationContext.builder().properties("spec": getClass().getSimpleName()).build() context.start() when: @@ -192,7 +192,7 @@ class ProxyLazyCachedTargetPrototypeBeanWithPreDestroySpec extends Specification void "test proxies are prototypes and dependent beans not destroyed when created by `getBean()`"() { given: - BeanContext context = new DefaultBeanContext() + BeanContext context = ApplicationContext.builder().properties("spec": getClass().getSimpleName()).build() context.start() when: @@ -221,10 +221,9 @@ class ProxyLazyCachedTargetPrototypeBeanWithPreDestroySpec extends Specification void "test that destroy events run in the right phase"() { given: - BeanContext context = new DefaultBeanContext() + BeanContext context = ApplicationContext.builder().properties("spec": getClass().getSimpleName()).build() context.start() - when: def pre = context.getBean(CPreDestroyEventListener) def post = context.getBean(CDestroyedListener) diff --git a/inject-java/src/test/groovy/io/micronaut/inject/lifecycle/proxytargetbeanprototypewithpredestroy/package-info.java b/inject-java/src/test/groovy/io/micronaut/inject/lifecycle/proxytargetbeanprototypewithpredestroy/package-info.java new file mode 100644 index 00000000000..fe528f0666c --- /dev/null +++ b/inject-java/src/test/groovy/io/micronaut/inject/lifecycle/proxytargetbeanprototypewithpredestroy/package-info.java @@ -0,0 +1,5 @@ + +@Requires(property = "spec", value = "ProxyLazyCachedTargetPrototypeBeanWithPreDestroySpec") +package io.micronaut.inject.lifecycle.proxytargetbeanprototypewithpredestroy; + +import io.micronaut.context.annotation.Requires; diff --git a/inject-java/src/test/groovy/io/micronaut/inject/lifecycle/proxytargetbeanwithpredestroy/ProxyTargetBeanWithPreDestroySpec.groovy b/inject-java/src/test/groovy/io/micronaut/inject/lifecycle/proxytargetbeanwithpredestroy/ProxyTargetBeanWithPreDestroySpec.groovy index b65aa31af1f..d2886c6868b 100644 --- a/inject-java/src/test/groovy/io/micronaut/inject/lifecycle/proxytargetbeanwithpredestroy/ProxyTargetBeanWithPreDestroySpec.groovy +++ b/inject-java/src/test/groovy/io/micronaut/inject/lifecycle/proxytargetbeanwithpredestroy/ProxyTargetBeanWithPreDestroySpec.groovy @@ -15,8 +15,8 @@ */ package io.micronaut.inject.lifecycle.proxytargetbeanwithpredestroy +import io.micronaut.context.ApplicationContext import io.micronaut.context.BeanContext -import io.micronaut.context.DefaultBeanContext import spock.lang.Specification class ProxyTargetBeanWithPreDestroySpec extends Specification { @@ -41,7 +41,7 @@ class ProxyTargetBeanWithPreDestroySpec extends Specification { void "test cannot destroyed a proxy bean by the class name"() { given: - BeanContext context = new DefaultBeanContext() + BeanContext context = ApplicationContext.builder().properties("spec": getClass().getSimpleName()).build() context.start() when: @@ -59,7 +59,7 @@ class ProxyTargetBeanWithPreDestroySpec extends Specification { void "test that a lazy target bean with a pre-destroy hook works"() { given: - BeanContext context = new DefaultBeanContext() + BeanContext context = ApplicationContext.builder().properties("spec": getClass().getSimpleName()).build() context.start() when: @@ -89,7 +89,7 @@ class ProxyTargetBeanWithPreDestroySpec extends Specification { void "test that a proxy pre-destroy is not called on not-initialized target"() { given: - BeanContext context = new DefaultBeanContext() + BeanContext context = ApplicationContext.builder().properties("spec": getClass().getSimpleName()).build() context.start() when: @@ -118,7 +118,7 @@ class ProxyTargetBeanWithPreDestroySpec extends Specification { void "test that a lazy proxy bean with a pre-destroy hook works when destroyed by registration"() { given: - BeanContext context = new DefaultBeanContext() + BeanContext context = ApplicationContext.builder().properties("spec": getClass().getSimpleName()).build() context.start() when: @@ -151,7 +151,7 @@ class ProxyTargetBeanWithPreDestroySpec extends Specification { void "test that a lazy proxy bean with a pre-destroy hook is not called on not-initialized target"() { given: - BeanContext context = new DefaultBeanContext() + BeanContext context = ApplicationContext.builder().properties("spec": getClass().getSimpleName()).build() context.start() when: @@ -182,7 +182,7 @@ class ProxyTargetBeanWithPreDestroySpec extends Specification { void "test that a bean with a pre-destroy hook works closed on close"() { given: - BeanContext context = new DefaultBeanContext() + BeanContext context = ApplicationContext.builder().properties("spec": getClass().getSimpleName()).build() context.start() when: @@ -210,7 +210,7 @@ class ProxyTargetBeanWithPreDestroySpec extends Specification { void "test proxies are prototypes and dependent beans not destroyed when created by `getBean()`"() { given: - BeanContext context = new DefaultBeanContext() + BeanContext context = ApplicationContext.builder().properties("spec": getClass().getSimpleName()).build() context.start() when: @@ -238,7 +238,7 @@ class ProxyTargetBeanWithPreDestroySpec extends Specification { void "test that destroy events run in the right phase"() { given: - BeanContext context = new DefaultBeanContext() + BeanContext context = ApplicationContext.builder().properties("spec": getClass().getSimpleName()).build() context.start() diff --git a/inject-java/src/test/groovy/io/micronaut/inject/lifecycle/proxytargetbeanwithpredestroy/package-info.java b/inject-java/src/test/groovy/io/micronaut/inject/lifecycle/proxytargetbeanwithpredestroy/package-info.java new file mode 100644 index 00000000000..aecee4075b0 --- /dev/null +++ b/inject-java/src/test/groovy/io/micronaut/inject/lifecycle/proxytargetbeanwithpredestroy/package-info.java @@ -0,0 +1,5 @@ + +@Requires(property = "spec", value = "ProxyTargetBeanWithPreDestroySpec") +package io.micronaut.inject.lifecycle.proxytargetbeanwithpredestroy; + +import io.micronaut.context.annotation.Requires; diff --git a/inject/src/main/java/io/micronaut/context/AbstractInitializableBeanDefinition.java b/inject/src/main/java/io/micronaut/context/AbstractInitializableBeanDefinition.java index 264bb273bf1..9e8731c8fcc 100644 --- a/inject/src/main/java/io/micronaut/context/AbstractInitializableBeanDefinition.java +++ b/inject/src/main/java/io/micronaut/context/AbstractInitializableBeanDefinition.java @@ -384,42 +384,53 @@ public final ConstructorInjectionPoint getConstructor() { return constructorInjectionPoint; } if (constructor == null) { - constructorInjectionPoint = new DefaultConstructorInjectionPoint<>( + DefaultConstructorInjectionPoint point = new DefaultConstructorInjectionPoint<>( this, getBeanType(), AnnotationMetadata.EMPTY_METADATA, Argument.ZERO_ARGUMENTS ); - } else { - if (constructor instanceof MethodReference methodConstructor) { - if ("".equals(methodConstructor.methodName)) { - this.constructorInjectionPoint = new DefaultConstructorInjectionPoint<>( - this, - methodConstructor.declaringType, - methodConstructor.annotationMetadata, - methodConstructor.arguments - ); - } else { - this.constructorInjectionPoint = new DefaultMethodConstructorInjectionPoint<>( - this, - methodConstructor.declaringType, - methodConstructor.methodName, - methodConstructor.arguments, - methodConstructor.annotationMetadata - ); + if (environment != null) { + point.configure(environment); + } + constructorInjectionPoint = point; + } else if (constructor instanceof MethodReference methodConstructor) { + if ("".equals(methodConstructor.methodName)) { + DefaultConstructorInjectionPoint point = new DefaultConstructorInjectionPoint<>( + this, + methodConstructor.declaringType, + methodConstructor.annotationMetadata, + methodConstructor.arguments + ); + if (environment != null) { + point.configure(environment); } - } else if (constructor instanceof FieldReference fieldConstructor) { - constructorInjectionPoint = new DefaultFieldConstructorInjectionPoint<>( + constructorInjectionPoint = point; + } else { + DefaultMethodConstructorInjectionPoint point = new DefaultMethodConstructorInjectionPoint<>( this, - fieldConstructor.declaringType, - type, - fieldConstructor.argument.getName(), - fieldConstructor.argument.getAnnotationMetadata() + methodConstructor.declaringType, + methodConstructor.methodName, + methodConstructor.arguments, + methodConstructor.annotationMetadata ); + if (environment != null) { + point.configure(environment); + } + constructorInjectionPoint = point; } - if (environment != null && constructorInjectionPoint instanceof EnvironmentConfigurable environmentConfigurable) { - environmentConfigurable.configure(environment); + } else if (constructor instanceof FieldReference fieldConstructor) { + DefaultFieldConstructorInjectionPoint point = new DefaultFieldConstructorInjectionPoint<>( + this, + fieldConstructor.declaringType, + type, + fieldConstructor.argument.getName(), + fieldConstructor.argument.getAnnotationMetadata() + ); + if (environment != null) { + point.configure(environment); } + constructorInjectionPoint = point; } return constructorInjectionPoint; } @@ -2131,6 +2142,9 @@ private K resolveBean( boolean isNotInnerConfiguration = !precalculatedInfo.isConfigurationProperties || !isInnerConfiguration(argument); ConfigurationPath previousPath = isNotInnerConfiguration ? resolutionContext.setConfigurationPath(null) : null; try { + if (argument.isDeclaredNullable()) { + return resolutionContext.findBean(argument, qualifier).orElse(null); + } return resolutionContext.getBean(argument, qualifier); } finally { if (previousPath != null) { @@ -2144,15 +2158,9 @@ private K resolveBean( if (isIterable() && getAnnotationMetadata().hasDeclaredAnnotation(EachBean.class)) { throw new DisabledBeanException("Bean [" + getBeanType().getSimpleName() + "] disabled by parent: " + e.getMessage()); } else { - if (argument.isDeclaredNullable()) { - return null; - } throw new DependencyInjectionException(resolutionContext, e); } } catch (NoSuchBeanException e) { - if (argument.isDeclaredNullable()) { - return null; - } throw new DependencyInjectionException(resolutionContext, e); } } diff --git a/inject/src/main/java/io/micronaut/context/AbstractInitializableBeanDefinitionReference.java b/inject/src/main/java/io/micronaut/context/AbstractInitializableBeanDefinitionReference.java index a11afc7532a..48afebbb258 100644 --- a/inject/src/main/java/io/micronaut/context/AbstractInitializableBeanDefinitionReference.java +++ b/inject/src/main/java/io/micronaut/context/AbstractInitializableBeanDefinitionReference.java @@ -176,8 +176,12 @@ public final Set> getExposedTypes() { @Override public BeanDefinition load(BeanContext context) { BeanDefinition definition = load(); - if (context instanceof ApplicationContext && definition instanceof EnvironmentConfigurable) { - ((EnvironmentConfigurable) definition).configure(((ApplicationContext) context).getEnvironment()); + if (context instanceof DefaultApplicationContext applicationContext + && definition instanceof EnvironmentConfigurable environmentConfigurable) { + // Performance optimization to check for the actual class to avoid the type-check pollution + environmentConfigurable.configure(applicationContext.getEnvironment()); + } else if (context instanceof ApplicationContext applicationContext && definition instanceof EnvironmentConfigurable environmentConfigurable) { + environmentConfigurable.configure(applicationContext.getEnvironment()); } return definition; } diff --git a/inject/src/main/java/io/micronaut/context/DefaultApplicationContext.java b/inject/src/main/java/io/micronaut/context/DefaultApplicationContext.java index b86a47d775d..68005577f6b 100644 --- a/inject/src/main/java/io/micronaut/context/DefaultApplicationContext.java +++ b/inject/src/main/java/io/micronaut/context/DefaultApplicationContext.java @@ -28,6 +28,7 @@ import io.micronaut.context.exceptions.ConfigurationException; import io.micronaut.context.exceptions.DependencyInjectionException; import io.micronaut.context.exceptions.NoSuchBeanException; +import io.micronaut.core.annotation.Internal; import io.micronaut.core.annotation.NonNull; import io.micronaut.core.annotation.Nullable; import io.micronaut.core.convert.ArgumentConversionContext; @@ -175,6 +176,14 @@ public Environment getEnvironment() { return environment; } + /** + * @param environment The environment + */ + @Internal + public void setEnvironment(Environment environment) { + this.environment = environment; + } + @Override @NonNull public synchronized ApplicationContext start() { @@ -265,7 +274,7 @@ protected void startEnvironment() { } @Override - protected void initializeContext(List contextScopeBeans, List processedBeans, List parallelBeans) { + protected void initializeContext(List contextScopeBeans, List processedBeans, List parallelBeans) { initializeTypeConverters(this); super.initializeContext(contextScopeBeans, processedBeans, parallelBeans); } @@ -456,7 +465,7 @@ private void transformConfigurationReaderBeanDefinition(BeanResolutionContex @SuppressWarnings("unchecked") Class declaringClass = (Class) candidate.getBeanType().getDeclaringClass(); if (declaringClass != null) { - Collection> beanCandidates = findBeanCandidates(resolutionContext, Argument.of(declaringClass), null, true); + Collection> beanCandidates = findBeanCandidates(resolutionContext, Argument.of(declaringClass), null); for (BeanDefinition beanCandidate : beanCandidates) { if (beanCandidate instanceof BeanDefinitionDelegate delegate) { ConfigurationPath cp = delegate.getConfigurationPath().orElse(configurationPath).copy(); @@ -810,12 +819,12 @@ protected void initializeEventListeners() { } @Override - protected void initializeContext(List contextScopeBeans, List processedBeans, List parallelBeans) { + protected void initializeContext(List contextScopeBeans, List processedBeans, List parallelBeans) { // no-op .. @Context scope beans are not started for bootstrap } @Override - protected void processParallelBeans(List parallelBeans) { + protected void processParallelBeans(List parallelBeans) { // no-op } diff --git a/inject/src/main/java/io/micronaut/context/DefaultApplicationContextBuilder.java b/inject/src/main/java/io/micronaut/context/DefaultApplicationContextBuilder.java index 659cafac302..8fe13d237ca 100644 --- a/inject/src/main/java/io/micronaut/context/DefaultApplicationContextBuilder.java +++ b/inject/src/main/java/io/micronaut/context/DefaultApplicationContextBuilder.java @@ -408,9 +408,7 @@ protected ApplicationContext newApplicationContext() { */ @NonNull private static ApplicationContextConfigurer loadApplicationContextCustomizer(@Nullable ClassLoader classLoader) { - SoftServiceLoader loader = classLoader != null ? SoftServiceLoader.load( - ApplicationContextConfigurer.class, classLoader - ) : SoftServiceLoader.load(ApplicationContextConfigurer.class); + SoftServiceLoader loader = SoftServiceLoader.load(ApplicationContextConfigurer.class, classLoader); List configurers = new ArrayList<>(10); loader.collectAll(configurers); if (configurers.isEmpty()) { diff --git a/inject/src/main/java/io/micronaut/context/DefaultBeanContext.java b/inject/src/main/java/io/micronaut/context/DefaultBeanContext.java index 13ad575e945..6ffa70bbb21 100644 --- a/inject/src/main/java/io/micronaut/context/DefaultBeanContext.java +++ b/inject/src/main/java/io/micronaut/context/DefaultBeanContext.java @@ -85,7 +85,6 @@ import io.micronaut.core.util.ArgumentUtils; import io.micronaut.core.util.ArrayUtils; import io.micronaut.core.util.CollectionUtils; -import io.micronaut.core.util.StreamUtils; import io.micronaut.core.util.StringUtils; import io.micronaut.core.util.clhm.ConcurrentLinkedHashMap; import io.micronaut.core.value.PropertyResolver; @@ -132,7 +131,6 @@ import java.util.HashSet; import java.util.IdentityHashMap; import java.util.Iterator; -import java.util.LinkedHashMap; import java.util.List; import java.util.ListIterator; import java.util.Map; @@ -140,12 +138,10 @@ import java.util.Optional; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.ForkJoinPool; import java.util.concurrent.Future; import java.util.concurrent.atomic.AtomicBoolean; -import java.util.function.Consumer; -import java.util.function.Function; import java.util.function.Predicate; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -161,7 +157,6 @@ public class DefaultBeanContext implements InitializableBeanContext { protected static final Logger LOG = LoggerFactory.getLogger(DefaultBeanContext.class); protected static final Logger LOG_LIFECYCLE = LoggerFactory.getLogger(DefaultBeanContext.class.getPackage().getName() + ".lifecycle"); - private static final String SCOPED_PROXY_ANN = "io.micronaut.runtime.context.scope.ScopedProxy"; private static final String INTRODUCTION_TYPE = "io.micronaut.aop.Introduction"; private static final String ADAPTER_TYPE = "io.micronaut.aop.Adapter"; @@ -174,6 +169,7 @@ public class DefaultBeanContext implements InitializableBeanContext { return Integer.compare(order1, order2); }; + protected final AtomicBoolean running = new AtomicBoolean(false); protected final AtomicBoolean initializing = new AtomicBoolean(false); protected final AtomicBoolean terminating = new AtomicBoolean(false); @@ -183,10 +179,13 @@ public class DefaultBeanContext implements InitializableBeanContext { private final SingletonScope singletonScope = new SingletonScope(); private final BeanContextConfiguration beanContextConfiguration; - private final Collection beanDefinitionsClasses = new ConcurrentLinkedQueue<>(); - private final Collection proxyTargetBeans = new ConcurrentLinkedQueue<>(); - private final Map, BeanDefinitionReference> disabledBeans = new ConcurrentHashMap<>(20); + // The collection should be modified only when new bean definition is added + // That shouldn't happen that often, so we can use CopyOnWriteArrayList + private final Collection beanDefinitionsClasses = new CopyOnWriteArrayList<>(); + private final Collection proxyTargetBeans = new CopyOnWriteArrayList<>(); + + private final Map, BeanDefinitionProducer> disabledBeans = new ConcurrentHashMap<>(20); private final Map> disabledConfigurations = new ConcurrentHashMap<>(5); private final Map beanConfigurations = new HashMap<>(10); private final Map containsBeanCache = new ConcurrentHashMap<>(30); @@ -202,7 +201,7 @@ public class DefaultBeanContext implements InitializableBeanContext { private final Map> beanCandidateCache = new ConcurrentLinkedHashMap.Builder>().maximumWeightedCapacity(30).build(); - private final Map, Collection> beanIndex = new ConcurrentHashMap<>(12); + private final Map, Collection> beanIndex = new ConcurrentHashMap<>(12); private final ClassLoader classLoader; private final Set> thisInterfaces = CollectionUtils.setOf( @@ -387,11 +386,11 @@ void trackDisabledComponent(@NonNull Cond Argument argument = (Argument) beanType.getGenericBeanType(); @SuppressWarnings("unchecked") Qualifier declaredQualifier = (Qualifier) beanType.getDeclaredQualifier(); - this.disabledBeans.put(new BeanKey<>(argument, declaredQualifier), new DisabledBean<>( + this.disabledBeans.put(new BeanKey<>(argument, declaredQualifier), new BeanDefinitionProducer(new DisabledBean<>( argument, declaredQualifier, reasons - )); + ))); } catch (Exception | NoClassDefFoundError e) { // it is theoretically possible that resolving the generic type results in an error // in this case just ignore this as the maps built here are purely to aid error diagnosis @@ -450,12 +449,22 @@ public synchronized BeanContext stop() { } } + singlesInCreation.clear(); singletonBeanRegistrations.clear(); beanConcreteCandidateCache.clear(); beanCandidateCache.clear(); + beanProxyTargetCache.clear(); containsBeanCache.clear(); beanConfigurations.clear(); + disabledConfigurations.clear(); singletonScope.clear(); + beanDefinitionsClasses.clear(); + disabledBeans.clear(); + proxyTargetBeans.clear(); + attributes.clear(); + beanIndex.clear(); + beanConfigurationsList = null; + beanDefinitionReferences = null; beanInitializedEventListeners = null; beanCreationEventListeners = null; beanPreDestroyEventListeners = null; @@ -1134,7 +1143,7 @@ private Map resolveArgumentValues(BeanResolutionContext reso LOG.trace("Creating bean for parameters: {}", ArrayUtils.toString(args)); } MutableConversionService conversionService = getConversionService(); - Map argumentValues = new LinkedHashMap<>(requiredArguments.length); + Map argumentValues = CollectionUtils.newLinkedHashMap(requiredArguments.length); BeanResolutionContext.Path currentPath = resolutionContext.getPath(); for (int i = 0; i < requiredArguments.length; i++) { Argument requiredArgument = requiredArguments[i]; @@ -1306,8 +1315,7 @@ private T triggerPreDestroyListeners(@NonNull BeanDefinition beanDefiniti private void destroyProxyTargetBean(@NonNull BeanRegistration registration, boolean dependent) { Set destroyed = Collections.emptySet(); - if (registration instanceof BeanDisposingRegistration) { - BeanDisposingRegistration disposingRegistration = (BeanDisposingRegistration) registration; + if (registration instanceof BeanDisposingRegistration disposingRegistration) { if (disposingRegistration.getDependents() != null) { destroyed = Collections.newSetFromMap(new IdentityHashMap<>()); for (BeanRegistration beanRegistration : disposingRegistration.getDependents()) { @@ -1319,7 +1327,7 @@ private void destroyProxyTargetBean(@NonNull BeanRegistration registratio BeanDefinition proxyTargetBeanDefinition = findProxyTargetBeanDefinition(registration.beanDefinition) .orElseThrow(() -> new IllegalStateException("Cannot find a proxy target bean definition for: " + registration.beanDefinition)); Optional> declaredScope = customScopeRegistry.findDeclaredScope(proxyTargetBeanDefinition); - if (!declaredScope.isPresent()) { + if (declaredScope.isEmpty()) { if (proxyTargetBeanDefinition.isSingleton()) { return; } @@ -1490,7 +1498,6 @@ public Collection getBeansOfType(@Nullable BeanResolutionContext resoluti return list; } - @SuppressWarnings("unchecked") @Override @NonNull public T getProxyTargetBean(@NonNull Class beanType, @Nullable Qualifier qualifier) { @@ -1583,7 +1590,9 @@ public Collection> getBeanDefinitions(@Nullable Qualifier reduced = qualifier.reduce(Object.class, beanDefinitionsClasses.stream()); + Stream reduced = qualifier.reduce(Object.class, beanDefinitionsClasses.stream() + .filter(p -> p.isReferenceEnabled(this)) + .map(BeanDefinitionProducer::getReference)); Stream candidateStream = qualifier.reduce(Object.class, reduced .map(ref -> ref.load(this)) @@ -1609,12 +1618,11 @@ public Collection> getAllBeanDefinitions() { } if (!beanDefinitionsClasses.isEmpty()) { - List collection = beanDefinitionsClasses + return beanDefinitionsClasses .stream() - .map(ref -> ref.load(this)) - .filter(candidate -> candidate.isEnabled(this)) + .filter(p -> p.isDefinitionEnabled(this)) + .map(p -> p.getDefinition(this)) .collect(Collectors.toList()); - return collection; } return (Collection>) Collections.emptyMap(); @@ -1626,7 +1634,8 @@ public Collection> getAllBeanDefinitions() { public Collection> getBeanDefinitionReferences() { if (!beanDefinitionsClasses.isEmpty()) { final List refs = beanDefinitionsClasses.stream() - .filter(ref -> ref.isEnabled(this)) + .filter(p -> p.isReferenceEnabled(this)) + .map(BeanDefinitionProducer::getReference) .toList(); return refs; @@ -1634,17 +1643,17 @@ public Collection> getBeanDefinitionReferences() { return Collections.emptyList(); } - @SuppressWarnings("unchecked") @Override @NonNull public BeanContext registerBeanDefinition(@NonNull RuntimeBeanDefinition definition) { Objects.requireNonNull(definition, "Bean definition cannot be null"); Class beanType = definition.getBeanType(); - this.beanDefinitionsClasses.add(definition); + BeanDefinitionProducer producer = new BeanDefinitionProducer(definition); + this.beanDefinitionsClasses.add(producer); for (Class indexedType : indexedTypes) { if (indexedType == beanType || indexedType.isAssignableFrom(beanType)) { - final Collection indexed = resolveTypeIndex(indexedType); - indexed.add(definition); + final Collection indexed = resolveTypeIndex(indexedType); + indexed.add(producer); break; } } @@ -1669,12 +1678,11 @@ void removeBeanDefinition(RuntimeBeanDefinition definition) { Class beanType = definition.getBeanType(); for (Class indexedType : indexedTypes) { if (indexedType == beanType || indexedType.isAssignableFrom(beanType)) { - final Collection indexed = resolveTypeIndex(indexedType); - indexed.remove(definition); + resolveTypeIndex(indexedType).forEach(p -> p.disable(definition)); break; } } - this.beanDefinitionsClasses.remove(definition); + beanDefinitionsClasses.forEach(p -> p.disable(definition)); purgeCacheForBeanType(definition.getBeanType()); } @@ -1971,7 +1979,7 @@ private Map, List>> getType if (beanDefinitions.isEmpty()) { return Collections.emptyMap(); } - final HashMap, List>> typeToListener = new HashMap<>(beanDefinitions.size(), 1); + final HashMap, List>> typeToListener = CollectionUtils.newHashMap(beanDefinitions.size()); for (BeanDefinition beanCreatedDefinition : beanDefinitions) { List> typeArguments = beanCreatedDefinition.getTypeArguments(listenerType); Argument argument = CollectionUtils.last(typeArguments); @@ -1987,142 +1995,117 @@ private Map, List>> getType /** * Initialize the context with the given {@link io.micronaut.context.annotation.Context} scope beans. * - * @param contextScopeBeans The context scope beans + * @param eagerInitBeans The context scope beans * @param processedBeans The beans that require {@link ExecutableMethodProcessor} handling * @param parallelBeans The parallel bean definitions */ + @Internal protected void initializeContext( - @NonNull List contextScopeBeans, - @NonNull List processedBeans, - @NonNull List parallelBeans) { + @NonNull List eagerInitBeans, + @NonNull List processedBeans, + @NonNull List parallelBeans) { - if (CollectionUtils.isNotEmpty(contextScopeBeans)) { - final List contextBeans = new ArrayList<>(contextScopeBeans.size()); - - for (BeanDefinitionReference contextScopeBean : contextScopeBeans) { + if (CollectionUtils.isNotEmpty(eagerInitBeans)) { + final List> eagerInit = new ArrayList<>(eagerInitBeans.size()); + for (BeanDefinitionProducer contextScopeBean : eagerInitBeans) { try { - loadContextScopeBean(contextScopeBean, contextBeans::add); + loadEagerBeans(contextScopeBean, eagerInit); } catch (Throwable e) { - throw new BeanInstantiationException("Bean definition [" + contextScopeBean.getName() + "] could not be loaded: " + e.getMessage(), e); + throw new BeanInstantiationException("Bean definition [" + contextScopeBean.getReference().getName() + "] could not be loaded: " + e.getMessage(), e); } } - filterReplacedBeans(null, (Collection) contextBeans); - OrderUtil.sort(contextBeans); - for (BeanDefinition contextScopeDefinition : contextBeans) { + filterReplacedBeans(null, eagerInit); + OrderUtil.sort(eagerInit); + for (BeanDefinition eagerInitDefinition : eagerInit) { try { - loadContextScopeBean(contextScopeDefinition); + initializeEagerBean(eagerInitDefinition); } catch (DisabledBeanException e) { if (AbstractBeanContextConditional.ConditionLog.LOG.isDebugEnabled()) { - AbstractBeanContextConditional.ConditionLog.LOG.debug("Bean of type [{}] disabled for reason: {}", contextScopeDefinition.getBeanType().getSimpleName(), e.getMessage()); + AbstractBeanContextConditional.ConditionLog.LOG.debug("Bean of type [{}] disabled for reason: {}", eagerInitDefinition.getBeanType().getSimpleName(), e.getMessage()); } } catch (Throwable e) { - throw new BeanInstantiationException("Bean definition [" + contextScopeDefinition.getName() + "] could not be loaded: " + e.getMessage(), e); + throw new BeanInstantiationException("Bean definition [" + eagerInitDefinition.getName() + "] could not be loaded: " + e.getMessage(), e); } } } if (!processedBeans.isEmpty()) { + List> methodsToProcess = new ArrayList<>(); + for (BeanDefinitionProducer processedBeanProducer : processedBeans) { + if (!processedBeanProducer.isDefinitionEnabled(this)) { + continue; + } + BeanDefinition definition = processedBeanProducer.getDefinition(this); + for (ExecutableMethod method : definition.getExecutableMethods()) { + if (method.hasStereotype(Executable.class)) { + methodsToProcess.add(BeanDefinitionMethodReference.of(definition, (ExecutableMethod) method)); + } + } + } - @SuppressWarnings("unchecked") Stream> methodStream = processedBeans - .stream() - // is the bean reference enabled - .filter(ref -> ref.isEnabled(this)) - // ok - continue and load it - .map((Function>) reference -> { - try { - return reference.load(this); - } catch (Exception e) { - throw new BeanInstantiationException("Bean definition [" + reference.getName() + "] could not be loaded: " + e.getMessage(), e); - } - }) - // is the bean itself enabled - .filter(bean -> bean.isEnabled(this)) - // ok continue and get all of the ExecutableMethod references - .flatMap(beanDefinition -> - beanDefinition.getExecutableMethods() - .parallelStream() - .filter(method -> method.hasStereotype(Executable.class)) - .map((Function, BeanDefinitionMethodReference>) executableMethod -> - BeanDefinitionMethodReference.of((BeanDefinition) beanDefinition, executableMethod) - ) - ); - + Map, List>> byAnnotation = CollectionUtils.newHashMap(methodsToProcess.size()); // group the method references by annotation type such that we have a map of Annotation -> MethodReference // ie. Class -> @Scheduled void someAnnotation() - Map, List>> byAnnotation = new HashMap<>(processedBeans.size()); - methodStream.forEach(reference -> { - List> annotations = reference.getAnnotationTypesByStereotype(Executable.class); - annotations.forEach(annotation -> byAnnotation.compute(annotation, (ann, list) -> { - if (list == null) { - list = new ArrayList<>(10); + for (BeanDefinitionMethodReference executableMethod : methodsToProcess) { + List> annotations = executableMethod.getAnnotationTypesByStereotype(Executable.class); + for (Class annotation : annotations) { + List> references = byAnnotation.get(annotation); + if (references == null) { + references = new ArrayList<>(10); + byAnnotation.put(annotation, references); } - list.add(reference); - return list; - })); - }); + references.add(executableMethod); + } + } // Find ExecutableMethodProcessor for each annotation and process the BeanDefinitionMethodReference - byAnnotation.forEach((annotationType, methods) -> - streamOfType(ExecutableMethodProcessor.class, Qualifiers.byTypeArguments(annotationType)) - .forEach(processor -> { - if (processor instanceof LifeCycle) { - ((LifeCycle) processor).start(); - } - for (BeanDefinitionMethodReference method : methods) { - - BeanDefinition beanDefinition = method.getBeanDefinition(); - - // Only process the method if the the annotation is not declared at the class level - // If declared at the class level it will already have been processed by AnnotationProcessorListener - if (!beanDefinition.hasStereotype(annotationType)) { - //noinspection unchecked - if (method.hasDeclaredStereotype(Parallel.class)) { - ForkJoinPool.commonPool().execute(() -> { - try { - processor.process(beanDefinition, method); - } catch (Throwable e) { - if (LOG.isErrorEnabled()) { - LOG.error("Error processing bean method " + beanDefinition + "." + method + " with processor (" + processor + "): " + e.getMessage(), e); - } - Boolean shutdownOnError = method.booleanValue(Parallel.class, "shutdownOnError").orElse(true); - if (shutdownOnError) { - stop(); - } - } - }); - } else { + for (Map.Entry, List>> entry : byAnnotation.entrySet()) { + Class annotationType = entry.getKey(); + List> methods = entry.getValue(); + streamOfType(ExecutableMethodProcessor.class, Qualifiers.byTypeArguments(annotationType)) + .forEach(processor -> { + if (processor instanceof LifeCycle) { + ((LifeCycle) processor).start(); + } + for (BeanDefinitionMethodReference method : methods) { + + BeanDefinition beanDefinition = method.getBeanDefinition(); + + // Only process the method if the annotation is not declared at the class level + // If declared at the class level it will already have been processed by AnnotationProcessorListener + if (!beanDefinition.hasStereotype(annotationType)) { + if (method.hasDeclaredStereotype(Parallel.class)) { + ForkJoinPool.commonPool().execute(() -> { + try { processor.process(beanDefinition, method); + } catch (Throwable e) { + if (LOG.isErrorEnabled()) { + LOG.error("Error processing bean method " + beanDefinition + "." + method + " with processor (" + processor + "): " + e.getMessage(), e); + } + Boolean shutdownOnError = method.booleanValue(Parallel.class, "shutdownOnError").orElse(true); + if (shutdownOnError) { + stop(); + } } - } + }); + } else { + processor.process(beanDefinition, method); } + } + } - if (processor instanceof LifeCycle) { - ((LifeCycle) processor).stop(); - } + if (processor instanceof LifeCycle) { + ((LifeCycle) processor).stop(); + } - })); + }); + } } if (CollectionUtils.isNotEmpty(parallelBeans)) { processParallelBeans(parallelBeans); } - final Runnable runnable = () -> - beanDefinitionsClasses.removeIf((BeanDefinitionReference beanDefinitionReference) -> - !beanDefinitionReference.isEnabled(this)); - ForkJoinPool.commonPool().execute(runnable); - } - - /** - * Find bean candidates for the given type. - * - * @param The bean generic type - * @param beanType The bean type - * @param filter A bean definition to filter out - * @return The candidates - */ - @NonNull - protected Collection> findBeanCandidates(@NonNull Class beanType, @Nullable BeanDefinition filter) { - return findBeanCandidates(null, Argument.of(beanType), filter, true); + ForkJoinPool.commonPool().execute(() -> beanDefinitionsClasses.forEach(p -> p.isReferenceEnabled(this))); } /** @@ -2132,14 +2115,12 @@ protected Collection> findBeanCandidates(@NonNull Class * @param resolutionContext The current resolution context * @param beanType The bean type * @param filter A bean definition to filter out - * @param filterProxied Whether to filter out bean proxy targets * @return The candidates */ @NonNull protected Collection> findBeanCandidates(@Nullable BeanResolutionContext resolutionContext, @NonNull Argument beanType, - @Nullable BeanDefinition filter, - boolean filterProxied) { + @Nullable BeanDefinition filter) { Predicate> predicate = filter == null ? null : definition -> !definition.equals(filter); return findBeanCandidates(resolutionContext, beanType, true, predicate); } @@ -2166,7 +2147,7 @@ protected Collection> findBeanCandidates(@Nullable BeanRes } // first traverse component definition classes and load candidates - Collection beanDefinitionsClasses; + Collection beanDefinitionsClasses; if (indexedTypes.contains(beanClass)) { beanDefinitionsClasses = beanIndex.get(beanClass); @@ -2191,29 +2172,25 @@ private Set> collectBeanCandidates( BeanResolutionContext resolutionContext, Argument beanType, boolean collectIterables, + @Nullable Predicate> predicate, - Collection beanDefinitionsClasses) { + Collection beanDefinitionProducers) { Set> candidates; - if (!beanDefinitionsClasses.isEmpty()) { + if (!beanDefinitionProducers.isEmpty()) { candidates = new HashSet<>(); - for (BeanDefinitionReference reference : beanDefinitionsClasses) { - if (!reference.isCandidateBean(beanType) || !reference.isEnabled(this, resolutionContext)) { + for (BeanDefinitionProducer producer : beanDefinitionProducers) { + if (producer.isDisabled() || !producer.isReferenceCandidateBean(beanType) || !producer.isReferenceEnabled(this, resolutionContext)) { continue; } - BeanDefinition loadedBean; - try { - loadedBean = reference.load(this); - } catch (Throwable e) { - throw new BeanContextException("Error loading bean [" + reference.getName() + "]: " + e.getMessage(), e); - } + BeanDefinition loadedBean = producer.getDefinition(this); if (!loadedBean.isCandidateBean(beanType)) { continue; } if (predicate != null && !predicate.test(loadedBean)) { continue; } - if (!loadedBean.isEnabled(this, resolutionContext)) { + if (!producer.isDefinitionEnabled(this, resolutionContext)) { continue; } @@ -2267,50 +2244,52 @@ protected Collection> findBeanCandidatesForInstance(@NonNu if (LOG.isDebugEnabled()) { LOG.debug("Finding candidate beans for instance: {}", instance); } - Collection> beanDefinitionsClasses = ((Collection) this.beanDefinitionsClasses); + Collection beanProducers = this.beanDefinitionsClasses; final Class beanClass = instance.getClass(); Argument beanType = Argument.of(beanClass); Collection> beanDefinitions = (Collection>) ((Map) beanCandidateCache).get(beanType); - if (beanDefinitions == null) { - // first traverse component definition classes and load candidates - if (!beanDefinitionsClasses.isEmpty()) { - List> candidates = new ArrayList<>(); - for (BeanDefinitionReference reference : beanDefinitionsClasses) { - if (!reference.isEnabled(this)) { - continue; - } - Class candidateType = reference.getBeanType(); - if (candidateType == null || !candidateType.isInstance(instance)) { - continue; - } - BeanDefinition candidate = reference.load(this); - if (!candidate.isEnabled(this)) { - continue; - } - candidates.add(candidate); - } - - if (candidates.size() > 1) { - // try narrow to exact type - candidates = candidates - .stream() - .filter(candidate -> - candidate.getBeanType() == beanClass - ) - .collect(Collectors.toList()); + if (beanDefinitions != null) { + return beanDefinitions; + } + // first traverse component definition classes and load candidates + if (!beanDefinitionsClasses.isEmpty()) { + List> candidates = new ArrayList<>(); + for (BeanDefinitionProducer producer : beanProducers) { + if (producer.isDisabled() || !producer.isReferenceEnabled(this)) { + continue; } - if (LOG.isDebugEnabled()) { - LOG.debug("Resolved bean candidates {} for instance: {}", candidates, instance); + BeanDefinitionReference reference = producer.getReference(); + Class candidateType = reference.getBeanType(); + if (candidateType == null || !candidateType.isInstance(instance)) { + continue; } - beanDefinitions = candidates; - } else { - if (LOG.isDebugEnabled()) { - LOG.debug("No bean candidates found for instance: {}", instance); + BeanDefinition candidate = reference.load(this); + if (!candidate.isEnabled(this)) { + continue; } - beanDefinitions = Collections.emptySet(); + candidates.add(candidate); + } + + if (candidates.size() > 1) { + // try narrow to exact type + candidates = candidates + .stream() + .filter(candidate -> + candidate.getBeanType() == beanClass + ) + .collect(Collectors.toList()); + } + if (LOG.isDebugEnabled()) { + LOG.debug("Resolved bean candidates {} for instance: {}", candidates, instance); } - beanCandidateCache.put(beanType, (Collection) beanDefinitions); + beanDefinitions = candidates; + } else { + if (LOG.isDebugEnabled()) { + LOG.debug("No bean candidates found for instance: {}", instance); + } + beanDefinitions = Collections.emptySet(); } + beanCandidateCache.put(beanType, (Collection) beanDefinitions); return beanDefinitions; } @@ -2453,10 +2432,10 @@ private Map getRequiredArgumentValues(@NonNull BeanResolutio @NonNull BeanDefinition beanDefinition) { Map convertedValues; if (argumentValues == null) { - convertedValues = requiredArguments.length == 0 ? null : new LinkedHashMap<>(); + convertedValues = requiredArguments.length == 0 ? null : CollectionUtils.newLinkedHashMap(requiredArguments.length); argumentValues = Collections.emptyMap(); } else { - convertedValues = new LinkedHashMap<>(); + convertedValues = CollectionUtils.newLinkedHashMap(requiredArguments.length); } if (convertedValues == null) { return Collections.emptyMap(); @@ -2509,16 +2488,20 @@ protected BeanDefinition findConcreteCandidate(@NonNull Class beanType * * @param parallelBeans The parallel beans */ - protected void processParallelBeans(List parallelBeans) { + @Internal + protected void processParallelBeans(List parallelBeans) { if (!parallelBeans.isEmpty()) { - List finalParallelBeans = parallelBeans.stream().filter(bdr -> bdr.isEnabled(this)).collect(Collectors.toList()); + List finalParallelBeans = parallelBeans.stream() + .filter(p -> p.isReferenceEnabled(this)) + .toList(); if (!finalParallelBeans.isEmpty()) { new Thread(() -> { - Collection parallelDefinitions = new ArrayList<>(); - finalParallelBeans.forEach(beanDefinitionReference -> { + Collection> parallelDefinitions = new ArrayList<>(); + finalParallelBeans.forEach(producer -> { try { - loadContextScopeBean(beanDefinitionReference, parallelDefinitions::add); + loadEagerBeans(producer, parallelDefinitions); } catch (Throwable e) { + BeanDefinitionReference beanDefinitionReference = producer.getReference(); LOG.error("Parallel Bean definition [" + beanDefinitionReference.getName() + "] could not be loaded: " + e.getMessage(), e); Boolean shutdownOnError = beanDefinitionReference.getAnnotationMetadata().booleanValue(Parallel.class, "shutdownOnError").orElse(true); if (shutdownOnError) { @@ -2527,11 +2510,11 @@ protected void processParallelBeans(List parallelBeans) } }); - filterReplacedBeans(null, (Collection) parallelDefinitions); + filterReplacedBeans(null, parallelDefinitions); parallelDefinitions.forEach(beanDefinition -> ForkJoinPool.commonPool().execute(() -> { try { - loadContextScopeBean(beanDefinition); + initializeEagerBean(beanDefinition); } catch (Throwable e) { LOG.error("Parallel Bean definition [" + beanDefinition.getName() + "] could not be loaded: " + e.getMessage(), e); Boolean shutdownOnError = beanDefinition.getAnnotationMetadata().booleanValue(Parallel.class, "shutdownOnError").orElse(true); @@ -2692,7 +2675,7 @@ private boolean checkIfTypeMatches(BeanDefinition definitionToBeReplaced, Class bt = getCanonicalBeanType(definitionToBeReplaced); if (annotationMetadata.hasAnnotation(DefaultImplementation.class)) { Optional defaultImpl = annotationMetadata.classValue(DefaultImplementation.class); - if (!defaultImpl.isPresent()) { + if (defaultImpl.isEmpty()) { defaultImpl = annotationMetadata.classValue(DefaultImplementation.class, "name"); } if (defaultImpl.filter(impl -> impl == bt).isPresent()) { @@ -2715,18 +2698,19 @@ private void doInjectAndInitialize(BeanResolutionContext resolutionContext, } } - private void loadContextScopeBean(BeanDefinitionReference contextScopeBean, Consumer beanDefinitionConsumer) { - if (contextScopeBean.isEnabled(this)) { - BeanDefinition beanDefinition = contextScopeBean.load(this); + private void loadEagerBeans(BeanDefinitionProducer producer, Collection> collector) { + if (producer.isReferenceEnabled(this)) { + BeanDefinitionReference reference = producer.getReference(); + BeanDefinition beanDefinition = reference.load(this); try (BeanResolutionContext resolutionContext = newResolutionContext(beanDefinition, null)) { if (beanDefinition.isEnabled(this, resolutionContext)) { - beanDefinitionConsumer.accept(beanDefinition); + collector.add(beanDefinition); } } } } - private void loadContextScopeBean(BeanDefinition beanDefinition) { + private void initializeEagerBean(BeanDefinition beanDefinition) { if (beanDefinition.isIterable() || beanDefinition.hasStereotype(ConfigurationReader.class.getName())) { Set> beanCandidates = new HashSet<>(5); @@ -3328,38 +3312,46 @@ private Collection> filterExactMatch(final Class beanTy } private void readAllBeanDefinitionClasses() { - List contextScopeBeans = new ArrayList<>(20); - List processedBeans = new ArrayList<>(10); - List parallelBeans = new ArrayList<>(10); + List eagerInitBeans = new ArrayList<>(20); + List processedBeans = new ArrayList<>(10); + List parallelBeans = new ArrayList<>(10); List beanDefinitionReferences = resolveBeanDefinitionReferences(); - List toRemove = new ArrayList<>(beanDefinitionReferences.size()); - beanDefinitionsClasses.addAll(beanDefinitionReferences); - Set configurationsDisabled = new HashSet<>(); - for (BeanConfiguration bc : beanConfigurations.values()) { + List producers = new ArrayList<>(beanDefinitionReferences.size()); + List proxyTargetBeans = new ArrayList<>(beanDefinitionReferences.size()); + for (BeanDefinitionReference beanDefinitionReference : beanDefinitionReferences) { + producers.add(new BeanDefinitionProducer(beanDefinitionReference)); + } + beanDefinitionsClasses.addAll(producers); + + Collection allConfigurations = beanConfigurations.values(); + List configurationsDisabled = new ArrayList<>(allConfigurations.size()); + for (BeanConfiguration bc : allConfigurations) { if (!bc.isEnabled(this)) { configurationsDisabled.add(bc); } } reference: - for (BeanDefinitionReference beanDefinitionReference : beanDefinitionReferences) { + for (BeanDefinitionProducer beanDefinitionProducer : producers) { + BeanDefinitionReference beanDefinitionReference = beanDefinitionProducer.reference; for (BeanConfiguration disableConfiguration : configurationsDisabled) { if (disableConfiguration.isWithin(beanDefinitionReference)) { - toRemove.add(beanDefinitionReference); + beanDefinitionProducer.referenceEnabled = false; continue reference; } } if (beanDefinitionReference.isProxiedBean()) { - toRemove.add(beanDefinitionReference); + beanDefinitionProducer.referenceEnabled = false; + BeanDefinitionProducer proxyBeanProducer = new BeanDefinitionProducer(beanDefinitionReference); if (beanDefinitionReference.requiresMethodProcessing()) { - processedBeans.add(beanDefinitionReference); + processedBeans.add(proxyBeanProducer); } // retain only if proxy target otherwise the target is never used if (beanDefinitionReference.isProxyTarget()) { - this.proxyTargetBeans.add(beanDefinitionReference); + proxyTargetBeans.add(proxyBeanProducer); } continue; } @@ -3370,34 +3362,35 @@ private void readAllBeanDefinitionClasses() { //noinspection ForLoopReplaceableByForEach for (int i = 0; i < indexes.length; i++) { Class indexedType = indexes[i]; - resolveTypeIndex(indexedType).add(beanDefinitionReference); + resolveTypeIndex(indexedType).add(beanDefinitionProducer); } } else { if (annotationMetadata.hasStereotype(ADAPTER_TYPE)) { final Class aClass = annotationMetadata.classValue(ADAPTER_TYPE, AnnotationMetadata.VALUE_MEMBER).orElse(null); if (indexedTypes.contains(aClass)) { - resolveTypeIndex(aClass).add(beanDefinitionReference); + resolveTypeIndex(aClass).add(beanDefinitionProducer); } } } if (isEagerInit(beanDefinitionReference)) { - contextScopeBeans.add(beanDefinitionReference); + eagerInitBeans.add(beanDefinitionProducer); } else if (annotationMetadata.hasDeclaredStereotype(PARALLEL_TYPE)) { - parallelBeans.add(beanDefinitionReference); + parallelBeans.add(beanDefinitionProducer); } if (beanDefinitionReference.requiresMethodProcessing()) { - processedBeans.add(beanDefinitionReference); + processedBeans.add(beanDefinitionProducer); } } - this.beanDefinitionsClasses.removeAll(toRemove); this.beanDefinitionReferences = null; this.beanConfigurationsList = null; + this.proxyTargetBeans.addAll(proxyTargetBeans); + initializeEventListeners(); - initializeContext(contextScopeBeans, processedBeans, parallelBeans); + initializeContext(eagerInitBeans, processedBeans, parallelBeans); } private boolean isEagerInit(BeanDefinitionReference beanDefinitionReference) { @@ -3407,7 +3400,7 @@ private boolean isEagerInit(BeanDefinitionReference beanDefinitionReference) { } @NonNull - private Collection resolveTypeIndex(Class indexedType) { + private Collection resolveTypeIndex(Class indexedType) { return beanIndex.computeIfAbsent(indexedType, aClass -> { indexedTypes.add(indexedType); return new ArrayList<>(20); @@ -3532,21 +3525,15 @@ private Collection> resolveBeanRegistrations(BeanResolut } addCandidateToList(resolutionContext, definition, beanType, qualifier, beansOfTypeList); } - Collection> result = beansOfTypeList; if (beansOfTypeList != Collections.EMPTY_SET) { - Stream> stream = beansOfTypeList.stream(); if (Ordered.class.isAssignableFrom(beanType.getType())) { - result = stream - .sorted(OrderUtil.COMPARATOR) - .collect(StreamUtils.toImmutableCollection()); - } else { - if (hasOrderAnnotation) { - stream = stream.sorted(BEAN_REGISTRATION_COMPARATOR); - } - result = stream.collect(StreamUtils.toImmutableCollection()); + return beansOfTypeList.stream().sorted(OrderUtil.COMPARATOR).toList(); + } + if (hasOrderAnnotation) { + return beansOfTypeList.stream().sorted(BEAN_REGISTRATION_COMPARATOR).toList(); } } - return result; + return beansOfTypeList; } private void logResolvedExistingBeanRegistrations(Argument beanType, Qualifier qualifier, Collection> existing) { @@ -3925,7 +3912,7 @@ public R invoke(Object... arguments) { interface ListenersSupplier { /** - * Retrived the listeners lazily. + * Retrieved the listeners lazily. * * @param beanResolutionContext The bean resolution context * @return the collection of listeners @@ -4103,48 +4090,108 @@ private static final class CollectionHolder { Collection> registrations; } - private final class ScanningBeanResolutionContext extends SingletonBeanResolutionContext { + /** + * The class adds the caching of the enabled decision + the definition instance. + * NOTE: The class can be accesed in multiple threads, we do allow for the fields to be possibly intitialized concurrently - multiple times. + * + * @since 4.0.0 + */ + @Internal + final static class BeanDefinitionProducer { - private final HashMap, Argument> beanCreationTargets; - private final Map, List>>> foundTargets = new HashMap<>(); + @Nullable + private volatile BeanDefinitionReference reference; + @Nullable + private volatile BeanDefinition definition; + @Nullable + private volatile Boolean referenceEnabled; + @Nullable + private volatile Boolean definitionEnabled; + + BeanDefinitionProducer(@NonNull BeanDefinitionReference reference) { + this.reference = reference; + } - private ScanningBeanResolutionContext(BeanDefinition beanDefinition, HashMap, Argument> beanCreationTargets) { - super(beanDefinition); - this.beanCreationTargets = beanCreationTargets; + public boolean isReferenceEnabled(DefaultBeanContext context) { + return isReferenceEnabled(context, null); } - private List> getHierarchy() { - List> hierarchy = new ArrayList<>(path.size()); - for (Iterator> it = path.descendingIterator(); it.hasNext();) { - BeanResolutionContext.Segment segment = it.next(); - hierarchy.add(segment.getArgument()); + public boolean isReferenceEnabled(DefaultBeanContext context, @Nullable BeanResolutionContext resolutionContext) { + if (reference == null) { + return false; } - return hierarchy; + if (referenceEnabled == null) { + if (reference.isEnabled(context, resolutionContext)) { + referenceEnabled = true; + } else { + referenceEnabled = false; + reference = null; + } + } + return referenceEnabled; } - @Override - protected void onNewSegment(Segment segment) { - Argument argument = segment.getArgument(); - if (argument.isContainerType()) { - argument = argument.getFirstTypeVariable().orElse(null); - if (argument == null) { - return; + public boolean isDisabled() { + if (reference == null) { + return true; + } + if (referenceEnabled != null && !referenceEnabled) { + return true; + } + return definitionEnabled != null && !definitionEnabled; + } + + public boolean isDefinitionEnabled(DefaultBeanContext defaultBeanContext) { + return isDefinitionEnabled(defaultBeanContext, null); + } + + public boolean isDefinitionEnabled(DefaultBeanContext context, @Nullable BeanResolutionContext resolutionContext) { + if (definitionEnabled == null) { + if (isReferenceEnabled(context, resolutionContext)) { + definition = getDefinition(context); + if (definition.isEnabled(context, resolutionContext)) { + definitionEnabled = true; + } else { + definitionEnabled = false; + definition = null; + } + } else { + definitionEnabled = false; } } - if (argument.isProvider()) { - return; + return definitionEnabled; + } + + public BeanDefinitionReference getReference() { + if (reference == null || referenceEnabled == null || !referenceEnabled) { + throw new IllegalStateException("The reference is not enabled"); } - for (Map.Entry, Argument> entry : beanCreationTargets.entrySet()) { - if (argument.isAssignableFrom(entry.getValue())) { - foundTargets.computeIfAbsent(entry.getKey(), bd -> new ArrayList<>(5)) - .add(getHierarchy()); + return reference; + } + + public BeanDefinition getDefinition(BeanContext beanContext) { + if (definitionEnabled != null && !definitionEnabled) { + throw new IllegalStateException("The definition is not enabled"); + } + try { + if (definition == null) { + definition = getReference().load(beanContext); } + return definition; + } catch (Throwable e) { + throw new BeanInstantiationException("Bean definition [" + reference.getName() + "] could not be loaded: " + e.getMessage(), e); } } - @SuppressWarnings("java:S1452") - Map, List>>> getFoundTargets() { - return foundTargets; + public boolean isReferenceCandidateBean(Argument beanType) { + return reference != null && reference.isCandidateBean(beanType); + } + + public void disable(BeanDefinitionReference reference) { + BeanDefinitionReference ref = this.reference; + if (ref != null && ref.equals(reference)) { + this.reference = null; + } } } } diff --git a/inject/src/main/java/io/micronaut/context/DisabledBean.java b/inject/src/main/java/io/micronaut/context/DisabledBean.java index d86ab0360f5..5890229afd3 100644 --- a/inject/src/main/java/io/micronaut/context/DisabledBean.java +++ b/inject/src/main/java/io/micronaut/context/DisabledBean.java @@ -88,4 +88,9 @@ public BeanDefinition load() { public boolean isPresent() { return true; } + + @Override + public int hashCode() { + return type.typeHashCode(); + } } diff --git a/inject/src/main/java/io/micronaut/context/RequiresCondition.java b/inject/src/main/java/io/micronaut/context/RequiresCondition.java index b0d44b639e2..dba78ea752a 100644 --- a/inject/src/main/java/io/micronaut/context/RequiresCondition.java +++ b/inject/src/main/java/io/micronaut/context/RequiresCondition.java @@ -92,26 +92,25 @@ public RequiresCondition(AnnotationMetadata annotationMetadata) { @Override public boolean matches(ConditionContext context) { + List> requirements = annotationMetadata.getAnnotationValuesByType(Requires.class); + if (requirements.isEmpty()) { + return true; + } AnnotationMetadataProvider component = context.getComponent(); boolean isBeanReference = component instanceof BeanDefinitionReference; - - List> requirements = annotationMetadata.getAnnotationValuesByType(Requires.class); - - if (!requirements.isEmpty()) { - // here we use AnnotationMetadata to avoid loading the classes referenced in the annotations directly - if (isBeanReference) { - for (AnnotationValue requirement : requirements) { - processPreStartRequirements(context, requirement); - if (context.isFailing()) { - return false; - } + // here we use AnnotationMetadata to avoid loading the classes referenced in the annotations directly + if (isBeanReference) { + for (AnnotationValue requirement : requirements) { + processPreStartRequirements(context, requirement); + if (context.isFailing()) { + return false; } - } else { - for (AnnotationValue requires : requirements) { - processPostStartRequirements(context, requires); - if (context.isFailing()) { - return false; - } + } + } else { + for (AnnotationValue requires : requirements) { + processPostStartRequirements(context, requires); + if (context.isFailing()) { + return false; } } } @@ -133,7 +132,7 @@ protected boolean matchesConfiguration(ConditionContext context, AnnotationValue BeanContext beanContext = context.getBeanContext(); String minimumVersion = requirements.stringValue(MEMBER_VERSION).orElse(null); Optional beanConfiguration = beanContext.findBeanConfiguration(configurationName); - if (!beanConfiguration.isPresent()) { + if (beanConfiguration.isEmpty()) { context.fail("Required configuration [" + configurationName + "] is not active"); return false; } else { @@ -172,11 +171,10 @@ private void processPreStartRequirements(ConditionContext context, AnnotationVal return; } - if (!matchesProperty(context, requirements)) { + if (!matchesProperty(context, requirements)) { return; } - - if (!matchesMissingProperty(context, requirements)) { + if (!matchesMissingProperty(context, requirements)) { return; } @@ -302,8 +300,7 @@ private boolean matchesEnvironment(ConditionContext context, AnnotationValue activeNames = environment.getActiveNames(); boolean result = Arrays.stream(env).anyMatch(activeNames::contains); @@ -317,8 +314,7 @@ private boolean matchesEnvironment(ConditionContext context, AnnotationValue activeNames = environment.getActiveNames(); boolean result = Arrays.stream(env).noneMatch(activeNames::contains); @@ -340,51 +336,47 @@ private boolean matchesCustomConditions(ConditionContext context, AnnotationValu final AnnotationClassValue annotationClassValue = requirements.annotationClassValue(MEMBER_CONDITION).orElse(null); if (annotationClassValue == null) { return true; - } else { - final Object instance = annotationClassValue.getInstance().orElse(null); - if (instance instanceof Condition) { - final boolean conditionResult = ((Condition) instance).matches(context); - if (!conditionResult) { - context.fail("Custom condition [" + instance.getClass() + "] failed evaluation"); - } - return conditionResult; - } else { - - final Class conditionClass = annotationClassValue.getType().orElse(null); - if (conditionClass == null || conditionClass == TrueCondition.class || !Condition.class.isAssignableFrom(conditionClass)) { - return true; - } - // try first via instantiated metadata - Optional condition = InstantiationUtils.tryInstantiate((Class) conditionClass); - if (condition.isPresent()) { - boolean conditionResult = condition.get().matches(context); - if (!conditionResult) { - context.fail("Custom condition [" + conditionClass + "] failed evaluation"); - } - return conditionResult; - } else { - // maybe a Groovy closure - Optional> constructor = ReflectionUtils.findConstructor((Class) conditionClass, Object.class, Object.class); - boolean conditionResult = constructor.flatMap(ctor -> - InstantiationUtils.tryInstantiate(ctor, null, null) - ).flatMap(obj -> { - Optional method = ReflectionUtils.findMethod(obj.getClass(), "call", ConditionContext.class); - if (method.isPresent()) { - Object result = ReflectionUtils.invokeMethod(obj, method.get(), context); - if (result instanceof Boolean) { - return Optional.of((Boolean) result); - } - } - return Optional.empty(); - }).orElse(false); - if (!conditionResult) { - context.fail("Custom condition [" + conditionClass + "] failed evaluation"); - } - return conditionResult; - + } + final Object instance = annotationClassValue.getInstance().orElse(null); + if (instance instanceof Condition condition) { + final boolean conditionResult = condition.matches(context); + if (!conditionResult) { + context.fail("Custom condition [" + instance.getClass() + "] failed evaluation"); + } + return conditionResult; + } + final Class conditionClass = annotationClassValue.getType().orElse(null); + if (conditionClass == null || conditionClass == TrueCondition.class || !Condition.class.isAssignableFrom(conditionClass)) { + return true; + } + // try first via instantiated metadata + Optional condition = InstantiationUtils.tryInstantiate((Class) conditionClass); + if (condition.isPresent()) { + boolean conditionResult = condition.get().matches(context); + if (!conditionResult) { + context.fail("Custom condition [" + conditionClass + "] failed evaluation"); + } + return conditionResult; + } + // maybe a Groovy closure + Optional> constructor = ReflectionUtils.findConstructor((Class) conditionClass, Object.class, Object.class); + boolean conditionResult = constructor.flatMap(ctor -> + InstantiationUtils.tryInstantiate(ctor, null, null) + ).flatMap(obj -> { + Optional method = ReflectionUtils.findMethod(obj.getClass(), "call", ConditionContext.class); + if (method.isPresent()) { + Object result = ReflectionUtils.invokeMethod(obj, method.get(), context); + if (result instanceof Boolean) { + return Optional.of((Boolean) result); } } + return Optional.empty(); + }).orElse(false); + if (!conditionResult) { + context.fail("Custom condition [" + conditionClass + "] failed evaluation"); } + return conditionResult; + } return !context.isFailing(); } @@ -525,7 +517,7 @@ private boolean matchesPresenceOfClasses(ConditionContext context, AnnotationVal if (requirements.contains(attr)) { AnnotationClassValue[] classValues = requirements.annotationClassValues(attr); for (AnnotationClassValue classValue : classValues) { - if (!classValue.getType().isPresent()) { + if (classValue.getType().isEmpty()) { context.fail("Class [" + classValue.getName() + "] is not present"); return false; } @@ -541,16 +533,16 @@ private boolean matchesPresenceOfEntities(ConditionContext context, AnnotationVa BeanContext beanContext = context.getBeanContext(); if (beanContext instanceof ApplicationContext) { ApplicationContext applicationContext = (ApplicationContext) beanContext; - final AnnotationClassValue[] classValues = classNames.get(); + final AnnotationClassValue[] classValues = classNames.get(); for (AnnotationClassValue classValue : classValues) { final Optional> entityType = classValue.getType(); - if (!entityType.isPresent()) { + if (entityType.isEmpty()) { context.fail("Annotation type [" + classValue.getName() + "] not present on classpath"); return false; } else { Environment environment = applicationContext.getEnvironment(); Class annotationType = entityType.get(); - if (!environment.scan(annotationType).findFirst().isPresent()) { + if (environment.scan(annotationType).findFirst().isEmpty()) { context.fail("No entities found in packages [" + String.join(", ", environment.getPackages()) + "] for annotation: " + annotationType); return false; } @@ -573,7 +565,7 @@ private boolean matchesPresenceOfBeans(ConditionContext context, AnnotationValue } if (ArrayUtils.isNotEmpty(beans)) { BeanContext beanContext = context.getBeanContext(); - for (Class type : beans) { + for (Class type : beans) { if (!beanContext.containsBean(type)) { context.fail("No bean of type [" + type + "] present within context"); return false; @@ -590,7 +582,6 @@ private boolean matchesAbsenceOfBeans(ConditionContext context, AnnotationValue< AnnotationMetadataProvider component = context.getComponent(); if (ArrayUtils.isNotEmpty(missingBeans) && component instanceof BeanDefinition) { BeanDefinition bd = (BeanDefinition) component; - DefaultBeanContext beanContext = (DefaultBeanContext) context.getBeanContext(); for (Class type : missingBeans) { @@ -598,8 +589,7 @@ private boolean matchesAbsenceOfBeans(ConditionContext context, AnnotationValue< final Collection> beanDefinitions = beanContext.findBeanCandidates( context.getBeanResolutionContext(), Argument.of(type), - bd, - true + bd ); for (BeanDefinition beanDefinition : beanDefinitions) { if (!beanDefinition.isAbstract()) { @@ -631,7 +621,7 @@ private boolean matchesPresenceOfResources(ConditionContext context, AnnotationV } resolver = new ResourceResolver(resourceLoaders); for (String resourcePath : resourcePaths) { - if (!resolver.getResource(resourcePath).isPresent()) { + if (resolver.getResource(resourcePath).isEmpty()) { context.fail("Resource [" + resourcePath + "] does not exist"); return false; } @@ -643,22 +633,18 @@ private boolean matchesPresenceOfResources(ConditionContext context, AnnotationV private boolean matchesCurrentOs(ConditionContext context, AnnotationValue requirements) { if (requirements.contains(MEMBER_OS)) { - final List os = Arrays.asList(requirements.enumValues(MEMBER_OS, Requires.Family.class)); + final Set os = requirements.enumValuesSet(MEMBER_OS, Requires.Family.class); Requires.Family currentOs = OperatingSystem.getCurrent().getFamily(); - if (!os.isEmpty()) { - if (!os.contains(currentOs)) { - context.fail("The current operating system [" + currentOs.name() + "] is not one of the required systems [" + os + "]"); - return false; - } + if (!os.contains(currentOs)) { + context.fail("The current operating system [" + currentOs.name() + "] is not one of the required systems [" + os + "]"); + return false; } } else if (requirements.contains(MEMBER_NOT_OS)) { Requires.Family currentOs = OperatingSystem.getCurrent().getFamily(); - final List notOs = Arrays.asList(requirements.enumValues(MEMBER_NOT_OS, Requires.Family.class)); - if (!notOs.isEmpty()) { - if (notOs.contains(currentOs)) { - context.fail("The current operating system [" + currentOs.name() + "] is one of the disallowed systems [" + notOs + "]"); - return false; - } + final Set notOs = requirements.enumValuesSet(MEMBER_NOT_OS, Requires.Family.class); + if (notOs.contains(currentOs)) { + context.fail("The current operating system [" + currentOs.name() + "] is one of the disallowed systems [" + notOs + "]"); + return false; } } return true; diff --git a/inject/src/main/java/io/micronaut/context/converters/ContextConverterRegistrar.java b/inject/src/main/java/io/micronaut/context/converters/ContextConverterRegistrar.java index 1125cd2dab4..7131597c58f 100644 --- a/inject/src/main/java/io/micronaut/context/converters/ContextConverterRegistrar.java +++ b/inject/src/main/java/io/micronaut/context/converters/ContextConverterRegistrar.java @@ -16,11 +16,11 @@ package io.micronaut.context.converters; import io.micronaut.context.BeanContext; +import io.micronaut.context.annotation.Prototype; import io.micronaut.core.annotation.Internal; import io.micronaut.core.convert.MutableConversionService; import io.micronaut.core.convert.TypeConverterRegistrar; import io.micronaut.core.reflect.ClassUtils; -import jakarta.inject.Singleton; import java.util.Arrays; import java.util.Map; @@ -33,7 +33,7 @@ * @author graemerocher * @since 2.0 */ -@Singleton +@Prototype @Internal public class ContextConverterRegistrar implements TypeConverterRegistrar { diff --git a/inject/src/main/java/io/micronaut/context/env/DefaultEnvironment.java b/inject/src/main/java/io/micronaut/context/env/DefaultEnvironment.java index 1d407213680..b106471f070 100644 --- a/inject/src/main/java/io/micronaut/context/env/DefaultEnvironment.java +++ b/inject/src/main/java/io/micronaut/context/env/DefaultEnvironment.java @@ -33,6 +33,7 @@ import io.micronaut.core.optim.StaticOptimizations; import io.micronaut.core.order.OrderUtil; import io.micronaut.core.reflect.ClassUtils; +import io.micronaut.core.util.CollectionUtils; import io.micronaut.core.util.StringUtils; import io.micronaut.inject.BeanConfiguration; import org.slf4j.Logger; @@ -435,11 +436,17 @@ protected void readPropertySources(String name) { } private void readConstantPropertySources(String name, List propertySources) { - Set propertySourceNames = Stream.concat(Stream.of(name), getActiveNames().stream().map(env -> name + "-" + env)) - .collect(Collectors.toSet()); - getConstantPropertySources().stream() - .filter(p -> propertySourceNames.contains(p.getName())) - .forEach(propertySources::add); + Set activeNames = getActiveNames(); + Set propertySourceNames = CollectionUtils.newHashSet(activeNames.size() + 1); + propertySourceNames.add(name); + for (String env : activeNames) { + propertySourceNames.add(name + "-" + env); + } + for (PropertySource p : getConstantPropertySources()) { + if (propertySourceNames.contains(p.getName())) { + propertySources.add(p); + } + } } /** diff --git a/inject/src/main/java/io/micronaut/context/env/PropertySourcePropertyResolver.java b/inject/src/main/java/io/micronaut/context/env/PropertySourcePropertyResolver.java index dda245b39b2..a203c4bcbc2 100644 --- a/inject/src/main/java/io/micronaut/context/env/PropertySourcePropertyResolver.java +++ b/inject/src/main/java/io/micronaut/context/env/PropertySourcePropertyResolver.java @@ -58,8 +58,6 @@ import java.util.function.Supplier; import java.util.regex.Matcher; import java.util.regex.Pattern; -import java.util.stream.Collectors; -import java.util.stream.Stream; /** *

A {@link PropertyResolver} that resolves from one or many {@link PropertySource} instances.

@@ -211,16 +209,22 @@ public Collection getPropertyEntries(@NonNull String name) { name, false, PropertyCatalog.NORMALIZED); if (entries != null) { String prefix = name + '.'; - return entries.keySet().stream().filter(k -> k.startsWith(prefix)) - .map(k -> { - String withoutPrefix = k.substring(prefix.length()); - int i = withoutPrefix.indexOf('.'); - if (i > -1) { - return withoutPrefix.substring(0, i); - } - return withoutPrefix; - }) - .collect(Collectors.toSet()); + Set result = new HashSet<>(); + Set strings = entries.keySet(); + for (String k : strings) { + if (k.startsWith(prefix)) { + String withoutPrefix = k.substring(prefix.length()); + int i = withoutPrefix.indexOf('.'); + String s; + if (i > -1) { + s = withoutPrefix.substring(0, i); + } else { + s = withoutPrefix; + } + result.add(s); + } + } + return result; } } return Collections.emptySet(); diff --git a/inject/src/main/java/io/micronaut/context/event/ApplicationEventPublisherFactory.java b/inject/src/main/java/io/micronaut/context/event/ApplicationEventPublisherFactory.java index 61ddccb6a13..dfbcbc18707 100644 --- a/inject/src/main/java/io/micronaut/context/event/ApplicationEventPublisherFactory.java +++ b/inject/src/main/java/io/micronaut/context/event/ApplicationEventPublisherFactory.java @@ -85,7 +85,7 @@ public boolean isAbstract() { @Override public boolean isCandidateBean(Argument beanType) { - return InstantiatableBeanDefinition.super.isCandidateBean(beanType); + return beanType.isAssignableFrom(ApplicationEventPublisher.class); } @Override @@ -214,7 +214,7 @@ private ApplicationEventPublisher getTypedEventPublisher(Argument eventType, Bea private ApplicationEventPublisher createEventPublisher(Argument eventType, BeanContext beanContext) { return new ApplicationEventPublisher() { - private final Supplier> lazyListeners = SupplierUtil.memoizedNonEmpty(() -> { + private final Supplier> lazyListeners = SupplierUtil.memoized(() -> { List listeners = new ArrayList<>( beanContext.getBeansOfType(ApplicationEventListener.class, Qualifiers.byTypeArguments(eventType.getType())) ); diff --git a/inject/src/main/java/io/micronaut/inject/annotation/AnnotationMetadataHierarchy.java b/inject/src/main/java/io/micronaut/inject/annotation/AnnotationMetadataHierarchy.java index f98163bba3e..205ab5d8321 100644 --- a/inject/src/main/java/io/micronaut/inject/annotation/AnnotationMetadataHierarchy.java +++ b/inject/src/main/java/io/micronaut/inject/annotation/AnnotationMetadataHierarchy.java @@ -115,12 +115,24 @@ public boolean hasPropertyExpressions() { @Override public Optional> getAnnotationType(@NonNull String name) { - return getAnnotationType((metadata) -> metadata.getAnnotationType(name)); + for (AnnotationMetadata metadata1 : hierarchy) { + final Optional> annotationType = ((Function>>) (metadata) -> metadata.getAnnotationType(name)).apply(metadata1); + if (annotationType.isPresent()) { + return annotationType; + } + } + return Optional.empty(); } @Override public Optional> getAnnotationType(@NonNull String name, @NonNull ClassLoader classLoader) { - return getAnnotationType((metadata) -> metadata.getAnnotationType(name, classLoader)); + for (AnnotationMetadata metadata1 : hierarchy) { + final Optional> annotationType = ((Function>>) (metadata) -> metadata.getAnnotationType(name, classLoader)).apply(metadata1); + if (annotationType.isPresent()) { + return annotationType; + } + } + return Optional.empty(); } /** @@ -389,7 +401,7 @@ public Class[] classValues(@NonNull String annotation, @NonNull String me for (AnnotationMetadata am : hierarchy) { list.addAll(Arrays.asList(am.classValues(annotation, member))); } - return ArrayUtils.toArray(list, Class[]::new); + return list.toArray(new Class[0]); } @Override @@ -808,7 +820,7 @@ public Class[] classValues(Class annotation, String for (AnnotationMetadata am : hierarchy) { list.addAll(Arrays.asList(am.classValues(annotation, member))); } - return ArrayUtils.toArray(list, Class[]::new); + return list.toArray(new Class[0]); } @Override @@ -868,7 +880,7 @@ public String[] stringValues(@NonNull Class annotation, @N strings.addAll(Arrays.asList(am.stringValues(annotation, member))); } } - return ArrayUtils.toArray(strings, String[]::new); + return strings.toArray(new String[0]); } @Override @@ -881,7 +893,7 @@ public String[] stringValues(String annotation, String member, Function iterator() { return ArrayUtils.reverseIterator(hierarchy); } - private Optional> getAnnotationType(Function>> annotationTypeSupplier) { - for (AnnotationMetadata metadata : hierarchy) { - final Optional> annotationType = annotationTypeSupplier.apply(metadata); - if (annotationType.isPresent()) { - return annotationType; - } - } - return Optional.empty(); - } - @Override public boolean isEmpty() { for (AnnotationMetadata metadata : hierarchy) { diff --git a/inject/src/main/java/io/micronaut/inject/annotation/AnnotationMetadataSupport.java b/inject/src/main/java/io/micronaut/inject/annotation/AnnotationMetadataSupport.java index 063a5b36036..0d609e0a8ac 100644 --- a/inject/src/main/java/io/micronaut/inject/annotation/AnnotationMetadataSupport.java +++ b/inject/src/main/java/io/micronaut/inject/annotation/AnnotationMetadataSupport.java @@ -15,15 +15,44 @@ */ package io.micronaut.inject.annotation; -import io.micronaut.context.annotation.*; -import io.micronaut.core.annotation.*; +import io.micronaut.context.annotation.AliasFor; +import io.micronaut.context.annotation.Aliases; +import io.micronaut.context.annotation.Bean; +import io.micronaut.context.annotation.Configuration; +import io.micronaut.context.annotation.ConfigurationBuilder; +import io.micronaut.context.annotation.ConfigurationProperties; +import io.micronaut.context.annotation.Context; +import io.micronaut.context.annotation.EachBean; +import io.micronaut.context.annotation.EachProperty; +import io.micronaut.context.annotation.Executable; +import io.micronaut.context.annotation.Factory; +import io.micronaut.context.annotation.Parameter; +import io.micronaut.context.annotation.Primary; +import io.micronaut.context.annotation.Property; +import io.micronaut.context.annotation.PropertySource; +import io.micronaut.context.annotation.Prototype; +import io.micronaut.context.annotation.Provided; +import io.micronaut.context.annotation.Replaces; +import io.micronaut.context.annotation.Requirements; +import io.micronaut.context.annotation.Requires; +import io.micronaut.context.annotation.Secondary; +import io.micronaut.context.annotation.Type; +import io.micronaut.context.annotation.Value; +import io.micronaut.core.annotation.AnnotationClassValue; +import io.micronaut.core.annotation.AnnotationUtil; +import io.micronaut.core.annotation.AnnotationValue; +import io.micronaut.core.annotation.AnnotationValueProvider; +import io.micronaut.core.annotation.Indexed; +import io.micronaut.core.annotation.Indexes; +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.Introspected; +import io.micronaut.core.annotation.NonNull; +import io.micronaut.core.annotation.Nullable; +import io.micronaut.core.annotation.UsedByGeneratedCode; import io.micronaut.core.reflect.ClassUtils; import io.micronaut.core.reflect.InstantiationUtils; import io.micronaut.core.reflect.ReflectionUtils; import io.micronaut.core.util.StringUtils; - -import io.micronaut.core.annotation.NonNull; -import io.micronaut.core.annotation.Nullable; import jakarta.annotation.PostConstruct; import jakarta.annotation.PreDestroy; import jakarta.inject.Inject; @@ -32,15 +61,19 @@ import jakarta.inject.Scope; import jakarta.inject.Singleton; -import javax.validation.constraints.NotNull; import java.lang.annotation.Annotation; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy; -import java.util.*; +import java.util.AbstractMap; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; import java.util.concurrent.ConcurrentHashMap; -import java.util.function.Consumer; /** * Support method for {@link io.micronaut.core.annotation.AnnotationMetadata}. @@ -52,7 +85,7 @@ public final class AnnotationMetadataSupport { private static final Map> ANNOTATION_DEFAULTS = new ConcurrentHashMap<>(20); - private static final Map REPEATABLE_ANNOTATIONS = new ConcurrentHashMap<>(20); + private static final Map REPEATABLE_ANNOTATIONS_CONTAINERS = new ConcurrentHashMap<>(20); private static final Map, Optional>> ANNOTATION_PROXY_CACHE = new ConcurrentHashMap<>(20); private static final Map> ANNOTATION_TYPES = new ConcurrentHashMap<>(20); @@ -95,10 +128,10 @@ public final class AnnotationMetadataSupport { for (Map.Entry, Class> e : getCoreRepeatableAnnotations()) { - REPEATABLE_ANNOTATIONS.put(e.getKey().getName(), e.getValue().getName()); + REPEATABLE_ANNOTATIONS_CONTAINERS.put(e.getKey().getName(), e.getValue().getName()); } - REPEATABLE_ANNOTATIONS.put("io.micronaut.aop.InterceptorBinding", "io.micronaut.aop.InterceptorBindingDefinitions"); + REPEATABLE_ANNOTATIONS_CONTAINERS.put("io.micronaut.aop.InterceptorBinding", "io.micronaut.aop.InterceptorBindingDefinitions"); } /** @@ -129,7 +162,7 @@ public static Map getDefaultValues(String annotation) { */ @Internal public static String getRepeatableAnnotation(String annotation) { - return REPEATABLE_ANNOTATIONS.get(annotation); + return REPEATABLE_ANNOTATIONS_CONTAINERS.get(annotation); } /** @@ -232,34 +265,22 @@ static void registerDefaultValues(AnnotationClassValue annotation, Map annotationClassValue) { final String name = annotationClassValue.getName(); if (!ANNOTATION_TYPES.containsKey(name)) { - annotationClassValue.getType().ifPresent((Consumer>) aClass -> { - if (Annotation.class.isAssignableFrom(aClass)) { - ANNOTATION_TYPES.put(name, (Class) aClass); - } - }); + Class aClass = annotationClassValue.getType().orElse(null); + if (aClass != null && Annotation.class.isAssignableFrom(aClass)) { + ANNOTATION_TYPES.put(name, (Class) aClass); + } } } /** - * Registers repeatable annotations. + * Registers repeatable annotation containers. + * @MyRepeatable -> @MyRepeatableContainer * * @param repeatableAnnotations the repeatable annotations */ @Internal static void registerRepeatableAnnotations(Map repeatableAnnotations) { - REPEATABLE_ANNOTATIONS.putAll(repeatableAnnotations); - } - - /** - * Remove Core repeatable annotations. - * - * @param repeatableAnnotations the repeatable annotations - */ - @Internal - static void removeCoreRepeatableAnnotations(@NotNull Map repeatableAnnotations) { - for (Map.Entry, Class> e : getCoreRepeatableAnnotations()) { - repeatableAnnotations.remove(e.getKey().getName()); - } + REPEATABLE_ANNOTATIONS_CONTAINERS.putAll(repeatableAnnotations); } /** diff --git a/inject/src/main/java/io/micronaut/inject/annotation/DefaultAnnotationMetadata.java b/inject/src/main/java/io/micronaut/inject/annotation/DefaultAnnotationMetadata.java index 7b28009a993..f514a733cc3 100644 --- a/inject/src/main/java/io/micronaut/inject/annotation/DefaultAnnotationMetadata.java +++ b/inject/src/main/java/io/micronaut/inject/annotation/DefaultAnnotationMetadata.java @@ -21,6 +21,7 @@ import io.micronaut.core.annotation.AnnotationUtil; import io.micronaut.core.annotation.AnnotationValue; import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.NextMajorVersion; import io.micronaut.core.annotation.NonNull; import io.micronaut.core.annotation.Nullable; import io.micronaut.core.annotation.UsedByGeneratedCode; @@ -33,7 +34,6 @@ import io.micronaut.core.value.OptionalValues; import java.lang.annotation.Annotation; -import java.lang.annotation.Repeatable; import java.lang.annotation.RetentionPolicy; import java.lang.reflect.Array; import java.util.AbstractMap; @@ -85,14 +85,12 @@ public class DefaultAnnotationMetadata extends AbstractAnnotationMetadata implem @Nullable Map> annotationDefaultValues; @Nullable - Map repeated; + Map annotationRepeatableContainer; @Nullable Set sourceRetentionAnnotations; private Map annotationValuesByType = new ConcurrentHashMap<>(2); private final boolean hasPropertyExpressions; - // This should be removed in the next major version - private final boolean useRepeatableDefaults; /** * Constructs empty annotation metadata. @@ -100,7 +98,6 @@ public class DefaultAnnotationMetadata extends AbstractAnnotationMetadata implem @Internal protected DefaultAnnotationMetadata() { hasPropertyExpressions = false; - useRepeatableDefaults = false; } /** @@ -115,11 +112,11 @@ protected DefaultAnnotationMetadata() { @Internal @UsedByGeneratedCode public DefaultAnnotationMetadata( - @Nullable Map> declaredAnnotations, - @Nullable Map> declaredStereotypes, - @Nullable Map> allStereotypes, - @Nullable Map> allAnnotations, - @Nullable Map> annotationsByStereotype) { + @Nullable Map> declaredAnnotations, + @Nullable Map> declaredStereotypes, + @Nullable Map> allStereotypes, + @Nullable Map> allAnnotations, + @Nullable Map> annotationsByStereotype) { this(declaredAnnotations, declaredStereotypes, allStereotypes, allAnnotations, annotationsByStereotype, true); } @@ -136,12 +133,12 @@ public DefaultAnnotationMetadata( @Internal @UsedByGeneratedCode public DefaultAnnotationMetadata( - @Nullable Map> declaredAnnotations, - @Nullable Map> declaredStereotypes, - @Nullable Map> allStereotypes, - @Nullable Map> allAnnotations, - @Nullable Map> annotationsByStereotype, - boolean hasPropertyExpressions) { + @Nullable Map> declaredAnnotations, + @Nullable Map> declaredStereotypes, + @Nullable Map> allStereotypes, + @Nullable Map> allAnnotations, + @Nullable Map> annotationsByStereotype, + boolean hasPropertyExpressions) { this(declaredAnnotations, declaredStereotypes, allStereotypes, allAnnotations, annotationsByStereotype, hasPropertyExpressions, false); } @@ -155,17 +152,20 @@ public DefaultAnnotationMetadata( * @param annotationsByStereotype The annotations by stereotype * @param hasPropertyExpressions Whether property expressions exist in the metadata * @param useRepeatableDefaults Use repeatable defaults + * @deprecated use the constructor without useRepeatableDefaults */ @Internal @UsedByGeneratedCode + @NextMajorVersion("Remove after Micronaut 4 Milestone 1") + @Deprecated(forRemoval = true, since = "4") public DefaultAnnotationMetadata( - @Nullable Map> declaredAnnotations, - @Nullable Map> declaredStereotypes, - @Nullable Map> allStereotypes, - @Nullable Map> allAnnotations, - @Nullable Map> annotationsByStereotype, - boolean hasPropertyExpressions, - boolean useRepeatableDefaults) { + @Nullable Map> declaredAnnotations, + @Nullable Map> declaredStereotypes, + @Nullable Map> allStereotypes, + @Nullable Map> allAnnotations, + @Nullable Map> annotationsByStereotype, + boolean hasPropertyExpressions, + boolean useRepeatableDefaults) { super(declaredAnnotations, allAnnotations); this.declaredAnnotations = declaredAnnotations; this.declaredStereotypes = declaredStereotypes; @@ -173,19 +173,18 @@ public DefaultAnnotationMetadata( this.allAnnotations = allAnnotations; this.annotationsByStereotype = annotationsByStereotype; this.hasPropertyExpressions = hasPropertyExpressions; - this.useRepeatableDefaults = useRepeatableDefaults; } @NonNull @Override public AnnotationMetadata getDeclaredMetadata() { return new DefaultAnnotationMetadata( - this.declaredAnnotations, - this.declaredStereotypes, - null, - null, - annotationsByStereotype, - hasPropertyExpressions + this.declaredAnnotations, + this.declaredStereotypes, + null, + null, + annotationsByStereotype, + hasPropertyExpressions ); } @@ -264,9 +263,9 @@ public > Optional enumValue(@NonNull Class> Optional enumValue(@NonNull Class annotation, @NonNull String member, Class enumType, @Nullable Function valueMapper) { ArgumentUtils.requireNonNull("annotation", annotation); ArgumentUtils.requireNonNull("member", member); - final Repeatable repeatable = annotation.getAnnotation(Repeatable.class); - if (repeatable != null) { - Object v = getRawSingleValue(repeatable.value().getName(), VALUE_MEMBER, valueMapper); + String repeatableTypeName = findRepeatableAnnotationContainerInternal(annotation.getName()); + if (repeatableTypeName != null) { + Object v = getRawSingleValue(repeatableTypeName, VALUE_MEMBER, valueMapper); if (v instanceof AnnotationValue) { return ((AnnotationValue) v).enumValue(member, enumType, valueMapper); } @@ -300,9 +299,9 @@ public > E[] enumValues(@NonNull Class a public > E[] enumValues(@NonNull Class annotation, @NonNull String member, Class enumType, @Nullable Function valueMapper) { ArgumentUtils.requireNonNull("annotation", annotation); ArgumentUtils.requireNonNull("enumType", enumType); - final Repeatable repeatable = annotation.getAnnotation(Repeatable.class); - if (repeatable != null) { - Object v = getRawValue(repeatable.value().getName(), member); + String repeatableTypeName = findRepeatableAnnotationContainerInternal(annotation.getName()); + if (repeatableTypeName != null) { + Object v = getRawValue(repeatableTypeName, member); if (v instanceof AnnotationValue) { return ((AnnotationValue) v).enumValues(member, enumType); } @@ -372,9 +371,9 @@ public Class[] classValues(@NonNull String annotation, @NonNull String me public Class[] classValues(@NonNull Class annotation, @NonNull String member) { ArgumentUtils.requireNonNull("annotation", annotation); ArgumentUtils.requireNonNull("member", member); - final Repeatable repeatable = annotation.getAnnotation(Repeatable.class); - if (repeatable != null) { - Object v = getRawSingleValue(repeatable.value().getName(), member, null); + String repeatableTypeName = findRepeatableAnnotationContainerInternal(annotation.getName()); + if (repeatableTypeName != null) { + Object v = getRawSingleValue(repeatableTypeName, member, null); if (v instanceof AnnotationValue) { Class[] classes = ((AnnotationValue) v).classValues(member); return (Class[]) classes; @@ -404,9 +403,9 @@ public Optional classValue(@NonNull Class annotatio public Optional classValue(@NonNull Class annotation, @NonNull String member, Function valueMapper) { ArgumentUtils.requireNonNull("annotation", annotation); ArgumentUtils.requireNonNull("member", member); - final Repeatable repeatable = annotation.getAnnotation(Repeatable.class); - if (repeatable != null) { - Object v = getRawSingleValue(repeatable.value().getName(), member, valueMapper); + String repeatableTypeName = findRepeatableAnnotationContainerInternal(annotation.getName()); + if (repeatableTypeName != null) { + Object v = getRawSingleValue(repeatableTypeName, member, valueMapper); if (v instanceof AnnotationValue) { return (Optional) ((AnnotationValue) v).classValue(member, valueMapper); } @@ -476,9 +475,9 @@ public OptionalInt intValue(@NonNull Class annotation, @No public OptionalInt intValue(@NonNull Class annotation, @NonNull String member, @Nullable Function valueMapper) { ArgumentUtils.requireNonNull("annotation", annotation); ArgumentUtils.requireNonNull("member", member); - final Repeatable repeatable = annotation.getAnnotation(Repeatable.class); - if (repeatable != null) { - Object v = getRawSingleValue(repeatable.value().getName(), VALUE_MEMBER, valueMapper); + String repeatableTypeName = findRepeatableAnnotationContainerInternal(annotation.getName()); + if (repeatableTypeName != null) { + Object v = getRawSingleValue(repeatableTypeName, VALUE_MEMBER, valueMapper); if (v instanceof AnnotationValue) { return ((AnnotationValue) v).intValue(member, valueMapper); } @@ -513,9 +512,9 @@ public Optional booleanValue(@NonNull Class annot public Optional booleanValue(@NonNull Class annotation, @NonNull String member, Function valueMapper) { ArgumentUtils.requireNonNull("annotation", annotation); ArgumentUtils.requireNonNull("member", member); - final Repeatable repeatable = annotation.getAnnotation(Repeatable.class); - if (repeatable != null) { - Object v = getRawSingleValue(repeatable.value().getName(), VALUE_MEMBER, null); + String repeatableTypeName = findRepeatableAnnotationContainerInternal(annotation.getName()); + if (repeatableTypeName != null) { + Object v = getRawSingleValue(repeatableTypeName, VALUE_MEMBER, null); if (v instanceof AnnotationValue) { return ((AnnotationValue) v).booleanValue(member, valueMapper); } @@ -573,9 +572,9 @@ public OptionalLong longValue(@NonNull Class annotation, @ public OptionalLong longValue(@NonNull Class annotation, @NonNull String member, @Nullable Function valueMapper) { ArgumentUtils.requireNonNull("annotation", annotation); ArgumentUtils.requireNonNull("member", member); - final Repeatable repeatable = annotation.getAnnotation(Repeatable.class); - if (repeatable != null) { - Object v = getRawSingleValue(repeatable.value().getName(), VALUE_MEMBER, valueMapper); + String repeatableTypeName = findRepeatableAnnotationContainerInternal(annotation.getName()); + if (repeatableTypeName != null) { + Object v = getRawSingleValue(repeatableTypeName, VALUE_MEMBER, valueMapper); if (v instanceof AnnotationValue) { return ((AnnotationValue) v).longValue(member, valueMapper); } @@ -660,9 +659,9 @@ public Optional stringValue(@NonNull Class annotat @Override public Optional stringValue(@NonNull Class annotation, @NonNull String member, Function valueMapper) { ArgumentUtils.requireNonNull("annotation", annotation); - final Repeatable repeatable = annotation.getAnnotation(Repeatable.class); - if (repeatable != null) { - Object v = getRawSingleValue(repeatable.value().getName(), VALUE_MEMBER, valueMapper); + String repeatableTypeName = findRepeatableAnnotationContainerInternal(annotation.getName()); + if (repeatableTypeName != null) { + Object v = getRawSingleValue(repeatableTypeName, VALUE_MEMBER, valueMapper); if (v instanceof AnnotationValue) { return ((AnnotationValue) v).stringValue(member, valueMapper); } @@ -696,9 +695,9 @@ public String[] stringValues(@NonNull String annotation, @NonNull String member) @NonNull public String[] stringValues(@NonNull Class annotation, @NonNull String member, Function valueMapper) { ArgumentUtils.requireNonNull("annotation", annotation); - final Repeatable repeatable = annotation.getAnnotation(Repeatable.class); - if (repeatable != null) { - Object v = getRawValue(repeatable.value().getName(), member); + String repeatableTypeName = findRepeatableAnnotationContainerInternal(annotation.getName()); + if (repeatableTypeName != null) { + Object v = getRawValue(repeatableTypeName, member); if (v instanceof AnnotationValue) { return ((AnnotationValue) v).stringValues(member, valueMapper); } @@ -748,7 +747,10 @@ public Optional stringValue(@NonNull String annotation, @NonNull String @NonNull public Optional stringValue(@NonNull String annotation, @NonNull String member, @Nullable Function valueMapper) { Object rawValue = getRawSingleValue(annotation, member, valueMapper); - if (rawValue instanceof CharSequence) { + if (rawValue instanceof String s) { + // Performance optimization to check for the actual class first to avoid the type-check polution + return Optional.of(s); + } else if (rawValue instanceof CharSequence) { return Optional.of(rawValue.toString()); } else if (rawValue instanceof Class) { String name = ((Class) rawValue).getName(); @@ -776,9 +778,9 @@ public boolean isTrue(@NonNull Class annotation, @NonNull public boolean isTrue(@NonNull Class annotation, @NonNull String member, Function valueMapper) { ArgumentUtils.requireNonNull("annotation", annotation); ArgumentUtils.requireNonNull("member", member); - final Repeatable repeatable = annotation.getAnnotation(Repeatable.class); - if (repeatable != null) { - Object v = getRawSingleValue(repeatable.value().getName(), VALUE_MEMBER, valueMapper); + String repeatableTypeName = findRepeatableAnnotationContainerInternal(annotation.getName()); + if (repeatableTypeName != null) { + Object v = getRawSingleValue(repeatableTypeName, VALUE_MEMBER, valueMapper); if (v instanceof AnnotationValue) { return ((AnnotationValue) v).isTrue(member, valueMapper); } @@ -849,9 +851,9 @@ public OptionalDouble doubleValue(@NonNull Class annotatio public OptionalDouble doubleValue(@NonNull Class annotation, @NonNull String member, @Nullable Function valueMapper) { ArgumentUtils.requireNonNull("annotation", annotation); ArgumentUtils.requireNonNull("member", member); - final Repeatable repeatable = annotation.getAnnotation(Repeatable.class); - if (repeatable != null) { - Object v = getRawSingleValue(repeatable.value().getName(), VALUE_MEMBER, valueMapper); + String repeatableTypeName = findRepeatableAnnotationContainerInternal(annotation.getName()); + if (repeatableTypeName != null) { + Object v = getRawSingleValue(repeatableTypeName, VALUE_MEMBER, valueMapper); if (v instanceof AnnotationValue) { return ((AnnotationValue) v).doubleValue(member, valueMapper); } @@ -892,15 +894,13 @@ public OptionalDouble doubleValue(@NonNull String annotation, @NonNull String me } @Override - public @NonNull - Optional getValue(@NonNull Class annotation, @NonNull String member, @NonNull Class requiredType) { + public @NonNull Optional getValue(@NonNull Class annotation, @NonNull String member, @NonNull Class requiredType) { ArgumentUtils.requireNonNull("annotation", annotation); ArgumentUtils.requireNonNull("member", member); ArgumentUtils.requireNonNull("requiredType", requiredType); - final Repeatable repeatable = annotation.getAnnotation(Repeatable.class); - final boolean isRepeatable = repeatable != null; - if (isRepeatable) { + String repeatableTypeName = findRepeatableAnnotationContainerInternal(annotation.getName()); + if (repeatableTypeName != null) { List> values = getAnnotationValuesByType(annotation); if (!values.isEmpty()) { return values.iterator().next().get(member, requiredType); @@ -913,8 +913,7 @@ Optional getValue(@NonNull Class annotation, @NonNu } @Override - public @NonNull - Optional getValue(@NonNull String annotation, @NonNull String member, @NonNull Argument requiredType) { + public @NonNull Optional getValue(@NonNull String annotation, @NonNull String member, @NonNull Argument requiredType) { return getValue(annotation, member, requiredType, null); } @@ -944,7 +943,7 @@ public Optional getValue(@NonNull String annotation, @NonNull String memb rawValue = valueMapper.apply(rawValue); } resolved = ConversionService.SHARED.convert( - rawValue, requiredType + rawValue, requiredType ); } } else if (allStereotypes != null) { @@ -956,7 +955,7 @@ public Optional getValue(@NonNull String annotation, @NonNull String memb rawValue = valueMapper.apply(rawValue); } resolved = ConversionService.SHARED.convert( - rawValue, requiredType + rawValue, requiredType ); } } @@ -971,8 +970,7 @@ public Optional getValue(@NonNull String annotation, @NonNull String memb } @Override - public @NonNull - Optional getDefaultValue(@NonNull String annotation, @NonNull String member, @NonNull Class requiredType) { + public @NonNull Optional getDefaultValue(@NonNull String annotation, @NonNull String member, @NonNull Class requiredType) { ArgumentUtils.requireNonNull("annotation", annotation); ArgumentUtils.requireNonNull("member", member); ArgumentUtils.requireNonNull("requiredType", requiredType); @@ -991,8 +989,7 @@ Optional getDefaultValue(@NonNull String annotation, @NonNull String memb @SuppressWarnings("unchecked") @Override - public @NonNull - List> getAnnotationValuesByType(@Nullable Class annotationType) { + public @NonNull List> getAnnotationValuesByType(@Nullable Class annotationType) { if (annotationType != null) { final String annotationTypeName = annotationType.getName(); List> results = annotationValuesByType.get(annotationTypeName); @@ -1021,17 +1018,14 @@ List> getAnnotationValuesByType(@Nulla @Override public List> getAnnotationValuesByName(String annotationType) { if (annotationType != null) { - String repeatableTypeName = getRepeatedName(annotationType); - if (repeatableTypeName == null) { - repeatableTypeName = AnnotationMetadataSupport.getRepeatableAnnotation(annotationType); - } + String repeatableTypeName = findRepeatableAnnotationContainerInternal(annotationType); if (repeatableTypeName != null) { List> results = - resolveRepeatableAnnotations(repeatableTypeName, - allAnnotations, - allStereotypes - ); + resolveRepeatableAnnotations(repeatableTypeName, + allAnnotations, + allStereotypes + ); if (results != null) { return results; } else if (allAnnotations != null) { @@ -1051,8 +1045,7 @@ public List> getAnnotationValuesByName } @Override - public @NonNull - List> getDeclaredAnnotationValuesByType(@NonNull Class annotationType) { + public @NonNull List> getDeclaredAnnotationValuesByType(@NonNull Class annotationType) { if (annotationType != null) { Map> sourceAnnotations = this.declaredAnnotations; Map> sourceStereotypes = this.declaredStereotypes; @@ -1070,15 +1063,9 @@ public List> getDeclaredAnnotationValu if (annotationType != null) { Map> sourceAnnotations = this.declaredAnnotations; Map> sourceStereotypes = this.declaredStereotypes; - String repeatableTypeName = getRepeatedName(annotationType); - if (repeatableTypeName == null) { - repeatableTypeName = AnnotationMetadataSupport.getRepeatableAnnotation(annotationType); - } + String repeatableTypeName = findRepeatableAnnotationContainerInternal(annotationType); List> results = - resolveRepeatableAnnotations(repeatableTypeName, - sourceAnnotations, - sourceStereotypes - ); + resolveRepeatableAnnotations(repeatableTypeName, sourceAnnotations, sourceStereotypes); if (results != null) { return results; } @@ -1094,8 +1081,8 @@ public T[] synthesizeAnnotationsByType(@NonNull Class List> values = getAnnotationValuesByType(annotationClass); return values.stream() - .map(entries -> AnnotationMetadataSupport.buildAnnotation(annotationClass, entries)) - .toArray(value -> (T[]) Array.newInstance(annotationClass, value)); + .map(entries -> AnnotationMetadataSupport.buildAnnotation(annotationClass, entries)) + .toArray(value -> (T[]) Array.newInstance(annotationClass, value)); } //noinspection unchecked @@ -1108,8 +1095,8 @@ public T[] synthesizeDeclaredAnnotationsByType(@NonNull C List> values = getAnnotationValuesByType(annotationClass); return values.stream() - .map(entries -> AnnotationMetadataSupport.buildAnnotation(annotationClass, entries)) - .toArray(value -> (T[]) Array.newInstance(annotationClass, value)); + .map(entries -> AnnotationMetadataSupport.buildAnnotation(annotationClass, entries)) + .toArray(value -> (T[]) Array.newInstance(annotationClass, value)); } //noinspection unchecked @@ -1212,10 +1199,7 @@ public List> getAnnotationValuesBySter if (annotations != null) { List> result = new ArrayList<>(annotations.size()); for (String annotation : annotations) { - String repeatableTypeName = getRepeatedName(annotation); - if (repeatableTypeName == null) { - repeatableTypeName = AnnotationMetadataSupport.getRepeatableAnnotation(annotation); - } + String repeatableTypeName = findRepeatableAnnotationContainerInternal(annotation); if (repeatableTypeName != null) { List> results = resolveRepeatableAnnotations(repeatableTypeName, @@ -1314,8 +1298,7 @@ Optional> getAnnotationType(@NonNull String name, @N @SuppressWarnings("Duplicates") @Override - public @NonNull - Optional> findAnnotation(@NonNull String annotation) { + public @NonNull Optional> findAnnotation(@NonNull String annotation) { ArgumentUtils.requireNonNull("annotation", annotation); if (allAnnotations != null && StringUtils.isNotEmpty(annotation)) { Map values = allAnnotations.get(annotation); @@ -1333,8 +1316,7 @@ Optional> findAnnotation(@NonNull Stri @SuppressWarnings("Duplicates") @Override - public @NonNull - Optional> findDeclaredAnnotation(@NonNull String annotation) { + public @NonNull Optional> findDeclaredAnnotation(@NonNull String annotation) { ArgumentUtils.requireNonNull("annotation", annotation); if (declaredAnnotations != null && StringUtils.isNotEmpty(annotation)) { Map values = declaredAnnotations.get(annotation); @@ -1351,8 +1333,7 @@ Optional> findDeclaredAnnotation(@NonN } @Override - public @NonNull - OptionalValues getValues(@NonNull String annotation, @NonNull Class valueType) { + public @NonNull OptionalValues getValues(@NonNull String annotation, @NonNull Class valueType) { ArgumentUtils.requireNonNull("annotation", annotation); ArgumentUtils.requireNonNull("valueType", valueType); if (allAnnotations != null && StringUtils.isNotEmpty(annotation)) { @@ -1388,8 +1369,7 @@ public Map getValues(@NonNull String annotation) { } @Override - public @NonNull - Optional getDefaultValue(@NonNull String annotation, @NonNull String member, @NonNull Argument requiredType) { + public @NonNull Optional getDefaultValue(@NonNull String annotation, @NonNull String member, @NonNull Argument requiredType) { ArgumentUtils.requireNonNull("annotation", annotation); ArgumentUtils.requireNonNull("member", member); ArgumentUtils.requireNonNull("requiredType", requiredType); @@ -1403,30 +1383,22 @@ Optional getDefaultValue(@NonNull String annotation, @NonNull String memb @Override public boolean isRepeatableAnnotation(Class annotation) { - if (useRepeatableDefaults) { - return isRepeatableAnnotation(annotation.getName()); - } else { - return super.isRepeatableAnnotation(annotation); - } + return isRepeatableAnnotation(annotation.getName()); } @Override public boolean isRepeatableAnnotation(String annotation) { - return AnnotationMetadataSupport.getRepeatableAnnotation(annotation) != null; + return findRepeatableAnnotationContainerInternal(annotation) != null; } @Override public Optional findRepeatableAnnotation(Class annotation) { - if (useRepeatableDefaults) { - return findRepeatableAnnotation(annotation.getName()); - } else { - return super.findRepeatableAnnotation(annotation); - } + return findRepeatableAnnotation(annotation.getName()); } @Override public Optional findRepeatableAnnotation(String annotation) { - return Optional.ofNullable(AnnotationMetadataSupport.getRepeatableAnnotation(annotation)); + return Optional.ofNullable(findRepeatableAnnotationContainerInternal(annotation)); } @Override @@ -1437,15 +1409,15 @@ public AnnotationMetadata copyAnnotationMetadata() { @Override public DefaultAnnotationMetadata clone() { DefaultAnnotationMetadata cloned = new DefaultAnnotationMetadata( - declaredAnnotations != null ? cloneMapOfMapValue(declaredAnnotations) : null, - declaredStereotypes != null ? cloneMapOfMapValue(declaredStereotypes) : null, - allStereotypes != null ? cloneMapOfMapValue(allStereotypes) : null, - allAnnotations != null ? cloneMapOfMapValue(allAnnotations) : null, - annotationsByStereotype != null ? cloneMapOfListValue(annotationsByStereotype) : null, - hasPropertyExpressions + declaredAnnotations != null ? cloneMapOfMapValue(declaredAnnotations) : null, + declaredStereotypes != null ? cloneMapOfMapValue(declaredStereotypes) : null, + allStereotypes != null ? cloneMapOfMapValue(allStereotypes) : null, + allAnnotations != null ? cloneMapOfMapValue(allAnnotations) : null, + annotationsByStereotype != null ? cloneMapOfListValue(annotationsByStereotype) : null, + hasPropertyExpressions ); - if (repeated != null) { - cloned.repeated = new HashMap<>(repeated); + if (annotationRepeatableContainer != null) { + cloned.annotationRepeatableContainer = new HashMap<>(annotationRepeatableContainer); } if (sourceRetentionAnnotations != null) { cloned.sourceRetentionAnnotations = new HashSet<>(sourceRetentionAnnotations); @@ -1458,14 +1430,14 @@ public DefaultAnnotationMetadata clone() { protected final Map> cloneMapOfMapValue(Map> toClone) { return toClone.entrySet().stream() - .map(e -> new AbstractMap.SimpleEntry<>(e.getKey(), cloneMap(e.getValue()))) - .collect(Collectors.toMap(AbstractMap.SimpleEntry::getKey, AbstractMap.SimpleEntry::getValue, (a, b) -> a, LinkedHashMap::new)); + .map(e -> new AbstractMap.SimpleEntry<>(e.getKey(), cloneMap(e.getValue()))) + .collect(Collectors.toMap(AbstractMap.SimpleEntry::getKey, AbstractMap.SimpleEntry::getValue, (a, b) -> a, LinkedHashMap::new)); } protected final Map> cloneMapOfListValue(Map> toClone) { return toClone.entrySet().stream() - .map(e -> new AbstractMap.SimpleEntry<>(e.getKey(), new ArrayList<>(e.getValue()))) - .collect(Collectors.toMap(AbstractMap.SimpleEntry::getKey, AbstractMap.SimpleEntry::getValue, (a, b) -> a, LinkedHashMap::new)); + .map(e -> new AbstractMap.SimpleEntry<>(e.getKey(), new ArrayList<>(e.getValue()))) + .collect(Collectors.toMap(AbstractMap.SimpleEntry::getKey, AbstractMap.SimpleEntry::getValue, (a, b) -> a, LinkedHashMap::new)); } protected final Map cloneMap(Map map) { @@ -1508,7 +1480,7 @@ protected void addAnnotation(String annotation, Map values @SuppressWarnings("WeakerAccess") protected void addAnnotation(String annotation, Map values, RetentionPolicy retentionPolicy) { if (annotation != null) { - String repeatedName = getRepeatedName(annotation); + String repeatedName = findRepeatableAnnotationContainerInternal(annotation); Object v = values.get(AnnotationMetadata.VALUE_MEMBER); if (v instanceof io.micronaut.core.annotation.AnnotationValue[]) { io.micronaut.core.annotation.AnnotationValue[] avs = (io.micronaut.core.annotation.AnnotationValue[]) v; @@ -1715,7 +1687,7 @@ protected final void addStereotype(List parentAnnotations, String stereo @SuppressWarnings("WeakerAccess") protected final void addStereotype(List parentAnnotations, String stereotype, Map values, RetentionPolicy retentionPolicy) { if (stereotype != null) { - String repeatedName = getRepeatedName(stereotype); + String repeatedName = findRepeatableAnnotationContainerInternal(stereotype); if (repeatedName != null) { Object v = values.get(AnnotationMetadata.VALUE_MEMBER); if (v instanceof io.micronaut.core.annotation.AnnotationValue[]) { @@ -1743,12 +1715,12 @@ protected final void addStereotype(List parentAnnotations, String stereo // add to stereotypes addAnnotation( - stereotype, - values, - null, - allStereotypes, - false, - retentionPolicy + stereotype, + values, + null, + allStereotypes, + false, + retentionPolicy ); } } @@ -1779,7 +1751,7 @@ protected void addDeclaredStereotype(List parentAnnotations, String ster @SuppressWarnings("WeakerAccess") protected void addDeclaredStereotype(List parentAnnotations, String stereotype, Map values, RetentionPolicy retentionPolicy) { if (stereotype != null) { - String repeatedName = getRepeatedName(stereotype); + String repeatedName = findRepeatableAnnotationContainerInternal(stereotype); if (repeatedName != null) { Object v = values.get(AnnotationMetadata.VALUE_MEMBER); if (v instanceof io.micronaut.core.annotation.AnnotationValue[]) { @@ -1807,12 +1779,12 @@ protected void addDeclaredStereotype(List parentAnnotations, String ster } addAnnotation( - stereotype, - values, - declaredStereotypes, - allStereotypes, - true, - retentionPolicy + stereotype, + values, + declaredStereotypes, + allStereotypes, + true, + retentionPolicy ); } @@ -1841,9 +1813,9 @@ protected void addDeclaredAnnotation(String annotation, Map values, RetentionPolicy retentionPolicy) { if (annotation != null) { boolean hasOtherMembers = false; - String repeatedName = getRepeatedName(annotation); + String repeatedName = findRepeatableAnnotationContainerInternal(annotation); if (repeatedName != null) { - for (Map.Entry entry: values.entrySet()) { + for (Map.Entry entry : values.entrySet()) { if (entry.getKey().equals(AnnotationMetadata.VALUE_MEMBER)) { Object v = entry.getValue(); if (v instanceof io.micronaut.core.annotation.AnnotationValue[]) { @@ -1887,13 +1859,11 @@ void dump() { } private List> resolveAnnotationValuesByType(Class annotationType, Map> sourceAnnotations, Map> sourceStereotypes) { - Repeatable repeatable = annotationType.getAnnotation(Repeatable.class); - if (repeatable != null) { - Class repeatableType = repeatable.value(); - final String repeatableTypeName = repeatableType.getName(); + String repeatableTypeName = findRepeatableAnnotationContainerInternal(annotationType.getName()); + if (repeatableTypeName != null) { return resolveRepeatableAnnotations(repeatableTypeName, - sourceStereotypes, - sourceAnnotations + sourceStereotypes, + sourceAnnotations ); } return null; @@ -2012,13 +1982,6 @@ private List getAnnotationsByStereotypeInternal(String stereotype) { return getAnnotationsByStereotypeInternal().computeIfAbsent(stereotype, s -> new ArrayList<>()); } - private String getRepeatedName(String annotation) { - if (repeated != null) { - return repeated.get(annotation); - } - return null; - } - @SuppressWarnings("MagicNumber") private Map> getAnnotationsByStereotypeInternal() { Map> annotations = this.annotationsByStereotype; @@ -2070,24 +2033,23 @@ private Object getRawValue(@NonNull String annotation, @NonNull String member) { } private void addRepeatableInternal( - String annotationName, - io.micronaut.core.annotation.AnnotationValue annotationValue, - Map> allAnnotations, - RetentionPolicy retentionPolicy) { + String annotationName, + io.micronaut.core.annotation.AnnotationValue annotationValue, + Map> allAnnotations, + RetentionPolicy retentionPolicy) { addRepeatableInternal(annotationName, AnnotationMetadata.VALUE_MEMBER, annotationValue, allAnnotations, retentionPolicy); } private void addRepeatableInternal( - String annotationName, - String member, - io.micronaut.core.annotation.AnnotationValue annotationValue, - Map> allAnnotations, - RetentionPolicy retentionPolicy) { - if (repeated == null) { - repeated = new HashMap<>(2); - } - - repeated.put(annotationName, annotationValue.getAnnotationName()); + String annotationName, + String member, + io.micronaut.core.annotation.AnnotationValue annotationValue, + Map> allAnnotations, + RetentionPolicy retentionPolicy) { + if (annotationRepeatableContainer == null) { + annotationRepeatableContainer = new HashMap<>(2); + } + annotationRepeatableContainer.put(annotationValue.getAnnotationName(), annotationName); if (retentionPolicy == RetentionPolicy.SOURCE) { addSourceRetentionAnnotation(annotationName); } @@ -2125,16 +2087,17 @@ private void addRepeatableInternal( */ @Internal public static AnnotationMetadata mutateMember( - AnnotationMetadata annotationMetadata, - String annotationName, - String member, - Object value) { + AnnotationMetadata annotationMetadata, + String annotationName, + String member, + Object value) { return mutateMember(annotationMetadata, annotationName, Collections.singletonMap(member, value)); } /** * Include the annotation metadata from the other instance of {@link DefaultAnnotationMetadata}. + * * @param annotationMetadata The annotation metadata * @since 4.0.0 */ @@ -2189,11 +2152,11 @@ protected void addAnnotationMetadata(DefaultAnnotationMetadata annotationMetadat } } } - if (annotationMetadata.repeated != null) { - if (repeated == null) { - repeated = new LinkedHashMap<>(annotationMetadata.repeated); + if (annotationMetadata.annotationRepeatableContainer != null) { + if (annotationRepeatableContainer == null) { + annotationRepeatableContainer = new LinkedHashMap<>(annotationMetadata.annotationRepeatableContainer); } else { - repeated.putAll(annotationMetadata.repeated); + annotationRepeatableContainer.putAll(annotationMetadata.annotationRepeatableContainer); } } if (annotationMetadata.sourceRetentionAnnotations != null) { @@ -2235,7 +2198,7 @@ public static void contributeDefaults(AnnotationMetadata target, AnnotationMetad final Map> additionalDefaults = damSource.annotationDefaultValues; if (additionalDefaults != null) { existingDefaults.putAll( - additionalDefaults + additionalDefaults ); } } else { @@ -2265,11 +2228,11 @@ public static void contributeRepeatable(AnnotationMetadata target, AnnotationMet if (target instanceof DefaultAnnotationMetadata && source instanceof DefaultAnnotationMetadata) { DefaultAnnotationMetadata damTarget = (DefaultAnnotationMetadata) target; DefaultAnnotationMetadata damSource = (DefaultAnnotationMetadata) source; - if (damSource.repeated != null && !damSource.repeated.isEmpty()) { - if (damTarget.repeated == null) { - damTarget.repeated = new HashMap<>(damSource.repeated); + if (damSource.annotationRepeatableContainer != null && !damSource.annotationRepeatableContainer.isEmpty()) { + if (damTarget.annotationRepeatableContainer == null) { + damTarget.annotationRepeatableContainer = new HashMap<>(damSource.annotationRepeatableContainer); } else { - damTarget.repeated.putAll(damSource.repeated); + damTarget.annotationRepeatableContainer.putAll(damSource.annotationRepeatableContainer); } } } @@ -2288,9 +2251,9 @@ public static void contributeRepeatable(AnnotationMetadata target, AnnotationMet */ @Internal public static AnnotationMetadata mutateMember( - AnnotationMetadata annotationMetadata, - String annotationName, - Map members) { + AnnotationMetadata annotationMetadata, + String annotationName, + Map members) { if (StringUtils.isEmpty(annotationName)) { throw new IllegalArgumentException("Argument [annotationName] cannot be blank"); } @@ -2314,7 +2277,7 @@ public static AnnotationMetadata mutateMember( defaultMetadata = defaultMetadata.clone(); defaultMetadata - .addDeclaredAnnotation(annotationName, members); + .addDeclaredAnnotation(annotationName, members); return defaultMetadata; } @@ -2322,11 +2285,12 @@ public static AnnotationMetadata mutateMember( /** * Removes an annotation for the given predicate. + * * @param predicate The predicate - * @param The annotation + * @param The annotation */ protected void removeAnnotationIf( - @NonNull Predicate> predicate) { + @NonNull Predicate> predicate) { removeAnnotationsIf(predicate, this.declaredAnnotations); removeAnnotationsIf(predicate, this.allAnnotations); } @@ -2346,6 +2310,7 @@ private void removeAnnotationsIf(@NonNull Predicate> dec if (o instanceof Collection) { Collection> col = (Collection) o; col.removeIf(av -> Arrays.stream(av.annotationClassValues(AnnotationMetadata.VALUE_MEMBER)) - .anyMatch(acv -> toBeRemoved.contains(acv.getName()))); + .anyMatch(acv -> toBeRemoved.contains(acv.getName()))); if (col.isEmpty()) { declaredAnnotations.remove(AnnotationUtil.ANN_INTERCEPTOR_BINDINGS); @@ -2447,4 +2413,14 @@ private void purgeInterceptorBindings(Map> dec } } } + + private String findRepeatableAnnotationContainerInternal(String annotation) { + if (annotationRepeatableContainer != null) { + String repeatedName = annotationRepeatableContainer.get(annotation); + if (repeatedName != null) { + return repeatedName; + } + } + return AnnotationMetadataSupport.getRepeatableAnnotation(annotation); + } } diff --git a/inject/src/main/java/io/micronaut/inject/annotation/MutableAnnotationMetadata.java b/inject/src/main/java/io/micronaut/inject/annotation/MutableAnnotationMetadata.java index 3338560d426..455b1c843fa 100644 --- a/inject/src/main/java/io/micronaut/inject/annotation/MutableAnnotationMetadata.java +++ b/inject/src/main/java/io/micronaut/inject/annotation/MutableAnnotationMetadata.java @@ -107,8 +107,8 @@ public MutableAnnotationMetadata clone() { if (annotationDefaultValues != null) { cloned.annotationDefaultValues = new LinkedHashMap<>(annotationDefaultValues); } - if (repeated != null) { - cloned.repeated = new HashMap<>(repeated); + if (annotationRepeatableContainer != null) { + cloned.annotationRepeatableContainer = new HashMap<>(annotationRepeatableContainer); } if (sourceRetentionAnnotations != null) { cloned.sourceRetentionAnnotations = new HashSet<>(sourceRetentionAnnotations); diff --git a/inject/src/main/java/io/micronaut/inject/provider/AbstractProviderDefinition.java b/inject/src/main/java/io/micronaut/inject/provider/AbstractProviderDefinition.java index 69c2355040f..4f2a9ae38e7 100644 --- a/inject/src/main/java/io/micronaut/inject/provider/AbstractProviderDefinition.java +++ b/inject/src/main/java/io/micronaut/inject/provider/AbstractProviderDefinition.java @@ -77,6 +77,11 @@ public boolean isContainerType() { return false; } + @Override + public boolean isCandidateBean(Argument beanType) { + return beanType.isAssignableFrom(getBeanType()); + } + @Override public boolean isEnabled(@NonNull BeanContext context, @Nullable BeanResolutionContext resolutionContext) { return isPresent(); diff --git a/inject/src/main/java/io/micronaut/inject/qualifiers/InterceptorBindingQualifier.java b/inject/src/main/java/io/micronaut/inject/qualifiers/InterceptorBindingQualifier.java index 3639a6a36d0..f0c280ae238 100644 --- a/inject/src/main/java/io/micronaut/inject/qualifiers/InterceptorBindingQualifier.java +++ b/inject/src/main/java/io/micronaut/inject/qualifiers/InterceptorBindingQualifier.java @@ -30,7 +30,6 @@ import java.util.ArrayList; import java.util.Collection; import java.util.Collections; -import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; @@ -52,29 +51,19 @@ public final class InterceptorBindingQualifier implements Qualifier { private final Set> supportedInterceptorTypes; InterceptorBindingQualifier(AnnotationMetadata annotationMetadata) { - final List> annotationValues = annotationMetadata - .findAnnotation(AnnotationUtil.ANN_INTERCEPTOR_BINDING_QUALIFIER) - .map(av -> av.getAnnotations(AnnotationMetadata.VALUE_MEMBER)) - .orElse(Collections.emptyList()); - this.supportedAnnotationNames = new HashMap<>(annotationValues.size()); - for (AnnotationValue annotationValue : annotationValues) { - final String name = annotationValue.stringValue().orElse(null); - if (name != null) { - final AnnotationValue members = - annotationValue.getAnnotation(META_MEMBER_MEMBERS).orElse(null); - if (members != null) { - List> existing = supportedAnnotationNames - .computeIfAbsent(name, k -> new ArrayList<>(5)); - existing.add(members); - } else { - supportedAnnotationNames.put(name, null); - } - } + final Collection> annotationValues; + AnnotationValue av = annotationMetadata.findAnnotation(AnnotationUtil.ANN_INTERCEPTOR_BINDING_QUALIFIER).orElse(null); + if (av == null) { + annotationValues = Collections.emptyList(); + } else { + annotationValues = (Collection) av.getAnnotations(AnnotationMetadata.VALUE_MEMBER); + } + supportedAnnotationNames = findSupportedAnnotations(annotationValues); + Set> supportedInterceptorTypes = CollectionUtils.newHashSet(annotationValues.size()); + for (AnnotationValue annotationValue : annotationValues) { + annotationValue.classValue(META_MEMBER_INTERCEPTOR_TYPE).ifPresent(supportedInterceptorTypes::add); } - this.supportedInterceptorTypes = annotationValues - .stream() - .flatMap(av -> av.classValue(META_MEMBER_INTERCEPTOR_TYPE).map(Stream::of).orElse(Stream.empty())) - .collect(Collectors.toSet()); + this.supportedInterceptorTypes = supportedInterceptorTypes; } /** @@ -83,27 +72,32 @@ public final class InterceptorBindingQualifier implements Qualifier { */ InterceptorBindingQualifier(Collection> bindingAnnotations) { if (CollectionUtils.isNotEmpty(bindingAnnotations)) { - this.supportedAnnotationNames = new HashMap<>(bindingAnnotations.size()); - for (AnnotationValue bindingAnnotation : bindingAnnotations) { - final String name = bindingAnnotation.stringValue().orElse(null); - if (name != null) { - final AnnotationValue members = - bindingAnnotation.getAnnotation(META_MEMBER_MEMBERS).orElse(null); - if (members != null) { - List> existing = supportedAnnotationNames - .computeIfAbsent(name, k -> new ArrayList<>(5)); - existing.add(members); - } else { - supportedAnnotationNames.putIfAbsent(name, null); - } - } - } + supportedAnnotationNames = findSupportedAnnotations(bindingAnnotations); } else { this.supportedAnnotationNames = Collections.emptyMap(); } this.supportedInterceptorTypes = Collections.emptySet(); } + private static Map>> findSupportedAnnotations(Collection> annotationValues) { + final Map>> supportedAnnotationNames = CollectionUtils.newHashMap(annotationValues.size()); + for (AnnotationValue annotationValue : annotationValues) { + final String name = annotationValue.stringValue().orElse(null); + if (name != null) { + final AnnotationValue members = + annotationValue.getAnnotation(META_MEMBER_MEMBERS).orElse(null); + if (members != null) { + List> existing = supportedAnnotationNames + .computeIfAbsent(name, k -> new ArrayList<>(5)); + existing.add(members); + } else { + supportedAnnotationNames.put(name, null); + } + } + } + return supportedAnnotationNames; + } + @Override public > Stream reduce(Class beanType, Stream candidates) { return candidates.filter(candidate -> { diff --git a/jackson-databind/src/main/java/io/micronaut/jackson/databind/convert/JacksonConverterRegistrar.java b/jackson-databind/src/main/java/io/micronaut/jackson/databind/convert/JacksonConverterRegistrar.java index ef256c55844..b0647e004c6 100644 --- a/jackson-databind/src/main/java/io/micronaut/jackson/databind/convert/JacksonConverterRegistrar.java +++ b/jackson-databind/src/main/java/io/micronaut/jackson/databind/convert/JacksonConverterRegistrar.java @@ -27,6 +27,7 @@ import com.fasterxml.jackson.databind.node.ObjectNode; import com.fasterxml.jackson.databind.type.TypeFactory; import io.micronaut.context.BeanProvider; +import io.micronaut.context.annotation.Prototype; import io.micronaut.core.annotation.Internal; import io.micronaut.core.annotation.NonNull; import io.micronaut.core.annotation.Nullable; @@ -41,7 +42,6 @@ import io.micronaut.core.util.StringUtils; import io.micronaut.jackson.JacksonConfiguration; import jakarta.inject.Inject; -import jakarta.inject.Singleton; import java.io.IOException; import java.util.ArrayList; @@ -55,7 +55,7 @@ * @author graemerocher * @since 2.0 */ -@Singleton +@Prototype @Internal public class JacksonConverterRegistrar implements TypeConverterRegistrar { diff --git a/json-core/src/main/java/io/micronaut/json/convert/JsonConverterRegistrar.java b/json-core/src/main/java/io/micronaut/json/convert/JsonConverterRegistrar.java index 1ae54d724f9..769954dac5a 100644 --- a/json-core/src/main/java/io/micronaut/json/convert/JsonConverterRegistrar.java +++ b/json-core/src/main/java/io/micronaut/json/convert/JsonConverterRegistrar.java @@ -16,6 +16,7 @@ package io.micronaut.json.convert; import io.micronaut.context.BeanProvider; +import io.micronaut.context.annotation.Prototype; import io.micronaut.core.annotation.Experimental; import io.micronaut.core.annotation.Internal; import io.micronaut.core.bind.ArgumentBinder; @@ -33,7 +34,6 @@ import io.micronaut.json.tree.JsonArray; import io.micronaut.json.tree.JsonNode; import jakarta.inject.Inject; -import jakarta.inject.Singleton; import java.io.IOException; import java.nio.charset.StandardCharsets; @@ -52,7 +52,7 @@ * @since 3.1 */ @Experimental -@Singleton +@Prototype public final class JsonConverterRegistrar implements TypeConverterRegistrar { private final BeanProvider objectCodec; private final ConversionService conversionService; diff --git a/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/basics/HelloControllerSpec.kt b/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/basics/HelloControllerSpec.kt index acc4857f989..e9c10f2c7ee 100644 --- a/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/basics/HelloControllerSpec.kt +++ b/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/basics/HelloControllerSpec.kt @@ -61,7 +61,7 @@ class HelloControllerSpec: StringSpec() { // tag::jsonmaptypes[] response = Flux.from(client.retrieve( GET("/greet/John"), - Argument.of(Map::class.java, String::class.java, String::class.java) // <1> + Argument.mapOf(String::class.java, String::class.java) // <1> )) // end::jsonmaptypes[] diff --git a/validation/src/test/groovy/io/micronaut/validation/ValidatedSpec.groovy b/validation/src/test/groovy/io/micronaut/validation/ValidatedSpec.groovy index 4cca99e5aba..87bb6894f35 100644 --- a/validation/src/test/groovy/io/micronaut/validation/ValidatedSpec.groovy +++ b/validation/src/test/groovy/io/micronaut/validation/ValidatedSpec.groovy @@ -112,7 +112,7 @@ class ValidatedSpec extends Specification { then: def e = thrown(ConstraintViolationException) - e.message == "String: must not be null" + e.message == "string: must not be null" cleanup: beanContext.close() @@ -128,7 +128,7 @@ class ValidatedSpec extends Specification { then: def e = thrown(ConstraintViolationException) - e.message == "Bar: must not be null" + e.message == "bar: must not be null" cleanup: beanContext.close() @@ -144,7 +144,7 @@ class ValidatedSpec extends Specification { then: def e = thrown(ConstraintViolationException) - e.message == "Bar.prop: must not be null" + e.message == "bar.prop: must not be null" cleanup: beanContext.close() @@ -160,7 +160,7 @@ class ValidatedSpec extends Specification { then: def e = thrown(ConstraintViolationException) - e.message == "List[0].prop: must not be null" + e.message == "list[0].prop: must not be null" cleanup: beanContext.close() @@ -176,7 +176,7 @@ class ValidatedSpec extends Specification { then: def e = thrown(ConstraintViolationException) - e.message == "Map[barObj].prop: must not be null" + e.message == "map[barObj].prop: must not be null" cleanup: beanContext.close() From 8d66590c58e44d3c1d1d28fabf10a028f8458824 Mon Sep 17 00:00:00 2001 From: Denis Stepanov Date: Tue, 17 Jan 2023 16:12:16 +0100 Subject: [PATCH 398/743] Fix adding repeatable annotations (#8618) --- .../core/annotation/AnnotationValue.java | 18 ++-- .../AddsRepeatableAnnotationSpec.groovy | 66 ++++++++++++++ .../ReplacesRepeatableAnnotationSpec.groovy | 67 ++++++++++++++ .../AddsRepeatableAnnotationSpec.groovy | 59 +++++++++++++ .../ReplacesRepeatableAnnotationSpec.groovy | 61 +++++++++++++ ...icronaut.inject.visitor.TypeElementVisitor | 4 +- .../annotation/AnnotationMetadataSupport.java | 4 +- .../annotation/DefaultAnnotationMetadata.java | 88 +++++++++---------- 8 files changed, 314 insertions(+), 53 deletions(-) create mode 100644 inject-groovy/src/test/groovy/io/micronaut/inject/annotation/repeatable/AddsRepeatableAnnotationSpec.groovy create mode 100644 inject-groovy/src/test/groovy/io/micronaut/inject/annotation/repeatable/ReplacesRepeatableAnnotationSpec.groovy create mode 100644 inject-java/src/test/groovy/io/micronaut/annotation/mapping/AddsRepeatableAnnotationSpec.groovy create mode 100644 inject-java/src/test/groovy/io/micronaut/annotation/mapping/ReplacesRepeatableAnnotationSpec.groovy diff --git a/core/src/main/java/io/micronaut/core/annotation/AnnotationValue.java b/core/src/main/java/io/micronaut/core/annotation/AnnotationValue.java index b4f2d24f8bd..6d6997a3f5d 100644 --- a/core/src/main/java/io/micronaut/core/annotation/AnnotationValue.java +++ b/core/src/main/java/io/micronaut/core/annotation/AnnotationValue.java @@ -1156,16 +1156,24 @@ final T getRequiredValue(String member, Class type) { ArgumentUtils.requireNonNull("member", member); Object v = values.get(member); - AnnotationValue[] values = null; + Collection> values = null; if (v instanceof AnnotationValue annotationValue) { - values = new AnnotationValue[]{annotationValue}; + values = Collections.singletonList(annotationValue); } else if (v instanceof AnnotationValue[] annotationValues) { - values = annotationValues; + values = Arrays.asList(annotationValues); + } else if (v instanceof Collection collection) { + final Iterator i = collection.iterator(); + if (i.hasNext()) { + final Object o = i.next(); + if (o instanceof AnnotationValue) { + values = (Collection>) collection; + } + } } - if (ArrayUtils.isEmpty(values)) { + if (CollectionUtils.isEmpty(values)) { return Collections.emptyList(); } - List> list = new ArrayList<>(values.length); + List> list = new ArrayList<>(values.size()); for (AnnotationValue value : values) { if (value == null) { continue; diff --git a/inject-groovy/src/test/groovy/io/micronaut/inject/annotation/repeatable/AddsRepeatableAnnotationSpec.groovy b/inject-groovy/src/test/groovy/io/micronaut/inject/annotation/repeatable/AddsRepeatableAnnotationSpec.groovy new file mode 100644 index 00000000000..3dff54fa7e9 --- /dev/null +++ b/inject-groovy/src/test/groovy/io/micronaut/inject/annotation/repeatable/AddsRepeatableAnnotationSpec.groovy @@ -0,0 +1,66 @@ +package io.micronaut.inject.annotation.repeatable + +import io.micronaut.ast.groovy.TypeElementVisitorStart +import io.micronaut.ast.transform.test.AbstractBeanDefinitionSpec +import io.micronaut.context.annotation.Requirements +import io.micronaut.context.annotation.Requires +import io.micronaut.core.annotation.AnnotationMetadata +import io.micronaut.core.annotation.AnnotationValue +import io.micronaut.inject.ast.ClassElement +import io.micronaut.inject.visitor.AllElementsVisitor +import io.micronaut.inject.visitor.TypeElementVisitor +import io.micronaut.inject.visitor.VisitorContext + +import java.util.stream.Stream + +class AddsRepeatableAnnotationSpec extends AbstractBeanDefinitionSpec { + def setup() { + System.setProperty(TypeElementVisitorStart.ELEMENT_VISITORS_PROPERTY, AddRepeatableTypeElementVisitor.name) + } + + def cleanup() { + System.setProperty(TypeElementVisitorStart.ELEMENT_VISITORS_PROPERTY, "") + AllElementsVisitor.clearVisited() + } + + void 'test replace simple annotation'() { + given: + def definition = buildBeanDefinition('addann.Test', ''' +package addann + +import io.micronaut.context.annotation.Requires; +import io.micronaut.inject.annotation.ScopeOne; +import io.micronaut.context.annotation.Bean; + +@Requires(property = "foo") +@Requires(property = "bar") +@Bean +class Test { + + @Requires(property = "xyz") + public Object myField; + +} +''') + expect: + definition.getAnnotationValuesByType(Requires).size() == 3 + definition.getAnnotationValuesByType(Requires).stream() + .flatMap { it.stringValue("property").stream() } + .toList().toSet() == ["foo", "bar", "xyz"].toSet() + } + + static class AddRepeatableTypeElementVisitor implements TypeElementVisitor { + @Override + void visitClass(ClassElement element, VisitorContext context) { + element.annotate(Requirements.class, builder -> builder.values( + element.getFields().stream().flatMap(this::getIndexes).toArray(AnnotationValue[]::new)) + ) + } + + private Stream> getIndexes(AnnotationMetadata am) { + return am.getAnnotationValuesByType(Requires.class).stream(); + } + } + + +} diff --git a/inject-groovy/src/test/groovy/io/micronaut/inject/annotation/repeatable/ReplacesRepeatableAnnotationSpec.groovy b/inject-groovy/src/test/groovy/io/micronaut/inject/annotation/repeatable/ReplacesRepeatableAnnotationSpec.groovy new file mode 100644 index 00000000000..340485e31a8 --- /dev/null +++ b/inject-groovy/src/test/groovy/io/micronaut/inject/annotation/repeatable/ReplacesRepeatableAnnotationSpec.groovy @@ -0,0 +1,67 @@ +package io.micronaut.inject.annotation.repeatable + +import io.micronaut.ast.groovy.TypeElementVisitorStart +import io.micronaut.ast.transform.test.AbstractBeanDefinitionSpec +import io.micronaut.context.annotation.Requirements +import io.micronaut.context.annotation.Requires +import io.micronaut.core.annotation.AnnotationMetadata +import io.micronaut.core.annotation.AnnotationValue +import io.micronaut.inject.ast.ClassElement +import io.micronaut.inject.visitor.AllElementsVisitor +import io.micronaut.inject.visitor.TypeElementVisitor +import io.micronaut.inject.visitor.VisitorContext + +import java.util.stream.Stream + +class ReplacesRepeatableAnnotationSpec extends AbstractBeanDefinitionSpec { + def setup() { + System.setProperty(TypeElementVisitorStart.ELEMENT_VISITORS_PROPERTY, ReplacesRepeatableTypeElementVisitor.name) + } + + def cleanup() { + System.setProperty(TypeElementVisitorStart.ELEMENT_VISITORS_PROPERTY, "") + AllElementsVisitor.clearVisited() + } + + void 'test replace simple annotation'() { + given: + def definition = buildBeanDefinition('addann.Test', ''' +package addann + +import io.micronaut.context.annotation.Requires; +import io.micronaut.inject.annotation.ScopeOne; +import io.micronaut.context.annotation.Bean; + +@Requires(property = "foo") +@Requires(property = "bar") +@Bean +class Test { + + @Requires(property = "xyz") + public Object myField; + +} +''') + expect: + definition.getAnnotationValuesByType(Requires).size() == 3 + definition.getAnnotationValuesByType(Requires).stream() + .flatMap { it.stringValue("property").stream() } + .toList().toSet() == ["foo", "bar", "xyz"].toSet() + } + + static class ReplacesRepeatableTypeElementVisitor implements TypeElementVisitor { + @Override + void visitClass(ClassElement element, VisitorContext context) { + final List> indexes = Stream.concat( + getIndexes(element), + element.getFields().stream().flatMap(this::getIndexes) + ).toList() + element.annotate(Requirements.class, builder -> builder.values(indexes.toArray(new AnnotationValue[]{}))); + } + + private Stream> getIndexes(AnnotationMetadata am) { + return am.getAnnotationValuesByType(Requires.class).stream(); + } + } + +} diff --git a/inject-java/src/test/groovy/io/micronaut/annotation/mapping/AddsRepeatableAnnotationSpec.groovy b/inject-java/src/test/groovy/io/micronaut/annotation/mapping/AddsRepeatableAnnotationSpec.groovy new file mode 100644 index 00000000000..c8fca4f188d --- /dev/null +++ b/inject-java/src/test/groovy/io/micronaut/annotation/mapping/AddsRepeatableAnnotationSpec.groovy @@ -0,0 +1,59 @@ +package io.micronaut.annotation.mapping + +import io.micronaut.annotation.processing.test.AbstractTypeElementSpec +import io.micronaut.context.annotation.Requirements +import io.micronaut.context.annotation.Requires +import io.micronaut.core.annotation.AnnotationMetadata +import io.micronaut.core.annotation.AnnotationValue +import io.micronaut.inject.ast.ClassElement +import io.micronaut.inject.visitor.TypeElementVisitor +import io.micronaut.inject.visitor.VisitorContext + +import java.util.stream.Stream + +class AddsRepeatableAnnotationSpec extends AbstractTypeElementSpec { + + void 'test replace simple annotation'() { + given: + def definition = buildBeanDefinition('addann.AddAnnotationsTo', ''' +package addann; + +import io.micronaut.context.annotation.Requires; +import io.micronaut.inject.annotation.ScopeOne; +import io.micronaut.context.annotation.Bean; + +@Requires(property = "foo") +@Requires(property = "bar") +@Bean +class AddAnnotationsTo { + + @Requires(property = "xyz") + public Object myField; + +} +''') + expect: + definition.getAnnotationValuesByType(Requires).size() == 3 + definition.getAnnotationValuesByType(Requires).stream() + .flatMap { it.stringValue("property").stream() } + .toList().toSet() == ["foo", "bar", "xyz"].toSet() + } + + static class AddRepeatableTypeElementVisitor implements TypeElementVisitor { + @Override + void visitClass(ClassElement element, VisitorContext context) { + if (element.getSimpleName() == "AddAnnotationsTo") { + element.annotate(Requirements.class, builder -> builder.values( + element.getFields().stream().flatMap(this::getIndexes).toArray(AnnotationValue[]::new))) + } + } + + private Stream> getIndexes(AnnotationMetadata am) { + if (am.getAnnotation(Requirements).getAnnotations("value", Requires).isEmpty()) { + throw new IllegalStateException(); + } + return am.getAnnotationValuesByType(Requires.class).stream() + } + } + +} diff --git a/inject-java/src/test/groovy/io/micronaut/annotation/mapping/ReplacesRepeatableAnnotationSpec.groovy b/inject-java/src/test/groovy/io/micronaut/annotation/mapping/ReplacesRepeatableAnnotationSpec.groovy new file mode 100644 index 00000000000..a634ccbf51a --- /dev/null +++ b/inject-java/src/test/groovy/io/micronaut/annotation/mapping/ReplacesRepeatableAnnotationSpec.groovy @@ -0,0 +1,61 @@ +package io.micronaut.annotation.mapping + +import io.micronaut.annotation.processing.test.AbstractTypeElementSpec +import io.micronaut.context.annotation.Requirements +import io.micronaut.context.annotation.Requires +import io.micronaut.core.annotation.AnnotationMetadata +import io.micronaut.core.annotation.AnnotationValue +import io.micronaut.inject.ast.ClassElement +import io.micronaut.inject.visitor.TypeElementVisitor +import io.micronaut.inject.visitor.VisitorContext + +import java.util.stream.Stream + +class ReplacesRepeatableAnnotationSpec extends AbstractTypeElementSpec { + + void 'test replace simple annotation'() { + given: + def definition = buildBeanDefinition('addann.ReplaceAnnotationsTo', ''' +package addann; + +import io.micronaut.context.annotation.Requires; +import io.micronaut.context.annotation.Bean; + +@Requires(property = "foo") +@Requires(property = "bar") +@Bean +class ReplaceAnnotationsTo { + + @Requires(property = "xyz") + public Object myField; + +} +''') + expect: + definition.getAnnotationValuesByType(Requires).size() == 3 + definition.getAnnotationValuesByType(Requires).stream() + .flatMap { it.stringValue("property").stream() } + .toList().toSet() == ["foo", "bar", "xyz"].toSet() + } + + static class ReplacesRepeatableTypeElementVisitor implements TypeElementVisitor { + @Override + void visitClass(ClassElement element, VisitorContext context) { + if (element.getSimpleName() == "ReplaceAnnotationsTo") { + final List> indexes = Stream.concat( + getIndexes(element), + element.getFields().stream().flatMap(this::getIndexes) + ).toList() + element.annotate(Requirements.class, builder -> builder.values(indexes.toArray(new AnnotationValue[]{}))) + } + } + + private Stream> getIndexes(AnnotationMetadata am) { + if (am.getAnnotation(Requirements).getAnnotations("value", Requires).isEmpty()) { + throw new IllegalStateException(); + } + return am.getAnnotationValuesByType(Requires.class).stream(); + } + } + +} diff --git a/inject-java/src/test/resources/META-INF/services/io.micronaut.inject.visitor.TypeElementVisitor b/inject-java/src/test/resources/META-INF/services/io.micronaut.inject.visitor.TypeElementVisitor index e5cbac15a83..0a527c1b32e 100644 --- a/inject-java/src/test/resources/META-INF/services/io.micronaut.inject.visitor.TypeElementVisitor +++ b/inject-java/src/test/resources/META-INF/services/io.micronaut.inject.visitor.TypeElementVisitor @@ -4,4 +4,6 @@ io.micronaut.visitors.AllElementsVisitor io.micronaut.visitors.AllClassesVisitor io.micronaut.visitors.InjectVisitor io.micronaut.inject.factory.BasicVisitor -io.micronaut.aop.introduction.repeatable.MyRepoVisitor \ No newline at end of file +io.micronaut.aop.introduction.repeatable.MyRepoVisitor +io.micronaut.annotation.mapping.AddsRepeatableAnnotationSpec$AddRepeatableTypeElementVisitor +io.micronaut.annotation.mapping.ReplacesRepeatableAnnotationSpec$ReplacesRepeatableTypeElementVisitor diff --git a/inject/src/main/java/io/micronaut/inject/annotation/AnnotationMetadataSupport.java b/inject/src/main/java/io/micronaut/inject/annotation/AnnotationMetadataSupport.java index 0d609e0a8ac..3ed6925f1d0 100644 --- a/inject/src/main/java/io/micronaut/inject/annotation/AnnotationMetadataSupport.java +++ b/inject/src/main/java/io/micronaut/inject/annotation/AnnotationMetadataSupport.java @@ -48,6 +48,7 @@ import io.micronaut.core.annotation.Introspected; import io.micronaut.core.annotation.NonNull; import io.micronaut.core.annotation.Nullable; +import io.micronaut.core.annotation.ReflectionConfig; import io.micronaut.core.annotation.UsedByGeneratedCode; import io.micronaut.core.reflect.ClassUtils; import io.micronaut.core.reflect.InstantiationUtils; @@ -143,7 +144,8 @@ public static List, Class(Indexed.class, Indexes.class), new AbstractMap.SimpleEntry<>(Requires.class, Requirements.class), new AbstractMap.SimpleEntry<>(AliasFor.class, Aliases.class), - new AbstractMap.SimpleEntry<>(Property.class, PropertySource.class) + new AbstractMap.SimpleEntry<>(Property.class, PropertySource.class), + new AbstractMap.SimpleEntry<>(ReflectionConfig.class, ReflectionConfig.ReflectionConfigList.class) ); } diff --git a/inject/src/main/java/io/micronaut/inject/annotation/DefaultAnnotationMetadata.java b/inject/src/main/java/io/micronaut/inject/annotation/DefaultAnnotationMetadata.java index f514a733cc3..35f94b1b965 100644 --- a/inject/src/main/java/io/micronaut/inject/annotation/DefaultAnnotationMetadata.java +++ b/inject/src/main/java/io/micronaut/inject/annotation/DefaultAnnotationMetadata.java @@ -1480,18 +1480,17 @@ protected void addAnnotation(String annotation, Map values @SuppressWarnings("WeakerAccess") protected void addAnnotation(String annotation, Map values, RetentionPolicy retentionPolicy) { if (annotation != null) { - String repeatedName = findRepeatableAnnotationContainerInternal(annotation); - Object v = values.get(AnnotationMetadata.VALUE_MEMBER); - if (v instanceof io.micronaut.core.annotation.AnnotationValue[]) { - io.micronaut.core.annotation.AnnotationValue[] avs = (io.micronaut.core.annotation.AnnotationValue[]) v; - for (io.micronaut.core.annotation.AnnotationValue av : avs) { - addRepeatable(annotation, av); - } - } else if (v instanceof Iterable && repeatedName != null) { - Iterable i = (Iterable) v; - for (Object o : i) { - if (o instanceof io.micronaut.core.annotation.AnnotationValue) { - addRepeatable(annotation, ((io.micronaut.core.annotation.AnnotationValue) o)); + if (isRepeatableAnnotationContainer(annotation)) { + Object v = values.get(AnnotationMetadata.VALUE_MEMBER); + if (v instanceof io.micronaut.core.annotation.AnnotationValue[] annotationValues) { + for (io.micronaut.core.annotation.AnnotationValue annotationValue : annotationValues) { + addRepeatable(annotation, annotationValue); + } + } else if (v instanceof Iterable iterable) { + for (Object o : iterable) { + if (o instanceof io.micronaut.core.annotation.AnnotationValue annotationValue) { + addRepeatable(annotation, annotationValue); + } } } } else { @@ -1687,19 +1686,16 @@ protected final void addStereotype(List parentAnnotations, String stereo @SuppressWarnings("WeakerAccess") protected final void addStereotype(List parentAnnotations, String stereotype, Map values, RetentionPolicy retentionPolicy) { if (stereotype != null) { - String repeatedName = findRepeatableAnnotationContainerInternal(stereotype); - if (repeatedName != null) { + if (isRepeatableAnnotationContainer(stereotype)) { Object v = values.get(AnnotationMetadata.VALUE_MEMBER); - if (v instanceof io.micronaut.core.annotation.AnnotationValue[]) { - io.micronaut.core.annotation.AnnotationValue[] avs = (io.micronaut.core.annotation.AnnotationValue[]) v; - for (io.micronaut.core.annotation.AnnotationValue av : avs) { - addRepeatableStereotype(parentAnnotations, stereotype, av); + if (v instanceof io.micronaut.core.annotation.AnnotationValue[] annotationValues) { + for (io.micronaut.core.annotation.AnnotationValue annotationValue : annotationValues) { + addRepeatableStereotype(parentAnnotations, stereotype, annotationValue); } - } else if (v instanceof Iterable) { - Iterable i = (Iterable) v; - for (Object o : i) { - if (o instanceof io.micronaut.core.annotation.AnnotationValue) { - addRepeatableStereotype(parentAnnotations, stereotype, (io.micronaut.core.annotation.AnnotationValue) o); + } else if (v instanceof Iterable iterable) { + for (Object o : iterable) { + if (o instanceof io.micronaut.core.annotation.AnnotationValue annotationValue) { + addRepeatableStereotype(parentAnnotations, stereotype, annotationValue); } } } @@ -1751,19 +1747,16 @@ protected void addDeclaredStereotype(List parentAnnotations, String ster @SuppressWarnings("WeakerAccess") protected void addDeclaredStereotype(List parentAnnotations, String stereotype, Map values, RetentionPolicy retentionPolicy) { if (stereotype != null) { - String repeatedName = findRepeatableAnnotationContainerInternal(stereotype); - if (repeatedName != null) { + if (isRepeatableAnnotationContainer(stereotype)) { Object v = values.get(AnnotationMetadata.VALUE_MEMBER); - if (v instanceof io.micronaut.core.annotation.AnnotationValue[]) { - io.micronaut.core.annotation.AnnotationValue[] avs = (io.micronaut.core.annotation.AnnotationValue[]) v; - for (io.micronaut.core.annotation.AnnotationValue av : avs) { - addDeclaredRepeatableStereotype(parentAnnotations, stereotype, av); + if (v instanceof io.micronaut.core.annotation.AnnotationValue[] annotationValues) { + for (io.micronaut.core.annotation.AnnotationValue annotationValue : annotationValues) { + addDeclaredRepeatableStereotype(parentAnnotations, stereotype, annotationValue); } - } else if (v instanceof Iterable) { - Iterable i = (Iterable) v; - for (Object o : i) { - if (o instanceof io.micronaut.core.annotation.AnnotationValue) { - addDeclaredRepeatableStereotype(parentAnnotations, stereotype, (io.micronaut.core.annotation.AnnotationValue) o); + } else if (v instanceof Iterable iterable) { + for (Object o : iterable) { + if (o instanceof io.micronaut.core.annotation.AnnotationValue annotationValue) { + addDeclaredRepeatableStereotype(parentAnnotations, stereotype, annotationValue); } } } @@ -1813,21 +1806,19 @@ protected void addDeclaredAnnotation(String annotation, Map values, RetentionPolicy retentionPolicy) { if (annotation != null) { boolean hasOtherMembers = false; - String repeatedName = findRepeatableAnnotationContainerInternal(annotation); - if (repeatedName != null) { + boolean repeatableAnnotationContainer = isRepeatableAnnotationContainer(annotation); + if (isRepeatableAnnotationContainer(annotation)) { for (Map.Entry entry : values.entrySet()) { if (entry.getKey().equals(AnnotationMetadata.VALUE_MEMBER)) { Object v = entry.getValue(); - if (v instanceof io.micronaut.core.annotation.AnnotationValue[]) { - io.micronaut.core.annotation.AnnotationValue[] avs = (io.micronaut.core.annotation.AnnotationValue[]) v; - for (io.micronaut.core.annotation.AnnotationValue av : avs) { - addDeclaredRepeatable(annotation, av); + if (v instanceof io.micronaut.core.annotation.AnnotationValue[] annotationValues) { + for (io.micronaut.core.annotation.AnnotationValue annotationValue : annotationValues) { + addDeclaredRepeatable(annotation, annotationValue); } - } else if (v instanceof Iterable) { - Iterable i = (Iterable) v; - for (Object o : i) { - if (o instanceof io.micronaut.core.annotation.AnnotationValue) { - addDeclaredRepeatable(annotation, ((io.micronaut.core.annotation.AnnotationValue) o)); + } else if (v instanceof Iterable iterable) { + for (Object o : iterable) { + if (o instanceof io.micronaut.core.annotation.AnnotationValue annotationValue) { + addDeclaredRepeatable(annotation, annotationValue); } } } @@ -1837,7 +1828,7 @@ protected void addDeclaredAnnotation(String annotation, Map> declaredAnnotations = getDeclaredAnnotationsInternal(); Map> allAnnotations = getAllAnnotations(); addAnnotation(annotation, values, declaredAnnotations, allAnnotations, true, retentionPolicy); @@ -2414,6 +2405,11 @@ private void purgeInterceptorBindings(Map> dec } } + private boolean isRepeatableAnnotationContainer(String annotation) { + // This method is only used during the compilation time so we don't bother checking AnnotationMetadataSupport + return annotationRepeatableContainer != null && annotationRepeatableContainer.values().contains(annotation); + } + private String findRepeatableAnnotationContainerInternal(String annotation) { if (annotationRepeatableContainer != null) { String repeatedName = annotationRepeatableContainer.get(annotation); From f7a46b8996342cd5de1552cb9d939ab64058945c Mon Sep 17 00:00:00 2001 From: Denis Stepanov Date: Tue, 17 Jan 2023 16:19:36 +0100 Subject: [PATCH 399/743] Use `PropertyExpressionResolver` for implementing random property values (#8614) --- .../value/random/RandomPropertySpec.groovy | 31 +++- .../env/PropertySourcePropertyResolver.java | 129 --------------- .../exp/RandomPropertyExpressionResolver.java | 151 ++++++++++++++++++ ...aut.context.env.PropertyExpressionResolver | 1 + .../PropertySourcePropertyResolverSpec.groovy | 10 +- 5 files changed, 187 insertions(+), 135 deletions(-) create mode 100644 inject/src/main/java/io/micronaut/context/env/exp/RandomPropertyExpressionResolver.java create mode 100644 inject/src/main/resources/META-INF/services/io.micronaut.context.env.PropertyExpressionResolver diff --git a/inject-java/src/test/groovy/io/micronaut/inject/value/random/RandomPropertySpec.groovy b/inject-java/src/test/groovy/io/micronaut/inject/value/random/RandomPropertySpec.groovy index 64bbc778b2b..9477176f3c9 100644 --- a/inject-java/src/test/groovy/io/micronaut/inject/value/random/RandomPropertySpec.groovy +++ b/inject-java/src/test/groovy/io/micronaut/inject/value/random/RandomPropertySpec.groovy @@ -22,6 +22,24 @@ import spock.lang.Specification class RandomPropertySpec extends Specification { + void "test random int"() { + given: + ApplicationContext context = ApplicationContext.run( + 'my.int':'${random.int}' + ) + + expect: + context.getProperty('my.int', Integer).isPresent() + // Validate that the random int is resolved to the same value all the time + context.getProperty('my.int', Integer).get() == context.getProperty('my.int', Integer).get() + context.getProperty('my.int', Integer).get() == context.getProperty('my.int', Integer).get() + context.getProperty('my.int', Integer).get() == context.getProperty('my.int', Integer).get() + context.getProperty('my.int', Integer).get() == context.getProperty('my.int', Integer).get() + + cleanup: + context.close() + } + void "test random port"() { given: ApplicationContext context = ApplicationContext.run( @@ -35,6 +53,8 @@ class RandomPropertySpec extends Specification { context.getProperty('my.port', Integer).get() < SocketUtils.MAX_PORT_RANGE context.getProperty('my.port', Integer).get() > SocketUtils.MIN_PORT_RANGE + cleanup: + context.close() } void "test random localhost port"() { @@ -49,7 +69,11 @@ class RandomPropertySpec extends Specification { context.getProperty('my.address', String).get() ==~ /localhost:\d+/ context.getProperty('my.addresses', String).isPresent() context.getProperty('my.addresses', String).get() ==~ /localhost:\d+,localhost:\d+/ + context.getProperty('my.address', String).get() == context.getProperty('my.address', String).get() + context.getProperty('my.addresses', String).get() == context.getProperty('my.addresses', String).get() + cleanup: + context.close() } void "test random integer"() { @@ -62,6 +86,8 @@ class RandomPropertySpec extends Specification { context.getProperty('my.number', Integer).isPresent() context.getProperty('my.number', Integer).get() == context.getProperty('my.number', Integer).get() + cleanup: + context.close() } void "test random invalid"() { @@ -69,11 +95,14 @@ class RandomPropertySpec extends Specification { ApplicationContext context = ApplicationContext.run( 'my.number':'${random.blah}' ) + context.getProperty('my.number', Integer).isPresent() then: def e = thrown(ConfigurationException) - e.message == 'Invalid random expression ${random.blah} for property: my.number' + e.message == 'Invalid random expression: random.blah' + cleanup: + context.close() } } diff --git a/inject/src/main/java/io/micronaut/context/env/PropertySourcePropertyResolver.java b/inject/src/main/java/io/micronaut/context/env/PropertySourcePropertyResolver.java index a203c4bcbc2..c65eb3243a7 100644 --- a/inject/src/main/java/io/micronaut/context/env/PropertySourcePropertyResolver.java +++ b/inject/src/main/java/io/micronaut/context/env/PropertySourcePropertyResolver.java @@ -15,7 +15,6 @@ */ package io.micronaut.context.env; -import io.micronaut.context.exceptions.ConfigurationException; import io.micronaut.core.annotation.AnnotationMetadata; import io.micronaut.core.annotation.NonNull; import io.micronaut.core.annotation.Nullable; @@ -23,7 +22,6 @@ import io.micronaut.core.convert.ConversionContext; import io.micronaut.core.convert.ConversionService; import io.micronaut.core.convert.format.MapFormat; -import io.micronaut.core.io.socket.SocketUtils; import io.micronaut.core.naming.NameUtils; import io.micronaut.core.naming.conventions.StringConvention; import io.micronaut.core.optim.StaticOptimizations; @@ -34,10 +32,8 @@ import io.micronaut.core.util.StringUtils; import io.micronaut.core.value.MapPropertyResolver; import io.micronaut.core.value.PropertyResolver; -import io.micronaut.core.value.ValueException; import org.slf4j.Logger; -import java.security.SecureRandom; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -52,7 +48,6 @@ import java.util.Optional; import java.util.Properties; import java.util.Set; -import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; import java.util.function.Consumer; import java.util.function.Supplier; @@ -72,11 +67,6 @@ public class PropertySourcePropertyResolver implements PropertyResolver, AutoClo private static final EnvironmentProperties CURRENT_ENV = StaticOptimizations.get(EnvironmentProperties.class) .orElseGet(EnvironmentProperties::empty); private static final Pattern DOT_PATTERN = Pattern.compile("\\."); - private static final String RANDOM_PREFIX = "\\s?random\\.(\\S+?)"; - private static final String RANDOM_UPPER_LIMIT = "(\\(-?\\d+(\\.\\d+)?\\))"; - private static final String RANDOM_RANGE = "(\\[-?\\d+(\\.\\d+)?,\\s?-?\\d+(\\.\\d+)?])"; - - private static final Pattern RANDOM_PATTERN = Pattern.compile("\\$\\{" + RANDOM_PREFIX + "(" + RANDOM_UPPER_LIMIT + "|" + RANDOM_RANGE + ")?\\}"); private static final Object NO_VALUE = new Object(); private static final PropertyCatalog[] CONVENTIONS = {PropertyCatalog.GENERATED, PropertyCatalog.RAW}; @@ -90,7 +80,6 @@ public class PropertySourcePropertyResolver implements PropertyResolver, AutoClo protected final Map[] catalog = new Map[58]; protected final Map[] rawCatalog = new Map[58]; protected final Map[] nonGenerated = new Map[58]; - private final SecureRandom random = new SecureRandom(); private final Map containsCache = new ConcurrentHashMap<>(20); private final Map resolvedValueCache = new ConcurrentHashMap<>(20); private final EnvironmentProperties environmentProperties = EnvironmentProperties.fork(CURRENT_ENV); @@ -586,21 +575,6 @@ protected void processPropertySource(PropertySource properties, PropertySource.P Object value = properties.get(property); - if (value instanceof CharSequence) { - value = processRandomExpressions(convention, property, (CharSequence) value); - } else if (value instanceof List) { - final ListIterator i = ((List) value).listIterator(); - while (i.hasNext()) { - final Object o = i.next(); - if (o instanceof CharSequence) { - final CharSequence newValue = processRandomExpressions(convention, property, (CharSequence) o); - if (newValue != o) { - i.set(newValue); - } - } - } - } - List resolvedProperties = resolvePropertiesForConvention(property, convention); boolean first = true; for (String resolvedProperty : resolvedProperties) { @@ -722,67 +696,6 @@ private void collapseProperty(String prefix, Map entries, Object } } - private CharSequence processRandomExpressions(PropertySource.PropertyConvention convention, String property, CharSequence str) { - if (convention != PropertySource.PropertyConvention.ENVIRONMENT_VARIABLE && str.toString().contains(propertyPlaceholderResolver.getPrefix())) { - StringBuffer newValue = new StringBuffer(); - Matcher matcher = RANDOM_PATTERN.matcher(str); - boolean hasRandoms = false; - while (matcher.find()) { - hasRandoms = true; - String type = matcher.group(1).trim().toLowerCase(); - String range = matcher.group(2); - if (range != null) { - range = range.substring(1, range.length() - 1); - } - String randomValue; - switch (type) { - case "port": - randomValue = String.valueOf(SocketUtils.findAvailableTcpPort()); - break; - case "int": - case "integer": - randomValue = String.valueOf(range == null ? random.nextInt() : getNextIntegerInRange(range, property)); - break; - case "long": - randomValue = String.valueOf(range == null ? random.nextLong() : getNextLongInRange(range, property)); - break; - case "float": - randomValue = String.valueOf(range == null ? random.nextFloat() : getNextFloatInRange(range, property)); - break; - case "shortuuid": - randomValue = UUID.randomUUID().toString().substring(25, 35); - break; - case "uuid": - randomValue = UUID.randomUUID().toString(); - break; - case "uuid2": - randomValue = UUID.randomUUID().toString().replace("-", ""); - break; - default: - throw new ConfigurationException("Invalid random expression " + matcher.group(0) + " for property: " + property); - } - matcher.appendReplacement(newValue, randomValue); - } - - if (hasRandoms) { - matcher.appendTail(newValue); - return newValue.toString(); - } - - } - return str; - } - - /** - * @param name The name - * @param allowCreate Whether allows creation - * @return The map with the resolved entries for the name - */ - @SuppressWarnings("MagicNumber") - protected Map resolveEntriesForKey(String name, boolean allowCreate) { - return resolveEntriesForKey(name, allowCreate, null); - } - /** * @param name The name * @param allowCreate Whether allows creation @@ -900,48 +813,6 @@ private void fill(List list, Integer toIndex, Object value) { } } - private int getNextIntegerInRange(String range, String property) { - try { - String[] tokens = range.split(","); - int lowerBound = Integer.parseInt(tokens[0]); - if (tokens.length == 1) { - return lowerBound >= 0 ? 1 : -1 * (random.nextInt(Math.abs(lowerBound))); - } - int upperBound = Integer.parseInt(tokens[1]); - return lowerBound + (int) (Math.random() * (upperBound - lowerBound)); - } catch (NumberFormatException ex) { - throw new ValueException("Invalid range: `" + range + "` found for type Integer while parsing property: " + property, ex); - } - } - - private long getNextLongInRange(String range, String property) { - try { - String[] tokens = range.split(","); - long lowerBound = Long.parseLong(tokens[0]); - if (tokens.length == 1) { - return (long) (Math.random() * (lowerBound)); - } - long upperBound = Long.parseLong(tokens[1]); - return lowerBound + (long) (Math.random() * (upperBound - lowerBound)); - } catch (NumberFormatException ex) { - throw new ValueException("Invalid range: `" + range + "` found for type Long while parsing property: " + property, ex); - } - } - - private float getNextFloatInRange(String range, String property) { - try { - String[] tokens = range.split(","); - float lowerBound = Float.parseFloat(tokens[0]); - if (tokens.length == 1) { - return (float) (Math.random() * (lowerBound)); - } - float upperBound = Float.parseFloat(tokens[1]); - return lowerBound + (float) (Math.random() * (upperBound - lowerBound)); - } catch (NumberFormatException ex) { - throw new ValueException("Invalid range: `" + range + "` found for type Float while parsing property: " + property, ex); - } - } - @Override public void close() throws Exception { if (propertyPlaceholderResolver instanceof AutoCloseable) { diff --git a/inject/src/main/java/io/micronaut/context/env/exp/RandomPropertyExpressionResolver.java b/inject/src/main/java/io/micronaut/context/env/exp/RandomPropertyExpressionResolver.java new file mode 100644 index 00000000000..0e23dd01383 --- /dev/null +++ b/inject/src/main/java/io/micronaut/context/env/exp/RandomPropertyExpressionResolver.java @@ -0,0 +1,151 @@ +/* + * Copyright 2017-2023 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.context.env.exp; + +import io.micronaut.context.env.PropertyExpressionResolver; +import io.micronaut.context.exceptions.ConfigurationException; +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.convert.ConversionService; +import io.micronaut.core.io.socket.SocketUtils; +import io.micronaut.core.value.PropertyResolver; +import io.micronaut.core.value.ValueException; + +import java.security.SecureRandom; +import java.util.Locale; +import java.util.Optional; +import java.util.UUID; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * The property expression for random values. + * + * @author Denis Stepanov + * @since 4.0.0 + */ +@Internal +public final class RandomPropertyExpressionResolver implements PropertyExpressionResolver { + + private static final String RANDOM_PREFIX = "random."; + + @Override + public Optional resolve(PropertyResolver propertyResolver, ConversionService conversionService, String expression, Class requiredType) { + expression = expression.toLowerCase(Locale.ROOT); + if (!expression.startsWith(RANDOM_PREFIX)) { + return Optional.empty(); + } + String value = expression.substring(RANDOM_PREFIX.length()).toLowerCase(); + return Optional.of(conversionService.convertRequired(resolveRandomValue(value, expression), requiredType)); + } + + private Object resolveRandomValue(String value, String expression) { + switch (value) { + case "port" -> { + return SocketUtils.findAvailableTcpPort(); + } + case "int", "integer" -> { + return LazyInit.RANDOM.nextInt(); + } + case "long" -> { + return LazyInit.RANDOM.nextLong(); + } + case "float" -> { + return LazyInit.RANDOM.nextFloat(); + } + case "shortuuid" -> { + return UUID.randomUUID().toString().substring(25, 35); + } + case "uuid" -> { + return UUID.randomUUID(); + } + case "uuid2" -> { + return UUID.randomUUID().toString().replace("-", ""); + } + default -> { + Matcher matcher = LazyInit.RANGE_PATTERN.matcher(value); + if (matcher.find()) { + String rangeType = matcher.group(1).trim().toLowerCase(); + String range = matcher.group(2); + if (range != null) { + range = range.substring(1, range.length() - 1); + switch (rangeType) { + case "int", "integer" -> { + return getNextIntegerInRange(range, expression); + } + case "long" -> { + return getNextLongInRange(range, expression); + } + case "float" -> { + return getNextFloatInRange(range, expression); + } + } + } + } + throw new ConfigurationException("Invalid random expression: " + expression); + } + } + } + + private int getNextIntegerInRange(String range, String expression) { + try { + String[] tokens = range.split(","); + int lowerBound = Integer.parseInt(tokens[0]); + if (tokens.length == 1) { + return (lowerBound >= 0 ? 1 : -1) * LazyInit.RANDOM.nextInt(Math.abs(lowerBound)); + } + int upperBound = Integer.parseInt(tokens[1]); + return LazyInit.RANDOM.nextInt(lowerBound, upperBound); + } catch (NumberFormatException ex) { + throw new ValueException("Invalid range: `" + range + "` found for type Integer for expression: " + expression, ex); + } + } + + private long getNextLongInRange(String range, String expression) { + try { + String[] tokens = range.split(","); + long lowerBound = Long.parseLong(tokens[0]); + if (tokens.length == 1) { + return (lowerBound >= 0 ? 1 : -1) * LazyInit.RANDOM.nextLong(Math.abs(lowerBound)); + } + long upperBound = Long.parseLong(tokens[1]); + return LazyInit.RANDOM.nextLong(lowerBound, upperBound); + } catch (NumberFormatException ex) { + throw new ValueException("Invalid range: `" + range + "` found for type Long for expression: " + expression, ex); + } + } + + private float getNextFloatInRange(String range, String expression) { + try { + String[] tokens = range.split(","); + float lowerBound = Float.parseFloat(tokens[0]); + if (tokens.length == 1) { + return (lowerBound >= 0 ? 1 : -1) * LazyInit.RANDOM.nextFloat(Math.abs(lowerBound)); + } + float upperBound = Float.parseFloat(tokens[1]); + return LazyInit.RANDOM.nextFloat(lowerBound, upperBound); + } catch (NumberFormatException ex) { + throw new ValueException("Invalid range: `" + range + "` found for type Float for expression: " + expression, ex); + } + } + + static class LazyInit { + private static final SecureRandom RANDOM = new SecureRandom(); + private static final String RANDOM_UPPER_LIMIT = "(\\(-?\\d+(\\.\\d+)?\\))"; + private static final String RANDOM_RANGE = "(\\[-?\\d+(\\.\\d+)?,\\s?-?\\d+(\\.\\d+)?])"; + private static final Pattern RANGE_PATTERN = Pattern.compile("\\s?(\\S+?)(" + RANDOM_UPPER_LIMIT + "|" + RANDOM_RANGE + ")"); + } + +} diff --git a/inject/src/main/resources/META-INF/services/io.micronaut.context.env.PropertyExpressionResolver b/inject/src/main/resources/META-INF/services/io.micronaut.context.env.PropertyExpressionResolver new file mode 100644 index 00000000000..30f94636d6f --- /dev/null +++ b/inject/src/main/resources/META-INF/services/io.micronaut.context.env.PropertyExpressionResolver @@ -0,0 +1 @@ +io.micronaut.context.env.exp.RandomPropertyExpressionResolver diff --git a/inject/src/test/groovy/io/micronaut/context/env/PropertySourcePropertyResolverSpec.groovy b/inject/src/test/groovy/io/micronaut/context/env/PropertySourcePropertyResolverSpec.groovy index d32c3e5524f..993ff756d1f 100644 --- a/inject/src/test/groovy/io/micronaut/context/env/PropertySourcePropertyResolverSpec.groovy +++ b/inject/src/test/groovy/io/micronaut/context/env/PropertySourcePropertyResolverSpec.groovy @@ -439,11 +439,11 @@ class PropertySourcePropertyResolverSpec extends Specification { ] new PropertySourcePropertyResolver( PropertySource.of("test", values) - ) + ).getProperty('random.integer', String).isPresent() then: def ex = thrown(ValueException) - ex.message == 'Invalid range: `9999999999` found for type Integer while parsing property: random.integer' + ex.message == 'Invalid range: `9999999999` found for type Integer for expression: random.integer(9999999999)' ex.cause != null ex.cause instanceof NumberFormatException } @@ -455,11 +455,11 @@ class PropertySourcePropertyResolverSpec extends Specification { ] new PropertySourcePropertyResolver( PropertySource.of("test", values) - ) + ).getProperty('random.long', String).isPresent() then: def ex = thrown(ValueException) - ex.message == 'Invalid range: `9999999999999999999` found for type Long while parsing property: random.long' + ex.message == 'Invalid range: `9999999999999999999` found for type Long for expression: random.long(9999999999999999999)' ex.cause != null ex.cause instanceof NumberFormatException } @@ -471,7 +471,7 @@ class PropertySourcePropertyResolverSpec extends Specification { ] new PropertySourcePropertyResolver( PropertySource.of("test", values) - ) + ).getProperty('random.invalid', String).isPresent() then: thrown(ConfigurationException) From ca1804004f02b302b17987f476b062e87ac87521 Mon Sep 17 00:00:00 2001 From: Gergely Kiss Date: Wed, 18 Jan 2023 12:45:45 +0100 Subject: [PATCH 400/743] Support Range requests when returning SystemFiles (#8553) Fixes #7911 --- ...ttySystemFileCustomizableResponseType.java | 86 +++++++++++++++++-- .../netty/types/FileTypeHandlerSpec.groovy | 33 ++++++- 2 files changed, 109 insertions(+), 10 deletions(-) diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/types/files/NettySystemFileCustomizableResponseType.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/types/files/NettySystemFileCustomizableResponseType.java index 1f87b9b5255..5f44f8a5183 100644 --- a/http-server-netty/src/main/java/io/micronaut/http/server/netty/types/files/NettySystemFileCustomizableResponseType.java +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/types/files/NettySystemFileCustomizableResponseType.java @@ -17,8 +17,12 @@ import io.micronaut.core.annotation.Internal; import io.micronaut.core.annotation.NonNull; +import io.micronaut.core.annotation.Nullable; import io.micronaut.core.util.SupplierUtil; +import io.micronaut.http.HttpHeaders; +import io.micronaut.http.HttpMethod; import io.micronaut.http.HttpRequest; +import io.micronaut.http.HttpStatus; import io.micronaut.http.MediaType; import io.micronaut.http.MutableHttpResponse; import io.micronaut.http.netty.NettyMutableHttpResponse; @@ -51,6 +55,8 @@ import java.util.Optional; import java.util.function.Supplier; +import static io.micronaut.http.HttpHeaders.CONTENT_RANGE; + /** * Writes a {@link File} to the Netty context. * @@ -62,6 +68,7 @@ public class NettySystemFileCustomizableResponseType extends SystemFile implements NettyFileCustomizableResponseType { private static final int LENGTH_8K = 8192; + private static final String UNIT_BYTES = "bytes"; private static final Logger LOG = LoggerFactory.getLogger(NettySystemFileCustomizableResponseType.class); protected Optional delegate = Optional.empty(); @@ -99,7 +106,6 @@ public MediaType getMediaType() { */ @Override public void process(MutableHttpResponse response) { - response.header(io.micronaut.http.HttpHeaders.CONTENT_LENGTH, String.valueOf(getLength())); delegate.ifPresent(type -> type.process(response)); } @@ -108,7 +114,35 @@ public ChannelFuture write(HttpRequest request, MutableHttpResponse respon if (response instanceof NettyMutableHttpResponse) { - NettyMutableHttpResponse nettyResponse = ((NettyMutableHttpResponse) response); + NettyMutableHttpResponse nettyResponse = ((NettyMutableHttpResponse) response); + + // Parse the range headers (if any), and determine the position and content length + // Only `bytes` ranges are supported. Only single ranges are supported. Invalid ranges fall back to returning the full response. + // See https://httpwg.org/specs/rfc9110.html#field.range + long fileLength = getLength(); + String rangeHeader = request.getHeaders().get(HttpHeaders.RANGE); + long position = 0; + long contentLength = fileLength; + if (rangeHeader != null + && request.getMethod() == HttpMethod.GET // A server MUST ignore a Range header field received with a request method that is unrecognized or for which range handling is not defined. + && rangeHeader.startsWith(UNIT_BYTES) // An origin server MUST ignore a Range header field that contains a range unit it does not understand. + && response.status() == HttpStatus.OK // The Range header field is evaluated after evaluating the precondition header fields defined in Section 13.1, and only if the result in absence of the Range header field would be a 200 (OK) response. + ) { + IntRange range = parseRangeHeader(rangeHeader, fileLength); + if (range != null // A server that supports range requests MAY ignore or reject a Range header field that contains an invalid ranges-specifier (Section 14.1.1) + && range.firstPos < range.lastPos // A server that supports range requests MAY ignore a Range header field when the selected representation has no content (i.e., the selected representation's data is of zero length). + && range.firstPos < fileLength + && range.lastPos < fileLength + ) { + position = range.firstPos; + contentLength = range.lastPos + 1 - range.firstPos; + response.status(HttpStatus.PARTIAL_CONTENT); + response.header(CONTENT_RANGE, String.format("%s %d-%d/%d", UNIT_BYTES, range.firstPos, range.lastPos, fileLength)); + } + } + + response.header(HttpHeaders.ACCEPT_RANGES, UNIT_BYTES); + response.header(HttpHeaders.CONTENT_LENGTH, Long.toString(contentLength)); // Write the request data final DefaultHttpResponse finalResponse = new DefaultHttpResponse(nettyResponse.getNettyHttpVersion(), nettyResponse.getNettyHttpStatus(), nettyResponse.getNettyHeaders()); @@ -121,19 +155,19 @@ public ChannelFuture write(HttpRequest request, MutableHttpResponse respon // Write the content. if (context.pipeline().get(SslHandler.class) == null && - context.pipeline().get(SmartHttpContentCompressor.class).shouldSkip(finalResponse) && - !(context.channel() instanceof Http2StreamChannel)) { + context.pipeline().get(SmartHttpContentCompressor.class).shouldSkip(finalResponse) && + !(context.channel() instanceof Http2StreamChannel)) { // SSL not enabled - can use zero-copy file transfer. - context.write(new DefaultFileRegion(file.raf.getChannel(), 0, getLength()), context.newProgressivePromise()) - .addListener(file); + context.write(new DefaultFileRegion(file.raf.getChannel(), position, contentLength), context.newProgressivePromise()) + .addListener(file); return context.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT); } else { // SSL enabled - cannot use zero-copy file transfer. try { // HttpChunkedInput will write the end marker (LastHttpContent) for us. - final HttpChunkedInput chunkedInput = new HttpChunkedInput(new ChunkedFile(file.raf, 0, getLength(), LENGTH_8K)); + final HttpChunkedInput chunkedInput = new HttpChunkedInput(new ChunkedFile(file.raf, position, contentLength, LENGTH_8K)); return context.writeAndFlush(chunkedInput, context.newProgressivePromise()) - .addListener(file); + .addListener(file); } catch (IOException e) { throw new CustomizableResponseTypeException("Could not read file", e); } @@ -143,6 +177,40 @@ public ChannelFuture write(HttpRequest request, MutableHttpResponse respon } } + @Nullable + private static IntRange parseRangeHeader(String value, long contentLength) { + int equalsIdx = value.indexOf('='); + if (equalsIdx < 0 || equalsIdx == value.length() - 1) { + return null; // Malformed range + } + + int minusIdx = value.indexOf('-', equalsIdx + 1); + if (minusIdx < 0) { + return null; // Malformed range + } + + String from = value.substring(equalsIdx + 1, minusIdx).trim(); + String to = value.substring(minusIdx + 1).trim(); + try { + long fromPosition = from.isEmpty() ? 0 : Long.parseLong(from); + long toPosition = to.isEmpty() ? contentLength - 1 : Long.parseLong(to); + return new IntRange(fromPosition, toPosition); + } catch (NumberFormatException e) { + return null; // Malformed range + } + } + + // See https://httpwg.org/specs/rfc9110.html#rule.int-range + private static class IntRange { + private final long firstPos; + private final long lastPos; + + IntRange(long firstPos, long lastPos) { + this.firstPos = firstPos; + this.lastPos = lastPos; + } + } + /** * Wrapper class around {@link RandomAccessFile} with two purposes: Leak detection, and implementation of * {@link ChannelFutureListener} that closes the file when called. @@ -150,7 +218,7 @@ public ChannelFuture write(HttpRequest request, MutableHttpResponse respon private static final class FileHolder implements ChannelFutureListener { //to avoid initializing Netty at build time private static final Supplier> LEAK_DETECTOR = SupplierUtil.memoized(() -> - ResourceLeakDetectorFactory.instance().newResourceLeakDetector(RandomAccessFile.class)); + ResourceLeakDetectorFactory.instance().newResourceLeakDetector(RandomAccessFile.class)); final RandomAccessFile raf; final long length; diff --git a/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/types/FileTypeHandlerSpec.groovy b/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/types/FileTypeHandlerSpec.groovy index c3900daae9e..6b36d98ccaa 100644 --- a/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/types/FileTypeHandlerSpec.groovy +++ b/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/types/FileTypeHandlerSpec.groovy @@ -47,13 +47,16 @@ import java.time.ZonedDateTime import java.time.temporal.ChronoUnit import java.util.concurrent.ExecutorService +import static io.micronaut.http.HttpHeaders.ACCEPT_RANGES import static io.micronaut.http.HttpHeaders.CACHE_CONTROL import static io.micronaut.http.HttpHeaders.CONTENT_DISPOSITION import static io.micronaut.http.HttpHeaders.CONTENT_LENGTH +import static io.micronaut.http.HttpHeaders.CONTENT_RANGE import static io.micronaut.http.HttpHeaders.CONTENT_TYPE import static io.micronaut.http.HttpHeaders.DATE import static io.micronaut.http.HttpHeaders.EXPIRES import static io.micronaut.http.HttpHeaders.LAST_MODIFIED +import static io.micronaut.http.HttpHeaders.RANGE class FileTypeHandlerSpec extends AbstractMicronautSpec { @@ -91,6 +94,34 @@ class FileTypeHandlerSpec extends AbstractMicronautSpec { response.header(DATE) } + void "test 206 is returned for Byte-Range queries"() { + when: + MutableHttpRequest request = HttpRequest.GET('/test/html') + request.headers.add(RANGE, range) + def response = rxClient.toBlocking().exchange(request, String) + + then: + response.code() == expectedStatus + response.header(ACCEPT_RANGES) == "bytes" + response.header(CONTENT_RANGE) == expectedContentRange + response.header(CONTENT_LENGTH) == Long.toString(expectedContent.length()) + response.body() == expectedContent + + where: + range | expectedStatus | expectedContentRange | expectedContent + "" | 200 | null | tempFileContents + "bytes" | 200 | null | tempFileContents + "bytes=" | 200 | null | tempFileContents + "bytes=2" | 200 | null | tempFileContents + "bytes=9000-" | 200 | null | tempFileContents + "bytes=0-9000" | 200 | null | tempFileContents + "bytes=abc-10" | 200 | null | tempFileContents + "bytes=0-def" | 200 | null | tempFileContents + "bytes=0-" | 206 | "bytes 0-${tempFile.length() - 1}/${tempFile.length()}" | tempFileContents + "bytes=10-" | 206 | "bytes 10-${tempFile.length() - 1}/${tempFile.length()}" | tempFileContents.substring(10) + "bytes=1-2" | 206 | "bytes 1-2/${tempFile.length()}" | tempFileContents.substring(1, 3) + } + void "test cache control can be overridden"() { when: MutableHttpRequest request = HttpRequest.GET('/test/custom-cache-control') @@ -314,7 +345,7 @@ class FileTypeHandlerSpec extends AbstractMicronautSpec { @Get('/custom-cache-control') HttpResponse cacheControl() { HttpResponse.ok(tempFile) - .header(CACHE_CONTROL, "public, immutable, max-age=31556926") + .header(CACHE_CONTROL, "public, immutable, max-age=31556926") } @Get('/different-name') From cc4e89a11ccb4c3061052704813fc1ee3009c58b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20S=C3=A1nchez-Mariscal?= Date: Wed, 18 Jan 2023 15:10:45 +0100 Subject: [PATCH 401/743] Refresh Maven core plugin versions (#8623) --- parent/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/parent/build.gradle b/parent/build.gradle index 9337761c630..6ba0d0da06c 100644 --- a/parent/build.gradle +++ b/parent/build.gradle @@ -51,9 +51,9 @@ ext.extraPomInfo = { 'function-maven-plugin.version'('0.9.8') 'jib-maven-plugin.version'('3.1.4') 'maven-compiler-plugin.version'('3.10.1') // Override actual Maven compiler version (3.1) because some bugs cause annotation processors doesn't work well - 'maven-deploy-plugin.version'('3.0.0-M2') + 'maven-deploy-plugin.version'('3.0.0') 'maven-failsafe-plugin.version'('2.22.2') // Override actual Maven surefire and failsafe version (2.12) to get native support for executing tests on the JUnit Platform (JUnit 5) - 'maven-install-plugin.version'('3.0.0-M1') + 'maven-install-plugin.version'('3.1.0') 'maven-jar-plugin.version'('3.2.2') 'maven-resources-plugin.version'('3.2.0') 'maven-shade-plugin.version'('3.2.4') From c5d2ce3cc07f791432f5334ee067477912d30602 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roger=20Vi=C3=B1as=20Alcon?= Date: Thu, 19 Jan 2023 10:54:58 +0100 Subject: [PATCH 402/743] Links to bootstrap configuration (#8609) --- .../distributedConfigurationAwsParameterStore.adoc | 2 +- .../cloudConfiguration/distributedConfigurationConsul.adoc | 2 +- .../cloudConfiguration/distributedConfigurationSpringCloud.adoc | 2 +- .../cloud/cloudConfiguration/distributedConfigurationVault.adoc | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/docs/guide/cloud/cloudConfiguration/distributedConfigurationAwsParameterStore.adoc b/src/main/docs/guide/cloud/cloudConfiguration/distributedConfigurationAwsParameterStore.adoc index 0770fbda845..e7f99d548df 100644 --- a/src/main/docs/guide/cloud/cloudConfiguration/distributedConfigurationAwsParameterStore.adoc +++ b/src/main/docs/guide/cloud/cloudConfiguration/distributedConfigurationAwsParameterStore.adoc @@ -2,7 +2,7 @@ Micronaut supports configuration sharing via AWS System Manager Parameter Store. dependency:io.micronaut.aws:micronaut-aws-parameter-store[] -To enable distributed configuration, create a `src/main/resources/bootstrap.yml` configuration file and add Parameter Store configuration: +To enable distributed configuration, make sure https://docs.micronaut.io/latest/guide/#bootstrap[bootstrap] is enabled and create a `src/main/resources/bootstrap.yml` file with the following configuration: .bootstrap.yml [source,yaml] diff --git a/src/main/docs/guide/cloud/cloudConfiguration/distributedConfigurationConsul.adoc b/src/main/docs/guide/cloud/cloudConfiguration/distributedConfigurationConsul.adoc index 827dcb508f8..45e90cb47cc 100644 --- a/src/main/docs/guide/cloud/cloudConfiguration/distributedConfigurationConsul.adoc +++ b/src/main/docs/guide/cloud/cloudConfiguration/distributedConfigurationConsul.adoc @@ -23,7 +23,7 @@ $ mn create-app my-app --features config-consul ---- ==== -To enable distributed configuration, similar to Spring Boot and Grails, create a `src/main/resources/bootstrap.yml` configuration file and enable the configuration client: +To enable distributed configuration, similar to Spring Boot and Grails, make sure https://docs.micronaut.io/latest/guide/#bootstrap[bootstrap] is enabled and create a `src/main/resources/bootstrap.yml` file with the following configuration: .bootstrap.yml [source,yaml] diff --git a/src/main/docs/guide/cloud/cloudConfiguration/distributedConfigurationSpringCloud.adoc b/src/main/docs/guide/cloud/cloudConfiguration/distributedConfigurationSpringCloud.adoc index 7934c6ff2e2..b55f0ee3168 100644 --- a/src/main/docs/guide/cloud/cloudConfiguration/distributedConfigurationSpringCloud.adoc +++ b/src/main/docs/guide/cloud/cloudConfiguration/distributedConfigurationSpringCloud.adoc @@ -1,6 +1,6 @@ Since 1.1, Micronaut features a native https://spring.io/projects/spring-cloud-config[Spring Cloud Configuration] for those who have not switched to a dedicated more complete solution like Consul. -To enable support for Spring Cloud Configuration, create a `src/main/resources/bootstrap.yml` configuration file and add the following configuration: +To enable support for Spring Cloud Configuration, make sure https://docs.micronaut.io/latest/guide/#bootstrap[bootstrap] is enabled and create a `src/main/resources/bootstrap.yml` file with the following configuration: .Integrating with Spring Cloud Configuration [source,yaml] diff --git a/src/main/docs/guide/cloud/cloudConfiguration/distributedConfigurationVault.adoc b/src/main/docs/guide/cloud/cloudConfiguration/distributedConfigurationVault.adoc index de53b221edd..cfd98750950 100644 --- a/src/main/docs/guide/cloud/cloudConfiguration/distributedConfigurationVault.adoc +++ b/src/main/docs/guide/cloud/cloudConfiguration/distributedConfigurationVault.adoc @@ -1,6 +1,6 @@ Micronaut integrates with https://www.vaultproject.io/[HashiCorp Vault] as a distributed configuration source. -To enable support for Vault Configuration, create a `src/main/resources/bootstrap.yml` configuration file and add the following configuration: +To enable support for Vault Configuration, make sure https://docs.micronaut.io/latest/guide/#bootstrap[bootstrap] is enabled and create a `src/main/resources/bootstrap.yml` file with the following configuration: .Integrating with HashiCorp Vault [source,yaml] From 41131456aad0338e21715e5053d9ec7cc8da8e50 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Champeau?= Date: Thu, 19 Jan 2023 11:03:58 +0100 Subject: [PATCH 403/743] Update docs to the new configuration macro (#8613) This commit reworks the YAML documentation samples to use the new `[configuration]` macro which generates a list of samples in multiple config languages instead. It also fixes a couple of issues which were discovered as part of doing so. Co-authored-by: Graeme Rocher --- settings.gradle | 2 +- src/main/docs/guide/aop/caching.adoc | 5 ++-- src/main/docs/guide/aop/scheduling.adoc | 5 ++-- src/main/docs/guide/appendix/breaks.adoc | 2 +- .../netflixRibbon.adoc | 6 ++--- ...ributedConfigurationAwsParameterStore.adoc | 2 +- .../distributedConfigurationConsul.adoc | 14 +++++----- .../distributedConfigurationSpringCloud.adoc | 4 +-- .../distributedConfigurationVault.adoc | 4 +-- .../serviceDiscoveryManual.adoc | 5 ++-- .../serviceDiscoveryRoute53.adoc | 26 +++++++++---------- src/main/docs/guide/config/bootstrap.adoc | 2 +- .../guide/config/configurationProperties.adoc | 24 ++--------------- .../docs/guide/config/propertySource.adoc | 17 +++++------- .../docs/guide/config/valueAnnotation.adoc | 2 +- .../dataAccess/mongoSupport.adoc | 6 ++--- .../dataAccess/neo4jSupport.adoc | 4 +-- .../dataAccess/redisSupport.adoc | 4 +-- .../otherConfigurations/rabbitmq.adoc | 2 +- .../clientAnnotationStreaming.adoc | 4 +-- .../clientAnnotation/clientHeaders.adoc | 4 +-- .../clientAnnotation/clientJackson.adoc | 4 +-- .../clientAnnotation/netflixHystrix.adoc | 4 +-- .../docs/guide/httpClient/clientHttp2.adoc | 2 +- .../lowLevelHttpClient/clientBasics.adoc | 5 ++-- .../clientConfiguration.adoc | 16 ++++++------ .../docs/guide/httpServer/apiVersioning.adoc | 12 ++++----- .../guide/httpServer/consumesAnnotation.adoc | 2 +- src/main/docs/guide/httpServer/formData.adoc | 2 +- .../docs/guide/httpServer/http2Server.adoc | 4 +-- .../docs/guide/httpServer/jsonBinding.adoc | 8 +++--- .../docs/guide/httpServer/reactiveServer.adoc | 2 +- .../reactiveServer/bodyAnnotation.adoc | 2 +- .../guide/httpServer/serverConfiguration.adoc | 6 ++--- .../serverConfiguration/accessLogger.adoc | 6 ++--- .../httpServer/serverConfiguration/cors.adoc | 20 +++++++------- .../serverConfiguration/dualProtocol.adoc | 8 +++--- .../httpServer/serverConfiguration/https.adoc | 8 +++--- .../serverConfiguration/threadPools.adoc | 2 +- .../threadPools/blockingOperations.adoc | 2 +- src/main/docs/guide/httpServer/sessions.adoc | 12 ++++----- .../httpServer/websocket/websocketServer.adoc | 4 +-- src/main/docs/guide/ioc/contextEvents.adoc | 4 +-- .../guide/logging/loggingConfiguration.adoc | 6 ++--- .../endpointConfiguration.adoc | 2 +- .../guide/management/providedEndpoints.adoc | 2 +- .../providedEndpoints/beansEndpoint.adoc | 2 +- .../environmentEndpoint.adoc | 2 +- .../providedEndpoints/healthEndpoint.adoc | 10 +++---- .../providedEndpoints/infoEndpoint.adoc | 2 +- .../providedEndpoints/loggersEndpoint.adoc | 2 +- .../providedEndpoints/refreshEndpoint.adoc | 2 +- .../providedEndpoints/routesEndpoint.adoc | 2 +- .../providedEndpoints/stopEndpoint.adoc | 2 +- .../providedEndpoints/threadDumpEndpoint.adoc | 2 +- src/main/docs/guide/quickStart.adoc | 1 - 56 files changed, 143 insertions(+), 173 deletions(-) diff --git a/settings.gradle b/settings.gradle index 8fe88096383..b2c70db8e22 100644 --- a/settings.gradle +++ b/settings.gradle @@ -14,7 +14,7 @@ pluginManagement { } plugins { - id 'io.micronaut.build.shared.settings' version '6.1.1' + id 'io.micronaut.build.shared.settings' version '6.2.0' } enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") diff --git a/src/main/docs/guide/aop/caching.adoc b/src/main/docs/guide/aop/caching.adoc index 2c43f932cd5..bf27ba3a9ef 100644 --- a/src/main/docs/guide/aop/caching.adoc +++ b/src/main/docs/guide/aop/caching.adoc @@ -20,10 +20,9 @@ In addition, if the underlying Cache implementation supports non-blocking cache == Configuring Caches -By default, https://github.com/ben-manes/caffeine[Caffeine] is used to create caches from application configuration. For example with `application.yml`: +By default, https://github.com/ben-manes/caffeine[Caffeine] is used to create caches from application configuration. For example: -.Cache Configuration Example -[source,yaml] +[configuration,title="Cache Configuration Example"] ---- micronaut: caches: diff --git a/src/main/docs/guide/aop/scheduling.adoc b/src/main/docs/guide/aop/scheduling.adoc index b8a124f51fe..84fdeb0ed64 100644 --- a/src/main/docs/guide/aop/scheduling.adoc +++ b/src/main/docs/guide/aop/scheduling.adoc @@ -55,11 +55,10 @@ The above example allows the task execution frequency to be configured with the Tasks executed by `@Scheduled` are run by default on a jdk:java.util.concurrent.ScheduledExecutorService[] configured to have twice the number of threads as available processors. -You can configure this thread pool using `application.yml`, for example: +You can configure this thread pool in your configuration file (e.g `application.yml`): //TODO: Move YAML snippet to ExecutorServiceConfigSpec -.Configuring Scheduled Task Thread Pool -[source,yaml] +[configuration,title="Configuring Scheduled Task Thread Pool"] ---- micronaut: executors: diff --git a/src/main/docs/guide/appendix/breaks.adoc b/src/main/docs/guide/appendix/breaks.adoc index 2c98942b7c6..4ea541ec9e6 100644 --- a/src/main/docs/guide/appendix/breaks.adoc +++ b/src/main/docs/guide/appendix/breaks.adoc @@ -114,7 +114,7 @@ To register a type converter into `ConversionService.SHARED`, the registration n - The <> is now disabled by default. To enable it, you must update your endpoint config: -[source,yaml] +[configuration] ---- endpoints: env: diff --git a/src/main/docs/guide/cloud/clientSideLoadBalancing/netflixRibbon.adoc b/src/main/docs/guide/cloud/clientSideLoadBalancing/netflixRibbon.adoc index 513a79eead1..5a5ca986de1 100644 --- a/src/main/docs/guide/cloud/clientSideLoadBalancing/netflixRibbon.adoc +++ b/src/main/docs/guide/cloud/clientSideLoadBalancing/netflixRibbon.adoc @@ -17,10 +17,10 @@ dependency:io.micronaut.netflix:micronaut-netflix-ribbon[] The api:http.client.LoadBalancer[] implementations will now be link:{micronautribbonapi}/io/micronaut/configuration/ribbon/RibbonLoadBalancer.html[RibbonLoadBalancer] instances. -Ribbon's https://netflix.github.io/ribbon/ribbon-core-javadoc/com/netflix/client/config/CommonClientConfigKey.html[Configuration options] can be set using the `ribbon` namespace in configuration. For example in `application.yml`: +Ribbon's https://netflix.github.io/ribbon/ribbon-core-javadoc/com/netflix/client/config/CommonClientConfigKey.html[Configuration options] can be set using the `ribbon` namespace in configuration. For example in your configuration file (e.g `application.yml`): .Configuring Ribbon -[source,yaml] +[configuration] ---- ribbon: VipAddress: test @@ -30,7 +30,7 @@ ribbon: Each discovered client can also be configured under `ribbon.clients`. For example given a `@Client(id = "hello-world")` you can configure Ribbon settings with: .Per Client Ribbon Settings -[source,yaml] +[configuration] ---- ribbon: clients: diff --git a/src/main/docs/guide/cloud/cloudConfiguration/distributedConfigurationAwsParameterStore.adoc b/src/main/docs/guide/cloud/cloudConfiguration/distributedConfigurationAwsParameterStore.adoc index e7f99d548df..2c07d7c5a92 100644 --- a/src/main/docs/guide/cloud/cloudConfiguration/distributedConfigurationAwsParameterStore.adoc +++ b/src/main/docs/guide/cloud/cloudConfiguration/distributedConfigurationAwsParameterStore.adoc @@ -5,7 +5,7 @@ dependency:io.micronaut.aws:micronaut-aws-parameter-store[] To enable distributed configuration, make sure https://docs.micronaut.io/latest/guide/#bootstrap[bootstrap] is enabled and create a `src/main/resources/bootstrap.yml` file with the following configuration: .bootstrap.yml -[source,yaml] +[configuration] ---- micronaut: application: diff --git a/src/main/docs/guide/cloud/cloudConfiguration/distributedConfigurationConsul.adoc b/src/main/docs/guide/cloud/cloudConfiguration/distributedConfigurationConsul.adoc index 45e90cb47cc..eaa2fec477a 100644 --- a/src/main/docs/guide/cloud/cloudConfiguration/distributedConfigurationConsul.adoc +++ b/src/main/docs/guide/cloud/cloudConfiguration/distributedConfigurationConsul.adoc @@ -23,10 +23,10 @@ $ mn create-app my-app --features config-consul ---- ==== -To enable distributed configuration, similar to Spring Boot and Grails, make sure https://docs.micronaut.io/latest/guide/#bootstrap[bootstrap] is enabled and create a `src/main/resources/bootstrap.yml` file with the following configuration: +To enable distributed configuration make sure <> is enabled and create a `src/main/resources/bootstrap.[yml/toml/properties]` file with the following configuration: -.bootstrap.yml -[source,yaml] +.bootstrap configuration +[configuration] ---- micronaut: application: @@ -66,7 +66,7 @@ Within the `/config` directory Micronaut searches values within the following di |=== -The value of `APPLICATION_NAME` is whatever your have configured `micronaut.application.name` to be in `bootstrap.yml`. +The value of `APPLICATION_NAME` is whatever your have configured `micronaut.application.name` to be in your `bootstrap` configuration file. To see this in action, use the following cURL command to store a property called `foo.bar` with a value of `myvalue` in the directory `/config/application`. @@ -88,8 +88,7 @@ You can set the `consul.client.config.format` option to configure the format wit For example, to configure JSON: -.application.yml -[source,yaml] +[configuration] ---- consul: client: @@ -116,8 +115,7 @@ You can setup a Git repository that contains files like `application.yml`, `hell In this case, each key in Consul represents a file with an extension, for example `/config/application.yml`, and you must configure the `FILE` format: -.application.yml -[source,yaml] +[configuration] ---- consul: client: diff --git a/src/main/docs/guide/cloud/cloudConfiguration/distributedConfigurationSpringCloud.adoc b/src/main/docs/guide/cloud/cloudConfiguration/distributedConfigurationSpringCloud.adoc index b55f0ee3168..ac469b78382 100644 --- a/src/main/docs/guide/cloud/cloudConfiguration/distributedConfigurationSpringCloud.adoc +++ b/src/main/docs/guide/cloud/cloudConfiguration/distributedConfigurationSpringCloud.adoc @@ -1,9 +1,9 @@ Since 1.1, Micronaut features a native https://spring.io/projects/spring-cloud-config[Spring Cloud Configuration] for those who have not switched to a dedicated more complete solution like Consul. -To enable support for Spring Cloud Configuration, make sure https://docs.micronaut.io/latest/guide/#bootstrap[bootstrap] is enabled and create a `src/main/resources/bootstrap.yml` file with the following configuration: +To enable distributed configuration make sure <> is enabled and create a `src/main/resources/bootstrap.[yml/toml/properties]` file with the following configuration: .Integrating with Spring Cloud Configuration -[source,yaml] +[configuration] ---- micronaut: application: diff --git a/src/main/docs/guide/cloud/cloudConfiguration/distributedConfigurationVault.adoc b/src/main/docs/guide/cloud/cloudConfiguration/distributedConfigurationVault.adoc index cfd98750950..b4d8f215164 100644 --- a/src/main/docs/guide/cloud/cloudConfiguration/distributedConfigurationVault.adoc +++ b/src/main/docs/guide/cloud/cloudConfiguration/distributedConfigurationVault.adoc @@ -1,9 +1,9 @@ Micronaut integrates with https://www.vaultproject.io/[HashiCorp Vault] as a distributed configuration source. -To enable support for Vault Configuration, make sure https://docs.micronaut.io/latest/guide/#bootstrap[bootstrap] is enabled and create a `src/main/resources/bootstrap.yml` file with the following configuration: +To enable distributed configuration make sure <> is enabled and create a `src/main/resources/bootstrap.[yml/toml/properties]` file with the following configuration: .Integrating with HashiCorp Vault -[source,yaml] +[configuration] ---- micronaut: application: diff --git a/src/main/docs/guide/cloud/serviceDiscovery/serviceDiscoveryManual.adoc b/src/main/docs/guide/cloud/serviceDiscovery/serviceDiscoveryManual.adoc index cc0644de104..8b8ef5e260a 100644 --- a/src/main/docs/guide/cloud/serviceDiscovery/serviceDiscoveryManual.adoc +++ b/src/main/docs/guide/cloud/serviceDiscovery/serviceDiscoveryManual.adoc @@ -3,7 +3,7 @@ If you do not wish to involve a service discovery server like Consul or you inte To do this, use the `micronaut.http.services` setting. For example: .Manually configuring services -[source,yaml] +[configuration] ---- micronaut: http: @@ -23,13 +23,12 @@ TIP: You can override this configuration in production by specifying an environm Note that by default no health checking will happen to assert that the referenced services are operational. You can alter that by enabling health checking and optionally specifying a health check path (the default is `/health`): .Enabling Health Checking -[source,yaml] +[configuration] ---- micronaut: http: services: foo: - ... health-check: true # <1> health-check-interval: 15s # <2> health-check-uri: /health # <3> diff --git a/src/main/docs/guide/cloud/serviceDiscovery/serviceDiscoveryRoute53.adoc b/src/main/docs/guide/cloud/serviceDiscovery/serviceDiscoveryRoute53.adoc index 6a013e8c93c..0abbf27b53c 100644 --- a/src/main/docs/guide/cloud/serviceDiscovery/serviceDiscoveryRoute53.adoc +++ b/src/main/docs/guide/cloud/serviceDiscovery/serviceDiscoveryRoute53.adoc @@ -14,14 +14,14 @@ Here are the steps: 4. Add Service ID to your application configuration file like so: .Sample application.yml -[source,yaml] +[configuration] ---- aws: route53: - registration - enabled: true - aws-service-id: srv-978fs98fsdf - namespace: micronaut.io + registration: + enabled: true + aws-service-id: srv-978fs98fsdf + namespace: micronaut.io micronaut: application: name: something @@ -34,7 +34,7 @@ dependency:io.micronaut.aws:micronaut-aws-route53[] 6. On the client side, you need the same dependencies and fewer configuration options: .Sample application.yml -[source,yaml] +[configuration] ---- aws: route53: @@ -120,28 +120,28 @@ You will get a service ID and an ARN back from this command if successful. Write Add the configuration to make your applications register with Route 53 Auto-discovery: .Registration Properties -[source,yaml] +[configuration] ---- aws: route53: registration: enabled: true - aws-service-id= + aws-service-id: discovery: - namespace-id= + namespace-id: ---- ==== Discovery Client Configuration .Discovery Properties -[source,yaml] +[configuration] ---- aws: route53: discovery: - client - enabled: true - aws-service-id: + client: + enabled: true + aws-service-id: ---- You can also call the following methods by getting the bean "Route53AutoNamingClient": diff --git a/src/main/docs/guide/config/bootstrap.adoc b/src/main/docs/guide/config/bootstrap.adoc index d3bc91af1b3..8dad586817a 100644 --- a/src/main/docs/guide/config/bootstrap.adoc +++ b/src/main/docs/guide/config/bootstrap.adoc @@ -1,4 +1,4 @@ -Most application configuration is stored in `application.yml`, environment-specific files like `application-{environment}.{extension}`, environment and system properties, etc. +Most application configuration is stored in your configuration file (e.g `application.yml`), environment-specific files like `application-{environment}.{extension}`, environment and system properties, etc. These configure the application context. But during application startup, before the application context is created, a "bootstrap" context can be created to store configuration necessary to retrieve additional configuration for the main context. Typically that additional configuration is in some remote source. diff --git a/src/main/docs/guide/config/configurationProperties.adoc b/src/main/docs/guide/config/configurationProperties.adoc index c64031788c6..ebb31f7b329 100644 --- a/src/main/docs/guide/config/configurationProperties.adoc +++ b/src/main/docs/guide/config/configurationProperties.adoc @@ -118,8 +118,7 @@ Durations can be specified by appending the unit with a number. Supported units For example to configure the default HTTP client read timeout: -.Using Duration Values -[source,yaml] +[configuration,title="Using Duration Values"] ---- micronaut: http: @@ -131,8 +130,7 @@ micronaut: Lists and arrays can be specified in Java properties files as comma-separated values, or in YAML using native YAML lists. The generic types are used to convert the values. For example in YAML: -.Specifying lists or arrays in YAML -[source,yaml] +[configuration,title="Specifying lists or arrays in YAML"] ---- my: app: @@ -144,24 +142,6 @@ my: - http://bar.com ---- -Or in Java properties file format: - -.Specifying lists or arrays in Java properties comma-separated -[source,properties] ----- -my.app.integers=1,2 -my.app.urls=http://foo.com,http://bar.com ----- - -Alternatively you can use an index: - -.Specifying lists or arrays in Java properties using index -[source,properties] ----- -my.app.integers[0]=1 -my.app.integers[1]=2 ----- - For the above example configurations you can define properties to bind to with the target type supplied via generics: [source,java] diff --git a/src/main/docs/guide/config/propertySource.adoc b/src/main/docs/guide/config/propertySource.adoc index 91fbba9de2d..51e479c724d 100644 --- a/src/main/docs/guide/config/propertySource.adoc +++ b/src/main/docs/guide/config/propertySource.adoc @@ -62,7 +62,7 @@ It is important to note that it is not recommended to store sensitive configurat It is good practise to instead externalize sensitive configuration completely from the application code using preferably a external secret manager system (there are many options here, many provided by Cloud providers) or environment variables that are set during the deployment of the application. You can also use property placeholders (see the following section), to customize names of the environment variables to use and supply default values: .Using Property Value Placeholders to Define Secure Configuration -[source,java] +[configuration] ---- datasources: default: @@ -87,10 +87,9 @@ As mentioned in the previous section, Micronaut includes a property placeholder TIP: Programmatic usage is also possible via the api:io.micronaut.context.env.PropertyPlaceholderResolver[] interface. -The basic syntax is to wrap a reference to a property in `${...}`. For example in `application.yml`: +The basic syntax is to wrap a reference to a property in `${...}`. For example: -.Defining Property Placeholders -[source,yaml] +[configuration,title="Defining Property Placeholders"] ---- myapp: endpoint: http://${micronaut.server.host}:${micronaut.server.port}/foo @@ -100,8 +99,7 @@ The above example embeds references to the `micronaut.server.host` and `micronau You can specify default values by defining a value after the `:` character. For example: -.Using Default Values -[source,yaml] +[configuration,title="Using Default Values"] ---- myapp: endpoint: http://${micronaut.server.host:localhost}:${micronaut.server.port:8080}/foo @@ -110,7 +108,7 @@ myapp: The above example defaults to `localhost` and port `8080` if no value is found (rather than throwing an exception). Note that if the default value contains a `:` character, you must escape it using backticks: .Using Backticks -[source,yaml] +[configuration] ---- myapp: endpoint: ${server.address:`http://localhost:8080`}/foo @@ -152,7 +150,7 @@ NOTE: The configuration above does not have any impact on property placeholders. You can use `random` values by using the following properties. These can be used in configuration files as variables like the following. -[source,yaml] +[configuration] ---- micronaut: application: @@ -195,7 +193,7 @@ The `random.int`, `random.integer`, `random.long` and `random.float` properties - `(max)` where max is an exclusive value - `[min,max]` where min being inclusive and max being exclusive values. -[source,yaml] +[configuration] ---- instance: id: ${random.int[5,10]} @@ -209,4 +207,3 @@ NOTE: The range could vary from negative to positive as well. For beans that inject required properties, the injection and potential failure will not occur until the bean is requested. To verify at startup that the properties exist and can be injected, the bean can be annotated with ann:io.micronaut.context.annotation.Context[]. Context-scoped beans are injected at startup, and startup fails if any required properties are missing or cannot be converted to the required type. IMPORTANT: It is recommended to use this feature sparingly to ensure fast startup. - diff --git a/src/main/docs/guide/config/valueAnnotation.adoc b/src/main/docs/guide/config/valueAnnotation.adoc index cbb657506e9..3a5c892767a 100644 --- a/src/main/docs/guide/config/valueAnnotation.adoc +++ b/src/main/docs/guide/config/valueAnnotation.adoc @@ -92,7 +92,7 @@ The above instead injects the value of the `my.url` property resolved from appli You can also use this feature to resolve sub maps. For example, consider the following configuration: .Example `application.yml` configuration -[source,yaml] +[configuration] ---- datasources: default: diff --git a/src/main/docs/guide/configurations/dataAccess/mongoSupport.adoc b/src/main/docs/guide/configurations/dataAccess/mongoSupport.adoc index 76e8664a5d0..00e9e6500ad 100644 --- a/src/main/docs/guide/configurations/dataAccess/mongoSupport.adoc +++ b/src/main/docs/guide/configurations/dataAccess/mongoSupport.adoc @@ -13,10 +13,10 @@ Micronaut can automatically configure the native MongoDB Java driver. To use thi dependency:micronaut-mongo-reactive[groupId="io.micronaut.mongodb"] -Then configure the URI of the MongoDB server in `application.yml`: +Then configure the URI of the MongoDB server in your configuration file (e.g `application.yml`): .Configuring a MongoDB server -[source,yaml] +[configuration] ---- mongodb: uri: mongodb://username:password@localhost:27017/databaseName @@ -30,7 +30,7 @@ To use the blocking driver, add a dependency to your build on the mongo-java-dri [source,groovy] ---- -compile "org.mongodb:mongo-java-driver" +runtimeOnly "org.mongodb:mongo-java-driver" ---- Then the blocking https://mongodb.github.io/mongo-java-driver/3.7/javadoc/com/mongodb/MongoClient.html[MongoClient] will be available for injection. diff --git a/src/main/docs/guide/configurations/dataAccess/neo4jSupport.adoc b/src/main/docs/guide/configurations/dataAccess/neo4jSupport.adoc index 539886ec565..e1bc1e3ce7f 100644 --- a/src/main/docs/guide/configurations/dataAccess/neo4jSupport.adoc +++ b/src/main/docs/guide/configurations/dataAccess/neo4jSupport.adoc @@ -13,10 +13,10 @@ To configure the Neo4j Bolt driver, first add the `neo4j-bolt` module to your bu dependency::micronaut-neo4j-bolt[groupId="io.micronaut.neo4j"] -Then configure the URI of the Neo4j server in `application.yml`: +Then configure the URI of the Neo4j server in your configuration file (e.g `application.yml`): .Configuring `neo4j.uri` -[source,yaml] +[configuration] ---- neo4j: uri: bolt://localhost diff --git a/src/main/docs/guide/configurations/dataAccess/redisSupport.adoc b/src/main/docs/guide/configurations/dataAccess/redisSupport.adoc index 56adbc45c68..1e2f9cd92a0 100644 --- a/src/main/docs/guide/configurations/dataAccess/redisSupport.adoc +++ b/src/main/docs/guide/configurations/dataAccess/redisSupport.adoc @@ -18,10 +18,10 @@ To configure the Lettuce driver, first add the `redis-lettuce` module to your bu compile "io.micronaut.redis:micronaut-redis-lettuce" ---- -Then configure the URI of the Redis server in `application.yml`: +Then configure the URI of the Redis server in your configuration file (e.g `application.yml`): .Configuring `redis.uri` -[source,yaml] +[configuration] ---- redis: uri: redis://localhost diff --git a/src/main/docs/guide/configurations/otherConfigurations/rabbitmq.adoc b/src/main/docs/guide/configurations/otherConfigurations/rabbitmq.adoc index 0f23f88dfc4..cad4d9423ab 100644 --- a/src/main/docs/guide/configurations/otherConfigurations/rabbitmq.adoc +++ b/src/main/docs/guide/configurations/otherConfigurations/rabbitmq.adoc @@ -15,7 +15,7 @@ A RabbitMQ connection factory bean will be provided based on the configuration v For example: -[source,yaml] +[configuration] ---- rabbitmq: uri: amqp://user:pass@host:10000/vhost diff --git a/src/main/docs/guide/httpClient/clientAnnotation/clientAnnotationStreaming.adoc b/src/main/docs/guide/httpClient/clientAnnotation/clientAnnotationStreaming.adoc index b6a9141b7e7..865ebe9a8f1 100644 --- a/src/main/docs/guide/httpClient/clientAnnotation/clientAnnotationStreaming.adoc +++ b/src/main/docs/guide/httpClient/clientAnnotation/clientAnnotationStreaming.adoc @@ -49,10 +49,10 @@ When streaming responses from servers, the underlying HTTP client will not apply Instead, the `read-idle-timeout` setting (which defaults to 5 minutes) dictates when to close a connection after it becomes idle. -If you stream data from a server that defines a longer delay than 5 minutes between items, you should adjust `readIdleTimeout`. The following configuration in `application.yml` demonstrates how: +If you stream data from a server that defines a longer delay than 5 minutes between items, you should adjust `readIdleTimeout`. The following configuration in your configuration file (e.g `application.yml`) demonstrates how: .Adjusting the readIdleTimeout -[source,yaml] +[configuration] ---- micronaut: http: diff --git a/src/main/docs/guide/httpClient/clientAnnotation/clientHeaders.adoc b/src/main/docs/guide/httpClient/clientAnnotation/clientHeaders.adoc index 080b7bd0c6c..fe22c573b4b 100644 --- a/src/main/docs/guide/httpClient/clientAnnotation/clientHeaders.adoc +++ b/src/main/docs/guide/httpClient/clientAnnotation/clientHeaders.adoc @@ -10,10 +10,10 @@ snippet::io.micronaut.docs.annotation.headers.PetClient[tags="class", indent=0, The above example defines a ann:http.annotation.Header[] annotation on the `PetClient` interface that reads the `pet.client.id` property using property placeholder configuration. -Then set the following in `application.yml` to populate the value: +Then set the following in your configuration file (e.g `application.yml`) to populate the value: .Configuring Headers in YAML -[source,yaml] +[configuration] ---- pet: client: diff --git a/src/main/docs/guide/httpClient/clientAnnotation/clientJackson.adoc b/src/main/docs/guide/httpClient/clientAnnotation/clientJackson.adoc index 0d1d40c884a..a7a9d85fb19 100644 --- a/src/main/docs/guide/httpClient/clientAnnotation/clientJackson.adoc +++ b/src/main/docs/guide/httpClient/clientAnnotation/clientJackson.adoc @@ -1,11 +1,11 @@ As mentioned previously, Jackson is used for message encoding to JSON. A default Jackson `ObjectMapper` is configured and used by Micronaut HTTP clients. -You can override the settings used to construct the `ObjectMapper` with properties defined by the api:jackson.JacksonConfiguration[] class in `application.yml`. +You can override the settings used to construct the `ObjectMapper` with properties defined by the api:jackson.JacksonConfiguration[] class in your configuration file (e.g `application.yml`). For example, the following configuration enables indented output for Jackson: .Example Jackson Configuration -[source,yaml] +[configuration] ---- jackson: serialization: diff --git a/src/main/docs/guide/httpClient/clientAnnotation/netflixHystrix.adoc b/src/main/docs/guide/httpClient/clientAnnotation/netflixHystrix.adoc index 3f39b851eda..ebe3589a278 100644 --- a/src/main/docs/guide/httpClient/clientAnnotation/netflixHystrix.adoc +++ b/src/main/docs/guide/httpClient/clientAnnotation/netflixHystrix.adoc @@ -34,10 +34,10 @@ TIP: For information on how to customize the Hystrix thread pool, group, and pro == Enabling Hystrix Stream and Dashboard -You can enable a Server Sent Event stream to feed into the https://github.com/Netflix-Skunkworks/hystrix-dashboard[Hystrix Dashboard] by setting the `hystrix.stream.enabled` setting to `true` in `application.yml`: +You can enable a Server Sent Event stream to feed into the https://github.com/Netflix-Skunkworks/hystrix-dashboard[Hystrix Dashboard] by setting the `hystrix.stream.enabled` setting to `true` in your configuration file (e.g `application.yml`): .Enabling Hystrix Stream -[source,yaml] +[configuration] ---- hystrix: stream: diff --git a/src/main/docs/guide/httpClient/clientHttp2.adoc b/src/main/docs/guide/httpClient/clientHttp2.adoc index d4e6b6f4504..df9f23e70e9 100644 --- a/src/main/docs/guide/httpClient/clientHttp2.adoc +++ b/src/main/docs/guide/httpClient/clientHttp2.adoc @@ -1,7 +1,7 @@ By default, Micronaut's HTTP client is configured to support HTTP 1.1. To enable support for HTTP/2, set the supported HTTP version in configuration: .Enabling HTTP/2 in Clients -[source,yaml] +[configuration] ---- micronaut: http: diff --git a/src/main/docs/guide/httpClient/lowLevelHttpClient/clientBasics.adoc b/src/main/docs/guide/httpClient/lowLevelHttpClient/clientBasics.adoc index af0085eea2e..2d7f9333594 100644 --- a/src/main/docs/guide/httpClient/lowLevelHttpClient/clientBasics.adoc +++ b/src/main/docs/guide/httpClient/lowLevelHttpClient/clientBasics.adoc @@ -60,10 +60,9 @@ To debug requests being sent and received from the HTTP client you can enable tr ==== Client Specific Debugging / Tracing -To enable client-specific logging you can configure the default logger for all HTTP clients. You can also configure different loggers for different clients using <<_client_specific_configuration, Client-Specific Configuration>>. For example, in `application.yml`: +To enable client-specific logging you can configure the default logger for all HTTP clients. You can also configure different loggers for different clients using <<_client_specific_configuration, Client-Specific Configuration>>. For example, in your configuration file (e.g `application.yml`): -.application.yml -[source,xml] +[configuration] ---- micronaut: http: diff --git a/src/main/docs/guide/httpClient/lowLevelHttpClient/clientConfiguration.adoc b/src/main/docs/guide/httpClient/lowLevelHttpClient/clientConfiguration.adoc index aedaeffbe25..1f93d67f502 100644 --- a/src/main/docs/guide/httpClient/lowLevelHttpClient/clientConfiguration.adoc +++ b/src/main/docs/guide/httpClient/lowLevelHttpClient/clientConfiguration.adoc @@ -1,9 +1,9 @@ === Global Configuration for All Clients -The default HTTP client configuration is a <> named api:http.client.DefaultHttpClientConfiguration[] that allows configuring the default behaviour for all HTTP clients. For example, in `application.yml`: +The default HTTP client configuration is a <> named api:http.client.DefaultHttpClientConfiguration[] that allows configuring the default behaviour for all HTTP clients. For example, in your configuration file (e.g `application.yml`): .Altering default HTTP client configuration -[source,yaml] +[configuration] ---- micronaut: http: @@ -15,10 +15,10 @@ The above example sets the `readTimeout` property of the api:http.client.HttpCli === Client Specific Configuration -To have separate configuration per-client, there are a couple of options. You can configure <> manually in `application.yml` and apply per-client configuration: +To have separate configuration per-client, there are a couple of options. You can configure <> manually in your configuration file (e.g `application.yml`) and apply per-client configuration: .Manually configuring HTTP services -[source,yaml] +[configuration] ---- micronaut: http: @@ -88,7 +88,7 @@ Each HTTP/1.1 connection can only support one request at a time, but can be reus To remove the overhead of opening a new connection for each request, the Micronaut HTTP Client will reuse HTTP connections wherever possible. They are managed in a _connection pool_. HTTP/1.1 connections are kept around using keep-alive and are used for new requests, and for HTTP/2, a single connection is used for all requests. .Manually configuring HTTP services -[source,yaml] +[configuration] ---- micronaut: http: @@ -114,7 +114,7 @@ By default, Micronaut shares a common Netty `EventLoopGroup` for worker threads This `EventLoopGroup` can be configured via the `micronaut.netty.event-loops.default` property: .Configuring The Default Event Loop -[source,yaml] +[configuration] ---- micronaut: netty: @@ -133,7 +133,7 @@ For example, if your interactions with an HTTP client involve CPU-intensive work The following example configures an additional event loop group called "other" with 10 threads: .Configuring Additional Event Loops -[source,yaml] +[configuration] ---- micronaut: netty: @@ -146,7 +146,7 @@ micronaut: Once an additional event loop has been configured you can alter the HTTP client configuration to use it: .Altering the Event Loop Group used by Clients -[source,yaml] +[configuration] ---- micronaut: http: diff --git a/src/main/docs/guide/httpServer/apiVersioning.adoc b/src/main/docs/guide/httpServer/apiVersioning.adoc index b9e9f6ad46f..966fecb30ae 100644 --- a/src/main/docs/guide/httpServer/apiVersioning.adoc +++ b/src/main/docs/guide/httpServer/apiVersioning.adoc @@ -7,10 +7,10 @@ snippet::io.micronaut.docs.web.router.version.VersionedController[tags="imports, <1> The `helloV1` method is declared as version `1` <2> The `helloV2` method is declared as version `2` -Then enable versioning by setting `micronaut.router.versioning.enabled` to `true` in `application.yml`: +Then enable versioning by setting `micronaut.router.versioning.enabled` to `true` in your configuration file (e.g `application.yml`): .Enabling Versioning -[source,yaml] +[configuration] ---- micronaut: router: @@ -21,7 +21,7 @@ micronaut: By default Micronaut has two strategies for resolving the version based on an HTTP header named `X-API-VERSION` or a request parameter named `api-version`, however this is configurable. A full configuration example can be seen below: .Configuring Versioning -[source,yaml] +[configuration] ---- micronaut: router: @@ -50,7 +50,7 @@ If this is not enough you can also implement the api:web.router.version.resoluti It is possible to supply a default version through configuration. .Configuring Default Version -[source,yaml] +[configuration] ---- micronaut: router: @@ -90,7 +90,7 @@ include::{includedir}configurationProperties/io.micronaut.http.client.intercepto For example to use `Accept-Version` as the header name: .Configuring Client Versioning -[source,yaml] +[configuration] ---- micronaut: http: @@ -105,7 +105,7 @@ micronaut: The `default` key refers to the default configuration. You can specify client-specific configuration by using the value passed to `@Client` (typically the service ID). For example: .Configuring Versioning -[source,yaml] +[configuration] ---- micronaut: http: diff --git a/src/main/docs/guide/httpServer/consumesAnnotation.adoc b/src/main/docs/guide/httpServer/consumesAnnotation.adoc index 34fbb971ff1..46e067a92ae 100644 --- a/src/main/docs/guide/httpServer/consumesAnnotation.adoc +++ b/src/main/docs/guide/httpServer/consumesAnnotation.adoc @@ -10,7 +10,7 @@ snippet::io.micronaut.docs.server.consumes.ConsumesController[tags="imports,claz Normally JSON parsing only happens if the content type is `application/json`. The other api:io.micronaut.http.codec.MediaTypeCodec[] classes behave similarly in that they have predefined content types they can process. To extend the list of media types that a given codec processes, provide configuration that will be stored in api:io.micronaut.http.codec.CodecConfiguration[]: -[source,yaml] +[configuration] ---- micronaut: codec: diff --git a/src/main/docs/guide/httpServer/formData.adoc b/src/main/docs/guide/httpServer/formData.adoc index 6b41738cadd..a67e6616d1b 100644 --- a/src/main/docs/guide/httpServer/formData.adoc +++ b/src/main/docs/guide/httpServer/formData.adoc @@ -6,7 +6,7 @@ In practice this means that to bind regular form data, the only change required snippet::io.micronaut.docs.server.json.PersonController[tags="class,regular,endclass", indent=0, title="Binding Form Data to POJOs"] -TIP: To avoid denial of service attacks, collection types and arrays created during binding are limited by the setting `jackson.arraySizeThreshold` in `application.yml` +TIP: To avoid denial of service attacks, collection types and arrays created during binding are limited by the setting `jackson.arraySizeThreshold` in your configuration file (e.g `application.yml`) Alternatively, instead of using a POJO you can bind form data directly to method parameters (which works with JSON too!): diff --git a/src/main/docs/guide/httpServer/http2Server.adoc b/src/main/docs/guide/httpServer/http2Server.adoc index e0fedcb5fb2..106baae965e 100644 --- a/src/main/docs/guide/httpServer/http2Server.adoc +++ b/src/main/docs/guide/httpServer/http2Server.adoc @@ -5,7 +5,7 @@ Since Micronaut 2.x, Micronaut's Netty-based HTTP server can be configured to su The first step is to set the supported HTTP version in the server configuration: .Enabling HTTP/2 Support -[source,yaml] +[configuration] ---- micronaut: server: @@ -17,7 +17,7 @@ With this configuration, Micronaut enables support for the `h2c` protocol (see h Since browsers don't support `h2c` and in general https://httpwg.org/specs/rfc7540.html#discover-https[HTTP/2 over TLS] (the `h2` protocol), it is recommended for production that you enable <>. For development this can be done with: .Enabling `h2` Protocol Support -[source,yaml] +[configuration] ---- micronaut: server: diff --git a/src/main/docs/guide/httpServer/jsonBinding.adoc b/src/main/docs/guide/httpServer/jsonBinding.adoc index d41d3cfc32e..609b368e45b 100644 --- a/src/main/docs/guide/httpServer/jsonBinding.adoc +++ b/src/main/docs/guide/httpServer/jsonBinding.adoc @@ -60,7 +60,7 @@ All Jackson configuration keys start with `jackson`. Example: -[source,yaml] +[configuration] ---- jackson: serializationInclusion: ALWAYS @@ -81,7 +81,7 @@ All features can be configured with their name as the key and a boolean to indic Example: -[source,yaml] +[configuration] ---- jackson: serialization: @@ -99,7 +99,7 @@ This can be achieved by providing your own `JsonFactory` bean, or by providing a === Support for `@JsonView` -You can use the `@JsonView` annotation on controller methods if you set `jackson.json-view.enabled` to `true` in `application.yml`. +You can use the `@JsonView` annotation on controller methods if you set `jackson.json-view.enabled` to `true` in your configuration file (e.g `application.yml`). Jackson's `@JsonView` annotation lets you control which properties are exposed on a per-response basis. See https://www.baeldung.com/jackson-json-view-annotation[Jackson JSON Views] for more information. @@ -124,7 +124,7 @@ During JSON parsing, the framework may convert any incoming data to an intermedi If you need full accuracy for number types, use the following configuration: -[source,yaml] +[configuration] ---- jackson: deserialization: diff --git a/src/main/docs/guide/httpServer/reactiveServer.adoc b/src/main/docs/guide/httpServer/reactiveServer.adoc index 9f59021be4a..a25df2ad1aa 100644 --- a/src/main/docs/guide/httpServer/reactiveServer.adoc +++ b/src/main/docs/guide/httpServer/reactiveServer.adoc @@ -5,7 +5,7 @@ This makes it critical that if you do any blocking I/O operations (for example i For example the following configuration configures the I/O thread pool as a fixed thread pool with 75 threads (similar to what a traditional blocking server such as Tomcat uses in the thread-per-request model): .Configuring the IO thread pool -[source,yaml] +[configuration] ---- micronaut: executors: diff --git a/src/main/docs/guide/httpServer/reactiveServer/bodyAnnotation.adoc b/src/main/docs/guide/httpServer/reactiveServer/bodyAnnotation.adoc index b4ba9baa50b..86cbac74193 100644 --- a/src/main/docs/guide/httpServer/reactiveServer/bodyAnnotation.adoc +++ b/src/main/docs/guide/httpServer/reactiveServer/bodyAnnotation.adoc @@ -10,7 +10,7 @@ snippet::io.micronaut.docs.server.body.MessageController[tags="imports,class,ech Note that reading the request body is done in a non-blocking manner in that the request contents are read as the data becomes available and accumulated into the String passed to the method. -TIP: The `micronaut.server.maxRequestSize` setting in `application.yml` limits the size of the data (the default maximum request size is 10MB) read/buffered by the server. `@Size` is *not* a replacement for this setting. +TIP: The `micronaut.server.maxRequestSize` setting in your configuration file (e.g `application.yml`) limits the size of the data (the default maximum request size is 10MB) read/buffered by the server. `@Size` is *not* a replacement for this setting. Regardless of the limit, for a large amount of data accumulating the data into a String in-memory may lead to memory strain on the server. A better approach is to include a Reactive library in your project (such as `Reactor`, `RxJava`,or `Akka`) that supports the Reactive streams implementation and stream the data it becomes available: diff --git a/src/main/docs/guide/httpServer/serverConfiguration.adoc b/src/main/docs/guide/httpServer/serverConfiguration.adoc index fdc460b8e0e..1a9fc0404ee 100644 --- a/src/main/docs/guide/httpServer/serverConfiguration.adoc +++ b/src/main/docs/guide/httpServer/serverConfiguration.adoc @@ -1,9 +1,9 @@ The HTTP server features a number of configuration options. They are defined in the api:http.server.netty.configuration.NettyHttpServerConfiguration[] configuration class, which extends api:http.server.HttpServerConfiguration[]. -The following example shows how to tweak configuration options for the server via `application.yml`: +The following example shows how to tweak configuration options for the server via your configuration file (e.g `application.yml`): .Configuring HTTP server settings -[source,yaml] +[configuration] ---- micronaut: server: @@ -48,7 +48,7 @@ dependency:netty-transport-native-epoll[groupId="io.netty",scope="runtimeOnly",c Then configure the default event loop group to prefer native transports: .Configuring The Default Event Loop to Prefer Native Transports -[source,yaml] +[configuration] ---- micronaut: netty: diff --git a/src/main/docs/guide/httpServer/serverConfiguration/accessLogger.adoc b/src/main/docs/guide/httpServer/serverConfiguration/accessLogger.adoc index 12e5d4d4423..9e6f8fec89f 100644 --- a/src/main/docs/guide/httpServer/serverConfiguration/accessLogger.adoc +++ b/src/main/docs/guide/httpServer/serverConfiguration/accessLogger.adoc @@ -1,9 +1,9 @@ In the spirit of https://httpd.apache.org/docs/current/mod/mod_log_config.html[apache mod_log_config] and https://tomcat.apache.org/tomcat-10.0-doc/config/valve.html#Access_Logging[Tomcat Access Log Valve], it is possible to enable an access logger for the HTTP server (this works for both HTTP/1 and HTTP/2). -To enable and configure the access logger, in `application.yml` set: +To enable and configure the access logger, in your configuration file (e.g `application.yml`) set: .Enabling the access logger -[source,yaml] +[configuration] ---- micronaut: server: @@ -19,7 +19,7 @@ micronaut: If you wish to not log access to certain paths, you can specify regular expression filters in the configuration: .Filtering the access logs -[source,yaml] +[configuration] ---- micronaut: server: diff --git a/src/main/docs/guide/httpServer/serverConfiguration/cors.adoc b/src/main/docs/guide/httpServer/serverConfiguration/cors.adoc index d43eba6a2e7..f479c16ae7a 100644 --- a/src/main/docs/guide/httpServer/serverConfiguration/cors.adoc +++ b/src/main/docs/guide/httpServer/serverConfiguration/cors.adoc @@ -1,7 +1,7 @@ -Micronaut supports CORS (link:https://www.w3.org/TR/cors/[Cross Origin Resource Sharing]) out of the box. By default, CORS requests are rejected. To enable processing of CORS requests, modify your configuration. For example with `application.yml`: +Micronaut supports CORS (link:https://www.w3.org/TR/cors/[Cross Origin Resource Sharing]) out of the box. By default, CORS requests are rejected. To enable processing of CORS requests, modify your configuration. For example: .CORS Configuration Example -[source,yaml] +[configuration] ---- micronaut: server: @@ -14,7 +14,7 @@ By only enabling CORS processing, a "wide open" strategy is adopted that allows To change the settings for all origins or a specific origin, change the configuration to provide one or more "configurations". By providing any configuration, the default "wide open" configuration is not configured. .CORS Configurations -[source,yaml] +[configuration] ---- micronaut: server: @@ -44,7 +44,7 @@ For multiple valid origins, set the `allowedOrigins` key of the configuration to Regular expressions are passed to link:{javase}java/util/regex/Pattern.html#compile-java.lang.String-[Pattern#compile] and compared to the request origin with link:{javase}java/util/regex/Matcher.html#matches--[Matcher#matches]. .Example CORS Configuration -[source,yaml] +[configuration] ---- micronaut: server: @@ -64,7 +64,7 @@ To allow any request method for a given configuration, don't include the `allowe For multiple allowed methods, set the `allowedMethods` key of the configuration to a list of strings. .Example CORS Configuration -[source,yaml] +[configuration] ---- micronaut: server: @@ -84,7 +84,7 @@ To allow any request header for a given configuration, don't include the `allowe For multiple allowed headers, set the `allowedHeaders` key of the configuration to a list of strings. .Example CORS Configuration -[source,yaml] +[configuration] ---- micronaut: server: @@ -102,7 +102,7 @@ micronaut: To configure the headers that are sent in the response to a CORS request through the `Access-Control-Expose-Headers` header, include a list of strings for the `exposedHeaders` key in your configuration. None are exposed by default. .Example CORS Configuration -[source,yaml] +[configuration] ---- micronaut: server: @@ -120,7 +120,7 @@ micronaut: Credentials are allowed by default for CORS requests. To disallow credentials, set the `allowCredentials` option to `false`. .Example CORS Configuration -[source,yaml] +[configuration] ---- micronaut: server: @@ -136,7 +136,7 @@ micronaut: The default maximum age that preflight requests can be cached is 30 minutes. To change that behavior, specify a value in seconds. .Example CORS Configuration -[source,yaml] +[configuration] ---- micronaut: server: @@ -151,7 +151,7 @@ micronaut: By default, when a header has multiple values, multiple headers are sent, each with a single value. It is possible to change the behavior to send a single header with a comma-separated list of values by setting a configuration option. -[source,yaml] +[configuration] ---- micronaut: server: diff --git a/src/main/docs/guide/httpServer/serverConfiguration/dualProtocol.adoc b/src/main/docs/guide/httpServer/serverConfiguration/dualProtocol.adoc index 65f916a0131..4c71faf3336 100644 --- a/src/main/docs/guide/httpServer/serverConfiguration/dualProtocol.adoc +++ b/src/main/docs/guide/httpServer/serverConfiguration/dualProtocol.adoc @@ -1,7 +1,7 @@ -Micronaut supports binding both HTTP and HTTPS. To enable dual protocol support, modify your configuration. For example with `application.yml`: +Micronaut supports binding both HTTP and HTTPS. To enable dual protocol support, modify your configuration. For example: .Dual Protocol Configuration Example -[source,yaml] +[configuration] ---- micronaut: server: @@ -14,10 +14,10 @@ micronaut: <2> Enabling both HTTP and HTTPS is an opt-in feature - setting the `dualProtocol` flag enables it. By default Micronaut only enables one -It is also possible to redirect automatically all HTTP request to HTTPS. Besides the previous configuration, you need to enable this option. For example, with `application.yml`: +It is also possible to redirect automatically all HTTP request to HTTPS. Besides the previous configuration, you need to enable this option. For example: .Enable HTTP to HTTPS Redirects -[source,yaml] +[configuration] ---- micronaut: server: diff --git a/src/main/docs/guide/httpServer/serverConfiguration/https.adoc b/src/main/docs/guide/httpServer/serverConfiguration/https.adoc index 766ab4460ae..4a2a438a7a4 100644 --- a/src/main/docs/guide/httpServer/serverConfiguration/https.adoc +++ b/src/main/docs/guide/httpServer/serverConfiguration/https.adoc @@ -1,7 +1,7 @@ -Micronaut supports HTTPS out of the box. By default HTTPS is disabled and all requests are served using HTTP. To enable HTTPS support, modify your configuration. For example with `application.yml`: +Micronaut supports HTTPS out of the box. By default HTTPS is disabled and all requests are served using HTTP. To enable HTTPS support, modify your configuration. For example: .HTTPS Configuration Example -[source,yaml] +[configuration] ---- micronaut: server: @@ -40,7 +40,7 @@ During the creation of the `server.p12` file it is necessary to define a passwor Now modify your configuration: .HTTPS Configuration Example -[source,yaml] +[configuration] ---- micronaut: ssl: @@ -95,7 +95,7 @@ WARNING: If either `srcstorepass` or `alias` are not the same as defined in the Now modify your configuration: .HTTPS Configuration Example -[source,yaml] +[configuration] ---- micronaut: ssl: diff --git a/src/main/docs/guide/httpServer/serverConfiguration/threadPools.adoc b/src/main/docs/guide/httpServer/serverConfiguration/threadPools.adoc index 09a7e43c057..7b2d3a11e32 100644 --- a/src/main/docs/guide/httpServer/serverConfiguration/threadPools.adoc +++ b/src/main/docs/guide/httpServer/serverConfiguration/threadPools.adoc @@ -11,7 +11,7 @@ TIP: The parent event loop can be configured with `micronaut.server.netty.parent The server can also be configured to use a different named worker event loop: .Using a different event loop for the server -[source,yaml] +[configuration] ---- micronaut: server: diff --git a/src/main/docs/guide/httpServer/serverConfiguration/threadPools/blockingOperations.adoc b/src/main/docs/guide/httpServer/serverConfiguration/threadPools/blockingOperations.adoc index 2dc5f1849b7..5f6d601243f 100644 --- a/src/main/docs/guide/httpServer/serverConfiguration/threadPools/blockingOperations.adoc +++ b/src/main/docs/guide/httpServer/serverConfiguration/threadPools/blockingOperations.adoc @@ -1,7 +1,7 @@ When dealing with blocking operations, Micronaut shifts the blocking operations to an unbound, caching I/O thread pool by default. You can configure the I/O thread pool using the api:scheduling.executor.ExecutorConfiguration[] named `io`. For example: .Configuring the Server I/O Thread Pool -[source,yaml] +[configuration] ---- micronaut: executors: diff --git a/src/main/docs/guide/httpServer/sessions.adoc b/src/main/docs/guide/httpServer/sessions.adoc index 4021a4834b3..fbcb0822415 100644 --- a/src/main/docs/guide/httpServer/sessions.adoc +++ b/src/main/docs/guide/httpServer/sessions.adoc @@ -20,14 +20,14 @@ To quickly get up and running with Redis sessions you must also have the `redis- .build.gradle [source,groovy] ---- -compile "io.micronaut-session:micronaut-session" -compile "io.micronaut.redis:micronaut-redis-lettuce" +implementation "io.micronaut-session:micronaut-session" +implementation "io.micronaut.redis:micronaut-redis-lettuce" ---- -And enable Redis sessions via configuration in `application.yml`: +And enable Redis sessions via configuration in your configuration file (e.g `application.yml`): .Enabling Redis Sessions -[source,yaml] +[configuration] ---- redis: uri: redis://localhost:6379 @@ -44,10 +44,10 @@ link:{micronautsessionapi}/io/micronaut/session/Session.html[Session] resolution By default, sessions are resolved using link:{micronautsessionapi}/io/micronaut/session/http/HttpSessionFilter.html[HttpSessionFilter] that looks for session identifiers via either an HTTP header (using the `Authorization-Info` or `X-Auth-Token` headers) or via a Cookie named `SESSION`. -You can disable either header resolution or cookie resolution via configuration in `application.yml`: +You can disable either header resolution or cookie resolution via configuration in your configuration file (e.g `application.yml`): .Disabling Cookie Resolution -[source,yaml] +[configuration] ---- micronaut: session: diff --git a/src/main/docs/guide/httpServer/websocket/websocketServer.adoc b/src/main/docs/guide/httpServer/websocket/websocketServer.adoc index 0e0dc5f56e2..b2adb2a7a13 100644 --- a/src/main/docs/guide/httpServer/websocket/websocketServer.adoc +++ b/src/main/docs/guide/httpServer/websocket/websocketServer.adoc @@ -71,7 +71,7 @@ $ mn create-websocket-server MyChat By default, Micronaut times out idle connections with no activity after five minutes. Normally this is not a problem as browsers automatically reconnect WebSocket sessions, however you can control this behaviour by setting the `micronaut.server.idle-timeout` setting (a negative value results in no timeout): .Setting the Connection Timeout for the Server -[source,yaml] +[configuration] ---- micronaut: server: @@ -81,7 +81,7 @@ micronaut: If you use Micronaut's WebSocket client you may also wish to set the timeout on the client: .Setting the Connection Timeout for the Client -[source,yaml] +[configuration] ---- micronaut: http: diff --git a/src/main/docs/guide/ioc/contextEvents.adoc b/src/main/docs/guide/ioc/contextEvents.adoc index c737cd4d7c3..067389c5d81 100644 --- a/src/main/docs/guide/ioc/contextEvents.adoc +++ b/src/main/docs/guide/ioc/contextEvents.adoc @@ -26,11 +26,11 @@ If your listener performs work that might take a while, use the ann:scheduling.a snippet::io.micronaut.docs.context.events.async.SampleEventListener,io.micronaut.docs.context.events.async.SampleEventListenerSpec[tags="imports,class",indent=0,title="Asynchronously listening for Events with `@EventListener`"] -The event listener by default runs on the `scheduled` executor. You can configure this thread pool as required in `application.yml`: +The event listener by default runs on the `scheduled` executor. You can configure this thread pool as required in your configuration file (e.g `application.yml`): //TODO: Move YAML snippet to ExecutorServiceConfigSpec .Configuring Scheduled Task Thread Pool -[source,yaml] +[configuration] ---- micronaut: executors: diff --git a/src/main/docs/guide/logging/loggingConfiguration.adoc b/src/main/docs/guide/logging/loggingConfiguration.adoc index ef08b83cc8f..c624348225c 100644 --- a/src/main/docs/guide/logging/loggingConfiguration.adoc +++ b/src/main/docs/guide/logging/loggingConfiguration.adoc @@ -1,6 +1,6 @@ -Log levels can be configured via properties defined in `application.yml` (and environment variables) with the `logger.levels` prefix: +Log levels can be configured via properties defined in your configuration file (e.g `application.yml`) (and environment variables) with the `logger.levels` prefix: -[source,yaml] +[configuration] ---- logger: levels: @@ -23,7 +23,7 @@ You can also set a custom Logback XML configuration file to be used via `logger. To disable a logger, you need to set the logger level to `OFF`: -[source,yaml] +[configuration] ---- logger: levels: diff --git a/src/main/docs/guide/management/buildingEndpoints/endpointConfiguration.adoc b/src/main/docs/guide/management/buildingEndpoints/endpointConfiguration.adoc index 6fd0a82084b..e4d2d860596 100644 --- a/src/main/docs/guide/management/buildingEndpoints/endpointConfiguration.adoc +++ b/src/main/docs/guide/management/buildingEndpoints/endpointConfiguration.adoc @@ -17,7 +17,7 @@ The configuration values for the endpoint override those for `all`. If `endpoint For all endpoints, the following configuration values can be set. -[source,yaml] +[configuration] ---- endpoints: : diff --git a/src/main/docs/guide/management/providedEndpoints.adoc b/src/main/docs/guide/management/providedEndpoints.adoc index eed2765aa51..8ffc1661c23 100644 --- a/src/main/docs/guide/management/providedEndpoints.adoc +++ b/src/main/docs/guide/management/providedEndpoints.adoc @@ -65,7 +65,7 @@ this should be used with care because private and sensitive information will be By default, all management endpoints are exposed over the same port as the application. You can alter this behaviour by specifying the `endpoints.all.port` setting: -[source,yaml] +[configuration] ---- endpoints: all: diff --git a/src/main/docs/guide/management/providedEndpoints/beansEndpoint.adoc b/src/main/docs/guide/management/providedEndpoints/beansEndpoint.adoc index f940661534c..8a44c698939 100644 --- a/src/main/docs/guide/management/providedEndpoints/beansEndpoint.adoc +++ b/src/main/docs/guide/management/providedEndpoints/beansEndpoint.adoc @@ -7,7 +7,7 @@ To execute the beans endpoint, send a GET request to /beans. To configure the beans endpoint, supply configuration through `endpoints.beans`. .Beans Endpoint Configuration Example -[source,yaml] +[configuration] ---- endpoints: beans: diff --git a/src/main/docs/guide/management/providedEndpoints/environmentEndpoint.adoc b/src/main/docs/guide/management/providedEndpoints/environmentEndpoint.adoc index 58f388d6377..00db56fadc0 100644 --- a/src/main/docs/guide/management/providedEndpoints/environmentEndpoint.adoc +++ b/src/main/docs/guide/management/providedEndpoints/environmentEndpoint.adoc @@ -5,7 +5,7 @@ The environment endpoint returns information about the api:context.env.Environme To enable and configure the environment endpoint, supply configuration through `endpoints.env`. .Environment Endpoint Configuration Example -[source,yaml] +[configuration] ---- endpoints: env: diff --git a/src/main/docs/guide/management/providedEndpoints/healthEndpoint.adoc b/src/main/docs/guide/management/providedEndpoints/healthEndpoint.adoc index 4c11d947dcb..50b3ae4e64f 100644 --- a/src/main/docs/guide/management/providedEndpoints/healthEndpoint.adoc +++ b/src/main/docs/guide/management/providedEndpoints/healthEndpoint.adoc @@ -7,7 +7,7 @@ To execute the health endpoint, send a GET request to /health. Additionally the To configure the health endpoint, supply configuration through `endpoints.health`. .Health Endpoint Configuration Example -[source,yaml] +[configuration] ---- endpoints: health: @@ -25,7 +25,7 @@ The `details-visible` setting controls whether health detail will be exposed to For example, setting: .Using `details-visible` -[source,yaml] +[configuration] ---- endpoints: health: @@ -50,10 +50,10 @@ The `endpoints.health.status.http-mapping` setting controls which status codes t |=== -You can provide custom mappings in `application.yml`: +You can provide custom mappings in your configuration file (e.g `application.yml`): .Custom Health Status Codes -[source,yaml] +[configuration] ---- endpoints: health: @@ -90,7 +90,7 @@ All Micronaut provided health indicators are exposed on /health and /health/read A health indicator is provided that determines the health of the application based on the amount of free disk space. Configuration for the disk space health indicator can be provided under the `endpoints.health.disk-space` key. .Disk Space Indicator Configuration Example -[source,yaml] +[configuration] ---- endpoints: health: diff --git a/src/main/docs/guide/management/providedEndpoints/infoEndpoint.adoc b/src/main/docs/guide/management/providedEndpoints/infoEndpoint.adoc index 918718de1ed..4c7f14f6de7 100644 --- a/src/main/docs/guide/management/providedEndpoints/infoEndpoint.adoc +++ b/src/main/docs/guide/management/providedEndpoints/infoEndpoint.adoc @@ -7,7 +7,7 @@ To execute the info endpoint, send a GET request to /info. To configure the info endpoint, supply configuration through `endpoints.info`. .Info Endpoint Configuration Example -[source,yaml] +[configuration] ---- endpoints: info: diff --git a/src/main/docs/guide/management/providedEndpoints/loggersEndpoint.adoc b/src/main/docs/guide/management/providedEndpoints/loggersEndpoint.adoc index cf11a6fae67..c4062c4796a 100644 --- a/src/main/docs/guide/management/providedEndpoints/loggersEndpoint.adoc +++ b/src/main/docs/guide/management/providedEndpoints/loggersEndpoint.adoc @@ -68,7 +68,7 @@ $ curl http://localhost:8080/loggers/ROOT To configure the loggers endpoint, supply configuration through `endpoints.loggers`. .Loggers Endpoint Configuration Example -[source,yaml] +[configuration] ---- endpoints: loggers: diff --git a/src/main/docs/guide/management/providedEndpoints/refreshEndpoint.adoc b/src/main/docs/guide/management/providedEndpoints/refreshEndpoint.adoc index d182f31734b..4f8b1a661d0 100644 --- a/src/main/docs/guide/management/providedEndpoints/refreshEndpoint.adoc +++ b/src/main/docs/guide/management/providedEndpoints/refreshEndpoint.adoc @@ -19,7 +19,7 @@ $ curl -X POST http://localhost:8080/refresh -H 'Content-Type: application/json' To configure the refresh endpoint, supply configuration through `endpoints.refresh`. .Beans Endpoint Configuration Example -[source,yaml] +[configuration] ---- endpoints: refresh: diff --git a/src/main/docs/guide/management/providedEndpoints/routesEndpoint.adoc b/src/main/docs/guide/management/providedEndpoints/routesEndpoint.adoc index b0f2dfb9128..e4295b4956a 100644 --- a/src/main/docs/guide/management/providedEndpoints/routesEndpoint.adoc +++ b/src/main/docs/guide/management/providedEndpoints/routesEndpoint.adoc @@ -7,7 +7,7 @@ To execute the routes endpoint, send a GET request to /routes. To configure the routes endpoint, supply configuration through `endpoints.routes`. .Routes Endpoint Configuration Example -[source,yaml] +[configuration] ---- endpoints: routes: diff --git a/src/main/docs/guide/management/providedEndpoints/stopEndpoint.adoc b/src/main/docs/guide/management/providedEndpoints/stopEndpoint.adoc index 4a761fcba2b..c61d9efbd3d 100644 --- a/src/main/docs/guide/management/providedEndpoints/stopEndpoint.adoc +++ b/src/main/docs/guide/management/providedEndpoints/stopEndpoint.adoc @@ -7,7 +7,7 @@ To execute the stop endpoint, send a POST request to /stop. To configure the stop endpoint, supply configuration through `endpoints.stop`. .Stop Endpoint Configuration Example -[source,yaml] +[configuration] ---- endpoints: stop: diff --git a/src/main/docs/guide/management/providedEndpoints/threadDumpEndpoint.adoc b/src/main/docs/guide/management/providedEndpoints/threadDumpEndpoint.adoc index e14e9f77924..fa1804aaeae 100644 --- a/src/main/docs/guide/management/providedEndpoints/threadDumpEndpoint.adoc +++ b/src/main/docs/guide/management/providedEndpoints/threadDumpEndpoint.adoc @@ -7,7 +7,7 @@ To execute the threaddump endpoint, send a GET request to /threaddump. To configure the threaddump endpoint, supply configuration through `endpoints.threaddump`. .Threaddump Endpoint Configuration Example -[source,yaml] +[configuration] ---- endpoints: threaddump: diff --git a/src/main/docs/guide/quickStart.adoc b/src/main/docs/guide/quickStart.adoc index b06dd196775..69d75e83423 100644 --- a/src/main/docs/guide/quickStart.adoc +++ b/src/main/docs/guide/quickStart.adoc @@ -1,4 +1,3 @@ The following sections walk you through a Quick Start on how to use Micronaut to setup a basic "Hello World" application. Before getting started ensure you have a Java 8 or higher JDK installed, and it is recommended that you use a <> such as IntelliJ IDEA. - From 0732980ebbde83b38ebf77ec8724f65eba244435 Mon Sep 17 00:00:00 2001 From: micronaut-build <65172877+micronaut-build@users.noreply.github.com> Date: Thu, 19 Jan 2023 12:45:29 +0100 Subject: [PATCH 404/743] Bump micronaut-security to 3.9.1 (#8628) --- gradle.properties | 2 +- gradle/libs.versions.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle.properties b/gradle.properties index bfe03719198..25fae817e33 100644 --- a/gradle.properties +++ b/gradle.properties @@ -52,7 +52,7 @@ kotlin.stdlib.default.dependency=false # For the docs graalVersion=22.0.0.2 -micronautSecurityVersion=3.9.0 +micronautSecurityVersion=3.9.1 org.gradle.caching=true org.gradle.parallel=true diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 7d537b525c9..9ee2332f062 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -112,7 +112,7 @@ managed-micronaut-rss = "3.2.0" managed-micronaut-rxjava1 = "1.0.0" managed-micronaut-rxjava2 = "1.3.0" managed-micronaut-rxjava3 = "2.4.0" -managed-micronaut-security = "3.9.0" +managed-micronaut-security = "3.9.1" managed-micronaut-serialization = "1.5.0" managed-micronaut-servlet = "3.3.3" managed-micronaut-spring = "4.4.0" From 033f0084433202c0d017bb36bd4f597d69a313aa Mon Sep 17 00:00:00 2001 From: micronaut-build <65172877+micronaut-build@users.noreply.github.com> Date: Thu, 19 Jan 2023 12:45:43 +0100 Subject: [PATCH 405/743] Bump micronaut-views to 3.8.1 (#8629) --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 9ee2332f062..128ee7cf634 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -122,7 +122,7 @@ managed-micronaut-test-resources = "1.2.3" managed-micronaut-toml = "1.1.3" managed-micronaut-tracing = "4.4.0" managed-micronaut-tracing-legacy = "3.2.7" -managed-micronaut-views = "3.8.0" +managed-micronaut-views = "3.8.1" managed-micronaut-xml = "3.2.0" managed-neo4j = "3.5.35" managed-neo4j-java-driver = "4.4.9" From ee93232127fc9f3efd21227f7a8580fafcc46b6a Mon Sep 17 00:00:00 2001 From: micronaut-build <65172877+micronaut-build@users.noreply.github.com> Date: Thu, 19 Jan 2023 12:45:55 +0100 Subject: [PATCH 406/743] Bump micronaut-spring to 4.5.0 (#8615) --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index f81c5ac87c5..5d18cf7a289 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -115,7 +115,7 @@ managed-micronaut-rxjava3 = "2.4.0" managed-micronaut-security = "3.9.0" managed-micronaut-serialization = "1.5.0" managed-micronaut-servlet = "3.3.3" -managed-micronaut-spring = "4.4.0" +managed-micronaut-spring = "4.5.0" managed-micronaut-sql = "4.7.2" managed-micronaut-test = "3.8.0" managed-micronaut-test-resources = "1.2.3" From 3cac50ae10d6ab84a64e6840fa53ea128bd833c8 Mon Sep 17 00:00:00 2001 From: Denis Stepanov Date: Thu, 19 Jan 2023 14:36:33 +0100 Subject: [PATCH 407/743] Fix NPE in isCandidateBean (#8631) Fixes #8627 --- .../proxybeanwithpredestroy/package-info.java | 2 + .../package-info.java | 2 + .../package-info.java | 2 + .../micronaut/context/DefaultBeanContext.java | 41 +++++++++++++------ 4 files changed, 34 insertions(+), 13 deletions(-) diff --git a/inject-java/src/test/groovy/io/micronaut/inject/lifecycle/proxybeanwithpredestroy/package-info.java b/inject-java/src/test/groovy/io/micronaut/inject/lifecycle/proxybeanwithpredestroy/package-info.java index 9240d01f269..855b819586b 100644 --- a/inject-java/src/test/groovy/io/micronaut/inject/lifecycle/proxybeanwithpredestroy/package-info.java +++ b/inject-java/src/test/groovy/io/micronaut/inject/lifecycle/proxybeanwithpredestroy/package-info.java @@ -1,5 +1,7 @@ +@Configuration @Requires(property = "spec", value = "ProxyBeanWithPreDestroySpec") package io.micronaut.inject.lifecycle.proxybeanwithpredestroy; +import io.micronaut.context.annotation.Configuration; import io.micronaut.context.annotation.Requires; diff --git a/inject-java/src/test/groovy/io/micronaut/inject/lifecycle/proxytargetbeanprototypewithpredestroy/package-info.java b/inject-java/src/test/groovy/io/micronaut/inject/lifecycle/proxytargetbeanprototypewithpredestroy/package-info.java index fe528f0666c..284c3e1d0d3 100644 --- a/inject-java/src/test/groovy/io/micronaut/inject/lifecycle/proxytargetbeanprototypewithpredestroy/package-info.java +++ b/inject-java/src/test/groovy/io/micronaut/inject/lifecycle/proxytargetbeanprototypewithpredestroy/package-info.java @@ -1,5 +1,7 @@ +@Configuration @Requires(property = "spec", value = "ProxyLazyCachedTargetPrototypeBeanWithPreDestroySpec") package io.micronaut.inject.lifecycle.proxytargetbeanprototypewithpredestroy; +import io.micronaut.context.annotation.Configuration; import io.micronaut.context.annotation.Requires; diff --git a/inject-java/src/test/groovy/io/micronaut/inject/lifecycle/proxytargetbeanwithpredestroy/package-info.java b/inject-java/src/test/groovy/io/micronaut/inject/lifecycle/proxytargetbeanwithpredestroy/package-info.java index aecee4075b0..380277dc81a 100644 --- a/inject-java/src/test/groovy/io/micronaut/inject/lifecycle/proxytargetbeanwithpredestroy/package-info.java +++ b/inject-java/src/test/groovy/io/micronaut/inject/lifecycle/proxytargetbeanwithpredestroy/package-info.java @@ -1,5 +1,7 @@ +@Configuration @Requires(property = "spec", value = "ProxyTargetBeanWithPreDestroySpec") package io.micronaut.inject.lifecycle.proxytargetbeanwithpredestroy; +import io.micronaut.context.annotation.Configuration; import io.micronaut.context.annotation.Requires; diff --git a/inject/src/main/java/io/micronaut/context/DefaultBeanContext.java b/inject/src/main/java/io/micronaut/context/DefaultBeanContext.java index 6ffa70bbb21..9553828ae2e 100644 --- a/inject/src/main/java/io/micronaut/context/DefaultBeanContext.java +++ b/inject/src/main/java/io/micronaut/context/DefaultBeanContext.java @@ -4117,11 +4117,13 @@ public boolean isReferenceEnabled(DefaultBeanContext context) { } public boolean isReferenceEnabled(DefaultBeanContext context, @Nullable BeanResolutionContext resolutionContext) { - if (reference == null) { + BeanDefinitionReference ref = reference; + // The reference needs to be assigned to a new variable as it can change between checks + if (ref == null) { return false; } if (referenceEnabled == null) { - if (reference.isEnabled(context, resolutionContext)) { + if (ref.isEnabled(context, resolutionContext)) { referenceEnabled = true; } else { referenceEnabled = false; @@ -4135,10 +4137,14 @@ public boolean isDisabled() { if (reference == null) { return true; } - if (referenceEnabled != null && !referenceEnabled) { + Boolean refEnabled = referenceEnabled; + // The reference needs to be assigned to a new variable as it can change between checks + if (refEnabled != null && !refEnabled) { return true; } - return definitionEnabled != null && !definitionEnabled; + Boolean defEnabled = definitionEnabled; + // The reference needs to be assigned to a new variable as it can change between checks + return defEnabled != null && !defEnabled; } public boolean isDefinitionEnabled(DefaultBeanContext defaultBeanContext) { @@ -4148,12 +4154,12 @@ public boolean isDefinitionEnabled(DefaultBeanContext defaultBeanContext) { public boolean isDefinitionEnabled(DefaultBeanContext context, @Nullable BeanResolutionContext resolutionContext) { if (definitionEnabled == null) { if (isReferenceEnabled(context, resolutionContext)) { - definition = getDefinition(context); - if (definition.isEnabled(context, resolutionContext)) { + BeanDefinition def = getDefinition(context); + if (def.isEnabled(context, resolutionContext)) { + definition = def; definitionEnabled = true; } else { definitionEnabled = false; - definition = null; } } else { definitionEnabled = false; @@ -4163,31 +4169,40 @@ public boolean isDefinitionEnabled(DefaultBeanContext context, @Nullable BeanRes } public BeanDefinitionReference getReference() { - if (reference == null || referenceEnabled == null || !referenceEnabled) { + // The reference needs to be assigned to a new variable as it can change between checks + Boolean refEnabled = referenceEnabled; + if (reference == null || refEnabled == null || !refEnabled) { throw new IllegalStateException("The reference is not enabled"); } return reference; } public BeanDefinition getDefinition(BeanContext beanContext) { - if (definitionEnabled != null && !definitionEnabled) { + // The reference needs to be assigned to a new variable as it can change between checks + Boolean defEnabled = definitionEnabled; + if (defEnabled != null && !defEnabled) { throw new IllegalStateException("The definition is not enabled"); } try { - if (definition == null) { - definition = getReference().load(beanContext); + BeanDefinition def = definition; + if (def == null) { + def = getReference().load(beanContext); + definition = def; } - return definition; + return def; } catch (Throwable e) { throw new BeanInstantiationException("Bean definition [" + reference.getName() + "] could not be loaded: " + e.getMessage(), e); } } public boolean isReferenceCandidateBean(Argument beanType) { - return reference != null && reference.isCandidateBean(beanType); + // The reference needs to be assigned to a new variable as it can change between checks + BeanDefinitionReference ref = reference; + return ref != null && ref.isCandidateBean(beanType); } public void disable(BeanDefinitionReference reference) { + // The reference needs to be assigned to a new variable as it can change between checks BeanDefinitionReference ref = this.reference; if (ref != null && ref.equals(reference)) { this.reference = null; From 6dfd2fe19e4a89232437103709e6e6f8b4802dd5 Mon Sep 17 00:00:00 2001 From: Auke Schrijnen Date: Thu, 19 Jan 2023 19:54:28 +0100 Subject: [PATCH 408/743] fix: handle inner classes in bean import (#8566) (#8567) --- .../inject/beanimport/BeanImportSpec.groovy | 5 ++++ .../visitor/JavaVisitorContext.java | 26 ++++++++++++------- .../visitor/JavaVisitorContextSpec.groovy | 24 +++++++++++++++++ 3 files changed, 46 insertions(+), 9 deletions(-) diff --git a/inject-java-test/src/test/groovy/io/micronaut/inject/beanimport/BeanImportSpec.groovy b/inject-java-test/src/test/groovy/io/micronaut/inject/beanimport/BeanImportSpec.groovy index 540d425094a..c30f07e89dd 100644 --- a/inject-java-test/src/test/groovy/io/micronaut/inject/beanimport/BeanImportSpec.groovy +++ b/inject-java-test/src/test/groovy/io/micronaut/inject/beanimport/BeanImportSpec.groovy @@ -2,6 +2,7 @@ package io.micronaut.inject.beanimport import io.micronaut.annotation.processing.test.AbstractTypeElementSpec import io.micronaut.context.ApplicationContext +import io.smallrye.faulttolerance.CdiFaultToleranceSpi import io.smallrye.faulttolerance.CircuitBreakerMaintenanceImpl import io.smallrye.faulttolerance.DefaultAsyncExecutorProvider import io.smallrye.faulttolerance.DefaultExistingCircuitBreakerNames @@ -23,7 +24,9 @@ class Application {} expect: context.containsBean(DefaultAsyncExecutorProvider) context.containsBean(CircuitBreakerMaintenanceImpl) + context.containsBean(CdiFaultToleranceSpi.EagerDependencies) context.containsBean(DefaultExistingCircuitBreakerNames) + context.containsBean(CdiFaultToleranceSpi.EagerDependencies) context.containsBean(DefaultFallbackHandlerProvider) context.getBeanDefinition(DefaultFallbackHandlerProvider) .injectedFields.size() == 1 @@ -50,6 +53,7 @@ import io.smallrye.faulttolerance.*; @io.micronaut.context.annotation.Import(classes={ DefaultAsyncExecutorProvider.class, CircuitBreakerMaintenanceImpl.class, + CdiFaultToleranceSpi.EagerDependencies.class, DefaultExistingCircuitBreakerNames.class, DefaultFallbackHandlerProvider.class, DefaultFaultToleranceOperationProvider.class, @@ -61,6 +65,7 @@ class Application {} expect: context.containsBean(DefaultAsyncExecutorProvider) context.containsBean(CircuitBreakerMaintenanceImpl) + context.containsBean(CdiFaultToleranceSpi.EagerDependencies) context.containsBean(DefaultExistingCircuitBreakerNames) context.containsBean(DefaultFallbackHandlerProvider) context.getBeanDefinition(DefaultFallbackHandlerProvider) diff --git a/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaVisitorContext.java b/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaVisitorContext.java index 33f1251316e..d67526570af 100644 --- a/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaVisitorContext.java +++ b/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaVisitorContext.java @@ -375,18 +375,26 @@ private void populateClassElements(@NonNull String[] stereotypes, PackageElement final List enclosedElements = packageElement.getEnclosedElements(); boolean includeAll = Arrays.equals(stereotypes, new String[] { "*" }); for (Element enclosedElement : enclosedElements) { - if (enclosedElement instanceof TypeElement) { - final AnnotationMetadata annotationMetadata = annotationUtils.getAnnotationMetadata(enclosedElement); - if (includeAll || Arrays.stream(stereotypes).anyMatch(annotationMetadata::hasStereotype)) { - JavaClassElement classElement = elementFactory.newClassElement((TypeElement) enclosedElement, annotationMetadata); + populateClassElements(stereotypes, includeAll, packageElement, enclosedElement, classElements); + } + } - if (!classElement.isAbstract()) { - classElements.add(classElement); - } + private void populateClassElements(@NonNull String[] stereotypes, boolean includeAll, PackageElement packageElement, Element enclosedElement, List classElements) { + if (enclosedElement instanceof TypeElement) { + final AnnotationMetadata annotationMetadata = annotationUtils.getAnnotationMetadata(enclosedElement); + if (includeAll || Arrays.stream(stereotypes).anyMatch(annotationMetadata::hasStereotype)) { + JavaClassElement classElement = elementFactory.newClassElement((TypeElement) enclosedElement, annotationMetadata); + + if (!classElement.isAbstract()) { + classElements.add(classElement); } - } else if (enclosedElement instanceof PackageElement) { - populateClassElements(stereotypes, (PackageElement) enclosedElement, classElements); } + List nestedElements = enclosedElement.getEnclosedElements(); + for (Element nestedElement : nestedElements) { + populateClassElements(stereotypes, includeAll, packageElement, nestedElement, classElements); + } + } else if (enclosedElement instanceof PackageElement) { + populateClassElements(stereotypes, (PackageElement) enclosedElement, classElements); } } diff --git a/inject-java/src/test/groovy/io/micronaut/annotation/processing/visitor/JavaVisitorContextSpec.groovy b/inject-java/src/test/groovy/io/micronaut/annotation/processing/visitor/JavaVisitorContextSpec.groovy index f24110d6b88..9aa52ea31f0 100644 --- a/inject-java/src/test/groovy/io/micronaut/annotation/processing/visitor/JavaVisitorContextSpec.groovy +++ b/inject-java/src/test/groovy/io/micronaut/annotation/processing/visitor/JavaVisitorContextSpec.groovy @@ -56,6 +56,30 @@ enum Foo {} lookedUp[0].name == 'example.Foo' } + def 'return inner class from getClassElements'() { + given: + ClassElement[] lookedUp + def typeVisitor = new TypeElementVisitor() { + @Override + void visitClass(ClassElement element, VisitorContext context) { + lookedUp = context.getClassElements(element.packageName, '*') + } + } + localTypeElementVisitors = [typeVisitor] + + buildClassLoader('example.Foo', ''' +package example; +class Foo { + class Bar {} +} +''') + + expect: + lookedUp.size() == 2 + lookedUp[0].name == 'example.Foo' + lookedUp[1].name == 'example.Foo$Bar' + } + @Override protected Collection getLocalTypeElementVisitors() { return localTypeElementVisitors From b5d405c99ac1ad5991cd601d1248cf70cc831256 Mon Sep 17 00:00:00 2001 From: Tim Yates Date: Thu, 19 Jan 2023 18:56:26 +0000 Subject: [PATCH 409/743] Disable prediction and distribution for test-suite-kotlin (#8612) * Disable predition and distribution for test-suite-kotlin * Trigger CI again --- test-suite-kotlin/build.gradle | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/test-suite-kotlin/build.gradle b/test-suite-kotlin/build.gradle index a85f16a8d55..a6cefd28057 100644 --- a/test-suite-kotlin/build.gradle +++ b/test-suite-kotlin/build.gradle @@ -82,3 +82,12 @@ tasks.named("compileTestKotlin") { tasks.named("test") { useJUnitPlatform() } + +tasks.withType(Test).configureEach { + distribution { + enabled = false + } + predictiveSelection { + enabled = false + } +} From 6b3e22c30f627add5d748dde29c4fafe37229fd6 Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Fri, 20 Jan 2023 09:42:28 +0100 Subject: [PATCH 410/743] doc: extract cors docs to sections (#8637) --- .../httpServer/serverConfiguration/cors.adoc | 161 +----------------- .../cors/corsAllowCredentials.adoc | 13 ++ .../cors/corsAllowedHeaders.adoc | 17 ++ .../cors/corsAllowedMethods.adoc | 17 ++ .../cors/corsAllowedOrigins.adoc | 19 +++ .../cors/corsConfiguration.adoc | 36 ++++ .../cors/corsExposedHeaders.adoc | 15 ++ .../serverConfiguration/cors/corsMaxAge.adoc | 13 ++ .../cors/corsMultipleHeaderValues.adoc | 9 + src/main/docs/guide/toc.yml | 11 +- 10 files changed, 150 insertions(+), 161 deletions(-) create mode 100644 src/main/docs/guide/httpServer/serverConfiguration/cors/corsAllowCredentials.adoc create mode 100644 src/main/docs/guide/httpServer/serverConfiguration/cors/corsAllowedHeaders.adoc create mode 100644 src/main/docs/guide/httpServer/serverConfiguration/cors/corsAllowedMethods.adoc create mode 100644 src/main/docs/guide/httpServer/serverConfiguration/cors/corsAllowedOrigins.adoc create mode 100644 src/main/docs/guide/httpServer/serverConfiguration/cors/corsConfiguration.adoc create mode 100644 src/main/docs/guide/httpServer/serverConfiguration/cors/corsExposedHeaders.adoc create mode 100644 src/main/docs/guide/httpServer/serverConfiguration/cors/corsMaxAge.adoc create mode 100644 src/main/docs/guide/httpServer/serverConfiguration/cors/corsMultipleHeaderValues.adoc diff --git a/src/main/docs/guide/httpServer/serverConfiguration/cors.adoc b/src/main/docs/guide/httpServer/serverConfiguration/cors.adoc index d43eba6a2e7..e9f41b40e26 100644 --- a/src/main/docs/guide/httpServer/serverConfiguration/cors.adoc +++ b/src/main/docs/guide/httpServer/serverConfiguration/cors.adoc @@ -1,160 +1 @@ -Micronaut supports CORS (link:https://www.w3.org/TR/cors/[Cross Origin Resource Sharing]) out of the box. By default, CORS requests are rejected. To enable processing of CORS requests, modify your configuration. For example with `application.yml`: - -.CORS Configuration Example -[source,yaml] ----- -micronaut: - server: - cors: - enabled: true ----- - -By only enabling CORS processing, a "wide open" strategy is adopted that allows requests from any origin. - -To change the settings for all origins or a specific origin, change the configuration to provide one or more "configurations". By providing any configuration, the default "wide open" configuration is not configured. - -.CORS Configurations -[source,yaml] ----- -micronaut: - server: - cors: - enabled: true - configurations: - all: - ... - web: - ... - mobile: - ... ----- - -In the above example, three configurations are provided. Their names (`all`, `web`, `mobile`) are not important and have no significance inside Micronaut. They are there purely to be able to easily recognize the intended user of the configuration. - -The same configuration properties can be applied to each configuration. See link:{api}/io/micronaut/http/server/cors/CorsOriginConfiguration.html[CorsOriginConfiguration] for properties that can be defined. The values of each configuration supplied will default to the default values of the corresponding fields. - -When a CORS request is made, configurations are searched for allowed origins that match exactly or match the request origin through a regular expression. - -== Allowed Origins - -To allow any origin for a given configuration, don't include the `allowedOrigins` key in your configuration. - -For multiple valid origins, set the `allowedOrigins` key of the configuration to a list of strings. Each value can either be a static value (`http://www.foo.com`) or a regular expression (`^http(|s)://www\.google\.com$`). - -Regular expressions are passed to link:{javase}java/util/regex/Pattern.html#compile-java.lang.String-[Pattern#compile] and compared to the request origin with link:{javase}java/util/regex/Matcher.html#matches--[Matcher#matches]. - -.Example CORS Configuration -[source,yaml] ----- -micronaut: - server: - cors: - enabled: true - configurations: - web: - allowedOrigins: - - http://foo.com - - ^http(|s):\/\/www\.google\.com$ ----- - -== Allowed Methods - -To allow any request method for a given configuration, don't include the `allowedMethods` key in your configuration. - -For multiple allowed methods, set the `allowedMethods` key of the configuration to a list of strings. - -.Example CORS Configuration -[source,yaml] ----- -micronaut: - server: - cors: - enabled: true - configurations: - web: - allowedMethods: - - POST - - PUT ----- - -== Allowed Headers - -To allow any request header for a given configuration, don't include the `allowedHeaders` key in your configuration. - -For multiple allowed headers, set the `allowedHeaders` key of the configuration to a list of strings. - -.Example CORS Configuration -[source,yaml] ----- -micronaut: - server: - cors: - enabled: true - configurations: - web: - allowedHeaders: - - Content-Type - - Authorization ----- - -== Exposed Headers - -To configure the headers that are sent in the response to a CORS request through the `Access-Control-Expose-Headers` header, include a list of strings for the `exposedHeaders` key in your configuration. None are exposed by default. - -.Example CORS Configuration -[source,yaml] ----- -micronaut: - server: - cors: - enabled: true - configurations: - web: - exposedHeaders: - - Content-Type - - Authorization ----- - -== Allow Credentials - -Credentials are allowed by default for CORS requests. To disallow credentials, set the `allowCredentials` option to `false`. - -.Example CORS Configuration -[source,yaml] ----- -micronaut: - server: - cors: - enabled: true - configurations: - web: - allowCredentials: false ----- - -== Max Age - -The default maximum age that preflight requests can be cached is 30 minutes. To change that behavior, specify a value in seconds. - -.Example CORS Configuration -[source,yaml] ----- -micronaut: - server: - cors: - enabled: true - configurations: - web: - maxAge: 3600 # 1 hour ----- - -== Multiple Header Values - -By default, when a header has multiple values, multiple headers are sent, each with a single value. It is possible to change the behavior to send a single header with a comma-separated list of values by setting a configuration option. - -[source,yaml] ----- -micronaut: - server: - cors: - single-header: true ----- +Micronaut supports CORS (link:https://www.w3.org/TR/cors/[Cross Origin Resource Sharing]) out of the box. By default, CORS requests are rejected. diff --git a/src/main/docs/guide/httpServer/serverConfiguration/cors/corsAllowCredentials.adoc b/src/main/docs/guide/httpServer/serverConfiguration/cors/corsAllowCredentials.adoc new file mode 100644 index 00000000000..f976c613a40 --- /dev/null +++ b/src/main/docs/guide/httpServer/serverConfiguration/cors/corsAllowCredentials.adoc @@ -0,0 +1,13 @@ +Credentials are allowed by default for CORS requests. To disallow credentials, set the `allowCredentials` option to `false`. + +.Example CORS Configuration +[source,yaml] +---- +micronaut: + server: + cors: + enabled: true + configurations: + web: + allowCredentials: false +---- diff --git a/src/main/docs/guide/httpServer/serverConfiguration/cors/corsAllowedHeaders.adoc b/src/main/docs/guide/httpServer/serverConfiguration/cors/corsAllowedHeaders.adoc new file mode 100644 index 00000000000..f4c215d0472 --- /dev/null +++ b/src/main/docs/guide/httpServer/serverConfiguration/cors/corsAllowedHeaders.adoc @@ -0,0 +1,17 @@ +To allow any request header for a given configuration, don't include the `allowedHeaders` key in your configuration. + +For multiple allowed headers, set the `allowedHeaders` key of the configuration to a list of strings. + +.Example CORS Configuration +[source,yaml] +---- +micronaut: + server: + cors: + enabled: true + configurations: + web: + allowedHeaders: + - Content-Type + - Authorization +---- diff --git a/src/main/docs/guide/httpServer/serverConfiguration/cors/corsAllowedMethods.adoc b/src/main/docs/guide/httpServer/serverConfiguration/cors/corsAllowedMethods.adoc new file mode 100644 index 00000000000..1d4c963efd1 --- /dev/null +++ b/src/main/docs/guide/httpServer/serverConfiguration/cors/corsAllowedMethods.adoc @@ -0,0 +1,17 @@ +To allow any request method for a given configuration, don't include the `allowedMethods` key in your configuration. + +For multiple allowed methods, set the `allowedMethods` key of the configuration to a list of strings. + +.Example CORS Configuration +[source,yaml] +---- +micronaut: + server: + cors: + enabled: true + configurations: + web: + allowedMethods: + - POST + - PUT +---- diff --git a/src/main/docs/guide/httpServer/serverConfiguration/cors/corsAllowedOrigins.adoc b/src/main/docs/guide/httpServer/serverConfiguration/cors/corsAllowedOrigins.adoc new file mode 100644 index 00000000000..50dbf256c61 --- /dev/null +++ b/src/main/docs/guide/httpServer/serverConfiguration/cors/corsAllowedOrigins.adoc @@ -0,0 +1,19 @@ +To allow any origin for a given configuration, don't include the `allowedOrigins` key in your configuration. + +For multiple valid origins, set the `allowedOrigins` key of the configuration to a list of strings. Each value can either be a static value (`http://www.foo.com`) or a regular expression (`^http(|s)://www\.google\.com$`). + +Regular expressions are passed to link:{javase}java/util/regex/Pattern.html#compile-java.lang.String-[Pattern#compile] and compared to the request origin with link:{javase}java/util/regex/Matcher.html#matches--[Matcher#matches]. + +.Example CORS Configuration +[source,yaml] +---- +micronaut: + server: + cors: + enabled: true + configurations: + web: + allowedOrigins: + - http://foo.com + - ^http(|s):\/\/www\.google\.com$ +---- diff --git a/src/main/docs/guide/httpServer/serverConfiguration/cors/corsConfiguration.adoc b/src/main/docs/guide/httpServer/serverConfiguration/cors/corsConfiguration.adoc new file mode 100644 index 00000000000..ccd7bd4f75b --- /dev/null +++ b/src/main/docs/guide/httpServer/serverConfiguration/cors/corsConfiguration.adoc @@ -0,0 +1,36 @@ +To enable processing of CORS requests, modify your configuration. For example with `application.yml`: + +.CORS Configuration Example +[source,yaml] +---- +micronaut: + server: + cors: + enabled: true +---- + +By only enabling CORS processing, a "wide open" strategy is adopted that allows requests from any origin. + +To change the settings for all origins or a specific origin, change the configuration to provide one or more "configurations". By providing any configuration, the default "wide open" configuration is not configured. + +.CORS Configurations +[source,yaml] +---- +micronaut: + server: + cors: + enabled: true + configurations: + all: + ... + web: + ... + mobile: + ... +---- + +In the above example, three configurations are provided. Their names (`all`, `web`, `mobile`) are not important and have no significance inside Micronaut. They are there purely to be able to easily recognize the intended user of the configuration. + +The same configuration properties can be applied to each configuration. See link:{api}/io/micronaut/http/server/cors/CorsOriginConfiguration.html[CorsOriginConfiguration] for properties that can be defined. The values of each configuration supplied will default to the default values of the corresponding fields. + +When a CORS request is made, configurations are searched for allowed origins that match exactly or match the request origin through a regular expression. diff --git a/src/main/docs/guide/httpServer/serverConfiguration/cors/corsExposedHeaders.adoc b/src/main/docs/guide/httpServer/serverConfiguration/cors/corsExposedHeaders.adoc new file mode 100644 index 00000000000..11a931a294d --- /dev/null +++ b/src/main/docs/guide/httpServer/serverConfiguration/cors/corsExposedHeaders.adoc @@ -0,0 +1,15 @@ +To configure the headers that are sent in the response to a CORS request through the `Access-Control-Expose-Headers` header, include a list of strings for the `exposedHeaders` key in your configuration. None are exposed by default. + +.Example CORS Configuration +[source,yaml] +---- +micronaut: + server: + cors: + enabled: true + configurations: + web: + exposedHeaders: + - Content-Type + - Authorization +---- diff --git a/src/main/docs/guide/httpServer/serverConfiguration/cors/corsMaxAge.adoc b/src/main/docs/guide/httpServer/serverConfiguration/cors/corsMaxAge.adoc new file mode 100644 index 00000000000..27a9cafe456 --- /dev/null +++ b/src/main/docs/guide/httpServer/serverConfiguration/cors/corsMaxAge.adoc @@ -0,0 +1,13 @@ +The default maximum age that preflight requests can be cached is 30 minutes. To change that behavior, specify a value in seconds. + +.Example CORS Configuration +[source,yaml] +---- +micronaut: + server: + cors: + enabled: true + configurations: + web: + maxAge: 3600 # 1 hour +---- diff --git a/src/main/docs/guide/httpServer/serverConfiguration/cors/corsMultipleHeaderValues.adoc b/src/main/docs/guide/httpServer/serverConfiguration/cors/corsMultipleHeaderValues.adoc new file mode 100644 index 00000000000..f90ed768539 --- /dev/null +++ b/src/main/docs/guide/httpServer/serverConfiguration/cors/corsMultipleHeaderValues.adoc @@ -0,0 +1,9 @@ +By default, when a header has multiple values, multiple headers are sent, each with a single value. It is possible to change the behavior to send a single header with a comma-separated list of values by setting a configuration option. + +[source,yaml] +---- +micronaut: + server: + cors: + single-header: true +---- diff --git a/src/main/docs/guide/toc.yml b/src/main/docs/guide/toc.yml index 7c681907379..f4f4d21ff63 100644 --- a/src/main/docs/guide/toc.yml +++ b/src/main/docs/guide/toc.yml @@ -128,7 +128,16 @@ httpServer: nettyClientPipeline: Configuring the Netty Client Pipeline nettyServerPipeline: Configuring the Netty Server Pipeline listener: Advanced Listener Configuration - cors: Configuring CORS + cors: + title: Configuring CORS + corsConfiguration: CORS via Configuration + corsAllowedOrigins: Allowed Origins + corsAllowedMethods: Allowed Methods + corsAllowedHeaders: Allowed Headers + corsExposedHeaders: Exposed Headers + corsAllowCredentials: Allow Credentials + corsMaxAge: Max Age + corsMultipleHeaderValues: Multiple Header Values https: Securing the Server with HTTPS dualProtocol: Enabling HTTP and HTTPS accessLogger: Enabling Access Logger From 3c45827f481152ce5c73eda9982af932c96cf88e Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Fri, 20 Jan 2023 10:13:00 +0100 Subject: [PATCH 411/743] build: Micronaut Micrometer 4.7.1 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index c4803fece00..b8088d8d3e3 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -90,7 +90,7 @@ managed-micronaut-jmx = "3.2.0" managed-micronaut-kafka = "4.5.0" managed-micronaut-kotlin = "3.2.2" managed-micronaut-kubernetes = "3.4.0" -managed-micronaut-micrometer = "4.7.0" +managed-micronaut-micrometer = "4.7.1" managed-micronaut-microstream = "1.3.0" managed-micronaut-liquibase = "5.6.0" managed-micronaut-mongo = "4.6.0" From 4926ab1683490d33620089ab5b21acd4f3b34440 Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Fri, 20 Jan 2023 10:41:00 +0100 Subject: [PATCH 412/743] build: Security 3.9.2 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index b8088d8d3e3..821e79f029a 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -112,7 +112,7 @@ managed-micronaut-rss = "3.2.0" managed-micronaut-rxjava1 = "1.0.0" managed-micronaut-rxjava2 = "1.3.0" managed-micronaut-rxjava3 = "2.4.0" -managed-micronaut-security = "3.9.1" +managed-micronaut-security = "3.9.2" managed-micronaut-serialization = "1.5.0" managed-micronaut-servlet = "3.3.5" managed-micronaut-spring = "4.4.0" From 52bf3d20f47ac8be096b20c04c4112714204ee19 Mon Sep 17 00:00:00 2001 From: micronaut-build Date: Fri, 20 Jan 2023 10:58:14 +0000 Subject: [PATCH 413/743] [skip ci] Release v3.8.2 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 25fae817e33..e186e602d60 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -projectVersion=3.8.2-SNAPSHOT +projectVersion=3.8.2 projectGroupId=io.micronaut title=Micronaut projectDesc=Natively Cloud Native From 69131c2481adeff34b5c94522aaf9a348cc04ad3 Mon Sep 17 00:00:00 2001 From: micronaut-build Date: Fri, 20 Jan 2023 11:12:02 +0000 Subject: [PATCH 414/743] Back to 3.8.3-SNAPSHOT --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index e186e602d60..a561ef36e89 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -projectVersion=3.8.2 +projectVersion=3.8.3-SNAPSHOT projectGroupId=io.micronaut title=Micronaut projectDesc=Natively Cloud Native From 2998a0219f83d884761f810af2c5543df053236a Mon Sep 17 00:00:00 2001 From: Milenko Supic Date: Mon, 23 Jan 2023 13:44:35 +0100 Subject: [PATCH 415/743] Fixed property placeholder expression issue (issue #8633) (PR #8645) --- .../core/annotation/AnnotationValue.java | 8 ++-- .../EnvironmentAnnotationValue.java | 35 +++++++++++++++ .../env/AnnotationPlaceholderSpec.groovy | 44 +++++++++++++++++++ .../io/micronaut/context/env/ChildValue.java | 13 ++++++ .../io/micronaut/context/env/ParentValue.java | 19 ++++++++ 5 files changed, 115 insertions(+), 4 deletions(-) create mode 100644 inject/src/test/groovy/io/micronaut/context/env/ChildValue.java create mode 100644 inject/src/test/groovy/io/micronaut/context/env/ParentValue.java diff --git a/core/src/main/java/io/micronaut/core/annotation/AnnotationValue.java b/core/src/main/java/io/micronaut/core/annotation/AnnotationValue.java index b0060cf2a63..04bcfb7bd85 100644 --- a/core/src/main/java/io/micronaut/core/annotation/AnnotationValue.java +++ b/core/src/main/java/io/micronaut/core/annotation/AnnotationValue.java @@ -1092,7 +1092,7 @@ final T getRequiredValue(String member, Class type) { * @return The result * @throws IllegalStateException If no member is available that conforms to the given name and type */ - public final @NonNull + public @NonNull List> getAnnotations(String member, Class type) { ArgumentUtils.requireNonNull("type", type); String typeName = type.getName(); @@ -1131,7 +1131,7 @@ List> getAnnotations(String member, Cl * @throws IllegalStateException If no member is available that conforms to the given name and type */ @SuppressWarnings("unchecked") - public final @NonNull + public @NonNull List> getAnnotations(String member) { ArgumentUtils.requireNonNull("member", member); Object v = values.get(member); @@ -1161,7 +1161,7 @@ List> getAnnotations(String member) { * @throws IllegalStateException If no member is available that conforms to the given name and type */ public @NonNull - final Optional> getAnnotation(String member, Class type) { + Optional> getAnnotation(String member, Class type) { ArgumentUtils.requireNonNull("type", type); String typeName = type.getName(); @@ -1196,7 +1196,7 @@ final Optional> getAnnotation(String m * @since 3.3.0 */ public @NonNull - final Optional> getAnnotation(@NonNull String member) { + Optional> getAnnotation(@NonNull String member) { ArgumentUtils.requireNonNull("member", member); Object v = values.get(member); if (v instanceof AnnotationValue) { diff --git a/inject/src/main/java/io/micronaut/inject/annotation/EnvironmentAnnotationValue.java b/inject/src/main/java/io/micronaut/inject/annotation/EnvironmentAnnotationValue.java index 96597a5eb34..4b6b749e1da 100644 --- a/inject/src/main/java/io/micronaut/inject/annotation/EnvironmentAnnotationValue.java +++ b/inject/src/main/java/io/micronaut/inject/annotation/EnvironmentAnnotationValue.java @@ -19,9 +19,13 @@ import io.micronaut.context.env.PropertyPlaceholderResolver; import io.micronaut.core.annotation.AnnotationValue; import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.NonNull; import java.lang.annotation.Annotation; import java.util.Arrays; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; import java.util.stream.Stream; /** @@ -34,6 +38,8 @@ @Internal class EnvironmentAnnotationValue extends AnnotationValue { + private final Environment environment; + /** * Default constructor. * @@ -78,5 +84,34 @@ class EnvironmentAnnotationValue extends AnnotationValue List> getAnnotations(String member, Class type) { + List> annotationValues = super.getAnnotations(member, type); + return annotationValues.stream().map(av -> new EnvironmentAnnotationValue<>(environment, av)).collect(Collectors.toList()); + } + + @Override + public @NonNull + List> getAnnotations(String member) { + List> annotationValues = super.getAnnotations(member); + return annotationValues.stream().map(av -> new EnvironmentAnnotationValue<>(environment, av)).collect(Collectors.toList()); + } + + @Override + public @NonNull + Optional> getAnnotation(String member, Class type) { + Optional> annotationValue = super.getAnnotation(member, type); + return annotationValue.map(av -> new EnvironmentAnnotationValue<>(environment, av)); + } + + @Override + public @NonNull + Optional> getAnnotation(@NonNull String member) { + Optional> annotationValue = super.getAnnotation(member); + return annotationValue.map(av -> new EnvironmentAnnotationValue<>(environment, av)); } } diff --git a/inject/src/test/groovy/io/micronaut/context/env/AnnotationPlaceholderSpec.groovy b/inject/src/test/groovy/io/micronaut/context/env/AnnotationPlaceholderSpec.groovy index 310ec1faebd..d14668dfaf7 100644 --- a/inject/src/test/groovy/io/micronaut/context/env/AnnotationPlaceholderSpec.groovy +++ b/inject/src/test/groovy/io/micronaut/context/env/AnnotationPlaceholderSpec.groovy @@ -1,6 +1,7 @@ package io.micronaut.context.env import io.micronaut.context.ApplicationContext +import io.micronaut.core.annotation.AnnotationValue import io.micronaut.inject.BeanDefinition import spock.lang.Specification import jakarta.inject.Singleton @@ -25,9 +26,52 @@ class AnnotationPlaceholderSpec extends Specification { values == ['a', 'b', 'c', 'd', 'e'] as String[] } + void "test nested annotation placeholder binding"() { + given: + ApplicationContext ctx = ApplicationContext.run(['child.value1': 'newChildValue1', 'child.value2': 'newChildValue2', 'child.value3': 'newChildValue3']) + BeanDefinition beanDefinition = ctx.getBeanDefinition(NestedTest) + + when: + AnnotationValue parentValue = beanDefinition.findAnnotation(ParentValue.class).get() + AnnotationValue childValue = parentValue.getAnnotation('childValue', ChildValue.class).get() + + then: + childValue.stringValue('value').get() == 'newChildValue1' + + when: + childValue = parentValue.getAnnotation('childValue').get() + + then: + childValue.stringValue('value').get() == 'newChildValue1' + + when: + List childValues = parentValue.getAnnotations('childValues', ChildValue.class) + + then: + childValues.size() == 2 + childValues.get(0).stringValue('value').get() == 'newChildValue2' + childValues.get(1).stringValue('value').get() == 'newChildValue3' + + when: + childValues = parentValue.getAnnotations('childValues') + + then: + childValues.size() == 2 + childValues.get(0).stringValue('value').get() == 'newChildValue2' + childValues.get(1).stringValue('value').get() == 'newChildValue3' + } + @Singleton @StringArrayValue(['${from.config}', '${more.values}']) static class Test { } + + @Singleton + @ParentValue(value = "parentValue", + childValue = @ChildValue(value = '${child.value1}'), + childValues = [@ChildValue(value = '${child.value2}'), @ChildValue(value = '${child.value3}')]) + static class NestedTest { + + } } diff --git a/inject/src/test/groovy/io/micronaut/context/env/ChildValue.java b/inject/src/test/groovy/io/micronaut/context/env/ChildValue.java new file mode 100644 index 00000000000..69ade0fe2e0 --- /dev/null +++ b/inject/src/test/groovy/io/micronaut/context/env/ChildValue.java @@ -0,0 +1,13 @@ +package io.micronaut.context.env; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +@Documented +@Retention(RetentionPolicy.RUNTIME) +@interface ChildValue { + + String value() default ""; + +} diff --git a/inject/src/test/groovy/io/micronaut/context/env/ParentValue.java b/inject/src/test/groovy/io/micronaut/context/env/ParentValue.java new file mode 100644 index 00000000000..25f02a3fda8 --- /dev/null +++ b/inject/src/test/groovy/io/micronaut/context/env/ParentValue.java @@ -0,0 +1,19 @@ +package io.micronaut.context.env; + +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; + +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE}) +@interface ParentValue { + + String value() default ""; + + ChildValue childValue() default @ChildValue(); + + ChildValue[] childValues() default {}; +} From d500b2f1c7a4133cc69934f8421ff099a569923f Mon Sep 17 00:00:00 2001 From: Graeme Rocher Date: Mon, 23 Jan 2023 15:22:32 +0100 Subject: [PATCH 416/743] Adds support for KSP (#8462) Adds a new `micronaut-inject-kotlin` module that implements initial support for Kotlin Symbol Processing (KSP). Co-authored-by: jameskleeh --- .../AbstractAnnotationMetadataBuilder.java | 8 + .../internal/KotlinDeprecatedTransformer.java | 43 + .../micronaut/inject/ast/ElementFactory.java | 2 + .../micronaut/inject/ast/PropertyElement.java | 10 + .../ast/utils/EnclosedElementsQuery.java | 28 +- .../visitor/BeanIntrospectionWriter.java | 2 +- .../AbstractBeanElementCreator.java | 9 +- .../BeanDefinitionCreatorFactory.java | 6 +- .../DeclaredBeanElementCreator.java | 3 +- .../processing/FactoryBeanElementCreator.java | 16 +- ...troductionInterfaceBeanElementCreator.java | 16 + .../processing/ProcessingException.java | 12 +- .../inject/writer/BeanDefinitionVisitor.java | 9 +- .../inject/writer/BeanDefinitionWriter.java | 22 +- ...ut.inject.annotation.AnnotationTransformer | 1 + .../core/annotation/AnnotationUtil.java | 1 + gradle/libs.versions.toml | 7 +- .../HttpClientIntroductionAdvice.java | 2 +- .../groovy/visitor/GroovyClassElement.java | 2 +- .../groovy/visitor/GroovyMethodElement.java | 3 +- .../beans/BeanIntrospectionSpec.groovy | 1 - .../processing/visitor/JavaClassElement.java | 6 +- .../processing/visitor/JavaMethodElement.java | 2 +- .../compile/AroundConstructCompileSpec.groovy | 14 - .../LifeCycleWithProxyTargetSpec.groovy | 35 +- ...roductionAdviceWithNewInterfaceSpec.groovy | 26 +- .../io/micronaut/aop/introduction/Stub.java | 1 - .../visitors/ClassElementSpec.groovy | 301 ++- inject-kotlin-test/build.gradle | 11 +- .../test/AbstractKotlinCompilerSpec.groovy | 132 +- .../processing/test/KotlinCompiler.java | 327 +++ .../processing/test/KotlinCompileHelper.kt | 249 -- .../processing/test/KotlinCompilerTest.groovy | 5 +- inject-kotlin/build.gradle | 65 + inject-kotlin/gradle.properties | 1 + .../kotlin/processing/KotlinOutputVisitor.kt | 81 + .../KotlinAnnotationMetadataBuilder.kt | 591 +++++ .../KotlinElementAnnotationMetadataFactory.kt | 30 + .../beans/BeanDefinitionProcessor.kt | 135 ++ .../beans/BeanDefinitionProcessorProvider.kt | 27 + .../micronaut/kotlin/processing/extensions.kt | 138 ++ .../visitor/AbstractKotlinElement.kt | 328 +++ .../visitor/KSAnnotatedReference.kt | 103 + .../processing/visitor/KotlinClassElement.kt | 876 +++++++ .../visitor/KotlinConstructorElement.kt | 53 + .../visitor/KotlinElementFactory.kt | 229 ++ .../visitor/KotlinEnumConstructorElement.kt | 51 + .../processing/visitor/KotlinEnumElement.kt | 50 + .../processing/visitor/KotlinFieldElement.kt | 99 + .../KotlinGenericPlaceholderElement.kt | 113 + .../processing/visitor/KotlinMethodElement.kt | 388 +++ .../visitor/KotlinParameterElement.kt | 82 + .../visitor/KotlinPropertyElement.kt | 525 ++++ .../visitor/KotlinVisitorContext.kt | 237 ++ .../visitor/KotlinWildcardElement.kt | 89 + .../processing/visitor/LoadedVisitor.kt | 114 + .../visitor/TypeElementSymbolProcessor.kt | 332 +++ .../TypeElementSymbolProcessorProvider.kt | 27 + ...ols.ksp.processing.SymbolProcessorProvider | 2 + inject-kotlin/src/main/resources/notes.txt | 3 + .../aop/adapter/MethodAdapterSpec.groovy | 380 +++ .../AbstractClassIntroductionSpec.groovy | 262 ++ .../AnnotatedConstructorArgumentSpec.groovy | 116 + .../aop/compile/AroundCompileSpec.groovy | 970 ++++++++ .../compile/AroundConstructCompileSpec.groovy | 746 ++++++ .../ExecutableFactoryMethodSpec.groovy | 107 + .../aop/compile/FinalModifierSpec.groovy | 243 ++ .../compile/GeneratedAnnotationSpec.groovy | 54 + .../InheritedAnnotationMetadataSpec.groovy | 157 ++ .../compile/IntroductionAnnotationSpec.groovy | 137 ++ .../compile/IntroductionCompileSpec.groovy | 110 + .../IntroductionGenericTypesSpec.groovy | 126 + .../IntroductionInnerInterfaceSpec.groovy | 39 + .../compile/IntroductionWithAroundSpec.groovy | 50 + .../aop/compile/LifeCycleWithProxySpec.groovy | 168 ++ .../LifeCycleWithProxyTargetSpec.groovy | 147 ++ ...PostConstructInterceptorCompileSpec.groovy | 293 +++ .../aop/compile/ValidatedNonBeanSpec.groovy | 33 + ...ceDefinedOnConcreteClassFactorySpec.groovy | 76 + .../factory/AdviceDefinedOnFactorySpec.groovy | 50 + ...AdviceDefinedOnInterfaceFactorySpec.groovy | 121 + .../aop/hotswap/ProxyHotswapSpec.groovy | 29 + ...AbstractClassIntroductionAdviceSpec.groovy | 28 + .../InterfaceIntroductionAdviceSpec.groovy | 55 + ...roductionAdviceWithNewInterfaceSpec.groovy | 281 +++ .../IntroductionOnConcreteClassSpec.groovy | 32 + ...ppedIntroductionOnConcreteClassSpec.groovy | 43 + .../MyRepoIntroductionSpec.groovy | 149 ++ .../DelegatingIntroductionAdviceSpec.groovy | 20 + .../IntroductionInnerInterfaceSpec.groovy | 50 + ...uctionWithAroundOnConcreteClassSpec.groovy | 135 ++ .../itfce/InterfaceMethodLevelAopSpec.groovy | 67 + .../aop/itfce/InterfaceTypeLevelSpec.groovy | 61 + .../aop/named/NamedAopAdviceSpec.groovy | 74 + .../ProxyingMethodLevelAopSpec.groovy | 80 + .../SimpleClassMethodLevelAopSpec.groovy | 107 + .../simple/SimpleClassTypeLevelAopSpec.groovy | 67 + .../processing/beans/AbstractBeanSpec.groovy | 59 + .../beans/BeanDefinitionSpec.groovy | 849 +++++++ .../beans/BeanRegistrationSpec.groovy | 70 + .../processing/beans/SingletonSpec.groovy | 111 + .../aliasfor/AliasForQualifierSpec.groovy | 50 + .../collect/InjectCollectionBeanSpec.groovy | 41 + .../ConfigPropertiesInnerClassSpec.groovy | 29 + .../ConfigurationPropertiesFactorySpec.groovy | 14 + .../ConfigurationPropertiesSpec.groovy | 148 ++ ...figurationPropertiesInheritanceSpec.groovy | 197 ++ .../executable/ExecutableBeanSpec.groovy | 128 + .../beans/executable/ExecutableSpec.groovy | 109 + .../InheritedExecutableSpec.groovy | 149 ++ .../PrototypeAnnotationSpec.groovy | 16 + .../beanproperty/FactoryBeanFieldSpec.groovy | 367 +++ .../InheritanceSingletonSpec.groovy | 32 + .../inject/ast/ClassElementSpec.groovy | 314 +++ .../ConfigPropertiesInnerClassSpec.groovy | 29 + .../ConfigPropertiesParseSpec.groovy | 1111 +++++++++ .../ConfigurationPropertiesBuilderSpec.groovy | 780 ++++++ .../ConfigurationPropertiesFactorySpec.groovy | 14 + .../ConfigurationPropertiesSpec.groovy | 133 ++ ...nfigurationPropertiesWithRawMapSpec.groovy | 23 + ...mmutableConfigurationPropertiesSpec.groovy | 164 ++ ...nterfaceConfigurationPropertiesSpec.groovy | 280 +++ ...rimitiveConfigurationPropertiesSpec.groovy | 27 + .../ValidatedConfigurationSpec.groovy | 77 + .../VisibilityIssuesSpec.groovy | 74 + .../ValidatedInterfaceConfigPropsSpec.groovy | 43 + .../generics/GenericTypeArgumentsSpec.groovy | 297 +++ .../visitor/BeanIntrospectionSpec.groovy | 2119 +++++++++++++++++ .../visitor/KotlinReconstructionSpec.groovy | 161 ++ .../micronaut/kotlin/processing/aop/Logged.kt | 26 + .../processing/aop/LoggedInterceptor.kt | 29 + .../kotlin/processing/aop/adapter/Test.kt | 20 + .../compile/AroundConstructAnnTransformer.kt | 24 + .../aop/compile/NamedTestAnnMapper.kt | 22 + .../compile/TestStereotypeAnnTransformer.kt | 24 + .../processing/aop/factory/AnotherClass.kt | 6 + .../processing/aop/factory/ConcreteClass.kt | 116 + .../aop/factory/ConcreteClassFactory.kt | 25 + .../processing/aop/factory/InterfaceClass.kt | 44 + .../aop/factory/InterfaceFactory.kt | 28 + .../processing/aop/factory/InterfaceImpl.kt | 86 + .../aop/factory/SessionFactoryFactory.kt | 17 + .../aop/hotswap/HotswappableProxyingClass.kt | 23 + .../aop/introduction/AbstractClass.kt | 16 + .../aop/introduction/AbstractCrudRepo.kt | 8 + .../AbstractCustomAbstractCrudRepo.kt | 10 + .../introduction/AbstractCustomCrudRepo.kt | 10 + .../aop/introduction/AbstractSuperClass.kt | 6 + .../aop/introduction/ChildIntroduction.kt | 4 + .../aop/introduction/ConcreteClass.kt | 7 + .../processing/aop/introduction/CrudRepo.kt | 8 + .../aop/introduction/CustomCrudRepo.kt | 10 + .../aop/introduction/DeleteByIdCrudRepo.kt | 23 + .../aop/introduction/InjectParentInterface.kt | 6 + .../InterfaceIntroductionClass.kt | 13 + .../aop/introduction/ListenerAdvice.kt | 32 + .../introduction/ListenerAdviceInterceptor.kt | 51 + .../aop/introduction/ListenerAdviceMarker.kt | 6 + .../ListenerAdviceMarkerMapper.kt | 26 + .../processing/aop/introduction/Marker.kt | 5 + .../processing/aop/introduction/MyRepo.kt | 9 + .../processing/aop/introduction/MyRepo2.kt | 9 + .../aop/introduction/MyRepoIntroducer.kt | 24 + .../aop/introduction/NotImplemented.kt | 25 + .../aop/introduction/NotImplementedAdvice.kt | 15 + .../aop/introduction/ParentInterface.kt | 3 + .../processing/aop/introduction/RepoDef.kt | 25 + .../processing/aop/introduction/Stub.kt | 10 + .../aop/introduction/StubIntroducer.kt | 28 + .../aop/introduction/SuperInterface.kt | 6 + .../processing/aop/introduction/SuperRepo.kt | 21 + .../aop/introduction/delegation/Delegating.kt | 5 + .../introduction/delegation/DelegatingImpl.kt | 8 + .../delegation/DelegatingInterceptor.kt | 43 + .../delegation/DelegatingIntroduced.kt | 6 + .../delegation/DelegationAdvice.kt | 10 + .../aop/introduction/temp/MyBean.kt | 18 + .../introduction/with_around/CustomProxy.kt | 5 + .../aop/introduction/with_around/MyBean1.kt | 36 + .../aop/introduction/with_around/MyBean2.kt | 19 + .../aop/introduction/with_around/MyBean3.kt | 19 + .../aop/introduction/with_around/MyBean4.kt | 22 + .../aop/introduction/with_around/MyBean5.kt | 19 + .../aop/introduction/with_around/MyBean6.kt | 19 + .../aop/introduction/with_around/MyBean7.kt | 22 + .../aop/introduction/with_around/MyBean8.kt | 22 + .../aop/introduction/with_around/MyBean9.kt | 23 + .../with_around/ObservableInterceptor.kt | 13 + .../with_around/ProxyAdviceInterceptor.kt | 55 + .../introduction/with_around/ProxyAround.kt | 11 + .../with_around/ProxyAroundInterceptor.kt | 16 + .../with_around/ProxyIntroduction.kt | 25 + .../with_around/ProxyIntroductionAndAround.kt | 7 + ...oxyIntroductionAndAroundAndIntrospected.kt | 15 + ...onAndAroundAndIntrospectedAndExecutable.kt | 17 + ...ProxyIntroductionAndAroundOneAnnotation.kt | 13 + .../ProxyIntroductionInterceptor.kt | 25 + .../aop/itfce/AbstractInterfaceImpl.kt | 12 + .../aop/itfce/AbstractInterfaceTypeLevel.kt | 12 + .../processing/aop/itfce/InterfaceClass.kt | 88 + .../processing/aop/itfce/InterfaceImpl.kt | 122 + .../aop/itfce/InterfaceTypeLevel.kt | 27 + .../aop/itfce/InterfaceTypeLevelImpl.kt | 98 + .../kotlin/processing/aop/named/Config.kt | 10 + .../processing/aop/named/NamedFactory.kt | 54 + .../processing/aop/named/NamedInterface.kt | 5 + .../kotlin/processing/aop/named/OtherBean.kt | 17 + .../processing/aop/named/OtherInterface.kt | 5 + .../aop/proxytarget/ArgMutatingInterceptor.kt | 26 + .../processing/aop/proxytarget/Mutating.kt | 13 + .../aop/proxytarget/ProxyingClass.kt | 164 ++ .../processing/aop/simple/AnotherClass.kt | 160 ++ .../aop/simple/ArgMutatingInterceptor.kt | 26 + .../kotlin/processing/aop/simple/Bar.kt | 6 + .../processing/aop/simple/CovariantClass.kt | 4 + .../kotlin/processing/aop/simple/Invalid.kt | 14 + .../aop/simple/InvalidInterceptor.kt | 19 + .../kotlin/processing/aop/simple/Mutating.kt | 20 + .../processing/aop/simple/SimpleClass.kt | 187 ++ .../processing/aop/simple/TestBinding.kt | 12 + .../beans/aliasfor/TestAnnotation.kt | 37 + .../processing/beans/collect/MyIterable.kt | 22 + .../beans/collect/MySetOfStrings.kt | 26 + .../beans/collect/ThingThatNeedsMyIterable.kt | 6 + .../collect/ThingThatNeedsMySetOfStrings.kt | 26 + .../beans/configproperties/AnnWithClass.kt | 8 + .../beans/configproperties/MapProperties.kt | 23 + .../beans/configproperties/MyConfig.kt | 80 + .../beans/configproperties/MyConfigInner.kt | 12 + .../beans/configproperties/Neo4jProperties.kt | 30 + .../Neo4jPropertiesFactory.kt | 24 + .../processing/beans/configproperties/Pojo.kt | 18 + .../beans/configproperties/RecConf.kt | 24 + .../beans/configproperties/ValidatedConfig.kt | 34 + .../inheritance/ChildConfig.kt | 23 + .../configproperties/inheritance/MyConfig.kt | 24 + .../inheritance/MyOtherConfig.kt | 24 + .../inheritance/ParentArrayEachProps.kt | 40 + .../inheritance/ParentArrayEachPropsCtor.kt | 39 + .../inheritance/ParentEachProps.kt | 30 + .../inheritance/ParentEachPropsCtor.kt | 34 + .../inheritance/ParentPojo.kt | 20 + .../processing/beans/configuration/Engine.kt | 23 + .../beans/executable/BookController.kt | 56 + .../beans/executable/BookService.kt | 21 + .../beans/executable/RepeatableExecutable.kt | 7 + .../beans/factory/beanannotation/A.kt | 21 + .../elementapi/EntityAnnotationMapper.kt | 90 + .../kotlin/processing/elementapi/Foo.kt | 5 + .../kotlin/processing/elementapi/GenBase.kt | 5 + .../processing/elementapi/MarkerAnnotation.kt | 3 + .../processing/elementapi/OtherTestBean.kt | 21 + .../kotlin/processing/elementapi/OuterBean.kt | 12 + .../kotlin/processing/elementapi/SomeEnum.kt | 5 + .../kotlin/processing/elementapi/TestBean.kt | 11 + .../kotlin/processing/elementapi/TestClass.kt | 6 + .../processing/elementapi/TestEntity.kt | 31 + .../ChildConfigPropertiesX.kt | 10 + .../inject/configproperties/MapProperties.kt | 24 + .../inject/configproperties/MyConfig.kt | 64 + .../inject/configproperties/MyConfigInner.kt | 12 + .../configproperties/MyHibernateConfig.kt | 12 + .../configproperties/MyHibernateConfig2.kt | 11 + .../configproperties/MyPrimitiveConfig.kt | 9 + .../configproperties/Neo4jProperties.kt | 17 + .../Neo4jPropertiesFactory.kt | 24 + .../inject/configproperties/Pojo.kt | 16 + .../inject/configproperties/RecConf.kt | 25 + .../configproperties/ValidatedConfig.kt | 19 + .../inheritance/ChildConfig.kt | 8 + .../configproperties/inheritance/MyConfig.kt | 9 + .../inheritance/MyOtherConfig.kt | 11 + .../inheritance/ParentArrayEachProps.kt | 22 + .../inheritance/ParentArrayEachPropsCtor.kt | 22 + .../inheritance/ParentEachProps.kt | 16 + .../inheritance/ParentEachPropsCtor.kt | 16 + .../inheritance/ParentPojo.kt | 5 + .../inject/configproperties/itfce/MyConfig.kt | 13 + .../configproperties/itfce/MyEachConfig.kt | 13 + .../other/ParentConfigProperties.kt | 33 + .../processing/inject/configuration/Engine.kt | 23 + ...cronaut.inject.annotation.AnnotationMapper | 3 + ...ut.inject.annotation.AnnotationTransformer | 2 + inject-kotlin/src/test/resources/logback.xml | 15 + .../micronaut/context/DefaultBeanContext.java | 2 +- settings.gradle | 2 + test-suite-kotlin-ksp/build.gradle | 96 + test-suite-kotlin-ksp/gradle.properties | 2 + .../annotation/processing/DemoController.kt | 47 + .../SuspendFunctionInterceptorSpec.kt | 90 + .../processing/SuspendMethodSpec.kt | 81 + .../core/beans/AbstractTestEntity.kt | 23 + .../kotlin/io/micronaut/core/beans/Item.kt | 28 + .../core/beans/KotlinBeanIntrospectionSpec.kt | 37 + .../core/beans/RecusiveGenericsSpec.kt | 15 + .../io/micronaut/core/beans/SomeEntity.kt | 10 + .../io/micronaut/core/beans/TestEntity.kt | 26 + .../io/micronaut/core/beans/TestEntity2.kt | 7 + .../io/micronaut/docs/annotation/Pet.kt | 28 + .../io/micronaut/docs/annotation/PetClient.kt | 31 + .../docs/annotation/PetController.kt | 38 + .../docs/annotation/PetControllerSpec.kt | 52 + .../docs/annotation/PetOperations.kt | 36 + .../docs/annotation/headers/HeaderSpec.kt | 27 + .../docs/annotation/headers/PetClient.kt | 39 + .../docs/annotation/headers/PetController.kt | 35 + .../requestattributes/RequestAttributeSpec.kt | 32 + .../annotation/requestattributes/Story.kt | 23 + .../requestattributes/StoryClient.kt | 34 + .../requestattributes/StoryClientFilter.kt | 42 + .../requestattributes/StoryController.kt | 31 + .../docs/annotation/retry/PetClient.kt | 31 + .../docs/annotation/retry/PetFallback.kt | 33 + .../io/micronaut/docs/aop/advice/MyBean.kt | 18 + .../io/micronaut/docs/aop/advice/Timed.java | 19 + .../docs/aop/advice/method/MyFactory.kt | 33 + .../docs/aop/advice/type/MyFactory.kt | 33 + .../micronaut/docs/aop/around/AroundSpec.kt | 23 + .../io/micronaut/docs/aop/around/NotNull.kt | 34 + .../docs/aop/around/NotNullExample.kt | 29 + .../docs/aop/around/NotNullInterceptor.kt | 46 + .../docs/aop/introduction/IntroductionSpec.kt | 22 + .../micronaut/docs/aop/introduction/Stub.kt | 37 + .../docs/aop/introduction/StubExample.kt | 29 + .../docs/aop/introduction/StubIntroduction.kt | 35 + .../docs/aop/lifecycle/LifeCycleAdviseSpec.kt | 24 + .../micronaut/docs/aop/lifecycle/Product.kt | 18 + .../docs/aop/lifecycle/ProductBean.kt | 20 + .../docs/aop/lifecycle/ProductInterceptors.kt | 51 + .../docs/aop/lifecycle/ProductService.kt | 25 + .../io/micronaut/docs/aop/retry/Book.kt | 18 + .../micronaut/docs/aop/retry/BookService.kt | 67 + .../docs/aop/scheduled/ScheduledExample.kt | 60 + .../scheduled/TaskSchedulerInjectExample.kt | 30 + .../kotlin/io/micronaut/docs/basics/Book.kt | 33 + .../micronaut/docs/basics/BookController.kt | 32 + .../docs/basics/BookControllerSpec.kt | 59 + .../micronaut/docs/basics/HelloController.kt | 71 + .../docs/basics/HelloControllerSpec.kt | 131 + .../io/micronaut/docs/basics/Message.kt | 26 + .../docs/client/ThirdPartyClientFilterSpec.kt | 108 + .../micronaut/docs/client/filter/BasicAuth.kt | 29 + .../docs/client/filter/BasicAuthClient.kt | 29 + .../client/filter/BasicAuthClientFilter.kt | 36 + .../docs/client/filter/BasicAuthFilterSpec.kt | 36 + .../docs/client/filter/GoogleAuthFilter.kt | 41 + .../client/upload/MultipartFileUploadSpec.kt | 122 + .../docs/client/versioning/HelloClient.kt | 37 + .../docs/config/builder/CrankShaft.kt | 52 + .../micronaut/docs/config/builder/Engine.kt | 23 + .../docs/config/builder/EngineConfig.kt | 36 + .../docs/config/builder/EngineFactory.kt | 32 + .../docs/config/builder/EngineImpl.kt | 60 + .../docs/config/builder/SparkPlug.kt | 58 + .../micronaut/docs/config/builder/Vehicle.kt | 28 + .../docs/config/builder/VehicleSpec.kt | 30 + .../converters/MapToLocalDateConverter.kt | 48 + .../converters/MyConfigurationProperties.kt | 32 + .../MyConfigurationPropertiesSpec.kt | 41 + .../config/env/DataSourceConfiguration.kt | 30 + .../docs/config/env/DataSourceFactory.kt | 41 + .../micronaut/docs/config/env/EachBeanTest.kt | 41 + .../docs/config/env/EachPropertyTest.kt | 66 + .../docs/config/env/EnvironmentTest.kt | 48 + .../docs/config/env/HighRateLimit.kt | 5 + .../micronaut/docs/config/env/LowRateLimit.kt | 5 + .../io/micronaut/docs/config/env/OrderTest.kt | 23 + .../io/micronaut/docs/config/env/RateLimit.kt | 5 + .../config/env/RateLimitsConfiguration.kt | 36 + .../docs/config/env/RateLimitsFactory.kt | 24 + .../micronaut/docs/config/immutable/Engine.kt | 31 + .../docs/config/immutable/EngineConfig.kt | 47 + .../docs/config/immutable/Vehicle.kt | 26 + .../docs/config/immutable/VehicleSpec.kt | 25 + .../io/micronaut/docs/config/itfce/Engine.kt | 29 + .../docs/config/itfce/EngineConfig.kt | 45 + .../io/micronaut/docs/config/itfce/Vehicle.kt | 26 + .../docs/config/itfce/VehicleSpec.kt | 41 + .../micronaut/docs/config/mapFormat/Engine.kt | 21 + .../docs/config/mapFormat/EngineConfig.kt | 34 + .../docs/config/mapFormat/EngineImpl.kt | 35 + .../docs/config/mapFormat/Vehicle.kt | 26 + .../docs/config/mapFormat/VehicleSpec.kt | 29 + .../docs/config/properties/Engine.kt | 22 + .../docs/config/properties/EngineConfig.kt | 42 + .../docs/config/properties/EngineImpl.kt | 31 + .../docs/config/properties/Vehicle.kt | 27 + .../docs/config/properties/VehicleSpec.kt | 23 + .../micronaut/docs/config/property/Engine.kt | 41 + .../docs/config/property/EngineSpec.kt | 24 + .../io/micronaut/docs/config/value/Engine.kt | 22 + .../micronaut/docs/config/value/EngineImpl.kt | 36 + .../io/micronaut/docs/config/value/Vehicle.kt | 25 + .../docs/config/value/VehicleSpec.kt | 42 + .../io/micronaut/docs/context/Application.kt | 38 + .../docs/context/annotation/primary/Blue.kt | 31 + .../context/annotation/primary/ColorPicker.kt | 22 + .../docs/context/annotation/primary/Green.kt | 34 + .../context/annotation/primary/PrimarySpec.kt | 29 + .../annotation/primary/TestController.kt | 32 + .../context/env/DefaultEnvironmentSpec.kt | 17 + .../docs/context/env/EnvironmentSpec.kt | 43 + .../docs/context/events/SampleEvent.kt | 19 + .../context/events/SampleEventEmitterBean.kt | 34 + .../events/application/SampleEventListener.kt | 33 + .../application/SampleEventListenerSpec.kt | 25 + .../events/async/SampleEventListener.kt | 39 + .../events/async/SampleEventListenerSpec.kt | 35 + .../events/listener/SampleEventListener.kt | 46 + .../listener/SampleEventListenerSpec.kt | 23 + .../docs/datavalidation/groups/Email.kt | 31 + .../datavalidation/groups/EmailController.kt | 45 + .../groups/EmailControllerSpec.kt | 81 + .../datavalidation/groups/FinalValidation.kt | 24 + .../datavalidation/params/EmailController.kt | 39 + .../params/EmailControllerSpec.kt | 38 + .../docs/datavalidation/pogo/Email.kt | 31 + .../datavalidation/pogo/EmailController.kt | 39 + .../pogo/EmailControllerSpec.kt | 45 + .../micronaut/docs/events/factory/Engine.kt | 23 + .../docs/events/factory/EngineFactory.kt | 44 + .../docs/events/factory/EngineInitializer.kt | 32 + .../micronaut/docs/events/factory/V8Engine.kt | 27 + .../micronaut/docs/events/factory/Vehicle.kt | 26 + .../docs/events/factory/VehicleSpec.kt | 25 + .../io/micronaut/docs/factories/CrankShaft.kt | 23 + .../io/micronaut/docs/factories/Engine.kt | 22 + .../micronaut/docs/factories/EngineFactory.kt | 36 + .../io/micronaut/docs/factories/V8Engine.kt | 26 + .../io/micronaut/docs/factories/Vehicle.kt | 28 + .../docs/factories/VehicleMockSpec.kt | 32 + .../micronaut/docs/factories/VehicleSpec.kt | 22 + .../docs/factories/nullable/Engine.kt | 22 + .../factories/nullable/EngineConfiguration.kt | 35 + .../docs/factories/nullable/EngineFactory.kt | 39 + .../docs/factories/nullable/EngineSpec.java | 44 + .../factories/primitive/CylinderFactory.kt | 20 + .../docs/factories/primitive/EngineSpec.kt | 21 + .../docs/factories/primitive/V8Engine.kt | 13 + .../http/client/bind/ClientBindController.kt | 21 + .../bind/annotation/AnnotationBinderSpec.kt | 23 + .../http/client/bind/annotation/Metadata.kt | 13 + .../client/bind/annotation/MetadataClient.kt | 13 + .../MetadataClientArgumentBinder.kt | 31 + .../client/bind/method/MethodBinderSpec.kt | 20 + .../client/bind/method/NameAuthorization.kt | 15 + .../bind/method/NameAuthorizationBinder.kt | 29 + .../bind/method/NameAuthorizedClient.kt | 14 + .../http/client/bind/type/CustomBinderSpec.kt | 18 + .../docs/http/client/bind/type/Metadata.kt | 3 + .../http/client/bind/type/MetadataClient.kt | 13 + .../bind/type/MetadataClientArgumentBinder.kt | 28 + .../docs/http/client/proxy/ProxyFilter.kt | 42 + .../bind/ShoppingCartControllerTests.kt | 59 + .../server/bind/annotation/ShoppingCart.kt | 14 + .../bind/annotation/ShoppingCartController.kt | 16 + .../ShoppingCartRequestArgumentBinder.kt | 44 + .../http/server/bind/type/ShoppingCart.kt | 11 + .../bind/type/ShoppingCartController.kt | 18 + .../type/ShoppingCartRequestArgumentBinder.kt | 43 + .../http/server/executeon/PersonController.kt | 22 + .../netty/websocket/ChatClientWebSocket.kt | 67 + .../netty/websocket/ChatServerWebSocket.kt | 56 + .../http/server/netty/websocket/Message.kt | 46 + .../websocket/PojoChatClientWebSocket.kt | 56 + .../ReactivePojoChatServerWebSocket.kt | 62 + .../http/server/reactive/PersonController.kt | 31 + .../http/server/reactive/PersonService.kt | 11 + .../server/secondary/SecondaryNettyServer.kt | 56 + .../server/secondary/SecondaryServerTest.kt | 44 + .../http/server/stream/StreamController.kt | 32 + .../server/stream/StreamControllerSpec.kt | 60 + .../BindHttpClientExceptionBodySpec.kt | 70 + .../docs/httpclientexceptionbody/Book.kt | 21 + .../BooksController.kt | 44 + .../httpclientexceptionbody/CustomError.kt | 21 + .../httpclientexceptionbody/OtherError.kt | 21 + .../kotlin/io/micronaut/docs/i18n/I18nSpec.kt | 57 + .../docs/i18n/MessageSourceFactory.kt | 14 + .../AnnotationInheritanceSpec.kt | 20 + .../anninheritance/BaseSqlRepository.kt | 6 + .../inject/anninheritance/BookRepository.kt | 11 + .../inject/anninheritance/SqlRepository.kt | 19 + .../docs/inject/generics/CylinderProvider.kt | 7 + .../micronaut/docs/inject/generics/Engine.kt | 14 + .../io/micronaut/docs/inject/generics/V6.kt | 7 + .../docs/inject/generics/V6Engine.kt | 11 + .../io/micronaut/docs/inject/generics/V8.kt | 7 + .../docs/inject/generics/V8Engine.kt | 11 + .../micronaut/docs/inject/generics/Vehicle.kt | 21 + .../docs/inject/generics/VehicleSpec.kt | 15 + .../io/micronaut/docs/inject/intro/Engine.kt | 25 + .../micronaut/docs/inject/intro/V8Engine.kt | 30 + .../io/micronaut/docs/inject/intro/Vehicle.kt | 27 + .../docs/inject/intro/VehicleSpec.kt | 21 + .../docs/inject/qualifiers/named/Engine.kt | 23 + .../docs/inject/qualifiers/named/V6Engine.kt | 30 + .../docs/inject/qualifiers/named/V8Engine.kt | 31 + .../docs/inject/qualifiers/named/Vehicle.kt | 31 + .../inject/qualifiers/named/VehicleSpec.kt | 21 + .../docs/inject/scope/RefreshEventSpec.kt | 108 + .../io/micronaut/docs/inject/typed/Engine.kt | 8 + .../micronaut/docs/inject/typed/EngineSpec.kt | 27 + .../micronaut/docs/inject/typed/V8Engine.kt | 16 + .../docs/injectionpoint/CrankShaft.kt | 23 + .../docs/injectionpoint/Cylinders.kt | 23 + .../micronaut/docs/injectionpoint/Engine.kt | 22 + .../docs/injectionpoint/EngineFactory.kt | 42 + .../micronaut/docs/injectionpoint/V6Engine.kt | 26 + .../micronaut/docs/injectionpoint/V8Engine.kt | 26 + .../micronaut/docs/injectionpoint/Vehicle.kt | 27 + .../docs/injectionpoint/VehicleSpec.kt | 22 + .../io/micronaut/docs/ioc/beans/Business.kt | 36 + .../docs/ioc/beans/IntrospectionSpec.kt | 68 + .../micronaut/docs/ioc/beans/Manufacturer.kt | 24 + .../io/micronaut/docs/ioc/beans/Person.kt | 27 + .../docs/ioc/beans/PersonConfiguration.kt | 23 + .../io/micronaut/docs/ioc/beans/Vehicle.kt | 30 + .../io/micronaut/docs/ioc/scopes/Car.kt | 18 + .../io/micronaut/docs/ioc/scopes/Driver.kt | 30 + .../micronaut/docs/ioc/validation/Person.kt | 28 + .../docs/ioc/validation/PersonService.kt | 30 + .../docs/ioc/validation/PersonServiceSpec.kt | 28 + .../ioc/validation/custom/DurationPattern.kt | 29 + .../custom/DurationPatternValidator.kt | 33 + .../custom/DurationPatternValidatorSpec.kt | 26 + .../ioc/validation/custom/HolidayService.kt | 32 + .../validation/custom/MyValidatorFactory.kt | 36 + .../docs/ioc/validation/custom/TimeOff.kt | 27 + .../docs/ioc/validation/pojo/PersonService.kt | 33 + .../ioc/validation/pojo/PersonServiceSpec.kt | 46 + .../io/micronaut/docs/lifecycle/Connection.kt | 30 + .../docs/lifecycle/ConnectionFactory.kt | 33 + .../io/micronaut/docs/lifecycle/Engine.kt | 23 + .../docs/lifecycle/PreDestroyBean.kt | 34 + .../docs/lifecycle/PreDestroyBeanSpec.kt | 25 + .../io/micronaut/docs/lifecycle/V8Engine.kt | 43 + .../io/micronaut/docs/lifecycle/Vehicle.kt | 29 + .../micronaut/docs/lifecycle/VehicleSpec.kt | 26 + .../netty/LogbookNettyClientCustomizer.kt | 42 + .../netty/LogbookNettyServerCustomizer.kt | 42 + .../docs/qualifiers/annotation/Engine.kt | 23 + .../docs/qualifiers/annotation/V6Engine.kt | 29 + .../docs/qualifiers/annotation/V8.kt | 27 + .../docs/qualifiers/annotation/V8Engine.kt | 29 + .../docs/qualifiers/annotation/Vehicle.kt | 31 + .../docs/qualifiers/annotation/VehicleSpec.kt | 22 + .../qualifiers/annotationmember/Cylinders.kt | 17 + .../qualifiers/annotationmember/Engine.kt | 8 + .../qualifiers/annotationmember/V6Engine.kt | 17 + .../qualifiers/annotationmember/V8Engine.kt | 16 + .../qualifiers/annotationmember/Vehicle.kt | 13 + .../annotationmember/VehicleSpec.kt | 18 + .../micronaut/docs/qualifiers/any/Vehicle.kt | 24 + .../docs/qualifiers/any/VehicleSpec.kt | 24 + .../defaultimpl/CustomResponseStrategy.kt | 25 + .../defaultimpl/DefaultImplementationSpec.kt | 17 + .../defaultimpl/DefaultResponseStrategy.kt | 23 + .../replaces/defaultimpl/ResponseStrategy.kt | 23 + .../reactor/ReactorContextPropagationSpec.kt | 156 ++ .../io/micronaut/docs/replaces/BookFactory.kt | 37 + .../io/micronaut/docs/replaces/BookService.kt | 22 + .../docs/replaces/CustomBookFactory.kt | 34 + .../docs/replaces/JdbcBookService.kt | 48 + .../docs/replaces/MockBookService.kt | 35 + .../micronaut/docs/replaces/RequiresSpec.kt | 19 + .../io/micronaut/docs/replaces/TextBook.kt | 20 + .../docs/replaces/TextBookFactory.kt | 33 + .../kotlin/io/micronaut/docs/requires/Book.kt | 18 + .../io/micronaut/docs/requires/BookService.kt | 20 + .../docs/requires/JdbcBookService.kt | 47 + .../micronaut/docs/requires/RequiresJdbc.kt | 30 + .../respondingnotfound/BooksController.kt | 38 + .../docs/server/binding/BindingController.kt | 109 + .../server/binding/BindingControllerTest.kt | 78 + .../docs/server/binding/BookmarkController.kt | 34 + .../server/binding/BookmarkControllerTest.kt | 31 + .../docs/server/binding/MovieTicketBean.kt | 35 + .../server/binding/MovieTicketController.kt | 38 + .../binding/MovieTicketControllerTest.kt | 30 + .../docs/server/binding/PaginationCommand.kt | 48 + .../docs/server/body/MessageController.kt | 56 + .../docs/server/body/MessageControllerSpec.kt | 40 + .../server/consumes/ConsumesController.kt | 48 + .../server/consumes/ConsumesControllerSpec.kt | 62 + .../docs/server/endpoint/AlertsEndpoint.kt | 38 + .../server/endpoint/AlertsEndpointSpec.kt | 57 + .../server/endpoint/CurrentDateEndpoint.kt | 81 + .../endpoint/CurrentDateEndpointSpec.kt | 80 + .../docs/server/endpoint/MessageEndpoint.kt | 79 + .../server/endpoint/MessageEndpointSpec.kt | 63 + .../docs/server/exception/BookController.kt | 35 + .../server/exception/ExceptionHandlerSpec.kt | 43 + .../server/exception/OutOfStockException.kt | 20 + .../exception/OutOfStockExceptionHandler.kt | 48 + .../docs/server/filters/TraceFilter.kt | 46 + .../docs/server/filters/TraceFilterSpec.kt | 30 + .../docs/server/filters/TraceService.kt | 44 + .../docs/server/intro/Application.kt | 30 + .../docs/server/intro/HelloClient.kt | 38 + .../docs/server/intro/HelloClientSpec.kt | 30 + .../docs/server/intro/HelloController.kt | 35 + .../docs/server/intro/HelloControllerSpec.kt | 48 + .../io/micronaut/docs/server/json/Person.kt | 30 + .../docs/server/json/PersonController.kt | 137 ++ .../docs/server/json/PersonControllerSpec.kt | 116 + .../docs/server/request/MessageController.kt | 75 + .../server/request/MessageControllerSpec.kt | 39 + .../server/response/ProducesController.kt | 48 + .../server/response/ProducesControllerSpec.kt | 37 + .../docs/server/response/StatusController.kt | 51 + .../server/response/StatusControllerSpec.kt | 41 + .../docs/server/routes/IssuesController.kt | 33 + .../server/routes/IssuesControllerTest.kt | 52 + .../micronaut/docs/server/routes/MyRoutes.kt | 37 + .../docs/server/routes/MyRoutesSpec.kt | 28 + .../routing/BackwardCompatibleController.kt | 34 + .../BackwardCompatibleControllerSpec.kt | 46 + .../io/micronaut/docs/server/sse/Headline.kt | 31 + .../docs/server/sse/HeadlineController.kt | 59 + .../docs/server/sse/HeadlineControllerSpec.kt | 43 + .../docs/server/suspend/MyContext.kt | 26 + .../server/suspend/MyContextInterceptorAnn.kt | 30 + .../docs/server/suspend/Repository.kt | 25 + .../docs/server/suspend/SuspendClient.kt | 47 + .../docs/server/suspend/SuspendController.kt | 189 ++ .../server/suspend/SuspendControllerSpec.kt | 272 +++ .../docs/server/suspend/SuspendFilter.kt | 24 + .../docs/server/suspend/SuspendInterceptor.kt | 49 + .../docs/server/suspend/SuspendRepository.kt | 24 + .../suspend/SuspendRepositoryInterceptor.kt | 46 + .../server/suspend/SuspendRepositorySpec.kt | 51 + .../suspend/SuspendRequestScopedService.kt | 9 + .../docs/server/suspend/SuspendService.kt | 90 + .../suspend/SuspendServiceInterceptorSpec.kt | 40 + .../multiple/CoroutineCrudRepository.kt | 101 + .../suspend/multiple/CustomRepository.kt | 32 + .../suspend/multiple/InterceptorSpec.kt | 61 + .../server/suspend/multiple/MyRepository.kt | 25 + .../multiple/MyRepositoryInterceptorImpl.kt | 46 + .../docs/server/suspend/multiple/MyService.kt | 35 + .../server/suspend/multiple/SomeEntity.kt | 4 + .../server/suspend/multiple/Transaction1.kt | 30 + .../multiple/Transaction1Interceptor.kt | 52 + .../server/suspend/multiple/Transaction2.kt | 30 + .../multiple/Transaction2Interceptor.kt | 51 + .../server/upload/BytesUploadController.kt | 45 + .../upload/CompletedUploadController.kt | 45 + .../docs/server/upload/UploadController.kt | 77 + .../server/upload/UploadControllerSpec.kt | 172 ++ .../upload/WholeBodyUploadController.kt | 63 + .../docs/server/uris/UriTemplateTest.kt | 19 + .../kotlin/io/micronaut/docs/session/Cart.kt | 23 + .../docs/session/ShoppingController.kt | 66 + .../docs/session/ShoppingControllerSpec.kt | 59 + .../io/micronaut/docs/sse/HeadlineClient.kt | 32 + .../micronaut/docs/sse/HeadlineController.kt | 45 + .../docs/sse/HeadlineControllerSpec.kt | 30 + .../io/micronaut/docs/streaming/Headline.kt | 20 + .../docs/streaming/HeadlineClient.kt | 39 + .../docs/streaming/HeadlineController.kt | 44 + .../docs/streaming/HeadlineControllerSpec.kt | 80 + .../docs/streaming/HeadlineFlowClient.kt | 34 + .../docs/streaming/HeadlineFlowController.kt | 56 + .../streaming/HeadlineFlowControllerSpec.kt | 55 + .../web/router/version/VersionedController.kt | 51 + .../micronaut/docs/whatsNew/CacheFactory.kt | 39 + .../docs/writable/TemplateController.kt | 58 + .../micronaut/http/client/GreetingClient.kt | 27 + .../io/micronaut/http/client/SuspendClient.kt | 19 + .../http/client/SuspendClientController.kt | 24 + .../http/client/SuspendClientSpec.kt | 45 + .../http/server/WebSocketSuspendTest.kt | 65 + .../server/upload/KotlinUploadController.kt | 43 + .../upload/KotlinUploadControllerSpec.kt | 59 + .../inject/constructor/nullableinjection/A.kt | 18 + .../inject/constructor/nullableinjection/B.kt | 21 + .../inject/constructor/nullableinjection/C.kt | 21 + .../ConstructorNullableInjectionSpec.kt | 45 + .../inject/field/nullableinjection/A.kt | 18 + .../inject/field/nullableinjection/B.kt | 25 + .../FieldNullableInjectionSpec.kt | 32 + .../inject/method/nullableinjection/A.kt | 18 + .../inject/method/nullableinjection/B.kt | 25 + .../inject/method/nullableinjection/C.kt | 28 + .../SetterWithNullableSpec.kt | 43 + .../inject/property/BeanWithProperty.kt | 41 + .../micronaut/inject/property/ConfigProps.kt | 36 + .../inject/property/MapFormatSpec.kt | 50 + .../inject/property/PropertyInjectSpec.kt | 18 + .../inject/repeatable/MultipleRequires.kt | 25 + .../inject/repeatable/RepeatableSpec.kt | 44 + .../inject/requires/RequiresFuture.kt | 23 + .../micronaut/inject/requires/RequiresOld.kt | 23 + .../inject/requires/RequiresSdkSpec.kt | 34 + .../visitor/beans/BeanIntrospectorSpec.kt | 38 + .../inject/visitor/beans/TestBean.kt | 27 + .../io/micronaut/retry/MyCustomException.kt | 20 + .../kotlin/io/micronaut/retry/RetrySpec.kt | 96 + .../runtime/event/EventListenerContract.kt | 9 + .../runtime/event/EventListenerImpl.kt | 13 + .../runtime/event/EventListenerSpec.groovy | 25 + .../io/micronaut/runtime/event/MyEvent.kt | 5 + .../instrument/CoroutineController.kt | 56 + .../MultipleInvocationInstrumenterSpec.kt | 31 + .../micronaut/validation/validator/Person.kt | 26 + .../validation/validator/ValidatorSpec.kt | 36 + .../org/atinject/jakartatck/auto/Car.kt | 18 + .../atinject/jakartatck/auto/Convertible.kt | 538 +++++ .../org/atinject/jakartatck/auto/Drivers.kt | 22 + .../atinject/jakartatck/auto/DriversSeat.kt | 23 + .../org/atinject/jakartatck/auto/Engine.kt | 72 + .../org/atinject/jakartatck/auto/FuelTank.kt | 21 + .../org/atinject/jakartatck/auto/GasEngine.kt | 30 + .../org/atinject/jakartatck/auto/Seat.kt | 25 + .../org/atinject/jakartatck/auto/Seatbelt.kt | 18 + .../org/atinject/jakartatck/auto/Tire.kt | 186 ++ .../org/atinject/jakartatck/auto/V8Engine.kt | 61 + .../jakartatck/auto/accessories/Cupholder.kt | 26 + .../jakartatck/auto/accessories/RoundThing.kt | 43 + .../jakartatck/auto/accessories/SpareTire.kt | 128 + .../kotlin/org/atinject/javaxtck/auto/Car.kt | 18 + .../org/atinject/javaxtck/auto/Convertible.kt | 538 +++++ .../org/atinject/javaxtck/auto/Drivers.kt | 22 + .../org/atinject/javaxtck/auto/DriversSeat.kt | 23 + .../org/atinject/javaxtck/auto/Engine.kt | 72 + .../org/atinject/javaxtck/auto/FuelTank.kt | 21 + .../org/atinject/javaxtck/auto/GasEngine.kt | 30 + .../kotlin/org/atinject/javaxtck/auto/Seat.kt | 25 + .../org/atinject/javaxtck/auto/Seatbelt.kt | 18 + .../kotlin/org/atinject/javaxtck/auto/Tire.kt | 186 ++ .../org/atinject/javaxtck/auto/V8Engine.kt | 61 + .../javaxtck/auto/accessories/Cupholder.kt | 26 + .../javaxtck/auto/accessories/RoundThing.kt | 43 + .../javaxtck/auto/accessories/SpareTire.kt | 128 + .../docs/i18n/messages_en.properties | 2 + .../docs/i18n/messages_es.properties | 2 + .../src/test/resources/logback.xml | 14 + .../inject/method/nullableinjection/C.kt | 2 + validation/build.gradle | 2 + 740 files changed, 41159 insertions(+), 400 deletions(-) create mode 100644 core-processor/src/main/java/io/micronaut/inject/annotation/internal/KotlinDeprecatedTransformer.java create mode 100644 inject-kotlin-test/src/main/groovy/io/micronaut/annotation/processing/test/KotlinCompiler.java delete mode 100644 inject-kotlin-test/src/main/kotlin/io/micronaut/annotation/processing/test/KotlinCompileHelper.kt create mode 100644 inject-kotlin/build.gradle create mode 100644 inject-kotlin/gradle.properties create mode 100644 inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/KotlinOutputVisitor.kt create mode 100644 inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/annotation/KotlinAnnotationMetadataBuilder.kt create mode 100644 inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/annotation/KotlinElementAnnotationMetadataFactory.kt create mode 100644 inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/beans/BeanDefinitionProcessor.kt create mode 100644 inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/beans/BeanDefinitionProcessorProvider.kt create mode 100644 inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/extensions.kt create mode 100644 inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/AbstractKotlinElement.kt create mode 100644 inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/KSAnnotatedReference.kt create mode 100644 inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/KotlinClassElement.kt create mode 100644 inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/KotlinConstructorElement.kt create mode 100644 inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/KotlinElementFactory.kt create mode 100644 inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/KotlinEnumConstructorElement.kt create mode 100644 inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/KotlinEnumElement.kt create mode 100644 inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/KotlinFieldElement.kt create mode 100644 inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/KotlinGenericPlaceholderElement.kt create mode 100644 inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/KotlinMethodElement.kt create mode 100644 inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/KotlinParameterElement.kt create mode 100644 inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/KotlinPropertyElement.kt create mode 100644 inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/KotlinVisitorContext.kt create mode 100644 inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/KotlinWildcardElement.kt create mode 100644 inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/LoadedVisitor.kt create mode 100644 inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/TypeElementSymbolProcessor.kt create mode 100644 inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/TypeElementSymbolProcessorProvider.kt create mode 100644 inject-kotlin/src/main/resources/META-INF/services/com.google.devtools.ksp.processing.SymbolProcessorProvider create mode 100644 inject-kotlin/src/main/resources/notes.txt create mode 100644 inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/adapter/MethodAdapterSpec.groovy create mode 100644 inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/compile/AbstractClassIntroductionSpec.groovy create mode 100644 inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/compile/AnnotatedConstructorArgumentSpec.groovy create mode 100644 inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/compile/AroundCompileSpec.groovy create mode 100644 inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/compile/AroundConstructCompileSpec.groovy create mode 100644 inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/compile/ExecutableFactoryMethodSpec.groovy create mode 100644 inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/compile/FinalModifierSpec.groovy create mode 100644 inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/compile/GeneratedAnnotationSpec.groovy create mode 100644 inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/compile/InheritedAnnotationMetadataSpec.groovy create mode 100644 inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/compile/IntroductionAnnotationSpec.groovy create mode 100644 inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/compile/IntroductionCompileSpec.groovy create mode 100644 inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/compile/IntroductionGenericTypesSpec.groovy create mode 100644 inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/compile/IntroductionInnerInterfaceSpec.groovy create mode 100644 inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/compile/IntroductionWithAroundSpec.groovy create mode 100644 inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/compile/LifeCycleWithProxySpec.groovy create mode 100644 inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/compile/LifeCycleWithProxyTargetSpec.groovy create mode 100644 inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/compile/PostConstructInterceptorCompileSpec.groovy create mode 100644 inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/compile/ValidatedNonBeanSpec.groovy create mode 100644 inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/factory/AdviceDefinedOnConcreteClassFactorySpec.groovy create mode 100644 inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/factory/AdviceDefinedOnFactorySpec.groovy create mode 100644 inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/factory/AdviceDefinedOnInterfaceFactorySpec.groovy create mode 100644 inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/hotswap/ProxyHotswapSpec.groovy create mode 100644 inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/introduction/AbstractClassIntroductionAdviceSpec.groovy create mode 100644 inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/introduction/InterfaceIntroductionAdviceSpec.groovy create mode 100644 inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/introduction/IntroductionAdviceWithNewInterfaceSpec.groovy create mode 100644 inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/introduction/IntroductionOnConcreteClassSpec.groovy create mode 100644 inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/introduction/MappedIntroductionOnConcreteClassSpec.groovy create mode 100644 inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/introduction/MyRepoIntroductionSpec.groovy create mode 100644 inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/introduction/delegation/DelegatingIntroductionAdviceSpec.groovy create mode 100644 inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/introduction/with_around/IntroductionInnerInterfaceSpec.groovy create mode 100644 inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/introduction/with_around/IntroductionWithAroundOnConcreteClassSpec.groovy create mode 100644 inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/itfce/InterfaceMethodLevelAopSpec.groovy create mode 100644 inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/itfce/InterfaceTypeLevelSpec.groovy create mode 100644 inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/named/NamedAopAdviceSpec.groovy create mode 100644 inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/proxytarget/ProxyingMethodLevelAopSpec.groovy create mode 100644 inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/simple/SimpleClassMethodLevelAopSpec.groovy create mode 100644 inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/simple/SimpleClassTypeLevelAopSpec.groovy create mode 100644 inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/beans/AbstractBeanSpec.groovy create mode 100644 inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/beans/BeanDefinitionSpec.groovy create mode 100644 inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/beans/BeanRegistrationSpec.groovy create mode 100644 inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/beans/SingletonSpec.groovy create mode 100644 inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/beans/aliasfor/AliasForQualifierSpec.groovy create mode 100644 inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/beans/collect/InjectCollectionBeanSpec.groovy create mode 100644 inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/beans/configproperties/ConfigPropertiesInnerClassSpec.groovy create mode 100644 inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/beans/configproperties/ConfigurationPropertiesFactorySpec.groovy create mode 100644 inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/beans/configproperties/ConfigurationPropertiesSpec.groovy create mode 100644 inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/beans/configproperties/inheritance/ConfigurationPropertiesInheritanceSpec.groovy create mode 100644 inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/beans/executable/ExecutableBeanSpec.groovy create mode 100644 inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/beans/executable/ExecutableSpec.groovy create mode 100644 inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/beans/executable/inheritance/InheritedExecutableSpec.groovy create mode 100644 inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/beans/factory/beanannotation/PrototypeAnnotationSpec.groovy create mode 100644 inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/beans/factory/beanproperty/FactoryBeanFieldSpec.groovy create mode 100644 inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/beans/inheritance/InheritanceSingletonSpec.groovy create mode 100644 inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/inject/ast/ClassElementSpec.groovy create mode 100644 inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/inject/configproperties/ConfigPropertiesInnerClassSpec.groovy create mode 100644 inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/inject/configproperties/ConfigPropertiesParseSpec.groovy create mode 100644 inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/inject/configproperties/ConfigurationPropertiesBuilderSpec.groovy create mode 100644 inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/inject/configproperties/ConfigurationPropertiesFactorySpec.groovy create mode 100644 inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/inject/configproperties/ConfigurationPropertiesSpec.groovy create mode 100644 inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/inject/configproperties/ConfigurationPropertiesWithRawMapSpec.groovy create mode 100644 inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/inject/configproperties/ImmutableConfigurationPropertiesSpec.groovy create mode 100644 inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/inject/configproperties/InterfaceConfigurationPropertiesSpec.groovy create mode 100644 inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/inject/configproperties/PrimitiveConfigurationPropertiesSpec.groovy create mode 100644 inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/inject/configproperties/ValidatedConfigurationSpec.groovy create mode 100644 inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/inject/configproperties/VisibilityIssuesSpec.groovy create mode 100644 inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/inject/configproperties/itfce/ValidatedInterfaceConfigPropsSpec.groovy create mode 100644 inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/inject/generics/GenericTypeArgumentsSpec.groovy create mode 100644 inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/visitor/BeanIntrospectionSpec.groovy create mode 100644 inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/visitor/KotlinReconstructionSpec.groovy create mode 100644 inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/Logged.kt create mode 100644 inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/LoggedInterceptor.kt create mode 100644 inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/adapter/Test.kt create mode 100644 inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/compile/AroundConstructAnnTransformer.kt create mode 100644 inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/compile/NamedTestAnnMapper.kt create mode 100644 inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/compile/TestStereotypeAnnTransformer.kt create mode 100644 inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/factory/AnotherClass.kt create mode 100644 inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/factory/ConcreteClass.kt create mode 100644 inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/factory/ConcreteClassFactory.kt create mode 100644 inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/factory/InterfaceClass.kt create mode 100644 inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/factory/InterfaceFactory.kt create mode 100644 inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/factory/InterfaceImpl.kt create mode 100644 inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/factory/SessionFactoryFactory.kt create mode 100644 inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/hotswap/HotswappableProxyingClass.kt create mode 100644 inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/AbstractClass.kt create mode 100644 inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/AbstractCrudRepo.kt create mode 100644 inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/AbstractCustomAbstractCrudRepo.kt create mode 100644 inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/AbstractCustomCrudRepo.kt create mode 100644 inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/AbstractSuperClass.kt create mode 100644 inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/ChildIntroduction.kt create mode 100644 inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/ConcreteClass.kt create mode 100644 inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/CrudRepo.kt create mode 100644 inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/CustomCrudRepo.kt create mode 100644 inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/DeleteByIdCrudRepo.kt create mode 100644 inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/InjectParentInterface.kt create mode 100644 inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/InterfaceIntroductionClass.kt create mode 100644 inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/ListenerAdvice.kt create mode 100644 inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/ListenerAdviceInterceptor.kt create mode 100644 inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/ListenerAdviceMarker.kt create mode 100644 inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/ListenerAdviceMarkerMapper.kt create mode 100644 inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/Marker.kt create mode 100644 inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/MyRepo.kt create mode 100644 inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/MyRepo2.kt create mode 100644 inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/MyRepoIntroducer.kt create mode 100644 inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/NotImplemented.kt create mode 100644 inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/NotImplementedAdvice.kt create mode 100644 inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/ParentInterface.kt create mode 100644 inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/RepoDef.kt create mode 100644 inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/Stub.kt create mode 100644 inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/StubIntroducer.kt create mode 100644 inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/SuperInterface.kt create mode 100644 inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/SuperRepo.kt create mode 100644 inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/delegation/Delegating.kt create mode 100644 inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/delegation/DelegatingImpl.kt create mode 100644 inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/delegation/DelegatingInterceptor.kt create mode 100644 inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/delegation/DelegatingIntroduced.kt create mode 100644 inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/delegation/DelegationAdvice.kt create mode 100644 inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/temp/MyBean.kt create mode 100644 inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/with_around/CustomProxy.kt create mode 100644 inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/with_around/MyBean1.kt create mode 100644 inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/with_around/MyBean2.kt create mode 100644 inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/with_around/MyBean3.kt create mode 100644 inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/with_around/MyBean4.kt create mode 100644 inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/with_around/MyBean5.kt create mode 100644 inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/with_around/MyBean6.kt create mode 100644 inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/with_around/MyBean7.kt create mode 100644 inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/with_around/MyBean8.kt create mode 100644 inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/with_around/MyBean9.kt create mode 100644 inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/with_around/ObservableInterceptor.kt create mode 100644 inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/with_around/ProxyAdviceInterceptor.kt create mode 100644 inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/with_around/ProxyAround.kt create mode 100644 inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/with_around/ProxyAroundInterceptor.kt create mode 100644 inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/with_around/ProxyIntroduction.kt create mode 100644 inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/with_around/ProxyIntroductionAndAround.kt create mode 100644 inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/with_around/ProxyIntroductionAndAroundAndIntrospected.kt create mode 100644 inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/with_around/ProxyIntroductionAndAroundAndIntrospectedAndExecutable.kt create mode 100644 inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/with_around/ProxyIntroductionAndAroundOneAnnotation.kt create mode 100644 inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/with_around/ProxyIntroductionInterceptor.kt create mode 100644 inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/itfce/AbstractInterfaceImpl.kt create mode 100644 inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/itfce/AbstractInterfaceTypeLevel.kt create mode 100644 inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/itfce/InterfaceClass.kt create mode 100644 inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/itfce/InterfaceImpl.kt create mode 100644 inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/itfce/InterfaceTypeLevel.kt create mode 100644 inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/itfce/InterfaceTypeLevelImpl.kt create mode 100644 inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/named/Config.kt create mode 100644 inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/named/NamedFactory.kt create mode 100644 inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/named/NamedInterface.kt create mode 100644 inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/named/OtherBean.kt create mode 100644 inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/named/OtherInterface.kt create mode 100644 inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/proxytarget/ArgMutatingInterceptor.kt create mode 100644 inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/proxytarget/Mutating.kt create mode 100644 inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/proxytarget/ProxyingClass.kt create mode 100644 inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/simple/AnotherClass.kt create mode 100644 inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/simple/ArgMutatingInterceptor.kt create mode 100644 inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/simple/Bar.kt create mode 100644 inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/simple/CovariantClass.kt create mode 100644 inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/simple/Invalid.kt create mode 100644 inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/simple/InvalidInterceptor.kt create mode 100644 inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/simple/Mutating.kt create mode 100644 inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/simple/SimpleClass.kt create mode 100644 inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/simple/TestBinding.kt create mode 100644 inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/beans/aliasfor/TestAnnotation.kt create mode 100644 inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/beans/collect/MyIterable.kt create mode 100644 inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/beans/collect/MySetOfStrings.kt create mode 100644 inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/beans/collect/ThingThatNeedsMyIterable.kt create mode 100644 inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/beans/collect/ThingThatNeedsMySetOfStrings.kt create mode 100644 inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/beans/configproperties/AnnWithClass.kt create mode 100644 inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/beans/configproperties/MapProperties.kt create mode 100644 inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/beans/configproperties/MyConfig.kt create mode 100644 inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/beans/configproperties/MyConfigInner.kt create mode 100644 inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/beans/configproperties/Neo4jProperties.kt create mode 100644 inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/beans/configproperties/Neo4jPropertiesFactory.kt create mode 100644 inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/beans/configproperties/Pojo.kt create mode 100644 inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/beans/configproperties/RecConf.kt create mode 100644 inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/beans/configproperties/ValidatedConfig.kt create mode 100644 inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/beans/configproperties/inheritance/ChildConfig.kt create mode 100644 inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/beans/configproperties/inheritance/MyConfig.kt create mode 100644 inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/beans/configproperties/inheritance/MyOtherConfig.kt create mode 100644 inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/beans/configproperties/inheritance/ParentArrayEachProps.kt create mode 100644 inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/beans/configproperties/inheritance/ParentArrayEachPropsCtor.kt create mode 100644 inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/beans/configproperties/inheritance/ParentEachProps.kt create mode 100644 inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/beans/configproperties/inheritance/ParentEachPropsCtor.kt create mode 100644 inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/beans/configproperties/inheritance/ParentPojo.kt create mode 100644 inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/beans/configuration/Engine.kt create mode 100644 inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/beans/executable/BookController.kt create mode 100644 inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/beans/executable/BookService.kt create mode 100644 inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/beans/executable/RepeatableExecutable.kt create mode 100644 inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/beans/factory/beanannotation/A.kt create mode 100644 inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/elementapi/EntityAnnotationMapper.kt create mode 100644 inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/elementapi/Foo.kt create mode 100644 inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/elementapi/GenBase.kt create mode 100644 inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/elementapi/MarkerAnnotation.kt create mode 100644 inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/elementapi/OtherTestBean.kt create mode 100644 inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/elementapi/OuterBean.kt create mode 100644 inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/elementapi/SomeEnum.kt create mode 100644 inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/elementapi/TestBean.kt create mode 100644 inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/elementapi/TestClass.kt create mode 100644 inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/elementapi/TestEntity.kt create mode 100644 inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/inject/configproperties/ChildConfigPropertiesX.kt create mode 100644 inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/inject/configproperties/MapProperties.kt create mode 100644 inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/inject/configproperties/MyConfig.kt create mode 100644 inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/inject/configproperties/MyConfigInner.kt create mode 100644 inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/inject/configproperties/MyHibernateConfig.kt create mode 100644 inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/inject/configproperties/MyHibernateConfig2.kt create mode 100644 inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/inject/configproperties/MyPrimitiveConfig.kt create mode 100644 inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/inject/configproperties/Neo4jProperties.kt create mode 100644 inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/inject/configproperties/Neo4jPropertiesFactory.kt create mode 100644 inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/inject/configproperties/Pojo.kt create mode 100644 inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/inject/configproperties/RecConf.kt create mode 100644 inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/inject/configproperties/ValidatedConfig.kt create mode 100644 inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/inject/configproperties/inheritance/ChildConfig.kt create mode 100644 inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/inject/configproperties/inheritance/MyConfig.kt create mode 100644 inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/inject/configproperties/inheritance/MyOtherConfig.kt create mode 100644 inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/inject/configproperties/inheritance/ParentArrayEachProps.kt create mode 100644 inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/inject/configproperties/inheritance/ParentArrayEachPropsCtor.kt create mode 100644 inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/inject/configproperties/inheritance/ParentEachProps.kt create mode 100644 inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/inject/configproperties/inheritance/ParentEachPropsCtor.kt create mode 100644 inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/inject/configproperties/inheritance/ParentPojo.kt create mode 100644 inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/inject/configproperties/itfce/MyConfig.kt create mode 100644 inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/inject/configproperties/itfce/MyEachConfig.kt create mode 100644 inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/inject/configproperties/other/ParentConfigProperties.kt create mode 100644 inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/inject/configuration/Engine.kt create mode 100644 inject-kotlin/src/test/resources/META-INF/services/io.micronaut.inject.annotation.AnnotationMapper create mode 100644 inject-kotlin/src/test/resources/META-INF/services/io.micronaut.inject.annotation.AnnotationTransformer create mode 100644 inject-kotlin/src/test/resources/logback.xml create mode 100644 test-suite-kotlin-ksp/build.gradle create mode 100644 test-suite-kotlin-ksp/gradle.properties create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/annotation/processing/DemoController.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/annotation/processing/SuspendFunctionInterceptorSpec.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/annotation/processing/SuspendMethodSpec.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/core/beans/AbstractTestEntity.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/core/beans/Item.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/core/beans/KotlinBeanIntrospectionSpec.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/core/beans/RecusiveGenericsSpec.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/core/beans/SomeEntity.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/core/beans/TestEntity.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/core/beans/TestEntity2.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/annotation/Pet.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/annotation/PetClient.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/annotation/PetController.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/annotation/PetControllerSpec.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/annotation/PetOperations.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/annotation/headers/HeaderSpec.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/annotation/headers/PetClient.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/annotation/headers/PetController.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/annotation/requestattributes/RequestAttributeSpec.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/annotation/requestattributes/Story.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/annotation/requestattributes/StoryClient.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/annotation/requestattributes/StoryClientFilter.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/annotation/requestattributes/StoryController.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/annotation/retry/PetClient.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/annotation/retry/PetFallback.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/aop/advice/MyBean.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/aop/advice/Timed.java create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/aop/advice/method/MyFactory.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/aop/advice/type/MyFactory.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/aop/around/AroundSpec.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/aop/around/NotNull.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/aop/around/NotNullExample.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/aop/around/NotNullInterceptor.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/aop/introduction/IntroductionSpec.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/aop/introduction/Stub.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/aop/introduction/StubExample.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/aop/introduction/StubIntroduction.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/aop/lifecycle/LifeCycleAdviseSpec.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/aop/lifecycle/Product.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/aop/lifecycle/ProductBean.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/aop/lifecycle/ProductInterceptors.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/aop/lifecycle/ProductService.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/aop/retry/Book.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/aop/retry/BookService.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/aop/scheduled/ScheduledExample.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/aop/scheduled/TaskSchedulerInjectExample.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/basics/Book.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/basics/BookController.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/basics/BookControllerSpec.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/basics/HelloController.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/basics/HelloControllerSpec.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/basics/Message.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/client/ThirdPartyClientFilterSpec.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/client/filter/BasicAuth.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/client/filter/BasicAuthClient.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/client/filter/BasicAuthClientFilter.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/client/filter/BasicAuthFilterSpec.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/client/filter/GoogleAuthFilter.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/client/upload/MultipartFileUploadSpec.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/client/versioning/HelloClient.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/builder/CrankShaft.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/builder/Engine.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/builder/EngineConfig.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/builder/EngineFactory.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/builder/EngineImpl.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/builder/SparkPlug.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/builder/Vehicle.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/builder/VehicleSpec.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/converters/MapToLocalDateConverter.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/converters/MyConfigurationProperties.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/converters/MyConfigurationPropertiesSpec.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/env/DataSourceConfiguration.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/env/DataSourceFactory.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/env/EachBeanTest.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/env/EachPropertyTest.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/env/EnvironmentTest.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/env/HighRateLimit.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/env/LowRateLimit.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/env/OrderTest.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/env/RateLimit.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/env/RateLimitsConfiguration.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/env/RateLimitsFactory.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/immutable/Engine.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/immutable/EngineConfig.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/immutable/Vehicle.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/immutable/VehicleSpec.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/itfce/Engine.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/itfce/EngineConfig.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/itfce/Vehicle.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/itfce/VehicleSpec.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/mapFormat/Engine.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/mapFormat/EngineConfig.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/mapFormat/EngineImpl.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/mapFormat/Vehicle.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/mapFormat/VehicleSpec.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/properties/Engine.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/properties/EngineConfig.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/properties/EngineImpl.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/properties/Vehicle.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/properties/VehicleSpec.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/property/Engine.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/property/EngineSpec.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/value/Engine.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/value/EngineImpl.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/value/Vehicle.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/value/VehicleSpec.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/context/Application.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/context/annotation/primary/Blue.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/context/annotation/primary/ColorPicker.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/context/annotation/primary/Green.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/context/annotation/primary/PrimarySpec.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/context/annotation/primary/TestController.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/context/env/DefaultEnvironmentSpec.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/context/env/EnvironmentSpec.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/context/events/SampleEvent.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/context/events/SampleEventEmitterBean.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/context/events/application/SampleEventListener.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/context/events/application/SampleEventListenerSpec.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/context/events/async/SampleEventListener.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/context/events/async/SampleEventListenerSpec.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/context/events/listener/SampleEventListener.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/context/events/listener/SampleEventListenerSpec.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/datavalidation/groups/Email.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/datavalidation/groups/EmailController.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/datavalidation/groups/EmailControllerSpec.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/datavalidation/groups/FinalValidation.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/datavalidation/params/EmailController.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/datavalidation/params/EmailControllerSpec.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/datavalidation/pogo/Email.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/datavalidation/pogo/EmailController.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/datavalidation/pogo/EmailControllerSpec.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/events/factory/Engine.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/events/factory/EngineFactory.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/events/factory/EngineInitializer.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/events/factory/V8Engine.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/events/factory/Vehicle.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/events/factory/VehicleSpec.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/factories/CrankShaft.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/factories/Engine.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/factories/EngineFactory.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/factories/V8Engine.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/factories/Vehicle.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/factories/VehicleMockSpec.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/factories/VehicleSpec.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/factories/nullable/Engine.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/factories/nullable/EngineConfiguration.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/factories/nullable/EngineFactory.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/factories/nullable/EngineSpec.java create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/factories/primitive/CylinderFactory.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/factories/primitive/EngineSpec.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/factories/primitive/V8Engine.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/client/bind/ClientBindController.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/client/bind/annotation/AnnotationBinderSpec.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/client/bind/annotation/Metadata.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/client/bind/annotation/MetadataClient.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/client/bind/annotation/MetadataClientArgumentBinder.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/client/bind/method/MethodBinderSpec.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/client/bind/method/NameAuthorization.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/client/bind/method/NameAuthorizationBinder.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/client/bind/method/NameAuthorizedClient.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/client/bind/type/CustomBinderSpec.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/client/bind/type/Metadata.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/client/bind/type/MetadataClient.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/client/bind/type/MetadataClientArgumentBinder.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/client/proxy/ProxyFilter.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/server/bind/ShoppingCartControllerTests.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/server/bind/annotation/ShoppingCart.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/server/bind/annotation/ShoppingCartController.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/server/bind/annotation/ShoppingCartRequestArgumentBinder.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/server/bind/type/ShoppingCart.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/server/bind/type/ShoppingCartController.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/server/bind/type/ShoppingCartRequestArgumentBinder.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/server/executeon/PersonController.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/server/netty/websocket/ChatClientWebSocket.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/server/netty/websocket/ChatServerWebSocket.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/server/netty/websocket/Message.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/server/netty/websocket/PojoChatClientWebSocket.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/server/netty/websocket/ReactivePojoChatServerWebSocket.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/server/reactive/PersonController.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/server/reactive/PersonService.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/server/secondary/SecondaryNettyServer.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/server/secondary/SecondaryServerTest.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/server/stream/StreamController.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/server/stream/StreamControllerSpec.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/httpclientexceptionbody/BindHttpClientExceptionBodySpec.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/httpclientexceptionbody/Book.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/httpclientexceptionbody/BooksController.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/httpclientexceptionbody/CustomError.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/httpclientexceptionbody/OtherError.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/i18n/I18nSpec.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/i18n/MessageSourceFactory.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/inject/anninheritance/AnnotationInheritanceSpec.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/inject/anninheritance/BaseSqlRepository.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/inject/anninheritance/BookRepository.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/inject/anninheritance/SqlRepository.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/inject/generics/CylinderProvider.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/inject/generics/Engine.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/inject/generics/V6.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/inject/generics/V6Engine.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/inject/generics/V8.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/inject/generics/V8Engine.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/inject/generics/Vehicle.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/inject/generics/VehicleSpec.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/inject/intro/Engine.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/inject/intro/V8Engine.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/inject/intro/Vehicle.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/inject/intro/VehicleSpec.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/inject/qualifiers/named/Engine.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/inject/qualifiers/named/V6Engine.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/inject/qualifiers/named/V8Engine.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/inject/qualifiers/named/Vehicle.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/inject/qualifiers/named/VehicleSpec.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/inject/scope/RefreshEventSpec.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/inject/typed/Engine.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/inject/typed/EngineSpec.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/inject/typed/V8Engine.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/injectionpoint/CrankShaft.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/injectionpoint/Cylinders.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/injectionpoint/Engine.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/injectionpoint/EngineFactory.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/injectionpoint/V6Engine.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/injectionpoint/V8Engine.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/injectionpoint/Vehicle.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/injectionpoint/VehicleSpec.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/ioc/beans/Business.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/ioc/beans/IntrospectionSpec.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/ioc/beans/Manufacturer.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/ioc/beans/Person.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/ioc/beans/PersonConfiguration.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/ioc/beans/Vehicle.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/ioc/scopes/Car.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/ioc/scopes/Driver.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/ioc/validation/Person.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/ioc/validation/PersonService.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/ioc/validation/PersonServiceSpec.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/ioc/validation/custom/DurationPattern.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/ioc/validation/custom/DurationPatternValidator.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/ioc/validation/custom/DurationPatternValidatorSpec.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/ioc/validation/custom/HolidayService.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/ioc/validation/custom/MyValidatorFactory.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/ioc/validation/custom/TimeOff.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/ioc/validation/pojo/PersonService.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/ioc/validation/pojo/PersonServiceSpec.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/lifecycle/Connection.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/lifecycle/ConnectionFactory.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/lifecycle/Engine.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/lifecycle/PreDestroyBean.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/lifecycle/PreDestroyBeanSpec.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/lifecycle/V8Engine.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/lifecycle/Vehicle.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/lifecycle/VehicleSpec.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/netty/LogbookNettyClientCustomizer.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/netty/LogbookNettyServerCustomizer.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/qualifiers/annotation/Engine.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/qualifiers/annotation/V6Engine.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/qualifiers/annotation/V8.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/qualifiers/annotation/V8Engine.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/qualifiers/annotation/Vehicle.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/qualifiers/annotation/VehicleSpec.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/qualifiers/annotationmember/Cylinders.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/qualifiers/annotationmember/Engine.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/qualifiers/annotationmember/V6Engine.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/qualifiers/annotationmember/V8Engine.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/qualifiers/annotationmember/Vehicle.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/qualifiers/annotationmember/VehicleSpec.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/qualifiers/any/Vehicle.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/qualifiers/any/VehicleSpec.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/qualifiers/replaces/defaultimpl/CustomResponseStrategy.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/qualifiers/replaces/defaultimpl/DefaultImplementationSpec.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/qualifiers/replaces/defaultimpl/DefaultResponseStrategy.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/qualifiers/replaces/defaultimpl/ResponseStrategy.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/reactor/ReactorContextPropagationSpec.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/replaces/BookFactory.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/replaces/BookService.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/replaces/CustomBookFactory.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/replaces/JdbcBookService.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/replaces/MockBookService.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/replaces/RequiresSpec.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/replaces/TextBook.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/replaces/TextBookFactory.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/requires/Book.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/requires/BookService.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/requires/JdbcBookService.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/requires/RequiresJdbc.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/respondingnotfound/BooksController.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/binding/BindingController.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/binding/BindingControllerTest.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/binding/BookmarkController.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/binding/BookmarkControllerTest.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/binding/MovieTicketBean.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/binding/MovieTicketController.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/binding/MovieTicketControllerTest.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/binding/PaginationCommand.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/body/MessageController.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/body/MessageControllerSpec.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/consumes/ConsumesController.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/consumes/ConsumesControllerSpec.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/endpoint/AlertsEndpoint.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/endpoint/AlertsEndpointSpec.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/endpoint/CurrentDateEndpoint.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/endpoint/CurrentDateEndpointSpec.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/endpoint/MessageEndpoint.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/endpoint/MessageEndpointSpec.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/exception/BookController.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/exception/ExceptionHandlerSpec.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/exception/OutOfStockException.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/exception/OutOfStockExceptionHandler.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/filters/TraceFilter.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/filters/TraceFilterSpec.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/filters/TraceService.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/intro/Application.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/intro/HelloClient.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/intro/HelloClientSpec.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/intro/HelloController.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/intro/HelloControllerSpec.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/json/Person.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/json/PersonController.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/json/PersonControllerSpec.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/request/MessageController.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/request/MessageControllerSpec.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/response/ProducesController.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/response/ProducesControllerSpec.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/response/StatusController.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/response/StatusControllerSpec.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/routes/IssuesController.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/routes/IssuesControllerTest.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/routes/MyRoutes.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/routes/MyRoutesSpec.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/routing/BackwardCompatibleController.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/routing/BackwardCompatibleControllerSpec.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/sse/Headline.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/sse/HeadlineController.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/sse/HeadlineControllerSpec.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/suspend/MyContext.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/suspend/MyContextInterceptorAnn.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/suspend/Repository.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/suspend/SuspendClient.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/suspend/SuspendController.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/suspend/SuspendControllerSpec.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/suspend/SuspendFilter.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/suspend/SuspendInterceptor.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/suspend/SuspendRepository.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/suspend/SuspendRepositoryInterceptor.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/suspend/SuspendRepositorySpec.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/suspend/SuspendRequestScopedService.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/suspend/SuspendService.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/suspend/SuspendServiceInterceptorSpec.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/suspend/multiple/CoroutineCrudRepository.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/suspend/multiple/CustomRepository.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/suspend/multiple/InterceptorSpec.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/suspend/multiple/MyRepository.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/suspend/multiple/MyRepositoryInterceptorImpl.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/suspend/multiple/MyService.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/suspend/multiple/SomeEntity.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/suspend/multiple/Transaction1.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/suspend/multiple/Transaction1Interceptor.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/suspend/multiple/Transaction2.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/suspend/multiple/Transaction2Interceptor.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/upload/BytesUploadController.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/upload/CompletedUploadController.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/upload/UploadController.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/upload/UploadControllerSpec.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/upload/WholeBodyUploadController.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/uris/UriTemplateTest.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/session/Cart.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/session/ShoppingController.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/session/ShoppingControllerSpec.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/sse/HeadlineClient.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/sse/HeadlineController.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/sse/HeadlineControllerSpec.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/streaming/Headline.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/streaming/HeadlineClient.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/streaming/HeadlineController.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/streaming/HeadlineControllerSpec.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/streaming/HeadlineFlowClient.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/streaming/HeadlineFlowController.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/streaming/HeadlineFlowControllerSpec.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/web/router/version/VersionedController.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/whatsNew/CacheFactory.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/writable/TemplateController.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/http/client/GreetingClient.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/http/client/SuspendClient.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/http/client/SuspendClientController.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/http/client/SuspendClientSpec.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/http/server/WebSocketSuspendTest.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/http/server/upload/KotlinUploadController.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/http/server/upload/KotlinUploadControllerSpec.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/inject/constructor/nullableinjection/A.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/inject/constructor/nullableinjection/B.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/inject/constructor/nullableinjection/C.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/inject/constructor/nullableinjection/ConstructorNullableInjectionSpec.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/inject/field/nullableinjection/A.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/inject/field/nullableinjection/B.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/inject/field/nullableinjection/FieldNullableInjectionSpec.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/inject/method/nullableinjection/A.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/inject/method/nullableinjection/B.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/inject/method/nullableinjection/C.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/inject/method/nullableinjection/SetterWithNullableSpec.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/inject/property/BeanWithProperty.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/inject/property/ConfigProps.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/inject/property/MapFormatSpec.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/inject/property/PropertyInjectSpec.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/inject/repeatable/MultipleRequires.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/inject/repeatable/RepeatableSpec.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/inject/requires/RequiresFuture.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/inject/requires/RequiresOld.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/inject/requires/RequiresSdkSpec.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/inject/visitor/beans/BeanIntrospectorSpec.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/inject/visitor/beans/TestBean.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/retry/MyCustomException.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/retry/RetrySpec.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/runtime/event/EventListenerContract.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/runtime/event/EventListenerImpl.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/runtime/event/EventListenerSpec.groovy create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/runtime/event/MyEvent.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/scheduling/instrument/CoroutineController.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/scheduling/instrument/MultipleInvocationInstrumenterSpec.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/validation/validator/Person.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/validation/validator/ValidatorSpec.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/org/atinject/jakartatck/auto/Car.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/org/atinject/jakartatck/auto/Convertible.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/org/atinject/jakartatck/auto/Drivers.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/org/atinject/jakartatck/auto/DriversSeat.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/org/atinject/jakartatck/auto/Engine.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/org/atinject/jakartatck/auto/FuelTank.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/org/atinject/jakartatck/auto/GasEngine.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/org/atinject/jakartatck/auto/Seat.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/org/atinject/jakartatck/auto/Seatbelt.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/org/atinject/jakartatck/auto/Tire.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/org/atinject/jakartatck/auto/V8Engine.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/org/atinject/jakartatck/auto/accessories/Cupholder.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/org/atinject/jakartatck/auto/accessories/RoundThing.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/org/atinject/jakartatck/auto/accessories/SpareTire.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/org/atinject/javaxtck/auto/Car.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/org/atinject/javaxtck/auto/Convertible.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/org/atinject/javaxtck/auto/Drivers.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/org/atinject/javaxtck/auto/DriversSeat.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/org/atinject/javaxtck/auto/Engine.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/org/atinject/javaxtck/auto/FuelTank.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/org/atinject/javaxtck/auto/GasEngine.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/org/atinject/javaxtck/auto/Seat.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/org/atinject/javaxtck/auto/Seatbelt.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/org/atinject/javaxtck/auto/Tire.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/org/atinject/javaxtck/auto/V8Engine.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/org/atinject/javaxtck/auto/accessories/Cupholder.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/org/atinject/javaxtck/auto/accessories/RoundThing.kt create mode 100644 test-suite-kotlin-ksp/src/test/kotlin/org/atinject/javaxtck/auto/accessories/SpareTire.kt create mode 100644 test-suite-kotlin-ksp/src/test/resources/io/micronaut/docs/i18n/messages_en.properties create mode 100644 test-suite-kotlin-ksp/src/test/resources/io/micronaut/docs/i18n/messages_es.properties create mode 100644 test-suite-kotlin-ksp/src/test/resources/logback.xml diff --git a/core-processor/src/main/java/io/micronaut/inject/annotation/AbstractAnnotationMetadataBuilder.java b/core-processor/src/main/java/io/micronaut/inject/annotation/AbstractAnnotationMetadataBuilder.java index 9427dd4f4e5..ccc96bd62b3 100644 --- a/core-processor/src/main/java/io/micronaut/inject/annotation/AbstractAnnotationMetadataBuilder.java +++ b/core-processor/src/main/java/io/micronaut/inject/annotation/AbstractAnnotationMetadataBuilder.java @@ -1104,9 +1104,17 @@ private AnnotationMetadata buildInternalMulti( Optional value = annotationMetadata.stringValue(DefaultScope.class); value.ifPresent(name -> annotationMetadata.addDeclaredAnnotation(name, Collections.emptyMap())); } + if (annotationMetadata instanceof MutableAnnotationMetadata mutableAnnotationMetadata) { + postProcess(mutableAnnotationMetadata, element); + } return annotationMetadata; } + protected void postProcess(MutableAnnotationMetadata mutableAnnotationMetadata, + T element) { + //no-op + } + private void includeAnnotations(DefaultAnnotationMetadata annotationMetadata, T element, boolean originatingElementIsSameParent, diff --git a/core-processor/src/main/java/io/micronaut/inject/annotation/internal/KotlinDeprecatedTransformer.java b/core-processor/src/main/java/io/micronaut/inject/annotation/internal/KotlinDeprecatedTransformer.java new file mode 100644 index 00000000000..95579e731dd --- /dev/null +++ b/core-processor/src/main/java/io/micronaut/inject/annotation/internal/KotlinDeprecatedTransformer.java @@ -0,0 +1,43 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.inject.annotation.internal; + +import io.micronaut.core.annotation.AnnotationValue; +import io.micronaut.inject.annotation.NamedAnnotationTransformer; +import io.micronaut.inject.visitor.VisitorContext; + +import java.lang.annotation.Annotation; +import java.util.Collections; +import java.util.List; + +/** + * Allows treating the Kotlin deprecated annotation as the Java one. + * + * @since 4.0.0 + */ +public final class KotlinDeprecatedTransformer implements NamedAnnotationTransformer { + @Override + public String getName() { + return "kotlin.Deprecated"; + } + + @Override + public List> transform(AnnotationValue annotation, VisitorContext visitorContext) { + return Collections.singletonList( + AnnotationValue.builder(Deprecated.class).build() + ); + } +} diff --git a/core-processor/src/main/java/io/micronaut/inject/ast/ElementFactory.java b/core-processor/src/main/java/io/micronaut/inject/ast/ElementFactory.java index 20478e793a4..6ffc5e37ec5 100644 --- a/core-processor/src/main/java/io/micronaut/inject/ast/ElementFactory.java +++ b/core-processor/src/main/java/io/micronaut/inject/ast/ElementFactory.java @@ -51,8 +51,10 @@ public interface ElementFactory { * @param resolvedGenerics The resolved generics * @return The class element * @since 4.0.0 + * @deprecated no longer used */ @NonNull + @Deprecated ClassElement newClassElement(@NonNull C type, @NonNull ElementAnnotationMetadataFactory annotationMetadataFactory, @NonNull Map resolvedGenerics); diff --git a/core-processor/src/main/java/io/micronaut/inject/ast/PropertyElement.java b/core-processor/src/main/java/io/micronaut/inject/ast/PropertyElement.java index cc0ada998a0..390cceb7412 100644 --- a/core-processor/src/main/java/io/micronaut/inject/ast/PropertyElement.java +++ b/core-processor/src/main/java/io/micronaut/inject/ast/PropertyElement.java @@ -127,6 +127,16 @@ default AccessKind getWriteAccessKind() { return AccessKind.METHOD; } + /** + * Does a this property override the given property. Supported only with languages that have native properties. + * @param overridden The overridden method. + * @return True this property overrides the given property. + * @since 4.0.0 + */ + default boolean overrides(PropertyElement overridden) { + return false; + } + /** * The access type for bean properties. * @since 4.0.0 diff --git a/core-processor/src/main/java/io/micronaut/inject/ast/utils/EnclosedElementsQuery.java b/core-processor/src/main/java/io/micronaut/inject/ast/utils/EnclosedElementsQuery.java index a55912414a7..bd53ed6e6e5 100644 --- a/core-processor/src/main/java/io/micronaut/inject/ast/utils/EnclosedElementsQuery.java +++ b/core-processor/src/main/java/io/micronaut/inject/ast/utils/EnclosedElementsQuery.java @@ -19,18 +19,20 @@ import io.micronaut.core.annotation.Internal; import io.micronaut.core.annotation.NonNull; import io.micronaut.core.annotation.Nullable; +import io.micronaut.core.util.clhm.ConcurrentLinkedHashMap; import io.micronaut.inject.ast.ClassElement; import io.micronaut.inject.ast.ConstructorElement; +import io.micronaut.inject.ast.Element; import io.micronaut.inject.ast.ElementModifier; import io.micronaut.inject.ast.ElementQuery; import io.micronaut.inject.ast.FieldElement; import io.micronaut.inject.ast.MemberElement; import io.micronaut.inject.ast.MethodElement; +import io.micronaut.inject.ast.PropertyElement; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; -import java.util.HashMap; import java.util.Iterator; import java.util.LinkedHashSet; import java.util.List; @@ -51,7 +53,7 @@ @Internal public abstract class EnclosedElementsQuery { - private final Map elementsCache = new HashMap<>(); + private final Map elementsCache = new ConcurrentLinkedHashMap.Builder().maximumWeightedCapacity(200).build(); /** * Return the elements that match the given query. @@ -172,6 +174,8 @@ private boolean reduceElements(io.micronaut.inject.ast.Element newElement, if (!result.isIncludeOverriddenMethods()) { if (newElement instanceof MethodElement && existingElement instanceof MethodElement) { return ((MethodElement) newElement).overrides((MethodElement) existingElement); + } else if (newElement instanceof PropertyElement newPropertyElement && existingElement instanceof PropertyElement existingPropertyElement) { + return newPropertyElement.overrides(existingPropertyElement); } } return false; @@ -188,7 +192,13 @@ private Collection getAllElements(C classNode, Set addedFromClassElements = new LinkedHashSet<>(); classElements: for (N element : classElements) { - io.micronaut.inject.ast.Element newElement = elementsCache.computeIfAbsent(element, this::toAstElement); + N cacheKey = getCacheKey(element); + io.micronaut.inject.ast.Element newElement = elementsCache.computeIfAbsent(cacheKey, e -> this.toAstElement(e, result.getElementType())); + if (!result.getElementType().isInstance(newElement)) { + // dirty cache + elementsCache.remove(cacheKey); + newElement = elementsCache.computeIfAbsent(cacheKey, e -> this.toAstElement(e, result.getElementType())); + } for (Iterator iterator = elements.iterator(); iterator.hasNext(); ) { io.micronaut.inject.ast.Element existingElement = iterator.next(); if (newElement.equals(existingElement)) { @@ -208,6 +218,15 @@ private Collection getAllElements(C classNode, return elements; } + /** + * get the cache key. + * @param element The element + * @return The cache key + */ + protected N getCacheKey(N element) { + return element; + } + private void collectHierarchy(C classNode, boolean onlyDeclared, List> hierarchy, @@ -279,9 +298,10 @@ protected Set getExcludedNativeElements(@NonNull ElementQuery.Result resul * Converts the native element to the AST element. * * @param enclosedElement The native element. + * @param elementType The result type * @return The AST element */ @NonNull - protected abstract io.micronaut.inject.ast.Element toAstElement(N enclosedElement); + protected abstract io.micronaut.inject.ast.Element toAstElement(N enclosedElement, Class elementType); } diff --git a/core-processor/src/main/java/io/micronaut/inject/beans/visitor/BeanIntrospectionWriter.java b/core-processor/src/main/java/io/micronaut/inject/beans/visitor/BeanIntrospectionWriter.java index 0517e9f43e8..b45f3d3b1ca 100644 --- a/core-processor/src/main/java/io/micronaut/inject/beans/visitor/BeanIntrospectionWriter.java +++ b/core-processor/src/main/java/io/micronaut/inject/beans/visitor/BeanIntrospectionWriter.java @@ -829,7 +829,7 @@ private void writeInstantiateMethod(ClassWriter classWriter, MethodElement const private void invokeBeanConstructor(GeneratorAdapter writer, MethodElement constructor, BiConsumer argumentsPusher) { boolean isConstructor = constructor instanceof ConstructorElement; - boolean isCompanion = constructor != defaultConstructor && constructor.getDeclaringType().getSimpleName().endsWith("$Companion"); + boolean isCompanion = constructor.getDeclaringType().getSimpleName().endsWith("$Companion"); List constructorArguments = Arrays.asList(constructor.getParameters()); Collection argumentTypes = constructorArguments.stream().map(pe -> diff --git a/core-processor/src/main/java/io/micronaut/inject/processing/AbstractBeanElementCreator.java b/core-processor/src/main/java/io/micronaut/inject/processing/AbstractBeanElementCreator.java index e2fd4cfad59..04ce327a635 100644 --- a/core-processor/src/main/java/io/micronaut/inject/processing/AbstractBeanElementCreator.java +++ b/core-processor/src/main/java/io/micronaut/inject/processing/AbstractBeanElementCreator.java @@ -21,6 +21,7 @@ import io.micronaut.aop.internal.intercepted.InterceptedMethodUtil; import io.micronaut.aop.writer.AopProxyWriter; import io.micronaut.context.RequiresCondition; +import io.micronaut.context.annotation.Executable; import io.micronaut.context.annotation.Requires; import io.micronaut.core.annotation.AnnotationMetadata; import io.micronaut.core.annotation.AnnotationUtil; @@ -90,7 +91,7 @@ protected void visitAnnotationMetadata(BeanDefinitionVisitor writer, AnnotationM annotation.stringValue(RequiresCondition.MEMBER_BEAN_PROPERTY) .ifPresent(beanProperty -> { annotation.stringValue(RequiresCondition.MEMBER_BEAN) - .map(className -> visitorContext.getClassElement(className, visitorContext.getElementAnnotationMetadataFactory().readOnly()).get()) + .flatMap(className -> visitorContext.getClassElement(className, visitorContext.getElementAnnotationMetadataFactory().readOnly())) .ifPresent(classElement -> { String requiredValue = annotation.stringValue().orElse(null); String notEqualsValue = annotation.stringValue(RequiresCondition.MEMBER_NOT_EQUALS).orElse(null); @@ -121,6 +122,12 @@ protected boolean visitIntrospectedMethod(BeanDefinitionVisitor visitor, ClassEl || InterceptedMethodUtil.hasDeclaredAroundAdvice(methodElement.getAnnotationMetadata())) { addToIntroduction(aopProxyWriter, typeElement, methodElement, false); return true; + } else if (!methodElement.isAbstract() && methodElement.hasDeclaredStereotype(Executable.class)) { + aopProxyWriter.visitExecutableMethod( + typeElement, + methodElement, + visitorContext + ); } return false; } diff --git a/core-processor/src/main/java/io/micronaut/inject/processing/BeanDefinitionCreatorFactory.java b/core-processor/src/main/java/io/micronaut/inject/processing/BeanDefinitionCreatorFactory.java index 522358f2a24..8b386174870 100644 --- a/core-processor/src/main/java/io/micronaut/inject/processing/BeanDefinitionCreatorFactory.java +++ b/core-processor/src/main/java/io/micronaut/inject/processing/BeanDefinitionCreatorFactory.java @@ -15,6 +15,7 @@ */ package io.micronaut.inject.processing; +import io.micronaut.aop.Interceptor; import io.micronaut.aop.internal.intercepted.InterceptedMethodUtil; import io.micronaut.context.annotation.Bean; import io.micronaut.context.annotation.DefaultScope; @@ -70,6 +71,9 @@ public static BeanDefinitionCreator produce(ClassElement classElement, VisitorCo if (classElement.hasStereotype("groovy.lang.Singleton")) { throw new ProcessingException(classElement, "Class annotated with groovy.lang.Singleton instead of jakarta.inject.Singleton. Import jakarta.inject.Singleton to use Micronaut Dependency Injection."); } + if (classElement.isEnum()) { + throw new ProcessingException(classElement, "Enum types cannot be defined as beans"); + } return new DeclaredBeanElementCreator(classElement, visitorContext, false); } return Collections::emptyList; @@ -110,7 +114,7 @@ private static boolean containsInjectPoint(AnnotationMetadata annotationMetadata } private static boolean isAopProxyType(ClassElement classElement) { - return !classElement.isAssignable("io.micronaut.aop.Interceptor") && InterceptedMethodUtil.hasAroundStereotype(classElement.getAnnotationMetadata()); + return !classElement.isAssignable(Interceptor.class) && InterceptedMethodUtil.hasAroundStereotype(classElement.getAnnotationMetadata()); } public static boolean isDeclaredBeanInMetadata(AnnotationMetadata concreteClassMetadata) { diff --git a/core-processor/src/main/java/io/micronaut/inject/processing/DeclaredBeanElementCreator.java b/core-processor/src/main/java/io/micronaut/inject/processing/DeclaredBeanElementCreator.java index 1a8806df2c0..79846771093 100644 --- a/core-processor/src/main/java/io/micronaut/inject/processing/DeclaredBeanElementCreator.java +++ b/core-processor/src/main/java/io/micronaut/inject/processing/DeclaredBeanElementCreator.java @@ -158,6 +158,7 @@ private void build(BeanDefinitionVisitor visitor) { if (processAsProperties()) { memberQuery = memberQuery.excludePropertyElements(); for (PropertyElement propertyElement : classElement.getBeanProperties()) { + propertyElement.getField().ifPresent(processedFields::add); visitPropertyInternal(visitor, propertyElement); } } else { @@ -176,7 +177,7 @@ private void build(BeanDefinitionVisitor visitor) { visitFieldInternal(visitor, fieldElement); } else if (memberElement instanceof MethodElement methodElement) { visitMethodInternal(visitor, methodElement); - } else { + } else if (!(memberElement instanceof PropertyElement)) { throw new IllegalStateException("Unknown element"); } } diff --git a/core-processor/src/main/java/io/micronaut/inject/processing/FactoryBeanElementCreator.java b/core-processor/src/main/java/io/micronaut/inject/processing/FactoryBeanElementCreator.java index 7e11e77e6e5..9bdeb03146f 100644 --- a/core-processor/src/main/java/io/micronaut/inject/processing/FactoryBeanElementCreator.java +++ b/core-processor/src/main/java/io/micronaut/inject/processing/FactoryBeanElementCreator.java @@ -186,8 +186,20 @@ private void buildProducedBeanDefinition(BeanDefinitionWriter producedBeanDefini producedType.annotate(ConfigurationReader.class, builder -> builder.member(ConfigurationReader.PREFIX, ConfigurationUtils.getRequiredTypePath(producedType))); } - if (producingElement instanceof MethodElement) { - producedBeanDefinitionWriter.visitBeanFactoryMethod(classElement, (MethodElement) producingElement); + if (producingElement instanceof PropertyElement propertyElement) { + MethodElement readMethod = propertyElement.getReadMethod().orElse(null); + if (readMethod != null) { + producedBeanDefinitionWriter.visitBeanFactoryMethod(classElement, readMethod); + } else { + FieldElement fieldElement = propertyElement.getField().orElse(null); + if (fieldElement != null && fieldElement.isAccessible()) { + producedBeanDefinitionWriter.visitBeanFactoryField(classElement, fieldElement); + } else { + throw new ProcessingException(producingElement, "A property element that defines the @Bean annotation must have an accessible getter or field"); + } + } + } else if (producingElement instanceof MethodElement methodElement) { + producedBeanDefinitionWriter.visitBeanFactoryMethod(classElement, methodElement); } else { producedBeanDefinitionWriter.visitBeanFactoryField(classElement, (FieldElement) producingElement); } diff --git a/core-processor/src/main/java/io/micronaut/inject/processing/IntroductionInterfaceBeanElementCreator.java b/core-processor/src/main/java/io/micronaut/inject/processing/IntroductionInterfaceBeanElementCreator.java index f717d4284f4..c61847e5e45 100644 --- a/core-processor/src/main/java/io/micronaut/inject/processing/IntroductionInterfaceBeanElementCreator.java +++ b/core-processor/src/main/java/io/micronaut/inject/processing/IntroductionInterfaceBeanElementCreator.java @@ -19,6 +19,7 @@ import io.micronaut.inject.ast.ClassElement; import io.micronaut.inject.ast.ElementQuery; import io.micronaut.inject.ast.MethodElement; +import io.micronaut.inject.ast.PropertyElement; import io.micronaut.inject.visitor.VisitorContext; import io.micronaut.inject.writer.BeanDefinitionVisitor; @@ -70,7 +71,22 @@ public void buildInternal() { for (MethodElement methodElement : methods) { visitIntrospectedMethod(aopProxyWriter, classElement, methodElement); } + List beanProperties = classElement.getSyntheticBeanProperties(); + for (PropertyElement beanProperty : beanProperties) { + handlePropertyMethod(aopProxyWriter, methods, beanProperty.getReadMethod().orElse(null)); + handlePropertyMethod(aopProxyWriter, methods, beanProperty.getWriteMethod().orElse(null)); + } beanDefinitionWriters.add(aopProxyWriter); } + private void handlePropertyMethod(BeanDefinitionVisitor aopProxyWriter, List methods, MethodElement method) { + if (method != null && method.isAbstract() && !methods.contains(method)) { + visitIntrospectedMethod( + aopProxyWriter, + this.classElement, + method + ); + } + } + } diff --git a/core-processor/src/main/java/io/micronaut/inject/processing/ProcessingException.java b/core-processor/src/main/java/io/micronaut/inject/processing/ProcessingException.java index f1d8899b74c..6176af8ec98 100644 --- a/core-processor/src/main/java/io/micronaut/inject/processing/ProcessingException.java +++ b/core-processor/src/main/java/io/micronaut/inject/processing/ProcessingException.java @@ -27,11 +27,15 @@ public final class ProcessingException extends RuntimeException { private final transient Element originatingElement; - private final String message; public ProcessingException(Element element, String message) { + super(message); this.originatingElement = element; - this.message = message; + } + + public ProcessingException(Element originatingElement, String message, Throwable cause) { + super(message, cause); + this.originatingElement = originatingElement; } @Nullable @@ -42,8 +46,4 @@ public Object getOriginatingElement() { return null; } - @Override - public String getMessage() { - return message; - } } diff --git a/core-processor/src/main/java/io/micronaut/inject/writer/BeanDefinitionVisitor.java b/core-processor/src/main/java/io/micronaut/inject/writer/BeanDefinitionVisitor.java index 4db6bb10595..e6534e5b26b 100644 --- a/core-processor/src/main/java/io/micronaut/inject/writer/BeanDefinitionVisitor.java +++ b/core-processor/src/main/java/io/micronaut/inject/writer/BeanDefinitionVisitor.java @@ -18,7 +18,12 @@ import io.micronaut.core.annotation.AnnotationMetadata; import io.micronaut.core.util.Toggleable; import io.micronaut.inject.BeanDefinition; -import io.micronaut.inject.ast.*; +import io.micronaut.inject.ast.ClassElement; +import io.micronaut.inject.ast.Element; +import io.micronaut.inject.ast.FieldElement; +import io.micronaut.inject.ast.MethodElement; +import io.micronaut.inject.ast.ParameterElement; +import io.micronaut.inject.ast.TypedElement; import io.micronaut.inject.configuration.ConfigurationMetadataBuilder; import io.micronaut.inject.visitor.VisitorContext; import org.objectweb.asm.Type; @@ -136,7 +141,7 @@ void visitDefaultConstructor( /** * Alter the super class of this bean definition. The passed class should be a subclass of - * {@link io.micronaut.context.AbstractBeanDefinition}. + * {@link io.micronaut.context.AbstractInitializableBeanDefinition}. * * @param name The super type */ diff --git a/core-processor/src/main/java/io/micronaut/inject/writer/BeanDefinitionWriter.java b/core-processor/src/main/java/io/micronaut/inject/writer/BeanDefinitionWriter.java index f1542a3a9fe..37ec9c8565e 100644 --- a/core-processor/src/main/java/io/micronaut/inject/writer/BeanDefinitionWriter.java +++ b/core-processor/src/main/java/io/micronaut/inject/writer/BeanDefinitionWriter.java @@ -637,6 +637,21 @@ public BeanDefinitionWriter(Element beanProducingElement, } final ClassElement declaringType = factoryMethodElement.getOwningType(); this.beanDefinitionName = declaringType.getPackageName() + "." + prefixClassName(declaringType.getSimpleName()) + "$" + upperCaseMethodName + uniqueIdentifier + CLASS_SUFFIX; + } else if (beanProducingElement instanceof PropertyElement factoryPropertyElement) { + autoApplyNamedToBeanProducingElement(beanProducingElement); + final ClassElement producedElement = factoryPropertyElement.getGenericType(); + this.beanTypeElement = producedElement; + this.packageName = producedElement.getPackageName(); + this.isInterface = producedElement.isInterface(); + this.isAbstract = beanProducingElement.isAbstract(); + this.beanFullClassName = producedElement.getName(); + this.beanSimpleClassName = producedElement.getSimpleName(); + String upperCaseMethodName = NameUtils.capitalize(factoryPropertyElement.getName()); + if (uniqueIdentifier == null) { + throw new IllegalArgumentException("Factory methods require passing a unique identifier"); + } + final ClassElement declaringType = factoryPropertyElement.getOwningType(); + this.beanDefinitionName = declaringType.getPackageName() + "." + prefixClassName(declaringType.getSimpleName()) + "$" + upperCaseMethodName + uniqueIdentifier + CLASS_SUFFIX; } else if (beanProducingElement instanceof FieldElement factoryMethodElement) { autoApplyNamedToBeanProducingElement(beanProducingElement); final ClassElement producedElement = factoryMethodElement.getGenericField(); @@ -663,11 +678,10 @@ public BeanDefinitionWriter(Element beanProducingElement, throw new IllegalArgumentException("Beans produced by addAssociatedBean(..) require passing a unique identifier"); } final Element originatingElement = beanElementBuilder.getOriginatingElement(); - if (originatingElement instanceof ClassElement) { - ClassElement originatingClass = (ClassElement) originatingElement; + if (originatingElement instanceof ClassElement originatingClass) { this.beanDefinitionName = getAssociatedBeanName(uniqueIdentifier, originatingClass); - } else if (originatingElement instanceof MethodElement) { - ClassElement originatingClass = ((MethodElement) originatingElement).getDeclaringType(); + } else if (originatingElement instanceof MethodElement methodElement) { + ClassElement originatingClass = methodElement.getDeclaringType(); this.beanDefinitionName = getAssociatedBeanName(uniqueIdentifier, originatingClass); } else { throw new IllegalArgumentException("Unsupported originating element"); diff --git a/core-processor/src/main/resources/META-INF/services/io.micronaut.inject.annotation.AnnotationTransformer b/core-processor/src/main/resources/META-INF/services/io.micronaut.inject.annotation.AnnotationTransformer index 5d45d537b5b..eca914babbf 100644 --- a/core-processor/src/main/resources/META-INF/services/io.micronaut.inject.annotation.AnnotationTransformer +++ b/core-processor/src/main/resources/META-INF/services/io.micronaut.inject.annotation.AnnotationTransformer @@ -2,6 +2,7 @@ io.micronaut.inject.annotation.internal.CoreNullableTransformer io.micronaut.inject.annotation.internal.CoreNonNullTransformer io.micronaut.inject.annotation.internal.KotlinNullableMapper io.micronaut.inject.annotation.internal.KotlinNotNullMapper +io.micronaut.inject.annotation.internal.KotlinDeprecatedTransformer io.micronaut.inject.annotation.internal.JakartaPostConstructTransformer io.micronaut.inject.annotation.internal.JakartaPreDestroyTransformer io.micronaut.inject.annotation.internal.JakartaNullableTransformer diff --git a/core/src/main/java/io/micronaut/core/annotation/AnnotationUtil.java b/core/src/main/java/io/micronaut/core/annotation/AnnotationUtil.java index 5ab0941b3f7..948aa36a3aa 100644 --- a/core/src/main/java/io/micronaut/core/annotation/AnnotationUtil.java +++ b/core/src/main/java/io/micronaut/core/annotation/AnnotationUtil.java @@ -42,6 +42,7 @@ public class AnnotationUtil { "javax.annotation.meta.TypeQualifier", "javax.annotation.meta.TypeQualifierNickname", "kotlin.annotation.Retention", + "kotlin.Annotation", Inherited.class.getName(), SuppressWarnings.class.getName(), Override.class.getName(), diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 2a7718c76a3..5f507d0c927 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -34,6 +34,7 @@ junit5 = "5.9.1" junit-platform="1.9.1" kotlin = "1.7.20" kotlin-coroutines = "1.6.4" +ksp-testing = "1.4.9" ktor = "1.6.8" managed-logback = "1.4.5" logbook-netty = "2.14.0" @@ -74,6 +75,7 @@ managed-slf4j = "2.0.4" managed-snakeyaml = "1.33" managed-validation = "2.0.1.Final" managed-java-parser-core = "3.24.9" +managed-ksp = "1.8.0-Beta-1.0.8" micronaut-docs = "2.0.0" [libraries] @@ -106,8 +108,9 @@ managed-jackson-datatype-jsr310 = { module = "com.fasterxml.jackson.datatype:jac managed-jackson-module-afterburner = { module = "com.fasterxml.jackson.module:jackson-module-afterburner", version.ref = "managed-jackson" } managed-jackson-module-kotlin = { module = "com.fasterxml.jackson.module:jackson-module-kotlin", version.ref = "managed-jackson" } managed-jackson-module-parameterNames = { module = "com.fasterxml.jackson.module:jackson-module-parameter-names", version.ref = "managed-jackson" } +managed-ksp-api = { module = "com.google.devtools.ksp:symbol-processing-api", version.ref = "managed-ksp" } +managed-ksp = { module = "com.google.devtools.ksp:symbol-processing", version.ref = "managed-ksp" } managed-java-parser-core = { module = "com.github.javaparser:javaparser-symbol-solver-core", version.ref = "managed-java-parser-core" } - managed-methvin-directoryWatcher = { module = "io.methvin:directory-watcher", version.ref = "managed-methvin-directory-watcher" } managed-netty-buffer = { module = "io.netty:netty-buffer", version.ref = "managed-netty" } @@ -204,6 +207,8 @@ kotlinx-coroutines-rx2 = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-rx kotlinx-coroutines-slf4j = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-slf4j", version.ref = "kotlin-coroutines" } kotlinx-coroutines-reactor = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-reactor", version.ref = "kotlin-coroutines" } +ksp-testing = { module = "com.github.tschuchortdev:kotlin-compile-testing-ksp", version.ref = "ksp-testing" } + log4j = { module = "org.apache.logging.log4j:log4j-core", version.ref = "log4j" } logbook-netty = { module = "org.zalando:logbook-netty", version.ref = "logbook-netty" } diff --git a/http-client-core/src/main/java/io/micronaut/http/client/interceptor/HttpClientIntroductionAdvice.java b/http-client-core/src/main/java/io/micronaut/http/client/interceptor/HttpClientIntroductionAdvice.java index 9c54f78404b..3b709fe96b5 100644 --- a/http-client-core/src/main/java/io/micronaut/http/client/interceptor/HttpClientIntroductionAdvice.java +++ b/http-client-core/src/main/java/io/micronaut/http/client/interceptor/HttpClientIntroductionAdvice.java @@ -139,7 +139,7 @@ public HttpClientIntroductionAdvice( } /** - * Interceptor to apply headers, cookies, parameter and body arguements. + * Interceptor to apply headers, cookies, parameter and body arguments. * * @param context The context * @return httpClient or future diff --git a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyClassElement.java b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyClassElement.java index f468f183303..7c876348b6b 100644 --- a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyClassElement.java +++ b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyClassElement.java @@ -905,7 +905,7 @@ protected boolean excludeClass(ClassNode classNode) { } @Override - protected Element toAstElement(AnnotatedNode enclosedElement) { + protected Element toAstElement(AnnotatedNode enclosedElement, Class elementType) { final GroovyElementFactory elementFactory = visitorContext.getElementFactory(); if (isSource) { if (!(enclosedElement instanceof ConstructorNode) && enclosedElement instanceof MethodNode methodNode) { diff --git a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyMethodElement.java b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyMethodElement.java index dfcc9acb390..389c55ae167 100644 --- a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyMethodElement.java +++ b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyMethodElement.java @@ -104,8 +104,7 @@ public ClassElement[] getThrownTypes() { return Arrays.stream(exceptions) .map(cn -> getGenericElement(cn, visitorContext.getElementFactory().newClassElement( cn, - elementAnnotationMetadataFactory, - Collections.emptyMap() + elementAnnotationMetadataFactory ))).toArray(ClassElement[]::new); } return ClassElement.ZERO_CLASS_ELEMENTS; diff --git a/inject-java-test/src/test/groovy/io/micronaut/inject/visitor/beans/BeanIntrospectionSpec.groovy b/inject-java-test/src/test/groovy/io/micronaut/inject/visitor/beans/BeanIntrospectionSpec.groovy index 4d9203eb4c2..4231e0566bb 100644 --- a/inject-java-test/src/test/groovy/io/micronaut/inject/visitor/beans/BeanIntrospectionSpec.groovy +++ b/inject-java-test/src/test/groovy/io/micronaut/inject/visitor/beans/BeanIntrospectionSpec.groovy @@ -302,7 +302,6 @@ package fieldaccess; import io.micronaut.core.annotation.*; - @Introspected(accessKind={Introspected.AccessKind.FIELD, Introspected.AccessKind.METHOD}) class Test { public String one; diff --git a/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaClassElement.java b/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaClassElement.java index b482d52c88f..0f39bb3790b 100644 --- a/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaClassElement.java +++ b/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaClassElement.java @@ -666,7 +666,7 @@ public List getBoundGenericTypes() { return typeArguments.stream() //return getGenericTypeInfo().getOrDefault(classElement.getQualifiedName().toString(), Collections.emptyMap()).values().stream() .map(tm -> mirrorToClassElement(tm, visitorContext, getGenericTypeInfo())) - .collect(Collectors.toList()); + .toList(); } @NonNull @@ -675,7 +675,7 @@ public List getDeclaredGenericPlaceholders( return classElement.getTypeParameters().stream() // we want the *declared* variables, so we don't pass in our genericsInfo. .map(tpe -> (GenericPlaceholderElement) mirrorToClassElement(tpe.asType(), visitorContext)) - .collect(Collectors.toList()); + .toList(); } @NonNull @@ -901,7 +901,7 @@ protected boolean excludeClass(TypeElement classNode) { } @Override - protected io.micronaut.inject.ast.Element toAstElement(Element enclosedElement) { + protected io.micronaut.inject.ast.Element toAstElement(Element enclosedElement, Class elementType) { final JavaElementFactory elementFactory = visitorContext.getElementFactory(); return switch (enclosedElement.getKind()) { case METHOD -> elementFactory.newMethodElement( diff --git a/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaMethodElement.java b/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaMethodElement.java index fc16506a18c..2a26ef2772c 100644 --- a/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaMethodElement.java +++ b/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaMethodElement.java @@ -276,7 +276,7 @@ protected ClassElement returnType(Map> info) { tm = ((WildcardType) tm).getSuperBound(); } // check Void - if ((tm instanceof DeclaredType) && sameType("kotlin.Unit", (DeclaredType) tm)) { + if ((tm instanceof DeclaredType dt) && sameType("kotlin.Unit", dt)) { return PrimitiveElement.VOID; } else { return mirrorToClassElement(tm, visitorContext, info, true); diff --git a/inject-java/src/test/groovy/io/micronaut/aop/compile/AroundConstructCompileSpec.groovy b/inject-java/src/test/groovy/io/micronaut/aop/compile/AroundConstructCompileSpec.groovy index 2aa04cd70b2..c03291c40a2 100644 --- a/inject-java/src/test/groovy/io/micronaut/aop/compile/AroundConstructCompileSpec.groovy +++ b/inject-java/src/test/groovy/io/micronaut/aop/compile/AroundConstructCompileSpec.groovy @@ -676,24 +676,12 @@ class MyBean { } } -@io.micronaut.context.annotation.Factory -class MyFactory { - @TestAnn - @Singleton - MyOtherBean test(io.micronaut.context.env.Environment env) { - return new MyOtherBean(); - } -} - -class MyOtherBean {} - @Retention(RUNTIME) @Target({ElementType.METHOD, ElementType.TYPE}) @AroundConstruct @interface TestAnn { } - @Factory class InterceptorFactory { boolean aroundConstructInvoked = false; @@ -722,10 +710,8 @@ class InterceptorFactory { !(instance instanceof Intercepted) factory.aroundConstructInvoked - cleanup: context.close() - } void 'test around construct with introduction advice'() { diff --git a/inject-java/src/test/groovy/io/micronaut/aop/compile/LifeCycleWithProxyTargetSpec.groovy b/inject-java/src/test/groovy/io/micronaut/aop/compile/LifeCycleWithProxyTargetSpec.groovy index 9c4daec3088..b24b710b500 100644 --- a/inject-java/src/test/groovy/io/micronaut/aop/compile/LifeCycleWithProxyTargetSpec.groovy +++ b/inject-java/src/test/groovy/io/micronaut/aop/compile/LifeCycleWithProxyTargetSpec.groovy @@ -1,14 +1,16 @@ package io.micronaut.aop.compile import io.micronaut.annotation.processing.test.AbstractTypeElementSpec +import io.micronaut.context.ApplicationContext import io.micronaut.inject.BeanDefinition +import io.micronaut.inject.InstantiatableBeanDefinition import io.micronaut.inject.writer.BeanDefinitionWriter class LifeCycleWithProxyTargetSpec extends AbstractTypeElementSpec { void "test that a proxy target AOP definition lifecycle hooks are invoked - annotation at class level"() { when: - BeanDefinition beanDefinition = buildBeanDefinition('test.$MyBean' + BeanDefinitionWriter.CLASS_SUFFIX + BeanDefinitionWriter.PROXY_SUFFIX, ''' + ApplicationContext context = buildContext( ''' package test; import io.micronaut.aop.proxytarget.*; @@ -21,16 +23,16 @@ class MyBean { @jakarta.inject.Inject public ConversionService conversionService; public int count = 0; - + public String someMethod() { return "good"; } - + @jakarta.annotation.PostConstruct void created() { count++; } - + @javax.annotation.PreDestroy void destroyed() { count--; @@ -38,12 +40,27 @@ class MyBean { } ''') + def beanDefinition = getBeanDefinition(context, 'test.MyBean') + then: !beanDefinition.isAbstract() beanDefinition != null beanDefinition.postConstructMethods.size() == 1 beanDefinition.preDestroyMethods.size() == 1 + when: + def instance = getBean(context, 'test.MyBean') + + then:"proxy post construct methods are not invoked" + instance.conversionService // injection works + instance.someMethod() == 'good' + instance.count == 0 + + and:"proxy target post construct methods are invoked" + instance.interceptedTarget().count == 1 + + cleanup: + context.close() } void "test that a proxy target AOP definition lifecycle hooks are invoked - annotation at method level with hooks last"() { @@ -60,7 +77,7 @@ class MyBean { @jakarta.inject.Inject public ConversionService conversionService; public int count = 0; - + @Mutating("someVal") public String someMethod() { return "good"; @@ -70,7 +87,7 @@ class MyBean { void created() { count++; } - + @javax.annotation.PreDestroy void destroyed() { count--; @@ -99,17 +116,17 @@ class MyBean { @jakarta.inject.Inject public ConversionService conversionService; public int count = 0; - + @jakarta.annotation.PostConstruct void created() { count++; } - + @javax.annotation.PreDestroy void destroyed() { count--; } - + @Mutating("someVal") public String someMethod() { return "good"; diff --git a/inject-java/src/test/groovy/io/micronaut/aop/introduction/IntroductionAdviceWithNewInterfaceSpec.groovy b/inject-java/src/test/groovy/io/micronaut/aop/introduction/IntroductionAdviceWithNewInterfaceSpec.groovy index bfa0fa6a334..0b6a6b7602b 100644 --- a/inject-java/src/test/groovy/io/micronaut/aop/introduction/IntroductionAdviceWithNewInterfaceSpec.groovy +++ b/inject-java/src/test/groovy/io/micronaut/aop/introduction/IntroductionAdviceWithNewInterfaceSpec.groovy @@ -28,6 +28,30 @@ import io.micronaut.inject.writer.BeanDefinitionVisitor */ class IntroductionAdviceWithNewInterfaceSpec extends AbstractTypeElementSpec { + + void "test introduction advice with primitive generics"() { + when: + def context = buildContext( 'test.MyRepo', ''' +package test; + +import io.micronaut.aop.introduction.*; +import javax.validation.constraints.NotNull; + +@RepoDef +interface MyRepo extends DeleteByIdCrudRepo { + + @Override void deleteById(@NotNull Integer integer); +} + + +''', true) + + def bean = + getBean(context, 'test.MyRepo') + then: + bean != null + } + void "test that it is possible for @Introduction advice to implement additional interfaces on concrete classes"() { when: BeanDefinition beanDefinition = buildBeanDefinition('test.MyBean' + BeanDefinitionVisitor.PROXY_SUFFIX, ''' @@ -138,7 +162,7 @@ interface MyBean { beanDefinition != null ApplicationEventListener.class.isAssignableFrom(beanDefinition.beanType) beanDefinition.injectedFields.size() == 0 - beanDefinition.executableMethods.size() == 2 + beanDefinition.executableMethods.size() == 3 beanDefinition.findMethod("getBar").isPresent() beanDefinition.findMethod("onApplicationEvent", Object).isPresent() diff --git a/inject-java/src/test/groovy/io/micronaut/aop/introduction/Stub.java b/inject-java/src/test/groovy/io/micronaut/aop/introduction/Stub.java index 128f7a785d1..3aeaa3162d4 100644 --- a/inject-java/src/test/groovy/io/micronaut/aop/introduction/Stub.java +++ b/inject-java/src/test/groovy/io/micronaut/aop/introduction/Stub.java @@ -16,7 +16,6 @@ package io.micronaut.aop.introduction; import io.micronaut.aop.Introduction; -import io.micronaut.context.annotation.Executable; import io.micronaut.context.annotation.Type; import java.lang.annotation.Documented; diff --git a/inject-java/src/test/groovy/io/micronaut/visitors/ClassElementSpec.groovy b/inject-java/src/test/groovy/io/micronaut/visitors/ClassElementSpec.groovy index 596932084e7..708a650ee32 100644 --- a/inject-java/src/test/groovy/io/micronaut/visitors/ClassElementSpec.groovy +++ b/inject-java/src/test/groovy/io/micronaut/visitors/ClassElementSpec.groovy @@ -31,6 +31,7 @@ import io.micronaut.inject.ast.MemberElement import io.micronaut.inject.ast.MethodElement import io.micronaut.inject.ast.PackageElement import io.micronaut.inject.ast.PrimitiveElement +import io.micronaut.inject.ast.PropertyElement import jakarta.inject.Singleton import spock.lang.IgnoreIf import spock.lang.Issue @@ -42,10 +43,213 @@ import java.sql.SQLException import java.util.function.Supplier class ClassElementSpec extends AbstractTypeElementSpec { + void "test class element generics"() { + given: + ClassElement classElement = buildClassElement(''' +package ast.test; + +import java.util.*; + +final class Test extends Parent implements One { + Test(String constructorProp) { + super(constructorProp); + } +} + +abstract class Parent extends java.util.AbstractCollection { + private final T parentConstructorProp; + private T conventionProp; + + Parent(T parentConstructorProp) { + this.parentConstructorProp = parentConstructorProp; + } + + public void setConventionProp(T conventionProp) { + this.conventionProp = conventionProp; + } + public T getConventionProp() { + return conventionProp; + } + public T getParentConstructorProp() { + return parentConstructorProp; + } + + @Override public int size() { + return 0; + } + + @Override public Iterator iterator() { + return null; + } + + public T publicFunc(T name) { + return name; + } + + public T parentFunc(T name) { + return name; + } + +} + +interface One {} +''') + List propertyElements = classElement.getBeanProperties() + List methodElements = classElement.getEnclosedElements(ElementQuery.ALL_METHODS) + Map methodMap = methodElements.collectEntries { + [it.name, it] + } + Map propMap = propertyElements.collectEntries { + [it.name, it] + } + + expect: + methodMap['add'].parameters[0].genericType.simpleName == 'String' + methodMap['add'].parameters[0].type.simpleName == 'Object' + methodMap['iterator'].returnType.firstTypeArgument.get().simpleName == 'CharSequence' // why? + methodMap['iterator'].genericReturnType.firstTypeArgument.get().simpleName == 'String' + methodMap['stream'].returnType.firstTypeArgument.get().simpleName == 'Object' + methodMap['stream'].genericReturnType.firstTypeArgument.get().simpleName == 'String' + propMap['conventionProp'].type.simpleName == 'String' + propMap['conventionProp'].genericType.simpleName == 'String' + propMap['conventionProp'].genericType.simpleName == 'String' + propMap['conventionProp'].readMethod.get().returnType.simpleName == 'CharSequence' + propMap['conventionProp'].readMethod.get().genericReturnType.simpleName == 'String' + propMap['conventionProp'].writeMethod.get().parameters[0].type.simpleName == 'CharSequence' + propMap['conventionProp'].writeMethod.get().parameters[0].genericType.simpleName == 'String' + propMap['parentConstructorProp'].type.simpleName == 'String' + propMap['parentConstructorProp'].genericType.simpleName == 'String' + methodMap['parentFunc'].returnType.simpleName == 'CharSequence' + methodMap['parentFunc'].genericReturnType.simpleName == 'String' + methodMap['parentFunc'].parameters[0].type.simpleName == 'CharSequence' + methodMap['parentFunc'].parameters[0].genericType.simpleName == 'String' + } + + void "test class element generics - records"() { + given: + ClassElement classElement = buildClassElement(''' +package ast.test; + +import org.jetbrains.annotations.NotNull;import java.util.*; + +record Test(String constructorProp) implements Parent, One { + @Override public String publicFunc(String name) { + return null; + } + @Override public int size() { + return 0; + } + @Override public boolean isEmpty() { + return false; + } + @Override public boolean contains(Object o) { + return false; + } + @NotNull @Override public Iterator iterator() { + return null; + } + @NotNull@Override public Object[] toArray() { + return new Object[0]; + } + @NotNull@Override public T[] toArray(@NotNull T[] a) { + return null; + } + @Override public boolean add(String s) { + return false; + } + @Override public boolean remove(Object o) { + return false; + } + @Override public boolean containsAll(@NotNull Collection c) { + return false; + } + @Override public boolean addAll(@NotNull Collection c) { + return false; + } + @Override public boolean addAll(int index,@NotNull Collection c) { + return false; + } + @Override public boolean removeAll(@NotNull Collection c) { + return false; + } + @Override public boolean retainAll(@NotNull Collection c) { + return false; + } + @Override public void clear() { + + } + @Override public void add(int index, String element) { + + + }@Override public String remove(int index) { + return null; + } + @Override public int indexOf(Object o) { + return 0; + } + @Override public int lastIndexOf(Object o) { + return 0; + } + @NotNull @Override public ListIterator listIterator() { + return null; + } + @NotNull @Override public ListIterator listIterator(int index) { + return null; + } + @NotNull @Override public List subList(int fromIndex, int toIndex) { + return null; + } +} + +interface Parent extends java.util.List { + + public T constructorProp(); + + public T publicFunc(T name); + + default T parentFunc(T name) { + return name; + } + + @Override default T get(int index) { + return null; + } + @Override default T set(int index, T element) { + return null; + } +} + +interface One {} +''') + List propertyElements = classElement.getBeanProperties() + List methodElements = classElement.getEnclosedElements(ElementQuery.ALL_METHODS) + Map methodMap = methodElements.collectEntries { + [it.name, it] + } + Map propMap = propertyElements.collectEntries { + [it.name, it] + } + + expect: + methodMap['add'].parameters[1].genericType.simpleName == 'String' + methodMap['add'].parameters[1].type.simpleName == 'String' + methodMap['iterator'].returnType.firstTypeArgument.get().simpleName == 'String' + methodMap['iterator'].genericReturnType.firstTypeArgument.get().simpleName == 'String' + methodMap['stream'].returnType.firstTypeArgument.get().simpleName == 'Object' + methodMap['stream'].genericReturnType.firstTypeArgument.get().simpleName == 'String' + propMap['constructorProp'].readMethod.get().returnType.simpleName == 'String' + propMap['constructorProp'].readMethod.get().genericReturnType.simpleName == 'String' + propMap['constructorProp'].type.simpleName == 'String' + propMap['constructorProp'].genericType.simpleName == 'String' + methodMap['parentFunc'].returnType.simpleName == 'CharSequence' + methodMap['parentFunc'].genericReturnType.simpleName == 'String' + methodMap['parentFunc'].parameters[0].type.simpleName == 'CharSequence' + methodMap['parentFunc'].parameters[0].genericType.simpleName == 'String' + } void "test equals with primitive"() { given: - def element = buildClassElement(""" + def element = buildClassElement(""" package test; class Test { @@ -54,14 +258,14 @@ class Test { """) expect: - element != PrimitiveElement.BOOLEAN - element != PrimitiveElement.VOID - element != PrimitiveElement.BOOLEAN.withArrayDimensions(4) - PrimitiveElement.VOID != element - PrimitiveElement.INT != element - PrimitiveElement.INT.withArrayDimensions(2) != element - element.getFields().get(0).getType() == PrimitiveElement.BOOLEAN - PrimitiveElement.BOOLEAN == element.getFields().get(0).getType() + element != PrimitiveElement.BOOLEAN + element != PrimitiveElement.VOID + element != PrimitiveElement.BOOLEAN.withArrayDimensions(4) + PrimitiveElement.VOID != element + PrimitiveElement.INT != element + PrimitiveElement.INT.withArrayDimensions(2) != element + element.getFields().get(0).getType() == PrimitiveElement.BOOLEAN + PrimitiveElement.BOOLEAN == element.getFields().get(0).getType() } void "test resolve receiver type on method"() { @@ -1071,7 +1275,7 @@ class Person { void "test find enum fields using ElementQuery"() { given: - ClassElement classElement = buildClassElement(''' + ClassElement classElement = buildClassElement(''' package elementquery; enum Test { @@ -1100,7 +1304,7 @@ enum Test { } ''') when: - List allFields = classElement.getEnclosedElements(ElementQuery.ALL_FIELDS) + List allFields = classElement.getEnclosedElements(ElementQuery.ALL_FIELDS) List expected = [ 'publicStaticFinalField', @@ -1139,10 +1343,11 @@ enum Test { } @Issue("https://github.com/eclipse-ee4j/cdi-tck/blob/master/lang-model/src/main/java/org/jboss/cdi/lang/model/tck/InheritedMethods.java") - @Requires({ jvm.isJava9Compatible() }) // private static Since Java 9 + @Requires({ jvm.isJava9Compatible() }) + // private static Since Java 9 void "test inherited methods using ElementQuery"() { given: - ClassElement classElement = buildClassElement(''' + ClassElement classElement = buildClassElement(''' package elementquery; class InheritedMethods extends SuperClassWithMethods implements SuperInterfaceWithMethods { @@ -1388,13 +1593,13 @@ public class TestController { ''') expect: - AllElementsVisitor.VISITED_METHOD_ELEMENTS[0].owningType.name == 'test.TestController' - AllElementsVisitor.VISITED_METHOD_ELEMENTS[0].declaringType.name == 'test.TestController' + AllElementsVisitor.VISITED_METHOD_ELEMENTS[0].owningType.name == 'test.TestController' + AllElementsVisitor.VISITED_METHOD_ELEMENTS[0].declaringType.name == 'test.TestController' } void "test fields selection"() { given: - ClassElement classElement = buildClassElement(''' + ClassElement classElement = buildClassElement(''' package test; import io.micronaut.http.annotation.*; @@ -1427,46 +1632,46 @@ class Pet { ''') when: - List publicFields = classElement.getFirstTypeArgument() - .get() - .getEnclosedElements(ElementQuery.ALL_FIELDS.modifiers(mods -> mods.contains(ElementModifier.PUBLIC) && mods.size() == 1)) + List publicFields = classElement.getFirstTypeArgument() + .get() + .getEnclosedElements(ElementQuery.ALL_FIELDS.modifiers(mods -> mods.contains(ElementModifier.PUBLIC) && mods.size() == 1)) then: - publicFields.size() == 1 - publicFields.stream().map(FieldElement::getName).toList() == ["pub"] + publicFields.size() == 1 + publicFields.stream().map(FieldElement::getName).toList() == ["pub"] when: - List publicFields2 = classElement.getFirstTypeArgument() - .get() - .getEnclosedElements(ElementQuery.ALL_FIELDS.filter(e -> e.isPublic())) + List publicFields2 = classElement.getFirstTypeArgument() + .get() + .getEnclosedElements(ElementQuery.ALL_FIELDS.filter(e -> e.isPublic())) then: - publicFields2.size() == 2 - publicFields2.stream().map(FieldElement::getName).toList() == ["pub", "PUB_CONST"] + publicFields2.size() == 2 + publicFields2.stream().map(FieldElement::getName).toList() == ["pub", "PUB_CONST"] when: - List protectedFields = classElement.getFirstTypeArgument() - .get() - .getEnclosedElements(ElementQuery.ALL_FIELDS.filter(e -> e.isProtected())) + List protectedFields = classElement.getFirstTypeArgument() + .get() + .getEnclosedElements(ElementQuery.ALL_FIELDS.filter(e -> e.isProtected())) then: - protectedFields.size() == 2 - protectedFields.stream().map(FieldElement::getName).toList() == ["protectme", "PROT_CONST"] + protectedFields.size() == 2 + protectedFields.stream().map(FieldElement::getName).toList() == ["protectme", "PROT_CONST"] when: - List privateFields = classElement.getFirstTypeArgument() - .get() - .getEnclosedElements(ElementQuery.ALL_FIELDS.filter(e -> e.isPrivate())) + List privateFields = classElement.getFirstTypeArgument() + .get() + .getEnclosedElements(ElementQuery.ALL_FIELDS.filter(e -> e.isPrivate())) then: - privateFields.size() == 2 - privateFields.stream().map(FieldElement::getName).toList() == ["prvn", "PRV_CONST"] + privateFields.size() == 2 + privateFields.stream().map(FieldElement::getName).toList() == ["prvn", "PRV_CONST"] when: - List packPrvFields = classElement.getFirstTypeArgument() - .get() - .getEnclosedElements(ElementQuery.ALL_FIELDS.filter(e -> e.isPackagePrivate())) + List packPrvFields = classElement.getFirstTypeArgument() + .get() + .getEnclosedElements(ElementQuery.ALL_FIELDS.filter(e -> e.isPackagePrivate())) then: - packPrvFields.size() == 2 - packPrvFields.stream().map(FieldElement::getName).toList() == ["packprivme", "PACK_PRV_CONST"] + packPrvFields.size() == 2 + packPrvFields.stream().map(FieldElement::getName).toList() == ["packprivme", "PACK_PRV_CONST"] } void "test annotations on generic type"() { given: - ClassElement classElement = buildClassElement(''' + ClassElement classElement = buildClassElement(''' package test; import io.micronaut.core.annotation.Introspected; @@ -1486,13 +1691,13 @@ class Pet { ''') when: - def method = classElement.getEnclosedElements(ElementQuery.ALL_METHODS.named("save")).get(0) - def returnType = method.getReturnType() - def genericReturnType = method.getGenericReturnType() + def method = classElement.getEnclosedElements(ElementQuery.ALL_METHODS.named("save")).get(0) + def returnType = method.getReturnType() + def genericReturnType = method.getGenericReturnType() then: - returnType.hasAnnotation(Introspected) - genericReturnType.hasAnnotation(Introspected) + returnType.hasAnnotation(Introspected) + genericReturnType.hasAnnotation(Introspected) } private void assertMethodsByName(List allMethods, String name, List expectedDeclaringTypeSimpleNames) { @@ -1513,7 +1718,7 @@ class Pet { private boolean oneElementPresentWithDeclaringType(Collection elements, String declaringTypeSimpleName) { elements.stream() - .filter { it -> it.getDeclaringType().getSimpleName() == declaringTypeSimpleName} + .filter { it -> it.getDeclaringType().getSimpleName() == declaringTypeSimpleName } .count() == 1 } diff --git a/inject-kotlin-test/build.gradle b/inject-kotlin-test/build.gradle index c1ba6607294..cf4b5160630 100644 --- a/inject-kotlin-test/build.gradle +++ b/inject-kotlin-test/build.gradle @@ -4,9 +4,7 @@ plugins { } dependencies { - annotationProcessor project(":inject-java") - - api project(":inject-java") + api project(":inject-kotlin") api libs.managed.groovy api(libs.spock) { exclude module:'groovy-all' @@ -14,16 +12,11 @@ dependencies { if (!JavaVersion.current().isJava9Compatible()) { api files(org.gradle.internal.jvm.Jvm.current().toolsJar) } - - testAnnotationProcessor project(":inject-java") - testCompileOnly project(":inject-groovy") + api(libs.ksp.testing) testImplementation libs.managed.validation testImplementation libs.javax.persistence testImplementation project(":runtime") api libs.blaze.persistence.core - - implementation libs.kotlin.compiler.embeddable - implementation libs.kotlin.annotation.processing.embeddable implementation libs.kotlin.stdlib } diff --git a/inject-kotlin-test/src/main/groovy/io/micronaut/annotation/processing/test/AbstractKotlinCompilerSpec.groovy b/inject-kotlin-test/src/main/groovy/io/micronaut/annotation/processing/test/AbstractKotlinCompilerSpec.groovy index 29dee21db30..9b14f0c32b6 100644 --- a/inject-kotlin-test/src/main/groovy/io/micronaut/annotation/processing/test/AbstractKotlinCompilerSpec.groovy +++ b/inject-kotlin-test/src/main/groovy/io/micronaut/annotation/processing/test/AbstractKotlinCompilerSpec.groovy @@ -15,27 +15,26 @@ */ package io.micronaut.annotation.processing.test -import io.micronaut.aop.internal.InterceptorRegistryBean + import io.micronaut.context.ApplicationContext -import io.micronaut.context.DefaultApplicationContext import io.micronaut.context.Qualifier -import io.micronaut.context.event.ApplicationEventPublisherFactory +import io.micronaut.core.annotation.Experimental +import io.micronaut.core.annotation.NonNull import io.micronaut.core.beans.BeanIntrospection -import io.micronaut.core.io.scan.ClassPathResourceLoader import io.micronaut.core.naming.NameUtils import io.micronaut.inject.BeanDefinition -import io.micronaut.inject.BeanDefinitionReference -import io.micronaut.inject.provider.BeanProviderDefinition -import io.micronaut.inject.provider.JakartaProviderBeanDefinition +import io.micronaut.inject.ast.ClassElement +import io.micronaut.inject.ast.GenericPlaceholderElement +import io.micronaut.inject.ast.WildcardElement import org.intellij.lang.annotations.Language import spock.lang.Specification +import java.util.function.Consumer import java.util.stream.Collectors class AbstractKotlinCompilerSpec extends Specification { protected ClassLoader buildClassLoader(String className, @Language("kotlin") String cls) { - def result = KotlinCompileHelper.INSTANCE.run(className, cls) - result.classLoader + KotlinCompiler.buildClassLoader(className, cls) } /** @@ -58,40 +57,105 @@ class AbstractKotlinCompilerSpec extends Specification { * @return the introspection if it is correct */ protected ApplicationContext buildContext(String className, @Language("kotlin") String cls, boolean includeAllBeans = false) { - def result = KotlinCompileHelper.INSTANCE.run(className, cls) - ClassLoader classLoader = result.classLoader + KotlinCompiler.buildContext(cls, includeAllBeans) + } - return new DefaultApplicationContext(ClassPathResourceLoader.defaultLoader(classLoader), "test") { - @Override - protected List resolveBeanDefinitionReferences() { - // we want only the definitions we just compiled - def stream = result.fileNames.stream() - .filter(s -> s.endsWith('$Definition$Reference.class')) - .map(n -> classLoader.loadClass(n.substring(0, n.size() - 6).replace('/', '.')).newInstance()) - return stream.collect(Collectors.toList()) + (includeAllBeans ? super.resolveBeanDefinitionReferences() : [ - new InterceptorRegistryBean(), - new BeanProviderDefinition(), - new JakartaProviderBeanDefinition(), - new ApplicationEventPublisherFactory<>() - ]) + /** + * Build and return a {@link io.micronaut.core.beans.BeanIntrospection} for the given class name and class data. + * + * @return the introspection if it is correct + */ + protected ApplicationContext buildContext(@Language("kotlin") String cls, boolean includeAllBeans = false) { + KotlinCompiler.buildContext(cls, includeAllBeans) + } + + /** + * Builds a class element for the given source code. + * @param cls The source + * @return The class element + */ + ClassElement buildClassElement(String className, @Language("kotlin") String cls) { + List elements = [] + KotlinCompiler.compile(className, cls, { + elements.add(it) + }) + return elements.find { it.name == className } + } + + /** + * Builds a class element for the given source code. + * @param cls The source + * @return The class element + */ + boolean buildClassElement(String className, @Language("kotlin") String cls, @NonNull Consumer processor) { + boolean invoked = false + KotlinCompiler.compile(className, cls) { + if (it.name == className) { + processor.accept(it) + invoked = true } - }.start() + } + return true } Object getBean(ApplicationContext context, String className, Qualifier qualifier = null) { context.getBean(context.classLoader.loadClass(className), qualifier) } + /** + * Gets a bean definition from the context for the given class name + * @param context The context + * @param className The class name + * @return The bean instance + */ + BeanDefinition getBeanDefinition(ApplicationContext context, String className, Qualifier qualifier = null) { + context.getBeanDefinition(context.classLoader.loadClass(className), qualifier) + } + protected BeanDefinition buildBeanDefinition(String className, @Language("kotlin") String cls) { - def beanDefName= '$' + NameUtils.getSimpleName(className) + '$Definition' - def packageName = NameUtils.getPackageName(className) - String beanFullName = "${packageName}.${beanDefName}" + KotlinCompiler.buildBeanDefinition(className, cls) + } - ClassLoader classLoader = buildClassLoader(className, cls) - try { - return (BeanDefinition)classLoader.loadClass(beanFullName).newInstance() - } catch (ClassNotFoundException e) { - return null + /** + * Create a rough source signature of the given ClassElement, using {@link io.micronaut.inject.ast.ClassElement#getBoundGenericTypes()}. + * Can be used to test that {@link io.micronaut.inject.ast.ClassElement#getBoundGenericTypes()} returns the right types in the right + * context. + * + * @param classElement The class element to reconstruct + * @param typeVarsAsDeclarations Whether type variables should be represented as declarations + * @return a String representing the type signature. + */ + @Experimental + protected static String reconstructTypeSignature(ClassElement classElement, boolean typeVarsAsDeclarations = false) { + if (classElement.isArray()) { + return "Array<" + reconstructTypeSignature(classElement.fromArray()) + ">" + } else if (classElement.isGenericPlaceholder()) { + def freeVar = (GenericPlaceholderElement) classElement + def name = freeVar.variableName + if (typeVarsAsDeclarations) { + def bounds = freeVar.bounds + if (reconstructTypeSignature(bounds[0]) != 'Object') { + name += bounds.stream().map(AbstractKotlinCompilerSpec::reconstructTypeSignature).collect(Collectors.joining(" & ", " out ", "")) + } + } + return name + } else if (classElement.isWildcard()) { + def we = (WildcardElement) classElement + if (!we.lowerBounds.isEmpty()) { + return we.lowerBounds.stream().map(AbstractKotlinCompilerSpec::reconstructTypeSignature).collect(Collectors.joining(" | ", "in ", "")) + } else if (we.upperBounds.size() == 1 && reconstructTypeSignature(we.upperBounds.get(0)) == "Object") { + return "*" + } else { + return we.upperBounds.stream().map(AbstractKotlinCompilerSpec::reconstructTypeSignature).collect(Collectors.joining(" & ", "out ", "")) + } + } else { + def boundTypeArguments = classElement.getBoundGenericTypes() + if (boundTypeArguments.isEmpty()) { + return classElement.getSimpleName() + } else { + return classElement.getSimpleName() + + boundTypeArguments.stream().map(AbstractKotlinCompilerSpec::reconstructTypeSignature).collect(Collectors.joining(", ", "<", ">")) + } } } -} \ No newline at end of file +} diff --git a/inject-kotlin-test/src/main/groovy/io/micronaut/annotation/processing/test/KotlinCompiler.java b/inject-kotlin-test/src/main/groovy/io/micronaut/annotation/processing/test/KotlinCompiler.java new file mode 100644 index 00000000000..17de74126bd --- /dev/null +++ b/inject-kotlin-test/src/main/groovy/io/micronaut/annotation/processing/test/KotlinCompiler.java @@ -0,0 +1,327 @@ +/* + * Copyright 2017-2023 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.annotation.processing.test; + +import com.google.devtools.ksp.processing.SymbolProcessor; +import com.google.devtools.ksp.processing.SymbolProcessorEnvironment; +import com.google.devtools.ksp.symbol.KSClassDeclaration; +import com.tschuchort.compiletesting.KotlinCompilation; +import com.tschuchort.compiletesting.KspKt; +import com.tschuchort.compiletesting.SourceFile; +import io.micronaut.aop.internal.InterceptorRegistryBean; +import io.micronaut.context.ApplicationContext; +import io.micronaut.context.ApplicationContextConfiguration; +import io.micronaut.context.BeanContext; +import io.micronaut.context.DefaultApplicationContext; +import io.micronaut.context.env.Environment; +import io.micronaut.context.event.ApplicationEventPublisherFactory; +import io.micronaut.core.beans.BeanIntrospection; +import io.micronaut.core.beans.BeanIntrospector; +import io.micronaut.core.naming.NameUtils; +import io.micronaut.inject.BeanDefinition; +import io.micronaut.inject.BeanDefinitionReference; +import io.micronaut.inject.ast.ClassElement; +import io.micronaut.inject.provider.BeanProviderDefinition; +import io.micronaut.inject.writer.BeanDefinitionReferenceWriter; +import io.micronaut.inject.writer.BeanDefinitionVisitor; +import io.micronaut.inject.writer.BeanDefinitionWriter; +import io.micronaut.kotlin.processing.beans.BeanDefinitionProcessorProvider; +import io.micronaut.kotlin.processing.visitor.KotlinVisitorContext; +import io.micronaut.kotlin.processing.visitor.TypeElementSymbolProcessor; +import io.micronaut.kotlin.processing.visitor.TypeElementSymbolProcessorProvider; +import kotlin.Pair; +import org.intellij.lang.annotations.Language; +import org.jetbrains.annotations.NotNull; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLClassLoader; +import java.nio.file.Files; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.function.Consumer; +import java.util.stream.Stream; + +public class KotlinCompiler { + private static final KotlinCompilation KOTLIN_COMPILATION = new KotlinCompilation(); + private static final KotlinCompilation KSP_COMPILATION = new KotlinCompilation(); + + static { + + KOTLIN_COMPILATION.setJvmDefault("all"); + KOTLIN_COMPILATION.setInheritClassPath(true); + + KSP_COMPILATION.setJavacArguments(Collections.singletonList("-Xopt-in=kotlin.RequiresOptIn")); + KSP_COMPILATION.setInheritClassPath(true); + KSP_COMPILATION.setClasspaths(Arrays.asList( + new File(KSP_COMPILATION.getWorkingDir(), "ksp/classes"), + new File(KSP_COMPILATION.getWorkingDir(), "ksp/sources/resources"), + KOTLIN_COMPILATION.getClassesDir())); + } + + public static URLClassLoader buildClassLoader(String name, @Language("kotlin") String clazz) { + Pair, Pair> resultPair = compile(name, clazz, classElement -> { + }); + return toClassLoader(resultPair); + } + + @NotNull + private static URLClassLoader toClassLoader(Pair, Pair> resultPair) { + try { + Pair sourcesCompilation = resultPair.component1(); + Pair kspCompilation = resultPair.component2(); + + KotlinCompilation.Result sourcesCompileResult = sourcesCompilation.component2(); + KotlinCompilation.Result kspCompileResult = kspCompilation.component2(); + List classpath = new ArrayList<>(); + classpath.add(sourcesCompileResult.getOutputDirectory().toURI().toURL()); + classpath.add(kspCompileResult.getOutputDirectory().toURI().toURL()); + classpath.addAll(kspCompilation.component1().getClasspaths().stream().flatMap(f -> { + try { + return Stream.of(f.toURI().toURL()); + } catch (MalformedURLException e) { + return Stream.empty(); + } + }).toList()); + classpath.addAll(sourcesCompilation.component1().getClasspaths().stream().flatMap(f -> { + try { + return Stream.of(f.toURI().toURL()); + } catch (MalformedURLException e) { + return Stream.empty(); + } + }).toList()); + + return new URLClassLoader( + classpath.toArray(URL[]::new), + KotlinCompiler.class.getClassLoader() + ); + } catch (MalformedURLException e) { + throw new IllegalStateException(e.getMessage(), e); + } + } + + public static Pair, Pair> compile(String name, @Language("kotlin") String clazz, Consumer classElements) { + try { + Files.deleteIfExists(KOTLIN_COMPILATION.getWorkingDir().toPath()); + } catch (IOException e) { + // ignore + } + KOTLIN_COMPILATION.setSources(Collections.singletonList(SourceFile.Companion.kotlin(name + ".kt", clazz, true))); + KotlinCompilation.Result result = KOTLIN_COMPILATION.compile(); + if (result.getExitCode() != KotlinCompilation.ExitCode.OK) { + throw new RuntimeException(result.getMessages()); + } + + KSP_COMPILATION.setSources(KOTLIN_COMPILATION.getSources()); + ClassElementTypeElementSymbolProcessorProvider classElementTypeElementSymbolProcessorProvider = new ClassElementTypeElementSymbolProcessorProvider(classElements); + KspKt.setSymbolProcessorProviders(KSP_COMPILATION, Arrays.asList(classElementTypeElementSymbolProcessorProvider, new BeanDefinitionProcessorProvider())); + KotlinCompilation.Result kspResult = KSP_COMPILATION.compile(); + if (kspResult.getExitCode() != KotlinCompilation.ExitCode.OK) { + throw new RuntimeException(kspResult.getMessages()); + } + + return new Pair<>(new Pair<>(KOTLIN_COMPILATION, result), new Pair<>(KSP_COMPILATION, kspResult)); + } + + public static BeanIntrospection buildBeanIntrospection(String name, @Language("kotlin") String clazz) { + final URLClassLoader classLoader = buildClassLoader(name, clazz); + try { + return BeanIntrospector.forClassLoader(classLoader).findIntrospection(classLoader.loadClass(name)).orElse(null); + } catch (ClassNotFoundException e) { + return null; + } + } + + public static BeanDefinition buildBeanDefinition(String name, @Language("kotlin") String clazz) throws InstantiationException, NoSuchMethodException, InvocationTargetException, IllegalAccessException { + return buildBeanDefinition(NameUtils.getPackageName(name), + NameUtils.getSimpleName(name), + clazz); + } + + public static BeanDefinition buildBeanDefinition(String packageName, String simpleName, @Language("kotlin") String clazz) throws InstantiationException, NoSuchMethodException, InvocationTargetException, IllegalAccessException { + final URLClassLoader classLoader = buildClassLoader(packageName + "." + simpleName, clazz); + String beanDefName = (simpleName.startsWith("$") ? "" : '$') + simpleName + BeanDefinitionWriter.CLASS_SUFFIX; + String beanFullName = packageName + "." + beanDefName; + return (BeanDefinition) loadDefinition(classLoader, beanFullName); + } + + public static BeanDefinitionReference buildBeanDefinitionReference(String name, @Language("kotlin") String clazz) throws InstantiationException, NoSuchMethodException, InvocationTargetException, IllegalAccessException { + return loadReference(name, clazz, BeanDefinitionWriter.CLASS_SUFFIX + BeanDefinitionReferenceWriter.REF_SUFFIX); + } + + public static BeanDefinition buildInterceptedBeanDefinition(String className, @Language("kotlin") String cls) throws InstantiationException, NoSuchMethodException, InvocationTargetException, IllegalAccessException { + return loadReference(className, cls, BeanDefinitionWriter.CLASS_SUFFIX + BeanDefinitionVisitor.PROXY_SUFFIX + BeanDefinitionWriter.CLASS_SUFFIX); + } + + public static BeanDefinitionReference buildInterceptedBeanDefinitionReference(String className, @Language("kotlin") String cls) throws InstantiationException, NoSuchMethodException, InvocationTargetException, IllegalAccessException { + return loadReference(className, cls, BeanDefinitionWriter.CLASS_SUFFIX + BeanDefinitionVisitor.PROXY_SUFFIX + BeanDefinitionWriter.CLASS_SUFFIX + BeanDefinitionReferenceWriter.REF_SUFFIX); + } + + private static T loadReference(String className, + @Language("kotlin") String cls, + String suffix + ) throws InstantiationException, NoSuchMethodException, InvocationTargetException, IllegalAccessException { + String simpleName = NameUtils.getSimpleName(className); + String beanDefName = (simpleName.startsWith("$") ? "" : '$') + simpleName + suffix; + String packageName = NameUtils.getPackageName(className); + String beanFullName = packageName + "." + beanDefName; + + ClassLoader classLoader = buildClassLoader(className, cls); + return (T) loadDefinition(classLoader, beanFullName); + } + + public static byte[] getClassBytes(String name, @Language("kotlin") String clazz) throws FileNotFoundException, IOException { + String simpleName = NameUtils.getSimpleName(name); + String className = (simpleName.startsWith("$") ? "" : '$') + simpleName; + String packageName = NameUtils.getPackageName(name); + String fileName = packageName.replace('.', '/') + '/' + className + ".class"; + URLClassLoader classLoader = buildClassLoader(className, clazz); + File file = null; + for (URL url: classLoader.getURLs()) { + file = new File(url.getFile(), fileName); + if (file.exists()) { + break; + } else { + file = null; + } + } + if (file != null) { + try (InputStream is = new FileInputStream(file)) { + ByteArrayOutputStream answer = new ByteArrayOutputStream(); + byte[] byteBuffer = new byte[8192]; + int nbByteRead; + while ((nbByteRead = is.read(byteBuffer)) != -1) { + answer.write(byteBuffer, 0, nbByteRead); + } + return answer.toByteArray(); + } + } + return null; + } + + public static ApplicationContext buildContext(@Language("kotlin") String clazz) { + return buildContext(clazz, false); + } + + public static ApplicationContext + buildContext(@Language("kotlin") String clazz, boolean includeAllBeans) { + return buildContext(clazz, includeAllBeans, Collections.emptyMap()); + } + + @SuppressWarnings("java:S2095") + public static ApplicationContext + buildContext(@Language("kotlin") String clazz, boolean includeAllBeans, Map config) { + Pair, Pair> pair = compile("temp", clazz, classElement -> { + }); + ClassLoader classLoader = toClassLoader(pair); + var builder = ApplicationContext.builder(); + builder.classLoader(classLoader); + builder.environments("test"); + builder.properties(config); + Environment environment = builder.build().getEnvironment(); + return new DefaultApplicationContext((ApplicationContextConfiguration) builder) { + + @Override + public Environment getEnvironment() { + return environment; + } + + @Override + protected List resolveBeanDefinitionReferences() { + List beanDefinitionNames = pair.component2().component1(). + getClasspaths().stream().filter(f -> f.toURI().toString().contains("/ksp/sources/resources")) + .flatMap(dir -> { + File[] files = new File(dir, "META-INF/micronaut/io.micronaut.inject.BeanDefinitionReference").listFiles(); + if (files == null) { + return Stream.empty(); + } + return Stream.of(files).filter(f -> f.isFile()); + }).map(f -> f.getName()).toList(); + + List beanDefinitions = new ArrayList<>(beanDefinitionNames.size()); + for (String name : beanDefinitionNames) { + try { + BeanDefinitionReference br = (BeanDefinitionReference) loadDefinition(classLoader, name); + if (br != null) { + beanDefinitions.add(br); + } + } catch (NoSuchMethodException | InvocationTargetException | InstantiationException | IllegalAccessException e) { + } + } + if (includeAllBeans) { + beanDefinitions.addAll(super.resolveBeanDefinitionReferences()); + } else { + beanDefinitions.add(new InterceptorRegistryBean()); + beanDefinitions.add(new BeanProviderDefinition()); + beanDefinitions.add(new ApplicationEventPublisherFactory<>()); + } + return beanDefinitions; + } + }.start(); + } + + public static Object getBean(BeanContext beanContext, String className) throws ClassNotFoundException { + return beanContext.getBean(beanContext.getClassLoader().loadClass(className)); + } + + public static BeanDefinition getBeanDefinition(BeanContext beanContext, String className) throws ClassNotFoundException { + return beanContext.getBeanDefinition(beanContext.getClassLoader().loadClass(className)); + } + + private static Object loadDefinition(ClassLoader classLoader, String name) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException { + try { + Class c = classLoader.loadClass(name); + Constructor constructor = c.getDeclaredConstructor(); + constructor.setAccessible(true); + return constructor.newInstance(); + } catch (ClassNotFoundException e) { + return null; + } + } + + private static class ClassElementTypeElementSymbolProcessorProvider extends TypeElementSymbolProcessorProvider { + Consumer classElements; + + public ClassElementTypeElementSymbolProcessorProvider(Consumer classElements) { + this.classElements = classElements; + } + + @NotNull + @Override + public SymbolProcessor create(@NotNull SymbolProcessorEnvironment environment) { + return new TypeElementSymbolProcessor(environment) { + @NotNull + @Override + public ClassElement newClassElement(@NotNull KotlinVisitorContext visitorContext, @NotNull KSClassDeclaration classDeclaration) { + ClassElement classElement = super.newClassElement(visitorContext, classDeclaration); + classElements.accept(classElement); + return classElement; + } + }; + } + } +} diff --git a/inject-kotlin-test/src/main/kotlin/io/micronaut/annotation/processing/test/KotlinCompileHelper.kt b/inject-kotlin-test/src/main/kotlin/io/micronaut/annotation/processing/test/KotlinCompileHelper.kt deleted file mode 100644 index 2735410a962..00000000000 --- a/inject-kotlin-test/src/main/kotlin/io/micronaut/annotation/processing/test/KotlinCompileHelper.kt +++ /dev/null @@ -1,249 +0,0 @@ -/* - * Copyright 2017-2020 original authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.micronaut.annotation.processing.test - -import org.jetbrains.kotlin.base.kapt3.DetectMemoryLeaksMode -import org.jetbrains.kotlin.base.kapt3.KaptOptions -import org.jetbrains.kotlin.cli.common.CLIConfigurationKeys -import org.jetbrains.kotlin.cli.common.messages.CompilerMessageSeverity -import org.jetbrains.kotlin.cli.common.messages.CompilerMessageSourceLocation -import org.jetbrains.kotlin.cli.common.messages.MessageCollector -import org.jetbrains.kotlin.cli.jvm.compiler.CliBindingTrace -import org.jetbrains.kotlin.cli.jvm.compiler.EnvironmentConfigFiles -import org.jetbrains.kotlin.cli.jvm.compiler.KotlinCoreEnvironment -import org.jetbrains.kotlin.cli.jvm.compiler.TopDownAnalyzerFacadeForJVM -import org.jetbrains.kotlin.cli.jvm.config.JvmClasspathRoot -import org.jetbrains.kotlin.codegen.ClassBuilderMode -import org.jetbrains.kotlin.codegen.DefaultCodegenFactory -import org.jetbrains.kotlin.codegen.KotlinCodegenFacade -import org.jetbrains.kotlin.codegen.OriginCollectingClassBuilderFactory -import org.jetbrains.kotlin.codegen.state.GenerationState -import org.jetbrains.kotlin.com.intellij.psi.PsiFileFactory -import org.jetbrains.kotlin.com.intellij.psi.impl.PsiFileFactoryImpl -import org.jetbrains.kotlin.com.intellij.testFramework.LightVirtualFile -import org.jetbrains.kotlin.config.CommonConfigurationKeys -import org.jetbrains.kotlin.config.CompilerConfiguration -import org.jetbrains.kotlin.config.JVMConfigurationKeys -import org.jetbrains.kotlin.idea.KotlinLanguage -import org.jetbrains.kotlin.kapt3.AbstractKapt3Extension -import org.jetbrains.kotlin.kapt3.base.LoadedProcessors -import org.jetbrains.kotlin.kapt3.base.incremental.DeclaredProcType -import org.jetbrains.kotlin.kapt3.base.incremental.IncrementalProcessor -import org.jetbrains.kotlin.kapt3.util.MessageCollectorBackedKaptLogger -import org.jetbrains.kotlin.psi.KtFile -import org.jetbrains.kotlin.resolve.AnalyzingUtils -import org.jetbrains.kotlin.resolve.jvm.extensions.AnalysisHandlerExtension -import java.io.File -import java.io.IOException -import java.net.URL -import java.net.URLClassLoader -import java.nio.charset.StandardCharsets -import java.nio.file.FileVisitResult -import java.nio.file.FileVisitor -import java.nio.file.Files -import java.nio.file.Path -import java.nio.file.attribute.BasicFileAttributes -import java.util.* -import javax.annotation.processing.Processor - -object KotlinCompileHelper { - init { - System.setProperty("idea.ignore.disabled.plugins", "true") - System.setProperty("idea.io.use.nio2", "true") - } - - fun run(className: String, code: String): Result { - val tmp = Files.createTempDirectory("KotlinCompileHelper") - try { - return run0(tmp, className, code) - } finally { - // delete tmp dir - Files.walkFileTree(tmp, object : FileVisitor { - override fun preVisitDirectory(dir: Path, attrs: BasicFileAttributes): FileVisitResult { - return FileVisitResult.CONTINUE - } - - override fun visitFile(file: Path, attrs: BasicFileAttributes): FileVisitResult { - Files.delete(file) - return FileVisitResult.CONTINUE - } - - override fun visitFileFailed(file: Path, exc: IOException): FileVisitResult { - throw exc - } - - override fun postVisitDirectory(dir: Path, exc: IOException?): FileVisitResult { - Files.delete(dir) - return FileVisitResult.CONTINUE - } - }) - } - } - - private fun run0( - tmp: Path, - className: String, - code: String - ): Result { - // hack around org.jetbrains.kotlin.com.intellij.openapi.util.BuildNumber - System.setProperty("idea.home.path", tmp.toAbsolutePath().toString()) - Files.write(tmp.resolve("build.txt"), "999.SNAPSHOT".toByteArray()) - - val outDir = tmp.resolve("out") - Files.createDirectory(outDir) - val stubsDir = tmp.resolve("stubs") - Files.createDirectory(stubsDir) - - val configuration = CompilerConfiguration() - configuration.put(CommonConfigurationKeys.MODULE_NAME, "test-module") - val messageCollector = object : MessageCollector { - override fun clear() { - } - - override fun hasErrors() = false - - override fun report( - severity: CompilerMessageSeverity, - message: String, - location: CompilerMessageSourceLocation? - ) { - // With Java 17 and Groovy 4.x this breaks inject-kotlin-test:KotlinCompilerTest as it throws an AssertionError for the Note: message - if (severity == CompilerMessageSeverity.ERROR && !message.startsWith("Note:")) { - throw AssertionError("Error reported in processing: $message") - } - } - } - configuration.put(CLIConfigurationKeys.MESSAGE_COLLECTOR_KEY, messageCollector) - configuration.put(JVMConfigurationKeys.IR, false) - configuration.put(JVMConfigurationKeys.OUTPUT_DIRECTORY, outDir.toFile()) - configuration.put(JVMConfigurationKeys.JDK_HOME, File(System.getProperty("java.home"))) - - val env = - KotlinCoreEnvironment.createForTests({ }, configuration, EnvironmentConfigFiles.JVM_CONFIG_FILES) - - val cp = getClasspath(KotlinCompileHelper::class.java.classLoader) + getClasspathFromSystemProperty("java.class.path") + getClasspathFromSystemProperty("sun.boot.class.path") - env.updateClasspath(cp.map { - JvmClasspathRoot(it, false) - }) - - val kaptOptions = KaptOptions.Builder() - kaptOptions.projectBaseDir = tmp.toFile() - kaptOptions.sourcesOutputDir = outDir.toFile() - kaptOptions.classesOutputDir = outDir.toFile() - kaptOptions.stubsOutputDir = stubsDir.toFile() - kaptOptions.detectMemoryLeaks = DetectMemoryLeaksMode.NONE - kaptOptions.compileClasspath.addAll(cp) - - class KaptExtension : AbstractKapt3Extension( - kaptOptions.build(), - MessageCollectorBackedKaptLogger( - isVerbose = false, - isInfoAsWarnings = true, - messageCollector = messageCollector - ), - configuration - ) { - override fun loadProcessors() = LoadedProcessors( - ServiceLoader.load(Processor::class.java) - .map { IncrementalProcessor(it, DeclaredProcType.NON_INCREMENTAL, logger) }, - KotlinCompileHelper::class.java.classLoader - ) - } - - AnalysisHandlerExtension.registerExtension(env.project, KaptExtension()) - - val classBuilderFactory = OriginCollectingClassBuilderFactory(ClassBuilderMode.FULL) - val vFile = - LightVirtualFile(className.substring(className.lastIndexOf('.') + 1) + ".kt", KotlinLanguage.INSTANCE, code) - vFile.charset = StandardCharsets.UTF_8 - val psiFileFactory = PsiFileFactory.getInstance(env.project) as PsiFileFactoryImpl - val ktFile = psiFileFactory.trySetupPsiForFile(vFile, KotlinLanguage.INSTANCE, true, false) as KtFile - - val trace = CliBindingTrace() - val analysisResult = TopDownAnalyzerFacadeForJVM.analyzeFilesWithJavaIntegration( - env.project, - listOf(ktFile), - trace, - configuration, - env::createPackagePartProvider - ) - if (analysisResult.isError()) { - throw analysisResult.error - } - AnalyzingUtils.throwExceptionOnErrors(analysisResult.bindingContext) - - val genState = GenerationState.Builder( - env.project, - classBuilderFactory, - analysisResult.moduleDescriptor, - trace.bindingContext, - listOf(ktFile), - configuration - ).codegenFactory(DefaultCodegenFactory).isIrBackend(false).build() - KotlinCodegenFacade.compileCorrectFiles(genState) - - AnalyzingUtils.throwExceptionOnErrors(genState.collectedExtraJvmDiagnostics) - - val cl = MemoryClassLoader(KotlinCompileHelper::class.java.classLoader) - for (outputFile in genState.factory.currentOutput) { - cl.files[outputFile.relativePath] = outputFile.asByteArray() - } - Files.walk(outDir).filter(Files::isRegularFile).forEach { p -> - cl.files[outDir.relativize(p).toString()] = Files.readAllBytes(p) - } - - return Result(cl, cl.files.keys) - } - - private fun getClasspath(cl: ClassLoader): List = - getClasspathSingle(cl) + (cl.parent?.let { getClasspath(it) } ?: emptyList()) - - private fun getClasspathSingle(cl: ClassLoader): List { - if (cl is URLClassLoader) { - return cl.urLs.map { File(it.toURI()) } - } - // ideally, we'd look at the system class loaders too (jdk.internal.loader.BuiltinClassLoader), but they're - // protected from reflection in newer JDKs. So, we fall back to using the java.class.path system property in the - // code above, and ignore those class loaders here. - - return emptyList() - } - - private fun getClasspathFromSystemProperty(prop: String): List { - val value = System.getProperty(prop) ?: return emptyList() - return value.split(System.getProperty("path.separator")).map { File(it) } - } - - data class Result( - val classLoader: ClassLoader, - val fileNames: Collection - ) - - private class MemoryClassLoader(parent: ClassLoader?) : ClassLoader(parent) { - val files = mutableMapOf() - - override fun findResource(name: String): URL? { - val resource = files[name] ?: return null - return URL("data:text/plain;base64," + Base64.getUrlEncoder().encodeToString(resource)) - } - - override fun findClass(name: String): Class<*> { - val path = name.replace('.', '/') + ".class" - val file = files[path] ?: throw ClassNotFoundException(name) - return defineClass(name, file, 0, file.size) - } - } -} diff --git a/inject-kotlin-test/src/test/groovy/io/micronaut/annotation/processing/test/KotlinCompilerTest.groovy b/inject-kotlin-test/src/test/groovy/io/micronaut/annotation/processing/test/KotlinCompilerTest.groovy index ed249f9cc34..6ca9b2bbefe 100644 --- a/inject-kotlin-test/src/test/groovy/io/micronaut/annotation/processing/test/KotlinCompilerTest.groovy +++ b/inject-kotlin-test/src/test/groovy/io/micronaut/annotation/processing/test/KotlinCompilerTest.groovy @@ -28,7 +28,7 @@ import io.micronaut.core.annotation.Introspected @Introspected class Test { - val a: String + val a: String = "test" } ''') expect: @@ -87,6 +87,7 @@ class Test( var name: String, var getSurname: String, var isDeleted: Boolean, + var isOptional: Boolean?, val isImportant: Boolean, var corrected: Boolean, val upgraded: Boolean, @@ -105,6 +106,6 @@ class Test( } ''') expect: - introspection.propertyNames.toList() == ['id', 'name', 'getSurname', 'isDeleted', 'isImportant', 'corrected', 'upgraded', 'isMyBool', 'isMyBool2', 'myBool3', 'myBool4', 'myBool5'] + introspection.propertyNames as Set == ['id', 'name', 'getSurname', 'isDeleted', 'isOptional', 'isImportant', 'corrected', 'upgraded', 'isMyBool', 'isMyBool2', 'myBool3', 'myBool4', 'myBool5'] as Set } } diff --git a/inject-kotlin/build.gradle b/inject-kotlin/build.gradle new file mode 100644 index 00000000000..2cf77e47a30 --- /dev/null +++ b/inject-kotlin/build.gradle @@ -0,0 +1,65 @@ +plugins { + id "io.micronaut.build.internal.convention-library" + id "org.jetbrains.kotlin.jvm" + id "com.google.devtools.ksp" version "1.8.0-Beta-1.0.8" + +} + +micronautBuild { + core { + usesMicronautTest() + } +} + +dependencies { + api project(":core-processor") + + implementation(libs.managed.ksp.api) + if (!JavaVersion.current().isJava9Compatible()) { + api files(org.gradle.internal.jvm.Jvm.current().toolsJar) + } + kspTest(project) + testImplementation project(":jackson-databind") + testImplementation project(":inject-kotlin-test") + testImplementation libs.kotlin.stdlib + testImplementation project(':http-client') + testImplementation libs.managed.jackson.annotations + testImplementation libs.managed.validation + testImplementation libs.managed.reactor + testImplementation libs.hibernate + testImplementation project(":validation") + testImplementation libs.javax.persistence + testImplementation project(":runtime") + testImplementation(libs.neo4j.bolt) + testImplementation libs.kotlinx.coroutines.core + testImplementation libs.kotlinx.coroutines.jdk8 + testImplementation libs.kotlinx.coroutines.rx2 + +} + +afterEvaluate { + sourcesJar { + from "$projectDir/src/main/kotlin" + } +} + +tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all { + kotlinOptions { + freeCompilerArgs = ['-Xjvm-default=all'] + } +} + +tasks.named("compileTestGroovy") { + classpath += files(tasks.compileTestKotlin) +} + +tasks.named("test") { + classpath += files(tasks.compileTestKotlin) +// testLogging { +// showStandardStreams = true +// } + maxHeapSize("1G") + forkEvery = 40 + maxParallelForks = 2 +} + diff --git a/inject-kotlin/gradle.properties b/inject-kotlin/gradle.properties new file mode 100644 index 00000000000..48335b1e143 --- /dev/null +++ b/inject-kotlin/gradle.properties @@ -0,0 +1 @@ +ksp.incremental = false diff --git a/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/KotlinOutputVisitor.kt b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/KotlinOutputVisitor.kt new file mode 100644 index 00000000000..4938996dd6e --- /dev/null +++ b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/KotlinOutputVisitor.kt @@ -0,0 +1,81 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.kotlin.processing + +import com.google.devtools.ksp.containingFile +import com.google.devtools.ksp.processing.Dependencies +import com.google.devtools.ksp.processing.SymbolProcessorEnvironment +import com.google.devtools.ksp.symbol.KSFile +import io.micronaut.inject.ast.Element +import io.micronaut.inject.writer.AbstractClassWriterOutputVisitor +import io.micronaut.inject.writer.GeneratedFile +import io.micronaut.kotlin.processing.visitor.AbstractKotlinElement +import io.micronaut.kotlin.processing.visitor.KotlinVisitorContext +import java.io.File +import java.io.OutputStream +import java.util.* + +class KotlinOutputVisitor(private val environment: SymbolProcessorEnvironment): AbstractClassWriterOutputVisitor(false) { + + override fun visitClass(classname: String, vararg originatingElements: Element): OutputStream { + return environment.codeGenerator.createNewFile( + getNativeElements(originatingElements), + classname.substringBeforeLast('.'), + classname.substringAfterLast('.'), + "class") + } + + override fun visitServiceDescriptor(type: String, classname: String, originatingElement: Element) { + environment.codeGenerator.createNewFile( + getNativeElements(arrayOf(originatingElement)), + "META-INF.micronaut", + "${type}${File.separator}${classname}", + "").use { + it.bufferedWriter().write("") + } + } + + override fun visitMetaInfFile(path: String, vararg originatingElements: Element): Optional { + val elements = path.split(File.separator).toMutableList() + elements.add(0, "META-INF") + val file = elements.removeAt(elements.size - 1) + + val stream = environment.codeGenerator.createNewFile( + getNativeElements(originatingElements), + elements.joinToString("."), + file.substringBeforeLast('.'), + file.substringAfterLast('.')) + + return Optional.of(KotlinVisitorContext.KspGeneratedFile(stream, elements.joinToString(File.separator))) + } + + override fun visitGeneratedFile(path: String?): Optional { + TODO("Not yet implemented") + } + + private fun getNativeElements(originatingElements: Array): Dependencies { + val originatingFiles: MutableList = ArrayList(originatingElements.size) + for (originatingElement in originatingElements) { + if (originatingElement is AbstractKotlinElement<*>) { + val nativeType = originatingElement.nativeType.unwrap().containingFile + if (nativeType is KSFile) { + originatingFiles.add(nativeType) + } + } + } + return Dependencies(aggregating = false, sources = originatingFiles.toTypedArray()) + } +} diff --git a/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/annotation/KotlinAnnotationMetadataBuilder.kt b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/annotation/KotlinAnnotationMetadataBuilder.kt new file mode 100644 index 00000000000..3a51a2de0b5 --- /dev/null +++ b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/annotation/KotlinAnnotationMetadataBuilder.kt @@ -0,0 +1,591 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.kotlin.processing.annotation + +import com.google.devtools.ksp.getClassDeclarationByName +import com.google.devtools.ksp.getDeclaredProperties +import com.google.devtools.ksp.isConstructor +import com.google.devtools.ksp.isDefault +import com.google.devtools.ksp.processing.Resolver +import com.google.devtools.ksp.processing.SymbolProcessorEnvironment +import com.google.devtools.ksp.symbol.* +import io.micronaut.context.annotation.ConfigurationReader +import io.micronaut.context.annotation.Property +import io.micronaut.core.annotation.AnnotationClassValue +import io.micronaut.core.annotation.AnnotationUtil +import io.micronaut.core.reflect.ReflectionUtils +import io.micronaut.core.util.ArrayUtils +import io.micronaut.core.util.clhm.ConcurrentLinkedHashMap +import io.micronaut.core.value.OptionalValues +import io.micronaut.inject.annotation.AbstractAnnotationMetadataBuilder +import io.micronaut.inject.annotation.MutableAnnotationMetadata +import io.micronaut.inject.visitor.VisitorContext +import io.micronaut.kotlin.processing.getBinaryName +import io.micronaut.kotlin.processing.getClassDeclaration +import io.micronaut.kotlin.processing.visitor.KotlinVisitorContext +import java.lang.annotation.Inherited +import java.lang.annotation.RetentionPolicy +import java.lang.reflect.Method +import java.util.* + +class KotlinAnnotationMetadataBuilder(private val symbolProcessorEnvironment: SymbolProcessorEnvironment, + private val resolver: Resolver, + private val visitorContext: KotlinVisitorContext): AbstractAnnotationMetadataBuilder() { + + private val annotationDefaultsCache: ConcurrentLinkedHashMap> = + ConcurrentLinkedHashMap.Builder>().maximumWeightedCapacity(200).build() + + companion object { + private fun getTypeForAnnotation(annotationMirror: KSAnnotation, visitorContext: KotlinVisitorContext): KSClassDeclaration { + return annotationMirror.annotationType.resolve().declaration.getClassDeclaration(visitorContext) + } + fun getAnnotationTypeName(annotationMirror: KSAnnotation, visitorContext: KotlinVisitorContext): String { + val type = getTypeForAnnotation(annotationMirror, visitorContext) + return if (type.qualifiedName != null) { + type.qualifiedName!!.asString() + } else { + println("Failed to get the qualified name of ${annotationMirror.shortName.asString()} annotation") + annotationMirror.shortName.asString() + } + } + } + + override fun getTypeForAnnotation(annotationMirror: KSAnnotation): KSClassDeclaration { + return Companion.getTypeForAnnotation(annotationMirror, visitorContext) + } + + override fun hasAnnotation(element: KSAnnotated, annotation: Class): Boolean { + return hasAnnotation(element, annotation.name) + } + + override fun hasAnnotation(element: KSAnnotated, annotation: String): Boolean { + return element.annotations.map { + it.annotationType.resolve().declaration.qualifiedName + }.any { + it?.asString() == annotation + } + } + + override fun hasAnnotations(element: KSAnnotated): Boolean { + return if (element is KSPropertyDeclaration) { + element.annotations.iterator().hasNext() || + element.getter?.annotations?.iterator()?.hasNext() ?: false + } else { + element.annotations.iterator().hasNext() + } + } + + override fun getAnnotationTypeName(annotationMirror: KSAnnotation): String { + return Companion.getAnnotationTypeName(annotationMirror, visitorContext) + } + + override fun getElementName(element: KSAnnotated): String { + if (element is KSDeclaration) { + return if (element is KSClassDeclaration) { + element.qualifiedName!!.asString() + } else { + element.simpleName.asString() + } + } + TODO("Not yet implemented") + } + + override fun getAnnotationsForType(element: KSAnnotated): MutableList { + val annotationMirrors : MutableList = mutableListOf() + + if (element is KSValueParameter) { + // fuse annotations for setter and property + val parent = element.parent + if (parent is KSPropertySetter) { + val property = parent.parent + if (property is KSPropertyDeclaration) { + annotationMirrors.addAll(property.annotations) + } + annotationMirrors.addAll(parent.annotations) + } + annotationMirrors.addAll(element.annotations) + } else if (element is KSPropertyGetter || element is KSPropertySetter) { + val property = element.parent + if (property is KSPropertyDeclaration) { + annotationMirrors.addAll(property.annotations) + } + annotationMirrors.addAll(element.annotations) + } else if (element is KSPropertyDeclaration) { + val parent : KSClassDeclaration? = findClassDeclaration(element) + if (parent is KSClassDeclaration && parent.classKind == ClassKind.ANNOTATION_CLASS) { + annotationMirrors.addAll(element.annotations) + val getter = element.getter + if (getter != null) { + annotationMirrors.addAll(getter.annotations) + } + } else { + annotationMirrors.addAll(element.annotations) + } + } else { + annotationMirrors.addAll(element.annotations) + } + val expanded : MutableList = mutableListOf() + for (ann in annotationMirrors) { + val annotationName = getAnnotationTypeName(ann) + var repeateable = false + var hasOtherMembers = false + for (arg in ann.arguments) { + if ("value" == arg.name?.asString()) { + val value = arg.value + if (value is Iterable<*>) { + for (nested in value) { + if (nested is KSAnnotation) { + val repeatableName = getRepeatableName(nested) + if (repeatableName != null && repeatableName == annotationName) { + expanded.add(nested) + repeateable = true + } + } + } + } + } else { + hasOtherMembers = true + } + } + + if (!repeateable || hasOtherMembers) { + expanded.add(ann) + } + } + return expanded + } + + private fun findClassDeclaration(element: KSPropertyDeclaration): KSClassDeclaration? { + var parent = element.parent + while (parent != null) { + if (parent is KSClassDeclaration) { + return parent + } + parent = parent.parent + } + return null + } + + override fun postProcess(annotationMetadata: MutableAnnotationMetadata, element: KSAnnotated) { + if (element is KSValueParameter && element.type.resolve().isMarkedNullable) { + annotationMetadata.addDeclaredAnnotation(AnnotationUtil.NULLABLE, emptyMap()) + } else if (element is KSFunctionDeclaration) { + val markedNullable = element.returnType?.resolve()?.isMarkedNullable + if (markedNullable != null && markedNullable) { + annotationMetadata.addDeclaredAnnotation(AnnotationUtil.NULLABLE, emptyMap()) + } + } else if (element is KSPropertyDeclaration) { + val markedNullable = element.type.resolve().isMarkedNullable + if (markedNullable) { + annotationMetadata.addDeclaredAnnotation(AnnotationUtil.NULLABLE, emptyMap()) + } + } else if (element is KSPropertySetter) { + if (!annotationMetadata.hasAnnotation(JvmField::class.java) && (annotationMetadata.hasStereotype(AnnotationUtil.QUALIFIER) || annotationMetadata.hasAnnotation(Property::class.java))) { + // implicitly inject + annotationMetadata.addDeclaredAnnotation(AnnotationUtil.INJECT, emptyMap()) + } + } + } + + override fun buildHierarchy( + element: KSAnnotated, + inheritTypeAnnotations: Boolean, + declaredOnly: Boolean + ): MutableList { + if (declaredOnly) { + return mutableListOf(element) + } + if (element is KSClassDeclaration) { + val hierarchy = mutableListOf() + hierarchy.add(element) + if (element.classKind == ClassKind.ANNOTATION_CLASS) { + return hierarchy + } + populateTypeHierarchy(element, hierarchy) + hierarchy.reverse() + return hierarchy + } else if (element is KSFunctionDeclaration) { + return if (element.isConstructor()) { + mutableListOf(element) + } else { + val hierarchy = mutableListOf(element) + var overidden = element.findOverridee() + while (overidden != null) { + hierarchy.add(overidden) + overidden = (overidden as KSFunctionDeclaration).findOverridee() + } + hierarchy + } + } else { + return mutableListOf(element) + } + } + + override fun readAnnotationRawValues( + originatingElement: KSAnnotated, + annotationName: String, + member: KSAnnotated, + memberName: String, + annotationValue: Any, + annotationValues: MutableMap + ) { + if (!annotationValues.containsKey(memberName)) { + val value = readAnnotationValue(originatingElement, member, memberName, annotationValue) + if (value != null) { + validateAnnotationValue(originatingElement, annotationName, member, memberName, value) + annotationValues[memberName] = value + } + } + } + + override fun isValidationRequired(member: KSAnnotated?): Boolean { + if (member != null) { + return member.annotations.any { + val name = it.annotationType.resolve().declaration.qualifiedName?.asString() + if (name != null) { + return name.startsWith("javax.validation") || name.startsWith("jakarta.validation") + } else { + return false + } + } + } + return false + } + + override fun addError(originatingElement: KSAnnotated, error: String) { + symbolProcessorEnvironment.logger.error(error, originatingElement) + } + + override fun addWarning(originatingElement: KSAnnotated, warning: String) { + symbolProcessorEnvironment.logger.warn(warning, originatingElement) + } + + override fun readAnnotationValue( + originatingElement: KSAnnotated, + member: KSAnnotated, + memberName: String, + annotationValue: Any + ): Any? { + return when (annotationValue) { + is Collection<*> -> { + toArray(annotationValue, originatingElement) + } + is Array<*> -> { + toArray(annotationValue.toList(), originatingElement) + } + else -> readAnnotationValue(originatingElement, annotationValue) + } + } + + private fun toArray( + annotationValue: Collection<*>, + originatingElement: KSAnnotated + ): Array? { + var valueType = Any::class.java + val collection = annotationValue.map { + val v = readAnnotationValue(originatingElement, it) + if (v != null) { + valueType = v.javaClass + } + v + } + return ArrayUtils.toArray(collection, valueType) + } + + override fun readAnnotationDefaultValues(annotationMirror: KSAnnotation): MutableMap { + val defaultArguments = annotationMirror.defaultArguments + val declaration = annotationMirror.annotationType.getClassDeclaration(visitorContext) + val allProperties = declaration.getAllProperties() + val map = mutableMapOf() + for (defaultArgument in defaultArguments) { + val name = defaultArgument.name + val value = defaultArgument.value + if (name != null && value != null) { + val dec = allProperties.find { it.simpleName.asString() == name.asString() } + if (dec != null) { + map[dec] = value + } + } + } + return map + } + + override fun readAnnotationDefaultValues( + annotationName: String, + annotationType: KSAnnotated + ): MutableMap { + // issue getting default values for an annotation here + // TODO: awful hack due to https://github.com/google/ksp/issues/642 and not being able to access annotation defaults for a type + val classDeclaration = annotationType.getClassDeclaration(visitorContext) + val qualifiedName = classDeclaration.qualifiedName + return if (qualifiedName != null) { + annotationDefaultsCache.computeIfAbsent(qualifiedName.asString()) { + readDefaultValuesReflectively( + classDeclaration, + annotationType, + "getDescriptor", + "getJClass", + "getMethods" + ) + } + } else { + mutableMapOf() + } + } + + private fun readDefaultValuesReflectively(classDeclaration : KSClassDeclaration, annotationType: KSAnnotated, vararg path : String): MutableMap { + var o: Any? = findValueReflectively(annotationType, *path) + val declaredProperties = classDeclaration.getDeclaredProperties() + val map = mutableMapOf() + if (o != null) { + if (o is Iterable<*>) { + for (m in o) { + if (m != null) { + val name = findValueReflectively(m, "getName") + // currently only handles JavaLiteralAnnotationArgument but probably should handle others + val value = + findValueReflectively(m, "getAnnotationParameterDefaultValue", "getValue") + if (value != null && name != null) { + val ksPropertyDeclaration = declaredProperties.find { it.simpleName.asString() == name.toString() } + if (ksPropertyDeclaration != null) { + map[ksPropertyDeclaration] = value + } + } + } + } + } + } + return map + } + + private fun findValueReflectively( + root: Any, + vararg path : String + ): Any? { + var m: Method? + var o: Any = root + for (p in path) { + m = ReflectionUtils.findMethod(o.javaClass, p).orElse(null) + if (m == null) { + return null + } else { + try { + o = m.invoke(o) + if (o == null) { + return null + } + } catch (e: Exception) { + return null + } + } + } + return o + } + + override fun readAnnotationRawValues(annotationMirror: KSAnnotation): MutableMap { + val map = mutableMapOf() + val declaration = annotationMirror.annotationType.resolve().declaration.getClassDeclaration(visitorContext) + declaration.getAllProperties().forEach { prop -> + val argument = annotationMirror.arguments.find { it.name == prop.simpleName } + if (argument?.value != null && !argument.isDefault()) { + val value = argument.value!! + map[prop] = value + } + } + return map + } + + override fun getAnnotationValues( + originatingElement: KSAnnotated, + member: KSAnnotated, + annotationType: Class<*> + ): OptionalValues<*> { + val annotationMirrors: MutableList = (member as KSPropertyDeclaration).getter!!.annotations.toMutableList() + annotationMirrors.addAll(member.annotations.toList()) + val annotationName = annotationType.name + for (annotationMirror in annotationMirrors) { + if (annotationMirror.annotationType.resolve().declaration.qualifiedName?.asString() == annotationName) { + val values: Map = readAnnotationRawValues(annotationMirror) + val converted: MutableMap = mutableMapOf() + for ((key, value1) in values) { + val value = value1!! + readAnnotationRawValues( + originatingElement, + annotationName, + member, + key.simpleName.asString(), + value, + converted + ) + } + return OptionalValues.of(Any::class.java, converted) + } + } + return OptionalValues.empty() + } + + override fun getAnnotationMemberName(member: KSAnnotated): String { + if (member is KSDeclaration) { + return member.simpleName.asString() + } + TODO("Not yet implemented") + } + + override fun getRepeatableName(annotationMirror: KSAnnotation): String? { + return getRepeatableNameForType(annotationMirror.annotationType) + } + + override fun getRepeatableNameForType(annotationType: KSAnnotated): String? { + val name = java.lang.annotation.Repeatable::class.java.name + val repeatable = annotationType.getClassDeclaration(visitorContext).annotations.find { + it.annotationType.resolve().declaration.qualifiedName?.asString() == name + } + if (repeatable != null) { + val value = repeatable.arguments.find { it.name?.asString() == "value" }?.value + if (value != null) { + val declaration = (value as KSType).declaration.getClassDeclaration(visitorContext) + return declaration.getBinaryName(resolver, visitorContext) + } + } + return null + } + + override fun getAnnotationMirror(annotationName: String): Optional { + return Optional.ofNullable(resolver.getClassDeclarationByName(annotationName)) + } + + override fun getAnnotationMember(originatingElement: KSAnnotated, member: CharSequence): KSAnnotated? { + if (originatingElement is KSAnnotation) { + return originatingElement.arguments.find { it.name == member } + } + return null + } + + override fun createVisitorContext(): VisitorContext { + return KotlinVisitorContext(symbolProcessorEnvironment, resolver) + } + + override fun getRetentionPolicy(annotation: KSAnnotated): RetentionPolicy { + var retention = annotation.annotations.find { + getAnnotationTypeName(it) == java.lang.annotation.Retention::class.java.name + } + if (retention != null) { + val value = retention.arguments.find { it.name?.asString() == "value" }?.value + if (value is KSType) { + return toRetentionPolicy(value) + } + } else { + retention = annotation.annotations.find { + getAnnotationTypeName(it) == Retention::class.java.name + } + if (retention != null) { + val value = retention.arguments.find { it.name?.asString() == "value" }?.value + if (value is KSType) { + return toJavaRetentionPolicy(value) + } + } + } + return RetentionPolicy.RUNTIME + } + + private fun toRetentionPolicy(value: KSType) = + RetentionPolicy.valueOf(value.declaration.qualifiedName!!.getShortName()) + + private fun toJavaRetentionPolicy(value: KSType) = + when (AnnotationRetention.valueOf(value.declaration.qualifiedName!!.getShortName())) { + AnnotationRetention.RUNTIME -> { + RetentionPolicy.RUNTIME + } + + AnnotationRetention.SOURCE -> { + RetentionPolicy.SOURCE + } + + AnnotationRetention.BINARY -> { + RetentionPolicy.CLASS + } + } + + override fun isInheritedAnnotation(annotationMirror: KSAnnotation): Boolean { + return annotationMirror.annotationType.resolve().declaration.annotations.any { + it.annotationType.resolve().declaration.qualifiedName?.asString() == Inherited::class.qualifiedName + } + } + + override fun isInheritedAnnotationType(annotationType: KSAnnotated): Boolean { + return annotationType.annotations.any { + it.annotationType.resolve().declaration.qualifiedName?.asString() == Inherited::class.qualifiedName + } + } + + private fun populateTypeHierarchy(element: KSClassDeclaration, hierarchy: MutableList) { + element.superTypes.forEach { + val t = it.resolve() + if (t != resolver.builtIns.anyType) { + val declaration = t.declaration + if (!hierarchy.contains(declaration)) { + hierarchy.add(declaration) + populateTypeHierarchy(declaration.getClassDeclaration(visitorContext), hierarchy) + } + } + } + } + + private fun readAnnotationValue(originatingElement: KSAnnotated, value: Any?): Any? { + if (value == null) { + return null + } + if (value is KSType) { + val declaration = value.declaration + if (declaration is KSClassDeclaration) { + if (declaration.classKind == ClassKind.ENUM_ENTRY) { + return declaration.qualifiedName?.getShortName() + } + if (declaration.classKind == ClassKind.CLASS || + declaration.classKind == ClassKind.INTERFACE || + declaration.classKind == ClassKind.ANNOTATION_CLASS) { + return AnnotationClassValue(declaration.getBinaryName(resolver, visitorContext)) + } + } + } + if (value is KSAnnotation) { + return readNestedAnnotationValue(originatingElement, value) + } + return value + } + + override fun getAnnotationMembers(annotationType: String): MutableMap { + val annotationMirror = getAnnotationMirror(annotationType) + val members = mutableMapOf() + if (annotationMirror.isPresent) { + (annotationMirror.get().getClassDeclaration(visitorContext)).getDeclaredProperties() + .forEach { + members[it.simpleName.asString()] = it + } + } + return members + } + + override fun hasSimpleAnnotation(element: KSAnnotated, simpleName: String): Boolean { + val annotationMirrors: MutableList = element.annotations.toMutableList() + if (element is KSPropertyDeclaration) { + annotationMirrors.addAll(element.getter!!.annotations) + } + return annotationMirrors.any { + it.annotationType.resolve().declaration.simpleName.asString() == simpleName + } + } +} diff --git a/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/annotation/KotlinElementAnnotationMetadataFactory.kt b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/annotation/KotlinElementAnnotationMetadataFactory.kt new file mode 100644 index 00000000000..ffb636f20fa --- /dev/null +++ b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/annotation/KotlinElementAnnotationMetadataFactory.kt @@ -0,0 +1,30 @@ +/* + * Copyright 2017-2023 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.kotlin.processing.annotation + +import com.google.devtools.ksp.symbol.KSAnnotated +import com.google.devtools.ksp.symbol.KSAnnotation +import io.micronaut.inject.ast.annotation.AbstractElementAnnotationMetadataFactory +import io.micronaut.inject.ast.annotation.ElementAnnotationMetadataFactory + +class KotlinElementAnnotationMetadataFactory( + isReadOnly: Boolean, + metadataBuilder: KotlinAnnotationMetadataBuilder +) : AbstractElementAnnotationMetadataFactory(isReadOnly, metadataBuilder) { + override fun readOnly(): ElementAnnotationMetadataFactory { + return KotlinElementAnnotationMetadataFactory(true, metadataBuilder as KotlinAnnotationMetadataBuilder) + } +} diff --git a/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/beans/BeanDefinitionProcessor.kt b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/beans/BeanDefinitionProcessor.kt new file mode 100644 index 00000000000..dc5243f8217 --- /dev/null +++ b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/beans/BeanDefinitionProcessor.kt @@ -0,0 +1,135 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.kotlin.processing.beans + +import com.google.devtools.ksp.processing.Resolver +import com.google.devtools.ksp.processing.SymbolProcessor +import com.google.devtools.ksp.processing.SymbolProcessorEnvironment +import com.google.devtools.ksp.symbol.* +import io.micronaut.context.annotation.Context +import io.micronaut.core.annotation.Generated +import io.micronaut.inject.annotation.AbstractAnnotationMetadataBuilder +import io.micronaut.inject.processing.BeanDefinitionCreator +import io.micronaut.inject.processing.BeanDefinitionCreatorFactory +import io.micronaut.inject.processing.ProcessingException +import io.micronaut.inject.visitor.VisitorConfiguration +import io.micronaut.inject.writer.BeanDefinitionReferenceWriter +import io.micronaut.inject.writer.BeanDefinitionVisitor +import io.micronaut.kotlin.processing.KotlinOutputVisitor +import io.micronaut.kotlin.processing.visitor.KotlinClassElement +import io.micronaut.kotlin.processing.visitor.KotlinVisitorContext +import java.io.IOException + +class BeanDefinitionProcessor(private val environment: SymbolProcessorEnvironment): SymbolProcessor { + + private val beanDefinitionMap = mutableMapOf() + + override fun process(resolver: Resolver): List { + val visitorContext = object : KotlinVisitorContext(environment, resolver) { + override fun getConfiguration(): VisitorConfiguration { + return object : VisitorConfiguration { + override fun includeTypeLevelAnnotationsInGenericArguments(): Boolean { + return false + } + } + } + } + + val elements = resolver.getAllFiles() + .flatMap { file: KSFile -> + file.declarations + } + .filterIsInstance() + .filter { declaration: KSClassDeclaration -> + declaration.annotations.none { ksAnnotation -> + ksAnnotation.shortName.getQualifier() == Generated::class.simpleName + } + } + .toList() + + processClassDeclarations(elements, visitorContext) + return emptyList() + } + + private fun processClassDeclarations( + elements: List, + visitorContext: KotlinVisitorContext + ) { + for (classDeclaration in elements) { + if (classDeclaration.classKind != ClassKind.ANNOTATION_CLASS) { + val classElement = + visitorContext.elementFactory.newClassElement(classDeclaration.asStarProjectedType()) as KotlinClassElement + val innerClasses = + classDeclaration.declarations + .filter { it is KSClassDeclaration } + .map { it as KSClassDeclaration } + .filter { !it.modifiers.contains(Modifier.INNER) } + .toList() + if (innerClasses.isNotEmpty()) { + processClassDeclarations(innerClasses, visitorContext) + } + beanDefinitionMap.computeIfAbsent(classElement.name) { + BeanDefinitionCreatorFactory.produce(classElement, visitorContext) + } + } + } + } + + override fun finish() { + try { + val outputVisitor = KotlinOutputVisitor(environment) + val processed = HashSet() + for (beanDefinitionCreator in beanDefinitionMap.values) { + for (writer in beanDefinitionCreator.build()) { + if (processed.add(writer.beanDefinitionName)) { + processBeanDefinitions(writer, outputVisitor, processed) + } + } + } + } catch (e: ProcessingException) { + environment.logger.error(e.message!!, e.originatingElement as KSNode) + } finally { + AbstractAnnotationMetadataBuilder.clearMutated() + beanDefinitionMap.clear() + } + } + + private fun processBeanDefinitions( + beanDefinitionWriter: BeanDefinitionVisitor, + outputVisitor: KotlinOutputVisitor, + processed: HashSet + ) { + try { + beanDefinitionWriter.visitBeanDefinitionEnd() + if (beanDefinitionWriter.isEnabled) { + beanDefinitionWriter.accept(outputVisitor) + val beanDefinitionReferenceWriter = BeanDefinitionReferenceWriter(beanDefinitionWriter) + beanDefinitionReferenceWriter.setRequiresMethodProcessing(beanDefinitionWriter.requiresMethodProcessing()) + val className = beanDefinitionReferenceWriter.beanDefinitionQualifiedClassName + processed.add(className) + beanDefinitionReferenceWriter.setContextScope( + beanDefinitionWriter.annotationMetadata.hasDeclaredAnnotation(Context::class.java) + ) + beanDefinitionReferenceWriter.accept(outputVisitor) + } + } catch (e: IOException) { + // raise a compile error + val message = e.message + error("Unexpected error ${e.javaClass.simpleName}:" + (message ?: e.javaClass.simpleName)) + } + } + +} diff --git a/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/beans/BeanDefinitionProcessorProvider.kt b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/beans/BeanDefinitionProcessorProvider.kt new file mode 100644 index 00000000000..ebcb347eb6a --- /dev/null +++ b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/beans/BeanDefinitionProcessorProvider.kt @@ -0,0 +1,27 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.kotlin.processing.beans + +import com.google.devtools.ksp.processing.SymbolProcessor +import com.google.devtools.ksp.processing.SymbolProcessorEnvironment +import com.google.devtools.ksp.processing.SymbolProcessorProvider + +class BeanDefinitionProcessorProvider: SymbolProcessorProvider { + + override fun create(environment: SymbolProcessorEnvironment): SymbolProcessor { + return BeanDefinitionProcessor(environment) + } +} diff --git a/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/extensions.kt b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/extensions.kt new file mode 100644 index 00000000000..e0e674d4ab8 --- /dev/null +++ b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/extensions.kt @@ -0,0 +1,138 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.kotlin.processing + +import com.google.devtools.ksp.KspExperimental +import com.google.devtools.ksp.getJavaClassByName +import com.google.devtools.ksp.processing.Resolver +import com.google.devtools.ksp.symbol.* +import io.micronaut.inject.ast.Element +import io.micronaut.kotlin.processing.visitor.AbstractKotlinElement +import io.micronaut.kotlin.processing.visitor.KSAnnotatedReference +import io.micronaut.kotlin.processing.visitor.KotlinVisitorContext +import java.lang.StringBuilder + +@OptIn(KspExperimental::class) +fun KSDeclaration.getBinaryName(resolver: Resolver, visitorContext: KotlinVisitorContext): String { + var declaration = unwrap() as KSDeclaration + if (declaration is KSFunctionDeclaration) { + val parent = declaration.parentDeclaration + if (parent != null) { + declaration = parent + } + } + val binaryName = resolver.mapKotlinNameToJava(declaration.qualifiedName!!)?.asString() + return if (binaryName != null) { + binaryName + } else { + val classDeclaration = declaration.getClassDeclaration(visitorContext) + val qn = classDeclaration.qualifiedName + if (qn != null) { + resolver.mapKotlinNameToJava(qn)?.asString() ?: computeName(declaration) + } else { + computeName(declaration) + } + } +} + +private fun computeName(declaration: KSDeclaration): String { + val className = StringBuilder(declaration.packageName.asString()) + val hierarchy = mutableListOf(declaration) + var parentDeclaration = declaration.parentDeclaration + while (parentDeclaration is KSClassDeclaration) { + hierarchy.add(0, parentDeclaration) + parentDeclaration = parentDeclaration.parentDeclaration + } + hierarchy.joinTo(className, "$", ".") + return className.toString() +} + +fun KSNode.unwrap() : KSNode { + return if (this is KSAnnotatedReference) { + this.node + } else { + this + } +} + +fun Element.kspNode() : Any { + return if (this is AbstractKotlinElement<*>) { + this.nativeType.unwrap() + } else { + this.nativeType + } +} + +fun KSPropertySetter.getVisibility(): Visibility { + val modifierSet = try { + this.modifiers + } catch (e: IllegalStateException) { + // KSP bug: IllegalStateException: unhandled visibility: invisible_fake + setOf(Modifier.INTERNAL) + } + return when { + modifierSet.contains(Modifier.PUBLIC) -> Visibility.PUBLIC + modifierSet.contains(Modifier.PRIVATE) -> Visibility.PRIVATE + modifierSet.contains(Modifier.PROTECTED) || + modifierSet.contains(Modifier.OVERRIDE) -> Visibility.PROTECTED + modifierSet.contains(Modifier.INTERNAL) -> Visibility.INTERNAL + else -> if (this.origin != Origin.JAVA && this.origin != Origin.JAVA_LIB) + Visibility.PUBLIC else Visibility.JAVA_PACKAGE + } +} + +@OptIn(KspExperimental::class) +fun KSAnnotated.getClassDeclaration(visitorContext: KotlinVisitorContext) : KSClassDeclaration { + when (this) { + is KSType -> { + return this.declaration.getClassDeclaration(visitorContext) + } + is KSClassDeclaration -> { + return this + } + is KSTypeReference -> { + return this.resolve().declaration.getClassDeclaration(visitorContext) + } + is KSTypeParameter -> { + return resolveDeclaration( + this.bounds.firstOrNull()?.resolve()?.declaration, + visitorContext + ) + } + is KSTypeArgument -> { + return resolveDeclaration(this.type?.resolve()?.declaration, visitorContext) + } + is KSTypeAlias -> { + val declaration = this.type.resolve().declaration + return declaration.getClassDeclaration(visitorContext) + } + else -> { + return visitorContext.resolver.getJavaClassByName(Object::class.java.name)!! + } + } +} + +@OptIn(KspExperimental::class) +private fun resolveDeclaration( + declaration: KSDeclaration?, + visitorContext: KotlinVisitorContext +): KSClassDeclaration { + return if (declaration is KSClassDeclaration) { + declaration + } else { + visitorContext.resolver.getJavaClassByName(Object::class.java.name)!! + } +} diff --git a/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/AbstractKotlinElement.kt b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/AbstractKotlinElement.kt new file mode 100644 index 00000000000..0106ab79c54 --- /dev/null +++ b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/AbstractKotlinElement.kt @@ -0,0 +1,328 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.kotlin.processing.visitor + +import com.google.devtools.ksp.KspExperimental +import com.google.devtools.ksp.getVisibility +import com.google.devtools.ksp.isJavaPackagePrivate +import com.google.devtools.ksp.isOpen +import com.google.devtools.ksp.symbol.* +import io.micronaut.core.annotation.AnnotationMetadata +import io.micronaut.core.annotation.AnnotationValue +import io.micronaut.core.annotation.AnnotationValueBuilder +import io.micronaut.inject.ast.ClassElement +import io.micronaut.inject.ast.Element +import io.micronaut.inject.ast.ElementModifier +import io.micronaut.inject.ast.GenericPlaceholderElement +import io.micronaut.inject.ast.annotation.ElementAnnotationMetadata +import io.micronaut.inject.ast.annotation.ElementAnnotationMetadataFactory +import io.micronaut.inject.ast.annotation.ElementMutableAnnotationMetadataDelegate +import io.micronaut.inject.ast.annotation.MutableAnnotationMetadataDelegate +import io.micronaut.kotlin.processing.getBinaryName +import io.micronaut.kotlin.processing.unwrap +import java.util.* +import java.util.function.Consumer +import java.util.function.Predicate + +abstract class AbstractKotlinElement(val declaration: T, + protected val annotationMetadataFactory: ElementAnnotationMetadataFactory, + protected val visitorContext: KotlinVisitorContext) : Element, ElementMutableAnnotationMetadataDelegate { + + protected var presetAnnotationMetadata: AnnotationMetadata? = null + private var elementAnnotationMetadata: ElementAnnotationMetadata? = null + + override fun getNativeType(): T { + return declaration + } + + override fun isProtected(): Boolean { + return if (declaration is KSDeclaration) { + declaration.getVisibility() == Visibility.PROTECTED + } else { + false + } + } + + override fun isStatic(): Boolean { + return if (declaration is KSDeclaration) { + declaration.modifiers.contains(Modifier.JAVA_STATIC) + } else { + false + } + } + + protected fun makeCopy(): AbstractKotlinElement { + val element: AbstractKotlinElement = copyThis() + copyValues(element) + return element + } + + /** + * @return copy of this element + */ + protected abstract fun copyThis(): AbstractKotlinElement + + /** + * @param element the values to be copied to + */ + protected open fun copyValues(element: AbstractKotlinElement) { + element.presetAnnotationMetadata = presetAnnotationMetadata + } + override fun withAnnotationMetadata(annotationMetadata: AnnotationMetadata): Element? { + val kotlinElement: AbstractKotlinElement = makeCopy() + kotlinElement.presetAnnotationMetadata = annotationMetadata + return kotlinElement + } + + override fun getAnnotationMetadata(): MutableAnnotationMetadataDelegate<*> { + if (elementAnnotationMetadata == null) { + + val factory = annotationMetadataFactory + if (presetAnnotationMetadata == null) { + elementAnnotationMetadata = factory.build(this) + } else { + elementAnnotationMetadata = factory.build(this, presetAnnotationMetadata) + } + } + return elementAnnotationMetadata!! + } + + override fun isPublic(): Boolean { + return if (declaration is KSDeclaration) { + declaration.getVisibility() == Visibility.PUBLIC + } else { + false + } + } + + override fun isPrivate(): Boolean { + return if (declaration is KSDeclaration) { + declaration.getVisibility() == Visibility.PRIVATE + } else { + false + } + } + + override fun isFinal(): Boolean { + return if (declaration is KSDeclaration) { + !declaration.isOpen() || declaration.modifiers.contains(Modifier.FINAL) + } else { + false + } + } + + override fun isAbstract(): Boolean { + return if (declaration is KSModifierListOwner) { + declaration.modifiers.contains(Modifier.ABSTRACT) + } else { + false + } + } + + @OptIn(KspExperimental::class) + override fun getModifiers(): MutableSet { + val dec = declaration.unwrap() + if (dec is KSDeclaration) { + val javaModifiers = visitorContext.resolver.effectiveJavaModifiers(dec) + return javaModifiers.mapNotNull { + when (it) { + Modifier.ABSTRACT -> ElementModifier.ABSTRACT + Modifier.FINAL -> ElementModifier.FINAL + Modifier.PRIVATE -> ElementModifier.PRIVATE + Modifier.PROTECTED -> ElementModifier.PROTECTED + Modifier.PUBLIC, Modifier.INTERNAL -> ElementModifier.PUBLIC + Modifier.JAVA_STATIC -> ElementModifier.STATIC + Modifier.JAVA_TRANSIENT -> ElementModifier.TRANSIENT + Modifier.JAVA_DEFAULT -> ElementModifier.DEFAULT + Modifier.JAVA_SYNCHRONIZED -> ElementModifier.SYNCHRONIZED + Modifier.JAVA_VOLATILE -> ElementModifier.VOLATILE + Modifier.JAVA_NATIVE -> ElementModifier.NATIVE + Modifier.JAVA_STRICT -> ElementModifier.STRICTFP + else -> null + } + }.toMutableSet() + } + return super.getModifiers() + } + + override fun annotate( + annotationType: String?, + consumer: Consumer>? + ): Element { + return super.annotate(annotationType, consumer) + } + + override fun annotate(annotationType: String?): Element { + return super.annotate(annotationType) + } + + override fun annotate( + annotationType: Class?, + consumer: Consumer>? + ): Element { + return super.annotate(annotationType, consumer) + } + + override fun annotate(annotationType: Class?): Element? { + return super.annotate(annotationType) + } + override fun annotate(annotationValue: AnnotationValue?): Element { + return super.annotate(annotationValue) + } + + override fun removeAnnotation(annotationType: String?): Element { + return super.removeAnnotation(annotationType) + } + + override fun removeAnnotation(annotationType: Class?): Element { + return super.removeAnnotation(annotationType) + } + + override fun removeAnnotationIf(predicate: Predicate>?): Element { + return super.removeAnnotationIf(predicate) + } + + override fun removeStereotype(annotationType: String?): Element { + return super.removeStereotype(annotationType) + } + + override fun removeStereotype(annotationType: Class?): Element { + return super.removeStereotype(annotationType) + } + + override fun isPackagePrivate(): Boolean { + return if (declaration is KSDeclaration) { + declaration.isJavaPackagePrivate() + } else { + false + } + } + + override fun getDocumentation(): Optional { + return if (declaration is KSDeclaration) { + Optional.ofNullable(declaration.docString) + } else { + Optional.empty() + } + } + + override fun getReturnInstance(): Element { + return this + } + + protected fun resolveGeneric( + parent: KSNode?, + type: ClassElement, + owningClass: ClassElement, + visitorContext: KotlinVisitorContext + ): ClassElement { + var resolvedType = type + if (parent is KSDeclaration && owningClass is KotlinClassElement) { + if (type is GenericPlaceholderElement) { + + val variableName = type.variableName + val genericTypeInfo = owningClass.getGenericTypeInfo() + val boundInfo = genericTypeInfo[parent.getBinaryName(visitorContext.resolver, visitorContext)] + if (boundInfo != null) { + val ksType = boundInfo[variableName] + if (ksType != null) { + resolvedType = visitorContext.elementFactory.newClassElement( + ksType, + visitorContext.elementAnnotationMetadataFactory, + true + ) + if (type.isArray) { + resolvedType = resolvedType.toArray() + } + } + } + } else if (type.declaredGenericPlaceholders.isNotEmpty() && type is KotlinClassElement) { + val genericTypeInfo = owningClass.getGenericTypeInfo() + val kotlinType = type.kotlinType + val boundInfo = if (parent.qualifiedName != null) genericTypeInfo[parent.getBinaryName(visitorContext.resolver, visitorContext)] else null + resolvedType = if (boundInfo != null) { + val boundArgs = kotlinType.arguments.map { arg -> + resolveTypeArgument(arg, boundInfo, visitorContext) + }.toMutableList() + type.withBoundGenericTypes(boundArgs) + } else { + type + } + } + } + return resolvedType + } + + private fun resolveTypeArgument( + arg: KSTypeArgument, + boundInfo: Map, + visitorContext: KotlinVisitorContext + ): ClassElement { + val n = arg.type?.toString() + val resolved = boundInfo[n] + return if (resolved != null) { + visitorContext.elementFactory.newClassElement( + resolved, + annotationMetadataFactory, + false + ) + } else { + if (arg.type != null) { + val t = arg.type!!.resolve() + if (t.arguments.isNotEmpty()) { + visitorContext.elementFactory.newClassElement( + t, + annotationMetadataFactory, + false + ).withBoundGenericTypes( + t.arguments.map { + resolveTypeArgument(it, boundInfo, visitorContext) + } + ) + } else { + visitorContext.elementFactory.newClassElement( + t, + annotationMetadataFactory, + false + ) + } + } else { + visitorContext.getClassElement(Object::class.java.name).get() + } + } + } + + override fun toString(): String { + return getDescription(false) + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as AbstractKotlinElement<*> + + if (nativeType != other.nativeType) return false + + return true + } + + override fun hashCode(): Int { + return nativeType.hashCode() + } + + +} diff --git a/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/KSAnnotatedReference.kt b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/KSAnnotatedReference.kt new file mode 100644 index 00000000000..92005234a71 --- /dev/null +++ b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/KSAnnotatedReference.kt @@ -0,0 +1,103 @@ +/* + * Copyright 2017-2023 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.kotlin.processing.visitor + +import com.google.devtools.ksp.symbol.* +import io.micronaut.core.reflect.ReflectionUtils.findMethod + +open class KSAnnotatedReference(open val nativeType: Any, val node: KSNode) { + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is KSAnnotatedReference) return false + + if (nativeType != other.nativeType) return false + + return true + } + + override fun hashCode(): Int { + return nativeType.hashCode() + } + + companion object Helper { + fun resolveNativeType(nativeType: Any, kind: String): Any { + val javaClass = nativeType.javaClass + val method = findMethod(javaClass, "getKt$kind") + .orElseGet { + findMethod(javaClass, "getPsi").orElseGet { + findMethod(javaClass, "getDescriptor").orElse(null) + } + } + + return if (method != null && method.canAccess(nativeType)) { + method.invoke(nativeType) + } else { + nativeType + } + } + } +} + +class KSClassReference( + private val nt: KSClassDeclaration +) : KSAnnotatedReference(resolveNativeType(nt, "ClassOrObject"), nt), KSClassDeclaration by nt { + override fun toString(): String { + return "Class(${nt.qualifiedName?.asString()})" + } +} + +class KSValueParameterReference( + private val nt: KSValueParameter +) : KSAnnotatedReference(resolveNativeType(nt, "Parameter"), nt), KSValueParameter by nt { + override fun toString(): String { + return "Parameter(${nt.name?.asString()})" + } +} + +class KSPropertyReference( + private val nt: KSPropertyDeclaration +) : KSAnnotatedReference(resolveNativeType(nt, "Property"), nt), KSPropertyDeclaration by nt { + override fun toString(): String { + return "Property(${nt.qualifiedName?.asString()})" + } +} + +class KSPropertySetterReference( + private val nt: KSPropertySetter +) : KSAnnotatedReference(resolveNativeType(nt, "PropertySetter"), nt), KSPropertySetter by nt { + override fun toString(): String { + return "PropertySetter(${nt.receiver.qualifiedName?.asString()})" + } +} + +class KSPropertyGetterReference( + private val nt: KSPropertyGetter +) : KSAnnotatedReference(resolveNativeType(nt, "PropertyGetter"), nt), KSPropertyGetter by nt { + override fun toString(): String { + return "PropertyGetter(${nt.receiver.qualifiedName?.asString()})" + } +} + + +class KSFunctionReference( + private val nt: KSFunctionDeclaration +) : KSAnnotatedReference(resolveNativeType(nt, "Function"), nt), KSFunctionDeclaration by nt { + override fun toString(): String { + return "Function(${nt.qualifiedName?.asString()})" + } +} + diff --git a/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/KotlinClassElement.kt b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/KotlinClassElement.kt new file mode 100644 index 00000000000..21c51ed8e3b --- /dev/null +++ b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/KotlinClassElement.kt @@ -0,0 +1,876 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.kotlin.processing.visitor + +import com.google.devtools.ksp.* +import com.google.devtools.ksp.symbol.* +import io.micronaut.context.annotation.BeanProperties +import io.micronaut.context.annotation.ConfigurationBuilder +import io.micronaut.context.annotation.ConfigurationReader +import io.micronaut.core.annotation.AnnotationMetadata +import io.micronaut.core.annotation.Creator +import io.micronaut.core.annotation.NonNull +import io.micronaut.inject.ast.* +import io.micronaut.inject.ast.annotation.ElementAnnotationMetadataFactory +import io.micronaut.inject.ast.utils.AstBeanPropertiesUtils +import io.micronaut.inject.ast.utils.EnclosedElementsQuery +import io.micronaut.inject.processing.ProcessingException +import io.micronaut.kotlin.processing.getBinaryName +import io.micronaut.kotlin.processing.getClassDeclaration +import java.util.* +import java.util.function.Function +import java.util.stream.Stream +import kotlin.collections.LinkedHashMap + +open class KotlinClassElement(val kotlinType: KSType, + protected val classDeclaration: KSClassDeclaration, + private val annotationInfo: KSAnnotated, + protected val elementAnnotationMetadataFactory: ElementAnnotationMetadataFactory, + visitorContext: KotlinVisitorContext, + private val arrayDimensions: Int = 0, + private val typeVariable: Boolean = false): AbstractKotlinElement(annotationInfo, elementAnnotationMetadataFactory, visitorContext), ArrayableClassElement { + + constructor( + ref: KSAnnotated, + elementAnnotationMetadataFactory: ElementAnnotationMetadataFactory, + visitorContext: KotlinVisitorContext, + arrayDimensions: Int = 0, + typeVariable: Boolean = false + ) : this(getType(ref, visitorContext), ref.getClassDeclaration(visitorContext), ref, elementAnnotationMetadataFactory, visitorContext, arrayDimensions, typeVariable) + + constructor( + type: KSType, + elementAnnotationMetadataFactory: ElementAnnotationMetadataFactory, + visitorContext: KotlinVisitorContext, + arrayDimensions: Int = 0, + typeVariable: Boolean = false + ) : this(type, type.declaration.getClassDeclaration(visitorContext), type.declaration.getClassDeclaration(visitorContext), elementAnnotationMetadataFactory, visitorContext, arrayDimensions, typeVariable) + + + val outerType: KSType? by lazy { + val outerDecl = classDeclaration.parentDeclaration as? KSClassDeclaration + outerDecl?.asType(kotlinType.arguments.subList(classDeclaration.typeParameters.size, kotlinType.arguments.size)) + } + + private val resolvedProperties : List by lazy { + getBeanProperties(PropertyElementQuery.of(this)) + } + private val enclosedElementsQuery = KotlinEnclosedElementsQuery() + private val nativeProperties : List by lazy { + classDeclaration.getAllProperties() + .filter { !it.isPrivate() } + .map { KotlinPropertyElement( + this, + visitorContext.elementFactory.newClassElement(it.type.resolve(), elementAnnotationMetadataFactory), + it, + elementAnnotationMetadataFactory, visitorContext + ) } + .filter { !it.hasAnnotation(JvmField::class.java) } + .toList() + } + private val internalGenerics : Map> by lazy { + val boundMirrors : Map = getBoundTypeMirrors() + val data = mutableMapOf>() + if (boundMirrors.isNotEmpty()) { + data[this.name] = boundMirrors + } + val classDeclaration = classDeclaration + populateGenericInfo(classDeclaration, data, boundMirrors) + data + } + private val internalCanonicalName : String by lazy { + classDeclaration.qualifiedName!!.asString() + } + private val internalName : String by lazy { + classDeclaration.getBinaryName(visitorContext.resolver, visitorContext) + } + + private var overrideBoundGenericTypes: MutableList? = null + private var resolvedTypeArguments : MutableMap? = null + + private val nt : KSAnnotated = if (annotationInfo is KSTypeArgument) annotationInfo else KSClassReference(classDeclaration) + override fun getNativeType(): KSAnnotated { + return nt + } + + companion object Helper { + fun getType(ref: KSAnnotated, visitorContext: KotlinVisitorContext) : KSType { + if (ref is KSType) { + return ref + } else if (ref is KSTypeReference) { + return ref.resolve() + } else if (ref is KSTypeParameter) { + return ref.bounds.firstOrNull()?.resolve() ?: visitorContext.resolver.builtIns.anyType + } else if (ref is KSClassDeclaration) { + return ref.asStarProjectedType() + } else if (ref is KSTypeArgument) { + val ksType = ref.type?.resolve() + if (ksType != null) { + return ksType + } else { + throw IllegalArgumentException("Unresolvable type argument $ref") + } + } else if (ref is KSTypeAlias) { + return ref.type.resolve() + } else { + throw IllegalArgumentException("Not a type $ref") + } + } + + + + } + + override fun getName(): String { + return internalName + } + + override fun getCanonicalName(): String { + return internalCanonicalName + } + + override fun getPackageName(): String { + return classDeclaration.packageName.asString() + } + + override fun isDeclaredNullable(): Boolean { + return kotlinType.isMarkedNullable + } + + override fun isNullable(): Boolean { + return kotlinType.isMarkedNullable + } + + override fun getSyntheticBeanProperties(): List { + return nativeProperties + } + + override fun getAccessibleStaticCreators(): MutableList { + val staticCreators: MutableList = mutableListOf() + staticCreators.addAll(super.getAccessibleStaticCreators()) + return staticCreators.ifEmpty { + val companion = classDeclaration.declarations.filter { + it is KSClassDeclaration && it.isCompanionObject + }.map { it as KSClassDeclaration } + .map { visitorContext.elementFactory.newClassElement(it, elementAnnotationMetadataFactory, false) } + .firstOrNull() + + if (companion != null) { + return companion.getEnclosedElements( + ElementQuery.ALL_METHODS + .annotated { it.hasStereotype( + Creator::class.java + )} + .modifiers { it.isEmpty() || it.contains(ElementModifier.PUBLIC) } + .filter { method -> + method.returnType.isAssignable(this) + } + ) + + } else { + return mutableListOf() + } + } + } + + override fun getBeanProperties(): List { + return resolvedProperties + } + + override fun getDeclaredGenericPlaceholders(): MutableList { + return kotlinType.declaration.typeParameters.map { + KotlinGenericPlaceholderElement(it, annotationMetadataFactory, visitorContext) + }.toMutableList() + } + + override fun withBoundGenericTypes(typeArguments: MutableList?): ClassElement { + if (typeArguments != null && typeArguments.size == kotlinType.declaration.typeParameters.size) { + val copy = copyThis() + copy.overrideBoundGenericTypes = typeArguments + + val i = typeArguments.iterator() + copy.resolvedTypeArguments = kotlinType.declaration.typeParameters.associate { + it.name.asString() to i.next() + }.toMutableMap() + return copy + } + return this + } + + override fun getBoundGenericTypes(): MutableList { + if (overrideBoundGenericTypes == null) { + val arguments = kotlinType.arguments + if (arguments.isEmpty()) { + return mutableListOf() + } else { + val elementFactory = visitorContext.elementFactory + this.overrideBoundGenericTypes = arguments.map { arg -> + when(arg.variance) { + Variance.STAR, Variance.COVARIANT, Variance.CONTRAVARIANT -> KotlinWildcardElement( // example List<*> + resolveUpperBounds(arg, elementFactory, visitorContext), + resolveLowerBounds(arg, elementFactory), + elementAnnotationMetadataFactory, visitorContext + ) + else -> elementFactory.newClassElement( // other cases + arg, + elementAnnotationMetadataFactory, + false + ) + } + }.toMutableList() + } + } + return overrideBoundGenericTypes!! + } + + fun getGenericTypeInfo() : Map> { + return this.internalGenerics + } + + private fun populateGenericInfo( + classDeclaration: KSClassDeclaration, + data: MutableMap>, + boundMirrors: Map? + ) { + classDeclaration.superTypes.forEach { + val superType = it.resolve() + if (superType != visitorContext.resolver.builtIns.anyType) { + val declaration = superType.declaration + val name = declaration.qualifiedName?.asString() + val binaryName = declaration.getBinaryName(visitorContext.resolver, visitorContext) + if (name != null && !data.containsKey(name)) { + val typeParameters = declaration.typeParameters + if (typeParameters.isEmpty()) { + data[binaryName] = emptyMap() + } else { + val ksTypeArguments = superType.arguments + if (typeParameters.size == ksTypeArguments.size) { + val resolved = LinkedHashMap() + var i = 0 + typeParameters.forEach { typeParameter -> + val parameterName = typeParameter.name.asString() + val typeArgument = ksTypeArguments[i] + val argumentType = typeArgument.type?.resolve() + val argumentName = argumentType?.declaration?.simpleName?.asString() + val bound = if (argumentName != null ) boundMirrors?.get(argumentName) else null + if (bound != null) { + resolved[parameterName] = bound + } else { + resolved[parameterName] = argumentType ?: typeParameter.bounds.firstOrNull()?.resolve() + ?: visitorContext.resolver.builtIns.anyType + } + i++ + } + data[binaryName] = resolved + } + } + if (declaration is KSClassDeclaration) { + val newBounds = data[binaryName] + populateGenericInfo( + declaration, + data, + newBounds + ) + } + } + } + + } + } + + private fun getBoundTypeMirrors(): Map { + val typeParameters: List = kotlinType.arguments + val parameterIterator = classDeclaration.typeParameters.iterator() + val tpi = typeParameters.iterator() + val map: MutableMap = LinkedHashMap() + while (tpi.hasNext() && parameterIterator.hasNext()) { + val tpe = tpi.next() + val parameter = parameterIterator.next() + val resolvedType = tpe.type?.resolve() + if (resolvedType != null) { + map[parameter.name.asString()] = resolvedType + } else { + map[parameter.name.asString()] = visitorContext.resolver.builtIns.anyType + } + } + return Collections.unmodifiableMap(map) + } + + private fun resolveLowerBounds(arg: KSTypeArgument, elementFactory: KotlinElementFactory): List { + return if (arg.variance == Variance.CONTRAVARIANT) { + listOf( + elementFactory.newClassElement(arg.type?.resolve()!!, elementAnnotationMetadataFactory, false) as KotlinClassElement + ) + } else { + return emptyList() + } + } + + private fun resolveUpperBounds( + arg: KSTypeArgument, + elementFactory: KotlinElementFactory, + visitorContext: KotlinVisitorContext + ): List { + return if (arg.variance == Variance.COVARIANT) { + listOf( + elementFactory.newClassElement(arg.type?.resolve()!!, elementAnnotationMetadataFactory, false) as KotlinClassElement + ) + } else { + val objectType = visitorContext.resolver.getClassDeclarationByName(Object::class.java.name)!! + listOf( + elementFactory.newClassElement(objectType.asStarProjectedType(), elementAnnotationMetadataFactory, false) as KotlinClassElement + ) + } + } + + override fun getBeanProperties(propertyElementQuery: PropertyElementQuery): MutableList { + val customReaderPropertyNameResolver = + Function> { Optional.empty() } + val customWriterPropertyNameResolver = + Function> { Optional.empty() } + val accessKinds = propertyElementQuery.accessKinds + val fieldAccess = accessKinds.contains(BeanProperties.AccessKind.FIELD) && !propertyElementQuery.accessKinds.contains(BeanProperties.AccessKind.METHOD) + if (fieldAccess) { + // all kotlin fields are private + return mutableListOf() + } + + val eq = ElementQuery.of(PropertyElement::class.java) + .named { n -> !propertyElementQuery.excludes.contains(n) } + .named { n -> propertyElementQuery.includes.isEmpty() || propertyElementQuery.includes.contains(n) } + .modifiers { + val visibility = propertyElementQuery.visibility + if (visibility == BeanProperties.Visibility.PUBLIC) { + it.contains(ElementModifier.PUBLIC) + } else { + !it.contains(ElementModifier.PRIVATE) + } + }.annotated { prop -> + if(prop.hasAnnotation(JvmField::class.java)) { + false + } else { + val excludedAnnotations = propertyElementQuery.excludedAnnotations + excludedAnnotations.isEmpty() || !excludedAnnotations.any { prop.hasAnnotation(it) } + } + } + + val allProperties : MutableList = mutableListOf() + // unfortunate hack since these are not excluded? + if (hasDeclaredStereotype(ConfigurationReader::class.java)) { + val configurationBuilderQuery = ElementQuery.of(PropertyElement::class.java) + .annotated { it.hasDeclaredAnnotation(ConfigurationBuilder::class.java) } + .onlyInstance() + val configBuilderProps = enclosedElementsQuery.getEnclosedElements(this, configurationBuilderQuery) + allProperties.addAll(configBuilderProps) + } + + allProperties.addAll(enclosedElementsQuery.getEnclosedElements(this, eq)) + val propertyNames = allProperties.map { it.name }.toSet() + + val resolvedProperties : MutableList = mutableListOf() + val methodProperties = AstBeanPropertiesUtils.resolveBeanProperties(propertyElementQuery, + this, + { + getEnclosedElements( + ElementQuery.ALL_METHODS + ) + }, + { + emptyList() + }, + false, propertyNames, + customReaderPropertyNameResolver, + customWriterPropertyNameResolver, + { value: AstBeanPropertiesUtils.BeanPropertyData -> + if (!value.isExcluded) { + this.mapToPropertyElement( + value + ) + } else { + null + } + }) + resolvedProperties.addAll(methodProperties) + resolvedProperties.addAll(allProperties) + return resolvedProperties + } + + private fun mapToPropertyElement(value: AstBeanPropertiesUtils.BeanPropertyData): KotlinPropertyElement { + return KotlinPropertyElement( + this@KotlinClassElement, + value.type, + value.propertyName, + value.field, + value.getter, + value.setter, + elementAnnotationMetadataFactory, + visitorContext, + value.isExcluded + ) + } + + @OptIn(KspExperimental::class) + override fun getSimpleName(): String { + var parentDeclaration = classDeclaration.parentDeclaration + return if (parentDeclaration == null) { + val qualifiedName = classDeclaration.qualifiedName + if (qualifiedName != null) { + visitorContext.resolver.mapKotlinNameToJava(qualifiedName)?.getShortName() + ?: classDeclaration.simpleName.asString() + } else + classDeclaration.simpleName.asString() + } else { + val builder = StringBuilder(classDeclaration.simpleName.asString()) + while (parentDeclaration != null) { + builder.insert(0, '$') + .insert(0, parentDeclaration.simpleName.asString()) + parentDeclaration = parentDeclaration.parentDeclaration + } + builder.toString() + } + } + + override fun getSuperType(): Optional { + val superType = classDeclaration.superTypes.firstOrNull { + val resolved = it.resolve() + if (resolved == visitorContext.resolver.builtIns.anyType) { + false + } else { + val declaration = resolved.declaration + declaration is KSClassDeclaration && declaration.classKind != ClassKind.INTERFACE + } + } + return Optional.ofNullable(superType) + .map { + visitorContext.elementFactory.newClassElement(it.resolve()) + } + } + + override fun getInterfaces(): Collection { + return classDeclaration.superTypes.map { it.resolve() } + .filter { + it != visitorContext.resolver.builtIns.anyType + } + .filter { + val declaration = it.declaration + declaration is KSClassDeclaration && declaration.classKind == ClassKind.INTERFACE + }.map { + visitorContext.elementFactory.newClassElement(it) + }.toList() + } + + override fun isStatic(): Boolean { + return if (isInner) { + // inner classes in Kotlin are by default static unless + // the 'inner' keyword is used + !classDeclaration.modifiers.contains(Modifier.INNER) + } else { + super.isStatic() + } + } + + override fun isInterface(): Boolean { + return classDeclaration.classKind == ClassKind.INTERFACE + } + + override fun isTypeVariable(): Boolean = typeVariable + + @OptIn(KspExperimental::class) + override fun isAssignable(type: String): Boolean { + var ksType = visitorContext.resolver.getClassDeclarationByName(type)?.asStarProjectedType() + if (ksType != null) { + if (ksType.isAssignableFrom(kotlinType)) { + return true + } + val kotlinName = visitorContext.resolver.mapJavaNameToKotlin( + visitorContext.resolver.getKSNameFromString(type)) + if (kotlinName != null) { + ksType = visitorContext.resolver.getKotlinClassByName(kotlinName)?.asStarProjectedType() + if (ksType != null && kotlinType.starProjection().isAssignableFrom(ksType)) { + return true + } + } + return false + } + return false + } + + override fun isAssignable(type: ClassElement): Boolean { + if (type is KotlinClassElement) { + return type.kotlinType.isAssignableFrom(kotlinType) + } + return super.isAssignable(type) + } + + override fun copyThis(): KotlinClassElement { + val copy = KotlinClassElement( + kotlinType, classDeclaration, annotationInfo, elementAnnotationMetadataFactory, visitorContext, arrayDimensions, typeVariable + ) + copy.resolvedTypeArguments = resolvedTypeArguments + return copy + } + + override fun withTypeArguments(typeArguments: MutableMap?): ClassElement { + val copy = copyThis() + copy.resolvedTypeArguments = typeArguments + return copy + } + + override fun isAbstract(): Boolean { + return classDeclaration.isAbstract() + } + + override fun withAnnotationMetadata(annotationMetadata: AnnotationMetadata): ClassElement { + return super.withAnnotationMetadata(annotationMetadata) as ClassElement + } + + override fun isArray(): Boolean { + return arrayDimensions > 0 + } + + override fun getArrayDimensions(): Int { + return arrayDimensions + } + + override fun withArrayDimensions(arrayDimensions: Int): ClassElement { + return KotlinClassElement(kotlinType, classDeclaration, annotationInfo, elementAnnotationMetadataFactory, visitorContext, arrayDimensions, typeVariable) + } + + override fun isInner(): Boolean { + return outerType != null + } + + override fun getPrimaryConstructor(): Optional { + val primaryConstructor = super.getPrimaryConstructor() + return if (primaryConstructor.isPresent) { + primaryConstructor + } else { + Optional.ofNullable(classDeclaration.primaryConstructor) + .filter { !it.isPrivate() } + .map { visitorContext.elementFactory.newConstructorElement( + this, + it, + elementAnnotationMetadataFactory + ) } + } + } + + override fun getDefaultConstructor(): Optional { + val defaultConstructor = super.getDefaultConstructor() + return if (defaultConstructor.isPresent) { + defaultConstructor + } else { + Optional.ofNullable(classDeclaration.primaryConstructor) + .filter { !it.isPrivate() && it.parameters.isEmpty() } + .map { visitorContext.elementFactory.newConstructorElement( + this, + it, + elementAnnotationMetadataFactory + ) } + } + } + + override fun getTypeArguments(): Map { + if (resolvedTypeArguments == null) { + val typeArguments = mutableMapOf() + val elementFactory = visitorContext.elementFactory + val typeParameters = kotlinType.declaration.typeParameters + if (kotlinType.arguments.isEmpty()) { + typeParameters.forEach { + typeArguments[it.name.asString()] = KotlinGenericPlaceholderElement(it, annotationMetadataFactory, visitorContext) + } + } else { + kotlinType.arguments.forEachIndexed { i, argument -> + val typeElement = elementFactory.newClassElement( + argument, + annotationMetadataFactory, + false + ) + typeArguments[typeParameters[i].name.asString()] = typeElement + } + } + resolvedTypeArguments = typeArguments + } + return resolvedTypeArguments!! + } + + override fun getTypeArguments(type: String): Map { + return allTypeArguments.getOrElse(type) { emptyMap() } + } + + override fun getAllTypeArguments(): Map> { + val genericInfo = getGenericTypeInfo() + return genericInfo.mapValues { entry -> + entry.value.mapValues { data -> + visitorContext.elementFactory.newClassElement(data.value, elementAnnotationMetadataFactory, false) + } + } + } + + override fun getEnclosingType(): Optional { + if (isInner) { + return Optional.of( + visitorContext.elementFactory.newClassElement( + outerType!!, + visitorContext.elementAnnotationMetadataFactory + ) + ) + } + return Optional.empty() + } + + override fun getEnclosedElements(@NonNull query: ElementQuery): MutableList { + val classElementToInspect: ClassElement = if (this is GenericPlaceholderElement) { + val bounds: List = this.bounds + if (bounds.isEmpty()) { + return mutableListOf() + } + bounds[0] + } else { + this + } + return enclosedElementsQuery.getEnclosedElements(classElementToInspect, query) + + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as KotlinClassElement + + if (arrayDimensions != other.arrayDimensions) return false + if (typeVariable != other.typeVariable) return false + if (internalCanonicalName != other.internalCanonicalName) return false + if (overrideBoundGenericTypes != other.overrideBoundGenericTypes) return false + + return true + } + + override fun hashCode(): Int { + var result = arrayDimensions + result = 31 * result + typeVariable.hashCode() + result = 31 * result + internalCanonicalName.hashCode() + result = 31 * result + (overrideBoundGenericTypes?.hashCode() ?: 0) + return result + } + + private inner class KotlinEnclosedElementsQuery : + EnclosedElementsQuery() { + override fun getExcludedNativeElements(result: ElementQuery.Result<*>): Set { + if (result.isExcludePropertyElements) { + val excludeElements: MutableSet = HashSet() + for (excludePropertyElement in beanProperties) { + excludePropertyElement.readMethod.ifPresent { methodElement: MethodElement -> + excludeElements.add( + methodElement.nativeType as KSNode + ) + } + excludePropertyElement.writeMethod.ifPresent { methodElement: MethodElement -> + excludeElements.add( + methodElement.nativeType as KSNode + ) + } + excludePropertyElement.field.ifPresent { fieldElement: FieldElement -> + excludeElements.add( + fieldElement.nativeType as KSNode + ) + } + } + return excludeElements + } + return emptySet() + } + + override fun getCacheKey(element: KSNode): KSNode { + return when(element) { + is KSFunctionDeclaration -> KSFunctionReference(element) + is KSPropertyDeclaration -> KSPropertyReference(element) + is KSClassDeclaration -> KSClassReference(element) + is KSValueParameter -> KSValueParameterReference(element) + is KSPropertyGetter -> KSPropertyGetterReference(element) + is KSPropertySetter -> KSPropertySetterReference(element) + else -> element + } + } + + override fun getSuperClass(classNode: KSClassDeclaration): KSClassDeclaration? { + val superTypes = classNode.superTypes + for (superclass in superTypes) { + val resolved = superclass.resolve() + val declaration = resolved.declaration + if (declaration is KSClassDeclaration) { + if (declaration.classKind == ClassKind.CLASS && declaration.qualifiedName?.asString() != Any::class.qualifiedName) { + return declaration + } + } + } + + return null + } + + override fun getInterfaces(classDeclaration: KSClassDeclaration): Collection { + val superTypes = classDeclaration.superTypes + val result: MutableCollection = ArrayList() + for (superclass in superTypes) { + val resolved = superclass.resolve() + val declaration = resolved.declaration + if (declaration is KSClassDeclaration) { + if (declaration.classKind == ClassKind.INTERFACE) { + result.add(declaration) + } + } + } + return result + } + + override fun getEnclosedElements( + classNode: KSClassDeclaration, + result: ElementQuery.Result<*> + ): List { + val elementType: Class<*> = result.elementType + return getEnclosedElements(classNode, result, elementType) + } + + private fun getEnclosedElements( + classNode: KSClassDeclaration, + result: ElementQuery.Result<*>, + elementType: Class<*> + ): List { + return when (elementType) { + MemberElement::class.java -> { + Stream.concat( + getEnclosedElements(classNode, result, FieldElement::class.java).stream(), + getEnclosedElements(classNode, result, MethodElement::class.java).stream() + ).toList() + } + MethodElement::class.java -> { + classNode.getDeclaredFunctions() + .filter { func: KSFunctionDeclaration -> + !func.isConstructor() && + func.origin != Origin.SYNTHETIC && + // this is a hack but no other way it seems + !listOf("hashCode", "toString", "equals").contains(func.simpleName.asString()) + } + .toList() + } + FieldElement::class.java -> { + classNode.getDeclaredProperties() + .filter { + it.hasBackingField && + it.origin != Origin.SYNTHETIC + } + .toList() + } + PropertyElement::class.java -> { + classNode.getDeclaredProperties().toList() + } + ConstructorElement::class.java -> { + classNode.getConstructors().toList() + } + ClassElement::class.java -> { + classNode.declarations.filter { + it is KSClassDeclaration + }.toList() + } + else -> { + throw java.lang.IllegalStateException("Unknown result type: $elementType") + } + } + } + + override fun excludeClass(classNode: KSClassDeclaration): Boolean { + val t = classNode.asStarProjectedType() + val builtIns = visitorContext.resolver.builtIns + return t == builtIns.anyType || + t == builtIns.nothingType || + t == builtIns.unitType || + classNode.qualifiedName.toString() == Enum::class.java.name + } + + override fun toAstElement( + enclosedElement: KSNode, + elementType: Class<*> + ): Element { + var ee = enclosedElement + if (ee is KSAnnotatedReference) { + ee = ee.node + } + val elementFactory: KotlinElementFactory = visitorContext.elementFactory + return when (ee) { + is KSFunctionDeclaration -> { + if (ee.isConstructor()) { + return elementFactory.newConstructorElement( + this@KotlinClassElement, + ee, + elementAnnotationMetadataFactory + ) + } else { + return elementFactory.newMethodElement( + this@KotlinClassElement, + ee, + elementAnnotationMetadataFactory + ) + } + } + + is KSPropertyDeclaration -> { + if (elementType == PropertyElement::class.java) { + val prop = KotlinPropertyElement( + this@KotlinClassElement, + visitorContext.elementFactory.newClassElement( + ee.type.resolve(), + elementAnnotationMetadataFactory + ), + ee, + elementAnnotationMetadataFactory, visitorContext + ) + if (!prop.hasAnnotation(JvmField::class.java)) { + return prop + } else { + return elementFactory.newFieldElement( + this@KotlinClassElement, + ee, + elementAnnotationMetadataFactory + ) + } + } else { + return elementFactory.newFieldElement( + this@KotlinClassElement, + ee, + elementAnnotationMetadataFactory + ) + } + } + + is KSType -> elementFactory.newClassElement( + ee, + elementAnnotationMetadataFactory + ) + + is KSClassDeclaration -> elementFactory.newClassElement( + ee, + elementAnnotationMetadataFactory, + false + ) + + else -> throw ProcessingException(this@KotlinClassElement, "Unknown element: $ee") + } + } + } + + +} diff --git a/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/KotlinConstructorElement.kt b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/KotlinConstructorElement.kt new file mode 100644 index 00000000000..bb5735799ad --- /dev/null +++ b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/KotlinConstructorElement.kt @@ -0,0 +1,53 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.kotlin.processing.visitor + +import com.google.devtools.ksp.closestClassDeclaration +import com.google.devtools.ksp.symbol.KSFunctionDeclaration +import com.google.devtools.ksp.symbol.Modifier +import io.micronaut.context.annotation.ConfigurationInject +import io.micronaut.context.annotation.ConfigurationReader +import io.micronaut.core.annotation.AnnotationMetadata +import io.micronaut.inject.ast.* +import io.micronaut.inject.ast.annotation.ElementAnnotationMetadataFactory + +class KotlinConstructorElement(method: KSFunctionDeclaration, + declaringType: ClassElement, + elementAnnotationMetadataFactory: ElementAnnotationMetadataFactory, + visitorContext: KotlinVisitorContext, + returnType: ClassElement +): ConstructorElement, KotlinMethodElement(method, declaringType, returnType, elementAnnotationMetadataFactory, visitorContext) { + + init { + if (method.closestClassDeclaration()?.modifiers?.contains(Modifier.DATA) == true && + declaringType.hasDeclaredStereotype(ConfigurationReader::class.java)) { + annotate(ConfigurationInject::class.java) + } + } + + override fun overrides(overridden: MethodElement): Boolean { + return false + } + + override fun hides(memberElement: MemberElement?): Boolean { + return false + } + + override fun getName() = "" + + override fun getReturnType(): ClassElement = declaringType + +} diff --git a/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/KotlinElementFactory.kt b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/KotlinElementFactory.kt new file mode 100644 index 00000000000..8d261903dbf --- /dev/null +++ b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/KotlinElementFactory.kt @@ -0,0 +1,229 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.kotlin.processing.visitor + +import com.google.devtools.ksp.symbol.* +import io.micronaut.core.annotation.AnnotationUtil +import io.micronaut.inject.ast.* +import io.micronaut.inject.ast.annotation.ElementAnnotationMetadataFactory + +class KotlinElementFactory( + private val visitorContext: KotlinVisitorContext): ElementFactory { + + companion object { + val primitives = mapOf( + "kotlin.Boolean" to PrimitiveElement.BOOLEAN, + "kotlin.Char" to PrimitiveElement.CHAR, + "kotlin.Short" to PrimitiveElement.SHORT, + "kotlin.Int" to PrimitiveElement.INT, + "kotlin.Long" to PrimitiveElement.LONG, + "kotlin.Float" to PrimitiveElement.FLOAT, + "kotlin.Double" to PrimitiveElement.DOUBLE, + "kotlin.Byte" to PrimitiveElement.BYTE, + "kotlin.Unit" to PrimitiveElement.VOID + ) + val primitiveArrays = mapOf( + "kotlin.BooleanArray" to PrimitiveElement.BOOLEAN.toArray(), + "kotlin.CharArray" to PrimitiveElement.CHAR.toArray(), + "kotlin.ShortArray" to PrimitiveElement.SHORT.toArray(), + "kotlin.IntArray" to PrimitiveElement.INT.toArray(), + "kotlin.LongArray" to PrimitiveElement.LONG.toArray(), + "kotlin.FloatArray" to PrimitiveElement.FLOAT.toArray(), + "kotlin.DoubleArray" to PrimitiveElement.DOUBLE.toArray(), + "kotlin.ByteArray" to PrimitiveElement.BYTE.toArray(), + ) + } + + fun newClassElement( + type: KSType + ): ClassElement { + return newClassElement(type, visitorContext.elementAnnotationMetadataFactory) + } + + override fun newClassElement( + type: KSType, + annotationMetadataFactory: ElementAnnotationMetadataFactory + ): ClassElement { + return newClassElement( + type, + annotationMetadataFactory, + true + ) + } + + override fun newClassElement( + type: KSType, + annotationMetadataFactory: ElementAnnotationMetadataFactory, + resolvedGenerics: Map + ): ClassElement { + return newClassElement( + type, + annotationMetadataFactory, + true + ) + } + + fun newClassElement(annotated: KSAnnotated, + elementAnnotationMetadataFactory: ElementAnnotationMetadataFactory, + allowPrimitive: Boolean): ClassElement { + val type = KotlinClassElement.getType(annotated, visitorContext) + val declaration = type.declaration + val qualifiedName = declaration.qualifiedName!!.asString() + val hasNoAnnotations = !annotated.annotations.iterator().hasNext() + var element = primitiveArrays[qualifiedName] + if (hasNoAnnotations && element != null) { + return element + } + if (qualifiedName == "kotlin.Array") { + val component = type.arguments[0].type!!.resolve() + val componentElement = newClassElement(component, elementAnnotationMetadataFactory, false) + return componentElement.toArray() + } else if (declaration is KSTypeParameter) { + return KotlinGenericPlaceholderElement(declaration, elementAnnotationMetadataFactory, visitorContext) + } + if (allowPrimitive && !type.isMarkedNullable) { + element = primitives[qualifiedName] + if (hasNoAnnotations && element != null ) { + return element + } + } + return if (declaration is KSClassDeclaration && declaration.classKind == ClassKind.ENUM_CLASS) { + KotlinEnumElement(type, elementAnnotationMetadataFactory, visitorContext) + } else { + KotlinClassElement(annotated, elementAnnotationMetadataFactory, visitorContext) + } + } + + fun newClassElement(type: KSType, + elementAnnotationMetadataFactory: ElementAnnotationMetadataFactory, + allowPrimitive: Boolean): ClassElement { + val declaration = type.declaration + val qualifiedName = declaration.qualifiedName!!.asString() + val hasNoAnnotations = !type.annotations.iterator().hasNext() + var element = primitiveArrays[qualifiedName] + if (hasNoAnnotations && element != null) { + return element + } + if (qualifiedName == "kotlin.Array") { + val component = type.arguments[0].type!!.resolve() + val componentElement = newClassElement(component, elementAnnotationMetadataFactory, false) + return componentElement.toArray() + } else if (declaration is KSTypeParameter) { + return KotlinGenericPlaceholderElement(declaration, elementAnnotationMetadataFactory, visitorContext) + } + if (allowPrimitive && !type.isMarkedNullable) { + element = primitives[qualifiedName] + if (hasNoAnnotations && element != null ) { + return element + } + } + return if (declaration is KSClassDeclaration && declaration.classKind == ClassKind.ENUM_CLASS) { + KotlinEnumElement(type, elementAnnotationMetadataFactory, visitorContext) + } else { + KotlinClassElement(type, elementAnnotationMetadataFactory, visitorContext) + } + } + + override fun newSourceClassElement( + type: KSType, + elementAnnotationMetadataFactory: ElementAnnotationMetadataFactory + ): ClassElement { + return newClassElement(type, elementAnnotationMetadataFactory) + } + + override fun newSourceMethodElement( + owningClass: ClassElement, + method: KSFunctionDeclaration, + elementAnnotationMetadataFactory: ElementAnnotationMetadataFactory + ): MethodElement { + return newMethodElement( + owningClass, method, elementAnnotationMetadataFactory + ) + } + + override fun newMethodElement( + declaringClass: ClassElement, + method: KSFunctionDeclaration, + elementAnnotationMetadataFactory: ElementAnnotationMetadataFactory + ): KotlinMethodElement { + val returnType = method.returnType!!.resolve() + + val returnTypeElement = newClassElement(returnType, elementAnnotationMetadataFactory) + + val kotlinMethodElement = KotlinMethodElement( + method, + declaringClass, + returnTypeElement, + elementAnnotationMetadataFactory, + visitorContext + ) + if (returnType.isMarkedNullable && !kotlinMethodElement.returnType.isPrimitive) { + kotlinMethodElement.annotate(AnnotationUtil.NULLABLE) + } + return kotlinMethodElement + } + + fun newMethodElement( + declaringClass: ClassElement, + propertyElement: KotlinPropertyElement, + method: KSPropertyGetter, + type: ClassElement, + elementAnnotationMetadataFactory: ElementAnnotationMetadataFactory + ): MethodElement { + return KotlinMethodElement(propertyElement, method, declaringClass, type, elementAnnotationMetadataFactory, visitorContext) + } + + fun newMethodElement( + declaringClass: ClassElement, + propertyElement: KotlinPropertyElement, + method: KSPropertySetter, + type: ClassElement, + elementAnnotationMetadataFactory: ElementAnnotationMetadataFactory + ): MethodElement { + return KotlinMethodElement( + type, + propertyElement, + method, + declaringClass, + elementAnnotationMetadataFactory, + visitorContext + ) + } + + override fun newConstructorElement( + owningClass: ClassElement, + constructor: KSFunctionDeclaration, + elementAnnotationMetadataFactory: ElementAnnotationMetadataFactory + ): ConstructorElement { + return KotlinConstructorElement(constructor, owningClass, elementAnnotationMetadataFactory, visitorContext, owningClass) + } + + override fun newFieldElement( + owningClass: ClassElement, + field: KSPropertyDeclaration, + elementAnnotationMetadataFactory: ElementAnnotationMetadataFactory + ): FieldElement { + return KotlinFieldElement(field, owningClass, elementAnnotationMetadataFactory, visitorContext) + } + + override fun newEnumConstantElement( + owningClass: ClassElement?, + enumConstant: KSPropertyDeclaration?, + elementAnnotationMetadataFactory: ElementAnnotationMetadataFactory? + ): EnumConstantElement { + TODO("Not yet implemented") + } +} diff --git a/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/KotlinEnumConstructorElement.kt b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/KotlinEnumConstructorElement.kt new file mode 100644 index 00000000000..7eac7e0ba80 --- /dev/null +++ b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/KotlinEnumConstructorElement.kt @@ -0,0 +1,51 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.kotlin.processing.visitor + +import io.micronaut.inject.ast.ClassElement +import io.micronaut.inject.ast.MethodElement +import io.micronaut.inject.ast.ParameterElement + +class KotlinEnumConstructorElement(private val classElement: ClassElement): MethodElement { + + override fun getName(): String = "valueOf" + + override fun isProtected() = false + + override fun isPublic() = true + + override fun getNativeType(): Any { + throw UnsupportedOperationException("No native type backing a kotlin enum static constructor") + } + + override fun isStatic(): Boolean = true + + override fun getDeclaringType(): ClassElement = classElement + + override fun getReturnType(): ClassElement = classElement + + override fun getParameters(): Array { + return arrayOf(ParameterElement.of(String::class.java, "s")) + } + + override fun withNewParameters(vararg newParameters: ParameterElement?): MethodElement { + throw UnsupportedOperationException("Cannot replace parameters of a kotlin enum static constructor") + } + + override fun withParameters(vararg newParameters: ParameterElement?): MethodElement { + throw UnsupportedOperationException("Cannot replace parameters of a kotlin enum static constructor") + } +} diff --git a/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/KotlinEnumElement.kt b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/KotlinEnumElement.kt new file mode 100644 index 00000000000..5ed1963d172 --- /dev/null +++ b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/KotlinEnumElement.kt @@ -0,0 +1,50 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.kotlin.processing.visitor + +import com.google.devtools.ksp.symbol.KSClassDeclaration +import com.google.devtools.ksp.symbol.KSType +import io.micronaut.inject.ast.EnumElement +import io.micronaut.inject.ast.MethodElement +import io.micronaut.inject.ast.annotation.ElementAnnotationMetadataFactory +import java.util.* + +class KotlinEnumElement(private val type: KSType, elementAnnotationMetadataFactory: ElementAnnotationMetadataFactory, visitorContext: KotlinVisitorContext): + KotlinClassElement(type, elementAnnotationMetadataFactory, visitorContext), EnumElement { + + override fun values(): List { + return classDeclaration.declarations + .filterIsInstance() + .map { decl -> decl.simpleName.asString() } + .toList() + } + + override fun getDefaultConstructor(): Optional { + return Optional.empty() + } + + override fun copyThis(): KotlinEnumElement { + return KotlinEnumElement( + type, + annotationMetadataFactory, + visitorContext + ) + } + + override fun getPrimaryConstructor(): Optional { + return Optional.of(KotlinEnumConstructorElement(this)) + } +} diff --git a/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/KotlinFieldElement.kt b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/KotlinFieldElement.kt new file mode 100644 index 00000000000..286873516ae --- /dev/null +++ b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/KotlinFieldElement.kt @@ -0,0 +1,99 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.kotlin.processing.visitor + +import com.google.devtools.ksp.symbol.KSPropertyDeclaration +import io.micronaut.core.annotation.AnnotationMetadata +import io.micronaut.inject.ast.ClassElement +import io.micronaut.inject.ast.ElementModifier +import io.micronaut.inject.ast.FieldElement +import io.micronaut.inject.ast.annotation.ElementAnnotationMetadataFactory + +class KotlinFieldElement(declaration: KSPropertyDeclaration, + private val declaringType: ClassElement, + elementAnnotationMetadataFactory: ElementAnnotationMetadataFactory, + visitorContext: KotlinVisitorContext +) : AbstractKotlinElement(KSPropertyReference(declaration), elementAnnotationMetadataFactory, visitorContext), FieldElement { + + private val internalName = declaration.simpleName.asString() + private val internalType : ClassElement by lazy { + visitorContext.elementFactory.newClassElement(declaration.type.resolve()) + } + + private val internalGenericType : ClassElement by lazy { + resolveGeneric(declaration.parent, type, declaringType, visitorContext) + } + + override fun isFinal(): Boolean { + return declaration.setter == null + } + + override fun isReflectionRequired(): Boolean { + return true // all Kotlin fields are private + } + + override fun isReflectionRequired(callingType: ClassElement?): Boolean { + return true // all Kotlin fields are private + } + + override fun isPublic(): Boolean { + return if (hasDeclaredAnnotation(JvmField::class.java)) { + super.isPublic() + } else { + false // all Kotlin fields are private + } + } + + override fun getType(): ClassElement { + return internalType + } + + override fun getName(): String { + return internalName + } + + override fun getGenericType(): ClassElement { + return internalGenericType + } + + override fun isPrimitive(): Boolean { + return type.isPrimitive + } + + override fun isArray(): Boolean { + return type.isArray + } + + override fun getArrayDimensions(): Int { + return type.arrayDimensions + } + + override fun copyThis(): AbstractKotlinElement { + return KotlinFieldElement(declaration, declaringType, annotationMetadataFactory, visitorContext) + } + + override fun isPrivate(): Boolean = true + + override fun withAnnotationMetadata(annotationMetadata: AnnotationMetadata): FieldElement { + return super.withAnnotationMetadata(annotationMetadata) as FieldElement + } + + override fun getDeclaringType() = declaringType + + override fun getModifiers(): MutableSet { + return super.getModifiers() + } +} diff --git a/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/KotlinGenericPlaceholderElement.kt b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/KotlinGenericPlaceholderElement.kt new file mode 100644 index 00000000000..355dadee52d --- /dev/null +++ b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/KotlinGenericPlaceholderElement.kt @@ -0,0 +1,113 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.kotlin.processing.visitor + +import com.google.devtools.ksp.closestClassDeclaration +import com.google.devtools.ksp.symbol.KSTypeParameter +import io.micronaut.core.annotation.AnnotationMetadata +import io.micronaut.inject.ast.ArrayableClassElement +import io.micronaut.inject.ast.ClassElement +import io.micronaut.inject.ast.Element +import io.micronaut.inject.ast.GenericPlaceholderElement +import io.micronaut.inject.ast.annotation.ElementAnnotationMetadataFactory +import io.micronaut.kotlin.processing.getBinaryName +import java.util.* +import java.util.function.Function + +class KotlinGenericPlaceholderElement( + private val parameter: KSTypeParameter, + elementAnnotationMetadataFactory: ElementAnnotationMetadataFactory, + visitorContext: KotlinVisitorContext, + private val arrayDimensions: Int = 0 +) : KotlinClassElement(parameter, elementAnnotationMetadataFactory, visitorContext, arrayDimensions, true), ArrayableClassElement, GenericPlaceholderElement { + override fun copyThis(): KotlinGenericPlaceholderElement { + return KotlinGenericPlaceholderElement( + parameter, + annotationMetadataFactory, + visitorContext, + arrayDimensions + ) + } + + override fun getName(): String { + val bounds = parameter.bounds.firstOrNull() + if (bounds != null) { + return bounds.resolve().declaration.getBinaryName(visitorContext.resolver, visitorContext) + } + return "java.lang.Object" + } + + override fun withAnnotationMetadata(annotationMetadata: AnnotationMetadata): ClassElement { + return super.withAnnotationMetadata(annotationMetadata) as ClassElement + } + + override fun isArray(): Boolean = arrayDimensions > 0 + + override fun getArrayDimensions(): Int = arrayDimensions + + override fun withArrayDimensions(arrayDimensions: Int): ClassElement { + return KotlinGenericPlaceholderElement(parameter, annotationMetadataFactory, visitorContext, arrayDimensions) + } + + override fun getBounds(): MutableList { + val elementFactory = visitorContext.elementFactory + val resolved = parameter.bounds.map { + val argumentType = it.resolve() + elementFactory.newClassElement(argumentType, annotationMetadataFactory) + }.toMutableList() + return if (resolved.isEmpty()) { + mutableListOf(visitorContext.getClassElement(Object::class.java.name).get()) + } else { + resolved + } + } + + override fun getVariableName(): String { + return parameter.simpleName.asString() + } + + override fun getDeclaringElement(): Optional { + val classDeclaration = parameter.closestClassDeclaration() + return Optional.ofNullable(classDeclaration).map { + visitorContext.elementFactory.newClassElement( + classDeclaration!!.asStarProjectedType(), + visitorContext.elementAnnotationMetadataFactory) + } + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + if (!super.equals(other)) return false + + other as KotlinGenericPlaceholderElement + + if (parameter.simpleName.asString() != other.parameter.simpleName.asString()) return false + + return true + } + + override fun hashCode(): Int { + var result = super.hashCode() + result = 31 * result + parameter.simpleName.asString().hashCode() + return result + } + + override fun foldBoundGenericTypes(fold: Function?): ClassElement { + Objects.requireNonNull(fold, "Function argument cannot be null") + return fold!!.apply(this) + } +} diff --git a/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/KotlinMethodElement.kt b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/KotlinMethodElement.kt new file mode 100644 index 00000000000..8c91c6da0b0 --- /dev/null +++ b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/KotlinMethodElement.kt @@ -0,0 +1,388 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.kotlin.processing.visitor + +import com.google.devtools.ksp.* +import com.google.devtools.ksp.symbol.* +import io.micronaut.core.annotation.AnnotationMetadata +import io.micronaut.core.util.ArrayUtils +import io.micronaut.inject.annotation.AnnotationMetadataHierarchy +import io.micronaut.inject.ast.* +import io.micronaut.inject.ast.annotation.ElementAnnotationMetadataFactory +import io.micronaut.kotlin.processing.getVisibility +import io.micronaut.kotlin.processing.kspNode +import io.micronaut.kotlin.processing.unwrap +import java.util.* +import java.util.function.Supplier +import kotlin.jvm.Throws + +@OptIn(KspExperimental::class) +open class KotlinMethodElement: AbstractKotlinElement, MethodElement { + + private val name: String + private val owningType: ClassElement + private val internalDeclaringType: ClassElement by lazy { + var parent = declaration.parent + if (parent is KSPropertyDeclaration) { + parent = parent.parent + } + val owner = getOwningType() + if (parent is KSClassDeclaration) { + if (owner.name.equals(parent.qualifiedName)) { + owner + } else { + visitorContext.elementFactory.newClassElement( + parent.asStarProjectedType() + ) + } + } else { + owner + } + } + + private var parameterInit : Supplier> = Supplier { emptyList() } + private val parameters: List by lazy { + parameterInit.get() + } + private val returnType: ClassElement + private val abstract: Boolean + private val public: Boolean + private val private: Boolean + private val protected: Boolean + private val internal: Boolean + private val propertyElement : KotlinPropertyElement? + + constructor(propertyType : ClassElement, + propertyElement: KotlinPropertyElement, + method: KSPropertySetter, + owningType: ClassElement, + elementAnnotationMetadataFactory: ElementAnnotationMetadataFactory, + visitorContext: KotlinVisitorContext + ) : super(KSPropertySetterReference(method), elementAnnotationMetadataFactory, visitorContext) { + this.name = visitorContext.resolver.getJvmName(method)!! + this.propertyElement = propertyElement + this.owningType = owningType + this.returnType = PrimitiveElement.VOID + this.abstract = method.receiver.isAbstract() + val visibility = method.getVisibility() + this.public = visibility == Visibility.PUBLIC + this.private = visibility == Visibility.PRIVATE + this.protected = visibility == Visibility.PROTECTED + this.internal = visibility == Visibility.INTERNAL + this.parameterInit = Supplier { + val parameterElement = KotlinParameterElement( + propertyType, this, method.parameter, elementAnnotationMetadataFactory, visitorContext + ) + listOf(parameterElement) + } + } + + constructor( + propertyElement: KotlinPropertyElement, + method: KSPropertyGetter, + owningType: ClassElement, + returnType: ClassElement, + elementAnnotationMetadataFactory: ElementAnnotationMetadataFactory, + visitorContext: KotlinVisitorContext, + ) : super(KSPropertyGetterReference(method), elementAnnotationMetadataFactory, visitorContext) { + this.name = visitorContext.resolver.getJvmName(method)!! + this.propertyElement = propertyElement + this.owningType = owningType + this.parameterInit = Supplier { emptyList() } + this.returnType = returnType + this.abstract = method.receiver.isAbstract() + this.public = method.receiver.isPublic() + this.private = method.receiver.isPrivate() + this.protected = method.receiver.isProtected() + this.internal = method.receiver.isInternal() + } + + constructor(method: KSFunctionDeclaration, + owningType: ClassElement, + returnType: ClassElement, + elementAnnotationMetadataFactory: ElementAnnotationMetadataFactory, + visitorContext: KotlinVisitorContext + ) : super(KSFunctionReference(method), elementAnnotationMetadataFactory, visitorContext) { + this.name = visitorContext.resolver.getJvmName(method)!! + this.owningType = owningType + this.parameterInit = Supplier { + method.parameters.map { + val t = visitorContext.elementFactory.newClassElement( + it.type.resolve(), + elementAnnotationMetadataFactory) + KotlinParameterElement( + t, + this, + it, + elementAnnotationMetadataFactory, + visitorContext + ) + } + } + this.propertyElement = null + this.returnType = returnType + this.abstract = method.isAbstract + this.public = method.isPublic() + this.private = method.isPrivate() + this.protected = method.isProtected() + this.internal = method.isInternal() + } + + protected constructor(method: KSAnnotated, + name: String, + owningType: ClassElement, + elementAnnotationMetadataFactory: ElementAnnotationMetadataFactory, + visitorContext: KotlinVisitorContext, + returnType: ClassElement, + parameters: List, + abstract: Boolean, + public: Boolean, + private: Boolean, + protected: Boolean, + internal: Boolean + ) : super(method, elementAnnotationMetadataFactory, visitorContext) { + this.name = name + this.owningType = owningType + this.parameterInit = Supplier { + parameters + } + this.propertyElement = null + this.returnType = returnType + this.abstract = abstract + this.public = public + this.private = private + this.protected = protected + this.internal = internal + } + + override fun getOwningType(): ClassElement { + return owningType + } + + override fun isSynthetic(): Boolean { + return if (declaration is KSPropertyGetter || declaration is KSPropertySetter) { + return true + } else { + if (declaration is KSFunctionDeclaration) { + return declaration.functionKind != FunctionKind.MEMBER && declaration.functionKind != FunctionKind.STATIC + } else { + return false + } + } + } + + override fun isFinal(): Boolean { + return if (declaration is KSPropertyGetter || declaration is KSPropertySetter) { + true + } else { + super.isFinal() + } + } + + override fun getModifiers(): MutableSet { + return super.getModifiers() + } + + override fun getDeclaredTypeVariables(): MutableList { + val nativeType = kspNode() + return if (nativeType is KSDeclaration) { + nativeType.typeParameters.map { + KotlinGenericPlaceholderElement(it, annotationMetadataFactory, visitorContext) + }.toMutableList() + } else { + super.getDeclaredTypeVariables() + } + } + + override fun isSuspend(): Boolean { + val nativeType = nativeType + return if (nativeType is KSModifierListOwner) { + nativeType.modifiers.contains(Modifier.SUSPEND) + } else { + false + } + } + + override fun getSuspendParameters(): Array { + val parameters = getParameters() + return if (isSuspend) { + val continuationParameter = visitorContext.getClassElement("kotlin.coroutines.Continuation") + .map { + var rt = genericReturnType + if (rt.isPrimitive && rt.name.equals("void")) { + rt = ClassElement.of(Unit::class.java) + } + val resolvedType = it.withTypeArguments(mapOf("T" to rt)) + ParameterElement.of( + resolvedType, + "continuation" + ) + }.orElse(null) + if (continuationParameter != null) { + + ArrayUtils.concat(parameters, continuationParameter) + } else { + parameters + } + } else { + parameters + } + } + + override fun overrides(overridden: MethodElement): Boolean { + val nativeType = kspNode() + val overriddenNativeType = overridden.kspNode() + if (nativeType == overriddenNativeType) { + return false + } else if (nativeType is KSFunctionDeclaration) { + return overriddenNativeType == nativeType.findOverridee() + } else if (nativeType is KSPropertySetter && overriddenNativeType is KSPropertySetter) { + return overriddenNativeType.receiver == nativeType.receiver.findOverridee() + } + return false + } + + override fun hides(memberElement: MemberElement?): Boolean { + // not sure how to implement this correctly for Kotlin + return false + } + + override fun withNewOwningType(owningType: ClassElement): MethodElement { + val newMethod = KotlinMethodElement( + declaration, + name, + owningType as KotlinClassElement, + annotationMetadataFactory, + visitorContext, + returnType, + parameters, + abstract, + public, + private, + protected, + internal + ) + copyValues(newMethod) + return newMethod + } + + override fun getName(): String { + return name + } + + override fun getDeclaringType(): ClassElement { + return internalDeclaringType + } + + override fun getReturnType(): ClassElement { + return returnType + } + + override fun getGenericReturnType(): ClassElement { + return if (this is ConstructorElement) { + returnType + } else { + resolveGeneric(declaration.parent, returnType, owningType, visitorContext) + } + } + + override fun getParameters(): Array { + return parameters.toTypedArray() + } + + override fun isAbstract(): Boolean = abstract + + override fun isPublic(): Boolean = public + + override fun isProtected(): Boolean = protected + override fun copyThis(): KotlinMethodElement { + if (declaration is KSPropertySetter) { + return KotlinMethodElement( + parameters[0].type, + propertyElement!!, + declaration.unwrap() as KSPropertySetter, + owningType, + annotationMetadataFactory, + visitorContext + ) + } else if (declaration is KSPropertyGetter) { + return KotlinMethodElement( + propertyElement!!, + declaration.unwrap() as KSPropertyGetter, + owningType, + returnType, + annotationMetadataFactory, + visitorContext + ) + } else if (declaration is KSFunctionDeclaration) { + return KotlinMethodElement( + declaration.unwrap() as KSFunctionDeclaration, + owningType, + returnType, + annotationMetadataFactory, + visitorContext + ) + } else { + + return KotlinMethodElement( + declaration, + name, + owningType, + annotationMetadataFactory, + visitorContext, + returnType, + parameters, + abstract, + public, + private, + protected, + internal + ) + } + } + + override fun isPrivate(): Boolean = private + override fun withAnnotationMetadata(annotationMetadata: AnnotationMetadata): MethodElement { + return super.withAnnotationMetadata(annotationMetadata) as MethodElement + } + + override fun toString(): String { + return "$simpleName(" + parameters.joinToString(",") { + if (it.type.isGenericPlaceholder) { + (it.type as GenericPlaceholderElement).variableName + } else { + it.genericType.name + } + } + ")" + } + + override fun withParameters(vararg newParameters: ParameterElement): MethodElement { + return KotlinMethodElement(declaration, name, owningType, annotationMetadataFactory, visitorContext, returnType, newParameters.toList(), abstract, public, private, protected, internal) + } + + override fun getThrownTypes(): Array { + return stringValues(Throws::class.java, "exceptionClasses") + .flatMap { + val ce = visitorContext.getClassElement(it).orElse(null) + if (ce != null) { + listOf(ce) + } else { + emptyList() + } + }.toTypedArray() + } + +} diff --git a/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/KotlinParameterElement.kt b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/KotlinParameterElement.kt new file mode 100644 index 00000000000..f53ab4e76b8 --- /dev/null +++ b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/KotlinParameterElement.kt @@ -0,0 +1,82 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.kotlin.processing.visitor + +import com.google.devtools.ksp.symbol.KSValueParameter +import io.micronaut.core.annotation.AnnotationMetadata +import io.micronaut.inject.ast.ClassElement +import io.micronaut.inject.ast.MethodElement +import io.micronaut.inject.ast.ParameterElement +import io.micronaut.inject.ast.annotation.ElementAnnotationMetadataFactory + +class KotlinParameterElement( + private val parameterType: ClassElement, + private val methodElement: KotlinMethodElement, + private val parameter: KSValueParameter, + elementAnnotationMetadataFactory: ElementAnnotationMetadataFactory, + visitorContext: KotlinVisitorContext +) : AbstractKotlinElement(KSValueParameterReference(parameter), elementAnnotationMetadataFactory, visitorContext), ParameterElement { + private val internalName : String by lazy { + parameter.name!!.asString() + } + private val internalGenericType : ClassElement by lazy { + resolveGeneric( + methodElement.declaration.parent, + parameterType, + methodElement.owningType, + visitorContext + ) + } + + override fun isPrimitive(): Boolean { + return parameterType.isPrimitive + } + + override fun isArray(): Boolean { + return parameterType.isArray + } + + override fun copyThis(): AbstractKotlinElement { + return KotlinParameterElement( + parameterType, + methodElement, + parameter, + annotationMetadataFactory, + visitorContext + ) + } + + override fun getMethodElement(): MethodElement { + return methodElement + } + + override fun getName(): String { + return internalName + } + + override fun withAnnotationMetadata(annotationMetadata: AnnotationMetadata): ParameterElement { + return super.withAnnotationMetadata(annotationMetadata) as ParameterElement + } + + override fun getType(): ClassElement = parameterType + + override fun getGenericType(): ClassElement { + return internalGenericType + } + + override fun getArrayDimensions(): Int = parameterType.arrayDimensions + +} diff --git a/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/KotlinPropertyElement.kt b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/KotlinPropertyElement.kt new file mode 100644 index 00000000000..15874b05664 --- /dev/null +++ b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/KotlinPropertyElement.kt @@ -0,0 +1,525 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.kotlin.processing.visitor + +import com.google.devtools.ksp.isAbstract +import com.google.devtools.ksp.symbol.* +import io.micronaut.core.annotation.AnnotationMetadata +import io.micronaut.core.annotation.AnnotationMetadataDelegate +import io.micronaut.core.annotation.AnnotationValue +import io.micronaut.core.annotation.AnnotationValueBuilder +import io.micronaut.inject.annotation.AnnotationMetadataHierarchy +import io.micronaut.inject.ast.* +import io.micronaut.inject.ast.annotation.ElementAnnotationMetadataFactory +import io.micronaut.inject.ast.annotation.MutableAnnotationMetadataDelegate +import io.micronaut.kotlin.processing.kspNode +import java.util.* +import java.util.function.Consumer +import java.util.function.Predicate + +class KotlinPropertyElement: AbstractKotlinElement, PropertyElement { + + private val name: String + private val classElement: ClassElement + private val type: ClassElement + private val setter: Optional + private val getter: Optional + private val field: Optional + private val abstract: Boolean + private val exc: Boolean + private var annotationMetadata: MutableAnnotationMetadataDelegate<*>? = null + private val internalDeclaringType: ClassElement by lazy { + var parent = declaration.parent + if (parent is KSPropertyDeclaration) { + parent = parent.parent + } + val owner = getOwningType() + if (parent is KSClassDeclaration) { + if (owner.name.equals(parent.qualifiedName)) { + owner + } else { + visitorContext.elementFactory.newClassElement( + parent.asStarProjectedType() + ) + } + } else { + owner + } + } + + constructor(classElement: ClassElement, + type: ClassElement, + property: KSPropertyDeclaration, + elementAnnotationMetadataFactory: ElementAnnotationMetadataFactory, + visitorContext: KotlinVisitorContext, + excluded : Boolean = false) : super(KSPropertyReference(property), elementAnnotationMetadataFactory, visitorContext) { + this.name = property.simpleName.asString() + this.exc = excluded + this.type = type + this.classElement = classElement + this.setter = Optional.ofNullable(property.setter) + .map { method -> + val modifiers = try { + method.modifiers + } catch (e: IllegalStateException) { + // KSP bug: IllegalStateException: unhandled visibility: invisible_fake + setOf(Modifier.INTERNAL) + } + return@map if (modifiers.contains(Modifier.PRIVATE)) { + null + } else { + visitorContext.elementFactory.newMethodElement(classElement, this, method, type, elementAnnotationMetadataFactory) + } + } + this.getter = Optional.ofNullable(property.getter) + .map { method -> + return@map visitorContext.elementFactory.newMethodElement(classElement, this, method, type, elementAnnotationMetadataFactory) + } + this.abstract = property.isAbstract() + if (property.hasBackingField) { + val newFieldElement = visitorContext.elementFactory.newFieldElement( + classElement, + property, + elementAnnotationMetadataFactory + ) + this.field = Optional.of(newFieldElement) + } else { + this.field = Optional.empty() + } + + val elements: MutableList = ArrayList(3) + setter.ifPresent { elements.add(it) } + getter.ifPresent { elements.add(it) } + field.ifPresent { elements.add(it) } + + + // The instance AnnotationMetadata of each element can change after a modification + // Set annotation metadata as actual elements so the changes are reflected + val propertyAnnotationMetadata: AnnotationMetadata + propertyAnnotationMetadata = if (elements.size == 1) { + elements.iterator().next() + } else { + AnnotationMetadataHierarchy( + true, + *elements.map { e: MemberElement -> + if (e is MethodElement) { + return@map object : AnnotationMetadataDelegate { + override fun getAnnotationMetadata(): AnnotationMetadata { + // Exclude type metadata + return e.getAnnotationMetadata().declaredMetadata + } + } + } + e + }.toTypedArray() + ) + } + this.annotationMetadata = object : MutableAnnotationMetadataDelegate { + override fun annotate(annotationValue: AnnotationValue): Element { + for (memberElement in elements) { + memberElement.annotate(annotationValue) + } + return this@KotlinPropertyElement + } + + override fun annotate( + annotationType: String, + consumer: Consumer> + ): Element { + for (memberElement in elements) { + memberElement.annotate(annotationType, consumer) + } + return this@KotlinPropertyElement + } + + override fun annotate(annotationType: Class): Element { + for (memberElement in elements) { + memberElement.annotate(annotationType) + } + return this@KotlinPropertyElement + } + + override fun annotate(annotationType: String): Element { + for (memberElement in elements) { + memberElement.annotate(annotationType) + } + return this@KotlinPropertyElement + } + + override fun annotate( + annotationType: Class, + consumer: Consumer> + ): Element { + for (memberElement in elements) { + memberElement.annotate(annotationType, consumer) + } + return this@KotlinPropertyElement + } + + override fun removeAnnotation(annotationType: String): Element { + for (memberElement in elements) { + memberElement.removeAnnotation(annotationType) + } + return this@KotlinPropertyElement + } + + override fun removeAnnotationIf(predicate: Predicate>): Element { + for (memberElement in elements) { + memberElement.removeAnnotationIf(predicate) + } + return this@KotlinPropertyElement + } + + override fun getAnnotationMetadata(): AnnotationMetadata { + return propertyAnnotationMetadata + } + } + } + constructor(classElement: ClassElement, + type: ClassElement, + name: String, + getter: KSFunctionDeclaration, + setter: KSFunctionDeclaration?, + elementAnnotationMetadataFactory: ElementAnnotationMetadataFactory, + visitorContext: KotlinVisitorContext, + excluded : Boolean = false) : super(getter, elementAnnotationMetadataFactory, visitorContext) { + this.name = name + this.type = type + this.exc = excluded + this.classElement = classElement + this.setter = Optional.ofNullable(setter) + .map { method -> + visitorContext.elementFactory.newMethodElement(classElement, method, elementAnnotationMetadataFactory) + } + this.getter = Optional.of(visitorContext.elementFactory.newMethodElement(classElement, getter, elementAnnotationMetadataFactory)) + this.abstract = getter.isAbstract || setter?.isAbstract == true + this.field = Optional.empty() + val elements: MutableList = ArrayList(3) + this.setter.ifPresent { elements.add(it) } + this.getter.ifPresent { elements.add(it) } + field.ifPresent { elements.add(it) } + + // The instance AnnotationMetadata of each element can change after a modification + // Set annotation metadata as actual elements so the changes are reflected + val propertyAnnotationMetadata: AnnotationMetadata + propertyAnnotationMetadata = if (elements.size == 1) { + elements.iterator().next() + } else { + AnnotationMetadataHierarchy( + true, + *elements.stream().map { e: MemberElement -> + if (e is MethodElement) { + return@map object : AnnotationMetadataDelegate { + override fun getAnnotationMetadata(): AnnotationMetadata { + // Exclude type metadata + return e.getAnnotationMetadata().declaredMetadata + } + } + } + e + }.toList().toTypedArray() + ) + } + this.annotationMetadata = object : MutableAnnotationMetadataDelegate { + override fun annotate(annotationValue: AnnotationValue): Element { + for (memberElement in elements) { + memberElement.annotate(annotationValue) + } + return this@KotlinPropertyElement + } + + override fun annotate( + annotationType: String, + consumer: Consumer> + ): Element { + for (memberElement in elements) { + memberElement.annotate(annotationType, consumer) + } + return this@KotlinPropertyElement + } + + override fun annotate(annotationType: Class): Element { + for (memberElement in elements) { + memberElement.annotate(annotationType) + } + return this@KotlinPropertyElement + } + + override fun annotate(annotationType: String): Element { + for (memberElement in elements) { + memberElement.annotate(annotationType) + } + return this@KotlinPropertyElement + } + + override fun annotate( + annotationType: Class, + consumer: Consumer> + ): Element { + for (memberElement in elements) { + memberElement.annotate(annotationType, consumer) + } + return this@KotlinPropertyElement + } + + override fun removeAnnotation(annotationType: String): Element { + for (memberElement in elements) { + memberElement.removeAnnotation(annotationType) + } + return this@KotlinPropertyElement + } + + override fun removeAnnotationIf(predicate: Predicate>): Element { + for (memberElement in elements) { + memberElement.removeAnnotationIf(predicate) + } + return this@KotlinPropertyElement + } + + override fun getAnnotationMetadata(): AnnotationMetadata { + return propertyAnnotationMetadata + } + } + } + + constructor(classElement: ClassElement, + type: ClassElement, + name: String, + field: FieldElement?, + getter: MethodElement?, + setter: MethodElement?, + elementAnnotationMetadataFactory: ElementAnnotationMetadataFactory, + visitorContext: KotlinVisitorContext, + excluded : Boolean = false) : super(pickDeclaration(type, field, getter, setter), elementAnnotationMetadataFactory, visitorContext) { + this.name = name + this.type = type + this.classElement = classElement + this.setter = Optional.ofNullable(setter) + this.getter = Optional.ofNullable(getter) + this.abstract = getter?.isAbstract == true || setter?.isAbstract == true + this.field = Optional.ofNullable(field) + val elements: MutableList = ArrayList(3) + this.setter.ifPresent { elements.add(it) } + this.getter.ifPresent { elements.add(it) } + this.field.ifPresent { elements.add(it) } + this.exc = excluded + + // The instance AnnotationMetadata of each element can change after a modification + // Set annotation metadata as actual elements so the changes are reflected + val propertyAnnotationMetadata: AnnotationMetadata + propertyAnnotationMetadata = if (elements.size == 1) { + elements.iterator().next().declaredMetadata + } else { + AnnotationMetadataHierarchy( + true, + *elements.stream().map { e: MemberElement -> + if (e is MethodElement) { + return@map object : AnnotationMetadataDelegate { + override fun getAnnotationMetadata(): AnnotationMetadata { + // Exclude type metadata + return e.getAnnotationMetadata().declaredMetadata + } + } + } + e + }.toList().toTypedArray() + ) + } + this.annotationMetadata = object : MutableAnnotationMetadataDelegate { + override fun annotate(annotationValue: AnnotationValue): Element { + for (memberElement in elements) { + memberElement.annotate(annotationValue) + } + return this@KotlinPropertyElement + } + + override fun annotate( + annotationType: String, + consumer: Consumer> + ): Element { + for (memberElement in elements) { + memberElement.annotate(annotationType, consumer) + } + return this@KotlinPropertyElement + } + + override fun annotate(annotationType: Class): Element { + for (memberElement in elements) { + memberElement.annotate(annotationType) + } + return this@KotlinPropertyElement + } + + override fun annotate(annotationType: String): Element { + for (memberElement in elements) { + memberElement.annotate(annotationType) + } + return this@KotlinPropertyElement + } + + override fun annotate( + annotationType: Class, + consumer: Consumer> + ): Element { + for (memberElement in elements) { + memberElement.annotate(annotationType, consumer) + } + return this@KotlinPropertyElement + } + + override fun removeAnnotation(annotationType: String): Element { + for (memberElement in elements) { + memberElement.removeAnnotation(annotationType) + } + return this@KotlinPropertyElement + } + + override fun removeAnnotationIf(predicate: Predicate>): Element { + for (memberElement in elements) { + memberElement.removeAnnotationIf(predicate) + } + return this@KotlinPropertyElement + } + + override fun getAnnotationMetadata(): AnnotationMetadata { + return propertyAnnotationMetadata + } + } + } + + companion object Helper { + private fun pickDeclaration( + type: ClassElement, + field: FieldElement?, + getter: MethodElement?, + setter: MethodElement? + ): KSNode { + return if (field?.nativeType != null) { + field.nativeType as KSNode + } else if (getter?.nativeType != null) { + getter.nativeType as KSNode + } else if (setter?.nativeType != null) { + setter.nativeType as KSNode + } else { + type.nativeType as KSNode + } + } + } + + override fun overrides(overridden: PropertyElement?): Boolean { + if (overridden == null) { + return false + } else { + val nativeType = kspNode() + val overriddenNativeType = overridden.kspNode() + if (nativeType == overriddenNativeType) { + return false + } else if (nativeType is KSPropertyDeclaration) { + return overriddenNativeType == nativeType.findOverridee() + } + return false + } + } + + override fun isExcluded(): Boolean { + return this.exc + } + + override fun getGenericType(): ClassElement { + return resolveGeneric(declaration.parent, getType(), classElement, visitorContext) + } + + override fun getAnnotationMetadata(): MutableAnnotationMetadataDelegate<*> { + return this.annotationMetadata!! + } + + override fun getField(): Optional { + return this.field + } + + override fun getName(): String = name + override fun getModifiers(): MutableSet { + return super.getModifiers() + } + + override fun getType(): ClassElement = type + + override fun getDeclaringType(): ClassElement { + return internalDeclaringType + } + + override fun getOwningType(): ClassElement = classElement + + override fun getReadMethod(): Optional = getter + + override fun getWriteMethod(): Optional = setter + + override fun isReadOnly(): Boolean { + return !setter.isPresent || setter.get().isPrivate + } + + override fun copyThis(): AbstractKotlinElement { + if (nativeType is KSPropertyDeclaration) { + val property : KSPropertyDeclaration = nativeType as KSPropertyDeclaration + return KotlinPropertyElement( + classElement, + type, + property, + annotationMetadataFactory, + visitorContext, + exc + ) + } else { + val getter : KSFunctionDeclaration = nativeType as KSFunctionDeclaration + return KotlinPropertyElement( + classElement, + type, + name, + getter, + setter.map { it.nativeType as KSFunctionDeclaration }.orElse(null), + annotationMetadataFactory, + visitorContext, + exc + ) + } + } + + override fun isAbstract() = abstract + override fun withAnnotationMetadata(annotationMetadata: AnnotationMetadata): MemberElement { + return super.withAnnotationMetadata(annotationMetadata) as MemberElement + } + + override fun isPrimitive(): Boolean { + return type.isPrimitive + } + + override fun isArray(): Boolean { + return type.isArray + } + + override fun getArrayDimensions(): Int { + return type.arrayDimensions + } + + override fun isDeclaredNullable(): Boolean { + return type is KotlinClassElement && type.kotlinType.isMarkedNullable + } + + override fun isNullable(): Boolean { + return type is KotlinClassElement && type.kotlinType.isMarkedNullable + } + +} diff --git a/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/KotlinVisitorContext.kt b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/KotlinVisitorContext.kt new file mode 100644 index 00000000000..639236fe2d8 --- /dev/null +++ b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/KotlinVisitorContext.kt @@ -0,0 +1,237 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.kotlin.processing.visitor + +import com.google.devtools.ksp.KspExperimental +import com.google.devtools.ksp.getClassDeclarationByName +import com.google.devtools.ksp.getJavaClassByName +import com.google.devtools.ksp.processing.Resolver +import com.google.devtools.ksp.processing.SymbolProcessorEnvironment +import com.google.devtools.ksp.symbol.KSClassDeclaration +import com.google.devtools.ksp.symbol.KSNode +import io.micronaut.core.convert.ArgumentConversionContext +import io.micronaut.core.convert.value.MutableConvertibleValues +import io.micronaut.core.convert.value.MutableConvertibleValuesMap +import io.micronaut.core.util.StringUtils +import io.micronaut.inject.annotation.AbstractAnnotationMetadataBuilder +import io.micronaut.inject.ast.ClassElement +import io.micronaut.inject.ast.Element +import io.micronaut.inject.ast.annotation.ElementAnnotationMetadataFactory +import io.micronaut.inject.visitor.VisitorContext +import io.micronaut.inject.writer.GeneratedFile +import io.micronaut.kotlin.processing.annotation.KotlinAnnotationMetadataBuilder +import io.micronaut.kotlin.processing.KotlinOutputVisitor +import io.micronaut.kotlin.processing.annotation.KotlinElementAnnotationMetadataFactory +import java.io.* +import java.net.URI +import java.nio.file.Files +import java.util.* +import java.util.function.BiConsumer + +@OptIn(KspExperimental::class) +open class KotlinVisitorContext(private val environment: SymbolProcessorEnvironment, + val resolver: Resolver) : VisitorContext { + + private val visitorAttributes: MutableConvertibleValues + private val elementFactory: KotlinElementFactory + private val outputVisitor = KotlinOutputVisitor(environment) + val annotationMetadataBuilder: KotlinAnnotationMetadataBuilder + private val elementAnnotationMetadataFactory: KotlinElementAnnotationMetadataFactory + + init { + visitorAttributes = MutableConvertibleValuesMap() + annotationMetadataBuilder = KotlinAnnotationMetadataBuilder(environment, resolver, this) + elementFactory = KotlinElementFactory(this) + elementAnnotationMetadataFactory = KotlinElementAnnotationMetadataFactory(false, annotationMetadataBuilder) + } + + override fun get(name: CharSequence?, conversionContext: ArgumentConversionContext?): Optional { + return visitorAttributes.get(name, conversionContext) + } + + override fun names(): MutableSet { + return visitorAttributes.names() + } + + override fun values(): MutableCollection { + return visitorAttributes.values() + } + + override fun put(key: CharSequence?, value: Any?): MutableConvertibleValues { + visitorAttributes.put(key, value) + return this + } + + override fun remove(key: CharSequence?): MutableConvertibleValues { + visitorAttributes.remove(key) + return this + } + + override fun clear(): MutableConvertibleValues { + visitorAttributes.clear() + return this + } + + override fun getClassElement(name: String): Optional { + var declaration = resolver.getClassDeclarationByName(name) + if (declaration == null) { + declaration = resolver.getClassDeclarationByName(name.replace('$', '.')) + } + return Optional.ofNullable(declaration?.asStarProjectedType()) + .map(elementFactory::newClassElement) + } + + @OptIn(KspExperimental::class) + override fun getClassElements(aPackage: String, vararg stereotypes: String): Array { + return resolver.getDeclarationsFromPackage(aPackage) + .filterIsInstance() + .filter { declaration -> + declaration.annotations.any { ann -> + stereotypes.contains(KotlinAnnotationMetadataBuilder.getAnnotationTypeName(ann, this)) + } + } + .map { declaration -> + elementFactory.newClassElement(declaration.asStarProjectedType()) + } + .toList() + .toTypedArray() + } + + override fun getServiceEntries(): MutableMap> { + return outputVisitor.serviceEntries + } + + override fun visitClass(classname: String, vararg originatingElements: Element): OutputStream { + return outputVisitor.visitClass(classname, *originatingElements) + } + + override fun visitServiceDescriptor(type: String, classname: String) { + outputVisitor.visitServiceDescriptor(type, classname) + } + + override fun visitServiceDescriptor(type: String, classname: String, originatingElement: Element) { + outputVisitor.visitServiceDescriptor(type, classname, originatingElement) + } + + override fun visitMetaInfFile(path: String, vararg originatingElements: Element): Optional { + return outputVisitor.visitMetaInfFile(path, *originatingElements) + } + + override fun visitGeneratedFile(path: String?): Optional { + return outputVisitor.visitGeneratedFile(path) + } + + override fun finish() { + outputVisitor.finish() + } + + override fun getClassElement( + name: String, + annotationMetadataFactory: ElementAnnotationMetadataFactory + ): Optional { + var declaration = resolver.getClassDeclarationByName(name) + if (declaration == null) { + declaration = resolver.getClassDeclarationByName(name.replace('$', '.')) + } + return Optional.ofNullable(declaration?.asStarProjectedType()) + .map { elementFactory.newClassElement(it, annotationMetadataFactory) } + } + + override fun getElementFactory(): KotlinElementFactory = elementFactory + override fun getElementAnnotationMetadataFactory(): ElementAnnotationMetadataFactory { + return elementAnnotationMetadataFactory + } + + override fun getAnnotationMetadataBuilder(): AbstractAnnotationMetadataBuilder<*, *> { + return annotationMetadataBuilder + } + + override fun info(message: String, element: Element?) { + printMessage(message, environment.logger::info, element) + } + + fun info(message: String, element: KSNode?) { + printMessage(message, environment.logger::info, element) + } + + override fun info(message: String) { + printMessage(message, environment.logger::info, null as KSNode?) + } + + override fun fail(message: String, element: Element?) { + printMessage(message, environment.logger::error, element) + } + + fun fail(message: String, element: KSNode?) { + printMessage(message, environment.logger::error, element) + } + + override fun warn(message: String, element: Element?) { + printMessage(message, environment.logger::warn, element) + } + + fun warn(message: String, element: KSNode?) { + printMessage(message, environment.logger::warn, element) + } + + private fun printMessage(message: String, logger: BiConsumer, element: Element?) { + if (element is AbstractKotlinElement<*>) { + val el = element.nativeType + printMessage(message, logger, el) + } else { + printMessage(message, logger, null as KSNode?) + } + } + + private fun printMessage(message: String, logger: BiConsumer, element: KSNode?) { + if (StringUtils.isNotEmpty(message)) { + logger.accept(message, element) + } + } + + class KspGeneratedFile(private val outputStream: OutputStream, + private val path: String) : GeneratedFile { + + private val file = File(path) + + override fun toURI(): URI { + return file.toURI() + } + + override fun getName(): String { + return file.name + } + + override fun openInputStream(): InputStream { + return Files.newInputStream(file.toPath()) + } + + override fun openOutputStream(): OutputStream = outputStream + + override fun openReader(): Reader { + return file.reader() + } + + override fun getTextContent(): CharSequence { + return file.readText() + } + + override fun openWriter(): Writer { + return OutputStreamWriter(outputStream) + } + + } +} diff --git a/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/KotlinWildcardElement.kt b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/KotlinWildcardElement.kt new file mode 100644 index 00000000000..170201abe24 --- /dev/null +++ b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/KotlinWildcardElement.kt @@ -0,0 +1,89 @@ +/* + * Copyright 2017-2023 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.kotlin.processing.visitor + +import io.micronaut.core.annotation.NonNull +import io.micronaut.inject.ast.ArrayableClassElement +import io.micronaut.inject.ast.ClassElement +import io.micronaut.inject.ast.WildcardElement +import io.micronaut.inject.ast.annotation.ElementAnnotationMetadataFactory +import java.util.function.Function + +class KotlinWildcardElement( + private val upperBounds: List, + private val lowerBounds: List, + elementAnnotationMetadataFactory: ElementAnnotationMetadataFactory, + visitorContext: KotlinVisitorContext, + arrayDimensions: Int = 0 +) : KotlinClassElement( + upperBounds[0]!!.nativeType, + elementAnnotationMetadataFactory, + visitorContext, + arrayDimensions, + false +), WildcardElement { + + override fun foldBoundGenericTypes(@NonNull fold: Function): ClassElement? { + val upperBounds: List = this.upperBounds + .map { ele -> + toKotlinClassElement( + ele?.foldBoundGenericTypes(fold) + ) + }.toList() + val lowerBounds: List = this.lowerBounds + .map { ele -> + toKotlinClassElement( + ele?.foldBoundGenericTypes(fold) + ) + }.toList() + return fold.apply( + if (upperBounds.contains(null) || lowerBounds.contains(null)) null else KotlinWildcardElement( + upperBounds, lowerBounds, elementAnnotationMetadataFactory, visitorContext, arrayDimensions + ) + ) + } + + override fun getUpperBounds(): MutableList { + val list = mutableListOf() + list.addAll(upperBounds) + return list + } + + override fun getLowerBounds(): MutableList { + val list = mutableListOf() + list.addAll(lowerBounds) + return list + } + + private fun toKotlinClassElement(element: ClassElement?): KotlinClassElement { + return if (element == null || element is KotlinClassElement) { + element as KotlinClassElement + } else { + if (element.isWildcard || element.isGenericPlaceholder) { + throw UnsupportedOperationException("Cannot convert wildcard / free type variable to JavaClassElement") + } else { + (visitorContext.getClassElement(element.name, elementAnnotationMetadataFactory) + .orElseThrow { + UnsupportedOperationException( + "Cannot convert ClassElement to JavaClassElement, class was not found on the visitor context" + ) + } as ArrayableClassElement) + .withArrayDimensions(element.arrayDimensions) + .withBoundGenericTypes(element.boundGenericTypes) as KotlinClassElement + } + } + } +} diff --git a/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/LoadedVisitor.kt b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/LoadedVisitor.kt new file mode 100644 index 00000000000..274955f379a --- /dev/null +++ b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/LoadedVisitor.kt @@ -0,0 +1,114 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.kotlin.processing.visitor + +import com.google.devtools.ksp.getClassDeclarationByName +import com.google.devtools.ksp.symbol.KSClassDeclaration +import com.google.devtools.ksp.symbol.KSType +import io.micronaut.core.annotation.AnnotationMetadata +import io.micronaut.core.order.Ordered +import io.micronaut.core.reflect.GenericTypeUtils +import io.micronaut.inject.visitor.TypeElementVisitor +import java.util.* + +class LoadedVisitor(val visitor: TypeElementVisitor<*, *>, + val visitorContext: KotlinVisitorContext): Ordered { + + companion object { + const val ANY = "kotlin.Any" + } + + var classAnnotation: String = ANY + var elementAnnotation: String = ANY + + init { + val javaClass = visitor.javaClass + val resolver = visitorContext.resolver + val declaration = resolver.getClassDeclarationByName(javaClass.name) + val tevClassName = TypeElementVisitor::class.java.name + + if (declaration != null) { + val reference = declaration.superTypes + .map { it.resolve() } + .find { + it.declaration.qualifiedName?.asString() == tevClassName + }!! + classAnnotation = getType(reference.arguments[0].type!!.resolve(), visitor.classType) + elementAnnotation = getType(reference.arguments[1].type!!.resolve(), visitor.elementType) + } else { + val classes = GenericTypeUtils.resolveInterfaceTypeArguments( + javaClass, + TypeElementVisitor::class.java + ) + if (classes != null && classes.size == 2) { + val classGeneric = classes[0] + classAnnotation = if (classGeneric == Any::class.java) { + visitor.classType + } else { + classGeneric.name + } + val elementGeneric = classes[1] + elementAnnotation = if (elementGeneric == Any::class.java) { + visitor.elementType + } else { + elementGeneric.name + } + } else { + classAnnotation = Any::class.java.name + elementAnnotation = Any::class.java.name + } + } + if (classAnnotation == ANY) { + classAnnotation = Object::class.java.name + } + if (elementAnnotation == ANY) { + elementAnnotation = Object::class.java.name + } + } + + override fun getOrder(): Int { + return visitor.order + } + + private fun getType(type: KSType, default: String): String { + return if (!type.isError) { + val elementAnnotation = type.declaration.qualifiedName!!.asString() + if (elementAnnotation == ANY) { + default + } else { + elementAnnotation + } + } else { + //sigh + UUID.randomUUID().toString() + } + } + + fun matches(classDeclaration: KSClassDeclaration): Boolean { + if (classAnnotation == "java.lang.Object") { + return true + } + val annotationMetadata = visitorContext.annotationMetadataBuilder.buildDeclared(classDeclaration) + return annotationMetadata.hasStereotype(classAnnotation) + } + + fun matches(annotationMetadata: AnnotationMetadata): Boolean { + if (elementAnnotation == "java.lang.Object") { + return true + } + return annotationMetadata.hasStereotype(elementAnnotation) + } +} diff --git a/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/TypeElementSymbolProcessor.kt b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/TypeElementSymbolProcessor.kt new file mode 100644 index 00000000000..37dd9d0b36e --- /dev/null +++ b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/TypeElementSymbolProcessor.kt @@ -0,0 +1,332 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.kotlin.processing.visitor + +import com.google.devtools.ksp.isConstructor +import com.google.devtools.ksp.isInternal +import com.google.devtools.ksp.processing.Resolver +import com.google.devtools.ksp.processing.SymbolProcessor +import com.google.devtools.ksp.processing.SymbolProcessorEnvironment +import com.google.devtools.ksp.symbol.* +import com.google.devtools.ksp.visitor.KSTopDownVisitor +import io.micronaut.context.annotation.Requires +import io.micronaut.context.annotation.Requires.Sdk +import io.micronaut.core.annotation.Generated +import io.micronaut.core.annotation.NonNull +import io.micronaut.core.order.OrderUtil +import io.micronaut.core.util.StringUtils +import io.micronaut.core.version.VersionUtils +import io.micronaut.inject.ast.* +import io.micronaut.inject.processing.ProcessingException +import io.micronaut.inject.visitor.TypeElementVisitor +import io.micronaut.inject.visitor.VisitorContext +import java.util.* + +open class TypeElementSymbolProcessor(private val environment: SymbolProcessorEnvironment): SymbolProcessor { + + private lateinit var loadedVisitors: MutableList + private lateinit var typeElementVisitors: Collection> + private lateinit var visitorContext: KotlinVisitorContext + + companion object { + private val SERVICE_LOADER = io.micronaut.core.io.service.SoftServiceLoader.load(TypeElementVisitor::class.java) + } + + open fun newClassElement( + visitorContext: KotlinVisitorContext, + classDeclaration: KSClassDeclaration + ) = visitorContext.elementFactory.newClassElement( + classDeclaration.asStarProjectedType(), + visitorContext.elementAnnotationMetadataFactory + ) + + override fun process(resolver: Resolver): List { + // set supported options as system properties to keep compatibility + // in particular for micronaut-openapi + environment.options.entries.stream() + .filter { (key) -> + key.startsWith( + VisitorContext.MICRONAUT_BASE_OPTION_NAME + ) + } + .forEach { (key, value) -> + System.setProperty( + key, + value + ) + } + + typeElementVisitors = findTypeElementVisitors() + loadedVisitors = ArrayList(typeElementVisitors.size) + visitorContext = KotlinVisitorContext(environment, resolver) + + start() + + if (loadedVisitors.isNotEmpty()) { + + + val elements = resolver.getAllFiles() + .flatMap { file: KSFile -> file.declarations } + .filterIsInstance() + .filter { declaration: KSClassDeclaration -> + declaration.annotations.none { ksAnnotation -> + ksAnnotation.shortName.getQualifier() == Generated::class.simpleName + } + } + .toList() + + if (elements.isNotEmpty()) { + + // The visitor X with a higher priority should process elements of A before + // the visitor Y which is processing elements of B but also using elements A + + // Micronaut Data use-case: EntityMapper with a higher priority needs to process entities first + // before RepositoryMapper is going to process repositories and read entities + for (loadedVisitor in loadedVisitors) { + for (typeElement in elements) { + if (!loadedVisitor.matches(typeElement)) { + continue + } + if (typeElement.classKind != ClassKind.ANNOTATION_CLASS) { + val className = typeElement.qualifiedName.toString() + try { + typeElement.accept(ElementVisitor(loadedVisitor, typeElement), className) + } catch (e: ProcessingException) { + val message = e.message + if (message != null) { + environment.logger.error(message, e.originatingElement as KSNode) + } else { + environment.logger.error("Unknown error processing element", e.originatingElement as KSNode) + val cause = e.cause + if (cause != null) { + environment.logger.exception(cause) + } else { + environment.logger.exception(e) + } + } + } + } + } + } + } + } + return emptyList() + } + + override fun finish() { + for (loadedVisitor in loadedVisitors) { + try { + loadedVisitor.visitor.finish(visitorContext) + } catch (e: Throwable) { + environment.logger.error("Error finalizing type visitor [${loadedVisitor.visitor}]: ${e.message}") + } + } + visitorContext.finish() + } + + override fun onError() { + + } + + private fun start() { + for (visitor in typeElementVisitors) { + try { + loadedVisitors.add( + LoadedVisitor( + visitor, + visitorContext + ) + ) + } catch (e: TypeNotPresentException) { + // ignored, means annotations referenced are not on the classpath + } catch (e: NoClassDefFoundError) { + } + + } + + OrderUtil.reverseSort(loadedVisitors) + + for (loadedVisitor in loadedVisitors) { + try { + loadedVisitor.visitor.start(visitorContext) + } catch (e: Throwable) { + environment.logger.error("Error initializing type visitor [${loadedVisitor.visitor}]: ${e.message}") + } + } + } + + @NonNull + private fun findTypeElementVisitors(): Collection> { + val typeElementVisitors: MutableMap> = HashMap(10) + for (definition in SERVICE_LOADER) { + if (definition.isPresent) { + val visitor: TypeElementVisitor<*, *>? = try { + definition.load() + } catch (e: Throwable) { + environment.logger.warn("TypeElementVisitor [" + definition.name + "] will be ignored due to loading error: " + e.message) + continue + } + if (visitor == null || !visitor.isEnabled) { + continue + } + val requires = visitor.javaClass.getAnnotation(Requires::class.java) + if (requires != null) { + val sdk: Sdk = requires.sdk + if (sdk == Sdk.MICRONAUT) { + val version: String = requires.version + if (StringUtils.isNotEmpty(version) && !VersionUtils.isAtLeastMicronautVersion(version)) { + try { + environment.logger.warn("TypeElementVisitor [" + definition.name + "] will be ignored because Micronaut version [" + VersionUtils.MICRONAUT_VERSION + "] must be at least " + version) + continue + } catch (e: IllegalArgumentException) { + // shouldn't happen, thrown when invalid version encountered + } + } + } + } + typeElementVisitors[definition.name] = visitor + } + } + return typeElementVisitors.values + } + + private inner class ElementVisitor(private val loadedVisitor: LoadedVisitor, + private val classDeclaration: KSClassDeclaration) : KSTopDownVisitor() { + + override fun visitClassDeclaration(classDeclaration: KSClassDeclaration, data: Any): Any { + if (classDeclaration.qualifiedName!!.asString() == "kotlin.Any") { + return data + } + if (classDeclaration.classKind == ClassKind.ENUM_ENTRY) { + return data + } + if (classDeclaration == this.classDeclaration) { + val visitorContext = loadedVisitor.visitorContext + if (loadedVisitor.matches(classDeclaration)) { + val classElement = newClassElement(visitorContext, classDeclaration) + + try { + loadedVisitor.visitor.visitClass(classElement, visitorContext) + } catch (e: Exception) { + throw ProcessingException(classElement, e.message) + } + + + classDeclaration.getAllFunctions() + .filter { it.isConstructor() && !it.isInternal() } + .forEach { + visitConstructor(classElement, it) + } + + visitMembers(classElement) + val innerClassQuery = + ElementQuery.ALL_INNER_CLASSES.onlyStatic().modifiers { it.contains(ElementModifier.PUBLIC) } + val innerClasses = classElement.getEnclosedElements(innerClassQuery) + innerClasses.forEach { + val visitor = loadedVisitor.visitor + val visitorContext = loadedVisitor.visitorContext + if (loadedVisitor.matches(it)) { + visitor.visitClass(it, visitorContext) + visitMembers(it) + } + } + } + } + return data + } + + private fun visitMembers(classElement: ClassElement) { + val properties = classElement.syntheticBeanProperties + for (property in properties) { + try { + visitNativeProperty(property) + } catch (e: Exception) { + throw ProcessingException(property, e.message, e) + } + } + val memberElements = classElement.getEnclosedElements(ElementQuery.ALL_FIELD_AND_METHODS) + for (memberElement in memberElements) { + when (memberElement) { + is FieldElement -> { + visitField(memberElement) + } + + is MethodElement -> { + visitMethod(memberElement) + } + } + } + } + + private fun visitMethod(memberElement: MethodElement) { + val visitor = loadedVisitor.visitor + val visitorContext = loadedVisitor.visitorContext + if (loadedVisitor.matches(memberElement)) { + try { + visitor.visitMethod(memberElement, visitorContext) + } catch (e: Exception) { + throw ProcessingException(memberElement, e.message) + } + } + } + + private fun visitField(memberElement: FieldElement) { + val visitor = loadedVisitor.visitor + val visitorContext = loadedVisitor.visitorContext + if (loadedVisitor.matches(memberElement)) { + try { + visitor.visitField(memberElement, visitorContext) + } catch (e: Exception) { + throw ProcessingException(memberElement, e.message) + } + } + } + + private fun visitConstructor(classElement: ClassElement, ctor: KSFunctionDeclaration) { + val visitor = loadedVisitor.visitor + val visitorContext = loadedVisitor.visitorContext + val ctorElement = visitorContext.elementFactory.newConstructorElement( + classElement, + ctor, + visitorContext.elementAnnotationMetadataFactory + ) + if (loadedVisitor.matches(ctorElement)) { + try { + visitor.visitConstructor(ctorElement, visitorContext) + } catch (e: Exception) { + throw ProcessingException(ctorElement, e.message) + } + } + } + + fun visitNativeProperty(propertyNode : PropertyElement) { + val visitor = loadedVisitor.visitor + val visitorContext = loadedVisitor.visitorContext + if (loadedVisitor.matches(propertyNode)) { + propertyNode.field.ifPresent { visitor.visitField(it, visitorContext)} + // visit synthetic getter/setter methods + propertyNode.writeMethod.ifPresent { visitor.visitMethod(it, visitorContext)} + propertyNode.readMethod.ifPresent{ visitor.visitMethod(it, visitorContext)} + } + } + + override fun defaultHandler(node: KSNode, data: Any): Any { + return data + } + + } +} + diff --git a/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/TypeElementSymbolProcessorProvider.kt b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/TypeElementSymbolProcessorProvider.kt new file mode 100644 index 00000000000..769d745762a --- /dev/null +++ b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/TypeElementSymbolProcessorProvider.kt @@ -0,0 +1,27 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.kotlin.processing.visitor + +import com.google.devtools.ksp.processing.SymbolProcessor +import com.google.devtools.ksp.processing.SymbolProcessorEnvironment +import com.google.devtools.ksp.processing.SymbolProcessorProvider + +open class TypeElementSymbolProcessorProvider: SymbolProcessorProvider { + + override fun create(environment: SymbolProcessorEnvironment): SymbolProcessor { + return TypeElementSymbolProcessor(environment) + } +} diff --git a/inject-kotlin/src/main/resources/META-INF/services/com.google.devtools.ksp.processing.SymbolProcessorProvider b/inject-kotlin/src/main/resources/META-INF/services/com.google.devtools.ksp.processing.SymbolProcessorProvider new file mode 100644 index 00000000000..4a2ff0176c0 --- /dev/null +++ b/inject-kotlin/src/main/resources/META-INF/services/com.google.devtools.ksp.processing.SymbolProcessorProvider @@ -0,0 +1,2 @@ +io.micronaut.kotlin.processing.visitor.TypeElementSymbolProcessorProvider +io.micronaut.kotlin.processing.beans.BeanDefinitionProcessorProvider diff --git a/inject-kotlin/src/main/resources/notes.txt b/inject-kotlin/src/main/resources/notes.txt new file mode 100644 index 00000000000..064c96547ea --- /dev/null +++ b/inject-kotlin/src/main/resources/notes.txt @@ -0,0 +1,3 @@ +Differences: + +In Kotlin the enums have implicit properties name and ordinal that I'm not able to distinguish between defined properties. In Java only the defined properties are properties in the introspeciton diff --git a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/adapter/MethodAdapterSpec.groovy b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/adapter/MethodAdapterSpec.groovy new file mode 100644 index 00000000000..d5184e9092b --- /dev/null +++ b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/adapter/MethodAdapterSpec.groovy @@ -0,0 +1,380 @@ +/* + * Copyright 2017-2019 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.kotlin.processing.aop.adapter + +import io.micronaut.context.ApplicationContext +import io.micronaut.context.annotation.Requires +import io.micronaut.context.event.ApplicationEventListener +import io.micronaut.context.event.StartupEvent +import io.micronaut.core.reflect.ReflectionUtils +import io.micronaut.inject.AdvisedBeanType +import io.micronaut.inject.BeanDefinition +import spock.lang.PendingFeature +import spock.lang.Specification + +import java.nio.charset.StandardCharsets + +import static io.micronaut.annotation.processing.test.KotlinCompiler.* + +class MethodAdapterSpec extends Specification { + + void 'test method adapter with failing requirements is not present'() { + given: + def context = buildContext(''' +package issue5640 + +import io.micronaut.aop.Adapter +import io.micronaut.context.annotation.Requires +import jakarta.inject.Singleton +import java.nio.charset.StandardCharsets + +@Singleton +@Requires(property="not.present") +class AsciiParser { + @Parse + fun parseAsAscii(value: ByteArray): String { + return String(value, StandardCharsets.US_ASCII) + } +} + +@Retention +@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION) +@Adapter(Parser::class) +annotation class Parse + +interface Parser { + fun parse(value: ByteArray): String +} +''') + def adaptedType = context.classLoader.loadClass('issue5640.Parser') + + expect: + !context.containsBean(adaptedType) + context.getBeansOfType(adaptedType).isEmpty() + } + + void 'test method adapter with byte[] argument'() { + given: + def context = buildContext(''' +package issue5054 + +import io.micronaut.aop.Adapter +import jakarta.inject.Singleton +import java.nio.charset.StandardCharsets + +@Singleton +class AsciiParser { + @Parse + fun parseAsAscii(value: ByteArray): String { + return String(value, StandardCharsets.US_ASCII) + } +} + +@Retention +@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION) +@Adapter(Parser::class) +annotation class Parse + +interface Parser { + fun parse(value: ByteArray): String +} +''') + def adaptedType = context.classLoader.loadClass('issue5054.Parser') + def parser = context.getBean(adaptedType) + def result = parser.parse("test".getBytes(StandardCharsets.US_ASCII)) + + expect: + result == 'test' + } + + void "test method adapter inherits metadata"() { + when:"An adapter method is parsed that has requirements" + BeanDefinition definition = buildBeanDefinition('test.Test$ApplicationEventListener$onStartup1$Intercepted','''\ +package test + +import io.micronaut.aop.Adapter +import io.micronaut.context.event.ApplicationEventListener +import io.micronaut.context.event.StartupEvent + +@jakarta.inject.Singleton +@io.micronaut.context.annotation.Requires(property="foo.bar") +class Test { + + @Adapter(ApplicationEventListener::class) + fun onStartup(event: StartupEvent) { + + } +} + +''') + then:"Then a bean is produced that is valid" + definition != null + definition.annotationMetadata.hasAnnotation(Requires) + definition.annotationMetadata.stringValue(Requires, "property").get() == 'foo.bar' + } + + void "test method adapter with around overloading"() { + given: + def context = buildContext(''' +package adapteroverloading + +import io.micronaut.context.event.* +import jakarta.inject.Singleton +import io.micronaut.runtime.event.annotation.* + +@Singleton +class Test { + var invoked = false + var shutdown = false + + @EventListener + fun receive(event: StartupEvent) { + invoked = true + } + + @EventListener + fun receive(event: ShutdownEvent) { + shutdown = true + } +} + +''') + + when: + def bean = getBean(context, 'adapteroverloading.Test') + + then: + bean.invoked + + when: + context.close() + + then: + bean.shutdown + } + + void "test method adapter with around advice"() { + given: + def context = buildContext(''' +package adapteraround + +import io.micronaut.context.event.StartupEvent +import io.micronaut.scheduling.annotation.Async +import jakarta.inject.Singleton +import java.util.concurrent.CompletableFuture +import io.micronaut.runtime.event.annotation.* + +@Singleton +open class Test { + + var invoked = false + private set + + @EventListener + @Async + open fun onStartup(event: StartupEvent): CompletableFuture { + invoked = true + return CompletableFuture.completedFuture(invoked) + } +} + +''') + + def bean = getBean(context,'adapteraround.Test') + + expect: + bean.invoked + + cleanup: + context.close() + } + + void "test method adapter produces additional bean"() { + when:"An adapter method is parsed" + BeanDefinition definition = buildBeanDefinition('test.Test$ApplicationEventListener$onStartup1$Intercepted','''\ +package test; + +import io.micronaut.aop.* +import io.micronaut.inject.annotation.* +import io.micronaut.context.annotation.* +import io.micronaut.context.event.* + +@jakarta.inject.Singleton +class Test { + + @Adapter(ApplicationEventListener::class) + fun onStartup(event: StartupEvent) { + + } +} + +''') + then:"Then a bean is produced that is valid" + definition != null + !(definition instanceof AdvisedBeanType) + ApplicationEventListener.isAssignableFrom(definition.getBeanType()) + !definition.getTypeArguments(ApplicationEventListener).isEmpty() + definition.getTypeArguments(ApplicationEventListener).get(0).type == StartupEvent + } + + void "test method adapter inherited from an interface produces additional bean"() { + when:"An adapter method is parsed" + BeanDefinition definition = buildBeanDefinition('test.Test$ApplicationEventListener$onStartup1$Intercepted','''\ +package test; + +import io.micronaut.aop.*; +import io.micronaut.inject.annotation.*; +import io.micronaut.context.annotation.*; +import io.micronaut.context.event.*; + +@jakarta.inject.Singleton +class Test: TestContract { + + override fun onStartup(event: StartupEvent) { + } +} + +interface TestContract { + + @Adapter(ApplicationEventListener::class) + fun onStartup(event: StartupEvent) +} +''') + then:"Then a bean is produced that is valid" + definition != null + ApplicationEventListener.isAssignableFrom(definition.getBeanType()) + !definition.getTypeArguments(ApplicationEventListener).isEmpty() + definition.getTypeArguments(ApplicationEventListener).get(0).type == StartupEvent + } + + void "test method adapter honours type restraints - correct path"() { + when:"An adapter method is parsed" + BeanDefinition definition = buildBeanDefinition('test.Test$Foo$myMethod1$Intercepted','''\ +package test + +import io.micronaut.aop.* +import io.micronaut.inject.annotation.* +import io.micronaut.context.annotation.* +import io.micronaut.context.event.* + +@jakarta.inject.Singleton +class Test { + + @Adapter(Foo::class) + fun myMethod(blah: String) { + + } +} + +interface Foo: java.util.function.Consumer +''') + then:"Then a bean is produced that is valid" + definition != null + ReflectionUtils.getAllInterfaces(definition.getBeanType()).find { it.name == 'test.Foo'} + !definition.getTypeArguments("test.Foo").isEmpty() + definition.getTypeArguments("test.Foo").get(0).type == String + } + + void "test method adapter honours type restraints - compilation error"() { + when:"An adapter method is parsed" + BeanDefinition definition = buildBeanDefinition('test.Test$Foo$myMethod$Intercepted','''\ +package test + +import io.micronaut.aop.* +import io.micronaut.inject.annotation.* +import io.micronaut.context.annotation.* +import io.micronaut.context.event.* + +@jakarta.inject.Singleton +class Test { + + @Adapter(Foo::class) + fun myMethod(blah: Integer) { + + } +} + +interface Foo: java.util.function.Consumer +''') + then:"An error occurs" + def e = thrown(RuntimeException) + e.message.contains 'Cannot adapt method [myMethod(java.lang.Integer)] to target method [accept(T)]. Type [java.lang.Integer] is not a subtype of type [java.lang.CharSequence] for argument at position 0' + } + + void "test method adapter wrong argument count"() { + when:"An adapter method is parsed" + buildBeanDefinition('test.Test$ApplicationEventListener$onStartup$Intercepted','''\ +package test + +import io.micronaut.aop.* +import io.micronaut.inject.annotation.* +import io.micronaut.context.annotation.* +import io.micronaut.context.event.* + +@jakarta.inject.Singleton +class Test { + + @Adapter(ApplicationEventListener::class) + fun onStartup(event: StartupEvent, stuff: Boolean) { + + } +} + +''') + then:"Then a bean is produced that is valid" + def e = thrown(RuntimeException) + e.message.contains("Cannot adapt method [onStartup(io.micronaut.context.event.StartupEvent,boolean)] to target method [onApplicationEvent(E)]. Argument lengths don't match.") + + } +/* + void "test method adapter argument order"() { + when:"An adapter method is parsed" + BeanDefinition definition = buildBeanDefinition('org.atinject.jakartatck.auto.events.EventListener$EventHandlerMultipleArguments$onEvent1$Intercepted','''\ +package org.atinject.jakartatck.auto.events; + +@jakarta.inject.Singleton +class EventListener { + + @EventHandler + public void onEvent(Metadata metadata, SomeEvent event) { + } + +} + +''') + then:"Then a bean is produced that is valid" + definition != null + EventHandlerMultipleArguments.isAssignableFrom(definition.getBeanType()) + definition.getTypeArguments(EventHandlerMultipleArguments).size() == 2 + definition.getTypeArguments(EventHandlerMultipleArguments).get(0).type == Metadata + definition.getTypeArguments(EventHandlerMultipleArguments).get(1).type == SomeEvent + } +*/ + + void "test adapter is invoked"() { + given: + ApplicationContext ctx = ApplicationContext.run("foo.bar":true) + + when: + Test t = ctx.getBean(Test) + + then: + t.invoked + + cleanup: + ctx.close() + } +} diff --git a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/compile/AbstractClassIntroductionSpec.groovy b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/compile/AbstractClassIntroductionSpec.groovy new file mode 100644 index 00000000000..612ee5df725 --- /dev/null +++ b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/compile/AbstractClassIntroductionSpec.groovy @@ -0,0 +1,262 @@ +package io.micronaut.kotlin.processing.aop.compile + +import io.micronaut.context.DefaultBeanContext +import io.micronaut.inject.BeanDefinition +import io.micronaut.inject.InstantiatableBeanDefinition +import io.micronaut.inject.writer.BeanDefinitionVisitor +import spock.lang.Specification + +import static io.micronaut.annotation.processing.test.KotlinCompiler.* + +class AbstractClassIntroductionSpec extends Specification { + + void "test that a non-abstract method defined in class is not overridden by the introduction advise"() { + when: + BeanDefinition beanDefinition = buildBeanDefinition('test.AbstractBean' + BeanDefinitionVisitor.PROXY_SUFFIX, ''' +package test + +import io.micronaut.kotlin.processing.aop.introduction.Stub + +@Stub +@jakarta.inject.Singleton +abstract class AbstractBean { + abstract fun isAbstract(): String + + fun nonAbstract(): String = "good" +} +''') + then: + !beanDefinition.isAbstract() + beanDefinition != null + + when: + def context = new DefaultBeanContext() + context.start() + def instance = ((InstantiatableBeanDefinition)beanDefinition).instantiate(context) + + then: + instance.isAbstract() == null + instance.nonAbstract() == 'good' + } + + void "test that a non-abstract method defined in class is and implemented from an interface not overridden by the introduction advise"() { + when: + BeanDefinition beanDefinition = buildBeanDefinition('test.AbstractBean' + BeanDefinitionVisitor.PROXY_SUFFIX, ''' +package test + +import io.micronaut.kotlin.processing.aop.introduction.Stub + +interface Foo { + fun nonAbstract(): String +} + +@Stub +@jakarta.inject.Singleton +abstract class AbstractBean: Foo { + abstract fun isAbstract(): String + + override fun nonAbstract(): String = "good" +} +''') + then: + !beanDefinition.isAbstract() + beanDefinition != null + + when: + def context = new DefaultBeanContext() + context.start() + def instance = ((InstantiatableBeanDefinition)beanDefinition).instantiate(context) + + then: + instance.isAbstract() == null + instance.nonAbstract() == 'good' + } + + void "test that a non-abstract method defined in class is and implemented from an interface not overridden by the introduction advise that also defines advice on the method"() { + when: + BeanDefinition beanDefinition = buildBeanDefinition('test.AbstractBean' + BeanDefinitionVisitor.PROXY_SUFFIX, ''' +package test + +import io.micronaut.kotlin.processing.aop.introduction.Stub + +interface Foo { + @Stub + fun nonAbstract(): String +} + +@Stub +@jakarta.inject.Singleton +abstract class AbstractBean: Foo { + + abstract fun isAbstract(): String + + override fun nonAbstract(): String = "good" +} +''') + then: + !beanDefinition.isAbstract() + beanDefinition != null + + when: + def context = new DefaultBeanContext() + context.start() + def instance = ((InstantiatableBeanDefinition)beanDefinition).instantiate(context) + + then: + instance.isAbstract() == null + instance.nonAbstract() == 'good' + } + + void "test that a non-abstract method defined in class is and implemented from an interface not overridden by the introduction advise that also defines advice on a super interface method"() { + when: + BeanDefinition beanDefinition = buildBeanDefinition('test.AbstractBean' + BeanDefinitionVisitor.PROXY_SUFFIX, ''' +package test + +import io.micronaut.kotlin.processing.aop.introduction.Stub + +interface Bar { + @Stub + fun nonAbstract(): String + + fun another(): String +} + +interface Foo: Bar + +@Stub +@jakarta.inject.Singleton +abstract class AbstractBean: Foo { + abstract fun isAbstract(): String + + override fun nonAbstract(): String = "good" + + override fun another(): String = "good" +} +''') + then: + !beanDefinition.isAbstract() + beanDefinition != null + + when: + def context = new DefaultBeanContext() + context.start() + def instance = ((InstantiatableBeanDefinition)beanDefinition).instantiate(context) + + + then: + instance.isAbstract() == null + instance.nonAbstract() == 'good' + instance.another() == 'good' + } + + void "test that a non-abstract method defined in class is and implemented from an interface not overridden by the introduction advise that also defines advice on the class"() { + when: + BeanDefinition beanDefinition = buildBeanDefinition('test.AbstractBean' + BeanDefinitionVisitor.PROXY_SUFFIX, ''' +package test + +import io.micronaut.kotlin.processing.aop.introduction.Stub + +@Stub +interface Foo { + fun nonAbstract(): String +} + +@Stub +@jakarta.inject.Singleton +abstract class AbstractBean: Foo { + abstract fun isAbstract(): String + + override fun nonAbstract(): String = "good" +} +''') + then: + !beanDefinition.isAbstract() + beanDefinition != null + + when: + def context = new DefaultBeanContext() + context.start() + def instance = ((InstantiatableBeanDefinition)beanDefinition).instantiate(context) + + then: + instance.isAbstract() == null + instance.nonAbstract() == 'good' + } + + void "test that a default method defined in a interface is not implemented by Introduction advice"() { + when: + BeanDefinition beanDefinition = buildBeanDefinition('test.AbstractBean' + BeanDefinitionVisitor.PROXY_SUFFIX, ''' +package test + +import io.micronaut.kotlin.processing.aop.introduction.Stub + +@Stub +interface Foo { + fun nonAbstract(): String + + fun anotherNonAbstract(): String = "good" +} + +@Stub +@jakarta.inject.Singleton +abstract class AbstractBean: Foo { + abstract fun isAbstract(): String + + override fun nonAbstract(): String = "good" +} +''') + then: + !beanDefinition.isAbstract() + beanDefinition != null + + when: + def context = new DefaultBeanContext() + context.start() + def instance = ((InstantiatableBeanDefinition)beanDefinition).instantiate(context) + + then: + instance.isAbstract() == null + instance.nonAbstract() == 'good' + instance.anotherNonAbstract() == 'good' + } + + void "test that a default method overridden from parent interface is not implemented by Introduction advice"() { + when: + BeanDefinition beanDefinition = buildBeanDefinition('test.AbstractBean' + BeanDefinitionVisitor.PROXY_SUFFIX, ''' +package test + +import io.micronaut.kotlin.processing.aop.introduction.Stub + +interface Bar { + fun anotherNonAbstract(): String +} + +interface Foo: Bar { + fun nonAbstract(): String + + override fun anotherNonAbstract(): String = "good" +} + +@Stub +@jakarta.inject.Singleton +abstract class AbstractBean: Foo { + abstract fun isAbstract(): String + + override fun nonAbstract(): String = "good" +} +''') + then: + !beanDefinition.isAbstract() + beanDefinition != null + + when: + def context = new DefaultBeanContext() + context.start() + def instance = ((InstantiatableBeanDefinition)beanDefinition).instantiate(context) + + then: + instance.isAbstract() == null + instance.nonAbstract() == 'good' + instance.anotherNonAbstract() == 'good' + } +} diff --git a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/compile/AnnotatedConstructorArgumentSpec.groovy b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/compile/AnnotatedConstructorArgumentSpec.groovy new file mode 100644 index 00000000000..7c29ef132a5 --- /dev/null +++ b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/compile/AnnotatedConstructorArgumentSpec.groovy @@ -0,0 +1,116 @@ +/* + * Copyright 2017-2019 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.kotlin.processing.aop.compile + +import io.micronaut.aop.InterceptorBinding +import io.micronaut.inject.InstantiatableBeanDefinition +import io.micronaut.kotlin.processing.aop.simple.Mutating +import io.micronaut.context.ApplicationContext +import io.micronaut.core.annotation.AnnotationMetadata +import io.micronaut.core.annotation.AnnotationUtil +import io.micronaut.inject.BeanDefinition +import io.micronaut.inject.writer.BeanDefinitionWriter +import spock.lang.Specification + +import static io.micronaut.annotation.processing.test.KotlinCompiler.* + +class AnnotatedConstructorArgumentSpec extends Specification { + + void "test that constructor arguments propagate annotation metadata"() { + when: + BeanDefinition beanDefinition = buildBeanDefinition('test.$MyBean' + BeanDefinitionWriter.CLASS_SUFFIX + BeanDefinitionWriter.PROXY_SUFFIX, ''' +package test + +import io.micronaut.kotlin.processing.aop.simple.Mutating +import io.micronaut.context.annotation.Value + +@Mutating("someVal") +@jakarta.inject.Singleton +open class MyBean(@Value("\\${foo.bar}") val myValue: String) { + + open fun someMethod(someVal: String): String = "$myValue $someVal" + + internal open fun someMethodPackagePrivateMethod(someVal: String): String = "$myValue $someVal" +} +''') + + then: + !beanDefinition.isAbstract() + beanDefinition != null + beanDefinition.constructor.arguments.size() == 5 + beanDefinition.constructor.arguments[0].name == 'myValue' + beanDefinition.constructor.arguments[1].name == '$beanResolutionContext' + beanDefinition.constructor.arguments[2].name == '$beanContext' + beanDefinition.constructor.arguments[3].name == '$qualifier' + beanDefinition.constructor.arguments[4].name == '$interceptors' + beanDefinition.constructor.arguments[4] + .annotationMetadata + .getAnnotation(AnnotationUtil.ANN_INTERCEPTOR_BINDING_QUALIFIER) + .getAnnotations(AnnotationMetadata.VALUE_MEMBER, InterceptorBinding)[0] + .stringValue().get() == Mutating.name + + when: + def context = ApplicationContext.run('foo.bar':'test') + def instance = ((InstantiatableBeanDefinition)beanDefinition).instantiate(context) + + then: + instance.someMethod("foo") == 'test changed' + instance.someMethodPackagePrivateMethod$main("foo") == 'test foo' + } + + void "test that constructor arguments propagate annotation metadata - method level AOP"() { + when: + BeanDefinition beanDefinition = buildBeanDefinition('test.$MyBean' + BeanDefinitionWriter.CLASS_SUFFIX + BeanDefinitionWriter.PROXY_SUFFIX, ''' +package test + +import io.micronaut.kotlin.processing.aop.simple.Mutating +import io.micronaut.context.annotation.Value + +@jakarta.inject.Singleton +open class MyBean(@Value("\\${foo.bar}") val myValue: String) { + + @Mutating("someVal") + open fun someMethod(someVal: String): String = "$myValue $someVal" + + @Mutating("someVal") + internal open fun someMethodPackagePrivateMethod(someVal: String): String = "$myValue $someVal" +} +''') + then: + !beanDefinition.isAbstract() + beanDefinition != null + beanDefinition.injectedFields.size() == 0 + beanDefinition.constructor.arguments.size() == 5 + beanDefinition.constructor.arguments[0].name == 'myValue' + beanDefinition.constructor.arguments[1].name == '$beanResolutionContext' + beanDefinition.constructor.arguments[2].name == '$beanContext' + beanDefinition.constructor.arguments[3].name == '$qualifier' + beanDefinition.constructor.arguments[4].name == '$interceptors' + beanDefinition.constructor.arguments[4] + .annotationMetadata + .getAnnotation(AnnotationUtil.ANN_INTERCEPTOR_BINDING_QUALIFIER) + .getAnnotations(AnnotationMetadata.VALUE_MEMBER, InterceptorBinding)[0] + .stringValue().get() == Mutating.name + + when: + def context = ApplicationContext.run('foo.bar':'test') + def instance = ((InstantiatableBeanDefinition)beanDefinition).instantiate(context) + + then: + instance.someMethod("foo") == 'test changed' + instance.someMethodPackagePrivateMethod$main("foo") == 'test changed' + } +} diff --git a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/compile/AroundCompileSpec.groovy b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/compile/AroundCompileSpec.groovy new file mode 100644 index 00000000000..9e25c93d5df --- /dev/null +++ b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/compile/AroundCompileSpec.groovy @@ -0,0 +1,970 @@ +package io.micronaut.kotlin.processing.aop.compile + +import io.micronaut.aop.Intercepted +import io.micronaut.aop.InterceptorBinding +import io.micronaut.aop.InterceptorKind +import io.micronaut.kotlin.processing.aop.simple.Mutating +import io.micronaut.kotlin.processing.aop.simple.TestBinding +import io.micronaut.context.ApplicationContext +import io.micronaut.inject.AdvisedBeanType +import io.micronaut.inject.BeanDefinition +import io.micronaut.inject.BeanDefinitionReference +import io.micronaut.inject.writer.BeanDefinitionWriter +import spock.lang.Issue +import spock.lang.PendingFeature +import spock.lang.Specification + +import static io.micronaut.annotation.processing.test.KotlinCompiler.* + +class AroundCompileSpec extends Specification { + + void 'test stereotype method level interceptor matching'() { + given: + ApplicationContext context = buildContext(''' +package annbinding2 + +import io.micronaut.aop.Around +import io.micronaut.aop.InterceptorBean +import io.micronaut.aop.Interceptor +import io.micronaut.aop.InvocationContext +import jakarta.inject.Singleton + +@Singleton +open class MyBean { + val name : String = "test" + + @TestAnn2 + open fun test() { + + } + +} + +@Retention +@Target(AnnotationTarget.FUNCTION, AnnotationTarget.ANNOTATION_CLASS) +@Around +annotation class TestAnn + +@Retention +@Target(AnnotationTarget.FUNCTION) +@TestAnn +annotation class TestAnn2 + +@InterceptorBean(TestAnn::class) +class TestInterceptor: Interceptor { + var invoked = false + + override fun intercept(context: InvocationContext): Any? { + invoked = true + return context.proceed() + } +} + +''') + def instance = getBean(context, 'annbinding2.MyBean') + def interceptor = getBean(context, 'annbinding2.TestInterceptor') + + when: + instance.test() + + then:"the interceptor was invoked" + instance instanceof Intercepted + interceptor.invoked + + cleanup: + context.close() + } + + void 'test stereotype type level interceptor matching'() { + given: + ApplicationContext context = buildContext(''' +package annbinding2 + +import io.micronaut.aop.Around +import io.micronaut.aop.InterceptorBean +import io.micronaut.aop.Interceptor +import io.micronaut.aop.InvocationContext +import jakarta.inject.Singleton + +@Singleton +@TestAnn2 +open class MyBean { + val name : String = "test" + open fun test() { + + } + +} + +@Retention +@Target(AnnotationTarget.FUNCTION, AnnotationTarget.ANNOTATION_CLASS) +@Around +annotation class TestAnn + +@Retention +@Target(AnnotationTarget.CLASS) +@TestAnn +annotation class TestAnn2 + +@InterceptorBean(TestAnn::class) +class TestInterceptor: Interceptor { + var invoked = false + + override fun intercept(context: InvocationContext): Any? { + invoked = true + return context.proceed() + } +} + +''') + def instance = getBean(context, 'annbinding2.MyBean') + def interceptor = getBean(context, 'annbinding2.TestInterceptor') + + when: + instance.test() + + then:"the interceptor was invoked" + instance instanceof Intercepted + interceptor.invoked + + cleanup: + context.close() + } + + void 'test apply interceptor binder with annotation mapper'() { + given: + ApplicationContext context = buildContext(''' +package mapperbinding + +import io.micronaut.aop.InterceptorBean +import io.micronaut.aop.Interceptor +import io.micronaut.aop.InvocationContext +import jakarta.inject.Singleton + +@Singleton +open class MyBean { + + @TestAnn + open fun test() { + } +} + +@Retention +@Target(AnnotationTarget.FUNCTION, AnnotationTarget.ANNOTATION_CLASS) +annotation class TestAnn + + +@InterceptorBean(TestAnn::class) +class TestInterceptor: Interceptor { + var invoked = false + + override fun intercept(context: InvocationContext): Any? { + invoked = true + return context.proceed() + } +} + +''') + def instance = getBean(context,'mapperbinding.MyBean') + def interceptor = getBean(context,'mapperbinding.TestInterceptor') + + when: + instance.test() + + then:"the interceptor was invoked" + instance instanceof Intercepted + interceptor.invoked + } + + void 'test apply interceptor binder with annotation mapper - plus members'() { + given: + ApplicationContext context = buildContext(''' +package mapperbindingmembers + +import io.micronaut.aop.InterceptorBean +import io.micronaut.aop.Interceptor +import io.micronaut.aop.InvocationContext +import jakarta.inject.Singleton + +@Singleton +open class MyBean { + @TestAnn(num=1) + open fun test() { + } +} + +@Retention +@Target(AnnotationTarget.ANNOTATION_CLASS) +annotation class MyInterceptorBinding + +@Retention +@Target(AnnotationTarget.FUNCTION, AnnotationTarget.CLASS) +@MyInterceptorBinding +annotation class TestAnn(val num: Int) + +@Singleton +@TestAnn(num=1) +class TestInterceptor: Interceptor { + var invoked = false + + override fun intercept(context: InvocationContext): Any? { + invoked = true + return context.proceed() + } +} + +@Singleton +@TestAnn(num=2) +class TestInterceptor2: Interceptor { + var invoked = false + + override fun intercept(context: InvocationContext): Any? { + invoked = true + return context.proceed() + } +} + +''') + def instance = getBean(context, 'mapperbindingmembers.MyBean') + def interceptor = getBean(context, 'mapperbindingmembers.TestInterceptor') + def interceptor2 = getBean(context, 'mapperbindingmembers.TestInterceptor2') + + when: + instance.test() + + then:"the interceptor was invoked" + instance instanceof Intercepted + interceptor.invoked + !interceptor2.invoked + } + + void 'test method level interceptor matching'() { + given: + ApplicationContext context = buildContext(''' +package annbinding2 + +import io.micronaut.aop.* +import jakarta.inject.Singleton + +@Singleton +open class MyBean { + + @TestAnn + open fun test() { + + } + + @TestAnn2 + open fun test2() { + + } +} + +@Retention +@Target(AnnotationTarget.FUNCTION) +@Around +annotation class TestAnn + +@Retention +@Target(AnnotationTarget.FUNCTION) +@Around +annotation class TestAnn2 + +@InterceptorBean(TestAnn::class) +class TestInterceptor: Interceptor { + var invoked = false + + override fun intercept(context: InvocationContext): Any? { + invoked = true + return context.proceed() + } +} + +@InterceptorBean(TestAnn2::class) +class AnotherInterceptor: Interceptor { + var invoked = false + + override fun intercept(context: InvocationContext): Any? { + invoked = true + return context.proceed() + } +} +''') + def instance = getBean(context, 'annbinding2.MyBean') + def interceptor = getBean(context, 'annbinding2.TestInterceptor') + def anotherInterceptor = getBean(context, 'annbinding2.AnotherInterceptor') + + when: + instance.test() + + then:"the interceptor was invoked" + instance instanceof Intercepted + interceptor.invoked + !anotherInterceptor.invoked + + when: + instance.test2() + + then: + anotherInterceptor.invoked + + cleanup: + context.close() + } + + void 'test annotation with just interceptor binding'() { + given: + ApplicationContext context = buildContext(''' +package annbinding1 + +import io.micronaut.aop.* +import jakarta.inject.Singleton + +@Singleton +@TestAnn +open class MyBean { + + open fun test() { + } +} + +@Retention +@Target(AnnotationTarget.CLASS) +@InterceptorBinding +annotation class TestAnn + +@Singleton +@InterceptorBinding(TestAnn::class) +class TestInterceptor: Interceptor { + var invoked = false + + override fun intercept(context: InvocationContext): Any? { + invoked = true + return context.proceed() + } +} + +@Singleton +class AnotherInterceptor: Interceptor { + var invoked = false + + override fun intercept(context: InvocationContext): Any? { + invoked = true + return context.proceed() + } +} +''') + def instance = getBean(context, 'annbinding1.MyBean') + def interceptor = getBean(context, 'annbinding1.TestInterceptor') + def anotherInterceptor = getBean(context, 'annbinding1.AnotherInterceptor') + instance.test() + + expect:"the interceptor was invoked" + instance instanceof Intercepted + interceptor.invoked + !anotherInterceptor.invoked + + cleanup: + context.close() + } + + @PendingFeature(reason = "annotation defaults") + void 'test multiple interceptor binding'() { + given: + ApplicationContext context = buildContext(''' +package multiplebinding + +import io.micronaut.aop.* +import io.micronaut.context.annotation.NonBinding +import jakarta.inject.Singleton + +@Retention +@InterceptorBinding(kind = InterceptorKind.AROUND) +annotation class Deadly + +@Retention +@InterceptorBinding(kind = InterceptorKind.AROUND) +annotation class Fast + +@Retention +@InterceptorBinding(kind = InterceptorKind.AROUND) +annotation class Slow + +interface Missile { + fun fire() +} + +@Fast +@Deadly +@Singleton +open class FastAndDeadlyMissile: Missile { + override fun fire() { + } +} + +@Deadly +@Singleton +open class AnyDeadlyMissile: Missile { + override fun fire() { + } +} + +@Singleton +open class GuidedMissile: Missile { + @Slow + @Deadly + open fun lockAndFire() { + } + + @Fast + @Deadly + override fun fire() { + } +} + +@Slow +@Deadly +@Singleton +open class SlowMissile: Missile { + override fun fire() { + } +} + +@Fast +@Deadly +@Singleton +class MissileInterceptor: MethodInterceptor { + var intercepted = false + + override fun intercept(context: MethodInvocationContext): Any? { + intercepted = true + return context.proceed() + } +} + +@Slow +@Deadly +@Singleton +class LockInterceptor: MethodInterceptor { + var intercepted = false + + override fun intercept(context: MethodInvocationContext): Any? { + intercepted = true + return context.proceed() + } +} +''') + def missileInterceptor = getBean(context, 'multiplebinding.MissileInterceptor') + def lockInterceptor = getBean(context, 'multiplebinding.LockInterceptor') + + when: + missileInterceptor.intercepted = false + lockInterceptor.intercepted = false + def guidedMissile = getBean(context, 'multiplebinding.GuidedMissile'); + guidedMissile.fire() + + then: + missileInterceptor.intercepted + !lockInterceptor.intercepted + + when: + missileInterceptor.intercepted = false + lockInterceptor.intercepted = false + def fastAndDeadlyMissile = getBean(context, 'multiplebinding.FastAndDeadlyMissile'); + fastAndDeadlyMissile.fire() + + then: + missileInterceptor.intercepted + !lockInterceptor.intercepted + + when: + missileInterceptor.intercepted = false + lockInterceptor.intercepted = false + def slowMissile = getBean(context, 'multiplebinding.SlowMissile'); + slowMissile.fire() + + then: + !missileInterceptor.intercepted + lockInterceptor.intercepted + + when: + missileInterceptor.intercepted = false + lockInterceptor.intercepted = false + def anyMissile = getBean(context, 'multiplebinding.AnyDeadlyMissile'); + anyMissile.fire() + + then: + missileInterceptor.intercepted + lockInterceptor.intercepted + + cleanup: + context.close() + } + + void 'test annotation with just interceptor binding - member binding'() { + given: + ApplicationContext context = buildContext(''' +package memberbinding + +import io.micronaut.aop.* +import io.micronaut.context.annotation.NonBinding +import jakarta.inject.Singleton + +@Singleton +@TestAnn(num=1, debug = false) +open class MyBean { + open fun test() { + } + + @TestAnn(num=2) // overrides binding on type + open fun test2() { + + } +} + +@Retention +@Target(AnnotationTarget.FUNCTION, AnnotationTarget.CLASS) +@InterceptorBinding(bindMembers = true) +annotation class TestAnn(val num: Int, @get:NonBinding val debug: Boolean = false) + +@InterceptorBean(TestAnn::class) +@TestAnn(num = 1, debug = true) +class TestInterceptor: Interceptor { + var invoked = false + + override fun intercept(context: InvocationContext): Any? { + invoked = true + return context.proceed() + } +} + +@InterceptorBean(TestAnn::class) +@TestAnn(num = 2) +class AnotherInterceptor: Interceptor { + var invoked = false + + override fun intercept(context: InvocationContext): Any? { + invoked = true + return context.proceed() + } +} +''') + def instance = getBean(context, 'memberbinding.MyBean') + def interceptor = getBean(context, 'memberbinding.TestInterceptor') + def anotherInterceptor = getBean(context, 'memberbinding.AnotherInterceptor') + + when: + instance.test() + + then:"the interceptor was invoked" + instance instanceof Intercepted + interceptor.invoked + !anotherInterceptor.invoked + + when: + interceptor.invoked = false + instance.test2() + + then: + !interceptor.invoked + anotherInterceptor.invoked + + cleanup: + context.close() + } + + + void 'test annotation with just around'() { + given: + ApplicationContext context = buildContext(''' +package justaround + +import io.micronaut.aop.* +import jakarta.inject.Singleton + +@Singleton +@TestAnn +open class MyBean { + open fun test() { + } +} + +@Retention +@Target(AnnotationTarget.CLASS) +@Around +annotation class TestAnn + +@InterceptorBean(TestAnn::class) +class TestInterceptor: Interceptor { + var invoked = false + + override fun intercept(context: InvocationContext): Any? { + invoked = true + return context.proceed() + } +} + +@Singleton +class AnotherInterceptor: Interceptor { + var invoked = false + + override fun intercept(context: InvocationContext): Any? { + invoked = true + return context.proceed() + } +} +''') + def instance = getBean(context, 'justaround.MyBean') + def interceptor = getBean(context, 'justaround.TestInterceptor') + def anotherInterceptor = getBean(context, 'justaround.AnotherInterceptor') + instance.test() + + expect:"the interceptor was invoked" + instance instanceof Intercepted + interceptor.invoked + !anotherInterceptor.invoked + + cleanup: + context.close() + } + + @Issue('https://github.com/micronaut-projects/micronaut-core/issues/5522') + void 'test Around annotation on private method fails'() { + when: + buildContext(''' +package around.priv.method + +import io.micronaut.aop.* +import jakarta.inject.Singleton + +@Singleton +open class MyBean { + @TestAnn + private fun test() { + } +} + +@Retention +@Target(AnnotationTarget.FUNCTION) +@Around +annotation class TestAnn +''') + + then: + Throwable t = thrown() + t.message.contains 'Method defines AOP advice but is declared final' + } + + void 'test byte[] return compile'() { + given: + ApplicationContext context = buildContext(''' +package test + +import io.micronaut.kotlin.processing.aop.simple.Mutating + +@jakarta.inject.Singleton +@Mutating("someVal") +open class MyBean { + + open fun test(someVal: ByteArray): ByteArray? { + return null + } +} +''') + def instance = getBean(context, 'test.MyBean') + + expect: + instance != null + + cleanup: + context.close() + } + + void 'compile simple AOP advice'() { + given: + BeanDefinition beanDefinition = buildInterceptedBeanDefinition('test.MyBean', ''' +package test + +import io.micronaut.kotlin.processing.aop.simple.* + +@jakarta.inject.Singleton +@Mutating("someVal") +@TestBinding +open class MyBean { + open fun test() {} +} +''') + + BeanDefinitionReference ref = buildInterceptedBeanDefinitionReference('test.MyBean', ''' +package test + +import io.micronaut.kotlin.processing.aop.simple.* + +@jakarta.inject.Singleton +@Mutating("someVal") +@TestBinding +open class MyBean { + open fun test() {} +} +''') + + def annotationMetadata = beanDefinition?.annotationMetadata + def values = annotationMetadata.getAnnotationValuesByType(InterceptorBinding) + + expect: + values.size() == 2 + values[0].stringValue().get() == Mutating.name + values[0].enumValue("kind", InterceptorKind).get() == InterceptorKind.AROUND + values[0].classValue("interceptorType").isPresent() + values[1].stringValue().get() == TestBinding.name + !values[1].classValue("interceptorType").isPresent() + values[1].enumValue("kind", InterceptorKind).get() == InterceptorKind.AROUND + beanDefinition != null + beanDefinition instanceof AdvisedBeanType + beanDefinition.interceptedType.name == 'test.MyBean' + ref in AdvisedBeanType + ref.interceptedType.name == 'test.MyBean' + } + + void 'test multiple annotations on a single method'() { + given: + ApplicationContext context = buildContext(''' +package annbinding2 + +import io.micronaut.aop.* +import jakarta.inject.Singleton + +@Singleton +open class MyBean { + + @TestAnn + @TestAnn2 + open fun test() { + } +} + +@Retention +@Target(AnnotationTarget.FUNCTION) +@Around +annotation class TestAnn + +@Retention +@Target(AnnotationTarget.FUNCTION) +@Around +annotation class TestAnn2 + +@InterceptorBean(TestAnn::class) +class TestInterceptor: Interceptor { + var invoked = false + + override fun intercept(context: InvocationContext): Any? { + invoked = true + return context.proceed() + } +} + +@InterceptorBean(TestAnn2::class) +class AnotherInterceptor: Interceptor { + var invoked = false + + override fun intercept(context: InvocationContext): Any? { + invoked = true + return context.proceed() + } +} +''') + def instance = getBean(context, 'annbinding2.MyBean') + def interceptor = getBean(context, 'annbinding2.TestInterceptor') + def anotherInterceptor = getBean(context, 'annbinding2.AnotherInterceptor') + + when: + instance.test() + + then:"the interceptor was invoked" + instance instanceof Intercepted + interceptor.invoked + anotherInterceptor.invoked + + cleanup: + context.close() + } + + void 'test multiple annotations on an interceptor and method'() { + given: + ApplicationContext context = buildContext(''' +package annbinding2 + +import io.micronaut.aop.* +import jakarta.inject.Singleton + +@Singleton +open class MyBean { + + @TestAnn + @TestAnn2 + open fun test() { + + } +} + +@Retention +@Target(AnnotationTarget.FUNCTION) +@Around +annotation class TestAnn + +@Retention +@Target(AnnotationTarget.FUNCTION) +@Around +annotation class TestAnn2 + +@InterceptorBean(TestAnn::class, TestAnn2::class) +class TestInterceptor: Interceptor { + var count = 0 + + override fun intercept(context: InvocationContext): Any? { + count++ + return context.proceed() + } +} +''') + def instance = getBean(context, 'annbinding2.MyBean') + def interceptor = getBean(context, 'annbinding2.TestInterceptor') + + when: + instance.test() + + then: + interceptor.count == 1 + + cleanup: + context.close() + } + + void 'test multiple annotations on an interceptor'() { + given: + ApplicationContext context = buildContext(''' +package annbinding2 + +import io.micronaut.aop.* +import jakarta.inject.Singleton + +@Singleton +open class MyBean { + + @TestAnn + open fun test() { + } + + @TestAnn2 + open fun test2() { + } + + @TestAnn + @TestAnn2 + open fun testBoth() { + } +} + +@Retention +@Target(AnnotationTarget.FUNCTION) +@Around +annotation class TestAnn + +@Retention +@Target(AnnotationTarget.FUNCTION) +@Around +annotation class TestAnn2 + +@InterceptorBean(TestAnn::class, TestAnn2::class) +class TestInterceptor: Interceptor { + var count = 0 + + override fun intercept(context: InvocationContext): Any? { + count++ + return context.proceed() + } +} +''') + def instance = getBean(context, 'annbinding2.MyBean') + def interceptor = getBean(context, 'annbinding2.TestInterceptor') + + when: + instance.test() + + then: + interceptor.count == 0 + + when: + instance.test2() + + then: + interceptor.count == 0 + + when: + instance.testBoth() + + then: + interceptor.count == 1 + + cleanup: + context.close() + } + + void "test validated on class with generics"() { + when: + BeanDefinition beanDefinition = buildBeanDefinition('test.$BaseEntityService' + BeanDefinitionWriter.CLASS_SUFFIX + BeanDefinitionWriter.PROXY_SUFFIX, """ +package test + +@io.micronaut.validation.Validated +open class BaseEntityService: BaseService() + +class BaseEntity + +abstract class BaseService: IBeanValidator { + override fun isValid(entity: T) = true +} + +interface IBeanValidator { + fun isValid(entity: T): Boolean +} +""") + + then: + noExceptionThrown() + beanDefinition != null + beanDefinition.getTypeArguments('test.BaseService')[0].type.name == 'test.BaseEntity' + } + + void "test aop with generics"() { + ApplicationContext context = buildContext( ''' +package test + +import io.micronaut.kotlin.processing.aop.simple.* +import jakarta.inject.Singleton + +@Singleton +open class Test { + + @Mutating("name") + open fun testGenericsWithExtends(name: T, age: Int): T { + return "Name is $name" as T + } + + @Mutating("name") + open fun testListWithWildCardIn(name: T, p2: CovariantClass): CovariantClass { + return CovariantClass(name.toString()) + } + + @Mutating("name") + open fun testListWithWildCardOut(name: T, p2: CovariantClass): CovariantClass { + return CovariantClass(name.toString()) + } +} +''', true) + def instance = getBean(context, 'test.Test') + + expect: + instance.testGenericsWithExtends("abc", 0) == "Name is changed" + } + +} diff --git a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/compile/AroundConstructCompileSpec.groovy b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/compile/AroundConstructCompileSpec.groovy new file mode 100644 index 00000000000..a28ca3cb6f6 --- /dev/null +++ b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/compile/AroundConstructCompileSpec.groovy @@ -0,0 +1,746 @@ +package io.micronaut.kotlin.processing.aop.compile + +import io.micronaut.aop.Intercepted +import io.micronaut.context.ApplicationContext +import io.micronaut.context.env.Environment +import spock.lang.PendingFeature +import spock.lang.Specification +import spock.lang.Unroll + +import static io.micronaut.annotation.processing.test.KotlinCompiler.* + +class AroundConstructCompileSpec extends Specification { + + void 'test around construct with annotation mapper - plus members'() { + given: + ApplicationContext context = buildContext(''' +package aroundconstructmapperbindingmembers + +import io.micronaut.aop.* +import jakarta.inject.Singleton + +@Singleton +@TestAnn2 +class MyBean @TestAnn(num=1) constructor() { + +} + +@Retention +@Target(AnnotationTarget.CONSTRUCTOR, AnnotationTarget.ANNOTATION_CLASS) +annotation class MyInterceptorBinding + +@Retention +@Target(AnnotationTarget.CONSTRUCTOR, AnnotationTarget.CLASS) +@MyInterceptorBinding +annotation class TestAnn(val num: Int) + +@Retention +@Target(AnnotationTarget.CONSTRUCTOR, AnnotationTarget.CLASS) +@MyInterceptorBinding +annotation class TestAnn2 + +@Singleton +@TestAnn(num=1) +class TestInterceptor: ConstructorInterceptor { + var invoked = false + + override fun intercept(context: ConstructorInvocationContext): Any { + invoked = true + return context.proceed() + } +} + +@Singleton +@TestAnn(num=2) +class TestInterceptor2: ConstructorInterceptor { + var invoked = false + + override fun intercept(context: ConstructorInvocationContext): Any { + invoked = true + return context.proceed() + } +} +''') + + + when: + def interceptor = getBean(context, 'aroundconstructmapperbindingmembers.TestInterceptor') + def interceptor2 = getBean(context, 'aroundconstructmapperbindingmembers.TestInterceptor2') + + then: + !interceptor.invoked + !interceptor2.invoked + + when: + def instance = getBean(context, 'aroundconstructmapperbindingmembers.MyBean') + + then:"the interceptor was invoked" + interceptor.invoked + !interceptor2.invoked + } + + void 'test around construct on type and constructor with proxy target + bind members'() { + given: + ApplicationContext context = buildContext(""" +package ctorbinding + +import io.micronaut.aop.* +import jakarta.inject.Singleton + +@FooClassBinding +@Singleton +open class Foo @FooCtorBinding constructor() { +} + +@Target(AnnotationTarget.CLASS, AnnotationTarget.CONSTRUCTOR) +@Retention +@MustBeDocumented +@InterceptorBinding(kind = InterceptorKind.AROUND, bindMembers = true) +@InterceptorBinding(kind = InterceptorKind.AROUND_CONSTRUCT, bindMembers = true) +annotation class FooCtorBinding + +@Target(AnnotationTarget.CLASS) +@Retention +@MustBeDocumented +@InterceptorBinding(kind = InterceptorKind.AROUND, bindMembers = true) +@InterceptorBinding(kind = InterceptorKind.AROUND_CONSTRUCT, bindMembers = true) +@Around(proxyTarget = true) +annotation class FooClassBinding + +@Singleton +@FooClassBinding +class Interceptor1: ConstructorInterceptor { + var intercepted = false + + override fun intercept(context: ConstructorInvocationContext): Any { + intercepted = true + return context.proceed() + } +} + +@Singleton +@FooCtorBinding +class Interceptor2: ConstructorInterceptor { + var intercepted = false + + override fun intercept(context: ConstructorInvocationContext): Any { + intercepted = true + return context.proceed() + } +} +""") + when: + def i1 = getBean(context, 'ctorbinding.Interceptor1') + def i2 = getBean(context, 'ctorbinding.Interceptor2') + + then: + !i1.intercepted + !i2.intercepted + + when: + def bean = getBean(context, 'ctorbinding.Foo') + + then: + i1.intercepted + i2.intercepted + + cleanup: + context.close() + } + + void 'test around construct on type and constructor with proxy target'() { + given: + ApplicationContext context = buildContext(""" +package ctorbinding + +import io.micronaut.aop.* +import jakarta.inject.Singleton + +@FooClassBinding +@Singleton +open class Foo @FooCtorBinding constructor() { +} + +@Target(AnnotationTarget.CLASS, AnnotationTarget.CONSTRUCTOR) +@Retention +@MustBeDocumented +@InterceptorBinding(kind = InterceptorKind.AROUND) +@InterceptorBinding(kind = InterceptorKind.AROUND_CONSTRUCT) +annotation class FooCtorBinding + +@Target(AnnotationTarget.CLASS) +@Retention +@MustBeDocumented +@InterceptorBinding(kind = InterceptorKind.AROUND) +@InterceptorBinding(kind = InterceptorKind.AROUND_CONSTRUCT) +@Around(proxyTarget = true) +annotation class FooClassBinding + +@Singleton +@FooClassBinding +class Interceptor1: ConstructorInterceptor { + var intercepted = false + + override fun intercept(context: ConstructorInvocationContext): Any { + intercepted = true + return context.proceed() + } +} + +@Singleton +@FooCtorBinding +class Interceptor2: ConstructorInterceptor { + var intercepted = false + + override fun intercept(context: ConstructorInvocationContext): Any { + intercepted = true + return context.proceed() + } +} +""") + when: + def i1 = getBean(context, 'ctorbinding.Interceptor1') + def i2 = getBean(context, 'ctorbinding.Interceptor2') + + then: + !i1.intercepted + !i2.intercepted + + when: + def bean = getBean(context, 'ctorbinding.Foo') + + then: + i1.intercepted + i2.intercepted + + cleanup: + context.close() + } + + void 'test around construct on type and constructor'() { + given: + ApplicationContext context = buildContext(""" +package ctorbinding + +import io.micronaut.aop.* +import jakarta.inject.Singleton + +@FooClassBinding +@Singleton +open class Foo @FooCtorBinding constructor() { +} + +@Target(AnnotationTarget.CLASS, AnnotationTarget.CONSTRUCTOR) +@Retention +@MustBeDocumented +@InterceptorBinding(kind = InterceptorKind.AROUND) +@InterceptorBinding(kind = InterceptorKind.AROUND_CONSTRUCT) +annotation class FooCtorBinding + +@Target(AnnotationTarget.CLASS) +@Retention +@MustBeDocumented +@InterceptorBinding(kind = InterceptorKind.AROUND) +@InterceptorBinding(kind = InterceptorKind.AROUND_CONSTRUCT) +annotation class FooClassBinding + +@Singleton +@FooClassBinding +class Interceptor1: ConstructorInterceptor { + var intercepted = false + + override fun intercept(context: ConstructorInvocationContext): Any { + intercepted = true + return context.proceed() + } +} + +@Singleton +@FooCtorBinding +class Interceptor2: ConstructorInterceptor { + var intercepted = false + + override fun intercept(context: ConstructorInvocationContext): Any { + intercepted = true + return context.proceed() + } +} +""") + when: + def i1 = getBean(context, 'ctorbinding.Interceptor1') + def i2 = getBean(context, 'ctorbinding.Interceptor2') + + then: + !i1.intercepted + !i2.intercepted + + when: + def bean = getBean(context, 'ctorbinding.Foo') + + then: + i1.intercepted + i2.intercepted + + cleanup: + context.close() + } + + @Unroll + void 'test around construct with around interception - proxyTarget = #proxyTarget'() { + given: + ApplicationContext context = buildContext(""" +package annbinding1 + +import io.micronaut.aop.* +import jakarta.inject.Singleton + +@Singleton +@TestAnn +open class MyBean(private val env: io.micronaut.context.env.Environment) { + + open fun test() { + } +} + +@io.micronaut.context.annotation.Factory +open class MyFactory { + + @TestAnn + @Singleton + open fun test(env: io.micronaut.context.env.Environment): MyOtherBean { + return MyOtherBean() + } +} + +open class MyOtherBean + +@Retention +@Target(AnnotationTarget.FUNCTION, AnnotationTarget.CLASS) +@Around(proxyTarget=$proxyTarget) +@AroundConstruct +annotation class TestAnn + +@Singleton +@InterceptorBean(TestAnn::class) +class TestConstructInterceptor: ConstructorInterceptor { + var invoked = false + var parameters: Array? = null + + override fun intercept(context: ConstructorInvocationContext): Any { + invoked = true + parameters = context.parameterValues + return context.proceed() + } +} + +@Singleton +@InterceptorBean(TestAnn::class) +class TypeSpecificConstructInterceptor: ConstructorInterceptor { + var invoked = false + var parameters: Array? = null + + override fun intercept(context: ConstructorInvocationContext): MyBean { + invoked = true + parameters = context.parameterValues + return context.proceed() + } +} + +@Singleton +@InterceptorBinding(TestAnn::class) +class TestInterceptor: MethodInterceptor { + var invoked = false + + override fun intercept(context: MethodInvocationContext): Any? { + invoked = true + return context.proceed() + } +} + +@Singleton +class AnotherInterceptor: Interceptor { + var invoked = false + + override fun intercept(context: InvocationContext): Any? { + invoked = true + return context.proceed() + } +} +""") + when: + def interceptor = getBean(context, 'annbinding1.TestInterceptor') + def constructorInterceptor = getBean(context, 'annbinding1.TestConstructInterceptor') + def typeSpecificInterceptor = getBean(context, 'annbinding1.TypeSpecificConstructInterceptor') + def anotherInterceptor = getBean(context, 'annbinding1.AnotherInterceptor') + + then: + !constructorInterceptor.invoked + !interceptor.invoked + !anotherInterceptor.invoked + + when:"A bean that features constructor injection is instantiated" + def instance = getBean(context, 'annbinding1.MyBean') + + then:"The constructor interceptor is invoked" + constructorInterceptor.invoked + typeSpecificInterceptor.invoked + constructorInterceptor.parameters.size() == 1 + + and:"Other non-constructor interceptors are not invoked" + !interceptor.invoked + !anotherInterceptor.invoked + + when:"A method with interception is invoked" + constructorInterceptor.invoked = false + typeSpecificInterceptor.invoked = false + instance.test() + + then:"the methods interceptor are invoked" + instance instanceof Intercepted + interceptor.invoked + !anotherInterceptor.invoked + + and:"The constructor interceptor is not" + !constructorInterceptor.invoked + !typeSpecificInterceptor.invoked + + when:"A bean that is created from a factory is instantiated" + constructorInterceptor.invoked = false + interceptor.invoked = false + def factoryCreatedInstance = getBean(context, 'annbinding1.MyOtherBean') + + then:"Constructor interceptors are invoked for the created instance" + constructorInterceptor.invoked + !typeSpecificInterceptor.invoked + constructorInterceptor.parameters.size() == 1 + + and:"Other interceptors are not" + !interceptor.invoked + !anotherInterceptor.invoked + + cleanup: + context.close() + + where: + proxyTarget << [true, false] + } + + void 'test around construct without around interception'() { + given: + ApplicationContext context = buildContext(""" +package annbinding1 + +import io.micronaut.aop.* +import jakarta.inject.Singleton + +@Singleton +@TestAnn +open class MyBean(private val env: io.micronaut.context.env.Environment) { + + open fun test() { + } +} + +@io.micronaut.context.annotation.Factory +open class MyFactory { + + @TestAnn + @Singleton + open fun test(env: io.micronaut.context.env.Environment): MyOtherBean { + return MyOtherBean() + } +} + +open class MyOtherBean + +@Retention +@Target(AnnotationTarget.FUNCTION, AnnotationTarget.CLASS) +@AroundConstruct +annotation class TestAnn + +@Singleton +@InterceptorBean(TestAnn::class) +class TestConstructInterceptor: ConstructorInterceptor { + var invoked = false + var parameters: Array? = null + + override fun intercept(context: ConstructorInvocationContext): Any { + invoked = true + parameters = context.parameterValues + return context.proceed() + } +} + +@Singleton +@InterceptorBinding(TestAnn::class) +class TestInterceptor: MethodInterceptor { + var invoked = false + + override fun intercept(context: MethodInvocationContext): Any? { + invoked = true + return context.proceed() + } +} + +@Singleton +class AnotherInterceptor: Interceptor { + var invoked = false + + override fun intercept(context: InvocationContext): Any? { + invoked = true + return context.proceed() + } +} +""") + when: + def interceptor = getBean(context, 'annbinding1.TestInterceptor') + def constructorInterceptor = getBean(context, 'annbinding1.TestConstructInterceptor') + def anotherInterceptor = getBean(context, 'annbinding1.AnotherInterceptor') + + then: + !constructorInterceptor.invoked + !interceptor.invoked + !anotherInterceptor.invoked + + when:"A bean that features constructor injection is instantiated" + def instance = getBean(context, 'annbinding1.MyBean') + + then:"The constructor interceptor is invoked" + !(instance instanceof Intercepted) + constructorInterceptor.invoked + constructorInterceptor.parameters.size() == 1 + + and:"Other non-constructor interceptors are not invoked" + !interceptor.invoked + !anotherInterceptor.invoked + + + when:"A method with interception is invoked" + constructorInterceptor.invoked = false + instance.test() + + then:"the methods interceptor are invoked" + !interceptor.invoked + !anotherInterceptor.invoked + + and:"The constructor interceptor is not" + !constructorInterceptor.invoked + + when:"A bean that is created from a factory is instantiated" + constructorInterceptor.invoked = false + interceptor.invoked = false + def factoryCreatedInstance = getBean(context, 'annbinding1.MyOtherBean') + + then:"Constructor interceptors are invoked for the created instance" + !(factoryCreatedInstance instanceof Intercepted) + constructorInterceptor.invoked + constructorInterceptor.parameters.size() == 1 + + and:"Other interceptors are not" + !interceptor.invoked + !anotherInterceptor.invoked + + cleanup: + context.close() + } + + void 'test around construct declared on constructor only'() { + given: + ApplicationContext context = buildContext(""" +package annbinding1 + +import io.micronaut.aop.* +import jakarta.inject.Singleton + +@Singleton +class MyBean @TestAnn constructor(env: io.micronaut.context.env.Environment) { + + fun test() { + } +} + +@Retention +@Target(AnnotationTarget.CONSTRUCTOR) +@AroundConstruct +@Around +annotation class TestAnn + +@Singleton +@InterceptorBean(TestAnn::class) +class TestConstructInterceptor: ConstructorInterceptor { + var invoked = false + var parameters: Array? = null + + override fun intercept(context: ConstructorInvocationContext): Any { + invoked = true + parameters = context.parameterValues + return context.proceed() + } +} + +""") + when: + def constructorInterceptor = getBean(context, 'annbinding1.TestConstructInterceptor') + + then: + !constructorInterceptor.invoked + + when:"A bean that features constructor injection is instantiated" + def instance = getBean(context, 'annbinding1.MyBean') + + then:"The constructor interceptor is invoked" + !(instance instanceof Intercepted) + constructorInterceptor.invoked + constructorInterceptor.parameters.size() == 1 + + cleanup: + context.close() + } + + void 'test around construct without around interception - interceptors from factory'() { + given: + ApplicationContext context = buildContext(""" +package annbinding1 + +import io.micronaut.aop.* +import io.micronaut.context.annotation.* +import jakarta.inject.Singleton + +@Singleton +@TestAnn +class MyBean(env: io.micronaut.context.env.Environment) { + + fun test() { + } +} + +@Retention +@Target(AnnotationTarget.FUNCTION, AnnotationTarget.CLASS) +@AroundConstruct +annotation class TestAnn + +@Factory +class InterceptorFactory { + var aroundConstructInvoked = false + + @InterceptorBean(TestAnn::class) + fun aroundIntercept(): ConstructorInterceptor { + return ConstructorInterceptor { context -> + this.aroundConstructInvoked = true + context.proceed() + } + } +} +""") + when: + def factory = getBean(context, 'annbinding1.InterceptorFactory') + + then: + !factory.aroundConstructInvoked + + when:"A bean that features constructor injection is instantiated" + def instance = getBean(context, 'annbinding1.MyBean') + + then:"The constructor interceptor is invoked" + !(instance instanceof Intercepted) + factory.aroundConstructInvoked + + cleanup: + context.close() + } + + void 'test around construct with introduction advice'() { + given: + ApplicationContext context = buildContext(""" +package annbinding1 + +import io.micronaut.aop.* +import jakarta.inject.Singleton + +@Singleton +@TestAnn +abstract class MyBean(env: io.micronaut.context.env.Environment) { + abstract fun test(): String +} + +@Retention +@Target(AnnotationTarget.FUNCTION, AnnotationTarget.CLASS) +@Introduction +@AroundConstruct +annotation class TestAnn + +@Singleton +@InterceptorBean(TestAnn::class) +class TestConstructInterceptor: ConstructorInterceptor { + var invoked = false + var parameters: Array? = null + + override fun intercept(context: ConstructorInvocationContext): Any { + invoked = true + parameters = context.parameterValues + return context.proceed() + } +} + +@Singleton +@InterceptorBinding(TestAnn::class) +class TestInterceptor: MethodInterceptor { + var invoked = false + + override fun intercept(context: MethodInvocationContext): Any? { + invoked = true + return "good" + } +} + +@Singleton +class AnotherInterceptor: Interceptor { + var invoked = false + + override fun intercept(context: InvocationContext): Any? { + invoked = true + return context.proceed() + } +} +""") + when: + def interceptor = getBean(context, 'annbinding1.TestInterceptor') + def constructorInterceptor = getBean(context, 'annbinding1.TestConstructInterceptor') + def anotherInterceptor = getBean(context, 'annbinding1.AnotherInterceptor') + + then: + !constructorInterceptor.invoked + !interceptor.invoked + !anotherInterceptor.invoked + + when:"A bean that features constructor injection is instantiated" + def instance = getBean(context, 'annbinding1.MyBean') + + then:"The constructor interceptor is invoked" + instance instanceof Intercepted + constructorInterceptor.invoked + constructorInterceptor.parameters.size() == 1 + constructorInterceptor.parameters[0] instanceof Environment + + and:"Other non-constructor interceptors are not invoked" + !interceptor.invoked + !anotherInterceptor.invoked + + when:"A method with interception is invoked" + constructorInterceptor.invoked = false + def result = instance.test() + + then:"the methods interceptor are invoked" + interceptor.invoked + result == 'good' + !anotherInterceptor.invoked + + and:"The constructor interceptor is not" + !constructorInterceptor.invoked + + cleanup: + context.close() + } + +} + diff --git a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/compile/ExecutableFactoryMethodSpec.groovy b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/compile/ExecutableFactoryMethodSpec.groovy new file mode 100644 index 00000000000..623e6463137 --- /dev/null +++ b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/compile/ExecutableFactoryMethodSpec.groovy @@ -0,0 +1,107 @@ +package io.micronaut.kotlin.processing.aop.compile + +import io.micronaut.inject.BeanDefinition +import reactor.core.publisher.Flux +import spock.lang.Specification + +import static io.micronaut.annotation.processing.test.KotlinCompiler.* + +class ExecutableFactoryMethodSpec extends Specification { + + void "test executing a default interface method"() { + when: + BeanDefinition beanDefinition = buildBeanDefinition('test.MyFactory$MyClass0', ''' +package test + +import io.micronaut.context.annotation.* +import jakarta.inject.Singleton + +interface SomeInterface { + + fun goDog(): String + + fun go(): String { + return "go" + } +} + +@Factory +class MyFactory { + + @Singleton + @Executable + fun myClass(): MyClass { + return MyClass() + } +} + +class MyClass: SomeInterface { + + override fun goDog(): String{ + return "go" + } +} +''') + + then: + noExceptionThrown() + beanDefinition != null + + when: + Object instance = beanDefinition.class.classLoader.loadClass('test.MyClass').newInstance() + + then: + beanDefinition.findMethod("go").get().invoke(instance) == "go" + beanDefinition.findMethod("goDog").get().invoke(instance) == "go" + } + + void "test executable factory with multiple interface inheritance"() { + when: + BeanDefinition beanDefinition = buildBeanDefinition('test.MyFactory$MyClient0', """ +package test + +import reactor.core.publisher.Flux +import io.micronaut.context.annotation.* +import jakarta.inject.Singleton +import org.reactivestreams.Publisher + +@Factory +class MyFactory { + + @Singleton + @Executable + fun myClient(): MyClient? { + return null + } +} + +interface HttpClient { + fun retrieve(): Publisher<*> +} +interface StreamingHttpClient: HttpClient { + fun stream(): Publisher +} +interface ReactorHttpClient: HttpClient { + override fun retrieve(): Flux<*> +} +interface ReactorStreamingHttpClient: StreamingHttpClient, ReactorHttpClient { + override fun stream(): Flux +} +interface MyClient: ReactorStreamingHttpClient { + fun blocking(): ByteArray +} +""") + + then: + noExceptionThrown() + beanDefinition != null + def retrieveMethod = beanDefinition.getRequiredMethod("retrieve") + def blockingMethod = beanDefinition.getRequiredMethod("blocking") + def streamMethod = beanDefinition.getRequiredMethod("stream") + retrieveMethod.returnType.type == Flux.class + streamMethod.returnType.type == Flux.class + retrieveMethod.returnType.typeParameters.length == 1 + retrieveMethod.returnType.typeParameters[0].type == Object.class + streamMethod.returnType.typeParameters[0].type == byte[].class + } +} diff --git a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/compile/FinalModifierSpec.groovy b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/compile/FinalModifierSpec.groovy new file mode 100644 index 00000000000..57bde1f14b5 --- /dev/null +++ b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/compile/FinalModifierSpec.groovy @@ -0,0 +1,243 @@ +/* + * Copyright 2017-2019 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.kotlin.processing.aop.compile + +import com.fasterxml.jackson.databind.ObjectMapper +import io.micronaut.aop.Intercepted +import io.micronaut.inject.qualifiers.Qualifiers +import io.micronaut.inject.writer.BeanDefinitionWriter +import spock.lang.Issue +import spock.lang.Specification + +import static io.micronaut.annotation.processing.test.KotlinCompiler.* + +class FinalModifierSpec extends Specification { + + @Issue('https://github.com/micronaut-projects/micronaut-core/issues/2530') + void 'test final modifier on external class produced by factory'() { + when: + def context = buildContext(''' +package test + +import io.micronaut.kotlin.processing.aop.simple.Mutating +import io.micronaut.context.annotation.* +import com.fasterxml.jackson.databind.ObjectMapper + +@Factory +class MyBeanFactory { + + @Mutating("someVal") + @jakarta.inject.Singleton + @jakarta.inject.Named("myMapper") + fun myMapper(): ObjectMapper { + return ObjectMapper() + } + +} + +''') + then: + context.getBean(ObjectMapper, Qualifiers.byName("myMapper")) instanceof Intercepted + + cleanup: + context.close() + } + + @Issue('https://github.com/micronaut-projects/micronaut-core/issues/2479') + void "test final modifier on inherited public method"() { + when: + def definition = buildBeanDefinition('test.CountryRepositoryImpl', ''' +package test + +import io.micronaut.kotlin.processing.aop.simple.Mutating +import io.micronaut.context.annotation.* + +abstract class BaseRepositoryImpl { + fun getContext(): Any { + return Object() + } +} + +interface CountryRepository + +@jakarta.inject.Singleton +@Mutating("someVal") +open class CountryRepositoryImpl: BaseRepositoryImpl(), CountryRepository { + + open fun someMethod(): String { + return "test"; + } +} +''') + then:"Compilation passes" + definition != null + } + + @Issue('https://github.com/micronaut-projects/micronaut-core/issues/2479') + void "test final modifier on inherited protected method"() { + when: + def definition = buildBeanDefinition('test.CountryRepositoryImpl', ''' +package test + +import io.micronaut.kotlin.processing.aop.simple.Mutating +import io.micronaut.context.annotation.* + +abstract class BaseRepositoryImpl { + + protected fun getContext(): Any { + return Object() + } +} + +interface CountryRepository + +@jakarta.inject.Singleton +@Mutating("someVal") +open class CountryRepositoryImpl: BaseRepositoryImpl(), CountryRepository { + + open fun someMethod(): String { + return "test" + } +} +''') + then:"Compilation passes" + definition != null + } + + @Issue('https://github.com/micronaut-projects/micronaut-core/issues/2479') + void "test final modifier on inherited protected method - 2"() { + when: + def definition = buildBeanDefinition('test.CountryRepositoryImpl', ''' +package test + +import io.micronaut.kotlin.processing.aop.simple.Mutating +import io.micronaut.context.annotation.* + +abstract class BaseRepositoryImpl { + protected fun getContext(): Any { + return Object() + } +} + +interface CountryRepository { + @Mutating("someVal") + fun someMethod(): String +} + +@jakarta.inject.Singleton +open class CountryRepositoryImpl: BaseRepositoryImpl(), CountryRepository { + + override fun someMethod(): String { + return "test" + } +} +''') + then:"Compilation passes" + definition != null + } + + void "test final modifier on factory with AOP advice doesn't compile"() { + when: + buildBeanDefinition('test.MyBeanFactory', ''' +package test + +import io.micronaut.kotlin.processing.aop.simple.Mutating +import io.micronaut.context.annotation.* + +@Factory +class MyBeanFactory { + + @Mutating("someVal") + @jakarta.inject.Singleton + fun myBean(): MyBean { + return MyBean() + } + +} + +class MyBean +''') + then: + def e = thrown(RuntimeException) + e.message.contains 'Cannot apply AOP advice to final class. Class must be made non-final to support proxying: test.MyBean' + } + + void "test final modifier on class with AOP advice doesn't compile"() { + when: + buildBeanDefinition('test.$MyBean' + BeanDefinitionWriter.CLASS_SUFFIX + BeanDefinitionWriter.PROXY_SUFFIX, ''' +package test + +import io.micronaut.kotlin.processing.aop.simple.Mutating +import io.micronaut.context.annotation.* + +@Mutating("someVal") +@jakarta.inject.Singleton +class MyBean(@Value("\\${foo.bar}") private val myValue: String) { + + open fun someMethod(): String { + return myValue + } +} +''') + then: + def e = thrown(RuntimeException) + e.message.contains 'Cannot apply AOP advice to final class. Class must be made non-final to support proxying: test.MyBean' + } + + void "test final modifier on method with AOP advice doesn't compile"() { + when: + buildBeanDefinition('test.$MyBean' + BeanDefinitionWriter.CLASS_SUFFIX + BeanDefinitionWriter.PROXY_SUFFIX, ''' +package test + +import io.micronaut.kotlin.processing.aop.simple.Mutating +import io.micronaut.context.annotation.* + +@Mutating("someVal") +@jakarta.inject.Singleton +open class MyBean(@Value("\\${foo.bar}") private val myValue: String) { + + fun someMethod(): String { + return myValue + } +} +''') + then: + def e = thrown(RuntimeException) + e.message.contains 'Public method inherits AOP advice but is declared final.' + } + + void "test final modifier on method with AOP advice on method doesn't compile"() { + when: + buildBeanDefinition('test.$MyBean' + BeanDefinitionWriter.CLASS_SUFFIX + BeanDefinitionWriter.PROXY_SUFFIX, ''' +package test + +import io.micronaut.kotlin.processing.aop.simple.Mutating +import io.micronaut.context.annotation.* + +@jakarta.inject.Singleton +class MyBean(@Value("\\${foo.bar}") private val myValue: String) { + + @Mutating("someVal") + fun someMethod(): String { + return myValue + } +} +''') + then: + def e = thrown(RuntimeException) + e.message.contains 'Method defines AOP advice but is declared final. Change the method to be non-final in order for AOP advice to be applied.' + } +} diff --git a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/compile/GeneratedAnnotationSpec.groovy b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/compile/GeneratedAnnotationSpec.groovy new file mode 100644 index 00000000000..0660f8b4977 --- /dev/null +++ b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/compile/GeneratedAnnotationSpec.groovy @@ -0,0 +1,54 @@ +package io.micronaut.kotlin.processing.aop.compile + +import io.micronaut.inject.writer.BeanDefinitionWriter +import org.objectweb.asm.AnnotationVisitor +import org.objectweb.asm.ClassReader +import org.objectweb.asm.ClassVisitor +import org.objectweb.asm.Opcodes +import spock.lang.Issue +import spock.lang.Specification + +import static io.micronaut.annotation.processing.test.KotlinCompiler.* + +class GeneratedAnnotationSpec extends Specification { + + @Issue('https://github.com/micronaut-projects/micronaut-core/issues/4127') + void 'test only 1 generated annotation is added'() { + when: + def bytes = getClassBytes('example.FooController' + BeanDefinitionWriter.CLASS_SUFFIX + BeanDefinitionWriter.PROXY_SUFFIX, ''' +package example + +import io.micronaut.http.annotation.Controller +import io.micronaut.http.annotation.Get +import io.micronaut.validation.Validated + +@Validated +@Controller("/") +open class FooController { + + @Get + open fun foo(): String { + return "" + } +} +''') + then: + bytes != null + + when: + ClassReader reader = new ClassReader(bytes) + int generatedAnnotations = 0 + reader.accept(new ClassVisitor(Opcodes.ASM5) { + @Override + AnnotationVisitor visitAnnotation(String descriptor, boolean visible) { + if (descriptor.contains("Generated")) { + generatedAnnotations++ + } + return super.visitAnnotation(descriptor, visible) + } + },ClassReader.SKIP_CODE) + + then:"Only one generated annotation is added" + generatedAnnotations == 1 + } +} diff --git a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/compile/InheritedAnnotationMetadataSpec.groovy b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/compile/InheritedAnnotationMetadataSpec.groovy new file mode 100644 index 00000000000..369306c62a1 --- /dev/null +++ b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/compile/InheritedAnnotationMetadataSpec.groovy @@ -0,0 +1,157 @@ +package io.micronaut.kotlin.processing.aop.compile + +import io.micronaut.context.ApplicationContext +import io.micronaut.core.annotation.Blocking +import io.micronaut.inject.BeanDefinition +import io.micronaut.inject.InstantiatableBeanDefinition +import io.micronaut.inject.writer.BeanDefinitionVisitor +import io.micronaut.inject.writer.BeanDefinitionWriter +import spock.lang.Specification + +import static io.micronaut.annotation.processing.test.KotlinCompiler.* + +class InheritedAnnotationMetadataSpec extends Specification { + + void "test that annotation metadata is inherited from overridden methods for introduction advice"() { + when: + BeanDefinition beanDefinition = buildBeanDefinition('test.MyBean' + BeanDefinitionVisitor.PROXY_SUFFIX, ''' +package test + +import io.micronaut.context.annotation.Executable +import io.micronaut.core.annotation.Blocking +import io.micronaut.kotlin.processing.aop.introduction.Stub + +@Stub +@jakarta.inject.Singleton +interface MyBean: MyInterface { + override fun someMethod(): String +} + +interface MyInterface { + @Blocking + @Executable + fun someMethod(): String +} +''') + then: + !beanDefinition.isAbstract() + beanDefinition != null + beanDefinition.injectedFields.size() == 0 + beanDefinition.executableMethods.size() == 1 + beanDefinition.executableMethods[0].hasAnnotation(Blocking) + !beanDefinition.executableMethods[0].hasDeclaredAnnotation(Blocking) + } + + void "test that annotation metadata is inherited from overridden methods for around advice"() { + when: + BeanDefinition beanDefinition = buildBeanDefinition('test.$MyBean' + BeanDefinitionWriter.CLASS_SUFFIX + BeanDefinitionWriter.PROXY_SUFFIX, ''' +package test + +import io.micronaut.kotlin.processing.aop.simple.Mutating +import io.micronaut.context.annotation.Executable +import io.micronaut.context.annotation.Value +import io.micronaut.core.annotation.Blocking + +@Mutating("someVal") +@jakarta.inject.Singleton +open class MyBean(@Value("\\${foo.bar}") private val myValue: String): MyInterface { + + override fun someMethod(): String { + return myValue + } +} + +interface MyInterface { + @Blocking + @Executable + fun someMethod(): String +} +''') + then: + beanDefinition != null + !beanDefinition.isAbstract() + beanDefinition.injectedFields.size() == 0 + beanDefinition.executableMethods.size() == 1 + beanDefinition.executableMethods[0].hasAnnotation(Blocking) + + when: + def context = ApplicationContext.run('foo.bar':'test') + def instance = ((InstantiatableBeanDefinition)beanDefinition).instantiate(context) + + then: + instance.someMethod() == 'test' + } + + void "test that a bean definition is not created for an abstract class"() { + when: + ApplicationContext ctx = buildContext(''' +package test + +import io.micronaut.aop.* +import io.micronaut.context.annotation.* +import io.micronaut.core.annotation.* +import io.micronaut.core.order.Ordered +import jakarta.inject.Singleton + +interface ContractService { + + @SomeAnnot + fun interfaceServiceMethod() +} + +abstract class BaseService { + + @SomeAnnot + open fun baseServiceMethod() {} +} + +@SomeAnnot +abstract class BaseAnnotatedService + +@Singleton +open class Service: BaseService(), ContractService { + + @SomeAnnot + open fun serviceMethod() {} + + override fun interfaceServiceMethod() {} +} + +@MustBeDocumented +@Retention +@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION, AnnotationTarget.VALUE_PARAMETER) +@Around +@Type(SomeInterceptor::class) +annotation class SomeAnnot + +@Singleton +class SomeInterceptor: MethodInterceptor, Ordered { + + override fun intercept(context: MethodInvocationContext): Any? { + return context.proceed() + } +} +''') + then: + Class clazz = ctx.classLoader.loadClass("test.ContractService") + ctx.getBean(clazz) + + when: + ctx.classLoader.loadClass("test.\$BaseService" + BeanDefinitionWriter.CLASS_SUFFIX) + + then: + thrown(ClassNotFoundException) + + when: + ctx.classLoader.loadClass("test.\$BaseService" + BeanDefinitionWriter.CLASS_SUFFIX + BeanDefinitionWriter.PROXY_SUFFIX) + + then: + thrown(ClassNotFoundException) + + when: + ctx.classLoader.loadClass("test.\$BaseAnnotatedService" + BeanDefinitionWriter.CLASS_SUFFIX) + + then: + thrown(ClassNotFoundException) + } +} diff --git a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/compile/IntroductionAnnotationSpec.groovy b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/compile/IntroductionAnnotationSpec.groovy new file mode 100644 index 00000000000..ecc10da9d20 --- /dev/null +++ b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/compile/IntroductionAnnotationSpec.groovy @@ -0,0 +1,137 @@ +package io.micronaut.kotlin.processing.aop.compile + +import io.micronaut.aop.exceptions.UnimplementedAdviceException +import io.micronaut.context.BeanContext +import io.micronaut.inject.AdvisedBeanType +import io.micronaut.inject.BeanDefinition +import io.micronaut.inject.InstantiatableBeanDefinition +import io.micronaut.inject.writer.BeanDefinitionVisitor +import io.micronaut.kotlin.processing.aop.introduction.NotImplementedAdvice +import spock.lang.Specification + +import javax.validation.constraints.Min +import javax.validation.constraints.NotBlank + +import static io.micronaut.annotation.processing.test.KotlinCompiler.* + +class IntroductionAnnotationSpec extends Specification { + + void 'test unimplemented introduction advice'() { + given: + BeanDefinition beanDefinition = buildBeanDefinition('test.MyBean' + BeanDefinitionVisitor.PROXY_SUFFIX, ''' +package test + +import io.micronaut.kotlin.processing.aop.introduction.NotImplemented + +@NotImplemented +interface MyBean { + fun test() +} +''') + def context = BeanContext.run() + def bean = ((InstantiatableBeanDefinition) beanDefinition).instantiate(context) + + when: + bean.test() + + then: + beanDefinition instanceof AdvisedBeanType + beanDefinition.interceptedType.name == 'test.MyBean' + thrown(UnimplementedAdviceException) + + cleanup: + context.close() + } + + void 'test unimplemented introduction advice on abstract class with concrete methods'() { + given: + BeanDefinition beanDefinition = buildBeanDefinition('test.MyBean' + BeanDefinitionVisitor.PROXY_SUFFIX, ''' +package test + +import io.micronaut.kotlin.processing.aop.introduction.NotImplemented +import io.micronaut.context.annotation.* +import io.micronaut.kotlin.processing.aop.simple.Mutating + +@NotImplemented +abstract class MyBean { + + abstract fun test() + + fun test2(): String { + return "good" + } + + @Mutating("arg") + open fun test3(arg: String): String { + return arg + } +} +''') + def context = BeanContext.run() + def bean = ((InstantiatableBeanDefinition) beanDefinition).instantiate(context) + def notImplementedAdvice = context.getBean(NotImplementedAdvice) + + when: + bean.test() + + then: + thrown(UnimplementedAdviceException) + notImplementedAdvice.invoked + + when: + notImplementedAdvice.invoked = false + + then: + bean.test2() == 'good' + bean.test3() == 'changed' + !notImplementedAdvice.invoked + + cleanup: + context.close() + } + + void "test @Min annotation"() { + when: + BeanDefinition beanDefinition = buildBeanDefinition('test.MyBean' + BeanDefinitionVisitor.PROXY_SUFFIX, ''' +package test + +import io.micronaut.kotlin.processing.aop.introduction.Stub +import io.micronaut.context.annotation.Executable +import javax.validation.constraints.Min +import javax.validation.constraints.NotBlank + +interface MyInterface{ + @Executable + fun save(@NotBlank name: String, @Min(1L) age: Int) + + @Executable + fun saveTwo(@Min(1L) name: String) +} + + +@Stub +@jakarta.inject.Singleton +interface MyBean: MyInterface +''') + then: + !beanDefinition.isAbstract() + beanDefinition != null + beanDefinition.injectedFields.size() == 0 + beanDefinition.executableMethods.size() == 2 + + def saveMethod = beanDefinition.executableMethods.find {it.methodName == 'save'} + saveMethod != null + saveMethod.returnType.type == void.class + saveMethod.arguments[0].getAnnotationMetadata().hasAnnotation(NotBlank) + saveMethod.arguments[1].getAnnotationMetadata().hasAnnotation(Min) + saveMethod.arguments[1].getAnnotationMetadata().getValue(Min, Integer).get() == 1 + + + def saveTwoMethod = beanDefinition.executableMethods.find {it.methodName == 'saveTwo'} + saveTwoMethod != null + saveTwoMethod.methodName == 'saveTwo' + saveTwoMethod.returnType.type == void.class + saveTwoMethod.arguments[0].getAnnotationMetadata().hasAnnotation(Min) + } + +} diff --git a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/compile/IntroductionCompileSpec.groovy b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/compile/IntroductionCompileSpec.groovy new file mode 100644 index 00000000000..a8e8394bcdb --- /dev/null +++ b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/compile/IntroductionCompileSpec.groovy @@ -0,0 +1,110 @@ +package io.micronaut.kotlin.processing.aop.compile + +import io.micronaut.annotation.processing.test.KotlinCompiler +import io.micronaut.aop.Intercepted +import io.micronaut.context.ApplicationContext +import io.micronaut.inject.BeanDefinition +import spock.lang.Specification + +import static io.micronaut.annotation.processing.test.KotlinCompiler.* + +class IntroductionCompileSpec extends Specification { + + void 'test coroutine repository'() { + given: + def context = KotlinCompiler.buildContext(''' +package test + +import io.micronaut.aop.Introduction +import jakarta.inject.Singleton + +import kotlinx.coroutines.flow.Flow + +class SomeEntity + +interface CoroutineCrudRepository { + + suspend fun save(entity: S): S + suspend fun update(entity: S): S + fun updateAll(entities: Iterable): Flow + fun saveAll(entities: Iterable): Flow + suspend fun findById(id: ID): E? + suspend fun existsById(id: ID): Boolean + fun findAll(): Flow + suspend fun count(): Long + suspend fun deleteById(id: ID): Int + suspend fun delete(entity: E): Int + suspend fun deleteAll(entities: Iterable): Int + suspend fun deleteAll(): Int +} + +@MyRepository +interface CustomRepository : CoroutineCrudRepository { + + // As of Kotlin version 1.7.20 and KAPT, this will generate JVM signature: "SomeEntity findById(long id, continuation)" + override suspend fun findById(id: Long): SomeEntity? + + suspend fun xyz(): String + + suspend fun abc(): String + + suspend fun count1(): String + + suspend fun count2(): String + +} + +@MustBeDocumented +@kotlin.annotation.Retention(AnnotationRetention.RUNTIME) +@Introduction +@Singleton +annotation class MyRepository +''') + def definition = getBeanDefinition(context, 'test.CustomRepository') + + expect: + definition != null + + cleanup: + context.close() + } + + void 'test apply introduction advise with interceptor binding'() { + given: + ApplicationContext context = buildContext(''' +package introductiontest + +import io.micronaut.aop.* +import jakarta.inject.Singleton + +@TestAnn +interface MyBean { + fun test(): Int +} + +@Retention +@Target(AnnotationTarget.FUNCTION, AnnotationTarget.CLASS) +@Introduction +annotation class TestAnn + +@InterceptorBean(TestAnn::class) +class StubIntroduction: Interceptor { + var invoked = 0 + override fun intercept(context: InvocationContext): Any { + invoked++ + return 10 + } +} +''') + def instance = getBean(context, 'introductiontest.MyBean') + def interceptor = getBean(context, 'introductiontest.StubIntroduction') + + when: + def result = instance.test() + + then:"the interceptor was invoked" + instance instanceof Intercepted + interceptor.invoked == 1 + result == 10 + } +} diff --git a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/compile/IntroductionGenericTypesSpec.groovy b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/compile/IntroductionGenericTypesSpec.groovy new file mode 100644 index 00000000000..ffb47a937bd --- /dev/null +++ b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/compile/IntroductionGenericTypesSpec.groovy @@ -0,0 +1,126 @@ +package io.micronaut.kotlin.processing.aop.compile + +import io.micronaut.context.DefaultBeanContext +import io.micronaut.core.type.ReturnType +import io.micronaut.inject.BeanDefinition +import io.micronaut.inject.InstantiatableBeanDefinition +import io.micronaut.inject.writer.BeanDefinitionVisitor +import spock.lang.Specification + +import static io.micronaut.annotation.processing.test.KotlinCompiler.* + +class IntroductionGenericTypesSpec extends Specification { + + void "test that generic return types are correct when implementing an interface with type arguments"() { + when: + BeanDefinition beanDefinition = buildBeanDefinition('test.MyBean' + BeanDefinitionVisitor.PROXY_SUFFIX, ''' +package test + +import io.micronaut.kotlin.processing.aop.introduction.Stub +import io.micronaut.context.annotation.* +import java.net.URL + +interface MyInterface { + + fun getURL(): T + + fun getURLs(): List +} + + +@Stub +@jakarta.inject.Singleton +@Executable +interface MyBean: MyInterface + +''') + then: + !beanDefinition.isAbstract() + beanDefinition != null + beanDefinition.injectedFields.size() == 0 + beanDefinition.executableMethods.size() == 2 + + beanDefinition.getRequiredMethod("getURL").targetMethod.returnType == URL + beanDefinition.getRequiredMethod("getURL").returnType.type == URL + beanDefinition.getRequiredMethod("getURLs").returnType.type == List + beanDefinition.getRequiredMethod("getURLs").returnType.asArgument().hasTypeVariables() + beanDefinition.getRequiredMethod("getURLs").returnType.asArgument().typeVariables['E'].type == URL + } + + void "test that generic return types are correct when implementing an interface with type arguments 2"() { + when: + BeanDefinition beanDefinition = buildBeanDefinition('test.MyBean' + BeanDefinitionVisitor.PROXY_SUFFIX, ''' +package test + +import io.micronaut.kotlin.processing.aop.introduction.Stub +import io.micronaut.context.annotation.* +import java.net.URL + +interface MyInterface { + + fun getPeopleSingle(): reactor.core.publisher.Mono> + + fun getPerson(): T + + fun getPeople(): List + + fun save(person: T) + + fun saveAll(person: List) + + fun getPeopleArray(): Array + + fun getPeopleListArray(): List> + + fun getPeopleMap(): Map +} + +@Stub +@jakarta.inject.Singleton +@Executable +interface MyBean: MyInterface + +open class Person + +class SubPerson: Person() + +''') + then: + !beanDefinition.isAbstract() + beanDefinition != null + returnType(beanDefinition, "getPerson").type.name == 'test.SubPerson' + returnType(beanDefinition, "getPeople").type == List + returnType(beanDefinition, "getPeople").asArgument().hasTypeVariables() + returnType(beanDefinition, "getPeople").asArgument().typeVariables['E'].type.name == 'test.SubPerson' + returnType(beanDefinition, "getPeopleMap").typeVariables['K'].type.name == 'test.SubPerson' + returnType(beanDefinition, "getPeopleMap").typeVariables['V'].type == URL + returnType(beanDefinition, "getPeopleArray").type.isArray() + returnType(beanDefinition, "getPeopleArray").type.name.contains('test.SubPerson') + returnType(beanDefinition, "getPeopleListArray").type == List + returnType(beanDefinition, "getPeopleListArray").typeVariables['E'].type.isArray() + beanDefinition.findPossibleMethods("save").findFirst().get().targetMethod != null + beanDefinition.findPossibleMethods("getPerson").findFirst().get().targetMethod != null + def getPeopleSingle = returnType(beanDefinition, "getPeopleSingle") + getPeopleSingle.typeVariables['T'].type== List + getPeopleSingle.typeVariables['T'].typeVariables['E'].type.name == 'test.SubPerson' + + + when: + def context = new DefaultBeanContext() + context.start() + def instance = ((InstantiatableBeanDefinition)beanDefinition).instantiate(context) + + + then:"the methods are invocable" + instance.getPerson() == null + instance.getPeople() == null + instance.getPeopleArray() == null + instance.getPeopleSingle() == null + instance.save(null) == null + instance.saveAll([]) == null + } + + ReturnType returnType(BeanDefinition bd, String name) { + bd.findPossibleMethods(name).findFirst().get().returnType + } +} diff --git a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/compile/IntroductionInnerInterfaceSpec.groovy b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/compile/IntroductionInnerInterfaceSpec.groovy new file mode 100644 index 00000000000..6eb9827cc0c --- /dev/null +++ b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/compile/IntroductionInnerInterfaceSpec.groovy @@ -0,0 +1,39 @@ +package io.micronaut.kotlin.processing.aop.compile + +import io.micronaut.aop.Intercepted +import io.micronaut.inject.writer.BeanDefinitionVisitor +import spock.lang.Specification + +import static io.micronaut.annotation.processing.test.KotlinCompiler.* + +class IntroductionInnerInterfaceSpec extends Specification { + + void 'test that an inner interface with introduction doesnt create advise for outer class'() { + given: + def clsName = 'inneritfce.Test' + def context = buildContext(''' +package inneritfce + +import jakarta.inject.Singleton +import io.micronaut.kotlin.processing.aop.introduction.Stub + +@Singleton +class Test { + + @Stub + interface InnerIntroduction +} +''') + when: + def bean = getBean(context, clsName) + + then:'outer bean is not AOP advice' + !(bean instanceof Intercepted) + + when: + context.classLoader.loadClass(clsName + BeanDefinitionVisitor.PROXY_SUFFIX) + + then:'proxy not generated for outer type' + thrown(ClassNotFoundException) + } +} diff --git a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/compile/IntroductionWithAroundSpec.groovy b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/compile/IntroductionWithAroundSpec.groovy new file mode 100644 index 00000000000..c9c8d5625df --- /dev/null +++ b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/compile/IntroductionWithAroundSpec.groovy @@ -0,0 +1,50 @@ +package io.micronaut.kotlin.processing.aop.compile + +import io.micronaut.context.ApplicationContext +import io.micronaut.inject.BeanDefinition +import io.micronaut.inject.InstantiatableBeanDefinition +import io.micronaut.inject.writer.BeanDefinitionVisitor +import spock.lang.Specification + +import static io.micronaut.annotation.processing.test.KotlinCompiler.* + +class IntroductionWithAroundSpec extends Specification { + + void "test that around advice is applied to introduction concrete methods"() { + when:"An introduction advice type is compiled that includes a concrete method that is annotated with around advice" + BeanDefinition beanDefinition = buildBeanDefinition('test.MyBean' + BeanDefinitionVisitor.PROXY_SUFFIX, ''' +package test; + +import io.micronaut.kotlin.processing.aop.introduction.Stub +import io.micronaut.kotlin.processing.aop.simple.Mutating +import javax.validation.constraints.* +import jakarta.inject.Singleton + +@Stub +@Singleton +abstract class MyBean { + abstract fun save(@NotBlank name: String, @Min(1L) age: Int) + abstract fun saveTwo(@Min(1L) name: String) + + @Mutating("name") + open fun myConcrete(name: String): String { + return name + } +} + +''') + + then:"The around advice is applied to the concrete method" + beanDefinition != null + + when: + ApplicationContext context = ApplicationContext.run() + def instance = ((InstantiatableBeanDefinition) beanDefinition).instantiate(context) + + then: + instance.myConcrete("test") == 'changed' + + cleanup: + context.close() + } +} diff --git a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/compile/LifeCycleWithProxySpec.groovy b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/compile/LifeCycleWithProxySpec.groovy new file mode 100644 index 00000000000..953a113e907 --- /dev/null +++ b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/compile/LifeCycleWithProxySpec.groovy @@ -0,0 +1,168 @@ +package io.micronaut.kotlin.processing.aop.compile + +import io.micronaut.context.ApplicationContext +import io.micronaut.inject.BeanDefinition +import io.micronaut.inject.InstantiatableBeanDefinition +import io.micronaut.inject.writer.BeanDefinitionWriter +import spock.lang.Specification + +import static io.micronaut.annotation.processing.test.KotlinCompiler.* + +class LifeCycleWithProxySpec extends Specification { + + void "test that a simple AOP definition lifecycle hooks are invoked - annotation at class level"() { + when: + BeanDefinition beanDefinition = buildBeanDefinition('test.$MyBean' + BeanDefinitionWriter.CLASS_SUFFIX + BeanDefinitionWriter.PROXY_SUFFIX, ''' +package test + +import io.micronaut.context.env.Environment +import io.micronaut.kotlin.processing.aop.simple.Mutating +import io.micronaut.core.convert.ConversionService + +@Mutating("someVal") +@jakarta.inject.Singleton +open class MyBean { + + @jakarta.inject.Inject + lateinit var conversionService: ConversionService + + var count = 0 + + open fun someMethod(): String { + return "good" + } + + @jakarta.annotation.PostConstruct + fun created() { + count++ + } + + @jakarta.annotation.PreDestroy + fun destroyed() { + count-- + } + +} +''') + then: + !beanDefinition.isAbstract() + beanDefinition != null + beanDefinition.postConstructMethods.size() == 1 + beanDefinition.preDestroyMethods.size() == 1 + + when: + def context = ApplicationContext.builder(beanDefinition.class.classLoader).start() + def instance = ((InstantiatableBeanDefinition) beanDefinition).instantiate(context) + + then: + instance.conversionService // injection works + instance.someMethod() == 'good' + instance.count == 1 + + cleanup: + context.close() + } + + void "test that a simple AOP definition lifecycle hooks are invoked - annotation at method level with hooks last"() { + when: + BeanDefinition beanDefinition = buildBeanDefinition('test.$MyBean' + BeanDefinitionWriter.CLASS_SUFFIX + BeanDefinitionWriter.PROXY_SUFFIX, ''' +package test + +import io.micronaut.kotlin.processing.aop.simple.Mutating +import io.micronaut.core.convert.ConversionService + +@jakarta.inject.Singleton +open class MyBean { + + @jakarta.inject.Inject + lateinit var conversionService: ConversionService + + var count = 0 + + @Mutating("someVal") + open fun someMethod(): String { + return "good" + } + + @jakarta.annotation.PostConstruct + fun created() { + count++ + } + + @jakarta.annotation.PreDestroy + fun destroyed() { + count-- + } + +} +''') + then: + !beanDefinition.isAbstract() + beanDefinition != null + beanDefinition.postConstructMethods.size() == 1 + beanDefinition.preDestroyMethods.size() == 1 + + when: + def context = ApplicationContext.builder(beanDefinition.class.classLoader).start() + def instance = ((InstantiatableBeanDefinition) beanDefinition).instantiate(context) + + then: + instance.conversionService != null + instance.someMethod() == 'good' + instance.count == 1 + + cleanup: + context.close() + } + + void "test that a simple AOP definition lifecycle hooks are invoked - annotation at method level"() { + when: + BeanDefinition beanDefinition = buildBeanDefinition('test.$MyBean' + BeanDefinitionWriter.CLASS_SUFFIX + BeanDefinitionWriter.PROXY_SUFFIX, ''' +package test + +import io.micronaut.kotlin.processing.aop.simple.Mutating +import io.micronaut.core.convert.ConversionService + +@jakarta.inject.Singleton +open class MyBean { + + @jakarta.inject.Inject + lateinit var conversionService: ConversionService + + var count = 0 + + @jakarta.annotation.PostConstruct + fun created() { + count++ + } + + @jakarta.annotation.PreDestroy + fun destroyed() { + count-- + } + + @Mutating("someVal") + open fun someMethod(): String { + return "good" + } + +} +''') + then: + !beanDefinition.isAbstract() + beanDefinition != null + + when: + def context = ApplicationContext.builder(beanDefinition.class.classLoader).start() + def instance = ((InstantiatableBeanDefinition) beanDefinition).instantiate(context) + + + then: + instance.conversionService != null + instance.someMethod() == 'good' + instance.count == 1 + + cleanup: + context.close() + } +} diff --git a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/compile/LifeCycleWithProxyTargetSpec.groovy b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/compile/LifeCycleWithProxyTargetSpec.groovy new file mode 100644 index 00000000000..7c5bfb505cf --- /dev/null +++ b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/compile/LifeCycleWithProxyTargetSpec.groovy @@ -0,0 +1,147 @@ +package io.micronaut.kotlin.processing.aop.compile + +import io.micronaut.context.ApplicationContext +import io.micronaut.inject.BeanDefinition +import io.micronaut.inject.InstantiatableBeanDefinition +import io.micronaut.inject.writer.BeanDefinitionWriter +import spock.lang.Specification + +import static io.micronaut.annotation.processing.test.KotlinCompiler.* + +class LifeCycleWithProxyTargetSpec extends Specification { + + void "test that a proxy target AOP definition lifecycle hooks are invoked - annotation at class level"() { + when: + BeanDefinition beanDefinition = buildBeanDefinition('test.$MyBean' + BeanDefinitionWriter.CLASS_SUFFIX + BeanDefinitionWriter.PROXY_SUFFIX, ''' +package test + +import io.micronaut.kotlin.processing.aop.proxytarget.Mutating +import io.micronaut.core.convert.ConversionService + +@Mutating("someVal") +@jakarta.inject.Singleton +open class MyBean { + + @jakarta.inject.Inject + lateinit var conversionService: ConversionService + + var count = 0 + + open fun someMethod(): String { + return "good" + } + + @jakarta.annotation.PostConstruct + fun created() { + count++ + } + + @jakarta.annotation.PreDestroy + fun destroyed() { + count-- + } + +} +''') + then: + !beanDefinition.isAbstract() + beanDefinition != null + beanDefinition.postConstructMethods.size() == 1 + beanDefinition.preDestroyMethods.size() == 1 + + when: + def context = ApplicationContext.builder(beanDefinition.class.classLoader).start() + def instance = ((InstantiatableBeanDefinition) beanDefinition).instantiate(context) + + then:"proxy post construct methods are not invoked" + instance.conversionService // injection works + instance.someMethod() == 'good' + instance.count == 0 + + and:"proxy target post construct methods are invoked" + instance.interceptedTarget().count == 1 + + cleanup: + context.close() + } + + void "test that a proxy target AOP definition lifecycle hooks are invoked - annotation at method level with hooks last"() { + when: + BeanDefinition beanDefinition = buildBeanDefinition('test.$MyBean' + BeanDefinitionWriter.CLASS_SUFFIX + BeanDefinitionWriter.PROXY_SUFFIX, ''' +package test; + +import io.micronaut.kotlin.processing.aop.proxytarget.Mutating +import io.micronaut.core.convert.ConversionService + +@jakarta.inject.Singleton +open class MyBean { + + @jakarta.inject.Inject + lateinit var conversionService: ConversionService + + var count = 0 + + @Mutating("someVal") + open fun someMethod(): String { + return "good" + } + + @jakarta.annotation.PostConstruct + fun created() { + count++ + } + + @jakarta.annotation.PreDestroy + fun destroyed() { + count-- + } + +} +''') + then: + !beanDefinition.isAbstract() + beanDefinition != null + beanDefinition.postConstructMethods.size() == 1 + beanDefinition.preDestroyMethods.size() == 1 + + } + + void "test that a proxy target AOP definition lifecycle hooks are invoked - annotation at method level"() { + when: + BeanDefinition beanDefinition = buildBeanDefinition('test.$MyBean' + BeanDefinitionWriter.CLASS_SUFFIX + BeanDefinitionWriter.PROXY_SUFFIX, ''' +package test; + +import io.micronaut.kotlin.processing.aop.proxytarget.Mutating +import io.micronaut.core.convert.ConversionService + +@jakarta.inject.Singleton +open class MyBean { + + @jakarta.inject.Inject + lateinit var conversionService: ConversionService + + var count = 0 + + @jakarta.annotation.PostConstruct + fun created() { + count++ + } + + @jakarta.annotation.PreDestroy + fun destroyed() { + count-- + } + + @Mutating("someVal") + open fun someMethod(): String { + return "good" + } + +} +''') + then: + !beanDefinition.isAbstract() + beanDefinition != null + } +} + diff --git a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/compile/PostConstructInterceptorCompileSpec.groovy b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/compile/PostConstructInterceptorCompileSpec.groovy new file mode 100644 index 00000000000..47493de217f --- /dev/null +++ b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/compile/PostConstructInterceptorCompileSpec.groovy @@ -0,0 +1,293 @@ +package io.micronaut.kotlin.processing.aop.compile + +import io.micronaut.aop.Intercepted +import io.micronaut.context.ApplicationContext +import spock.lang.PendingFeature +import spock.lang.Specification +import spock.lang.Unroll +import static io.micronaut.annotation.processing.test.KotlinCompiler.* + +class PostConstructInterceptorCompileSpec extends Specification { + + @Unroll + void 'test post construct with around interception - proxyTarget = #proxyTarget'() { + given: + ApplicationContext context = buildContext(""" +package annbinding1 + +import io.micronaut.aop.* +import jakarta.inject.* +import jakarta.annotation.PostConstruct + +@Singleton +@TestAnn +open class MyBean(env: io.micronaut.context.env.Environment) { + + @Inject lateinit var env: io.micronaut.context.env.Environment + + var invoked = 0 + + open fun test() { + } + + @PostConstruct + fun init() { + println("INVOKED POST CONSTRUCT") + invoked++ + } +} + +@io.micronaut.context.annotation.Factory +class MyFactory { + + @TestAnn + @Singleton + fun test(env: io.micronaut.context.env.Environment): MyOtherBean { + return MyOtherBean() + } +} + +open class MyOtherBean + +@Retention +@Target(AnnotationTarget.FUNCTION, AnnotationTarget.CLASS) +@Around(proxyTarget=$proxyTarget) +@InterceptorBinding(kind=InterceptorKind.POST_CONSTRUCT) +@InterceptorBinding(kind=InterceptorKind.PRE_DESTROY) +annotation class TestAnn + + +@Singleton +@InterceptorBean(TestAnn::class) +class TestInterceptor: MethodInterceptor { + var invoked = 0 + + override fun intercept(context: MethodInvocationContext): Any? { + invoked++ + return context.proceed() + } +} + +@Singleton +@InterceptorBinding(value=TestAnn::class, kind=InterceptorKind.POST_CONSTRUCT) +class PostConstructTestInterceptor: MethodInterceptor { + var invoked = 0 + + override fun intercept(context: MethodInvocationContext): Any? { + invoked++ + return context.proceed() + } +} + +@Singleton +@InterceptorBinding(value=TestAnn::class, kind=InterceptorKind.PRE_DESTROY) +class PreDestroyTestInterceptor: MethodInterceptor { + var invoked = 0 + + override fun intercept(context: MethodInvocationContext): Any? { + invoked++ + return context.proceed() + } +} + +@Singleton +class AnotherInterceptor: Interceptor { + var invoked = 0 + override fun intercept(context: InvocationContext): Any? { + invoked++ + return context.proceed() + } +} +""") + when: + def interceptor = getBean(context, 'annbinding1.TestInterceptor') + def constructorInterceptor = getBean(context, 'annbinding1.PostConstructTestInterceptor') + def destroyInterceptor = getBean(context, 'annbinding1.PreDestroyTestInterceptor') + def anotherInterceptor = getBean(context, 'annbinding1.AnotherInterceptor') + + then: + !interceptor.invoked + !anotherInterceptor.invoked + !constructorInterceptor.invoked + + when:"A bean that featuring post construct injection is instantiated" + def instance = getBean(context, 'annbinding1.MyBean') + + then:"The interceptors that apply to post construction are invoked" + (proxyTarget ? instance.interceptedTarget() : instance).invoked == 1 + interceptor.invoked == 1 + constructorInterceptor.invoked == 1 + anotherInterceptor.invoked == 0 + destroyInterceptor.invoked == 0 + + + when:"A method with interception is invoked" + instance.test() + + then:"the methods interceptor are invoked" + instance instanceof Intercepted + interceptor.invoked == 2 + constructorInterceptor.invoked == 1 + anotherInterceptor.invoked == 0 + + + when:"A bean that is created from a factory is instantiated" + def factoryCreatedInstance = getBean(context, 'annbinding1.MyOtherBean') + + then:"post construct interceptors are invoked for the created instance" + interceptor.invoked == 3 + constructorInterceptor.invoked == 2 + anotherInterceptor.invoked == 0 + + when: + context.stop() + + then: + // TODO: Discuss why we are invoking destroy hooks for proxies + interceptor.invoked == proxyTarget ? 6 : 5 + constructorInterceptor.invoked == 2 + anotherInterceptor.invoked == 0 + // TODO: Discuss why we are invoking destroy hooks for proxies + destroyInterceptor.invoked == proxyTarget ? 3 : 2 + + + where: + proxyTarget << [true, false] + } + + void 'test post construct & pre destroy without around interception'() { + given: + ApplicationContext context = buildContext(""" +package annbinding1 + +import io.micronaut.aop.* +import jakarta.inject.* +import jakarta.annotation.PostConstruct + +@Singleton +@TestAnn +open class MyBean(env: io.micronaut.context.env.Environment) { + + @Inject lateinit var env: io.micronaut.context.env.Environment + + var invoked = 0 + + open fun test() { + } + + @PostConstruct + fun init() { + println("INVOKED POST CONSTRUCT") + invoked++ + } +} + +@io.micronaut.context.annotation.Factory +class MyFactory { + + @TestAnn + @Singleton + fun test(env: io.micronaut.context.env.Environment): MyOtherBean { + return MyOtherBean() + } +} + +class MyOtherBean + +@Retention +@Target(AnnotationTarget.FUNCTION, AnnotationTarget.CLASS) +@InterceptorBinding(kind=InterceptorKind.POST_CONSTRUCT) +@InterceptorBinding(kind=InterceptorKind.PRE_DESTROY) +annotation class TestAnn + +@Singleton +@InterceptorBean(TestAnn::class) +class TestInterceptor: MethodInterceptor { + var invoked = 0 + + override fun intercept(context: MethodInvocationContext): Any? { + invoked++ + return context.proceed() + } +} + +@Singleton +@InterceptorBinding(value=TestAnn::class, kind=InterceptorKind.POST_CONSTRUCT) +class PostConstructTestInterceptor: MethodInterceptor { + var invoked = 0 + + override fun intercept(context: MethodInvocationContext): Any? { + invoked++ + return context.proceed() + } +} + +@Singleton +@InterceptorBinding(value=TestAnn::class, kind=InterceptorKind.PRE_DESTROY) +class PreDestroyTestInterceptor: MethodInterceptor { + var invoked = 0 + + override fun intercept(context: MethodInvocationContext): Any? { + invoked++ + return context.proceed() + } +} + +@Singleton +class AnotherInterceptor: Interceptor { + var invoked = 0 + override fun intercept(context: InvocationContext): Any? { + invoked++ + return context.proceed() + } +} +""") + when: + def interceptor = getBean(context, 'annbinding1.TestInterceptor') + def constructorInterceptor = getBean(context, 'annbinding1.PostConstructTestInterceptor') + def destroyInterceptor = getBean(context, 'annbinding1.PreDestroyTestInterceptor') + def anotherInterceptor = getBean(context, 'annbinding1.AnotherInterceptor') + + then: + !interceptor.invoked + !anotherInterceptor.invoked + !constructorInterceptor.invoked + + when:"A bean that featuring post construct injection is instantiated" + def instance = getBean(context, 'annbinding1.MyBean') + + then:"The interceptors that apply to post construction are invoked" + interceptor.invoked == 1 + instance.invoked == 1 + constructorInterceptor.invoked == 1 + anotherInterceptor.invoked == 0 + destroyInterceptor.invoked == 0 + + + when:"A method with interception is invoked" + instance.test() + + then:"the methods interceptor are invoked" + interceptor.invoked == 1 + constructorInterceptor.invoked == 1 + anotherInterceptor.invoked == 0 + + + when:"A bean that is created from a factory is instantiated" + def factoryCreatedInstance = getBean(context, 'annbinding1.MyOtherBean') + + then:"post construct interceptors are invoked for the created instance" + interceptor.invoked == 2 + constructorInterceptor.invoked == 2 + anotherInterceptor.invoked == 0 + + when: + context.stop() + + then: + interceptor.invoked == 4 + constructorInterceptor.invoked == 2 + anotherInterceptor.invoked == 0 + destroyInterceptor.invoked == 2 + } +} diff --git a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/compile/ValidatedNonBeanSpec.groovy b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/compile/ValidatedNonBeanSpec.groovy new file mode 100644 index 00000000000..2a60ee60047 --- /dev/null +++ b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/compile/ValidatedNonBeanSpec.groovy @@ -0,0 +1,33 @@ +package io.micronaut.kotlin.processing.aop.compile + +import io.micronaut.inject.BeanDefinition +import spock.lang.Specification +import static io.micronaut.annotation.processing.test.KotlinCompiler.* + +class ValidatedNonBeanSpec extends Specification { + + void "test a class with only a validation annotation is not a bean"() { + when: + BeanDefinition beanDefinition = buildBeanDefinition("test.DefaultContract", """ +package test + +import javax.validation.constraints.NotNull +import io.micronaut.context.annotation.* +import jakarta.inject.Singleton + +class DefaultContract: Contract { + + override fun parseLong(@NotNull sequence: CharSequence): Long { + return 0L + } +} + +interface Contract { + fun parseLong(@NotNull sequence: CharSequence): Long +} + +""") + then: + beanDefinition == null + } +} diff --git a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/factory/AdviceDefinedOnConcreteClassFactorySpec.groovy b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/factory/AdviceDefinedOnConcreteClassFactorySpec.groovy new file mode 100644 index 00000000000..e2978af1e36 --- /dev/null +++ b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/factory/AdviceDefinedOnConcreteClassFactorySpec.groovy @@ -0,0 +1,76 @@ +package io.micronaut.kotlin.processing.aop.factory + +import io.micronaut.aop.Intercepted +import io.micronaut.context.BeanContext +import io.micronaut.context.DefaultBeanContext +import io.micronaut.inject.qualifiers.Qualifiers +import io.micronaut.kotlin.processing.aop.simple.CovariantClass +import spock.lang.Specification +import spock.lang.Unroll + +class AdviceDefinedOnConcreteClassFactorySpec extends Specification { + + @Unroll + void "test AOP method invocation @Named bean for method #method"() { + given: + BeanContext beanContext = new DefaultBeanContext().start() + ConcreteClass foo = beanContext.getBean(ConcreteClass, Qualifiers.byName("another")) + + expect: + foo instanceof Intercepted + args.isEmpty() ? foo."$method"() : foo."$method"(*args) == result + + where: + method | args | result + 'test' | ['test'] | "Name is changed" // test for single string arg + 'test' | ['test', 10] | "Name is changed and age is 10" // test for multiple args, one primitive + 'test' | [] | "noargs" // test for no args + 'testVoid' | ['test'] | null // test for void return type + 'testVoid' | ['test', 10] | null // test for void return type + 'testBoolean' | ['test'] | true // test for boolean return type + 'testBoolean' | ['test', 10] | true // test for boolean return type + 'testInt' | ['test'] | 1 // test for int return type + 'testShort' | ['test'] | 1 // test for short return type + 'testChar' | ['test'] | 1 // test for char return type + 'testByte' | ['test'] | 1 // test for byte return type + 'testFloat' | ['test'] | 1 // test for float return type + 'testDouble' | ['test'] | 1 // test for double return type + 'testByteArray' | ['test', 'test'.bytes] | 'test'.bytes // test for byte array + 'testGenericsWithExtends' | ['test', 10] | 'Name is changed' // test for generics + 'testGenericsFromType' | ['test', 10] | 'Name is changed' // test for generics + 'testListWithWildCardIn' | ['test', new CovariantClass<>()] | new CovariantClass<>('changed') // test for generics + 'testListWithWildCardOut' | ['test', new CovariantClass<>()] | new CovariantClass<>('changed') // test for generics + } + + @Unroll + void "test AOP method invocation for method #method"() { + given: + BeanContext beanContext = new DefaultBeanContext().start() + ConcreteClass foo = beanContext.getBean(ConcreteClass) + + expect: + foo instanceof Intercepted + args.isEmpty() ? foo."$method"() : foo."$method"(*args) == result + + where: + method | args | result + 'test' | ['test'] | "Name is changed" // test for single string arg + 'test' | ['test', 10] | "Name is changed and age is 10" // test for multiple args, one primitive + 'test' | [] | "noargs" // test for no args + 'testVoid' | ['test'] | null // test for void return type + 'testVoid' | ['test', 10] | null // test for void return type + 'testBoolean' | ['test'] | true // test for boolean return type + 'testBoolean' | ['test', 10] | true // test for boolean return type + 'testInt' | ['test'] | 1 // test for int return type + 'testShort' | ['test'] | 1 // test for short return type + 'testChar' | ['test'] | 1 // test for char return type + 'testByte' | ['test'] | 1 // test for byte return type + 'testFloat' | ['test'] | 1 // test for float return type + 'testDouble' | ['test'] | 1 // test for double return type + 'testByteArray' | ['test', 'test'.bytes] | 'test'.bytes // test for byte array + 'testGenericsWithExtends' | ['test', 10] | 'Name is changed' // test for generics + 'testGenericsFromType' | ['test', 10] | 'Name is changed' // test for generics + 'testListWithWildCardIn' | ['test', new CovariantClass<>()] | new CovariantClass<>('changed') // test for generics + 'testListWithWildCardOut' | ['test', new CovariantClass<>()] | new CovariantClass<>('changed') // test for generics + } +} diff --git a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/factory/AdviceDefinedOnFactorySpec.groovy b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/factory/AdviceDefinedOnFactorySpec.groovy new file mode 100644 index 00000000000..a84ac473bb7 --- /dev/null +++ b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/factory/AdviceDefinedOnFactorySpec.groovy @@ -0,0 +1,50 @@ +/* + * Copyright 2017-2019 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.kotlin.processing.aop.factory + +import io.micronaut.inject.BeanDefinition +import io.micronaut.inject.writer.BeanDefinitionVisitor +import io.micronaut.inject.writer.BeanDefinitionWriter +import spock.lang.Specification + +import static io.micronaut.annotation.processing.test.KotlinCompiler.* + +class AdviceDefinedOnFactorySpec extends Specification { + + void "test advice defined at the class level of a factory"() { + when:"Advice is defined at the class level of the factory" + BeanDefinition beanDefinition = buildBeanDefinition('test.$MyFactory' + BeanDefinitionWriter.CLASS_SUFFIX + BeanDefinitionVisitor.PROXY_SUFFIX, ''' +package test + +import io.micronaut.context.annotation.* +import io.micronaut.kotlin.processing.aop.simple.Mutating + +@Factory +@Mutating("name") +open class MyFactory { + + @Bean + @Executable + open fun myBean(@Parameter name: String): String { + return name + } +} + +''') + then:"The methods of the factory have AOP advice applied, but not the created beans" + beanDefinition.executableMethods.size() == 1 + } +} diff --git a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/factory/AdviceDefinedOnInterfaceFactorySpec.groovy b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/factory/AdviceDefinedOnInterfaceFactorySpec.groovy new file mode 100644 index 00000000000..a3146f90715 --- /dev/null +++ b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/factory/AdviceDefinedOnInterfaceFactorySpec.groovy @@ -0,0 +1,121 @@ +package io.micronaut.kotlin.processing.aop.factory + +import io.micronaut.aop.Intercepted +import io.micronaut.context.BeanContext +import io.micronaut.context.DefaultBeanContext +import io.micronaut.core.reflect.ReflectionUtils +import io.micronaut.inject.BeanDefinition +import io.micronaut.inject.qualifiers.Qualifiers +import io.micronaut.kotlin.processing.aop.simple.CovariantClass +import org.hibernate.SessionFactory +import spock.lang.Specification +import spock.lang.Unroll + +class AdviceDefinedOnInterfaceFactorySpec extends Specification { + @Unroll + void "test AOP method invocation @Named bean for method #method"() { + given: + BeanContext beanContext = new DefaultBeanContext().start() + InterfaceClass foo = beanContext.getBean(InterfaceClass, Qualifiers.byName("another")) + + expect: + args.isEmpty() ? foo."$method"() : foo."$method"(*args) == result + + where: + method | args | result + 'test' | ['test'] | "Name is changed" // test for single string arg + 'test' | ['test', 10] | "Name is changed and age is 10" // test for multiple args, one primitive + 'test' | [] | "noargs" // test for no args + 'testVoid' | ['test'] | null // test for void return type + 'testVoid' | ['test', 10] | null // test for void return type + 'testBoolean' | ['test'] | true // test for boolean return type + 'testBoolean' | ['test', 10] | true // test for boolean return type + 'testInt' | ['test'] | 1 // test for int return type + 'testShort' | ['test'] | 1 // test for short return type + 'testChar' | ['test'] | 1 // test for char return type + 'testByte' | ['test'] | 1 // test for byte return type + 'testFloat' | ['test'] | 1 // test for float return type + 'testDouble' | ['test'] | 1 // test for double return type + 'testByteArray' | ['test', 'test'.bytes] | 'test'.bytes // test for byte array + 'testGenericsWithExtends' | ['test', 10] | 'Name is changed' // test for generics + 'testGenericsFromType' | ['test', 10] | 'Name is changed' // test for generics + 'testListWithWildCardIn' | ['test', new CovariantClass<>()] | new CovariantClass<>('changed') // test for generics + 'testListWithWildCardOut' | ['test', new CovariantClass<>()] | new CovariantClass<>('changed') // test for generics + } + + @Unroll + void "test AOP method invocation for method #method"() { + given: + BeanContext beanContext = new DefaultBeanContext().start() + InterfaceClass foo = beanContext.getBean(InterfaceClass) + + expect: + args.isEmpty() ? foo."$method"() : foo."$method"(*args) == result + + where: + method | args | result + 'test' | ['test'] | "Name is changed" // test for single string arg + 'test' | ['test', 10] | "Name is changed and age is 10" // test for multiple args, one primitive + 'test' | [] | "noargs" // test for no args + 'testVoid' | ['test'] | null // test for void return type + 'testVoid' | ['test', 10] | null // test for void return type + 'testBoolean' | ['test'] | true // test for boolean return type + 'testBoolean' | ['test', 10] | true // test for boolean return type + 'testInt' | ['test'] | 1 // test for int return type + 'testShort' | ['test'] | 1 // test for short return type + 'testChar' | ['test'] | 1 // test for char return type + 'testByte' | ['test'] | 1 // test for byte return type + 'testFloat' | ['test'] | 1 // test for float return type + 'testDouble' | ['test'] | 1 // test for double return type + 'testByteArray' | ['test', 'test'.bytes] | 'test'.bytes // test for byte array + 'testGenericsWithExtends' | ['test', 10] | 'Name is changed' // test for generics + 'testGenericsFromType' | ['test', 10] | 'Name is changed' // test for generics + 'testListWithWildCardIn' | ['test', new CovariantClass<>()] | new CovariantClass<>('changed') // test for generics + 'testListWithWildCardOut' | ['test', new CovariantClass<>()] | new CovariantClass<>('changed') // test for generics + } + + + void "test session factory proxy"() { + given: + BeanContext beanContext = new DefaultBeanContext().start() + + when: + BeanDefinition beanDefinition = beanContext.findBeanDefinition(SessionFactory).get() + SessionFactory sessionFactory = beanContext.getBean(SessionFactory) + + // make sure all the public method are implemented + def clazz = sessionFactory.getClass() + int count = 1 // proxy methods + def interfaces = ReflectionUtils.getAllInterfaces(SessionFactory.class) + interfaces += SessionFactory.class + for(i in interfaces) { + for(m in i.declaredMethods) { + count++ + assert clazz.getDeclaredMethod(m.name, m.parameterTypes) + } + } + + then: + sessionFactory instanceof Intercepted + } + + void "test AOP setup"() { + given: + BeanContext beanContext = new DefaultBeanContext().start() + + + when: + InterfaceClass foo = beanContext.getBean(InterfaceClass) + InterfaceClass another = beanContext.getBean(InterfaceClass, Qualifiers.byName("another")) + + then: + foo instanceof Intercepted + another instanceof Intercepted + // should not be a reflection based method + foo.test("test") == "Name is changed" + + cleanup: + beanContext.close() + + } +} diff --git a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/hotswap/ProxyHotswapSpec.groovy b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/hotswap/ProxyHotswapSpec.groovy new file mode 100644 index 00000000000..6122a439ed9 --- /dev/null +++ b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/hotswap/ProxyHotswapSpec.groovy @@ -0,0 +1,29 @@ +package io.micronaut.kotlin.processing.aop.hotswap + +import io.micronaut.aop.HotSwappableInterceptedProxy +import io.micronaut.context.BeanContext +import io.micronaut.context.DefaultBeanContext +import spock.lang.Specification + +class ProxyHotswapSpec extends Specification { + + void "test AOP setup attributes"() { + given: + BeanContext beanContext = new DefaultBeanContext().start() + def newInstance = new HotswappableProxyingClass() + + when: + HotswappableProxyingClass foo = beanContext.getBean(HotswappableProxyingClass) + then: + foo instanceof HotSwappableInterceptedProxy + foo.interceptedTarget().getClass() == HotswappableProxyingClass + foo.test("test") == "Name is changed" + foo.test2("test") == "Name is test" + foo.interceptedTarget().invocationCount == 2 + + foo.swap(newInstance) + foo.interceptedTarget().invocationCount == 0 + foo.interceptedTarget() != foo + foo.interceptedTarget().is(newInstance) + } +} diff --git a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/introduction/AbstractClassIntroductionAdviceSpec.groovy b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/introduction/AbstractClassIntroductionAdviceSpec.groovy new file mode 100644 index 00000000000..5230cf95b57 --- /dev/null +++ b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/introduction/AbstractClassIntroductionAdviceSpec.groovy @@ -0,0 +1,28 @@ +package io.micronaut.kotlin.processing.aop.introduction + +import io.micronaut.aop.Intercepted +import io.micronaut.context.BeanContext +import io.micronaut.context.DefaultBeanContext +import spock.lang.Specification +import spock.lang.Unroll + +class AbstractClassIntroductionAdviceSpec extends Specification { + + @Unroll + void "test AOP method invocation @Named bean for method #method"() { + given: + BeanContext beanContext = new DefaultBeanContext().start() + AbstractClass foo = beanContext.getBean(AbstractClass) + + expect: + foo instanceof Intercepted + args.isEmpty() ? foo."$method"() : foo."$method"(*args) == result + + where: + method | args | result + 'test' | ['test'] | "changed" // test for single string arg + 'nonAbstract' | ['test'] | "changed" // test for single string arg + 'test' | ['test', 10] | "changed" // test for multiple args, one primitive + 'testGenericsFromType' | ['test', 10] | "changed" // test for multiple args, one primitive + } +} diff --git a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/introduction/InterfaceIntroductionAdviceSpec.groovy b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/introduction/InterfaceIntroductionAdviceSpec.groovy new file mode 100644 index 00000000000..b2b5a33b198 --- /dev/null +++ b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/introduction/InterfaceIntroductionAdviceSpec.groovy @@ -0,0 +1,55 @@ +package io.micronaut.kotlin.processing.aop.introduction + +import io.micronaut.aop.Intercepted +import io.micronaut.context.BeanContext +import io.micronaut.context.DefaultBeanContext +import spock.lang.Specification +import spock.lang.Unroll +import static io.micronaut.annotation.processing.test.KotlinCompiler.* + +class InterfaceIntroductionAdviceSpec extends Specification { + + @Unroll + void "test AOP method invocation @Named bean for method #method"() { + given: + BeanContext beanContext = new DefaultBeanContext().start() + InterfaceIntroductionClass foo = beanContext.getBean(InterfaceIntroductionClass) + + expect: + foo instanceof Intercepted + args.isEmpty() ? foo."$method"() : foo."$method"(*args) == result + + where: + method | args | result + 'test' | ['test'] | "changed" // test for single string arg + 'test' | ['test', 10] | "changed" // test for multiple args, one primitive + 'testGenericsFromType' | ['test', 10] | "changed" // test for multiple args, one primitive + } + + void "test injecting an introduction advice with generics"() { + BeanContext beanContext = new DefaultBeanContext().start() + + when: + InjectParentInterface foo = beanContext.getBean(InjectParentInterface) + + then: + noExceptionThrown() + + cleanup: + beanContext.close() + } + + void "test typeArgumentsMap are created for introduction advice"() { + def definition = buildBeanDefinition("test.Test\$Intercepted", """ +package test + +import io.micronaut.kotlin.processing.aop.introduction.* + +@Stub +interface Test: ParentInterface> +""") + + expect: + !definition.getTypeArguments(ParentInterface).isEmpty() + } +} diff --git a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/introduction/IntroductionAdviceWithNewInterfaceSpec.groovy b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/introduction/IntroductionAdviceWithNewInterfaceSpec.groovy new file mode 100644 index 00000000000..8578aba07f7 --- /dev/null +++ b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/introduction/IntroductionAdviceWithNewInterfaceSpec.groovy @@ -0,0 +1,281 @@ +package io.micronaut.kotlin.processing.aop.introduction + +import io.micronaut.context.BeanContext +import io.micronaut.context.DefaultBeanContext +import io.micronaut.context.event.ApplicationEventListener +import io.micronaut.inject.BeanDefinition +import io.micronaut.inject.InstantiatableBeanDefinition +import io.micronaut.inject.writer.BeanDefinitionVisitor +import spock.lang.Specification +import static io.micronaut.annotation.processing.test.KotlinCompiler.* + +class IntroductionAdviceWithNewInterfaceSpec extends Specification { + + void "test configuration advice with Kotlin properties"() { + when: + def context = buildContext(''' +package test + +import io.micronaut.kotlin.processing.aop.introduction.* +import io.micronaut.context.annotation.* +import io.micronaut.context.annotation.ConfigurationProperties + +@ConfigurationProperties("test") +interface MyBean { + val foo : String +} + +''', true, ['test.foo':'test']) + def bean = getBean(context, 'test.MyBean') + + then: + bean.foo == 'test' + + cleanup: + context.close() + } + + void "test configuration advice with Kotlin properties inner classes"() { + when: + def context = buildContext(''' +package test + +import io.micronaut.kotlin.processing.aop.introduction.* +import io.micronaut.context.annotation.* +import io.micronaut.context.annotation.ConfigurationProperties + +@ConfigurationProperties("test") +interface MyBean { + val foo : String + + @ConfigurationProperties("more") + interface InnerBean { + val foo : String + } +} + +''', true, ['test.more.foo':'test']) + def bean = getBean(context, 'test.MyBean$InnerBean') + + then: + bean.foo == 'test' + + cleanup: + context.close() + } + + void "test introduction advice with Kotlin properties"() { + when: + def context = buildContext(''' +package test + +import io.micronaut.kotlin.processing.aop.introduction.* +import io.micronaut.context.annotation.* + +@Stub("test") +interface MyBean { + val foo : String +} + +''', true) + def bean = getBean(context, 'test.MyBean') + + then: + bean.foo == 'test' + + cleanup: + context.close() + } + + void "test introduction advice with primitive generics"() { + when: + def context = buildContext( ''' +package test + +import io.micronaut.kotlin.processing.aop.introduction.* +import javax.validation.constraints.NotNull + +@RepoDef +interface MyRepo : DeleteByIdCrudRepo { + + override fun deleteById(@NotNull id: Int) +} + + +''', true) + + def bean = + getBean(context, 'test.MyRepo') + then: + bean != null + } + + void "test that it is possible for @Introduction advice to implement additional interfaces on concrete classes"() { + when: + BeanDefinition beanDefinition = buildBeanDefinition('test.MyBean' + BeanDefinitionVisitor.PROXY_SUFFIX, ''' +package test + +import io.micronaut.kotlin.processing.aop.introduction.* +import io.micronaut.context.annotation.* + +@ListenerAdvice +@Stub +@jakarta.inject.Singleton +open class MyBean { + + @Executable + fun getFoo() = "good" +} + +''') + then: + !beanDefinition.isAbstract() + beanDefinition != null + ApplicationEventListener.class.isAssignableFrom(beanDefinition.beanType) + beanDefinition.injectedFields.size() == 0 + beanDefinition.executableMethods.size() == 2 + beanDefinition.findMethod("getFoo").isPresent() + beanDefinition.findMethod("onApplicationEvent", Object).isPresent() + + when: + def context = new DefaultBeanContext() + context.start() + def instance = ((InstantiatableBeanDefinition)beanDefinition).instantiate(context) + ListenerAdviceInterceptor listenerAdviceInterceptor= context.getBean(ListenerAdviceInterceptor) + listenerAdviceInterceptor.recievedMessages.clear() + then:"the methods are invocable" + listenerAdviceInterceptor.recievedMessages.isEmpty() + instance.getFoo() == "good" + instance.onApplicationEvent(new Object()) == null + !listenerAdviceInterceptor.recievedMessages.isEmpty() + + } + + void "test that it is possible for @Introduction advice to implement additional interfaces on abstract classes"() { + when: + BeanDefinition beanDefinition = buildBeanDefinition('test.MyBean' + BeanDefinitionVisitor.PROXY_SUFFIX, ''' +package test + +import io.micronaut.kotlin.processing.aop.introduction.* +import io.micronaut.context.annotation.* + +@ListenerAdvice +@Stub +@jakarta.inject.Singleton +abstract class MyBean { + + @Executable + fun getFoo() = "good" +} + +''') + then: + !beanDefinition.isAbstract() + beanDefinition != null + ApplicationEventListener.class.isAssignableFrom(beanDefinition.beanType) + beanDefinition.injectedFields.size() == 0 + beanDefinition.executableMethods.size() == 2 + beanDefinition.findMethod("getFoo").isPresent() + beanDefinition.findMethod("onApplicationEvent", Object).isPresent() + + when: + def context = new DefaultBeanContext() + context.start() + def instance = ((InstantiatableBeanDefinition)beanDefinition).instantiate(context) + ListenerAdviceInterceptor listenerAdviceInterceptor= context.getBean(ListenerAdviceInterceptor) + listenerAdviceInterceptor.recievedMessages.clear() + then:"the methods are invocable" + listenerAdviceInterceptor.recievedMessages.isEmpty() + instance.getFoo() == "good" + instance.onApplicationEvent(new Object()) == null + !listenerAdviceInterceptor.recievedMessages.isEmpty() + } + + void "test that it is possible for @Introduction advice to implement additional interfaces on interfaces"() { + when: + BeanDefinition beanDefinition = buildBeanDefinition('test.MyBean' + BeanDefinitionVisitor.PROXY_SUFFIX, ''' +package test + +import io.micronaut.kotlin.processing.aop.introduction.* +import io.micronaut.context.annotation.* + +@ListenerAdvice +@Stub +@jakarta.inject.Singleton +interface MyBean { + + @Executable + fun getBar(): String + + @Executable + fun getFoo() = "good" +} + +''') + then: + !beanDefinition.isAbstract() + beanDefinition != null + ApplicationEventListener.class.isAssignableFrom(beanDefinition.beanType) + beanDefinition.injectedFields.size() == 0 + beanDefinition.executableMethods.size() == 3 + beanDefinition.findMethod("getBar").isPresent() + beanDefinition.findMethod("onApplicationEvent", Object).isPresent() + + when: + def context = BeanContext.run() + def instance = ((InstantiatableBeanDefinition)beanDefinition).instantiate(context) + ListenerAdviceInterceptor listenerAdviceInterceptor= context.getBean(ListenerAdviceInterceptor) + listenerAdviceInterceptor.recievedMessages.clear() + + then:"the methods are invocable" + listenerAdviceInterceptor.recievedMessages.isEmpty() + instance.getFoo() == "good" + instance.getBar() == null + instance.onApplicationEvent(new Object()) == null + !listenerAdviceInterceptor.recievedMessages.isEmpty() + listenerAdviceInterceptor.recievedMessages.size() == 1 + + cleanup: + context.close() + } + + void "test an interface with non overriding but subclass return type method"() { + when: + BeanDefinition beanDefinition = buildBeanDefinition('test.MyBean' + BeanDefinitionVisitor.PROXY_SUFFIX, ''' +package test + +import io.micronaut.kotlin.processing.aop.introduction.* +import io.micronaut.context.annotation.* + +@Stub +@jakarta.inject.Singleton +interface MyBean: GenericInterface, SpecificInterface + +open class Generic + +class Specific: Generic() + +interface GenericInterface { + fun getObject(): Generic +} + +interface SpecificInterface { + fun getObject(): Specific +} +''') + + then: + noExceptionThrown() + beanDefinition != null + + when: + def context = new DefaultBeanContext() + context.start() + def instance = ((InstantiatableBeanDefinition)beanDefinition).instantiate(context) + + then: + //I ended up going this route because actually calling the methods here would be relying on + //having the target interface in the bytecode of the test + instance.$proxyMethods.length == 2 + } +} diff --git a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/introduction/IntroductionOnConcreteClassSpec.groovy b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/introduction/IntroductionOnConcreteClassSpec.groovy new file mode 100644 index 00000000000..1368f7a1ce2 --- /dev/null +++ b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/introduction/IntroductionOnConcreteClassSpec.groovy @@ -0,0 +1,32 @@ +package io.micronaut.kotlin.processing.aop.introduction + +import io.micronaut.context.ApplicationContext +import io.micronaut.context.event.ApplicationEventListener +import io.micronaut.context.event.StartupEvent +import spock.lang.AutoCleanup +import spock.lang.Shared +import spock.lang.Specification + +class IntroductionOnConcreteClassSpec extends Specification { + + @Shared @AutoCleanup ApplicationContext applicationContext = ApplicationContext.run() + + void "test introduction of new interface on concrete class"() { + when: + ConcreteClass cc = applicationContext.getBean(ConcreteClass) + def listenerAdviceInterceptor = applicationContext.getBean(ListenerAdviceInterceptor) + + then: + cc instanceof ApplicationEventListener + + when: + def event = new StartupEvent(applicationContext) + cc.onApplicationEvent(event) + + then: + listenerAdviceInterceptor.recievedMessages.contains(event) + + cleanup: + listenerAdviceInterceptor.recievedMessages.clear() + } +} diff --git a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/introduction/MappedIntroductionOnConcreteClassSpec.groovy b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/introduction/MappedIntroductionOnConcreteClassSpec.groovy new file mode 100644 index 00000000000..5afef055eef --- /dev/null +++ b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/introduction/MappedIntroductionOnConcreteClassSpec.groovy @@ -0,0 +1,43 @@ +package io.micronaut.kotlin.processing.aop.introduction + +import io.micronaut.context.ApplicationContext +import io.micronaut.context.event.ApplicationEventListener +import io.micronaut.context.event.StartupEvent +import spock.lang.Specification + +import static io.micronaut.annotation.processing.test.KotlinCompiler.* + +class MappedIntroductionOnConcreteClassSpec extends Specification { + + void "test mapped introduction of new interface on concrete class"() { + given: + ApplicationContext applicationContext = buildContext(''' +package test + +import jakarta.inject.Singleton + +@io.micronaut.kotlin.processing.aop.introduction.ListenerAdviceMarker +@Singleton +open class MyBeanWithMappedIntroduction +''') + applicationContext.registerSingleton(new ListenerAdviceInterceptor()) + + when: + def beanClass = applicationContext.classLoader.loadClass('test.MyBeanWithMappedIntroduction') + def cc = applicationContext.getBean(beanClass) + def listenerAdviceInterceptor = applicationContext.getBean(ListenerAdviceInterceptor) + + then: + cc instanceof ApplicationEventListener + + when: + def event = new StartupEvent(applicationContext) + cc.onApplicationEvent(event) + + then: + listenerAdviceInterceptor.recievedMessages.contains(event) + + cleanup: + listenerAdviceInterceptor.recievedMessages.clear() + } +} diff --git a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/introduction/MyRepoIntroductionSpec.groovy b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/introduction/MyRepoIntroductionSpec.groovy new file mode 100644 index 00000000000..45b95ebdf0a --- /dev/null +++ b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/introduction/MyRepoIntroductionSpec.groovy @@ -0,0 +1,149 @@ +package io.micronaut.kotlin.processing.aop.introduction + +import io.micronaut.context.ApplicationContext +import spock.lang.AutoCleanup +import spock.lang.Shared +import spock.lang.Specification + +import java.util.stream.Collectors + +class MyRepoIntroductionSpec extends Specification { + + void "test generated introduction methods"() { + when: + def applicationContext = ApplicationContext.run() + def bean = applicationContext.getBean(MyRepo) + def interceptorDeclaredMethods = Arrays.stream(bean.getClass().getMethods()).filter(m -> m.getDeclaringClass() == bean.getClass()).collect(Collectors.toList()) + def repoDeclaredMethods = Arrays.stream(MyRepo.class.getMethods()).filter(m -> m.getDeclaringClass() == MyRepo.class).collect(Collectors.toList()) + def myRepoIntroducer = applicationContext.getBean(MyRepoIntroducer) + + then: + repoDeclaredMethods.size() == 3 + interceptorDeclaredMethods.size() == 4 + bean.getClass().getName().contains("Intercepted") + myRepoIntroducer.executableMethods.isEmpty() + + when: + bean.aBefore() + bean.xAfter() + bean.findAll() + + then: + myRepoIntroducer.executableMethods.size() == 3 + myRepoIntroducer.executableMethods.contains repoDeclaredMethods.find { method -> method.name == "aBefore" } + myRepoIntroducer.executableMethods.contains repoDeclaredMethods.find { method -> method.name == "xAfter" } + myRepoIntroducer.executableMethods.contains repoDeclaredMethods.find { method -> method.name == "findAll" && method.returnType == List.class } + + cleanup: + applicationContext.close() + } + + void "test interface overridden method"() { + when: + def applicationContext = ApplicationContext.run() + def bean = applicationContext.getBean(CustomCrudRepo) + def beanDef = applicationContext.getBeanDefinition(CustomCrudRepo) + def findByIdMethods = beanDef.getExecutableMethods().findAll(m -> m.getName() == "findById") + def myRepoIntroducer = applicationContext.getBean(MyRepoIntroducer) + + then: + myRepoIntroducer.executableMethods.size() == 0 + findByIdMethods.size() == 1 + findByIdMethods[0].hasAnnotation(Marker) + + when: + bean.findById(111) + + then: + myRepoIntroducer.executableMethods.size() == 1 + myRepoIntroducer.executableMethods.clear() + + when: + CrudRepo crudRepo = bean + crudRepo.findById(111) + + then: + myRepoIntroducer.executableMethods.size() == 1 + + cleanup: + applicationContext.close() + } + + void "test interface abstract overridden method"() { + when: + def applicationContext = ApplicationContext.run() + def bean = applicationContext.getBean(AbstractCustomCrudRepo) + def beanDef = applicationContext.getBeanDefinition(AbstractCustomCrudRepo) + def findByIdMethods = beanDef.getExecutableMethods().findAll(m -> m.getName() == "findById") + def myRepoIntroducer = applicationContext.getBean(MyRepoIntroducer) + + then: + myRepoIntroducer.executableMethods.size() == 0 + findByIdMethods.size() == 1 + findByIdMethods[0].hasAnnotation(Marker) + + when: + bean.findById(111) + + then: + myRepoIntroducer.executableMethods.size() == 1 + myRepoIntroducer.executableMethods.clear() + + when: + CrudRepo crudRepo = bean + crudRepo.findById(111) + + then: + myRepoIntroducer.executableMethods.size() == 1 + + cleanup: + applicationContext.close() + } + + void "test abstract overridden method"() { + when: + def applicationContext = ApplicationContext.run() + def bean = applicationContext.getBean(AbstractCustomAbstractCrudRepo) + def beanDef = applicationContext.getBeanDefinition(AbstractCustomAbstractCrudRepo) + def findByIdMethods = beanDef.getExecutableMethods().findAll(m -> m.getName() == "findById") + def myRepoIntroducer = applicationContext.getBean(MyRepoIntroducer) + + then: + myRepoIntroducer.executableMethods.size() == 0 + findByIdMethods.size() == 1 + findByIdMethods[0].hasAnnotation(Marker) + + when: + bean.findById(111) + + then: + myRepoIntroducer.executableMethods.size() == 1 + myRepoIntroducer.executableMethods.clear() + + when: + AbstractCrudRepo crudRepo = bean + crudRepo.findById(111) + + then: + myRepoIntroducer.executableMethods.size() == 1 + + cleanup: + applicationContext.close() + } + + void "test overridden void methods"() { + when: + def applicationContext = ApplicationContext.run() + def bean = applicationContext.getBean(MyRepo2) + def myRepoIntroducer = applicationContext.getBean(MyRepoIntroducer) + bean.deleteById(1) + + then: + myRepoIntroducer.executableMethods.size() == 1 + myRepoIntroducer.executableMethods.clear() + + cleanup: + applicationContext.close() + } + +} diff --git a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/introduction/delegation/DelegatingIntroductionAdviceSpec.groovy b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/introduction/delegation/DelegatingIntroductionAdviceSpec.groovy new file mode 100644 index 00000000000..f51963ab956 --- /dev/null +++ b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/introduction/delegation/DelegatingIntroductionAdviceSpec.groovy @@ -0,0 +1,20 @@ +package io.micronaut.kotlin.processing.aop.introduction.delegation + +import io.micronaut.context.ApplicationContext +import spock.lang.AutoCleanup +import spock.lang.Shared +import spock.lang.Specification + +class DelegatingIntroductionAdviceSpec extends Specification { + + @Shared @AutoCleanup ApplicationContext context = ApplicationContext.run() + + void "test that delegation advice works"() { + given: + DelegatingIntroduced delegating = (DelegatingIntroduced)context.getBean(Delegating) + + expect: + delegating.test2() == 'good' + delegating.test() == 'good' + } +} diff --git a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/introduction/with_around/IntroductionInnerInterfaceSpec.groovy b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/introduction/with_around/IntroductionInnerInterfaceSpec.groovy new file mode 100644 index 00000000000..306c19be59e --- /dev/null +++ b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/introduction/with_around/IntroductionInnerInterfaceSpec.groovy @@ -0,0 +1,50 @@ +package io.micronaut.kotlin.processing.aop.introduction.with_around + +import io.micronaut.context.ApplicationContext +import io.micronaut.inject.BeanDefinition +import io.micronaut.inject.InstantiatableBeanDefinition +import io.micronaut.inject.writer.BeanDefinitionVisitor +import spock.lang.Specification + +import static io.micronaut.annotation.processing.test.KotlinCompiler.* + +class IntroductionInnerInterfaceSpec extends Specification { + + void "test an inner class passed to @Introduction(interfaces = "() { + when: + BeanDefinition beanDefinition = buildBeanDefinition("test.MyBean" + BeanDefinitionVisitor.PROXY_SUFFIX, """ +package test + +import io.micronaut.aop.Around +import io.micronaut.aop.Introduction +import io.micronaut.context.annotation.Type +import jakarta.inject.Singleton + +@Around +@Type(io.micronaut.kotlin.processing.aop.introduction.with_around.ObservableInterceptor::class) +@Introduction(interfaces = [ObservableUI.Inner::class]) +@Retention +annotation class ObservableUI { + + interface Inner { + fun hello(): String + } +} + +@Singleton +@ObservableUI +open class MyBean +""") + + then: + noExceptionThrown() + beanDefinition != null + + when: + def context = ApplicationContext.run() + def instance = ((InstantiatableBeanDefinition) beanDefinition).instantiate(context) + + then: + instance.hello() == "World" + } +} diff --git a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/introduction/with_around/IntroductionWithAroundOnConcreteClassSpec.groovy b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/introduction/with_around/IntroductionWithAroundOnConcreteClassSpec.groovy new file mode 100644 index 00000000000..724310bc232 --- /dev/null +++ b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/introduction/with_around/IntroductionWithAroundOnConcreteClassSpec.groovy @@ -0,0 +1,135 @@ +package io.micronaut.kotlin.processing.aop.introduction.with_around + +import io.micronaut.context.ApplicationContext +import io.micronaut.core.beans.BeanIntrospection +import io.micronaut.inject.ExecutableMethod +import spock.lang.AutoCleanup +import spock.lang.Shared +import spock.lang.Specification +import spock.lang.Unroll + +import static io.micronaut.annotation.processing.test.KotlinCompiler.* + +class IntroductionWithAroundOnConcreteClassSpec extends Specification { + + @Shared + @AutoCleanup + ApplicationContext applicationContext = ApplicationContext.run() + + void "test introduction with around compile"() { + given: + def context = buildContext(''' +package aroundwithintro + +import io.micronaut.kotlin.processing.aop.introduction.with_around.ProxyIntroductionAndAroundOneAnnotation + +@ProxyIntroductionAndAroundOneAnnotation +open class Test +''', true) + + expect: + getBean(context, 'aroundwithintro.Test') + + cleanup: + context.close() + } + + @Unroll + void "test introduction with around for #clazz"(Class clazz) { + when: + def beanDefinition = applicationContext.getBeanDefinition(clazz, null) + def proxyTargetBeanDefinition = applicationContext.findProxyTargetBeanDefinition(clazz, null).orElse(beanDefinition) + def bean = applicationContext.getBean(beanDefinition) + + then: + bean instanceof CustomProxy + ((CustomProxy) bean).isProxy() + bean.getId() == 1 + bean.getName() == null + + when: + bean = beanDefinition != proxyTargetBeanDefinition ? applicationContext.getProxyTargetBean(clazz, null) : applicationContext.getBean(clazz, null) + + then: + bean instanceof CustomProxy + ((CustomProxy) bean).isProxy() + bean.getId() == 1 + bean.getName() == null + + and: + beanDefinition.getExecutableMethods().size() == 5 + proxyTargetBeanDefinition.getExecutableMethods().size() == 5 + + where: + clazz << [MyBean1, MyBean2, MyBean3, MyBean4, MyBean5] + } + + void "test introspected preset"(Class clazz) { + when: + def introspection = BeanIntrospection.getIntrospection(clazz) + + then: + introspection + + where: + clazz << [MyBean4, MyBean5, MyBean6] + } + + void "test executable methods count for introduction with executable"() { + when: + def clazz = MyBean7.class + def beanDefinition = applicationContext.getBeanDefinition(clazz, null) + + then: + beanDefinition.getExecutableMethods().size() == 5 + } + + void "test executable methods count for around with executable"() { + when: + def clazz = MyBean8.class + def beanDefinition = applicationContext.getBeanDefinition(clazz, null) + + then: + beanDefinition.getExecutableMethods().size() == 4 + + when: + MyBean8 myBean8 = applicationContext.getBean(clazz) + + then: + myBean8.getId() == 1L + } + + void "test a multidimensional array property"() { + when: + def clazz = MyBean9.class + def beanDefinition = applicationContext.getBeanDefinition(clazz, null) + + then: + beanDefinition.getExecutableMethods().size() == 5 + beanDefinition.findMethod("getMultidim").get().getReturnType().asArgument().getType() == String[][].class + beanDefinition.findMethod("setMultidim", String[][].class).isPresent() + beanDefinition.findMethod("getPrimitiveMultidim").get().getReturnType().asArgument().getType() == int[][].class + beanDefinition.findMethod("setPrimitiveMultidim", int[][].class).isPresent() + + when: + MyBean9 bean = applicationContext.getBean(beanDefinition) + ExecutableMethod getMultiDim = beanDefinition.findMethod('getMultidim').get() + ExecutableMethod setMultiDim = beanDefinition.findMethod('setMultidim', String[][].class).get() + ExecutableMethod getPrimitiveMultidim = beanDefinition.findMethod('getPrimitiveMultidim').get() + ExecutableMethod setPrimitiveMultidim = beanDefinition.findMethod('setPrimitiveMultidim', int[][].class).get() + + then: + getMultiDim.invoke(bean) == null + getPrimitiveMultidim.invoke(bean) == null + + when: + setMultiDim.invoke(bean, new Object[] { new String[][] { new String[] { "test" }, new String[] { "abc" } } }) + setPrimitiveMultidim.invoke(bean, new Object[] { new int[][] { new int[] { 1 }, new int[] { 2 }} }) + + then: + bean.getMultidim()[0][0] == "test" + bean.getPrimitiveMultidim()[0][0] == 1 + getMultiDim.invoke(bean)[0][0] == "test" + getPrimitiveMultidim.invoke(bean)[0][0] == 1 + } +} diff --git a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/itfce/InterfaceMethodLevelAopSpec.groovy b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/itfce/InterfaceMethodLevelAopSpec.groovy new file mode 100644 index 00000000000..ffb4b6433cc --- /dev/null +++ b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/itfce/InterfaceMethodLevelAopSpec.groovy @@ -0,0 +1,67 @@ +package io.micronaut.kotlin.processing.aop.itfce + +import io.micronaut.aop.Intercepted +import io.micronaut.context.BeanContext +import io.micronaut.context.DefaultBeanContext +import io.micronaut.kotlin.processing.aop.simple.CovariantClass +import spock.lang.Specification +import spock.lang.Unroll + +class InterfaceMethodLevelAopSpec extends Specification { + + @Unroll + void "test AOP method invocation for method #method"() { + given: + BeanContext beanContext = new DefaultBeanContext().start() + InterfaceClass foo = beanContext.getBean(InterfaceClass) + + expect: + args.isEmpty() ? foo."$method"() : foo."$method"(*args) == result + + where: + method | args | result + 'test' | ['test'] | "Name is changed" // test for single string arg + 'test' | [10] | "Age is 20" // test for single primitive + 'test' | ['test', 10] | "Name is changed and age is 10" // test for multiple args, one primitive + 'test' | [] | "noargs" // test for no args + 'testVoid' | ['test'] | null // test for void return type + 'testVoid' | ['test', 10] | null // test for void return type + 'testBoolean' | ['test'] | true // test for boolean return type + 'testBoolean' | ['test', 10] | true // test for boolean return type + 'testInt' | ['test'] | 1 // test for int return type + 'testInt' | ['test', 10] | 20 // test for int return type + 'testShort' | ['test'] | 1 // test for short return type + 'testShort' | ['test', 10] | 20 // test for short return type + 'testChar' | ['test'] | 1 // test for char return type + 'testChar' | ['test', 10] | 20 // test for char return type + 'testByte' | ['test'] | 1 // test for byte return type + 'testByte' | ['test', 10] | 20 // test for byte return type + 'testFloat' | ['test'] | 1 // test for float return type + 'testFloat' | ['test', 10] | 20 // test for float return type + 'testDouble' | ['test'] | 1 // test for double return type + 'testDouble' | ['test', 10] | 20 // test for double return type + 'testByteArray' | ['test', 'test'.bytes] | 'test'.bytes // test for byte array + 'testGenericsWithExtends' | ['test', 10] | 'Name is changed' // test for generics + 'testGenericsFromType' | ['test', 10] | 'Name is changed' // test for generics + 'testListWithWildCardIn' | ['test', new CovariantClass<>()] | new CovariantClass<>('changed') // test for generics + 'testListWithWildCardOut' | ['test', new CovariantClass<>()] | new CovariantClass<>('changed') + } + + + void "test AOP setup"() { + given: + BeanContext beanContext = new DefaultBeanContext().start() + + when: + InterfaceClass foo = beanContext.getBean(InterfaceClass) + + + then: + foo instanceof Intercepted + foo.test("test") == "Name is changed" + + cleanup: + beanContext.close() + } + +} diff --git a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/itfce/InterfaceTypeLevelSpec.groovy b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/itfce/InterfaceTypeLevelSpec.groovy new file mode 100644 index 00000000000..05bcf0e241a --- /dev/null +++ b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/itfce/InterfaceTypeLevelSpec.groovy @@ -0,0 +1,61 @@ +package io.micronaut.kotlin.processing.aop.itfce + +import io.micronaut.aop.Intercepted +import io.micronaut.context.BeanContext +import io.micronaut.context.DefaultBeanContext +import io.micronaut.kotlin.processing.aop.simple.CovariantClass +import spock.lang.Specification +import spock.lang.Unroll + +class InterfaceTypeLevelSpec extends Specification { + + @Unroll + void "test AOP method invocation for method #method"() { + given: + BeanContext beanContext = new DefaultBeanContext().start() + InterfaceTypeLevel foo = beanContext.getBean(InterfaceTypeLevel) + + expect: + args.isEmpty() ? foo."$method"() : foo."$method"(*args) == result + + where: + method | args | result + 'test' | ['test'] | "Name is changed" // test for single string arg + 'test' | ['test', 10] | "Name is changed and age is 10" // test for multiple args, one primitive + 'test' | [] | "noargs" // test for no args + 'testVoid' | ['test'] | null // test for void return type + 'testVoid' | ['test', 10] | null // test for void return type + 'testBoolean' | ['test'] | true // test for boolean return type + 'testBoolean' | ['test', 10] | true // test for boolean return type + 'testInt' | ['test'] | 1 // test for int return type + 'testShort' | ['test'] | 1 // test for short return type + 'testChar' | ['test'] | 1 // test for char return type + 'testByte' | ['test'] | 1 // test for byte return type + 'testFloat' | ['test'] | 1 // test for float return type + 'testDouble' | ['test'] | 1 // test for double return type + 'testByteArray' | ['test', 'test'.bytes] | 'test'.bytes // test for byte array + 'testGenericsWithExtends' | ['test', 10] | 'Name is changed' // test for generics + 'testGenericsFromType' | ['test', 10] | 'Name is changed' // test for generics + 'testListWithWildCardIn' | ['test', new CovariantClass<>()] | new CovariantClass<>('changed') // test for generics + 'testListWithWildCardOut' | ['test', new CovariantClass<>()] | new CovariantClass<>('changed') + } + + + void "test AOP setup"() { + given: + BeanContext beanContext = new DefaultBeanContext().start() + + when: + InterfaceTypeLevel foo = beanContext.getBean(InterfaceTypeLevel) + + + then: + foo instanceof Intercepted + foo.test("test") == "Name is changed" + + cleanup: + beanContext.close() + + } +} + diff --git a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/named/NamedAopAdviceSpec.groovy b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/named/NamedAopAdviceSpec.groovy new file mode 100644 index 00000000000..ff5c08fcb50 --- /dev/null +++ b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/named/NamedAopAdviceSpec.groovy @@ -0,0 +1,74 @@ +package io.micronaut.kotlin.processing.aop.named + +import io.micronaut.aop.Intercepted +import io.micronaut.context.ApplicationContext +import io.micronaut.inject.qualifiers.Qualifiers +import spock.lang.PendingFeature +import spock.lang.Specification + +class NamedAopAdviceSpec extends Specification { + + void "test that named beans that have AOP advice applied lookup the correct target named bean - primary included"() { + given: + def context = ApplicationContext.run( + 'aop.test.named.default': 0, + 'aop.test.named.one': 1, + 'aop.test.named.two': 2, + ) + + expect: + context.getBean(NamedInterface) instanceof Intercepted + context.getBean(NamedInterface).doStuff() == 'default' + context.getBean(NamedInterface, Qualifiers.byName("one")).doStuff() == 'one' + context.getBean(NamedInterface, Qualifiers.byName("two")).doStuff() == 'two' + context.getBeansOfType(NamedInterface).size() == 3 + context.getBeansOfType(NamedInterface).every({ it instanceof Intercepted }) + + cleanup: + context.close() + } + + void "test that named beans that have AOP advice applied lookup the correct target named bean - no primary"() { + given: + def context = ApplicationContext.run( + 'aop.test.named.one': 1, + 'aop.test.named.two': 2, + ) + + expect: + context.getBean(NamedInterface, Qualifiers.byName("one")).doStuff() == 'one' + context.getBean(NamedInterface, Qualifiers.byName("two")).doStuff() == 'two' + context.getBeansOfType(NamedInterface).size() == 2 + context.getBeansOfType(NamedInterface).every({ it instanceof Intercepted }) + + cleanup: + context.close() + } + + void "test manually named beans with AOP advice"() { + given: + def context = ApplicationContext.run() + + expect: + context.getBean(OtherInterface, Qualifiers.byName("first")).doStuff() == 'first' + context.getBean(OtherInterface, Qualifiers.byName("second")).doStuff() == 'second' + context.getBeansOfType(OtherInterface).size() == 2 + context.getBeansOfType(OtherInterface).every({ it instanceof Intercepted }) + context.getBean(OtherBean).first.doStuff() == "first" + context.getBean(OtherBean).second.doStuff() == "second" + + cleanup: + context.close() + } + + void "test named bean relying on non iterable config"() { + given: + def context = ApplicationContext.run(['other.interfaces.third': 'third']) + + expect: + context.getBean(OtherInterface, Qualifiers.byName("third")).doStuff() == 'third' + + cleanup: + context.close() + } +} diff --git a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/proxytarget/ProxyingMethodLevelAopSpec.groovy b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/proxytarget/ProxyingMethodLevelAopSpec.groovy new file mode 100644 index 00000000000..7be21bee147 --- /dev/null +++ b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/proxytarget/ProxyingMethodLevelAopSpec.groovy @@ -0,0 +1,80 @@ +package io.micronaut.kotlin.processing.aop.proxytarget + +import io.micronaut.aop.InterceptedProxy +import io.micronaut.context.BeanContext +import io.micronaut.context.DefaultBeanContext +import io.micronaut.inject.BeanDefinition +import io.micronaut.kotlin.processing.aop.simple.CovariantClass +import spock.lang.Specification +import spock.lang.Unroll + +class ProxyingMethodLevelAopSpec extends Specification { + + @Unroll + void "test AOP method invocation for method #method"() { + given: + BeanContext beanContext = new DefaultBeanContext().start() + ProxyingClass foo = beanContext.getBean(ProxyingClass) + + expect: + args.isEmpty() ? foo."$method"() : foo."$method"(*args) == result + foo.lifeCycleCount == 0 + foo instanceof InterceptedProxy + foo.interceptedTarget().lifeCycleCount == 1 + + where: + method | args | result + 'test' | ['test'] | "Name is changed" // test for single string arg + 'test' | [10] | "Age is 20" // test for single primitive + 'test' | ['test', 10] | "Name is changed and age is 10" // test for multiple args, one primitive + 'test' | [] | "noargs" // test for no args + 'testVoid' | ['test'] | null // test for void return type + 'testVoid' | ['test', 10] | null // test for void return type + 'testBoolean' | ['test'] | true // test for boolean return type + 'testBoolean' | ['test', 10] | true // test for boolean return type + 'testInt' | ['test'] | 1 // test for int return type + 'testInt' | ['test', 10] | 20 // test for int return type + 'testShort' | ['test'] | 1 // test for short return type + 'testShort' | ['test', 10] | 20 // test for short return type + 'testChar' | ['test'] | 1 // test for char return type + 'testChar' | ['test', 10] | 20 // test for char return type + 'testByte' | ['test'] | 1 // test for byte return type + 'testByte' | ['test', 10] | 20 // test for byte return type + 'testFloat' | ['test'] | 1 // test for float return type + 'testFloat' | ['test', 10] | 20 // test for float return type + 'testDouble' | ['test'] | 1 // test for double return type + 'testDouble' | ['test', 10] | 20 // test for double return type + 'testByteArray' | ['test', 'test'.bytes] | 'test'.bytes // test for byte array + 'testGenericsWithExtends' | ['test', 10] | 'Name is changed' // test for generics + 'testGenericsFromType' | ['test', 10] | 'Name is changed' // test for generics + 'testListWithWildCardIn' | ['test', new CovariantClass<>()] | new CovariantClass<>('changed') // test for generics + 'testListWithWildCardOut' | ['test', new CovariantClass<>()] | new CovariantClass<>('changed') + } + + + void "test AOP setup"() { + given: + BeanContext beanContext = new DefaultBeanContext().start() + + when: "the bean definition is obtained" + BeanDefinition beanDefinition = beanContext.findBeanDefinition(ProxyingClass).get() + + then: + beanDefinition.findMethod("test", String).isPresent() + // should not be a reflection based method + !beanDefinition.findMethod("test", String).get().getClass().getName().contains("Reflection") + + when: + ProxyingClass foo = beanContext.getBean(ProxyingClass) + + + then: + foo instanceof InterceptedProxy + beanContext.findExecutableMethod(ProxyingClass, "test", String).isPresent() + // should not be a reflection based method + !beanContext.findExecutableMethod(ProxyingClass, "test", String).get().getClass().getName().contains("Reflection") + foo.test("test") == "Name is changed" + + } + +} diff --git a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/simple/SimpleClassMethodLevelAopSpec.groovy b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/simple/SimpleClassMethodLevelAopSpec.groovy new file mode 100644 index 00000000000..af37ca6615b --- /dev/null +++ b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/simple/SimpleClassMethodLevelAopSpec.groovy @@ -0,0 +1,107 @@ +/* + * Copyright 2017-2019 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.kotlin.processing.aop.simple + +import io.micronaut.aop.Intercepted +import io.micronaut.context.BeanContext +import io.micronaut.context.DefaultBeanContext +import io.micronaut.inject.BeanDefinition +import spock.lang.Specification +import spock.lang.Unroll + +/** + * @author Graeme Rocher + * @since 1.0 + */ +class SimpleClassMethodLevelAopSpec extends Specification { + + @Unroll + void "test AOP method invocation for method #method"() { + given: + BeanContext beanContext = new DefaultBeanContext().start() + SimpleClass foo = beanContext.getBean(SimpleClass) + + expect: + args.isEmpty() ? foo."$method"() : foo."$method"(*args) == result + foo.postConstructInvoked + + where: + method | args | result + 'test' | ['test'] | "Name is changed" // test for single string arg + 'test' | [10] | "Age is 20" // test for single primitive + 'test' | ['test', 10] | "Name is changed and age is 10" // test for multiple args, one primitive + 'test' | [] | "noargs" // test for no args + 'testVoid' | ['test'] | null // test for void return type + 'testVoid' | ['test', 10] | null // test for void return type + 'testBoolean' | ['test'] | true // test for boolean return type + 'testBoolean' | ['test', 10] | true // test for boolean return type + 'testInt' | ['test'] | 1 // test for int return type + 'testInt' | ['test', 10] | 20 // test for int return type + 'testShort' | ['test'] | 1 // test for short return type + 'testShort' | ['test', 10] | 20 // test for short return type + 'testChar' | ['test'] | 1 // test for char return type + 'testChar' | ['test', 10] | 20 // test for char return type + 'testByte' | ['test'] | 1 // test for byte return type + 'testByte' | ['test', 10] | 20 // test for byte return type + 'testFloat' | ['test'] | 1 // test for float return type + 'testFloat' | ['test', 10] | 20 // test for float return type + 'testDouble' | ['test'] | 1 // test for double return type + 'testDouble' | ['test', 10] | 20 // test for double return type + 'testByteArray' | ['test', 'test'.bytes] | 'test'.bytes // test for byte array + 'testGenericsWithExtends' | ['test', 10] | 'Name is changed' // test for generics + 'testGenericsFromType' | ['test', 10] | 'Name is changed' // test for generics + 'testListWithWildCardIn' | ['test', new CovariantClass<>()] | new CovariantClass<>('changed') // test for generics + 'testListWithWildCardOut' | ['test', new CovariantClass<>()] | new CovariantClass<>('changed') // test for generics + } + + + void "test AOP setup"() { + given: + BeanContext beanContext = new DefaultBeanContext().start() + + when: "the bean definition is obtained" + BeanDefinition beanDefinition = beanContext.findBeanDefinition(SimpleClass).get() + + then: + beanDefinition.findMethod("test", String).isPresent() + // should not be a reflection based method + !beanDefinition.findMethod("test", String).get().getClass().getName().contains("Reflection") + + when: + SimpleClass foo = beanContext.getBean(SimpleClass) + + + then: + foo instanceof Intercepted + beanContext.findExecutableMethod(SimpleClass, "test", String).isPresent() + // should not be a reflection based method + !beanContext.findExecutableMethod(SimpleClass, "test", String).get().getClass().getName().contains("Reflection") + foo.test("test") == "Name is changed" + } + + void "test modifying the interceptor parameters is not supported"() { + given: + BeanContext beanContext = new DefaultBeanContext().start() + SimpleClass foo = beanContext.getBean(SimpleClass) + + when: "the interceptor is called" + foo.invalidInterceptor() + + then: + thrown(UnsupportedOperationException) + } + +} diff --git a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/simple/SimpleClassTypeLevelAopSpec.groovy b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/simple/SimpleClassTypeLevelAopSpec.groovy new file mode 100644 index 00000000000..5526d4635b6 --- /dev/null +++ b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/simple/SimpleClassTypeLevelAopSpec.groovy @@ -0,0 +1,67 @@ +/* + * Copyright 2017-2019 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.kotlin.processing.aop.simple + +import io.micronaut.context.BeanContext +import io.micronaut.context.DefaultBeanContext +import spock.lang.Specification +import spock.lang.Unroll + +/** + * @author Graeme Rocher + * @since 1.0 + */ +class SimpleClassTypeLevelAopSpec extends Specification { + + @Unroll + void "test AOP method invocation for method #method"() { + given: + BeanContext beanContext = new DefaultBeanContext().start() + AnotherClass foo = beanContext.getBean(AnotherClass) + + expect: + args.isEmpty() ? foo."$method"() : foo."$method"(*args) == expected + + where: + method | args | expected + 'test' | ['test'] | "Name is changed" // test for single string arg + 'test' | [10] | "Age is 20" // test for single primitive + 'test' | ['test', 10] | "Name is changed and age is 10" // test for multiple args, one primitive + 'test' | [] | "noargs" // test for no args + 'testVoid' | ['test'] | null // test for void return type + 'testVoid' | ['test', 10] | null // test for void return type + 'testBoolean' | ['test'] | true // test for boolean return type + 'testBoolean' | ['test', 10] | true // test for boolean return type + 'testInt' | ['test'] | 1 // test for int return type + 'testInt' | ['test', 10] | 20 // test for int return type + 'testShort' | ['test'] | 1 // test for short return type + 'testShort' | ['test', 10] | 20 // test for short return type + 'testChar' | ['test'] | 1 // test for char return type + 'testChar' | ['test', 10] | 20 // test for char return type + 'testByte' | ['test'] | 1 // test for byte return type + 'testByte' | ['test', 10] | 20 // test for byte return type + 'testFloat' | ['test'] | 1 // test for float return type + 'testFloat' | ['test', 10] | 20 // test for float return type + 'testDouble' | ['test'] | 1 // test for double return type + 'testDouble' | ['test', 10] | 20 // test for double return type + 'testByteArray' | ['test', 'test'.bytes] | 'test'.bytes // test for byte array + 'testGenericsWithExtends' | ['test', 10] | 'Name is changed' // test for generics + 'testGenericsFromType' | ['test', 10] | 'Name is changed' // test for generics + 'testListWithWildCardIn' | ['test', new CovariantClass<>()] | new CovariantClass<>('changed') // test for generics + 'testListWithWildCardOut' | ['test', new CovariantClass<>()] | new CovariantClass<>('changed') + } + +} diff --git a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/beans/AbstractBeanSpec.groovy b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/beans/AbstractBeanSpec.groovy new file mode 100644 index 00000000000..fd5f6090f50 --- /dev/null +++ b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/beans/AbstractBeanSpec.groovy @@ -0,0 +1,59 @@ +package io.micronaut.kotlin.processing.beans + +import io.micronaut.inject.BeanDefinition +import spock.lang.Specification + +import static io.micronaut.annotation.processing.test.KotlinCompiler.* + +class AbstractBeanSpec extends Specification { + + void "test bean definitions are created for classes with only a qualifier"() { + when: + BeanDefinition beanDefinition = buildBeanDefinition('test.Bean', ''' +package test + +@jakarta.inject.Named("a") +class Bean +''') + then: + beanDefinition != null + !beanDefinition.isSingleton() + } + + void "test abstract classes with only a qualifier do not generate bean definitions"() { + when: + BeanDefinition beanDefinition = buildBeanDefinition('test.Bean', ''' +package test + +@jakarta.inject.Named("a") +abstract class Bean +''') + then: + beanDefinition == null + } + + void "test classes with only AOP advice generate bean definitions"() { + when: + BeanDefinition beanDefinition = buildBeanDefinition('test.Bean', ''' +package test + +@io.micronaut.validation.Validated +open class Bean +''') + then: + beanDefinition != null + } + + void "test abstract classes with only AOP advice do not generate bean definitions"() { + when: + BeanDefinition beanDefinition = buildBeanDefinition('test.Bean', ''' +package test; + +@io.micronaut.validation.Validated +abstract class Bean +''') + then: + beanDefinition == null + } + +} diff --git a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/beans/BeanDefinitionSpec.groovy b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/beans/BeanDefinitionSpec.groovy new file mode 100644 index 00000000000..4a530725f3e --- /dev/null +++ b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/beans/BeanDefinitionSpec.groovy @@ -0,0 +1,849 @@ +package io.micronaut.kotlin.processing.beans + +import io.micronaut.annotation.processing.test.KotlinCompiler +import io.micronaut.context.ApplicationContext +import io.micronaut.core.annotation.AnnotationUtil +import io.micronaut.core.annotation.Introspected +import io.micronaut.core.annotation.Order +import io.micronaut.core.bind.annotation.Bindable +import io.micronaut.http.annotation.Header +import io.micronaut.http.annotation.HttpMethodMapping +import io.micronaut.http.client.annotation.Client +import io.micronaut.inject.BeanDefinition +import io.micronaut.inject.qualifiers.Qualifiers +import io.micronaut.inject.writer.BeanDefinitionVisitor +import spock.lang.PendingFeature +import spock.lang.Specification + +import static io.micronaut.annotation.processing.test.KotlinCompiler.* + +class BeanDefinitionSpec extends Specification { + + void "test jvm field"() { + given: + def definition = KotlinCompiler.buildBeanDefinition('test.JvmFieldTest', ''' +package test + +import jakarta.inject.Inject +import jakarta.inject.Singleton + +@Singleton +class JvmFieldTest { + @JvmField + @Inject + var f : F? = null +} + +@Singleton +class F +''') + + expect: + definition.injectedMethods.size() == 0 + definition.injectedFields.size() == 1 + } + + @PendingFeature(reason = "difficult to achieve with current design without a significant rewrite or how native properties are handled") + void "test injection order for inheritance"() { + given: + def context = KotlinCompiler.buildContext(''' +package inherit + +import jakarta.inject.* +import jakarta.inject.Inject + +@Singleton +class Child : Parent() { + + var parentMethodInjectBeforeChildMethod : Boolean = false + var parentMethodInjectBeforeChildField : Boolean = false + var childFieldInjectedBeforeChildMethod : Boolean = false + + @Inject + var childProp : Other? = Other() + set(value) { + if (parentProp != null && parentMethod != null) { + parentMethodInjectBeforeChildField = true + } + } + lateinit var childMethod : Other + + @Inject + fun antherMethod(other : Other) { + if (parentProp != null && parentMethod != null) { + parentMethodInjectBeforeChildMethod = true + } + if (childProp != null) { + childFieldInjectedBeforeChildMethod = true + } + childMethod = other + } +} + +open class Parent { + var parentPropInjectedBeforeParentMethod : Boolean = false + + @Inject + lateinit var parentProp : Other + lateinit var parentMethod : Other + + @Inject + fun someMethod(other : Other) { + if (parentProp != null) { + parentPropInjectedBeforeParentMethod = true + } + parentMethod = other + } +} + +@Singleton +class Other +''') + def bean = KotlinCompiler.getBean(context, 'inherit.Child') + + expect:"The parent property was injected before the parent method" + bean.parentPropInjectedBeforeParentMethod + + and:"All injection points of the parent were injected before the child method" + bean.parentInjectBeforeChildMethod + + and:"All injection points of the parent were injected before the child field" + bean.parentInjectBeforeChildField + + and:"The child property was injected before the child method" + bean.childFieldInjectedBeforeChildMethod + + cleanup: + context.close() + } + + void "test suspend function with executable"() { + given: + def definition = buildBeanDefinition('test.SuspendTest', ''' +package test + +import io.micronaut.context.annotation.Executable +import jakarta.inject.Inject +import jakarta.inject.Named +import jakarta.inject.Singleton + +@Singleton +class SuspendTest { + @Executable + suspend fun test() { + TODO() + } +} + +@Singleton +class A +''') + expect: + definition != null + definition.executableMethods.size() == 1 + } + + void "test @Inject on set of Kotlin property"() { + given: + def definition = buildBeanDefinition('test.SetterInjectBean', ''' +package test + +import jakarta.inject.Inject +import jakarta.inject.Named +import jakarta.inject.Singleton + +@Singleton +class SetterInjectBean { + internal var _a: A? = null + internal var a: A + get() = _a!! + @Inject set(value) { _a = value; } +} + +@Singleton +class A +''') + expect: + definition != null + definition.injectedMethods.size() == 1 + } + + void "test requires validation adds bean introspection"() { + given: + def definition = buildBeanDefinition('test.EngineConfig', ''' +package test + +import io.micronaut.context.annotation.ConfigurationProperties +import io.micronaut.core.convert.format.MapFormat +import javax.validation.constraints.Min +// end::imports[] + +// tag::class[] +@ConfigurationProperties("my.engine") +class EngineConfig { + + @Min(1L) + var cylinders: Int = 0 + + @MapFormat(transformation = MapFormat.MapTransformation.FLAT) //<1> + var sensors: Map? = null +} +''') + expect: + definition.hasAnnotation(Introspected) + } + + void "test repeated annotations - auto unwrap"() { + given: + def definition = buildBeanDefinition('test.RepeatedTest', ''' +package test + +import io.micronaut.context.annotation.Executable +import io.micronaut.http.annotation.Header +import io.micronaut.http.annotation.Headers +import jakarta.inject.Singleton + +@Singleton +@Headers( + Header(name="Foo"), + Header(name="Bar") +) + +class RepeatedTest { + @Executable + @Headers( + Header(name="Baz"), + Header(name="Stuff") + ) + fun test() : String { + return "Ok" + } +} +''') + expect: + definition.getRequiredMethod("test").getAnnotationValuesByType(Header).size() == 4 + definition.getRequiredMethod("test").getAnnotationNamesByStereotype(Bindable).size() == 2 + } + + void "test repeated annotations"() { + given: + def definition = buildBeanDefinition('test.RepeatedTest', ''' +package test + +import io.micronaut.context.annotation.Executable +import io.micronaut.http.annotation.Header +import jakarta.inject.Singleton + +@Singleton +@Header(name="Foo") +@Header(name="Bar") +class RepeatedTest { + @Executable + @Header(name="Baz") + @Header(name="Stuff") + fun test() : String { + return "Ok" + } +} +''') + expect: + definition.getRequiredMethod("test").getAnnotationValuesByType(Header).size() == 4 + } + + void "test annotation defaults"() { + given: + def definition = KotlinCompiler.buildBeanDefinition('test.TestClient' + BeanDefinitionVisitor.PROXY_SUFFIX, ''' +package test + +import io.micronaut.http.annotation.Post +import io.micronaut.http.client.annotation.Client + +@Client("/") +interface TestClient { + @Post + fun save(str : String) : String +} +''') + expect: + definition.getRequiredMethod("save", String) + .getAnnotation(HttpMethodMapping) + .getRequiredValue(String) == '/' + } + + void "test annotation defaults - inherited"() { + given: + def definition = KotlinCompiler.buildBeanDefinition('test.TestClient' + BeanDefinitionVisitor.PROXY_SUFFIX, ''' +package test + +import io.micronaut.http.annotation.Post +import io.micronaut.http.client.annotation.Client + +@Client("/") +interface TestClient : TestOperations { + override fun save(str : String) : String +} + +interface TestOperations { + @Post + fun save(str : String) : String +} +''') + expect: + definition.getRequiredMethod("save", String) + .getAnnotation(HttpMethodMapping) + .getRequiredValue(String) == '/' + } + + void "test @Inject internal var"() { + given: + def context = KotlinCompiler.buildContext(''' +package test + +import io.micronaut.context.event.ApplicationEventPublisher +import jakarta.inject.Inject +import jakarta.inject.Singleton + +@Singleton +class SampleEventEmitterBean { + + @Inject + internal var eventPublisher: ApplicationEventPublisher? = null + + fun publishSampleEvent() { + eventPublisher!!.publishEvent(SampleEvent()) + } + +} + +class SampleEvent + +''') + + def bean = getBean(context, 'test.SampleEventEmitterBean') + def definition = KotlinCompiler.getBeanDefinition(context, 'test.SampleEventEmitterBean') + expect: + definition.injectedFields.size() == 0 + definition.injectedMethods.size() == 1 + + bean.eventPublisher + + cleanup: + context.close() + } + void "test @Property targeting field"() { + given: + def context = buildContext(''' +package test + +import io.micronaut.context.annotation.Property + +import jakarta.inject.Inject +import jakarta.inject.Singleton + +@Singleton +class Engine { + + @field:Property(name = "my.engine.cylinders") // <1> + protected var cylinders: Int = 0 // <2> + + @set:Inject + @setparam:Property(name = "my.engine.manufacturer") // <3> + var manufacturer: String? = null + + @Inject + @Property(name = "my.engine.manufacturer") // <3> + var manufacturer2: String? = null + + @Property(name = "my.engine.manufacturer") // <3> + var manufacturer3: String? = null + + fun cylinders(): Int { + return cylinders + } +} +''', false, ['my.engine.cylinders': 8, 'my.engine.manufacturer':'Ford']) + def definition = getBeanDefinition(context, 'test.Engine') + def bean = getBean(context, 'test.Engine') + + expect:"field targeting injects fields" + definition.injectedMethods.size() == 4 + definition.injectedFields.size() == 0 + bean.cylinders() == 8 + bean.manufacturer == 'Ford' + bean.manufacturer2 == 'Ford' + bean.manufacturer3 == 'Ford' + } + + void "test non-binding qualifier"() { + given: + def definition = KotlinCompiler.buildBeanDefinition('test.V8Engine', ''' +package test + +import io.micronaut.context.annotation.NonBinding +import jakarta.inject.Qualifier +import jakarta.inject.Singleton +import kotlin.annotation.Retention + +@Cylinders(value = 8, description = "test") +@Singleton +class V8Engine + +@Qualifier +@Retention(AnnotationRetention.RUNTIME) +annotation class Cylinders( + val value: Int, + @get:NonBinding // <2> + val description: String = "" +) +''') + expect:"the non-binding member is not there" + definition.declaredQualifier.qualifierAnn.memberNames == ["value"] as Set + } + + void "test property annotation on properties and targeting params"() { + given: + def context = KotlinCompiler.buildContext(''' +package test +import io.micronaut.context.annotation.Property +import io.micronaut.core.convert.format.MapFormat +import jakarta.inject.Inject +import jakarta.inject.Singleton + +@Singleton +class BeanWithProperty { + + @set:Inject + @setparam:Property(name="app.string") + var stringParam:String ?= null + + @set:Inject + @setparam:Property(name="app.map") + @setparam:MapFormat(transformation = MapFormat.MapTransformation.FLAT) + var mapParam:Map ?= null + + @Property(name="app.string") + var stringParamTwo:String ?= null + + @Property(name="app.map") + @MapFormat(transformation = MapFormat.MapTransformation.FLAT) + var mapParamTwo:Map ?= null +} +''', false, ["app.string": "Hello", "app.map.yyy.xxx": 2, "app.map.yyy.yyy": 3]) + + def bean = KotlinCompiler.getBean(context, 'test.BeanWithProperty') + + expect: + bean.stringParam == 'Hello' + + cleanup: + context.close() + } + + void "test annotations targeting field on properties"() { + given: + def definition = buildBeanDefinition('test.TestBean', ''' +package test + +import jakarta.inject.Inject +import jakarta.inject.Named +import jakarta.inject.Singleton + +@Singleton +class TestBean { + @Inject @field:Named("one") lateinit var otherBean: OtherBean +} + +@Singleton +@Named("one") +class OtherBean +''') + expect: + definition != null + definition.injectedMethods.size() == 1 + definition.injectedMethods[0].annotationMetadata.hasAnnotation(AnnotationUtil.NAMED) + definition.injectedMethods[0].arguments[0].annotationMetadata.hasAnnotation(AnnotationUtil.NAMED) + } + + void "test annotations targeting field on properties - client"() { + given: + def definition = buildBeanDefinition('test.TestBean', ''' +package test + +import io.micronaut.http.client.HttpClient +import io.micronaut.http.client.annotation.Client +import jakarta.inject.Inject +import jakarta.inject.Named +import jakarta.inject.Singleton + +@Singleton +class TestBean { + @Inject @field:Client("/test") lateinit var client: HttpClient +} + +''') + expect: + definition != null + definition.injectedMethods.size() == 1 + definition.injectedMethods[0].annotationMetadata.hasAnnotation(Client) + definition.injectedMethods[0].arguments[0].annotationMetadata.hasAnnotation(Client) + } + + void "test controller with constructor arguments"() { + given: + def definition = buildBeanDefinition('controller.TestController', ''' +package controller + +import io.micronaut.context.annotation.* +import io.micronaut.http.annotation.Controller +import jakarta.inject.* +import jakarta.inject.Singleton + +@Controller +class TestController(var bar : Bar) + +@Singleton +class Bar +''') + expect: + definition != null + } + + void "test limit the exposed bean types"() { + given: + def definition = buildBeanDefinition('limittypes.Test', ''' +package limittypes + +import io.micronaut.context.annotation.* +import jakarta.inject.* + +@Singleton +@Bean(typed = [Runnable::class]) +class Test: Runnable { + + override fun run() { + } +} + +''') + expect: + definition.exposedTypes == [Runnable] as Set + } + + void "test limit the exposed bean types - reference"() { + given: + def reference = buildBeanDefinitionReference('limittypes.Test', ''' +package limittypes + +import io.micronaut.context.annotation.* +import jakarta.inject.* + +@Singleton +@Bean(typed = [Runnable::class]) +class Test: Runnable { + + override fun run(){ + } +} + +''') + expect: + reference.exposedTypes == [Runnable] as Set + } + + void "test fail compilation on invalid exposed bean type"() { + when: + buildBeanDefinition('limittypes.Test', ''' +package limittypes + +import io.micronaut.context.annotation.* +import jakarta.inject.* + +@Singleton +@Bean(typed = [Runnable::class]) +class Test +''') + then: + def e = thrown(RuntimeException) + e.message.contains("Bean defines an exposed type [java.lang.Runnable] that is not implemented by the bean type") + } + + void "test exposed types on factory with AOP"() { + when: + buildBeanDefinition('limittypes.Test$Method0', ''' +package limittypes + +import io.micronaut.kotlin.processing.aop.Logged +import io.micronaut.context.annotation.* +import jakarta.inject.Singleton + +@Factory +class Test { + + @Singleton + @Bean(typed = [X::class]) + @Logged + fun method(): Y { + return Y() + } +} + +interface X + +open class Y: X +''') + + then: + noExceptionThrown() + } + + void "test fail compilation on exposed subclass of bean type"() { + when: + buildBeanDefinition('limittypes.Test', ''' +package limittypes + +import io.micronaut.context.annotation.* +import jakarta.inject.* + +@Singleton +@Bean(typed = [X::class]) +open class Test + +class X : Test() +''') + then: + def e = thrown(RuntimeException) + e.message.contains("Bean defines an exposed type [limittypes.X] that is not implemented by the bean type") + } + + void "test fail compilation on exposed subclass of bean type with factory"() { + when: + buildBeanDefinition('limittypes.Test$Method0', ''' +package limittypes + +import io.micronaut.context.annotation.* +import jakarta.inject.Singleton + +@Factory +class Test { + + @Singleton + @Bean(typed = [X::class, Y::class]) + fun method(): X { + return Y() + } +} + +interface X + +class Y: X +''') + + then: + def e = thrown(RuntimeException) + e.message.contains("Bean defines an exposed type [limittypes.Y] that is not implemented by the bean type") + } + + void "test exposed bean types with factory invalid type"() { + when: + buildBeanDefinition('limittypes.Test$Method0', ''' +package limittypes + +import io.micronaut.context.annotation.* +import jakarta.inject.Singleton + +@Factory +class Test { + + @Singleton + @Bean(typed = [Z::class]) + fun method(): X { + return Y() + } +} + +interface Z +interface X +class Y: X +''') + + then: + def e = thrown(RuntimeException) + e.message.contains("Bean defines an exposed type [limittypes.Z] that is not implemented by the bean type") + } + + void 'test order annotation'() { + given: + def definition = buildBeanDefinition('test.TestOrder', ''' +package test + +import io.micronaut.core.annotation.* +import io.micronaut.context.annotation.* +import jakarta.inject.* + +@Singleton +@Order(value = 10) +class TestOrder +''') + expect: + + definition.intValue(Order).getAsInt() == 10 + } + + void 'test qualifier for named only'() { + given: + def definition = buildBeanDefinition('test.Test', ''' +package test + +@jakarta.inject.Named("foo") +class Test +''') + expect: + definition.getDeclaredQualifier() == Qualifiers.byName("foo") + } + + void 'test no qualifier / only scope'() { + given: + def definition = buildBeanDefinition('test.Test', ''' +package test; + +@jakarta.inject.Singleton +class Test +''') + expect: + definition.getDeclaredQualifier() == null + } + + void 'test named via alias'() { + given: + def definition = buildBeanDefinition('aliastest.Test', ''' +package aliastest + +import io.micronaut.context.annotation.* + +@MockBean(named="foo") +class Test + +@Bean +annotation class MockBean( + + @get:Aliases(AliasFor(annotation = Replaces::class, member = "named"), AliasFor(annotation = jakarta.inject.Named::class, member = "value")) + val named: String = "" +) +''') + expect: + definition.getDeclaredQualifier() == Qualifiers.byName("foo") + definition.getAnnotationNameByStereotype(AnnotationUtil.QUALIFIER).get() == AnnotationUtil.NAMED + } + + void 'test qualifier annotation'() { + given: + def definition = buildBeanDefinition('test.Test', ''' +package test + +import io.micronaut.context.annotation.* + +@MyQualifier +class Test + +@jakarta.inject.Qualifier +annotation class MyQualifier ( + + @get:Aliases(AliasFor(annotation = Replaces::class, member = "named"), AliasFor(annotation = jakarta.inject.Named::class, member = "value")) + val named: String = "" +) +''') + + expect: + definition.getDeclaredQualifier() == Qualifiers.byAnnotation(definition.getAnnotationMetadata(), "test.MyQualifier") + definition.getAnnotationNameByStereotype(AnnotationUtil.QUALIFIER).get() == "test.MyQualifier" + } + + /* @Issue("https://github.com/micronaut-projects/micronaut-core/issues/5001") + void "test building a bean with generics that dont have a type"() { + when: + def definition = buildBeanDefinition('test.NumberThingManager', ''' +package test; + +import jakarta.inject.Singleton + +interface Thing + +interface NumberThing> extends Thing {} + +class AbstractThingManager> {} + +@Singleton +public class NumberThingManager extends AbstractThingManager> {} +''') + + then: + noExceptionThrown() + definition != null + definition.getTypeArguments("test.AbstractThingManager")[0].getTypeVariables().get("T").getType() == Number.class + }*/ + + void "test a bean definition in a package with uppercase letters"() { + when: + def definition = buildBeanDefinition('test.A', 'TestBean', ''' +package test.A + +@jakarta.inject.Singleton +class TestBean +''') + then: + noExceptionThrown() + definition != null + } + + void "test a bean definition inner static class"() { + when: + def definition = buildBeanDefinition('test.TestBean$TestBeanInner', ''' +package test + +class TestBean { + + @jakarta.inject.Singleton + class TestBeanInner { + + } +} +''') + then: + noExceptionThrown() + definition != null + } + + void "test a bean definition is not created for inner class"() { + when: + def definition = buildBeanDefinition('test.TestBean$TestBeanInner', ''' +package test + +class TestBean { + + @jakarta.inject.Singleton + inner class TestBeanInner { + + } +} +''') + then: + noExceptionThrown() + definition == null + } + + void "test nullable constructor arg"() { + when: + def definition = buildBeanDefinition('test.TestBean', ''' +package test + +@jakarta.inject.Singleton +class TestBean(private val other: Other?) { +} + +class Other +''') + then: + noExceptionThrown() + definition.constructor.arguments[0].isDeclaredNullable() + } +} diff --git a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/beans/BeanRegistrationSpec.groovy b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/beans/BeanRegistrationSpec.groovy new file mode 100644 index 00000000000..e10491c1775 --- /dev/null +++ b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/beans/BeanRegistrationSpec.groovy @@ -0,0 +1,70 @@ +package io.micronaut.kotlin.processing.beans + +import io.micronaut.context.BeanRegistration +import spock.lang.Specification + +import static io.micronaut.annotation.processing.test.KotlinCompiler.* + +class BeanRegistrationSpec extends Specification { + + void 'test inject bean registrations'() { + given: + def className = 'beanreg.Test' + def context = buildContext( ''' +package beanreg + +import jakarta.inject.Singleton +import jakarta.inject.Inject +import jakarta.inject.Named +import io.micronaut.context.BeanRegistration + +@Singleton +class Test(val registrations: Collection>, val primaryBean: BeanRegistration) { + + @Inject + lateinit var fieldRegistrations: Collection> + + @Inject + lateinit var fieldArrayRegistrations: Array> + + @Inject + lateinit var methodRegistrations: List> + + @Named("two") + @Inject + lateinit var secondaryBean: BeanRegistration +} + +interface Foo + +@Singleton +@io.micronaut.context.annotation.Primary +class Foo1: Foo + +@Singleton +@Named("two") +class Foo2: Foo +''') + + def bean = getBean(context, className) + + Collection registrations = bean.registrations + Collection fieldRegistrations = bean.fieldRegistrations + Collection methodRegistrations = bean.methodRegistrations + Collection fieldArrayRegistrations = bean.fieldArrayRegistrations.toList() + + expect: + bean.primaryBean.bean.getClass().name == 'beanreg.Foo1' + bean.secondaryBean.bean.getClass().name == 'beanreg.Foo2' + registrations.size() == 2 + fieldRegistrations.size() == 2 + fieldRegistrations == registrations + fieldRegistrations as List == methodRegistrations + fieldRegistrations as List == fieldArrayRegistrations + registrations.any { it.bean.getClass().name == 'beanreg.Foo1'} + registrations.any { it.bean.getClass().name == 'beanreg.Foo2'} + + cleanup: + context.close() + } +} diff --git a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/beans/SingletonSpec.groovy b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/beans/SingletonSpec.groovy new file mode 100644 index 00000000000..96c19189832 --- /dev/null +++ b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/beans/SingletonSpec.groovy @@ -0,0 +1,111 @@ +package io.micronaut.kotlin.processing.beans + +import io.micronaut.annotation.processing.test.AbstractKotlinCompilerSpec +import io.micronaut.inject.BeanDefinition + +import spock.lang.Specification + +class SingletonSpec extends AbstractKotlinCompilerSpec { + + void "test simple singleton bean"() { + when: + def context = buildContext(""" +package test + +import jakarta.inject.Singleton + +@Singleton +class Test +""") + + then: + noExceptionThrown() + + when: + Class test = context.classLoader.loadClass("test.Test") + context.getBean(test) + + then: + noExceptionThrown() + } + + void "test singleton bean from a factory property"() { + when: + def context = buildContext(""" +package test + +import io.micronaut.context.annotation.Bean +import io.micronaut.context.annotation.Factory +import jakarta.inject.Singleton + +@Factory +class Test { + + @Singleton + @Bean + val one = Foo("one") + +} + +class Foo(val name: String) +""") + + then: + noExceptionThrown() + Class foo = context.classLoader.loadClass("test.Foo") + context.getBean(foo).getName() == "one" + } + + void "test singleton bean from a factory method"() { + when: + def context = buildContext(""" +package test + +import io.micronaut.context.annotation.Bean +import io.micronaut.context.annotation.Factory +import jakarta.inject.Singleton + +@Factory +class Test { + @Singleton + fun one() = Foo("one") +} + +class Foo(val name: String) +""") + + then: + noExceptionThrown() + Class foo = context.classLoader.loadClass("test.Foo") + context.getBean(foo).getName() == "one" + } + + void "test singleton abstract class"() { + when: + BeanDefinition beanDefinition = buildBeanDefinition('test.AbstractBean', ''' +package test + +import jakarta.inject.Singleton + +@Singleton +abstract class AbstractBean { + +} +''') + then: + beanDefinition.isAbstract() + } + + void "test that using @Singleton on an enum results in a compilation error"() { + when: + buildBeanDefinition('test.Test','''\ +package test + +@jakarta.inject.Singleton +enum class Test +''') + then: + def e = thrown(RuntimeException) + e.message.contains('Enum types cannot be defined as beans') + } +} diff --git a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/beans/aliasfor/AliasForQualifierSpec.groovy b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/beans/aliasfor/AliasForQualifierSpec.groovy new file mode 100644 index 00000000000..830bb485411 --- /dev/null +++ b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/beans/aliasfor/AliasForQualifierSpec.groovy @@ -0,0 +1,50 @@ +/* + * Copyright 2017-2019 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.kotlin.processing.beans.aliasfor + +import io.micronaut.annotation.processing.test.KotlinCompiler +import io.micronaut.core.annotation.AnnotationUtil +import io.micronaut.inject.BeanDefinition + +import spock.lang.Specification + +class AliasForQualifierSpec extends Specification { + + void "test that when an alias is created for a named qualifier the stereotypes are correct"() { + given: + BeanDefinition definition = KotlinCompiler.buildBeanDefinition('test.Test$MyFunc0','''\ +package test + +import io.micronaut.context.annotation.Factory +import io.micronaut.kotlin.processing.beans.aliasfor.TestAnnotation + +@Factory +class Test { + + @TestAnnotation("foo") + fun myFunc(): (String) -> Int { + return { str -> 10 } + } +} + +''') + expect: + definition != null + definition.getAnnotationNameByStereotype(AnnotationUtil.QUALIFIER).isPresent() + definition.getAnnotationNameByStereotype(AnnotationUtil.QUALIFIER).get() == AnnotationUtil.NAMED + definition.getValue(AnnotationUtil.NAMED, String).get() == 'foo' + } +} diff --git a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/beans/collect/InjectCollectionBeanSpec.groovy b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/beans/collect/InjectCollectionBeanSpec.groovy new file mode 100644 index 00000000000..3bddb37b104 --- /dev/null +++ b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/beans/collect/InjectCollectionBeanSpec.groovy @@ -0,0 +1,41 @@ +/* + * Copyright 2017-2019 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.kotlin.processing.beans.collect + +import io.micronaut.context.BeanContext +import spock.lang.Specification + +class InjectCollectionBeanSpec extends Specification { + + void "test resolve collection bean"() { + given: + def ctx = BeanContext.run() + + expect: + ctx.getBean(ThingThatNeedsMySetOfStrings).strings.size() == 1 + ctx.getBean(ThingThatNeedsMySetOfStrings).strings == ctx.getBean(ThingThatNeedsMySetOfStrings).otherStrings + } + + void "test resolve iterable bean"() { + when: + def ctx = BeanContext.run() + ctx.getBean(MyIterable) + ctx.getBean(ThingThatNeedsMyIterable) + + then: + noExceptionThrown() + } +} diff --git a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/beans/configproperties/ConfigPropertiesInnerClassSpec.groovy b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/beans/configproperties/ConfigPropertiesInnerClassSpec.groovy new file mode 100644 index 00000000000..436be5635d5 --- /dev/null +++ b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/beans/configproperties/ConfigPropertiesInnerClassSpec.groovy @@ -0,0 +1,29 @@ +package io.micronaut.kotlin.processing.beans.configproperties + +import io.micronaut.context.ApplicationContext +import io.micronaut.context.DefaultApplicationContext +import io.micronaut.context.env.PropertySource +import spock.lang.Specification + +class ConfigPropertiesInnerClassSpec extends Specification { + + void "test configuration properties binding with inner class"() { + given: + ApplicationContext applicationContext = new DefaultApplicationContext("test") + applicationContext.environment.addPropertySource(PropertySource.of( + 'test', + ['foo.bar.innerVals': [ + ['expire-unsigned-seconds': 123], ['expireUnsignedSeconds': 600] + ]] + )) + + applicationContext.start() + + MyConfigInner config = applicationContext.getBean(MyConfigInner) + + expect: + config.innerVals.size() == 2 + config.innerVals[0].expireUnsignedSeconds == 123 + config.innerVals[1].expireUnsignedSeconds == 600 + } +} diff --git a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/beans/configproperties/ConfigurationPropertiesFactorySpec.groovy b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/beans/configproperties/ConfigurationPropertiesFactorySpec.groovy new file mode 100644 index 00000000000..7e1356f3184 --- /dev/null +++ b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/beans/configproperties/ConfigurationPropertiesFactorySpec.groovy @@ -0,0 +1,14 @@ +package io.micronaut.kotlin.processing.beans.configproperties + +import io.micronaut.context.ApplicationContext +import spock.lang.Specification + +class ConfigurationPropertiesFactorySpec extends Specification { + + void "test replacing a configuration properties via a factory"() { + ApplicationContext ctx = ApplicationContext.run(["spec.name": ConfigurationPropertiesFactorySpec.simpleName]) + + expect: + ctx.getBean(Neo4jProperties).uri.getHost() == "google.com" + } +} diff --git a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/beans/configproperties/ConfigurationPropertiesSpec.groovy b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/beans/configproperties/ConfigurationPropertiesSpec.groovy new file mode 100644 index 00000000000..3729b3460fd --- /dev/null +++ b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/beans/configproperties/ConfigurationPropertiesSpec.groovy @@ -0,0 +1,148 @@ +/* + * Copyright 2017-2019 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.kotlin.processing.beans.configproperties + +import io.micronaut.context.ApplicationContext +import io.micronaut.context.DefaultApplicationContext +import io.micronaut.context.env.PropertySource +import io.micronaut.core.util.CollectionUtils +import spock.lang.Specification + +class ConfigurationPropertiesSpec extends Specification { + + void "test submap with generics binding"() { + given: + ApplicationContext ctx = ApplicationContext.run( + 'foo.bar.map.key1.key2.property':10, + 'foo.bar.map.key1.key2.property2.property':10 + ) + + expect: + ctx.getBean(MyConfig).map.containsKey('key1') + ctx.getBean(MyConfig).map.get("key1") instanceof Map + ctx.getBean(MyConfig).map.get("key1").get("key2") instanceof MyConfig.Value + ctx.getBean(MyConfig).map.get("key1").get("key2").property == 10 + ctx.getBean(MyConfig).map.get("key1").get("key2").property2 + ctx.getBean(MyConfig).map.get("key1").get("key2").property2.property == 10 + + cleanup: + ctx.close() + } + + void "test submap with generics binding and conversion"() { + given: + ApplicationContext ctx = ApplicationContext.run( + 'foo.bar.map.key1.key2.property':'10', + 'foo.bar.map.key1.key2.property2.property':'10' + ) + + expect: + ctx.getBean(MyConfig).map.containsKey('key1') + ctx.getBean(MyConfig).map.get("key1") instanceof Map + ctx.getBean(MyConfig).map.get("key1").get("key2") instanceof MyConfig.Value + ctx.getBean(MyConfig).map.get("key1").get("key2").property == 10 + ctx.getBean(MyConfig).map.get("key1").get("key2").property2 + ctx.getBean(MyConfig).map.get("key1").get("key2").property2.property == 10 + + cleanup: + ctx.close() + } + + void "test configuration properties binding"() { + given: + ApplicationContext applicationContext = new DefaultApplicationContext("test") + applicationContext.environment.addPropertySource(PropertySource.of( + 'test', + ['foo.bar.innerVals': [ + ['expire-unsigned-seconds': 123], ['expireUnsignedSeconds': 600] + ], + 'foo.bar.port':'8080', + 'foo.bar.max-size':'1MB', + 'foo.bar.another-size':'1MB', + 'foo.bar.anotherPort':'9090', + 'foo.bar.intList':"1,2,3", + 'foo.bar.stringList':"1,2", + 'foo.bar.flags.one':'1', + 'foo.bar.flags.two':'2', + 'foo.bar.urlList':"http://test.com, http://test2.com", + 'foo.bar.urlList2':["http://test.com", "http://test2.com"], + 'foo.bar.url':'http://test.com'] + )) + + applicationContext.start() + + MyConfig config = applicationContext.getBean(MyConfig) + + expect: + config.innerVals.size() == 2 + config.innerVals[0].expireUnsignedSeconds == 123 + config.innerVals[1].expireUnsignedSeconds == 600 + config.port == 8080 + config.maxSize == 1048576 + config.anotherPort == 9090 + config.intList == [1,2,3] + config.flags == [one:1, two:2] + config.urlList == [new URL('http://test.com'),new URL('http://test2.com')] + config.urlList2 == [new URL('http://test.com'),new URL('http://test2.com')] + config.stringList == ["1", "2"] + config.emptyList == null + config.url.get() == new URL('http://test.com') + !config.anotherUrl.isPresent() + config.defaultPort == 9999 + config.defaultValue == 9999 + } + + void "test configuration inner class properties binding"() { + given: + ApplicationContext applicationContext = new DefaultApplicationContext("test") + applicationContext.environment.addPropertySource(PropertySource.of( + 'foo.bar.inner.enabled':'true', + )) + + applicationContext.start() + + MyConfig config = applicationContext.getBean(MyConfig) + + expect: + config.inner.enabled + } + + void "test binding to a map setter"() { + ApplicationContext context = ApplicationContext.run(CollectionUtils.mapOf("map.setter.yyy.zzz", 3, "map.setter.yyy.xxx", 2, "map.setter.yyy.yyy", 3)) + MapProperties config = context.getBean(MapProperties.class) + + expect: + config.setter.containsKey('yyy') + + cleanup: + context.close() + } + + void "test camelCase vs kebab_case"() { + ApplicationContext context1 = ApplicationContext.run("rec1") + ApplicationContext context2 = ApplicationContext.run("rec2") + + RecConf config1 = context1.getBean(RecConf.class) + RecConf config2 = context2.getBean(RecConf.class) + + expect: + config1 == config2 + + cleanup: + context1.close() + context2.close() + } +} diff --git a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/beans/configproperties/inheritance/ConfigurationPropertiesInheritanceSpec.groovy b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/beans/configproperties/inheritance/ConfigurationPropertiesInheritanceSpec.groovy new file mode 100644 index 00000000000..6d07513f669 --- /dev/null +++ b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/beans/configproperties/inheritance/ConfigurationPropertiesInheritanceSpec.groovy @@ -0,0 +1,197 @@ +/* + * Copyright 2017-2019 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.kotlin.processing.beans.configproperties.inheritance + +import io.micronaut.context.ApplicationContext +import io.micronaut.context.DefaultApplicationContext +import io.micronaut.context.env.PropertySource +import io.micronaut.inject.qualifiers.Qualifiers +import spock.lang.Specification + +/** + * @author Graeme Rocher + * @since 1.0 + */ +class ConfigurationPropertiesInheritanceSpec extends Specification { + + void "test configuration properties binding"() { + given: + ApplicationContext applicationContext = new DefaultApplicationContext("test") + applicationContext.environment.addPropertySource(PropertySource.of( + 'test', + ['foo.bar.port':'8080', + 'foo.bar.host':'localhost', + 'foo.bar.baz.stuff': 'test'] + )) + + applicationContext.start() + + ChildConfig config = applicationContext.getBean(ChildConfig) + MyConfig parent = applicationContext.getBean(MyConfig) + + expect: +// parent.is(config) + parent.host == 'localhost' + parent.port == 8080 + config.port == 8080 + config.host == 'localhost' + config.stuff == 'test' + + cleanup: + applicationContext.stop() + } + + void "test configuration properties binding extending POJO"() { + given: + ApplicationContext applicationContext = new DefaultApplicationContext("test") + applicationContext.environment.addPropertySource(PropertySource.of( + 'test', + ['foo.baz.otherProperty':'x', + 'foo.baz.onlySetter':'y', + 'foo.baz.port': 55] + )) + + applicationContext.start() + + MyOtherConfig config = applicationContext.getBean(MyOtherConfig) + + expect: + config.port == 55 + config.otherProperty == 'x' + config.onlySetter == 'y' + + cleanup: + applicationContext.stop() + } + + void "test EachProperty inner ConfigurationProperties with setter"() { + given: + ApplicationContext context = ApplicationContext.run([ + 'teams.cubs.wins': 5, + 'teams.cubs.manager.age': 40, + 'teams.mets.wins': 6 + ]) + + when: + ParentEachProps cubs = context.getBean(ParentEachProps, Qualifiers.byName("cubs")) + + then: + cubs.wins == 5 + cubs.manager.age == 40 + + when: + ParentEachProps.ManagerProps cubsManager = context.getBean(ParentEachProps.ManagerProps, Qualifiers.byName("cubs")) + + then: "The instance is the same" + cubsManager.is(cubs.manager) + + when: + ParentEachProps mets = context.getBean(ParentEachProps, Qualifiers.byName("mets")) + + then: + mets.wins == 6 + mets.manager == null + + and: + !context.findBean(ParentEachProps.ManagerProps, Qualifiers.byName("mets")).isPresent() + context.getBeansOfType(ParentEachProps).size() == 2 + context.getBeansOfType(ParentEachProps.ManagerProps).size() == 1 + } + + void "test EachProperty inner ConfigurationProperties with constructor"() { + given: + ApplicationContext context = ApplicationContext.run([ + 'teams.cubs.wins': 5, + 'teams.cubs.manager.age': 40, + 'teams.mets.wins': 6 + ]) + + when: + ParentEachPropsCtor cubs = context.getBean(ParentEachPropsCtor, Qualifiers.byName("cubs")) + + then: + cubs.wins == 5 + cubs.manager.age == 40 + cubs.name == "cubs" + + when: + ParentEachPropsCtor.ManagerProps cubsManager = context.getBean(ParentEachPropsCtor.ManagerProps, Qualifiers.byName("cubs")) + + then: "The instance is the same" + cubsManager.is(cubs.manager) + cubsManager.name == "cubs" + + when: + ParentEachPropsCtor mets = context.getBean(ParentEachPropsCtor, Qualifiers.byName("mets")) + + then: + mets.wins == 6 + mets.manager == null + mets.name == "mets" + + and: + !context.findBean(ParentEachPropsCtor.ManagerProps, Qualifiers.byName("mets")).isPresent() + context.getBeansOfType(ParentEachPropsCtor).size() == 2 + context.getBeansOfType(ParentEachPropsCtor.ManagerProps).size() == 1 + } + + void "test EachProperty array inner ConfigurationProperties with setter"() { + given: + ApplicationContext context = ApplicationContext.run([ + 'teams': [['wins': 5, 'manager': ['age': 40]], ['wins': 6]] + ]) + + when: + Collection teams = context.getBeansOfType(ParentArrayEachProps) + + then: + teams[0].wins == 5 + teams[0].manager.age == 40 + teams[1].wins == 6 + teams[1].manager == null + + when: + Collection managers = context.getBeansOfType(ParentArrayEachProps.ManagerProps) + + then: "The instance is the same" + managers.size() == 1 + managers[0].is(teams[0].manager) + } + + void "test EachProperty array inner ConfigurationProperties with constructor"() { + given: + ApplicationContext context = ApplicationContext.run([ + 'teams': [['wins': 5, 'manager': ['age': 40]], ['wins': 6]] + + ]) + + when: + Collection teams = context.getBeansOfType(ParentArrayEachPropsCtor) + + then: + teams[0].wins == 5 + teams[0].manager.age == 40 + teams[1].wins == 6 + teams[1].manager == null + + when: + Collection managers = context.getBeansOfType(ParentArrayEachPropsCtor.ManagerProps) + + then: "The instance is the same" + managers.size() == 1 + managers[0].is(teams[0].manager) + } +} diff --git a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/beans/executable/ExecutableBeanSpec.groovy b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/beans/executable/ExecutableBeanSpec.groovy new file mode 100644 index 00000000000..2ea6a1db23e --- /dev/null +++ b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/beans/executable/ExecutableBeanSpec.groovy @@ -0,0 +1,128 @@ +/* + * Copyright 2017-2019 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.kotlin.processing.beans.executable + +import io.micronaut.inject.BeanDefinition +import spock.lang.Issue +import spock.lang.Specification + +import static io.micronaut.annotation.processing.test.KotlinCompiler.* + +class ExecutableBeanSpec extends Specification { + + void "test executable method return types"() { + given: + BeanDefinition definition = buildBeanDefinition('test.ExecutableBean1','''\ +package test + +import io.micronaut.context.annotation.Executable +import kotlin.math.roundToInt + +@jakarta.inject.Singleton +@Executable +class ExecutableBean1 { + + fun round(num: Float): Int { + return num.roundToInt() + } +} +''') + expect: + definition != null + definition.findMethod("round", float.class).get().returnType.type == int.class + } + + @Issue('#2789') + void "test don't generate executable methods for inherited protected or package private methods"() { + given: + BeanDefinition definition = buildBeanDefinition('test.MyBean','''\ +package test + +import io.micronaut.context.annotation.Executable +import kotlin.math.roundToInt + +@jakarta.inject.Singleton +@Executable +class MyBean: Parent() { + + fun round(num: Float): Int { + return num.roundToInt() + } +} + +open class Parent { + protected fun protectedMethod() { + } + + internal fun packagePrivateMethod() { + } + + private fun privateMethod() { + } +} +''') + expect: + definition != null + !definition.findMethod("privateMethod").isPresent() + !definition.findMethod("packagePrivateMethod").isPresent() + !definition.findMethod("protectedMethod").isPresent() + } + + void "bean definition should not be created for class with only executable methods"() { + given: + BeanDefinition definition = buildBeanDefinition('test.MyBean','''\ +package test + +import io.micronaut.context.annotation.Executable +import kotlin.math.roundToInt + +class MyBean { + + @Executable + fun round(num: Float): Int { + return num.roundToInt() + } +} + +''') + + expect: + definition == null + } + + void "test multiple executable annotations on a method"() { + given: + BeanDefinition definition = buildBeanDefinition('test.MyBean','''\ +package test + +import io.micronaut.kotlin.processing.beans.executable.RepeatableExecutable + +@jakarta.inject.Singleton +class MyBean { + + @RepeatableExecutable("a") + @RepeatableExecutable("b") + fun run() { + + } +} +''') + expect: + definition != null + definition.findMethod("run").isPresent() + } +} + diff --git a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/beans/executable/ExecutableSpec.groovy b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/beans/executable/ExecutableSpec.groovy new file mode 100644 index 00000000000..29c9976bca2 --- /dev/null +++ b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/beans/executable/ExecutableSpec.groovy @@ -0,0 +1,109 @@ +/* + * Copyright 2017-2019 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.kotlin.processing.beans.executable + +import io.micronaut.context.ApplicationContext +import io.micronaut.context.DefaultApplicationContext +import io.micronaut.core.annotation.AnnotationUtil +import io.micronaut.inject.BeanDefinition +import io.micronaut.inject.ExecutableMethod +import io.micronaut.inject.ExecutionHandle +import io.micronaut.inject.MethodExecutionHandle +import spock.lang.Specification + +import static io.micronaut.annotation.processing.test.KotlinCompiler.* + +class ExecutableSpec extends Specification { + + void "test executable compile spec"() { + given:"A bean that defines no explicit scope" + when: + BeanDefinition beanDefinition = buildBeanDefinition('test.MyBean', ''' +package test + +import io.micronaut.context.annotation.Executable + +@Executable +class MyBean { + + fun methodOne(@jakarta.inject.Named("foo") one: String): String { + return "good" + } + + fun methodTwo(one: String, two: String): String { + return "good" + } + + fun methodZero(): String { + return "good" + } +} + + +''') + then:"the default scope is singleton" + beanDefinition.executableMethods.size() == 3 + beanDefinition.executableMethods[0].methodName == 'methodOne' + beanDefinition.executableMethods[0].getArguments()[0].getAnnotationMetadata().stringValue(AnnotationUtil.NAMED).get() == 'foo' + } + + void "test executable metadata"() { + given: + ApplicationContext applicationContext = new DefaultApplicationContext("test").start() + + when: + Optional method = applicationContext.findExecutionHandle(BookController, "show", Long) + ExecutableMethod executableMethod = applicationContext.findBeanDefinition(BookController).get().findMethod("show", Long).get() + + then: + method.isPresent() + + when: + MethodExecutionHandle executionHandle = method.get() + + then: + executionHandle.returnType.type == String + executionHandle.invoke(1L) == "1 - The Stand" + + when: + executionHandle.invoke("bad") + + then: + def e = thrown(IllegalArgumentException) + e.message == 'Invalid type [java.lang.String] for argument [Long id] of method: String show(Long id)' + + } + + void "test executable responses"() { + given: + ApplicationContext applicationContext = new DefaultApplicationContext("test").start() + + expect: + applicationContext.findExecutionHandle(BookController, methodName, argTypes as Class[]).isPresent() + ExecutionHandle method = applicationContext.findExecutionHandle(BookController, methodName, argTypes as Class[]).get() + method.invoke(args as Object[]) == result + + + where: + methodName | argTypes | args | result + "show" | [Long] | [1L] | "1 - The Stand" + "showArray" | [Long[].class] | [[1L] as Long[]] | "1 - The Stand" + "showPrimitive" | [long.class] | [1L as long] | "1 - The Stand" + "showPrimitiveArray" | [long[].class] | [[1L] as long[]] | "1 - The Stand" + "showVoidReturn" | [Iterable.class] | [['test']] | null + "showPrimitiveReturn" | [int[].class] | [[1] as int[]] | 1 + } +} diff --git a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/beans/executable/inheritance/InheritedExecutableSpec.groovy b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/beans/executable/inheritance/InheritedExecutableSpec.groovy new file mode 100644 index 00000000000..0899c96b11c --- /dev/null +++ b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/beans/executable/inheritance/InheritedExecutableSpec.groovy @@ -0,0 +1,149 @@ +package io.micronaut.kotlin.processing.beans.executable.inheritance + +import io.micronaut.inject.BeanDefinition +import spock.lang.PendingFeature +import spock.lang.Specification + +import static io.micronaut.annotation.processing.test.KotlinCompiler.* + +class InheritedExecutableSpec extends Specification { + + void "test extending an abstract class with an executable method"() { + given: + BeanDefinition definition = buildBeanDefinition("test.GenericController", """ +package test + +import io.micronaut.context.annotation.Executable + +abstract class GenericController { + + abstract fun getPath(): String + + @Executable + fun save(entity: T): String { + return "parent" + } +} + +""") + expect: + definition == null + } + + void "test the same method isn't written twice"() { + BeanDefinition definition = buildBeanDefinition("test.StatusController", """ +package test + +import io.micronaut.context.annotation.Executable +import jakarta.inject.Singleton + +@Executable +@Singleton +class StatusController: GenericController() { + + override fun getPath(): String { + return "/statuses" + } + + override fun save(entity: String): String { + return "child" + } + +} + +abstract class GenericController { + + abstract fun getPath(): String + + @Executable + open fun save(entity: T): String { + return "parent" + } + + @Executable + open fun save(): String { + return "parent" + } +} + +""") + expect: + definition != null + definition.getExecutableMethods().any { it.methodName == "getPath" } + definition.getExecutableMethods().any { it.methodName == "save" && it.argumentTypes == [String] as Class[] } + definition.getExecutableMethods().any { it.methodName == "save" && it.argumentTypes.length == 0 } + definition.getExecutableMethods().size() == 3 + } + + void "test with multiple generics"() { + BeanDefinition definition = buildBeanDefinition("test.StatusController",""" +package test + +import io.micronaut.context.annotation.Executable +import jakarta.inject.Singleton +import java.io.Serializable + +abstract class GenericController { + + @Executable + fun save(entity: T): T { + return entity + } + + @Executable + fun find(id: ID): T? { + return null + } + + abstract fun create(id: ID): T +} + +@Executable +@Singleton +class StatusController: GenericController() { + + override fun create(id: Int): String { + return id.toString() + } +} +""") + expect: + definition != null + definition.getExecutableMethods().any { it.methodName == "create" && it.argumentTypes == [int] as Class[] } + definition.getExecutableMethods().any { it.methodName == "save" && it.argumentTypes == [String] as Class[] } + definition.getExecutableMethods().any { it.methodName == "find" && it.argumentTypes == [int] as Class[] } + definition.getExecutableMethods().size() == 3 + } + + void "test multiple inheritance"() { + BeanDefinition definition = buildBeanDefinition("test.Z", """ +package test + +import io.micronaut.context.annotation.Executable +import jakarta.inject.Singleton + +interface X { + + @Executable + fun test() +} + +abstract class Y : X { + + override fun test(){ + } + +} + +@Singleton +class Z : Y() { + + override fun test(){ + } +} +""") + expect: + definition != null + definition.executableMethods.size() == 1 + } +} diff --git a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/beans/factory/beanannotation/PrototypeAnnotationSpec.groovy b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/beans/factory/beanannotation/PrototypeAnnotationSpec.groovy new file mode 100644 index 00000000000..af8bf4e599c --- /dev/null +++ b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/beans/factory/beanannotation/PrototypeAnnotationSpec.groovy @@ -0,0 +1,16 @@ +package io.micronaut.kotlin.processing.beans.factory.beanannotation + +import io.micronaut.context.BeanContext +import io.micronaut.context.DefaultBeanContext +import spock.lang.Specification + +class PrototypeAnnotationSpec extends Specification{ + + void "test @bean annotation makes a class available as a bean"() { + given: + BeanContext beanContext = new DefaultBeanContext().start() + + expect: + beanContext.getBean(A) != beanContext.getBean(A) // prototype by default + } +} diff --git a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/beans/factory/beanproperty/FactoryBeanFieldSpec.groovy b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/beans/factory/beanproperty/FactoryBeanFieldSpec.groovy new file mode 100644 index 00000000000..0ba4b59f7fa --- /dev/null +++ b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/beans/factory/beanproperty/FactoryBeanFieldSpec.groovy @@ -0,0 +1,367 @@ +package io.micronaut.kotlin.processing.beans.factory.beanproperty + +import spock.lang.Specification +import spock.lang.Unroll + +import static io.micronaut.annotation.processing.test.KotlinCompiler.* + +class FactoryBeanFieldSpec extends Specification { + + void "test fail compilation for AOP advice for primitive array type from field"() { + when: + buildBeanDefinition('primitive.fields.factory.errors.PrimitiveFactory',""" +package primitive.fields.factory.errors + +import io.micronaut.context.annotation.Bean +import io.micronaut.context.annotation.Factory +import jakarta.inject.Named +import io.micronaut.kotlin.processing.aop.simple.Mutating + +@Factory +class PrimitiveFactory { + + @Bean + @Named("totals") + @Mutating("test") + val totals: Array = arrayOf(10) +} +""") + + then: + def e = thrown(RuntimeException) + e.message.contains("Cannot apply AOP advice to arrays") + } + + void "test fail compilation for AOP advice to primitive type from field"() { + when: + buildBeanDefinition('primitive.fields.factory.errors.PrimitiveFactory',""" +package primitive.fields.factory.errors + +import io.micronaut.context.annotation.Bean +import io.micronaut.context.annotation.Factory +import jakarta.inject.Named +import io.micronaut.kotlin.processing.aop.simple.Mutating + +@Factory +class PrimitiveFactory { + + @Bean + @Named("total") + @Mutating("test") + val totals = 10 +} +""") + + then: + def e = thrown(RuntimeException) + e.message.contains("Cannot apply AOP advice to primitive beans") + } + + void "test fail compilation when defining preDestroy for primitive type from field"() { + when: + buildBeanDefinition('primitive.fields.factory.errors.PrimitiveFactory',""" +package primitive.fields.factory.errors + +import io.micronaut.context.annotation.Bean +import io.micronaut.context.annotation.Factory +import jakarta.inject.Named + +@Factory +class PrimitiveFactory { + + @Bean(preDestroy="close") + @Named("total") + val totals = 10 +} +""") + + then: + def e = thrown(RuntimeException) + e.message.contains("Using 'preDestroy' is not allowed on primitive type beans") + } + + @Unroll + void "test produce bean for primitive #primitiveType array type from field"() { + given: + def context = buildContext(""" +package primitive.fields.factory + +import io.micronaut.context.annotation.Bean +import io.micronaut.context.annotation.Factory +import jakarta.inject.Inject +import jakarta.inject.Named +import jakarta.inject.Singleton + +@Factory +class PrimitiveFactory { + + @Bean + @Named("totals") + val totals: ${primitiveType}Array = ${primitiveType.toLowerCase()}ArrayOf(10${primitiveType == 'Double' ? '.0' : (primitiveType == 'Float' ? 'F': '')}) +} + +@Singleton +class MyBean(@Named("totals") val totals: ${primitiveType}Array) { + + @Inject + @Named("totals") + lateinit var totalsFromProperty: ${primitiveType}Array + + var totalsFromMethod: ${primitiveType}Array? = null + + @Inject + fun setTotals(@Named("totals") totals: ${primitiveType}Array) { + this.totalsFromMethod = totals + } +} +""") + + def bean = getBean(context, 'primitive.fields.factory.MyBean') + + expect: + bean.totals[0] == 10 + bean.totalsFromProperty[0] == 10 + bean.totalsFromMethod[0] == 10 + + where: + primitiveType << ['Int', 'Short', 'Long', 'Double', 'Float', 'Byte'] + } + + @Unroll + void "test produce bean for primitive #primitiveType matrix array type from field"() { + given: + def context = buildContext(""" +package primitive.fields.factory + +import io.micronaut.context.annotation.Bean +import io.micronaut.context.annotation.Factory +import jakarta.inject.Inject +import jakarta.inject.Named +import jakarta.inject.Singleton + +@Factory +class PrimitiveFactory { + @Bean + @Named("totals") + val totals: Array<${primitiveType}Array> = arrayOf(${primitiveType.toLowerCase()}ArrayOf(10${primitiveType == 'Double' ? '.0' : (primitiveType == 'Float' ? 'F': '')})) +} + +@Singleton +class MyBean(@Named("totals") val totals: Array<${primitiveType}Array>) { + + @Inject + @Named("totals") + lateinit var totalsFromProperty: Array<${primitiveType}Array> + + var totalsFromMethod: Array<${primitiveType}Array>? = null + + @Inject + fun setTotals(@Named("totals") totals: Array<${primitiveType}Array>) { + this.totalsFromMethod = totals + } +} +""") + + def bean = getBean(context, 'primitive.fields.factory.MyBean') + + expect: + bean.totals[0][0] == 10 + bean.totalsFromProperty[0][0] == 10 + bean.totalsFromMethod[0][0] == 10 + + where: + primitiveType << ['Int', 'Short', 'Long', 'Double', 'Float', 'Byte'] + } + + @Unroll + void "test produce bean for primitive #primitiveType type from field"() { + given: + def context = buildContext(""" +package primitive.fields.factory + +import io.micronaut.context.annotation.Bean +import io.micronaut.context.annotation.Factory +import jakarta.inject.Inject +import jakarta.inject.Named +import jakarta.inject.Singleton + +@Factory +class PrimitiveFactory { + @Bean + @Named("total") + val total: $primitiveType = 10${primitiveType == 'Double' ? '.0' : (primitiveType == 'Float' ? 'F': '')} +} + +@Singleton +class MyBean(@Named("total") val total: ${primitiveType}) { + + @Inject + @Named("total") + var totalFromProperty: ${primitiveType} = 0${primitiveType == 'Double' ? '.0' : (primitiveType == 'Float' ? 'F': '')} + + var totalFromMethod: ${primitiveType}? = null + + @Inject + fun setTotals(@Named("total") total: ${primitiveType}) { + this.totalFromMethod = total + } +} +""") + + def bean = getBean(context, 'primitive.fields.factory.MyBean') + + expect: + bean.total == 10 + bean.totalFromProperty == 10 + bean.totalFromMethod == 10 + + where: + primitiveType << ['Int', 'Short', 'Long', 'Double', 'Float', 'Byte'] + } + + /* + void "test a factory bean can be supplied from a field"() { + given: + ApplicationContext context = buildContext('''\ +package test; + +import static java.lang.annotation.RetentionPolicy.RUNTIME; +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; + +import io.micronaut.inject.annotation.*; +import io.micronaut.aop.*; +import io.micronaut.context.annotation.*; +import io.micronaut.inject.factory.enummethod.TestEnum; +import jakarta.inject.*; +import java.util.Locale; +import jakarta.inject.Singleton; + +@Factory +class TestFactory$TestField { + + @Singleton + @Bean + @io.micronaut.context.annotation.Primary + Foo one = new Foo("one"); + + // final fields are implicitly singleton + @Bean + @Named("two") + final Foo two = new Foo("two"); + + // non-final fields are prototype + @Bean + @Named("three") + Foo three = new Foo("three"); + + @SomeMeta + @Bean + Foo four = new Foo("four"); + + @Bean + @Mutating + Bar bar = new Bar(); +} + +class Bar { + public String test(String test) { + return test; + } +} + +class Foo { + final String name; + Foo(String name) { + this.name = name; + } +} + +@Retention(RUNTIME) +@Singleton +@Named("four") +@AroundConstruct +@interface SomeMeta { +} + +@Retention(RUNTIME) +@Singleton +@Around +@interface Mutating { +} + +@Singleton +@InterceptorBean(SomeMeta.class) +class TestConstructInterceptor implements ConstructorInterceptor { + boolean invoked = false; + Object[] parameters; + + @Override + public Object intercept(ConstructorInvocationContext context) { + invoked = true; + parameters = context.getParameterValues(); + return context.proceed(); + } +} + +@InterceptorBean(Mutating.class) +class TestInterceptor implements MethodInterceptor { + @Override public Object intercept(MethodInvocationContext context) { + final Object[] parameterValues = context.getParameterValues(); + parameterValues[0] = parameterValues[0].toString().toUpperCase(Locale.ENGLISH); + System.out.println(parameterValues[0]); + return context.proceed(); + } +} +''') + + def barBean = getBean(context, 'test.Bar') + + expect: + barBean.test("good") == 'GOOD' // proxied + getBean(context, "test.Foo").name == 'one' + getBean(context, "test.Foo", Qualifiers.byName("two")).name == 'two' + getBean(context, "test.Foo", Qualifiers.byName("two")).is( + getBean(context, "test.Foo", Qualifiers.byName("two")) + ) + getBean(context, "test.Foo", Qualifiers.byName("three")).is( + getBean(context, "test.Foo", Qualifiers.byName("three")) + ) + getBean(context, 'test.TestConstructInterceptor').invoked == false + getBean(context, "test.Foo", Qualifiers.byName("four")) // around construct + getBean(context, 'test.TestConstructInterceptor').invoked == true + + cleanup: + context.close() + } +*/ + @Unroll + void 'test fail compilation on invalid modifier #modifier'() { + when: + def ctx = buildContext( """ +package invalidmod + +import io.micronaut.context.annotation.Factory +import io.micronaut.context.annotation.Bean + +@Factory +class TestFactory { + + @Bean + $modifier val test: Test = Test() +} + +class Test +""") + + then: + def e = thrown(RuntimeException) + e.message.contains("cannot be ") + e.message.contains(modifier) + + where: + modifier << ['private'] + + + } +} diff --git a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/beans/inheritance/InheritanceSingletonSpec.groovy b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/beans/inheritance/InheritanceSingletonSpec.groovy new file mode 100644 index 00000000000..0e406063996 --- /dev/null +++ b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/beans/inheritance/InheritanceSingletonSpec.groovy @@ -0,0 +1,32 @@ +package io.micronaut.kotlin.processing.beans.inheritance + +import io.micronaut.inject.qualifiers.Qualifiers +import spock.lang.Specification + +import static io.micronaut.annotation.processing.test.KotlinCompiler.* + +class InheritanceSingletonSpec extends Specification { + + void "test getBeansOfType returns the same instance"() { + def ctx = buildContext(""" +package test + +import jakarta.inject.Singleton + +@Singleton +class BankService: AbstractService() + +abstract class AbstractService: ServiceContract + +interface ServiceContract +""") + + + when: + def bankService = ctx.getBean(ctx.classLoader.loadClass("test.BankService")) + def otherBankService = ctx.getBeansOfType(ctx.classLoader.loadClass("test.ServiceContract"), Qualifiers.byTypeArgumentsClosest(String))[0] + + then: + bankService.is(otherBankService) + } +} diff --git a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/inject/ast/ClassElementSpec.groovy b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/inject/ast/ClassElementSpec.groovy new file mode 100644 index 00000000000..2e0a606c0b7 --- /dev/null +++ b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/inject/ast/ClassElementSpec.groovy @@ -0,0 +1,314 @@ +package io.micronaut.kotlin.processing.inject.ast + +import io.micronaut.annotation.processing.test.AbstractKotlinCompilerSpec +import io.micronaut.inject.ast.ClassElement +import io.micronaut.inject.ast.ConstructorElement +import io.micronaut.inject.ast.ElementModifier +import io.micronaut.inject.ast.ElementQuery +import io.micronaut.inject.ast.MethodElement +import io.micronaut.inject.ast.PropertyElement +import spock.lang.PendingFeature + +class ClassElementSpec extends AbstractKotlinCompilerSpec { + + void "test class element"() { + expect: + buildClassElement('ast.test.Test', ''' +package ast.test + +import java.lang.IllegalStateException +import kotlin.jvm.Throws + + +class Test( + val publicConstructorReadOnly : String, + private val privateConstructorReadOnly : String, + protected val protectedConstructorReadOnly : Boolean +) : Parent(), One, Two { + + val publicReadOnlyProp : Boolean = true + protected val protectedReadOnlyProp : Boolean? = true + private val privateReadOnlyProp : Boolean? = true + var publicReadWriteProp : Boolean = true + protected var protectedReadWriteProp : String? = "ok" + private var privateReadWriteProp : String = "ok" + private var conventionProp : String = "ok" + + private fun privateFunc(name : String) : String { + return "ok" + } + + open fun openFunc(name : String) : String { + return "ok" + } + + protected fun protectedFunc(name : String) : String { + return "ok" + } + + @Throws(IllegalStateException::class) + override fun publicFunc(name : String) : String { + return "ok" + } + + suspend fun suspendFunc(name : String) : String { + return "ok" + } + + fun getConventionProp() : String { + return conventionProp + } + + fun setConventionProp(name : String) { + this.conventionProp = name + } + + + companion object Helper { + fun publicStatic() : String { + return "ok" + } + + private fun privateStatic() : String { + return "ok" + } + } + + inner class InnerClass1 + + class InnerClass2 +} + +open class Parent : Three { + open fun publicFunc(name : String) : String { + return "ok" + } + + fun parentFunc() : Boolean { + return true + } + + companion object ParentHelper { + fun publicStatic() : String { + return "ok" + } + } +} + +interface One +interface Two +interface Three +''') { ClassElement classElement -> + List constructorElements = classElement.getEnclosedElements(ElementQuery.CONSTRUCTORS) + List allInnerClasses = classElement.getEnclosedElements(ElementQuery.ALL_INNER_CLASSES) + List declaredInnerClasses = classElement.getEnclosedElements(ElementQuery.ALL_INNER_CLASSES.onlyDeclared()) + List propertyElements = classElement.getBeanProperties() + List syntheticProperties = classElement.getSyntheticBeanProperties() + List methodElements = classElement.getEnclosedElements(ElementQuery.ALL_METHODS) + List declaredMethodElements = classElement.getEnclosedElements(ElementQuery.ALL_METHODS.onlyDeclared()) + List includeOverridden = classElement.getEnclosedElements(ElementQuery.ALL_METHODS.includeOverriddenMethods()) + Map methodMap = methodElements.collectEntries { + [it.name, it] + } + Map declaredMethodMap = declaredMethodElements.collectEntries { + [it.name, it] + } + Map propMap = propertyElements.collectEntries { + [it.name, it] + } + Map synthPropMap = syntheticProperties.collectEntries { + [it.name, it] + } + Map declaredInnerMap = declaredInnerClasses.collectEntries { + [it.simpleName, it] + } + Map innerMap = allInnerClasses.collectEntries { + [it.simpleName, it] + } + + def overridden = includeOverridden.find { it.declaringType.simpleName == 'Parent' && it.name == 'publicFunc' } + + assert classElement != null + assert classElement.interfaces*.simpleName as Set == ['One', "Two"] as Set + assert methodElements != null + assert !classElement.isAbstract() + assert classElement.name == 'ast.test.Test' + assert !classElement.isPrivate() + assert classElement.isPublic() + assert classElement.modifiers == [ElementModifier.FINAL, ElementModifier.PUBLIC] as Set + assert constructorElements.size() == 1 + assert constructorElements[0].parameters.size() == 3 + assert classElement.superType.isPresent() + assert classElement.superType.get().simpleName == 'Parent' + assert !classElement.superType.get().getSuperType().isPresent() + assert propertyElements.size() == 7 + assert propMap.size() == 7 + assert synthPropMap.size() == 6 + assert methodElements.size() == 8 + assert includeOverridden.size() == 9 + assert declaredMethodElements.size() == 7 + assert propMap.keySet() == ['conventionProp', 'publicReadOnlyProp', 'protectedReadOnlyProp', 'publicReadWriteProp', 'protectedReadWriteProp', 'publicConstructorReadOnly', 'protectedConstructorReadOnly'] as Set + assert synthPropMap.keySet() == ['publicReadOnlyProp', 'protectedReadOnlyProp', 'publicReadWriteProp', 'protectedReadWriteProp', 'publicConstructorReadOnly', 'protectedConstructorReadOnly'] as Set + // inner classes + assert allInnerClasses.size() == 4 + assert declaredInnerClasses.size() == 3 + assert !declaredInnerMap['Test$InnerClass1'].isStatic() + assert declaredInnerMap['Test$InnerClass2'].isStatic() + assert declaredInnerMap['Test$InnerClass1'].isPublic() + assert declaredInnerMap['Test$InnerClass2'].isPublic() + + // read-only public + assert propMap['publicReadOnlyProp'].isReadOnly() + assert !propMap['publicReadOnlyProp'].isWriteOnly() + assert propMap['publicReadOnlyProp'].isPublic() + assert propMap['publicReadOnlyProp'].readMethod.isPresent() + assert propMap['publicReadOnlyProp'].readMethod.get().isSynthetic() + assert !propMap['publicReadOnlyProp'].writeMethod.isPresent() + // read/write public property + assert !propMap['publicReadWriteProp'].isReadOnly() + assert !propMap['publicReadWriteProp'].isWriteOnly() + assert propMap['publicReadWriteProp'].isPublic() + assert propMap['publicReadWriteProp'].readMethod.isPresent() + assert propMap['publicReadWriteProp'].readMethod.get().isSynthetic() + assert propMap['publicReadWriteProp'].writeMethod.isPresent() + assert propMap['publicReadWriteProp'].writeMethod.get().isSynthetic() + // convention prop + assert !propMap['conventionProp'].isReadOnly() + assert !propMap['conventionProp'].isWriteOnly() + assert propMap['conventionProp'].isPublic() + assert propMap['conventionProp'].readMethod.isPresent() + assert !propMap['conventionProp'].readMethod.get().isSynthetic() + assert propMap['conventionProp'].writeMethod.isPresent() + assert !propMap['conventionProp'].writeMethod.get().isSynthetic() + + // methods + assert methodMap.keySet() == ['publicFunc', 'parentFunc', 'openFunc', 'privateFunc', 'protectedFunc', 'suspendFunc', 'getConventionProp', 'setConventionProp'] as Set + assert declaredMethodMap.keySet() == ['publicFunc', 'openFunc', 'privateFunc', 'protectedFunc', 'suspendFunc', 'getConventionProp', 'setConventionProp'] as Set + assert methodMap['suspendFunc'].isSuspend() + assert methodMap['suspendFunc'].returnType.name == String.name + assert methodMap['suspendFunc'].parameters.size() == 1 + assert methodMap['suspendFunc'].suspendParameters.size() == 2 + assert !methodMap['openFunc'].isFinal() + assert !methodMap['publicFunc'].isPackagePrivate() + assert !methodMap['publicFunc'].isPrivate() + assert !methodMap['publicFunc'].isStatic() + assert !methodMap['publicFunc'].isReflectionRequired() + assert methodMap['publicFunc'].hasParameters() + assert methodMap['publicFunc'].thrownTypes.size() == 1 + assert methodMap['publicFunc'].thrownTypes[0].name == IllegalStateException.name + assert methodMap['publicFunc'].isPublic() + assert methodMap['publicFunc'].owningType.name == 'ast.test.Test' + assert methodMap['publicFunc'].declaringType.name == 'ast.test.Test' + assert !methodMap['publicFunc'].isFinal() // should be final? But apparently not + assert overridden != null + assert methodMap['publicFunc'].overrides(overridden) + } + } + + void "test class element generics"() { + expect: + buildClassElement('ast.test.Test', ''' +package ast.test + +/** +* Class docs +* +* @param constructorProp construct prop +*/ +class Test( + val constructorProp : String) : Parent(constructorProp), One { + /** + * Property doc + */ + val publicReadOnlyProp : Boolean = true + override val size: Int = 10 + override fun get(index: Int): String { + return "ok" + } + + open fun openFunc(name : String) : String { + return "ok" + } + + /** + * Method doc + * @param name Param name + */ + override fun publicFunc(name : String) : String { + return "ok" + } +} + +open abstract class Parent(val parentConstructorProp : T) : AbstractMutableList() { + + var parentProp : T = parentConstructorProp + private var conventionProp : T = parentConstructorProp + + fun getConventionProp() : T { + return conventionProp + } + override fun add(index: Int, element: T){ + TODO("Not yet implemented") + } + override fun removeAt(index: Int): T{ + TODO("Not yet implemented") + } + override fun set(index: Int, element: T): T{ + TODO("Not yet implemented") + } + fun setConventionProp(name : T) { + this.conventionProp = name + } + + open fun publicFunc(name : T) : T { + TODO("not yet implemented") + } + + fun parentFunc(name : T) : T { + TODO("not yet implemented") + } + + suspend fun suspendFunc(name : T) : T { + TODO("not yet implemented") + } +} + +interface One +interface Two +interface Three +''') { ClassElement classElement -> + List constructorElements = classElement.getEnclosedElements(ElementQuery.CONSTRUCTORS) + List propertyElements = classElement.getBeanProperties() + List syntheticProperties = classElement.getSyntheticBeanProperties() + List methodElements = classElement.getEnclosedElements(ElementQuery.ALL_METHODS) + Map methodMap = methodElements.collectEntries { + [it.name, it] + } + Map propMap = propertyElements.collectEntries { + [it.name, it] + } + + assert classElement.documentation.isPresent() + assert methodMap['add'].parameters[1].genericType.simpleName == 'String' + assert methodMap['add'].parameters[1].type.simpleName == 'CharSequence' + assert methodMap['iterator'].returnType.firstTypeArgument.get().simpleName == 'Object' + assert methodMap['iterator'].genericReturnType.firstTypeArgument.get().simpleName == 'String' + assert methodMap['stream'].returnType.firstTypeArgument.get().simpleName == 'Object' + assert methodMap['stream'].genericReturnType.firstTypeArgument.get().simpleName == 'String' + assert propMap['conventionProp'].type.simpleName == 'String' + assert propMap['conventionProp'].genericType.simpleName == 'String' + assert propMap['conventionProp'].genericType.simpleName == 'String' + assert propMap['conventionProp'].readMethod.get().returnType.simpleName == 'CharSequence' + assert propMap['conventionProp'].readMethod.get().genericReturnType.simpleName == 'String' + assert propMap['conventionProp'].writeMethod.get().parameters[0].type.simpleName == 'CharSequence' + assert propMap['conventionProp'].writeMethod.get().parameters[0].genericType.simpleName == 'String' + assert propMap['parentConstructorProp'].type.simpleName == 'CharSequence' + assert propMap['parentConstructorProp'].genericType.simpleName == 'String' + assert methodMap['publicFunc'].documentation.isPresent() + assert methodMap['parentFunc'].returnType.simpleName == 'CharSequence' + assert methodMap['parentFunc'].genericReturnType.simpleName == 'String' + assert methodMap['parentFunc'].parameters[0].type.simpleName == 'CharSequence' + assert methodMap['parentFunc'].parameters[0].genericType.simpleName == 'String' + } + } +} diff --git a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/inject/configproperties/ConfigPropertiesInnerClassSpec.groovy b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/inject/configproperties/ConfigPropertiesInnerClassSpec.groovy new file mode 100644 index 00000000000..b791218e659 --- /dev/null +++ b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/inject/configproperties/ConfigPropertiesInnerClassSpec.groovy @@ -0,0 +1,29 @@ +package io.micronaut.kotlin.processing.inject.configproperties + +import io.micronaut.context.ApplicationContext +import io.micronaut.context.DefaultApplicationContext +import io.micronaut.context.env.PropertySource +import spock.lang.Specification + +class ConfigPropertiesInnerClassSpec extends Specification { + + void "test configuration properties binding with inner class"() { + given: + ApplicationContext applicationContext = new DefaultApplicationContext("test") + applicationContext.environment.addPropertySource(PropertySource.of( + 'test', + ['foo.bar.innerVals': [ + ['expire-unsigned-seconds': 123], ['expireUnsignedSeconds': 600] + ]] + )) + + applicationContext.start() + + MyConfigInner config = applicationContext.getBean(MyConfigInner) + + expect: + config.innerVals.size() == 2 + config.innerVals[0].expireUnsignedSeconds == 123 + config.innerVals[1].expireUnsignedSeconds == 600 + } +} diff --git a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/inject/configproperties/ConfigPropertiesParseSpec.groovy b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/inject/configproperties/ConfigPropertiesParseSpec.groovy new file mode 100644 index 00000000000..b734347fa11 --- /dev/null +++ b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/inject/configproperties/ConfigPropertiesParseSpec.groovy @@ -0,0 +1,1111 @@ +package io.micronaut.kotlin.processing.inject.configproperties + +import io.micronaut.annotation.processing.test.KotlinCompiler +import io.micronaut.context.ApplicationContext +import io.micronaut.context.BeanContext +import io.micronaut.context.annotation.ConfigurationReader +import io.micronaut.context.annotation.Property +import io.micronaut.core.convert.format.ReadableBytes +import io.micronaut.inject.BeanDefinition +import io.micronaut.inject.InstantiatableBeanDefinition +import io.micronaut.inject.MethodInjectionPoint +import io.micronaut.kotlin.processing.inject.configuration.Engine +import spock.lang.Specification + +import static io.micronaut.annotation.processing.test.KotlinCompiler.* + +class ConfigPropertiesParseSpec extends Specification { + + void "test data classes that are configuration properties inject values"() { + given: + + def config = ['foo.bar.host': 'test', 'foo.bar.baz.stuff': "good"] + def context = buildContext(''' +package test + +import io.micronaut.context.annotation.* + +@ConfigurationProperties("foo.bar") +data class DataConfigTest(val host : String, val child: ChildConfig ) { + @ConfigurationProperties("baz") + data class ChildConfig(var stuff: String) +} +''', false, config) + + def bean = getBean(context, 'test.DataConfigTest') + + expect: + bean.host == 'test' + bean.child.stuff == 'good' + + cleanup: + context.close() + } + + void "test inner class paths - pojo inheritance"() { + when: + BeanDefinition beanDefinition = buildBeanDefinition('test.MyConfig$ChildConfig', ''' +package test + +import io.micronaut.context.annotation.* +import java.time.Duration + +@ConfigurationProperties("foo.bar") +class MyConfig { + var host: String? = null + + @ConfigurationProperties("baz") + open class ChildConfig: ParentConfig() { + protected var stuff: String? = null + } +} + +open class ParentConfig { + var foo: String? = null +} +''') + then: + beanDefinition.synthesize(ConfigurationReader).prefix() == 'foo.bar.baz' + beanDefinition.injectedMethods.size() == 2 + + def setStuff = beanDefinition.injectedMethods.find { it.name == 'setStuff'} + setStuff.getAnnotationMetadata().hasAnnotation(Property) + setStuff.getAnnotationMetadata().synthesize(Property).name() == 'foo.bar.baz.stuff' + setStuff.name == 'setStuff' + + + def setFooMethod = beanDefinition.injectedMethods.find { it.name == 'setFoo'} + setFooMethod.getAnnotationMetadata().hasAnnotation(Property) + setFooMethod.getAnnotationMetadata().synthesize(Property).name() == 'foo.bar.baz.foo' + setFooMethod.name == 'setFoo' + } + + void "test inner class paths - fields"() { + when: + BeanDefinition beanDefinition = buildBeanDefinition('test.MyConfig$ChildConfig', ''' +package test + +import io.micronaut.context.annotation.* + +@ConfigurationProperties("foo.bar") +class MyConfig { + + var host: String? = null + + @ConfigurationProperties("baz") + open class ChildConfig { + protected var stuff: String? = null + } +} +''') + then: + beanDefinition.synthesize(ConfigurationReader).prefix() == 'foo.bar.baz' + beanDefinition.injectedFields.size() == 0 + beanDefinition.injectedMethods.size() == 1 + beanDefinition.injectedMethods[0].getAnnotationMetadata().hasAnnotation(Property) + beanDefinition.injectedMethods[0].getAnnotationMetadata().synthesize(Property).name() == 'foo.bar.baz.stuff' + beanDefinition.injectedMethods[0].name == 'setStuff' + } + + void "test inner class paths - one level"() { + when: + BeanDefinition beanDefinition = buildBeanDefinition('test.MyConfig$ChildConfig', ''' +package test + +import io.micronaut.context.annotation.* + +@ConfigurationProperties("foo.bar") +class MyConfig { + var host: String? = null + + @ConfigurationProperties("baz") + class ChildConfig { + var stuff: String? = null + } +} +''') + then: + beanDefinition.injectedFields.size() == 0 + beanDefinition.injectedMethods.size() == 1 + beanDefinition.injectedMethods[0].getAnnotationMetadata().hasAnnotation(Property) + beanDefinition.injectedMethods[0].getAnnotationMetadata().synthesize(Property).name() == 'foo.bar.baz.stuff' + beanDefinition.injectedMethods[0].name == 'setStuff' + } + + + void "test inner class paths - two levels"() { + when: + BeanDefinition beanDefinition = buildBeanDefinition('test.MyConfig$ChildConfig$MoreConfig', ''' +package test + +import io.micronaut.context.annotation.* + +@ConfigurationProperties("foo.bar") +class MyConfig { + var host: String? = null + + @ConfigurationProperties("baz") + class ChildConfig { + var stuff: String? = null + + @ConfigurationProperties("more") + class MoreConfig { + var stuff: String? = null + } + } +} +''') + then: + beanDefinition.injectedFields.size() == 0 + beanDefinition.injectedMethods.size() == 1 + beanDefinition.injectedMethods[0].getAnnotationMetadata().hasAnnotation(Property) + beanDefinition.injectedMethods[0].getAnnotationMetadata().synthesize(Property).name() == 'foo.bar.baz.more.stuff' + beanDefinition.injectedMethods[0].name == 'setStuff' + } + + void "test inner class paths - with parent inheritance"() { + when: + BeanDefinition beanDefinition = buildBeanDefinition('test.MyConfig$ChildConfig', ''' +package test + +import io.micronaut.context.annotation.* + +@ConfigurationProperties("foo.bar") +class MyConfig: ParentConfig() { + var host: String? = null + + @ConfigurationProperties("baz") + class ChildConfig { + var stuff: String? = null + } +} + +@ConfigurationProperties("parent") +open class ParentConfig +''') + then: + beanDefinition.injectedFields.size() == 0 + beanDefinition.injectedMethods.size() == 1 + beanDefinition.injectedMethods[0].getAnnotationMetadata().hasAnnotation(Property) + beanDefinition.injectedMethods[0].getAnnotationMetadata().synthesize(Property).name() == 'parent.foo.bar.baz.stuff' + beanDefinition.injectedMethods[0].name == 'setStuff' + } + + void "test setters with two arguments are not injected"() { + when: + BeanDefinition beanDefinition = buildBeanDefinition('test.MyConfig', ''' +package test + +import io.micronaut.context.annotation.* + +@ConfigurationProperties("foo.bar") +class MyConfig { + + private var host: String = "localhost" + + fun getHost() = host + + fun setHost(host: String, port: Int) { + this.host = host + } +} +''') + then: + beanDefinition.injectedFields.size() == 0 + beanDefinition.injectedMethods.size() == 0 + } + + void "test setters with two arguments from abstract parent are not injected"() { + when: + BeanDefinition beanDefinition = buildBeanDefinition('test.ChildConfig', ''' +package test + +import io.micronaut.context.annotation.* + +abstract class MyConfig { + private var host: String = "localhost" + + fun getHost() = host + + fun setHost(host: String, port: Int) { + this.host = host + } +} + +@ConfigurationProperties("baz") +class ChildConfig: MyConfig() { + var stuff: String? = null +} +''') + then: + beanDefinition.injectedFields.size() == 0 + beanDefinition.injectedMethods.size() == 1 + beanDefinition.injectedMethods[0].name == 'setStuff' + } + + void "test inheritance with setters"() { + when: + BeanDefinition beanDefinition = buildBeanDefinition('test.ChildConfig', ''' +package test + +import io.micronaut.context.annotation.* + +@ConfigurationProperties("foo.bar") +open class MyConfig { + protected var port: Int = 0 + var host: String? = null +} + +@ConfigurationProperties("baz") +class ChildConfig: MyConfig() { + var stuff: String? = null +} +''') + then: + beanDefinition.injectedFields.size() == 0 + beanDefinition.injectedMethods.size() == 3 + + def stuffMethod = beanDefinition.injectedMethods.find { it.name == 'setStuff'} + stuffMethod.name == 'setStuff' + stuffMethod.getAnnotationMetadata().hasAnnotation(Property) + stuffMethod.getAnnotationMetadata().synthesize(Property).name() == 'foo.bar.baz.stuff' + + def setPortMethod = beanDefinition.injectedMethods.find { it.name == 'setPort'} + setPortMethod.name == 'setPort' + setPortMethod.getAnnotationMetadata().synthesize(Property).name() == 'foo.bar.port' + + def setHostMethod = beanDefinition.injectedMethods.find { it.name == 'setHost'} + setHostMethod.getAnnotationMetadata().hasAnnotation(Property) + setHostMethod.getAnnotationMetadata().synthesize(Property).name() == 'foo.bar.host' + setHostMethod.name == 'setHost' + + } + + void "test annotation on property"() { + when: + BeanDefinition beanDefinition = buildBeanDefinition('test.HttpClientConfiguration', ''' +package test + +import io.micronaut.core.convert.format.* +import io.micronaut.context.annotation.* + +@ConfigurationProperties("http.client") +class HttpClientConfiguration { + @ReadableBytes + var maxContentLength: Int = 1024 * 1024 * 10 // 10MB +} +''') + then: + beanDefinition.injectedFields.size() == 0 + beanDefinition.injectedMethods.size() == 1 + beanDefinition.injectedMethods[0].arguments[0].synthesize(ReadableBytes) + } + + void "test different inject types for config properties"() { + when: + BeanDefinition beanDefinition = buildBeanDefinition('test.MyProperties', ''' +package test + +import io.micronaut.context.annotation.* + +@ConfigurationProperties("foo") +open class MyProperties { + protected var fieldTest: String = "unconfigured" + private val privateFinal = true + protected val protectedFinal = true + private var anotherField: Boolean = false + private var internalField = "unconfigured" + + fun setSetterTest(s: String) { + this.internalField = s + } + + fun getSetter() = internalField +} +''') + then: + beanDefinition.injectedMethods.size() == 2 + beanDefinition.injectedMethods.find { it.name == 'setFieldTest' } + beanDefinition.injectedMethods.find {it.name == 'setSetterTest' } + + when: + InstantiatableBeanDefinition factory = beanDefinition + ApplicationContext applicationContext = ApplicationContext.builder().start() + def bean = factory.instantiate(applicationContext) + + then: + bean != null + bean.setter == "unconfigured" + bean.@fieldTest == "unconfigured" + + when: + applicationContext.environment.addPropertySource( + "test", + ['foo.setterTest' :'foo', + 'foo.fieldTest' :'bar'] + ) + bean = factory.instantiate(applicationContext) + + then: + bean != null + bean.setter == "foo" + bean.@fieldTest == "bar" + } + + void "test configuration properties inheritance from non-configuration properties"() { + when: + BeanDefinition beanDefinition = buildBeanDefinition('test.MyProperties', ''' +package test + +import io.micronaut.context.annotation.* + +@ConfigurationProperties("foo") +open class MyProperties: Parent() { + protected var fieldTest: String = "unconfigured" + private val privateFinal = true + protected val protectedFinal = true + private var anotherField: Boolean = false + private var internalField = "unconfigured" + + fun setSetterTest(s: String) { + this.internalField = s + } + + fun getSetter() = internalField +} + +open class Parent { + private var parentField: String? = null + + fun setParentTest(s: String) { + this.parentField = s + } + + fun getParentTest() = parentField +} +''') + then: + beanDefinition.injectedMethods.size() == 3 + + def fieldTest = beanDefinition.injectedMethods.find { it.name == 'setFieldTest'} + fieldTest.getAnnotationMetadata().hasAnnotation(Property) + fieldTest.getAnnotationMetadata().synthesize(Property).name() == 'foo.field-test' + fieldTest.name == 'setFieldTest' + + def setterTest = beanDefinition.injectedMethods.find { it.name == 'setSetterTest'} + setterTest.getAnnotationMetadata().hasAnnotation(Property) + setterTest.getAnnotationMetadata().synthesize(Property).name() == 'foo.setter-test' + setterTest.name == 'setSetterTest' + + def parentTest = beanDefinition.injectedMethods.find { it.name == 'setParentTest'} + parentTest.name == 'setParentTest' + parentTest.getAnnotationMetadata().hasAnnotation(Property) + parentTest.getAnnotationMetadata().synthesize(Property).name() == 'foo.parent-test' + + + when: + InstantiatableBeanDefinition factory = beanDefinition + ApplicationContext applicationContext = ApplicationContext.builder().start() + def bean = factory.instantiate(applicationContext) + + then: + bean != null + bean.setter == "unconfigured" + bean.@fieldTest == "unconfigured" + + when: + applicationContext.environment.addPropertySource( + "test", + ['foo.setterTest' :'foo', + 'foo.fieldTest' :'bar'] + ) + bean = factory.instantiate(applicationContext) + + then: + bean != null + bean.setter == "foo" + bean.@fieldTest == "bar" + } + + void "test boolean fields starting with is[A-Z] map to set methods"() { + when: + BeanDefinition beanDefinition = buildBeanDefinition("micronaut.issuer.FooConfigurationProperties", """ +package micronaut.issuer + +import io.micronaut.context.annotation.ConfigurationProperties + +@ConfigurationProperties("foo") +class FooConfigurationProperties { + + private var issuer: String? = null + private var isEnabled = false + + fun setIssuer(issuer: String) { + this.issuer = issuer + } + + //isEnabled field maps to setEnabled method + fun setEnabled(enabled: Boolean) { + this.isEnabled = enabled + } +} +""") + then: + noExceptionThrown() + beanDefinition.injectedMethods[0].name == "setIssuer" + beanDefinition.injectedMethods[1].name == "setEnabled" + } + + void "test includes on fields"() { + when: + BeanDefinition beanDefinition = buildBeanDefinition('test.MyProperties', ''' +package test + +import io.micronaut.context.annotation.* + +@ConfigurationProperties(value = "foo", includes = ["publicField", "parentPublicField"]) +class MyProperties: Parent() { + var publicField: String? = null + var anotherPublicField: String? = null +} + +open class Parent { + var parentPublicField: String? = null + var anotherParentPublicField: String? = null +} +''') + then: + noExceptionThrown() + beanDefinition.injectedMethods.size() == 2 + beanDefinition.injectedMethods.find { it.name == "setPublicField" } + beanDefinition.injectedMethods.find { it.name == "setParentPublicField" } + } + + void "test includes on methods"() { + when: + BeanDefinition beanDefinition = buildBeanDefinition('test.MyProperties', ''' +package test + +import io.micronaut.context.annotation.* + +@ConfigurationProperties(value = "foo", includes = ["publicMethod", "parentPublicMethod"]) +class MyProperties: Parent() { + + fun setPublicMethod(value: String) {} + fun setAnotherPublicMethod(value: String) {} +} + +open class Parent { + fun setParentPublicMethod(value: String) {} + fun setAnotherParentPublicMethod(value: String) {} +} +''') + then: + noExceptionThrown() + beanDefinition.injectedMethods.size() == 2 + beanDefinition.injectedMethods.find { it.name == "setParentPublicMethod" } + beanDefinition.injectedMethods.find { it.name == "setPublicMethod" } + } + + void "test excludes on fields"() { + when: + BeanDefinition beanDefinition = buildBeanDefinition('test.MyProperties', ''' +package test + +import io.micronaut.context.annotation.* + +@ConfigurationProperties(value = "foo", excludes = ["anotherPublicField", "anotherParentPublicField"]) +class MyProperties: Parent() { + var publicField: String? = null + var anotherPublicField: String? = null +} + +open class Parent { + var parentPublicField: String? = null + var anotherParentPublicField: String? = null +} +''') + then: + noExceptionThrown() + beanDefinition.injectedMethods.size() == 2 + beanDefinition.injectedMethods.find { it.name == "setParentPublicField" } + beanDefinition.injectedMethods.find { it.name == "setPublicField" } + } + + void "test excludes on methods"() { + when: + BeanDefinition beanDefinition = buildBeanDefinition('test.MyProperties', ''' +package test + +import io.micronaut.context.annotation.* + +@ConfigurationProperties(value = "foo", excludes = ["anotherPublicMethod", "anotherParentPublicMethod"]) +class MyProperties: Parent() { + + fun setPublicMethod(value: String) {} + fun setAnotherPublicMethod(value: String) {} +} + +open class Parent { + fun setParentPublicMethod(value: String) {} + fun setAnotherParentPublicMethod(value: String) {} +} +''') + then: + noExceptionThrown() + beanDefinition.injectedMethods.size() == 2 + beanDefinition.injectedMethods.find { it.name == "setParentPublicMethod" } + beanDefinition.injectedMethods.find { it.name == "setPublicMethod" } + } + + void "test excludes on configuration builder"() { + when: + BeanDefinition beanDefinition = buildBeanDefinition('test.MyProperties', ''' +package test + +import io.micronaut.context.annotation.* +import io.micronaut.kotlin.processing.inject.configuration.Engine + +@ConfigurationProperties(value = "foo", excludes = ["engine", "engine2"]) +class MyProperties: Parent() { + + @ConfigurationBuilder(prefixes = ["with"]) + val engine: Engine.Builder = Engine.builder() + + @ConfigurationBuilder(configurationPrefix = "two", prefixes = ["with"]) + var engine2: Engine.Builder = Engine.builder() +} + +open class Parent { + fun setEngine(engine: Engine.Builder) {} +} +''') + then: + noExceptionThrown() + beanDefinition.injectedMethods.isEmpty() + beanDefinition.injectedFields.isEmpty() + + when: + InstantiatableBeanDefinition factory = beanDefinition + ApplicationContext applicationContext = ApplicationContext.run( + 'foo.manufacturer':'Subaru', + 'foo.two.manufacturer':'Subaru' + ) + def bean = factory.instantiate(applicationContext) + + then: + ((Engine.Builder) bean.engine).build().manufacturer == 'Subaru' + ((Engine.Builder) bean.getEngine2()).build().manufacturer == 'Subaru' + } + + void "test name is correct with inner classes of non config props class"() { + when: + BeanDefinition beanDefinition = buildBeanDefinition("test.Test\$TestNestedConfig", ''' +package test + +import io.micronaut.context.annotation.* + +class Test { + + @ConfigurationProperties("test") + class TestNestedConfig { + var vall: String? = null + } +} +''') + + then: + noExceptionThrown() + beanDefinition.injectedMethods[0].annotationMetadata.getAnnotationValuesByType(Property.class).get(0).stringValue("name").get() == "test.vall" + } + + void "test property names with numbers"() { + when: + BeanDefinition beanDefinition = buildBeanDefinition('test.AwsConfig', ''' +package test + +import io.micronaut.context.annotation.ConfigurationProperties + +@ConfigurationProperties("aws") +class AwsConfig { + + var disableEc2Metadata: String? = null + var disableEcMetadata: String? = null + var disableEc2instanceMetadata: String? = null +} +''') + + then: + noExceptionThrown() + beanDefinition.injectedMethods[0].getAnnotationMetadata().getAnnotationValuesByType(Property.class).get(0).stringValue("name").get() == "aws.disable-ec2-metadata" + beanDefinition.injectedMethods[1].getAnnotationMetadata().getAnnotationValuesByType(Property.class).get(0).stringValue("name").get() == "aws.disable-ec-metadata" + beanDefinition.injectedMethods[2].getAnnotationMetadata().getAnnotationValuesByType(Property.class).get(0).stringValue("name").get() == "aws.disable-ec2instance-metadata" + } + + void "test inner interface EachProperty list = true"() { + when: + BeanDefinition beanDefinition = buildBeanDefinition('test.Parent$Child$Intercepted', ''' +package test + +import io.micronaut.context.annotation.ConfigurationProperties +import io.micronaut.context.annotation.EachProperty + +import jakarta.inject.Inject + +@ConfigurationProperties("parent") +class Parent @Inject constructor(val children: List) { + + @EachProperty(value = "children", list = true) + interface Child { + fun getPropA(): String + fun getPropB(): String + } +} +''') + + then: + noExceptionThrown() + beanDefinition != null + beanDefinition.getAnnotationMetadata().stringValue(ConfigurationReader.class, "prefix").get() == "parent.children[*]" + beanDefinition.getRequiredMethod("getPropA").getAnnotationMetadata().getAnnotationValuesByType(Property.class).get(0).stringValue("name").get() == "parent.children[*].prop-a" + } + + void "test config props with post construct first in file"() { + given: + BeanContext context = buildContext(""" +package test + +import io.micronaut.context.annotation.ConfigurationProperties +import jakarta.annotation.PostConstruct + +@ConfigurationProperties("app.entity") +class EntityProperties { + + @PostConstruct + fun init() { + println("prop = " + prop) + } + + var prop: String? = null +} +""") + + when: + context.getBean(context.classLoader.loadClass("test.EntityProperties")) + + then: + noExceptionThrown() + } + + void "test inner class paths - two levels"() { + when: + BeanDefinition beanDefinition = buildBeanDefinition('test.MyConfig$ChildConfig$MoreConfig', ''' +package test + +import io.micronaut.context.annotation.ConfigurationProperties + +@ConfigurationProperties("foo.bar") +class MyConfig { + var host: String? = null + + @ConfigurationProperties("baz") + class ChildConfig { + var stuff: String? = null + + @ConfigurationProperties("more") + class MoreConfig { + var stuff: String? = null + } + } +} +''') + then: + beanDefinition.injectedFields.size() == 0 + beanDefinition.injectedMethods.size() == 1 + beanDefinition.injectedMethods[0].getAnnotationMetadata().hasAnnotation(Property) + beanDefinition.injectedMethods[0].getAnnotationMetadata().synthesize(Property).name() == 'foo.bar.baz.more.stuff' + beanDefinition.injectedMethods[0].name == 'setStuff' + } + + void "test inner class paths - with parent inheritance"() { + when: + BeanDefinition beanDefinition = buildBeanDefinition('test.MyConfig$ChildConfig', ''' +package test + +import io.micronaut.context.annotation.ConfigurationProperties + +@ConfigurationProperties("foo.bar") +class MyConfig: ParentConfig() { + var host: String? = null + + @ConfigurationProperties("baz") + class ChildConfig { + var stuff: String? = null + } +} + +@ConfigurationProperties("parent") +open class ParentConfig +''') + then: + beanDefinition.injectedFields.size() == 0 + beanDefinition.injectedMethods.size() == 1 + beanDefinition.injectedMethods[0].getAnnotationMetadata().hasAnnotation(Property) + beanDefinition.injectedMethods[0].getAnnotationMetadata().synthesize(Property).name() == 'parent.foo.bar.baz.stuff' + beanDefinition.injectedMethods[0].name == 'setStuff' + } + + void "test annotation on setters arguments"() { + when: + BeanDefinition beanDefinition = buildBeanDefinition('test.HttpClientConfiguration', ''' +package test + +import io.micronaut.core.convert.format.ReadableBytes +import io.micronaut.context.annotation.ConfigurationProperties + +@ConfigurationProperties("http.client") +class HttpClientConfiguration { + + @ReadableBytes + var maxContentLength: Int = 1024 * 1024 * 10 + +} +''') + then: + beanDefinition.injectedFields.size() == 0 + beanDefinition.injectedMethods.size() == 1 + beanDefinition.injectedMethods[0].arguments[0].synthesize(ReadableBytes) + } + + void "test different inject types for config properties"() { + when: + BeanDefinition beanDefinition = buildBeanDefinition('test.MyProperties', ''' +package test + +import io.micronaut.context.annotation.ConfigurationProperties + +@ConfigurationProperties("foo") +open class MyProperties { + open var fieldTest: String = "unconfigured" + private val privateFinal = true + open val protectedFinal = true + private val anotherField = false + private var internalField = "unconfigured" + + fun setSetterTest(s: String) { + this.internalField = s + } + + fun getSetter() = internalField +} +''') + then: + beanDefinition != null + beanDefinition.injectedMethods.size() == 2 + beanDefinition.injectedMethods.find { it.name == 'setFieldTest' } + beanDefinition.injectedMethods.find { it.name == 'setSetterTest' } + + when: + InstantiatableBeanDefinition factory = beanDefinition + ApplicationContext applicationContext = ApplicationContext.builder().start() + def bean = factory.instantiate(applicationContext) + + then: + bean != null + bean.setter == "unconfigured" + bean.@fieldTest == "unconfigured" + + when: + applicationContext.environment.addPropertySource( + "test", + ['foo.setterTest' :'foo', + 'foo.fieldTest' :'bar'] + ) + bean = factory.instantiate(applicationContext) + + then: + bean != null + bean.setter == "foo" + bean.@fieldTest == "bar" + } + + void "test configuration properties inheritance from non-configuration properties"() { + when: + BeanDefinition beanDefinition = buildBeanDefinition('test.MyProperties', ''' +package test + +import io.micronaut.context.annotation.ConfigurationProperties + +@ConfigurationProperties("foo") +class MyProperties: Parent() { + + open var fieldTest: String = "unconfigured" + private val privateFinal = true + open val protectedFinal = true + private val anotherField = false + private var internalField = "unconfigured" + + fun setSetterTest(s: String) { + this.internalField = s + } + + fun getSetter() = internalField +} + +open class Parent { + var parentTest: String?= null +} +''') + then: + beanDefinition.injectedMethods.size() == 3 + + def setFieldMethod = beanDefinition.injectedMethods.find { it.name == 'setFieldTest'} + setFieldMethod.name == 'setFieldTest' + setFieldMethod.getAnnotationMetadata().hasAnnotation(Property) + setFieldMethod.getAnnotationMetadata().synthesize(Property).name() == 'foo.field-test' + + + def setParentMethod = beanDefinition.injectedMethods.find { it.name == 'setParentTest'} + setParentMethod.name == 'setParentTest' + setParentMethod.getAnnotationMetadata().hasAnnotation(Property) + setParentMethod.getAnnotationMetadata().synthesize(Property).name() == 'foo.parent-test' + + + def setSetterTest = beanDefinition.injectedMethods.find { it.name == 'setSetterTest'} + setSetterTest.name == 'setSetterTest' + setSetterTest.getAnnotationMetadata().hasAnnotation(Property) + setSetterTest.getAnnotationMetadata().synthesize(Property).name() == 'foo.setter-test' + + when: + InstantiatableBeanDefinition factory = beanDefinition + ApplicationContext applicationContext = ApplicationContext.builder().start() + def bean = factory.instantiate(applicationContext) + + then: + bean != null + bean.setter == "unconfigured" + bean.@fieldTest == "unconfigured" + bean.parentTest == null + + when: + applicationContext.environment.addPropertySource( + "test", + ['foo.setterTest' :'foo', + 'foo.fieldTest' :'bar', + 'foo.parentTest': 'baz'] + ) + bean = factory.instantiate(applicationContext) + + then: + bean != null + bean.setter == "foo" + bean.@fieldTest == "bar" + bean.parentTest == "baz" + } + + void "test includes on properties"() { + when: + BeanDefinition beanDefinition = buildBeanDefinition('test.MyProperties', ''' +package test + +import io.micronaut.context.annotation.ConfigurationProperties + +@ConfigurationProperties(value = "foo", includes = ["publicField", "parentPublicField"]) +class MyProperties: Parent() { + var publicField: String? = null + var anotherPublicField: String? = null +} + +open class Parent { + var parentPublicField: String? = null + var anotherParentPublicField: String? = null +} +''') + then: + noExceptionThrown() + beanDefinition.injectedMethods.size() == 2 + beanDefinition.injectedMethods.find { it.name == "setPublicField" } + beanDefinition.injectedMethods.find { it.name == "setParentPublicField" } + } + + void "test excludes on properties"() { + when: + BeanDefinition beanDefinition = buildBeanDefinition('test.MyProperties', ''' +package test + +import io.micronaut.context.annotation.ConfigurationProperties + +@ConfigurationProperties(value = "foo", excludes = ["anotherPublicField", "anotherParentPublicField"]) +class MyProperties: Parent() { + var publicField: String? = null + var anotherPublicField: String? = null +} + +open class Parent { + var parentPublicField: String? = null + var anotherParentPublicField: String? = null +} +''') + then: + noExceptionThrown() + beanDefinition.injectedMethods.size() == 2 + beanDefinition.injectedMethods.find { it.name == "setPublicField" } + beanDefinition.injectedMethods.find { it.name == "setParentPublicField" } + } + + void "test name is correct with inner classes of non config props class"() { + when: + BeanDefinition beanDefinition = buildBeanDefinition("test.Test\$TestNestedConfig", ''' +package test + +import io.micronaut.context.annotation.ConfigurationProperties + +class Test { + + @ConfigurationProperties("test") + class TestNestedConfig { + var x: String? = null + } + +} +''') + + then: + noExceptionThrown() + beanDefinition.injectedMethods[0].annotationMetadata.getAnnotationValuesByType(Property.class).get(0).stringValue("name").get() == "test.x" + } + + void "test property names with numbers"() { + when: + BeanDefinition beanDefinition = buildBeanDefinition('test.AwsConfig', ''' +package test + +import io.micronaut.context.annotation.ConfigurationProperties + +@ConfigurationProperties("aws") +class AwsConfig { + + var disableEc2Metadata: String? = null + var disableEcMetadata: String? = null + var disableEc2instanceMetadata: String? = null +} +''') + + then: + noExceptionThrown() + beanDefinition.injectedMethods[0].getAnnotationMetadata().getAnnotationValuesByType(Property.class).get(0).stringValue("name").get() == "aws.disable-ec2-metadata" + beanDefinition.injectedMethods[1].getAnnotationMetadata().getAnnotationValuesByType(Property.class).get(0).stringValue("name").get() == "aws.disable-ec-metadata" + beanDefinition.injectedMethods[2].getAnnotationMetadata().getAnnotationValuesByType(Property.class).get(0).stringValue("name").get() == "aws.disable-ec2instance-metadata" + } + + void "test inner class EachProperty list = true"() { + when: + BeanDefinition beanDefinition = buildBeanDefinition('test.Parent$Child', ''' +package test + +import io.micronaut.context.annotation.ConfigurationProperties +import io.micronaut.context.annotation.EachProperty + +import jakarta.inject.Inject + +@ConfigurationProperties("parent") +class Parent(val children: List) { + + @EachProperty(value = "children", list = true) + class Child { + var propA: String? = null + var propB: String? = null + } +} +''') + + then: + noExceptionThrown() + beanDefinition != null + beanDefinition.getAnnotationMetadata().stringValue(ConfigurationReader.class, "prefix").get() == "parent.children[*]" + beanDefinition.injectedMethods[0].getAnnotationMetadata().getAnnotationValuesByType(Property.class).get(0).stringValue("name").get() == "parent.children[*].prop-a" + } + + void "test config props with post construct first in file"() { + given: + BeanContext context = buildContext(""" +package test + +import io.micronaut.context.annotation.ConfigurationProperties +import jakarta.annotation.PostConstruct + +@ConfigurationProperties("app.entity") +class EntityProperties { + + @PostConstruct + fun init() { + println("prop = \$prop") + } + + var prop: String? = null +} +""") + + when: + getBean(context, "test.EntityProperties") + + then: + noExceptionThrown() + } + + void "test configuration properties inheriting config props class"() { + when: + BeanDefinition beanDefinition = buildBeanDefinition('test.MyProperties', ''' +package test + +import io.micronaut.context.annotation.ConfigurationProperties + +@ConfigurationProperties(value = "child") +class MyProperties: Parent() { + var childProp: String? = null +} + +@ConfigurationProperties(value = "parent") +open class Parent { + var prop: String? = null +} +''') + then: + noExceptionThrown() + beanDefinition.injectedMethods.size() == 2 + + def setChildProp = beanDefinition.injectedMethods.find { it.name == 'setChildProp'} + setChildProp.name == "setChildProp" + setChildProp.annotationMetadata.stringValue(Property, "name").get() == "parent.child.child-prop" + + def setProp = beanDefinition.injectedMethods.find { it.name == 'setProp'} + setProp.name == "setProp" + setProp.annotationMetadata.stringValue(Property, "name").get() == "parent.prop" + } + + void "test inner each bean internal constructor"() { + when: + BeanDefinition beanDefinition = buildBeanDefinition("test.ParentEachPropsCtor\$ManagerProps", """ +package test + +import io.micronaut.context.annotation.* + +@EachProperty("teams") +class ParentEachPropsCtor internal constructor( + @Parameter val name: String, + val manager: ManagerProps? +) { + var wins: Int? = null + + @ConfigurationProperties("manager") + class ManagerProps internal constructor(@Parameter val name: String) { + var age: Int? = null + } +} +""") + + then: + noExceptionThrown() + beanDefinition != null + } +} diff --git a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/inject/configproperties/ConfigurationPropertiesBuilderSpec.groovy b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/inject/configproperties/ConfigurationPropertiesBuilderSpec.groovy new file mode 100644 index 00000000000..b20ef8d800e --- /dev/null +++ b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/inject/configproperties/ConfigurationPropertiesBuilderSpec.groovy @@ -0,0 +1,780 @@ +package io.micronaut.kotlin.processing.inject.configproperties + +import io.micronaut.context.ApplicationContext +import io.micronaut.context.env.PropertySource +import io.micronaut.inject.BeanDefinition +import io.micronaut.inject.InstantiatableBeanDefinition +import org.neo4j.driver.v1.Config +import spock.lang.PendingFeature +import spock.lang.Specification +import static io.micronaut.annotation.processing.test.KotlinCompiler.* + +class ConfigurationPropertiesBuilderSpec extends Specification { + + void "test configuration builder on method"() { + given: + BeanDefinition beanDefinition = buildBeanDefinition('test.MyProperties', ''' +package test; + +import io.micronaut.context.annotation.*; + +@ConfigurationProperties("test") +class MyProperties { + + @ConfigurationBuilder(factoryMethod="build") + var test: Test? = null +} + +class Test private constructor() { + + var foo: String? = null + + companion object { + @JvmStatic + fun build(): Test { + return Test() + } + } +} +''') + + when:"The bean was built and a warning was logged" + InstantiatableBeanDefinition factory = beanDefinition + ApplicationContext applicationContext = ApplicationContext.run( + 'test.foo':'good' + ) + def bean = factory.instantiate(applicationContext) + + then: + bean.test.foo == 'good' + } + + void "test configuration builder with includes"() { + given: + BeanDefinition beanDefinition = buildBeanDefinition('test.MyProperties', ''' +package test; + +import io.micronaut.context.annotation.*; + +@ConfigurationProperties("test") +class MyProperties { + + @ConfigurationBuilder(factoryMethod="build", includes=["foo"]) + var test: Test? = null +} + +class Test private constructor() { + + var foo: String? = null + var bar: String? = null + + companion object { + @JvmStatic + fun build(): Test { + return Test() + } + } +} +''') + + when:"The bean was built and a warning was logged" + InstantiatableBeanDefinition factory = beanDefinition + ApplicationContext applicationContext = ApplicationContext.run( + 'test.foo':'good', + 'test.bar':'bad' + ) + def bean = factory.instantiate(applicationContext) + + then: + bean.test.foo == 'good' + bean.test.bar == null + } + + void "test catch and log NoSuchMethodError for when underlying builder changes"() { + given: + BeanDefinition beanDefinition = buildBeanDefinition('test.MyProperties', ''' +package test + +import io.micronaut.context.annotation.* + +@ConfigurationProperties("test") +class MyProperties { + + @ConfigurationBuilder + var test = Test() +} + +class Test { + fun setFoo(s: String) { + throw NoSuchMethodError("setFoo") + } +} +''') + + expect:"The bean was built and a warning was logged" + InstantiatableBeanDefinition factory = beanDefinition + ApplicationContext applicationContext = ApplicationContext.run( + 'test.foo':'good', + ) + factory.instantiate(applicationContext) + } + + void "test with setters that return void"() { + given: + BeanDefinition beanDefinition = buildBeanDefinition('test.MyProperties', ''' +package test + +import io.micronaut.context.annotation.* + +@ConfigurationProperties("test") +class MyProperties { + + @ConfigurationBuilder + var test = Test() +} + +class Test { + var foo: String? = null + var bar: Int = 0 + @Deprecated("message") + var baz: Long? = null +} +''') + + when: + InstantiatableBeanDefinition factory = beanDefinition + ApplicationContext applicationContext = ApplicationContext.run( + 'test.foo':'good', + 'test.bar': '10', + 'test.baz':'20' + ) + def bean = factory.instantiate(applicationContext) + + then: + bean != null + bean.test != null + + when: + def test = bean.test + + then: + test.foo == 'good' + test.bar == 10 + test.baz == null //deprecated properties not settable + } + + void "test different inject types for config properties"() { + when: + BeanDefinition beanDefinition = buildBeanDefinition('test.Neo4jProperties', ''' +package test + +import io.micronaut.context.annotation.* +import org.neo4j.driver.v1.* + +@ConfigurationProperties("neo4j.test") +class Neo4jProperties { + protected var uri: java.net.URI? = null + + @ConfigurationBuilder( + prefixes=["with"], + allowZeroArgs=true + ) + var options: Config.ConfigBuilder = Config.build() +} +''') + then: + beanDefinition.injectedMethods.size() == 1 + beanDefinition.injectedMethods.first().name == 'setUri' + + when: + InstantiatableBeanDefinition factory = beanDefinition + ApplicationContext applicationContext = ApplicationContext.run( + 'neo4j.test.encryptionLevel':'none', + 'neo4j.test.leakedSessionsLogging':true, + 'neo4j.test.maxIdleSessions':2 + ) + def bean = factory.instantiate(applicationContext) + + then: + bean != null + bean.options != null + + when: + Config config = bean.options.toConfig() + + then: + config.maxIdleConnectionPoolSize() == 2 + config.encrypted() == true // deprecated properties are ignored + config.logLeakedSessions() + } + + void "test specifying a configuration prefix"() { + when: + BeanDefinition beanDefinition = buildBeanDefinition('test.Neo4jProperties', ''' +package test + +import io.micronaut.context.annotation.* +import org.neo4j.driver.v1.* + +@ConfigurationProperties("neo4j.test") +class Neo4jProperties { + protected var uri: java.net.URI? = null + + @ConfigurationBuilder( + prefixes=["with"], + allowZeroArgs=true, + configurationPrefix="options" + ) + var options: Config.ConfigBuilder = Config.build() +} +''') + then: + beanDefinition.injectedMethods.size() == 1 + beanDefinition.injectedMethods.first().name == 'setUri' + + when: + InstantiatableBeanDefinition factory = beanDefinition + ApplicationContext applicationContext = ApplicationContext.run( + 'neo4j.test.options.encryptionLevel':'none', + 'neo4j.test.options.leakedSessionsLogging':true, + 'neo4j.test.options.maxIdleSessions':2 + ) + def bean = factory.instantiate(applicationContext) + + then: + bean != null + bean.options != null + + when: + Config config = bean.options.toConfig() + + then: + config.maxIdleConnectionPoolSize() == 2 + config.encrypted() == true // deprecated properties are ignored + config.logLeakedSessions() + } + + void "test specifying a configuration prefix with value"() { + when: + BeanDefinition beanDefinition = buildBeanDefinition('test.Neo4jProperties', ''' +package test + +import io.micronaut.context.annotation.* +import org.neo4j.driver.v1.* + +@ConfigurationProperties("neo4j.test") +class Neo4jProperties { + protected var uri: java.net.URI? = null + + @ConfigurationBuilder( + prefixes=["with"], + allowZeroArgs=true, + value="options" + ) + var options: Config.ConfigBuilder = Config.build() +} +''') + then: + beanDefinition.injectedMethods.size() == 1 + beanDefinition.injectedMethods.first().name == 'setUri' + + when: + InstantiatableBeanDefinition factory = beanDefinition + ApplicationContext applicationContext = ApplicationContext.run( + 'neo4j.test.options.encryptionLevel':'none', + 'neo4j.test.options.leakedSessionsLogging':true, + 'neo4j.test.options.maxIdleSessions':2 + ) + def bean = factory.instantiate(applicationContext) + + then: + bean != null + bean.options != null + + when: + Config config = bean.options.toConfig() + + then: + config.maxIdleConnectionPoolSize() == 2 + config.encrypted() == true // deprecated properties are ignored + config.logLeakedSessions() + } + + void "test specifying a configuration prefix with value using @AccessorsStyle"() { + when: + BeanDefinition beanDefinition = buildBeanDefinition('test.Neo4jProperties', ''' +package test; + +import io.micronaut.context.annotation.*; +import io.micronaut.core.annotation.AccessorsStyle; +import org.neo4j.driver.v1.*; + +@ConfigurationProperties("neo4j.test") +class Neo4jProperties { + protected var uri: java.net.URI? = null + + @ConfigurationBuilder( + allowZeroArgs = true, + value = "options" + ) + @AccessorsStyle(writePrefixes = ["with"]) + var options: Config.ConfigBuilder = Config.build() +} +''') + then: + beanDefinition.injectedMethods.size() == 1 + beanDefinition.injectedMethods.first().name == 'setUri' + + when: + InstantiatableBeanDefinition factory = beanDefinition + ApplicationContext applicationContext = ApplicationContext.run( + 'neo4j.test.options.encryptionLevel':'none', + 'neo4j.test.options.leakedSessionsLogging':true, + 'neo4j.test.options.maxIdleSessions':2 + ) + def bean = factory.instantiate(applicationContext) + + then: + bean != null + bean.options != null + + when: + Config config = bean.options.toConfig() + + then: + config.maxIdleConnectionPoolSize() == 2 + config.encrypted() == true // deprecated properties are ignored + config.logLeakedSessions() + } + + void "test builder method long and TimeUnit arguments"() { + when: + BeanDefinition beanDefinition = buildBeanDefinition('test.Neo4jProperties', ''' +package test + +import io.micronaut.context.annotation.* +import org.neo4j.driver.v1.* + +@ConfigurationProperties("neo4j.test") +class Neo4jProperties { + protected var uri: java.net.URI? = null + + @ConfigurationBuilder( + prefixes=["with"], + allowZeroArgs=true + ) + var options: Config.ConfigBuilder = Config.build() + +} +''') + then: + beanDefinition.injectedMethods.size() == 1 + beanDefinition.injectedMethods.first().name == 'setUri' + + when: + InstantiatableBeanDefinition factory = beanDefinition + ApplicationContext applicationContext = ApplicationContext.run( + 'neo4j.test.connectionLivenessCheckTimeout': '6s' + ) + def bean = factory.instantiate(applicationContext) + + then: + bean != null + bean.options != null + + when: + Config config = bean.options.toConfig() + + then: + config.idleTimeBeforeConnectionTest() == 6000 + } + + void "test using a builder that is marked final"() { + when: + BeanDefinition beanDefinition = buildBeanDefinition('test.Neo4jProperties', ''' +package test + +import io.micronaut.context.annotation.* +import org.neo4j.driver.v1.* + +@ConfigurationProperties("neo4j.test") +class Neo4jProperties { + + @ConfigurationBuilder( + prefixes=["with"], + allowZeroArgs=true + ) + val options: Config.ConfigBuilder = Config.build() + +} +''') + InstantiatableBeanDefinition factory = beanDefinition + ApplicationContext applicationContext = ApplicationContext.run( + 'neo4j.test.connectionLivenessCheckTimeout': '17s' + ) + def bean = factory.instantiate(applicationContext) + + then: + bean != null + bean.options != null + + when: + Config config = bean.options.toConfig() + + then: + config.idleTimeBeforeConnectionTest() == 17000 + } + + void "test with setter methods that return this"() { + given: + BeanDefinition beanDefinition = buildBeanDefinition('test.MyProperties', ''' +package test + +import io.micronaut.context.annotation.* + +@ConfigurationProperties("test") +class MyProperties { + + @ConfigurationBuilder(factoryMethod="build") + var test: Test? = null +} + +class Test private constructor() { + + private var foo: String? = null + + fun getFoo() = foo + + fun setFoo(foo: String): Test { + this.foo = foo + return this + } + + private var bar: Int = 0 + + fun getBar() = bar + + fun setBar(bar: Int): Test { + this.bar = bar + return this + } + + private var baz: Long? = null + + fun getBaz() = baz + + @Deprecated("do not use") + fun setBaz(baz: Long): Test { + this.baz = baz + return this + } + + companion object { + @JvmStatic fun build(): Test { + return Test() + } + } +} +''') + + when: + InstantiatableBeanDefinition factory = beanDefinition + ApplicationContext applicationContext = ApplicationContext.run( + 'test.foo':'good', + 'test.bar': '10', + 'test.baz':'20' + ) + def bean = factory.instantiate(applicationContext) + + then: + bean != null + bean.test != null + + when: + def test = bean.test + + then: + test.foo == 'good' + test.bar == 10 + test.baz == null //deprecated properties not settable + } + + void "test different inject types for config properties"() { + when: + BeanDefinition beanDefinition = buildBeanDefinition('test.Neo4jProperties', ''' +package test + +import io.micronaut.context.annotation.* +import org.neo4j.driver.v1.* + +@ConfigurationProperties("neo4j.test") +class Neo4jProperties { + internal var uri: java.net.URI? = null + + @ConfigurationBuilder( + prefixes=["with"], + allowZeroArgs=true + ) + val options: Config.ConfigBuilder = Config.build() +} +''') + InstantiatableBeanDefinition factory = beanDefinition + ApplicationContext applicationContext = ApplicationContext.run( + 'neo4j.test.encryptionLevel':'none', + 'neo4j.test.leakedSessionsLogging':true, + 'neo4j.test.maxIdleSessions':2 + ) + def bean = factory.instantiate(applicationContext) + + then: + bean != null + bean.options != null + + when: + Config config = bean.options.toConfig() + + then: + config.maxIdleConnectionPoolSize() == 2 + config.encrypted() == true // deprecated properties are ignored + config.logLeakedSessions() + } + + void "test specifying a configuration prefix"() { + when: + BeanDefinition beanDefinition = buildBeanDefinition('test.Neo4jProperties', ''' +package test + +import io.micronaut.context.annotation.* +import org.neo4j.driver.v1.* + +@ConfigurationProperties("neo4j.test") +class Neo4jProperties { + internal var uri: java.net.URI? = null + + @ConfigurationBuilder( + prefixes=["with"], + allowZeroArgs=true, + configurationPrefix="options" + ) + val options: Config.ConfigBuilder = Config.build() +} +''') + then: + beanDefinition.injectedMethods.size() == 1 + beanDefinition.injectedMethods.first().name == 'setUri$main' + + when: + InstantiatableBeanDefinition factory = beanDefinition + ApplicationContext applicationContext = ApplicationContext.run( + 'neo4j.test.options.encryptionLevel':'none', + 'neo4j.test.options.leakedSessionsLogging':true, + 'neo4j.test.options.maxIdleSessions':2 + ) + def bean = factory.instantiate(applicationContext) + + then: + bean != null + bean.options != null + + when: + Config config = bean.options.toConfig() + + then: + config.maxIdleConnectionPoolSize() == 2 + config.encrypted() == true // deprecated properties are ignored + config.logLeakedSessions() + } + + void "test specifying a configuration prefix with value"() { + when: + BeanDefinition beanDefinition = buildBeanDefinition('test.Neo4jProperties', ''' +package test; + +import io.micronaut.context.annotation.*; +import org.neo4j.driver.v1.*; + +@ConfigurationProperties("neo4j.test") +class Neo4jProperties { + internal var uri: java.net.URI? = null + + @ConfigurationBuilder( + prefixes=["with"], + allowZeroArgs=true, + value="options" + ) + val options: Config.ConfigBuilder = Config.build() + + +} +''') + InstantiatableBeanDefinition factory = beanDefinition + ApplicationContext applicationContext = ApplicationContext.run( + 'neo4j.test.options.encryptionLevel':'none', + 'neo4j.test.options.leakedSessionsLogging':true, + 'neo4j.test.options.maxIdleSessions':2 + ) + def bean = factory.instantiate(applicationContext) + + then: + bean != null + bean.options != null + + when: + Config config = bean.options.toConfig() + + then: + config.maxIdleConnectionPoolSize() == 2 + config.encrypted() == true // deprecated properties are ignored + config.logLeakedSessions() + } + + void "test specifying a configuration prefix with value using @AccessorsStyle"() { + when: + BeanDefinition beanDefinition = buildBeanDefinition('test.Neo4jProperties', ''' +package test + +import io.micronaut.context.annotation.* +import io.micronaut.core.annotation.AccessorsStyle +import org.neo4j.driver.v1.* + +@ConfigurationProperties("neo4j.test") +class Neo4jProperties { + internal var uri: java.net.URI? = null + + @ConfigurationBuilder( + allowZeroArgs = true, + value = "options" + ) + @AccessorsStyle(writePrefixes = ["with"]) + val options: Config.ConfigBuilder = Config.build() +} +''') + InstantiatableBeanDefinition factory = beanDefinition + ApplicationContext applicationContext = ApplicationContext.run( + 'neo4j.test.options.encryptionLevel':'none', + 'neo4j.test.options.leakedSessionsLogging':true, + 'neo4j.test.options.maxIdleSessions':2 + ) + def bean = factory.instantiate(applicationContext) + + then: + bean != null + bean.options != null + + when: + Config config = bean.options.toConfig() + + then: + config.maxIdleConnectionPoolSize() == 2 + config.encrypted() == true // deprecated properties are ignored + config.logLeakedSessions() + } + + void "test builder method long and TimeUnit arguments"() { + when: + BeanDefinition beanDefinition = buildBeanDefinition('test.Neo4jProperties', ''' +package test + +import io.micronaut.context.annotation.* +import org.neo4j.driver.v1.* + +@ConfigurationProperties("neo4j.test") +class Neo4jProperties { + internal var uri: java.net.URI? = null + + @ConfigurationBuilder( + prefixes=["with"], + allowZeroArgs=true + ) + val options: Config.ConfigBuilder = Config.build() + +} +''') + InstantiatableBeanDefinition factory = beanDefinition + ApplicationContext applicationContext = ApplicationContext.run( + 'neo4j.test.connectionLivenessCheckTimeout': '6s' + ) + def bean = factory.instantiate(applicationContext) + + then: + bean != null + bean.options != null + + when: + Config config = bean.options.toConfig() + + then: + config.idleTimeBeforeConnectionTest() == 6000 + } + + void "test configuration builder that are interfaces"() { + given: + ApplicationContext ctx = buildContext(''' +package test + +import io.micronaut.context.annotation.* +import io.micronaut.kotlin.processing.beans.configproperties.AnnWithClass + +@ConfigurationProperties("pool") +class PoolConfig { + + @ConfigurationBuilder(prefixes = [""]) + var builder: ConnectionPool.Builder = DefaultConnectionPool.builder() + +} + +interface ConnectionPool { + + interface Builder { + fun maxConcurrency(maxConcurrency: Int?): Builder + fun foo(foo: Foo): Builder + fun build(): ConnectionPool + } + + fun getMaxConcurrency(): Int? +} + +class DefaultConnectionPool(private val maxConcurrency: Int?): ConnectionPool { + + companion object { + @JvmStatic + fun builder(): ConnectionPool.Builder { + return DefaultBuilder() + } + } + + override fun getMaxConcurrency(): Int? = maxConcurrency + + private class DefaultBuilder: ConnectionPool.Builder { + + private var maxConcurrency: Int? = null + + override fun maxConcurrency(maxConcurrency: Int?): ConnectionPool.Builder{ + this.maxConcurrency = maxConcurrency + return this + } + + override fun foo(foo: Foo): ConnectionPool.Builder { + return this + } + + override fun build(): ConnectionPool{ + return DefaultConnectionPool(maxConcurrency) + } + } +} + +@AnnWithClass(String::class) +interface Foo +''') + ctx.getEnvironment().addPropertySource(PropertySource.of(["pool.max-concurrency": 123])) + + when: + Class testProps = ctx.classLoader.loadClass("test.PoolConfig") + def testPropBean = ctx.getBean(testProps) + + then: + noExceptionThrown() + testPropBean.builder.build().getMaxConcurrency() == 123 + } +} diff --git a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/inject/configproperties/ConfigurationPropertiesFactorySpec.groovy b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/inject/configproperties/ConfigurationPropertiesFactorySpec.groovy new file mode 100644 index 00000000000..12ddc6d3654 --- /dev/null +++ b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/inject/configproperties/ConfigurationPropertiesFactorySpec.groovy @@ -0,0 +1,14 @@ +package io.micronaut.kotlin.processing.inject.configproperties + +import io.micronaut.context.ApplicationContext +import spock.lang.Specification + +class ConfigurationPropertiesFactorySpec extends Specification { + + void "test replacing a configuration properties via a factory"() { + ApplicationContext ctx = ApplicationContext.run(["spec.name": ConfigurationPropertiesFactorySpec.simpleName]) + + expect: + ctx.getBean(Neo4jProperties).uri.getHost() == "google.com" + } +} diff --git a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/inject/configproperties/ConfigurationPropertiesSpec.groovy b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/inject/configproperties/ConfigurationPropertiesSpec.groovy new file mode 100644 index 00000000000..6931bc80712 --- /dev/null +++ b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/inject/configproperties/ConfigurationPropertiesSpec.groovy @@ -0,0 +1,133 @@ +package io.micronaut.kotlin.processing.inject.configproperties + +import io.micronaut.context.ApplicationContext +import io.micronaut.context.DefaultApplicationContext +import io.micronaut.context.env.PropertySource +import io.micronaut.core.util.CollectionUtils +import spock.lang.Specification + +class ConfigurationPropertiesSpec extends Specification { + + void "test submap with generics binding"() { + given: + ApplicationContext ctx = ApplicationContext.run( + 'foo.bar.map.key1.key2.property':10, + 'foo.bar.map.key1.key2.property2.property':10 + ) + + expect: + ctx.getBean(MyConfig).map.containsKey('key1') + ctx.getBean(MyConfig).map.get("key1") instanceof Map + ctx.getBean(MyConfig).map.get("key1").get("key2") instanceof MyConfig.Value + ctx.getBean(MyConfig).map.get("key1").get("key2").property == 10 + ctx.getBean(MyConfig).map.get("key1").get("key2").property2 + ctx.getBean(MyConfig).map.get("key1").get("key2").property2.property == 10 + + cleanup: + ctx.close() + } + + void "test submap with generics binding and conversion"() { + given: + ApplicationContext ctx = ApplicationContext.run( + 'foo.bar.map.key1.key2.property':'10', + 'foo.bar.map.key1.key2.property2.property':'10' + ) + + expect: + ctx.getBean(MyConfig).map.containsKey('key1') + ctx.getBean(MyConfig).map.get("key1") instanceof Map + ctx.getBean(MyConfig).map.get("key1").get("key2") instanceof MyConfig.Value + ctx.getBean(MyConfig).map.get("key1").get("key2").property == 10 + ctx.getBean(MyConfig).map.get("key1").get("key2").property2 + ctx.getBean(MyConfig).map.get("key1").get("key2").property2.property == 10 + + cleanup: + ctx.close() + } + + void "test configuration properties binding"() { + given: + ApplicationContext applicationContext = new DefaultApplicationContext("test") + applicationContext.environment.addPropertySource(PropertySource.of( + 'test', + ['foo.bar.innerVals': [ + ['expire-unsigned-seconds': 123], ['expireUnsignedSeconds': 600] + ], + 'foo.bar.port':'8080', + 'foo.bar.max-size':'1MB', + 'foo.bar.another-size':'1MB', + 'foo.bar.anotherPort':'9090', + 'foo.bar.intList':"1,2,3", + 'foo.bar.stringList':"1,2", + 'foo.bar.flags.one':'1', + 'foo.bar.flags.two':'2', + 'foo.bar.urlList':"http://test.com, http://test2.com", + 'foo.bar.urlList2':["http://test.com", "http://test2.com"], + 'foo.bar.url':'http://test.com'] + )) + + applicationContext.start() + + MyConfig config = applicationContext.getBean(MyConfig) + + expect: + config.innerVals.size() == 2 + config.innerVals[0].expireUnsignedSeconds == 123 + config.innerVals[1].expireUnsignedSeconds == 600 + config.port == 8080 + config.maxSize == 1048576 + config.anotherPort == 9090 + config.intList == [1,2,3] + config.flags == [one:1, two:2] + config.urlList == [new URL('http://test.com'),new URL('http://test2.com')] + config.urlList2 == [new URL('http://test.com'),new URL('http://test2.com')] + config.stringList == ["1", "2"] + config.emptyList == null + config.url.get() == new URL('http://test.com') + !config.anotherUrl.isPresent() + config.defaultPort == 9999 + config.defaultValue == 9999 + } + + void "test configuration inner class properties binding"() { + given: + ApplicationContext applicationContext = new DefaultApplicationContext("test") + applicationContext.environment.addPropertySource(PropertySource.of( + 'foo.bar.inner.enabled':'true', + )) + + applicationContext.start() + + MyConfig config = applicationContext.getBean(MyConfig) + + expect: + config.inner.enabled + } + + void "test binding to a map property"() { + ApplicationContext context = ApplicationContext.run(CollectionUtils.mapOf("map.property.yyy.zzz", 3, "map.property.yyy.xxx", 2, "map.property.yyy.yyy", 3)) + MapProperties config = context.getBean(MapProperties.class) + + expect: + config.property.containsKey('yyy') + + cleanup: + context.close() + } + + void "test camelCase vs kebab_case"() { + ApplicationContext context1 = ApplicationContext.run("rec1") + ApplicationContext context2 = ApplicationContext.run("rec2") + + RecConf config1 = context1.getBean(RecConf.class) + RecConf config2 = context2.getBean(RecConf.class) + + expect: + config1 == config2 + + cleanup: + context1.close() + context2.close() + } +} diff --git a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/inject/configproperties/ConfigurationPropertiesWithRawMapSpec.groovy b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/inject/configproperties/ConfigurationPropertiesWithRawMapSpec.groovy new file mode 100644 index 00000000000..a275d33d696 --- /dev/null +++ b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/inject/configproperties/ConfigurationPropertiesWithRawMapSpec.groovy @@ -0,0 +1,23 @@ +package io.micronaut.kotlin.processing.inject.configproperties + +import io.micronaut.context.ApplicationContext +import spock.lang.Specification + +class ConfigurationPropertiesWithRawMapSpec extends Specification { + + void 'test that injected raw properties are correct'() { + given: + ApplicationContext context = ApplicationContext.run( + 'jpa.properties.hibernate.fooBar':'good', + 'jpa.properties.hibernate.CAP':'whatever' + ) + + expect:"when using StringConvention.RAW the map is injected as is" + context.getBean(MyHibernateConfig) + .properties == ['hibernate.fooBar':'good', 'hibernate.CAP': 'whatever'] + + and:"When not using StringConvention.RAW then you get the normalized versions" + context.getBean(MyHibernateConfig2) + .properties == ['hibernate.foo-bar':'good', 'hibernate.cap': 'whatever'] + } +} diff --git a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/inject/configproperties/ImmutableConfigurationPropertiesSpec.groovy b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/inject/configproperties/ImmutableConfigurationPropertiesSpec.groovy new file mode 100644 index 00000000000..36fb65b58c6 --- /dev/null +++ b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/inject/configproperties/ImmutableConfigurationPropertiesSpec.groovy @@ -0,0 +1,164 @@ +package io.micronaut.kotlin.processing.inject.configproperties + +import io.micronaut.context.ApplicationContext +import io.micronaut.context.DefaultBeanResolutionContext +import io.micronaut.context.annotation.Property +import io.micronaut.core.naming.Named +import io.micronaut.inject.BeanDefinition +import io.micronaut.inject.InstantiatableBeanDefinition +import io.micronaut.inject.ValidatedBeanDefinition +import spock.lang.Specification + +import javax.validation.Constraint + +import static io.micronaut.annotation.processing.test.KotlinCompiler.* + +class ImmutableConfigurationPropertiesSpec extends Specification { + + void 'test interface immutable properties'() { + when: + BeanDefinition beanDefinition = buildBeanDefinition('interfaceprops.MyConfig$Intercepted', ''' +package interfaceprops + +import io.micronaut.context.annotation.EachProperty + +@EachProperty("foo.bar") +interface MyConfig { + + @javax.validation.constraints.NotBlank + fun getHost(): String + + fun getPort(): Int +} + + +''') + then: + beanDefinition instanceof ValidatedBeanDefinition + beanDefinition.getRequiredMethod("getHost").synthesize(Property).name() == 'foo.bar.*.host' + beanDefinition.getRequiredMethod("getPort").synthesize(Property).name() == 'foo.bar.*.port' + } + + void "test parse immutable configuration properties"() { + + when: + BeanDefinition beanDefinition = buildBeanDefinition('test.MyConfig', ''' +package test + +import io.micronaut.context.annotation.* + +@ConfigurationProperties("foo.bar") +class MyConfig @ConfigurationInject constructor(@javax.validation.constraints.NotBlank val host: String, val serverPort: Int) + +''') + def arguments = beanDefinition.constructor.arguments + then: + beanDefinition instanceof ValidatedBeanDefinition + arguments.length == 2 + arguments[0].synthesize(Property) + .name() == 'foo.bar.host' + arguments[1].synthesize(Property) + .name() == 'foo.bar.server-port' + + when: + def context = ApplicationContext.run('foo.bar.host': 'test', 'foo.bar.server-port': '9999') + def config = ((InstantiatableBeanDefinition) beanDefinition).instantiate(context) + + then: + config.host == 'test' + config.serverPort == 9999 + + cleanup: + context.close() + } + + void "test parse immutable configuration properties - child config"() { + when: + BeanDefinition beanDefinition = buildBeanDefinition('test.MyConfig$ChildConfig', ''' +package test + +import io.micronaut.context.annotation.* + +@ConfigurationProperties("foo.bar") +class MyConfig @ConfigurationInject constructor(@javax.validation.constraints.NotBlank val host: String, val serverPort: Int) { + + @ConfigurationProperties("baz") + class ChildConfig @ConfigurationInject constructor(val stuff: String) +} + +''') + def arguments = beanDefinition.constructor.arguments + then: + arguments.length == 1 + arguments[0].synthesize(Property) + .name() == 'foo.bar.baz.stuff' + + when: + def context = ApplicationContext.run('foo.bar.baz.stuff': 'test') + def config = ((InstantiatableBeanDefinition) beanDefinition).instantiate(context) + + then: + config.stuff == 'test' + + cleanup: + context.close() + + } + + void "test parse immutable configuration properties - each property"() { + + when: + ApplicationContext context = buildContext( ''' +package test; + +import io.micronaut.context.annotation.*; +import java.time.Duration; + +@EachProperty("foo.bar") +class MyConfig @ConfigurationInject constructor(@javax.validation.constraints.NotBlank val host: String, val serverPort: Int) +''', false, ['foo.bar.one.host': 'test', 'foo.bar.one.server-port': '9999']) + def config = getBean(context, 'test.MyConfig') + + then: + config.host == 'test' + config.serverPort == 9999 + + cleanup: + context.close() + } + + + void "test parse immutable configuration properties - init method"() { + + when: + BeanDefinition beanDefinition = buildBeanDefinition('test.MyConfig', ''' +package test; + +import io.micronaut.context.annotation.*; +import java.time.Duration; + +@ConfigurationProperties("foo.bar") +class MyConfig { + var host: String? = null + private set + var serverPort: Int = 0 + private set + + @ConfigurationInject + fun init(host: String, serverPort: Int) { + this.host = host + this.serverPort = serverPort + } +} +''') + def context = ApplicationContext.run('foo.bar.host': 'test', 'foo.bar.server-port': '9999') + def config = ((InstantiatableBeanDefinition) beanDefinition).instantiate(context) + + then: + config.host == 'test' + config.serverPort == 9999 + + cleanup: + context.close() + } +} diff --git a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/inject/configproperties/InterfaceConfigurationPropertiesSpec.groovy b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/inject/configproperties/InterfaceConfigurationPropertiesSpec.groovy new file mode 100644 index 00000000000..bc475ab685c --- /dev/null +++ b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/inject/configproperties/InterfaceConfigurationPropertiesSpec.groovy @@ -0,0 +1,280 @@ +package io.micronaut.kotlin.processing.inject.configproperties + +import io.micronaut.context.ApplicationContext +import io.micronaut.context.annotation.Property +import io.micronaut.context.exceptions.NoSuchBeanException +import io.micronaut.inject.BeanDefinition +import io.micronaut.inject.InstantiatableBeanDefinition +import io.micronaut.inject.ValidatedBeanDefinition +import io.micronaut.runtime.context.env.ConfigurationAdvice +import spock.lang.Specification +import static io.micronaut.annotation.processing.test.KotlinCompiler.* + +class InterfaceConfigurationPropertiesSpec extends Specification { + + + void "test simple interface config props"() { + when: + BeanDefinition beanDefinition = buildBeanDefinition('test.MyConfig$Intercepted', ''' +package test + +import io.micronaut.context.annotation.* + +@ConfigurationProperties("foo.bar") +interface MyConfig { + @javax.validation.constraints.NotBlank + fun getHost(): String? + + @javax.validation.constraints.Min(10L) + fun getServerPort(): Int +} +''') + then: + beanDefinition.getAnnotationMetadata().getAnnotationType(ConfigurationAdvice.class.getName()).isPresent() + beanDefinition instanceof ValidatedBeanDefinition + beanDefinition.getRequiredMethod("getHost") + .stringValue(Property, "name").get() == 'foo.bar.host' + beanDefinition.getRequiredMethod("getServerPort") + .stringValue(Property, "name").get() == 'foo.bar.server-port' + + when: + def context = ApplicationContext.run('foo.bar.host': 'test', 'foo.bar.server-port': '9999') + def config = ((InstantiatableBeanDefinition) beanDefinition).instantiate(context) + + then: + config.host == 'test' + config.serverPort == 9999 + + cleanup: + context.close() + } + + void "test optional interface config props"() { + + when: + BeanDefinition beanDefinition = buildBeanDefinition('test.MyConfig$Intercepted', ''' +package test + +import io.micronaut.context.annotation.* +import java.net.URL +import java.util.Optional + +@ConfigurationProperties("foo.bar") +@Executable +interface MyConfig { + + fun getHost(): String? + + @javax.validation.constraints.Min(10L) + fun getServerPort(): Optional + + @io.micronaut.core.bind.annotation.Bindable(defaultValue = "http://default") + fun getURL(): Optional +} + +''') + then: + beanDefinition.getAnnotationMetadata().getAnnotationType(ConfigurationAdvice.class.getName()).isPresent() + beanDefinition instanceof ValidatedBeanDefinition + beanDefinition.getRequiredMethod("getHost") + .stringValue(Property, "name").get() == 'foo.bar.host' + beanDefinition.getRequiredMethod("getServerPort") + .stringValue(Property, "name").get() == 'foo.bar.server-port' + beanDefinition.getRequiredMethod("getURL") + .stringValue(Property, "name").get() == 'foo.bar.url' + + when: + def context = ApplicationContext.run() + def config = ((InstantiatableBeanDefinition) beanDefinition).instantiate(context) + + then: + config.host == null + config.serverPort == Optional.empty() + config.URL == Optional.of(new URL("http://default")) + + when: + def context2 = ApplicationContext.run('foo.bar.host': 'test', 'foo.bar.server-port': '9999', 'foo.bar.url': 'http://test') + def config2 = ((InstantiatableBeanDefinition) beanDefinition).instantiate(context2) + + then: + config2.host == 'test' + config2.serverPort == Optional.of(9999) + config2.URL == Optional.of(new URL("http://test")) + + cleanup: + context.close() + context2.close() + } + + void "test inheritance interface config props"() { + when: + BeanDefinition beanDefinition = buildBeanDefinition('test.MyConfig$Intercepted', ''' +package test; + +import io.micronaut.context.annotation.* + +@ConfigurationProperties("bar") +interface MyConfig: ParentConfig { + + @Executable + @javax.validation.constraints.Min(10L) + fun getServerPort(): Int +} + +@ConfigurationProperties("foo") +interface ParentConfig { + + @Executable + @javax.validation.constraints.NotBlank + fun getHost(): String? +} + +''') + then: + beanDefinition instanceof ValidatedBeanDefinition + beanDefinition.getRequiredMethod("getHost") + .stringValue(Property, "name").get() == 'foo.bar.host' + beanDefinition.getRequiredMethod("getServerPort") + .stringValue(Property, "name").get() == 'foo.bar.server-port' + + when: + def context = ApplicationContext.run('foo.bar.host': 'test', 'foo.bar.server-port': '9999') + def config = ((InstantiatableBeanDefinition) beanDefinition).instantiate(context) + + then: + config.host == 'test' + config.serverPort == 9999 + + cleanup: + context.close() + + } + + void "test nested interface config props"() { + + when: + BeanDefinition beanDefinition = buildBeanDefinition('test.MyConfig$ChildConfig$Intercepted', ''' +package test + +import io.micronaut.context.annotation.* +import java.net.URL + +@ConfigurationProperties("foo.bar") +interface MyConfig { + @Executable + @javax.validation.constraints.NotBlank + fun getHost(): String? + + @Executable + @javax.validation.constraints.Min(10L) + fun getServerPort(): Int + + @ConfigurationProperties("child") + interface ChildConfig { + @Executable + fun getURL(): URL? + } +} +''') + then: + beanDefinition instanceof BeanDefinition + beanDefinition.getRequiredMethod("getURL") + .stringValue(Property, "name").get() == 'foo.bar.child.url' + + when: + def context = ApplicationContext.run('foo.bar.child.url': 'http://test') + def config = ((InstantiatableBeanDefinition) beanDefinition).instantiate(context) + + then: + config.URL == new URL("http://test") + + cleanup: + context.close() + } + + void "test nested interface config props - get child"() { + when: + BeanDefinition beanDefinition = buildBeanDefinition('test.MyConfig$Intercepted', ''' +package test + +import io.micronaut.context.annotation.* +import java.net.URL + +@ConfigurationProperties("foo.bar") +interface MyConfig { + @javax.validation.constraints.NotBlank + @Executable + fun getHost(): String + + @javax.validation.constraints.Min(10L) + @Executable + fun getServerPort(): Int + + @Executable + fun getChild(): ChildConfig + + @ConfigurationProperties("child") + interface ChildConfig { + @Executable + fun getURL(): URL? + } +} + +''') + then: + beanDefinition instanceof BeanDefinition + def method = beanDefinition.getRequiredMethod("getChild") + method.isTrue(ConfigurationAdvice, "bean") + + when: + def context = ApplicationContext.run('foo.bar.child.url': 'http://test') + def config = ((InstantiatableBeanDefinition) beanDefinition).instantiate(context) + config.child + + then:"we expect a bean resolution" + def e = thrown(NoSuchBeanException) + e.message.contains("No bean of type [test.MyConfig\$ChildConfig] exists") + + cleanup: + context.close() + } + + void "test invalid method"() { + when: + buildBeanDefinition('test.MyConfig$Intercepted', ''' +package test + +import io.micronaut.context.annotation.* + +@ConfigurationProperties("foo.bar") +interface MyConfig { + @javax.validation.constraints.NotBlank + fun junk(s: String): String + + @javax.validation.constraints.Min(10L) + fun getServerPort(): Int +} + +''') + then: + def e = thrown(RuntimeException) + e.message.contains('Only getter methods are allowed on @ConfigurationProperties interfaces: junk(java.lang.String). You can change the accessors using @AccessorsStyle annotation'); + } + + void "test getter that returns void method"() { + when: + buildBeanDefinition('test.MyConfig$Intercepted', ''' +package test + +import io.micronaut.context.annotation.* + +@ConfigurationProperties("foo.bar") +interface MyConfig { + fun getServerPort() +} +''') + then: + def e = thrown(RuntimeException) + e.message.contains('Getter methods must return a value @ConfigurationProperties interfaces') + } +} diff --git a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/inject/configproperties/PrimitiveConfigurationPropertiesSpec.groovy b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/inject/configproperties/PrimitiveConfigurationPropertiesSpec.groovy new file mode 100644 index 00000000000..065e56e9198 --- /dev/null +++ b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/inject/configproperties/PrimitiveConfigurationPropertiesSpec.groovy @@ -0,0 +1,27 @@ +package io.micronaut.kotlin.processing.inject.configproperties + +import io.micronaut.context.ApplicationContext +import io.micronaut.context.DefaultApplicationContext +import io.micronaut.context.env.PropertySource +import spock.lang.Specification + +class PrimitiveConfigurationPropertiesSpec extends Specification { + + // this was just to get the corner case for primitives working + void "test configuration properties binding"() { + given: + ApplicationContext applicationContext = new DefaultApplicationContext("test") + applicationContext.environment.addPropertySource(PropertySource.of( + 'test', + ['foo.bar.port':'8080'] + )) + + applicationContext.start() + + MyPrimitiveConfig config = applicationContext.getBean(MyPrimitiveConfig) + + expect: + config.port == 8080 + config.defaultValue == 9999 + } +} diff --git a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/inject/configproperties/ValidatedConfigurationSpec.groovy b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/inject/configproperties/ValidatedConfigurationSpec.groovy new file mode 100644 index 00000000000..add763e9d71 --- /dev/null +++ b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/inject/configproperties/ValidatedConfigurationSpec.groovy @@ -0,0 +1,77 @@ +package io.micronaut.kotlin.processing.inject.configproperties + +import io.micronaut.context.ApplicationContext +import io.micronaut.context.env.PropertySource +import io.micronaut.context.exceptions.BeanInstantiationException +import io.micronaut.inject.BeanDefinition +import io.micronaut.inject.ValidatedBeanDefinition +import spock.lang.Specification +import static io.micronaut.annotation.processing.test.KotlinCompiler.* + +class ValidatedConfigurationSpec extends Specification { + + void "test validated config with invalid config"() { + given: + ApplicationContext applicationContext = ApplicationContext.run(["spec.name": getClass().simpleName], "test") + + when: + ValidatedConfig config = applicationContext.getBean(ValidatedConfig) + + then: + applicationContext.getBeanDefinition(ValidatedConfig) instanceof ValidatedBeanDefinition + def e = thrown(BeanInstantiationException) + e.message.contains('url - must not be null') + e.message.contains('name - must not be blank') + + + cleanup: + applicationContext.close() + } + + void "test validated config with valid config"() { + given: + ApplicationContext applicationContext = ApplicationContext.builder() + .properties(["spec.name": getClass().simpleName]) + .environments("test") + .build() + applicationContext.environment.addPropertySource(PropertySource.of( + 'foo.bar.url':'http://localhost', + 'foo.bar.name':'test' + )) + + applicationContext.start() + + when: + ValidatedConfig config = applicationContext.getBean(ValidatedConfig) + + then: + config != null + config.url == new URL("http://localhost") + config.name == 'test' + + cleanup: + applicationContext.close() + } + + void "test config props with @Valid on field is a validating bean definition"() { + when: + BeanDefinition beanDefinition = buildBeanDefinition('test.MyConfig', ''' +package test + +import io.micronaut.context.annotation.ConfigurationProperties +import io.micronaut.kotlin.processing.inject.configproperties.Pojo + +import javax.validation.Valid + +@ConfigurationProperties("test.valid") +class MyConfig { + + @Valid + var pojos: List? = null +} +''') + + then: + beanDefinition instanceof ValidatedBeanDefinition + } +} diff --git a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/inject/configproperties/VisibilityIssuesSpec.groovy b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/inject/configproperties/VisibilityIssuesSpec.groovy new file mode 100644 index 00000000000..9292366dc4d --- /dev/null +++ b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/inject/configproperties/VisibilityIssuesSpec.groovy @@ -0,0 +1,74 @@ +package io.micronaut.kotlin.processing.inject.configproperties + +import io.micronaut.context.ApplicationContext +import io.micronaut.inject.BeanDefinition +import io.micronaut.inject.InstantiatableBeanDefinition +import spock.lang.Specification +import static io.micronaut.annotation.processing.test.KotlinCompiler.* + +class VisibilityIssuesSpec extends Specification { + + void "test extending a class with protected method in a different package fails compilation"() { + given: + BeanDefinition beanDefinition = buildBeanDefinition("io.micronaut.inject.configproperties.ChildConfigProperties", """ +package io.micronaut.inject.configproperties + +import io.micronaut.context.annotation.ConfigurationProperties; +import io.micronaut.kotlin.processing.inject.configproperties.other.ParentConfigProperties; + +@ConfigurationProperties("child") +class ChildConfigProperties: ParentConfigProperties() { + var age: Int? = null +} +""") + + when: + def context = ApplicationContext.run( + 'parent.child.age': 22, + 'parent.name': 'Sally', + 'parent.engine.manufacturer': 'Chevy') + def instance = ((InstantiatableBeanDefinition)beanDefinition).instantiate(context) + + then: + beanDefinition.injectedMethods.size() == 3 + beanDefinition.injectedMethods.find {it.name == "setAge" } + beanDefinition.injectedMethods.find {it.name == "setName" } + beanDefinition.injectedMethods.find {it.name == "setNationality" } + instance.getName() == null //methods that require reflection are not injected + instance.getAge() == 22 + instance.getBuilder().build().getManufacturer() == 'Chevy' + + cleanup: + context.close() + } + + void "test extending a class with protected field in a different package fails compilation"() { + given: + BeanDefinition beanDefinition = buildBeanDefinition("io.micronaut.inject.configproperties.ChildConfigProperties", """ +package io.micronaut.inject.configproperties + +import io.micronaut.context.annotation.ConfigurationProperties +import io.micronaut.kotlin.processing.inject.configproperties.other.ParentConfigProperties + +@ConfigurationProperties("child") +open class ChildConfigProperties: ParentConfigProperties() { + override var name: String? = null +} +""") + + when: + def context = ApplicationContext.run('parent.nationality': 'Italian', 'parent.child.name': 'Sally') + def instance = ((InstantiatableBeanDefinition)beanDefinition).instantiate(context) + + then: + beanDefinition.injectedMethods.size() == 2 + beanDefinition.injectedMethods.find {it.name == "setName" } + beanDefinition.injectedMethods.find {it.name == "setNationality" } + instance.getName() == "Sally" + instance.getNationality() == null //methods that require reflection are not injected + + cleanup: + context.close() + } + +} diff --git a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/inject/configproperties/itfce/ValidatedInterfaceConfigPropsSpec.groovy b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/inject/configproperties/itfce/ValidatedInterfaceConfigPropsSpec.groovy new file mode 100644 index 00000000000..2e42a070d75 --- /dev/null +++ b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/inject/configproperties/itfce/ValidatedInterfaceConfigPropsSpec.groovy @@ -0,0 +1,43 @@ +package io.micronaut.kotlin.processing.inject.configproperties.itfce + +import io.micronaut.context.ApplicationContext +import io.micronaut.context.exceptions.BeanInstantiationException +import io.micronaut.inject.qualifiers.Qualifiers +import spock.lang.Specification + +class ValidatedInterfaceConfigPropsSpec extends Specification { + + void 'test validated interface config with invalid config'() { + given: + ApplicationContext context = ApplicationContext.run( + 'my.config.name':'', + 'my.config.foo.name':'', + 'my.config.default.name':'', + 'my.config.foo.nested.bar.name':'', + ) + + when: + context.getBean(MyConfig) + + then: + def e = thrown(BeanInstantiationException) + e.message.contains('MyConfig.getName - must not be blank') + + when: + context.getBean(MyEachConfig, Qualifiers.byName("foo")) + + then: + e = thrown(BeanInstantiationException) + e.message.contains('MyEachConfig.getName - must not be blank') + + when: + context.getBean(MyEachConfig) + + then: + e = thrown(BeanInstantiationException) + e.message.contains('MyEachConfig.getName - must not be blank') + + cleanup: + context.close() + } +} diff --git a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/inject/generics/GenericTypeArgumentsSpec.groovy b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/inject/generics/GenericTypeArgumentsSpec.groovy new file mode 100644 index 00000000000..d341757b9d9 --- /dev/null +++ b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/inject/generics/GenericTypeArgumentsSpec.groovy @@ -0,0 +1,297 @@ +package io.micronaut.kotlin.processing.inject.generics + +import io.micronaut.annotation.processing.test.AbstractKotlinCompilerSpec +import io.micronaut.context.BeanContext +import io.micronaut.context.event.BeanCreatedEventListener +import io.micronaut.inject.BeanDefinition +import spock.lang.Unroll + +import javax.validation.ConstraintViolationException +import java.util.function.Function +import java.util.function.Supplier + +class GenericTypeArgumentsSpec extends AbstractKotlinCompilerSpec { + void "test generic type arguments with inner classes resolve"() { + given: + def definition = buildBeanDefinition('innergenerics.Outer$FooImpl', ''' +package innergenerics + +class Outer { + + interface Foo + + @jakarta.inject.Singleton + class FooImpl : Foo +} +''') + def itfe = definition.beanType.classLoader.loadClass('innergenerics.Outer$Foo') + + expect: + definition.getTypeParameters(itfe).length == 1 + } + + void "test type arguments with inherited fields"() { + given: + BeanContext context = buildContext('inheritedfields.UserDaoClient', ''' +package inheritedfields + +import jakarta.inject.* + +@Singleton +class UserDaoClient : DaoClient() + +@Singleton +class UserDao : Dao() +class User + +open class DaoClient { + + @Inject + lateinit var dao : Dao +} + +open class Dao + +@Singleton +class FooDao : Dao() +class Foo +''') + def definition = getBeanDefinition(context, 'inheritedfields.UserDaoClient') + + expect: + definition.injectedMethods.first().arguments[0].typeParameters.length == 1 + definition.injectedMethods.first().arguments[0].typeParameters[0].type.simpleName == "User" + getBean(context, 'inheritedfields.UserDaoClient').dao.getClass().simpleName == 'UserDao' + } + + void "test type arguments for exception handler"() { + given: + BeanDefinition definition = buildBeanDefinition('exceptionhandler.Test', '''\ +package exceptionhandler + +import io.micronaut.inject.annotation.* +import io.micronaut.context.annotation.* +import javax.validation.ConstraintViolationException + +@Context +class Test : ExceptionHandler?> { + override fun handle(request : String, e: ConstraintViolationException) : java.util.function.Supplier? { + return null + } +} + +class Foo +interface ExceptionHandler { + fun handle(request : String, exception : T) : R +} +''') + expect: + definition != null + def typeArgs = definition.getTypeArguments("exceptionhandler.ExceptionHandler") + typeArgs.size() == 2 + typeArgs[0].type == ConstraintViolationException + typeArgs[1].type == Supplier + } + + void "test type arguments for factory returning interface"() { + given: + BeanDefinition definition = buildBeanDefinition('factorygenerics.Test$MyFunc0', '''\ +package factorygenerics + +import io.micronaut.inject.annotation.* +import io.micronaut.context.annotation.* +import io.micronaut.context.event.* + +@Factory +class Test { + @Bean + fun myFunc() : BeanCreatedEventListener { + return BeanCreatedEventListener { event -> event.getBean() } + } +} + +interface Foo + +''') + expect: + definition != null + definition.getTypeArguments(BeanCreatedEventListener).size() == 1 + definition.getTypeArguments(BeanCreatedEventListener)[0].type.name == 'factorygenerics.Foo' + } + + @Unroll + void "test generic return type resolution for return type: #returnType"() { + given: + BeanDefinition definition = buildBeanDefinition('test.Test', """\ +package test + +import io.micronaut.inject.annotation.* +import io.micronaut.context.annotation.* +import java.util.* + +@jakarta.inject.Singleton +class Test { + + @Executable + fun test() : $returnType? { + return null + } +} +""") + def method = definition.getRequiredMethod("test") + + expect: + method.getDescription(true).startsWith("$returnType" ) + + where: + returnType << + ['List>', + 'List>', + 'List', + 'Map'] + } + + void "test type arguments for interface"() { + given: + BeanDefinition definition = buildBeanDefinition('test.Test', '''\ +package test + +import io.micronaut.inject.annotation.* +import io.micronaut.context.annotation.* + +@jakarta.inject.Singleton +class Test : java.util.function.Function{ + + override fun apply(str : String) : Int { + return 10 + } +} + +class Foo +''') + expect: + definition != null + definition.getTypeArguments(Function).size() == 2 + definition.getTypeArguments(Function)[0].name == 'T' + definition.getTypeArguments(Function)[1].name == 'R' + definition.getTypeArguments(Function)[0].type == String + definition.getTypeArguments(Function)[1].type == Integer + } + + void "test type arguments for inherited interface"() { + given: + BeanDefinition definition = buildBeanDefinition('test.Test', '''\ +package test + +import io.micronaut.inject.annotation.* +import io.micronaut.context.annotation.* + +@jakarta.inject.Singleton +class Test : Foo { + + override fun apply(str : String) : Int { + return 10 + } +} + +interface Foo : java.util.function.Function +''') + expect: + definition != null + definition.getTypeArguments(Function).size() == 2 + definition.getTypeArguments(Function)[0].name == 'T' + definition.getTypeArguments(Function)[1].name == 'R' + definition.getTypeArguments(Function)[0].type == String + definition.getTypeArguments(Function)[1].type == Integer + } + + + void "test type arguments for inherited interface 2"() { + given: + BeanDefinition definition = buildBeanDefinition('test.Test', '''\ +package test + +import io.micronaut.inject.annotation.* +import io.micronaut.context.annotation.* + +@jakarta.inject.Singleton +class Test : Bar { + + override fun apply(str : String) : Int { + return 10 + } +} + +interface Bar : Foo +interface Foo : java.util.function.Function +''') + expect: + definition != null + definition.getTypeArguments(Function).size() == 2 + definition.getTypeArguments(Function)[0].name == 'T' + definition.getTypeArguments(Function)[1].name == 'R' + definition.getTypeArguments(Function)[0].type == String + definition.getTypeArguments(Function)[1].type == Integer + } + + void "test type arguments for inherited interface - using same name as another type parameter"() { + given: + BeanDefinition definition = buildBeanDefinition('test.Test', '''\ +package test + +import io.micronaut.inject.annotation.* +import io.micronaut.context.annotation.* + +@jakarta.inject.Singleton +class Test : Bar { + + override fun apply(str : String) : Int { + return 10 + } +} + +interface Bar : Foo +interface Foo : java.util.function.Function +''') + expect: + definition != null + definition.getTypeArguments(Function).size() == 2 + definition.getTypeArguments(Function)[0].name == 'T' + definition.getTypeArguments(Function)[1].name == 'R' + definition.getTypeArguments(Function)[0].type == String + definition.getTypeArguments(Function)[1].type == Integer + } + + void "test type arguments for factory with inheritance"() { + given: + BeanDefinition definition = buildBeanDefinition('test.Test$MyFunc0', '''\ +package test + +import io.micronaut.inject.annotation.* +import io.micronaut.context.annotation.* + +@Factory +class Test { + + @Bean + fun myFunc() : Foo { + return object : Foo { + override fun apply(t: String): Int { + return 10 + } + } + } +} + +interface Foo : java.util.function.Function + +''') + expect: + definition != null + definition.getTypeArguments(Function).size() == 2 + definition.getTypeArguments(Function)[0].name == 'T' + definition.getTypeArguments(Function)[1].name == 'R' + definition.getTypeArguments(Function)[0].type == String + definition.getTypeArguments(Function)[1].type == Integer + } +} diff --git a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/visitor/BeanIntrospectionSpec.groovy b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/visitor/BeanIntrospectionSpec.groovy new file mode 100644 index 00000000000..ec0123849fd --- /dev/null +++ b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/visitor/BeanIntrospectionSpec.groovy @@ -0,0 +1,2119 @@ +package io.micronaut.kotlin.processing.visitor + +import com.fasterxml.jackson.annotation.JsonClassDescription +import com.fasterxml.jackson.annotation.JsonProperty +import io.micronaut.annotation.processing.test.AbstractKotlinCompilerSpec +import io.micronaut.context.annotation.Executable +import io.micronaut.core.annotation.Introspected +import io.micronaut.core.beans.BeanIntrospection +import io.micronaut.core.beans.BeanIntrospectionReference +import io.micronaut.core.beans.BeanIntrospector +import io.micronaut.core.beans.BeanMethod +import io.micronaut.core.beans.BeanProperty +import io.micronaut.core.convert.ConversionContext +import io.micronaut.core.convert.TypeConverter +import io.micronaut.core.reflect.InstantiationUtils +import io.micronaut.core.reflect.exception.InstantiationException +import io.micronaut.core.type.Argument +import io.micronaut.inject.ExecutableMethod +import io.micronaut.inject.beans.visitor.IntrospectedTypeElementVisitor +import io.micronaut.inject.beans.visitor.MappedSuperClassIntrospectionMapper +import io.micronaut.kotlin.processing.elementapi.SomeEnum +import io.micronaut.kotlin.processing.elementapi.TestClass +import spock.lang.Specification + +import javax.persistence.Column +import javax.persistence.Entity +import javax.persistence.Id +import javax.persistence.Version +import javax.validation.Constraint +import javax.validation.constraints.Min +import javax.validation.constraints.NotBlank +import javax.validation.constraints.Size +import java.lang.reflect.Field + +class BeanIntrospectionSpec extends AbstractKotlinCompilerSpec { + + void "test basic introspection"() { + when: + def introspection = buildBeanIntrospection("test.Test", """ +package test + +import io.micronaut.core.annotation.Introspected + +@Introspected +class Test +""") + + then: + noExceptionThrown() + introspection != null + introspection.instantiate().class.name == "test.Test" + } + + void "test generics in arrays don't stack overflow"() { + given: + def introspection = buildBeanIntrospection('arraygenerics.Test', ''' +package arraygenerics + +import io.micronaut.core.annotation.Introspected +import io.micronaut.context.annotation.Executable + +@Introspected +class Test { + + lateinit var array: Array + lateinit var starArray: Array<*> + lateinit var stringArray: Array + + @Executable + fun myMethod(): Array = array +} +''') + expect: + introspection.beanProperties.size() == 3 + introspection.getRequiredProperty("array", CharSequence[].class).type == CharSequence[].class + introspection.getRequiredProperty("starArray", Object[].class).type == Object[].class + introspection.getRequiredProperty("stringArray", String[].class).type == String[].class + introspection.beanMethods.first().returnType.type == CharSequence[].class + } + + void 'test favor method access'() { + given: + BeanIntrospection introspection = buildBeanIntrospection('fieldaccess.Test','''\ +package fieldaccess + +import io.micronaut.core.annotation.* + +@Introspected(accessKind=[Introspected.AccessKind.METHOD, Introspected.AccessKind.FIELD]) +class Test { + var one: String? = null + private set + get() { + invoked = true + return field + } + var invoked = false +} +''') + + when: + def properties = introspection.getBeanProperties() + def instance = introspection.instantiate() + + then: + properties.size() == 2 + + when: + def one = introspection.getRequiredProperty("one", String) + instance.one = 'test' + + + then: + one.get(instance) == 'test' + instance.invoked + } + + void 'test favor field access'() { + given: + BeanIntrospection introspection = buildBeanIntrospection('fieldaccess.Test','''\ +package fieldaccess; + +import io.micronaut.core.annotation.* + + +@Introspected(accessKind = [Introspected.AccessKind.FIELD, Introspected.AccessKind.METHOD]) +class Test { + var one: String? = null + private set + get() { + invoked = true + return field + } + var invoked = false +} +'''); + when: + def properties = introspection.getBeanProperties() + def instance = introspection.instantiate() + + then: + properties.size() == 2 + + when: + def one = introspection.getRequiredProperty("one", String) + instance.one = 'test' + + then: + one.get(instance) == 'test' + instance.invoked // fields are always private in kotlin so the method will always be referenced + } + + void 'test field access only'() { + given: + BeanIntrospection introspection = buildBeanIntrospection('fieldaccess.Test','''\ +package fieldaccess + +import io.micronaut.core.annotation.* + +@Introspected(accessKind=[Introspected.AccessKind.FIELD]) +open class Test(val two: Integer?) { // read-only + var one: String? = null // read/write + internal var three: String? = null // package protected + protected var four: String? = null // not included since protected + private var five: String? = null // not included since private +} +'''); + when: + def properties = introspection.getBeanProperties() + + then: 'all fields are private in Kotlin' + properties.isEmpty() + } + + void 'test bean constructor'() { + given: + BeanIntrospection introspection = buildBeanIntrospection('beanctor.Test','''\ +package beanctor + +import java.net.URL + +@io.micronaut.core.annotation.Introspected +class Test @com.fasterxml.jackson.annotation.JsonCreator constructor(private val another: String) +''') + + + when: + def constructor = introspection.getConstructor() + def newInstance = constructor.instantiate("test") + + then: + newInstance != null + newInstance.another == "test" + !introspection.getAnnotationMetadata().hasDeclaredAnnotation(com.fasterxml.jackson.annotation.JsonCreator) + constructor.getAnnotationMetadata().hasDeclaredAnnotation(com.fasterxml.jackson.annotation.JsonCreator) + !constructor.getAnnotationMetadata().hasDeclaredAnnotation(Introspected) + !constructor.getAnnotationMetadata().hasAnnotation(Introspected) + !constructor.getAnnotationMetadata().hasStereotype(Introspected) + constructor.arguments.length == 1 + constructor.arguments[0].type == String + } + + void "test generate bean method for introspected class"() { + given: + BeanIntrospection introspection = buildBeanIntrospection('test.MethodTest', ''' +package test + +import io.micronaut.core.annotation.Introspected +import io.micronaut.context.annotation.Executable + +@Introspected +class MethodTest : SuperType(), SomeInt { + + fun nonAnnotated() = true + + @Executable + override fun invokeMe(str: String): String { + return str + } + + @Executable + fun invokePrim(i: Int): Int { + return i + } +} + +open class SuperType { + + @Executable + fun superMethod(str: String): String { + return str + } + + @Executable + open fun invokeMe(str: String): String { + return str + } +} + +interface SomeInt { + + @Executable + fun ok() = true + + fun getName() = "ok" +} +''') + when: + def properties = introspection.getBeanProperties() + Collection beanMethods = introspection.getBeanMethods() + + then: + properties.size() == 1 + beanMethods*.name as Set == ['invokeMe', 'invokePrim', 'superMethod', 'ok'] as Set + beanMethods.every({it.annotationMetadata.hasAnnotation(Executable)}) + beanMethods.every { it.declaringBean == introspection} + + when: + + def invokeMe = beanMethods.find { it.name == 'invokeMe' } + def invokePrim = beanMethods.find { it.name == 'invokePrim' } + def itfeMethod = beanMethods.find { it.name == 'ok' } + def bean = introspection.instantiate() + + then: + invokeMe instanceof ExecutableMethod + invokeMe.invoke(bean, "test") == 'test' + invokePrim.invoke(bean, 10) == 10 + itfeMethod.invoke(bean) == true + } + + void "test custom with prefix"() { + given: + BeanIntrospection introspection = buildBeanIntrospection('customwith.CopyMe', '''\ +package customwith + +import java.net.URL +import java.util.Locale + +@io.micronaut.core.annotation.Introspected(withPrefix = "alter") +class CopyMe(val another: String) { + + fun alterAnother(another: String): CopyMe { + return if (another == this.another) { + this + } else { + CopyMe(another.uppercase(Locale.getDefault())) + } + } +} +''') + when: + def another = introspection.getRequiredProperty("another", String) + def newInstance = introspection.instantiate("test") + + then: + newInstance.another == "test" + + when:"An explicit with method is used" + def result = another.withValue(newInstance, "changed") + + then:"It was invoked" + !result.is(newInstance) + result.another == 'CHANGED' + } + + void "test copy constructor via mutate method"() { + given: + BeanIntrospection introspection = buildBeanIntrospection('test.CopyMe','''\ +package test + +import java.net.URL +import java.util.Locale + +@io.micronaut.core.annotation.Introspected +class CopyMe(val name: String, + val another: String) { + + var url: URL? = null + + fun withAnother(a: String): CopyMe { + return if (this.another == a) { + this + } else { + CopyMe(this.name, a.uppercase(Locale.getDefault())) + } + } +} +''') + when: + def copyMe = introspection.instantiate("Test", "Another") + def expectUrl = new URL("http://test.com") + copyMe.url = expectUrl + + then: + copyMe.name == 'Test' + copyMe.another == "Another" + copyMe.url == expectUrl + + + when: + def property = introspection.getRequiredProperty("name", String) + def another = introspection.getRequiredProperty("another", String) + def newInstance = property.withValue(copyMe, "Changed") + + then: + !newInstance.is(copyMe) + newInstance.name == 'Changed' + newInstance.url == expectUrl + newInstance.another == "Another" + + when:"the instance is changed with the same value" + def result = property.withValue(newInstance, "Changed") + + then:"The existing instance is returned" + newInstance.is(result) + + when:"An explicit with method is used" + result = another.withValue(newInstance, "changed") + + then:"It was invoked" + !result.is(newInstance) + result.another == 'CHANGED' + } + + void "test secondary constructor for data classes"() { + given: + BeanIntrospection introspection = buildBeanIntrospection('test.Foo', ''' +package test + +@io.micronaut.core.annotation.Introspected +data class Foo(val x: Int, val y: Int) { + + constructor(x: Int) : this(x, 20) + + constructor() : this(20, 20) +} +''') + when: + def obj = introspection.instantiate(5, 10) + + then: + obj.getX() == 5 + obj.getY() == 10 + } + + void "test secondary constructor with @Creator for data classes"() { + given: + BeanIntrospection introspection = buildBeanIntrospection('test.Foo', ''' +package test + +import io.micronaut.core.annotation.Creator + +@io.micronaut.core.annotation.Introspected +data class Foo(val x: Int, val y: Int) { + + @Creator + constructor(x: Int) : this(x, 20) + + constructor() : this(20, 20) +} +''') + when: + def obj = introspection.instantiate(5) + + then: + obj.getX() == 5 + obj.getY() == 20 + } + + void "test annotations on generic type arguments for data classes"() { + given: + BeanIntrospection introspection = buildBeanIntrospection('test.Foo', ''' +package test + +import io.micronaut.core.annotation.Creator +import javax.validation.constraints.Min + +@io.micronaut.core.annotation.Introspected +data class Foo(val value: List<@Min(10) Long>) +''') + + when: + BeanProperty property = introspection.getRequiredProperty("value", List) + def genericTypeArg = property.asArgument().getTypeParameters()[0] + + then: + property != null + genericTypeArg.type == Long + genericTypeArg.annotationMetadata.hasStereotype(Constraint) + genericTypeArg.annotationMetadata.hasAnnotation(Min) + genericTypeArg.annotationMetadata.intValue(Min).getAsInt() == 10 + } + + void 'test annotations on generic type arguments'() { + given: + BeanIntrospection introspection = buildBeanIntrospection('test.Foo', ''' +package test + +import javax.validation.constraints.Min +import kotlin.annotation.AnnotationTarget.* + +@io.micronaut.core.annotation.Introspected +class Foo { + var value : List<@Min(10) @SomeAnn Long>? = null +} + +@MustBeDocumented +@Retention(AnnotationRetention.RUNTIME) +@Target(FUNCTION, PROPERTY, ANNOTATION_CLASS, CONSTRUCTOR, VALUE_PARAMETER, TYPE) +annotation class SomeAnn +''') + when: + BeanProperty property = introspection.getRequiredProperty("value", List) + def genericTypeArg = property.asArgument().getTypeParameters()[0] + + then: + property != null + genericTypeArg.annotationMetadata.hasAnnotation(Min) + genericTypeArg.annotationMetadata.intValue(Min).getAsInt() == 10 + } + + void "test bean introspection on a data class"() { + given: + BeanIntrospection introspection = buildBeanIntrospection('test.Foo', ''' +package test + +@io.micronaut.core.annotation.Introspected +data class Foo(@javax.validation.constraints.NotBlank val name: String, val age: Int) +''') + when: + def test = introspection.instantiate("test", 20) + def property = introspection.getRequiredProperty("name", String) + def argument = introspection.getConstructorArguments()[0] + + then: + argument.name == 'name' + argument.getAnnotationMetadata().hasStereotype(Constraint) + argument.getAnnotationMetadata().hasAnnotation(NotBlank) + test.name == 'test' + test.getName() == 'test' + introspection.propertyNames.length == 2 + introspection.propertyNames == ['name', 'age'] as String[] + property.hasAnnotation(NotBlank) + property.isReadOnly() + property.hasSetterOrConstructorArgument() + property.name == 'name' + property.get(test) == 'test' + + when:"a mutation is applied" + def newTest = property.withValue(test, "Changed") + + then:"a new instance is returned" + !newTest.is(test) + newTest.getName() == 'Changed' + newTest.getAge() == 20 + } + + void "test create bean introspection for external inner class"() { + given: + ClassLoader classLoader = buildClassLoader('test.Foo', ''' +package test + +import io.micronaut.core.annotation.* +import io.micronaut.kotlin.processing.elementapi.OuterBean + +@Introspected(classes=[OuterBean.InnerBean::class]) +class Test +''') + + when:"the reference is loaded" + def clazz = classLoader.loadClass('test.$Test$IntrospectionRef0') + BeanIntrospectionReference reference = clazz.newInstance() + String className = "io.micronaut.kotlin.processing.elementapi.OuterBean\$InnerBean" + def innerType = classLoader.loadClass(className) + + then:"The reference is valid" + reference != null + reference.getBeanType().name == className + + when: + BeanIntrospection i = reference.load() + + then: + i.propertyNames.length == 1 + i.propertyNames[0] == 'name' + + when: + innerType.newInstance() + + then: + noExceptionThrown() + + when: + def o = i.instantiate() + + then: + noExceptionThrown() + o.class.name == className + innerType.isInstance(o) + } + + void "test create bean introspection for external inner interface"() { + given: + ClassLoader classLoader = buildClassLoader('test.Foo', ''' +package test + +import io.micronaut.core.annotation.* +import io.micronaut.kotlin.processing.elementapi.OuterBean + +@Introspected(classes=[OuterBean.InnerInterface::class]) +class Test +''') + + when:"the reference is loaded" + def clazz = classLoader.loadClass('test.$Test$IntrospectionRef0') + BeanIntrospectionReference reference = clazz.newInstance() + String className = "io.micronaut.kotlin.processing.elementapi.OuterBean\$InnerInterface" + + then:"The reference is valid" + reference != null + reference.getBeanType().name == className + + when: + BeanIntrospection i = reference.load() + + then: + i.propertyNames.length == 1 + i.propertyNames[0] == 'name' + + when: + def o = i.instantiate() + + then: + def e = thrown(InstantiationException) + e.message == 'No default constructor exists' + } + + void "test bean introspection with property of generic interface"() { + given: + BeanIntrospection introspection = buildBeanIntrospection('test.Foo', ''' +package test + +@io.micronaut.core.annotation.Introspected +class Foo : GenBase { + override fun getName() = "test" +} + +interface GenBase { + fun getName(): T +} +''') + when: + def test = introspection.instantiate() + def property = introspection.getRequiredProperty("name", String) + + then: + introspection.beanProperties.first().type == String + property.get(test) == 'test' + !property.hasSetterOrConstructorArgument() + + when: + property.withValue(test, 'try change') + + then: + def e = thrown(UnsupportedOperationException) + e.message =='Cannot mutate property [name] that is not mutable via a setter method, field or constructor argument for type: test.Foo' + } + + void "test bean introspection with property of generic superclass"() { + given: + BeanIntrospection introspection = buildBeanIntrospection('test.Foo', ''' +package test + +@io.micronaut.core.annotation.Introspected +class Foo: GenBase() { + override fun getName() = "test" +} + +abstract class GenBase { + abstract fun getName(): T + + fun getOther(): T { + return "other" as T + } +} +''') + when: + def test = introspection.instantiate() + + def beanProperties = introspection.beanProperties.toList() + then: + beanProperties.size() == 2 + beanProperties[0].type == String + beanProperties[1].type == String + introspection.getRequiredProperty("name", String) + .get(test) == 'test' + introspection.getRequiredProperty("other", String) + .get(test) == 'other' + } + + void "test bean introspection with argument of generic interface"() { + given: + BeanIntrospection introspection = buildBeanIntrospection('test.Foo', ''' +package test + +@io.micronaut.core.annotation.Introspected +class Foo: GenBase { + override var value: Long? = null +} + +interface GenBase { + var value: T +} + +''') + when: + def test = introspection.instantiate() + BeanProperty bp = introspection.getRequiredProperty("value", Long) + bp.set(test, Long.valueOf(5)) + + then: + bp.get(test) == Long.valueOf(5) + + when: + def returnedBean = bp.withValue(test, Long.valueOf(10)) + + then: + returnedBean.is(test) + bp.get(test) == Long.valueOf(10) + } + + void "test bean introspection with property with static creator method on interface"() { + given: + BeanIntrospection introspection = buildBeanIntrospection('test.Foo', ''' +package test + +import io.micronaut.core.annotation.Creator + +@io.micronaut.core.annotation.Introspected +fun interface Foo { + + fun getName(): String + + companion object { + @Creator + fun create(name: String): Foo { + return Foo { name } + } + } +} + +''') + when: + def test = introspection.instantiate("test") + + then: + introspection.constructorArguments.length == 1 + introspection.getRequiredProperty("name", String) + .get(test) == 'test' + } + + void "test bean introspection with property with static creator method on interface with generic type arguments"() { + given: + BeanIntrospection introspection = buildBeanIntrospection('test.Foo', ''' +package test; + +import io.micronaut.core.annotation.Creator; + +@io.micronaut.core.annotation.Introspected +fun interface Foo { + + fun getName(): String + + companion object { + @Creator + fun create(name: String): Foo { + return Foo { name } + } + } +} + +''') + when: + def test = introspection.instantiate("test") + + then: + introspection.constructorArguments.length == 1 + introspection.getRequiredProperty("name", String) + .get(test) == 'test' + } + + void "test bean introspection with property from default interface method"() { + given: + BeanIntrospection introspection = buildBeanIntrospection('test.Test', ''' +package test + +@io.micronaut.core.annotation.Introspected +class Test: Foo + +interface Foo { + fun getBar(): String = "good" +} + +''') + when: + def test = introspection.instantiate() + + then: + introspection.getRequiredProperty("bar", String) + .get(test) == 'good' + } + + void "test generate bean introspection for interface"() { + when: + BeanIntrospection introspection = buildBeanIntrospection('test.Test','''\ +package test + +@io.micronaut.core.annotation.Introspected +interface Test : io.micronaut.core.naming.Named { + fun setName(name: String) +} +''') + then: + introspection != null + introspection.propertyNames.length == 1 + introspection.propertyNames[0] == 'name' + + when: + introspection.instantiate() + + then: + def e = thrown(InstantiationException) + e.message == 'No default constructor exists' + + when: + def property = introspection.getRequiredProperty("name", String) + String setNameValue + def named = [getName:{-> "test"}, setName:{String n -> setNameValue= n }].asType(introspection.beanType) + + property.set(named, "test") + + then: + property.get(named) == 'test' + setNameValue == 'test' + } + + void "test build introspection"() { + given: + def classLoader = buildClassLoader('test.Address', ''' +package test + +import javax.validation.constraints.* + +@io.micronaut.core.annotation.Introspected +class Address { + + @NotBlank(groups = [GroupOne::class]) + @NotBlank(groups = [GroupThree::class], message = "different message") + @Size(min = 5, max = 20, groups = [GroupTwo::class]) + private var street: String? = null +} + +interface GroupOne +interface GroupTwo +interface GroupThree +''') + def clazz = classLoader.loadClass('test.$Address$IntrospectionRef') + BeanIntrospectionReference reference = clazz.newInstance() + + expect: + reference != null + reference.load() + } + + void "test primary constructor is preferred"() { + given: + def classLoader = buildClassLoader('test.Book', ''' +package test + +@io.micronaut.core.annotation.Introspected +class Book(val title: String) { + + private var author: String? = null + + constructor(title: String, author: String) : this(title) { + this.author = author + } +} +''') + Class clazz = classLoader.loadClass('test.$Book$IntrospectionRef') + BeanIntrospectionReference reference = (BeanIntrospectionReference) clazz.newInstance() + + expect: + reference != null + + when: + BeanIntrospection introspection = reference.load() + + then: + introspection != null + introspection.hasAnnotation(Introspected) + introspection.propertyNames.length == 1 + + when: + introspection.instantiate() + + then: + thrown(InstantiationException) + + when: "update introspectionMap" + BeanIntrospector introspector = BeanIntrospector.SHARED + Field introspectionMapField = introspector.getClass().getDeclaredField("introspectionMap") + introspectionMapField.setAccessible(true) + introspectionMapField.set(introspector, new HashMap>()); + Map map = (Map) introspectionMapField.get(introspector) + map.put(reference.getName(), reference) + + and: + def book = InstantiationUtils.tryInstantiate(introspection.getBeanType(), ["title": "The Stand"], ConversionContext.of(Argument.of(introspection.beanType))) + def prop = introspection.getRequiredProperty("title", String) + + then: + prop.get(book.get()) == "The Stand" + + cleanup: + introspectionMapField.set(introspector, null) + } + + void "test multiple constructors with primary constructor marked as @Creator"() { + given: + def classLoader = buildClassLoader('test.Book', ''' +package test + +import io.micronaut.core.annotation.Creator + +@io.micronaut.core.annotation.Introspected +class Book { + + private var author: String? = null + val title: String + + constructor(title: String, author: String) : this(title) { + this.author = author + } + + @Creator + constructor(title: String) { + this.title = title + } +} +''') + Class clazz = classLoader.loadClass('test.$Book$IntrospectionRef') + BeanIntrospectionReference reference = (BeanIntrospectionReference) clazz.newInstance() + + expect: + reference != null + + when: + BeanIntrospection introspection = reference.load() + + then: + introspection != null + introspection.hasAnnotation(Introspected) + introspection.propertyNames.length == 1 + + when: + introspection.instantiate() + + then: + thrown(InstantiationException) + + when: "update introspectionMap" + BeanIntrospector introspector = BeanIntrospector.SHARED + Field introspectionMapField = introspector.getClass().getDeclaredField("introspectionMap") + introspectionMapField.setAccessible(true) + introspectionMapField.set(introspector, new HashMap>()); + Map map = (Map) introspectionMapField.get(introspector) + map.put(reference.getName(), reference) + + and: + def book = InstantiationUtils.tryInstantiate(introspection.getBeanType(), ["title": "The Stand"], ConversionContext.of(Argument.of(introspection.beanType))) + def prop = introspection.getRequiredProperty("title", String) + + then: + prop.get(book.get()) == "The Stand" + + cleanup: + introspectionMapField.set(introspector, null) + } + + void "test default constructor "() { + given: + def classLoader = buildClassLoader('test.Book', ''' +package test + +@io.micronaut.core.annotation.Introspected +class Book { + var title: String? = null +} +''') + Class clazz = classLoader.loadClass('test.$Book$IntrospectionRef') + BeanIntrospectionReference reference = (BeanIntrospectionReference) clazz.newInstance() + + expect: + reference != null + + when: + BeanIntrospection introspection = reference.load() + + then: + introspection != null + introspection.hasAnnotation(Introspected) + introspection.propertyNames.length == 1 + + when: "update introspectionMap" + BeanIntrospector introspector = BeanIntrospector.SHARED + Field introspectionMapField = introspector.getClass().getDeclaredField("introspectionMap") + introspectionMapField.setAccessible(true) + introspectionMapField.set(introspector, new HashMap>()); + Map map = (Map) introspectionMapField.get(introspector) + map.put(reference.getName(), reference) + + and: + def book = InstantiationUtils.tryInstantiate(introspection.getBeanType(), ["title": "The Stand"], ConversionContext.of(Argument.of(introspection.beanType))) + def prop = introspection.getRequiredProperty("title", String) + + then: + prop.get(book.get()) == null + + cleanup: + introspectionMapField.set(introspector, null) + } + + void "test multiple constructors with @JsonCreator"() { + given: + def classLoader = buildClassLoader('test.Test', ''' +package test + +import io.micronaut.core.annotation.* +import javax.validation.constraints.* +import java.util.* +import com.fasterxml.jackson.annotation.* + +@Introspected +class Test { + private var name: String? = null + var age: Int = 0 + + @JsonCreator + constructor(@JsonProperty("name") name: String) { + this.name = name + } + + constructor(age: Int) { + this.age = age + } + + fun getName(): String? = name + + fun setName(n: String): Test { + this.name = n + return this + } +} + +''') + + when:"the reference is loaded" + def clazz = classLoader.loadClass('test.$Test$IntrospectionRef') + BeanIntrospectionReference reference = clazz.newInstance() + + then:"The reference is valid" + reference != null + reference.getAnnotationMetadata().hasAnnotation(Introspected) + reference.isPresent() + reference.beanType.name == 'test.Test' + + when:"the introspection is loaded" + BeanIntrospection introspection = reference.load() + + then:"The introspection is valid" + introspection != null + introspection.hasAnnotation(Introspected) + introspection.propertyNames.length == 2 + + when: + def test = introspection.instantiate("Fred") + def prop = introspection.getRequiredProperty("name", String) + + then: + prop.get(test) == 'Fred' + } + + void "test write bean introspection with builder style properties"() { + given: + def classLoader = buildClassLoader('test.Test', ''' +package test + +import io.micronaut.core.annotation.* +import javax.validation.constraints.* +import java.util.* + +@Introspected +class Test { + private var name: String? = null + + fun getName(): String? = name + fun setName(n: String): Test { + this.name = n + return this + } +} + +''') + + when:"the reference is loaded" + def clazz = classLoader.loadClass('test.$Test$IntrospectionRef') + BeanIntrospectionReference reference = clazz.newInstance() + + then:"The reference is valid" + reference != null + reference.getAnnotationMetadata().hasAnnotation(Introspected) + reference.isPresent() + reference.beanType.name == 'test.Test' + + when:"the introspection is loaded" + BeanIntrospection introspection = reference.load() + + then:"The introspection is valid" + introspection != null + introspection.hasAnnotation(Introspected) + introspection.propertyNames.length == 1 + + when: + def test = introspection.instantiate() + def prop = introspection.getRequiredProperty("name", String) + prop.set(test, "Foo") + + then: + prop.get(test) == 'Foo' + } + + void "test write bean introspection with inner classes"() { + given: + def classLoader = buildClassLoader('test.Test', ''' +package test + +import io.micronaut.core.annotation.* +import javax.validation.constraints.* +import java.util.* + +@Introspected +class Test { + + private var status: Status? = null + + fun setStatus(status: Status) { + this.status = status + } + + fun getStatus(): Status? { + return this.status + } + + enum class Status { + UP, DOWN + } +} + +''') + + when:"the reference is loaded" + def clazz = classLoader.loadClass('test.$Test$IntrospectionRef') + BeanIntrospectionReference reference = clazz.newInstance() + + then:"The reference is valid" + reference != null + reference.getAnnotationMetadata().hasAnnotation(Introspected) + reference.isPresent() + reference.beanType.name == 'test.Test' + + when:"the introspection is loaded" + BeanIntrospection introspection = reference.load() + + then:"The introspection is valid" + introspection != null + introspection.hasAnnotation(Introspected) + introspection.propertyNames.length == 1 + } + + void "test bean introspection with constructor"() { + given: + def classLoader = buildClassLoader('test.Test', ''' +package test + +import javax.validation.constraints.* +import javax.persistence.* + +@Entity +class Test( + @Column(name="test_name") var name: String, + @Size(max=100) var age: Int, + primitiveArray: Array) { + + @Id + @GeneratedValue + var id: Long? = null + + @Version + var version: Long? = null + + private var primitiveArray: Array? = null + + private var v: Long? = null + + @Version + fun getAnotherVersion(): Long? { + return v; + } + + fun setAnotherVersion(v: Long) { + this.v = v + } +} +''') + + when:"the reference is loaded" + def clazz = classLoader.loadClass('test.$Test$IntrospectionRef') + BeanIntrospectionReference reference = clazz.newInstance() + + then:"The reference is valid" + reference != null + + when:"The introspection is loaded" + BeanIntrospection bi = reference.load() + + then:"it is correct" + bi.getConstructorArguments().length == 3 + bi.getConstructorArguments()[0].name == 'name' + bi.getConstructorArguments()[0].type == String + bi.getConstructorArguments()[1].name == 'age' + bi.getConstructorArguments()[1].getAnnotationMetadata().hasAnnotation(Size) + bi.getIndexedProperties(Id).size() == 1 + bi.getIndexedProperty(Id).isPresent() + !bi.getIndexedProperty(Column, null).isPresent() + bi.getIndexedProperty(Column, "test_name").isPresent() + bi.getIndexedProperty(Column, "test_name").get().name == 'name' + bi.getProperty("version").get().hasAnnotation(Version) + bi.getProperty("anotherVersion").get().hasAnnotation(Version) + // should not inherit metadata from class + !bi.getProperty("anotherVersion").get().hasAnnotation(Entity) + + when: + BeanProperty idProp = bi.getIndexedProperties(Id).first() + + then: + idProp.name == 'id' + !idProp.hasAnnotation(Entity) + !idProp.hasStereotype(Entity) + + + when: + def object = bi.instantiate("test", 10, [20] as Integer[]) + + then: + object.name == 'test' + object.age == 10 + } + + void "test write bean introspection data for entity"() { + given: + def classLoader = buildClassLoader('test.Test', ''' +package test + +import javax.validation.constraints.* +import javax.persistence.* + +@Entity +class Test { + + @Id + @GeneratedValue + var id: Long? = null + + @Version + var version: Long? = null + + var name: String? = null + + @Size(max=100) + var age: Int? = null +} +''') + + when:"the reference is loaded" + def clazz = classLoader.loadClass('test.$Test$IntrospectionRef') + BeanIntrospectionReference reference = clazz.newInstance() + + then:"The reference is valid" + reference != null + + when:"The introspection is loaded" + BeanIntrospection bi = reference.load() + + then:"it is correct" + bi.instantiate() + bi.getIndexedProperties(Id).size() == 1 + bi.getIndexedProperties(Id).first().name == 'id' + } + + void "test write bean introspection data for class in another package"() { + given: + def classLoader = buildClassLoader('test.Test', ''' +package test + +import io.micronaut.core.annotation.Introspected +import io.micronaut.kotlin.processing.elementapi.OtherTestBean + +@Introspected(classes=[OtherTestBean::class]) +class Test +''') + + when:"the reference is loaded" + def clazz = classLoader.loadClass('test.$Test$IntrospectionRef0') + BeanIntrospectionReference reference = clazz.newInstance() + + then:"The reference is valid" + reference != null + reference.getBeanType().name == "io.micronaut.kotlin.processing.elementapi.OtherTestBean" + + when: + def introspection = reference.load() + + then: "the introspection is under the reference package" + noExceptionThrown() + introspection.class.name == "test.\$io_micronaut_kotlin_processing_elementapi_OtherTestBean\$Introspection" + introspection.instantiate() + } + + void "test write bean introspection data for class already introspected"() { + given: + def classLoader = buildClassLoader('test.Test', ''' +package test + +import io.micronaut.core.annotation.Introspected +import io.micronaut.kotlin.processing.elementapi.TestBean + +@Introspected(classes=[TestBean::class]) +class Test +''') + + when:"the reference is loaded" + classLoader.loadClass('test.$Test$IntrospectionRef0') + + then:"The reference is not written" + thrown(ClassNotFoundException) + } + + void "test write bean introspection data for package with sources"() { + given: + def classLoader = buildClassLoader('test.Test', ''' +package test + +import io.micronaut.core.annotation.* +import io.micronaut.kotlin.processing.elementapi.MarkerAnnotation + +@Introspected(packages = ["io.micronaut.kotlin.processing.elementapi"], includedAnnotations = [MarkerAnnotation::class]) +class Test +''') + + when:"the reference is loaded" + def clazz = classLoader.loadClass('test.$Test$IntrospectionRef0') + BeanIntrospectionReference reference = clazz.newInstance() + + then:"The reference is generated" + reference != null + } + + void "test write bean introspection data for package with compiled classes"() { + given: + def classLoader = buildClassLoader('test.Test', ''' +package test + +import io.micronaut.core.annotation.* + +@Introspected(packages=["io.micronaut.inject.beans.visitor"], includedAnnotations=[Internal::class]) +class Test +''') + + when:"the reference is loaded" + def clazz = classLoader.loadClass('test.$Test$IntrospectionRef0') + BeanIntrospectionReference reference = clazz.newInstance() + + then:"The reference is valid" + reference != null + reference.getBeanType() == MappedSuperClassIntrospectionMapper + } + + void "test write bean introspection data"() { + given: + def classLoader = buildClassLoader('test.Test', ''' +package test + +import io.micronaut.core.annotation.Introspected +import io.micronaut.core.convert.TypeConverter +import javax.validation.constraints.Size + +@Introspected +class Test: ParentBean() { + val readOnly: String = "test" + var name: String? = null + + @Size(max=100) + var age: Int = 0 + + var list: List? = null + var stringArray: Array? = null + var primitiveArray: Array? = null + var flag: Boolean = false + val genericsTest: TypeConverter>? = null + val genericsArrayTest: TypeConverter>? = null +} + +open class ParentBean { + var listOfBytes: List? = null +} +''') + + when:"the reference is loaded" + def clazz = classLoader.loadClass('test.$Test$IntrospectionRef') + BeanIntrospectionReference reference = clazz.newInstance() + + then:"The reference is valid" + reference != null + reference.getAnnotationMetadata().hasAnnotation(Introspected) + reference.isPresent() + reference.beanType.name == 'test.Test' + + when:"the introspection is loaded" + BeanIntrospection introspection = reference.load() + + then:"The introspection is valid" + introspection != null + introspection.hasAnnotation(Introspected) + introspection.instantiate().getClass().name == 'test.Test' + introspection.getBeanProperties().size() == 10 + introspection.getProperty("name").isPresent() + introspection.getProperty("name", String).isPresent() + !introspection.getProperty("name", Integer).isPresent() + + when: + BeanProperty nameProp = introspection.getProperty("name", String).get() + BeanProperty boolProp = introspection.getProperty("flag", boolean.class).get() + BeanProperty ageProp = introspection.getProperty("age", int.class).get() + BeanProperty listProp = introspection.getProperty("list").get() + BeanProperty primitiveArrayProp = introspection.getProperty("primitiveArray").get() + BeanProperty stringArrayProp = introspection.getProperty("stringArray").get() + BeanProperty listOfBytes = introspection.getProperty("listOfBytes").get() + BeanProperty genericsTest = introspection.getProperty("genericsTest").get() + BeanProperty genericsArrayTest = introspection.getProperty("genericsArrayTest").get() + def readOnlyProp = introspection.getProperty("readOnly", String).get() + def instance = introspection.instantiate() + + then: + readOnlyProp.isReadOnly() + nameProp != null + !nameProp.isReadOnly() + !nameProp.isWriteOnly() + nameProp.isReadWrite() + boolProp.get(instance) == false + nameProp.get(instance) == null + ageProp.get(instance) == 0 + genericsTest != null + genericsTest.type == TypeConverter + genericsTest.asArgument().typeParameters.size() == 2 + genericsTest.asArgument().typeParameters[0].type == String + genericsTest.asArgument().typeParameters[1].type == Collection + genericsTest.asArgument().typeParameters[1].typeParameters.length == 1 + genericsArrayTest.type == TypeConverter + genericsArrayTest.asArgument().typeParameters.size() == 2 + genericsArrayTest.asArgument().typeParameters[0].type == String + genericsArrayTest.asArgument().typeParameters[1].type == Object[].class + stringArrayProp.get(instance) == null + stringArrayProp.type == String[] + primitiveArrayProp.get(instance) == null + ageProp.hasAnnotation(Size) + listOfBytes.asArgument().getFirstTypeVariable().get().type == byte[].class + listProp.asArgument().getFirstTypeVariable().isPresent() + listProp.asArgument().getFirstTypeVariable().get().type == Number + + when: + boolProp.set(instance, true) + nameProp.set(instance, "foo") + ageProp.set(instance, 10) + primitiveArrayProp.set(instance, [10] as Integer[]) + stringArrayProp.set(instance, ['foo'] as String[]) + + + then: + boolProp.get(instance) == true + nameProp.get(instance) == 'foo' + ageProp.get(instance) == 10 + stringArrayProp.get(instance) == ['foo'] as String[] + primitiveArrayProp.get(instance) == [10] as Integer[] + + when: + ageProp.convertAndSet(instance, "20") + nameProp.set(instance, "100" ) + + then: + ageProp.get(instance) == 20 + nameProp.get(instance, Integer, null) == 100 + + when: + introspection.instantiate("blah") // illegal argument + + then: + def e = thrown(InstantiationException) + e.message == 'Argument count [1] doesn\'t match required argument count: 0' + } + + void "test constructor argument generics"() { + given: + BeanIntrospection introspection = buildBeanIntrospection('test.Test', ''' +package test + +import io.micronaut.core.annotation.* + +@Introspected +class Test(var properties: Map) +''') + expect: + introspection.constructorArguments[0].getTypeVariable("K").get().getType() == String + introspection.constructorArguments[0].getTypeVariable("V").get().getType() == String + } + + void "test static creator"() { + BeanIntrospection introspection = buildBeanIntrospection('test.Test', ''' +package test + +import io.micronaut.core.annotation.* + +@Introspected +class Test private constructor(val name: String) { + + companion object { + @Creator + fun forName(name: String): Test { + return Test(name) + } + } +} +''') + + expect: + introspection != null + + when: + def instance = introspection.instantiate("Sally") + + then: + introspection.getRequiredProperty("name", String).get(instance) == "Sally" + + when: + introspection.instantiate(new Object[0]) + + then: + thrown(InstantiationException) + + when: + introspection.instantiate() + + then: + thrown(InstantiationException) + } + + void "test static creator with no args"() { + BeanIntrospection introspection = buildBeanIntrospection('test.Test', ''' +package test + +import io.micronaut.core.annotation.* + +@Introspected +class Test private constructor(val name: String) { + + companion object { + @Creator + fun forName(): Test { + return Test("default") + } + } +} +''') + expect: + introspection != null + + when: + def instance = introspection.instantiate("Sally") + + then: + thrown(InstantiationException) + + when: + instance = introspection.instantiate(new Object[0]) + + then: + introspection.getRequiredProperty("name", String).get(instance) == "default" + + when: + instance = introspection.instantiate() + + then: + introspection.getRequiredProperty("name", String).get(instance) == "default" + } + + void "test static creator multiple"() { + BeanIntrospection introspection = buildBeanIntrospection('test.Test', ''' +package test + +import io.micronaut.core.annotation.* + +@Introspected +class Test private constructor(val name: String) { + + companion object { + @Creator + fun forName(): Test { + return Test("default") + } + + @Creator + fun forName(name: String): Test { + return Test(name) + } + } +} +''') + + expect: + introspection != null + + when: + def instance = introspection.instantiate("Sally") + + then: + introspection.getRequiredProperty("name", String).get(instance) == "Sally" + + when: + instance = introspection.instantiate(new Object[0]) + + then: + introspection.getRequiredProperty("name", String).get(instance) == "default" + + when: + instance = introspection.instantiate() + + then: + introspection.getRequiredProperty("name", String).get(instance) == "default" + } + + void "test introspections are not created for super classes"() { + BeanIntrospection introspection = buildBeanIntrospection('test.Test', ''' +package test + +import io.micronaut.core.annotation.* + +@Introspected +class Test: Foo() + +open class Foo +''') + + expect: + introspection != null + + when: + introspection.getClass().getClassLoader().loadClass("test.\$Foo\$Introspection") + + then: + thrown(ClassNotFoundException) + } + + void "test enum bean properties"() { + BeanIntrospection introspection = buildBeanIntrospection('test.Test', ''' +package test + +import io.micronaut.core.annotation.* + +@Introspected +enum class Test(val number: Int) { + A(0), B(1), C(2); +} +''') + + expect: + introspection != null + introspection.beanProperties.size() == 1 + introspection.getProperty("number").isPresent() + + when: + def instance = introspection.instantiate("A") + + then: + instance.name() == "A" + introspection.getRequiredProperty("number", int).get(instance) == 0 + + when: + introspection.instantiate() + + then: + thrown(InstantiationException) + + when: + introspection.getClass().getClassLoader().loadClass("java.lang.\$Enum\$Introspection") + + then: + thrown(ClassNotFoundException) + } + + void "test instantiating an enum"() { + BeanIntrospection introspection = buildBeanIntrospection('test.Test', ''' +package test + +import io.micronaut.core.annotation.Introspected + +@Introspected +enum class Test { + A, B, C; +} +''') + + expect: + introspection != null + + when: + def instance = introspection.instantiate("A") + + then: + instance.name() == "A" + + when: + introspection.instantiate() + + then: + thrown(InstantiationException) + } + + void "test constructor argument nested generics"() { + BeanIntrospection introspection = buildBeanIntrospection('test.Test', ''' +package test + +import io.micronaut.core.annotation.Introspected +import java.util.List +import java.util.Map + +@Introspected +class Test(map: Map>) + +class Action +''') + + expect: + introspection != null + introspection.constructorArguments[0].typeParameters.size() == 2 + introspection.constructorArguments[0].typeParameters[0].typeName == 'java.lang.String' + introspection.constructorArguments[0].typeParameters[1].typeName == 'java.util.List' + introspection.constructorArguments[0].typeParameters[1].typeParameters.size() == 1 + introspection.constructorArguments[0].typeParameters[1].typeParameters[0].typeName == 'test.Action' + } + + void "test primitive multi-dimensional arrays"() { + when: + BeanIntrospection introspection = buildBeanIntrospection('test.Test', ''' +package test + +import io.micronaut.core.annotation.Introspected + +@Introspected +class Test { + var oneDimension: IntArray? = null + var twoDimensions: Array? = null + var threeDimensions: Array>? = null +} +''') + + then: + noExceptionThrown() + introspection != null + + when: + def instance = introspection.instantiate() + def property = introspection.getRequiredProperty("oneDimension", int[].class) + int[] level1 = [1, 2, 3] as int[] + property.set(instance, level1) + + then: + property.get(instance) == level1 + + when: + property = introspection.getRequiredProperty("twoDimensions", int[][].class) + int[] level2 = [4, 5, 6] as int[] + int[][] twoDimensions = [level1, level2] as int[][] + property.set(instance, twoDimensions) + + then: + property.get(instance) == twoDimensions + + when: + property = introspection.getRequiredProperty("threeDimensions", int[][][].class) + int[][][] threeDimensions = [[level1], [level2]] as int[][][] + property.set(instance, threeDimensions) + + then: + property.get(instance) == threeDimensions + } + + void "test class multi-dimensional arrays"() { + when: + BeanIntrospection introspection = buildBeanIntrospection('test.Test', ''' +package test + +import io.micronaut.core.annotation.Introspected + +@Introspected +class Test { + var oneDimension: Array? = null + var twoDimensions: Array>? = null + var threeDimensions: Array>>? = null +} +''') + + then: + noExceptionThrown() + introspection != null + + when: + def instance = introspection.instantiate() + def property = introspection.getRequiredProperty("oneDimension", String[].class) + String[] level1 = ["1", "2", "3"] as String[] + property.set(instance, level1) + + then: + property.get(instance) == level1 + + when: + property = introspection.getRequiredProperty("twoDimensions", String[][].class) + String[] level2 = ["4", "5", "6"] as String[] + String[][] twoDimensions = [level1, level2] as String[][] + property.set(instance, twoDimensions) + + then: + property.get(instance) == twoDimensions + + when: + property = introspection.getRequiredProperty("threeDimensions", String[][][].class) + String[][][] threeDimensions = [[level1], [level2]] as String[][][] + property.set(instance, threeDimensions) + + then: + property.get(instance) == threeDimensions + } + + void "test enum multi-dimensional arrays"() { + when: + BeanIntrospection introspection = buildBeanIntrospection('test.Test', ''' +package test + +import io.micronaut.core.annotation.Introspected +import io.micronaut.kotlin.processing.elementapi.SomeEnum + +@Introspected +class Test { + var oneDimension: Array? = null + var twoDimensions: Array>? = null + var threeDimensions: Array>>? = null +} +''') + + then: + noExceptionThrown() + introspection != null + + when: + def instance = introspection.instantiate() + def property = introspection.getRequiredProperty("oneDimension", SomeEnum[].class) + SomeEnum[] level1 = [SomeEnum.A, SomeEnum.B, SomeEnum.A] as SomeEnum[] + property.set(instance, level1) + + then: + property.get(instance) == level1 + + when: + property = introspection.getRequiredProperty("twoDimensions", SomeEnum[][].class) + SomeEnum[] level2 = [SomeEnum.B, SomeEnum.A, SomeEnum.B] as SomeEnum[] + SomeEnum[][] twoDimensions = [level1, level2] as SomeEnum[][] + property.set(instance, twoDimensions) + + then: + property.get(instance) == twoDimensions + + when: + property = introspection.getRequiredProperty("threeDimensions", SomeEnum[][][].class) + SomeEnum[][][] threeDimensions = [[level1], [level2]] as SomeEnum[][][] + property.set(instance, threeDimensions) + + then: + property.get(instance) == threeDimensions + } + + void "test superclass methods are read before interface methods"() { + BeanIntrospection introspection = buildBeanIntrospection('test.Test', ''' +package test + +import io.micronaut.core.annotation.Introspected +import javax.validation.constraints.NotNull + +interface IEmail { + fun getEmail(): String? +} + +@Introspected +open class SuperClass: IEmail { + @NotNull + override fun getEmail(): String? = null +} + +@Introspected +class SubClass: SuperClass() + +@Introspected +class Test: SuperClass(), IEmail + +''') + expect: + introspection != null + introspection.getProperty("email").isPresent() + introspection.getIndexedProperties(Constraint).size() == 1 + } + + void "test introspection on abstract class"() { + BeanIntrospection beanIntrospection = buildBeanIntrospection("test.Test", """ +package test + +import io.micronaut.core.annotation.Introspected + +@Introspected +abstract class Test { + var name: String? = null + var author: String? = null +} +""") + + expect: + beanIntrospection != null + beanIntrospection.getBeanProperties().size() == 2 + } + + void "test targeting abstract class with @Introspected(classes = "() { + ClassLoader classLoader = buildClassLoader("test.Test", """ +package test + +import io.micronaut.core.annotation.Introspected + +@Introspected(classes = [io.micronaut.kotlin.processing.elementapi.TestClass::class]) +class MyConfig +""") + + when: + BeanIntrospector beanIntrospector = BeanIntrospector.forClassLoader(classLoader) + + then: + BeanIntrospection beanIntrospection = beanIntrospector.getIntrospection(TestClass) + beanIntrospection != null + beanIntrospection.getBeanProperties().size() == 2 + } + + void "test introspection on abstract class with extra getter"() { + BeanIntrospection beanIntrospection = buildBeanIntrospection("test.Test", """ +package test + +import io.micronaut.core.annotation.Introspected + +@Introspected +abstract class Test { + var name: String? = null + var author: String? = null + + fun getAge(): Int = 0 +} +""") + + expect: + beanIntrospection != null + beanIntrospection.getBeanProperties().size() == 3 + } + + void "test class loading is not shared between the introspection and the ref"() { + when: + BeanIntrospection beanIntrospection = buildBeanIntrospection("test.Test", """ +package test; + +import io.micronaut.core.annotation.Introspected; +import java.util.Set; + +@Introspected(excludedAnnotations = [Deprecated::class]) +public class Test { + var authors: Set? = null +} + +@Introspected(excludedAnnotations = [Deprecated::class]) +class Author { + var name: String? = null +} +""") + + then: + noExceptionThrown() + beanIntrospection != null + beanIntrospection.getBeanProperties().size() == 1 + } + + void "test annotation on setter"() { + when: + BeanIntrospection beanIntrospection = buildBeanIntrospection("test.Test", """ +package test + +import com.fasterxml.jackson.annotation.JsonProperty +import io.micronaut.core.annotation.Introspected + +@Introspected +class Test { + @set:JsonProperty + var foo: String = "bar" +} +""") + + then: + noExceptionThrown() + beanIntrospection != null + beanIntrospection.getBeanProperties().size() == 1 + beanIntrospection.getBeanProperties()[0].annotationMetadata.hasAnnotation(JsonProperty) + } + + void "test annotation on field"() { + when: + BeanIntrospection beanIntrospection = buildBeanIntrospection("test.Test", """ +package test + +import com.fasterxml.jackson.annotation.JsonProperty +import io.micronaut.core.annotation.Introspected + +@Introspected +class Test { + @field:JsonProperty + var foo: String = "bar" +} +""") + + then: + noExceptionThrown() + beanIntrospection != null + beanIntrospection.getBeanProperties().size() == 1 + beanIntrospection.getBeanProperties()[0].annotationMetadata.hasAnnotation(JsonProperty) + } + + void "test field annotation overrides getter and setter"() { + when: + BeanIntrospection beanIntrospection = buildBeanIntrospection("test.Test", """ +package test + +import com.fasterxml.jackson.annotation.JsonProperty +import io.micronaut.core.annotation.Introspected + +@Introspected +class Test { + @field:JsonProperty("field") + @get:JsonProperty("getter") + @set:JsonProperty("setter") + var foo: String? = null +} +""") + + then: + noExceptionThrown() + beanIntrospection != null + beanIntrospection.getBeanProperties().size() == 1 + beanIntrospection.getBeanProperties()[0].annotationMetadata.getAnnotation(JsonProperty).stringValue().get() == 'field' + } + + void "test getter annotation overrides setter"() { + when: + BeanIntrospection beanIntrospection = buildBeanIntrospection("test.Test", """ +package test + +import com.fasterxml.jackson.annotation.JsonProperty +import io.micronaut.core.annotation.Introspected + +@Introspected +class Test { + @get:JsonProperty("getter") + @set:JsonProperty("setter") + var foo: String? = null +} +""") + + then: + noExceptionThrown() + beanIntrospection != null + beanIntrospection.getBeanProperties().size() == 1 + beanIntrospection.getBeanProperties()[0].annotationMetadata.getAnnotation(JsonProperty).stringValue().get() == 'getter' + } + + void "test create bean introspection for interface"() { + given: + def classLoader = buildClassLoader('itfcetest.MyInterface',''' +package itfcetest + +import com.fasterxml.jackson.annotation.JsonClassDescription +import io.micronaut.core.annotation.Introspected +import io.micronaut.context.annotation.Executable + +@Introspected(classes = [MyInterface::class]) +class Test + +@JsonClassDescription +public interface MyInterface { + fun getName(): String + + @Executable + fun name(): String = getName() +} + +class MyImpl: MyInterface { + override fun getName(): String = "ok" +} +''') + when:"the reference is loaded" + def clazz = classLoader.loadClass('itfcetest.$Test$IntrospectionRef0') + BeanIntrospectionReference reference = clazz.newInstance() + BeanIntrospection introspection = reference.load() + + then: + introspection.getBeanType().isInterface() + introspection.beanProperties.size() == 1 + introspection.beanMethods.size() == 1 + introspection.hasAnnotation(JsonClassDescription) + } + + void "test create bean introspection for interface - only methods"() { + given: + def classLoader = buildClassLoader('itfcetest.MyInterface',''' +package itfcetest + +import io.micronaut.core.annotation.Introspected +import io.micronaut.context.annotation.Executable + +@Introspected(classes = [MyInterface::class]) +class Test + +interface MyInterface { + @Executable + fun name(): String +} + +class MyImpl: MyInterface { + override fun name(): String = "ok" +} +''') + when:"the reference is loaded" + def clazz = classLoader.loadClass('itfcetest.$Test$IntrospectionRef0') + BeanIntrospectionReference reference = clazz.newInstance() + BeanIntrospection introspection = reference.load() + + then: + introspection.getBeanType().isInterface() + introspection.beanProperties.size() == 0 + introspection.beanMethods.size() == 1 + } +} diff --git a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/visitor/KotlinReconstructionSpec.groovy b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/visitor/KotlinReconstructionSpec.groovy new file mode 100644 index 00000000000..98c96816e1c --- /dev/null +++ b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/visitor/KotlinReconstructionSpec.groovy @@ -0,0 +1,161 @@ +package io.micronaut.kotlin.processing.visitor + +import io.micronaut.annotation.processing.test.AbstractKotlinCompilerSpec +import io.micronaut.inject.ast.GenericPlaceholderElement +import spock.lang.PendingFeature +import spock.lang.Unroll + + +class KotlinReconstructionSpec extends AbstractKotlinCompilerSpec { + @PendingFeature(reason = "Not yet implemented") + @Unroll("field type is #fieldType") + def 'field type'() { + given: + def element = buildClassElement("example.Test", """ +package example; + +import java.util.*; + +class Test { + lateinit var field : $fieldType +} +""") + def field = element.getFields()[0] + + expect: + reconstructTypeSignature(field.genericType) == fieldType + + where: + fieldType << [ + 'String', + 'List', + 'List', + 'List>', + 'List', +// 'List', // doesn't work? + 'List>', + 'List>>>' + ] + } + + @Unroll("super type is #superType") + def 'super type'() { + given: + def element = buildClassElement("example.Test", """ +package example; + +import java.util.*; + +abstract class Test : $superType() { +} +""") + + expect: + reconstructTypeSignature(element.superType.get()) == superType + + where: + superType << [ +// 'AbstractList', raw types not supported + 'AbstractList', + 'AbstractList', + 'AbstractList>', + 'AbstractList>', + 'AbstractList>>', + 'AbstractList>>>', + 'AbstractList>' + ] + } + + @Unroll("super interface is #superType") + def 'super interface'() { + given: + def element = buildClassElement("example.Test", """ +package example; + +import java.util.*; + +abstract class Test : $superType { +} +""") + + expect: + reconstructTypeSignature(element.interfaces[0]) == superType + + where: + superType << [ +// 'List', + 'List', + 'List', + 'List>', + 'List>', +// 'List>', + 'List>>', + 'List>>>', +// 'List', + 'List>', + ] + } + + @Unroll("type var is #decl") + @PendingFeature + def 'type vars declared on type'() { + given: + def element = buildClassElement("example.Test", """ +package example; + +import java.util.*; + +abstract class Test { +} +""") + + expect: + reconstructTypeSignature(element.declaredGenericPlaceholders[1], true) == decl + + where: + decl << [ + 'T', + 'out T : CharSequence', + 'T : A', +// 'T extends List', +// 'T extends List', +// 'T extends List', +// 'T extends List', +// 'T extends List', +// 'T extends List', + ] + } + + @Unroll('declaration is #decl') + @PendingFeature(reason = "Not yet implemented") + def 'fold type variable to null'() { + given: + def classElement = buildClassElement("example.Test", """ +package example; + +import java.util.*; + +class Test { + lateinit var field : $decl; +} +""") + def fieldType = classElement.fields[0].type + + expect: + reconstructTypeSignature(fieldType.foldBoundGenericTypes { + if (it != null && it.isGenericPlaceholder() && ((GenericPlaceholderElement) it).variableName == 'T') { + return null + } else { + return it + } + }) == expected + + where: + decl | expected + 'String' | 'String' + 'List' | 'List' + 'Map' | 'Map' + 'List' | 'List' +// 'List' | 'List' + } +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/Logged.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/Logged.kt new file mode 100644 index 00000000000..fa8fd47e1de --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/Logged.kt @@ -0,0 +1,26 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.kotlin.processing.aop + +import io.micronaut.aop.Around +import io.micronaut.context.annotation.Type + +@Around +@Type(LoggedInterceptor::class) +@MustBeDocumented +@Retention(AnnotationRetention.RUNTIME) +@Target(AnnotationTarget.FUNCTION, AnnotationTarget.CLASS) +annotation class Logged diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/LoggedInterceptor.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/LoggedInterceptor.kt new file mode 100644 index 00000000000..ea77fff35cd --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/LoggedInterceptor.kt @@ -0,0 +1,29 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.kotlin.processing.aop + +import io.micronaut.aop.Interceptor +import io.micronaut.aop.InvocationContext + +class LoggedInterceptor : Interceptor { + + override fun intercept(context: InvocationContext): Any? { + println("Starting method") + val value = context.proceed() + println("Finished method") + return value + } +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/adapter/Test.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/adapter/Test.kt new file mode 100644 index 00000000000..e768d06f310 --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/adapter/Test.kt @@ -0,0 +1,20 @@ +package io.micronaut.kotlin.processing.aop.adapter + +import io.micronaut.aop.Adapter +import io.micronaut.context.annotation.Requires +import io.micronaut.context.event.ApplicationEventListener +import io.micronaut.context.event.StartupEvent +import jakarta.inject.Singleton + +@Singleton +@Requires(property = "foo.bar") +internal class Test { + + var isInvoked = false + private set + + @Adapter(ApplicationEventListener::class) + fun onStartup(event: StartupEvent) { + isInvoked = true + } +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/compile/AroundConstructAnnTransformer.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/compile/AroundConstructAnnTransformer.kt new file mode 100644 index 00000000000..fc685ffa5ab --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/compile/AroundConstructAnnTransformer.kt @@ -0,0 +1,24 @@ +package io.micronaut.kotlin.processing.aop.compile + +import io.micronaut.aop.InterceptorBinding +import io.micronaut.aop.InterceptorKind +import io.micronaut.core.annotation.AnnotationValue +import io.micronaut.inject.annotation.NamedAnnotationTransformer +import io.micronaut.inject.visitor.VisitorContext + +class AroundConstructAnnTransformer: NamedAnnotationTransformer { + + override fun transform( + annotation: AnnotationValue, + visitorContext: VisitorContext + ): List> { + return listOf(AnnotationValue.builder(InterceptorBinding::class.java) + .member("kind", InterceptorKind.AROUND_CONSTRUCT) + .member("bindMembers", true) + .build()) + } + + override fun getName(): String { + return "aroundconstructmapperbindingmembers.MyInterceptorBinding" + } +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/compile/NamedTestAnnMapper.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/compile/NamedTestAnnMapper.kt new file mode 100644 index 00000000000..b5487336666 --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/compile/NamedTestAnnMapper.kt @@ -0,0 +1,22 @@ +package io.micronaut.kotlin.processing.aop.compile + +import io.micronaut.aop.InterceptorBinding +import io.micronaut.core.annotation.AnnotationValue +import io.micronaut.inject.annotation.NamedAnnotationMapper +import io.micronaut.inject.visitor.VisitorContext + +class NamedTestAnnMapper: NamedAnnotationMapper { + + override fun map( + annotation: AnnotationValue, + visitorContext: VisitorContext + ): List> { + return listOf(AnnotationValue.builder(InterceptorBinding::class.java) + .value(name) + .build()) + } + + override fun getName(): String { + return "mapperbinding.TestAnn" + } +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/compile/TestStereotypeAnnTransformer.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/compile/TestStereotypeAnnTransformer.kt new file mode 100644 index 00000000000..faab2706980 --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/compile/TestStereotypeAnnTransformer.kt @@ -0,0 +1,24 @@ +package io.micronaut.kotlin.processing.aop.compile + +import io.micronaut.aop.InterceptorBinding +import io.micronaut.aop.InterceptorKind +import io.micronaut.core.annotation.AnnotationValue +import io.micronaut.inject.annotation.NamedAnnotationTransformer +import io.micronaut.inject.visitor.VisitorContext + +class TestStereotypeAnnTransformer: NamedAnnotationTransformer { + + override fun transform( + annotation: AnnotationValue, + visitorContext: VisitorContext + ): List> { + return listOf(AnnotationValue.builder(InterceptorBinding::class.java) + .member("kind", InterceptorKind.AROUND) + .member("bindMembers", true) + .build()) + } + + override fun getName(): String { + return "mapperbindingmembers.MyInterceptorBinding" + } +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/factory/AnotherClass.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/factory/AnotherClass.kt new file mode 100644 index 00000000000..2fcbb771ccd --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/factory/AnotherClass.kt @@ -0,0 +1,6 @@ +package io.micronaut.kotlin.processing.aop.factory + +import jakarta.inject.Singleton + +@Singleton +class AnotherClass diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/factory/ConcreteClass.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/factory/ConcreteClass.kt new file mode 100644 index 00000000000..56d46518f79 --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/factory/ConcreteClass.kt @@ -0,0 +1,116 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.kotlin.processing.aop.factory + +import io.micronaut.core.annotation.Creator +import io.micronaut.kotlin.processing.aop.simple.CovariantClass +import io.micronaut.kotlin.processing.aop.simple.Mutating + +/** + * @author Graeme Rocher + * @since 1.0 + */ +open class ConcreteClass { + private val anotherClass: AnotherClass? + + @Creator + constructor() { + anotherClass = null + } + + constructor(anotherClass: AnotherClass?) { + this.anotherClass = anotherClass + } + + open fun test(name: String): String { + return "Name is $name" + } + + open fun test(name: String, age: Int): String { + return "Name is $name and age is $age" + } + + open fun test(): String { + return "noargs" + } + + open fun testVoid(name: String) { + assert(name == "changed") + } + + open fun testVoid(name: String, age: Int) { + assert(name == "changed") + assert(age == 10) + } + + open fun testBoolean(name: String): Boolean { + return name == "changed" + } + + open fun testBoolean(name: String, age: Int): Boolean { + assert(age == 10) + return name == "changed" + } + + open fun testInt(name: String): Int { + return if (name == "changed") 1 else 0 + } + + open fun testLong(name: String): Long { + return if (name == "changed") 1 else 0 + } + + open fun testShort(name: String): Short { + return (if (name == "changed") 1 else 0).toShort() + } + + open fun testByte(name: String): Byte { + return (if (name == "changed") 1 else 0).toByte() + } + + open fun testDouble(name: String): Double { + return if (name == "changed") 1.0 else 0.0 + } + + open fun testFloat(name: String): Float { + return if (name == "changed") 1F else 0F + } + + open fun testChar(name: String): Char { + return (if (name == "changed") 1 else 0).toChar() + } + + open fun testByteArray(name: String, data: ByteArray): ByteArray { + assert(name == "changed") + return data + } + + open fun testGenericsWithExtends(name: T, age: Int): T { + return "Name is $name" as T + } + + open fun testListWithWildCardIn(name: T, p2: CovariantClass): CovariantClass { + return CovariantClass(name.toString()) + } + + open fun testListWithWildCardOut(name: T, p2: CovariantClass): CovariantClass { + return CovariantClass(name.toString()) + } + + open fun testGenericsFromType(name: Any, age: Int): Any { + return "Name is $name" + } +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/factory/ConcreteClassFactory.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/factory/ConcreteClassFactory.kt new file mode 100644 index 00000000000..c1681508616 --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/factory/ConcreteClassFactory.kt @@ -0,0 +1,25 @@ +package io.micronaut.kotlin.processing.aop.factory + +import io.micronaut.context.annotation.Factory +import io.micronaut.context.annotation.Primary +import io.micronaut.context.annotation.Prototype +import io.micronaut.kotlin.processing.aop.simple.Mutating +import jakarta.inject.Named + +@Factory +class ConcreteClassFactory { + + @Prototype + @Mutating("name") + @Primary + fun concreteClass(): ConcreteClass { + return ConcreteClass(AnotherClass()) + } + + @Prototype + @Mutating("name") + @Named("another") + fun anotherImpl(): ConcreteClass { + return ConcreteClass(AnotherClass()) + } +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/factory/InterfaceClass.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/factory/InterfaceClass.kt new file mode 100644 index 00000000000..2365862ae68 --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/factory/InterfaceClass.kt @@ -0,0 +1,44 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.kotlin.processing.aop.factory + +import io.micronaut.kotlin.processing.aop.simple.CovariantClass + +/** + * @author Graeme Rocher + * @since 1.0 + */ +interface InterfaceClass { + fun test(name: String): String + fun test(name: String, age: Int): String + fun test(): String + fun testVoid(name: String) + fun testVoid(name: String, age: Int) + fun testBoolean(name: String): Boolean + fun testBoolean(name: String, age: Int): Boolean + fun testInt(name: String): Int + fun testLong(name: String): Long + fun testShort(name: String): Short + fun testByte(name: String): Byte + fun testDouble(name: String): Double + fun testFloat(name: String): Float + fun testChar(name: String): Char + fun testByteArray(name: String, data: ByteArray): ByteArray + fun testGenericsWithExtends(name: T, age: Int): T + fun testListWithWildCardIn(name: T, p2: CovariantClass): CovariantClass + fun testListWithWildCardOut(name: T, p2: CovariantClass): CovariantClass + fun testGenericsFromType(name: A, age: Int): A +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/factory/InterfaceFactory.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/factory/InterfaceFactory.kt new file mode 100644 index 00000000000..7786993b088 --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/factory/InterfaceFactory.kt @@ -0,0 +1,28 @@ +package io.micronaut.kotlin.processing.aop.factory + +import io.micronaut.context.annotation.Executable +import io.micronaut.context.annotation.Factory +import io.micronaut.context.annotation.Primary +import io.micronaut.context.annotation.Prototype +import io.micronaut.kotlin.processing.aop.simple.Mutating +import jakarta.inject.Named + +@Factory +class InterfaceFactory { + + @Prototype + @Mutating("name") + @Primary + @Executable + fun interfaceClass(): InterfaceClass<*> { + return InterfaceImpl() + } + + @Prototype + @Mutating("name") + @Named("another") + @Executable + fun anotherImpl(): InterfaceClass<*> { + return InterfaceImpl() + } +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/factory/InterfaceImpl.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/factory/InterfaceImpl.kt new file mode 100644 index 00000000000..75a0cafe049 --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/factory/InterfaceImpl.kt @@ -0,0 +1,86 @@ +package io.micronaut.kotlin.processing.aop.factory; + +import io.micronaut.kotlin.processing.aop.simple.CovariantClass + +class InterfaceImpl: InterfaceClass { + + override fun test(): String { + return "noargs" + } + + override fun test(name: String): String { + return "Name is $name" + } + + override fun test(name: String, age: Int): String { + return "Name is $name and age is $age" + } + + override fun testVoid(name: String) { + assert(name == "changed") + } + + override fun testVoid(name: String, age: Int) { + assert(name == "changed") + assert(age == 10) + } + + override fun testBoolean(name: String): Boolean { + return name == "changed" + } + + override fun testBoolean(name: String, age: Int): Boolean { + assert(age == 10) + return name == "changed" + } + + override fun testInt(name: String): Int { + return if (name == "changed") 1 else 0 + } + + override fun testLong(name: String): Long { + return if (name == "changed") 1 else 0 + } + + override fun testShort(name: String): Short { + return (if (name == "changed") 1 else 0).toShort() + } + + override fun testByte(name: String): Byte { + return (if (name == "changed") 1 else 0).toByte() + } + + override fun testDouble(name: String): Double { + return if (name == "changed") 1.0 else 0.0 + } + + override fun testFloat(name: String): Float { + return if (name == "changed") 1F else 0F + } + + override fun testChar(name: String): Char { + return (if (name == "changed") 1 else 0).toChar() + } + + override fun testByteArray(name: String, data: ByteArray): ByteArray { + assert(name == "changed") + return data + } + + override fun testGenericsWithExtends(name: T, age: Int): T { + return "Name is $name" as T + } + + override fun testListWithWildCardIn(name: T, p2: CovariantClass): CovariantClass { + return CovariantClass(name.toString()) + } + + + override fun testListWithWildCardOut(name: T, p2: CovariantClass): CovariantClass { + return CovariantClass(name.toString()) + } + + override fun testGenericsFromType(name: Any, age: Int): Any { + return "Name is $name" + } +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/factory/SessionFactoryFactory.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/factory/SessionFactoryFactory.kt new file mode 100644 index 00000000000..db791740b8d --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/factory/SessionFactoryFactory.kt @@ -0,0 +1,17 @@ +package io.micronaut.kotlin.processing.aop.factory + +import io.micronaut.kotlin.processing.aop.simple.Mutating +import io.micronaut.context.annotation.Factory +import io.micronaut.context.annotation.Prototype +import org.hibernate.SessionFactory +import org.hibernate.engine.spi.SessionFactoryDelegatingImpl + +@Factory +class SessionFactoryFactory { + + @Mutating("name") + @Prototype + fun sessionFactory(): SessionFactory { + return SessionFactoryDelegatingImpl(null) + } +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/hotswap/HotswappableProxyingClass.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/hotswap/HotswappableProxyingClass.kt new file mode 100644 index 00000000000..e17384c8064 --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/hotswap/HotswappableProxyingClass.kt @@ -0,0 +1,23 @@ +package io.micronaut.kotlin.processing.aop.hotswap + +import io.micronaut.aop.Around +import io.micronaut.kotlin.processing.aop.proxytarget.Mutating +import jakarta.inject.Singleton + +@Around(proxyTarget = true, hotswap = true) +@Singleton +open class HotswappableProxyingClass { + + var invocationCount = 0 + + @Mutating("name") + open fun test(name: String): String { + invocationCount++ + return "Name is $name" + } + + open fun test2(another: String): String { + invocationCount++ + return "Name is $another" + } +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/AbstractClass.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/AbstractClass.kt new file mode 100644 index 00000000000..733e19c634d --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/AbstractClass.kt @@ -0,0 +1,16 @@ +package io.micronaut.kotlin.processing.aop.introduction + +import io.micronaut.kotlin.processing.aop.simple.Mutating +import jakarta.inject.Singleton + +@Stub +@Singleton +@Mutating("name") +abstract class AbstractClass : AbstractSuperClass() { + + abstract fun test(name: String): String + + fun nonAbstract(name: String): String { + return test(name) + } +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/AbstractCrudRepo.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/AbstractCrudRepo.kt new file mode 100644 index 00000000000..6c8bcd94477 --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/AbstractCrudRepo.kt @@ -0,0 +1,8 @@ +package io.micronaut.kotlin.processing.aop.introduction + +import java.util.* + +abstract class AbstractCrudRepo { + + abstract fun findById(id: ID): Optional +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/AbstractCustomAbstractCrudRepo.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/AbstractCustomAbstractCrudRepo.kt new file mode 100644 index 00000000000..1ce5debbba9 --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/AbstractCustomAbstractCrudRepo.kt @@ -0,0 +1,10 @@ +package io.micronaut.kotlin.processing.aop.introduction + +import java.util.* + +@RepoDef +abstract class AbstractCustomAbstractCrudRepo : AbstractCrudRepo() { + + @Marker + abstract override fun findById(aLong: Long): Optional +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/AbstractCustomCrudRepo.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/AbstractCustomCrudRepo.kt new file mode 100644 index 00000000000..714d01072a3 --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/AbstractCustomCrudRepo.kt @@ -0,0 +1,10 @@ +package io.micronaut.kotlin.processing.aop.introduction + +import java.util.* + +@RepoDef +abstract class AbstractCustomCrudRepo : CrudRepo { + + @Marker + abstract override fun findById(aLong: Long): Optional +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/AbstractSuperClass.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/AbstractSuperClass.kt new file mode 100644 index 00000000000..84fcc4a0580 --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/AbstractSuperClass.kt @@ -0,0 +1,6 @@ +package io.micronaut.kotlin.processing.aop.introduction + +abstract class AbstractSuperClass : SuperInterface { + + abstract fun test(name: String, age: Int): String +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/ChildIntroduction.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/ChildIntroduction.kt new file mode 100644 index 00000000000..621baaa45b9 --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/ChildIntroduction.kt @@ -0,0 +1,4 @@ +package io.micronaut.kotlin.processing.aop.introduction + +@Stub +interface ChildIntroduction : ParentInterface> diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/ConcreteClass.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/ConcreteClass.kt new file mode 100644 index 00000000000..5458510e739 --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/ConcreteClass.kt @@ -0,0 +1,7 @@ +package io.micronaut.kotlin.processing.aop.introduction + +import jakarta.inject.Singleton + +@ListenerAdvice +@Singleton +open class ConcreteClass diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/CrudRepo.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/CrudRepo.kt new file mode 100644 index 00000000000..b9706373c8a --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/CrudRepo.kt @@ -0,0 +1,8 @@ +package io.micronaut.kotlin.processing.aop.introduction + +import java.util.* + +interface CrudRepo { + + fun findById(id: ID): Optional +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/CustomCrudRepo.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/CustomCrudRepo.kt new file mode 100644 index 00000000000..07721f6152f --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/CustomCrudRepo.kt @@ -0,0 +1,10 @@ +package io.micronaut.kotlin.processing.aop.introduction + +import java.util.* + +@RepoDef +interface CustomCrudRepo : CrudRepo { + + @Marker + override fun findById(aLong: Long): Optional +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/DeleteByIdCrudRepo.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/DeleteByIdCrudRepo.kt new file mode 100644 index 00000000000..afa59aaef8b --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/DeleteByIdCrudRepo.kt @@ -0,0 +1,23 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.kotlin.processing.aop.introduction + +import javax.validation.constraints.NotNull + +interface DeleteByIdCrudRepo { + + fun deleteById(@NotNull id: ID) +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/InjectParentInterface.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/InjectParentInterface.kt new file mode 100644 index 00000000000..98101b04c18 --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/InjectParentInterface.kt @@ -0,0 +1,6 @@ +package io.micronaut.kotlin.processing.aop.introduction + +import jakarta.inject.Singleton + +@Singleton +class InjectParentInterface(parentInterface: ParentInterface<*>) diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/InterfaceIntroductionClass.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/InterfaceIntroductionClass.kt new file mode 100644 index 00000000000..18989ed5518 --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/InterfaceIntroductionClass.kt @@ -0,0 +1,13 @@ +package io.micronaut.kotlin.processing.aop.introduction + +import io.micronaut.kotlin.processing.aop.simple.Mutating +import jakarta.inject.Singleton + +@Stub +@Mutating("name") +@Singleton +interface InterfaceIntroductionClass : SuperInterface { + + fun test(name: String): String + fun test(name: String, age: Int): String +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/ListenerAdvice.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/ListenerAdvice.kt new file mode 100644 index 00000000000..84bcb2f07b2 --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/ListenerAdvice.kt @@ -0,0 +1,32 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.kotlin.processing.aop.introduction + +import io.micronaut.aop.Introduction +import io.micronaut.context.annotation.Executable +import io.micronaut.context.annotation.Type +import io.micronaut.context.event.ApplicationEventListener + +/** + * @author graemerocher + * @since 1.0 + */ +@Introduction(interfaces = [ApplicationEventListener::class]) +@Type(ListenerAdviceInterceptor::class) +@MustBeDocumented +@Retention +@Executable +annotation class ListenerAdvice diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/ListenerAdviceInterceptor.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/ListenerAdviceInterceptor.kt new file mode 100644 index 00000000000..d9829015ae0 --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/ListenerAdviceInterceptor.kt @@ -0,0 +1,51 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.kotlin.processing.aop.introduction + +import io.micronaut.aop.MethodInterceptor +import io.micronaut.aop.MethodInvocationContext +import io.micronaut.core.annotation.Nullable +import jakarta.inject.Singleton +import java.util.HashSet + +/** + * @author graemerocher + * @since 1.0 + */ +@Singleton +class ListenerAdviceInterceptor : MethodInterceptor { + + private val recievedMessages: MutableSet = HashSet() + + override fun getOrder(): Int { + return StubIntroducer.POSITION - 10 + } + + fun getRecievedMessages(): Set { + return recievedMessages + } + + @Nullable + override fun intercept(context: MethodInvocationContext): Any? { + return if (context.methodName == "onApplicationEvent") { + val v = context.parameterValues[0] + recievedMessages.add(v) + null + } else { + context.proceed() + } + } +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/ListenerAdviceMarker.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/ListenerAdviceMarker.kt new file mode 100644 index 00000000000..cc1e186228a --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/ListenerAdviceMarker.kt @@ -0,0 +1,6 @@ +package io.micronaut.kotlin.processing.aop.introduction + +@MustBeDocumented +@Retention +@Target(AnnotationTarget.ANNOTATION_CLASS, AnnotationTarget.CLASS) +annotation class ListenerAdviceMarker diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/ListenerAdviceMarkerMapper.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/ListenerAdviceMarkerMapper.kt new file mode 100644 index 00000000000..e6bf4119cd2 --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/ListenerAdviceMarkerMapper.kt @@ -0,0 +1,26 @@ +package io.micronaut.kotlin.processing.aop.introduction + +import io.micronaut.core.annotation.AnnotationValue +import io.micronaut.inject.annotation.TypedAnnotationMapper +import io.micronaut.inject.visitor.VisitorContext +import java.util.ArrayList + +class ListenerAdviceMarkerMapper : TypedAnnotationMapper { + + override fun annotationType(): Class { + return ListenerAdviceMarker::class.java + } + + override fun map( + annotation: AnnotationValue, + visitorContext: VisitorContext + ): List> { + val mappedAnnotations: MutableList> = ArrayList() + mappedAnnotations.add( + AnnotationValue.builder( + ListenerAdvice::class.java + ).build() + ) + return mappedAnnotations + } +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/Marker.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/Marker.kt new file mode 100644 index 00000000000..495d3b56415 --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/Marker.kt @@ -0,0 +1,5 @@ +package io.micronaut.kotlin.processing.aop.introduction + +@MustBeDocumented +@Retention +annotation class Marker diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/MyRepo.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/MyRepo.kt new file mode 100644 index 00000000000..f5410d53c4c --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/MyRepo.kt @@ -0,0 +1,9 @@ +package io.micronaut.kotlin.processing.aop.introduction + +@RepoDef +interface MyRepo : SuperRepo { + + fun aBefore(): String + override fun findAll(): List + fun xAfter(): String +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/MyRepo2.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/MyRepo2.kt new file mode 100644 index 00000000000..651a31fd14f --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/MyRepo2.kt @@ -0,0 +1,9 @@ +package io.micronaut.kotlin.processing.aop.introduction + +import javax.validation.constraints.NotNull + +@RepoDef +interface MyRepo2 : DeleteByIdCrudRepo { + + override fun deleteById(@NotNull id: Int) +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/MyRepoIntroducer.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/MyRepoIntroducer.kt new file mode 100644 index 00000000000..944ca133698 --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/MyRepoIntroducer.kt @@ -0,0 +1,24 @@ +package io.micronaut.kotlin.processing.aop.introduction + +import io.micronaut.aop.MethodInterceptor +import io.micronaut.aop.MethodInvocationContext +import io.micronaut.core.annotation.Nullable +import jakarta.inject.Singleton +import java.lang.reflect.Method +import java.util.ArrayList + +@Singleton +class MyRepoIntroducer : MethodInterceptor { + + var executableMethods = mutableListOf() + + override fun getOrder(): Int { + return 0 + } + + @Nullable + override fun intercept(context: MethodInvocationContext): Any? { + executableMethods.add(context.executableMethod.targetMethod) + return null + } +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/NotImplemented.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/NotImplemented.kt new file mode 100644 index 00000000000..78b8e891204 --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/NotImplemented.kt @@ -0,0 +1,25 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.kotlin.processing.aop.introduction + +import io.micronaut.aop.Introduction +import io.micronaut.context.annotation.Type + +@Introduction +@Type(NotImplementedAdvice::class) +@MustBeDocumented +@Retention +annotation class NotImplemented diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/NotImplementedAdvice.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/NotImplementedAdvice.kt new file mode 100644 index 00000000000..2c64c7c400a --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/NotImplementedAdvice.kt @@ -0,0 +1,15 @@ +package io.micronaut.kotlin.processing.aop.introduction + +import io.micronaut.aop.MethodInterceptor +import io.micronaut.aop.MethodInvocationContext +import jakarta.inject.Singleton + +@Singleton +class NotImplementedAdvice : MethodInterceptor { + var invoked = false + + override fun intercept(context: MethodInvocationContext): Any? { + invoked = true + return context.proceed() + } +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/ParentInterface.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/ParentInterface.kt new file mode 100644 index 00000000000..471fe863e46 --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/ParentInterface.kt @@ -0,0 +1,3 @@ +package io.micronaut.kotlin.processing.aop.introduction + +interface ParentInterface> diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/RepoDef.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/RepoDef.kt new file mode 100644 index 00000000000..c253209a26b --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/RepoDef.kt @@ -0,0 +1,25 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.kotlin.processing.aop.introduction + +import io.micronaut.aop.Introduction +import io.micronaut.context.annotation.Type + +@Introduction +@Type(MyRepoIntroducer::class) +@MustBeDocumented +@Retention +annotation class RepoDef diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/Stub.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/Stub.kt new file mode 100644 index 00000000000..bd43ac0d015 --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/Stub.kt @@ -0,0 +1,10 @@ +package io.micronaut.kotlin.processing.aop.introduction + +import io.micronaut.aop.Introduction +import io.micronaut.context.annotation.Type + +@Introduction +@Type(StubIntroducer::class) +@MustBeDocumented +@Retention +annotation class Stub(val value: String = "") diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/StubIntroducer.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/StubIntroducer.kt new file mode 100644 index 00000000000..232adee3639 --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/StubIntroducer.kt @@ -0,0 +1,28 @@ +package io.micronaut.kotlin.processing.aop.introduction + +import io.micronaut.aop.MethodInterceptor +import io.micronaut.aop.MethodInvocationContext +import io.micronaut.core.type.MutableArgumentValue +import io.micronaut.core.util.StringUtils +import jakarta.inject.Singleton + +@Singleton +class StubIntroducer : MethodInterceptor { + + override fun getOrder(): Int { + return POSITION + } + + companion object { + const val POSITION = 0 + } + + override fun intercept(context: MethodInvocationContext): Any? { + return context.stringValue(// <3> + Stub::class.java + ).filter { StringUtils.isNotEmpty(it) }.orElseGet { + val iterator: Iterator> = context.parameters.values.iterator() + if (iterator.hasNext()) iterator.next().value?.toString() else null + } + } +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/SuperInterface.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/SuperInterface.kt new file mode 100644 index 00000000000..6ef1d465472 --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/SuperInterface.kt @@ -0,0 +1,6 @@ +package io.micronaut.kotlin.processing.aop.introduction + +interface SuperInterface { + + fun testGenericsFromType(name: A, age: Int): A +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/SuperRepo.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/SuperRepo.kt new file mode 100644 index 00000000000..f7739aacb18 --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/SuperRepo.kt @@ -0,0 +1,21 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.kotlin.processing.aop.introduction + +interface SuperRepo { + + fun findAll(): Iterable +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/delegation/Delegating.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/delegation/Delegating.kt new file mode 100644 index 00000000000..597ce0ea73a --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/delegation/Delegating.kt @@ -0,0 +1,5 @@ +package io.micronaut.kotlin.processing.aop.introduction.delegation + +interface Delegating { + fun test(): String +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/delegation/DelegatingImpl.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/delegation/DelegatingImpl.kt new file mode 100644 index 00000000000..a77c1085dff --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/delegation/DelegatingImpl.kt @@ -0,0 +1,8 @@ +package io.micronaut.kotlin.processing.aop.introduction.delegation + +class DelegatingImpl : Delegating { + + override fun test(): String { + return "good" + } +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/delegation/DelegatingInterceptor.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/delegation/DelegatingInterceptor.kt new file mode 100644 index 00000000000..02fde438a3b --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/delegation/DelegatingInterceptor.kt @@ -0,0 +1,43 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.kotlin.processing.aop.introduction.delegation + +import io.micronaut.aop.MethodInterceptor +import io.micronaut.aop.MethodInvocationContext +import jakarta.inject.Singleton + +@Singleton +class DelegatingInterceptor : MethodInterceptor { + + override fun intercept(context: MethodInvocationContext): Any? { + val executableMethod = context.executableMethod + val parameterValues = context.parameterValues + return if (executableMethod.name == "test2") { + val instance: DelegatingIntroduced = object : DelegatingIntroduced { + override fun test2(): String { + return "good" + } + + override fun test(): String { + return "good" + } + } + executableMethod.invoke(instance, *parameterValues) + } else { + executableMethod.invoke(DelegatingImpl(), *parameterValues) + } + } +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/delegation/DelegatingIntroduced.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/delegation/DelegatingIntroduced.kt new file mode 100644 index 00000000000..82be9b6817e --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/delegation/DelegatingIntroduced.kt @@ -0,0 +1,6 @@ +package io.micronaut.kotlin.processing.aop.introduction.delegation + +@DelegationAdvice +interface DelegatingIntroduced : Delegating { + fun test2(): String +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/delegation/DelegationAdvice.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/delegation/DelegationAdvice.kt new file mode 100644 index 00000000000..c7021dfa9cc --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/delegation/DelegationAdvice.kt @@ -0,0 +1,10 @@ +package io.micronaut.kotlin.processing.aop.introduction.delegation + +import io.micronaut.aop.Introduction +import io.micronaut.context.annotation.Type + +@Introduction +@Type(DelegatingInterceptor::class) +@MustBeDocumented +@Retention +annotation class DelegationAdvice diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/temp/MyBean.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/temp/MyBean.kt new file mode 100644 index 00000000000..14cfe1d9d40 --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/temp/MyBean.kt @@ -0,0 +1,18 @@ +package io.micronaut.kotlin.processing.aop.introduction.temp + +import io.micronaut.kotlin.processing.aop.introduction.* +import io.micronaut.context.annotation.* + +@ListenerAdvice +@Stub +@jakarta.inject.Singleton +interface MyBean { + + @Executable + fun getBar(): String + + @Executable + fun getFoo() : String { + return "good" + } +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/with_around/CustomProxy.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/with_around/CustomProxy.kt new file mode 100644 index 00000000000..16009baeda7 --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/with_around/CustomProxy.kt @@ -0,0 +1,5 @@ +package io.micronaut.kotlin.processing.aop.introduction.with_around + +interface CustomProxy { + fun isProxy(): Boolean +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/with_around/MyBean1.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/with_around/MyBean1.kt new file mode 100644 index 00000000000..dd4203926c3 --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/with_around/MyBean1.kt @@ -0,0 +1,36 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.kotlin.processing.aop.introduction.with_around + +@ProxyIntroduction +@ProxyAround +open class MyBean1 { + + private var id: Long? = null + private var name: String? = null + + open fun getId(): Long? = id + + open fun setId(id: Long?) { + this.id = id + } + + open fun getName(): String? = name + + open fun setId(name: String?) { + this.name = name + } +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/with_around/MyBean2.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/with_around/MyBean2.kt new file mode 100644 index 00000000000..df4d5701ca2 --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/with_around/MyBean2.kt @@ -0,0 +1,19 @@ +package io.micronaut.kotlin.processing.aop.introduction.with_around + +@ProxyIntroductionAndAround +open class MyBean2 { + private var id: Long? = null + private var name: String? = null + + open fun getId(): Long? = id + + open fun setId(id: Long?) { + this.id = id + } + + open fun getName(): String? = name + + open fun setId(name: String?) { + this.name = name + } +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/with_around/MyBean3.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/with_around/MyBean3.kt new file mode 100644 index 00000000000..389b53c90ee --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/with_around/MyBean3.kt @@ -0,0 +1,19 @@ +package io.micronaut.kotlin.processing.aop.introduction.with_around + +@ProxyIntroductionAndAroundOneAnnotation +open class MyBean3 { + private var id: Long? = null + private var name: String? = null + + open fun getId(): Long? = id + + open fun setId(id: Long?) { + this.id = id + } + + open fun getName(): String? = name + + open fun setId(name: String?) { + this.name = name + } +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/with_around/MyBean4.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/with_around/MyBean4.kt new file mode 100644 index 00000000000..ccbdb2a0685 --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/with_around/MyBean4.kt @@ -0,0 +1,22 @@ +package io.micronaut.kotlin.processing.aop.introduction.with_around + +import io.micronaut.core.annotation.Introspected + +@ProxyIntroductionAndAroundOneAnnotation +@Introspected +open class MyBean4 { + private var id: Long? = null + private var name: String? = null + + open fun getId(): Long? = id + + open fun setId(id: Long?) { + this.id = id + } + + open fun getName(): String? = name + + open fun setId(name: String?) { + this.name = name + } +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/with_around/MyBean5.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/with_around/MyBean5.kt new file mode 100644 index 00000000000..8c1e16936dc --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/with_around/MyBean5.kt @@ -0,0 +1,19 @@ +package io.micronaut.kotlin.processing.aop.introduction.with_around + +@ProxyIntroductionAndAroundAndIntrospected +open class MyBean5 { + private var id: Long? = null + private var name: String? = null + + open fun getId(): Long? = id + + open fun setId(id: Long?) { + this.id = id + } + + open fun getName(): String? = name + + open fun setId(name: String?) { + this.name = name + } +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/with_around/MyBean6.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/with_around/MyBean6.kt new file mode 100644 index 00000000000..a001afa9ff1 --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/with_around/MyBean6.kt @@ -0,0 +1,19 @@ +package io.micronaut.kotlin.processing.aop.introduction.with_around + +@ProxyIntroductionAndAroundAndIntrospectedAndExecutable +open class MyBean6 { + private var id: Long? = null + private var name: String? = null + + open fun getId(): Long? = id + + open fun setId(id: Long?) { + this.id = id + } + + open fun getName(): String? = name + + open fun setId(name: String?) { + this.name = name + } +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/with_around/MyBean7.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/with_around/MyBean7.kt new file mode 100644 index 00000000000..bf67ba9c8f9 --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/with_around/MyBean7.kt @@ -0,0 +1,22 @@ +package io.micronaut.kotlin.processing.aop.introduction.with_around + +import io.micronaut.context.annotation.Executable + +@Executable +@ProxyIntroduction +open class MyBean7 { + private var id: Long? = null + private var name: String? = null + + open fun getId(): Long? = id + + open fun setId(id: Long?) { + this.id = id + } + + open fun getName(): String? = name + + open fun setId(name: String?) { + this.name = name + } +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/with_around/MyBean8.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/with_around/MyBean8.kt new file mode 100644 index 00000000000..ab565fbaeb4 --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/with_around/MyBean8.kt @@ -0,0 +1,22 @@ +package io.micronaut.kotlin.processing.aop.introduction.with_around + +import io.micronaut.context.annotation.Executable + +@Executable +@ProxyAround +open class MyBean8 { + private var id: Long? = null + private var name: String? = null + + open fun getId(): Long? = id + + open fun setId(id: Long?) { + this.id = id + } + + open fun getName(): String? = name + + open fun setId(name: String?) { + this.name = name + } +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/with_around/MyBean9.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/with_around/MyBean9.kt new file mode 100644 index 00000000000..c6baba97827 --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/with_around/MyBean9.kt @@ -0,0 +1,23 @@ +package io.micronaut.kotlin.processing.aop.introduction.with_around + +@ProxyIntroductionAndAroundAndIntrospectedAndExecutable +open class MyBean9 { + private var multidim: Array>? = null + private var primitiveMultidim: Array? = null + + open fun getMultidim(): Array>? { + return multidim + } + + open fun setMultidim(multidim: Array>?) { + this.multidim = multidim + } + + open fun getPrimitiveMultidim(): Array? { + return primitiveMultidim + } + + open fun setPrimitiveMultidim(primitiveMultidim: Array?) { + this.primitiveMultidim = primitiveMultidim + } +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/with_around/ObservableInterceptor.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/with_around/ObservableInterceptor.kt new file mode 100644 index 00000000000..de4228fc145 --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/with_around/ObservableInterceptor.kt @@ -0,0 +1,13 @@ +package io.micronaut.kotlin.processing.aop.introduction.with_around + +import io.micronaut.aop.MethodInterceptor +import io.micronaut.aop.MethodInvocationContext +import jakarta.inject.Singleton + +@Singleton +class ObservableInterceptor : MethodInterceptor { + + override fun intercept(context: MethodInvocationContext?): Any { + return "World" + } +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/with_around/ProxyAdviceInterceptor.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/with_around/ProxyAdviceInterceptor.kt new file mode 100644 index 00000000000..acc881927de --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/with_around/ProxyAdviceInterceptor.kt @@ -0,0 +1,55 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.kotlin.processing.aop.introduction.with_around + +import io.micronaut.aop.MethodInterceptor +import io.micronaut.aop.MethodInvocationContext +import io.micronaut.context.BeanContext +import io.micronaut.core.annotation.Nullable +import jakarta.inject.Singleton +import java.lang.RuntimeException + +@Singleton +class ProxyAdviceInterceptor(private val beanContext: BeanContext) : MethodInterceptor { + + @Nullable + override fun intercept(context: MethodInvocationContext): Any? { + if (context.methodName.equals("getId", ignoreCase = true)) { + // Test invocation delegation + return if (context.target is MyBean5) { + val delegate = MyBean5() + delegate.setId(1L) + context.executableMethod.invoke(delegate, *context.parameterValues) + } else if (context.target is MyBean6) { + try { + val proxyTargetMethod = beanContext.getProxyTargetMethod( + MyBean6::class.java, context.methodName, *context.argumentTypes + ) + val delegate = MyBean6() + delegate.setId(1L) + proxyTargetMethod.invoke(delegate, *context.parameterValues) + } catch (e: NoSuchMethodException) { + throw RuntimeException(e) + } + } else { + 1L + } + } + return if (context.methodName.equals("isProxy", ignoreCase = true)) { + true + } else context.proceed() + } +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/with_around/ProxyAround.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/with_around/ProxyAround.kt new file mode 100644 index 00000000000..42be6f1ab36 --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/with_around/ProxyAround.kt @@ -0,0 +1,11 @@ +package io.micronaut.kotlin.processing.aop.introduction.with_around + +import io.micronaut.aop.Around +import io.micronaut.context.annotation.Type + +@Around +@Type(ProxyAroundInterceptor::class) +@MustBeDocumented +@Retention +@Target(AnnotationTarget.ANNOTATION_CLASS, AnnotationTarget.CLASS) +annotation class ProxyAround diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/with_around/ProxyAroundInterceptor.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/with_around/ProxyAroundInterceptor.kt new file mode 100644 index 00000000000..d0b9c840639 --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/with_around/ProxyAroundInterceptor.kt @@ -0,0 +1,16 @@ +package io.micronaut.kotlin.processing.aop.introduction.with_around + +import io.micronaut.aop.MethodInterceptor +import io.micronaut.aop.MethodInvocationContext +import jakarta.inject.Singleton + +@Singleton +class ProxyAroundInterceptor : MethodInterceptor { + + override fun intercept(context: MethodInvocationContext): Any? { + // Intercept everything other when CustomProxy + return if (context.methodName == "getId") { + 1L + } else context.proceed() + } +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/with_around/ProxyIntroduction.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/with_around/ProxyIntroduction.kt new file mode 100644 index 00000000000..f862bf31e25 --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/with_around/ProxyIntroduction.kt @@ -0,0 +1,25 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.kotlin.processing.aop.introduction.with_around + +import io.micronaut.aop.Introduction +import io.micronaut.context.annotation.Type + +@Introduction(interfaces = [CustomProxy::class]) +@Type(ProxyIntroductionInterceptor::class) +@MustBeDocumented +@Target(AnnotationTarget.ANNOTATION_CLASS, AnnotationTarget.CLASS) +annotation class ProxyIntroduction diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/with_around/ProxyIntroductionAndAround.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/with_around/ProxyIntroductionAndAround.kt new file mode 100644 index 00000000000..92d68e676d8 --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/with_around/ProxyIntroductionAndAround.kt @@ -0,0 +1,7 @@ +package io.micronaut.kotlin.processing.aop.introduction.with_around + +@MustBeDocumented +@Retention +@ProxyIntroduction +@ProxyAround +annotation class ProxyIntroductionAndAround diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/with_around/ProxyIntroductionAndAroundAndIntrospected.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/with_around/ProxyIntroductionAndAroundAndIntrospected.kt new file mode 100644 index 00000000000..f87c4a05eeb --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/with_around/ProxyIntroductionAndAroundAndIntrospected.kt @@ -0,0 +1,15 @@ +package io.micronaut.kotlin.processing.aop.introduction.with_around + +import io.micronaut.aop.Around +import io.micronaut.aop.Introduction +import io.micronaut.context.annotation.Type +import io.micronaut.core.annotation.Introspected + +@Around +@Introduction(interfaces = [CustomProxy::class]) +@Type(ProxyAdviceInterceptor::class) +@MustBeDocumented +@Retention +@Target(AnnotationTarget.ANNOTATION_CLASS, AnnotationTarget.CLASS, AnnotationTarget.ANNOTATION_CLASS) +@Introspected +annotation class ProxyIntroductionAndAroundAndIntrospected diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/with_around/ProxyIntroductionAndAroundAndIntrospectedAndExecutable.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/with_around/ProxyIntroductionAndAroundAndIntrospectedAndExecutable.kt new file mode 100644 index 00000000000..47b695e94a6 --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/with_around/ProxyIntroductionAndAroundAndIntrospectedAndExecutable.kt @@ -0,0 +1,17 @@ +package io.micronaut.kotlin.processing.aop.introduction.with_around + +import io.micronaut.aop.Around +import io.micronaut.aop.Introduction +import io.micronaut.context.annotation.Executable +import io.micronaut.context.annotation.Type +import io.micronaut.core.annotation.Introspected + +@Around(proxyTarget = true) +@Introduction(interfaces = [CustomProxy::class]) +@Type(ProxyAdviceInterceptor::class) +@MustBeDocumented +@Retention +@Target(AnnotationTarget.ANNOTATION_CLASS, AnnotationTarget.CLASS, AnnotationTarget.ANNOTATION_CLASS) +@Introspected +@Executable +annotation class ProxyIntroductionAndAroundAndIntrospectedAndExecutable diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/with_around/ProxyIntroductionAndAroundOneAnnotation.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/with_around/ProxyIntroductionAndAroundOneAnnotation.kt new file mode 100644 index 00000000000..85ddd527f3d --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/with_around/ProxyIntroductionAndAroundOneAnnotation.kt @@ -0,0 +1,13 @@ +package io.micronaut.kotlin.processing.aop.introduction.with_around + +import io.micronaut.aop.Around +import io.micronaut.aop.Introduction +import io.micronaut.context.annotation.Type + +@Around +@Introduction(interfaces = [CustomProxy::class]) +@Type(ProxyAdviceInterceptor::class) +@MustBeDocumented +@Retention +@Target(AnnotationTarget.ANNOTATION_CLASS, AnnotationTarget.CLASS) +annotation class ProxyIntroductionAndAroundOneAnnotation diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/with_around/ProxyIntroductionInterceptor.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/with_around/ProxyIntroductionInterceptor.kt new file mode 100644 index 00000000000..8c08cab7b55 --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/with_around/ProxyIntroductionInterceptor.kt @@ -0,0 +1,25 @@ +package io.micronaut.kotlin.processing.aop.introduction.with_around + +import io.micronaut.aop.MethodInterceptor +import io.micronaut.aop.MethodInvocationContext +import io.micronaut.core.annotation.Nullable +import jakarta.inject.Singleton + +@Singleton +class ProxyIntroductionInterceptor : MethodInterceptor { + + @Nullable + override fun intercept(context: MethodInvocationContext): Any? { + // Only intercept CustomProxy + if (context.methodName.equals("isProxy", ignoreCase = true)) { + // test introduced interface delegation + val customProxy = object : CustomProxy { + override fun isProxy(): Boolean { + return true + } + } + return context.executableMethod.invoke(customProxy, *context.parameterValues) + } + return context.proceed() + } +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/itfce/AbstractInterfaceImpl.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/itfce/AbstractInterfaceImpl.kt new file mode 100644 index 00000000000..5d3e913144c --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/itfce/AbstractInterfaceImpl.kt @@ -0,0 +1,12 @@ +package io.micronaut.kotlin.processing.aop.itfce + +abstract class AbstractInterfaceImpl : InterfaceClass { + + override fun test(name: String): String { + return "Name is $name" + } + + override fun test(name: String, age: Int): String { + return "Name is $name and age is $age" + } +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/itfce/AbstractInterfaceTypeLevel.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/itfce/AbstractInterfaceTypeLevel.kt new file mode 100644 index 00000000000..c4ae2f9c7a0 --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/itfce/AbstractInterfaceTypeLevel.kt @@ -0,0 +1,12 @@ +package io.micronaut.kotlin.processing.aop.itfce + +abstract class AbstractInterfaceTypeLevel : InterfaceTypeLevel { + + override fun test(name: String): String { + return "Name is $name" + } + + override fun test(name: String, age: Int): String { + return "Name is $name and age is $age" + } +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/itfce/InterfaceClass.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/itfce/InterfaceClass.kt new file mode 100644 index 00000000000..edb981f3280 --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/itfce/InterfaceClass.kt @@ -0,0 +1,88 @@ +package io.micronaut.kotlin.processing.aop.itfce + +import io.micronaut.kotlin.processing.aop.simple.CovariantClass +import io.micronaut.kotlin.processing.aop.simple.Mutating + +interface InterfaceClass { + + @Mutating("name") + fun test(name: String): String + + @Mutating("age") + fun test(age: Int): String + + @Mutating("name") + fun test(name: String, age: Int): String + + @Mutating("name") + fun test(): String + + @Mutating("name") + fun testVoid(name: String) + + @Mutating("name") + fun testVoid(name: String, age: Int) + + @Mutating("name") + fun testBoolean(name: String): Boolean + + @Mutating("name") + fun testBoolean(name: String, age: Int): Boolean + + @Mutating("name") + fun testInt(name: String): Int + + @Mutating("age") + fun testInt(name: String, age: Int): Int + + @Mutating("name") + fun testLong(name: String): Long + + @Mutating("age") + fun testLong(name: String, age: Int): Long + + @Mutating("name") + fun testShort(name: String): Short + + @Mutating("age") + fun testShort(name: String, age: Int): Short + + @Mutating("name") + fun testByte(name: String): Byte + + @Mutating("age") + fun testByte(name: String, age: Int): Byte + + @Mutating("name") + fun testDouble(name: String): Double + + @Mutating("age") + fun testDouble(name: String, age: Int): Double + + @Mutating("name") + fun testFloat(name: String): Float + + @Mutating("age") + fun testFloat(name: String, age: Int): Float + + @Mutating("name") + fun testChar(name: String): Char + + @Mutating("age") + fun testChar(name: String, age: Int): Char + + @Mutating("name") + fun testByteArray(name: String, data: ByteArray): ByteArray + + @Mutating("name") + fun testGenericsWithExtends(name: T, age: Int): T + + @Mutating("name") + fun testListWithWildCardIn(name: T, p2: CovariantClass): CovariantClass + + @Mutating("name") + fun testListWithWildCardOut(name: T, p2: CovariantClass): CovariantClass + + @Mutating("name") + fun testGenericsFromType(name: A, age: Int): A +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/itfce/InterfaceImpl.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/itfce/InterfaceImpl.kt new file mode 100644 index 00000000000..0455f099a24 --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/itfce/InterfaceImpl.kt @@ -0,0 +1,122 @@ +package io.micronaut.kotlin.processing.aop.itfce + +import io.micronaut.kotlin.processing.aop.simple.CovariantClass +import jakarta.inject.Singleton + +@Singleton +open class InterfaceImpl : AbstractInterfaceImpl() { + + override fun test(name: String): String { + return "Name is $name" + } + + override fun test(age: Int): String { + return "Age is $age" + } + + override fun test(): String { + return "noargs" + } + + override fun testVoid(name: String) { + assert(name == "changed") + } + + override fun testVoid(name: String, age: Int) { + assert(name == "changed") + assert(age == 10) + } + + override fun testBoolean(name: String): Boolean { + return name == "changed" + } + + override fun testBoolean(name: String, age: Int): Boolean { + assert(age == 10) + return name == "changed" + } + + override fun testInt(name: String): Int { + return if (name == "changed") 1 else 0 + } + + override fun testInt(name: String, age: Int): Int { + assert(name == "test") + return age + } + + override fun testLong(name: String): Long { + return if (name == "changed") 1 else 0 + } + + override fun testLong(name: String, age: Int): Long { + assert(name == "test") + return age.toLong() + } + + override fun testShort(name: String): Short { + return (if (name == "changed") 1 else 0).toShort() + } + + override fun testShort(name: String, age: Int): Short { + assert(name == "test") + return age.toShort() + } + + override fun testByte(name: String): Byte { + return (if (name == "changed") 1 else 0).toByte() + } + + override fun testByte(name: String, age: Int): Byte { + assert(name == "test") + return age.toByte() + } + + override fun testDouble(name: String): Double { + return if (name == "changed") 1.0 else 0.0 + } + + override fun testDouble(name: String, age: Int): Double { + assert(name == "test") + return age.toDouble() + } + + override fun testFloat(name: String): Float { + return if (name == "changed") 1F else 0F + } + + override fun testFloat(name: String, age: Int): Float { + assert(name == "test") + return age.toFloat() + } + + override fun testChar(name: String): Char { + return (if (name == "changed") 1 else 0).toChar() + } + + override fun testChar(name: String, age: Int): Char { + assert(name == "test") + return age.toChar() + } + + override fun testByteArray(name: String, data: ByteArray): ByteArray { + assert(name == "changed") + return data + } + + override fun testGenericsWithExtends(name: T, age: Int): T { + return "Name is $name" as T + } + + override fun testListWithWildCardIn(name: T, p2: CovariantClass): CovariantClass { + return CovariantClass(name.toString()) + } + + override fun testListWithWildCardOut(name: T, p2: CovariantClass): CovariantClass { + return CovariantClass(name.toString()) + } + + override fun testGenericsFromType(name: A, age: Int): A { + return "Name is $name" as A + } +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/itfce/InterfaceTypeLevel.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/itfce/InterfaceTypeLevel.kt new file mode 100644 index 00000000000..760d8757e08 --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/itfce/InterfaceTypeLevel.kt @@ -0,0 +1,27 @@ +package io.micronaut.kotlin.processing.aop.itfce + +import io.micronaut.kotlin.processing.aop.simple.Mutating +import io.micronaut.kotlin.processing.aop.simple.CovariantClass + +@Mutating("name") +interface InterfaceTypeLevel { + fun test(name: String): String + fun test(name: String, age: Int): String + fun test(): String + fun testVoid(name: String) + fun testVoid(name: String, age: Int) + fun testBoolean(name: String): Boolean + fun testBoolean(name: String, age: Int): Boolean + fun testInt(name: String): Int + fun testLong(name: String): Long + fun testShort(name: String): Short + fun testByte(name: String): Byte + fun testDouble(name: String): Double + fun testFloat(name: String): Float + fun testChar(name: String): Char + fun testByteArray(name: String, data: ByteArray): ByteArray + fun testGenericsWithExtends(name: T, age: Int): T + fun testListWithWildCardIn(name: T, p2: CovariantClass): CovariantClass + fun testListWithWildCardOut(name: T, p2: CovariantClass): CovariantClass + fun testGenericsFromType(name: A, age: Int): A +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/itfce/InterfaceTypeLevelImpl.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/itfce/InterfaceTypeLevelImpl.kt new file mode 100644 index 00000000000..ca7172d6f93 --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/itfce/InterfaceTypeLevelImpl.kt @@ -0,0 +1,98 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.kotlin.processing.aop.itfce + +import io.micronaut.kotlin.processing.aop.simple.CovariantClass +import jakarta.inject.Singleton + +/** + * @author Graeme Rocher + * @since 1.0 + */ +@Singleton +open class InterfaceTypeLevelImpl : AbstractInterfaceTypeLevel() { + + override fun test(): String { + return "noargs" + } + + override fun testVoid(name: String) { + assert(name == "changed") + } + + override fun testVoid(name: String, age: Int) { + assert(name == "changed") + assert(age == 10) + } + + override fun testBoolean(name: String): Boolean { + return name == "changed" + } + + override fun testBoolean(name: String, age: Int): Boolean { + assert(age == 10) + return name == "changed" + } + + override fun testInt(name: String): Int { + return if (name == "changed") 1 else 0 + } + + override fun testLong(name: String): Long { + return if (name == "changed") 1 else 0 + } + + override fun testShort(name: String): Short { + return (if (name == "changed") 1 else 0).toShort() + } + + override fun testByte(name: String): Byte { + return (if (name == "changed") 1 else 0).toByte() + } + + override fun testDouble(name: String): Double { + return if (name == "changed") 1.0 else 0.0 + } + + override fun testFloat(name: String): Float { + return if (name == "changed") 1F else 0F + } + + override fun testChar(name: String): Char { + return (if (name == "changed") 1 else 0).toChar() + } + + override fun testByteArray(name: String, data: ByteArray): ByteArray { + assert(name == "changed") + return data + } + + override fun testGenericsWithExtends(name: T, age: Int): T { + return "Name is $name" as T + } + + override fun testListWithWildCardIn(name: T, p2: CovariantClass): CovariantClass { + return CovariantClass(name.toString()) + } + + override fun testListWithWildCardOut(name: T, p2: CovariantClass): CovariantClass { + return CovariantClass(name.toString()) + } + + override fun testGenericsFromType(name: Any, age: Int): Any { + return "Name is $name" + } +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/named/Config.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/named/Config.kt new file mode 100644 index 00000000000..93c4eb59dfb --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/named/Config.kt @@ -0,0 +1,10 @@ +package io.micronaut.kotlin.processing.aop.named + +import io.micronaut.context.annotation.ConfigurationProperties + +@ConfigurationProperties("config") +class Config(inner: Inner) { + + @ConfigurationProperties("inner") + class Inner +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/named/NamedFactory.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/named/NamedFactory.kt new file mode 100644 index 00000000000..cd957fb543e --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/named/NamedFactory.kt @@ -0,0 +1,54 @@ +package io.micronaut.kotlin.processing.aop.named + +import io.micronaut.context.annotation.EachProperty +import io.micronaut.context.annotation.Factory +import io.micronaut.context.annotation.Parameter +import io.micronaut.kotlin.processing.aop.Logged +import io.micronaut.runtime.context.scope.Refreshable +import jakarta.inject.Named +import jakarta.inject.Singleton + +@Factory +class NamedFactory { + + @EachProperty(value = "aop.test.named", primary = "default") + @Refreshable + fun namedInterface(@Parameter name: String): NamedInterface { + return object : NamedInterface { + override fun doStuff(): String { + return name + } + } + } + + @Named("first") + @Logged + @Singleton + fun first(): OtherInterface { + return object : OtherInterface { + override fun doStuff(): String { + return "first" + } + } + } + + @Named("second") + @Logged + @Singleton + fun second(): OtherInterface { + return object : OtherInterface { + override fun doStuff(): String { + return "second" + } + } + } + + @EachProperty("other.interfaces") + fun third(config: Config, @Parameter name: String): OtherInterface { + return object : OtherInterface { + override fun doStuff(): String { + return name + } + } + } +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/named/NamedInterface.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/named/NamedInterface.kt new file mode 100644 index 00000000000..2f9e888e91f --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/named/NamedInterface.kt @@ -0,0 +1,5 @@ +package io.micronaut.kotlin.processing.aop.named + +interface NamedInterface { + fun doStuff(): String +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/named/OtherBean.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/named/OtherBean.kt new file mode 100644 index 00000000000..d0c4d2ccc19 --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/named/OtherBean.kt @@ -0,0 +1,17 @@ +package io.micronaut.kotlin.processing.aop.named + +import jakarta.inject.Inject +import jakarta.inject.Named +import jakarta.inject.Singleton + +@Singleton +class OtherBean { + + @Inject + @Named("first") + lateinit var first: OtherInterface + + @Inject + @Named("second") + lateinit var second: OtherInterface +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/named/OtherInterface.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/named/OtherInterface.kt new file mode 100644 index 00000000000..e91971d1ffe --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/named/OtherInterface.kt @@ -0,0 +1,5 @@ +package io.micronaut.kotlin.processing.aop.named + +interface OtherInterface { + fun doStuff(): String +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/proxytarget/ArgMutatingInterceptor.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/proxytarget/ArgMutatingInterceptor.kt new file mode 100644 index 00000000000..a1ee028d99c --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/proxytarget/ArgMutatingInterceptor.kt @@ -0,0 +1,26 @@ +package io.micronaut.kotlin.processing.aop.proxytarget + +import io.micronaut.aop.Interceptor +import io.micronaut.aop.InvocationContext +import io.micronaut.core.type.MutableArgumentValue +import jakarta.inject.Singleton + +@Singleton +class ArgMutatingInterceptor : Interceptor { + + override fun intercept(context: InvocationContext): Any? { + val m = context.synthesize( + Mutating::class.java + ) + val arg = context.parameters[m.value] as MutableArgumentValue? + if (arg != null) { + val value = arg.value + if (value is Number) { + arg.setValue((value.toInt() * 2)) + } else { + arg.setValue("changed") + } + } + return context.proceed() + } +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/proxytarget/Mutating.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/proxytarget/Mutating.kt new file mode 100644 index 00000000000..685a259130b --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/proxytarget/Mutating.kt @@ -0,0 +1,13 @@ +package io.micronaut.kotlin.processing.aop.proxytarget + +import io.micronaut.aop.Around +import io.micronaut.context.annotation.Type + +@Around(proxyTarget = true) +@Type(ArgMutatingInterceptor::class) +@MustBeDocumented +@Retention +@Target(AnnotationTarget.FUNCTION, AnnotationTarget.CLASS) +annotation class Mutating(val value: String) + + diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/proxytarget/ProxyingClass.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/proxytarget/ProxyingClass.kt new file mode 100644 index 00000000000..b297355e51b --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/proxytarget/ProxyingClass.kt @@ -0,0 +1,164 @@ +package io.micronaut.kotlin.processing.aop.proxytarget + +import io.micronaut.kotlin.processing.aop.simple.Bar +import io.micronaut.kotlin.processing.aop.simple.CovariantClass +import jakarta.annotation.PostConstruct +import jakarta.inject.Singleton + +@Singleton +open class ProxyingClass(private val bar: Bar?) { + + var lifeCycleCount = 0 + var invocationCount = 0 + + @PostConstruct + fun init() { + lifeCycleCount++ + } + + @Mutating("name") + open fun test(name: String): String { + invocationCount++ + return "Name is $name" + } + + @Mutating("age") + open fun test(age: Int): String { + return "Age is $age" + } + + @Mutating("name") + open fun test(name: String, age: Int): String { + return "Name is $name and age is $age" + } + + @Mutating("name") + open fun test(): String { + return "noargs" + } + + @Mutating("name") + open fun testVoid(name: String) { + assert(name == "changed") + } + + @Mutating("name") + open fun testVoid(name: String, age: Int) { + assert(name == "changed") + assert(age == 10) + } + + @Mutating("name") + open fun testBoolean(name: String): Boolean { + return name == "changed" + } + + @Mutating("name") + open fun testBoolean(name: String, age: Int): Boolean { + assert(age == 10) + return name == "changed" + } + + @Mutating("name") + open fun testInt(name: String): Int { + return if (name == "changed") 1 else 0 + } + + @Mutating("age") + open fun testInt(name: String, age: Int): Int { + assert(name == "test") + return age + } + + @Mutating("name") + open fun testLong(name: String): Long { + return if (name == "changed") 1 else 0 + } + + @Mutating("age") + open fun testLong(name: String, age: Int): Long { + assert(name == "test") + return age.toLong() + } + + @Mutating("name") + open fun testShort(name: String): Short { + return (if (name == "changed") 1 else 0).toShort() + } + + @Mutating("age") + open fun testShort(name: String, age: Int): Short { + assert(name == "test") + return age.toShort() + } + + @Mutating("name") + open fun testByte(name: String): Byte { + return (if (name == "changed") 1 else 0).toByte() + } + + @Mutating("age") + open fun testByte(name: String, age: Int): Byte { + assert(name == "test") + return age.toByte() + } + + @Mutating("name") + open fun testDouble(name: String): Double { + return if (name == "changed") 1.0 else 0.0 + } + + @Mutating("age") + open fun testDouble(name: String, age: Int): Double { + assert(name == "test") + return age.toDouble() + } + + @Mutating("name") + open fun testFloat(name: String): Float { + return if (name == "changed") 1F else 0F + } + + @Mutating("age") + open fun testFloat(name: String, age: Int): Float { + assert(name == "test") + return age.toFloat() + } + + @Mutating("name") + open fun testChar(name: String): Char { + return (if (name == "changed") 1 else 0).toChar() + } + + @Mutating("age") + open fun testChar(name: String, age: Int): Char { + assert(name == "test") + return age.toChar() + } + + @Mutating("name") + open fun testByteArray(name: String, data: ByteArray): ByteArray { + assert(name == "changed") + return data + } + + @Mutating("name") + open fun testGenericsWithExtends(name: T, age: Int): T { + return "Name is $name" as T + } + + @Mutating("name") + open fun testListWithWildCardIn(name: T, p2: CovariantClass): CovariantClass { + return CovariantClass(name.toString()) + } + + @Mutating("name") + open fun testListWithWildCardOut(name: T, p2: CovariantClass): CovariantClass { + return CovariantClass(name.toString()) + } + + @Mutating("name") + open fun testGenericsFromType(name: A, age: Int): A { + return "Name is $name" as A + } +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/simple/AnotherClass.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/simple/AnotherClass.kt new file mode 100644 index 00000000000..02b834cbab9 --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/simple/AnotherClass.kt @@ -0,0 +1,160 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.kotlin.processing.aop.simple + +/** + * @author Graeme Rocher + * @since 1.0 + */ +@Mutating("name") +open class AnotherClass { + + // protected methods not proxied + protected fun testProtected(name: String): String { + return "Name is $name" + } + + // protected methods not proxied + private fun testPrivate(name: String): String { + return "Name is $name" + } + + open fun test(name: String): String { + return "Name is $name" + } + + @Mutating("age") + open fun test(age: Int): String { + return "Age is $age" + } + + open fun test(name: String, age: Int): String { + return "Name is $name and age is $age" + } + + open fun test(): String { + return "noargs" + } + + open fun testVoid(name: String) { + assert(name == "changed") + } + + open fun testVoid(name: String, age: Int) { + assert(name == "changed") + assert(age == 10) + } + + open fun testBoolean(name: String): Boolean { + return name == "changed" + } + + open fun testBoolean(name: String, age: Int): Boolean { + assert(age == 10) + return name == "changed" + } + + open fun testInt(name: String): Int { + return if (name == "changed") 1 else 0 + } + + @Mutating("age") + open fun testInt(name: String, age: Int): Int { + assert(name == "test") + return age + } + + open fun testLong(name: String): Long { + return if (name == "changed") 1 else 0 + } + + @Mutating("age") + open fun testLong(name: String, age: Int): Long { + assert(name == "test") + return age.toLong() + } + + open fun testShort(name: String): Short { + return (if (name == "changed") 1 else 0).toShort() + } + + @Mutating("age") + open fun testShort(name: String, age: Int): Short { + assert(name == "test") + return age.toShort() + } + + open fun testByte(name: String): Byte { + return (if (name == "changed") 1 else 0).toByte() + } + + @Mutating("age") + open fun testByte(name: String, age: Int): Byte { + assert(name == "test") + return age.toByte() + } + + open fun testDouble(name: String): Double { + return if (name == "changed") 1.0 else 0.0 + } + + @Mutating("age") + open fun testDouble(name: String, age: Int): Double { + assert(name == "test") + return age.toDouble() + } + + open fun testFloat(name: String): Float { + return if (name == "changed") 1F else 0F + } + + @Mutating("age") + open fun testFloat(name: String, age: Int): Float { + assert(name == "test") + return age.toFloat() + } + + open fun testChar(name: String): Char { + return (if (name == "changed") 1 else 0).toChar() + } + + @Mutating("age") + open fun testChar(name: String, age: Int): Char { + assert(name == "test") + return age.toChar() + } + + open fun testByteArray(name: String, data: ByteArray): ByteArray { + assert(name == "changed") + return data + } + + open fun testGenericsWithExtends(name: T, age: Int): T { + return "Name is $name" as T + } + + open fun testListWithWildCardIn(name: T, p2: CovariantClass): CovariantClass { + return CovariantClass(name.toString()) + } + + open fun testListWithWildCardOut(name: T, p2: CovariantClass): CovariantClass { + return CovariantClass(name.toString()) + } + + open fun testGenericsFromType(name: A, age: Int): A { + return "Name is $name" as A + } +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/simple/ArgMutatingInterceptor.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/simple/ArgMutatingInterceptor.kt new file mode 100644 index 00000000000..4ef395b0bec --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/simple/ArgMutatingInterceptor.kt @@ -0,0 +1,26 @@ +package io.micronaut.kotlin.processing.aop.simple + +import io.micronaut.aop.Interceptor +import io.micronaut.aop.InvocationContext +import io.micronaut.core.type.MutableArgumentValue +import jakarta.inject.Singleton + +@Singleton +class ArgMutatingInterceptor : Interceptor { + + override fun intercept(context: InvocationContext): Any? { + val m = context.synthesize( + Mutating::class.java + ) + val arg = context.parameters[m.value] as MutableArgumentValue? + if (arg != null) { + val value = arg.value + if (value is Number) { + arg.setValue(value.toInt() * 2) + } else { + arg.setValue("changed") + } + } + return context.proceed() + } +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/simple/Bar.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/simple/Bar.kt new file mode 100644 index 00000000000..6098d18455b --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/simple/Bar.kt @@ -0,0 +1,6 @@ +package io.micronaut.kotlin.processing.aop.simple + +import jakarta.inject.Singleton + +@Singleton +class Bar diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/simple/CovariantClass.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/simple/CovariantClass.kt new file mode 100644 index 00000000000..2ff2d7234c8 --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/simple/CovariantClass.kt @@ -0,0 +1,4 @@ +package io.micronaut.kotlin.processing.aop.simple + +data class CovariantClass(private val value: T) { +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/simple/Invalid.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/simple/Invalid.kt new file mode 100644 index 00000000000..7cee90a100b --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/simple/Invalid.kt @@ -0,0 +1,14 @@ +package io.micronaut.kotlin.processing.aop.simple + +import io.micronaut.aop.Around +import io.micronaut.context.annotation.Type + +@Around +@Type(InvalidInterceptor::class) +@MustBeDocumented +@Retention +@Target( + AnnotationTarget.FUNCTION, + AnnotationTarget.CLASS +) +annotation class Invalid diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/simple/InvalidInterceptor.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/simple/InvalidInterceptor.kt new file mode 100644 index 00000000000..5624236144b --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/simple/InvalidInterceptor.kt @@ -0,0 +1,19 @@ +package io.micronaut.kotlin.processing.aop.simple + +import io.micronaut.aop.Interceptor +import io.micronaut.aop.InvocationContext +import io.micronaut.core.type.Argument +import io.micronaut.core.type.MutableArgumentValue +import jakarta.inject.Singleton + +@Singleton +class InvalidInterceptor : Interceptor { + + override fun intercept(context: InvocationContext): Any? { + context.parameters["test"] = MutableArgumentValue.create( + Argument.STRING, + "value" + ) + return context.proceed() + } +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/simple/Mutating.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/simple/Mutating.kt new file mode 100644 index 00000000000..ae2937a8902 --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/simple/Mutating.kt @@ -0,0 +1,20 @@ +package io.micronaut.kotlin.processing.aop.simple + +import io.micronaut.aop.Around +import io.micronaut.context.annotation.Type +import java.lang.annotation.Inherited + +@Around +@Type(ArgMutatingInterceptor::class) +@MustBeDocumented +@Retention +@Target( + AnnotationTarget.FUNCTION, + AnnotationTarget.PROPERTY_GETTER, + AnnotationTarget.PROPERTY_SETTER, + AnnotationTarget.ANNOTATION_CLASS, + AnnotationTarget.CLASS, + AnnotationTarget.FIELD +) +@Inherited +annotation class Mutating(val value: String) diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/simple/SimpleClass.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/simple/SimpleClass.kt new file mode 100644 index 00000000000..569c8e3a30c --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/simple/SimpleClass.kt @@ -0,0 +1,187 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.kotlin.processing.aop.simple + +import jakarta.annotation.PostConstruct +import jakarta.inject.Singleton + +/** + * @author Graeme Rocher + * @since 1.0 + */ +@Singleton +open class SimpleClass(private val bar: Bar?) { + var isPostConstructInvoked = false + private set + + init { + assert(bar != null) + } + + @PostConstruct + fun onCreate() { + isPostConstructInvoked = true + } + + @Mutating("name") + open fun test(name: String): String { + return "Name is $name" + } + + @Mutating("age") + open fun test(age: Int): String { + return "Age is $age" + } + + @Mutating("name") + open fun test(name: String, age: Int): String { + return "Name is $name and age is $age" + } + + @Mutating("name") + open fun test(): String { + return "noargs" + } + + @Mutating("name") + open fun testVoid(name: String) { + assert(name == "changed") + } + + @Mutating("name") + open fun testVoid(name: String, age: Int) { + assert(name == "changed") + assert(age == 10) + } + + @Mutating("name") + open fun testBoolean(name: String): Boolean { + return name == "changed" + } + + @Mutating("name") + open fun testBoolean(name: String, age: Int): Boolean { + assert(age == 10) + return name == "changed" + } + + @Mutating("name") + open fun testInt(name: String): Int { + return if (name == "changed") 1 else 0 + } + + @Mutating("age") + open fun testInt(name: String, age: Int): Int { + assert(name == "test") + return age + } + + @Mutating("name") + open fun testLong(name: String): Long { + return if (name == "changed") 1 else 0 + } + + @Mutating("age") + open fun testLong(name: String, age: Int): Long { + assert(name == "test") + return age.toLong() + } + + @Mutating("name") + open fun testShort(name: String): Short { + return (if (name == "changed") 1 else 0).toShort() + } + + @Mutating("age") + open fun testShort(name: String, age: Int): Short { + assert(name == "test") + return age.toShort() + } + + @Mutating("name") + open fun testByte(name: String): Byte { + return (if (name == "changed") 1 else 0).toByte() + } + + @Mutating("age") + open fun testByte(name: String, age: Int): Byte { + assert(name == "test") + return age.toByte() + } + + @Mutating("name") + open fun testDouble(name: String): Double { + return if (name == "changed") 1.0 else 0.0 + } + + @Mutating("age") + open fun testDouble(name: String, age: Int): Double { + assert(name == "test") + return age.toDouble() + } + + @Mutating("name") + open fun testFloat(name: String): Float { + return if (name == "changed") 1F else 0F + } + + @Mutating("age") + open fun testFloat(name: String, age: Int): Float { + assert(name == "test") + return age.toFloat() + } + + @Mutating("name") + open fun testChar(name: String): Char { + return (if (name == "changed") 1 else 0).toChar() + } + + @Mutating("age") + open fun testChar(name: String, age: Int): Char { + assert(name == "test") + return age.toChar() + } + + @Mutating("name") + open fun testByteArray(name: String, data: ByteArray): ByteArray { + assert(name == "changed") + return data + } + + @Mutating("name") + open fun testGenericsWithExtends(name: T, age: Int): T { + return "Name is $name" as T + } + + @Mutating("name") + open fun testListWithWildCardIn(name: T, p2: CovariantClass): CovariantClass { + return CovariantClass(name.toString()) + } + + @Mutating("name") + open fun testListWithWildCardOut(name: T, p2: CovariantClass): CovariantClass { + return CovariantClass(name.toString()) + } + + @Mutating("name") + open fun testGenericsFromType(name: A, age: Int): A { + return "Name is $name" as A + } + + @Invalid + open fun invalidInterceptor() { + } +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/simple/TestBinding.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/simple/TestBinding.kt new file mode 100644 index 00000000000..7a05ea30fa2 --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/simple/TestBinding.kt @@ -0,0 +1,12 @@ +package io.micronaut.kotlin.processing.aop.simple + +import io.micronaut.aop.Around + +@Around +@MustBeDocumented +@Retention +@Target( + AnnotationTarget.FUNCTION, + AnnotationTarget.CLASS +) +annotation class TestBinding diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/beans/aliasfor/TestAnnotation.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/beans/aliasfor/TestAnnotation.kt new file mode 100644 index 00000000000..75b55d4b7f9 --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/beans/aliasfor/TestAnnotation.kt @@ -0,0 +1,37 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.kotlin.processing.beans.aliasfor + +import io.micronaut.context.annotation.AliasFor +import io.micronaut.context.annotation.Executable +import jakarta.inject.Named +import jakarta.inject.Singleton + +@MustBeDocumented +@Retention(AnnotationRetention.RUNTIME) +@Target( + AnnotationTarget.ANNOTATION_CLASS, + AnnotationTarget.CLASS, + AnnotationTarget.FUNCTION, + AnnotationTarget.PROPERTY_GETTER, + AnnotationTarget.PROPERTY_SETTER +) +@Singleton +@Executable +annotation class TestAnnotation ( + @get:AliasFor(annotation = Named::class, member = "value") + val value: String = "" +) diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/beans/collect/MyIterable.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/beans/collect/MyIterable.kt new file mode 100644 index 00000000000..a1bd9834d2c --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/beans/collect/MyIterable.kt @@ -0,0 +1,22 @@ +package io.micronaut.kotlin.processing.beans.collect + +import jakarta.inject.Singleton + +@Singleton +class MyIterable : Iterable { + override fun iterator(): MutableIterator { + return object : MutableIterator { + override fun hasNext(): Boolean { + return false + } + + override fun next(): String? { + return null + } + + override fun remove() { + + } + } + } +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/beans/collect/MySetOfStrings.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/beans/collect/MySetOfStrings.kt new file mode 100644 index 00000000000..df2a15de1d5 --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/beans/collect/MySetOfStrings.kt @@ -0,0 +1,26 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.kotlin.processing.beans.collect + +import jakarta.inject.Singleton +import java.util.HashSet + +@Singleton +class MySetOfStrings : HashSet() { + init { + add("foo") + } +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/beans/collect/ThingThatNeedsMyIterable.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/beans/collect/ThingThatNeedsMyIterable.kt new file mode 100644 index 00000000000..65be801492a --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/beans/collect/ThingThatNeedsMyIterable.kt @@ -0,0 +1,6 @@ +package io.micronaut.kotlin.processing.beans.collect + +import jakarta.inject.Singleton + +@Singleton +class ThingThatNeedsMyIterable(myIterable: MyIterable) diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/beans/collect/ThingThatNeedsMySetOfStrings.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/beans/collect/ThingThatNeedsMySetOfStrings.kt new file mode 100644 index 00000000000..3f53c8afa90 --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/beans/collect/ThingThatNeedsMySetOfStrings.kt @@ -0,0 +1,26 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.kotlin.processing.beans.collect + +import jakarta.inject.Inject +import jakarta.inject.Singleton + +@Singleton +class ThingThatNeedsMySetOfStrings(var strings: MySetOfStrings) { + + @Inject + var otherStrings: MySetOfStrings? = null +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/beans/configproperties/AnnWithClass.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/beans/configproperties/AnnWithClass.kt new file mode 100644 index 00000000000..99ae8d9d9f4 --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/beans/configproperties/AnnWithClass.kt @@ -0,0 +1,8 @@ +package io.micronaut.kotlin.processing.beans.configproperties + +import kotlin.reflect.KClass + +@MustBeDocumented +@Retention +@Target(AnnotationTarget.ANNOTATION_CLASS, AnnotationTarget.CLASS) +annotation class AnnWithClass(val value: KClass<*>) diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/beans/configproperties/MapProperties.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/beans/configproperties/MapProperties.kt new file mode 100644 index 00000000000..d7c42f3e5f6 --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/beans/configproperties/MapProperties.kt @@ -0,0 +1,23 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.kotlin.processing.beans.configproperties + +import io.micronaut.context.annotation.ConfigurationProperties + +@ConfigurationProperties("map") +class MapProperties { + var setter: Map? = null +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/beans/configproperties/MyConfig.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/beans/configproperties/MyConfig.kt new file mode 100644 index 00000000000..c2a93fa32eb --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/beans/configproperties/MyConfig.kt @@ -0,0 +1,80 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.kotlin.processing.beans.configproperties + +import io.micronaut.context.annotation.ConfigurationProperties +import io.micronaut.core.convert.format.ReadableBytes +import java.net.URL +import java.util.* + +@ConfigurationProperties("foo.bar") +open class MyConfig { + var port = 0 + var defaultValue = 9999 + var stringList: List? = null + var intList: List? = null + var urlList: List? = null + var urlList2: List? = null + var emptyList: List? = null + var flags: Map? = null + var url: Optional? = null + var anotherUrl = Optional.empty() + var inner: Inner? = null + var defaultPort = 9999 + protected set + var anotherPort: Int? = null + protected set + var innerVals: List? = null + + @ReadableBytes + var maxSize = 0 + + @ReadableBytes + var anotherSize = 0 + var map: Map> = HashMap() + + class Value { + var property = 0 + var property2: Value2? = null + + constructor() {} + constructor(property: Int, property2: Value2?) { + this.property = property + this.property2 = property2 + } + } + + class Value2 { + var property = 0 + + constructor() {} + constructor(property: Int) { + this.property = property + } + } + + @ConfigurationProperties("inner") + class Inner { + var enabled = false + fun isEnabled(): Boolean { + return enabled + } + } +} + +class InnerVal { + var expireUnsignedSeconds: Int? = null +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/beans/configproperties/MyConfigInner.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/beans/configproperties/MyConfigInner.kt new file mode 100644 index 00000000000..205fc317f83 --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/beans/configproperties/MyConfigInner.kt @@ -0,0 +1,12 @@ +package io.micronaut.kotlin.processing.beans.configproperties + +import io.micronaut.context.annotation.ConfigurationProperties + +@ConfigurationProperties("foo.bar") +class MyConfigInner { + var innerVals: List? = null + + class InnerVal { + var expireUnsignedSeconds: Int? = null + } +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/beans/configproperties/Neo4jProperties.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/beans/configproperties/Neo4jProperties.kt new file mode 100644 index 00000000000..18aa6dd60a6 --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/beans/configproperties/Neo4jProperties.kt @@ -0,0 +1,30 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.kotlin.processing.beans.configproperties + +import io.micronaut.context.annotation.ConfigurationBuilder +import io.micronaut.context.annotation.ConfigurationProperties +import org.neo4j.driver.v1.Config +import java.net.URI +import java.net.URISyntaxException + +@ConfigurationProperties("neo4j.test") +class Neo4jProperties { + var uri: URI? = null + + @ConfigurationBuilder(prefixes = ["with"], allowZeroArgs = true) + var options = Config.build() +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/beans/configproperties/Neo4jPropertiesFactory.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/beans/configproperties/Neo4jPropertiesFactory.kt new file mode 100644 index 00000000000..23a106f2dea --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/beans/configproperties/Neo4jPropertiesFactory.kt @@ -0,0 +1,24 @@ +package io.micronaut.kotlin.processing.beans.configproperties + +import io.micronaut.context.annotation.Factory +import io.micronaut.context.annotation.Replaces +import io.micronaut.context.annotation.Requires +import jakarta.inject.Singleton +import java.net.URI +import java.net.URISyntaxException + +@Factory +class Neo4jPropertiesFactory { + + @Singleton + @Replaces(Neo4jProperties::class) + @Requires(property = "spec.name", value = "ConfigurationPropertiesFactorySpec") + fun neo4jProperties(): Neo4jProperties { + val props = Neo4jProperties() + try { + props.uri = URI("https://google.com") + } catch (e: URISyntaxException) { + } + return props + } +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/beans/configproperties/Pojo.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/beans/configproperties/Pojo.kt new file mode 100644 index 00000000000..aeee41d218a --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/beans/configproperties/Pojo.kt @@ -0,0 +1,18 @@ +package io.micronaut.kotlin.processing.beans.configproperties + +import io.micronaut.core.annotation.Introspected + +import javax.validation.constraints.Email +import javax.validation.constraints.NotBlank + +@Introspected +class Pojo { + + @Email(message = "Email should be valid") + var email: String? = null + + @NotBlank + var name: String? = null + +} + diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/beans/configproperties/RecConf.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/beans/configproperties/RecConf.kt new file mode 100644 index 00000000000..5c09f752526 --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/beans/configproperties/RecConf.kt @@ -0,0 +1,24 @@ +package io.micronaut.kotlin.processing.beans.configproperties + +import io.micronaut.context.annotation.ConfigurationProperties +import java.util.* + +@ConfigurationProperties("rec") +class RecConf { + var namesListOf: List? = null + var mapChildren: Map? = null + var listChildren: List? = null + + override fun equals(o: Any?): Boolean { + if (this === o) return true + if (o == null || javaClass != o.javaClass) return false + val recConf = o as RecConf + return namesListOf == recConf.namesListOf && + mapChildren == recConf.mapChildren && + listChildren == recConf.listChildren + } + + override fun hashCode(): Int { + return Objects.hash(namesListOf, mapChildren, listChildren) + } +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/beans/configproperties/ValidatedConfig.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/beans/configproperties/ValidatedConfig.kt new file mode 100644 index 00000000000..5c0a45549bd --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/beans/configproperties/ValidatedConfig.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.kotlin.processing.beans.configproperties; + +import io.micronaut.context.annotation.ConfigurationProperties +import io.micronaut.context.annotation.Requires + +import javax.validation.constraints.NotBlank +import javax.validation.constraints.NotNull +import java.net.URL + +@Requires(property = "spec.name", value = "ValidatedConfigurationSpec") +@ConfigurationProperties("foo.bar") +class ValidatedConfig { + + @NotNull + var url: URL? = null + + @NotBlank + internal var name: String? = null +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/beans/configproperties/inheritance/ChildConfig.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/beans/configproperties/inheritance/ChildConfig.kt new file mode 100644 index 00000000000..b32c211ea03 --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/beans/configproperties/inheritance/ChildConfig.kt @@ -0,0 +1,23 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.kotlin.processing.beans.configproperties.inheritance + +import io.micronaut.context.annotation.ConfigurationProperties + +@ConfigurationProperties("baz") +class ChildConfig : MyConfig() { + var stuff: String? = null +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/beans/configproperties/inheritance/MyConfig.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/beans/configproperties/inheritance/MyConfig.kt new file mode 100644 index 00000000000..b086b7847e6 --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/beans/configproperties/inheritance/MyConfig.kt @@ -0,0 +1,24 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.kotlin.processing.beans.configproperties.inheritance + +import io.micronaut.context.annotation.ConfigurationProperties + +@ConfigurationProperties("foo.bar") +open class MyConfig { + var port = 0 + var host: String? = null +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/beans/configproperties/inheritance/MyOtherConfig.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/beans/configproperties/inheritance/MyOtherConfig.kt new file mode 100644 index 00000000000..1b7afa9f8b4 --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/beans/configproperties/inheritance/MyOtherConfig.kt @@ -0,0 +1,24 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.kotlin.processing.beans.configproperties.inheritance + +import io.micronaut.context.annotation.ConfigurationProperties + +@ConfigurationProperties("foo.baz") +class MyOtherConfig : ParentPojo() { + var otherProperty: String? = null + var onlySetter: String? = null +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/beans/configproperties/inheritance/ParentArrayEachProps.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/beans/configproperties/inheritance/ParentArrayEachProps.kt new file mode 100644 index 00000000000..df859ad5b13 --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/beans/configproperties/inheritance/ParentArrayEachProps.kt @@ -0,0 +1,40 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.kotlin.processing.beans.configproperties.inheritance + +import io.micronaut.context.annotation.ConfigurationProperties +import io.micronaut.context.annotation.EachProperty +import io.micronaut.context.annotation.Parameter +import io.micronaut.core.order.Ordered + +@EachProperty(value = "teams", list = true) +class ParentArrayEachProps internal constructor(@Parameter private val index: Int) : Ordered { + var wins: Int? = null + var manager: ManagerProps? = null + + override fun getOrder(): Int { + return index + } + + @ConfigurationProperties("manager") + class ManagerProps internal constructor(@Parameter private val index: Int) : Ordered { + var age: Int? = null + + override fun getOrder(): Int { + return index + } + } +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/beans/configproperties/inheritance/ParentArrayEachPropsCtor.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/beans/configproperties/inheritance/ParentArrayEachPropsCtor.kt new file mode 100644 index 00000000000..4d29f440e1b --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/beans/configproperties/inheritance/ParentArrayEachPropsCtor.kt @@ -0,0 +1,39 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.kotlin.processing.beans.configproperties.inheritance + +import io.micronaut.context.annotation.ConfigurationProperties +import io.micronaut.context.annotation.EachProperty +import io.micronaut.context.annotation.Parameter +import io.micronaut.core.annotation.Nullable +import io.micronaut.core.order.Ordered + +@EachProperty(value = "teams", list = true) +class ParentArrayEachPropsCtor internal constructor( + @Parameter private val index: Int, + val manager: ManagerProps? +) : Ordered { + var wins: Int? = null + + override fun getOrder(): Int = index + + @ConfigurationProperties("manager") + class ManagerProps internal constructor(@Parameter private val index: Int) : Ordered { + var age: Int? = null + + override fun getOrder(): Int = index + } +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/beans/configproperties/inheritance/ParentEachProps.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/beans/configproperties/inheritance/ParentEachProps.kt new file mode 100644 index 00000000000..8d1abdffd3e --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/beans/configproperties/inheritance/ParentEachProps.kt @@ -0,0 +1,30 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.kotlin.processing.beans.configproperties.inheritance + +import io.micronaut.context.annotation.ConfigurationProperties +import io.micronaut.context.annotation.EachProperty + +@EachProperty("teams") +class ParentEachProps { + var wins: Int? = null + var manager: ManagerProps? = null + + @ConfigurationProperties("manager") + class ManagerProps { + var age: Int? = null + } +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/beans/configproperties/inheritance/ParentEachPropsCtor.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/beans/configproperties/inheritance/ParentEachPropsCtor.kt new file mode 100644 index 00000000000..283c8af39c2 --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/beans/configproperties/inheritance/ParentEachPropsCtor.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.kotlin.processing.beans.configproperties.inheritance + +import io.micronaut.context.annotation.ConfigurationProperties +import io.micronaut.context.annotation.EachProperty +import io.micronaut.context.annotation.Parameter +import io.micronaut.core.annotation.Nullable + +@EachProperty("teams") +class ParentEachPropsCtor internal constructor( + @Parameter val name: String, + val manager: ManagerProps? +) { + var wins: Int? = null + + @ConfigurationProperties("manager") + class ManagerProps internal constructor(@Parameter val name: String) { + var age: Int? = null + } +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/beans/configproperties/inheritance/ParentPojo.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/beans/configproperties/inheritance/ParentPojo.kt new file mode 100644 index 00000000000..719c11ce6da --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/beans/configproperties/inheritance/ParentPojo.kt @@ -0,0 +1,20 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.kotlin.processing.beans.configproperties.inheritance + +open class ParentPojo { + var port = 0 +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/beans/configuration/Engine.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/beans/configuration/Engine.kt new file mode 100644 index 00000000000..cf238fe185f --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/beans/configuration/Engine.kt @@ -0,0 +1,23 @@ +package io.micronaut.kotlin.processing.beans.configuration + +class Engine(val manufacturer: String) { + + class Builder { + private var manufacturer = "Ford" + + fun withManufacturer(manufacturer: String): Builder { + this.manufacturer = manufacturer + return this + } + + fun build(): Engine { + return Engine(manufacturer) + } + } + + companion object { + fun builder(): Builder { + return Builder() + } + } +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/beans/executable/BookController.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/beans/executable/BookController.kt new file mode 100644 index 00000000000..2f45d58b4f0 --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/beans/executable/BookController.kt @@ -0,0 +1,56 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.kotlin.processing.beans.executable + +import io.micronaut.context.annotation.Executable +import jakarta.inject.Inject + +@Executable +class BookController { + + @Inject + lateinit var bookService: BookService + + @Executable + fun show(id: Long?): String { + return String.format("%d - The Stand", id) + } + + @Executable + fun showArray(id: Array): String { + return String.format("%d - The Stand", id[0]) + } + + @Executable + fun showPrimitive(id: Long): String { + return String.format("%d - The Stand", id) + } + + @Executable + fun showPrimitiveArray(id: LongArray): String { + return String.format("%d - The Stand", id[0]) + } + + @Executable + fun showVoidReturn(jobNames: MutableList) { + jobNames.add("test") + } + + @Executable + fun showPrimitiveReturn(values: IntArray): Int { + return values[0] + } +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/beans/executable/BookService.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/beans/executable/BookService.kt new file mode 100644 index 00000000000..8213d2cc910 --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/beans/executable/BookService.kt @@ -0,0 +1,21 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.kotlin.processing.beans.executable + +import jakarta.inject.Singleton + +@Singleton +class BookService diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/beans/executable/RepeatableExecutable.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/beans/executable/RepeatableExecutable.kt new file mode 100644 index 00000000000..989082e78d0 --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/beans/executable/RepeatableExecutable.kt @@ -0,0 +1,7 @@ +package io.micronaut.kotlin.processing.beans.executable + +import io.micronaut.context.annotation.Executable + +@Repeatable +@Executable(processOnStartup = true) +annotation class RepeatableExecutable(val value: String) diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/beans/factory/beanannotation/A.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/beans/factory/beanannotation/A.kt new file mode 100644 index 00000000000..acde1f29523 --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/beans/factory/beanannotation/A.kt @@ -0,0 +1,21 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.kotlin.processing.beans.factory.beanannotation + +import io.micronaut.context.annotation.Prototype + +@Prototype +class A diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/elementapi/EntityAnnotationMapper.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/elementapi/EntityAnnotationMapper.kt new file mode 100644 index 00000000000..a09ac58b3ec --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/elementapi/EntityAnnotationMapper.kt @@ -0,0 +1,90 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.kotlin.processing.elementapi + +import io.micronaut.core.annotation.AnnotationValue +import io.micronaut.core.annotation.Introspected +import io.micronaut.core.annotation.NonNull +import io.micronaut.inject.annotation.NamedAnnotationMapper +import io.micronaut.inject.visitor.VisitorContext + +class EntityAnnotationMapper : NamedAnnotationMapper { + @NonNull + override fun getName(): String { + return "javax.persistence.Entity" + } + + override fun map( + annotation: AnnotationValue, + visitorContext: VisitorContext + ): List> { + val builder = AnnotationValue.builder( + Introspected::class.java + ) // don't bother with transients properties + .member("excludedAnnotations", "javax.persistence.Transient") // following are indexed for fast lookups + .member( + "indexed", + AnnotationValue.builder( + Introspected.IndexedAnnotation::class.java + ) + .member("annotation", "javax.persistence.Id").build(), + AnnotationValue.builder( + Introspected.IndexedAnnotation::class.java + ) + .member("annotation", "javax.persistence.Version").build(), + AnnotationValue.builder( + Introspected.IndexedAnnotation::class.java + ) + .member("annotation", "javax.persistence.GeneratedValue").build(), + AnnotationValue.builder( + Introspected.IndexedAnnotation::class.java + ) + .member("annotation", "javax.persistence.Basic").build(), + AnnotationValue.builder( + Introspected.IndexedAnnotation::class.java + ) + .member("annotation", "javax.persistence.Embedded").build(), + AnnotationValue.builder( + Introspected.IndexedAnnotation::class.java + ) + .member("annotation", "javax.persistence.OneToMany").build(), + AnnotationValue.builder( + Introspected.IndexedAnnotation::class.java + ) + .member("annotation", "javax.persistence.OneToOne").build(), + AnnotationValue.builder( + Introspected.IndexedAnnotation::class.java + ) + .member("annotation", "javax.persistence.ManyToOne").build(), + AnnotationValue.builder( + Introspected.IndexedAnnotation::class.java + ) + .member("annotation", "javax.persistence.ElementCollection").build(), + AnnotationValue.builder( + Introspected.IndexedAnnotation::class.java + ) + .member("annotation", "javax.persistence.Enumerated").build(), + AnnotationValue.builder( + Introspected.IndexedAnnotation::class.java + ) + .member("annotation", "javax.persistence.Column") + .member("member", "name").build() + ) + return listOf>( + builder.build() + ) + } +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/elementapi/Foo.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/elementapi/Foo.kt new file mode 100644 index 00000000000..8ec10244dc8 --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/elementapi/Foo.kt @@ -0,0 +1,5 @@ +package io.micronaut.kotlin.processing.elementapi + +class Foo: GenBase { + override var value: Long? = null +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/elementapi/GenBase.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/elementapi/GenBase.kt new file mode 100644 index 00000000000..98338e7c02f --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/elementapi/GenBase.kt @@ -0,0 +1,5 @@ +package io.micronaut.kotlin.processing.elementapi + +interface GenBase { + var value: T +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/elementapi/MarkerAnnotation.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/elementapi/MarkerAnnotation.kt new file mode 100644 index 00000000000..14478918a59 --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/elementapi/MarkerAnnotation.kt @@ -0,0 +1,3 @@ +package io.micronaut.kotlin.processing.elementapi + +annotation class MarkerAnnotation diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/elementapi/OtherTestBean.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/elementapi/OtherTestBean.kt new file mode 100644 index 00000000000..6328c0a7ce2 --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/elementapi/OtherTestBean.kt @@ -0,0 +1,21 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.kotlin.processing.elementapi + +@MarkerAnnotation +class OtherTestBean { + var name: String? = null +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/elementapi/OuterBean.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/elementapi/OuterBean.kt new file mode 100644 index 00000000000..d16c756535f --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/elementapi/OuterBean.kt @@ -0,0 +1,12 @@ +package io.micronaut.kotlin.processing.elementapi + +class OuterBean { + + class InnerBean { + var name: String? = null + } + + interface InnerInterface { + fun getName(): String + } +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/elementapi/SomeEnum.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/elementapi/SomeEnum.kt new file mode 100644 index 00000000000..8ea0e7e4068 --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/elementapi/SomeEnum.kt @@ -0,0 +1,5 @@ +package io.micronaut.kotlin.processing.elementapi + +enum class SomeEnum { + A, B +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/elementapi/TestBean.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/elementapi/TestBean.kt new file mode 100644 index 00000000000..d4385bd2718 --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/elementapi/TestBean.kt @@ -0,0 +1,11 @@ +package io.micronaut.kotlin.processing.elementapi + +import io.micronaut.core.annotation.Introspected + +@Introspected +class TestBean { + var flag = false + var name: String? = null + var age = 0 + var stringArray: Array? = null +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/elementapi/TestClass.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/elementapi/TestClass.kt new file mode 100644 index 00000000000..d4a831ec6c9 --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/elementapi/TestClass.kt @@ -0,0 +1,6 @@ +package io.micronaut.kotlin.processing.elementapi + +abstract class TestClass { + var name: String? = null + var author: String? = null +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/elementapi/TestEntity.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/elementapi/TestEntity.kt new file mode 100644 index 00000000000..eec2a968e22 --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/elementapi/TestEntity.kt @@ -0,0 +1,31 @@ +package io.micronaut.kotlin.processing.elementapi + +import javax.validation.constraints.* +import javax.persistence.* + +@Entity +class TestEntity( + @Column(name="test_name") var name: String, + @Size(max=100) var age: Int, + primitiveArray: Array) { + + @Id + @GeneratedValue + var id: Long? = null + + @Version + var version: Long? = null + + private var primitiveArray: Array? = null + + private var v: Long? = null + + @Version + fun getAnotherVersion(): Long? { + return v; + } + + fun setAnotherVersion(v: Long) { + this.v = v + } +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/inject/configproperties/ChildConfigPropertiesX.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/inject/configproperties/ChildConfigPropertiesX.kt new file mode 100644 index 00000000000..dbcf28a6a8a --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/inject/configproperties/ChildConfigPropertiesX.kt @@ -0,0 +1,10 @@ +package io.micronaut.kotlin.processing.inject.configproperties + +import io.micronaut.context.annotation.ConfigurationProperties +import io.micronaut.kotlin.processing.inject.configproperties.other.ParentConfigProperties + +@ConfigurationProperties("child") +class ChildConfigPropertiesX: ParentConfigProperties() { + + var age: Int? = null +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/inject/configproperties/MapProperties.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/inject/configproperties/MapProperties.kt new file mode 100644 index 00000000000..3744843f252 --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/inject/configproperties/MapProperties.kt @@ -0,0 +1,24 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.kotlin.processing.inject.configproperties; + +import io.micronaut.context.annotation.ConfigurationProperties; + +@ConfigurationProperties("map") +class MapProperties { + + var property: Map? = null +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/inject/configproperties/MyConfig.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/inject/configproperties/MyConfig.kt new file mode 100644 index 00000000000..ef4e92798d3 --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/inject/configproperties/MyConfig.kt @@ -0,0 +1,64 @@ +package io.micronaut.kotlin.processing.inject.configproperties + +import io.micronaut.context.annotation.ConfigurationProperties +import io.micronaut.core.convert.format.ReadableBytes + +import java.net.URL +import java.util.Optional + +@ConfigurationProperties("foo.bar") +class MyConfig { + var port: Int = 0 + var defaultValue: Int = 9999 + var stringList: List? = null + var intList: List? = null + var urlList: List? = null + var urlList2: List? = null + var emptyList: List? = null + var flags: Map? = null + var url: Optional? = null + var anotherUrl: Optional = Optional.empty() + var inner: Inner? = null + protected var defaultPort: Int = 9999 + protected var anotherPort: Int? = null + var innerVals: List? = null + + @ReadableBytes + var maxSize: Int = 0 + + var map: Map> = mapOf() + + class Value { + var property: Int = 0 + var property2: Value2? = null + + constructor() + + constructor(property: Int, property2: Value2) { + this.property = property + this.property2 = property2 + } + } + + class Value2 { + var property: Int = 0 + + constructor() + + constructor(property: Int) { + this.property = property + } + } + + @ConfigurationProperties("inner") + class Inner { + var enabled = false + + fun isEnabled() = enabled + } + +} + +class InnerVal { + var expireUnsignedSeconds: Int? = null +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/inject/configproperties/MyConfigInner.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/inject/configproperties/MyConfigInner.kt new file mode 100644 index 00000000000..74b587ac1b7 --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/inject/configproperties/MyConfigInner.kt @@ -0,0 +1,12 @@ +package io.micronaut.kotlin.processing.inject.configproperties + +import io.micronaut.context.annotation.ConfigurationProperties + +@ConfigurationProperties("foo.bar") +class MyConfigInner { + var innerVals: List? = null + + class InnerVal { + var expireUnsignedSeconds: Int? = null + } +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/inject/configproperties/MyHibernateConfig.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/inject/configproperties/MyHibernateConfig.kt new file mode 100644 index 00000000000..8d0f5a869dc --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/inject/configproperties/MyHibernateConfig.kt @@ -0,0 +1,12 @@ +package io.micronaut.kotlin.processing.inject.configproperties + +import io.micronaut.context.annotation.ConfigurationProperties +import io.micronaut.core.convert.format.MapFormat +import io.micronaut.core.naming.conventions.StringConvention + +@ConfigurationProperties("jpa") +class MyHibernateConfig { + + @MapFormat(keyFormat = StringConvention.RAW, transformation = MapFormat.MapTransformation.FLAT) + var properties: Map? = null +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/inject/configproperties/MyHibernateConfig2.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/inject/configproperties/MyHibernateConfig2.kt new file mode 100644 index 00000000000..1feeea8cd90 --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/inject/configproperties/MyHibernateConfig2.kt @@ -0,0 +1,11 @@ +package io.micronaut.kotlin.processing.inject.configproperties + +import io.micronaut.context.annotation.ConfigurationProperties +import io.micronaut.core.convert.format.MapFormat + +@ConfigurationProperties("jpa") +class MyHibernateConfig2 { + + @MapFormat(transformation = MapFormat.MapTransformation.FLAT) + var properties: Map? = null +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/inject/configproperties/MyPrimitiveConfig.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/inject/configproperties/MyPrimitiveConfig.kt new file mode 100644 index 00000000000..9277355a928 --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/inject/configproperties/MyPrimitiveConfig.kt @@ -0,0 +1,9 @@ +package io.micronaut.kotlin.processing.inject.configproperties + +import io.micronaut.context.annotation.ConfigurationProperties + +@ConfigurationProperties("foo.bar") +class MyPrimitiveConfig { + var port = 0 + var defaultValue = 9999 +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/inject/configproperties/Neo4jProperties.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/inject/configproperties/Neo4jProperties.kt new file mode 100644 index 00000000000..8c5a1b8da4f --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/inject/configproperties/Neo4jProperties.kt @@ -0,0 +1,17 @@ +package io.micronaut.kotlin.processing.inject.configproperties + +import io.micronaut.context.annotation.ConfigurationBuilder +import io.micronaut.context.annotation.ConfigurationProperties +import org.neo4j.driver.v1.Config +import java.net.URI + +@ConfigurationProperties("neo4j.test") +class Neo4jProperties { + var uri: URI? = null + + @ConfigurationBuilder( + prefixes=["with"], + allowZeroArgs=true + ) + val options: Config.ConfigBuilder = Config.build() +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/inject/configproperties/Neo4jPropertiesFactory.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/inject/configproperties/Neo4jPropertiesFactory.kt new file mode 100644 index 00000000000..3b89ca07593 --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/inject/configproperties/Neo4jPropertiesFactory.kt @@ -0,0 +1,24 @@ +package io.micronaut.kotlin.processing.inject.configproperties + +import io.micronaut.context.annotation.Factory +import io.micronaut.context.annotation.Replaces +import io.micronaut.context.annotation.Requires +import jakarta.inject.Singleton +import java.net.URI +import java.net.URISyntaxException + +@Factory +class Neo4jPropertiesFactory { + + @Singleton + @Replaces(Neo4jProperties::class) + @Requires(property = "spec.name", value = "ConfigurationPropertiesFactorySpec") + fun neo4jProperties(): Neo4jProperties { + val props = Neo4jProperties() + try { + props.uri = URI("https://google.com") + } catch (e: URISyntaxException) { + } + return props + } +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/inject/configproperties/Pojo.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/inject/configproperties/Pojo.kt new file mode 100644 index 00000000000..baa7b01a6f6 --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/inject/configproperties/Pojo.kt @@ -0,0 +1,16 @@ +package io.micronaut.kotlin.processing.inject.configproperties + +import io.micronaut.core.annotation.Introspected +import javax.validation.constraints.Email +import javax.validation.constraints.NotBlank + +@Introspected +class Pojo { + + @Email(message = "Email should be valid") + var email: String? = null + + @NotBlank + var name: String? = null +} + diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/inject/configproperties/RecConf.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/inject/configproperties/RecConf.kt new file mode 100644 index 00000000000..2413c6b76e5 --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/inject/configproperties/RecConf.kt @@ -0,0 +1,25 @@ +package io.micronaut.kotlin.processing.inject.configproperties + +import io.micronaut.context.annotation.ConfigurationProperties +import java.util.* + +@ConfigurationProperties("rec") +class RecConf { + + var namesListOf: List? = null + var mapChildren: Map? = null + var listChildren: List? = null + + override fun equals(other: Any?): Boolean { + if (this === other) return true; + if (other == null || this.javaClass != other.javaClass) return false; + val recConf = other as RecConf + return Objects.equals(namesListOf, recConf.namesListOf) && + Objects.equals(mapChildren, recConf.mapChildren) && + Objects.equals(listChildren, recConf.listChildren) + } + + override fun hashCode(): Int { + return Objects.hash(namesListOf, mapChildren, listChildren) + } +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/inject/configproperties/ValidatedConfig.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/inject/configproperties/ValidatedConfig.kt new file mode 100644 index 00000000000..b8217e6b454 --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/inject/configproperties/ValidatedConfig.kt @@ -0,0 +1,19 @@ +package io.micronaut.kotlin.processing.inject.configproperties + +import io.micronaut.context.annotation.ConfigurationProperties +import io.micronaut.context.annotation.Requires +import io.micronaut.core.annotation.Introspected +import javax.validation.constraints.NotBlank +import javax.validation.constraints.NotNull +import java.net.URL + +@Requires(property = "spec.name", value = "ValidatedConfigurationSpec") +@ConfigurationProperties("foo.bar") +@Introspected +class ValidatedConfig { + + @NotNull + var url: URL? = null + @NotBlank + var name: String? = null +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/inject/configproperties/inheritance/ChildConfig.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/inject/configproperties/inheritance/ChildConfig.kt new file mode 100644 index 00000000000..d3494fbd57d --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/inject/configproperties/inheritance/ChildConfig.kt @@ -0,0 +1,8 @@ +package io.micronaut.kotlin.processing.inject.configproperties.inheritance + +import io.micronaut.context.annotation.ConfigurationProperties + +@ConfigurationProperties("baz") +class ChildConfig: MyConfig() { + var stuff: String? = null +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/inject/configproperties/inheritance/MyConfig.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/inject/configproperties/inheritance/MyConfig.kt new file mode 100644 index 00000000000..cb33dcc4f9e --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/inject/configproperties/inheritance/MyConfig.kt @@ -0,0 +1,9 @@ +package io.micronaut.kotlin.processing.inject.configproperties.inheritance + +import io.micronaut.context.annotation.ConfigurationProperties + +@ConfigurationProperties("foo.bar") +open class MyConfig { + var port: Int = 0 + var host: String? = null +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/inject/configproperties/inheritance/MyOtherConfig.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/inject/configproperties/inheritance/MyOtherConfig.kt new file mode 100644 index 00000000000..77415fd8a8a --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/inject/configproperties/inheritance/MyOtherConfig.kt @@ -0,0 +1,11 @@ +package io.micronaut.kotlin.processing.inject.configproperties.inheritance + +import io.micronaut.context.annotation.ConfigurationProperties + +@ConfigurationProperties("foo.baz") +class MyOtherConfig: ParentPojo() { + + var onlySetter: String? = null + var otherProperty: String? = null + +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/inject/configproperties/inheritance/ParentArrayEachProps.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/inject/configproperties/inheritance/ParentArrayEachProps.kt new file mode 100644 index 00000000000..999e0cb7b89 --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/inject/configproperties/inheritance/ParentArrayEachProps.kt @@ -0,0 +1,22 @@ +package io.micronaut.kotlin.processing.inject.configproperties.inheritance + +import io.micronaut.context.annotation.ConfigurationProperties +import io.micronaut.context.annotation.EachProperty +import io.micronaut.context.annotation.Parameter +import io.micronaut.core.order.Ordered + +@EachProperty(value = "teams", list = true) +class ParentArrayEachProps(@Parameter private val index: Int): Ordered { + var wins: Int? = null + var manager: ManagerProps? = null + + override fun getOrder() = index + + @ConfigurationProperties("manager") + class ManagerProps(@Parameter private val index: Int): Ordered { + + var age: Int? = null + + override fun getOrder() = index + } +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/inject/configproperties/inheritance/ParentArrayEachPropsCtor.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/inject/configproperties/inheritance/ParentArrayEachPropsCtor.kt new file mode 100644 index 00000000000..1e027105ec7 --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/inject/configproperties/inheritance/ParentArrayEachPropsCtor.kt @@ -0,0 +1,22 @@ +package io.micronaut.kotlin.processing.inject.configproperties.inheritance + +import io.micronaut.context.annotation.ConfigurationProperties +import io.micronaut.context.annotation.EachProperty +import io.micronaut.context.annotation.Parameter +import io.micronaut.core.order.Ordered + +@EachProperty(value = "teams", list = true) +class ParentArrayEachPropsCtor(@Parameter private val index: Int, val manager: ManagerProps?): Ordered { + + var wins: Int? = null + + override fun getOrder() = index + + @ConfigurationProperties("manager") + class ManagerProps(@Parameter private val index: Int): Ordered { + + var age: Int? = null + + override fun getOrder() = index + } +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/inject/configproperties/inheritance/ParentEachProps.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/inject/configproperties/inheritance/ParentEachProps.kt new file mode 100644 index 00000000000..20d446be888 --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/inject/configproperties/inheritance/ParentEachProps.kt @@ -0,0 +1,16 @@ +package io.micronaut.kotlin.processing.inject.configproperties.inheritance + +import io.micronaut.context.annotation.ConfigurationProperties +import io.micronaut.context.annotation.EachProperty + +@EachProperty("teams") +class ParentEachProps { + + var wins: Int? = null + var manager: ManagerProps? = null + + @ConfigurationProperties("manager") + class ManagerProps { + var age: Int? = null + } +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/inject/configproperties/inheritance/ParentEachPropsCtor.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/inject/configproperties/inheritance/ParentEachPropsCtor.kt new file mode 100644 index 00000000000..b4aa17763fb --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/inject/configproperties/inheritance/ParentEachPropsCtor.kt @@ -0,0 +1,16 @@ +package io.micronaut.kotlin.processing.inject.configproperties.inheritance + +import io.micronaut.context.annotation.ConfigurationProperties +import io.micronaut.context.annotation.EachProperty +import io.micronaut.context.annotation.Parameter + +@EachProperty("teams") +class ParentEachPropsCtor(@Parameter val name: String, val manager: ManagerProps?) { + + var wins: Int? = null + + @ConfigurationProperties("manager") + class ManagerProps(@Parameter val name: String) { + var age: Int? = null + } +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/inject/configproperties/inheritance/ParentPojo.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/inject/configproperties/inheritance/ParentPojo.kt new file mode 100644 index 00000000000..30191f8d4c8 --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/inject/configproperties/inheritance/ParentPojo.kt @@ -0,0 +1,5 @@ +package io.micronaut.kotlin.processing.inject.configproperties.inheritance; + +open class ParentPojo { + var port: Int = 0 +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/inject/configproperties/itfce/MyConfig.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/inject/configproperties/itfce/MyConfig.kt new file mode 100644 index 00000000000..6b6542d6370 --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/inject/configproperties/itfce/MyConfig.kt @@ -0,0 +1,13 @@ +package io.micronaut.kotlin.processing.inject.configproperties.itfce + +import io.micronaut.context.annotation.ConfigurationProperties +import io.micronaut.context.annotation.Requires +import javax.validation.constraints.NotBlank + +@ConfigurationProperties("my.config") +@Requires(property = "my.config") +interface MyConfig { + + @NotBlank + fun getName(): String +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/inject/configproperties/itfce/MyEachConfig.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/inject/configproperties/itfce/MyEachConfig.kt new file mode 100644 index 00000000000..d4733248853 --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/inject/configproperties/itfce/MyEachConfig.kt @@ -0,0 +1,13 @@ +package io.micronaut.kotlin.processing.inject.configproperties.itfce + +import io.micronaut.context.annotation.EachProperty +import io.micronaut.context.annotation.Requires +import javax.validation.constraints.NotBlank + +@EachProperty(value = "my.config", primary = "default") +@Requires(property = "my.config") +interface MyEachConfig { + + @NotBlank + fun getName(): String +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/inject/configproperties/other/ParentConfigProperties.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/inject/configproperties/other/ParentConfigProperties.kt new file mode 100644 index 00000000000..b7054dfcac5 --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/inject/configproperties/other/ParentConfigProperties.kt @@ -0,0 +1,33 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.kotlin.processing.inject.configproperties.other; + +import io.micronaut.context.annotation.ConfigurationBuilder; +import io.micronaut.context.annotation.ConfigurationProperties; +import io.micronaut.kotlin.processing.inject.configuration.Engine; + +@ConfigurationProperties("parent") +open class ParentConfigProperties { + + open var name: String? = null + protected set + + protected var nationality: String? = null + + @ConfigurationBuilder(value = "engine", prefixes = ["with"]) + val builder = Engine.builder() + +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/inject/configuration/Engine.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/inject/configuration/Engine.kt new file mode 100644 index 00000000000..ce19009400a --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/inject/configuration/Engine.kt @@ -0,0 +1,23 @@ +package io.micronaut.kotlin.processing.inject.configuration + +class Engine private constructor(val manufacturer: String) { + + companion object { + fun builder(): Builder { + return Builder() + } + } + + class Builder { + private var manufacturer = "Ford"; + + fun withManufacturer(manufacturer: String): Builder { + this.manufacturer = manufacturer + return this + } + + fun build(): Engine { + return Engine(manufacturer) + } + } +} diff --git a/inject-kotlin/src/test/resources/META-INF/services/io.micronaut.inject.annotation.AnnotationMapper b/inject-kotlin/src/test/resources/META-INF/services/io.micronaut.inject.annotation.AnnotationMapper new file mode 100644 index 00000000000..59dc2f50c60 --- /dev/null +++ b/inject-kotlin/src/test/resources/META-INF/services/io.micronaut.inject.annotation.AnnotationMapper @@ -0,0 +1,3 @@ +io.micronaut.kotlin.processing.elementapi.EntityAnnotationMapper +io.micronaut.kotlin.processing.aop.compile.NamedTestAnnMapper +io.micronaut.kotlin.processing.aop.introduction.ListenerAdviceMarkerMapper diff --git a/inject-kotlin/src/test/resources/META-INF/services/io.micronaut.inject.annotation.AnnotationTransformer b/inject-kotlin/src/test/resources/META-INF/services/io.micronaut.inject.annotation.AnnotationTransformer new file mode 100644 index 00000000000..d7080d203e5 --- /dev/null +++ b/inject-kotlin/src/test/resources/META-INF/services/io.micronaut.inject.annotation.AnnotationTransformer @@ -0,0 +1,2 @@ +io.micronaut.kotlin.processing.aop.compile.TestStereotypeAnnTransformer +io.micronaut.kotlin.processing.aop.compile.AroundConstructAnnTransformer diff --git a/inject-kotlin/src/test/resources/logback.xml b/inject-kotlin/src/test/resources/logback.xml new file mode 100644 index 00000000000..8294ddb0484 --- /dev/null +++ b/inject-kotlin/src/test/resources/logback.xml @@ -0,0 +1,15 @@ + + + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + + diff --git a/inject/src/main/java/io/micronaut/context/DefaultBeanContext.java b/inject/src/main/java/io/micronaut/context/DefaultBeanContext.java index 9553828ae2e..14bcad8f133 100644 --- a/inject/src/main/java/io/micronaut/context/DefaultBeanContext.java +++ b/inject/src/main/java/io/micronaut/context/DefaultBeanContext.java @@ -4097,7 +4097,7 @@ private static final class CollectionHolder { * @since 4.0.0 */ @Internal - final static class BeanDefinitionProducer { + static final class BeanDefinitionProducer { @Nullable private volatile BeanDefinitionReference reference; diff --git a/settings.gradle b/settings.gradle index b2c70db8e22..f68d10d1920 100644 --- a/settings.gradle +++ b/settings.gradle @@ -46,6 +46,7 @@ include "inject-groovy" include "inject-groovy-test" include "inject-java" include "inject-java-test" +include 'inject-kotlin' include 'inject-kotlin-test' include "inject-test-utils" include "jackson-core" @@ -68,6 +69,7 @@ include "test-suite-javax-inject" include "test-suite-jakarta-inject-bean-import" include "test-suite-http-server-tck-netty" include "test-suite-kotlin" +include "test-suite-kotlin-ksp" include "test-suite-graal" include "test-suite-groovy" include "test-utils" diff --git a/test-suite-kotlin-ksp/build.gradle b/test-suite-kotlin-ksp/build.gradle new file mode 100644 index 00000000000..69159ecedc5 --- /dev/null +++ b/test-suite-kotlin-ksp/build.gradle @@ -0,0 +1,96 @@ +plugins { + id "io.micronaut.build.internal.convention-test-library" + id "org.jetbrains.kotlin.jvm" + id("com.google.devtools.ksp") version "1.8.0-1.0.8" +} + +micronautBuild { + core { + usesMicronautTestJunit() + usesMicronautTestSpock() + usesMicronautTestKotest() + } +} + +repositories { + mavenLocal() + mavenCentral() + maven { + url "https://s01.oss.sonatype.org/content/repositories/snapshots/" + mavenContent { + snapshotsOnly() + } + } +} + +dependencies { + api libs.kotlin.stdlib + api libs.kotlin.reflect + api libs.kotlinx.coroutines.core + api libs.kotlinx.coroutines.jdk8 + api libs.kotlinx.coroutines.rx2 + api project(':http-server-netty') + api project(':http-client') + api project(':runtime') + + testImplementation project(":context") + testImplementation libs.kotlin.test + testImplementation libs.kotlinx.coroutines.core + testImplementation libs.kotlinx.coroutines.rx2 + testImplementation libs.kotlinx.coroutines.slf4j + testImplementation libs.kotlinx.coroutines.reactor + + // Adding these for now since micronaut-test isnt resolving correctly ... probably need to upgrade gradle there too + testImplementation libs.junit.jupiter.api + + testImplementation project(":validation") + testImplementation project(":management") + testImplementation project(':inject-java') + testImplementation project(":inject") + testImplementation libs.jcache + testImplementation project(':validation') + testImplementation project(":http-client") + testImplementation(libs.micronaut.session) + testImplementation project(":jackson-databind") + testImplementation libs.managed.groovy.templates + + testImplementation project(":function-client") + testImplementation project(":function-web") + testImplementation libs.kotlin.kotest.junit5 + testImplementation libs.logbook.netty + kspTest project(':inject-kotlin') + kspTest project(':validation') + testImplementation libs.javax.inject + testImplementation(platform(libs.test.boms.micronaut.tracing)) + testImplementation(libs.micronaut.tracing.zipkin) { + exclude module: 'micronaut-bom' + exclude module: 'micronaut-http-client' + exclude module: 'micronaut-inject' + exclude module: 'micronaut-runtime' + } + + testRuntimeOnly libs.junit.jupiter.engine + testRuntimeOnly(platform(libs.test.boms.micronaut.aws)) + testRuntimeOnly libs.aws.java.sdk.lambda + if (JavaVersion.current().isCompatibleWith(JavaVersion.VERSION_15)) { + testImplementation libs.bcpkix + } + + testImplementation libs.managed.reactor +} + +configurations.testRuntimeClasspath { + resolutionStrategy.eachDependency { + if (it.requested.group == 'org.jetbrains.kotlin') { + it.useVersion(libs.versions.kotlin.asProvider().get()) + } + } +} + +tasks.named("compileTestKotlin") { + kotlinOptions.jvmTarget = "17" +} + +tasks.named("test") { + useJUnitPlatform() +} diff --git a/test-suite-kotlin-ksp/gradle.properties b/test-suite-kotlin-ksp/gradle.properties new file mode 100644 index 00000000000..1d70cd4a961 --- /dev/null +++ b/test-suite-kotlin-ksp/gradle.properties @@ -0,0 +1,2 @@ +skipDocumentation=true +ksp.incremental = false diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/annotation/processing/DemoController.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/annotation/processing/DemoController.kt new file mode 100644 index 00000000000..0f4b1a26c44 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/annotation/processing/DemoController.kt @@ -0,0 +1,47 @@ +/* + * Copyright 2017-2021 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.annotation.processing + +import io.micronaut.http.MediaType +import io.micronaut.http.annotation.Controller +import io.micronaut.http.annotation.Get +import io.micronaut.http.annotation.Produces + +@Controller("/demo") +@Produces(MediaType.TEXT_PLAIN) +class DemoController { + + @Get("/sync/any") + fun syncAny(): Any { + return "sync any" + } + + @Get("/sync/string") + fun syncStr(): String { + return "sync string" + } + + @Get("/async/any") + suspend fun asyncAny(): Any { + return "async any" + } + + @Get("/async/string") + suspend fun asyncStr(): String { + return "async string" + } + +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/annotation/processing/SuspendFunctionInterceptorSpec.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/annotation/processing/SuspendFunctionInterceptorSpec.kt new file mode 100644 index 00000000000..fc557105f7a --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/annotation/processing/SuspendFunctionInterceptorSpec.kt @@ -0,0 +1,90 @@ +package io.micronaut.annotation.processing + +import io.kotest.matchers.shouldBe +import io.micronaut.http.MediaType +import io.micronaut.http.annotation.Consumes +import io.micronaut.http.annotation.Get +import io.micronaut.http.client.annotation.Client +import io.micronaut.test.extensions.junit5.annotation.MicronautTest +import jakarta.inject.Inject +import kotlinx.coroutines.asCoroutineDispatcher +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import java.util.concurrent.CountDownLatch +import java.util.concurrent.Executors +import java.util.concurrent.TimeUnit +import java.util.concurrent.atomic.AtomicBoolean +import kotlin.coroutines.Continuation +import kotlin.coroutines.ContinuationInterceptor +import kotlin.coroutines.CoroutineContext +import kotlin.coroutines.startCoroutine +import kotlin.test.Test +import kotlin.test.assertTrue + +@MicronautTest +class SuspendFunctionInterceptorSpec { + + @Inject + lateinit var demoClient: DemoClient + + @Test + fun interceptSuspendMethod() { + val interceptor = TestCoroutineInterceptor() + val latch = CountDownLatch(1) + var answer: String? = null + demoClient::getSyncString.startCoroutine(Continuation(interceptor) { result -> + answer = result.getOrNull() + latch.countDown() + }) + latch.await(1, TimeUnit.SECONDS) shouldBe true + assertTrue(interceptor.didIntercept()) + answer shouldBe "sync string" + } + + @Test + fun returnToCallerThreadWithSuspendClient() { + val singleThreadDispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher() + runBlocking { + launch(singleThreadDispatcher) { + val threadId = Thread.currentThread().id + demoClient.getSyncString() shouldBe "sync string" + Thread.currentThread().id shouldBe threadId + } + } + } + + @Client("/demo") + @Consumes(MediaType.TEXT_PLAIN) + interface DemoClient { + @Get("/sync/string") + suspend fun getSyncString(): String + } + + class TestCoroutineInterceptor : ContinuationInterceptor { + private val didIntercept = AtomicBoolean(false) + + fun didIntercept() = didIntercept.get() + + override val key: CoroutineContext.Key<*> + get() = ContinuationInterceptor.Key + + override fun interceptContinuation(continuation: Continuation): Continuation { + return InterceptedContinuation(didIntercept, continuation) + } + + class InterceptedContinuation( + private val didIntercept: AtomicBoolean, + private val continuation: Continuation + ) : Continuation { + override val context: CoroutineContext + get() = continuation.context + + override fun resumeWith(result: Result) { + if (result as Any? !== Unit) { // startCoroutine directly calls resumeWith(Unit) after starting the coroutine + didIntercept.set(true) + } + continuation.resumeWith(result) + } + } + } +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/annotation/processing/SuspendMethodSpec.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/annotation/processing/SuspendMethodSpec.kt new file mode 100644 index 00000000000..b10327e3bc2 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/annotation/processing/SuspendMethodSpec.kt @@ -0,0 +1,81 @@ +/* + * Copyright 2017-2021 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.annotation.processing + +import io.micronaut.http.HttpRequest +import io.micronaut.http.client.HttpClient +import io.micronaut.http.client.annotation.Client +import io.micronaut.test.extensions.junit5.annotation.MicronautTest +import org.junit.jupiter.api.Test +import reactor.core.publisher.Flux +import javax.inject.Inject +import kotlin.test.assertEquals + +// issue https://github.com/micronaut-projects/micronaut-core/issues/5396 +@MicronautTest +class SuspendMethodSpec { + + @Inject + @field:Client("/demo") + lateinit var client: HttpClient + + @Test + fun testSyncMethodReturnTypeAny() { + val res = Flux.from(client + .retrieve( + HttpRequest.GET("/sync/any"), + Any::class.java + )).blockFirst() + + assertEquals("sync any", res) + } + + @Test + fun testSyncMethodReturnTypeString() { + val res = Flux.from(client + .retrieve( + HttpRequest.GET("/sync/string"), + Any::class.java + )).blockFirst() + + assertEquals("sync string", res) + } + + + @Test + fun testAsyncMethodReturnTypeAny() { + val res = Flux.from(client + .retrieve( + HttpRequest.GET("/async/any"), + Any::class.java + )).blockFirst() + + assertEquals("async any", res) + } + + @Test + fun testAsyncMethodReturnTypeString() { + val res = Flux.from(client + .retrieve( + HttpRequest.GET("/async/string"), + Any::class.java + )).blockFirst() + + assertEquals("async string", res) + } + + +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/core/beans/AbstractTestEntity.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/core/beans/AbstractTestEntity.kt new file mode 100644 index 00000000000..4f23eeea784 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/core/beans/AbstractTestEntity.kt @@ -0,0 +1,23 @@ +package io.micronaut.core.beans + +abstract class AbstractTestEntity( + var id: Long, + var name: String, + var getSurname: String, + var isDeleted: Boolean, + val isImportant: Boolean, + var corrected: Boolean, + val upgraded: Boolean, +) { + val isMyBool: Boolean + get() = false + var isMyBool2: Boolean + get() = false + set(v) {} + var myBool3: Boolean + get() = false + set(v) {} + val myBool4: Boolean + get() = false + var myBool5: Boolean = false +} \ No newline at end of file diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/core/beans/Item.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/core/beans/Item.kt new file mode 100644 index 00000000000..9407455d76e --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/core/beans/Item.kt @@ -0,0 +1,28 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.core.beans + + +import io.micronaut.core.annotation.Introspected +import java.util.* + +@Introspected +abstract class Item> { + + var id: Long? = null + + var revisions: MutableList = ArrayList() +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/core/beans/KotlinBeanIntrospectionSpec.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/core/beans/KotlinBeanIntrospectionSpec.kt new file mode 100644 index 00000000000..ec5b3dc7e17 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/core/beans/KotlinBeanIntrospectionSpec.kt @@ -0,0 +1,37 @@ +package io.micronaut.core.beans + +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertThrows +import org.junit.jupiter.api.Test +import io.micronaut.core.reflect.exception.InstantiationException + +class KotlinBeanIntrospectionSpec { + + @Test + fun testWithValueOnKotlinDataClassWithDefaultValues() { + val introspection = BeanIntrospection.getIntrospection(SomeEntity::class.java) + + val instance = introspection.instantiate(10L, "foo") + + assertEquals(10, instance.id) + assertEquals("foo", instance.something) + + val changed = introspection.getRequiredProperty("something", String::class.java) + .withValue(instance, "changed") + + assertEquals(10, changed.id) + assertEquals("changed", changed.something) + + } + + @Test + fun testIsProperties() { + val introspection = BeanIntrospection.getIntrospection(TestEntity::class.java) + + assertEquals(listOf("id", "name", "getSurname", "isDeleted", "isImportant", "corrected", "upgraded", "isMyBool", "isMyBool2", "myBool3", "myBool4", "myBool5"), introspection.propertyNames.asList()) + + val introspection2 = BeanIntrospection.getIntrospection(TestEntity2::class.java) + + assertEquals(listOf("id", "name", "getSurname", "isDeleted", "isImportant", "corrected", "upgraded", "isMyBool", "isMyBool2", "myBool3", "myBool4", "myBool5"), introspection2.propertyNames.asList()) + } +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/core/beans/RecusiveGenericsSpec.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/core/beans/RecusiveGenericsSpec.kt new file mode 100644 index 00000000000..50d645513c4 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/core/beans/RecusiveGenericsSpec.kt @@ -0,0 +1,15 @@ +package io.micronaut.core.beans + +import org.junit.jupiter.api.Assertions.assertNotNull +import org.junit.jupiter.api.Test + +class RecusiveGenericsSpec { + + // issue https://github.com/micronaut-projects/micronaut-core/issues/1607 + @Test + fun testRecursiveGenericsOnBeanIntrospection() { + val introspection = BeanIntrospection.getIntrospection(Item::class.java) + // just check compilation works + assertNotNull(introspection) + } +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/core/beans/SomeEntity.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/core/beans/SomeEntity.kt new file mode 100644 index 00000000000..a6ac35a377d --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/core/beans/SomeEntity.kt @@ -0,0 +1,10 @@ +package io.micronaut.core.beans + +import io.micronaut.core.annotation.Creator +import io.micronaut.core.annotation.Introspected + +@Introspected +data class SomeEntity @Creator constructor( + val id: Long? = null, + val something: String? = null +) diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/core/beans/TestEntity.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/core/beans/TestEntity.kt new file mode 100644 index 00000000000..8ee06c6e0eb --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/core/beans/TestEntity.kt @@ -0,0 +1,26 @@ +package io.micronaut.core.beans + +import io.micronaut.core.annotation.Introspected + +@Introspected +class TestEntity( + var id: Long, + var name: String, + var getSurname: String, + var isDeleted: Boolean, + val isImportant: Boolean, + var corrected: Boolean, + val upgraded: Boolean, +) { + val isMyBool: Boolean + get() = false + var isMyBool2: Boolean + get() = false + set(v) {} + var myBool3: Boolean + get() = false + set(v) {} + val myBool4: Boolean + get() = false + var myBool5: Boolean = false +} \ No newline at end of file diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/core/beans/TestEntity2.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/core/beans/TestEntity2.kt new file mode 100644 index 00000000000..0ca460691c6 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/core/beans/TestEntity2.kt @@ -0,0 +1,7 @@ +package io.micronaut.core.beans + +import io.micronaut.core.annotation.Introspected + +@Introspected +class TestEntity2(id: Long, name: String, getSurname: String, isDeleted: Boolean, isImportant: Boolean, corrected: Boolean, upgraded: Boolean) : AbstractTestEntity(id, name, getSurname, isDeleted, isImportant, corrected, upgraded) { +} \ No newline at end of file diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/annotation/Pet.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/annotation/Pet.kt new file mode 100644 index 00000000000..de1c94e98ee --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/annotation/Pet.kt @@ -0,0 +1,28 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.annotation + +/** + * @author graemerocher + * @since 1.0 + */ + +// tag::class[] +class Pet { + var name: String? = null + var age: Int = 0 +} +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/annotation/PetClient.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/annotation/PetClient.kt new file mode 100644 index 00000000000..123b200c1e6 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/annotation/PetClient.kt @@ -0,0 +1,31 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.annotation + +// tag::imports[] +import io.micronaut.http.client.annotation.Client +import io.micronaut.core.async.annotation.SingleResult +import org.reactivestreams.Publisher +// end::imports[] + +// tag::class[] +@Client("/pets") // <1> +interface PetClient : PetOperations { // <2> + + @SingleResult + override fun save(name: String, age: Int): Publisher // <3> +} +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/annotation/PetController.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/annotation/PetController.kt new file mode 100644 index 00000000000..005d525b269 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/annotation/PetController.kt @@ -0,0 +1,38 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.annotation + +// tag::imports[] +import io.micronaut.http.annotation.Controller +import reactor.core.publisher.Mono +import io.micronaut.core.async.annotation.SingleResult +import org.reactivestreams.Publisher +// end::imports[] + +// tag::class[] +@Controller("/pets") +open class PetController : PetOperations { + + @SingleResult + override fun save(name: String, age: Int): Publisher { + val pet = Pet() + pet.name = name + pet.age = age + // save to database or something + return Mono.just(pet) + } +} +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/annotation/PetControllerSpec.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/annotation/PetControllerSpec.kt new file mode 100644 index 00000000000..3e60c89d5da --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/annotation/PetControllerSpec.kt @@ -0,0 +1,52 @@ +package io.micronaut.docs.annotation + +import io.kotest.matchers.shouldBe +import io.kotest.core.spec.style.StringSpec +import io.micronaut.context.ApplicationContext +import io.micronaut.http.client.HttpClient +import io.micronaut.runtime.server.EmbeddedServer +import reactor.core.publisher.Mono + +import javax.validation.ConstraintViolationException + +import java.lang.Exception + +class PetControllerSpec: StringSpec() { + + val embeddedServer = autoClose( + ApplicationContext.run(EmbeddedServer::class.java) + ) + + val client = autoClose( + embeddedServer.applicationContext.createBean(HttpClient::class.java, embeddedServer.url) + ) + + init { + "test post pet" { + val client = embeddedServer.applicationContext.getBean(PetClient::class.java) + + // tag::post[] + val pet = Mono.from(client.save("Dino", 10)).block() + + pet.name shouldBe "Dino" + pet.age.toLong() shouldBe 10 + // end::post[] + } + + "test post pet validation" { + val client = embeddedServer.applicationContext.getBean(PetClient::class.java) + + // tag::error[] + try { + Mono.from(client.save("Fred", -1)).block() + } catch (e: Exception) { + e.javaClass shouldBe ConstraintViolationException::class.java + e.message shouldBe "save.age: must be greater than or equal to 1" + } + // end::error[] + } + } + + // tag::errorRule[] + // end::errorRule[] +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/annotation/PetOperations.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/annotation/PetOperations.kt new file mode 100644 index 00000000000..19816ea8a64 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/annotation/PetOperations.kt @@ -0,0 +1,36 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.annotation + +// tag::imports[] +import io.micronaut.http.annotation.Post +import io.micronaut.validation.Validated +import javax.validation.constraints.Min +import javax.validation.constraints.NotBlank +import io.micronaut.core.async.annotation.SingleResult +import org.reactivestreams.Publisher +// end::imports[] + +// tag::class[] +@Validated +interface PetOperations { + // tag::save[] + @Post + @SingleResult + fun save(@NotBlank name: String, @Min(1L) age: Int): Publisher + // end::save[] +} +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/annotation/headers/HeaderSpec.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/annotation/headers/HeaderSpec.kt new file mode 100644 index 00000000000..67b5adf3b10 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/annotation/headers/HeaderSpec.kt @@ -0,0 +1,27 @@ +package io.micronaut.docs.annotation.headers + +import io.kotest.core.spec.style.StringSpec +import io.kotest.matchers.shouldBe +import io.kotest.matchers.shouldNotBe + +import io.micronaut.context.ApplicationContext +import io.micronaut.runtime.server.EmbeddedServer +import reactor.core.publisher.Mono + +class HeaderSpec: StringSpec() { + + val embeddedServer = autoClose( + ApplicationContext.run(EmbeddedServer::class.java, mapOf("pet.client.id" to "11") ) + ) + + init { + "test sender headers" { + val client = embeddedServer.applicationContext.getBean(PetClient::class.java) + + val pet = Mono.from(client["Fred"]).block() + + pet shouldNotBe null + pet.age.toLong() shouldBe 11 + } + } +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/annotation/headers/PetClient.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/annotation/headers/PetClient.kt new file mode 100644 index 00000000000..e44b099c0b3 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/annotation/headers/PetClient.kt @@ -0,0 +1,39 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.annotation.headers + +import io.micronaut.docs.annotation.Pet +import io.micronaut.docs.annotation.PetOperations +import io.micronaut.http.annotation.Get +import io.micronaut.http.annotation.Header +import io.micronaut.http.client.annotation.Client +import org.reactivestreams.Publisher +import io.micronaut.core.async.annotation.SingleResult +import reactor.core.publisher.Mono + +// tag::class[] +@Client("/pets") +@Header(name = "X-Pet-Client", value = "\${pet.client.id}") +interface PetClient : PetOperations { + + @SingleResult + override fun save(name: String, age: Int): Publisher + + @Get("/{name}") + @SingleResult + operator fun get(name: String): Publisher +} +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/annotation/headers/PetController.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/annotation/headers/PetController.kt new file mode 100644 index 00000000000..f72abd76e25 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/annotation/headers/PetController.kt @@ -0,0 +1,35 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.annotation.headers + +import io.micronaut.docs.annotation.Pet +import io.micronaut.http.HttpResponse +import io.micronaut.http.annotation.Controller +import io.micronaut.http.annotation.Get +import io.micronaut.http.annotation.Header + +@Controller("/pets") +class PetController { + + @Get("/{name}") + operator fun get(name: String, @Header("X-Pet-Client") clientId: String): HttpResponse { + val pet = Pet() + pet.name = name + pet.age = Integer.valueOf(clientId) + return HttpResponse.ok(pet) + .header("X-Pet-Client", clientId) + } +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/annotation/requestattributes/RequestAttributeSpec.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/annotation/requestattributes/RequestAttributeSpec.kt new file mode 100644 index 00000000000..91c4edb809a --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/annotation/requestattributes/RequestAttributeSpec.kt @@ -0,0 +1,32 @@ +package io.micronaut.docs.annotation.requestattributes + +import io.kotest.matchers.shouldBe +import io.kotest.matchers.shouldNotBe +import io.kotest.core.spec.style.StringSpec +import io.micronaut.context.ApplicationContext +import io.micronaut.runtime.server.EmbeddedServer +import reactor.core.publisher.Mono + +class RequestAttributeSpec: StringSpec() { + + val embeddedServer = autoClose( + ApplicationContext.run(EmbeddedServer::class.java) + ) + + init { + "test sender attributes" { + val client = embeddedServer.applicationContext.getBean(StoryClient::class.java) + val filter = embeddedServer.applicationContext.getBean(StoryClientFilter::class.java) + + val story = Mono.from(client.getById("jan2019")).block() + val attributes = filter.latestRequestAttributes + + story shouldNotBe null + attributes shouldNotBe null + + attributes.get("story-id") shouldBe "jan2019" + attributes.get("client-name") shouldBe "storyClient" + attributes.get("version") shouldBe "1" + } + } +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/annotation/requestattributes/Story.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/annotation/requestattributes/Story.kt new file mode 100644 index 00000000000..c4835562120 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/annotation/requestattributes/Story.kt @@ -0,0 +1,23 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.annotation.requestattributes + +// tag::class[] +class Story { + var id: String? = null + var title: String? = null +} +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/annotation/requestattributes/StoryClient.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/annotation/requestattributes/StoryClient.kt new file mode 100644 index 00000000000..3e6ba9c8c76 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/annotation/requestattributes/StoryClient.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.annotation.requestattributes + +import io.micronaut.http.annotation.Get +import io.micronaut.http.annotation.RequestAttribute +import io.micronaut.http.annotation.RequestAttributes +import io.micronaut.http.client.annotation.Client +import io.micronaut.core.async.annotation.SingleResult +import org.reactivestreams.Publisher + +// tag::class[] +@Client("/story") +@RequestAttributes(RequestAttribute(name = "client-name", value = "storyClient"), RequestAttribute(name = "version", value = "1")) +interface StoryClient { + + @Get("/{storyId}") + @SingleResult + fun getById(@RequestAttribute storyId: String): Publisher +} +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/annotation/requestattributes/StoryClientFilter.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/annotation/requestattributes/StoryClientFilter.kt new file mode 100644 index 00000000000..6c4809d2d79 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/annotation/requestattributes/StoryClientFilter.kt @@ -0,0 +1,42 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.annotation.requestattributes + +import io.micronaut.http.HttpResponse +import io.micronaut.http.MutableHttpRequest +import io.micronaut.http.annotation.Filter +import io.micronaut.http.filter.ClientFilterChain +import io.micronaut.http.filter.HttpClientFilter +import org.reactivestreams.Publisher + +import java.util.HashMap + +@Filter("/story/**") +class StoryClientFilter : HttpClientFilter { + + private var attributes: Map? = null + + /** + * strictly for unit testing + */ + internal val latestRequestAttributes: Map + get() = HashMap(attributes!!) + + override fun doFilter(request: MutableHttpRequest<*>, chain: ClientFilterChain): Publisher> { + attributes = request.attributes.asMap() + return chain.proceed(request) + } +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/annotation/requestattributes/StoryController.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/annotation/requestattributes/StoryController.kt new file mode 100644 index 00000000000..87306e9becf --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/annotation/requestattributes/StoryController.kt @@ -0,0 +1,31 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.annotation.requestattributes + +import io.micronaut.http.HttpResponse +import io.micronaut.http.annotation.Controller +import io.micronaut.http.annotation.Get + +@Controller("/story") +class StoryController { + + @Get("/{id}") + operator fun get(id: String): HttpResponse { + val story = Story() + story.id = id + return HttpResponse.ok(story) + } +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/annotation/retry/PetClient.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/annotation/retry/PetClient.kt new file mode 100644 index 00000000000..442e937c601 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/annotation/retry/PetClient.kt @@ -0,0 +1,31 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.annotation.retry + +import io.micronaut.docs.annotation.Pet +import io.micronaut.docs.annotation.PetOperations +import io.micronaut.http.client.annotation.Client +import io.micronaut.retry.annotation.Retryable +import reactor.core.publisher.Mono + +// tag::class[] +@Client("/pets") +@Retryable +interface PetClient : PetOperations { + + override fun save(name: String, age: Int): Mono +} +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/annotation/retry/PetFallback.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/annotation/retry/PetFallback.kt new file mode 100644 index 00000000000..394b38e7b66 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/annotation/retry/PetFallback.kt @@ -0,0 +1,33 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.annotation.retry + +import io.micronaut.docs.annotation.Pet +import io.micronaut.docs.annotation.PetOperations +import io.micronaut.retry.annotation.Fallback +import reactor.core.publisher.Mono + +// tag::class[] +@Fallback +open class PetFallback : PetOperations { + override fun save(name: String, age: Int): Mono { + val pet = Pet() + pet.age = age + pet.name = name + return Mono.just(pet) + } +} +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/aop/advice/MyBean.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/aop/advice/MyBean.kt new file mode 100644 index 00000000000..ccbc92514b0 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/aop/advice/MyBean.kt @@ -0,0 +1,18 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.aop.advice + +open class MyBean diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/aop/advice/Timed.java b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/aop/advice/Timed.java new file mode 100644 index 00000000000..26623527f47 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/aop/advice/Timed.java @@ -0,0 +1,19 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.aop.advice; + +public @interface Timed { +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/aop/advice/method/MyFactory.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/aop/advice/method/MyFactory.kt new file mode 100644 index 00000000000..2f16febc202 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/aop/advice/method/MyFactory.kt @@ -0,0 +1,33 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.aop.advice.method + +import io.micronaut.context.annotation.Factory +import io.micronaut.context.annotation.Prototype +import io.micronaut.docs.aop.advice.MyBean +import io.micronaut.docs.aop.advice.Timed + +// tag::class[] +@Factory +open class MyFactory { + + @Prototype + @Timed + open fun myBean(): MyBean { + return MyBean() + } +} +// end::class[] \ No newline at end of file diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/aop/advice/type/MyFactory.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/aop/advice/type/MyFactory.kt new file mode 100644 index 00000000000..c58cf6cc4b0 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/aop/advice/type/MyFactory.kt @@ -0,0 +1,33 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.aop.advice.type + +import io.micronaut.context.annotation.Factory +import io.micronaut.context.annotation.Prototype +import io.micronaut.docs.aop.advice.MyBean +import io.micronaut.docs.aop.advice.Timed + +// tag::class[] +@Timed +@Factory +open class MyFactory { + + @Prototype + open fun myBean(): MyBean { + return MyBean() + } +} +// end::class[] \ No newline at end of file diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/aop/around/AroundSpec.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/aop/around/AroundSpec.kt new file mode 100644 index 00000000000..04e38d277e1 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/aop/around/AroundSpec.kt @@ -0,0 +1,23 @@ +package io.micronaut.docs.aop.around + +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.matchers.shouldBe +import io.kotest.core.spec.style.AnnotationSpec +import io.micronaut.context.ApplicationContext + +class AroundSpec: AnnotationSpec() { + + // tag::test[] + @Test + fun testNotNull() { + val applicationContext = ApplicationContext.run() + val exampleBean = applicationContext.getBean(NotNullExample::class.java) + + val exception = shouldThrow { + exampleBean.doWork(null) + } + exception.message shouldBe "Null parameter [taskName] not allowed" + applicationContext.close() + } + // end::test[] +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/aop/around/NotNull.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/aop/around/NotNull.kt new file mode 100644 index 00000000000..6dbe67012af --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/aop/around/NotNull.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.aop.around + +// tag::imports[] +import io.micronaut.aop.Around +import kotlin.annotation.AnnotationRetention.RUNTIME +import kotlin.annotation.AnnotationTarget.CLASS +import kotlin.annotation.AnnotationTarget.FILE +import kotlin.annotation.AnnotationTarget.FUNCTION +import kotlin.annotation.AnnotationTarget.PROPERTY_GETTER +import kotlin.annotation.AnnotationTarget.PROPERTY_SETTER +// end::imports[] + +// tag::annotation[] +@MustBeDocumented +@Retention(RUNTIME) // <1> +@Target(CLASS, FILE, FUNCTION, PROPERTY_GETTER, PROPERTY_SETTER) // <2> +@Around // <3> +annotation class NotNull +// end::annotation[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/aop/around/NotNullExample.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/aop/around/NotNullExample.kt new file mode 100644 index 00000000000..c490943deda --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/aop/around/NotNullExample.kt @@ -0,0 +1,29 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.aop.around + +// tag::example[] +import jakarta.inject.Singleton + +@Singleton +open class NotNullExample { + + @NotNull + open fun doWork(taskName: String?) { + println("Doing job: $taskName") + } +} +// end::example[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/aop/around/NotNullInterceptor.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/aop/around/NotNullInterceptor.kt new file mode 100644 index 00000000000..4bbb58a9e05 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/aop/around/NotNullInterceptor.kt @@ -0,0 +1,46 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.aop.around + +// tag::imports[] +import io.micronaut.aop.InterceptorBean +import io.micronaut.aop.MethodInterceptor +import io.micronaut.aop.MethodInvocationContext +import java.util.Objects +import jakarta.inject.Singleton +// end::imports[] + +// tag::interceptor[] +@Singleton +@InterceptorBean(NotNull::class) // <1> +class NotNullInterceptor : MethodInterceptor { // <2> + override fun intercept(context: MethodInvocationContext): Any? { + val nullParam = context.parameters + .entries + .stream() + .filter { entry -> + val argumentValue = entry.value + Objects.isNull(argumentValue.value) + } + .findFirst() // <3> + return if (nullParam.isPresent) { + throw IllegalArgumentException("Null parameter [${nullParam.get().key}] not allowed") // <4> + } else { + context.proceed() // <5> + } + } +} +// end::interceptor[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/aop/introduction/IntroductionSpec.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/aop/introduction/IntroductionSpec.kt new file mode 100644 index 00000000000..4a6ecc78524 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/aop/introduction/IntroductionSpec.kt @@ -0,0 +1,22 @@ +package io.micronaut.docs.aop.introduction + +import io.kotest.matchers.shouldBe +import io.kotest.core.spec.style.AnnotationSpec +import io.micronaut.context.ApplicationContext + +class IntroductionSpec: AnnotationSpec() { + + @Test + fun testStubIntroduction() { + val applicationContext = ApplicationContext.run() + + // tag::test[] + val stubExample = applicationContext.getBean(StubExample::class.java) + + stubExample.number.shouldBe(10) + stubExample.date.shouldBe(null) + // end::test[] + + applicationContext.stop() + } +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/aop/introduction/Stub.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/aop/introduction/Stub.kt new file mode 100644 index 00000000000..5f16d064ec6 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/aop/introduction/Stub.kt @@ -0,0 +1,37 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.aop.introduction + +// tag::imports[] +import io.micronaut.aop.Introduction +import io.micronaut.context.annotation.Bean +import kotlin.annotation.AnnotationRetention.RUNTIME +import kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS +import kotlin.annotation.AnnotationTarget.CLASS +import kotlin.annotation.AnnotationTarget.FILE +import kotlin.annotation.AnnotationTarget.FUNCTION +import kotlin.annotation.AnnotationTarget.PROPERTY_GETTER +import kotlin.annotation.AnnotationTarget.PROPERTY_SETTER +// end::imports[] + +// tag::class[] +@Introduction // <1> +@Bean // <2> +@MustBeDocumented +@Retention(RUNTIME) +@Target(CLASS, FILE, ANNOTATION_CLASS, FUNCTION, PROPERTY_GETTER, PROPERTY_SETTER) +annotation class Stub(val value: String = "") +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/aop/introduction/StubExample.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/aop/introduction/StubExample.kt new file mode 100644 index 00000000000..34aaee46d26 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/aop/introduction/StubExample.kt @@ -0,0 +1,29 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.aop.introduction + +import java.time.LocalDateTime + +// tag::class[] +@Stub +interface StubExample { + + @get:Stub("10") + val number: Int + + val date: LocalDateTime? +} +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/aop/introduction/StubIntroduction.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/aop/introduction/StubIntroduction.kt new file mode 100644 index 00000000000..134762d1dfe --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/aop/introduction/StubIntroduction.kt @@ -0,0 +1,35 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.aop.introduction + +// tag::imports[] +import io.micronaut.aop.* +import jakarta.inject.Singleton +// end::imports[] + +// tag::class[] +@Singleton +@InterceptorBean(Stub::class) // <1> +class StubIntroduction : MethodInterceptor { // <2> + + override fun intercept(context: MethodInvocationContext): Any? { + return context.getValue( // <3> + Stub::class.java, + context.returnType.type + ).orElse(null) // <4> + } +} +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/aop/lifecycle/LifeCycleAdviseSpec.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/aop/lifecycle/LifeCycleAdviseSpec.kt new file mode 100644 index 00000000000..13054c28666 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/aop/lifecycle/LifeCycleAdviseSpec.kt @@ -0,0 +1,24 @@ +package io.micronaut.docs.aop.lifecycle + +import io.micronaut.context.ApplicationContext +import org.junit.jupiter.api.Assertions.assertFalse +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test + +class LifeCycleAdviseSpec { + @Test + fun testLifeCycleAdvise() { + ApplicationContext.run().use { applicationContext -> + val productService = + applicationContext.getBean(ProductService::class.java) + val product = + applicationContext.createBean(Product::class.java, "Apple") // + assertTrue(product.active) + assertTrue(productService.findProduct("APPLE").isPresent) + + applicationContext.destroyBean(product) + assertFalse(product.active) + assertFalse(productService.findProduct("APPLE").isPresent) + } + } +} \ No newline at end of file diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/aop/lifecycle/Product.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/aop/lifecycle/Product.kt new file mode 100644 index 00000000000..4ba20db623b --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/aop/lifecycle/Product.kt @@ -0,0 +1,18 @@ +package io.micronaut.docs.aop.lifecycle + +// tag::imports[] +import io.micronaut.context.annotation.Parameter +import jakarta.annotation.PreDestroy +// end::imports[] + +// tag::class[] +@ProductBean // <1> +class Product(@param:Parameter val productName: String ) { // <2> + + var active: Boolean = false + @PreDestroy + fun disable() { // <3> + active = false + } +} +// end::class[] \ No newline at end of file diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/aop/lifecycle/ProductBean.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/aop/lifecycle/ProductBean.kt new file mode 100644 index 00000000000..2e7cb40879d --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/aop/lifecycle/ProductBean.kt @@ -0,0 +1,20 @@ +package io.micronaut.docs.aop.lifecycle + +// tag::imports[] +import io.micronaut.aop.AroundConstruct +import io.micronaut.aop.InterceptorBinding +import io.micronaut.aop.InterceptorBindingDefinitions +import io.micronaut.aop.InterceptorKind +import io.micronaut.context.annotation.Prototype +// end::imports[] + +// tag::class[] +@Retention(AnnotationRetention.RUNTIME) +@AroundConstruct // <1> +@InterceptorBindingDefinitions( + InterceptorBinding(kind = InterceptorKind.POST_CONSTRUCT), // <2> + InterceptorBinding(kind = InterceptorKind.PRE_DESTROY) // <3> +) +@Prototype // <4> +annotation class ProductBean +// end::class[] \ No newline at end of file diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/aop/lifecycle/ProductInterceptors.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/aop/lifecycle/ProductInterceptors.kt new file mode 100644 index 00000000000..d1b8007a71e --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/aop/lifecycle/ProductInterceptors.kt @@ -0,0 +1,51 @@ +package io.micronaut.docs.aop.lifecycle + +// tag::imports[] +import io.micronaut.aop.* +import io.micronaut.context.annotation.Factory +// end::imports[] + +// tag::class[] +@Factory +class ProductInterceptors(private val productService: ProductService) { +// end::class[] + + // tag::constructor-interceptor[] + @InterceptorBean(ProductBean::class) + fun aroundConstruct(): ConstructorInterceptor { // <1> + return ConstructorInterceptor { context: ConstructorInvocationContext -> + val parameterValues = context.parameterValues // <2> + val parameterValue = parameterValues[0] + require(!(parameterValue == null || parameterValues[0].toString().isEmpty())) { "Invalid product name" } + val productName = parameterValues[0].toString().uppercase() + parameterValues[0] = productName + val product = context.proceed() // <3> + productService.addProduct(product) + product + } + } + // end::constructor-interceptor[] + + // tag::method-interceptor[] + @InterceptorBean(ProductBean::class) + fun aroundInvoke(): MethodInterceptor { // <1> + return MethodInterceptor { context: MethodInvocationContext -> + val product = context.target + return@MethodInterceptor when (context.kind) { + InterceptorKind.POST_CONSTRUCT -> { // <2> + product.active = true + context.proceed() + } + InterceptorKind.PRE_DESTROY -> { // <3> + productService.removeProduct(product) + context.proceed() + } + else -> context.proceed() + } + } + } + // end::method-interceptor[] + +// tag::class[] +} +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/aop/lifecycle/ProductService.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/aop/lifecycle/ProductService.kt new file mode 100644 index 00000000000..5c3ea51248d --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/aop/lifecycle/ProductService.kt @@ -0,0 +1,25 @@ +package io.micronaut.docs.aop.lifecycle + +// tag::imports[] +import java.util.* +import jakarta.inject.Singleton +// end::imports[] + +// tag::class[] +@Singleton +class ProductService { + private val products: MutableMap = HashMap() + fun addProduct(product: Product) { + products[product.productName] = product + } + + fun removeProduct(product: Product) { + product.active = false + products.remove(product.productName) + } + + fun findProduct(name: String): Optional { + return Optional.ofNullable(products[name]) + } +} +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/aop/retry/Book.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/aop/retry/Book.kt new file mode 100644 index 00000000000..a9cb47a8067 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/aop/retry/Book.kt @@ -0,0 +1,18 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.aop.retry + +class Book(val title: String) diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/aop/retry/BookService.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/aop/retry/BookService.kt new file mode 100644 index 00000000000..76901cc8ca5 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/aop/retry/BookService.kt @@ -0,0 +1,67 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.aop.retry + +import io.micronaut.retry.annotation.CircuitBreaker +import io.micronaut.retry.annotation.Retryable +import reactor.core.publisher.Flux + +open class BookService { + + // tag::simple[] + @Retryable + open fun listBooks(): List { + // ... + // end::simple[] + return listOf(Book("The Stand")) + } + + // tag::circuit[] + @CircuitBreaker(reset = "30s") + open fun findBooks(): List { + // ... + // end::circuit[] + return listOf(Book("The Stand")) + } + + // tag::attempts[] + @Retryable(attempts = "5", + delay = "2s") + open fun findBook(title: String): Book { + // ... + // end::attempts[] + return Book(title) + } + + // tag::config[] + @Retryable(attempts = "\${book.retry.attempts:3}", + delay = "\${book.retry.delay:1s}") + open fun getBook(title: String): Book { + // ... + // end::config[] + return Book(title) + } + + // tag::reactive[] + @Retryable + open fun streamBooks(): Flux { + // ... + // end::reactive[] + return Flux.just( + Book("The Stand") + ) + } +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/aop/scheduled/ScheduledExample.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/aop/scheduled/ScheduledExample.kt new file mode 100644 index 00000000000..92dcb1f677b --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/aop/scheduled/ScheduledExample.kt @@ -0,0 +1,60 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.aop.scheduled + +import io.micronaut.scheduling.annotation.Scheduled + +import jakarta.inject.Singleton + +@Singleton +class ScheduledExample { + + // tag::fixedRate[] + @Scheduled(fixedRate = "5m") + internal fun everyFiveMinutes() { + println("Executing everyFiveMinutes()") + } + // end::fixedRate[] + + // tag::fixedDelay[] + @Scheduled(fixedDelay = "5m") + internal fun fiveMinutesAfterLastExecution() { + println("Executing fiveMinutesAfterLastExecution()") + } + // end::fixedDelay[] + + // tag::cron[] + @Scheduled(cron = "0 15 10 ? * MON") + internal fun everyMondayAtTenFifteenAm() { + println("Executing everyMondayAtTenFifteenAm()") + } + // end::cron[] + + // tag::initialDelay[] + @Scheduled(initialDelay = "1m") + internal fun onceOneMinuteAfterStartup() { + println("Executing onceOneMinuteAfterStartup()") + } + // end::initialDelay[] + + // tag::configured[] + @Scheduled(fixedRate = "\${my.task.rate:5m}", + initialDelay = "\${my.task.delay:1m}") + internal fun configuredTask() { + println("Executing configuredTask()") + } + // end::configured[] +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/aop/scheduled/TaskSchedulerInjectExample.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/aop/scheduled/TaskSchedulerInjectExample.kt new file mode 100644 index 00000000000..299a1498892 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/aop/scheduled/TaskSchedulerInjectExample.kt @@ -0,0 +1,30 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.aop.scheduled + +import io.micronaut.scheduling.TaskExecutors +import io.micronaut.scheduling.TaskScheduler + +import jakarta.inject.Inject +import jakarta.inject.Named + +class TaskSchedulerInjectExample { + // tag::inject[] + @Inject + @Named(TaskExecutors.SCHEDULED) + lateinit var taskScheduler: TaskScheduler + // end::inject[] +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/basics/Book.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/basics/Book.kt new file mode 100644 index 00000000000..ef012b15c34 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/basics/Book.kt @@ -0,0 +1,33 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.basics + +import com.fasterxml.jackson.annotation.JsonCreator +import com.fasterxml.jackson.annotation.JsonProperty +import io.micronaut.core.annotation.Introspected + +@Introspected +class Book { + + var title: String? = null + + @JsonCreator + constructor(@JsonProperty("title") title: String) { + this.title = title + } + + internal constructor() {} +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/basics/BookController.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/basics/BookController.kt new file mode 100644 index 00000000000..48209af31a5 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/basics/BookController.kt @@ -0,0 +1,32 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.basics + +import io.micronaut.http.HttpStatus +import io.micronaut.http.MediaType +import io.micronaut.http.annotation.Controller +import io.micronaut.http.annotation.Post +import io.micronaut.http.annotation.Status + +@Controller("/amazon") +class BookController { + + @Post(value = "/book/{title}", consumes = [MediaType.APPLICATION_JSON, MediaType.APPLICATION_FORM_URLENCODED]) + @Status(HttpStatus.CREATED) + internal fun save(title: String): Book { + return Book(title) + } +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/basics/BookControllerSpec.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/basics/BookControllerSpec.kt new file mode 100644 index 00000000000..08e9633ab9c --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/basics/BookControllerSpec.kt @@ -0,0 +1,59 @@ +package io.micronaut.docs.basics + +import io.kotest.matchers.shouldBe +import io.kotest.core.spec.style.StringSpec +import io.micronaut.context.ApplicationContext +import io.micronaut.http.HttpRequest.POST +import io.micronaut.http.HttpStatus +import io.micronaut.http.MediaType +import io.micronaut.http.client.HttpClient +import io.micronaut.runtime.server.EmbeddedServer +import reactor.core.publisher.Flux + +class BookControllerSpec: StringSpec() { + + val embeddedServer = autoClose( + ApplicationContext.run(EmbeddedServer::class.java) + ) + + val client = autoClose( + embeddedServer.applicationContext.createBean(HttpClient::class.java, embeddedServer.url) + ) + + init { + "test post with uri template" { + // tag::posturitemplate[] + val call = client.exchange( + POST("/amazon/book/{title}", Book("The Stand")), + Book::class.java + ) + // end::posturitemplate[] + + val response = Flux.from(call).blockFirst() + val message = response.getBody(Book::class.java) // <2> + // check the status + response.status shouldBe HttpStatus.CREATED // <3> + // check the body + message.isPresent shouldBe true + message.get().title shouldBe "The Stand" + } + + "test post with form data" { + // tag::postform[] + val call = client.exchange( + POST("/amazon/book/{title}", Book("The Stand")) + .contentType(MediaType.APPLICATION_FORM_URLENCODED), + Book::class.java + ) + // end::postform[] + + val response = Flux.from(call).blockFirst() + val message = response.getBody(Book::class.java) // <2> + // check the status + response.status shouldBe HttpStatus.CREATED // <3> + // check the body + message.isPresent shouldBe true + message.get().title shouldBe "The Stand" + } + } +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/basics/HelloController.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/basics/HelloController.kt new file mode 100644 index 00000000000..20b8d027bbb --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/basics/HelloController.kt @@ -0,0 +1,71 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.basics + +import io.micronaut.context.annotation.Requires +// tag::imports[] +import io.micronaut.http.HttpRequest.GET +import io.micronaut.http.HttpStatus.CREATED +import io.micronaut.http.MediaType.TEXT_PLAIN +import io.micronaut.http.annotation.Body +import io.micronaut.http.annotation.Controller +import io.micronaut.http.annotation.Get +import io.micronaut.http.annotation.Post +import io.micronaut.http.annotation.Status +import io.micronaut.http.client.HttpClient +import io.micronaut.http.client.annotation.Client +import org.reactivestreams.Publisher +import reactor.core.publisher.Flux +import io.micronaut.core.async.annotation.SingleResult +// end::imports[] + +@Requires(property = "spec.name", value = "HelloControllerSpec") +@Controller("/") +class HelloController(@param:Client("/endpoint") private val httpClient: HttpClient) { + + // tag::nonblocking[] + @Get("/hello/{name}") + @SingleResult + internal fun hello(name: String): Publisher { // <1> + return Flux.from(httpClient.retrieve(GET("/hello/$name"))) + .next() // <2> + } + // end::nonblocking[] + + @Get("/endpoint/hello/{name}") + internal fun helloEndpoint(name: String): String { + return "Hello $name" + } + + // tag::json[] + @Get("/greet/{name}") + internal fun greet(name: String): Message { + return Message("Hello $name") + } + // end::json[] + + @Post("/greet") + @Status(CREATED) + internal fun echo(@Body message: Message): Message { + return message + } + + @Post(value = "/hello", consumes = [TEXT_PLAIN], produces = [TEXT_PLAIN]) + @Status(CREATED) + internal fun echoHello(@Body message: String): String { + return message + } +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/basics/HelloControllerSpec.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/basics/HelloControllerSpec.kt new file mode 100644 index 00000000000..acc4857f989 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/basics/HelloControllerSpec.kt @@ -0,0 +1,131 @@ +package io.micronaut.docs.basics + +import io.kotest.matchers.shouldBe +import io.kotest.core.spec.style.StringSpec +import io.micronaut.context.ApplicationContext +import io.micronaut.core.type.Argument +import io.micronaut.http.HttpRequest.GET +import io.micronaut.http.HttpRequest.POST +import io.micronaut.http.HttpStatus +import io.micronaut.http.MediaType +import io.micronaut.http.client.HttpClient +import io.micronaut.http.uri.UriBuilder +import io.micronaut.runtime.server.EmbeddedServer +import reactor.core.publisher.Flux +import java.util.Collections + +class HelloControllerSpec: StringSpec() { + + val embeddedServer = autoClose( + ApplicationContext.run(EmbeddedServer::class.java, mapOf("spec.name" to HelloControllerSpec::class.simpleName)) + ) + + val client = autoClose( + embeddedServer.applicationContext.createBean(HttpClient::class.java, embeddedServer.url) + ) + + init { + "test simple retrieve" { + // tag::simple[] + val uri = UriBuilder.of("/hello/{name}") + .expand(Collections.singletonMap("name", "John")) + .toString() + uri shouldBe "/hello/John" + + val result = client.toBlocking().retrieve(uri) + + result shouldBe "Hello John" + // end::simple[] + } + + "test retrieve with headers" { + // tag::headers[] + val response = client.retrieve( + GET("/hello/John") + .header("X-My-Header", "SomeValue") + ) + // end::headers[] + + Flux.from(response).blockFirst() shouldBe "Hello John" + } + + "test retrieve with JSON" { + // tag::jsonmap[] + var response: Flux> = Flux.from(client.retrieve( + GET("/greet/John"), Map::class.java + )) + // end::jsonmap[] + + response.blockFirst()["text"] shouldBe "Hello John" + + // tag::jsonmaptypes[] + response = Flux.from(client.retrieve( + GET("/greet/John"), + Argument.of(Map::class.java, String::class.java, String::class.java) // <1> + )) + // end::jsonmaptypes[] + + response.blockFirst()["text"] shouldBe "Hello John" + } + + "test retrieve with POJO" { + // tag::jsonpojo[] + val response = Flux.from(client.retrieve( + GET("/greet/John"), Message::class.java + )) + + response.blockFirst().text shouldBe "Hello John" + // end::jsonpojo[] + } + + "test retrieve with POJO response" { + // tag::pojoresponse[] + val call = client.exchange( + GET("/greet/John"), Message::class.java // <1> + ) + + val response = Flux.from(call).blockFirst() + val message = response.getBody(Message::class.java) // <2> + // check the status + response.status shouldBe HttpStatus.OK // <3> + // check the body + message.isPresent shouldBe true + message.get().text shouldBe "Hello John" + // end::pojoresponse[] + } + + "test post request with string" { + // tag::poststring[] + val call = client.exchange( + POST("/hello", "Hello John") // <1> + .contentType(MediaType.TEXT_PLAIN_TYPE) + .accept(MediaType.TEXT_PLAIN_TYPE), String::class.java // <3> + ) + // end::poststring[] + + val response = Flux.from(call).blockFirst() + val message = response.getBody(String::class.java) // <2> + // check the status + response.status shouldBe HttpStatus.CREATED // <3> + // check the body + message.isPresent shouldBe true + message.get() shouldBe "Hello John" + } + + "test post request with POJO" { + // tag::postpojo[] + val call = client.exchange( + POST("/greet", Message("Hello John")), Message::class.java // <2> + ) + // end::postpojo[] + + val response = Flux.from(call).blockFirst() + val message = response.getBody(Message::class.java) // <2> + // check the status + response.status shouldBe HttpStatus.CREATED // <3> + // check the body + message.isPresent shouldBe true + message.get().text shouldBe "Hello John" + } + } +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/basics/Message.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/basics/Message.kt new file mode 100644 index 00000000000..442df0bff0f --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/basics/Message.kt @@ -0,0 +1,26 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.basics + +// tag::imports[] +import com.fasterxml.jackson.annotation.JsonCreator +import com.fasterxml.jackson.annotation.JsonProperty +// end::imports[] + +// tag::class[] +class Message @JsonCreator +constructor(@param:JsonProperty("text") val text: String) +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/client/ThirdPartyClientFilterSpec.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/client/ThirdPartyClientFilterSpec.kt new file mode 100644 index 00000000000..09eaceaf383 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/client/ThirdPartyClientFilterSpec.kt @@ -0,0 +1,108 @@ +package io.micronaut.docs.client + +import io.kotest.matchers.shouldBe +import io.kotest.core.spec.style.StringSpec +import io.micronaut.context.ApplicationContext +import io.micronaut.context.annotation.Requires +import io.micronaut.context.annotation.Value +import io.micronaut.http.HttpRequest +import io.micronaut.http.HttpResponse +import io.micronaut.http.MutableHttpRequest +import io.micronaut.http.annotation.Controller +import io.micronaut.http.annotation.Filter +import io.micronaut.http.annotation.Get +import io.micronaut.http.annotation.Header +import io.micronaut.http.client.HttpClient +import io.micronaut.http.client.annotation.Client +import io.micronaut.http.filter.ClientFilterChain +import io.micronaut.http.filter.HttpClientFilter +import io.micronaut.runtime.server.EmbeddedServer +import org.reactivestreams.Publisher +import java.util.Base64 +import jakarta.inject.Singleton +import reactor.core.publisher.Flux + +class ThirdPartyClientFilterSpec: StringSpec() { + private var result: String? = null + private val token = "XXXX" + private val username = "john" + + val embeddedServer = autoClose( + ApplicationContext.run(EmbeddedServer::class.java, + mapOf( + "bintray.username" to username, + "bintray.token" to token, + "bintray.organization" to "grails", + "spec.name" to ThirdPartyClientFilterSpec::class.simpleName)) + ) + + val client = autoClose( + embeddedServer.applicationContext.createBean(HttpClient::class.java, embeddedServer.url) + ) + + init { + "a client filter is applied to the request and adds the authorization header" { + val bintrayService = embeddedServer.applicationContext.getBean(BintrayService::class.java) + + result = bintrayService.fetchRepositories().blockFirst().body() + + val encoded = Base64.getEncoder().encodeToString("$username:$token".toByteArray()) + val expected = "Basic $encoded" + + result shouldBe expected + } + } + + @Controller("/repos") + class HeaderController { + + @Get(value = "/grails") + fun echoAuthorization(@Header authorization: String): String { + return authorization + } + } +} + +//tag::bintrayService[] +@Singleton +internal class BintrayService( + @param:Client(BintrayApi.URL) val client: HttpClient, // <1> + @param:Value("\${bintray.organization}") val org: String) { + + fun fetchRepositories(): Flux> { + return Flux.from(client.exchange(HttpRequest.GET("/repos/$org"), String::class.java)) // <2> + } + + fun fetchPackages(repo: String): Flux> { + return Flux.from(client.exchange(HttpRequest.GET("/repos/$org/$repo/packages"), String::class.java)) // <2> + } +} +//end::bintrayService[] + +@Requires(property = "spec.name", value = "ThirdPartyClientFilterSpec") +//tag::bintrayFilter[] +@Filter("/repos/**") // <1> +internal class BintrayFilter( + @param:Value("\${bintray.username}") val username: String, // <2> + @param:Value("\${bintray.token}") val token: String)// <2> + : HttpClientFilter { + + override fun doFilter(request: MutableHttpRequest<*>, chain: ClientFilterChain): Publisher> { + return chain.proceed( + request.basicAuth(username, token) // <3> + ) + } +} +//end::bintrayFilter[] + +/* +//tag::bintrayApiConstants[] +class BintrayApi { + public static final String URL = 'https://api.bintray.com' +} +//end::bintrayApiConstants[] +*/ + +internal object BintrayApi { + const val URL = "/" +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/client/filter/BasicAuth.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/client/filter/BasicAuth.kt new file mode 100644 index 00000000000..eda14309668 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/client/filter/BasicAuth.kt @@ -0,0 +1,29 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.client.filter + +//tag::class[] +import io.micronaut.http.annotation.FilterMatcher +import kotlin.annotation.AnnotationRetention.RUNTIME +import kotlin.annotation.AnnotationTarget.CLASS +import kotlin.annotation.AnnotationTarget.VALUE_PARAMETER + +@FilterMatcher // <1> +@MustBeDocumented +@Retention(RUNTIME) +@Target(CLASS, VALUE_PARAMETER) +annotation class BasicAuth +//end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/client/filter/BasicAuthClient.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/client/filter/BasicAuthClient.kt new file mode 100644 index 00000000000..577a50d898d --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/client/filter/BasicAuthClient.kt @@ -0,0 +1,29 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.client.filter + +//tag::class[] +import io.micronaut.http.annotation.Get +import io.micronaut.http.client.annotation.Client + +@BasicAuth // <1> +@Client("/message") +interface BasicAuthClient { + + @Get + fun getMessage(): String +} +//end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/client/filter/BasicAuthClientFilter.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/client/filter/BasicAuthClientFilter.kt new file mode 100644 index 00000000000..3f3acf1b51d --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/client/filter/BasicAuthClientFilter.kt @@ -0,0 +1,36 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.client.filter + +//tag::class[] +import io.micronaut.http.HttpResponse +import io.micronaut.http.MutableHttpRequest +import io.micronaut.http.filter.ClientFilterChain +import io.micronaut.http.filter.HttpClientFilter +import org.reactivestreams.Publisher + +import jakarta.inject.Singleton + +@BasicAuth // <1> +@Singleton // <2> +class BasicAuthClientFilter : HttpClientFilter { + + override fun doFilter(request: MutableHttpRequest<*>, + chain: ClientFilterChain): Publisher> { + return chain.proceed(request.basicAuth("user", "pass")) + } +} +//end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/client/filter/BasicAuthFilterSpec.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/client/filter/BasicAuthFilterSpec.kt new file mode 100644 index 00000000000..9ef21f2bec5 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/client/filter/BasicAuthFilterSpec.kt @@ -0,0 +1,36 @@ +package io.micronaut.docs.client.filter + +import io.kotest.matchers.shouldBe +import io.kotest.core.spec.style.StringSpec +import io.micronaut.context.ApplicationContext +import io.micronaut.context.annotation.Requires +import io.micronaut.http.annotation.Controller +import io.micronaut.http.annotation.Get +import io.micronaut.runtime.server.EmbeddedServer + +class BasicAuthFilterSpec: StringSpec() { + + val context = autoClose( + ApplicationContext.run(EmbeddedServer::class.java, + mapOf("spec.name" to BasicAuthFilterSpec::class.simpleName)).applicationContext + ) + + init { + "test the filter is applied"() { + val client = context.getBean(BasicAuthClient::class.java) + + client.getMessage() shouldBe "user:pass" + } + } + + @Requires(property = "spec.name", value = "BasicAuthFilterSpec") + @Controller("/message") + class BasicAuthController { + + @Get + internal fun message(basicAuth: io.micronaut.http.BasicAuth): String { + return basicAuth.username + ":" + basicAuth.password + } + } + +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/client/filter/GoogleAuthFilter.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/client/filter/GoogleAuthFilter.kt new file mode 100644 index 00000000000..476f283b6a4 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/client/filter/GoogleAuthFilter.kt @@ -0,0 +1,41 @@ +package io.micronaut.docs.client.filter + +//tag::class[] +import io.micronaut.context.BeanProvider +import io.micronaut.context.annotation.Requires +import io.micronaut.context.env.Environment +import io.micronaut.http.HttpRequest +import io.micronaut.http.HttpResponse +import io.micronaut.http.MutableHttpRequest +import io.micronaut.http.annotation.Filter +import io.micronaut.http.client.HttpClient +import io.micronaut.http.filter.ClientFilterChain +import io.micronaut.http.filter.HttpClientFilter +import org.reactivestreams.Publisher +import reactor.core.publisher.Mono +import java.net.URLEncoder + +@Requires(env = [Environment.GOOGLE_COMPUTE]) +@Filter(patterns = ["/google-auth/api/**"]) +class GoogleAuthFilter ( + private val authClientProvider: BeanProvider) : HttpClientFilter { // <1> + + override fun doFilter(request: MutableHttpRequest<*>, + chain: ClientFilterChain): Publisher?> { + return Mono.fromCallable { encodeURI(request) } + .flux() + .map { authURI: String -> + authClientProvider.get().retrieve(HttpRequest.GET(authURI) + .header("Metadata-Flavor", "Google") // <2> + ) + }.flatMap { t -> chain.proceed(request.bearerAuth(t.toString())) } + } + + private fun encodeURI(request: MutableHttpRequest<*>): String { + val receivingURI = "${request.uri.scheme}://${request.uri.host}" + return "http://metadata/computeMetadata/v1/instance/service-accounts/default/identity?audience=" + + URLEncoder.encode(receivingURI, "UTF-8") + } + +} +//end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/client/upload/MultipartFileUploadSpec.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/client/upload/MultipartFileUploadSpec.kt new file mode 100644 index 00000000000..71a1773d7cc --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/client/upload/MultipartFileUploadSpec.kt @@ -0,0 +1,122 @@ +package io.micronaut.docs.client.upload + +// tag::imports[] +import io.kotest.matchers.shouldBe +import io.kotest.core.spec.style.StringSpec +import io.micronaut.context.ApplicationContext +import io.micronaut.http.HttpRequest +import io.micronaut.http.HttpResponse +import io.micronaut.http.MediaType +import io.micronaut.http.annotation.Post +import io.micronaut.runtime.server.EmbeddedServer +import java.io.File +import java.io.FileWriter +// end::imports[] + +// tag::multipartBodyImports[] +import io.micronaut.http.client.multipart.MultipartBody +// end::multipartBodyImports[] + +// tag::controllerImports[] +import io.micronaut.http.annotation.Controller +import io.micronaut.http.client.HttpClient +import reactor.core.publisher.Flux + +// end::controllerImports[] + +// tag::class[] +class MultipartFileUploadSpec: StringSpec() { + + val embeddedServer = autoClose( + ApplicationContext.run(EmbeddedServer::class.java) + ) + + val client = autoClose( + embeddedServer.applicationContext.createBean(HttpClient::class.java, embeddedServer.url) + ) + + init { + "test multipart file request byte[]" { + // tag::file[] + val toWrite = "test file" + val file = File.createTempFile("data", ".txt") + val writer = FileWriter(file) + writer.write(toWrite) + writer.close() + // end::file[] + + // tag::multipartBody[] + val requestBody = MultipartBody.builder() // <1> + .addPart( // <2> + "data", + file.name, + MediaType.TEXT_PLAIN_TYPE, + file + ).build() // <3> + + // end::multipartBody[] + + val flowable = Flux.from(client!!.exchange( + + // tag::request[] + HttpRequest.POST("/multipart/upload", requestBody) // <1> + .contentType(MediaType.MULTIPART_FORM_DATA_TYPE) // <2> + // end::request[] + .accept(MediaType.TEXT_PLAIN_TYPE), + String::class.java + )) + val response = flowable.blockFirst() + val body = response.body.get() + + body shouldBe "Uploaded 9 bytes" + } + + "test multipart file request byte[] with ContentType" { + // tag::multipartBodyBytes[] + val requestBody = MultipartBody.builder() + .addPart("data", "sample.txt", MediaType.TEXT_PLAIN_TYPE, "test content".toByteArray()) + .build() + // end::multipartBodyBytes[] + + val flowable = Flux.from(client!!.exchange( + HttpRequest.POST("/multipart/upload", requestBody) + .contentType(MediaType.MULTIPART_FORM_DATA_TYPE) + .accept(MediaType.TEXT_PLAIN_TYPE), + String::class.java + )) + val response = flowable.blockFirst() + val body = response.body.get() + + body shouldBe "Uploaded 12 bytes" + } + + "test multipart file request byte[] without ContentType" { + val toWrite = "test file" + val file = File.createTempFile("data", ".txt") + val writer = FileWriter(file) + writer.write(toWrite) + writer.close() + file.createNewFile() + + val flowable = Flux.from(client!!.exchange( + HttpRequest.POST("/multipart/upload", MultipartBody.builder().addPart("data", file.name, file)) + .contentType(MediaType.MULTIPART_FORM_DATA_TYPE) + .accept(MediaType.TEXT_PLAIN_TYPE), + String::class.java + )) + val response = flowable.blockFirst() + val body = response.body.get() + + body shouldBe "Uploaded 9 bytes" + } + } + + @Controller("/multipart") + internal class MultipartController { + + @Post(value = "/upload", consumes = [MediaType.MULTIPART_FORM_DATA], produces = [MediaType.TEXT_PLAIN]) + fun upload(data: ByteArray): HttpResponse { + return HttpResponse.ok("Uploaded ${data.size} bytes") + } + } +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/client/versioning/HelloClient.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/client/versioning/HelloClient.kt new file mode 100644 index 00000000000..35248f4b577 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/client/versioning/HelloClient.kt @@ -0,0 +1,37 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.client.versioning + +// tag::imports[] +import io.micronaut.core.version.annotation.Version +import io.micronaut.http.annotation.Get +import io.micronaut.http.client.annotation.Client +import reactor.core.publisher.Mono +// end::imports[] + +// tag::clazz[] +@Client("/hello") +@Version("1") // <1> +interface HelloClient { + + @Get("/greeting/{name}") + fun sayHello(name : String) : String + + @Version("2") + @Get("/greeting/{name}") + fun sayHelloTwo(name : String) : Mono // <2> +} +// end::clazz[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/builder/CrankShaft.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/builder/CrankShaft.kt new file mode 100644 index 00000000000..defb6e6e5df --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/builder/CrankShaft.kt @@ -0,0 +1,52 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/* +* Copyright 2017-2019 original authors +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ +package io.micronaut.docs.config.builder + +internal class CrankShaft(val rodLength: Double?) { + + class Builder { + private var rodLength: Double? = null + fun withRodLength(rodLength: Double): Builder { + this.rodLength = rodLength + return this + } + + fun build(): CrankShaft { + return CrankShaft(rodLength) + } + } + + companion object { + fun builder(): Builder { + return Builder() + } + } +} \ No newline at end of file diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/builder/Engine.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/builder/Engine.kt new file mode 100644 index 00000000000..d46d49cdcb8 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/builder/Engine.kt @@ -0,0 +1,23 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.config.builder + +// tag::class[] +internal interface Engine { // <1> + val cylinders: Int + fun start(): String +} +// end::class[] \ No newline at end of file diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/builder/EngineConfig.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/builder/EngineConfig.kt new file mode 100644 index 00000000000..06ae14a1735 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/builder/EngineConfig.kt @@ -0,0 +1,36 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.config.builder + +// tag::imports[] +import io.micronaut.context.annotation.ConfigurationBuilder +import io.micronaut.context.annotation.ConfigurationProperties +// end::imports[] + +// tag::class[] +@ConfigurationProperties("my.engine") // <1> +internal class EngineConfig { + + @ConfigurationBuilder(prefixes = ["with"]) // <2> + val builder = EngineImpl.builder() + + @ConfigurationBuilder(prefixes = ["with"], configurationPrefix = "crank-shaft") // <3> + val crankShaft = CrankShaft.builder() + + @set:ConfigurationBuilder(prefixes = ["with"], configurationPrefix = "spark-plug") // <4> + var sparkPlug = SparkPlug.builder() +} +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/builder/EngineFactory.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/builder/EngineFactory.kt new file mode 100644 index 00000000000..f649927774c --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/builder/EngineFactory.kt @@ -0,0 +1,32 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.config.builder + +// tag::imports[] +import io.micronaut.context.annotation.Factory +import jakarta.inject.Singleton +// end::imports[] + +// tag::class[] +@Factory +internal class EngineFactory { + + @Singleton + fun buildEngine(engineConfig: EngineConfig): EngineImpl { + return engineConfig.builder.build(engineConfig.crankShaft, engineConfig.sparkPlug) + } +} +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/builder/EngineImpl.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/builder/EngineImpl.kt new file mode 100644 index 00000000000..b693f32c3c1 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/builder/EngineImpl.kt @@ -0,0 +1,60 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.config.builder + +// tag::class[] +internal class EngineImpl(manufacturer: String, cylinders: Int, crankShaft: CrankShaft, sparkPlug: SparkPlug) : Engine { + override var cylinders: Int = 0 + private val manufacturer: String + private val crankShaft: CrankShaft + private val sparkPlug: SparkPlug + + init { + this.crankShaft = crankShaft + this.cylinders = cylinders + this.manufacturer = manufacturer + this.sparkPlug = sparkPlug + } + + override fun start(): String { + return "$manufacturer Engine Starting V$cylinders [rodLength=${crankShaft.rodLength ?: 6.0}, sparkPlug=$sparkPlug]" + } + + class Builder { + private var manufacturer = "Ford" + private var cylinders: Int = 0 + fun withManufacturer(manufacturer: String): Builder { + this.manufacturer = manufacturer + return this + } + + fun withCylinders(cylinders: Int): Builder { + this.cylinders = cylinders + return this + } + + fun build(crankShaft: CrankShaft.Builder, sparkPlug: SparkPlug.Builder): EngineImpl { + return EngineImpl(manufacturer, cylinders, crankShaft.build(), sparkPlug.build()) + } + } + + companion object { + fun builder(): Builder { + return Builder() + } + } +} +// end::class[] \ No newline at end of file diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/builder/SparkPlug.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/builder/SparkPlug.kt new file mode 100644 index 00000000000..4a6014b8d29 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/builder/SparkPlug.kt @@ -0,0 +1,58 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.config.builder + +internal data class SparkPlug( + val name: String?, + val type: String?, + val companyName: String? +) { + override fun toString(): String { + return "${type ?: ""}(${companyName ?: ""} ${name ?: ""})" + } + + companion object { + fun builder(): Builder { + return Builder() + } + } + + data class Builder( + var name: String? = "4504 PK20TT", + var type: String? = "Platinum TT", + var companyName: String? = "Denso" + ) { + fun withName(name: String?): Builder { + this.name = name + return this + } + + fun withType(type: String?): Builder { + this.type = type + return this + } + + fun withCompany(companyName: String?): Builder { + this.companyName = companyName + return this + } + + fun build(): SparkPlug { + return SparkPlug(name, type, companyName) + } + } + +} \ No newline at end of file diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/builder/Vehicle.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/builder/Vehicle.kt new file mode 100644 index 00000000000..1a16454cd8d --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/builder/Vehicle.kt @@ -0,0 +1,28 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.config.builder + +import jakarta.inject.Singleton + +// tag::class[] +@Singleton +internal class Vehicle(val engine: Engine) { + + fun start(): String { + return engine.start() + } +} +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/builder/VehicleSpec.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/builder/VehicleSpec.kt new file mode 100644 index 00000000000..29a37879372 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/builder/VehicleSpec.kt @@ -0,0 +1,30 @@ +package io.micronaut.docs.config.builder + +import io.kotest.matchers.shouldBe +import io.kotest.core.spec.style.StringSpec +import io.micronaut.context.ApplicationContext + +internal class VehicleSpec : StringSpec({ + + "test start vehicle" { + // tag::start[] + val applicationContext = ApplicationContext.run( + mapOf( + "my.engine.cylinders" to "4", + "my.engine.manufacturer" to "Subaru", + "my.engine.crank-shaft.rod-length" to 4, + "my.engine.spark-plug.name" to "6619 LFR6AIX", + "my.engine.spark-plug.type" to "Iridium", + "my.engine.spark-plug.company" to "NGK" + ), + "test" + ) + + val vehicle = applicationContext.getBean(Vehicle::class.java) + println(vehicle.start()) + // end::start[] + + vehicle.start().shouldBe("Subaru Engine Starting V4 [rodLength=4.0, sparkPlug=Iridium(NGK 6619 LFR6AIX)]") + applicationContext.close() + } +}) diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/converters/MapToLocalDateConverter.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/converters/MapToLocalDateConverter.kt new file mode 100644 index 00000000000..c4b3c050665 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/converters/MapToLocalDateConverter.kt @@ -0,0 +1,48 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.config.converters + +// tag::imports[] +import io.micronaut.context.annotation.Prototype +import io.micronaut.core.convert.ConversionContext +import io.micronaut.core.convert.ConversionService +import io.micronaut.core.convert.TypeConverter +import java.time.DateTimeException +import java.time.LocalDate +import java.util.Optional +import jakarta.inject.Singleton +// end::imports[] + +// tag::class[] +@Prototype +class MapToLocalDateConverter : TypeConverter, LocalDate> { // <1> + override fun convert(propertyMap: Map<*, *>, targetType: Class, context: ConversionContext): Optional { + val day = ConversionService.SHARED.convert(propertyMap["day"], Int::class.java) + val month = ConversionService.SHARED.convert(propertyMap["month"], Int::class.java) + val year = ConversionService.SHARED.convert(propertyMap["year"], Int::class.java) + if (day.isPresent && month.isPresent && year.isPresent) { + try { + return Optional.of(LocalDate.of(year.get(), month.get(), day.get())) // <2> + } catch (e: DateTimeException) { + context.reject(propertyMap, e) // <3> + return Optional.empty() + } + } + + return Optional.empty() + } +} +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/converters/MyConfigurationProperties.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/converters/MyConfigurationProperties.kt new file mode 100644 index 00000000000..3b98e900250 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/converters/MyConfigurationProperties.kt @@ -0,0 +1,32 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.config.converters + +import io.micronaut.context.annotation.ConfigurationProperties +import java.time.LocalDate + +// tag::class[] +@ConfigurationProperties(MyConfigurationProperties.PREFIX) +class MyConfigurationProperties { + + var updatedAt: LocalDate? = null + protected set + + companion object { + const val PREFIX = "myapp" + } +} +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/converters/MyConfigurationPropertiesSpec.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/converters/MyConfigurationPropertiesSpec.kt new file mode 100644 index 00000000000..65616ce7e63 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/converters/MyConfigurationPropertiesSpec.kt @@ -0,0 +1,41 @@ +package io.micronaut.docs.config.converters + +import io.kotest.core.spec.style.AnnotationSpec +import io.micronaut.context.ApplicationContext +import org.junit.Assert.assertEquals +import java.time.LocalDate + +//tag::configSpec[] +class MyConfigurationPropertiesSpec : AnnotationSpec() { + + //tag::runContext[] + lateinit var ctx: ApplicationContext + + @BeforeEach + fun setup() { + ctx = ApplicationContext.run( + mapOf( + "myapp.updatedAt" to mapOf( // <1> + "day" to 28, + "month" to 10, + "year" to 1982 + ) + ) + ) + } + + @AfterEach + fun teardown() { + ctx?.close() + } + //end::runContext[] + + @Test + fun testConvertDateFromMap() { + val props = ctx.getBean(MyConfigurationProperties::class.java) + + val expectedDate = LocalDate.of(1982, 10, 28) + assertEquals(expectedDate, props.updatedAt) + } +} +//end::configSpec[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/env/DataSourceConfiguration.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/env/DataSourceConfiguration.kt new file mode 100644 index 00000000000..81bb4bec3e8 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/env/DataSourceConfiguration.kt @@ -0,0 +1,30 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.config.env + +// tag::eachProperty[] +import io.micronaut.context.annotation.EachProperty +import io.micronaut.context.annotation.Parameter +import java.net.URI +import java.net.URISyntaxException + +@EachProperty("test.datasource") // <1> +class DataSourceConfiguration +@Throws(URISyntaxException::class) +constructor(@param:Parameter val name: String) { // <2> + var url = URI("localhost") // <3> +} +// end::eachProperty[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/env/DataSourceFactory.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/env/DataSourceFactory.kt new file mode 100644 index 00000000000..a3e74382bde --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/env/DataSourceFactory.kt @@ -0,0 +1,41 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.config.env + +import io.micronaut.context.annotation.EachBean +import io.micronaut.context.annotation.Factory + +import java.net.URI +import java.sql.Connection + +// tag::eachBean[] +@Factory // <1> +class DataSourceFactory { + + @EachBean(DataSourceConfiguration::class) // <2> + internal fun dataSource(configuration: DataSourceConfiguration): DataSource { // <3> + val url = configuration.url + return DataSource(url) + } +// end::eachBean[] + + internal class DataSource(private val uri: URI) { + + fun connect(): Connection { + throw UnsupportedOperationException("Can't really connect. I'm not a real data source") + } + } +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/env/EachBeanTest.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/env/EachBeanTest.kt new file mode 100644 index 00000000000..fd117219b35 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/env/EachBeanTest.kt @@ -0,0 +1,41 @@ +package io.micronaut.docs.config.env + +import io.kotest.core.spec.style.AnnotationSpec +import io.micronaut.context.ApplicationContext +import io.micronaut.context.env.PropertySource +import io.micronaut.docs.config.env.DataSourceFactory.DataSource +import io.micronaut.inject.qualifiers.Qualifiers +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotNull +import org.junit.Test +import java.net.URISyntaxException + +class EachBeanTest : AnnotationSpec() { + + @Test + @Throws(URISyntaxException::class) + fun testEachBean() { + // tag::config[] + val applicationContext = ApplicationContext.run(PropertySource.of( + "test", + mapOf( + "test.datasource.one.url" to "jdbc:mysql://localhost/one", + "test.datasource.two.url" to "jdbc:mysql://localhost/two") + )) + // end::config[] + + // tag::beans[] + val beansOfType = applicationContext.getBeansOfType(DataSource::class.java) + assertEquals(2, beansOfType.size) // <1> + + val firstConfig = applicationContext.getBean( + DataSource::class.java, + Qualifiers.byName("one") // <2> + ) + // end::beans[] + + assertNotNull(firstConfig) + + applicationContext.close() + } +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/env/EachPropertyTest.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/env/EachPropertyTest.kt new file mode 100644 index 00000000000..b98c695dbfe --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/env/EachPropertyTest.kt @@ -0,0 +1,66 @@ +package io.micronaut.docs.config.env + +import io.kotest.core.spec.style.AnnotationSpec +import io.micronaut.context.ApplicationContext +import io.micronaut.context.env.PropertySource +import io.micronaut.core.util.CollectionUtils +import io.micronaut.inject.qualifiers.Qualifiers +import org.junit.Assert.assertEquals +import java.net.URI +import java.net.URISyntaxException +import java.util.stream.Collectors + +class EachPropertyTest : AnnotationSpec() { + + @Test + @Throws(URISyntaxException::class) + fun testEachProperty() { + // tag::config[] + val applicationContext = ApplicationContext.run(PropertySource.of( + "test", + mapOf( + "test.datasource.one.url" to "jdbc:mysql://localhost/one", + "test.datasource.two.url" to "jdbc:mysql://localhost/two" + ) + )) + // end::config[] + + // tag::beans[] + val beansOfType = applicationContext.getBeansOfType(DataSourceConfiguration::class.java) + assertEquals(2, beansOfType.size) // <1> + + val firstConfig = applicationContext.getBean( + DataSourceConfiguration::class.java, + Qualifiers.byName("one") // <2> + ) + + assertEquals( + URI("jdbc:mysql://localhost/one"), + firstConfig.url + ) + // end::beans[] + applicationContext.close() + } + + @Test + fun testEachPropertyList() { + val limits: MutableList> = ArrayList() + limits.add(CollectionUtils.mapOf("period", "10s", "limit", "1000")) + limits.add(CollectionUtils.mapOf("period", "1m", "limit", "5000")) + val applicationContext = ApplicationContext.run( + mapOf("ratelimits" to listOf( + mapOf("period" to "10s", "limit" to "1000"), + mapOf("period" to "1m", "limit" to "5000")))) + + val beansOfType = applicationContext.streamOfType(RateLimitsConfiguration::class.java).collect(Collectors.toList()) + + assertEquals( + 2, + beansOfType.size + ) + assertEquals(1000, beansOfType[0].limit) + assertEquals(5000, beansOfType[1].limit) + + applicationContext.close() + } +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/env/EnvironmentTest.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/env/EnvironmentTest.kt new file mode 100644 index 00000000000..f2a1f9310ed --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/env/EnvironmentTest.kt @@ -0,0 +1,48 @@ +package io.micronaut.docs.config.env + +import io.kotest.core.spec.style.AnnotationSpec +import io.micronaut.context.ApplicationContext +import io.micronaut.context.env.Environment +import io.micronaut.context.env.PropertySource +import io.micronaut.core.util.CollectionUtils +import org.junit.Test + +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue + +class EnvironmentTest: AnnotationSpec(){ + + @Test + fun testRunEnvironment() { + // tag::env[] + val applicationContext = ApplicationContext.run("test", "android") + val environment = applicationContext.environment + + assertTrue(environment.activeNames.contains("test")) + assertTrue(environment.activeNames.contains("android")) + // end::env[] + applicationContext.close() + } + + @Test + fun testRunEnvironmentWithProperties() { + // tag::envProps[] + val applicationContext = ApplicationContext.run( + PropertySource.of( + "test", + mapOf( + "micronaut.server.host" to "foo", + "micronaut.server.port" to 8080 + ) + ), + "test", "android") + val environment = applicationContext.environment + + assertEquals( + "foo", + environment.getProperty("micronaut.server.host", String::class.java).orElse("localhost") + ) + // end::envProps[] + applicationContext.close() + } +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/env/HighRateLimit.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/env/HighRateLimit.kt new file mode 100644 index 00000000000..a2ab0f32faa --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/env/HighRateLimit.kt @@ -0,0 +1,5 @@ +package io.micronaut.docs.config.env + +import java.time.Duration + +class HighRateLimit(period: Duration?, limit: Int) : RateLimit(period, limit) diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/env/LowRateLimit.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/env/LowRateLimit.kt new file mode 100644 index 00000000000..9058bd62b37 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/env/LowRateLimit.kt @@ -0,0 +1,5 @@ +package io.micronaut.docs.config.env + +import java.time.Duration + +class LowRateLimit(period: Duration?, limit: Int) : RateLimit(period, limit) diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/env/OrderTest.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/env/OrderTest.kt new file mode 100644 index 00000000000..2a50dac6cc2 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/env/OrderTest.kt @@ -0,0 +1,23 @@ +package io.micronaut.docs.config.env + +import io.micronaut.context.ApplicationContext +import org.junit.Assert +import org.junit.jupiter.api.Test +import java.util.stream.Collectors + +class OrderTest { + + @Test + fun testOrderOnFactories() { + val applicationContext = ApplicationContext.run() + val rateLimits = applicationContext.streamOfType(RateLimit::class.java) + .collect(Collectors.toList()) + Assert.assertEquals( + 2, + rateLimits.size + .toLong()) + Assert.assertEquals(1000L, rateLimits[0].limit.toLong()) + Assert.assertEquals(100L, rateLimits[1].limit.toLong()) + applicationContext.close() + } +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/env/RateLimit.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/env/RateLimit.kt new file mode 100644 index 00000000000..637c6b505c3 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/env/RateLimit.kt @@ -0,0 +1,5 @@ +package io.micronaut.docs.config.env + +import java.time.Duration + +open class RateLimit(val period: Duration?, val limit: Int) diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/env/RateLimitsConfiguration.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/env/RateLimitsConfiguration.kt new file mode 100644 index 00000000000..77cd0ee9e4a --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/env/RateLimitsConfiguration.kt @@ -0,0 +1,36 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.config.env + +// tag::clazz[] +import io.micronaut.context.annotation.EachProperty +import io.micronaut.context.annotation.Parameter +import io.micronaut.core.order.Ordered +import java.time.Duration + +@EachProperty(value = "ratelimits", list = true) // <1> +class RateLimitsConfiguration + constructor(@param:Parameter private val index: Int) // <3> + : Ordered { // <2> + + var period: Duration? = null + var limit: Int? = null + + override fun getOrder(): Int { + return index + } +} +// end::clazz[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/env/RateLimitsFactory.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/env/RateLimitsFactory.kt new file mode 100644 index 00000000000..3be4ffab393 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/env/RateLimitsFactory.kt @@ -0,0 +1,24 @@ +package io.micronaut.docs.config.env + +//tag::clazz[] +import io.micronaut.context.annotation.Factory +import io.micronaut.core.annotation.Order +import java.time.Duration +import jakarta.inject.Singleton + +@Factory +class RateLimitsFactory { + + @Singleton + @Order(20) + fun rateLimit2(): LowRateLimit { + return LowRateLimit(Duration.ofMinutes(50), 100) + } + + @Singleton + @Order(10) + fun rateLimit1(): HighRateLimit { + return HighRateLimit(Duration.ofMinutes(50), 1000) + } +} +//end::clazz[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/immutable/Engine.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/immutable/Engine.kt new file mode 100644 index 00000000000..e4e6d3f4c8d --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/immutable/Engine.kt @@ -0,0 +1,31 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.config.immutable + +import jakarta.inject.Singleton + +// tag::class[] +@Singleton +class Engine(val config: EngineConfig)// <1> +{ + val cylinders: Int + get() = config.cylinders + + fun start(): String {// <2> + return "${config.manufacturer} Engine Starting V${config.cylinders} [rodLength=${config.crankShaft.getRodLength().orElse(6.0)}]" + } +} +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/immutable/EngineConfig.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/immutable/EngineConfig.kt new file mode 100644 index 00000000000..04a8ecd1824 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/immutable/EngineConfig.kt @@ -0,0 +1,47 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.config.immutable + +// tag::imports[] +import io.micronaut.context.annotation.ConfigurationInject +import io.micronaut.context.annotation.ConfigurationProperties +import io.micronaut.core.bind.annotation.Bindable +import java.util.Optional +import javax.validation.constraints.Min +import javax.validation.constraints.NotBlank +import javax.validation.constraints.NotNull +// end::imports[] + +// tag::class[] +@ConfigurationProperties("my.engine") // <1> +data class EngineConfig @ConfigurationInject // <2> + constructor( + @Bindable(defaultValue = "Ford") @NotBlank val manufacturer: String, // <3> + @Min(1) val cylinders: Int, // <4> + @NotNull val crankShaft: CrankShaft) { + + @ConfigurationProperties("crank-shaft") + data class CrankShaft @ConfigurationInject + constructor(// <5> + private val rodLength: Double? // <6> + ) { + + fun getRodLength(): Optional { + return Optional.ofNullable(rodLength) + } + } +} +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/immutable/Vehicle.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/immutable/Vehicle.kt new file mode 100644 index 00000000000..605770ebcfa --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/immutable/Vehicle.kt @@ -0,0 +1,26 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.config.immutable + +import jakarta.inject.Singleton + +@Singleton +class Vehicle(val engine: Engine)// <6> +{ + fun start(): String { + return engine.start() + } +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/immutable/VehicleSpec.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/immutable/VehicleSpec.kt new file mode 100644 index 00000000000..5a747d9019f --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/immutable/VehicleSpec.kt @@ -0,0 +1,25 @@ +package io.micronaut.docs.config.immutable + +import io.kotest.matchers.shouldBe +import io.kotest.core.spec.style.StringSpec +import io.micronaut.context.ApplicationContext + +class VehicleSpec: StringSpec({ + + "test start vehicle" { + // tag::start[] + val map = mapOf( + "my.engine.cylinders" to "8", + "my.engine.crank-shaft.rod-length" to "7.0" + ) + val applicationContext = ApplicationContext.run(map) + + val vehicle = applicationContext.getBean(Vehicle::class.java) + println(vehicle.start()) + // end::start[] + + vehicle.start().shouldBe("Ford Engine Starting V8 [rodLength=7.0]") + + applicationContext.close() + } +}) diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/itfce/Engine.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/itfce/Engine.kt new file mode 100644 index 00000000000..311ffc7387d --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/itfce/Engine.kt @@ -0,0 +1,29 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.config.itfce + +import jakarta.inject.Singleton + +@Singleton +class Engine(val config: EngineConfig)// <1> +{ + val cylinders: Int + get() = config.cylinders + + fun start(): String {// <2> + return "${config.manufacturer} Engine Starting V${config.cylinders} [rodLength=${config.crankShaft.rodLength ?: 6.0}]" + } +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/itfce/EngineConfig.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/itfce/EngineConfig.kt new file mode 100644 index 00000000000..2cc9e4d4961 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/itfce/EngineConfig.kt @@ -0,0 +1,45 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.config.itfce + +// tag::imports[] +import io.micronaut.context.annotation.ConfigurationProperties +import io.micronaut.core.bind.annotation.Bindable +import javax.validation.constraints.Min +import javax.validation.constraints.NotBlank +import javax.validation.constraints.NotNull +// end::imports[] + +// tag::class[] +@ConfigurationProperties("my.engine") // <1> +interface EngineConfig { + + @get:Bindable(defaultValue = "Ford") // <2> + @get:NotBlank // <3> + val manufacturer: String + + @get:Min(1L) + val cylinders: Int + + @get:NotNull + val crankShaft: CrankShaft // <4> + + @ConfigurationProperties("crank-shaft") + interface CrankShaft { // <5> + val rodLength: Double? // <6> + } +} +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/itfce/Vehicle.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/itfce/Vehicle.kt new file mode 100644 index 00000000000..7568e699c49 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/itfce/Vehicle.kt @@ -0,0 +1,26 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.config.itfce + +import jakarta.inject.Singleton + +@Singleton +class Vehicle(val engine: Engine)// <6> +{ + fun start(): String { + return engine.start() + } +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/itfce/VehicleSpec.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/itfce/VehicleSpec.kt new file mode 100644 index 00000000000..9d438d8d128 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/itfce/VehicleSpec.kt @@ -0,0 +1,41 @@ +package io.micronaut.docs.config.itfce + +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.matchers.shouldBe +import io.kotest.core.spec.style.StringSpec +import io.kotest.matchers.string.shouldContain +import io.micronaut.context.ApplicationContext +import io.micronaut.context.exceptions.BeanInstantiationException + +class VehicleSpec: StringSpec({ + + "test start vehicle" { + // tag::start[] + val map = mapOf( + "my.engine.cylinders" to "8", + "my.engine.crank-shaft.rod-length" to "7.0" + ) + val applicationContext = ApplicationContext.run(map) + + val vehicle = applicationContext.getBean(Vehicle::class.java) + // end::start[] + + vehicle.start().shouldBe("Ford Engine Starting V8 [rodLength=7.0]") + + applicationContext.close() + } + + "test start vehicle - invalid" { + // tag::start[] + val map = mapOf( + "my.engine.cylinders" to "-10", + "my.engine.crank-shaft.rod-length" to "7.0" + ) + val applicationContext = ApplicationContext.run(map) + val exception = shouldThrow { + applicationContext.getBean(Vehicle::class.java) + } + exception.message.shouldContain("EngineConfig.getCylinders - must be greater than or equal to 1") + applicationContext.close() + } +}) diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/mapFormat/Engine.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/mapFormat/Engine.kt new file mode 100644 index 00000000000..5fa41458c08 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/mapFormat/Engine.kt @@ -0,0 +1,21 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.config.mapFormat + +interface Engine { + val sensors: Map<*, *>? + fun start(): String +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/mapFormat/EngineConfig.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/mapFormat/EngineConfig.kt new file mode 100644 index 00000000000..ceb841f3c77 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/mapFormat/EngineConfig.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.config.mapFormat + +// tag::imports[] +import io.micronaut.context.annotation.ConfigurationProperties +import io.micronaut.core.convert.format.MapFormat +import javax.validation.constraints.Min +// end::imports[] + +// tag::class[] +@ConfigurationProperties("my.engine") +class EngineConfig { + + @Min(1L) + var cylinders: Int = 0 + + @MapFormat(transformation = MapFormat.MapTransformation.FLAT) //<1> + var sensors: Map? = null +} +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/mapFormat/EngineImpl.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/mapFormat/EngineImpl.kt new file mode 100644 index 00000000000..ac899f477e1 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/mapFormat/EngineImpl.kt @@ -0,0 +1,35 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.config.mapFormat + +import jakarta.inject.Inject +import jakarta.inject.Singleton + +// tag::class[] +@Singleton +class EngineImpl : Engine { + + override val sensors: Map<*, *>? + get() = config!!.sensors + + @Inject + var config: EngineConfig? = null + + override fun start(): String { + return "Engine Starting V${config!!.cylinders} [sensors=${sensors!!.size}]" + } +} +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/mapFormat/Vehicle.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/mapFormat/Vehicle.kt new file mode 100644 index 00000000000..b7a61c5e109 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/mapFormat/Vehicle.kt @@ -0,0 +1,26 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.config.mapFormat + +import jakarta.inject.Singleton + +@Singleton +class Vehicle(val engine: Engine) { + + fun start(): String { + return engine.start() + } +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/mapFormat/VehicleSpec.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/mapFormat/VehicleSpec.kt new file mode 100644 index 00000000000..8477d267cdb --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/mapFormat/VehicleSpec.kt @@ -0,0 +1,29 @@ +package io.micronaut.docs.config.mapFormat + +import io.kotest.matchers.shouldBe +import io.kotest.core.spec.style.StringSpec +import io.micronaut.context.ApplicationContext + +class VehicleSpec: StringSpec({ + "test start vehicle" { + // tag::start[] + val subMap = mapOf( + 0 to "thermostat", + 1 to "fuel pressure" + ) + val map = mapOf( + "my.engine.cylinders" to "8", + "my.engine.sensors" to subMap + ) + + val applicationContext = ApplicationContext.run(map, "test") + + val vehicle = applicationContext.getBean(Vehicle::class.java) + println(vehicle.start()) + // end::start[] + + vehicle.start().shouldBe("Engine Starting V8 [sensors=2]") + + applicationContext.close() + } +}) diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/properties/Engine.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/properties/Engine.kt new file mode 100644 index 00000000000..06989cc5a4b --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/properties/Engine.kt @@ -0,0 +1,22 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.config.properties + +interface Engine { + val cylinders: Int + + fun start(): String +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/properties/EngineConfig.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/properties/EngineConfig.kt new file mode 100644 index 00000000000..198beb4f6ef --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/properties/EngineConfig.kt @@ -0,0 +1,42 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.config.properties + +// tag::imports[] +import io.micronaut.context.annotation.ConfigurationProperties +import java.util.Optional +import javax.validation.constraints.Min +import javax.validation.constraints.NotBlank +// end::imports[] + +// tag::class[] +@ConfigurationProperties("my.engine") // <1> +class EngineConfig { + + @NotBlank // <2> + var manufacturer = "Ford" // <3> + + @Min(1L) + var cylinders: Int = 0 + + var crankShaft = CrankShaft() + + @ConfigurationProperties("crank-shaft") + class CrankShaft { // <4> + var rodLength: Optional = Optional.empty() // <5> + } +} +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/properties/EngineImpl.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/properties/EngineImpl.kt new file mode 100644 index 00000000000..b52d39f91a4 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/properties/EngineImpl.kt @@ -0,0 +1,31 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.config.properties + +import jakarta.inject.Singleton + +// tag::class[] +@Singleton +class EngineImpl(val config: EngineConfig) : Engine {// <1> + + override val cylinders: Int + get() = config.cylinders + + override fun start(): String {// <2> + return "${config.manufacturer} Engine Starting V${config.cylinders} [rodLength=${config.crankShaft.rodLength.orElse(6.0)}]" + } +} +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/properties/Vehicle.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/properties/Vehicle.kt new file mode 100644 index 00000000000..ec24edae554 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/properties/Vehicle.kt @@ -0,0 +1,27 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.config.properties + +import jakarta.inject.Singleton + +@Singleton +class Vehicle(val engine: Engine)// <6> +{ + + fun start(): String { + return engine.start() + } +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/properties/VehicleSpec.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/properties/VehicleSpec.kt new file mode 100644 index 00000000000..b8b477fd20e --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/properties/VehicleSpec.kt @@ -0,0 +1,23 @@ +package io.micronaut.docs.config.properties + +import io.kotest.matchers.shouldBe +import io.kotest.core.spec.style.StringSpec +import io.micronaut.context.ApplicationContext + +class VehicleSpec: StringSpec({ + + "test start vehicle" { + // tag::start[] + val map = mapOf( "my.engine.cylinders" to "8") + val applicationContext = ApplicationContext.run(map, "test") + + val vehicle = applicationContext.getBean(Vehicle::class.java) + println(vehicle.start()) + // end::start[] + + vehicle.start().shouldBe("Ford Engine Starting V8 [rodLength=6.0]") + + applicationContext.close() + } + +}) diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/property/Engine.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/property/Engine.kt new file mode 100644 index 00000000000..8685317738b --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/property/Engine.kt @@ -0,0 +1,41 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.config.property + +// tag::imports[] +import io.micronaut.context.annotation.Property + +import jakarta.inject.Inject +import jakarta.inject.Singleton + +// end::imports[] + +// tag::class[] +@Singleton +class Engine { + + @field:Property(name = "my.engine.cylinders") // <1> + protected var cylinders: Int = 0 // <2> + + @set:Inject + @setparam:Property(name = "my.engine.manufacturer") // <3> + var manufacturer: String? = null + + fun cylinders(): Int { + return cylinders + } +} +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/property/EngineSpec.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/property/EngineSpec.kt new file mode 100644 index 00000000000..ba2504c1bc6 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/property/EngineSpec.kt @@ -0,0 +1,24 @@ +package io.micronaut.docs.config.property + +import io.kotest.matchers.shouldBe +import io.kotest.core.spec.style.StringSpec +import io.micronaut.context.ApplicationContext +import org.junit.Test + +import java.util.LinkedHashMap + +import org.junit.Assert.assertEquals + +class EngineSpec : StringSpec({ + + "test start vehicle with configuration" { + val ctx = ApplicationContext.run(mapOf("my.engine.cylinders" to "8", "my.engine.manufacturer" to "Honda")) + + val engine = ctx.getBean(Engine::class.java) + + engine.manufacturer shouldBe "Honda" + engine.cylinders() shouldBe 8 + + ctx.close() + } +}) diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/value/Engine.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/value/Engine.kt new file mode 100644 index 00000000000..ca9b9481140 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/value/Engine.kt @@ -0,0 +1,22 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.config.value + +interface Engine { + val cylinders: Int + + fun start(): String +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/value/EngineImpl.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/value/EngineImpl.kt new file mode 100644 index 00000000000..c715c3b7e60 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/value/EngineImpl.kt @@ -0,0 +1,36 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.config.value + +// tag::imports[] +import io.micronaut.context.annotation.Value + +import jakarta.inject.Singleton +// end::imports[] + +// tag::class[] +@Singleton +class EngineImpl : Engine { + + @Value("\${my.engine.cylinders:6}") // <1> + override var cylinders: Int = 0 + protected set + + override fun start(): String { // <2> + return "Starting V$cylinders Engine" + } +} +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/value/Vehicle.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/value/Vehicle.kt new file mode 100644 index 00000000000..faa25f79933 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/value/Vehicle.kt @@ -0,0 +1,25 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.config.value + +import jakarta.inject.Singleton + +@Singleton +class Vehicle(val engine: Engine) {// <6> + fun start(): String { + return engine.start() + } +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/value/VehicleSpec.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/value/VehicleSpec.kt new file mode 100644 index 00000000000..9b68e46963c --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/value/VehicleSpec.kt @@ -0,0 +1,42 @@ +package io.micronaut.docs.config.value + +import io.kotest.matchers.shouldBe +import io.kotest.core.spec.style.StringSpec +import io.micronaut.context.ApplicationContext +import io.micronaut.context.DefaultApplicationContext +import io.micronaut.context.env.PropertySource +import org.codehaus.groovy.runtime.DefaultGroovyMethods + +class VehicleSpec : StringSpec({ + + "test start vehicle with configuration" { + // tag::start[] + val applicationContext = DefaultApplicationContext("test") + val map = mapOf("my.engine.cylinders" to "8") + applicationContext.getEnvironment().addPropertySource(PropertySource.of("test", map)) + applicationContext.start() + + val vehicle = applicationContext.getBean(Vehicle::class.java) + DefaultGroovyMethods.println(this, vehicle.start()) + // end::start[] + + vehicle.start().shouldBe("Starting V8 Engine") + + applicationContext.close() + } + + "test start vehicle without configuration" { + // tag::start[] + val applicationContext = DefaultApplicationContext("test") + applicationContext.start() + + val vehicle = applicationContext.getBean(Vehicle::class.java) + DefaultGroovyMethods.println(this, vehicle.start()) + // end::start[] + + vehicle.start().shouldBe("Starting V6 Engine") + + applicationContext.close() + } + +}) diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/context/Application.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/context/Application.kt new file mode 100644 index 00000000000..8c86c012ad4 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/context/Application.kt @@ -0,0 +1,38 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.context + +// tag::imports[] +import io.micronaut.runtime.Micronaut +// end::imports[] + +// tag::class[] +object Application { + + @JvmStatic + fun main(args: Array) { + Micronaut.build(null) + .mainClass(Application::class.java) + .environmentPropertySource(false) + //or + .environmentVariableIncludes("THIS_ENV_ONLY") + //or + .environmentVariableExcludes("EXCLUDED_ENV") + .start() + } +} +// end::class[] + diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/context/annotation/primary/Blue.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/context/annotation/primary/Blue.kt new file mode 100644 index 00000000000..34e1528e814 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/context/annotation/primary/Blue.kt @@ -0,0 +1,31 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.context.annotation.primary + +import io.micronaut.context.annotation.Requires +//tag::imports[] +import jakarta.inject.Singleton +//end::imports[] + +@Requires(property = "spec.name", value = "primaryspec") +//tag::clazz[] +@Singleton +class Blue: ColorPicker { + override fun color(): String { + return "blue" + } +} +//end::clazz[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/context/annotation/primary/ColorPicker.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/context/annotation/primary/ColorPicker.kt new file mode 100644 index 00000000000..e4d18b0924d --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/context/annotation/primary/ColorPicker.kt @@ -0,0 +1,22 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.context.annotation.primary + +//tag::clazz[] +interface ColorPicker { + fun color(): String +} +//end::clazz[] \ No newline at end of file diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/context/annotation/primary/Green.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/context/annotation/primary/Green.kt new file mode 100644 index 00000000000..a173d92ba4f --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/context/annotation/primary/Green.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.context.annotation.primary + +import io.micronaut.context.annotation.Requires + +//tag::imports[] +import io.micronaut.context.annotation.Primary +import jakarta.inject.Singleton +//end::imports[] + +@Requires(property = "spec.name", value = "primaryspec") +//tag::clazz[] +@Primary +@Singleton +class Green: ColorPicker { + override fun color(): String { + return "green" + } +} +//end::clazz[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/context/annotation/primary/PrimarySpec.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/context/annotation/primary/PrimarySpec.kt new file mode 100644 index 00000000000..eec17a5a6ab --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/context/annotation/primary/PrimarySpec.kt @@ -0,0 +1,29 @@ +package io.micronaut.docs.context.annotation.primary + +import io.kotest.matchers.shouldBe +import io.kotest.core.spec.style.StringSpec +import io.micronaut.context.ApplicationContext +import io.micronaut.context.env.Environment +import io.micronaut.http.HttpRequest +import io.micronaut.http.HttpStatus +import io.micronaut.http.client.HttpClient +import io.micronaut.runtime.server.EmbeddedServer + +class PrimarySpec : StringSpec() { + + val embeddedServer = autoClose(ApplicationContext.run(EmbeddedServer::class.java, mapOf( + "spec.name" to "primaryspec" + ), Environment.TEST)) + + val rxClient = autoClose(embeddedServer.applicationContext.createBean(HttpClient::class.java, embeddedServer.getURL())) + + init { + "test @Primary annotated beans gets injected in case of a collection" { + embeddedServer.applicationContext.getBeansOfType(ColorPicker::class.java).size.shouldBe(2) + val rsp = rxClient.toBlocking().exchange(HttpRequest.GET("/test"), String::class.java) + + rsp.status.shouldBe(HttpStatus.OK) + rsp.body().shouldBe("green") + } + } +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/context/annotation/primary/TestController.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/context/annotation/primary/TestController.kt new file mode 100644 index 00000000000..16400790075 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/context/annotation/primary/TestController.kt @@ -0,0 +1,32 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.context.annotation.primary + +import io.micronaut.context.annotation.Requires +import io.micronaut.http.annotation.Controller +import io.micronaut.http.annotation.Get + +@Requires(property = "spec.name", value = "primaryspec") +//tag::clazz[] +@Controller("/test") +class TestController(val colorPicker: ColorPicker) { // <1> + + @Get + fun index(): String { + return colorPicker.color() + } +} +//end::clazz[] \ No newline at end of file diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/context/env/DefaultEnvironmentSpec.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/context/env/DefaultEnvironmentSpec.kt new file mode 100644 index 00000000000..b61ce0f2e03 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/context/env/DefaultEnvironmentSpec.kt @@ -0,0 +1,17 @@ +package io.micronaut.docs.context.env + +import io.kotest.core.spec.style.StringSpec +import io.micronaut.context.ApplicationContext +import io.micronaut.context.env.Environment +import org.junit.Assert.assertFalse + +class DefaultEnvironmentSpec : StringSpec({ + + // tag::disableEnvDeduction[] + "test disable environment deduction via builder"() { + val ctx = ApplicationContext.builder().deduceEnvironment(false).start() + assertFalse(ctx.environment.activeNames.contains(Environment.TEST)) + ctx.close() + } + // end::disableEnvDeduction[] +}) diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/context/env/EnvironmentSpec.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/context/env/EnvironmentSpec.kt new file mode 100644 index 00000000000..388c75a99e0 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/context/env/EnvironmentSpec.kt @@ -0,0 +1,43 @@ +package io.micronaut.docs.context.env + +import io.kotest.core.spec.style.StringSpec +import io.micronaut.context.ApplicationContext +import io.micronaut.context.env.PropertySource +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue + +class EnvironmentSpec : StringSpec({ + + "test run environment" { + // tag::env[] + val applicationContext = ApplicationContext.run("test", "android") + val environment = applicationContext.environment + + assertTrue(environment.activeNames.contains("test")) + assertTrue(environment.activeNames.contains("android")) + // end::env[] + applicationContext.close() + } + + "test run environment with properties" { + // tag::envProps[] + val applicationContext = ApplicationContext.run( + PropertySource.of( + "test", + mapOf( + "micronaut.server.host" to "foo", + "micronaut.server.port" to 8080 + ) + ), + "test", "android" + ) + val environment = applicationContext.environment + + assertEquals( + "foo", + environment.getProperty("micronaut.server.host", String::class.java).orElse("localhost") + ) + // end::envProps[] + applicationContext.close() + } +}) diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/context/events/SampleEvent.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/context/events/SampleEvent.kt new file mode 100644 index 00000000000..df89e5c157f --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/context/events/SampleEvent.kt @@ -0,0 +1,19 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.context.events +// tag::class[] +data class SampleEvent(val message: String = "Something happened") +// end::class[] \ No newline at end of file diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/context/events/SampleEventEmitterBean.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/context/events/SampleEventEmitterBean.kt new file mode 100644 index 00000000000..a040a023fd5 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/context/events/SampleEventEmitterBean.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.context.events + +// tag::class[] +import io.micronaut.context.event.ApplicationEventPublisher +import jakarta.inject.Inject +import jakarta.inject.Singleton + +@Singleton +class SampleEventEmitterBean { + + @Inject + internal var eventPublisher: ApplicationEventPublisher? = null + + fun publishSampleEvent() { + eventPublisher!!.publishEvent(SampleEvent()) + } + +} +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/context/events/application/SampleEventListener.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/context/events/application/SampleEventListener.kt new file mode 100644 index 00000000000..9e269d3f1eb --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/context/events/application/SampleEventListener.kt @@ -0,0 +1,33 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.context.events.application + +// tag::imports[] +import io.micronaut.context.event.ApplicationEventListener +import io.micronaut.docs.context.events.SampleEvent +import jakarta.inject.Singleton +// end::imports[] + +// tag::class[] +@Singleton +class SampleEventListener : ApplicationEventListener { + var invocationCounter = 0 + + override fun onApplicationEvent(event: SampleEvent) { + invocationCounter++ + } +} +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/context/events/application/SampleEventListenerSpec.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/context/events/application/SampleEventListenerSpec.kt new file mode 100644 index 00000000000..fe1967802d6 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/context/events/application/SampleEventListenerSpec.kt @@ -0,0 +1,25 @@ +package io.micronaut.docs.context.events.application + +// tag::imports[] +import io.kotest.matchers.shouldBe +import io.kotest.core.spec.style.AnnotationSpec +import io.micronaut.context.ApplicationContext +import io.micronaut.docs.context.events.SampleEventEmitterBean +// end::imports[] + +// tag::class[] +class SampleEventListenerSpec : AnnotationSpec() { + + @Test + fun testEventListenerWasNotified() { + val context = ApplicationContext.run() + val emitter = context.getBean(SampleEventEmitterBean::class.java) + val listener = context.getBean(SampleEventListener::class.java) + listener.invocationCounter.shouldBe(0) + emitter.publishSampleEvent() + listener.invocationCounter.shouldBe(1) + + context.close() + } +} +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/context/events/async/SampleEventListener.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/context/events/async/SampleEventListener.kt new file mode 100644 index 00000000000..4e0ca3c3c5e --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/context/events/async/SampleEventListener.kt @@ -0,0 +1,39 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.context.events.async + +// tag::imports[] +import io.micronaut.docs.context.events.SampleEvent +import io.micronaut.runtime.event.annotation.EventListener +import io.micronaut.scheduling.annotation.Async +import java.util.concurrent.atomic.AtomicInteger +// end::imports[] +import jakarta.inject.Singleton + +// tag::class[] +@Singleton +open class SampleEventListener { + + var invocationCounter = AtomicInteger(0) + + @EventListener + @Async + open fun onSampleEvent(event: SampleEvent) { + println("Incrementing invocation counter...") + invocationCounter.getAndIncrement() + } +} +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/context/events/async/SampleEventListenerSpec.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/context/events/async/SampleEventListenerSpec.kt new file mode 100644 index 00000000000..81a441f7487 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/context/events/async/SampleEventListenerSpec.kt @@ -0,0 +1,35 @@ +package io.micronaut.docs.context.events.async + +// tag::imports[] +import io.kotest.assertions.timing.eventually +import io.kotest.matchers.shouldBe +import io.kotest.core.spec.style.AnnotationSpec +import io.micronaut.context.ApplicationContext +import io.micronaut.docs.context.events.SampleEventEmitterBean +import org.opentest4j.AssertionFailedError +import kotlin.time.DurationUnit +import kotlin.time.ExperimentalTime +import kotlin.time.toDuration +// end::imports[] + +// tag::class[] +@ExperimentalTime +class SampleEventListenerSpec : AnnotationSpec() { + + @Test + suspend fun testEventListenerWasNotified() { + val context = ApplicationContext.run() + val emitter = context.getBean(SampleEventEmitterBean::class.java) + val listener = context.getBean(SampleEventListener::class.java) + listener.invocationCounter.get().shouldBe(0) + emitter.publishSampleEvent() + + eventually(5.toDuration(DurationUnit.SECONDS), AssertionFailedError::class) { + println("Current value of counter: " + listener.invocationCounter.get()) + listener.invocationCounter.get().shouldBe(1) + } + + context.close() + } +} +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/context/events/listener/SampleEventListener.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/context/events/listener/SampleEventListener.kt new file mode 100644 index 00000000000..b5d8580cf17 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/context/events/listener/SampleEventListener.kt @@ -0,0 +1,46 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.context.events.listener + +// tag::imports[] +import io.micronaut.docs.context.events.SampleEvent +import io.micronaut.context.event.StartupEvent +import io.micronaut.context.event.ShutdownEvent +import io.micronaut.runtime.event.annotation.EventListener +// end::imports[] +import jakarta.inject.Singleton + +// tag::class[] +@Singleton +class SampleEventListener { + var invocationCounter = 0 + + @EventListener + internal fun onSampleEvent(event: SampleEvent) { + invocationCounter++ + } + + @EventListener + internal fun onStartupEvent(event: StartupEvent) { + // startup logic here + } + + @EventListener + internal fun onShutdownEvent(event: ShutdownEvent) { + // shutdown logic here + } +} +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/context/events/listener/SampleEventListenerSpec.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/context/events/listener/SampleEventListenerSpec.kt new file mode 100644 index 00000000000..14532cc948e --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/context/events/listener/SampleEventListenerSpec.kt @@ -0,0 +1,23 @@ +package io.micronaut.docs.context.events.listener + +import io.kotest.core.spec.style.AnnotationSpec +import io.kotest.matchers.shouldBe +import io.micronaut.context.ApplicationContext +import io.micronaut.docs.context.events.SampleEventEmitterBean + +// tag::class[] +class SampleEventListenerSpec : AnnotationSpec() { + + @Test + fun testEventListenerWasNotified() { + val context = ApplicationContext.run() + val emitter = context.getBean(SampleEventEmitterBean::class.java) + val listener = context.getBean(SampleEventListener::class.java) + listener.invocationCounter.shouldBe(0) + emitter.publishSampleEvent() + listener.invocationCounter.shouldBe(1) + + context.close() + } +} +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/datavalidation/groups/Email.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/datavalidation/groups/Email.kt new file mode 100644 index 00000000000..db0e6ef981f --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/datavalidation/groups/Email.kt @@ -0,0 +1,31 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.datavalidation.groups + +//tag::clazz[] +import io.micronaut.core.annotation.Introspected +import javax.validation.constraints.NotBlank + +@Introspected +open class Email { + + @NotBlank // <1> + var subject: String? = null + + @NotBlank(groups = [FinalValidation::class]) // <2> + var recipient: String? = null +} +//end::clazz[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/datavalidation/groups/EmailController.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/datavalidation/groups/EmailController.kt new file mode 100644 index 00000000000..3bb5439d8ed --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/datavalidation/groups/EmailController.kt @@ -0,0 +1,45 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.datavalidation.groups + +import io.micronaut.context.annotation.Requires +//tag::imports[] +import io.micronaut.http.HttpResponse +import io.micronaut.http.annotation.Body +import io.micronaut.http.annotation.Controller +import io.micronaut.http.annotation.Post +import io.micronaut.validation.Validated +import javax.validation.Valid +//end::imports[] + +@Requires(property = "spec.name", value = "datavalidationgroups") +//tag::clazz[] +@Validated // <1> +@Controller("/email") +open class EmailController { + + @Post("/createDraft") + open fun createDraft(@Body @Valid email: Email): HttpResponse<*> { // <2> + return HttpResponse.ok(mapOf("msg" to "OK")) + } + + @Post("/send") + @Validated(groups = [FinalValidation::class]) // <3> + open fun send(@Body @Valid email: Email): HttpResponse<*> { // <4> + return HttpResponse.ok(mapOf("msg" to "OK")) + } +} +//end::clazz[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/datavalidation/groups/EmailControllerSpec.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/datavalidation/groups/EmailControllerSpec.kt new file mode 100644 index 00000000000..994781d828c --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/datavalidation/groups/EmailControllerSpec.kt @@ -0,0 +1,81 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.datavalidation.groups + +import io.kotest.matchers.shouldBe +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.core.spec.style.StringSpec +import io.micronaut.context.ApplicationContext +import io.micronaut.http.HttpRequest +import io.micronaut.http.HttpStatus +import io.micronaut.http.client.HttpClient +import io.micronaut.http.client.exceptions.HttpClientResponseException +import io.micronaut.runtime.server.EmbeddedServer + +class EmailControllerSpec: StringSpec() { + + val embeddedServer = autoClose( + ApplicationContext.run(EmbeddedServer::class.java, mapOf("spec.name" to "datavalidationgroups")) + ) + + val client = autoClose( + embeddedServer.applicationContext.createBean(HttpClient::class.java, embeddedServer.url) + ) + + init { + //tag::pojovalidateddefault[] + "test pojo validation using default validation groups" { + val e = shouldThrow { + val email = Email() + email.subject = "" + email.recipient = "" + client.toBlocking().exchange(HttpRequest.POST("/email/createDraft", email)) + } + var response = e.response + + response.status shouldBe HttpStatus.BAD_REQUEST + + val email = Email() + email.subject = "Hi" + email.recipient = "" + response = client.toBlocking().exchange(HttpRequest.POST("/email/createDraft", email)) + + response.status shouldBe HttpStatus.OK + } + //end::pojovalidateddefault[] + + //tag::pojovalidatedfinal[] + "test pojo validation using FinalValidation validation group" { + val e = shouldThrow { + val email = Email() + email.subject = "Hi" + email.recipient = "" + client.toBlocking().exchange(HttpRequest.POST("/email/send", email)) + } + var response = e.response + + response.status shouldBe HttpStatus.BAD_REQUEST + + val email = Email() + email.subject = "Hi" + email.recipient = "me@micronaut.example" + response = client.toBlocking().exchange(HttpRequest.POST("/email/send", email)) + + response.status shouldBe HttpStatus.OK + } + //end::pojovalidatedfinal[] + } +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/datavalidation/groups/FinalValidation.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/datavalidation/groups/FinalValidation.kt new file mode 100644 index 00000000000..a8fe62a3e1d --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/datavalidation/groups/FinalValidation.kt @@ -0,0 +1,24 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.datavalidation.groups + +//tag::clazz[] + +import javax.validation.groups.Default + +interface FinalValidation : Default {} // <1> + +//end::clazz[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/datavalidation/params/EmailController.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/datavalidation/params/EmailController.kt new file mode 100644 index 00000000000..66bd525b9c9 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/datavalidation/params/EmailController.kt @@ -0,0 +1,39 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.datavalidation.params + +import io.micronaut.context.annotation.Requires +//tag::imports[] +import io.micronaut.http.HttpResponse +import io.micronaut.http.annotation.Controller +import io.micronaut.http.annotation.Get +import io.micronaut.validation.Validated +import javax.validation.constraints.NotBlank +//end::imports[] + +@Requires(property = "spec.name", value = "datavalidationparams") +//tag::clazz[] +@Validated // <1> +@Controller("/email") +open class EmailController { + + @Get("/send") + open fun send(@NotBlank recipient: String, // <2> + @NotBlank subject: String): HttpResponse<*> { // <2> + return HttpResponse.ok(mapOf("msg" to "OK")) + } +} +//end::clazz[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/datavalidation/params/EmailControllerSpec.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/datavalidation/params/EmailControllerSpec.kt new file mode 100644 index 00000000000..dd163e21500 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/datavalidation/params/EmailControllerSpec.kt @@ -0,0 +1,38 @@ +package io.micronaut.docs.datavalidation.params + +import io.kotest.matchers.shouldBe +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.core.spec.style.StringSpec +import io.micronaut.context.ApplicationContext +import io.micronaut.http.HttpStatus +import io.micronaut.http.client.HttpClient +import io.micronaut.http.client.exceptions.HttpClientResponseException +import io.micronaut.runtime.server.EmbeddedServer + +class EmailControllerSpec: StringSpec() { + + val embeddedServer = autoClose( + ApplicationContext.run(EmbeddedServer::class.java, mapOf("spec.name" to "datavalidationparams")) + ) + + val client = autoClose( + embeddedServer.applicationContext.createBean(HttpClient::class.java, embeddedServer.url) + ) + + init { + //tag::paramsvalidated[] + "test params are validated"() { + val e = shouldThrow { + client.toBlocking().exchange("/email/send?subject=Hi&recipient=") + } + var response = e.response + + response.status shouldBe HttpStatus.BAD_REQUEST + + response = client.toBlocking().exchange("/email/send?subject=Hi&recipient=me@micronaut.example") + + response.status shouldBe HttpStatus.OK + } + //end::paramsvalidated[] + } +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/datavalidation/pogo/Email.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/datavalidation/pogo/Email.kt new file mode 100644 index 00000000000..970db1d7b7b --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/datavalidation/pogo/Email.kt @@ -0,0 +1,31 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.datavalidation.pogo + +//tag::clazz[] +import io.micronaut.core.annotation.Introspected +import javax.validation.constraints.NotBlank + +@Introspected +open class Email { + + @NotBlank // <1> + var subject: String? = null + + @NotBlank // <1> + var recipient: String? = null +} +//end::clazz[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/datavalidation/pogo/EmailController.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/datavalidation/pogo/EmailController.kt new file mode 100644 index 00000000000..7933d0c764a --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/datavalidation/pogo/EmailController.kt @@ -0,0 +1,39 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.datavalidation.pogo + +import io.micronaut.context.annotation.Requires +//tag::imports[] +import io.micronaut.http.HttpResponse +import io.micronaut.http.annotation.Body +import io.micronaut.http.annotation.Controller +import io.micronaut.http.annotation.Post +import io.micronaut.validation.Validated +import javax.validation.Valid +//end::imports[] + +@Requires(property = "spec.name", value = "datavalidationpogo") +//tag::clazz[] +@Validated // <1> +@Controller("/email") +open class EmailController { + + @Post("/send") + open fun send(@Body @Valid email: Email): HttpResponse<*> { // <2> + return HttpResponse.ok(mapOf("msg" to "OK")) + } +} +//end::clazz[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/datavalidation/pogo/EmailControllerSpec.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/datavalidation/pogo/EmailControllerSpec.kt new file mode 100644 index 00000000000..c74cd35edd7 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/datavalidation/pogo/EmailControllerSpec.kt @@ -0,0 +1,45 @@ +package io.micronaut.docs.datavalidation.pogo + +import io.kotest.matchers.shouldBe +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.core.spec.style.StringSpec +import io.micronaut.context.ApplicationContext +import io.micronaut.http.HttpRequest +import io.micronaut.http.HttpStatus +import io.micronaut.http.client.HttpClient +import io.micronaut.http.client.exceptions.HttpClientResponseException +import io.micronaut.runtime.server.EmbeddedServer + +class EmailControllerSpec: StringSpec() { + + val embeddedServer = autoClose( + ApplicationContext.run(EmbeddedServer::class.java, mapOf("spec.name" to "datavalidationpogo")) + ) + + val client = autoClose( + embeddedServer.applicationContext.createBean(HttpClient::class.java, embeddedServer.url) + ) + + init { + //tag::pojovalidated[] + "test pojo validation" { + val e = shouldThrow { + val email = Email() + email.subject = "Hi" + email.recipient = "" + client.toBlocking().exchange(HttpRequest.POST("/email/send", email)) + } + var response = e.response + + response.status shouldBe HttpStatus.BAD_REQUEST + + val email = Email() + email.subject = "Hi" + email.recipient = "me@micronaut.example" + response = client.toBlocking().exchange(HttpRequest.POST("/email/send", email)) + + response.status shouldBe HttpStatus.OK + } + //end::pojovalidated[] + } +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/events/factory/Engine.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/events/factory/Engine.kt new file mode 100644 index 00000000000..3057e797081 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/events/factory/Engine.kt @@ -0,0 +1,23 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.events.factory + +// tag::class[] +interface Engine { + val cylinders: Int + fun start(): String +} +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/events/factory/EngineFactory.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/events/factory/EngineFactory.kt new file mode 100644 index 00000000000..b142ad5cce5 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/events/factory/EngineFactory.kt @@ -0,0 +1,44 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.events.factory + +import io.micronaut.context.annotation.Factory + +import jakarta.annotation.PostConstruct +import jakarta.inject.Singleton + +// tag::class[] +@Factory +class EngineFactory { + + private var engine: V8Engine? = null + private var rodLength = 5.7 + + @PostConstruct + fun initialize() { + engine = V8Engine(rodLength) // <2> + } + + @Singleton + fun v8Engine(): Engine? { + return engine// <3> + } + + fun setRodLength(rodLength: Double) { + this.rodLength = rodLength + } +} +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/events/factory/EngineInitializer.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/events/factory/EngineInitializer.kt new file mode 100644 index 00000000000..b1e9b9f6c53 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/events/factory/EngineInitializer.kt @@ -0,0 +1,32 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.events.factory + +import io.micronaut.context.event.BeanInitializedEventListener +import io.micronaut.context.event.BeanInitializingEvent + +import jakarta.inject.Singleton + +// tag::class[] +@Singleton +class EngineInitializer : BeanInitializedEventListener { // <4> + override fun onInitialized(event: BeanInitializingEvent): EngineFactory { + val engineFactory = event.bean + engineFactory.setRodLength(6.6) // <5> + return engineFactory + } +} +// tag::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/events/factory/V8Engine.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/events/factory/V8Engine.kt new file mode 100644 index 00000000000..189d1e46b05 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/events/factory/V8Engine.kt @@ -0,0 +1,27 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.events.factory + +// tag::class[] +class V8Engine(var rodLength: Double) : Engine { // <1> + + override val cylinders = 8 + + override fun start(): String { + return "Starting V$cylinders [rodLength=$rodLength]" + } +} +// end::class[] \ No newline at end of file diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/events/factory/Vehicle.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/events/factory/Vehicle.kt new file mode 100644 index 00000000000..4b48d41ea8f --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/events/factory/Vehicle.kt @@ -0,0 +1,26 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.events.factory + +import jakarta.inject.Singleton + +@Singleton +class Vehicle(val engine: Engine) { + + fun start(): String { + return engine.start() + } +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/events/factory/VehicleSpec.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/events/factory/VehicleSpec.kt new file mode 100644 index 00000000000..20ef41d36ba --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/events/factory/VehicleSpec.kt @@ -0,0 +1,25 @@ +package io.micronaut.docs.events.factory + +import io.kotest.matchers.shouldBe +import io.kotest.core.spec.style.StringSpec +import io.micronaut.context.BeanContext +import io.micronaut.context.DefaultBeanContext +import org.junit.Test + +import org.junit.Assert.assertEquals + +class VehicleSpec : StringSpec({ + + "test start vehicle" { + // tag::start[] + val context = BeanContext.run() + val vehicle = context + .getBean(Vehicle::class.java) + println(vehicle.start()) + // end::start[] + + vehicle.start().shouldBe("Starting V8 [rodLength=6.6]") + context.close() + } + +}) diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/factories/CrankShaft.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/factories/CrankShaft.kt new file mode 100644 index 00000000000..8f3f179a63a --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/factories/CrankShaft.kt @@ -0,0 +1,23 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.factories + +import jakarta.inject.Singleton + +// tag::class[] +@Singleton +internal class CrankShaft +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/factories/Engine.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/factories/Engine.kt new file mode 100644 index 00000000000..bb0a39a7f6f --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/factories/Engine.kt @@ -0,0 +1,22 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.factories + +// tag::class[] +interface Engine { + fun start(): String +} +// end::class[] \ No newline at end of file diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/factories/EngineFactory.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/factories/EngineFactory.kt new file mode 100644 index 00000000000..ab63495cd32 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/factories/EngineFactory.kt @@ -0,0 +1,36 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.factories + +import io.micronaut.context.annotation.Bean +import io.micronaut.context.annotation.Factory + +import jakarta.inject.Singleton + +/** + * @author Graeme Rocher + * @since 1.0 + */ +// tag::class[] +@Factory +internal class EngineFactory { + + @Singleton + fun v8Engine(crankShaft: CrankShaft): Engine { + return V8Engine(crankShaft) + } +} +// tag::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/factories/V8Engine.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/factories/V8Engine.kt new file mode 100644 index 00000000000..5fd6f820043 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/factories/V8Engine.kt @@ -0,0 +1,26 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.factories + +// tag::class[] +internal class V8Engine(private val crankShaft: CrankShaft) : Engine { + private val cylinders = 8 + + override fun start(): String { + return "Starting V8" + } +} +// end::class[] \ No newline at end of file diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/factories/Vehicle.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/factories/Vehicle.kt new file mode 100644 index 00000000000..6e0b0fb4fd9 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/factories/Vehicle.kt @@ -0,0 +1,28 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.factories + +import jakarta.inject.Singleton + +// tag::class[] +@Singleton +class Vehicle(val engine: Engine) { + + fun start(): String { + return engine.start() + } +} +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/factories/VehicleMockSpec.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/factories/VehicleMockSpec.kt new file mode 100644 index 00000000000..bdcb2d73b22 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/factories/VehicleMockSpec.kt @@ -0,0 +1,32 @@ +package io.micronaut.docs.factories + +// tag::imports[] +import io.micronaut.context.annotation.Bean +import io.micronaut.context.annotation.Replaces +import io.micronaut.test.annotation.MockBean +import io.micronaut.test.extensions.junit5.annotation.MicronautTest +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Test +import jakarta.inject.Inject +// end::imports[] + +// tag::class[] +@MicronautTest +class VehicleMockSpec { + @MockBean(Engine::class) + val mockEngine: Engine = object : Engine { // <1> + override fun start(): String { + return "Mock Started" + } + } + + @Inject + lateinit var vehicle : Vehicle // <2> + + @Test + fun testStartEngine() { + val result = vehicle.start() + Assertions.assertEquals("Mock Started", result) // <3> + } +} +// tag::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/factories/VehicleSpec.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/factories/VehicleSpec.kt new file mode 100644 index 00000000000..4ee90674d38 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/factories/VehicleSpec.kt @@ -0,0 +1,22 @@ +package io.micronaut.docs.factories + +import io.micronaut.context.BeanContext +import io.micronaut.context.DefaultBeanContext +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test + +internal class VehicleSpec { + + @Test + fun testStartVehicle() { + // tag::start[] + val context = BeanContext.run() + val vehicle = context + .getBean(Vehicle::class.java) + println(vehicle.start()) + // end::start[] + + assertEquals("Starting V8", vehicle.start()) + context.close() + } +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/factories/nullable/Engine.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/factories/nullable/Engine.kt new file mode 100644 index 00000000000..dca422eb79c --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/factories/nullable/Engine.kt @@ -0,0 +1,22 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.factories.nullable + +// tag::class[] +interface Engine { + fun getCylinders(): Int +} +// end::class[] \ No newline at end of file diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/factories/nullable/EngineConfiguration.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/factories/nullable/EngineConfiguration.kt new file mode 100644 index 00000000000..9f925b06b0b --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/factories/nullable/EngineConfiguration.kt @@ -0,0 +1,35 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.factories.nullable + +import io.micronaut.context.annotation.EachProperty +import io.micronaut.core.util.Toggleable +import javax.validation.constraints.NotNull + +// tag::class[] +@EachProperty("engines") +class EngineConfiguration : Toggleable { + + var enabled = true + + @NotNull + val cylinders: Int? = null + + override fun isEnabled(): Boolean { + return enabled + } +} +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/factories/nullable/EngineFactory.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/factories/nullable/EngineFactory.kt new file mode 100644 index 00000000000..e8e6c303f6a --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/factories/nullable/EngineFactory.kt @@ -0,0 +1,39 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.factories.nullable + +import io.micronaut.context.annotation.EachBean +import io.micronaut.context.annotation.Factory +import io.micronaut.context.exceptions.DisabledBeanException + +// tag::class[] +@Factory +class EngineFactory { + + @EachBean(EngineConfiguration::class) + fun buildEngine(engineConfiguration: EngineConfiguration): Engine? { + return if (engineConfiguration.isEnabled) { + object : Engine { + override fun getCylinders(): Int { + return engineConfiguration.cylinders!! + } + } + } else { + throw DisabledBeanException("Engine configuration disabled") + } + } +} +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/factories/nullable/EngineSpec.java b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/factories/nullable/EngineSpec.java new file mode 100644 index 00000000000..b055e913d38 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/factories/nullable/EngineSpec.java @@ -0,0 +1,44 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.factories.nullable; + +import io.micronaut.context.ApplicationContext; +import org.junit.Test; + +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; + +import static org.junit.Assert.assertEquals; + +public class EngineSpec { + + @Test + public void testEngineNull() { + Map configuration = new HashMap<>(); + configuration.put("engines.subaru.cylinders", 4); + configuration.put("engines.ford.cylinders", 8); + configuration.put("engines.ford.enabled", false); + configuration.put("engines.lamborghini.cylinders", 12); + ApplicationContext applicationContext = ApplicationContext.run(configuration); + + Collection engines = applicationContext.getBeansOfType(Engine.class); + + assertEquals("There are 2 engines", 2, engines.size()); + int totalCylinders = engines.stream().mapToInt(Engine::getCylinders).sum(); + assertEquals("Subaru + Lamborghini equals 16 cylinders", 16, totalCylinders); + } +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/factories/primitive/CylinderFactory.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/factories/primitive/CylinderFactory.kt new file mode 100644 index 00000000000..3325c0e0007 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/factories/primitive/CylinderFactory.kt @@ -0,0 +1,20 @@ +package io.micronaut.docs.factories.primitive + +// tag::imports[] +import io.micronaut.context.annotation.Bean +import io.micronaut.context.annotation.Factory +import jakarta.inject.Named +// end::imports[] + +// tag::class[] +@Factory +class CylinderFactory { + @get:Bean + @get:Named("V8") // <1> + val v8 = 8 + + @get:Bean + @get:Named("V6") // <1> + val v6 = 6 +} +// end::class[] \ No newline at end of file diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/factories/primitive/EngineSpec.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/factories/primitive/EngineSpec.kt new file mode 100644 index 00000000000..c8dfc11bd41 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/factories/primitive/EngineSpec.kt @@ -0,0 +1,21 @@ +package io.micronaut.docs.factories.primitive + +import io.micronaut.context.BeanContext +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Test + +class EngineSpec { + @Test + fun testEngine() { + BeanContext.run().use { beanContext -> + val engine = + beanContext.getBean( + V8Engine::class.java + ) + Assertions.assertEquals( + 8, + engine.cylinders + ) + } + } +} \ No newline at end of file diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/factories/primitive/V8Engine.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/factories/primitive/V8Engine.kt new file mode 100644 index 00000000000..a902155ca26 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/factories/primitive/V8Engine.kt @@ -0,0 +1,13 @@ +package io.micronaut.docs.factories.primitive + +// tag::imports[] +import jakarta.inject.Named +import jakarta.inject.Singleton +// end::imports[] + +// tag::class[] +@Singleton +class V8Engine( + @param:Named("V8") val cylinders: Int // <1> +) +// end::class[] \ No newline at end of file diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/client/bind/ClientBindController.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/client/bind/ClientBindController.kt new file mode 100644 index 00000000000..f73d7adcdd7 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/client/bind/ClientBindController.kt @@ -0,0 +1,21 @@ +package io.micronaut.docs.http.client.bind + +import io.micronaut.http.annotation.Controller +import io.micronaut.http.annotation.Get +import io.micronaut.http.annotation.Header +import io.micronaut.http.annotation.QueryValue +import javax.annotation.Nullable + +@Controller +class ClientBindController { + + @Get("/client/bind") + fun test(@Header("X-Metadata-Version") version: String): String { + return version + } + + @Get("/client/authorized-resource{?name}") + fun authorized(@QueryValue @Nullable name: String): String { + return "Hello, $name" + } +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/client/bind/annotation/AnnotationBinderSpec.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/client/bind/annotation/AnnotationBinderSpec.kt new file mode 100644 index 00000000000..3db5fcd8526 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/client/bind/annotation/AnnotationBinderSpec.kt @@ -0,0 +1,23 @@ +package io.micronaut.docs.http.client.bind.annotation + +import io.micronaut.context.ApplicationContext +import io.micronaut.runtime.server.EmbeddedServer +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Test +import java.util.* + +class AnnotationBinderSpec { + + @Test + fun testBindingToTheRequest() { + val server = ApplicationContext.run(EmbeddedServer::class.java) + val client = server.applicationContext.getBean(MetadataClient::class.java) + + val metadata: MutableMap = LinkedHashMap() + metadata["version"] = 3.6 + metadata["deploymentId"] = 42L + val resp = client.get(metadata) + Assertions.assertEquals("3.6", resp) + server.close() + } +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/client/bind/annotation/Metadata.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/client/bind/annotation/Metadata.kt new file mode 100644 index 00000000000..e78e124c85d --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/client/bind/annotation/Metadata.kt @@ -0,0 +1,13 @@ +package io.micronaut.docs.http.client.bind.annotation + +//tag::clazz[] +import io.micronaut.core.bind.annotation.Bindable +import kotlin.annotation.AnnotationRetention.RUNTIME +import kotlin.annotation.AnnotationTarget.VALUE_PARAMETER + +@MustBeDocumented +@Retention(RUNTIME) +@Target(VALUE_PARAMETER) +@Bindable +annotation class Metadata +//end::clazz[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/client/bind/annotation/MetadataClient.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/client/bind/annotation/MetadataClient.kt new file mode 100644 index 00000000000..eb19dff9ad4 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/client/bind/annotation/MetadataClient.kt @@ -0,0 +1,13 @@ +package io.micronaut.docs.http.client.bind.annotation + +import io.micronaut.http.annotation.Get +import io.micronaut.http.client.annotation.Client + +//tag::clazz[] +@Client("/") +interface MetadataClient { + + @Get("/client/bind") + operator fun get(@Metadata metadata: Map): String +} +//end::clazz[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/client/bind/annotation/MetadataClientArgumentBinder.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/client/bind/annotation/MetadataClientArgumentBinder.kt new file mode 100644 index 00000000000..89aeace9955 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/client/bind/annotation/MetadataClientArgumentBinder.kt @@ -0,0 +1,31 @@ +package io.micronaut.docs.http.client.bind.annotation + +//tag::clazz[] +import io.micronaut.core.convert.ArgumentConversionContext +import io.micronaut.core.naming.NameUtils +import io.micronaut.core.util.StringUtils +import io.micronaut.http.MutableHttpRequest +import io.micronaut.http.client.bind.AnnotatedClientArgumentRequestBinder +import io.micronaut.http.client.bind.ClientRequestUriContext +import jakarta.inject.Singleton + +@Singleton +class MetadataClientArgumentBinder : AnnotatedClientArgumentRequestBinder { + + override fun getAnnotationType(): Class { + return Metadata::class.java + } + + override fun bind(context: ArgumentConversionContext, + uriContext: ClientRequestUriContext, + value: Any, + request: MutableHttpRequest<*>) { + if (value is Map<*, *>) { + for ((key1, value1) in value) { + val key = NameUtils.hyphenate(StringUtils.capitalize(key1.toString()), false) + request.header("X-Metadata-$key", value1.toString()) + } + } + } +} +//end::clazz[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/client/bind/method/MethodBinderSpec.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/client/bind/method/MethodBinderSpec.kt new file mode 100644 index 00000000000..0c544111b06 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/client/bind/method/MethodBinderSpec.kt @@ -0,0 +1,20 @@ +package io.micronaut.docs.http.client.bind.method; + +import io.micronaut.context.ApplicationContext +import io.micronaut.runtime.server.EmbeddedServer +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Test + +class MethodBinderSpec { + + @Test + fun testBindingToTheRequest() { + val server = ApplicationContext.run(EmbeddedServer::class.java) + val client = server.applicationContext.getBean(NameAuthorizedClient::class.java) + + val resp = client.get() + Assertions.assertEquals("Hello, Bob", resp) + + server.close() + } +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/client/bind/method/NameAuthorization.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/client/bind/method/NameAuthorization.kt new file mode 100644 index 00000000000..eb0118d1138 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/client/bind/method/NameAuthorization.kt @@ -0,0 +1,15 @@ +package io.micronaut.docs.http.client.bind.method; + +import io.micronaut.context.annotation.AliasFor +import io.micronaut.core.bind.annotation.Bindable +import kotlin.annotation.AnnotationRetention.RUNTIME +import kotlin.annotation.AnnotationTarget.FUNCTION + +//tag::clazz[] +@MustBeDocumented +@Retention(RUNTIME) +@Target(FUNCTION) // <1> +@Bindable +annotation class NameAuthorization(val name: String = "") + +//end::clazz[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/client/bind/method/NameAuthorizationBinder.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/client/bind/method/NameAuthorizationBinder.kt new file mode 100644 index 00000000000..41f3244bb01 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/client/bind/method/NameAuthorizationBinder.kt @@ -0,0 +1,29 @@ +package io.micronaut.docs.http.client.bind.method; + +import io.micronaut.aop.MethodInvocationContext; +import io.micronaut.core.annotation.NonNull; +import io.micronaut.http.MutableHttpRequest; +import io.micronaut.http.client.bind.ClientRequestUriContext; +import jakarta.inject.Singleton; + +//tag::clazz[] +import io.micronaut.http.client.bind.AnnotatedClientRequestBinder + +@Singleton // <1> +class NameAuthorizationBinder: AnnotatedClientRequestBinder { // <2> + @NonNull + override fun getAnnotationType(): Class { + return NameAuthorization::class.java + } + + override fun bind( // <3> + @NonNull context: MethodInvocationContext, + @NonNull uriContext: ClientRequestUriContext, + @NonNull request: MutableHttpRequest<*> + ) { + context.getValue(NameAuthorization::class.java, "name") + .ifPresent { name -> uriContext.addQueryParameter("name", name.toString()) } + + } +} +//end::clazz[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/client/bind/method/NameAuthorizedClient.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/client/bind/method/NameAuthorizedClient.kt new file mode 100644 index 00000000000..28fbe4dc1ab --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/client/bind/method/NameAuthorizedClient.kt @@ -0,0 +1,14 @@ +package io.micronaut.docs.http.client.bind.method; + +import io.micronaut.http.annotation.Get; +import io.micronaut.http.client.annotation.Client; + +//tag::clazz[] +@Client("/") +public interface NameAuthorizedClient { + + @Get("/client/authorized-resource") + @NameAuthorization(name="Bob") // <1> + fun get(): String +} +//end::clazz[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/client/bind/type/CustomBinderSpec.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/client/bind/type/CustomBinderSpec.kt new file mode 100644 index 00000000000..c3bd68aa922 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/client/bind/type/CustomBinderSpec.kt @@ -0,0 +1,18 @@ +package io.micronaut.docs.http.client.bind.type + +import io.micronaut.context.ApplicationContext +import io.micronaut.runtime.server.EmbeddedServer +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Test + +class CustomBinderSpec { + + @Test + fun testBindingToTheRequest() { + val server = ApplicationContext.run(EmbeddedServer::class.java) + val client = server.applicationContext.getBean(MetadataClient::class.java) + val resp = client.get(Metadata(3.6, 42L)) + Assertions.assertEquals("3.6", resp) + server.close() + } +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/client/bind/type/Metadata.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/client/bind/type/Metadata.kt new file mode 100644 index 00000000000..d75d2eba32c --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/client/bind/type/Metadata.kt @@ -0,0 +1,3 @@ +package io.micronaut.docs.http.client.bind.type + +class Metadata(val version: Double, val deploymentId: Long) diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/client/bind/type/MetadataClient.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/client/bind/type/MetadataClient.kt new file mode 100644 index 00000000000..819017883af --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/client/bind/type/MetadataClient.kt @@ -0,0 +1,13 @@ +package io.micronaut.docs.http.client.bind.type + +import io.micronaut.http.annotation.Get +import io.micronaut.http.client.annotation.Client + +//tag::clazz[] +@Client("/") +interface MetadataClient { + + @Get("/client/bind") + operator fun get(metadata: Metadata?): String? +} +//end::clazz[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/client/bind/type/MetadataClientArgumentBinder.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/client/bind/type/MetadataClientArgumentBinder.kt new file mode 100644 index 00000000000..60fcd65bfa1 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/client/bind/type/MetadataClientArgumentBinder.kt @@ -0,0 +1,28 @@ +package io.micronaut.docs.http.client.bind.type + +//tag::clazz[] +import io.micronaut.core.convert.ArgumentConversionContext +import io.micronaut.core.type.Argument +import io.micronaut.http.MutableHttpRequest +import io.micronaut.http.client.bind.ClientRequestUriContext +import io.micronaut.http.client.bind.TypedClientArgumentRequestBinder +import jakarta.inject.Singleton + +@Singleton +class MetadataClientArgumentBinder : TypedClientArgumentRequestBinder { + + override fun argumentType(): Argument { + return Argument.of(Metadata::class.java) + } + + override fun bind( + context: ArgumentConversionContext, + uriContext: ClientRequestUriContext, + value: Metadata, + request: MutableHttpRequest<*> + ) { + request.header("X-Metadata-Version", value.version.toString()) + request.header("X-Metadata-Deployment-Id", value.deploymentId.toString()) + } +} +//end::clazz[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/client/proxy/ProxyFilter.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/client/proxy/ProxyFilter.kt new file mode 100644 index 00000000000..27ee9d40dd6 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/client/proxy/ProxyFilter.kt @@ -0,0 +1,42 @@ +package io.micronaut.docs.http.client.proxy + +// tag::imports[] +import io.micronaut.core.async.publisher.Publishers +import io.micronaut.core.util.StringUtils +import io.micronaut.http.HttpRequest +import io.micronaut.http.MutableHttpResponse +import io.micronaut.http.annotation.Filter +import io.micronaut.http.client.ProxyHttpClient +import io.micronaut.http.filter.HttpServerFilter +import io.micronaut.http.filter.ServerFilterChain +import io.micronaut.http.uri.UriBuilder +import io.micronaut.runtime.server.EmbeddedServer +import org.reactivestreams.Publisher +// end::imports[] + +// tag::class[] +@Filter("/proxy/**") +class ProxyFilter( + private val client: ProxyHttpClient, // <2> + private val embeddedServer: EmbeddedServer +) : HttpServerFilter { // <1> + + override fun doFilter(request: HttpRequest<*>, + chain: ServerFilterChain): Publisher> { + return Publishers.map(client.proxy( // <3> + request.mutate() // <4> + .uri { b: UriBuilder -> // <5> + b.apply { + scheme("http") + host(embeddedServer.host) + port(embeddedServer.port) + replacePath(StringUtils.prependUri( + "/real", + request.path.substring("/proxy".length)) + ) + } + } + .header("X-My-Request-Header", "XXX") // <6> + ), { response: MutableHttpResponse<*> -> response.header("X-My-Response-Header", "YYY") }) + } +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/server/bind/ShoppingCartControllerTests.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/server/bind/ShoppingCartControllerTests.kt new file mode 100644 index 00000000000..33ac7b0287b --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/server/bind/ShoppingCartControllerTests.kt @@ -0,0 +1,59 @@ +package io.micronaut.docs.http.server.bind + +import io.kotest.matchers.shouldBe +import io.kotest.matchers.shouldNotBe +import io.kotest.core.spec.style.StringSpec +import io.micronaut.context.ApplicationContext +import io.micronaut.core.type.Argument +import io.micronaut.http.HttpRequest +import io.micronaut.http.client.HttpClient +import io.micronaut.http.client.exceptions.HttpClientResponseException +import io.micronaut.http.cookie.Cookie +import io.micronaut.runtime.server.EmbeddedServer +import org.junit.jupiter.api.Assertions + +class ShoppingCartControllerTest: StringSpec(){ + + val embeddedServer = autoClose( + ApplicationContext.run(EmbeddedServer::class.java) + ) + + val client = autoClose( + embeddedServer.applicationContext.createBean(HttpClient::class.java, embeddedServer.url) + ) + + init { + "test binding bad credentials" { + val request: HttpRequest<*> = HttpRequest.GET("/customBinding/annotated") + .cookie(Cookie.of("shoppingCart", "{}")) + + val responseException = Assertions.assertThrows(HttpClientResponseException::class.java) { + client.toBlocking().retrieve(request) + } + val embedded: Map<*, *> = responseException.response.getBody(Map::class.java).get().get("_embedded") as Map<*, *> + val message = ((embedded.get("errors") as java.util.List<*>).get(0) as Map<*, *>).get("message") + + responseException shouldNotBe null + message shouldBe "Required ShoppingCart [sessionId] not specified" + } + + "test annotation binding" { + val request: HttpRequest<*> = HttpRequest.GET("/customBinding/annotated") + .cookie(Cookie.of("shoppingCart", "{\"sessionId\":5}")) + val response: String = client.toBlocking().retrieve(request, String::class.java) + + response shouldNotBe null + response shouldBe "Session:5" + } + + "test typed binding" { + val request: HttpRequest<*> = HttpRequest.GET("/customBinding/typed") + .cookie(Cookie.of("shoppingCart", "{\"sessionId\": 5, \"total\": 20}")) + val body: Map = client.toBlocking().retrieve(request, Argument.mapOf(String::class.java, Any::class.java)) + + body shouldNotBe null + body["sessionId"] shouldBe "5" + body["total"] shouldBe 20 + } + } +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/server/bind/annotation/ShoppingCart.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/server/bind/annotation/ShoppingCart.kt new file mode 100644 index 00000000000..530483cfa50 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/server/bind/annotation/ShoppingCart.kt @@ -0,0 +1,14 @@ +package io.micronaut.docs.http.server.bind.annotation + +// tag::class[] +import io.micronaut.core.bind.annotation.Bindable +import kotlin.annotation.AnnotationRetention.RUNTIME +import kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS +import kotlin.annotation.AnnotationTarget.FIELD +import kotlin.annotation.AnnotationTarget.VALUE_PARAMETER + +@Target(FIELD, VALUE_PARAMETER, ANNOTATION_CLASS) +@Retention(RUNTIME) +@Bindable //<1> +annotation class ShoppingCart(val value: String = "") +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/server/bind/annotation/ShoppingCartController.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/server/bind/annotation/ShoppingCartController.kt new file mode 100644 index 00000000000..c8c80c029eb --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/server/bind/annotation/ShoppingCartController.kt @@ -0,0 +1,16 @@ +package io.micronaut.docs.http.server.bind.annotation + +import io.micronaut.http.HttpResponse +import io.micronaut.http.annotation.Controller +import io.micronaut.http.annotation.Get + +@Controller("/customBinding") +class ShoppingCartController { + + // tag::method[] + @Get("/annotated") + fun checkSession(@ShoppingCart sessionId: Long): HttpResponse { //<1> + return HttpResponse.ok("Session:$sessionId") + } + // end::method[] +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/server/bind/annotation/ShoppingCartRequestArgumentBinder.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/server/bind/annotation/ShoppingCartRequestArgumentBinder.kt new file mode 100644 index 00000000000..c6f7c3653b8 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/server/bind/annotation/ShoppingCartRequestArgumentBinder.kt @@ -0,0 +1,44 @@ +package io.micronaut.docs.http.server.bind.annotation + +// tag::class[] +import io.micronaut.core.bind.ArgumentBinder.BindingResult +import io.micronaut.core.convert.ArgumentConversionContext +import io.micronaut.core.convert.ConversionService +import io.micronaut.core.type.Argument +import io.micronaut.http.HttpRequest +import io.micronaut.http.bind.binders.AnnotatedRequestArgumentBinder +import io.micronaut.jackson.serialize.JacksonObjectSerializer +import java.util.Optional +import jakarta.inject.Singleton + +@Singleton +class ShoppingCartRequestArgumentBinder( + private val conversionService: ConversionService, + private val objectSerializer: JacksonObjectSerializer +) : AnnotatedRequestArgumentBinder { //<1> + + override fun getAnnotationType(): Class { + return ShoppingCart::class.java + } + + override fun bind(context: ArgumentConversionContext, + source: HttpRequest<*>): BindingResult { //<2> + + val parameterName = context.annotationMetadata + .stringValue(ShoppingCart::class.java) + .orElse(context.argument.name) + + val cookie = source.cookies.get("shoppingCart") ?: return BindingResult.EMPTY + + val cookieValue: Optional> = objectSerializer.deserialize( + cookie.value.toByteArray(), + Argument.mapOf(String::class.java, Any::class.java)) + + return BindingResult { + cookieValue.flatMap { map: Map -> + conversionService.convert(map[parameterName], context) + } + } + } +} +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/server/bind/type/ShoppingCart.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/server/bind/type/ShoppingCart.kt new file mode 100644 index 00000000000..19c95471948 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/server/bind/type/ShoppingCart.kt @@ -0,0 +1,11 @@ +package io.micronaut.docs.http.server.bind.type + +// tag::class[] +import io.micronaut.core.annotation.Introspected + +@Introspected +class ShoppingCart { + var sessionId: String? = null + var total: Int? = null +} +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/server/bind/type/ShoppingCartController.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/server/bind/type/ShoppingCartController.kt new file mode 100644 index 00000000000..68d912680b8 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/server/bind/type/ShoppingCartController.kt @@ -0,0 +1,18 @@ +package io.micronaut.docs.http.server.bind.type + +import io.micronaut.http.HttpResponse +import io.micronaut.http.annotation.Controller +import io.micronaut.http.annotation.Get + +@Controller("/customBinding") +class ShoppingCartController { + + // tag::method[] + @Get("/typed") + fun loadCart(shoppingCart: ShoppingCart): HttpResponse<*> { //<1> + return HttpResponse.ok(mapOf( + "sessionId" to shoppingCart.sessionId, + "total" to shoppingCart.total)) + } + // end::method[] +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/server/bind/type/ShoppingCartRequestArgumentBinder.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/server/bind/type/ShoppingCartRequestArgumentBinder.kt new file mode 100644 index 00000000000..ab1b5704187 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/server/bind/type/ShoppingCartRequestArgumentBinder.kt @@ -0,0 +1,43 @@ +package io.micronaut.docs.http.server.bind.type + +// tag::class[] +import io.micronaut.core.bind.ArgumentBinder +import io.micronaut.core.bind.ArgumentBinder.BindingResult +import io.micronaut.core.convert.ArgumentConversionContext +import io.micronaut.core.type.Argument +import io.micronaut.http.HttpRequest +import io.micronaut.http.bind.binders.TypedRequestArgumentBinder +import io.micronaut.jackson.serialize.JacksonObjectSerializer +import java.util.Optional +import jakarta.inject.Singleton + +@Singleton +class ShoppingCartRequestArgumentBinder(private val objectSerializer: JacksonObjectSerializer) : + TypedRequestArgumentBinder { + + override fun bind( + context: ArgumentConversionContext, + source: HttpRequest<*> + ): BindingResult { //<1> + + val cookie = source.cookies["shoppingCart"] + + return if (cookie == null) + BindingResult { + Optional.empty() + } + else { + BindingResult { + objectSerializer.deserialize( // <2> + cookie.value.toByteArray(), + ShoppingCart::class.java + ) + } + } + } + + override fun argumentType(): Argument { + return Argument.of(ShoppingCart::class.java) //<3> + } +} +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/server/executeon/PersonController.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/server/executeon/PersonController.kt new file mode 100644 index 00000000000..90b0246d1cb --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/server/executeon/PersonController.kt @@ -0,0 +1,22 @@ +package io.micronaut.docs.http.server.executeon + +// tag::imports[] +import io.micronaut.docs.http.server.reactive.PersonService +import io.micronaut.docs.ioc.beans.Person +import io.micronaut.http.annotation.Controller +import io.micronaut.http.annotation.Get +import io.micronaut.scheduling.TaskExecutors +import io.micronaut.scheduling.annotation.ExecuteOn +// end::imports[] + +// tag::class[] +@Controller("/executeOn/people") +class PersonController (private val personService: PersonService) { + + @Get("/{name}") + @ExecuteOn(TaskExecutors.IO) // <1> + fun byName(name: String): Person { + return personService.findByName(name) + } +} +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/server/netty/websocket/ChatClientWebSocket.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/server/netty/websocket/ChatClientWebSocket.kt new file mode 100644 index 00000000000..9f6520ff33f --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/server/netty/websocket/ChatClientWebSocket.kt @@ -0,0 +1,67 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.http.server.netty.websocket + +// tag::imports[] +import io.micronaut.http.HttpRequest +import io.micronaut.websocket.WebSocketSession +import io.micronaut.websocket.annotation.ClientWebSocket +import io.micronaut.websocket.annotation.OnMessage +import io.micronaut.websocket.annotation.OnOpen +import reactor.core.publisher.Mono +import java.util.concurrent.ConcurrentLinkedQueue +import java.util.concurrent.Future +// end::imports[] + +// tag::class[] +@ClientWebSocket("/chat/{topic}/{username}") // <1> +abstract class ChatClientWebSocket : AutoCloseable { // <2> + + var session: WebSocketSession? = null + private set + var request: HttpRequest<*>? = null + private set + var topic: String? = null + private set + var username: String? = null + private set + private val replies = ConcurrentLinkedQueue() + + @OnOpen + fun onOpen(topic: String, username: String, + session: WebSocketSession, request: HttpRequest<*>) { // <3> + this.topic = topic + this.username = username + this.session = session + this.request = request + } + + fun getReplies(): Collection { + return replies + } + + @OnMessage + fun onMessage(message: String) { + replies.add(message) // <4> + } + + // end::class[] + abstract fun send(message: String) + + abstract fun sendAsync(message: String): Future + + abstract fun sendRx(message: String): Mono +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/server/netty/websocket/ChatServerWebSocket.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/server/netty/websocket/ChatServerWebSocket.kt new file mode 100644 index 00000000000..5eb22739c03 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/server/netty/websocket/ChatServerWebSocket.kt @@ -0,0 +1,56 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.http.server.netty.websocket + +//tag::clazz[] +import io.micronaut.websocket.WebSocketBroadcaster +import io.micronaut.websocket.WebSocketSession +import io.micronaut.websocket.annotation.OnClose +import io.micronaut.websocket.annotation.OnMessage +import io.micronaut.websocket.annotation.OnOpen +import io.micronaut.websocket.annotation.ServerWebSocket + +import java.util.function.Predicate + +@ServerWebSocket("/chat/{topic}/{username}") // <1> +class ChatServerWebSocket(private val broadcaster: WebSocketBroadcaster) { + + @OnOpen // <2> + fun onOpen(topic: String, username: String, session: WebSocketSession) { + val msg = "[$username] Joined!" + broadcaster.broadcastSync(msg, isValid(topic, session)) + } + + @OnMessage // <3> + fun onMessage(topic: String, username: String, + message: String, session: WebSocketSession) { + val msg = "[$username] $message" + broadcaster.broadcastSync(msg, isValid(topic, session)) // <4> + } + + @OnClose // <5> + fun onClose(topic: String, username: String, session: WebSocketSession) { + val msg = "[$username] Disconnected!" + broadcaster.broadcastSync(msg, isValid(topic, session)) + } + + private fun isValid(topic: String, session: WebSocketSession): Predicate { + return Predicate { + (it !== session && topic.equals(it.uriVariables.get("topic", String::class.java, null), ignoreCase = true)) + } + } +} +//end::clazz[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/server/netty/websocket/Message.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/server/netty/websocket/Message.kt new file mode 100644 index 00000000000..0874a3e4559 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/server/netty/websocket/Message.kt @@ -0,0 +1,46 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.http.server.netty.websocket + +import java.util.Objects + +class Message { + + var text: String? = null + + constructor(text: String) { + this.text = text + } + + internal constructor() {} + + override fun equals(o: Any?): Boolean { + if (this === o) return true + if (o == null || javaClass != o.javaClass) return false + val message = o as Message? + return text == message!!.text + } + + override fun hashCode(): Int { + return Objects.hash(text) + } + + override fun toString(): String { + return "Message{" + + "text='" + text + '\''.toString() + + '}'.toString() + } +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/server/netty/websocket/PojoChatClientWebSocket.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/server/netty/websocket/PojoChatClientWebSocket.kt new file mode 100644 index 00000000000..b80fd46444c --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/server/netty/websocket/PojoChatClientWebSocket.kt @@ -0,0 +1,56 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.http.server.netty.websocket + +import io.micronaut.websocket.annotation.ClientWebSocket +import io.micronaut.websocket.annotation.OnMessage +import io.micronaut.websocket.annotation.OnOpen +import reactor.core.publisher.Mono +import java.util.concurrent.ConcurrentLinkedQueue +import java.util.concurrent.Future + +@ClientWebSocket("/pojo/chat/{topic}/{username}") +abstract class PojoChatClientWebSocket : AutoCloseable { + + var topic: String? = null + private set + var username: String? = null + private set + private val replies = ConcurrentLinkedQueue() + + @OnOpen + fun onOpen(topic: String, username: String) { + this.topic = topic + this.username = username + } + + fun getReplies(): Collection { + return replies + } + + @OnMessage + fun onMessage( + message: Message) { + println("Client received message = $message") + replies.add(message) + } + + abstract fun send(message: Message) + + abstract fun sendAsync(message: Message): Future + + abstract fun sendRx(message: Message): Mono +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/server/netty/websocket/ReactivePojoChatServerWebSocket.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/server/netty/websocket/ReactivePojoChatServerWebSocket.kt new file mode 100644 index 00000000000..15fb17b6ac8 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/server/netty/websocket/ReactivePojoChatServerWebSocket.kt @@ -0,0 +1,62 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.http.server.netty.websocket + +import io.micronaut.websocket.WebSocketBroadcaster +import io.micronaut.websocket.WebSocketSession +import io.micronaut.websocket.annotation.OnClose +import io.micronaut.websocket.annotation.OnMessage +import io.micronaut.websocket.annotation.OnOpen +import io.micronaut.websocket.annotation.ServerWebSocket +import org.reactivestreams.Publisher + +import java.util.function.Predicate + +@ServerWebSocket("/pojo/chat/{topic}/{username}") +class ReactivePojoChatServerWebSocket(private val broadcaster: WebSocketBroadcaster) { + + @OnOpen + fun onOpen(topic: String, username: String, session: WebSocketSession): Publisher { + val text = "[$username] Joined!" + val message = Message(text) + return broadcaster.broadcast(message, isValid(topic, session)) + } + + // tag::onmessage[] + @OnMessage + fun onMessage(topic: String, username: String, + message: Message, session: WebSocketSession): Publisher { + val text = "[" + username + "] " + message.text + val newMessage = Message(text) + return broadcaster.broadcast(newMessage, isValid(topic, session)) + } + // end::onmessage[] + + @OnClose + fun onClose(topic: String, username: String, + session: WebSocketSession): Publisher { + val text = "[$username] Disconnected!" + val message = Message(text) + return broadcaster.broadcast(message, isValid(topic, session)) + } + + private fun isValid(topic: String, session: WebSocketSession): Predicate { + return Predicate { + it !== session && topic.equals( + it.uriVariables.get("topic", String::class.java, null), ignoreCase = true) + } + } +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/server/reactive/PersonController.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/server/reactive/PersonController.kt new file mode 100644 index 00000000000..3e29f1639d3 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/server/reactive/PersonController.kt @@ -0,0 +1,31 @@ +package io.micronaut.docs.http.server.reactive + +// tag::imports[] +import io.micronaut.docs.ioc.beans.Person +import io.micronaut.http.annotation.Controller +import io.micronaut.http.annotation.Get +import io.micronaut.scheduling.TaskExecutors +import java.util.concurrent.ExecutorService +import jakarta.inject.Named +import reactor.core.publisher.Mono +import reactor.core.scheduler.Scheduler +import reactor.core.scheduler.Schedulers + +// end::imports[] + +// tag::class[] +@Controller("/subscribeOn/people") +class PersonController internal constructor( + @Named(TaskExecutors.IO) executorService: ExecutorService, // <1> + private val personService: PersonService) { + + private val scheduler: Scheduler = Schedulers.fromExecutorService(executorService) + + @Get("/{name}") + fun byName(name: String): Mono { + return Mono + .fromCallable { personService.findByName(name) } // <2> + .subscribeOn(scheduler) // <3> + } +} +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/server/reactive/PersonService.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/server/reactive/PersonService.kt new file mode 100644 index 00000000000..8aa0ff870ca --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/server/reactive/PersonService.kt @@ -0,0 +1,11 @@ +package io.micronaut.docs.http.server.reactive + +import io.micronaut.docs.ioc.beans.Person +import jakarta.inject.Singleton + +@Singleton +class PersonService { + fun findByName(name: String): Person { + return Person(name) + } +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/server/secondary/SecondaryNettyServer.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/server/secondary/SecondaryNettyServer.kt new file mode 100644 index 00000000000..afab5186fd3 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/server/secondary/SecondaryNettyServer.kt @@ -0,0 +1,56 @@ +package io.micronaut.docs.http.server.secondary + +// tag::imports[] +import io.micronaut.context.annotation.Bean +import io.micronaut.context.annotation.Context +import io.micronaut.context.annotation.Factory +import io.micronaut.context.annotation.Requires +import io.micronaut.context.env.Environment +import io.micronaut.core.util.StringUtils +import io.micronaut.discovery.ServiceInstanceList +import io.micronaut.discovery.StaticServiceInstanceList +import io.micronaut.http.server.netty.NettyEmbeddedServer +import io.micronaut.http.server.netty.NettyEmbeddedServerFactory +import io.micronaut.http.server.netty.configuration.NettyHttpServerConfiguration +import io.micronaut.http.ssl.ServerSslConfiguration +import jakarta.inject.Named +// end::imports[] + +@Requires(property = "secondary.enabled", value = StringUtils.TRUE) +// tag::class[] +@Factory +class SecondaryNettyServer { + companion object { + const val SERVER_ID = "another" // <1> + } + + @Named(SERVER_ID) + @Context + @Bean(preDestroy = "close") // <2> + @Requires(beans = [Environment::class]) + fun nettyEmbeddedServer( + serverFactory: NettyEmbeddedServerFactory // <3> + ) : NettyEmbeddedServer { + val configuration = NettyHttpServerConfiguration() // <4> + val sslConfiguration = ServerSslConfiguration() // <5> + + sslConfiguration.setBuildSelfSigned(true) + sslConfiguration.isEnabled = true + sslConfiguration.port = -1 // random port + + // configure server programmatically + val embeddedServer = serverFactory.build(configuration, sslConfiguration) // <6> + embeddedServer.start() // <7> + return embeddedServer // <8> + } + + @Bean + fun serviceInstanceList( // <9> + @Named(SERVER_ID) nettyEmbeddedServer: NettyEmbeddedServer + ): ServiceInstanceList { + return StaticServiceInstanceList( + SERVER_ID, setOf(nettyEmbeddedServer.uri) + ) + } +} +// end::class[] \ No newline at end of file diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/server/secondary/SecondaryServerTest.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/server/secondary/SecondaryServerTest.kt new file mode 100644 index 00000000000..d6df1a2582a --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/server/secondary/SecondaryServerTest.kt @@ -0,0 +1,44 @@ +package io.micronaut.docs.http.server.secondary + +import io.micronaut.context.annotation.Property +import io.micronaut.core.util.StringUtils +import io.micronaut.http.HttpRequest +import io.micronaut.http.annotation.Controller +import io.micronaut.http.annotation.Get +import io.micronaut.http.client.HttpClient +import io.micronaut.http.client.annotation.Client +import io.micronaut.runtime.server.EmbeddedServer +import io.micronaut.test.extensions.junit5.annotation.MicronautTest +import jakarta.inject.Inject +import jakarta.inject.Named +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Test + +@MicronautTest +@Property(name = "secondary.enabled", value = StringUtils.TRUE) +@Property(name = "micronaut.http.client.ssl.insecure-trust-all-certificates", value = StringUtils.TRUE) +class SecondaryServerTest { + // tag::inject[] + @Inject + @field:Client(path = "/", id = SecondaryNettyServer.SERVER_ID) + lateinit var httpClient : HttpClient // <1> + + @Inject + @field:Named(SecondaryNettyServer.SERVER_ID) + lateinit var embeddedServer : EmbeddedServer // <2> + // end::inject[] + + @Test + fun testCallSecondaryServer() { + val result = httpClient.toBlocking().retrieve("/test/secondary/server") + Assertions.assertTrue(result.endsWith(embeddedServer.port.toString())) + } +} + +@Controller("/test/secondary/server") +class TestController { + @Get + fun hello(request: HttpRequest<*>): String { + return "Hello from: " + request.serverAddress.port + } +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/server/stream/StreamController.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/server/stream/StreamController.kt new file mode 100644 index 00000000000..bdf6c5c89e4 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/server/stream/StreamController.kt @@ -0,0 +1,32 @@ +package io.micronaut.docs.http.server.stream + +import io.micronaut.core.io.IOUtils +import io.micronaut.http.MediaType +import io.micronaut.http.annotation.Body +import io.micronaut.http.annotation.Controller +import io.micronaut.http.annotation.Get +import io.micronaut.http.annotation.Post +import io.micronaut.scheduling.annotation.ExecuteOn +import io.micronaut.scheduling.TaskExecutors +import java.io.* +import java.nio.charset.StandardCharsets + +@Controller("/stream") +class StreamController { + + // tag::write[] + @Get(value = "/write", produces = [MediaType.TEXT_PLAIN]) + fun write(): InputStream { + val bytes = "test".toByteArray(StandardCharsets.UTF_8) + return ByteArrayInputStream(bytes) // <1> + } + // end::write[] + + // tag::read[] + @Post(value = "/read", processes = [MediaType.TEXT_PLAIN]) + @ExecuteOn(TaskExecutors.IO) // <1> + fun read(@Body inputStream: InputStream): String { // <2> + return IOUtils.readText(BufferedReader(InputStreamReader(inputStream))) // <3> + } + // end::read[] +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/server/stream/StreamControllerSpec.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/server/stream/StreamControllerSpec.kt new file mode 100644 index 00000000000..b0db1830b48 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/server/stream/StreamControllerSpec.kt @@ -0,0 +1,60 @@ +package io.micronaut.docs.http.server.stream + +import io.micronaut.context.ApplicationContext +import io.micronaut.http.HttpRequest +import io.micronaut.http.MediaType +import io.micronaut.http.client.HttpClient +import io.micronaut.runtime.server.EmbeddedServer +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test + +class StreamControllerSpec { + + lateinit var ctx: ApplicationContext + lateinit var client: HttpClient + + @BeforeEach + fun setup() { + val server = ApplicationContext.run( + EmbeddedServer::class.java, + mapOf( + "myapp.updatedAt" to mapOf( // <1> + "day" to 28, + "month" to 10, + "year" to 1982 + ) + ) + ) + ctx = server.applicationContext + client = ctx.createBean(HttpClient::class.java, server.url) + } + + @AfterEach + fun teardown() { + ctx.close() + } + + + @Test + fun testReceivingAStream() { + val response: String = client.toBlocking().retrieve( + HttpRequest.GET("/stream/write"), + String::class.java + ) + + Assertions.assertEquals("test", response) + } + + @Test + fun testReturningAStream() { + val body = "My body" + val response = client.toBlocking().retrieve( + HttpRequest.POST("/stream/read", body) + .contentType(MediaType.TEXT_PLAIN_TYPE), String::class.java) + + Assertions.assertEquals(body, response) + } + +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/httpclientexceptionbody/BindHttpClientExceptionBodySpec.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/httpclientexceptionbody/BindHttpClientExceptionBodySpec.kt new file mode 100644 index 00000000000..7e439d5b6c2 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/httpclientexceptionbody/BindHttpClientExceptionBodySpec.kt @@ -0,0 +1,70 @@ +package io.micronaut.docs.httpclientexceptionbody + +import io.kotest.matchers.shouldBe +import io.kotest.matchers.shouldNotBe +import io.kotest.core.spec.style.StringSpec +import io.micronaut.context.ApplicationContext +import io.micronaut.core.type.Argument +import io.micronaut.http.HttpRequest +import io.micronaut.http.HttpStatus +import io.micronaut.http.client.HttpClient +import io.micronaut.http.client.exceptions.HttpClientResponseException +import io.micronaut.runtime.server.EmbeddedServer + +class BindHttpClientExceptionBodySpec: StringSpec() { + + val embeddedServer = autoClose( + ApplicationContext.run( + EmbeddedServer::class.java, + mapOf( + "spec.name" to BindHttpClientExceptionBodySpec::class.java.simpleName, + "spec.lang" to "java" + ) + ) + ) + + val client = autoClose( + embeddedServer.applicationContext.createBean(HttpClient::class.java, embeddedServer.url) + ) + + init { + //tag::test[] + "after an httpclient exception the response body can be bound to a POJO" { + try { + client.toBlocking().exchange(HttpRequest.GET("/books/1680502395"), + Argument.of(Book::class.java), // <1> + Argument.of(CustomError::class.java)) // <2> + } catch (e: HttpClientResponseException) { + e.response.status shouldBe HttpStatus.UNAUTHORIZED + } + } + //end::test[] + + "exception binding error response" { + try { + client.toBlocking().exchange(HttpRequest.GET("/books/1680502395"), + Argument.of(Book::class.java), // <1> + Argument.of(OtherError::class.java)) // <2> + } catch (e: HttpClientResponseException) { + e.response.status shouldBe HttpStatus.UNAUTHORIZED + + val jsonError = e.response.getBody(OtherError::class.java) + + jsonError shouldNotBe null + jsonError.isPresent shouldNotBe true + } + } + + "verify bind error is thrown" { + try { + client.toBlocking().exchange(HttpRequest.GET("/books/1491950358"), + Argument.of(Book::class.java), + Argument.of(CustomError::class.java)) + } catch (e: HttpClientResponseException) { + e.response.status shouldBe HttpStatus.OK + e.message!!.startsWith("Error decoding HTTP response body") shouldBe true + e.message!!.contains("cannot deserialize from Object value") shouldBe true // the jackson error + } + } + } +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/httpclientexceptionbody/Book.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/httpclientexceptionbody/Book.kt new file mode 100644 index 00000000000..4ea2424c544 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/httpclientexceptionbody/Book.kt @@ -0,0 +1,21 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.httpclientexceptionbody + +import groovy.transform.CompileStatic + +@CompileStatic +class Book internal constructor(var isbn: String?, var title: String?) diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/httpclientexceptionbody/BooksController.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/httpclientexceptionbody/BooksController.kt new file mode 100644 index 00000000000..40475903960 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/httpclientexceptionbody/BooksController.kt @@ -0,0 +1,44 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.httpclientexceptionbody + +import io.micronaut.context.annotation.Requires +import io.micronaut.http.HttpResponse +import io.micronaut.http.HttpStatus +import io.micronaut.http.annotation.Controller +import io.micronaut.http.annotation.Get + +@Requires(property = "spec.name", value = "BindHttpClientExceptionBodySpec") +//tag::clazz[] +@Controller("/books") +class BooksController { + + @Get("/{isbn}") + fun find(isbn: String): HttpResponse<*> { + if (isbn == "1680502395") { + val m = mapOf( + "status" to 401, + "error" to "Unauthorized", + "message" to "No message available", + "path" to "/books/$isbn" + ) + return HttpResponse.status(HttpStatus.UNAUTHORIZED).body(m) + } + + return HttpResponse.ok(Book("1491950358", "Building Microservices")) + } +} +//end::clazz[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/httpclientexceptionbody/CustomError.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/httpclientexceptionbody/CustomError.kt new file mode 100644 index 00000000000..138e365b96f --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/httpclientexceptionbody/CustomError.kt @@ -0,0 +1,21 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.httpclientexceptionbody + +import groovy.transform.CompileStatic + +@CompileStatic +class CustomError internal constructor(var status: Int?, var error: String?, var message: String?, var path: String?) \ No newline at end of file diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/httpclientexceptionbody/OtherError.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/httpclientexceptionbody/OtherError.kt new file mode 100644 index 00000000000..4e20ed52ed5 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/httpclientexceptionbody/OtherError.kt @@ -0,0 +1,21 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.httpclientexceptionbody + +import groovy.transform.CompileStatic + +@CompileStatic +class OtherError internal constructor(var status: Int?, var error: String?, var message: String?, var path: String?) diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/i18n/I18nSpec.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/i18n/I18nSpec.kt new file mode 100644 index 00000000000..33ef72378f5 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/i18n/I18nSpec.kt @@ -0,0 +1,57 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.i18n + +import io.micronaut.context.MessageSource +import io.micronaut.context.i18n.ResourceBundleMessageSource +import io.micronaut.test.extensions.junit5.annotation.MicronautTest +import jakarta.inject.Inject +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Test +import java.util.* + +@MicronautTest(startApplication = false) +class I18nTest { + @Inject + lateinit var messageSource: MessageSource + + @Test + fun itIsPossibleToCreateAMessageSourceFromResourceBundle() { + //tag::test[] + Assertions.assertEquals("Hola", messageSource.getMessage("hello", MessageSource.MessageContext.of(Locale("es"))).get()) + Assertions.assertEquals("Hello", messageSource.getMessage("hello", MessageSource.MessageContext.of(Locale.ENGLISH)).get()) + //end::test[] + + Assertions.assertEquals("Hola", messageSource.getMessage("hello", Locale("es")).get()) + Assertions.assertEquals("Hello", messageSource.getMessage("hello", Locale.ENGLISH).get()) + + Assertions.assertTrue(messageSource.getMessage("hello.name", Locale("es"), "Sergio").isPresent) + Assertions.assertEquals("Hola Sergio", messageSource.getMessage("hello.name", Locale("es"), "Sergio").get()) + Assertions.assertTrue(messageSource.getMessage("hello.name", Locale.ENGLISH, "Sergio").isPresent) + Assertions.assertEquals("Hello Sergio", messageSource.getMessage("hello.name", Locale.ENGLISH, "Sergio").get()) + + Assertions.assertTrue(messageSource.getMessage("hello.name", Locale("es"), mapOf(Pair("0", "Sergio"))).isPresent) + Assertions.assertEquals( + "Hola Sergio", + messageSource.getMessage("hello.name", Locale("es"), mapOf(Pair("0", "Sergio"))).get() + ) + Assertions.assertTrue(messageSource.getMessage("hello.name", Locale.ENGLISH, mapOf(Pair("0", "Sergio"))).isPresent) + Assertions.assertEquals( + "Hello Sergio", + messageSource.getMessage("hello.name", Locale.ENGLISH, mapOf(Pair("0", "Sergio"))).get() + ) + } +} \ No newline at end of file diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/i18n/MessageSourceFactory.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/i18n/MessageSourceFactory.kt new file mode 100644 index 00000000000..cbdb3a6ade0 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/i18n/MessageSourceFactory.kt @@ -0,0 +1,14 @@ +package io.micronaut.docs.i18n + +//tag::clazz[] +import io.micronaut.context.MessageSource +import io.micronaut.context.annotation.Factory +import io.micronaut.context.i18n.ResourceBundleMessageSource +import jakarta.inject.Singleton + +@Factory +internal class MessageSourceFactory { + @Singleton + fun createMessageSource(): MessageSource = ResourceBundleMessageSource("io.micronaut.docs.i18n.messages") +} +//end::clazz[] \ No newline at end of file diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/inject/anninheritance/AnnotationInheritanceSpec.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/inject/anninheritance/AnnotationInheritanceSpec.kt new file mode 100644 index 00000000000..8587487a356 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/inject/anninheritance/AnnotationInheritanceSpec.kt @@ -0,0 +1,20 @@ +package io.micronaut.docs.inject.anninheritance + +import io.micronaut.context.ApplicationContext +import io.micronaut.core.annotation.AnnotationUtil +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test + +class AnnotationInheritanceSpec { + @Test + fun testAnnotationInheritance() { + val config = mapOf("datasource.url" to "jdbc://someurl") + ApplicationContext.run(config).use { context -> + val beanDefinition = context.getBeanDefinition(BookRepository::class.java) + val name = beanDefinition.stringValue(AnnotationUtil.NAMED).orElse(null) + assertEquals("bookRepository", name) + assertTrue(beanDefinition.isSingleton) + } + } +} \ No newline at end of file diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/inject/anninheritance/BaseSqlRepository.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/inject/anninheritance/BaseSqlRepository.kt new file mode 100644 index 00000000000..b265f6f8d36 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/inject/anninheritance/BaseSqlRepository.kt @@ -0,0 +1,6 @@ +package io.micronaut.docs.inject.anninheritance + +// tag::class[] +@SqlRepository +abstract class BaseSqlRepository +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/inject/anninheritance/BookRepository.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/inject/anninheritance/BookRepository.kt new file mode 100644 index 00000000000..c8c2203fc55 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/inject/anninheritance/BookRepository.kt @@ -0,0 +1,11 @@ +package io.micronaut.docs.inject.anninheritance + +//tag::imports[] +import jakarta.inject.Named +import javax.sql.DataSource +//end::imports[] + +//tag::class[] +@Named("bookRepository") +class BookRepository(private val dataSource: DataSource) : BaseSqlRepository() +//end::class[] \ No newline at end of file diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/inject/anninheritance/SqlRepository.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/inject/anninheritance/SqlRepository.kt new file mode 100644 index 00000000000..e707b5e5d54 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/inject/anninheritance/SqlRepository.kt @@ -0,0 +1,19 @@ +package io.micronaut.docs.inject.anninheritance + +//tag::imports[] +import io.micronaut.context.annotation.Requires +import jakarta.inject.Named +import jakarta.inject.Singleton +import java.lang.annotation.Inherited +//end::imports[] + +//tag::class[] +@Inherited // <1> +@kotlin.annotation.Retention(AnnotationRetention.RUNTIME) +@Requires(property = "datasource.url") // <2> +@Named // <3> +@Singleton // <4> +annotation class SqlRepository( + val value: String = "" +) +//end::class[] \ No newline at end of file diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/inject/generics/CylinderProvider.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/inject/generics/CylinderProvider.kt new file mode 100644 index 00000000000..6262df0a147 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/inject/generics/CylinderProvider.kt @@ -0,0 +1,7 @@ +package io.micronaut.docs.inject.generics + +// tag::class[] +interface CylinderProvider { + val cylinders: Int +} +// end::class[] \ No newline at end of file diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/inject/generics/Engine.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/inject/generics/Engine.kt new file mode 100644 index 00000000000..3df937f7e49 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/inject/generics/Engine.kt @@ -0,0 +1,14 @@ +package io.micronaut.docs.inject.generics + +// tag::class[] +interface Engine { // <1> + val cylinders: Int + get() = cylinderProvider.cylinders + + fun start(): String { + return "Starting ${cylinderProvider.javaClass.simpleName}" + } + + val cylinderProvider: T +} +// end::class[] \ No newline at end of file diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/inject/generics/V6.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/inject/generics/V6.kt new file mode 100644 index 00000000000..2902c797ecc --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/inject/generics/V6.kt @@ -0,0 +1,7 @@ +package io.micronaut.docs.inject.generics + +// tag::class[] +class V6 : CylinderProvider { + override val cylinders: Int = 6 +} +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/inject/generics/V6Engine.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/inject/generics/V6Engine.kt new file mode 100644 index 00000000000..6c8fdfd2169 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/inject/generics/V6Engine.kt @@ -0,0 +1,11 @@ +package io.micronaut.docs.inject.generics + +import jakarta.inject.Singleton + +// tag::class[] +@Singleton +class V6Engine : Engine { // <1> + override val cylinderProvider: V6 + get() = V6() +} +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/inject/generics/V8.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/inject/generics/V8.kt new file mode 100644 index 00000000000..898b6bc9df0 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/inject/generics/V8.kt @@ -0,0 +1,7 @@ +package io.micronaut.docs.inject.generics + +// tag::class[] +class V8 : CylinderProvider { + override val cylinders: Int = 8 +} +// end::class[] \ No newline at end of file diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/inject/generics/V8Engine.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/inject/generics/V8Engine.kt new file mode 100644 index 00000000000..024ce948867 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/inject/generics/V8Engine.kt @@ -0,0 +1,11 @@ +package io.micronaut.docs.inject.generics + +import jakarta.inject.Singleton + +// tag::class[] +@Singleton +class V8Engine : Engine { // <1> + override val cylinderProvider: V8 + get() = V8() +} +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/inject/generics/Vehicle.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/inject/generics/Vehicle.kt new file mode 100644 index 00000000000..78eab26203c --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/inject/generics/Vehicle.kt @@ -0,0 +1,21 @@ +package io.micronaut.docs.inject.generics + +import jakarta.inject.Inject +import jakarta.inject.Singleton + +// tag::constructor[] +@Singleton +class Vehicle(val engine: Engine) { +// end::constructor[] + + @Inject + lateinit var v6Engines: List> + + @set:Inject + lateinit var anotherV8: Engine + + + fun start(): String { + return engine.start() + } +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/inject/generics/VehicleSpec.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/inject/generics/VehicleSpec.kt new file mode 100644 index 00000000000..a39561b1c5a --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/inject/generics/VehicleSpec.kt @@ -0,0 +1,15 @@ +package io.micronaut.docs.inject.generics + +import io.micronaut.test.extensions.junit5.annotation.MicronautTest +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test + +@MicronautTest +class VehicleSpec(private val vehicle: Vehicle) { + @Test + fun testStartVehicle() { + assertEquals("Starting V8", vehicle.start()) + assertEquals(listOf(6), vehicle.v6Engines + .map { it.cylinders }) + } +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/inject/intro/Engine.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/inject/intro/Engine.kt new file mode 100644 index 00000000000..11dd1d065ce --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/inject/intro/Engine.kt @@ -0,0 +1,25 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.inject.intro + +// tag::class[] +interface Engine { + // <1> + val cylinders: Int + + fun start(): String +} +// end::class[] \ No newline at end of file diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/inject/intro/V8Engine.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/inject/intro/V8Engine.kt new file mode 100644 index 00000000000..48c476e18bc --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/inject/intro/V8Engine.kt @@ -0,0 +1,30 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.inject.intro + +import jakarta.inject.Singleton + +// tag::class[] +@Singleton// <2> +class V8Engine : Engine { + + override var cylinders = 8 + + override fun start(): String { + return "Starting V8" + } +} +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/inject/intro/Vehicle.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/inject/intro/Vehicle.kt new file mode 100644 index 00000000000..bbaa20964ea --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/inject/intro/Vehicle.kt @@ -0,0 +1,27 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.inject.intro + +import jakarta.inject.Singleton + +// tag::class[] +@Singleton +class Vehicle(private val engine: Engine) { // <3> + fun start(): String { + return engine.start() + } +} +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/inject/intro/VehicleSpec.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/inject/intro/VehicleSpec.kt new file mode 100644 index 00000000000..b91de60e55a --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/inject/intro/VehicleSpec.kt @@ -0,0 +1,21 @@ +package io.micronaut.docs.inject.intro + +import io.micronaut.context.BeanContext +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test + +class VehicleSpec { + @Test + fun testStartVehicle() { + // tag::start[] + val context = BeanContext.run() + val vehicle = context.getBean(Vehicle::class.java) + println(vehicle.start()) + // end::start[] + + assertEquals("Starting V8", vehicle.start()) + + context.close() + } + +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/inject/qualifiers/named/Engine.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/inject/qualifiers/named/Engine.kt new file mode 100644 index 00000000000..f73e2d99308 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/inject/qualifiers/named/Engine.kt @@ -0,0 +1,23 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.inject.qualifiers.named + +// tag::class[] +interface Engine { // <1> + val cylinders: Int + fun start(): String +} +// tag::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/inject/qualifiers/named/V6Engine.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/inject/qualifiers/named/V6Engine.kt new file mode 100644 index 00000000000..4f3a23973c4 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/inject/qualifiers/named/V6Engine.kt @@ -0,0 +1,30 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.inject.qualifiers.named + +import jakarta.inject.Singleton + +// tag::class[] +@Singleton +class V6Engine : Engine { // <2> + + override var cylinders: Int = 6 + + override fun start(): String { + return "Starting V6" + } +} +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/inject/qualifiers/named/V8Engine.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/inject/qualifiers/named/V8Engine.kt new file mode 100644 index 00000000000..95c922c63dd --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/inject/qualifiers/named/V8Engine.kt @@ -0,0 +1,31 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.inject.qualifiers.named + +import jakarta.inject.Singleton + +// tag::class[] +@Singleton +class V8Engine : Engine { + + override var cylinders: Int = 8 + + override fun start(): String { + return "Starting V8" + } + +} +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/inject/qualifiers/named/Vehicle.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/inject/qualifiers/named/Vehicle.kt new file mode 100644 index 00000000000..ad633b78af8 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/inject/qualifiers/named/Vehicle.kt @@ -0,0 +1,31 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.inject.qualifiers.named + +import jakarta.inject.Inject +import jakarta.inject.Named +import jakarta.inject.Singleton + +// tag::class[] +@Singleton +class Vehicle @Inject +constructor(@param:Named("v8") private val engine: Engine) { // <4> + + fun start(): String { + return engine.start() // <5> + } +} +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/inject/qualifiers/named/VehicleSpec.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/inject/qualifiers/named/VehicleSpec.kt new file mode 100644 index 00000000000..8af2e3eeec5 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/inject/qualifiers/named/VehicleSpec.kt @@ -0,0 +1,21 @@ +package io.micronaut.docs.inject.qualifiers.named + +import io.micronaut.context.BeanContext +import io.micronaut.context.DefaultBeanContext +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test + +class VehicleSpec { + @Test + fun testStartVehicle() { + // tag::start[] + val context = BeanContext.run() + val vehicle = context.getBean(Vehicle::class.java) + println(vehicle.start()) + // end::start[] + + assertEquals("Starting V8", vehicle.start()) + context.close() + } + +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/inject/scope/RefreshEventSpec.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/inject/scope/RefreshEventSpec.kt new file mode 100644 index 00000000000..b28aa680d82 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/inject/scope/RefreshEventSpec.kt @@ -0,0 +1,108 @@ +package io.micronaut.docs.inject.scope + +import io.micronaut.context.ApplicationContext +import io.micronaut.context.annotation.Requires +import io.micronaut.context.env.Environment +import io.micronaut.http.HttpRequest +import io.micronaut.http.HttpResponse +import io.micronaut.http.MutableHttpResponse +import io.micronaut.http.annotation.Controller +import io.micronaut.http.annotation.Get +import io.micronaut.http.annotation.Post +import io.micronaut.http.client.HttpClient +import io.micronaut.runtime.context.scope.Refreshable +import io.micronaut.runtime.context.scope.refresh.RefreshEvent +import io.micronaut.runtime.server.EmbeddedServer +import org.junit.Assert.* +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import java.text.SimpleDateFormat +import java.util.* +import jakarta.annotation.PostConstruct +import jakarta.inject.Inject + +class RefreshEventSpec { + + lateinit var embeddedServer: EmbeddedServer + lateinit var client: HttpClient + + @BeforeEach + fun setup() { + embeddedServer = ApplicationContext.run(EmbeddedServer::class.java, mapOf("spec.name" to RefreshEventSpec::class.simpleName), Environment.TEST) + client = HttpClient.create(embeddedServer.url) + } + + @AfterEach + fun teardown() { + client.close() + embeddedServer.close() + } + + @Test + fun publishingARefreshEventDestroysBeanWithRefreshableScope() { + val firstResponse = fetchForecast() + + assertTrue(firstResponse.contains("{\"forecast\":\"Scattered Clouds")) + + val secondResponse = fetchForecast() + + assertEquals(firstResponse, secondResponse) + + val response = evictForecast() + + assertEquals( + // tag::evictResponse[] + "{\"msg\":\"OK\"}", response)// end::evictResponse[] + + val thirdResponse = fetchForecast() + + assertNotEquals(thirdResponse, secondResponse) + assertTrue(thirdResponse.contains("\"forecast\":\"Scattered Clouds")) + } + + fun fetchForecast(): String { + return client.toBlocking().retrieve("/weather/forecast") + } + + fun evictForecast(): String { + return client.toBlocking().retrieve(HttpRequest.POST( + "/weather/evict", + emptyMap() + )) + } + + //tag::weatherService[] + @Refreshable // <1> + open class WeatherService { + private var forecast: String? = null + + @PostConstruct + open fun init() { + forecast = "Scattered Clouds " + SimpleDateFormat("dd/MMM/yy HH:mm:ss.SSS").format(Date())// <2> + } + + open fun latestForecast(): String? { + return forecast + } + } + //end::weatherService[] + + @Requires(property = "spec.name", value = "RefreshEventSpec") + @Controller("/weather") + open class WeatherController(@Inject private val weatherService: WeatherService, @Inject private val applicationContext: ApplicationContext) { + + @Get(value = "/forecast") + fun index(): MutableHttpResponse>? { + return HttpResponse.ok(mapOf("forecast" to weatherService.latestForecast())) + } + + @Post("/evict") + fun evict(): HttpResponse> { + //tag::publishEvent[] + applicationContext.publishEvent(RefreshEvent()) + //end::publishEvent[] + return HttpResponse.ok(mapOf("msg" to "OK")) + } + } +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/inject/typed/Engine.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/inject/typed/Engine.kt new file mode 100644 index 00000000000..956fd1b30a7 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/inject/typed/Engine.kt @@ -0,0 +1,8 @@ +package io.micronaut.docs.inject.typed + +// tag::class[] +interface Engine { + val cylinders: Int + fun start(): String +} +// tag::class[] \ No newline at end of file diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/inject/typed/EngineSpec.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/inject/typed/EngineSpec.kt new file mode 100644 index 00000000000..8ce4b4bc6e0 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/inject/typed/EngineSpec.kt @@ -0,0 +1,27 @@ +package io.micronaut.docs.inject.typed + +import io.micronaut.context.BeanContext +import io.micronaut.context.exceptions.NoSuchBeanException +import io.micronaut.test.extensions.junit5.annotation.MicronautTest +import org.junit.jupiter.api.Assertions.assertThrows +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test +import jakarta.inject.Inject + +// tag::class[] +@MicronautTest +class EngineSpec { + @Inject + lateinit var beanContext: BeanContext + + @Test + fun testEngine() { + assertThrows(NoSuchBeanException::class.java) { + beanContext.getBean(V8Engine::class.java) // <1> + } + + val engine = beanContext.getBean(Engine::class.java) // <2> + assertTrue(engine is V8Engine) + } +} +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/inject/typed/V8Engine.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/inject/typed/V8Engine.kt new file mode 100644 index 00000000000..9387d9bf9fa --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/inject/typed/V8Engine.kt @@ -0,0 +1,16 @@ +package io.micronaut.docs.inject.typed + +import io.micronaut.context.annotation.Bean +import jakarta.inject.Singleton + +// tag::class[] +@Singleton +@Bean(typed = [Engine::class]) // <1> +class V8Engine : Engine { // <2> + override fun start(): String { + return "Starting V8" + } + + override val cylinders: Int = 8 +} +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/injectionpoint/CrankShaft.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/injectionpoint/CrankShaft.kt new file mode 100644 index 00000000000..95d15451e95 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/injectionpoint/CrankShaft.kt @@ -0,0 +1,23 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.injectionpoint + +import jakarta.inject.Singleton + +// tag::class[] +@Singleton +class CrankShaft +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/injectionpoint/Cylinders.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/injectionpoint/Cylinders.kt new file mode 100644 index 00000000000..00b46535ff6 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/injectionpoint/Cylinders.kt @@ -0,0 +1,23 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.injectionpoint + +// tag::class[] +@MustBeDocumented +@Retention(AnnotationRetention.RUNTIME) +@Target(AnnotationTarget.VALUE_PARAMETER) +annotation class Cylinders(val value: Int = 8) +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/injectionpoint/Engine.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/injectionpoint/Engine.kt new file mode 100644 index 00000000000..58b489e2ae7 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/injectionpoint/Engine.kt @@ -0,0 +1,22 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.injectionpoint + +// tag::class[] +interface Engine { + fun start(): String +} +// end::class[] \ No newline at end of file diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/injectionpoint/EngineFactory.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/injectionpoint/EngineFactory.kt new file mode 100644 index 00000000000..eeae0510170 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/injectionpoint/EngineFactory.kt @@ -0,0 +1,42 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.injectionpoint + +import io.micronaut.context.annotation.Factory +import io.micronaut.context.annotation.Prototype +import io.micronaut.inject.InjectionPoint + +/** + * @author Graeme Rocher + * @since 1.0 + */ +// tag::class[] +@Factory +internal class EngineFactory { + + @Prototype + fun v8Engine(injectionPoint: InjectionPoint<*>, crankShaft: CrankShaft): Engine { // <1> + val cylinders = injectionPoint + .annotationMetadata + .intValue(Cylinders::class.java).orElse(8) // <2> + return when (cylinders) { // <3> + 6 -> V6Engine(crankShaft) + 8 -> V8Engine(crankShaft) + else -> throw IllegalArgumentException("Unsupported number of cylinders specified: $cylinders") + } + } +} +// tag::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/injectionpoint/V6Engine.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/injectionpoint/V6Engine.kt new file mode 100644 index 00000000000..c48b106d4af --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/injectionpoint/V6Engine.kt @@ -0,0 +1,26 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.injectionpoint + +// tag::class[] +internal class V6Engine(private val crankShaft: CrankShaft) : Engine { + private val cylinders = 6 + + override fun start(): String { + return "Starting V6" + } +} +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/injectionpoint/V8Engine.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/injectionpoint/V8Engine.kt new file mode 100644 index 00000000000..fd475372a72 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/injectionpoint/V8Engine.kt @@ -0,0 +1,26 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.injectionpoint + +// tag::class[] +internal class V8Engine(private val crankShaft: CrankShaft) : Engine { + private val cylinders = 8 + + override fun start(): String { + return "Starting V8" + } +} +// end::class[] \ No newline at end of file diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/injectionpoint/Vehicle.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/injectionpoint/Vehicle.kt new file mode 100644 index 00000000000..74e9cbe471b --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/injectionpoint/Vehicle.kt @@ -0,0 +1,27 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.injectionpoint + +import jakarta.inject.Singleton + +// tag::class[] +@Singleton +internal class Vehicle(@param:Cylinders(6) private val engine: Engine) { + fun start(): String { + return engine.start() + } +} +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/injectionpoint/VehicleSpec.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/injectionpoint/VehicleSpec.kt new file mode 100644 index 00000000000..0755dda439c --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/injectionpoint/VehicleSpec.kt @@ -0,0 +1,22 @@ +package io.micronaut.docs.injectionpoint + +import io.micronaut.context.BeanContext +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Test + + +internal class VehicleSpec { + + @Test + fun testStartVehicle() { + // tag::start[] + BeanContext.run().use { + val vehicle = it.getBean(Vehicle::class.java) + println(vehicle.start()) + + + Assertions.assertEquals("Starting V6", vehicle.start()) + } + // end::start[] + } +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/ioc/beans/Business.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/ioc/beans/Business.kt new file mode 100644 index 00000000000..b8f663e8039 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/ioc/beans/Business.kt @@ -0,0 +1,36 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.ioc.beans + +// tag::class[] +import io.micronaut.core.annotation.Creator +import io.micronaut.core.annotation.Introspected + +import javax.annotation.concurrent.Immutable + +@Introspected +@Immutable +class Business private constructor(val name: String) { + companion object { + + @Creator // <1> + fun forName(name: String): Business { + return Business(name) + } + } + +} +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/ioc/beans/IntrospectionSpec.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/ioc/beans/IntrospectionSpec.kt new file mode 100644 index 00000000000..33bec029b79 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/ioc/beans/IntrospectionSpec.kt @@ -0,0 +1,68 @@ +package io.micronaut.docs.ioc.beans + +import io.micronaut.core.beans.BeanIntrospection +import io.micronaut.core.beans.BeanProperty +import io.micronaut.core.beans.BeanWrapper +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test + +class IntrospectionSpec { + + @Test + fun testRetrieveInspection() { + + // tag::usage[] + val introspection = BeanIntrospection.getIntrospection(Person::class.java) // <1> + val person : Person = introspection.instantiate("John") // <2> + print("Hello ${person.name}") + + val property : BeanProperty = introspection.getRequiredProperty("name", String::class.java) // <3> + property.set(person, "Fred") // <4> + val name = property.get(person) // <5> + print("Hello ${person.name}") + // end::usage[] + + assertEquals("Fred", name) + } + + @Test + fun testBeanWrapper() { + // tag::wrapper[] + val wrapper = BeanWrapper.getWrapper(Person("Fred")) // <1> + + wrapper.setProperty("age", "20") // <2> + val newAge = wrapper.getRequiredProperty("age", Int::class.java) // <3> + + println("Person's age now $newAge") + // end::wrapper[] + assertEquals(20, newAge) + } + + @Test + fun testNullable() { + val introspection = BeanIntrospection.getIntrospection(Manufacturer::class.java) + val manufacturer: Manufacturer = introspection.instantiate(null, "John") + + val property : BeanProperty = introspection.getRequiredProperty("name", String::class.java) + property.set(manufacturer, "Jane") + val name = property.get(manufacturer) + + assertEquals("Jane", name) + } + + @Test + fun testVehicle() { + val introspection = BeanIntrospection.getIntrospection(Vehicle::class.java) + val vehicle = introspection.instantiate("Subaru", "WRX", 2) + assertEquals("Subaru", vehicle.make) + assertEquals("WRX", vehicle.model) + assertEquals(2, vehicle.axles) + } + + @Test + fun testBusiness() { + val introspection = BeanIntrospection.getIntrospection(Business::class.java) + val business = introspection.instantiate("Apple") + assertEquals("Apple", business.name) + } +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/ioc/beans/Manufacturer.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/ioc/beans/Manufacturer.kt new file mode 100644 index 00000000000..e6b2f5484ab --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/ioc/beans/Manufacturer.kt @@ -0,0 +1,24 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.ioc.beans + +import io.micronaut.core.annotation.Introspected + +@Introspected +data class Manufacturer( + var id: Long?, + var name: String +) \ No newline at end of file diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/ioc/beans/Person.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/ioc/beans/Person.kt new file mode 100644 index 00000000000..89248ffb793 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/ioc/beans/Person.kt @@ -0,0 +1,27 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.ioc.beans + +// tag::imports[] +import io.micronaut.core.annotation.Introspected +// end::imports[] + +// tag::class[] +@Introspected +data class Person(var name : String) { + var age : Int = 18 +} +// end::class[] \ No newline at end of file diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/ioc/beans/PersonConfiguration.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/ioc/beans/PersonConfiguration.kt new file mode 100644 index 00000000000..49ff64f6b48 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/ioc/beans/PersonConfiguration.kt @@ -0,0 +1,23 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.ioc.beans + +// tag::class[] +import io.micronaut.core.annotation.Introspected + +@Introspected(classes = [Person::class]) +class PersonConfiguration +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/ioc/beans/Vehicle.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/ioc/beans/Vehicle.kt new file mode 100644 index 00000000000..a67c4e2d512 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/ioc/beans/Vehicle.kt @@ -0,0 +1,30 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.ioc.beans + +// tag::class[] +import io.micronaut.core.annotation.Creator +import io.micronaut.core.annotation.Introspected + +import javax.annotation.concurrent.Immutable + +@Introspected +@Immutable +class Vehicle @Creator constructor(val make: String, val model: String, val axles: Int) { // <1> + + constructor(make: String, model: String) : this(make, model, 2) {} +} +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/ioc/scopes/Car.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/ioc/scopes/Car.kt new file mode 100644 index 00000000000..6af706eebe1 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/ioc/scopes/Car.kt @@ -0,0 +1,18 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.ioc.scopes + +class Car(val brand: String) diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/ioc/scopes/Driver.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/ioc/scopes/Driver.kt new file mode 100644 index 00000000000..564abce830a --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/ioc/scopes/Driver.kt @@ -0,0 +1,30 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.ioc.scopes + +// tag::imports[] +import io.micronaut.context.annotation.Requires +import jakarta.inject.Singleton +import kotlin.annotation.AnnotationRetention.RUNTIME +// end::imports[] + +// tag::class[] +@Requires(classes = [Car::class]) // <1> +@Singleton // <2> +@MustBeDocumented +@Retention(RUNTIME) +annotation class Driver +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/ioc/validation/Person.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/ioc/validation/Person.kt new file mode 100644 index 00000000000..b869cf98c45 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/ioc/validation/Person.kt @@ -0,0 +1,28 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.ioc.validation + +// tag::class[] +import io.micronaut.core.annotation.Introspected +import javax.validation.constraints.Min +import javax.validation.constraints.NotBlank + +@Introspected +data class Person( + @field:NotBlank var name: String, + @field:Min(18) var age: Int +) +// end::class[] \ No newline at end of file diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/ioc/validation/PersonService.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/ioc/validation/PersonService.kt new file mode 100644 index 00000000000..dd1e2e9c230 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/ioc/validation/PersonService.kt @@ -0,0 +1,30 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.ioc.validation + +// tag::imports[] +import jakarta.inject.Singleton +import javax.validation.constraints.NotBlank +// end::imports[] + +// tag::class[] +@Singleton +open class PersonService { + open fun sayHello(@NotBlank name: String) { + println("Hello $name") + } +} +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/ioc/validation/PersonServiceSpec.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/ioc/validation/PersonServiceSpec.kt new file mode 100644 index 00000000000..c248f7c2949 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/ioc/validation/PersonServiceSpec.kt @@ -0,0 +1,28 @@ +package io.micronaut.docs.ioc.validation + +// tag::imports[] +import io.micronaut.test.extensions.junit5.annotation.MicronautTest +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertThrows +import org.junit.jupiter.api.Test +import jakarta.inject.Inject +import javax.validation.ConstraintViolationException +// end::imports[] + +// tag::test[] +@MicronautTest +class PersonServiceSpec { + + @Inject + lateinit var personService: PersonService + + @Test + fun testThatNameIsValidated() { + val exception = assertThrows(ConstraintViolationException::class.java) { + personService.sayHello("") // <1> + } + + assertEquals("sayHello.name: must not be blank", exception.message) // <2> + } +} +// end::test[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/ioc/validation/custom/DurationPattern.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/ioc/validation/custom/DurationPattern.kt new file mode 100644 index 00000000000..f166300ac6b --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/ioc/validation/custom/DurationPattern.kt @@ -0,0 +1,29 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.ioc.validation.custom + +// tag::imports[] +import javax.validation.Constraint +import kotlin.annotation.AnnotationRetention.RUNTIME +// end::imports[] + +// tag::class[] +@Retention(RUNTIME) +@Constraint(validatedBy = []) // <1> +annotation class DurationPattern( + val message: String = "invalid duration ({validatedValue})" // <2> +) +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/ioc/validation/custom/DurationPatternValidator.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/ioc/validation/custom/DurationPatternValidator.kt new file mode 100644 index 00000000000..b24c226e1c6 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/ioc/validation/custom/DurationPatternValidator.kt @@ -0,0 +1,33 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.ioc.validation.custom + +// tag::imports[] +import io.micronaut.core.annotation.AnnotationValue +import io.micronaut.validation.validator.constraints.ConstraintValidator +import io.micronaut.validation.validator.constraints.ConstraintValidatorContext +// end::imports[] + +// tag::class[] +class DurationPatternValidator : ConstraintValidator { + override fun isValid( + value: CharSequence?, + annotationMetadata: AnnotationValue, + context: ConstraintValidatorContext): Boolean { + return value == null || value.toString().matches("^PT?[\\d]+[SMHD]{1}$".toRegex()) + } +} +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/ioc/validation/custom/DurationPatternValidatorSpec.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/ioc/validation/custom/DurationPatternValidatorSpec.kt new file mode 100644 index 00000000000..f8a6b0e2fc1 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/ioc/validation/custom/DurationPatternValidatorSpec.kt @@ -0,0 +1,26 @@ +package io.micronaut.docs.ioc.validation.custom + +import io.micronaut.test.extensions.junit5.annotation.MicronautTest +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertThrows +import org.junit.jupiter.api.Test +import jakarta.inject.Inject +import javax.validation.ConstraintViolationException + +@MicronautTest +internal class DurationPatternValidatorSpec { + + // tag::test[] + @Inject + lateinit var holidayService: HolidayService + + @Test + fun testCustomValidator() { + val exception = assertThrows(ConstraintViolationException::class.java) { + holidayService.startHoliday("Fred", "junk") // <1> + } + + assertEquals("startHoliday.duration: invalid duration (junk), additional custom message", exception.message) // <2> + } + // end::test[] +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/ioc/validation/custom/HolidayService.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/ioc/validation/custom/HolidayService.kt new file mode 100644 index 00000000000..8e70b21d637 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/ioc/validation/custom/HolidayService.kt @@ -0,0 +1,32 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.ioc.validation.custom + +import java.time.Duration +import jakarta.inject.Singleton +import javax.validation.constraints.NotBlank + +// tag::class[] +@Singleton +open class HolidayService { + + open fun startHoliday(@NotBlank person: String, + @DurationPattern duration: String): String { + val d = Duration.parse(duration) + return "Person $person is off on holiday for ${d.toMinutes()} minutes" + } +} +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/ioc/validation/custom/MyValidatorFactory.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/ioc/validation/custom/MyValidatorFactory.kt new file mode 100644 index 00000000000..799fe4d6bce --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/ioc/validation/custom/MyValidatorFactory.kt @@ -0,0 +1,36 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.ioc.validation.custom + +// tag::imports[] +import io.micronaut.context.annotation.Factory +import io.micronaut.validation.validator.constraints.ConstraintValidator +import jakarta.inject.Singleton +// end::imports[] + +// tag::class[] +@Factory +class MyValidatorFactory { + + @Singleton + fun durationPatternValidator() : ConstraintValidator { + return ConstraintValidator { value, annotation, context -> + context.messageTemplate("invalid duration ({validatedValue}), additional custom message") // <1> + value == null || value.toString().matches("^PT?[\\d]+[SMHD]{1}$".toRegex()) + } + } +} +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/ioc/validation/custom/TimeOff.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/ioc/validation/custom/TimeOff.kt new file mode 100644 index 00000000000..903b4de79e2 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/ioc/validation/custom/TimeOff.kt @@ -0,0 +1,27 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.ioc.validation.custom + +// tag::imports[] +import kotlin.annotation.AnnotationRetention.RUNTIME +// end::imports[] + +// tag::class[] +@Retention(RUNTIME) +annotation class TimeOff( + @DurationPattern val duration: String +) +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/ioc/validation/pojo/PersonService.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/ioc/validation/pojo/PersonService.kt new file mode 100644 index 00000000000..9e463115a99 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/ioc/validation/pojo/PersonService.kt @@ -0,0 +1,33 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.ioc.validation.pojo + +// tag::imports[] +import io.micronaut.docs.ioc.validation.Person + +import jakarta.inject.Singleton +import javax.validation.Valid + +// end::imports[] + +// tag::class[] +@Singleton +open class PersonService { + open fun sayHello(@Valid person: Person) { + println("Hello ${person.name}") + } +} +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/ioc/validation/pojo/PersonServiceSpec.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/ioc/validation/pojo/PersonServiceSpec.kt new file mode 100644 index 00000000000..7e9932adc1b --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/ioc/validation/pojo/PersonServiceSpec.kt @@ -0,0 +1,46 @@ +package io.micronaut.docs.ioc.validation.pojo + +// tag::imports[] +import io.micronaut.docs.ioc.validation.Person +import io.micronaut.test.extensions.junit5.annotation.MicronautTest +import io.micronaut.validation.validator.Validator +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertThrows +import org.junit.jupiter.api.Test +import jakarta.inject.Inject +import javax.validation.ConstraintViolationException +// end::imports[] + +// tag::test[] +@MicronautTest +class PersonServiceSpec { + + // tag::validator[] + @Inject + lateinit var validator: Validator + + @Test + fun testThatPersonIsValidWithValidator() { + val person = Person("", 10) + val constraintViolations = validator.validate(person) // <1> + + assertEquals(2, constraintViolations.size) // <2> + } + // end::validator[] + + // tag::validate-service[] + @Inject + lateinit var personService: PersonService + + @Test + fun testThatPersonIsValid() { + val person = Person("", 10) + val exception = assertThrows(ConstraintViolationException::class.java) { + personService.sayHello(person) // <1> + } + + assertEquals(2, exception.constraintViolations.size) // <2> + } + // end::validate-service[] +} +// end::test[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/lifecycle/Connection.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/lifecycle/Connection.kt new file mode 100644 index 00000000000..3a9e0f19e34 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/lifecycle/Connection.kt @@ -0,0 +1,30 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.lifecycle + +// tag::class[] +import java.util.concurrent.atomic.AtomicBoolean + +class Connection { + + internal var stopped = AtomicBoolean(false) + + fun stop() { // <2> + stopped.compareAndSet(false, true) + } + +} +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/lifecycle/ConnectionFactory.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/lifecycle/ConnectionFactory.kt new file mode 100644 index 00000000000..82056d6053c --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/lifecycle/ConnectionFactory.kt @@ -0,0 +1,33 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.lifecycle + +// tag::class[] +import io.micronaut.context.annotation.Bean +import io.micronaut.context.annotation.Factory + +import jakarta.inject.Singleton + +@Factory +class ConnectionFactory { + + @Bean(preDestroy = "stop") // <1> + @Singleton + fun connection(): Connection { + return Connection() + } +} +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/lifecycle/Engine.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/lifecycle/Engine.kt new file mode 100644 index 00000000000..b02e513a17c --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/lifecycle/Engine.kt @@ -0,0 +1,23 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.lifecycle + +// tag::class[] +interface Engine { // <1> + val cylinders: Int + fun start(): String +} +// end::class[] \ No newline at end of file diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/lifecycle/PreDestroyBean.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/lifecycle/PreDestroyBean.kt new file mode 100644 index 00000000000..e90338e44e5 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/lifecycle/PreDestroyBean.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.lifecycle + +// tag::class[] +import jakarta.annotation.PreDestroy // <1> +import jakarta.inject.Singleton +import java.util.concurrent.atomic.AtomicBoolean + +@Singleton +class PreDestroyBean : AutoCloseable { + + internal var stopped = AtomicBoolean(false) + + @PreDestroy // <2> + @Throws(Exception::class) + override fun close() { + stopped.compareAndSet(false, true) + } +} +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/lifecycle/PreDestroyBeanSpec.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/lifecycle/PreDestroyBeanSpec.kt new file mode 100644 index 00000000000..01e14f92994 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/lifecycle/PreDestroyBeanSpec.kt @@ -0,0 +1,25 @@ +package io.micronaut.docs.lifecycle + +import io.kotest.matchers.shouldBe +import io.kotest.core.spec.style.StringSpec +import io.micronaut.context.BeanContext +import org.junit.Test + +import org.junit.Assert.assertTrue + +class PreDestroyBeanSpec: StringSpec() { + + init { + "test bean closing on context close" { + // tag::start[] + val ctx = BeanContext.run() + val preDestroyBean = ctx.getBean(PreDestroyBean::class.java) + val connection = ctx.getBean(Connection::class.java) + ctx.stop() + // end::start[] + + preDestroyBean.stopped.get() shouldBe true + connection.stopped.get() shouldBe true + } + } +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/lifecycle/V8Engine.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/lifecycle/V8Engine.kt new file mode 100644 index 00000000000..02898237586 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/lifecycle/V8Engine.kt @@ -0,0 +1,43 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.lifecycle + +// tag::imports[] +import jakarta.annotation.PostConstruct +import jakarta.inject.Singleton +// end::imports[] + +// tag::class[] +@Singleton +class V8Engine : Engine { + + override val cylinders = 8 + + var initialized = false + private set // <2> + + override fun start(): String { + check(initialized) { "Engine not initialized!" } + + return "Starting V8" + } + + @PostConstruct // <3> + fun initialize() { + initialized = true + } +} +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/lifecycle/Vehicle.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/lifecycle/Vehicle.kt new file mode 100644 index 00000000000..9488049c5fb --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/lifecycle/Vehicle.kt @@ -0,0 +1,29 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.lifecycle + +import jakarta.inject.Singleton + +// tag::class[] +@Singleton +class Vehicle internal constructor(internal val engine: Engine)// <3> +{ + + fun start(): String { + return engine.start() + } +} +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/lifecycle/VehicleSpec.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/lifecycle/VehicleSpec.kt new file mode 100644 index 00000000000..44aae406c37 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/lifecycle/VehicleSpec.kt @@ -0,0 +1,26 @@ +package io.micronaut.docs.lifecycle + +import io.kotest.matchers.shouldBe +import io.kotest.core.spec.style.StringSpec +import io.micronaut.context.BeanContext +import io.micronaut.context.DefaultBeanContext + +class VehicleSpec: StringSpec() { + + init { + "test start vehicle" { + // tag::start[] + val context = BeanContext.run() + val vehicle = context + .getBean(Vehicle::class.java) + + println(vehicle.start()) + // end::start[] + + vehicle.engine.javaClass shouldBe V8Engine::class.java + (vehicle.engine as V8Engine).initialized shouldBe true + + context.close() + } + } +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/netty/LogbookNettyClientCustomizer.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/netty/LogbookNettyClientCustomizer.kt new file mode 100644 index 00000000000..7fdeb32a624 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/netty/LogbookNettyClientCustomizer.kt @@ -0,0 +1,42 @@ +package io.micronaut.docs.netty + +// tag::imports[] +import io.micronaut.context.annotation.Requires +import io.micronaut.context.event.BeanCreatedEvent +import io.micronaut.context.event.BeanCreatedEventListener +import io.micronaut.http.client.netty.NettyClientCustomizer +import io.micronaut.http.client.netty.NettyClientCustomizer.ChannelRole +import io.micronaut.http.netty.channel.ChannelPipelineCustomizer +import io.netty.channel.Channel +import jakarta.inject.Singleton +import org.zalando.logbook.Logbook +import org.zalando.logbook.netty.LogbookClientHandler +// end::imports[] + +// tag::class[] +@Requires(beans = [Logbook::class]) +@Singleton +class LogbookNettyClientCustomizer(private val logbook: Logbook) : + BeanCreatedEventListener { // <1> + + override fun onCreated(event: BeanCreatedEvent): NettyClientCustomizer.Registry { + val registry = event.bean + registry.register(Customizer(null)) // <2> + return registry + } + + private inner class Customizer constructor(private val channel: Channel?) : + NettyClientCustomizer { // <3> + + override fun specializeForChannel(channel: Channel, role: ChannelRole) = Customizer(channel) // <4> + + override fun onRequestPipelineBuilt() { + channel!!.pipeline().addBefore( // <5> + ChannelPipelineCustomizer.HANDLER_HTTP_STREAM, + "logbook", + LogbookClientHandler(logbook) + ) + } + } +} +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/netty/LogbookNettyServerCustomizer.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/netty/LogbookNettyServerCustomizer.kt new file mode 100644 index 00000000000..5ac765c1f24 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/netty/LogbookNettyServerCustomizer.kt @@ -0,0 +1,42 @@ +package io.micronaut.docs.netty + +// tag::imports[] +import io.micronaut.context.annotation.Requires +import io.micronaut.context.event.BeanCreatedEvent +import io.micronaut.context.event.BeanCreatedEventListener +import io.micronaut.http.netty.channel.ChannelPipelineCustomizer +import io.micronaut.http.server.netty.NettyServerCustomizer +import io.micronaut.http.server.netty.NettyServerCustomizer.ChannelRole +import io.netty.channel.Channel +import jakarta.inject.Singleton +import org.zalando.logbook.Logbook +import org.zalando.logbook.netty.LogbookServerHandler +// end::imports[] + +// tag::class[] +@Requires(beans = [Logbook::class]) +@Singleton +class LogbookNettyServerCustomizer(private val logbook: Logbook) : + BeanCreatedEventListener { // <1> + + override fun onCreated(event: BeanCreatedEvent): NettyServerCustomizer.Registry { + val registry = event.bean + registry.register(Customizer(null)) // <2> + return registry + } + + private inner class Customizer constructor(private val channel: Channel?) : + NettyServerCustomizer { // <3> + + override fun specializeForChannel(channel: Channel, role: ChannelRole) = Customizer(channel) // <4> + + override fun onStreamPipelineBuilt() { + channel!!.pipeline().addBefore( // <5> + ChannelPipelineCustomizer.HANDLER_HTTP_STREAM, + "logbook", + LogbookServerHandler(logbook) + ) + } + } +} +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/qualifiers/annotation/Engine.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/qualifiers/annotation/Engine.kt new file mode 100644 index 00000000000..b0126b22649 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/qualifiers/annotation/Engine.kt @@ -0,0 +1,23 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.qualifiers.annotation + +// tag::class[] +interface Engine { // <1> + val cylinders : Int + fun start(): String +} +// end::class[] \ No newline at end of file diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/qualifiers/annotation/V6Engine.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/qualifiers/annotation/V6Engine.kt new file mode 100644 index 00000000000..fe65ffddf60 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/qualifiers/annotation/V6Engine.kt @@ -0,0 +1,29 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.qualifiers.annotation + +import jakarta.inject.Singleton + +// tag::class[] +@Singleton +class V6Engine : Engine { // <2> + override val cylinders = 6 + + override fun start(): String { + return "Starting V6" + } +} +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/qualifiers/annotation/V8.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/qualifiers/annotation/V8.kt new file mode 100644 index 00000000000..5fbdb05a7f3 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/qualifiers/annotation/V8.kt @@ -0,0 +1,27 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.qualifiers.annotation + +// tag::imports[] +import jakarta.inject.Qualifier +import java.lang.annotation.Retention +import java.lang.annotation.RetentionPolicy.RUNTIME +// end::imports[] +// tag::class[] +@Qualifier +@Retention(RUNTIME) +annotation class V8 +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/qualifiers/annotation/V8Engine.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/qualifiers/annotation/V8Engine.kt new file mode 100644 index 00000000000..cf6aa089579 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/qualifiers/annotation/V8Engine.kt @@ -0,0 +1,29 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.qualifiers.annotation + +import jakarta.inject.Singleton + +// tag::class[] +@Singleton +class V8Engine : Engine { // <2> + override val cylinders = 8 + + override fun start(): String { + return "Starting V8" + } +} +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/qualifiers/annotation/Vehicle.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/qualifiers/annotation/Vehicle.kt new file mode 100644 index 00000000000..692ae3c53ce --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/qualifiers/annotation/Vehicle.kt @@ -0,0 +1,31 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.qualifiers.annotation + +import jakarta.inject.Inject +import jakarta.inject.Singleton + +// tag::class[] +@Singleton +class Vehicle // tag::constructor[] +@Inject constructor(@V8 val engine: Engine) { + + // end::constructor[] + fun start(): String { + return engine.start() // <5> + } +} +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/qualifiers/annotation/VehicleSpec.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/qualifiers/annotation/VehicleSpec.kt new file mode 100644 index 00000000000..bcfade7bbb4 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/qualifiers/annotation/VehicleSpec.kt @@ -0,0 +1,22 @@ +package io.micronaut.docs.qualifiers.annotation + +import io.kotest.matchers.shouldBe +import io.kotest.core.spec.style.StringSpec +import io.micronaut.context.BeanContext +import io.micronaut.context.DefaultBeanContext + + +class VehicleSpec : StringSpec({ + + "test vehicle start uses v8" { + // tag::start[] + val context = BeanContext.run() + val vehicle = context.getBean(Vehicle::class.java) + println(vehicle.start()) + // end::start[] + + vehicle.start().shouldBe("Starting V8") + + context.close() + } +}) diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/qualifiers/annotationmember/Cylinders.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/qualifiers/annotationmember/Cylinders.kt new file mode 100644 index 00000000000..5008b029f20 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/qualifiers/annotationmember/Cylinders.kt @@ -0,0 +1,17 @@ +package io.micronaut.docs.qualifiers.annotationmember + +// tag::imports[] +import io.micronaut.context.annotation.NonBinding +import jakarta.inject.Qualifier +import kotlin.annotation.Retention +// end::imports[] + +// tag::class[] +@Qualifier // <1> +@Retention(AnnotationRetention.RUNTIME) +annotation class Cylinders( + val value: Int, + @get:NonBinding // <2> + val description: String = "" +) +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/qualifiers/annotationmember/Engine.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/qualifiers/annotationmember/Engine.kt new file mode 100644 index 00000000000..a9fa5c6e5b3 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/qualifiers/annotationmember/Engine.kt @@ -0,0 +1,8 @@ +package io.micronaut.docs.qualifiers.annotationmember + +// tag::class[] +interface Engine { + val cylinders: Int + fun start(): String +} +// end::class[] \ No newline at end of file diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/qualifiers/annotationmember/V6Engine.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/qualifiers/annotationmember/V6Engine.kt new file mode 100644 index 00000000000..166e327d919 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/qualifiers/annotationmember/V6Engine.kt @@ -0,0 +1,17 @@ +package io.micronaut.docs.qualifiers.annotationmember + +import jakarta.inject.Singleton + +// tag::class[] +@Singleton +@Cylinders(value = 6, description = "6-cylinder V6 engine") // <1> +class V6Engine : Engine { // <2> + // <2> + override val cylinders: Int + get() = 6 + + override fun start(): String { + return "Starting V6" + } +} +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/qualifiers/annotationmember/V8Engine.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/qualifiers/annotationmember/V8Engine.kt new file mode 100644 index 00000000000..fffaa525b53 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/qualifiers/annotationmember/V8Engine.kt @@ -0,0 +1,16 @@ +package io.micronaut.docs.qualifiers.annotationmember + +import jakarta.inject.Singleton + +// tag::class[] +@Singleton +@Cylinders(value = 8, description = "8-cylinder V8 engine") // <1> +class V8Engine : Engine { // <2> + override val cylinders: Int + get() = 8 + + override fun start(): String { + return "Starting V8" + } +} +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/qualifiers/annotationmember/Vehicle.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/qualifiers/annotationmember/Vehicle.kt new file mode 100644 index 00000000000..ea6ebf3131e --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/qualifiers/annotationmember/Vehicle.kt @@ -0,0 +1,13 @@ +package io.micronaut.docs.qualifiers.annotationmember + +import jakarta.inject.Singleton + + +// tag::constructor[] +@Singleton +class Vehicle(@param:Cylinders(8) val engine: Engine) { + fun start(): String { + return engine.start() + } +} +// end::constructor[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/qualifiers/annotationmember/VehicleSpec.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/qualifiers/annotationmember/VehicleSpec.kt new file mode 100644 index 00000000000..bdfa5c9a1c4 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/qualifiers/annotationmember/VehicleSpec.kt @@ -0,0 +1,18 @@ +package io.micronaut.docs.qualifiers.annotationmember + +import io.micronaut.context.ApplicationContext +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Test + +class VehicleSpec { + @Test + fun testStartVehicle() { + // tag::start[] + val context = ApplicationContext.run() + val vehicle = context.getBean(Vehicle::class.java) + println(vehicle.start()) + // end::start[] + Assertions.assertEquals("Starting V8", vehicle.start()) + context.close() + } +} \ No newline at end of file diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/qualifiers/any/Vehicle.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/qualifiers/any/Vehicle.kt new file mode 100644 index 00000000000..8723789d10c --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/qualifiers/any/Vehicle.kt @@ -0,0 +1,24 @@ +package io.micronaut.docs.qualifiers.any + +import io.micronaut.docs.qualifiers.annotationmember.Engine +// tag::imports[] +import io.micronaut.context.BeanProvider +import io.micronaut.context.annotation.Any +import jakarta.inject.Singleton +// end::imports[] + +// tag::clazz[] +@Singleton +class Vehicle(@param:Any val engineProvider: BeanProvider) { // <1> + fun start() { + engineProvider.ifPresent { it.start() } // <2> + } + // tag::startAll[] + fun startAll() { + if (engineProvider.isPresent) { // <1> + engineProvider.forEach { it.start() } // <2> + } + } // end::startAll[] +// tag::clazz[] +} +// end::clazz[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/qualifiers/any/VehicleSpec.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/qualifiers/any/VehicleSpec.kt new file mode 100644 index 00000000000..2ba960b1bec --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/qualifiers/any/VehicleSpec.kt @@ -0,0 +1,24 @@ +package io.micronaut.docs.qualifiers.any + +import io.micronaut.docs.qualifiers.annotationmember.Engine +// tag::imports[] +import io.micronaut.context.annotation.Any +import io.micronaut.test.extensions.junit5.annotation.MicronautTest +import org.junit.jupiter.api.Assertions.assertNotNull +import org.junit.jupiter.api.Test +import jakarta.inject.Inject +// end::imports[] + +@MicronautTest +class VehicleSpec { + // tag::any[] + @Inject + @field:Any + lateinit var engine: Engine + // end::any[] + + @Test + fun testEngine() { + assertNotNull(engine) + } +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/qualifiers/replaces/defaultimpl/CustomResponseStrategy.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/qualifiers/replaces/defaultimpl/CustomResponseStrategy.kt new file mode 100644 index 00000000000..00cc491ce80 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/qualifiers/replaces/defaultimpl/CustomResponseStrategy.kt @@ -0,0 +1,25 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.qualifiers.replaces.defaultimpl + +//tag::clazz[] +import io.micronaut.context.annotation.Replaces +import jakarta.inject.Singleton + +@Singleton +@Replaces(ResponseStrategy::class) +class CustomResponseStrategy : ResponseStrategy +//end::clazz[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/qualifiers/replaces/defaultimpl/DefaultImplementationSpec.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/qualifiers/replaces/defaultimpl/DefaultImplementationSpec.kt new file mode 100644 index 00000000000..3d9b8f88a84 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/qualifiers/replaces/defaultimpl/DefaultImplementationSpec.kt @@ -0,0 +1,17 @@ +package io.micronaut.docs.qualifiers.replaces.defaultimpl + +import io.kotest.core.spec.style.StringSpec +import io.kotest.matchers.types.shouldBeInstanceOf +import io.micronaut.context.BeanContext + +class DefaultImplementationSpec : StringSpec({ + + "test the default implementation is replaced" { + val ctx = BeanContext.run() + val responseStrategy = ctx.getBean(ResponseStrategy::class.java) + + responseStrategy.shouldBeInstanceOf() + + ctx.close() + } +}) diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/qualifiers/replaces/defaultimpl/DefaultResponseStrategy.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/qualifiers/replaces/defaultimpl/DefaultResponseStrategy.kt new file mode 100644 index 00000000000..f601d662295 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/qualifiers/replaces/defaultimpl/DefaultResponseStrategy.kt @@ -0,0 +1,23 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.qualifiers.replaces.defaultimpl + +//tag::clazz[] +import jakarta.inject.Singleton + +@Singleton +internal class DefaultResponseStrategy : ResponseStrategy +//end::clazz[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/qualifiers/replaces/defaultimpl/ResponseStrategy.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/qualifiers/replaces/defaultimpl/ResponseStrategy.kt new file mode 100644 index 00000000000..f02e3c0ea72 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/qualifiers/replaces/defaultimpl/ResponseStrategy.kt @@ -0,0 +1,23 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.qualifiers.replaces.defaultimpl + +//tag::clazz[] +import io.micronaut.context.annotation.DefaultImplementation + +@DefaultImplementation(DefaultResponseStrategy::class) +interface ResponseStrategy +//end::clazz[] \ No newline at end of file diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/reactor/ReactorContextPropagationSpec.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/reactor/ReactorContextPropagationSpec.kt new file mode 100644 index 00000000000..49afc400023 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/reactor/ReactorContextPropagationSpec.kt @@ -0,0 +1,156 @@ +package io.micronaut.docs.reactor + +import io.micronaut.context.ApplicationContext +import io.micronaut.context.annotation.Requires +import io.micronaut.core.annotation.Introspected +import io.micronaut.http.HttpRequest +import io.micronaut.http.MutableHttpResponse +import io.micronaut.http.annotation.* +import io.micronaut.http.client.HttpClient +import io.micronaut.http.filter.HttpServerFilter +import io.micronaut.http.filter.ServerFilterChain +import io.micronaut.runtime.server.EmbeddedServer +import jakarta.inject.Singleton +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.currentCoroutineContext +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.single +import kotlinx.coroutines.reactive.asFlow +import kotlinx.coroutines.reactor.ReactorContext +import kotlinx.coroutines.reactor.asCoroutineContext +import kotlinx.coroutines.reactor.mono +import kotlinx.coroutines.withContext +import org.junit.jupiter.api.Test +import org.reactivestreams.Publisher +import reactor.core.publisher.Flux +import reactor.core.publisher.Mono +import reactor.util.context.Context +import reactor.util.function.Tuple2 +import reactor.util.function.Tuples +import java.util.* + +class ReactorContextPropagationSpec { + + @Test + fun testKotlinPropagation() { + val embeddedServer = ApplicationContext.run(EmbeddedServer::class.java, + mapOf("mdc.reactortestpropagation.enabled" to "true" as Any) + ) + val client = embeddedServer.applicationContext.getBean(HttpClient::class.java) + + val result: MutableList> = Flux.range(1, 1000) + .flatMap { + val tracingId = UUID.randomUUID().toString() + val get = HttpRequest.POST("http://localhost:${embeddedServer.port}/trigger", NameRequestBody("sss-" + tracingId)).header("X-TrackingId", tracingId) + Mono.from(client.retrieve(get, String::class.java)) + .map { Tuples.of(it as String, tracingId) } + } + .collectList() + .block() + + for (t in result) { + assert(t.t1 == t.t2) + } + + embeddedServer.stop() + } + + +} + +@Requires(property = "mdc.reactortestpropagation.enabled") +@Controller +class TestController(private val someService: SomeService) { + + @Post("/trigger") + suspend fun trigger(request: HttpRequest<*>, @Body requestBody: SomeBody): String { + return withContext(Dispatchers.IO) { + someService.findValue() + } + } + + // tag::readctx[] + @Get("/data") + suspend fun getTracingId(request: HttpRequest<*>): String { + val reactorContextView = currentCoroutineContext()[ReactorContext.Key]!!.context + return reactorContextView.get("reactorTrackingId") as String + } + // end::readctx[] + +} + +@Introspected +class SomeBody(val name: String) + +@Requires(property = "mdc.reactortestpropagation.enabled") +@Singleton +class SomeService { + + suspend fun findValue(): String { + delay(50) + return withContext(Dispatchers.Default) { + delay(50) + val context = currentCoroutineContext()[ReactorContext.Key]!!.context + val reactorTrackingId = context.get("reactorTrackingId") as String + val suspendTrackingId = context.get("suspendTrackingId") as String + if (reactorTrackingId != suspendTrackingId) { + throw IllegalArgumentException() + } + suspendTrackingId + } + } + +} + +@Introspected +class NameRequestBody(val name: String) + +@Requires(property = "mdc.reactortestpropagation.enabled") +// tag::simplefilter[] +@Filter(Filter.MATCH_ALL_PATTERN) +class ReactorHttpServerFilter : HttpServerFilter { + + override fun doFilter(request: HttpRequest<*>, chain: ServerFilterChain): Publisher> { + val trackingId = request.headers["X-TrackingId"] as String + return Mono.from(chain.proceed(request)).contextWrite { + it.put("reactorTrackingId", trackingId) + } + } + + override fun getOrder(): Int = 1 +} +// end::simplefilter[] + +@Requires(property = "mdc.reactortestpropagation.enabled") +// tag::suspendfilter[] +@Filter(Filter.MATCH_ALL_PATTERN) +class SuspendHttpServerFilter : CoroutineHttpServerFilter { + + override suspend fun filter(request: HttpRequest<*>, chain: ServerFilterChain): MutableHttpResponse<*> { + val trackingId = request.headers["X-TrackingId"] as String + //withContext does not merge the current context so data may be lost + return withContext(Context.of("suspendTrackingId", trackingId).asCoroutineContext()) { + chain.next(request) + } + } + + override fun getOrder(): Int = 0 +} + +interface CoroutineHttpServerFilter : HttpServerFilter { + + suspend fun filter(request: HttpRequest<*>, chain: ServerFilterChain): MutableHttpResponse<*> + + override fun doFilter(request: HttpRequest<*>, chain: ServerFilterChain): Publisher> { + return mono { + filter(request, chain) + } + } + +} + +suspend fun ServerFilterChain.next(request: HttpRequest<*>): MutableHttpResponse<*> { + return this.proceed(request).asFlow().single() +} +// end::suspendfilter[] + diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/replaces/BookFactory.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/replaces/BookFactory.kt new file mode 100644 index 00000000000..897f530d0d1 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/replaces/BookFactory.kt @@ -0,0 +1,37 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.replaces + +import io.micronaut.context.annotation.Factory +import io.micronaut.docs.requires.Book + +import jakarta.inject.Singleton + +// tag::class[] +@Factory +class BookFactory { + + @Singleton + internal fun novel(): Book { + return Book("A Great Novel") + } + + @Singleton + internal fun textBook(): TextBook { + return TextBook("Learning 101") + } +} +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/replaces/BookService.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/replaces/BookService.kt new file mode 100644 index 00000000000..3699744e4e1 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/replaces/BookService.kt @@ -0,0 +1,22 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.replaces + +import io.micronaut.docs.requires.Book + +interface BookService { + fun findBook(title: String): Book? +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/replaces/CustomBookFactory.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/replaces/CustomBookFactory.kt new file mode 100644 index 00000000000..4e1bf1ed192 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/replaces/CustomBookFactory.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.replaces + +import io.micronaut.context.annotation.Factory +import io.micronaut.context.annotation.Replaces +import io.micronaut.docs.requires.Book + +import jakarta.inject.Singleton + +// tag::class[] +@Factory +@Replaces(factory = BookFactory::class) +class CustomBookFactory { + + @Singleton + internal fun otherNovel(): Book { + return Book("An OK Novel") + } +} +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/replaces/JdbcBookService.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/replaces/JdbcBookService.kt new file mode 100644 index 00000000000..304d3768498 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/replaces/JdbcBookService.kt @@ -0,0 +1,48 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.replaces + +import io.micronaut.context.annotation.Requires +import io.micronaut.docs.requires.Book + +import jakarta.inject.Singleton +import javax.sql.DataSource +import java.sql.SQLException + +// tag::replaces[] +@Singleton +@Requires(beans = [DataSource::class]) +class JdbcBookService(internal var dataSource: DataSource) : BookService { + + // end::replaces[] + + override fun findBook(title: String): Book? { + try { + dataSource.connection.use { connection -> + val ps = connection.prepareStatement("select * from books where title = ?") + ps.setString(1, title) + val rs = ps.executeQuery() + if (rs.next()) { + return Book(rs.getString("title")) + } + } + } catch (ex: SQLException) { + return null + } + + return null + } +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/replaces/MockBookService.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/replaces/MockBookService.kt new file mode 100644 index 00000000000..d18572661b2 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/replaces/MockBookService.kt @@ -0,0 +1,35 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.replaces + +import io.micronaut.context.annotation.Replaces +import io.micronaut.docs.requires.Book + +import jakarta.inject.Singleton +import java.util.LinkedHashMap + +// tag::class[] +@Replaces(JdbcBookService::class) // <1> +@Singleton +class MockBookService : BookService { + + var bookMap: Map = LinkedHashMap() + + override fun findBook(title: String): Book? { + return bookMap[title] + } +} +// tag::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/replaces/RequiresSpec.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/replaces/RequiresSpec.kt new file mode 100644 index 00000000000..fd891b66c99 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/replaces/RequiresSpec.kt @@ -0,0 +1,19 @@ +package io.micronaut.docs.replaces + +import io.kotest.matchers.shouldBe +import io.kotest.core.spec.style.StringSpec +import io.kotest.matchers.types.shouldBeInstanceOf +import io.micronaut.context.ApplicationContext +import io.micronaut.docs.requires.Book + +class RequiresSpec : StringSpec({ + + "test bean replaces" { + val applicationContext = ApplicationContext.run() + applicationContext.getBean(BookService::class.java).shouldBeInstanceOf() + applicationContext.getBean(Book::class.java).title.shouldBe("An OK Novel") + applicationContext.getBean(TextBook::class.java).title.shouldBe("Learning 305") + + applicationContext.close() + } +}) diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/replaces/TextBook.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/replaces/TextBook.kt new file mode 100644 index 00000000000..8415d3a00a7 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/replaces/TextBook.kt @@ -0,0 +1,20 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.replaces + +import io.micronaut.docs.requires.Book + +class TextBook(title: String) : Book(title) diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/replaces/TextBookFactory.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/replaces/TextBookFactory.kt new file mode 100644 index 00000000000..dd83ed0db28 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/replaces/TextBookFactory.kt @@ -0,0 +1,33 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.replaces + +import io.micronaut.context.annotation.Factory +import io.micronaut.context.annotation.Replaces + +import jakarta.inject.Singleton + +// tag::class[] +@Factory +class TextBookFactory { + + @Singleton + @Replaces(value = TextBook::class, factory = BookFactory::class) + internal fun textBook(): TextBook { + return TextBook("Learning 305") + } +} +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/requires/Book.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/requires/Book.kt new file mode 100644 index 00000000000..8d6e19b5371 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/requires/Book.kt @@ -0,0 +1,18 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.requires + +open class Book(val title: String) diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/requires/BookService.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/requires/BookService.kt new file mode 100644 index 00000000000..97cd71a54c2 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/requires/BookService.kt @@ -0,0 +1,20 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.requires + +interface BookService { + fun findBook(title: String): Book? +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/requires/JdbcBookService.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/requires/JdbcBookService.kt new file mode 100644 index 00000000000..18b109fc790 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/requires/JdbcBookService.kt @@ -0,0 +1,47 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.requires + +import io.micronaut.context.annotation.Requirements +import io.micronaut.context.annotation.Requires + +import jakarta.inject.Singleton +import javax.sql.DataSource +import java.sql.SQLException + +// tag::requires[] +@Singleton +@Requirements(Requires(beans = [DataSource::class]), Requires(property = "datasource.url")) +class JdbcBookService(internal var dataSource: DataSource) : BookService { +// end::requires[] + + override fun findBook(title: String): Book? { + try { + dataSource.connection.use { connection -> + val ps = connection.prepareStatement("select * from books where title = ?") + ps.setString(1, title) + val rs = ps.executeQuery() + if (rs.next()) { + return Book(rs.getString("title")) + } + } + } catch (ignored: SQLException) { + return null + } + + return null + } +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/requires/RequiresJdbc.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/requires/RequiresJdbc.kt new file mode 100644 index 00000000000..2df47280cf3 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/requires/RequiresJdbc.kt @@ -0,0 +1,30 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.requires + +import io.micronaut.context.annotation.Requirements +import io.micronaut.context.annotation.Requires + +import javax.sql.DataSource +import java.lang.annotation.* + +// tag::annotation[] +@Documented +@Retention(AnnotationRetention.RUNTIME) +@Target(AnnotationTarget.CLASS, AnnotationTarget.FILE) +@Requirements(Requires(beans = [DataSource::class]), Requires(property = "datasource.url")) +annotation class RequiresJdbc +// end::annotation[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/respondingnotfound/BooksController.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/respondingnotfound/BooksController.kt new file mode 100644 index 00000000000..07d45f80ad5 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/respondingnotfound/BooksController.kt @@ -0,0 +1,38 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.respondingnotfound + +import io.micronaut.context.annotation.Requires +import io.micronaut.http.annotation.Controller +import io.micronaut.http.annotation.Get +import reactor.core.publisher.Mono + +@Requires(property = "spec.name", value = "respondingnotfound") +//tag::clazz[] +@Controller("/books") +class BooksController { + + @Get("/stock/{isbn}") + fun stock(isbn: String): Map<*, *>? { + return null //<1> + } + + @Get("/maybestock/{isbn}") + fun maybestock(isbn: String): Mono> { + return Mono.empty() //<2> + } +} +//end::clazz[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/binding/BindingController.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/binding/BindingController.kt new file mode 100644 index 00000000000..59582ec56cb --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/binding/BindingController.kt @@ -0,0 +1,109 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.server.binding + +import io.micronaut.core.convert.format.Format +import io.micronaut.http.annotation.Controller +import io.micronaut.http.annotation.CookieValue +import io.micronaut.http.annotation.Get +import io.micronaut.http.annotation.Header +import java.time.ZonedDateTime + +@Controller("/binding") +class BindingController { + + // tag::cookie1[] + @Get("/cookieName") + fun cookieName(@CookieValue("myCookie") myCookie: String): String { + // ... + // end::cookie1[] + return myCookie + // tag::cookie1[] + } + // end::cookie1[] + + // tag::cookie2[] + @Get("/cookieInferred") + fun cookieInferred(@CookieValue myCookie: String): String { + // ... + // end::cookie2[] + return myCookie + // tag::cookie2[] + } + // end::cookie2[] + + // tag::cookieMultiple[] + @Get("/cookieMultiple") + fun cookieMultiple(@CookieValue("myCookieA") myCookieA: String, + @CookieValue("myCookieB") myCookieB: String): List { + // ... + // end::cookieMultiple[] + return listOf(myCookieA, myCookieB) + // tag::cookieMultiple[] + } + // end::cookieMultiple[] + + + // tag::header1[] + @Get("/headerName") + fun headerName(@Header("Content-Type") contentType: String): String { + // ... + // end::header1[] + return contentType + // tag::header1[] + } + // end::header1[] + + // tag::header2[] + @Get("/headerInferred") + fun headerInferred(@Header contentType: String): String { + // ... + // end::header2[] + return contentType + // tag::header2[] + } + // end::header2[] + + // tag::header3[] + @Get("/headerNullable") + fun headerNullable(@Header contentType: String?): String? { + // ... + // end::header3[] + return contentType + // tag::header3[] + } + // end::header3[] + + // tag::format1[] + @Get("/date") + fun date(@Header date: ZonedDateTime): String { + // ... + // end::format1[] + return date.toString() + // tag::format1[] + } + // end::format1[] + + // tag::format2[] + @Get("/dateFormat") + fun dateFormat(@Format("dd/MM/yyyy hh:mm:ss a z") @Header date: ZonedDateTime): String { + // ... + // end::format2[] + return date.toString() + // tag::format2[] + } + // end::format2[] +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/binding/BindingControllerTest.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/binding/BindingControllerTest.kt new file mode 100644 index 00000000000..5e835f9a931 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/binding/BindingControllerTest.kt @@ -0,0 +1,78 @@ +package io.micronaut.docs.server.binding + +import io.kotest.matchers.shouldBe +import io.kotest.matchers.shouldNotBe +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.core.spec.style.StringSpec +import io.micronaut.context.ApplicationContext +import io.micronaut.http.HttpRequest +import io.micronaut.http.HttpStatus +import io.micronaut.http.client.HttpClient +import io.micronaut.http.client.exceptions.HttpClientResponseException +import io.micronaut.http.cookie.Cookie +import io.micronaut.runtime.server.EmbeddedServer + +class BindingControllerTest: StringSpec() { + + val embeddedServer = autoClose( + ApplicationContext.run(EmbeddedServer::class.java) + ) + + val client = autoClose( + embeddedServer.applicationContext.createBean(HttpClient::class.java, embeddedServer.getURL()) + ) + + init { + "test cookie binding" { + var body = client.toBlocking().retrieve(HttpRequest.GET("/binding/cookieName").cookie(Cookie.of("myCookie", "cookie value"))) + + body shouldNotBe null + body shouldBe "cookie value" + + body = client.toBlocking().retrieve(HttpRequest.GET("/binding/cookieInferred").cookie(Cookie.of("myCookie", "cookie value"))) + + body shouldNotBe null + body shouldBe "cookie value" + } + + "test multiple cookie binding" { + val cookies = HashSet() + cookies.add(Cookie.of("myCookieA", "cookie A value")) + cookies.add(Cookie.of("myCookieB", "cookie B value")) + + var body = client.toBlocking().retrieve(HttpRequest.GET("/binding/cookieMultiple").cookies(cookies)) + + body shouldNotBe null + body shouldBe "[\"cookie A value\",\"cookie B value\"]" + } + + "test header binding"() { + var body = client.toBlocking().retrieve(HttpRequest.GET("/binding/headerName").header("Content-Type", "test")) + + body shouldNotBe null + body shouldBe "test" + + body = client.toBlocking().retrieve(HttpRequest.GET("/binding/headerInferred").header("Content-Type", "test")) + + body shouldNotBe null + body shouldBe "test" + + val ex = shouldThrow { + client.toBlocking().retrieve(HttpRequest.GET("/binding/headerNullable")) + } + ex.response.status shouldBe HttpStatus.NOT_FOUND + } + + "test header date binding"() { + var body = client.toBlocking().retrieve(HttpRequest.GET("/binding/date").header("date", "Tue, 3 Jun 2008 11:05:30 GMT")) + + body shouldNotBe null + body shouldBe "2008-06-03T11:05:30Z" + + body = client.toBlocking().retrieve(HttpRequest.GET("/binding/dateFormat").header("date", "03/06/2008 11:05:30 AM GMT")) + + body shouldNotBe null + body shouldBe "2008-06-03T11:05:30Z[GMT]" + } + } +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/binding/BookmarkController.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/binding/BookmarkController.kt new file mode 100644 index 00000000000..dc3f37b025e --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/binding/BookmarkController.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.server.binding + +// tag::imports[] +import io.micronaut.http.HttpStatus +import io.micronaut.http.annotation.Controller +import io.micronaut.http.annotation.Get +import javax.validation.Valid +// end::imports[] + +// tag::class[] +@Controller("/api") +open class BookmarkController { + + @Get("/bookmarks/list{?paginationCommand*}") + open fun list(@Valid paginationCommand: PaginationCommand): HttpStatus { + return HttpStatus.OK + } +} +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/binding/BookmarkControllerTest.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/binding/BookmarkControllerTest.kt new file mode 100644 index 00000000000..b8c86dde8ee --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/binding/BookmarkControllerTest.kt @@ -0,0 +1,31 @@ +package io.micronaut.docs.server.binding + +import io.micronaut.context.ApplicationContext +import io.micronaut.runtime.server.EmbeddedServer +import io.kotest.matchers.shouldBe +import io.kotest.core.spec.style.StringSpec +import io.micronaut.http.HttpStatus +import io.micronaut.http.client.HttpClient +import io.micronaut.http.uri.UriTemplate + +class BookmarkControllerTest: StringSpec() { + + val embeddedServer = autoClose( + ApplicationContext.run(EmbeddedServer::class.java) + ) + + val client = autoClose( + embeddedServer.applicationContext.createBean(HttpClient::class.java, embeddedServer.getURL()) + ) + + init { + "test bookmark controller" { + var template = UriTemplate("/api/bookmarks/list{?offset,max,sort,order}") + var uri = template.expand(mapOf("offset" to 0, "max" to 10)) + + var response = client.toBlocking().exchange(uri) + + response.status shouldBe HttpStatus.OK + } + } +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/binding/MovieTicketBean.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/binding/MovieTicketBean.kt new file mode 100644 index 00000000000..e633ca5c60d --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/binding/MovieTicketBean.kt @@ -0,0 +1,35 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.server.binding + +// tag::imports[] +import io.micronaut.core.annotation.Introspected +import io.micronaut.http.HttpRequest +import io.micronaut.http.annotation.PathVariable +import io.micronaut.http.annotation.QueryValue +import javax.annotation.Nullable +import javax.validation.constraints.PositiveOrZero +// end::imports[] + +// tag::class[] +@Introspected +data class MovieTicketBean( + val httpRequest: HttpRequest, + @field:PathVariable val movieId: String, + @field:QueryValue @field:PositiveOrZero @field:Nullable val minPrice: Double, + @field:QueryValue @field:PositiveOrZero @field:Nullable val maxPrice: Double +) +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/binding/MovieTicketController.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/binding/MovieTicketController.kt new file mode 100644 index 00000000000..a93fe966291 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/binding/MovieTicketController.kt @@ -0,0 +1,38 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.server.binding + +// tag::imports[] +import io.micronaut.http.HttpStatus +import io.micronaut.http.annotation.Controller +import io.micronaut.http.annotation.Get +import io.micronaut.http.annotation.RequestBean +import javax.validation.Valid +// end::imports[] + +// tag::class[] +@Controller("/api") +open class MovieTicketController { + + // You can also omit query parameters like: + // @Get("/movie/ticket/{movieId} + @Get("/movie/ticket/{movieId}{?minPrice,maxPrice}") + open fun list(@Valid @RequestBean bean: MovieTicketBean): HttpStatus { + return HttpStatus.OK + } + +} +// end::class[] \ No newline at end of file diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/binding/MovieTicketControllerTest.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/binding/MovieTicketControllerTest.kt new file mode 100644 index 00000000000..5fe8bbe80d8 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/binding/MovieTicketControllerTest.kt @@ -0,0 +1,30 @@ +package io.micronaut.docs.server.binding + +import io.kotest.matchers.shouldBe +import io.kotest.core.spec.style.StringSpec +import io.micronaut.context.ApplicationContext +import io.micronaut.http.HttpStatus +import io.micronaut.http.client.HttpClient +import io.micronaut.http.uri.UriTemplate +import io.micronaut.runtime.server.EmbeddedServer + +class MovieTicketControllerTest : StringSpec() { + val embeddedServer = autoClose( + ApplicationContext.run(EmbeddedServer::class.java) + ) + + val client = autoClose( + embeddedServer.applicationContext.createBean(HttpClient::class.java, embeddedServer.getURL()) + ) + + init { + "test bookmark controller" { + var template = UriTemplate("/api/movie/ticket/terminator{?minPrice,maxPrice}") + var uri = template.expand(mapOf("minPrice" to 5.0, "maxPrice" to 20.0)) + + var response = client.toBlocking().exchange(uri) + + response.status shouldBe HttpStatus.OK + } + } +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/binding/PaginationCommand.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/binding/PaginationCommand.kt new file mode 100644 index 00000000000..3396c8824d8 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/binding/PaginationCommand.kt @@ -0,0 +1,48 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.server.binding + +// tag::imports[] +import io.micronaut.core.annotation.Introspected +import javax.validation.constraints.Pattern +import javax.validation.constraints.Positive +import javax.validation.constraints.PositiveOrZero + +// end::imports[] + +/** + * @author Puneet Behl + * @since 1.0 + */ +// tag::class[] +@Introspected +class PaginationCommand { + // end::class[] + + // tag::props[] + @PositiveOrZero + var offset: Int? = null + + @Positive + var max: Int? = null + + @Pattern(regexp = "name|href|title") + var sort: String? = null + + @Pattern(regexp = "asc|desc|ASC|DESC") + var order: String? = null + // end::props[] +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/body/MessageController.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/body/MessageController.kt new file mode 100644 index 00000000000..b6079d18794 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/body/MessageController.kt @@ -0,0 +1,56 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.server.body + +// tag::imports[] +import io.micronaut.http.HttpResponse +import io.micronaut.http.MediaType +import io.micronaut.http.annotation.Body +import io.micronaut.http.annotation.Controller +import io.micronaut.http.annotation.Post +import javax.validation.constraints.Size +// end::imports[] +// tag::importsreactive[] +import org.reactivestreams.Publisher +import io.micronaut.core.async.annotation.SingleResult +import reactor.core.publisher.Flux +// end::importsreactive[] + +// tag::class[] +@Controller("/receive") +open class MessageController { +// end::class[] + + // tag::echo[] + @Post(value = "/echo", consumes = [MediaType.TEXT_PLAIN]) // <1> + open fun echo(@Size(max = 1024) @Body text: String): String { // <2> + return text // <3> + } + // end::echo[] + + // tag::echoReactive[] + @Post(value = "/echo-publisher", consumes = [MediaType.TEXT_PLAIN]) // <1> + @SingleResult + open fun echoFlow(@Body text: Publisher): Publisher> { //<2> + return Flux.from(text) + .collect({ StringBuffer() }, { obj, str -> obj.append(str) }) // <3> + .map { buffer -> HttpResponse.ok(buffer.toString()) } + } + // end::echoReactive[] + +// tag::endclass[] +} +// end::endclass[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/body/MessageControllerSpec.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/body/MessageControllerSpec.kt new file mode 100644 index 00000000000..78576a20c8a --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/body/MessageControllerSpec.kt @@ -0,0 +1,40 @@ +package io.micronaut.docs.server.body + +import io.kotest.matchers.shouldBe +import io.kotest.core.spec.style.StringSpec +import io.micronaut.context.ApplicationContext +import io.micronaut.http.HttpRequest +import io.micronaut.http.MediaType +import io.micronaut.http.client.HttpClient +import io.micronaut.runtime.server.EmbeddedServer + +class MessageControllerSpec: StringSpec() { + + val embeddedServer = autoClose( + ApplicationContext.run(EmbeddedServer::class.java) + ) + + val client = autoClose( + embeddedServer.applicationContext.createBean(HttpClient::class.java, embeddedServer.getURL()) + ) + + init { + "test echo response"() { + val body = "My Text" + val response = client.toBlocking().retrieve( + HttpRequest.POST("/receive/echo", body) + .contentType(MediaType.TEXT_PLAIN_TYPE), String::class.java) + + response shouldBe body + } + + "test echo reactive response"() { + val body = "My Text" + val response = client.toBlocking().retrieve( + HttpRequest.POST("/receive/echo-publisher", body) + .contentType(MediaType.TEXT_PLAIN_TYPE), String::class.java) + + response shouldBe body + } + } +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/consumes/ConsumesController.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/consumes/ConsumesController.kt new file mode 100644 index 00000000000..cfcc14fded5 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/consumes/ConsumesController.kt @@ -0,0 +1,48 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.server.consumes + +//tag::imports[] +import io.micronaut.context.annotation.Requires +import io.micronaut.http.HttpResponse +import io.micronaut.http.MediaType +import io.micronaut.http.annotation.Consumes +import io.micronaut.http.annotation.Controller +import io.micronaut.http.annotation.Post +//end::imports[] + +@Requires(property = "spec.name", value = "consumesspec") +//tag::clazz[] +@Controller("/consumes") +class ConsumesController { + + @Post // <1> + fun index(): HttpResponse<*> { + return HttpResponse.ok() + } + + @Consumes(MediaType.APPLICATION_FORM_URLENCODED, MediaType.APPLICATION_JSON) // <2> + @Post("/multiple") + fun multipleConsumes(): HttpResponse<*> { + return HttpResponse.ok() + } + + @Post(value = "/member", consumes = [MediaType.TEXT_PLAIN]) // <3> + fun consumesMember(): HttpResponse<*> { + return HttpResponse.ok() + } +} +//end::clazz[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/consumes/ConsumesControllerSpec.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/consumes/ConsumesControllerSpec.kt new file mode 100644 index 00000000000..ed2a14afc3b --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/consumes/ConsumesControllerSpec.kt @@ -0,0 +1,62 @@ +package io.micronaut.docs.server.consumes + +import io.kotest.assertions.throwables.shouldNotThrowAny +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.core.spec.style.StringSpec +import io.micronaut.context.ApplicationContext +import io.micronaut.core.annotation.Introspected +import io.micronaut.http.HttpRequest +import io.micronaut.http.MediaType +import io.micronaut.http.client.HttpClient +import io.micronaut.http.client.exceptions.HttpClientResponseException +import io.micronaut.runtime.server.EmbeddedServer + +class ConsumesControllerSpec: StringSpec() { + + val embeddedServer = autoClose( + ApplicationContext.run(EmbeddedServer::class.java, mapOf("spec.name" to "consumesspec")) + ) + + val client = autoClose( + embeddedServer.applicationContext.createBean(HttpClient::class.java, embeddedServer.getURL()) + ) + + init { + "test consumes"() { + val book = Book() + book.title = "The Stand" + book.pages = 1000 + + shouldThrow { + client.toBlocking().exchange(HttpRequest.POST("/consumes", book) + .contentType(MediaType.APPLICATION_FORM_URLENCODED_TYPE)) + } + + shouldNotThrowAny { + client.toBlocking().exchange(HttpRequest.POST("/consumes", book) + .contentType(MediaType.APPLICATION_JSON)) + } + + shouldNotThrowAny { + client.toBlocking().exchange(HttpRequest.POST("/consumes/multiple", book) + .contentType(MediaType.APPLICATION_FORM_URLENCODED_TYPE)) + } + + shouldNotThrowAny { + client.toBlocking().exchange(HttpRequest.POST("/consumes/multiple", book) + .contentType(MediaType.APPLICATION_JSON)) + } + + shouldNotThrowAny { + client.toBlocking().exchange(HttpRequest.POST("/consumes/member", book) + .contentType(MediaType.TEXT_PLAIN)) + } + } + } + + @Introspected + class Book { + var title: String? = null + var pages: Int? = null + } +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/endpoint/AlertsEndpoint.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/endpoint/AlertsEndpoint.kt new file mode 100644 index 00000000000..d15e9d9a826 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/endpoint/AlertsEndpoint.kt @@ -0,0 +1,38 @@ +package io.micronaut.docs.server.endpoint + +import io.micronaut.context.annotation.Requires +//tag::imports[] +import io.micronaut.http.MediaType +import io.micronaut.management.endpoint.annotation.Delete +import io.micronaut.management.endpoint.annotation.Endpoint +import io.micronaut.management.endpoint.annotation.Read +import io.micronaut.management.endpoint.annotation.Sensitive +import io.micronaut.management.endpoint.annotation.Write +import java.util.concurrent.CopyOnWriteArrayList +//end::imports[] + +@Requires(property = "spec.name", value = "AlertsEndpointSpec") +//tag::clazz[] +@Endpoint(id = "alerts", defaultSensitive = false) // <1> +class AlertsEndpoint { + + private val alerts: MutableList = CopyOnWriteArrayList() + + @Read + fun getAlerts(): List { + return alerts + } + + @Delete + @Sensitive(true) // <2> + fun clearAlerts() { + alerts.clear() + } + + @Write(consumes = [MediaType.TEXT_PLAIN]) + @Sensitive(property = "add.sensitive", defaultValue = true) // <3> + fun addAlert(alert: String) { + alerts.add(alert) + } +} +//end::clazz[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/endpoint/AlertsEndpointSpec.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/endpoint/AlertsEndpointSpec.kt new file mode 100644 index 00000000000..48471d00049 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/endpoint/AlertsEndpointSpec.kt @@ -0,0 +1,57 @@ +package io.micronaut.docs.server.endpoint + +import io.kotest.matchers.shouldBe +import io.kotest.core.spec.style.StringSpec +import io.micronaut.context.ApplicationContext +import io.micronaut.core.type.Argument +import io.micronaut.http.HttpRequest +import io.micronaut.http.HttpStatus +import io.micronaut.http.MediaType +import io.micronaut.http.client.HttpClient +import io.micronaut.http.client.exceptions.HttpClientResponseException +import io.micronaut.runtime.server.EmbeddedServer +import reactor.core.publisher.Flux + +class AlertsEndpointSpec: StringSpec() { + + init { + "test adding an alert" { + var server = ApplicationContext.run(EmbeddedServer::class.java, mapOf("spec.name" to AlertsEndpointSpec::class.simpleName)) + var client = server.applicationContext.createBean(HttpClient::class.java, server.url) + try { + client.toBlocking().exchange(HttpRequest.POST("/alerts", "First alert").contentType(MediaType.TEXT_PLAIN_TYPE), String::class.java) + } catch (ex: HttpClientResponseException) { + ex.response.status() shouldBe HttpStatus.UNAUTHORIZED + } + server.close() + } + + "test adding an alert not sensitive" { + var server = ApplicationContext.run(EmbeddedServer::class.java, + mapOf("spec.name" to AlertsEndpointSpec::class.simpleName, + "endpoints.alerts.add.sensitive" to false) + ) + var client = server.applicationContext.createBean(HttpClient::class.java, server.url) + + val response = client.toBlocking().exchange(HttpRequest.POST("/alerts", "First alert").contentType(MediaType.TEXT_PLAIN_TYPE), String::class.java) + response.status() shouldBe HttpStatus.OK + + val alerts = client.toBlocking().retrieve(HttpRequest.GET("/alerts"), Argument.LIST_OF_STRING) + alerts[0] shouldBe "First alert" + + server.close() + } + + "test clearing alerts" { + var server = ApplicationContext.run(EmbeddedServer::class.java, mapOf("spec.name" to AlertsEndpointSpec::class.simpleName)) + var client = server.applicationContext.createBean(HttpClient::class.java, server.url) + try { + client.toBlocking().exchange(HttpRequest.DELETE("/alerts"), String::class.java) + } catch (ex: HttpClientResponseException) { + ex.response.status() shouldBe HttpStatus.UNAUTHORIZED + } + server.close() + } + } + +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/endpoint/CurrentDateEndpoint.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/endpoint/CurrentDateEndpoint.kt new file mode 100644 index 00000000000..7322fdaac41 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/endpoint/CurrentDateEndpoint.kt @@ -0,0 +1,81 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.server.endpoint + +//tag::endpointImport[] +import io.micronaut.management.endpoint.annotation.Endpoint +//end::endpointImport[] + +//tag::readImport[] +import io.micronaut.management.endpoint.annotation.Read +//end::readImport[] + +//tag::mediaTypeImport[] +import io.micronaut.http.MediaType +import io.micronaut.management.endpoint.annotation.Selector +//end::mediaTypeImport[] + +//tag::writeImport[] +import io.micronaut.management.endpoint.annotation.Write +//end::writeImport[] + +import jakarta.annotation.PostConstruct +import java.util.Date + +//tag::endpointClassBegin[] +@Endpoint(id = "date", prefix = "custom", defaultEnabled = true, defaultSensitive = false) +class CurrentDateEndpoint { + //end::endpointClassBegin[] + + //tag::methodSummary[] + //.. endpoint methods + //end::methodSummary[] + + //tag::currentDate[] + private var currentDate: Date? = null + //end::currentDate[] + + @PostConstruct + fun init() { + currentDate = Date() + } + + //tag::simpleRead[] + @Read + fun currentDate(): Date? { + return currentDate + } + //end::simpleRead[] + + //tag::readArg[] + @Read(produces = [MediaType.TEXT_PLAIN]) //<1> + fun currentDatePrefix(@Selector prefix: String): String { + return "$prefix: $currentDate" + } + //end::readArg[] + + //tag::simpleWrite[] + @Write + fun reset(): String { + currentDate = Date() + + return "Current date reset" + } + //end::simpleWrite[] + //tag::endpointClassEnd[] +} +//end::endpointClassEnd[] + diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/endpoint/CurrentDateEndpointSpec.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/endpoint/CurrentDateEndpointSpec.kt new file mode 100644 index 00000000000..f90685b28fd --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/endpoint/CurrentDateEndpointSpec.kt @@ -0,0 +1,80 @@ +package io.micronaut.docs.server.endpoint + +import io.kotest.matchers.shouldBe +import io.kotest.core.spec.style.StringSpec +import io.micronaut.context.ApplicationContext +import io.micronaut.http.HttpRequest +import io.micronaut.http.HttpStatus +import io.micronaut.http.MediaType +import io.micronaut.http.client.HttpClient +import io.micronaut.http.client.exceptions.HttpClientResponseException +import io.micronaut.runtime.server.EmbeddedServer +import reactor.core.publisher.Flux + +import java.util.Date + +class CurrentDateEndpointSpec: StringSpec() { + + val embeddedServer = autoClose( + ApplicationContext.run(EmbeddedServer::class.java) + ) + + val client = autoClose( + embeddedServer.applicationContext.createBean(HttpClient::class.java, embeddedServer.url) + ) + + init { + "test read custom date endpoint" { + val response = client.toBlocking().exchange("/date", String::class.java) + + response.code() shouldBe HttpStatus.OK.code + } + + "test read custom date endpoint with argument" { + val response = client.toBlocking().exchange("/date/current_date_is", String::class.java) + + response.code() shouldBe HttpStatus.OK.code + response.body()!!.startsWith("current_date_is: ") shouldBe true + } + + // issue https://github.com/micronaut-projects/micronaut-core/issues/883 + "test read with produces" { + val response = client.toBlocking().exchange("/date/current_date_is", String::class.java) + + response.contentType.get() shouldBe MediaType.TEXT_PLAIN_TYPE + } + + "test write custom date endpoint" { + val originalDate: Date + val resetDate: Date + + var response = client.toBlocking().exchange("/date", String::class.java) + originalDate = Date(java.lang.Long.parseLong(response.body()!!)) + + response = client.toBlocking().exchange(HttpRequest.POST>("/date", mapOf()), String::class.java) + + response.code() shouldBe HttpStatus.OK.code + response.body() shouldBe "Current date reset" + + response = client.toBlocking().exchange("/date", String::class.java) + resetDate = Date(java.lang.Long.parseLong(response.body()!!)) + + assert(resetDate.time > originalDate.time) + } + + "test disable endpoint" { + embeddedServer.stop() // top the previously created server otherwise a port conflict will occur + + val server = ApplicationContext.run(EmbeddedServer::class.java, mapOf("custom.date.enabled" to false)) + val rxClient = server.applicationContext.createBean(HttpClient::class.java, server.url) + + try { + rxClient.toBlocking().exchange("/date", String::class.java) + } catch (ex: HttpClientResponseException) { + ex.response.code() shouldBe HttpStatus.NOT_FOUND.code + } + + server.close() + } + } +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/endpoint/MessageEndpoint.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/endpoint/MessageEndpoint.kt new file mode 100644 index 00000000000..0a1c78e09e2 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/endpoint/MessageEndpoint.kt @@ -0,0 +1,79 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.server.endpoint + +//tag::endpointImport[] +import io.micronaut.context.annotation.Requires +import io.micronaut.management.endpoint.annotation.Endpoint +//end::endpointImport[] + +//tag::mediaTypeImport[] +import io.micronaut.http.MediaType +//end::mediaTypeImport[] + +//tag::writeImport[] +import io.micronaut.management.endpoint.annotation.Write +//end::writeImport[] + +//tag::deleteImport[] +import io.micronaut.management.endpoint.annotation.Delete +//end::deleteImport[] + +import io.micronaut.management.endpoint.annotation.Read + +import jakarta.annotation.PostConstruct + +@Requires(property = "spec.name", value = "MessageEndpointSpec") +//tag::endpointClassBegin[] +@Endpoint(id = "message", defaultSensitive = false) +class MessageEndpoint { + //end::endpointClassBegin[] + + //tag::message[] + internal var message: String? = null + //end::message[] + + @PostConstruct + fun init() { + this.message = "default message" + } + + @Read + fun message(): String? { + return this.message + } + + //tag::writeArg[] + @Write(consumes = [MediaType.APPLICATION_FORM_URLENCODED], produces = [MediaType.TEXT_PLAIN]) + fun updateMessage(newMessage: String): String { //<1> + this.message = newMessage + + return "Message updated" + } + //end::writeArg[] + + //tag::simpleDelete[] + @Delete + fun deleteMessage(): String { + this.message = null + + return "Message deleted" + } + //end::simpleDelete[] + + //tag::endpointClassEnd[] +} +//end::endpointClassEnd[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/endpoint/MessageEndpointSpec.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/endpoint/MessageEndpointSpec.kt new file mode 100644 index 00000000000..86144134c72 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/endpoint/MessageEndpointSpec.kt @@ -0,0 +1,63 @@ +package io.micronaut.docs.server.endpoint + +import io.kotest.matchers.shouldBe +import io.kotest.core.spec.style.StringSpec +import io.micronaut.context.ApplicationContext +import io.micronaut.http.HttpRequest +import io.micronaut.http.HttpStatus +import io.micronaut.http.MediaType +import io.micronaut.http.client.HttpClient +import io.micronaut.http.client.exceptions.HttpClientResponseException +import io.micronaut.runtime.server.EmbeddedServer + +import org.junit.Assert.fail +import reactor.core.publisher.Flux + +class MessageEndpointSpec: StringSpec() { + + val embeddedServer = autoClose( + ApplicationContext.run(EmbeddedServer::class.java, + mapOf("spec.name" to MessageEndpointSpec::class.java.simpleName, "endpoints.message.enabled" to true)) + ) + + val client = autoClose( + embeddedServer.applicationContext.createBean(HttpClient::class.java, embeddedServer.url) + ) + + init { + "test read message endpoint" { + val response = client.toBlocking().exchange("/message", String::class.java) + + response.code() shouldBe HttpStatus.OK.code + response.body() shouldBe "default message" + } + + "test write message endpoint" { + var response = Flux.from(client.exchange(HttpRequest.POST>("/message", mapOf("newMessage" to "A new message")) + .contentType(MediaType.APPLICATION_FORM_URLENCODED), String::class.java)).blockFirst() + + response.code() shouldBe HttpStatus.OK.code + response.body() shouldBe "Message updated" + response.contentType.get() shouldBe MediaType.TEXT_PLAIN_TYPE + + response = client.toBlocking().exchange("/message", String::class.java) + + response.body() shouldBe "A new message" + } + + "test delete message endpoint" { + val response = client.toBlocking().exchange(HttpRequest.DELETE("/message"), String::class.java) + + response.code() shouldBe HttpStatus.OK.code + response.body() shouldBe "Message deleted" + + try { + client.toBlocking().exchange("/message", String::class.java) + } catch (e: HttpClientResponseException) { + e.status.code shouldBe 404 + } catch (e: Exception) { + fail("Wrong exception thrown") + } + } + } +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/exception/BookController.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/exception/BookController.kt new file mode 100644 index 00000000000..a8268e461ed --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/exception/BookController.kt @@ -0,0 +1,35 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.server.exception + +import io.micronaut.context.annotation.Requires +import io.micronaut.http.MediaType +import io.micronaut.http.annotation.Controller +import io.micronaut.http.annotation.Get +import io.micronaut.http.annotation.Produces + +@Requires(property = "spec.name", value = "ExceptionHandlerSpec") +//tag::clazz[] +@Controller("/books") +class BookController { + + @Produces(MediaType.TEXT_PLAIN) + @Get("/stock/{isbn}") + internal fun stock(isbn: String): Int? { + throw OutOfStockException() + } +} +//end::clazz[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/exception/ExceptionHandlerSpec.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/exception/ExceptionHandlerSpec.kt new file mode 100644 index 00000000000..d6dfc78f22e --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/exception/ExceptionHandlerSpec.kt @@ -0,0 +1,43 @@ +package io.micronaut.docs.server.exception + +import io.kotest.matchers.shouldBe +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.core.spec.style.StringSpec +import io.micronaut.context.ApplicationContext +import io.micronaut.core.type.Argument +import io.micronaut.http.HttpRequest +import io.micronaut.http.HttpStatus +import io.micronaut.http.client.HttpClient +import io.micronaut.http.client.exceptions.HttpClientResponseException +import io.micronaut.runtime.server.EmbeddedServer + +class ExceptionHandlerSpec: StringSpec() { + + val embeddedServer = autoClose( + ApplicationContext.run(EmbeddedServer::class.java, mapOf("spec.name" to ExceptionHandlerSpec::class.simpleName)) + ) + + val client = autoClose( + embeddedServer.applicationContext.createBean(HttpClient::class.java, embeddedServer.getURL()) + ) + + init { + "test exception is handled"() { + val request = HttpRequest.GET("/books/stock/1234") + val errorType = Argument.mapOf( + String::class.java, + Any::class.java + ) + val ex = shouldThrow { + client!!.toBlocking().retrieve(request, Argument.LONG, errorType) + } + + val response = ex.response + val embedded: Map<*, *> = response.getBody(Map::class.java).get().get("_embedded") as Map<*, *> + val message = ((embedded.get("errors") as java.util.List<*>).get(0) as Map<*, *>).get("message") + + response.status().shouldBe(HttpStatus.BAD_REQUEST) + message shouldBe("No stock available") + } + } +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/exception/OutOfStockException.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/exception/OutOfStockException.kt new file mode 100644 index 00000000000..65c8afa2a75 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/exception/OutOfStockException.kt @@ -0,0 +1,20 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.server.exception + +//tag::clazz[] +class OutOfStockException : RuntimeException() +//end::clazz[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/exception/OutOfStockExceptionHandler.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/exception/OutOfStockExceptionHandler.kt new file mode 100644 index 00000000000..9f4c8a2d551 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/exception/OutOfStockExceptionHandler.kt @@ -0,0 +1,48 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.server.exception + +import io.micronaut.context.annotation.Requirements +import io.micronaut.context.annotation.Requires +import io.micronaut.http.HttpRequest +import io.micronaut.http.HttpResponse +import io.micronaut.http.annotation.Produces +import io.micronaut.http.server.exceptions.ExceptionHandler +import io.micronaut.http.server.exceptions.response.ErrorContext +import io.micronaut.http.server.exceptions.response.ErrorResponseProcessor +import jakarta.inject.Singleton + +//tag::clazz[] +@Produces +@Singleton +@Requirements( +//end::clazz[] + Requires(property = "spec.name", value = "ExceptionHandlerSpec"), +//tag::clazz[] + Requires(classes = [OutOfStockException::class, ExceptionHandler::class]) +) +class OutOfStockExceptionHandler(private val errorResponseProcessor: ErrorResponseProcessor) : + ExceptionHandler> { + + override fun handle(request: HttpRequest<*>, exception: OutOfStockException): HttpResponse<*> { + return errorResponseProcessor.processResponse( + ErrorContext.builder(request) + .cause(exception) + .errorMessage("No stock available") + .build(), HttpResponse.badRequest()) // <1> + } +} +//end::clazz[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/filters/TraceFilter.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/filters/TraceFilter.kt new file mode 100644 index 00000000000..1f455669336 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/filters/TraceFilter.kt @@ -0,0 +1,46 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.server.filters + +// tag::imports[] +import io.micronaut.http.HttpRequest +import io.micronaut.http.MutableHttpResponse +import io.micronaut.http.annotation.Filter +import io.micronaut.http.filter.HttpServerFilter +import io.micronaut.http.filter.ServerFilterChain +import org.reactivestreams.Publisher +// end::imports[] + +// tag::class[] +@Filter("/hello/**") // <1> +class TraceFilter(// <2> + private val traceService: TraceService)// <3> + : HttpServerFilter { + // end::class[] + + // tag::doFilter[] + override fun doFilter(request: HttpRequest<*>, + chain: ServerFilterChain): Publisher> { + return traceService.trace(request) // <1> + .switchMap { aBoolean -> chain.proceed(request) } // <2> + .doOnNext { res -> + res.headers.add("X-Trace-Enabled", "true") // <3> + } + } + // end::doFilter[] +// tag::endclass[] +} +// end::endclass[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/filters/TraceFilterSpec.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/filters/TraceFilterSpec.kt new file mode 100644 index 00000000000..494d401af96 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/filters/TraceFilterSpec.kt @@ -0,0 +1,30 @@ +package io.micronaut.docs.server.filters + +import io.kotest.matchers.shouldBe +import io.kotest.core.spec.style.StringSpec +import io.micronaut.context.ApplicationContext +import io.micronaut.docs.server.intro.HelloControllerSpec +import io.micronaut.http.HttpRequest +import io.micronaut.http.client.HttpClient +import io.micronaut.runtime.server.EmbeddedServer + +class TraceFilterSpec: StringSpec() { + + val embeddedServer = autoClose( + ApplicationContext.run(EmbeddedServer::class.java, + mapOf("spec.name" to HelloControllerSpec::class.java.simpleName, "spec.lang" to "java")) + ) + + val client = autoClose( + embeddedServer.applicationContext.createBean(HttpClient::class.java, embeddedServer.url) + ) + + init { + "test trace filter" { + val response = client.toBlocking().exchange(HttpRequest.GET("/hello")) + + response.headers.get("X-Trace-Enabled") shouldBe "true" + } + } +} + diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/filters/TraceService.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/filters/TraceService.kt new file mode 100644 index 00000000000..c44230d32e5 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/filters/TraceService.kt @@ -0,0 +1,44 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.server.filters + +// tag::imports[] +import io.micronaut.http.HttpRequest +import org.slf4j.LoggerFactory +import jakarta.inject.Singleton +import reactor.core.publisher.Flux +import reactor.core.publisher.Mono +import reactor.core.scheduler.Schedulers + +// end::imports[] + +// tag::class[] +@Singleton +class TraceService { + + private val LOG = LoggerFactory.getLogger(TraceService::class.java) + + internal fun trace(request: HttpRequest<*>): Flux { + return Mono.fromCallable { + // <1> + LOG.debug("Tracing request: {}", request.uri) + // trace logic here, potentially performing I/O <2> + true + }.subscribeOn(Schedulers.boundedElastic()) // <3> + .flux() + } +} +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/intro/Application.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/intro/Application.kt new file mode 100644 index 00000000000..c350c1cbf91 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/intro/Application.kt @@ -0,0 +1,30 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.server.intro + +// tag::imports[] +import io.micronaut.runtime.Micronaut +// end::imports[] + +// tag::class[] +object Application { + + @JvmStatic + fun main(args: Array) { + Micronaut.run(Application.javaClass) + } +} +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/intro/HelloClient.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/intro/HelloClient.kt new file mode 100644 index 00000000000..6ca0d8d9969 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/intro/HelloClient.kt @@ -0,0 +1,38 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.server.intro + +// tag::imports[] +import io.micronaut.http.MediaType +import io.micronaut.http.annotation.Get +import io.micronaut.http.client.annotation.Client +import io.micronaut.core.async.annotation.SingleResult +import org.reactivestreams.Publisher +// end::imports[] + +/** + * @author graemerocher + * @since 1.0 + */ +// tag::class[] +@Client("/hello") // <1> +interface HelloClient { + + @Get(consumes = [MediaType.TEXT_PLAIN]) // <2> + @SingleResult + fun hello(): Publisher // <3> +} +// end::class[] \ No newline at end of file diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/intro/HelloClientSpec.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/intro/HelloClientSpec.kt new file mode 100644 index 00000000000..3dd87fd50df --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/intro/HelloClientSpec.kt @@ -0,0 +1,30 @@ +package io.micronaut.docs.server.intro + +// tag::imports[] +import io.micronaut.context.annotation.Property +import io.micronaut.test.extensions.junit5.annotation.MicronautTest +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import jakarta.inject.Inject +import reactor.core.publisher.Mono + +// end::imports[] + +/** + * @author graemerocher + * @since 1.0 + */ +@Property(name = "spec.name", value = "HelloControllerSpec") +// tag::class[] +@MicronautTest // <1> +class HelloClientSpec { + + @Inject + lateinit var client: HelloClient // <2> + + @Test + fun testHelloWorldResponse() { + assertEquals("Hello World", Mono.from(client.hello()).block())// <3> + } +} +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/intro/HelloController.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/intro/HelloController.kt new file mode 100644 index 00000000000..a065daf5260 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/intro/HelloController.kt @@ -0,0 +1,35 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.server.intro + +import io.micronaut.context.annotation.Requires +// tag::imports[] +import io.micronaut.http.MediaType +import io.micronaut.http.annotation.Controller +import io.micronaut.http.annotation.Get +// end::imports[] + +@Requires(property = "spec.name", value = "HelloControllerSpec") +// tag::class[] +@Controller("/hello") // <1> +class HelloController { + + @Get(produces = [MediaType.TEXT_PLAIN]) // <2> + fun index(): String { + return "Hello World" // <3> + } +} +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/intro/HelloControllerSpec.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/intro/HelloControllerSpec.kt new file mode 100644 index 00000000000..acce91eaef2 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/intro/HelloControllerSpec.kt @@ -0,0 +1,48 @@ +/* + * Copyright 2017-2019 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.server.intro + +import io.micronaut.context.annotation.Property +// tag::imports[] +import io.micronaut.http.client.HttpClient +import io.micronaut.http.client.annotation.Client +import io.micronaut.runtime.server.EmbeddedServer +import io.micronaut.test.extensions.junit5.annotation.MicronautTest +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import jakarta.inject.Inject +// end::imports[] + +@Property(name = "spec.name", value = "HelloControllerSpec") +// tag::class[] +@MicronautTest +class HelloControllerSpec { + + @Inject + lateinit var server: EmbeddedServer // <1> + + @Inject + @field:Client("/") + lateinit var client: HttpClient // <2> + + @Test + fun testHelloWorldResponse() { + val rsp: String = client.toBlocking() // <3> + .retrieve("/hello") + assertEquals("Hello World", rsp) // <4> + } +} +//end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/json/Person.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/json/Person.kt new file mode 100644 index 00000000000..2acc0d4b576 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/json/Person.kt @@ -0,0 +1,30 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.server.json + +class Person { + + lateinit var firstName: String + lateinit var lastName: String + var age: Int = 0 + + constructor(firstName: String, lastName: String) { + this.firstName = firstName + this.lastName = lastName + } + + constructor() {} +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/json/PersonController.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/json/PersonController.kt new file mode 100644 index 00000000000..6ae2156ecd4 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/json/PersonController.kt @@ -0,0 +1,137 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.server.json + +import com.fasterxml.jackson.core.JsonParseException +import io.micronaut.context.annotation.Requires +import io.micronaut.http.HttpRequest +import io.micronaut.http.HttpResponse +import io.micronaut.http.HttpStatus +import io.micronaut.http.annotation.Body +import io.micronaut.http.annotation.Controller +import io.micronaut.http.annotation.Error +import io.micronaut.http.annotation.Get +import io.micronaut.http.annotation.Post +import io.micronaut.http.hateoas.JsonError +import io.micronaut.http.hateoas.Link +import org.reactivestreams.Publisher +import reactor.core.publisher.Mono +import java.util.Optional +import java.util.concurrent.CompletableFuture +import java.util.concurrent.ConcurrentHashMap +import io.micronaut.core.async.annotation.SingleResult + +@Requires(property = "spec.name", value = "PersonControllerSpec") +// tag::class[] +@Controller("/people") +class PersonController { + + internal var inMemoryDatastore: MutableMap = ConcurrentHashMap() + // end::class[] + + @Get + fun index(): Collection { + return inMemoryDatastore.values + } + + @Get("/{name}") + @SingleResult + operator fun get(name: String): Publisher { + return if (inMemoryDatastore.containsKey(name)) { + Mono.just(inMemoryDatastore[name]) + } else Mono.empty() + } + + // tag::single[] + @Post("/saveReactive") + @SingleResult + fun save(@Body person: Publisher): Publisher> { // <1> + return Mono.from(person).map { p -> + inMemoryDatastore[p.firstName] = p // <2> + HttpResponse.created(p) // <3> + } + } + // end::single[] + + // tag::args[] + @Post("/saveWithArgs") + fun save(firstName: String, lastName: String, age: Optional): HttpResponse { + val p = Person(firstName, lastName) + age.ifPresent { p.age = it } + inMemoryDatastore[p.firstName] = p + return HttpResponse.created(p) + } + // end::args[] + + // tag::future[] + @Post("/saveFuture") + fun save(@Body person: CompletableFuture): CompletableFuture> { + return person.thenApply { p -> + inMemoryDatastore[p.firstName] = p + HttpResponse.created(p) + } + } + // end::future[] + + // tag::regular[] + @Post + fun save(@Body person: Person): HttpResponse { + inMemoryDatastore[person.firstName] = person + return HttpResponse.created(person) + } + // end::regular[] + + // tag::localError[] + @Error + fun jsonError(request: HttpRequest<*>, e: JsonParseException): HttpResponse { // <1> + val error = JsonError("Invalid JSON: ${e.message}") // <2> + .link(Link.SELF, Link.of(request.uri)) + + return HttpResponse.status(HttpStatus.BAD_REQUEST, "Fix Your JSON") + .body(error) // <3> + } + // end::localError[] + + @Get("/error") + fun throwError(): String { + throw RuntimeException("Something went wrong") + } + + // tag::globalError[] + @Error(global = true) // <1> + fun error(request: HttpRequest<*>, e: Throwable): HttpResponse { + val error = JsonError("Bad Things Happened: ${e.message}") // <2> + .link(Link.SELF, Link.of(request.uri)) + + return HttpResponse.serverError() + .body(error) // <3> + } + // end::globalError[] + + // tag::statusError[] + @Error(status = HttpStatus.NOT_FOUND) + fun notFound(request: HttpRequest<*>): HttpResponse { // <1> + val error = JsonError("Person Not Found") // <2> + .link(Link.SELF, Link.of(request.uri)) + + return HttpResponse.notFound() + .body(error) // <3> + } + // end::statusError[] + + // tag::endclass[] +} +// end::endclass[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/json/PersonControllerSpec.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/json/PersonControllerSpec.kt new file mode 100644 index 00000000000..d49147448ad --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/json/PersonControllerSpec.kt @@ -0,0 +1,116 @@ +/* + * Copyright 2017-2019 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.server.json + +import io.kotest.matchers.shouldBe +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.core.spec.style.StringSpec +import io.micronaut.context.ApplicationContext +import io.micronaut.core.type.Argument +import io.micronaut.http.HttpRequest +import io.micronaut.http.HttpResponse +import io.micronaut.http.HttpStatus +import io.micronaut.http.client.HttpClient +import io.micronaut.http.client.exceptions.HttpClientResponseException +import io.micronaut.runtime.server.EmbeddedServer +import org.junit.jupiter.api.Assertions + +import org.junit.Assert.assertTrue +import reactor.core.publisher.Flux + +class PersonControllerSpec: StringSpec() { + + val embeddedServer = autoClose( + ApplicationContext.run(EmbeddedServer::class.java, mapOf("spec.name" to PersonControllerSpec::class.simpleName)) + ) + + val client = autoClose( + embeddedServer.applicationContext.createBean(HttpClient::class.java, embeddedServer.getURL()) + ) + + init { + "test global error handler"() { + val e = Assertions.assertThrows(HttpClientResponseException::class.java) { + Flux.from(client!!.exchange("/people/error", Map::class.java)) + .blockFirst() + } + val response = e.response as HttpResponse> + + response.status shouldBe HttpStatus.INTERNAL_SERVER_ERROR + response.body.get()["message"] shouldBe "Bad Things Happened: Something went wrong" + } + + "test save"() { + var response = client.toBlocking().exchange(HttpRequest.POST("/people", "{\"firstName\":\"Fred\",\"lastName\":\"Flintstone\",\"age\":45}"), Person::class.java) + var person = response.body.get() + + person.firstName shouldBe "Fred" + response.status shouldBe HttpStatus.CREATED + + response = client.toBlocking().exchange(HttpRequest.GET("/people/Fred"), Person::class.java) + person = response.body.get() + + person.firstName shouldBe "Fred" + response.status shouldBe HttpStatus.OK + } + + "test save reactive"() { + val response = client.toBlocking().exchange(HttpRequest.POST("/people/saveReactive", "{\"firstName\":\"Wilma\",\"lastName\":\"Flintstone\",\"age\":36}"), Person::class.java) + val person = response.body.get() + + person.firstName shouldBe "Wilma" + response.status shouldBe HttpStatus.CREATED + } + + "test save future"() { + val response = client!!.toBlocking().exchange(HttpRequest.POST("/people/saveFuture", "{\"firstName\":\"Pebbles\",\"lastName\":\"Flintstone\",\"age\":0}"), Person::class.java) + val person = response.body.get() + + person.firstName shouldBe "Pebbles" + response.status shouldBe HttpStatus.CREATED + } + + "test save args"() { + val response = client!!.toBlocking().exchange(HttpRequest.POST("/people/saveWithArgs", "{\"firstName\":\"Dino\",\"lastName\":\"Flintstone\",\"age\":3}"), Person::class.java) + val person = response.body.get() + + person.firstName shouldBe "Dino" + response.status shouldBe HttpStatus.CREATED + } + + "test person not found"() { + val e = shouldThrow { + Flux.from(client.exchange("/people/Sally", Map::class.java)) + .blockFirst() + } + val response = e.response as HttpResponse> + + response.body.get()["message"] shouldBe "Person Not Found" + response.status shouldBe HttpStatus.NOT_FOUND + } + + "test save invalid json"() { + val e = shouldThrow { + client.toBlocking().exchange(HttpRequest.POST("/people", "{\""), Argument.of(Person::class.java), Argument.of(Map::class.java)) + } + val response = e.response as HttpResponse> + + assertTrue(response.getBody(Map::class.java).get()["message"].toString().startsWith("Invalid JSON: Unexpected end-of-input")) + response.status shouldBe HttpStatus.BAD_REQUEST + } + } + +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/request/MessageController.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/request/MessageController.kt new file mode 100644 index 00000000000..ce3d9cbdea5 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/request/MessageController.kt @@ -0,0 +1,75 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.server.request + +// tag::imports[] +import io.micronaut.http.HttpRequest +import io.micronaut.http.HttpResponse +import io.micronaut.http.annotation.Controller +import io.micronaut.http.annotation.Get +import io.micronaut.http.context.ServerRequestContext +import reactor.core.publisher.Mono +import reactor.util.context.ContextView + +// end::imports[] + +// tag::class[] +@Controller("/request") +class MessageController { +// end::class[] + + // tag::request[] + @Get("/hello") // <1> + fun hello(request: HttpRequest<*>): HttpResponse { + val name = request.parameters + .getFirst("name") + .orElse("Nobody") // <2> + + return HttpResponse.ok("Hello $name!!") + .header("X-My-Header", "Foo") // <3> + } + // end::request[] + + // tag::static-request[] + @Get("/hello-static") // <1> + fun helloStatic(): HttpResponse { + val request: HttpRequest<*> = ServerRequestContext.currentRequest() // <1> + .orElseThrow { RuntimeException("No request present") } + val name = request.parameters + .getFirst("name") + .orElse("Nobody") + return HttpResponse.ok("Hello $name!!") + .header("X-My-Header", "Foo") + } + // end::static-request[] + + // tag::request-context[] + @Get("/hello-reactor") + fun helloReactor(): Mono?>? { + return Mono.deferContextual { ctx: ContextView -> // <1> + val request = ctx.get>(ServerRequestContext.KEY) // <2> + val name = request.parameters + .getFirst("name") + .orElse("Nobody") + Mono.just(HttpResponse.ok("Hello $name!!") + .header("X-My-Header", "Foo")) + } + } + // end::request-context[] +// tag::endclass[] +} +// end::endclass[] + diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/request/MessageControllerSpec.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/request/MessageControllerSpec.kt new file mode 100644 index 00000000000..1055fde505d --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/request/MessageControllerSpec.kt @@ -0,0 +1,39 @@ +package io.micronaut.docs.server.request + +import io.kotest.matchers.shouldNotBe +import io.kotest.matchers.shouldBe +import io.kotest.core.spec.style.StringSpec +import io.micronaut.context.ApplicationContext +import io.micronaut.http.client.HttpClient +import io.micronaut.runtime.server.EmbeddedServer + +class MessageControllerSpec: StringSpec() { + + val embeddedServer = autoClose( // <2> + ApplicationContext.run(EmbeddedServer::class.java) // <1> + ) + + val client = autoClose( // <2> + embeddedServer.applicationContext.createBean(HttpClient::class.java, embeddedServer.getURL()) // <1> + ) + + init { + "test message controller"() { + var body = client.toBlocking().retrieve("/request/hello?name=John") + + body shouldNotBe null + body shouldBe "Hello John!!" + + body = client.toBlocking().retrieve("/request/hello-static?name=John") + + body shouldNotBe null + body shouldBe "Hello John!!" + + body = client.toBlocking().retrieve("/request/hello-reactor?name=John") + + body shouldNotBe null + body shouldBe "Hello John!!" + } + } + +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/response/ProducesController.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/response/ProducesController.kt new file mode 100644 index 00000000000..2ac49b02811 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/response/ProducesController.kt @@ -0,0 +1,48 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.server.response + +//tag::imports[] +import io.micronaut.context.annotation.Requires +import io.micronaut.http.HttpResponse +import io.micronaut.http.MediaType +import io.micronaut.http.annotation.Controller +import io.micronaut.http.annotation.Get +import io.micronaut.http.annotation.Produces +//end::imports[] + +@Requires(property = "spec.name", value = "producesspec") +//tag::clazz[] +@Controller("/produces") +class ProducesController { + + @Get // <1> + fun index(): HttpResponse<*> { + return HttpResponse.ok().body("{\"msg\":\"This is JSON\"}") + } + + @Produces(MediaType.TEXT_HTML) + @Get("/html") // <2> + fun html(): String { + return "<h1>HTML</h1>" + } + + @Get(value = "/xml", produces = [MediaType.TEXT_XML]) // <3> + fun xml(): String { + return "<h1>XML</h1>" + } +} +//end::clazz[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/response/ProducesControllerSpec.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/response/ProducesControllerSpec.kt new file mode 100644 index 00000000000..357ffdd90de --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/response/ProducesControllerSpec.kt @@ -0,0 +1,37 @@ +package io.micronaut.docs.server.response + +import io.kotest.matchers.shouldBe +import io.kotest.core.spec.style.StringSpec +import io.micronaut.context.ApplicationContext +import io.micronaut.http.HttpRequest +import io.micronaut.http.MediaType +import io.micronaut.http.client.HttpClient +import io.micronaut.runtime.server.EmbeddedServer + +class ProducesControllerSpec: StringSpec() { + + val embeddedServer = autoClose( + ApplicationContext.run(EmbeddedServer::class.java, mapOf("spec.name" to "producesspec")) + ) + + val client = autoClose( + embeddedServer.applicationContext.createBean(HttpClient::class.java, embeddedServer.getURL()) + ) + + init { + "test content types"() { + var response = client.toBlocking().exchange(HttpRequest.GET("/produces"), String::class.java) + + response.contentType.get() shouldBe MediaType.APPLICATION_JSON_TYPE + + response = client.toBlocking().exchange(HttpRequest.GET("/produces/html"), String::class.java) + + response.contentType.get() shouldBe MediaType.TEXT_HTML_TYPE + + response = client.toBlocking().exchange(HttpRequest.GET("/produces/xml"), String::class.java) + + response.contentType.get() shouldBe MediaType.TEXT_XML_TYPE + } + } + +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/response/StatusController.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/response/StatusController.kt new file mode 100644 index 00000000000..408ffac1481 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/response/StatusController.kt @@ -0,0 +1,51 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.server.response + +import io.micronaut.context.annotation.Requires +import io.micronaut.http.HttpResponse +import io.micronaut.http.HttpStatus +import io.micronaut.http.MediaType +import io.micronaut.http.annotation.Controller +import io.micronaut.http.annotation.Get +import io.micronaut.http.annotation.Status + +@Requires(property = "spec.name", value = "httpstatus") +@Controller("/status") +class StatusController { + + //tag::atstatus[] + @Status(HttpStatus.CREATED) + @Get(produces = [MediaType.TEXT_PLAIN]) + fun index(): String { + return "success" + } + //end::atstatus[] + + //tag::httpstatus[] + @Get("/http-status") + fun httpStatus(): HttpStatus { + return HttpStatus.CREATED + } + //end::httpstatus[] + + //tag::httpresponse[] + @Get(value = "/http-response", produces = [MediaType.TEXT_PLAIN]) + fun httpResponse(): HttpResponse { + return HttpResponse.status(HttpStatus.CREATED).body("success") + } + //end::httpresponse[] +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/response/StatusControllerSpec.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/response/StatusControllerSpec.kt new file mode 100644 index 00000000000..91457ea2d65 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/response/StatusControllerSpec.kt @@ -0,0 +1,41 @@ +package io.micronaut.docs.server.response + +import io.kotest.matchers.shouldBe +import io.kotest.core.spec.style.StringSpec +import io.micronaut.context.ApplicationContext +import io.micronaut.http.HttpRequest +import io.micronaut.http.HttpStatus +import io.micronaut.http.client.HttpClient +import io.micronaut.runtime.server.EmbeddedServer + +class StatusControllerSpec: StringSpec() { + + val embeddedServer = autoClose( + ApplicationContext.run(EmbeddedServer::class.java, mapOf("spec.name" to "httpstatus")) + ) + + val client = autoClose( + embeddedServer.applicationContext.createBean(HttpClient::class.java, embeddedServer.getURL()) + ) + + init { + "test status"() { + var response = client.toBlocking().exchange(HttpRequest.GET("/status"), String::class.java) + var body = response.body + + response.status shouldBe HttpStatus.CREATED + body.get() shouldBe "success" + + response = client.toBlocking().exchange(HttpRequest.GET("/status/http-response"), String::class.java) + body = response.body + + response.status shouldBe HttpStatus.CREATED + body.get() shouldBe "success" + + response = client.toBlocking().exchange(HttpRequest.GET("/status/http-status"), String::class.java) + + response.status shouldBe HttpStatus.CREATED + } + } + +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/routes/IssuesController.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/routes/IssuesController.kt new file mode 100644 index 00000000000..180d62d6266 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/routes/IssuesController.kt @@ -0,0 +1,33 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.server.routes + +// tag::imports[] +import io.micronaut.http.annotation.Controller +import io.micronaut.http.annotation.Get +import io.micronaut.http.annotation.PathVariable +// end::imports[] + +// tag::class[] +@Controller("/issues") // <1> +class IssuesController { + + @Get("/{number}") // <2> + fun issue(@PathVariable number: Int): String { // <3> + return "Issue # $number!" // <4> + } +} +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/routes/IssuesControllerTest.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/routes/IssuesControllerTest.kt new file mode 100644 index 00000000000..712ad1fe04c --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/routes/IssuesControllerTest.kt @@ -0,0 +1,52 @@ +package io.micronaut.docs.server.routes + +// tag::imports[] +import io.kotest.matchers.shouldBe +import io.kotest.matchers.shouldNotBe +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.core.spec.style.StringSpec +import io.micronaut.context.ApplicationContext +import io.micronaut.http.client.HttpClient +import io.micronaut.http.client.exceptions.HttpClientResponseException +import io.micronaut.runtime.server.EmbeddedServer +// end::imports[] + +// tag::class[] +class IssuesControllerTest: StringSpec() { + + val embeddedServer = autoClose( // <2> + ApplicationContext.run(EmbeddedServer::class.java) // <1> + ) + + val client = autoClose( // <2> + embeddedServer.applicationContext.createBean( + HttpClient::class.java, + embeddedServer.url) // <1> + ) + + init { + "test issue" { + val body = client.toBlocking().retrieve("/issues/12") // <3> + + body shouldNotBe null + body shouldBe "Issue # 12!" // <4> + } + + "test issue with invalid integer" { + val e = shouldThrow { + client.toBlocking().exchange("/issues/hello") + } + + e.status.code shouldBe 400 // <5> + } + + "test issue without number" { + val e = shouldThrow { + client.toBlocking().exchange("/issues/") + } + + e.status.code shouldBe 404 // <6> + } + } +} +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/routes/MyRoutes.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/routes/MyRoutes.kt new file mode 100644 index 00000000000..d102bda222b --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/routes/MyRoutes.kt @@ -0,0 +1,37 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.server.routes + +// tag::imports[] +import io.micronaut.context.ExecutionHandleLocator +import io.micronaut.web.router.DefaultRouteBuilder +import io.micronaut.web.router.RouteBuilder +import jakarta.inject.Inject +import jakarta.inject.Singleton +// end::imports[] + +// tag::class[] +@Singleton +class MyRoutes(executionHandleLocator: ExecutionHandleLocator, + uriNamingStrategy: RouteBuilder.UriNamingStrategy) : + DefaultRouteBuilder(executionHandleLocator, uriNamingStrategy) { // <1> + + @Inject + fun issuesRoutes(issuesController: IssuesController) { // <2> + GET("/issues/show/{number}", issuesController, "issue", Int::class.java) // <3> + } +} +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/routes/MyRoutesSpec.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/routes/MyRoutesSpec.kt new file mode 100644 index 00000000000..00868c82e0f --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/routes/MyRoutesSpec.kt @@ -0,0 +1,28 @@ +package io.micronaut.docs.server.routes + +import io.kotest.matchers.shouldBe +import io.kotest.matchers.shouldNotBe +import io.kotest.core.spec.style.StringSpec +import io.micronaut.context.ApplicationContext +import io.micronaut.http.client.HttpClient +import io.micronaut.runtime.server.EmbeddedServer + +class MyRoutesSpec: StringSpec() { + + val embeddedServer = autoClose( // <2> + ApplicationContext.run(EmbeddedServer::class.java) // <1> + ) + + val client = autoClose( // <2> + embeddedServer.applicationContext.createBean(HttpClient::class.java, embeddedServer.getURL()) // <1> + ) + + init { + "test custom route" { + val body = client.toBlocking().retrieve("/issues/show/12") // <3> + + body shouldNotBe null + body shouldBe "Issue # 12!" // <4> + } + } +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/routing/BackwardCompatibleController.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/routing/BackwardCompatibleController.kt new file mode 100644 index 00000000000..7f815b58ea6 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/routing/BackwardCompatibleController.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.server.routing + +import io.micronaut.context.annotation.Requires +// tag::imports[] +import io.micronaut.http.annotation.Controller +import io.micronaut.http.annotation.Get +// end::imports[] + +@Requires(property = "spec.name", value = "BackwardCompatibleControllerSpec") +// tag::class[] +@Controller("/hello") +class BackwardCompatibleController { + + @Get(uris = ["/{name}", "/person/{name}"]) // <1> + fun hello(name: String): String { // <2> + return "Hello, $name" + } +} +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/routing/BackwardCompatibleControllerSpec.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/routing/BackwardCompatibleControllerSpec.kt new file mode 100644 index 00000000000..55cc8bd85c1 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/routing/BackwardCompatibleControllerSpec.kt @@ -0,0 +1,46 @@ +/* + * Copyright 2017-2019 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.server.routing + +import io.micronaut.context.annotation.Property +import io.micronaut.http.HttpRequest +import io.micronaut.http.client.HttpClient +import io.micronaut.http.client.annotation.Client +import io.micronaut.test.extensions.junit5.annotation.MicronautTest +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import jakarta.inject.Inject + +@Property(name = "spec.name", value = "BackwardCompatibleControllerSpec") +@MicronautTest +class BackwardCompatibleControllerSpec { + + @Inject + @field:Client("/") + lateinit var client: HttpClient + + @Test + fun testHelloWorldResponse() { + var response = client.toBlocking() + .retrieve(HttpRequest.GET("/hello/World")) + assertEquals("Hello, World", response) + + response = client.toBlocking() + .retrieve(HttpRequest.GET("/hello/person/John")) + + assertEquals("Hello, John", response) + } +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/sse/Headline.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/sse/Headline.kt new file mode 100644 index 00000000000..465593a6ddf --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/sse/Headline.kt @@ -0,0 +1,31 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.server.sse + +// tag::class[] +class Headline { + + var title: String? = null + var description: String? = null + + constructor() + + constructor(title: String, description: String) { + this.title = title + this.description = description + } +} +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/sse/HeadlineController.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/sse/HeadlineController.kt new file mode 100644 index 00000000000..d77abf1e5da --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/sse/HeadlineController.kt @@ -0,0 +1,59 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.server.sse + +// tag::imports[] +import io.micronaut.http.MediaType +import io.micronaut.http.annotation.Controller +import io.micronaut.http.annotation.Get +import io.micronaut.http.sse.Event +import io.micronaut.scheduling.TaskExecutors +import io.micronaut.scheduling.annotation.ExecuteOn +import org.reactivestreams.Publisher +import reactor.core.publisher.Flux +import reactor.core.publisher.SynchronousSink +import java.util.concurrent.Callable +import java.util.function.BiFunction + +// end::imports[] + +// tag::class[] +@Controller("/headlines") +class HeadlineController { + + @ExecuteOn(TaskExecutors.IO) + @Get(produces = [MediaType.TEXT_EVENT_STREAM]) + fun index(): Publisher> { // <1> + val versions = arrayOf("1.0", "2.0") // <2> + return Flux.generate( + { 0 }, + BiFunction { i: Int, emitter: SynchronousSink> -> // <3> + if (i < versions.size) { + emitter.next( // <4> + Event.of( + Headline( + "Micronaut " + versions[i] + " Released", "Come and get it" + ) + ) + ) + } else { + emitter.complete() // <5> + } + return@BiFunction i + 1 + }) + } +} +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/sse/HeadlineControllerSpec.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/sse/HeadlineControllerSpec.kt new file mode 100644 index 00000000000..76118b4d950 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/sse/HeadlineControllerSpec.kt @@ -0,0 +1,43 @@ +package io.micronaut.docs.server.sse + +import io.kotest.assertions.timing.eventually +import io.kotest.matchers.shouldBe +import io.kotest.core.spec.style.StringSpec +import io.micronaut.context.ApplicationContext +import io.micronaut.http.HttpRequest +import io.micronaut.http.client.sse.SseClient +import io.micronaut.http.sse.Event +import io.micronaut.runtime.server.EmbeddedServer +import org.opentest4j.AssertionFailedError +import reactor.core.publisher.Flux + +import java.util.ArrayList +import kotlin.time.DurationUnit +import kotlin.time.ExperimentalTime +import kotlin.time.toDuration + +@ExperimentalTime +class HeadlineControllerSpec: StringSpec() { + + val embeddedServer = autoClose( + ApplicationContext.run(EmbeddedServer::class.java) + ) + + init { + "test consume eventstream object" { + val client = embeddedServer.applicationContext.createBean(SseClient::class.java, embeddedServer.url) + + val events = ArrayList>() + + Flux.from(client.eventStream(HttpRequest.GET("/headlines"), Headline::class.java)).subscribe { + events.add(it) + } + + eventually(2.toDuration(DurationUnit.SECONDS), AssertionFailedError::class) { + events.size shouldBe 2 + events[0].data.title shouldBe "Micronaut 1.0 Released" + events[0].data.description shouldBe "Come and get it" + } + } + } +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/suspend/MyContext.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/suspend/MyContext.kt new file mode 100644 index 00000000000..f3b95b50442 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/suspend/MyContext.kt @@ -0,0 +1,26 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.server.suspend + +import kotlin.coroutines.CoroutineContext + +class MyContext(val value: String) : CoroutineContext.Element { + + companion object Key : CoroutineContext.Key + + override val key: CoroutineContext.Key get() = Key + +} \ No newline at end of file diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/suspend/MyContextInterceptorAnn.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/suspend/MyContextInterceptorAnn.kt new file mode 100644 index 00000000000..18c1c5b38fe --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/suspend/MyContextInterceptorAnn.kt @@ -0,0 +1,30 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.server.suspend + +import io.micronaut.aop.Around +import kotlin.annotation.AnnotationRetention.RUNTIME +import kotlin.annotation.AnnotationTarget.CLASS +import kotlin.annotation.AnnotationTarget.FILE +import kotlin.annotation.AnnotationTarget.FUNCTION +import kotlin.annotation.AnnotationTarget.PROPERTY_GETTER +import kotlin.annotation.AnnotationTarget.PROPERTY_SETTER + +@MustBeDocumented +@Retention(RUNTIME) +@Target(CLASS, FILE, FUNCTION, PROPERTY_GETTER, PROPERTY_SETTER) +@Around +annotation class MyContextInterceptorAnn diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/suspend/Repository.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/suspend/Repository.kt new file mode 100644 index 00000000000..2ac1a09fa16 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/suspend/Repository.kt @@ -0,0 +1,25 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.server.suspend + +import io.micronaut.aop.Introduction +import jakarta.inject.Singleton + +@MustBeDocumented +@kotlin.annotation.Retention(AnnotationRetention.RUNTIME) +@Introduction +@Singleton +annotation class Repository() diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/suspend/SuspendClient.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/suspend/SuspendClient.kt new file mode 100644 index 00000000000..e68c8cf424d --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/suspend/SuspendClient.kt @@ -0,0 +1,47 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.server.suspend + +import io.micronaut.http.HttpResponse +import io.micronaut.http.MediaType +import io.micronaut.http.annotation.Get +import io.micronaut.http.client.annotation.Client + +@Client("/suspend") +interface SuspendClient { + + @Get("/simple", consumes = [MediaType.TEXT_PLAIN]) + suspend fun simple(): String + + @Get("/simple", consumes = [MediaType.TEXT_PLAIN]) + suspend fun simpleIgnoreResult() + + @Get("/simple", consumes = [MediaType.TEXT_PLAIN]) + suspend fun simpleResponse(): HttpResponse + + @Get("/simple", consumes = [MediaType.TEXT_PLAIN]) + suspend fun simpleResponseIgnoreResult(): HttpResponse + + @Get("/delayed", consumes = [MediaType.TEXT_PLAIN]) + suspend fun delayed(): String + + @Get("/illegal", consumes = [MediaType.ALL]) + suspend fun errorCall(): String + + @Get("/illegal", consumes = [MediaType.ALL]) + suspend fun errorCallResponse(): HttpResponse + +} \ No newline at end of file diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/suspend/SuspendController.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/suspend/SuspendController.kt new file mode 100644 index 00000000000..7900fc5cb3f --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/suspend/SuspendController.kt @@ -0,0 +1,189 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.server.suspend + +import io.micronaut.http.* +import io.micronaut.http.annotation.* +import io.micronaut.http.bind.binders.HttpCoroutineContextFactory +import io.micronaut.http.context.ServerRequestContext +import io.micronaut.scheduling.TaskExecutors +import io.micronaut.tracing.instrument.kotlin.CoroutineTracingDispatcher +import kotlinx.coroutines.* +import java.util.concurrent.ExecutorService +import java.util.concurrent.atomic.AtomicInteger +import jakarta.inject.Named +import org.slf4j.MDC + +@Controller("/suspend") +class SuspendController( + @Named(TaskExecutors.IO) private val executor: ExecutorService, + private val suspendService: SuspendService, + private val suspendRequestScopedService: SuspendRequestScopedService, + private val coroutineTracingDispatcherFactory: HttpCoroutineContextFactory +) { + + private val coroutineDispatcher: CoroutineDispatcher + + init { + coroutineDispatcher = executor.asCoroutineDispatcher() + } + + // tag::suspend[] + @Get("/simple", produces = [MediaType.TEXT_PLAIN]) + suspend fun simple(): String { // <1> + return "Hello" + } + // end::suspend[] + + // tag::suspendDelayed[] + @Get("/delayed", produces = [MediaType.TEXT_PLAIN]) + suspend fun delayed(): String { // <1> + delay(1) // <2> + return "Delayed" + } + // end::suspendDelayed[] + + // tag::suspendStatus[] + @Status(HttpStatus.CREATED) // <1> + @Get("/status") + suspend fun status() { + } + // end::suspendStatus[] + + // tag::suspendStatusDelayed[] + @Status(HttpStatus.CREATED) + @Get("/statusDelayed") + suspend fun statusDelayed() { + delay(1) + } + // end::suspendStatusDelayed[] + + val count = AtomicInteger(0) + + @Get("/count") + suspend fun count(): Int { // <1> + return count.incrementAndGet() + } + + @Get("/greet") + suspend fun suspendingGreet(name: String, request: HttpRequest): HttpResponse { + val json = "{\"message\":\"hello\"}" + return HttpResponse.ok(json).header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON) + } + + @Get("/illegal") + suspend fun illegal() { + throw IllegalArgumentException() + } + + @Get("/illegalWithContext") + suspend fun illegalWithContext(): String = withContext(coroutineDispatcher) { + throw IllegalArgumentException() + } + + @Status(HttpStatus.BAD_REQUEST) + @Error(exception = IllegalArgumentException::class) + @Produces(MediaType.TEXT_PLAIN) + suspend fun onIllegalArgument(e: IllegalArgumentException): String { + return "illegal.argument" + } + + @Get("/callSuspendServiceWithRetries") + suspend fun callSuspendServiceWithRetries(): String { + return suspendService.delayedCalculation1() + } + + @Get("/callSuspendServiceWithRetriesBlocked") + fun callSuspendServiceWithRetriesBlocked(): String { + // Bypass ContinuationArgumentBinder + return runBlocking { + suspendService.delayedCalculation2() + } + } + + @Get("/callSuspendServiceWithRetriesWithoutDelay") + suspend fun callSuspendServiceWithRetriesWithoutDelay(): String { + return suspendService.calculation3() + } + + @Get("/keepRequestScopeInsideCoroutine") + suspend fun keepRequestScopeInsideCoroutine() = coroutineScope { + val before = "${suspendRequestScopedService.requestId},${Thread.currentThread().id}" + val after = async { "${suspendRequestScopedService.requestId},${Thread.currentThread().id}" }.await() + "$before,$after" + } + + @Get("/keepRequestScopeInsideCoroutineWithRetry") + suspend fun keepRequestScopeInsideCoroutineWithRetry() = coroutineScope { + val before = "${suspendRequestScopedService.requestId},${Thread.currentThread().id}" + val after = async { suspendService.requestScopedCalculation() }.await() + "$before,$after" + } + + @Get("/keepRequestScopeAfterSuspend") + suspend fun keepRequestScopeAfterSuspend(): String { + val before = "${suspendRequestScopedService.requestId},${Thread.currentThread().id}" + delay(10) // suspend + val after = "${suspendRequestScopedService.requestId},${Thread.currentThread().id}" + return "$before,$after" + } + + @Get("/requestContext") + suspend fun requestContext(): String { + return suspendService.requestContext() + } + + @Get("/requestContext2") + suspend fun requestContext2(): String = supervisorScope { + require(ServerRequestContext.currentRequest().isPresent) { + "Initial request is not set" + } + val result = withContext(coroutineContext) { + require(ServerRequestContext.currentRequest().isPresent) { + "Request is not available in `withContext`" + } + "test" + } + require(ServerRequestContext.currentRequest().isPresent) { + "Request is lost after `withContext`" + } + result + } + + @Get("/keepTracingContextAfterDelay") + suspend fun keepTracingContextAfterDelay() = coroutineScope { + val before = currentTraceId() + delay(1L) + val after = currentTraceId() + "$before,$after" + } + + @Get("/keepTracingContextInsideCoroutine") + suspend fun keepTracingContextInsideCoroutine() = coroutineScope { + val before = currentTraceId() + val after = withContext(Dispatchers.Default) { currentTraceId() } + "$before,$after" + } + + @Get("/keepTracingContextUsingCoroutineTracingDispatcherExplicitly") + fun keepTracingContextUsingCoroutineTracingDispatcherExplicitly() = runBlocking { + val before = currentTraceId() + val after = withContext(Dispatchers.Default + coroutineTracingDispatcherFactory.create()) { currentTraceId() } + "$before,$after" + } + + private fun currentTraceId(): String? = MDC.get("traceId") +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/suspend/SuspendControllerSpec.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/suspend/SuspendControllerSpec.kt new file mode 100644 index 00000000000..8d42b81d188 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/suspend/SuspendControllerSpec.kt @@ -0,0 +1,272 @@ +/* + * Copyright 2017-2019 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.server.suspend + +import io.kotest.matchers.shouldBe +import io.kotest.matchers.shouldNotBe +import io.kotest.assertions.throwables.shouldThrowExactly +import io.kotest.core.spec.style.StringSpec +import io.kotest.matchers.should +import io.micronaut.context.ApplicationContext +import io.micronaut.http.HttpHeaders.* +import io.micronaut.http.HttpMethod +import io.micronaut.http.HttpRequest.GET +import io.micronaut.http.HttpRequest.OPTIONS +import io.micronaut.http.HttpStatus +import io.micronaut.http.client.HttpClient +import io.micronaut.http.client.exceptions.HttpClientResponseException +import io.micronaut.runtime.server.EmbeddedServer +import kotlinx.coroutines.reactive.awaitSingle + +class SuspendControllerSpec : StringSpec() { + + val embeddedServer = autoClose( + ApplicationContext.run( + EmbeddedServer::class.java, mapOf( + "micronaut.server.cors.enabled" to true, + "micronaut.server.cors.configurations.dev.allowedOrigins" to listOf("foo.com"), + "micronaut.server.cors.configurations.dev.allowedMethods" to listOf("GET"), + "micronaut.server.cors.configurations.dev.allowedHeaders" to listOf(ACCEPT, CONTENT_TYPE), + "tracing.zipkin.enabled" to true + ) + ) + ) + + val client = autoClose( + embeddedServer.applicationContext.createBean(HttpClient::class.java, embeddedServer.url) + ) + + private var suspendClient = embeddedServer.applicationContext.createBean(SuspendClient::class.java, embeddedServer.url) + + init { + + "test suspend applies CORS options" { + val origin = "foo.com" + val headers = "$CONTENT_TYPE,$ACCEPT" + val method = HttpMethod.GET + val optionsResponse = client.exchange( + OPTIONS("/suspend/greet") + .header(ORIGIN, origin) + .header(ACCESS_CONTROL_REQUEST_METHOD, method) + .header(ACCESS_CONTROL_REQUEST_HEADERS, headers) + ).awaitSingle() + + optionsResponse.status shouldBe HttpStatus.OK + optionsResponse.header(ACCESS_CONTROL_ALLOW_ORIGIN) shouldBe origin + optionsResponse.header(ACCESS_CONTROL_ALLOW_METHODS) shouldBe method.toString() + optionsResponse.headers.getAll(ACCESS_CONTROL_ALLOW_HEADERS).joinToString(",") shouldBe headers + + val response = client.exchange( + GET("/suspend/greet?name=Fred") + .header(ORIGIN, origin) + ).awaitSingle() + + response.status shouldBe HttpStatus.OK + response.header(ACCESS_CONTROL_ALLOW_ORIGIN) shouldBe origin + } + + "test suspend service with retries" { + val response = client.exchange(GET("/suspend/callSuspendServiceWithRetries"), String::class.java).awaitSingle() + val body = response.body.get() + + body shouldBe "delayedCalculation1" + response.status shouldBe HttpStatus.OK + } + + "test suspend service with retries blocked" { + val response = client.exchange(GET("/suspend/callSuspendServiceWithRetriesBlocked"), String::class.java).awaitSingle() + val body = response.body.get() + + body shouldBe "delayedCalculation2" + response.status shouldBe HttpStatus.OK + } + + "test suspend service with retries without delay" { + val response = client.exchange(GET("/suspend/callSuspendServiceWithRetriesWithoutDelay"), String::class.java).awaitSingle() + val body = response.body.get() + + body shouldBe "delayedCalculation3" + response.status shouldBe HttpStatus.OK + } + + "test suspend" { + val response = client.exchange(GET("/suspend/simple"), String::class.java).awaitSingle() + val body = response.body.get() + + body shouldBe "Hello" + response.status shouldBe HttpStatus.OK + } + + "test suspend calling client" { + val body = suspendClient.simple() + + body shouldBe "Hello" + } + + "test suspend calling client ignore result" { + suspendClient.simpleIgnoreResult() + // No exception thrown + } + + "test suspend calling client method with response return" { + val response = suspendClient.simpleResponse() + val body = response.body.get() + + body shouldBe "Hello" + response.status shouldBe HttpStatus.OK + } + + "test suspend calling client method with response return ignore result" { + val response = suspendClient.simpleResponse() + val body = response.body.get() + + body shouldBe "Hello" + response.status shouldBe HttpStatus.OK + } + + "test suspend delayed" { + val response = client.exchange(GET("/suspend/delayed"), String::class.java).awaitSingle() + val body = response.body.get() + + body shouldBe "Delayed" + response.status shouldBe HttpStatus.OK + } + + "test suspend status" { + val response = client.exchange(GET("/suspend/status"), String::class.java).awaitSingle() + + response.status shouldBe HttpStatus.CREATED + } + + "test suspend status delayed" { + val response = client.exchange(GET("/suspend/statusDelayed"), String::class.java).awaitSingle() + + response.status shouldBe HttpStatus.CREATED + } + + "test suspend invoked once" { + val response = client.exchange(GET("/suspend/count"), Integer::class.java).awaitSingle() + val body = response.body.get() + + body shouldBe 1 + response.status shouldBe HttpStatus.OK + } + + "test error route" { + val ex = shouldThrowExactly { + client.exchange(GET("/suspend/illegal"), String::class.java).awaitSingle() + } + val body = ex.response.getBody(String::class.java).get() + + ex.status shouldBe HttpStatus.BAD_REQUEST + body shouldBe "illegal.argument" + } + + "test error route with client response" { + val ex = shouldThrowExactly { + suspendClient.errorCallResponse() + } + val body = ex.response.getBody(String::class.java).get() + + ex.status shouldBe HttpStatus.BAD_REQUEST + body shouldBe "illegal.argument" + } + + "test error route with client string response" { + val ex = shouldThrowExactly { + suspendClient.errorCall() + } + val body = ex.response.getBody(String::class.java).get() + + ex.status shouldBe HttpStatus.BAD_REQUEST + body shouldBe "illegal.argument" + } + + "test suspend functions that throw exceptions inside withContext emit an error response to filters" { + val ex = shouldThrowExactly { + client.exchange(GET("/suspend/illegalWithContext"), String::class.java).awaitSingle() + } + val body = ex.response.getBody(String::class.java).get() + val filter = embeddedServer.applicationContext.getBean(SuspendFilter::class.java) + + ex.status shouldBe HttpStatus.BAD_REQUEST + body shouldBe "illegal.argument" + filter.response.status shouldBe HttpStatus.BAD_REQUEST + filter.error should { t -> t is IllegalArgumentException } + } + + "test keeping request scope inside coroutine" { + val response = client.exchange(GET("/suspend/keepRequestScopeInsideCoroutine"), String::class.java).awaitSingle() + val body = response.body.get() + + val (beforeRequestId, beforeThreadId, afterRequestId, afterThreadId) = body.split(',') + beforeRequestId shouldBe afterRequestId + beforeThreadId shouldNotBe afterThreadId + response.status shouldBe HttpStatus.OK + } + + "test keeping request scope after a suspend" { + val response = client.exchange(GET("/suspend/keepRequestScopeAfterSuspend"), String::class.java).awaitSingle() + val body = response.body.get() + val (beforeRequestId, beforeThreadId, afterRequestId, afterThreadId) = body.split(',') + beforeRequestId shouldBe afterRequestId + beforeThreadId shouldNotBe afterThreadId // it will be the default co-routine dispatcher + response.status shouldBe HttpStatus.OK + } + + "test request context is available" { + val response = client.exchange(GET("/suspend/requestContext"), String::class.java).awaitSingle() + val body = response.body.get() + body shouldBe "/suspend/requestContext" + response.status shouldBe HttpStatus.OK + } + + "test request context is available2" { + val response = client.exchange(GET("/suspend/requestContext2"), String::class.java).awaitSingle() + val body = response.body.get() + body shouldBe "test" + response.status shouldBe HttpStatus.OK + } + + "test keeping tracing context after delay" { + val response = client.exchange(GET("/suspend/keepTracingContextAfterDelay"), String::class.java).awaitSingle() + val body = response.body.get() + + val (beforeTraceId, afterTraceId) = body.split(',') + beforeTraceId shouldBe afterTraceId + response.status shouldBe HttpStatus.OK + } + + "test keeping tracing context inside coroutine" { + val response = client.exchange(GET("/suspend/keepTracingContextInsideCoroutine"), String::class.java).awaitSingle() + val body = response.body.get() + + val (beforeTraceId, afterTraceId) = body.split(',') + beforeTraceId shouldBe afterTraceId + response.status shouldBe HttpStatus.OK + } + +// TODO: HttpCoroutineTracingDispatcherFactory#create should eliminate nulls +// "test keeping tracing context using CoroutineTracingDispatcher explicitly" { +// val response = client.exchange(GET("/suspend/keepTracingContextUsingCoroutineTracingDispatcherExplicitly"), String::class.java).awaitSingle() +// val body = response.body.get() +// +// val (beforeTraceId, afterTraceId) = body.split(',') +// beforeTraceId shouldBe afterTraceId +// response.status shouldBe HttpStatus.OK +// } + } +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/suspend/SuspendFilter.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/suspend/SuspendFilter.kt new file mode 100644 index 00000000000..eefc42b8c2a --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/suspend/SuspendFilter.kt @@ -0,0 +1,24 @@ +package io.micronaut.docs.server.suspend + +import io.micronaut.http.HttpAttributes +import io.micronaut.http.HttpRequest +import io.micronaut.http.MutableHttpResponse +import io.micronaut.http.annotation.Filter +import io.micronaut.http.filter.OncePerRequestHttpServerFilter +import io.micronaut.http.filter.ServerFilterChain +import org.reactivestreams.Publisher +import reactor.core.publisher.Flux + +@Filter("/suspend/illegalWithContext") +class SuspendFilter : OncePerRequestHttpServerFilter() { + + lateinit var response: MutableHttpResponse<*> + var error: Throwable? = null + + override fun doFilterOnce(request: HttpRequest<*>, chain: ServerFilterChain): Publisher> { + return Flux.from(chain.proceed(request)).doOnNext { rsp -> + response = rsp + error = rsp.getAttribute(HttpAttributes.EXCEPTION, Throwable::class.java).orElse(null) + } + } +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/suspend/SuspendInterceptor.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/suspend/SuspendInterceptor.kt new file mode 100644 index 00000000000..b22e96d20fb --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/suspend/SuspendInterceptor.kt @@ -0,0 +1,49 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.server.suspend + +import io.micronaut.aop.InterceptedMethod +import io.micronaut.aop.InterceptorBean +import io.micronaut.aop.MethodInterceptor +import io.micronaut.aop.MethodInvocationContext +import io.micronaut.aop.kotlin.KotlinInterceptedMethod +import jakarta.inject.Singleton + +@InterceptorBean(MyContextInterceptorAnn::class) +@Singleton +class SuspendInterceptor : MethodInterceptor { + override fun intercept(context: MethodInvocationContext): Any? { + val interceptedMethod = InterceptedMethod.of(context) + return try { + return if (interceptedMethod.resultType() == InterceptedMethod.ResultType.COMPLETION_STAGE) { + if (interceptedMethod is KotlinInterceptedMethod && interceptedMethod.coroutineContext != null) { + val existingContext = interceptedMethod.coroutineContext[MyContext] + if (existingContext == null) { + interceptedMethod.updateCoroutineContext(interceptedMethod.coroutineContext + MyContext(context.methodName)) + } + } + + interceptedMethod.handleResult( + interceptedMethod.interceptResultAsCompletionStage() + ) + } else { + context.proceed() + } + } catch (e: Exception) { + interceptedMethod.handleException(e) + } + } +} \ No newline at end of file diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/suspend/SuspendRepository.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/suspend/SuspendRepository.kt new file mode 100644 index 00000000000..62166d34779 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/suspend/SuspendRepository.kt @@ -0,0 +1,24 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.server.suspend + + +@Repository +interface SuspendRepository { + + suspend fun get(): String + +} \ No newline at end of file diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/suspend/SuspendRepositoryInterceptor.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/suspend/SuspendRepositoryInterceptor.kt new file mode 100644 index 00000000000..c28ad8a10aa --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/suspend/SuspendRepositoryInterceptor.kt @@ -0,0 +1,46 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.server.suspend + +import io.micronaut.aop.InterceptedMethod +import io.micronaut.aop.InterceptorBean +import io.micronaut.aop.MethodInterceptor +import io.micronaut.aop.MethodInvocationContext +import jakarta.inject.Singleton +import java.io.IOException +import java.util.concurrent.CompletableFuture + +@InterceptorBean(Repository::class) +@Singleton +class SuspendRepositoryInterceptor : MethodInterceptor { + override fun intercept(context: MethodInvocationContext?): Any? { + val interceptedMethod = InterceptedMethod.of(context) + return try { + if (interceptedMethod.resultType() == InterceptedMethod.ResultType.COMPLETION_STAGE) { + var cf = CompletableFuture() + cf.complete("hello") + cf = cf.thenApply { + throw IOException() + } + interceptedMethod.handleResult(cf) + } else { + context?.proceed() + } + } catch (e: Exception) { + interceptedMethod.handleException(e) + } + } +} \ No newline at end of file diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/suspend/SuspendRepositorySpec.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/suspend/SuspendRepositorySpec.kt new file mode 100644 index 00000000000..5850fdc7ab1 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/suspend/SuspendRepositorySpec.kt @@ -0,0 +1,51 @@ +/* + * Copyright 2017-2019 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.server.suspend + +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.matchers.shouldBe +import io.kotest.matchers.shouldNotBe +import io.kotest.assertions.throwables.shouldThrowExactly +import io.kotest.core.spec.style.StringSpec +import io.kotest.matchers.should +import io.micronaut.context.ApplicationContext +import io.micronaut.http.HttpHeaders.* +import io.micronaut.http.HttpMethod +import io.micronaut.http.HttpRequest.GET +import io.micronaut.http.HttpRequest.OPTIONS +import io.micronaut.http.HttpStatus +import io.micronaut.http.client.HttpClient +import io.micronaut.http.client.exceptions.HttpClientResponseException +import io.micronaut.runtime.server.EmbeddedServer +import kotlinx.coroutines.reactive.awaitSingle +import java.io.IOException + +class SuspendRepositorySpec : StringSpec() { + + val context = autoClose( + ApplicationContext.run() + ) + + private var suspendRepository = context.getBean(SuspendRepository::class.java) + + init { + "test exception unwrapped" { + shouldThrow { + suspendRepository.get() + } + } + } +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/suspend/SuspendRequestScopedService.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/suspend/SuspendRequestScopedService.kt new file mode 100644 index 00000000000..5beff87fc89 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/suspend/SuspendRequestScopedService.kt @@ -0,0 +1,9 @@ +package io.micronaut.docs.server.suspend + +import io.micronaut.runtime.http.scope.RequestScope +import java.util.* + +@RequestScope +open class SuspendRequestScopedService { + open val requestId = UUID.randomUUID().toString() +} \ No newline at end of file diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/suspend/SuspendService.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/suspend/SuspendService.kt new file mode 100644 index 00000000000..e332f5b8ec5 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/suspend/SuspendService.kt @@ -0,0 +1,90 @@ +package io.micronaut.docs.server.suspend + +import io.micronaut.http.HttpRequest +import io.micronaut.http.context.ServerRequestContext +import io.micronaut.retry.annotation.Retryable +import kotlinx.coroutines.delay +import jakarta.inject.Singleton +import kotlin.coroutines.coroutineContext + +@Singleton +open class SuspendService( + private val suspendRequestScopedService: SuspendRequestScopedService +) { + var counter1: Int = 0 + var counter2: Int = 0 + var counter3: Int = 0 + var counter4: Int = 0 + + @Retryable + open suspend fun delayedCalculation1(): String { + if (counter1 != 2) { + delay(1) + counter1++ + throw RuntimeException("error $counter1") + } + delay(1) + return "delayedCalculation1" + } + + @Retryable + open suspend fun delayedCalculation2(): String { + if (counter2 != 2) { + delay(1) + counter2++ + throw RuntimeException("error $counter2") + } + delay(1) + return "delayedCalculation2" + } + + @Retryable + open suspend fun calculation3(): String { + if (counter3 != 2) { + counter3++ + throw RuntimeException("error $counter3") + } + return "delayedCalculation3" + } + + @Retryable + open suspend fun requestScopedCalculation(): String { + if (counter4 != 2) { + counter4++ + throw RuntimeException("error $counter4") + } + return "${suspendRequestScopedService.requestId},${Thread.currentThread().id}" + } + + suspend fun requestContext(): String { + delay(1) + // called from a suspend controller function + val currentRequest = ServerRequestContext.currentRequest>().orElseGet { + error("Expected a current http server request") + } + return currentRequest.path + } + + suspend fun findMyContextValue(): String? { + return coroutineContext[MyContext]?.value + } + + @MyContextInterceptorAnn + open suspend fun call1(): String? { + return findMyContextValue() + } + + @MyContextInterceptorAnn + open suspend fun call2(): String? { + return call1() + } + + open suspend fun call3(): String? { + return call1() + } + + @MyContextInterceptorAnn + open suspend fun call4(): String? { + return call3() + } +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/suspend/SuspendServiceInterceptorSpec.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/suspend/SuspendServiceInterceptorSpec.kt new file mode 100644 index 00000000000..3ac9ac0d393 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/suspend/SuspendServiceInterceptorSpec.kt @@ -0,0 +1,40 @@ +/* + * Copyright 2017-2019 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.server.suspend + +import io.kotest.core.spec.style.StringSpec +import io.kotest.matchers.shouldBe +import io.micronaut.context.ApplicationContext + +class SuspendServiceInterceptorSpec : StringSpec() { + + val context = autoClose( + ApplicationContext.run() + ) + + private var suspendService = context.getBean(SuspendService::class.java) + + init { + "should append to context " { + coroutineContext[MyContext] shouldBe null + + suspendService.call1() shouldBe "call1" + suspendService.call2() shouldBe "call2" + suspendService.call3() shouldBe "call1" + suspendService.call4() shouldBe "call4" + } + } +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/suspend/multiple/CoroutineCrudRepository.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/suspend/multiple/CoroutineCrudRepository.kt new file mode 100644 index 00000000000..46583c3a2c0 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/suspend/multiple/CoroutineCrudRepository.kt @@ -0,0 +1,101 @@ +package io.micronaut.docs.server.suspend.multiple + +import kotlinx.coroutines.flow.Flow + +interface CoroutineCrudRepository { + + /** + * Saves the given valid entity, returning a possibly new entity representing the saved state. Note that certain implementations may not be able to detect whether a save or update should be performed and may always perform an insert. The [.update] method can be used in this case to explicitly request an update. + * + * @param entity The entity to save. Must not be null. + * @return The saved entity will never be null. + * @param The generic type + */ + suspend fun save(entity: S): S + + /** + * This method issues an explicit update for the given entity. The method differs from [.save] in that an update will be generated regardless if the entity has been saved previously or not. If the entity has no assigned ID then an exception will be thrown. + * + * @param entity The entity to save. Must not be null. + * @return The updated entity will never be null. + * @param The generic type + */ + suspend fun update(entity: S): S + + /** + * This method issues an explicit update for the given entities. The method differs from [.saveAll] in that an update will be generated regardless if the entity has been saved previously or not. If the entity has no assigned ID then an exception will be thrown. + * + * @param entities The entities to update. Must not be null. + * @return The updated entities will never be null. + * @param The generic type + */ + fun updateAll(entities: Iterable): Flow + + /** + * Saves all given entities, possibly returning new instances representing the saved state. + * + * @param entities The entities to saved. Must not be null. + * @param The generic type + * @return The saved entities objects. will never be null. + */ + fun saveAll(entities: Iterable): Flow + + /** + * Retrieves an entity by its id. + * + * @param id The ID of the entity to retrieve. Must not be null. + * @return the entity with the given id or none. + */ + suspend fun findById(id: ID): E? + + /** + * Returns whether an entity with the given id exists. + * + * @param id must not be null. + * @return true if an entity with the given id exists, false otherwise. + */ + suspend fun existsById(id: ID): Boolean + + /** + * Returns all instances of the type. + * + * @return all entities + */ + fun findAll(): Flow + + /** + * Returns the number of entities available. + * + * @return the number of entities + */ + suspend fun count(): Long + + /** + * Deletes the entity with the given id. + * + * @param id the id. + */ + suspend fun deleteById(id: ID): Int + + /** + * Deletes a given entity. + * + * @param entity The entity to delete + * @return the number of entities deleted + */ + suspend fun delete(entity: E): Int + + /** + * Deletes the given entities. + * + * @param entities The entities to delete + * @return the number of entities deleted + */ + suspend fun deleteAll(entities: Iterable): Int + + /** + * Deletes all entities managed by the repository. + * @return the number of entities deleted + */ + suspend fun deleteAll(): Int +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/suspend/multiple/CustomRepository.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/suspend/multiple/CustomRepository.kt new file mode 100644 index 00000000000..375d95059d9 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/suspend/multiple/CustomRepository.kt @@ -0,0 +1,32 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.server.suspend.multiple + +@MyRepository +interface CustomRepository : CoroutineCrudRepository { + + // As of Kotlin version 1.7.20 and KAPT, this will generate JVM signature: "SomeEntity findById(long id, continuation)" + override suspend fun findById(id: Long): SomeEntity? + + suspend fun xyz(): String + + suspend fun abc(): String + + suspend fun count1(): String + + suspend fun count2(): String + +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/suspend/multiple/InterceptorSpec.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/suspend/multiple/InterceptorSpec.kt new file mode 100644 index 00000000000..1e3b97db72e --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/suspend/multiple/InterceptorSpec.kt @@ -0,0 +1,61 @@ +/* + * Copyright 2017-2019 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.server.suspend.multiple + +import io.kotest.core.spec.style.StringSpec +import io.kotest.matchers.ints.shouldBeExactly +import io.kotest.matchers.shouldBe +import io.micronaut.context.ApplicationContext +import kotlinx.coroutines.runBlocking + +class InterceptorSpec : StringSpec() { + + val context = autoClose( + ApplicationContext.run() + ) + + private var myService = context.getBean(MyService::class.java) + + private var repository = context.getBean(CustomRepository::class.java) + + init { + "test correct interceptors calls" { + runBlocking { + MyService.events.clear() + myService.someCall() + MyService.events.size shouldBeExactly 8 + MyService.events[0] shouldBe "intercept1-start" + MyService.events[1] shouldBe "intercept2-start" + MyService.events[2] shouldBe "repository-abc" + MyService.events[3] shouldBe "repository-xyz" + MyService.events[4] shouldBe "intercept2-end" + MyService.events[5] shouldBe "intercept1-end" + MyService.events[6] shouldBe "repository-count1" + MyService.events[7] shouldBe "repository-count2" + } + } + + "test calling generic method" { + runBlocking { + MyService.events.clear() + // Validate that no bytecode error is produced + repository.findById(111) + MyService.events.size shouldBeExactly 1 + MyService.events[0] shouldBe "repository-findById" + } + } + } +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/suspend/multiple/MyRepository.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/suspend/multiple/MyRepository.kt new file mode 100644 index 00000000000..55ed35fee4d --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/suspend/multiple/MyRepository.kt @@ -0,0 +1,25 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.server.suspend.multiple + +import io.micronaut.aop.Introduction +import jakarta.inject.Singleton + +@MustBeDocumented +@kotlin.annotation.Retention(AnnotationRetention.RUNTIME) +@Introduction +@Singleton +annotation class MyRepository() diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/suspend/multiple/MyRepositoryInterceptorImpl.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/suspend/multiple/MyRepositoryInterceptorImpl.kt new file mode 100644 index 00000000000..0590cfeddad --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/suspend/multiple/MyRepositoryInterceptorImpl.kt @@ -0,0 +1,46 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.server.suspend.multiple + +import io.micronaut.aop.InterceptedMethod +import io.micronaut.aop.InterceptorBean +import io.micronaut.aop.MethodInterceptor +import io.micronaut.aop.MethodInvocationContext +import jakarta.inject.Singleton +import java.io.IOException +import java.util.concurrent.CompletableFuture + +@InterceptorBean(MyRepository::class) +@Singleton +class MyRepositoryInterceptorImpl : MethodInterceptor { + override fun intercept(context: MethodInvocationContext?): Any? { + val interceptedMethod = InterceptedMethod.of(context) + return try { + if (interceptedMethod.resultType() == InterceptedMethod.ResultType.COMPLETION_STAGE) { + MyService.events.add("repository-" + context!!.methodName) + val cf: CompletableFuture = CompletableFuture.supplyAsync{ + Thread.sleep(1000) + context!!.methodName + } + interceptedMethod.handleResult(cf) + } else { + throw IllegalStateException() + } + } catch (e: Exception) { + interceptedMethod.handleException(e) + } + } +} \ No newline at end of file diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/suspend/multiple/MyService.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/suspend/multiple/MyService.kt new file mode 100644 index 00000000000..7da2e746538 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/suspend/multiple/MyService.kt @@ -0,0 +1,35 @@ +package io.micronaut.docs.server.suspend.multiple + +import jakarta.inject.Singleton +import java.util.* +import kotlin.collections.ArrayList + +@Singleton +open class MyService( + private val repository: CustomRepository +) { + + companion object { + val events: MutableList = Collections.synchronizedList(ArrayList()) + } + + open suspend fun someCall() { + // Simulate accessing two different data-source repositories using two transactions + tx1() + // Call another coroutine + repository.count1() + repository.count2() + } + + @Transaction1 + open suspend fun tx1() { + tx2() + } + + @Transaction2 + open suspend fun tx2() { + repository.abc() + repository.xyz() + } + +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/suspend/multiple/SomeEntity.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/suspend/multiple/SomeEntity.kt new file mode 100644 index 00000000000..68b7aa896c9 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/suspend/multiple/SomeEntity.kt @@ -0,0 +1,4 @@ +package io.micronaut.docs.server.suspend.multiple + +class SomeEntity { +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/suspend/multiple/Transaction1.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/suspend/multiple/Transaction1.kt new file mode 100644 index 00000000000..60722c0556e --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/suspend/multiple/Transaction1.kt @@ -0,0 +1,30 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.server.suspend.multiple + +import io.micronaut.aop.Around +import kotlin.annotation.AnnotationRetention.RUNTIME +import kotlin.annotation.AnnotationTarget.CLASS +import kotlin.annotation.AnnotationTarget.FILE +import kotlin.annotation.AnnotationTarget.FUNCTION +import kotlin.annotation.AnnotationTarget.PROPERTY_GETTER +import kotlin.annotation.AnnotationTarget.PROPERTY_SETTER + +@MustBeDocumented +@Retention(RUNTIME) +@Target(CLASS, FILE, FUNCTION, PROPERTY_GETTER, PROPERTY_SETTER) +@Around +annotation class Transaction1 diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/suspend/multiple/Transaction1Interceptor.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/suspend/multiple/Transaction1Interceptor.kt new file mode 100644 index 00000000000..d48d3afea70 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/suspend/multiple/Transaction1Interceptor.kt @@ -0,0 +1,52 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.server.suspend.multiple + +import io.micronaut.aop.InterceptedMethod +import io.micronaut.aop.InterceptorBean +import io.micronaut.aop.MethodInterceptor +import io.micronaut.aop.MethodInvocationContext +import jakarta.inject.Singleton +import java.util.concurrent.CompletableFuture +import java.util.function.BiConsumer + +@InterceptorBean(Transaction1::class) +@Singleton +class Transaction1Interceptor : MethodInterceptor { + override fun intercept(context: MethodInvocationContext): Any? { + val interceptedMethod = InterceptedMethod.of(context) + return try { + return if (interceptedMethod.resultType() == InterceptedMethod.ResultType.COMPLETION_STAGE) { + MyService.events.add("intercept1-start") + val completionStage = interceptedMethod.interceptResultAsCompletionStage() + val cf = CompletableFuture() + completionStage.whenComplete { value, throwable -> + MyService.events.add("intercept1-end") + if (throwable == null) { + cf.complete(value) + } else { + cf.completeExceptionally(throwable) + } + } + interceptedMethod.handleResult(cf) + } else { + throw IllegalStateException() + } + } catch (e: Exception) { + interceptedMethod.handleException(e) + } + } +} \ No newline at end of file diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/suspend/multiple/Transaction2.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/suspend/multiple/Transaction2.kt new file mode 100644 index 00000000000..d32723cf1cf --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/suspend/multiple/Transaction2.kt @@ -0,0 +1,30 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.server.suspend.multiple + +import io.micronaut.aop.Around +import kotlin.annotation.AnnotationRetention.RUNTIME +import kotlin.annotation.AnnotationTarget.CLASS +import kotlin.annotation.AnnotationTarget.FILE +import kotlin.annotation.AnnotationTarget.FUNCTION +import kotlin.annotation.AnnotationTarget.PROPERTY_GETTER +import kotlin.annotation.AnnotationTarget.PROPERTY_SETTER + +@MustBeDocumented +@Retention(RUNTIME) +@Target(CLASS, FILE, FUNCTION, PROPERTY_GETTER, PROPERTY_SETTER) +@Around +annotation class Transaction2 diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/suspend/multiple/Transaction2Interceptor.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/suspend/multiple/Transaction2Interceptor.kt new file mode 100644 index 00000000000..7fe950117e7 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/suspend/multiple/Transaction2Interceptor.kt @@ -0,0 +1,51 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.server.suspend.multiple + +import io.micronaut.aop.InterceptedMethod +import io.micronaut.aop.InterceptorBean +import io.micronaut.aop.MethodInterceptor +import io.micronaut.aop.MethodInvocationContext +import jakarta.inject.Singleton +import java.util.concurrent.CompletableFuture + +@InterceptorBean(Transaction2::class) +@Singleton +class Transaction2Interceptor : MethodInterceptor { + override fun intercept(context: MethodInvocationContext): Any? { + val interceptedMethod = InterceptedMethod.of(context) + return try { + return if (interceptedMethod.resultType() == InterceptedMethod.ResultType.COMPLETION_STAGE) { + MyService.events.add("intercept2-start") + val completionStage = interceptedMethod.interceptResultAsCompletionStage() + val cf = CompletableFuture() + completionStage.whenComplete { value, throwable -> + MyService.events.add("intercept2-end") + if (throwable == null) { + cf.complete(value) + } else { + cf.completeExceptionally(throwable) + } + } + interceptedMethod.handleResult(cf) + } else { + throw IllegalStateException() + } + } catch (e: Exception) { + interceptedMethod.handleException(e) + } + } +} \ No newline at end of file diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/upload/BytesUploadController.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/upload/BytesUploadController.kt new file mode 100644 index 00000000000..0638eb00cd7 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/upload/BytesUploadController.kt @@ -0,0 +1,45 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.server.upload + +// tag::class[] +import io.micronaut.http.HttpResponse +import io.micronaut.http.MediaType +import io.micronaut.http.MediaType.MULTIPART_FORM_DATA +import io.micronaut.http.MediaType.TEXT_PLAIN +import io.micronaut.http.annotation.Controller +import io.micronaut.http.annotation.Post +import java.io.File +import java.io.IOException +import java.nio.file.Files +import java.nio.file.Paths + +@Controller("/upload") +class BytesUploadController { + + @Post(value = "/bytes", consumes = [MULTIPART_FORM_DATA], produces = [TEXT_PLAIN]) // <1> + fun uploadBytes(file: ByteArray, fileName: String): HttpResponse { // <2> + return try { + val tempFile = File.createTempFile(fileName, "temp") + val path = Paths.get(tempFile.absolutePath) + Files.write(path, file) // <3> + HttpResponse.ok("Uploaded") + } catch (e: IOException) { + HttpResponse.badRequest("Upload Failed") + } + } +} +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/upload/CompletedUploadController.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/upload/CompletedUploadController.kt new file mode 100644 index 00000000000..9b6d637f690 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/upload/CompletedUploadController.kt @@ -0,0 +1,45 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.server.upload + +// tag::class[] +import io.micronaut.http.HttpResponse +import io.micronaut.http.MediaType.MULTIPART_FORM_DATA +import io.micronaut.http.MediaType.TEXT_PLAIN +import io.micronaut.http.annotation.Controller +import io.micronaut.http.annotation.Post +import io.micronaut.http.multipart.CompletedFileUpload +import java.io.File +import java.io.IOException +import java.nio.file.Files +import java.nio.file.Paths + +@Controller("/upload") +class CompletedUploadController { + + @Post(value = "/completed", consumes = [MULTIPART_FORM_DATA], produces = [TEXT_PLAIN]) // <1> + fun uploadCompleted(file: CompletedFileUpload): HttpResponse { // <2> + return try { + val tempFile = File.createTempFile(file.filename, "temp") //<3> + val path = Paths.get(tempFile.absolutePath) + Files.write(path, file.bytes) //<3> + HttpResponse.ok("Uploaded") + } catch (e: IOException) { + HttpResponse.badRequest("Upload Failed") + } + } +} +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/upload/UploadController.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/upload/UploadController.kt new file mode 100644 index 00000000000..8ef22b66d05 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/upload/UploadController.kt @@ -0,0 +1,77 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.server.upload + +// tag::class[] +import io.micronaut.core.async.annotation.SingleResult +import io.micronaut.http.HttpResponse +import io.micronaut.http.HttpStatus.CONFLICT +import io.micronaut.http.MediaType.MULTIPART_FORM_DATA +import io.micronaut.http.MediaType.TEXT_PLAIN +import io.micronaut.http.annotation.Controller +import io.micronaut.http.annotation.Post +import io.micronaut.http.multipart.StreamingFileUpload +import org.reactivestreams.Publisher +import reactor.core.publisher.Mono +import java.io.ByteArrayOutputStream +import java.io.File +import java.io.OutputStream + +@Controller("/upload") +class UploadController { +// end::class[] + + // tag::file[] + @Post(value = "/", consumes = [MULTIPART_FORM_DATA], produces = [TEXT_PLAIN]) // <1> + fun upload(file: StreamingFileUpload): Mono> { // <2> + + val tempFile = File.createTempFile(file.filename, "temp") + val uploadPublisher = file.transferTo(tempFile) // <3> + + return Mono.from(uploadPublisher) // <4> + .map { success -> + if (success) { + HttpResponse.ok("Uploaded") + } else { + HttpResponse.status(CONFLICT) + .body("Upload Failed") + } + } + } + // end::file[] + + // tag::outputStream[] + @Post(value = "/outputStream", consumes = [MULTIPART_FORM_DATA], produces = [TEXT_PLAIN]) // <1> + @SingleResult + fun uploadOutputStream(file: StreamingFileUpload): Mono> { // <2> + val outputStream = ByteArrayOutputStream() // <3> + val uploadPublisher = file.transferTo(outputStream) // <4> + + return Mono.from(uploadPublisher) // <5> + .map { success: Boolean -> + return@map if (success) { + HttpResponse.ok("Uploaded") + } else { + HttpResponse.status(CONFLICT) + .body("Upload Failed") + } + } + } + // end::outputStream[] + +// tag::endclass[] +} +// end::endclass] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/upload/UploadControllerSpec.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/upload/UploadControllerSpec.kt new file mode 100644 index 00000000000..049f6a3b3e1 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/upload/UploadControllerSpec.kt @@ -0,0 +1,172 @@ +package io.micronaut.docs.server.upload + +import io.kotest.matchers.shouldBe +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.core.spec.style.StringSpec +import io.micronaut.context.ApplicationContext +import io.micronaut.http.HttpRequest +import io.micronaut.http.HttpStatus +import io.micronaut.http.MediaType +import io.micronaut.http.client.HttpClient +import io.micronaut.http.client.exceptions.HttpClientResponseException +import io.micronaut.http.client.multipart.MultipartBody +import io.micronaut.runtime.server.EmbeddedServer +import reactor.core.publisher.Flux + +class UploadControllerSpec: StringSpec() { + + val embeddedServer = autoClose( + ApplicationContext.run(EmbeddedServer::class.java) + ) + + val client = autoClose( + embeddedServer.applicationContext.createBean(HttpClient::class.java, embeddedServer.getURL()) + ) + + init { + "test file upload"() { + val body = MultipartBody.builder() + .addPart("file", "file.json", MediaType.APPLICATION_JSON_TYPE, "{\"title\":\"Foo\"}".toByteArray()) + .build() + + val flowable = Flux.from(client.exchange( + HttpRequest.POST("/upload", body) + .contentType(MediaType.MULTIPART_FORM_DATA) + .accept(MediaType.TEXT_PLAIN_TYPE), + String::class.java + )) + val response = flowable.blockFirst() + + response.status() shouldBe HttpStatus.OK + response.body.get() shouldBe "Uploaded" + } + + "test file upload outputstream"() { + val body = MultipartBody.builder() + .addPart("file", "file.json", MediaType.APPLICATION_JSON_TYPE, "{\"title\":\"Foo\"}".toByteArray()) + .build() + + val flowable = Flux.from(client.exchange( + HttpRequest.POST("/upload/outputStream", body) + .contentType(MediaType.MULTIPART_FORM_DATA) + .accept(MediaType.TEXT_PLAIN_TYPE), + String::class.java + )) + val response = flowable.blockFirst() + + response.status() shouldBe HttpStatus.OK + response.body.get() shouldBe "Uploaded" + } + + "test completed file upload"() { + val body = MultipartBody.builder() + .addPart("file", "file.json", MediaType.APPLICATION_JSON_TYPE, "{\"title\":\"Foo\"}".toByteArray()) + .build() + + val flowable = Flux.from(client!!.exchange( + HttpRequest.POST("/upload/completed", body) + .contentType(MediaType.MULTIPART_FORM_DATA) + .accept(MediaType.TEXT_PLAIN_TYPE), + String::class.java + )) + val response = flowable.blockFirst() + + response.status() shouldBe HttpStatus.OK + response.body.get() shouldBe "Uploaded" + } + + "test completed file upload with filename but no bytes"() { + val body = MultipartBody.builder() + .addPart("file", "file.json", MediaType.APPLICATION_JSON_TYPE, ByteArray(0)) + .build() + + val flowable = Flux.from(client!!.exchange( + HttpRequest.POST("/upload/completed", body) + .contentType(MediaType.MULTIPART_FORM_DATA) + .accept(MediaType.TEXT_PLAIN_TYPE), + String::class.java + )) + val response = flowable.blockFirst() + + response.status() shouldBe HttpStatus.OK + response.body.get() shouldBe "Uploaded" + } + + "test completed file upload with no name but with bytes"() { + val body = MultipartBody.builder() + .addPart("file", "", MediaType.APPLICATION_JSON_TYPE, "{\"title\":\"Foo\"}".toByteArray()) + .build() + + val flowable = Flux.from(client.exchange( + HttpRequest.POST("/upload/completed", body) + .contentType(MediaType.MULTIPART_FORM_DATA) + .accept(MediaType.TEXT_PLAIN_TYPE), + String::class.java + )) + + val ex = shouldThrow { flowable.blockFirst() } + val response = ex.response + val embedded: Map<*, *> = response.getBody(Map::class.java).get().get("_embedded") as Map<*, *> + val message = ((embedded.get("errors") as java.util.List<*>).get(0) as Map<*, *>).get("message") + + message shouldBe "Required argument [CompletedFileUpload file] not specified" + } + + "test completed file upload with no filename and no bytes"() { + val body = MultipartBody.builder() + .addPart("file", "", MediaType.APPLICATION_JSON_TYPE, ByteArray(0)) + .build() + + val flowable = Flux.from(client!!.exchange( + HttpRequest.POST("/upload/completed", body) + .contentType(MediaType.MULTIPART_FORM_DATA) + .accept(MediaType.TEXT_PLAIN_TYPE), + String::class.java + )) + + val ex = shouldThrow { flowable.blockFirst() } + val response = ex.response + val embedded: Map<*, *> = response.getBody(Map::class.java).get().get("_embedded") as Map<*, *> + val message = ((embedded.get("errors") as java.util.List<*>).get(0) as Map<*, *>).get("message") + + message shouldBe "Required argument [CompletedFileUpload file] not specified" + } + + "test completed file upload with no part"() { + val body = MultipartBody.builder() + .addPart("filex", "", MediaType.APPLICATION_JSON_TYPE, ByteArray(0)) + .build() + + val flowable = Flux.from(client!!.exchange( + HttpRequest.POST("/upload/completed", body) + .contentType(MediaType.MULTIPART_FORM_DATA) + .accept(MediaType.TEXT_PLAIN_TYPE), + String::class.java + )) + val ex = shouldThrow { flowable.blockFirst() } + val response = ex.response + val embedded: Map<*, *> = response.getBody(Map::class.java).get().get("_embedded") as Map<*, *> + val message = ((embedded.get("errors") as java.util.List<*>).get(0) as Map<*, *>).get("message") + + message shouldBe "Required argument [CompletedFileUpload file] not specified" + } + + "test file bytes uploaded"() { + val body = MultipartBody.builder() + .addPart("file", "file.json", MediaType.TEXT_PLAIN_TYPE, "some data".toByteArray()) + .addPart("fileName", "bar") + .build() + + val flowable = Flux.from(client!!.exchange( + HttpRequest.POST("/upload/bytes", body) + .contentType(MediaType.MULTIPART_FORM_DATA) + .accept(MediaType.TEXT_PLAIN_TYPE), + String::class.java + )) + val response = flowable.blockFirst() + + response.status() shouldBe HttpStatus.OK + response.body.get() shouldBe "Uploaded" + } + } +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/upload/WholeBodyUploadController.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/upload/WholeBodyUploadController.kt new file mode 100644 index 00000000000..fdb5577644b --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/upload/WholeBodyUploadController.kt @@ -0,0 +1,63 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.server.upload + +// tag::class[] +import io.micronaut.http.MediaType.MULTIPART_FORM_DATA +import io.micronaut.http.MediaType.TEXT_PLAIN +import io.micronaut.http.annotation.Body +import io.micronaut.http.annotation.Controller +import io.micronaut.http.annotation.Post +import io.micronaut.http.multipart.CompletedFileUpload +import io.micronaut.http.multipart.CompletedPart +import io.micronaut.http.server.multipart.MultipartBody +import org.reactivestreams.Subscriber +import org.reactivestreams.Subscription +import reactor.core.publisher.Mono + +@Controller("/upload") +class WholeBodyUploadController { + + @Post(value = "/whole-body", consumes = [MULTIPART_FORM_DATA], produces = [TEXT_PLAIN]) // <1> + fun uploadBytes(@Body body: MultipartBody): Mono { // <2> + return Mono.create { emitter -> + body.subscribe(object : Subscriber { + private var s: Subscription? = null + + override fun onSubscribe(s: Subscription) { + this.s = s + s.request(1) + } + + override fun onNext(completedPart: CompletedPart) { + val partName = completedPart.name + if (completedPart is CompletedFileUpload) { + val originalFileName = completedPart.filename + } + } + + override fun onError(t: Throwable) { + emitter.error(t) + } + + override fun onComplete() { + emitter.success("Uploaded") + } + }) + } + } +} +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/uris/UriTemplateTest.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/uris/UriTemplateTest.kt new file mode 100644 index 00000000000..0a70ad7d1ba --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/uris/UriTemplateTest.kt @@ -0,0 +1,19 @@ +package io.micronaut.docs.server.uris + +import io.micronaut.http.uri.UriMatchTemplate +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Test + +class UriTemplateTest { + + @Test + fun testUriTemplate() { + // tag::match[] + val template = UriMatchTemplate.of("/hello/{name}") + + assertTrue(template.match("/hello/John").isPresent) // <1> + assertEquals("/hello/John", template.expand(mapOf("name" to "John"))) // <2> + // end::match[] + } +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/session/Cart.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/session/Cart.kt new file mode 100644 index 00000000000..fc92e9bdfc9 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/session/Cart.kt @@ -0,0 +1,23 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.session + +import java.util.ArrayList + +class Cart { + + var items: MutableList = ArrayList() +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/session/ShoppingController.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/session/ShoppingController.kt new file mode 100644 index 00000000000..b940093265c --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/session/ShoppingController.kt @@ -0,0 +1,66 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.session + +// tag::imports[] +import io.micronaut.http.annotation.Controller +import io.micronaut.http.annotation.Get +import io.micronaut.http.annotation.Post +import io.micronaut.session.Session +import io.micronaut.session.annotation.SessionValue +// end::imports[] + +// tag::class[] +@Controller("/shopping") +class ShoppingController { + + companion object { + private const val ATTR_CART = "cart" // <1> + } +// end::class[] + + // tag::view[] + @Get("/cart") + @SessionValue(ATTR_CART) // <1> + internal fun viewCart(@SessionValue cart: Cart?): Cart { // <2> + return cart ?: Cart() + } + // end::view[] + + // tag::add[] + @Post("/cart/{name}") + internal fun addItem(session: Session, name: String): Cart { // <2> + require(name.isNotBlank()) { "Name cannot be blank" } + val cart = session.get(ATTR_CART, Cart::class.java).orElseGet { // <3> + val newCart = Cart() + session.put(ATTR_CART, newCart) // <4> + newCart + } + cart.items.add(name) + return cart + } + // end::add[] + + // tag::clear[] + @Post("/cart/clear") + internal fun clearCart(session: Session?) { + session?.remove(ATTR_CART) + } + // end::clear[] + +// tag::endclass[] +} +// end::endclass[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/session/ShoppingControllerSpec.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/session/ShoppingControllerSpec.kt new file mode 100644 index 00000000000..a4d46c90e3d --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/session/ShoppingControllerSpec.kt @@ -0,0 +1,59 @@ +package io.micronaut.docs.session + +import io.kotest.matchers.shouldBe +import io.kotest.core.spec.style.StringSpec +import io.micronaut.context.ApplicationContext +import io.micronaut.http.HttpHeaders +import io.micronaut.http.HttpRequest +import io.micronaut.http.client.HttpClient +import io.micronaut.runtime.server.EmbeddedServer +import reactor.core.publisher.Flux +import kotlin.test.assertNotNull + +class ShoppingControllerSpec: StringSpec() { + + val embeddedServer = autoClose( + ApplicationContext.run(EmbeddedServer::class.java) + ) + + val client = autoClose( + embeddedServer.applicationContext.createBean(HttpClient::class.java, embeddedServer.url) + ) + + init { + "testSessionValueUsedOnReturnValue" { + // tag::view[] + var response = Flux.from(client.exchange(HttpRequest.GET("/shopping/cart"), Cart::class.java)) // <1> + .blockFirst() + var cart = response.body() + + assertNotNull(response.header(HttpHeaders.AUTHORIZATION_INFO)) // <2> + assertNotNull(cart) + cart.items.isEmpty() + // end::view[] + + // tag::add[] + val sessionId = response.header(HttpHeaders.AUTHORIZATION_INFO) // <1> + + response = Flux.from(client.exchange(HttpRequest.POST("/shopping/cart/Apple", "") + .header(HttpHeaders.AUTHORIZATION_INFO, sessionId), Cart::class.java)) // <2> + .blockFirst() + cart = response.body() + // end::add[] + + assertNotNull(cart) + cart.items.size shouldBe 1 + + response = Flux.from(client.exchange(HttpRequest.GET("/shopping/cart") + .header(HttpHeaders.AUTHORIZATION_INFO, sessionId), Cart::class.java)) + .blockFirst() + cart = response.body() + + response.header(HttpHeaders.AUTHORIZATION_INFO) + assertNotNull(cart) + + cart.items.size shouldBe 1 + cart.items[0] shouldBe "Apple" + } + } +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/sse/HeadlineClient.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/sse/HeadlineClient.kt new file mode 100644 index 00000000000..79ae5cc15cf --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/sse/HeadlineClient.kt @@ -0,0 +1,32 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.sse + +import io.micronaut.docs.streaming.Headline +import io.micronaut.http.MediaType.TEXT_EVENT_STREAM +import io.micronaut.http.annotation.Get +import io.micronaut.http.client.annotation.Client +import io.micronaut.http.sse.Event +import reactor.core.publisher.Flux + +// tag::class[] +@Client("/streaming/sse") +interface HeadlineClient { + + @Get(value = "/headlines", processes = [TEXT_EVENT_STREAM]) + fun streamHeadlines(): Flux> +} +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/sse/HeadlineController.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/sse/HeadlineController.kt new file mode 100644 index 00000000000..ff188b4a19c --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/sse/HeadlineController.kt @@ -0,0 +1,45 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.sse + +import io.micronaut.docs.streaming.Headline +import io.micronaut.http.MediaType.TEXT_EVENT_STREAM +import io.micronaut.http.annotation.Controller +import io.micronaut.http.annotation.Get +import io.micronaut.http.sse.Event +import reactor.core.publisher.Flux +import reactor.core.publisher.FluxSink +import java.time.Duration +import java.time.ZonedDateTime +import java.time.temporal.ChronoUnit + +@Controller("/streaming/sse") +class HeadlineController { + + // tag::streaming[] + @Get(value = "/headlines", processes = [TEXT_EVENT_STREAM]) // <1> + internal fun streamHeadlines(): Flux> { + return Flux.create>( { emitter -> // <2> + val headline = Headline() + headline.text = "Latest Headline at ${ZonedDateTime.now()}" + emitter.next(Event.of(headline)) + emitter.complete() + }, FluxSink.OverflowStrategy.BUFFER) + .repeat(100) // <3> + .delayElements(Duration.of(1, ChronoUnit.SECONDS)) // <4> + } + // end::streaming[] +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/sse/HeadlineControllerSpec.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/sse/HeadlineControllerSpec.kt new file mode 100644 index 00000000000..87f1ef38b02 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/sse/HeadlineControllerSpec.kt @@ -0,0 +1,30 @@ +package io.micronaut.docs.sse + +import io.kotest.core.spec.style.StringSpec +import io.micronaut.context.ApplicationContext +import io.micronaut.runtime.server.EmbeddedServer + +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertTrue + +class HeadlineControllerSpec: StringSpec() { + + val embeddedServer = autoClose( + ApplicationContext.run(EmbeddedServer::class.java) + ) + + init { + // tag::streamingClient[] + "test client annotations streaming" { + val headlineClient = embeddedServer + .applicationContext + .getBean(HeadlineClient::class.java) + + val headline = headlineClient.streamHeadlines().blockFirst() + + assertNotNull(headline) + assertTrue(headline!!.data.text!!.startsWith("Latest Headline")) + } + // end::streamingClient[] + } +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/streaming/Headline.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/streaming/Headline.kt new file mode 100644 index 00000000000..5f2a2c5b39f --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/streaming/Headline.kt @@ -0,0 +1,20 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.streaming + +class Headline { + var text: String? = null +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/streaming/HeadlineClient.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/streaming/HeadlineClient.kt new file mode 100644 index 00000000000..5e45cb07008 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/streaming/HeadlineClient.kt @@ -0,0 +1,39 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.streaming + +// tag::imports[] +import io.micronaut.http.MediaType.APPLICATION_JSON_STREAM +import io.micronaut.http.annotation.Get +import io.micronaut.http.client.annotation.Client +import reactor.core.publisher.Flux + +// end::imports[] + +// tag::class[] +@Client("/streaming") +interface HeadlineClient { + + @Get(value = "/headlines", processes = [APPLICATION_JSON_STREAM]) // <1> + fun streamHeadlines(): Flux // <2> +// end::class[] + + @Get(value = "/headlines", processes = [APPLICATION_JSON_STREAM]) // <1> + fun streamFlux(): Flux + +// tag::endclass[] +} +// end::endclass[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/streaming/HeadlineController.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/streaming/HeadlineController.kt new file mode 100644 index 00000000000..e98cf98a1e0 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/streaming/HeadlineController.kt @@ -0,0 +1,44 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.streaming + +// tag::imports[] +import io.micronaut.http.MediaType.APPLICATION_JSON_STREAM +import io.micronaut.http.annotation.Controller +import io.micronaut.http.annotation.Get +import reactor.core.publisher.Flux +import reactor.core.publisher.Mono +import java.time.Duration +import java.time.ZonedDateTime +import java.time.temporal.ChronoUnit +import java.util.concurrent.TimeUnit.SECONDS +// end::imports[] + +@Controller("/streaming") +class HeadlineController { + + // tag::streaming[] + @Get(value = "/headlines", processes = [APPLICATION_JSON_STREAM]) // <1> + internal fun streamHeadlines(): Flux { + return Mono.fromCallable { // <2> + val headline = Headline() + headline.text = "Latest Headline at ${ZonedDateTime.now()}" + headline + }.repeat(100) // <3> + .delayElements(Duration.of(1, ChronoUnit.SECONDS)) // <4> + } + // end::streaming[] +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/streaming/HeadlineControllerSpec.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/streaming/HeadlineControllerSpec.kt new file mode 100644 index 00000000000..d8542fb7ed1 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/streaming/HeadlineControllerSpec.kt @@ -0,0 +1,80 @@ +package io.micronaut.docs.streaming + +import io.kotest.matchers.shouldNotBe +import io.kotest.core.spec.style.StringSpec +import io.kotest.matchers.string.shouldStartWith +import io.micronaut.context.ApplicationContext +import io.micronaut.http.HttpRequest.GET +import io.micronaut.http.client.HttpClient +import io.micronaut.http.client.StreamingHttpClient +import io.micronaut.runtime.server.EmbeddedServer +import org.junit.Assert.fail +import org.reactivestreams.Subscriber +import org.reactivestreams.Subscription +import java.util.concurrent.CompletableFuture +import java.util.concurrent.TimeUnit + +class HeadlineControllerSpec: StringSpec() { + + val embeddedServer = autoClose( + ApplicationContext.run(EmbeddedServer::class.java) + ) + + val client = autoClose( + embeddedServer.applicationContext.createBean(HttpClient::class.java, embeddedServer.url) + ) + + init { + // tag::streamingClient[] + "test client annotation streaming" { + val headlineClient = embeddedServer + .applicationContext + .getBean(HeadlineClient::class.java) // <1> + + val firstHeadline = headlineClient.streamHeadlines().next() // <2> + + val headline = firstHeadline.block() // <3> + + headline shouldNotBe null + headline.text shouldStartWith "Latest Headline" + } + // end::streamingClient[] + + "test streaming client" { + val client = embeddedServer.applicationContext.createBean( + StreamingHttpClient::class.java, embeddedServer.url) + + // tag::streaming[] + val headlineStream = client.jsonStream( + GET("/streaming/headlines"), Headline::class.java) // <1> + val future = CompletableFuture() // <2> + headlineStream.subscribe(object : Subscriber { + override fun onSubscribe(s: Subscription) { + s.request(1) // <3> + } + + override fun onNext(headline: Headline) { + println("Received Headline = ${headline.text!!}") + future.complete(headline) // <4> + } + + override fun onError(t: Throwable) { + future.completeExceptionally(t) // <5> + } + + override fun onComplete() { + // no-op // <6> + } + }) + // end::streaming[] + + try { + val headline = future.get(3, TimeUnit.SECONDS) + headline.text shouldStartWith "Latest Headline" + } catch (e: Throwable) { + fail("Asynchronous error occurred: " + (e.message ?: e.javaClass.simpleName)) + } + client.stop() + } + } +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/streaming/HeadlineFlowClient.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/streaming/HeadlineFlowClient.kt new file mode 100644 index 00000000000..f87782b7b30 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/streaming/HeadlineFlowClient.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.streaming + +// tag::imports[] +import io.micronaut.http.MediaType +import io.micronaut.http.annotation.Get +import io.micronaut.http.client.annotation.Client +import kotlinx.coroutines.flow.Flow +// end::imports[] + +// tag::class[] +@Client("/streaming") +interface HeadlineFlowClient { + // end::class[] + + // tag::streamingWithFlow[] + @Get(value = "/headlinesWithFlow", processes = [MediaType.APPLICATION_JSON_STREAM]) // <1> + fun streamFlow(): Flow // <2> + // tag::streamingWithFlow[] +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/streaming/HeadlineFlowController.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/streaming/HeadlineFlowController.kt new file mode 100644 index 00000000000..17bbfea7d3f --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/streaming/HeadlineFlowController.kt @@ -0,0 +1,56 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.streaming + +// tag::imports[] +import io.micronaut.http.HttpResponse +import io.micronaut.http.MediaType +import io.micronaut.http.annotation.Controller +import io.micronaut.http.annotation.Error +import io.micronaut.http.annotation.Get +import io.micronaut.http.annotation.Produces +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow +import java.time.ZonedDateTime +// end::imports[] + +@Controller("/streaming") +class HeadlineFlowController { + + // tag::streamingWithFlow[] + @Get(value = "/headlinesWithFlow", processes = [MediaType.APPLICATION_JSON_STREAM]) + internal fun streamHeadlinesWithFlow(): Flow = // <1> + flow { // <2> + repeat(100) { // <3> + with (Headline()) { + text = "Latest Headline at ${ZonedDateTime.now()}" + emit(this) // <4> + delay(1_000) // <5> + } + } + } + // end::streamingWithFlow[] + + @Get(value = "/illegal") + fun illegal(): Any = throw IllegalArgumentException() + + @Error(exception = IllegalArgumentException::class) + @Produces(MediaType.TEXT_PLAIN) + fun onIllegalArgument(e: IllegalArgumentException): Flow> = flow { + emit(HttpResponse.badRequest("illegal.argument")) + } +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/streaming/HeadlineFlowControllerSpec.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/streaming/HeadlineFlowControllerSpec.kt new file mode 100644 index 00000000000..a8fe98a5d31 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/streaming/HeadlineFlowControllerSpec.kt @@ -0,0 +1,55 @@ +package io.micronaut.docs.streaming + +import io.kotest.matchers.shouldBe +import io.kotest.matchers.shouldNotBe +import io.kotest.assertions.throwables.shouldThrowExactly +import io.kotest.core.annotation.Ignored +import io.kotest.core.spec.style.StringSpec +import io.kotest.matchers.string.shouldStartWith +import io.micronaut.context.ApplicationContext +import io.micronaut.http.HttpRequest +import io.micronaut.http.HttpStatus +import io.micronaut.http.client.HttpClient +import io.micronaut.http.client.exceptions.HttpClientResponseException +import io.micronaut.runtime.server.EmbeddedServer +import kotlinx.coroutines.flow.take +import kotlinx.coroutines.flow.toList + +// Flow converters moved to Kotlin Module re-enable once +// new version of micronaut-kotlin-runtime is published +@Ignored +class HeadlineFlowControllerSpec: StringSpec() { + + val embeddedServer = autoClose( + ApplicationContext.run(EmbeddedServer::class.java) + ) + + val client = autoClose( + embeddedServer.applicationContext.createBean(HttpClient::class.java, embeddedServer.url) + ) + + init { + // tag::streamingClientWithFlow[] + "test client annotation streaming with Flow" { + val headlineClient = embeddedServer + .applicationContext + .getBean(HeadlineFlowClient::class.java) + + val headline = headlineClient.streamFlow().take(1).toList().first() + + headline shouldNotBe null + headline.text shouldStartWith "Latest Headline" + } + // end::streamingClientWithFlow[] + + "test error route with Flow" { + val ex = shouldThrowExactly { + client.toBlocking().exchange(HttpRequest.GET("/streaming/illegal"), String::class.java) + } + val body = ex.response.getBody(String::class.java).get() + + ex.status shouldBe HttpStatus.BAD_REQUEST + body shouldBe "illegal.argument" + } + } +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/web/router/version/VersionedController.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/web/router/version/VersionedController.kt new file mode 100644 index 00000000000..4bf2b402cf6 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/web/router/version/VersionedController.kt @@ -0,0 +1,51 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.web.router.version + +// tag::imports[] +import io.micronaut.core.version.annotation.Version +import io.micronaut.http.annotation.Controller +import io.micronaut.http.annotation.Get +// end::imports[] + +// tag::clazz[] +@Controller("/versioned") +internal class VersionedController { + + @Version("1") // <1> + @Get("/hello") + fun helloV1(): String { + return "helloV1" + } + + @Version("2") // <2> + @Get("/hello") + fun helloV2(): String { + return "helloV2" + } + // end::clazz[] + + @Version("2") + @Get("/hello") + fun duplicatedHelloV2(): String { + return "duplicatedHelloV2" + } + + @Get("/hello") + fun hello(): String { + return "hello" + } +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/whatsNew/CacheFactory.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/whatsNew/CacheFactory.kt new file mode 100644 index 00000000000..43a081412dd --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/whatsNew/CacheFactory.kt @@ -0,0 +1,39 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.whatsNew + +// tag::imports[] +import io.micronaut.context.annotation.Bean +import io.micronaut.context.annotation.Factory + +import javax.cache.CacheManager +import javax.cache.Caching +import javax.cache.configuration.MutableConfiguration +import jakarta.inject.Singleton +// end::imports[] + +// tag::class[] +@Factory +class CacheFactory { + + @Singleton + fun cacheManager(): CacheManager { + val cacheManager = Caching.getCachingProvider().cacheManager + cacheManager.createCache("my-cache", MutableConfiguration()) + return cacheManager + } +} +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/writable/TemplateController.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/writable/TemplateController.kt new file mode 100644 index 00000000000..2dfb1457f14 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/writable/TemplateController.kt @@ -0,0 +1,58 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.writable + +//tag::imports[] +import groovy.text.SimpleTemplateEngine +import groovy.text.Template +import io.micronaut.core.io.Writable +import io.micronaut.http.MediaType +import io.micronaut.http.annotation.Controller +import io.micronaut.http.annotation.Get +import io.micronaut.http.server.exceptions.HttpServerException +import java.io.Writer +//end::imports[] + +//tag::clazz[] +@Controller("/template") +class TemplateController { + + private val templateEngine = SimpleTemplateEngine() + private val template = initTemplate() // <1> + + @Get(value = "/welcome", produces = [MediaType.TEXT_PLAIN]) + internal fun render(): Writable { // <2> + return { writer: Writer -> + template.make( // <3> + mapOf( + "firstName" to "Fred", + "lastName" to "Flintstone" + ) + ).writeTo(writer) + } as Writable + } + + private fun initTemplate(): Template { + return try { + templateEngine.createTemplate( + "Dear \$firstName \$lastName. Nice to meet you." + ) + } catch (e: Exception) { + throw HttpServerException("Cannot create template") + } + } +} +//end::clazz[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/http/client/GreetingClient.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/http/client/GreetingClient.kt new file mode 100644 index 00000000000..8e85dfe9f45 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/http/client/GreetingClient.kt @@ -0,0 +1,27 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.http.client + +import io.micronaut.http.annotation.Get +import io.micronaut.http.client.annotation.Client + +// tests that nullable can compile +// issue: https://github.com/micronaut-projects/micronaut-core/issues/1080 +@Client("/") +interface GreetingClient { + @Get("/greeting{?name}") + fun greet(name : String? ) : String +} \ No newline at end of file diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/http/client/SuspendClient.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/http/client/SuspendClient.kt new file mode 100644 index 00000000000..a4eba50ab56 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/http/client/SuspendClient.kt @@ -0,0 +1,19 @@ +package io.micronaut.http.client + +import io.micronaut.http.HttpResponse +import io.micronaut.http.annotation.Get +import io.micronaut.http.annotation.Put +import io.micronaut.http.client.annotation.Client + +@Client("/") +interface SuspendClient { + + @Put + suspend fun call(newState: String): String + + @Get + suspend fun notFound(): HttpResponse + + @Get + suspend fun notFoundWithoutHttpResponseWrapper(): String? +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/http/client/SuspendClientController.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/http/client/SuspendClientController.kt new file mode 100644 index 00000000000..891080133cf --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/http/client/SuspendClientController.kt @@ -0,0 +1,24 @@ +package io.micronaut.http.client + +import io.micronaut.context.annotation.Requires +import io.micronaut.http.annotation.Body +import io.micronaut.http.annotation.Controller +import io.micronaut.http.annotation.Get +import io.micronaut.http.annotation.Put +import kotlinx.coroutines.delay + +@Requires(property = "spec.name", value = "SuspendClientSpec") +@Controller +class SuspendClientController { + + @Put + fun echo(@Body body: String): String { + return body + } + + @Get + suspend fun notFound(): String? { + delay(1) + return null + } +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/http/client/SuspendClientSpec.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/http/client/SuspendClientSpec.kt new file mode 100644 index 00000000000..606cee07790 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/http/client/SuspendClientSpec.kt @@ -0,0 +1,45 @@ +package io.micronaut.http.client + +import io.micronaut.context.ApplicationContext +import io.micronaut.http.HttpStatus +import io.micronaut.runtime.server.EmbeddedServer +import kotlinx.coroutines.runBlocking +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Test + +class SuspendClientSpec { + + @Test + fun testSuspendClientBody() { + val server = ApplicationContext.run(EmbeddedServer::class.java, mapOf("spec.name" to "SuspendClientSpec")) + val ctx = server.applicationContext + val response = runBlocking { + ctx.getBean(SuspendClient::class.java).call("test") + } + + Assertions.assertEquals(response, "{\"newState\":\"test\"}") + } + + @Test + fun testNotFound() { + val server = ApplicationContext.run(EmbeddedServer::class.java, mapOf("spec.name" to "SuspendClientSpec")) + val ctx = server.applicationContext + val response = runBlocking { + ctx.getBean(SuspendClient::class.java).notFound() + } + + Assertions.assertEquals(response.status, HttpStatus.NOT_FOUND) + } + + @Test + fun testNotFoundWithoutHttpResponseWrapper() { + val server = ApplicationContext.run(EmbeddedServer::class.java, mapOf("spec.name" to "SuspendClientSpec")) + val ctx = server.applicationContext + val response = runBlocking { + ctx.getBean(SuspendClient::class.java).notFoundWithoutHttpResponseWrapper() + } + + Assertions.assertNull(response) + } + +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/http/server/WebSocketSuspendTest.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/http/server/WebSocketSuspendTest.kt new file mode 100644 index 00000000000..cf6d7eb541b --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/http/server/WebSocketSuspendTest.kt @@ -0,0 +1,65 @@ +package io.micronaut.http.server + +import io.micronaut.context.annotation.Property +import io.micronaut.context.annotation.Requires +import io.micronaut.runtime.server.EmbeddedServer +import io.micronaut.test.extensions.junit5.annotation.MicronautTest +import io.micronaut.websocket.WebSocketClient +import io.micronaut.websocket.WebSocketSession +import io.micronaut.websocket.annotation.ClientWebSocket +import io.micronaut.websocket.annotation.OnMessage +import io.micronaut.websocket.annotation.ServerWebSocket +import jakarta.inject.Inject +import kotlinx.coroutines.delay +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.Timeout +import reactor.core.publisher.Flux +import spock.lang.Issue + +@MicronautTest +@Property(name = "spec.name", value = "WebSocketSuspendTest") +class WebSocketSuspendTest { + @Inject + lateinit var server: EmbeddedServer + + @Inject + lateinit var client: WebSocketClient + + @Issue("https://github.com/micronaut-projects/micronaut-core/issues/6582") + @Test + @Timeout(10) + fun test() { + val cl = Flux.from(client.connect(TestWebSocketClient::class.java, server.uri.toString() + "/demo/ws")).blockFirst()!! + cl.send("foo") + while (true) { + Thread.sleep(100) + if (cl.received == "foo") { + break + } + } + cl.close() + } + + @Requires(property = "spec.name", value = "WebSocketSuspendTest") + @ServerWebSocket("/demo/ws") + class TestWebSocketController { + @OnMessage + suspend fun messageHandler(message: String, session: WebSocketSession) { + delay(100) + session.sendSync(message) + } + } + + @Requires(property = "spec.name", value = "WebSocketSuspendTest") + @ClientWebSocket("/demo/ws") + abstract class TestWebSocketClient : AutoCloseable { + var received: String = "" + + abstract fun send(msg: String) + + @OnMessage + fun onMessage(msg: String) { + this.received = msg + } + } +} \ No newline at end of file diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/http/server/upload/KotlinUploadController.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/http/server/upload/KotlinUploadController.kt new file mode 100644 index 00000000000..810c607289f --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/http/server/upload/KotlinUploadController.kt @@ -0,0 +1,43 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.http.server.upload + +import io.micronaut.context.annotation.Requires +import io.micronaut.http.MediaType.MULTIPART_FORM_DATA +import io.micronaut.http.MediaType.TEXT_PLAIN +import io.micronaut.http.annotation.Controller +import io.micronaut.http.annotation.Post +import io.micronaut.http.multipart.StreamingFileUpload +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.reduce +import kotlinx.coroutines.reactive.asFlow +import kotlinx.coroutines.reactive.awaitFirstOrNull +import reactor.core.publisher.Flux + +@Requires(property = "spec.name", value = "KotlinUploadControllerSpec") +@Controller("/upload") +class KotlinUploadController { + + @Post(value = "/flow", consumes = [MULTIPART_FORM_DATA], produces = [TEXT_PLAIN]) + suspend fun uploadFlow(file: StreamingFileUpload): Int { + return file.asFlow().map { it.bytes.size }.reduce { accumulator, value -> accumulator + value } + } + + @Post(value = "/await", consumes = [MULTIPART_FORM_DATA], produces = [TEXT_PLAIN]) + suspend fun uploadAwaitFlux(file: StreamingFileUpload): Int { + return Flux.from(file).map { it.bytes.size }.reduce { accumulator, value -> accumulator + value }.awaitFirstOrNull() ?: 0 + } +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/http/server/upload/KotlinUploadControllerSpec.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/http/server/upload/KotlinUploadControllerSpec.kt new file mode 100644 index 00000000000..dbec91da936 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/http/server/upload/KotlinUploadControllerSpec.kt @@ -0,0 +1,59 @@ +package io.micronaut.http.server.upload + +import io.kotest.matchers.shouldBe +import io.kotest.core.spec.style.StringSpec +import io.micronaut.context.ApplicationContext +import io.micronaut.http.HttpRequest +import io.micronaut.http.HttpStatus +import io.micronaut.http.MediaType +import io.micronaut.http.client.HttpClient +import io.micronaut.http.client.multipart.MultipartBody +import io.micronaut.runtime.server.EmbeddedServer +import reactor.core.publisher.Flux + +class KotlinUploadControllerSpec: StringSpec() { + + val embeddedServer = autoClose( + ApplicationContext.run(EmbeddedServer::class.java, mapOf("spec.name" to KotlinUploadControllerSpec::class.simpleName)) + ) + + val client = autoClose( + embeddedServer.applicationContext.createBean(HttpClient::class.java, embeddedServer.getURL()) + ) + + init { + "test file upload with kotlin flow"() { + val body = MultipartBody.builder() + .addPart("file", "file.json", MediaType.APPLICATION_JSON_TYPE, "{\"title\":\"Foo\"}".toByteArray()) + .build() + + val flowable = Flux.from(client.exchange( + HttpRequest.POST("/upload/flow", body) + .contentType(MediaType.MULTIPART_FORM_DATA) + .accept(MediaType.TEXT_PLAIN_TYPE), + Int::class.java + )) + val response = flowable.blockFirst() + + response.status() shouldBe HttpStatus.OK + response.body.get() shouldBe 15 + } + + "test file upload with kotlin await"() { + val body = MultipartBody.builder() + .addPart("file", "file.json", MediaType.APPLICATION_JSON_TYPE, "{\"title\":\"Foo\"}".toByteArray()) + .build() + + val flowable = Flux.from(client.exchange( + HttpRequest.POST("/upload/await", body) + .contentType(MediaType.MULTIPART_FORM_DATA) + .accept(MediaType.TEXT_PLAIN_TYPE), + Int::class.java + )) + val response = flowable.blockFirst() + + response.status() shouldBe HttpStatus.OK + response.body.get() shouldBe 15 + } + } +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/inject/constructor/nullableinjection/A.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/inject/constructor/nullableinjection/A.kt new file mode 100644 index 00000000000..eb7938182f1 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/inject/constructor/nullableinjection/A.kt @@ -0,0 +1,18 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.inject.constructor.nullableinjection + +interface A diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/inject/constructor/nullableinjection/B.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/inject/constructor/nullableinjection/B.kt new file mode 100644 index 00000000000..e6b0bfe3ab4 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/inject/constructor/nullableinjection/B.kt @@ -0,0 +1,21 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.inject.constructor.nullableinjection + +import jakarta.inject.Inject + +class B @Inject +internal constructor(val a: A?) diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/inject/constructor/nullableinjection/C.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/inject/constructor/nullableinjection/C.kt new file mode 100644 index 00000000000..3a512199411 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/inject/constructor/nullableinjection/C.kt @@ -0,0 +1,21 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.inject.constructor.nullableinjection + +import jakarta.inject.Inject + +class C @Inject +internal constructor(val a: A) diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/inject/constructor/nullableinjection/ConstructorNullableInjectionSpec.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/inject/constructor/nullableinjection/ConstructorNullableInjectionSpec.kt new file mode 100644 index 00000000000..0a880cb9a61 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/inject/constructor/nullableinjection/ConstructorNullableInjectionSpec.kt @@ -0,0 +1,45 @@ +/* + * Copyright 2017-2019 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.inject.constructor.nullableinjection + +import io.micronaut.context.BeanContext +import io.micronaut.context.exceptions.DependencyInjectionException +import junit.framework.TestCase +import org.junit.jupiter.api.Assertions.assertNull +import org.junit.jupiter.api.Assertions.fail +import org.junit.jupiter.api.Test + +class ConstructorNullableInjectionSpec { + + @Test + fun testNullableInjectionInConstructor() { + val context = BeanContext.run() + val b = context.getBean(B::class.java) + assertNull(b.a) + + context.close() + } + + @Test + fun testNormalInjectionStillFails() { + val context = BeanContext.run() + try { + context.getBean(C::class.java) + fail("Expected a DependencyInjectionException to be thrown") + } catch (e: DependencyInjectionException) {} + context.close() + } +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/inject/field/nullableinjection/A.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/inject/field/nullableinjection/A.kt new file mode 100644 index 00000000000..49b37cb5f33 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/inject/field/nullableinjection/A.kt @@ -0,0 +1,18 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.inject.field.nullableinjection + +interface A diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/inject/field/nullableinjection/B.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/inject/field/nullableinjection/B.kt new file mode 100644 index 00000000000..2534aac70b3 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/inject/field/nullableinjection/B.kt @@ -0,0 +1,25 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.inject.field.nullableinjection + +import jakarta.inject.Inject +import jakarta.inject.Singleton + +@Singleton +class B { + internal var a: A? = null + @Inject set +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/inject/field/nullableinjection/FieldNullableInjectionSpec.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/inject/field/nullableinjection/FieldNullableInjectionSpec.kt new file mode 100644 index 00000000000..262179d12da --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/inject/field/nullableinjection/FieldNullableInjectionSpec.kt @@ -0,0 +1,32 @@ +/* + * Copyright 2017-2019 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.inject.field.nullableinjection + +import io.micronaut.context.BeanContext +import org.junit.jupiter.api.Assertions.assertNull +import org.junit.jupiter.api.Test + +class FieldNullableInjectionSpec { + + @Test + fun testNullableFieldInjection() { + val context = BeanContext.run() + val b = context.getBean(B::class.java) + assertNull(b.a) + context.close() + } + +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/inject/method/nullableinjection/A.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/inject/method/nullableinjection/A.kt new file mode 100644 index 00000000000..871413bc78a --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/inject/method/nullableinjection/A.kt @@ -0,0 +1,18 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.inject.method.nullableinjection + +interface A diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/inject/method/nullableinjection/B.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/inject/method/nullableinjection/B.kt new file mode 100644 index 00000000000..ef4bce0bb00 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/inject/method/nullableinjection/B.kt @@ -0,0 +1,25 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.inject.method.nullableinjection + +import jakarta.inject.Inject +import jakarta.inject.Singleton + +@Singleton +class B { + internal var a: A? = null + @Inject set +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/inject/method/nullableinjection/C.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/inject/method/nullableinjection/C.kt new file mode 100644 index 00000000000..3dc10b3e7e1 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/inject/method/nullableinjection/C.kt @@ -0,0 +1,28 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.inject.method.nullableinjection + +import jakarta.inject.Inject +import jakarta.inject.Singleton + + +@Singleton +class C { + internal var _a: A? = null + internal var a: A + get() = _a!! + @Inject set(value) { _a = value; } +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/inject/method/nullableinjection/SetterWithNullableSpec.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/inject/method/nullableinjection/SetterWithNullableSpec.kt new file mode 100644 index 00000000000..52099874c0d --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/inject/method/nullableinjection/SetterWithNullableSpec.kt @@ -0,0 +1,43 @@ +/* + * Copyright 2017-2019 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.inject.method.nullableinjection + +import io.micronaut.context.BeanContext +import io.micronaut.context.exceptions.DependencyInjectionException +import org.junit.jupiter.api.Assertions.assertNull +import org.junit.jupiter.api.Assertions.fail +import org.junit.jupiter.api.Test + +class SetterWithNullableSpec { + + @Test + fun testInjectionOfNullableObjects() { + val context = BeanContext.run() + val b = context.getBean(B::class.java) + assertNull(b.a) + context.close() + } + + @Test + fun testNormalInjectionStillFails() { + val context = BeanContext.run() + try { + context.getBean(C::class.java) + fail("Expected a DependencyInjectionException to be thrown") + } catch (e: DependencyInjectionException) {} + context.close() + } +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/inject/property/BeanWithProperty.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/inject/property/BeanWithProperty.kt new file mode 100644 index 00000000000..1dfeed93488 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/inject/property/BeanWithProperty.kt @@ -0,0 +1,41 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.inject.property + +import io.micronaut.context.annotation.Property +import io.micronaut.core.convert.format.MapFormat +import jakarta.inject.Inject +import jakarta.inject.Singleton + +@Singleton +class BeanWithProperty { + + @set:Inject + @setparam:Property(name="app.string") + var stringParam:String ?= null + + @set:Inject + @setparam:Property(name="app.map") + @setparam:MapFormat(transformation = MapFormat.MapTransformation.FLAT) + var mapParam:Map ?= null + + @Property(name="app.string") + var stringParamTwo:String ?= null + + @Property(name="app.map") + @MapFormat(transformation = MapFormat.MapTransformation.FLAT) + var mapParamTwo:Map ?= null +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/inject/property/ConfigProps.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/inject/property/ConfigProps.kt new file mode 100644 index 00000000000..16e6396507b --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/inject/property/ConfigProps.kt @@ -0,0 +1,36 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.inject.property + +import io.micronaut.context.annotation.ConfigurationProperties +import io.micronaut.core.convert.format.MapFormat + +@ConfigurationProperties("test") +class ConfigProps { + + @setparam:MapFormat(transformation = MapFormat.MapTransformation.FLAT) + var properties: Map? = null + + var otherProperties: Map? = null + + private var setterProperties: Map? = null + + fun setSetterProperties(setterProperties: Map) { + this.setterProperties = setterProperties + } + + fun getSetterProperties() = setterProperties +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/inject/property/MapFormatSpec.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/inject/property/MapFormatSpec.kt new file mode 100644 index 00000000000..6f15c59ded0 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/inject/property/MapFormatSpec.kt @@ -0,0 +1,50 @@ +/* + * Copyright 2017-2019 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.inject.property + +import io.micronaut.context.ApplicationContext +import junit.framework.TestCase +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import kotlin.test.assertTrue + + +class MapFormatSpec { + + @Test + fun testMapFormatOnProperty() { + val context = ApplicationContext.run(mapOf("text.properties.yyy.zzz" to 3, "test.properties.yyy.xxx" to 2, "test.properties.yyy.yyy" to 3)) + val config = context.getBean(ConfigProps::class.java) + assertEquals(config.properties?.get("yyy.xxx"), 2) + context.close() + } + + @Test + fun testMapProperty() { + val context = ApplicationContext.run(mapOf("text.other-properties.yyy.zzz" to 3, "test.other-properties.yyy.xxx" to 2, "test.properties.yyy.yyy" to 3)) + val config = context.getBean(ConfigProps::class.java) + assertTrue(config.otherProperties?.containsKey("yyy") ?: false) + context.close() + } + + @Test + fun testMapPropertySetter() { + val context = ApplicationContext.run(mapOf("text.setter-properties.yyy.zzz" to 3, "test.setter-properties.yyy.xxx" to 2, "test.properties.yyy.yyy" to 3)) + val config = context.getBean(ConfigProps::class.java) + assertTrue(config.getSetterProperties()?.containsKey("yyy") ?: false) + context.close() + } +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/inject/property/PropertyInjectSpec.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/inject/property/PropertyInjectSpec.kt new file mode 100644 index 00000000000..4b93a412c83 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/inject/property/PropertyInjectSpec.kt @@ -0,0 +1,18 @@ +package io.micronaut.inject.property + +import io.micronaut.context.ApplicationContext +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Test + +class PropertyInjectSpec { + + @Test + fun testPropertyInjection() { + val context = ApplicationContext.run(mapOf("app.string" to "Hello", "app.map.yyy.xxx" to 2, "app.map.yyy.yyy" to 3)) + val config = context.getBean(BeanWithProperty::class.java) + Assertions.assertEquals(config.stringParam, "Hello") + Assertions.assertEquals(config.mapParam?.get("yyy.xxx"), "2") + Assertions.assertEquals(config.mapParam?.get("yyy.yyy"), "3") + context.close() + } +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/inject/repeatable/MultipleRequires.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/inject/repeatable/MultipleRequires.kt new file mode 100644 index 00000000000..ba117e9fa7b --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/inject/repeatable/MultipleRequires.kt @@ -0,0 +1,25 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.inject.repeatable + +import io.micronaut.context.annotation.Requirements +import io.micronaut.context.annotation.Requires +import jakarta.inject.Singleton + +@Singleton +@Requirements(Requires(property = "foo"), Requires(property = "bar")) +class MultipleRequires { +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/inject/repeatable/RepeatableSpec.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/inject/repeatable/RepeatableSpec.kt new file mode 100644 index 00000000000..862060a0b71 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/inject/repeatable/RepeatableSpec.kt @@ -0,0 +1,44 @@ +/* + * Copyright 2017-2019 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.inject.repeatable + +import io.micronaut.context.ApplicationContext +import org.junit.jupiter.api.Assertions.assertFalse +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test + +class RepeatableSpec { + + @Test + fun testBeanIsNotAvailable() { + val context = ApplicationContext.run() + assertFalse(context.containsBean(MultipleRequires::class.java)) + context.close() + } + + @Test + fun testBeanIsNotAvailable2() { + val context = ApplicationContext.run(hashMapOf("foo" to "true") as Map) + assertFalse(context.containsBean(MultipleRequires::class.java)) + context.close() + } + + fun testBeanIsAvailable() { + val context = ApplicationContext.run(hashMapOf("foo" to "true", "bar" to "y") as Map) + assertTrue(context.containsBean(MultipleRequires::class.java)) + context.close() + } +} \ No newline at end of file diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/inject/requires/RequiresFuture.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/inject/requires/RequiresFuture.kt new file mode 100644 index 00000000000..80033c9c4ac --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/inject/requires/RequiresFuture.kt @@ -0,0 +1,23 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.inject.requires + +import io.micronaut.context.annotation.Requires +import jakarta.inject.Singleton + +@Singleton +@Requires(sdk = Requires.Sdk.KOTLIN, version = "10.3.70") +class RequiresFuture diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/inject/requires/RequiresOld.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/inject/requires/RequiresOld.kt new file mode 100644 index 00000000000..b2a8889b158 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/inject/requires/RequiresOld.kt @@ -0,0 +1,23 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.inject.requires + +import io.micronaut.context.annotation.Requires +import jakarta.inject.Singleton + +@Singleton +@Requires(sdk = Requires.Sdk.KOTLIN, version = "1.0.0") +class RequiresOld diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/inject/requires/RequiresSdkSpec.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/inject/requires/RequiresSdkSpec.kt new file mode 100644 index 00000000000..7c898ee0bb1 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/inject/requires/RequiresSdkSpec.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2017-2019 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.inject.requires + +import io.micronaut.context.ApplicationContext +import junit.framework.TestCase +import org.junit.jupiter.api.Assertions.assertFalse +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test + +class RequiresSdkSpec { + + @Test + fun testRequiresKotlinSDKworks() { + val context = ApplicationContext.run() + assertFalse(context.containsBean(RequiresFuture::class.java)) + assertTrue(context.containsBean(RequiresOld::class.java)) + context.close() + } + +} \ No newline at end of file diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/inject/visitor/beans/BeanIntrospectorSpec.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/inject/visitor/beans/BeanIntrospectorSpec.kt new file mode 100644 index 00000000000..d1c2b774389 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/inject/visitor/beans/BeanIntrospectorSpec.kt @@ -0,0 +1,38 @@ +package io.micronaut.inject.visitor.beans + +import io.micronaut.core.beans.BeanIntrospector +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.Test + +class BeanIntrospectorSpec { + + @Test + fun testGetIntrospection() { + val introspection = BeanIntrospector.SHARED.getIntrospection(TestBean::class.java) + + assertEquals(5, introspection.propertyNames.size) + assertTrue(introspection.getProperty("age").isPresent) + assertTrue(introspection.getProperty("name").isPresent) + + val testBean = introspection.instantiate("fred", 10, arrayOf("one")) + + assertEquals("fred", testBean.name) + assertFalse(testBean.flag) + + try { + introspection.getProperty("name").get().set(testBean, "bob") + fail("Should have failed with unsupported operation, readonly") + } catch (e: UnsupportedOperationException) { + } + + assertEquals("default", testBean.stuff) + + introspection.getProperty("stuff").get().set(testBean, "newvalue") + introspection.getProperty("flag").get().set(testBean, true) + assertEquals(true, introspection.getProperty("flag", Boolean::class.java).get().get(testBean)) + + assertEquals("newvalue", testBean.stuff) + } + + +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/inject/visitor/beans/TestBean.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/inject/visitor/beans/TestBean.kt new file mode 100644 index 00000000000..7fa69c4ad4e --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/inject/visitor/beans/TestBean.kt @@ -0,0 +1,27 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.inject.visitor.beans + +import io.micronaut.core.annotation.Introspected + +@Introspected +data class TestBean( + val name : String, + val age : Int, + val stringArray : Array) { + var stuff : String = "default" + var flag : Boolean = false +} \ No newline at end of file diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/retry/MyCustomException.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/retry/MyCustomException.kt new file mode 100644 index 00000000000..076befdd85e --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/retry/MyCustomException.kt @@ -0,0 +1,20 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.retry + +import java.lang.RuntimeException + +class MyCustomException: RuntimeException() \ No newline at end of file diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/retry/RetrySpec.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/retry/RetrySpec.kt new file mode 100644 index 00000000000..8e3320cf40d --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/retry/RetrySpec.kt @@ -0,0 +1,96 @@ +package io.micronaut.retry + +import io.micronaut.context.ApplicationContext +import io.micronaut.retry.annotation.Retryable +import io.micronaut.retry.event.RetryEvent +import io.micronaut.retry.event.RetryEventListener +import jakarta.inject.Singleton +import org.junit.jupiter.api.Test +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith + +class RetrySpec { + + @Test + fun testRetryWithIncludes() { + val context = ApplicationContext.run() + val counterService = context.getBean(CounterService::class.java) + + assertFailsWith(IllegalStateException::class) { + counterService.getCountIncludes(true) + } + assertEquals(counterService.countIncludes, 1) + + counterService.getCountIncludes(false) + + assertEquals(counterService.countIncludes, counterService.countThreshold) + + context.stop() + } + + @Test + fun testRetryWithExcludes() { + val context = ApplicationContext.run() + val counterService = context.getBean(CounterService::class.java) + + assertFailsWith(MyCustomException::class) { + counterService.getCountExcludes(false) + } + + assertEquals(counterService.countExcludes, 1) + + counterService.getCountExcludes(true) + + assertEquals(counterService.countExcludes, counterService.countThreshold) + + context.stop() + } + + @Singleton + class MyRetryListener : RetryEventListener { + + val events: ArrayList = ArrayList() + + fun reset() { + events.clear() + } + + override fun onApplicationEvent(event: RetryEvent) { + events.add(event) + } + } + + @Singleton + open class CounterService { + + var countIncludes = 0 + var countExcludes = 0 + var countThreshold = 3 + + @Retryable(attempts = "5", delay = "5ms", includes = [MyCustomException::class]) + open fun getCountIncludes(illegalState: Boolean): Int { + countIncludes++ + if(countIncludes < countThreshold) { + if (illegalState) { + throw IllegalStateException("Bad count") + } else { + throw MyCustomException() + } + } + return countIncludes + } + + @Retryable(attempts = "5", delay = "5ms", excludes = [MyCustomException::class]) + open fun getCountExcludes(illegalState: Boolean): Int { + countExcludes++ + if(countExcludes < countThreshold) { + if (illegalState) { + throw IllegalStateException("Bad count") + } else { + throw MyCustomException() + } + } + return countExcludes + } + } +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/runtime/event/EventListenerContract.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/runtime/event/EventListenerContract.kt new file mode 100644 index 00000000000..8f22caa04a1 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/runtime/event/EventListenerContract.kt @@ -0,0 +1,9 @@ +package io.micronaut.runtime.event + +import io.micronaut.runtime.event.annotation.EventListener + +interface EventListenerContract { + + @EventListener + fun doOnEvent(myEvent: MyEvent) +} \ No newline at end of file diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/runtime/event/EventListenerImpl.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/runtime/event/EventListenerImpl.kt new file mode 100644 index 00000000000..3d3b6c54ebd --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/runtime/event/EventListenerImpl.kt @@ -0,0 +1,13 @@ +package io.micronaut.runtime.event + +import jakarta.inject.Singleton + +@Singleton +class EventListenerImpl : EventListenerContract { + + var called = false + + override fun doOnEvent(myEvent: MyEvent) { + called = true + } +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/runtime/event/EventListenerSpec.groovy b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/runtime/event/EventListenerSpec.groovy new file mode 100644 index 00000000000..2eab565cea9 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/runtime/event/EventListenerSpec.groovy @@ -0,0 +1,25 @@ +package io.micronaut.runtime.event + +import io.micronaut.context.ApplicationContext +import org.junit.jupiter.api.Test +import spock.lang.Specification + +import static org.junit.jupiter.api.Assertions.assertFalse +import static org.junit.jupiter.api.Assertions.assertTrue + +class EventListenerSpec { + + @Test + void testImplementingAnInterfaceWithEventListener() { + ApplicationContext ctx = ApplicationContext.run() + EventListenerImpl impl = ctx.getBean(EventListenerImpl) + + assertFalse(impl.called) + + ctx.publishEvent(new MyEvent("")) + + assertTrue(impl.called) + + ctx.close() + } +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/runtime/event/MyEvent.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/runtime/event/MyEvent.kt new file mode 100644 index 00000000000..9f9e1b0c77a --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/runtime/event/MyEvent.kt @@ -0,0 +1,5 @@ +package io.micronaut.runtime.event + +import io.micronaut.context.event.ApplicationEvent + +class MyEvent(source: Any) : ApplicationEvent(source) \ No newline at end of file diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/scheduling/instrument/CoroutineController.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/scheduling/instrument/CoroutineController.kt new file mode 100644 index 00000000000..d13fd9cd5ea --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/scheduling/instrument/CoroutineController.kt @@ -0,0 +1,56 @@ +package io.micronaut.scheduling.instrument + +import io.micronaut.http.annotation.Controller +import io.micronaut.http.annotation.Get +import io.micronaut.http.annotation.QueryValue +import io.micronaut.scheduling.TaskExecutors +import io.reactivex.Observable +import io.reactivex.schedulers.Schedulers +import jakarta.inject.Named +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Runnable +import kotlinx.coroutines.async +import kotlinx.coroutines.future.asCompletableFuture +import kotlinx.coroutines.rx2.await +import java.util.concurrent.CompletableFuture +import java.util.concurrent.ExecutorService +import kotlin.coroutines.CoroutineContext + +typealias TokenDetail = String +@Controller +class Controller(@Named(TaskExecutors.IO) private val executorService: ExecutorService) : CoroutineScope { + + override val coroutineContext: CoroutineContext = object : CoroutineDispatcher() { + override fun dispatch(context: CoroutineContext, block: Runnable) { + executorService.execute(block) + } + + } + + val stream: Observable by lazy { + requestNextToken(0).replay(1).autoConnect() + } + + fun current() = stream.take(1).singleOrError()!! + + private fun requestNextToken(idx: Long): Observable { + return Observable.just(idx).map { + Thread.sleep(5000) + "idx + $it" + }.subscribeOn(Schedulers.io()) + } + + @Get("/tryout/{times}") + fun tryout(@QueryValue("times") times: Int) = asyncResult { + (1..times).map { + async { current().await() } + }.map { + it.await() + } + } + + private fun asyncResult(block: suspend CoroutineScope.() -> T): CompletableFuture { + return async { block() }.asCompletableFuture() + } +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/scheduling/instrument/MultipleInvocationInstrumenterSpec.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/scheduling/instrument/MultipleInvocationInstrumenterSpec.kt new file mode 100644 index 00000000000..bc671e1e251 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/scheduling/instrument/MultipleInvocationInstrumenterSpec.kt @@ -0,0 +1,31 @@ +package io.micronaut.scheduling.instrument + +import io.micronaut.context.annotation.Property +import io.micronaut.http.HttpRequest +import io.micronaut.http.client.HttpClient +import io.micronaut.http.client.annotation.Client +import io.micronaut.test.extensions.junit5.annotation.MicronautTest +import org.junit.jupiter.api.Test +import jakarta.inject.Inject +import reactor.core.publisher.Flux +import kotlin.test.assertTrue + +@MicronautTest +@Property(name = "tracing.zipkin.enabled", value = "true") +class MultipleInvocationInstrumenterSpec { + + @Inject + @field:Client("/") + lateinit var client : HttpClient; + + @Test + fun testMultipleInvocationInstrumenter() { + val map: List<*> = Flux.from(client + .retrieve( + HttpRequest.GET("/tryout/100"), + MutableList::class.java + )).blockFirst() + + assertTrue(map.isNotEmpty()) + } +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/validation/validator/Person.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/validation/validator/Person.kt new file mode 100644 index 00000000000..1b3da56f2c6 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/validation/validator/Person.kt @@ -0,0 +1,26 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.validation.validator + +import io.micronaut.core.annotation.Introspected +import javax.validation.constraints.Min +import javax.validation.constraints.NotBlank + +@Introspected +data class Person( + @NotBlank var name: String, + @Min(18) var age: Int +) \ No newline at end of file diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/validation/validator/ValidatorSpec.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/validation/validator/ValidatorSpec.kt new file mode 100644 index 00000000000..e29239dd2b6 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/validation/validator/ValidatorSpec.kt @@ -0,0 +1,36 @@ +package io.micronaut.validation.validator + +import io.micronaut.context.ApplicationContext +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.fail +import javax.validation.ConstraintViolationException + +class ValidatorSpec { + + @Test + fun testValidateInstance() { + val context = ApplicationContext.run() + val validator = context.getBean(Validator::class.java) + + val person = Person("", 10) + val violations = validator.validate(person) +// TODO: currently fails because bean introspection API does not handle data classes +// assertEquals(2, violations.size) + context.close() + } + + @Test + fun testValidateNew() { + val context = ApplicationContext.run() + val validator = context.getBean(Validator::class.java).forExecutables() + + try { + val person = validator.createValid(Person::class.java, "", 10) + fail("should have failed with validation errors") + } catch (e: ConstraintViolationException) { + assertEquals(2, e.constraintViolations.size) + } + context.close() + } +} \ No newline at end of file diff --git a/test-suite-kotlin-ksp/src/test/kotlin/org/atinject/jakartatck/auto/Car.kt b/test-suite-kotlin-ksp/src/test/kotlin/org/atinject/jakartatck/auto/Car.kt new file mode 100644 index 00000000000..bb3fb890cd6 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/org/atinject/jakartatck/auto/Car.kt @@ -0,0 +1,18 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.atinject.jakartatck.auto + +interface Car diff --git a/test-suite-kotlin-ksp/src/test/kotlin/org/atinject/jakartatck/auto/Convertible.kt b/test-suite-kotlin-ksp/src/test/kotlin/org/atinject/jakartatck/auto/Convertible.kt new file mode 100644 index 00000000000..5f5a3b0a583 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/org/atinject/jakartatck/auto/Convertible.kt @@ -0,0 +1,538 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.atinject.jakartatck.auto + +import io.micronaut.context.BeanContext +import io.micronaut.context.DefaultBeanContext +import org.atinject.jakartatck.auto.accessories.Cupholder +import org.atinject.jakartatck.auto.accessories.RoundThing +import org.atinject.jakartatck.auto.accessories.SpareTire +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.Test + +import jakarta.inject.Inject +import jakarta.inject.Named +import jakarta.inject.Provider +import org.junit.jupiter.api.TestInstance + +open class Convertible : Car { + + @Inject @field:Drivers internal var driversSeatA: Seat? = null + @Inject @field:Drivers internal var driversSeatB: Seat? = null + @Inject internal var spareTire: SpareTire? = null + @Inject internal var cupholder: Cupholder? = null + @Inject internal var engineProvider: Provider? = null + + private var methodWithZeroParamsInjected: Boolean = false + private var methodWithMultipleParamsInjected: Boolean = false + private var methodWithNonVoidReturnInjected: Boolean = false + + private var constructorPlainSeat: Seat? = null + private var constructorDriversSeat: Seat? = null + private var constructorPlainTire: Tire? = null + private var constructorSpareTire: Tire? = null + private var constructorPlainSeatProvider = nullProvider() + private var constructorDriversSeatProvider = nullProvider() + private var constructorPlainTireProvider = nullProvider() + private var constructorSpareTireProvider = nullProvider() + + @Inject protected var fieldPlainSeat: Seat? = null + @Inject @field:Drivers protected var fieldDriversSeat: Seat? = null + @Inject protected var fieldPlainTire: Tire? = null + @Inject @field:Named("spare") protected var fieldSpareTire: Tire? = null + @Inject protected var fieldPlainSeatProvider = nullProvider() + @Inject @field:Drivers protected var fieldDriversSeatProvider = nullProvider() + @Inject protected var fieldPlainTireProvider = nullProvider() + @Inject @field:Named("spare") protected var fieldSpareTireProvider = nullProvider() + + private var methodPlainSeat: Seat? = null + private var methodDriversSeat: Seat? = null + private var methodPlainTire: Tire? = null + private var methodSpareTire: Tire? = null + private var methodPlainSeatProvider = nullProvider() + private var methodDriversSeatProvider = nullProvider() + private var methodPlainTireProvider = nullProvider() + private var methodSpareTireProvider = nullProvider() + + @Inject internal constructor( + plainSeat: Seat, + @Drivers driversSeat: Seat, + plainTire: Tire, + @Named("spare") spareTire: Tire, + plainSeatProvider: Provider, + @Drivers driversSeatProvider: Provider, + plainTireProvider: Provider, + @Named("spare") spareTireProvider: Provider) { + constructorPlainSeat = plainSeat + constructorDriversSeat = driversSeat + constructorPlainTire = plainTire + constructorSpareTire = spareTire + constructorPlainSeatProvider = plainSeatProvider + constructorDriversSeatProvider = driversSeatProvider + constructorPlainTireProvider = plainTireProvider + constructorSpareTireProvider = spareTireProvider + } + + internal constructor() { + throw AssertionError("Unexpected call to non-injectable constructor") + } + + internal fun setSeat(unused: Seat) { + throw AssertionError("Unexpected call to non-injectable method") + } + + @Inject internal fun injectMethodWithZeroArgs() { + methodWithZeroParamsInjected = true + } + + @Inject internal fun injectMethodWithNonVoidReturn(): String { + methodWithNonVoidReturnInjected = true + return "unused" + } + + @Inject internal fun injectInstanceMethodWithManyArgs( + plainSeat: Seat, + @Drivers driversSeat: Seat, + plainTire: Tire, + @Named("spare") spareTire: Tire, + plainSeatProvider: Provider, + @Drivers driversSeatProvider: Provider, + plainTireProvider: Provider, + @Named("spare") spareTireProvider: Provider) { + methodWithMultipleParamsInjected = true + + methodPlainSeat = plainSeat + methodDriversSeat = driversSeat + methodPlainTire = plainTire + methodSpareTire = spareTire + methodPlainSeatProvider = plainSeatProvider + methodDriversSeatProvider = driversSeatProvider + methodPlainTireProvider = plainTireProvider + methodSpareTireProvider = spareTireProvider + } + + internal class NullProvider : Provider { + + override fun get(): T? { + return null + } + } + + @TestInstance(TestInstance.Lifecycle.PER_CLASS) + class Tests { + + private val context = BeanContext.run() + private val car = context.getBean(Convertible::class.java) + private val cupholder = car.cupholder + private val spareTire = car.spareTire + private val plainTire = car.fieldPlainTire + private val engine = car.engineProvider!!.get() + + // smoke tests: if these fail all bets are off + + @Test + fun testFieldsInjected() { + assertTrue(cupholder != null && spareTire != null) + } + + @Test + fun testProviderReturnedValues() { + assertTrue(engine != null) + } + + // injecting different kinds of members + + @Test + fun testMethodWithZeroParametersInjected() { + assertTrue(car.methodWithZeroParamsInjected) + } + + @Test + fun testMethodWithMultipleParametersInjected() { + assertTrue(car.methodWithMultipleParamsInjected) + } + + @Test + fun testNonVoidMethodInjected() { + assertTrue(car.methodWithNonVoidReturnInjected) + } + + @Test + fun testPublicNoArgsConstructorInjected() { + assertTrue(engine!!.publicNoArgsConstructorInjected) + } + + @Test + fun testSubtypeFieldsInjected() { + assertTrue(spareTire!!.hasSpareTireBeenFieldInjected()) + } + + @Test + fun testSubtypeMethodsInjected() { + assertTrue(spareTire!!.hasSpareTireBeenMethodInjected()) + } + + @Test + fun testSupertypeFieldsInjected() { + assertTrue(spareTire!!.hasTireBeenFieldInjected()) + } + + @Test + fun testSupertypeMethodsInjected() { + assertTrue(spareTire!!.hasTireBeenMethodInjected()) + } + + @Test + fun testTwiceOverriddenMethodInjectedWhenMiddleLacksAnnotation() { + assertTrue(engine!!.overriddenTwiceWithOmissionInMiddleInjected) + } + + // injected values + +/* @Test + fun testQualifiersNotInheritedFromOverriddenMethod() { + assertTrue(engine!!.overriddenMethodInjected) + assertFalse(engine!!.qualifiersInheritedFromOverriddenMethod) + }*/ + + @Test + fun testConstructorInjectionWithValues() { + assertFalse(car.constructorPlainSeat is DriversSeat,"Expected unqualified value") + assertFalse(car.constructorPlainTire is SpareTire,"Expected unqualified value") + assertTrue(car.constructorDriversSeat is DriversSeat,"Expected qualified value") + assertTrue(car.constructorSpareTire is SpareTire,"Expected qualified value") + } + + @Test + fun testFieldInjectionWithValues() { + assertFalse(car.fieldPlainSeat is DriversSeat,"Expected unqualified value") + assertFalse(car.fieldPlainTire is SpareTire,"Expected unqualified value") + assertTrue(car.fieldDriversSeat is DriversSeat,"Expected qualified value") + assertTrue(car.fieldSpareTire is SpareTire,"Expected qualified value") + } + + @Test + fun testMethodInjectionWithValues() { + assertFalse(car.methodPlainSeat is DriversSeat,"Expected unqualified value") + assertFalse(car.methodPlainTire is SpareTire,"Expected unqualified value") + assertTrue(car.methodDriversSeat is DriversSeat,"Expected qualified value") + assertTrue(car.methodSpareTire is SpareTire,"Expected qualified value") + } + + // injected providers + + @Test + fun testConstructorInjectionWithProviders() { + assertFalse(car.constructorPlainSeatProvider.get() is DriversSeat,"Expected unqualified value") + assertFalse(car.constructorPlainTireProvider.get() is SpareTire,"Expected unqualified value") + assertTrue(car.constructorDriversSeatProvider.get() is DriversSeat,"Expected qualified value") + assertTrue(car.constructorSpareTireProvider.get() is SpareTire,"Expected qualified value") + } + + @Test + fun testFieldInjectionWithProviders() { + assertFalse(car.fieldPlainSeatProvider.get() is DriversSeat,"Expected unqualified value") + assertFalse(car.fieldPlainTireProvider.get() is SpareTire,"Expected unqualified value") + assertTrue(car.fieldDriversSeatProvider.get() is DriversSeat,"Expected qualified value") + assertTrue(car.fieldSpareTireProvider.get() is SpareTire,"Expected qualified value") + } + + @Test + fun testMethodInjectionWithProviders() { + assertFalse(car.methodPlainSeatProvider.get() is DriversSeat,"Expected unqualified value") + assertFalse(car.methodPlainTireProvider.get() is SpareTire,"Expected unqualified value") + assertTrue(car.methodDriversSeatProvider.get() is DriversSeat,"Expected qualified value") + assertTrue(car.methodSpareTireProvider.get() is SpareTire,"Expected qualified value") + } + + + // singletons + + @Test + fun testConstructorInjectedProviderYieldsSingleton() { + assertSame(car.constructorPlainSeatProvider.get(), car.constructorPlainSeatProvider.get(),"Expected same value") + } + + @Test + fun testFieldInjectedProviderYieldsSingleton() { + assertSame(car.fieldPlainSeatProvider.get(), car.fieldPlainSeatProvider.get(),"Expected same value") + } + + @Test + fun testMethodInjectedProviderYieldsSingleton() { + assertSame(car.methodPlainSeatProvider.get(), car.methodPlainSeatProvider.get(),"Expected same value") + } + + @Test + fun testCircularlyDependentSingletons() { + // uses provider.get() to get around circular deps + assertSame(cupholder!!.seatProvider.get().cupholder, cupholder) + } + + + // non singletons + @Test + fun testSingletonAnnotationNotInheritedFromSupertype() { + assertNotSame(car.driversSeatA, car.driversSeatB) + } + + @Test + fun testConstructorInjectedProviderYieldsDistinctValues() { + assertNotSame(car.constructorDriversSeatProvider.get(), car.constructorDriversSeatProvider.get(),"Expected distinct values") + assertNotSame(car.constructorPlainTireProvider.get(), car.constructorPlainTireProvider.get(),"Expected distinct values") + assertNotSame(car.constructorSpareTireProvider.get(), car.constructorSpareTireProvider.get(),"Expected distinct values") + } + + @Test + fun testFieldInjectedProviderYieldsDistinctValues() { + assertNotSame(car.fieldDriversSeatProvider.get(), car.fieldDriversSeatProvider.get(),"Expected distinct values") + assertNotSame(car.fieldPlainTireProvider.get(), car.fieldPlainTireProvider.get(),"Expected distinct values") + assertNotSame(car.fieldSpareTireProvider.get(), car.fieldSpareTireProvider.get(),"Expected distinct values") + } + + @Test + fun testMethodInjectedProviderYieldsDistinctValues() { + assertNotSame(car.methodDriversSeatProvider.get(), car.methodDriversSeatProvider.get(),"Expected distinct values") + assertNotSame(car.methodPlainTireProvider.get(), car.methodPlainTireProvider.get(),"Expected distinct values") + assertNotSame(car.methodSpareTireProvider.get(), car.methodSpareTireProvider.get(),"Expected distinct values") + } + + + // mix inheritance + visibility + + @Test + fun testPackagePrivateMethodInjectedDifferentPackages() { + assertTrue(spareTire!!.subPackagePrivateMethodInjected) + //Not valid because in Kotlin it is an override + //assertTrue(spareTire.superPackagePrivateMethodInjected) + } + + @Test + fun testOverriddenProtectedMethodInjection() { + assertTrue(spareTire!!.subProtectedMethodInjected) + assertFalse(spareTire.superProtectedMethodInjected) + } + + @Test + fun testOverriddenPublicMethodNotInjected() { + assertTrue(spareTire!!.subPublicMethodInjected) + assertFalse(spareTire.superPublicMethodInjected) + } + + + // inject in order + + @Test + fun testFieldsInjectedBeforeMethods() { + //Added to assert that fields are injected before methods in Kotlin + assertFalse(plainTire!!.methodInjectedBeforeFields) + //Ignored because fields override in Kotlin + //assertFalse(spareTire!!.methodInjectedBeforeFields) + } + + @Test + fun testSupertypeMethodsInjectedBeforeSubtypeFields() { + // FIXME: difficult to achieve with current design without a significant rewrite or how native properties are handled +// assertFalse(spareTire!!.subtypeFieldInjectedBeforeSupertypeMethods) + } + + @Test + fun testSupertypeMethodInjectedBeforeSubtypeMethods() { + assertFalse(spareTire!!.subtypeMethodInjectedBeforeSupertypeMethods) + } + + + // necessary injections occur + + @Test + fun testPackagePrivateMethodInjectedEvenWhenSimilarMethodLacksAnnotation() { + //Not valid because in Kotlin the method is overridden + //assertTrue(spareTire!!.subPackagePrivateMethodForOverrideInjected) + } + + + // override or similar method without @Inject + + @Test + fun testPrivateMethodNotInjectedWhenSupertypeHasAnnotatedSimilarMethod() { + assertFalse(spareTire!!.superPrivateMethodForOverrideInjected) + } + + @Test + fun testPackagePrivateMethodNotInjectedWhenOverrideLacksAnnotation() { + assertFalse(engine!!.subPackagePrivateMethodForOverrideInjected) + assertFalse(engine.superPackagePrivateMethodForOverrideInjected) + } + + @Test + fun testPackagePrivateMethodNotInjectedWhenSupertypeHasAnnotatedSimilarMethod() { + assertFalse(spareTire!!.superPackagePrivateMethodForOverrideInjected) + } + + @Test + fun testProtectedMethodNotInjectedWhenOverrideNotAnnotated() { + assertFalse(spareTire!!.protectedMethodForOverrideInjected) + } + + @Test + fun testPublicMethodNotInjectedWhenOverrideNotAnnotated() { + assertFalse(spareTire!!.publicMethodForOverrideInjected) + } + + @Test + fun testTwiceOverriddenMethodNotInjectedWhenOverrideLacksAnnotation() { + assertFalse(engine!!.overriddenTwiceWithOmissionInSubclassInjected) + } + + @Test + fun testOverridingMixedWithPackagePrivate2() { + assertTrue(spareTire!!.spareTirePackagePrivateMethod2Injected) + //Not valid in Kotlin because the method is overridden + //assertTrue((spareTire as Tire).tirePackagePrivateMethod2Injected) + assertFalse((spareTire as RoundThing).roundThingPackagePrivateMethod2Injected) + + assertTrue(plainTire!!.tirePackagePrivateMethod2Injected) + //Not valid in Kotlin because the method is overridden + //assertTrue((plainTire as RoundThing).roundThingPackagePrivateMethod2Injected) + } + + @Test + fun testOverridingMixedWithPackagePrivate3() { + assertFalse(spareTire!!.spareTirePackagePrivateMethod3Injected) + //Not valid in Kotlin because the method is overridden + //assertTrue((spareTire as Tire).tirePackagePrivateMethod3Injected) + assertFalse((spareTire as RoundThing).roundThingPackagePrivateMethod3Injected) + + assertTrue(plainTire!!.tirePackagePrivateMethod3Injected) + //Not valid in Kotlin because the method is overridden + //assertTrue((plainTire as RoundThing).roundThingPackagePrivateMethod3Injected) + } + + @Test + fun testOverridingMixedWithPackagePrivate4() { + assertFalse(plainTire!!.tirePackagePrivateMethod4Injected) + //Not the same as Java because package private can be overridden by any subclass in the project + //assertTrue((plainTire as RoundThing).roundThingPackagePrivateMethod4Injected) + } + + // inject only once + + @Test + fun testOverriddenPackagePrivateMethodInjectedOnlyOnce() { + assertFalse(engine!!.overriddenPackagePrivateMethodInjectedTwice) + } + + @Test + fun testSimilarPackagePrivateMethodInjectedOnlyOnce() { + assertFalse(spareTire!!.similarPackagePrivateMethodInjectedTwice) + } + + @Test + fun testOverriddenProtectedMethodInjectedOnlyOnce() { + assertFalse(spareTire!!.overriddenProtectedMethodInjectedTwice) + } + + @Test + fun testOverriddenPublicMethodInjectedOnlyOnce() { + assertFalse(spareTire!!.overriddenPublicMethodInjectedTwice) + } + + } + + class PrivateTests { + private val context = DefaultBeanContext().start() + private val car = context.getBean(Convertible::class.java) + private val engine = car.engineProvider!!.get() + private val spareTire = car.spareTire + + @Test + fun testSupertypePrivateMethodInjected() { + assertTrue(spareTire!!.superPrivateMethodInjected) + assertTrue(spareTire.subPrivateMethodInjected) + } + + @Test + fun testPackagePrivateMethodInjectedSamePackage() { + assertTrue(engine.subPackagePrivateMethodInjected) + assertFalse(engine.superPackagePrivateMethodInjected) + } + + @Test + fun testPrivateMethodInjectedEvenWhenSimilarMethodLacksAnnotation() { + assertTrue(spareTire!!.subPrivateMethodForOverrideInjected) + } + + @Test + fun testSimilarPrivateMethodInjectedOnlyOnce() { + assertFalse(spareTire!!.similarPrivateMethodInjectedTwice) + } + } + + companion object { + + @Inject internal var staticFieldPlainSeat: Seat? = null + @Inject + @Drivers internal var staticFieldDriversSeat: Seat? = null + @Inject internal var staticFieldPlainTire: Tire? = null + @Inject + @Named("spare") internal var staticFieldSpareTire: Tire? = null + @Inject internal var staticFieldPlainSeatProvider = nullProvider() + @Inject + @Drivers internal var staticFieldDriversSeatProvider = nullProvider() + @Inject internal var staticFieldPlainTireProvider = nullProvider() + @Inject + @Named("spare") internal var staticFieldSpareTireProvider = nullProvider() + + private var staticMethodPlainSeat: Seat? = null + private var staticMethodDriversSeat: Seat? = null + private var staticMethodPlainTire: Tire? = null + private var staticMethodSpareTire: Tire? = null + private var staticMethodPlainSeatProvider = nullProvider() + private var staticMethodDriversSeatProvider = nullProvider() + private var staticMethodPlainTireProvider = nullProvider() + private var staticMethodSpareTireProvider = nullProvider() + + @Inject internal fun injectStaticMethodWithManyArgs( + plainSeat: Seat, + @Drivers driversSeat: Seat, + plainTire: Tire, + @Named("spare") spareTire: Tire, + plainSeatProvider: Provider, + @Drivers driversSeatProvider: Provider, + plainTireProvider: Provider, + @Named("spare") spareTireProvider: Provider) { + staticMethodPlainSeat = plainSeat + staticMethodDriversSeat = driversSeat + staticMethodPlainTire = plainTire + staticMethodSpareTire = spareTire + staticMethodPlainSeatProvider = plainSeatProvider + staticMethodDriversSeatProvider = driversSeatProvider + staticMethodPlainTireProvider = plainTireProvider + staticMethodSpareTireProvider = spareTireProvider + + } + + /** + * Returns a provider that always returns null. This is used as a default + * value to avoid null checks for omitted provider injections. + */ + private fun nullProvider(): Provider { + return NullProvider() + } + + var localConvertible = ThreadLocal() + } +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/org/atinject/jakartatck/auto/Drivers.kt b/test-suite-kotlin-ksp/src/test/kotlin/org/atinject/jakartatck/auto/Drivers.kt new file mode 100644 index 00000000000..c816f241569 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/org/atinject/jakartatck/auto/Drivers.kt @@ -0,0 +1,22 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.atinject.jakartatck.auto + +import jakarta.inject.Qualifier + +@Retention(AnnotationRetention.RUNTIME) +@Qualifier +annotation class Drivers diff --git a/test-suite-kotlin-ksp/src/test/kotlin/org/atinject/jakartatck/auto/DriversSeat.kt b/test-suite-kotlin-ksp/src/test/kotlin/org/atinject/jakartatck/auto/DriversSeat.kt new file mode 100644 index 00000000000..bdfb93c8128 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/org/atinject/jakartatck/auto/DriversSeat.kt @@ -0,0 +1,23 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.atinject.jakartatck.auto + +import org.atinject.jakartatck.auto.accessories.Cupholder + +import jakarta.inject.Inject + +open class DriversSeat @Inject +constructor(cupholder: Cupholder) : Seat(cupholder) diff --git a/test-suite-kotlin-ksp/src/test/kotlin/org/atinject/jakartatck/auto/Engine.kt b/test-suite-kotlin-ksp/src/test/kotlin/org/atinject/jakartatck/auto/Engine.kt new file mode 100644 index 00000000000..539250e801b --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/org/atinject/jakartatck/auto/Engine.kt @@ -0,0 +1,72 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.atinject.jakartatck.auto + +import org.atinject.jakartatck.auto.accessories.SpareTire + +import jakarta.inject.Inject +import jakarta.inject.Named + +abstract class Engine { + + var publicNoArgsConstructorInjected: Boolean = false + var subPackagePrivateMethodInjected: Boolean = false + var superPackagePrivateMethodInjected: Boolean = false + var subPackagePrivateMethodForOverrideInjected: Boolean = false + var superPackagePrivateMethodForOverrideInjected: Boolean = false + + var overriddenTwiceWithOmissionInMiddleInjected: Boolean = false + var overriddenTwiceWithOmissionInSubclassInjected: Boolean = false + + protected var seatA: Seat? = null + protected var seatB: Seat? = null + protected var tireA: Tire? = null + protected var tireB: Tire? = null + + var overriddenPackagePrivateMethodInjectedTwice: Boolean = false + var qualifiersInheritedFromOverriddenMethod: Boolean = false + var overriddenMethodInjected: Boolean = false + + @Inject internal open fun injectPackagePrivateMethod() { + superPackagePrivateMethodInjected = true + } + + @Inject internal open fun injectPackagePrivateMethodForOverride() { + superPackagePrivateMethodForOverrideInjected = true + } + + @Inject + open fun injectQualifiers(@Drivers seatA: Seat, seatB: Seat, + @Named("spare") tireA: Tire, tireB: Tire) { + overriddenMethodInjected = true + if (seatA !is DriversSeat + || seatB is DriversSeat + || tireA !is SpareTire + || tireB is SpareTire) { + qualifiersInheritedFromOverriddenMethod = true + } + } + + @Inject + open fun injectTwiceOverriddenWithOmissionInMiddle() { + overriddenTwiceWithOmissionInMiddleInjected = true + } + + @Inject + open fun injectTwiceOverriddenWithOmissionInSubclass() { + overriddenTwiceWithOmissionInSubclassInjected = true + } +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/org/atinject/jakartatck/auto/FuelTank.kt b/test-suite-kotlin-ksp/src/test/kotlin/org/atinject/jakartatck/auto/FuelTank.kt new file mode 100644 index 00000000000..fac49e3e6e1 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/org/atinject/jakartatck/auto/FuelTank.kt @@ -0,0 +1,21 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.atinject.jakartatck.auto + +import jakarta.inject.Singleton + +@Singleton +class FuelTank diff --git a/test-suite-kotlin-ksp/src/test/kotlin/org/atinject/jakartatck/auto/GasEngine.kt b/test-suite-kotlin-ksp/src/test/kotlin/org/atinject/jakartatck/auto/GasEngine.kt new file mode 100644 index 00000000000..bf8824eee91 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/org/atinject/jakartatck/auto/GasEngine.kt @@ -0,0 +1,30 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.atinject.jakartatck.auto + +import jakarta.inject.Inject + +abstract class GasEngine : Engine() { + + override fun injectTwiceOverriddenWithOmissionInMiddle() { + overriddenTwiceWithOmissionInMiddleInjected = true + } + + @Inject + override fun injectTwiceOverriddenWithOmissionInSubclass() { + overriddenTwiceWithOmissionInSubclassInjected = true + } +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/org/atinject/jakartatck/auto/Seat.kt b/test-suite-kotlin-ksp/src/test/kotlin/org/atinject/jakartatck/auto/Seat.kt new file mode 100644 index 00000000000..85121f696fb --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/org/atinject/jakartatck/auto/Seat.kt @@ -0,0 +1,25 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.atinject.jakartatck.auto + +import org.atinject.jakartatck.auto.accessories.Cupholder + +import jakarta.inject.Inject +import jakarta.inject.Singleton + +@Singleton +open class Seat @Inject +internal constructor(val cupholder: Cupholder) diff --git a/test-suite-kotlin-ksp/src/test/kotlin/org/atinject/jakartatck/auto/Seatbelt.kt b/test-suite-kotlin-ksp/src/test/kotlin/org/atinject/jakartatck/auto/Seatbelt.kt new file mode 100644 index 00000000000..76155816726 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/org/atinject/jakartatck/auto/Seatbelt.kt @@ -0,0 +1,18 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.atinject.jakartatck.auto + +class Seatbelt diff --git a/test-suite-kotlin-ksp/src/test/kotlin/org/atinject/jakartatck/auto/Tire.kt b/test-suite-kotlin-ksp/src/test/kotlin/org/atinject/jakartatck/auto/Tire.kt new file mode 100644 index 00000000000..af147c5f853 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/org/atinject/jakartatck/auto/Tire.kt @@ -0,0 +1,186 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.atinject.jakartatck.auto + +import org.atinject.jakartatck.auto.accessories.RoundThing +import org.atinject.jakartatck.auto.accessories.SpareTire + +import jakarta.inject.Inject +import java.util.LinkedHashSet + +open class Tire @Inject +constructor(constructorInjection: FuelTank) : RoundThing() { + + internal open var constructorInjection = NEVER_INJECTED + @Inject protected open var fieldInjection = NEVER_INJECTED + internal open var methodInjection = NEVER_INJECTED + + internal var constructorInjected: Boolean = false + + var superPrivateMethodInjected: Boolean = false + var superPackagePrivateMethodInjected: Boolean = false + var superProtectedMethodInjected: Boolean = false + var superPublicMethodInjected: Boolean = false + var subPrivateMethodInjected: Boolean = false + var subPackagePrivateMethodInjected: Boolean = false + var subProtectedMethodInjected: Boolean = false + var subPublicMethodInjected: Boolean = false + + var superPrivateMethodForOverrideInjected: Boolean = false + var superPackagePrivateMethodForOverrideInjected: Boolean = false + var subPrivateMethodForOverrideInjected: Boolean = false + var subPackagePrivateMethodForOverrideInjected: Boolean = false + var protectedMethodForOverrideInjected: Boolean = false + var publicMethodForOverrideInjected: Boolean = false + + var methodInjectedBeforeFields: Boolean = false + var subtypeFieldInjectedBeforeSupertypeMethods: Boolean = false + var subtypeMethodInjectedBeforeSupertypeMethods: Boolean = false + var similarPrivateMethodInjectedTwice: Boolean = false + var similarPackagePrivateMethodInjectedTwice: Boolean = false + var overriddenProtectedMethodInjectedTwice: Boolean = false + var overriddenPublicMethodInjectedTwice: Boolean = false + + var tirePackagePrivateMethod2Injected: Boolean = false + + var tirePackagePrivateMethod3Injected: Boolean = false + + var tirePackagePrivateMethod4Injected: Boolean = false + + init { + this.constructorInjection = constructorInjection + } + + @Inject internal fun supertypeMethodInjection(methodInjection: FuelTank) { + if (!hasTireBeenFieldInjected()) { + methodInjectedBeforeFields = true + } + if (hasSpareTireBeenFieldInjected()) { + subtypeFieldInjectedBeforeSupertypeMethods = true + } + if (hasSpareTireBeenMethodInjected()) { + subtypeMethodInjectedBeforeSupertypeMethods = true + } + this.methodInjection = methodInjection + } + + @Inject private fun injectPrivateMethod() { + if (superPrivateMethodInjected) { + similarPrivateMethodInjectedTwice = true + } + superPrivateMethodInjected = true + } + + @Inject internal open fun injectPackagePrivateMethod() { + if (superPackagePrivateMethodInjected) { + similarPackagePrivateMethodInjectedTwice = true + } + superPackagePrivateMethodInjected = true + } + + @Inject protected open fun injectProtectedMethod() { + if (superProtectedMethodInjected) { + overriddenProtectedMethodInjectedTwice = true + } + superProtectedMethodInjected = true + } + + @Inject + open fun injectPublicMethod() { + if (superPublicMethodInjected) { + overriddenPublicMethodInjectedTwice = true + } + superPublicMethodInjected = true + } + + @Inject private fun injectPrivateMethodForOverride() { + subPrivateMethodForOverrideInjected = true + } + + @Inject internal open fun injectPackagePrivateMethodForOverride() { + subPackagePrivateMethodForOverrideInjected = true + } + + @Inject protected open fun injectProtectedMethodForOverride() { + protectedMethodForOverrideInjected = true + } + + @Inject + open fun injectPublicMethodForOverride() { + publicMethodForOverrideInjected = true + } + + fun hasTireBeenFieldInjected(): Boolean { + return fieldInjection != NEVER_INJECTED + } + + protected open fun hasSpareTireBeenFieldInjected(): Boolean { + return false + } + + fun hasTireBeenMethodInjected(): Boolean { + return methodInjection != NEVER_INJECTED + } + + protected open fun hasSpareTireBeenMethodInjected(): Boolean { + return false + } + + @Inject override fun injectPackagePrivateMethod2() { + tirePackagePrivateMethod2Injected = true + } + + @Inject override fun injectPackagePrivateMethod3() { + tirePackagePrivateMethod3Injected = true + } + + override fun injectPackagePrivateMethod4() { + tirePackagePrivateMethod4Injected = true + } + + companion object { + + val NEVER_INJECTED = FuelTank() + + protected val moreProblems: Set = LinkedHashSet() + @Inject internal var staticFieldInjection = NEVER_INJECTED + internal var staticMethodInjection = NEVER_INJECTED + var staticMethodInjectedBeforeStaticFields: Boolean = false + var subtypeStaticFieldInjectedBeforeSupertypeStaticMethods: Boolean = false + var subtypeStaticMethodInjectedBeforeSupertypeStaticMethods: Boolean = false + + @Inject internal fun supertypeStaticMethodInjection(methodInjection: FuelTank) { + if (!Tire.hasBeenStaticFieldInjected()) { + staticMethodInjectedBeforeStaticFields = true + } + if (SpareTire.hasBeenStaticFieldInjected()) { + subtypeStaticFieldInjectedBeforeSupertypeStaticMethods = true + } + if (SpareTire.hasBeenStaticMethodInjected()) { + subtypeStaticMethodInjectedBeforeSupertypeStaticMethods = true + } + staticMethodInjection = methodInjection + } + + protected fun hasBeenStaticFieldInjected(): Boolean { + return staticFieldInjection != NEVER_INJECTED + } + + protected fun hasBeenStaticMethodInjected(): Boolean { + return staticMethodInjection != NEVER_INJECTED + } + } +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/org/atinject/jakartatck/auto/V8Engine.kt b/test-suite-kotlin-ksp/src/test/kotlin/org/atinject/jakartatck/auto/V8Engine.kt new file mode 100644 index 00000000000..26be32cd607 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/org/atinject/jakartatck/auto/V8Engine.kt @@ -0,0 +1,61 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.atinject.jakartatck.auto + +import org.atinject.jakartatck.auto.accessories.SpareTire + +import jakarta.inject.Inject +import jakarta.inject.Named + +class V8Engine : GasEngine() { + init { + publicNoArgsConstructorInjected = true + } + + @Inject override fun injectPackagePrivateMethod() { + if (subPackagePrivateMethodInjected) { + overriddenPackagePrivateMethodInjectedTwice = true + } + subPackagePrivateMethodInjected = true + } + + /** + * Qualifiers are swapped from how they appear in the superclass. + */ + override fun injectQualifiers(seatA: Seat, @Drivers seatB: Seat, + tireA: Tire, @Named("spare") tireB: Tire) { + overriddenMethodInjected = true + if (seatA is DriversSeat + || seatB !is DriversSeat + || tireA is SpareTire + || tireB !is SpareTire) { + qualifiersInheritedFromOverriddenMethod = true + } + } + + override fun injectPackagePrivateMethodForOverride() { + subPackagePrivateMethodForOverrideInjected = true + } + + @Inject + override fun injectTwiceOverriddenWithOmissionInMiddle() { + overriddenTwiceWithOmissionInMiddleInjected = true + } + + override fun injectTwiceOverriddenWithOmissionInSubclass() { + overriddenTwiceWithOmissionInSubclassInjected = true + } +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/org/atinject/jakartatck/auto/accessories/Cupholder.kt b/test-suite-kotlin-ksp/src/test/kotlin/org/atinject/jakartatck/auto/accessories/Cupholder.kt new file mode 100644 index 00000000000..ce2986107cd --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/org/atinject/jakartatck/auto/accessories/Cupholder.kt @@ -0,0 +1,26 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.atinject.jakartatck.auto.accessories + +import org.atinject.jakartatck.auto.Seat + +import jakarta.inject.Inject +import jakarta.inject.Provider +import jakarta.inject.Singleton + +@Singleton +open class Cupholder @Inject +constructor(val seatProvider: Provider) diff --git a/test-suite-kotlin-ksp/src/test/kotlin/org/atinject/jakartatck/auto/accessories/RoundThing.kt b/test-suite-kotlin-ksp/src/test/kotlin/org/atinject/jakartatck/auto/accessories/RoundThing.kt new file mode 100644 index 00000000000..5e0e2c8242d --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/org/atinject/jakartatck/auto/accessories/RoundThing.kt @@ -0,0 +1,43 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.atinject.jakartatck.auto.accessories + + +import jakarta.inject.Inject + +open class RoundThing { + + var roundThingPackagePrivateMethod2Injected: Boolean = false + private set + + var roundThingPackagePrivateMethod3Injected: Boolean = false + private set + + var roundThingPackagePrivateMethod4Injected: Boolean = false + private set + + @Inject open internal fun injectPackagePrivateMethod2() { + roundThingPackagePrivateMethod2Injected = true + } + + @Inject open internal fun injectPackagePrivateMethod3() { + roundThingPackagePrivateMethod3Injected = true + } + + @Inject open internal fun injectPackagePrivateMethod4() { + roundThingPackagePrivateMethod4Injected = true + } +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/org/atinject/jakartatck/auto/accessories/SpareTire.kt b/test-suite-kotlin-ksp/src/test/kotlin/org/atinject/jakartatck/auto/accessories/SpareTire.kt new file mode 100644 index 00000000000..54521b06063 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/org/atinject/jakartatck/auto/accessories/SpareTire.kt @@ -0,0 +1,128 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.atinject.jakartatck.auto.accessories + +import org.atinject.jakartatck.auto.FuelTank +import org.atinject.jakartatck.auto.Tire + +import jakarta.inject.Inject + +open class SpareTire @Inject +constructor(forSupertype: FuelTank, forSubtype: FuelTank) : Tire(forSupertype) { + + override var constructorInjection = Tire.NEVER_INJECTED + @Inject override var fieldInjection = Tire.NEVER_INJECTED + override var methodInjection = Tire.NEVER_INJECTED + + var spareTirePackagePrivateMethod2Injected: Boolean = false + private set + + var spareTirePackagePrivateMethod3Injected: Boolean = false + private set + + init { + this.constructorInjection = forSubtype + } + + @Inject internal fun subtypeMethodInjection(methodInjection: FuelTank) { + if (!hasSpareTireBeenFieldInjected()) { + methodInjectedBeforeFields = true + } + this.methodInjection = methodInjection + } + + @Inject private fun injectPrivateMethod() { + if (subPrivateMethodInjected) { + similarPrivateMethodInjectedTwice = true + } + subPrivateMethodInjected = true + } + + @Inject override fun injectPackagePrivateMethod() { + if (subPackagePrivateMethodInjected) { + similarPackagePrivateMethodInjectedTwice = true + } + subPackagePrivateMethodInjected = true + } + + @Inject override fun injectProtectedMethod() { + if (subProtectedMethodInjected) { + overriddenProtectedMethodInjectedTwice = true + } + subProtectedMethodInjected = true + } + + @Inject + override fun injectPublicMethod() { + if (subPublicMethodInjected) { + overriddenPublicMethodInjectedTwice = true + } + subPublicMethodInjected = true + } + + private fun injectPrivateMethodForOverride() { + superPrivateMethodForOverrideInjected = true + } + + override fun injectPackagePrivateMethodForOverride() { + superPackagePrivateMethodForOverrideInjected = true + } + + override fun injectProtectedMethodForOverride() { + protectedMethodForOverrideInjected = true + } + + override fun injectPublicMethodForOverride() { + publicMethodForOverrideInjected = true + } + + public override fun hasSpareTireBeenFieldInjected(): Boolean { + return fieldInjection !== Tire.NEVER_INJECTED + } + + @Override + public override fun hasSpareTireBeenMethodInjected(): Boolean { + return methodInjection !== Tire.NEVER_INJECTED + } + + @Inject override fun injectPackagePrivateMethod2() { + spareTirePackagePrivateMethod2Injected = true + } + + override fun injectPackagePrivateMethod3() { + spareTirePackagePrivateMethod3Injected = true + } + + companion object { + @Inject internal var staticFieldInjection = Tire.NEVER_INJECTED + internal var staticMethodInjection = Tire.NEVER_INJECTED + + @Inject internal fun subtypeStaticMethodInjection(methodInjection: FuelTank) { + if (!hasBeenStaticFieldInjected()) { + Tire.staticMethodInjectedBeforeStaticFields = true + } + staticMethodInjection = methodInjection + } + + fun hasBeenStaticFieldInjected(): Boolean { + return staticFieldInjection !== Tire.NEVER_INJECTED + } + + fun hasBeenStaticMethodInjected(): Boolean { + return staticMethodInjection !== Tire.NEVER_INJECTED + } + } +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/org/atinject/javaxtck/auto/Car.kt b/test-suite-kotlin-ksp/src/test/kotlin/org/atinject/javaxtck/auto/Car.kt new file mode 100644 index 00000000000..132fb2a7202 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/org/atinject/javaxtck/auto/Car.kt @@ -0,0 +1,18 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.atinject.javaxtck.auto + +interface Car diff --git a/test-suite-kotlin-ksp/src/test/kotlin/org/atinject/javaxtck/auto/Convertible.kt b/test-suite-kotlin-ksp/src/test/kotlin/org/atinject/javaxtck/auto/Convertible.kt new file mode 100644 index 00000000000..e99cef5f60b --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/org/atinject/javaxtck/auto/Convertible.kt @@ -0,0 +1,538 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.atinject.javaxtck.auto + +import io.micronaut.context.BeanContext +import io.micronaut.context.DefaultBeanContext +import org.atinject.javaxtck.auto.accessories.Cupholder +import org.atinject.javaxtck.auto.accessories.RoundThing +import org.atinject.javaxtck.auto.accessories.SpareTire +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance + +import javax.inject.Inject +import javax.inject.Named +import javax.inject.Provider + +open class Convertible : Car { + + @Inject @field:Drivers internal var driversSeatA: Seat? = null + @Inject @field:Drivers internal var driversSeatB: Seat? = null + @Inject internal var spareTire: SpareTire? = null + @Inject internal var cupholder: Cupholder? = null + @Inject internal var engineProvider: Provider? = null + + private var methodWithZeroParamsInjected: Boolean = false + private var methodWithMultipleParamsInjected: Boolean = false + private var methodWithNonVoidReturnInjected: Boolean = false + + private var constructorPlainSeat: Seat? = null + private var constructorDriversSeat: Seat? = null + private var constructorPlainTire: Tire? = null + private var constructorSpareTire: Tire? = null + private var constructorPlainSeatProvider = nullProvider() + private var constructorDriversSeatProvider = nullProvider() + private var constructorPlainTireProvider = nullProvider() + private var constructorSpareTireProvider = nullProvider() + + @Inject protected var fieldPlainSeat: Seat? = null + @Inject @field:Drivers protected var fieldDriversSeat: Seat? = null + @Inject protected var fieldPlainTire: Tire? = null + @Inject @field:Named("spare") protected var fieldSpareTire: Tire? = null + @Inject protected var fieldPlainSeatProvider = nullProvider() + @Inject @field:Drivers protected var fieldDriversSeatProvider = nullProvider() + @Inject protected var fieldPlainTireProvider = nullProvider() + @Inject @field:Named("spare") protected var fieldSpareTireProvider = nullProvider() + + private var methodPlainSeat: Seat? = null + private var methodDriversSeat: Seat? = null + private var methodPlainTire: Tire? = null + private var methodSpareTire: Tire? = null + private var methodPlainSeatProvider = nullProvider() + private var methodDriversSeatProvider = nullProvider() + private var methodPlainTireProvider = nullProvider() + private var methodSpareTireProvider = nullProvider() + + @Inject internal constructor( + plainSeat: Seat, + @Drivers driversSeat: Seat, + plainTire: Tire, + @Named("spare") spareTire: Tire, + plainSeatProvider: Provider, + @Drivers driversSeatProvider: Provider, + plainTireProvider: Provider, + @Named("spare") spareTireProvider: Provider) { + constructorPlainSeat = plainSeat + constructorDriversSeat = driversSeat + constructorPlainTire = plainTire + constructorSpareTire = spareTire + constructorPlainSeatProvider = plainSeatProvider + constructorDriversSeatProvider = driversSeatProvider + constructorPlainTireProvider = plainTireProvider + constructorSpareTireProvider = spareTireProvider + } + + internal constructor() { + throw AssertionError("Unexpected call to non-injectable constructor") + } + + internal fun setSeat(unused: Seat) { + throw AssertionError("Unexpected call to non-injectable method") + } + + @Inject internal fun injectMethodWithZeroArgs() { + methodWithZeroParamsInjected = true + } + + @Inject internal fun injectMethodWithNonVoidReturn(): String { + methodWithNonVoidReturnInjected = true + return "unused" + } + + @Inject internal fun injectInstanceMethodWithManyArgs( + plainSeat: Seat, + @Drivers driversSeat: Seat, + plainTire: Tire, + @Named("spare") spareTire: Tire, + plainSeatProvider: Provider, + @Drivers driversSeatProvider: Provider, + plainTireProvider: Provider, + @Named("spare") spareTireProvider: Provider) { + methodWithMultipleParamsInjected = true + + methodPlainSeat = plainSeat + methodDriversSeat = driversSeat + methodPlainTire = plainTire + methodSpareTire = spareTire + methodPlainSeatProvider = plainSeatProvider + methodDriversSeatProvider = driversSeatProvider + methodPlainTireProvider = plainTireProvider + methodSpareTireProvider = spareTireProvider + } + + internal class NullProvider : Provider { + + override fun get(): T? { + return null + } + } + + @TestInstance(TestInstance.Lifecycle.PER_CLASS) + class Tests { + + private val context = BeanContext.run() + private val car = context.getBean(Convertible::class.java) + private val cupholder = car.cupholder + private val spareTire = car.spareTire + private val plainTire = car.fieldPlainTire + private val engine = car.engineProvider!!.get() + + // smoke tests: if these fail all bets are off + + @Test + fun testFieldsInjected() { + assertTrue(cupholder != null && spareTire != null) + } + + @Test + fun testProviderReturnedValues() { + assertTrue(engine != null) + } + + // injecting different kinds of members + + @Test + fun testMethodWithZeroParametersInjected() { + assertTrue(car.methodWithZeroParamsInjected) + } + + @Test + fun testMethodWithMultipleParametersInjected() { + assertTrue(car.methodWithMultipleParamsInjected) + } + + @Test + fun testNonVoidMethodInjected() { + assertTrue(car.methodWithNonVoidReturnInjected) + } + + @Test + fun testPublicNoArgsConstructorInjected() { + assertTrue(engine!!.publicNoArgsConstructorInjected) + } + + @Test + fun testSubtypeFieldsInjected() { + assertTrue(spareTire!!.hasSpareTireBeenFieldInjected()) + } + + @Test + fun testSubtypeMethodsInjected() { + assertTrue(spareTire!!.hasSpareTireBeenMethodInjected()) + } + + @Test + fun testSupertypeFieldsInjected() { + assertTrue(spareTire!!.hasTireBeenFieldInjected()) + } + + @Test + fun testSupertypeMethodsInjected() { + assertTrue(spareTire!!.hasTireBeenMethodInjected()) + } + + @Test + fun testTwiceOverriddenMethodInjectedWhenMiddleLacksAnnotation() { + assertTrue(engine!!.overriddenTwiceWithOmissionInMiddleInjected) + } + + // injected values + +/* @Test + fun testQualifiersNotInheritedFromOverriddenMethod() { + assertTrue(engine!!.overriddenMethodInjected) + assertFalse(engine!!.qualifiersInheritedFromOverriddenMethod) + }*/ + + @Test + fun testConstructorInjectionWithValues() { + assertFalse(car.constructorPlainSeat is DriversSeat,"Expected unqualified value") + assertFalse(car.constructorPlainTire is SpareTire,"Expected unqualified value") + assertTrue(car.constructorDriversSeat is DriversSeat,"Expected qualified value") + assertTrue(car.constructorSpareTire is SpareTire,"Expected qualified value") + } + + @Test + fun testFieldInjectionWithValues() { + assertFalse(car.fieldPlainSeat is DriversSeat,"Expected unqualified value") + assertFalse(car.fieldPlainTire is SpareTire,"Expected unqualified value") + assertTrue(car.fieldDriversSeat is DriversSeat,"Expected qualified value") + assertTrue(car.fieldSpareTire is SpareTire,"Expected qualified value") + } + + @Test + fun testMethodInjectionWithValues() { + assertFalse(car.methodPlainSeat is DriversSeat,"Expected unqualified value") + assertFalse(car.methodPlainTire is SpareTire,"Expected unqualified value") + assertTrue(car.methodDriversSeat is DriversSeat,"Expected qualified value") + assertTrue(car.methodSpareTire is SpareTire,"Expected qualified value") + } + + // injected providers + + @Test + fun testConstructorInjectionWithProviders() { + assertFalse(car.constructorPlainSeatProvider.get() is DriversSeat,"Expected unqualified value") + assertFalse(car.constructorPlainTireProvider.get() is SpareTire,"Expected unqualified value") + assertTrue(car.constructorDriversSeatProvider.get() is DriversSeat,"Expected qualified value") + assertTrue(car.constructorSpareTireProvider.get() is SpareTire,"Expected qualified value") + } + + @Test + fun testFieldInjectionWithProviders() { + assertFalse(car.fieldPlainSeatProvider.get() is DriversSeat,"Expected unqualified value") + assertFalse(car.fieldPlainTireProvider.get() is SpareTire,"Expected unqualified value") + assertTrue(car.fieldDriversSeatProvider.get() is DriversSeat,"Expected qualified value") + assertTrue(car.fieldSpareTireProvider.get() is SpareTire,"Expected qualified value") + } + + @Test + fun testMethodInjectionWithProviders() { + assertFalse(car.methodPlainSeatProvider.get() is DriversSeat,"Expected unqualified value") + assertFalse(car.methodPlainTireProvider.get() is SpareTire,"Expected unqualified value") + assertTrue(car.methodDriversSeatProvider.get() is DriversSeat,"Expected qualified value") + assertTrue(car.methodSpareTireProvider.get() is SpareTire,"Expected qualified value") + } + + + // singletons + + @Test + fun testConstructorInjectedProviderYieldsSingleton() { + assertSame(car.constructorPlainSeatProvider.get(), car.constructorPlainSeatProvider.get(), "Expected same value") + } + + @Test + fun testFieldInjectedProviderYieldsSingleton() { + assertSame(car.fieldPlainSeatProvider.get(), car.fieldPlainSeatProvider.get(), "Expected same value") + } + + @Test + fun testMethodInjectedProviderYieldsSingleton() { + assertSame( + car.methodPlainSeatProvider.get(), car.methodPlainSeatProvider.get(), "Expected same value") + } + + @Test + fun testCircularlyDependentSingletons() { + // uses provider.get() to get around circular deps + assertSame(cupholder!!.seatProvider.get().cupholder, cupholder) + } + + + // non singletons + @Test + fun testSingletonAnnotationNotInheritedFromSupertype() { + assertNotSame(car.driversSeatA, car.driversSeatB) + } + + @Test + fun testConstructorInjectedProviderYieldsDistinctValues() { + assertNotSame(car.constructorDriversSeatProvider.get(), car.constructorDriversSeatProvider.get(), "Expected distinct values") + assertNotSame(car.constructorPlainTireProvider.get(), car.constructorPlainTireProvider.get(), "Expected distinct values") + assertNotSame(car.constructorSpareTireProvider.get(), car.constructorSpareTireProvider.get(), "Expected distinct values") + } + + @Test + fun testFieldInjectedProviderYieldsDistinctValues() { + assertNotSame(car.fieldDriversSeatProvider.get(), car.fieldDriversSeatProvider.get(), "Expected distinct values") + assertNotSame(car.fieldPlainTireProvider.get(), car.fieldPlainTireProvider.get(), "Expected distinct values") + assertNotSame(car.fieldSpareTireProvider.get(), car.fieldSpareTireProvider.get(), "Expected distinct values") + } + + @Test + fun testMethodInjectedProviderYieldsDistinctValues() { + assertNotSame(car.methodDriversSeatProvider.get(), car.methodDriversSeatProvider.get(), "Expected distinct values") + assertNotSame(car.methodPlainTireProvider.get(), car.methodPlainTireProvider.get(), "Expected distinct values") + assertNotSame(car.methodSpareTireProvider.get(), car.methodSpareTireProvider.get(), "Expected distinct values") + } + + + // mix inheritance + visibility + @Test + fun testPackagePrivateMethodInjectedDifferentPackages() { + assertTrue(spareTire!!.subPackagePrivateMethodInjected) + //Not valid because in Kotlin it is an override + //assertTrue(spareTire.superPackagePrivateMethodInjected) + } + + @Test + fun testOverriddenProtectedMethodInjection() { + assertTrue(spareTire!!.subProtectedMethodInjected) + assertFalse(spareTire.superProtectedMethodInjected) + } + + @Test + fun testOverriddenPublicMethodNotInjected() { + assertTrue(spareTire!!.subPublicMethodInjected) + assertFalse(spareTire.superPublicMethodInjected) + } + + + // inject in order + + @Test + fun testFieldsInjectedBeforeMethods() { + //Added to assert that fields are injected before methods in Kotlin + assertFalse(plainTire!!.methodInjectedBeforeFields) + //Ignored because fields override in Kotlin + //assertFalse(spareTire!!.methodInjectedBeforeFields) + } + + @Test + fun testSupertypeMethodsInjectedBeforeSubtypeFields() { + // FIXME: difficult to achieve with current design without a significant rewrite or how native properties are handled +// assertFalse(spareTire!!.subtypeFieldInjectedBeforeSupertypeMethods) + } + + @Test + fun testSupertypeMethodInjectedBeforeSubtypeMethods() { + assertFalse(spareTire!!.subtypeMethodInjectedBeforeSupertypeMethods) + } + + + // necessary injections occur + + @Test + fun testPackagePrivateMethodInjectedEvenWhenSimilarMethodLacksAnnotation() { + //Not valid because in Kotlin the method is overridden + //assertTrue(spareTire!!.subPackagePrivateMethodForOverrideInjected) + } + + + // override or similar method without @Inject + + @Test + fun testPrivateMethodNotInjectedWhenSupertypeHasAnnotatedSimilarMethod() { + assertFalse(spareTire!!.superPrivateMethodForOverrideInjected) + } + + @Test + fun testPackagePrivateMethodNotInjectedWhenOverrideLacksAnnotation() { + assertFalse(engine!!.subPackagePrivateMethodForOverrideInjected) + assertFalse(engine.superPackagePrivateMethodForOverrideInjected) + } + + @Test + fun testPackagePrivateMethodNotInjectedWhenSupertypeHasAnnotatedSimilarMethod() { + assertFalse(spareTire!!.superPackagePrivateMethodForOverrideInjected) + } + + @Test + fun testProtectedMethodNotInjectedWhenOverrideNotAnnotated() { + assertFalse(spareTire!!.protectedMethodForOverrideInjected) + } + + @Test + fun testPublicMethodNotInjectedWhenOverrideNotAnnotated() { + assertFalse(spareTire!!.publicMethodForOverrideInjected) + } + + @Test + fun testTwiceOverriddenMethodNotInjectedWhenOverrideLacksAnnotation() { + assertFalse(engine!!.overriddenTwiceWithOmissionInSubclassInjected) + } + + @Test + fun testOverridingMixedWithPackagePrivate2() { + assertTrue(spareTire!!.spareTirePackagePrivateMethod2Injected) + //Not valid in Kotlin because the method is overridden + //assertTrue((spareTire as Tire).tirePackagePrivateMethod2Injected) + assertFalse((spareTire as RoundThing).roundThingPackagePrivateMethod2Injected) + + assertTrue(plainTire!!.tirePackagePrivateMethod2Injected) + //Not valid in Kotlin because the method is overridden + //assertTrue((plainTire as RoundThing).roundThingPackagePrivateMethod2Injected) + } + + @Test + fun testOverridingMixedWithPackagePrivate3() { + assertFalse(spareTire!!.spareTirePackagePrivateMethod3Injected) + //Not valid in Kotlin because the method is overridden + //assertTrue((spareTire as Tire).tirePackagePrivateMethod3Injected) + assertFalse((spareTire as RoundThing).roundThingPackagePrivateMethod3Injected) + + assertTrue(plainTire!!.tirePackagePrivateMethod3Injected) + //Not valid in Kotlin because the method is overridden + //assertTrue((plainTire as RoundThing).roundThingPackagePrivateMethod3Injected) + } + + @Test + fun testOverridingMixedWithPackagePrivate4() { + assertFalse(plainTire!!.tirePackagePrivateMethod4Injected) + //Not the same as Java because package private can be overridden by any subclass in the project + //assertTrue((plainTire as RoundThing).roundThingPackagePrivateMethod4Injected) + } + + // inject only once + + @Test + fun testOverriddenPackagePrivateMethodInjectedOnlyOnce() { + assertFalse(engine!!.overriddenPackagePrivateMethodInjectedTwice) + } + + @Test + fun testSimilarPackagePrivateMethodInjectedOnlyOnce() { + assertFalse(spareTire!!.similarPackagePrivateMethodInjectedTwice) + } + + @Test + fun testOverriddenProtectedMethodInjectedOnlyOnce() { + assertFalse(spareTire!!.overriddenProtectedMethodInjectedTwice) + } + + @Test + fun testOverriddenPublicMethodInjectedOnlyOnce() { + assertFalse(spareTire!!.overriddenPublicMethodInjectedTwice) + } + + } + + class PrivateTests { + private val context = DefaultBeanContext().start() + private val car = context.getBean(Convertible::class.java) + private val engine = car.engineProvider!!.get() + private val spareTire = car.spareTire + + @Test + fun testSupertypePrivateMethodInjected() { + assertTrue(spareTire!!.superPrivateMethodInjected) + assertTrue(spareTire.subPrivateMethodInjected) + } + + @Test + fun testPackagePrivateMethodInjectedSamePackage() { + assertTrue(engine.subPackagePrivateMethodInjected) + assertFalse(engine.superPackagePrivateMethodInjected) + } + + @Test + fun testPrivateMethodInjectedEvenWhenSimilarMethodLacksAnnotation() { + assertTrue(spareTire!!.subPrivateMethodForOverrideInjected) + } + + @Test + fun testSimilarPrivateMethodInjectedOnlyOnce() { + assertFalse(spareTire!!.similarPrivateMethodInjectedTwice) + } + } + + companion object { + + @Inject internal var staticFieldPlainSeat: Seat? = null + @Inject + @Drivers internal var staticFieldDriversSeat: Seat? = null + @Inject internal var staticFieldPlainTire: Tire? = null + @Inject + @Named("spare") internal var staticFieldSpareTire: Tire? = null + @Inject internal var staticFieldPlainSeatProvider = nullProvider() + @Inject + @Drivers internal var staticFieldDriversSeatProvider = nullProvider() + @Inject internal var staticFieldPlainTireProvider = nullProvider() + @Inject + @Named("spare") internal var staticFieldSpareTireProvider = nullProvider() + + private var staticMethodPlainSeat: Seat? = null + private var staticMethodDriversSeat: Seat? = null + private var staticMethodPlainTire: Tire? = null + private var staticMethodSpareTire: Tire? = null + private var staticMethodPlainSeatProvider = nullProvider() + private var staticMethodDriversSeatProvider = nullProvider() + private var staticMethodPlainTireProvider = nullProvider() + private var staticMethodSpareTireProvider = nullProvider() + + @Inject internal fun injectStaticMethodWithManyArgs( + plainSeat: Seat, + @Drivers driversSeat: Seat, + plainTire: Tire, + @Named("spare") spareTire: Tire, + plainSeatProvider: Provider, + @Drivers driversSeatProvider: Provider, + plainTireProvider: Provider, + @Named("spare") spareTireProvider: Provider) { + staticMethodPlainSeat = plainSeat + staticMethodDriversSeat = driversSeat + staticMethodPlainTire = plainTire + staticMethodSpareTire = spareTire + staticMethodPlainSeatProvider = plainSeatProvider + staticMethodDriversSeatProvider = driversSeatProvider + staticMethodPlainTireProvider = plainTireProvider + staticMethodSpareTireProvider = spareTireProvider + + } + + /** + * Returns a provider that always returns null. This is used as a default + * value to avoid null checks for omitted provider injections. + */ + private fun nullProvider(): Provider { + return NullProvider() + } + + var localConvertible = ThreadLocal() + } +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/org/atinject/javaxtck/auto/Drivers.kt b/test-suite-kotlin-ksp/src/test/kotlin/org/atinject/javaxtck/auto/Drivers.kt new file mode 100644 index 00000000000..5c34e0e605d --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/org/atinject/javaxtck/auto/Drivers.kt @@ -0,0 +1,22 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.atinject.javaxtck.auto + +import javax.inject.Qualifier + +@Retention(AnnotationRetention.RUNTIME) +@Qualifier +annotation class Drivers diff --git a/test-suite-kotlin-ksp/src/test/kotlin/org/atinject/javaxtck/auto/DriversSeat.kt b/test-suite-kotlin-ksp/src/test/kotlin/org/atinject/javaxtck/auto/DriversSeat.kt new file mode 100644 index 00000000000..e77ebe6b5d7 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/org/atinject/javaxtck/auto/DriversSeat.kt @@ -0,0 +1,23 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.atinject.javaxtck.auto + +import org.atinject.javaxtck.auto.accessories.Cupholder + +import javax.inject.Inject + +open class DriversSeat @Inject +constructor(cupholder: Cupholder) : Seat(cupholder) diff --git a/test-suite-kotlin-ksp/src/test/kotlin/org/atinject/javaxtck/auto/Engine.kt b/test-suite-kotlin-ksp/src/test/kotlin/org/atinject/javaxtck/auto/Engine.kt new file mode 100644 index 00000000000..5cf9f9a16f2 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/org/atinject/javaxtck/auto/Engine.kt @@ -0,0 +1,72 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.atinject.javaxtck.auto + +import org.atinject.javaxtck.auto.accessories.SpareTire + +import javax.inject.Inject +import javax.inject.Named + +abstract class Engine { + + var publicNoArgsConstructorInjected: Boolean = false + var subPackagePrivateMethodInjected: Boolean = false + var superPackagePrivateMethodInjected: Boolean = false + var subPackagePrivateMethodForOverrideInjected: Boolean = false + var superPackagePrivateMethodForOverrideInjected: Boolean = false + + var overriddenTwiceWithOmissionInMiddleInjected: Boolean = false + var overriddenTwiceWithOmissionInSubclassInjected: Boolean = false + + protected var seatA: Seat? = null + protected var seatB: Seat? = null + protected var tireA: Tire? = null + protected var tireB: Tire? = null + + var overriddenPackagePrivateMethodInjectedTwice: Boolean = false + var qualifiersInheritedFromOverriddenMethod: Boolean = false + var overriddenMethodInjected: Boolean = false + + @Inject internal open fun injectPackagePrivateMethod() { + superPackagePrivateMethodInjected = true + } + + @Inject internal open fun injectPackagePrivateMethodForOverride() { + superPackagePrivateMethodForOverrideInjected = true + } + + @Inject + open fun injectQualifiers(@Drivers seatA: Seat, seatB: Seat, + @Named("spare") tireA: Tire, tireB: Tire) { + overriddenMethodInjected = true + if (seatA !is DriversSeat + || seatB is DriversSeat + || tireA !is SpareTire + || tireB is SpareTire) { + qualifiersInheritedFromOverriddenMethod = true + } + } + + @Inject + open fun injectTwiceOverriddenWithOmissionInMiddle() { + overriddenTwiceWithOmissionInMiddleInjected = true + } + + @Inject + open fun injectTwiceOverriddenWithOmissionInSubclass() { + overriddenTwiceWithOmissionInSubclassInjected = true + } +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/org/atinject/javaxtck/auto/FuelTank.kt b/test-suite-kotlin-ksp/src/test/kotlin/org/atinject/javaxtck/auto/FuelTank.kt new file mode 100644 index 00000000000..bd3446a7584 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/org/atinject/javaxtck/auto/FuelTank.kt @@ -0,0 +1,21 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.atinject.javaxtck.auto + +import javax.inject.Singleton + +@Singleton +class FuelTank diff --git a/test-suite-kotlin-ksp/src/test/kotlin/org/atinject/javaxtck/auto/GasEngine.kt b/test-suite-kotlin-ksp/src/test/kotlin/org/atinject/javaxtck/auto/GasEngine.kt new file mode 100644 index 00000000000..47e99df64f1 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/org/atinject/javaxtck/auto/GasEngine.kt @@ -0,0 +1,30 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.atinject.javaxtck.auto + +import javax.inject.Inject + +abstract class GasEngine : Engine() { + + override fun injectTwiceOverriddenWithOmissionInMiddle() { + overriddenTwiceWithOmissionInMiddleInjected = true + } + + @Inject + override fun injectTwiceOverriddenWithOmissionInSubclass() { + overriddenTwiceWithOmissionInSubclassInjected = true + } +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/org/atinject/javaxtck/auto/Seat.kt b/test-suite-kotlin-ksp/src/test/kotlin/org/atinject/javaxtck/auto/Seat.kt new file mode 100644 index 00000000000..74eaf9dc040 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/org/atinject/javaxtck/auto/Seat.kt @@ -0,0 +1,25 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.atinject.javaxtck.auto + +import org.atinject.javaxtck.auto.accessories.Cupholder + +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +open class Seat @Inject +internal constructor(val cupholder: Cupholder) diff --git a/test-suite-kotlin-ksp/src/test/kotlin/org/atinject/javaxtck/auto/Seatbelt.kt b/test-suite-kotlin-ksp/src/test/kotlin/org/atinject/javaxtck/auto/Seatbelt.kt new file mode 100644 index 00000000000..20d7344a83c --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/org/atinject/javaxtck/auto/Seatbelt.kt @@ -0,0 +1,18 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.atinject.javaxtck.auto + +class Seatbelt diff --git a/test-suite-kotlin-ksp/src/test/kotlin/org/atinject/javaxtck/auto/Tire.kt b/test-suite-kotlin-ksp/src/test/kotlin/org/atinject/javaxtck/auto/Tire.kt new file mode 100644 index 00000000000..ae612990bd0 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/org/atinject/javaxtck/auto/Tire.kt @@ -0,0 +1,186 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.atinject.javaxtck.auto + +import org.atinject.javaxtck.auto.accessories.RoundThing +import org.atinject.javaxtck.auto.accessories.SpareTire + +import javax.inject.Inject +import java.util.LinkedHashSet + +open class Tire @Inject +constructor(constructorInjection: FuelTank) : RoundThing() { + + internal open var constructorInjection = NEVER_INJECTED + @Inject protected open var fieldInjection = NEVER_INJECTED + internal open var methodInjection = NEVER_INJECTED + + internal var constructorInjected: Boolean = false + + var superPrivateMethodInjected: Boolean = false + var superPackagePrivateMethodInjected: Boolean = false + var superProtectedMethodInjected: Boolean = false + var superPublicMethodInjected: Boolean = false + var subPrivateMethodInjected: Boolean = false + var subPackagePrivateMethodInjected: Boolean = false + var subProtectedMethodInjected: Boolean = false + var subPublicMethodInjected: Boolean = false + + var superPrivateMethodForOverrideInjected: Boolean = false + var superPackagePrivateMethodForOverrideInjected: Boolean = false + var subPrivateMethodForOverrideInjected: Boolean = false + var subPackagePrivateMethodForOverrideInjected: Boolean = false + var protectedMethodForOverrideInjected: Boolean = false + var publicMethodForOverrideInjected: Boolean = false + + var methodInjectedBeforeFields: Boolean = false + var subtypeFieldInjectedBeforeSupertypeMethods: Boolean = false + var subtypeMethodInjectedBeforeSupertypeMethods: Boolean = false + var similarPrivateMethodInjectedTwice: Boolean = false + var similarPackagePrivateMethodInjectedTwice: Boolean = false + var overriddenProtectedMethodInjectedTwice: Boolean = false + var overriddenPublicMethodInjectedTwice: Boolean = false + + var tirePackagePrivateMethod2Injected: Boolean = false + + var tirePackagePrivateMethod3Injected: Boolean = false + + var tirePackagePrivateMethod4Injected: Boolean = false + + init { + this.constructorInjection = constructorInjection + } + + @Inject internal fun supertypeMethodInjection(methodInjection: FuelTank) { + if (!hasTireBeenFieldInjected()) { + methodInjectedBeforeFields = true + } + if (hasSpareTireBeenFieldInjected()) { + subtypeFieldInjectedBeforeSupertypeMethods = true + } + if (hasSpareTireBeenMethodInjected()) { + subtypeMethodInjectedBeforeSupertypeMethods = true + } + this.methodInjection = methodInjection + } + + @Inject private fun injectPrivateMethod() { + if (superPrivateMethodInjected) { + similarPrivateMethodInjectedTwice = true + } + superPrivateMethodInjected = true + } + + @Inject internal open fun injectPackagePrivateMethod() { + if (superPackagePrivateMethodInjected) { + similarPackagePrivateMethodInjectedTwice = true + } + superPackagePrivateMethodInjected = true + } + + @Inject protected open fun injectProtectedMethod() { + if (superProtectedMethodInjected) { + overriddenProtectedMethodInjectedTwice = true + } + superProtectedMethodInjected = true + } + + @Inject + open fun injectPublicMethod() { + if (superPublicMethodInjected) { + overriddenPublicMethodInjectedTwice = true + } + superPublicMethodInjected = true + } + + @Inject private fun injectPrivateMethodForOverride() { + subPrivateMethodForOverrideInjected = true + } + + @Inject internal open fun injectPackagePrivateMethodForOverride() { + subPackagePrivateMethodForOverrideInjected = true + } + + @Inject protected open fun injectProtectedMethodForOverride() { + protectedMethodForOverrideInjected = true + } + + @Inject + open fun injectPublicMethodForOverride() { + publicMethodForOverrideInjected = true + } + + fun hasTireBeenFieldInjected(): Boolean { + return fieldInjection != NEVER_INJECTED + } + + protected open fun hasSpareTireBeenFieldInjected(): Boolean { + return false + } + + fun hasTireBeenMethodInjected(): Boolean { + return methodInjection != NEVER_INJECTED + } + + protected open fun hasSpareTireBeenMethodInjected(): Boolean { + return false + } + + @Inject override fun injectPackagePrivateMethod2() { + tirePackagePrivateMethod2Injected = true + } + + @Inject override fun injectPackagePrivateMethod3() { + tirePackagePrivateMethod3Injected = true + } + + override fun injectPackagePrivateMethod4() { + tirePackagePrivateMethod4Injected = true + } + + companion object { + + val NEVER_INJECTED = FuelTank() + + protected val moreProblems: Set = LinkedHashSet() + @Inject internal var staticFieldInjection = NEVER_INJECTED + internal var staticMethodInjection = NEVER_INJECTED + var staticMethodInjectedBeforeStaticFields: Boolean = false + var subtypeStaticFieldInjectedBeforeSupertypeStaticMethods: Boolean = false + var subtypeStaticMethodInjectedBeforeSupertypeStaticMethods: Boolean = false + + @Inject internal fun supertypeStaticMethodInjection(methodInjection: FuelTank) { + if (!Tire.hasBeenStaticFieldInjected()) { + staticMethodInjectedBeforeStaticFields = true + } + if (SpareTire.hasBeenStaticFieldInjected()) { + subtypeStaticFieldInjectedBeforeSupertypeStaticMethods = true + } + if (SpareTire.hasBeenStaticMethodInjected()) { + subtypeStaticMethodInjectedBeforeSupertypeStaticMethods = true + } + staticMethodInjection = methodInjection + } + + protected fun hasBeenStaticFieldInjected(): Boolean { + return staticFieldInjection != NEVER_INJECTED + } + + protected fun hasBeenStaticMethodInjected(): Boolean { + return staticMethodInjection != NEVER_INJECTED + } + } +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/org/atinject/javaxtck/auto/V8Engine.kt b/test-suite-kotlin-ksp/src/test/kotlin/org/atinject/javaxtck/auto/V8Engine.kt new file mode 100644 index 00000000000..c93c1151954 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/org/atinject/javaxtck/auto/V8Engine.kt @@ -0,0 +1,61 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.atinject.javaxtck.auto + +import org.atinject.javaxtck.auto.accessories.SpareTire + +import javax.inject.Inject +import javax.inject.Named + +class V8Engine : GasEngine() { + init { + publicNoArgsConstructorInjected = true + } + + @Inject override fun injectPackagePrivateMethod() { + if (subPackagePrivateMethodInjected) { + overriddenPackagePrivateMethodInjectedTwice = true + } + subPackagePrivateMethodInjected = true + } + + /** + * Qualifiers are swapped from how they appear in the superclass. + */ + override fun injectQualifiers(seatA: Seat, @Drivers seatB: Seat, + tireA: Tire, @Named("spare") tireB: Tire) { + overriddenMethodInjected = true + if (seatA is DriversSeat + || seatB !is DriversSeat + || tireA is SpareTire + || tireB !is SpareTire) { + qualifiersInheritedFromOverriddenMethod = true + } + } + + override fun injectPackagePrivateMethodForOverride() { + subPackagePrivateMethodForOverrideInjected = true + } + + @Inject + override fun injectTwiceOverriddenWithOmissionInMiddle() { + overriddenTwiceWithOmissionInMiddleInjected = true + } + + override fun injectTwiceOverriddenWithOmissionInSubclass() { + overriddenTwiceWithOmissionInSubclassInjected = true + } +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/org/atinject/javaxtck/auto/accessories/Cupholder.kt b/test-suite-kotlin-ksp/src/test/kotlin/org/atinject/javaxtck/auto/accessories/Cupholder.kt new file mode 100644 index 00000000000..c1a1167ac29 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/org/atinject/javaxtck/auto/accessories/Cupholder.kt @@ -0,0 +1,26 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.atinject.javaxtck.auto.accessories + +import org.atinject.javaxtck.auto.Seat + +import javax.inject.Inject +import javax.inject.Provider +import javax.inject.Singleton + +@Singleton +open class Cupholder @Inject +constructor(val seatProvider: Provider) diff --git a/test-suite-kotlin-ksp/src/test/kotlin/org/atinject/javaxtck/auto/accessories/RoundThing.kt b/test-suite-kotlin-ksp/src/test/kotlin/org/atinject/javaxtck/auto/accessories/RoundThing.kt new file mode 100644 index 00000000000..aa6e8d89a27 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/org/atinject/javaxtck/auto/accessories/RoundThing.kt @@ -0,0 +1,43 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.atinject.javaxtck.auto.accessories + + +import javax.inject.Inject + +open class RoundThing { + + var roundThingPackagePrivateMethod2Injected: Boolean = false + private set + + var roundThingPackagePrivateMethod3Injected: Boolean = false + private set + + var roundThingPackagePrivateMethod4Injected: Boolean = false + private set + + @Inject open internal fun injectPackagePrivateMethod2() { + roundThingPackagePrivateMethod2Injected = true + } + + @Inject open internal fun injectPackagePrivateMethod3() { + roundThingPackagePrivateMethod3Injected = true + } + + @Inject open internal fun injectPackagePrivateMethod4() { + roundThingPackagePrivateMethod4Injected = true + } +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/org/atinject/javaxtck/auto/accessories/SpareTire.kt b/test-suite-kotlin-ksp/src/test/kotlin/org/atinject/javaxtck/auto/accessories/SpareTire.kt new file mode 100644 index 00000000000..a85c6fb262c --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/org/atinject/javaxtck/auto/accessories/SpareTire.kt @@ -0,0 +1,128 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.atinject.javaxtck.auto.accessories + +import org.atinject.javaxtck.auto.FuelTank +import org.atinject.javaxtck.auto.Tire + +import javax.inject.Inject + +open class SpareTire @Inject +constructor(forSupertype: FuelTank, forSubtype: FuelTank) : Tire(forSupertype) { + + override var constructorInjection = Tire.NEVER_INJECTED + @Inject override var fieldInjection = Tire.NEVER_INJECTED + override var methodInjection = Tire.NEVER_INJECTED + + var spareTirePackagePrivateMethod2Injected: Boolean = false + private set + + var spareTirePackagePrivateMethod3Injected: Boolean = false + private set + + init { + this.constructorInjection = forSubtype + } + + @Inject internal fun subtypeMethodInjection(methodInjection: FuelTank) { + if (!hasSpareTireBeenFieldInjected()) { + methodInjectedBeforeFields = true + } + this.methodInjection = methodInjection + } + + @Inject private fun injectPrivateMethod() { + if (subPrivateMethodInjected) { + similarPrivateMethodInjectedTwice = true + } + subPrivateMethodInjected = true + } + + @Inject override fun injectPackagePrivateMethod() { + if (subPackagePrivateMethodInjected) { + similarPackagePrivateMethodInjectedTwice = true + } + subPackagePrivateMethodInjected = true + } + + @Inject override fun injectProtectedMethod() { + if (subProtectedMethodInjected) { + overriddenProtectedMethodInjectedTwice = true + } + subProtectedMethodInjected = true + } + + @Inject + override fun injectPublicMethod() { + if (subPublicMethodInjected) { + overriddenPublicMethodInjectedTwice = true + } + subPublicMethodInjected = true + } + + private fun injectPrivateMethodForOverride() { + superPrivateMethodForOverrideInjected = true + } + + override fun injectPackagePrivateMethodForOverride() { + superPackagePrivateMethodForOverrideInjected = true + } + + override fun injectProtectedMethodForOverride() { + protectedMethodForOverrideInjected = true + } + + override fun injectPublicMethodForOverride() { + publicMethodForOverrideInjected = true + } + + public override fun hasSpareTireBeenFieldInjected(): Boolean { + return fieldInjection !== Tire.NEVER_INJECTED + } + + @Override + public override fun hasSpareTireBeenMethodInjected(): Boolean { + return methodInjection !== Tire.NEVER_INJECTED + } + + @Inject override fun injectPackagePrivateMethod2() { + spareTirePackagePrivateMethod2Injected = true + } + + override fun injectPackagePrivateMethod3() { + spareTirePackagePrivateMethod3Injected = true + } + + companion object { + @Inject internal var staticFieldInjection = Tire.NEVER_INJECTED + internal var staticMethodInjection = Tire.NEVER_INJECTED + + @Inject internal fun subtypeStaticMethodInjection(methodInjection: FuelTank) { + if (!hasBeenStaticFieldInjected()) { + Tire.staticMethodInjectedBeforeStaticFields = true + } + staticMethodInjection = methodInjection + } + + fun hasBeenStaticFieldInjected(): Boolean { + return staticFieldInjection !== Tire.NEVER_INJECTED + } + + fun hasBeenStaticMethodInjected(): Boolean { + return staticMethodInjection !== Tire.NEVER_INJECTED + } + } +} diff --git a/test-suite-kotlin-ksp/src/test/resources/io/micronaut/docs/i18n/messages_en.properties b/test-suite-kotlin-ksp/src/test/resources/io/micronaut/docs/i18n/messages_en.properties new file mode 100644 index 00000000000..1fb30cae2d7 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/resources/io/micronaut/docs/i18n/messages_en.properties @@ -0,0 +1,2 @@ +hello=Hello +hello.name=Hello {0} \ No newline at end of file diff --git a/test-suite-kotlin-ksp/src/test/resources/io/micronaut/docs/i18n/messages_es.properties b/test-suite-kotlin-ksp/src/test/resources/io/micronaut/docs/i18n/messages_es.properties new file mode 100644 index 00000000000..31ecb39a52e --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/resources/io/micronaut/docs/i18n/messages_es.properties @@ -0,0 +1,2 @@ +hello=Hola +hello.name=Hola {0} \ No newline at end of file diff --git a/test-suite-kotlin-ksp/src/test/resources/logback.xml b/test-suite-kotlin-ksp/src/test/resources/logback.xml new file mode 100644 index 00000000000..afaebf8e17d --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/resources/logback.xml @@ -0,0 +1,14 @@ + + + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + \ No newline at end of file diff --git a/test-suite-kotlin/src/test/kotlin/io/micronaut/inject/method/nullableinjection/C.kt b/test-suite-kotlin/src/test/kotlin/io/micronaut/inject/method/nullableinjection/C.kt index 69b4fccf5ba..3dc10b3e7e1 100644 --- a/test-suite-kotlin/src/test/kotlin/io/micronaut/inject/method/nullableinjection/C.kt +++ b/test-suite-kotlin/src/test/kotlin/io/micronaut/inject/method/nullableinjection/C.kt @@ -16,8 +16,10 @@ package io.micronaut.inject.method.nullableinjection import jakarta.inject.Inject +import jakarta.inject.Singleton +@Singleton class C { internal var _a: A? = null internal var a: A diff --git a/validation/build.gradle b/validation/build.gradle index 457feca1d29..a2100d9e15e 100644 --- a/validation/build.gradle +++ b/validation/build.gradle @@ -35,6 +35,8 @@ dependencies { } //compileTestGroovy.groovyOptions.forkOptions.jvmArgs = ['-Xdebug', '-Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=5005'] +//compileTestGroovy.groovyOptions.fork = true + spotless { java { From 128161badf453a3376c4f8565fe9de53fe948af6 Mon Sep 17 00:00:00 2001 From: Juan Pablo Prado Date: Tue, 24 Jan 2023 06:08:51 -0600 Subject: [PATCH 417/743] chore(doc): upgrade lombok patch version (#8648) --- src/main/docs/guide/languageSupport/java/lombok.adoc | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/docs/guide/languageSupport/java/lombok.adoc b/src/main/docs/guide/languageSupport/java/lombok.adoc index af3f76c0376..0f9d56e480a 100644 --- a/src/main/docs/guide/languageSupport/java/lombok.adoc +++ b/src/main/docs/guide/languageSupport/java/lombok.adoc @@ -7,8 +7,8 @@ If you use Gradle, add the following dependencies: .Configuring Lombok in Gradle [source,groovy] ---- -compileOnly 'org.projectlombok:lombok:1.18.12' -annotationProcessor "org.projectlombok:lombok:1.18.12" +compileOnly 'org.projectlombok:lombok:1.18.24' +annotationProcessor "org.projectlombok:lombok:1.18.24" ... // Micronaut processor defined after Lombok annotationProcessor "io.micronaut:micronaut-inject-java" @@ -23,7 +23,7 @@ Or if using Maven: org.projectlombok lombok - 1.18.12 + 1.18.24 provided @@ -33,7 +33,7 @@ Or if using Maven: org.projectlombok lombok - 1.18.12 + 1.18.24 io.micronaut From 9e5b1d105279045d8aa87d070a93b9eeb5baddd5 Mon Sep 17 00:00:00 2001 From: Denis Stepanov Date: Tue, 24 Jan 2023 23:11:04 +0100 Subject: [PATCH 418/743] Rewrite `AbstractAnnotationMetadataBuilder` bugfixes and tests (#8632) --- .../AbstractAnnotationMetadataBuilder.java | 2210 ++++++----------- .../annotation/AnnotationMetadataWriter.java | 2 + .../IntrospectedTypeElementVisitor.java | 8 +- .../inject/writer/BeanDefinitionWriter.java | 9 +- .../core/annotation/AnnotationMetadata.java | 2 +- .../AnnotationMetadataDelegate.java | 2 +- .../core/annotation/AnnotationUtil.java | 7 +- .../core/annotation/AnnotationValue.java | 94 +- .../annotation/AnnotationValueBuilder.java | 19 +- .../annotation/EmptyAnnotationMetadata.java | 2 +- .../DefaultMutableConversionService.java | 28 +- .../core/reflect/ReflectionUtils.java | 20 + .../GroovyAnnotationMetadataBuilder.java | 354 +-- .../AnnotationMetadataWriterSpec.groovy | 105 + .../inject/annotation/MyAnnotation.groovy | 56 + .../annotation/MyAnnotation2Aliases.groovy | 81 + .../inject/annotation/MyAnnotation3.groovy | 17 + .../micronaut/inject/annotation/MyEnum2.java | 5 + .../JavaAnnotationMetadataBuilder.java | 139 +- .../AddsRepeatableAnnotationSpec.groovy | 21 +- ...UnseenInnerRepeatableAnnotationSpec.groovy | 47 + .../AddsUnseenRepeatableAnnotationSpec.groovy | 47 + .../annotation/mapping/MapMeToRepeatable.java | 29 + .../MapToRepeatableAnnotationSpec.groovy | 50 + .../mapping/MappedValueHasDefaultSpec.groovy | 55 + .../annotation/mapping/MyRequirements.java | 30 + .../annotation/mapping/MyRequirements2.java | 43 + .../annotation/mapping/MyRequires.java | 33 + .../mapping/MySourceAnnotation.java | 33 + .../mapping/MySourceAnnotation2.java | 33 + .../mapping/RemapMeToRepeatable.java | 29 + .../RemapToRepeatableAnnotationSpec.groovy | 54 + .../ReplacesRepeatableAnnotationSpec.groovy | 23 +- .../io/micronaut/annotation/mapping/Seen.java | 28 + .../SourceAnnotationHasDefaultsSpec.groovy | 44 + .../mapping/TransformMeToRepeatable.java | 29 + ...ransformsToRepeatableAnnotationSpec.groovy | 51 + .../AnnotationMetadataWriterSpec.groovy | 97 + .../inject/annotation/MyAnnotation2.java | 63 + .../annotation/MyAnnotation2Aliases.java | 82 + .../inject/annotation/MyAnnotation3.java | 17 + .../micronaut/inject/annotation/MyEnum2.java | 5 + ...eritedConfigurationReaderPrefixSpec.groovy | 4 +- ...cronaut.inject.annotation.AnnotationMapper | 4 +- ...onaut.inject.annotation.AnnotationRemapper | 3 +- ...ut.inject.annotation.AnnotationTransformer | 3 +- ...icronaut.inject.visitor.TypeElementVisitor | 3 + .../KotlinAnnotationMetadataBuilder.kt | 65 +- .../AnnotationMetadataHierarchy.java | 29 +- .../annotation/AnnotationMetadataSupport.java | 22 +- .../annotation/DefaultAnnotationMetadata.java | 89 +- .../annotation/MutableAnnotationMetadata.java | 4 +- .../validator/DefaultValidator.java | 6 +- 53 files changed, 2497 insertions(+), 1838 deletions(-) create mode 100644 inject-groovy/src/test/groovy/io/micronaut/inject/annotation/MyAnnotation.groovy create mode 100644 inject-groovy/src/test/groovy/io/micronaut/inject/annotation/MyAnnotation2Aliases.groovy create mode 100644 inject-groovy/src/test/groovy/io/micronaut/inject/annotation/MyAnnotation3.groovy create mode 100644 inject-groovy/src/test/groovy/io/micronaut/inject/annotation/MyEnum2.java create mode 100644 inject-java/src/test/groovy/io/micronaut/annotation/mapping/AddsUnseenInnerRepeatableAnnotationSpec.groovy create mode 100644 inject-java/src/test/groovy/io/micronaut/annotation/mapping/AddsUnseenRepeatableAnnotationSpec.groovy create mode 100644 inject-java/src/test/groovy/io/micronaut/annotation/mapping/MapMeToRepeatable.java create mode 100644 inject-java/src/test/groovy/io/micronaut/annotation/mapping/MapToRepeatableAnnotationSpec.groovy create mode 100644 inject-java/src/test/groovy/io/micronaut/annotation/mapping/MappedValueHasDefaultSpec.groovy create mode 100644 inject-java/src/test/groovy/io/micronaut/annotation/mapping/MyRequirements.java create mode 100644 inject-java/src/test/groovy/io/micronaut/annotation/mapping/MyRequirements2.java create mode 100644 inject-java/src/test/groovy/io/micronaut/annotation/mapping/MyRequires.java create mode 100644 inject-java/src/test/groovy/io/micronaut/annotation/mapping/MySourceAnnotation.java create mode 100644 inject-java/src/test/groovy/io/micronaut/annotation/mapping/MySourceAnnotation2.java create mode 100644 inject-java/src/test/groovy/io/micronaut/annotation/mapping/RemapMeToRepeatable.java create mode 100644 inject-java/src/test/groovy/io/micronaut/annotation/mapping/RemapToRepeatableAnnotationSpec.groovy create mode 100644 inject-java/src/test/groovy/io/micronaut/annotation/mapping/Seen.java create mode 100644 inject-java/src/test/groovy/io/micronaut/annotation/mapping/SourceAnnotationHasDefaultsSpec.groovy create mode 100644 inject-java/src/test/groovy/io/micronaut/annotation/mapping/TransformMeToRepeatable.java create mode 100644 inject-java/src/test/groovy/io/micronaut/annotation/mapping/TransformsToRepeatableAnnotationSpec.groovy create mode 100644 inject-java/src/test/groovy/io/micronaut/inject/annotation/MyAnnotation2.java create mode 100644 inject-java/src/test/groovy/io/micronaut/inject/annotation/MyAnnotation2Aliases.java create mode 100644 inject-java/src/test/groovy/io/micronaut/inject/annotation/MyAnnotation3.java create mode 100644 inject-java/src/test/groovy/io/micronaut/inject/annotation/MyEnum2.java diff --git a/core-processor/src/main/java/io/micronaut/inject/annotation/AbstractAnnotationMetadataBuilder.java b/core-processor/src/main/java/io/micronaut/inject/annotation/AbstractAnnotationMetadataBuilder.java index ccc96bd62b3..7a45d4504ce 100644 --- a/core-processor/src/main/java/io/micronaut/inject/annotation/AbstractAnnotationMetadataBuilder.java +++ b/core-processor/src/main/java/io/micronaut/inject/annotation/AbstractAnnotationMetadataBuilder.java @@ -15,7 +15,11 @@ */ package io.micronaut.inject.annotation; -import io.micronaut.context.annotation.*; +import io.micronaut.context.annotation.AliasFor; +import io.micronaut.context.annotation.Aliases; +import io.micronaut.context.annotation.DefaultScope; +import io.micronaut.context.annotation.NonBinding; +import io.micronaut.context.annotation.Type; import io.micronaut.core.annotation.AnnotatedElement; import io.micronaut.core.annotation.AnnotationClassValue; import io.micronaut.core.annotation.AnnotationMetadata; @@ -23,7 +27,6 @@ import io.micronaut.core.annotation.AnnotationUtil; import io.micronaut.core.annotation.AnnotationValue; import io.micronaut.core.annotation.AnnotationValueBuilder; -import io.micronaut.core.annotation.Experimental; import io.micronaut.core.annotation.InstantiatedMember; import io.micronaut.core.annotation.Internal; import io.micronaut.core.annotation.NonNull; @@ -32,10 +35,10 @@ import io.micronaut.core.naming.NameUtils; import io.micronaut.core.util.CollectionUtils; import io.micronaut.core.util.StringUtils; -import io.micronaut.core.value.OptionalValues; import io.micronaut.inject.qualifiers.InterceptorBindingQualifier; import io.micronaut.inject.visitor.VisitorContext; import jakarta.inject.Qualifier; +import org.jetbrains.annotations.NotNull; import java.lang.annotation.Annotation; import java.lang.annotation.RetentionPolicy; @@ -47,15 +50,13 @@ import java.util.LinkedHashMap; import java.util.LinkedList; import java.util.List; -import java.util.ListIterator; import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.Set; -import java.util.function.BiConsumer; import java.util.function.Predicate; import java.util.function.Supplier; -import java.util.stream.Collectors; +import java.util.stream.Stream; /** * An abstract implementation that builds {@link AnnotationMetadata}. @@ -68,8 +69,6 @@ @Internal public abstract class AbstractAnnotationMetadataBuilder { - protected static final List EXCLUDES = Arrays.asList(AnnotationUtil.KOTLIN_METADATA, "jdk.internal.ValueBased"); - /** * Names of annotations that should produce deprecation warnings. * The key in the map is the deprecated annotation the value the replacement. @@ -80,14 +79,12 @@ public abstract class AbstractAnnotationMetadataBuilder { private static final Map> ANNOTATION_REMAPPERS = new HashMap<>(5); private static final Map, CachedAnnotationMetadata> MUTATED_ANNOTATION_METADATA = new HashMap<>(100); private static final Map> NON_BINDING_CACHE = new HashMap<>(50); - private static final List DEFAULT_ANNOTATE_EXCLUDES = Arrays.asList(Internal.class.getName(), - Experimental.class.getName(), "jdk.internal.ValueBased"); - private static final Map> ANNOTATION_DEFAULTS = new HashMap<>(20); + private static final Map> ANNOTATION_DEFAULTS = new HashMap<>(20); private static final String MSG_UNRECOGNIZED_ANNOTATION_METADATA = "Unrecognized annotation metadata: "; static { for (AnnotationMapper mapper : SoftServiceLoader.load(AnnotationMapper.class, AbstractAnnotationMetadataBuilder.class.getClassLoader()) - .disableFork().collectAll()) { + .disableFork().collectAll()) { try { String name = null; if (mapper instanceof TypedAnnotationMapper) { @@ -104,7 +101,7 @@ public abstract class AbstractAnnotationMetadataBuilder { } for (AnnotationTransformer transformer : SoftServiceLoader.load(AnnotationTransformer.class, AbstractAnnotationMetadataBuilder.class.getClassLoader()) - .disableFork().collectAll()) { + .disableFork().collectAll()) { try { String name = null; if (transformer instanceof TypedAnnotationTransformer) { @@ -121,7 +118,7 @@ public abstract class AbstractAnnotationMetadataBuilder { } for (AnnotationRemapper mapper : SoftServiceLoader.load(AnnotationRemapper.class, AbstractAnnotationMetadataBuilder.class.getClassLoader()) - .disableFork().collectAll()) { + .disableFork().collectAll()) { try { String name = mapper.getPackageName(); if (StringUtils.isNotEmpty(name)) { @@ -160,12 +157,12 @@ private AnnotationMetadata metadataForError(RuntimeException e) { * @return The {@link AnnotationMetadata} */ public AnnotationMetadata buildDeclared(T element) { - DefaultAnnotationMetadata annotationMetadata = new MutableAnnotationMetadata(); + MutableAnnotationMetadata annotationMetadata = new MutableAnnotationMetadata(); try { AnnotationMetadata metadata = buildInternalMulti( - Collections.emptyList(), - element, - annotationMetadata, true, true, true + Collections.emptyList(), + element, + annotationMetadata, true, true ); if (metadata.isEmpty()) { return AnnotationMetadata.EMPTY_METADATA; @@ -188,16 +185,17 @@ public AnnotationMetadata buildDeclared(T element, List annotations if (CollectionUtils.isEmpty(annotations)) { return AnnotationMetadata.EMPTY_METADATA; } - DefaultAnnotationMetadata annotationMetadata = new MutableAnnotationMetadata(); + MutableAnnotationMetadata annotationMetadata = new MutableAnnotationMetadata(); if (includeTypeAnnotations) { buildInternalMulti( - Collections.emptyList(), - element, - annotationMetadata, false, true, true + Collections.emptyList(), + element, + annotationMetadata, false, true ); } try { - includeAnnotations(annotationMetadata, element, false, true, annotations, true); + addAnnotations(annotationMetadata, element, false, true, + annotations, Collections.emptyList()); if (annotationMetadata.isEmpty()) { return AnnotationMetadata.EMPTY_METADATA; } @@ -259,12 +257,12 @@ private CachedAnnotationMetadata lookupOrBuild(boolean inheritTypeAnnotations, T } private AnnotationMetadata buildInternal(boolean inheritTypeAnnotations, boolean declaredOnly, T element) { - DefaultAnnotationMetadata annotationMetadata = new MutableAnnotationMetadata(); + MutableAnnotationMetadata annotationMetadata = new MutableAnnotationMetadata(); try { return buildInternalMulti( - Collections.emptyList(), - element, - annotationMetadata, inheritTypeAnnotations, declaredOnly, true + Collections.emptyList(), + element, + annotationMetadata, inheritTypeAnnotations, declaredOnly ); } catch (RuntimeException e) { return metadataForError(e); @@ -355,12 +353,12 @@ private AnnotationMetadata buildInternal(boolean inheritTypeAnnotations, boolean * @param annotationValues The values to populate */ protected abstract void readAnnotationRawValues( - T originatingElement, - String annotationName, - T member, - String memberName, - Object annotationValue, - Map annotationValues); + T originatingElement, + String annotationName, + T member, + String memberName, + Object annotationValue, + Map annotationValues); /** * Validates an annotation value. @@ -383,7 +381,7 @@ protected void validateAnnotationValue(T originatingElement, final AnnotatedElementValidator elementValidator = getElementValidator(); if (elementValidator != null && !erroneousElements.contains(member)) { boolean shouldValidate = !(annotationName.equals(AliasFor.class.getName())) && - (!(resolvedValue instanceof String) || !resolvedValue.toString().contains("${")); + (!(resolvedValue instanceof String) || !resolvedValue.toString().contains("${")); if (shouldValidate) { shouldValidate = isValidationRequired(member); } @@ -433,8 +431,8 @@ public AnnotationMetadata getAnnotationMetadata() { * * @return The validator. */ - protected @Nullable - AnnotatedElementValidator getElementValidator() { + @Nullable + protected AnnotatedElementValidator getElementValidator() { return null; } @@ -465,14 +463,6 @@ AnnotatedElementValidator getElementValidator() { */ protected abstract Object readAnnotationValue(T originatingElement, T member, String memberName, Object annotationValue); - /** - * Read the raw default annotation values from the given annotation. - * - * @param annotationMirror The annotation - * @return The values - */ - protected abstract Map readAnnotationDefaultValues(A annotationMirror); - /** * Read the raw default annotation values from the given annotation. * @@ -496,9 +486,10 @@ AnnotatedElementValidator getElementValidator() { * @param originatingElement The originating element * @param member The member * @param annotationType The type + * @param The annotation type * @return The values */ - protected abstract OptionalValues getAnnotationValues(T originatingElement, T member, Class annotationType); + protected abstract Optional> getAnnotationValues(T originatingElement, T member, Class annotationType); /** * Read the name of an annotation member. @@ -514,8 +505,8 @@ AnnotatedElementValidator getElementValidator() { * @param annotationMirror The annotation mirror * @return Return the name or null */ - protected abstract @Nullable - String getRepeatableName(A annotationMirror); + @Nullable + protected abstract String getRepeatableName(A annotationMirror); /** * Obtain the name of the repeatable annotation if the annotation is is one. @@ -523,51 +514,50 @@ AnnotatedElementValidator getElementValidator() { * @param annotationType The annotation mirror * @return Return the name or null */ - protected abstract @Nullable - String getRepeatableNameForType(T annotationType); + @Nullable + protected abstract String getRepeatableNameForType(T annotationType); /** - * @param originatingElement The originating element - * @param annotationMirror The annotation + * @param annotationElement The annotation element + * @param annotationType The annotation type * @return The annotation value */ - protected io.micronaut.core.annotation.AnnotationValue readNestedAnnotationValue(T originatingElement, A annotationMirror) { - io.micronaut.core.annotation.AnnotationValue av; - Map annotationValues = readAnnotationRawValues(annotationMirror); - final String annotationTypeName = getAnnotationTypeName(annotationMirror); + protected AnnotationValue readNestedAnnotationValue(T annotationElement, A annotationType) { + final String annotationTypeName = getAnnotationTypeName(annotationType); + Map annotationValues = readAnnotationRawValues(annotationType); if (annotationValues.isEmpty()) { - av = new io.micronaut.core.annotation.AnnotationValue(annotationTypeName); - } else { - - Map resolvedValues = new LinkedHashMap<>(); - for (Map.Entry entry : annotationValues.entrySet()) { - T member = entry.getKey(); - OptionalValues aliasForValues = getAnnotationValues(originatingElement, member, AliasFor.class); - Object annotationValue = entry.getValue(); - Optional aliasMember = aliasForValues.get("member"); - Optional aliasAnnotation = aliasForValues.get("annotation"); - Optional aliasAnnotationName = aliasForValues.get("annotationName"); + return new AnnotationValue<>(annotationTypeName, Collections.emptyMap()); + } + + Map resolvedValues = CollectionUtils.newLinkedHashMap(annotationValues.size()); + for (Map.Entry entry : annotationValues.entrySet()) { + T member = entry.getKey(); + Optional> aliasForValues = getAnnotationValues(annotationElement, member, AliasFor.class); + Object annotationValue = entry.getValue(); + if (aliasForValues.isPresent()) { + AnnotationValue aliasFor = aliasForValues.get(); + Optional aliasMember = aliasFor.stringValue("member"); + Optional aliasAnnotation = aliasFor.stringValue("annotation"); + Optional aliasAnnotationName = aliasFor.stringValue("annotationName"); if (aliasMember.isPresent() && !(aliasAnnotation.isPresent() || aliasAnnotationName.isPresent())) { - String aliasedNamed = aliasMember.get().toString(); - readAnnotationRawValues(originatingElement, - annotationTypeName, - member, - aliasedNamed, - annotationValue, - resolvedValues); + String aliasedNamed = aliasMember.get(); + readAnnotationRawValues(annotationElement, + annotationTypeName, + member, + aliasedNamed, + annotationValue, + resolvedValues); } - String memberName = getAnnotationMemberName(member); - readAnnotationRawValues(originatingElement, + } + String memberName = getAnnotationMemberName(member); + readAnnotationRawValues(annotationElement, annotationTypeName, member, memberName, annotationValue, resolvedValues); - } - av = new io.micronaut.core.annotation.AnnotationValue(annotationTypeName, resolvedValues); } - - return av; + return new AnnotationValue<>(annotationTypeName, resolvedValues); } /** @@ -578,290 +568,15 @@ protected io.micronaut.core.annotation.AnnotationValue readNestedAnnotationValue */ protected abstract Optional getAnnotationMirror(String annotationName); - /** - * Populate the annotation data for the given annotation. - * - * @param originatingElement The element the annotation data originates from - * @param parent The parent element - * @param annotationMirror The annotation - * @param metadata the metadata - * @param isDeclared Is the annotation a declared annotation - * @param retentionPolicy The retention policy - * @param allowAliases Whether aliases are allowed - * @return The annotation values - */ - protected Map populateAnnotationData( - T originatingElement, - @Nullable T parent, - A annotationMirror, - DefaultAnnotationMetadata metadata, - boolean isDeclared, - RetentionPolicy retentionPolicy, - boolean allowAliases) { - return populateAnnotationData( - originatingElement, - parent == originatingElement, - annotationMirror, - metadata, - isDeclared, - retentionPolicy, - allowAliases - ); - } - - /** - * Populate the annotation data for the given annotation. - * - * @param originatingElement The element the annotation data originates from - * @param originatingElementIsSameParent Whether the originating element is considered a parent element - * @param annotationMirror The annotation - * @param metadata the metadata - * @param isDeclared Is the annotation a declared annotation - * @param retentionPolicy The retention policy - * @param allowAliases Whether aliases are allowed - * @return The annotation values - */ - protected Map populateAnnotationData( - T originatingElement, - boolean originatingElementIsSameParent, - A annotationMirror, - DefaultAnnotationMetadata metadata, - boolean isDeclared, - RetentionPolicy retentionPolicy, - boolean allowAliases) { - String annotationName = getAnnotationTypeName(annotationMirror); - - if (retentionPolicy == RetentionPolicy.RUNTIME) { - processAnnotationDefaults(originatingElement, - metadata, - annotationName, - () -> readAnnotationDefaultValues(annotationMirror)); - } - - List parentAnnotations = new ArrayList<>(); - parentAnnotations.add(annotationName); - Map elementValues = readAnnotationRawValues(annotationMirror); - Map annotationValues; - if (CollectionUtils.isEmpty(elementValues)) { - annotationValues = new LinkedHashMap<>(3); - } else { - annotationValues = new LinkedHashMap<>(5); - Set nonBindingMembers = new HashSet<>(2); - for (Map.Entry entry : elementValues.entrySet()) { - T member = entry.getKey(); - - if (member == null) { - continue; - } - - Object annotationValue = entry.getValue(); - if (hasAnnotations(member)) { - final DefaultAnnotationMetadata memberMetadata = new DefaultAnnotationMetadata(); - final List annotationsForMember = getAnnotationsForType(member) - .stream().filter((a) -> !getAnnotationTypeName(a).equals(annotationName)) - .collect(Collectors.toList()); - includeAnnotations(memberMetadata, member, false, true, annotationsForMember, false); - - boolean isInstantiatedMember = memberMetadata.hasAnnotation(InstantiatedMember.class); - - if (memberMetadata.hasAnnotation(NonBinding.class)) { - final String memberName = getElementName(member); - nonBindingMembers.add(memberName); - } - if (isInstantiatedMember) { - final String memberName = getAnnotationMemberName(member); - final Object rawValue = readAnnotationValue(originatingElement, member, memberName, annotationValue); - if (rawValue instanceof AnnotationClassValue) { - AnnotationClassValue acv = (AnnotationClassValue) rawValue; - annotationValues.put(memberName, new AnnotationClassValue(acv.getName(), true)); - } - } - } - - if (allowAliases) { - handleAnnotationAlias( - originatingElement, - metadata, - isDeclared, - annotationName, - parentAnnotations, - annotationValues, - member, - annotationValue - ); - } - } - - if (!nonBindingMembers.isEmpty()) { - T annotationType = getTypeForAnnotation(annotationMirror); - if (hasAnnotation(annotationType, AnnotationUtil.QUALIFIER) || - hasAnnotation(annotationType, Qualifier.class)) { - metadata.addDeclaredStereotype( - Collections.singletonList(getAnnotationTypeName(annotationMirror)), - AnnotationUtil.QUALIFIER, - Collections.singletonMap("nonBinding", nonBindingMembers) - ); - } - } - } - List> mappers = getAnnotationMappers(annotationName); - if (mappers != null) { - AnnotationValue annotationValue = new AnnotationValue(annotationName, annotationValues); - VisitorContext visitorContext = createVisitorContext(); - for (AnnotationMapper mapper : mappers) { - List mapped = mapper.map(annotationValue, visitorContext); - if (mapped != null) { - for (Object o : mapped) { - if (o instanceof AnnotationValue) { - AnnotationValue av = (AnnotationValue) o; - retentionPolicy = av.getRetentionPolicy(); - String mappedAnnotationName = av.getAnnotationName(); - - Optional mappedMirror = getAnnotationMirror(mappedAnnotationName); - String repeatableName = mappedMirror.map(this::getRepeatableNameForType).orElse(null); - if (repeatableName != null) { - if (isDeclared) { - metadata.addDeclaredRepeatable( - repeatableName, - av, - retentionPolicy - ); - } else { - metadata.addRepeatable( - repeatableName, - av, - retentionPolicy - ); - } - } else { - Map values = av.getValues(); - - if (isDeclared) { - metadata.addDeclaredAnnotation( - mappedAnnotationName, - values, - retentionPolicy - ); - } else { - metadata.addAnnotation( - mappedAnnotationName, - values, - retentionPolicy - ); - } - - } - - RetentionPolicy finalRetentionPolicy = retentionPolicy; - mappedMirror.ifPresent(annMirror -> { - Map values = av.getValues(); - values.forEach((key, value) -> { - T member = getAnnotationMember(annMirror, key); - if (member != null) { - handleAnnotationAlias( - originatingElement, - metadata, - isDeclared, - mappedAnnotationName, - Collections.emptyList(), - annotationValues, - member, - value - ); - } - }); - if (finalRetentionPolicy == RetentionPolicy.RUNTIME) { - processAnnotationDefaults(originatingElement, - metadata, - mappedAnnotationName, - () -> readAnnotationDefaultValues(mappedAnnotationName, annMirror)); - } - final ArrayList parents = new ArrayList<>(); - processAnnotationStereotype( - parents, - annMirror, - mappedAnnotationName, - metadata, - isDeclared, - isInheritedAnnotationType(annMirror) || originatingElementIsSameParent); - - }); - } - } - } - } - } - return annotationValues; - } - - private void handleAnnotationAlias(T originatingElement, - DefaultAnnotationMetadata metadata, - boolean isDeclared, - String annotationName, - List parentAnnotations, - Map annotationValues, - T member, - Object annotationValue) { - Optional aliases = getAnnotationValues(originatingElement, member, Aliases.class).get("value"); - if (aliases.isPresent()) { - Object value = aliases.get(); - if (value instanceof AnnotationValue[]) { - AnnotationValue[] values = (AnnotationValue[]) value; - for (AnnotationValue av : values) { - OptionalValues aliasForValues = OptionalValues.of(Object.class, av.getValues()); - processAnnotationAlias( - originatingElement, - annotationName, - member, metadata, - isDeclared, - parentAnnotations, - annotationValues, - annotationValue, - aliasForValues - ); - } - } - readAnnotationRawValues(originatingElement, - annotationName, - member, - getAnnotationMemberName(member), - annotationValue, - annotationValues); - } else { - OptionalValues aliasForValues = getAnnotationValues( - originatingElement, - member, - AliasFor.class - ); - processAnnotationAlias( - originatingElement, - annotationName, - member, - metadata, - isDeclared, - parentAnnotations, - annotationValues, - annotationValue, - aliasForValues - ); - readAnnotationRawValues(originatingElement, - annotationName, - member, - getAnnotationMemberName(member), - annotationValue, - annotationValues); - } - } - /** * Get the annotation member. * - * @param originatingElement The originatig element - * @param member The member - * @return The annotation member + * @param annotationElement The annotation element + * @param member The member + * @return The annotation member element */ - protected abstract @Nullable - T getAnnotationMember(T originatingElement, CharSequence member); + @Nullable + protected abstract T getAnnotationMember(T annotationElement, CharSequence member); /** * Obtain the annotation mappers for the given annotation name. @@ -869,20 +584,21 @@ private void handleAnnotationAlias(T originatingElement, * @param annotationName The annotation name * @return The mappers */ - protected @NonNull - List> getAnnotationMappers(@NonNull String annotationName) { - return ANNOTATION_MAPPERS.get(annotationName); + @NonNull + protected List> getAnnotationMappers(@NonNull String annotationName) { + return (List) ANNOTATION_MAPPERS.get(annotationName); } /** * Obtain the transformers mappers for the given annotation name. * * @param annotationName The annotation name + * @param The annotation type * @return The transformers */ - protected @NonNull - List> getAnnotationTransformers(@NonNull String annotationName) { - return ANNOTATION_TRANSFORMERS.get(annotationName); + @NonNull + protected List> getAnnotationTransformers(@NonNull String annotationName) { + return (List) ANNOTATION_TRANSFORMERS.get(annotationName); } /** @@ -892,50 +608,27 @@ List> getAnnotationTransformers(@NonNull Strin */ protected abstract VisitorContext createVisitorContext(); - private void processAnnotationDefaults(T originatingElement, - DefaultAnnotationMetadata metadata, - String annotationName, - Supplier> elementDefaultValues) { - Map defaultValues; - final Map defaults = ANNOTATION_DEFAULTS.get(annotationName); - if (defaults != null) { - defaultValues = new LinkedHashMap<>(defaults); - } else { - defaultValues = getAnnotationDefaults(originatingElement, annotationName, elementDefaultValues.get()); - if (defaultValues != null) { - ANNOTATION_DEFAULTS.put(annotationName, defaultValues.entrySet().stream() - .collect(Collectors.toMap( - (entry) -> entry.getKey().toString(), - Map.Entry::getValue))); - } else { - defaultValues = Collections.emptyMap(); - } - } - metadata.addDefaultAnnotationValues(annotationName, defaultValues); - } - private Map getAnnotationDefaults(T originatingElement, String annotationName, Map elementDefaultValues) { - if (elementDefaultValues != null) { - Map defaultValues = new LinkedHashMap<>(); - for (Map.Entry entry : elementDefaultValues.entrySet()) { - T member = entry.getKey(); - String memberName = getAnnotationMemberName(member); - if (!defaultValues.containsKey(memberName)) { - Object annotationValue = entry.getValue(); - readAnnotationRawValues(originatingElement, + if (elementDefaultValues == null) { + return null; + } + Map defaultValues = CollectionUtils.newLinkedHashMap(elementDefaultValues.size()); + for (Map.Entry entry : elementDefaultValues.entrySet()) { + T member = entry.getKey(); + String memberName = getAnnotationMemberName(member); + if (!defaultValues.containsKey(memberName)) { + Object annotationValue = entry.getValue(); + readAnnotationRawValues(originatingElement, annotationName, member, memberName, annotationValue, defaultValues); - } } - return defaultValues; - } else { - return null; } + return defaultValues; } @NonNull @@ -943,113 +636,36 @@ private CachedAnnotationMetadata lookupExisting(T[] elements, Supplier(elements), metadataKey -> new DefaultCachedAnnotationMetadata(annotationMetadataSupplier.get())); } - private void processAnnotationAlias( - T originatingElement, - String annotationName, - T member, - DefaultAnnotationMetadata metadata, - boolean isDeclared, - List parentAnnotations, - Map annotationValues, - Object annotationValue, - OptionalValues aliasForValues) { - Optional aliasAnnotation = aliasForValues.get("annotation"); - Optional aliasAnnotationName = aliasForValues.get("annotationName"); - Optional aliasMember = aliasForValues.get("member"); + @Nullable + private void processAnnotationAlias(Map annotationValues, + Object annotationValue, + AnnotationValue aliasForAnnotation, + RetentionPolicy retentionPolicy, + List introducedAnnotations) { + Optional aliasAnnotation = aliasForAnnotation.stringValue("annotation"); + Optional aliasAnnotationName = aliasForAnnotation.stringValue("annotationName"); + Optional aliasMember = aliasForAnnotation.stringValue("member"); if (aliasAnnotation.isPresent() || aliasAnnotationName.isPresent()) { if (aliasMember.isPresent()) { String aliasedAnnotation; - if (aliasAnnotation.isPresent()) { - aliasedAnnotation = aliasAnnotation.get().toString(); - } else { - aliasedAnnotation = aliasAnnotationName.get().toString(); - } - String aliasedMemberName = aliasMember.get().toString(); - Object v = readAnnotationValue(originatingElement, member, aliasedMemberName, annotationValue); - - if (v != null) { - final List> remappedValues = remapAnnotation(aliasedAnnotation); - for (AnnotationValue remappedAnnotation : remappedValues) { - String aliasedAnnotationName = remappedAnnotation.getAnnotationName(); - Optional annotationMirror = getAnnotationMirror(aliasedAnnotationName); - RetentionPolicy retentionPolicy = RetentionPolicy.RUNTIME; - String repeatableName = null; - if (annotationMirror.isPresent()) { - final T annotationTypeMirror = annotationMirror.get(); - processAnnotationDefaults(originatingElement, - metadata, - aliasedAnnotationName, - () -> readAnnotationDefaultValues(aliasedAnnotationName, - annotationTypeMirror)); - retentionPolicy = getRetentionPolicy(annotationTypeMirror); - repeatableName = getRepeatableNameForType(annotationTypeMirror); - } - - if (isDeclared) { - if (StringUtils.isNotEmpty(repeatableName)) { - metadata.addDeclaredRepeatableStereotype( - parentAnnotations, - repeatableName, - AnnotationValue.builder(aliasedAnnotationName, retentionPolicy) - .members(Collections.singletonMap(aliasedMemberName, v)) - .build() - ); - } else { - metadata.addDeclaredStereotype( - Collections.emptyList(), - aliasedAnnotationName, - Collections.singletonMap(aliasedMemberName, v), - retentionPolicy - ); - } - } else { - if (StringUtils.isNotEmpty(repeatableName)) { - metadata.addRepeatableStereotype( - parentAnnotations, - repeatableName, - AnnotationValue.builder(aliasedAnnotationName, retentionPolicy) - .members(Collections.singletonMap(aliasedMemberName, v)) - .build() - ); - } else { - - metadata.addStereotype( - Collections.emptyList(), - aliasedAnnotationName, - Collections.singletonMap(aliasedMemberName, v), - retentionPolicy - ); - } - } - - if (annotationMirror.isPresent()) { - final T am = annotationMirror.get(); - processAnnotationStereotype( - Collections.singletonList(aliasedAnnotationName), - am, - aliasedAnnotationName, - metadata, - isDeclared, - isInheritedAnnotationType(am) - ); - } else { - processAnnotationStereotype( - Collections.singletonList(aliasedAnnotationName), - remappedAnnotation, - metadata, - isDeclared); - } - } + aliasedAnnotation = aliasAnnotation.orElseGet(aliasAnnotationName::get); + String aliasedMemberName = aliasMember.get(); + if (annotationValue != null) { + introducedAnnotations.add( + toProcessedAnnotation( + AnnotationValue.builder(aliasedAnnotation, retentionPolicy) + .members(Collections.singletonMap(aliasedMemberName, annotationValue)) + .build() + ) + ); } } } else if (aliasMember.isPresent()) { - String aliasedNamed = aliasMember.get().toString(); - Object v = readAnnotationValue(originatingElement, member, aliasedNamed, annotationValue); - if (v != null) { - annotationValues.put(aliasedNamed, v); + String aliasedNamed = aliasMember.get(); + if (annotationValue != null) { + annotationValues.put(aliasedNamed, annotationValue); } - readAnnotationRawValues(originatingElement, annotationName, member, aliasedNamed, annotationValue, annotationValues); } } @@ -1059,16 +675,15 @@ private void processAnnotationAlias( * @param annotation The annotation * @return The retention policy */ - protected abstract @NonNull - RetentionPolicy getRetentionPolicy(@NonNull T annotation); + @NonNull + protected abstract RetentionPolicy getRetentionPolicy(@NonNull T annotation); private AnnotationMetadata buildInternalMulti( - List parents, - T element, - DefaultAnnotationMetadata annotationMetadata, - boolean inheritTypeAnnotations, - boolean declaredOnly, - boolean allowAliases) { + List parents, + T element, + MutableAnnotationMetadata annotationMetadata, + boolean inheritTypeAnnotations, + boolean declaredOnly) { List hierarchy = buildHierarchy(element, inheritTypeAnnotations, declaredOnly); for (T parent : parents) { final List parentHierarchy = buildHierarchy(parent, inheritTypeAnnotations, declaredOnly); @@ -1089,24 +704,24 @@ private AnnotationMetadata buildInternalMulti( continue; } - includeAnnotations( - annotationMetadata, - currentElement, - parents.contains(currentElement), - currentElement == element, - annotationHierarchy, - allowAliases + boolean originatingElementIsSameParent = parents.contains(currentElement); + boolean isDeclared = currentElement == element; + addAnnotations( + annotationMetadata, + currentElement, + originatingElementIsSameParent, + isDeclared, + annotationHierarchy, + Collections.emptyList() ); } if (!annotationMetadata.hasDeclaredStereotype(AnnotationUtil.SCOPE) && annotationMetadata.hasDeclaredStereotype( - DefaultScope.class)) { + DefaultScope.class)) { Optional value = annotationMetadata.stringValue(DefaultScope.class); value.ifPresent(name -> annotationMetadata.addDeclaredAnnotation(name, Collections.emptyMap())); } - if (annotationMetadata instanceof MutableAnnotationMetadata mutableAnnotationMetadata) { - postProcess(mutableAnnotationMetadata, element); - } + postProcess(annotationMetadata, element); return annotationMetadata; } @@ -1115,490 +730,220 @@ protected void postProcess(MutableAnnotationMetadata mutableAnnotationMetadata, //no-op } - private void includeAnnotations(DefaultAnnotationMetadata annotationMetadata, - T element, - boolean originatingElementIsSameParent, - boolean isDeclared, - List annotationHierarchy, - boolean allowAliases) { - final ArrayList hierarchyCopy = new ArrayList<>(annotationHierarchy); - final ListIterator listIterator = hierarchyCopy.listIterator(); - while (listIterator.hasNext()) { - A annotationMirror = listIterator.next(); - String annotationName = getAnnotationTypeName(annotationMirror); - if (isExcludedAnnotation(element, annotationName)) { - continue; - } - if (DEPRECATED_ANNOTATION_NAMES.containsKey(annotationName)) { - addWarning(element, - "Usages of deprecated annotation " + annotationName + " found. You should use " + DEPRECATED_ANNOTATION_NAMES.get( - annotationName) + " instead."); - } - - final T annotationType = getTypeForAnnotation(annotationMirror); - RetentionPolicy retentionPolicy = getRetentionPolicy(annotationType); - Map annotationValues = populateAnnotationData( - element, - originatingElementIsSameParent, - annotationMirror, - annotationMetadata, - isDeclared, - retentionPolicy, - allowAliases - ); - - if (isDeclared) { - applyTransformations( - listIterator, - annotationMetadata, - true, - annotationType, - annotationValues, - Collections.emptyList(), - null, - annotationMetadata::addDeclaredRepeatable, - annotationMetadata::addDeclaredAnnotation); - } else { - if (isInheritedAnnotation(annotationMirror) || originatingElementIsSameParent) { - applyTransformations( - listIterator, - annotationMetadata, - false, - annotationType, - annotationValues, - Collections.emptyList(), - null, - annotationMetadata::addRepeatable, - annotationMetadata::addAnnotation); - } else { - listIterator.remove(); - } - } - } - for (A annotationMirror : hierarchyCopy) { - String annotationTypeName = getAnnotationTypeName(annotationMirror); - String packageName = NameUtils.getPackageName(annotationTypeName); - if (!AnnotationUtil.STEREOTYPE_EXCLUDES.contains(packageName)) { - processAnnotationStereotype(element, - originatingElementIsSameParent, - annotationMirror, - annotationMetadata, - isDeclared); - } - } + private void addAnnotations(MutableAnnotationMetadata annotationMetadata, + T element, + boolean originatingElementIsSameParent, + boolean isDeclared, + List annotationHierarchy, + List parentAnnotations) { + Stream stream = annotationHierarchy.stream(); + Stream annotationValues = annotationMirrorToAnnotationValue(stream, + element, originatingElementIsSameParent, annotationMetadata, isDeclared, false); + addAnnotations(annotationMetadata, annotationValues, isDeclared, parentAnnotations); } - /** - * Is the given annotation excluded for the specified element. - * - * @param element The element - * @param annotationName The annotation name - * @return True if it is excluded - */ - protected boolean isExcludedAnnotation(@NonNull T element, @NonNull String annotationName) { - return AnnotationUtil.INTERNAL_ANNOTATION_NAMES.contains(annotationName); + @NotNull + private Stream annotationMirrorToAnnotationValue(Stream stream, + T element, + boolean originatingElementIsSameParent, + MutableAnnotationMetadata annotationMetadata, + boolean isDeclared, + boolean isStereotype) { + return stream + .filter(annotationMirror -> { + String annotationName = getAnnotationTypeName(annotationMirror); + if (AnnotationUtil.INTERNAL_ANNOTATION_NAMES.contains(annotationName) + || isExcludedAnnotation(element, annotationName)) { + return false; + } + if (DEPRECATED_ANNOTATION_NAMES.containsKey(annotationName)) { + addWarning(element, + "Usages of deprecated annotation " + annotationName + " found. You should use " + DEPRECATED_ANNOTATION_NAMES.get( + annotationName) + " instead."); + } + return isStereotype || isDeclared || isInheritedAnnotation(annotationMirror) || originatingElementIsSameParent; + }).map(annotationMirror -> createAnnotationValue(element, annotationMirror, annotationMetadata)); } - /** - * Test whether the annotation mirror is inherited. - * - * @param annotationMirror The mirror - * @return True if it is - */ - protected abstract boolean isInheritedAnnotation(@NonNull A annotationMirror); + private ProcessedAnnotation createAnnotationValue(T originatingElement, + A annotationMirror, + MutableAnnotationMetadata metadata) { + String annotationName = getAnnotationTypeName(annotationMirror); + final T annotationType = getTypeForAnnotation(annotationMirror); + RetentionPolicy retentionPolicy = getRetentionPolicy(annotationType); - /** - * Test whether the annotation mirror is inherited. - * - * @param annotationType The mirror - * @return True if it is - */ - protected abstract boolean isInheritedAnnotationType(@NonNull T annotationType); - - private void buildStereotypeHierarchy( - List parents, - T element, - DefaultAnnotationMetadata metadata, - boolean isDeclared, - boolean isInherited, - boolean allowAliases, - List excludes) { - List annotationMirrors = getAnnotationsForType(element); + Map elementValues = readAnnotationRawValues(annotationMirror); + Map annotationValues; + if (CollectionUtils.isEmpty(elementValues)) { + annotationValues = new LinkedHashMap<>(3); + } else { + annotationValues = new LinkedHashMap<>(5); + Set nonBindingMembers = new HashSet<>(2); + for (Map.Entry entry : elementValues.entrySet()) { + T member = entry.getKey(); - LinkedList> interceptorBindings = new LinkedList<>(); - final String lastParent = CollectionUtils.last(parents); - if (!annotationMirrors.isEmpty()) { - - // first add the top level annotations - List topLevel = new ArrayList<>(); - final ListIterator listIterator = annotationMirrors.listIterator(); - while (listIterator.hasNext()) { - A annotationMirror = listIterator.next(); - String annotationName = getAnnotationTypeName(annotationMirror); - if (annotationName.equals(getElementName(element))) { + if (member == null) { continue; } - if (!AnnotationUtil.INTERNAL_ANNOTATION_NAMES.contains(annotationName) && !excludes.contains(annotationName)) { - if (AnnotationUtil.ADVICE_STEREOTYPES.contains(lastParent)) { - if (AnnotationUtil.ANN_INTERCEPTOR_BINDING.equals(annotationName)) { - // skip @InterceptorBinding stereotype handled in last round - continue; - } - } - topLevel.add(annotationMirror); - final T annotationTypeMirror = getTypeForAnnotation(annotationMirror); - final RetentionPolicy retentionPolicy = getRetentionPolicy(annotationTypeMirror); - Map data = populateAnnotationData( - element, - null, - annotationMirror, - metadata, - isDeclared, - retentionPolicy, - allowAliases - ); - - handleAnnotationStereotype( - parents, - metadata, - isDeclared, - isInherited, - interceptorBindings, - lastParent, - listIterator, - annotationTypeMirror, - annotationName, - data - ); - } - } - // remove any annotations stripped out by transformations - topLevel.removeIf((a) -> !annotationMirrors.contains(a)); - // now add meta annotations - for (A annotationMirror : topLevel) { - processAnnotationStereotype( - parents, - annotationMirror, - metadata, - isDeclared, - isInherited - ); - } - } + Object annotationValue = entry.getValue(); + if (hasAnnotations(member)) { + final MutableAnnotationMetadata memberMetadata = new MutableAnnotationMetadata(); + final List annotationsForMember = getAnnotationsForType(member) + .stream().filter((a) -> !getAnnotationTypeName(a).equals(annotationName)) + .toList(); - if (lastParent != null) { - AnnotationMetadata modifiedStereotypes = MUTATED_ANNOTATION_METADATA.get(new MetadataKey<>(element)); - if (modifiedStereotypes != null && !modifiedStereotypes.isEmpty()) { - Set annotationNames = modifiedStereotypes.getAnnotationNames(); - handleModifiedStereotypes(parents, - metadata, - isDeclared, - isInherited, - excludes, - interceptorBindings, - lastParent, - modifiedStereotypes); + addAnnotations(memberMetadata, member, false, + true, annotationsForMember, Collections.emptyList()); - for (String annotationName : annotationNames) { - AnnotationValue a = modifiedStereotypes.getAnnotation(annotationName); - if (a != null) { - String stereotypeName = a.getAnnotationName(); - if (!AnnotationUtil.INTERNAL_ANNOTATION_NAMES.contains(stereotypeName) && !excludes.contains( - stereotypeName)) { - final T annotationType = getAnnotationMirror(stereotypeName).orElse(null); - if (annotationType != null) { - Map values = a.getValues(); - handleAnnotationStereotype( - parents, - metadata, - isDeclared, - isInherited, - interceptorBindings, - lastParent, - null, - annotationType, - annotationName, - values - ); - } else { - // a meta annotation not actually on the classpath - if (isDeclared) { - metadata.addDeclaredStereotype( - parents, - stereotypeName, - a.getValues(), - a.getRetentionPolicy() - ); - } else { - metadata.addStereotype( - parents, - stereotypeName, - a.getValues(), - a.getRetentionPolicy() - ); - } - } + boolean isInstantiatedMember = memberMetadata.hasAnnotation(InstantiatedMember.class); + if (memberMetadata.hasAnnotation(NonBinding.class)) { + final String memberName = getElementName(member); + nonBindingMembers.add(memberName); + } + if (isInstantiatedMember) { + final String memberName = getAnnotationMemberName(member); + final Object rawValue = readAnnotationValue(originatingElement, member, memberName, annotationValue); + if (rawValue instanceof AnnotationClassValue annotationClassValue) { + annotationValues.put(memberName, new AnnotationClassValue<>(annotationClassValue.getName(), true)); } } } - } - } - if (!interceptorBindings.isEmpty()) { - for (AnnotationValueBuilder interceptorBinding : interceptorBindings) { + readAnnotationRawValues(originatingElement, + annotationName, + member, + getAnnotationMemberName(member), + annotationValue, + annotationValues); - if (isDeclared) { - metadata.addDeclaredRepeatable( - AnnotationUtil.ANN_INTERCEPTOR_BINDINGS, - interceptorBinding.build() - ); - } else { - metadata.addRepeatable( - AnnotationUtil.ANN_INTERCEPTOR_BINDINGS, - interceptorBinding.build() - ); - } } - } - } - private void handleModifiedStereotypes(List parents, - DefaultAnnotationMetadata metadata, - boolean isDeclared, - boolean isInherited, - List excludes, - LinkedList> interceptorBindings, - String lastParent, - AnnotationMetadata modifiedStereotypes) { - final Set stereotypeAnnotationNames = modifiedStereotypes.getStereotypeAnnotationNames(); - for (String stereotypeName : stereotypeAnnotationNames) { - final AnnotationValue a = modifiedStereotypes.getAnnotation(stereotypeName); - if (a != null && !AnnotationUtil.INTERNAL_ANNOTATION_NAMES.contains(stereotypeName) && !excludes.contains( - stereotypeName)) { - final T annotationType = getAnnotationMirror(stereotypeName).orElse(null); - final List stereotypeParents = modifiedStereotypes.getAnnotationNamesByStereotype( - stereotypeName); - List resolvedParents = new ArrayList<>(parents); - resolvedParents.addAll(stereotypeParents); - Map values = a.getValues(); - if (annotationType != null) { - - handleAnnotationStereotype( - resolvedParents, - metadata, - isDeclared, - isInherited, - interceptorBindings, - lastParent, - null, - annotationType, - stereotypeName, - values - ); - } else { - metadata.addStereotype( - resolvedParents, - stereotypeName, - values, - RetentionPolicy.RUNTIME + if (!nonBindingMembers.isEmpty()) { + if (hasAnnotation(annotationType, AnnotationUtil.QUALIFIER) || hasAnnotation(annotationType, Qualifier.class)) { + metadata.addDeclaredStereotype( + Collections.singletonList(getAnnotationTypeName(annotationMirror)), + AnnotationUtil.QUALIFIER, + Collections.singletonMap("nonBinding", nonBindingMembers) ); } } } - } - private void handleAnnotationStereotype( - List parents, - DefaultAnnotationMetadata metadata, - boolean isDeclared, - boolean isInherited, - LinkedList> interceptorBindings, - String lastParent, - @Nullable ListIterator listIterator, - T annotationType, - String annotationName, - Map data) { - addToInterceptorBindingsIfNecessary(interceptorBindings, lastParent, annotationName); - - final boolean hasInterceptorBinding = !interceptorBindings.isEmpty(); - if (hasInterceptorBinding && AnnotationUtil.ANN_INTERCEPTOR_BINDING.equals(annotationName)) { - handleMemberBinding(metadata, lastParent, data); - interceptorBindings.getLast().members(data); - return; - } - // special case: don't add stereotype for @Nonnull when it's marked as UNKNOWN/MAYBE/NEVER. - // https://github.com/micronaut-projects/micronaut-core/issues/6795 - if (annotationName.equals("javax.annotation.Nonnull")) { - String when = Objects.toString(data.get("when")); - if (when.equals("UNKNOWN") || when.equals("MAYBE") || when.equals("NEVER")) { - return; - } - } - if (hasInterceptorBinding && Type.class.getName().equals(annotationName)) { - final Object o = data.get(AnnotationMetadata.VALUE_MEMBER); - AnnotationClassValue interceptorType = null; - if (o instanceof AnnotationClassValue) { - interceptorType = (AnnotationClassValue) o; - } else if (o instanceof AnnotationClassValue[]) { - final AnnotationClassValue[] values = (AnnotationClassValue[]) o; - if (values.length > 0) { - interceptorType = values[0]; - } - } - if (interceptorType != null) { - for (AnnotationValueBuilder interceptorBinding : interceptorBindings) { - interceptorBinding.member("interceptorType", interceptorType); - } + Map defaultValues = getCachedAnnotationDefaults(annotationName, annotationType); + + return new ProcessedAnnotation( + annotationType, + new AnnotationValue<>(annotationName, annotationValues, defaultValues, retentionPolicy) + ); + } + + @NotNull + private Map getCachedAnnotationDefaults(String annotationName, T annotationType) { + Map defaultValues; + final Map defaults = ANNOTATION_DEFAULTS.get(annotationName); + if (defaults != null) { + defaultValues = new LinkedHashMap<>(defaults); + } else { + Map annotationDefaultValues = readAnnotationDefaultValues(annotationName, annotationType); + defaultValues = getAnnotationDefaults(annotationType, annotationName, annotationDefaultValues); + if (defaultValues != null) { + // Add the default for any retention type annotation + ANNOTATION_DEFAULTS.put(annotationName, new LinkedHashMap<>(defaultValues)); + } else { + defaultValues = Collections.emptyMap(); } } + return defaultValues; + } - if (isDeclared) { - applyTransformations(listIterator, metadata, true, annotationType, data, parents, interceptorBindings, - (string, av) -> metadata.addDeclaredRepeatableStereotype(parents, string, av), - (string, values, rp) -> metadata.addDeclaredStereotype(parents, string, values, rp)); - } else if (isInherited) { - applyTransformations(listIterator, metadata, false, annotationType, data, parents, interceptorBindings, - (string, av) -> metadata.addRepeatableStereotype(parents, string, av), - (string, values, rp) -> metadata.addStereotype(parents, string, values, rp)); + private void handleAnnotationAlias(T originatingElement, + Map annotationValues, + T annotationMember, + Object annotationValue, + RetentionPolicy retentionPolicy, + List introducedAnnotations) { + Optional> aliases = getAnnotationValues(originatingElement, annotationMember, Aliases.class); + if (aliases.isPresent()) { + for (AnnotationValue av : aliases.get().getAnnotations(AnnotationMetadata.VALUE_MEMBER)) { + processAnnotationAlias( + annotationValues, + annotationValue, + av, + retentionPolicy, + introducedAnnotations + ); + } } else { - if (listIterator != null) { - listIterator.remove(); + Optional> aliasForValues = getAnnotationValues(originatingElement, annotationMember, AliasFor.class); + if (aliasForValues.isPresent()) { + processAnnotationAlias( + annotationValues, + annotationValue, + aliasForValues.get(), + retentionPolicy, + introducedAnnotations + ); } } } - private void handleMemberBinding(DefaultAnnotationMetadata metadata, String lastParent, Map data) { - if (data.containsKey(InterceptorBindingQualifier.META_MEMBER_MEMBERS)) { - final Object o = data.remove(InterceptorBindingQualifier.META_MEMBER_MEMBERS); - if (o instanceof Boolean && ((Boolean) o)) { - Map values = metadata.getValues(lastParent); - if (!values.isEmpty()) { - Set nonBinding = NON_BINDING_CACHE.computeIfAbsent(lastParent, (annotationName) -> { - final HashSet nonBindingResult = new HashSet<>(5); - Map members = getAnnotationMembers(lastParent); - if (CollectionUtils.isNotEmpty(members)) { - members.forEach((name, ann) -> { - if (hasSimpleAnnotation(ann, NonBinding.class.getSimpleName())) { - nonBindingResult.add(name); - } - }); - } - return nonBindingResult.isEmpty() ? Collections.emptySet() : nonBindingResult; - }); + private void addAnnotations(MutableAnnotationMetadata annotationMetadata, + Stream stream, + boolean isDeclared, + List parentAnnotations) { + stream = filterAndTransformAnnotations(stream, parentAnnotations); - if (!nonBinding.isEmpty()) { - values = new HashMap<>(values); - values.keySet().removeAll(nonBinding); - } - final AnnotationValueBuilder builder = - AnnotationValue - .builder(lastParent) - .members(values); - data.put( - InterceptorBindingQualifier.META_MEMBER_MEMBERS, - builder.build() - ); + List introducedAliasForAnnotations = new ArrayList<>(); - } - } - } - } + stream = stream.map(processedAnnotation -> processAliases(processedAnnotation, introducedAliasForAnnotations)); - /** - * Gets the annotation members for the given type. - * - * @param annotationType The annotation type - * @return The members - * @since 3.3.0 - */ - protected abstract @NonNull - Map getAnnotationMembers(@NonNull String annotationType); + List processedAnnotations = addAnnotations(stream, annotationMetadata, isDeclared, false, parentAnnotations).toList(); - /** - * Returns true if a simple meta annotation is present for the given element and annotation type. - * - * @param element The element - * @param simpleName The simple name, ie {@link Class#getSimpleName()} - * @return True an annotation with the given simple name exists on the element - */ - protected abstract boolean hasSimpleAnnotation(T element, String simpleName); + if (CollectionUtils.isNotEmpty(introducedAliasForAnnotations)) { + // Add annotation created by @AliasFor + addStereotypeAnnotations( + introducedAliasForAnnotations.stream(), + null, + parentAnnotations, + annotationMetadata, + isDeclared + ); + } - private void addToInterceptorBindingsIfNecessary(LinkedList> interceptorBindings, - String lastParent, - String annotationName) { - if (lastParent != null) { - AnnotationValueBuilder interceptorBinding = null; - if (AnnotationUtil.ANN_AROUND.equals(annotationName) || AnnotationUtil.ANN_INTERCEPTOR_BINDING.equals(annotationName)) { - interceptorBinding = AnnotationValue.builder(AnnotationUtil.ANN_INTERCEPTOR_BINDING) - .member(AnnotationMetadata.VALUE_MEMBER, new AnnotationClassValue<>(lastParent)) - .member("kind", "AROUND"); - } else if (AnnotationUtil.ANN_INTRODUCTION.equals(annotationName)) { - interceptorBinding = AnnotationValue.builder(AnnotationUtil.ANN_INTERCEPTOR_BINDING) - .member(AnnotationMetadata.VALUE_MEMBER, new AnnotationClassValue<>(lastParent)) - .member("kind", "INTRODUCTION"); - } else if (AnnotationUtil.ANN_AROUND_CONSTRUCT.equals(annotationName)) { - interceptorBinding = AnnotationValue.builder(AnnotationUtil.ANN_INTERCEPTOR_BINDING) - .member(AnnotationMetadata.VALUE_MEMBER, new AnnotationClassValue<>(lastParent)) - .member("kind", "AROUND_CONSTRUCT"); - } - if (interceptorBinding != null) { - interceptorBindings.add(interceptorBinding); - } + // After annotations are processes process their stereotypes + for (ProcessedAnnotation processedAnnotation : processedAnnotations) { + processStereotypes(annotationMetadata, isDeclared, parentAnnotations, processedAnnotation); } } - private void buildStereotypeHierarchy( - List parents, - AnnotationValue annotationValue, - DefaultAnnotationMetadata metadata, - boolean isDeclared, - List excludes) { - List> annotationMirrors = annotationValue.getStereotypes(); + private Stream processInterceptors(Stream annotationValues, + MutableAnnotationMetadata annotationMetadata, + String lastParent, + LinkedList> interceptorBindings) { - LinkedList> interceptorBindings = new LinkedList<>(); - final String lastParent = CollectionUtils.last(parents); - if (CollectionUtils.isNotEmpty(annotationMirrors)) { - - // first add the top level annotations - List> topLevel = new ArrayList<>(); - for (AnnotationValue annotationMirror : annotationMirrors) { - String annotationName = annotationMirror.getAnnotationName(); - if (annotationName.equals(annotationValue.getAnnotationName())) { - continue; - } + return annotationValues + .map(processedAnnotation -> { + AnnotationValue annotationValue = processedAnnotation.getAnnotationValue(); + String annotationName = annotationValue.getAnnotationName(); - if (!AnnotationUtil.INTERNAL_ANNOTATION_NAMES.contains(annotationName) && !excludes.contains(annotationName)) { - if (AnnotationUtil.ADVICE_STEREOTYPES.contains(lastParent)) { - if (AnnotationUtil.ANN_INTERCEPTOR_BINDING.equals(annotationName)) { - // skip @InterceptorBinding stereotype handled in last round - continue; - } - } addToInterceptorBindingsIfNecessary(interceptorBindings, lastParent, annotationName); - final RetentionPolicy retentionPolicy = annotationMirror.getRetentionPolicy(); - - topLevel.add(annotationMirror); - - Map data = annotationMirror.getValues(); - final boolean hasInterceptorBinding = !interceptorBindings.isEmpty(); if (hasInterceptorBinding && AnnotationUtil.ANN_INTERCEPTOR_BINDING.equals(annotationName)) { - handleMemberBinding(metadata, lastParent, data); - interceptorBindings.getLast().members(data); - continue; + annotationValue = handleMemberBinding(annotationMetadata, lastParent, annotationValue); + interceptorBindings.getLast().members(annotationValue.getValues()); + return processedAnnotation.withAnnotationValue(annotationValue); } if (hasInterceptorBinding && Type.class.getName().equals(annotationName)) { - final Object o = data.get(AnnotationMetadata.VALUE_MEMBER); + final Object o = annotationValue.getValues().get(AnnotationMetadata.VALUE_MEMBER); AnnotationClassValue interceptorType = null; - if (o instanceof AnnotationClassValue) { - interceptorType = (AnnotationClassValue) o; - } else if (o instanceof AnnotationClassValue[]) { - final AnnotationClassValue[] values = (AnnotationClassValue[]) o; - if (values.length > 0) { - interceptorType = values[0]; + if (o instanceof AnnotationClassValue annotationClassValue) { + interceptorType = annotationClassValue; + } else if (o instanceof AnnotationClassValue[] annotationClassValues) { + if (annotationClassValues.length > 0) { + interceptorType = annotationClassValues[0]; } } if (interceptorType != null) { @@ -1607,474 +952,519 @@ private void buildStereotypeHierarchy( } } } - - if (isDeclared) { - metadata.addDeclaredStereotype(parents, annotationName, data, retentionPolicy); - } else { - metadata.addStereotype(parents, annotationName, data, retentionPolicy); - } - } - } - // now add meta annotations - for (AnnotationValue annotationMirror : topLevel) { - processAnnotationStereotype(parents, annotationMirror, metadata, isDeclared); - } - } - - if (!interceptorBindings.isEmpty()) { - for (AnnotationValueBuilder interceptorBinding : interceptorBindings) { - - if (isDeclared) { - metadata.addDeclaredRepeatable( - AnnotationUtil.ANN_INTERCEPTOR_BINDINGS, - interceptorBinding.build() - ); - } else { - metadata.addRepeatable( - AnnotationUtil.ANN_INTERCEPTOR_BINDINGS, - interceptorBinding.build() - ); - } - } - } + return processedAnnotation; + }); } - private void processAnnotationStereotype( - T element, - boolean originatingElementIsSameParent, - A annotationMirror, - DefaultAnnotationMetadata annotationMetadata, - boolean isDeclared) { - T annotationType = getTypeForAnnotation(annotationMirror); - String parentAnnotationName = getAnnotationTypeName(annotationMirror); - if (!parentAnnotationName.endsWith(".Nullable")) { - processAnnotationStereotypes( - annotationMetadata, - isDeclared, - isInheritedAnnotation(annotationMirror) || originatingElementIsSameParent, - annotationType, - parentAnnotationName, - Collections.emptyList() - ); - } + private Stream addAnnotations(Stream annotationValues, + MutableAnnotationMetadata annotationMetadata, + boolean isDeclared, + boolean isStereotype, + List parentAnnotations) { + return annotationValues + .peek(processedAnnotation -> { + addAnnotationDefaults(annotationMetadata, processedAnnotation); + addAnnotation(annotationMetadata, parentAnnotations, isDeclared, isStereotype, processedAnnotation); + }); } - private void processAnnotationStereotypes( - DefaultAnnotationMetadata annotationMetadata, - boolean isDeclared, - boolean isInherited, - T annotationType, - String annotationName, - List excludes) { - List parentAnnotations = new ArrayList<>(); - parentAnnotations.add(annotationName); - buildStereotypeHierarchy( - parentAnnotations, - annotationType, - annotationMetadata, - isDeclared, - isInherited, - true, - excludes - ); + private Stream filterAndTransformAnnotations(Stream annotationValues, List parentAnnotations) { + return annotationValues + .filter(processedAnnotation -> { + AnnotationValue annotationValue = processedAnnotation.getAnnotationValue(); + return !AnnotationUtil.INTERNAL_ANNOTATION_NAMES.contains(annotationValue.getAnnotationName()) + && !parentAnnotations.contains(annotationValue.getAnnotationName()); + }) + .flatMap(this::transform) + .flatMap(this::flattenRepeatable); } - private void processAnnotationStereotypes(DefaultAnnotationMetadata annotationMetadata, - boolean isDeclared, - AnnotationValue annotation, - List parents) { - List parentAnnotations = new ArrayList<>(parents); - parentAnnotations.add(annotation.getAnnotationName()); - buildStereotypeHierarchy( - parentAnnotations, - annotation, - annotationMetadata, - isDeclared, - Collections.emptyList() - ); + private Stream transform(ProcessedAnnotation toTransform) { + // Transform annotation using: + // - io.micronaut.inject.annotation.AnnotationMapper + // - io.micronaut.inject.annotation.AnnotationRemapper + // - io.micronaut.inject.annotation.AnnotationTransformer + // Each result of the transformation will be also transformed + // To eliminate infinity loops "processedVisitors" will track and eliminate processed mappers/transformers + Set> processedVisitors = new HashSet<>(); + return transform(toTransform, processedVisitors); } - private void processAnnotationStereotype( - List parents, - A annotationMirror, - DefaultAnnotationMetadata metadata, - boolean isDeclared, - boolean isInherited) { - T typeForAnnotation = getTypeForAnnotation(annotationMirror); - String annotationTypeName = getAnnotationTypeName(annotationMirror); - processAnnotationStereotype(parents, typeForAnnotation, annotationTypeName, metadata, isDeclared, isInherited); + private Stream transform(ProcessedAnnotation toTransform, Set> processedVisitors) { + return processAnnotationMappers(toTransform, processedVisitors) + .flatMap(annotation -> processAnnotationRemappers(annotation, processedVisitors)) + .flatMap(annotation -> processAnnotationTransformers(annotation, processedVisitors)); } - private void processAnnotationStereotype( - List parents, - T annotationType, - String annotationTypeName, - DefaultAnnotationMetadata metadata, - boolean isDeclared, - boolean isInherited) { - List stereoTypeParents = new ArrayList<>(parents); - stereoTypeParents.add(annotationTypeName); - buildStereotypeHierarchy(stereoTypeParents, - annotationType, - metadata, - isDeclared, - isInherited, - true, - Collections.emptyList()); + private Stream flattenRepeatable(ProcessedAnnotation processedAnnotation) { + // In a case of a repeatable container process it as a stream of repeatable annotation values + AnnotationValue annotationValue = processedAnnotation.getAnnotationValue(); + List> repeatableAnnotations = annotationValue.getAnnotations(AnnotationMetadata.VALUE_MEMBER); + boolean isRepeatableAnnotationContainer = !repeatableAnnotations.isEmpty() && repeatableAnnotations.stream() + .allMatch(value -> { + T annotationMirror = getAnnotationMirror(value.getAnnotationName()).orElse(null); + return annotationMirror != null && getRepeatableNameForType(annotationMirror) != null; + }); + if (isRepeatableAnnotationContainer) { + // Repeatable annotations container is being added with values + // We will add every repeatable annotation separately to properly detect its container and run transformations + Map containerValues = new LinkedHashMap<>(annotationValue.getValues()); + containerValues.remove(AnnotationMetadata.VALUE_MEMBER); + return Stream.concat( + Stream.of( + // Add repeatable container for possible stereotype annotation retrieval + // and additional members defined in the container annotation + toProcessedAnnotation(new AnnotationValue<>(annotationValue.getAnnotationName(), containerValues)) + ), + repeatableAnnotations.stream().map(this::toProcessedAnnotation) + ); + } + return Stream.of(processedAnnotation); } - private void processAnnotationStereotype( - List parents, - AnnotationValue annotationType, - DefaultAnnotationMetadata metadata, - boolean isDeclared) { - List stereoTypeParents = new ArrayList<>(parents); - stereoTypeParents.add(annotationType.getAnnotationName()); - buildStereotypeHierarchy(stereoTypeParents, annotationType, metadata, isDeclared, Collections.emptyList()); + private void addAnnotationDefaults(MutableAnnotationMetadata annotationMetadata, ProcessedAnnotation processedAnnotation) { + AnnotationValue annotationValue = processedAnnotation.getAnnotationValue(); + String annotationName = annotationValue.getAnnotationName(); + T annotationType = processedAnnotation.getAnnotationType(); + Map annotationDefaults = annotationValue.getDefaultValues(); + if (annotationDefaults == null && annotationType != null) { + annotationDefaults = getCachedAnnotationDefaults(annotationName, annotationType); + } + annotationMetadata.addDefaultAnnotationValues(annotationName, annotationDefaults, annotationValue.getRetentionPolicy()); } - private void applyTransformations(@Nullable ListIterator hierarchyIterator, - DefaultAnnotationMetadata annotationMetadata, - boolean isDeclared, - @NonNull T annotationType, - Map data, - List parents, - @Nullable LinkedList> interceptorBindings, - BiConsumer addRepeatableAnnotation, - TriConsumer, RetentionPolicy> addAnnotation) { - applyTransformationsForAnnotationType( - hierarchyIterator, - annotationMetadata, - isDeclared, - annotationType, - data, - parents, - interceptorBindings, - addRepeatableAnnotation, - addAnnotation + private ProcessedAnnotation processAliases(ProcessedAnnotation processedAnnotation, List introducedAnnotations) { + T annotationType = processedAnnotation.getAnnotationType(); + if (annotationType == null) { + return processedAnnotation; + } + AnnotationValue annotationValue = processedAnnotation.getAnnotationValue(); + Map newValues = new LinkedHashMap<>(annotationValue.getValues()); + for (Map.Entry entry : annotationValue.getValues().entrySet()) { + CharSequence key = entry.getKey(); + Object value = entry.getValue(); + T member = getAnnotationMember(annotationType, key); + if (member != null) { + handleAnnotationAlias( + annotationType, + newValues, + member, + value, + annotationValue.getRetentionPolicy(), + introducedAnnotations + ); + } + } + + // @AliasFor can modify the annotation values by aliasing to a member from the same annotation + if (newValues.equals(annotationValue.getValues())) { + return processedAnnotation; + } + return processedAnnotation.withAnnotationValue( + AnnotationValue.builder(annotationValue).members(newValues).build() ); } - private void applyTransformationsForAnnotationType( - @Nullable ListIterator hierarchyIterator, - DefaultAnnotationMetadata annotationMetadata, - boolean isDeclared, - @NonNull T annotationType, - Map data, - List parents, - @Nullable LinkedList> interceptorBindings, - BiConsumer addRepeatableAnnotation, - TriConsumer, RetentionPolicy> addAnnotation) { - String annotationName = getElementName(annotationType); + private void processStereotypes(MutableAnnotationMetadata annotationMetadata, + boolean isDeclared, + List parentAnnotations, + ProcessedAnnotation processedAnnotation) { + AnnotationValue annotationValue = processedAnnotation.getAnnotationValue(); + String annotationName = annotationValue.getAnnotationName(); String packageName = NameUtils.getPackageName(annotationName); - String repeatableName = getRepeatableNameForType(annotationType); + if (AnnotationUtil.STEREOTYPE_EXCLUDES.contains(packageName) || annotationName.endsWith(".Nullable")) { + return; + } + T annotationType = processedAnnotation.getAnnotationType(); + List newParentAnnotations = new ArrayList<>(parentAnnotations); + newParentAnnotations.add(annotationName); - RetentionPolicy retentionPolicy = getRetentionPolicy(annotationType); - List annotationRemappers = ANNOTATION_REMAPPERS.get(packageName); - List> annotationTransformers = getAnnotationTransformers(annotationName); - boolean remapped = CollectionUtils.isNotEmpty(annotationRemappers); - boolean transformed = CollectionUtils.isNotEmpty(annotationTransformers); - - if (repeatableName != null) { - if (!remapped && !transformed) { - io.micronaut.core.annotation.AnnotationValue av = new io.micronaut.core.annotation.AnnotationValue(annotationName, - data); - addRepeatableAnnotation.accept(repeatableName, av); - } else if (remapped) { - - VisitorContext visitorContext = createVisitorContext(); - io.micronaut.core.annotation.AnnotationValue av = - new io.micronaut.core.annotation.AnnotationValue<>(annotationName, data); - AnnotationValue repeatableAnn = AnnotationValue.builder(repeatableName) - .values(av) - .build(); - boolean wasRemapped = false; - for (AnnotationRemapper annotationRemapper : annotationRemappers) { - List> remappedRepeatable = annotationRemapper.remap(repeatableAnn, visitorContext); - List> remappedValue = annotationRemapper.remap(av, visitorContext); - if (CollectionUtils.isNotEmpty(remappedRepeatable)) { - for (AnnotationValue repeatable : remappedRepeatable) { - for (AnnotationValue rmv : remappedValue) { - if (rmv == av && remappedValue.size() == 1) { - // bail, the re-mapper just returned the same annotation - addRepeatableAnnotation.accept(repeatableName, av); - break; - } else { - wasRemapped = true; - addRepeatableAnnotation.accept(repeatable.getAnnotationName(), rmv); - } - } - } - } - } - if (wasRemapped && hierarchyIterator != null) { - hierarchyIterator.remove(); + Stream stereotypes; + if (annotationType == null || CollectionUtils.isNotEmpty(annotationValue.getStereotypes())) { + // Annotation is not on the classpath or a transformer/mapper provided a value with custom stereotypes + stereotypes = annotationValue.getStereotypes() == null ? Stream.empty() : annotationValue.getStereotypes().stream().map(this::toProcessedAnnotation); + } else { + stereotypes = annotationMirrorToAnnotationValue(getAnnotationsForType(annotationType).stream(), + annotationType, false, annotationMetadata, isDeclared, true); + } + addStereotypeAnnotations( + stereotypes, + annotationType, + newParentAnnotations, + annotationMetadata, + isDeclared + ); + } + + private void addAnnotation(MutableAnnotationMetadata mutableAnnotationMetadata, + List parentAnnotations, + boolean isDeclared, + boolean isStereotype, + ProcessedAnnotation processedAnnotation) { + AnnotationValue annotationValue = processedAnnotation.getAnnotationValue(); + String repeatableContainer = processedAnnotation.getAnnotationType() == null ? null : getRepeatableNameForType(processedAnnotation.getAnnotationType()); + if (isStereotype) { + if (repeatableContainer != null) { + if (isDeclared) { + mutableAnnotationMetadata.addDeclaredRepeatableStereotype( + parentAnnotations, + repeatableContainer, + annotationValue + ); + } else { + mutableAnnotationMetadata.addRepeatableStereotype( + parentAnnotations, + repeatableContainer, + annotationValue + ); } } else { - VisitorContext visitorContext = createVisitorContext(); - io.micronaut.core.annotation.AnnotationValue av = - new io.micronaut.core.annotation.AnnotationValue<>(annotationName, data); - AnnotationValue repeatableAnn = AnnotationValue.builder(repeatableName).values(av).build(); - final List> repeatableTransformers = getAnnotationTransformers(repeatableName); - if (hierarchyIterator != null) { - hierarchyIterator.remove(); - } - if (CollectionUtils.isNotEmpty(repeatableTransformers)) { - for (AnnotationTransformer repeatableTransformer : repeatableTransformers) { - final List> transformedRepeatable = repeatableTransformer.transform(repeatableAnn, - visitorContext); - for (AnnotationValue annotationValue : transformedRepeatable) { - for (AnnotationTransformer transformer : annotationTransformers) { - final List> tav = transformer.transform(av, visitorContext); - for (AnnotationValue value : tav) { - addRepeatableAnnotation.accept(annotationValue.getAnnotationName(), value); - if (CollectionUtils.isNotEmpty(value.getStereotypes())) { - addTransformedStereotypes(annotationMetadata, isDeclared, value, parents); - } else { - addTransformedStereotypes(annotationMetadata, - isDeclared, - value.getAnnotationName(), - parents); - } - } - } - - } - } + if (isDeclared) { + mutableAnnotationMetadata.addDeclaredStereotype( + parentAnnotations, + annotationValue.getAnnotationName(), + annotationValue.getValues(), + annotationValue.getRetentionPolicy() + ); } else { - for (AnnotationTransformer transformer : annotationTransformers) { - final List> tav = transformer.transform(av, visitorContext); - for (AnnotationValue value : tav) { - addRepeatableAnnotation.accept(repeatableName, value); - if (CollectionUtils.isNotEmpty(value.getStereotypes())) { - addTransformedStereotypes(annotationMetadata, isDeclared, value, parents); - } else { - addTransformedStereotypes(annotationMetadata, isDeclared, value.getAnnotationName(), parents); - } - } - } + mutableAnnotationMetadata.addStereotype( + parentAnnotations, + annotationValue.getAnnotationName(), + annotationValue.getValues(), + annotationValue.getRetentionPolicy() + ); } } } else { - if (!remapped && !transformed) { - addAnnotation.accept(annotationName, data, retentionPolicy); - } else if (remapped) { - io.micronaut.core.annotation.AnnotationValue av = new io.micronaut.core.annotation.AnnotationValue( - annotationName, - data); - VisitorContext visitorContext = createVisitorContext(); - - boolean wasRemapped = false; - for (AnnotationRemapper annotationRemapper : annotationRemappers) { - List> remappedValues = annotationRemapper.remap(av, visitorContext); - if (CollectionUtils.isNotEmpty(remappedValues)) { - for (AnnotationValue annotationValue : remappedValues) { - if (annotationValue == av && remappedValues.size() == 1) { - // bail, the re-mapper just returned the same annotation - addAnnotation.accept(annotationName, data, retentionPolicy); - break; - } else { - wasRemapped = true; - final String transformedAnnotationName = handleTransformedAnnotationValue(parents, - interceptorBindings, - addRepeatableAnnotation, - addAnnotation, - annotationValue, - annotationMetadata); - if (CollectionUtils.isNotEmpty(annotationValue.getStereotypes())) { - addTransformedStereotypes(annotationMetadata, isDeclared, annotationValue, parents); - } else { - addTransformedStereotypes(annotationMetadata, isDeclared, transformedAnnotationName, parents); - } - } - } - } - } - if (wasRemapped && hierarchyIterator != null) { - hierarchyIterator.remove(); + if (repeatableContainer != null) { + if (isDeclared) { + mutableAnnotationMetadata.addDeclaredRepeatable(repeatableContainer, annotationValue); + } else { + mutableAnnotationMetadata.addRepeatable(repeatableContainer, annotationValue); } } else { - io.micronaut.core.annotation.AnnotationValue av = - new io.micronaut.core.annotation.AnnotationValue<>(annotationName, data); - VisitorContext visitorContext = createVisitorContext(); - if (hierarchyIterator != null) { - hierarchyIterator.remove(); - } - for (AnnotationTransformer annotationTransformer : annotationTransformers) { - final List> transformedValues = annotationTransformer.transform(av, visitorContext); - for (AnnotationValue transformedValue : transformedValues) { - final String transformedAnnotationName = handleTransformedAnnotationValue(parents, - interceptorBindings, - addRepeatableAnnotation, - addAnnotation, - transformedValue, - annotationMetadata - - ); - if (CollectionUtils.isNotEmpty(transformedValue.getStereotypes())) { - addTransformedStereotypes(annotationMetadata, isDeclared, transformedValue, parents); - } else { - addTransformedStereotypes(annotationMetadata, isDeclared, transformedAnnotationName, parents); - } - } + if (isDeclared) { + mutableAnnotationMetadata.addDeclaredAnnotation( + annotationValue.getAnnotationName(), + annotationValue.getValues(), + annotationValue.getRetentionPolicy() + ); + } else { + mutableAnnotationMetadata.addAnnotation( + annotationValue.getAnnotationName(), + annotationValue.getValues(), + annotationValue.getRetentionPolicy() + ); } } } } - private String handleTransformedAnnotationValue(List parents, - LinkedList> interceptorBindings, - BiConsumer addRepeatableAnnotation, - TriConsumer, RetentionPolicy> addAnnotation, - AnnotationValue transformedValue, - DefaultAnnotationMetadata annotationMetadata) { - final String transformedAnnotationName = transformedValue.getAnnotationName(); - addTransformedInterceptorBindingsIfNecessary( - parents, - interceptorBindings, - transformedValue, - transformedAnnotationName, - annotationMetadata - ); - final String transformedRepeatableName; + /** + * Is the given annotation excluded for the specified element. + * + * @param element The element + * @param annotationName The annotation name + * @return True if it is excluded + */ + protected boolean isExcludedAnnotation(@NonNull T element, @NonNull String annotationName) { + return AnnotationUtil.INTERNAL_ANNOTATION_NAMES.contains(annotationName); + } - if (isRepeatableCandidate(transformedAnnotationName)) { - String resolvedName = null; - // wrap with exception handling just in case there is any problems loading the type - try { - resolvedName = getAnnotationMirror(transformedAnnotationName) - .map(this::getRepeatableNameForType) - .orElse(null); - } catch (Exception e) { - // ignore + /** + * Test whether the annotation mirror is inherited. + * + * @param annotationMirror The mirror + * @return True if it is + */ + protected abstract boolean isInheritedAnnotation(@NonNull A annotationMirror); + + private void addStereotypeAnnotations(Stream stream, + @Nullable + T element, + List parentAnnotations, + MutableAnnotationMetadata metadata, + boolean isDeclared) { + + final String lastParent = CollectionUtils.last(parentAnnotations); + LinkedList> interceptorBindings = new LinkedList<>(); + + stream = filterAndTransformAnnotations(stream, parentAnnotations); + + stream = stream.filter(processedAnnotation -> { + AnnotationValue annotationValue = processedAnnotation.getAnnotationValue(); + + String annotationName = annotationValue.getAnnotationName(); + if (!AnnotationUtil.INTERNAL_ANNOTATION_NAMES.contains(annotationName) && !parentAnnotations.contains(annotationName)) { + if (AnnotationUtil.ADVICE_STEREOTYPES.contains(lastParent)) { + if (AnnotationUtil.ANN_INTERCEPTOR_BINDING.equals(annotationName)) { + // skip @InterceptorBinding stereotype handled in last round + return false; + } + } } - transformedRepeatableName = resolvedName; - } else { - transformedRepeatableName = null; + + // special case: don't add stereotype for @Nonnull when it's marked as UNKNOWN/MAYBE/NEVER. + // https://github.com/micronaut-projects/micronaut-core/issues/6795 + if (annotationValue.getAnnotationName().equals("javax.annotation.Nonnull")) { + String when = Objects.toString(annotationValue.getValues().get("when")); + return !(when.equals("UNKNOWN") || when.equals("MAYBE") || when.equals("NEVER")); + } + return true; + }); + stream = processInterceptors(stream, metadata, lastParent, interceptorBindings); + + List introducedAliasForAnnotations = new ArrayList<>(); + + stream = stream.map(processedAnnotation -> processAliases(processedAnnotation, introducedAliasForAnnotations)); + + List processedAnnotations = addAnnotations(stream, metadata, isDeclared, true, parentAnnotations).toList(); + + if (CollectionUtils.isNotEmpty(introducedAliasForAnnotations)) { + // Add annotation created by @AliasFor + addStereotypeAnnotations( + introducedAliasForAnnotations.stream(), + null, + parentAnnotations, + metadata, + isDeclared + ); } - if (transformedRepeatableName != null) { - addRepeatableAnnotation.accept(transformedRepeatableName, transformedValue); - } else { - addAnnotation.accept(transformedAnnotationName, - transformedValue.getValues(), - transformedValue.getRetentionPolicy()); + // After annotations are processes process their stereotypes + for (ProcessedAnnotation processedAnnotation : processedAnnotations) { + processStereotypes(metadata, isDeclared, parentAnnotations, processedAnnotation); + } + + handleAnnotationsWithMutatedMetadata(element, parentAnnotations, metadata, isDeclared, lastParent); + + if (!interceptorBindings.isEmpty()) { + for (AnnotationValueBuilder interceptorBinding : interceptorBindings) { + if (isDeclared) { + metadata.addDeclaredRepeatable(AnnotationUtil.ANN_INTERCEPTOR_BINDINGS, interceptorBinding.build()); + } else { + metadata.addRepeatable(AnnotationUtil.ANN_INTERCEPTOR_BINDINGS, interceptorBinding.build()); + } + } } - return transformedAnnotationName; } - private void addTransformedInterceptorBindingsIfNecessary(List parents, - LinkedList> interceptorBindings, - AnnotationValue transformedValue, - String transformedAnnotationName, - DefaultAnnotationMetadata annotationMetadata) { - if (interceptorBindings != null && !parents.isEmpty() && AnnotationUtil.ANN_INTERCEPTOR_BINDING.equals( - transformedAnnotationName)) { - final AnnotationValueBuilder newBuilder = AnnotationValue - .builder(transformedAnnotationName, transformedValue.getRetentionPolicy()) - .members(transformedValue.getValues()); - if (!transformedValue.contains(AnnotationMetadata.VALUE_MEMBER)) { - newBuilder.value(parents.get(parents.size() - 1)); + private void handleAnnotationsWithMutatedMetadata(T element, + List parentAnnotations, + MutableAnnotationMetadata metadata, + boolean isDeclared, + String lastParent) { + if (lastParent != null && element != null) { + CachedAnnotationMetadata modifiedStereotypes = MUTATED_ANNOTATION_METADATA.get(new MetadataKey<>(element)); + if (modifiedStereotypes != null && !modifiedStereotypes.isEmpty() && modifiedStereotypes.isMutated()) { + for (String stereotypeName : modifiedStereotypes.getStereotypeAnnotationNames()) { + final AnnotationValue a = modifiedStereotypes.getAnnotation(stereotypeName); + if (a == null) { + continue; + } + final List stereotypeParents = modifiedStereotypes.getAnnotationNamesByStereotype(stereotypeName); + List newParentAnnotations = new ArrayList<>(parentAnnotations); + newParentAnnotations.addAll(stereotypeParents); + + addStereotypeAnnotations( + Stream.of(toProcessedAnnotation(a)), + null, + newParentAnnotations, + metadata, + isDeclared + ); + } + + for (String annotationName : modifiedStereotypes.getAnnotationNames()) { + AnnotationValue a = modifiedStereotypes.getAnnotation(annotationName); + if (a == null) { + continue; + } + addStereotypeAnnotations( + Stream.of(toProcessedAnnotation(a)), + null, + parentAnnotations, + metadata, + isDeclared + ); + } + } - if (transformedValue.booleanValue("bindMembers").orElse(false)) { + } + } - final String parent = CollectionUtils.last(parents); - final HashMap data = new HashMap<>(transformedValue.getValues()); - handleMemberBinding( - annotationMetadata, - parent, - data + private AnnotationValue handleMemberBinding(DefaultAnnotationMetadata metadata, String lastParent, AnnotationValue annotationValue) { + Map data = annotationValue.getValues(); + if (!data.containsKey(InterceptorBindingQualifier.META_MEMBER_MEMBERS)) { + return annotationValue; + } + data = new LinkedHashMap<>(data); + final Object o = data.remove(InterceptorBindingQualifier.META_MEMBER_MEMBERS); + if (o instanceof Boolean && ((Boolean) o)) { + Map values = metadata.getValues(lastParent); + if (!values.isEmpty()) { + Set nonBinding = NON_BINDING_CACHE.computeIfAbsent(lastParent, (annotationName) -> { + final HashSet nonBindingResult = new HashSet<>(5); + Map members = getAnnotationMembers(lastParent); + if (CollectionUtils.isNotEmpty(members)) { + members.forEach((name, ann) -> { + if (hasSimpleAnnotation(ann, NonBinding.class.getSimpleName())) { + nonBindingResult.add(name); + } + }); + } + return nonBindingResult.isEmpty() ? Collections.emptySet() : nonBindingResult; + }); + + if (!nonBinding.isEmpty()) { + values = new HashMap<>(values); + values.keySet().removeAll(nonBinding); + } + final AnnotationValueBuilder builder = + AnnotationValue + .builder(lastParent) + .members(values); + data.put( + InterceptorBindingQualifier.META_MEMBER_MEMBERS, + builder.build() ); - newBuilder.members(data); + } - interceptorBindings.add(newBuilder); } + return AnnotationValue.builder(annotationValue).members(data).build(); } - private List> remapAnnotation(String annotationName) { - String packageName = NameUtils.getPackageName(annotationName); - List annotationRemappers = ANNOTATION_REMAPPERS.get(packageName); - List> mappedAnnotations = new ArrayList<>(); - if (annotationRemappers == null || annotationRemappers.isEmpty()) { - mappedAnnotations.add(AnnotationValue.builder(annotationName).build()); - return mappedAnnotations; - } + /** + * Gets the annotation members for the given type. + * + * @param annotationType The annotation type + * @return The members + * @since 3.3.0 + */ + @NonNull + protected abstract Map getAnnotationMembers(@NonNull String annotationType); - VisitorContext visitorContext = createVisitorContext(); - io.micronaut.core.annotation.AnnotationValue av = new AnnotationValue<>(annotationName); + /** + * Returns true if a simple meta annotation is present for the given element and annotation type. + * + * @param element The element + * @param simpleName The simple name, ie {@link Class#getSimpleName()} + * @return True an annotation with the given simple name exists on the element + */ + protected abstract boolean hasSimpleAnnotation(T element, String simpleName); - for (AnnotationRemapper annotationRemapper : annotationRemappers) { - List> remappedValues = annotationRemapper.remap(av, visitorContext); - if (CollectionUtils.isNotEmpty(remappedValues)) { - for (AnnotationValue annotationValue : remappedValues) { - if (annotationValue == av && remappedValues.size() == 1) { - // bail, the re-mapper just returned the same annotation - break; - } else { - mappedAnnotations.add(annotationValue); - } - } + private void addToInterceptorBindingsIfNecessary(List> interceptorBindings, + String lastParent, + String annotationName) { + if (lastParent != null) { + AnnotationValueBuilder interceptorBinding = null; + if (AnnotationUtil.ANN_AROUND.equals(annotationName) || AnnotationUtil.ANN_INTERCEPTOR_BINDING.equals(annotationName)) { + interceptorBinding = AnnotationValue.builder(AnnotationUtil.ANN_INTERCEPTOR_BINDING) + .member(AnnotationMetadata.VALUE_MEMBER, new AnnotationClassValue<>(lastParent)) + .member("kind", "AROUND"); + } else if (AnnotationUtil.ANN_INTRODUCTION.equals(annotationName)) { + interceptorBinding = AnnotationValue.builder(AnnotationUtil.ANN_INTERCEPTOR_BINDING) + .member(AnnotationMetadata.VALUE_MEMBER, new AnnotationClassValue<>(lastParent)) + .member("kind", "INTRODUCTION"); + } else if (AnnotationUtil.ANN_AROUND_CONSTRUCT.equals(annotationName)) { + interceptorBinding = AnnotationValue.builder(AnnotationUtil.ANN_INTERCEPTOR_BINDING) + .member(AnnotationMetadata.VALUE_MEMBER, new AnnotationClassValue<>(lastParent)) + .member("kind", "AROUND_CONSTRUCT"); + } + if (interceptorBinding != null) { + interceptorBindings.add(interceptorBinding); } } - return mappedAnnotations; } - private boolean isRepeatableCandidate(String transformedAnnotationName) { - return !AnnotationUtil.INTERNAL_ANNOTATION_NAMES.contains(transformedAnnotationName) && - !AnnotationUtil.NULLABLE.equals(transformedAnnotationName) && - !AnnotationUtil.NON_NULL.equals(transformedAnnotationName); + private List eliminateProcessed(List visitors, Set> processedVisitors) { + if (visitors == null) { + return null; + } + return visitors.stream().filter(v -> !processedVisitors.contains(v.getClass())).toList(); } - private void addTransformedStereotypes(DefaultAnnotationMetadata annotationMetadata, - boolean isDeclared, - String transformedAnnotationName, - List parents) { - if (!AnnotationUtil.INTERNAL_ANNOTATION_NAMES.contains(transformedAnnotationName)) { - String packageName = NameUtils.getPackageName(transformedAnnotationName); - if (!AnnotationUtil.STEREOTYPE_EXCLUDES.contains(packageName)) { - getAnnotationMirror(transformedAnnotationName).ifPresent(a -> processAnnotationStereotypes( - annotationMetadata, - isDeclared, - false, - a, - transformedAnnotationName, - parents - )); + private Stream processAnnotationRemappers(ProcessedAnnotation processedAnnotation, + Set> processedVisitors) { + AnnotationValue annotationValue = processedAnnotation.getAnnotationValue(); + String packageName = NameUtils.getPackageName(annotationValue.getAnnotationName()); + List annotationRemappers = ANNOTATION_REMAPPERS.get(packageName); + annotationRemappers = eliminateProcessed(annotationRemappers, processedVisitors); + if (CollectionUtils.isEmpty(annotationRemappers)) { + return Stream.of(processedAnnotation); + } + VisitorContext visitorContext = createVisitorContext(); + List result = new ArrayList<>(); + for (AnnotationRemapper annotationRemapper : annotationRemappers) { + processedVisitors.add(annotationRemapper.getClass()); + for (AnnotationValue newAnnotationValue : annotationRemapper.remap(annotationValue, visitorContext)) { + if (newAnnotationValue == annotationValue) { + result.add(processedAnnotation); // Retain the same value + } else { + result.add(toProcessedAnnotation(newAnnotationValue)); + } } } + // Transform new remapped annotations + return result.stream().flatMap(annotation -> transform(annotation, processedVisitors)); } - private void addTransformedStereotypes(DefaultAnnotationMetadata annotationMetadata, - boolean isDeclared, - AnnotationValue transformedAnnotation, - List parents) { - String transformedAnnotationName = transformedAnnotation.getAnnotationName(); - if (!AnnotationUtil.INTERNAL_ANNOTATION_NAMES.contains(transformedAnnotationName)) { - String packageName = NameUtils.getPackageName(transformedAnnotationName); - if (!AnnotationUtil.STEREOTYPE_EXCLUDES.contains(packageName)) { - processAnnotationStereotypes( - annotationMetadata, - isDeclared, - transformedAnnotation, - parents); + private Stream processAnnotationTransformers(ProcessedAnnotation processedAnnotation, + Set> processedVisitors) { + AnnotationValue annotationValue = (AnnotationValue) processedAnnotation.getAnnotationValue(); + List> annotationTransformers = getAnnotationTransformers(annotationValue.getAnnotationName()); + annotationTransformers = eliminateProcessed(annotationTransformers, processedVisitors); + if (CollectionUtils.isEmpty(annotationTransformers)) { + return Stream.of(processedAnnotation); + } + VisitorContext visitorContext = createVisitorContext(); + List result = new ArrayList<>(); + for (AnnotationTransformer annotationTransformer : annotationTransformers) { + processedVisitors.add(annotationTransformer.getClass()); + for (AnnotationValue newAnnotationValue : annotationTransformer.transform(annotationValue, visitorContext)) { + if (newAnnotationValue == annotationValue) { + result.add(processedAnnotation); // Retain the same value + } else { + result.add(toProcessedAnnotation(newAnnotationValue)); + } } } + // Transform new transformed annotations + return result.stream().flatMap(annotation -> transform(annotation, processedVisitors)); } - /** - * Used to store metadata mutations at compilation time. Not for public consumption. - * - * @param owningType The owning type - * @param element The element - * @return True if the annotation metadata was mutated - */ - @Internal - public boolean isMetadataMutated(T owningType, T element) { - if (element != null) { - CachedAnnotationMetadata entry = MUTATED_ANNOTATION_METADATA.get(new MetadataKey(owningType, element)); - return entry != null && entry.isMutated(); + private Stream processAnnotationMappers(ProcessedAnnotation processedAnnotation, + Set> processedVisitors) { + AnnotationValue annotationValue = (AnnotationValue) processedAnnotation.getAnnotationValue(); + List> mappers = getAnnotationMappers(annotationValue.getAnnotationName()); + mappers = eliminateProcessed(mappers, processedVisitors); + if (CollectionUtils.isEmpty(mappers)) { + return Stream.of(processedAnnotation); } - return false; + VisitorContext visitorContext = createVisitorContext(); + List result = new ArrayList<>(); + result.add(processedAnnotation); // Mapper retains the original value + for (AnnotationMapper mapper : mappers) { + processedVisitors.add(mapper.getClass()); + List> mappedToAnnotationValues = mapper.map(annotationValue, visitorContext); + if (mappedToAnnotationValues != null) { + for (AnnotationValue mappedToAnnotationValue : mappedToAnnotationValues) { + if (mappedToAnnotationValue != annotationValue) { + result.add(toProcessedAnnotation(mappedToAnnotationValue)); + } + // else: Mapper returned the same value, but it's already included + } + } + } + // Transform new mapped annotations + return result.stream().flatMap(annotation -> transform(annotation, processedVisitors)); + } + + private ProcessedAnnotation toProcessedAnnotation(AnnotationValue av) { + return new ProcessedAnnotation( + getAnnotationMirror(av.getAnnotationName()).orElse(null), + av + ); } /** @@ -2103,21 +1493,6 @@ public static void copyToRuntime() { ANNOTATION_DEFAULTS.forEach(DefaultAnnotationMetadata::registerAnnotationDefaults); } - /** - * Returns whether the given annotation is a mapped annotation. - * - * @param annotationName The annotation name - * @return True if it is - */ - @Internal - public static boolean isAnnotationMapped(@Nullable String annotationName) { - return annotationName != null && - ( - ANNOTATION_MAPPERS.containsKey(annotationName) || - ANNOTATION_TRANSFORMERS.containsKey(annotationName) || - ANNOTATION_TRANSFORMERS.keySet().stream().anyMatch(annotationName::startsWith)); - } - /** * @return Additional mapped annotation names */ @@ -2144,55 +1519,27 @@ public static Set getMappedAnnotationPackages() { * @param The annotation type * @return The mutated metadata */ - public AnnotationMetadata annotate( - AnnotationMetadata annotationMetadata, - AnnotationValue annotationValue) { - String annotationName = annotationValue.getAnnotationName(); + public AnnotationMetadata annotate(AnnotationMetadata annotationMetadata, + AnnotationValue annotationValue) { final boolean isReference = annotationMetadata instanceof AnnotationMetadataReference; boolean isReferenceOrEmpty = annotationMetadata == AnnotationMetadata.EMPTY_METADATA || isReference; - if (annotationMetadata instanceof DefaultAnnotationMetadata || isReferenceOrEmpty) { - final DefaultAnnotationMetadata defaultMetadata = isReferenceOrEmpty - ? new MutableAnnotationMetadata() - : (DefaultAnnotationMetadata) annotationMetadata; - T annotationMirror = getAnnotationMirror(annotationName).orElse(null); - if (annotationMirror != null) { - applyTransformationsForAnnotationType( - null, - defaultMetadata, + if (annotationMetadata instanceof MutableAnnotationMetadata || isReferenceOrEmpty) { + final MutableAnnotationMetadata mutableAnnotationMetadata = isReferenceOrEmpty + ? new MutableAnnotationMetadata() + : (MutableAnnotationMetadata) annotationMetadata; + + addAnnotations( + mutableAnnotationMetadata, + Stream.of(toProcessedAnnotation(annotationValue)), true, - annotationMirror, - annotationValue.getValues(), - Collections.emptyList(), - new LinkedList<>(), - defaultMetadata::addDeclaredRepeatable, - defaultMetadata::addDeclaredAnnotation - ); - processAnnotationDefaults( - annotationMirror, - defaultMetadata, - annotationName, - () -> readAnnotationDefaultValues(annotationName, annotationMirror) - ); - processAnnotationStereotypes( - defaultMetadata, - true, - isInheritedAnnotationType(annotationMirror), - annotationMirror, - annotationName, - DEFAULT_ANNOTATE_EXCLUDES - ); - } else { - defaultMetadata.addDeclaredAnnotation( - annotationName, - annotationValue.getValues() - ); - } + Collections.emptyList() + ); if (isReference) { AnnotationMetadataReference ref = (AnnotationMetadataReference) annotationMetadata; - return new AnnotationMetadataHierarchy(ref, defaultMetadata); + return new AnnotationMetadataHierarchy(ref, mutableAnnotationMetadata); } else { - return defaultMetadata; + return mutableAnnotationMetadata; } } else if (annotationMetadata instanceof AnnotationMetadataHierarchy hierarchy) { AnnotationMetadata declaredMetadata = annotate(hierarchy.getDeclaredMetadata(), annotationValue); @@ -2292,8 +1639,8 @@ public AnnotationMetadata removeStereotype(AnnotationMetadata annotationMetadata * @return The potentially modified metadata */ public @NonNull AnnotationMetadata removeAnnotationIf( - @NonNull AnnotationMetadata annotationMetadata, - @NonNull Predicate> predicate) { + @NonNull AnnotationMetadata annotationMetadata, + @NonNull Predicate> predicate) { if (annotationMetadata.isEmpty()) { return annotationMetadata; } @@ -2315,6 +1662,37 @@ public AnnotationMetadata removeStereotype(AnnotationMetadata annotationMetadata throw new IllegalStateException(MSG_UNRECOGNIZED_ANNOTATION_METADATA + annotationMetadata); } + /** + * Simple tuple object combining the annotation value plus the native annotation type. + * NOTE: Some implementation like Groovy don't return correct annotation native type with type hierarchies. + * We need to carry the provided type. + * + * @since 4.0.0 + */ + private final class ProcessedAnnotation { + @Nullable + private final T annotationType; + private final AnnotationValue annotationValue; + + private ProcessedAnnotation(@Nullable T annotationType, AnnotationValue annotationValue) { + this.annotationType = annotationType; + this.annotationValue = annotationValue; + } + + public ProcessedAnnotation withAnnotationValue(AnnotationValue annotationValue) { + return new ProcessedAnnotation(annotationType, annotationValue); + } + + @Nullable + public T getAnnotationType() { + return annotationType; + } + + public AnnotationValue getAnnotationValue() { + return annotationValue; + } + } + /** * The caching entry. * diff --git a/core-processor/src/main/java/io/micronaut/inject/annotation/AnnotationMetadataWriter.java b/core-processor/src/main/java/io/micronaut/inject/annotation/AnnotationMetadataWriter.java index 9ce4fb46bee..c7f06d46d2e 100644 --- a/core-processor/src/main/java/io/micronaut/inject/annotation/AnnotationMetadataWriter.java +++ b/core-processor/src/main/java/io/micronaut/inject/annotation/AnnotationMetadataWriter.java @@ -509,6 +509,8 @@ private ClassWriter generateClassBytes() { constructor.visitInsn(RETURN); constructor.visitMaxs(1, 1); constructor.visitEnd(); + defaultsStorage.clear(); // Defaults were valid only in the constructor scope + if (writeAnnotationDefaults) { writeAnnotationDefaults(annotationMetadata, classWriter, owningType, defaultsStorage, loadTypeMethods); } diff --git a/core-processor/src/main/java/io/micronaut/inject/beans/visitor/IntrospectedTypeElementVisitor.java b/core-processor/src/main/java/io/micronaut/inject/beans/visitor/IntrospectedTypeElementVisitor.java index dd9799db59a..2b95d97d51e 100644 --- a/core-processor/src/main/java/io/micronaut/inject/beans/visitor/IntrospectedTypeElementVisitor.java +++ b/core-processor/src/main/java/io/micronaut/inject/beans/visitor/IntrospectedTypeElementVisitor.java @@ -242,9 +242,7 @@ private void processElement(boolean metadata, writers.put(writer.getBeanType().getClassName(), writer); - if (!ce.isAbstract() || constructorElement.isPresent() || !ce.hasStereotype(Introspected.class)) { - addExecutableMethods(ce, writer, beanProperties); - } + addExecutableMethods(ce, writer, beanProperties); } private void addExecutableMethods(ClassElement ce, BeanIntrospectionWriter writer, List beanProperties) { @@ -253,11 +251,11 @@ private void addExecutableMethods(ClassElement ce, BeanIntrospectionWriter write if (beanProperty.isExcluded()) { continue; } - beanProperty.getReadMethod().filter(m -> m.hasStereotype(Executable.class)).ifPresent(methodElement -> { + beanProperty.getReadMethod().filter(m -> m.hasStereotype(Executable.class) && !m.isAbstract()).ifPresent(methodElement -> { added.add(methodElement); writer.visitBeanMethod(methodElement); }); - beanProperty.getWriteMethod().filter(m -> m.hasStereotype(Executable.class)).ifPresent(methodElement -> { + beanProperty.getWriteMethod().filter(m -> m.hasStereotype(Executable.class) && !m.isAbstract()).ifPresent(methodElement -> { added.add(methodElement); writer.visitBeanMethod(methodElement); }); diff --git a/core-processor/src/main/java/io/micronaut/inject/writer/BeanDefinitionWriter.java b/core-processor/src/main/java/io/micronaut/inject/writer/BeanDefinitionWriter.java index 37ec9c8565e..9f3cf001254 100644 --- a/core-processor/src/main/java/io/micronaut/inject/writer/BeanDefinitionWriter.java +++ b/core-processor/src/main/java/io/micronaut/inject/writer/BeanDefinitionWriter.java @@ -110,7 +110,6 @@ import java.io.IOException; import java.io.OutputStream; import java.lang.annotation.Annotation; -import java.lang.annotation.Repeatable; import java.lang.reflect.Constructor; import java.lang.reflect.Method; import java.time.Duration; @@ -2322,13 +2321,11 @@ private void pushQualifierForAnnotation(GeneratorAdapter generatorAdapter, t ); } else { - final String repeatableName = visitorContext - .getClassElement(annotationName) - .flatMap(ce -> ce.stringValue(Repeatable.class)).orElse(null); + final String repeatableContainerName = element.findRepeatableAnnotation(annotationName).orElse(null); resolveArgument.run(); retrieveAnnotationMetadataFromProvider(generatorAdapter); - if (repeatableName != null) { - generatorAdapter.push(repeatableName); + if (repeatableContainerName != null) { + generatorAdapter.push(repeatableContainerName); generatorAdapter.invokeStatic(TYPE_QUALIFIERS, METHOD_QUALIFIER_BY_REPEATABLE_ANNOTATION); } else { generatorAdapter.push(annotationName); diff --git a/core/src/main/java/io/micronaut/core/annotation/AnnotationMetadata.java b/core/src/main/java/io/micronaut/core/annotation/AnnotationMetadata.java index d93f016a12a..ace0c41212c 100644 --- a/core/src/main/java/io/micronaut/core/annotation/AnnotationMetadata.java +++ b/core/src/main/java/io/micronaut/core/annotation/AnnotationMetadata.java @@ -362,7 +362,7 @@ default boolean hasDeclaredStereotype(@Nullable String... annotations) { * @param annotation The annotation name * @return The default values */ - default @NonNull Map getDefaultValues(@NonNull String annotation) { + default @NonNull Map getDefaultValues(@NonNull String annotation) { return Collections.emptyMap(); } diff --git a/core/src/main/java/io/micronaut/core/annotation/AnnotationMetadataDelegate.java b/core/src/main/java/io/micronaut/core/annotation/AnnotationMetadataDelegate.java index 1b15c9f4731..d63004f715e 100644 --- a/core/src/main/java/io/micronaut/core/annotation/AnnotationMetadataDelegate.java +++ b/core/src/main/java/io/micronaut/core/annotation/AnnotationMetadataDelegate.java @@ -237,7 +237,7 @@ default OptionalDouble doubleValue(@NonNull Class annotati @NonNull @Override - default Map getDefaultValues(@NonNull String annotation) { + default Map getDefaultValues(@NonNull String annotation) { return getAnnotationMetadata().getDefaultValues(annotation); } diff --git a/core/src/main/java/io/micronaut/core/annotation/AnnotationUtil.java b/core/src/main/java/io/micronaut/core/annotation/AnnotationUtil.java index 948aa36a3aa..ec81d4b2ab6 100644 --- a/core/src/main/java/io/micronaut/core/annotation/AnnotationUtil.java +++ b/core/src/main/java/io/micronaut/core/annotation/AnnotationUtil.java @@ -51,7 +51,9 @@ public class AnnotationUtil { "kotlin.annotation.MustBeDocumented", Target.class.getName(), "kotlin.annotation.Target", - KOTLIN_METADATA + Experimental.class.getName(), + KOTLIN_METADATA, + "jdk.internal.ValueBased" ); /** @@ -61,7 +63,8 @@ public class AnnotationUtil { "javax.annotation", "java.lang.annotation", "io.micronaut.core.annotation", - "edu.umd.cs.findbugs.annotations" + "edu.umd.cs.findbugs.annotations", + "jdk.internal" ); /** diff --git a/core/src/main/java/io/micronaut/core/annotation/AnnotationValue.java b/core/src/main/java/io/micronaut/core/annotation/AnnotationValue.java index 6d6997a3f5d..ee23effa46a 100644 --- a/core/src/main/java/io/micronaut/core/annotation/AnnotationValue.java +++ b/core/src/main/java/io/micronaut/core/annotation/AnnotationValue.java @@ -52,9 +52,11 @@ public class AnnotationValue implements AnnotationValueRes private final String annotationName; private final ConvertibleValues convertibleValues; private final Map values; - private final Map defaultValues; + @Nullable + private final Map defaultValues; private final Function valueMapper; private final RetentionPolicy retentionPolicy; + @Nullable private final List> stereotypes; /** @@ -64,7 +66,7 @@ public class AnnotationValue implements AnnotationValueRes @UsedByGeneratedCode @Internal public AnnotationValue(String annotationName, Map values) { - this(annotationName, values, Collections.emptyMap()); + this(annotationName, values, null, RetentionPolicy.RUNTIME); } /** @@ -74,7 +76,7 @@ public AnnotationValue(String annotationName, Map values) */ @Internal public AnnotationValue(String annotationName, Map values, RetentionPolicy retentionPolicy) { - this(annotationName, values, Collections.emptyMap(), retentionPolicy, null); + this(annotationName, values, null, retentionPolicy, null); } /** @@ -85,7 +87,7 @@ public AnnotationValue(String annotationName, Map values, */ @Internal public AnnotationValue(String annotationName, Map values, RetentionPolicy retentionPolicy, List> stereotypes) { - this(annotationName, values, Collections.emptyMap(), retentionPolicy, stereotypes); + this(annotationName, values, null, retentionPolicy, stereotypes); } @@ -96,7 +98,7 @@ public AnnotationValue(String annotationName, Map values, */ @UsedByGeneratedCode @Internal - public AnnotationValue(String annotationName, Map values, Map defaultValues) { + public AnnotationValue(String annotationName, Map values, Map defaultValues) { this(annotationName, values, defaultValues, RetentionPolicy.RUNTIME, null); } @@ -108,7 +110,7 @@ public AnnotationValue(String annotationName, Map values, */ @UsedByGeneratedCode @Internal - public AnnotationValue(String annotationName, Map values, Map defaultValues, RetentionPolicy retentionPolicy) { + public AnnotationValue(String annotationName, Map values, Map defaultValues, RetentionPolicy retentionPolicy) { this(annotationName, values, defaultValues, retentionPolicy, null); } @@ -120,11 +122,11 @@ public AnnotationValue(String annotationName, Map values, * @param stereotypes The stereotypes of the annotation */ @Internal - public AnnotationValue(String annotationName, Map values, Map defaultValues, RetentionPolicy retentionPolicy, List> stereotypes) { + public AnnotationValue(String annotationName, Map values, Map defaultValues, RetentionPolicy retentionPolicy, List> stereotypes) { this.annotationName = annotationName; this.convertibleValues = newConvertibleValues(values); this.values = values; - this.defaultValues = defaultValues != null ? defaultValues : Collections.emptyMap(); + this.defaultValues = defaultValues; this.valueMapper = null; this.retentionPolicy = retentionPolicy != null ? retentionPolicy : RetentionPolicy.RUNTIME; this.stereotypes = stereotypes; @@ -147,9 +149,8 @@ public AnnotationValue(String annotationName) { public AnnotationValue(String annotationName, ConvertibleValues convertibleValues) { this.annotationName = annotationName; this.convertibleValues = convertibleValues; - Map existing = convertibleValues.asMap(); - this.values = new HashMap<>(existing); - this.defaultValues = Collections.emptyMap(); + this.values = new LinkedHashMap<>(convertibleValues.asMap()); + this.defaultValues = null; this.valueMapper = null; this.retentionPolicy = RetentionPolicy.RUNTIME; this.stereotypes = null; @@ -165,13 +166,12 @@ public AnnotationValue(String annotationName, ConvertibleValues converti */ @Internal @UsedByGeneratedCode - protected AnnotationValue( - AnnotationValue target, - Map defaultValues, - ConvertibleValues convertibleValues, - Function valueMapper) { + protected AnnotationValue(AnnotationValue target, + Map defaultValues, + ConvertibleValues convertibleValues, + Function valueMapper) { this.annotationName = target.annotationName; - this.defaultValues = defaultValues != null ? defaultValues : target.defaultValues; + this.defaultValues = defaultValues; this.values = target.values; this.convertibleValues = convertibleValues; this.valueMapper = valueMapper; @@ -195,6 +195,16 @@ public List> getStereotypes() { return stereotypes; } + /** + * The default values. + * @return The default of the annotation or null if not specified. + * @since 4.0.0 + */ + @Nullable + public Map getDefaultValues() { + return defaultValues; + } + /** * Resolves a map of properties for a member that is an array of annotations that have members called "name" or "key" to represent the key and "value" to represent the value. * @@ -256,7 +266,6 @@ public Map getProperties(@NonNull String member, String keyMembe * @return An {@link Optional} of the enum value */ @Override - @SuppressWarnings("unchecked") public Optional enumValue(@NonNull String member, @NonNull Class enumType) { return enumValue(member, enumType, valueMapper); } @@ -1025,8 +1034,8 @@ public boolean isFalse(String member) { * * @return The annotation name */ - public @NonNull - final String getAnnotationName() { + @NonNull + public final String getAnnotationName() { return annotationName; } @@ -1045,8 +1054,8 @@ public final boolean contains(String member) { * * @return The names of the members */ - public @NonNull - final Set getMemberNames() { + @NonNull + public final Set getMemberNames() { return values.keySet(); } @@ -1054,17 +1063,16 @@ final Set getMemberNames() { * @return The attribute values */ @Override - @SuppressWarnings("unchecked") - public @NonNull - Map getValues() { + @NonNull + public Map getValues() { return Collections.unmodifiableMap(values); } /** * @return The convertible values */ - public @NonNull - ConvertibleValues getConvertibleValues() { + @NonNull + public ConvertibleValues getConvertibleValues() { return convertibleValues; } @@ -1074,9 +1082,11 @@ public Optional get(CharSequence member, ArgumentConversionContext con if (result.isPresent()) { return result; } - Object dv = defaultValues.get(member.toString()); - if (dv != null) { - return ConversionService.SHARED.convert(dv, conversionContext); + if (defaultValues != null) { + Object dv = defaultValues.get(member.toString()); + if (dv != null) { + return ConversionService.SHARED.convert(dv, conversionContext); + } } return result; } @@ -1122,8 +1132,8 @@ public final Optional getValue(Class type) { * @return The result * @throws IllegalStateException If no member is available that conforms to the given type */ - public @NonNull - final T getRequiredValue(Class type) { + @NonNull + public final T getRequiredValue(Class type) { return getRequiredValue(AnnotationMetadata.VALUE_MEMBER, type); } @@ -1136,8 +1146,8 @@ final T getRequiredValue(Class type) { * @return The result * @throws IllegalStateException If no member is available that conforms to the given name and type */ - public @NonNull - final T getRequiredValue(String member, Class type) { + @NonNull + public final T getRequiredValue(String member, Class type) { return get(member, ConversionContext.of(type)).orElseThrow(() -> new IllegalStateException("No value available for annotation member @" + annotationName + "[" + member + "] of type: " + type)); } @@ -1150,7 +1160,8 @@ final T getRequiredValue(String member, Class type) { * @return The result * @throws IllegalStateException If no member is available that conforms to the given name and type */ - public final @NonNull List> getAnnotations(String member, Class type) { + @NonNull + public final List> getAnnotations(String member, Class type) { ArgumentUtils.requireNonNull("type", type); String typeName = type.getName(); @@ -1370,6 +1381,19 @@ public static AnnotationValueBuilder builder(Class return new AnnotationValueBuilder<>(annotation); } + /** + * Start building a new annotation existing value. + * + * @param annotation The annotation name + * @param The annotation type + * @return The builder + * @since 4.0.0 + */ + public static AnnotationValueBuilder builder(@NonNull AnnotationValue annotation) { + ArgumentUtils.requireNonNull("annotation", annotation); + return new AnnotationValueBuilder<>(annotation, annotation.getRetentionPolicy()); + } + /** * Start building a new annotation existing value and retention policy. * diff --git a/core/src/main/java/io/micronaut/core/annotation/AnnotationValueBuilder.java b/core/src/main/java/io/micronaut/core/annotation/AnnotationValueBuilder.java index 9be79029c99..2f34139521a 100644 --- a/core/src/main/java/io/micronaut/core/annotation/AnnotationValueBuilder.java +++ b/core/src/main/java/io/micronaut/core/annotation/AnnotationValueBuilder.java @@ -36,8 +36,10 @@ public class AnnotationValueBuilder { private final String annotationName; private final Map values = new LinkedHashMap<>(5); private final RetentionPolicy retentionPolicy; - private final List> stereotypes = new ArrayList<>(); - private final Map defaultValues = new LinkedHashMap<>(); + @Nullable + private List> stereotypes; + @Nullable + private Map defaultValues; /** * Default constructor. @@ -81,6 +83,8 @@ public class AnnotationValueBuilder { AnnotationValueBuilder(AnnotationValue value, RetentionPolicy retentionPolicy) { this.annotationName = value.getAnnotationName(); this.values.putAll(value.getValues()); + this.defaultValues = value.getDefaultValues(); + this.stereotypes = value.getStereotypes(); this.retentionPolicy = retentionPolicy != null ? retentionPolicy : RetentionPolicy.RUNTIME; } @@ -103,6 +107,9 @@ public AnnotationValue build() { @NonNull public AnnotationValueBuilder stereotype(AnnotationValue annotation) { if (annotation != null) { + if (stereotypes == null) { + stereotypes = new ArrayList<>(10); + } stereotypes.add(annotation); } return this; @@ -115,9 +122,13 @@ public AnnotationValueBuilder stereotype(AnnotationValue annotation) { * @return This builder */ @NonNull - public AnnotationValueBuilder defaultValues(Map defaultValues) { + public AnnotationValueBuilder defaultValues(Map defaultValues) { if (defaultValues != null) { - this.defaultValues.putAll(defaultValues); + if (this.defaultValues == null) { + this.defaultValues = new LinkedHashMap<>(defaultValues); + } else { + this.defaultValues.putAll(defaultValues); + } } return this; } diff --git a/core/src/main/java/io/micronaut/core/annotation/EmptyAnnotationMetadata.java b/core/src/main/java/io/micronaut/core/annotation/EmptyAnnotationMetadata.java index 14dadf99f33..55cfc676e0d 100644 --- a/core/src/main/java/io/micronaut/core/annotation/EmptyAnnotationMetadata.java +++ b/core/src/main/java/io/micronaut/core/annotation/EmptyAnnotationMetadata.java @@ -137,7 +137,7 @@ public boolean hasDeclaredStereotype(@Nullable String annotation) { @NonNull @Override - public Map getDefaultValues(@NonNull String annotation) { + public Map getDefaultValues(@NonNull String annotation) { return Collections.emptyMap(); } diff --git a/core/src/main/java/io/micronaut/core/convert/DefaultMutableConversionService.java b/core/src/main/java/io/micronaut/core/convert/DefaultMutableConversionService.java index 88f16146b87..61a3bb23e95 100644 --- a/core/src/main/java/io/micronaut/core/convert/DefaultMutableConversionService.java +++ b/core/src/main/java/io/micronaut/core/convert/DefaultMutableConversionService.java @@ -199,23 +199,27 @@ public void addConverter(Class sourceType, Class targetType, Functi */ @SuppressWarnings({"OptionalIsPresent", "unchecked"}) private void registerDefaultConverters() { + LinkedHashMap, Class> primitiveArrays = new LinkedHashMap<>(); + primitiveArrays.put(Boolean[].class, boolean[].class); + primitiveArrays.put(Byte[].class, byte[].class); + primitiveArrays.put(Character[].class, char[].class); + primitiveArrays.put(Double[].class, double[].class); + primitiveArrays.put(Float[].class, float[].class); + primitiveArrays.put(Integer[].class, int[].class); + primitiveArrays.put(Long[].class, long[].class); + primitiveArrays.put(Short[].class, short[].class); + // primitive array to wrapper array @SuppressWarnings("rawtypes") Function primitiveArrayToWrapperArray = ArrayUtils::toWrapperArray; - addConverter(double[].class, Double[].class, primitiveArrayToWrapperArray); - addConverter(byte[].class, Byte[].class, primitiveArrayToWrapperArray); - addConverter(short[].class, Short[].class, primitiveArrayToWrapperArray); - addConverter(boolean[].class, Boolean[].class, primitiveArrayToWrapperArray); - addConverter(int[].class, Integer[].class, primitiveArrayToWrapperArray); - addConverter(float[].class, Float[].class, primitiveArrayToWrapperArray); - addConverter(double[].class, Double[].class, primitiveArrayToWrapperArray); - addConverter(char[].class, Character[].class, primitiveArrayToWrapperArray); // wrapper to primitive array converters Function wrapperArrayToPrimitiveArray = ArrayUtils::toPrimitiveArray; - //noinspection rawtypes - addConverter(Double[].class, double[].class, (Function) wrapperArrayToPrimitiveArray); - //noinspection rawtypes - addConverter(Integer[].class, int[].class, (Function) wrapperArrayToPrimitiveArray); + for (Map.Entry, Class> e : primitiveArrays.entrySet()) { + Class wrapperArray = e.getKey(); + Class primitiveArray = e.getValue(); + addConverter(primitiveArray, wrapperArray, primitiveArrayToWrapperArray); + addConverter(wrapperArray, primitiveArray, (Function) wrapperArrayToPrimitiveArray); + } // Object -> List addConverter(Object.class, List.class, (object, targetType, context) -> { diff --git a/core/src/main/java/io/micronaut/core/reflect/ReflectionUtils.java b/core/src/main/java/io/micronaut/core/reflect/ReflectionUtils.java index 84b80e16122..2642e9523fd 100644 --- a/core/src/main/java/io/micronaut/core/reflect/ReflectionUtils.java +++ b/core/src/main/java/io/micronaut/core/reflect/ReflectionUtils.java @@ -295,6 +295,26 @@ public static Field getRequiredField(Class type, String name) { } } + /** + * Finds field's value or return an empty if exception occurs or if the value is null. + * + * @param fieldOwnerClass The field owner class + * @param fieldName The field name + * @param instance The instance + * @return An {@link Optional} contains the value or empty of the value is null or an error occurred + * @since 4.0.0 + */ + @Internal + public static Optional getFieldValue(@NonNull Class fieldOwnerClass, @NonNull String fieldName, @NonNull Object instance) { + try { + final Field f = getRequiredField(fieldOwnerClass, fieldName); + f.setAccessible(true); + return Optional.ofNullable(f.get(instance)); + } catch (Throwable t) { + return Optional.empty(); + } + } + /** * Finds a field in the type or super type. * diff --git a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/annotation/GroovyAnnotationMetadataBuilder.java b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/annotation/GroovyAnnotationMetadataBuilder.java index da522eda39e..c634258ccaa 100644 --- a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/annotation/GroovyAnnotationMetadataBuilder.java +++ b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/annotation/GroovyAnnotationMetadataBuilder.java @@ -31,7 +31,6 @@ import io.micronaut.core.reflect.ReflectionUtils; import io.micronaut.core.util.CollectionUtils; import io.micronaut.core.util.StringUtils; -import io.micronaut.core.value.OptionalValues; import io.micronaut.inject.annotation.AbstractAnnotationMetadataBuilder; import io.micronaut.inject.annotation.AnnotatedElementValidator; import io.micronaut.inject.visitor.VisitorContext; @@ -68,6 +67,7 @@ import java.lang.reflect.Field; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; import java.util.Collections; import java.util.Iterator; import java.util.LinkedHashMap; @@ -96,8 +96,8 @@ public GroovyAnnotationMetadataBuilder(SourceUnit sourceUnit, CompilationUnit co final ModuleNode ast = sourceUnit.getAST(); if (ast != null) { Object validator = ast.getNodeMetaData(VALIDATOR_KEY); - if (validator instanceof AnnotatedElementValidator) { - elementValidator = (AnnotatedElementValidator) validator; + if (validator instanceof AnnotatedElementValidator annotatedElementValidator) { + elementValidator = annotatedElementValidator; } else { this.elementValidator = SoftServiceLoader.load(AnnotatedElementValidator.class).firstAvailable().orElse(null); ast.putNodeMetaData(VALIDATOR_KEY, this.elementValidator); @@ -128,7 +128,7 @@ protected boolean isValidationRequired(AnnotatedNode member) { @Override protected boolean isExcludedAnnotation(@NonNull AnnotatedNode element, @NonNull String annotationName) { - if (element instanceof ClassNode && ((ClassNode) element).isAnnotationDefinition() && annotationName.startsWith("java.lang.annotation")) { + if (element instanceof ClassNode classNode && classNode.isAnnotationDefinition() && annotationName.startsWith("java.lang.annotation")) { return false; } else { return super.isExcludedAnnotation(element, annotationName); @@ -136,9 +136,9 @@ protected boolean isExcludedAnnotation(@NonNull AnnotatedNode element, @NonNull } @Override - protected AnnotatedNode getAnnotationMember(AnnotatedNode originatingElement, CharSequence member) { - if (originatingElement instanceof ClassNode) { - final List methods = ((ClassNode) originatingElement).getMethods(member.toString()); + protected AnnotatedNode getAnnotationMember(AnnotatedNode annotationElement, CharSequence member) { + if (annotationElement instanceof ClassNode classNode) { + final List methods = classNode.getMethods(member.toString()); if (CollectionUtils.isNotEmpty(methods)) { return methods.iterator().next(); } @@ -154,10 +154,9 @@ protected RetentionPolicy getRetentionPolicy(@NonNull AnnotatedNode annotation) final Iterator i = ann.getMembers().values().iterator(); if (i.hasNext()) { final Expression expr = i.next(); - if (expr instanceof PropertyExpression) { - PropertyExpression pe = (PropertyExpression) expr; + if (expr instanceof PropertyExpression propertyExpression) { try { - return RetentionPolicy.valueOf(pe.getPropertyAsString()); + return RetentionPolicy.valueOf(propertyExpression.getPropertyAsString()); } catch (Throwable e) { // should never happen return RetentionPolicy.RUNTIME; @@ -238,8 +237,8 @@ protected Optional getAnnotationMirror(String annotationName) { return classNode; } ClassNode cn = ClassUtils.forName(annotationName, GroovyAnnotationMetadataBuilder.class.getClassLoader()) - .map(ClassHelper::make) - .orElseGet(() -> ClassHelper.make(annotationName)); + .map(ClassHelper::make) + .orElseGet(() -> ClassHelper.make(annotationName)); if (!cn.getName().equals(ClassHelper.OBJECT)) { return Optional.of(cn); } else { @@ -275,13 +274,13 @@ protected List getAnnotationsForType(AnnotatedNode ele for (AnnotationNode node : annotations) { Expression value = node.getMember("value"); boolean repeatable = false; - if (value instanceof ListExpression) { - for (Expression expression : ((ListExpression) value).getExpressions()) { - if (expression instanceof AnnotationConstantExpression) { + if (value instanceof ListExpression listExpression) { + for (Expression expression : listExpression.getExpressions()) { + if (expression instanceof AnnotationConstantExpression annotationConstantExpression) { String name = getRepeatableNameForType(expression.getType()); if (name != null && name.equals(node.getClassNode().getName())) { repeatable = true; - expanded.add((AnnotationNode) ((AnnotationConstantExpression) expression).getValue()); + expanded.add((AnnotationNode) annotationConstantExpression.getValue()); } } } @@ -297,40 +296,37 @@ protected List getAnnotationsForType(AnnotatedNode ele protected List buildHierarchy(AnnotatedNode element, boolean inheritTypeAnnotations, boolean declaredOnly) { if (declaredOnly) { return new ArrayList<>(Collections.singletonList(element)); - } else if (element instanceof ClassNode) { + } else if (element instanceof ClassNode classNode) { List hierarchy = new ArrayList<>(); - ClassNode cn = (ClassNode) element; - hierarchy.add(cn); - if (cn.isAnnotationDefinition()) { + hierarchy.add(classNode); + if (classNode.isAnnotationDefinition()) { return hierarchy; } - populateTypeHierarchy(cn, hierarchy); + populateTypeHierarchy(classNode, hierarchy); Collections.reverse(hierarchy); return hierarchy; - } else if (element instanceof MethodNode) { - MethodNode mn = (MethodNode) element; + } else if (element instanceof MethodNode methodNode) { List hierarchy; if (inheritTypeAnnotations) { - hierarchy = buildHierarchy(mn.getDeclaringClass(), false, declaredOnly); + hierarchy = buildHierarchy(methodNode.getDeclaringClass(), false, declaredOnly); } else { hierarchy = new ArrayList<>(); } - if (!mn.getAnnotations(ANN_OVERRIDE).isEmpty()) { - hierarchy.addAll(findOverriddenMethods(mn)); + if (!methodNode.getAnnotations(ANN_OVERRIDE).isEmpty()) { + hierarchy.addAll(findOverriddenMethods(methodNode)); } - hierarchy.add(mn); + hierarchy.add(methodNode); return hierarchy; - } else if (element instanceof ExtendedParameter) { - ExtendedParameter p = (ExtendedParameter) element; + } else if (element instanceof ExtendedParameter extendedParameter) { List hierarchy = new ArrayList<>(); - MethodNode methodNode = p.getMethodNode(); + MethodNode methodNode = extendedParameter.getMethodNode(); if (!methodNode.getAnnotations(ANN_OVERRIDE).isEmpty()) { - int variableIdx = Arrays.asList(methodNode.getParameters()).indexOf(p.getParameter()); + int variableIdx = Arrays.asList(methodNode.getParameters()).indexOf(extendedParameter.getParameter()); for (MethodNode overridden : findOverriddenMethods(methodNode)) { hierarchy.add(new ExtendedParameter(overridden, overridden.getParameters()[variableIdx])); } } - hierarchy.add(p); + hierarchy.add(extendedParameter); return hierarchy; } else { if (element == null) { @@ -343,12 +339,12 @@ protected List buildHierarchy(AnnotatedNode element, boolean inhe @Override protected void readAnnotationRawValues( - AnnotatedNode originatingElement, - String annotationName, - AnnotatedNode member, - String memberName, - Object annotationValue, - Map annotationValues) { + AnnotatedNode originatingElement, + String annotationName, + AnnotatedNode member, + String memberName, + Object annotationValue, + Map annotationValues) { if (!annotationValues.containsKey(memberName)) { final Object v = readAnnotationValue(originatingElement, member, memberName, annotationValue); if (v != null) { @@ -361,56 +357,24 @@ protected void readAnnotationRawValues( @Override protected Map readAnnotationDefaultValues(String annotationName, AnnotatedNode annotationType) { Map defaultValues = new LinkedHashMap<>(); - if (annotationType instanceof ClassNode) { - ClassNode classNode = (ClassNode) annotationType; + if (annotationType instanceof ClassNode classNode) { List methods = new ArrayList<>(classNode.getMethods()); - - // TODO: Remove this branch of the code after upgrading to Groovy 3.0 - // https://issues.apache.org/jira/browse/GROOVY-8696 - if (classNode.isResolved()) { - Class resolved = classNode.getTypeClass(); - for (MethodNode method : methods) { - try { - final Object defaultValue = resolved.getDeclaredMethod(method.getName()).getDefaultValue(); - if (defaultValue != null) { - if (defaultValue instanceof Class) { - defaultValues.put(method, new ClassExpression(ClassHelper.makeCached((Class) defaultValue))); - } else { - if (defaultValue instanceof String) { - if (StringUtils.isNotEmpty((String) defaultValue)) { - defaultValues.put(method, new ConstantExpression(defaultValue)); - } - } else { - defaultValues.put(method, new ConstantExpression(defaultValue)); - } - } - } - } catch (NoSuchMethodError | NoSuchMethodException e) { - // method no longer exists alias annotation - // ignore and continue - } + for (MethodNode method : methods) { + Statement stmt = method.getCode(); + Expression expression = null; + if (stmt instanceof ReturnStatement returnStatement) { + expression = returnStatement.getExpression(); + } else if (stmt instanceof ExpressionStatement expressionStatement) { + expression = expressionStatement.getExpression(); } - } else { - for (MethodNode method : methods) { - Statement stmt = method.getCode(); - Expression expression = null; - if (stmt instanceof ReturnStatement) { - expression = ((ReturnStatement) stmt).getExpression(); - } else if (stmt instanceof ExpressionStatement) { - expression = ((ExpressionStatement) stmt).getExpression(); - } - if (expression instanceof ConstantExpression) { - ConstantExpression ce = (ConstantExpression) expression; - final Object v = ce.getValue(); - if (v != null) { - if (v instanceof String) { - if (StringUtils.isNotEmpty((String) v)) { - defaultValues.put(method, new ConstantExpression(v)); - } - } else { - defaultValues.put(method, expression); - } + if (expression instanceof ConstantExpression constantExpression) { + final Object v = constantExpression.getValue(); + if (v instanceof String s) { + if (StringUtils.isNotEmpty(s)) { + defaultValues.put(method, new ConstantExpression(v)); } + } else if (v != null) { + defaultValues.put(method, expression); } } } @@ -423,18 +387,7 @@ protected boolean isInheritedAnnotation(@NonNull AnnotationNode annotationMirror final List annotations = annotationMirror.getClassNode().getAnnotations(); if (CollectionUtils.isNotEmpty(annotations)) { return annotations.stream().anyMatch((ann) -> - ann.getClassNode().getName().equals(Inherited.class.getName()) - ); - } - return false; - } - - @Override - protected boolean isInheritedAnnotationType(@NonNull AnnotatedNode annotationType) { - final List annotations = annotationType.getAnnotations(); - if (CollectionUtils.isNotEmpty(annotations)) { - return annotations.stream().anyMatch((ann) -> - ann.getClassNode().getName().equals(Inherited.class.getName()) + ann.getClassNode().getName().equals(Inherited.class.getName()) ); } return false; @@ -443,8 +396,7 @@ protected boolean isInheritedAnnotationType(@NonNull AnnotatedNode annotationTyp @Override protected Map getAnnotationMembers(String annotationType) { final AnnotatedNode node = getAnnotationMirror(annotationType).orElse(null); - if (node instanceof ClassNode) { - final ClassNode cn = (ClassNode) node; + if (node instanceof final ClassNode cn) { if (cn.isAnnotationDefinition()) { return cn.getDeclaredMethodsMap(); } @@ -465,34 +417,13 @@ protected boolean hasSimpleAnnotation(AnnotatedNode element, String simpleName) return false; } - @Override - protected Map readAnnotationDefaultValues(AnnotationNode annotationMirror) { - ClassNode classNode = annotationMirror.getClassNode(); - String annotationName = classNode.getName(); - return readAnnotationDefaultValues(annotationName, classNode); - } - @Override protected Object readAnnotationValue(AnnotatedNode originatingElement, AnnotatedNode member, String memberName, Object annotationValue) { - if (annotationValue instanceof ConstantExpression) { - if (annotationValue instanceof AnnotationConstantExpression) { - AnnotationConstantExpression ann = (AnnotationConstantExpression) annotationValue; - AnnotationNode value = (AnnotationNode) ann.getValue(); - final AnnotationValue av = readNestedAnnotationValue(originatingElement, value); - if (member instanceof MethodNode && ((MethodNode) member).getReturnType().isArray()) { - return new AnnotationValue[]{av}; - } else { - return av; - } - } else { - return ((ConstantExpression) annotationValue).getValue(); - } - - } else if (annotationValue instanceof PropertyExpression) { - PropertyExpression pe = (PropertyExpression) annotationValue; - if (pe.getObjectExpression() instanceof ClassExpression) { - ClassExpression ce = (ClassExpression) pe.getObjectExpression(); - ClassNode propertyType = ce.getType(); + if (annotationValue instanceof ConstantExpression constantExpression) { + return readConstantExpression(originatingElement, member, constantExpression); + } else if (annotationValue instanceof PropertyExpression pe) { + if (pe.getObjectExpression() instanceof ClassExpression classExpression) { + ClassNode propertyType = classExpression.getType(); if (propertyType.isEnum()) { return pe.getPropertyAsString(); } else { @@ -508,65 +439,40 @@ protected Object readAnnotationValue(AnnotatedNode originatingElement, Annotated } } } - } else if (annotationValue instanceof ClassExpression) { - return new AnnotationClassValue<>(((ClassExpression) annotationValue).getType().getName()); - } else if (annotationValue instanceof ListExpression) { - ListExpression le = (ListExpression) annotationValue; - List converted = new ArrayList<>(); - Class arrayType = Object.class; - for (Expression exp : le.getExpressions()) { - if (exp instanceof PropertyExpression) { - PropertyExpression propertyExpression = (PropertyExpression) exp; + } else if (annotationValue instanceof ClassExpression classExpression) { + return new AnnotationClassValue<>(classExpression.getType().getName()); + } else if (annotationValue instanceof ListExpression listExpression) { + List expressions = listExpression.getExpressions(); + List converted = new ArrayList<>(expressions.size()); + for (Expression exp : expressions) { + if (exp instanceof PropertyExpression propertyExpression) { Expression valueExpression = propertyExpression.getProperty(); Expression objectExpression = propertyExpression.getObjectExpression(); - if (valueExpression instanceof ConstantExpression && objectExpression instanceof ClassExpression) { - Object value = ((ConstantExpression) valueExpression).getValue(); + if (valueExpression instanceof ConstantExpression constantExpression && objectExpression instanceof ClassExpression) { + Object value = readConstantExpression(originatingElement, member, constantExpression); if (value != null) { - if (value instanceof CharSequence) { - value = value.toString(); - } - ClassNode enumType = objectExpression.getType(); - if (enumType.isResolved()) { - arrayType = enumType.getTypeClass(); - } else { - arrayType = String.class; - } converted.add(value); } } } - if (exp instanceof AnnotationConstantExpression) { - arrayType = AnnotationValue.class; - AnnotationConstantExpression ann = (AnnotationConstantExpression) exp; - AnnotationNode value = (AnnotationNode) ann.getValue(); - converted.add(readNestedAnnotationValue(originatingElement, value)); - } else if (exp instanceof ConstantExpression) { - Object value = ((ConstantExpression) exp).getValue(); + if (exp instanceof ConstantExpression constantExpression) { + Object value = readConstantExpression(originatingElement, member, constantExpression); if (value != null) { - if (value instanceof CharSequence) { - value = value.toString(); - } - arrayType = value.getClass(); converted.add(value); } - } else if (exp instanceof ClassExpression) { - arrayType = AnnotationClassValue.class; - ClassExpression classExp = ((ClassExpression) exp); + } else if (exp instanceof ClassExpression classExpression) { String typeName; - if (classExp.getType().isArray()) { - typeName = "[L" + classExp.getType().getComponentType().getName() + ";"; + if (classExpression.getType().isArray()) { + typeName = "[L" + classExpression.getType().getComponentType().getName() + ";"; } else { - typeName = classExp.getType().getName(); + typeName = classExpression.getType().getName(); } converted.add(new AnnotationClassValue<>(typeName)); } } - // for some reason this is necessary to produce correct array type in Groovy - return ConversionService.SHARED.convert(converted, Array.newInstance(arrayType, 0).getClass()) - .orElse(null); - } else if (annotationValue instanceof VariableExpression) { - VariableExpression ve = (VariableExpression) annotationValue; - Variable variable = ve.getAccessedVariable(); + return toArray(member, converted); + } else if (annotationValue instanceof VariableExpression variableExpression) { + Variable variable = variableExpression.getAccessedVariable(); if (variable != null && variable.hasInitialExpression()) { return readAnnotationValue(originatingElement, member, memberName, variable.getInitialExpression()); } @@ -578,30 +484,128 @@ protected Object readAnnotationValue(AnnotatedNode originatingElement, Annotated return null; } + private static Object toArray(AnnotatedNode member, Collection collection) { + if (!(member instanceof MethodNode methodNode)) { + throw new IllegalStateException("Expected instance of MethodNode got: " + member); + } + ClassNode returnType = methodNode.getReturnType(); + if (!returnType.isArray()) { + throw new IllegalStateException("Expected a method returning an array got: " + member); + } + Class arrayType = Object.class; + ClassNode component = returnType.getComponentType(); + if (component != null) { + if (component.isEnum()) { + arrayType = String.class; + collection = collection.stream().map(val -> { + if (val instanceof Enum anEnum) { + return anEnum.name(); + } + return val; + }).toList(); + } else if (component.isResolved()) { + arrayType = component.getTypeClass(); + if (Annotation.class.isAssignableFrom(arrayType)) { + arrayType = AnnotationValue.class; + } else if (Class.class.isAssignableFrom(arrayType)) { + arrayType = AnnotationClassValue.class; + } + } + } + if (collection.isEmpty()) { + return Array.newInstance(arrayType, 0); + } + if (collection.stream().allMatch(val -> val instanceof AnnotationClassValue)) { + arrayType = AnnotationClassValue.class; + } else if (collection.stream().allMatch(val -> val instanceof AnnotationValue)) { + arrayType = AnnotationValue.class; + } + if (arrayType.isPrimitive()) { + Class wrapperType = ReflectionUtils.getWrapperType(arrayType); + Class primitiveArrayType = Array.newInstance(arrayType, 0).getClass(); + Object[] emptyWrapperArray = (Object[]) Array.newInstance(wrapperType, 0); + Object[] wrapperArray = collection.toArray(emptyWrapperArray); + // Convert to a proper primitive type array + return ConversionService.SHARED.convertRequired(wrapperArray, primitiveArrayType); + } + return ConversionService.SHARED.convert(collection, Array.newInstance(arrayType, 0).getClass()) + .orElse(null); + } + + private Object readConstantExpression(AnnotatedNode originatingElement, AnnotatedNode member, ConstantExpression constantExpression) { + if (constantExpression instanceof AnnotationConstantExpression ann) { + AnnotationNode value = (AnnotationNode) ann.getValue(); + return readNestedAnnotationValue(originatingElement, value); + } else { + Object value = constantExpression.getValue(); + if (value == null) { + return null; + } + if (value instanceof Collection collection) { + collection = collection.stream().map(this::convertConstantValue).toList(); + return toArray(member, collection); + } + return convertConstantValue(value); + } + } + + private Object convertConstantValue(Object value) { + if (value instanceof ClassNode classNode) { + return new AnnotationClassValue<>(classNode.getName()); + } + Class valueClass = value.getClass(); + // Groovy 4.0.6 will return EnumConstantWrapper as a default value + if (valueClass.getName().equals("org.codehaus.groovy.ast.decompiled.EnumConstantWrapper")) { + return ReflectionUtils.getFieldValue(valueClass, "constant", value).orElse(null); + } + if (valueClass.getName().equals("org.codehaus.groovy.ast.decompiled.TypeWrapper")) { + String desc = (String) ReflectionUtils.getFieldValue(valueClass, "desc", value).orElse(null); + if (desc == null) { + return null; + } + // Desc will return "Ljava/lang/String;" + StringBuilder arraySuffix = new StringBuilder(); + while (desc.startsWith("[")) { + desc = desc.substring(1); + arraySuffix.append("[]"); + } + String className = desc.substring(1, desc.length() - 1).replace("/", ".") + arraySuffix; + return new AnnotationClassValue<>(className); + } + if (value instanceof CharSequence) { + value = value.toString(); + } + return value; + } + @Override protected Map readAnnotationRawValues(AnnotationNode annotationMirror) { Map members = annotationMirror.getMembers(); Map values = new LinkedHashMap<>(); ClassNode annotationClassNode = annotationMirror.getClassNode(); members.forEach((key, value) -> - values.put(annotationClassNode.getMethods(key).get(0), value)); + values.put(annotationClassNode.getMethods(key).get(0), value)); return values; } @Override - protected OptionalValues getAnnotationValues(AnnotatedNode originatingElement, AnnotatedNode member, Class annotationType) { + protected Optional> getAnnotationValues(AnnotatedNode originatingElement, AnnotatedNode member, Class annotationType) { if (member != null) { final List anns = member.getAnnotations(ClassHelper.make(annotationType)); if (CollectionUtils.isNotEmpty(anns)) { AnnotationNode ann = anns.get(0); Map converted = new LinkedHashMap<>(); - ann.getMembers().forEach((key, value) -> - readAnnotationRawValues(originatingElement, annotationType.getName(), member, key, value, converted) - ); - return OptionalValues.of(Object.class, converted); + ClassNode annotationNode = ann.getClassNode(); + for (Map.Entry entry : ann.getMembers().entrySet()) { + String key = entry.getKey(); + Expression value = entry.getValue(); + AnnotatedNode annotationMember = annotationNode.getMethod(key, new Parameter[0]); + readAnnotationRawValues(originatingElement, annotationType.getName(), annotationMember, key, value, converted); + } + return Optional.of(AnnotationValue.builder(annotationType).members(converted).build()); } } - return OptionalValues.empty(); + return Optional.empty(); } @Override diff --git a/inject-groovy/src/test/groovy/io/micronaut/inject/annotation/AnnotationMetadataWriterSpec.groovy b/inject-groovy/src/test/groovy/io/micronaut/inject/annotation/AnnotationMetadataWriterSpec.groovy index 2831515afe3..a79115db4f7 100644 --- a/inject-groovy/src/test/groovy/io/micronaut/inject/annotation/AnnotationMetadataWriterSpec.groovy +++ b/inject-groovy/src/test/groovy/io/micronaut/inject/annotation/AnnotationMetadataWriterSpec.groovy @@ -21,6 +21,7 @@ import io.micronaut.context.annotation.Requirements import io.micronaut.context.annotation.Requires import io.micronaut.core.annotation.AnnotationClassValue import io.micronaut.core.annotation.AnnotationMetadata +import io.micronaut.core.annotation.AnnotationValue import io.micronaut.core.annotation.TypeHint import javax.inject.Named @@ -475,4 +476,108 @@ class Test { metadata.stringValues(TypeHint)[0] == '[Ljava.util.UUID;' metadata.stringValues(TypeHint)[1] == 'java.util.UUID' } + + void "test defaults"() { + given: + AnnotationMetadata toWrite = buildTypeAnnotationMetadata("test.Test",'''\ +package test; + +@io.micronaut.inject.annotation.MyAnnotation +class Test { +} + +''') + when: + AnnotationMetadata metadata = writeAndLoadMetadata("test", toWrite) + def defaults = metadata.getDefaultValues("io.micronaut.inject.annotation.MyAnnotation") + def av = metadata.getAnnotation("io.micronaut.inject.annotation.MyAnnotation") + + then: + defaults["num"] == 10 + defaults["bool"] == false + defaults["intArray1"] == new int[] {} + defaults["intArray2"] == new int[] {1, 2, 3} + defaults["intArray3"] == null + defaults["stringArray1"] == new String[] {} + defaults["stringArray2"] == new String[] {""} + defaults["stringArray3"] == new String[] {"A"} + defaults["stringArray4"] == null + defaults["boolArray1"] == new boolean[] {} + defaults["boolArray2"] == new boolean[] {true} + defaults["boolArray3"] == new boolean[] {false} + defaults["boolArray4"] == null + defaults["myEnumArray1"] == new String[] {} + defaults["myEnumArray2"] == new String[] {"ABC"} + defaults["myEnumArray3"] == new String[] {"FOO", "BAR"} + defaults["myEnumArray4"] == null + defaults["classesArray1"] == new AnnotationClassValue[0] + defaults["classesArray2"] == new AnnotationClassValue[] {new AnnotationClassValue(String.class)} + defaults["classesArray3"] == new AnnotationClassValue[] {new AnnotationClassValue(String[].class)} + defaults["classesArray4"] == new AnnotationClassValue[] {new AnnotationClassValue(String[][].class)} + + defaults["annotationsArray1"] == new AnnotationValue[0] +// Default annotation values are crashing Groovy compiler +// defaults["annotationsArray2"] == new AnnotationValue[] { AnnotationValue.builder(MyAnnotation3).value("foo").build(), AnnotationValue.builder(MyAnnotation3).value("bar").build() } +// defaults["ann"] == AnnotationValue.builder(MyAnnotation3).value("foo").build() + + av.getRequiredValue("num", Integer.class) == 10 + av.getRequiredValue("bool", Boolean.class) == false + av.getRequiredValue("intArray1", int[].class) == new int[] {} + av.getRequiredValue("intArray2", int[].class) == new int[] {1, 2, 3} + av.getRequiredValue("stringArray1", String[].class) == new String[] {} + av.getRequiredValue("stringArray2", String[].class) == new String[] {""} + av.getRequiredValue("stringArray3", String[].class) == new String[] {"A"} + av.getRequiredValue("myEnumArray1", String[].class) == new String[] {} + av.getRequiredValue("myEnumArray2", String[].class) == new String[] {"ABC"} + av.getRequiredValue("myEnumArray3", String[].class) == new String[] {"FOO", "BAR"} + } + + void "test aliases"() { + given: + AnnotationMetadata toWrite = buildTypeAnnotationMetadata("test.Test", '''\ +package test; + +@io.micronaut.inject.annotation.MyAnnotation2Aliases( + intArray1Alias = [], + intArray2Alias = [1, 2, 3], + stringArray1Alias = [], + stringArray2Alias = "", + stringArray3Alias = "A", + myEnumArray1Alias = [], + myEnumArray2Alias = io.micronaut.inject.annotation.MyEnum2.ABC, + myEnumArray3Alias = [io.micronaut.inject.annotation.MyEnum2.FOO, io.micronaut.inject.annotation.MyEnum2.BAR], + classesArray1Alias = [], + classesArray2Alias = [String.class], +// ann = @io.micronaut.inject.annotation.MyAnnotation3("foo"), + annotationsArray1Alias = [], + annotationsArray2Alias = [ + @io.micronaut.inject.annotation.MyAnnotation3("foo"), + @io.micronaut.inject.annotation.MyAnnotation3("bar") + ] + ) +class Test { +} + +''') + when: + AnnotationMetadata metadata = writeAndLoadMetadata("test", toWrite) + def values = metadata.getValues("io.micronaut.inject.annotation.MyAnnotation2Aliases") + def av = metadata.getAnnotation("io.micronaut.inject.annotation.MyAnnotation2Aliases") + + then: + values["intArray1"] == new int[] {} + values["intArray2"] == new int[] {1, 2, 3} + values["stringArray1"] == new String[] {} + values["stringArray2"] == new String[] {""} + values["stringArray3"] == new String[] {"A"} + values["myEnumArray1"] == new String[] {} + values["myEnumArray2"] == new String[] {"ABC"} + values["myEnumArray3"] == new String[] {"FOO", "BAR"} + values["classesArray1"] == new AnnotationClassValue[0] + values["classesArray2"] == new AnnotationClassValue[] {new AnnotationClassValue(String)} +// Default annotation values are crashing Groovy compiler +// values["ann"] == AnnotationValue.builder(MyAnnotation3).value("foo").build() + values["annotationsArray1"] == new AnnotationValue[0] + values["annotationsArray2"] == new AnnotationValue[] { AnnotationValue.builder(MyAnnotation3).value("foo").build(), AnnotationValue.builder(MyAnnotation3).value("bar").build() } + } } diff --git a/inject-groovy/src/test/groovy/io/micronaut/inject/annotation/MyAnnotation.groovy b/inject-groovy/src/test/groovy/io/micronaut/inject/annotation/MyAnnotation.groovy new file mode 100644 index 00000000000..cf6974bec19 --- /dev/null +++ b/inject-groovy/src/test/groovy/io/micronaut/inject/annotation/MyAnnotation.groovy @@ -0,0 +1,56 @@ +package io.micronaut.inject.annotation + +@interface MyAnnotation { + + int num() default 10; + + String value() default ""; + + boolean bool() default false; + + MyEnum2 myEnum() default MyEnum2.ABC; + + int[] intArray1() default []; + + int[] intArray2() default [1, 2, 3]; + + int[] intArray3(); + + String[] stringArray1() default []; + + String[] stringArray2() default [""]; + + String[] stringArray3() default ["A"]; + + String[] stringArray4(); + + boolean[] boolArray1() default []; + + boolean[] boolArray2() default [true]; + + boolean[] boolArray3() default [false]; + + boolean[] boolArray4(); + + MyEnum2[] myEnumArray1() default []; + + MyEnum2[] myEnumArray2() default [MyEnum2.ABC]; + + MyEnum2[] myEnumArray3() default [MyEnum2.FOO, MyEnum2.BAR]; + + MyEnum2[] myEnumArray4(); + + Class[] classesArray1() default []; + + Class[] classesArray2() default [String.class]; + + Class[] classesArray3() default [String[].class]; + + Class[] classesArray4() default [String[][].class]; + + MyAnnotation3[] annotationsArray1() default []; + +// Default annotation values are crashing Groovy compiler +// MyAnnotation3[] annotationsArray2() default [@MyAnnotation3("foo"), @MyAnnotation3("bar")]; + +} diff --git a/inject-groovy/src/test/groovy/io/micronaut/inject/annotation/MyAnnotation2Aliases.groovy b/inject-groovy/src/test/groovy/io/micronaut/inject/annotation/MyAnnotation2Aliases.groovy new file mode 100644 index 00000000000..4cd9ed0fcc7 --- /dev/null +++ b/inject-groovy/src/test/groovy/io/micronaut/inject/annotation/MyAnnotation2Aliases.groovy @@ -0,0 +1,81 @@ +package io.micronaut.inject.annotation; + +import io.micronaut.context.annotation.AliasFor; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +@Documented +@Retention(RUNTIME) +@Target([ElementType.TYPE]) +public @interface MyAnnotation2Aliases { + + int[] intArray1() default []; + + int[] intArray2() default []; + + @AliasFor(member = "intArray1") + int[] intArray1Alias(); + + @AliasFor(member = "intArray2") + int[] intArray2Alias(); + + String[] stringArray1() default []; + + String[] stringArray2() default []; + + String[] stringArray3() default []; + + @AliasFor(member = "stringArray1") + String[] stringArray1Alias(); + + @AliasFor(member = "stringArray2") + String[] stringArray2Alias(); + + @AliasFor(member = "stringArray3") + String[] stringArray3Alias(); + + MyEnum2[] myEnumArray1() default []; + + MyEnum2[] myEnumArray2() default []; + + MyEnum2[] myEnumArray3() default []; + + @AliasFor(member = "myEnumArray1") + MyEnum2[] myEnumArray1Alias(); + + @AliasFor(member = "myEnumArray2") + MyEnum2[] myEnumArray2Alias(); + + @AliasFor(member = "myEnumArray3") + MyEnum2[] myEnumArray3Alias(); + + Class[] classesArray1() default []; + + Class[] classesArray2() default []; + + @AliasFor(member = "classesArray1") + Class[] classesArray1Alias(); + + @AliasFor(member = "classesArray2") + Class[] classesArray2Alias(); + +// Default annotation values are crashing Groovy compiler +// MyAnnotation3 ann() default @MyAnnotation3("default"); +// @AliasFor(member = "ann") +// MyAnnotation3 annAlias(); + + MyAnnotation3[] annotationsArray1() default []; + + MyAnnotation3[] annotationsArray2() default []; + + @AliasFor(member = "annotationsArray1") + MyAnnotation3[] annotationsArray1Alias(); + + @AliasFor(member = "annotationsArray2") + MyAnnotation3[] annotationsArray2Alias(); +} diff --git a/inject-groovy/src/test/groovy/io/micronaut/inject/annotation/MyAnnotation3.groovy b/inject-groovy/src/test/groovy/io/micronaut/inject/annotation/MyAnnotation3.groovy new file mode 100644 index 00000000000..67a48b73ea6 --- /dev/null +++ b/inject-groovy/src/test/groovy/io/micronaut/inject/annotation/MyAnnotation3.groovy @@ -0,0 +1,17 @@ +package io.micronaut.inject.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +@Documented +@Retention(RUNTIME) +@Target(ElementType.TYPE) +public @interface MyAnnotation3 { + + String value() default ""; + +} diff --git a/inject-groovy/src/test/groovy/io/micronaut/inject/annotation/MyEnum2.java b/inject-groovy/src/test/groovy/io/micronaut/inject/annotation/MyEnum2.java new file mode 100644 index 00000000000..53df25351ee --- /dev/null +++ b/inject-groovy/src/test/groovy/io/micronaut/inject/annotation/MyEnum2.java @@ -0,0 +1,5 @@ +package io.micronaut.inject.annotation; + +public enum MyEnum2 { + FOO, BAR, ABC +} diff --git a/inject-java/src/main/java/io/micronaut/annotation/processing/JavaAnnotationMetadataBuilder.java b/inject-java/src/main/java/io/micronaut/annotation/processing/JavaAnnotationMetadataBuilder.java index 0b68e03ad81..3b3310051e6 100644 --- a/inject-java/src/main/java/io/micronaut/annotation/processing/JavaAnnotationMetadataBuilder.java +++ b/inject-java/src/main/java/io/micronaut/annotation/processing/JavaAnnotationMetadataBuilder.java @@ -19,12 +19,12 @@ import io.micronaut.core.annotation.AnnotationUtil; import io.micronaut.core.annotation.NonNull; import io.micronaut.core.annotation.Nullable; +import io.micronaut.core.convert.ConversionService; import io.micronaut.core.reflect.ClassUtils; -import io.micronaut.core.util.ArrayUtils; +import io.micronaut.core.reflect.ReflectionUtils; import io.micronaut.core.util.CollectionUtils; import io.micronaut.core.util.StringUtils; import io.micronaut.core.util.clhm.ConcurrentLinkedHashMap; -import io.micronaut.core.value.OptionalValues; import io.micronaut.inject.annotation.AbstractAnnotationMetadataBuilder; import io.micronaut.inject.annotation.AnnotatedElementValidator; import io.micronaut.inject.processing.JavaModelUtils; @@ -40,6 +40,7 @@ import javax.lang.model.element.VariableElement; import javax.lang.model.type.ArrayType; import javax.lang.model.type.DeclaredType; +import javax.lang.model.type.NullType; import javax.lang.model.type.TypeMirror; import javax.lang.model.util.AbstractAnnotationValueVisitor8; import javax.lang.model.util.ElementFilter; @@ -149,7 +150,12 @@ protected String getRepeatableNameForType(Element annotationType) { @Override protected Optional getAnnotationMirror(String annotationName) { - return Optional.ofNullable(elementUtils.getTypeElement(annotationName)); + TypeElement typeElement = elementUtils.getTypeElement(annotationName); + if (typeElement == null) { + // maybe inner class? + typeElement = elementUtils.getTypeElement(annotationName.replace('$', '.')); + } + return Optional.ofNullable(typeElement); } @Override @@ -190,16 +196,6 @@ protected boolean isInheritedAnnotation(@NonNull AnnotationMirror annotationMirr return false; } - @Override - protected boolean isInheritedAnnotationType(@NonNull Element annotationType) { - for (AnnotationMirror mirror : annotationType.getAnnotationMirrors()) { - if (getAnnotationTypeName(mirror).equals(Inherited.class.getName())) { - return true; - } - } - return false; - } - @Override protected Map getAnnotationMembers(String annotationType) { final Element element = getAnnotationMirror(annotationType).orElse(null); @@ -241,10 +237,8 @@ protected Element getTypeForAnnotation(AnnotationMirror annotationMirror) { @Override protected List getAnnotationsForType(Element element) { - List annotationMirrors = new ArrayList<>(element.getAnnotationMirrors()); - annotationMirrors.removeIf(mirror -> EXCLUDES.contains(getAnnotationTypeName(mirror))); - List expanded = new ArrayList<>(annotationMirrors.size()); - for (AnnotationMirror annotation : annotationMirrors) { + List expanded = new ArrayList<>(); + for (AnnotationMirror annotation : element.getAnnotationMirrors()) { boolean repeatable = false; boolean hasOtherMembers = false; for (Map.Entry entry : annotation.getElementValues().entrySet()) { @@ -299,11 +293,10 @@ protected List buildHierarchy(Element element, boolean inheritTypeAnnot populateTypeHierarchy(element, hierarchy); Collections.reverse(hierarchy); return hierarchy; - } else if (element instanceof ExecutableElement) { + } else if (element instanceof ExecutableElement executableElement) { // we have a method // for methods we merge the data from any overridden interface or abstract methods // with type level data - ExecutableElement executableElement = (ExecutableElement) element; // the starting hierarchy is the type and super types of this method List hierarchy; if (inheritTypeAnnotations) { @@ -314,12 +307,10 @@ protected List buildHierarchy(Element element, boolean inheritTypeAnnot hierarchy.addAll(findOverriddenMethods(executableElement)); hierarchy.add(element); return hierarchy; - } else if (element instanceof VariableElement) { + } else if (element instanceof VariableElement variable) { List hierarchy = new ArrayList<>(); - VariableElement variable = (VariableElement) element; Element enclosingElement = variable.getEnclosingElement(); - if (enclosingElement instanceof ExecutableElement) { - ExecutableElement executableElement = (ExecutableElement) enclosingElement; + if (enclosingElement instanceof ExecutableElement executableElement) { int variableIdx = executableElement.getParameters().indexOf(variable); for (ExecutableElement overridden : findOverriddenMethods(executableElement)) { hierarchy.add(overridden.getParameters().get(variableIdx)); @@ -341,9 +332,9 @@ protected List buildHierarchy(Element element, boolean inheritTypeAnnot @Nullable @Override - protected Element getAnnotationMember(Element originatingElement, CharSequence member) { - if (originatingElement instanceof TypeElement) { - List enclosedElements = originatingElement.getEnclosedElements(); + protected Element getAnnotationMember(Element annotationElement, CharSequence member) { + if (annotationElement instanceof TypeElement) { + List enclosedElements = annotationElement.getEnclosedElements(); for (Element enclosedElement : enclosedElements) { if (enclosedElement instanceof ExecutableElement && enclosedElement.getSimpleName().toString().equals(member.toString())) { return enclosedElement; @@ -354,7 +345,7 @@ protected Element getAnnotationMember(Element originatingElement, CharSequence m } @Override - protected OptionalValues getAnnotationValues(Element originatingElement, Element member, Class annotationType) { + protected Optional> getAnnotationValues(Element originatingElement, Element member, Class annotationType) { List annotationMirrors = member.getAnnotationMirrors(); String annotationName = annotationType.getName(); for (AnnotationMirror annotationMirror : annotationMirrors) { @@ -364,23 +355,24 @@ protected OptionalValues getAnnotationValues(Element originatingElement, Elem for (Map.Entry entry : values.entrySet()) { Element key = entry.getKey(); Object value = entry.getValue(); - readAnnotationRawValues(originatingElement, annotationName, member, key.getSimpleName().toString(), value, converted); + readAnnotationRawValues(originatingElement, annotationName, key, key.getSimpleName().toString(), value, converted); } - return OptionalValues.of(Object.class, converted); + return Optional.of(io.micronaut.core.annotation.AnnotationValue.builder(annotationType).members(converted).build()); } } - return OptionalValues.empty(); + return Optional.empty(); } @Override protected void readAnnotationRawValues( Element originatingElement, - String annotationName, Element member, + String annotationName, + Element member, String memberName, Object annotationValue, Map annotationValues) { if (memberName != null && annotationValue instanceof javax.lang.model.element.AnnotationValue && !annotationValues.containsKey(memberName)) { - final MetadataAnnotationValueVisitor resolver = new MetadataAnnotationValueVisitor(originatingElement); + final MetadataAnnotationValueVisitor resolver = new MetadataAnnotationValueVisitor(originatingElement, (ExecutableElement) member); ((javax.lang.model.element.AnnotationValue) annotationValue).accept(resolver, this); final Object resolvedValue = resolver.resolvedValue; if (resolvedValue != null) { @@ -420,7 +412,7 @@ private boolean isValidationRequired(List annotation @Override protected Object readAnnotationValue(Element originatingElement, Element member, String memberName, Object annotationValue) { if (memberName != null && annotationValue instanceof javax.lang.model.element.AnnotationValue) { - final MetadataAnnotationValueVisitor visitor = new MetadataAnnotationValueVisitor(originatingElement); + final MetadataAnnotationValueVisitor visitor = new MetadataAnnotationValueVisitor(originatingElement, (ExecutableElement) member); ((javax.lang.model.element.AnnotationValue) annotationValue).accept(visitor, this); return visitor.resolvedValue; } else if (memberName != null && annotationValue != null && ClassUtils.isJavaLangType(annotationValue.getClass())) { @@ -430,14 +422,6 @@ protected Object readAnnotationValue(Element originatingElement, Element member, return null; } - @Override - protected Map readAnnotationDefaultValues(AnnotationMirror annotationMirror) { - - final String annotationTypeName = getAnnotationTypeName(annotationMirror); - Element element = annotationMirror.getAnnotationType().asElement(); - return readAnnotationDefaultValues(annotationTypeName, element); - } - @Override protected Map readAnnotationDefaultValues(String annotationTypeName, Element element) { Map defaultValues = new LinkedHashMap<>(); @@ -642,13 +626,16 @@ public static boolean hasAnnotation(ExecutableElement method, Class { private final Element originatingElement; + private final ExecutableElement member; private Object resolvedValue; /** * @param originatingElement + * @param member */ - MetadataAnnotationValueVisitor(Element originatingElement) { + MetadataAnnotationValueVisitor(Element originatingElement, ExecutableElement member) { this.originatingElement = originatingElement; + this.member = member; } @Override @@ -733,7 +720,7 @@ public Object visitAnnotation(AnnotationMirror a, Object o) { @Override public Object visitArray(List vals, Object o) { - ArrayValueVisitor arrayValueVisitor = new ArrayValueVisitor(); + ArrayValueVisitor arrayValueVisitor = new ArrayValueVisitor(member); for (javax.lang.model.element.AnnotationValue val : vals) { val.accept(arrayValueVisitor, o); } @@ -745,90 +732,114 @@ public Object visitArray(List { + private final class ArrayValueVisitor extends AbstractAnnotationValueVisitor8 { private List values = new ArrayList(); - private Class arrayType; + private final ExecutableElement member; + + private ArrayValueVisitor(ExecutableElement member) { + this.member = member; + } - Object[] getValues() { - if (arrayType != null) { - for (Object value : values) { - if (value != null && !arrayType.isInstance(value)) { - return ArrayUtils.EMPTY_OBJECT_ARRAY; + Object getValues() { + final Types typeUtils = modelUtils.getTypeUtils(); + TypeMirror arrayType; + TypeMirror methodReturnType = member.getReturnType(); + if (methodReturnType instanceof NullType) { + return null; + } + if (methodReturnType instanceof ArrayType at) { + arrayType = at.getComponentType(); + } else { + throw new IllegalStateException("Expected an array got: " + methodReturnType + " " + originatingElement + " " + member); + } + Class type = ClassUtils.getPrimitiveType(arrayType.toString()).orElse(null); + if (type == null) { + Element element = typeUtils.asElement(arrayType); + if (element != null) { + if (element.getKind() == ElementKind.ENUM) { + type = String.class; + } else if (element.getKind() == ElementKind.ANNOTATION_TYPE) { + type = io.micronaut.core.annotation.AnnotationValue.class; + } else if (Class.class.getName().equals(element.toString())) { + type = io.micronaut.core.annotation.AnnotationClassValue.class; + } else { + // Annotations allow only basic Java classes so this should be fine + type = ClassUtils.forName(element.toString(), getClass().getClassLoader()).orElse(null); } } - return values.toArray((Object[]) Array.newInstance(arrayType, values.size())); - } else { - return values.toArray(new Object[0]); } + if (type == null) { + throw new IllegalStateException("Cannot determine the type of: " + methodReturnType); + } + type = ReflectionUtils.getPrimitiveType(type); + if (type.isPrimitive()) { + Class wrapperType = ReflectionUtils.getWrapperType(type); + Class primitiveArrayType = Array.newInstance(type, 0).getClass(); + Object[] emptyWrapperArray = (Object[]) Array.newInstance(wrapperType, 0); + Object[] wrapperArray = values.toArray(emptyWrapperArray); + // Convert to a proper primitive type array + return ConversionService.SHARED.convertRequired(wrapperArray, primitiveArrayType); + } + return ConversionService.SHARED.convertRequired(values, Array.newInstance(type, 0).getClass()); } @Override public Object visitBoolean(boolean b, Object o) { - arrayType = Boolean.class; values.add(b); return null; } @Override public Object visitByte(byte b, Object o) { - arrayType = Byte.class; values.add(b); return null; } @Override public Object visitChar(char c, Object o) { - arrayType = Character.class; values.add(c); return null; } @Override public Object visitDouble(double d, Object o) { - arrayType = Double.class; values.add(d); return null; } @Override public Object visitFloat(float f, Object o) { - arrayType = Float.class; values.add(f); return null; } @Override public Object visitInt(int i, Object o) { - arrayType = Integer.class; values.add(i); return null; } @Override public Object visitLong(long i, Object o) { - arrayType = Long.class; values.add(i); return null; } @Override public Object visitShort(short s, Object o) { - arrayType = Short.class; values.add(s); return null; } @Override public Object visitString(String s, Object o) { - arrayType = String.class; values.add(s); return null; } @Override public Object visitType(TypeMirror t, Object o) { - arrayType = AnnotationClassValue.class; if (t instanceof DeclaredType) { Element typeElement = ((DeclaredType) t).asElement(); if (typeElement instanceof TypeElement) { @@ -850,14 +861,12 @@ public Object visitType(TypeMirror t, Object o) { @Override public Object visitEnumConstant(VariableElement c, Object o) { - arrayType = String.class; values.add(c.getSimpleName().toString()); return null; } @Override public Object visitAnnotation(AnnotationMirror a, Object o) { - arrayType = io.micronaut.core.annotation.AnnotationValue.class; io.micronaut.core.annotation.AnnotationValue annotationValue = readNestedAnnotationValue(originatingElement, a); values.add(annotationValue); return null; diff --git a/inject-java/src/test/groovy/io/micronaut/annotation/mapping/AddsRepeatableAnnotationSpec.groovy b/inject-java/src/test/groovy/io/micronaut/annotation/mapping/AddsRepeatableAnnotationSpec.groovy index c8fca4f188d..691638517aa 100644 --- a/inject-java/src/test/groovy/io/micronaut/annotation/mapping/AddsRepeatableAnnotationSpec.groovy +++ b/inject-java/src/test/groovy/io/micronaut/annotation/mapping/AddsRepeatableAnnotationSpec.groovy @@ -1,8 +1,6 @@ package io.micronaut.annotation.mapping import io.micronaut.annotation.processing.test.AbstractTypeElementSpec -import io.micronaut.context.annotation.Requirements -import io.micronaut.context.annotation.Requires import io.micronaut.core.annotation.AnnotationMetadata import io.micronaut.core.annotation.AnnotationValue import io.micronaut.inject.ast.ClassElement @@ -18,23 +16,22 @@ class AddsRepeatableAnnotationSpec extends AbstractTypeElementSpec { def definition = buildBeanDefinition('addann.AddAnnotationsTo', ''' package addann; -import io.micronaut.context.annotation.Requires; import io.micronaut.inject.annotation.ScopeOne; import io.micronaut.context.annotation.Bean; -@Requires(property = "foo") -@Requires(property = "bar") +@io.micronaut.annotation.mapping.MyRequires(property = "foo") +@io.micronaut.annotation.mapping.MyRequires(property = "bar") @Bean class AddAnnotationsTo { - @Requires(property = "xyz") + @io.micronaut.annotation.mapping.MyRequires(property = "xyz") public Object myField; } ''') expect: - definition.getAnnotationValuesByType(Requires).size() == 3 - definition.getAnnotationValuesByType(Requires).stream() + definition.getAnnotationValuesByType(MyRequires).size() == 3 + definition.getAnnotationValuesByType(MyRequires).stream() .flatMap { it.stringValue("property").stream() } .toList().toSet() == ["foo", "bar", "xyz"].toSet() } @@ -43,16 +40,16 @@ class AddAnnotationsTo { @Override void visitClass(ClassElement element, VisitorContext context) { if (element.getSimpleName() == "AddAnnotationsTo") { - element.annotate(Requirements.class, builder -> builder.values( + element.annotate(MyRequirements.class, builder -> builder.values( element.getFields().stream().flatMap(this::getIndexes).toArray(AnnotationValue[]::new))) } } - private Stream> getIndexes(AnnotationMetadata am) { - if (am.getAnnotation(Requirements).getAnnotations("value", Requires).isEmpty()) { + private Stream> getIndexes(AnnotationMetadata am) { + if (am.getAnnotation(MyRequirements).getAnnotations("value", MyRequires).isEmpty()) { throw new IllegalStateException(); } - return am.getAnnotationValuesByType(Requires.class).stream() + return am.getAnnotationValuesByType(MyRequires.class).stream() } } diff --git a/inject-java/src/test/groovy/io/micronaut/annotation/mapping/AddsUnseenInnerRepeatableAnnotationSpec.groovy b/inject-java/src/test/groovy/io/micronaut/annotation/mapping/AddsUnseenInnerRepeatableAnnotationSpec.groovy new file mode 100644 index 00000000000..41697df6575 --- /dev/null +++ b/inject-java/src/test/groovy/io/micronaut/annotation/mapping/AddsUnseenInnerRepeatableAnnotationSpec.groovy @@ -0,0 +1,47 @@ +package io.micronaut.annotation.mapping + +import io.micronaut.annotation.processing.test.AbstractTypeElementSpec +import io.micronaut.core.annotation.AnnotationValue +import io.micronaut.inject.ast.ClassElement +import io.micronaut.inject.visitor.TypeElementVisitor +import io.micronaut.inject.visitor.VisitorContext + +class AddsUnseenInnerRepeatableAnnotationSpec extends AbstractTypeElementSpec { + + void 'test replace simple annotation'() { + given: + def definition = buildBeanDefinition('addann.AddUnseenAnnotationsTo2', ''' +package addann; + +import io.micronaut.inject.annotation.ScopeOne; +import io.micronaut.context.annotation.Bean; + +@Bean +class AddUnseenAnnotationsTo2 { + + public Object myField; + +} +''') + expect: + definition.getAnnotationValuesByType(MyRequirements2.MyRequires2).size() == 2 + definition.getAnnotationValuesByType(MyRequirements2.MyRequires2).stream() + .flatMap { it.stringValue("property").stream() } + .toList().toSet() == ["foo", "bar"].toSet() + } + + static class AddUnseenRepeatableTypeElementVisitor implements TypeElementVisitor { + @Override + + void visitClass(ClassElement element, VisitorContext context) { + if (element.getSimpleName() == "AddUnseenAnnotationsTo2") { + List> values = new ArrayList<>() + values.add(AnnotationValue.builder(MyRequirements2.MyRequires2).member("property", "foo").build()) + values.add(AnnotationValue.builder(MyRequirements2.MyRequires2).member("property", "bar").build()) + element.annotate(MyRequirements2.class, builder -> builder.values(values.toArray(new AnnotationValue[0]))) + } + } + + } + +} diff --git a/inject-java/src/test/groovy/io/micronaut/annotation/mapping/AddsUnseenRepeatableAnnotationSpec.groovy b/inject-java/src/test/groovy/io/micronaut/annotation/mapping/AddsUnseenRepeatableAnnotationSpec.groovy new file mode 100644 index 00000000000..d31c4e09274 --- /dev/null +++ b/inject-java/src/test/groovy/io/micronaut/annotation/mapping/AddsUnseenRepeatableAnnotationSpec.groovy @@ -0,0 +1,47 @@ +package io.micronaut.annotation.mapping + +import io.micronaut.annotation.processing.test.AbstractTypeElementSpec +import io.micronaut.core.annotation.AnnotationValue +import io.micronaut.inject.ast.ClassElement +import io.micronaut.inject.visitor.TypeElementVisitor +import io.micronaut.inject.visitor.VisitorContext + +class AddsUnseenRepeatableAnnotationSpec extends AbstractTypeElementSpec { + + void 'test replace simple annotation'() { + given: + def definition = buildBeanDefinition('addann.AddUnseenAnnotationsTo', ''' +package addann; + +import io.micronaut.inject.annotation.ScopeOne; +import io.micronaut.context.annotation.Bean; + +@Bean +class AddUnseenAnnotationsTo { + + public Object myField; + +} +''') + expect: + definition.getAnnotationValuesByType(MyRequires).size() == 2 + definition.getAnnotationValuesByType(MyRequires).stream() + .flatMap { it.stringValue("property").stream() } + .toList().toSet() == ["foo", "bar"].toSet() + } + + static class AddUnseenRepeatableTypeElementVisitor implements TypeElementVisitor { + @Override + + void visitClass(ClassElement element, VisitorContext context) { + if (element.getSimpleName() == "AddUnseenAnnotationsTo") { + List> values = new ArrayList<>() + values.add(AnnotationValue.builder(MyRequires).member("property", "foo").build()) + values.add(AnnotationValue.builder(MyRequires).member("property", "bar").build()) + element.annotate(MyRequirements.class, builder -> builder.values(values.toArray(new AnnotationValue[0]))) + } + } + + } + +} diff --git a/inject-java/src/test/groovy/io/micronaut/annotation/mapping/MapMeToRepeatable.java b/inject-java/src/test/groovy/io/micronaut/annotation/mapping/MapMeToRepeatable.java new file mode 100644 index 00000000000..56e98b0b165 --- /dev/null +++ b/inject-java/src/test/groovy/io/micronaut/annotation/mapping/MapMeToRepeatable.java @@ -0,0 +1,29 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.annotation.mapping; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +@Documented +@Target({TYPE}) +@Retention(RUNTIME) +public @interface MapMeToRepeatable { +} diff --git a/inject-java/src/test/groovy/io/micronaut/annotation/mapping/MapToRepeatableAnnotationSpec.groovy b/inject-java/src/test/groovy/io/micronaut/annotation/mapping/MapToRepeatableAnnotationSpec.groovy new file mode 100644 index 00000000000..2aa693ae86d --- /dev/null +++ b/inject-java/src/test/groovy/io/micronaut/annotation/mapping/MapToRepeatableAnnotationSpec.groovy @@ -0,0 +1,50 @@ +package io.micronaut.annotation.mapping + +import io.micronaut.annotation.processing.test.AbstractTypeElementSpec +import io.micronaut.core.annotation.AnnotationValue +import io.micronaut.inject.annotation.TypedAnnotationMapper +import io.micronaut.inject.visitor.VisitorContext + +class MapToRepeatableAnnotationSpec extends AbstractTypeElementSpec { + + void 'test remapping'() { + given: + def definition = buildBeanDefinition('addann.RemapToRepeatableAnnotationsTo', ''' +package addann; + +import io.micronaut.context.annotation.Bean; + +@Bean +@io.micronaut.annotation.mapping.MapMeToRepeatable +class RemapToRepeatableAnnotationsTo { + + public Object myField; + +} +''') + expect: + definition.hasAnnotation(MapMeToRepeatable) + definition.getAnnotationValuesByType(MyRequires).size() == 2 + definition.getAnnotationValuesByType(MyRequires).stream() + .flatMap { it.stringValue("property").stream() } + .toList().toSet() == ["fooM", "barM"].toSet() + } + + static class TheAnnotationMapper implements TypedAnnotationMapper { + + @Override + List> map(AnnotationValue annotation, VisitorContext visitorContext) { + return Arrays.asList( + AnnotationValue.builder(MyRequires).member("property", "fooM").build(), + AnnotationValue.builder(MyRequires).member("property", "barM").build() + ) + } + + @Override + Class annotationType() { + return MapMeToRepeatable.class + } + } + + +} diff --git a/inject-java/src/test/groovy/io/micronaut/annotation/mapping/MappedValueHasDefaultSpec.groovy b/inject-java/src/test/groovy/io/micronaut/annotation/mapping/MappedValueHasDefaultSpec.groovy new file mode 100644 index 00000000000..21f748c8031 --- /dev/null +++ b/inject-java/src/test/groovy/io/micronaut/annotation/mapping/MappedValueHasDefaultSpec.groovy @@ -0,0 +1,55 @@ +package io.micronaut.annotation.mapping + +import io.micronaut.annotation.processing.test.AbstractTypeElementSpec +import io.micronaut.core.annotation.AnnotationValue +import io.micronaut.inject.annotation.TypedAnnotationMapper +import io.micronaut.inject.visitor.VisitorContext + +class MappedValueHasDefaultSpec extends AbstractTypeElementSpec { + + void 'test remapping'() { + given: + def definition = buildBeanDefinition('addann.MappedValueHasDefault', ''' +package addann; + +import io.micronaut.context.annotation.Bean; + +@Bean +@io.micronaut.annotation.mapping.MySourceAnnotation2 +class MappedValueHasDefault { + + public Object myField; + +} +''') + expect: + !definition.hasAnnotation(MySourceAnnotation2) + definition.getAnnotationValuesByType(MyRequires).size() == 2 + definition.getAnnotationValuesByType(MyRequires).stream() + .flatMap { it.stringValue("property").stream() } + .toList().toSet() == ["fooX", "barX"].toSet() + } + + static class TheAnnotationMapper implements TypedAnnotationMapper { + + @Override + List> map(AnnotationValue annotation, VisitorContext visitorContext) { + def propertyValue = annotation.getRequiredValue("property", String.class) + def countValue = annotation.getRequiredValue("count", Integer.class) + if (propertyValue != "foo" || countValue != 123) { + throw new IllegalStateException() + } + return Arrays.asList( + AnnotationValue.builder(MyRequires).member("property", "fooX").build(), + AnnotationValue.builder(MyRequires).member("property", "barX").build() + ) + } + + @Override + Class annotationType() { + return MySourceAnnotation2.class + } + } + + +} diff --git a/inject-java/src/test/groovy/io/micronaut/annotation/mapping/MyRequirements.java b/inject-java/src/test/groovy/io/micronaut/annotation/mapping/MyRequirements.java new file mode 100644 index 00000000000..058a5010a19 --- /dev/null +++ b/inject-java/src/test/groovy/io/micronaut/annotation/mapping/MyRequirements.java @@ -0,0 +1,30 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.annotation.mapping; + +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; + +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.PACKAGE, ElementType.TYPE, ElementType.ANNOTATION_TYPE, ElementType.METHOD, ElementType.FIELD}) +public @interface MyRequirements { + + MyRequires[] value(); +} diff --git a/inject-java/src/test/groovy/io/micronaut/annotation/mapping/MyRequirements2.java b/inject-java/src/test/groovy/io/micronaut/annotation/mapping/MyRequirements2.java new file mode 100644 index 00000000000..f8eca71574c --- /dev/null +++ b/inject-java/src/test/groovy/io/micronaut/annotation/mapping/MyRequirements2.java @@ -0,0 +1,43 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.annotation.mapping; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Repeatable; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.PACKAGE, ElementType.TYPE, ElementType.ANNOTATION_TYPE, ElementType.METHOD, ElementType.FIELD}) +public @interface MyRequirements2 { + + MyRequires2[] value(); + + + @Documented + @Retention(RetentionPolicy.RUNTIME) + @Target({ElementType.PACKAGE, ElementType.TYPE, ElementType.ANNOTATION_TYPE, ElementType.METHOD, ElementType.FIELD}) + @Repeatable(MyRequirements2.class) + @interface MyRequires2 { + + String property() default ""; + + } + +} diff --git a/inject-java/src/test/groovy/io/micronaut/annotation/mapping/MyRequires.java b/inject-java/src/test/groovy/io/micronaut/annotation/mapping/MyRequires.java new file mode 100644 index 00000000000..1ee424aca5a --- /dev/null +++ b/inject-java/src/test/groovy/io/micronaut/annotation/mapping/MyRequires.java @@ -0,0 +1,33 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.annotation.mapping; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Repeatable; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.PACKAGE, ElementType.TYPE, ElementType.ANNOTATION_TYPE, ElementType.METHOD, ElementType.FIELD}) +@Repeatable(MyRequirements.class) +public @interface MyRequires { + + String property() default ""; + +} diff --git a/inject-java/src/test/groovy/io/micronaut/annotation/mapping/MySourceAnnotation.java b/inject-java/src/test/groovy/io/micronaut/annotation/mapping/MySourceAnnotation.java new file mode 100644 index 00000000000..caecc4be300 --- /dev/null +++ b/inject-java/src/test/groovy/io/micronaut/annotation/mapping/MySourceAnnotation.java @@ -0,0 +1,33 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.annotation.mapping; + +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; + +@Documented +@Retention(RetentionPolicy.SOURCE) +@Target({ElementType.PACKAGE, ElementType.TYPE, ElementType.ANNOTATION_TYPE, ElementType.METHOD, ElementType.FIELD}) +public @interface MySourceAnnotation { + + String property() default "foo"; + + int count() default 123; + +} diff --git a/inject-java/src/test/groovy/io/micronaut/annotation/mapping/MySourceAnnotation2.java b/inject-java/src/test/groovy/io/micronaut/annotation/mapping/MySourceAnnotation2.java new file mode 100644 index 00000000000..626b0312d5b --- /dev/null +++ b/inject-java/src/test/groovy/io/micronaut/annotation/mapping/MySourceAnnotation2.java @@ -0,0 +1,33 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.annotation.mapping; + +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; + +@Documented +@Retention(RetentionPolicy.SOURCE) +@Target({ElementType.PACKAGE, ElementType.TYPE, ElementType.ANNOTATION_TYPE, ElementType.METHOD, ElementType.FIELD}) +public @interface MySourceAnnotation2 { + + String property() default "foo"; + + int count() default 123; + +} diff --git a/inject-java/src/test/groovy/io/micronaut/annotation/mapping/RemapMeToRepeatable.java b/inject-java/src/test/groovy/io/micronaut/annotation/mapping/RemapMeToRepeatable.java new file mode 100644 index 00000000000..cbb7616a05a --- /dev/null +++ b/inject-java/src/test/groovy/io/micronaut/annotation/mapping/RemapMeToRepeatable.java @@ -0,0 +1,29 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.annotation.mapping; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +@Documented +@Target({TYPE}) +@Retention(RUNTIME) +public @interface RemapMeToRepeatable { +} diff --git a/inject-java/src/test/groovy/io/micronaut/annotation/mapping/RemapToRepeatableAnnotationSpec.groovy b/inject-java/src/test/groovy/io/micronaut/annotation/mapping/RemapToRepeatableAnnotationSpec.groovy new file mode 100644 index 00000000000..981c4693e02 --- /dev/null +++ b/inject-java/src/test/groovy/io/micronaut/annotation/mapping/RemapToRepeatableAnnotationSpec.groovy @@ -0,0 +1,54 @@ +package io.micronaut.annotation.mapping + +import io.micronaut.annotation.processing.test.AbstractTypeElementSpec +import io.micronaut.core.annotation.AnnotationValue +import io.micronaut.inject.annotation.AnnotationRemapper +import io.micronaut.inject.visitor.VisitorContext + +class RemapToRepeatableAnnotationSpec extends AbstractTypeElementSpec { + + void 'test remapping'() { + given: + def definition = buildBeanDefinition('addann.RemapToRepeatableAnnotationsTo', ''' +package addann; + +import io.micronaut.context.annotation.Bean; + +@Bean +@io.micronaut.annotation.mapping.RemapMeToRepeatable +class RemapToRepeatableAnnotationsTo { + + public Object myField; + +} +''') + expect: + !definition.hasAnnotation(RemapMeToRepeatable) + definition.getAnnotationValuesByType(MyRequires).size() == 2 + definition.getAnnotationValuesByType(MyRequires).stream() + .flatMap { it.stringValue("property").stream() } + .toList().toSet() == ["fooR", "barR"].toSet() + } + + static class TheAnnotationMapper implements AnnotationRemapper { + + + @Override + String getPackageName() { + return "io.micronaut.annotation.mapping" + } + + @Override + List> remap(AnnotationValue annotation, VisitorContext visitorContext) { + if (annotation.getAnnotationName() == RemapMeToRepeatable.class.name) { + return Arrays.asList( + AnnotationValue.builder(MyRequires).member("property", "fooR").build(), + AnnotationValue.builder(MyRequires).member("property", "barR").build() + ) + } + return Collections.singletonList(annotation) + } + } + + +} diff --git a/inject-java/src/test/groovy/io/micronaut/annotation/mapping/ReplacesRepeatableAnnotationSpec.groovy b/inject-java/src/test/groovy/io/micronaut/annotation/mapping/ReplacesRepeatableAnnotationSpec.groovy index a634ccbf51a..64d872c0116 100644 --- a/inject-java/src/test/groovy/io/micronaut/annotation/mapping/ReplacesRepeatableAnnotationSpec.groovy +++ b/inject-java/src/test/groovy/io/micronaut/annotation/mapping/ReplacesRepeatableAnnotationSpec.groovy @@ -1,8 +1,6 @@ package io.micronaut.annotation.mapping import io.micronaut.annotation.processing.test.AbstractTypeElementSpec -import io.micronaut.context.annotation.Requirements -import io.micronaut.context.annotation.Requires import io.micronaut.core.annotation.AnnotationMetadata import io.micronaut.core.annotation.AnnotationValue import io.micronaut.inject.ast.ClassElement @@ -18,22 +16,21 @@ class ReplacesRepeatableAnnotationSpec extends AbstractTypeElementSpec { def definition = buildBeanDefinition('addann.ReplaceAnnotationsTo', ''' package addann; -import io.micronaut.context.annotation.Requires; import io.micronaut.context.annotation.Bean; -@Requires(property = "foo") -@Requires(property = "bar") +@io.micronaut.annotation.mapping.MyRequires(property = "foo") +@io.micronaut.annotation.mapping.MyRequires(property = "bar") @Bean class ReplaceAnnotationsTo { - @Requires(property = "xyz") + @io.micronaut.annotation.mapping.MyRequires(property = "xyz") public Object myField; } ''') expect: - definition.getAnnotationValuesByType(Requires).size() == 3 - definition.getAnnotationValuesByType(Requires).stream() + definition.getAnnotationValuesByType(MyRequires).size() == 3 + definition.getAnnotationValuesByType(MyRequires).stream() .flatMap { it.stringValue("property").stream() } .toList().toSet() == ["foo", "bar", "xyz"].toSet() } @@ -42,19 +39,19 @@ class ReplaceAnnotationsTo { @Override void visitClass(ClassElement element, VisitorContext context) { if (element.getSimpleName() == "ReplaceAnnotationsTo") { - final List> indexes = Stream.concat( + final List> indexes = Stream.concat( getIndexes(element), element.getFields().stream().flatMap(this::getIndexes) ).toList() - element.annotate(Requirements.class, builder -> builder.values(indexes.toArray(new AnnotationValue[]{}))) + element.annotate(MyRequirements.class, builder -> builder.values(indexes.toArray(new AnnotationValue[]{}))) } } - private Stream> getIndexes(AnnotationMetadata am) { - if (am.getAnnotation(Requirements).getAnnotations("value", Requires).isEmpty()) { + private Stream> getIndexes(AnnotationMetadata am) { + if (am.getAnnotation(MyRequirements).getAnnotations("value", MyRequires).isEmpty()) { throw new IllegalStateException(); } - return am.getAnnotationValuesByType(Requires.class).stream(); + return am.getAnnotationValuesByType(MyRequires.class).stream(); } } diff --git a/inject-java/src/test/groovy/io/micronaut/annotation/mapping/Seen.java b/inject-java/src/test/groovy/io/micronaut/annotation/mapping/Seen.java new file mode 100644 index 00000000000..c775fe58a28 --- /dev/null +++ b/inject-java/src/test/groovy/io/micronaut/annotation/mapping/Seen.java @@ -0,0 +1,28 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.annotation.mapping; + +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; + +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.PACKAGE, ElementType.TYPE, ElementType.ANNOTATION_TYPE, ElementType.METHOD, ElementType.FIELD}) +public @interface Seen { +} diff --git a/inject-java/src/test/groovy/io/micronaut/annotation/mapping/SourceAnnotationHasDefaultsSpec.groovy b/inject-java/src/test/groovy/io/micronaut/annotation/mapping/SourceAnnotationHasDefaultsSpec.groovy new file mode 100644 index 00000000000..adaf4c360a7 --- /dev/null +++ b/inject-java/src/test/groovy/io/micronaut/annotation/mapping/SourceAnnotationHasDefaultsSpec.groovy @@ -0,0 +1,44 @@ +package io.micronaut.annotation.mapping + +import io.micronaut.annotation.processing.test.AbstractTypeElementSpec +import io.micronaut.inject.ast.ClassElement +import io.micronaut.inject.visitor.TypeElementVisitor +import io.micronaut.inject.visitor.VisitorContext + +class SourceAnnotationHasDefaultsSpec extends AbstractTypeElementSpec { + + void 'test source annotation has defaults'() { + given: + def definition = buildBeanDefinition('addann.SourceDefaultsAnnotationTest', ''' +package addann; + +import io.micronaut.inject.annotation.ScopeOne; +import io.micronaut.context.annotation.Bean; + +@io.micronaut.annotation.mapping.MySourceAnnotation +@Bean +class SourceDefaultsAnnotationTest { +} +''') + expect: + definition.hasAnnotation(Seen.class) + } + + static class TheVisitor implements TypeElementVisitor { + + @Override + void visitClass(ClassElement element, VisitorContext context) { + if (element.getSimpleName() == "SourceDefaultsAnnotationTest") { + def annotation = element.getAnnotation(MySourceAnnotation.class) + def propertyValue = annotation.getRequiredValue("property", String.class) + def countValue = annotation.getRequiredValue("count", Integer.class) + if (propertyValue != "foo" || countValue != 123) { + throw new IllegalStateException() + } else { + element.annotate(Seen.class) + } + } + } + } + +} diff --git a/inject-java/src/test/groovy/io/micronaut/annotation/mapping/TransformMeToRepeatable.java b/inject-java/src/test/groovy/io/micronaut/annotation/mapping/TransformMeToRepeatable.java new file mode 100644 index 00000000000..5a021e9ff41 --- /dev/null +++ b/inject-java/src/test/groovy/io/micronaut/annotation/mapping/TransformMeToRepeatable.java @@ -0,0 +1,29 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.annotation.mapping; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +@Documented +@Target({TYPE}) +@Retention(RUNTIME) +public @interface TransformMeToRepeatable { +} diff --git a/inject-java/src/test/groovy/io/micronaut/annotation/mapping/TransformsToRepeatableAnnotationSpec.groovy b/inject-java/src/test/groovy/io/micronaut/annotation/mapping/TransformsToRepeatableAnnotationSpec.groovy new file mode 100644 index 00000000000..40678ff4a07 --- /dev/null +++ b/inject-java/src/test/groovy/io/micronaut/annotation/mapping/TransformsToRepeatableAnnotationSpec.groovy @@ -0,0 +1,51 @@ +package io.micronaut.annotation.mapping + +import io.micronaut.annotation.processing.test.AbstractTypeElementSpec +import io.micronaut.core.annotation.AnnotationValue +import io.micronaut.inject.annotation.TypedAnnotationTransformer +import io.micronaut.inject.visitor.VisitorContext + +class TransformsToRepeatableAnnotationSpec extends AbstractTypeElementSpec { + + void 'test remapping'() { + given: + def definition = buildBeanDefinition('addann.TransformToRepeatableAnnotationsTo', ''' +package addann; + +import io.micronaut.inject.annotation.ScopeOne; +import io.micronaut.context.annotation.Bean; + +@Bean +@io.micronaut.annotation.mapping.TransformMeToRepeatable +class TransformToRepeatableAnnotationsTo { + + public Object myField; + +} +''') + expect: + !definition.hasAnnotation(TransformMeToRepeatable.class) + definition.getAnnotationValuesByType(MyRequires).size() == 2 + definition.getAnnotationValuesByType(MyRequires).stream() + .flatMap { it.stringValue("property").stream() } + .toList().toSet() == ["fooT", "barT"].toSet() + } + + static class TheAnnotationTransformer implements TypedAnnotationTransformer { + + @Override + List> transform(AnnotationValue annotation, VisitorContext visitorContext) { + return Arrays.asList( + AnnotationValue.builder(MyRequires).member("property", "fooT").build(), + AnnotationValue.builder(MyRequires).member("property", "barT").build() + ) + } + + @Override + Class annotationType() { + return TransformMeToRepeatable.class + } + } + + +} diff --git a/inject-java/src/test/groovy/io/micronaut/inject/annotation/AnnotationMetadataWriterSpec.groovy b/inject-java/src/test/groovy/io/micronaut/inject/annotation/AnnotationMetadataWriterSpec.groovy index 2b14629a97e..c4b74e88d5d 100644 --- a/inject-java/src/test/groovy/io/micronaut/inject/annotation/AnnotationMetadataWriterSpec.groovy +++ b/inject-java/src/test/groovy/io/micronaut/inject/annotation/AnnotationMetadataWriterSpec.groovy @@ -693,5 +693,102 @@ class Test { metadata.classValues(TypeHint)[1] == UUID.class } + void "test defaults"() { + given: + AnnotationMetadata toWrite = buildTypeAnnotationMetadata('''\ +package test; + +@io.micronaut.inject.annotation.MyAnnotation2(intArray3 = 1, stringArray4 = "X", boolArray4 = false, myEnumArray4 = io.micronaut.inject.annotation.MyEnum2.FOO) +class Test { +} + +''') + when: + AnnotationMetadata metadata = writeAndLoadMetadata("test", toWrite) + def defaults = metadata.getDefaultValues("io.micronaut.inject.annotation.MyAnnotation2") + def av = metadata.getAnnotation("io.micronaut.inject.annotation.MyAnnotation2") + + then: + defaults["num"] == 10 + defaults["bool"] == false + defaults["intArray1"] == new int[] {} + defaults["intArray2"] == new int[] {1, 2, 3} + defaults["intArray3"] == null + defaults["stringArray1"] == new String[] {} + defaults["stringArray2"] == new String[] {""} + defaults["stringArray3"] == new String[] {"A"} + defaults["stringArray4"] == null + defaults["boolArray1"] == new boolean[] {} + defaults["boolArray2"] == new boolean[] {true} + defaults["boolArray3"] == new boolean[] {false} + defaults["boolArray4"] == null + defaults["myEnumArray1"] == new String[] {} + defaults["myEnumArray2"] == new String[] {"ABC"} + defaults["myEnumArray3"] == new String[] {"FOO", "BAR"} + defaults["myEnumArray4"] == null + defaults["classesArray1"] == new AnnotationClassValue[0] + defaults["classesArray2"] == new AnnotationClassValue[] {new AnnotationClassValue(String)} + defaults["ann"] == AnnotationValue.builder(MyAnnotation3).value("foo").build() + defaults["annotationsArray1"] == new AnnotationValue[0] + defaults["annotationsArray2"] == new AnnotationValue[] { AnnotationValue.builder(MyAnnotation3).value("foo").build(), AnnotationValue.builder(MyAnnotation3).value("bar").build() } + + av.getRequiredValue("num", Integer.class) == 10 + av.getRequiredValue("bool", Boolean.class) == false + av.getRequiredValue("intArray1", int[].class) == new int[] {} + av.getRequiredValue("intArray2", int[].class) == new int[] {1, 2, 3} + av.getRequiredValue("stringArray1", String[].class) == new String[] {} + av.getRequiredValue("stringArray2", String[].class) == new String[] {""} + av.getRequiredValue("stringArray3", String[].class) == new String[] {"A"} + av.getRequiredValue("myEnumArray1", String[].class) == new String[] {} + av.getRequiredValue("myEnumArray2", String[].class) == new String[] {"ABC"} + av.getRequiredValue("myEnumArray3", String[].class) == new String[] {"FOO", "BAR"} + } + + void "test aliases"() { + given: + AnnotationMetadata toWrite = buildTypeAnnotationMetadata('''\ +package test; + +@io.micronaut.inject.annotation.MyAnnotation2Aliases( + intArray1Alias = {}, + intArray2Alias = {1, 2, 3}, + stringArray1Alias = {}, + stringArray2Alias = "", + stringArray3Alias = "A", + myEnumArray1Alias = {}, + myEnumArray2Alias = io.micronaut.inject.annotation.MyEnum2.ABC, + myEnumArray3Alias = {io.micronaut.inject.annotation.MyEnum2.FOO, io.micronaut.inject.annotation.MyEnum2.BAR}, + classesArray1Alias = {}, + classesArray2Alias = {String.class}, + annAlias = @io.micronaut.inject.annotation.MyAnnotation3("foo"), + annotationsArray1Alias = {}, + annotationsArray2Alias = { + @io.micronaut.inject.annotation.MyAnnotation3("foo"), + @io.micronaut.inject.annotation.MyAnnotation3("bar") + } +)class Test { +} + +''') + when: + AnnotationMetadata metadata = writeAndLoadMetadata("test", toWrite) + def values = metadata.getValues("io.micronaut.inject.annotation.MyAnnotation2Aliases") + def av = metadata.getAnnotation("io.micronaut.inject.annotation.MyAnnotation2Aliases") + + then: + values["intArray1"] == new int[] {} + values["intArray2"] == new int[] {1, 2, 3} + values["stringArray1"] == new String[] {} + values["stringArray2"] == new String[] {""} + values["stringArray3"] == new String[] {"A"} + values["myEnumArray1"] == new String[] {} + values["myEnumArray2"] == new String[] {"ABC"} + values["myEnumArray3"] == new String[] {"FOO", "BAR"} + values["classesArray1"] == new AnnotationClassValue[0] + values["classesArray2"] == new AnnotationClassValue[] {new AnnotationClassValue(String)} + values["ann"] == AnnotationValue.builder(MyAnnotation3).value("foo").build() + values["annotationsArray1"] == new AnnotationValue[0] + values["annotationsArray2"] == new AnnotationValue[] { AnnotationValue.builder(MyAnnotation3).value("foo").build(), AnnotationValue.builder(MyAnnotation3).value("bar").build() } + } } diff --git a/inject-java/src/test/groovy/io/micronaut/inject/annotation/MyAnnotation2.java b/inject-java/src/test/groovy/io/micronaut/inject/annotation/MyAnnotation2.java new file mode 100644 index 00000000000..cbdcc49db21 --- /dev/null +++ b/inject-java/src/test/groovy/io/micronaut/inject/annotation/MyAnnotation2.java @@ -0,0 +1,63 @@ +package io.micronaut.inject.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +@Documented +@Retention(RUNTIME) +@Target({ElementType.TYPE}) +public @interface MyAnnotation2 { + + int num() default 10; + + String value() default ""; + + boolean bool() default false; + + MyAnnotation3 ann() default @MyAnnotation3("foo"); + + MyEnum2 myEnum() default MyEnum2.ABC; + + int[] intArray1() default {}; + + int[] intArray2() default {1, 2, 3}; + + int[] intArray3(); + + String[] stringArray1() default {}; + + String[] stringArray2() default {""}; + + String[] stringArray3() default {"A"}; + + String[] stringArray4(); + + boolean[] boolArray1() default {}; + + boolean[] boolArray2() default {true}; + + boolean[] boolArray3() default {false}; + + boolean[] boolArray4(); + + MyEnum2[] myEnumArray1() default {}; + + MyEnum2[] myEnumArray2() default {MyEnum2.ABC}; + + MyEnum2[] myEnumArray3() default {MyEnum2.FOO, MyEnum2.BAR}; + + MyEnum2[] myEnumArray4(); + + Class[] classesArray1() default {}; + + Class[] classesArray2() default {String.class}; + + MyAnnotation3[] annotationsArray1() default {}; + + MyAnnotation3[] annotationsArray2() default {@MyAnnotation3("foo"), @MyAnnotation3("bar")}; + +} diff --git a/inject-java/src/test/groovy/io/micronaut/inject/annotation/MyAnnotation2Aliases.java b/inject-java/src/test/groovy/io/micronaut/inject/annotation/MyAnnotation2Aliases.java new file mode 100644 index 00000000000..73e98b4904d --- /dev/null +++ b/inject-java/src/test/groovy/io/micronaut/inject/annotation/MyAnnotation2Aliases.java @@ -0,0 +1,82 @@ +package io.micronaut.inject.annotation; + +import io.micronaut.context.annotation.AliasFor; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +@Documented +@Retention(RUNTIME) +@Target({ElementType.TYPE}) +public @interface MyAnnotation2Aliases { + + int[] intArray1() default {}; + + int[] intArray2() default {}; + + @AliasFor(member = "intArray1") + int[] intArray1Alias(); + + @AliasFor(member = "intArray2") + int[] intArray2Alias(); + + String[] stringArray1() default {}; + + String[] stringArray2() default {}; + + String[] stringArray3() default {}; + + @AliasFor(member = "stringArray1") + String[] stringArray1Alias(); + + @AliasFor(member = "stringArray2") + String[] stringArray2Alias(); + + @AliasFor(member = "stringArray3") + String[] stringArray3Alias(); + + MyEnum2[] myEnumArray1() default {}; + + MyEnum2[] myEnumArray2() default {}; + + MyEnum2[] myEnumArray3() default {}; + + @AliasFor(member = "myEnumArray1") + MyEnum2[] myEnumArray1Alias(); + + @AliasFor(member = "myEnumArray2") + MyEnum2[] myEnumArray2Alias(); + + @AliasFor(member = "myEnumArray3") + MyEnum2[] myEnumArray3Alias(); + + Class[] classesArray1() default {}; + + Class[] classesArray2() default {}; + + @AliasFor(member = "classesArray1") + Class[] classesArray1Alias(); + + @AliasFor(member = "classesArray2") + Class[] classesArray2Alias(); + + MyAnnotation3 ann() default @MyAnnotation3("default"); + + @AliasFor(member = "ann") + MyAnnotation3 annAlias(); + + MyAnnotation3[] annotationsArray1() default {}; + + MyAnnotation3[] annotationsArray2() default {}; + + @AliasFor(member = "annotationsArray1") + MyAnnotation3[] annotationsArray1Alias(); + + @AliasFor(member = "annotationsArray2") + MyAnnotation3[] annotationsArray2Alias(); + +} diff --git a/inject-java/src/test/groovy/io/micronaut/inject/annotation/MyAnnotation3.java b/inject-java/src/test/groovy/io/micronaut/inject/annotation/MyAnnotation3.java new file mode 100644 index 00000000000..f2eaeaef4d9 --- /dev/null +++ b/inject-java/src/test/groovy/io/micronaut/inject/annotation/MyAnnotation3.java @@ -0,0 +1,17 @@ +package io.micronaut.inject.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +@Documented +@Retention(RUNTIME) +@Target({ElementType.TYPE}) +public @interface MyAnnotation3 { + + String value() default ""; + +} diff --git a/inject-java/src/test/groovy/io/micronaut/inject/annotation/MyEnum2.java b/inject-java/src/test/groovy/io/micronaut/inject/annotation/MyEnum2.java new file mode 100644 index 00000000000..53df25351ee --- /dev/null +++ b/inject-java/src/test/groovy/io/micronaut/inject/annotation/MyEnum2.java @@ -0,0 +1,5 @@ +package io.micronaut.inject.annotation; + +public enum MyEnum2 { + FOO, BAR, ABC +} diff --git a/inject-java/src/test/groovy/io/micronaut/inject/configproperties/InheritedConfigurationReaderPrefixSpec.groovy b/inject-java/src/test/groovy/io/micronaut/inject/configproperties/InheritedConfigurationReaderPrefixSpec.groovy index a12185ab02c..b8131792119 100644 --- a/inject-java/src/test/groovy/io/micronaut/inject/configproperties/InheritedConfigurationReaderPrefixSpec.groovy +++ b/inject-java/src/test/groovy/io/micronaut/inject/configproperties/InheritedConfigurationReaderPrefixSpec.groovy @@ -45,7 +45,7 @@ class MyBean { beanDefinition.getInjectedMethods()[0].name == 'setMyValue' def metadata = beanDefinition.getInjectedMethods()[0].getAnnotationMetadata() metadata.hasAnnotation(Property) - metadata.getValue(Property, "name", String).get() == 'endpoints.my-value' + metadata.getValue(Property, "name", String).get() == 'simple.my-value' } void "property path is overriding the existing one without base prefix"() { @@ -99,7 +99,7 @@ class MyBean { beanDefinition.getInjectedMethods()[0].name == 'setMyValue' def metadata = beanDefinition.getInjectedMethods()[0].getAnnotationMetadata() metadata.hasAnnotation(Property) - metadata.getValue(Property, "name", String).get() == 'endpoints.my-value' + metadata.getValue(Property, "name", String).get() == 'endpoints.simple.my-value' } void "property path is overriding the existing one"() { diff --git a/inject-java/src/test/resources/META-INF/services/io.micronaut.inject.annotation.AnnotationMapper b/inject-java/src/test/resources/META-INF/services/io.micronaut.inject.annotation.AnnotationMapper index f5b8ebe7310..e754548583b 100644 --- a/inject-java/src/test/resources/META-INF/services/io.micronaut.inject.annotation.AnnotationMapper +++ b/inject-java/src/test/resources/META-INF/services/io.micronaut.inject.annotation.AnnotationMapper @@ -4,4 +4,6 @@ io.micronaut.inject.annotation.CustomHeaderMapper io.micronaut.aop.introduction.ListenerAdviceMarkerMapper io.micronaut.aop.compile.AroundCompileSpec$NamedTestAnnMapper io.micronaut.annotation.mapping.CustomEmbeddedIdMapper -io.micronaut.annotation.mapping.NonNullProducingMapper \ No newline at end of file +io.micronaut.annotation.mapping.NonNullProducingMapper +io.micronaut.annotation.mapping.MapToRepeatableAnnotationSpec$TheAnnotationMapper +io.micronaut.annotation.mapping.MappedValueHasDefaultSpec$TheAnnotationMapper diff --git a/inject-java/src/test/resources/META-INF/services/io.micronaut.inject.annotation.AnnotationRemapper b/inject-java/src/test/resources/META-INF/services/io.micronaut.inject.annotation.AnnotationRemapper index 6ace27bd068..16d1298f4b8 100644 --- a/inject-java/src/test/resources/META-INF/services/io.micronaut.inject.annotation.AnnotationRemapper +++ b/inject-java/src/test/resources/META-INF/services/io.micronaut.inject.annotation.AnnotationRemapper @@ -1 +1,2 @@ -io.micronaut.inject.beanbuilder.TestInterceptorBindingRemapper \ No newline at end of file +io.micronaut.inject.beanbuilder.TestInterceptorBindingRemapper +io.micronaut.annotation.mapping.RemapToRepeatableAnnotationSpec$TheAnnotationMapper diff --git a/inject-java/src/test/resources/META-INF/services/io.micronaut.inject.annotation.AnnotationTransformer b/inject-java/src/test/resources/META-INF/services/io.micronaut.inject.annotation.AnnotationTransformer index 8278b164a3a..29d34b98218 100644 --- a/inject-java/src/test/resources/META-INF/services/io.micronaut.inject.annotation.AnnotationTransformer +++ b/inject-java/src/test/resources/META-INF/services/io.micronaut.inject.annotation.AnnotationTransformer @@ -1,3 +1,4 @@ io.micronaut.inject.beanbuilder.TestInterceptorBindingTransformer io.micronaut.aop.compile.AroundCompileSpec$TestStereotypeAnnTransformer -io.micronaut.aop.compile.AroundConstructCompileSpec$TestStereotypeAnnTransformer \ No newline at end of file +io.micronaut.aop.compile.AroundConstructCompileSpec$TestStereotypeAnnTransformer +io.micronaut.annotation.mapping.TransformsToRepeatableAnnotationSpec$TheAnnotationTransformer diff --git a/inject-java/src/test/resources/META-INF/services/io.micronaut.inject.visitor.TypeElementVisitor b/inject-java/src/test/resources/META-INF/services/io.micronaut.inject.visitor.TypeElementVisitor index 0a527c1b32e..f8c16c41471 100644 --- a/inject-java/src/test/resources/META-INF/services/io.micronaut.inject.visitor.TypeElementVisitor +++ b/inject-java/src/test/resources/META-INF/services/io.micronaut.inject.visitor.TypeElementVisitor @@ -7,3 +7,6 @@ io.micronaut.inject.factory.BasicVisitor io.micronaut.aop.introduction.repeatable.MyRepoVisitor io.micronaut.annotation.mapping.AddsRepeatableAnnotationSpec$AddRepeatableTypeElementVisitor io.micronaut.annotation.mapping.ReplacesRepeatableAnnotationSpec$ReplacesRepeatableTypeElementVisitor +io.micronaut.annotation.mapping.AddsUnseenRepeatableAnnotationSpec$AddUnseenRepeatableTypeElementVisitor +io.micronaut.annotation.mapping.AddsUnseenInnerRepeatableAnnotationSpec$AddUnseenRepeatableTypeElementVisitor +io.micronaut.annotation.mapping.SourceAnnotationHasDefaultsSpec$TheVisitor diff --git a/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/annotation/KotlinAnnotationMetadataBuilder.kt b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/annotation/KotlinAnnotationMetadataBuilder.kt index 3a51a2de0b5..c11710b38d4 100644 --- a/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/annotation/KotlinAnnotationMetadataBuilder.kt +++ b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/annotation/KotlinAnnotationMetadataBuilder.kt @@ -22,14 +22,13 @@ import com.google.devtools.ksp.isDefault import com.google.devtools.ksp.processing.Resolver import com.google.devtools.ksp.processing.SymbolProcessorEnvironment import com.google.devtools.ksp.symbol.* -import io.micronaut.context.annotation.ConfigurationReader import io.micronaut.context.annotation.Property import io.micronaut.core.annotation.AnnotationClassValue import io.micronaut.core.annotation.AnnotationUtil +import io.micronaut.core.annotation.AnnotationValue import io.micronaut.core.reflect.ReflectionUtils import io.micronaut.core.util.ArrayUtils import io.micronaut.core.util.clhm.ConcurrentLinkedHashMap -import io.micronaut.core.value.OptionalValues import io.micronaut.inject.annotation.AbstractAnnotationMetadataBuilder import io.micronaut.inject.annotation.MutableAnnotationMetadata import io.micronaut.inject.visitor.VisitorContext @@ -305,24 +304,6 @@ class KotlinAnnotationMetadataBuilder(private val symbolProcessorEnvironment: Sy return ArrayUtils.toArray(collection, valueType) } - override fun readAnnotationDefaultValues(annotationMirror: KSAnnotation): MutableMap { - val defaultArguments = annotationMirror.defaultArguments - val declaration = annotationMirror.annotationType.getClassDeclaration(visitorContext) - val allProperties = declaration.getAllProperties() - val map = mutableMapOf() - for (defaultArgument in defaultArguments) { - val name = defaultArgument.name - val value = defaultArgument.value - if (name != null && value != null) { - val dec = allProperties.find { it.simpleName.asString() == name.asString() } - if (dec != null) { - map[dec] = value - } - } - } - return map - } - override fun readAnnotationDefaultValues( annotationName: String, annotationType: KSAnnotated @@ -408,11 +389,11 @@ class KotlinAnnotationMetadataBuilder(private val symbolProcessorEnvironment: Sy return map } - override fun getAnnotationValues( + override fun getAnnotationValues( originatingElement: KSAnnotated, - member: KSAnnotated, - annotationType: Class<*> - ): OptionalValues<*> { + member: KSAnnotated?, + annotationType: Class + ): Optional> { val annotationMirrors: MutableList = (member as KSPropertyDeclaration).getter!!.annotations.toMutableList() annotationMirrors.addAll(member.annotations.toList()) val annotationName = annotationType.name @@ -425,23 +406,18 @@ class KotlinAnnotationMetadataBuilder(private val symbolProcessorEnvironment: Sy readAnnotationRawValues( originatingElement, annotationName, - member, + key, key.simpleName.asString(), value, converted ) } - return OptionalValues.of(Any::class.java, converted) + return Optional.of( + AnnotationValue.builder(annotationType).members(converted).build() + ) } } - return OptionalValues.empty() - } - - override fun getAnnotationMemberName(member: KSAnnotated): String { - if (member is KSDeclaration) { - return member.simpleName.asString() - } - TODO("Not yet implemented") + return Optional.empty() } override fun getRepeatableName(annotationMirror: KSAnnotation): String? { @@ -467,11 +443,18 @@ class KotlinAnnotationMetadataBuilder(private val symbolProcessorEnvironment: Sy return Optional.ofNullable(resolver.getClassDeclarationByName(annotationName)) } - override fun getAnnotationMember(originatingElement: KSAnnotated, member: CharSequence): KSAnnotated? { - if (originatingElement is KSAnnotation) { - return originatingElement.arguments.find { it.name == member } + override fun getAnnotationMember(annotationElement: KSAnnotated, member: CharSequence): KSAnnotated? { + if (annotationElement is KSClassDeclaration) { + return annotationElement.getAllProperties().find { it.simpleName.asString() == member } } - return null + throw IllegalStateException("Unknown annotation element: $annotationElement") + } + + override fun getAnnotationMemberName(member: KSAnnotated): String { + if (member is KSPropertyDeclaration) { + return member.simpleName.asString() + } + throw IllegalStateException("Unknown annotation member element: $member") } override fun createVisitorContext(): VisitorContext { @@ -525,12 +508,6 @@ class KotlinAnnotationMetadataBuilder(private val symbolProcessorEnvironment: Sy } } - override fun isInheritedAnnotationType(annotationType: KSAnnotated): Boolean { - return annotationType.annotations.any { - it.annotationType.resolve().declaration.qualifiedName?.asString() == Inherited::class.qualifiedName - } - } - private fun populateTypeHierarchy(element: KSClassDeclaration, hierarchy: MutableList) { element.superTypes.forEach { val t = it.resolve() diff --git a/inject/src/main/java/io/micronaut/inject/annotation/AnnotationMetadataHierarchy.java b/inject/src/main/java/io/micronaut/inject/annotation/AnnotationMetadataHierarchy.java index 205ab5d8321..4bd11ba34a1 100644 --- a/inject/src/main/java/io/micronaut/inject/annotation/AnnotationMetadataHierarchy.java +++ b/inject/src/main/java/io/micronaut/inject/annotation/AnnotationMetadataHierarchy.java @@ -23,6 +23,7 @@ import io.micronaut.core.annotation.Nullable; import io.micronaut.core.type.Argument; import io.micronaut.core.util.ArrayUtils; +import io.micronaut.core.util.CollectionUtils; import io.micronaut.core.value.OptionalValues; import java.lang.annotation.Annotation; @@ -32,7 +33,6 @@ import java.util.Collections; import java.util.HashSet; import java.util.Iterator; -import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Optional; @@ -254,21 +254,27 @@ public Optional> findAnnotation(@NonNu @Nullable private AnnotationValue mergeValue(@NonNull String annotation, @Nullable AnnotationValue existingValue, - @Nullable AnnotationValue newValud) { - if (newValud == null) { + @Nullable AnnotationValue newValue) { + if (newValue == null) { return existingValue; } if (existingValue == null) { - return newValud; + return newValue; } - final Map values = newValud.getValues(); + final Map values = newValue.getValues(); final Map existing = existingValue.getValues(); - Map newValues = new LinkedHashMap<>(values.size() + existing.size()); + Map newValues = CollectionUtils.newLinkedHashMap(values.size() + existing.size()); newValues.putAll(existing); for (Map.Entry entry : values.entrySet()) { newValues.putIfAbsent(entry.getKey(), entry.getValue()); } - return new AnnotationValue<>(annotation, newValues, AnnotationMetadataSupport.getDefaultValues(annotation)); + Map newDefaults = newValue.getDefaultValues(); + Map existingDefaults = existingValue.getDefaultValues(); + return new AnnotationValue<>( + annotation, + newValues, + existingDefaults != null ? existingDefaults : newDefaults + ); } @NonNull @@ -603,9 +609,9 @@ public boolean isFalse(Class annotation, String member) { @NonNull @Override - public Map getDefaultValues(@NonNull String annotation) { + public Map getDefaultValues(@NonNull String annotation) { for (AnnotationMetadata annotationMetadata : hierarchy) { - final Map defaultValues = annotationMetadata.getDefaultValues(annotation); + final Map defaultValues = annotationMetadata.getDefaultValues(annotation); if (!defaultValues.isEmpty()) { return defaultValues; } @@ -1066,6 +1072,11 @@ public AnnotationMetadata copyAnnotationMetadata() { ); } + @NonNull + private static AnnotationValue newAnnotationValue(String annotationType, Map values) { + return new AnnotationValue<>(annotationType, values, AnnotationMetadataSupport.getDefaultValuesOrNull(annotationType)); + } + /** * The size of the hierarchy. * @return The size diff --git a/inject/src/main/java/io/micronaut/inject/annotation/AnnotationMetadataSupport.java b/inject/src/main/java/io/micronaut/inject/annotation/AnnotationMetadataSupport.java index 3ed6925f1d0..9f3c61f8fbe 100644 --- a/inject/src/main/java/io/micronaut/inject/annotation/AnnotationMetadataSupport.java +++ b/inject/src/main/java/io/micronaut/inject/annotation/AnnotationMetadataSupport.java @@ -85,7 +85,7 @@ @Internal public final class AnnotationMetadataSupport { - private static final Map> ANNOTATION_DEFAULTS = new ConcurrentHashMap<>(20); + private static final Map> ANNOTATION_DEFAULTS = new ConcurrentHashMap<>(20); private static final Map REPEATABLE_ANNOTATIONS_CONTAINERS = new ConcurrentHashMap<>(20); private static final Map, Optional>> ANNOTATION_PROXY_CACHE = new ConcurrentHashMap<>(20); @@ -154,10 +154,20 @@ public static List, Class getDefaultValues(String annotation) { + @NonNull + public static Map getDefaultValues(String annotation) { return ANNOTATION_DEFAULTS.getOrDefault(annotation, Collections.emptyMap()); } + /** + * @param annotation The annotation + * @return The default values for the annotation + */ + @Nullable + public static Map getDefaultValuesOrNull(String annotation) { + return ANNOTATION_DEFAULTS.get(annotation); + } + /** * @param annotation The annotation * @return The repeatable annotation container. @@ -219,7 +229,7 @@ static Optional> getRegisteredAnnotationType(String * @return The default values for the annotation */ @SuppressWarnings("unchecked") - static Map getDefaultValues(Class annotation) { + static Map getDefaultValues(Class annotation) { return getDefaultValues(annotation.getName()); } @@ -239,7 +249,7 @@ static boolean hasDefaultValues(String annotation) { * @param annotation The annotation * @param defaultValues The default values */ - static void registerDefaultValues(String annotation, Map defaultValues) { + static void registerDefaultValues(String annotation, Map defaultValues) { if (StringUtils.isNotEmpty(annotation)) { ANNOTATION_DEFAULTS.put(annotation, defaultValues); } @@ -251,7 +261,7 @@ static void registerDefaultValues(String annotation, Map default * @param annotation The annotation * @param defaultValues The default values */ - static void registerDefaultValues(AnnotationClassValue annotation, Map defaultValues) { + static void registerDefaultValues(AnnotationClassValue annotation, Map defaultValues) { if (defaultValues != null) { registerDefaultValues(annotation.getName(), defaultValues); } @@ -306,7 +316,7 @@ static Optional> getProxyClass(Class T buildAnnotation(Class annotationClass, @Nullable AnnotationValue annotationValue) { Optional> proxyClass = getProxyClass(annotationClass); if (proxyClass.isPresent()) { - Map values = new HashMap<>(getDefaultValues(annotationClass)); + Map values = new HashMap<>(getDefaultValues(annotationClass)); if (annotationValue != null) { final Map annotationValues = annotationValue.getValues(); annotationValues.forEach((key, o) -> values.put(key.toString(), o)); diff --git a/inject/src/main/java/io/micronaut/inject/annotation/DefaultAnnotationMetadata.java b/inject/src/main/java/io/micronaut/inject/annotation/DefaultAnnotationMetadata.java index 35f94b1b965..a26b3dc80dc 100644 --- a/inject/src/main/java/io/micronaut/inject/annotation/DefaultAnnotationMetadata.java +++ b/inject/src/main/java/io/micronaut/inject/annotation/DefaultAnnotationMetadata.java @@ -88,6 +88,8 @@ public class DefaultAnnotationMetadata extends AbstractAnnotationMetadata implem Map annotationRepeatableContainer; @Nullable Set sourceRetentionAnnotations; + @Nullable + Map> sourceAnnotationDefaultValues; private Map annotationValuesByType = new ConcurrentHashMap<>(2); private final boolean hasPropertyExpressions; @@ -206,7 +208,7 @@ Set getSourceRetentionAnnotations() { @NonNull @Override - public Map getDefaultValues(@NonNull String annotation) { + public Map getDefaultValues(@NonNull String annotation) { ArgumentUtils.requireNonNull("annotation", annotation); return AnnotationMetadataSupport.getDefaultValues(annotation); } @@ -975,7 +977,7 @@ public Optional getValue(@NonNull String annotation, @NonNull String memb ArgumentUtils.requireNonNull("member", member); ArgumentUtils.requireNonNull("requiredType", requiredType); - Map defaultValues = getDefaultValues(annotation); + Map defaultValues = getDefaultValues(annotation); if (defaultValues.containsKey(member)) { final Object v = defaultValues.get(member); if (requiredType.isInstance(v)) { @@ -1001,7 +1003,7 @@ public Optional getValue(@NonNull String annotation, @NonNull String memb } else if (allAnnotations != null) { final Map values = allAnnotations.get(annotationTypeName); if (values != null) { - results = Collections.singletonList(new AnnotationValue<>(annotationTypeName, values)); + results = Collections.singletonList(newAnnotationValue(annotationTypeName, values)); } } @@ -1031,7 +1033,7 @@ public List> getAnnotationValuesByName } else if (allAnnotations != null) { final Map values = allAnnotations.get(annotationType); if (values != null) { - results = Collections.singletonList(new AnnotationValue<>(annotationType, values)); + results = Collections.singletonList(newAnnotationValue(annotationType, values)); } } @@ -1044,6 +1046,21 @@ public List> getAnnotationValuesByName return Collections.emptyList(); } + @NonNull + private AnnotationValue newAnnotationValue(String annotationType, Map values) { + Map defaultValues = null; + if (annotationDefaultValues != null) { + defaultValues = annotationDefaultValues.get(annotationType); + } + if (defaultValues == null && sourceAnnotationDefaultValues != null) { + defaultValues = sourceAnnotationDefaultValues.get(annotationType); + } + if (defaultValues == null) { + defaultValues = AnnotationMetadataSupport.getDefaultValuesOrNull(annotationType); + } + return new AnnotationValue<>(annotationType, values, defaultValues); + } + @Override public @NonNull List> getDeclaredAnnotationValuesByType(@NonNull Class annotationType) { if (annotationType != null) { @@ -1303,11 +1320,11 @@ Optional> getAnnotationType(@NonNull String name, @N if (allAnnotations != null && StringUtils.isNotEmpty(annotation)) { Map values = allAnnotations.get(annotation); if (values != null) { - return Optional.of(new AnnotationValue<>(annotation, values, getDefaultValues(annotation))); + return Optional.of(newAnnotationValue(annotation, values)); } else if (allStereotypes != null) { values = allStereotypes.get(annotation); if (values != null) { - return Optional.of(new AnnotationValue<>(annotation, values, getDefaultValues(annotation))); + return Optional.of(newAnnotationValue(annotation, values)); } } } @@ -1321,11 +1338,11 @@ Optional> getAnnotationType(@NonNull String name, @N if (declaredAnnotations != null && StringUtils.isNotEmpty(annotation)) { Map values = declaredAnnotations.get(annotation); if (values != null) { - return Optional.of(new AnnotationValue<>(annotation, values, getDefaultValues(annotation))); + return Optional.of(newAnnotationValue(annotation, values)); } else if (declaredStereotypes != null) { values = declaredStereotypes.get(annotation); if (values != null) { - return Optional.of(new AnnotationValue<>(annotation, values, getDefaultValues(annotation))); + return Optional.of(newAnnotationValue(annotation, values)); } } } @@ -1374,7 +1391,7 @@ public Map getValues(@NonNull String annotation) { ArgumentUtils.requireNonNull("member", member); ArgumentUtils.requireNonNull("requiredType", requiredType); // Note this method should never reference the "annotationDefaultValues" field, which is used only at compile time - Map defaultValues = getDefaultValues(annotation); + Map defaultValues = getDefaultValues(annotation); if (defaultValues.containsKey(member)) { return ConversionService.SHARED.convert(defaultValues.get(member), requiredType); } @@ -1425,6 +1442,9 @@ public DefaultAnnotationMetadata clone() { if (annotationDefaultValues != null) { cloned.annotationDefaultValues = cloneMapOfMapValue(annotationDefaultValues); } + if (sourceAnnotationDefaultValues != null) { + cloned.sourceAnnotationDefaultValues = cloneMapOfMapValue(sourceAnnotationDefaultValues); + } return cloned; } @@ -1508,15 +1528,36 @@ protected void addAnnotation(String annotation, Map values * @param values The values */ protected final void addDefaultAnnotationValues(String annotation, Map values) { - if (annotation != null) { - Map> annotationDefaults = this.annotationDefaultValues; + addDefaultAnnotationValues(annotation, values, RetentionPolicy.RUNTIME); + } + + /** + * Adds an annotation directly declared on the element and its member values, if the annotation already exists the + * data will be merged with existing values replaced. + * + * @param annotation The annotation + * @param values The values + * @param retentionPolicy The retention policy + */ + protected final void addDefaultAnnotationValues(String annotation, Map values, RetentionPolicy retentionPolicy) { + if (annotation == null) { + return; + } + Map> annotationDefaults; + if (retentionPolicy == RetentionPolicy.RUNTIME) { + annotationDefaults = this.annotationDefaultValues; if (annotationDefaults == null) { this.annotationDefaultValues = new LinkedHashMap<>(); annotationDefaults = this.annotationDefaultValues; } - - putValues(annotation, values, annotationDefaults); + } else { + annotationDefaults = this.sourceAnnotationDefaultValues; + if (annotationDefaults == null) { + this.sourceAnnotationDefaultValues = new LinkedHashMap<>(); + annotationDefaults = this.sourceAnnotationDefaultValues; + } } + putValues(annotation, values, annotationDefaults); } /** @@ -1528,7 +1569,7 @@ protected final void addDefaultAnnotationValues(String annotation, Map defaultValues) { + public static void registerAnnotationDefaults(String annotation, Map defaultValues) { AnnotationMetadataSupport.registerDefaultValues(annotation, defaultValues); } @@ -1541,7 +1582,7 @@ public static void registerAnnotationDefaults(String annotation, Map annotation, Map defaultValues) { + public static void registerAnnotationDefaults(AnnotationClassValue annotation, Map defaultValues) { AnnotationMetadataSupport.registerDefaultValues(annotation, defaultValues); } @@ -2165,6 +2206,14 @@ protected void addAnnotationMetadata(DefaultAnnotationMetadata annotationMetadat annotationDefaultValues.putAll(annotationMetadata.annotationDefaultValues); } } + if (annotationMetadata.sourceAnnotationDefaultValues != null) { + if (sourceAnnotationDefaultValues == null) { + sourceAnnotationDefaultValues = new LinkedHashMap<>(annotationMetadata.sourceAnnotationDefaultValues); + } else { + // No need to merge values + sourceAnnotationDefaultValues.putAll(annotationMetadata.sourceAnnotationDefaultValues); + } + } } /** @@ -2181,24 +2230,22 @@ public static void contributeDefaults(AnnotationMetadata target, AnnotationMetad if (source instanceof AnnotationMetadataHierarchy) { source = ((AnnotationMetadataHierarchy) source).merge(); } - if (target instanceof DefaultAnnotationMetadata && source instanceof DefaultAnnotationMetadata) { - DefaultAnnotationMetadata damTarget = (DefaultAnnotationMetadata) target; - DefaultAnnotationMetadata damSource = (DefaultAnnotationMetadata) source; + if (target instanceof DefaultAnnotationMetadata damTarget && source instanceof DefaultAnnotationMetadata damSource) { final Map> existingDefaults = damTarget.annotationDefaultValues; + final Map> additionalDefaults = damSource.annotationDefaultValues; if (existingDefaults != null) { - final Map> additionalDefaults = damSource.annotationDefaultValues; if (additionalDefaults != null) { existingDefaults.putAll( additionalDefaults ); } } else { - final Map> additionalDefaults = damSource.annotationDefaultValues; if (additionalDefaults != null) { additionalDefaults.forEach(damTarget::addDefaultAnnotationValues); } } } + // We don't need to contribute the default source annotation contributeRepeatable(target, source); } @@ -2290,7 +2337,7 @@ private void removeAnnotationsIf(@NonNull Predicate { final String annotationName = entry.getKey(); - if (predicate.test(new AnnotationValue<>(annotationName, entry.getValue()))) { + if (predicate.test(newAnnotationValue(annotationName, entry.getValue()))) { removeFromStereotypes(annotationName, annotations); return true; } diff --git a/inject/src/main/java/io/micronaut/inject/annotation/MutableAnnotationMetadata.java b/inject/src/main/java/io/micronaut/inject/annotation/MutableAnnotationMetadata.java index 455b1c843fa..d72a8911ad0 100644 --- a/inject/src/main/java/io/micronaut/inject/annotation/MutableAnnotationMetadata.java +++ b/inject/src/main/java/io/micronaut/inject/annotation/MutableAnnotationMetadata.java @@ -122,8 +122,8 @@ public MutableAnnotationMetadata clone() { @NonNull @Override - public Map getDefaultValues(@NonNull String annotation) { - Map values = super.getDefaultValues(annotation); + public Map getDefaultValues(@NonNull String annotation) { + Map values = super.getDefaultValues(annotation); if (values.isEmpty() && annotationDefaultValues != null) { final Map compileTimeDefaults = annotationDefaultValues.get(annotation); if (compileTimeDefaults != null && !compileTimeDefaults.isEmpty()) { diff --git a/validation/src/main/java/io/micronaut/validation/validator/DefaultValidator.java b/validation/src/main/java/io/micronaut/validation/validator/DefaultValidator.java index e230e0cf732..88dbecffb8b 100644 --- a/validation/src/main/java/io/micronaut/validation/validator/DefaultValidator.java +++ b/validation/src/main/java/io/micronaut/validation/validator/DefaultValidator.java @@ -1576,9 +1576,9 @@ private Map newConstraintVariables(AnnotationValue annotationVal variables.put(entry.getKey().toString(), entry.getValue()); } variables.put("validatedValue", propertyValue); - final Map defaultValues = annotationMetadata.getDefaultValues(annotationValue.getAnnotationName()); - for (Map.Entry entry : defaultValues.entrySet()) { - final String n = entry.getKey(); + final Map defaultValues = annotationMetadata.getDefaultValues(annotationValue.getAnnotationName()); + for (Map.Entry entry : defaultValues.entrySet()) { + final String n = entry.getKey().toString(); if (!variables.containsKey(n)) { final Object v = entry.getValue(); if (v != null) { From a90cfdc81892821c940ab66f0e199197e122eea8 Mon Sep 17 00:00:00 2001 From: Denis Stepanov Date: Wed, 25 Jan 2023 13:59:01 +0100 Subject: [PATCH 419/743] Move mutable operations out of the default annotation metadata. Cleanup. (#8664) --- .../AbstractAnnotationMetadataBuilder.java | 151 +- .../annotation/AnnotationMetadataWriter.java | 25 +- .../inject/ast/ReflectParameterElement.java | 5 +- .../visitor/BeanIntrospectionWriter.java | 17 +- .../visitor/util/VisitorContextUtils.java | 11 +- .../AbstractAnnotationMetadataWriter.java | 19 +- .../writer/AbstractClassFileWriter.java | 5 +- .../inject/writer/BeanDefinitionWriter.java | 28 +- .../inject/writer/ExecutableMethodWriter.java | 13 +- .../ExecutableMethodsDefinitionWriter.java | 15 +- .../AnnotationMetadataWriterSpec.groovy | 4 +- .../AnnotationMetadataHierarchy.java | 91 +- .../annotation/DefaultAnnotationMetadata.java | 1456 ++++------------- .../annotation/MutableAnnotationMetadata.java | 988 ++++++++++- .../annotation/AnnotationMetadataSpec.groovy | 6 +- 15 files changed, 1351 insertions(+), 1483 deletions(-) diff --git a/core-processor/src/main/java/io/micronaut/inject/annotation/AbstractAnnotationMetadataBuilder.java b/core-processor/src/main/java/io/micronaut/inject/annotation/AbstractAnnotationMetadataBuilder.java index 7a45d4504ce..4e006c9cdbc 100644 --- a/core-processor/src/main/java/io/micronaut/inject/annotation/AbstractAnnotationMetadataBuilder.java +++ b/core-processor/src/main/java/io/micronaut/inject/annotation/AbstractAnnotationMetadataBuilder.java @@ -54,6 +54,7 @@ import java.util.Objects; import java.util.Optional; import java.util.Set; +import java.util.function.Consumer; import java.util.function.Predicate; import java.util.function.Supplier; import java.util.stream.Stream; @@ -75,22 +76,21 @@ public abstract class AbstractAnnotationMetadataBuilder { */ private static final Map DEPRECATED_ANNOTATION_NAMES = Collections.emptyMap(); private static final Map>> ANNOTATION_MAPPERS = new HashMap<>(10); - private static final Map>> ANNOTATION_TRANSFORMERS = new HashMap<>(5); + private static final Map>> ANNOTATION_TRANSFORMERS = new HashMap<>(5); private static final Map> ANNOTATION_REMAPPERS = new HashMap<>(5); private static final Map, CachedAnnotationMetadata> MUTATED_ANNOTATION_METADATA = new HashMap<>(100); private static final Map> NON_BINDING_CACHE = new HashMap<>(50); private static final Map> ANNOTATION_DEFAULTS = new HashMap<>(20); - private static final String MSG_UNRECOGNIZED_ANNOTATION_METADATA = "Unrecognized annotation metadata: "; static { - for (AnnotationMapper mapper : SoftServiceLoader.load(AnnotationMapper.class, AbstractAnnotationMetadataBuilder.class.getClassLoader()) + for (AnnotationMapper mapper : SoftServiceLoader.load(AnnotationMapper.class, AbstractAnnotationMetadataBuilder.class.getClassLoader()) .disableFork().collectAll()) { try { String name = null; - if (mapper instanceof TypedAnnotationMapper) { - name = ((TypedAnnotationMapper) mapper).annotationType().getName(); - } else if (mapper instanceof NamedAnnotationMapper) { - name = ((NamedAnnotationMapper) mapper).getName(); + if (mapper instanceof TypedAnnotationMapper typedAnnotationMapper) { + name = typedAnnotationMapper.annotationType().getName(); + } else if (mapper instanceof NamedAnnotationMapper namedAnnotationMapper) { + name = namedAnnotationMapper.getName(); } if (StringUtils.isNotEmpty(name)) { ANNOTATION_MAPPERS.computeIfAbsent(name, s -> new ArrayList<>(2)).add(mapper); @@ -100,14 +100,14 @@ public abstract class AbstractAnnotationMetadataBuilder { } } - for (AnnotationTransformer transformer : SoftServiceLoader.load(AnnotationTransformer.class, AbstractAnnotationMetadataBuilder.class.getClassLoader()) + for (AnnotationTransformer transformer : SoftServiceLoader.load(AnnotationTransformer.class, AbstractAnnotationMetadataBuilder.class.getClassLoader()) .disableFork().collectAll()) { try { String name = null; - if (transformer instanceof TypedAnnotationTransformer) { - name = ((TypedAnnotationTransformer) transformer).annotationType().getName(); - } else if (transformer instanceof NamedAnnotationTransformer) { - name = ((NamedAnnotationTransformer) transformer).getName(); + if (transformer instanceof TypedAnnotationTransformer typedAnnotationTransformer) { + name = typedAnnotationTransformer.annotationType().getName(); + } else if (transformer instanceof NamedAnnotationTransformer namedAnnotationTransformer) { + name = namedAnnotationTransformer.getName(); } if (StringUtils.isNotEmpty(name)) { ANNOTATION_TRANSFORMERS.computeIfAbsent(name, s -> new ArrayList<>(2)).add(transformer); @@ -1519,33 +1519,17 @@ public static Set getMappedAnnotationPackages() { * @param The annotation type * @return The mutated metadata */ - public AnnotationMetadata annotate(AnnotationMetadata annotationMetadata, - AnnotationValue annotationValue) { - final boolean isReference = annotationMetadata instanceof AnnotationMetadataReference; - boolean isReferenceOrEmpty = annotationMetadata == AnnotationMetadata.EMPTY_METADATA || isReference; - if (annotationMetadata instanceof MutableAnnotationMetadata || isReferenceOrEmpty) { - final MutableAnnotationMetadata mutableAnnotationMetadata = isReferenceOrEmpty - ? new MutableAnnotationMetadata() - : (MutableAnnotationMetadata) annotationMetadata; - + @NonNull + public AnnotationMetadata annotate(@NonNull AnnotationMetadata annotationMetadata, + @NonNull AnnotationValue annotationValue) { + return modify(annotationMetadata, metadata -> { addAnnotations( - mutableAnnotationMetadata, + metadata, Stream.of(toProcessedAnnotation(annotationValue)), true, Collections.emptyList() ); - - if (isReference) { - AnnotationMetadataReference ref = (AnnotationMetadataReference) annotationMetadata; - return new AnnotationMetadataHierarchy(ref, mutableAnnotationMetadata); - } else { - return mutableAnnotationMetadata; - } - } else if (annotationMetadata instanceof AnnotationMetadataHierarchy hierarchy) { - AnnotationMetadata declaredMetadata = annotate(hierarchy.getDeclaredMetadata(), annotationValue); - return hierarchy.createSibling(declaredMetadata); - } - throw new IllegalStateException(MSG_UNRECOGNIZED_ANNOTATION_METADATA + annotationMetadata); + }); } /** @@ -1556,38 +1540,22 @@ public AnnotationMetadata annotate(AnnotationMetadata an * @return The updated metadata * @since 3.0.0 */ - public AnnotationMetadata removeAnnotation(AnnotationMetadata annotationMetadata, String annotationType) { - if (annotationMetadata.isEmpty()) { - return annotationMetadata; - } - // we only care if the metadata is an hierarchy or default mutable - final boolean isHierarchy = annotationMetadata instanceof AnnotationMetadataHierarchy; - AnnotationMetadata declaredMetadata = annotationMetadata; - if (isHierarchy) { - declaredMetadata = annotationMetadata.getDeclaredMetadata(); - } - // if it is anything else other than DefaultAnnotationMetadata here it is probably empty - // in which case nothing needs to be done - if (declaredMetadata instanceof DefaultAnnotationMetadata defaultMetadata) { + @NonNull + public AnnotationMetadata removeAnnotation(@NonNull AnnotationMetadata annotationMetadata, + @NonNull String annotationType) { + return modify(annotationMetadata, metadata -> { T annotationMirror = getAnnotationMirror(annotationType).orElse(null); if (annotationMirror != null) { String repeatableName = getRepeatableNameForType(annotationMirror); if (repeatableName != null) { - defaultMetadata.removeAnnotation(repeatableName); + metadata.removeAnnotation(repeatableName); } else { - defaultMetadata.removeAnnotation(annotationType); + metadata.removeAnnotation(annotationType); } } else { - defaultMetadata.removeAnnotation(annotationType); + metadata.removeAnnotation(annotationType); } - - if (isHierarchy) { - return ((AnnotationMetadataHierarchy) annotationMetadata).createSibling(declaredMetadata); - } - return declaredMetadata; - } else { - throw new IllegalStateException(MSG_UNRECOGNIZED_ANNOTATION_METADATA + annotationMetadata); - } + }); } /** @@ -1598,36 +1566,22 @@ public AnnotationMetadata removeAnnotation(AnnotationMetadata annotationMetadata * @return The updated metadata * @since 3.0.0 */ - public AnnotationMetadata removeStereotype(AnnotationMetadata annotationMetadata, String annotationType) { - if (annotationMetadata.isEmpty()) { - return annotationMetadata; - } - // we only care if the metadata is an hierarchy or default mutable - final boolean isHierarchy = annotationMetadata instanceof AnnotationMetadataHierarchy; - AnnotationMetadata declaredMetadata = annotationMetadata; - if (isHierarchy) { - declaredMetadata = annotationMetadata.getDeclaredMetadata(); - } - // if it is anything else other than DefaultAnnotationMetadata here it is probably empty - // in which case nothing needs to be done - if (declaredMetadata instanceof DefaultAnnotationMetadata defaultMetadata) { + @NonNull + public AnnotationMetadata removeStereotype(@NonNull AnnotationMetadata annotationMetadata, + @NonNull String annotationType) { + return modify(annotationMetadata, metadata -> { T annotationMirror = getAnnotationMirror(annotationType).orElse(null); if (annotationMirror != null) { String repeatableName = getRepeatableNameForType(annotationMirror); if (repeatableName != null) { - defaultMetadata.removeStereotype(repeatableName); + metadata.removeStereotype(repeatableName); } else { - defaultMetadata.removeStereotype(annotationType); + metadata.removeStereotype(annotationType); } } else { - defaultMetadata.removeStereotype(annotationType); + metadata.removeStereotype(annotationType); } - if (isHierarchy) { - return ((AnnotationMetadataHierarchy) annotationMetadata).createSibling(declaredMetadata); - } - return declaredMetadata; - } - throw new IllegalStateException(MSG_UNRECOGNIZED_ANNOTATION_METADATA + annotationMetadata); + }); } /** @@ -1638,28 +1592,33 @@ public AnnotationMetadata removeStereotype(AnnotationMetadata annotationMetadata * @param The annotation type * @return The potentially modified metadata */ - public @NonNull AnnotationMetadata removeAnnotationIf( - @NonNull AnnotationMetadata annotationMetadata, - @NonNull Predicate> predicate) { - if (annotationMetadata.isEmpty()) { - return annotationMetadata; - } - // we only care if the metadata is an hierarchy or default mutable + @NonNull + public AnnotationMetadata removeAnnotationIf(@NonNull AnnotationMetadata annotationMetadata, + @NonNull Predicate> predicate) { + return modify(annotationMetadata, metadata -> metadata.removeAnnotationIf(predicate)); + } + + private AnnotationMetadata modify(AnnotationMetadata annotationMetadata, Consumer consumer) { final boolean isHierarchy = annotationMetadata instanceof AnnotationMetadataHierarchy; AnnotationMetadata declaredMetadata = annotationMetadata; if (isHierarchy) { declaredMetadata = annotationMetadata.getDeclaredMetadata(); } - // if it is anything else other than DefaultAnnotationMetadata here it is probably empty - // in which case nothing needs to be done - if (declaredMetadata instanceof DefaultAnnotationMetadata defaultMetadata) { - defaultMetadata.removeAnnotationIf(predicate); - if (isHierarchy) { - return ((AnnotationMetadataHierarchy) annotationMetadata).createSibling(declaredMetadata); - } - return declaredMetadata; + MutableAnnotationMetadata mutableAnnotationMetadata; + if (declaredMetadata == AnnotationMetadata.EMPTY_METADATA) { + mutableAnnotationMetadata = new MutableAnnotationMetadata(); + } else if (declaredMetadata instanceof MutableAnnotationMetadata mutable) { + mutableAnnotationMetadata = mutable; + } else if (declaredMetadata instanceof DefaultAnnotationMetadata) { + mutableAnnotationMetadata = MutableAnnotationMetadata.of(declaredMetadata); + } else { + throw new IllegalStateException("Unrecognized annotation metadata: " + annotationMetadata); + } + consumer.accept(mutableAnnotationMetadata); + if (isHierarchy) { + return ((AnnotationMetadataHierarchy) annotationMetadata).createSibling(mutableAnnotationMetadata); } - throw new IllegalStateException(MSG_UNRECOGNIZED_ANNOTATION_METADATA + annotationMetadata); + return mutableAnnotationMetadata; } /** diff --git a/core-processor/src/main/java/io/micronaut/inject/annotation/AnnotationMetadataWriter.java b/core-processor/src/main/java/io/micronaut/inject/annotation/AnnotationMetadataWriter.java index c7f06d46d2e..e97af9966e8 100644 --- a/core-processor/src/main/java/io/micronaut/inject/annotation/AnnotationMetadataWriter.java +++ b/core-processor/src/main/java/io/micronaut/inject/annotation/AnnotationMetadataWriter.java @@ -186,9 +186,8 @@ public AnnotationMetadataWriter( } if (annotationMetadata instanceof DefaultAnnotationMetadata) { this.annotationMetadata = annotationMetadata; - } else if (annotationMetadata instanceof AnnotationMetadataHierarchy) { - final AnnotationMetadataHierarchy hierarchy = (AnnotationMetadataHierarchy) annotationMetadata; - this.annotationMetadata = hierarchy.getDeclaredMetadata(); + } else if (annotationMetadata instanceof AnnotationMetadataHierarchy annotationMetadataHierarchy) { + this.annotationMetadata = annotationMetadataHierarchy.getDeclaredMetadata(); } else { throw new ClassGenerationException("Compile time metadata required to generate class: " + className); } @@ -251,7 +250,7 @@ public void writeTo(OutputStream outputStream) { } /** - * Writes out the byte code necessary to instantiate the given {@link DefaultAnnotationMetadata}. + * Writes out the byte code necessary to instantiate the given {@link MutableAnnotationMetadata}. * * @param owningType The owning type * @param declaringClassWriter The declaring class writer @@ -262,7 +261,7 @@ public void writeTo(OutputStream outputStream) { */ @Internal @UsedByGeneratedCode - public static void instantiateNewMetadata(Type owningType, ClassWriter declaringClassWriter, GeneratorAdapter generatorAdapter, DefaultAnnotationMetadata annotationMetadata, Map defaultsStorage, Map loadTypeMethods) { + public static void instantiateNewMetadata(Type owningType, ClassWriter declaringClassWriter, GeneratorAdapter generatorAdapter, MutableAnnotationMetadata annotationMetadata, Map defaultsStorage, Map loadTypeMethods) { instantiateInternal(owningType, declaringClassWriter, generatorAdapter, annotationMetadata, true, defaultsStorage, loadTypeMethods); } @@ -338,12 +337,12 @@ private static void pushNewAnnotationMetadataOrReference( annotationMetadata = annotationMetadata.getTargetAnnotationMetadata(); if (annotationMetadata.isEmpty()) { generatorAdapter.getStatic(Type.getType(AnnotationMetadata.class), "EMPTY_METADATA", Type.getType(AnnotationMetadata.class)); - } else if (annotationMetadata instanceof DefaultAnnotationMetadata) { + } else if (annotationMetadata instanceof MutableAnnotationMetadata mutableAnnotationMetadata) { instantiateNewMetadata( owningType, classWriter, generatorAdapter, - (DefaultAnnotationMetadata) annotationMetadata, + mutableAnnotationMetadata, defaultsStorage, loadTypeMethods ); @@ -355,7 +354,7 @@ private static void pushNewAnnotationMetadataOrReference( } /** - * Writes out the byte code necessary to instantiate the given {@link DefaultAnnotationMetadata}. + * Writes out the byte code necessary to instantiate the given {@link MutableAnnotationMetadata}. * * @param annotationMetadata The annotation metadata * @param classWriter The class writer @@ -364,7 +363,7 @@ private static void pushNewAnnotationMetadataOrReference( * @param loadTypeMethods The generated load type methods */ @Internal - public static void writeAnnotationDefaults(DefaultAnnotationMetadata annotationMetadata, ClassWriter classWriter, Type owningType, Map defaultsStorage, Map loadTypeMethods) { + public static void writeAnnotationDefaults(MutableAnnotationMetadata annotationMetadata, ClassWriter classWriter, Type owningType, Map defaultsStorage, Map loadTypeMethods) { final Map> annotationDefaultValues = annotationMetadata.annotationDefaultValues; if (CollectionUtils.isNotEmpty(annotationDefaultValues)) { @@ -394,10 +393,10 @@ public static void writeAnnotationDefaults( Type owningType, ClassWriter classWriter, GeneratorAdapter staticInit, - DefaultAnnotationMetadata annotationMetadata, + MutableAnnotationMetadata annotationMetadata, Map defaultsStorage, Map loadTypeMethods) { - final Map> annotationDefaultValues = annotationMetadata.annotationDefaultValues; + final Map> annotationDefaultValues = annotationMetadata.annotationDefaultValues; if (CollectionUtils.isNotEmpty(annotationDefaultValues)) { for (Map.Entry> entry : annotationDefaultValues.entrySet()) { final Map annotationValues = entry.getValue(); @@ -453,7 +452,7 @@ private static void instantiateInternal( Type owningType, ClassWriter declaringClassWriter, GeneratorAdapter generatorAdapter, - DefaultAnnotationMetadata annotationMetadata, + MutableAnnotationMetadata annotationMetadata, boolean isNew, Map defaultsStorage, Map loadTypeMethods) { @@ -494,7 +493,7 @@ private ClassWriter generateClassBytes() { startClass(classWriter, getInternalName(className), TYPE_DEFAULT_ANNOTATION_METADATA); GeneratorAdapter constructor = startConstructor(classWriter); - DefaultAnnotationMetadata annotationMetadata = (DefaultAnnotationMetadata) this.annotationMetadata; + MutableAnnotationMetadata annotationMetadata = (MutableAnnotationMetadata) this.annotationMetadata; Map defaultsStorage = new HashMap<>(3); final HashMap loadTypeMethods = new HashMap<>(5); diff --git a/core-processor/src/main/java/io/micronaut/inject/ast/ReflectParameterElement.java b/core-processor/src/main/java/io/micronaut/inject/ast/ReflectParameterElement.java index 7661bd34a02..29a341c1b98 100644 --- a/core-processor/src/main/java/io/micronaut/inject/ast/ReflectParameterElement.java +++ b/core-processor/src/main/java/io/micronaut/inject/ast/ReflectParameterElement.java @@ -20,7 +20,6 @@ import io.micronaut.core.annotation.AnnotationValueBuilder; import io.micronaut.core.annotation.Internal; import io.micronaut.core.annotation.NonNull; -import io.micronaut.inject.annotation.DefaultAnnotationMetadata; import io.micronaut.inject.annotation.MutableAnnotationMetadata; import java.lang.annotation.Annotation; @@ -36,7 +35,7 @@ class ReflectParameterElement implements ParameterElement { private final ClassElement classElement; private final String name; - private AnnotationMetadata annotationMetadata = AnnotationMetadata.EMPTY_METADATA; + private MutableAnnotationMetadata annotationMetadata = new MutableAnnotationMetadata(); ReflectParameterElement(ClassElement classElement, String name) { this.classElement = classElement; @@ -104,7 +103,7 @@ public Element annotate(@NonNull String annotationType, @ } else { AnnotationValueBuilder builder = AnnotationValue.builder(annotationType); consumer.accept(builder); - this.annotationMetadata = DefaultAnnotationMetadata.mutateMember(annotationMetadata, annotationType, builder.build().getValues()); + this.annotationMetadata = MutableAnnotationMetadata.mutateMember(annotationMetadata, annotationType, builder.build().getValues()); } return this; } diff --git a/core-processor/src/main/java/io/micronaut/inject/beans/visitor/BeanIntrospectionWriter.java b/core-processor/src/main/java/io/micronaut/inject/beans/visitor/BeanIntrospectionWriter.java index b45f3d3b1ca..47aa39be5c9 100644 --- a/core-processor/src/main/java/io/micronaut/inject/beans/visitor/BeanIntrospectionWriter.java +++ b/core-processor/src/main/java/io/micronaut/inject/beans/visitor/BeanIntrospectionWriter.java @@ -30,7 +30,7 @@ import io.micronaut.inject.annotation.AnnotationMetadataHierarchy; import io.micronaut.inject.annotation.AnnotationMetadataReference; import io.micronaut.inject.annotation.AnnotationMetadataWriter; -import io.micronaut.inject.annotation.DefaultAnnotationMetadata; +import io.micronaut.inject.annotation.MutableAnnotationMetadata; import io.micronaut.inject.ast.ClassElement; import io.micronaut.inject.ast.ConstructorElement; import io.micronaut.inject.ast.ElementQuery; @@ -202,7 +202,7 @@ void visitProperty( @Nullable AnnotationMetadata annotationMetadata, @Nullable Map typeArguments) { - DefaultAnnotationMetadata.contributeDefaults( + MutableAnnotationMetadata.contributeDefaults( this.annotationMetadata, annotationMetadata ); @@ -919,24 +919,23 @@ private void pushAnnotationMetadata(ClassWriter classWriter, GeneratorAdapter st annotationMetadata = annotationMetadata.getTargetAnnotationMetadata(); if (annotationMetadata.isEmpty()) { staticInit.push((String) null); - } else if (annotationMetadata instanceof AnnotationMetadataReference) { - AnnotationMetadataReference reference = (AnnotationMetadataReference) annotationMetadata; - String className = reference.getClassName(); + } else if (annotationMetadata instanceof AnnotationMetadataReference annotationMetadataReference) { + String className = annotationMetadataReference.getClassName(); staticInit.getStatic(getTypeReferenceForName(className), FIELD_ANNOTATION_METADATA, Type.getType(AnnotationMetadata.class)); - } else if (annotationMetadata instanceof AnnotationMetadataHierarchy) { + } else if (annotationMetadata instanceof AnnotationMetadataHierarchy annotationMetadataHierarchy) { AnnotationMetadataWriter.instantiateNewMetadataHierarchy( introspectionType, classWriter, staticInit, - (AnnotationMetadataHierarchy) annotationMetadata, + annotationMetadataHierarchy, defaults, loadTypeMethods); - } else if (annotationMetadata instanceof DefaultAnnotationMetadata) { + } else if (annotationMetadata instanceof MutableAnnotationMetadata mutableAnnotationMetadata) { AnnotationMetadataWriter.instantiateNewMetadata( introspectionType, classWriter, staticInit, - (DefaultAnnotationMetadata) annotationMetadata, + mutableAnnotationMetadata, defaults, loadTypeMethods); } else { diff --git a/core-processor/src/main/java/io/micronaut/inject/visitor/util/VisitorContextUtils.java b/core-processor/src/main/java/io/micronaut/inject/visitor/util/VisitorContextUtils.java index 3688b376e49..5fef1e840c5 100644 --- a/core-processor/src/main/java/io/micronaut/inject/visitor/util/VisitorContextUtils.java +++ b/core-processor/src/main/java/io/micronaut/inject/visitor/util/VisitorContextUtils.java @@ -18,12 +18,17 @@ import io.micronaut.context.env.CachedEnvironment; import io.micronaut.core.annotation.AnnotationMetadata; import io.micronaut.core.annotation.Internal; -import io.micronaut.inject.annotation.DefaultAnnotationMetadata; +import io.micronaut.inject.annotation.MutableAnnotationMetadata; import io.micronaut.inject.ast.ClassElement; import io.micronaut.inject.visitor.VisitorContext; import javax.annotation.processing.ProcessingEnvironment; -import java.util.*; +import java.util.AbstractMap; +import java.util.Collections; +import java.util.HashSet; +import java.util.Map; +import java.util.Optional; +import java.util.Set; import java.util.stream.Collectors; /** @@ -88,7 +93,7 @@ public static void contributeRepeatable(AnnotationMetadata target, ClassElement private static void contributeRepeatable(AnnotationMetadata target, ClassElement classElement, Set alreadySeen) { alreadySeen.add(classElement); - DefaultAnnotationMetadata.contributeRepeatable(target, classElement.getAnnotationMetadata()); + MutableAnnotationMetadata.contributeRepeatable(target, classElement.getAnnotationMetadata()); for (ClassElement element : classElement.getTypeArguments().values()) { if (alreadySeen.contains(classElement)) { continue; diff --git a/core-processor/src/main/java/io/micronaut/inject/writer/AbstractAnnotationMetadataWriter.java b/core-processor/src/main/java/io/micronaut/inject/writer/AbstractAnnotationMetadataWriter.java index fbbe7b01bdd..909d912e9f8 100644 --- a/core-processor/src/main/java/io/micronaut/inject/writer/AbstractAnnotationMetadataWriter.java +++ b/core-processor/src/main/java/io/micronaut/inject/writer/AbstractAnnotationMetadataWriter.java @@ -21,7 +21,7 @@ import io.micronaut.inject.annotation.AnnotationMetadataHierarchy; import io.micronaut.inject.annotation.AnnotationMetadataReference; import io.micronaut.inject.annotation.AnnotationMetadataWriter; -import io.micronaut.inject.annotation.DefaultAnnotationMetadata; +import io.micronaut.inject.annotation.MutableAnnotationMetadata; import io.micronaut.inject.ast.Element; import org.objectweb.asm.ClassWriter; import org.objectweb.asm.Label; @@ -147,16 +147,15 @@ protected void writeAnnotationMetadataStaticInitializer(ClassWriter classWriter, if (annotationMetadata.isEmpty()) { staticInit.getStatic(Type.getType(AnnotationMetadata.class), FIELD_EMPTY_METADATA, Type.getType(AnnotationMetadata.class)); } else { - if (annotationMetadata instanceof AnnotationMetadataHierarchy) { - annotationMetadata = ((AnnotationMetadataHierarchy) annotationMetadata).merge(); + if (annotationMetadata instanceof AnnotationMetadataHierarchy annotationMetadataHierarchy) { + annotationMetadata = annotationMetadataHierarchy.merge(); } - if (annotationMetadata instanceof DefaultAnnotationMetadata) { - DefaultAnnotationMetadata dam = (DefaultAnnotationMetadata) annotationMetadata; + if (annotationMetadata instanceof MutableAnnotationMetadata mutableAnnotationMetadata) { AnnotationMetadataWriter.writeAnnotationDefaults( targetClassType, classWriter, staticInit, - dam, + mutableAnnotationMetadata, defaults, loadTypeMethods ); @@ -185,21 +184,21 @@ protected void initializeAnnotationMetadata(GeneratorAdapter staticInit, ClassWr AnnotationMetadata annotationMetadata = this.annotationMetadata.getTargetAnnotationMetadata(); if (annotationMetadata.isEmpty()) { staticInit.getStatic(Type.getType(AnnotationMetadata.class), FIELD_EMPTY_METADATA, Type.getType(AnnotationMetadata.class)); - } else if (annotationMetadata instanceof DefaultAnnotationMetadata) { + } else if (annotationMetadata instanceof MutableAnnotationMetadata mutableAnnotationMetadata) { AnnotationMetadataWriter.instantiateNewMetadata( targetClassType, classWriter, staticInit, - (DefaultAnnotationMetadata) annotationMetadata, + mutableAnnotationMetadata, defaults, loadTypeMethods ); - } else if (annotationMetadata instanceof AnnotationMetadataHierarchy) { + } else if (annotationMetadata instanceof AnnotationMetadataHierarchy annotationMetadataHierarchy) { AnnotationMetadataWriter.instantiateNewMetadataHierarchy( targetClassType, classWriter, staticInit, - (AnnotationMetadataHierarchy) annotationMetadata, + annotationMetadataHierarchy, defaults, loadTypeMethods ); diff --git a/core-processor/src/main/java/io/micronaut/inject/writer/AbstractClassFileWriter.java b/core-processor/src/main/java/io/micronaut/inject/writer/AbstractClassFileWriter.java index 6f36d03ecb7..63162d5b769 100644 --- a/core-processor/src/main/java/io/micronaut/inject/writer/AbstractClassFileWriter.java +++ b/core-processor/src/main/java/io/micronaut/inject/writer/AbstractClassFileWriter.java @@ -28,7 +28,6 @@ import io.micronaut.core.util.CollectionUtils; import io.micronaut.inject.annotation.AnnotationMetadataReference; import io.micronaut.inject.annotation.AnnotationMetadataWriter; -import io.micronaut.inject.annotation.DefaultAnnotationMetadata; import io.micronaut.inject.annotation.MutableAnnotationMetadata; import io.micronaut.inject.ast.ClassElement; import io.micronaut.inject.ast.Element; @@ -407,7 +406,7 @@ protected static void buildArgumentWithGenerics( owningType, owningClassWriter, generatorAdapter, - (DefaultAnnotationMetadata) annotationMetadata, + (MutableAnnotationMetadata) annotationMetadata, defaults, loadTypeMethods ); @@ -631,7 +630,7 @@ protected static void pushCreateArgument( owningType, declaringClassWriter, generatorAdapter, - (DefaultAnnotationMetadata) annotationMetadata, + (MutableAnnotationMetadata) annotationMetadata, defaults, loadTypeMethods ); diff --git a/core-processor/src/main/java/io/micronaut/inject/writer/BeanDefinitionWriter.java b/core-processor/src/main/java/io/micronaut/inject/writer/BeanDefinitionWriter.java index 9f3cf001254..0105936fd91 100644 --- a/core-processor/src/main/java/io/micronaut/inject/writer/BeanDefinitionWriter.java +++ b/core-processor/src/main/java/io/micronaut/inject/writer/BeanDefinitionWriter.java @@ -75,7 +75,6 @@ import io.micronaut.inject.ValidatedBeanDefinition; import io.micronaut.inject.annotation.AnnotationMetadataHierarchy; import io.micronaut.inject.annotation.AnnotationMetadataWriter; -import io.micronaut.inject.annotation.DefaultAnnotationMetadata; import io.micronaut.inject.annotation.MutableAnnotationMetadata; import io.micronaut.inject.ast.ClassElement; import io.micronaut.inject.ast.ConstructorElement; @@ -1553,12 +1552,12 @@ public int visitExecutableMethod(TypedElement declaringType, String interceptedProxyClassName, String interceptedProxyBridgeMethodName) { - DefaultAnnotationMetadata.contributeDefaults( + MutableAnnotationMetadata.contributeDefaults( this.annotationMetadata, methodElement.getAnnotationMetadata() ); for (ParameterElement parameterElement : methodElement.getSuspendParameters()) { - DefaultAnnotationMetadata.contributeDefaults( + MutableAnnotationMetadata.contributeDefaults( this.annotationMetadata, parameterElement.getAnnotationMetadata() ); @@ -2148,7 +2147,7 @@ private void visitFieldInjectionPointInternal( boolean requiresGenericType) { autoApplyNamedIfPresent(fieldElement, annotationMetadata); - DefaultAnnotationMetadata.contributeDefaults(this.annotationMetadata, annotationMetadata); + MutableAnnotationMetadata.contributeDefaults(this.annotationMetadata, annotationMetadata); VisitorContextUtils.contributeRepeatable(this.annotationMetadata, fieldElement.getGenericField()); GeneratorAdapter injectMethodVisitor = this.injectMethodVisitor; @@ -2401,14 +2400,14 @@ private void visitMethodInjectionPointInternal(MethodVisitData methodVisitData, final String methodName = methodElement.getName(); final boolean requiresReflection = methodVisitData.requiresReflection; final ClassElement returnType = methodElement.getReturnType(); - DefaultAnnotationMetadata.contributeDefaults(this.annotationMetadata, annotationMetadata); + MutableAnnotationMetadata.contributeDefaults(this.annotationMetadata, annotationMetadata); VisitorContextUtils.contributeRepeatable(this.annotationMetadata, returnType); boolean hasArguments = methodElement.hasParameters(); int argCount = hasArguments ? argumentTypes.size() : 0; Type declaringTypeRef = JavaModelUtils.getTypeReference(declaringType); boolean hasInjectScope = false; for (ParameterElement value : argumentTypes) { - DefaultAnnotationMetadata.contributeDefaults(this.annotationMetadata, value.getAnnotationMetadata()); + MutableAnnotationMetadata.contributeDefaults(this.annotationMetadata, value.getAnnotationMetadata()); VisitorContextUtils.contributeRepeatable(this.annotationMetadata, value.getGenericType()); if (value.hasDeclaredAnnotation(InjectScope.class)) { hasInjectScope = true; @@ -2766,8 +2765,7 @@ private void pushInvokeGetPropertyPlaceholderValueForSetter(GeneratorAdapter inj } private void removeAnnotations(AnnotationMetadata annotationMetadata, String... annotationNames) { - if (annotationMetadata instanceof MutableAnnotationMetadata) { - MutableAnnotationMetadata mutableAnnotationMetadata = (MutableAnnotationMetadata) annotationMetadata; + if (annotationMetadata instanceof MutableAnnotationMetadata mutableAnnotationMetadata) { for (String annotation : annotationNames) { mutableAnnotationMetadata.removeAnnotation(annotation); } @@ -2777,7 +2775,7 @@ private void removeAnnotations(AnnotationMetadata annotationMetadata, String... private void applyDefaultNamedToParameters(List argumentTypes) { for (ParameterElement parameterElement : argumentTypes) { final AnnotationMetadata annotationMetadata = parameterElement.getAnnotationMetadata(); - DefaultAnnotationMetadata.contributeDefaults(this.annotationMetadata, annotationMetadata); + MutableAnnotationMetadata.contributeDefaults(this.annotationMetadata, annotationMetadata); VisitorContextUtils.contributeRepeatable(this.annotationMetadata, parameterElement.getGenericType()); autoApplyNamedIfPresent(parameterElement, annotationMetadata); } @@ -3992,7 +3990,7 @@ private void visitBeanDefinitionConstructorInternal(GeneratorAdapter staticInit, if (constructor instanceof MethodElement methodElement) { AnnotationMetadata constructorMetadata = methodElement.getAnnotationMetadata(); - DefaultAnnotationMetadata.contributeDefaults(this.annotationMetadata, constructorMetadata); + MutableAnnotationMetadata.contributeDefaults(this.annotationMetadata, constructorMetadata); VisitorContextUtils.contributeRepeatable(this.annotationMetadata, methodElement.getGenericReturnType()); ParameterElement[] parameters = methodElement.getParameters(); List parameterList = Arrays.asList(parameters); @@ -4152,7 +4150,7 @@ private void pushNewMethodReference(GeneratorAdapter staticInit, boolean isPreDestroyMethod) { annotationMetadata = annotationMetadata.getTargetAnnotationMetadata(); for (ParameterElement value : methodElement.getParameters()) { - DefaultAnnotationMetadata.contributeDefaults(this.annotationMetadata, value.getAnnotationMetadata()); + MutableAnnotationMetadata.contributeDefaults(this.annotationMetadata, value.getAnnotationMetadata()); VisitorContextUtils.contributeRepeatable(this.annotationMetadata, value.getGenericType()); } staticInit.newInstance(Type.getType(AbstractInitializableBeanDefinition.MethodReference.class)); @@ -4232,20 +4230,20 @@ private void pushAnnotationMetadata(GeneratorAdapter staticInit, AnnotationMetad annotationMetadata = annotationMetadata.getTargetAnnotationMetadata(); if (annotationMetadata == AnnotationMetadata.EMPTY_METADATA || annotationMetadata.isEmpty()) { staticInit.push((String) null); - } else if (annotationMetadata instanceof AnnotationMetadataHierarchy) { + } else if (annotationMetadata instanceof AnnotationMetadataHierarchy annotationMetadataHierarchy) { AnnotationMetadataWriter.instantiateNewMetadataHierarchy( beanDefinitionType, classWriter, staticInit, - (AnnotationMetadataHierarchy) annotationMetadata, + annotationMetadataHierarchy, defaultsStorage, loadTypeMethods); - } else if (annotationMetadata instanceof DefaultAnnotationMetadata) { + } else if (annotationMetadata instanceof MutableAnnotationMetadata mutableAnnotationMetadata) { AnnotationMetadataWriter.instantiateNewMetadata( beanDefinitionType, classWriter, staticInit, - (DefaultAnnotationMetadata) annotationMetadata, + mutableAnnotationMetadata, defaultsStorage, loadTypeMethods); } else { diff --git a/core-processor/src/main/java/io/micronaut/inject/writer/ExecutableMethodWriter.java b/core-processor/src/main/java/io/micronaut/inject/writer/ExecutableMethodWriter.java index 454079236ad..86204bcb77a 100644 --- a/core-processor/src/main/java/io/micronaut/inject/writer/ExecutableMethodWriter.java +++ b/core-processor/src/main/java/io/micronaut/inject/writer/ExecutableMethodWriter.java @@ -15,15 +15,15 @@ */ package io.micronaut.inject.writer; -import io.micronaut.core.annotation.NonNull; import io.micronaut.context.AbstractExecutableMethod; import io.micronaut.core.annotation.AnnotationMetadata; import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.NonNull; import io.micronaut.core.reflect.ReflectionUtils; import io.micronaut.core.type.Argument; import io.micronaut.inject.ExecutableMethod; import io.micronaut.inject.annotation.AnnotationMetadataReference; -import io.micronaut.inject.annotation.DefaultAnnotationMetadata; +import io.micronaut.inject.annotation.MutableAnnotationMetadata; import io.micronaut.inject.ast.ClassElement; import io.micronaut.inject.ast.MethodElement; import io.micronaut.inject.ast.ParameterElement; @@ -40,7 +40,12 @@ import java.io.IOException; import java.io.OutputStream; -import java.util.*; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Locale; /** * Writes out {@link io.micronaut.inject.ExecutableMethod} implementations. @@ -271,7 +276,7 @@ public void visitMethod(TypedElement declaringType, ); for (ParameterElement pe : argumentTypes) { - DefaultAnnotationMetadata.contributeDefaults(this.annotationMetadata, pe.getAnnotationMetadata()); + MutableAnnotationMetadata.contributeDefaults(this.annotationMetadata, pe.getAnnotationMetadata()); VisitorContextUtils.contributeRepeatable(this.annotationMetadata, pe.getGenericType()); } // now invoke super(..) if no arg constructor diff --git a/core-processor/src/main/java/io/micronaut/inject/writer/ExecutableMethodsDefinitionWriter.java b/core-processor/src/main/java/io/micronaut/inject/writer/ExecutableMethodsDefinitionWriter.java index a9fcf1c7037..8e94b5fe13e 100644 --- a/core-processor/src/main/java/io/micronaut/inject/writer/ExecutableMethodsDefinitionWriter.java +++ b/core-processor/src/main/java/io/micronaut/inject/writer/ExecutableMethodsDefinitionWriter.java @@ -23,7 +23,7 @@ import io.micronaut.inject.annotation.AnnotationMetadataHierarchy; import io.micronaut.inject.annotation.AnnotationMetadataReference; import io.micronaut.inject.annotation.AnnotationMetadataWriter; -import io.micronaut.inject.annotation.DefaultAnnotationMetadata; +import io.micronaut.inject.annotation.MutableAnnotationMetadata; import io.micronaut.inject.ast.ClassElement; import io.micronaut.inject.ast.MethodElement; import io.micronaut.inject.ast.ParameterElement; @@ -420,24 +420,23 @@ private void pushNewMethodReference(ClassWriter classWriter, private void pushAnnotationMetadata(ClassWriter classWriter, GeneratorAdapter staticInit, AnnotationMetadata annotationMetadata) { if (annotationMetadata == AnnotationMetadata.EMPTY_METADATA || annotationMetadata.isEmpty()) { staticInit.push((String) null); - } else if (annotationMetadata instanceof AnnotationMetadataReference) { - AnnotationMetadataReference reference = (AnnotationMetadataReference) annotationMetadata; - String className = reference.getClassName(); + } else if (annotationMetadata instanceof AnnotationMetadataReference annotationMetadataReference) { + String className = annotationMetadataReference.getClassName(); staticInit.getStatic(getTypeReferenceForName(className), AbstractAnnotationMetadataWriter.FIELD_ANNOTATION_METADATA, Type.getType(AnnotationMetadata.class)); - } else if (annotationMetadata instanceof AnnotationMetadataHierarchy) { + } else if (annotationMetadata instanceof AnnotationMetadataHierarchy annotationMetadataHierarchy) { AnnotationMetadataWriter.instantiateNewMetadataHierarchy( thisType, classWriter, staticInit, - (AnnotationMetadataHierarchy) annotationMetadata, + annotationMetadataHierarchy, defaultsStorage, loadTypeMethods); - } else if (annotationMetadata instanceof DefaultAnnotationMetadata) { + } else if (annotationMetadata instanceof MutableAnnotationMetadata mutableAnnotationMetadata) { AnnotationMetadataWriter.instantiateNewMetadata( thisType, classWriter, staticInit, - (DefaultAnnotationMetadata) annotationMetadata, + mutableAnnotationMetadata, defaultsStorage, loadTypeMethods); } else { diff --git a/inject-java/src/test/groovy/io/micronaut/inject/annotation/AnnotationMetadataWriterSpec.groovy b/inject-java/src/test/groovy/io/micronaut/inject/annotation/AnnotationMetadataWriterSpec.groovy index c4b74e88d5d..7012fc2c962 100644 --- a/inject-java/src/test/groovy/io/micronaut/inject/annotation/AnnotationMetadataWriterSpec.groovy +++ b/inject-java/src/test/groovy/io/micronaut/inject/annotation/AnnotationMetadataWriterSpec.groovy @@ -124,7 +124,7 @@ class Test { void "test write annotation metadata with primitive arrays"() { given: - AnnotationMetadata toWrite = new DefaultAnnotationMetadata( + AnnotationMetadata toWrite = new MutableAnnotationMetadata( [ "io.micrometer.core.annotation.Timed": [ percentiles: [1.1d] as double[] @@ -135,7 +135,7 @@ class Test { percentiles: [1.1d] as double[] ] - ], null + ], null, false ) when: def className = "test" diff --git a/inject/src/main/java/io/micronaut/inject/annotation/AnnotationMetadataHierarchy.java b/inject/src/main/java/io/micronaut/inject/annotation/AnnotationMetadataHierarchy.java index 4bd11ba34a1..61f0c640887 100644 --- a/inject/src/main/java/io/micronaut/inject/annotation/AnnotationMetadataHierarchy.java +++ b/inject/src/main/java/io/micronaut/inject/annotation/AnnotationMetadataHierarchy.java @@ -115,8 +115,8 @@ public boolean hasPropertyExpressions() { @Override public Optional> getAnnotationType(@NonNull String name) { - for (AnnotationMetadata metadata1 : hierarchy) { - final Optional> annotationType = ((Function>>) (metadata) -> metadata.getAnnotationType(name)).apply(metadata1); + for (AnnotationMetadata metadata : hierarchy) { + Optional> annotationType = metadata.getAnnotationType(name); if (annotationType.isPresent()) { return annotationType; } @@ -126,8 +126,8 @@ public Optional> getAnnotationType(@NonNull String n @Override public Optional> getAnnotationType(@NonNull String name, @NonNull ClassLoader classLoader) { - for (AnnotationMetadata metadata1 : hierarchy) { - final Optional> annotationType = ((Function>>) (metadata) -> metadata.getAnnotationType(name, classLoader)).apply(metadata1); + for (AnnotationMetadata metadata : hierarchy) { + Optional> annotationType = metadata.getAnnotationType(name, classLoader); if (annotationType.isPresent()) { return annotationType; } @@ -623,8 +623,8 @@ public Map getDefaultValues(@NonNull String annotation) { public > Optional enumValue(@NonNull Class annotation, @NonNull String member, Class enumType, @Nullable Function valueMapper) { for (AnnotationMetadata annotationMetadata : hierarchy) { final Optional o; - if (annotationMetadata instanceof EnvironmentAnnotationMetadata) { - o = ((EnvironmentAnnotationMetadata) annotationMetadata).enumValue(annotation, member, enumType, valueMapper); + if (annotationMetadata instanceof EnvironmentAnnotationMetadata environmentAnnotationMetadata) { + o = environmentAnnotationMetadata.enumValue(annotation, member, enumType, valueMapper); } else { o = annotationMetadata.enumValue(annotation, member, enumType); } @@ -639,8 +639,8 @@ public > Optional enumValue(@NonNull Class> Optional enumValue(@NonNull String annotation, @NonNull String member, Class enumType, @Nullable Function valueMapper) { for (AnnotationMetadata annotationMetadata : hierarchy) { final Optional o; - if (annotationMetadata instanceof EnvironmentAnnotationMetadata) { - o = ((EnvironmentAnnotationMetadata) annotationMetadata).enumValue(annotation, member, enumType, valueMapper); + if (annotationMetadata instanceof EnvironmentAnnotationMetadata environmentAnnotationMetadata) { + o = environmentAnnotationMetadata.enumValue(annotation, member, enumType, valueMapper); } else { o = annotationMetadata.enumValue(annotation, member, enumType); } @@ -683,8 +683,8 @@ public > E[] enumValues(@NonNull String annotation, @NonNull S public Optional classValue(@NonNull Class annotation, @NonNull String member, Function valueMapper) { for (AnnotationMetadata annotationMetadata : hierarchy) { final Optional o; - if (annotationMetadata instanceof EnvironmentAnnotationMetadata) { - o = ((EnvironmentAnnotationMetadata) annotationMetadata).classValue(annotation, member, valueMapper); + if (annotationMetadata instanceof EnvironmentAnnotationMetadata environmentAnnotationMetadata) { + o = environmentAnnotationMetadata.classValue(annotation, member, valueMapper); } else { o = annotationMetadata.classValue(annotation, member); } @@ -699,8 +699,8 @@ public Optional classValue(@NonNull Class annotatio public Optional classValue(@NonNull String annotation, @NonNull String member, @Nullable Function valueMapper) { for (AnnotationMetadata annotationMetadata : hierarchy) { final Optional o; - if (annotationMetadata instanceof EnvironmentAnnotationMetadata) { - o = ((EnvironmentAnnotationMetadata) annotationMetadata).classValue(annotation, member, valueMapper); + if (annotationMetadata instanceof EnvironmentAnnotationMetadata environmentAnnotationMetadata) { + o = environmentAnnotationMetadata.classValue(annotation, member, valueMapper); } else { o = annotationMetadata.classValue(annotation, member); } @@ -715,8 +715,8 @@ public Optional classValue(@NonNull String annotation, @NonNull String me public OptionalInt intValue(@NonNull Class annotation, @NonNull String member, @Nullable Function valueMapper) { for (AnnotationMetadata annotationMetadata : hierarchy) { final OptionalInt o; - if (annotationMetadata instanceof EnvironmentAnnotationMetadata) { - o = ((EnvironmentAnnotationMetadata) annotationMetadata).intValue(annotation, member, valueMapper); + if (annotationMetadata instanceof EnvironmentAnnotationMetadata environmentAnnotationMetadata) { + o = environmentAnnotationMetadata.intValue(annotation, member, valueMapper); } else { o = annotationMetadata.intValue(annotation, member); } @@ -731,8 +731,8 @@ public OptionalInt intValue(@NonNull Class annotation, @No public Optional booleanValue(@NonNull Class annotation, @NonNull String member, Function valueMapper) { for (AnnotationMetadata annotationMetadata : hierarchy) { final Optional o; - if (annotationMetadata instanceof EnvironmentAnnotationMetadata) { - o = ((EnvironmentAnnotationMetadata) annotationMetadata).booleanValue(annotation, member, valueMapper); + if (annotationMetadata instanceof EnvironmentAnnotationMetadata environmentAnnotationMetadata) { + o = environmentAnnotationMetadata.booleanValue(annotation, member, valueMapper); } else { o = annotationMetadata.booleanValue(annotation, member); } @@ -748,8 +748,8 @@ public Optional booleanValue(@NonNull Class annot public Optional booleanValue(@NonNull String annotation, @NonNull String member, @Nullable Function valueMapper) { for (AnnotationMetadata annotationMetadata : hierarchy) { final Optional o; - if (annotationMetadata instanceof EnvironmentAnnotationMetadata) { - o = ((EnvironmentAnnotationMetadata) annotationMetadata).booleanValue(annotation, member, valueMapper); + if (annotationMetadata instanceof EnvironmentAnnotationMetadata environmentAnnotationMetadata) { + o = environmentAnnotationMetadata.booleanValue(annotation, member, valueMapper); } else { o = annotationMetadata.booleanValue(annotation, member); } @@ -764,8 +764,8 @@ public Optional booleanValue(@NonNull String annotation, @NonNull Strin public OptionalLong longValue(@NonNull Class annotation, @NonNull String member, @Nullable Function valueMapper) { for (AnnotationMetadata annotationMetadata : hierarchy) { final OptionalLong o; - if (annotationMetadata instanceof EnvironmentAnnotationMetadata) { - o = ((EnvironmentAnnotationMetadata) annotationMetadata).longValue(annotation, member, valueMapper); + if (annotationMetadata instanceof EnvironmentAnnotationMetadata environmentAnnotationMetadata) { + o = environmentAnnotationMetadata.longValue(annotation, member, valueMapper); } else { o = annotationMetadata.longValue(annotation, member); } @@ -781,8 +781,8 @@ public OptionalLong longValue(@NonNull Class annotation, @ public OptionalLong longValue(@NonNull String annotation, @NonNull String member, @Nullable Function valueMapper) { for (AnnotationMetadata annotationMetadata : hierarchy) { final OptionalLong o; - if (annotationMetadata instanceof EnvironmentAnnotationMetadata) { - o = ((EnvironmentAnnotationMetadata) annotationMetadata).longValue(annotation, member, valueMapper); + if (annotationMetadata instanceof EnvironmentAnnotationMetadata environmentAnnotationMetadata) { + o = environmentAnnotationMetadata.longValue(annotation, member, valueMapper); } else { o = annotationMetadata.longValue(annotation, member); } @@ -798,8 +798,8 @@ public OptionalLong longValue(@NonNull String annotation, @NonNull String member public OptionalInt intValue(@NonNull String annotation, @NonNull String member, @Nullable Function valueMapper) { for (AnnotationMetadata annotationMetadata : hierarchy) { final OptionalInt o; - if (annotationMetadata instanceof EnvironmentAnnotationMetadata) { - o = ((EnvironmentAnnotationMetadata) annotationMetadata).intValue(annotation, member, valueMapper); + if (annotationMetadata instanceof EnvironmentAnnotationMetadata environmentAnnotationMetadata) { + o = environmentAnnotationMetadata.intValue(annotation, member, valueMapper); } else { o = annotationMetadata.intValue(annotation, member); } @@ -863,8 +863,8 @@ public boolean isPresent(Class annotation, String member) public Optional stringValue(@NonNull Class annotation, @NonNull String member, Function valueMapper) { for (AnnotationMetadata annotationMetadata : hierarchy) { final Optional o; - if (annotationMetadata instanceof EnvironmentAnnotationMetadata) { - o = ((EnvironmentAnnotationMetadata) annotationMetadata).stringValue(annotation, member, valueMapper); + if (annotationMetadata instanceof EnvironmentAnnotationMetadata environmentAnnotationMetadata) { + o = environmentAnnotationMetadata.stringValue(annotation, member, valueMapper); } else { o = annotationMetadata.stringValue(annotation, member); } @@ -880,8 +880,8 @@ public Optional stringValue(@NonNull Class annotat public String[] stringValues(@NonNull Class annotation, @NonNull String member, Function valueMapper) { List strings = new ArrayList<>(); for (AnnotationMetadata am : hierarchy) { - if (am instanceof EnvironmentAnnotationMetadata) { - strings.addAll(Arrays.asList(((EnvironmentAnnotationMetadata) am).stringValues(annotation, member, valueMapper))); + if (am instanceof EnvironmentAnnotationMetadata environmentAnnotationMetadata) { + strings.addAll(Arrays.asList(environmentAnnotationMetadata.stringValues(annotation, member, valueMapper))); } else { strings.addAll(Arrays.asList(am.stringValues(annotation, member))); } @@ -893,8 +893,8 @@ public String[] stringValues(@NonNull Class annotation, @N public String[] stringValues(String annotation, String member, Function valueMapper) { List strings = new ArrayList<>(); for (AnnotationMetadata am : hierarchy) { - if (am instanceof EnvironmentAnnotationMetadata) { - strings.addAll(Arrays.asList(((EnvironmentAnnotationMetadata) am).stringValues(annotation, member, valueMapper))); + if (am instanceof EnvironmentAnnotationMetadata environmentAnnotationMetadata) { + strings.addAll(Arrays.asList(environmentAnnotationMetadata.stringValues(annotation, member, valueMapper))); } else { strings.addAll(Arrays.asList(am.stringValues(annotation, member))); } @@ -907,8 +907,8 @@ public String[] stringValues(String annotation, String member, Function stringValue(@NonNull String annotation, @NonNull String member, @Nullable Function valueMapper) { for (AnnotationMetadata annotationMetadata : hierarchy) { final Optional o; - if (annotationMetadata instanceof EnvironmentAnnotationMetadata) { - o = ((EnvironmentAnnotationMetadata) annotationMetadata).stringValue(annotation, member, valueMapper); + if (annotationMetadata instanceof EnvironmentAnnotationMetadata environmentAnnotationMetadata) { + o = environmentAnnotationMetadata.stringValue(annotation, member, valueMapper); } else { o = annotationMetadata.stringValue(annotation, member); } @@ -933,8 +933,8 @@ public boolean isTrue(@NonNull String annotation, @NonNull String member, @Nulla public OptionalDouble doubleValue(@NonNull Class annotation, @NonNull String member, @Nullable Function valueMapper) { for (AnnotationMetadata annotationMetadata : hierarchy) { final OptionalDouble o; - if (annotationMetadata instanceof EnvironmentAnnotationMetadata) { - o = ((EnvironmentAnnotationMetadata) annotationMetadata).doubleValue(annotation, member, valueMapper); + if (annotationMetadata instanceof EnvironmentAnnotationMetadata environmentAnnotationMetadata) { + o = environmentAnnotationMetadata.doubleValue(annotation, member, valueMapper); } else { o = annotationMetadata.doubleValue(annotation, member); } @@ -950,8 +950,8 @@ public OptionalDouble doubleValue(@NonNull Class annotatio public OptionalDouble doubleValue(@NonNull String annotation, @NonNull String member, Function valueMapper) { for (AnnotationMetadata annotationMetadata : hierarchy) { final OptionalDouble o; - if (annotationMetadata instanceof EnvironmentAnnotationMetadata) { - o = ((EnvironmentAnnotationMetadata) annotationMetadata).doubleValue(annotation, member, valueMapper); + if (annotationMetadata instanceof EnvironmentAnnotationMetadata environmentAnnotationMetadata) { + o = environmentAnnotationMetadata.doubleValue(annotation, member, valueMapper); } else { o = annotationMetadata.doubleValue(annotation, member); } @@ -967,8 +967,8 @@ public OptionalDouble doubleValue(@NonNull String annotation, @NonNull String me public Optional getValue(@NonNull String annotation, @NonNull String member, @NonNull Argument requiredType, @Nullable Function valueMapper) { for (AnnotationMetadata annotationMetadata : hierarchy) { final Optional o; - if (annotationMetadata instanceof EnvironmentAnnotationMetadata) { - o = ((EnvironmentAnnotationMetadata) annotationMetadata).getValue(annotation, member, requiredType, valueMapper); + if (annotationMetadata instanceof EnvironmentAnnotationMetadata environmentAnnotationMetadata) { + o = environmentAnnotationMetadata.getValue(annotation, member, requiredType, valueMapper); } else { o = annotationMetadata.getValue(annotation, member, requiredType); } @@ -1050,10 +1050,12 @@ public MutableAnnotationMetadata merge() { if (annotationMetadata.isEmpty()) { continue; } - if (annotationMetadata instanceof AnnotationMetadataHierarchy) { - newAnnotationMetadata.addAnnotationMetadata(((AnnotationMetadataHierarchy) annotationMetadata).merge()); - } else if (annotationMetadata instanceof DefaultAnnotationMetadata) { - newAnnotationMetadata.addAnnotationMetadata((DefaultAnnotationMetadata) annotationMetadata); + if (annotationMetadata instanceof AnnotationMetadataHierarchy annotationMetadataHierarchy) { + newAnnotationMetadata.addAnnotationMetadata(annotationMetadataHierarchy.merge()); + } else if (annotationMetadata instanceof MutableAnnotationMetadata mutableAnnotationMetadata) { + newAnnotationMetadata.addAnnotationMetadata(mutableAnnotationMetadata); + } else if (annotationMetadata instanceof DefaultAnnotationMetadata defaultAnnotationMetadata) { + newAnnotationMetadata.addAnnotationMetadata(defaultAnnotationMetadata); } else { throw new IllegalStateException("Unknown instance of AnnotationMetadata: " + annotationMetadata.getClass()); } @@ -1072,11 +1074,6 @@ public AnnotationMetadata copyAnnotationMetadata() { ); } - @NonNull - private static AnnotationValue newAnnotationValue(String annotationType, Map values) { - return new AnnotationValue<>(annotationType, values, AnnotationMetadataSupport.getDefaultValuesOrNull(annotationType)); - } - /** * The size of the hierarchy. * @return The size diff --git a/inject/src/main/java/io/micronaut/inject/annotation/DefaultAnnotationMetadata.java b/inject/src/main/java/io/micronaut/inject/annotation/DefaultAnnotationMetadata.java index a26b3dc80dc..31feb48c1c3 100644 --- a/inject/src/main/java/io/micronaut/inject/annotation/DefaultAnnotationMetadata.java +++ b/inject/src/main/java/io/micronaut/inject/annotation/DefaultAnnotationMetadata.java @@ -34,15 +34,12 @@ import io.micronaut.core.value.OptionalValues; import java.lang.annotation.Annotation; -import java.lang.annotation.RetentionPolicy; import java.lang.reflect.Array; import java.util.AbstractMap; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashMap; -import java.util.HashSet; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.LinkedHashSet; @@ -56,7 +53,6 @@ import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.function.Function; -import java.util.function.Predicate; import java.util.stream.Collectors; /** @@ -82,15 +78,8 @@ public class DefaultAnnotationMetadata extends AbstractAnnotationMetadata implem Map> allStereotypes; @Nullable Map> annotationsByStereotype; - @Nullable - Map> annotationDefaultValues; - @Nullable - Map annotationRepeatableContainer; - @Nullable - Set sourceRetentionAnnotations; - @Nullable - Map> sourceAnnotationDefaultValues; - private Map annotationValuesByType = new ConcurrentHashMap<>(2); + + private final Map annotationValuesByType = new ConcurrentHashMap<>(2); private final boolean hasPropertyExpressions; @@ -114,11 +103,11 @@ protected DefaultAnnotationMetadata() { @Internal @UsedByGeneratedCode public DefaultAnnotationMetadata( - @Nullable Map> declaredAnnotations, - @Nullable Map> declaredStereotypes, - @Nullable Map> allStereotypes, - @Nullable Map> allAnnotations, - @Nullable Map> annotationsByStereotype) { + @Nullable Map> declaredAnnotations, + @Nullable Map> declaredStereotypes, + @Nullable Map> allStereotypes, + @Nullable Map> allAnnotations, + @Nullable Map> annotationsByStereotype) { this(declaredAnnotations, declaredStereotypes, allStereotypes, allAnnotations, annotationsByStereotype, true); } @@ -135,12 +124,12 @@ public DefaultAnnotationMetadata( @Internal @UsedByGeneratedCode public DefaultAnnotationMetadata( - @Nullable Map> declaredAnnotations, - @Nullable Map> declaredStereotypes, - @Nullable Map> allStereotypes, - @Nullable Map> allAnnotations, - @Nullable Map> annotationsByStereotype, - boolean hasPropertyExpressions) { + @Nullable Map> declaredAnnotations, + @Nullable Map> declaredStereotypes, + @Nullable Map> allStereotypes, + @Nullable Map> allAnnotations, + @Nullable Map> annotationsByStereotype, + boolean hasPropertyExpressions) { this(declaredAnnotations, declaredStereotypes, allStereotypes, allAnnotations, annotationsByStereotype, hasPropertyExpressions, false); } @@ -161,13 +150,13 @@ public DefaultAnnotationMetadata( @NextMajorVersion("Remove after Micronaut 4 Milestone 1") @Deprecated(forRemoval = true, since = "4") public DefaultAnnotationMetadata( - @Nullable Map> declaredAnnotations, - @Nullable Map> declaredStereotypes, - @Nullable Map> allStereotypes, - @Nullable Map> allAnnotations, - @Nullable Map> annotationsByStereotype, - boolean hasPropertyExpressions, - boolean useRepeatableDefaults) { + @Nullable Map> declaredAnnotations, + @Nullable Map> declaredStereotypes, + @Nullable Map> allStereotypes, + @Nullable Map> allAnnotations, + @Nullable Map> annotationsByStereotype, + boolean hasPropertyExpressions, + boolean useRepeatableDefaults) { super(declaredAnnotations, allAnnotations); this.declaredAnnotations = declaredAnnotations; this.declaredStereotypes = declaredStereotypes; @@ -181,12 +170,12 @@ public DefaultAnnotationMetadata( @Override public AnnotationMetadata getDeclaredMetadata() { return new DefaultAnnotationMetadata( - this.declaredAnnotations, - this.declaredStereotypes, - null, - null, - annotationsByStereotype, - hasPropertyExpressions + this.declaredAnnotations, + this.declaredStereotypes, + null, + null, + annotationsByStereotype, + hasPropertyExpressions ); } @@ -195,17 +184,6 @@ public boolean hasPropertyExpressions() { return hasPropertyExpressions; } - /** - * @return The annotations that are source retention. - */ - @Internal - Set getSourceRetentionAnnotations() { - if (sourceRetentionAnnotations != null) { - return Collections.unmodifiableSet(sourceRetentionAnnotations); - } - return Collections.emptySet(); - } - @NonNull @Override public Map getDefaultValues(@NonNull String annotation) { @@ -215,19 +193,20 @@ public Map getDefaultValues(@NonNull String annotation) { @Override public boolean isPresent(@NonNull String annotation, @NonNull String member) { - boolean isPresent = false; - if (allAnnotations != null && StringUtils.isNotEmpty(annotation)) { - Map values = allAnnotations.get(annotation); + if (allAnnotations == null || StringUtils.isEmpty(annotation)) { + return false; + } + Map values = allAnnotations.get(annotation); + if (values != null) { + return values.containsKey(member); + } + if (allStereotypes != null) { + values = allStereotypes.get(annotation); if (values != null) { - isPresent = values.containsKey(member); - } else if (allStereotypes != null) { - values = allStereotypes.get(annotation); - if (values != null) { - isPresent = values.containsKey(member); - } + return values.containsKey(member); } } - return isPresent; + return false; } @Override @@ -268,8 +247,8 @@ public > Optional enumValue(@NonNull Class) v).enumValue(member, enumType, valueMapper); + if (v instanceof AnnotationValue annotationValue) { + return annotationValue.enumValue(member, enumType, valueMapper); } return Optional.empty(); } else { @@ -304,8 +283,8 @@ public > E[] enumValues(@NonNull Class a String repeatableTypeName = findRepeatableAnnotationContainerInternal(annotation.getName()); if (repeatableTypeName != null) { Object v = getRawValue(repeatableTypeName, member); - if (v instanceof AnnotationValue) { - return ((AnnotationValue) v).enumValues(member, enumType); + if (v instanceof AnnotationValue annotationValue) { + return annotationValue.enumValues(member, enumType); } return (E[]) Array.newInstance(enumType, 0); } else { @@ -376,8 +355,8 @@ public Class[] classValues(@NonNull Class annotatio String repeatableTypeName = findRepeatableAnnotationContainerInternal(annotation.getName()); if (repeatableTypeName != null) { Object v = getRawSingleValue(repeatableTypeName, member, null); - if (v instanceof AnnotationValue) { - Class[] classes = ((AnnotationValue) v).classValues(member); + if (v instanceof AnnotationValue annotationValue) { + Class[] classes = annotationValue.classValues(member); return (Class[]) classes; } //noinspection unchecked @@ -408,8 +387,8 @@ public Optional classValue(@NonNull Class annotatio String repeatableTypeName = findRepeatableAnnotationContainerInternal(annotation.getName()); if (repeatableTypeName != null) { Object v = getRawSingleValue(repeatableTypeName, member, valueMapper); - if (v instanceof AnnotationValue) { - return (Optional) ((AnnotationValue) v).classValue(member, valueMapper); + if (v instanceof AnnotationValue annotationValue) { + return (Optional) (annotationValue.classValue(member, valueMapper)); } return Optional.empty(); } else { @@ -425,7 +404,6 @@ public Optional classValue(@NonNull String annotation, @NonNull String me return classValue(annotation, member, null); } - /** * Retrieve the class value and optionally map its value. * @@ -438,9 +416,8 @@ public Optional classValue(@NonNull String annotation, @NonNull String me @Internal public Optional classValue(@NonNull String annotation, @NonNull String member, @Nullable Function valueMapper) { Object rawValue = getRawSingleValue(annotation, member, valueMapper); - - if (rawValue instanceof AnnotationClassValue) { - return ((AnnotationClassValue) rawValue).getType(); + if (rawValue instanceof AnnotationClassValue annotationClassValue) { + return annotationClassValue.getType(); } else if (rawValue instanceof Class) { return Optional.of((Class) rawValue); } else if (rawValue != null) { @@ -480,8 +457,8 @@ public OptionalInt intValue(@NonNull Class annotation, @No String repeatableTypeName = findRepeatableAnnotationContainerInternal(annotation.getName()); if (repeatableTypeName != null) { Object v = getRawSingleValue(repeatableTypeName, VALUE_MEMBER, valueMapper); - if (v instanceof AnnotationValue) { - return ((AnnotationValue) v).intValue(member, valueMapper); + if (v instanceof AnnotationValue annotationValue) { + return annotationValue.intValue(member, valueMapper); } return OptionalInt.empty(); } else { @@ -517,8 +494,8 @@ public Optional booleanValue(@NonNull Class annot String repeatableTypeName = findRepeatableAnnotationContainerInternal(annotation.getName()); if (repeatableTypeName != null) { Object v = getRawSingleValue(repeatableTypeName, VALUE_MEMBER, null); - if (v instanceof AnnotationValue) { - return ((AnnotationValue) v).booleanValue(member, valueMapper); + if (v instanceof AnnotationValue annotationValue) { + return annotationValue.booleanValue(member, valueMapper); } return Optional.empty(); } else { @@ -538,8 +515,8 @@ public Optional booleanValue(@NonNull Class annot @NonNull public Optional booleanValue(@NonNull String annotation, @NonNull String member, @Nullable Function valueMapper) { Object rawValue = getRawSingleValue(annotation, member, valueMapper); - if (rawValue instanceof Boolean) { - return Optional.of((Boolean) rawValue); + if (rawValue instanceof Boolean aBoolean) { + return Optional.of(aBoolean); } else if (rawValue != null) { return Optional.of(StringUtils.isTrue(rawValue.toString())); } @@ -577,8 +554,8 @@ public OptionalLong longValue(@NonNull Class annotation, @ String repeatableTypeName = findRepeatableAnnotationContainerInternal(annotation.getName()); if (repeatableTypeName != null) { Object v = getRawSingleValue(repeatableTypeName, VALUE_MEMBER, valueMapper); - if (v instanceof AnnotationValue) { - return ((AnnotationValue) v).longValue(member, valueMapper); + if (v instanceof AnnotationValue annotationValue) { + return annotationValue.longValue(member, valueMapper); } return OptionalLong.empty(); } else { @@ -598,8 +575,8 @@ public OptionalLong longValue(@NonNull Class annotation, @ @NonNull public OptionalLong longValue(@NonNull String annotation, @NonNull String member, @Nullable Function valueMapper) { Object rawValue = getRawSingleValue(annotation, member, valueMapper); - if (rawValue instanceof Number) { - return OptionalLong.of(((Number) rawValue).longValue()); + if (rawValue instanceof Number number) { + return OptionalLong.of(number.longValue()); } else if (rawValue instanceof CharSequence) { final String str = rawValue.toString(); if (StringUtils.isNotEmpty(str)) { @@ -627,8 +604,8 @@ public OptionalLong longValue(@NonNull String annotation, @NonNull String member @NonNull public OptionalInt intValue(@NonNull String annotation, @NonNull String member, @Nullable Function valueMapper) { Object rawValue = getRawSingleValue(annotation, member, valueMapper); - if (rawValue instanceof Number) { - return OptionalInt.of(((Number) rawValue).intValue()); + if (rawValue instanceof Number number) { + return OptionalInt.of(number.intValue()); } else if (rawValue instanceof CharSequence) { final String str = rawValue.toString(); if (StringUtils.isNotEmpty(str)) { @@ -664,8 +641,8 @@ public Optional stringValue(@NonNull Class annotat String repeatableTypeName = findRepeatableAnnotationContainerInternal(annotation.getName()); if (repeatableTypeName != null) { Object v = getRawSingleValue(repeatableTypeName, VALUE_MEMBER, valueMapper); - if (v instanceof AnnotationValue) { - return ((AnnotationValue) v).stringValue(member, valueMapper); + if (v instanceof AnnotationValue annotationValue) { + return annotationValue.stringValue(member, valueMapper); } return Optional.empty(); } else { @@ -700,8 +677,8 @@ public String[] stringValues(@NonNull Class annotation, @N String repeatableTypeName = findRepeatableAnnotationContainerInternal(annotation.getName()); if (repeatableTypeName != null) { Object v = getRawValue(repeatableTypeName, member); - if (v instanceof AnnotationValue) { - return ((AnnotationValue) v).stringValues(member, valueMapper); + if (v instanceof AnnotationValue annotationValue) { + return annotationValue.stringValues(member, valueMapper); } return StringUtils.EMPTY_STRING_ARRAY; } else { @@ -711,7 +688,6 @@ public String[] stringValues(@NonNull Class annotation, @N } } - /** * Retrieve the string value and optionally map its value. * @@ -754,8 +730,8 @@ public Optional stringValue(@NonNull String annotation, @NonNull String return Optional.of(s); } else if (rawValue instanceof CharSequence) { return Optional.of(rawValue.toString()); - } else if (rawValue instanceof Class) { - String name = ((Class) rawValue).getName(); + } else if (rawValue instanceof Class aClass) { + String name = aClass.getName(); return Optional.of(name); } else if (rawValue != null) { return Optional.of(rawValue.toString()); @@ -783,8 +759,8 @@ public boolean isTrue(@NonNull Class annotation, @NonNull String repeatableTypeName = findRepeatableAnnotationContainerInternal(annotation.getName()); if (repeatableTypeName != null) { Object v = getRawSingleValue(repeatableTypeName, VALUE_MEMBER, valueMapper); - if (v instanceof AnnotationValue) { - return ((AnnotationValue) v).isTrue(member, valueMapper); + if (v instanceof AnnotationValue annotationValue) { + return annotationValue.isTrue(member, valueMapper); } return false; } else { @@ -811,9 +787,8 @@ public boolean isTrue(@NonNull String annotation, @NonNull String member) { @Override public boolean isTrue(@NonNull String annotation, @NonNull String member, @Nullable Function valueMapper) { Object rawValue = getRawSingleValue(annotation, member, valueMapper); - - if (rawValue instanceof Boolean) { - return (Boolean) rawValue; + if (rawValue instanceof Boolean aBoolean) { + return aBoolean; } else if (rawValue != null) { String booleanString = rawValue.toString().toLowerCase(Locale.ENGLISH); return StringUtils.isTrue(booleanString); @@ -856,8 +831,8 @@ public OptionalDouble doubleValue(@NonNull Class annotatio String repeatableTypeName = findRepeatableAnnotationContainerInternal(annotation.getName()); if (repeatableTypeName != null) { Object v = getRawSingleValue(repeatableTypeName, VALUE_MEMBER, valueMapper); - if (v instanceof AnnotationValue) { - return ((AnnotationValue) v).doubleValue(member, valueMapper); + if (v instanceof AnnotationValue annotationValue) { + return annotationValue.doubleValue(member, valueMapper); } return OptionalDouble.empty(); } else { @@ -878,8 +853,8 @@ public OptionalDouble doubleValue(@NonNull Class annotatio @Internal public OptionalDouble doubleValue(@NonNull String annotation, @NonNull String member, Function valueMapper) { Object rawValue = getRawSingleValue(annotation, member, valueMapper); - if (rawValue instanceof Number) { - return OptionalDouble.of(((Number) rawValue).doubleValue()); + if (rawValue instanceof Number number) { + return OptionalDouble.of(number.doubleValue()); } else if (rawValue instanceof CharSequence) { final String str = rawValue.toString(); if (StringUtils.isNotEmpty(str)) { @@ -944,9 +919,7 @@ public Optional getValue(@NonNull String annotation, @NonNull String memb if (valueMapper != null) { rawValue = valueMapper.apply(rawValue); } - resolved = ConversionService.SHARED.convert( - rawValue, requiredType - ); + resolved = ConversionService.SHARED.convert(rawValue, requiredType); } } else if (allStereotypes != null) { values = allStereotypes.get(annotation); @@ -956,18 +929,14 @@ public Optional getValue(@NonNull String annotation, @NonNull String memb if (valueMapper != null) { rawValue = valueMapper.apply(rawValue); } - resolved = ConversionService.SHARED.convert( - rawValue, requiredType - ); + resolved = ConversionService.SHARED.convert(rawValue, requiredType); } } } } - - if (!resolved.isPresent() && hasStereotype(annotation)) { + if (resolved.isEmpty() && hasStereotype(annotation)) { return getDefaultValue(annotation, member, requiredType); } - return resolved; } @@ -978,114 +947,95 @@ public Optional getValue(@NonNull String annotation, @NonNull String memb ArgumentUtils.requireNonNull("requiredType", requiredType); Map defaultValues = getDefaultValues(annotation); - if (defaultValues.containsKey(member)) { - final Object v = defaultValues.get(member); - if (requiredType.isInstance(v)) { - return (Optional) Optional.of(v); - } else { - return ConversionService.SHARED.convert(v, requiredType); - } + final Object v = defaultValues.get(member); + if (v == null) { + return Optional.empty(); } - return Optional.empty(); + if (requiredType.isInstance(v)) { + return (Optional) Optional.of(v); + } + return ConversionService.SHARED.convert(v, requiredType); } @SuppressWarnings("unchecked") @Override public @NonNull List> getAnnotationValuesByType(@Nullable Class annotationType) { - if (annotationType != null) { - final String annotationTypeName = annotationType.getName(); - List> results = annotationValuesByType.get(annotationTypeName); - if (results == null) { - - results = resolveAnnotationValuesByType(annotationType, allAnnotations, allStereotypes); - if (results != null) { - return results; - } else if (allAnnotations != null) { - final Map values = allAnnotations.get(annotationTypeName); - if (values != null) { - results = Collections.singletonList(newAnnotationValue(annotationTypeName, values)); - } - } - - if (results == null) { - results = Collections.emptyList(); + if (annotationType == null) { + return Collections.emptyList(); + } + final String annotationTypeName = annotationType.getName(); + List> results = annotationValuesByType.get(annotationTypeName); + if (results == null) { + results = resolveAnnotationValuesByType(annotationType, allAnnotations, allStereotypes); + if (results != null) { + return results; + } else if (allAnnotations != null) { + final Map values = allAnnotations.get(annotationTypeName); + if (values != null) { + results = Collections.singletonList(newAnnotationValue(annotationTypeName, values)); } - annotationValuesByType.put(annotationTypeName, results); } - return results; + if (results == null) { + results = Collections.emptyList(); + } + annotationValuesByType.put(annotationTypeName, results); } - return Collections.emptyList(); + return results; } @Override public List> getAnnotationValuesByName(String annotationType) { - if (annotationType != null) { - String repeatableTypeName = findRepeatableAnnotationContainerInternal(annotationType); - if (repeatableTypeName != null) { - - List> results = - resolveRepeatableAnnotations(repeatableTypeName, - allAnnotations, - allStereotypes - ); - if (results != null) { - return results; - } else if (allAnnotations != null) { - final Map values = allAnnotations.get(annotationType); - if (values != null) { - results = Collections.singletonList(newAnnotationValue(annotationType, values)); - } - } - - if (results == null) { - results = Collections.emptyList(); - } - annotationValuesByType.put(annotationType, results); + if (annotationType == null) { + return Collections.emptyList(); + } + String repeatableTypeName = findRepeatableAnnotationContainerInternal(annotationType); + if (repeatableTypeName == null) { + return Collections.emptyList(); + } + List> results = resolveRepeatableAnnotations(repeatableTypeName, allAnnotations, allStereotypes); + if (results != null) { + return results; + } + if (allAnnotations != null) { + final Map values = allAnnotations.get(annotationType); + if (values != null) { + results = Collections.singletonList(newAnnotationValue(annotationType, values)); } } + if (results == null) { + results = Collections.emptyList(); + } + annotationValuesByType.put(annotationType, results); return Collections.emptyList(); } @NonNull - private AnnotationValue newAnnotationValue(String annotationType, Map values) { - Map defaultValues = null; - if (annotationDefaultValues != null) { - defaultValues = annotationDefaultValues.get(annotationType); - } - if (defaultValues == null && sourceAnnotationDefaultValues != null) { - defaultValues = sourceAnnotationDefaultValues.get(annotationType); - } - if (defaultValues == null) { - defaultValues = AnnotationMetadataSupport.getDefaultValuesOrNull(annotationType); - } - return new AnnotationValue<>(annotationType, values, defaultValues); + protected AnnotationValue newAnnotationValue(String annotationType, Map values) { + return new AnnotationValue<>(annotationType, values, AnnotationMetadataSupport.getDefaultValuesOrNull(annotationType)); } + @NonNull @Override - public @NonNull List> getDeclaredAnnotationValuesByType(@NonNull Class annotationType) { - if (annotationType != null) { - Map> sourceAnnotations = this.declaredAnnotations; - Map> sourceStereotypes = this.declaredStereotypes; - - List> results = resolveAnnotationValuesByType(annotationType, sourceAnnotations, sourceStereotypes); - if (results != null) { - return results; - } + public List> getDeclaredAnnotationValuesByType(@NonNull Class annotationType) { + if (annotationType == null) { + return Collections.emptyList(); + } + List> results = resolveAnnotationValuesByType(annotationType, declaredAnnotations, declaredStereotypes); + if (results != null) { + return results; } return Collections.emptyList(); } @Override public List> getDeclaredAnnotationValuesByName(String annotationType) { - if (annotationType != null) { - Map> sourceAnnotations = this.declaredAnnotations; - Map> sourceStereotypes = this.declaredStereotypes; - String repeatableTypeName = findRepeatableAnnotationContainerInternal(annotationType); - List> results = - resolveRepeatableAnnotations(repeatableTypeName, sourceAnnotations, sourceStereotypes); - if (results != null) { - return results; - } + if (annotationType == null) { + return Collections.emptyList(); + } + String repeatableTypeName = findRepeatableAnnotationContainerInternal(annotationType); + List> results = resolveRepeatableAnnotations(repeatableTypeName, declaredAnnotations, declaredStereotypes); + if (results != null) { + return results; } return Collections.emptyList(); } @@ -1093,31 +1043,22 @@ public List> getDeclaredAnnotationValu @SuppressWarnings("unchecked") @Override public T[] synthesizeAnnotationsByType(@NonNull Class annotationClass) { - - if (annotationClass != null) { - List> values = getAnnotationValuesByType(annotationClass); - - return values.stream() + if (annotationClass == null) { + return (T[]) AnnotationUtil.ZERO_ANNOTATIONS; + } + return getAnnotationValuesByType(annotationClass).stream() .map(entries -> AnnotationMetadataSupport.buildAnnotation(annotationClass, entries)) .toArray(value -> (T[]) Array.newInstance(annotationClass, value)); - } - - //noinspection unchecked - return (T[]) AnnotationUtil.ZERO_ANNOTATIONS; } @Override public T[] synthesizeDeclaredAnnotationsByType(@NonNull Class annotationClass) { - if (annotationClass != null) { - List> values = getAnnotationValuesByType(annotationClass); - - return values.stream() + if (annotationClass == null) { + return (T[]) AnnotationUtil.ZERO_ANNOTATIONS; + } + return getAnnotationValuesByType(annotationClass).stream() .map(entries -> AnnotationMetadataSupport.buildAnnotation(annotationClass, entries)) .toArray(value -> (T[]) Array.newInstance(annotationClass, value)); - } - - //noinspection unchecked - return (T[]) AnnotationUtil.ZERO_ANNOTATIONS; } @Override @@ -1148,46 +1089,48 @@ public boolean hasDeclaredStereotype(String annotation) { @NonNull @Override public Optional> getAnnotationTypeByStereotype(@Nullable String stereotype) { - if (stereotype != null) { - if (annotationsByStereotype != null) { - List annotations = annotationsByStereotype.get(stereotype); - if (CollectionUtils.isNotEmpty(annotations)) { - return getAnnotationType(annotations.get(0)); - } - } - if (allAnnotations != null && allAnnotations.containsKey(stereotype)) { - return getAnnotationType(stereotype); - } - if (declaredAnnotations != null && declaredAnnotations.containsKey(stereotype)) { - return getAnnotationType(stereotype); + if (stereotype == null) { + return Optional.empty(); + } + if (annotationsByStereotype != null) { + List annotations = annotationsByStereotype.get(stereotype); + if (CollectionUtils.isNotEmpty(annotations)) { + return getAnnotationType(annotations.get(0)); } } + if (allAnnotations != null && allAnnotations.containsKey(stereotype)) { + return getAnnotationType(stereotype); + } + if (declaredAnnotations != null && declaredAnnotations.containsKey(stereotype)) { + return getAnnotationType(stereotype); + } return Optional.empty(); } @NonNull @Override public Optional getAnnotationNameByStereotype(@Nullable String stereotype) { - if (stereotype != null) { - if (annotationsByStereotype != null) { - List annotations = annotationsByStereotype.get(stereotype); - if (CollectionUtils.isNotEmpty(annotations)) { - return Optional.of(annotations.get(0)); - } - } - if (allAnnotations != null && allAnnotations.containsKey(stereotype)) { - return Optional.of(stereotype); - } - if (declaredAnnotations != null && declaredAnnotations.containsKey(stereotype)) { - return Optional.of(stereotype); + if (stereotype == null) { + return Optional.empty(); + } + if (annotationsByStereotype != null) { + List annotations = annotationsByStereotype.get(stereotype); + if (CollectionUtils.isNotEmpty(annotations)) { + return Optional.of(annotations.get(0)); } } + if (allAnnotations != null && allAnnotations.containsKey(stereotype)) { + return Optional.of(stereotype); + } + if (declaredAnnotations != null && declaredAnnotations.containsKey(stereotype)) { + return Optional.of(stereotype); + } return Optional.empty(); } + @NonNull @Override - public @NonNull - List getAnnotationNamesByStereotype(@Nullable String stereotype) { + public List getAnnotationNamesByStereotype(@Nullable String stereotype) { if (stereotype == null) { return Collections.emptyList(); } @@ -1219,10 +1162,10 @@ public List> getAnnotationValuesBySter String repeatableTypeName = findRepeatableAnnotationContainerInternal(annotation); if (repeatableTypeName != null) { List> results = - resolveRepeatableAnnotations(repeatableTypeName, - allAnnotations, - allStereotypes - ); + resolveRepeatableAnnotations(repeatableTypeName, + allAnnotations, + allStereotypes + ); if (results != null) { result.addAll(results); } @@ -1242,15 +1185,16 @@ public List> getAnnotationValuesBySter return Collections.emptyList(); } + @NonNull @Override - public @NonNull - Set getAnnotationNames() { + public Set getAnnotationNames() { if (allAnnotations != null) { return allAnnotations.keySet(); } return Collections.emptySet(); } + @NonNull @Override public Set getStereotypeAnnotationNames() { if (allStereotypes != null) { @@ -1259,6 +1203,7 @@ public Set getStereotypeAnnotationNames() { return Collections.emptySet(); } + @NonNull @Override public Set getDeclaredStereotypeAnnotationNames() { if (declaredStereotypes != null) { @@ -1267,18 +1212,18 @@ public Set getDeclaredStereotypeAnnotationNames() { return Collections.emptySet(); } + @NonNull @Override - public @NonNull - Set getDeclaredAnnotationNames() { + public Set getDeclaredAnnotationNames() { if (declaredAnnotations != null) { return declaredAnnotations.keySet(); } return Collections.emptySet(); } + @NonNull @Override - public @NonNull - List getDeclaredAnnotationNamesByStereotype(@Nullable String stereotype) { + public List getDeclaredAnnotationNamesByStereotype(@Nullable String stereotype) { if (stereotype == null) { return Collections.emptyList(); } @@ -1301,49 +1246,55 @@ List getDeclaredAnnotationNamesByStereotype(@Nullable String stereotype) return Collections.emptyList(); } + @NonNull @Override - public @NonNull - Optional> getAnnotationType(@NonNull String name) { + public Optional> getAnnotationType(@NonNull String name) { return AnnotationMetadataSupport.getAnnotationType(name); } + @NonNull @Override - public @NonNull - Optional> getAnnotationType(@NonNull String name, @NonNull ClassLoader classLoader) { + public Optional> getAnnotationType(@NonNull String name, @NonNull ClassLoader classLoader) { return AnnotationMetadataSupport.getAnnotationType(name, classLoader); } @SuppressWarnings("Duplicates") + @NonNull @Override - public @NonNull Optional> findAnnotation(@NonNull String annotation) { + public Optional> findAnnotation(@NonNull String annotation) { ArgumentUtils.requireNonNull("annotation", annotation); - if (allAnnotations != null && StringUtils.isNotEmpty(annotation)) { - Map values = allAnnotations.get(annotation); + if (allAnnotations == null || StringUtils.isEmpty(annotation)) { + return Optional.empty(); + } + Map values = allAnnotations.get(annotation); + if (values != null) { + return Optional.of(newAnnotationValue(annotation, values)); + } + if (allStereotypes != null) { + values = allStereotypes.get(annotation); if (values != null) { return Optional.of(newAnnotationValue(annotation, values)); - } else if (allStereotypes != null) { - values = allStereotypes.get(annotation); - if (values != null) { - return Optional.of(newAnnotationValue(annotation, values)); - } } } return Optional.empty(); } @SuppressWarnings("Duplicates") + @NonNull @Override - public @NonNull Optional> findDeclaredAnnotation(@NonNull String annotation) { + public Optional> findDeclaredAnnotation(@NonNull String annotation) { ArgumentUtils.requireNonNull("annotation", annotation); - if (declaredAnnotations != null && StringUtils.isNotEmpty(annotation)) { - Map values = declaredAnnotations.get(annotation); + if (declaredAnnotations == null || StringUtils.isEmpty(annotation)) { + return Optional.empty(); + } + Map values = declaredAnnotations.get(annotation); + if (values != null) { + return Optional.of(newAnnotationValue(annotation, values)); + } + if (declaredStereotypes != null) { + values = declaredStereotypes.get(annotation); if (values != null) { return Optional.of(newAnnotationValue(annotation, values)); - } else if (declaredStereotypes != null) { - values = declaredStereotypes.get(annotation); - if (values != null) { - return Optional.of(newAnnotationValue(annotation, values)); - } } } return Optional.empty(); @@ -1353,15 +1304,17 @@ Optional> getAnnotationType(@NonNull String name, @N public @NonNull OptionalValues getValues(@NonNull String annotation, @NonNull Class valueType) { ArgumentUtils.requireNonNull("annotation", annotation); ArgumentUtils.requireNonNull("valueType", valueType); - if (allAnnotations != null && StringUtils.isNotEmpty(annotation)) { - Map values = allAnnotations.get(annotation); + if (allAnnotations == null || StringUtils.isEmpty(annotation)) { + return OptionalValues.empty(); + } + Map values = allAnnotations.get(annotation); + if (values != null) { + return OptionalValues.of(valueType, values); + } + if (allStereotypes != null) { + values = allStereotypes.get(annotation); if (values != null) { return OptionalValues.of(valueType, values); - } else if (allStereotypes != null) { - values = allStereotypes.get(annotation); - if (values != null) { - return OptionalValues.of(valueType, values); - } } } return OptionalValues.empty(); @@ -1371,29 +1324,33 @@ Optional> getAnnotationType(@NonNull String name, @N @Override public Map getValues(@NonNull String annotation) { ArgumentUtils.requireNonNull("annotation", annotation); - if (allAnnotations != null && StringUtils.isNotEmpty(annotation)) { - Map values = allAnnotations.get(annotation); + if (allAnnotations == null || StringUtils.isEmpty(annotation)) { + return Collections.emptyMap(); + } + Map values = allAnnotations.get(annotation); + if (values != null) { + return Collections.unmodifiableMap(values); + } + if (allStereotypes != null) { + values = allStereotypes.get(annotation); if (values != null) { return Collections.unmodifiableMap(values); - } else if (allStereotypes != null) { - values = allStereotypes.get(annotation); - if (values != null) { - return Collections.unmodifiableMap(values); - } } } return Collections.emptyMap(); } + @NonNull @Override - public @NonNull Optional getDefaultValue(@NonNull String annotation, @NonNull String member, @NonNull Argument requiredType) { + public Optional getDefaultValue(@NonNull String annotation, @NonNull String member, @NonNull Argument requiredType) { ArgumentUtils.requireNonNull("annotation", annotation); ArgumentUtils.requireNonNull("member", member); ArgumentUtils.requireNonNull("requiredType", requiredType); // Note this method should never reference the "annotationDefaultValues" field, which is used only at compile time Map defaultValues = getDefaultValues(annotation); - if (defaultValues.containsKey(member)) { - return ConversionService.SHARED.convert(defaultValues.get(member), requiredType); + Object value = defaultValues.get(member); + if (value != null) { + return ConversionService.SHARED.convert(value, requiredType); } return Optional.empty(); } @@ -1426,44 +1383,32 @@ public AnnotationMetadata copyAnnotationMetadata() { @Override public DefaultAnnotationMetadata clone() { DefaultAnnotationMetadata cloned = new DefaultAnnotationMetadata( - declaredAnnotations != null ? cloneMapOfMapValue(declaredAnnotations) : null, - declaredStereotypes != null ? cloneMapOfMapValue(declaredStereotypes) : null, - allStereotypes != null ? cloneMapOfMapValue(allStereotypes) : null, - allAnnotations != null ? cloneMapOfMapValue(allAnnotations) : null, - annotationsByStereotype != null ? cloneMapOfListValue(annotationsByStereotype) : null, - hasPropertyExpressions + declaredAnnotations != null ? cloneMapOfMapValue(declaredAnnotations) : null, + declaredStereotypes != null ? cloneMapOfMapValue(declaredStereotypes) : null, + allStereotypes != null ? cloneMapOfMapValue(allStereotypes) : null, + allAnnotations != null ? cloneMapOfMapValue(allAnnotations) : null, + annotationsByStereotype != null ? cloneMapOfListValue(annotationsByStereotype) : null, + hasPropertyExpressions ); - if (annotationRepeatableContainer != null) { - cloned.annotationRepeatableContainer = new HashMap<>(annotationRepeatableContainer); - } - if (sourceRetentionAnnotations != null) { - cloned.sourceRetentionAnnotations = new HashSet<>(sourceRetentionAnnotations); - } - if (annotationDefaultValues != null) { - cloned.annotationDefaultValues = cloneMapOfMapValue(annotationDefaultValues); - } - if (sourceAnnotationDefaultValues != null) { - cloned.sourceAnnotationDefaultValues = cloneMapOfMapValue(sourceAnnotationDefaultValues); - } return cloned; } protected final Map> cloneMapOfMapValue(Map> toClone) { return toClone.entrySet().stream() - .map(e -> new AbstractMap.SimpleEntry<>(e.getKey(), cloneMap(e.getValue()))) - .collect(Collectors.toMap(AbstractMap.SimpleEntry::getKey, AbstractMap.SimpleEntry::getValue, (a, b) -> a, LinkedHashMap::new)); + .map(e -> new AbstractMap.SimpleEntry<>(e.getKey(), cloneMap(e.getValue()))) + .collect(Collectors.toMap(AbstractMap.SimpleEntry::getKey, AbstractMap.SimpleEntry::getValue, (a, b) -> a, LinkedHashMap::new)); } protected final Map> cloneMapOfListValue(Map> toClone) { return toClone.entrySet().stream() - .map(e -> new AbstractMap.SimpleEntry<>(e.getKey(), new ArrayList<>(e.getValue()))) - .collect(Collectors.toMap(AbstractMap.SimpleEntry::getKey, AbstractMap.SimpleEntry::getValue, (a, b) -> a, LinkedHashMap::new)); + .map(e -> new AbstractMap.SimpleEntry<>(e.getKey(), new ArrayList<>(e.getValue()))) + .collect(Collectors.toMap(AbstractMap.SimpleEntry::getKey, AbstractMap.SimpleEntry::getValue, (a, b) -> a, LinkedHashMap::new)); } protected final Map cloneMap(Map map) { Map newMap; - if (map instanceof LinkedHashMap) { - newMap = (Map) ((LinkedHashMap) map).clone(); + if (map instanceof LinkedHashMap linkedHashMap) { + newMap = (Map) linkedHashMap.clone(); } else { newMap = new LinkedHashMap<>(map); } @@ -1476,90 +1421,6 @@ protected final Map cloneMap(Map map) { return new HashMap<>(newMap); } - /** - * Adds an annotation and its member values, if the annotation already exists the data will be merged with existing - * values replaced. - * - * @param annotation The annotation - * @param values The values - */ - @SuppressWarnings("WeakerAccess") - protected void addAnnotation(String annotation, Map values) { - addAnnotation(annotation, values, RetentionPolicy.RUNTIME); - } - - - /** - * Adds an annotation and its member values, if the annotation already exists the data will be merged with existing - * values replaced. - * - * @param annotation The annotation - * @param values The values - * @param retentionPolicy The retention policy - */ - @SuppressWarnings("WeakerAccess") - protected void addAnnotation(String annotation, Map values, RetentionPolicy retentionPolicy) { - if (annotation != null) { - if (isRepeatableAnnotationContainer(annotation)) { - Object v = values.get(AnnotationMetadata.VALUE_MEMBER); - if (v instanceof io.micronaut.core.annotation.AnnotationValue[] annotationValues) { - for (io.micronaut.core.annotation.AnnotationValue annotationValue : annotationValues) { - addRepeatable(annotation, annotationValue); - } - } else if (v instanceof Iterable iterable) { - for (Object o : iterable) { - if (o instanceof io.micronaut.core.annotation.AnnotationValue annotationValue) { - addRepeatable(annotation, annotationValue); - } - } - } - } else { - Map> allAnnotations = getAllAnnotations(); - addAnnotation(annotation, values, null, allAnnotations, false, retentionPolicy); - } - } - } - - /** - * Adds an annotation directly declared on the element and its member values, if the annotation already exists the - * data will be merged with existing values replaced. - * - * @param annotation The annotation - * @param values The values - */ - protected final void addDefaultAnnotationValues(String annotation, Map values) { - addDefaultAnnotationValues(annotation, values, RetentionPolicy.RUNTIME); - } - - /** - * Adds an annotation directly declared on the element and its member values, if the annotation already exists the - * data will be merged with existing values replaced. - * - * @param annotation The annotation - * @param values The values - * @param retentionPolicy The retention policy - */ - protected final void addDefaultAnnotationValues(String annotation, Map values, RetentionPolicy retentionPolicy) { - if (annotation == null) { - return; - } - Map> annotationDefaults; - if (retentionPolicy == RetentionPolicy.RUNTIME) { - annotationDefaults = this.annotationDefaultValues; - if (annotationDefaults == null) { - this.annotationDefaultValues = new LinkedHashMap<>(); - annotationDefaults = this.annotationDefaultValues; - } - } else { - annotationDefaults = this.sourceAnnotationDefaultValues; - if (annotationDefaults == null) { - this.sourceAnnotationDefaultValues = new LinkedHashMap<>(); - annotationDefaults = this.sourceAnnotationDefaultValues; - } - } - putValues(annotation, values, annotationDefaults); - } - /** * Registers annotation default values. Used by generated byte code. DO NOT REMOVE. * @@ -1610,273 +1471,6 @@ public static void registerRepeatableAnnotations(Map repeatableA AnnotationMetadataSupport.registerRepeatableAnnotations(repeatableAnnotations); } - /** - * Adds a repeatable annotation value. If a value already exists will be added - * - * @param annotationName The annotation name - * @param annotationValue The annotation value - */ - protected void addRepeatable(String annotationName, io.micronaut.core.annotation.AnnotationValue annotationValue) { - addRepeatable(annotationName, annotationValue, annotationValue.getRetentionPolicy()); - } - - /** - * Adds a repeatable annotation value. If a value already exists will be added - * - * @param annotationName The annotation name - * @param annotationValue The annotation value - * @param retentionPolicy The retention policy - */ - protected void addRepeatable(String annotationName, io.micronaut.core.annotation.AnnotationValue annotationValue, RetentionPolicy retentionPolicy) { - if (StringUtils.isNotEmpty(annotationName) && annotationValue != null) { - Map> allAnnotations = getAllAnnotations(); - - addRepeatableInternal(annotationName, annotationValue, allAnnotations, retentionPolicy); - } - } - - /** - * Adds a repeatable stereotype value. If a value already exists will be added - * - * @param parents The parent annotations - * @param stereotype The annotation name - * @param annotationValue The annotation value - */ - protected void addRepeatableStereotype(List parents, String stereotype, io.micronaut.core.annotation.AnnotationValue annotationValue) { - Map> allStereotypes = getAllStereotypes(); - List annotationList = getAnnotationsByStereotypeInternal(stereotype); - for (String parentAnnotation : parents) { - if (!annotationList.contains(parentAnnotation)) { - annotationList.add(parentAnnotation); - } - } - - addRepeatableInternal(stereotype, annotationValue, allStereotypes, RetentionPolicy.RUNTIME); - } - - /** - * Adds a repeatable declared stereotype value. If a value already exists will be added - * - * @param parents The parent annotations - * @param stereotype The annotation name - * @param annotationValue The annotation value - */ - protected void addDeclaredRepeatableStereotype(List parents, String stereotype, io.micronaut.core.annotation.AnnotationValue annotationValue) { - Map> declaredStereotypes = getDeclaredStereotypesInternal(); - List annotationList = getAnnotationsByStereotypeInternal(stereotype); - for (String parentAnnotation : parents) { - if (!annotationList.contains(parentAnnotation)) { - annotationList.add(parentAnnotation); - } - } - - addRepeatableInternal(stereotype, annotationValue, declaredStereotypes, RetentionPolicy.RUNTIME); - addRepeatableInternal(stereotype, annotationValue, getAllStereotypes(), RetentionPolicy.RUNTIME); - } - - /** - * Adds a repeatable annotation value. If a value already exists will be added - * - * @param annotationName The annotation name - * @param annotationValue The annotation value - */ - protected void addDeclaredRepeatable(String annotationName, io.micronaut.core.annotation.AnnotationValue annotationValue) { - addDeclaredRepeatable(annotationName, annotationValue, annotationValue.getRetentionPolicy()); - } - - /** - * Adds a repeatable annotation value. If a value already exists will be added - * - * @param annotationName The annotation name - * @param annotationValue The annotation value - * @param retentionPolicy The retention policy - */ - protected void addDeclaredRepeatable(String annotationName, io.micronaut.core.annotation.AnnotationValue annotationValue, RetentionPolicy retentionPolicy) { - if (StringUtils.isNotEmpty(annotationName) && annotationValue != null) { - Map> allAnnotations = getDeclaredAnnotationsInternal(); - - addRepeatableInternal(annotationName, annotationValue, allAnnotations, retentionPolicy); - - addRepeatable(annotationName, annotationValue); - } - } - - /** - * Adds a stereotype and its member values, if the annotation already exists the data will be merged with existing - * values replaced. - * - * @param parentAnnotations The parent annotations - * @param stereotype The annotation - * @param values The values - */ - @SuppressWarnings("WeakerAccess") - protected final void addStereotype(List parentAnnotations, String stereotype, Map values) { - addStereotype(parentAnnotations, stereotype, values, RetentionPolicy.RUNTIME); - } - - - /** - * Adds a stereotype and its member values, if the annotation already exists the data will be merged with existing - * values replaced. - * - * @param parentAnnotations The parent annotations - * @param stereotype The annotation - * @param values The values - * @param retentionPolicy The retention policy - */ - @SuppressWarnings("WeakerAccess") - protected final void addStereotype(List parentAnnotations, String stereotype, Map values, RetentionPolicy retentionPolicy) { - if (stereotype != null) { - if (isRepeatableAnnotationContainer(stereotype)) { - Object v = values.get(AnnotationMetadata.VALUE_MEMBER); - if (v instanceof io.micronaut.core.annotation.AnnotationValue[] annotationValues) { - for (io.micronaut.core.annotation.AnnotationValue annotationValue : annotationValues) { - addRepeatableStereotype(parentAnnotations, stereotype, annotationValue); - } - } else if (v instanceof Iterable iterable) { - for (Object o : iterable) { - if (o instanceof io.micronaut.core.annotation.AnnotationValue annotationValue) { - addRepeatableStereotype(parentAnnotations, stereotype, annotationValue); - } - } - } - } else { - Map> allStereotypes = getAllStereotypes(); - List annotationList = getAnnotationsByStereotypeInternal(stereotype); - if (!parentAnnotations.isEmpty()) { - final String parentAnnotation = CollectionUtils.last(parentAnnotations); - if (!annotationList.contains(parentAnnotation)) { - annotationList.add(parentAnnotation); - } - } - - // add to stereotypes - addAnnotation( - stereotype, - values, - null, - allStereotypes, - false, - retentionPolicy - ); - } - } - } - - /** - * Adds a stereotype and its member values, if the annotation already exists the data will be merged with existing - * values replaced. - * - * @param parentAnnotations The parent annotations - * @param stereotype The annotation - * @param values The values - */ - @SuppressWarnings("WeakerAccess") - protected void addDeclaredStereotype(List parentAnnotations, String stereotype, Map values) { - addDeclaredStereotype(parentAnnotations, stereotype, values, RetentionPolicy.RUNTIME); - } - - /** - * Adds a stereotype and its member values, if the annotation already exists the data will be merged with existing - * values replaced. - * - * @param parentAnnotations The parent annotations - * @param stereotype The annotation - * @param values The values - * @param retentionPolicy The retention policy - */ - @SuppressWarnings("WeakerAccess") - protected void addDeclaredStereotype(List parentAnnotations, String stereotype, Map values, RetentionPolicy retentionPolicy) { - if (stereotype != null) { - if (isRepeatableAnnotationContainer(stereotype)) { - Object v = values.get(AnnotationMetadata.VALUE_MEMBER); - if (v instanceof io.micronaut.core.annotation.AnnotationValue[] annotationValues) { - for (io.micronaut.core.annotation.AnnotationValue annotationValue : annotationValues) { - addDeclaredRepeatableStereotype(parentAnnotations, stereotype, annotationValue); - } - } else if (v instanceof Iterable iterable) { - for (Object o : iterable) { - if (o instanceof io.micronaut.core.annotation.AnnotationValue annotationValue) { - addDeclaredRepeatableStereotype(parentAnnotations, stereotype, annotationValue); - } - } - } - } else { - Map> declaredStereotypes = getDeclaredStereotypesInternal(); - Map> allStereotypes = getAllStereotypes(); - List annotationList = getAnnotationsByStereotypeInternal(stereotype); - if (!parentAnnotations.isEmpty()) { - final String parentAnnotation = CollectionUtils.last(parentAnnotations); - if (!annotationList.contains(parentAnnotation)) { - annotationList.add(parentAnnotation); - } - } - - addAnnotation( - stereotype, - values, - declaredStereotypes, - allStereotypes, - true, - retentionPolicy - ); - } - - } - } - - /** - * Adds an annotation directly declared on the element and its member values, if the annotation already exists the - * data will be merged with existing values replaced. - * - * @param annotation The annotation - * @param values The values - */ - protected void addDeclaredAnnotation(String annotation, Map values) { - addDeclaredAnnotation(annotation, values, RetentionPolicy.RUNTIME); - } - - /** - * Adds an annotation directly declared on the element and its member values, if the annotation already exists the - * data will be merged with existing values replaced. - * - * @param annotation The annotation - * @param values The values - * @param retentionPolicy The retention policy - */ - protected void addDeclaredAnnotation(String annotation, Map values, RetentionPolicy retentionPolicy) { - if (annotation != null) { - boolean hasOtherMembers = false; - boolean repeatableAnnotationContainer = isRepeatableAnnotationContainer(annotation); - if (isRepeatableAnnotationContainer(annotation)) { - for (Map.Entry entry : values.entrySet()) { - if (entry.getKey().equals(AnnotationMetadata.VALUE_MEMBER)) { - Object v = entry.getValue(); - if (v instanceof io.micronaut.core.annotation.AnnotationValue[] annotationValues) { - for (io.micronaut.core.annotation.AnnotationValue annotationValue : annotationValues) { - addDeclaredRepeatable(annotation, annotationValue); - } - } else if (v instanceof Iterable iterable) { - for (Object o : iterable) { - if (o instanceof io.micronaut.core.annotation.AnnotationValue annotationValue) { - addDeclaredRepeatable(annotation, annotationValue); - } - } - } - } else { - hasOtherMembers = true; - } - } - } - - if (!repeatableAnnotationContainer || hasOtherMembers) { - Map> declaredAnnotations = getDeclaredAnnotationsInternal(); - Map> allAnnotations = getAllAnnotations(); - addAnnotation(annotation, values, declaredAnnotations, allAnnotations, true, retentionPolicy); - } - } - } - /** * Dump the values. */ @@ -1894,8 +1488,8 @@ private List List List> resolveRepeatableAnnotations(String repeatableTypeName, Map> sourceStereotypes, Map> sourceAnnotations) { - if (hasStereotype(repeatableTypeName)) { - List> results = new ArrayList<>(); - if (sourceAnnotations != null) { - Map values = sourceAnnotations.get(repeatableTypeName); - addAnnotationValuesFromData(results, values); - } - - if (sourceStereotypes != null) { - Map values = sourceStereotypes.get(repeatableTypeName); - addAnnotationValuesFromData(results, values); - } - - return results; - } - return null; - } - - private void addAnnotation(String annotation, - Map values, - Map> declaredAnnotations, - Map> allAnnotations, - boolean isDeclared, - RetentionPolicy retentionPolicy) { - if (isDeclared && declaredAnnotations != null) { - putValues(annotation, values, declaredAnnotations); - } - putValues(annotation, values, allAnnotations); - - // Annotations with retention CLASS need not be retained at run time - if (retentionPolicy == RetentionPolicy.SOURCE || retentionPolicy == RetentionPolicy.CLASS) { - addSourceRetentionAnnotation(annotation); - } - } - - private void addSourceRetentionAnnotation(String annotation) { - if (sourceRetentionAnnotations == null) { - sourceRetentionAnnotations = new HashSet<>(5); - } - sourceRetentionAnnotations.add(annotation); - } - - private void putValues(String annotation, Map values, Map> currentAnnotationValues) { - Map existing = currentAnnotationValues.get(annotation); - boolean hasValues = CollectionUtils.isNotEmpty(values); - if (existing != null && hasValues) { - if (existing.isEmpty()) { - existing = new LinkedHashMap<>(); - currentAnnotationValues.put(annotation, existing); - } - for (CharSequence key : values.keySet()) { - if (!existing.containsKey(key)) { - existing.put(key, values.get(key)); - } - } - } else { - if (!hasValues) { - existing = existing == null ? new LinkedHashMap<>(3) : existing; - } else { - existing = new LinkedHashMap<>(values.size()); - existing.putAll(values); - } - currentAnnotationValues.put(annotation, existing); + if (!hasStereotype(repeatableTypeName)) { + return null; } - } - - @SuppressWarnings("MagicNumber") - private Map> getAllStereotypes() { - Map> stereotypes = this.allStereotypes; - if (stereotypes == null) { - stereotypes = new HashMap<>(3); - this.allStereotypes = stereotypes; - } - return stereotypes; - } - - @SuppressWarnings("MagicNumber") - private Map> getDeclaredStereotypesInternal() { - Map> stereotypes = this.declaredStereotypes; - if (stereotypes == null) { - stereotypes = new HashMap<>(3); - this.declaredStereotypes = stereotypes; - } - return stereotypes; - } - - @SuppressWarnings("MagicNumber") - private Map> getAllAnnotations() { - Map> annotations = this.allAnnotations; - if (annotations == null) { - annotations = new HashMap<>(3); - this.allAnnotations = annotations; - } - return annotations; - } - - @SuppressWarnings("MagicNumber") - private Map> getDeclaredAnnotationsInternal() { - Map> annotations = this.declaredAnnotations; - if (annotations == null) { - annotations = new HashMap<>(3); - this.declaredAnnotations = annotations; + List> results = new ArrayList<>(); + if (sourceAnnotations != null) { + Map values = sourceAnnotations.get(repeatableTypeName); + addAnnotationValuesFromData(results, values); } - return annotations; - } - - private List getAnnotationsByStereotypeInternal(String stereotype) { - return getAnnotationsByStereotypeInternal().computeIfAbsent(stereotype, s -> new ArrayList<>()); - } - - @SuppressWarnings("MagicNumber") - private Map> getAnnotationsByStereotypeInternal() { - Map> annotations = this.annotationsByStereotype; - if (annotations == null) { - annotations = new HashMap<>(3); - this.annotationsByStereotype = annotations; + if (sourceStereotypes != null) { + Map values = sourceStereotypes.get(repeatableTypeName); + addAnnotationValuesFromData(results, values); } - return annotations; + return results; } - private @Nullable - Object getRawSingleValue(@NonNull String annotation, @NonNull String member, @Nullable Function valueMapper) { + @Nullable + private Object getRawSingleValue(@NonNull String annotation, @NonNull String member, @Nullable Function valueMapper) { Object rawValue = getRawValue(annotation, member); if (rawValue != null) { if (rawValue.getClass().isArray()) { @@ -2033,8 +1523,8 @@ Object getRawSingleValue(@NonNull String annotation, @NonNull String member, @Nu if (len > 0) { rawValue = Array.get(rawValue, 0); } - } else if (rawValue instanceof Iterable) { - Iterator i = ((Iterable) rawValue).iterator(); + } else if (rawValue instanceof Iterable iterable) { + Iterator i = iterable.iterator(); if (i.hasNext()) { rawValue = i.next(); } @@ -2049,421 +1539,29 @@ Object getRawSingleValue(@NonNull String annotation, @NonNull String member, @Nu @Nullable private Object getRawValue(@NonNull String annotation, @NonNull String member) { - Object rawValue = null; - if (allAnnotations != null && StringUtils.isNotEmpty(annotation)) { - Map values = allAnnotations.get(annotation); - if (values != null) { - rawValue = values.get(member); - } else if (allStereotypes != null) { - values = allStereotypes.get(annotation); - if (values != null) { - rawValue = values.get(member); - } - } - } - return rawValue; - } - - private void addRepeatableInternal( - String annotationName, - io.micronaut.core.annotation.AnnotationValue annotationValue, - Map> allAnnotations, - RetentionPolicy retentionPolicy) { - addRepeatableInternal(annotationName, AnnotationMetadata.VALUE_MEMBER, annotationValue, allAnnotations, retentionPolicy); - } - - private void addRepeatableInternal( - String annotationName, - String member, - io.micronaut.core.annotation.AnnotationValue annotationValue, - Map> allAnnotations, - RetentionPolicy retentionPolicy) { - if (annotationRepeatableContainer == null) { - annotationRepeatableContainer = new HashMap<>(2); - } - annotationRepeatableContainer.put(annotationValue.getAnnotationName(), annotationName); - if (retentionPolicy == RetentionPolicy.SOURCE) { - addSourceRetentionAnnotation(annotationName); - } - - Map values = allAnnotations.computeIfAbsent(annotationName, s -> new HashMap<>()); - Object v = values.get(member); - if (v != null) { - if (v.getClass().isArray()) { - Object[] array = (Object[]) v; - Set newValues = new LinkedHashSet(array.length + 1); - newValues.addAll(Arrays.asList(array)); - newValues.add(annotationValue); - values.put(member, newValues); - } else if (v instanceof Collection) { - ((Collection) v).add(annotationValue); - } - } else { - Set newValues = new LinkedHashSet<>(2); - newValues.add(annotationValue); - values.put(member, newValues); - } - } - - /** - *

Sets a member of the given {@link AnnotationMetadata} return a new annotation metadata instance without - * mutating the existing.

- * - *

WARNING: for internal use only be the framework

- * - * @param annotationMetadata The metadata - * @param annotationName The annotation name - * @param member The member - * @param value The value - * @return The metadata - */ - @Internal - public static AnnotationMetadata mutateMember( - AnnotationMetadata annotationMetadata, - String annotationName, - String member, - Object value) { - - return mutateMember(annotationMetadata, annotationName, Collections.singletonMap(member, value)); - } - - /** - * Include the annotation metadata from the other instance of {@link DefaultAnnotationMetadata}. - * - * @param annotationMetadata The annotation metadata - * @since 4.0.0 - */ - @Internal - protected void addAnnotationMetadata(DefaultAnnotationMetadata annotationMetadata) { - if (annotationMetadata.declaredAnnotations != null && !annotationMetadata.declaredAnnotations.isEmpty()) { - if (declaredAnnotations == null) { - declaredAnnotations = new LinkedHashMap<>(); - } - for (Map.Entry> entry : annotationMetadata.declaredAnnotations.entrySet()) { - putValues(entry.getKey(), entry.getValue(), declaredAnnotations); - } - } - if (annotationMetadata.declaredStereotypes != null && !annotationMetadata.declaredStereotypes.isEmpty()) { - if (declaredStereotypes == null) { - declaredStereotypes = new LinkedHashMap<>(); - } - for (Map.Entry> entry : annotationMetadata.declaredStereotypes.entrySet()) { - putValues(entry.getKey(), entry.getValue(), declaredStereotypes); - } - } - if (annotationMetadata.allStereotypes != null && !annotationMetadata.allStereotypes.isEmpty()) { - if (allStereotypes == null) { - allStereotypes = new LinkedHashMap<>(); - } - for (Map.Entry> entry : annotationMetadata.allStereotypes.entrySet()) { - putValues(entry.getKey(), entry.getValue(), allStereotypes); - } - } - if (annotationMetadata.allAnnotations != null && !annotationMetadata.allAnnotations.isEmpty()) { - if (allAnnotations == null) { - allAnnotations = new LinkedHashMap<>(); - } - for (Map.Entry> entry : annotationMetadata.allAnnotations.entrySet()) { - putValues(entry.getKey(), entry.getValue(), allAnnotations); - } + if (allAnnotations == null || StringUtils.isEmpty(annotation)) { + return null; } - Map> source = annotationMetadata.annotationsByStereotype; - if (source != null && !source.isEmpty()) { - if (annotationsByStereotype == null) { - annotationsByStereotype = new LinkedHashMap<>(); - } - for (Map.Entry> entry : source.entrySet()) { - String ann = entry.getKey(); - List prevValues = annotationsByStereotype.get(ann); - if (prevValues == null) { - annotationsByStereotype.put(ann, new ArrayList<>(entry.getValue())); - } else { - Set prevValuesSet = new LinkedHashSet<>(prevValues); - prevValuesSet.addAll(entry.getValue()); - annotationsByStereotype.put(ann, new ArrayList<>(prevValuesSet)); - } - } + Map values = allAnnotations.get(annotation); + if (values != null) { + return values.get(member); } - if (annotationMetadata.annotationRepeatableContainer != null) { - if (annotationRepeatableContainer == null) { - annotationRepeatableContainer = new LinkedHashMap<>(annotationMetadata.annotationRepeatableContainer); - } else { - annotationRepeatableContainer.putAll(annotationMetadata.annotationRepeatableContainer); - } - } - if (annotationMetadata.sourceRetentionAnnotations != null) { - if (sourceRetentionAnnotations == null) { - sourceRetentionAnnotations = new HashSet<>(annotationMetadata.sourceRetentionAnnotations); - } else { - sourceRetentionAnnotations.addAll(annotationMetadata.sourceRetentionAnnotations); - } - } - if (annotationMetadata.annotationDefaultValues != null) { - if (annotationDefaultValues == null) { - annotationDefaultValues = new LinkedHashMap<>(annotationMetadata.annotationDefaultValues); - } else { - // No need to merge values - annotationDefaultValues.putAll(annotationMetadata.annotationDefaultValues); - } - } - if (annotationMetadata.sourceAnnotationDefaultValues != null) { - if (sourceAnnotationDefaultValues == null) { - sourceAnnotationDefaultValues = new LinkedHashMap<>(annotationMetadata.sourceAnnotationDefaultValues); - } else { - // No need to merge values - sourceAnnotationDefaultValues.putAll(annotationMetadata.sourceAnnotationDefaultValues); - } - } - } - - /** - * Contributes defaults to the given target. - * - *

WARNING: for internal use only be the framework

- * - * @param target The target - * @param source The source - */ - @Internal - public static void contributeDefaults(AnnotationMetadata target, AnnotationMetadata source) { - source = source.getTargetAnnotationMetadata(); - if (source instanceof AnnotationMetadataHierarchy) { - source = ((AnnotationMetadataHierarchy) source).merge(); - } - if (target instanceof DefaultAnnotationMetadata damTarget && source instanceof DefaultAnnotationMetadata damSource) { - final Map> existingDefaults = damTarget.annotationDefaultValues; - final Map> additionalDefaults = damSource.annotationDefaultValues; - if (existingDefaults != null) { - if (additionalDefaults != null) { - existingDefaults.putAll( - additionalDefaults - ); - } - } else { - if (additionalDefaults != null) { - additionalDefaults.forEach(damTarget::addDefaultAnnotationValues); - } - } - } - // We don't need to contribute the default source annotation - contributeRepeatable(target, source); - } - - /** - * Contributes repeatable annotation metadata to the given target. - * - *

WARNING: for internal use only be the framework

- * - * @param target The target - * @param source The source - */ - @Internal - public static void contributeRepeatable(AnnotationMetadata target, AnnotationMetadata source) { - source = source.getTargetAnnotationMetadata(); - if (source instanceof AnnotationMetadataHierarchy) { - source = ((AnnotationMetadataHierarchy) source).merge(); - } - if (target instanceof DefaultAnnotationMetadata && source instanceof DefaultAnnotationMetadata) { - DefaultAnnotationMetadata damTarget = (DefaultAnnotationMetadata) target; - DefaultAnnotationMetadata damSource = (DefaultAnnotationMetadata) source; - if (damSource.annotationRepeatableContainer != null && !damSource.annotationRepeatableContainer.isEmpty()) { - if (damTarget.annotationRepeatableContainer == null) { - damTarget.annotationRepeatableContainer = new HashMap<>(damSource.annotationRepeatableContainer); - } else { - damTarget.annotationRepeatableContainer.putAll(damSource.annotationRepeatableContainer); - } - } - } - } - - /** - *

Sets a member of the given {@link AnnotationMetadata} return a new annotation metadata instance without - * mutating the existing.

- * - *

WARNING: for internal use only be the framework

- * - * @param annotationMetadata The metadata - * @param annotationName The annotation name - * @param members The key/value set of members and values - * @return The metadata - */ - @Internal - public static AnnotationMetadata mutateMember( - AnnotationMetadata annotationMetadata, - String annotationName, - Map members) { - if (StringUtils.isEmpty(annotationName)) { - throw new IllegalArgumentException("Argument [annotationName] cannot be blank"); - } - if (!members.isEmpty()) { - for (Map.Entry entry : members.entrySet()) { - if (StringUtils.isEmpty(entry.getKey())) { - throw new IllegalArgumentException("Argument [members] cannot have a blank key"); - } - if (entry.getValue() == null) { - throw new IllegalArgumentException("Argument [members] cannot have a null value. Key [" + entry.getKey() + "]"); - } - } - } - if (!(annotationMetadata instanceof DefaultAnnotationMetadata)) { - MutableAnnotationMetadata mutableAnnotationMetadata = new MutableAnnotationMetadata(); - mutableAnnotationMetadata.addDeclaredAnnotation(annotationName, members); - return mutableAnnotationMetadata; - } else { - DefaultAnnotationMetadata defaultMetadata = (DefaultAnnotationMetadata) annotationMetadata; - - defaultMetadata = defaultMetadata.clone(); - - defaultMetadata - .addDeclaredAnnotation(annotationName, members); - - return defaultMetadata; - } - } - - /** - * Removes an annotation for the given predicate. - * - * @param predicate The predicate - * @param The annotation - */ - protected void removeAnnotationIf( - @NonNull Predicate> predicate) { - removeAnnotationsIf(predicate, this.declaredAnnotations); - removeAnnotationsIf(predicate, this.allAnnotations); - } - - private void removeAnnotationsIf(@NonNull Predicate> predicate, Map> annotations) { - if (annotations != null) { - annotations.entrySet().removeIf(entry -> { - final String annotationName = entry.getKey(); - if (predicate.test(newAnnotationValue(annotationName, entry.getValue()))) { - removeFromStereotypes(annotationName, annotations); - return true; - } - return false; - }); - } - } - - /** - * Removes an annotation for the given annotation type. - * - * @param annotationType The annotation type - * @since 3.0.0 - */ - protected void removeAnnotation(String annotationType) { - if (annotationType != null) { - if (annotationDefaultValues != null) { - this.annotationDefaultValues.remove(annotationType); - } - if (allAnnotations != null) { - this.allAnnotations.remove(annotationType); - } - final Map> declaredAnnotations = this.declaredAnnotations; - if (declaredAnnotations != null) { - declaredAnnotations.remove(annotationType); - removeFromStereotypes(annotationType, declaredAnnotations); - } - if (this.annotationRepeatableContainer != null) { - this.annotationRepeatableContainer.remove(annotationType); + if (allStereotypes != null) { + values = allStereotypes.get(annotation); + if (values != null) { + return values.get(member); } } + return null; } /** - * Removes a stereotype annotation for the given annotation type. - * - * @param annotationType The annotation type - * @since 3.0.0 + * Find annotation's repeatable container. + * @param annotation The annotation + * @return the repeatable container or null */ - protected void removeStereotype(String annotationType) { - if (annotationType != null) { - if (annotationsByStereotype != null && annotationsByStereotype.remove(annotationType) != null) { - if (allStereotypes != null) { - this.allStereotypes.remove(annotationType); - } - if (declaredStereotypes != null) { - this.declaredStereotypes.remove(annotationType); - } - - final Iterator>> i = annotationsByStereotype.entrySet().iterator(); - while (i.hasNext()) { - Map.Entry> entry = i.next(); - final List value = entry.getValue(); - if (value.remove(annotationType)) { - if (value.isEmpty()) { - i.remove(); - } - } - } - } - } - } - - private void removeFromStereotypes(String annotationType, Map> declaredAnnotations) { - if (annotationsByStereotype != null) { - final Iterator>> i = annotationsByStereotype.entrySet().iterator(); - Set toBeRemoved = CollectionUtils.setOf(annotationType); - while (i.hasNext()) { - final Map.Entry> entry = i.next(); - final String stereotypeName = entry.getKey(); - final List value = entry.getValue(); - if (value.removeAll(toBeRemoved)) { - if (value.isEmpty()) { - toBeRemoved.add(stereotypeName); - i.remove(); - if (allStereotypes != null) { - this.allStereotypes.remove(stereotypeName); - } - if (declaredStereotypes != null) { - this.declaredStereotypes.remove(stereotypeName); - } - if (annotationDefaultValues != null) { - annotationDefaultValues.remove(stereotypeName); - } - } - - if (AnnotationUtil.ANN_AROUND.equals(stereotypeName) || AnnotationUtil.ANN_INTRODUCTION.equals(stereotypeName) || AnnotationUtil.ANN_AROUND_CONSTRUCT.equals(stereotypeName)) { - // purge from interceptor binding - purgeInterceptorBindings(declaredAnnotations, toBeRemoved); - purgeInterceptorBindings(this.allAnnotations, toBeRemoved); - } - } - } - } - } - - private void purgeInterceptorBindings(Map> declaredAnnotations, Set toBeRemoved) { - if (declaredAnnotations != null) { - final Map v = declaredAnnotations.get(AnnotationUtil.ANN_INTERCEPTOR_BINDINGS); - if (v != null) { - final Object o = v.get(AnnotationMetadata.VALUE_MEMBER); - if (o instanceof Collection) { - Collection> col = (Collection) o; - col.removeIf(av -> Arrays.stream(av.annotationClassValues(AnnotationMetadata.VALUE_MEMBER)) - .anyMatch(acv -> toBeRemoved.contains(acv.getName()))); - - if (col.isEmpty()) { - declaredAnnotations.remove(AnnotationUtil.ANN_INTERCEPTOR_BINDINGS); - } - } - } - } - } - - private boolean isRepeatableAnnotationContainer(String annotation) { - // This method is only used during the compilation time so we don't bother checking AnnotationMetadataSupport - return annotationRepeatableContainer != null && annotationRepeatableContainer.values().contains(annotation); - } - - private String findRepeatableAnnotationContainerInternal(String annotation) { - if (annotationRepeatableContainer != null) { - String repeatedName = annotationRepeatableContainer.get(annotation); - if (repeatedName != null) { - return repeatedName; - } - } + @Nullable + protected String findRepeatableAnnotationContainerInternal(@NonNull String annotation) { return AnnotationMetadataSupport.getRepeatableAnnotation(annotation); } } diff --git a/inject/src/main/java/io/micronaut/inject/annotation/MutableAnnotationMetadata.java b/inject/src/main/java/io/micronaut/inject/annotation/MutableAnnotationMetadata.java index d72a8911ad0..f26a097a892 100644 --- a/inject/src/main/java/io/micronaut/inject/annotation/MutableAnnotationMetadata.java +++ b/inject/src/main/java/io/micronaut/inject/annotation/MutableAnnotationMetadata.java @@ -17,20 +17,28 @@ import io.micronaut.context.env.DefaultPropertyPlaceholderResolver; import io.micronaut.core.annotation.AnnotationMetadata; +import io.micronaut.core.annotation.AnnotationUtil; import io.micronaut.core.annotation.AnnotationValue; +import io.micronaut.core.annotation.Internal; import io.micronaut.core.annotation.NonNull; import io.micronaut.core.annotation.Nullable; import io.micronaut.core.util.CollectionUtils; +import io.micronaut.core.util.StringUtils; import java.lang.annotation.Annotation; import java.lang.annotation.RetentionPolicy; +import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; import java.util.HashMap; import java.util.HashSet; +import java.util.Iterator; import java.util.LinkedHashMap; +import java.util.LinkedHashSet; import java.util.List; import java.util.Map; -import java.util.Objects; +import java.util.Set; import java.util.function.Predicate; import java.util.stream.Collectors; @@ -43,6 +51,14 @@ public class MutableAnnotationMetadata extends DefaultAnnotationMetadata { private boolean hasPropertyExpressions = false; + @Nullable + Map> annotationDefaultValues; + @Nullable + private Set sourceRetentionAnnotations; + @Nullable + private Map> sourceAnnotationDefaultValues; + @Nullable + Map annotationRepeatableContainer; /** * Default constructor. @@ -83,12 +99,6 @@ public static MutableAnnotationMetadata of(AnnotationMetadata annotationMetadata } } - @Override - protected void addAnnotationMetadata(DefaultAnnotationMetadata annotationMetadata) { - hasPropertyExpressions |= annotationMetadata.hasPropertyExpressions(); - super.addAnnotationMetadata(annotationMetadata); - } - @Override public boolean hasPropertyExpressions() { return hasPropertyExpressions; @@ -116,6 +126,9 @@ public MutableAnnotationMetadata clone() { if (annotationDefaultValues != null) { cloned.annotationDefaultValues = cloneMapOfMapValue(annotationDefaultValues); } + if (sourceAnnotationDefaultValues != null) { + cloned.sourceAnnotationDefaultValues = cloneMapOfMapValue(sourceAnnotationDefaultValues); + } cloned.hasPropertyExpressions = hasPropertyExpressions; return cloned; } @@ -124,132 +137,931 @@ public MutableAnnotationMetadata clone() { @Override public Map getDefaultValues(@NonNull String annotation) { Map values = super.getDefaultValues(annotation); - if (values.isEmpty() && annotationDefaultValues != null) { - final Map compileTimeDefaults = annotationDefaultValues.get(annotation); - if (compileTimeDefaults != null && !compileTimeDefaults.isEmpty()) { - return compileTimeDefaults.entrySet().stream() - .collect(Collectors.toMap(e -> e.getKey().toString(), Map.Entry::getValue)); - } + if (!values.isEmpty() || annotationDefaultValues == null) { + return values; + } + final Map compileTimeDefaults = annotationDefaultValues.get(annotation); + if (compileTimeDefaults != null && !compileTimeDefaults.isEmpty()) { + return compileTimeDefaults.entrySet().stream() + .collect(Collectors.toMap(e -> e.getKey().toString(), Map.Entry::getValue)); } return values; } - @Override - public void removeAnnotationIf(@NonNull Predicate> predicate) { - super.removeAnnotationIf(predicate); + private boolean computeHasPropertyExpressions(Map values, RetentionPolicy retentionPolicy) { + return hasPropertyExpressions || values != null && retentionPolicy == RetentionPolicy.RUNTIME && hasPropertyExpressions(values); } - @Override - public void removeAnnotation(String annotationType) { - super.removeAnnotation(annotationType); + private boolean hasPropertyExpressions(Map values) { + if (CollectionUtils.isEmpty(values)) { + return false; + } + return values.values().stream().anyMatch(v -> { + if (v instanceof CharSequence) { + return v.toString().contains(DefaultPropertyPlaceholderResolver.PREFIX); + } else if (v instanceof String[] strings) { + return Arrays.stream(strings).anyMatch(s -> s.contains(DefaultPropertyPlaceholderResolver.PREFIX)); + } else if (v instanceof AnnotationValue annotationValue) { + return hasPropertyExpressions(annotationValue.getValues()); + } else if (v instanceof AnnotationValue[] annotationValues) { + if (annotationValues.length > 0) { + return Arrays.stream(annotationValues).anyMatch(av -> hasPropertyExpressions(av.getValues())); + } else { + return false; + } + } else { + return false; + } + }); } - @Override - public void removeStereotype(String annotationType) { - super.removeStereotype(annotationType); + /** + * @return The annotations that are source retention. + */ + @Internal + Set getSourceRetentionAnnotations() { + if (sourceRetentionAnnotations != null) { + return Collections.unmodifiableSet(sourceRetentionAnnotations); + } + return Collections.emptySet(); } - @Override + /** + * Adds an annotation and its member values, if the annotation already exists the data will be merged with existing + * values replaced. + * + * @param annotation The annotation + * @param values The values + */ + @SuppressWarnings("WeakerAccess") public void addAnnotation(String annotation, Map values) { - this.hasPropertyExpressions = computeHasPropertyExpressions(values, RetentionPolicy.RUNTIME); - super.addAnnotation(annotation, values); + addAnnotation(annotation, values, RetentionPolicy.RUNTIME); } - @Override + + /** + * Adds an annotation and its member values, if the annotation already exists the data will be merged with existing + * values replaced. + * + * @param annotation The annotation + * @param values The values + * @param retentionPolicy The retention policy + */ + @SuppressWarnings("WeakerAccess") public void addAnnotation(String annotation, Map values, RetentionPolicy retentionPolicy) { - this.hasPropertyExpressions = computeHasPropertyExpressions(values, retentionPolicy); - super.addAnnotation(annotation, values, retentionPolicy); + if (annotation == null) { + return; + } + if (isRepeatableAnnotationContainer(annotation)) { + Object v = values.get(AnnotationMetadata.VALUE_MEMBER); + if (v instanceof io.micronaut.core.annotation.AnnotationValue[] annotationValues) { + for (io.micronaut.core.annotation.AnnotationValue annotationValue : annotationValues) { + addRepeatable(annotation, annotationValue); + } + } else if (v instanceof Iterable iterable) { + for (Object o : iterable) { + if (o instanceof io.micronaut.core.annotation.AnnotationValue annotationValue) { + addRepeatable(annotation, annotationValue); + } + } + } + } else { + Map> allAnnotations = getAllAnnotations(); + addAnnotation(annotation, values, null, allAnnotations, false, retentionPolicy); + } } - @Override - public void addRepeatableStereotype(List parents, String stereotype, AnnotationValue annotationValue) { - Objects.requireNonNull(annotationValue, "Annotation Value cannot be null"); - this.hasPropertyExpressions = computeHasPropertyExpressions(annotationValue.getValues(), RetentionPolicy.RUNTIME); - super.addRepeatableStereotype(parents, stereotype, annotationValue); + /** + * Adds an annotation directly declared on the element and its member values, if the annotation already exists the + * data will be merged with existing values replaced. + * + * @param annotation The annotation + * @param values The values + */ + public final void addDefaultAnnotationValues(String annotation, Map values) { + addDefaultAnnotationValues(annotation, values, RetentionPolicy.RUNTIME); } - @Override - public void addDeclaredRepeatableStereotype(List parents, String stereotype, AnnotationValue annotationValue) { - Objects.requireNonNull(annotationValue, "Annotation Value cannot be null"); - this.hasPropertyExpressions = computeHasPropertyExpressions(annotationValue.getValues(), RetentionPolicy.RUNTIME); - super.addDeclaredRepeatableStereotype(parents, stereotype, annotationValue); + /** + * Adds an annotation directly declared on the element and its member values, if the annotation already exists the + * data will be merged with existing values replaced. + * + * @param annotation The annotation + * @param values The values + * @param retentionPolicy The retention policy + */ + public final void addDefaultAnnotationValues(String annotation, Map values, RetentionPolicy retentionPolicy) { + if (annotation == null) { + return; + } + Map> annotationDefaults; + if (retentionPolicy == RetentionPolicy.RUNTIME) { + annotationDefaults = this.annotationDefaultValues; + if (annotationDefaults == null) { + this.annotationDefaultValues = new LinkedHashMap<>(); + annotationDefaults = this.annotationDefaultValues; + } + } else { + annotationDefaults = this.sourceAnnotationDefaultValues; + if (annotationDefaults == null) { + this.sourceAnnotationDefaultValues = new LinkedHashMap<>(); + annotationDefaults = this.sourceAnnotationDefaultValues; + } + } + putValues(annotation, values, annotationDefaults); } - @Override - public void addDeclaredAnnotation(String annotation, Map values) { - this.hasPropertyExpressions = computeHasPropertyExpressions(values, RetentionPolicy.RUNTIME); - super.addDeclaredAnnotation(annotation, values); + /** + * Adds a repeatable annotation value. If a value already exists will be added + * + * @param annotationName The annotation name + * @param annotationValue The annotation value + */ + public void addRepeatable(String annotationName, AnnotationValue annotationValue) { + addRepeatable(annotationName, annotationValue, annotationValue.getRetentionPolicy()); } - @Override - public void addDeclaredAnnotation(String annotation, Map values, RetentionPolicy retentionPolicy) { - this.hasPropertyExpressions = computeHasPropertyExpressions(values, retentionPolicy); - super.addDeclaredAnnotation(annotation, values, retentionPolicy); + /** + * Adds a repeatable annotation value. If a value already exists will be added + * + * @param annotationName The annotation name + * @param annotationValue The annotation value + * @param retentionPolicy The retention policy + */ + public void addRepeatable(String annotationName, AnnotationValue annotationValue, RetentionPolicy retentionPolicy) { + if (StringUtils.isNotEmpty(annotationName) && annotationValue != null) { + Map> allAnnotations = getAllAnnotations(); + + addRepeatableInternal(annotationName, annotationValue, allAnnotations, retentionPolicy); + } } - @Override - public void addRepeatable(String annotationName, AnnotationValue annotationValue) { - Objects.requireNonNull(annotationValue, "Annotation Value cannot be null"); - this.hasPropertyExpressions = computeHasPropertyExpressions(annotationValue.getValues(), RetentionPolicy.RUNTIME); - super.addRepeatable(annotationName, annotationValue); + /** + * Adds a repeatable stereotype value. If a value already exists will be added + * + * @param parents The parent annotations + * @param stereotype The annotation name + * @param annotationValue The annotation value + */ + public void addRepeatableStereotype(List parents, String stereotype, AnnotationValue annotationValue) { + Map> allStereotypes = getAllStereotypes(); + List annotationList = getAnnotationsByStereotypeInternal(stereotype); + for (String parentAnnotation : parents) { + if (!annotationList.contains(parentAnnotation)) { + annotationList.add(parentAnnotation); + } + } + addRepeatableInternal(stereotype, annotationValue, allStereotypes, RetentionPolicy.RUNTIME); } - @Override - public void addRepeatable(String annotationName, AnnotationValue annotationValue, RetentionPolicy retentionPolicy) { - Objects.requireNonNull(annotationValue, "Annotation Value cannot be null"); - this.hasPropertyExpressions = computeHasPropertyExpressions(annotationValue.getValues(), retentionPolicy); - super.addRepeatable(annotationName, annotationValue, retentionPolicy); + /** + * Adds a repeatable declared stereotype value. If a value already exists will be added + * + * @param parents The parent annotations + * @param stereotype The annotation name + * @param annotationValue The annotation value + */ + public void addDeclaredRepeatableStereotype(List parents, String stereotype, AnnotationValue annotationValue) { + Map> declaredStereotypes = getDeclaredStereotypesInternal(); + List annotationList = getAnnotationsByStereotypeInternal(stereotype); + for (String parentAnnotation : parents) { + if (!annotationList.contains(parentAnnotation)) { + annotationList.add(parentAnnotation); + } + } + addRepeatableInternal(stereotype, annotationValue, declaredStereotypes, RetentionPolicy.RUNTIME); + addRepeatableInternal(stereotype, annotationValue, getAllStereotypes(), RetentionPolicy.RUNTIME); } - @Override - public void addDeclaredRepeatable(String annotationName, AnnotationValue annotationValue) { - Objects.requireNonNull(annotationValue, "Annotation Value cannot be null"); - this.hasPropertyExpressions = computeHasPropertyExpressions(annotationValue.getValues(), RetentionPolicy.RUNTIME); - super.addDeclaredRepeatable(annotationName, annotationValue); + /** + * Adds a repeatable annotation value. If a value already exists will be added + * + * @param annotationName The annotation name + * @param annotationValue The annotation value + */ + public void addDeclaredRepeatable(String annotationName, AnnotationValue annotationValue) { + addDeclaredRepeatable(annotationName, annotationValue, annotationValue.getRetentionPolicy()); } - @Override - public void addDeclaredRepeatable(String annotationName, AnnotationValue annotationValue, RetentionPolicy retentionPolicy) { - Objects.requireNonNull(annotationValue, "Annotation Value cannot be null"); - this.hasPropertyExpressions = computeHasPropertyExpressions(annotationValue.getValues(), retentionPolicy); - super.addDeclaredRepeatable(annotationName, annotationValue, retentionPolicy); + /** + * Adds a repeatable annotation value. If a value already exists will be added + * + * @param annotationName The annotation name + * @param annotationValue The annotation value + * @param retentionPolicy The retention policy + */ + public void addDeclaredRepeatable(String annotationName, AnnotationValue annotationValue, RetentionPolicy retentionPolicy) { + if (StringUtils.isNotEmpty(annotationName) && annotationValue != null) { + Map> allAnnotations = getDeclaredAnnotationsInternal(); + + addRepeatableInternal(annotationName, annotationValue, allAnnotations, retentionPolicy); + + addRepeatable(annotationName, annotationValue); + } } - @Override + /** + * Adds a stereotype and its member values, if the annotation already exists the data will be merged with existing + * values replaced. + * + * @param parentAnnotations The parent annotations + * @param stereotype The annotation + * @param values The values + */ + @SuppressWarnings("WeakerAccess") + public final void addStereotype(List parentAnnotations, String stereotype, Map values) { + addStereotype(parentAnnotations, stereotype, values, RetentionPolicy.RUNTIME); + } + + + /** + * Adds a stereotype and its member values, if the annotation already exists the data will be merged with existing + * values replaced. + * + * @param parentAnnotations The parent annotations + * @param stereotype The annotation + * @param values The values + * @param retentionPolicy The retention policy + */ + @SuppressWarnings("WeakerAccess") + public final void addStereotype(List parentAnnotations, String stereotype, Map values, RetentionPolicy retentionPolicy) { + if (stereotype == null) { + return; + } + if (isRepeatableAnnotationContainer(stereotype)) { + Object v = values.get(AnnotationMetadata.VALUE_MEMBER); + if (v instanceof io.micronaut.core.annotation.AnnotationValue[] annotationValues) { + for (io.micronaut.core.annotation.AnnotationValue annotationValue : annotationValues) { + addRepeatableStereotype(parentAnnotations, stereotype, annotationValue); + } + } else if (v instanceof Iterable iterable) { + for (Object o : iterable) { + if (o instanceof io.micronaut.core.annotation.AnnotationValue annotationValue) { + addRepeatableStereotype(parentAnnotations, stereotype, annotationValue); + } + } + } + } else { + Map> allStereotypes = getAllStereotypes(); + List annotationList = getAnnotationsByStereotypeInternal(stereotype); + if (!parentAnnotations.isEmpty()) { + final String parentAnnotation = CollectionUtils.last(parentAnnotations); + if (!annotationList.contains(parentAnnotation)) { + annotationList.add(parentAnnotation); + } + } + + // add to stereotypes + addAnnotation( + stereotype, + values, + null, + allStereotypes, + false, + retentionPolicy + ); + } + } + + /** + * Adds a stereotype and its member values, if the annotation already exists the data will be merged with existing + * values replaced. + * + * @param parentAnnotations The parent annotations + * @param stereotype The annotation + * @param values The values + */ + @SuppressWarnings("WeakerAccess") public void addDeclaredStereotype(List parentAnnotations, String stereotype, Map values) { - super.addDeclaredStereotype(parentAnnotations, stereotype, values); + addDeclaredStereotype(parentAnnotations, stereotype, values, RetentionPolicy.RUNTIME); } - @Override + /** + * Adds a stereotype and its member values, if the annotation already exists the data will be merged with existing + * values replaced. + * + * @param parentAnnotations The parent annotations + * @param stereotype The annotation + * @param values The values + * @param retentionPolicy The retention policy + */ + @SuppressWarnings("WeakerAccess") public void addDeclaredStereotype(List parentAnnotations, String stereotype, Map values, RetentionPolicy retentionPolicy) { - super.addDeclaredStereotype(parentAnnotations, stereotype, values, retentionPolicy); + if (stereotype == null) { + return; + } + if (isRepeatableAnnotationContainer(stereotype)) { + Object v = values.get(AnnotationMetadata.VALUE_MEMBER); + if (v instanceof io.micronaut.core.annotation.AnnotationValue[] annotationValues) { + for (io.micronaut.core.annotation.AnnotationValue annotationValue : annotationValues) { + addDeclaredRepeatableStereotype(parentAnnotations, stereotype, annotationValue); + } + } else if (v instanceof Iterable iterable) { + for (Object o : iterable) { + if (o instanceof io.micronaut.core.annotation.AnnotationValue annotationValue) { + addDeclaredRepeatableStereotype(parentAnnotations, stereotype, annotationValue); + } + } + } + } else { + Map> declaredStereotypes = getDeclaredStereotypesInternal(); + Map> allStereotypes = getAllStereotypes(); + List annotationList = getAnnotationsByStereotypeInternal(stereotype); + if (!parentAnnotations.isEmpty()) { + final String parentAnnotation = CollectionUtils.last(parentAnnotations); + if (!annotationList.contains(parentAnnotation)) { + annotationList.add(parentAnnotation); + } + } + + addAnnotation( + stereotype, + values, + declaredStereotypes, + allStereotypes, + true, + retentionPolicy + ); + } } - private boolean computeHasPropertyExpressions(Map values, RetentionPolicy retentionPolicy) { - return hasPropertyExpressions || values != null && retentionPolicy == RetentionPolicy.RUNTIME && hasPropertyExpressions(values); + /** + * Adds an annotation directly declared on the element and its member values, if the annotation already exists the + * data will be merged with existing values replaced. + * + * @param annotation The annotation + * @param values The values + */ + public void addDeclaredAnnotation(String annotation, Map values) { + addDeclaredAnnotation(annotation, values, RetentionPolicy.RUNTIME); } - private boolean hasPropertyExpressions(Map values) { - if (CollectionUtils.isEmpty(values)) { - return false; + /** + * Adds an annotation directly declared on the element and its member values, if the annotation already exists the + * data will be merged with existing values replaced. + * + * @param annotation The annotation + * @param values The values + * @param retentionPolicy The retention policy + */ + public void addDeclaredAnnotation(String annotation, Map values, RetentionPolicy retentionPolicy) { + if (annotation == null) { + return; } - return values.values().stream().anyMatch(v -> { - if (v instanceof CharSequence) { - return v.toString().contains(DefaultPropertyPlaceholderResolver.PREFIX); - } else if (v instanceof String[]) { - return Arrays.stream((String[]) v).anyMatch(s -> s.contains(DefaultPropertyPlaceholderResolver.PREFIX)); - } else if (v instanceof AnnotationValue) { - return hasPropertyExpressions(((AnnotationValue) v).getValues()); - } else if (v instanceof AnnotationValue[]) { - final AnnotationValue[] a = (AnnotationValue[]) v; - if (a.length > 0) { - return Arrays.stream(a).anyMatch(av -> hasPropertyExpressions(av.getValues())); + boolean hasOtherMembers = false; + boolean repeatableAnnotationContainer = isRepeatableAnnotationContainer(annotation); + if (isRepeatableAnnotationContainer(annotation)) { + for (Map.Entry entry : values.entrySet()) { + if (entry.getKey().equals(AnnotationMetadata.VALUE_MEMBER)) { + Object v = entry.getValue(); + if (v instanceof io.micronaut.core.annotation.AnnotationValue[] annotationValues) { + for (io.micronaut.core.annotation.AnnotationValue annotationValue : annotationValues) { + addDeclaredRepeatable(annotation, annotationValue); + } + } else if (v instanceof Iterable iterable) { + for (Object o : iterable) { + if (o instanceof io.micronaut.core.annotation.AnnotationValue annotationValue) { + addDeclaredRepeatable(annotation, annotationValue); + } + } + } } else { - return false; + hasOtherMembers = true; } + } + } + if (!repeatableAnnotationContainer || hasOtherMembers) { + Map> declaredAnnotations = getDeclaredAnnotationsInternal(); + Map> allAnnotations = getAllAnnotations(); + addAnnotation(annotation, values, declaredAnnotations, allAnnotations, true, retentionPolicy); + } + } + + private void addAnnotation(String annotation, + Map values, + Map> declaredAnnotations, + Map> allAnnotations, + boolean isDeclared, + RetentionPolicy retentionPolicy) { + hasPropertyExpressions = computeHasPropertyExpressions(values, retentionPolicy); + if (isDeclared && declaredAnnotations != null) { + putValues(annotation, values, declaredAnnotations); + } + putValues(annotation, values, allAnnotations); + + // Annotations with retention CLASS need not be retained at run time + if (retentionPolicy == RetentionPolicy.SOURCE || retentionPolicy == RetentionPolicy.CLASS) { + addSourceRetentionAnnotation(annotation); + } + } + + private void addSourceRetentionAnnotation(String annotation) { + if (sourceRetentionAnnotations == null) { + sourceRetentionAnnotations = new HashSet<>(5); + } + sourceRetentionAnnotations.add(annotation); + } + + private void putValues(String annotation, Map values, Map> currentAnnotationValues) { + Map existing = currentAnnotationValues.get(annotation); + boolean hasValues = CollectionUtils.isNotEmpty(values); + if (existing != null && hasValues) { + if (existing.isEmpty()) { + existing = new LinkedHashMap<>(); + currentAnnotationValues.put(annotation, existing); + } + for (CharSequence key : values.keySet()) { + if (!existing.containsKey(key)) { + existing.put(key, values.get(key)); + } + } + } else { + if (!hasValues) { + existing = existing == null ? new LinkedHashMap<>(3) : existing; } else { - return false; + existing = new LinkedHashMap<>(values.size()); + existing.putAll(values); } + currentAnnotationValues.put(annotation, existing); + } + } + + @SuppressWarnings("MagicNumber") + private Map> getAllStereotypes() { + Map> stereotypes = this.allStereotypes; + if (stereotypes == null) { + stereotypes = new HashMap<>(3); + this.allStereotypes = stereotypes; + } + return stereotypes; + } + + @SuppressWarnings("MagicNumber") + private Map> getDeclaredStereotypesInternal() { + Map> stereotypes = this.declaredStereotypes; + if (stereotypes == null) { + stereotypes = new HashMap<>(3); + this.declaredStereotypes = stereotypes; + } + return stereotypes; + } + + @SuppressWarnings("MagicNumber") + private Map> getAllAnnotations() { + Map> annotations = this.allAnnotations; + if (annotations == null) { + annotations = new HashMap<>(3); + this.allAnnotations = annotations; + } + return annotations; + } + + @SuppressWarnings("MagicNumber") + private Map> getDeclaredAnnotationsInternal() { + Map> annotations = this.declaredAnnotations; + if (annotations == null) { + annotations = new HashMap<>(3); + this.declaredAnnotations = annotations; + } + return annotations; + } + + private List getAnnotationsByStereotypeInternal(String stereotype) { + return getAnnotationsByStereotypeInternal().computeIfAbsent(stereotype, s -> new ArrayList<>()); + } + + @SuppressWarnings("MagicNumber") + private Map> getAnnotationsByStereotypeInternal() { + Map> annotations = this.annotationsByStereotype; + if (annotations == null) { + annotations = new HashMap<>(3); + this.annotationsByStereotype = annotations; + } + return annotations; + } + + @Nullable + private Object getRawValue(@NonNull String annotation, @NonNull String member) { + Object rawValue = null; + if (allAnnotations != null && StringUtils.isNotEmpty(annotation)) { + Map values = allAnnotations.get(annotation); + if (values != null) { + rawValue = values.get(member); + } else if (allStereotypes != null) { + values = allStereotypes.get(annotation); + if (values != null) { + rawValue = values.get(member); + } + } + } + return rawValue; + } + + private void addRepeatableInternal(String repeatableAnnotationContainer, + AnnotationValue annotationValue, + Map> allAnnotations, + RetentionPolicy retentionPolicy) { + hasPropertyExpressions = computeHasPropertyExpressions(annotationValue.getValues(), retentionPolicy); + if (annotationRepeatableContainer == null) { + annotationRepeatableContainer = new HashMap<>(2); + } + annotationRepeatableContainer.put(annotationValue.getAnnotationName(), repeatableAnnotationContainer); + if (retentionPolicy == RetentionPolicy.SOURCE) { + addSourceRetentionAnnotation(repeatableAnnotationContainer); + } + + Map values = allAnnotations.computeIfAbsent(repeatableAnnotationContainer, s -> new HashMap<>()); + Object v = values.get(AnnotationMetadata.VALUE_MEMBER); + if (v != null) { + if (v.getClass().isArray()) { + Object[] array = (Object[]) v; + Set newValues = CollectionUtils.newLinkedHashSet(array.length + 1); + newValues.addAll(Arrays.asList(array)); + newValues.add(annotationValue); + values.put(AnnotationMetadata.VALUE_MEMBER, newValues); + } else if (v instanceof Collection collection) { + collection.add(annotationValue); + } + } else { + Set newValues = new LinkedHashSet<>(2); + newValues.add(annotationValue); + values.put(AnnotationMetadata.VALUE_MEMBER, newValues); + } + } + + /** + *

Sets a member of the given {@link AnnotationMetadata} return a new annotation metadata instance without + * mutating the existing.

+ * + *

WARNING: for internal use only be the framework

+ * + * @param annotationMetadata The metadata + * @param annotationName The annotation name + * @param member The member + * @param value The value + * @return The metadata + */ + @Internal + public static MutableAnnotationMetadata mutateMember(MutableAnnotationMetadata annotationMetadata, + String annotationName, + String member, + Object value) { + + return mutateMember(annotationMetadata, annotationName, Collections.singletonMap(member, value)); + } + + @Override + protected AnnotationValue newAnnotationValue(String annotationType, Map values) { + Map defaultValues = null; + if (annotationDefaultValues != null) { + defaultValues = annotationDefaultValues.get(annotationType); + } + if (defaultValues == null && sourceAnnotationDefaultValues != null) { + defaultValues = sourceAnnotationDefaultValues.get(annotationType); + } + if (defaultValues == null) { + defaultValues = AnnotationMetadataSupport.getDefaultValuesOrNull(annotationType); + } + return new AnnotationValue<>(annotationType, values, defaultValues); + } + + /** + * Include the annotation metadata from the other instance of {@link DefaultAnnotationMetadata}. + * + * @param annotationMetadata The annotation metadata + * @since 4.0.0 + */ + @Internal + public void addAnnotationMetadata(DefaultAnnotationMetadata annotationMetadata) { + hasPropertyExpressions |= annotationMetadata.hasPropertyExpressions(); + if (annotationMetadata.declaredAnnotations != null && !annotationMetadata.declaredAnnotations.isEmpty()) { + if (declaredAnnotations == null) { + declaredAnnotations = new LinkedHashMap<>(); + } + for (Map.Entry> entry : annotationMetadata.declaredAnnotations.entrySet()) { + putValues(entry.getKey(), entry.getValue(), declaredAnnotations); + } + } + if (annotationMetadata.declaredStereotypes != null && !annotationMetadata.declaredStereotypes.isEmpty()) { + if (declaredStereotypes == null) { + declaredStereotypes = new LinkedHashMap<>(); + } + for (Map.Entry> entry : annotationMetadata.declaredStereotypes.entrySet()) { + putValues(entry.getKey(), entry.getValue(), declaredStereotypes); + } + } + if (annotationMetadata.allStereotypes != null && !annotationMetadata.allStereotypes.isEmpty()) { + if (allStereotypes == null) { + allStereotypes = new LinkedHashMap<>(); + } + for (Map.Entry> entry : annotationMetadata.allStereotypes.entrySet()) { + putValues(entry.getKey(), entry.getValue(), allStereotypes); + } + } + if (annotationMetadata.allAnnotations != null && !annotationMetadata.allAnnotations.isEmpty()) { + if (allAnnotations == null) { + allAnnotations = new LinkedHashMap<>(); + } + for (Map.Entry> entry : annotationMetadata.allAnnotations.entrySet()) { + putValues(entry.getKey(), entry.getValue(), allAnnotations); + } + } + Map> source = annotationMetadata.annotationsByStereotype; + if (source != null && !source.isEmpty()) { + if (annotationsByStereotype == null) { + annotationsByStereotype = new LinkedHashMap<>(); + } + for (Map.Entry> entry : source.entrySet()) { + String ann = entry.getKey(); + List prevValues = annotationsByStereotype.get(ann); + if (prevValues == null) { + annotationsByStereotype.put(ann, new ArrayList<>(entry.getValue())); + } else { + Set prevValuesSet = new LinkedHashSet<>(prevValues); + prevValuesSet.addAll(entry.getValue()); + annotationsByStereotype.put(ann, new ArrayList<>(prevValuesSet)); + } + } + } + } + + /** + * Include the annotation metadata from the other instance of {@link DefaultAnnotationMetadata}. + * + * @param annotationMetadata The annotation metadata + * @since 4.0.0 + */ + @Internal + public void addAnnotationMetadata(MutableAnnotationMetadata annotationMetadata) { + addAnnotationMetadata((DefaultAnnotationMetadata) annotationMetadata); + hasPropertyExpressions |= annotationMetadata.hasPropertyExpressions; + if (annotationMetadata.sourceRetentionAnnotations != null) { + if (sourceRetentionAnnotations == null) { + sourceRetentionAnnotations = new HashSet<>(annotationMetadata.sourceRetentionAnnotations); + } else { + sourceRetentionAnnotations.addAll(annotationMetadata.sourceRetentionAnnotations); + } + } + if (annotationMetadata.annotationDefaultValues != null) { + if (annotationDefaultValues == null) { + annotationDefaultValues = new LinkedHashMap<>(annotationMetadata.annotationDefaultValues); + } else { + // No need to merge values + annotationDefaultValues.putAll(annotationMetadata.annotationDefaultValues); + } + } + if (annotationMetadata.sourceAnnotationDefaultValues != null) { + if (sourceAnnotationDefaultValues == null) { + sourceAnnotationDefaultValues = new LinkedHashMap<>(annotationMetadata.sourceAnnotationDefaultValues); + } else { + // No need to merge values + sourceAnnotationDefaultValues.putAll(annotationMetadata.sourceAnnotationDefaultValues); + } + } + if (annotationMetadata.annotationRepeatableContainer != null) { + if (annotationRepeatableContainer == null) { + annotationRepeatableContainer = new LinkedHashMap<>(annotationMetadata.annotationRepeatableContainer); + } else { + annotationRepeatableContainer.putAll(annotationMetadata.annotationRepeatableContainer); + } + } + } + + /** + * Contributes defaults to the given target. + * + *

WARNING: for internal use only be the framework

+ * + * @param target The target + * @param source The source + */ + @Internal + public static void contributeDefaults(AnnotationMetadata target, AnnotationMetadata source) { + source = source.getTargetAnnotationMetadata(); + if (source instanceof AnnotationMetadataHierarchy) { + source = ((AnnotationMetadataHierarchy) source).merge(); + } + if (target instanceof MutableAnnotationMetadata damTarget && source instanceof MutableAnnotationMetadata damSource) { + final Map> existingDefaults = damTarget.annotationDefaultValues; + final Map> additionalDefaults = damSource.annotationDefaultValues; + if (existingDefaults != null) { + if (additionalDefaults != null) { + existingDefaults.putAll( + additionalDefaults + ); + } + } else { + if (additionalDefaults != null) { + additionalDefaults.forEach(damTarget::addDefaultAnnotationValues); + } + } + } + // We don't need to contribute the default source annotation + contributeRepeatable(target, source); + } + + /** + * Contributes repeatable annotation metadata to the given target. + * + *

WARNING: for internal use only be the framework

+ * + * @param target The target + * @param source The source + */ + @Internal + public static void contributeRepeatable(AnnotationMetadata target, AnnotationMetadata source) { + source = source.getTargetAnnotationMetadata(); + if (source instanceof AnnotationMetadataHierarchy) { + source = ((AnnotationMetadataHierarchy) source).merge(); + } + if (target instanceof MutableAnnotationMetadata damTarget && source instanceof MutableAnnotationMetadata damSource) { + if (damSource.annotationRepeatableContainer != null && !damSource.annotationRepeatableContainer.isEmpty()) { + if (damTarget.annotationRepeatableContainer == null) { + damTarget.annotationRepeatableContainer = new HashMap<>(damSource.annotationRepeatableContainer); + } else { + damTarget.annotationRepeatableContainer.putAll(damSource.annotationRepeatableContainer); + } + } + } + } + + /** + *

Sets a member of the given {@link AnnotationMetadata} return a new annotation metadata instance without + * mutating the existing.

+ * + *

WARNING: for internal use only be the framework

+ * + * @param annotationMetadata The metadata + * @param annotationName The annotation name + * @param members The key/value set of members and values + * @return The metadata + */ + @Internal + public static MutableAnnotationMetadata mutateMember(MutableAnnotationMetadata annotationMetadata, + String annotationName, + Map members) { + if (StringUtils.isEmpty(annotationName)) { + throw new IllegalArgumentException("Argument [annotationName] cannot be blank"); + } + if (!members.isEmpty()) { + for (Map.Entry entry : members.entrySet()) { + if (StringUtils.isEmpty(entry.getKey())) { + throw new IllegalArgumentException("Argument [members] cannot have a blank key"); + } + if (entry.getValue() == null) { + throw new IllegalArgumentException("Argument [members] cannot have a null value. Key [" + entry.getKey() + "]"); + } + } + } + annotationMetadata = annotationMetadata.clone(); + annotationMetadata.addDeclaredAnnotation(annotationName, members); + return annotationMetadata; + } + + /** + * Removes an annotation for the given predicate. + * + * @param predicate The predicate + * @param The annotation + */ + public void removeAnnotationIf(@NonNull Predicate> predicate) { + removeAnnotationsIf(predicate, this.declaredAnnotations); + removeAnnotationsIf(predicate, this.allAnnotations); + } + + private void removeAnnotationsIf(@NonNull Predicate> predicate, Map> annotations) { + if (annotations == null) { + return; + } + annotations.entrySet().removeIf(entry -> { + final String annotationName = entry.getKey(); + if (predicate.test(newAnnotationValue(annotationName, entry.getValue()))) { + removeFromStereotypes(annotationName, annotations); + return true; + } + return false; }); } + + /** + * Removes an annotation for the given annotation type. + * + * @param annotationType The annotation type + * @since 3.0.0 + */ + public void removeAnnotation(String annotationType) { + if (annotationType == null) { + return; + } + if (annotationDefaultValues != null) { + annotationDefaultValues.remove(annotationType); + } + if (allAnnotations != null) { + allAnnotations.remove(annotationType); + } + if (declaredAnnotations != null) { + declaredAnnotations.remove(annotationType); + removeFromStereotypes(annotationType, declaredAnnotations); + } + if (annotationRepeatableContainer != null) { + annotationRepeatableContainer.remove(annotationType); + } + } + + /** + * Removes a stereotype annotation for the given annotation type. + * + * @param annotationType The annotation type + * @since 3.0.0 + */ + public void removeStereotype(String annotationType) { + if (annotationType == null) { + return; + } + if (annotationsByStereotype == null || annotationsByStereotype.remove(annotationType) == null) { + return; + } + if (allStereotypes != null) { + allStereotypes.remove(annotationType); + } + if (declaredStereotypes != null) { + declaredStereotypes.remove(annotationType); + } + final Iterator>> i = annotationsByStereotype.entrySet().iterator(); + while (i.hasNext()) { + Map.Entry> entry = i.next(); + final List value = entry.getValue(); + if (value.remove(annotationType) && value.isEmpty()) { + i.remove(); + } + } + } + + private void removeFromStereotypes(String annotationType, Map> declaredAnnotations) { + if (annotationsByStereotype == null) { + return; + } + final Iterator>> i = annotationsByStereotype.entrySet().iterator(); + Set toBeRemoved = CollectionUtils.setOf(annotationType); + while (i.hasNext()) { + final Map.Entry> entry = i.next(); + final String stereotypeName = entry.getKey(); + final List value = entry.getValue(); + if (value.removeAll(toBeRemoved)) { + if (value.isEmpty()) { + toBeRemoved.add(stereotypeName); + i.remove(); + if (allStereotypes != null) { + this.allStereotypes.remove(stereotypeName); + } + if (declaredStereotypes != null) { + this.declaredStereotypes.remove(stereotypeName); + } + if (annotationDefaultValues != null) { + annotationDefaultValues.remove(stereotypeName); + } + } + + if (AnnotationUtil.ANN_AROUND.equals(stereotypeName) || AnnotationUtil.ANN_INTRODUCTION.equals(stereotypeName) || AnnotationUtil.ANN_AROUND_CONSTRUCT.equals(stereotypeName)) { + // purge from interceptor binding + purgeInterceptorBindings(declaredAnnotations, toBeRemoved); + purgeInterceptorBindings(this.allAnnotations, toBeRemoved); + } + } + } + } + + private void purgeInterceptorBindings(Map> declaredAnnotations, Set toBeRemoved) { + if (declaredAnnotations == null) { + return; + } + final Map v = declaredAnnotations.get(AnnotationUtil.ANN_INTERCEPTOR_BINDINGS); + if (v != null) { + final Object o = v.get(AnnotationMetadata.VALUE_MEMBER); + if (o instanceof Collection) { + Collection> col = (Collection) o; + col.removeIf(av -> Arrays.stream(av.annotationClassValues(AnnotationMetadata.VALUE_MEMBER)) + .anyMatch(acv -> toBeRemoved.contains(acv.getName()))); + + if (col.isEmpty()) { + declaredAnnotations.remove(AnnotationUtil.ANN_INTERCEPTOR_BINDINGS); + } + } + } + } + + private boolean isRepeatableAnnotationContainer(String annotation) { + return annotationRepeatableContainer != null && annotationRepeatableContainer.containsValue(annotation); + } + + @Override + protected String findRepeatableAnnotationContainerInternal(String annotation) { + if (annotationRepeatableContainer != null) { + String repeatedName = annotationRepeatableContainer.get(annotation); + if (repeatedName != null) { + return repeatedName; + } + } + return AnnotationMetadataSupport.getRepeatableAnnotation(annotation); + } } diff --git a/inject/src/test/groovy/io/micronaut/inject/annotation/AnnotationMetadataSpec.groovy b/inject/src/test/groovy/io/micronaut/inject/annotation/AnnotationMetadataSpec.groovy index e4fab35b942..a83877141d3 100644 --- a/inject/src/test/groovy/io/micronaut/inject/annotation/AnnotationMetadataSpec.groovy +++ b/inject/src/test/groovy/io/micronaut/inject/annotation/AnnotationMetadataSpec.groovy @@ -37,7 +37,7 @@ class AnnotationMetadataSpec extends Specification { void "test empty values then append"() { given: - DefaultAnnotationMetadata metadata = new DefaultAnnotationMetadata([:], null, null, [:], null) + MutableAnnotationMetadata metadata = new MutableAnnotationMetadata([:], null, null, [:], null, false) metadata.addAnnotation("foo.Bar", [:]) when: @@ -56,8 +56,8 @@ class AnnotationMetadataSpec extends Specification { annotations.put(av.annotationName, av.values) } - return new DefaultAnnotationMetadata( - annotations, null, null, annotations, null + return new MutableAnnotationMetadata( + annotations, null, null, annotations, null, false ) } } From 4e44102976c264a0331219ba22db91bb8c861333 Mon Sep 17 00:00:00 2001 From: Graeme Rocher Date: Wed, 25 Jan 2023 13:59:32 +0100 Subject: [PATCH 420/743] improve KSP error handling (#8666) --- .../beans/BeanDefinitionProcessor.kt | 30 +++++++++++++++++-- .../visitor/TypeElementSymbolProcessor.kt | 16 +++------- 2 files changed, 32 insertions(+), 14 deletions(-) diff --git a/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/beans/BeanDefinitionProcessor.kt b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/beans/BeanDefinitionProcessor.kt index dc5243f8217..7c722567658 100644 --- a/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/beans/BeanDefinitionProcessor.kt +++ b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/beans/BeanDefinitionProcessor.kt @@ -29,6 +29,7 @@ import io.micronaut.inject.visitor.VisitorConfiguration import io.micronaut.inject.writer.BeanDefinitionReferenceWriter import io.micronaut.inject.writer.BeanDefinitionVisitor import io.micronaut.kotlin.processing.KotlinOutputVisitor +import io.micronaut.kotlin.processing.unwrap import io.micronaut.kotlin.processing.visitor.KotlinClassElement import io.micronaut.kotlin.processing.visitor.KotlinVisitorContext import java.io.IOException @@ -60,7 +61,11 @@ class BeanDefinitionProcessor(private val environment: SymbolProcessorEnvironmen } .toList() - processClassDeclarations(elements, visitorContext) + try { + processClassDeclarations(elements, visitorContext) + } catch (e: ProcessingException) { + handleProcessingException(environment, e) + } return emptyList() } @@ -100,13 +105,34 @@ class BeanDefinitionProcessor(private val environment: SymbolProcessorEnvironmen } } } catch (e: ProcessingException) { - environment.logger.error(e.message!!, e.originatingElement as KSNode) + handleProcessingException(environment, e) } finally { AbstractAnnotationMetadataBuilder.clearMutated() beanDefinitionMap.clear() } } + + + companion object Helper { + fun handleProcessingException(environment: SymbolProcessorEnvironment, e: ProcessingException) { + val message = e.message + val originatingNode = (e.originatingElement as KSNode).unwrap() + if (message != null) { + environment.logger.error("Originating element: $originatingNode") + environment.logger.error(message, originatingNode) + } else { + environment.logger.error("Unknown error processing element", originatingNode) + val cause = e.cause + if (cause != null) { + environment.logger.exception(cause) + } else { + environment.logger.exception(e) + } + } + } + } + private fun processBeanDefinitions( beanDefinitionWriter: BeanDefinitionVisitor, outputVisitor: KotlinOutputVisitor, diff --git a/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/TypeElementSymbolProcessor.kt b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/TypeElementSymbolProcessor.kt index 37dd9d0b36e..c04364e6ee9 100644 --- a/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/TypeElementSymbolProcessor.kt +++ b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/TypeElementSymbolProcessor.kt @@ -33,6 +33,7 @@ import io.micronaut.inject.ast.* import io.micronaut.inject.processing.ProcessingException import io.micronaut.inject.visitor.TypeElementVisitor import io.micronaut.inject.visitor.VisitorContext +import io.micronaut.kotlin.processing.beans.BeanDefinitionProcessor import java.util.* open class TypeElementSymbolProcessor(private val environment: SymbolProcessorEnvironment): SymbolProcessor { @@ -105,18 +106,7 @@ open class TypeElementSymbolProcessor(private val environment: SymbolProcessorEn try { typeElement.accept(ElementVisitor(loadedVisitor, typeElement), className) } catch (e: ProcessingException) { - val message = e.message - if (message != null) { - environment.logger.error(message, e.originatingElement as KSNode) - } else { - environment.logger.error("Unknown error processing element", e.originatingElement as KSNode) - val cause = e.cause - if (cause != null) { - environment.logger.exception(cause) - } else { - environment.logger.exception(e) - } - } + BeanDefinitionProcessor.handleProcessingException(environment, e) } } } @@ -130,6 +120,8 @@ open class TypeElementSymbolProcessor(private val environment: SymbolProcessorEn for (loadedVisitor in loadedVisitors) { try { loadedVisitor.visitor.finish(visitorContext) + } catch (e: ProcessingException) { + BeanDefinitionProcessor.handleProcessingException(environment, e) } catch (e: Throwable) { environment.logger.error("Error finalizing type visitor [${loadedVisitor.visitor}]: ${e.message}") } From a205d4311df1e0d8d442ab7e482efe460add1f8f Mon Sep 17 00:00:00 2001 From: micronaut-build <65172877+micronaut-build@users.noreply.github.com> Date: Wed, 25 Jan 2023 13:59:55 +0100 Subject: [PATCH 421/743] Bump micronaut-oracle-cloud to 2.3.2 (#8658) --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 821e79f029a..e8a1e3bec46 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -101,7 +101,7 @@ managed-micronaut-nats = "3.1.0" managed-micronaut-netflix = "2.1.0" managed-micronaut-object-storage = "1.1.0" managed-micronaut-openapi = "4.8.1" -managed-micronaut-oraclecloud = "2.3.1" +managed-micronaut-oraclecloud = "2.3.2" managed-micronaut-picocli = "4.3.0" managed-micronaut-problem = "2.6.0" managed-micronaut-rabbitmq = "3.4.0" From fcfe0acddb892db7f966598ecf0c4cc9e73fae6c Mon Sep 17 00:00:00 2001 From: Denis Stepanov Date: Wed, 25 Jan 2023 14:59:03 +0100 Subject: [PATCH 422/743] Fix NPE in bean context (#8665) --- .../src/main/java/io/micronaut/context/DefaultBeanContext.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/inject/src/main/java/io/micronaut/context/DefaultBeanContext.java b/inject/src/main/java/io/micronaut/context/DefaultBeanContext.java index 14bcad8f133..7a3d344413f 100644 --- a/inject/src/main/java/io/micronaut/context/DefaultBeanContext.java +++ b/inject/src/main/java/io/micronaut/context/DefaultBeanContext.java @@ -3335,6 +3335,9 @@ private void readAllBeanDefinitionClasses() { reference: for (BeanDefinitionProducer beanDefinitionProducer : producers) { + if (beanDefinitionProducer.isDisabled()) { + continue; + } BeanDefinitionReference beanDefinitionReference = beanDefinitionProducer.reference; for (BeanConfiguration disableConfiguration : configurationsDisabled) { if (disableConfiguration.isWithin(beanDefinitionReference)) { From 6348b1ea5101ba7a90f4acbc574b7480c60170f6 Mon Sep 17 00:00:00 2001 From: Graeme Rocher Date: Wed, 25 Jan 2023 15:02:50 +0100 Subject: [PATCH 423/743] add test for #4887 (#8655) --- .../ConfigPropertiesParseSpec.groovy | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/inject/configproperties/ConfigPropertiesParseSpec.groovy b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/inject/configproperties/ConfigPropertiesParseSpec.groovy index b734347fa11..e95fcdb655d 100644 --- a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/inject/configproperties/ConfigPropertiesParseSpec.groovy +++ b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/inject/configproperties/ConfigPropertiesParseSpec.groovy @@ -16,6 +16,29 @@ import static io.micronaut.annotation.processing.test.KotlinCompiler.* class ConfigPropertiesParseSpec extends Specification { + void "test default as a property name"() { + given: + + def config = ['foo.bar.host': 'test', 'foo.bar.default': true] + def context = buildContext(''' +package test + +import io.micronaut.context.annotation.* + +@ConfigurationProperties("foo.bar") +data class DefaultConfigTest(val host : String, val default: Boolean ) +''', false, config) + + def bean = getBean(context, 'test.DefaultConfigTest') + + expect: + bean.host == 'test' + bean.default + + cleanup: + context.close() + } + void "test data classes that are configuration properties inject values"() { given: From db35ed12f28a325f4127e703fb832fbb26132ae4 Mon Sep 17 00:00:00 2001 From: Graeme Rocher Date: Wed, 25 Jan 2023 15:05:17 +0100 Subject: [PATCH 424/743] Don't process overridden interface methods in introduction advice (#8662) Fixes #8610 Co-authored-by: Denis Stepanov --- ...troductionInterfaceBeanElementCreator.java | 9 +- ...roductionAdviceWithNewInterfaceSpec.groovy | 2 +- .../MyRepoIntroductionSpec.groovy | 2 +- .../compile/IntroductionCompileSpec.groovy | 141 +++++++++++++++++- ...roductionAdviceWithNewInterfaceSpec.groovy | 2 +- ...roductionAdviceWithNewInterfaceSpec.groovy | 2 +- 6 files changed, 151 insertions(+), 7 deletions(-) diff --git a/core-processor/src/main/java/io/micronaut/inject/processing/IntroductionInterfaceBeanElementCreator.java b/core-processor/src/main/java/io/micronaut/inject/processing/IntroductionInterfaceBeanElementCreator.java index c61847e5e45..d6b09bb1e81 100644 --- a/core-processor/src/main/java/io/micronaut/inject/processing/IntroductionInterfaceBeanElementCreator.java +++ b/core-processor/src/main/java/io/micronaut/inject/processing/IntroductionInterfaceBeanElementCreator.java @@ -15,6 +15,7 @@ */ package io.micronaut.inject.processing; +import io.micronaut.aop.internal.intercepted.InterceptedMethodUtil; import io.micronaut.core.annotation.Internal; import io.micronaut.inject.ast.ClassElement; import io.micronaut.inject.ast.ElementQuery; @@ -66,7 +67,13 @@ public void buildInternal() { // The introduction will include overridden methods* (find(List) <- find(Iterable)*) but ordinary class introduction doesn't // Because of the caching we need to process declared methods first - List methods = new ArrayList<>(classElement.getEnclosedElements(ElementQuery.ALL_METHODS.includeHiddenElements().includeOverriddenMethods())); + List allMethods = new ArrayList<>(classElement.getEnclosedElements(ElementQuery.ALL_METHODS.includeOverriddenMethods().includeOverriddenMethods())); + List methods = new ArrayList<>(allMethods); + List nonAbstractMethods = methods.stream().filter(m -> !m.isAbstract()).toList(); + // Remove abstract methods overridden by non-abstract ones + methods.removeIf(method -> method.isAbstract() && nonAbstractMethods.stream().anyMatch(nonAbstractMethod -> nonAbstractMethod.overrides(method))); + // Remove non-abstract methods without explicit around advice + methods.removeIf(method -> !method.isAbstract() && !InterceptedMethodUtil.hasDeclaredAroundAdvice(method.getAnnotationMetadata())); Collections.reverse(methods); // reverse to process hierarchy starting from declared methods for (MethodElement methodElement : methods) { visitIntrospectedMethod(aopProxyWriter, classElement, methodElement); diff --git a/inject-groovy/src/test/groovy/io/micronaut/aop/introduction/IntroductionAdviceWithNewInterfaceSpec.groovy b/inject-groovy/src/test/groovy/io/micronaut/aop/introduction/IntroductionAdviceWithNewInterfaceSpec.groovy index 08269a19d7c..62182d57201 100644 --- a/inject-groovy/src/test/groovy/io/micronaut/aop/introduction/IntroductionAdviceWithNewInterfaceSpec.groovy +++ b/inject-groovy/src/test/groovy/io/micronaut/aop/introduction/IntroductionAdviceWithNewInterfaceSpec.groovy @@ -190,7 +190,7 @@ interface SpecificInterface { then: //I ended up going this route because actually calling the methods here would be relying on //having the target interface in the bytecode of the test - instance.$proxyMethods.length == 2 + instance.$proxyMethods.length == 1 } void "test interface multiple inheritance"() { diff --git a/inject-groovy/src/test/groovy/io/micronaut/aop/introduction/MyRepoIntroductionSpec.groovy b/inject-groovy/src/test/groovy/io/micronaut/aop/introduction/MyRepoIntroductionSpec.groovy index 3b177e5f5f5..d96324292b8 100644 --- a/inject-groovy/src/test/groovy/io/micronaut/aop/introduction/MyRepoIntroductionSpec.groovy +++ b/inject-groovy/src/test/groovy/io/micronaut/aop/introduction/MyRepoIntroductionSpec.groovy @@ -35,7 +35,7 @@ class MyRepoIntroductionSpec extends Specification { def repoDeclaredMethods = Arrays.stream(MyRepo.class.getMethods()).filter(m -> m.getDeclaringClass() == MyRepo.class).collect(Collectors.toList()) then: repoDeclaredMethods.size() == 3 // Groovy will exclude overridden methods (Java would have 4) - interceptorDeclaredMethods.size() == 4 // We need to intercept overridden methods + interceptorDeclaredMethods.size() == 3 // We need to intercept overridden methods bean.getClass().getName().contains("Intercepted") MyRepoIntroducer.EXECUTED_METHODS.isEmpty() when: diff --git a/inject-java/src/test/groovy/io/micronaut/aop/compile/IntroductionCompileSpec.groovy b/inject-java/src/test/groovy/io/micronaut/aop/compile/IntroductionCompileSpec.groovy index 64ce635d81b..7dbd25cc16a 100644 --- a/inject-java/src/test/groovy/io/micronaut/aop/compile/IntroductionCompileSpec.groovy +++ b/inject-java/src/test/groovy/io/micronaut/aop/compile/IntroductionCompileSpec.groovy @@ -5,6 +5,143 @@ import io.micronaut.aop.Intercepted import io.micronaut.context.ApplicationContext class IntroductionCompileSpec extends AbstractTypeElementSpec { + + void "test inherited default methods are not overridden"() { + given: + ApplicationContext context = buildContext(''' +package introductiontest; + +import java.lang.annotation.*; +import io.micronaut.aop.*; +import jakarta.inject.*; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + + +@TestAnn +interface MyBean extends Parent { + + int test(); + + default String getName() { + return "my-bean"; + } + + @Override + default String getDescription() { + return "description"; + } +} + +interface Parent { + default String getParentName() { + return "parent"; + } + + String getDescription(); +} + +@Retention(RUNTIME) +@Target({ElementType.METHOD, ElementType.TYPE}) +@Introduction +@interface TestAnn { +} + +@InterceptorBean(TestAnn.class) +class StubIntroduction implements Interceptor { + int invoked = 0; + @Override + public Object intercept(InvocationContext context) { + invoked++; + return 10; + } +} + +''') + def instance = getBean(context, 'introductiontest.MyBean') + def interceptor = getBean(context, 'introductiontest.StubIntroduction') + + when: + def result = instance.test() + + then:"the interceptor was invoked" + instance instanceof Intercepted + instance.name == 'my-bean' + instance.description == 'description' + instance.parentName == 'parent' + result == 10 + interceptor.invoked == 1 + } + + void "test inherited default or abstract methods are not overridden"() { + given: + ApplicationContext context = buildContext(''' +package introductiontest; + +import java.lang.annotation.*; +import io.micronaut.aop.*; +import jakarta.inject.*; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +@TestAnn +abstract class MyBean extends Parent implements MyInterface { + + abstract int test(); + + public String getName() { + return "my-bean"; + } + + @Override + public String getDescription() { + return "description"; + } +} + +abstract class Parent { + public String getParentName() { + return "parent"; + } + + abstract String getDescription(); +} + +interface MyInterface { + + String getDescription(); +} + +@Retention(RUNTIME) +@Target({ElementType.METHOD, ElementType.TYPE}) +@Introduction +@interface TestAnn { +} + +@InterceptorBean(TestAnn.class) +class StubIntroduction implements Interceptor { + int invoked = 0; + @Override + public Object intercept(InvocationContext context) { + invoked++; + return 10; + } +} + +''') + def instance = getBean(context, 'introductiontest.MyBean') + def interceptor = getBean(context, 'introductiontest.StubIntroduction') + + when: + def result = instance.test() + + then:"the interceptor was invoked" + instance instanceof Intercepted + instance.name == 'my-bean' + instance.description == 'description' + instance.parentName == 'parent' + result == 10 + interceptor.invoked == 1 + } + void 'test apply introduction advise with interceptor binding'() { given: ApplicationContext context = buildContext(''' @@ -18,7 +155,7 @@ import static java.lang.annotation.RetentionPolicy.RUNTIME; @TestAnn interface MyBean { - + int test(); } @@ -36,7 +173,7 @@ class StubIntroduction implements Interceptor { invoked++; return 10; } -} +} ''') def instance = getBean(context, 'introductiontest.MyBean') diff --git a/inject-java/src/test/groovy/io/micronaut/aop/introduction/IntroductionAdviceWithNewInterfaceSpec.groovy b/inject-java/src/test/groovy/io/micronaut/aop/introduction/IntroductionAdviceWithNewInterfaceSpec.groovy index 0b6a6b7602b..d13ae0989a2 100644 --- a/inject-java/src/test/groovy/io/micronaut/aop/introduction/IntroductionAdviceWithNewInterfaceSpec.groovy +++ b/inject-java/src/test/groovy/io/micronaut/aop/introduction/IntroductionAdviceWithNewInterfaceSpec.groovy @@ -162,7 +162,7 @@ interface MyBean { beanDefinition != null ApplicationEventListener.class.isAssignableFrom(beanDefinition.beanType) beanDefinition.injectedFields.size() == 0 - beanDefinition.executableMethods.size() == 3 + beanDefinition.executableMethods.size() == 2 beanDefinition.findMethod("getBar").isPresent() beanDefinition.findMethod("onApplicationEvent", Object).isPresent() diff --git a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/introduction/IntroductionAdviceWithNewInterfaceSpec.groovy b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/introduction/IntroductionAdviceWithNewInterfaceSpec.groovy index 8578aba07f7..4cd1000d9cb 100644 --- a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/introduction/IntroductionAdviceWithNewInterfaceSpec.groovy +++ b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/introduction/IntroductionAdviceWithNewInterfaceSpec.groovy @@ -217,7 +217,7 @@ interface MyBean { beanDefinition != null ApplicationEventListener.class.isAssignableFrom(beanDefinition.beanType) beanDefinition.injectedFields.size() == 0 - beanDefinition.executableMethods.size() == 3 + beanDefinition.executableMethods.size() == 2 beanDefinition.findMethod("getBar").isPresent() beanDefinition.findMethod("onApplicationEvent", Object).isPresent() From d32e7d8d5c466eb9b02334c3bc52fad00c79bac7 Mon Sep 17 00:00:00 2001 From: micronaut-build <65172877+micronaut-build@users.noreply.github.com> Date: Wed, 25 Jan 2023 15:09:12 +0100 Subject: [PATCH 425/743] Update common files (#8597) --- .github/workflows/graalvm.yml | 14 +++++++++++--- .github/workflows/gradle.yml | 6 +++++- .github/workflows/release.yml | 4 +++- config/checkstyle/checkstyle.xml | 2 +- 4 files changed, 20 insertions(+), 6 deletions(-) diff --git a/.github/workflows/graalvm.yml b/.github/workflows/graalvm.yml index c0f68cc0fc9..6e05b7a6927 100644 --- a/.github/workflows/graalvm.yml +++ b/.github/workflows/graalvm.yml @@ -19,8 +19,14 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - java: ['17'] - graalvm: ['latest', 'dev'] + graalvm: [ 'latest', 'dev' ] + java: [ '17' ] + include: + - graalvm: 'dev' + java: '19' + exclude: + - graalvm: 'dev' + java: '17' steps: # https://github.com/actions/virtual-environments/issues/709 - name: Free disk space @@ -56,6 +62,8 @@ jobs: fi env: TESTCONTAINERS_RYUK_DISABLED: true + GH_TOKEN_PUBLIC_REPOS_READONLY: ${{ secrets.GH_TOKEN_PUBLIC_REPOS_READONLY }} + GH_USERNAME: ${{ secrets.GH_USERNAME }} GRADLE_ENTERPRISE_ACCESS_KEY: ${{ secrets.GRADLE_ENTERPRISE_ACCESS_KEY }} GRADLE_ENTERPRISE_CACHE_USERNAME: ${{ secrets.GRADLE_ENTERPRISE_CACHE_USERNAME }} GRADLE_ENTERPRISE_CACHE_PASSWORD: ${{ secrets.GRADLE_ENTERPRISE_CACHE_PASSWORD }} @@ -74,7 +82,7 @@ jobs: }) - name: Publish Test Report if: always() - uses: mikepenz/action-junit-report@v3.7.0 + uses: mikepenz/action-junit-report@v3.7.1 with: check_name: GraalVM CE CI / Test Report (Java ${{ matrix.java }}) report_paths: '**/build/test-results/test/TEST-*.xml' diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index 26ffebb068b..0ea9d934cb7 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -48,6 +48,8 @@ jobs: run: | ./gradlew check --no-daemon --parallel --continue env: + GH_TOKEN_PUBLIC_REPOS_READONLY: ${{ secrets.GH_TOKEN_PUBLIC_REPOS_READONLY }} + GH_USERNAME: ${{ secrets.GH_USERNAME }} TESTCONTAINERS_RYUK_DISABLED: true GRADLE_ENTERPRISE_ACCESS_KEY: ${{ secrets.GRADLE_ENTERPRISE_ACCESS_KEY }} GRADLE_ENTERPRISE_CACHE_USERNAME: ${{ secrets.GRADLE_ENTERPRISE_CACHE_USERNAME }} @@ -67,7 +69,7 @@ jobs: }) - name: Publish Test Report if: always() - uses: mikepenz/action-junit-report@v3.7.0 + uses: mikepenz/action-junit-report@v3.7.1 with: check_name: Java CI / Test Report (${{ matrix.java }}) report_paths: '**/build/test-results/test/TEST-*.xml' @@ -81,6 +83,8 @@ jobs: - name: Publish to Sonatype Snapshots if: success() && github.event_name == 'push' && matrix.java == '17' env: + GH_TOKEN_PUBLIC_REPOS_READONLY: ${{ secrets.GH_TOKEN_PUBLIC_REPOS_READONLY }} + GH_USERNAME: ${{ secrets.GH_USERNAME }} SONATYPE_USERNAME: ${{ secrets.SONATYPE_USERNAME }} SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }} GRADLE_ENTERPRISE_ACCESS_KEY: ${{ secrets.GRADLE_ENTERPRISE_ACCESS_KEY }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 70ddd45525a..908ba288e13 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -78,11 +78,13 @@ jobs: path: artifacts-sha256 retention-days: 5 - name: Generate docs + run: ./gradlew docs env: GRADLE_ENTERPRISE_ACCESS_KEY: ${{ secrets.GRADLE_ENTERPRISE_ACCESS_KEY }} GRADLE_ENTERPRISE_CACHE_USERNAME: ${{ secrets.GRADLE_ENTERPRISE_CACHE_USERNAME }} GRADLE_ENTERPRISE_CACHE_PASSWORD: ${{ secrets.GRADLE_ENTERPRISE_CACHE_PASSWORD }} - run: ./gradlew docs + GH_TOKEN_PUBLIC_REPOS_READONLY: ${{ secrets.GH_TOKEN_PUBLIC_REPOS_READONLY }} + GH_USERNAME: ${{ secrets.GH_USERNAME }} - name: Export Gradle Properties uses: micronaut-projects/github-actions/export-gradle-properties@master - name: Publish to Github Pages diff --git a/config/checkstyle/checkstyle.xml b/config/checkstyle/checkstyle.xml index c1b8e128046..149d892039f 100644 --- a/config/checkstyle/checkstyle.xml +++ b/config/checkstyle/checkstyle.xml @@ -120,7 +120,7 @@ - + From 01adb21e57137caaf7004313d2055c5a78b1f47b Mon Sep 17 00:00:00 2001 From: Tim Yates Date: Fri, 27 Jan 2023 12:20:20 +0000 Subject: [PATCH 426/743] bug: fix Cors for alternate local addresses (#8642) --- .github/workflows/graalvm.yml | 7 +- .../server/netty/cors/CorsFilterSpec.groovy | 2 + .../http/server/tck/RequestSupplier.java | 30 +++++ .../http/server/tck/TestScenario.java | 42 ++++++- .../tck/tests/cors/CorsSimpleRequestTest.java | 116 +++++++++++++++--- .../http/server/cors/CorsFilter.java | 52 ++++++-- 6 files changed, 213 insertions(+), 36 deletions(-) create mode 100644 http-server-tck/src/main/java/io/micronaut/http/server/tck/RequestSupplier.java diff --git a/.github/workflows/graalvm.yml b/.github/workflows/graalvm.yml index 199710c6e17..9d5c3c23af0 100644 --- a/.github/workflows/graalvm.yml +++ b/.github/workflows/graalvm.yml @@ -19,11 +19,8 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - java: ['17'] - graalvm: ['latest', 'dev'] - include: - - graalvm: 'latest' - java: '11' + java: ['11', '17'] + graalvm: ['latest'] steps: # https://github.com/actions/virtual-environments/issues/709 - name: Free disk space diff --git a/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/cors/CorsFilterSpec.groovy b/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/cors/CorsFilterSpec.groovy index 7eb73af108b..d7b3be1f0db 100644 --- a/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/cors/CorsFilterSpec.groovy +++ b/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/cors/CorsFilterSpec.groovy @@ -129,6 +129,8 @@ class CorsFilterSpec extends Specification { ['^http://www\\.(foo|bar)\\.com$'] | 'http://www.foo.com' ['.*bar$', '.*foo$'] | 'asdfasdf foo' ['.*bar$', '.*foo$'] | 'asdfasdf bar' + ['.*bar$', '.*foo$'] | 'http://asdfasdf.foo' + ['.*bar$', '.*foo$'] | 'http://asdfasdf.bar' } void "test handleRequest with disallowed method"() { diff --git a/http-server-tck/src/main/java/io/micronaut/http/server/tck/RequestSupplier.java b/http-server-tck/src/main/java/io/micronaut/http/server/tck/RequestSupplier.java new file mode 100644 index 00000000000..c58dad9356f --- /dev/null +++ b/http-server-tck/src/main/java/io/micronaut/http/server/tck/RequestSupplier.java @@ -0,0 +1,30 @@ +/* + * Copyright 2017-2023 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.http.server.tck; + +import io.micronaut.http.HttpRequest; + +import java.util.function.Function; + +/** + * Allows defining an HttpRequest based on some property of the server (ie: Port). + * + * @author Tim Yates + * @since 3.8.3 + */ +@FunctionalInterface +public interface RequestSupplier extends Function> { +} diff --git a/http-server-tck/src/main/java/io/micronaut/http/server/tck/TestScenario.java b/http-server-tck/src/main/java/io/micronaut/http/server/tck/TestScenario.java index dc2d4ccafe6..f86e1010c2f 100644 --- a/http-server-tck/src/main/java/io/micronaut/http/server/tck/TestScenario.java +++ b/http-server-tck/src/main/java/io/micronaut/http/server/tck/TestScenario.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2022 original authors + * Copyright 2017-2023 original authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -32,12 +32,12 @@ public final class TestScenario { private final String specName; private final Map configuration; - private final HttpRequest request; + private final RequestSupplier request; private final BiConsumer> assertion; private TestScenario(String specName, Map configuration, - HttpRequest request, + RequestSupplier request, BiConsumer> assertion) { this.specName = specName; this.configuration = configuration; @@ -65,6 +65,26 @@ public static void asserts(String specName, .run(); } + /** + * + * @param specName Value for {@literal spec.name} property. Used to avoid bean pollution. + * @param configuration Test Scenario configuration + * @param request HTTP Request to be sent in the test scenario + * @param assertion Assertion for a request and server. + * @throws IOException Exception thrown while getting the server under test. + */ + public static void asserts(String specName, + Map configuration, + RequestSupplier request, + BiConsumer> assertion) throws IOException { + TestScenario.builder() + .specName(specName) + .configuration(configuration) + .request(request) + .assertion(assertion) + .run(); + } + /** * * @param specName Value for {@literal spec.name} property. Used to avoid bean pollution. @@ -93,7 +113,7 @@ public static TestScenario.Builder builder() { private void run() throws IOException { try (ServerUnderTest server = ServerUnderTestProviderUtils.getServerUnderTestProvider().getServer(specName, configuration)) { if (assertion != null) { - assertion.accept(server, request); + assertion.accept(server, request.apply(server)); } } } @@ -109,7 +129,7 @@ public static class Builder { private BiConsumer> assertion; - private HttpRequest request; + private RequestSupplier request; /** * @@ -127,7 +147,17 @@ public Builder specName(String specName) { * @return The Test Scneario Builder */ public Builder request(HttpRequest request) { - this.request = request; + this.request = server -> request; + return this; + } + + /** + * + * @param request HTTP Request supplier that given a server, provides the request to be sent in the test scenario + * @return The Test Scenario Builder + */ + public Builder request(RequestSupplier request) { + this.request = request; return this; } diff --git a/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/cors/CorsSimpleRequestTest.java b/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/cors/CorsSimpleRequestTest.java index 9cd73212c44..bed3249894c 100644 --- a/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/cors/CorsSimpleRequestTest.java +++ b/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/cors/CorsSimpleRequestTest.java @@ -30,6 +30,8 @@ import io.micronaut.http.client.multipart.MultipartBody; import io.micronaut.http.server.tck.AssertionUtils; import io.micronaut.http.server.tck.HttpResponseAssertion; +import io.micronaut.http.server.tck.RequestSupplier; +import io.micronaut.http.server.tck.ServerUnderTest; import io.micronaut.runtime.context.scope.refresh.RefreshEvent; import jakarta.inject.Inject; import jakarta.inject.Singleton; @@ -69,15 +71,30 @@ void corsSimpleRequestNotAllowedForLocalhostAndAny() throws IOException { asserts(SPECNAME, Collections.singletonMap(PROPERTY_MICRONAUT_SERVER_CORS_ENABLED, StringUtils.TRUE), createRequest("https://foo.com"), - (server, request) -> { - RefreshCounter refreshCounter = server.getApplicationContext().getBean(RefreshCounter.class); - assertEquals(0, refreshCounter.getRefreshCount()); - AssertionUtils.assertThrows(server, request, HttpResponseAssertion.builder() - .status(HttpStatus.FORBIDDEN) - .assertResponse(response -> assertFalse(response.getHeaders().contains("Vary"))) - .build()); - assertEquals(0, refreshCounter.getRefreshCount()); - }); + CorsSimpleRequestTest::isForbidden + ); + } + + /** + * @see GHSA-583g-g682-crxf + * + * A malicious/compromised website can make HTTP requests to 127.0.0.1. Normally, such requests would trigger a CORS preflight check which would prevent the request; however, some requests are "simple" and do not require a preflight check. These endpoints, if enabled and not secured, are vulnerable to being triggered. + * Example with Javascript: + *
+     * let url = "http://127.0.0.1:8080/refresh";
+     * let body = new FormData();
+     * body.append("force", "true");
+     * fetch(url, { method: "POST", body });
+     * 
+ * @throws IOException may throw the try for resources + */ + @Test + void corsSimpleRequestNotAllowedFor127AndAny() throws IOException { + asserts(SPECNAME, + Collections.singletonMap(PROPERTY_MICRONAUT_SERVER_CORS_ENABLED, StringUtils.TRUE), + createRequestFor("127.0.0.1", "https://foo.com"), + CorsSimpleRequestTest::isForbidden + ); } /** @@ -89,14 +106,50 @@ void corsSimpleRequestAllowedForLocalhostAndOriginLocalhost() throws IOException asserts(SPECNAME, Collections.singletonMap(PROPERTY_MICRONAUT_SERVER_CORS_ENABLED, StringUtils.TRUE), createRequest("http://localhost:8000"), - (server, request) -> { - RefreshCounter refreshCounter = server.getApplicationContext().getBean(RefreshCounter.class); - assertEquals(0, refreshCounter.getRefreshCount()); - AssertionUtils.assertDoesNotThrow(server, request, HttpResponseAssertion.builder() - .status(HttpStatus.OK) - .build()); - assertEquals(1, refreshCounter.getRefreshCount()); - }); + CorsSimpleRequestTest::isSuccessful + ); + } + + /** + * A request to localhost with an origin of 127.0.0.1 should be allowed as they are both local. + * + * @throws IOException + */ + @Test + void corsSimpleRequestAllowedForLocalhostAnd127Origin() throws IOException { + asserts(SPECNAME, + Collections.singletonMap(PROPERTY_MICRONAUT_SERVER_CORS_ENABLED, StringUtils.TRUE), + createRequestFor("localhost", "http://127.0.0.1:8000"), + CorsSimpleRequestTest::isSuccessful + ); + } + + /** + * Spoof attempt with origin should fail. + * + * @throws IOException + */ + @Test + void corsSimpleRequestFailsForLocalhostAndSpoofed127Origin() throws IOException { + asserts(SPECNAME, + Collections.singletonMap(PROPERTY_MICRONAUT_SERVER_CORS_ENABLED, StringUtils.TRUE), + createRequestFor("localhost", "http://127.0.0.1.hac0r.com:8000"), + CorsSimpleRequestTest::isForbidden + ); + } + + /** + * A request to 127.0.0.1 with an origin of localhost should succeed as they're both local. + * + * @throws IOException + */ + @Test + void corsSimpleRequestAllowedFor127RequestAndLocalhostOrigin() throws IOException { + asserts(SPECNAME, + Collections.singletonMap(PROPERTY_MICRONAUT_SERVER_CORS_ENABLED, StringUtils.TRUE), + createRequestFor("127.0.0.1", "http://localhost:8000"), + CorsSimpleRequestTest::isSuccessful + ); } /** @@ -131,8 +184,35 @@ void corsSimpleRequestForLocalhostCanBeAllowedViaConfiguration() throws IOExcept }); } + private RequestSupplier createRequestFor(String host, String origin) { + return server -> createRequest(server.getPort().map(p -> "http://" + host + ":" + p + "/refresh").orElseThrow(() -> new RuntimeException("Unknown port for " + server)), origin); + } + + static void isForbidden(ServerUnderTest server, HttpRequest request) { + RefreshCounter refreshCounter = server.getApplicationContext().getBean(RefreshCounter.class); + assertEquals(0, refreshCounter.getRefreshCount()); + AssertionUtils.assertThrows(server, request, HttpResponseAssertion.builder() + .status(HttpStatus.FORBIDDEN) + .assertResponse(response -> assertFalse(response.getHeaders().contains("Vary"))) + .build()); + assertEquals(0, refreshCounter.getRefreshCount()); + } + + static void isSuccessful(ServerUnderTest server, HttpRequest request) { + RefreshCounter refreshCounter = server.getApplicationContext().getBean(RefreshCounter.class); + assertEquals(0, refreshCounter.getRefreshCount()); + AssertionUtils.assertDoesNotThrow(server, request, HttpResponseAssertion.builder() + .status(HttpStatus.OK) + .build()); + assertEquals(1, refreshCounter.getRefreshCount()); + } + static HttpRequest createRequest(String origin) { - return HttpRequest.POST("/refresh", MultipartBody.builder().addPart("force", StringUtils.TRUE).build()) + return createRequest("/refresh", origin); + } + + static HttpRequest createRequest(String uri, String origin) { + return HttpRequest.POST(uri, MultipartBody.builder().addPart("force", StringUtils.TRUE).build()) .header("Content-Type", "multipart/form-data; boundary=----WebKitFormBoundarywxiDZy8kMlSE59h1") .header("Origin", origin) .header("Accept-Encoding", "gzip, deflate") diff --git a/http-server/src/main/java/io/micronaut/http/server/cors/CorsFilter.java b/http-server/src/main/java/io/micronaut/http/server/cors/CorsFilter.java index 55cc1ca0ca0..f89dc75bc78 100644 --- a/http-server/src/main/java/io/micronaut/http/server/cors/CorsFilter.java +++ b/http-server/src/main/java/io/micronaut/http/server/cors/CorsFilter.java @@ -21,6 +21,7 @@ import io.micronaut.core.convert.ArgumentConversionContext; import io.micronaut.core.convert.ConversionContext; import io.micronaut.core.convert.ImmutableArgumentConversionContext; +import io.micronaut.core.io.socket.SocketUtils; import io.micronaut.core.util.StringUtils; import io.micronaut.http.HttpHeaders; import io.micronaut.http.HttpMethod; @@ -40,6 +41,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.net.URI; import java.util.ArrayList; import java.util.List; import java.util.Optional; @@ -62,7 +64,6 @@ public class CorsFilter implements HttpServerFilter { private static final Logger LOG = LoggerFactory.getLogger(CorsFilter.class); private static final ArgumentConversionContext CONVERSION_CONTEXT_HTTP_METHOD = ImmutableArgumentConversionContext.of(HttpMethod.class); - private static final String LOCALHOST = "http://localhost"; protected final HttpServerConfiguration.CorsConfiguration corsConfiguration; @@ -121,7 +122,7 @@ public Publisher> doFilter(HttpRequest request, Server * * @param corsOriginConfiguration CORS Origin configuration for request's HTTP Header origin. * @param request HTTP Request - * @return {@literal true} if the resolved host starts with {@literal http://localhost} and the CORS configuration has any for allowed origins. + * @return {@literal true} if the resolved host is localhost or 127.0.0.1 address and the CORS configuration has any for allowed origins. */ protected boolean shouldDenyToPreventDriveByLocalhostAttack(@NonNull CorsOriginConfiguration corsOriginConfiguration, @NonNull HttpRequest request) { @@ -132,19 +133,18 @@ protected boolean shouldDenyToPreventDriveByLocalhostAttack(@NonNull CorsOriginC if (origin == null) { return false; } - if (origin.startsWith(LOCALHOST)) { + if (isOriginLocal(origin)) { return false; } String host = httpHostResolver.resolve(request); - return isAny(corsOriginConfiguration.getAllowedOrigins()) && host.startsWith(LOCALHOST); - + return isAny(corsOriginConfiguration.getAllowedOrigins()) && isHostLocal(host); } /** * * @param origin HTTP Header {@link HttpHeaders#ORIGIN} value. * @param request HTTP Request - * @return {@literal true} if the resolved host starts with {@literal http://localhost} and origin does not start with localhost deny it. + * @return {@literal true} if the resolved host is localhost or 127.0.0.1 and origin is not one of these then deny it. */ protected boolean shouldDenyToPreventDriveByLocalhostAttack(@NonNull String origin, @NonNull HttpRequest request) { @@ -152,7 +152,45 @@ protected boolean shouldDenyToPreventDriveByLocalhostAttack(@NonNull String orig return false; } String host = httpHostResolver.resolve(request); - return !origin.startsWith(LOCALHOST) && host.startsWith(LOCALHOST); + return !isOriginLocal(origin) && isHostLocal(host); + } + + /* + * We only need to check host for starting with "localhost" "127." (as there are multiple loopback addresses on linux) + * + * This is fine for host, as the request had to get here. + * + * We check the first character as a performance optimization prior to calling startsWith. + */ + private boolean isHostLocal(@NonNull String hostString) { + if (hostString.isEmpty()) { + return false; + } + char initialChar = hostString.charAt(0); + if (initialChar != 'h' && initialChar != 'w') { + return false; + } + return hostString.startsWith("http://localhost") + || hostString.startsWith("https://localhost") + || hostString.startsWith("http://127.") + || hostString.startsWith("https://127.") + || hostString.startsWith("ws://localhost") + || hostString.startsWith("wss://localhost") + || hostString.startsWith("ws://127.") + || hostString.startsWith("wss://127."); + } + + /* + * For Origin, we need to be more strict as otherwise an address like 127.malicious.com would be allowed. + */ + private boolean isOriginLocal(@NonNull String hostString) { + try { + URI uri = URI.create(hostString); + String host = uri.getHost(); + return SocketUtils.LOCALHOST.equals(host) || "127.0.0.1".equals(host); + } catch (IllegalArgumentException e) { + return false; + } } @Override From c9820a7c45c9f38d3b3503ac8b4aa787614df6b2 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 27 Jan 2023 13:23:40 +0100 Subject: [PATCH 427/743] fix(deps): update dependency io.micronaut.tracing:micronaut-tracing-bom to v4.5.0 (#8554) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 5f507d0c927..e44b28d4132 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -45,7 +45,7 @@ micronaut-session = "1.0.0-SNAPSHOT" micronaut-sql = "4.7.2" micronaut-test = "4.0.0-SNAPSHOT" micronaut-serde = "2.0.0-SNAPSHOT" -micronaut-tracing = "4.4.0" +micronaut-tracing = "4.5.0" micrometer = "1.10.2" neo4j-java-driver = "1.4.5" selenium = "4.7.2" From 2f7f440b3df33df86da5d032bbb9d053ed4e2b58 Mon Sep 17 00:00:00 2001 From: Tim Yates Date: Fri, 27 Jan 2023 12:24:41 +0000 Subject: [PATCH 428/743] Add discovery 3.3.0 BOM to build [skip ci] (#8659) Once 3.3.0 is released with a BOM, we can add it to the catalog --- gradle/libs.versions.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 50052c8fca1..271e20ae0aa 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -74,7 +74,7 @@ managed-micronaut-cassandra = "5.1.1" managed-micronaut-coherence = "3.7.2" managed-micronaut-crac = "1.1.1" managed-micronaut-data = "3.9.4" -managed-micronaut-discovery = "3.2.0" +managed-micronaut-discovery = "3.3.0" managed-micronaut-elasticsearch = "4.3.0" managed-micronaut-email = "1.5.0" managed-micronaut-flyway = "5.4.1" @@ -154,6 +154,7 @@ boms-micronaut-coherence = { module = "io.micronaut.coherence:micronaut-coherenc boms-micronaut-crac = { module = "io.micronaut.crac:micronaut-crac-bom", version.ref = "managed-micronaut-crac" } boms-micronaut-email = { module = "io.micronaut.email:micronaut-email-bom", version.ref = "managed-micronaut-email" } boms-micronaut-data = { module = "io.micronaut.data:micronaut-data-bom", version.ref = "managed-micronaut-data" } +boms-micronaut-discovery = { module = "io.micronaut.discovery:micronaut-discovery-client-bom", version.ref = "managed-micronaut-discovery" } boms-micronaut-gcp = { module = "io.micronaut.gcp:micronaut-gcp-bom", version.ref = "managed-micronaut-gcp" } boms-micronaut-grpc = { module = "io.micronaut.grpc:micronaut-grpc-bom", version.ref = "managed-micronaut-grpc" } boms-micronaut-groovy = { module = "io.micronaut.groovy:micronaut-groovy-bom", version.ref = "managed-micronaut-groovy" } @@ -291,7 +292,6 @@ managed-netty-transport-native-unix-common = { module = "io.netty:netty-transpor managed-micronaut-acme = { module = "io.micronaut.acme:micronaut-acme", version.ref = "managed-micronaut-acme" } managed-micronaut-cassandra = { module = "io.micronaut.cassandra:micronaut-cassandra", version.ref = "managed-micronaut-cassandra" } -managed-micronaut-discovery = { module = "io.micronaut.discovery:micronaut-discovery-client", version.ref = "managed-micronaut-discovery" } managed-micronaut-elasticsearch = { module = "io.micronaut.elasticsearch:micronaut-elasticsearch", version.ref = "managed-micronaut-elasticsearch" } managed-micronaut-graphql = { module = "io.micronaut.graphql:micronaut-graphql", version.ref = "managed-micronaut-graphql" } managed-micronaut-ignite-core = { module = "io.micronaut.ignite:micronaut-ignite-core", version.ref = "managed-micronaut-ignite" } From 3be27837b924b16959e4b3ce8a37f7bc324a8eec Mon Sep 17 00:00:00 2001 From: micronaut-build Date: Sat, 28 Jan 2023 04:23:50 +0000 Subject: [PATCH 429/743] [skip ci] Release v3.7.6 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 03c9dbfe0d9..af01097dd2f 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -projectVersion=3.7.6-SNAPSHOT +projectVersion=3.7.6 projectGroupId=io.micronaut title=Micronaut projectDesc=Natively Cloud Native From 142c10af0349b183b9c8664758a7d89cb5a226d9 Mon Sep 17 00:00:00 2001 From: micronaut-build Date: Sat, 28 Jan 2023 04:41:34 +0000 Subject: [PATCH 430/743] Back to 3.7.7-SNAPSHOT --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index af01097dd2f..9455edf2ec1 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -projectVersion=3.7.6 +projectVersion=3.7.7-SNAPSHOT projectGroupId=io.micronaut title=Micronaut projectDesc=Natively Cloud Native From 8ab03d2c5ea2c7e3a92d7830256829301c117f05 Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Sat, 28 Jan 2023 08:43:37 +0100 Subject: [PATCH 431/743] build: micronau-tracing 4.5.0 (#8672) Close #8548 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 271e20ae0aa..4b7b54f6eaf 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -120,7 +120,7 @@ managed-micronaut-sql = "4.7.2" managed-micronaut-test = "3.8.0" managed-micronaut-test-resources = "1.2.3" managed-micronaut-toml = "1.1.3" -managed-micronaut-tracing = "4.4.0" +managed-micronaut-tracing = "4.5.0" managed-micronaut-tracing-legacy = "3.2.7" managed-micronaut-views = "3.8.1" managed-micronaut-xml = "3.2.0" From fec5ecfdc101b2a559f68e7b942fe3d666f450cb Mon Sep 17 00:00:00 2001 From: micronaut-build <65172877+micronaut-build@users.noreply.github.com> Date: Sat, 28 Jan 2023 08:46:02 +0100 Subject: [PATCH 432/743] build: bump micronaut-openapi to 4.8.2 (#8656) --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index e8a1e3bec46..64fd5ff6bd5 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -100,7 +100,7 @@ managed-micronaut-neo4j = "5.2.0" managed-micronaut-nats = "3.1.0" managed-micronaut-netflix = "2.1.0" managed-micronaut-object-storage = "1.1.0" -managed-micronaut-openapi = "4.8.1" +managed-micronaut-openapi = "4.8.2" managed-micronaut-oraclecloud = "2.3.2" managed-micronaut-picocli = "4.3.0" managed-micronaut-problem = "2.6.0" From 68f9bb0a78fa930865d37fca39252b9ec66e4a43 Mon Sep 17 00:00:00 2001 From: micronaut-build Date: Sat, 28 Jan 2023 08:54:51 +0000 Subject: [PATCH 433/743] [skip ci] Release v3.8.3 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index a561ef36e89..5edab0a927b 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -projectVersion=3.8.3-SNAPSHOT +projectVersion=3.8.3 projectGroupId=io.micronaut title=Micronaut projectDesc=Natively Cloud Native From 4bfd29c8468f7f17a5a473e752e70b26ff7f30e1 Mon Sep 17 00:00:00 2001 From: micronaut-build Date: Sat, 28 Jan 2023 09:07:44 +0000 Subject: [PATCH 434/743] Back to 3.8.4-SNAPSHOT --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 5edab0a927b..d5f61384e93 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -projectVersion=3.8.3 +projectVersion=3.8.4-SNAPSHOT projectGroupId=io.micronaut title=Micronaut projectDesc=Natively Cloud Native From 34c6f8eb7a393bf67cb0746c75eb0e15b56b1e49 Mon Sep 17 00:00:00 2001 From: micronaut-build <65172877+micronaut-build@users.noreply.github.com> Date: Mon, 30 Jan 2023 07:24:25 +0100 Subject: [PATCH 435/743] Bump micronaut-micrometer to 4.7.2 (#8681) --- gradle/libs.versions.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 64fd5ff6bd5..02503b36646 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -64,7 +64,7 @@ managed-logback = "1.2.11" managed-lombok = "1.18.24" managed-maven-native-plugin = "0.9.19" managed-methvin-directory-watcher = "0.16.1" -managed-micrometer = "1.9.4" +managed-micrometer = "" managed-micronaut-acme = "3.2.0" managed-micronaut-aot = "1.1.1" managed-micronaut-aws = "3.10.5" @@ -90,7 +90,7 @@ managed-micronaut-jmx = "3.2.0" managed-micronaut-kafka = "4.5.0" managed-micronaut-kotlin = "3.2.2" managed-micronaut-kubernetes = "3.4.0" -managed-micronaut-micrometer = "4.7.1" +managed-micronaut-micrometer = "4.7.2" managed-micronaut-microstream = "1.3.0" managed-micronaut-liquibase = "5.6.0" managed-micronaut-mongo = "4.6.0" From 818d1adce75099be4effbf1fd5544c4d32899e8e Mon Sep 17 00:00:00 2001 From: micronaut-build <65172877+micronaut-build@users.noreply.github.com> Date: Mon, 30 Jan 2023 08:34:16 +0100 Subject: [PATCH 436/743] build: Bump micronaut-azure to 3.7.1 (#8682) --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 02503b36646..75f62b107a4 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -68,7 +68,7 @@ managed-micrometer = "" managed-micronaut-acme = "3.2.0" managed-micronaut-aot = "1.1.1" managed-micronaut-aws = "3.10.5" -managed-micronaut-azure = "4.0.0" +managed-micronaut-azure = "3.7.1" managed-micronaut-cache = "3.5.0" managed-micronaut-cassandra = "5.1.1" managed-micronaut-coherence = "3.7.2" From 5f473c0637cfb5cc4ff3bd1a32be646455b89d59 Mon Sep 17 00:00:00 2001 From: micronaut-build <65172877+micronaut-build@users.noreply.github.com> Date: Mon, 30 Jan 2023 09:19:58 +0100 Subject: [PATCH 437/743] Bump micronaut-azure to 3.8.0 (#8683) --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index aca2470a628..7e2e5c9b52b 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -68,7 +68,7 @@ managed-micrometer = "1.9.4" managed-micronaut-acme = "3.2.0" managed-micronaut-aot = "1.1.1" managed-micronaut-aws = "3.12.0" -managed-micronaut-azure = "4.0.0" +managed-micronaut-azure = "3.8.0" managed-micronaut-cache = "3.5.0" managed-micronaut-cassandra = "5.1.1" managed-micronaut-coherence = "3.7.2" From e32c6536e52f393e7bd474a5de2516b921239e67 Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Mon, 30 Jan 2023 13:48:12 +0100 Subject: [PATCH 438/743] build: back to managed-micrometer 1.9.4 changed accidentally via https://github.com/micronaut-projects/micronaut-core/pull/8681 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 75f62b107a4..a92a1b1c6fd 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -64,7 +64,7 @@ managed-logback = "1.2.11" managed-lombok = "1.18.24" managed-maven-native-plugin = "0.9.19" managed-methvin-directory-watcher = "0.16.1" -managed-micrometer = "" +managed-micrometer = "1.9.4" managed-micronaut-acme = "3.2.0" managed-micronaut-aot = "1.1.1" managed-micronaut-aws = "3.10.5" From 870caee0c021a7932be10807249cff94c9822b6c Mon Sep 17 00:00:00 2001 From: Tim Yates Date: Mon, 30 Jan 2023 12:48:54 +0000 Subject: [PATCH 439/743] Fix build by re-adding the managed-micrometer version (#8685) This PR https://github.com/micronaut-projects/micronaut-core/pull/8681 deleted the micrometer version Here we replace it with the version of Micrometer that is found in micronaut-micrometer 4.7.2 Co-authored-by: Sergio del Amo --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index a92a1b1c6fd..f2d28d46d5f 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -64,7 +64,7 @@ managed-logback = "1.2.11" managed-lombok = "1.18.24" managed-maven-native-plugin = "0.9.19" managed-methvin-directory-watcher = "0.16.1" -managed-micrometer = "1.9.4" +managed-micrometer = "1.10.3" managed-micronaut-acme = "3.2.0" managed-micronaut-aot = "1.1.1" managed-micronaut-aws = "3.10.5" From d345f1107b2c7069c14a057a2446a44467f42cec Mon Sep 17 00:00:00 2001 From: Jonas Konrad Date: Mon, 30 Jan 2023 16:08:50 +0100 Subject: [PATCH 440/743] Add docs on self-signed cert setup (#8684) --- .../guide/httpServer/serverConfiguration/https.adoc | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/main/docs/guide/httpServer/serverConfiguration/https.adoc b/src/main/docs/guide/httpServer/serverConfiguration/https.adoc index 766ab4460ae..f576c0e5e97 100644 --- a/src/main/docs/guide/httpServer/serverConfiguration/https.adoc +++ b/src/main/docs/guide/httpServer/serverConfiguration/https.adoc @@ -1,3 +1,4 @@ + Micronaut supports HTTPS out of the box. By default HTTPS is disabled and all requests are served using HTTP. To enable HTTPS support, modify your configuration. For example with `application.yml`: .HTTPS Configuration Example @@ -13,6 +14,15 @@ micronaut: TIP: By default Micronaut with HTTPS support starts on port `8443` but you can change the port with the property `micronaut.server.ssl.port`. +For generating self-signed certificates, the Micronaut HTTP server will use netty. Netty uses one of two approaches to generate the certificate. + +If you use a pre-generated certificate (as you should, for security), these steps are not necessary. + +- Netty can use the JDK-internal `sun.security.x509` package. On newer JDK versions, this package is restricted and may not work. You may need to add `--add-exports=java.base/sun.security.x509=ALL-UNNAMED` as a VM parameter. +- Alternatively, netty will use the Bouncy Castle BCPKIX API. This needs an additional dependency: + +dependency:org.bouncycastle:bcpkix-jdk15on[scope="implementation"] + WARNING: This configuration will generate a warning in the browser. image::https-warning.jpg[] From d40eb254683f7cb9fa824c7c012994e54d96a6de Mon Sep 17 00:00:00 2001 From: micronaut-build <65172877+micronaut-build@users.noreply.github.com> Date: Wed, 1 Feb 2023 06:34:25 +0100 Subject: [PATCH 441/743] Bump micronaut-aws to 3.13.1 (#8694) --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 7e2e5c9b52b..1136c171fbf 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -67,7 +67,7 @@ managed-methvin-directory-watcher = "0.16.1" managed-micrometer = "1.9.4" managed-micronaut-acme = "3.2.0" managed-micronaut-aot = "1.1.1" -managed-micronaut-aws = "3.12.0" +managed-micronaut-aws = "3.13.1" managed-micronaut-azure = "3.8.0" managed-micronaut-cache = "3.5.0" managed-micronaut-cassandra = "5.1.1" From 8bf18c176e17ce3ecff27d787265d0a33556aca0 Mon Sep 17 00:00:00 2001 From: micronaut-build <65172877+micronaut-build@users.noreply.github.com> Date: Wed, 1 Feb 2023 06:34:44 +0100 Subject: [PATCH 442/743] Bump micronaut-tracing to 4.4.1 (#8692) --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 8249c4c1f51..3370dcbb159 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -120,7 +120,7 @@ managed-micronaut-sql = "4.7.2" managed-micronaut-test = "3.6.2" managed-micronaut-test-resources = "1.1.3" managed-micronaut-toml = "1.1.1" -managed-micronaut-tracing = "4.4.0" +managed-micronaut-tracing = "4.4.1" managed-micronaut-tracing-legacy = "3.2.7" managed-micronaut-views = "3.6.0" managed-micronaut-xml = "3.1.0" From 03dfd87fe95d655a27ff3daadaba43b298c57f81 Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Wed, 1 Feb 2023 09:37:15 +0100 Subject: [PATCH 443/743] Revert "build: micronau-tracing 4.5.0 (#8672)" This reverts commit 8ab03d2c5ea2c7e3a92d7830256829301c117f05. --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index bd0bbbd15a8..379c8a51dd7 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -120,7 +120,7 @@ managed-micronaut-sql = "4.7.2" managed-micronaut-test = "3.8.0" managed-micronaut-test-resources = "1.2.3" managed-micronaut-toml = "1.1.3" -managed-micronaut-tracing = "4.5.0" +managed-micronaut-tracing = "4.4.0" managed-micronaut-tracing-legacy = "3.2.7" managed-micronaut-views = "3.8.1" managed-micronaut-xml = "3.2.0" From 51c4106feeacbef7cc8da432df8e268d18998a71 Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Wed, 1 Feb 2023 12:40:45 +0100 Subject: [PATCH 444/743] test: require docker for test (#8698) --- .../io/micronaut/docs/http/client/proxy/ClientProxySpec.groovy | 3 +++ 1 file changed, 3 insertions(+) diff --git a/test-suite/src/test/groovy/io/micronaut/docs/http/client/proxy/ClientProxySpec.groovy b/test-suite/src/test/groovy/io/micronaut/docs/http/client/proxy/ClientProxySpec.groovy index 13fc2569040..dfd56840873 100644 --- a/test-suite/src/test/groovy/io/micronaut/docs/http/client/proxy/ClientProxySpec.groovy +++ b/test-suite/src/test/groovy/io/micronaut/docs/http/client/proxy/ClientProxySpec.groovy @@ -7,13 +7,16 @@ import io.micronaut.http.HttpStatus import io.micronaut.http.client.HttpClient import io.micronaut.http.client.exceptions.HttpClientException import io.micronaut.runtime.server.EmbeddedServer +import org.testcontainers.DockerClientFactory import org.testcontainers.containers.GenericContainer import org.testcontainers.containers.wait.strategy.HostPortWaitStrategy import org.testcontainers.utility.MountableFile import spock.lang.AutoCleanup +import spock.lang.Requires import spock.lang.Retry import spock.lang.Specification +@Requires({ DockerClientFactory.instance().isDockerAvailable() }) @Retry class ClientProxySpec extends Specification { From 15c0dfb68d4f04211bcba397499029579366c4b2 Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Wed, 1 Feb 2023 15:33:42 +0100 Subject: [PATCH 445/743] build: add back managed-micronaut-discovery (#8702) * build: Micronaut Build Plugin 5.4.2 * build: add back managed-micronaut-discovery otherwise ./gradlew bom:checkVersionCatalogueCompatibility fails it was removed in https://github.com/micronaut-projects/micronaut-core/pull/8659 --- gradle/libs.versions.toml | 1 + settings.gradle | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 379c8a51dd7..12a63b08e02 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -292,6 +292,7 @@ managed-netty-transport-native-unix-common = { module = "io.netty:netty-transpor managed-micronaut-acme = { module = "io.micronaut.acme:micronaut-acme", version.ref = "managed-micronaut-acme" } managed-micronaut-cassandra = { module = "io.micronaut.cassandra:micronaut-cassandra", version.ref = "managed-micronaut-cassandra" } +managed-micronaut-discovery = { module = "io.micronaut.discovery:micronaut-discovery-client", version.ref = "managed-micronaut-discovery" } managed-micronaut-elasticsearch = { module = "io.micronaut.elasticsearch:micronaut-elasticsearch", version.ref = "managed-micronaut-elasticsearch" } managed-micronaut-graphql = { module = "io.micronaut.graphql:micronaut-graphql", version.ref = "managed-micronaut-graphql" } managed-micronaut-ignite-core = { module = "io.micronaut.ignite:micronaut-ignite-core", version.ref = "managed-micronaut-ignite" } diff --git a/settings.gradle b/settings.gradle index 7724dccad4c..10ad890d07a 100644 --- a/settings.gradle +++ b/settings.gradle @@ -14,7 +14,7 @@ pluginManagement { } plugins { - id 'io.micronaut.build.shared.settings' version '5.3.16' + id 'io.micronaut.build.shared.settings' version '5.4.2' } enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") From c54771712c992c08653cc066ec447906afc26d54 Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Wed, 1 Feb 2023 16:46:14 +0100 Subject: [PATCH 446/743] build: micronau-tracing 4.5.0 (#8700) * Silence Micronaut Tracing BOM validation problems Co-authored-by: Cedric Champeau --- bom/build.gradle | 26 ++++++++++++++++++++++++-- gradle/libs.versions.toml | 2 +- 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/bom/build.gradle b/bom/build.gradle index 3fb8269bca8..79178f1f5e6 100644 --- a/bom/build.gradle +++ b/bom/build.gradle @@ -47,10 +47,32 @@ micronautBom { "io.opentelemetry.instrumentation:opentelemetry-instrumentation-bom-alpha", ["io.opentelemetry.javaagent", "io.opentelemetry", "io.opentelemetry.instrumentation", "io.opentelemetry.javaagent.instrumentation"] as Set ) - dependencies.add("io.opentelemetry:opentelemetry-bom:1.15.0") - dependencies.add("io.opentelemetry:opentelemetry-bom-alpha:1.15.0-alpha") + dependencies.add("io.opentelemetry:opentelemetry-bom:1.19.0") + dependencies.add("io.opentelemetry:opentelemetry-bom-alpha:1.19.0-alpha") + dependencies.add("io.opentelemetry.instrumentation:opentelemetry-instrumentation-bom:1.19.2") + dependencies.add("io.zipkin.brave:brave-bom:5.14.1") + dependencies.add("io.zipkin.reporter2:zipkin-reporter-bom:2.16.3") // The R2DBC bom that we include mentions dependencies which do not belong to io.r2dbc group bomAuthorizedGroupIds.put("io.r2dbc:r2dbc-bom", ["com.google.cloud", "com.oracle.database.r2dbc", "org.mariadb", "dev.miku"] as Set) + + // Tracing + bomAuthorizedGroupIds.put( + "io.zipkin.brave:brave-bom", + [ + "io.zipkin.brave", + "io.zipkin.zipkin2", + "io.zipkin.reporter2", + "io.zipkin.proto3" + ] as Set + ) + bomAuthorizedGroupIds.put( + "io.zipkin.reporter2:zipkin-reporter-bom", + [ + "io.zipkin.reporter2", + "io.zipkin.zipkin2", + "io.zipkin.proto3" + ] as Set + ) } } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 12a63b08e02..4794365be3f 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -120,7 +120,7 @@ managed-micronaut-sql = "4.7.2" managed-micronaut-test = "3.8.0" managed-micronaut-test-resources = "1.2.3" managed-micronaut-toml = "1.1.3" -managed-micronaut-tracing = "4.4.0" +managed-micronaut-tracing = "4.5.0" managed-micronaut-tracing-legacy = "3.2.7" managed-micronaut-views = "3.8.1" managed-micronaut-xml = "3.2.0" From 1ab8748fba370bf20c03819d8e2085fbcbed1d93 Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Wed, 1 Feb 2023 16:54:20 +0100 Subject: [PATCH 447/743] remove unused file --- .../core/exceptions/UncheckedIOException.java | 29 ------------------- 1 file changed, 29 deletions(-) delete mode 100644 core/src/main/java/io/micronaut/core/exceptions/UncheckedIOException.java diff --git a/core/src/main/java/io/micronaut/core/exceptions/UncheckedIOException.java b/core/src/main/java/io/micronaut/core/exceptions/UncheckedIOException.java deleted file mode 100644 index dec749a3cd6..00000000000 --- a/core/src/main/java/io/micronaut/core/exceptions/UncheckedIOException.java +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright 2017-2022 original authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.micronaut.core.exceptions; - -import java.io.IOException; - -/** - * Wraps a {@link IOException} in a {@link RuntimeException}. - * @author Sergio del Amo - * @since 3.5.6 - */ -public class UncheckedIOException extends RuntimeException { - public UncheckedIOException(IOException e) { - super(e); - } -} From 8e483c36e48221c7b0ae17f123a9c83c580639ad Mon Sep 17 00:00:00 2001 From: micronaut-build <65172877+micronaut-build@users.noreply.github.com> Date: Mon, 6 Feb 2023 10:17:29 +0100 Subject: [PATCH 448/743] build: Bump micronaut-rabbitmq to 3.4.1 (#8709) --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 7e8a255c567..6933b588d86 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -104,7 +104,7 @@ managed-micronaut-openapi = "4.8.2" managed-micronaut-oraclecloud = "2.3.2" managed-micronaut-picocli = "4.3.0" managed-micronaut-problem = "2.6.0" -managed-micronaut-rabbitmq = "3.4.0" +managed-micronaut-rabbitmq = "3.4.1" managed-micronaut-r2dbc = "4.0.0" managed-micronaut-reactor = "2.5.0" managed-micronaut-redis = "5.3.2" From 8ea12140565ac5bad9754517f237e62303d748ee Mon Sep 17 00:00:00 2001 From: micronaut-build <65172877+micronaut-build@users.noreply.github.com> Date: Mon, 6 Feb 2023 10:18:07 +0100 Subject: [PATCH 449/743] build: micronaut-data to 3.9.6 (#8711) --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 6933b588d86..76d84422216 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -73,7 +73,7 @@ managed-micronaut-cache = "3.5.0" managed-micronaut-cassandra = "5.1.1" managed-micronaut-coherence = "3.7.2" managed-micronaut-crac = "1.1.1" -managed-micronaut-data = "3.9.4" +managed-micronaut-data = "3.9.6" managed-micronaut-discovery = "3.2.0" managed-micronaut-elasticsearch = "4.3.0" managed-micronaut-email = "1.5.0" From b7a2b0b2af73462a1bdded4da7c340fb90c8647f Mon Sep 17 00:00:00 2001 From: Tim Yates Date: Mon, 6 Feb 2023 10:30:16 +0000 Subject: [PATCH 450/743] Allow programmatic logback config if xml config is absent (#8674) * Allow programmatic logback config if xml config is absent #closes 8650 * extract utils class * Add tests for #8678 and #8679 * Check programattic config first * Rollback runtime tests * More tests * Add a comment to the test * Checkstyle * Update javadoc * Switch to use a Supplier, so we don't resolve the URL unless necessary --------- Co-authored-by: Sergio del Amo --- .../loggers/impl/LogbackLoggingSystem.java | 17 +--- .../logging/impl/LogbackLoggingSystem.java | 17 +--- .../micronaut/logging/impl/LogbackUtils.java | 99 +++++++++++++++++++ .../LogbackLogLevelConfigurerSpec.groovy | 2 - settings.gradle | 1 + test-suite-logback/build.gradle | 18 ++++ .../logback/LoggerConfigurationSpec.groovy | 33 +++++++ .../logback/LoggerEndpointSpec.groovy | 71 +++++++++++++ .../micronaut/logback/LoggerLevelSpec.groovy | 46 +++++++++ .../micronaut/logback/MemoryAppender.groovy | 17 ++++ .../io/micronaut/logback/Application.java | 15 +++ .../logback/config/CustomConfigurator.java | 75 ++++++++++++++ .../controllers/HelloWorldController.java | 21 ++++ .../ch.qos.logback.classic.spi.Configurator | 1 + .../src/test/resources/logback.xml | 16 +++ 15 files changed, 416 insertions(+), 33 deletions(-) create mode 100644 runtime/src/main/java/io/micronaut/logging/impl/LogbackUtils.java create mode 100644 test-suite-logback/build.gradle create mode 100644 test-suite-logback/src/test/groovy/io/micronaut/logback/LoggerConfigurationSpec.groovy create mode 100644 test-suite-logback/src/test/groovy/io/micronaut/logback/LoggerEndpointSpec.groovy create mode 100644 test-suite-logback/src/test/groovy/io/micronaut/logback/LoggerLevelSpec.groovy create mode 100644 test-suite-logback/src/test/groovy/io/micronaut/logback/MemoryAppender.groovy create mode 100644 test-suite-logback/src/test/java/io/micronaut/logback/Application.java create mode 100644 test-suite-logback/src/test/java/io/micronaut/logback/config/CustomConfigurator.java create mode 100644 test-suite-logback/src/test/java/io/micronaut/logback/controllers/HelloWorldController.java create mode 100644 test-suite-logback/src/test/resources/META-INF/services/ch.qos.logback.classic.spi.Configurator create mode 100644 test-suite-logback/src/test/resources/logback.xml diff --git a/management/src/main/java/io/micronaut/management/endpoint/loggers/impl/LogbackLoggingSystem.java b/management/src/main/java/io/micronaut/management/endpoint/loggers/impl/LogbackLoggingSystem.java index d0b6440b505..99a9745cffe 100644 --- a/management/src/main/java/io/micronaut/management/endpoint/loggers/impl/LogbackLoggingSystem.java +++ b/management/src/main/java/io/micronaut/management/endpoint/loggers/impl/LogbackLoggingSystem.java @@ -18,15 +18,13 @@ import ch.qos.logback.classic.Level; import ch.qos.logback.classic.Logger; import ch.qos.logback.classic.LoggerContext; -import ch.qos.logback.classic.util.ContextInitializer; -import ch.qos.logback.core.joran.spi.JoranException; import io.micronaut.context.annotation.Property; import io.micronaut.context.annotation.Replaces; import io.micronaut.context.annotation.Requires; import io.micronaut.core.annotation.NonNull; import io.micronaut.core.annotation.Nullable; import io.micronaut.logging.LogLevel; -import io.micronaut.logging.LoggingSystemException; +import io.micronaut.logging.impl.LogbackUtils; import io.micronaut.management.endpoint.loggers.LoggerConfiguration; import io.micronaut.management.endpoint.loggers.LoggersEndpoint; import io.micronaut.management.endpoint.loggers.ManagedLoggingSystem; @@ -34,9 +32,7 @@ import jakarta.inject.Singleton; import org.slf4j.LoggerFactory; -import java.net.URL; import java.util.Collection; -import java.util.Objects; import java.util.stream.Collectors; /** @@ -135,15 +131,6 @@ private static Level toLevel(LogLevel logLevel) { public void refresh() { LoggerContext context = getLoggerContext(); context.reset(); - URL resource = getClass().getClassLoader().getResource(logbackXmlLocation); - if (Objects.isNull(resource)) { - throw new LoggingSystemException("Resource " + logbackXmlLocation + " not found"); - } - - try { - new ContextInitializer(context).configureByResource(resource); - } catch (JoranException e) { - throw new LoggingSystemException("Error while refreshing Logback", e); - } + LogbackUtils.configure(getClass().getClassLoader(), context, logbackXmlLocation); } } diff --git a/runtime/src/main/java/io/micronaut/logging/impl/LogbackLoggingSystem.java b/runtime/src/main/java/io/micronaut/logging/impl/LogbackLoggingSystem.java index 9aa81b153d5..bb7c0cc3ad4 100644 --- a/runtime/src/main/java/io/micronaut/logging/impl/LogbackLoggingSystem.java +++ b/runtime/src/main/java/io/micronaut/logging/impl/LogbackLoggingSystem.java @@ -15,20 +15,14 @@ */ package io.micronaut.logging.impl; -import java.net.URL; -import java.util.Objects; - import ch.qos.logback.classic.Level; import ch.qos.logback.classic.LoggerContext; -import ch.qos.logback.classic.util.ContextInitializer; -import ch.qos.logback.core.joran.spi.JoranException; import io.micronaut.context.annotation.Property; import io.micronaut.context.annotation.Requires; import io.micronaut.core.annotation.Internal; import io.micronaut.core.annotation.Nullable; import io.micronaut.logging.LogLevel; import io.micronaut.logging.LoggingSystem; -import io.micronaut.logging.LoggingSystemException; import jakarta.inject.Singleton; import org.slf4j.LoggerFactory; @@ -60,16 +54,7 @@ public void setLogLevel(String name, LogLevel level) { public void refresh() { LoggerContext context = getLoggerContext(); context.reset(); - URL resource = getClass().getClassLoader().getResource(logbackXmlLocation); - if (Objects.isNull(resource)) { - throw new LoggingSystemException("Resource " + logbackXmlLocation + " not found"); - } - - try { - new ContextInitializer(context).configureByResource(resource); - } catch (JoranException e) { - throw new LoggingSystemException("Error while refreshing Logback", e); - } + LogbackUtils.configure(getClass().getClassLoader(), context, logbackXmlLocation); } /** diff --git a/runtime/src/main/java/io/micronaut/logging/impl/LogbackUtils.java b/runtime/src/main/java/io/micronaut/logging/impl/LogbackUtils.java new file mode 100644 index 00000000000..79933d8facb --- /dev/null +++ b/runtime/src/main/java/io/micronaut/logging/impl/LogbackUtils.java @@ -0,0 +1,99 @@ +/* + * Copyright 2017-2023 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.logging.impl; + +import ch.qos.logback.classic.LoggerContext; +import ch.qos.logback.classic.spi.Configurator; +import ch.qos.logback.classic.util.ContextInitializer; +import ch.qos.logback.classic.util.EnvUtil; +import ch.qos.logback.core.joran.spi.JoranException; +import ch.qos.logback.core.status.InfoStatus; +import io.micronaut.core.annotation.NonNull; +import io.micronaut.logging.LoggingSystemException; + +import java.net.URL; +import java.util.function.Supplier; + +/** + * Utility methods to configure {@link LoggerContext}. + * + * @author Sergio del Amo + * @since 3.8.4 + */ +public final class LogbackUtils { + + private LogbackUtils() { + } + + /** + * Configures a Logger Context. + * + * @param classLoader Class Loader + * @param context Logger Context + * @param logbackXmlLocation the location of the xml logback config file + */ + public static void configure(@NonNull ClassLoader classLoader, + @NonNull LoggerContext context, + @NonNull String logbackXmlLocation) { + configure(context, logbackXmlLocation, () -> classLoader.getResource(logbackXmlLocation)); + } + + /** + * Configures a Logger Context. + * + * Searches fpr a custom {@link Configurator} via a service loader. + * If not present it configures the context with the resource. + * + * @param context Logger Context + * @param logbackXmlLocation the location of the xml logback config file + * @param resourceSupplier A resource for example logback.xml + */ + private static void configure( + @NonNull LoggerContext context, + @NonNull String logbackXmlLocation, + Supplier resourceSupplier + ) { + Configurator configurator = EnvUtil.loadFromServiceLoader(Configurator.class); + if (configurator != null) { + context.getStatusManager().add(new InfoStatus("Using " + configurator.getClass().getName(), context)); + programmaticConfiguration(context, configurator); + } else { + URL resource = resourceSupplier.get(); + if (resource != null) { + try { + new ContextInitializer(context).configureByResource(resource); + } catch (JoranException e) { + throw new LoggingSystemException("Error while refreshing Logback", e); + } + } else { + throw new LoggingSystemException("Resource " + logbackXmlLocation + " not found"); + } + } + } + + /** + * Taken from {@link ch.qos.logback.classic.util.ContextInitializer#autoConfig}. + */ + private static void programmaticConfiguration(@NonNull LoggerContext context, + @NonNull Configurator configurator) { + try { + configurator.setContext(context); + configurator.configure(context); + } catch (Exception e) { + throw new LoggingSystemException(String.format("Failed to initialize Configurator: %s using ServiceLoader", configurator.getClass().getCanonicalName()), e); + } + } +} diff --git a/runtime/src/test/groovy/io/micronaut/logging/LogbackLogLevelConfigurerSpec.groovy b/runtime/src/test/groovy/io/micronaut/logging/LogbackLogLevelConfigurerSpec.groovy index 949225d8974..972053990ff 100644 --- a/runtime/src/test/groovy/io/micronaut/logging/LogbackLogLevelConfigurerSpec.groovy +++ b/runtime/src/test/groovy/io/micronaut/logging/LogbackLogLevelConfigurerSpec.groovy @@ -14,11 +14,9 @@ import io.micronaut.context.exceptions.BeanInstantiationException import io.micronaut.context.exceptions.ConfigurationException import org.slf4j.LoggerFactory import spock.lang.Specification -import spock.lang.Unroll class LogbackLogLevelConfigurerSpec extends Specification { - @Unroll void 'test that log levels on logger "#loggerName" can be configured via properties'() { given: def loggerLevels = [ diff --git a/settings.gradle b/settings.gradle index 7724dccad4c..f5f04723bbf 100644 --- a/settings.gradle +++ b/settings.gradle @@ -69,6 +69,7 @@ include "test-suite-kotlin" include "test-suite-graal" include "test-suite-groovy" include "test-suite-groovy" +include "test-suite-logback" include "test-utils" // benchmarks diff --git a/test-suite-logback/build.gradle b/test-suite-logback/build.gradle new file mode 100644 index 00000000000..ecebb063ce4 --- /dev/null +++ b/test-suite-logback/build.gradle @@ -0,0 +1,18 @@ +plugins { + id("io.micronaut.build.internal.convention-test-library") +} + +dependencies { + testAnnotationProcessor(projects.injectJava) + + testImplementation(libs.managed.micronaut.test.spock) { + exclude(group: "io.micronaut", module: "micronaut-aop") + } + testImplementation(projects.context) + testImplementation(projects.injectGroovy) + testImplementation(libs.managed.logback) + testImplementation(projects.management) + testImplementation(projects.httpClient) + + testRuntimeOnly(projects.httpServerNetty) +} diff --git a/test-suite-logback/src/test/groovy/io/micronaut/logback/LoggerConfigurationSpec.groovy b/test-suite-logback/src/test/groovy/io/micronaut/logback/LoggerConfigurationSpec.groovy new file mode 100644 index 00000000000..f5b709c2498 --- /dev/null +++ b/test-suite-logback/src/test/groovy/io/micronaut/logback/LoggerConfigurationSpec.groovy @@ -0,0 +1,33 @@ +package io.micronaut.logback + +import ch.qos.logback.classic.Level +import ch.qos.logback.classic.Logger +import io.micronaut.context.annotation.Property +import io.micronaut.http.client.HttpClient +import io.micronaut.http.client.annotation.Client +import io.micronaut.test.extensions.spock.annotation.MicronautTest +import jakarta.inject.Inject +import org.slf4j.LoggerFactory +import spock.lang.Specification + +@MicronautTest +// Setting a level in a property forces a refresh, so the XML configuration is ignored. Without this in 3.8.x, the test fails. +@Property(name = "logger.levels.set.by.property", value = "DEBUG") +class LoggerConfigurationSpec extends Specification { + + @Inject + @Client("/") + HttpClient client + + void "if configuration is supplied, xml should be ignored"() { + given: + Logger fromXml = (Logger) LoggerFactory.getLogger("i.should.not.exist") + Logger fromConfigurator = (Logger) LoggerFactory.getLogger("i.should.exist") + Logger fromProperties = (Logger) LoggerFactory.getLogger("set.by.property") + + expect: + fromXml.level == null + fromConfigurator.level == Level.TRACE + fromProperties.level == Level.DEBUG + } +} diff --git a/test-suite-logback/src/test/groovy/io/micronaut/logback/LoggerEndpointSpec.groovy b/test-suite-logback/src/test/groovy/io/micronaut/logback/LoggerEndpointSpec.groovy new file mode 100644 index 00000000000..d7aef0db31d --- /dev/null +++ b/test-suite-logback/src/test/groovy/io/micronaut/logback/LoggerEndpointSpec.groovy @@ -0,0 +1,71 @@ +package io.micronaut.logback + +import ch.qos.logback.classic.Logger +import ch.qos.logback.classic.spi.ILoggingEvent +import ch.qos.logback.core.AppenderBase +import io.micronaut.context.annotation.Property +import io.micronaut.http.HttpRequest +import io.micronaut.http.MediaType +import io.micronaut.http.client.HttpClient +import io.micronaut.http.client.annotation.Client +import io.micronaut.logback.controllers.HelloWorldController +import io.micronaut.test.extensions.spock.annotation.MicronautTest +import jakarta.inject.Inject +import org.slf4j.LoggerFactory +import spock.lang.Issue +import spock.lang.Specification + +@MicronautTest +@Property(name = "logger.levels.io.micronaut.logback", value = "INFO") +@Property(name = "endpoints.loggers.enabled", value = "true") +@Property(name = "endpoints.loggers.sensitive", value = "false") +@Property(name = "endpoints.loggers.write-sensitive", value = "false") +@Issue("https://github.com/micronaut-projects/micronaut-core/issues/8679") +class LoggerEndpointSpec extends Specification { + + @Inject + @Client("/") + HttpClient client + + void "logback configuration from properties is as expected"() { + when: + def response = client.toBlocking().retrieve("/loggers/io.micronaut.logback") + + then: + response.contains("INFO") + } + + void "logback can be configured"() { + given: + MemoryAppender appender = new MemoryAppender() + Logger l = (Logger) LoggerFactory.getLogger("io.micronaut.logback.controllers") + l.addAppender(appender) + appender.start() + + when: + def response = client.toBlocking().retrieve("/", String) + + then: 'response is as expected' + response == HelloWorldController.RESPONSE + + and: 'no log message is emitted' + appender.events.empty + + when: 'log level is changed to TRACE' + def body = '{ "configuredLevel": "TRACE" }' + def post = HttpRequest.POST("/loggers/io.micronaut.logback.controllers", body).contentType(MediaType.APPLICATION_JSON_TYPE) + client.toBlocking().exchange(post) + + and: + response = client.toBlocking().retrieve("/", String) + + then: 'response is as expected' + response == HelloWorldController.RESPONSE + + and: 'log message is emitted' + appender.events == [HelloWorldController.LOG_MESSAGE] + + cleanup: + appender.stop() + } +} diff --git a/test-suite-logback/src/test/groovy/io/micronaut/logback/LoggerLevelSpec.groovy b/test-suite-logback/src/test/groovy/io/micronaut/logback/LoggerLevelSpec.groovy new file mode 100644 index 00000000000..b953c4869e0 --- /dev/null +++ b/test-suite-logback/src/test/groovy/io/micronaut/logback/LoggerLevelSpec.groovy @@ -0,0 +1,46 @@ +package io.micronaut.logback + +import ch.qos.logback.classic.Logger +import ch.qos.logback.classic.spi.ILoggingEvent +import ch.qos.logback.core.AppenderBase +import io.micronaut.context.annotation.Property +import io.micronaut.http.HttpRequest +import io.micronaut.http.MediaType +import io.micronaut.http.client.HttpClient +import io.micronaut.http.client.annotation.Client +import io.micronaut.logback.controllers.HelloWorldController +import io.micronaut.test.extensions.spock.annotation.MicronautTest +import jakarta.inject.Inject +import org.slf4j.LoggerFactory +import spock.lang.Issue +import spock.lang.Specification + +@MicronautTest +@Property(name = "logger.levels.io.micronaut.logback.controllers", value = "TRACE") +@Issue("https://github.com/micronaut-projects/micronaut-core/issues/8678") +class LoggerLevelSpec extends Specification { + + @Inject + @Client("/") + HttpClient client + + void "logback can be configured via properties"() { + given: + MemoryAppender appender = new MemoryAppender() + Logger l = (Logger) LoggerFactory.getLogger("io.micronaut.logback.controllers") + l.addAppender(appender) + appender.start() + + when: + def response = client.toBlocking().retrieve("/", String) + + then: 'response is as expected' + response == HelloWorldController.RESPONSE + + and: 'log message is emitted' + appender.events == [HelloWorldController.LOG_MESSAGE] + + cleanup: + appender.stop() + } +} diff --git a/test-suite-logback/src/test/groovy/io/micronaut/logback/MemoryAppender.groovy b/test-suite-logback/src/test/groovy/io/micronaut/logback/MemoryAppender.groovy new file mode 100644 index 00000000000..3e978a6888e --- /dev/null +++ b/test-suite-logback/src/test/groovy/io/micronaut/logback/MemoryAppender.groovy @@ -0,0 +1,17 @@ +package io.micronaut.logback + +import ch.qos.logback.classic.spi.ILoggingEvent +import ch.qos.logback.core.AppenderBase +import groovy.transform.CompileStatic +import groovy.transform.PackageScope + +@PackageScope +@CompileStatic +class MemoryAppender extends AppenderBase { + List events = [] + + @Override + protected void append(ILoggingEvent e) { + events << e.formattedMessage + } +} diff --git a/test-suite-logback/src/test/java/io/micronaut/logback/Application.java b/test-suite-logback/src/test/java/io/micronaut/logback/Application.java new file mode 100644 index 00000000000..8f06535b06c --- /dev/null +++ b/test-suite-logback/src/test/java/io/micronaut/logback/Application.java @@ -0,0 +1,15 @@ +package io.micronaut.logback; + +import io.micronaut.runtime.Micronaut; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class Application { + + private static final Logger LOG = LoggerFactory.getLogger(Application.class); + + public static void main(String[] args) { + LOG.trace("starting the app"); + Micronaut.run(Application.class, args); + } +} diff --git a/test-suite-logback/src/test/java/io/micronaut/logback/config/CustomConfigurator.java b/test-suite-logback/src/test/java/io/micronaut/logback/config/CustomConfigurator.java new file mode 100644 index 00000000000..7b98abdaa7b --- /dev/null +++ b/test-suite-logback/src/test/java/io/micronaut/logback/config/CustomConfigurator.java @@ -0,0 +1,75 @@ +package io.micronaut.logback.config; + +import ch.qos.logback.classic.Level; +import ch.qos.logback.classic.Logger; +import ch.qos.logback.classic.LoggerContext; +import ch.qos.logback.classic.layout.TTLLLayout; +import ch.qos.logback.classic.spi.Configurator; +import ch.qos.logback.classic.spi.ILoggingEvent; +import ch.qos.logback.core.ConsoleAppender; +import ch.qos.logback.core.encoder.LayoutWrappingEncoder; +import ch.qos.logback.core.spi.ContextAwareBase; + +/** + * @see Logback Class Basic Configurator + */ +public class CustomConfigurator extends ContextAwareBase implements Configurator { + + public static final Level ROOT_LOGGER_LEVEL = Level.INFO; + + @Override + public void configure(LoggerContext lc) { + addInfo("Setting up default configuration."); + ConsoleAppender ca = startConsoleAppender(lc); + configureRootRootLogger(lc, ca); + + final String pkg = "io.micronaut.logback"; + Logger appPkgLogger = lc.getLogger(pkg); + appPkgLogger.setLevel(Level.TRACE); + appPkgLogger.setAdditive(false); + appPkgLogger.addAppender(ca); + + Logger controllersLogger = lc.getLogger(pkg + ".controllers"); + controllersLogger.setLevel(Level.INFO); + controllersLogger.setAdditive(false); + controllersLogger.addAppender(ca); + + Logger configuredLogger = lc.getLogger("i.should.exist"); + configuredLogger.setLevel(Level.TRACE); + configuredLogger.setAdditive(false); + configuredLogger.addAppender(ca); + + Logger mnLogger = lc.getLogger("io.micronaut.runtime.Micronaut"); + mnLogger.setLevel(Level.INFO); + mnLogger.setAdditive(false); + mnLogger.addAppender(ca); + } + + private void configureRootRootLogger(LoggerContext lc, ConsoleAppender ca) { + Logger rootLogger = lc.getLogger(Logger.ROOT_LOGGER_NAME); + rootLogger.setLevel(ROOT_LOGGER_LEVEL); + rootLogger.addAppender(ca); + } + + private ConsoleAppender startConsoleAppender(LoggerContext lc) { + ConsoleAppender ca = new ConsoleAppender<>(); + ca.setContext(lc); + ca.setName("stdout"); + LayoutWrappingEncoder encoder = new LayoutWrappingEncoder<>(); + encoder.setContext(lc); + + // same as + // PatternLayout layout = new PatternLayout(); + // layout.setPattern("%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - + // %msg%n"); + TTLLLayout layout = new TTLLLayout(); + + layout.setContext(lc); + layout.start(); + encoder.setLayout(layout); + + ca.setEncoder(encoder); + ca.start(); + return ca; + } +} diff --git a/test-suite-logback/src/test/java/io/micronaut/logback/controllers/HelloWorldController.java b/test-suite-logback/src/test/java/io/micronaut/logback/controllers/HelloWorldController.java new file mode 100644 index 00000000000..d431f7bb72b --- /dev/null +++ b/test-suite-logback/src/test/java/io/micronaut/logback/controllers/HelloWorldController.java @@ -0,0 +1,21 @@ +package io.micronaut.logback.controllers; + +import io.micronaut.http.annotation.Controller; +import io.micronaut.http.annotation.Get; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@Controller +public class HelloWorldController { + + public static final String RESPONSE = "Hello world!"; + public static final String LOG_MESSAGE = "inside hello world"; + + private static final Logger LOG = LoggerFactory.getLogger(HelloWorldController.class); + + @Get + String index() { + LOG.trace(LOG_MESSAGE); + return RESPONSE; + } +} diff --git a/test-suite-logback/src/test/resources/META-INF/services/ch.qos.logback.classic.spi.Configurator b/test-suite-logback/src/test/resources/META-INF/services/ch.qos.logback.classic.spi.Configurator new file mode 100644 index 00000000000..ef82e20431c --- /dev/null +++ b/test-suite-logback/src/test/resources/META-INF/services/ch.qos.logback.classic.spi.Configurator @@ -0,0 +1 @@ +io.micronaut.logback.config.CustomConfigurator diff --git a/test-suite-logback/src/test/resources/logback.xml b/test-suite-logback/src/test/resources/logback.xml new file mode 100644 index 00000000000..7b053ed9a39 --- /dev/null +++ b/test-suite-logback/src/test/resources/logback.xml @@ -0,0 +1,16 @@ + + + + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + + From bd085e063bf2a537c5fe13f9e7b945f5faf8ad7f Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Mon, 6 Feb 2023 12:59:30 +0100 Subject: [PATCH 451/743] build: Micronaut Build Gradle Plugin 5.4.5 --- settings.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/settings.gradle b/settings.gradle index 758ac99e0a6..cab5ba8eefc 100644 --- a/settings.gradle +++ b/settings.gradle @@ -14,7 +14,7 @@ pluginManagement { } plugins { - id 'io.micronaut.build.shared.settings' version '5.4.4' + id 'io.micronaut.build.shared.settings' version '5.4.5' } rootProject.name = 'micronaut' From bf7624315f6680bc944c15c32d18119663ded2bc Mon Sep 17 00:00:00 2001 From: Tim Yates Date: Mon, 6 Feb 2023 12:35:01 +0000 Subject: [PATCH 452/743] test: add octet stream serialization to the TCK (#8712) Inspired by https://github.com/micronaut-projects/micronaut-aws/issues/1545 --- .../http/server/tck/AssertionUtils.java | 4 +- .../http/server/tck/BodyAssertion.java | 116 ++++++++++++++---- .../server/tck/HttpResponseAssertion.java | 10 +- .../http/server/tck/tests/MiscTest.java | 1 - .../http/server/tck/tests/OctetTest.java | 70 +++++++++++ 5 files changed, 172 insertions(+), 29 deletions(-) create mode 100644 http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/OctetTest.java diff --git a/http-server-tck/src/main/java/io/micronaut/http/server/tck/AssertionUtils.java b/http-server-tck/src/main/java/io/micronaut/http/server/tck/AssertionUtils.java index 6dd2bb26073..873e083952e 100644 --- a/http-server-tck/src/main/java/io/micronaut/http/server/tck/AssertionUtils.java +++ b/http-server-tck/src/main/java/io/micronaut/http/server/tck/AssertionUtils.java @@ -98,9 +98,9 @@ public static void assertDoesNotThrow(@NonNull ServerUnderTest server, assertion.getResponseConsumer().ifPresent(httpResponseConsumer -> httpResponseConsumer.accept(response)); } - private static void assertBody(@NonNull HttpResponse response, @Nullable BodyAssertion bodyAssertion) { + private static void assertBody(@NonNull HttpResponse response, @Nullable BodyAssertion bodyAssertion) { if (bodyAssertion != null) { - Optional bodyOptional = response.getBody(String.class); + Optional bodyOptional = response.getBody(bodyAssertion.getBodyType()); assertTrue(bodyOptional.isPresent()); bodyOptional.ifPresent(bodyAssertion::evaluate); } diff --git a/http-server-tck/src/main/java/io/micronaut/http/server/tck/BodyAssertion.java b/http-server-tck/src/main/java/io/micronaut/http/server/tck/BodyAssertion.java index 71355557883..e37021054ad 100644 --- a/http-server-tck/src/main/java/io/micronaut/http/server/tck/BodyAssertion.java +++ b/http-server-tck/src/main/java/io/micronaut/http/server/tck/BodyAssertion.java @@ -17,37 +17,69 @@ import io.micronaut.core.annotation.Experimental; -import java.util.function.BiFunction; +import java.util.Arrays; +import java.util.function.BiPredicate; import static org.junit.jupiter.api.Assertions.assertTrue; /** - * HTTP Reponse's body assertions. + * HTTP Response's body assertions. + * + * @param The body type */ @Experimental -public final class BodyAssertion { - private final String expected; - private final BiFunction evaluator; +public final class BodyAssertion { + + private final Class bodyType; + private final T expected; + private final BiPredicate evaluator; - private BodyAssertion(String expected, BiFunction evaluator) { + private BodyAssertion(Class bodyType, T expected, BiPredicate evaluator) { + this.bodyType = bodyType; this.expected = expected; this.evaluator = evaluator; } + /** + * @return a Builder; + */ + public static BodyAssertion.Builder builder() { + return new BodyAssertion.Builder(); + } + /** * Evaluates the HTTP Response Body. + * * @param body The HTTP Response Body */ - public void evaluate(String body) { - assertTrue(this.evaluator.apply(expected, body)); + @SuppressWarnings("java:S5960") // Assertion is the whole point of this method + public void evaluate(T body) { + assertTrue(this.evaluator.test(expected, body)); + } + + /** + * @return The expected body type + */ + public Class getBodyType() { + return bodyType; } /** + * The interface for typed BodyAssertion Builders. * - * @return a Builder; + * @param The body type */ - public static BodyAssertion.Builder builder() { - return new BodyAssertion.Builder(); + public interface AssertionBuilder { + + /** + * @return a body assertion which verifiers the HTTP Response's body contains the expected body + */ + BodyAssertion contains(); + + /** + * @return a body assertion which verifiers the HTTP Response's body is equals to the expected body + */ + BodyAssertion equals(); } /** @@ -55,32 +87,74 @@ public static BodyAssertion.Builder builder() { */ public static class Builder { - private String body; + /** + * @param expected Expected Body + * @return The Builder + */ + public AssertionBuilder body(String expected) { + return new StringBodyAssertionBuilder(expected); + } /** - * * @param expected Expected Body * @return The Builder */ - public Builder body(String expected) { + public AssertionBuilder body(byte[] expected) { + return new ByteArrayBodyAssertionBuilder(expected); + } + } + + /** + * String BodyAssertion Builder. + */ + public static class StringBodyAssertionBuilder extends BodyAssertion.Builder implements AssertionBuilder { + + private final String body; + + public StringBodyAssertionBuilder(String expected) { + this.body = expected; + } + + /** + * @return a body assertion which verifiers the HTTP Response's body contains the expected body + */ + public BodyAssertion contains() { + return new BodyAssertion<>(String.class, this.body, (required, received) -> received.contains(required)); + } + + /** + * @return a body assertion which verifiers the HTTP Response's body is equals to the expected body + */ + public BodyAssertion equals() { + return new BodyAssertion<>(String.class, this.body, (required, received) -> received.equals(required)); + } + } + + /** + * Byte Array BodyAssertion Builder. + */ + public static class ByteArrayBodyAssertionBuilder extends BodyAssertion.Builder implements BodyAssertion.AssertionBuilder { + + private final byte[] body; + + public ByteArrayBodyAssertionBuilder(byte[] expected) { this.body = expected; - return this; } /** - * * @return a body assertion which verifiers the HTTP Response's body contains the expected body */ - public BodyAssertion contains() { - return new BodyAssertion(this.body, (expected, body) -> body.contains(expected)); + public BodyAssertion contains() { + return new BodyAssertion<>(byte[].class, this.body, (required, received) -> { + throw new AssertionError("Not implemented yet!"); + }); } /** - * * @return a body assertion which verifiers the HTTP Response's body is equals to the expected body */ - public BodyAssertion equals() { - return new BodyAssertion(this.body, (expected, body) -> body.equals(expected)); + public BodyAssertion equals() { + return new BodyAssertion<>(byte[].class, this.body, (required, received) -> Arrays.equals(received, required)); } } } diff --git a/http-server-tck/src/main/java/io/micronaut/http/server/tck/HttpResponseAssertion.java b/http-server-tck/src/main/java/io/micronaut/http/server/tck/HttpResponseAssertion.java index 0ec962345b8..1b39cdfb938 100644 --- a/http-server-tck/src/main/java/io/micronaut/http/server/tck/HttpResponseAssertion.java +++ b/http-server-tck/src/main/java/io/micronaut/http/server/tck/HttpResponseAssertion.java @@ -36,14 +36,14 @@ public final class HttpResponseAssertion { private final HttpStatus httpStatus; private final Map headers; - private final BodyAssertion bodyAssertion; + private final BodyAssertion bodyAssertion; @Nullable private final Consumer> responseConsumer; private HttpResponseAssertion(HttpStatus httpStatus, Map headers, - BodyAssertion bodyAssertion, + BodyAssertion bodyAssertion, @Nullable Consumer> responseConsumer) { this.httpStatus = httpStatus; this.headers = headers; @@ -77,7 +77,7 @@ public Map getHeaders() { * @return Expected HTTP Response body */ - public BodyAssertion getBody() { + public BodyAssertion getBody() { return bodyAssertion; } @@ -95,7 +95,7 @@ public static HttpResponseAssertion.Builder builder() { public static class Builder { private HttpStatus httpStatus; private Map headers; - private BodyAssertion bodyAssertion; + private BodyAssertion bodyAssertion; private Consumer> responseConsumer; @@ -148,7 +148,7 @@ public Builder body(String containsBody) { * @param bodyAssertion Response Body Assertion * @return HTTP Response Assertion Builder */ - public Builder body(BodyAssertion bodyAssertion) { + public Builder body(BodyAssertion bodyAssertion) { this.bodyAssertion = bodyAssertion; return this; } diff --git a/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/MiscTest.java b/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/MiscTest.java index 5e665b41c0a..a7104d96e4f 100644 --- a/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/MiscTest.java +++ b/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/MiscTest.java @@ -38,7 +38,6 @@ import java.util.Collections; import java.util.Map; - @SuppressWarnings({ "java:S5960", // We're allowed assertions, as these are used in tests only "checkstyle:MissingJavadocType", diff --git a/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/OctetTest.java b/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/OctetTest.java new file mode 100644 index 00000000000..5778c8c1e42 --- /dev/null +++ b/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/OctetTest.java @@ -0,0 +1,70 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.http.server.tck.tests; + +import io.micronaut.context.annotation.Requires; +import io.micronaut.http.HttpRequest; +import io.micronaut.http.HttpResponse; +import io.micronaut.http.HttpStatus; +import io.micronaut.http.MediaType; +import io.micronaut.http.annotation.Controller; +import io.micronaut.http.annotation.Get; +import io.micronaut.http.server.tck.AssertionUtils; +import io.micronaut.http.server.tck.BodyAssertion; +import io.micronaut.http.server.tck.HttpResponseAssertion; +import org.junit.jupiter.api.Test; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.stream.IntStream; + +import static io.micronaut.http.server.tck.TestScenario.asserts; + +@SuppressWarnings({ + "java:S5960", // We're allowed assertions, as these are used in tests only + "checkstyle:MissingJavadocType", + "checkstyle:DesignForExtension" +}) +public class OctetTest { + + public static final String SPEC_NAME = "OctetTest"; + + @Test + void canReadOctetEncodedData() throws IOException { + asserts(SPEC_NAME, + HttpRequest.GET("/octets"), + (server, request) -> AssertionUtils.assertDoesNotThrow(server, request, HttpResponseAssertion.builder() + .status(HttpStatus.OK) + .body(BodyAssertion.builder().body(OctetController.BODY_BYTES).equals()) + .build())); + } + + @Controller("/octets") + @Requires(property = "spec.name", value = SPEC_NAME) + static class OctetController { + + static final byte[] BODY_BYTES = IntStream.iterate(1, i -> i + 1) + .limit(256) + .map(i -> (byte) i) + .collect(ByteArrayOutputStream::new, ByteArrayOutputStream::write, (a, b) -> a.write(b.toByteArray(), 0, b.size())) + .toByteArray(); + + @Get(produces = MediaType.APPLICATION_OCTET_STREAM) + HttpResponse byteArray() { + return HttpResponse.ok(BODY_BYTES); + } + } +} From adbf3c7c3f5e3f984c4ddeb8c2af5ac769d528ab Mon Sep 17 00:00:00 2001 From: micronaut-build Date: Mon, 6 Feb 2023 14:34:53 +0000 Subject: [PATCH 453/743] [skip ci] Release v3.7.7 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 9455edf2ec1..647006578ef 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -projectVersion=3.7.7-SNAPSHOT +projectVersion=3.7.7 projectGroupId=io.micronaut title=Micronaut projectDesc=Natively Cloud Native From 0c898c51a662bbd85adeb0fa9629a68dbc6e6162 Mon Sep 17 00:00:00 2001 From: micronaut-build Date: Mon, 6 Feb 2023 14:58:15 +0000 Subject: [PATCH 454/743] Back to 3.7.8-SNAPSHOT --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 647006578ef..582742fc35a 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -projectVersion=3.7.7 +projectVersion=3.7.8-SNAPSHOT projectGroupId=io.micronaut title=Micronaut projectDesc=Natively Cloud Native From 9862a11b498968ded6b7df1a8671055be101ed55 Mon Sep 17 00:00:00 2001 From: micronaut-build <65172877+micronaut-build@users.noreply.github.com> Date: Mon, 6 Feb 2023 17:30:28 +0100 Subject: [PATCH 455/743] build: micronaut-openapi to 4.8.3 (#8724) --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 76d84422216..e74cf00f5d4 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -100,7 +100,7 @@ managed-micronaut-neo4j = "5.2.0" managed-micronaut-nats = "3.1.0" managed-micronaut-netflix = "2.1.0" managed-micronaut-object-storage = "1.1.0" -managed-micronaut-openapi = "4.8.2" +managed-micronaut-openapi = "4.8.3" managed-micronaut-oraclecloud = "2.3.2" managed-micronaut-picocli = "4.3.0" managed-micronaut-problem = "2.6.0" From 35388afb7a8b8c461ec12daff70ea3e4a23620cf Mon Sep 17 00:00:00 2001 From: micronaut-build <65172877+micronaut-build@users.noreply.github.com> Date: Tue, 7 Feb 2023 07:14:18 +0100 Subject: [PATCH 456/743] build: micronaut-test to 3.8.2 (#8728) --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index e74cf00f5d4..59ce940f2be 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -117,7 +117,7 @@ managed-micronaut-serialization = "1.5.0" managed-micronaut-servlet = "3.3.5" managed-micronaut-spring = "4.4.0" managed-micronaut-sql = "4.7.2" -managed-micronaut-test = "3.8.0" +managed-micronaut-test = "3.8.2" managed-micronaut-test-resources = "1.2.3" managed-micronaut-toml = "1.1.3" managed-micronaut-tracing = "4.4.1" From 5206a13bbe4dfc9326a6f7470cdff1a28f31bcf8 Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Tue, 7 Feb 2023 12:27:58 +0100 Subject: [PATCH 457/743] doc: Highlight virtual threads support (#8734) - Use asciidoc snippet macro - Extract virtualthreads to its own file - Link to virtual threas from what's new section - LInk to Open JDK JEP 425 - Move 4.0.0 to the top of what's new --- .../threadPools/blockingOperations.adoc | 17 -------- .../threadPools/virtualThreads.adoc | 7 ++++ .../docs/guide/introduction/whatsNew.adoc | 22 +++++----- src/main/docs/guide/toc.yml | 1 + .../taskexecutors/HelloWorldController.groovy | 22 ++++++++++ .../TaskExecutorsBlockingSpec.groovy | 41 +++++++++++++++++++ .../taskexecutors/HelloWorldController.kt | 19 +++++++++ .../TaskExecutorsBlockingTest.kt | 38 +++++++++++++++++ .../taskexecutors/HelloWorldController.java | 22 ++++++++++ .../TaskExecutorsBlockingTest.java | 40 ++++++++++++++++++ 10 files changed, 202 insertions(+), 27 deletions(-) create mode 100644 src/main/docs/guide/httpServer/serverConfiguration/threadPools/virtualThreads.adoc create mode 100644 test-suite-groovy/src/test/groovy/io/micronaut/docs/taskexecutors/HelloWorldController.groovy create mode 100644 test-suite-groovy/src/test/groovy/io/micronaut/docs/taskexecutors/TaskExecutorsBlockingSpec.groovy create mode 100644 test-suite-kotlin/src/test/kotlin/io/micronaut/docs/taskexecutors/HelloWorldController.kt create mode 100644 test-suite-kotlin/src/test/kotlin/io/micronaut/docs/taskexecutors/TaskExecutorsBlockingTest.kt create mode 100644 test-suite/src/test/java/io/micronaut/docs/taskexecutors/HelloWorldController.java create mode 100644 test-suite/src/test/java/io/micronaut/docs/taskexecutors/TaskExecutorsBlockingTest.java diff --git a/src/main/docs/guide/httpServer/serverConfiguration/threadPools/blockingOperations.adoc b/src/main/docs/guide/httpServer/serverConfiguration/threadPools/blockingOperations.adoc index 5f6d601243f..640007c0a64 100644 --- a/src/main/docs/guide/httpServer/serverConfiguration/threadPools/blockingOperations.adoc +++ b/src/main/docs/guide/httpServer/serverConfiguration/threadPools/blockingOperations.adoc @@ -12,20 +12,3 @@ micronaut: The above configuration creates a fixed thread pool with 75 threads. -== Virtual Threads - -Since Java 19, the JVM includes experimental support for virtual threads ("project loom"). As it is a preview feature, you need to pass `--enable-preview` as a JVM parameter to enable it. - -The Micronaut framework will detect virtual thread support and use it for the executor named `blocking` if available. If virtual threads are not supported, this executor will be aliased to the `io` thread pool. - -To use the `blocking` executor, simply mark e.g. a controller with `ExecuteOn`: - -.Configuring the Server I/O Thread Pool -[source,java] ----- -@ExecuteOn(TaskExecutors.BLOCKING) -@GET -String hello() { - return "foo" -} ----- diff --git a/src/main/docs/guide/httpServer/serverConfiguration/threadPools/virtualThreads.adoc b/src/main/docs/guide/httpServer/serverConfiguration/threadPools/virtualThreads.adoc new file mode 100644 index 00000000000..37e028efa26 --- /dev/null +++ b/src/main/docs/guide/httpServer/serverConfiguration/threadPools/virtualThreads.adoc @@ -0,0 +1,7 @@ +Since Java 19, the JVM includes experimental support for https://openjdk.org/jeps/425[virtual threads ("project loom")]. As it is a preview feature, you need to pass `--enable-preview` as a JVM parameter to enable it. + +The Micronaut framework will detect virtual thread support and use it for the executor named `blocking` if available. If virtual threads are not supported, this executor will be aliased to the `io` thread pool. + +To use the `blocking` executor, simply mark e.g. a controller with `ExecuteOn`: + +snippet::io.micronaut.docs.taskexecutors.HelloWorldController[tags="clazz", indent=0, title="Configuring the Server I/O Thread Pool"] diff --git a/src/main/docs/guide/introduction/whatsNew.adoc b/src/main/docs/guide/introduction/whatsNew.adoc index c3a37231a52..e136bc6f6dd 100644 --- a/src/main/docs/guide/introduction/whatsNew.adoc +++ b/src/main/docs/guide/introduction/whatsNew.adoc @@ -1,18 +1,10 @@ //Micronaut {version} includes the following changes: -== 3.8.0 - -Key features: - -- https://www.graalvm.org/release-notes/22_3/[GraalVM 22.3 Support] -- With Micronaut `3.8.0`, you can use `@RequestBean` annotations with https://docs.oracle.com/en/java/javase/14/language/records.html[Records]. Before `3.8.0`, you could use a POJO as a controller method parameter and annotate the parameter with `@RequestBean` to bind any Bindable value (e.g., `HttpRequest`, `@PathVariable`, `@QueryValue` or `@Header` fields). -- If you enable CORS from any origin while running your app in localhost (e.g., test or development), since `3.8.0`, the `CorsFilter` returns 403 for non-localhost origins to protect you against drive-by localhost attacks. - -Please read the https://micronaut.io/2022/12/27/micronaut-framework-3-8-0-released/[Micronaut Framework 3.8.0 announcement blog post]. You will find a detailed overview of what’s new in Micronaut 3.8.0. - == 4.0.0 === Core Changes +* <> + ==== Injection of Maps It is now possible to inject a `java.util.Map` of beans where the key is the bean name. The name of the bean is derived from the <> or (if not present) the simple name of the class. @@ -25,6 +17,16 @@ When a bean annotated with ann:context.annotation.EachProperty[] or ann:context. - Kotlin 1.7.10 +== 3.8.0 + +Key features: + +- https://www.graalvm.org/release-notes/22_3/[GraalVM 22.3 Support] +- With Micronaut `3.8.0`, you can use `@RequestBean` annotations with https://docs.oracle.com/en/java/javase/14/language/records.html[Records]. Before `3.8.0`, you could use a POJO as a controller method parameter and annotate the parameter with `@RequestBean` to bind any Bindable value (e.g., `HttpRequest`, `@PathVariable`, `@QueryValue` or `@Header` fields). +- If you enable CORS from any origin while running your app in localhost (e.g., test or development), since `3.8.0`, the `CorsFilter` returns 403 for non-localhost origins to protect you against drive-by localhost attacks. + +Please read the https://micronaut.io/2022/12/27/micronaut-framework-3-8-0-released/[Micronaut Framework 3.8.0 announcement blog post]. You will find a detailed overview of what’s new in Micronaut 3.8.0. + == 3.7.0 Several improvements: diff --git a/src/main/docs/guide/toc.yml b/src/main/docs/guide/toc.yml index f4f4d21ff63..56862d8826b 100644 --- a/src/main/docs/guide/toc.yml +++ b/src/main/docs/guide/toc.yml @@ -124,6 +124,7 @@ httpServer: threadPools: title: Configuring Server Thread Pools blockingOperations: Blocking Operations + virtualThreads: Virtual Threads atBlocking: '@Blocking' nettyClientPipeline: Configuring the Netty Client Pipeline nettyServerPipeline: Configuring the Netty Server Pipeline diff --git a/test-suite-groovy/src/test/groovy/io/micronaut/docs/taskexecutors/HelloWorldController.groovy b/test-suite-groovy/src/test/groovy/io/micronaut/docs/taskexecutors/HelloWorldController.groovy new file mode 100644 index 00000000000..30d5f4bcfd9 --- /dev/null +++ b/test-suite-groovy/src/test/groovy/io/micronaut/docs/taskexecutors/HelloWorldController.groovy @@ -0,0 +1,22 @@ +package io.micronaut.docs.taskexecutors +import io.micronaut.context.annotation.Requires +import io.micronaut.http.MediaType +import io.micronaut.http.annotation.Controller +import io.micronaut.http.annotation.Get +import io.micronaut.http.annotation.Produces +import io.micronaut.scheduling.TaskExecutors +import io.micronaut.scheduling.annotation.ExecuteOn + +@Requires(property = "spec.name", value = "TaskExecutorsBlockingTest") +//tag::clazz[] +@Controller("/hello") +class HelloWorldController { + + @ExecuteOn(TaskExecutors.BLOCKING) + @Produces(MediaType.TEXT_PLAIN) + @Get("/world") + String index() { + "Hello World" + } +} +//end::clazz[] diff --git a/test-suite-groovy/src/test/groovy/io/micronaut/docs/taskexecutors/TaskExecutorsBlockingSpec.groovy b/test-suite-groovy/src/test/groovy/io/micronaut/docs/taskexecutors/TaskExecutorsBlockingSpec.groovy new file mode 100644 index 00000000000..44dc28011c9 --- /dev/null +++ b/test-suite-groovy/src/test/groovy/io/micronaut/docs/taskexecutors/TaskExecutorsBlockingSpec.groovy @@ -0,0 +1,41 @@ +package io.micronaut.docs.taskexecutors + +import io.micronaut.context.annotation.Property +import io.micronaut.http.HttpRequest +import io.micronaut.http.HttpResponse +import io.micronaut.http.HttpStatus +import io.micronaut.http.MediaType +import io.micronaut.http.client.BlockingHttpClient +import io.micronaut.http.client.HttpClient +import io.micronaut.http.client.annotation.Client +import io.micronaut.http.uri.UriBuilder +import io.micronaut.test.extensions.spock.annotation.MicronautTest +import jakarta.inject.Inject + +@Property(name = "spec.name", value = "TaskExecutorsBlockingTest") +@MicronautTest +class TaskExecutorsBlockingTest { + @Inject + @Client("/") + HttpClient httpClient + + void "test method annotated with TaskExecutors at Blocking"() { + given: + BlockingHttpClient client = httpClient.toBlocking() + HttpRequest request = HttpRequest.GET(UriBuilder.of("/hello").path("world").build()).accept(MediaType.TEXT_PLAIN) + + when: + HttpResponse response = client.exchange(request, String) + + then: + noExceptionThrown() + HttpStatus.OK == response.status() + + when: + Optional txt = response.getBody(String) + + then: + txt.isPresent() + "Hello World" == txt.get() + } +} diff --git a/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/taskexecutors/HelloWorldController.kt b/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/taskexecutors/HelloWorldController.kt new file mode 100644 index 00000000000..d4e275e53ef --- /dev/null +++ b/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/taskexecutors/HelloWorldController.kt @@ -0,0 +1,19 @@ +package io.micronaut.docs.taskexecutors +import io.micronaut.context.annotation.Requires +import io.micronaut.http.MediaType +import io.micronaut.http.annotation.Controller +import io.micronaut.http.annotation.Get +import io.micronaut.http.annotation.Produces +import io.micronaut.scheduling.TaskExecutors +import io.micronaut.scheduling.annotation.ExecuteOn + +@Requires(property = "spec.name", value = "TaskExecutorsBlockingTest") +//tag::clazz[] +@Controller("/hello") +class HelloWorldController { + @ExecuteOn(TaskExecutors.BLOCKING) + @Produces(MediaType.TEXT_PLAIN) + @Get("/world") + fun index() = "Hello World" +} +//end::clazz[] diff --git a/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/taskexecutors/TaskExecutorsBlockingTest.kt b/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/taskexecutors/TaskExecutorsBlockingTest.kt new file mode 100644 index 00000000000..93326a12971 --- /dev/null +++ b/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/taskexecutors/TaskExecutorsBlockingTest.kt @@ -0,0 +1,38 @@ +package io.micronaut.docs.taskexecutors + +import io.micronaut.context.annotation.Property +import io.micronaut.http.HttpRequest +import io.micronaut.http.HttpResponse +import io.micronaut.http.HttpStatus +import io.micronaut.http.MediaType +import io.micronaut.http.MutableHttpRequest +import io.micronaut.http.client.BlockingHttpClient +import io.micronaut.http.client.HttpClient +import io.micronaut.http.client.annotation.Client +import io.micronaut.http.uri.UriBuilder +import io.micronaut.test.extensions.junit5.annotation.MicronautTest +import jakarta.inject.Inject +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.function.ThrowingSupplier +import java.util.Optional +import org.junit.jupiter.api.Assertions; + +@Property(name = "spec.name", value = "TaskExecutorsBlockingTest") +@MicronautTest +class TaskExecutorsBlockingTest { + @Inject + @field:Client("/") + lateinit var httpClient : HttpClient + + @Test + fun testMethodAnnotatedWithTaskExecutorsBlocking() { + val client = httpClient.toBlocking() + val uri = UriBuilder.of("/hello").path("world").build() + val request = HttpRequest.GET(uri).accept(MediaType.TEXT_PLAIN) + val response : HttpResponse = client.exchange(request, String::class.java); + Assertions.assertEquals(HttpStatus.OK, response.status()); + val txt = response.getBody(String::class.java) + Assertions.assertTrue(txt.isPresent()); + Assertions.assertEquals("Hello World", txt.get()); + } +} diff --git a/test-suite/src/test/java/io/micronaut/docs/taskexecutors/HelloWorldController.java b/test-suite/src/test/java/io/micronaut/docs/taskexecutors/HelloWorldController.java new file mode 100644 index 00000000000..2f42dab4b23 --- /dev/null +++ b/test-suite/src/test/java/io/micronaut/docs/taskexecutors/HelloWorldController.java @@ -0,0 +1,22 @@ +package io.micronaut.docs.taskexecutors; +import io.micronaut.context.annotation.Requires; +import io.micronaut.http.MediaType; +import io.micronaut.http.annotation.Controller; +import io.micronaut.http.annotation.Get; +import io.micronaut.http.annotation.Produces; +import io.micronaut.scheduling.TaskExecutors; +import io.micronaut.scheduling.annotation.ExecuteOn; + +@Requires(property = "spec.name", value = "TaskExecutorsBlockingTest") +//tag::clazz[] +@Controller("/hello") +class HelloWorldController { + + @ExecuteOn(TaskExecutors.BLOCKING) + @Produces(MediaType.TEXT_PLAIN) + @Get("/world") + String index() { + return "Hello World"; + } +} +//end::clazz[] diff --git a/test-suite/src/test/java/io/micronaut/docs/taskexecutors/TaskExecutorsBlockingTest.java b/test-suite/src/test/java/io/micronaut/docs/taskexecutors/TaskExecutorsBlockingTest.java new file mode 100644 index 00000000000..58308a843b1 --- /dev/null +++ b/test-suite/src/test/java/io/micronaut/docs/taskexecutors/TaskExecutorsBlockingTest.java @@ -0,0 +1,40 @@ +package io.micronaut.docs.taskexecutors; + +import io.micronaut.context.annotation.Property; +import io.micronaut.http.HttpRequest; +import io.micronaut.http.HttpResponse; +import io.micronaut.http.HttpStatus; +import io.micronaut.http.MediaType; +import io.micronaut.http.client.BlockingHttpClient; +import io.micronaut.http.client.HttpClient; +import io.micronaut.http.client.annotation.Client; +import io.micronaut.http.uri.UriBuilder; +import io.micronaut.test.extensions.junit5.annotation.MicronautTest; +import jakarta.inject.Inject; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.function.ThrowingSupplier; + +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.*; + + +@Property(name = "spec.name", value = "TaskExecutorsBlockingTest") +@MicronautTest +class TaskExecutorsBlockingTest { + @Inject + @Client("/") + HttpClient httpClient; + + @Test + void testMethodAnnotatedWithTaskExecutorsBlocking() { + BlockingHttpClient client = httpClient.toBlocking(); + HttpRequest request = HttpRequest.GET(UriBuilder.of("/hello").path("world").build()).accept(MediaType.TEXT_PLAIN); + ThrowingSupplier> supplier = () -> client.exchange(request, String.class); + HttpResponse response = assertDoesNotThrow(supplier); + assertEquals(HttpStatus.OK, response.status()); + Optional txt = response.getBody(String.class); + assertTrue(txt.isPresent()); + assertEquals("Hello World", txt.get()); + } +} From e3a67e3e696a00a7a0a2aa6907f48722ac2f343b Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Tue, 7 Feb 2023 12:28:19 +0100 Subject: [PATCH 458/743] doc: use asciidoc inner link (#8732) --- .../distributedConfigurationAwsParameterStore.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/docs/guide/cloud/cloudConfiguration/distributedConfigurationAwsParameterStore.adoc b/src/main/docs/guide/cloud/cloudConfiguration/distributedConfigurationAwsParameterStore.adoc index 2c07d7c5a92..fdcd3e4ad10 100644 --- a/src/main/docs/guide/cloud/cloudConfiguration/distributedConfigurationAwsParameterStore.adoc +++ b/src/main/docs/guide/cloud/cloudConfiguration/distributedConfigurationAwsParameterStore.adoc @@ -2,7 +2,7 @@ Micronaut supports configuration sharing via AWS System Manager Parameter Store. dependency:io.micronaut.aws:micronaut-aws-parameter-store[] -To enable distributed configuration, make sure https://docs.micronaut.io/latest/guide/#bootstrap[bootstrap] is enabled and create a `src/main/resources/bootstrap.yml` file with the following configuration: +To enable distributed configuration, make sure <> is enabled and create a `src/main/resources/bootstrap.yml` file with the following configuration: .bootstrap.yml [configuration] From 2c2fc0c965cbd8243163f1a4119c3215b33524f1 Mon Sep 17 00:00:00 2001 From: Denis Stepanov Date: Tue, 7 Feb 2023 16:33:58 +0100 Subject: [PATCH 459/743] Fixes for generic type arguments and their annotations (#8630) This PR includes: * Rewrite of the generics in ClassElement AST for Java/Groovy - that fixed some pending tests, allowed to read annotations on the generic arguments * Limiting generics recursion by replacing the action type argument with Object Work is still required to: * KSP needs to have similar changes * Groovy needs some work to allow to read the generic argument annotations * Probably some extra work to support TYPE annotations for Java --------- Co-authored-by: Andriy Dmytruk --- .../micronaut/aop/writer/AopProxyWriter.java | 19 +- .../AbstractAnnotationMetadataBuilder.java | 1 + .../io/micronaut/inject/ast/ClassElement.java | 194 ++++--- .../inject/ast/GenericPlaceholderElement.java | 1 + .../micronaut/inject/ast/WildcardElement.java | 32 ++ .../ElementAnnotationMetadataFactory.java | 42 -- .../writer/AbstractClassFileWriter.java | 108 ++-- .../ast/groovy/utils/AstGenericUtils.groovy | 2 +- .../groovy/visitor/AbstractGroovyElement.java | 441 +++++++++------ .../groovy/visitor/GroovyClassElement.java | 519 ++++++------------ .../groovy/visitor/GroovyElementFactory.java | 47 +- .../groovy/visitor/GroovyFieldElement.java | 34 +- .../GroovyGenericPlaceholderElement.java | 68 ++- .../groovy/visitor/GroovyMethodElement.java | 101 +--- .../visitor/GroovyParameterElement.java | 17 +- .../groovy/visitor/GroovyWildcardElement.java | 18 +- .../visitor/GroovyReconstructionSpec.groovy | 87 +-- .../generics/GenericTypeArgumentsSpec.groovy | 20 + .../visitor/BeanIntrospectionSpec.groovy | 95 ++++ .../inject/visitor/ClassElementSpec.groovy | 460 +++++++++++++++- .../micronaut/inject/visitor/MyBuilder.java | 4 + .../inject/visitor/RecursiveGenerics.java | 19 + .../test/AbstractTypeElementSpec.groovy | 11 +- .../beans/BeanIntrospectionSpec.groovy | 47 +- .../annotation/processing/GenericUtils.java | 225 +------- .../JavaElementAnnotationMetadataFactory.java | 29 + .../visitor/AbstractJavaElement.java | 362 ++++++------ .../processing/visitor/JavaClassElement.java | 453 +++++---------- .../visitor/JavaElementFactory.java | 124 +---- .../processing/visitor/JavaEnumElement.java | 8 +- .../processing/visitor/JavaFieldElement.java | 25 +- .../JavaGenericPlaceholderElement.java | 48 +- .../processing/visitor/JavaMethodElement.java | 69 +-- .../visitor/JavaParameterElement.java | 18 +- .../visitor/JavaWildcardElement.java | 54 +- .../visitor/JavaReconstructionSpec.groovy | 120 +++- .../ExecutableFactoryMethodSpec.groovy | 3 +- .../inject/beans/BeanDefinitionSpec.groovy | 82 +++ .../visitors/AllElementsVisitor.java | 4 + .../visitors/ClassElementSpec.groovy | 437 ++++++++++++++- .../context/ExecutionHandleLocator.java | 34 +- 41 files changed, 2640 insertions(+), 1842 deletions(-) create mode 100644 inject-groovy/src/test/groovy/io/micronaut/inject/visitor/MyBuilder.java create mode 100644 inject-groovy/src/test/groovy/io/micronaut/inject/visitor/RecursiveGenerics.java diff --git a/core-processor/src/main/java/io/micronaut/aop/writer/AopProxyWriter.java b/core-processor/src/main/java/io/micronaut/aop/writer/AopProxyWriter.java index e51fca44bd5..a7602010107 100644 --- a/core-processor/src/main/java/io/micronaut/aop/writer/AopProxyWriter.java +++ b/core-processor/src/main/java/io/micronaut/aop/writer/AopProxyWriter.java @@ -987,7 +987,8 @@ public void visitBeanDefinitionEnd() { buildProxyLookupArgument(proxyConstructorGenerator, targetType); proxyConstructorGenerator.loadArg(qualifierIndex); - pushMethodNameAndTypesArguments(proxyConstructorGenerator, methodRef.name, methodRef.argumentTypes); + // Arguments are written as generic types, so we need to look for the method using the generic arguments + pushMethodNameAndTypesArguments(proxyConstructorGenerator, methodRef.name, methodRef.genericArgumentTypes); proxyConstructorGenerator.invokeInterface( Type.getType(ExecutionHandleLocator.class), METHOD_GET_PROXY_TARGET @@ -1612,16 +1613,18 @@ private void processAlreadyVisitedMethods(BeanDefinitionWriter parent) { * Method Reference class with names and a list of argument types. Used as the targets. */ private static final class MethodRef { - protected final String name; - protected final List argumentTypes; - protected final Type returnType; - int methodIndex; + private final String name; + private final List argumentTypes; + private final List genericArgumentTypes; + private final Type returnType; private final List rawTypes; + int methodIndex; - public MethodRef(String name, List argumentTypes, Type returnType) { + public MethodRef(String name, List parameterElements, Type returnType) { this.name = name; - this.argumentTypes = argumentTypes.stream().map(ParameterElement::getType).collect(Collectors.toList()); - this.rawTypes = this.argumentTypes.stream().map(ClassElement::getName).collect(Collectors.toList()); + this.argumentTypes = parameterElements.stream().map(ParameterElement::getType).toList(); + this.genericArgumentTypes = parameterElements.stream().map(ParameterElement::getGenericType).toList(); + this.rawTypes = this.argumentTypes.stream().map(ClassElement::getName).toList(); this.returnType = returnType; } diff --git a/core-processor/src/main/java/io/micronaut/inject/annotation/AbstractAnnotationMetadataBuilder.java b/core-processor/src/main/java/io/micronaut/inject/annotation/AbstractAnnotationMetadataBuilder.java index 4e006c9cdbc..308553e9fa1 100644 --- a/core-processor/src/main/java/io/micronaut/inject/annotation/AbstractAnnotationMetadataBuilder.java +++ b/core-processor/src/main/java/io/micronaut/inject/annotation/AbstractAnnotationMetadataBuilder.java @@ -582,6 +582,7 @@ protected AnnotationValue readNestedAnnotationValue(T annotationElement, A an * Obtain the annotation mappers for the given annotation name. * * @param annotationName The annotation name + * @param The annotation type * @return The mappers */ @NonNull diff --git a/core-processor/src/main/java/io/micronaut/inject/ast/ClassElement.java b/core-processor/src/main/java/io/micronaut/inject/ast/ClassElement.java index 7ace87fc055..e2df06f9d6b 100644 --- a/core-processor/src/main/java/io/micronaut/inject/ast/ClassElement.java +++ b/core-processor/src/main/java/io/micronaut/inject/ast/ClassElement.java @@ -25,6 +25,7 @@ import io.micronaut.core.naming.NameUtils; import io.micronaut.core.type.DefaultArgument; import io.micronaut.core.util.ArgumentUtils; +import io.micronaut.core.util.CollectionUtils; import io.micronaut.inject.ast.beans.BeanElementBuilder; import java.lang.reflect.GenericArrayType; @@ -36,6 +37,8 @@ import java.util.Arrays; import java.util.Collection; import java.util.Collections; +import java.util.Iterator; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Objects; @@ -43,8 +46,9 @@ import java.util.OptionalDouble; import java.util.OptionalInt; import java.util.OptionalLong; +import java.util.Set; import java.util.function.Function; -import java.util.stream.Collectors; +import java.util.stream.Stream; import static io.micronaut.inject.writer.BeanDefinitionVisitor.PROXY_SUFFIX; @@ -101,6 +105,16 @@ default boolean isWildcard() { return this instanceof WildcardElement; } + /** + * Is raw type. + * @return true if the type is raw + * @since 4.0.0 + */ + @Experimental + default boolean isRawType() { + return false; + } + /** * Tests whether one type is assignable to another. * @@ -124,6 +138,7 @@ default boolean isOptional() { /** * Checks whether the bean type is a container type. + * * @return Whether the type is a container type like {@link Iterable}. * @since 4.0.0 */ @@ -192,7 +207,7 @@ default boolean isProxy() { /** * Find and return a single primary constructor. If more than constructor candidate exists, then return empty unless a - * constructor is found that is annotated with either {@link io.micronaut.core.annotation.Creator} or {@link javax.inject.Inject}. + * constructor is found that is annotated with either {@link io.micronaut.core.annotation.Creator} or {@link AnnotationUtil#INJECT}. * * @return The primary constructor if one is present */ @@ -213,15 +228,15 @@ default boolean isProxy() { return Optional.of(constructors.get(0)); } Optional annotatedConstructor = constructors.stream() - .filter(c -> c.hasStereotype(AnnotationUtil.INJECT) || c.hasStereotype(Creator.class)) - .findFirst(); + .filter(c -> c.hasStereotype(AnnotationUtil.INJECT) || c.hasStereotype(Creator.class)) + .findFirst(); if (annotatedConstructor.isPresent()) { return annotatedConstructor.map(c -> c); } return constructors.stream() - .filter(io.micronaut.inject.ast.Element::isPublic) - .map(c -> c) - .findFirst(); + .filter(io.micronaut.inject.ast.Element::isPublic) + .map(c -> c) + .findFirst(); } /** @@ -240,8 +255,8 @@ default Optional getDefaultConstructor() { return Optional.empty(); } List constructors = getAccessibleConstructors() - .stream() - .filter(ctor -> ctor.getParameters().length == 0).toList(); + .stream() + .filter(ctor -> ctor.getParameters().length == 0).toList(); if (constructors.isEmpty()) { return Optional.empty(); } @@ -249,9 +264,9 @@ default Optional getDefaultConstructor() { return Optional.of(constructors.get(0)); } return constructors.stream() - .filter(Element::isPublic) - .map(c -> c) - .findFirst(); + .filter(Element::isPublic) + .map(c -> c) + .findFirst(); } @@ -271,7 +286,7 @@ default Optional findStaticCreator() { } //Can be multiple static @Creator methods. Prefer one with args here. The no arg method (if present) will //be picked up by findDefaultStaticCreator - List withArgs = staticCreators.stream().filter(method -> method.getParameters().length > 0).collect(Collectors.toList()); + List withArgs = staticCreators.stream().filter(method -> method.getParameters().length > 0).toList(); if (withArgs.size() == 1) { return Optional.of(withArgs.get(0)); } else { @@ -284,13 +299,14 @@ default Optional findStaticCreator() { * Find and return a single default static creator. A default static creator is one * without arguments that is accessible. * * + * * @return a static creator * @since 4.0.0 */ default Optional findDefaultStaticCreator() { List staticCreators = getAccessibleStaticCreators() - .stream() - .filter(c -> c.getParameters().length == 0).toList(); + .stream() + .filter(c -> c.getParameters().length == 0).toList(); if (staticCreators.isEmpty()) { return Optional.empty(); } @@ -309,9 +325,9 @@ default Optional findDefaultStaticCreator() { @NonNull default List getAccessibleConstructors() { return getEnclosedElements(ElementQuery.CONSTRUCTORS) - .stream() - .filter(ctor -> !ctor.isPrivate()) - .collect(Collectors.toList()); + .stream() + .filter(ctor -> !ctor.isPrivate()) + .toList(); } /** @@ -325,23 +341,23 @@ default List getAccessibleConstructors() { @NonNull default List getAccessibleStaticCreators() { List creators = getEnclosedElements(ElementQuery.ALL_METHODS - .onlyDeclared() - .onlyStatic() - .onlyAccessible() - .annotated(annotationMetadata -> annotationMetadata.hasStereotype(Creator.class)) - ) - .stream() - .filter(method -> method.getReturnType().isAssignable(this)) - .collect(Collectors.toList()); - if (creators.isEmpty() && isEnum()) { - return getEnclosedElements(ElementQuery.ALL_METHODS - .named("valueOf") + .onlyDeclared() .onlyStatic() .onlyAccessible() - ) + .annotated(annotationMetadata -> annotationMetadata.hasStereotype(Creator.class)) + ) .stream() .filter(method -> method.getReturnType().isAssignable(this)) - .collect(Collectors.toList()); + .toList(); + if (creators.isEmpty() && isEnum()) { + return getEnclosedElements(ElementQuery.ALL_METHODS + .named("valueOf") + .onlyStatic() + .onlyAccessible() + ) + .stream() + .filter(method -> method.getReturnType().isAssignable(this)) + .toList(); } return creators; } @@ -540,7 +556,7 @@ default List getDeclaredGenericPlaceholders @NonNull @Experimental default ClassElement getRawClassElement() { - return withBoundGenericTypes(Collections.emptyList()); + return withTypeArguments(Collections.emptyList()); } /** @@ -551,11 +567,13 @@ default ClassElement getRawClassElement() { * @param typeArguments The new type arguments. * @return A {@link ClassElement} of the same raw class with the new type arguments. * @throws UnsupportedOperationException If any of the given type arguments are unsupported. + * @deprecated replaced with {@link #withTypeArguments(Collection)} for consistent API. */ @NonNull @Experimental + @Deprecated(since = "4", forRemoval = true) default ClassElement withBoundGenericTypes(@NonNull List typeArguments) { - return this; + return withTypeArguments((Collection) typeArguments); } /** @@ -577,11 +595,11 @@ default ClassElement withBoundGenericTypes(@NonNull List */ @Experimental default ClassElement foldBoundGenericTypes(@NonNull Function fold) { - List typeArgs = getBoundGenericTypes().stream().map(arg -> arg.foldBoundGenericTypes(fold)).collect(Collectors.toList()); + List typeArgs = getBoundGenericTypes().stream().map(arg -> arg.foldBoundGenericTypes(fold)).toList(); if (typeArgs.contains(null)) { typeArgs = Collections.emptyList(); } - return fold.apply(withBoundGenericTypes(typeArgs)); + return fold.apply(withTypeArguments(typeArgs)); } /** @@ -591,8 +609,10 @@ default ClassElement foldBoundGenericTypes(@NonNull Function getTypeArguments(@NonNull String type) { - return Collections.emptyMap(); + @NonNull + default Map getTypeArguments(@NonNull String type) { + ArgumentUtils.requireNonNull("type", type); + return getAllTypeArguments().getOrDefault(type, Collections.emptyMap()); } /** @@ -601,7 +621,8 @@ default ClassElement foldBoundGenericTypes(@NonNull Function getTypeArguments(@NonNull Class type) { + @NonNull + default Map getTypeArguments(@NonNull Class type) { ArgumentUtils.requireNonNull("type", type); return getTypeArguments(type.getName()); } @@ -609,7 +630,8 @@ default ClassElement foldBoundGenericTypes(@NonNull Function getTypeArguments() { + @NonNull + default Map getTypeArguments() { return Collections.emptyMap(); } @@ -619,8 +641,15 @@ default ClassElement foldBoundGenericTypes(@NonNull Function> getAllTypeArguments() { - return Collections.emptyMap(); + @NonNull + default Map> getAllTypeArguments() { + Map> result = new LinkedHashMap<>(); + Stream.concat( + getInterfaces().stream(), + getSuperType().stream() + ).map(ClassElement::getAllTypeArguments).forEach(result::putAll); + result.put(getName(), getTypeArguments()); + return result; } /** @@ -668,8 +697,8 @@ default boolean isAssignable(Class type) { * @param type The type of the bean * @return A bean builder */ - default @NonNull - BeanElementBuilder addAssociatedBean(@NonNull ClassElement type) { + @NonNull + default BeanElementBuilder addAssociatedBean(@NonNull ClassElement type) { throw new UnsupportedOperationException("Element of type [" + getClass() + "] does not support adding associated beans at compilation time"); } @@ -685,19 +714,48 @@ default ClassElement withAnnotationMetadata(AnnotationMetadata annotationMetadat * @return A new element * @since 4.0.0 */ + @NonNull default ClassElement withTypeArguments(Map typeArguments) { throw new UnsupportedOperationException("Element of type [" + getClass() + "] does not support copy constructor"); } + /** + * Copies this element and overrides its type arguments. + * Variation of {@link #withTypeArguments(Map)} that doesn't require type argument names. + * + * @param typeArguments The type arguments + * @return A new element + * @since 4.0.0 + */ + @NonNull + default ClassElement withTypeArguments(@NonNull Collection typeArguments) { + if (typeArguments.isEmpty()) { + // Allow to eliminate all arguments + return withTypeArguments(Collections.emptyMap()); + } + Set genericNames = getTypeArguments().keySet(); + if (genericNames.size() != typeArguments.size()) { + throw new IllegalStateException("Expected to have: " + genericNames.size() + " type arguments! Got: " + typeArguments.size()); + } + Map boundByName = CollectionUtils.newLinkedHashMap(typeArguments.size()); + Iterator keys = genericNames.iterator(); + Iterator args = typeArguments.iterator(); + while (keys.hasNext() && args.hasNext()) { + boundByName.put(keys.next(), args.next()); + } + return withTypeArguments(boundByName); + } + /** * Create a class element for the given simple type. * * @param type The type * @return The class element */ - static @NonNull ClassElement of(@NonNull Class type) { + @NonNull + static ClassElement of(@NonNull Class type) { return new ReflectClassElement( - Objects.requireNonNull(type, "Type cannot be null") + Objects.requireNonNull(type, "Type cannot be null") ); } @@ -711,14 +769,13 @@ default ClassElement withTypeArguments(Map typeArguments) @NonNull static ClassElement of(@NonNull Type type) { Objects.requireNonNull(type, "Type cannot be null"); - if (type instanceof Class) { - return new ReflectClassElement((Class) type); - } else if (type instanceof TypeVariable) { - return new ReflectGenericPlaceholderElement((TypeVariable) type, 0); - } else if (type instanceof WildcardType) { - return new ReflectWildcardElement((WildcardType) type); - } else if (type instanceof ParameterizedType) { - ParameterizedType pType = (ParameterizedType) type; + if (type instanceof Class aClass) { + return new ReflectClassElement(aClass); + } else if (type instanceof TypeVariable typeVariable) { + return new ReflectGenericPlaceholderElement(typeVariable, 0); + } else if (type instanceof WildcardType wildcardType) { + return new ReflectWildcardElement(wildcardType); + } else if (type instanceof ParameterizedType pType) { if (pType.getOwnerType() != null) { throw new UnsupportedOperationException("Owner types are not supported"); } @@ -727,12 +784,12 @@ static ClassElement of(@NonNull Type type) { @Override public List getBoundGenericTypes() { return Arrays.stream(pType.getActualTypeArguments()) - .map(ClassElement::of) - .collect(Collectors.toList()); + .map(ClassElement::of) + .toList(); } }; - } else if (type instanceof GenericArrayType) { - return of(((GenericArrayType) type).getGenericComponentType()).toArray(); + } else if (type instanceof GenericArrayType genericArrayType) { + return of(genericArrayType.getGenericComponentType()).toArray(); } else { throw new IllegalArgumentException("Bad type: " + type.getClass().getName()); } @@ -747,14 +804,14 @@ public List getBoundGenericTypes() { * @return The class element * @since 2.4.0 */ - static @NonNull ClassElement of( - @NonNull Class type, - @NonNull AnnotationMetadata annotationMetadata, - @NonNull Map typeArguments) { + @NonNull + static ClassElement of(@NonNull Class type, + @NonNull AnnotationMetadata annotationMetadata, + @NonNull Map typeArguments) { Objects.requireNonNull(annotationMetadata, "Annotation metadata cannot be null"); Objects.requireNonNull(typeArguments, "Type arguments cannot be null"); return new ReflectClassElement( - Objects.requireNonNull(type, "Type cannot be null") + Objects.requireNonNull(type, "Type cannot be null") ) { @Override public AnnotationMetadata getAnnotationMetadata() { @@ -770,8 +827,8 @@ public Map getTypeArguments() { @Override public List getBoundGenericTypes() { return getDeclaredGenericPlaceholders().stream() - .map(tv -> typeArguments.get(tv.getVariableName())) - .collect(Collectors.toList()); + .map(tv -> typeArguments.get(tv.getVariableName())) + .toList(); } }; } @@ -783,7 +840,8 @@ public List getBoundGenericTypes() { * @return The class element */ @Internal - static @NonNull ClassElement of(@NonNull String typeName) { + @NonNull + static ClassElement of(@NonNull String typeName) { return new SimpleClassElement(typeName); } @@ -796,7 +854,8 @@ public List getBoundGenericTypes() { * @return The class element */ @Internal - static @NonNull ClassElement of(@NonNull String typeName, boolean isInterface, @Nullable AnnotationMetadata annotationMetadata) { + @NonNull + static ClassElement of(@NonNull String typeName, boolean isInterface, @Nullable AnnotationMetadata annotationMetadata) { return new SimpleClassElement(typeName, isInterface, annotationMetadata); } @@ -810,7 +869,8 @@ public List getBoundGenericTypes() { * @return The class element */ @Internal - static @NonNull ClassElement of(@NonNull String typeName, boolean isInterface, @Nullable AnnotationMetadata annotationMetadata, Map typeArguments) { + @NonNull + static ClassElement of(@NonNull String typeName, boolean isInterface, @Nullable AnnotationMetadata annotationMetadata, Map typeArguments) { return new SimpleClassElement(typeName, isInterface, annotationMetadata); } } diff --git a/core-processor/src/main/java/io/micronaut/inject/ast/GenericPlaceholderElement.java b/core-processor/src/main/java/io/micronaut/inject/ast/GenericPlaceholderElement.java index 913b2b5d015..df99d728064 100644 --- a/core-processor/src/main/java/io/micronaut/inject/ast/GenericPlaceholderElement.java +++ b/core-processor/src/main/java/io/micronaut/inject/ast/GenericPlaceholderElement.java @@ -32,6 +32,7 @@ */ @Experimental public interface GenericPlaceholderElement extends ClassElement { + /** * Returns the bounds of this the generic placeholder empty. Always returns a non-empty list. * diff --git a/core-processor/src/main/java/io/micronaut/inject/ast/WildcardElement.java b/core-processor/src/main/java/io/micronaut/inject/ast/WildcardElement.java index 130d565f859..9240dfa6ab9 100644 --- a/core-processor/src/main/java/io/micronaut/inject/ast/WildcardElement.java +++ b/core-processor/src/main/java/io/micronaut/inject/ast/WildcardElement.java @@ -42,4 +42,36 @@ public interface WildcardElement extends ClassElement { */ @NonNull List getLowerBounds(); + + /** + * Is bounded wildcard - not "< ? >". + * @return true if the wildcard is bounded, false otherwise + * @since 4.0.0 + */ + default boolean isBounded() { + return !getName().equals("java.lang.Object"); + } + + /** + * Find the most upper type. + * @param bounds1 The bounds 1 + * @param bounds2 The bounds 2 + * @param The class element type + * @return the most upper type + */ + @NonNull + static T findUpperType(@NonNull List bounds1, @NonNull List bounds2) { + T upper = null; + for (T lowerBound : bounds2) { + if (upper == null || lowerBound.isAssignable(upper)) { + upper = lowerBound; + } + } + for (T upperBound : bounds1) { + if (upper == null || upperBound.isAssignable(upper)) { + upper = upperBound; + } + } + return upper; + } } diff --git a/core-processor/src/main/java/io/micronaut/inject/ast/annotation/ElementAnnotationMetadataFactory.java b/core-processor/src/main/java/io/micronaut/inject/ast/annotation/ElementAnnotationMetadataFactory.java index 629b224bc78..6eda2ba4d9a 100644 --- a/core-processor/src/main/java/io/micronaut/inject/ast/annotation/ElementAnnotationMetadataFactory.java +++ b/core-processor/src/main/java/io/micronaut/inject/ast/annotation/ElementAnnotationMetadataFactory.java @@ -16,12 +16,9 @@ package io.micronaut.inject.ast.annotation; import io.micronaut.core.annotation.AnnotationMetadata; -import io.micronaut.core.annotation.Experimental; import io.micronaut.core.annotation.NonNull; import io.micronaut.inject.ast.Element; -import java.util.function.Function; - /** * Element's annotation metadata factory. * @@ -58,43 +55,4 @@ public interface ElementAnnotationMetadataFactory { @NonNull ElementAnnotationMetadataFactory readOnly(); - /** - * Creates a factory wrapper that would override the annotation metadata value for the provided native type. - * @param nativeType The native type - * @param fn The function to build the annotation metadata - * @return a new factory - */ - @Experimental - @NonNull - default ElementAnnotationMetadataFactory overrideForNativeType(Object nativeType, - Function fn) { - ElementAnnotationMetadataFactory thisFactory = this; - return new ElementAnnotationMetadataFactory() { - - private boolean fetched; - - @Override - public ElementAnnotationMetadata build(Element element) { - if (!fetched && element.getNativeType().equals(nativeType)) { - fetched = true; - return fn.apply(element); - } - return thisFactory.build(element); - } - - @Override - public ElementAnnotationMetadata build(Element element, AnnotationMetadata annotationMetadata) { - if (!fetched && element.getNativeType().equals(nativeType)) { - fetched = true; - return fn.apply(element); - } - return thisFactory.build(element, annotationMetadata); - } - - @Override - public ElementAnnotationMetadataFactory readOnly() { - throw new IllegalStateException("Not supported!"); - } - }; - } } diff --git a/core-processor/src/main/java/io/micronaut/inject/writer/AbstractClassFileWriter.java b/core-processor/src/main/java/io/micronaut/inject/writer/AbstractClassFileWriter.java index 63162d5b769..5e7f57e0c01 100644 --- a/core-processor/src/main/java/io/micronaut/inject/writer/AbstractClassFileWriter.java +++ b/core-processor/src/main/java/io/micronaut/inject/writer/AbstractClassFileWriter.java @@ -235,8 +235,7 @@ protected static void pushTypeArgumentElements( generatorAdapter.visitInsn(ACONST_NULL); return; } - Set visitedTypes = new HashSet<>(5); - pushTypeArgumentElements(owningType, owningTypeWriter, generatorAdapter, declaringElementName, types, visitedTypes, defaults, loadTypeMethods); + pushTypeArgumentElements(owningType, owningTypeWriter, generatorAdapter, declaringElementName, null, types, new HashSet<>(5), defaults, loadTypeMethods); } private static void pushTypeArgumentElements( @@ -244,55 +243,60 @@ private static void pushTypeArgumentElements( ClassWriter declaringClassWriter, GeneratorAdapter generatorAdapter, String declaringElementName, + @Nullable + ClassElement element, Map types, - Set visitedTypes, + Set visitedTypes, Map defaults, Map loadTypeMethods) { - if (visitedTypes.contains(declaringElementName)) { - generatorAdapter.getStatic( - TYPE_ARGUMENT, - ZERO_ARGUMENTS_CONSTANT, - TYPE_ARGUMENT_ARRAY - ); - } else { - visitedTypes.add(declaringElementName); + if (element == null || element.getClass().getSimpleName().equals("KotlinClassElement")) { + if (visitedTypes.contains(declaringElementName)) { + generatorAdapter.getStatic( + TYPE_ARGUMENT, + ZERO_ARGUMENTS_CONSTANT, + TYPE_ARGUMENT_ARRAY + ); + return; + } else { + visitedTypes.add(declaringElementName); + } + } - int len = types.size(); - // Build calls to Argument.create(...) - pushNewArray(generatorAdapter, Argument.class, len); - int i = 0; - for (Map.Entry entry : types.entrySet()) { - // the array index - generatorAdapter.push(i); - String argumentName = entry.getKey(); - ClassElement classElement = entry.getValue(); - Type classReference = JavaModelUtils.getTypeReference(classElement); - Map typeArguments = classElement.getTypeArguments(); - if (CollectionUtils.isNotEmpty(typeArguments) || !classElement.getAnnotationMetadata().isEmpty()) { - buildArgumentWithGenerics( - owningType, - declaringClassWriter, - generatorAdapter, - argumentName, - classReference, - classElement, - typeArguments, - visitedTypes, - defaults, - loadTypeMethods - ); - } else { - buildArgument(generatorAdapter, argumentName, classElement); - } + int len = types.size(); + // Build calls to Argument.create(...) + pushNewArray(generatorAdapter, Argument.class, len); + int i = 0; + for (Map.Entry entry : types.entrySet()) { + // the array index + generatorAdapter.push(i); + String argumentName = entry.getKey(); + ClassElement classElement = entry.getValue(); + Type classReference = JavaModelUtils.getTypeReference(classElement); + Map typeArguments = classElement.getTypeArguments(); + if (CollectionUtils.isNotEmpty(typeArguments) || !classElement.getAnnotationMetadata().isEmpty()) { + buildArgumentWithGenerics( + owningType, + declaringClassWriter, + generatorAdapter, + argumentName, + classReference, + classElement, + typeArguments, + visitedTypes, + defaults, + loadTypeMethods + ); + } else { + buildArgument(generatorAdapter, argumentName, classElement); + } - // store the type reference - generatorAdapter.visitInsn(AASTORE); - // if we are not at the end of the array duplicate array onto the stack - if (i != (len - 1)) { - generatorAdapter.visitInsn(DUP); - } - i++; + // store the type reference + generatorAdapter.visitInsn(AASTORE); + // if we are not at the end of the array duplicate array onto the stack + if (i != (len - 1)) { + generatorAdapter.visitInsn(DUP); } + i++; } } @@ -378,7 +382,7 @@ protected static void buildArgumentWithGenerics( Type typeReference, ClassElement classElement, Map typeArguments, - Set visitedTypes, + Set visitedTypes, Map defaults, Map loadTypeMethods) { // 1st argument: the type @@ -389,7 +393,16 @@ protected static void buildArgumentWithGenerics( AnnotationMetadata annotationMetadata = MutableAnnotationMetadata.of(classElement.getAnnotationMetadata()); boolean hasAnnotationMetadata = !annotationMetadata.isEmpty(); - if (!hasAnnotationMetadata && typeArguments.isEmpty()) { + boolean isRecursiveType = false; + if (classElement instanceof GenericPlaceholderElement) { + if (visitedTypes.contains(classElement)) { + isRecursiveType = true; + } else { + visitedTypes.add(classElement); + } + } + + if (isRecursiveType || !hasAnnotationMetadata && typeArguments.isEmpty()) { invokeInterfaceStaticMethod( generatorAdapter, Argument.class, @@ -418,6 +431,7 @@ protected static void buildArgumentWithGenerics( owningClassWriter, generatorAdapter, classElement.getName(), + classElement, typeArguments, visitedTypes, defaults, diff --git a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/utils/AstGenericUtils.groovy b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/utils/AstGenericUtils.groovy index 38203821e3d..26f342e8da6 100644 --- a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/utils/AstGenericUtils.groovy +++ b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/utils/AstGenericUtils.groovy @@ -235,7 +235,7 @@ class AstGenericUtils { } } - /** + /** * Builds all the generic information for the given type * @param classNode * @return diff --git a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/AbstractGroovyElement.java b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/AbstractGroovyElement.java index 1a15f9f7f34..14bea6af771 100644 --- a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/AbstractGroovyElement.java +++ b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/AbstractGroovyElement.java @@ -26,12 +26,15 @@ import io.micronaut.core.util.StringUtils; import io.micronaut.inject.ast.ClassElement; import io.micronaut.inject.ast.Element; +import io.micronaut.inject.ast.ElementModifier; +import io.micronaut.inject.ast.PrimitiveElement; +import io.micronaut.inject.ast.WildcardElement; import io.micronaut.inject.ast.annotation.ElementAnnotationMetadata; import io.micronaut.inject.ast.annotation.ElementAnnotationMetadataFactory; -import io.micronaut.inject.ast.ElementModifier; -import io.micronaut.inject.ast.annotation.MutableAnnotationMetadataDelegate; import io.micronaut.inject.ast.annotation.ElementMutableAnnotationMetadataDelegate; +import io.micronaut.inject.ast.annotation.MutableAnnotationMetadataDelegate; import org.codehaus.groovy.ast.AnnotatedNode; +import org.codehaus.groovy.ast.ClassHelper; import org.codehaus.groovy.ast.ClassNode; import org.codehaus.groovy.ast.FieldNode; import org.codehaus.groovy.ast.GenericsType; @@ -42,9 +45,10 @@ import java.lang.annotation.Annotation; import java.lang.reflect.Modifier; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; -import java.util.HashMap; import java.util.HashSet; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Objects; @@ -53,11 +57,13 @@ import java.util.function.Consumer; import java.util.function.Predicate; import java.util.regex.Pattern; +import java.util.stream.Stream; /** * Abstract Groovy element. * * @author Graeme Rocher + * @author Denis Stepanov * @since 1.1 */ @@ -203,182 +209,295 @@ public boolean isPackagePrivate() { return hasDeclaredAnnotation(PackageScope.class); } - /** - * Align the given generic types. - * - * @param genericsTypes The generic types - * @param redirectTypes The redirect types - * @param genericsSpec The current generics spec - * @return The new generic spec - */ - protected Map alignNewGenericsInfo( - @NonNull GenericsType[] genericsTypes, - @NonNull GenericsType[] redirectTypes, - @NonNull Map genericsSpec) { - if (redirectTypes == null || redirectTypes.length != genericsTypes.length) { - return Collections.emptyMap(); - } else { + @NonNull + protected final ClassElement newClassElement(@NonNull ClassNode type, @Nullable Map genericsSpec) { + if (genericsSpec == null) { + return newClassElement(type); + } + return newClassElement(type, genericsSpec, new HashSet<>(), false, false); + } - Map newSpec = new HashMap<>(genericsSpec.size()); - for (int i = 0; i < genericsTypes.length; i++) { - GenericsType genericsType = genericsTypes[i]; - GenericsType redirectType = redirectTypes[i]; - String name = genericsType.getName(); - if (genericsType.isWildcard()) { - ClassNode[] upperBounds = genericsType.getUpperBounds(); - if (ArrayUtils.isNotEmpty(upperBounds)) { - name = upperBounds[0].getUnresolvedName(); - } else { - ClassNode lowerBound = genericsType.getLowerBound(); - if (lowerBound != null) { - name = lowerBound.getUnresolvedName(); - } - } - ClassNode cn = resolveGenericPlaceholder(genericsSpec, name); - toNewGenericSpec(genericsSpec, newSpec, redirectType.getName(), cn); + @NonNull + protected final ClassElement newClassElement(@NonNull GenericsType genericsType) { + return newClassElement(genericsType, genericsType, Collections.emptyMap(), new HashSet<>(), false); + } + + @NonNull + protected final ClassElement newClassElement(@NonNull ClassNode type) { + return newClassElement(type, Collections.emptyMap(), new HashSet<>(), false, false); + } + + @NonNull + private ClassElement newClassElement(GenericsType genericsType, + GenericsType redirectType, + Map parentTypeArguments, + Set visitedTypes, + boolean isRawType) { + if (parentTypeArguments == null) { + parentTypeArguments = Collections.emptyMap(); + } + if (genericsType.isWildcard()) { + return resolveWildcard(genericsType, redirectType, parentTypeArguments, visitedTypes); + } + if (genericsType.isPlaceholder()) { + return resolvePlaceholder(genericsType, redirectType, parentTypeArguments, visitedTypes, isRawType); + } + return newClassElement(genericsType.getType(), parentTypeArguments, visitedTypes, true, isRawType); + } + + @NonNull + private ClassElement newClassElement(ClassNode classNode, + Map parentTypeArguments, + Set visitedTypes, + boolean isTypeVariable, + boolean isRawTypeParameter) { + return newClassElement(classNode, parentTypeArguments, visitedTypes, isTypeVariable, isRawTypeParameter, false); + } + + @NonNull + private ClassElement newClassElement(ClassNode classNode, + Map parentTypeArguments, + Set visitedTypes, + boolean isTypeVariable, + boolean isRawTypeParameter, + boolean stripTypeArguments) { + if (parentTypeArguments == null) { + parentTypeArguments = Collections.emptyMap(); + } + if (classNode.isArray()) { + ClassNode componentType = classNode.getComponentType(); + return newClassElement(componentType, parentTypeArguments, visitedTypes, isTypeVariable, isRawTypeParameter) + .toArray(); + } + if (classNode.isGenericsPlaceHolder()) { + GenericsType genericsType; + GenericsType redirectType; + GenericsType[] genericsTypes = classNode.getGenericsTypes(); + if (ArrayUtils.isNotEmpty(genericsTypes)) { + genericsType = genericsTypes[0]; + GenericsType[] redirectTypes = classNode.redirect().getGenericsTypes(); + if (ArrayUtils.isNotEmpty(redirectTypes)) { + redirectType = redirectTypes[0]; } else { - ClassNode classNode = genericsType.getType(); - GenericsType[] typeParameters = classNode.getGenericsTypes(); - - if (ArrayUtils.isNotEmpty(typeParameters)) { - GenericsType[] redirectParameters = classNode.redirect().getGenericsTypes(); - if (redirectParameters != null && typeParameters.length == redirectParameters.length) { - List resolvedTypes = new ArrayList<>(typeParameters.length); - for (int j = 0; j < redirectParameters.length; j++) { - ClassNode type = typeParameters[j].getType(); - if (type.isGenericsPlaceHolder()) { - String unresolvedName = type.getUnresolvedName(); - ClassNode resolvedType = resolveGenericPlaceholder(genericsSpec, unresolvedName); - if (resolvedType != null) { - resolvedTypes.add(resolvedType); - } else { - resolvedTypes.add(type); - } - } else { - resolvedTypes.add(type); - } - } - - ClassNode plainNodeReference = classNode.getPlainNodeReference(); - plainNodeReference.setUsingGenerics(true); - plainNodeReference.setGenericsTypes(resolvedTypes.stream().map(GenericsType::new).toArray(GenericsType[]::new)); - newSpec.put(redirectType.getName(), plainNodeReference); - } else { - ClassNode cn = resolveGenericPlaceholder(genericsSpec, name); - if (cn != null) { - newSpec.put(redirectType.getName(), cn); - } else { - newSpec.put(redirectType.getName(), classNode); - } - } - } else { - ClassNode cn = resolveGenericPlaceholder(genericsSpec, name); - toNewGenericSpec(genericsSpec, newSpec, redirectType.getName(), cn); - } + redirectType = new GenericsType(classNode.redirect()); } + } else { + // Bypass Groovy compiler weirdness + genericsType = new GenericsType(classNode.redirect()); + redirectType = genericsType; } - return newSpec; + return newClassElement(genericsType, redirectType, parentTypeArguments, visitedTypes, isRawTypeParameter); + } + if (ClassHelper.isPrimitiveType(classNode)) { + return PrimitiveElement.valueOf(classNode.getName()); } + if (classNode.isEnum()) { + return new GroovyEnumElement(visitorContext, classNode, elementAnnotationMetadataFactory); + } + if (classNode.isAnnotationDefinition()) { + return new GroovyAnnotationElement(visitorContext, classNode, elementAnnotationMetadataFactory); + } + Map newTypeArguments; + if (stripTypeArguments) { + newTypeArguments = resolveTypeArgumentsToObject(classNode); + } else { + newTypeArguments = resolveTypeArguments(classNode, parentTypeArguments, visitedTypes); + } + return new GroovyClassElement(visitorContext, classNode, elementAnnotationMetadataFactory, newTypeArguments, 0, isTypeVariable); } - private @Nullable ClassNode resolveGenericPlaceholder(@NonNull Map genericsSpec, String name) { - ClassNode classNode = genericsSpec.get(name); - while (classNode != null && classNode.isGenericsPlaceHolder()) { - ClassNode cn = genericsSpec.get(classNode.getUnresolvedName()); - if (cn == classNode) { - break; - } - if (cn != null) { - classNode = cn; + private ClassElement resolvePlaceholder(GenericsType genericsType, + GenericsType redirectType, + Map parentTypeArguments, + Set visitedTypes, + boolean isRawType) { + PlaceholderEntry entry = new PlaceholderEntry(getNativeType(), genericsType.getName()); + String variableName = genericsType.getName(); + ClassElement boundVariable = parentTypeArguments.get(variableName); + if (boundVariable != null) { + if (boundVariable instanceof WildcardElement wildcardElement) { + if (wildcardElement.isBounded()) { + return wildcardElement; + } } else { - break; + return boundVariable; } } - return classNode; - } + List classNodeBounds = new ArrayList<>(); + addBounds(genericsType, classNodeBounds); + if (genericsType != redirectType) { + addBounds(redirectType, classNodeBounds); + } - private void toNewGenericSpec(Map genericsSpec, Map newSpec, String name, ClassNode cn) { - if (cn != null) { + boolean alreadyVisitedPlaceholder = visitedTypes.contains(entry); + if (!alreadyVisitedPlaceholder) { + visitedTypes.add(entry); + } - if (cn.isGenericsPlaceHolder()) { - String n = cn.getUnresolvedName(); - ClassNode resolved = resolveGenericPlaceholder(genericsSpec, n); - if (resolved == cn) { - newSpec.put(name, cn); - } else { - toNewGenericSpec(genericsSpec, newSpec, name, resolved); + List bounds = classNodeBounds + .stream() + .map(classNode -> { + if (alreadyVisitedPlaceholder && classNode.isGenericsPlaceHolder()) { + classNode = classNode.redirect(); + } + return classNode; + }) + .filter(classNode -> !alreadyVisitedPlaceholder || !classNode.isGenericsPlaceHolder()) + .map(classNode -> { + // Strip declared type arguments and replace with an Object to prevent recursion + boolean stripTypeArguments = alreadyVisitedPlaceholder; + return (GroovyClassElement) newClassElement(classNode, parentTypeArguments, visitedTypes, true, isRawType, stripTypeArguments); + }) + .toList(); + + if (bounds.isEmpty()) { + bounds = Collections.singletonList((GroovyClassElement) getObjectClassElement()); + } + return new GroovyGenericPlaceholderElement(visitorContext, genericsType.getType(), bounds, isRawType); + } + + private static void addBounds(GenericsType genericsType, List classNodeBounds) { + if (genericsType.getUpperBounds() != null) { + for (ClassNode ub : genericsType.getUpperBounds()) { + if (!classNodeBounds.contains(ub)) { + classNodeBounds.add(ub); } - } else { - newSpec.put(name, cn); + } + } else { + ClassNode type = genericsType.getType(); + if (!classNodeBounds.contains(type)) { + classNodeBounds.add(type); } } } - /** - * Get a generic element for the given element and data. - * - * @param sourceUnit The source unit - * @param type The type - * @param rawElement A raw element to fall back to - * @param genericsSpec The generics spec - * @return The element, never null. - */ - @NonNull - protected ClassElement getGenericElement( - @NonNull SourceUnit sourceUnit, - @NonNull ClassNode type, - @NonNull ClassElement rawElement, - @NonNull Map genericsSpec) { - if (CollectionUtils.isNotEmpty(genericsSpec)) { - ClassElement classNode = resolveGenericType(genericsSpec, type); - if (classNode != null) { - return classNode; - } else { - GenericsType[] genericsTypes = type.getGenericsTypes(); - GenericsType[] redirectTypes = type.redirect().getGenericsTypes(); - if (genericsTypes != null && redirectTypes != null) { - genericsSpec = alignNewGenericsInfo(genericsTypes, redirectTypes, genericsSpec); - return new GroovyClassElement(visitorContext, type, elementAnnotationMetadataFactory, Collections.singletonMap( - type.getName(), - genericsSpec - ), 0); + private ClassElement getObjectClassElement() { + return visitorContext.getClassElement("java.lang.Object").get(); + } + + private ClassElement resolveWildcard(GenericsType genericsType, + GenericsType redirectType, + Map parentTypeArguments, + Set visitedTypes) { + Stream lowerBounds = Stream.ofNullable(genericsType.getLowerBound()); + Stream upperBounds; + ClassNode[] genericsUpperBounds = genericsType.getUpperBounds(); + if (genericsUpperBounds == null || genericsUpperBounds.length == 0) { + upperBounds = Stream.empty(); + } else { + upperBounds = Arrays.stream(genericsUpperBounds); + } + List upperBoundsAsElements = upperBounds + .map(classNode -> newClassElement(classNode, parentTypeArguments, visitedTypes, true, false)) + .toList(); + List lowerBoundsAsElements = lowerBounds + .map(classNode ->newClassElement(classNode, parentTypeArguments, visitedTypes, true, false)) + .toList(); + if (upperBoundsAsElements.isEmpty()) { + upperBoundsAsElements = Collections.singletonList(getObjectClassElement()); + } + ClassElement upperType = WildcardElement.findUpperType(upperBoundsAsElements, lowerBoundsAsElements); + if (upperType.getType().getName().equals("java.lang.Object")) { + // Not bounded wildcard: + if (redirectType != null && redirectType != genericsType) { + ClassElement definedTypeBound = newClassElement(redirectType, redirectType, parentTypeArguments, visitedTypes, false); + // Use originating parameter to extract the bound defined + if (definedTypeBound instanceof GroovyGenericPlaceholderElement groovyGenericPlaceholderElement) { + upperType = WildcardElement.findUpperType(groovyGenericPlaceholderElement.getBounds(), Collections.emptyList()); } } } - return rawElement; + if (upperType.isPrimitive()) { + // TODO: Support primitives for wildcards (? extends byte[]) + return upperType; + } + return new GroovyWildcardElement( + (GroovyClassElement) upperType, + upperBoundsAsElements.stream().map(GroovyClassElement.class::cast).toList(), + lowerBoundsAsElements.stream().map(GroovyClassElement.class::cast).toList(), + elementAnnotationMetadataFactory + ); } - private ClassElement resolveGenericType(Map typeGenericInfo, ClassNode returnType) { - if (returnType.isGenericsPlaceHolder()) { - String unresolvedName = returnType.getUnresolvedName(); - ClassNode classNode = resolveGenericPlaceholder(typeGenericInfo, unresolvedName); - if (classNode != null) { - if (classNode.isGenericsPlaceHolder() && classNode != returnType) { - return resolveGenericType(typeGenericInfo, classNode); + @NonNull + protected final Map resolveTypeArguments(MethodNode methodNode, + @Nullable Map parentTypeArguments) { + if (parentTypeArguments == null) { + parentTypeArguments = Collections.emptyMap(); + } + Set visitedTypes = new HashSet<>(); + GenericsType[] genericsTypes = methodNode.getGenericsTypes(); + if (ArrayUtils.isEmpty(genericsTypes)) { + return parentTypeArguments; + } + Map newTypeArguments = new LinkedHashMap<>(parentTypeArguments); + for (GenericsType genericsType : genericsTypes) { + String variableName = genericsType.getName(); + ClassNode classNode; + if (genericsType.isPlaceholder()) { + ClassNode[] upperBounds = genericsType.getUpperBounds(); + ClassNode lowerBound = genericsType.getLowerBound(); + if (ArrayUtils.isNotEmpty(upperBounds)) { + classNode = upperBounds[0]; + } else if (lowerBound != null) { + classNode = lowerBound; } else { - return adjustTypeAnnotationMetadata(visitorContext.getElementFactory().newClassElement( - classNode, elementAnnotationMetadataFactory - )); + classNode = ClassHelper.OBJECT_TYPE; } + } else { + classNode = genericsType.getType(); } - } else if (returnType.isArray()) { - ClassNode componentType = returnType.getComponentType(); - if (componentType.isGenericsPlaceHolder()) { - String unresolvedName = componentType.getUnresolvedName(); - ClassNode classNode = resolveGenericPlaceholder(typeGenericInfo, unresolvedName); - if (classNode != null) { - if (classNode.isGenericsPlaceHolder() && classNode != returnType) { - return resolveGenericType(typeGenericInfo, classNode); - } else { - ClassNode cn = classNode.makeArray(); - return adjustTypeAnnotationMetadata(visitorContext.getElementFactory().newClassElement( - cn, elementAnnotationMetadataFactory - )); - } - } + newTypeArguments.put( + variableName, + newClassElement(classNode, parentTypeArguments, visitedTypes, true, false) + ); + } + return newTypeArguments; + } + + @NonNull + protected final Map resolveTypeArguments(ClassNode classNode, + @Nullable Map parentTypeArguments, + Set visitedTypes) { + GenericsType[] genericsTypes = classNode.getGenericsTypes(); + GenericsType[] redirectTypes = classNode.redirect().getGenericsTypes(); + if (redirectTypes == null || redirectTypes.length == 0) { + return Collections.emptyMap(); + } + Map resolved = CollectionUtils.newLinkedHashMap(redirectTypes.length); + if (genericsTypes != null && genericsTypes.length == redirectTypes.length) { + for (int i = 0; i < genericsTypes.length; i++) { + GenericsType genericsType = genericsTypes[i]; + GenericsType redirectType = redirectTypes[i]; + ClassElement classElement = newClassElement(genericsType, redirectType, parentTypeArguments, visitedTypes, false); + resolved.put(redirectType.getName(), classElement); + } + } else { + boolean isRaw = genericsTypes == null; + for (GenericsType redirectType : redirectTypes) { + String variableName = redirectType.getName(); + resolved.put( + variableName, + newClassElement(redirectType, redirectType, parentTypeArguments, visitedTypes, isRaw) + ); } } - return null; + return resolved; + } + + @NonNull + protected final Map resolveTypeArgumentsToObject(ClassNode classNode) { + GenericsType[] redirectTypes = classNode.redirect().getGenericsTypes(); + if (redirectTypes == null || redirectTypes.length == 0) { + return Collections.emptyMap(); + } + ClassElement objectClassElement = getObjectClassElement(); + Map resolved = CollectionUtils.newLinkedHashMap(redirectTypes.length); + for (GenericsType redirectType : redirectTypes) { + String variableName = redirectType.getName(); + resolved.put(variableName, objectClassElement); + } + return resolved; } @Override @@ -389,21 +508,6 @@ public Optional getDocumentation() { return Optional.of(JAVADOC_PATTERN.matcher(annotatedNode.getGroovydoc().getContent()).replaceAll(StringUtils.EMPTY_STRING).trim()); } - /** - * The method will replace the annotation metadata with empty value if {@link io.micronaut.inject.visitor.VisitorConfiguration#includeTypeLevelAnnotationsInGenericArguments()} - * is false. - * - * @param classElement The class element to adjust annotation metadata - * @return the adjusted element or the same value - */ - @NonNull - protected final ClassElement adjustTypeAnnotationMetadata(@NonNull ClassElement classElement) { - if (classElement.isPrimitive() || visitorContext.getConfiguration().includeTypeLevelAnnotationsInGenericArguments()) { - return classElement; - } - return classElement.withAnnotationMetadata(AnnotationMetadata.EMPTY_METADATA); - } - @Override public boolean equals(Object o) { if (this == o) { @@ -470,5 +574,8 @@ private Set resolveModifiers(int mod) { } return modifiers; } + + record PlaceholderEntry(AnnotatedNode owner, String placeholderName) { + } } diff --git a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyClassElement.java b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyClassElement.java index 7c876348b6b..0feee6f3319 100644 --- a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyClassElement.java +++ b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyClassElement.java @@ -19,7 +19,6 @@ import groovy.lang.GroovyObjectSupport; import groovy.lang.Script; import io.micronaut.ast.groovy.utils.AstClassUtils; -import io.micronaut.ast.groovy.utils.AstGenericUtils; import io.micronaut.context.annotation.BeanProperties; import io.micronaut.core.annotation.AnnotationMetadata; import io.micronaut.core.annotation.AnnotationMetadataProvider; @@ -32,11 +31,9 @@ import io.micronaut.core.util.CollectionUtils; import io.micronaut.inject.annotation.AnnotationMetadataHierarchy; import io.micronaut.inject.ast.ArrayableClassElement; -import io.micronaut.inject.ast.PropertyElementQuery; import io.micronaut.inject.ast.ClassElement; import io.micronaut.inject.ast.ConstructorElement; import io.micronaut.inject.ast.Element; -import io.micronaut.inject.ast.annotation.ElementAnnotationMetadataFactory; import io.micronaut.inject.ast.ElementModifier; import io.micronaut.inject.ast.ElementQuery; import io.micronaut.inject.ast.FieldElement; @@ -47,6 +44,8 @@ import io.micronaut.inject.ast.ParameterElement; import io.micronaut.inject.ast.PrimitiveElement; import io.micronaut.inject.ast.PropertyElement; +import io.micronaut.inject.ast.PropertyElementQuery; +import io.micronaut.inject.ast.annotation.ElementAnnotationMetadataFactory; import io.micronaut.inject.ast.utils.AstBeanPropertiesUtils; import io.micronaut.inject.ast.utils.EnclosedElementsQuery; import org.codehaus.groovy.ast.AnnotatedNode; @@ -68,7 +67,7 @@ import java.util.Collection; import java.util.Collections; import java.util.HashSet; -import java.util.LinkedHashMap; +import java.util.Iterator; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; @@ -100,32 +99,30 @@ public class GroovyClassElement extends AbstractGroovyElement implements Arrayab String methodName = m.getName(); return m.isStaticConstructor() || - methodName.startsWith("$") || - methodName.contains("trait$") || - methodName.startsWith("super$") || - methodName.equals("setMetaClass") || - m.getReturnType().getNameWithoutPackage().equals("MetaClass") || - m.getDeclaringClass().equals(ClassHelper.GROOVY_OBJECT_TYPE) || - m.getDeclaringClass().equals(ClassHelper.OBJECT_TYPE); + methodName.startsWith("$") || + methodName.contains("trait$") || + methodName.startsWith("super$") || + methodName.equals("setMetaClass") || + m.getReturnType().getNameWithoutPackage().equals("MetaClass") || + m.getDeclaringClass().equals(ClassHelper.GROOVY_OBJECT_TYPE) || + m.getDeclaringClass().equals(ClassHelper.OBJECT_TYPE); }; private static final Predicate JUNK_FIELD_FILTER = m -> { String fieldName = m.getName(); return fieldName.startsWith("$") || - fieldName.startsWith("__$") || - fieldName.contains("trait$") || - fieldName.equals("metaClass") || - m.getDeclaringClass().equals(ClassHelper.GROOVY_OBJECT_TYPE) || - m.getDeclaringClass().equals(ClassHelper.OBJECT_TYPE); + fieldName.startsWith("__$") || + fieldName.contains("trait$") || + fieldName.equals("metaClass") || + m.getDeclaringClass().equals(ClassHelper.GROOVY_OBJECT_TYPE) || + m.getDeclaringClass().equals(ClassHelper.OBJECT_TYPE); }; protected final ClassNode classNode; + protected Map resolvedTypeArguments; private final int arrayDimensions; private final boolean isTypeVar; - private List overrideBoundGenericTypes; - private Map> genericInfo; private List properties; private List nativeProperties; - private Map resolvedTypeArguments; private final GroovyEnclosedElementsQuery groovyEnclosedElementsQuery = new GroovyEnclosedElementsQuery(false); private final GroovyEnclosedElementsQuery groovySourceEnclosedElementsQuery = new GroovyEnclosedElementsQuery(true); @@ -144,36 +141,35 @@ public GroovyClassElement(GroovyVisitorContext visitorContext, * @param visitorContext The visitor context * @param classNode The {@link ClassNode} * @param annotationMetadataFactory The annotation metadata factory - * @param genericInfo The generic info + * @param resolvedTypeArguments The resolved type arguments * @param arrayDimensions The number of array dimensions */ GroovyClassElement( - GroovyVisitorContext visitorContext, - ClassNode classNode, - ElementAnnotationMetadataFactory annotationMetadataFactory, - Map> genericInfo, - int arrayDimensions) { - this(visitorContext, classNode, annotationMetadataFactory, genericInfo, arrayDimensions, false); + GroovyVisitorContext visitorContext, + ClassNode classNode, + ElementAnnotationMetadataFactory annotationMetadataFactory, + Map resolvedTypeArguments, + int arrayDimensions) { + this(visitorContext, classNode, annotationMetadataFactory, resolvedTypeArguments, arrayDimensions, false); } /** * @param visitorContext The visitor context * @param classNode The {@link ClassNode} * @param annotationMetadataFactory The annotation metadata factory - * @param genericInfo The generic info + * @param resolvedTypeArguments The resolved type arguments * @param arrayDimensions The number of array dimensions * @param isTypeVar Is the element a type variable */ - GroovyClassElement( - GroovyVisitorContext visitorContext, - ClassNode classNode, - ElementAnnotationMetadataFactory annotationMetadataFactory, - Map> genericInfo, - int arrayDimensions, - boolean isTypeVar) { + GroovyClassElement(GroovyVisitorContext visitorContext, + ClassNode classNode, + ElementAnnotationMetadataFactory annotationMetadataFactory, + Map resolvedTypeArguments, + int arrayDimensions, + boolean isTypeVar) { super(visitorContext, classNode, annotationMetadataFactory); this.classNode = classNode; - this.genericInfo = genericInfo; + this.resolvedTypeArguments = resolvedTypeArguments; this.arrayDimensions = arrayDimensions; if (classNode.isArray()) { classNode.setName(classNode.getComponentType().getName()); @@ -183,7 +179,7 @@ public GroovyClassElement(GroovyVisitorContext visitorContext, @Override protected GroovyClassElement copyConstructor() { - return new GroovyClassElement(visitorContext, classNode, elementAnnotationMetadataFactory, genericInfo, arrayDimensions, isTypeVar); + return new GroovyClassElement(visitorContext, classNode, elementAnnotationMetadataFactory, resolvedTypeArguments, arrayDimensions, isTypeVar); } @Override @@ -199,11 +195,35 @@ public ClassElement withAnnotationMetadata(AnnotationMetadata annotationMetadata @Override public ClassElement withTypeArguments(Map typeArguments) { - GroovyClassElement groovyClassElement = new GroovyClassElement(visitorContext, classNode, elementAnnotationMetadataFactory, genericInfo, arrayDimensions); + GroovyClassElement groovyClassElement = new GroovyClassElement(visitorContext, classNode, elementAnnotationMetadataFactory, resolvedTypeArguments, arrayDimensions); groovyClassElement.resolvedTypeArguments = typeArguments; return groovyClassElement; } + @NonNull + public final ClassElement withTypeArguments(@NonNull Collection typeArguments) { + if (typeArguments.isEmpty()) { + // Allow to eliminate all arguments + return withTypeArguments(Collections.emptyMap()); + } + Set genericNames = getTypeArguments().keySet(); + if (genericNames.size() != typeArguments.size()) { + throw new IllegalStateException("Expected to have: " + genericNames.size() + " type arguments! Got: " + typeArguments.size()); + } + Map boundByName = CollectionUtils.newLinkedHashMap(typeArguments.size()); + Iterator keys = genericNames.iterator(); + Iterator args = typeArguments.iterator(); + while (keys.hasNext() && args.hasNext()) { + ClassElement next = args.next(); + Object nativeType = next.getNativeType(); + if (nativeType instanceof Class aClass) { + next = visitorContext.getClassElement(aClass).orElse(next); + } + boundByName.put(keys.next(), next); + } + return withTypeArguments(boundByName); + } + @Override public boolean isTypeVariable() { return isTypeVar; @@ -240,7 +260,7 @@ public boolean isInner() { @Override public Optional getEnclosingType() { if (isInner()) { - return Optional.ofNullable(classNode.getOuterClass()).map(this::toGroovyClassElement); + return Optional.ofNullable(classNode.getOuterClass()).map(inner -> newClassElement(inner, getTypeArguments())); } return Optional.empty(); } @@ -259,26 +279,20 @@ public boolean isPrimitive() { public Collection getInterfaces() { final ClassNode[] interfaces = classNode.getInterfaces(); if (ArrayUtils.isNotEmpty(interfaces)) { - return Arrays.stream(interfaces).map(this::toGroovyClassElement).collect(Collectors.toList()); + return Arrays.stream(interfaces).map(inf -> newClassElement(inf, getTypeArguments())).toList(); } return Collections.emptyList(); } @Override public Optional getSuperType() { - final ClassNode superClass = classNode.getUnresolvedSuperClass(false); + final ClassNode superClass = classNode.getUnresolvedSuperClass(); if (superClass != null && !superClass.equals(ClassHelper.OBJECT_TYPE)) { - return Optional.of( - toGroovyClassElement(superClass) - ); + return Optional.of(newClassElement(superClass, getTypeArguments())); } return Optional.empty(); } - private ClassElement toGroovyClassElement(ClassNode superClass) { - return visitorContext.getElementFactory().newClassElement(superClass, elementAnnotationMetadataFactory); - } - @NonNull @Override public Optional getPrimaryConstructor() { @@ -317,173 +331,15 @@ private Optional createMethodElement(MethodNode method) { }); } - /** - * Builds and returns the generic type information. - * - * @return The generic type info - */ - public Map> getGenericTypeInfo() { - if (genericInfo == null) { - genericInfo = AstGenericUtils.buildAllGenericElementInfo(classNode, new GroovyVisitorContext(sourceUnit, compilationUnit)); - } - return genericInfo; - } - - @NonNull - @Override - public Map getTypeArguments(@NonNull String type) { - Map> allData = getGenericTypeInfo(); - Map thisSpec = allData.get(getName()); - Map forType = allData.get(type); - if (forType != null) { - Map typeArgs = new LinkedHashMap<>(forType.size()); - for (Map.Entry entry : forType.entrySet()) { - ClassNode classNode = entry.getValue(); - ClassElement rawElement = new GroovyClassElement(visitorContext, classNode, elementAnnotationMetadataFactory); - rawElement = adjustTypeAnnotationMetadata(rawElement); - if (thisSpec != null) { - rawElement = getGenericElement(sourceUnit, classNode, rawElement, thisSpec); - } - typeArgs.put(entry.getKey(), rawElement); - } - return Collections.unmodifiableMap(typeArgs); - } - return Collections.emptyMap(); - } - - @NonNull - @Override - public Map> getAllTypeArguments() { - Map> genericInfo = - AstGenericUtils.buildAllGenericElementInfo(classNode, new GroovyVisitorContext(sourceUnit, compilationUnit)); - Map> results = new LinkedHashMap<>(genericInfo.size()); - - genericInfo.forEach((name, generics) -> { - Map resolved = new LinkedHashMap<>(generics.size()); - generics.forEach((variable, type) -> { - ClassElement classElement = new GroovyClassElement(visitorContext, type, elementAnnotationMetadataFactory); - classElement = adjustTypeAnnotationMetadata(classElement); - resolved.put(variable, classElement); - }); - results.put(name, resolved); - }); - results.put(getName(), getTypeArguments()); - return results; - } - @Override @NonNull public Map getTypeArguments() { if (resolvedTypeArguments == null) { - Map> genericInfo = getGenericTypeInfo(); - Map info = genericInfo.get(classNode.getName()); - resolvedTypeArguments = resolveGenericMap(info); + resolvedTypeArguments = resolveTypeArguments(classNode, Collections.emptyMap(), new HashSet<>()); } return resolvedTypeArguments; } - @NonNull - private Map resolveGenericMap(Map info) { - if (info != null) { - Map typeArgumentMap = new LinkedHashMap<>(info.size()); - GenericsType[] genericsTypes = classNode.getGenericsTypes(); - GenericsType[] redirectTypes = classNode.redirect().getGenericsTypes(); - if (genericsTypes != null && redirectTypes != null && genericsTypes.length == redirectTypes.length) { - for (int i = 0; i < genericsTypes.length; i++) { - GenericsType gt = genericsTypes[i]; - GenericsType redirectType = redirectTypes[i]; - if (gt.isPlaceholder()) { - ClassNode cn = resolveTypeArgument(info, redirectType.getName()); - if (cn != null) { - Map newInfo = alignNewGenericsInfo(genericsTypes, redirectTypes, info); - typeArgumentMap.put(redirectType.getName(), adjustTypeAnnotationMetadata(new GroovyClassElement( - visitorContext, - cn, - elementAnnotationMetadataFactory, - Collections.singletonMap(cn.getName(), newInfo), - cn.isArray() ? computeDimensions(cn) : 0, - true - ))); - } - } else { - ClassNode type; - String unresolvedName = redirectType.getType().getUnresolvedName(); - ClassNode cn = info.get(unresolvedName); - if (cn != null) { - type = cn; - } else { - type = gt.getType(); - } - typeArgumentMap.put(redirectType.getName(), adjustTypeAnnotationMetadata(new GroovyClassElement( - visitorContext, - type, - elementAnnotationMetadataFactory, - Collections.emptyMap(), - type.isArray() ? computeDimensions(type) : 0 - ))); - } - } - } else if (redirectTypes != null) { - for (GenericsType gt : redirectTypes) { - String name = gt.getName(); - ClassNode cn = resolveTypeArgument(info, name); - if (cn != null) { - Map newInfo = Collections.emptyMap(); - if (genericsTypes != null) { - newInfo = alignNewGenericsInfo(genericsTypes, redirectTypes, info); - } - typeArgumentMap.put(gt.getName(), adjustTypeAnnotationMetadata(new GroovyClassElement( - visitorContext, - cn, - elementAnnotationMetadataFactory, - Collections.singletonMap(cn.getName(), newInfo), - cn.isArray() ? computeDimensions(cn) : 0 - ))); - } - } - } - if (CollectionUtils.isNotEmpty(typeArgumentMap)) { - return typeArgumentMap; - } - } - Map spec = AstGenericUtils.createGenericsSpec(classNode); - if (!spec.isEmpty()) { - Map map = new LinkedHashMap<>(spec.size()); - for (Map.Entry entry : spec.entrySet()) { - ClassNode cn = entry.getValue(); - ClassElement classElement = visitorContext.getElementFactory().newClassElement(cn, elementAnnotationMetadataFactory); - classElement = adjustTypeAnnotationMetadata(classElement); - map.put(entry.getKey(), classElement); - } - return Collections.unmodifiableMap(map); - } - return Collections.emptyMap(); - } - - @Nullable - private ClassNode resolveTypeArgument(Map info, String name) { - ClassNode cn = info.get(name); - while (cn != null && cn.isGenericsPlaceHolder()) { - name = cn.getUnresolvedName(); - ClassNode next = info.get(name); - if (next == cn) { - break; - } - cn = next; - } - return cn; - } - - private int computeDimensions(ClassNode cn) { - ClassNode componentType = cn.getComponentType(); - int i = 1; - while (componentType != null && componentType.isArray()) { - i++; - componentType = componentType.getComponentType(); - } - return i; - } - @Override public List getSyntheticBeanProperties() { // Native properties should be composed of field + synthetic getter/setter @@ -492,14 +348,14 @@ public List getSyntheticBeanProperties() { configuration.allowStaticProperties(true); Set nativeProps = getPropertyNodes().stream().map(PropertyNode::getName).collect(Collectors.toCollection(LinkedHashSet::new)); nativeProperties = AstBeanPropertiesUtils.resolveBeanProperties(configuration, - this, - () -> getEnclosedElements(ElementQuery.ALL_METHODS.onlyInstance()), - () -> getPropertyNodes().stream().map(propertyNode -> visitorContext.getElementFactory().newFieldElement(this, propertyNode.getField(), elementAnnotationMetadataFactory)).collect(Collectors.toList()), - true, - nativeProps, - methodElement -> Optional.empty(), - methodElement -> Optional.empty(), - value -> mapPropertyElement(nativeProps, value, configuration, true)); + this, + () -> getEnclosedElements(ElementQuery.ALL_METHODS.onlyInstance()), + () -> getPropertyNodes().stream().map(propertyNode -> visitorContext.getElementFactory().newFieldElement(this, propertyNode.getField(), elementAnnotationMetadataFactory)).collect(Collectors.toList()), + true, + nativeProps, + methodElement -> Optional.empty(), + methodElement -> Optional.empty(), + value -> mapPropertyElement(nativeProps, value, configuration, true)); } return nativeProperties; } @@ -508,14 +364,14 @@ public List getSyntheticBeanProperties() { public List getBeanProperties(PropertyElementQuery propertyElementQuery) { Set nativeProps = getPropertyNodes().stream().map(PropertyNode::getName).collect(Collectors.toCollection(LinkedHashSet::new)); return AstBeanPropertiesUtils.resolveBeanProperties(propertyElementQuery, - this, - () -> getEnclosedElements(ElementQuery.ALL_METHODS.onlyInstance()), - () -> getEnclosedElements(ElementQuery.ALL_FIELDS), - true, - nativeProps, - methodElement -> Optional.empty(), - methodElement -> Optional.empty(), - value -> mapPropertyElement(nativeProps, value, propertyElementQuery, false)); + this, + () -> getEnclosedElements(ElementQuery.ALL_METHODS.onlyInstance()), + () -> getEnclosedElements(ElementQuery.ALL_FIELDS), + true, + nativeProps, + methodElement -> Optional.empty(), + methodElement -> Optional.empty(), + value -> mapPropertyElement(nativeProps, value, propertyElementQuery, false)); } @Override @@ -548,19 +404,19 @@ public AnnotationMetadata getAnnotationMetadata() { } if (value.field != null && value.readAccessKind != BeanProperties.AccessKind.METHOD) { String getterName = NameUtils.getterNameFor( - value.propertyName, - value.type.equals(PrimitiveElement.BOOLEAN) + value.propertyName, + value.type.equals(PrimitiveElement.BOOLEAN) ); value.getter = MethodElement.of( - this, - value.field.getDeclaringType(), - annotationMetadataProvider, - visitorContext.getAnnotationMetadataBuilder(), - value.field.getGenericType(), - value.field.getGenericType(), - getterName, - value.field.isStatic(), - value.field.isFinal() + this, + value.field.getDeclaringType(), + annotationMetadataProvider, + visitorContext.getAnnotationMetadataBuilder(), + value.field.getGenericType(), + value.field.getGenericType(), + getterName, + value.field.isStatic(), + value.field.isFinal() ); value.readAccessKind = BeanProperties.AccessKind.METHOD; } else if (nativePropertiesOnly) { @@ -569,16 +425,16 @@ public AnnotationMetadata getAnnotationMetadata() { } if (value.field != null && !value.field.isFinal() && value.writeAccessKind != BeanProperties.AccessKind.METHOD) { value.setter = MethodElement.of( - this, - value.field.getDeclaringType(), - annotationMetadataProvider, - visitorContext.getAnnotationMetadataBuilder(), - PrimitiveElement.VOID, - PrimitiveElement.VOID, - NameUtils.setterNameFor(value.propertyName), - value.field.isStatic(), - value.field.isFinal(), - ParameterElement.of(value.field.getGenericType(), value.propertyName, annotationMetadataProvider, visitorContext.getAnnotationMetadataBuilder()) + this, + value.field.getDeclaringType(), + annotationMetadataProvider, + visitorContext.getAnnotationMetadataBuilder(), + PrimitiveElement.VOID, + PrimitiveElement.VOID, + NameUtils.setterNameFor(value.propertyName), + value.field.isStatic(), + value.field.isFinal(), + ParameterElement.of(value.field.getGenericType(), value.propertyName, annotationMetadataProvider, visitorContext.getAnnotationMetadataBuilder()) ); value.writeAccessKind = BeanProperties.AccessKind.METHOD; } else if (nativePropertiesOnly) { @@ -596,17 +452,17 @@ public AnnotationMetadata getAnnotationMetadata() { value.getter = null; } GroovyPropertyElement propertyElement = new GroovyPropertyElement( - visitorContext, - this, - value.type, - value.getter, - value.setter, - value.field, - elementAnnotationMetadataFactory, - value.propertyName, - value.readAccessKind == null ? PropertyElement.AccessKind.METHOD : PropertyElement.AccessKind.valueOf(value.readAccessKind.name()), - value.writeAccessKind == null ? PropertyElement.AccessKind.METHOD : PropertyElement.AccessKind.valueOf(value.writeAccessKind.name()), - value.isExcluded); + visitorContext, + this, + value.type, + value.getter, + value.setter, + value.field, + elementAnnotationMetadataFactory, + value.propertyName, + value.readAccessKind == null ? PropertyElement.AccessKind.METHOD : PropertyElement.AccessKind.valueOf(value.readAccessKind.name()), + value.writeAccessKind == null ? PropertyElement.AccessKind.METHOD : PropertyElement.AccessKind.valueOf(value.writeAccessKind.name()), + value.isExcluded); ref.set(propertyElement); return propertyElement; } @@ -618,7 +474,7 @@ public boolean isArray() { @Override public ClassElement withArrayDimensions(int arrayDimensions) { - return new GroovyClassElement(visitorContext, classNode, elementAnnotationMetadataFactory, getGenericTypeInfo(), arrayDimensions); + return new GroovyClassElement(visitorContext, classNode, elementAnnotationMetadataFactory, resolvedTypeArguments, arrayDimensions); } @Override @@ -651,9 +507,9 @@ public PackageElement getPackage() { final PackageNode aPackage = classNode.getPackage(); if (aPackage != null) { return new GroovyPackageElement( - visitorContext, - aPackage, - elementAnnotationMetadataFactory + visitorContext, + aPackage, + elementAnnotationMetadataFactory ); } else { return PackageElement.DEFAULT_PACKAGE; @@ -727,67 +583,18 @@ public Optional getOptionalValueType() { @NonNull @Override public List getBoundGenericTypes() { - if (overrideBoundGenericTypes == null) { - overrideBoundGenericTypes = getBoundGenericTypes(classNode); - } - return overrideBoundGenericTypes; - } - - @NonNull - private List getBoundGenericTypes(ClassNode classNode) { GenericsType[] genericsTypes = classNode.getGenericsTypes(); if (genericsTypes == null) { return Collections.emptyList(); - } else { - return Arrays.stream(genericsTypes) - .map(cn -> { - if (cn.isWildcard()) { - List upperBounds; - if (cn.getUpperBounds() != null && cn.getUpperBounds().length > 0) { - upperBounds = Arrays.stream(cn.getUpperBounds()) - .map(bound -> (GroovyClassElement) toClassElement(bound)) - .collect(Collectors.toList()); - } else { - upperBounds = Collections.singletonList((GroovyClassElement) visitorContext.getClassElement(Object.class).get()); - } - List lowerBounds; - if (cn.getLowerBound() == null) { - lowerBounds = Collections.emptyList(); - } else { - lowerBounds = Collections.singletonList((GroovyClassElement) toClassElement(cn.getLowerBound())); - } - return new GroovyWildcardElement( - upperBounds, - lowerBounds, - elementAnnotationMetadataFactory - ); - } else { - return toClassElement(cn.getType()); - } - }) - .collect(Collectors.toList()); } + return Arrays.stream(genericsTypes).map(this::newClassElement).toList(); } @NonNull @Override public List getDeclaredGenericPlaceholders() { //noinspection unchecked - return (List) getBoundGenericTypes(classNode.redirect()); - } - - protected final ClassElement toClassElement(ClassNode classNode) { - return visitorContext.getElementFactory().newClassElement(classNode, elementAnnotationMetadataFactory) - .withAnnotationMetadata(AnnotationMetadata.EMPTY_METADATA); - } - - @NonNull - @Override - public ClassElement withBoundGenericTypes(@NonNull List typeArguments) { - // we can't create a new ClassNode, so we have to go this route. - GroovyClassElement copy = new GroovyClassElement(visitorContext, classNode, elementAnnotationMetadataFactory); - copy.overrideBoundGenericTypes = typeArguments; - return copy; + return (List) getBoundGenericTypes(); } private List getPropertyNodes() { @@ -823,11 +630,11 @@ protected Set getExcludedNativeElements(ElementQuery.Result re Set excluded = new HashSet<>(); for (PropertyElement excludePropertyElement : getBeanProperties()) { excludePropertyElement.getReadMethod() - .filter(m -> !m.isSynthetic()) - .ifPresent(methodElement -> excluded.add((AnnotatedNode) methodElement.getNativeType())); + .filter(m -> !m.isSynthetic()) + .ifPresent(methodElement -> excluded.add((AnnotatedNode) methodElement.getNativeType())); excludePropertyElement.getWriteMethod() - .filter(m -> !m.isSynthetic()) - .ifPresent(methodElement -> excluded.add((AnnotatedNode) methodElement.getNativeType())); + .filter(m -> !m.isSynthetic()) + .ifPresent(methodElement -> excluded.add((AnnotatedNode) methodElement.getNativeType())); excludePropertyElement.getField().ifPresent(fieldElement -> excluded.add((AnnotatedNode) fieldElement.getNativeType())); } return excluded; @@ -843,8 +650,8 @@ protected ClassNode getSuperClass(ClassNode classNode) { @Override protected Collection getInterfaces(ClassNode classNode) { return Arrays.stream(classNode.getInterfaces()) - .filter(interfaceNode -> !interfaceNode.getName().equals(GroovyObject.class.getName())) - .toList(); + .filter(interfaceNode -> !interfaceNode.getName().equals(GroovyObject.class.getName())) + .toList(); } @Override @@ -857,34 +664,34 @@ protected List getEnclosedElements(ClassNode classNode, private List getEnclosedElements(ClassNode classNode, ElementQuery.Result result, Class elementType) { if (elementType == MemberElement.class) { return Stream.concat( - getEnclosedElements(classNode, result, FieldElement.class).stream(), - getEnclosedElements(classNode, result, MethodElement.class).stream() + getEnclosedElements(classNode, result, FieldElement.class).stream(), + getEnclosedElements(classNode, result, MethodElement.class).stream() ).toList(); } else if (elementType == MethodElement.class) { return classNode.getMethods() - .stream() - .filter(methodNode -> !JUNK_METHOD_FILTER.test(methodNode) && (methodNode.getModifiers() & Opcodes.ACC_SYNTHETIC) == 0) - .map(m -> m) - .toList(); + .stream() + .filter(methodNode -> !JUNK_METHOD_FILTER.test(methodNode) && (methodNode.getModifiers() & Opcodes.ACC_SYNTHETIC) == 0) + .map(m -> m) + .toList(); } else if (elementType == FieldElement.class) { return classNode.getFields().stream() - .filter(fieldNode -> (!fieldNode.isEnum() || result.isIncludeEnumConstants()) && !JUNK_FIELD_FILTER.test(fieldNode) && (fieldNode.getModifiers() & Opcodes.ACC_SYNTHETIC) == 0) - .map(m -> m) - .toList(); + .filter(fieldNode -> (!fieldNode.isEnum() || result.isIncludeEnumConstants()) && !JUNK_FIELD_FILTER.test(fieldNode) && (fieldNode.getModifiers() & Opcodes.ACC_SYNTHETIC) == 0) + .map(m -> m) + .toList(); } else if (elementType == ConstructorElement.class) { return classNode.getDeclaredConstructors() - .stream() - .filter(methodNode -> !JUNK_METHOD_FILTER.test(methodNode) && (methodNode.getModifiers() & Opcodes.ACC_SYNTHETIC) == 0) - .map(m -> m) - .toList(); + .stream() + .filter(methodNode -> !JUNK_METHOD_FILTER.test(methodNode) && (methodNode.getModifiers() & Opcodes.ACC_SYNTHETIC) == 0) + .map(m -> m) + .toList(); } else if (elementType == ClassElement.class) { return StreamSupport.stream( - Spliterators.spliteratorUnknownSize(classNode.getInnerClasses(), Spliterator.ORDERED), - false) - .filter(innerClassNode -> (innerClassNode.getModifiers() & Opcodes.ACC_SYNTHETIC) == 0) - .map(m -> m) - .toList(); + Spliterators.spliteratorUnknownSize(classNode.getInnerClasses(), Spliterator.ORDERED), + false) + .filter(innerClassNode -> (innerClassNode.getModifiers() & Opcodes.ACC_SYNTHETIC) == 0) + .map(m -> m) + .toList(); } else { throw new IllegalStateException("Unknown result type: " + elementType); } @@ -894,14 +701,14 @@ private List getEnclosedElements(ClassNode classNode, ElementQuer protected boolean excludeClass(ClassNode classNode) { String packageName = Objects.requireNonNullElse(classNode.getPackageName(), ""); if (packageName.startsWith("org.spockframework.lang") || packageName.startsWith("spock.mock") || packageName.startsWith("spock.lang")) { - // Performance optimization to exclude Spock;s deep hierarchy + // Performance optimization to exclude Spock's deep hierarchy return true; } String className = classNode.getName(); return Object.class.getName().equals(className) - || Enum.class.getName().equals(className) - || GroovyObjectSupport.class.getName().equals(className) - || Script.class.getName().equals(className); + || Enum.class.getName().equals(className) + || GroovyObjectSupport.class.getName().equals(className) + || Script.class.getName().equals(className); } @Override @@ -910,50 +717,50 @@ protected Element toAstElement(AnnotatedNode enclosedElement, Class elementTy if (isSource) { if (!(enclosedElement instanceof ConstructorNode) && enclosedElement instanceof MethodNode methodNode) { return elementFactory.newSourceMethodElement( - GroovyClassElement.this, - methodNode, - elementAnnotationMetadataFactory + GroovyClassElement.this, + methodNode, + elementAnnotationMetadataFactory ); } if (enclosedElement instanceof ClassNode cn) { return elementFactory.newSourceClassElement( - cn, - elementAnnotationMetadataFactory + cn, + elementAnnotationMetadataFactory ); } } if (enclosedElement instanceof ConstructorNode constructorNode) { return elementFactory.newConstructorElement( - GroovyClassElement.this, - constructorNode, - elementAnnotationMetadataFactory + GroovyClassElement.this, + constructorNode, + elementAnnotationMetadataFactory ); } if (enclosedElement instanceof MethodNode methodNode) { return elementFactory.newMethodElement( - GroovyClassElement.this, - methodNode, - elementAnnotationMetadataFactory + GroovyClassElement.this, + methodNode, + elementAnnotationMetadataFactory ); } if (enclosedElement instanceof FieldNode fieldNode) { if (fieldNode.isEnum()) { return elementFactory.newEnumConstantElement( - GroovyClassElement.this, - fieldNode, - elementAnnotationMetadataFactory + GroovyClassElement.this, + fieldNode, + elementAnnotationMetadataFactory ); } return elementFactory.newFieldElement( - GroovyClassElement.this, - fieldNode, - elementAnnotationMetadataFactory + GroovyClassElement.this, + fieldNode, + elementAnnotationMetadataFactory ); } if (enclosedElement instanceof ClassNode cn) { return elementFactory.newClassElement( - cn, - elementAnnotationMetadataFactory + cn, + elementAnnotationMetadataFactory ); } throw new IllegalStateException("Unknown element: " + enclosedElement); diff --git a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyElementFactory.java b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyElementFactory.java index db31cc0ecb1..a696a9692c5 100644 --- a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyElementFactory.java +++ b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyElementFactory.java @@ -16,12 +16,13 @@ package io.micronaut.ast.groovy.visitor; import io.micronaut.core.annotation.NonNull; +import io.micronaut.core.util.CollectionUtils; import io.micronaut.inject.ast.ClassElement; import io.micronaut.inject.ast.ConstructorElement; -import io.micronaut.inject.ast.annotation.ElementAnnotationMetadataFactory; import io.micronaut.inject.ast.ElementFactory; import io.micronaut.inject.ast.EnumConstantElement; import io.micronaut.inject.ast.PrimitiveElement; +import io.micronaut.inject.ast.annotation.ElementAnnotationMetadataFactory; import io.micronaut.inject.ast.beans.BeanElementBuilder; import io.micronaut.inject.configuration.ConfigurationMetadataBuilder; import org.codehaus.groovy.ast.AnnotatedNode; @@ -62,11 +63,10 @@ public ClassElement newClassElement(ClassNode classNode, ElementAnnotationMetada if (classNode.isAnnotationDefinition()) { return new GroovyAnnotationElement(visitorContext, classNode, annotationMetadataFactory); } - if (classNode.isGenericsPlaceHolder()) { - return new GroovyGenericPlaceholderElement(visitorContext, classNode, annotationMetadataFactory, 0); - } else { - return new GroovyClassElement(visitorContext, classNode, annotationMetadataFactory); - } +// if (classNode.isGenericsPlaceHolder()) { +// return new GroovyGenericPlaceholderElement(visitorContext, classNode, annotationMetadataFactory, Collections.emptyMap(), 0, Collections.emptyList(),false); +// } + return new GroovyClassElement(visitorContext, classNode, annotationMetadataFactory); } @NonNull @@ -74,39 +74,10 @@ public ClassElement newClassElement(ClassNode classNode, ElementAnnotationMetada public ClassElement newClassElement(ClassNode classNode, ElementAnnotationMetadataFactory annotationMetadataFactory, Map resolvedGenerics) { - if (classNode.isArray()) { - ClassNode componentType = classNode.getComponentType(); - ClassElement componentElement = newClassElement(componentType, annotationMetadataFactory); - return componentElement.toArray(); - } - if (ClassHelper.isPrimitiveType(classNode)) { - return PrimitiveElement.valueOf(classNode.getName()); - } - if (classNode.isEnum()) { - return new GroovyEnumElement(visitorContext, classNode, annotationMetadataFactory) { - @NonNull - @Override - public Map getTypeArguments() { - if (resolvedGenerics != null) { - return resolvedGenerics; - } - return super.getTypeArguments(); - } - }; + if (CollectionUtils.isNotEmpty(resolvedGenerics)) { + return newClassElement(classNode, annotationMetadataFactory).withTypeArguments(resolvedGenerics); } - if (classNode.isAnnotationDefinition()) { - return new GroovyAnnotationElement(visitorContext, classNode, annotationMetadataFactory); - } - return new GroovyClassElement(visitorContext, classNode, annotationMetadataFactory) { - @NonNull - @Override - public Map getTypeArguments() { - if (resolvedGenerics != null) { - return resolvedGenerics; - } - return super.getTypeArguments(); - } - }; + return newClassElement(classNode, annotationMetadataFactory); } @NonNull diff --git a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyFieldElement.java b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyFieldElement.java index 126ec231817..b96b2d4380a 100644 --- a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyFieldElement.java +++ b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyFieldElement.java @@ -17,16 +17,15 @@ import io.micronaut.core.annotation.AnnotationMetadata; import io.micronaut.core.annotation.NonNull; -import io.micronaut.core.reflect.ClassUtils; import io.micronaut.inject.ast.ClassElement; -import io.micronaut.inject.ast.annotation.ElementAnnotationMetadataFactory; import io.micronaut.inject.ast.ElementModifier; import io.micronaut.inject.ast.FieldElement; -import org.codehaus.groovy.ast.ClassHelper; +import io.micronaut.inject.ast.annotation.ElementAnnotationMetadataFactory; import org.codehaus.groovy.ast.ClassNode; import org.codehaus.groovy.ast.FieldNode; import java.lang.reflect.Modifier; +import java.util.Map; import java.util.Set; /** @@ -88,21 +87,7 @@ public String toString() { @Override public ClassElement getGenericField() { - if (isPrimitive()) { - ClassNode cn = ClassHelper.make(ClassUtils.getPrimitiveType(getType().getName()).orElse(null)); - if (cn != null) { - return new GroovyClassElement(visitorContext, cn, elementAnnotationMetadataFactory) { - - @Override - public boolean isPrimitive() { - return true; - } - }; - } else { - return getGenericType(); - } - } - return new GroovyClassElement(visitorContext, (ClassNode) getGenericType().getNativeType(), elementAnnotationMetadataFactory); + return newClassElement(fieldNode.getType(), getDeclaringType().getTypeArguments()); } @Override @@ -163,7 +148,12 @@ public boolean isPackagePrivate() { @NonNull @Override public ClassElement getType() { - return visitorContext.getElementFactory().newClassElement(fieldNode.getType(), elementAnnotationMetadataFactory); + return newClassElement(fieldNode.getType()); + } + + @Override + public ClassElement getGenericType() { + return newClassElement(fieldNode.getType(), getDeclaringType().getTypeArguments()); } @Override @@ -172,6 +162,10 @@ public GroovyClassElement getDeclaringType() { if (declaringClass == null) { throw new IllegalStateException("Declaring class could not be established"); } - return (GroovyClassElement) visitorContext.getElementFactory().newClassElement(declaringClass, elementAnnotationMetadataFactory); + if (owningType.getNativeType().equals(declaringClass)) { + return owningType; + } + Map typeArguments = getOwningType().getTypeArguments(declaringClass.getName()); + return (GroovyClassElement) newClassElement(declaringClass, typeArguments); } } diff --git a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyGenericPlaceholderElement.java b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyGenericPlaceholderElement.java index 4330aff4cae..76207becb02 100644 --- a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyGenericPlaceholderElement.java +++ b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyGenericPlaceholderElement.java @@ -19,8 +19,8 @@ import io.micronaut.core.annotation.NonNull; import io.micronaut.inject.ast.ClassElement; import io.micronaut.inject.ast.Element; -import io.micronaut.inject.ast.annotation.ElementAnnotationMetadataFactory; import io.micronaut.inject.ast.GenericPlaceholderElement; +import io.micronaut.inject.ast.WildcardElement; import org.codehaus.groovy.ast.ClassNode; import java.util.Collections; @@ -38,31 +38,73 @@ @Internal final class GroovyGenericPlaceholderElement extends GroovyClassElement implements GenericPlaceholderElement { + private final GroovyClassElement mostUpper; + private final List bounds; + private final boolean rawType; + private final ClassNode placeholderClassNode; + + GroovyGenericPlaceholderElement(GroovyVisitorContext visitorContext, + ClassNode placeholderClassNode, + List bounds, + boolean rawType) { + this(visitorContext, placeholderClassNode, WildcardElement.findUpperType(bounds, Collections.emptyList()), bounds, 0, rawType); + } + GroovyGenericPlaceholderElement(GroovyVisitorContext visitorContext, - ClassNode classNode, - ElementAnnotationMetadataFactory annotationMetadataFactory, - int arrayDimensions) { - super(visitorContext, classNode, annotationMetadataFactory, null, arrayDimensions); + ClassNode placeholderClassNode, + GroovyClassElement mostUpper, + List bounds, + int arrayDimensions, + boolean rawType) { + super(visitorContext, mostUpper.classNode, mostUpper.elementAnnotationMetadataFactory, mostUpper.resolvedTypeArguments, arrayDimensions); + this.mostUpper = mostUpper; + this.bounds = bounds; + this.rawType = rawType; + this.placeholderClassNode = placeholderClassNode; + } + + @Override + public int hashCode() { + return placeholderClassNode.hashCode(); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null) { + return false; + } + if (!(o instanceof Element that)) { + return false; + } + if (that instanceof GroovyGenericPlaceholderElement placeholderElement) { + return placeholderElement.placeholderClassNode.equals(placeholderClassNode); + } + return false; + } + + @Override + public boolean isRawType() { + return rawType; } @Override protected GroovyClassElement copyConstructor() { - return new GroovyGenericPlaceholderElement(visitorContext, classNode, elementAnnotationMetadataFactory, getArrayDimensions()); + return new GroovyGenericPlaceholderElement(visitorContext, placeholderClassNode, mostUpper, bounds, getArrayDimensions(), rawType); } @NonNull @Override - public List getBounds() { - // this is a hack: .redirect() follows the entire chain of redirects, but using this approach, we can only go - // one down. - ClassNode singleRedirect = this.classNode.asGenericsType().getUpperBounds()[0]; - return Collections.singletonList(toClassElement(singleRedirect)); + public List getBounds() { + return bounds; } @NonNull @Override public String getVariableName() { - return classNode.getUnresolvedName(); + return placeholderClassNode.getUnresolvedName(); } @Override @@ -72,7 +114,7 @@ public Optional getDeclaringElement() { @Override public ClassElement withArrayDimensions(int arrayDimensions) { - return new GroovyGenericPlaceholderElement(visitorContext, classNode, elementAnnotationMetadataFactory, arrayDimensions); + return new GroovyGenericPlaceholderElement(visitorContext, placeholderClassNode, mostUpper, bounds, arrayDimensions, rawType); } @Override diff --git a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyMethodElement.java b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyMethodElement.java index 389c55ae167..1692642eec8 100644 --- a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyMethodElement.java +++ b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyMethodElement.java @@ -15,17 +15,15 @@ */ package io.micronaut.ast.groovy.visitor; -import io.micronaut.ast.groovy.utils.AstGenericUtils; import io.micronaut.core.annotation.AnnotationMetadata; import io.micronaut.core.annotation.NonNull; import io.micronaut.core.util.ArrayUtils; -import io.micronaut.core.util.CollectionUtils; import io.micronaut.inject.ast.ClassElement; -import io.micronaut.inject.ast.annotation.ElementAnnotationMetadataFactory; import io.micronaut.inject.ast.ElementModifier; import io.micronaut.inject.ast.GenericPlaceholderElement; import io.micronaut.inject.ast.MethodElement; import io.micronaut.inject.ast.ParameterElement; +import io.micronaut.inject.ast.annotation.ElementAnnotationMetadataFactory; import org.codehaus.groovy.ast.ClassNode; import org.codehaus.groovy.ast.GenericsType; import org.codehaus.groovy.ast.MethodNode; @@ -33,7 +31,6 @@ import java.util.Arrays; import java.util.Collections; -import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; @@ -50,8 +47,7 @@ public class GroovyMethodElement extends AbstractGroovyElement implements Method protected ParameterElement[] parameters; private final MethodNode methodNode; private final GroovyClassElement owningType; - private Map genericsSpec; - private ClassElement declaringElement; + private ClassElement declaringType; /** * @param owningType The owning type @@ -102,10 +98,8 @@ public ClassElement[] getThrownTypes() { final ClassNode[] exceptions = methodNode.getExceptions(); if (ArrayUtils.isNotEmpty(exceptions)) { return Arrays.stream(exceptions) - .map(cn -> getGenericElement(cn, visitorContext.getElementFactory().newClassElement( - cn, - elementAnnotationMetadataFactory - ))).toArray(ClassElement[]::new); + .map(cn -> newClassElement(cn, getDeclaringType().getTypeArguments())) + .toArray(ClassElement[]::new); } return ClassElement.ZERO_CLASS_ELEMENTS; } @@ -177,84 +171,46 @@ public MethodNode getNativeType() { @NonNull @Override public ClassElement getGenericReturnType() { - ClassNode returnType = methodNode.getReturnType(); - ClassElement rawElement = getReturnType(); - return getGenericElement(returnType, rawElement); - } - - /** - * Obtains the generic element if present otherwise returns the raw element. - * - * @param type The type - * @param rawElement The raw element - * @return The class element - */ - @NonNull - ClassElement getGenericElement(@NonNull ClassNode type, @NonNull ClassElement rawElement) { - Map genericsSpec = getGenericsSpec(); - - return getGenericElement(sourceUnit, type, rawElement, genericsSpec); - } - - /** - * Resolves the generics spec for this method. - * - * @return The generic spec - */ - @NonNull - Map getGenericsSpec() { - if (genericsSpec == null) { - Map> info = owningType.getGenericTypeInfo(); - if (CollectionUtils.isNotEmpty(info)) { - ClassNode declaringClazz = methodNode.getDeclaringClass(); - if (declaringClazz == null) { - declaringClazz = owningType.getNativeType(); - } - Map typeGenericInfo = info.get(declaringClazz.getName()); - if (CollectionUtils.isNotEmpty(typeGenericInfo)) { - genericsSpec = AstGenericUtils.createGenericsSpec(methodNode, new HashMap<>(typeGenericInfo)); - } - } - if (genericsSpec == null) { - genericsSpec = Collections.emptyMap(); - } - } - return genericsSpec; + Map parentTypeArguments = getDeclaringType().getTypeArguments(); + Map methodTypeArguments = resolveTypeArguments(methodNode, parentTypeArguments); + return newClassElement(methodNode.getReturnType(), methodTypeArguments); } @Override @NonNull public ClassElement getReturnType() { - return visitorContext.getElementFactory().newClassElement(methodNode.getReturnType(), elementAnnotationMetadataFactory); + return newClassElement(methodNode.getReturnType()); } @Override public ParameterElement[] getParameters() { Parameter[] parameters = methodNode.getParameters(); if (this.parameters == null) { - this.parameters = Arrays.stream(parameters).map(parameter -> - new GroovyParameterElement( - this, - visitorContext, - parameter, - elementAnnotationMetadataFactory - ) - ).toArray(ParameterElement[]::new); + this.parameters = Arrays.stream(parameters).map(this::newParameter).toArray(ParameterElement[]::new); } - return this.parameters; } + private GroovyParameterElement newParameter(Parameter parameter) { + return new GroovyParameterElement( + this, + visitorContext, + parameter, + elementAnnotationMetadataFactory + ); + } + @Override public ClassElement getDeclaringType() { - if (this.declaringElement == null) { - ClassNode methodDeclaringClass = methodNode.getDeclaringClass(); - if (methodDeclaringClass == null) { + if (declaringType == null) { + ClassNode declaringClassNode = methodNode.getDeclaringClass(); + if (declaringClassNode == null) { return owningType; } - this.declaringElement = visitorContext.getElementFactory().newClassElement(methodDeclaringClass, elementAnnotationMetadataFactory); + Map typeArguments = getOwningType().getTypeArguments(declaringClassNode.getName()); + declaringType = newClassElement(declaringClassNode, typeArguments); } - return this.declaringElement; + return declaringType; } @Override @@ -265,10 +221,11 @@ public GroovyClassElement getOwningType() { @Override public List getDeclaredTypeVariables() { GenericsType[] genericsTypes = methodNode.getGenericsTypes(); - return genericsTypes == null ? - Collections.emptyList() : - Arrays.stream(genericsTypes) - .map(gt -> (GenericPlaceholderElement) visitorContext.getElementFactory().newClassElement(gt.getType(), elementAnnotationMetadataFactory)) + if (genericsTypes == null) { + return Collections.emptyList(); + } + return Arrays.stream(genericsTypes) + .map(gt -> (GenericPlaceholderElement) newClassElement(gt)) .collect(Collectors.toList()); } diff --git a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyParameterElement.java b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyParameterElement.java index 2cf62497115..d8eab4c5afe 100644 --- a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyParameterElement.java +++ b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyParameterElement.java @@ -24,6 +24,8 @@ import io.micronaut.inject.ast.ParameterElement; import org.codehaus.groovy.ast.Parameter; +import java.util.Map; + /** * Implementation of {@link ParameterElement} for Groovy. * @@ -83,11 +85,12 @@ public int getArrayDimensions() { @Nullable @Override public ClassElement getGenericType() { - if (this.genericType == null) { - ClassElement type = getType(); - this.genericType = methodElement.getGenericElement(parameter.getType(), type); + if (genericType == null) { + Map parentTypeArguments = getMethodElement().getDeclaringType().getTypeArguments(); + Map methodTypeArguments = resolveTypeArguments(methodElement.getNativeType(), parentTypeArguments); + genericType = newClassElement(parameter.getType(), methodTypeArguments); } - return this.genericType; + return genericType; } @Override @@ -118,10 +121,10 @@ public GroovyMethodElement getMethodElement() { @NonNull @Override public ClassElement getType() { - if (this.typeElement == null) { - this.typeElement = visitorContext.getElementFactory().newClassElement(parameter.getType(), elementAnnotationMetadataFactory); + if (typeElement == null) { + typeElement = newClassElement(parameter.getType()); } - return this.typeElement; + return typeElement; } } diff --git a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyWildcardElement.java b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyWildcardElement.java index ea4ab5a2473..03bebb2eb1c 100644 --- a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyWildcardElement.java +++ b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyWildcardElement.java @@ -22,6 +22,7 @@ import io.micronaut.inject.ast.annotation.ElementAnnotationMetadataFactory; import io.micronaut.inject.ast.WildcardElement; +import java.util.Collection; import java.util.List; import java.util.function.Function; import java.util.stream.Collectors; @@ -34,26 +35,29 @@ */ @Internal final class GroovyWildcardElement extends GroovyClassElement implements WildcardElement { + private final GroovyClassElement upperType; private final List upperBounds; private final List lowerBounds; - GroovyWildcardElement(@NonNull List upperBounds, + GroovyWildcardElement(@NonNull GroovyClassElement upperType, + @NonNull List upperBounds, @NonNull List lowerBounds, ElementAnnotationMetadataFactory annotationMetadataFactory) { super( - upperBounds.get(0).visitorContext, - upperBounds.get(0).classNode, + upperType.visitorContext, + upperType.classNode, annotationMetadataFactory, - upperBounds.get(0).getGenericTypeInfo(), + upperType.getTypeArguments(), 0 ); + this.upperType = upperType; this.upperBounds = upperBounds; this.lowerBounds = lowerBounds; } @Override protected GroovyClassElement copyConstructor() { - return new GroovyWildcardElement(upperBounds, lowerBounds, elementAnnotationMetadataFactory); + return new GroovyWildcardElement(upperType, upperBounds, lowerBounds, elementAnnotationMetadataFactory); } @NonNull @@ -80,7 +84,7 @@ public ClassElement withArrayDimensions(int arrayDimensions) { public ClassElement foldBoundGenericTypes(@NonNull Function fold) { List upperBounds = this.upperBounds.stream().map(ele -> toGroovyClassElement(ele.foldBoundGenericTypes(fold))).collect(Collectors.toList()); List lowerBounds = this.lowerBounds.stream().map(ele -> toGroovyClassElement(ele.foldBoundGenericTypes(fold))).collect(Collectors.toList()); - return fold.apply(upperBounds.contains(null) || lowerBounds.contains(null) ? null : new GroovyWildcardElement(upperBounds, lowerBounds, elementAnnotationMetadataFactory)); + return fold.apply(upperBounds.contains(null) || lowerBounds.contains(null) ? null : new GroovyWildcardElement(upperType, upperBounds, lowerBounds, elementAnnotationMetadataFactory)); } private GroovyClassElement toGroovyClassElement(ClassElement element) { @@ -93,7 +97,7 @@ private GroovyClassElement toGroovyClassElement(ClassElement element) { return (GroovyClassElement) ((ArrayableClassElement) visitorContext.getClassElement(element.getName(), elementAnnotationMetadataFactory) .orElseThrow(() -> new UnsupportedOperationException("Cannot convert ClassElement to GroovyClassElement, class was not found on the visitor context"))) .withArrayDimensions(element.getArrayDimensions()) - .withBoundGenericTypes(element.getBoundGenericTypes()); + .withTypeArguments((Collection) element.getBoundGenericTypes()); } } } diff --git a/inject-groovy/src/test/groovy/io/micronaut/ast/groovy/visitor/GroovyReconstructionSpec.groovy b/inject-groovy/src/test/groovy/io/micronaut/ast/groovy/visitor/GroovyReconstructionSpec.groovy index b10a2dc918d..f8fe32b689a 100644 --- a/inject-groovy/src/test/groovy/io/micronaut/ast/groovy/visitor/GroovyReconstructionSpec.groovy +++ b/inject-groovy/src/test/groovy/io/micronaut/ast/groovy/visitor/GroovyReconstructionSpec.groovy @@ -7,7 +7,6 @@ import io.micronaut.inject.ast.ElementQuery import io.micronaut.inject.ast.GenericPlaceholderElement import io.micronaut.inject.ast.MethodElement import io.micronaut.inject.ast.WildcardElement -import spock.lang.PendingFeature import spock.lang.Unroll import java.util.stream.Collectors @@ -52,12 +51,14 @@ class GroovyReconstructionSpec extends AbstractBeanDefinitionSpec { return we.upperBounds.stream().map(GroovyReconstructionSpec::reconstructTypeSignature).collect(Collectors.joining(" & ", "? extends ", "")) } } else { - def boundTypeArguments = classElement.getBoundGenericTypes() - if (boundTypeArguments.isEmpty()) { + def typeArguments = classElement.getTypeArguments().values() + if (typeArguments.isEmpty()) { + return classElement.getSimpleName() + } else if (typeArguments.stream().allMatch { it.isRawType() }) { return classElement.getSimpleName() } else { return classElement.getSimpleName() + - boundTypeArguments.stream().map(GroovyReconstructionSpec::reconstructTypeSignature).collect(Collectors.joining(", ", "<", ">")) + typeArguments.stream().map(GroovyReconstructionSpec::reconstructTypeSignature).collect(Collectors.joining(", ", "<", ">")) } } } @@ -80,18 +81,43 @@ class Test { reconstructTypeSignature(field.genericType) == fieldType where: - fieldType << [ + fieldType << [ 'String', + 'byte[]', + 'byte[][]', 'List', 'List', 'List', + 'List', 'List', 'List', 'List', + 'List', + 'List', 'List[]>', + 'List[][]>', + 'List[][]>', 'List', 'List>', - ] + ] + } + + def 'field type is wildcard extending byte[]'() { + given: + def element = buildClassElement(""" +package example; + +import java.util.*; + +class Test { + List field; +} +""") + def field = element.getFields()[0] + + expect: + // Wildcards with arrays not supported yet + reconstructTypeSignature(field.genericType) == 'List' } @Unroll("super type is #superType") @@ -173,7 +199,7 @@ abstract class Test { decl << [ 'T', 'T extends CharSequence', - //'T extends A', + 'T extends A', 'T extends List', 'T extends List', 'T extends List', @@ -214,7 +240,6 @@ abstract class Test { ] } - @PendingFeature @Unroll("field type is #fieldType") def 'bound field type'() { given: @@ -223,12 +248,12 @@ package example; import java.util.*; -class Wrapper { - Test test; -} class Test { $fieldType field; } +class Wrapper { + Test test; +} """) def field = element.getFields()[0].genericType.getFields()[0] @@ -250,7 +275,7 @@ class Test { } @Unroll("field type is #fieldType") - def 'bound field type - bound variables not implemented'() { + def 'bound field type 2'() { given: def element = buildClassElement(""" package example; @@ -274,17 +299,16 @@ class Wrapper { fieldType | expectedType 'String' | 'String' 'List' | 'List' - 'List' | 'List' - 'List' | 'List' + 'List' | 'List' + 'List' | 'List' 'List' | 'List' 'List' | 'List' - 'List' | 'List' - 'List[]>' | 'List[]>' + 'List' | 'List' + 'List[]>' | 'List[]>' 'List' | 'List' 'List>' | 'List>' } - @PendingFeature @Unroll("field type is #fieldType") def 'bound field type to other variable'() { given: @@ -334,7 +358,9 @@ class Wrapper { Test test; } """) - def field = element.getEnclosedElement(ElementQuery.ALL_FIELDS.named(s -> s == 'test')).get() + + def get = element.getEnclosedElement(ElementQuery.ALL_FIELDS.named(s -> s == 'test')).get() + def field = get .genericType.getEnclosedElement(ElementQuery.ALL_FIELDS.named(s -> s == 'field')).get() expect: @@ -344,12 +370,12 @@ class Wrapper { fieldType | expectedType 'String' | 'String' 'List' | 'List' - 'List' | 'List' - 'List' | 'List' + 'List' | 'List' + 'List' | 'List' 'List' | 'List' 'List' | 'List' - 'List' | 'List' - 'List[]>' | 'List[]>' + 'List' | 'List' + 'List[]>' | 'List[]>' 'List' | 'List' 'List>' | 'List>' } @@ -389,7 +415,6 @@ class Sub implements Sup<$params> { 'T' | 'List' | 'Sup>' } - @PendingFeature def 'bound super type'() { given: def superElement = buildClassElement(""" @@ -401,7 +426,7 @@ class Sup<$decl> { } class Sub extends Sup<$params> { } -""").withBoundGenericTypes([ClassElement.of(String)]) +""").withTypeArguments([ClassElement.of(String)]) def interfaceElement = buildClassElement(""" package example; @@ -411,7 +436,7 @@ interface Sup<$decl> { } class Sub implements Sup<$params> { } -""").withBoundGenericTypes([ClassElement.of(String)]) +""").withTypeArguments([ClassElement.of(String)]) expect: reconstructTypeSignature(superElement.getSuperType().get()) == expected @@ -425,7 +450,7 @@ class Sub implements Sup<$params> { 'T' | 'List' | 'Sup>' } - def 'bound super type - binding not implemented'() { + def 'bound super type 2'() { given: def superElement = buildClassElement(""" package example; @@ -436,7 +461,7 @@ class Sup<$decl> { } class Sub extends Sup<$params> { } -""").withBoundGenericTypes([ClassElement.of(String)]) +""").withTypeArguments([ClassElement.of(String)]) def interfaceElement = buildClassElement(""" package example; @@ -446,7 +471,7 @@ interface Sup<$decl> { } class Sub implements Sup<$params> { } -""").withBoundGenericTypes([ClassElement.of(String)]) +""").withTypeArguments([ClassElement.of(String)]) expect: reconstructTypeSignature(superElement.getSuperType().get()) == expected @@ -455,9 +480,9 @@ class Sub implements Sup<$params> { where: decl | params | expected 'T' | 'String' | 'Sup' - 'T' | 'List' | 'Sup>' - 'T' | 'List' | 'Sup>' - 'T' | 'List' | 'Sup>' + 'T' | 'List' | 'Sup>' + 'T' | 'List' | 'Sup>' + 'T' | 'List' | 'Sup>' } @Unroll('declaration is #decl') diff --git a/inject-groovy/src/test/groovy/io/micronaut/inject/generics/GenericTypeArgumentsSpec.groovy b/inject-groovy/src/test/groovy/io/micronaut/inject/generics/GenericTypeArgumentsSpec.groovy index 7b475b47adb..76c50cb3066 100644 --- a/inject-groovy/src/test/groovy/io/micronaut/inject/generics/GenericTypeArgumentsSpec.groovy +++ b/inject-groovy/src/test/groovy/io/micronaut/inject/generics/GenericTypeArgumentsSpec.groovy @@ -333,4 +333,24 @@ class FactoryReplace { expect: definition != null } + + void "test recusive generic type parameter"() { + given: + BeanDefinition definition = buildBeanDefinition('test.TrackedSortedSet', '''\ +package test; + +import io.micronaut.inject.annotation.*; +import io.micronaut.context.annotation.*; + +@jakarta.inject.Singleton +final class TrackedSortedSet> { + public TrackedSortedSet(java.util.Collection initial) { + super(); + } +} + +''') + expect: + definition != null + } } diff --git a/inject-groovy/src/test/groovy/io/micronaut/inject/visitor/BeanIntrospectionSpec.groovy b/inject-groovy/src/test/groovy/io/micronaut/inject/visitor/BeanIntrospectionSpec.groovy index 6e8691a2c7f..4f179a3df92 100644 --- a/inject-groovy/src/test/groovy/io/micronaut/inject/visitor/BeanIntrospectionSpec.groovy +++ b/inject-groovy/src/test/groovy/io/micronaut/inject/visitor/BeanIntrospectionSpec.groovy @@ -2183,4 +2183,99 @@ class Test { introspection.beanProperties.size() == 2 } + @Issue('https://github.com/micronaut-projects/micronaut-core/issues/2059') + void "test annotation metadata doesn't cause stackoverflow"() { + BeanIntrospection introspection = buildBeanIntrospection('test.Test','''\ +package test; + +import io.micronaut.core.annotation.*; + +@Introspected +public class Test { + int num; + String str; + + @Creator + public > Test(int num, String str, Class enumClass) { + this(num, str + enumClass.getName()); + } + + public > Test(int num, String str) { + this.num = num; + this.str = str; + } +} + + +''') + expect: + introspection != null + } + + void "test annotation metadata doesn't cause stackoverflow 2"() { + def bd = buildBeanDefinition('test.SessionFactoryFactory','''\ +package test; + +import io.micronaut.aop.interceptors.Mutating + +import io.micronaut.context.annotation.Factory +import io.micronaut.context.annotation.Prototype +import org.hibernate.SessionFactory +import org.hibernate.engine.spi.SessionFactoryDelegatingImpl + +@Factory +class SessionFactoryFactory { + + @Mutating("name") + @Prototype + SessionFactory sessionFactory() { + return new SessionFactoryDelegatingImpl(null) + } +} + + +''') + expect: + bd != null + } + + @Issue("https://github.com/micronaut-projects/micronaut-core/issues/1645") + void "test recursive generics 2"() { + given: + BeanIntrospection introspection = buildBeanIntrospection('test.Test','''\ +package test; + +@io.micronaut.core.annotation.Introspected +class Test { + private T child; + public T getChild() { + return child; + } +} +class B {} + +''') + expect: + introspection != null + } + + @Issue('https://github.com/micronaut-projects/micronaut-core/issues/1607') + void "test recursive generics"() { + given: + BeanIntrospection introspection = buildBeanIntrospection('test.Test','''\ +package test; + +import io.micronaut.inject.visitor.RecursiveGenerics; + +@io.micronaut.core.annotation.Introspected +class Test extends RecursiveGenerics { + +} +''') + + expect: + introspection != null + } + + } diff --git a/inject-groovy/src/test/groovy/io/micronaut/inject/visitor/ClassElementSpec.groovy b/inject-groovy/src/test/groovy/io/micronaut/inject/visitor/ClassElementSpec.groovy index df5159f60aa..7db67895bda 100644 --- a/inject-groovy/src/test/groovy/io/micronaut/inject/visitor/ClassElementSpec.groovy +++ b/inject-groovy/src/test/groovy/io/micronaut/inject/visitor/ClassElementSpec.groovy @@ -31,6 +31,7 @@ import io.micronaut.inject.ast.PrimitiveElement import io.micronaut.inject.ast.PropertyElement import io.micronaut.inject.ast.TypedElement import spock.lang.Issue +import spock.lang.PendingFeature import spock.lang.Unroll import spock.util.environment.RestoreSystemProperties @@ -487,6 +488,7 @@ public class TestController implements java.util.function.Supplier { expect: AllElementsVisitor.VISITED_CLASS_ELEMENTS.size() == 1 AllElementsVisitor.VISITED_METHOD_ELEMENTS.size() == 1 + AllElementsVisitor.VISITED_CLASS_ELEMENTS[0].getAllTypeArguments().get(Supplier.class.name).get("T").name == String.name AllElementsVisitor.VISITED_CLASS_ELEMENTS[0].getTypeArguments(Supplier).get("T").name == String.name } @@ -681,9 +683,11 @@ class Foo {} expect: AllElementsVisitor.VISITED_CLASS_ELEMENTS.size() == 1 AllElementsVisitor.VISITED_METHOD_ELEMENTS.size() == 1 - AllElementsVisitor.VISITED_METHOD_ELEMENTS[0].returnType.name == 'java.util.List' - AllElementsVisitor.VISITED_METHOD_ELEMENTS[0].returnType.typeArguments.size() == 1 - AllElementsVisitor.VISITED_METHOD_ELEMENTS[0].returnType.typeArguments.get("E").name == 'clselem8.Foo' + + def type = AllElementsVisitor.VISITED_METHOD_ELEMENTS[0].returnType + type.name == 'java.util.List' + type.typeArguments.size() == 1 + type.typeArguments.get("E").name == 'clselem8.Foo' AllElementsVisitor.VISITED_METHOD_ELEMENTS[0].parameters.size() == 1 AllElementsVisitor.VISITED_METHOD_ELEMENTS[0].parameters[0].type.name == 'java.util.Set' AllElementsVisitor.VISITED_METHOD_ELEMENTS[0].parameters[0].type.typeArguments.get("E").name == 'clselem8.Foo' @@ -1035,4 +1039,454 @@ class Pet { returnType.hasAnnotation(Introspected) genericReturnType.hasAnnotation(Introspected) } + + @PendingFeature + void "test annotation metadata present on deep type parameters for field"() { + ClassElement ce = buildClassElement(''' +package test; +import io.micronaut.core.annotation.*; +import javax.validation.constraints.*; +import java.util.List; + +class Test { + List<@Size(min=1, max=2) List<@NotEmpty List<@NotNull String>>> deepList; +} +''') + expect: + def field = ce.getFields().find { it.name == "deepList"} + def fieldType = field.getGenericType() + + fieldType.getAnnotationMetadata().getAnnotationNames().size() == 0 + + assertListGenericArgument(fieldType, { ClassElement listArg1 -> + assert listArg1.getAnnotationMetadata().getAnnotationNames().asList() == ['javax.validation.constraints.Size$List'] + assertListGenericArgument(listArg1, { ClassElement listArg2 -> + assert listArg2.getAnnotationMetadata().getAnnotationNames().asList() == ['javax.validation.constraints.NotEmpty$List'] + assertListGenericArgument(listArg2, { ClassElement listArg3 -> + assert listArg3.getAnnotationMetadata().getAnnotationNames().asList() == ['javax.validation.constraints.NotNull$List'] + }) + }) + }) + + def level1 = fieldType.getTypeArguments()["E"] + level1.getAnnotationMetadata().getAnnotationNames().asList() == ['javax.validation.constraints.Size$List'] + def level2 = level1.getTypeArguments()["E"] + level2.getAnnotationMetadata().getAnnotationNames().asList() == ['javax.validation.constraints.NotEmpty$List'] + def level3 = level2.getTypeArguments()["E"] + level3.getAnnotationMetadata().getAnnotationNames().asList() == ['javax.validation.constraints.NotNull$List'] + } + + @PendingFeature + void "test annotation metadata present on deep type parameters for method"() { + ClassElement ce = buildClassElement(''' +package test; +import io.micronaut.core.annotation.*; +import javax.validation.constraints.*; +import java.util.List; + +class Test { + List<@Size(min=1, max=2) List<@NotEmpty List<@NotNull String>>> deepList() { + return null; + } +} +''') + expect: + def method = ce.getEnclosedElement(ElementQuery.ALL_METHODS.named("deepList")).get() + def theType = method.getGenericReturnType() + + theType.getAnnotationMetadata().getAnnotationNames().size() == 0 + + assertListGenericArgument(theType, { ClassElement listArg1 -> + assert listArg1.getAnnotationMetadata().getAnnotationNames().asList() == ['javax.validation.constraints.Size$List'] + assertListGenericArgument(listArg1, { ClassElement listArg2 -> + assert listArg2.getAnnotationMetadata().getAnnotationNames().asList() == ['javax.validation.constraints.NotEmpty$List'] + assertListGenericArgument(listArg2, { ClassElement listArg3 -> + assert listArg3.getAnnotationMetadata().getAnnotationNames().asList() == ['javax.validation.constraints.NotNull$List'] + }) + }) + }) + + def level1 = theType.getTypeArguments()["E"] + level1.getAnnotationMetadata().getAnnotationNames().asList() == ['javax.validation.constraints.Size$List'] + def level2 = level1.getTypeArguments()["E"] + level2.getAnnotationMetadata().getAnnotationNames().asList() == ['javax.validation.constraints.NotEmpty$List'] + def level3 = level2.getTypeArguments()["E"] + level3.getAnnotationMetadata().getAnnotationNames().asList() == ['javax.validation.constraints.NotNull$List'] + } + + void "test recursive generic type parameter"() { + given: + ClassElement ce = buildClassElement('''\ +package test; + +final class TrackedSortedSet> { +} + +''') + expect: + def typeArguments = ce.getTypeArguments() + typeArguments.size() == 1 + def typeArgument = typeArguments.get("T") + typeArgument.name == "java.lang.Comparable" + def nextTypeArguments = typeArgument.getTypeArguments() + def nextTypeArgument = nextTypeArguments.get("T") + nextTypeArgument.name == "java.lang.Comparable" + def nextNextTypeArguments = nextTypeArgument.getTypeArguments() + def nextNextTypeArgument = nextNextTypeArguments.get("T") + nextNextTypeArgument.name == "java.lang.Object" + } + + void "test recursive generic type parameter 2"() { + given: + ClassElement ce = buildClassElement('''\ +package test; + +final class Test { // Missing argument +} + +''') + expect: + def typeArguments = ce.getTypeArguments() + typeArguments.size() == 1 + def typeArgument = typeArguments.get("T") + typeArgument.name == "test.Test" + def nextTypeArguments = typeArgument.getTypeArguments() + def nextTypeArgument = nextTypeArguments.get("T") + nextTypeArgument.name == "test.Test" + def nextNextTypeArguments = nextTypeArgument.getTypeArguments() + def nextNextTypeArgument = nextNextTypeArguments.get("T") + nextNextTypeArgument.name == "java.lang.Object" + } + + void "test recursive generic type parameter 3"() { + given: + ClassElement ce = buildClassElement('''\ +package test; + +final class Test> { +} + +''') + expect: + def typeArguments = ce.getTypeArguments() + typeArguments.size() == 1 + def typeArgument = typeArguments.get("T") + typeArgument.name == "test.Test" + def nextTypeArguments = typeArgument.getTypeArguments() + def nextTypeArgument = nextTypeArguments.get("T") + nextTypeArgument.name == "test.Test" + def nextNextTypeArguments = nextTypeArgument.getTypeArguments() + def nextNextTypeArgument = nextNextTypeArguments.get("T") + nextNextTypeArgument.name == "java.lang.Object" + } + + void "test recursive generic type parameter 4"() { + given: + ClassElement ce = buildClassElement('''\ +package test; + +final class Test> { +} + +''') + expect: + def typeArguments = ce.getTypeArguments() + typeArguments.size() == 1 + def typeArgument = typeArguments.get("T") + typeArgument.name == "test.Test" + def nextTypeArguments = typeArgument.getTypeArguments() + def nextTypeArgument = nextTypeArguments.get("T") + nextTypeArgument.name == "test.Test" + def nextNextTypeArguments = nextTypeArgument.getTypeArguments() + def nextNextTypeArgument = nextNextTypeArguments.get("T") + nextNextTypeArgument.name == "java.lang.Object" + } + + void "test recursive generic method return"() { + given: + ClassElement ce = buildClassElement('''\ +package test; + +import org.hibernate.SessionFactory; +import org.hibernate.engine.spi.SessionFactoryDelegatingImpl; + +class MyFactory { + + SessionFactory sessionFactory() { + return new SessionFactoryDelegatingImpl(null); + } +} + +''') + expect: + def sessionFactoryMethod = ce.getEnclosedElement(ElementQuery.ALL_METHODS.named("sessionFactory")).get() + def withOptionsMethod = sessionFactoryMethod.getReturnType().getEnclosedElement(ElementQuery.ALL_METHODS.named("withOptions")).get() + def typeArguments = withOptionsMethod.getReturnType().getTypeArguments() + typeArguments.size() == 1 + def typeArgument = typeArguments.get("T") + typeArgument.name == "org.hibernate.SessionBuilder" + def nextTypeArguments = typeArgument.getTypeArguments() + def nextTypeArgument = nextTypeArguments.get("T") + nextTypeArgument.name == "org.hibernate.SessionBuilder" + def nextNextTypeArguments = nextTypeArgument.getTypeArguments() + def nextNextTypeArgument = nextNextTypeArguments.get("T") + nextNextTypeArgument.name == "java.lang.Object" + } + + void "test recursive generic method return 2"() { + given: + ClassElement ce = buildClassElement('''\ +package test; + +interface MyBuilder { + T build(); +} + +class MyBean { + + MyBuilder myBuilder() { + return null; + } + +} + +class MyFactory { + + MyBean myBean() { + return new MyBean(); + } +} + + +''') + expect: + def myBeanMethod = ce.getEnclosedElement(ElementQuery.ALL_METHODS.named("myBean")).get() + def myBuilderMethod = myBeanMethod.getReturnType().getEnclosedElement(ElementQuery.ALL_METHODS.named("myBuilder")).get() + def typeArguments = myBuilderMethod.getReturnType().getTypeArguments() + typeArguments.size() == 1 + def typeArgument = typeArguments.get("T") + typeArgument.name == "test.MyBuilder" + def nextTypeArguments = typeArgument.getTypeArguments() + def nextTypeArgument = nextTypeArguments.get("T") + nextTypeArgument.name == "test.MyBuilder" + def nextNextTypeArguments = nextTypeArgument.getTypeArguments() + def nextNextTypeArgument = nextNextTypeArguments.get("T") + nextNextTypeArgument.name == "test.MyBuilder" + def nextNextNextTypeArguments = nextNextTypeArgument.getTypeArguments() + def nextNextNextTypeArgument = nextNextNextTypeArguments.get("T") + nextNextNextTypeArgument.name == "java.lang.Object" + } + + void "test recursive generic method return 3"() { + given: + ClassElement ce = buildClassElement('''\ +package test; + +interface MyBuilder { + T build(); +} + +class MyBean { + + MyBuilder myBuilder() { + return null; + } + +} + +class MyFactory { + + MyBean myBean() { + return new MyBean(); + } +} + +''') + expect: + def myBeanMethod = ce.getEnclosedElement(ElementQuery.ALL_METHODS.named("myBean")).get() + def myBuilderMethod = myBeanMethod.getReturnType().getEnclosedElement(ElementQuery.ALL_METHODS.named("myBuilder")).get() + def typeArguments = myBuilderMethod.getReturnType().getTypeArguments() + typeArguments.size() == 1 + def typeArgument = typeArguments.get("T") + typeArgument.name == "test.MyBuilder" + def nextTypeArguments = typeArgument.getTypeArguments() + def nextTypeArgument = nextTypeArguments.get("T") + nextTypeArgument.name == "test.MyBuilder" + def nextNextTypeArguments = nextTypeArgument.getTypeArguments() + def nextNextTypeArgument = nextNextTypeArguments.get("T") + nextNextTypeArgument.name == "java.lang.Object" + } + + void "test recursive generic method return 4"() { + given: + ClassElement ce = buildClassElement('''\ +package test; + +interface MyBuilder { + T build(); +} + +class MyBean { + + MyBuilder myBuilder() { + return null; + } + +} + +class MyFactory { + + MyBean myBean() { + return new MyBean(); + } +} + +''') + expect: + def myBeanMethod = ce.getEnclosedElement(ElementQuery.ALL_METHODS.named("myBean")).get() + def myBuilderMethod = myBeanMethod.getReturnType().getEnclosedElement(ElementQuery.ALL_METHODS.named("myBuilder")).get() + def typeArguments = myBuilderMethod.getReturnType().getTypeArguments() + typeArguments.size() == 1 + def typeArgument = typeArguments.get("T") + typeArgument.name == "test.MyBuilder" + def nextTypeArguments = typeArgument.getTypeArguments() + def nextTypeArgument = nextTypeArguments.get("T") + nextTypeArgument.name == "test.MyBuilder" + def nextNextTypeArguments = nextTypeArgument.getTypeArguments() + def nextNextTypeArgument = nextNextTypeArguments.get("T") + nextNextTypeArgument.name == "java.lang.Object" + } + + void "test recursive generic method return 5"() { + given: + ClassElement ce = buildClassElement('''\ +package test; + +interface MyBuilder { + T build(); +} + +class MyBean { + + MyBuilder myBuilder() { + return null; + } + +} + +class MyFactory { + + MyBean myBean() { + return new MyBean(); + } +} + +''') + expect: + def myBeanMethod = ce.getEnclosedElement(ElementQuery.ALL_METHODS.named("myBean")).get() + def myBuilderMethod = myBeanMethod.getReturnType().getEnclosedElement(ElementQuery.ALL_METHODS.named("myBuilder")).get() + def typeArguments = myBuilderMethod.getReturnType().getTypeArguments() + typeArguments.size() == 1 + def typeArgument = typeArguments.get("T") + typeArgument.name == "test.MyBuilder" + def nextTypeArguments = typeArgument.getTypeArguments() + def nextTypeArgument = nextTypeArguments.get("T") + nextTypeArgument.name == "test.MyBuilder" + def nextNextTypeArguments = nextTypeArgument.getTypeArguments() + def nextNextTypeArgument = nextNextTypeArguments.get("T") + nextNextTypeArgument.name == "test.MyBuilder" + def nextNextNextTypeArguments = nextNextTypeArgument.getTypeArguments() + def nextNextNextTypeArgument = nextNextNextTypeArguments.get("T") + nextNextNextTypeArgument.name == "java.lang.Object" + } + + void "test recursive generic method return 6"() { + given: + ClassElement ce = buildClassElement('''\ +package test; + +interface MyBuilder { + T build(); +} + +class MyBean { + + MyBuilder myBuilder() { + return null; + } + +} + +class MyFactory { + + MyBean myBean() { + return new MyBean(); + } +} + +''') + expect: + def myBeanMethod = ce.getEnclosedElement(ElementQuery.ALL_METHODS.named("myBean")).get() + def myBuilderMethod = myBeanMethod.getReturnType().getEnclosedElement(ElementQuery.ALL_METHODS.named("myBuilder")).get() + def typeArguments = myBuilderMethod.getReturnType().getTypeArguments() + typeArguments.size() == 1 + def typeArgument = typeArguments.get("T") + typeArgument.name == "test.MyBuilder" + def nextTypeArguments = typeArgument.getTypeArguments() + def nextTypeArgument = nextTypeArguments.get("T") + nextTypeArgument.name == "test.MyBuilder" + def nextNextTypeArguments = nextTypeArgument.getTypeArguments() + def nextNextTypeArgument = nextNextTypeArguments.get("T") + nextNextTypeArgument.name == "java.lang.Object" + } + + void "test recursive generic method return 7"() { + given: + ClassElement ce = buildClassElement('''\ +package test; + +interface MyBuilder { + T build(); +} + +class MyBean { + + MyBuilder myBuilder() { + return null; + } + +} + +class MyFactory { + + MyBean myBean() { + return new MyBean(); + } +} + +''') + expect: + def myBeanMethod = ce.getEnclosedElement(ElementQuery.ALL_METHODS.named("myBean")).get() + def myBuilderMethod = myBeanMethod.getReturnType().getEnclosedElement(ElementQuery.ALL_METHODS.named("myBuilder")).get() + def typeArguments = myBuilderMethod.getReturnType().getTypeArguments() + typeArguments.size() == 1 + def typeArgument = typeArguments.get("T") + typeArgument.name == "test.MyBuilder" + def nextTypeArguments = typeArgument.getTypeArguments() + def nextTypeArgument = nextTypeArguments.get("T") + nextTypeArgument.name == "test.MyBuilder" + def nextNextTypeArguments = nextTypeArgument.getTypeArguments() + def nextNextTypeArgument = nextNextTypeArguments.get("T") + nextNextTypeArgument.name == "java.lang.Object" + } + + private void assertListGenericArgument(ClassElement type, Closure cl) { + def arg1 = type.getAllTypeArguments().get(List.class.name).get("E") + def arg2 = type.getAllTypeArguments().get(Collection.class.name).get("E") + def arg3 = type.getAllTypeArguments().get(Iterable.class.name).get("T") + cl.call(arg1) + cl.call(arg2) + cl.call(arg3) + } + } diff --git a/inject-groovy/src/test/groovy/io/micronaut/inject/visitor/MyBuilder.java b/inject-groovy/src/test/groovy/io/micronaut/inject/visitor/MyBuilder.java new file mode 100644 index 00000000000..40195331d4a --- /dev/null +++ b/inject-groovy/src/test/groovy/io/micronaut/inject/visitor/MyBuilder.java @@ -0,0 +1,4 @@ +package io.micronaut.inject.visitor; + +public interface MyBuilder { +} diff --git a/inject-groovy/src/test/groovy/io/micronaut/inject/visitor/RecursiveGenerics.java b/inject-groovy/src/test/groovy/io/micronaut/inject/visitor/RecursiveGenerics.java new file mode 100644 index 00000000000..4444633e1a6 --- /dev/null +++ b/inject-groovy/src/test/groovy/io/micronaut/inject/visitor/RecursiveGenerics.java @@ -0,0 +1,19 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.inject.visitor; + +public abstract class RecursiveGenerics> { +} diff --git a/inject-java-test/src/main/groovy/io/micronaut/annotation/processing/test/AbstractTypeElementSpec.groovy b/inject-java-test/src/main/groovy/io/micronaut/annotation/processing/test/AbstractTypeElementSpec.groovy index 59ce2579cab..802a6188940 100644 --- a/inject-java-test/src/main/groovy/io/micronaut/annotation/processing/test/AbstractTypeElementSpec.groovy +++ b/inject-java-test/src/main/groovy/io/micronaut/annotation/processing/test/AbstractTypeElementSpec.groovy @@ -571,7 +571,7 @@ class Test { def name = freeVar.variableName if (typeVarsAsDeclarations) { def bounds = freeVar.bounds - if (reconstructTypeSignature(bounds[0]) != 'Object') { + if ( reconstructTypeSignature(bounds[0]) != 'Object') { name += bounds.stream().map(AbstractTypeElementSpec::reconstructTypeSignature).collect(Collectors.joining(" & ", " extends ", "")) } } @@ -586,12 +586,13 @@ class Test { return we.upperBounds.stream().map(AbstractTypeElementSpec::reconstructTypeSignature).collect(Collectors.joining(" & ", "? extends ", "")) } } else { - def boundTypeArguments = classElement.getBoundGenericTypes() - if (boundTypeArguments.isEmpty()) { + def typeArguments = classElement.getTypeArguments().values() + if (typeArguments.isEmpty()) { + return classElement.getSimpleName() + } else if (typeArguments.stream().allMatch { it.isRawType() }) { return classElement.getSimpleName() } else { - return classElement.getSimpleName() + - boundTypeArguments.stream().map(AbstractTypeElementSpec::reconstructTypeSignature).collect(Collectors.joining(", ", "<", ">")) + return classElement.getSimpleName() + typeArguments.stream().map(AbstractTypeElementSpec::reconstructTypeSignature).collect(Collectors.joining(", ", "<", ">")) } } } diff --git a/inject-java-test/src/test/groovy/io/micronaut/inject/visitor/beans/BeanIntrospectionSpec.groovy b/inject-java-test/src/test/groovy/io/micronaut/inject/visitor/beans/BeanIntrospectionSpec.groovy index 4231e0566bb..e76222d5a02 100644 --- a/inject-java-test/src/test/groovy/io/micronaut/inject/visitor/beans/BeanIntrospectionSpec.groovy +++ b/inject-java-test/src/test/groovy/io/micronaut/inject/visitor/beans/BeanIntrospectionSpec.groovy @@ -830,7 +830,6 @@ public record Foo(int x, int y){ obj.y() == 10 } - @Requires({ jvm.isJava14Compatible() }) void "test serializing records respects json annotations"() { given: BeanIntrospection introspection = buildBeanIntrospection('json.test.Foo', ''' @@ -1903,11 +1902,11 @@ public class Test { int num; String str; + @Creator public > Test(int num, String str, Class enumClass) { this(num, str + enumClass.getName()); } - @Creator public > Test(int num, String str) { this.num = num; this.str = str; @@ -1920,14 +1919,46 @@ public class Test { introspection != null } + void "test annotation metadata present on deep type parameters"() { + BeanIntrospection introspection = buildBeanIntrospection('test.Test','''\ +package test; +import io.micronaut.core.annotation.*; +import javax.validation.constraints.*; +import java.util.List; +import java.util.Set; + +@Introspected +public class Test { + List<@Size(min=1, max=2) List<@NotEmpty List<@NotNull String>>> deepList; + List>>>>> deepList2; + + Test(List>> deepList) { this.deepList = deepList; } + List>> getDeepList() { return deepList; } + List>>>>> getDeepList2() { return deepList2; } +} +''') + expect: + introspection != null + def property = introspection.getProperty("deepList").get().asArgument() + property.getTypeParameters().length == 1 + def param1 = property.getTypeParameters()[0] + param1.getTypeParameters().length == 1 + def param2 = param1.getTypeParameters()[0] + param2.getTypeParameters().length == 1 + def param3 = param2.getTypeParameters()[0] + + property.getAnnotationMetadata().getAnnotationNames().size() == 0 + param1.getAnnotationMetadata().getAnnotationNames().size() == 1 + param1.getAnnotationMetadata().getAnnotationNames().asList() == ['javax.validation.constraints.Size$List'] + param2.getAnnotationMetadata().getAnnotationNames().size() == 1 + param2.getAnnotationMetadata().getAnnotationNames().asList() == ['javax.validation.constraints.NotEmpty$List'] + param3.getAnnotationMetadata().getAnnotationNames().size() == 1 + param3.getAnnotationMetadata().getAnnotationNames().asList() == ['javax.validation.constraints.NotNull$List'] + } + @Issue('https://github.com/micronaut-projects/micronaut-core/issues/2083') void "test class references in constructor arguments"() { given: -// TraceClassVisitor traceClassVisitor = -// new TraceClassVisitor(null, new ASMifier(), new PrintWriter(System.out)); -// new ClassReader('io.micronaut.inject.visitor.beans.TestConstructorIntrospection') -// .accept(traceClassVisitor, ClassReader.EXPAND_FRAMES) - BeanIntrospection introspection = buildBeanIntrospection('test.Test','''\ package test; import com.fasterxml.jackson.databind.annotation.JsonSerialize; @@ -1951,7 +1982,7 @@ class Test { } @Issue("https://github.com/micronaut-projects/micronaut-core/issues/1645") - void "test recusive generics 2"() { + void "test recursive generics 2"() { given: BeanIntrospection introspection = buildBeanIntrospection('test.Test','''\ package test; diff --git a/inject-java/src/main/java/io/micronaut/annotation/processing/GenericUtils.java b/inject-java/src/main/java/io/micronaut/annotation/processing/GenericUtils.java index f8495c81f0e..d3dc4954374 100644 --- a/inject-java/src/main/java/io/micronaut/annotation/processing/GenericUtils.java +++ b/inject-java/src/main/java/io/micronaut/annotation/processing/GenericUtils.java @@ -63,39 +63,6 @@ Map> buildGenericTypeArgumentInfo(DeclaredType d return buildGenericTypeArgumentInfo(element, dt, Collections.emptyMap()); } - /** - * Builds type argument information for the given type. - * - * @param element The element - * @return The type argument information - */ - public Map> buildGenericTypeArgumentElementInfo(@NonNull Element element) { - return buildGenericTypeArgumentElementInfo(element, null); - } - - /** - * Builds type argument information for the given type. - * - * @param element The element - * @param declaredType The declared type - * @return The type argument information - */ - public Map> buildGenericTypeArgumentElementInfo(@NonNull Element element, @Nullable DeclaredType declaredType) { - return buildGenericTypeArgumentInfo(element, declaredType, Collections.emptyMap()); - } - - /** - * Builds type argument information for the given type. - * - * @param element The element - * @param declaredType The declared type - * @param boundTypes The type variables - * @return The type argument information - */ - public Map> buildGenericTypeArgumentElementInfo(@NonNull Element element, @Nullable DeclaredType declaredType, Map boundTypes) { - return buildGenericTypeArgumentInfo(element, declaredType, boundTypes); - } - private Map> buildGenericTypeArgumentInfo(@NonNull Element element, @Nullable DeclaredType dt, Map boundTypes) { Map> beanTypeArguments = new LinkedHashMap<>(); @@ -143,26 +110,6 @@ public List interfaceGenericTypesFor(TypeElement element, return Collections.emptyList(); } - /** - * Return the first type argument for the given type mirror. For example for Optional<String> this will - * return {@code String}. - * - * @param type The type - * @return The first argument. - */ - protected Optional getFirstTypeArgument(TypeMirror type) { - TypeMirror typeMirror = null; - - if (type instanceof DeclaredType) { - DeclaredType declaredType = (DeclaredType) type; - List typeArguments = declaredType.getTypeArguments(); - if (CollectionUtils.isNotEmpty(typeArguments)) { - typeMirror = typeArguments.get(0); - } - } - return Optional.ofNullable(typeMirror); - } - /** * Resolve the generic type arguments for the given type mirror and bound type arguments. * @@ -239,14 +186,6 @@ private Map resolveGenericTypes(DeclaredType type, TypeEleme return resolvedParameters; } - /** - * @param mirror The {@link TypeMirror} - * @return The resolved type reference - */ - protected TypeMirror resolveTypeReference(TypeMirror mirror) { - return resolveTypeReference(mirror, Collections.emptyMap()); - } - /** * Resolve a type reference to use for the given type mirror taking into account generic type variables. * @@ -307,161 +246,6 @@ protected Map resolveBoundTypes(DeclaredType type) { return boundTypes; } - /** - * Takes a type element and the bound generic information and re-aligns for the new type. - * - * @param typeElement The type element - * @param typeArguments The type arguments - * @param genericsInfo The generic info - * @return The aligned generics - */ - public Map> alignNewGenericsInfo( - TypeElement typeElement, - List typeArguments, - Map genericsInfo) { - String typeName = typeElement.getQualifiedName().toString(); - List typeParameters = typeElement.getTypeParameters(); - Map resolved = alignNewGenericsInfo(typeParameters, typeArguments, genericsInfo); - if (!resolved.isEmpty()) { - return Collections.singletonMap( - typeName, - resolved - ); - } - return Collections.emptyMap(); - } - - /** - * Takes the bound generic information and re-aligns for the new type. - * - * @param typeParameters The type parameters - * @param typeArguments The type arguments - * @param genericsInfo The generic info - * @return The aligned generics - */ - public Map alignNewGenericsInfo( - List typeParameters, - List typeArguments, - Map genericsInfo) { - if (typeArguments.size() == typeParameters.size()) { - - Map resolved = new HashMap<>(typeArguments.size()); - Iterator i = typeArguments.iterator(); - for (TypeParameterElement typeParameter : typeParameters) { - TypeMirror typeParameterMirror = i.next(); - String variableName = typeParameter.getSimpleName().toString(); - resolveVariableForMirror(genericsInfo, resolved, variableName, typeParameterMirror); - } - return resolved; - } - return Collections.emptyMap(); - } - - private void resolveVariableForMirror( - Map genericsInfo, - Map resolved, - String variableName, - TypeMirror mirror) { - if (mirror instanceof TypeVariable) { - TypeVariable tv = (TypeVariable) mirror; - resolveTypeVariable(genericsInfo, resolved, variableName, tv); - } else { - if (mirror instanceof WildcardType) { - WildcardType wt = (WildcardType) mirror; - TypeMirror extendsBound = wt.getExtendsBound(); - if (extendsBound != null) { - resolveVariableForMirror(genericsInfo, resolved, variableName, extendsBound); - } else { - TypeMirror superBound = wt.getSuperBound(); - resolveVariableForMirror(genericsInfo, resolved, variableName, superBound); - } - } else if (mirror instanceof DeclaredType) { - DeclaredType dt = (DeclaredType) mirror; - List typeArguments = dt.getTypeArguments(); - if (CollectionUtils.isNotEmpty(typeArguments) && CollectionUtils.isNotEmpty(genericsInfo)) { - List resolvedArguments = new ArrayList<>(typeArguments.size()); - for (TypeMirror typeArgument : typeArguments) { - if (typeArgument instanceof TypeVariable) { - TypeVariable tv = (TypeVariable) typeArgument; - String name = tv.toString(); - TypeMirror bound = genericsInfo.get(name); - if (bound != null) { - resolvedArguments.add(bound); - } else { - resolvedArguments.add(typeArgument); - } - } else { - resolvedArguments.add(typeArgument); - } - } - TypeMirror[] typeMirrors = resolvedArguments.toArray(new TypeMirror[0]); - resolved.put(variableName, typeUtils.getDeclaredType((TypeElement) dt.asElement(), typeMirrors)); - } else { - resolved.put(variableName, mirror); - } - } else if (mirror instanceof ArrayType) { - resolved.put(variableName, mirror); - } - } - } - - private void resolveTypeVariable( - Map genericsInfo, - Map resolved, - String variableName, - TypeVariable variable) { - String name = variable.toString(); - TypeMirror element = genericsInfo.get(name); - if (element != null) { - if (element instanceof DeclaredType) { - DeclaredType dt = (DeclaredType) element; - List typeArguments = dt.getTypeArguments(); - for (TypeMirror typeArgument : typeArguments) { - if (typeArgument instanceof TypeVariable) { - TypeVariable tv = (TypeVariable) typeArgument; - TypeMirror upperBound = tv.getUpperBound(); - if (upperBound instanceof DeclaredType) { - resolved.put(variableName, upperBound); - break; - } - - TypeMirror lowerBound = tv.getLowerBound(); - if (lowerBound instanceof DeclaredType) { - resolved.put(variableName, lowerBound); - break; - } - } - } - - if (!resolved.containsKey(variableName)) { - resolved.put(variableName, element); - } - } else { - resolved.put(variableName, element); - } - } else { - TypeMirror upperBound = variable.getUpperBound(); - if (upperBound instanceof TypeVariable) { - resolveTypeVariable(genericsInfo, resolved, variableName, (TypeVariable) upperBound); - } else if (upperBound instanceof DeclaredType) { - resolved.put( - variableName, - upperBound - ); - } else { - TypeMirror lowerBound = variable.getLowerBound(); - if (lowerBound instanceof TypeVariable) { - resolveTypeVariable(genericsInfo, resolved, variableName, (TypeVariable) lowerBound); - } else if (lowerBound instanceof DeclaredType) { - resolved.put( - variableName, - lowerBound - ); - } - } - } - } - private void resolveGenericTypeParameter(Map resolvedParameters, String parameterName, TypeMirror mirror, Map boundTypes) { if (mirror instanceof DeclaredType) { resolvedParameters.put( @@ -500,8 +284,7 @@ private void populateTypeArguments(TypeElement typeElement, Map superArguments = dt.getTypeArguments(); @@ -542,11 +325,9 @@ private void populateTypeArguments(TypeElement typeElement, Map> typeArguments, TypeElement child) { for (TypeMirror anInterface : child.getInterfaces()) { - if (anInterface instanceof DeclaredType) { - DeclaredType declaredType = (DeclaredType) anInterface; + if (anInterface instanceof DeclaredType declaredType) { Element element = declaredType.asElement(); - if (element instanceof TypeElement) { - TypeElement te = (TypeElement) element; + if (element instanceof TypeElement te) { String name = JavaModelUtils.getClassName(te); if (!typeArguments.containsKey(name)) { Map boundTypes = typeArguments.get(JavaModelUtils.getClassName(child)); diff --git a/inject-java/src/main/java/io/micronaut/annotation/processing/JavaElementAnnotationMetadataFactory.java b/inject-java/src/main/java/io/micronaut/annotation/processing/JavaElementAnnotationMetadataFactory.java index 10c8510cf4a..09b9ab90a8f 100644 --- a/inject-java/src/main/java/io/micronaut/annotation/processing/JavaElementAnnotationMetadataFactory.java +++ b/inject-java/src/main/java/io/micronaut/annotation/processing/JavaElementAnnotationMetadataFactory.java @@ -15,7 +15,10 @@ */ package io.micronaut.annotation.processing; +import io.micronaut.annotation.processing.visitor.AbstractJavaElement; +import io.micronaut.core.annotation.AnnotationMetadata; import io.micronaut.inject.ast.annotation.AbstractElementAnnotationMetadataFactory; +import io.micronaut.inject.ast.annotation.ElementAnnotationMetadata; import io.micronaut.inject.ast.annotation.ElementAnnotationMetadataFactory; import javax.lang.model.element.AnnotationMirror; @@ -29,6 +32,9 @@ */ public final class JavaElementAnnotationMetadataFactory extends AbstractElementAnnotationMetadataFactory { + private static final ElementAnnotationMetadata EMPTY = new ElementAnnotationMetadata() { + }; + public JavaElementAnnotationMetadataFactory(boolean isReadOnly, JavaAnnotationMetadataBuilder metadataBuilder) { super(isReadOnly, metadataBuilder); } @@ -38,4 +44,27 @@ public ElementAnnotationMetadataFactory readOnly() { return new JavaElementAnnotationMetadataFactory(true, (JavaAnnotationMetadataBuilder) metadataBuilder); } + @Override + public ElementAnnotationMetadata build(io.micronaut.inject.ast.Element element) { + AbstractJavaElement javaElement = (AbstractJavaElement) element; + if (notAllowedAnnotations(javaElement)) { + return EMPTY; + } + return super.build(element); + } + + private static boolean notAllowedAnnotations(AbstractJavaElement javaElement) { + return !(javaElement.getNativeType() instanceof Element); + } + + @Override + public ElementAnnotationMetadata build(io.micronaut.inject.ast.Element element, AnnotationMetadata defaultAnnotationMetadata) { + if (defaultAnnotationMetadata == null) { + AbstractJavaElement javaElement = (AbstractJavaElement) element; + if (notAllowedAnnotations(javaElement)) { + return EMPTY; + } + } + return super.build(element, defaultAnnotationMetadata); + } } diff --git a/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/AbstractJavaElement.java b/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/AbstractJavaElement.java index 0f6b3e701f5..3a7dce472e8 100644 --- a/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/AbstractJavaElement.java +++ b/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/AbstractJavaElement.java @@ -21,10 +21,12 @@ import io.micronaut.core.annotation.AnnotationValueBuilder; import io.micronaut.core.annotation.NonNull; import io.micronaut.core.annotation.Nullable; +import io.micronaut.core.util.CollectionUtils; import io.micronaut.inject.ast.ClassElement; import io.micronaut.inject.ast.ElementModifier; import io.micronaut.inject.ast.PrimitiveElement; import io.micronaut.inject.ast.TypedElement; +import io.micronaut.inject.ast.WildcardElement; import io.micronaut.inject.ast.annotation.ElementAnnotationMetadata; import io.micronaut.inject.ast.annotation.ElementAnnotationMetadataFactory; import io.micronaut.inject.ast.annotation.ElementMutableAnnotationMetadataDelegate; @@ -35,18 +37,21 @@ import javax.lang.model.element.ElementKind; import javax.lang.model.element.Modifier; import javax.lang.model.element.TypeElement; +import javax.lang.model.element.TypeParameterElement; import javax.lang.model.type.ArrayType; import javax.lang.model.type.DeclaredType; import javax.lang.model.type.IntersectionType; import javax.lang.model.type.NoType; import javax.lang.model.type.PrimitiveType; -import javax.lang.model.type.TypeKind; import javax.lang.model.type.TypeMirror; import javax.lang.model.type.TypeVariable; import javax.lang.model.type.UnionType; import javax.lang.model.type.WildcardType; import java.lang.annotation.Annotation; +import java.util.ArrayList; import java.util.Collections; +import java.util.HashSet; +import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Optional; @@ -184,8 +189,8 @@ public io.micronaut.inject.ast.Element annotate(Annotatio public boolean isPackagePrivate() { Set modifiers = element.getModifiers(); return !(modifiers.contains(PUBLIC) - || modifiers.contains(PROTECTED) - || modifiers.contains(PRIVATE)); + || modifiers.contains(PROTECTED) + || modifiers.contains(PRIVATE)); } @Override @@ -196,9 +201,9 @@ public String getName() { @Override public Set getModifiers() { return element - .getModifiers().stream() - .map(m -> ElementModifier.valueOf(m.name())) - .collect(Collectors.toSet()); + .getModifiers().stream() + .map(m -> ElementModifier.valueOf(m.name())) + .collect(Collectors.toSet()); } @Override @@ -247,214 +252,227 @@ public String toString() { return element.toString(); } - /** - * Returns a class element with aligned generic information. - * - * @param typeMirror The type mirror - * @param visitorContext The visitor context - * @param declaredGenericInfo The declared generic info - * @return The class element - */ - protected @NonNull ClassElement parameterizedClassElement( - TypeMirror typeMirror, - JavaVisitorContext visitorContext, - Map> declaredGenericInfo) { - return mirrorToClassElement( - typeMirror, - visitorContext, - declaredGenericInfo, - true); - } - /** * Obtain the ClassElement for the given mirror. * - * @param returnType The return type - * @param visitorContext The visitor context + * @param type The type + * @param declaredElementTypeArguments The type arguments of the declaring element (method, class) * @return The class element */ - protected @NonNull ClassElement mirrorToClassElement(TypeMirror returnType, JavaVisitorContext visitorContext) { - return mirrorToClassElement(returnType, visitorContext, Collections.emptyMap(), true); - } - - /** - * Obtain the ClassElement for the given mirror. - * - * @param returnType The return type - * @param visitorContext The visitor context - * @param genericsInfo The generic information. - * @return The class element - */ - protected @NonNull ClassElement mirrorToClassElement(TypeMirror returnType, JavaVisitorContext visitorContext, Map> genericsInfo) { - return mirrorToClassElement(returnType, visitorContext, genericsInfo, true); + @NonNull + protected final ClassElement newClassElement(TypeMirror type, + Map declaredElementTypeArguments) { + return newClassElement(type, declaredElementTypeArguments, new HashSet<>(), true, false); } - /** - * Obtain the ClassElement for the given mirror. - * - * @param returnType The return type - * @param visitorContext The visitor context - * @param genericsInfo The generic information. - * @param includeTypeAnnotations Whether to include type level annotations in the metadata for the element - * @return The class element - */ - protected @NonNull ClassElement mirrorToClassElement(TypeMirror returnType, JavaVisitorContext visitorContext, Map> genericsInfo, boolean includeTypeAnnotations) { - return mirrorToClassElement(returnType, visitorContext, genericsInfo, includeTypeAnnotations, returnType instanceof TypeVariable); + @NonNull + private ClassElement newClassElement(TypeMirror type, + Map declaredTypeArguments, + Set visitedTypes, + boolean includeTypeAnnotations, + boolean isTypeVariable) { + return newClassElement(type, declaredTypeArguments, visitedTypes, includeTypeAnnotations, isTypeVariable, false, null); } - /** - * Obtain the ClassElement for the given mirror. - * - * @param returnType The return type - * @param visitorContext The visitor context - * @param genericsInfo The generic information. - * @param includeTypeAnnotations Whether to include type level annotations in the metadata for the element - * @param isTypeVariable is the type a type variable - * @return The class element - */ - protected @NonNull ClassElement mirrorToClassElement(TypeMirror returnType, - JavaVisitorContext visitorContext, - Map> genericsInfo, - boolean includeTypeAnnotations, - boolean isTypeVariable) { - if (genericsInfo == null) { - genericsInfo = Collections.emptyMap(); + @NonNull + private ClassElement newClassElement(TypeMirror type, + Map declaredTypeArguments, + Set visitedTypes, + boolean includeTypeAnnotations, + boolean isTypeVariable, + boolean isRawTypeParameter, + @Nullable + TypeParameterElement representedTypeParameter) { + if (declaredTypeArguments == null) { + declaredTypeArguments = Collections.emptyMap(); } - if (returnType instanceof NoType) { + if (type instanceof NoType) { return PrimitiveElement.VOID; } - if (returnType instanceof DeclaredType dt) { + if (type instanceof DeclaredType dt) { Element e = dt.asElement(); // Declared types can wrap other types, like primitives if (!(e.asType() instanceof DeclaredType)) { - return mirrorToClassElement(e.asType(), visitorContext, genericsInfo, includeTypeAnnotations); + return newClassElement(e.asType(), declaredTypeArguments, visitedTypes, includeTypeAnnotations, isTypeVariable); } if (e instanceof TypeElement typeElement) { - List typeArguments = dt.getTypeArguments(); - Map boundGenerics = resolveBoundGenerics(visitorContext, genericsInfo); + List typeMirrorArguments = dt.getTypeArguments(); + Map resolvedTypeArguments; + if (visitedTypes.contains(dt) || typeElement.equals(element)) { + ClassElement objectElement = visitorContext.getClassElement("java.lang.Object").get(); + List typeParameters = typeElement.getTypeParameters(); + Map resolved = CollectionUtils.newHashMap(typeMirrorArguments.size()); + for (TypeParameterElement typeParameter : typeParameters) { + String variableName = typeParameter.getSimpleName().toString(); + resolved.put(variableName, objectElement); + } + resolvedTypeArguments = resolved; + } else { + visitedTypes.add(dt); + resolvedTypeArguments = resolveTypeArguments(typeElement, typeMirrorArguments, declaredTypeArguments, visitedTypes); + } if (visitorContext.getModelUtils().resolveKind(typeElement, ElementKind.ENUM).isPresent()) { return new JavaEnumElement( - typeElement, - resolveElementAnnotationMetadataFactory(typeElement, dt, includeTypeAnnotations), - visitorContext - ); + typeElement, + elementAnnotationMetadataFactory, + visitorContext + ).withAnnotationMetadata(createAnnotationMetadata(typeElement, dt, includeTypeAnnotations)); } - genericsInfo = visitorContext.getGenericUtils().alignNewGenericsInfo( - typeElement, - typeArguments, - boundGenerics - ); return new JavaClassElement( - typeElement, - resolveElementAnnotationMetadataFactory(typeElement, dt, includeTypeAnnotations), - visitorContext, - typeArguments, - genericsInfo, - isTypeVariable - ); + typeElement, + elementAnnotationMetadataFactory, + visitorContext, + typeMirrorArguments, + resolvedTypeArguments, + 0, + isTypeVariable + ).withAnnotationMetadata(createAnnotationMetadata(typeElement, dt, includeTypeAnnotations)); } return PrimitiveElement.VOID; } - if (returnType instanceof TypeVariable tv) { - return resolveTypeVariable(visitorContext, genericsInfo, includeTypeAnnotations, tv, tv); + if (type instanceof TypeVariable tv) { + return resolveTypeVariable(declaredTypeArguments, visitedTypes, includeTypeAnnotations, tv, isRawTypeParameter); } - if (returnType instanceof ArrayType at) { + if (type instanceof ArrayType at) { TypeMirror componentType = at.getComponentType(); - ClassElement arrayType; - if (componentType instanceof TypeVariable tv && componentType.getKind() == TypeKind.TYPEVAR) { - arrayType = resolveTypeVariable(visitorContext, genericsInfo, includeTypeAnnotations, tv, at); - } else { - arrayType = mirrorToClassElement(componentType, visitorContext, genericsInfo, includeTypeAnnotations); - } - return arrayType.toArray(); + return newClassElement(componentType, declaredTypeArguments, visitedTypes, includeTypeAnnotations, isTypeVariable) + .toArray(); } - if (returnType instanceof PrimitiveType pt) { + if (type instanceof PrimitiveType pt) { return PrimitiveElement.valueOf(pt.getKind().name()); } - if (returnType instanceof WildcardType wt) { - Map> finalGenericsInfo = genericsInfo; - TypeMirror superBound = wt.getSuperBound(); - Stream lowerBounds; - if (superBound instanceof UnionType unionType) { - lowerBounds = unionType.getAlternatives().stream(); - } else { - lowerBounds = Stream.ofNullable(superBound); - } - TypeMirror extendsBound = wt.getExtendsBound(); - Stream upperBounds; - if (extendsBound instanceof IntersectionType it) { - upperBounds = it.getBounds().stream(); - } else if (extendsBound == null) { - upperBounds = Stream.of(visitorContext.getElements().getTypeElement("java.lang.Object").asType()); - } else { - upperBounds = Stream.of(extendsBound); - } - return new JavaWildcardElement( - elementAnnotationMetadataFactory, - wt, - upperBounds - .map(tm -> (JavaClassElement) mirrorToClassElement(tm, visitorContext, finalGenericsInfo, includeTypeAnnotations)) - .toList(), - lowerBounds - .map(tm -> (JavaClassElement) mirrorToClassElement(tm, visitorContext, finalGenericsInfo, includeTypeAnnotations)) - .toList() - ); + if (type instanceof WildcardType wt) { + return resolveWildcard(visitorContext, declaredTypeArguments, visitedTypes, includeTypeAnnotations, representedTypeParameter, wt); } return PrimitiveElement.VOID; } - @NonNull - private ElementAnnotationMetadataFactory resolveElementAnnotationMetadataFactory(TypeElement typeElement, DeclaredType dt, boolean includeTypeAnnotations) { - return elementAnnotationMetadataFactory.overrideForNativeType(typeElement, element -> { - AnnotationUtils annotationUtils = visitorContext - .getAnnotationUtils(); - AnnotationMetadata newAnnotationMetadata; - List annotationMirrors = dt.getAnnotationMirrors(); - if (!annotationMirrors.isEmpty()) { - newAnnotationMetadata = annotationUtils.newAnnotationBuilder().buildDeclared(typeElement, annotationMirrors, includeTypeAnnotations); - } else { - newAnnotationMetadata = includeTypeAnnotations ? annotationUtils.newAnnotationBuilder().lookupOrBuildForType(typeElement).copyAnnotationMetadata() : AnnotationMetadata.EMPTY_METADATA; + private ClassElement resolveWildcard(JavaVisitorContext visitorContext, + Map declaredTypeArguments, + Set visitedTypes, + boolean includeTypeAnnotations, + TypeParameterElement representedTypeParameter, + WildcardType wt) { + TypeMirror superBound = wt.getSuperBound(); + Stream lowerBounds; + if (superBound instanceof UnionType unionType) { + lowerBounds = unionType.getAlternatives().stream(); + } else { + lowerBounds = Stream.ofNullable(superBound); + } + TypeMirror extendsBound = wt.getExtendsBound(); + Stream upperBounds; + if (extendsBound instanceof IntersectionType it) { + upperBounds = it.getBounds().stream(); + } else if (extendsBound == null) { + upperBounds = Stream.of(visitorContext.getElements().getTypeElement("java.lang.Object").asType()); + } else { + upperBounds = Stream.of(extendsBound); + } + List upperBoundsAsElements = upperBounds + .map(tm -> newClassElement(tm, declaredTypeArguments, visitedTypes, includeTypeAnnotations, true)) + .toList(); + List lowerBoundsAsElements = lowerBounds + .map(tm -> newClassElement(tm, declaredTypeArguments, visitedTypes, includeTypeAnnotations, true)) + .toList(); + ClassElement upperType = WildcardElement.findUpperType(upperBoundsAsElements, lowerBoundsAsElements); + if (upperType.getType().getName().equals("java.lang.Object")) { + // Not bounded wildcard: + if (representedTypeParameter != null) { + ClassElement definedTypeBound = newClassElement(representedTypeParameter.asType(), declaredTypeArguments, visitedTypes, includeTypeAnnotations, true); + // Use originating parameter to extract the bound defined + if (definedTypeBound instanceof JavaGenericPlaceholderElement javaGenericPlaceholderElement) { + upperType = WildcardElement.findUpperType(javaGenericPlaceholderElement.getBounds(), Collections.emptyList()); + } + } + } + if (upperType.isPrimitive()) { + // TODO: Support primitives for wildcards (? extends byte[]) + return upperType; + } + return new JavaWildcardElement( + elementAnnotationMetadataFactory, + wt, + (JavaClassElement) upperType, + upperBoundsAsElements.stream().map(JavaClassElement.class::cast).toList(), + lowerBoundsAsElements.stream().map(JavaClassElement.class::cast).toList() + ); + } + + protected final Map resolveTypeArguments(TypeElement typeElement, + @Nullable + List typeMirrorArguments, + Map declaredElementTypeArguments, + Set visitedTypes) { + List typeParameters = typeElement.getTypeParameters(); + if (typeParameters.isEmpty()) { + return Collections.emptyMap(); + } + Map resolved = CollectionUtils.newLinkedHashMap(typeParameters.size()); + if (typeMirrorArguments != null && typeMirrorArguments.size() == typeParameters.size()) { + Iterator i = typeMirrorArguments.iterator(); + for (TypeParameterElement typeParameter : typeParameters) { + TypeMirror typeParameterMirror = i.next(); + String variableName = typeParameter.getSimpleName().toString(); + resolved.put( + variableName, + newClassElement(typeParameterMirror, declaredElementTypeArguments, visitedTypes, true, true, false, typeParameter) + ); + } + } else { + // Not null means raw type definition: "List myMethod()" + // Null value means a class definition: "class List {}" + boolean isRaw = typeMirrorArguments != null; + for (TypeParameterElement typeParameter : typeParameters) { + String variableName = typeParameter.getSimpleName().toString(); + resolved.put( + variableName, + newClassElement(typeParameter.asType(), declaredElementTypeArguments, visitedTypes, true, true, isRaw, null) + ); } - return elementAnnotationMetadataFactory.build(element, newAnnotationMetadata); - }); + } + return resolved; } - private ClassElement resolveTypeVariable(JavaVisitorContext visitorContext, - Map> genericsInfo, + private ClassElement resolveTypeVariable(Map genericsInfo, + Set visitedTypes, boolean includeTypeAnnotations, TypeVariable tv, - TypeMirror declaration) { - TypeMirror upperBound = tv.getUpperBound(); - Map boundGenerics = resolveBoundGenerics(visitorContext, genericsInfo); - - TypeMirror bound = boundGenerics.get(tv.toString()); - if (bound != null && bound != declaration) { - return mirrorToClassElement(bound, visitorContext, genericsInfo, includeTypeAnnotations, true); + boolean isRawType) { + String variableName = tv.toString(); + ClassElement b = genericsInfo.get(variableName); + if (b != null) { + if (b instanceof WildcardElement wildcardElement) { + if (wildcardElement.isBounded()) { + return wildcardElement; + } + } else { + return b; + } } + List bounds = new ArrayList<>(); + TypeMirror upperBound = tv.getUpperBound(); // type variable is still free. List boundsUnresolved = upperBound instanceof IntersectionType ? - ((IntersectionType) upperBound).getBounds() : - Collections.singletonList(upperBound); - List bounds = boundsUnresolved.stream() - .map(tm -> (JavaClassElement) mirrorToClassElement(tm, - visitorContext, - genericsInfo, - includeTypeAnnotations)) - .toList(); - return new JavaGenericPlaceholderElement(tv, bounds, elementAnnotationMetadataFactory, 0); - } - - private Map resolveBoundGenerics(JavaVisitorContext visitorContext, Map> genericsInfo) { - String declaringTypeName = null; - TypeElement typeElement = visitorContext.getModelUtils().classElementFor(element); - if (typeElement != null) { - declaringTypeName = typeElement.getQualifiedName().toString(); - } - Map boundGenerics = genericsInfo.get(declaringTypeName); - if (boundGenerics == null) { - boundGenerics = Collections.emptyMap(); + ((IntersectionType) upperBound).getBounds() : + Collections.singletonList(upperBound); + boundsUnresolved.stream() + .map(tm -> (JavaClassElement) newClassElement(tm, genericsInfo, visitedTypes, includeTypeAnnotations, true)) + .forEach(bounds::add); + return new JavaGenericPlaceholderElement(tv, bounds, elementAnnotationMetadataFactory, 0, isRawType); + } + + private AnnotationMetadata createAnnotationMetadata(TypeElement typeElement, DeclaredType dt, boolean includeTypeAnnotations) { + AnnotationUtils annotationUtils = visitorContext + .getAnnotationUtils(); + AnnotationMetadata newAnnotationMetadata; + List annotationMirrors = dt.getAnnotationMirrors(); + if (!annotationMirrors.isEmpty()) { + newAnnotationMetadata = annotationUtils.newAnnotationBuilder().buildDeclared(typeElement, annotationMirrors, includeTypeAnnotations); + } else { + newAnnotationMetadata = includeTypeAnnotations ? annotationUtils.newAnnotationBuilder().lookupOrBuildForType(typeElement).copyAnnotationMetadata() : AnnotationMetadata.EMPTY_METADATA; } - return boundGenerics; + return newAnnotationMetadata; } private boolean hasModifier(Modifier modifier) { diff --git a/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaClassElement.java b/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaClassElement.java index 0f39bb3790b..da8b74c172b 100644 --- a/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaClassElement.java +++ b/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaClassElement.java @@ -21,23 +21,22 @@ import io.micronaut.core.annotation.Creator; import io.micronaut.core.annotation.Internal; import io.micronaut.core.annotation.NonNull; +import io.micronaut.core.annotation.Nullable; import io.micronaut.core.naming.NameUtils; import io.micronaut.core.reflect.ClassUtils; -import io.micronaut.core.util.StringUtils; import io.micronaut.inject.ast.ArrayableClassElement; -import io.micronaut.inject.ast.PropertyElementQuery; import io.micronaut.inject.ast.ClassElement; import io.micronaut.inject.ast.ConstructorElement; -import io.micronaut.inject.ast.ParameterElement; -import io.micronaut.inject.ast.annotation.ElementAnnotationMetadataFactory; import io.micronaut.inject.ast.ElementQuery; import io.micronaut.inject.ast.FieldElement; import io.micronaut.inject.ast.GenericPlaceholderElement; import io.micronaut.inject.ast.MemberElement; import io.micronaut.inject.ast.MethodElement; import io.micronaut.inject.ast.PackageElement; +import io.micronaut.inject.ast.ParameterElement; import io.micronaut.inject.ast.PropertyElement; -import io.micronaut.inject.ast.WildcardElement; +import io.micronaut.inject.ast.PropertyElementQuery; +import io.micronaut.inject.ast.annotation.ElementAnnotationMetadataFactory; import io.micronaut.inject.ast.utils.AstBeanPropertiesUtils; import io.micronaut.inject.ast.utils.EnclosedElementsQuery; import io.micronaut.inject.processing.JavaModelUtils; @@ -52,7 +51,6 @@ import javax.lang.model.element.VariableElement; import javax.lang.model.type.DeclaredType; import javax.lang.model.type.TypeMirror; -import javax.lang.model.type.TypeVariable; import javax.lang.model.util.Types; import java.util.ArrayList; import java.util.Collection; @@ -84,16 +82,23 @@ public class JavaClassElement extends AbstractJavaElement implements ArrayableCl private static final String KOTLIN_METADATA = "kotlin.Metadata"; private static final String PREFIX_IS = "is"; protected final TypeElement classElement; - final List typeArguments; - private final int arrayDimensions; + protected final int arrayDimensions; private final boolean isTypeVariable; private List beanProperties; - private Map> genericTypeInfo; private String simpleName; private String name; private String packageName; + @Nullable private Map resolvedTypeArguments; + @Nullable + private Map> resolvedAllTypeArguments; + @Nullable + private ClassElement resolvedSuperType; private final JavaEnclosedElementsQuery enclosedElementsQuery = new JavaEnclosedElementsQuery(); + @Nullable + // Not null means raw type definition: "List myMethod()" + // Null value means a class definition: "class List {}" + final List typeArguments; /** * @param classElement The {@link TypeElement} @@ -102,7 +107,7 @@ public class JavaClassElement extends AbstractJavaElement implements ArrayableCl */ @Internal public JavaClassElement(TypeElement classElement, ElementAnnotationMetadataFactory annotationMetadataFactory, JavaVisitorContext visitorContext) { - this(classElement, annotationMetadataFactory, visitorContext, Collections.emptyList(), null, 0, false); + this(classElement, annotationMetadataFactory, visitorContext, null, null, 0, false); } /** @@ -110,15 +115,15 @@ public JavaClassElement(TypeElement classElement, ElementAnnotationMetadataFacto * @param annotationMetadataFactory The annotation metadata factory * @param visitorContext The visitor context * @param typeArguments The declared type arguments - * @param genericsInfo The generic type info + * @param resolvedTypeArguments The resolvedTypeArguments */ - JavaClassElement( - TypeElement classElement, - ElementAnnotationMetadataFactory annotationMetadataFactory, - JavaVisitorContext visitorContext, - List typeArguments, - Map> genericsInfo) { - this(classElement, annotationMetadataFactory, visitorContext, typeArguments, genericsInfo, 0, false); + JavaClassElement(TypeElement classElement, + ElementAnnotationMetadataFactory annotationMetadataFactory, + JavaVisitorContext visitorContext, + List typeArguments, + @Nullable + Map resolvedTypeArguments) { + this(classElement, annotationMetadataFactory, visitorContext, typeArguments, resolvedTypeArguments, 0, false); } /** @@ -126,17 +131,17 @@ public JavaClassElement(TypeElement classElement, ElementAnnotationMetadataFacto * @param annotationMetadataFactory The annotation metadata factory * @param visitorContext The visitor context * @param typeArguments The declared type arguments - * @param genericsInfo The generic type info + * @param resolvedTypeArguments The resolvedTypeArguments * @param arrayDimensions The number of array dimensions */ - JavaClassElement( - TypeElement classElement, - ElementAnnotationMetadataFactory annotationMetadataFactory, - JavaVisitorContext visitorContext, - List typeArguments, - Map> genericsInfo, - int arrayDimensions) { - this(classElement, annotationMetadataFactory, visitorContext, typeArguments, genericsInfo, arrayDimensions, false); + JavaClassElement(TypeElement classElement, + ElementAnnotationMetadataFactory annotationMetadataFactory, + JavaVisitorContext visitorContext, + List typeArguments, + @Nullable + Map resolvedTypeArguments, + int arrayDimensions) { + this(classElement, annotationMetadataFactory, visitorContext, typeArguments, resolvedTypeArguments, arrayDimensions, false); } /** @@ -144,17 +149,17 @@ public JavaClassElement(TypeElement classElement, ElementAnnotationMetadataFacto * @param annotationMetadataFactory The annotation metadata factory * @param visitorContext The visitor context * @param typeArguments The declared type arguments - * @param genericsInfo The generic type info + * @param resolvedTypeArguments The resolvedTypeArguments * @param isTypeVariable Is the class element a type variable */ - JavaClassElement( - TypeElement classElement, - ElementAnnotationMetadataFactory annotationMetadataFactory, - JavaVisitorContext visitorContext, - List typeArguments, - Map> genericsInfo, - boolean isTypeVariable) { - this(classElement, annotationMetadataFactory, visitorContext, typeArguments, genericsInfo, 0, isTypeVariable); + JavaClassElement(TypeElement classElement, + ElementAnnotationMetadataFactory annotationMetadataFactory, + JavaVisitorContext visitorContext, + List typeArguments, + @Nullable + Map resolvedTypeArguments, + boolean isTypeVariable) { + this(classElement, annotationMetadataFactory, visitorContext, typeArguments, resolvedTypeArguments, 0, isTypeVariable); } /** @@ -162,29 +167,30 @@ public JavaClassElement(TypeElement classElement, ElementAnnotationMetadataFacto * @param annotationMetadataFactory The annotation metadata factory * @param visitorContext The visitor context * @param typeArguments The declared type arguments - * @param genericsInfo The generic type info + * @param resolvedTypeArguments The resolvedTypeArguments * @param arrayDimensions The number of array dimensions * @param isTypeVariable Is the type a type variable */ JavaClassElement( - TypeElement classElement, - ElementAnnotationMetadataFactory annotationMetadataFactory, - JavaVisitorContext visitorContext, - List typeArguments, - Map> genericsInfo, - int arrayDimensions, - boolean isTypeVariable) { + TypeElement classElement, + ElementAnnotationMetadataFactory annotationMetadataFactory, + JavaVisitorContext visitorContext, + List typeArguments, + @Nullable + Map resolvedTypeArguments, + int arrayDimensions, + boolean isTypeVariable) { super(classElement, annotationMetadataFactory, visitorContext); this.classElement = classElement; this.typeArguments = typeArguments; - this.genericTypeInfo = genericsInfo; + this.resolvedTypeArguments = resolvedTypeArguments; this.arrayDimensions = arrayDimensions; this.isTypeVariable = isTypeVariable; } @Override protected JavaClassElement copyThis() { - return new JavaClassElement(classElement, elementAnnotationMetadataFactory, visitorContext, typeArguments, genericTypeInfo); + return new JavaClassElement(classElement, elementAnnotationMetadataFactory, visitorContext, typeArguments, resolvedTypeArguments); } @Override @@ -229,29 +235,6 @@ public final TypeElement getNativeTypeElement() { return classElement; } - @NonNull - @Override - public Map getTypeArguments(@NonNull String type) { - if (StringUtils.isNotEmpty(type)) { - Map> data = visitorContext.getGenericUtils().buildGenericTypeArgumentElementInfo(classElement, null, getBoundTypeMirrors()); - Map forType = data.get(type); - if (forType != null) { - Map typeArgs = new LinkedHashMap<>(forType.size()); - for (Map.Entry entry : forType.entrySet()) { - TypeMirror v = entry.getValue(); - ClassElement ce = v != null ? mirrorToClassElement(v, visitorContext, Collections.emptyMap(), visitorContext.getConfiguration().includeTypeLevelAnnotationsInGenericArguments()) : null; - if (ce == null) { - return Collections.emptyMap(); - } else { - typeArgs.put(entry.getKey(), ce); - } - } - return Collections.unmodifiableMap(typeArgs); - } - } - return Collections.emptyMap(); - } - @Override public boolean isPrimitive() { return ClassUtils.getPrimitiveType(getName()).isPresent(); @@ -259,42 +242,25 @@ public boolean isPrimitive() { @Override public Collection getInterfaces() { - final List interfaces = classElement.getInterfaces(); - if (!interfaces.isEmpty()) { - return Collections.unmodifiableList(interfaces.stream().map((mirror) -> - mirrorToClassElement(mirror, visitorContext, genericTypeInfo)).collect(Collectors.toList()) - ); - } - return Collections.emptyList(); + return classElement.getInterfaces().stream().map(mirror -> newClassElement(mirror, getTypeArguments())).toList(); } @Override public Optional getSuperType() { - final TypeMirror superclass = classElement.getSuperclass(); - if (superclass != null) { + if (resolvedSuperType == null) { + final TypeMirror superclass = classElement.getSuperclass(); + if (superclass == null) { + return Optional.empty(); + } final Element element = visitorContext.getTypes().asElement(superclass); - if (element instanceof TypeElement superElement) { - if (!Object.class.getName().equals(superElement.getQualifiedName().toString())) { - // if super type has type arguments, then build a parameterized ClassElement - if (superclass instanceof DeclaredType && !((DeclaredType) superclass).getTypeArguments().isEmpty()) { - return Optional.of( - parameterizedClassElement( - superclass, - visitorContext, - visitorContext.getGenericUtils().buildGenericTypeArgumentElementInfo(classElement, null, getBoundTypeMirrors()))); - } - return Optional.of( - new JavaClassElement( - superElement, - elementAnnotationMetadataFactory, - visitorContext - ) - ); + if (Object.class.getName().equals(superElement.getQualifiedName().toString())) { + return Optional.empty(); } + resolvedSuperType = newClassElement(superclass, getTypeArguments()); } } - return Optional.empty(); + return Optional.ofNullable(resolvedSuperType); } @Override @@ -319,23 +285,23 @@ public List getBeanProperties() { public List getBeanProperties(PropertyElementQuery propertyElementQuery) { if (isRecord()) { return AstBeanPropertiesUtils.resolveBeanProperties(propertyElementQuery, - this, - this::getRecordMethods, - this::getRecordFields, - true, - Collections.emptySet(), - methodElement -> Optional.empty(), - methodElement -> Optional.empty(), - this::mapToPropertyElement); + this, + this::getRecordMethods, + this::getRecordFields, + true, + Collections.emptySet(), + methodElement -> Optional.empty(), + methodElement -> Optional.empty(), + this::mapToPropertyElement); } Function> customReaderPropertyNameResolver = methodElement -> Optional.empty(); Function> customWriterPropertyNameResolver = methodElement -> Optional.empty(); if (isKotlinClass(getNativeTypeElement())) { Set isProperties = getEnclosedElements(ElementQuery.ALL_METHODS) - .stream() - .map(io.micronaut.inject.ast.Element::getName) - .filter(method -> method.startsWith(PREFIX_IS)) - .collect(Collectors.toSet()); + .stream() + .map(io.micronaut.inject.ast.Element::getName) + .filter(method -> method.startsWith(PREFIX_IS)) + .collect(Collectors.toSet()); if (!isProperties.isEmpty()) { customReaderPropertyNameResolver = methodElement -> { String methodName = methodElement.getSimpleName(); @@ -356,29 +322,29 @@ public List getBeanProperties(PropertyElementQuery propertyElem } } return AstBeanPropertiesUtils.resolveBeanProperties(propertyElementQuery, - this, - () -> getEnclosedElements(ElementQuery.ALL_METHODS), - () -> getEnclosedElements(ElementQuery.ALL_FIELDS), - false, - Collections.emptySet(), - customReaderPropertyNameResolver, - customWriterPropertyNameResolver, - this::mapToPropertyElement); + this, + () -> getEnclosedElements(ElementQuery.ALL_METHODS), + () -> getEnclosedElements(ElementQuery.ALL_FIELDS), + false, + Collections.emptySet(), + customReaderPropertyNameResolver, + customWriterPropertyNameResolver, + this::mapToPropertyElement); } private JavaPropertyElement mapToPropertyElement(AstBeanPropertiesUtils.BeanPropertyData value) { return new JavaPropertyElement( - JavaClassElement.this, - value.type, - value.readAccessKind == null ? null : value.getter, - value.writeAccessKind == null ? null : value.setter, - value.field, - elementAnnotationMetadataFactory, - value.propertyName, - value.readAccessKind == null ? PropertyElement.AccessKind.METHOD : PropertyElement.AccessKind.valueOf(value.readAccessKind.name()), - value.writeAccessKind == null ? PropertyElement.AccessKind.METHOD : PropertyElement.AccessKind.valueOf(value.writeAccessKind.name()), - value.isExcluded, - visitorContext); + JavaClassElement.this, + value.type, + value.readAccessKind == null ? null : value.getter, + value.writeAccessKind == null ? null : value.setter, + value.field, + elementAnnotationMetadataFactory, + value.propertyName, + value.readAccessKind == null ? PropertyElement.AccessKind.METHOD : PropertyElement.AccessKind.valueOf(value.readAccessKind.name()), + value.writeAccessKind == null ? PropertyElement.AccessKind.METHOD : PropertyElement.AccessKind.valueOf(value.writeAccessKind.name()), + value.isExcluded, + visitorContext); } private List getRecordMethods() { @@ -413,7 +379,7 @@ protected void accept(DeclaredType type, Element element, Object o) { if (element instanceof ExecutableElement) { if (recordComponents.contains(name)) { methodElements.add( - new JavaMethodElement(JavaClassElement.this, (ExecutableElement) element, elementAnnotationMetadataFactory, visitorContext) + new JavaMethodElement(JavaClassElement.this, (ExecutableElement) element, elementAnnotationMetadataFactory, visitorContext) ); } } else if (element instanceof VariableElement) { @@ -441,8 +407,8 @@ public Object visitDeclared(DeclaredType type, Object o) { List enclosedElements = element.getEnclosedElements(); for (Element enclosedElement : enclosedElements) { if ((JavaModelUtils.isRecordComponent(enclosedElement) - || enclosedElement instanceof ExecutableElement) - && enclosedElement.getKind() != ElementKind.CONSTRUCTOR) { + || enclosedElement instanceof ExecutableElement) + && enclosedElement.getKind() != ElementKind.CONSTRUCTOR) { accept(type, enclosedElement, o); } } @@ -454,7 +420,7 @@ public Object visitDeclared(DeclaredType type, Object o) { protected void accept(DeclaredType type, Element element, Object o) { if (element instanceof VariableElement) { fieldElements.add( - new JavaFieldElement(JavaClassElement.this, (VariableElement) element, elementAnnotationMetadataFactory, visitorContext) + new JavaFieldElement(JavaClassElement.this, (VariableElement) element, elementAnnotationMetadataFactory, visitorContext) ); } } @@ -497,7 +463,7 @@ public ClassElement withArrayDimensions(int arrayDimensions) { if (arrayDimensions == this.arrayDimensions) { return this; } - return new JavaClassElement(classElement, elementAnnotationMetadataFactory, visitorContext, typeArguments, getGenericTypeInfo(), arrayDimensions, false); + return new JavaClassElement(classElement, elementAnnotationMetadataFactory, visitorContext, typeArguments, resolvedTypeArguments, arrayDimensions, false); } @Override @@ -532,14 +498,13 @@ public PackageElement getPackage() { } if (enclosingElement instanceof javax.lang.model.element.PackageElement packageElement) { return new JavaPackageElement( - packageElement, - elementAnnotationMetadataFactory, - visitorContext + packageElement, + elementAnnotationMetadataFactory, + visitorContext ); } else { return PackageElement.DEFAULT_PACKAGE; } - } @Override @@ -602,14 +567,15 @@ public Optional getPrimaryConstructor() { } List constructors = getAccessibleConstructors(); Optional annotatedConstructor = constructors.stream() - .filter(c -> c.hasStereotype(AnnotationUtil.INJECT) || c.hasStereotype(Creator.class)) - .findFirst(); + .filter(c -> c.hasStereotype(AnnotationUtil.INJECT) || c.hasStereotype(Creator.class)) + .findFirst(); if (annotatedConstructor.isPresent()) { return annotatedConstructor.map(c -> c); } // with records the record constructor is always the last constructor List recordComponents = classElement.getRecordComponents(); - constructorSearch: for (ConstructorElement constructor : constructors) { + constructorSearch: + for (ConstructorElement constructor : constructors) { ParameterElement[] parameters = constructor.getParameters(); if (parameters.length == recordComponents.size()) { for (int i = 0; i < parameters.length; i++) { @@ -638,12 +604,12 @@ public List getAccessibleStaticCreators() { return staticCreators; } return visitorContext.getClassElement(getName() + "$Companion", elementAnnotationMetadataFactory) - .filter(io.micronaut.inject.ast.Element::isStatic) - .flatMap(typeElement -> typeElement.getEnclosedElements(ElementQuery.ALL_METHODS - .annotated(annotationMetadata -> annotationMetadata.hasStereotype(Creator.class))).stream().findFirst() - ) - .filter(method -> !method.isPrivate() && method.getReturnType().equals(this)) - .map(Collections::singletonList).orElse(Collections.emptyList()); + .filter(io.micronaut.inject.ast.Element::isStatic) + .flatMap(typeElement -> typeElement.getEnclosedElements(ElementQuery.ALL_METHODS + .annotated(annotationMetadata -> annotationMetadata.hasStereotype(Creator.class))).stream().findFirst() + ) + .filter(method -> !method.isPrivate() && method.getReturnType().equals(this)) + .stream().toList(); } @Override @@ -652,8 +618,8 @@ public Optional getEnclosingType() { Element enclosingElement = this.classElement.getEnclosingElement(); if (enclosingElement instanceof TypeElement typeElement) { return Optional.of(visitorContext.getElementFactory().newClassElement( - typeElement, - elementAnnotationMetadataFactory + typeElement, + elementAnnotationMetadataFactory )); } } @@ -663,180 +629,66 @@ public Optional getEnclosingType() { @NonNull @Override public List getBoundGenericTypes() { + if (typeArguments == null) { + return Collections.emptyList(); + } return typeArguments.stream() - //return getGenericTypeInfo().getOrDefault(classElement.getQualifiedName().toString(), Collections.emptyMap()).values().stream() - .map(tm -> mirrorToClassElement(tm, visitorContext, getGenericTypeInfo())) - .toList(); + .map(tm -> newClassElement(tm, getTypeArguments())) + .toList(); } @NonNull @Override public List getDeclaredGenericPlaceholders() { return classElement.getTypeParameters().stream() - // we want the *declared* variables, so we don't pass in our genericsInfo. - .map(tpe -> (GenericPlaceholderElement) mirrorToClassElement(tpe.asType(), visitorContext)) - .toList(); + // we want the *declared* variables, so we don't pass in our genericsInfo. + .map(tpe -> (GenericPlaceholderElement) newClassElement(tpe.asType(), Collections.emptyMap())) + .toList(); } @NonNull @Override public ClassElement getRawClassElement() { return visitorContext.getElementFactory().newClassElement(classElement, elementAnnotationMetadataFactory) - .withArrayDimensions(getArrayDimensions()); - } - - private TypeMirror toTypeMirror(JavaVisitorContext visitorContext, ClassElement element) { - if (element.isArray()) { - return visitorContext.getTypes().getArrayType(toTypeMirror(visitorContext, element.fromArray())); - } else if (element.isWildcard()) { - WildcardElement wildcardElement = (WildcardElement) element; - List upperBounds = wildcardElement.getUpperBounds(); - if (upperBounds.size() != 1) { - throw new UnsupportedOperationException("Multiple upper bounds not supported"); - } - TypeMirror upperBound = toTypeMirror(visitorContext, upperBounds.get(0)); - if (upperBound.toString().equals("java.lang.Object")) { - upperBound = null; - } - List lowerBounds = wildcardElement.getLowerBounds(); - if (lowerBounds.size() > 1) { - throw new UnsupportedOperationException("Multiple upper bounds not supported"); - } - TypeMirror lowerBound = lowerBounds.isEmpty() ? null : toTypeMirror(visitorContext, lowerBounds.get(0)); - return visitorContext.getTypes().getWildcardType(upperBound, lowerBound); - } else if (element.isGenericPlaceholder()) { - if (!(element instanceof JavaGenericPlaceholderElement)) { - throw new UnsupportedOperationException("Free type variable on non-java class"); - } - return ((JavaGenericPlaceholderElement) element).realTypeVariable; - } else { - if (element instanceof JavaClassElement) { - return visitorContext.getTypes().getDeclaredType( - ((JavaClassElement) element).classElement, - ((JavaClassElement) element).typeArguments.toArray(new TypeMirror[0])); - } else { - ClassElement classElement1 = visitorContext.getRequiredClassElement(element.getName(), elementAnnotationMetadataFactory); - return visitorContext.getTypes().getDeclaredType( - ((JavaClassElement) classElement1).classElement, - element.getBoundGenericTypes().stream().map(ce -> toTypeMirror(visitorContext, ce)).toArray(TypeMirror[]::new)); - } - } + .withArrayDimensions(getArrayDimensions()); } @NonNull @Override - public ClassElement withBoundGenericTypes(@NonNull List typeArguments) { - if (typeArguments.isEmpty() && this.typeArguments.isEmpty()) { + public ClassElement withTypeArguments(@NonNull Collection typeArguments) { + if (getTypeArguments().equals(typeArguments)) { return this; } - - List typeMirrors = typeArguments.stream() - .map(ce -> toTypeMirror(visitorContext, ce)) - .collect(Collectors.toList()); - return withBoundGenericTypeMirrors(typeMirrors); - } - - private ClassElement withBoundGenericTypeMirrors(@NonNull List typeMirrors) { - if (typeMirrors.equals(this.typeArguments)) { - return this; - } - - Map boundByName = new LinkedHashMap<>(); + Map boundByName = new LinkedHashMap<>(); Iterator tpes = classElement.getTypeParameters().iterator(); - Iterator args = typeMirrors.iterator(); + Iterator args = typeArguments.iterator(); while (tpes.hasNext() && args.hasNext()) { - boundByName.put(tpes.next().getSimpleName().toString(), args.next()); + ClassElement next = args.next(); + Object nativeType = next.getNativeType(); + if (nativeType instanceof Class aClass) { + next = visitorContext.getClassElement(aClass).orElse(next); + } + boundByName.put(tpes.next().getSimpleName().toString(), next); } - - Map> genericsInfo = visitorContext.getGenericUtils().buildGenericTypeArgumentElementInfo(classElement, null, boundByName); - return new JavaClassElement(classElement, elementAnnotationMetadataFactory, visitorContext, typeMirrors, genericsInfo, arrayDimensions); + return withTypeArguments(boundByName); } @Override @NonNull public Map getTypeArguments() { if (resolvedTypeArguments == null) { - resolvedTypeArguments = resolveTypeArguments(); + resolvedTypeArguments = resolveTypeArguments(classElement, typeArguments, Collections.emptyMap(), new HashSet<>()); } return resolvedTypeArguments; } - private Map resolveTypeArguments() { - List typeParameters = classElement.getTypeParameters(); - Iterator tpi = typeParameters.iterator(); - - Map map = new LinkedHashMap<>(); - while (tpi.hasNext()) { - TypeParameterElement tpe = tpi.next(); - ClassElement classElement = mirrorToClassElement(tpe.asType(), visitorContext, this.genericTypeInfo, visitorContext.getConfiguration().includeTypeLevelAnnotationsInGenericArguments()); - map.put(tpe.toString(), classElement); - } - - return Collections.unmodifiableMap(map); - } - - private Map getBoundTypeMirrors() { - List typeParameters = classElement.getTypeParameters(); - Iterator tpi = typeParameters.iterator(); - - Map map = new LinkedHashMap<>(); - while (tpi.hasNext()) { - TypeParameterElement tpe = tpi.next(); - TypeMirror t = tpe.asType(); - map.put(tpe.toString(), t); - } - - return Collections.unmodifiableMap(map); - } - @NonNull @Override public Map> getAllTypeArguments() { - Map typeArguments = getBoundTypeMirrors(); - Map> info = visitorContext.getGenericUtils() - .buildGenericTypeArgumentElementInfo( - classElement, - null, - typeArguments - ); - Map> result = new LinkedHashMap<>(info.size()); - info.forEach((name, generics) -> { - Map resolved = new LinkedHashMap<>(generics.size()); - generics.forEach((variable, mirror) -> { - final Map typeInfo = this.genericTypeInfo != null ? this.genericTypeInfo.get(getName()) : null; - TypeMirror resolvedType = mirror; - if (mirror instanceof TypeVariable && typeInfo != null) { - final TypeMirror tm = typeInfo.get(mirror.toString()); - if (tm != null) { - resolvedType = tm; - } - } - ClassElement classElement = mirrorToClassElement( - resolvedType, - visitorContext, - info, - visitorContext.getConfiguration().includeTypeLevelAnnotationsInGenericArguments(), - mirror instanceof TypeVariable - ); - resolved.put(variable, classElement); - }); - result.put(name, resolved); - }); - - if (!typeArguments.isEmpty()) { - result.put(JavaModelUtils.getClassName(this.classElement), getTypeArguments()); + if (resolvedAllTypeArguments == null) { + resolvedAllTypeArguments = ArrayableClassElement.super.getAllTypeArguments(); } - return result; - } - - /** - * @return The generic type info for this class. - */ - Map> getGenericTypeInfo() { - if (genericTypeInfo == null) { - genericTypeInfo = visitorContext.getGenericUtils().buildGenericTypeArgumentElementInfo(classElement, null, getBoundTypeMirrors()); - } - return genericTypeInfo; + return resolvedAllTypeArguments; } private final class JavaEnclosedElementsQuery extends EnclosedElementsQuery { @@ -897,7 +749,7 @@ protected List getEnclosedElements(TypeElement classNode, ElementQuery. @Override protected boolean excludeClass(TypeElement classNode) { return classNode.getQualifiedName().toString().equals(Object.class.getName()) - || classNode.getQualifiedName().toString().equals(Enum.class.getName()); + || classNode.getQualifiedName().toString().equals(Enum.class.getName()); } @Override @@ -905,29 +757,28 @@ protected io.micronaut.inject.ast.Element toAstElement(Element enclosedElement, final JavaElementFactory elementFactory = visitorContext.getElementFactory(); return switch (enclosedElement.getKind()) { case METHOD -> elementFactory.newMethodElement( - JavaClassElement.this, - (ExecutableElement) enclosedElement, - elementAnnotationMetadataFactory, - genericTypeInfo + JavaClassElement.this, + (ExecutableElement) enclosedElement, + elementAnnotationMetadataFactory ); case FIELD -> elementFactory.newFieldElement( - JavaClassElement.this, - (VariableElement) enclosedElement, - elementAnnotationMetadataFactory + JavaClassElement.this, + (VariableElement) enclosedElement, + elementAnnotationMetadataFactory ); case ENUM_CONSTANT -> elementFactory.newEnumConstantElement( - JavaClassElement.this, - (VariableElement) enclosedElement, - elementAnnotationMetadataFactory + JavaClassElement.this, + (VariableElement) enclosedElement, + elementAnnotationMetadataFactory ); case CONSTRUCTOR -> elementFactory.newConstructorElement( - JavaClassElement.this, - (ExecutableElement) enclosedElement, - elementAnnotationMetadataFactory + JavaClassElement.this, + (ExecutableElement) enclosedElement, + elementAnnotationMetadataFactory ); case CLASS, ENUM -> elementFactory.newClassElement( - (TypeElement) enclosedElement, - elementAnnotationMetadataFactory + (TypeElement) enclosedElement, + elementAnnotationMetadataFactory ); default -> throw new IllegalStateException("Unknown element: " + enclosedElement); }; diff --git a/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaElementFactory.java b/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaElementFactory.java index fdaf37ddfb2..efc83edba98 100644 --- a/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaElementFactory.java +++ b/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaElementFactory.java @@ -15,14 +15,10 @@ */ package io.micronaut.annotation.processing.visitor; -import io.micronaut.annotation.processing.JavaElementAnnotationMetadataFactory; import io.micronaut.annotation.processing.PostponeToNextRoundException; -import io.micronaut.core.annotation.AnnotationMetadata; import io.micronaut.core.annotation.NonNull; -import io.micronaut.core.annotation.Nullable; import io.micronaut.inject.ast.ClassElement; import io.micronaut.inject.ast.ElementFactory; -import io.micronaut.inject.ast.MethodElement; import io.micronaut.inject.ast.TypedElement; import io.micronaut.inject.ast.annotation.ElementAnnotationMetadataFactory; import io.micronaut.inject.ast.beans.BeanElementBuilder; @@ -53,37 +49,28 @@ public JavaElementFactory(JavaVisitorContext visitorContext) { this.visitorContext = Objects.requireNonNull(visitorContext, "Visitor context cannot be null"); } - private ElementAnnotationMetadataFactory defaultAnnotationMetadata(Object nativeType, - AnnotationMetadata annotationMetadata) { - JavaElementAnnotationMetadataFactory elementAnnotationMetadataFactory = visitorContext.getElementAnnotationMetadataFactory(); - return elementAnnotationMetadataFactory.overrideForNativeType(nativeType, element -> elementAnnotationMetadataFactory.build(element, annotationMetadata)); - } - @NonNull @Override public JavaClassElement newClassElement(@NonNull TypeElement type, @NonNull ElementAnnotationMetadataFactory annotationMetadataFactory) { ElementKind kind = type.getKind(); - switch (kind) { - case ENUM: - return new JavaEnumElement( + return switch (kind) { + case ENUM -> new JavaEnumElement( type, annotationMetadataFactory, visitorContext - ); - case ANNOTATION_TYPE: - return new JavaAnnotationElement( + ); + case ANNOTATION_TYPE -> new JavaAnnotationElement( type, annotationMetadataFactory, visitorContext - ); - default: - return new JavaClassElement( + ); + default -> new JavaClassElement( type, annotationMetadataFactory, visitorContext - ); - } + ); + }; } @NonNull @@ -91,41 +78,10 @@ public JavaClassElement newClassElement(@NonNull TypeElement type, public ClassElement newClassElement(@NonNull TypeElement type, @NonNull ElementAnnotationMetadataFactory annotationMetadataFactory, @NonNull Map resolvedGenerics) { - ElementKind kind = type.getKind(); - switch (kind) { - case ENUM: - return new JavaEnumElement( - type, - annotationMetadataFactory, - visitorContext - ) { - @NonNull - @Override - public Map getTypeArguments() { - if (resolvedGenerics != null) { - return resolvedGenerics; - } - return super.getTypeArguments(); - } - }; - case ANNOTATION_TYPE: - return new JavaAnnotationElement(type, annotationMetadataFactory, visitorContext); - default: - return new JavaClassElement( - type, - annotationMetadataFactory, - visitorContext - ) { - @NonNull - @Override - public Map getTypeArguments() { - if (resolvedGenerics != null) { - return resolvedGenerics; - } - return super.getTypeArguments(); - } - }; + if (resolvedGenerics.isEmpty()) { + return newClassElement(type, annotationMetadataFactory); } + return newClassElement(type, annotationMetadataFactory).withTypeArguments(resolvedGenerics); } @NonNull @@ -213,58 +169,6 @@ public JavaMethodElement newMethodElement(ClassElement owningType, ); } - /** - * Constructs a method element with the given generic type information. - * - * @param owningType The owning class - * @param method The method - * @param annotationMetadataFactory The annotationMetadataFactory - * @param genericTypes The generic type info - * @return The method element - */ - public JavaMethodElement newMethodElement(ClassElement owningType, - @NonNull ExecutableElement method, - @NonNull ElementAnnotationMetadataFactory annotationMetadataFactory, - @Nullable Map> genericTypes) { - validateOwningClass(owningType); - failIfPostponeIsNeeded(owningType, method); - final JavaClassElement javaDeclaringClass = (JavaClassElement) owningType; - final JavaVisitorContext javaVisitorContext = visitorContext; - - return new JavaMethodElement( - javaDeclaringClass, - method, - annotationMetadataFactory, - javaVisitorContext - ) { - @NonNull - @Override - protected JavaParameterElement newParameterElement(@NonNull MethodElement methodElement, @NonNull VariableElement variableElement) { - return new JavaParameterElement(javaDeclaringClass, methodElement, variableElement, elementAnnotationMetadataFactory, javaVisitorContext) { - @NonNull - @Override - public ClassElement getGenericType() { - if (genericTypes != null) { - return parameterizedClassElement(getNativeType().asType(), javaVisitorContext, genericTypes); - } else { - return super.getGenericType(); - } - } - }; - } - - @Override - @NonNull - public ClassElement getGenericReturnType() { - if (genericTypes != null) { - return super.returnType(genericTypes); - } else { - return super.getGenericReturnType(); - } - } - }; - } - @NonNull @Override public JavaConstructorElement newConstructorElement(ClassElement owningType, @@ -299,12 +203,12 @@ public JavaEnumConstantElement newEnumConstantElement(ClassElement owningType, @NonNull @Override - public JavaFieldElement newFieldElement(ClassElement declaringClass, + public JavaFieldElement newFieldElement(ClassElement owningType, @NonNull VariableElement field, @NonNull ElementAnnotationMetadataFactory annotationMetadataFactory) { - failIfPostponeIsNeeded(declaringClass, field); + failIfPostponeIsNeeded(owningType, field); return new JavaFieldElement( - (JavaClassElement) declaringClass, + (JavaClassElement) owningType, field, annotationMetadataFactory, visitorContext diff --git a/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaEnumElement.java b/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaEnumElement.java index 8cf73cbcb80..1cc92b4722f 100644 --- a/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaEnumElement.java +++ b/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaEnumElement.java @@ -17,9 +17,9 @@ import io.micronaut.core.annotation.Internal; import io.micronaut.inject.ast.ClassElement; -import io.micronaut.inject.ast.annotation.ElementAnnotationMetadataFactory; import io.micronaut.inject.ast.EnumConstantElement; import io.micronaut.inject.ast.EnumElement; +import io.micronaut.inject.ast.annotation.ElementAnnotationMetadataFactory; import javax.lang.model.element.Element; import javax.lang.model.element.ElementKind; @@ -65,6 +65,11 @@ class JavaEnumElement extends JavaClassElement implements EnumElement { super(classElement, annotationMetadataFactory, visitorContext, Collections.emptyList(), Collections.emptyMap(), arrayDimensions, false); } + @Override + protected JavaClassElement copyThis() { + return new JavaEnumElement(classElement, elementAnnotationMetadataFactory, visitorContext, arrayDimensions); + } + @Override public List values() { if (values != null) { @@ -107,4 +112,5 @@ private void initEnum() { public ClassElement withArrayDimensions(int arrayDimensions) { return new JavaEnumElement(classElement, elementAnnotationMetadataFactory, visitorContext, arrayDimensions); } + } diff --git a/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaFieldElement.java b/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaFieldElement.java index 9191219d823..f984dff837e 100644 --- a/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaFieldElement.java +++ b/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaFieldElement.java @@ -19,14 +19,16 @@ import io.micronaut.core.annotation.Internal; import io.micronaut.core.annotation.NonNull; import io.micronaut.inject.ast.ClassElement; -import io.micronaut.inject.ast.annotation.ElementAnnotationMetadataFactory; import io.micronaut.inject.ast.FieldElement; import io.micronaut.inject.ast.MemberElement; +import io.micronaut.inject.ast.annotation.ElementAnnotationMetadataFactory; import javax.lang.model.element.Element; import javax.lang.model.element.TypeElement; import javax.lang.model.element.VariableElement; import javax.lang.model.type.TypeMirror; +import java.util.Collections; +import java.util.Map; /** * A field element returning data from a {@link VariableElement}. @@ -85,12 +87,8 @@ public ClassElement getGenericType() { if (owningType == null) { this.genericType = getType(); } else { - this.genericType = mirrorToClassElement( - variableElement.asType(), - visitorContext, - owningType.getGenericTypeInfo(), - true - ); + ClassElement declaringType = getDeclaringType(); + this.genericType = newClassElement(variableElement.asType(), declaringType.getTypeArguments()); } } return this.genericType; @@ -115,8 +113,7 @@ public int getArrayDimensions() { @Override public ClassElement getType() { if (this.typeElement == null) { - TypeMirror returnType = variableElement.asType(); - this.typeElement = mirrorToClassElement(returnType, visitorContext); + this.typeElement = newClassElement(variableElement.asType(), Collections.emptyMap()); } return this.typeElement; } @@ -125,12 +122,14 @@ public ClassElement getType() { public ClassElement getDeclaringType() { if (resolvedDeclaringClass == null) { Element enclosingElement = variableElement.getEnclosingElement(); - if (enclosingElement instanceof TypeElement) { - TypeElement te = (TypeElement) enclosingElement; - if (owningType.getName().equals(te.getQualifiedName().toString())) { + if (enclosingElement instanceof TypeElement te) { + String typeName = te.getQualifiedName().toString(); + if (owningType.getName().equals(typeName)) { resolvedDeclaringClass = owningType; } else { - resolvedDeclaringClass = mirrorToClassElement(te.asType(), visitorContext, owningType.getGenericTypeInfo()); + TypeMirror returnType = te.asType(); + Map genericsInfo = owningType.getTypeArguments(typeName); + resolvedDeclaringClass = newClassElement(returnType, genericsInfo); } } else { return owningType; diff --git a/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaGenericPlaceholderElement.java b/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaGenericPlaceholderElement.java index e7dea2a60e7..a762911047a 100644 --- a/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaGenericPlaceholderElement.java +++ b/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaGenericPlaceholderElement.java @@ -24,6 +24,7 @@ import io.micronaut.inject.ast.annotation.MutableAnnotationMetadataDelegate; import javax.lang.model.element.TypeParameterElement; +import javax.lang.model.type.TypeMirror; import javax.lang.model.type.TypeVariable; import java.util.List; import java.util.Objects; @@ -41,22 +42,54 @@ final class JavaGenericPlaceholderElement extends JavaClassElement implements GenericPlaceholderElement { final TypeVariable realTypeVariable; private final List bounds; + private final boolean isRawType; JavaGenericPlaceholderElement(@NonNull TypeVariable realTypeVariable, @NonNull List bounds, @NonNull ElementAnnotationMetadataFactory annotationMetadataFactory, - int arrayDimensions) { + int arrayDimensions, + boolean isRawType) { super( bounds.get(0).classElement, annotationMetadataFactory, bounds.get(0).visitorContext, bounds.get(0).typeArguments, - bounds.get(0).getGenericTypeInfo(), - arrayDimensions, - true + bounds.get(0).getTypeArguments(), + arrayDimensions ); this.realTypeVariable = realTypeVariable; this.bounds = bounds; + this.isRawType = isRawType; + } + + @Override + public boolean isTypeVariable() { + return true; + } + + @Override + public boolean isRawType() { + return isRawType; + } + + @Override + public int hashCode() { + return realTypeVariable.hashCode(); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null) { + return false; + } + io.micronaut.inject.ast.Element that = (io.micronaut.inject.ast.Element) o; + if (that instanceof JavaGenericPlaceholderElement placeholderElement) { + return placeholderElement.realTypeVariable.equals(realTypeVariable); + } + return false; } @Override @@ -72,7 +105,7 @@ public Object getNativeType() { @NonNull @Override - public List getBounds() { + public List getBounds() { return bounds; } @@ -88,12 +121,13 @@ public String getVariableName() { @Override public Optional getDeclaringElement() { - return Optional.of(mirrorToClassElement(getParameterElement().getGenericElement().asType(), visitorContext, getGenericTypeInfo())); + TypeMirror returnType = getParameterElement().getGenericElement().asType(); + return Optional.of(newClassElement(returnType, getTypeArguments())); } @Override public ClassElement withArrayDimensions(int arrayDimensions) { - return new JavaGenericPlaceholderElement(realTypeVariable, bounds, elementAnnotationMetadataFactory, arrayDimensions); + return new JavaGenericPlaceholderElement(realTypeVariable, bounds, elementAnnotationMetadataFactory, arrayDimensions, isRawType); } @Override diff --git a/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaMethodElement.java b/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaMethodElement.java index 2a26ef2772c..7d6a7950535 100644 --- a/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaMethodElement.java +++ b/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaMethodElement.java @@ -21,12 +21,12 @@ import io.micronaut.core.util.ArrayUtils; import io.micronaut.core.util.CollectionUtils; import io.micronaut.inject.ast.ClassElement; -import io.micronaut.inject.ast.annotation.ElementAnnotationMetadataFactory; import io.micronaut.inject.ast.GenericPlaceholderElement; import io.micronaut.inject.ast.MemberElement; import io.micronaut.inject.ast.MethodElement; import io.micronaut.inject.ast.ParameterElement; import io.micronaut.inject.ast.PrimitiveElement; +import io.micronaut.inject.ast.annotation.ElementAnnotationMetadataFactory; import javax.lang.model.element.Element; import javax.lang.model.element.ExecutableElement; @@ -42,7 +42,6 @@ import java.util.List; import java.util.Map; import java.util.Optional; -import java.util.stream.Collectors; /** * A method element returning data from a {@link ExecutableElement}. @@ -104,9 +103,7 @@ public Optional getReceiverType() { final TypeMirror receiverType = executableElement.getReceiverType(); if (receiverType != null) { if (receiverType.getKind() != TypeKind.NONE) { - final ClassElement classElement = mirrorToClassElement(receiverType, - visitorContext, - owningType.getGenericTypeInfo()); + final ClassElement classElement = newClassElement(receiverType, getDeclaringType().getTypeArguments()); return Optional.of(classElement); } } @@ -119,11 +116,8 @@ public ClassElement[] getThrownTypes() { final List thrownTypes = executableElement.getThrownTypes(); if (!thrownTypes.isEmpty()) { return thrownTypes.stream() - .map(tm -> mirrorToClassElement( - tm, - visitorContext, - owningType.getGenericTypeInfo() - )).toArray(ClassElement[]::new); + .map(tm -> newClassElement(tm, getDeclaringType().getTypeArguments())) + .toArray(ClassElement[]::new); } return ClassElement.ZERO_CLASS_ELEMENTS; @@ -134,24 +128,6 @@ public boolean isDefault() { return executableElement.isDefault(); } - @Override - public boolean overrides(MethodElement overridden) { -// if (this.equals(overridden) || isStatic() || overridden.isStatic()) { -// return false; -// } -// if (overridden instanceof JavaMethodElement) { -// boolean overrides = visitorContext.getElements().overrides( -// executableElement, -// ((JavaMethodElement) overridden).executableElement, -// owningType.classElement -// ); -// if (overrides) { -// return true; -// } -// } - return MethodElement.super.overrides(overridden); - } - @Override public boolean hides(MemberElement hidden) { if (isStatic() && getDeclaringType().isInterface()) { @@ -163,26 +139,26 @@ public boolean hides(MemberElement hidden) { @NonNull @Override public ClassElement getGenericReturnType() { - if (this.genericReturnType == null) { - this.genericReturnType = returnType(owningType.getGenericTypeInfo()); + if (genericReturnType == null) { + genericReturnType = returnType(getDeclaringType().getTypeArguments()); } - return this.genericReturnType; + return genericReturnType; } @Override @NonNull public ClassElement getReturnType() { - if (this.returnType == null) { - this.returnType = returnType(Collections.emptyMap()); + if (returnType == null) { + returnType = returnType(Collections.emptyMap()); } - return this.returnType; + return returnType; } @Override public List getDeclaredTypeVariables() { return executableElement.getTypeParameters().stream() - .map(tpe -> (GenericPlaceholderElement) mirrorToClassElement(tpe.asType(), visitorContext)) - .collect(Collectors.toList()); + .map(tpe -> (GenericPlaceholderElement) newClassElement(tpe.asType(), Collections.emptyMap())) + .toList(); } @Override @@ -242,12 +218,13 @@ protected JavaParameterElement newParameterElement(@NonNull MethodElement method public JavaClassElement getDeclaringType() { if (resolvedDeclaringClass == null) { Element enclosingElement = executableElement.getEnclosingElement(); - if (enclosingElement instanceof TypeElement) { - TypeElement te = (TypeElement) enclosingElement; - if (owningType.getName().equals(te.getQualifiedName().toString())) { + if (enclosingElement instanceof TypeElement te) { + String typeName = te.getQualifiedName().toString(); + if (owningType.getName().equals(typeName)) { resolvedDeclaringClass = owningType; } else { - resolvedDeclaringClass = (JavaClassElement) mirrorToClassElement(te.asType(), visitorContext, owningType.getGenericTypeInfo()); + Map typeArguments = owningType.getTypeArguments(typeName); + resolvedDeclaringClass = (JavaClassElement) newClassElement(te.asType(), typeArguments); } } else { return owningType; @@ -261,13 +238,7 @@ public ClassElement getOwningType() { return owningType; } - /** - * The return type for the given info. - * - * @param info The info - * @return The return type - */ - protected ClassElement returnType(Map> info) { + private ClassElement returnType(Map genericInfo) { VariableElement varElement = CollectionUtils.last(executableElement.getParameters()); if (isSuspend(varElement)) { DeclaredType dType = (DeclaredType) varElement.asType(); @@ -279,11 +250,11 @@ protected ClassElement returnType(Map> info) { if ((tm instanceof DeclaredType dt) && sameType("kotlin.Unit", dt)) { return PrimitiveElement.VOID; } else { - return mirrorToClassElement(tm, visitorContext, info, true); + return newClassElement(tm, genericInfo); } } final TypeMirror returnType = executableElement.getReturnType(); - return mirrorToClassElement(returnType, visitorContext, info, true); + return newClassElement(returnType, genericInfo); } private static boolean sameType(String type, DeclaredType dt) { diff --git a/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaParameterElement.java b/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaParameterElement.java index 9a3f022619c..40638a35c03 100644 --- a/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaParameterElement.java +++ b/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaParameterElement.java @@ -19,13 +19,12 @@ import io.micronaut.core.annotation.Internal; import io.micronaut.core.annotation.NonNull; import io.micronaut.inject.ast.ClassElement; -import io.micronaut.inject.ast.annotation.ElementAnnotationMetadataFactory; import io.micronaut.inject.ast.MethodElement; import io.micronaut.inject.ast.ParameterElement; +import io.micronaut.inject.ast.annotation.ElementAnnotationMetadataFactory; import javax.lang.model.element.VariableElement; -import javax.lang.model.type.TypeMirror; -import java.util.Map; +import java.util.Collections; /** * Implementation of the {@link ParameterElement} interface for Java. @@ -34,7 +33,7 @@ * @since 1.0 */ @Internal -class JavaParameterElement extends AbstractJavaElement implements ParameterElement { +final class JavaParameterElement extends AbstractJavaElement implements ParameterElement { private final JavaClassElement owningType; private final MethodElement methodElement; @@ -91,8 +90,7 @@ public int getArrayDimensions() { @NonNull public ClassElement getType() { if (typeElement == null) { - TypeMirror parameterType = getNativeType().asType(); - this.typeElement = mirrorToClassElement(parameterType, visitorContext); + typeElement = newClassElement(getNativeType().asType(), Collections.emptyMap()); } return typeElement; } @@ -100,12 +98,10 @@ public ClassElement getType() { @NonNull @Override public ClassElement getGenericType() { - if (this.genericTypeElement == null) { - TypeMirror returnType = getNativeType().asType(); - Map> declaredGenericInfo = owningType.getGenericTypeInfo(); - this.genericTypeElement = parameterizedClassElement(returnType, visitorContext, declaredGenericInfo); + if (genericTypeElement == null) { + genericTypeElement = newClassElement(getNativeType().asType(), methodElement.getDeclaringType().getTypeArguments()); } - return this.genericTypeElement; + return genericTypeElement; } @Override diff --git a/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaWildcardElement.java b/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaWildcardElement.java index 375e2ada429..1178dbafeaa 100644 --- a/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaWildcardElement.java +++ b/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaWildcardElement.java @@ -19,13 +19,13 @@ import io.micronaut.core.annotation.NonNull; import io.micronaut.inject.ast.ArrayableClassElement; import io.micronaut.inject.ast.ClassElement; -import io.micronaut.inject.ast.annotation.ElementAnnotationMetadataFactory; import io.micronaut.inject.ast.WildcardElement; +import io.micronaut.inject.ast.annotation.ElementAnnotationMetadataFactory; import javax.lang.model.type.WildcardType; +import java.util.Collection; import java.util.List; import java.util.function.Function; -import java.util.stream.Collectors; /** * Implementation of {@link io.micronaut.inject.ast.WildcardElement} for Java. @@ -36,30 +36,58 @@ @Internal final class JavaWildcardElement extends JavaClassElement implements WildcardElement { private final WildcardType wildcardType; + private final JavaClassElement upperBound; private final List upperBounds; private final List lowerBounds; JavaWildcardElement(ElementAnnotationMetadataFactory elementAnnotationMetadataFactory, @NonNull WildcardType wildcardType, + @NonNull JavaClassElement mostUpper, @NonNull List upperBounds, @NonNull List lowerBounds) { super( - upperBounds.get(0).classElement, - elementAnnotationMetadataFactory, - upperBounds.get(0).visitorContext, - upperBounds.get(0).typeArguments, - upperBounds.get(0).getGenericTypeInfo() + mostUpper.classElement, + elementAnnotationMetadataFactory, + mostUpper.visitorContext, + mostUpper.typeArguments, + mostUpper.getTypeArguments() ); this.wildcardType = wildcardType; + this.upperBound = mostUpper; this.upperBounds = upperBounds; this.lowerBounds = lowerBounds; } + @Override + public boolean isTypeVariable() { + return true; + } + @Override public Object getNativeType() { return wildcardType; } + @Override + public int hashCode() { + return wildcardType.hashCode(); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null) { + return false; + } + io.micronaut.inject.ast.Element that = (io.micronaut.inject.ast.Element) o; + if (that instanceof JavaWildcardElement wildcardElement) { + return wildcardElement.wildcardType.equals(wildcardType); + } + return false; + } + @NonNull @Override public List getUpperBounds() { @@ -82,9 +110,9 @@ public ClassElement withArrayDimensions(int arrayDimensions) { @Override public ClassElement foldBoundGenericTypes(@NonNull Function fold) { - List upperBounds = this.upperBounds.stream().map(ele -> toJavaClassElement(ele.foldBoundGenericTypes(fold))).collect(Collectors.toList()); - List lowerBounds = this.lowerBounds.stream().map(ele -> toJavaClassElement(ele.foldBoundGenericTypes(fold))).collect(Collectors.toList()); - return fold.apply(upperBounds.contains(null) || lowerBounds.contains(null) ? null : new JavaWildcardElement(elementAnnotationMetadataFactory, wildcardType, upperBounds, lowerBounds)); + List upperBounds = this.upperBounds.stream().map(ele -> toJavaClassElement(ele.foldBoundGenericTypes(fold))).toList(); + List lowerBounds = this.lowerBounds.stream().map(ele -> toJavaClassElement(ele.foldBoundGenericTypes(fold))).toList(); + return fold.apply(upperBounds.contains(null) || lowerBounds.contains(null) ? null : new JavaWildcardElement(elementAnnotationMetadataFactory, wildcardType, upperBound, upperBounds, lowerBounds)); } private JavaClassElement toJavaClassElement(ClassElement element) { @@ -95,9 +123,9 @@ private JavaClassElement toJavaClassElement(ClassElement element) { throw new UnsupportedOperationException("Cannot convert wildcard / free type variable to JavaClassElement"); } else { return (JavaClassElement) ((ArrayableClassElement) visitorContext.getClassElement(element.getName(), elementAnnotationMetadataFactory) - .orElseThrow(() -> new UnsupportedOperationException("Cannot convert ClassElement to JavaClassElement, class was not found on the visitor context"))) - .withArrayDimensions(element.getArrayDimensions()) - .withBoundGenericTypes(element.getBoundGenericTypes()); + .orElseThrow(() -> new UnsupportedOperationException("Cannot convert ClassElement to JavaClassElement, class was not found on the visitor context"))) + .withArrayDimensions(element.getArrayDimensions()) + .withTypeArguments((Collection) element.getBoundGenericTypes()); } } } diff --git a/inject-java/src/test/groovy/io/micronaut/annotation/processing/visitor/JavaReconstructionSpec.groovy b/inject-java/src/test/groovy/io/micronaut/annotation/processing/visitor/JavaReconstructionSpec.groovy index e4c41a800f1..b9627a65b89 100644 --- a/inject-java/src/test/groovy/io/micronaut/annotation/processing/visitor/JavaReconstructionSpec.groovy +++ b/inject-java/src/test/groovy/io/micronaut/annotation/processing/visitor/JavaReconstructionSpec.groovy @@ -5,7 +5,7 @@ import io.micronaut.inject.ast.ClassElement import io.micronaut.inject.ast.ElementQuery import io.micronaut.inject.ast.GenericPlaceholderElement import io.micronaut.inject.ast.MethodElement -import spock.lang.PendingFeature +import io.micronaut.inject.ast.WildcardElement import spock.lang.Unroll /** @@ -34,18 +34,43 @@ class Test { where: fieldType << [ 'String', + 'byte[]', + 'byte[][]', 'List', 'List', 'List', + 'List', 'List', 'List', 'List', + 'List', + 'List', 'List[]>', + 'List[][]>', + 'List[][]>', 'List', 'List>', ] } + def 'field type is wildcard extending byte[]'() { + given: + def element = buildClassElement(""" +package example; + +import java.util.*; + +class Test { + List field; +} +""") + def field = element.getFields()[0] + + expect: + // Wildcards with arrays not supported yet + reconstructTypeSignature(field.genericType) == 'List' + } + @Unroll("super type is #superType") def 'super type'() { given: @@ -166,7 +191,6 @@ abstract class Test { ] } - @PendingFeature @Unroll("field type is #fieldType") def 'bound field type'() { given: @@ -225,17 +249,16 @@ class Test { fieldType | expectedType 'String' | 'String' 'List' | 'List' - 'List' | 'List' - 'List' | 'List' + 'List' | 'List' + 'List' | 'List' 'List' | 'List' 'List' | 'List' - 'List' | 'List' - 'List[]>' | 'List[]>' + 'List' | 'List' + 'List[]>' | 'List[]>' 'List' | 'List' 'List>' | 'List>' } - @PendingFeature @Unroll("field type is #fieldType") def 'bound field type to other variable'() { given: @@ -294,12 +317,12 @@ class Test { fieldType | expectedType 'String' | 'String' 'List' | 'List' - 'List' | 'List' - 'List' | 'List' + 'List' | 'List' + 'List' | 'List' 'List' | 'List' 'List' | 'List' - 'List' | 'List' - 'List[]>' | 'List[]>' + 'List' | 'List' + 'List[]>' | 'List[]>' 'List' | 'List' 'List>' | 'List>' } @@ -339,7 +362,6 @@ interface Sup<$decl> { 'T' | 'List' | 'Sup>' } - @PendingFeature def 'bound super type'() { given: def superElement = buildClassElement(""" @@ -351,7 +373,7 @@ class Sub extends Sup<$params> { } class Sup<$decl> { } -""").withBoundGenericTypes([ClassElement.of(String)]) +""").withTypeArguments([ClassElement.of(String)]) def interfaceElement = buildClassElement(""" package example; @@ -361,7 +383,7 @@ class Sub implements Sup<$params> { } interface Sup<$decl> { } -""").withBoundGenericTypes([ClassElement.of(String)]) +""").withTypeArguments([ClassElement.of(String)]) expect: reconstructTypeSignature(superElement.getSuperType().get()) == expected @@ -386,7 +408,7 @@ class Sub extends Sup<$params> { } class Sup<$decl> { } -""").withBoundGenericTypes([ClassElement.of(String)]) +""").withTypeArguments([ClassElement.of(String)]) def interfaceElement = buildClassElement(""" package example; @@ -396,7 +418,7 @@ class Sub implements Sup<$params> { } interface Sup<$decl> { } -""").withBoundGenericTypes([ClassElement.of(String)]) +""").withTypeArguments([ClassElement.of(String)]) expect: reconstructTypeSignature(superElement.getSuperType().get()) == expected @@ -405,9 +427,9 @@ interface Sup<$decl> { where: decl | params | expected 'T' | 'String' | 'Sup' - 'T' | 'List' | 'Sup>' - 'T' | 'List' | 'Sup>' - 'T' | 'List' | 'Sup>' + 'T' | 'List' | 'Sup>' + 'T' | 'List' | 'Sup>' + 'T' | 'List' | 'Sup>' } @Unroll('declaration is #decl') @@ -497,12 +519,72 @@ class Test { expect: rawType.boundGenericTypes.isEmpty() + rawType.typeArguments["E"].type.name == "java.lang.Object" + rawType.typeArguments["E"].isRawType() + !rawType.typeArguments["E"].isWildcard() + rawType.typeArguments["E"].isGenericPlaceholder() wildcardType.boundGenericTypes.size() == 1 wildcardType.boundGenericTypes[0].isWildcard() wildcardType.boundGenericTypes[0].getNativeType().class.name == 'com.sun.tools.javac.code.Type$WildcardType' + wildcardType.typeArguments["E"].type.name == "java.lang.Object" + wildcardType.typeArguments["E"].isWildcard() + !wildcardType.typeArguments["E"].isRawType() objectType.boundGenericTypes.size() == 1 !objectType.boundGenericTypes[0].isWildcard() + objectType.typeArguments["E"].type.name == "java.lang.Object" + !objectType.typeArguments["E"].isWildcard() + !objectType.typeArguments["E"].isRawType() + !objectType.typeArguments["E"].isGenericPlaceholder() + } + + def 'distinguish list types 2'() { + given: + def classElement = buildClassElement(""" +package example; + +import java.util.*; +import java.lang.Number; + +class Test { + List field1; + List field2; + List field3; + List field4; + List field5; +} +""") + def rawType = classElement.fields[0].type + def wildcardType = classElement.fields[1].type + def objectType = classElement.fields[2].type + def stringType = classElement.fields[3].type + def numberType = classElement.fields[4].type + + expect: + rawType.typeArguments["E"].type.name == "java.lang.Object" + rawType.typeArguments["E"].isRawType() + !rawType.typeArguments["E"].isWildcard() + rawType.typeArguments["E"].isGenericPlaceholder() + + wildcardType.typeArguments["E"].type.name == "java.lang.Object" + wildcardType.typeArguments["E"].isWildcard() + !((WildcardElement)wildcardType.typeArguments["E"]).isBounded() + !wildcardType.typeArguments["E"].isRawType() + + objectType.typeArguments["E"].type.name == "java.lang.Object" + !objectType.typeArguments["E"].isWildcard() + !objectType.typeArguments["E"].isRawType() + !objectType.typeArguments["E"].isGenericPlaceholder() + + stringType.typeArguments["E"].type.name == "java.lang.String" + !stringType.typeArguments["E"].isWildcard() + !stringType.typeArguments["E"].isRawType() + !stringType.typeArguments["E"].isGenericPlaceholder() + + numberType.typeArguments["E"].type.name == "java.lang.Number" + numberType.typeArguments["E"].isWildcard() + ((WildcardElement)numberType.typeArguments["E"]).isBounded() + !numberType.typeArguments["E"].isRawType() } } diff --git a/inject-java/src/test/groovy/io/micronaut/aop/compile/ExecutableFactoryMethodSpec.groovy b/inject-java/src/test/groovy/io/micronaut/aop/compile/ExecutableFactoryMethodSpec.groovy index b7bd9957cf3..a64a5b9369e 100644 --- a/inject-java/src/test/groovy/io/micronaut/aop/compile/ExecutableFactoryMethodSpec.groovy +++ b/inject-java/src/test/groovy/io/micronaut/aop/compile/ExecutableFactoryMethodSpec.groovy @@ -31,7 +31,7 @@ class MyFactory { } class MyClass implements SomeInterface { - + @Override public String goDog() { return "go"; @@ -101,5 +101,6 @@ interface MyClient extends ReactorStreamingHttpClient { retrieveMethod.returnType.typeParameters.length == 1 retrieveMethod.returnType.typeParameters[0].type == Object.class streamMethod.returnType.typeParameters[0].type == byte[].class + blockingMethod.returnType.type == byte[].class } } diff --git a/inject-java/src/test/groovy/io/micronaut/inject/beans/BeanDefinitionSpec.groovy b/inject-java/src/test/groovy/io/micronaut/inject/beans/BeanDefinitionSpec.groovy index 2b2f6536ac3..d9ecc8c735c 100644 --- a/inject-java/src/test/groovy/io/micronaut/inject/beans/BeanDefinitionSpec.groovy +++ b/inject-java/src/test/groovy/io/micronaut/inject/beans/BeanDefinitionSpec.groovy @@ -3,6 +3,7 @@ package io.micronaut.inject.beans import io.micronaut.annotation.processing.test.AbstractTypeElementSpec import io.micronaut.core.annotation.AnnotationUtil import io.micronaut.core.annotation.Order +import io.micronaut.inject.BeanDefinition import io.micronaut.inject.qualifiers.Qualifiers import spock.lang.Issue @@ -396,6 +397,29 @@ public class NumberThingManager extends AbstractThingManager> {} definition.getTypeArguments("test.AbstractThingManager")[0].getTypeVariables().get("T").getType() == Number.class } + void "test building a bean with generics wildcard extending"() { + when: + def definition = buildBeanDefinition('test.NumberThingManager', ''' +package test; + +import jakarta.inject.Singleton; + +interface Thing {} + +interface NumberThing> extends Thing {} + +class AbstractThingManager> {} + +@Singleton +public class NumberThingManager extends AbstractThingManager> {} +''') + + then: + noExceptionThrown() + definition != null + definition.getTypeArguments("test.AbstractThingManager")[0].getTypeVariables().get("T").getType() == Double.class + } + void "test a bean definition in a package with uppercase letters"() { when: def definition = buildBeanDefinition('test.A', 'TestBean', ''' @@ -410,4 +434,62 @@ class TestBean { noExceptionThrown() definition != null } + + void "test deep type parameters are created in definition"() { + given: + BeanDefinition definition = buildBeanDefinition('test','Test',''' +package test; +import java.util.List; + +@jakarta.inject.Singleton +public class Test { + List>> deepList; + public Test(List>> deepList) { this.deepList = deepList; } +} + ''') + + expect: + definition != null + def constructor = definition.getConstructor() + + def param = constructor.getArguments()[0] + param.getTypeParameters().length == 1 + def param1 = param.getTypeParameters()[0] + param1.getTypeParameters().length == 1 + def param2 = param1.getTypeParameters()[0] + param2.getTypeParameters().length == 1 + def param3 = param2.getTypeParameters()[0] + param3.getType() == String.class + } + + void "test annotation metadata present on deep type parameters of definition"() { + given: + BeanDefinition definition = buildBeanDefinition('test','Test',''' +package test; +import javax.validation.constraints.*; +import java.util.List; + +@jakarta.inject.Singleton +public class Test { + public Test(List<@Size(min=1) List<@NotEmpty List<@NotNull String>>> deepList) { } +} + ''') + + when: + definition != null + def constructor = definition.getConstructor() + def param = constructor.getArguments()[0] + def param1 = param.getTypeParameters()[0] + def param2 = param1.getTypeParameters()[0] + def param3 = param2.getTypeParameters()[0] + + then: + param.getAnnotationMetadata().getAnnotationNames().size() == 0 + param1.getAnnotationMetadata().getAnnotationNames().size() == 1 + param1.getAnnotationMetadata().getAnnotationNames().asList() == ['javax.validation.constraints.Size$List'] + param2.getAnnotationMetadata().getAnnotationNames().size() == 1 + param2.getAnnotationMetadata().getAnnotationNames().asList() == ['javax.validation.constraints.NotEmpty$List'] + param3.getAnnotationMetadata().getAnnotationNames().size() == 1 + param3.getAnnotationMetadata().getAnnotationNames().asList() == ['javax.validation.constraints.NotNull$List'] + } } diff --git a/inject-java/src/test/groovy/io/micronaut/visitors/AllElementsVisitor.java b/inject-java/src/test/groovy/io/micronaut/visitors/AllElementsVisitor.java index b82823eed81..4c4f167535d 100644 --- a/inject-java/src/test/groovy/io/micronaut/visitors/AllElementsVisitor.java +++ b/inject-java/src/test/groovy/io/micronaut/visitors/AllElementsVisitor.java @@ -46,6 +46,10 @@ public void visitClass(ClassElement element, VisitorContext context) { visit(element); element.getBeanProperties(); // Preload properties for tests otherwise it fails because the compiler is done element.getAnnotationMetadata(); + element.getSuperType().ifPresent(superType -> { + superType.getAllTypeArguments(); + superType.getTypeArguments(); + }); VISITED_CLASS_ELEMENTS.add(element); } diff --git a/inject-java/src/test/groovy/io/micronaut/visitors/ClassElementSpec.groovy b/inject-java/src/test/groovy/io/micronaut/visitors/ClassElementSpec.groovy index 708a650ee32..57f6842f573 100644 --- a/inject-java/src/test/groovy/io/micronaut/visitors/ClassElementSpec.groovy +++ b/inject-java/src/test/groovy/io/micronaut/visitors/ClassElementSpec.groovy @@ -982,18 +982,6 @@ class Foo {} AllElementsVisitor.VISITED_METHOD_ELEMENTS[0].parameters[0].type.typeArguments.get("E").name == 'test.Foo' } - // TODO: Investigate why this fails on JDK 11 - // com.sun.tools.javac.util.PropagatedException: java.lang.IllegalStateException - // at jdk.compiler/com.sun.tools.javac.api.JavacTaskImpl.prepareCompiler(JavacTaskImpl.java:187) - // at jdk.compiler/com.sun.tools.javac.api.JavacTaskImpl.enter(JavacTaskImpl.java:290) - // at jdk.compiler/com.sun.tools.javac.api.JavacTaskImpl.ensureEntered(JavacTaskImpl.java:481) - // at jdk.compiler/com.sun.tools.javac.model.JavacElements.ensureEntered(JavacElements.java:779) - // at jdk.compiler/com.sun.tools.javac.model.JavacElements.doGetTypeElement(JavacElements.java:171) - // at jdk.compiler/com.sun.tools.javac.model.JavacElements.getTypeElement(JavacElements.java:160) - // at jdk.compiler/com.sun.tools.javac.model.JavacElements.getTypeElement(JavacElements.java:87) - // at io.micronaut.annotation.processing.GenericUtils.buildGenericTypeArgumentElementInfo(GenericUtils.java:91) - // at io.micronaut.annotation.processing.visitor.JavaClassElement.getSuperType(JavaClassElement.java:127) - @IgnoreIf({ Jvm.current.isJava9Compatible() }) void "test JavaClassElement.getSuperType() with generic types"() { given: buildBeanDefinition('test.MyBean', ''' @@ -1700,6 +1688,431 @@ class Pet { genericReturnType.hasAnnotation(Introspected) } + void "test annotation metadata present on deep type parameters for field"() { + ClassElement ce = buildClassElement(''' +package test; +import io.micronaut.core.annotation.*; +import javax.validation.constraints.*; +import java.util.List; + +class Test { + List<@Size(min=1, max=2) List<@NotEmpty List<@NotNull String>>> deepList; +} +''') + expect: + def field = ce.getFields().find { it.name == "deepList"} + def fieldType = field.getGenericType() + + fieldType.getAnnotationMetadata().getAnnotationNames().size() == 0 + + assertListGenericArgument(fieldType, { ClassElement listArg1 -> + assert listArg1.getAnnotationMetadata().getAnnotationNames().asList() == ['javax.validation.constraints.Size$List'] + assertListGenericArgument(listArg1, { ClassElement listArg2 -> + assert listArg2.getAnnotationMetadata().getAnnotationNames().asList() == ['javax.validation.constraints.NotEmpty$List'] + assertListGenericArgument(listArg2, { ClassElement listArg3 -> + assert listArg3.getAnnotationMetadata().getAnnotationNames().asList() == ['javax.validation.constraints.NotNull$List'] + }) + }) + }) + + def level1 = fieldType.getTypeArguments()["E"] + level1.getAnnotationMetadata().getAnnotationNames().asList() == ['javax.validation.constraints.Size$List'] + def level2 = level1.getTypeArguments()["E"] + level2.getAnnotationMetadata().getAnnotationNames().asList() == ['javax.validation.constraints.NotEmpty$List'] + def level3 = level2.getTypeArguments()["E"] + level3.getAnnotationMetadata().getAnnotationNames().asList() == ['javax.validation.constraints.NotNull$List'] + } + + void "test annotation metadata present on deep type parameters for method"() { + ClassElement ce = buildClassElement(''' +package test; +import io.micronaut.core.annotation.*; +import javax.validation.constraints.*; +import java.util.List; + +class Test { + List<@Size(min=1, max=2) List<@NotEmpty List<@NotNull String>>> deepList() { + return null; + } +} +''') + expect: + def method = ce.getEnclosedElement(ElementQuery.ALL_METHODS.named("deepList")).get() + def theType = method.getGenericReturnType() + + theType.getAnnotationMetadata().getAnnotationNames().size() == 0 + + assertListGenericArgument(theType, { ClassElement listArg1 -> + assert listArg1.getAnnotationMetadata().getAnnotationNames().asList() == ['javax.validation.constraints.Size$List'] + assertListGenericArgument(listArg1, { ClassElement listArg2 -> + assert listArg2.getAnnotationMetadata().getAnnotationNames().asList() == ['javax.validation.constraints.NotEmpty$List'] + assertListGenericArgument(listArg2, { ClassElement listArg3 -> + assert listArg3.getAnnotationMetadata().getAnnotationNames().asList() == ['javax.validation.constraints.NotNull$List'] + }) + }) + }) + + def level1 = theType.getTypeArguments()["E"] + level1.getAnnotationMetadata().getAnnotationNames().asList() == ['javax.validation.constraints.Size$List'] + def level2 = level1.getTypeArguments()["E"] + level2.getAnnotationMetadata().getAnnotationNames().asList() == ['javax.validation.constraints.NotEmpty$List'] + def level3 = level2.getTypeArguments()["E"] + level3.getAnnotationMetadata().getAnnotationNames().asList() == ['javax.validation.constraints.NotNull$List'] + } + + void "test recursive generic type parameter"() { + given: + ClassElement ce = buildClassElement('''\ +package test; + +final class TrackedSortedSet> { +} + +''') + expect: + def typeArguments = ce.getTypeArguments() + typeArguments.size() == 1 + def typeArgument = typeArguments.get("T") + typeArgument.name == "java.lang.Comparable" + def nextTypeArguments = typeArgument.getTypeArguments() + def nextTypeArgument = nextTypeArguments.get("T") + nextTypeArgument.name == "java.lang.Comparable" + def nextNextTypeArguments = nextTypeArgument.getTypeArguments() + def nextNextTypeArgument = nextNextTypeArguments.get("T") + nextNextTypeArgument.name == "java.lang.Object" + } + + void "test recursive generic type parameter 2"() { + given: + ClassElement ce = buildClassElement('''\ +package test; + +final class Test { // Missing argument +} + +''') + expect: + def typeArguments = ce.getTypeArguments() + typeArguments.size() == 1 + def typeArgument = typeArguments.get("T") + typeArgument.name == "test.Test" + def nextTypeArguments = typeArgument.getTypeArguments() + def nextTypeArgument = nextTypeArguments.get("T") + nextTypeArgument.name == "java.lang.Object" + } + + void "test recursive generic type parameter 3"() { + given: + ClassElement ce = buildClassElement('''\ +package test; + +final class Test> { +} + +''') + expect: + def typeArguments = ce.getTypeArguments() + typeArguments.size() == 1 + def typeArgument = typeArguments.get("T") + typeArgument.name == "test.Test" + def nextTypeArguments = typeArgument.getTypeArguments() + def nextTypeArgument = nextTypeArguments.get("T") + nextTypeArgument.name == "java.lang.Object" + } + + void "test recursive generic type parameter 4"() { + given: + ClassElement ce = buildClassElement('''\ +package test; + +final class Test> { +} + +''') + expect: + def typeArguments = ce.getTypeArguments() + typeArguments.size() == 1 + def typeArgument = typeArguments.get("T") + typeArgument.name == "test.Test" + def nextTypeArguments = typeArgument.getTypeArguments() + def nextTypeArgument = nextTypeArguments.get("T") + nextTypeArgument.name == "java.lang.Object" + } + + void "test recursive generic method return"() { + given: + ClassElement ce = buildClassElement('''\ +package test; + +import org.hibernate.SessionFactory; +import org.hibernate.engine.spi.SessionFactoryDelegatingImpl; + +class MyFactory { + + SessionFactory sessionFactory() { + return new SessionFactoryDelegatingImpl(null); + } +} + +''') + expect: + def sessionFactoryMethod = ce.getEnclosedElement(ElementQuery.ALL_METHODS.named("sessionFactory")).get() + def withOptionsMethod = sessionFactoryMethod.getReturnType().getEnclosedElement(ElementQuery.ALL_METHODS.named("withOptions")).get() + def typeArguments = withOptionsMethod.getReturnType().getTypeArguments() + typeArguments.size() == 1 + def typeArgument = typeArguments.get("T") + typeArgument.name == "org.hibernate.SessionBuilder" + def nextTypeArguments = typeArgument.getTypeArguments() + def nextTypeArgument = nextTypeArguments.get("T") + nextTypeArgument.name == "java.lang.Object" + } + + void "test recursive generic method return 2"() { + given: + ClassElement ce = buildClassElement('''\ +package test; + +class MyFactory { + + MyBean myBean() { + return new MyBean(); + } +} + +interface MyBuilder { + T build(); +} + +class MyBean { + + MyBuilder myBuilder() { + return null; + } + +} + +''') + expect: + def myBeanMethod = ce.getEnclosedElement(ElementQuery.ALL_METHODS.named("myBean")).get() + def myBuilderMethod = myBeanMethod.getReturnType().getEnclosedElement(ElementQuery.ALL_METHODS.named("myBuilder")).get() + def typeArguments = myBuilderMethod.getReturnType().getTypeArguments() + typeArguments.size() == 1 + def typeArgument = typeArguments.get("T") + typeArgument.name == "test.MyBuilder" + def nextTypeArguments = typeArgument.getTypeArguments() + def nextTypeArgument = nextTypeArguments.get("T") + nextTypeArgument.name == "test.MyBuilder" + def nextNextTypeArguments = nextTypeArgument.getTypeArguments() + def nextNextTypeArgument = nextNextTypeArguments.get("T") + nextNextTypeArgument.name == "java.lang.Object" + } + + void "test recursive generic method return 3"() { + given: + ClassElement ce = buildClassElement('''\ +package test; + +class MyFactory { + + MyBean myBean() { + return new MyBean(); + } +} + +interface MyBuilder { + T build(); +} + +class MyBean { + + MyBuilder myBuilder() { + return null; + } + +} + +''') + expect: + def myBeanMethod = ce.getEnclosedElement(ElementQuery.ALL_METHODS.named("myBean")).get() + def myBuilderMethod = myBeanMethod.getReturnType().getEnclosedElement(ElementQuery.ALL_METHODS.named("myBuilder")).get() + def typeArguments = myBuilderMethod.getReturnType().getTypeArguments() + typeArguments.size() == 1 + def typeArgument = typeArguments.get("T") + typeArgument.name == "test.MyBuilder" + def nextTypeArguments = typeArgument.getTypeArguments() + def nextTypeArgument = nextTypeArguments.get("T") + nextTypeArgument.name == "java.lang.Object" + } + + void "test recursive generic method return 4"() { + given: + ClassElement ce = buildClassElement('''\ +package test; + +class MyFactory { + + MyBean myBean() { + return new MyBean(); + } +} + +interface MyBuilder { + T build(); +} + +class MyBean { + + MyBuilder myBuilder() { + return null; + } + +} + +''') + expect: + def myBeanMethod = ce.getEnclosedElement(ElementQuery.ALL_METHODS.named("myBean")).get() + def myBuilderMethod = myBeanMethod.getReturnType().getEnclosedElement(ElementQuery.ALL_METHODS.named("myBuilder")).get() + def typeArguments = myBuilderMethod.getReturnType().getTypeArguments() + typeArguments.size() == 1 + def typeArgument = typeArguments.get("T") + typeArgument.name == "test.MyBuilder" + def nextTypeArguments = typeArgument.getTypeArguments() + def nextTypeArgument = nextTypeArguments.get("T") + nextTypeArgument.name == "test.MyBuilder" + def nextNextTypeArguments = nextTypeArgument.getTypeArguments() + def nextNextTypeArgument = nextNextTypeArguments.get("T") + nextNextTypeArgument.name == "java.lang.Object" + } + + void "test recursive generic method return 5"() { + given: + ClassElement ce = buildClassElement('''\ +package test; + +class MyFactory { + + MyBean myBean() { + return new MyBean(); + } +} + +interface MyBuilder { + T build(); +} + +class MyBean { + + MyBuilder myBuilder() { + return null; + } + +} + +''') + expect: + def myBeanMethod = ce.getEnclosedElement(ElementQuery.ALL_METHODS.named("myBean")).get() + def myBuilderMethod = myBeanMethod.getReturnType().getEnclosedElement(ElementQuery.ALL_METHODS.named("myBuilder")).get() + def typeArguments = myBuilderMethod.getReturnType().getTypeArguments() + typeArguments.size() == 1 + def typeArgument = typeArguments.get("T") + typeArgument.name == "test.MyBuilder" + def nextTypeArguments = typeArgument.getTypeArguments() + def nextTypeArgument = nextTypeArguments.get("T") + nextTypeArgument.name == "test.MyBuilder" + def nextNextTypeArguments = nextTypeArgument.getTypeArguments() + def nextNextTypeArgument = nextNextTypeArguments.get("T") + nextNextTypeArgument.name == "java.lang.Object" + } + + void "test recursive generic method return 6"() { + given: + ClassElement ce = buildClassElement('''\ +package test; + +class MyFactory { + + MyBean myBean() { + return new MyBean(); + } +} + +interface MyBuilder { + T build(); +} + +class MyBean { + + MyBuilder myBuilder() { + return null; + } + +} + +''') + expect: + def myBeanMethod = ce.getEnclosedElement(ElementQuery.ALL_METHODS.named("myBean")).get() + def myBuilderMethod = myBeanMethod.getReturnType().getEnclosedElement(ElementQuery.ALL_METHODS.named("myBuilder")).get() + def typeArguments = myBuilderMethod.getReturnType().getTypeArguments() + typeArguments.size() == 1 + def typeArgument = typeArguments.get("T") + typeArgument.name == "test.MyBuilder" + def nextTypeArguments = typeArgument.getTypeArguments() + def nextTypeArgument = nextTypeArguments.get("T") + nextTypeArgument.name == "test.MyBuilder" + def nextNextTypeArguments = nextTypeArgument.getTypeArguments() + def nextNextTypeArgument = nextNextTypeArguments.get("T") + nextNextTypeArgument.name == "java.lang.Object" + } + + void "test recursive generic method return 7"() { + given: + ClassElement ce = buildClassElement('''\ +package test; + +class MyFactory { + + MyBean myBean() { + return new MyBean(); + } +} + +interface MyBuilder { + T build(); +} + +class MyBean { + + MyBuilder myBuilder() { + return null; + } + +} + +''') + expect: + def myBeanMethod = ce.getEnclosedElement(ElementQuery.ALL_METHODS.named("myBean")).get() + def myBuilderMethod = myBeanMethod.getReturnType().getEnclosedElement(ElementQuery.ALL_METHODS.named("myBuilder")).get() + def typeArguments = myBuilderMethod.getReturnType().getTypeArguments() + typeArguments.size() == 1 + def typeArgument = typeArguments.get("T") + typeArgument.name == "test.MyBuilder" + def nextTypeArguments = typeArgument.getTypeArguments() + def nextTypeArgument = nextTypeArguments.get("T") + nextTypeArgument.name == "test.MyBuilder" + def nextNextTypeArguments = nextTypeArgument.getTypeArguments() + def nextNextTypeArgument = nextNextTypeArguments.get("T") + nextNextTypeArgument.name == "java.lang.Object" + } + + private void assertListGenericArgument(ClassElement type, Closure cl) { + def arg1 = type.getAllTypeArguments().get(List.class.name).get("E") + def arg2 = type.getAllTypeArguments().get(Collection.class.name).get("E") + def arg3 = type.getAllTypeArguments().get(Iterable.class.name).get("T") + cl.call(arg1) + cl.call(arg2) + cl.call(arg3) + } + private void assertMethodsByName(List allMethods, String name, List expectedDeclaringTypeSimpleNames) { Collection methods = collectElements(allMethods, name) assert expectedDeclaringTypeSimpleNames.size() == methods.size() diff --git a/inject/src/main/java/io/micronaut/context/ExecutionHandleLocator.java b/inject/src/main/java/io/micronaut/context/ExecutionHandleLocator.java index a1b25b10da6..91e0e7ab04d 100644 --- a/inject/src/main/java/io/micronaut/context/ExecutionHandleLocator.java +++ b/inject/src/main/java/io/micronaut/context/ExecutionHandleLocator.java @@ -15,6 +15,7 @@ */ package io.micronaut.context; +import io.micronaut.core.annotation.NonNull; import io.micronaut.core.annotation.UsedByGeneratedCode; import io.micronaut.core.type.Argument; import io.micronaut.inject.BeanDefinition; @@ -158,9 +159,7 @@ default Optional> findProxyTargetMethod(Argument ExecutableMethod getExecutableMethod(Class beanType, String method, Class... arguments) throws NoSuchMethodException { Optional> executableMethod = this.findExecutableMethod(beanType, method, arguments); - return executableMethod.orElseThrow(() -> - new NoSuchMethodException("No such method [" + method + "(" + Arrays.stream(arguments).map(Class::getName).collect(Collectors.joining(",")) + ") ] for bean [" + beanType.getName() + "]") - ); + return executableMethod.orElseThrow(() -> newNoSuchMethodException(beanType.getName(), method, arguments)); } /** @@ -178,9 +177,7 @@ default ExecutableMethod getExecutableMethod(Class beanType, Str @UsedByGeneratedCode default ExecutableMethod getProxyTargetMethod(Class beanType, String method, Class... arguments) throws NoSuchMethodException { Optional> executableMethod = this.findProxyTargetMethod(beanType, method, arguments); - return executableMethod.orElseThrow(() -> - new NoSuchMethodException("No such method [" + method + "(" + Arrays.stream(arguments).map(Class::getName).collect(Collectors.joining(",")) + ") ] for bean [" + beanType.getName() + "]") - ); + return executableMethod.orElseThrow(() -> newNoSuchMethodException(beanType.getName(), method, arguments)); } /** @@ -199,9 +196,7 @@ default ExecutableMethod getProxyTargetMethod(Class beanType, St @UsedByGeneratedCode default ExecutableMethod getProxyTargetMethod(Class beanType, Qualifier qualifier, String method, Class... arguments) throws NoSuchMethodException { Optional> executableMethod = this.findProxyTargetMethod(beanType, qualifier, method, arguments); - return executableMethod.orElseThrow(() -> - new NoSuchMethodException("No such method [" + method + "(" + Arrays.stream(arguments).map(Class::getName).collect(Collectors.joining(",")) + ") ] for bean [" + beanType.getName() + "]") - ); + return executableMethod.orElseThrow(() -> newNoSuchMethodException(beanType.getName(), method, arguments)); } /** @@ -220,10 +215,8 @@ default ExecutableMethod getProxyTargetMethod(Class beanType, Qu */ @UsedByGeneratedCode default ExecutableMethod getProxyTargetMethod(Argument beanType, Qualifier qualifier, String method, Class... arguments) throws NoSuchMethodException { - Optional> executableMethod = this.findProxyTargetMethod(beanType, qualifier, method, arguments); - return executableMethod.orElseThrow(() -> - new NoSuchMethodException("No such method [" + method + "(" + Arrays.stream(arguments).map(Class::getName).collect(Collectors.joining(",")) + ") ] for bean [" + beanType.getName() + "]") - ); + return this.findProxyTargetMethod(beanType, qualifier, method, arguments) + .orElseThrow(() -> newNoSuchMethodException(beanType.getName(), method, arguments)); } /** @@ -239,9 +232,8 @@ default ExecutableMethod getProxyTargetMethod(Argument beanType, * @throws NoSuchMethodException if the method cannot be found */ default MethodExecutionHandle getExecutionHandle(Class beanType, String method, Class... arguments) throws NoSuchMethodException { - return this.findExecutionHandle(beanType, method, arguments).orElseThrow(() -> - new NoSuchMethodException("No such method [" + method + "(" + Arrays.stream(arguments).map(Class::getName).collect(Collectors.joining(",")) + ") ] for bean [" + beanType.getName() + "]") - ); + return this.findExecutionHandle(beanType, method, arguments) + .orElseThrow(() -> newNoSuchMethodException(beanType.getName(), method, arguments)); } /** @@ -256,9 +248,8 @@ default MethodExecutionHandle getExecutionHandle(Class beanType, * @throws NoSuchMethodException if the method cannot be found */ default MethodExecutionHandle getExecutionHandle(T bean, String method, Class... arguments) throws NoSuchMethodException { - return this.findExecutionHandle(bean, method, arguments).orElseThrow(() -> - new NoSuchMethodException("No such method [" + method + "(" + Arrays.stream(arguments).map(Class::getName).collect(Collectors.joining(",")) + ") ] for bean [" + bean + "]") - ); + return this.findExecutionHandle(bean, method, arguments) + .orElseThrow(() -> newNoSuchMethodException(bean.toString(), method, arguments)); } /** @@ -270,4 +261,9 @@ default MethodExecutionHandle getExecutionHandle(T bean, String met default MethodExecutionHandle createExecutionHandle(BeanDefinition beanDefinition, ExecutableMethod method) { throw new UnsupportedOperationException("No such method [" + method + "(" + Arrays.stream(method.getArgumentTypes()).map(Class::getName).collect(Collectors.joining(",")) + ") ] for bean [" + beanDefinition.getBeanType() + "]"); } + + @NonNull + private static NoSuchMethodException newNoSuchMethodException(@NonNull String bean, @NonNull String method, @NonNull Class[] arguments) { + return new NoSuchMethodException("No such method [" + method + "(" + Arrays.stream(arguments).map(Class::getName).collect(Collectors.joining(",")) + ")] for bean [" + bean + "]"); + } } From d9bae5341a45112883fd711aedcb7107d9f1cba4 Mon Sep 17 00:00:00 2001 From: micronaut-build Date: Tue, 7 Feb 2023 15:53:01 +0000 Subject: [PATCH 460/743] [skip ci] Release v3.8.4 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index d5f61384e93..8cefb9c9c04 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -projectVersion=3.8.4-SNAPSHOT +projectVersion=3.8.4 projectGroupId=io.micronaut title=Micronaut projectDesc=Natively Cloud Native From 5df7aa70638d35d585750f53f9ab4acdda0d8cbd Mon Sep 17 00:00:00 2001 From: Graeme Rocher Date: Tue, 7 Feb 2023 17:07:10 +0100 Subject: [PATCH 461/743] Fix NPE producing anntotion metadata for KSP (#8736) --- .../visitor/BeanIntrospectionWriter.java | 29 +- gradle/libs.versions.toml | 7 +- inject-kotlin-test/build.gradle | 6 +- .../processing/test/KotlinCompiler.java | 6 +- .../test/support/AbstractKotlinCompilation.kt | 301 +++++++++ .../test/support/DefaultPropertyDelegate.kt | 48 ++ .../test/support/HostEnvironment.kt | 81 +++ .../processing/test/support/JavacUtils.kt | 127 ++++ .../test/support/KotlinCompilation.kt | 608 ++++++++++++++++++ .../annotation/processing/test/support/Ksp.kt | 246 +++++++ .../test/support/MainComponentRegistrar.kt | 61 ++ .../processing/test/support/SourceFile.kt | 75 +++ .../processing/test/support/StreamUtils.kt | 69 ++ .../test/support/SynchronizedToolProvider.kt | 51 ++ .../processing/test/support/Utils.kt | 88 +++ ....kotlin.compiler.plugin.ComponentRegistrar | 1 + inject-kotlin/build.gradle | 4 +- .../KotlinAnnotationMetadataBuilder.kt | 25 +- .../beans/BeanDefinitionProcessor.kt | 5 + .../beans/BeanDefinitionSpec.groovy | 30 + .../visitor/KotlinReconstructionSpec.groovy | 2 + 21 files changed, 1846 insertions(+), 24 deletions(-) create mode 100644 inject-kotlin-test/src/main/kotlin/io/micronaut/annotation/processing/test/support/AbstractKotlinCompilation.kt create mode 100644 inject-kotlin-test/src/main/kotlin/io/micronaut/annotation/processing/test/support/DefaultPropertyDelegate.kt create mode 100644 inject-kotlin-test/src/main/kotlin/io/micronaut/annotation/processing/test/support/HostEnvironment.kt create mode 100644 inject-kotlin-test/src/main/kotlin/io/micronaut/annotation/processing/test/support/JavacUtils.kt create mode 100644 inject-kotlin-test/src/main/kotlin/io/micronaut/annotation/processing/test/support/KotlinCompilation.kt create mode 100644 inject-kotlin-test/src/main/kotlin/io/micronaut/annotation/processing/test/support/Ksp.kt create mode 100644 inject-kotlin-test/src/main/kotlin/io/micronaut/annotation/processing/test/support/MainComponentRegistrar.kt create mode 100644 inject-kotlin-test/src/main/kotlin/io/micronaut/annotation/processing/test/support/SourceFile.kt create mode 100644 inject-kotlin-test/src/main/kotlin/io/micronaut/annotation/processing/test/support/StreamUtils.kt create mode 100644 inject-kotlin-test/src/main/kotlin/io/micronaut/annotation/processing/test/support/SynchronizedToolProvider.kt create mode 100644 inject-kotlin-test/src/main/kotlin/io/micronaut/annotation/processing/test/support/Utils.kt create mode 100644 inject-kotlin-test/src/main/resources/META-INF/services/org.jetbrains.kotlin.compiler.plugin.ComponentRegistrar diff --git a/core-processor/src/main/java/io/micronaut/inject/beans/visitor/BeanIntrospectionWriter.java b/core-processor/src/main/java/io/micronaut/inject/beans/visitor/BeanIntrospectionWriter.java index 47aa39be5c9..9175be0b417 100644 --- a/core-processor/src/main/java/io/micronaut/inject/beans/visitor/BeanIntrospectionWriter.java +++ b/core-processor/src/main/java/io/micronaut/inject/beans/visitor/BeanIntrospectionWriter.java @@ -86,22 +86,22 @@ final class BeanIntrospectionWriter extends AbstractAnnotationMetadataWriter { private static final String FIELD_BEAN_PROPERTIES_REFERENCES = "$PROPERTIES_REFERENCES"; private static final String FIELD_BEAN_METHODS_REFERENCES = "$METHODS_REFERENCES"; private static final Method PROPERTY_INDEX_OF = Method.getMethod( - ReflectionUtils.findMethod(BeanIntrospection.class, "propertyIndexOf", String.class).get() + ReflectionUtils.getRequiredInternalMethod(BeanIntrospection.class, "propertyIndexOf", String.class) ); private static final Method FIND_PROPERTY_BY_INDEX_METHOD = Method.getMethod( - ReflectionUtils.findMethod(AbstractInitializableBeanIntrospection.class, "getPropertyByIndex", int.class).get() + ReflectionUtils.getRequiredInternalMethod(AbstractInitializableBeanIntrospection.class, "getPropertyByIndex", int.class) ); private static final Method FIND_INDEXED_PROPERTY_METHOD = Method.getMethod( - ReflectionUtils.findMethod(AbstractInitializableBeanIntrospection.class, "findIndexedProperty", Class.class, String.class).get() + ReflectionUtils.getRequiredInternalMethod(AbstractInitializableBeanIntrospection.class, "findIndexedProperty", Class.class, String.class) ); private static final Method GET_INDEXED_PROPERTIES = Method.getMethod( - ReflectionUtils.findMethod(AbstractInitializableBeanIntrospection.class, "getIndexedProperties", Class.class).get() + ReflectionUtils.getRequiredInternalMethod(AbstractInitializableBeanIntrospection.class, "getIndexedProperties", Class.class) ); private static final Method GET_BP_INDEXED_SUBSET_METHOD = Method.getMethod( - ReflectionUtils.findMethod(AbstractInitializableBeanIntrospection.class, "getBeanPropertiesIndexedSubset", int[].class).get() + ReflectionUtils.getRequiredInternalMethod(AbstractInitializableBeanIntrospection.class, "getBeanPropertiesIndexedSubset", int[].class) ); private static final Method COLLECTIONS_EMPTY_LIST = Method.getMethod( - ReflectionUtils.findMethod(Collections.class, "emptyList").get() + ReflectionUtils.getRequiredInternalMethod(Collections.class, "emptyList") ); private final ClassWriter referenceWriter; @@ -970,6 +970,22 @@ private static String computeIntrospectionName(String generatingName, String cla */ void visitConstructor(MethodElement constructor) { this.constructor = constructor; + processAnnotationDefaults(constructor); + } + + private void processAnnotationDefaults(MethodElement constructor) { + if (constructor != null) { + MutableAnnotationMetadata.contributeDefaults( + this.annotationMetadata, + constructor.getAnnotationMetadata().getTargetAnnotationMetadata() + ); + for (ParameterElement parameter : constructor.getParameters()) { + MutableAnnotationMetadata.contributeDefaults( + this.annotationMetadata, + parameter.getAnnotationMetadata().getTargetAnnotationMetadata() + ); + } + } } /** @@ -979,6 +995,7 @@ void visitConstructor(MethodElement constructor) { */ void visitDefaultConstructor(MethodElement constructor) { this.defaultConstructor = constructor; + processAnnotationDefaults(constructor); } private static final class ExceptionDispatchTarget implements DispatchWriter.DispatchTarget { diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index e44b28d4132..c0a630744df 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -32,9 +32,8 @@ javax-el-impl = "2.2.1-b05" jcache = "1.1.1" junit5 = "5.9.1" junit-platform="1.9.1" -kotlin = "1.7.20" +kotlin = "1.8.10" kotlin-coroutines = "1.6.4" -ksp-testing = "1.4.9" ktor = "1.6.8" managed-logback = "1.4.5" logbook-netty = "2.14.0" @@ -75,7 +74,7 @@ managed-slf4j = "2.0.4" managed-snakeyaml = "1.33" managed-validation = "2.0.1.Final" managed-java-parser-core = "3.24.9" -managed-ksp = "1.8.0-Beta-1.0.8" +managed-ksp = "1.8.0-1.0.9" micronaut-docs = "2.0.0" [libraries] @@ -207,8 +206,6 @@ kotlinx-coroutines-rx2 = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-rx kotlinx-coroutines-slf4j = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-slf4j", version.ref = "kotlin-coroutines" } kotlinx-coroutines-reactor = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-reactor", version.ref = "kotlin-coroutines" } -ksp-testing = { module = "com.github.tschuchortdev:kotlin-compile-testing-ksp", version.ref = "ksp-testing" } - log4j = { module = "org.apache.logging.log4j:log4j-core", version.ref = "log4j" } logbook-netty = { module = "org.zalando:logbook-netty", version.ref = "logbook-netty" } diff --git a/inject-kotlin-test/build.gradle b/inject-kotlin-test/build.gradle index cf4b5160630..5bd45de21c1 100644 --- a/inject-kotlin-test/build.gradle +++ b/inject-kotlin-test/build.gradle @@ -12,7 +12,11 @@ dependencies { if (!JavaVersion.current().isJava9Compatible()) { api files(org.gradle.internal.jvm.Jvm.current().toolsJar) } - api(libs.ksp.testing) + api(libs.managed.ksp.api) + api(libs.managed.ksp) + implementation(libs.kotlin.compiler.embeddable) + implementation "com.squareup.okio:okio:3.2.0" + implementation "io.github.classgraph:classgraph:4.8.149" testImplementation libs.managed.validation testImplementation libs.javax.persistence testImplementation project(":runtime") diff --git a/inject-kotlin-test/src/main/groovy/io/micronaut/annotation/processing/test/KotlinCompiler.java b/inject-kotlin-test/src/main/groovy/io/micronaut/annotation/processing/test/KotlinCompiler.java index 17de74126bd..6a5d01432c9 100644 --- a/inject-kotlin-test/src/main/groovy/io/micronaut/annotation/processing/test/KotlinCompiler.java +++ b/inject-kotlin-test/src/main/groovy/io/micronaut/annotation/processing/test/KotlinCompiler.java @@ -18,9 +18,9 @@ import com.google.devtools.ksp.processing.SymbolProcessor; import com.google.devtools.ksp.processing.SymbolProcessorEnvironment; import com.google.devtools.ksp.symbol.KSClassDeclaration; -import com.tschuchort.compiletesting.KotlinCompilation; -import com.tschuchort.compiletesting.KspKt; -import com.tschuchort.compiletesting.SourceFile; +import io.micronaut.annotation.processing.test.support.KotlinCompilation; +import io.micronaut.annotation.processing.test.support.KspKt; +import io.micronaut.annotation.processing.test.support.SourceFile; import io.micronaut.aop.internal.InterceptorRegistryBean; import io.micronaut.context.ApplicationContext; import io.micronaut.context.ApplicationContextConfiguration; diff --git a/inject-kotlin-test/src/main/kotlin/io/micronaut/annotation/processing/test/support/AbstractKotlinCompilation.kt b/inject-kotlin-test/src/main/kotlin/io/micronaut/annotation/processing/test/support/AbstractKotlinCompilation.kt new file mode 100644 index 00000000000..d27b4523a34 --- /dev/null +++ b/inject-kotlin-test/src/main/kotlin/io/micronaut/annotation/processing/test/support/AbstractKotlinCompilation.kt @@ -0,0 +1,301 @@ +/* + * Copyright 2017-2023 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.annotation.processing.test.support + +import okio.Buffer +import org.jetbrains.kotlin.cli.common.CLICompiler +import org.jetbrains.kotlin.cli.common.ExitCode +import org.jetbrains.kotlin.cli.common.arguments.CommonCompilerArguments +import org.jetbrains.kotlin.cli.common.arguments.parseCommandLineArguments +import org.jetbrains.kotlin.cli.common.arguments.validateArguments +import org.jetbrains.kotlin.cli.common.messages.MessageRenderer +import org.jetbrains.kotlin.cli.common.messages.PrintingMessageCollector +import org.jetbrains.kotlin.compiler.plugin.CommandLineProcessor +import org.jetbrains.kotlin.compiler.plugin.ComponentRegistrar +import org.jetbrains.kotlin.compiler.plugin.ExperimentalCompilerApi +import org.jetbrains.kotlin.config.Services +import org.jetbrains.kotlin.load.java.JvmAbi +import org.jetbrains.kotlin.util.ServiceLoaderLite +import java.io.File +import java.io.OutputStream +import java.io.PrintStream +import java.net.URI +import java.nio.file.Files +import java.nio.file.Paths + +/** + * Base compilation class for sharing common compiler arguments and + * functionality. Should not be used outside of this library as it is an + * implementation detail. + */ +abstract class AbstractKotlinCompilation internal constructor() { + /** Working directory for the compilation */ + var workingDir: File by default { + val path = Files.createTempDirectory("Kotlin-Compilation") + log("Created temporary working directory at ${path.toAbsolutePath()}") + return@default path.toFile() + } + + /** + * Paths to directories or .jar files that contain classes + * to be made available in the compilation (i.e. added to + * the classpath) + */ + var classpaths: List = emptyList() + + /** + * Paths to plugins to be made available in the compilation + */ + var pluginClasspaths: List = emptyList() + + /** + * Compiler plugins that should be added to the compilation + */ + @OptIn(ExperimentalCompilerApi::class) + var compilerPlugins: List = emptyList() + + /** + * Commandline processors for compiler plugins that should be added to the compilation + */ + @OptIn(ExperimentalCompilerApi::class) + var commandLineProcessors: List = emptyList() + + /** Source files to be compiled */ + var sources: List = emptyList() + + /** Print verbose logging info */ + var verbose: Boolean = true + + /** + * Helpful information (if [verbose] = true) and the compiler + * system output will be written to this stream + */ + var messageOutputStream: OutputStream = System.out + + /** Inherit classpath from calling process */ + var inheritClassPath: Boolean = false + + /** Suppress all warnings */ + var suppressWarnings: Boolean = false + + /** All warnings should be treated as errors */ + var allWarningsAsErrors: Boolean = false + + /** Report locations of files generated by the compiler */ + var reportOutputFiles: Boolean by default { verbose } + + /** Report on performance of the compilation */ + var reportPerformance: Boolean = false + + var languageVersion: String? = null + + /** Use the new experimental K2 compiler */ + var useK2: Boolean by default { false } + + /** Enable experimental multiplatform support */ + var multiplatform: Boolean = false + + /** Do not check presence of 'actual' modifier in multi-platform projects */ + var noCheckActual: Boolean = false + + /** Enable usages of API that requires opt-in with an opt-in requirement marker with the given fully qualified name */ + var optIn: List? = null + + /** Additional string arguments to the Kotlin compiler */ + var kotlincArguments: List = emptyList() + + /** Options to be passed to compiler plugins: -P plugin::=*/ + var pluginOptions: List = emptyList() + + /** + * Path to the kotlin-stdlib-common.jar + * If none is given, it will be searched for in the host + * process' classpaths + */ + var kotlinStdLibCommonJar: File? by default { + HostEnvironment.kotlinStdLibCommonJar + } + + // Directory for input source files + protected val sourcesDir get() = workingDir.resolve("sources") + + protected inline fun CommonCompilerArguments.trySetDeprecatedOption(optionSimpleName: String, value: T) { + try { + this.javaClass.getMethod(JvmAbi.setterName(optionSimpleName), T::class.java).invoke(this, value) + } catch (e: ReflectiveOperationException) { + throw IllegalArgumentException( + "The deprecated option $optionSimpleName is no longer available in the kotlin version you are using", + e + ) + } + } + + protected fun commonArguments(args: A, configuration: (args: A) -> Unit): A { + args.pluginClasspaths = pluginClasspaths.map(File::getAbsolutePath).toTypedArray() + + args.verbose = verbose + + args.suppressWarnings = suppressWarnings + args.allWarningsAsErrors = allWarningsAsErrors + args.reportOutputFiles = reportOutputFiles + args.reportPerf = reportPerformance + args.useK2 = useK2 + args.multiPlatform = multiplatform + args.noCheckActual = noCheckActual + args.optIn = optIn?.toTypedArray() + + if (languageVersion != null) + args.languageVersion = this.languageVersion + + configuration(args) + + + /* Parse extra CLI arguments that are given as strings so users can specify arguments that are not yet + implemented here as well-typed properties. */ + parseCommandLineArguments(kotlincArguments, args) + + validateArguments(args.errors)?.let { + throw IllegalArgumentException("Errors parsing kotlinc CLI arguments:\n$it") + } + + return args + } + + /** Performs the compilation step to compile Kotlin source files */ + @OptIn(ExperimentalCompilerApi::class) + protected fun compileKotlin(sources: List, compiler: CLICompiler, arguments: A): KotlinCompilation.ExitCode { + + /** + * Here the list of compiler plugins is set + * + * To avoid that the annotation processors are executed twice, + * the list is set to empty + */ + MainComponentRegistrar.threadLocalParameters.set( + MainComponentRegistrar.ThreadLocalParameters( + compilerPlugins + ) + ) + + // in this step also include source files generated by kapt in the previous step + val args = arguments.also { args -> + args.freeArgs = + sources.map(File::getAbsolutePath).distinct() + if (sources.none(File::hasKotlinFileExtension)) { + /* __HACK__: The Kotlin compiler expects at least one Kotlin source file or it will crash, + so we trick the compiler by just including an empty .kt-File. We need the compiler to run + even if there are no Kotlin files because some compiler plugins may also process Java files. */ + listOf(SourceFile.new("emptyKotlinFile.kt", "").writeIfNeeded(sourcesDir).absolutePath) + } else { + emptyList() + } + args.pluginClasspaths = (args.pluginClasspaths ?: emptyArray()) + + /** The resources path contains the MainComponentRegistrar and MainCommandLineProcessor which will + be found by the Kotlin compiler's service loader. We add it only when the user has actually given + us ComponentRegistrar instances to be loaded by the MainComponentRegistrar because the experimental + K2 compiler doesn't support plugins yet. This way, users of K2 can prevent MainComponentRegistrar + from being loaded and crashing K2 by setting both [compilerPlugins] and [commandLineProcessors] to + the emptyList. */ + if (compilerPlugins.isNotEmpty() || commandLineProcessors.isNotEmpty()) + arrayOf(getResourcesPath()) + else emptyArray() + } + + val compilerMessageCollector = PrintingMessageCollector( + internalMessageStream, MessageRenderer.GRADLE_STYLE, verbose + ) + + return convertKotlinExitCode( + compiler.exec(compilerMessageCollector, Services.EMPTY, args) + ) + } + + @OptIn(ExperimentalCompilerApi::class) + protected fun getResourcesPath(): String { + val resourceName = "META-INF/services/org.jetbrains.kotlin.compiler.plugin.ComponentRegistrar" + return this::class.java.classLoader.getResources(resourceName) + .asSequence() + .mapNotNull { url -> + val uri = URI.create(url.toString().removeSuffix("/$resourceName")) + when (uri.scheme) { + "jar" -> Paths.get(URI.create(uri.schemeSpecificPart.removeSuffix("!"))) + "file" -> Paths.get(uri) + else -> return@mapNotNull null + }.toAbsolutePath() + } + .find { resourcesPath -> + ServiceLoaderLite.findImplementations(ComponentRegistrar::class.java, listOf(resourcesPath.toFile())) + .any { implementation -> implementation == MainComponentRegistrar::class.java.name } + }?.toString() ?: throw AssertionError("Could not get path to ComponentRegistrar service from META-INF") + } + + /** Searches compiler log for known errors that are hard to debug for the user */ + protected fun searchSystemOutForKnownErrors(compilerSystemOut: String) { + if (compilerSystemOut.contains("No enum constant com.sun.tools.javac.main.Option.BOOT_CLASS_PATH")) { + warn( + "${this::class.simpleName} has detected that the compiler output contains an error message that may be " + + "caused by including a tools.jar file together with a JDK of version 9 or later. " + + if (inheritClassPath) + "Make sure that no tools.jar (or unwanted JDK) is in the inherited classpath" + else "" + ) + } + + if (compilerSystemOut.contains("Unable to find package java.")) { + warn( + "${this::class.simpleName} has detected that the compiler output contains an error message " + + "that may be caused by a missing JDK. This can happen if jdkHome=null and inheritClassPath=false." + ) + } + } + + protected val hostClasspaths by lazy { HostEnvironment.classpath } + + /* This internal buffer and stream is used so it can be easily converted to a string + that is put into the [Result] object, in addition to printing immediately to the user's + stream. */ + protected val internalMessageBuffer = Buffer() + protected val internalMessageStream = PrintStream( + TeeOutputStream( + object : OutputStream() { + override fun write(b: Int) = messageOutputStream.write(b) + override fun write(b: ByteArray) = messageOutputStream.write(b) + override fun write(b: ByteArray, off: Int, len: Int) = messageOutputStream.write(b, off, len) + override fun flush() = messageOutputStream.flush() + override fun close() = messageOutputStream.close() + }, + internalMessageBuffer.outputStream() + ) + ) + + protected fun log(s: String) { + if (verbose) + internalMessageStream.println("logging: $s") + } + + protected fun warn(s: String) = internalMessageStream.println("warning: $s") + protected fun error(s: String) = internalMessageStream.println("error: $s") + + internal val internalMessageStreamAccess: PrintStream get() = internalMessageStream +} + +internal fun convertKotlinExitCode(code: ExitCode) = when(code) { + ExitCode.OK -> KotlinCompilation.ExitCode.OK + ExitCode.OOM_ERROR, + ExitCode.INTERNAL_ERROR -> KotlinCompilation.ExitCode.INTERNAL_ERROR + ExitCode.COMPILATION_ERROR -> KotlinCompilation.ExitCode.COMPILATION_ERROR + ExitCode.SCRIPT_EXECUTION_ERROR -> KotlinCompilation.ExitCode.SCRIPT_EXECUTION_ERROR + ExitCode.OOM_ERROR -> throw OutOfMemoryError("Kotlin compiler ran out of memory") +} diff --git a/inject-kotlin-test/src/main/kotlin/io/micronaut/annotation/processing/test/support/DefaultPropertyDelegate.kt b/inject-kotlin-test/src/main/kotlin/io/micronaut/annotation/processing/test/support/DefaultPropertyDelegate.kt new file mode 100644 index 00000000000..5e7bf37a777 --- /dev/null +++ b/inject-kotlin-test/src/main/kotlin/io/micronaut/annotation/processing/test/support/DefaultPropertyDelegate.kt @@ -0,0 +1,48 @@ +/* + * Copyright 2017-2023 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.annotation.processing.test.support + +import kotlin.properties.ReadWriteProperty +import kotlin.reflect.KProperty + +@Suppress("MemberVisibilityCanBePrivate") +internal class DefaultPropertyDelegate(private val createDefault: () -> T) : ReadWriteProperty { + val hasDefaultValue + @Synchronized get() = (value == DEFAULT) + + private var value: Any? = DEFAULT + val defaultValue by lazy { createDefault() } + + @Synchronized + override operator fun getValue(thisRef: R, property: KProperty<*>): T { + @Suppress("UNCHECKED_CAST") + return if(hasDefaultValue) + defaultValue + else + value as T + } + + @Synchronized + override operator fun setValue(thisRef: R, property: KProperty<*>, value: T) { + this.value = value + } + + companion object { + private object DEFAULT + } +} + +internal fun default(createDefault: () -> T) = DefaultPropertyDelegate(createDefault) diff --git a/inject-kotlin-test/src/main/kotlin/io/micronaut/annotation/processing/test/support/HostEnvironment.kt b/inject-kotlin-test/src/main/kotlin/io/micronaut/annotation/processing/test/support/HostEnvironment.kt new file mode 100644 index 00000000000..efed0491926 --- /dev/null +++ b/inject-kotlin-test/src/main/kotlin/io/micronaut/annotation/processing/test/support/HostEnvironment.kt @@ -0,0 +1,81 @@ +/* + * Copyright 2017-2023 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.annotation.processing.test.support + +import io.github.classgraph.ClassGraph +import java.io.File + +/** + * Utility object to provide everything we might discover from the host environment. + */ +internal object HostEnvironment { + val classpath by lazy { + getHostClasspaths() + } + + val kotlinStdLibJar: File? by lazy { + findInClasspath(kotlinDependencyRegex("(kotlin-stdlib|kotlin-runtime)")) + } + + val kotlinStdLibCommonJar: File? by lazy { + findInClasspath(kotlinDependencyRegex("kotlin-stdlib-common")) + } + + val kotlinStdLibJdkJar: File? by lazy { + findInClasspath(kotlinDependencyRegex("kotlin-stdlib-jdk[0-9]+")) + } + + val kotlinStdLibJsJar: File? by default { + findInClasspath(kotlinDependencyRegex("kotlin-stdlib-js")) + } + + val kotlinReflectJar: File? by lazy { + findInClasspath(kotlinDependencyRegex("kotlin-reflect")) + } + + val kotlinScriptRuntimeJar: File? by lazy { + findInClasspath(kotlinDependencyRegex("kotlin-script-runtime")) + } + + val toolsJar: File? by lazy { + findInClasspath(Regex("tools.jar")) + } + + private fun kotlinDependencyRegex(prefix: String): Regex { + return Regex("$prefix(-[0-9]+\\.[0-9]+(\\.[0-9]+)?)([-0-9a-zA-Z]+)?\\.jar") + } + + /** Tries to find a file matching the given [regex] in the host process' classpath */ + private fun findInClasspath(regex: Regex): File? { + val jarFile = classpath.firstOrNull { classpath -> + classpath.name.matches(regex) + //TODO("check that jar file actually contains the right classes") + } + return jarFile + } + + /** Returns the files on the classloader's classpath and modulepath */ + private fun getHostClasspaths(): List { + val classGraph = ClassGraph() + .enableSystemJarsAndModules() + .removeTemporaryFilesAfterScan() + + val classpaths = classGraph.classpathFiles + val modules = classGraph.modules.mapNotNull { it.locationFile } + + return (classpaths + modules).distinctBy(File::getAbsolutePath) + } +} diff --git a/inject-kotlin-test/src/main/kotlin/io/micronaut/annotation/processing/test/support/JavacUtils.kt b/inject-kotlin-test/src/main/kotlin/io/micronaut/annotation/processing/test/support/JavacUtils.kt new file mode 100644 index 00000000000..dd4864c795d --- /dev/null +++ b/inject-kotlin-test/src/main/kotlin/io/micronaut/annotation/processing/test/support/JavacUtils.kt @@ -0,0 +1,127 @@ +/* + * Copyright 2017-2023 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.annotation.processing.test.support + +import okio.Buffer +import java.io.* +import java.net.URI +import java.nio.charset.Charset +import javax.tools.JavaCompiler +import javax.tools.JavaFileObject +import javax.tools.SimpleJavaFileObject +import kotlin.IllegalArgumentException + +/** + * A [JavaFileObject] created from a source [File]. + * + * Used for interfacing with javac ([JavaCompiler]). + */ +internal class FileJavaFileObject(val sourceFile: File, val charset: Charset = Charset.defaultCharset()) + : SimpleJavaFileObject(sourceFile.toURI(), + deduceKind(sourceFile.toURI()) +) { + + init { + require(sourceFile.isFile) + require(sourceFile.canRead()) + } + + companion object { + private fun deduceKind(uri: URI): JavaFileObject.Kind + = JavaFileObject.Kind.values().firstOrNull { + uri.path.endsWith(it.extension, ignoreCase = true) + } ?: JavaFileObject.Kind.OTHER + } + + override fun openInputStream(): InputStream = sourceFile.inputStream() + + override fun getCharContent(ignoreEncodingErrors: Boolean): CharSequence + = sourceFile.readText(charset) +} + +/** + * A [JavaFileObject] created from a [String]. + * + * Used for interfacing with javac ([JavaCompiler]). + */ +internal class StringJavaFileObject(className: String, private val contents: String) + : SimpleJavaFileObject( + URI.create("string:///" + className.replace('.', '/') + JavaFileObject.Kind.SOURCE.extension), + JavaFileObject.Kind.SOURCE +){ + private var _lastModified = System.currentTimeMillis() + + override fun getCharContent(ignoreEncodingErrors: Boolean): CharSequence = contents + + override fun openInputStream(): InputStream + = ByteArrayInputStream(contents.toByteArray(Charset.defaultCharset())) + + override fun openReader(ignoreEncodingErrors: Boolean): Reader = StringReader(contents) + + override fun getLastModified(): Long = _lastModified +} + +/** + * Gets the version string of a javac executable that can be started using the + * given [javacCommand] via [Runtime.exec]. + */ +internal fun getJavacVersionString(javacCommand: String): String { + val javacProc = ProcessBuilder(listOf(javacCommand, "-version")) + .redirectErrorStream(true) + .start() + + val buffer = Buffer() + + javacProc.inputStream.copyTo(buffer.outputStream()) + javacProc.waitFor() + + val output = buffer.readUtf8() + + return parseVersionString(output) ?: throw IllegalStateException( + "Command '$javacCommand -version' did not print expected output. " + + "Output was: '$output'" + ) +} + +internal fun parseVersionString(output: String) = + Regex("javac (.*)?[\\s\\S]*").find(output)?.destructured?.component1() + +internal fun isJavac9OrLater(javacVersionString: String): Boolean { + try { + val (majorv, minorv, patchv, otherv) = Regex("([0-9]*)(?:\\.([0-9]*))?(?:\\.([0-9]*))?(.*)") + .matchEntire(javacVersionString)?.destructured + ?: throw IllegalArgumentException("Could not match version regex") + + check(majorv.isNotBlank()) { "Major version can not be blank" } + + if (majorv.toInt() == 1) + check(minorv.isNotBlank()) { "Minor version can not be blank if major version is 1" } + + return (majorv.toInt() == 1 && minorv.toInt() >= 9) // old versioning scheme: 1.8.x + || (majorv.toInt() >= 9) // new versioning scheme: 10.x.x + } + catch (t: Throwable) { + throw IllegalArgumentException("Could not parse javac version string: '$javacVersionString'", t) + } +} + +/** Finds the tools.jar given a path to a JDK 8 or earlier */ +internal fun findToolsJarFromJdk(jdkHome: File): File { + return jdkHome.resolve("../lib/tools.jar").existsOrNull() + ?: jdkHome.resolve("lib/tools.jar").existsOrNull() + ?: jdkHome.resolve("tools.jar").existsOrNull() + ?: throw IllegalStateException("Can not find tools.jar from JDK with path ${jdkHome.absolutePath}") +} diff --git a/inject-kotlin-test/src/main/kotlin/io/micronaut/annotation/processing/test/support/KotlinCompilation.kt b/inject-kotlin-test/src/main/kotlin/io/micronaut/annotation/processing/test/support/KotlinCompilation.kt new file mode 100644 index 00000000000..d61599a0231 --- /dev/null +++ b/inject-kotlin-test/src/main/kotlin/io/micronaut/annotation/processing/test/support/KotlinCompilation.kt @@ -0,0 +1,608 @@ +/* + * Copyright 2017-2018 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.annotation.processing.test.support + +import org.jetbrains.kotlin.cli.common.arguments.K2JVMCompilerArguments +import org.jetbrains.kotlin.cli.common.messages.MessageRenderer +import org.jetbrains.kotlin.cli.common.messages.PrintingMessageCollector +import org.jetbrains.kotlin.cli.jvm.K2JVMCompiler +import org.jetbrains.kotlin.compiler.plugin.ExperimentalCompilerApi +import org.jetbrains.kotlin.config.JVMAssertionsMode +import org.jetbrains.kotlin.config.JvmDefaultMode +import org.jetbrains.kotlin.config.JvmTarget +import org.jetbrains.kotlin.config.Services +import java.io.* +import java.lang.RuntimeException +import java.net.URLClassLoader +import java.nio.file.Path +import javax.annotation.processing.Processor +import javax.tools.* + +data class PluginOption(val pluginId: PluginId, val optionName: OptionName, val optionValue: OptionValue) + +typealias PluginId = String +typealias OptionName = String +typealias OptionValue = String + +@Suppress("MemberVisibilityCanBePrivate") +class KotlinCompilation : AbstractKotlinCompilation() { + /** Arbitrary arguments to be passed to kapt */ + var kaptArgs: MutableMap = mutableMapOf() + + /** Annotation processors to be passed to kapt */ + var annotationProcessors: List = emptyList() + + /** Include Kotlin runtime in to resulting .jar */ + var includeRuntime: Boolean = false + + /** Name of the generated .kotlin_module file */ + var moduleName: String? = null + + /** Target version of the generated JVM bytecode */ + var jvmTarget: String = JvmTarget.DEFAULT.description + + /** Generate metadata for Java 1.8 reflection on method parameters */ + var javaParameters: Boolean = false + + /** Use the IR backend */ + var useIR: Boolean = false + + /** Use the old JVM backend */ + var useOldBackend: Boolean = false + + /** Paths where to find Java 9+ modules */ + var javaModulePath: Path? = null + + /** + * Root modules to resolve in addition to the initial modules, + * or all modules on the module path if is ALL-MODULE-PATH + */ + var additionalJavaModules: MutableList = mutableListOf() + + /** Don't generate not-null assertions for arguments of platform types */ + var noCallAssertions: Boolean = false + + /** Don't generate not-null assertion for extension receiver arguments of platform types */ + var noReceiverAssertions: Boolean = false + + /** Don't generate not-null assertions on parameters of methods accessible from Java */ + var noParamAssertions: Boolean = false + + /** Disable optimizations */ + var noOptimize: Boolean = false + + /** Assert calls behaviour {always-enable|always-disable|jvm|legacy} */ + var assertionsMode: String? = JVMAssertionsMode.DEFAULT.description + + /** Path to the .xml build file to compile */ + var buildFile: File? = null + + /** Compile multifile classes as a hierarchy of parts and facade */ + var inheritMultifileParts: Boolean = false + + /** Use type table in metadata serialization */ + var useTypeTable: Boolean = false + + /** Path to JSON file to dump Java to Kotlin declaration mappings */ + var declarationsOutputPath: File? = null + + /** Suppress the \"cannot access built-in declaration\" error (useful with -no-stdlib) */ + var suppressMissingBuiltinsError: Boolean = false + + /** Script resolver environment in key-value pairs (the value could be quoted and escaped) */ + var scriptResolverEnvironment: MutableMap = mutableMapOf() + + /** Java compiler arguments */ + var javacArguments: MutableList = mutableListOf() + + /** Package prefix for Java files */ + var javaPackagePrefix: String? = null + + /** + * Specify behavior for Checker Framework compatqual annotations (NullableDecl/NonNullDecl). + * Default value is 'enable' + */ + var supportCompatqualCheckerFrameworkAnnotations: String? = null + + /** Allow to use '@JvmDefault' annotation for JVM default method support. + * {disable|enable|compatibility} + * */ + var jvmDefault: String = JvmDefaultMode.DEFAULT.description + + /** Generate metadata with strict version semantics (see kdoc on Metadata.extraInt) */ + var strictMetadataVersionSemantics: Boolean = false + + /** + * Transform '(' and ')' in method names to some other character sequence. + * This mode can BREAK BINARY COMPATIBILITY and is only supposed to be used as a workaround + * of an issue in the ASM bytecode framework. See KT-29475 for more details + */ + var sanitizeParentheses: Boolean = false + + /** Paths to output directories for friend modules (whose internals should be visible) */ + var friendPaths: List = emptyList() + + /** + * Path to the JDK to be used + * + * If null, no JDK will be used with kotlinc (option -no-jdk) + * and the system java compiler will be used with empty bootclasspath + * (on JDK8) or --system none (on JDK9+). This can be useful if all + * the JDK classes you need are already on the (inherited) classpath. + * */ + var jdkHome: File? by default { processJdkHome } + + /** + * Path to the kotlin-stdlib.jar + * If none is given, it will be searched for in the host + * process' classpaths + */ + var kotlinStdLibJar: File? by default { + HostEnvironment.kotlinStdLibJar + } + + /** + * Path to the kotlin-stdlib-jdk*.jar + * If none is given, it will be searched for in the host + * process' classpaths + */ + var kotlinStdLibJdkJar: File? by default { + HostEnvironment.kotlinStdLibJdkJar + } + + /** + * Path to the kotlin-reflect.jar + * If none is given, it will be searched for in the host + * process' classpaths + */ + var kotlinReflectJar: File? by default { + HostEnvironment.kotlinReflectJar + } + + /** + * Path to the kotlin-script-runtime.jar + * If none is given, it will be searched for in the host + * process' classpaths + */ + var kotlinScriptRuntimeJar: File? by default { + HostEnvironment.kotlinScriptRuntimeJar + } + + /** + * Path to the tools.jar file needed for kapt when using a JDK 8. + * + * Note: Using a tools.jar file with a JDK 9 or later leads to an + * internal compiler error! + */ + var toolsJar: File? by default { + if (!isJdk9OrLater()) + jdkHome?.let { findToolsJarFromJdk(it) } + ?: HostEnvironment.toolsJar + else + null + } + + // *.class files, Jars and resources (non-temporary) that are created by the + // compilation will land here + val classesDir get() = workingDir.resolve("classes") + + // Base directory for kapt stuff + private val kaptBaseDir get() = workingDir.resolve("kapt") + + // Java annotation processors that are compile by kapt will put their generated files here + val kaptSourceDir get() = kaptBaseDir.resolve("sources") + + // Output directory for Kotlin source files generated by kapt + val kaptKotlinGeneratedDir get() = kaptArgs[OPTION_KAPT_KOTLIN_GENERATED] + ?.let { path -> + require(File(path).isDirectory) { "$OPTION_KAPT_KOTLIN_GENERATED must be a directory" } + File(path) + } + ?: File(kaptBaseDir, "kotlinGenerated") + + val kaptStubsDir get() = kaptBaseDir.resolve("stubs") + val kaptIncrementalDataDir get() = kaptBaseDir.resolve("incrementalData") + + /** ExitCode of the entire Kotlin compilation process */ + enum class ExitCode { + OK, INTERNAL_ERROR, COMPILATION_ERROR, SCRIPT_EXECUTION_ERROR + } + + /** Result of the compilation */ + inner class Result( + /** The exit code of the compilation */ + val exitCode: ExitCode, + /** Messages that were printed by the compilation */ + val messages: String + ) { + /** class loader to load the compile classes */ + val classLoader = URLClassLoader( + // Include the original classpaths and the output directory to be able to load classes from dependencies. + classpaths.plus(outputDirectory).map { it.toURI().toURL() }.toTypedArray(), + this::class.java.classLoader + ) + + /** The directory where only the final output class and resources files will be */ + val outputDirectory: File get() = classesDir + + } + + + // setup common arguments for the two kotlinc calls + private fun commonK2JVMArgs() = commonArguments(K2JVMCompilerArguments()) { args -> + args.destination = classesDir.absolutePath + args.classpath = commonClasspaths().joinToString(separator = File.pathSeparator) + + if(jdkHome != null) { + args.jdkHome = jdkHome!!.absolutePath + } + else { + log("Using option -no-jdk. Kotlinc won't look for a JDK.") + args.noJdk = true + } + + args.includeRuntime = includeRuntime + + // the compiler should never look for stdlib or reflect in the + // kotlinHome directory (which is null anyway). We will put them + // in the classpath manually if they're needed + args.noStdlib = true + args.noReflect = true + + if(moduleName != null) + args.moduleName = moduleName + + args.jvmTarget = jvmTarget + args.javaParameters = javaParameters + args.useIR = useIR + args.useOldBackend = useOldBackend + + if(javaModulePath != null) + args.javaModulePath = javaModulePath!!.toString() + + args.additionalJavaModules = additionalJavaModules.map(File::getAbsolutePath).toTypedArray() + args.noCallAssertions = noCallAssertions + args.noParamAssertions = noParamAssertions + args.noReceiverAssertions = noReceiverAssertions + + args.noOptimize = noOptimize + + if(assertionsMode != null) + args.assertionsMode = assertionsMode + + if(buildFile != null) + args.buildFile = buildFile!!.toString() + + args.inheritMultifileParts = inheritMultifileParts + args.useTypeTable = useTypeTable + + if(declarationsOutputPath != null) + args.declarationsOutputPath = declarationsOutputPath!!.toString() + + if(javacArguments.isNotEmpty()) + args.javacArguments = javacArguments.toTypedArray() + + if(supportCompatqualCheckerFrameworkAnnotations != null) + args.supportCompatqualCheckerFrameworkAnnotations = supportCompatqualCheckerFrameworkAnnotations + + args.jvmDefault = jvmDefault + args.strictMetadataVersionSemantics = strictMetadataVersionSemantics + args.sanitizeParentheses = sanitizeParentheses + + if(friendPaths.isNotEmpty()) + args.friendPaths = friendPaths.map(File::getAbsolutePath).toTypedArray() + + if(scriptResolverEnvironment.isNotEmpty()) + args.scriptResolverEnvironment = scriptResolverEnvironment.map { (key, value) -> "$key=\"$value\"" }.toTypedArray() + + args.javaPackagePrefix = javaPackagePrefix + args.suppressMissingBuiltinsError = suppressMissingBuiltinsError + } + + /** Performs the 1st and 2nd compilation step to generate stubs and run annotation processors */ + @OptIn(ExperimentalCompilerApi::class) + private fun stubsAndApt(sourceFiles: List): ExitCode { + if(annotationProcessors.isEmpty()) { + log("No services were given. Not running kapt steps.") + return ExitCode.OK + } + + + val compilerMessageCollector = PrintingMessageCollector( + internalMessageStream, MessageRenderer.GRADLE_STYLE, verbose + ) + + /** The main compiler plugin (MainComponentRegistrar) + * is instantiated by K2JVMCompiler using + * a service locator. So we can't just pass parameters to it easily. + * Instead, we need to use a thread-local global variable to pass + * any parameters that change between compilations + * + */ + MainComponentRegistrar.threadLocalParameters.set( + MainComponentRegistrar.ThreadLocalParameters( + compilerPlugins + ) + ) + + val kotlinSources = sourceFiles.filter(File::hasKotlinFileExtension) + val javaSources = sourceFiles.filter(File::hasJavaFileExtension) + + val sourcePaths = mutableListOf().apply { + addAll(javaSources) + + if(kotlinSources.isNotEmpty()) { + addAll(kotlinSources) + } + else { + /* __HACK__: The K2JVMCompiler expects at least one Kotlin source file or it will crash. + We still need kapt to run even if there are no Kotlin sources because it executes APs + on Java sources as well. Alternatively we could call the JavaCompiler instead of kapt + to do annotation processing when there are only Java sources, but that's quite a lot + of work (It can not be done in the compileJava step because annotation processors on + Java files might generate Kotlin files which then need to be compiled in the + compileKotlin step before the compileJava step). So instead we trick K2JVMCompiler + by just including an empty .kt-File. */ + add(SourceFile.new("emptyKotlinFile.kt", "").writeIfNeeded(kaptBaseDir)) + } + }.map(File::getAbsolutePath).distinct() + + if(!isJdk9OrLater()) { + try { + Class.forName("com.sun.tools.javac.util.Context") + } + catch (e: ClassNotFoundException) { + require(toolsJar != null) { + "toolsJar must not be null on JDK 8 or earlier if it's classes aren't already on the classpath" + } + + require(toolsJar!!.exists()) { "toolsJar file does not exist" } + (ClassLoader.getSystemClassLoader() as URLClassLoader).addUrl(toolsJar!!.toURI().toURL()) + } + } + + if (pluginClasspaths.isNotEmpty()) + warn("Included plugins in pluginsClasspaths will be executed twice.") + + val k2JvmArgs = commonK2JVMArgs().also { + it.freeArgs = sourcePaths + it.pluginClasspaths = (it.pluginClasspaths ?: emptyArray()) + arrayOf(getResourcesPath()) + } + + return convertKotlinExitCode( + K2JVMCompiler().exec(compilerMessageCollector, Services.EMPTY, k2JvmArgs) + ) + } + + /** Performs the 3rd compilation step to compile Kotlin source files */ + private fun compileJvmKotlin(sourceFiles: List): ExitCode { + val sources = sourceFiles + + kaptKotlinGeneratedDir.listFilesRecursively() + + kaptSourceDir.listFilesRecursively() + + return compileKotlin(sources, K2JVMCompiler(), commonK2JVMArgs()) + } + + /** + * Base javac arguments that only depend on the the arguments given by the user + * Depending on which compiler implementation is actually used, more arguments + * may be added + */ + private fun baseJavacArgs(isJavac9OrLater: Boolean) = mutableListOf().apply { + if(verbose) { + add("-verbose") + add("-Xlint:path") // warn about invalid paths in CLI + add("-Xlint:options") // warn about invalid options in CLI + + if(isJavac9OrLater) + add("-Xlint:module") // warn about issues with the module system + } + + addAll("-d", classesDir.absolutePath) + + add("-proc:none") // disable annotation processing + + if(allWarningsAsErrors) + add("-Werror") + + addAll(javacArguments) + + // also add class output path to javac classpath so it can discover + // already compiled Kotlin classes + addAll("-cp", (commonClasspaths() + classesDir) + .joinToString(File.pathSeparator, transform = File::getAbsolutePath)) + } + + /** Performs the 4th compilation step to compile Java source files */ + private fun compileJava(sourceFiles: List): ExitCode { + val javaSources = (sourceFiles + kaptSourceDir.listFilesRecursively()) + .filterNot(File::hasKotlinFileExtension) + + if(javaSources.isEmpty()) + return ExitCode.OK + + if(jdkHome != null && jdkHome!!.canonicalPath != processJdkHome.canonicalPath) { + /* If a JDK home is given, try to run javac from there so it uses the same JDK + as K2JVMCompiler. Changing the JDK of the system java compiler via the + "--system" and "-bootclasspath" options is not so easy. + If the jdkHome is the same as the current process, we still run an in process compilation because it is + expensive to fork a process to compile. + */ + log("compiling java in a sub-process because a jdkHome is specified") + val jdkBinFile = File(jdkHome, "bin") + check(jdkBinFile.exists()) { "No JDK bin folder found at: ${jdkBinFile.toPath()}" } + + val javacCommand = jdkBinFile.absolutePath + File.separatorChar + "javac" + + val isJavac9OrLater = isJavac9OrLater(getJavacVersionString(javacCommand)) + val javacArgs = baseJavacArgs(isJavac9OrLater) + + val javacProc = ProcessBuilder(listOf(javacCommand) + javacArgs + javaSources.map(File::getAbsolutePath)) + .directory(workingDir) + .redirectErrorStream(true) + .start() + + javacProc.inputStream.copyTo(internalMessageStream) + javacProc.errorStream.copyTo(internalMessageStream) + + return when(javacProc.waitFor()) { + 0 -> ExitCode.OK + 1 -> ExitCode.COMPILATION_ERROR + else -> ExitCode.INTERNAL_ERROR + } + } + else { + /* If no JDK is given, we will use the host process' system java compiler. + If it is set to `null`, we will erase the bootclasspath. The user is then on their own to somehow + provide the JDK classes via the regular classpath because javac won't + work at all without them */ + log("jdkHome is not specified. Using system java compiler of the host process.") + val isJavac9OrLater = isJdk9OrLater() + val javacArgs = baseJavacArgs(isJavac9OrLater).apply { + if (jdkHome == null) { + log("jdkHome is set to null, removing boot classpath from java compilation") + // erase bootclasspath or JDK path because no JDK was specified + if (isJavac9OrLater) + addAll("--system", "none") + else + addAll("-bootclasspath", "") + } + } + + val javac = SynchronizedToolProvider.systemJavaCompiler + val javaFileManager = javac.getStandardFileManager(null, null, null) + val diagnosticCollector = DiagnosticCollector() + + fun printDiagnostics() = diagnosticCollector.diagnostics.forEach { diag -> + when(diag.kind) { + Diagnostic.Kind.ERROR -> error(diag.getMessage(null)) + Diagnostic.Kind.WARNING, + Diagnostic.Kind.MANDATORY_WARNING -> warn(diag.getMessage(null)) + else -> log(diag.getMessage(null)) + } + } + + try { + val noErrors = javac.getTask( + OutputStreamWriter(internalMessageStream), javaFileManager, + diagnosticCollector, javacArgs, + /* classes to be annotation processed */ null, + javaSources.map { FileJavaFileObject(it) } + .filter { it.kind == JavaFileObject.Kind.SOURCE } + ).call() + + printDiagnostics() + + return if(noErrors) + ExitCode.OK + else + ExitCode.COMPILATION_ERROR + } + catch (e: Exception) { + if(e is RuntimeException || e is IllegalArgumentException) { + printDiagnostics() + error(e.toString()) + return ExitCode.INTERNAL_ERROR + } + else + throw e + } + } + } + + /** Runs the compilation task */ + fun compile(): Result { + // make sure all needed directories exist + sourcesDir.mkdirs() + classesDir.mkdirs() + kaptSourceDir.mkdirs() + kaptStubsDir.mkdirs() + kaptIncrementalDataDir.mkdirs() + kaptKotlinGeneratedDir.mkdirs() + + // write given sources to working directory + val sourceFiles = sources.map { it.writeIfNeeded(sourcesDir) } + + pluginClasspaths.forEach { filepath -> + if (!filepath.exists()) { + error("Plugin $filepath not found") + return makeResult(ExitCode.INTERNAL_ERROR) + } + } + + /* + There are 4 steps to the compilation process: + 1. Generate stubs (using kotlinc with kapt plugin which does no further compilation) + 2. Run apt (using kotlinc with kapt plugin which does no further compilation) + 3. Run kotlinc with the normal Kotlin sources and Kotlin sources generated in step 2 + 4. Run javac with Java sources and the compiled Kotlin classes + */ + + /* Work around for warning that sometimes happens: + "Failed to initialize native filesystem for Windows + java.lang.RuntimeException: Could not find installation home path. + Please make sure bin/idea.properties is present in the installation directory" + See: https://github.com/arturbosch/detekt/issues/630 + */ + withSystemProperty("idea.use.native.fs.for.win", "false") { + // step 1 and 2: generate stubs and run annotation processors + try { + val exitCode = stubsAndApt(sourceFiles) + if (exitCode != ExitCode.OK) { + return makeResult(exitCode) + } + } finally { + MainComponentRegistrar.threadLocalParameters.remove() + } + + // step 3: compile Kotlin files + compileJvmKotlin(sourceFiles).let { exitCode -> + if(exitCode != ExitCode.OK) { + return makeResult(exitCode) + } + } + } + + // step 4: compile Java files + return makeResult(compileJava(sourceFiles)) + } + + private fun makeResult(exitCode: ExitCode): Result { + val messages = internalMessageBuffer.readUtf8() + + if(exitCode != ExitCode.OK) + searchSystemOutForKnownErrors(messages) + + return Result(exitCode, messages) + } + + private fun commonClasspaths() = mutableListOf().apply { + addAll(classpaths) + addAll(listOfNotNull(kotlinStdLibJar, kotlinStdLibCommonJar, kotlinStdLibJdkJar, + kotlinReflectJar, kotlinScriptRuntimeJar + )) + + if(inheritClassPath) { + addAll(hostClasspaths) + log("Inheriting classpaths: " + hostClasspaths.joinToString(File.pathSeparator)) + } + }.distinct() + + companion object { + const val OPTION_KAPT_KOTLIN_GENERATED = "kapt.kotlin.generated" + } +} + diff --git a/inject-kotlin-test/src/main/kotlin/io/micronaut/annotation/processing/test/support/Ksp.kt b/inject-kotlin-test/src/main/kotlin/io/micronaut/annotation/processing/test/support/Ksp.kt new file mode 100644 index 00000000000..1b1c95c1463 --- /dev/null +++ b/inject-kotlin-test/src/main/kotlin/io/micronaut/annotation/processing/test/support/Ksp.kt @@ -0,0 +1,246 @@ +/* + * Copyright 2017-2023 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.annotation.processing.test.support + +import com.google.devtools.ksp.AbstractKotlinSymbolProcessingExtension +import com.google.devtools.ksp.KspOptions +import com.google.devtools.ksp.processing.KSPLogger +import com.google.devtools.ksp.processing.SymbolProcessorProvider +import com.google.devtools.ksp.processing.impl.MessageCollectorBasedKSPLogger +import org.jetbrains.kotlin.cli.common.CLIConfigurationKeys +import org.jetbrains.kotlin.cli.common.messages.MessageRenderer +import org.jetbrains.kotlin.cli.common.messages.PrintingMessageCollector +import org.jetbrains.kotlin.cli.jvm.config.JavaSourceRoot +import org.jetbrains.kotlin.com.intellij.core.CoreApplicationEnvironment +import org.jetbrains.kotlin.com.intellij.mock.MockProject +import org.jetbrains.kotlin.com.intellij.psi.PsiTreeChangeAdapter +import org.jetbrains.kotlin.com.intellij.psi.PsiTreeChangeListener +import org.jetbrains.kotlin.compiler.plugin.ComponentRegistrar +import org.jetbrains.kotlin.compiler.plugin.ExperimentalCompilerApi +import org.jetbrains.kotlin.config.CompilerConfiguration +import org.jetbrains.kotlin.resolve.jvm.extensions.AnalysisHandlerExtension +import org.jetbrains.kotlin.utils.addToStdlib.firstIsInstanceOrNull +import java.io.File + +/** + * The list of symbol processors for the kotlin compilation. + * https://goo.gle/ksp + */ +var KotlinCompilation.symbolProcessorProviders: List + get() = getKspRegistrar().providers + set(value) { + val registrar = getKspRegistrar() + registrar.providers = value + } + +/** + * The directory where generated KSP sources are written + */ +val KotlinCompilation.kspSourcesDir: File + get() = kspWorkingDir.resolve("sources") + +/** + * Arbitrary arguments to be passed to ksp + */ +var KotlinCompilation.kspArgs: MutableMap + get() = getKspRegistrar().options + set(value) { + val registrar = getKspRegistrar() + registrar.options = value + } + +/** + * Controls for enabling incremental processing in KSP. + */ +var KotlinCompilation.kspIncremental: Boolean + get() = getKspRegistrar().incremental + set(value) { + val registrar = getKspRegistrar() + registrar.incremental = value + } + +/** + * Controls for enabling incremental processing logs in KSP. + */ +var KotlinCompilation.kspIncrementalLog: Boolean + get() = getKspRegistrar().incrementalLog + set(value) { + val registrar = getKspRegistrar() + registrar.incrementalLog = value + } + +/** + * Controls for enabling all warnings as errors in KSP. + */ +var KotlinCompilation.kspAllWarningsAsErrors: Boolean + get() = getKspRegistrar().allWarningsAsErrors + set(value) { + val registrar = getKspRegistrar() + registrar.allWarningsAsErrors = value + } + +/** + * Run processors and compilation in a single compiler invocation if true. + * See [com.google.devtools.ksp.KspCliOption.WITH_COMPILATION_OPTION]. + */ +var KotlinCompilation.kspWithCompilation: Boolean + get() = getKspRegistrar().withCompilation + set(value) { + val registrar = getKspRegistrar() + registrar.withCompilation = value + } + +private val KotlinCompilation.kspJavaSourceDir: File + get() = kspSourcesDir.resolve("java") + +private val KotlinCompilation.kspKotlinSourceDir: File + get() = kspSourcesDir.resolve("kotlin") + +private val KotlinCompilation.kspResources: File + get() = kspSourcesDir.resolve("resources") + +/** + * The working directory for KSP + */ +private val KotlinCompilation.kspWorkingDir: File + get() = workingDir.resolve("ksp") + +/** + * The directory where compiled KSP classes are written + */ +// TODO this seems to be ignored by KSP and it is putting classes into regular classes directory +// but we still need to provide it in the KSP options builder as it is required +// once it works, we should make the property public. +private val KotlinCompilation.kspClassesDir: File + get() = kspWorkingDir.resolve("classes") + +/** + * The directory where compiled KSP caches are written + */ +private val KotlinCompilation.kspCachesDir: File + get() = kspWorkingDir.resolve("caches") + +/** + * Custom subclass of [AbstractKotlinSymbolProcessingExtension] where processors are pre-defined instead of being + * loaded via ServiceLocator. + */ +private class KspTestExtension( + options: KspOptions, + processorProviders: List, + logger: KSPLogger +) : AbstractKotlinSymbolProcessingExtension( + options = options, + logger = logger, + testMode = false +) { + private val loadedProviders = processorProviders + + override fun loadProviders() = loadedProviders +} + +/** + * Registers the [KspTestExtension] to load the given list of processors. + */ +@OptIn(ExperimentalCompilerApi::class) +private class KspCompileTestingComponentRegistrar( + private val compilation: KotlinCompilation +) : ComponentRegistrar { + var providers = emptyList() + + var options: MutableMap = mutableMapOf() + + var incremental: Boolean = false + var incrementalLog: Boolean = false + var allWarningsAsErrors: Boolean = false + var withCompilation: Boolean = false + + override fun registerProjectComponents(project: MockProject, configuration: CompilerConfiguration) { + if (providers.isEmpty()) { + return + } + val options = KspOptions.Builder().apply { + this.projectBaseDir = compilation.kspWorkingDir + + this.processingOptions.putAll(compilation.kspArgs) + + this.incremental = this@KspCompileTestingComponentRegistrar.incremental + this.incrementalLog = this@KspCompileTestingComponentRegistrar.incrementalLog + this.allWarningsAsErrors = this@KspCompileTestingComponentRegistrar.allWarningsAsErrors + this.withCompilation = this@KspCompileTestingComponentRegistrar.withCompilation + + this.cachesDir = compilation.kspCachesDir.also { + it.deleteRecursively() + it.mkdirs() + } + this.kspOutputDir = compilation.kspSourcesDir.also { + it.deleteRecursively() + it.mkdirs() + } + this.classOutputDir = compilation.kspClassesDir.also { + it.deleteRecursively() + it.mkdirs() + } + this.javaOutputDir = compilation.kspJavaSourceDir.also { + it.deleteRecursively() + it.mkdirs() + } + this.kotlinOutputDir = compilation.kspKotlinSourceDir.also { + it.deleteRecursively() + it.mkdirs() + } + this.resourceOutputDir = compilation.kspResources.also { + it.deleteRecursively() + it.mkdirs() + } + configuration[CLIConfigurationKeys.CONTENT_ROOTS] + ?.filterIsInstance() + ?.forEach { + this.javaSourceRoots.add(it.file) + } + + }.build() + + // Temporary until friend-paths is fully supported https://youtrack.jetbrains.com/issue/KT-34102 + @Suppress("invisible_member") + val messageCollector = PrintingMessageCollector( + compilation.internalMessageStreamAccess, + MessageRenderer.GRADLE_STYLE, + compilation.verbose + ) + val messageCollectorBasedKSPLogger = MessageCollectorBasedKSPLogger( + messageCollector = messageCollector, + wrappedMessageCollector = messageCollector, + allWarningsAsErrors = allWarningsAsErrors + ) + val registrar = KspTestExtension(options, providers, messageCollectorBasedKSPLogger) + AnalysisHandlerExtension.registerExtension(project, registrar) + // Dummy extension point; Required by dropPsiCaches(). + CoreApplicationEnvironment.registerExtensionPoint(project.extensionArea, PsiTreeChangeListener.EP.name, PsiTreeChangeAdapter::class.java) + } +} + +/** + * Gets the test registrar from the plugin list or adds if it does not exist. + */ +@OptIn(ExperimentalCompilerApi::class) +private fun KotlinCompilation.getKspRegistrar(): KspCompileTestingComponentRegistrar { + compilerPlugins.firstIsInstanceOrNull()?.let { + return it + } + val kspRegistrar = KspCompileTestingComponentRegistrar(this) + compilerPlugins = compilerPlugins + kspRegistrar + return kspRegistrar +} diff --git a/inject-kotlin-test/src/main/kotlin/io/micronaut/annotation/processing/test/support/MainComponentRegistrar.kt b/inject-kotlin-test/src/main/kotlin/io/micronaut/annotation/processing/test/support/MainComponentRegistrar.kt new file mode 100644 index 00000000000..55986ba8d73 --- /dev/null +++ b/inject-kotlin-test/src/main/kotlin/io/micronaut/annotation/processing/test/support/MainComponentRegistrar.kt @@ -0,0 +1,61 @@ +/* + * Copyright 2017-2016 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.annotation.processing.test.support + +import org.jetbrains.kotlin.com.intellij.mock.MockProject +import org.jetbrains.kotlin.compiler.plugin.ComponentRegistrar +import org.jetbrains.kotlin.compiler.plugin.ExperimentalCompilerApi +import org.jetbrains.kotlin.config.CompilerConfiguration + +@OptIn(ExperimentalCompilerApi::class) +internal class MainComponentRegistrar : ComponentRegistrar { + + override fun registerProjectComponents(project: MockProject, configuration: CompilerConfiguration) { + // Handle unset parameters gracefully because this plugin may be accidentally called by other tools that + // discover it on the classpath (for example the kotlin jupyter kernel). + if (threadLocalParameters.get() == null) { + System.err.println("WARNING: MainComponentRegistrar::registerProjectComponents accessed before thread local parameters have been set") + return + } + + val parameters = threadLocalParameters.get() + + /* + The order of registering plugins here matters. If the kapt plugin is registered first, then + it will be executed first and any changes made to the AST by later plugins won't apply to the + generated stub files and thus won't be visible to any annotation processors. So we decided + to register third-party plugins before kapt and hope that it works, although we don't + know for sure if that is the correct way. + */ + parameters.compilerPlugins.forEach { componentRegistrar-> + componentRegistrar.registerProjectComponents(project,configuration) + } + + } + + companion object { + /** This compiler plugin is instantiated by K2JVMCompiler using + * a service locator. So we can't just pass parameters to it easily. + * Instead, we need to use a thread-local global variable to pass + * any parameters that change between compilations + */ + val threadLocalParameters: ThreadLocal = ThreadLocal() + } + + data class ThreadLocalParameters @OptIn(ExperimentalCompilerApi::class) constructor( + val compilerPlugins: List + ) +} diff --git a/inject-kotlin-test/src/main/kotlin/io/micronaut/annotation/processing/test/support/SourceFile.kt b/inject-kotlin-test/src/main/kotlin/io/micronaut/annotation/processing/test/support/SourceFile.kt new file mode 100644 index 00000000000..27a8eebdb84 --- /dev/null +++ b/inject-kotlin-test/src/main/kotlin/io/micronaut/annotation/processing/test/support/SourceFile.kt @@ -0,0 +1,75 @@ +/* + * Copyright 2017-2023 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.annotation.processing.test.support + +import okio.buffer +import okio.sink +import org.intellij.lang.annotations.Language +import java.io.File + +/** + * A source file for the [KotlinCompilation] + */ +abstract class SourceFile { + internal abstract fun writeIfNeeded(dir: File): File + + companion object { + /** + * Create a new Java source file for the compilation when the compilation is run + */ + fun java(name: String, @Language("java") contents: String, trimIndent: Boolean = true): SourceFile { + require(File(name).hasJavaFileExtension()) + val finalContents = if (trimIndent) contents.trimIndent() else contents + return new(name, finalContents) + } + + /** + * Create a new Kotlin source file for the compilation when the compilation is run + */ + fun kotlin(name: String, @Language("kotlin") contents: String, trimIndent: Boolean = true): SourceFile { + require(File(name).hasKotlinFileExtension()) + val finalContents = if (trimIndent) contents.trimIndent() else contents + return new(name, finalContents) + } + + /** + * Create a new source file for the compilation when the compilation is run + */ + fun new(name: String, contents: String) = object : SourceFile() { + override fun writeIfNeeded(dir: File): File { + val file = dir.resolve(name) + file.createNewFile() + + file.sink().buffer().use { + it.writeUtf8(contents) + } + + return file + } + } + + /** + * Compile an existing source file + */ + fun fromPath(path: File) = object : SourceFile() { + init { + require(path.isFile) + } + + override fun writeIfNeeded(dir: File): File = path + } + } +} diff --git a/inject-kotlin-test/src/main/kotlin/io/micronaut/annotation/processing/test/support/StreamUtils.kt b/inject-kotlin-test/src/main/kotlin/io/micronaut/annotation/processing/test/support/StreamUtils.kt new file mode 100644 index 00000000000..0eb9b13897c --- /dev/null +++ b/inject-kotlin-test/src/main/kotlin/io/micronaut/annotation/processing/test/support/StreamUtils.kt @@ -0,0 +1,69 @@ +/* + * Copyright 2017-2023 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.annotation.processing.test.support + +import java.io.* + + +/** A combined stream that writes to all the output streams in [streams]. */ +@Suppress("MemberVisibilityCanBePrivate") +internal class TeeOutputStream(val streams: Collection) : OutputStream() { + + constructor(vararg streams: OutputStream) : this(streams.toList()) + + @Synchronized + @Throws(IOException::class) + override fun write(b: Int) { + for(stream in streams) + stream.write(b) + } + + @Synchronized + @Throws(IOException::class) + override fun write(b: ByteArray) { + for(stream in streams) + stream.write(b) + } + + @Synchronized + @Throws(IOException::class) + override fun write(b: ByteArray, off: Int, len: Int) { + for(stream in streams) + stream.write(b, off, len) + } + + @Throws(IOException::class) + override fun flush() { + for(stream in streams) + stream.flush() + } + + @Throws(IOException::class) + override fun close() { + closeImpl(streams) + } + + @Throws(IOException::class) + private fun closeImpl(streamsToClose : Collection) { + try { + streamsToClose.firstOrNull()?.close() + } + finally { + if(streamsToClose.size > 1) + closeImpl(streamsToClose.drop(1)) + } + } +} diff --git a/inject-kotlin-test/src/main/kotlin/io/micronaut/annotation/processing/test/support/SynchronizedToolProvider.kt b/inject-kotlin-test/src/main/kotlin/io/micronaut/annotation/processing/test/support/SynchronizedToolProvider.kt new file mode 100644 index 00000000000..cf0b31d0ca5 --- /dev/null +++ b/inject-kotlin-test/src/main/kotlin/io/micronaut/annotation/processing/test/support/SynchronizedToolProvider.kt @@ -0,0 +1,51 @@ +/* + * Copyright 2017-2018 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.annotation.processing.test.support + +import java.lang.reflect.InvocationTargetException +import java.lang.reflect.Method +import javax.tools.JavaCompiler +import javax.tools.ToolProvider + + +/** + * ToolProvider has no synchronization internally, so if we don't synchronize from the outside we + * could wind up loading the compiler classes multiple times from different class loaders. + */ +internal object SynchronizedToolProvider { + private var getPlatformClassLoaderMethod: Method? = null + + val systemJavaCompiler: JavaCompiler + get() { + val compiler = synchronized(ToolProvider::class.java) { + ToolProvider.getSystemJavaCompiler() + } + + check(compiler != null) { "System java compiler is null! Are you running without JDK?" } + return compiler + } + + init { + if (isJdk9OrLater()) { + try { + getPlatformClassLoaderMethod = ClassLoader::class.java.getMethod("getPlatformClassLoader") + } catch (e: NoSuchMethodException) { + throw RuntimeException(e) + } + + } + } +} diff --git a/inject-kotlin-test/src/main/kotlin/io/micronaut/annotation/processing/test/support/Utils.kt b/inject-kotlin-test/src/main/kotlin/io/micronaut/annotation/processing/test/support/Utils.kt new file mode 100644 index 00000000000..eadb58f25ff --- /dev/null +++ b/inject-kotlin-test/src/main/kotlin/io/micronaut/annotation/processing/test/support/Utils.kt @@ -0,0 +1,88 @@ +/* + * Copyright 2017-2023 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.annotation.processing.test.support + +import java.io.File +import java.net.URL +import java.net.URLClassLoader +import javax.lang.model.SourceVersion + +internal fun MutableCollection.addAll(vararg elems: E) = addAll(elems) + +internal fun getJavaHome(): File { + val path = System.getProperty("java.home") + ?: System.getenv("JAVA_HOME") + ?: throw IllegalStateException("no java home found") + + return File(path).also { check(it.isDirectory) } +} + +internal val processJdkHome by lazy { + if(isJdk9OrLater()) + getJavaHome() + else + getJavaHome().parentFile +} + +/** Checks if the JDK of the host process is version 9 or later */ +internal fun isJdk9OrLater(): Boolean + = SourceVersion.latestSupported().compareTo(SourceVersion.RELEASE_8) > 0 + +internal fun File.listFilesRecursively(): List { + return listFiles().flatMap { file -> + if(file.isDirectory) + file.listFilesRecursively() + else + listOf(file) + } +} + +internal fun File.hasKotlinFileExtension() = hasFileExtension(listOf("kt", "kts")) + +internal fun File.hasJavaFileExtension() = hasFileExtension(listOf("java")) + +internal fun File.hasFileExtension(extensions: List) + = extensions.any{ it.equals(extension, ignoreCase = true) } + +internal fun URLClassLoader.addUrl(url: URL) { + val addUrlMethod = URLClassLoader::class.java.getDeclaredMethod("addURL", URL::class.java) + addUrlMethod.isAccessible = true + addUrlMethod.invoke(this, url) +} + +internal inline fun withSystemProperty(key: String, value: String, f: () -> T): T + = withSystemProperties(mapOf(key to value), f) + + +internal inline fun withSystemProperties(properties: Map, f: () -> T): T { + val previousProperties = mutableMapOf() + + for ((key, value) in properties) { + previousProperties[key] = System.getProperty(key) + System.setProperty(key, value) + } + + try { + return f() + } finally { + for ((key, value) in previousProperties) { + if (value != null) + System.setProperty(key, value) + } + } +} + +internal fun File.existsOrNull(): File? = if (exists()) this else null diff --git a/inject-kotlin-test/src/main/resources/META-INF/services/org.jetbrains.kotlin.compiler.plugin.ComponentRegistrar b/inject-kotlin-test/src/main/resources/META-INF/services/org.jetbrains.kotlin.compiler.plugin.ComponentRegistrar new file mode 100644 index 00000000000..0123c2d3f5a --- /dev/null +++ b/inject-kotlin-test/src/main/resources/META-INF/services/org.jetbrains.kotlin.compiler.plugin.ComponentRegistrar @@ -0,0 +1 @@ +io.micronaut.annotation.processing.test.support.MainComponentRegistrar diff --git a/inject-kotlin/build.gradle b/inject-kotlin/build.gradle index 2cf77e47a30..13d11b2098b 100644 --- a/inject-kotlin/build.gradle +++ b/inject-kotlin/build.gradle @@ -8,6 +8,7 @@ plugins { micronautBuild { core { usesMicronautTest() + usesMicronautTestKotest() } } @@ -34,7 +35,8 @@ dependencies { testImplementation libs.kotlinx.coroutines.core testImplementation libs.kotlinx.coroutines.jdk8 testImplementation libs.kotlinx.coroutines.rx2 - + testImplementation libs.micronaut.test.junit5 + testImplementation libs.kotlin.kotest.junit5 } afterEvaluate { diff --git a/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/annotation/KotlinAnnotationMetadataBuilder.kt b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/annotation/KotlinAnnotationMetadataBuilder.kt index c11710b38d4..5c8cf83d682 100644 --- a/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/annotation/KotlinAnnotationMetadataBuilder.kt +++ b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/annotation/KotlinAnnotationMetadataBuilder.kt @@ -56,7 +56,6 @@ class KotlinAnnotationMetadataBuilder(private val symbolProcessorEnvironment: Sy return if (type.qualifiedName != null) { type.qualifiedName!!.asString() } else { - println("Failed to get the qualified name of ${annotationMirror.shortName.asString()} annotation") annotationMirror.shortName.asString() } } @@ -124,11 +123,21 @@ class KotlinAnnotationMetadataBuilder(private val symbolProcessorEnvironment: Sy annotationMirrors.addAll(element.annotations) } else if (element is KSPropertyDeclaration) { val parent : KSClassDeclaration? = findClassDeclaration(element) - if (parent is KSClassDeclaration && parent.classKind == ClassKind.ANNOTATION_CLASS) { - annotationMirrors.addAll(element.annotations) - val getter = element.getter - if (getter != null) { - annotationMirrors.addAll(getter.annotations) + if (parent is KSClassDeclaration) { + if (parent.classKind == ClassKind.ANNOTATION_CLASS) { + annotationMirrors.addAll(element.annotations) + val getter = element.getter + if (getter != null) { + annotationMirrors.addAll(getter.annotations) + } + } else if (parent.modifiers.contains(Modifier.DATA)) { + val parameter = parent.primaryConstructor?.parameters?.find { it.name == element.simpleName } + if (parameter != null) { + annotationMirrors.addAll(parameter.annotations) + } + annotationMirrors.addAll(element.annotations) + } else { + annotationMirrors.addAll(element.annotations) } } else { annotationMirrors.addAll(element.annotations) @@ -294,13 +303,13 @@ class KotlinAnnotationMetadataBuilder(private val symbolProcessorEnvironment: Sy originatingElement: KSAnnotated ): Array? { var valueType = Any::class.java - val collection = annotationValue.map { + val collection = annotationValue.mapNotNull { val v = readAnnotationValue(originatingElement, it) if (v != null) { valueType = v.javaClass } v - } + } // annotation values can't be null return ArrayUtils.toArray(collection, valueType) } diff --git a/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/beans/BeanDefinitionProcessor.kt b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/beans/BeanDefinitionProcessor.kt index 7c722567658..c727f00069a 100644 --- a/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/beans/BeanDefinitionProcessor.kt +++ b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/beans/BeanDefinitionProcessor.kt @@ -97,13 +97,18 @@ class BeanDefinitionProcessor(private val environment: SymbolProcessorEnvironmen try { val outputVisitor = KotlinOutputVisitor(environment) val processed = HashSet() + var count = 0 for (beanDefinitionCreator in beanDefinitionMap.values) { for (writer in beanDefinitionCreator.build()) { if (processed.add(writer.beanDefinitionName)) { processBeanDefinitions(writer, outputVisitor, processed) + count++ } } } + if (count > 0) { + environment.logger.info("Created $count bean definitions") + } } catch (e: ProcessingException) { handleProcessingException(environment, e) } finally { diff --git a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/beans/BeanDefinitionSpec.groovy b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/beans/BeanDefinitionSpec.groovy index 4a530725f3e..33f3c2230a9 100644 --- a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/beans/BeanDefinitionSpec.groovy +++ b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/beans/BeanDefinitionSpec.groovy @@ -2,6 +2,7 @@ package io.micronaut.kotlin.processing.beans import io.micronaut.annotation.processing.test.KotlinCompiler import io.micronaut.context.ApplicationContext +import io.micronaut.context.exceptions.NoSuchBeanException import io.micronaut.core.annotation.AnnotationUtil import io.micronaut.core.annotation.Introspected import io.micronaut.core.annotation.Order @@ -19,6 +20,35 @@ import static io.micronaut.annotation.processing.test.KotlinCompiler.* class BeanDefinitionSpec extends Specification { + void "test bean annotated with @MicronautTest"() { + when: + def context = buildContext(''' +package test + +import io.micronaut.runtime.EmbeddedApplication +import io.micronaut.test.extensions.kotest5.annotation.MicronautTest +import io.kotest.core.spec.style.StringSpec + +@MicronautTest +class ExampleTest(private val application: EmbeddedApplication<*>): StringSpec({ + + "test the server is running" { + assert(application.isRunning) + } +}) +''') + + then: + context != null + + when: + getBean(context, 'test.ExampleTest') + + then: + def e = thrown(NoSuchBeanException) + e.message.contains("Bean of type [test.ExampleTest] is disabled") + } + void "test jvm field"() { given: def definition = KotlinCompiler.buildBeanDefinition('test.JvmFieldTest', ''' diff --git a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/visitor/KotlinReconstructionSpec.groovy b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/visitor/KotlinReconstructionSpec.groovy index 98c96816e1c..70c43a88bc3 100644 --- a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/visitor/KotlinReconstructionSpec.groovy +++ b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/visitor/KotlinReconstructionSpec.groovy @@ -38,6 +38,7 @@ class Test { ] } + @PendingFeature(reason = "Breaks because you can't use kotlin elements outside of a compilation execution") @Unroll("super type is #superType") def 'super type'() { given: @@ -66,6 +67,7 @@ abstract class Test : $superType() { ] } + @PendingFeature(reason = "Breaks because you can't use kotlin elements outside of a compilation execution") @Unroll("super interface is #superType") def 'super interface'() { given: From c178b99e2be4619da8ce95f991147fab029cbbf2 Mon Sep 17 00:00:00 2001 From: micronaut-build Date: Tue, 7 Feb 2023 16:12:53 +0000 Subject: [PATCH 462/743] Back to 3.8.5-SNAPSHOT --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 8cefb9c9c04..b43d3cc7ff6 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -projectVersion=3.8.4 +projectVersion=3.8.5-SNAPSHOT projectGroupId=io.micronaut title=Micronaut projectDesc=Natively Cloud Native From 4b739f0e8596bac4f83a4d4efef7519ad490cddd Mon Sep 17 00:00:00 2001 From: Tim Yates Date: Wed, 8 Feb 2023 09:29:02 +0000 Subject: [PATCH 463/743] ci: Run coretto on version named branches (#8722) --- .github/workflows/corretto.yml | 74 +++++++++++++++++++++------------- 1 file changed, 47 insertions(+), 27 deletions(-) diff --git a/.github/workflows/corretto.yml b/.github/workflows/corretto.yml index aacb77c2c95..7cd1b0dfbf0 100644 --- a/.github/workflows/corretto.yml +++ b/.github/workflows/corretto.yml @@ -2,37 +2,57 @@ name: Corretto CI on: push: branches: - - master + - '[1-9]+.[0-9]+.x' jobs: build: runs-on: ubuntu-latest strategy: matrix: java: ['8', '11', '17'] - container: amazoncorretto:${{ matrix.java }} steps: - - name: Display Java and Linux version - run: java -version && cat /etc/system-release - - name: Install tar && gzip - run: yum install -y tar gzip - - uses: actions/checkout@v3 - - uses: actions/cache@v3.0.2 - with: - path: ~/.gradle/caches - key: ${{ runner.os }}-micronaut-core-gradle-${{ hashFiles('**/*.gradle') }} - restore-keys: | - ${{ runner.os }}-micronaut-core-gradle- - - uses: actions/cache@v3.0.2 - with: - path: ~/.gradle/wrapper - key: ${{ runner.os }}-micronaut-core-wrapper-${{ hashFiles('**/*.gradle') }} - restore-keys: | - ${{ runner.os }}-micronaut-core-wrapper- - - name: Build with Gradle - env: - GH_TOKEN_PUBLIC_REPOS_READONLY: ${{ secrets.GH_TOKEN_PUBLIC_REPOS_READONLY }} - GH_USERNAME: ${{ secrets.GH_USERNAME }} - GRADLE_ENTERPRISE_ACCESS_KEY: ${{ secrets.GRADLE_ENTERPRISE_ACCESS_KEY }} - GRADLE_ENTERPRISE_CACHE_USERNAME: ${{ secrets.GRADLE_ENTERPRISE_CACHE_USERNAME }} - GRADLE_ENTERPRISE_CACHE_PASSWORD: ${{ secrets.GRADLE_ENTERPRISE_CACHE_PASSWORD }} - run: unset HOSTNAME ; LANG=en_US.utf-8 LC_ALL=en_US.utf-8 ./gradlew check --no-daemon --parallel --continue + # https://github.com/actions/virtual-environments/issues/709 + - name: Free disk space + run: | + sudo rm -rf "/usr/local/share/boost" + sudo rm -rf "$AGENT_TOOLSDIRECTORY" + sudo apt-get clean + df -h + - uses: actions/checkout@v3 + - name: Set up JDK + uses: actions/setup-java@v3 + with: + distribution: 'corretto' + java-version: ${{ matrix.java }} + - name: Setup Gradle + uses: gradle/gradle-build-action@v2.3.3 + - name: Optional setup step + env: + GRADLE_ENTERPRISE_ACCESS_KEY: ${{ secrets.GRADLE_ENTERPRISE_ACCESS_KEY }} + GRADLE_ENTERPRISE_CACHE_USERNAME: ${{ secrets.GRADLE_ENTERPRISE_CACHE_USERNAME }} + GRADLE_ENTERPRISE_CACHE_PASSWORD: ${{ secrets.GRADLE_ENTERPRISE_CACHE_PASSWORD }} + run: | + [ -f ./setup.sh ] && ./setup.sh || true + - name: Build with Gradle + id: gradle + run: | + ./gradlew check --no-daemon --parallel --continue + env: + GH_TOKEN_PUBLIC_REPOS_READONLY: ${{ secrets.GH_TOKEN_PUBLIC_REPOS_READONLY }} + GH_USERNAME: ${{ secrets.GH_USERNAME }} + TESTCONTAINERS_RYUK_DISABLED: true + GRADLE_ENTERPRISE_ACCESS_KEY: ${{ secrets.GRADLE_ENTERPRISE_ACCESS_KEY }} + GRADLE_ENTERPRISE_CACHE_USERNAME: ${{ secrets.GRADLE_ENTERPRISE_CACHE_USERNAME }} + GRADLE_ENTERPRISE_CACHE_PASSWORD: ${{ secrets.GRADLE_ENTERPRISE_CACHE_PASSWORD }} + PREDICTIVE_TEST_SELECTION: "${{ github.event_name == 'pull_request' && 'true' || 'false' }}" + - name: Add build scan URL as PR comment + uses: actions/github-script@v5 + if: github.event_name == 'pull_request' && failure() + with: + github-token: ${{secrets.GITHUB_TOKEN}} + script: | + github.rest.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body: '❌ ${{ github.workflow }} failed: ${{ steps.gradle.outputs.build-scan-url }}' + }) From b7ac9e8081a23d01241158367e69d3dfbed9ceaf Mon Sep 17 00:00:00 2001 From: Denis Stepanov Date: Wed, 8 Feb 2023 11:17:18 +0100 Subject: [PATCH 464/743] Correct messaging event publishing (#8723) --- messaging/build.gradle | 6 ++++ .../messaging/MessagingApplication.java | 16 +++++---- .../messaging/annotation/EventCatcher.java | 24 +++++++++++++ .../messaging/annotation/MessagingSpec.groovy | 34 +++++++++++++++++++ 4 files changed, 73 insertions(+), 7 deletions(-) create mode 100644 messaging/src/test/groovy/io/micronaut/messaging/annotation/EventCatcher.java create mode 100644 messaging/src/test/groovy/io/micronaut/messaging/annotation/MessagingSpec.groovy diff --git a/messaging/build.gradle b/messaging/build.gradle index 07d16a12528..b3263a661d2 100644 --- a/messaging/build.gradle +++ b/messaging/build.gradle @@ -2,6 +2,12 @@ plugins { id "io.micronaut.build.internal.convention-library" } +micronautBuild { + core { + usesMicronautTestSpock() + } +} + dependencies { annotationProcessor project(":inject-java") api project(":context") diff --git a/messaging/src/main/java/io/micronaut/messaging/MessagingApplication.java b/messaging/src/main/java/io/micronaut/messaging/MessagingApplication.java index 11530568a5a..1b93442fded 100644 --- a/messaging/src/main/java/io/micronaut/messaging/MessagingApplication.java +++ b/messaging/src/main/java/io/micronaut/messaging/MessagingApplication.java @@ -80,13 +80,15 @@ public boolean isServer() { @NonNull public MessagingApplication start() { ApplicationContext applicationContext = getApplicationContext(); - if (applicationContext != null && !applicationContext.isRunning()) { - try { - applicationContext.start(); - applicationContext.publishEvent(new ApplicationStartupEvent(this)); - } catch (Throwable e) { - throw new ApplicationStartupException("Error starting messaging server: " + e.getMessage(), e); + if (applicationContext != null) { + if (!applicationContext.isRunning()) { + try { + applicationContext.start(); + } catch (Throwable e) { + throw new ApplicationStartupException("Error starting messaging server: " + e.getMessage(), e); + } } + applicationContext.publishEvent(new ApplicationStartupEvent(this)); } return this; } @@ -96,8 +98,8 @@ public MessagingApplication start() { public MessagingApplication stop() { ApplicationContext applicationContext = getApplicationContext(); if (applicationContext != null && applicationContext.isRunning()) { - applicationContext.stop(); applicationContext.publishEvent(new ApplicationShutdownEvent(this)); + applicationContext.stop(); } return this; } diff --git a/messaging/src/test/groovy/io/micronaut/messaging/annotation/EventCatcher.java b/messaging/src/test/groovy/io/micronaut/messaging/annotation/EventCatcher.java new file mode 100644 index 00000000000..7cb653f9b42 --- /dev/null +++ b/messaging/src/test/groovy/io/micronaut/messaging/annotation/EventCatcher.java @@ -0,0 +1,24 @@ +package io.micronaut.messaging.annotation; + +import io.micronaut.runtime.event.ApplicationShutdownEvent; +import io.micronaut.runtime.event.ApplicationStartupEvent; +import io.micronaut.runtime.event.annotation.EventListener; +import jakarta.inject.Singleton; + +@Singleton +public class EventCatcher { + + boolean applicationStarted; + boolean applicationStopped; + + @EventListener + void on(ApplicationStartupEvent startupEvent) { + applicationStarted = true; + } + + @EventListener + void on(ApplicationShutdownEvent shutdownEvent) { + applicationStopped = true; + } + +} diff --git a/messaging/src/test/groovy/io/micronaut/messaging/annotation/MessagingSpec.groovy b/messaging/src/test/groovy/io/micronaut/messaging/annotation/MessagingSpec.groovy new file mode 100644 index 00000000000..f9d45359f5b --- /dev/null +++ b/messaging/src/test/groovy/io/micronaut/messaging/annotation/MessagingSpec.groovy @@ -0,0 +1,34 @@ +package io.micronaut.messaging.annotation + + +import io.micronaut.messaging.MessagingApplication +import io.micronaut.runtime.EmbeddedApplication +import io.micronaut.test.extensions.spock.annotation.MicronautTest +import jakarta.inject.Inject +import spock.lang.Specification + +@MicronautTest +class MessagingSpec extends Specification { + + @Inject + EmbeddedApplication embeddedApplication + + def 'embedded application is messaging'() { + expect: + embeddedApplication instanceof MessagingApplication + } + + void "events are properly published"() { + when: + def eventCatcher = embeddedApplication.getApplicationContext().getBean(EventCatcher) + then: + eventCatcher.applicationStarted + + when: + embeddedApplication.stop() + then: + eventCatcher.applicationStopped + !embeddedApplication.getApplicationContext().isRunning() + } + +} From b7b3749ef03ae6a298b070afcd87c7a944492cb2 Mon Sep 17 00:00:00 2001 From: Denis Stepanov Date: Wed, 8 Feb 2023 14:18:27 +0100 Subject: [PATCH 465/743] Groovy annotations and generic improvements (#8740) --------- Co-authored-by: Andriy Dmytruk --- .../AbstractAnnotationMetadataBuilder.java | 17 +- .../io/micronaut/inject/ast/ClassElement.java | 12 + .../micronaut/inject/ast/GenericElement.java | 43 ++ .../inject/ast/GenericPlaceholderElement.java | 2 +- .../micronaut/inject/ast/MethodElement.java | 33 ++ .../micronaut/inject/ast/WildcardElement.java | 3 +- ...tractElementAnnotationMetadataFactory.java | 80 +++- .../ast/utils/EnclosedElementsQuery.java | 49 +- .../writer/AbstractClassFileWriter.java | 8 +- .../ast/groovy/InjectTransform.groovy | 5 +- .../GroovyAnnotationMetadataBuilder.java | 13 +- ...roovyElementAnnotationMetadataFactory.java | 38 ++ .../groovy/visitor/AbstractGroovyElement.java | 205 ++++---- .../visitor/GroovyAnnotationElement.java | 5 +- .../groovy/visitor/GroovyClassElement.java | 55 ++- .../visitor/GroovyConstructorElement.java | 6 +- .../groovy/visitor/GroovyElementFactory.java | 90 ++-- .../visitor/GroovyEnumConstantElement.java | 15 +- .../ast/groovy/visitor/GroovyEnumElement.java | 28 +- .../groovy/visitor/GroovyFieldElement.java | 9 +- .../GroovyGenericPlaceholderElement.java | 53 +-- .../groovy/visitor/GroovyMethodElement.java | 36 +- .../groovy/visitor/GroovyNativeElement.java | 103 ++++ .../groovy/visitor/GroovyPackageElement.java | 8 +- .../visitor/GroovyParameterElement.java | 19 +- .../groovy/visitor/GroovyPropertyElement.java | 13 +- .../groovy/visitor/GroovyVisitorContext.java | 8 +- .../groovy/visitor/GroovyWildcardElement.java | 2 +- .../visitor/GroovyClassElementSpec.groovy | 4 +- .../visitor/BeanIntrospectionSpec.groovy | 39 ++ .../inject/visitor/ClassElementSpec.groovy | 439 +++++++++++++++++- .../micronaut/inject/visitor/MyParameter.java | 20 + .../inject/visitor/TypeUseClassAnn.java | 11 + .../inject/visitor/TypeUseRuntimeAnn.java | 11 + .../JavaGenericPlaceholderElement.java | 25 +- 35 files changed, 1155 insertions(+), 352 deletions(-) create mode 100644 core-processor/src/main/java/io/micronaut/inject/ast/GenericElement.java create mode 100644 inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyNativeElement.java create mode 100644 inject-groovy/src/test/groovy/io/micronaut/inject/visitor/MyParameter.java create mode 100644 inject-groovy/src/test/groovy/io/micronaut/inject/visitor/TypeUseClassAnn.java create mode 100644 inject-groovy/src/test/groovy/io/micronaut/inject/visitor/TypeUseRuntimeAnn.java diff --git a/core-processor/src/main/java/io/micronaut/inject/annotation/AbstractAnnotationMetadataBuilder.java b/core-processor/src/main/java/io/micronaut/inject/annotation/AbstractAnnotationMetadataBuilder.java index 308553e9fa1..ebc6201eae5 100644 --- a/core-processor/src/main/java/io/micronaut/inject/annotation/AbstractAnnotationMetadataBuilder.java +++ b/core-processor/src/main/java/io/micronaut/inject/annotation/AbstractAnnotationMetadataBuilder.java @@ -78,7 +78,7 @@ public abstract class AbstractAnnotationMetadataBuilder { private static final Map>> ANNOTATION_MAPPERS = new HashMap<>(10); private static final Map>> ANNOTATION_TRANSFORMERS = new HashMap<>(5); private static final Map> ANNOTATION_REMAPPERS = new HashMap<>(5); - private static final Map, CachedAnnotationMetadata> MUTATED_ANNOTATION_METADATA = new HashMap<>(100); + private static final Map MUTATED_ANNOTATION_METADATA = new HashMap<>(100); private static final Map> NON_BINDING_CACHE = new HashMap<>(50); private static final Map> ANNOTATION_DEFAULTS = new HashMap<>(20); @@ -256,6 +256,21 @@ private CachedAnnotationMetadata lookupOrBuild(boolean inheritTypeAnnotations, T }); } + /** + * Lookup or build new annotation metadata. + * + * @param key The cache key + * @param element The type element + * @param includeTypeAnnotations Whether to include type level annotations in the metadata for the element + * @return The annotation metadata + */ + public CachedAnnotationMetadata lookupOrBuild(Object key, T element, boolean includeTypeAnnotations) { + return MUTATED_ANNOTATION_METADATA.computeIfAbsent( + key, + metadataKey -> new DefaultCachedAnnotationMetadata(buildInternal(includeTypeAnnotations, false, element)) + ); + } + private AnnotationMetadata buildInternal(boolean inheritTypeAnnotations, boolean declaredOnly, T element) { MutableAnnotationMetadata annotationMetadata = new MutableAnnotationMetadata(); try { diff --git a/core-processor/src/main/java/io/micronaut/inject/ast/ClassElement.java b/core-processor/src/main/java/io/micronaut/inject/ast/ClassElement.java index e2df06f9d6b..4f23c8cc566 100644 --- a/core-processor/src/main/java/io/micronaut/inject/ast/ClassElement.java +++ b/core-processor/src/main/java/io/micronaut/inject/ast/ClassElement.java @@ -457,6 +457,18 @@ default List getFields() { return getEnclosedElements(ElementQuery.ALL_FIELDS); } + /** + * Find a method with a name. + * + * @param name The method name + * @return The method + * @since 4.0.0 + */ + @NonNull + default Optional findMethod(String name) { + return getEnclosedElement(ElementQuery.ALL_METHODS.named(name)); + } + /** * Return the elements that match the given query. * diff --git a/core-processor/src/main/java/io/micronaut/inject/ast/GenericElement.java b/core-processor/src/main/java/io/micronaut/inject/ast/GenericElement.java new file mode 100644 index 00000000000..cba6409ac6f --- /dev/null +++ b/core-processor/src/main/java/io/micronaut/inject/ast/GenericElement.java @@ -0,0 +1,43 @@ +/* + * Copyright 2017-2021 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.inject.ast; + +import io.micronaut.core.annotation.Experimental; +import org.jetbrains.annotations.NotNull; + +/** + * Represents a generic element that can appear as a type argument. + * + * @since 4.0.0 + * @author Denis Stepanov + */ +@Experimental +public interface GenericElement extends ClassElement { + + /** + * The native type that represents the generic element. + * It is expected that the generic element representing 'T extends java.lang.Number` + * should be equal to the class element `java.lang.Number`. + * To find matching placeholders we can use this method to match the native generic type. + * + * @return The generic native type + */ + @NotNull + default Object getGenericNativeType() { + return getNativeType(); + } + +} diff --git a/core-processor/src/main/java/io/micronaut/inject/ast/GenericPlaceholderElement.java b/core-processor/src/main/java/io/micronaut/inject/ast/GenericPlaceholderElement.java index df99d728064..2d204ff1f3b 100644 --- a/core-processor/src/main/java/io/micronaut/inject/ast/GenericPlaceholderElement.java +++ b/core-processor/src/main/java/io/micronaut/inject/ast/GenericPlaceholderElement.java @@ -31,7 +31,7 @@ * @author graemerocher */ @Experimental -public interface GenericPlaceholderElement extends ClassElement { +public interface GenericPlaceholderElement extends GenericElement { /** * Returns the bounds of this the generic placeholder empty. Always returns a non-empty list. diff --git a/core-processor/src/main/java/io/micronaut/inject/ast/MethodElement.java b/core-processor/src/main/java/io/micronaut/inject/ast/MethodElement.java index be1ac7ed2cf..fc6353c7591 100644 --- a/core-processor/src/main/java/io/micronaut/inject/ast/MethodElement.java +++ b/core-processor/src/main/java/io/micronaut/inject/ast/MethodElement.java @@ -19,10 +19,12 @@ import io.micronaut.core.annotation.AnnotationMetadataProvider; import io.micronaut.core.annotation.AnnotationValue; import io.micronaut.core.annotation.AnnotationValueBuilder; +import io.micronaut.core.annotation.Experimental; import io.micronaut.core.annotation.NonNull; import io.micronaut.core.annotation.Nullable; import io.micronaut.core.util.ArgumentUtils; import io.micronaut.core.util.ArrayUtils; +import io.micronaut.core.util.CollectionUtils; import io.micronaut.inject.annotation.AbstractAnnotationMetadataBuilder; import io.micronaut.inject.ast.beans.BeanElementBuilder; @@ -30,6 +32,7 @@ import java.util.Arrays; import java.util.Collections; import java.util.List; +import java.util.Map; import java.util.Optional; import java.util.function.Consumer; import java.util.function.Predicate; @@ -56,6 +59,36 @@ default List getDeclaredTypeVariables() { return Collections.emptyList(); } + /** + * The type arguments for this method element. + * The type arguments should include the type arguments added to the method plus the type arguments of the declaring class. + * + * @return The type arguments for this method element + * @since 4.0.0 + */ + @Experimental + @NonNull + default Map getTypeArguments() { + Map typeArguments = getDeclaringType().getTypeArguments(); + Map methodTypeArguments = getDeclaredTypeArguments(); + Map newTypeArguments = CollectionUtils.newLinkedHashMap(typeArguments.size() + methodTypeArguments.size()); + newTypeArguments.putAll(typeArguments); + newTypeArguments.putAll(methodTypeArguments); + return newTypeArguments; + } + + /** + * The declared type arguments for this method element. + * + * @return The declared type arguments for this method element + * @since 4.0.0 + */ + @Experimental + @NonNull + default Map getDeclaredTypeArguments() { + return Collections.emptyMap(); + } + /** *

Returns the receiver type of this executable, or empty if the method has no receiver type.

* diff --git a/core-processor/src/main/java/io/micronaut/inject/ast/WildcardElement.java b/core-processor/src/main/java/io/micronaut/inject/ast/WildcardElement.java index 9240dfa6ab9..4b3a64b6bc7 100644 --- a/core-processor/src/main/java/io/micronaut/inject/ast/WildcardElement.java +++ b/core-processor/src/main/java/io/micronaut/inject/ast/WildcardElement.java @@ -28,7 +28,8 @@ * @author Jonas Konrad */ @Experimental -public interface WildcardElement extends ClassElement { +public interface WildcardElement extends GenericElement { + /** * @return The upper bounds of this wildcard. Never empty. To match this wildcard, a type must be assignable to all * upper bounds (must extend all upper bounds). diff --git a/core-processor/src/main/java/io/micronaut/inject/ast/annotation/AbstractElementAnnotationMetadataFactory.java b/core-processor/src/main/java/io/micronaut/inject/ast/annotation/AbstractElementAnnotationMetadataFactory.java index 7efbe85f7c6..3684f57dfef 100644 --- a/core-processor/src/main/java/io/micronaut/inject/ast/annotation/AbstractElementAnnotationMetadataFactory.java +++ b/core-processor/src/main/java/io/micronaut/inject/ast/annotation/AbstractElementAnnotationMetadataFactory.java @@ -89,6 +89,58 @@ public ElementAnnotationMetadata build(Element element, AnnotationMetadata defau throw new IllegalStateException("Unknown element: " + element.getClass() + " with native type: " + element.getNativeType()); } + /** + * Lookup annotation metadata for the package. + * @param packageElement The element + * @return The annotation metadata + */ + protected AbstractAnnotationMetadataBuilder.CachedAnnotationMetadata lookupForPackage(PackageElement packageElement) { + return metadataBuilder.lookupOrBuildForType((K) packageElement.getNativeType()); + } + + /** + * Lookup annotation metadata for the parameter. + * @param parameterElement The element + * @return The annotation metadata + */ + protected AbstractAnnotationMetadataBuilder.CachedAnnotationMetadata lookupForParameter(ParameterElement parameterElement) { + return metadataBuilder.lookupOrBuildForParameter( + (K) parameterElement.getMethodElement().getOwningType().getNativeType(), + (K) parameterElement.getMethodElement().getNativeType(), + (K) parameterElement.getNativeType() + ); + } + + /** + * Lookup annotation metadata for the field. + * @param fieldElement The element + * @return The annotation metadata + */ + protected AbstractAnnotationMetadataBuilder.CachedAnnotationMetadata lookupForField(FieldElement fieldElement) { + return metadataBuilder.lookupOrBuildForField( + (K) fieldElement.getOwningType().getNativeType(), + (K) fieldElement.getNativeType() + ); + } + + /** + * Lookup annotation metadata for the method. + * @param methodElement The element + * @return The annotation metadata + */ + protected AbstractAnnotationMetadataBuilder.CachedAnnotationMetadata lookupForMethod(MethodElement methodElement) { + return metadataBuilder.lookupOrBuildForMethod((K) methodElement.getOwningType().getNativeType(), (K) methodElement.getNativeType()); + } + + /** + * Lookup annotation metadata for the class. + * @param classElement The element + * @return The annotation metadata + */ + protected AbstractAnnotationMetadataBuilder.CachedAnnotationMetadata lookupForClass(ClassElement classElement) { + return metadataBuilder.lookupOrBuildForType((K) classElement.getNativeType()); + } + @NonNull private AbstractElementAnnotationMetadata buildForProperty(@Nullable AnnotationMetadata defaultAnnotationMetadata, @NonNull PropertyElement propertyElement) { return new AbstractElementAnnotationMetadata(defaultAnnotationMetadata) { @@ -112,10 +164,7 @@ private AbstractElementAnnotationMetadata buildForEnumConstantElement(@Nullable @Override protected AbstractAnnotationMetadataBuilder.CachedAnnotationMetadata lookup() { - return metadataBuilder.lookupOrBuildForField( - (K) enumConstantElement.getOwningType().getNativeType(), - (K) enumConstantElement.getNativeType() - ); + return lookupForField(enumConstantElement); } @Override @@ -131,7 +180,7 @@ private AbstractElementAnnotationMetadata buildForPackage(@Nullable AnnotationMe @Override protected AbstractAnnotationMetadataBuilder.CachedAnnotationMetadata lookup() { - return metadataBuilder.lookupOrBuildForType((K) packageElement.getNativeType()); + return lookupForPackage(packageElement); } @Override @@ -147,11 +196,7 @@ private AbstractElementAnnotationMetadata buildForParameter(@Nullable Annotation @Override protected AbstractAnnotationMetadataBuilder.CachedAnnotationMetadata lookup() { - return metadataBuilder.lookupOrBuildForParameter( - (K) parameterElement.getMethodElement().getOwningType().getNativeType(), - (K) parameterElement.getMethodElement().getNativeType(), - (K) parameterElement.getNativeType() - ); + return lookupForParameter(parameterElement); } @Override @@ -168,10 +213,7 @@ private AbstractElementAnnotationMetadata buildForField(@Nullable AnnotationMeta @Override protected AbstractAnnotationMetadataBuilder.CachedAnnotationMetadata lookup() { - return metadataBuilder.lookupOrBuildForField( - (K) fieldElement.getOwningType().getNativeType(), - (K) fieldElement.getNativeType() - ); + return lookupForField(fieldElement); } @Override @@ -188,7 +230,7 @@ private AbstractElementAnnotationMetadata buildForMethod(@Nullable AnnotationMet @Override protected AbstractAnnotationMetadataBuilder.CachedAnnotationMetadata lookup() { - return metadataBuilder.lookupOrBuildForMethod((K) methodElement.getOwningType().getNativeType(), (K) methodElement.getNativeType()); + return lookupForMethod(methodElement); } @Override @@ -205,10 +247,7 @@ private AbstractElementAnnotationMetadata buildForConstructor(@Nullable Annotati @Override protected AbstractAnnotationMetadataBuilder.CachedAnnotationMetadata lookup() { - return metadataBuilder.lookupOrBuildForMethod( - (K) constructorElement.getOwningType().getNativeType(), - (K) constructorElement.getNativeType() - ); + return lookupForMethod(constructorElement); } @Override @@ -224,7 +263,7 @@ private AbstractElementAnnotationMetadata buildForClass(@Nullable AnnotationMeta @Override protected AbstractAnnotationMetadataBuilder.CachedAnnotationMetadata lookup() { - return metadataBuilder.lookupOrBuildForType((K) classElement.getNativeType()); + return lookupForClass(classElement); } @Override @@ -234,6 +273,7 @@ public String toString() { }; } + /** * Abstract implementation of {@link ElementAnnotationMetadata}. * diff --git a/core-processor/src/main/java/io/micronaut/inject/ast/utils/EnclosedElementsQuery.java b/core-processor/src/main/java/io/micronaut/inject/ast/utils/EnclosedElementsQuery.java index bd53ed6e6e5..982791c86ac 100644 --- a/core-processor/src/main/java/io/micronaut/inject/ast/utils/EnclosedElementsQuery.java +++ b/core-processor/src/main/java/io/micronaut/inject/ast/utils/EnclosedElementsQuery.java @@ -55,6 +55,26 @@ public abstract class EnclosedElementsQuery { private final Map elementsCache = new ConcurrentLinkedHashMap.Builder().maximumWeightedCapacity(200).build(); + /** + * Get native class element. + * + * @param classElement The class element + * @return The native element + */ + protected C getNativeClassType(ClassElement classElement) { + return (C) classElement.getNativeType(); + } + + /** + * Get native element. + * + * @param element The element + * @return The native element + */ + protected N getNativeType(Element element) { + return (N) element.getNativeType(); + } + /** * Return the elements that match the given query. * @@ -68,7 +88,7 @@ public List getEnclosedElements(C ElementQuery.Result result = query.result(); Set excludeElements = getExcludedNativeElements(result); Predicate filter = element -> { - if (excludeElements.contains(element.getNativeType())) { + if (excludeElements.contains(getNativeType(element))) { return false; } List> elementPredicates = result.getElementPredicates(); @@ -136,12 +156,12 @@ public List getEnclosedElements(C ClassElement ce; if (element instanceof ConstructorElement) { ce = classElement; - } else if (element instanceof MethodElement) { - ce = ((MethodElement) element).getGenericReturnType(); - } else if (element instanceof ClassElement) { - ce = (ClassElement) element; - } else if (element instanceof FieldElement) { - ce = ((FieldElement) element).getGenericField(); + } else if (element instanceof MethodElement methodElement) { + ce = methodElement.getGenericReturnType(); + } else if (element instanceof ClassElement theClass) { + ce = theClass; + } else if (element instanceof FieldElement fieldElement) { + ce = fieldElement.getGenericField(); } else { throw new IllegalStateException("Unknown element: " + element); } @@ -152,7 +172,7 @@ public List getEnclosedElements(C } return true; }; - return (List) getAllElements((C) classElement.getNativeType(), result.isOnlyDeclared(), (t1, t2) -> reduceElements(t1, t2, result), result) + return (List) getAllElements(getNativeClassType(classElement), result.isOnlyDeclared(), (t1, t2) -> reduceElements(t1, t2, result), result) .stream() .filter(filter) .toList(); @@ -162,18 +182,18 @@ private boolean reduceElements(io.micronaut.inject.ast.Element newElement, io.micronaut.inject.ast.Element existingElement, ElementQuery.Result result) { if (!result.isIncludeHiddenElements()) { - if (newElement instanceof FieldElement && existingElement instanceof FieldElement) { - return ((FieldElement) newElement).hides((FieldElement) existingElement); + if (newElement instanceof FieldElement newFiledElement && existingElement instanceof FieldElement existingFieldElement) { + return newFiledElement.hides(existingFieldElement); } - if (newElement instanceof MethodElement && existingElement instanceof MethodElement) { - if (((MethodElement) newElement).hides((MethodElement) existingElement)) { + if (newElement instanceof MethodElement newMethodElement && existingElement instanceof MethodElement existingMethodElement) { + if (newMethodElement.hides(existingMethodElement)) { return true; } } } if (!result.isIncludeOverriddenMethods()) { - if (newElement instanceof MethodElement && existingElement instanceof MethodElement) { - return ((MethodElement) newElement).overrides((MethodElement) existingElement); + if (newElement instanceof MethodElement newMethodElement && existingElement instanceof MethodElement existingMethodElement) { + return newMethodElement.overrides(existingMethodElement); } else if (newElement instanceof PropertyElement newPropertyElement && existingElement instanceof PropertyElement existingPropertyElement) { return newPropertyElement.overrides(existingPropertyElement); } @@ -220,6 +240,7 @@ private Collection getAllElements(C classNode, /** * get the cache key. + * * @param element The element * @return The cache key */ diff --git a/core-processor/src/main/java/io/micronaut/inject/writer/AbstractClassFileWriter.java b/core-processor/src/main/java/io/micronaut/inject/writer/AbstractClassFileWriter.java index 5e7f57e0c01..e34f4b57cdb 100644 --- a/core-processor/src/main/java/io/micronaut/inject/writer/AbstractClassFileWriter.java +++ b/core-processor/src/main/java/io/micronaut/inject/writer/AbstractClassFileWriter.java @@ -394,11 +394,13 @@ protected static void buildArgumentWithGenerics( boolean hasAnnotationMetadata = !annotationMetadata.isEmpty(); boolean isRecursiveType = false; - if (classElement instanceof GenericPlaceholderElement) { - if (visitedTypes.contains(classElement)) { + if (classElement instanceof GenericPlaceholderElement placeholderElement) { + // Prevent placeholder recursion + Object genericNativeType = placeholderElement.getGenericNativeType(); + if (visitedTypes.contains(genericNativeType)) { isRecursiveType = true; } else { - visitedTypes.add(classElement); + visitedTypes.add(genericNativeType); } } diff --git a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/InjectTransform.groovy b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/InjectTransform.groovy index 00ace8b874c..348b5799fc7 100644 --- a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/InjectTransform.groovy +++ b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/InjectTransform.groovy @@ -20,13 +20,14 @@ import groovy.transform.CompileStatic import io.micronaut.ast.groovy.utils.AstMessageUtils import io.micronaut.ast.groovy.utils.InMemoryByteCodeGroovyClassLoader import io.micronaut.ast.groovy.utils.InMemoryClassWriterOutputVisitor +import io.micronaut.ast.groovy.visitor.GroovyNativeElement import io.micronaut.ast.groovy.visitor.GroovyPackageElement import io.micronaut.ast.groovy.visitor.GroovyVisitorContext import io.micronaut.context.annotation.Configuration import io.micronaut.context.annotation.Context -import io.micronaut.inject.processing.ProcessingException import io.micronaut.inject.processing.BeanDefinitionCreator import io.micronaut.inject.processing.BeanDefinitionCreatorFactory +import io.micronaut.inject.processing.ProcessingException import io.micronaut.inject.visitor.VisitorConfiguration import io.micronaut.inject.writer.BeanConfigurationWriter import io.micronaut.inject.writer.BeanDefinitionReferenceWriter @@ -126,7 +127,7 @@ class InjectTransform implements ASTTransformation, CompilationUnitAware { } }) } catch (ProcessingException ex) { - groovyVisitorContext.fail(ex.getMessage(), ex.getOriginatingElement() as ASTNode) + groovyVisitorContext.fail(ex.getMessage(), (ex.getOriginatingElement() as GroovyNativeElement).annotatedNode()) } } diff --git a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/annotation/GroovyAnnotationMetadataBuilder.java b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/annotation/GroovyAnnotationMetadataBuilder.java index c634258ccaa..0edbd8e7ea1 100644 --- a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/annotation/GroovyAnnotationMetadataBuilder.java +++ b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/annotation/GroovyAnnotationMetadataBuilder.java @@ -23,6 +23,7 @@ import io.micronaut.ast.groovy.utils.ExtendedParameter; import io.micronaut.ast.groovy.visitor.GroovyVisitorContext; import io.micronaut.core.annotation.AnnotationClassValue; +import io.micronaut.core.annotation.AnnotationMetadata; import io.micronaut.core.annotation.AnnotationValue; import io.micronaut.core.annotation.NonNull; import io.micronaut.core.convert.ConversionService; @@ -270,9 +271,16 @@ protected String getElementName(AnnotatedNode element) { @Override protected List getAnnotationsForType(AnnotatedNode element) { List annotations = element.getAnnotations(); - List expanded = new ArrayList<>(annotations.size()); + List typeAnnotations = element instanceof ClassNode classNode ? classNode.getTypeAnnotations() : Collections.emptyList(); + List expanded = new ArrayList<>(annotations.size() + typeAnnotations.size()); + expandAnnotations(annotations, expanded); + expandAnnotations(typeAnnotations, expanded); + return expanded; + } + + private void expandAnnotations(List annotations, List expanded) { for (AnnotationNode node : annotations) { - Expression value = node.getMember("value"); + Expression value = node.getMember(AnnotationMetadata.VALUE_MEMBER); boolean repeatable = false; if (value instanceof ListExpression listExpression) { for (Expression expression : listExpression.getExpressions()) { @@ -289,7 +297,6 @@ protected List getAnnotationsForType(AnnotatedNode ele expanded.add(node); } } - return expanded; } @Override diff --git a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/annotation/GroovyElementAnnotationMetadataFactory.java b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/annotation/GroovyElementAnnotationMetadataFactory.java index 42eded1e218..fe261a3ef3a 100644 --- a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/annotation/GroovyElementAnnotationMetadataFactory.java +++ b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/annotation/GroovyElementAnnotationMetadataFactory.java @@ -15,6 +15,14 @@ */ package io.micronaut.ast.groovy.annotation; +import io.micronaut.ast.groovy.utils.ExtendedParameter; +import io.micronaut.ast.groovy.visitor.GroovyNativeElement; +import io.micronaut.inject.annotation.AbstractAnnotationMetadataBuilder; +import io.micronaut.inject.ast.ClassElement; +import io.micronaut.inject.ast.FieldElement; +import io.micronaut.inject.ast.MethodElement; +import io.micronaut.inject.ast.PackageElement; +import io.micronaut.inject.ast.ParameterElement; import io.micronaut.inject.ast.annotation.AbstractElementAnnotationMetadataFactory; import io.micronaut.inject.ast.annotation.ElementAnnotationMetadataFactory; import org.codehaus.groovy.ast.AnnotatedNode; @@ -37,4 +45,34 @@ public ElementAnnotationMetadataFactory readOnly() { return new GroovyElementAnnotationMetadataFactory(true, (GroovyAnnotationMetadataBuilder) metadataBuilder); } + @Override + protected AbstractAnnotationMetadataBuilder.CachedAnnotationMetadata lookupForPackage(PackageElement packageElement) { + GroovyNativeElement groovyNativeElement = (GroovyNativeElement) packageElement.getNativeType(); + return metadataBuilder.lookupOrBuild(groovyNativeElement, groovyNativeElement.annotatedNode(), true); + } + + @Override + protected AbstractAnnotationMetadataBuilder.CachedAnnotationMetadata lookupForParameter(ParameterElement parameterElement) { + GroovyNativeElement.Parameter parameter = (GroovyNativeElement.Parameter) parameterElement.getNativeType(); + return metadataBuilder.lookupOrBuild(parameter, new ExtendedParameter(parameter.methodNode(), parameter.annotatedNode()), false); + } + + @Override + protected AbstractAnnotationMetadataBuilder.CachedAnnotationMetadata lookupForField(FieldElement fieldElement) { + GroovyNativeElement groovyNativeElement = (GroovyNativeElement) fieldElement.getNativeType(); + return metadataBuilder.lookupOrBuild(groovyNativeElement, groovyNativeElement.annotatedNode(), false); + } + + @Override + protected AbstractAnnotationMetadataBuilder.CachedAnnotationMetadata lookupForMethod(MethodElement methodElement) { + GroovyNativeElement groovyNativeElement = (GroovyNativeElement) methodElement.getNativeType(); + return metadataBuilder.lookupOrBuild(groovyNativeElement, groovyNativeElement.annotatedNode(), false); + } + + @Override + protected AbstractAnnotationMetadataBuilder.CachedAnnotationMetadata lookupForClass(ClassElement classElement) { + GroovyNativeElement groovyNativeElement = (GroovyNativeElement) classElement.getNativeType(); + return metadataBuilder.lookupOrBuild(groovyNativeElement, groovyNativeElement.annotatedNode(), true); + } + } diff --git a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/AbstractGroovyElement.java b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/AbstractGroovyElement.java index 14bea6af771..8dff43095a4 100644 --- a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/AbstractGroovyElement.java +++ b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/AbstractGroovyElement.java @@ -48,10 +48,8 @@ import java.util.Arrays; import java.util.Collections; import java.util.HashSet; -import java.util.LinkedHashMap; import java.util.List; import java.util.Map; -import java.util.Objects; import java.util.Optional; import java.util.Set; import java.util.function.Consumer; @@ -77,21 +75,21 @@ public abstract class AbstractGroovyElement implements Element, ElementMutableAn protected final ElementAnnotationMetadataFactory elementAnnotationMetadataFactory; protected AnnotationMetadata presetAnnotationMetadata; private ElementAnnotationMetadata elementAnnotationMetadata; - private final AnnotatedNode annotatedNode; + private final GroovyNativeElement nativeElement; /** * Default constructor. * * @param visitorContext The groovy visitor context - * @param annotatedNode The annotated node + * @param nativeElement The native element * @param annotationMetadataFactory The annotation metadata factory */ protected AbstractGroovyElement(GroovyVisitorContext visitorContext, - AnnotatedNode annotatedNode, + GroovyNativeElement nativeElement, ElementAnnotationMetadataFactory annotationMetadataFactory) { this.visitorContext = visitorContext; this.compilationUnit = visitorContext.getCompilationUnit(); - this.annotatedNode = annotatedNode; + this.nativeElement = nativeElement; this.elementAnnotationMetadataFactory = annotationMetadataFactory; this.sourceUnit = visitorContext.getSourceUnit(); } @@ -200,8 +198,8 @@ public io.micronaut.inject.ast.Element withAnnotationMetadata(AnnotationMetadata } @Override - public AnnotatedNode getNativeType() { - return annotatedNode; + public GroovyNativeElement getNativeType() { + return nativeElement; } @Override @@ -210,25 +208,28 @@ public boolean isPackagePrivate() { } @NonNull - protected final ClassElement newClassElement(@NonNull ClassNode type, @Nullable Map genericsSpec) { + protected final ClassElement newClassElement(@NonNull ClassNode type, + @Nullable Map genericsSpec) { if (genericsSpec == null) { return newClassElement(type); } - return newClassElement(type, genericsSpec, new HashSet<>(), false, false); + return newClassElement(getNativeType(), type, genericsSpec, new HashSet<>(), false, false); } @NonNull - protected final ClassElement newClassElement(@NonNull GenericsType genericsType) { - return newClassElement(genericsType, genericsType, Collections.emptyMap(), new HashSet<>(), false); + protected final ClassElement newClassElement(GenericsType genericsType) { + return newClassElement(getNativeType(), getNativeType().annotatedNode(), genericsType, genericsType, Collections.emptyMap(), new HashSet<>(), false); } @NonNull - protected final ClassElement newClassElement(@NonNull ClassNode type) { - return newClassElement(type, Collections.emptyMap(), new HashSet<>(), false, false); + protected final ClassElement newClassElement(ClassNode type) { + return newClassElement(getNativeType(), type, Collections.emptyMap(), new HashSet<>(), false, false); } @NonNull - private ClassElement newClassElement(GenericsType genericsType, + private ClassElement newClassElement(@Nullable GroovyNativeElement declaredElement, + AnnotatedNode genericsOwner, + GenericsType genericsType, GenericsType redirectType, Map parentTypeArguments, Set visitedTypes, @@ -237,25 +238,27 @@ private ClassElement newClassElement(GenericsType genericsType, parentTypeArguments = Collections.emptyMap(); } if (genericsType.isWildcard()) { - return resolveWildcard(genericsType, redirectType, parentTypeArguments, visitedTypes); + return resolveWildcard(declaredElement, genericsOwner, genericsType, redirectType, parentTypeArguments, visitedTypes); } if (genericsType.isPlaceholder()) { - return resolvePlaceholder(genericsType, redirectType, parentTypeArguments, visitedTypes, isRawType); + return resolvePlaceholder(declaredElement, genericsOwner, genericsType, redirectType, parentTypeArguments, visitedTypes, isRawType); } - return newClassElement(genericsType.getType(), parentTypeArguments, visitedTypes, true, isRawType); + return newClassElement(declaredElement, genericsType.getType(), parentTypeArguments, visitedTypes, true, isRawType); } @NonNull - private ClassElement newClassElement(ClassNode classNode, + private ClassElement newClassElement(@Nullable GroovyNativeElement declaredElement, + ClassNode classNode, Map parentTypeArguments, Set visitedTypes, boolean isTypeVariable, boolean isRawTypeParameter) { - return newClassElement(classNode, parentTypeArguments, visitedTypes, isTypeVariable, isRawTypeParameter, false); + return newClassElement(declaredElement, classNode, parentTypeArguments, visitedTypes, isTypeVariable, isRawTypeParameter, false); } @NonNull - private ClassElement newClassElement(ClassNode classNode, + private ClassElement newClassElement(@Nullable GroovyNativeElement declaredElement, + ClassNode classNode, Map parentTypeArguments, Set visitedTypes, boolean isTypeVariable, @@ -266,8 +269,8 @@ private ClassElement newClassElement(ClassNode classNode, } if (classNode.isArray()) { ClassNode componentType = classNode.getComponentType(); - return newClassElement(componentType, parentTypeArguments, visitedTypes, isTypeVariable, isRawTypeParameter) - .toArray(); + return newClassElement(declaredElement, componentType, parentTypeArguments, visitedTypes, isTypeVariable, isRawTypeParameter) + .toArray(); } if (classNode.isGenericsPlaceHolder()) { GenericsType genericsType; @@ -286,33 +289,43 @@ private ClassElement newClassElement(ClassNode classNode, genericsType = new GenericsType(classNode.redirect()); redirectType = genericsType; } - return newClassElement(genericsType, redirectType, parentTypeArguments, visitedTypes, isRawTypeParameter); + return newClassElement(declaredElement, getNativeType().annotatedNode(), genericsType, redirectType, parentTypeArguments, visitedTypes, isRawTypeParameter); } if (ClassHelper.isPrimitiveType(classNode)) { return PrimitiveElement.valueOf(classNode.getName()); } if (classNode.isEnum()) { - return new GroovyEnumElement(visitorContext, classNode, elementAnnotationMetadataFactory); + return new GroovyEnumElement(visitorContext, new GroovyNativeElement.Class(classNode), elementAnnotationMetadataFactory); } if (classNode.isAnnotationDefinition()) { - return new GroovyAnnotationElement(visitorContext, classNode, elementAnnotationMetadataFactory); + return new GroovyAnnotationElement(visitorContext, new GroovyNativeElement.Class(classNode), elementAnnotationMetadataFactory); } Map newTypeArguments; + GroovyNativeElement groovyNativeElement; + if (declaredElement == null) { + groovyNativeElement = new GroovyNativeElement.Class(classNode); + } else { + groovyNativeElement = new GroovyNativeElement.ClassWithOwner(classNode, declaredElement); + } if (stripTypeArguments) { newTypeArguments = resolveTypeArgumentsToObject(classNode); } else { - newTypeArguments = resolveTypeArguments(classNode, parentTypeArguments, visitedTypes); + newTypeArguments = resolveClassTypeArguments(groovyNativeElement, classNode, parentTypeArguments, visitedTypes); } - return new GroovyClassElement(visitorContext, classNode, elementAnnotationMetadataFactory, newTypeArguments, 0, isTypeVariable); + return new GroovyClassElement(visitorContext, groovyNativeElement, elementAnnotationMetadataFactory, newTypeArguments, 0, isTypeVariable); } - private ClassElement resolvePlaceholder(GenericsType genericsType, + @NonNull + private ClassElement resolvePlaceholder(GroovyNativeElement declaredElement, + AnnotatedNode genericsOwner, + GenericsType genericsType, GenericsType redirectType, Map parentTypeArguments, Set visitedTypes, boolean isRawType) { - PlaceholderEntry entry = new PlaceholderEntry(getNativeType(), genericsType.getName()); String variableName = genericsType.getName(); + GroovyNativeElement groovyPlaceholderNativeElement + = new GroovyNativeElement.Placeholder(genericsType.getType(), declaredElement, variableName); ClassElement boundVariable = parentTypeArguments.get(variableName); if (boundVariable != null) { if (boundVariable instanceof WildcardElement wildcardElement) { @@ -329,31 +342,33 @@ private ClassElement resolvePlaceholder(GenericsType genericsType, addBounds(redirectType, classNodeBounds); } - boolean alreadyVisitedPlaceholder = visitedTypes.contains(entry); + PlaceholderEntry placeholderEntry = new PlaceholderEntry(genericsOwner, variableName); + boolean alreadyVisitedPlaceholder = visitedTypes.contains(placeholderEntry); if (!alreadyVisitedPlaceholder) { - visitedTypes.add(entry); + visitedTypes.add(placeholderEntry); } List bounds = classNodeBounds - .stream() - .map(classNode -> { - if (alreadyVisitedPlaceholder && classNode.isGenericsPlaceHolder()) { - classNode = classNode.redirect(); - } - return classNode; - }) - .filter(classNode -> !alreadyVisitedPlaceholder || !classNode.isGenericsPlaceHolder()) - .map(classNode -> { - // Strip declared type arguments and replace with an Object to prevent recursion - boolean stripTypeArguments = alreadyVisitedPlaceholder; - return (GroovyClassElement) newClassElement(classNode, parentTypeArguments, visitedTypes, true, isRawType, stripTypeArguments); - }) - .toList(); + .stream() + .map(classNode -> { + if (alreadyVisitedPlaceholder && classNode.isGenericsPlaceHolder()) { + classNode = classNode.redirect(); + } + return classNode; + }) + .filter(classNode -> !alreadyVisitedPlaceholder || !classNode.isGenericsPlaceHolder()) + .map(classNode -> { + // Strip declared type arguments and replace with an Object to prevent recursion + boolean stripTypeArguments = alreadyVisitedPlaceholder; + + return (GroovyClassElement) newClassElement(groovyPlaceholderNativeElement, classNode, parentTypeArguments, visitedTypes, true, isRawType, stripTypeArguments); + }) + .toList(); if (bounds.isEmpty()) { bounds = Collections.singletonList((GroovyClassElement) getObjectClassElement()); } - return new GroovyGenericPlaceholderElement(visitorContext, genericsType.getType(), bounds, isRawType); + return new GroovyGenericPlaceholderElement(visitorContext, this, groovyPlaceholderNativeElement, bounds, isRawType, variableName); } private static void addBounds(GenericsType genericsType, List classNodeBounds) { @@ -371,11 +386,15 @@ private static void addBounds(GenericsType genericsType, List classNo } } + @NonNull private ClassElement getObjectClassElement() { return visitorContext.getClassElement("java.lang.Object").get(); } - private ClassElement resolveWildcard(GenericsType genericsType, + @NonNull + private ClassElement resolveWildcard(GroovyNativeElement declaredElement, + AnnotatedNode genericsOwner, + GenericsType genericsType, GenericsType redirectType, Map parentTypeArguments, Set visitedTypes) { @@ -388,11 +407,11 @@ private ClassElement resolveWildcard(GenericsType genericsType, upperBounds = Arrays.stream(genericsUpperBounds); } List upperBoundsAsElements = upperBounds - .map(classNode -> newClassElement(classNode, parentTypeArguments, visitedTypes, true, false)) - .toList(); + .map(classNode -> newClassElement(declaredElement, classNode, parentTypeArguments, visitedTypes, true, false)) + .toList(); List lowerBoundsAsElements = lowerBounds - .map(classNode ->newClassElement(classNode, parentTypeArguments, visitedTypes, true, false)) - .toList(); + .map(classNode -> newClassElement(declaredElement, classNode, parentTypeArguments, visitedTypes, true, false)) + .toList(); if (upperBoundsAsElements.isEmpty()) { upperBoundsAsElements = Collections.singletonList(getObjectClassElement()); } @@ -400,7 +419,7 @@ private ClassElement resolveWildcard(GenericsType genericsType, if (upperType.getType().getName().equals("java.lang.Object")) { // Not bounded wildcard: if (redirectType != null && redirectType != genericsType) { - ClassElement definedTypeBound = newClassElement(redirectType, redirectType, parentTypeArguments, visitedTypes, false); + ClassElement definedTypeBound = newClassElement(declaredElement, genericsOwner, redirectType, redirectType, parentTypeArguments, visitedTypes, false); // Use originating parameter to extract the bound defined if (definedTypeBound instanceof GroovyGenericPlaceholderElement groovyGenericPlaceholderElement) { upperType = WildcardElement.findUpperType(groovyGenericPlaceholderElement.getBounds(), Collections.emptyList()); @@ -412,55 +431,38 @@ private ClassElement resolveWildcard(GenericsType genericsType, return upperType; } return new GroovyWildcardElement( - (GroovyClassElement) upperType, - upperBoundsAsElements.stream().map(GroovyClassElement.class::cast).toList(), - lowerBoundsAsElements.stream().map(GroovyClassElement.class::cast).toList(), - elementAnnotationMetadataFactory + (GroovyClassElement) upperType, + upperBoundsAsElements.stream().map(GroovyClassElement.class::cast).toList(), + lowerBoundsAsElements.stream().map(GroovyClassElement.class::cast).toList(), + elementAnnotationMetadataFactory ); } @NonNull - protected final Map resolveTypeArguments(MethodNode methodNode, - @Nullable Map parentTypeArguments) { + protected final Map resolveMethodTypeArguments(GroovyNativeElement declaredElement, + MethodNode methodNode, + @Nullable Map parentTypeArguments) { if (parentTypeArguments == null) { parentTypeArguments = Collections.emptyMap(); } - Set visitedTypes = new HashSet<>(); - GenericsType[] genericsTypes = methodNode.getGenericsTypes(); - if (ArrayUtils.isEmpty(genericsTypes)) { - return parentTypeArguments; - } - Map newTypeArguments = new LinkedHashMap<>(parentTypeArguments); - for (GenericsType genericsType : genericsTypes) { - String variableName = genericsType.getName(); - ClassNode classNode; - if (genericsType.isPlaceholder()) { - ClassNode[] upperBounds = genericsType.getUpperBounds(); - ClassNode lowerBound = genericsType.getLowerBound(); - if (ArrayUtils.isNotEmpty(upperBounds)) { - classNode = upperBounds[0]; - } else if (lowerBound != null) { - classNode = lowerBound; - } else { - classNode = ClassHelper.OBJECT_TYPE; - } - } else { - classNode = genericsType.getType(); - } - newTypeArguments.put( - variableName, - newClassElement(classNode, parentTypeArguments, visitedTypes, true, false) - ); - } - return newTypeArguments; + return resolveTypeArguments(declaredElement, methodNode, methodNode.getGenericsTypes(), methodNode.getGenericsTypes(), parentTypeArguments, new HashSet<>()); } @NonNull - protected final Map resolveTypeArguments(ClassNode classNode, - @Nullable Map parentTypeArguments, - Set visitedTypes) { - GenericsType[] genericsTypes = classNode.getGenericsTypes(); - GenericsType[] redirectTypes = classNode.redirect().getGenericsTypes(); + protected final Map resolveClassTypeArguments(GroovyNativeElement declaredElement, + ClassNode classNode, + @Nullable Map parentTypeArguments, + Set visitedTypes) { + return resolveTypeArguments(declaredElement, classNode, classNode.getGenericsTypes(), classNode.redirect().getGenericsTypes(), parentTypeArguments, visitedTypes); + } + + @NonNull + private Map resolveTypeArguments(GroovyNativeElement declaredElement, + AnnotatedNode genericsOwner, + GenericsType[] genericsTypes, + GenericsType[] redirectTypes, + @Nullable Map parentTypeArguments, + Set visitedTypes) { if (redirectTypes == null || redirectTypes.length == 0) { return Collections.emptyMap(); } @@ -469,7 +471,7 @@ protected final Map resolveTypeArguments(ClassNode classNo for (int i = 0; i < genericsTypes.length; i++) { GenericsType genericsType = genericsTypes[i]; GenericsType redirectType = redirectTypes[i]; - ClassElement classElement = newClassElement(genericsType, redirectType, parentTypeArguments, visitedTypes, false); + ClassElement classElement = newClassElement(declaredElement, genericsOwner, genericsType, redirectType, parentTypeArguments, visitedTypes, false); resolved.put(redirectType.getName(), classElement); } } else { @@ -477,8 +479,8 @@ protected final Map resolveTypeArguments(ClassNode classNo for (GenericsType redirectType : redirectTypes) { String variableName = redirectType.getName(); resolved.put( - variableName, - newClassElement(redirectType, redirectType, parentTypeArguments, visitedTypes, isRaw) + variableName, + newClassElement(declaredElement, genericsOwner, redirectType, redirectType, parentTypeArguments, visitedTypes, isRaw) ); } } @@ -502,6 +504,8 @@ protected final Map resolveTypeArgumentsToObject(ClassNode @Override public Optional getDocumentation() { + GroovyNativeElement nativeType = getNativeType(); + AnnotatedNode annotatedNode = nativeType.annotatedNode(); if (annotatedNode.getGroovydoc() == null || annotatedNode.getGroovydoc().getContent() == null) { return Optional.empty(); } @@ -513,16 +517,19 @@ public boolean equals(Object o) { if (this == o) { return true; } - if (o == null || getClass() != o.getClass()) { + if (o == null) { + return false; + } + if (!(o instanceof AbstractGroovyElement that)) { return false; } - AbstractGroovyElement that = (AbstractGroovyElement) o; - return annotatedNode.equals(that.annotatedNode); + // Allow to match GroovyClassElement / GroovyGenericPlaceholderElement / GroovyWildcardElement + return nativeElement.annotatedNode().equals(that.nativeElement.annotatedNode()); } @Override public int hashCode() { - return Objects.hash(annotatedNode); + return nativeElement.annotatedNode().hashCode(); } /** diff --git a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyAnnotationElement.java b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyAnnotationElement.java index 98604a25d15..49bbbff88e6 100644 --- a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyAnnotationElement.java +++ b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyAnnotationElement.java @@ -17,7 +17,6 @@ import io.micronaut.inject.ast.AnnotationElement; import io.micronaut.inject.ast.annotation.ElementAnnotationMetadataFactory; -import org.codehaus.groovy.ast.ClassNode; /** * Groovy implementation of {@link io.micronaut.inject.ast.AnnotationElement}. @@ -28,8 +27,8 @@ final class GroovyAnnotationElement extends GroovyClassElement implements AnnotationElement { public GroovyAnnotationElement(GroovyVisitorContext visitorContext, - ClassNode classNode, + GroovyNativeElement nativeElement, ElementAnnotationMetadataFactory annotationMetadataFactory) { - super(visitorContext, classNode, annotationMetadataFactory); + super(visitorContext, nativeElement, annotationMetadataFactory); } } diff --git a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyClassElement.java b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyClassElement.java index 0feee6f3319..5cd1ef78e43 100644 --- a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyClassElement.java +++ b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyClassElement.java @@ -128,49 +128,48 @@ public class GroovyClassElement extends AbstractGroovyElement implements Arrayab /** * @param visitorContext The visitor context - * @param classNode The {@link ClassNode} + * @param nativeElement The native element * @param annotationMetadataFactory The annotation metadata */ public GroovyClassElement(GroovyVisitorContext visitorContext, - ClassNode classNode, + GroovyNativeElement nativeElement, ElementAnnotationMetadataFactory annotationMetadataFactory) { - this(visitorContext, classNode, annotationMetadataFactory, null, 0); + this(visitorContext, nativeElement, annotationMetadataFactory, null, 0); } /** * @param visitorContext The visitor context - * @param classNode The {@link ClassNode} + * @param nativeElement The native element * @param annotationMetadataFactory The annotation metadata factory * @param resolvedTypeArguments The resolved type arguments * @param arrayDimensions The number of array dimensions */ - GroovyClassElement( - GroovyVisitorContext visitorContext, - ClassNode classNode, - ElementAnnotationMetadataFactory annotationMetadataFactory, - Map resolvedTypeArguments, - int arrayDimensions) { - this(visitorContext, classNode, annotationMetadataFactory, resolvedTypeArguments, arrayDimensions, false); + GroovyClassElement(GroovyVisitorContext visitorContext, + GroovyNativeElement nativeElement, + ElementAnnotationMetadataFactory annotationMetadataFactory, + Map resolvedTypeArguments, + int arrayDimensions) { + this(visitorContext, nativeElement, annotationMetadataFactory, resolvedTypeArguments, arrayDimensions, false); } /** * @param visitorContext The visitor context - * @param classNode The {@link ClassNode} + * @param nativeElement The native element * @param annotationMetadataFactory The annotation metadata factory * @param resolvedTypeArguments The resolved type arguments * @param arrayDimensions The number of array dimensions * @param isTypeVar Is the element a type variable */ GroovyClassElement(GroovyVisitorContext visitorContext, - ClassNode classNode, + GroovyNativeElement nativeElement, ElementAnnotationMetadataFactory annotationMetadataFactory, Map resolvedTypeArguments, int arrayDimensions, boolean isTypeVar) { - super(visitorContext, classNode, annotationMetadataFactory); - this.classNode = classNode; + super(visitorContext, nativeElement, annotationMetadataFactory); this.resolvedTypeArguments = resolvedTypeArguments; this.arrayDimensions = arrayDimensions; + classNode = (ClassNode) nativeElement.annotatedNode(); if (classNode.isArray()) { classNode.setName(classNode.getComponentType().getName()); } @@ -179,7 +178,7 @@ public GroovyClassElement(GroovyVisitorContext visitorContext, @Override protected GroovyClassElement copyConstructor() { - return new GroovyClassElement(visitorContext, classNode, elementAnnotationMetadataFactory, resolvedTypeArguments, arrayDimensions, isTypeVar); + return new GroovyClassElement(visitorContext, getNativeType(), elementAnnotationMetadataFactory, resolvedTypeArguments, arrayDimensions, isTypeVar); } @Override @@ -195,7 +194,7 @@ public ClassElement withAnnotationMetadata(AnnotationMetadata annotationMetadata @Override public ClassElement withTypeArguments(Map typeArguments) { - GroovyClassElement groovyClassElement = new GroovyClassElement(visitorContext, classNode, elementAnnotationMetadataFactory, resolvedTypeArguments, arrayDimensions); + GroovyClassElement groovyClassElement = new GroovyClassElement(visitorContext, getNativeType(), elementAnnotationMetadataFactory, resolvedTypeArguments, arrayDimensions); groovyClassElement.resolvedTypeArguments = typeArguments; return groovyClassElement; } @@ -335,7 +334,7 @@ private Optional createMethodElement(MethodNode method) { @NonNull public Map getTypeArguments() { if (resolvedTypeArguments == null) { - resolvedTypeArguments = resolveTypeArguments(classNode, Collections.emptyMap(), new HashSet<>()); + resolvedTypeArguments = resolveClassTypeArguments(getNativeType(), classNode, Collections.emptyMap(), new HashSet<>()); } return resolvedTypeArguments; } @@ -474,7 +473,7 @@ public boolean isArray() { @Override public ClassElement withArrayDimensions(int arrayDimensions) { - return new GroovyClassElement(visitorContext, classNode, elementAnnotationMetadataFactory, resolvedTypeArguments, arrayDimensions); + return new GroovyClassElement(visitorContext, getNativeType(), elementAnnotationMetadataFactory, resolvedTypeArguments, arrayDimensions); } @Override @@ -548,11 +547,6 @@ public boolean isProtected() { return Modifier.isProtected(classNode.getModifiers()); } - @Override - public ClassNode getNativeType() { - return classNode; - } - @Override public boolean isAssignable(String type) { return AstClassUtils.isSubclassOfOrImplementsInterface(classNode, type); @@ -587,7 +581,7 @@ public List getBoundGenericTypes() { if (genericsTypes == null) { return Collections.emptyList(); } - return Arrays.stream(genericsTypes).map(this::newClassElement).toList(); + return Arrays.stream(genericsTypes).map(gt -> newClassElement(gt)).toList(); } @NonNull @@ -624,6 +618,11 @@ private GroovyEnclosedElementsQuery(boolean isSource) { this.isSource = isSource; } + @Override + protected ClassNode getNativeClassType(ClassElement classElement) { + return (ClassNode) ((GroovyClassElement) classElement).getNativeType().annotatedNode(); + } + @Override protected Set getExcludedNativeElements(ElementQuery.Result result) { if (result.isExcludePropertyElements()) { @@ -631,11 +630,11 @@ protected Set getExcludedNativeElements(ElementQuery.Result re for (PropertyElement excludePropertyElement : getBeanProperties()) { excludePropertyElement.getReadMethod() .filter(m -> !m.isSynthetic()) - .ifPresent(methodElement -> excluded.add((AnnotatedNode) methodElement.getNativeType())); + .ifPresent(methodElement -> excluded.add(((GroovyNativeElement) methodElement.getNativeType()).annotatedNode())); excludePropertyElement.getWriteMethod() .filter(m -> !m.isSynthetic()) - .ifPresent(methodElement -> excluded.add((AnnotatedNode) methodElement.getNativeType())); - excludePropertyElement.getField().ifPresent(fieldElement -> excluded.add((AnnotatedNode) fieldElement.getNativeType())); + .ifPresent(methodElement -> excluded.add(((GroovyNativeElement) methodElement.getNativeType()).annotatedNode())); + excludePropertyElement.getField().ifPresent(fieldElement -> excluded.add(((GroovyNativeElement) fieldElement.getNativeType()).annotatedNode())); } return excluded; } diff --git a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyConstructorElement.java b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyConstructorElement.java index d47f5a8e80b..d30d19f8549 100644 --- a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyConstructorElement.java +++ b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyConstructorElement.java @@ -30,19 +30,21 @@ public class GroovyConstructorElement extends GroovyMethodElement implements Con /** * @param owningType The owning class * @param visitorContext The visitor context + * @param nativeElement The native element * @param methodNode The {@link ConstructorNode} * @param annotationMetadataFactory The annotation metadata */ GroovyConstructorElement(GroovyClassElement owningType, GroovyVisitorContext visitorContext, + GroovyNativeElement nativeElement, ConstructorNode methodNode, ElementAnnotationMetadataFactory annotationMetadataFactory) { - super(owningType, visitorContext, methodNode, annotationMetadataFactory); + super(owningType, visitorContext, nativeElement, methodNode, annotationMetadataFactory); } @Override protected AbstractGroovyElement copyConstructor() { - return new GroovyConstructorElement(getOwningType(), visitorContext, (ConstructorNode) getNativeType(), elementAnnotationMetadataFactory); + return new GroovyConstructorElement(getOwningType(), visitorContext, getNativeType(), (ConstructorNode) getNativeType().annotatedNode(), elementAnnotationMetadataFactory); } @Override diff --git a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyElementFactory.java b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyElementFactory.java index a696a9692c5..a345bf14e09 100644 --- a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyElementFactory.java +++ b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyElementFactory.java @@ -58,15 +58,15 @@ public ClassElement newClassElement(ClassNode classNode, ElementAnnotationMetada return PrimitiveElement.valueOf(classNode.getName()); } if (classNode.isEnum()) { - return new GroovyEnumElement(visitorContext, classNode, annotationMetadataFactory); + return new GroovyEnumElement(visitorContext, new GroovyNativeElement.Class(classNode), annotationMetadataFactory); } if (classNode.isAnnotationDefinition()) { - return new GroovyAnnotationElement(visitorContext, classNode, annotationMetadataFactory); + return new GroovyAnnotationElement(visitorContext, new GroovyNativeElement.Class(classNode), annotationMetadataFactory); } -// if (classNode.isGenericsPlaceHolder()) { -// return new GroovyGenericPlaceholderElement(visitorContext, classNode, annotationMetadataFactory, Collections.emptyMap(), 0, Collections.emptyList(),false); -// } - return new GroovyClassElement(visitorContext, classNode, annotationMetadataFactory); + if (classNode.isGenericsPlaceHolder()) { + throw new IllegalArgumentException("Placeholder cannot be created without declared element!"); + } + return new GroovyClassElement(visitorContext, new GroovyNativeElement.Class(classNode), annotationMetadataFactory); } @NonNull @@ -89,10 +89,11 @@ public GroovyMethodElement newMethodElement(ClassElement owningType, throw new IllegalArgumentException("Declaring class must be a GroovyClassElement"); } return new GroovyMethodElement( - (GroovyClassElement) owningType, - visitorContext, - method, - elementAnnotationMetadataFactory + (GroovyClassElement) owningType, + visitorContext, + new GroovyNativeElement.Method(method), + method, + elementAnnotationMetadataFactory ); } @@ -106,30 +107,30 @@ public ClassElement newSourceClassElement(ClassNode classNode, ElementAnnotation } else if (ClassHelper.isPrimitiveType(classNode)) { return PrimitiveElement.valueOf(classNode.getName()); } else if (classNode.isEnum()) { - return new GroovyEnumElement(visitorContext, classNode, annotationMetadataFactory) { + return new GroovyEnumElement(visitorContext, new GroovyNativeElement.Class(classNode), annotationMetadataFactory) { @NonNull @Override public BeanElementBuilder addAssociatedBean(@NonNull ClassElement type) { return new GroovyBeanDefinitionBuilder( - this, - type, - ConfigurationMetadataBuilder.INSTANCE, - annotationMetadataFactory, - visitorContext + this, + type, + ConfigurationMetadataBuilder.INSTANCE, + annotationMetadataFactory, + visitorContext ); } }; } else { - return new GroovyClassElement(visitorContext, classNode, annotationMetadataFactory) { + return new GroovyClassElement(visitorContext, new GroovyNativeElement.Class(classNode), annotationMetadataFactory) { @NonNull @Override public BeanElementBuilder addAssociatedBean(@NonNull ClassElement type) { return new GroovyBeanDefinitionBuilder( - this, - type, - ConfigurationMetadataBuilder.INSTANCE, - annotationMetadataFactory, - visitorContext + this, + type, + ConfigurationMetadataBuilder.INSTANCE, + annotationMetadataFactory, + visitorContext ); } }; @@ -138,26 +139,27 @@ public BeanElementBuilder addAssociatedBean(@NonNull ClassElement type) { @Override public GroovyMethodElement newSourceMethodElement(ClassElement owningType, - MethodNode method, - ElementAnnotationMetadataFactory elementAnnotationMetadataFactory) { + MethodNode method, + ElementAnnotationMetadataFactory elementAnnotationMetadataFactory) { if (!(owningType instanceof GroovyClassElement)) { throw new IllegalArgumentException("Declaring class must be a GroovyClassElement"); } return new GroovyMethodElement( - (GroovyClassElement) owningType, - visitorContext, - method, - elementAnnotationMetadataFactory + (GroovyClassElement) owningType, + visitorContext, + new GroovyNativeElement.Method(method), + method, + elementAnnotationMetadataFactory ) { @NonNull @Override public BeanElementBuilder addAssociatedBean(@NonNull ClassElement type) { return new GroovyBeanDefinitionBuilder( - this, - type, - ConfigurationMetadataBuilder.INSTANCE, - elementAnnotationMetadataFactory, - visitorContext + this, + type, + ConfigurationMetadataBuilder.INSTANCE, + elementAnnotationMetadataFactory, + visitorContext ); } }; @@ -165,20 +167,21 @@ public BeanElementBuilder addAssociatedBean(@NonNull ClassElement type) { @NonNull @Override - public ConstructorElement newConstructorElement(ClassElement declaringClass, + public ConstructorElement newConstructorElement(ClassElement owningType, MethodNode constructor, ElementAnnotationMetadataFactory annotationMetadataFactory) { - if (!(declaringClass instanceof GroovyClassElement)) { + if (!(owningType instanceof GroovyClassElement)) { throw new IllegalArgumentException("Declaring class must be a GroovyClassElement"); } if (!(constructor instanceof ConstructorNode)) { throw new IllegalArgumentException("Constructor must be a ConstructorNode"); } return new GroovyConstructorElement( - (GroovyClassElement) declaringClass, - visitorContext, - (ConstructorNode) constructor, - annotationMetadataFactory + (GroovyClassElement) owningType, + visitorContext, + new GroovyNativeElement.Method(constructor), + (ConstructorNode) constructor, + annotationMetadataFactory ); } @@ -190,11 +193,10 @@ public EnumConstantElement newEnumConstantElement(ClassElement declaringClass, throw new IllegalArgumentException("Declaring class must be a GroovyEnumElement"); } return new GroovyEnumConstantElement( - (GroovyClassElement) declaringClass, - visitorContext, - enumConstant, - enumConstant, - annotationMetadataFactory + (GroovyClassElement) declaringClass, + visitorContext, + enumConstant, + annotationMetadataFactory ); } diff --git a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyEnumConstantElement.java b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyEnumConstantElement.java index 3efa0eee5b2..409192ee07d 100644 --- a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyEnumConstantElement.java +++ b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyEnumConstantElement.java @@ -19,11 +19,10 @@ import io.micronaut.core.annotation.Internal; import io.micronaut.core.annotation.NonNull; import io.micronaut.inject.ast.ClassElement; -import io.micronaut.inject.ast.annotation.ElementAnnotationMetadataFactory; import io.micronaut.inject.ast.ElementModifier; import io.micronaut.inject.ast.EnumConstantElement; import io.micronaut.inject.ast.FieldElement; -import org.codehaus.groovy.ast.AnnotatedNode; +import io.micronaut.inject.ast.annotation.ElementAnnotationMetadataFactory; import org.codehaus.groovy.ast.FieldNode; import java.util.Set; @@ -43,21 +42,20 @@ public final class GroovyEnumConstantElement extends AbstractGroovyElement imple * @param declaringEnum The declaring enum * @param visitorContext The visitor context * @param variable The {@link org.codehaus.groovy.ast.Variable} - * @param annotatedNode The annotated node * @param annotationMetadataFactory The annotation medatada */ GroovyEnumConstantElement(GroovyClassElement declaringEnum, GroovyVisitorContext visitorContext, - FieldNode variable, AnnotatedNode annotatedNode, + FieldNode variable, ElementAnnotationMetadataFactory annotationMetadataFactory) { - super(visitorContext, annotatedNode, annotationMetadataFactory); + super(visitorContext, new GroovyNativeElement.Field(variable, declaringEnum.getNativeType()), annotationMetadataFactory); this.declaringEnum = declaringEnum; this.variable = variable; } @Override protected AbstractGroovyElement copyConstructor() { - return new GroovyEnumConstantElement(declaringEnum, visitorContext, variable, getNativeType(), elementAnnotationMetadataFactory); + return new GroovyEnumConstantElement(declaringEnum, visitorContext, variable, elementAnnotationMetadataFactory); } @Override @@ -131,11 +129,6 @@ public String getName() { return variable.getName(); } - @Override - public FieldNode getNativeType() { - return variable; - } - @Override public String toString() { return variable.getName(); diff --git a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyEnumElement.java b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyEnumElement.java index e9e37447811..598260f26a6 100644 --- a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyEnumElement.java +++ b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyEnumElement.java @@ -16,10 +16,9 @@ package io.micronaut.ast.groovy.visitor; import io.micronaut.inject.ast.ClassElement; -import io.micronaut.inject.ast.annotation.ElementAnnotationMetadataFactory; import io.micronaut.inject.ast.EnumConstantElement; import io.micronaut.inject.ast.EnumElement; -import org.codehaus.groovy.ast.ClassNode; +import io.micronaut.inject.ast.annotation.ElementAnnotationMetadataFactory; import org.codehaus.groovy.ast.FieldNode; import java.util.ArrayList; @@ -38,27 +37,27 @@ class GroovyEnumElement extends GroovyClassElement implements EnumElement { protected List values; /** - * @param visitorContext The visitor context - * @param classNode The {@link ClassNode} + * @param visitorContext The visitor context + * @param nativeElement The native element * @param annotationMetadataFactory The annotation metadata factory */ GroovyEnumElement(GroovyVisitorContext visitorContext, - ClassNode classNode, + GroovyNativeElement nativeElement, ElementAnnotationMetadataFactory annotationMetadataFactory) { - this(visitorContext, classNode, annotationMetadataFactory, 0); + this(visitorContext, nativeElement, annotationMetadataFactory, 0); } /** - * @param visitorContext The visitor context - * @param classNode The {@link ClassNode} + * @param visitorContext The visitor context + * @param nativeElement The native element * @param annotationMetadataFactory The annotation metadata - * @param arrayDimensions The number of array dimensions factory + * @param arrayDimensions The number of array dimensions factory */ GroovyEnumElement(GroovyVisitorContext visitorContext, - ClassNode classNode, + GroovyNativeElement nativeElement, ElementAnnotationMetadataFactory annotationMetadataFactory, int arrayDimensions) { - super(visitorContext, classNode, annotationMetadataFactory, null, arrayDimensions); + super(visitorContext, nativeElement, annotationMetadataFactory, null, arrayDimensions); } @Override @@ -82,14 +81,13 @@ public List elements() { private void initEnum() { values = new ArrayList<>(); enumConstants = new ArrayList<>(); - ClassNode nativeType = getNativeType(); - for (FieldNode field : nativeType.getFields()) { + for (FieldNode field : classNode.getFields()) { if (field.getName().equals("MAX_VALUE") || field.getName().equals("MIN_VALUE")) { continue; } if (field.isEnum()) { values.add(field.getName()); - enumConstants.add(new GroovyEnumConstantElement(this, visitorContext, field, field, elementAnnotationMetadataFactory)); + enumConstants.add(new GroovyEnumConstantElement(this, visitorContext, field, elementAnnotationMetadataFactory)); } } @@ -99,7 +97,7 @@ private void initEnum() { @Override public ClassElement withArrayDimensions(int arrayDimensions) { - return new GroovyEnumElement(visitorContext, classNode, elementAnnotationMetadataFactory, arrayDimensions); + return new GroovyEnumElement(visitorContext, getNativeType(), elementAnnotationMetadataFactory, arrayDimensions); } } diff --git a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyFieldElement.java b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyFieldElement.java index b96b2d4380a..afb5db2b951 100644 --- a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyFieldElement.java +++ b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyFieldElement.java @@ -50,7 +50,7 @@ public class GroovyFieldElement extends AbstractGroovyElement implements FieldEl GroovyClassElement owningType, FieldNode fieldNode, ElementAnnotationMetadataFactory annotationMetadataFactory) { - super(visitorContext, fieldNode, annotationMetadataFactory); + super(visitorContext, new GroovyNativeElement.Field(fieldNode, owningType.getNativeType()), annotationMetadataFactory); this.owningType = owningType; this.fieldNode = fieldNode; } @@ -65,11 +65,6 @@ public FieldElement withAnnotationMetadata(AnnotationMetadata annotationMetadata return (FieldElement) super.withAnnotationMetadata(annotationMetadata); } - @Override - public FieldNode getNativeType() { - return fieldNode; - } - @Override public GroovyClassElement getOwningType() { return owningType; @@ -162,7 +157,7 @@ public GroovyClassElement getDeclaringType() { if (declaringClass == null) { throw new IllegalStateException("Declaring class could not be established"); } - if (owningType.getNativeType().equals(declaringClass)) { + if (owningType.getNativeType().annotatedNode().equals(declaringClass)) { return owningType; } Map typeArguments = getOwningType().getTypeArguments(declaringClass.getName()); diff --git a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyGenericPlaceholderElement.java b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyGenericPlaceholderElement.java index 76207becb02..f5c6a3a50cc 100644 --- a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyGenericPlaceholderElement.java +++ b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyGenericPlaceholderElement.java @@ -21,7 +21,6 @@ import io.micronaut.inject.ast.Element; import io.micronaut.inject.ast.GenericPlaceholderElement; import io.micronaut.inject.ast.WildcardElement; -import org.codehaus.groovy.ast.ClassNode; import java.util.Collections; import java.util.List; @@ -38,51 +37,41 @@ @Internal final class GroovyGenericPlaceholderElement extends GroovyClassElement implements GenericPlaceholderElement { + private final GroovyNativeElement placeholderNativeElement; + private final Element declaringElement; + private final String variableName; private final GroovyClassElement mostUpper; private final List bounds; private final boolean rawType; - private final ClassNode placeholderClassNode; GroovyGenericPlaceholderElement(GroovyVisitorContext visitorContext, - ClassNode placeholderClassNode, + Element declaringElement, + GroovyNativeElement placeholderNativeElement, List bounds, - boolean rawType) { - this(visitorContext, placeholderClassNode, WildcardElement.findUpperType(bounds, Collections.emptyList()), bounds, 0, rawType); + boolean rawType, + String variableName) { + this(visitorContext, declaringElement, placeholderNativeElement, variableName, WildcardElement.findUpperType(bounds, Collections.emptyList()), bounds, 0, rawType); } GroovyGenericPlaceholderElement(GroovyVisitorContext visitorContext, - ClassNode placeholderClassNode, - GroovyClassElement mostUpper, + Element declaringElement, + GroovyNativeElement placeholderNativeElement, + String variableName, GroovyClassElement mostUpper, List bounds, int arrayDimensions, boolean rawType) { - super(visitorContext, mostUpper.classNode, mostUpper.elementAnnotationMetadataFactory, mostUpper.resolvedTypeArguments, arrayDimensions); + super(visitorContext, mostUpper.getNativeType(), mostUpper.elementAnnotationMetadataFactory, mostUpper.resolvedTypeArguments, arrayDimensions); + this.declaringElement = declaringElement; + this.placeholderNativeElement = placeholderNativeElement; + this.variableName = variableName; this.mostUpper = mostUpper; this.bounds = bounds; this.rawType = rawType; - this.placeholderClassNode = placeholderClassNode; - } - - @Override - public int hashCode() { - return placeholderClassNode.hashCode(); } @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null) { - return false; - } - if (!(o instanceof Element that)) { - return false; - } - if (that instanceof GroovyGenericPlaceholderElement placeholderElement) { - return placeholderElement.placeholderClassNode.equals(placeholderClassNode); - } - return false; + public Object getGenericNativeType() { + return placeholderNativeElement; } @Override @@ -92,7 +81,7 @@ public boolean isRawType() { @Override protected GroovyClassElement copyConstructor() { - return new GroovyGenericPlaceholderElement(visitorContext, placeholderClassNode, mostUpper, bounds, getArrayDimensions(), rawType); + return new GroovyGenericPlaceholderElement(visitorContext, declaringElement, placeholderNativeElement, variableName, mostUpper, bounds, getArrayDimensions(), rawType); } @NonNull @@ -104,17 +93,17 @@ public List getBounds() { @NonNull @Override public String getVariableName() { - return placeholderClassNode.getUnresolvedName(); + return variableName; } @Override public Optional getDeclaringElement() { - return Optional.empty(); + return Optional.ofNullable(declaringElement); } @Override public ClassElement withArrayDimensions(int arrayDimensions) { - return new GroovyGenericPlaceholderElement(visitorContext, placeholderClassNode, mostUpper, bounds, arrayDimensions, rawType); + return new GroovyGenericPlaceholderElement(visitorContext, declaringElement, placeholderNativeElement, variableName, mostUpper, bounds, arrayDimensions, rawType); } @Override diff --git a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyMethodElement.java b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyMethodElement.java index 1692642eec8..9bab314ec6a 100644 --- a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyMethodElement.java +++ b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyMethodElement.java @@ -48,25 +48,33 @@ public class GroovyMethodElement extends AbstractGroovyElement implements Method private final MethodNode methodNode; private final GroovyClassElement owningType; private ClassElement declaringType; + private Map declaredTypeArguments; + private Map typeArguments; /** * @param owningType The owning type * @param visitorContext The visitor context + * @param nativeElement The native element * @param methodNode The {@link MethodNode} * @param annotationMetadata The annotation metadata */ GroovyMethodElement(GroovyClassElement owningType, GroovyVisitorContext visitorContext, + GroovyNativeElement nativeElement, MethodNode methodNode, ElementAnnotationMetadataFactory annotationMetadata) { - super(visitorContext, methodNode, annotationMetadata); + super(visitorContext, nativeElement, annotationMetadata); this.methodNode = methodNode; this.owningType = owningType; } + public final MethodNode getMethodNode() { + return methodNode; + } + @Override protected AbstractGroovyElement copyConstructor() { - return new GroovyMethodElement(owningType, visitorContext, methodNode, elementAnnotationMetadataFactory); + return new GroovyMethodElement(owningType, visitorContext, getNativeType(), methodNode, elementAnnotationMetadataFactory); } @Override @@ -88,7 +96,7 @@ public MethodElement withParameters(ParameterElement... newParameters) { @Override public MethodElement withNewOwningType(ClassElement owningType) { - GroovyMethodElement groovyMethodElement = new GroovyMethodElement((GroovyClassElement) owningType, visitorContext, methodNode, elementAnnotationMetadataFactory); + GroovyMethodElement groovyMethodElement = new GroovyMethodElement((GroovyClassElement) owningType, visitorContext, getNativeType(), methodNode, elementAnnotationMetadataFactory); copyValues(groovyMethodElement); return groovyMethodElement; } @@ -113,7 +121,7 @@ public Set getModifiers() { public String toString() { ClassNode declaringClass = methodNode.getDeclaringClass(); if (declaringClass == null) { - declaringClass = owningType.getNativeType(); + declaringClass = owningType.classNode; } return declaringClass.getName() + "." + methodNode.getName() + "(..)"; } @@ -164,16 +172,25 @@ public boolean isDefault() { } @Override - public MethodNode getNativeType() { - return methodNode; + public Map getDeclaredTypeArguments() { + if (declaredTypeArguments == null) { + declaredTypeArguments = resolveMethodTypeArguments(getNativeType(), methodNode, getDeclaringType().getTypeArguments()); + } + return declaredTypeArguments; + } + + @Override + public Map getTypeArguments() { + if (typeArguments == null) { + typeArguments = MethodElement.super.getTypeArguments(); + } + return typeArguments; } @NonNull @Override public ClassElement getGenericReturnType() { - Map parentTypeArguments = getDeclaringType().getTypeArguments(); - Map methodTypeArguments = resolveTypeArguments(methodNode, parentTypeArguments); - return newClassElement(methodNode.getReturnType(), methodTypeArguments); + return newClassElement(methodNode.getReturnType(), getTypeArguments()); } @Override @@ -195,6 +212,7 @@ private GroovyParameterElement newParameter(Parameter parameter) { return new GroovyParameterElement( this, visitorContext, + new GroovyNativeElement.Parameter(parameter, methodNode), parameter, elementAnnotationMetadataFactory ); diff --git a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyNativeElement.java b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyNativeElement.java new file mode 100644 index 00000000000..b7d7e03980f --- /dev/null +++ b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyNativeElement.java @@ -0,0 +1,103 @@ +/* + * Copyright 2017-2024 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.ast.groovy.visitor; + +import org.codehaus.groovy.ast.AnnotatedNode; +import org.codehaus.groovy.ast.ClassNode; +import org.codehaus.groovy.ast.FieldNode; +import org.codehaus.groovy.ast.MethodNode; +import org.codehaus.groovy.ast.PackageNode; + +/** + * Groovy's native element. + * + * @author Denis Stepanov + * @since 4.0.0 + */ +public sealed interface GroovyNativeElement { + + /** + * @return The annotated node representing the type. + */ + AnnotatedNode annotatedNode(); + + /** + * The class element. + * + * @param annotatedNode The class node + */ + record Class(ClassNode annotatedNode) implements GroovyNativeElement { + } + + /** + * The class element with an owner (Generic type etc). + * + * @param annotatedNode The class node + * @param owner The owner + */ + record ClassWithOwner(ClassNode annotatedNode, + GroovyNativeElement owner) implements GroovyNativeElement { + } + + /** + * The method element. + * + * @param annotatedNode The method node + */ + record Method(MethodNode annotatedNode) implements GroovyNativeElement { + } + + /** + * The parameter element. + * + * @param annotatedNode The parameter element. + * @param methodNode The method element. + */ + record Parameter(org.codehaus.groovy.ast.Parameter annotatedNode, + MethodNode methodNode) implements GroovyNativeElement { + } + + /** + * The package element. + * + * @param annotatedNode The package node + */ + record Package(PackageNode annotatedNode) implements GroovyNativeElement { + } + + /** + * The field element. + * + * @param annotatedNode The field node + * @param owner The owner node + */ + record Field(FieldNode annotatedNode, + GroovyNativeElement owner) implements GroovyNativeElement { + } + + /** + * The placeholder element. + * + * @param annotatedNode The placeholder node + * @param owner The owner node + * @param variableName The variable name + */ + record Placeholder(ClassNode annotatedNode, + GroovyNativeElement owner, + String variableName) implements GroovyNativeElement { + } + +} diff --git a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyPackageElement.java b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyPackageElement.java index 955109ed5c8..f8368a6abff 100644 --- a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyPackageElement.java +++ b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyPackageElement.java @@ -41,7 +41,7 @@ public class GroovyPackageElement extends AbstractGroovyElement implements Packa public GroovyPackageElement(GroovyVisitorContext visitorContext, PackageNode packageNode, ElementAnnotationMetadataFactory annotationMetadataFactory) { - super(visitorContext, packageNode, annotationMetadataFactory); + super(visitorContext, new GroovyNativeElement.Package(packageNode), annotationMetadataFactory); this.packageNode = packageNode; } @@ -80,10 +80,4 @@ public boolean isPublic() { return true; } - @NonNull - @Override - public PackageNode getNativeType() { - return packageNode; - } - } diff --git a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyParameterElement.java b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyParameterElement.java index d8eab4c5afe..1ed3607b357 100644 --- a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyParameterElement.java +++ b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyParameterElement.java @@ -20,12 +20,10 @@ import io.micronaut.core.annotation.NonNull; import io.micronaut.core.annotation.Nullable; import io.micronaut.inject.ast.ClassElement; -import io.micronaut.inject.ast.annotation.ElementAnnotationMetadataFactory; import io.micronaut.inject.ast.ParameterElement; +import io.micronaut.inject.ast.annotation.ElementAnnotationMetadataFactory; import org.codehaus.groovy.ast.Parameter; -import java.util.Map; - /** * Implementation of {@link ParameterElement} for Groovy. * @@ -45,21 +43,23 @@ public class GroovyParameterElement extends AbstractGroovyElement implements Par * * @param methodElement The parent method element * @param visitorContext The visitor context + * @param nativeElement The nativeElement * @param parameter The parameter * @param elementAnnotationMetadata The annotation metadata */ GroovyParameterElement(GroovyMethodElement methodElement, GroovyVisitorContext visitorContext, + GroovyNativeElement nativeElement, Parameter parameter, ElementAnnotationMetadataFactory elementAnnotationMetadata) { - super(visitorContext, parameter, elementAnnotationMetadata); + super(visitorContext, nativeElement, elementAnnotationMetadata); this.parameter = parameter; this.methodElement = methodElement; } @Override protected AbstractGroovyElement copyConstructor() { - return new GroovyParameterElement(methodElement, visitorContext, parameter, elementAnnotationMetadataFactory); + return new GroovyParameterElement(methodElement, visitorContext, getNativeType(), parameter, elementAnnotationMetadataFactory); } @Override @@ -86,9 +86,7 @@ public int getArrayDimensions() { @Override public ClassElement getGenericType() { if (genericType == null) { - Map parentTypeArguments = getMethodElement().getDeclaringType().getTypeArguments(); - Map methodTypeArguments = resolveTypeArguments(methodElement.getNativeType(), parentTypeArguments); - genericType = newClassElement(parameter.getType(), methodTypeArguments); + genericType = newClassElement(parameter.getType(), methodElement.getTypeArguments()); } return genericType; } @@ -108,11 +106,6 @@ public boolean isPublic() { return true; } - @Override - public Parameter getNativeType() { - return parameter; - } - @Override public GroovyMethodElement getMethodElement() { return methodElement; diff --git a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyPropertyElement.java b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyPropertyElement.java index 12a2fd5b23f..6933af72c98 100644 --- a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyPropertyElement.java +++ b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyPropertyElement.java @@ -24,13 +24,12 @@ import io.micronaut.inject.annotation.AnnotationMetadataHierarchy; import io.micronaut.inject.ast.ClassElement; import io.micronaut.inject.ast.Element; -import io.micronaut.inject.ast.annotation.ElementAnnotationMetadataFactory; import io.micronaut.inject.ast.FieldElement; import io.micronaut.inject.ast.MemberElement; import io.micronaut.inject.ast.MethodElement; -import io.micronaut.inject.ast.annotation.MutableAnnotationMetadataDelegate; import io.micronaut.inject.ast.PropertyElement; -import org.codehaus.groovy.ast.AnnotatedNode; +import io.micronaut.inject.ast.annotation.ElementAnnotationMetadataFactory; +import io.micronaut.inject.ast.annotation.MutableAnnotationMetadataDelegate; import java.lang.annotation.Annotation; import java.util.ArrayList; @@ -192,17 +191,17 @@ public MemberElement withAnnotationMetadata(AnnotationMetadata annotationMetadat return (MemberElement) super.withAnnotationMetadata(annotationMetadata); } - private static AnnotatedNode selectNativeType(MethodElement getter, + private static GroovyNativeElement selectNativeType(MethodElement getter, MethodElement setter, FieldElement field) { if (getter instanceof AbstractGroovyElement) { - return (AnnotatedNode) getter.getNativeType(); + return (GroovyNativeElement) getter.getNativeType(); } if (setter instanceof AbstractGroovyElement) { - return (AnnotatedNode) setter.getNativeType(); + return (GroovyNativeElement) setter.getNativeType(); } if (field instanceof AbstractGroovyElement) { - return (AnnotatedNode) field.getNativeType(); + return (GroovyNativeElement) field.getNativeType(); } throw new IllegalStateException(); } diff --git a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyVisitorContext.java b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyVisitorContext.java index 0f7a6e3c73c..854e9b998c9 100644 --- a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyVisitorContext.java +++ b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyVisitorContext.java @@ -194,8 +194,8 @@ public void info(String message) { @Override public void fail(String message, @Nullable Element element) { - if (element instanceof AbstractGroovyElement) { - AstMessageUtils.error(sourceUnit, ((AbstractGroovyElement) element).getNativeType(), message); + if (element instanceof AbstractGroovyElement abstractGroovyElement) { + AstMessageUtils.error(sourceUnit, abstractGroovyElement.getNativeType().annotatedNode(), message); } else { AstMessageUtils.error(sourceUnit, null, message); } @@ -207,8 +207,8 @@ public final void fail(String message, ASTNode expr) { @Override public void warn(String message, @Nullable Element element) { - if (element instanceof AbstractGroovyElement) { - AstMessageUtils.warning(sourceUnit, ((AbstractGroovyElement) element).getNativeType(), message); + if (element instanceof AbstractGroovyElement abstractGroovyElement) { + AstMessageUtils.warning(sourceUnit, abstractGroovyElement.getNativeType().annotatedNode(), message); } else { AstMessageUtils.warning(sourceUnit, null, message); } diff --git a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyWildcardElement.java b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyWildcardElement.java index 03bebb2eb1c..e4ee4d5fd62 100644 --- a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyWildcardElement.java +++ b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyWildcardElement.java @@ -45,7 +45,7 @@ final class GroovyWildcardElement extends GroovyClassElement implements Wildcard ElementAnnotationMetadataFactory annotationMetadataFactory) { super( upperType.visitorContext, - upperType.classNode, + upperType.getNativeType(), annotationMetadataFactory, upperType.getTypeArguments(), 0 diff --git a/inject-groovy/src/test/groovy/io/micronaut/ast/groovy/visitor/GroovyClassElementSpec.groovy b/inject-groovy/src/test/groovy/io/micronaut/ast/groovy/visitor/GroovyClassElementSpec.groovy index dacbcbdc270..ddb6bbea924 100644 --- a/inject-groovy/src/test/groovy/io/micronaut/ast/groovy/visitor/GroovyClassElementSpec.groovy +++ b/inject-groovy/src/test/groovy/io/micronaut/ast/groovy/visitor/GroovyClassElementSpec.groovy @@ -26,7 +26,7 @@ class GroovyClassElementSpec extends AbstractClassElementSpec { @Override protected List getClassElements() { def visitorContext = new GroovyVisitorContext(Mock(SourceUnit), null) - return [new GroovyClassElement(visitorContext, ClassHelper.OBJECT_TYPE, null), - new GroovyEnumElement(visitorContext, ClassHelper.Enum_Type, null)] + return [new GroovyClassElement(visitorContext, new GroovyNativeElement.Class(ClassHelper.OBJECT_TYPE), null), + new GroovyEnumElement(visitorContext, new GroovyNativeElement.Class(ClassHelper.Enum_Type), null)] } } diff --git a/inject-groovy/src/test/groovy/io/micronaut/inject/visitor/BeanIntrospectionSpec.groovy b/inject-groovy/src/test/groovy/io/micronaut/inject/visitor/BeanIntrospectionSpec.groovy index 4f179a3df92..c61db467007 100644 --- a/inject-groovy/src/test/groovy/io/micronaut/inject/visitor/BeanIntrospectionSpec.groovy +++ b/inject-groovy/src/test/groovy/io/micronaut/inject/visitor/BeanIntrospectionSpec.groovy @@ -2277,5 +2277,44 @@ class Test extends RecursiveGenerics { introspection != null } + void "test type_use annotations"() { + given: + def introspection = buildBeanIntrospection('test.Test', ''' +package test; +import io.micronaut.core.annotation.Introspected; +import io.micronaut.context.annotation.*; +import io.micronaut.inject.visitor.*; +@Introspected +class Test { + @io.micronaut.inject.visitor.TypeUseRuntimeAnn + private String name; + @io.micronaut.inject.visitor.TypeUseClassAnn + private String secondName; + public String getName() { + return name; + } + public void setName(String name) { + this.name = name; + } + public String getSecondName() { + return name; + } + public void setSecondName(String secondName) { + this.secondName = secondName; + } +} +''') + def nameField = introspection.getProperty("name").orElse(null) + def secondNameField = introspection.getProperty("secondName").orElse(null) + + expect: + nameField + secondNameField + + nameField.hasStereotype(TypeUseRuntimeAnn) + nameField.hasStereotype("io.micronaut.inject.visitor.TypeUseRuntimeAnn") + !secondNameField.hasStereotype(TypeUseClassAnn) + !secondNameField.hasStereotype("io.micronaut.inject.visitor.TypeUseClassAnn") + } } diff --git a/inject-groovy/src/test/groovy/io/micronaut/inject/visitor/ClassElementSpec.groovy b/inject-groovy/src/test/groovy/io/micronaut/inject/visitor/ClassElementSpec.groovy index 7db67895bda..710668ee056 100644 --- a/inject-groovy/src/test/groovy/io/micronaut/inject/visitor/ClassElementSpec.groovy +++ b/inject-groovy/src/test/groovy/io/micronaut/inject/visitor/ClassElementSpec.groovy @@ -24,12 +24,14 @@ import io.micronaut.inject.ast.ElementModifier import io.micronaut.inject.ast.ElementQuery import io.micronaut.inject.ast.EnumElement import io.micronaut.inject.ast.FieldElement +import io.micronaut.inject.ast.GenericPlaceholderElement import io.micronaut.inject.ast.MemberElement import io.micronaut.inject.ast.MethodElement import io.micronaut.inject.ast.PackageElement import io.micronaut.inject.ast.PrimitiveElement import io.micronaut.inject.ast.PropertyElement import io.micronaut.inject.ast.TypedElement +import io.micronaut.inject.ast.WildcardElement import spock.lang.Issue import spock.lang.PendingFeature import spock.lang.Unroll @@ -1040,7 +1042,6 @@ class Pet { genericReturnType.hasAnnotation(Introspected) } - @PendingFeature void "test annotation metadata present on deep type parameters for field"() { ClassElement ce = buildClassElement(''' package test; @@ -1076,7 +1077,6 @@ class Test { level3.getAnnotationMetadata().getAnnotationNames().asList() == ['javax.validation.constraints.NotNull$List'] } - @PendingFeature void "test annotation metadata present on deep type parameters for method"() { ClassElement ce = buildClassElement(''' package test; @@ -1114,6 +1114,77 @@ class Test { level3.getAnnotationMetadata().getAnnotationNames().asList() == ['javax.validation.constraints.NotNull$List'] } + void "test annotation metadata present on deep type parameters for method 2"() { + ClassElement ce = buildClassElement(''' +package test; +import io.micronaut.core.annotation.*; +import javax.validation.constraints.*; +import java.util.List; + +class Test { + List>> deepList() { + return null; + } +} +''') + expect: + def method = ce.getEnclosedElement(ElementQuery.ALL_METHODS.named("deepList")).get() + def theType = method.getGenericReturnType() + + theType.getAnnotationMetadata().getAnnotationNames().size() == 0 + + assertListGenericArgument(theType, { ClassElement listArg1 -> + assertListGenericArgument(listArg1, { ClassElement listArg2 -> + assertListGenericArgument(listArg2, { ClassElement listArg3 -> + assert listArg3.getAnnotationMetadata().getAnnotationNames().asList() == ['io.micronaut.inject.visitor.TypeUseRuntimeAnn', 'io.micronaut.inject.visitor.TypeUseClassAnn'] + }) + }) + }) + + def level1 = theType.getTypeArguments()["E"] + def level2 = level1.getTypeArguments()["E"] + def level3 = level2.getTypeArguments()["E"] + level3.getAnnotationMetadata().getAnnotationNames().asList() == ['io.micronaut.inject.visitor.TypeUseRuntimeAnn', 'io.micronaut.inject.visitor.TypeUseClassAnn' ] + } + + void "test annotations on recursive generic type parameter 1"() { + given: + ClassElement ce = buildClassElement('''\ +package test; + +final class TrackedSortedSet> { +} + +''') + expect: + def typeArguments = ce.getTypeArguments() + typeArguments.size() == 1 + def typeArgument = typeArguments.get("T") + typeArgument.name == "java.lang.Comparable" + typeArgument.getAnnotationNames().asList() == ['io.micronaut.inject.visitor.TypeUseRuntimeAnn'] + } + + @PendingFeature + void "test annotations on recursive generic type parameter 2"() { + given: + ClassElement ce = buildClassElement('''\ +package test; + +final class TrackedSortedSet> { +} + +''') + expect: + def typeArguments = ce.getTypeArguments() + typeArguments.size() == 1 + def typeArgument = typeArguments.get("T") + typeArgument.name == "java.lang.Comparable" + def nextTypeArguments = typeArgument.getTypeArguments() + def nextTypeArgument = nextTypeArguments.get("T") + nextTypeArgument.name == "java.lang.Comparable" + nextTypeArgument.getAnnotationNames().asList() == ['io.micronaut.inject.visitor.TypeUseRuntimeAnn'] + } + void "test recursive generic type parameter"() { given: ClassElement ce = buildClassElement('''\ @@ -1477,7 +1548,369 @@ class MyFactory { nextTypeArgument.name == "test.MyBuilder" def nextNextTypeArguments = nextTypeArgument.getTypeArguments() def nextNextTypeArgument = nextNextTypeArguments.get("T") - nextNextTypeArgument.name == "java.lang.Object" + nextNextTypeArgument.name == "test.MyBuilder" + def nextNextNextTypeArguments = nextNextTypeArgument.getTypeArguments() + def nextNextNextTypeArgument = nextNextNextTypeArguments.get("T") + nextNextNextTypeArgument.name == "java.lang.Object" + } + + void "test generics model"() { + ClassElement ce = buildClassElement(''' +package test; +import java.util.List; + +class Test { + List>> method1() { + return null; + } +} +''') + expect: + def method1 = ce.getEnclosedElement(ElementQuery.ALL_METHODS.named("method1")).get() + def genericType = method1.getGenericReturnType() + def genericTypeLevel1 = genericType.getTypeArguments()["E"] + !genericTypeLevel1.isGenericPlaceholder() + !genericTypeLevel1.isWildcard() + def genericTypeLevel2 = genericTypeLevel1.getTypeArguments()["E"] + !genericTypeLevel2.isGenericPlaceholder() + !genericTypeLevel2.isWildcard() + def genericTypeLevel3 = genericTypeLevel2.getTypeArguments()["E"] + !genericTypeLevel3.isGenericPlaceholder() + !genericTypeLevel3.isWildcard() + + def type = method1.getReturnType() + def typeLevel1 = type.getTypeArguments()["E"] + !typeLevel1.isGenericPlaceholder() + !typeLevel1.isWildcard() + def typeLevel2 = typeLevel1.getTypeArguments()["E"] + !typeLevel2.isGenericPlaceholder() + !typeLevel2.isWildcard() + def typeLevel3 = typeLevel2.getTypeArguments()["E"] + !typeLevel3.isGenericPlaceholder() + !typeLevel3.isWildcard() + } + + void "test generics model for wildcard"() { + ClassElement ce = buildClassElement(''' +package test; +import java.util.List; + +class Test { + + List method() { + return null; + } +} +''') + expect: + def method = ce.getEnclosedElement(ElementQuery.ALL_METHODS.named("method")).get() + def genericTypeArgument = method.getGenericReturnType().getTypeArguments()["E"] + !genericTypeArgument.isGenericPlaceholder() + !genericTypeArgument.isRawType() + genericTypeArgument.isWildcard() + + def typeArgument = method.getReturnType().getTypeArguments()["E"] + !typeArgument.isGenericPlaceholder() + !typeArgument.isRawType() + typeArgument.isWildcard() + } + + void "test generics model for placeholder"() { + ClassElement ce = buildClassElement(''' +package test; +import java.util.List; + +class Test { + + List method() { + return null; + } +} +''') + expect: + def method = ce.getEnclosedElement(ElementQuery.ALL_METHODS.named("method")).get() + def genericTypeArgument = method.getGenericReturnType().getTypeArguments()["E"] + genericTypeArgument.isGenericPlaceholder() + !genericTypeArgument.isRawType() + !genericTypeArgument.isWildcard() + + def typeArgument = method.getReturnType().getTypeArguments()["E"] + typeArgument.isGenericPlaceholder() + !typeArgument.isRawType() + !typeArgument.isWildcard() + } + + void "test generics model for class placeholder wildcard"() { + ClassElement ce = buildClassElement(''' +package test; +import java.util.List; + +class Test { + + List method() { + return null; + } +} +''') + expect: + def method = ce.getEnclosedElement(ElementQuery.ALL_METHODS.named("method")).get() + def genericTypeArgument = method.getGenericReturnType().getTypeArguments()["E"] + !genericTypeArgument.isGenericPlaceholder() + !genericTypeArgument.isRawType() + genericTypeArgument.isWildcard() + + def genericWildcard = genericTypeArgument as WildcardElement + !genericWildcard.lowerBounds + genericWildcard.upperBounds.size() == 1 + def genericUpperBound = genericWildcard.upperBounds[0] + genericUpperBound.name == "java.lang.Object" + genericUpperBound.isGenericPlaceholder() + !genericUpperBound.isWildcard() + !genericUpperBound.isRawType() + def genericPlaceholderUpperBound = genericUpperBound as GenericPlaceholderElement + genericPlaceholderUpperBound.variableName == "T" + genericPlaceholderUpperBound.declaringElement.get() == ce + + def typeArgument = method.getReturnType().getTypeArguments()["E"] + !typeArgument.isGenericPlaceholder() + !typeArgument.isRawType() + typeArgument.isWildcard() + + def wildcard = genericTypeArgument as WildcardElement + !wildcard.lowerBounds + wildcard.upperBounds.size() == 1 + def upperBound = wildcard.upperBounds[0] + upperBound.name == "java.lang.Object" + upperBound.isGenericPlaceholder() + !upperBound.isWildcard() + !upperBound.isRawType() + def placeholderUpperBound = upperBound as GenericPlaceholderElement + placeholderUpperBound.variableName == "T" + placeholderUpperBound.declaringElement.get() == ce + } + + void "test generics model for method placeholder wildcard"() { + ClassElement ce = buildClassElement(''' +package test; +import java.util.List; + +class Test { + + List method() { + return null; + } +} +''') + expect: + def method = ce.getEnclosedElement(ElementQuery.ALL_METHODS.named("method")).get() + method.getDeclaredTypeVariables().size() == 1 + method.getDeclaredTypeVariables()[0].declaringElement.get() == method + method.getDeclaredTypeVariables()[0].variableName == "T" + method.getDeclaredTypeArguments().size() == 1 + def placeholder = method.getDeclaredTypeArguments()["T"] as GenericPlaceholderElement + placeholder.declaringElement.get() == method + placeholder.variableName == "T" + def genericTypeArgument = method.getGenericReturnType().getTypeArguments()["E"] + !genericTypeArgument.isGenericPlaceholder() + !genericTypeArgument.isRawType() + genericTypeArgument.isWildcard() + + def genericWildcard = genericTypeArgument as WildcardElement + !genericWildcard.lowerBounds + genericWildcard.upperBounds.size() == 1 + def genericUpperBound = genericWildcard.upperBounds[0] + genericUpperBound.name == "java.lang.Object" + genericUpperBound.isGenericPlaceholder() + !genericUpperBound.isWildcard() + !genericUpperBound.isRawType() + def genericPlaceholderUpperBound = genericUpperBound as GenericPlaceholderElement + genericPlaceholderUpperBound.variableName == "T" + genericPlaceholderUpperBound.declaringElement.get() == method + + def typeArgument = method.getReturnType().getTypeArguments()["E"] + !typeArgument.isGenericPlaceholder() + !typeArgument.isRawType() + typeArgument.isWildcard() + + def wildcard = genericTypeArgument as WildcardElement + !wildcard.lowerBounds + wildcard.upperBounds.size() == 1 + def upperBound = wildcard.upperBounds[0] + upperBound.name == "java.lang.Object" + upperBound.isGenericPlaceholder() + !upperBound.isWildcard() + !upperBound.isRawType() + def placeholderUpperBound = upperBound as GenericPlaceholderElement + placeholderUpperBound.variableName == "T" + placeholderUpperBound.declaringElement.get() == method + } + + void "test generics model for constructor placeholder wildcard"() { + ClassElement ce = buildClassElement(''' +package test; +import java.util.List; + +class Test { + + Test(List list) { + } +} +''') + expect: + def method = ce.getPrimaryConstructor().get() + method.getDeclaredTypeVariables().size() == 1 + method.getDeclaredTypeVariables()[0].declaringElement.get() == method + method.getDeclaredTypeVariables()[0].variableName == "T" + method.getDeclaredTypeArguments().size() == 1 + def placeholder = method.getDeclaredTypeArguments()["T"] as GenericPlaceholderElement + placeholder.declaringElement.get() == method + placeholder.variableName == "T" + def genericTypeArgument = method.getParameters()[0].getGenericType().getTypeArguments()["E"] + !genericTypeArgument.isGenericPlaceholder() + !genericTypeArgument.isRawType() + genericTypeArgument.isWildcard() + + def genericWildcard = genericTypeArgument as WildcardElement + !genericWildcard.lowerBounds + genericWildcard.upperBounds.size() == 1 + def genericUpperBound = genericWildcard.upperBounds[0] + genericUpperBound.name == "java.lang.Object" + genericUpperBound.isGenericPlaceholder() + !genericUpperBound.isWildcard() + !genericUpperBound.isRawType() + def genericPlaceholderUpperBound = genericUpperBound as GenericPlaceholderElement + genericPlaceholderUpperBound.variableName == "T" + genericPlaceholderUpperBound.declaringElement.get() == method + + def typeArgument = method.getParameters()[0].getType().getTypeArguments()["E"] + !typeArgument.isGenericPlaceholder() + !typeArgument.isRawType() + typeArgument.isWildcard() + + def wildcard = genericTypeArgument as WildcardElement + !wildcard.lowerBounds + wildcard.upperBounds.size() == 1 + def upperBound = wildcard.upperBounds[0] + upperBound.name == "java.lang.Object" + upperBound.isGenericPlaceholder() + !upperBound.isWildcard() + !upperBound.isRawType() + def placeholderUpperBound = upperBound as GenericPlaceholderElement + placeholderUpperBound.variableName == "T" + placeholderUpperBound.declaringElement.get() == method + } + + void "test generics equality"() { + ClassElement ce = buildClassElement(''' +package test; +import java.util.List; + +class Test { + + Number number; + + Test(List list) { + } + + List method1() { + return null; + } + + List method2() { + return null; + } + + T method3() { + return null; + } + + List> method4() { + return null; + } + + List> method5() { + return null; + } + + Test method6() { + return null; + } + + Test method7() { + return null; + } + + Test method8() { + return null; + } + + Test method9() { + return null; + } + + Test method10() { + return null; + } +} +''') + expect: + def numberType = ce.getFields()[0].getType() + def constructor = ce.getPrimaryConstructor().get() + constructor.getParameters()[0].getGenericType().getTypeArguments(List).get("E") == numberType + constructor.getParameters()[0].getType().getTypeArguments(List).get("E") == numberType + + ce.findMethod("method1").get().getGenericReturnType().getTypeArguments(List).get("E") == numberType + ce.findMethod("method1").get().getReturnType().getTypeArguments(List).get("E") == numberType + + ce.findMethod("method2").get().getGenericReturnType().getTypeArguments(List).get("E") == numberType + ce.findMethod("method2").get().getReturnType().getTypeArguments(List).get("E") == numberType + + ce.findMethod("method3").get().getGenericReturnType() == numberType + ce.findMethod("method3").get().getReturnType() == numberType + + ce.findMethod("method4").get().getGenericReturnType().getTypeArguments(List).get("E").getTypeArguments(List).get("E") == numberType + ce.findMethod("method4").get().getReturnType().getTypeArguments(List).get("E").getTypeArguments(List).get("E") == numberType + + ce.findMethod("method5").get().getGenericReturnType().getTypeArguments(List).get("E").getTypeArguments(List).get("E") == numberType + ce.findMethod("method5").get().getReturnType().getTypeArguments(List).get("E").getTypeArguments(List).get("E") == numberType + + ce.findMethod("method6").get().getGenericReturnType().getTypeArguments("test.Test").get("T") == numberType + ce.findMethod("method6").get().getReturnType().getTypeArguments("test.Test").get("T") == numberType + + ce.findMethod("method7").get().getGenericReturnType().getTypeArguments("test.Test").get("T") == numberType + ce.findMethod("method7").get().getReturnType().getTypeArguments("test.Test").get("T") == numberType + + ce.findMethod("method8").get().getGenericReturnType().getTypeArguments("test.Test").get("T") == numberType + ce.findMethod("method8").get().getReturnType().getTypeArguments("test.Test").get("T") == numberType + + ce.findMethod("method9").get().getGenericReturnType().getTypeArguments("test.Test").get("T") == numberType + ce.findMethod("method9").get().getReturnType().getTypeArguments("test.Test").get("T") == numberType + + ce.findMethod("method10").get().getGenericReturnType().getTypeArguments("test.Test").get("T") == numberType + ce.findMethod("method10").get().getReturnType().getTypeArguments("test.Test").get("T") == numberType + } + + void "test inherit parameter annotation"() { + ClassElement ce = buildClassElement(''' +package test; +import java.util.List; + +interface MyApi { + + String get(@io.micronaut.inject.visitor.MyParameter('X-username') String username) +} + +class UserController implements MyApi { + + @Override + String get(String username) { + } + +} + +''') + expect: + ce.findMethod("get").get().getParameters()[0].hasAnnotation(MyParameter) } private void assertListGenericArgument(ClassElement type, Closure cl) { diff --git a/inject-groovy/src/test/groovy/io/micronaut/inject/visitor/MyParameter.java b/inject-groovy/src/test/groovy/io/micronaut/inject/visitor/MyParameter.java new file mode 100644 index 00000000000..2f0da9921ca --- /dev/null +++ b/inject-groovy/src/test/groovy/io/micronaut/inject/visitor/MyParameter.java @@ -0,0 +1,20 @@ +package io.micronaut.inject.visitor; + +import jakarta.inject.Qualifier; + +import java.lang.annotation.Documented; +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; + +@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.ANNOTATION_TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Qualifier +@Inherited +public @interface MyParameter { + + String value() default ""; +} diff --git a/inject-groovy/src/test/groovy/io/micronaut/inject/visitor/TypeUseClassAnn.java b/inject-groovy/src/test/groovy/io/micronaut/inject/visitor/TypeUseClassAnn.java new file mode 100644 index 00000000000..fabf9afd8b2 --- /dev/null +++ b/inject-groovy/src/test/groovy/io/micronaut/inject/visitor/TypeUseClassAnn.java @@ -0,0 +1,11 @@ +package io.micronaut.inject.visitor; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.TYPE_USE) +@Retention(RetentionPolicy.CLASS) +@interface TypeUseClassAnn { +} diff --git a/inject-groovy/src/test/groovy/io/micronaut/inject/visitor/TypeUseRuntimeAnn.java b/inject-groovy/src/test/groovy/io/micronaut/inject/visitor/TypeUseRuntimeAnn.java new file mode 100644 index 00000000000..40f18a22baa --- /dev/null +++ b/inject-groovy/src/test/groovy/io/micronaut/inject/visitor/TypeUseRuntimeAnn.java @@ -0,0 +1,11 @@ +package io.micronaut.inject.visitor; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.TYPE_USE) +@Retention(RetentionPolicy.RUNTIME) +@interface TypeUseRuntimeAnn { +} diff --git a/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaGenericPlaceholderElement.java b/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaGenericPlaceholderElement.java index a762911047a..d1c40907fe9 100644 --- a/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaGenericPlaceholderElement.java +++ b/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaGenericPlaceholderElement.java @@ -62,6 +62,11 @@ final class JavaGenericPlaceholderElement extends JavaClassElement implements Ge this.isRawType = isRawType; } + @Override + public Object getGenericNativeType() { + return realTypeVariable; + } + @Override public boolean isTypeVariable() { return true; @@ -72,26 +77,6 @@ public boolean isRawType() { return isRawType; } - @Override - public int hashCode() { - return realTypeVariable.hashCode(); - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null) { - return false; - } - io.micronaut.inject.ast.Element that = (io.micronaut.inject.ast.Element) o; - if (that instanceof JavaGenericPlaceholderElement placeholderElement) { - return placeholderElement.realTypeVariable.equals(realTypeVariable); - } - return false; - } - @Override public MutableAnnotationMetadataDelegate getAnnotationMetadata() { return bounds.get(0).getAnnotationMetadata(); From 484b0bd67300fdd7aa0e3fde6bcd36f8968e5ea8 Mon Sep 17 00:00:00 2001 From: Graeme Rocher Date: Wed, 8 Feb 2023 16:50:43 +0100 Subject: [PATCH 466/743] don't run listeners for bean providers (#8742) --- .../AllBeansListener.java | 35 +++++++++++++++++++ .../beancreationeventlistener/B.java | 1 + .../BeanCreationEventListenerSpec.groovy | 7 ++++ .../beancreationeventlistener/C.java | 4 +++ .../beancreationeventlistener/H.java | 7 ++++ .../micronaut/context/DefaultBeanContext.java | 22 +++++++----- 6 files changed, 67 insertions(+), 9 deletions(-) create mode 100644 inject-java/src/test/groovy/io/micronaut/inject/lifecycle/beancreationeventlistener/AllBeansListener.java create mode 100644 inject-java/src/test/groovy/io/micronaut/inject/lifecycle/beancreationeventlistener/H.java diff --git a/inject-java/src/test/groovy/io/micronaut/inject/lifecycle/beancreationeventlistener/AllBeansListener.java b/inject-java/src/test/groovy/io/micronaut/inject/lifecycle/beancreationeventlistener/AllBeansListener.java new file mode 100644 index 00000000000..112f514c88d --- /dev/null +++ b/inject-java/src/test/groovy/io/micronaut/inject/lifecycle/beancreationeventlistener/AllBeansListener.java @@ -0,0 +1,35 @@ +package io.micronaut.inject.lifecycle.beancreationeventlistener; + +import io.micronaut.context.BeanProvider; +import io.micronaut.context.env.Environment; +import io.micronaut.context.event.BeanCreatedEvent; +import io.micronaut.context.event.BeanCreatedEventListener; +import io.micronaut.context.event.BeanInitializedEventListener; +import io.micronaut.context.event.BeanInitializingEvent; +import jakarta.inject.Singleton; + +@Singleton +public class AllBeansListener implements BeanCreatedEventListener, BeanInitializedEventListener { + + public AllBeansListener( + BeanProvider provider +// uncommenting the following two lines stack overflows, perhaps we should fail compilation? +// , Environment environment, +// H h + ) { + } + + static boolean executed = false; + static boolean initialized = false; + @Override + public Object onCreated(BeanCreatedEvent event) { + executed = true; + return event.getBean(); + } + + @Override + public Object onInitialized(BeanInitializingEvent event) { + initialized = true; + return event.getBean(); + } +} diff --git a/inject-java/src/test/groovy/io/micronaut/inject/lifecycle/beancreationeventlistener/B.java b/inject-java/src/test/groovy/io/micronaut/inject/lifecycle/beancreationeventlistener/B.java index d2055e004a3..db1ab613847 100644 --- a/inject-java/src/test/groovy/io/micronaut/inject/lifecycle/beancreationeventlistener/B.java +++ b/inject-java/src/test/groovy/io/micronaut/inject/lifecycle/beancreationeventlistener/B.java @@ -19,6 +19,7 @@ @Singleton public class B { + String name; public String getName() { diff --git a/inject-java/src/test/groovy/io/micronaut/inject/lifecycle/beancreationeventlistener/BeanCreationEventListenerSpec.groovy b/inject-java/src/test/groovy/io/micronaut/inject/lifecycle/beancreationeventlistener/BeanCreationEventListenerSpec.groovy index 3fce86fb4fc..6db0d801205 100644 --- a/inject-java/src/test/groovy/io/micronaut/inject/lifecycle/beancreationeventlistener/BeanCreationEventListenerSpec.groovy +++ b/inject-java/src/test/groovy/io/micronaut/inject/lifecycle/beancreationeventlistener/BeanCreationEventListenerSpec.groovy @@ -94,6 +94,13 @@ class BeanCreationEventListenerSpec extends Specification { OffendingInterfaceListener.executed == true OffendingMethodListener.initialized == true OffendingMethodListener.executed == true + AllBeansListener.executed == true + + when: + C c = context.getBean(C) + + then: + c != null cleanup: context.close() diff --git a/inject-java/src/test/groovy/io/micronaut/inject/lifecycle/beancreationeventlistener/C.java b/inject-java/src/test/groovy/io/micronaut/inject/lifecycle/beancreationeventlistener/C.java index beb60292f43..4edc3668bc6 100644 --- a/inject-java/src/test/groovy/io/micronaut/inject/lifecycle/beancreationeventlistener/C.java +++ b/inject-java/src/test/groovy/io/micronaut/inject/lifecycle/beancreationeventlistener/C.java @@ -1,7 +1,11 @@ package io.micronaut.inject.lifecycle.beancreationeventlistener; +import io.micronaut.context.BeanProvider; +import io.micronaut.context.env.Environment; import jakarta.inject.Singleton; @Singleton public class C { + public C(BeanProvider env) { + } } diff --git a/inject-java/src/test/groovy/io/micronaut/inject/lifecycle/beancreationeventlistener/H.java b/inject-java/src/test/groovy/io/micronaut/inject/lifecycle/beancreationeventlistener/H.java new file mode 100644 index 00000000000..752d438809d --- /dev/null +++ b/inject-java/src/test/groovy/io/micronaut/inject/lifecycle/beancreationeventlistener/H.java @@ -0,0 +1,7 @@ +package io.micronaut.inject.lifecycle.beancreationeventlistener; + +import jakarta.inject.Singleton; + +@Singleton +public class H { +} diff --git a/inject/src/main/java/io/micronaut/context/DefaultBeanContext.java b/inject/src/main/java/io/micronaut/context/DefaultBeanContext.java index 7a3d344413f..2173c0fd3ac 100644 --- a/inject/src/main/java/io/micronaut/context/DefaultBeanContext.java +++ b/inject/src/main/java/io/micronaut/context/DefaultBeanContext.java @@ -109,6 +109,8 @@ import io.micronaut.inject.ProxyBeanDefinition; import io.micronaut.inject.QualifiedBeanType; import io.micronaut.inject.ValidatedBeanDefinition; +import io.micronaut.inject.provider.AbstractProviderDefinition; +import io.micronaut.inject.provider.BeanProviderDefinition; import io.micronaut.inject.proxy.InterceptedBeanProxy; import io.micronaut.inject.qualifiers.AnyQualifier; import io.micronaut.inject.qualifiers.Qualified; @@ -2408,15 +2410,17 @@ private T triggerBeanCreatedEventListener(@NonNull BeanResolutionContext res @NonNull BeanDefinition beanDefinition, @NonNull T bean, @Nullable Qualifier finalQualifier) { - Class beanType = beanDefinition.getBeanType(); - if (!(bean instanceof BeanCreatedEventListener) && CollectionUtils.isNotEmpty(beanCreationEventListeners)) { - for (Map.Entry, ListenersSupplier> entry : beanCreationEventListeners) { - if (entry.getKey().isAssignableFrom(beanType)) { - BeanKey beanKey = new BeanKey<>(beanDefinition, finalQualifier); - for (BeanCreatedEventListener listener : entry.getValue().get(resolutionContext)) { - bean = (T) listener.onCreated(new BeanCreatedEvent(this, beanDefinition, beanKey, bean)); - if (bean == null) { - throw new BeanInstantiationException(resolutionContext, "Listener [" + listener + "] returned null from onCreated event"); + if (!(beanDefinition instanceof AbstractProviderDefinition)) { + Class beanType = beanDefinition.getBeanType(); + if (!(bean instanceof BeanCreatedEventListener) && CollectionUtils.isNotEmpty(beanCreationEventListeners)) { + for (Map.Entry, ListenersSupplier> entry : beanCreationEventListeners) { + if (entry.getKey().isAssignableFrom(beanType)) { + BeanKey beanKey = new BeanKey<>(beanDefinition, finalQualifier); + for (BeanCreatedEventListener listener : entry.getValue().get(resolutionContext)) { + bean = (T) listener.onCreated(new BeanCreatedEvent(this, beanDefinition, beanKey, bean)); + if (bean == null) { + throw new BeanInstantiationException(resolutionContext, "Listener [" + listener + "] returned null from onCreated event"); + } } } } From 9467956fafbfcda5fc7121b81390009ac8376a82 Mon Sep 17 00:00:00 2001 From: Denis Stepanov Date: Thu, 9 Feb 2023 09:48:23 +0100 Subject: [PATCH 467/743] Correct how annotations are propagated for generics (#8743) * Correct how annotations are propagated for generics and build method metadata in a custom method --- .../AbstractAnnotationMetadataBuilder.java | 45 +++++-- ...tractElementAnnotationMetadataFactory.java | 2 +- ...trospectedToBeanPropertiesTransformer.java | 3 +- .../ExecutableMethodsDefinitionWriter.java | 52 +++++++- .../core/annotation/Introspected.java | 1 + .../io/micronaut/inject/visitor/Book.java | 8 ++ .../inject/visitor/ClassElementSpec.groovy | 83 ++++++++++++ .../io/micronaut/inject/visitor/MyEntity.java | 15 +++ .../visitor/JavaWildcardElement.java | 6 + .../io/micronaut/inject/executable/Book.java | 8 ++ .../executable/ExecutableBeanSpec.groovy | 121 +++++++++++++++++- .../micronaut/inject/executable/MyEntity.java | 15 +++ .../groovy/io/micronaut/visitors/Book.java | 8 ++ .../visitors/ClassElementSpec.groovy | 83 ++++++++++++ .../io/micronaut/visitors/MyEntity.java | 15 +++ 15 files changed, 442 insertions(+), 23 deletions(-) create mode 100644 inject-groovy/src/test/groovy/io/micronaut/inject/visitor/Book.java create mode 100644 inject-groovy/src/test/groovy/io/micronaut/inject/visitor/MyEntity.java create mode 100644 inject-java/src/test/groovy/io/micronaut/inject/executable/Book.java create mode 100644 inject-java/src/test/groovy/io/micronaut/inject/executable/MyEntity.java create mode 100644 inject-java/src/test/groovy/io/micronaut/visitors/Book.java create mode 100644 inject-java/src/test/groovy/io/micronaut/visitors/MyEntity.java diff --git a/core-processor/src/main/java/io/micronaut/inject/annotation/AbstractAnnotationMetadataBuilder.java b/core-processor/src/main/java/io/micronaut/inject/annotation/AbstractAnnotationMetadataBuilder.java index ebc6201eae5..477ed0f7366 100644 --- a/core-processor/src/main/java/io/micronaut/inject/annotation/AbstractAnnotationMetadataBuilder.java +++ b/core-processor/src/main/java/io/micronaut/inject/annotation/AbstractAnnotationMetadataBuilder.java @@ -265,10 +265,15 @@ private CachedAnnotationMetadata lookupOrBuild(boolean inheritTypeAnnotations, T * @return The annotation metadata */ public CachedAnnotationMetadata lookupOrBuild(Object key, T element, boolean includeTypeAnnotations) { - return MUTATED_ANNOTATION_METADATA.computeIfAbsent( - key, - metadataKey -> new DefaultCachedAnnotationMetadata(buildInternal(includeTypeAnnotations, false, element)) - ); + CachedAnnotationMetadata cachedAnnotationMetadata = MUTATED_ANNOTATION_METADATA.get(key); + if (cachedAnnotationMetadata == null) { + AnnotationMetadata annotationMetadata = buildInternal(includeTypeAnnotations, false, element); + cachedAnnotationMetadata = new DefaultCachedAnnotationMetadata(annotationMetadata); + } + // Don't use `computeIfAbsent` as it can lead to a concurrent exception because the cache is accessed during in `buildInternal` + MUTATED_ANNOTATION_METADATA.put(key, cachedAnnotationMetadata); + return cachedAnnotationMetadata; + } private AnnotationMetadata buildInternal(boolean inheritTypeAnnotations, boolean declaredOnly, T element) { @@ -649,14 +654,20 @@ private Map getAnnotationDefaults(T originatingElement, @NonNull private CachedAnnotationMetadata lookupExisting(T[] elements, Supplier annotationMetadataSupplier) { - return MUTATED_ANNOTATION_METADATA.computeIfAbsent(new MetadataKey<>(elements), metadataKey -> new DefaultCachedAnnotationMetadata(annotationMetadataSupplier.get())); + Object key = new MetadataKey<>(elements); + CachedAnnotationMetadata cachedAnnotationMetadata = MUTATED_ANNOTATION_METADATA.get(key); + if (cachedAnnotationMetadata == null) { + cachedAnnotationMetadata = new DefaultCachedAnnotationMetadata(annotationMetadataSupplier.get()); + } + // Don't use `computeIfAbsent` as it can lead to a concurrent exception because the cache is accessed during in `buildInternal` + MUTATED_ANNOTATION_METADATA.put(key, cachedAnnotationMetadata); + return cachedAnnotationMetadata; } @Nullable private void processAnnotationAlias(Map annotationValues, Object annotationValue, AnnotationValue aliasForAnnotation, - RetentionPolicy retentionPolicy, List introducedAnnotations) { Optional aliasAnnotation = aliasForAnnotation.stringValue("annotation"); Optional aliasAnnotationName = aliasForAnnotation.stringValue("annotationName"); @@ -670,7 +681,7 @@ private void processAnnotationAlias(Map annotationValues, if (annotationValue != null) { introducedAnnotations.add( toProcessedAnnotation( - AnnotationValue.builder(aliasedAnnotation, retentionPolicy) + AnnotationValue.builder(aliasedAnnotation, getRetentionPolicy(aliasedAnnotation)) .members(Collections.singletonMap(aliasedMemberName, annotationValue)) .build() ) @@ -694,6 +705,17 @@ private void processAnnotationAlias(Map annotationValues, @NonNull protected abstract RetentionPolicy getRetentionPolicy(@NonNull T annotation); + /** + * Gets the retention policy for the given annotation. + * + * @param annotation The annotation + * @return The retention policy + */ + @NonNull + public RetentionPolicy getRetentionPolicy(@NonNull String annotation) { + return getAnnotationMirror(annotation).map(this::getRetentionPolicy).orElse(RetentionPolicy.RUNTIME); + } + private AnnotationMetadata buildInternalMulti( List parents, T element, @@ -878,7 +900,6 @@ private void handleAnnotationAlias(T originatingElement, Map annotationValues, T annotationMember, Object annotationValue, - RetentionPolicy retentionPolicy, List introducedAnnotations) { Optional> aliases = getAnnotationValues(originatingElement, annotationMember, Aliases.class); if (aliases.isPresent()) { @@ -887,7 +908,6 @@ private void handleAnnotationAlias(T originatingElement, annotationValues, annotationValue, av, - retentionPolicy, introducedAnnotations ); } @@ -898,7 +918,6 @@ private void handleAnnotationAlias(T originatingElement, annotationValues, annotationValue, aliasForValues.get(), - retentionPolicy, introducedAnnotations ); } @@ -1030,7 +1049,10 @@ private Stream flattenRepeatable(ProcessedAnnotation proces Stream.of( // Add repeatable container for possible stereotype annotation retrieval // and additional members defined in the container annotation - toProcessedAnnotation(new AnnotationValue<>(annotationValue.getAnnotationName(), containerValues)) + toProcessedAnnotation(new AnnotationValue<>( + annotationValue.getAnnotationName(), + containerValues, + getRetentionPolicy(annotationValue.getAnnotationName()))) ), repeatableAnnotations.stream().map(this::toProcessedAnnotation) ); @@ -1066,7 +1088,6 @@ private ProcessedAnnotation processAliases(ProcessedAnnotation processedAnnotati newValues, member, value, - annotationValue.getRetentionPolicy(), introducedAnnotations ); } diff --git a/core-processor/src/main/java/io/micronaut/inject/ast/annotation/AbstractElementAnnotationMetadataFactory.java b/core-processor/src/main/java/io/micronaut/inject/ast/annotation/AbstractElementAnnotationMetadataFactory.java index 3684f57dfef..8075ea9fc85 100644 --- a/core-processor/src/main/java/io/micronaut/inject/ast/annotation/AbstractElementAnnotationMetadataFactory.java +++ b/core-processor/src/main/java/io/micronaut/inject/ast/annotation/AbstractElementAnnotationMetadataFactory.java @@ -371,7 +371,7 @@ private AnnotationMetadata replaceAnnotationsInternal(AnnotationMetadata annotat @SuppressWarnings("java:S1192") public AnnotationMetadata annotate(@NonNull String annotationType, @NonNull Consumer> consumer) { ArgumentUtils.requireNonNull("annotationType", annotationType); - AnnotationValueBuilder builder = AnnotationValue.builder(annotationType); + AnnotationValueBuilder builder = AnnotationValue.builder(annotationType, metadataBuilder.getRetentionPolicy(annotationType)); //noinspection ConstantConditions if (consumer != null) { consumer.accept(builder); diff --git a/core-processor/src/main/java/io/micronaut/inject/beans/visitor/IntrospectedToBeanPropertiesTransformer.java b/core-processor/src/main/java/io/micronaut/inject/beans/visitor/IntrospectedToBeanPropertiesTransformer.java index 1a270f86bbb..c79e76f5218 100644 --- a/core-processor/src/main/java/io/micronaut/inject/beans/visitor/IntrospectedToBeanPropertiesTransformer.java +++ b/core-processor/src/main/java/io/micronaut/inject/beans/visitor/IntrospectedToBeanPropertiesTransformer.java @@ -23,6 +23,7 @@ import io.micronaut.inject.annotation.TypedAnnotationTransformer; import io.micronaut.inject.visitor.VisitorContext; +import java.lang.annotation.RetentionPolicy; import java.util.Arrays; import java.util.List; @@ -49,7 +50,7 @@ public List> transform(AnnotationValue annotati } return Arrays.asList( annotation, - AnnotationValue.builder(BeanProperties.class) + AnnotationValue.builder(BeanProperties.class.getName(), RetentionPolicy.CLASS) .member(BeanProperties.MEMBER_ACCESS_KIND, accessKinds) .member(BeanProperties.MEMBER_VISIBILITY, visibilities) .member(BeanProperties.MEMBER_INCLUDES, annotation.stringValues(BeanProperties.MEMBER_INCLUDES)) diff --git a/core-processor/src/main/java/io/micronaut/inject/writer/ExecutableMethodsDefinitionWriter.java b/core-processor/src/main/java/io/micronaut/inject/writer/ExecutableMethodsDefinitionWriter.java index 8e94b5fe13e..1e03bdfadcf 100644 --- a/core-processor/src/main/java/io/micronaut/inject/writer/ExecutableMethodsDefinitionWriter.java +++ b/core-processor/src/main/java/io/micronaut/inject/writer/ExecutableMethodsDefinitionWriter.java @@ -41,10 +41,11 @@ import java.io.OutputStream; import java.util.ArrayList; import java.util.Arrays; -import java.util.HashMap; +import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.TreeMap; import java.util.stream.Collectors; @@ -89,12 +90,13 @@ public class ExecutableMethodsDefinitionWriter extends AbstractClassFileWriter i private final Type thisType; private final String beanDefinitionReferenceClassName; - private final Map defaultsStorage = new HashMap<>(); private final Map loadTypeMethods = new LinkedHashMap<>(); private final List addedMethods = new ArrayList<>(); private final DispatchWriter methodDispatchWriter; + private final Set methodNames = new HashSet<>(); + public ExecutableMethodsDefinitionWriter(String beanDefinitionClassName, String beanDefinitionReferenceClassName, OriginatingElements originatingElements) { @@ -358,6 +360,42 @@ private void pushNewMethodReference(ClassWriter classWriter, GeneratorAdapter staticInit, TypedElement declaringType, MethodElement methodElement) { + int index = 1; + String prefix = "$metadata$"; + String methodName = prefix + methodElement.getName(); + while (methodNames.contains(methodName)) { + methodName = prefix + methodElement.getName() + "$" + (index++); + } + methodNames.add(methodName); + + Method newMethod = new Method(methodName, Type.getType(AbstractExecutableMethodsDefinition.MethodReference.class), new Type[0]); + + GeneratorAdapter newMethodAdapter = new GeneratorAdapter(classWriter.visitMethod( + Opcodes.ACC_PRIVATE | Opcodes.ACC_FINAL | Opcodes.ACC_STATIC, + newMethod.getName(), + newMethod.getDescriptor(), + null, + null), + ACC_PRIVATE | Opcodes.ACC_FINAL | Opcodes.ACC_STATIC, + newMethod.getName(), + newMethod.getDescriptor() + ); + + pushNewMethodReference0(classWriter, newMethodAdapter, declaringType, methodElement, new LinkedHashMap<>()); + + newMethodAdapter.returnValue(); + newMethodAdapter.visitMaxs(DEFAULT_MAX_STACK, 1); + newMethodAdapter.visitEnd(); + + staticInit.invokeStatic(thisType, newMethod); + } + + private void pushNewMethodReference0(ClassWriter classWriter, + GeneratorAdapter staticInit, + TypedElement declaringType, + MethodElement methodElement, + Map defaultsStorage) { + staticInit.newInstance(Type.getType(AbstractExecutableMethodsDefinition.MethodReference.class)); staticInit.dup(); // 1: declaringType @@ -366,8 +404,7 @@ private void pushNewMethodReference(ClassWriter classWriter, // 2: annotationMetadata AnnotationMetadata annotationMetadata = methodElement.getTargetAnnotationMetadata(); - if (annotationMetadata instanceof AnnotationMetadataHierarchy) { - AnnotationMetadataHierarchy hierarchy = (AnnotationMetadataHierarchy) annotationMetadata; + if (annotationMetadata instanceof AnnotationMetadataHierarchy hierarchy) { if (hierarchy.size() != 2) { throw new IllegalStateException("Expected the size of 2"); } @@ -379,7 +416,7 @@ private void pushNewMethodReference(ClassWriter classWriter, } } - pushAnnotationMetadata(classWriter, staticInit, annotationMetadata); + pushAnnotationMetadata(classWriter, staticInit, annotationMetadata, defaultsStorage); // 3: methodName staticInit.push(methodElement.getName()); // 4: return argument @@ -417,7 +454,10 @@ private void pushNewMethodReference(ClassWriter classWriter, boolean.class); } - private void pushAnnotationMetadata(ClassWriter classWriter, GeneratorAdapter staticInit, AnnotationMetadata annotationMetadata) { + private void pushAnnotationMetadata(ClassWriter classWriter, + GeneratorAdapter staticInit, + AnnotationMetadata annotationMetadata, + Map defaultsStorage) { if (annotationMetadata == AnnotationMetadata.EMPTY_METADATA || annotationMetadata.isEmpty()) { staticInit.push((String) null); } else if (annotationMetadata instanceof AnnotationMetadataReference annotationMetadataReference) { diff --git a/core/src/main/java/io/micronaut/core/annotation/Introspected.java b/core/src/main/java/io/micronaut/core/annotation/Introspected.java index cb8034244eb..6767e6ac0f7 100644 --- a/core/src/main/java/io/micronaut/core/annotation/Introspected.java +++ b/core/src/main/java/io/micronaut/core/annotation/Introspected.java @@ -17,6 +17,7 @@ import java.lang.annotation.*; +import static java.lang.annotation.RetentionPolicy.CLASS; import static java.lang.annotation.RetentionPolicy.RUNTIME; /** diff --git a/inject-groovy/src/test/groovy/io/micronaut/inject/visitor/Book.java b/inject-groovy/src/test/groovy/io/micronaut/inject/visitor/Book.java new file mode 100644 index 00000000000..7d5e11ee218 --- /dev/null +++ b/inject-groovy/src/test/groovy/io/micronaut/inject/visitor/Book.java @@ -0,0 +1,8 @@ +package io.micronaut.inject.visitor; + +import io.micronaut.core.annotation.Introspected; + +@MyEntity +@Introspected +public class Book { +} diff --git a/inject-groovy/src/test/groovy/io/micronaut/inject/visitor/ClassElementSpec.groovy b/inject-groovy/src/test/groovy/io/micronaut/inject/visitor/ClassElementSpec.groovy index 710668ee056..7ca5952f4d8 100644 --- a/inject-groovy/src/test/groovy/io/micronaut/inject/visitor/ClassElementSpec.groovy +++ b/inject-groovy/src/test/groovy/io/micronaut/inject/visitor/ClassElementSpec.groovy @@ -1913,6 +1913,89 @@ class UserController implements MyApi { ce.findMethod("get").get().getParameters()[0].hasAnnotation(MyParameter) } + void "test how the annotations from the type are propagated"() { + given: + ClassElement ce = buildClassElement('''\ +package test; + +import io.micronaut.inject.annotation.*; +import io.micronaut.context.annotation.*; +import java.util.List; +import io.micronaut.inject.visitor.Book; + +@jakarta.inject.Singleton +class MyBean { + + @Executable + public void saveAll(List books) { + } + + @Executable + public void saveAll2(List book) { + } + + @Executable + public void saveAll3(List book) { + } + + @Executable + public void save2(Book book) { + } + + @Executable + public void save3(T book) { + } + + @Executable + public Book get() { + return null; + } +} + +''') + when: + def saveAll = ce.findMethod("saveAll").get() + def listTypeArgument = saveAll.getParameters()[0].getType().getTypeArguments(List).get("E") + then: + listTypeArgument.hasAnnotation(MyEntity) + listTypeArgument.hasAnnotation(Introspected.class) + + when: + def saveAll2 = ce.findMethod("saveAll2").get() + def listTypeArgument2 = saveAll2.getParameters()[0].getType().getTypeArguments(List).get("E") + then: + listTypeArgument2.hasAnnotation(MyEntity.class) + listTypeArgument2.hasAnnotation(Introspected.class) + + when: + def saveAll3 = ce.findMethod("saveAll3").get() + def listTypeArgument3 = saveAll3.getParameters()[0].getType().getTypeArguments(List).get("E") + then: + listTypeArgument3.hasAnnotation(MyEntity.class) + listTypeArgument3.hasAnnotation(Introspected.class) + + when: + def save2 = ce.findMethod("save2").get() + def parameter2 = save2.getParameters()[0].getType() + then: + parameter2.hasAnnotation(MyEntity.class) + parameter2.hasAnnotation(Introspected.class) + + when: + def save3 = ce.findMethod("save3").get() + def parameter3 = save3.getParameters()[0].getType() + then: + parameter3.hasAnnotation(MyEntity.class) + parameter3.hasAnnotation(Introspected.class) + + when: + def get = ce.findMethod("get").get() + def returnType = get.getReturnType() + then: + returnType.hasAnnotation(MyEntity.class) + returnType.hasAnnotation(Introspected.class) + } + private void assertListGenericArgument(ClassElement type, Closure cl) { def arg1 = type.getAllTypeArguments().get(List.class.name).get("E") def arg2 = type.getAllTypeArguments().get(Collection.class.name).get("E") diff --git a/inject-groovy/src/test/groovy/io/micronaut/inject/visitor/MyEntity.java b/inject-groovy/src/test/groovy/io/micronaut/inject/visitor/MyEntity.java new file mode 100644 index 00000000000..1b1d96980df --- /dev/null +++ b/inject-groovy/src/test/groovy/io/micronaut/inject/visitor/MyEntity.java @@ -0,0 +1,15 @@ +package io.micronaut.inject.visitor; + +import io.micronaut.core.annotation.Internal; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +@Retention(RUNTIME) +@Target({ElementType.METHOD, ElementType.TYPE, ElementType.FIELD, ElementType.PARAMETER}) +@Internal +public @interface MyEntity { +} diff --git a/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaWildcardElement.java b/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaWildcardElement.java index 1178dbafeaa..5b75cc6d911 100644 --- a/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaWildcardElement.java +++ b/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaWildcardElement.java @@ -21,6 +21,7 @@ import io.micronaut.inject.ast.ClassElement; import io.micronaut.inject.ast.WildcardElement; import io.micronaut.inject.ast.annotation.ElementAnnotationMetadataFactory; +import io.micronaut.inject.ast.annotation.MutableAnnotationMetadataDelegate; import javax.lang.model.type.WildcardType; import java.util.Collection; @@ -58,6 +59,11 @@ final class JavaWildcardElement extends JavaClassElement implements WildcardElem this.lowerBounds = lowerBounds; } + @Override + public MutableAnnotationMetadataDelegate getAnnotationMetadata() { + return upperBound.getAnnotationMetadata(); + } + @Override public boolean isTypeVariable() { return true; diff --git a/inject-java/src/test/groovy/io/micronaut/inject/executable/Book.java b/inject-java/src/test/groovy/io/micronaut/inject/executable/Book.java new file mode 100644 index 00000000000..a882ce4950e --- /dev/null +++ b/inject-java/src/test/groovy/io/micronaut/inject/executable/Book.java @@ -0,0 +1,8 @@ +package io.micronaut.inject.executable; + +import io.micronaut.core.annotation.Introspected; + +@MyEntity +@Introspected +public class Book { +} diff --git a/inject-java/src/test/groovy/io/micronaut/inject/executable/ExecutableBeanSpec.groovy b/inject-java/src/test/groovy/io/micronaut/inject/executable/ExecutableBeanSpec.groovy index aa79fd3e826..3e48c263f37 100644 --- a/inject-java/src/test/groovy/io/micronaut/inject/executable/ExecutableBeanSpec.groovy +++ b/inject-java/src/test/groovy/io/micronaut/inject/executable/ExecutableBeanSpec.groovy @@ -16,7 +16,10 @@ package io.micronaut.inject.executable import io.micronaut.annotation.processing.test.AbstractTypeElementSpec +import io.micronaut.context.annotation.BeanProperties +import io.micronaut.core.annotation.Introspected import io.micronaut.inject.BeanDefinition +import io.micronaut.inject.validation.RequiresValidation import spock.lang.Issue class ExecutableBeanSpec extends AbstractTypeElementSpec { @@ -65,10 +68,10 @@ class MyBean extends Parent { class Parent { protected void protectedMethod() { } - + void packagePrivateMethod() { } - + private void privateMethod() { } } @@ -102,6 +105,118 @@ class MyBean { definition == null } + void "test how annotations are preserved"() { + given: + BeanDefinition definition = buildBeanDefinition('test.MyBean','''\ +package test; + +import io.micronaut.inject.annotation.*; +import io.micronaut.context.annotation.*; +import javax.validation.Valid; +import java.util.List; + +@jakarta.inject.Singleton +class MyBean { + + @Executable + public void saveAll(@Valid List books) { + } + + @Executable + public void saveAll2(@Valid List book) { + } + + @Executable + public void saveAll3(@Valid List book) { + } + + @Executable + public void save2(@Valid io.micronaut.inject.executable.Book book) { + } + + @Executable + public void save3(@Valid T book) { + } + + @Executable + public io.micronaut.inject.executable.Book get() { + return null; + } +} + +''') + when: + def saveAll = definition.findMethod("saveAll", List.class).get() + def listTypeArgument = saveAll.getArguments()[0].getTypeParameters()[0] + then: + !saveAll.hasAnnotation(RequiresValidation) + !saveAll.hasStereotype(RequiresValidation) + listTypeArgument.getAnnotationMetadata().hasAnnotation(MyEntity.class) + listTypeArgument.getAnnotationMetadata().hasAnnotation(Introspected.class) + listTypeArgument.getAnnotationMetadata().hasStereotype(Introspected.class) + !listTypeArgument.getAnnotationMetadata().hasAnnotation(BeanProperties.class) + !listTypeArgument.getAnnotationMetadata().hasStereotype(BeanProperties.class) + + when: + def saveAll2 = definition.findMethod("saveAll2", List.class).get() + def listTypeArgument2 = saveAll2.getArguments()[0].getTypeParameters()[0] + then: + !saveAll2.hasAnnotation(RequiresValidation) + !saveAll2.hasStereotype(RequiresValidation) + listTypeArgument2.getAnnotationMetadata().hasAnnotation(MyEntity.class) + listTypeArgument2.getAnnotationMetadata().hasAnnotation(Introspected.class) + listTypeArgument2.getAnnotationMetadata().hasStereotype(Introspected.class) + !listTypeArgument2.getAnnotationMetadata().hasAnnotation(BeanProperties.class) + !listTypeArgument2.getAnnotationMetadata().hasStereotype(BeanProperties.class) + + when: + def saveAll3 = definition.findMethod("saveAll3", List.class).get() + def listTypeArgument3 = saveAll3.getArguments()[0].getTypeParameters()[0] + then: + !saveAll3.hasAnnotation(RequiresValidation) + !saveAll3.hasStereotype(RequiresValidation) + listTypeArgument3.getAnnotationMetadata().hasAnnotation(MyEntity.class) + listTypeArgument3.getAnnotationMetadata().hasAnnotation(Introspected.class) + listTypeArgument3.getAnnotationMetadata().hasStereotype(Introspected.class) + !listTypeArgument3.getAnnotationMetadata().hasAnnotation(BeanProperties.class) + !listTypeArgument3.getAnnotationMetadata().hasStereotype(BeanProperties.class) +// TODO: validate this behaviour +// +// when: +// def save2 = definition.findMethod("save2", Book.class).get() +// def parameter2 = save2.getArguments()[0] +// then: +// !save2.hasAnnotation(RequiresValidation) +// !save2.hasStereotype(RequiresValidation) +// parameter2.getAnnotationMetadata().hasAnnotation(MyEntity.class) +// parameter2.getAnnotationMetadata().hasAnnotation(Introspected.class) +// parameter2.getAnnotationMetadata().hasStereotype(Introspected.class) +// !parameter2.getAnnotationMetadata().hasAnnotation(BeanProperties.class) +// !parameter2.getAnnotationMetadata().hasStereotype(BeanProperties.class) +// +// when: +// def save3 = definition.findMethod("save3", Book.class).get() +// def parameter3 = save3.getArguments()[0] +// then: +// !save3.hasAnnotation(RequiresValidation) +// !save3.hasStereotype(RequiresValidation) +// parameter3.getAnnotationMetadata().hasAnnotation(MyEntity.class) +// parameter3.getAnnotationMetadata().hasAnnotation(Introspected.class) +// parameter3.getAnnotationMetadata().hasStereotype(Introspected.class) +// !parameter3.getAnnotationMetadata().hasAnnotation(BeanProperties.class) +// !parameter3.getAnnotationMetadata().hasStereotype(BeanProperties.class) +// +// when: +// def get = definition.findMethod("get").get() +// def returnType = get.getReturnType() +// then: +// returnType.getAnnotationMetadata().hasAnnotation(MyEntity.class) +// returnType.getAnnotationMetadata().hasAnnotation(Introspected.class) +// returnType.getAnnotationMetadata().hasStereotype(Introspected.class) +// !returnType.getAnnotationMetadata().hasAnnotation(BeanProperties.class) +// !returnType.getAnnotationMetadata().hasStereotype(BeanProperties.class) + } + void "test multiple executable annotations on a method"() { given: BeanDefinition definition = buildBeanDefinition('test.MyBean','''\ @@ -117,7 +232,7 @@ class MyBean { @RepeatableExecutable("a") @RepeatableExecutable("b") public void run() { - + } } ''') diff --git a/inject-java/src/test/groovy/io/micronaut/inject/executable/MyEntity.java b/inject-java/src/test/groovy/io/micronaut/inject/executable/MyEntity.java new file mode 100644 index 00000000000..80b37b9c634 --- /dev/null +++ b/inject-java/src/test/groovy/io/micronaut/inject/executable/MyEntity.java @@ -0,0 +1,15 @@ +package io.micronaut.inject.executable; + +import io.micronaut.core.annotation.Internal; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +@Retention(RUNTIME) +@Target({ElementType.METHOD, ElementType.TYPE, ElementType.FIELD, ElementType.PARAMETER}) +@Internal +public @interface MyEntity { +} diff --git a/inject-java/src/test/groovy/io/micronaut/visitors/Book.java b/inject-java/src/test/groovy/io/micronaut/visitors/Book.java new file mode 100644 index 00000000000..22e0e1ddb96 --- /dev/null +++ b/inject-java/src/test/groovy/io/micronaut/visitors/Book.java @@ -0,0 +1,8 @@ +package io.micronaut.visitors; + +import io.micronaut.core.annotation.Introspected; + +@MyEntity +@Introspected +public class Book { +} diff --git a/inject-java/src/test/groovy/io/micronaut/visitors/ClassElementSpec.groovy b/inject-java/src/test/groovy/io/micronaut/visitors/ClassElementSpec.groovy index 57f6842f573..2ff266996f7 100644 --- a/inject-java/src/test/groovy/io/micronaut/visitors/ClassElementSpec.groovy +++ b/inject-java/src/test/groovy/io/micronaut/visitors/ClassElementSpec.groovy @@ -2104,6 +2104,89 @@ class MyBean { nextNextTypeArgument.name == "java.lang.Object" } + void "test how the annotations from the type are propagated"() { + given: + ClassElement ce = buildClassElement('''\ +package test; + +import io.micronaut.inject.annotation.*; +import io.micronaut.context.annotation.*; +import java.util.List; +import io.micronaut.visitors.Book; + +@jakarta.inject.Singleton +class MyBean { + + @Executable + public void saveAll(List books) { + } + + @Executable + public void saveAll2(List book) { + } + + @Executable + public void saveAll3(List book) { + } + + @Executable + public void save2(Book book) { + } + + @Executable + public void save3(T book) { + } + + @Executable + public Book get() { + return null; + } +} + +''') + when: + def saveAll = ce.findMethod("saveAll").get() + def listTypeArgument = saveAll.getParameters()[0].getType().getTypeArguments(List).get("E") + then: + listTypeArgument.hasAnnotation(MyEntity.class) + listTypeArgument.hasAnnotation(Introspected.class) + + when: + def saveAll2 = ce.findMethod("saveAll2").get() + def listTypeArgument2 = saveAll2.getParameters()[0].getType().getTypeArguments(List).get("E") + then: + listTypeArgument2.hasAnnotation(MyEntity.class) + listTypeArgument2.hasAnnotation(Introspected.class) + + when: + def saveAll3 = ce.findMethod("saveAll3").get() + def listTypeArgument3 = saveAll3.getParameters()[0].getType().getTypeArguments(List).get("E") + then: + listTypeArgument3.hasAnnotation(MyEntity.class) + listTypeArgument3.hasAnnotation(Introspected.class) + + when: + def save2 = ce.findMethod("save2").get() + def parameter2 = save2.getParameters()[0].getType() + then: + parameter2.hasAnnotation(MyEntity.class) + parameter2.hasAnnotation(Introspected.class) + + when: + def save3 = ce.findMethod("save3").get() + def parameter3 = save3.getParameters()[0].getType() + then: + parameter3.hasAnnotation(MyEntity.class) + parameter3.hasAnnotation(Introspected.class) + + when: + def get = ce.findMethod("get").get() + def returnType = get.getReturnType() + then: + returnType.hasAnnotation(MyEntity.class) + returnType.hasAnnotation(Introspected.class) + } + private void assertListGenericArgument(ClassElement type, Closure cl) { def arg1 = type.getAllTypeArguments().get(List.class.name).get("E") def arg2 = type.getAllTypeArguments().get(Collection.class.name).get("E") diff --git a/inject-java/src/test/groovy/io/micronaut/visitors/MyEntity.java b/inject-java/src/test/groovy/io/micronaut/visitors/MyEntity.java new file mode 100644 index 00000000000..4fcacc81b15 --- /dev/null +++ b/inject-java/src/test/groovy/io/micronaut/visitors/MyEntity.java @@ -0,0 +1,15 @@ +package io.micronaut.visitors; + +import io.micronaut.core.annotation.Internal; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +@Retention(RUNTIME) +@Target({ElementType.METHOD, ElementType.TYPE, ElementType.FIELD, ElementType.PARAMETER}) +@Internal +public @interface MyEntity { +} From 484958548f6c0cda74e5c61553320a8ff36febaf Mon Sep 17 00:00:00 2001 From: Graeme Rocher Date: Thu, 9 Feb 2023 15:34:27 +0100 Subject: [PATCH 468/743] Fix support for executable methods on introspections that require reflection (#8748) --- .../inject/visitor/beans/Auditable.java | 20 ++++++++++ .../BeanIntrospectionGenericsSpec.groovy | 3 ++ .../beans/BeanIntrospectionSpec.groovy | 39 +++++++++++++++++++ .../AbstractExecutableMethodsDefinition.java | 15 ++----- ...bstractInitializableBeanIntrospection.java | 31 ++++++++++++++- .../visitor/BeanIntrospectionWriter.java | 3 +- .../inject/writer/DispatchWriter.java | 22 ++++++++--- 7 files changed, 115 insertions(+), 18 deletions(-) create mode 100644 inject-java-test/src/test/groovy/io/micronaut/inject/visitor/beans/Auditable.java diff --git a/inject-java-test/src/test/groovy/io/micronaut/inject/visitor/beans/Auditable.java b/inject-java-test/src/test/groovy/io/micronaut/inject/visitor/beans/Auditable.java new file mode 100644 index 00000000000..ec718a37647 --- /dev/null +++ b/inject-java-test/src/test/groovy/io/micronaut/inject/visitor/beans/Auditable.java @@ -0,0 +1,20 @@ +package io.micronaut.inject.visitor.beans; + +import io.micronaut.context.annotation.Executable; + +import java.time.Instant; + +public abstract class Auditable { + + private Instant updatedAt; + + @Executable(processOnStartup = true) + protected void beforeInsert() { + updatedAt = Instant.now(); + } + + public Instant getUpdatedAt() { + return updatedAt; + } +} + diff --git a/inject-java-test/src/test/groovy/io/micronaut/inject/visitor/beans/BeanIntrospectionGenericsSpec.groovy b/inject-java-test/src/test/groovy/io/micronaut/inject/visitor/beans/BeanIntrospectionGenericsSpec.groovy index 6c6ddff695b..1a6ac0b8d1d 100644 --- a/inject-java-test/src/test/groovy/io/micronaut/inject/visitor/beans/BeanIntrospectionGenericsSpec.groovy +++ b/inject-java-test/src/test/groovy/io/micronaut/inject/visitor/beans/BeanIntrospectionGenericsSpec.groovy @@ -1,10 +1,13 @@ package io.micronaut.inject.visitor.beans import io.micronaut.annotation.processing.test.AbstractTypeElementSpec +import io.micronaut.core.beans.BeanIntrospection import io.micronaut.core.type.GenericPlaceholder +import spock.lang.Issue class BeanIntrospectionGenericsSpec extends AbstractTypeElementSpec { + void "test generic placeholder for bean properties"() { given: def introspection = buildBeanIntrospection('test.Test', ''' diff --git a/inject-java-test/src/test/groovy/io/micronaut/inject/visitor/beans/BeanIntrospectionSpec.groovy b/inject-java-test/src/test/groovy/io/micronaut/inject/visitor/beans/BeanIntrospectionSpec.groovy index fcef82b024f..d6b90ee6214 100644 --- a/inject-java-test/src/test/groovy/io/micronaut/inject/visitor/beans/BeanIntrospectionSpec.groovy +++ b/inject-java-test/src/test/groovy/io/micronaut/inject/visitor/beans/BeanIntrospectionSpec.groovy @@ -5,6 +5,7 @@ import com.fasterxml.jackson.annotation.JsonClassDescription import com.fasterxml.jackson.annotation.JsonCreator import com.fasterxml.jackson.annotation.JsonProperty import com.fasterxml.jackson.databind.ObjectMapper +import groovy.transform.PackageScope import io.micronaut.annotation.processing.TypeElementVisitorProcessor import io.micronaut.annotation.processing.test.AbstractTypeElementSpec import io.micronaut.annotation.processing.test.JavaParser @@ -42,9 +43,45 @@ import javax.validation.constraints.Min import javax.validation.constraints.NotBlank import javax.validation.constraints.Size import java.lang.reflect.Field +import java.time.Instant class BeanIntrospectionSpec extends AbstractTypeElementSpec { + @Issue("https://github.com/micronaut-projects/micronaut-core/issues/8657") + void "test executable method on abstract class with introspection"() { + when: + def introspection = buildBeanIntrospection('issue8657.Test', ''' +package issue8657; + +import io.micronaut.core.annotation.Introspected; +import io.micronaut.context.annotation.Executable; +import io.micronaut.inject.visitor.beans.Auditable; + +@Introspected +class Test extends Auditable { + private String name; + public void setName(String name) { + this.name = name; + } + public String getName() { + return name; + } +} + +''') + + then: + introspection.beanMethods.size() == 1 + + when: + def bean = introspection.instantiate() + def method = introspection.beanMethods.first() + method.invoke(bean) + + then: + bean.updatedAt != null + } + void "test generics in arrays don't stack overflow"() { given: def introspection = buildBeanIntrospection('arraygenerics.Test', ''' @@ -4288,3 +4325,5 @@ public record Foo(String name, String isSurname, boolean contains, Boolean purge } } } + + diff --git a/inject/src/main/java/io/micronaut/context/AbstractExecutableMethodsDefinition.java b/inject/src/main/java/io/micronaut/context/AbstractExecutableMethodsDefinition.java index 8c5d5c3c26d..ae6321585ce 100644 --- a/inject/src/main/java/io/micronaut/context/AbstractExecutableMethodsDefinition.java +++ b/inject/src/main/java/io/micronaut/context/AbstractExecutableMethodsDefinition.java @@ -44,7 +44,6 @@ public abstract class AbstractExecutableMethodsDefinition implements Executab private final DispatchedExecutableMethod[] executableMethods; private Environment environment; private List> executableMethodsList; - private Method[] reflectiveMethods; protected AbstractExecutableMethodsDefinition(MethodReference[] methodsReferences) { this.methodsReferences = methodsReferences; @@ -161,17 +160,11 @@ protected Object dispatch(int index, T target, Object[] args) { // this logic must allow reflection @SuppressWarnings("java:S3011") protected final Method getAccessibleTargetMethodByIndex(int index) { - if (reflectiveMethods == null) { - reflectiveMethods = new Method[methodsReferences.length]; - } - Method method = reflectiveMethods[index]; - if (method == null) { - method = getTargetMethodByIndex(index); - if (ClassUtils.REFLECTION_LOGGER.isDebugEnabled()) { - ClassUtils.REFLECTION_LOGGER.debug("Reflectively accessing method {} of type {}", method, method.getDeclaringClass()); - } - method.setAccessible(true); + Method method = getTargetMethodByIndex(index); + if (ClassUtils.REFLECTION_LOGGER.isDebugEnabled()) { + ClassUtils.REFLECTION_LOGGER.debug("Reflectively accessing method {} of type {}", method, method.getDeclaringClass()); } + method.setAccessible(true); return method; } diff --git a/inject/src/main/java/io/micronaut/inject/beans/AbstractInitializableBeanIntrospection.java b/inject/src/main/java/io/micronaut/inject/beans/AbstractInitializableBeanIntrospection.java index 5559ed42fc0..f9e1526df18 100644 --- a/inject/src/main/java/io/micronaut/inject/beans/AbstractInitializableBeanIntrospection.java +++ b/inject/src/main/java/io/micronaut/inject/beans/AbstractInitializableBeanIntrospection.java @@ -119,6 +119,35 @@ protected BeanProperty getPropertyByIndex(int index) { return beanProperties.get(index); } + /** + * Find {@link Method} representation at the method by index. Used by {@link ExecutableMethod#getTargetMethod()}. + * + * @param index The index + * @return The method + */ + @UsedByGeneratedCode + @Internal + protected abstract Method getTargetMethodByIndex(int index); + + /** + * Find {@link Method} representation at the method by index. Used by {@link ExecutableMethod#getTargetMethod()}. + * + * @param index The index + * @return The method + * @since 3.8.5 + */ + @UsedByGeneratedCode + // this logic must allow reflection + @SuppressWarnings("java:S3011") + protected final Method getAccessibleTargetMethodByIndex(int index) { + Method method = getTargetMethodByIndex(index); + if (ClassUtils.REFLECTION_LOGGER.isDebugEnabled()) { + ClassUtils.REFLECTION_LOGGER.debug("Reflectively accessing method {} of type {}", method, method.getDeclaringClass()); + } + method.setAccessible(true); + return method; + } + /** * Triggers the invocation of the method at index. * @@ -577,7 +606,7 @@ public Method getTargetMethod() { if (ClassUtils.REFLECTION_LOGGER.isWarnEnabled()) { ClassUtils.REFLECTION_LOGGER.warn("Using getTargetMethod for method {} on type {} requires the use of reflection. GraalVM configuration necessary", getName(), getDeclaringType()); } - return ReflectionUtils.getRequiredMethod(getDeclaringType(), getMethodName(), getArgumentTypes()); + return getTargetMethodByIndex(ref.methodIndex); } @Override diff --git a/inject/src/main/java/io/micronaut/inject/beans/visitor/BeanIntrospectionWriter.java b/inject/src/main/java/io/micronaut/inject/beans/visitor/BeanIntrospectionWriter.java index 25af490ec81..7e86c9baa36 100644 --- a/inject/src/main/java/io/micronaut/inject/beans/visitor/BeanIntrospectionWriter.java +++ b/inject/src/main/java/io/micronaut/inject/beans/visitor/BeanIntrospectionWriter.java @@ -136,7 +136,7 @@ final class BeanIntrospectionWriter extends AbstractAnnotationMetadataWriter { this.introspectionName = computeIntrospectionName(name); this.introspectionType = getTypeReferenceForName(introspectionName); this.beanType = getTypeReferenceForName(name); - this.dispatchWriter = new DispatchWriter(introspectionType); + this.dispatchWriter = new DispatchWriter(introspectionType, Type.getType(AbstractInitializableBeanIntrospection.class)); } /** @@ -618,6 +618,7 @@ private void writeIntrospectionClass(ClassWriterOutputVisitor classWriterOutputV dispatchWriter.buildDispatchOneMethod(classWriter); dispatchWriter.buildDispatchMethod(classWriter); + dispatchWriter.buildGetTargetMethodByIndex(classWriter); buildPropertyIndexOfMethod(classWriter); buildFindIndexedProperty(classWriter); buildGetIndexedProperties(classWriter); diff --git a/inject/src/main/java/io/micronaut/inject/writer/DispatchWriter.java b/inject/src/main/java/io/micronaut/inject/writer/DispatchWriter.java index 301a64ea864..452d9b8fb1a 100644 --- a/inject/src/main/java/io/micronaut/inject/writer/DispatchWriter.java +++ b/inject/src/main/java/io/micronaut/inject/writer/DispatchWriter.java @@ -71,11 +71,19 @@ public final class DispatchWriter extends AbstractClassFileWriter implements Opc private final List dispatchTargets = new ArrayList<>(); private final Type thisType; + + private final Type dispatchSuperType; + private boolean hasInterceptedMethod; public DispatchWriter(Type thisType) { + this(thisType, ExecutableMethodsDefinitionWriter.SUPER_TYPE); + } + + public DispatchWriter(Type thisType, Type dispatchSuperType) { super(); this.thisType = thisType; + this.dispatchSuperType = dispatchSuperType; } /** @@ -118,7 +126,7 @@ public int addMethod(TypedElement declaringType, MethodElement methodElement) { * @return the target index */ public int addMethod(TypedElement declaringType, MethodElement methodElement, boolean useOneDispatch) { - return addDispatchTarget(new MethodDispatchTarget(declaringType, methodElement, useOneDispatch, !useOneDispatch)); + return addDispatchTarget(new MethodDispatchTarget(dispatchSuperType, declaringType, methodElement, useOneDispatch, !useOneDispatch)); } /** @@ -136,6 +144,7 @@ public int addInterceptedMethod(TypedElement declaringType, String interceptedProxyBridgeMethodName) { hasInterceptedMethod = true; return addDispatchTarget(new InterceptableMethodDispatchTarget( + dispatchSuperType, declaringType, methodElement, interceptedProxyClassName, @@ -466,15 +475,17 @@ public void writeDispatchOne(GeneratorAdapter writer) { @Internal @SuppressWarnings("FinalClass") public static class MethodDispatchTarget implements DispatchTarget { + final Type dispatchSuperType; final TypedElement declaringType; final MethodElement methodElement; final boolean oneDispatch; final boolean multiDispatch; - private MethodDispatchTarget(TypedElement declaringType, + private MethodDispatchTarget(Type dispatchSuperType, TypedElement declaringType, MethodElement methodElement, boolean oneDispatch, boolean multiDispatch) { + this.dispatchSuperType = dispatchSuperType; this.declaringType = declaringType; this.methodElement = methodElement; this.oneDispatch = oneDispatch; @@ -519,7 +530,7 @@ public void writeDispatchMulti(GeneratorAdapter writer, int methodIndex) { } writer.loadThis(); writer.push(methodIndex); - writer.invokeVirtual(ExecutableMethodsDefinitionWriter.SUPER_TYPE, GET_ACCESSIBLE_TARGET_METHOD); + writer.invokeVirtual(dispatchSuperType, GET_ACCESSIBLE_TARGET_METHOD); if (hasArgs) { writer.loadArg(2); } else { @@ -598,12 +609,13 @@ public static final class InterceptableMethodDispatchTarget extends MethodDispat final String interceptedProxyBridgeMethodName; final Type thisType; - private InterceptableMethodDispatchTarget(TypedElement declaringType, + private InterceptableMethodDispatchTarget(Type dispatchSuperType, + TypedElement declaringType, MethodElement methodElement, String interceptedProxyClassName, String interceptedProxyBridgeMethodName, Type thisType) { - super(declaringType, methodElement, false, true); + super(dispatchSuperType, declaringType, methodElement, false, true); this.interceptedProxyClassName = interceptedProxyClassName; this.interceptedProxyBridgeMethodName = interceptedProxyBridgeMethodName; this.thisType = thisType; From 4f2f20a1cb56e69a1f6b769e809689454a4aa74e Mon Sep 17 00:00:00 2001 From: Tim Yates Date: Fri, 10 Feb 2023 10:38:52 +0000 Subject: [PATCH 469/743] feat: allow user to explicity turn off the localhost drive-by CORS check (#8751) To disallow drive-by localhost attacks on a server under development, we added an Origin check for all requests even if Cors itself was disabled. However, under certain circumstances (such as those described in #8749), this is not the required behavior. This PR adds a new configuration setting `micronaut.server.cors.localhost-pass-through` which defaults to false (for the current behavior). Setting this to `true` will skip this localhost/origin test and the request will be allowed." --- .../tck/tests/cors/CorsSimpleRequestTest.java | 19 +++++++++++++++ .../SimpleRequestWithCorsNotEnabledTest.java | 24 +++++++++++++++++++ .../http/server/HttpServerConfiguration.java | 21 ++++++++++++++++ .../http/server/cors/CorsFilter.java | 6 +++++ 4 files changed, 70 insertions(+) diff --git a/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/cors/CorsSimpleRequestTest.java b/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/cors/CorsSimpleRequestTest.java index bed3249894c..aff8053c321 100644 --- a/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/cors/CorsSimpleRequestTest.java +++ b/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/cors/CorsSimpleRequestTest.java @@ -52,6 +52,7 @@ public class CorsSimpleRequestTest { private static final String SPECNAME = "CorsSimpleRequestTest"; private static final String PROPERTY_MICRONAUT_SERVER_CORS_ENABLED = "micronaut.server.cors.enabled"; + private static final String PROPERTY_MICRONAUT_SERVER_CORS_LOCALHOST_PASS_THROUGH = "micronaut.server.cors.localhost-pass-through"; /** * @see GHSA-583g-g682-crxf @@ -75,6 +76,24 @@ void corsSimpleRequestNotAllowedForLocalhostAndAny() throws IOException { ); } + /** + * Test that a simple request is allowed for localhost and origin:any when specifically turned off. + * @see PR-8751 + * + * @throws IOException + */ + @Test + void corsSimpleRequestAllowedForLocalhostAndAnyWhenSpecificallyTurnedOff() throws IOException { + asserts(SPECNAME, + CollectionUtils.mapOf( + PROPERTY_MICRONAUT_SERVER_CORS_ENABLED, StringUtils.TRUE, + PROPERTY_MICRONAUT_SERVER_CORS_LOCALHOST_PASS_THROUGH, StringUtils.TRUE + ), + createRequest("https://foo.com"), + CorsSimpleRequestTest::isSuccessful + ); + } + /** * @see GHSA-583g-g682-crxf * diff --git a/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/cors/SimpleRequestWithCorsNotEnabledTest.java b/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/cors/SimpleRequestWithCorsNotEnabledTest.java index 618b37bfcaa..285663ce22a 100644 --- a/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/cors/SimpleRequestWithCorsNotEnabledTest.java +++ b/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/cors/SimpleRequestWithCorsNotEnabledTest.java @@ -18,6 +18,7 @@ import io.micronaut.context.annotation.Requires; import io.micronaut.context.event.ApplicationEventListener; import io.micronaut.context.event.ApplicationEventPublisher; +import io.micronaut.core.util.StringUtils; import io.micronaut.http.HttpRequest; import io.micronaut.http.HttpStatus; import io.micronaut.http.annotation.Controller; @@ -43,7 +44,9 @@ "checkstyle:MissingJavadocType", }) public class SimpleRequestWithCorsNotEnabledTest { + private static final String SPECNAME = "SimpleRequestWithCorsNotEnabledTest"; + private static final String PROPERTY_MICRONAUT_SERVER_CORS_LOCALHOST_PASS_THROUGH = "micronaut.server.cors.localhost-pass-through"; /** * @see GHSA-583g-g682-crxf @@ -66,6 +69,27 @@ void corsSimpleRequestNotAllowedForLocalhostAndAny() throws IOException { }); } + /** + * This test verifies a CORS simple request is allowed when invoked against a Micronaut application running in localhost without cors enabled but with localhost-pass-through switched on. + * @see PR-8751 + * + * @throws IOException + */ + @Test + void corsSimpleRequestAllowedForLocalhostAndAnyWhenConfiguredToAllowIt() throws IOException { + asserts(SPECNAME, + Collections.singletonMap(PROPERTY_MICRONAUT_SERVER_CORS_LOCALHOST_PASS_THROUGH, StringUtils.TRUE), + createRequest("https://sdelamo.github.io"), + (server, request) -> { + RefreshCounter refreshCounter = server.getApplicationContext().getBean(RefreshCounter.class); + assertEquals(0, refreshCounter.getRefreshCount()); + AssertionUtils.assertDoesNotThrow(server, request, HttpResponseAssertion.builder() + .status(HttpStatus.OK) + .build()); + assertEquals(1, refreshCounter.getRefreshCount()); + }); + } + /** * It should not deny a cors request coming from a localhost origin if the micronaut application resolved host is localhost. * @throws IOException scenario step fails diff --git a/http-server/src/main/java/io/micronaut/http/server/HttpServerConfiguration.java b/http-server/src/main/java/io/micronaut/http/server/HttpServerConfiguration.java index 18f515beefa..53a90d61ac8 100644 --- a/http-server/src/main/java/io/micronaut/http/server/HttpServerConfiguration.java +++ b/http-server/src/main/java/io/micronaut/http/server/HttpServerConfiguration.java @@ -664,9 +664,11 @@ public static class CorsConfiguration implements Toggleable { public static final boolean DEFAULT_ENABLED = false; public static final boolean DEFAULT_SINGLE_HEADER = false; + public static final boolean DEFAULT_LOCALHOST_PASS_THROUGH = false; private boolean enabled = DEFAULT_ENABLED; private boolean singleHeader = DEFAULT_SINGLE_HEADER; + private boolean localhostPassThrough = DEFAULT_LOCALHOST_PASS_THROUGH; private Map configurations = Collections.emptyMap(); @@ -680,6 +682,14 @@ public boolean isEnabled() { return enabled; } + /** + * @return Whether localhost pass-through is enabled. Defaults to {@value #DEFAULT_LOCALHOST_PASS_THROUGH}. + * @since 3.8.5 + */ + public boolean isLocalhostPassThrough() { + return localhostPassThrough; + } + /** * @return The cors configurations */ @@ -708,6 +718,17 @@ public void setEnabled(boolean enabled) { this.enabled = enabled; } + /** + * Sets whether localhost pass-through is enabled. Default value {@value #DEFAULT_LOCALHOST_PASS_THROUGH}. + * Setting this to true will allow requests to be made to localhost from any origin. + * + * @param localhostPassThrough True if localhost pass-through is enabled + * @since 3.8.5 + */ + public void setLocalhostPassThrough(boolean localhostPassThrough) { + this.localhostPassThrough = localhostPassThrough; + } + /** * Sets the CORS configurations. * @param configurations The CORS configurations diff --git a/http-server/src/main/java/io/micronaut/http/server/cors/CorsFilter.java b/http-server/src/main/java/io/micronaut/http/server/cors/CorsFilter.java index f89dc75bc78..58eef88f473 100644 --- a/http-server/src/main/java/io/micronaut/http/server/cors/CorsFilter.java +++ b/http-server/src/main/java/io/micronaut/http/server/cors/CorsFilter.java @@ -126,6 +126,9 @@ public Publisher> doFilter(HttpRequest request, Server */ protected boolean shouldDenyToPreventDriveByLocalhostAttack(@NonNull CorsOriginConfiguration corsOriginConfiguration, @NonNull HttpRequest request) { + if (corsConfiguration.isLocalhostPassThrough()) { + return false; + } if (httpHostResolver == null) { return false; } @@ -148,6 +151,9 @@ protected boolean shouldDenyToPreventDriveByLocalhostAttack(@NonNull CorsOriginC */ protected boolean shouldDenyToPreventDriveByLocalhostAttack(@NonNull String origin, @NonNull HttpRequest request) { + if (corsConfiguration.isLocalhostPassThrough()) { + return false; + } if (httpHostResolver == null) { return false; } From 7e3e06204eb90b0bc6e8941c2ab0a0d828af22ae Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Fri, 10 Feb 2023 11:39:20 +0100 Subject: [PATCH 470/743] doc: change copy of EachProperty (#8747) see: https://github.com/micronaut-projects/micronaut-guides/pull/1237#discussion_r1101170983 --- src/main/docs/guide/config/eachProperty.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/docs/guide/config/eachProperty.adoc b/src/main/docs/guide/config/eachProperty.adoc index 5d178d7d6ed..23d71b373b1 100644 --- a/src/main/docs/guide/config/eachProperty.adoc +++ b/src/main/docs/guide/config/eachProperty.adoc @@ -1,6 +1,6 @@ The link:{api}/io/micronaut/context/annotation/ConfigurationProperties.html[@ConfigurationProperties] annotation is great for a single configuration class, but sometimes you want multiple instances, each with its own distinct configuration. That is where link:{api}/io/micronaut/context/annotation/EachProperty.html[EachProperty] comes in. -The ann:context.annotation.EachProperty[] annotation creates a `ConfigurationProperties` bean for each sub-property within the given property. As an example consider the following class: +The ann:context.annotation.EachProperty[] annotation creates a `ConfigurationProperties` bean for each sub-property within the given name. As an example consider the following class: snippet::io.micronaut.docs.config.env.DataSourceConfiguration[tags="eachProperty", indent=0, title="Using @EachProperty"] From 02992a905cf9a2279b7fe8e49927ff080cb937d5 Mon Sep 17 00:00:00 2001 From: Graeme Rocher Date: Fri, 10 Feb 2023 15:31:38 +0100 Subject: [PATCH 471/743] Implement more lenient restrictions on mixed types for getters/setters (#8757) --- .../ast/utils/AstBeanPropertiesUtils.java | 11 +++-- .../beans/BeanIntrospectionSpec.groovy | 47 +++++++++++++++++++ 2 files changed, 55 insertions(+), 3 deletions(-) diff --git a/core-processor/src/main/java/io/micronaut/inject/ast/utils/AstBeanPropertiesUtils.java b/core-processor/src/main/java/io/micronaut/inject/ast/utils/AstBeanPropertiesUtils.java index 7d7ac9de371..46b66119798 100644 --- a/core-processor/src/main/java/io/micronaut/inject/ast/utils/AstBeanPropertiesUtils.java +++ b/core-processor/src/main/java/io/micronaut/inject/ast/utils/AstBeanPropertiesUtils.java @@ -135,7 +135,7 @@ public static List resolveBeanProperties(PropertyElementQuery c // ensure types match ClassElement getterType = value.getter.getGenericReturnType(); ClassElement setterType = value.setter.getParameters()[0].getGenericType(); - if (!getterType.equals(setterType)) { + if (isIncompatibleSetterType(setterType, getterType)) { // getter and setter don't match, remove setter value.setter = null; value.type = getterType; @@ -252,8 +252,9 @@ private static void processSetter(ClassElement classElement, Map Date: Mon, 13 Feb 2023 10:45:10 +0100 Subject: [PATCH 472/743] Bump micronaut-kafka to 4.5.1 (#8750) --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 59ce940f2be..2c87308555e 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -87,7 +87,7 @@ managed-micronaut-ignite = "1.0.0.RC1" managed-micronaut-jaxrs = "3.4.0" managed-micronaut-jms = "2.1.0" managed-micronaut-jmx = "3.2.0" -managed-micronaut-kafka = "4.5.0" +managed-micronaut-kafka = "4.5.1" managed-micronaut-kotlin = "3.2.2" managed-micronaut-kubernetes = "3.4.0" managed-micronaut-micrometer = "4.7.2" From abafa30a8c9dcedc9ea4d56a7261902efe60b431 Mon Sep 17 00:00:00 2001 From: Graeme Rocher Date: Mon, 13 Feb 2023 10:45:38 +0100 Subject: [PATCH 473/743] fix GraalVM deprecation warnings (#8753) --- .../src/main/java/io/micronaut/buffer/netty/NettyFeature.java | 2 -- .../micronaut-buffer-netty/native-image.properties | 1 + .../io/micronaut/core/graal/ServiceLoaderInitialization.java | 2 -- .../io.micronaut/micronaut-core/native-image.properties | 2 +- .../java/io/micronaut/http/netty/graal/HttpNettyFeature.java | 2 -- .../io.micronaut/micronaut-http-netty/native-image.properties | 1 + .../io.micronaut/micronaut-inject/native-image.properties | 3 +-- .../main/java/io/micronaut/jackson/JacksonDatabindFeature.java | 2 -- .../micronaut-jackson-databind/native-image.properties | 1 + 9 files changed, 5 insertions(+), 11 deletions(-) create mode 100644 buffer-netty/src/main/resources/META-INF/native-image/io.micronaut/micronaut-buffer-netty/native-image.properties create mode 100644 http-netty/src/main/resources/META-INF/native-image/io.micronaut/micronaut-http-netty/native-image.properties create mode 100644 jackson-databind/src/main/resources/META-INF/native-image/io.micronaut/micronaut-jackson-databind/native-image.properties diff --git a/buffer-netty/src/main/java/io/micronaut/buffer/netty/NettyFeature.java b/buffer-netty/src/main/java/io/micronaut/buffer/netty/NettyFeature.java index dd1b22442de..dc320f7c73f 100644 --- a/buffer-netty/src/main/java/io/micronaut/buffer/netty/NettyFeature.java +++ b/buffer-netty/src/main/java/io/micronaut/buffer/netty/NettyFeature.java @@ -15,7 +15,6 @@ */ package io.micronaut.buffer.netty; -import com.oracle.svm.core.annotate.AutomaticFeature; import com.oracle.svm.core.jdk.SystemPropertiesSupport; import io.micronaut.core.annotation.Internal; import io.micronaut.core.graal.AutomaticFeatureUtils; @@ -36,7 +35,6 @@ * @since 3.3.0 */ @Internal -@AutomaticFeature final class NettyFeature implements Feature { @Override public void beforeAnalysis(BeforeAnalysisAccess access) { diff --git a/buffer-netty/src/main/resources/META-INF/native-image/io.micronaut/micronaut-buffer-netty/native-image.properties b/buffer-netty/src/main/resources/META-INF/native-image/io.micronaut/micronaut-buffer-netty/native-image.properties new file mode 100644 index 00000000000..7552ba4d263 --- /dev/null +++ b/buffer-netty/src/main/resources/META-INF/native-image/io.micronaut/micronaut-buffer-netty/native-image.properties @@ -0,0 +1 @@ +Args = --features=io.micronaut.buffer.netty.NettyFeature diff --git a/core/src/main/java/io/micronaut/core/graal/ServiceLoaderInitialization.java b/core/src/main/java/io/micronaut/core/graal/ServiceLoaderInitialization.java index 17c3cbfe216..20ba5d41d01 100644 --- a/core/src/main/java/io/micronaut/core/graal/ServiceLoaderInitialization.java +++ b/core/src/main/java/io/micronaut/core/graal/ServiceLoaderInitialization.java @@ -32,7 +32,6 @@ import java.util.Map; import java.util.Set; -import com.oracle.svm.core.annotate.AutomaticFeature; import com.oracle.svm.core.annotate.Substitute; import com.oracle.svm.core.annotate.TargetClass; import io.micronaut.core.annotation.Internal; @@ -51,7 +50,6 @@ * @since 3.5.0 */ @SuppressWarnings("unused") -@AutomaticFeature final class ServiceLoaderFeature implements Feature { @Override diff --git a/core/src/main/resources/META-INF/native-image/io.micronaut/micronaut-core/native-image.properties b/core/src/main/resources/META-INF/native-image/io.micronaut/micronaut-core/native-image.properties index d90f2dd7a7f..8e2a59489d3 100644 --- a/core/src/main/resources/META-INF/native-image/io.micronaut/micronaut-core/native-image.properties +++ b/core/src/main/resources/META-INF/native-image/io.micronaut/micronaut-core/native-image.properties @@ -14,4 +14,4 @@ # limitations under the License. # -Args = --initialize-at-run-time=io.micronaut.core.io.socket.SocketUtils +Args = --initialize-at-run-time=io.micronaut.core.io.socket.SocketUtils --features=io.micronaut.core.graal.ServiceLoaderFeature diff --git a/http-netty/src/main/java/io/micronaut/http/netty/graal/HttpNettyFeature.java b/http-netty/src/main/java/io/micronaut/http/netty/graal/HttpNettyFeature.java index 4b9cb7b37ee..5a1c40d6fa5 100644 --- a/http-netty/src/main/java/io/micronaut/http/netty/graal/HttpNettyFeature.java +++ b/http-netty/src/main/java/io/micronaut/http/netty/graal/HttpNettyFeature.java @@ -15,7 +15,6 @@ */ package io.micronaut.http.netty.graal; -import com.oracle.svm.core.annotate.AutomaticFeature; import io.micronaut.core.annotation.Internal; import io.micronaut.http.bind.binders.ContinuationArgumentBinder; import io.micronaut.http.netty.channel.NettyThreadFactory; @@ -32,7 +31,6 @@ * @since 2.0.0 */ @Internal -@AutomaticFeature public class HttpNettyFeature implements Feature { @Override diff --git a/http-netty/src/main/resources/META-INF/native-image/io.micronaut/micronaut-http-netty/native-image.properties b/http-netty/src/main/resources/META-INF/native-image/io.micronaut/micronaut-http-netty/native-image.properties new file mode 100644 index 00000000000..52279101dc6 --- /dev/null +++ b/http-netty/src/main/resources/META-INF/native-image/io.micronaut/micronaut-http-netty/native-image.properties @@ -0,0 +1 @@ +Args = --features=io.micronaut.http.netty.graal.HttpNettyFeature diff --git a/inject/src/main/resources/META-INF/native-image/io.micronaut/micronaut-inject/native-image.properties b/inject/src/main/resources/META-INF/native-image/io.micronaut/micronaut-inject/native-image.properties index 572a2029998..e7ace086350 100644 --- a/inject/src/main/resources/META-INF/native-image/io.micronaut/micronaut-inject/native-image.properties +++ b/inject/src/main/resources/META-INF/native-image/io.micronaut/micronaut-inject/native-image.properties @@ -14,7 +14,6 @@ # limitations under the License. # -Args = --allow-incomplete-classpath \ - -H:EnableURLProtocols=http,https \ +Args = -H:EnableURLProtocols=http,https \ --initialize-at-run-time=io.micronaut.inject.provider.JakartaProviderBeanDefinition \ --initialize-at-run-time=io.micronaut.context.env.CachedEnvironment diff --git a/jackson-databind/src/main/java/io/micronaut/jackson/JacksonDatabindFeature.java b/jackson-databind/src/main/java/io/micronaut/jackson/JacksonDatabindFeature.java index 1df9d6b2d42..24fca4d8269 100644 --- a/jackson-databind/src/main/java/io/micronaut/jackson/JacksonDatabindFeature.java +++ b/jackson-databind/src/main/java/io/micronaut/jackson/JacksonDatabindFeature.java @@ -17,7 +17,6 @@ import com.fasterxml.jackson.databind.PropertyNamingStrategies; import com.fasterxml.jackson.databind.PropertyNamingStrategy; -import com.oracle.svm.core.annotate.AutomaticFeature; import io.micronaut.core.annotation.Internal; import org.graalvm.nativeimage.hosted.Feature; import org.graalvm.nativeimage.hosted.RuntimeReflection; @@ -31,7 +30,6 @@ * @since 3.4.1 */ @Internal -@AutomaticFeature final class JacksonDatabindFeature implements Feature { @SuppressWarnings("deprecation") @Override diff --git a/jackson-databind/src/main/resources/META-INF/native-image/io.micronaut/micronaut-jackson-databind/native-image.properties b/jackson-databind/src/main/resources/META-INF/native-image/io.micronaut/micronaut-jackson-databind/native-image.properties new file mode 100644 index 00000000000..bb128a8a00c --- /dev/null +++ b/jackson-databind/src/main/resources/META-INF/native-image/io.micronaut/micronaut-jackson-databind/native-image.properties @@ -0,0 +1 @@ +Args = --features=io.micronaut.jackson.JacksonDatabindFeature From 33836af21efe99b686d92731f95f288da836e8f7 Mon Sep 17 00:00:00 2001 From: Tim Yates Date: Mon, 13 Feb 2023 09:46:16 +0000 Subject: [PATCH 474/743] Stop using EnvUtil logback programmatic config (#8763) In #8674 we re-enabled programmatic logback configuration. However the Logback class we used 'EnvUtil' moved in logback 1.3.0, so micronaut fails to start if a newer version of logback is pulled in as a dependency. To fix this, this PR removes the requirement for EnvUtil, and just performs the ServiceLoader call from inside Micronaut itself. Fixes #8758 --- .../io/micronaut/logging/impl/LogbackUtils.java | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/runtime/src/main/java/io/micronaut/logging/impl/LogbackUtils.java b/runtime/src/main/java/io/micronaut/logging/impl/LogbackUtils.java index 79933d8facb..29bb45f652f 100644 --- a/runtime/src/main/java/io/micronaut/logging/impl/LogbackUtils.java +++ b/runtime/src/main/java/io/micronaut/logging/impl/LogbackUtils.java @@ -18,13 +18,15 @@ import ch.qos.logback.classic.LoggerContext; import ch.qos.logback.classic.spi.Configurator; import ch.qos.logback.classic.util.ContextInitializer; -import ch.qos.logback.classic.util.EnvUtil; import ch.qos.logback.core.joran.spi.JoranException; import ch.qos.logback.core.status.InfoStatus; import io.micronaut.core.annotation.NonNull; +import io.micronaut.core.annotation.Nullable; import io.micronaut.logging.LoggingSystemException; import java.net.URL; +import java.util.Iterator; +import java.util.ServiceLoader; import java.util.function.Supplier; /** @@ -66,7 +68,7 @@ private static void configure( @NonNull String logbackXmlLocation, Supplier resourceSupplier ) { - Configurator configurator = EnvUtil.loadFromServiceLoader(Configurator.class); + Configurator configurator = loadFromServiceLoader(); if (configurator != null) { context.getStatusManager().add(new InfoStatus("Using " + configurator.getClass().getName(), context)); programmaticConfiguration(context, configurator); @@ -96,4 +98,14 @@ private static void programmaticConfiguration(@NonNull LoggerContext context, throw new LoggingSystemException(String.format("Failed to initialize Configurator: %s using ServiceLoader", configurator.getClass().getCanonicalName()), e); } } + + /** + * Find a Logback Configurator service. + * We can't use {@link ch.qos.logback.classic.util.EnvUtil#loadFromServiceLoader} because the class moved in v1.3.0 + */ + @Nullable + private static Configurator loadFromServiceLoader() { + Iterator it = ServiceLoader.load(Configurator.class).iterator(); + return it.hasNext() ? it.next() : null; + } } From 27c7e0fccf41bb3584ab2ac762b8f36a99987319 Mon Sep 17 00:00:00 2001 From: micronaut-build <65172877+micronaut-build@users.noreply.github.com> Date: Mon, 13 Feb 2023 11:40:12 +0100 Subject: [PATCH 475/743] Bump micronaut-oracle-cloud to 2.3.4 (#8769) --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 2c87308555e..2e62cbcb1d9 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -101,7 +101,7 @@ managed-micronaut-nats = "3.1.0" managed-micronaut-netflix = "2.1.0" managed-micronaut-object-storage = "1.1.0" managed-micronaut-openapi = "4.8.3" -managed-micronaut-oraclecloud = "2.3.2" +managed-micronaut-oraclecloud = "2.3.4" managed-micronaut-picocli = "4.3.0" managed-micronaut-problem = "2.6.0" managed-micronaut-rabbitmq = "3.4.1" From 753f6f15a5c776e44e15b5f2881bbc4dca1ee6b1 Mon Sep 17 00:00:00 2001 From: Tim Yates Date: Tue, 14 Feb 2023 08:59:45 +0000 Subject: [PATCH 476/743] Replace deprecated output method for Github actions (#8721) --- .github/workflows/release-notes.yml | 2 +- .github/workflows/release.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release-notes.yml b/.github/workflows/release-notes.yml index 35c3bf7a7a9..ecb1f667c65 100644 --- a/.github/workflows/release-notes.yml +++ b/.github/workflows/release-notes.yml @@ -21,7 +21,7 @@ jobs: id: check_release_drafter run: | has_release_drafter=$([ -f .github/release-drafter.yml ] && echo "true" || echo "false") - echo ::set-output name=has_release_drafter::${has_release_drafter} + echo "has_release_drafter=${has_release_drafter}" >> $GITHUB_OUTPUT # If it has release drafter: - uses: release-drafter/release-drafter@v5 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 71d383ad7da..b0bd720498a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -25,7 +25,7 @@ jobs: distribution: 'temurin' - name: Set the current release version id: release_version - run: echo ::set-output name=release_version::${GITHUB_REF:11} + run: echo "release_version=${GITHUB_REF:11}" >> $GITHUB_OUTPUT - name: Run pre-release uses: micronaut-projects/github-actions/pre-release@master env: From d00dca9f21a83fe0df7f03a4f96fa187a84a08f2 Mon Sep 17 00:00:00 2001 From: Dean Wette Date: Tue, 14 Feb 2023 03:11:03 -0600 Subject: [PATCH 477/743] doc: configuration asciidoc macro migration from #8613, plus some extra cleanup (#8744) closes #8731 --- src/main/docs/guide/aop/caching.adoc | 5 ++- src/main/docs/guide/aop/scheduling.adoc | 5 ++- src/main/docs/guide/appendix/breaks.adoc | 2 +- .../netflixRibbon.adoc | 6 ++-- ...ributedConfigurationAwsParameterStore.adoc | 5 ++- .../distributedConfigurationConsul.adoc | 13 +++---- .../distributedConfigurationSpringCloud.adoc | 11 +++--- .../distributedConfigurationVault.adoc | 4 +-- .../serviceDiscoveryManual.adoc | 17 +++++---- .../serviceDiscoveryRoute53.adoc | 29 ++++++++------- src/main/docs/guide/config/bootstrap.adoc | 2 +- .../guide/config/configurationProperties.adoc | 24 ++----------- .../docs/guide/config/propertySource.adoc | 17 ++++----- .../docs/guide/config/valueAnnotation.adoc | 3 +- .../dataAccess/mongoSupport.adoc | 6 ++-- .../dataAccess/neo4jSupport.adoc | 4 +-- .../dataAccess/redisSupport.adoc | 4 +-- .../otherConfigurations/rabbitmq.adoc | 2 +- .../clientAnnotationStreaming.adoc | 4 +-- .../clientAnnotation/clientHeaders.adoc | 6 ++-- .../clientAnnotation/clientJackson.adoc | 4 +-- .../clientAnnotation/netflixHystrix.adoc | 4 +-- .../docs/guide/httpClient/clientHttp2.adoc | 2 +- .../lowLevelHttpClient/clientBasics.adoc | 5 ++- .../clientConfiguration.adoc | 27 +++++++------- .../docs/guide/httpServer/apiVersioning.adoc | 36 +++++++++---------- .../guide/httpServer/consumesAnnotation.adoc | 2 +- src/main/docs/guide/httpServer/formData.adoc | 2 +- .../docs/guide/httpServer/http2Server.adoc | 4 +-- .../docs/guide/httpServer/jsonBinding.adoc | 8 ++--- .../docs/guide/httpServer/reactiveServer.adoc | 2 +- .../reactiveServer/bodyAnnotation.adoc | 2 +- .../guide/httpServer/runningSpecificPort.adoc | 4 +-- .../guide/httpServer/serverConfiguration.adoc | 22 ++++++------ .../serverConfiguration/accessLogger.adoc | 27 +++++++++----- .../cors/corsAllowCredentials.adoc | 2 +- .../cors/corsAllowedHeaders.adoc | 2 +- .../cors/corsAllowedMethods.adoc | 2 +- .../cors/corsAllowedOrigins.adoc | 2 +- .../cors/corsConfiguration.adoc | 6 ++-- .../cors/corsExposedHeaders.adoc | 2 +- .../serverConfiguration/cors/corsMaxAge.adoc | 2 +- .../cors/corsMultipleHeaderValues.adoc | 2 +- .../serverConfiguration/dualProtocol.adoc | 21 +++++------ .../httpServer/serverConfiguration/https.adoc | 23 ++++++------ .../serverConfiguration/listener.adoc | 17 +++++---- .../serverConfiguration/threadPools.adoc | 2 +- .../threadPools/blockingOperations.adoc | 4 +-- src/main/docs/guide/httpServer/sessions.adoc | 8 ++--- .../httpServer/websocket/websocketServer.adoc | 8 ++--- src/main/docs/guide/ioc/contextEvents.adoc | 4 +-- .../guide/logging/loggingConfiguration.adoc | 14 ++++---- .../endpointConfiguration.adoc | 2 +- .../guide/management/providedEndpoints.adoc | 2 +- .../providedEndpoints/beansEndpoint.adoc | 2 +- .../environmentEndpoint.adoc | 8 +++-- .../providedEndpoints/healthEndpoint.adoc | 21 ++++++----- .../providedEndpoints/infoEndpoint.adoc | 2 +- .../providedEndpoints/loggersEndpoint.adoc | 2 +- .../providedEndpoints/refreshEndpoint.adoc | 2 +- .../providedEndpoints/routesEndpoint.adoc | 2 +- .../providedEndpoints/stopEndpoint.adoc | 2 +- .../providedEndpoints/threadDumpEndpoint.adoc | 2 +- 63 files changed, 238 insertions(+), 250 deletions(-) diff --git a/src/main/docs/guide/aop/caching.adoc b/src/main/docs/guide/aop/caching.adoc index 2c43f932cd5..bf27ba3a9ef 100644 --- a/src/main/docs/guide/aop/caching.adoc +++ b/src/main/docs/guide/aop/caching.adoc @@ -20,10 +20,9 @@ In addition, if the underlying Cache implementation supports non-blocking cache == Configuring Caches -By default, https://github.com/ben-manes/caffeine[Caffeine] is used to create caches from application configuration. For example with `application.yml`: +By default, https://github.com/ben-manes/caffeine[Caffeine] is used to create caches from application configuration. For example: -.Cache Configuration Example -[source,yaml] +[configuration,title="Cache Configuration Example"] ---- micronaut: caches: diff --git a/src/main/docs/guide/aop/scheduling.adoc b/src/main/docs/guide/aop/scheduling.adoc index b8a124f51fe..84fdeb0ed64 100644 --- a/src/main/docs/guide/aop/scheduling.adoc +++ b/src/main/docs/guide/aop/scheduling.adoc @@ -55,11 +55,10 @@ The above example allows the task execution frequency to be configured with the Tasks executed by `@Scheduled` are run by default on a jdk:java.util.concurrent.ScheduledExecutorService[] configured to have twice the number of threads as available processors. -You can configure this thread pool using `application.yml`, for example: +You can configure this thread pool in your configuration file (e.g `application.yml`): //TODO: Move YAML snippet to ExecutorServiceConfigSpec -.Configuring Scheduled Task Thread Pool -[source,yaml] +[configuration,title="Configuring Scheduled Task Thread Pool"] ---- micronaut: executors: diff --git a/src/main/docs/guide/appendix/breaks.adoc b/src/main/docs/guide/appendix/breaks.adoc index c3c5792f2cb..7781d29da25 100644 --- a/src/main/docs/guide/appendix/breaks.adoc +++ b/src/main/docs/guide/appendix/breaks.adoc @@ -4,7 +4,7 @@ This section documents breaking changes between Micronaut versions - The <> is now disabled by default. To enable it, you must update your endpoint config: -[source,yaml] +[configuration] ---- endpoints: env: diff --git a/src/main/docs/guide/cloud/clientSideLoadBalancing/netflixRibbon.adoc b/src/main/docs/guide/cloud/clientSideLoadBalancing/netflixRibbon.adoc index 513a79eead1..5a5ca986de1 100644 --- a/src/main/docs/guide/cloud/clientSideLoadBalancing/netflixRibbon.adoc +++ b/src/main/docs/guide/cloud/clientSideLoadBalancing/netflixRibbon.adoc @@ -17,10 +17,10 @@ dependency:io.micronaut.netflix:micronaut-netflix-ribbon[] The api:http.client.LoadBalancer[] implementations will now be link:{micronautribbonapi}/io/micronaut/configuration/ribbon/RibbonLoadBalancer.html[RibbonLoadBalancer] instances. -Ribbon's https://netflix.github.io/ribbon/ribbon-core-javadoc/com/netflix/client/config/CommonClientConfigKey.html[Configuration options] can be set using the `ribbon` namespace in configuration. For example in `application.yml`: +Ribbon's https://netflix.github.io/ribbon/ribbon-core-javadoc/com/netflix/client/config/CommonClientConfigKey.html[Configuration options] can be set using the `ribbon` namespace in configuration. For example in your configuration file (e.g `application.yml`): .Configuring Ribbon -[source,yaml] +[configuration] ---- ribbon: VipAddress: test @@ -30,7 +30,7 @@ ribbon: Each discovered client can also be configured under `ribbon.clients`. For example given a `@Client(id = "hello-world")` you can configure Ribbon settings with: .Per Client Ribbon Settings -[source,yaml] +[configuration] ---- ribbon: clients: diff --git a/src/main/docs/guide/cloud/cloudConfiguration/distributedConfigurationAwsParameterStore.adoc b/src/main/docs/guide/cloud/cloudConfiguration/distributedConfigurationAwsParameterStore.adoc index 0770fbda845..f666eed73b9 100644 --- a/src/main/docs/guide/cloud/cloudConfiguration/distributedConfigurationAwsParameterStore.adoc +++ b/src/main/docs/guide/cloud/cloudConfiguration/distributedConfigurationAwsParameterStore.adoc @@ -2,10 +2,9 @@ Micronaut supports configuration sharing via AWS System Manager Parameter Store. dependency:io.micronaut.aws:micronaut-aws-parameter-store[] -To enable distributed configuration, create a `src/main/resources/bootstrap.yml` configuration file and add Parameter Store configuration: +To enable distributed configuration, make sure https://docs.micronaut.io/latest/guide/#bootstrap[bootstrap] is enabled and create a `src/main/resources/bootstrap.yml` file with the following configuration: -.bootstrap.yml -[source,yaml] +[configuration] ---- micronaut: application: diff --git a/src/main/docs/guide/cloud/cloudConfiguration/distributedConfigurationConsul.adoc b/src/main/docs/guide/cloud/cloudConfiguration/distributedConfigurationConsul.adoc index 827dcb508f8..1f1bfffef9d 100644 --- a/src/main/docs/guide/cloud/cloudConfiguration/distributedConfigurationConsul.adoc +++ b/src/main/docs/guide/cloud/cloudConfiguration/distributedConfigurationConsul.adoc @@ -23,10 +23,9 @@ $ mn create-app my-app --features config-consul ---- ==== -To enable distributed configuration, similar to Spring Boot and Grails, create a `src/main/resources/bootstrap.yml` configuration file and enable the configuration client: +To enable distributed configuration make sure <> is enabled and create a `src/main/resources/bootstrap.[yml/toml/properties]` file with the following configuration: -.bootstrap.yml -[source,yaml] +[configuration] ---- micronaut: application: @@ -66,7 +65,7 @@ Within the `/config` directory Micronaut searches values within the following di |=== -The value of `APPLICATION_NAME` is whatever your have configured `micronaut.application.name` to be in `bootstrap.yml`. +The value of `APPLICATION_NAME` is whatever your have configured `micronaut.application.name` to be in your `bootstrap` configuration file. To see this in action, use the following cURL command to store a property called `foo.bar` with a value of `myvalue` in the directory `/config/application`. @@ -88,8 +87,7 @@ You can set the `consul.client.config.format` option to configure the format wit For example, to configure JSON: -.application.yml -[source,yaml] +[configuration] ---- consul: client: @@ -116,8 +114,7 @@ You can setup a Git repository that contains files like `application.yml`, `hell In this case, each key in Consul represents a file with an extension, for example `/config/application.yml`, and you must configure the `FILE` format: -.application.yml -[source,yaml] +[configuration] ---- consul: client: diff --git a/src/main/docs/guide/cloud/cloudConfiguration/distributedConfigurationSpringCloud.adoc b/src/main/docs/guide/cloud/cloudConfiguration/distributedConfigurationSpringCloud.adoc index 7934c6ff2e2..5cccfd2de9d 100644 --- a/src/main/docs/guide/cloud/cloudConfiguration/distributedConfigurationSpringCloud.adoc +++ b/src/main/docs/guide/cloud/cloudConfiguration/distributedConfigurationSpringCloud.adoc @@ -1,9 +1,9 @@ Since 1.1, Micronaut features a native https://spring.io/projects/spring-cloud-config[Spring Cloud Configuration] for those who have not switched to a dedicated more complete solution like Consul. -To enable support for Spring Cloud Configuration, create a `src/main/resources/bootstrap.yml` configuration file and add the following configuration: +To enable distributed configuration make sure <> is enabled and create a `src/main/resources/bootstrap.[yml/toml/properties]` file with the following configuration: .Integrating with Spring Cloud Configuration -[source,yaml] +[configuration] ---- micronaut: application: @@ -15,10 +15,13 @@ spring: config: enabled: true uri: http://localhost:8888/ - retry-attempts: 4 # optional, number of times to retry - retry-delay: 2s # optional, delay between retries + retry-attempts: 4 + retry-delay: 2s ---- +- `retry-attempts` is optional, and specifies the number of times to retry +- `retry-delay` is optional, and specifies the delay between retries + Micronaut uses the configured `micronaut.application.name` to look up property sources for the application from Spring Cloud config server configured via `spring.cloud.config.uri`. See the https://spring.io/projects/spring-cloud-config#learn[Documentation for Spring Cloud Config Server] for more information on how to set up the server. diff --git a/src/main/docs/guide/cloud/cloudConfiguration/distributedConfigurationVault.adoc b/src/main/docs/guide/cloud/cloudConfiguration/distributedConfigurationVault.adoc index de53b221edd..b4d8f215164 100644 --- a/src/main/docs/guide/cloud/cloudConfiguration/distributedConfigurationVault.adoc +++ b/src/main/docs/guide/cloud/cloudConfiguration/distributedConfigurationVault.adoc @@ -1,9 +1,9 @@ Micronaut integrates with https://www.vaultproject.io/[HashiCorp Vault] as a distributed configuration source. -To enable support for Vault Configuration, create a `src/main/resources/bootstrap.yml` configuration file and add the following configuration: +To enable distributed configuration make sure <> is enabled and create a `src/main/resources/bootstrap.[yml/toml/properties]` file with the following configuration: .Integrating with HashiCorp Vault -[source,yaml] +[configuration] ---- micronaut: application: diff --git a/src/main/docs/guide/cloud/serviceDiscovery/serviceDiscoveryManual.adoc b/src/main/docs/guide/cloud/serviceDiscovery/serviceDiscoveryManual.adoc index cc0644de104..d9f16e610d8 100644 --- a/src/main/docs/guide/cloud/serviceDiscovery/serviceDiscoveryManual.adoc +++ b/src/main/docs/guide/cloud/serviceDiscovery/serviceDiscoveryManual.adoc @@ -3,7 +3,7 @@ If you do not wish to involve a service discovery server like Consul or you inte To do this, use the `micronaut.http.services` setting. For example: .Manually configuring services -[source,yaml] +[configuration] ---- micronaut: http: @@ -23,20 +23,19 @@ TIP: You can override this configuration in production by specifying an environm Note that by default no health checking will happen to assert that the referenced services are operational. You can alter that by enabling health checking and optionally specifying a health check path (the default is `/health`): .Enabling Health Checking -[source,yaml] +[configuration] ---- micronaut: http: services: foo: - ... - health-check: true # <1> - health-check-interval: 15s # <2> - health-check-uri: /health # <3> + health-check: true + health-check-interval: 15s + health-check-uri: /health ---- -<1> Whether to health check the service -<2> The interval between checks -<3> The URI of the health check request +- `health-check` indicates whether to health check the service +- `health-check-interval` is the interval between checks +- `health-check-uri` specifies the endpoint URI of the health check request Micronaut starts a background thread to check the health status of the service and if any of the configured services respond with an error code, they are removed from the list of available services. diff --git a/src/main/docs/guide/cloud/serviceDiscovery/serviceDiscoveryRoute53.adoc b/src/main/docs/guide/cloud/serviceDiscovery/serviceDiscoveryRoute53.adoc index 6a013e8c93c..44c72040c42 100644 --- a/src/main/docs/guide/cloud/serviceDiscovery/serviceDiscoveryRoute53.adoc +++ b/src/main/docs/guide/cloud/serviceDiscovery/serviceDiscoveryRoute53.adoc @@ -13,15 +13,15 @@ Here are the steps: 3. Add health checks or custom health checks (optional) 4. Add Service ID to your application configuration file like so: -.Sample application.yml -[source,yaml] +.Sample application configuration +[configuration] ---- aws: route53: - registration - enabled: true - aws-service-id: srv-978fs98fsdf - namespace: micronaut.io + registration: + enabled: true + aws-service-id: srv-978fs98fsdf + namespace: micronaut.io micronaut: application: name: something @@ -33,8 +33,7 @@ dependency:io.micronaut.aws:micronaut-aws-route53[] 6. On the client side, you need the same dependencies and fewer configuration options: -.Sample application.yml -[source,yaml] +[configuration] ---- aws: route53: @@ -120,28 +119,28 @@ You will get a service ID and an ARN back from this command if successful. Write Add the configuration to make your applications register with Route 53 Auto-discovery: .Registration Properties -[source,yaml] +[configuration] ---- aws: route53: registration: enabled: true - aws-service-id= + aws-service-id: discovery: - namespace-id= + namespace-id: ---- ==== Discovery Client Configuration .Discovery Properties -[source,yaml] +[configuration] ---- aws: route53: discovery: - client - enabled: true - aws-service-id: + client: + enabled: true + aws-service-id: ---- You can also call the following methods by getting the bean "Route53AutoNamingClient": diff --git a/src/main/docs/guide/config/bootstrap.adoc b/src/main/docs/guide/config/bootstrap.adoc index d3bc91af1b3..8dad586817a 100644 --- a/src/main/docs/guide/config/bootstrap.adoc +++ b/src/main/docs/guide/config/bootstrap.adoc @@ -1,4 +1,4 @@ -Most application configuration is stored in `application.yml`, environment-specific files like `application-{environment}.{extension}`, environment and system properties, etc. +Most application configuration is stored in your configuration file (e.g `application.yml`), environment-specific files like `application-{environment}.{extension}`, environment and system properties, etc. These configure the application context. But during application startup, before the application context is created, a "bootstrap" context can be created to store configuration necessary to retrieve additional configuration for the main context. Typically that additional configuration is in some remote source. diff --git a/src/main/docs/guide/config/configurationProperties.adoc b/src/main/docs/guide/config/configurationProperties.adoc index c64031788c6..ebb31f7b329 100644 --- a/src/main/docs/guide/config/configurationProperties.adoc +++ b/src/main/docs/guide/config/configurationProperties.adoc @@ -118,8 +118,7 @@ Durations can be specified by appending the unit with a number. Supported units For example to configure the default HTTP client read timeout: -.Using Duration Values -[source,yaml] +[configuration,title="Using Duration Values"] ---- micronaut: http: @@ -131,8 +130,7 @@ micronaut: Lists and arrays can be specified in Java properties files as comma-separated values, or in YAML using native YAML lists. The generic types are used to convert the values. For example in YAML: -.Specifying lists or arrays in YAML -[source,yaml] +[configuration,title="Specifying lists or arrays in YAML"] ---- my: app: @@ -144,24 +142,6 @@ my: - http://bar.com ---- -Or in Java properties file format: - -.Specifying lists or arrays in Java properties comma-separated -[source,properties] ----- -my.app.integers=1,2 -my.app.urls=http://foo.com,http://bar.com ----- - -Alternatively you can use an index: - -.Specifying lists or arrays in Java properties using index -[source,properties] ----- -my.app.integers[0]=1 -my.app.integers[1]=2 ----- - For the above example configurations you can define properties to bind to with the target type supplied via generics: [source,java] diff --git a/src/main/docs/guide/config/propertySource.adoc b/src/main/docs/guide/config/propertySource.adoc index 91fbba9de2d..51e479c724d 100644 --- a/src/main/docs/guide/config/propertySource.adoc +++ b/src/main/docs/guide/config/propertySource.adoc @@ -62,7 +62,7 @@ It is important to note that it is not recommended to store sensitive configurat It is good practise to instead externalize sensitive configuration completely from the application code using preferably a external secret manager system (there are many options here, many provided by Cloud providers) or environment variables that are set during the deployment of the application. You can also use property placeholders (see the following section), to customize names of the environment variables to use and supply default values: .Using Property Value Placeholders to Define Secure Configuration -[source,java] +[configuration] ---- datasources: default: @@ -87,10 +87,9 @@ As mentioned in the previous section, Micronaut includes a property placeholder TIP: Programmatic usage is also possible via the api:io.micronaut.context.env.PropertyPlaceholderResolver[] interface. -The basic syntax is to wrap a reference to a property in `${...}`. For example in `application.yml`: +The basic syntax is to wrap a reference to a property in `${...}`. For example: -.Defining Property Placeholders -[source,yaml] +[configuration,title="Defining Property Placeholders"] ---- myapp: endpoint: http://${micronaut.server.host}:${micronaut.server.port}/foo @@ -100,8 +99,7 @@ The above example embeds references to the `micronaut.server.host` and `micronau You can specify default values by defining a value after the `:` character. For example: -.Using Default Values -[source,yaml] +[configuration,title="Using Default Values"] ---- myapp: endpoint: http://${micronaut.server.host:localhost}:${micronaut.server.port:8080}/foo @@ -110,7 +108,7 @@ myapp: The above example defaults to `localhost` and port `8080` if no value is found (rather than throwing an exception). Note that if the default value contains a `:` character, you must escape it using backticks: .Using Backticks -[source,yaml] +[configuration] ---- myapp: endpoint: ${server.address:`http://localhost:8080`}/foo @@ -152,7 +150,7 @@ NOTE: The configuration above does not have any impact on property placeholders. You can use `random` values by using the following properties. These can be used in configuration files as variables like the following. -[source,yaml] +[configuration] ---- micronaut: application: @@ -195,7 +193,7 @@ The `random.int`, `random.integer`, `random.long` and `random.float` properties - `(max)` where max is an exclusive value - `[min,max]` where min being inclusive and max being exclusive values. -[source,yaml] +[configuration] ---- instance: id: ${random.int[5,10]} @@ -209,4 +207,3 @@ NOTE: The range could vary from negative to positive as well. For beans that inject required properties, the injection and potential failure will not occur until the bean is requested. To verify at startup that the properties exist and can be injected, the bean can be annotated with ann:io.micronaut.context.annotation.Context[]. Context-scoped beans are injected at startup, and startup fails if any required properties are missing or cannot be converted to the required type. IMPORTANT: It is recommended to use this feature sparingly to ensure fast startup. - diff --git a/src/main/docs/guide/config/valueAnnotation.adoc b/src/main/docs/guide/config/valueAnnotation.adoc index cbb657506e9..14ff27ba646 100644 --- a/src/main/docs/guide/config/valueAnnotation.adoc +++ b/src/main/docs/guide/config/valueAnnotation.adoc @@ -91,8 +91,7 @@ The above instead injects the value of the `my.url` property resolved from appli You can also use this feature to resolve sub maps. For example, consider the following configuration: -.Example `application.yml` configuration -[source,yaml] +[configuration] ---- datasources: default: diff --git a/src/main/docs/guide/configurations/dataAccess/mongoSupport.adoc b/src/main/docs/guide/configurations/dataAccess/mongoSupport.adoc index 76e8664a5d0..00e9e6500ad 100644 --- a/src/main/docs/guide/configurations/dataAccess/mongoSupport.adoc +++ b/src/main/docs/guide/configurations/dataAccess/mongoSupport.adoc @@ -13,10 +13,10 @@ Micronaut can automatically configure the native MongoDB Java driver. To use thi dependency:micronaut-mongo-reactive[groupId="io.micronaut.mongodb"] -Then configure the URI of the MongoDB server in `application.yml`: +Then configure the URI of the MongoDB server in your configuration file (e.g `application.yml`): .Configuring a MongoDB server -[source,yaml] +[configuration] ---- mongodb: uri: mongodb://username:password@localhost:27017/databaseName @@ -30,7 +30,7 @@ To use the blocking driver, add a dependency to your build on the mongo-java-dri [source,groovy] ---- -compile "org.mongodb:mongo-java-driver" +runtimeOnly "org.mongodb:mongo-java-driver" ---- Then the blocking https://mongodb.github.io/mongo-java-driver/3.7/javadoc/com/mongodb/MongoClient.html[MongoClient] will be available for injection. diff --git a/src/main/docs/guide/configurations/dataAccess/neo4jSupport.adoc b/src/main/docs/guide/configurations/dataAccess/neo4jSupport.adoc index 539886ec565..e1bc1e3ce7f 100644 --- a/src/main/docs/guide/configurations/dataAccess/neo4jSupport.adoc +++ b/src/main/docs/guide/configurations/dataAccess/neo4jSupport.adoc @@ -13,10 +13,10 @@ To configure the Neo4j Bolt driver, first add the `neo4j-bolt` module to your bu dependency::micronaut-neo4j-bolt[groupId="io.micronaut.neo4j"] -Then configure the URI of the Neo4j server in `application.yml`: +Then configure the URI of the Neo4j server in your configuration file (e.g `application.yml`): .Configuring `neo4j.uri` -[source,yaml] +[configuration] ---- neo4j: uri: bolt://localhost diff --git a/src/main/docs/guide/configurations/dataAccess/redisSupport.adoc b/src/main/docs/guide/configurations/dataAccess/redisSupport.adoc index 56adbc45c68..1e2f9cd92a0 100644 --- a/src/main/docs/guide/configurations/dataAccess/redisSupport.adoc +++ b/src/main/docs/guide/configurations/dataAccess/redisSupport.adoc @@ -18,10 +18,10 @@ To configure the Lettuce driver, first add the `redis-lettuce` module to your bu compile "io.micronaut.redis:micronaut-redis-lettuce" ---- -Then configure the URI of the Redis server in `application.yml`: +Then configure the URI of the Redis server in your configuration file (e.g `application.yml`): .Configuring `redis.uri` -[source,yaml] +[configuration] ---- redis: uri: redis://localhost diff --git a/src/main/docs/guide/configurations/otherConfigurations/rabbitmq.adoc b/src/main/docs/guide/configurations/otherConfigurations/rabbitmq.adoc index 0f23f88dfc4..cad4d9423ab 100644 --- a/src/main/docs/guide/configurations/otherConfigurations/rabbitmq.adoc +++ b/src/main/docs/guide/configurations/otherConfigurations/rabbitmq.adoc @@ -15,7 +15,7 @@ A RabbitMQ connection factory bean will be provided based on the configuration v For example: -[source,yaml] +[configuration] ---- rabbitmq: uri: amqp://user:pass@host:10000/vhost diff --git a/src/main/docs/guide/httpClient/clientAnnotation/clientAnnotationStreaming.adoc b/src/main/docs/guide/httpClient/clientAnnotation/clientAnnotationStreaming.adoc index b6a9141b7e7..865ebe9a8f1 100644 --- a/src/main/docs/guide/httpClient/clientAnnotation/clientAnnotationStreaming.adoc +++ b/src/main/docs/guide/httpClient/clientAnnotation/clientAnnotationStreaming.adoc @@ -49,10 +49,10 @@ When streaming responses from servers, the underlying HTTP client will not apply Instead, the `read-idle-timeout` setting (which defaults to 5 minutes) dictates when to close a connection after it becomes idle. -If you stream data from a server that defines a longer delay than 5 minutes between items, you should adjust `readIdleTimeout`. The following configuration in `application.yml` demonstrates how: +If you stream data from a server that defines a longer delay than 5 minutes between items, you should adjust `readIdleTimeout`. The following configuration in your configuration file (e.g `application.yml`) demonstrates how: .Adjusting the readIdleTimeout -[source,yaml] +[configuration] ---- micronaut: http: diff --git a/src/main/docs/guide/httpClient/clientAnnotation/clientHeaders.adoc b/src/main/docs/guide/httpClient/clientAnnotation/clientHeaders.adoc index 080b7bd0c6c..df59fc0ba07 100644 --- a/src/main/docs/guide/httpClient/clientAnnotation/clientHeaders.adoc +++ b/src/main/docs/guide/httpClient/clientAnnotation/clientHeaders.adoc @@ -10,10 +10,10 @@ snippet::io.micronaut.docs.annotation.headers.PetClient[tags="class", indent=0, The above example defines a ann:http.annotation.Header[] annotation on the `PetClient` interface that reads the `pet.client.id` property using property placeholder configuration. -Then set the following in `application.yml` to populate the value: +Then set the following in your configuration file (e.g `application.yml`) to populate the value: -.Configuring Headers in YAML -[source,yaml] +.Configuring Headers +[configuration] ---- pet: client: diff --git a/src/main/docs/guide/httpClient/clientAnnotation/clientJackson.adoc b/src/main/docs/guide/httpClient/clientAnnotation/clientJackson.adoc index 0d1d40c884a..a7a9d85fb19 100644 --- a/src/main/docs/guide/httpClient/clientAnnotation/clientJackson.adoc +++ b/src/main/docs/guide/httpClient/clientAnnotation/clientJackson.adoc @@ -1,11 +1,11 @@ As mentioned previously, Jackson is used for message encoding to JSON. A default Jackson `ObjectMapper` is configured and used by Micronaut HTTP clients. -You can override the settings used to construct the `ObjectMapper` with properties defined by the api:jackson.JacksonConfiguration[] class in `application.yml`. +You can override the settings used to construct the `ObjectMapper` with properties defined by the api:jackson.JacksonConfiguration[] class in your configuration file (e.g `application.yml`). For example, the following configuration enables indented output for Jackson: .Example Jackson Configuration -[source,yaml] +[configuration] ---- jackson: serialization: diff --git a/src/main/docs/guide/httpClient/clientAnnotation/netflixHystrix.adoc b/src/main/docs/guide/httpClient/clientAnnotation/netflixHystrix.adoc index 3f39b851eda..ebe3589a278 100644 --- a/src/main/docs/guide/httpClient/clientAnnotation/netflixHystrix.adoc +++ b/src/main/docs/guide/httpClient/clientAnnotation/netflixHystrix.adoc @@ -34,10 +34,10 @@ TIP: For information on how to customize the Hystrix thread pool, group, and pro == Enabling Hystrix Stream and Dashboard -You can enable a Server Sent Event stream to feed into the https://github.com/Netflix-Skunkworks/hystrix-dashboard[Hystrix Dashboard] by setting the `hystrix.stream.enabled` setting to `true` in `application.yml`: +You can enable a Server Sent Event stream to feed into the https://github.com/Netflix-Skunkworks/hystrix-dashboard[Hystrix Dashboard] by setting the `hystrix.stream.enabled` setting to `true` in your configuration file (e.g `application.yml`): .Enabling Hystrix Stream -[source,yaml] +[configuration] ---- hystrix: stream: diff --git a/src/main/docs/guide/httpClient/clientHttp2.adoc b/src/main/docs/guide/httpClient/clientHttp2.adoc index d4e6b6f4504..df9f23e70e9 100644 --- a/src/main/docs/guide/httpClient/clientHttp2.adoc +++ b/src/main/docs/guide/httpClient/clientHttp2.adoc @@ -1,7 +1,7 @@ By default, Micronaut's HTTP client is configured to support HTTP 1.1. To enable support for HTTP/2, set the supported HTTP version in configuration: .Enabling HTTP/2 in Clients -[source,yaml] +[configuration] ---- micronaut: http: diff --git a/src/main/docs/guide/httpClient/lowLevelHttpClient/clientBasics.adoc b/src/main/docs/guide/httpClient/lowLevelHttpClient/clientBasics.adoc index af0085eea2e..2d7f9333594 100644 --- a/src/main/docs/guide/httpClient/lowLevelHttpClient/clientBasics.adoc +++ b/src/main/docs/guide/httpClient/lowLevelHttpClient/clientBasics.adoc @@ -60,10 +60,9 @@ To debug requests being sent and received from the HTTP client you can enable tr ==== Client Specific Debugging / Tracing -To enable client-specific logging you can configure the default logger for all HTTP clients. You can also configure different loggers for different clients using <<_client_specific_configuration, Client-Specific Configuration>>. For example, in `application.yml`: +To enable client-specific logging you can configure the default logger for all HTTP clients. You can also configure different loggers for different clients using <<_client_specific_configuration, Client-Specific Configuration>>. For example, in your configuration file (e.g `application.yml`): -.application.yml -[source,xml] +[configuration] ---- micronaut: http: diff --git a/src/main/docs/guide/httpClient/lowLevelHttpClient/clientConfiguration.adoc b/src/main/docs/guide/httpClient/lowLevelHttpClient/clientConfiguration.adoc index 0b65eb51d34..732834233ce 100644 --- a/src/main/docs/guide/httpClient/lowLevelHttpClient/clientConfiguration.adoc +++ b/src/main/docs/guide/httpClient/lowLevelHttpClient/clientConfiguration.adoc @@ -1,9 +1,9 @@ === Global Configuration for All Clients -The default HTTP client configuration is a <> named api:http.client.DefaultHttpClientConfiguration[] that allows configuring the default behaviour for all HTTP clients. For example, in `application.yml`: +The default HTTP client configuration is a <> named api:http.client.DefaultHttpClientConfiguration[] that allows configuring the default behaviour for all HTTP clients. For example, in your configuration file (e.g `application.yml`): .Altering default HTTP client configuration -[source,yaml] +[configuration] ---- micronaut: http: @@ -15,10 +15,10 @@ The above example sets the `readTimeout` property of the api:http.client.HttpCli === Client Specific Configuration -To have separate configuration per-client, there are a couple of options. You can configure <> manually in `application.yml` and apply per-client configuration: +To have separate configuration per-client, there are a couple of options. You can configure <> manually in your configuration file (e.g `application.yml`) and apply per-client configuration: .Manually configuring HTTP services -[source,yaml] +[configuration] ---- micronaut: http: @@ -27,10 +27,10 @@ micronaut: urls: - http://foo1 - http://foo2 - read-timeout: 5s # <1> + read-timeout: 5s ---- -<1> The read timeout is applied to the `foo` client. +- The `read-timeout` is applied to the `foo` client. WARN: This client configuration can be used in conjunction with the `@Client` annotation, either by injecting an `HttpClient` directly or use on a client interface. In any case, all other attributes on the annotation *will be ignored* except the service id. @@ -80,7 +80,7 @@ ReactorHttpClient httpClient; A client that handles a significant number of requests will benefit from enabling HTTP client connection pooling. The following configuration enables pooling for the `foo` client: .Manually configuring HTTP services -[source,yaml] +[configuration] ---- micronaut: http: @@ -90,12 +90,11 @@ micronaut: - http://foo1 - http://foo2 pool: - enabled: true # <1> - max-connections: 50 # <2> + enabled: true + max-connections: 50 ---- -<1> Enables the pool -<2> Sets the maximum number of connections in the pool +- `pool` enables the pool and sets the maximum number of connections for it See the API for link:{api}/io/micronaut/http/client/HttpClientConfiguration.ConnectionPoolConfiguration.html[ConnectionPoolConfiguration] for details on available pool configuration options. @@ -106,7 +105,7 @@ By default, Micronaut shares a common Netty `EventLoopGroup` for worker threads This `EventLoopGroup` can be configured via the `micronaut.netty.event-loops.default` property: .Configuring The Default Event Loop -[source,yaml] +[configuration] ---- micronaut: netty: @@ -125,7 +124,7 @@ For example, if your interactions with an HTTP client involve CPU-intensive work The following example configures an additional event loop group called "other" with 10 threads: .Configuring Additional Event Loops -[source,yaml] +[configuration] ---- micronaut: netty: @@ -138,7 +137,7 @@ micronaut: Once an additional event loop has been configured you can alter the HTTP client configuration to use it: .Altering the Event Loop Group used by Clients -[source,yaml] +[configuration] ---- micronaut: http: diff --git a/src/main/docs/guide/httpServer/apiVersioning.adoc b/src/main/docs/guide/httpServer/apiVersioning.adoc index b9e9f6ad46f..b2ae1ce9520 100644 --- a/src/main/docs/guide/httpServer/apiVersioning.adoc +++ b/src/main/docs/guide/httpServer/apiVersioning.adoc @@ -7,10 +7,10 @@ snippet::io.micronaut.docs.web.router.version.VersionedController[tags="imports, <1> The `helloV1` method is declared as version `1` <2> The `helloV2` method is declared as version `2` -Then enable versioning by setting `micronaut.router.versioning.enabled` to `true` in `application.yml`: +Then enable versioning by setting `micronaut.router.versioning.enabled` to `true` in your configuration file (e.g `application.yml`): .Enabling Versioning -[source,yaml] +[configuration] ---- micronaut: router: @@ -21,27 +21,27 @@ micronaut: By default Micronaut has two strategies for resolving the version based on an HTTP header named `X-API-VERSION` or a request parameter named `api-version`, however this is configurable. A full configuration example can be seen below: .Configuring Versioning -[source,yaml] +[configuration] ---- micronaut: router: versioning: - enabled: true <1> + enabled: true parameter: - enabled: false # <2> - names: 'v,api-version' # <3> + enabled: false + names: 'v,api-version' header: - enabled: true # <4> - names: # <5> + enabled: true + names: - 'X-API-VERSION' - 'Accept-Version' ---- -<1> Enables versioning -<2> Enables or disables parameter-based versioning -<3> Specify the parameter names as a comma-separated list -<4> Enables or disables header-based versioning -<5> Specify the header names as a list +- This example enables versioning +- `parameter.enabled` enables or disables parameter-based versioning +- `parameter.names` specifies the parameter names as a comma-separated list +- `header.enabled` enables or disables header-based versioning +- `header.names` specifies the header names as a list If this is not enough you can also implement the api:web.router.version.resolution.RequestVersionResolver[] interface which receives the api:http.HttpRequest[] and can implement any strategy you choose. @@ -50,16 +50,16 @@ If this is not enough you can also implement the api:web.router.version.resoluti It is possible to supply a default version through configuration. .Configuring Default Version -[source,yaml] +[configuration] ---- micronaut: router: versioning: enabled: true - default-version: 3 <1> + default-version: 3 ---- -<1> Sets the default version +- This example enables versioning and sets the default version A route is *not* matched if the following conditions are met: @@ -90,7 +90,7 @@ include::{includedir}configurationProperties/io.micronaut.http.client.intercepto For example to use `Accept-Version` as the header name: .Configuring Client Versioning -[source,yaml] +[configuration] ---- micronaut: http: @@ -105,7 +105,7 @@ micronaut: The `default` key refers to the default configuration. You can specify client-specific configuration by using the value passed to `@Client` (typically the service ID). For example: .Configuring Versioning -[source,yaml] +[configuration] ---- micronaut: http: diff --git a/src/main/docs/guide/httpServer/consumesAnnotation.adoc b/src/main/docs/guide/httpServer/consumesAnnotation.adoc index 34fbb971ff1..46e067a92ae 100644 --- a/src/main/docs/guide/httpServer/consumesAnnotation.adoc +++ b/src/main/docs/guide/httpServer/consumesAnnotation.adoc @@ -10,7 +10,7 @@ snippet::io.micronaut.docs.server.consumes.ConsumesController[tags="imports,claz Normally JSON parsing only happens if the content type is `application/json`. The other api:io.micronaut.http.codec.MediaTypeCodec[] classes behave similarly in that they have predefined content types they can process. To extend the list of media types that a given codec processes, provide configuration that will be stored in api:io.micronaut.http.codec.CodecConfiguration[]: -[source,yaml] +[configuration] ---- micronaut: codec: diff --git a/src/main/docs/guide/httpServer/formData.adoc b/src/main/docs/guide/httpServer/formData.adoc index 6b41738cadd..a67e6616d1b 100644 --- a/src/main/docs/guide/httpServer/formData.adoc +++ b/src/main/docs/guide/httpServer/formData.adoc @@ -6,7 +6,7 @@ In practice this means that to bind regular form data, the only change required snippet::io.micronaut.docs.server.json.PersonController[tags="class,regular,endclass", indent=0, title="Binding Form Data to POJOs"] -TIP: To avoid denial of service attacks, collection types and arrays created during binding are limited by the setting `jackson.arraySizeThreshold` in `application.yml` +TIP: To avoid denial of service attacks, collection types and arrays created during binding are limited by the setting `jackson.arraySizeThreshold` in your configuration file (e.g `application.yml`) Alternatively, instead of using a POJO you can bind form data directly to method parameters (which works with JSON too!): diff --git a/src/main/docs/guide/httpServer/http2Server.adoc b/src/main/docs/guide/httpServer/http2Server.adoc index e0fedcb5fb2..106baae965e 100644 --- a/src/main/docs/guide/httpServer/http2Server.adoc +++ b/src/main/docs/guide/httpServer/http2Server.adoc @@ -5,7 +5,7 @@ Since Micronaut 2.x, Micronaut's Netty-based HTTP server can be configured to su The first step is to set the supported HTTP version in the server configuration: .Enabling HTTP/2 Support -[source,yaml] +[configuration] ---- micronaut: server: @@ -17,7 +17,7 @@ With this configuration, Micronaut enables support for the `h2c` protocol (see h Since browsers don't support `h2c` and in general https://httpwg.org/specs/rfc7540.html#discover-https[HTTP/2 over TLS] (the `h2` protocol), it is recommended for production that you enable <>. For development this can be done with: .Enabling `h2` Protocol Support -[source,yaml] +[configuration] ---- micronaut: server: diff --git a/src/main/docs/guide/httpServer/jsonBinding.adoc b/src/main/docs/guide/httpServer/jsonBinding.adoc index d41d3cfc32e..609b368e45b 100644 --- a/src/main/docs/guide/httpServer/jsonBinding.adoc +++ b/src/main/docs/guide/httpServer/jsonBinding.adoc @@ -60,7 +60,7 @@ All Jackson configuration keys start with `jackson`. Example: -[source,yaml] +[configuration] ---- jackson: serializationInclusion: ALWAYS @@ -81,7 +81,7 @@ All features can be configured with their name as the key and a boolean to indic Example: -[source,yaml] +[configuration] ---- jackson: serialization: @@ -99,7 +99,7 @@ This can be achieved by providing your own `JsonFactory` bean, or by providing a === Support for `@JsonView` -You can use the `@JsonView` annotation on controller methods if you set `jackson.json-view.enabled` to `true` in `application.yml`. +You can use the `@JsonView` annotation on controller methods if you set `jackson.json-view.enabled` to `true` in your configuration file (e.g `application.yml`). Jackson's `@JsonView` annotation lets you control which properties are exposed on a per-response basis. See https://www.baeldung.com/jackson-json-view-annotation[Jackson JSON Views] for more information. @@ -124,7 +124,7 @@ During JSON parsing, the framework may convert any incoming data to an intermedi If you need full accuracy for number types, use the following configuration: -[source,yaml] +[configuration] ---- jackson: deserialization: diff --git a/src/main/docs/guide/httpServer/reactiveServer.adoc b/src/main/docs/guide/httpServer/reactiveServer.adoc index 9f59021be4a..a25df2ad1aa 100644 --- a/src/main/docs/guide/httpServer/reactiveServer.adoc +++ b/src/main/docs/guide/httpServer/reactiveServer.adoc @@ -5,7 +5,7 @@ This makes it critical that if you do any blocking I/O operations (for example i For example the following configuration configures the I/O thread pool as a fixed thread pool with 75 threads (similar to what a traditional blocking server such as Tomcat uses in the thread-per-request model): .Configuring the IO thread pool -[source,yaml] +[configuration] ---- micronaut: executors: diff --git a/src/main/docs/guide/httpServer/reactiveServer/bodyAnnotation.adoc b/src/main/docs/guide/httpServer/reactiveServer/bodyAnnotation.adoc index b4ba9baa50b..86cbac74193 100644 --- a/src/main/docs/guide/httpServer/reactiveServer/bodyAnnotation.adoc +++ b/src/main/docs/guide/httpServer/reactiveServer/bodyAnnotation.adoc @@ -10,7 +10,7 @@ snippet::io.micronaut.docs.server.body.MessageController[tags="imports,class,ech Note that reading the request body is done in a non-blocking manner in that the request contents are read as the data becomes available and accumulated into the String passed to the method. -TIP: The `micronaut.server.maxRequestSize` setting in `application.yml` limits the size of the data (the default maximum request size is 10MB) read/buffered by the server. `@Size` is *not* a replacement for this setting. +TIP: The `micronaut.server.maxRequestSize` setting in your configuration file (e.g `application.yml`) limits the size of the data (the default maximum request size is 10MB) read/buffered by the server. `@Size` is *not* a replacement for this setting. Regardless of the limit, for a large amount of data accumulating the data into a String in-memory may lead to memory strain on the server. A better approach is to include a Reactive library in your project (such as `Reactor`, `RxJava`,or `Akka`) that supports the Reactive streams implementation and stream the data it becomes available: diff --git a/src/main/docs/guide/httpServer/runningSpecificPort.adoc b/src/main/docs/guide/httpServer/runningSpecificPort.adoc index 83ff4816656..137dda39e8c 100644 --- a/src/main/docs/guide/httpServer/runningSpecificPort.adoc +++ b/src/main/docs/guide/httpServer/runningSpecificPort.adoc @@ -1,6 +1,6 @@ By default the server runs on port 8080. However, you can set the server to run on a specific port: -[source, yaml] +[configuration] ---- micronaut: server: @@ -11,7 +11,7 @@ TIP: This is also configurable from an environment variable, e.g. `MICRONAUT_SER To run on a random port: -[source, yaml] +[configuration] ---- micronaut: server: diff --git a/src/main/docs/guide/httpServer/serverConfiguration.adoc b/src/main/docs/guide/httpServer/serverConfiguration.adoc index fdc460b8e0e..16971d46536 100644 --- a/src/main/docs/guide/httpServer/serverConfiguration.adoc +++ b/src/main/docs/guide/httpServer/serverConfiguration.adoc @@ -1,25 +1,25 @@ The HTTP server features a number of configuration options. They are defined in the api:http.server.netty.configuration.NettyHttpServerConfiguration[] configuration class, which extends api:http.server.HttpServerConfiguration[]. -The following example shows how to tweak configuration options for the server via `application.yml`: +The following example shows how to tweak configuration options for the server via your configuration file (e.g `application.yml`): .Configuring HTTP server settings -[source,yaml] +[configuration] ---- micronaut: server: maxRequestSize: 1MB - host: localhost # <1> + host: localhost netty: - maxHeaderSize: 500KB # <2> + maxHeaderSize: 500KB worker: - threads: 8 # <3> + threads: 8 childOptions: - autoRead: true # <4> + autoRead: true ---- -<1> By default Micronaut binds to all network interfaces. Use `localhost` to bind only to loopback network interface -<2> Maximum size for headers -<3> Number of Netty worker threads -<4> Auto read request body +- By default Micronaut binds to all network interfaces. Use `localhost` to bind only to loopback network interface +- `maxHeaderSize` sets the maximum size for headers +- `worker.threads` specifies the number of Netty worker threads +- `autoRead` enables request body auto read include::{includedir}configurationProperties/io.micronaut.http.server.netty.configuration.NettyHttpServerConfiguration.adoc[] @@ -48,7 +48,7 @@ dependency:netty-transport-native-epoll[groupId="io.netty",scope="runtimeOnly",c Then configure the default event loop group to prefer native transports: .Configuring The Default Event Loop to Prefer Native Transports -[source,yaml] +[configuration] ---- micronaut: netty: diff --git a/src/main/docs/guide/httpServer/serverConfiguration/accessLogger.adoc b/src/main/docs/guide/httpServer/serverConfiguration/accessLogger.adoc index 12e5d4d4423..34da0cf2cc9 100644 --- a/src/main/docs/guide/httpServer/serverConfiguration/accessLogger.adoc +++ b/src/main/docs/guide/httpServer/serverConfiguration/accessLogger.adoc @@ -1,38 +1,47 @@ In the spirit of https://httpd.apache.org/docs/current/mod/mod_log_config.html[apache mod_log_config] and https://tomcat.apache.org/tomcat-10.0-doc/config/valve.html#Access_Logging[Tomcat Access Log Valve], it is possible to enable an access logger for the HTTP server (this works for both HTTP/1 and HTTP/2). -To enable and configure the access logger, in `application.yml` set: +To enable and configure the access logger, in your configuration file (e.g `application.yml`) set: .Enabling the access logger -[source,yaml] +[configuration] ---- micronaut: server: netty: access-logger: - enabled: true # Enables the access logger - logger-name: my-access-logger # A logger name, optional, default is `HTTP_ACCESS_LOGGER` - log-format: common # A log format, optional, default is Common Log Format + enabled: true + logger-name: my-access-logger + log-format: common ---- +- `enabled` Enables the access logger +- optionally specify a `logger-name`, which defaults to `HTTP_ACCESS_LOGGER` +- optionally specify a `log-format`, which defaults to the Common Log Format + ==== Filtering access logs If you wish to not log access to certain paths, you can specify regular expression filters in the configuration: .Filtering the access logs -[source,yaml] +[configuration] ---- micronaut: server: netty: access-logger: - enabled: true # Enables the access logger - logger-name: my-access-logger # A logger name, optional, default is `HTTP_ACCESS_LOGGER` - log-format: common # A log format, optional, default is Common Log Format + enabled: true + logger-name: my-access-logger + log-format: common exclusions: - /health - /path/.+ ---- +- `enabled` Enables the access logger +- optionally specify a `logger-name`, which defaults to `HTTP_ACCESS_LOGGER` +- optionally specify a `log-format`, which defaults to the Common Log Format + + ==== Logback Configuration In addition to enabling the access logger, you must add a logger for the specified or default logger name. For instance using the default logger name for logback: diff --git a/src/main/docs/guide/httpServer/serverConfiguration/cors/corsAllowCredentials.adoc b/src/main/docs/guide/httpServer/serverConfiguration/cors/corsAllowCredentials.adoc index f976c613a40..65c5b6b121e 100644 --- a/src/main/docs/guide/httpServer/serverConfiguration/cors/corsAllowCredentials.adoc +++ b/src/main/docs/guide/httpServer/serverConfiguration/cors/corsAllowCredentials.adoc @@ -1,7 +1,7 @@ Credentials are allowed by default for CORS requests. To disallow credentials, set the `allowCredentials` option to `false`. .Example CORS Configuration -[source,yaml] +[configuration] ---- micronaut: server: diff --git a/src/main/docs/guide/httpServer/serverConfiguration/cors/corsAllowedHeaders.adoc b/src/main/docs/guide/httpServer/serverConfiguration/cors/corsAllowedHeaders.adoc index f4c215d0472..ea08df88177 100644 --- a/src/main/docs/guide/httpServer/serverConfiguration/cors/corsAllowedHeaders.adoc +++ b/src/main/docs/guide/httpServer/serverConfiguration/cors/corsAllowedHeaders.adoc @@ -3,7 +3,7 @@ To allow any request header for a given configuration, don't include the `allowe For multiple allowed headers, set the `allowedHeaders` key of the configuration to a list of strings. .Example CORS Configuration -[source,yaml] +[configuration] ---- micronaut: server: diff --git a/src/main/docs/guide/httpServer/serverConfiguration/cors/corsAllowedMethods.adoc b/src/main/docs/guide/httpServer/serverConfiguration/cors/corsAllowedMethods.adoc index 1d4c963efd1..a2b033ae3d5 100644 --- a/src/main/docs/guide/httpServer/serverConfiguration/cors/corsAllowedMethods.adoc +++ b/src/main/docs/guide/httpServer/serverConfiguration/cors/corsAllowedMethods.adoc @@ -3,7 +3,7 @@ To allow any request method for a given configuration, don't include the `allowe For multiple allowed methods, set the `allowedMethods` key of the configuration to a list of strings. .Example CORS Configuration -[source,yaml] +[configuration] ---- micronaut: server: diff --git a/src/main/docs/guide/httpServer/serverConfiguration/cors/corsAllowedOrigins.adoc b/src/main/docs/guide/httpServer/serverConfiguration/cors/corsAllowedOrigins.adoc index 50dbf256c61..a1a10186380 100644 --- a/src/main/docs/guide/httpServer/serverConfiguration/cors/corsAllowedOrigins.adoc +++ b/src/main/docs/guide/httpServer/serverConfiguration/cors/corsAllowedOrigins.adoc @@ -5,7 +5,7 @@ For multiple valid origins, set the `allowedOrigins` key of the configuration to Regular expressions are passed to link:{javase}java/util/regex/Pattern.html#compile-java.lang.String-[Pattern#compile] and compared to the request origin with link:{javase}java/util/regex/Matcher.html#matches--[Matcher#matches]. .Example CORS Configuration -[source,yaml] +[configuration] ---- micronaut: server: diff --git a/src/main/docs/guide/httpServer/serverConfiguration/cors/corsConfiguration.adoc b/src/main/docs/guide/httpServer/serverConfiguration/cors/corsConfiguration.adoc index ccd7bd4f75b..96862b8e19e 100644 --- a/src/main/docs/guide/httpServer/serverConfiguration/cors/corsConfiguration.adoc +++ b/src/main/docs/guide/httpServer/serverConfiguration/cors/corsConfiguration.adoc @@ -1,7 +1,7 @@ -To enable processing of CORS requests, modify your configuration. For example with `application.yml`: +To enable processing of CORS requests, modify your configuration in the application configuration file: .CORS Configuration Example -[source,yaml] +[configuration] ---- micronaut: server: @@ -14,7 +14,7 @@ By only enabling CORS processing, a "wide open" strategy is adopted that allows To change the settings for all origins or a specific origin, change the configuration to provide one or more "configurations". By providing any configuration, the default "wide open" configuration is not configured. .CORS Configurations -[source,yaml] +[configuration] ---- micronaut: server: diff --git a/src/main/docs/guide/httpServer/serverConfiguration/cors/corsExposedHeaders.adoc b/src/main/docs/guide/httpServer/serverConfiguration/cors/corsExposedHeaders.adoc index 11a931a294d..ed2aeb6df5f 100644 --- a/src/main/docs/guide/httpServer/serverConfiguration/cors/corsExposedHeaders.adoc +++ b/src/main/docs/guide/httpServer/serverConfiguration/cors/corsExposedHeaders.adoc @@ -1,7 +1,7 @@ To configure the headers that are sent in the response to a CORS request through the `Access-Control-Expose-Headers` header, include a list of strings for the `exposedHeaders` key in your configuration. None are exposed by default. .Example CORS Configuration -[source,yaml] +[configuration] ---- micronaut: server: diff --git a/src/main/docs/guide/httpServer/serverConfiguration/cors/corsMaxAge.adoc b/src/main/docs/guide/httpServer/serverConfiguration/cors/corsMaxAge.adoc index 27a9cafe456..93f2c2557f9 100644 --- a/src/main/docs/guide/httpServer/serverConfiguration/cors/corsMaxAge.adoc +++ b/src/main/docs/guide/httpServer/serverConfiguration/cors/corsMaxAge.adoc @@ -1,7 +1,7 @@ The default maximum age that preflight requests can be cached is 30 minutes. To change that behavior, specify a value in seconds. .Example CORS Configuration -[source,yaml] +[configuration] ---- micronaut: server: diff --git a/src/main/docs/guide/httpServer/serverConfiguration/cors/corsMultipleHeaderValues.adoc b/src/main/docs/guide/httpServer/serverConfiguration/cors/corsMultipleHeaderValues.adoc index f90ed768539..81d929a8e18 100644 --- a/src/main/docs/guide/httpServer/serverConfiguration/cors/corsMultipleHeaderValues.adoc +++ b/src/main/docs/guide/httpServer/serverConfiguration/cors/corsMultipleHeaderValues.adoc @@ -1,6 +1,6 @@ By default, when a header has multiple values, multiple headers are sent, each with a single value. It is possible to change the behavior to send a single header with a comma-separated list of values by setting a configuration option. -[source,yaml] +[configuration] ---- micronaut: server: diff --git a/src/main/docs/guide/httpServer/serverConfiguration/dualProtocol.adoc b/src/main/docs/guide/httpServer/serverConfiguration/dualProtocol.adoc index 65f916a0131..58df6ab710e 100644 --- a/src/main/docs/guide/httpServer/serverConfiguration/dualProtocol.adoc +++ b/src/main/docs/guide/httpServer/serverConfiguration/dualProtocol.adoc @@ -1,23 +1,23 @@ -Micronaut supports binding both HTTP and HTTPS. To enable dual protocol support, modify your configuration. For example with `application.yml`: +Micronaut supports binding both HTTP and HTTPS. To enable dual protocol support, modify your configuration. For example: .Dual Protocol Configuration Example -[source,yaml] +[configuration] ---- micronaut: server: ssl: enabled: true - build-self-signed: true # <1> - dual-protocol : true #<2> + build-self-signed: true + dual-protocol : true ---- -<1> You must configure SSL for HTTPS to work. In this example we are just using a self-signed certificate, but see <> for other configurations -<2> Enabling both HTTP and HTTPS is an opt-in feature - setting the `dualProtocol` flag enables it. By default Micronaut only enables one +- You must configure SSL for HTTPS to work. In this example we are just using a self-signed certificate with `build-self-signed`, but see <> for other configurations +- `dual-protocol` enables both HTTP and HTTPS is an opt-in feature - setting the `dualProtocol` flag enables it. By default Micronaut only enables one -It is also possible to redirect automatically all HTTP request to HTTPS. Besides the previous configuration, you need to enable this option. For example, with `application.yml`: +It is also possible to redirect automatically all HTTP request to HTTPS. Besides the previous configuration, you need to enable this option. For example: .Enable HTTP to HTTPS Redirects -[source,yaml] +[configuration] ---- micronaut: server: @@ -25,6 +25,7 @@ micronaut: enabled: true build-self-signed: true dual-protocol : true - http-to-https-redirect: true # <1> + http-to-https-redirect: true ---- -<1> Enable HTTP to HTTPS redirects + +- `http-to-https-redirect` enables HTTP to HTTPS redirects diff --git a/src/main/docs/guide/httpServer/serverConfiguration/https.adoc b/src/main/docs/guide/httpServer/serverConfiguration/https.adoc index f576c0e5e97..4660633ed59 100644 --- a/src/main/docs/guide/httpServer/serverConfiguration/https.adoc +++ b/src/main/docs/guide/httpServer/serverConfiguration/https.adoc @@ -1,18 +1,17 @@ - -Micronaut supports HTTPS out of the box. By default HTTPS is disabled and all requests are served using HTTP. To enable HTTPS support, modify your configuration. For example with `application.yml`: +Micronaut supports HTTPS out of the box. By default HTTPS is disabled and all requests are served using HTTP. To enable HTTPS support, modify your configuration. For example: .HTTPS Configuration Example -[source,yaml] +[configuration] ---- micronaut: server: ssl: enabled: true - buildSelfSigned: true # <1> + buildSelfSigned: true ---- -<1> Micronaut will create a self-signed certificate. +- Micronaut will create a self-signed certificate. -TIP: By default Micronaut with HTTPS support starts on port `8443` but you can change the port with the property `micronaut.server.ssl.port`. +TIP: By default, Micronaut with HTTPS support starts on port `8443` but you can change the port with the property `micronaut.server.ssl.port`. For generating self-signed certificates, the Micronaut HTTP server will use netty. Netty uses one of two approaches to generate the certificate. @@ -50,18 +49,18 @@ During the creation of the `server.p12` file it is necessary to define a passwor Now modify your configuration: .HTTPS Configuration Example -[source,yaml] +[configuration] ---- micronaut: ssl: enabled: true key-store: - path: classpath:server.p12 # <1> - password: mypassword # <2> + path: classpath:server.p12 + password: mypassword type: PKCS12 ---- -<1> The `p12` file. It can also be referenced as `file:/path/to/the/file` -<2> The password defined during the export +- Specify the `p12` file path. It can also be referenced as `file:/path/to/the/file` +- Also provide the `password` defined during the export With this configuration, if we start Micronaut and connect to `https://localhost:8443` we still see the warning in the browser, but if we inspect the certificate we can check that it is the one generated by Let's Encrypt. @@ -105,7 +104,7 @@ WARNING: If either `srcstorepass` or `alias` are not the same as defined in the Now modify your configuration: .HTTPS Configuration Example -[source,yaml] +[configuration] ---- micronaut: ssl: diff --git a/src/main/docs/guide/httpServer/serverConfiguration/listener.adoc b/src/main/docs/guide/httpServer/serverConfiguration/listener.adoc index de69117950a..2f4f21988be 100644 --- a/src/main/docs/guide/httpServer/serverConfiguration/listener.adoc +++ b/src/main/docs/guide/httpServer/serverConfiguration/listener.adoc @@ -1,13 +1,13 @@ Instead of configuring a single port, you can also specify each listener manually. -[source, yaml] +[configuration] ---- micronaut: server: netty: listeners: - httpListener: # listener name can be an arbitrary value - host: 127.0.0.1 # optional, by default binds to all interfaces + httpListener: + host: 127.0.0.1 port: 8086 ssl: false httpsListener: @@ -15,6 +15,9 @@ micronaut: ssl: true ---- +- `httpListener` is a listener name, and can be an arbitrary value +- `host` is optional, and by default binds to all interfaces + WARNING: If you specify listeners manually, other configuration such as `micronaut.server.port` will be ignored. SSL can be enabled or disabled for each listener individually. When enabled, the SSL will be configured <>. @@ -25,16 +28,18 @@ dependency:netty-transport-native-unix-common[groupId="io.netty",artifactId="net The server must also be configured to <> (epoll or kqueue). -[source, yaml] +[configuration] ---- micronaut: server: netty: listeners: - unixListener: # listener name can be an arbitrary value + unixListener: family: UNIX path: /run/micronaut.socket ssl: true ---- -NOTE: To use an abstract domain socket instead of a normal one, prefix the path with a NUL character, like `"\0/run/micronaut.socket"` \ No newline at end of file +- `unixListener` is a listener name, and can be an arbitrary value + +NOTE: To use an abstract domain socket instead of a normal one, prefix the path with a NUL character, like `"\0/run/micronaut.socket"` diff --git a/src/main/docs/guide/httpServer/serverConfiguration/threadPools.adoc b/src/main/docs/guide/httpServer/serverConfiguration/threadPools.adoc index fb9aaee4b0c..62d928e7583 100644 --- a/src/main/docs/guide/httpServer/serverConfiguration/threadPools.adoc +++ b/src/main/docs/guide/httpServer/serverConfiguration/threadPools.adoc @@ -11,7 +11,7 @@ TIP: The parent event loop can be configured with `micronaut.server.netty.parent The server can also be configured to use a different named worker event loop: .Using a different event loop for the server -[source,yaml] +[configuration] ---- micronaut: server: diff --git a/src/main/docs/guide/httpServer/serverConfiguration/threadPools/blockingOperations.adoc b/src/main/docs/guide/httpServer/serverConfiguration/threadPools/blockingOperations.adoc index 903c5608200..845e4317797 100644 --- a/src/main/docs/guide/httpServer/serverConfiguration/threadPools/blockingOperations.adoc +++ b/src/main/docs/guide/httpServer/serverConfiguration/threadPools/blockingOperations.adoc @@ -1,7 +1,7 @@ When dealing with blocking operations, Micronaut shifts the blocking operations to an unbound, caching I/O thread pool by default. You can configure the I/O thread pool using the api:scheduling.executor.ExecutorConfiguration[] named `io`. For example: .Configuring the Server I/O Thread Pool -[source,yaml] +[configuration] ---- micronaut: executors: @@ -10,4 +10,4 @@ micronaut: nThreads: 75 ---- -The above configuration creates a fixed thread pool with 75 threads. \ No newline at end of file +The above configuration creates a fixed thread pool with 75 threads. diff --git a/src/main/docs/guide/httpServer/sessions.adoc b/src/main/docs/guide/httpServer/sessions.adoc index 09a4e316153..50b8ea2a988 100644 --- a/src/main/docs/guide/httpServer/sessions.adoc +++ b/src/main/docs/guide/httpServer/sessions.adoc @@ -24,10 +24,10 @@ compile "io.micronaut:micronaut-session" compile "io.micronaut.redis:micronaut-redis-lettuce" ---- -And enable Redis sessions via configuration in `application.yml`: +And enable Redis sessions via configuration in your configuration file (e.g `application.yml`): .Enabling Redis Sessions -[source,yaml] +[configuration] ---- redis: uri: redis://localhost:6379 @@ -44,10 +44,10 @@ api:session.Session[] resolution can be configured with api:session.http.HttpSes By default, sessions are resolved using an api:session.http.HttpSessionFilter[] that looks for session identifiers via either an HTTP header (using the `Authorization-Info` or `X-Auth-Token` headers) or via a Cookie named `SESSION`. -You can disable either header resolution or cookie resolution via configuration in `application.yml`: +You can disable either header resolution or cookie resolution via configuration in your configuration file (e.g `application.yml`): .Disabling Cookie Resolution -[source,yaml] +[configuration] ---- micronaut: session: diff --git a/src/main/docs/guide/httpServer/websocket/websocketServer.adoc b/src/main/docs/guide/httpServer/websocket/websocketServer.adoc index 0e0dc5f56e2..9ce0b803bfa 100644 --- a/src/main/docs/guide/httpServer/websocket/websocketServer.adoc +++ b/src/main/docs/guide/httpServer/websocket/websocketServer.adoc @@ -71,20 +71,20 @@ $ mn create-websocket-server MyChat By default, Micronaut times out idle connections with no activity after five minutes. Normally this is not a problem as browsers automatically reconnect WebSocket sessions, however you can control this behaviour by setting the `micronaut.server.idle-timeout` setting (a negative value results in no timeout): .Setting the Connection Timeout for the Server -[source,yaml] +[configuration] ---- micronaut: server: - idle-timeout: 30m # 30 minutes + idle-timeout: 30m ---- If you use Micronaut's WebSocket client you may also wish to set the timeout on the client: .Setting the Connection Timeout for the Client -[source,yaml] +[configuration] ---- micronaut: http: client: - read-idle-timeout: 30m # 30 minutes + read-idle-timeout: 30m ---- diff --git a/src/main/docs/guide/ioc/contextEvents.adoc b/src/main/docs/guide/ioc/contextEvents.adoc index c737cd4d7c3..067389c5d81 100644 --- a/src/main/docs/guide/ioc/contextEvents.adoc +++ b/src/main/docs/guide/ioc/contextEvents.adoc @@ -26,11 +26,11 @@ If your listener performs work that might take a while, use the ann:scheduling.a snippet::io.micronaut.docs.context.events.async.SampleEventListener,io.micronaut.docs.context.events.async.SampleEventListenerSpec[tags="imports,class",indent=0,title="Asynchronously listening for Events with `@EventListener`"] -The event listener by default runs on the `scheduled` executor. You can configure this thread pool as required in `application.yml`: +The event listener by default runs on the `scheduled` executor. You can configure this thread pool as required in your configuration file (e.g `application.yml`): //TODO: Move YAML snippet to ExecutorServiceConfigSpec .Configuring Scheduled Task Thread Pool -[source,yaml] +[configuration] ---- micronaut: executors: diff --git a/src/main/docs/guide/logging/loggingConfiguration.adoc b/src/main/docs/guide/logging/loggingConfiguration.adoc index ef08b83cc8f..84fa68a8a77 100644 --- a/src/main/docs/guide/logging/loggingConfiguration.adoc +++ b/src/main/docs/guide/logging/loggingConfiguration.adoc @@ -1,6 +1,6 @@ -Log levels can be configured via properties defined in `application.yml` (and environment variables) with the `logger.levels` prefix: +Log levels can be configured via properties defined in your configuration file (e.g `application.yml`) (and environment variables) with the `logger.levels` prefix: -[source,yaml] +[configuration] ---- logger: levels: @@ -11,11 +11,11 @@ The same configuration can be achieved by setting the environment variable `LOGG ==== Custom Logback XML Configuration -[source,yaml] +[configuration] ---- logger: config: custom-logback.xml ----- +---- You can also set a custom Logback XML configuration file to be used via `logger.config`. Be aware that **the referenced file should be an accessible resource on your classpath**! @@ -23,13 +23,13 @@ You can also set a custom Logback XML configuration file to be used via `logger. To disable a logger, you need to set the logger level to `OFF`: -[source,yaml] +[configuration] ---- logger: levels: - io.verbose.logger.who.CriedWolf: OFF <1> + io.verbose.logger.who.CriedWolf: OFF ---- -1. This will disable ALL logging for the class `io.verbose.logger.who.CriedWolf` +- This will disable ALL logging for the class `io.verbose.logger.who.CriedWolf` Note that the ability to control log levels via config is controlled via the api:logging.LoggingSystem[] interface. Currently, Micronaut includes a single implementation that allows setting log levels for the Logback library. If you use another library, you should provide a bean that implements this interface. diff --git a/src/main/docs/guide/management/buildingEndpoints/endpointConfiguration.adoc b/src/main/docs/guide/management/buildingEndpoints/endpointConfiguration.adoc index 6fd0a82084b..e4d2d860596 100644 --- a/src/main/docs/guide/management/buildingEndpoints/endpointConfiguration.adoc +++ b/src/main/docs/guide/management/buildingEndpoints/endpointConfiguration.adoc @@ -17,7 +17,7 @@ The configuration values for the endpoint override those for `all`. If `endpoint For all endpoints, the following configuration values can be set. -[source,yaml] +[configuration] ---- endpoints: : diff --git a/src/main/docs/guide/management/providedEndpoints.adoc b/src/main/docs/guide/management/providedEndpoints.adoc index eed2765aa51..8ffc1661c23 100644 --- a/src/main/docs/guide/management/providedEndpoints.adoc +++ b/src/main/docs/guide/management/providedEndpoints.adoc @@ -65,7 +65,7 @@ this should be used with care because private and sensitive information will be By default, all management endpoints are exposed over the same port as the application. You can alter this behaviour by specifying the `endpoints.all.port` setting: -[source,yaml] +[configuration] ---- endpoints: all: diff --git a/src/main/docs/guide/management/providedEndpoints/beansEndpoint.adoc b/src/main/docs/guide/management/providedEndpoints/beansEndpoint.adoc index f940661534c..8a44c698939 100644 --- a/src/main/docs/guide/management/providedEndpoints/beansEndpoint.adoc +++ b/src/main/docs/guide/management/providedEndpoints/beansEndpoint.adoc @@ -7,7 +7,7 @@ To execute the beans endpoint, send a GET request to /beans. To configure the beans endpoint, supply configuration through `endpoints.beans`. .Beans Endpoint Configuration Example -[source,yaml] +[configuration] ---- endpoints: beans: diff --git a/src/main/docs/guide/management/providedEndpoints/environmentEndpoint.adoc b/src/main/docs/guide/management/providedEndpoints/environmentEndpoint.adoc index 58f388d6377..0c494d3442c 100644 --- a/src/main/docs/guide/management/providedEndpoints/environmentEndpoint.adoc +++ b/src/main/docs/guide/management/providedEndpoints/environmentEndpoint.adoc @@ -5,14 +5,16 @@ The environment endpoint returns information about the api:context.env.Environme To enable and configure the environment endpoint, supply configuration through `endpoints.env`. .Environment Endpoint Configuration Example -[source,yaml] +[configuration] ---- endpoints: env: - enabled: Boolean # default: false - sensitive: Boolean # default: true + enabled: Boolean + sensitive: Boolean ---- +- defaults are false for `enabled` and true for `sensitive` + By default the endpoint will mask all values. To customize this masking you need to supply a Bean that implements api:management.endpoint.env.EnvironmentEndpointFilter[]. diff --git a/src/main/docs/guide/management/providedEndpoints/healthEndpoint.adoc b/src/main/docs/guide/management/providedEndpoints/healthEndpoint.adoc index 4c11d947dcb..40b3bbe09f2 100644 --- a/src/main/docs/guide/management/providedEndpoints/healthEndpoint.adoc +++ b/src/main/docs/guide/management/providedEndpoints/healthEndpoint.adoc @@ -7,25 +7,25 @@ To execute the health endpoint, send a GET request to /health. Additionally the To configure the health endpoint, supply configuration through `endpoints.health`. .Health Endpoint Configuration Example -[source,yaml] +[configuration] ---- endpoints: health: enabled: Boolean sensitive: Boolean - details-visible: String <1> + details-visible: String status: http-mapping: Map ---- -<1> One of api:management.endpoint.health.DetailsVisibility[] +- `details-visible` is one of api:management.endpoint.health.DetailsVisibility[] The `details-visible` setting controls whether health detail will be exposed to users who are not authenticated. For example, setting: .Using `details-visible` -[source,yaml] +[configuration] ---- endpoints: health: @@ -50,10 +50,10 @@ The `endpoints.health.status.http-mapping` setting controls which status codes t |=== -You can provide custom mappings in `application.yml`: +You can provide custom mappings in your configuration file (e.g `application.yml`): .Custom Health Status Codes -[source,yaml] +[configuration] ---- endpoints: health: @@ -90,16 +90,19 @@ All Micronaut provided health indicators are exposed on /health and /health/read A health indicator is provided that determines the health of the application based on the amount of free disk space. Configuration for the disk space health indicator can be provided under the `endpoints.health.disk-space` key. .Disk Space Indicator Configuration Example -[source,yaml] +[configuration] ---- endpoints: health: disk-space: enabled: Boolean - path: String #The file path used to determine the disk space - threshold: String | Long #The minimum amount of free space + path: String + threshold: String | Long ---- +- `path` specifies the path used to determine the disk space +- `threshold` specifies the minimum amount of free space + The threshold can be provided as a string like "10MB" or "200KB", or the number of bytes. === JDBC diff --git a/src/main/docs/guide/management/providedEndpoints/infoEndpoint.adoc b/src/main/docs/guide/management/providedEndpoints/infoEndpoint.adoc index 918718de1ed..4c7f14f6de7 100644 --- a/src/main/docs/guide/management/providedEndpoints/infoEndpoint.adoc +++ b/src/main/docs/guide/management/providedEndpoints/infoEndpoint.adoc @@ -7,7 +7,7 @@ To execute the info endpoint, send a GET request to /info. To configure the info endpoint, supply configuration through `endpoints.info`. .Info Endpoint Configuration Example -[source,yaml] +[configuration] ---- endpoints: info: diff --git a/src/main/docs/guide/management/providedEndpoints/loggersEndpoint.adoc b/src/main/docs/guide/management/providedEndpoints/loggersEndpoint.adoc index cf11a6fae67..c4062c4796a 100644 --- a/src/main/docs/guide/management/providedEndpoints/loggersEndpoint.adoc +++ b/src/main/docs/guide/management/providedEndpoints/loggersEndpoint.adoc @@ -68,7 +68,7 @@ $ curl http://localhost:8080/loggers/ROOT To configure the loggers endpoint, supply configuration through `endpoints.loggers`. .Loggers Endpoint Configuration Example -[source,yaml] +[configuration] ---- endpoints: loggers: diff --git a/src/main/docs/guide/management/providedEndpoints/refreshEndpoint.adoc b/src/main/docs/guide/management/providedEndpoints/refreshEndpoint.adoc index d182f31734b..4f8b1a661d0 100644 --- a/src/main/docs/guide/management/providedEndpoints/refreshEndpoint.adoc +++ b/src/main/docs/guide/management/providedEndpoints/refreshEndpoint.adoc @@ -19,7 +19,7 @@ $ curl -X POST http://localhost:8080/refresh -H 'Content-Type: application/json' To configure the refresh endpoint, supply configuration through `endpoints.refresh`. .Beans Endpoint Configuration Example -[source,yaml] +[configuration] ---- endpoints: refresh: diff --git a/src/main/docs/guide/management/providedEndpoints/routesEndpoint.adoc b/src/main/docs/guide/management/providedEndpoints/routesEndpoint.adoc index b0f2dfb9128..e4295b4956a 100644 --- a/src/main/docs/guide/management/providedEndpoints/routesEndpoint.adoc +++ b/src/main/docs/guide/management/providedEndpoints/routesEndpoint.adoc @@ -7,7 +7,7 @@ To execute the routes endpoint, send a GET request to /routes. To configure the routes endpoint, supply configuration through `endpoints.routes`. .Routes Endpoint Configuration Example -[source,yaml] +[configuration] ---- endpoints: routes: diff --git a/src/main/docs/guide/management/providedEndpoints/stopEndpoint.adoc b/src/main/docs/guide/management/providedEndpoints/stopEndpoint.adoc index 4a761fcba2b..c61d9efbd3d 100644 --- a/src/main/docs/guide/management/providedEndpoints/stopEndpoint.adoc +++ b/src/main/docs/guide/management/providedEndpoints/stopEndpoint.adoc @@ -7,7 +7,7 @@ To execute the stop endpoint, send a POST request to /stop. To configure the stop endpoint, supply configuration through `endpoints.stop`. .Stop Endpoint Configuration Example -[source,yaml] +[configuration] ---- endpoints: stop: diff --git a/src/main/docs/guide/management/providedEndpoints/threadDumpEndpoint.adoc b/src/main/docs/guide/management/providedEndpoints/threadDumpEndpoint.adoc index e14e9f77924..fa1804aaeae 100644 --- a/src/main/docs/guide/management/providedEndpoints/threadDumpEndpoint.adoc +++ b/src/main/docs/guide/management/providedEndpoints/threadDumpEndpoint.adoc @@ -7,7 +7,7 @@ To execute the threaddump endpoint, send a GET request to /threaddump. To configure the threaddump endpoint, supply configuration through `endpoints.threaddump`. .Threaddump Endpoint Configuration Example -[source,yaml] +[configuration] ---- endpoints: threaddump: From 0edb327406628a90c7bd7f2d6f040cf0fc67facc Mon Sep 17 00:00:00 2001 From: Jonas Konrad Date: Tue, 14 Feb 2023 10:37:12 +0100 Subject: [PATCH 478/743] feat: Optimize HttpRequestCertificateHandler (#8771) --- .../server/netty/HttpPipelineBuilder.java | 36 ++++++++--------- .../ssl/HttpRequestCertificateHandler.java | 39 ++++++++++--------- 2 files changed, 38 insertions(+), 37 deletions(-) diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/HttpPipelineBuilder.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/HttpPipelineBuilder.java index 396eb0350a9..3e1bb834ebf 100644 --- a/http-server-netty/src/main/java/io/micronaut/http/server/netty/HttpPipelineBuilder.java +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/HttpPipelineBuilder.java @@ -16,6 +16,7 @@ package io.micronaut.http.server.netty; import io.micronaut.core.annotation.NonNull; +import io.micronaut.core.annotation.Nullable; import io.micronaut.core.naming.Named; import io.micronaut.core.util.SupplierUtil; import io.micronaut.http.context.event.HttpRequestReceivedEvent; @@ -106,8 +107,6 @@ final class HttpPipelineBuilder { private final HttpRequestDecoder requestDecoder; private final HttpResponseEncoder responseEncoder; - private final HttpRequestCertificateHandler requestCertificateHandler = new HttpRequestCertificateHandler(); - private final NettyServerCustomizer serverCustomizer; HttpPipelineBuilder(NettyHttpServer server, NettyEmbeddedServices embeddedServices, ServerSslConfiguration sslConfiguration, RoutingInBoundHandler routingInBoundHandler, HttpHostResolver hostResolver, NettyServerCustomizer serverCustomizer) { @@ -147,14 +146,15 @@ final class ConnectionPipeline { private final Channel channel; private final ChannelPipeline pipeline; - private final boolean ssl; + @Nullable + private final SslHandler sslHandler; private final NettyServerCustomizer connectionCustomizer; ConnectionPipeline(Channel channel, boolean ssl) { this.channel = channel; this.pipeline = channel.pipeline(); - this.ssl = ssl; + this.sslHandler = ssl ? sslContext.newHandler(channel.alloc()) : null; this.connectionCustomizer = serverCustomizer.specializeForChannel(channel, NettyServerCustomizer.ChannelRole.CONNECTION); } @@ -211,7 +211,7 @@ void initChannel() { if (server.getServerConfiguration().getHttpVersion() != io.micronaut.http.HttpVersion.HTTP_2_0) { configureForHttp1(); } else { - if (ssl) { + if (sslHandler != null) { configureForAlpn(); } else { configureForH2cSupport(); @@ -225,8 +225,7 @@ void initChannel() { void insertOuterTcpHandlers() { insertPcapLoggingHandler("encapsulated"); - if (ssl) { - SslHandler sslHandler = sslContext.newHandler(channel.alloc()); + if (sslHandler != null) { sslHandler.setHandshakeTimeoutMillis(sslConfiguration.getHandshakeTimeout().toMillis()); pipeline.addLast(ChannelPipelineCustomizer.HANDLER_SSL, sslHandler); @@ -264,7 +263,7 @@ void configureForHttp1() { pipeline.addLast(ChannelPipelineCustomizer.HANDLER_HTTP_SERVER_CODEC, createServerCodec()); - new StreamPipeline(channel, ssl, connectionCustomizer).insertHttp1DownstreamHandlers(); + new StreamPipeline(channel, sslHandler, connectionCustomizer).insertHttp1DownstreamHandlers(); connectionCustomizer.onInitialPipelineBuilt(); connectionCustomizer.onStreamPipelineBuilt(); @@ -281,7 +280,7 @@ private void configureForHttp2() { pipeline.addLast(new Http2MultiplexHandler(new ChannelInitializer() { @Override protected void initChannel(@NonNull Channel ch) { - StreamPipeline streamPipeline = new StreamPipeline(ch, ssl, connectionCustomizer.specializeForChannel(ch, NettyServerCustomizer.ChannelRole.REQUEST_STREAM)); + StreamPipeline streamPipeline = new StreamPipeline(ch, sslHandler, connectionCustomizer.specializeForChannel(ch, NettyServerCustomizer.ChannelRole.REQUEST_STREAM)); streamPipeline.insertHttp2FrameHandlers(); streamPipeline.streamCustomizer.onStreamPipelineBuilt(); } @@ -364,7 +363,7 @@ void configureForH2cSupport() { return new Http2ServerUpgradeCodec(connectionHandler, new Http2MultiplexHandler(new ChannelInitializer() { @Override protected void initChannel(@NonNull Http2StreamChannel ch) { - StreamPipeline streamPipeline = new StreamPipeline(ch, ssl, connectionCustomizer.specializeForChannel(ch, NettyServerCustomizer.ChannelRole.REQUEST_STREAM)); + StreamPipeline streamPipeline = new StreamPipeline(ch, sslHandler, connectionCustomizer.specializeForChannel(ch, NettyServerCustomizer.ChannelRole.REQUEST_STREAM)); streamPipeline.insertHttp2FrameHandlers(); streamPipeline.streamCustomizer.onStreamPipelineBuilt(); } @@ -403,7 +402,7 @@ protected void channelRead0(ChannelHandlerContext ctx, HttpMessage msg) { // reconfigure for http1 // note: we have to reuse the serverCodec in case it still has some data buffered - new StreamPipeline(channel, ssl, connectionCustomizer).insertHttp1DownstreamHandlers(); + new StreamPipeline(channel, sslHandler, connectionCustomizer).insertHttp1DownstreamHandlers(); connectionCustomizer.onStreamPipelineBuilt(); onRequestPipelineBuilt(); cp.fireChannelRead(ReferenceCountUtil.retain(msg)); @@ -427,19 +426,20 @@ private HttpServerCodec createServerCodec() { final class StreamPipeline { private final Channel channel; private final ChannelPipeline pipeline; - private final boolean ssl; + @Nullable + private final SslHandler sslHandler; private final NettyServerCustomizer streamCustomizer; - private StreamPipeline(Channel channel, boolean ssl, NettyServerCustomizer streamCustomizer) { + private StreamPipeline(Channel channel, @Nullable SslHandler sslHandler, NettyServerCustomizer streamCustomizer) { this.channel = channel; this.pipeline = channel.pipeline(); - this.ssl = ssl; + this.sslHandler = sslHandler; this.streamCustomizer = streamCustomizer; } void initializeChildPipelineForPushPromise(Channel childChannel) { - StreamPipeline promisePipeline = new StreamPipeline(childChannel, ssl, streamCustomizer.specializeForChannel(childChannel, NettyServerCustomizer.ChannelRole.PUSH_PROMISE_STREAM)); + StreamPipeline promisePipeline = new StreamPipeline(childChannel, sslHandler, streamCustomizer.specializeForChannel(childChannel, NettyServerCustomizer.ChannelRole.PUSH_PROMISE_STREAM)); promisePipeline.insertHttp2FrameHandlers(); promisePipeline.streamCustomizer.onStreamPipelineBuilt(); } @@ -479,11 +479,11 @@ private void insertMicronautHandlers() { pipeline.addLast(ChannelPipelineCustomizer.HANDLER_HTTP_STREAM, new HttpStreamsServerHandler()); pipeline.addLast(ChannelPipelineCustomizer.HANDLER_HTTP_CHUNK, new ChunkedWriteHandler()); pipeline.addLast(HttpRequestDecoder.ID, requestDecoder); - if (server.getServerConfiguration().isDualProtocol() && server.getServerConfiguration().isHttpToHttpsRedirect() && !ssl) { + if (server.getServerConfiguration().isDualProtocol() && server.getServerConfiguration().isHttpToHttpsRedirect() && sslHandler == null) { pipeline.addLast(ChannelPipelineCustomizer.HANDLER_HTTP_TO_HTTPS_REDIRECT, new HttpToHttpsRedirectHandler(sslConfiguration, hostResolver)); } - if (ssl) { - pipeline.addLast("request-certificate-handler", requestCertificateHandler); + if (sslHandler != null) { + pipeline.addLast("request-certificate-handler", new HttpRequestCertificateHandler(sslHandler)); } pipeline.addLast(HttpResponseEncoder.ID, responseEncoder); embeddedServices.getWebSocketUpgradeHandler(server).ifPresent(websocketHandler -> diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/ssl/HttpRequestCertificateHandler.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/ssl/HttpRequestCertificateHandler.java index 7fe88e1e709..d8824ea55b7 100644 --- a/http-server-netty/src/main/java/io/micronaut/http/server/netty/ssl/HttpRequestCertificateHandler.java +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/ssl/HttpRequestCertificateHandler.java @@ -16,16 +16,15 @@ package io.micronaut.http.server.netty.ssl; import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.Nullable; import io.micronaut.http.HttpAttributes; import io.micronaut.http.HttpMessage; -import io.netty.channel.ChannelHandler; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelInboundHandlerAdapter; import io.netty.handler.ssl.SslHandler; import javax.net.ssl.SSLPeerUnverifiedException; import java.security.cert.Certificate; -import java.util.Optional; /** * Adds the certificate to the decoded request. @@ -34,35 +33,37 @@ * @author Björn Heinrichs * @since 1.3.0 */ -@ChannelHandler.Sharable @Internal public class HttpRequestCertificateHandler extends ChannelInboundHandlerAdapter { + private final SslHandler sslHandler; + private Certificate certificate; + + public HttpRequestCertificateHandler(SslHandler sslHandler) { + this.sslHandler = sslHandler; + } @Override public void channelRead(final ChannelHandlerContext ctx, final Object msg) throws Exception { - if (msg instanceof HttpMessage) { - HttpMessage request = (HttpMessage) msg; - Optional certificate = getCertificate(ctx.pipeline().get(SslHandler.class)); - - if (certificate.isPresent()) { - request.setAttribute(HttpAttributes.X509_CERTIFICATE, certificate.get()); - } else { - request.removeAttribute(HttpAttributes.X509_CERTIFICATE, Certificate.class); + if (msg instanceof HttpMessage http) { + if (certificate == null) { + certificate = getCertificate(sslHandler); + if (certificate == null) { + ctx.pipeline().remove(this); + super.channelRead(ctx, msg); + return; + } } + http.setAttribute(HttpAttributes.X509_CERTIFICATE, certificate); } super.channelRead(ctx, msg); } - private static Optional getCertificate(final SslHandler handler) { - if (handler == null) { - return Optional.empty(); - } + @Nullable + private static Certificate getCertificate(final SslHandler handler) { try { - return Optional.of( - handler.engine().getSession().getPeerCertificates()[0] - ); + return handler.engine().getSession().getPeerCertificates()[0]; } catch (SSLPeerUnverifiedException ex) { - return Optional.empty(); + return null; } } } From e76a17c96e9d647104e926aa096e01ab103dc424 Mon Sep 17 00:00:00 2001 From: micronaut-build <65172877+micronaut-build@users.noreply.github.com> Date: Tue, 14 Feb 2023 10:39:14 +0100 Subject: [PATCH 479/743] build: Bump micronaut-openapi to 4.8.4 (#8770) --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 2e62cbcb1d9..4a957974b95 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -100,7 +100,7 @@ managed-micronaut-neo4j = "5.2.0" managed-micronaut-nats = "3.1.0" managed-micronaut-netflix = "2.1.0" managed-micronaut-object-storage = "1.1.0" -managed-micronaut-openapi = "4.8.3" +managed-micronaut-openapi = "4.8.4" managed-micronaut-oraclecloud = "2.3.4" managed-micronaut-picocli = "4.3.0" managed-micronaut-problem = "2.6.0" From 2be0a44ddbfa82a4cbc1580f1635e7e205fdc96e Mon Sep 17 00:00:00 2001 From: Graeme Rocher Date: Tue, 14 Feb 2023 18:48:36 +0100 Subject: [PATCH 480/743] Move away from annotation API that now returns defaults (#8776) --- .../processor/ScheduledMethodProcessor.java | 14 +++++++------- .../core/annotation/AnnotationValue.java | 2 +- .../io/micronaut/context/RequiresCondition.java | 7 +++---- .../micronaut/jackson/codec/JacksonFeatures.java | 16 ++++++++-------- .../intercept/AnnotationRetryStateBuilder.java | 12 +++++++----- 5 files changed, 26 insertions(+), 25 deletions(-) diff --git a/context/src/main/java/io/micronaut/scheduling/processor/ScheduledMethodProcessor.java b/context/src/main/java/io/micronaut/scheduling/processor/ScheduledMethodProcessor.java index 63d7beb84b5..b488ed69cd9 100644 --- a/context/src/main/java/io/micronaut/scheduling/processor/ScheduledMethodProcessor.java +++ b/context/src/main/java/io/micronaut/scheduling/processor/ScheduledMethodProcessor.java @@ -91,9 +91,9 @@ public void process(BeanDefinition beanDefinition, ExecutableMethod met List> scheduledAnnotations = method.getAnnotationValuesByType(Scheduled.class); for (AnnotationValue scheduledAnnotation : scheduledAnnotations) { - String fixedRate = scheduledAnnotation.get(MEMBER_FIXED_RATE, String.class).orElse(null); + String fixedRate = scheduledAnnotation.stringValue(MEMBER_FIXED_RATE).orElse(null); - String initialDelayStr = scheduledAnnotation.get(MEMBER_INITIAL_DELAY, String.class).orElse(null); + String initialDelayStr = scheduledAnnotation.stringValue(MEMBER_INITIAL_DELAY).orElse(null); Duration initialDelay = null; if (StringUtils.hasText(initialDelayStr)) { initialDelay = conversionService.convert(initialDelayStr, Duration.class).orElseThrow(() -> @@ -101,11 +101,11 @@ public void process(BeanDefinition beanDefinition, ExecutableMethod met ); } - String scheduler = scheduledAnnotation.get(MEMBER_SCHEDULER, String.class).orElse(TaskExecutors.SCHEDULED); + String scheduler = scheduledAnnotation.stringValue(MEMBER_SCHEDULER).orElse(TaskExecutors.SCHEDULED); Optional optionalTaskScheduler = beanContext .findBean(TaskScheduler.class, Qualifiers.byName(scheduler)); - if (!optionalTaskScheduler.isPresent()) { + if (optionalTaskScheduler.isEmpty()) { optionalTaskScheduler = beanContext.findBean(ExecutorService.class, Qualifiers.byName(scheduler)) .filter(ScheduledExecutorService.class::isInstance) .map(ScheduledExecutorTaskScheduler::new); @@ -142,9 +142,9 @@ public void process(BeanDefinition beanDefinition, ExecutableMethod met } }; - String cronExpr = scheduledAnnotation.get(MEMBER_CRON, String.class, null); - String zoneIdStr = scheduledAnnotation.get(MEMBER_ZONE_ID, String.class, null); - String fixedDelay = scheduledAnnotation.get(MEMBER_FIXED_DELAY, String.class).orElse(null); + String cronExpr = scheduledAnnotation.stringValue(MEMBER_CRON).orElse(null); + String zoneIdStr = scheduledAnnotation.stringValue(MEMBER_ZONE_ID).orElse(null); + String fixedDelay = scheduledAnnotation.stringValue(MEMBER_FIXED_DELAY).orElse(null); if (StringUtils.isNotEmpty(cronExpr)) { if (LOG.isDebugEnabled()) { diff --git a/core/src/main/java/io/micronaut/core/annotation/AnnotationValue.java b/core/src/main/java/io/micronaut/core/annotation/AnnotationValue.java index d8c9d78db02..006a8b34239 100644 --- a/core/src/main/java/io/micronaut/core/annotation/AnnotationValue.java +++ b/core/src/main/java/io/micronaut/core/annotation/AnnotationValue.java @@ -1147,7 +1147,7 @@ public final T getRequiredValue(Class type) { * @throws IllegalStateException If no member is available that conforms to the given name and type */ @NonNull - public final T getRequiredValue(String member, Class type) { + public final T getRequiredValue(String member, Class type) { return get(member, ConversionContext.of(type)).orElseThrow(() -> new IllegalStateException("No value available for annotation member @" + annotationName + "[" + member + "] of type: " + type)); } diff --git a/inject/src/main/java/io/micronaut/context/RequiresCondition.java b/inject/src/main/java/io/micronaut/context/RequiresCondition.java index dba78ea752a..e8b62dbc8b1 100644 --- a/inject/src/main/java/io/micronaut/context/RequiresCondition.java +++ b/inject/src/main/java/io/micronaut/context/RequiresCondition.java @@ -528,13 +528,12 @@ private boolean matchesPresenceOfClasses(ConditionContext context, AnnotationVal private boolean matchesPresenceOfEntities(ConditionContext context, AnnotationValue annotationValue) { if (annotationValue.contains(MEMBER_ENTITIES)) { - Optional classNames = annotationValue.get(MEMBER_ENTITIES, AnnotationClassValue[].class); - if (classNames.isPresent()) { + AnnotationClassValue[] classNames = annotationValue.annotationClassValues(MEMBER_ENTITIES); + if (ArrayUtils.isNotEmpty(classNames)) { BeanContext beanContext = context.getBeanContext(); if (beanContext instanceof ApplicationContext) { ApplicationContext applicationContext = (ApplicationContext) beanContext; - final AnnotationClassValue[] classValues = classNames.get(); - for (AnnotationClassValue classValue : classValues) { + for (AnnotationClassValue classValue : classNames) { final Optional> entityType = classValue.getType(); if (entityType.isEmpty()) { context.fail("Annotation type [" + classValue.getName() + "] not present on classpath"); diff --git a/jackson-databind/src/main/java/io/micronaut/jackson/codec/JacksonFeatures.java b/jackson-databind/src/main/java/io/micronaut/jackson/codec/JacksonFeatures.java index 2726f30b293..e7971a75b6f 100644 --- a/jackson-databind/src/main/java/io/micronaut/jackson/codec/JacksonFeatures.java +++ b/jackson-databind/src/main/java/io/micronaut/jackson/codec/JacksonFeatures.java @@ -58,31 +58,31 @@ public static JacksonFeatures fromAnnotation(AnnotationValue retry = annotationMetadata.findAnnotation(Retryable.class) .orElseThrow(() -> new IllegalStateException("Missing @Retryable annotation")); - int attempts = retry.get(ATTEMPTS, Integer.class).orElse(DEFAULT_RETRY_ATTEMPTS); + int attempts = retry.intValue(ATTEMPTS).orElse(DEFAULT_RETRY_ATTEMPTS); Duration delay = retry.get(DELAY, Duration.class).orElse(Duration.ofSeconds(1)); - Class predicateClass = retry.get(PREDICATE, Class.class) - .orElse(DefaultRetryPredicate.class); + @SuppressWarnings("unchecked") + Class predicateClass = (Class) retry.classValue(PREDICATE).orElse(DefaultRetryPredicate.class); RetryPredicate predicate = createPredicate(predicateClass, retry); - Class capturedException = retry.get(CAPTUREDEXCEPTION, Class.class) - .orElse(RuntimeException.class); + @SuppressWarnings("unchecked") + Class capturedException = (Class) retry + .classValue(CAPTUREDEXCEPTION) + .orElse(RuntimeException.class); return new SimpleRetry( attempts, From 13f35b35c1cdccd1d7da0cc9ce2d02600daae3e9 Mon Sep 17 00:00:00 2001 From: Tim Yates Date: Wed, 15 Feb 2023 11:00:44 +0000 Subject: [PATCH 481/743] test: Deflake the BinaryWebSocketSpec (#8725) --- .../server/netty/websocket/BinaryChatClientWebSocket.java | 4 ++-- .../server/netty/websocket/BinaryChatServerWebSocket.java | 6 +++--- .../http/server/netty/websocket/BinaryWebSocketSpec.groovy | 7 ------- 3 files changed, 5 insertions(+), 12 deletions(-) diff --git a/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/websocket/BinaryChatClientWebSocket.java b/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/websocket/BinaryChatClientWebSocket.java index 533483b0142..4d2ecf914dc 100644 --- a/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/websocket/BinaryChatClientWebSocket.java +++ b/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/websocket/BinaryChatClientWebSocket.java @@ -46,7 +46,7 @@ public void onOpen(String topic, String username, WebSocketSession session) { this.topic = topic; this.username = username; this.session = session; - System.out.println("Client session opened for username = " + username); + System.out.println("Client session " + session.getId() + " opened for username = " + username); } public String getTopic() { @@ -72,7 +72,7 @@ public WebSocketSession getSession() { @OnMessage public void onMessage( byte[] message) { - System.out.println("Client received message = " + new String(message)); + System.out.println("Client " + username + " received message = " + new String(message)); replies.add(new String(message)); } diff --git a/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/websocket/BinaryChatServerWebSocket.java b/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/websocket/BinaryChatServerWebSocket.java index 49b581b7d64..141e417db33 100644 --- a/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/websocket/BinaryChatServerWebSocket.java +++ b/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/websocket/BinaryChatServerWebSocket.java @@ -36,7 +36,7 @@ public void onOpen(String topic, String username, WebSocketSession session) { if(isValid(topic, session, openSession)) { String msg = "[" + username + "] Joined!"; System.out.println("Server sending msg = " + msg); - openSession.sendSync(msg.getBytes()); + openSession.sendAsync(msg.getBytes()); } } } @@ -55,7 +55,7 @@ public void onMessage( if(isValid(topic, session, openSession)) { String msg = "[" + username + "] " + new String(message); System.out.println("Server sending msg = " + msg); - openSession.sendSync(msg.getBytes()); + openSession.sendAsync(msg.getBytes()); } } } @@ -72,7 +72,7 @@ public void onClose( if(isValid(topic, session, openSession)) { String msg = "[" + username + "] Disconnected!"; System.out.println("Server sending msg = " + msg); - openSession.sendSync(msg.getBytes()); + openSession.sendAsync(msg.getBytes()); } } } diff --git a/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/websocket/BinaryWebSocketSpec.groovy b/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/websocket/BinaryWebSocketSpec.groovy index a4bd3a39ca2..8c935de3c18 100644 --- a/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/websocket/BinaryWebSocketSpec.groovy +++ b/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/websocket/BinaryWebSocketSpec.groovy @@ -32,17 +32,14 @@ import jakarta.inject.Singleton import reactor.core.publisher.Flux import reactor.core.publisher.Mono import spock.lang.Issue -import spock.lang.Retry import spock.lang.Specification import spock.util.concurrent.PollingConditions import java.nio.ByteBuffer import java.nio.charset.StandardCharsets -@Retry class BinaryWebSocketSpec extends Specification { - @Retry void "test binary websocket exchange"() { given: EmbeddedServer embeddedServer = ApplicationContext.builder('micronaut.server.netty.log-level':'TRACE').run(EmbeddedServer) @@ -69,7 +66,6 @@ class BinaryWebSocketSpec extends Specification { fred.replies.size() == 1 } - when:"A message is sent" fred.send("Hello bob!".bytes) @@ -86,7 +82,6 @@ class BinaryWebSocketSpec extends Specification { then: conditions.eventually { - fred.replies.contains("[bob] Hi fred. How are things?") fred.replies.size() == 2 bob.replies.contains("[fred] Hello bob!") @@ -99,8 +94,6 @@ class BinaryWebSocketSpec extends Specification { when: bob.close() - sleep(1000) - then: conditions.eventually { From 9ef26ca039ff60fdf2793674c2e6e30731793b3f Mon Sep 17 00:00:00 2001 From: Graeme Rocher Date: Wed, 15 Feb 2023 16:34:18 +0100 Subject: [PATCH 482/743] fix primitive import with test (#8778) Primitive types in constructors of imported beans where handled improperly, an array of primitives in synthesized as a type with the name of the primitive (e.g. byte[] becomes Lbyte; instead of `[B;`). This is caused by a bug in JavaModelUtils.getTypeReference where the given type element is used instead of the actual class element. Fixes #8635 --------- Co-authored-by: Auke Schrijnen --- .../inject/beanimport/BeanImportSpec.groovy | 43 +++++++++++++++++++ .../inject/processing/JavaModelUtils.java | 14 +++--- 2 files changed, 50 insertions(+), 7 deletions(-) diff --git a/inject-java-test/src/test/groovy/io/micronaut/inject/beanimport/BeanImportSpec.groovy b/inject-java-test/src/test/groovy/io/micronaut/inject/beanimport/BeanImportSpec.groovy index c30f07e89dd..d89a6eb4ce7 100644 --- a/inject-java-test/src/test/groovy/io/micronaut/inject/beanimport/BeanImportSpec.groovy +++ b/inject-java-test/src/test/groovy/io/micronaut/inject/beanimport/BeanImportSpec.groovy @@ -9,9 +9,39 @@ import io.smallrye.faulttolerance.DefaultExistingCircuitBreakerNames import io.smallrye.faulttolerance.DefaultFallbackHandlerProvider import io.smallrye.faulttolerance.DefaultFaultToleranceOperationProvider import io.smallrye.faulttolerance.ExecutorHolder +import jakarta.inject.Named + +import java.nio.charset.StandardCharsets class BeanImportSpec extends AbstractTypeElementSpec { + void "test bean import with primitive array constructor"() { + given: + ApplicationContext context = buildContext(''' +package beanimporttest1; + +import io.micronaut.context.annotation.*; +import jakarta.inject.Named; +import java.nio.charset.StandardCharsets; + +@Import(classes=io.micronaut.inject.beanimport.UpstreamByteConstructorBean.class) +class Application {} + +@Factory +class BytesFactory { + @Bean + @Named("some-bytes") + byte[] myBytes() { + return "test".getBytes(StandardCharsets.UTF_8); + } +} +''') + def bean = context.getBean(UpstreamByteConstructorBean) + + expect: + bean.toString() == 'test' + } + void 'test bean import for package'() { given: ApplicationContext context = buildContext(''' @@ -84,3 +114,16 @@ class Application {} context.close() } } +class UpstreamByteConstructorBean { + + private final byte[] bytes; + + public UpstreamByteConstructorBean(@Named("some-bytes") byte[] bytes) { + this.bytes = bytes; + } + + @Override + public String toString() { + return new String(bytes, StandardCharsets.UTF_8); + } +} diff --git a/inject/src/main/java/io/micronaut/inject/processing/JavaModelUtils.java b/inject/src/main/java/io/micronaut/inject/processing/JavaModelUtils.java index 9e84d40faff..dc23bc7c6b4 100644 --- a/inject/src/main/java/io/micronaut/inject/processing/JavaModelUtils.java +++ b/inject/src/main/java/io/micronaut/inject/processing/JavaModelUtils.java @@ -267,11 +267,11 @@ public static boolean isRecordComponent(Element e) { */ public static Type getTypeReference(TypedElement type) { ClassElement classElement = type.getType(); - if (type.isPrimitive()) { + if (classElement.isPrimitive()) { String internalName = NAME_TO_TYPE_MAP.get(classElement.getName()); - if (type.isArray()) { + if (classElement.isArray()) { StringBuilder name = new StringBuilder(internalName); - for (int i = 0; i < type.getArrayDimensions(); i++) { + for (int i = 0; i < classElement.getArrayDimensions(); i++) { name.insert(0, "["); } return Type.getObjectType(name.toString()); @@ -279,19 +279,19 @@ public static Type getTypeReference(TypedElement type) { return Type.getType(internalName); } } else { - Object nativeType = type.getNativeType(); + Object nativeType = classElement.getNativeType(); if (nativeType instanceof Class) { Class t = (Class) nativeType; return Type.getType(t); } else { - String internalName = type.getType().getName().replace('.', '/'); + String internalName = classElement.getName().replace('.', '/'); if (internalName.isEmpty()) { return Type.getType(Object.class); } - if (type.isArray()) { + if (classElement.isArray()) { StringBuilder name = new StringBuilder(internalName); name.insert(0, "L"); - for (int i = 0; i < type.getArrayDimensions(); i++) { + for (int i = 0; i < classElement.getArrayDimensions(); i++) { name.insert(0, "["); } name.append(";"); From 74e32c1174d4512c79ea144931af61e8015d8fdc Mon Sep 17 00:00:00 2001 From: Denis Stepanov Date: Wed, 15 Feb 2023 17:25:13 +0100 Subject: [PATCH 483/743] Support type annotations for Java & Groovy (#8764) Introduced a way to access type annotations on ClassElement#getTypeAnnotationMetadata. To do that the native type has to carry the actual type mirror (for Java). If the type mirror is present that indicates that ClassElement supports type annotations and the actual annotation metadata of the class is a hierarchy of both. Modifying annotation metadata of such class will modify the type annotations, modifying the actual class annotation can be done on the #getType class element. The type annotations are now the ones that are persisted for an executable method's return type, parameters, and type parameters. There is no need for hacks to persist generic type annotations in some cases. There is some modification to how the generic placeholders are preserved bound to make sure everything works correctly. Added a lot of tests to test annotating methods, return types, parameters, fields, and field types. --- .../AbstractAnnotationMetadataBuilder.java | 500 +++++++------ .../io/micronaut/inject/ast/ClassElement.java | 40 ++ .../micronaut/inject/ast/GenericElement.java | 15 + .../inject/ast/GenericPlaceholderElement.java | 20 + .../AbstractElementAnnotationMetadata.java | 31 + ...tractElementAnnotationMetadataFactory.java | 216 +++++- .../AbstractMutableAnnotationMetadata.java | 121 ++++ .../ElementAnnotationMetadataFactory.java | 20 + .../MutableAnnotationMetadataDelegate.java | 6 + .../ast/utils/AstBeanPropertiesUtils.java | 9 +- .../ast/utils/EnclosedElementsQuery.java | 43 +- .../processing/FactoryBeanElementCreator.java | 37 +- .../inject/visitor/VisitorConfiguration.java | 16 - .../writer/AbstractClassFileWriter.java | 91 ++- .../test/AbstractBeanDefinitionSpec.groovy | 13 +- .../ast/groovy/InjectTransform.groovy | 13 +- .../groovy/TypeElementVisitorTransform.groovy | 2 +- .../GroovyAnnotationMetadataBuilder.java | 7 +- ...roovyElementAnnotationMetadataFactory.java | 48 +- .../groovy/visitor/AbstractGroovyElement.java | 173 +++-- .../visitor/GroovyAnnotationElement.java | 2 + .../visitor/GroovyBeanDefinitionBuilder.java | 8 +- .../groovy/visitor/GroovyClassElement.java | 57 +- .../GroovyClassWriterOutputVisitor.java | 14 +- .../visitor/GroovyConstructorElement.java | 2 + .../groovy/visitor/GroovyElementFactory.java | 2 + .../ast/groovy/visitor/GroovyEnumElement.java | 2 + .../groovy/visitor/GroovyFieldElement.java | 2 + .../GroovyGenericPlaceholderElement.java | 110 ++- .../groovy/visitor/GroovyMethodElement.java | 14 +- .../groovy/visitor/GroovyNativeElement.java | 2 + .../groovy/visitor/GroovyPropertyElement.java | 9 +- .../groovy/visitor/GroovyVisitorContext.java | 7 +- .../groovy/visitor/GroovyWildcardElement.java | 75 +- .../visitor/GroovyReconstructionSpec.groovy | 122 +++- .../modify/AnnotateFieldSpec.groovy | 95 +++ .../modify/AnnotateFieldTypeSpec.groovy | 210 ++++++ .../modify/AnnotateMethodParameterSpec.groovy | 270 +++++++ .../modify/AnnotateMethodReturnSpec.groovy | 270 +++++++ .../modify/AnnotateMethodSpec.groovy | 96 +++ .../annotation/modify/MyAnnotation.java | 28 + .../inject/beans/BeanDefinitionSpec.groovy | 89 ++- .../io/micronaut/inject/executable/Book.java | 8 + .../executable/ExecutableBeanSpec.groovy | 343 +++++++++ .../micronaut/inject/executable/MyEntity.java | 15 + .../inject/executable/TypeUseRuntimeAnn.java | 11 + .../visitor/BeanIntrospectionSpec.groovy | 48 ++ .../inject/visitor/ClassElementSpec.groovy | 255 +++++++ .../inject/visitor/TypeUseRuntimeAnn.java | 2 +- .../test/AbstractTypeElementSpec.groovy | 13 +- .../beans/BeanIntrospectionSpec.groovy | 88 +++ .../inject/visitor/beans/TypeUseClassAnn.java | 11 + .../visitor/beans/TypeUseRuntimeAnn.java | 11 + inject-java/build.gradle | 2 +- .../AnnotationProcessingOutputVisitor.java | 9 +- .../processing/AnnotationsElement.java | 115 +++ .../BeanDefinitionInjectProcessor.java | 17 +- .../JavaElementAnnotationMetadataFactory.java | 51 +- .../TypeElementVisitorProcessor.java | 20 +- .../visitor/AbstractJavaElement.java | 254 ++++--- .../visitor/JavaAnnotationElement.java | 13 +- .../visitor/JavaBeanDefinitionBuilder.java | 2 + .../processing/visitor/JavaClassElement.java | 314 +++++---- .../visitor/JavaConstructorElement.java | 20 +- .../visitor/JavaElementFactory.java | 34 +- .../visitor/JavaEnumConstantElement.java | 19 +- .../processing/visitor/JavaEnumElement.java | 20 +- .../processing/visitor/JavaFieldElement.java | 52 +- .../JavaGenericPlaceholderElement.java | 135 +++- .../processing/visitor/JavaMethodElement.java | 50 +- .../processing/visitor/JavaNativeElement.java | 94 +++ .../visitor/JavaPackageElement.java | 2 +- .../visitor/JavaParameterElement.java | 25 +- .../visitor/JavaPropertyElement.java | 72 +- .../visitor/JavaVisitorContext.java | 6 +- .../visitor/JavaWildcardElement.java | 75 +- .../annotation/AnnotateFieldSpec.groovy | 84 +++ .../annotation/AnnotateFieldTypeSpec.groovy | 254 +++++++ .../AnnotateMethodParameterSpec.groovy | 259 +++++++ .../AnnotateMethodReturnSpec.groovy | 324 +++++++++ .../annotation/AnnotateMethodSpec.groovy | 85 +++ .../io/micronaut/annotation/MyAnnotation.java | 28 + .../visitor/JavaReconstructionSpec.groovy | 119 +++- .../aop/named2/NamedAopAdviceSpec.groovy | 103 +++ .../inject/beans/BeanDefinitionSpec.groovy | 80 +++ .../executable/ExecutableBeanSpec.groovy | 350 ++++++++-- .../micronaut/inject/executable/MyBean.java | 35 + .../inject/executable/TypeUseRuntimeAnn.java | 11 + .../visitors/AllElementsVisitor.java | 9 +- .../visitors/ClassElementSpec.groovy | 657 ++++++++++++++++++ .../groovy/io/micronaut/visitors/MyBean.java | 39 ++ .../io/micronaut/visitors/MyParameter.java | 20 + .../visitors/TypeFieldRuntimeAnn.java | 11 + .../visitors/TypeMethodRuntimeAnn.java | 11 + .../visitors/TypeParameterRuntimeAnn.java | 11 + .../micronaut/visitors/TypeUseClassAnn.java | 11 + .../micronaut/visitors/TypeUseRuntimeAnn.java | 11 + ...icronaut.inject.visitor.TypeElementVisitor | 5 + inject-kotlin/build.gradle | 2 +- .../beans/BeanDefinitionProcessor.kt | 10 +- .../beans/BeanDefinitionSpec.groovy | 10 +- .../AbstractExecutableMethodsDefinition.java | 185 +++-- .../AnnotationMetadataHierarchy.java | 9 +- 103 files changed, 6696 insertions(+), 1150 deletions(-) create mode 100644 core-processor/src/main/java/io/micronaut/inject/ast/annotation/AbstractElementAnnotationMetadata.java create mode 100644 core-processor/src/main/java/io/micronaut/inject/ast/annotation/AbstractMutableAnnotationMetadata.java create mode 100644 inject-groovy/src/test/groovy/io/micronaut/inject/annotation/modify/AnnotateFieldSpec.groovy create mode 100644 inject-groovy/src/test/groovy/io/micronaut/inject/annotation/modify/AnnotateFieldTypeSpec.groovy create mode 100644 inject-groovy/src/test/groovy/io/micronaut/inject/annotation/modify/AnnotateMethodParameterSpec.groovy create mode 100644 inject-groovy/src/test/groovy/io/micronaut/inject/annotation/modify/AnnotateMethodReturnSpec.groovy create mode 100644 inject-groovy/src/test/groovy/io/micronaut/inject/annotation/modify/AnnotateMethodSpec.groovy create mode 100644 inject-groovy/src/test/groovy/io/micronaut/inject/annotation/modify/MyAnnotation.java create mode 100644 inject-groovy/src/test/groovy/io/micronaut/inject/executable/Book.java create mode 100644 inject-groovy/src/test/groovy/io/micronaut/inject/executable/MyEntity.java create mode 100644 inject-groovy/src/test/groovy/io/micronaut/inject/executable/TypeUseRuntimeAnn.java create mode 100644 inject-java-test/src/test/groovy/io/micronaut/inject/visitor/beans/TypeUseClassAnn.java create mode 100644 inject-java-test/src/test/groovy/io/micronaut/inject/visitor/beans/TypeUseRuntimeAnn.java create mode 100644 inject-java/src/main/java/io/micronaut/annotation/processing/AnnotationsElement.java create mode 100644 inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaNativeElement.java create mode 100644 inject-java/src/test/groovy/io/micronaut/annotation/AnnotateFieldSpec.groovy create mode 100644 inject-java/src/test/groovy/io/micronaut/annotation/AnnotateFieldTypeSpec.groovy create mode 100644 inject-java/src/test/groovy/io/micronaut/annotation/AnnotateMethodParameterSpec.groovy create mode 100644 inject-java/src/test/groovy/io/micronaut/annotation/AnnotateMethodReturnSpec.groovy create mode 100644 inject-java/src/test/groovy/io/micronaut/annotation/AnnotateMethodSpec.groovy create mode 100644 inject-java/src/test/groovy/io/micronaut/annotation/MyAnnotation.java create mode 100644 inject-java/src/test/groovy/io/micronaut/aop/named2/NamedAopAdviceSpec.groovy create mode 100644 inject-java/src/test/groovy/io/micronaut/inject/executable/MyBean.java create mode 100644 inject-java/src/test/groovy/io/micronaut/inject/executable/TypeUseRuntimeAnn.java create mode 100644 inject-java/src/test/groovy/io/micronaut/visitors/MyBean.java create mode 100644 inject-java/src/test/groovy/io/micronaut/visitors/MyParameter.java create mode 100644 inject-java/src/test/groovy/io/micronaut/visitors/TypeFieldRuntimeAnn.java create mode 100644 inject-java/src/test/groovy/io/micronaut/visitors/TypeMethodRuntimeAnn.java create mode 100644 inject-java/src/test/groovy/io/micronaut/visitors/TypeParameterRuntimeAnn.java create mode 100644 inject-java/src/test/groovy/io/micronaut/visitors/TypeUseClassAnn.java create mode 100644 inject-java/src/test/groovy/io/micronaut/visitors/TypeUseRuntimeAnn.java diff --git a/core-processor/src/main/java/io/micronaut/inject/annotation/AbstractAnnotationMetadataBuilder.java b/core-processor/src/main/java/io/micronaut/inject/annotation/AbstractAnnotationMetadataBuilder.java index 477ed0f7366..5744d000586 100644 --- a/core-processor/src/main/java/io/micronaut/inject/annotation/AbstractAnnotationMetadataBuilder.java +++ b/core-processor/src/main/java/io/micronaut/inject/annotation/AbstractAnnotationMetadataBuilder.java @@ -43,7 +43,6 @@ import java.lang.annotation.Annotation; import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; @@ -56,7 +55,6 @@ import java.util.Set; import java.util.function.Consumer; import java.util.function.Predicate; -import java.util.function.Supplier; import java.util.stream.Stream; /** @@ -84,7 +82,7 @@ public abstract class AbstractAnnotationMetadataBuilder { static { for (AnnotationMapper mapper : SoftServiceLoader.load(AnnotationMapper.class, AbstractAnnotationMetadataBuilder.class.getClassLoader()) - .disableFork().collectAll()) { + .disableFork().collectAll()) { try { String name = null; if (mapper instanceof TypedAnnotationMapper typedAnnotationMapper) { @@ -101,7 +99,7 @@ public abstract class AbstractAnnotationMetadataBuilder { } for (AnnotationTransformer transformer : SoftServiceLoader.load(AnnotationTransformer.class, AbstractAnnotationMetadataBuilder.class.getClassLoader()) - .disableFork().collectAll()) { + .disableFork().collectAll()) { try { String name = null; if (transformer instanceof TypedAnnotationTransformer typedAnnotationTransformer) { @@ -118,7 +116,7 @@ public abstract class AbstractAnnotationMetadataBuilder { } for (AnnotationRemapper mapper : SoftServiceLoader.load(AnnotationRemapper.class, AbstractAnnotationMetadataBuilder.class.getClassLoader()) - .disableFork().collectAll()) { + .disableFork().collectAll()) { try { String name = mapper.getPackageName(); if (StringUtils.isNotEmpty(name)) { @@ -160,9 +158,9 @@ public AnnotationMetadata buildDeclared(T element) { MutableAnnotationMetadata annotationMetadata = new MutableAnnotationMetadata(); try { AnnotationMetadata metadata = buildInternalMulti( - Collections.emptyList(), - element, - annotationMetadata, true, true + Collections.emptyList(), + element, + annotationMetadata, true, true ); if (metadata.isEmpty()) { return AnnotationMetadata.EMPTY_METADATA; @@ -188,14 +186,14 @@ public AnnotationMetadata buildDeclared(T element, List annotations MutableAnnotationMetadata annotationMetadata = new MutableAnnotationMetadata(); if (includeTypeAnnotations) { buildInternalMulti( - Collections.emptyList(), - element, - annotationMetadata, false, true + Collections.emptyList(), + element, + annotationMetadata, false, true ); } try { addAnnotations(annotationMetadata, element, false, true, - annotations, Collections.emptyList()); + annotations, Collections.emptyList()); if (annotationMetadata.isEmpty()) { return AnnotationMetadata.EMPTY_METADATA; } @@ -214,7 +212,7 @@ public AnnotationMetadata buildDeclared(T element, List annotations * @return The {@link AnnotationMetadata} */ public CachedAnnotationMetadata lookupOrBuildForParameter(T owningType, T methodElement, T parameterElement) { - return lookupOrBuild(false, owningType, methodElement, parameterElement); + return lookupOrBuild(new Key3<>(owningType, methodElement, parameterElement), parameterElement); } /** @@ -224,7 +222,7 @@ public CachedAnnotationMetadata lookupOrBuildForParameter(T owningType, T method * @return The {@link AnnotationMetadata} */ public CachedAnnotationMetadata lookupOrBuildForType(T typeElement) { - return lookupOrBuild(true, typeElement); + return lookupOrBuild(typeElement, typeElement); } /** @@ -235,7 +233,7 @@ public CachedAnnotationMetadata lookupOrBuildForType(T typeElement) { * @return The {@link CachedAnnotationMetadata} */ public CachedAnnotationMetadata lookupOrBuildForMethod(T owningType, T element) { - return lookupOrBuild(false, owningType, element); + return lookupOrBuild(new Key2<>(owningType, element), element); } /** @@ -246,14 +244,19 @@ public CachedAnnotationMetadata lookupOrBuildForMethod(T owningType, T element) * @return The {@link CachedAnnotationMetadata} */ public CachedAnnotationMetadata lookupOrBuildForField(T owningType, T element) { - return lookupOrBuild(false, owningType, element); + return lookupOrBuild(new Key2<>(owningType, element), element); } - private CachedAnnotationMetadata lookupOrBuild(boolean inheritTypeAnnotations, T... elements) { - return lookupExisting(elements, () -> { - T element = elements[elements.length - 1]; - return buildInternal(inheritTypeAnnotations, false, element); - }); + /** + * Lookup or build new annotation metadata. + * + * @param key The cache key + * @param element The type element + * @return The annotation metadata + * @since 4.0.0 + */ + public CachedAnnotationMetadata lookupOrBuild(Object key, T element) { + return lookupOrBuild(key, element, false); } /** @@ -263,26 +266,26 @@ private CachedAnnotationMetadata lookupOrBuild(boolean inheritTypeAnnotations, T * @param element The type element * @param includeTypeAnnotations Whether to include type level annotations in the metadata for the element * @return The annotation metadata + * @since 4.0.0 */ public CachedAnnotationMetadata lookupOrBuild(Object key, T element, boolean includeTypeAnnotations) { CachedAnnotationMetadata cachedAnnotationMetadata = MUTATED_ANNOTATION_METADATA.get(key); if (cachedAnnotationMetadata == null) { AnnotationMetadata annotationMetadata = buildInternal(includeTypeAnnotations, false, element); cachedAnnotationMetadata = new DefaultCachedAnnotationMetadata(annotationMetadata); + // Don't use `computeIfAbsent` as it can lead to a concurrent exception because the cache is accessed during in `buildInternal` + MUTATED_ANNOTATION_METADATA.put(key, cachedAnnotationMetadata); } - // Don't use `computeIfAbsent` as it can lead to a concurrent exception because the cache is accessed during in `buildInternal` - MUTATED_ANNOTATION_METADATA.put(key, cachedAnnotationMetadata); return cachedAnnotationMetadata; - } private AnnotationMetadata buildInternal(boolean inheritTypeAnnotations, boolean declaredOnly, T element) { MutableAnnotationMetadata annotationMetadata = new MutableAnnotationMetadata(); try { return buildInternalMulti( - Collections.emptyList(), - element, - annotationMetadata, inheritTypeAnnotations, declaredOnly + Collections.emptyList(), + element, + annotationMetadata, inheritTypeAnnotations, declaredOnly ); } catch (RuntimeException e) { return metadataForError(e); @@ -373,12 +376,12 @@ private AnnotationMetadata buildInternal(boolean inheritTypeAnnotations, boolean * @param annotationValues The values to populate */ protected abstract void readAnnotationRawValues( - T originatingElement, - String annotationName, - T member, - String memberName, - Object annotationValue, - Map annotationValues); + T originatingElement, + String annotationName, + T member, + String memberName, + Object annotationValue, + Map annotationValues); /** * Validates an annotation value. @@ -401,7 +404,7 @@ protected void validateAnnotationValue(T originatingElement, final AnnotatedElementValidator elementValidator = getElementValidator(); if (elementValidator != null && !erroneousElements.contains(member)) { boolean shouldValidate = !(annotationName.equals(AliasFor.class.getName())) && - (!(resolvedValue instanceof String) || !resolvedValue.toString().contains("${")); + (!(resolvedValue instanceof String) || !resolvedValue.toString().contains("${")); if (shouldValidate) { shouldValidate = isValidationRequired(member); } @@ -562,20 +565,20 @@ protected AnnotationValue readNestedAnnotationValue(T annotationElement, A an if (aliasMember.isPresent() && !(aliasAnnotation.isPresent() || aliasAnnotationName.isPresent())) { String aliasedNamed = aliasMember.get(); readAnnotationRawValues(annotationElement, - annotationTypeName, - member, - aliasedNamed, - annotationValue, - resolvedValues); + annotationTypeName, + member, + aliasedNamed, + annotationValue, + resolvedValues); } } String memberName = getAnnotationMemberName(member); readAnnotationRawValues(annotationElement, - annotationTypeName, - member, - memberName, - annotationValue, - resolvedValues); + annotationTypeName, + member, + memberName, + annotationValue, + resolvedValues); } return new AnnotationValue<>(annotationTypeName, resolvedValues); } @@ -642,28 +645,16 @@ private Map getAnnotationDefaults(T originatingElement, if (!defaultValues.containsKey(memberName)) { Object annotationValue = entry.getValue(); readAnnotationRawValues(originatingElement, - annotationName, - member, - memberName, - annotationValue, - defaultValues); + annotationName, + member, + memberName, + annotationValue, + defaultValues); } } return defaultValues; } - @NonNull - private CachedAnnotationMetadata lookupExisting(T[] elements, Supplier annotationMetadataSupplier) { - Object key = new MetadataKey<>(elements); - CachedAnnotationMetadata cachedAnnotationMetadata = MUTATED_ANNOTATION_METADATA.get(key); - if (cachedAnnotationMetadata == null) { - cachedAnnotationMetadata = new DefaultCachedAnnotationMetadata(annotationMetadataSupplier.get()); - } - // Don't use `computeIfAbsent` as it can lead to a concurrent exception because the cache is accessed during in `buildInternal` - MUTATED_ANNOTATION_METADATA.put(key, cachedAnnotationMetadata); - return cachedAnnotationMetadata; - } - @Nullable private void processAnnotationAlias(Map annotationValues, Object annotationValue, @@ -680,11 +671,11 @@ private void processAnnotationAlias(Map annotationValues, String aliasedMemberName = aliasMember.get(); if (annotationValue != null) { introducedAnnotations.add( - toProcessedAnnotation( - AnnotationValue.builder(aliasedAnnotation, getRetentionPolicy(aliasedAnnotation)) - .members(Collections.singletonMap(aliasedMemberName, annotationValue)) - .build() - ) + toProcessedAnnotation( + AnnotationValue.builder(aliasedAnnotation, getRetentionPolicy(aliasedAnnotation)) + .members(Collections.singletonMap(aliasedMemberName, annotationValue)) + .build() + ) ); } } @@ -717,11 +708,11 @@ public RetentionPolicy getRetentionPolicy(@NonNull String annotation) { } private AnnotationMetadata buildInternalMulti( - List parents, - T element, - MutableAnnotationMetadata annotationMetadata, - boolean inheritTypeAnnotations, - boolean declaredOnly) { + List parents, + T element, + MutableAnnotationMetadata annotationMetadata, + boolean inheritTypeAnnotations, + boolean declaredOnly) { List hierarchy = buildHierarchy(element, inheritTypeAnnotations, declaredOnly); for (T parent : parents) { final List parentHierarchy = buildHierarchy(parent, inheritTypeAnnotations, declaredOnly); @@ -745,17 +736,17 @@ private AnnotationMetadata buildInternalMulti( boolean originatingElementIsSameParent = parents.contains(currentElement); boolean isDeclared = currentElement == element; addAnnotations( - annotationMetadata, - currentElement, - originatingElementIsSameParent, - isDeclared, - annotationHierarchy, - Collections.emptyList() + annotationMetadata, + currentElement, + originatingElementIsSameParent, + isDeclared, + annotationHierarchy, + Collections.emptyList() ); } if (!annotationMetadata.hasDeclaredStereotype(AnnotationUtil.SCOPE) && annotationMetadata.hasDeclaredStereotype( - DefaultScope.class)) { + DefaultScope.class)) { Optional value = annotationMetadata.stringValue(DefaultScope.class); value.ifPresent(name -> annotationMetadata.addDeclaredAnnotation(name, Collections.emptyMap())); } @@ -776,7 +767,7 @@ private void addAnnotations(MutableAnnotationMetadata annotationMetadata, List parentAnnotations) { Stream stream = annotationHierarchy.stream(); Stream annotationValues = annotationMirrorToAnnotationValue(stream, - element, originatingElementIsSameParent, annotationMetadata, isDeclared, false); + element, originatingElementIsSameParent, annotationMetadata, isDeclared, false); addAnnotations(annotationMetadata, annotationValues, isDeclared, parentAnnotations); } @@ -788,19 +779,19 @@ private Stream annotationMirrorToAnnotationValue(Stream { - String annotationName = getAnnotationTypeName(annotationMirror); - if (AnnotationUtil.INTERNAL_ANNOTATION_NAMES.contains(annotationName) - || isExcludedAnnotation(element, annotationName)) { - return false; - } - if (DEPRECATED_ANNOTATION_NAMES.containsKey(annotationName)) { - addWarning(element, - "Usages of deprecated annotation " + annotationName + " found. You should use " + DEPRECATED_ANNOTATION_NAMES.get( - annotationName) + " instead."); - } - return isStereotype || isDeclared || isInheritedAnnotation(annotationMirror) || originatingElementIsSameParent; - }).map(annotationMirror -> createAnnotationValue(element, annotationMirror, annotationMetadata)); + .filter(annotationMirror -> { + String annotationName = getAnnotationTypeName(annotationMirror); + if (AnnotationUtil.INTERNAL_ANNOTATION_NAMES.contains(annotationName) + || isExcludedAnnotation(element, annotationName)) { + return false; + } + if (DEPRECATED_ANNOTATION_NAMES.containsKey(annotationName)) { + addWarning(element, + "Usages of deprecated annotation " + annotationName + " found. You should use " + DEPRECATED_ANNOTATION_NAMES.get( + annotationName) + " instead."); + } + return isStereotype || isDeclared || isInheritedAnnotation(annotationMirror) || originatingElementIsSameParent; + }).map(annotationMirror -> createAnnotationValue(element, annotationMirror, annotationMetadata)); } private ProcessedAnnotation createAnnotationValue(T originatingElement, @@ -828,11 +819,11 @@ private ProcessedAnnotation createAnnotationValue(T originatingElement, if (hasAnnotations(member)) { final MutableAnnotationMetadata memberMetadata = new MutableAnnotationMetadata(); final List annotationsForMember = getAnnotationsForType(member) - .stream().filter((a) -> !getAnnotationTypeName(a).equals(annotationName)) - .toList(); + .stream().filter((a) -> !getAnnotationTypeName(a).equals(annotationName)) + .toList(); addAnnotations(memberMetadata, member, false, - true, annotationsForMember, Collections.emptyList()); + true, annotationsForMember, Collections.emptyList()); boolean isInstantiatedMember = memberMetadata.hasAnnotation(InstantiatedMember.class); @@ -850,20 +841,20 @@ private ProcessedAnnotation createAnnotationValue(T originatingElement, } readAnnotationRawValues(originatingElement, - annotationName, - member, - getAnnotationMemberName(member), - annotationValue, - annotationValues); + annotationName, + member, + getAnnotationMemberName(member), + annotationValue, + annotationValues); } if (!nonBindingMembers.isEmpty()) { if (hasAnnotation(annotationType, AnnotationUtil.QUALIFIER) || hasAnnotation(annotationType, Qualifier.class)) { metadata.addDeclaredStereotype( - Collections.singletonList(getAnnotationTypeName(annotationMirror)), - AnnotationUtil.QUALIFIER, - Collections.singletonMap("nonBinding", nonBindingMembers) + Collections.singletonList(getAnnotationTypeName(annotationMirror)), + AnnotationUtil.QUALIFIER, + Collections.singletonMap("nonBinding", nonBindingMembers) ); } } @@ -872,8 +863,8 @@ private ProcessedAnnotation createAnnotationValue(T originatingElement, Map defaultValues = getCachedAnnotationDefaults(annotationName, annotationType); return new ProcessedAnnotation( - annotationType, - new AnnotationValue<>(annotationName, annotationValues, defaultValues, retentionPolicy) + annotationType, + new AnnotationValue<>(annotationName, annotationValues, defaultValues, retentionPolicy) ); } @@ -905,20 +896,20 @@ private void handleAnnotationAlias(T originatingElement, if (aliases.isPresent()) { for (AnnotationValue av : aliases.get().getAnnotations(AnnotationMetadata.VALUE_MEMBER)) { processAnnotationAlias( - annotationValues, - annotationValue, - av, - introducedAnnotations + annotationValues, + annotationValue, + av, + introducedAnnotations ); } } else { Optional> aliasForValues = getAnnotationValues(originatingElement, annotationMember, AliasFor.class); if (aliasForValues.isPresent()) { processAnnotationAlias( - annotationValues, - annotationValue, - aliasForValues.get(), - introducedAnnotations + annotationValues, + annotationValue, + aliasForValues.get(), + introducedAnnotations ); } } @@ -939,11 +930,11 @@ private void addAnnotations(MutableAnnotationMetadata annotationMetadata, if (CollectionUtils.isNotEmpty(introducedAliasForAnnotations)) { // Add annotation created by @AliasFor addStereotypeAnnotations( - introducedAliasForAnnotations.stream(), - null, - parentAnnotations, - annotationMetadata, - isDeclared + introducedAliasForAnnotations.stream(), + null, + parentAnnotations, + annotationMetadata, + isDeclared ); } @@ -959,36 +950,36 @@ private Stream processInterceptors(Stream> interceptorBindings) { return annotationValues - .map(processedAnnotation -> { - AnnotationValue annotationValue = processedAnnotation.getAnnotationValue(); - String annotationName = annotationValue.getAnnotationName(); + .map(processedAnnotation -> { + AnnotationValue annotationValue = processedAnnotation.getAnnotationValue(); + String annotationName = annotationValue.getAnnotationName(); - addToInterceptorBindingsIfNecessary(interceptorBindings, lastParent, annotationName); + addToInterceptorBindingsIfNecessary(interceptorBindings, lastParent, annotationName); - final boolean hasInterceptorBinding = !interceptorBindings.isEmpty(); - if (hasInterceptorBinding && AnnotationUtil.ANN_INTERCEPTOR_BINDING.equals(annotationName)) { - annotationValue = handleMemberBinding(annotationMetadata, lastParent, annotationValue); - interceptorBindings.getLast().members(annotationValue.getValues()); - return processedAnnotation.withAnnotationValue(annotationValue); - } - if (hasInterceptorBinding && Type.class.getName().equals(annotationName)) { - final Object o = annotationValue.getValues().get(AnnotationMetadata.VALUE_MEMBER); - AnnotationClassValue interceptorType = null; - if (o instanceof AnnotationClassValue annotationClassValue) { - interceptorType = annotationClassValue; - } else if (o instanceof AnnotationClassValue[] annotationClassValues) { - if (annotationClassValues.length > 0) { - interceptorType = annotationClassValues[0]; - } + final boolean hasInterceptorBinding = !interceptorBindings.isEmpty(); + if (hasInterceptorBinding && AnnotationUtil.ANN_INTERCEPTOR_BINDING.equals(annotationName)) { + annotationValue = handleMemberBinding(annotationMetadata, lastParent, annotationValue); + interceptorBindings.getLast().members(annotationValue.getValues()); + return processedAnnotation.withAnnotationValue(annotationValue); + } + if (hasInterceptorBinding && Type.class.getName().equals(annotationName)) { + final Object o = annotationValue.getValues().get(AnnotationMetadata.VALUE_MEMBER); + AnnotationClassValue interceptorType = null; + if (o instanceof AnnotationClassValue annotationClassValue) { + interceptorType = annotationClassValue; + } else if (o instanceof AnnotationClassValue[] annotationClassValues) { + if (annotationClassValues.length > 0) { + interceptorType = annotationClassValues[0]; } - if (interceptorType != null) { - for (AnnotationValueBuilder interceptorBinding : interceptorBindings) { - interceptorBinding.member("interceptorType", interceptorType); - } + } + if (interceptorType != null) { + for (AnnotationValueBuilder interceptorBinding : interceptorBindings) { + interceptorBinding.member("interceptorType", interceptorType); } } - return processedAnnotation; - }); + } + return processedAnnotation; + }); } private Stream addAnnotations(Stream annotationValues, @@ -997,21 +988,21 @@ private Stream addAnnotations(Stream a boolean isStereotype, List parentAnnotations) { return annotationValues - .peek(processedAnnotation -> { - addAnnotationDefaults(annotationMetadata, processedAnnotation); - addAnnotation(annotationMetadata, parentAnnotations, isDeclared, isStereotype, processedAnnotation); - }); + .peek(processedAnnotation -> { + addAnnotationDefaults(annotationMetadata, processedAnnotation); + addAnnotation(annotationMetadata, parentAnnotations, isDeclared, isStereotype, processedAnnotation); + }); } private Stream filterAndTransformAnnotations(Stream annotationValues, List parentAnnotations) { return annotationValues - .filter(processedAnnotation -> { - AnnotationValue annotationValue = processedAnnotation.getAnnotationValue(); - return !AnnotationUtil.INTERNAL_ANNOTATION_NAMES.contains(annotationValue.getAnnotationName()) - && !parentAnnotations.contains(annotationValue.getAnnotationName()); - }) - .flatMap(this::transform) - .flatMap(this::flattenRepeatable); + .filter(processedAnnotation -> { + AnnotationValue annotationValue = processedAnnotation.getAnnotationValue(); + return !AnnotationUtil.INTERNAL_ANNOTATION_NAMES.contains(annotationValue.getAnnotationName()) + && !parentAnnotations.contains(annotationValue.getAnnotationName()); + }) + .flatMap(this::transform) + .flatMap(this::flattenRepeatable); } private Stream transform(ProcessedAnnotation toTransform) { @@ -1027,8 +1018,8 @@ private Stream transform(ProcessedAnnotation toTransform) { private Stream transform(ProcessedAnnotation toTransform, Set> processedVisitors) { return processAnnotationMappers(toTransform, processedVisitors) - .flatMap(annotation -> processAnnotationRemappers(annotation, processedVisitors)) - .flatMap(annotation -> processAnnotationTransformers(annotation, processedVisitors)); + .flatMap(annotation -> processAnnotationRemappers(annotation, processedVisitors)) + .flatMap(annotation -> processAnnotationTransformers(annotation, processedVisitors)); } private Stream flattenRepeatable(ProcessedAnnotation processedAnnotation) { @@ -1036,25 +1027,25 @@ private Stream flattenRepeatable(ProcessedAnnotation proces AnnotationValue annotationValue = processedAnnotation.getAnnotationValue(); List> repeatableAnnotations = annotationValue.getAnnotations(AnnotationMetadata.VALUE_MEMBER); boolean isRepeatableAnnotationContainer = !repeatableAnnotations.isEmpty() && repeatableAnnotations.stream() - .allMatch(value -> { - T annotationMirror = getAnnotationMirror(value.getAnnotationName()).orElse(null); - return annotationMirror != null && getRepeatableNameForType(annotationMirror) != null; - }); + .allMatch(value -> { + T annotationMirror = getAnnotationMirror(value.getAnnotationName()).orElse(null); + return annotationMirror != null && getRepeatableNameForType(annotationMirror) != null; + }); if (isRepeatableAnnotationContainer) { // Repeatable annotations container is being added with values // We will add every repeatable annotation separately to properly detect its container and run transformations Map containerValues = new LinkedHashMap<>(annotationValue.getValues()); containerValues.remove(AnnotationMetadata.VALUE_MEMBER); return Stream.concat( - Stream.of( - // Add repeatable container for possible stereotype annotation retrieval - // and additional members defined in the container annotation - toProcessedAnnotation(new AnnotationValue<>( - annotationValue.getAnnotationName(), - containerValues, - getRetentionPolicy(annotationValue.getAnnotationName()))) - ), - repeatableAnnotations.stream().map(this::toProcessedAnnotation) + Stream.of( + // Add repeatable container for possible stereotype annotation retrieval + // and additional members defined in the container annotation + toProcessedAnnotation(new AnnotationValue<>( + annotationValue.getAnnotationName(), + containerValues, + getRetentionPolicy(annotationValue.getAnnotationName()))) + ), + repeatableAnnotations.stream().map(this::toProcessedAnnotation) ); } return Stream.of(processedAnnotation); @@ -1084,11 +1075,11 @@ private ProcessedAnnotation processAliases(ProcessedAnnotation processedAnnotati T member = getAnnotationMember(annotationType, key); if (member != null) { handleAnnotationAlias( - annotationType, - newValues, - member, - value, - introducedAnnotations + annotationType, + newValues, + member, + value, + introducedAnnotations ); } } @@ -1098,7 +1089,7 @@ private ProcessedAnnotation processAliases(ProcessedAnnotation processedAnnotati return processedAnnotation; } return processedAnnotation.withAnnotationValue( - AnnotationValue.builder(annotationValue).members(newValues).build() + AnnotationValue.builder(annotationValue).members(newValues).build() ); } @@ -1122,14 +1113,14 @@ private void processStereotypes(MutableAnnotationMetadata annotationMetadata, stereotypes = annotationValue.getStereotypes() == null ? Stream.empty() : annotationValue.getStereotypes().stream().map(this::toProcessedAnnotation); } else { stereotypes = annotationMirrorToAnnotationValue(getAnnotationsForType(annotationType).stream(), - annotationType, false, annotationMetadata, isDeclared, true); + annotationType, false, annotationMetadata, isDeclared, true); } addStereotypeAnnotations( - stereotypes, - annotationType, - newParentAnnotations, - annotationMetadata, - isDeclared + stereotypes, + annotationType, + newParentAnnotations, + annotationMetadata, + isDeclared ); } @@ -1144,31 +1135,31 @@ private void addAnnotation(MutableAnnotationMetadata mutableAnnotationMetadata, if (repeatableContainer != null) { if (isDeclared) { mutableAnnotationMetadata.addDeclaredRepeatableStereotype( - parentAnnotations, - repeatableContainer, - annotationValue + parentAnnotations, + repeatableContainer, + annotationValue ); } else { mutableAnnotationMetadata.addRepeatableStereotype( - parentAnnotations, - repeatableContainer, - annotationValue + parentAnnotations, + repeatableContainer, + annotationValue ); } } else { if (isDeclared) { mutableAnnotationMetadata.addDeclaredStereotype( - parentAnnotations, - annotationValue.getAnnotationName(), - annotationValue.getValues(), - annotationValue.getRetentionPolicy() + parentAnnotations, + annotationValue.getAnnotationName(), + annotationValue.getValues(), + annotationValue.getRetentionPolicy() ); } else { mutableAnnotationMetadata.addStereotype( - parentAnnotations, - annotationValue.getAnnotationName(), - annotationValue.getValues(), - annotationValue.getRetentionPolicy() + parentAnnotations, + annotationValue.getAnnotationName(), + annotationValue.getValues(), + annotationValue.getRetentionPolicy() ); } } @@ -1182,15 +1173,15 @@ private void addAnnotation(MutableAnnotationMetadata mutableAnnotationMetadata, } else { if (isDeclared) { mutableAnnotationMetadata.addDeclaredAnnotation( - annotationValue.getAnnotationName(), - annotationValue.getValues(), - annotationValue.getRetentionPolicy() + annotationValue.getAnnotationName(), + annotationValue.getValues(), + annotationValue.getRetentionPolicy() ); } else { mutableAnnotationMetadata.addAnnotation( - annotationValue.getAnnotationName(), - annotationValue.getValues(), - annotationValue.getRetentionPolicy() + annotationValue.getAnnotationName(), + annotationValue.getValues(), + annotationValue.getRetentionPolicy() ); } } @@ -1260,11 +1251,11 @@ private void addStereotypeAnnotations(Stream stream, if (CollectionUtils.isNotEmpty(introducedAliasForAnnotations)) { // Add annotation created by @AliasFor addStereotypeAnnotations( - introducedAliasForAnnotations.stream(), - null, - parentAnnotations, - metadata, - isDeclared + introducedAliasForAnnotations.stream(), + null, + parentAnnotations, + metadata, + isDeclared ); } @@ -1292,7 +1283,7 @@ private void handleAnnotationsWithMutatedMetadata(T element, boolean isDeclared, String lastParent) { if (lastParent != null && element != null) { - CachedAnnotationMetadata modifiedStereotypes = MUTATED_ANNOTATION_METADATA.get(new MetadataKey<>(element)); + CachedAnnotationMetadata modifiedStereotypes = MUTATED_ANNOTATION_METADATA.get(element); if (modifiedStereotypes != null && !modifiedStereotypes.isEmpty() && modifiedStereotypes.isMutated()) { for (String stereotypeName : modifiedStereotypes.getStereotypeAnnotationNames()) { final AnnotationValue a = modifiedStereotypes.getAnnotation(stereotypeName); @@ -1304,11 +1295,11 @@ private void handleAnnotationsWithMutatedMetadata(T element, newParentAnnotations.addAll(stereotypeParents); addStereotypeAnnotations( - Stream.of(toProcessedAnnotation(a)), - null, - newParentAnnotations, - metadata, - isDeclared + Stream.of(toProcessedAnnotation(a)), + null, + newParentAnnotations, + metadata, + isDeclared ); } @@ -1318,11 +1309,11 @@ private void handleAnnotationsWithMutatedMetadata(T element, continue; } addStereotypeAnnotations( - Stream.of(toProcessedAnnotation(a)), - null, - parentAnnotations, - metadata, - isDeclared + Stream.of(toProcessedAnnotation(a)), + null, + parentAnnotations, + metadata, + isDeclared ); } @@ -1358,12 +1349,12 @@ private AnnotationValue handleMemberBinding(DefaultAnnotationMetadata metadat values.keySet().removeAll(nonBinding); } final AnnotationValueBuilder builder = - AnnotationValue - .builder(lastParent) - .members(values); + AnnotationValue + .builder(lastParent) + .members(values); data.put( - InterceptorBindingQualifier.META_MEMBER_MEMBERS, - builder.build() + InterceptorBindingQualifier.META_MEMBER_MEMBERS, + builder.build() ); } @@ -1397,16 +1388,16 @@ private void addToInterceptorBindingsIfNecessary(List> AnnotationValueBuilder interceptorBinding = null; if (AnnotationUtil.ANN_AROUND.equals(annotationName) || AnnotationUtil.ANN_INTERCEPTOR_BINDING.equals(annotationName)) { interceptorBinding = AnnotationValue.builder(AnnotationUtil.ANN_INTERCEPTOR_BINDING) - .member(AnnotationMetadata.VALUE_MEMBER, new AnnotationClassValue<>(lastParent)) - .member("kind", "AROUND"); + .member(AnnotationMetadata.VALUE_MEMBER, new AnnotationClassValue<>(lastParent)) + .member("kind", "AROUND"); } else if (AnnotationUtil.ANN_INTRODUCTION.equals(annotationName)) { interceptorBinding = AnnotationValue.builder(AnnotationUtil.ANN_INTERCEPTOR_BINDING) - .member(AnnotationMetadata.VALUE_MEMBER, new AnnotationClassValue<>(lastParent)) - .member("kind", "INTRODUCTION"); + .member(AnnotationMetadata.VALUE_MEMBER, new AnnotationClassValue<>(lastParent)) + .member("kind", "INTRODUCTION"); } else if (AnnotationUtil.ANN_AROUND_CONSTRUCT.equals(annotationName)) { interceptorBinding = AnnotationValue.builder(AnnotationUtil.ANN_INTERCEPTOR_BINDING) - .member(AnnotationMetadata.VALUE_MEMBER, new AnnotationClassValue<>(lastParent)) - .member("kind", "AROUND_CONSTRUCT"); + .member(AnnotationMetadata.VALUE_MEMBER, new AnnotationClassValue<>(lastParent)) + .member("kind", "AROUND_CONSTRUCT"); } if (interceptorBinding != null) { interceptorBindings.add(interceptorBinding); @@ -1499,8 +1490,8 @@ private Stream processAnnotationMapp private ProcessedAnnotation toProcessedAnnotation(AnnotationValue av) { return new ProcessedAnnotation( - getAnnotationMirror(av.getAnnotationName()).orElse(null), - av + getAnnotationMirror(av.getAnnotationName()).orElse(null), + av ); } @@ -1561,10 +1552,10 @@ public AnnotationMetadata annotate(@NonNull AnnotationMe @NonNull AnnotationValue annotationValue) { return modify(annotationMetadata, metadata -> { addAnnotations( - metadata, - Stream.of(toProcessedAnnotation(annotationValue)), - true, - Collections.emptyList() + metadata, + Stream.of(toProcessedAnnotation(annotationValue)), + true, + Collections.emptyList() ); }); } @@ -1721,37 +1712,24 @@ public interface CachedAnnotationMetadata extends AnnotationMetadataDelegate { /** * Key used to reference mutated metadata. * + * @param e1 The element 1 + * @param e2 The element 2 * @param the element type */ - private static class MetadataKey { - final T[] elements; - final int hashCode; - - MetadataKey(T... elements) { - this.elements = elements; - this.hashCode = Objects.hash((Object[]) elements); - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - MetadataKey that = (MetadataKey) o; - return Arrays.equals(elements, that.elements); - } - - @Override - public int hashCode() { - return hashCode; - } + @Internal + private record Key2(T e1, T e2) { } - private interface TriConsumer { - void accept(T t, U u, V v); + /** + * Key used to reference mutated metadata. + * + * @param e1 The element 1 + * @param e2 The element 2 + * @param e3 The element 3 + * @param the element type + */ + @Internal + private record Key3(T e1, T e2, T e3) { } private static final class DefaultCachedAnnotationMetadata implements CachedAnnotationMetadata { diff --git a/core-processor/src/main/java/io/micronaut/inject/ast/ClassElement.java b/core-processor/src/main/java/io/micronaut/inject/ast/ClassElement.java index 4f23c8cc566..86fc079a052 100644 --- a/core-processor/src/main/java/io/micronaut/inject/ast/ClassElement.java +++ b/core-processor/src/main/java/io/micronaut/inject/ast/ClassElement.java @@ -26,6 +26,7 @@ import io.micronaut.core.type.DefaultArgument; import io.micronaut.core.util.ArgumentUtils; import io.micronaut.core.util.CollectionUtils; +import io.micronaut.inject.ast.annotation.MutableAnnotationMetadataDelegate; import io.micronaut.inject.ast.beans.BeanElementBuilder; import java.lang.reflect.GenericArrayType; @@ -67,6 +68,20 @@ public interface ClassElement extends TypedElement { */ ClassElement[] ZERO_CLASS_ELEMENTS = new ClassElement[0]; + /** + * Returns the type annotations. + * Added by: + * - The declaration of the type variable {@link java.lang.annotation.ElementType#TYPE_PARAMETER} + * - The use of the type {@link java.lang.annotation.ElementType#TYPE} + * @return the type annotations + * @since 4.0.0 + */ + @Experimental + @NonNull + default MutableAnnotationMetadataDelegate getTypeAnnotationMetadata() { + return (MutableAnnotationMetadataDelegate) MutableAnnotationMetadataDelegate.EMPTY; + } + /** * Tests whether one type is assignable to another. * @@ -457,6 +472,30 @@ default List getFields() { return getEnclosedElements(ElementQuery.ALL_FIELDS); } + /** + * Find an instance/static field with a name in this class, super class or an interface. + * + * @param name The field name + * @return The field + * @since 4.0.0 + */ + @Experimental + @NonNull + default Optional findField(String name) { + return getEnclosedElement(ElementQuery.ALL_FIELDS.named(name)); + } + + /** + * Find an instance/static method with a name in this class, super class or an interface. + * + * @return The methods + * @since 4.0.0 + */ + @NonNull + default List getMethods() { + return getEnclosedElements(ElementQuery.ALL_METHODS); + } + /** * Find a method with a name. * @@ -465,6 +504,7 @@ default List getFields() { * @since 4.0.0 */ @NonNull + @Experimental default Optional findMethod(String name) { return getEnclosedElement(ElementQuery.ALL_METHODS.named(name)); } diff --git a/core-processor/src/main/java/io/micronaut/inject/ast/GenericElement.java b/core-processor/src/main/java/io/micronaut/inject/ast/GenericElement.java index cba6409ac6f..e296a86991c 100644 --- a/core-processor/src/main/java/io/micronaut/inject/ast/GenericElement.java +++ b/core-processor/src/main/java/io/micronaut/inject/ast/GenericElement.java @@ -15,7 +15,9 @@ */ package io.micronaut.inject.ast; +import io.micronaut.core.annotation.AnnotationMetadata; import io.micronaut.core.annotation.Experimental; +import io.micronaut.inject.ast.annotation.MutableAnnotationMetadataDelegate; import org.jetbrains.annotations.NotNull; /** @@ -40,4 +42,17 @@ default Object getGenericNativeType() { return getNativeType(); } + /** + * Returns the type parameter annotations. + * Added to this generic element by: + * - The declaration of the type variable {@link java.lang.annotation.ElementType#TYPE_PARAMETER} + * - The use of the type {@link java.lang.annotation.ElementType#TYPE} + * @return the type annotations + */ + @Experimental + @NotNull + default MutableAnnotationMetadataDelegate getGenericTypeAnnotationMetadata() { + return (MutableAnnotationMetadataDelegate) MutableAnnotationMetadataDelegate.EMPTY; + } + } diff --git a/core-processor/src/main/java/io/micronaut/inject/ast/GenericPlaceholderElement.java b/core-processor/src/main/java/io/micronaut/inject/ast/GenericPlaceholderElement.java index 2d204ff1f3b..149fccf3507 100644 --- a/core-processor/src/main/java/io/micronaut/inject/ast/GenericPlaceholderElement.java +++ b/core-processor/src/main/java/io/micronaut/inject/ast/GenericPlaceholderElement.java @@ -51,4 +51,24 @@ public interface GenericPlaceholderElement extends GenericElement { * @return The element declaring this variable, if it can be determined. Must be either a method or a class. */ Optional getDeclaringElement(); + + /** + * @return The required element declaring this variable, if it can be determined. Must be either a method or a class. Or throws an exception. + * @since 4.0.0 + */ + @NonNull + default Element getRequiredDeclaringElement() { + return getDeclaringElement().orElseThrow(() -> new IllegalStateException("Declared element is not present!")); + } + + /** + * In some cases the class element can be a resolved placeholder. + * We want to keep the placeholder to reference the type annotations etc. + * + * @return The resolved value of the placeholder. + * @since 4.0.0 + */ + default Optional getResolved() { + return Optional.empty(); + } } diff --git a/core-processor/src/main/java/io/micronaut/inject/ast/annotation/AbstractElementAnnotationMetadata.java b/core-processor/src/main/java/io/micronaut/inject/ast/annotation/AbstractElementAnnotationMetadata.java new file mode 100644 index 00000000000..28f231d3ef6 --- /dev/null +++ b/core-processor/src/main/java/io/micronaut/inject/ast/annotation/AbstractElementAnnotationMetadata.java @@ -0,0 +1,31 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.inject.ast.annotation; + +import io.micronaut.core.annotation.AnnotationMetadata; +import io.micronaut.core.annotation.Internal; + +/** + * Mutable annotation metadata provider. + * + * @author Denis Stepanov + * @since 4.0.0 + */ +@Internal +public abstract class AbstractElementAnnotationMetadata + extends AbstractMutableAnnotationMetadata + implements ElementAnnotationMetadata { +} diff --git a/core-processor/src/main/java/io/micronaut/inject/ast/annotation/AbstractElementAnnotationMetadataFactory.java b/core-processor/src/main/java/io/micronaut/inject/ast/annotation/AbstractElementAnnotationMetadataFactory.java index 8075ea9fc85..118935e4bd4 100644 --- a/core-processor/src/main/java/io/micronaut/inject/ast/annotation/AbstractElementAnnotationMetadataFactory.java +++ b/core-processor/src/main/java/io/micronaut/inject/ast/annotation/AbstractElementAnnotationMetadataFactory.java @@ -28,14 +28,19 @@ import io.micronaut.inject.ast.Element; import io.micronaut.inject.ast.EnumConstantElement; import io.micronaut.inject.ast.FieldElement; +import io.micronaut.inject.ast.GenericElement; +import io.micronaut.inject.ast.GenericPlaceholderElement; import io.micronaut.inject.ast.MethodElement; import io.micronaut.inject.ast.PackageElement; import io.micronaut.inject.ast.ParameterElement; import io.micronaut.inject.ast.PropertyElement; +import io.micronaut.inject.ast.WildcardElement; import java.lang.annotation.Annotation; +import java.util.Arrays; import java.util.function.Consumer; import java.util.function.Predicate; +import java.util.stream.Stream; /** * Abstract element annotation metadata factory. @@ -89,56 +94,135 @@ public ElementAnnotationMetadata build(Element element, AnnotationMetadata defau throw new IllegalStateException("Unknown element: " + element.getClass() + " with native type: " + element.getNativeType()); } + @Override + public ElementAnnotationMetadata buildTypeAnnotations(ClassElement element) { + return buildTypeAnnotationsForClass(element); + } + + @Override + public ElementAnnotationMetadata buildGenericTypeAnnotations(GenericElement element) { + if (element instanceof GenericPlaceholderElement placeholderElement) { + return buildTypeAnnotationsForGenericPlaceholder(null, placeholderElement); + } + if (element instanceof WildcardElement wildcardElement) { + return buildTypeAnnotationsForWildcard(null, wildcardElement); + } + throw new IllegalStateException("Unknown generic element: " + element.getClass() + " with native type: " + element.getNativeType()); + } + + /** + * Resolve native element. + * + * @param element The element + * @return The native element + */ + protected K getNativeElement(Element element) { + return (K) element.getNativeType(); + } + /** * Lookup annotation metadata for the package. + * * @param packageElement The element * @return The annotation metadata */ protected AbstractAnnotationMetadataBuilder.CachedAnnotationMetadata lookupForPackage(PackageElement packageElement) { - return metadataBuilder.lookupOrBuildForType((K) packageElement.getNativeType()); + return metadataBuilder.lookupOrBuildForType(getNativeElement(packageElement)); } /** * Lookup annotation metadata for the parameter. + * * @param parameterElement The element * @return The annotation metadata */ protected AbstractAnnotationMetadataBuilder.CachedAnnotationMetadata lookupForParameter(ParameterElement parameterElement) { return metadataBuilder.lookupOrBuildForParameter( - (K) parameterElement.getMethodElement().getOwningType().getNativeType(), - (K) parameterElement.getMethodElement().getNativeType(), - (K) parameterElement.getNativeType() + getNativeElement(parameterElement.getMethodElement().getOwningType()), + getNativeElement(parameterElement.getMethodElement()), + getNativeElement(parameterElement) ); } /** * Lookup annotation metadata for the field. + * * @param fieldElement The element * @return The annotation metadata */ protected AbstractAnnotationMetadataBuilder.CachedAnnotationMetadata lookupForField(FieldElement fieldElement) { return metadataBuilder.lookupOrBuildForField( - (K) fieldElement.getOwningType().getNativeType(), - (K) fieldElement.getNativeType() + getNativeElement(fieldElement.getOwningType()), + getNativeElement(fieldElement) ); } /** * Lookup annotation metadata for the method. + * * @param methodElement The element * @return The annotation metadata */ protected AbstractAnnotationMetadataBuilder.CachedAnnotationMetadata lookupForMethod(MethodElement methodElement) { - return metadataBuilder.lookupOrBuildForMethod((K) methodElement.getOwningType().getNativeType(), (K) methodElement.getNativeType()); + return metadataBuilder.lookupOrBuildForMethod( + getNativeElement(methodElement.getOwningType()), + getNativeElement(methodElement) + ); } /** * Lookup annotation metadata for the class. + * * @param classElement The element * @return The annotation metadata */ protected AbstractAnnotationMetadataBuilder.CachedAnnotationMetadata lookupForClass(ClassElement classElement) { - return metadataBuilder.lookupOrBuildForType((K) classElement.getNativeType()); + return metadataBuilder.lookupOrBuildForType(getNativeElement(classElement)); + } + + /** + * Lookup annotation metadata for the class. + * + * @param classElement The element + * @return The annotation metadata + */ + protected AbstractAnnotationMetadataBuilder.CachedAnnotationMetadata lookupTypeAnnotationsForClass(ClassElement classElement) { + return new AbstractAnnotationMetadataBuilder.CachedAnnotationMetadata() { + @Override + public AnnotationMetadata getAnnotationMetadata() { + return AnnotationMetadata.EMPTY_METADATA; + } + + @Override + public boolean isMutated() { + return false; + } + + @Override + public void update(AnnotationMetadata annotationMetadata) { + throw new IllegalStateException("Class element: [" + classElement + "] doesn't support type annotations!"); + } + }; + } + + /** + * Lookup annotation metadata for the placeholder. + * + * @param placeholderElement The element + * @return The annotation metadata + */ + protected AbstractAnnotationMetadataBuilder.CachedAnnotationMetadata lookupTypeAnnotationsForGenericPlaceholder(GenericPlaceholderElement placeholderElement) { + return metadataBuilder.lookupOrBuildForType(getNativeElement(placeholderElement)); + } + + /** + * Lookup annotation metadata for the wildcard. + * + * @param wildcardElement The element + * @return The annotation metadata + */ + protected AbstractAnnotationMetadataBuilder.CachedAnnotationMetadata lookupTypeAnnotationsForWildcard(WildcardElement wildcardElement) { + return metadataBuilder.lookupOrBuildForType(getNativeElement(wildcardElement)); } @NonNull @@ -226,7 +310,12 @@ public String toString() { @NonNull private AbstractElementAnnotationMetadata buildForMethod(@Nullable AnnotationMetadata defaultAnnotationMetadata, @NonNull MethodElement methodElement) { - return new AbstractElementAnnotationMetadata(isReadOnly, methodElement.getOwningType(), defaultAnnotationMetadata) { + return new AbstractElementAnnotationMetadata(defaultAnnotationMetadata) { + + @Override + protected AnnotationMetadata[] getParentAnnotationMetadata() { + return new AnnotationMetadata[]{methodElement.getOwningType()}; + } @Override protected AbstractAnnotationMetadataBuilder.CachedAnnotationMetadata lookup() { @@ -257,6 +346,22 @@ public String toString() { }; } + @NonNull + private AbstractElementAnnotationMetadata buildTypeAnnotationsForClass(@NonNull ClassElement classElement) { + return new AbstractElementAnnotationMetadata(null) { + + @Override + protected AbstractAnnotationMetadataBuilder.CachedAnnotationMetadata lookup() { + return lookupTypeAnnotationsForClass(classElement); + } + + @Override + public String toString() { + return classElement.toString(); + } + }; + } + @NonNull private AbstractElementAnnotationMetadata buildForClass(@Nullable AnnotationMetadata defaultAnnotationMetadata, @NonNull ClassElement classElement) { return new AbstractElementAnnotationMetadata(defaultAnnotationMetadata) { @@ -273,6 +378,39 @@ public String toString() { }; } + @NonNull + private AbstractElementAnnotationMetadata buildTypeAnnotationsForGenericPlaceholder(@Nullable AnnotationMetadata defaultAnnotationMetadata, + @NonNull GenericPlaceholderElement placeholderElement) { + return new AbstractElementAnnotationMetadata(defaultAnnotationMetadata) { + + @Override + protected AbstractAnnotationMetadataBuilder.CachedAnnotationMetadata lookup() { + return lookupTypeAnnotationsForGenericPlaceholder(placeholderElement); + } + + @Override + public String toString() { + return placeholderElement.toString(); + } + }; + } + + @NonNull + private AbstractElementAnnotationMetadata buildTypeAnnotationsForWildcard(@Nullable AnnotationMetadata defaultAnnotationMetadata, + @NonNull WildcardElement wildcardElement) { + return new AbstractElementAnnotationMetadata(defaultAnnotationMetadata) { + + @Override + protected AbstractAnnotationMetadataBuilder.CachedAnnotationMetadata lookup() { + return lookupTypeAnnotationsForWildcard(wildcardElement); + } + + @Override + public String toString() { + return wildcardElement.toString(); + } + }; + } /** * Abstract implementation of {@link ElementAnnotationMetadata}. @@ -284,22 +422,16 @@ protected abstract class AbstractElementAnnotationMetadata implements ElementAnn protected AnnotationMetadata preloadedAnnotationMetadata; private final boolean readOnly; - private AbstractAnnotationMetadataBuilder.CachedAnnotationMetadata cachedAnnotationMetadata; - private final ClassElement classElement; + private AbstractAnnotationMetadataBuilder.CachedAnnotationMetadata cacheEntry; + private AnnotationMetadata cacheAnnotationMetadata; protected AbstractElementAnnotationMetadata(@Nullable AnnotationMetadata annotationMetadata) { this(AbstractElementAnnotationMetadataFactory.this.isReadOnly, annotationMetadata); } - protected AbstractElementAnnotationMetadata(boolean readOnly, @Nullable AnnotationMetadata annotationMetadata) { - this(readOnly, null, annotationMetadata); - } - protected AbstractElementAnnotationMetadata(boolean readOnly, - ClassElement classElement, @Nullable AnnotationMetadata annotationMetadata) { this.readOnly = readOnly; - this.classElement = classElement; this.preloadedAnnotationMetadata = annotationMetadata; if (preloadedAnnotationMetadata instanceof AbstractAnnotationMetadataBuilder.CachedAnnotationMetadata) { throw new IllegalStateException(); @@ -309,37 +441,52 @@ protected AbstractElementAnnotationMetadata(boolean readOnly, } } + /** + * @return The parent annotation metadata for the annotation hierarchy + */ + @NonNull + protected AnnotationMetadata[] getParentAnnotationMetadata() { + return new AnnotationMetadata[0]; + } + protected abstract AbstractAnnotationMetadataBuilder.CachedAnnotationMetadata lookup(); private AbstractAnnotationMetadataBuilder.CachedAnnotationMetadata getCacheEntry() { - if (cachedAnnotationMetadata == null) { - cachedAnnotationMetadata = lookup(); + if (cacheEntry == null) { + cacheEntry = lookup(); } - return cachedAnnotationMetadata; + return cacheEntry; } @Override public AnnotationMetadata getAnnotationMetadata() { - if (preloadedAnnotationMetadata != null) { - if (classElement != null) { - if (preloadedAnnotationMetadata instanceof AnnotationMetadataHierarchy) { - return preloadedAnnotationMetadata; - } - return new AnnotationMetadataHierarchy(classElement, preloadedAnnotationMetadata); + if (cacheAnnotationMetadata == null) { + if (preloadedAnnotationMetadata instanceof AnnotationMetadataHierarchy) { + return preloadedAnnotationMetadata; + } + AnnotationMetadata[] parentAnnotationMetadata = getParentAnnotationMetadata(); + AnnotationMetadata annotationMetadata; + if (preloadedAnnotationMetadata != null) { + annotationMetadata = preloadedAnnotationMetadata; + } else { + annotationMetadata = getCacheEntry(); + } + if (parentAnnotationMetadata.length > 0) { + cacheAnnotationMetadata = new AnnotationMetadataHierarchy( + Stream.concat(Arrays.stream(parentAnnotationMetadata), Stream.of(annotationMetadata)).toArray(AnnotationMetadata[]::new) + ); + } else { + cacheAnnotationMetadata = annotationMetadata; } - return preloadedAnnotationMetadata; - } - if (classElement != null) { - return new AnnotationMetadataHierarchy(classElement, getCacheEntry()); } - return getCacheEntry(); + return cacheAnnotationMetadata; } private AnnotationMetadata getAnnotationMetadataToModify() { + if (preloadedAnnotationMetadata instanceof AnnotationMetadataHierarchy) { + return preloadedAnnotationMetadata.getDeclaredMetadata().copyAnnotationMetadata(); + } if (preloadedAnnotationMetadata != null) { - if (preloadedAnnotationMetadata instanceof AnnotationMetadataHierarchy) { - return preloadedAnnotationMetadata.getDeclaredMetadata().copyAnnotationMetadata(); - } return preloadedAnnotationMetadata; } return getCacheEntry().copyAnnotationMetadata(); @@ -364,6 +511,7 @@ private AnnotationMetadata replaceAnnotationsInternal(AnnotationMetadata annotat } else { preloadedAnnotationMetadata = annotationMetadata; } + cacheAnnotationMetadata = null; return getAnnotationMetadata(); } diff --git a/core-processor/src/main/java/io/micronaut/inject/ast/annotation/AbstractMutableAnnotationMetadata.java b/core-processor/src/main/java/io/micronaut/inject/ast/annotation/AbstractMutableAnnotationMetadata.java new file mode 100644 index 00000000000..04631dab3f6 --- /dev/null +++ b/core-processor/src/main/java/io/micronaut/inject/ast/annotation/AbstractMutableAnnotationMetadata.java @@ -0,0 +1,121 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.inject.ast.annotation; + +import io.micronaut.core.annotation.AnnotationValue; +import io.micronaut.core.annotation.AnnotationValueBuilder; +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.NonNull; + +import java.lang.annotation.Annotation; +import java.util.function.Consumer; +import java.util.function.Predicate; + +/** + * Mutable annotation metadata provider. + * + * @param The return type + * @author Denis Stepanov + * @since 4.0.0 + */ +@Internal +public abstract class AbstractMutableAnnotationMetadata implements MutableAnnotationMetadataDelegate { + + /** + * Provides the return type instance. + * + * @return the return instance + */ + @NonNull + protected abstract R getReturnInstance(); + + /** + * @return The annotation metadata to modify + */ + @NonNull + protected abstract MutableAnnotationMetadataDelegate getAnnotationMetadataToWrite(); + + @Override + @NonNull + public R annotate(@NonNull String annotationType, @NonNull Consumer> consumer) { + getAnnotationMetadataToWrite().annotate(annotationType, consumer); + return getReturnInstance(); + } + + @Override + @NonNull + public R removeAnnotation(@NonNull String annotationType) { + getAnnotationMetadataToWrite().removeAnnotation(annotationType); + return getReturnInstance(); + } + + @Override + @NonNull + public R removeAnnotation(@NonNull Class annotationType) { + getAnnotationMetadataToWrite().removeAnnotation(annotationType); + return getReturnInstance(); + } + + @Override + @NonNull + public R removeAnnotationIf(@NonNull Predicate> predicate) { + getAnnotationMetadataToWrite().removeAnnotationIf(predicate); + return getReturnInstance(); + } + + @Override + @NonNull + public R removeStereotype(@NonNull String annotationType) { + getAnnotationMetadataToWrite().removeStereotype(annotationType); + return getReturnInstance(); + } + + @Override + @NonNull + public R removeStereotype(@NonNull Class annotationType) { + getAnnotationMetadataToWrite().removeStereotype(annotationType); + return getReturnInstance(); + } + + @Override + @NonNull + public R annotate(@NonNull String annotationType) { + getAnnotationMetadataToWrite().annotate(annotationType); + return getReturnInstance(); + } + + @Override + @NonNull + public R annotate(@NonNull Class annotationType, @NonNull Consumer> consumer) { + getAnnotationMetadataToWrite().annotate(annotationType, consumer); + return getReturnInstance(); + } + + @Override + @NonNull + public R annotate(@NonNull Class annotationType) { + getAnnotationMetadataToWrite().annotate(annotationType); + return getReturnInstance(); + } + + @Override + @NonNull + public R annotate(@NonNull AnnotationValue annotationValue) { + getAnnotationMetadataToWrite().annotate(annotationValue); + return getReturnInstance(); + } + +} diff --git a/core-processor/src/main/java/io/micronaut/inject/ast/annotation/ElementAnnotationMetadataFactory.java b/core-processor/src/main/java/io/micronaut/inject/ast/annotation/ElementAnnotationMetadataFactory.java index 6eda2ba4d9a..3e45fab8fea 100644 --- a/core-processor/src/main/java/io/micronaut/inject/ast/annotation/ElementAnnotationMetadataFactory.java +++ b/core-processor/src/main/java/io/micronaut/inject/ast/annotation/ElementAnnotationMetadataFactory.java @@ -17,7 +17,9 @@ import io.micronaut.core.annotation.AnnotationMetadata; import io.micronaut.core.annotation.NonNull; +import io.micronaut.inject.ast.ClassElement; import io.micronaut.inject.ast.Element; +import io.micronaut.inject.ast.GenericElement; /** * Element's annotation metadata factory. @@ -36,6 +38,24 @@ public interface ElementAnnotationMetadataFactory { @NonNull ElementAnnotationMetadata build(@NonNull Element element); + /** + * Build new class element type annotation metadata from the class element. + * + * @param element The element + * @return the element's metadata + */ + @NonNull + ElementAnnotationMetadata buildTypeAnnotations(@NonNull ClassElement element); + + /** + * Build new generic element type annotation metadata from the class element. + * + * @param element The element + * @return the element's metadata + */ + @NonNull + ElementAnnotationMetadata buildGenericTypeAnnotations(@NonNull GenericElement element); + /** * Build new element annotation metadata from the element with preloaded annotations. * This method will avoid fetching default annotation metadata from cache. diff --git a/core-processor/src/main/java/io/micronaut/inject/ast/annotation/MutableAnnotationMetadataDelegate.java b/core-processor/src/main/java/io/micronaut/inject/ast/annotation/MutableAnnotationMetadataDelegate.java index 1603509c871..b8ecba2534b 100644 --- a/core-processor/src/main/java/io/micronaut/inject/ast/annotation/MutableAnnotationMetadataDelegate.java +++ b/core-processor/src/main/java/io/micronaut/inject/ast/annotation/MutableAnnotationMetadataDelegate.java @@ -35,6 +35,12 @@ */ public interface MutableAnnotationMetadataDelegate extends AnnotationMetadataDelegate { + /** + * The empty metadata. + */ + MutableAnnotationMetadataDelegate EMPTY = new MutableAnnotationMetadataDelegate<>() { + }; + /** * Annotate this element with the given annotation type. If the annotation is already present then * any values populated by the builder will be merged/overridden with the existing values. diff --git a/core-processor/src/main/java/io/micronaut/inject/ast/utils/AstBeanPropertiesUtils.java b/core-processor/src/main/java/io/micronaut/inject/ast/utils/AstBeanPropertiesUtils.java index 46b66119798..5a0dfeb942f 100644 --- a/core-processor/src/main/java/io/micronaut/inject/ast/utils/AstBeanPropertiesUtils.java +++ b/core-processor/src/main/java/io/micronaut/inject/ast/utils/AstBeanPropertiesUtils.java @@ -153,14 +153,14 @@ public static List resolveBeanProperties(PropertyElementQuery c // and it has more type arguments annotations - use it as the property type if (value.field != null && value.field.getType().equals(value.type) - && countGenericTypeAnnotations(value.field.getType()) > countGenericTypeAnnotations(value.type.getType())) { + && hasMoreAnnotations(value.field.getType(), value.type)) { value.type = value.field.getGenericType(); } // In a case when the getter's type is the same as the selected property type, // and it has more type arguments annotations - use it as the property type if (value.getter != null && value.getter.getGenericReturnType().equals(value.type) - && countGenericTypeAnnotations(value.getter.getGenericReturnType()) > countGenericTypeAnnotations(value.type.getType())) { + && hasMoreAnnotations(value.getter.getGenericReturnType(), value.type)) { value.type = value.getter.getGenericReturnType(); } if (value.readAccessKind != null || value.writeAccessKind != null) { @@ -179,6 +179,11 @@ && countGenericTypeAnnotations(value.getter.getGenericReturnType()) > countGener return Collections.emptyList(); } + private static boolean hasMoreAnnotations(ClassElement c1, ClassElement c2) { + return countGenericTypeAnnotations(c1) > countGenericTypeAnnotations(c2.getType()) + || c1.getTypeAnnotationMetadata().getAnnotationNames().size() > c2.getTypeAnnotationMetadata().getAnnotationNames().size(); + } + private static boolean isFieldInRole(FieldElement fieldElement) { return fieldElement.hasDeclaredAnnotation(AnnotationUtil.INJECT) || fieldElement.hasStereotype(Value.class) diff --git a/core-processor/src/main/java/io/micronaut/inject/ast/utils/EnclosedElementsQuery.java b/core-processor/src/main/java/io/micronaut/inject/ast/utils/EnclosedElementsQuery.java index 982791c86ac..0caa4467840 100644 --- a/core-processor/src/main/java/io/micronaut/inject/ast/utils/EnclosedElementsQuery.java +++ b/core-processor/src/main/java/io/micronaut/inject/ast/utils/EnclosedElementsQuery.java @@ -19,7 +19,6 @@ import io.micronaut.core.annotation.Internal; import io.micronaut.core.annotation.NonNull; import io.micronaut.core.annotation.Nullable; -import io.micronaut.core.util.clhm.ConcurrentLinkedHashMap; import io.micronaut.inject.ast.ClassElement; import io.micronaut.inject.ast.ConstructorElement; import io.micronaut.inject.ast.Element; @@ -34,6 +33,7 @@ import java.util.Collection; import java.util.Collections; import java.util.Iterator; +import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; @@ -53,7 +53,8 @@ @Internal public abstract class EnclosedElementsQuery { - private final Map elementsCache = new ConcurrentLinkedHashMap.Builder().maximumWeightedCapacity(200).build(); + private static final int MAX_ITEMS_IN_CACHE = 200; + private final Map elementsCache = new LinkedHashMap<>(); /** * Get native class element. @@ -87,14 +88,14 @@ public List getEnclosedElements(C Objects.requireNonNull(query, "Query cannot be null"); ElementQuery.Result result = query.result(); Set excludeElements = getExcludedNativeElements(result); - Predicate filter = element -> { + Predicate filter = element -> { if (excludeElements.contains(getNativeType(element))) { return false; } List> elementPredicates = result.getElementPredicates(); if (!elementPredicates.isEmpty()) { for (Predicate elementPredicate : elementPredicates) { - if (!elementPredicate.test((T) element)) { + if (!elementPredicate.test(element)) { return false; } } @@ -172,10 +173,11 @@ public List getEnclosedElements(C } return true; }; - return (List) getAllElements(getNativeClassType(classElement), result.isOnlyDeclared(), (t1, t2) -> reduceElements(t1, t2, result), result) - .stream() - .filter(filter) - .toList(); + Collection allElements = getAllElements(getNativeClassType(classElement), result.isOnlyDeclared(), (t1, t2) -> reduceElements(t1, t2, result), result); + return allElements + .stream() + .filter(filter) + .toList(); } private boolean reduceElements(io.micronaut.inject.ast.Element newElement, @@ -201,26 +203,31 @@ private boolean reduceElements(io.micronaut.inject.ast.Element newElement, return false; } - private Collection getAllElements(C classNode, - boolean onlyDeclared, - BiPredicate reduce, - ElementQuery.Result result) { - Set elements = new LinkedHashSet<>(); + private Collection getAllElements(C classNode, + boolean onlyDeclared, + BiPredicate reduce, + ElementQuery.Result result) { + Set elements = new LinkedHashSet<>(); List> hierarchy = new ArrayList<>(); collectHierarchy(classNode, onlyDeclared, hierarchy, result); for (List classElements : hierarchy) { - Set addedFromClassElements = new LinkedHashSet<>(); + Set addedFromClassElements = new LinkedHashSet<>(); classElements: for (N element : classElements) { N cacheKey = getCacheKey(element); - io.micronaut.inject.ast.Element newElement = elementsCache.computeIfAbsent(cacheKey, e -> this.toAstElement(e, result.getElementType())); + T newElement = (T) elementsCache.computeIfAbsent(cacheKey, e -> this.toAstElement(e, result.getElementType())); + if (elementsCache.size() == MAX_ITEMS_IN_CACHE) { + Iterator> iterator = elementsCache.entrySet().iterator(); + iterator.next(); + iterator.remove(); + } if (!result.getElementType().isInstance(newElement)) { // dirty cache elementsCache.remove(cacheKey); - newElement = elementsCache.computeIfAbsent(cacheKey, e -> this.toAstElement(e, result.getElementType())); + newElement = (T) elementsCache.computeIfAbsent(cacheKey, e -> this.toAstElement(e, result.getElementType())); } - for (Iterator iterator = elements.iterator(); iterator.hasNext(); ) { - io.micronaut.inject.ast.Element existingElement = iterator.next(); + for (Iterator iterator = elements.iterator(); iterator.hasNext(); ) { + T existingElement = iterator.next(); if (newElement.equals(existingElement)) { continue; } diff --git a/core-processor/src/main/java/io/micronaut/inject/processing/FactoryBeanElementCreator.java b/core-processor/src/main/java/io/micronaut/inject/processing/FactoryBeanElementCreator.java index 9bdeb03146f..d9b1789591b 100644 --- a/core-processor/src/main/java/io/micronaut/inject/processing/FactoryBeanElementCreator.java +++ b/core-processor/src/main/java/io/micronaut/inject/processing/FactoryBeanElementCreator.java @@ -109,27 +109,15 @@ protected boolean visitPropertyWriteElement(BeanDefinitionVisitor visitor, Prope void visitBeanFactoryElement(BeanDefinitionVisitor visitor, ClassElement producedType, MemberElement producingElement) { if (producedType.isPrimitive()) { - BeanDefinitionWriter producedBeanDefinitionWriter = new BeanDefinitionWriter(producingElement, - OriginatingElements.of(producingElement), - visitorContext, - factoryMethodIndex.getAndIncrement() - ); - buildProducedBeanDefinition(producedBeanDefinitionWriter, producedType, producingElement, producingElement.getAnnotationMetadata()); + buildProducedBeanDefinition(producedType, producingElement, producingElement.getAnnotationMetadata()); } else { AnnotationMetadata producedTypeAnnotationMetadata = createProducedTypeAnnotationMetadata(producedType, producingElement); producedType = producedType.withAnnotationMetadata(producedTypeAnnotationMetadata); AnnotationMetadata producingElementAnnotationMetadata = createProducingElementAnnotationMetadata(producedTypeAnnotationMetadata); producingElement = producingElement.withAnnotationMetadata(producingElementAnnotationMetadata); - BeanDefinitionWriter producedBeanDefinitionWriter = new BeanDefinitionWriter( - producingElement, - OriginatingElements.of(producingElement), - visitorContext, - factoryMethodIndex.getAndIncrement() - ); - // producingElement = producingElement.withAnnotationMetadata(producedTypeAnnotationMetadata); - buildProducedBeanDefinition(producedBeanDefinitionWriter, producedType, producingElement, producedType.getAnnotationMetadata()); + buildProducedBeanDefinition(producedType, producingElement, producedType.getAnnotationMetadata()); if (producingElement instanceof MethodElement methodElement) { if (isAopProxy && visitAopMethod(visitor, methodElement)) { @@ -172,20 +160,27 @@ private AnnotationMetadata createProducedTypeAnnotationMetadata(ClassElement pro return producedAnnotationMetadata; } - private void buildProducedBeanDefinition(BeanDefinitionWriter producedBeanDefinitionWriter, - ClassElement producedType, + private void buildProducedBeanDefinition(ClassElement producedType, MemberElement producingElement, AnnotationMetadata producedAnnotationMetadata) { - visitAnnotationMetadata(producedBeanDefinitionWriter, producedAnnotationMetadata); - producedBeanDefinitionWriter.visitTypeArguments(producedType.getAllTypeArguments()); - - beanDefinitionWriters.add(producedBeanDefinitionWriter); - if (producedType.hasStereotype(EachProperty.class)) { producedType.annotate(ConfigurationReader.class, builder -> builder.member(ConfigurationReader.PREFIX, ConfigurationUtils.getRequiredTypePath(producedType))); + producingElement.annotate(ConfigurationReader.class, builder -> builder.member(ConfigurationReader.PREFIX, ConfigurationUtils.getRequiredTypePath(producedType))); } + BeanDefinitionWriter producedBeanDefinitionWriter = new BeanDefinitionWriter( + producingElement, + OriginatingElements.of(producingElement), + visitorContext, + factoryMethodIndex.getAndIncrement() + ); + + beanDefinitionWriters.add(producedBeanDefinitionWriter); + + visitAnnotationMetadata(producedBeanDefinitionWriter, producedAnnotationMetadata); + producedBeanDefinitionWriter.visitTypeArguments(producedType.getAllTypeArguments()); + if (producingElement instanceof PropertyElement propertyElement) { MethodElement readMethod = propertyElement.getReadMethod().orElse(null); if (readMethod != null) { diff --git a/core-processor/src/main/java/io/micronaut/inject/visitor/VisitorConfiguration.java b/core-processor/src/main/java/io/micronaut/inject/visitor/VisitorConfiguration.java index bc1591f2c3f..4dbb7a95ec9 100644 --- a/core-processor/src/main/java/io/micronaut/inject/visitor/VisitorConfiguration.java +++ b/core-processor/src/main/java/io/micronaut/inject/visitor/VisitorConfiguration.java @@ -24,20 +24,4 @@ public interface VisitorConfiguration { VisitorConfiguration DEFAULT = new VisitorConfiguration() { }; - - /** - * This configures whether to include type level annotations on generic arguments when materializing the AST nodes via - * the {@link io.micronaut.inject.ast.Element} API. - * - *

If {@code true} is returned then methods like {@link io.micronaut.inject.ast.ClassElement#getTypeArguments()} will include annotations declared on the classes themselves within the annotation metadata for each resulting {@link io.micronaut.inject.ast.ClassElement} within the generic arguments.

- * - *

This can be undesirable in the use case where you need to differentiate annotations on the type arguments themselves vs annotations declared on the type, in which case you should return false.

- * - * @return True if annotations should be included - * @see io.micronaut.inject.ast.ElementFactory - */ - default boolean includeTypeLevelAnnotationsInGenericArguments() { - return true; - } - } diff --git a/core-processor/src/main/java/io/micronaut/inject/writer/AbstractClassFileWriter.java b/core-processor/src/main/java/io/micronaut/inject/writer/AbstractClassFileWriter.java index e34f4b57cdb..597e2a4786a 100644 --- a/core-processor/src/main/java/io/micronaut/inject/writer/AbstractClassFileWriter.java +++ b/core-processor/src/main/java/io/micronaut/inject/writer/AbstractClassFileWriter.java @@ -26,6 +26,7 @@ import io.micronaut.core.type.Argument; import io.micronaut.core.util.ArrayUtils; import io.micronaut.core.util.CollectionUtils; +import io.micronaut.inject.annotation.AnnotationMetadataHierarchy; import io.micronaut.inject.annotation.AnnotationMetadataReference; import io.micronaut.inject.annotation.AnnotationMetadataWriter; import io.micronaut.inject.annotation.MutableAnnotationMetadata; @@ -333,12 +334,16 @@ protected static void buildArgument(GeneratorAdapter generatorAdapter, String ar generatorAdapter.push(getTypeReference(objectType)); // 2nd argument: the name generatorAdapter.push(argumentName); - boolean isTypeVariable = objectType instanceof GenericPlaceholderElement || objectType.isTypeVariable(); - if (isTypeVariable) { + + if (objectType instanceof GenericPlaceholderElement placeholderElement) { + // Persist resolved placeholder for backward compatibility + objectType = placeholderElement.getResolved().orElse(placeholderElement); + } + + if (objectType instanceof GenericPlaceholderElement || objectType.isTypeVariable()) { String variableName = argumentName; - if (objectType instanceof GenericPlaceholderElement) { - GenericPlaceholderElement gpe = (GenericPlaceholderElement) objectType; - variableName = gpe.getVariableName(); + if (objectType instanceof GenericPlaceholderElement placeholderElement) { + variableName = placeholderElement.getVariableName(); } boolean hasVariable = !variableName.equals(argumentName); if (hasVariable) { @@ -390,7 +395,17 @@ protected static void buildArgumentWithGenerics( // 2nd argument: the name generatorAdapter.push(argumentName); - AnnotationMetadata annotationMetadata = MutableAnnotationMetadata.of(classElement.getAnnotationMetadata()); + if (classElement instanceof GenericPlaceholderElement placeholderElement) { + // Persist resolved placeholder for backward compatibility + classElement = placeholderElement.getResolved().orElse(classElement); + } + + // Persist only type annotations added to the type argument + AnnotationMetadata annotationMetadata = MutableAnnotationMetadata.of(classElement.getTypeAnnotationMetadata()); + if (classElement.getClass().getSimpleName().startsWith("Kotlin")) { + // Remove after KSP supports type annotations + annotationMetadata = MutableAnnotationMetadata.of(classElement.getAnnotationMetadata()); + } boolean hasAnnotationMetadata = !annotationMetadata.isEmpty(); boolean isRecursiveType = false; @@ -404,7 +419,9 @@ protected static void buildArgumentWithGenerics( } } - if (isRecursiveType || !hasAnnotationMetadata && typeArguments.isEmpty()) { + boolean typeVariable = classElement.isTypeVariable(); + + if (isRecursiveType || !typeVariable && !hasAnnotationMetadata && typeArguments.isEmpty()) { invokeInterfaceStaticMethod( generatorAdapter, Argument.class, @@ -444,7 +461,7 @@ protected static void buildArgumentWithGenerics( invokeInterfaceStaticMethod( generatorAdapter, Argument.class, - classElement.isTypeVariable() ? METHOD_CREATE_TYPE_VAR_WITH_ANNOTATION_METADATA_GENERICS : METHOD_CREATE_ARGUMENT_WITH_ANNOTATION_METADATA_GENERICS + typeVariable ? METHOD_CREATE_TYPE_VAR_WITH_ANNOTATION_METADATA_GENERICS : METHOD_CREATE_ARGUMENT_WITH_ANNOTATION_METADATA_GENERICS ); } @@ -514,7 +531,10 @@ protected static void pushBuildArgumentsForMethod( ClassElement classElement = entry.getGenericType(); String argumentName = entry.getName(); - AnnotationMetadata annotationMetadata = entry.getAnnotationMetadata(); + AnnotationMetadata annotationMetadata = new AnnotationMetadataHierarchy( + entry.getAnnotationMetadata(), + entry.getType().getTypeAnnotationMetadata() + ).merge(); Map typeArguments = classElement.getTypeArguments(); pushCreateArgument( declaringElementName, @@ -554,32 +574,37 @@ protected void pushReturnTypeArgument(Type owningType, ClassElement argument, Map defaults, Map loadTypeMethods) { - Type type = Type.getType(Argument.class); - if (argument.isPrimitive() && !argument.isArray()) { - String constantName = argument.getName().toUpperCase(Locale.ENGLISH); - // refer to constant for primitives - generatorAdapter.getStatic(type, constantName, type); - } else { + // Persist only type annotations added + AnnotationMetadata annotationMetadata = argument.getTypeAnnotationMetadata(); + + if (annotationMetadata.isEmpty()) { + Type type = Type.getType(Argument.class); + if (argument.isPrimitive() && !argument.isArray()) { + String constantName = argument.getName().toUpperCase(Locale.ENGLISH); + // refer to constant for primitives + generatorAdapter.getStatic(type, constantName, type); + return; + } if (!argument.isArray() && String.class.getName().equals(argument.getType().getName()) && argument.getName().equals(argument.getType().getName()) && argument.getAnnotationMetadata().isEmpty()) { - generatorAdapter.getStatic(type, "STRING", type); - return; + generatorAdapter.getStatic(type, "STRING", type); + return; } - - pushCreateArgument( - declaringTypeName, - owningType, - classWriter, - generatorAdapter, - argument.getName(), - argument, - AnnotationMetadata.EMPTY_METADATA, // Don't store return type annotations, method annotations are returned - argument.getTypeArguments(), - defaults, - loadTypeMethods - ); } + + pushCreateArgument( + declaringTypeName, + owningType, + classWriter, + generatorAdapter, + argument.getName(), + argument, + annotationMetadata, + argument.getTypeArguments(), + defaults, + loadTypeMethods + ); } /** @@ -619,8 +644,12 @@ protected static void pushCreateArgument( boolean hasAnnotations = !annotationMetadata.isEmpty(); boolean hasTypeArguments = typeArguments != null && !typeArguments.isEmpty(); + if (typedElement instanceof GenericPlaceholderElement placeholderElement) { + // Persist resolved placeholder for backward compatibility + typedElement = placeholderElement.getResolved().orElse(placeholderElement); + } boolean isGenericPlaceholder = typedElement instanceof GenericPlaceholderElement; - boolean isTypeVariable = isGenericPlaceholder || ((typedElement instanceof ClassElement) && ((ClassElement) typedElement).isTypeVariable()); + boolean isTypeVariable = isGenericPlaceholder || ((typedElement instanceof ClassElement classElement) && classElement.isTypeVariable()); String variableName = argumentName; if (isGenericPlaceholder) { variableName = ((GenericPlaceholderElement) typedElement).getVariableName(); diff --git a/inject-groovy-test/src/main/groovy/io/micronaut/ast/transform/test/AbstractBeanDefinitionSpec.groovy b/inject-groovy-test/src/main/groovy/io/micronaut/ast/transform/test/AbstractBeanDefinitionSpec.groovy index 07cae25f985..b461fa4a46e 100644 --- a/inject-groovy-test/src/main/groovy/io/micronaut/ast/transform/test/AbstractBeanDefinitionSpec.groovy +++ b/inject-groovy-test/src/main/groovy/io/micronaut/ast/transform/test/AbstractBeanDefinitionSpec.groovy @@ -22,12 +22,12 @@ import io.micronaut.ast.groovy.utils.InMemoryByteCodeGroovyClassLoader import io.micronaut.ast.groovy.visitor.GroovyElementFactory import io.micronaut.ast.groovy.visitor.GroovyVisitorContext import io.micronaut.context.ApplicationContext +import io.micronaut.context.ApplicationContextConfiguration import io.micronaut.context.DefaultApplicationContext import io.micronaut.context.Qualifier import io.micronaut.context.event.ApplicationEventPublisherFactory import io.micronaut.core.annotation.AnnotationMetadata import io.micronaut.core.beans.BeanIntrospection -import io.micronaut.core.io.scan.ClassPathResourceLoader import io.micronaut.core.naming.NameUtils import io.micronaut.inject.BeanDefinition import io.micronaut.inject.BeanDefinitionReference @@ -263,11 +263,14 @@ abstract class AbstractBeanDefinitionSpec extends Specification { context.getBean(context.classLoader.loadClass(className), qualifier) } - protected ApplicationContext buildContext(@Language("groovy") String cls, boolean includeAllBeans = false) { + protected ApplicationContext buildContext(@Language("groovy") String cls, boolean includeAllBeans = false, Map properties = [:]) { InMemoryByteCodeGroovyClassLoader classLoader = buildClassLoader(cls) - - return new DefaultApplicationContext( - ClassPathResourceLoader.defaultLoader(classLoader),"test") { + def builder = ApplicationContext.builder() + builder.classLoader(classLoader) + builder.environments("test") + builder.properties(properties) + def env = builder.build().environment + return new DefaultApplicationContext((ApplicationContextConfiguration) builder) { @Override protected List resolveBeanDefinitionReferences() { def references = classLoader.generatedClasses.keySet() diff --git a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/InjectTransform.groovy b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/InjectTransform.groovy index 348b5799fc7..c81b429b294 100644 --- a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/InjectTransform.groovy +++ b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/InjectTransform.groovy @@ -28,7 +28,6 @@ import io.micronaut.context.annotation.Context import io.micronaut.inject.processing.BeanDefinitionCreator import io.micronaut.inject.processing.BeanDefinitionCreatorFactory import io.micronaut.inject.processing.ProcessingException -import io.micronaut.inject.visitor.VisitorConfiguration import io.micronaut.inject.writer.BeanConfigurationWriter import io.micronaut.inject.writer.BeanDefinitionReferenceWriter import io.micronaut.inject.writer.BeanDefinitionVisitor @@ -98,17 +97,7 @@ class InjectTransform implements ASTTransformation, CompilationUnitAware { } } - GroovyVisitorContext groovyVisitorContext = new GroovyVisitorContext(source, unit) { - @Override - VisitorConfiguration getConfiguration() { - new VisitorConfiguration() { - @Override - boolean includeTypeLevelAnnotationsInGenericArguments() { - return false - } - } - } - } + GroovyVisitorContext groovyVisitorContext = new GroovyVisitorContext(source, unit) def elementAnnotationMetadataFactory = groovyVisitorContext .getElementAnnotationMetadataFactory() .readOnly() diff --git a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/TypeElementVisitorTransform.groovy b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/TypeElementVisitorTransform.groovy index afc2a929536..44cdcc9e27d 100644 --- a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/TypeElementVisitorTransform.groovy +++ b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/TypeElementVisitorTransform.groovy @@ -131,7 +131,7 @@ class TypeElementVisitorTransform implements ASTTransformation, CompilationUnitA } void visitClass(ClassNode node) { - if (targetClassElement.getNativeType() != node) { + if ((targetClassElement as GroovyClassElement).getNativeType().annotatedNode() != node) { targetClassElement = visitorContext.getElementFactory().newSourceClassElement(node, visitorContext.getElementAnnotationMetadataFactory()) } for (LoadedVisitor it : typeElementVisitors) { diff --git a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/annotation/GroovyAnnotationMetadataBuilder.java b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/annotation/GroovyAnnotationMetadataBuilder.java index 0edbd8e7ea1..914a5d089e4 100644 --- a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/annotation/GroovyAnnotationMetadataBuilder.java +++ b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/annotation/GroovyAnnotationMetadataBuilder.java @@ -129,7 +129,8 @@ protected boolean isValidationRequired(AnnotatedNode member) { @Override protected boolean isExcludedAnnotation(@NonNull AnnotatedNode element, @NonNull String annotationName) { - if (element instanceof ClassNode classNode && classNode.isAnnotationDefinition() && annotationName.startsWith("java.lang.annotation")) { + if (element instanceof ClassNode classNode && classNode.isAnnotationDefinition() + && (annotationName.startsWith("java.lang.annotation") || annotationName.startsWith("org.codehaus.groovy.transform"))) { return false; } else { return super.isExcludedAnnotation(element, annotationName); @@ -271,10 +272,8 @@ protected String getElementName(AnnotatedNode element) { @Override protected List getAnnotationsForType(AnnotatedNode element) { List annotations = element.getAnnotations(); - List typeAnnotations = element instanceof ClassNode classNode ? classNode.getTypeAnnotations() : Collections.emptyList(); - List expanded = new ArrayList<>(annotations.size() + typeAnnotations.size()); + List expanded = new ArrayList<>(annotations.size()); expandAnnotations(annotations, expanded); - expandAnnotations(typeAnnotations, expanded); return expanded; } diff --git a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/annotation/GroovyElementAnnotationMetadataFactory.java b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/annotation/GroovyElementAnnotationMetadataFactory.java index fe261a3ef3a..0e0ae6cf40b 100644 --- a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/annotation/GroovyElementAnnotationMetadataFactory.java +++ b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/annotation/GroovyElementAnnotationMetadataFactory.java @@ -15,18 +15,21 @@ */ package io.micronaut.ast.groovy.annotation; -import io.micronaut.ast.groovy.utils.ExtendedParameter; +import io.micronaut.ast.groovy.visitor.AbstractGroovyElement; import io.micronaut.ast.groovy.visitor.GroovyNativeElement; +import io.micronaut.core.util.CollectionUtils; import io.micronaut.inject.annotation.AbstractAnnotationMetadataBuilder; import io.micronaut.inject.ast.ClassElement; -import io.micronaut.inject.ast.FieldElement; -import io.micronaut.inject.ast.MethodElement; -import io.micronaut.inject.ast.PackageElement; -import io.micronaut.inject.ast.ParameterElement; +import io.micronaut.inject.ast.Element; +import io.micronaut.inject.ast.GenericPlaceholderElement; +import io.micronaut.inject.ast.WildcardElement; import io.micronaut.inject.ast.annotation.AbstractElementAnnotationMetadataFactory; import io.micronaut.inject.ast.annotation.ElementAnnotationMetadataFactory; import org.codehaus.groovy.ast.AnnotatedNode; import org.codehaus.groovy.ast.AnnotationNode; +import org.codehaus.groovy.ast.ClassNode; + +import java.util.List; /** * Groovy element annotation metadata factory. @@ -46,33 +49,34 @@ public ElementAnnotationMetadataFactory readOnly() { } @Override - protected AbstractAnnotationMetadataBuilder.CachedAnnotationMetadata lookupForPackage(PackageElement packageElement) { - GroovyNativeElement groovyNativeElement = (GroovyNativeElement) packageElement.getNativeType(); - return metadataBuilder.lookupOrBuild(groovyNativeElement, groovyNativeElement.annotatedNode(), true); + protected AnnotatedNode getNativeElement(Element element) { + return ((AbstractGroovyElement) element).getNativeType().annotatedNode(); } @Override - protected AbstractAnnotationMetadataBuilder.CachedAnnotationMetadata lookupForParameter(ParameterElement parameterElement) { - GroovyNativeElement.Parameter parameter = (GroovyNativeElement.Parameter) parameterElement.getNativeType(); - return metadataBuilder.lookupOrBuild(parameter, new ExtendedParameter(parameter.methodNode(), parameter.annotatedNode()), false); + protected AbstractAnnotationMetadataBuilder.CachedAnnotationMetadata lookupTypeAnnotationsForClass(ClassElement classElement) { + GroovyNativeElement clazz = (GroovyNativeElement) classElement.getNativeType(); + return metadataBuilder.lookupOrBuild(clazz, getTypeAnnotationsOnly((ClassNode) clazz.annotatedNode())); } @Override - protected AbstractAnnotationMetadataBuilder.CachedAnnotationMetadata lookupForField(FieldElement fieldElement) { - GroovyNativeElement groovyNativeElement = (GroovyNativeElement) fieldElement.getNativeType(); - return metadataBuilder.lookupOrBuild(groovyNativeElement, groovyNativeElement.annotatedNode(), false); + protected AbstractAnnotationMetadataBuilder.CachedAnnotationMetadata lookupTypeAnnotationsForGenericPlaceholder(GenericPlaceholderElement placeholderElement) { + GroovyNativeElement.Placeholder placeholder = (GroovyNativeElement.Placeholder) placeholderElement.getGenericNativeType(); + return metadataBuilder.lookupOrBuild(placeholder, getTypeAnnotationsOnly(placeholder.annotatedNode())); } @Override - protected AbstractAnnotationMetadataBuilder.CachedAnnotationMetadata lookupForMethod(MethodElement methodElement) { - GroovyNativeElement groovyNativeElement = (GroovyNativeElement) methodElement.getNativeType(); - return metadataBuilder.lookupOrBuild(groovyNativeElement, groovyNativeElement.annotatedNode(), false); + protected AbstractAnnotationMetadataBuilder.CachedAnnotationMetadata lookupTypeAnnotationsForWildcard(WildcardElement wildcardElement) { + GroovyNativeElement wildcard = (GroovyNativeElement) wildcardElement.getGenericNativeType(); + return metadataBuilder.lookupOrBuild(wildcard, getTypeAnnotationsOnly((ClassNode) wildcard.annotatedNode())); } - @Override - protected AbstractAnnotationMetadataBuilder.CachedAnnotationMetadata lookupForClass(ClassElement classElement) { - GroovyNativeElement groovyNativeElement = (GroovyNativeElement) classElement.getNativeType(); - return metadataBuilder.lookupOrBuild(groovyNativeElement, groovyNativeElement.annotatedNode(), true); + private AnnotatedNode getTypeAnnotationsOnly(ClassNode classNode) { + AnnotatedNode annotatedNode = new AnnotatedNode(); + List typeAnnotations = classNode.getTypeAnnotations(); + if (CollectionUtils.isNotEmpty(typeAnnotations)) { + annotatedNode.addAnnotations(typeAnnotations); + } + return annotatedNode; } - } diff --git a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/AbstractGroovyElement.java b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/AbstractGroovyElement.java index 8dff43095a4..d4499df2cc0 100644 --- a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/AbstractGroovyElement.java +++ b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/AbstractGroovyElement.java @@ -19,6 +19,7 @@ import io.micronaut.core.annotation.AnnotationMetadata; import io.micronaut.core.annotation.AnnotationValue; import io.micronaut.core.annotation.AnnotationValueBuilder; +import io.micronaut.core.annotation.Internal; import io.micronaut.core.annotation.NonNull; import io.micronaut.core.annotation.Nullable; import io.micronaut.core.util.ArrayUtils; @@ -31,7 +32,6 @@ import io.micronaut.inject.ast.WildcardElement; import io.micronaut.inject.ast.annotation.ElementAnnotationMetadata; import io.micronaut.inject.ast.annotation.ElementAnnotationMetadataFactory; -import io.micronaut.inject.ast.annotation.ElementMutableAnnotationMetadataDelegate; import io.micronaut.inject.ast.annotation.MutableAnnotationMetadataDelegate; import org.codehaus.groovy.ast.AnnotatedNode; import org.codehaus.groovy.ast.ClassHelper; @@ -64,8 +64,8 @@ * @author Denis Stepanov * @since 1.1 */ - -public abstract class AbstractGroovyElement implements Element, ElementMutableAnnotationMetadataDelegate { +@Internal +public abstract class AbstractGroovyElement implements Element { private static final Pattern JAVADOC_PATTERN = Pattern.compile("(/\\s*\\*\\*)|\\s*\\*|(\\s*[*/])"); @@ -95,62 +95,74 @@ protected AbstractGroovyElement(GroovyVisitorContext visitorContext, } @Override - public Element annotate(String annotationType, Consumer> consumer) { - return ElementMutableAnnotationMetadataDelegate.super.annotate(annotationType, consumer); + public io.micronaut.inject.ast.Element annotate(String annotationType, Consumer> consumer) { + getAnnotationMetadataToWrite().annotate(annotationType, consumer); + return this; } @Override - public Element removeAnnotation(String annotationType) { - return ElementMutableAnnotationMetadataDelegate.super.removeAnnotation(annotationType); + public io.micronaut.inject.ast.Element removeAnnotation(String annotationType) { + getAnnotationMetadataToWrite().removeAnnotation(annotationType); + return this; } @Override - public Element removeAnnotation(Class annotationType) { - return ElementMutableAnnotationMetadataDelegate.super.removeAnnotation(annotationType); + public io.micronaut.inject.ast.Element removeAnnotation(Class annotationType) { + getAnnotationMetadataToWrite().removeAnnotation(annotationType); + return this; } @Override - public Element removeAnnotationIf(Predicate> predicate) { - return ElementMutableAnnotationMetadataDelegate.super.removeAnnotationIf(predicate); + public io.micronaut.inject.ast.Element removeAnnotationIf(Predicate> predicate) { + getAnnotationMetadataToWrite().removeAnnotationIf(predicate); + return this; } @Override - public Element removeStereotype(String annotationType) { - return ElementMutableAnnotationMetadataDelegate.super.removeStereotype(annotationType); + public io.micronaut.inject.ast.Element removeStereotype(String annotationType) { + getAnnotationMetadataToWrite().removeStereotype(annotationType); + return this; } @Override - public Element removeStereotype(Class annotationType) { - return ElementMutableAnnotationMetadataDelegate.super.removeStereotype(annotationType); + public io.micronaut.inject.ast.Element removeStereotype(Class annotationType) { + getAnnotationMetadataToWrite().removeStereotype(annotationType); + return this; } @Override - public Element annotate(String annotationType) { - return ElementMutableAnnotationMetadataDelegate.super.annotate(annotationType); + public io.micronaut.inject.ast.Element annotate(String annotationType) { + getAnnotationMetadataToWrite().annotate(annotationType); + return this; } @Override - public Element annotate(Class annotationType, Consumer> consumer) { - return ElementMutableAnnotationMetadataDelegate.super.annotate(annotationType, consumer); + public io.micronaut.inject.ast.Element annotate(Class annotationType, Consumer> consumer) { + getAnnotationMetadataToWrite().annotate(annotationType, consumer); + return this; } @Override - public Element annotate(Class annotationType) { - return ElementMutableAnnotationMetadataDelegate.super.annotate(annotationType); + public io.micronaut.inject.ast.Element annotate(Class annotationType) { + getAnnotationMetadataToWrite().annotate(annotationType); + return this; } @Override - public Element annotate(AnnotationValue annotationValue) { - return ElementMutableAnnotationMetadataDelegate.super.annotate(annotationValue); + public io.micronaut.inject.ast.Element annotate(AnnotationValue annotationValue) { + getAnnotationMetadataToWrite().annotate(annotationValue); + return this; } @Override - public Element getReturnInstance() { - return this; + public AnnotationMetadata getAnnotationMetadata() { + return getElementAnnotationMetadata(); } - @Override - public MutableAnnotationMetadataDelegate getAnnotationMetadata() { + /** + * @return The element annotation metadata + */ + protected ElementAnnotationMetadata getElementAnnotationMetadata() { if (elementAnnotationMetadata == null) { if (presetAnnotationMetadata == null) { elementAnnotationMetadata = elementAnnotationMetadataFactory.build(this); @@ -161,6 +173,14 @@ public MutableAnnotationMetadataDelegate getAnnotationMetadata() { return elementAnnotationMetadata; } + /** + * Get annotation metadata to add or remove annotations. + * @return The annotation metadata to write + */ + protected MutableAnnotationMetadataDelegate getAnnotationMetadataToWrite() { + return getElementAnnotationMetadata(); + } + /** * Constructs this element by invoking the constructor. * @@ -316,59 +336,76 @@ private ClassElement newClassElement(@Nullable GroovyNativeElement declaredEleme } @NonNull - private ClassElement resolvePlaceholder(GroovyNativeElement declaredElement, + private ClassElement resolvePlaceholder(GroovyNativeElement owner, AnnotatedNode genericsOwner, GenericsType genericsType, GenericsType redirectType, Map parentTypeArguments, Set visitedTypes, boolean isRawType) { + ClassNode placeholderClassNode = genericsType.getType(); String variableName = genericsType.getName(); - GroovyNativeElement groovyPlaceholderNativeElement - = new GroovyNativeElement.Placeholder(genericsType.getType(), declaredElement, variableName); - ClassElement boundVariable = parentTypeArguments.get(variableName); - if (boundVariable != null) { - if (boundVariable instanceof WildcardElement wildcardElement) { + + ClassElement resolvedBound = parentTypeArguments.get(variableName); + List bounds = null; + Element declaredElement = this; + GroovyClassElement resolved = null; + int arrayDimensions = 0; + if (resolvedBound != null) { + if (resolvedBound instanceof WildcardElement wildcardElement) { if (wildcardElement.isBounded()) { return wildcardElement; } + } else if (resolvedBound instanceof GroovyGenericPlaceholderElement groovyGenericPlaceholderElement) { + bounds = groovyGenericPlaceholderElement.getBounds(); + declaredElement = groovyGenericPlaceholderElement.getRequiredDeclaringElement(); + resolved = groovyGenericPlaceholderElement.getResolvedInternal(); + arrayDimensions = groovyGenericPlaceholderElement.getArrayDimensions(); + isRawType = groovyGenericPlaceholderElement.isRawType(); + } else if (resolvedBound instanceof GroovyClassElement resolvedClassElement) { + resolved = resolvedClassElement; + arrayDimensions = resolved.getArrayDimensions(); + isRawType = resolved.isRawType(); } else { - return boundVariable; + // Most likely primitive array + return resolvedBound; } } - List classNodeBounds = new ArrayList<>(); - addBounds(genericsType, classNodeBounds); - if (genericsType != redirectType) { - addBounds(redirectType, classNodeBounds); - } - - PlaceholderEntry placeholderEntry = new PlaceholderEntry(genericsOwner, variableName); - boolean alreadyVisitedPlaceholder = visitedTypes.contains(placeholderEntry); - if (!alreadyVisitedPlaceholder) { - visitedTypes.add(placeholderEntry); - } - - List bounds = classNodeBounds - .stream() - .map(classNode -> { - if (alreadyVisitedPlaceholder && classNode.isGenericsPlaceHolder()) { - classNode = classNode.redirect(); - } - return classNode; - }) - .filter(classNode -> !alreadyVisitedPlaceholder || !classNode.isGenericsPlaceHolder()) - .map(classNode -> { - // Strip declared type arguments and replace with an Object to prevent recursion - boolean stripTypeArguments = alreadyVisitedPlaceholder; - - return (GroovyClassElement) newClassElement(groovyPlaceholderNativeElement, classNode, parentTypeArguments, visitedTypes, true, isRawType, stripTypeArguments); - }) - .toList(); + GroovyNativeElement groovyPlaceholderNativeElement = new GroovyNativeElement.Placeholder(placeholderClassNode, owner, variableName); + if (bounds == null) { + List classNodeBounds = new ArrayList<>(); + addBounds(genericsType, classNodeBounds); + if (genericsType != redirectType) { + addBounds(redirectType, classNodeBounds); + } - if (bounds.isEmpty()) { - bounds = Collections.singletonList((GroovyClassElement) getObjectClassElement()); + PlaceholderEntry placeholderEntry = new PlaceholderEntry(genericsOwner, variableName); + boolean alreadyVisitedPlaceholder = visitedTypes.contains(placeholderEntry); + if (!alreadyVisitedPlaceholder) { + visitedTypes.add(placeholderEntry); + } + boolean finalIsRawType = isRawType; + bounds = classNodeBounds + .stream() + .map(classNode -> { + if (alreadyVisitedPlaceholder && classNode.isGenericsPlaceHolder()) { + classNode = classNode.redirect(); + } + return classNode; + }) + .filter(classNode -> !alreadyVisitedPlaceholder || !classNode.isGenericsPlaceHolder()) + .map(classNode -> { + // Strip declared type arguments and replace with an Object to prevent recursion + boolean stripTypeArguments = alreadyVisitedPlaceholder; + + return (GroovyClassElement) newClassElement(groovyPlaceholderNativeElement, classNode, parentTypeArguments, visitedTypes, true, finalIsRawType, stripTypeArguments); + }) + .toList(); + if (bounds.isEmpty()) { + bounds = Collections.singletonList((GroovyClassElement) getObjectClassElement()); + } } - return new GroovyGenericPlaceholderElement(visitorContext, this, groovyPlaceholderNativeElement, bounds, isRawType, variableName); + return new GroovyGenericPlaceholderElement(visitorContext, declaredElement, groovyPlaceholderNativeElement, resolved, bounds, arrayDimensions, isRawType, variableName); } private static void addBounds(GenericsType genericsType, List classNodeBounds) { @@ -430,11 +467,9 @@ private ClassElement resolveWildcard(GroovyNativeElement declaredElement, // TODO: Support primitives for wildcards (? extends byte[]) return upperType; } + GroovyNativeElement wildcardNativeElement = new GroovyNativeElement.ClassWithOwner(genericsType.getType(), declaredElement); return new GroovyWildcardElement( - (GroovyClassElement) upperType, - upperBoundsAsElements.stream().map(GroovyClassElement.class::cast).toList(), - lowerBoundsAsElements.stream().map(GroovyClassElement.class::cast).toList(), - elementAnnotationMetadataFactory + wildcardNativeElement, upperBoundsAsElements.stream().map(GroovyClassElement.class::cast).toList(), lowerBoundsAsElements.stream().map(GroovyClassElement.class::cast).toList(), elementAnnotationMetadataFactory, (GroovyClassElement) upperType ); } diff --git a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyAnnotationElement.java b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyAnnotationElement.java index 49bbbff88e6..c16885f6c07 100644 --- a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyAnnotationElement.java +++ b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyAnnotationElement.java @@ -15,6 +15,7 @@ */ package io.micronaut.ast.groovy.visitor; +import io.micronaut.core.annotation.Internal; import io.micronaut.inject.ast.AnnotationElement; import io.micronaut.inject.ast.annotation.ElementAnnotationMetadataFactory; @@ -24,6 +25,7 @@ * @since 3.1.0 * @author graemerocher */ +@Internal final class GroovyAnnotationElement extends GroovyClassElement implements AnnotationElement { public GroovyAnnotationElement(GroovyVisitorContext visitorContext, diff --git a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyBeanDefinitionBuilder.java b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyBeanDefinitionBuilder.java index 110e6ee139c..bcfada416dc 100644 --- a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyBeanDefinitionBuilder.java +++ b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyBeanDefinitionBuilder.java @@ -28,16 +28,15 @@ import io.micronaut.inject.annotation.AnnotationMetadataHierarchy; import io.micronaut.inject.ast.ClassElement; import io.micronaut.inject.ast.Element; -import io.micronaut.inject.ast.annotation.ElementAnnotationMetadataFactory; import io.micronaut.inject.ast.FieldElement; import io.micronaut.inject.ast.MethodElement; import io.micronaut.inject.ast.TypedElement; +import io.micronaut.inject.ast.annotation.ElementAnnotationMetadataFactory; import io.micronaut.inject.ast.beans.BeanParameterElement; import io.micronaut.inject.configuration.ConfigurationMetadataBuilder; import io.micronaut.inject.writer.AbstractBeanDefinitionBuilder; import io.micronaut.inject.writer.BeanDefinitionVisitor; import io.micronaut.inject.writer.BeanDefinitionWriter; -import org.codehaus.groovy.ast.ClassNode; import java.lang.annotation.Annotation; import java.util.function.BiConsumer; @@ -229,10 +228,9 @@ protected void removeAnnotation(AnnotationMetadata annotationMetadata, String an } private ClassElement resolveParent(ClassElement parentType, GroovyElementFactory elementFactory) { - Object nativeType = parentType.getNativeType(); ClassElement resolvedParent = parentType; - if (nativeType instanceof ClassNode) { - resolvedParent = elementFactory.newClassElement((ClassNode) nativeType, elementAnnotationMetadataFactory); + if (parentType instanceof GroovyClassElement groovyClassElement) { + resolvedParent = elementFactory.newClassElement(groovyClassElement.classNode, elementAnnotationMetadataFactory); } return resolvedParent; } diff --git a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyClassElement.java b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyClassElement.java index 5cd1ef78e43..49cbc3edefa 100644 --- a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyClassElement.java +++ b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyClassElement.java @@ -45,7 +45,9 @@ import io.micronaut.inject.ast.PrimitiveElement; import io.micronaut.inject.ast.PropertyElement; import io.micronaut.inject.ast.PropertyElementQuery; +import io.micronaut.inject.ast.annotation.ElementAnnotationMetadata; import io.micronaut.inject.ast.annotation.ElementAnnotationMetadataFactory; +import io.micronaut.inject.ast.annotation.MutableAnnotationMetadataDelegate; import io.micronaut.inject.ast.utils.AstBeanPropertiesUtils; import io.micronaut.inject.ast.utils.EnclosedElementsQuery; import org.codehaus.groovy.ast.AnnotatedNode; @@ -123,8 +125,14 @@ public class GroovyClassElement extends AbstractGroovyElement implements Arrayab private final boolean isTypeVar; private List properties; private List nativeProperties; + @Nullable + private ElementAnnotationMetadata elementTypeAnnotationMetadata; + @Nullable + private ClassElement theType; private final GroovyEnclosedElementsQuery groovyEnclosedElementsQuery = new GroovyEnclosedElementsQuery(false); private final GroovyEnclosedElementsQuery groovySourceEnclosedElementsQuery = new GroovyEnclosedElementsQuery(true); + @Nullable + private AnnotationMetadata annotationMetadata; /** * @param visitorContext The visitor context @@ -182,9 +190,23 @@ protected GroovyClassElement copyConstructor() { } @Override - protected void copyValues(AbstractGroovyElement element) { - super.copyValues(element); - ((GroovyClassElement) element).resolvedTypeArguments = resolvedTypeArguments; + public AnnotationMetadata getAnnotationMetadata() { + if (annotationMetadata == null) { + if (getNativeType() instanceof GroovyNativeElement.ClassWithOwner) { + annotationMetadata = new AnnotationMetadataHierarchy(true, super.getAnnotationMetadata(), getTypeAnnotationMetadata()); + } else { + annotationMetadata = super.getAnnotationMetadata(); + } + } + return annotationMetadata; + } + + @Override + protected MutableAnnotationMetadataDelegate getAnnotationMetadataToWrite() { + if (getNativeType() instanceof GroovyNativeElement.ClassWithOwner) { + return getTypeAnnotationMetadata(); + } + return super.getAnnotationMetadataToWrite(); } @Override @@ -194,9 +216,7 @@ public ClassElement withAnnotationMetadata(AnnotationMetadata annotationMetadata @Override public ClassElement withTypeArguments(Map typeArguments) { - GroovyClassElement groovyClassElement = new GroovyClassElement(visitorContext, getNativeType(), elementAnnotationMetadataFactory, resolvedTypeArguments, arrayDimensions); - groovyClassElement.resolvedTypeArguments = typeArguments; - return groovyClassElement; + return new GroovyClassElement(visitorContext, getNativeType(), elementAnnotationMetadataFactory, typeArguments, arrayDimensions); } @NonNull @@ -278,7 +298,10 @@ public boolean isPrimitive() { public Collection getInterfaces() { final ClassNode[] interfaces = classNode.getInterfaces(); if (ArrayUtils.isNotEmpty(interfaces)) { - return Arrays.stream(interfaces).map(inf -> newClassElement(inf, getTypeArguments())).toList(); + return Arrays.stream(interfaces) + .filter(inf -> !inf.getName().equals("groovy.lang.GroovyObject")) + .map(inf -> newClassElement(inf, getTypeArguments())) + .toList(); } return Collections.emptyList(); } @@ -607,6 +630,26 @@ private List getPropertyNodes() { return propertyElements; } + @Override + public ClassElement getType() { + if (theType == null) { + GroovyNativeElement nativeType = getNativeType(); + ClassNode thisClassNode = (ClassNode) nativeType.annotatedNode(); + ClassNode redirect = thisClassNode.redirect(); + // This should eliminate type annotations + theType = new GroovyClassElement(visitorContext, new GroovyNativeElement.Class(redirect), elementAnnotationMetadataFactory, resolvedTypeArguments, arrayDimensions, isTypeVar); + } + return theType; + } + + @Override + public MutableAnnotationMetadataDelegate getTypeAnnotationMetadata() { + if (elementTypeAnnotationMetadata == null) { + elementTypeAnnotationMetadata = elementAnnotationMetadataFactory.buildTypeAnnotations(this); + } + return elementTypeAnnotationMetadata; + } + /** * The groovy elements query helper. */ diff --git a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyClassWriterOutputVisitor.java b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyClassWriterOutputVisitor.java index 92529af025f..9c6a3133abd 100644 --- a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyClassWriterOutputVisitor.java +++ b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyClassWriterOutputVisitor.java @@ -15,13 +15,8 @@ */ package io.micronaut.ast.groovy.visitor; -import java.io.ByteArrayOutputStream; -import java.io.File; -import java.io.IOException; -import java.io.OutputStream; -import java.util.Optional; - import io.micronaut.ast.groovy.utils.InMemoryByteCodeGroovyClassLoader; +import io.micronaut.core.annotation.Internal; import io.micronaut.core.annotation.Nullable; import io.micronaut.inject.ast.Element; import io.micronaut.inject.writer.ClassWriterOutputVisitor; @@ -29,6 +24,13 @@ import io.micronaut.inject.writer.GeneratedFile; import org.codehaus.groovy.control.CompilationUnit; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.io.OutputStream; +import java.util.Optional; + +@Internal class GroovyClassWriterOutputVisitor implements ClassWriterOutputVisitor { private final CompilationUnit compilationUnit; diff --git a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyConstructorElement.java b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyConstructorElement.java index d30d19f8549..90bd8776e2b 100644 --- a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyConstructorElement.java +++ b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyConstructorElement.java @@ -16,6 +16,7 @@ package io.micronaut.ast.groovy.visitor; import io.micronaut.core.annotation.AnnotationMetadata; +import io.micronaut.core.annotation.Internal; import io.micronaut.inject.ast.ConstructorElement; import io.micronaut.inject.ast.annotation.ElementAnnotationMetadataFactory; import org.codehaus.groovy.ast.ConstructorNode; @@ -26,6 +27,7 @@ * @author graemerocher * @since 1.0 */ +@Internal public class GroovyConstructorElement extends GroovyMethodElement implements ConstructorElement { /** * @param owningType The owning class diff --git a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyElementFactory.java b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyElementFactory.java index a345bf14e09..2f30eace595 100644 --- a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyElementFactory.java +++ b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyElementFactory.java @@ -15,6 +15,7 @@ */ package io.micronaut.ast.groovy.visitor; +import io.micronaut.core.annotation.Internal; import io.micronaut.core.annotation.NonNull; import io.micronaut.core.util.CollectionUtils; import io.micronaut.inject.ast.ClassElement; @@ -40,6 +41,7 @@ * @author graemerocher * @since 2.3.0 */ +@Internal public class GroovyElementFactory implements ElementFactory { private final GroovyVisitorContext visitorContext; diff --git a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyEnumElement.java b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyEnumElement.java index 598260f26a6..a14492ce16f 100644 --- a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyEnumElement.java +++ b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyEnumElement.java @@ -15,6 +15,7 @@ */ package io.micronaut.ast.groovy.visitor; +import io.micronaut.core.annotation.Internal; import io.micronaut.inject.ast.ClassElement; import io.micronaut.inject.ast.EnumConstantElement; import io.micronaut.inject.ast.EnumElement; @@ -31,6 +32,7 @@ * @author graemerocher * @since 1.0 */ +@Internal class GroovyEnumElement extends GroovyClassElement implements EnumElement { protected List enumConstants; diff --git a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyFieldElement.java b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyFieldElement.java index afb5db2b951..9cd00eb9894 100644 --- a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyFieldElement.java +++ b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyFieldElement.java @@ -16,6 +16,7 @@ package io.micronaut.ast.groovy.visitor; import io.micronaut.core.annotation.AnnotationMetadata; +import io.micronaut.core.annotation.Internal; import io.micronaut.core.annotation.NonNull; import io.micronaut.inject.ast.ClassElement; import io.micronaut.inject.ast.ElementModifier; @@ -35,6 +36,7 @@ * @author James Kleeh * @since 1.0 */ +@Internal public class GroovyFieldElement extends AbstractGroovyElement implements FieldElement { private final GroovyClassElement owningType; diff --git a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyGenericPlaceholderElement.java b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyGenericPlaceholderElement.java index f5c6a3a50cc..a50cbb70b8d 100644 --- a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyGenericPlaceholderElement.java +++ b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyGenericPlaceholderElement.java @@ -15,14 +15,20 @@ */ package io.micronaut.ast.groovy.visitor; +import io.micronaut.core.annotation.AnnotationMetadata; import io.micronaut.core.annotation.Internal; import io.micronaut.core.annotation.NonNull; +import io.micronaut.core.annotation.Nullable; +import io.micronaut.inject.annotation.AnnotationMetadataHierarchy; import io.micronaut.inject.ast.ClassElement; import io.micronaut.inject.ast.Element; import io.micronaut.inject.ast.GenericPlaceholderElement; import io.micronaut.inject.ast.WildcardElement; +import io.micronaut.inject.ast.annotation.AbstractElementAnnotationMetadata; +import io.micronaut.inject.ast.annotation.ElementAnnotationMetadata; +import io.micronaut.inject.ast.annotation.MutableAnnotationMetadataDelegate; -import java.util.Collections; +import java.util.ArrayList; import java.util.List; import java.util.Objects; import java.util.Optional; @@ -40,37 +46,111 @@ final class GroovyGenericPlaceholderElement extends GroovyClassElement implement private final GroovyNativeElement placeholderNativeElement; private final Element declaringElement; private final String variableName; - private final GroovyClassElement mostUpper; + private final GroovyClassElement resolved; private final List bounds; private final boolean rawType; + private final ElementAnnotationMetadata typeAnnotationMetadata; + @Nullable + private ElementAnnotationMetadata genericTypeAnnotationMetadata; GroovyGenericPlaceholderElement(GroovyVisitorContext visitorContext, Element declaringElement, GroovyNativeElement placeholderNativeElement, + @Nullable + GroovyClassElement resolved, List bounds, + int arrayDimensions, boolean rawType, String variableName) { - this(visitorContext, declaringElement, placeholderNativeElement, variableName, WildcardElement.findUpperType(bounds, Collections.emptyList()), bounds, 0, rawType); + this(visitorContext, declaringElement, placeholderNativeElement, variableName, resolved, bounds, selectClassElementRepresentingThisPlaceholder(resolved, bounds), arrayDimensions, rawType); } GroovyGenericPlaceholderElement(GroovyVisitorContext visitorContext, Element declaringElement, GroovyNativeElement placeholderNativeElement, - String variableName, GroovyClassElement mostUpper, + String variableName, + @Nullable + GroovyClassElement resolved, List bounds, + GroovyClassElement classElementRepresentingThisPlaceholder, int arrayDimensions, boolean rawType) { - super(visitorContext, mostUpper.getNativeType(), mostUpper.elementAnnotationMetadataFactory, mostUpper.resolvedTypeArguments, arrayDimensions); + super(visitorContext, + classElementRepresentingThisPlaceholder.getNativeType(), + classElementRepresentingThisPlaceholder.elementAnnotationMetadataFactory, + classElementRepresentingThisPlaceholder.resolvedTypeArguments, + arrayDimensions); this.declaringElement = declaringElement; this.placeholderNativeElement = placeholderNativeElement; this.variableName = variableName; - this.mostUpper = mostUpper; + this.resolved = resolved; this.bounds = bounds; this.rawType = rawType; + typeAnnotationMetadata = new AbstractElementAnnotationMetadata() { + + AnnotationMetadata annotationMetadata; + + public AnnotationMetadata getReturnInstance() { + return getAnnotationMetadata(); + } + + @Override + protected MutableAnnotationMetadataDelegate getAnnotationMetadataToWrite() { + return getGenericTypeAnnotationMetadata(); + } + + @Override + public AnnotationMetadata getAnnotationMetadata() { + if (annotationMetadata == null) { + List allAnnotationMetadata = new ArrayList<>(); + getBounds().forEach(ce -> allAnnotationMetadata.add(ce.getTypeAnnotationMetadata())); + allAnnotationMetadata.add(GroovyGenericPlaceholderElement.super.getTypeAnnotationMetadata()); + allAnnotationMetadata.add(getGenericTypeAnnotationMetadata()); + annotationMetadata = new AnnotationMetadataHierarchy(true, allAnnotationMetadata.toArray(AnnotationMetadata[]::new)); + } + return annotationMetadata; + } + }; + } + + private static GroovyClassElement selectClassElementRepresentingThisPlaceholder(@Nullable GroovyClassElement resolved, + @NonNull List bounds) { + if (resolved != null) { + return resolved; + } + return WildcardElement.findUpperType(bounds, bounds); } @Override - public Object getGenericNativeType() { + public boolean isTypeVariable() { + return true; + } + + @Override + protected MutableAnnotationMetadataDelegate getAnnotationMetadataToWrite() { + return getGenericTypeAnnotationMetadata(); + } + + @Override + public MutableAnnotationMetadataDelegate getGenericTypeAnnotationMetadata() { + if (genericTypeAnnotationMetadata == null) { + genericTypeAnnotationMetadata = elementAnnotationMetadataFactory.buildGenericTypeAnnotations(this); + } + return genericTypeAnnotationMetadata; + } + + @Override + public MutableAnnotationMetadataDelegate getTypeAnnotationMetadata() { + return typeAnnotationMetadata; + } + + @Override + public AnnotationMetadata getAnnotationMetadata() { + return new AnnotationMetadataHierarchy(true, super.getAnnotationMetadata(), getGenericTypeAnnotationMetadata()); + } + + @Override + public GroovyNativeElement getGenericNativeType() { return placeholderNativeElement; } @@ -81,12 +161,12 @@ public boolean isRawType() { @Override protected GroovyClassElement copyConstructor() { - return new GroovyGenericPlaceholderElement(visitorContext, declaringElement, placeholderNativeElement, variableName, mostUpper, bounds, getArrayDimensions(), rawType); + return new GroovyGenericPlaceholderElement(visitorContext, declaringElement, placeholderNativeElement, variableName, resolved, bounds, selectClassElementRepresentingThisPlaceholder(resolved, bounds), getArrayDimensions(), rawType); } @NonNull @Override - public List getBounds() { + public List getBounds() { return bounds; } @@ -103,7 +183,7 @@ public Optional getDeclaringElement() { @Override public ClassElement withArrayDimensions(int arrayDimensions) { - return new GroovyGenericPlaceholderElement(visitorContext, declaringElement, placeholderNativeElement, variableName, mostUpper, bounds, arrayDimensions, rawType); + return new GroovyGenericPlaceholderElement(visitorContext, declaringElement, placeholderNativeElement, variableName, resolved, bounds, selectClassElementRepresentingThisPlaceholder(resolved, bounds), arrayDimensions, rawType); } @Override @@ -111,4 +191,14 @@ public ClassElement foldBoundGenericTypes(@NonNull Function getResolved() { + return Optional.ofNullable(resolved); + } + + @Nullable + public GroovyClassElement getResolvedInternal() { + return resolved; + } } diff --git a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyMethodElement.java b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyMethodElement.java index 9bab314ec6a..8de53d114ff 100644 --- a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyMethodElement.java +++ b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyMethodElement.java @@ -16,7 +16,9 @@ package io.micronaut.ast.groovy.visitor; import io.micronaut.core.annotation.AnnotationMetadata; +import io.micronaut.core.annotation.Internal; import io.micronaut.core.annotation.NonNull; +import io.micronaut.core.annotation.Nullable; import io.micronaut.core.util.ArrayUtils; import io.micronaut.inject.ast.ClassElement; import io.micronaut.inject.ast.ElementModifier; @@ -34,7 +36,6 @@ import java.util.List; import java.util.Map; import java.util.Set; -import java.util.stream.Collectors; /** * A method element returning data from a {@link MethodNode}. @@ -42,6 +43,7 @@ * @author James Kleeh * @since 1.0 */ +@Internal public class GroovyMethodElement extends AbstractGroovyElement implements MethodElement { protected ParameterElement[] parameters; @@ -50,6 +52,10 @@ public class GroovyMethodElement extends AbstractGroovyElement implements Method private ClassElement declaringType; private Map declaredTypeArguments; private Map typeArguments; + @Nullable + private ClassElement returnType; + @Nullable + private ClassElement genericReturnType; /** * @param owningType The owning type @@ -68,10 +74,6 @@ public class GroovyMethodElement extends AbstractGroovyElement implements Method this.owningType = owningType; } - public final MethodNode getMethodNode() { - return methodNode; - } - @Override protected AbstractGroovyElement copyConstructor() { return new GroovyMethodElement(owningType, visitorContext, getNativeType(), methodNode, elementAnnotationMetadataFactory); @@ -244,7 +246,7 @@ public List getDeclaredTypeVariables() { } return Arrays.stream(genericsTypes) .map(gt -> (GenericPlaceholderElement) newClassElement(gt)) - .collect(Collectors.toList()); + .toList(); } } diff --git a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyNativeElement.java b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyNativeElement.java index b7d7e03980f..0702b0f65b6 100644 --- a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyNativeElement.java +++ b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyNativeElement.java @@ -15,6 +15,7 @@ */ package io.micronaut.ast.groovy.visitor; +import io.micronaut.core.annotation.Internal; import org.codehaus.groovy.ast.AnnotatedNode; import org.codehaus.groovy.ast.ClassNode; import org.codehaus.groovy.ast.FieldNode; @@ -27,6 +28,7 @@ * @author Denis Stepanov * @since 4.0.0 */ +@Internal public sealed interface GroovyNativeElement { /** diff --git a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyPropertyElement.java b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyPropertyElement.java index 6933af72c98..b13c753ee41 100644 --- a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyPropertyElement.java +++ b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyPropertyElement.java @@ -23,13 +23,12 @@ import io.micronaut.core.annotation.Nullable; import io.micronaut.inject.annotation.AnnotationMetadataHierarchy; import io.micronaut.inject.ast.ClassElement; -import io.micronaut.inject.ast.Element; import io.micronaut.inject.ast.FieldElement; import io.micronaut.inject.ast.MemberElement; import io.micronaut.inject.ast.MethodElement; import io.micronaut.inject.ast.PropertyElement; +import io.micronaut.inject.ast.annotation.ElementAnnotationMetadata; import io.micronaut.inject.ast.annotation.ElementAnnotationMetadataFactory; -import io.micronaut.inject.ast.annotation.MutableAnnotationMetadataDelegate; import java.lang.annotation.Annotation; import java.util.ArrayList; @@ -59,7 +58,7 @@ final class GroovyPropertyElement extends AbstractGroovyElement implements Prope @Nullable private final FieldElement field; private final boolean excluded; - private final MutableAnnotationMetadataDelegate annotationMetadata; + private final ElementAnnotationMetadata annotationMetadata; GroovyPropertyElement(GroovyVisitorContext visitorContext, ClassElement owningElement, @@ -114,7 +113,7 @@ public AnnotationMetadata getAnnotationMetadata() { }).toArray(AnnotationMetadata[]::new) ); } - annotationMetadata = new MutableAnnotationMetadataDelegate() { + annotationMetadata = new ElementAnnotationMetadata() { @Override public io.micronaut.inject.ast.Element annotate(AnnotationValue annotationValue) { @@ -212,7 +211,7 @@ public boolean isExcluded() { } @Override - public MutableAnnotationMetadataDelegate getAnnotationMetadata() { + protected ElementAnnotationMetadata getElementAnnotationMetadata() { return annotationMetadata; } diff --git a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyVisitorContext.java b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyVisitorContext.java index 854e9b998c9..8b6df759e99 100644 --- a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyVisitorContext.java +++ b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyVisitorContext.java @@ -64,6 +64,7 @@ * @author Graeme Rocher * @since 1.0 */ +@Internal public class GroovyVisitorContext implements VisitorContext { private static final MutableConvertibleValues VISITOR_ATTRIBUTES = new MutableConvertibleValuesMap<>(); private final CompilationUnit compilationUnit; @@ -145,7 +146,7 @@ public ClassElement[] getClassElements(@NonNull String aPackage, @NonNull String ArgumentUtils.requireNonNull("stereotypes", stereotypes); if (compilationUnit == null) { - return new ClassElement[0]; + return ClassElement.ZERO_CLASS_ELEMENTS; } ClassPathAnnotationScanner scanner = new ClassPathAnnotationScanner(compilationUnit.getClassLoader()); @@ -179,8 +180,8 @@ public AbstractAnnotationMetadataBuilder getAnnotationMetadataBuilder() { @Override public void info(String message, @Nullable Element element) { StringBuilder msg = new StringBuilder("Note: ").append(message); - if (element != null) { - ASTNode expr = (ASTNode) element.getNativeType(); + if (element instanceof AbstractGroovyElement abstractGroovyElement) { + ASTNode expr = abstractGroovyElement.getNativeType().annotatedNode(); final String sample = sourceUnit.getSample(expr.getLineNumber(), expr.getColumnNumber(), new Janitor()); msg.append("\n\n").append(sample); } diff --git a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyWildcardElement.java b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyWildcardElement.java index e4ee4d5fd62..d36f585fd84 100644 --- a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyWildcardElement.java +++ b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyWildcardElement.java @@ -15,13 +15,20 @@ */ package io.micronaut.ast.groovy.visitor; +import io.micronaut.core.annotation.AnnotationMetadata; import io.micronaut.core.annotation.Internal; import io.micronaut.core.annotation.NonNull; +import io.micronaut.core.annotation.Nullable; +import io.micronaut.inject.annotation.AnnotationMetadataHierarchy; import io.micronaut.inject.ast.ArrayableClassElement; import io.micronaut.inject.ast.ClassElement; +import io.micronaut.inject.ast.annotation.AbstractElementAnnotationMetadata; +import io.micronaut.inject.ast.annotation.ElementAnnotationMetadata; import io.micronaut.inject.ast.annotation.ElementAnnotationMetadataFactory; import io.micronaut.inject.ast.WildcardElement; +import io.micronaut.inject.ast.annotation.MutableAnnotationMetadataDelegate; +import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.function.Function; @@ -35,14 +42,19 @@ */ @Internal final class GroovyWildcardElement extends GroovyClassElement implements WildcardElement { + private final GroovyNativeElement wildcardNativeElement; private final GroovyClassElement upperType; private final List upperBounds; private final List lowerBounds; + private final ElementAnnotationMetadata typeAnnotationMetadata; + @Nullable + private ElementAnnotationMetadata genericTypeAnnotationMetadata; - GroovyWildcardElement(@NonNull GroovyClassElement upperType, + GroovyWildcardElement(@NonNull GroovyNativeElement wildcardNativeElement, @NonNull List upperBounds, @NonNull List lowerBounds, - ElementAnnotationMetadataFactory annotationMetadataFactory) { + @NonNull ElementAnnotationMetadataFactory annotationMetadataFactory, + @NonNull GroovyClassElement upperType) { super( upperType.visitorContext, upperType.getNativeType(), @@ -50,14 +62,69 @@ final class GroovyWildcardElement extends GroovyClassElement implements Wildcard upperType.getTypeArguments(), 0 ); + this.wildcardNativeElement = wildcardNativeElement; this.upperType = upperType; this.upperBounds = upperBounds; this.lowerBounds = lowerBounds; + typeAnnotationMetadata = new AbstractElementAnnotationMetadata() { + + AnnotationMetadata annotationMetadata; + + public AnnotationMetadata getReturnInstance() { + return getAnnotationMetadata(); + } + + @Override + protected MutableAnnotationMetadataDelegate getAnnotationMetadataToWrite() { + return getGenericTypeAnnotationMetadata(); + } + + @Override + public AnnotationMetadata getAnnotationMetadata() { + if (annotationMetadata == null) { + List allAnnotationMetadata = new ArrayList<>(); + getLowerBounds().forEach(ce -> allAnnotationMetadata.add(ce.getTypeAnnotationMetadata())); + getUpperBounds().forEach(ce -> allAnnotationMetadata.add(ce.getTypeAnnotationMetadata())); + allAnnotationMetadata.add(GroovyWildcardElement.super.getTypeAnnotationMetadata()); + allAnnotationMetadata.add(getGenericTypeAnnotationMetadata()); + annotationMetadata = new AnnotationMetadataHierarchy(true, allAnnotationMetadata.toArray(AnnotationMetadata[]::new)); + } + return annotationMetadata; + } + }; + } + + @Override + public MutableAnnotationMetadataDelegate getGenericTypeAnnotationMetadata() { + if (genericTypeAnnotationMetadata == null) { + genericTypeAnnotationMetadata = elementAnnotationMetadataFactory.buildGenericTypeAnnotations(this); + } + return genericTypeAnnotationMetadata; + } + + @Override + protected MutableAnnotationMetadataDelegate getAnnotationMetadataToWrite() { + return getGenericTypeAnnotationMetadata(); + } + + @Override + public MutableAnnotationMetadataDelegate getTypeAnnotationMetadata() { + return typeAnnotationMetadata; + } + + @Override + public AnnotationMetadata getAnnotationMetadata() { + return new AnnotationMetadataHierarchy(true, super.getAnnotationMetadata(), getGenericTypeAnnotationMetadata()); + } + + @Override + public Object getGenericNativeType() { + return wildcardNativeElement; } @Override protected GroovyClassElement copyConstructor() { - return new GroovyWildcardElement(upperType, upperBounds, lowerBounds, elementAnnotationMetadataFactory); + return new GroovyWildcardElement(wildcardNativeElement, upperBounds, lowerBounds, elementAnnotationMetadataFactory, upperType); } @NonNull @@ -84,7 +151,7 @@ public ClassElement withArrayDimensions(int arrayDimensions) { public ClassElement foldBoundGenericTypes(@NonNull Function fold) { List upperBounds = this.upperBounds.stream().map(ele -> toGroovyClassElement(ele.foldBoundGenericTypes(fold))).collect(Collectors.toList()); List lowerBounds = this.lowerBounds.stream().map(ele -> toGroovyClassElement(ele.foldBoundGenericTypes(fold))).collect(Collectors.toList()); - return fold.apply(upperBounds.contains(null) || lowerBounds.contains(null) ? null : new GroovyWildcardElement(upperType, upperBounds, lowerBounds, elementAnnotationMetadataFactory)); + return fold.apply(upperBounds.contains(null) || lowerBounds.contains(null) ? null : new GroovyWildcardElement(wildcardNativeElement, upperBounds, lowerBounds, elementAnnotationMetadataFactory, upperType)); } private GroovyClassElement toGroovyClassElement(ClassElement element) { diff --git a/inject-groovy/src/test/groovy/io/micronaut/ast/groovy/visitor/GroovyReconstructionSpec.groovy b/inject-groovy/src/test/groovy/io/micronaut/ast/groovy/visitor/GroovyReconstructionSpec.groovy index f8fe32b689a..67e0530d073 100644 --- a/inject-groovy/src/test/groovy/io/micronaut/ast/groovy/visitor/GroovyReconstructionSpec.groovy +++ b/inject-groovy/src/test/groovy/io/micronaut/ast/groovy/visitor/GroovyReconstructionSpec.groovy @@ -32,13 +32,15 @@ class GroovyReconstructionSpec extends AbstractBeanDefinitionSpec { if (classElement.isArray()) { return reconstructTypeSignature(classElement.fromArray()) + "[]" } else if (classElement.isGenericPlaceholder()) { - def freeVar = (GenericPlaceholderElement) classElement - def name = freeVar.variableName + def genericPlaceholderElement = (GenericPlaceholderElement) classElement + def name = genericPlaceholderElement.variableName if (typeVarsAsDeclarations) { - def bounds = freeVar.bounds + def bounds = genericPlaceholderElement.bounds if (reconstructTypeSignature(bounds[0]) != 'Object') { name += bounds.stream().map(GroovyReconstructionSpec::reconstructTypeSignature).collect(Collectors.joining(" & ", " extends ", "")) } + } else if (genericPlaceholderElement.resolved) { + return reconstructTypeSignature(genericPlaceholderElement.resolved.get()) } return name } else if (classElement.isWildcard()) { @@ -333,12 +335,12 @@ class Wrapper { fieldType | expectedType 'String' | 'String' 'List' | 'List' - 'List' | 'List' - 'List' | 'List' + 'List' | 'List' + 'List' | 'List' 'List' | 'List' 'List' | 'List' - 'List' | 'List' - 'List[]>' | 'List[]>' + 'List' | 'List' + 'List[]>' | 'List[]>' 'List' | 'List' 'List>' | 'List>' } @@ -370,12 +372,12 @@ class Wrapper { fieldType | expectedType 'String' | 'String' 'List' | 'List' - 'List' | 'List' - 'List' | 'List' + 'List' | 'List' + 'List' | 'List' 'List' | 'List' 'List' | 'List' - 'List' | 'List' - 'List[]>' | 'List[]>' + 'List' | 'List' + 'List[]>' | 'List[]>' 'List' | 'List' 'List>' | 'List>' } @@ -549,4 +551,102 @@ class Test { 'List' | 'List' 'List' | 'List' } + + def 'distinguish base list type'() { + given: + def classElement = buildClassElement(""" +package example; + +import java.util.*; +import java.lang.Number; + +abstract class Base { + List field1; + List field2; + List field3; + List field4; +} + +class Test extends Base { +} + +""") + def rawType = classElement.fields[0].type + def wildcardType = classElement.fields[1].type + def objectType = classElement.fields[2].type + def genericType = classElement.fields[3].type + + expect: + rawType.typeArguments["E"].type.name == "java.lang.Object" + rawType.typeArguments["E"].isRawType() + !rawType.typeArguments["E"].isWildcard() + rawType.typeArguments["E"].isGenericPlaceholder() + + wildcardType.typeArguments["E"].type.name == "java.lang.Object" + wildcardType.typeArguments["E"].isWildcard() + !((WildcardElement)wildcardType.typeArguments["E"]).isBounded() + !wildcardType.typeArguments["E"].isRawType() + + objectType.typeArguments["E"].type.name == "java.lang.Object" + !objectType.typeArguments["E"].isWildcard() + !objectType.typeArguments["E"].isRawType() + !objectType.typeArguments["E"].isGenericPlaceholder() + + genericType.typeArguments["E"].type.name == "java.lang.Object" + !genericType.typeArguments["E"].isWildcard() + !genericType.typeArguments["E"].isRawType() + genericType.typeArguments["E"].isGenericPlaceholder() + (genericType.typeArguments["E"] as GenericPlaceholderElement).getResolved().isEmpty() + } + + def 'distinguish base list generic type'() { + given: + def classElement = buildClassElement(""" +package example; + +import java.util.*; +import java.lang.Number; + +abstract class Base { + List field1; + List field2; + List field3; + List field4; +} + +class Test extends Base { +} + +""") + def rawType = classElement.fields[0].genericType + def wildcardType = classElement.fields[1].genericType + def objectType = classElement.fields[2].genericType + def genericType = classElement.fields[3].genericType + + expect: + rawType.typeArguments["E"].type.name == "java.lang.Object" + rawType.typeArguments["E"].isRawType() + !rawType.typeArguments["E"].isWildcard() + rawType.typeArguments["E"].isGenericPlaceholder() + + wildcardType.typeArguments["E"].type.name == "java.lang.Object" + wildcardType.typeArguments["E"].isWildcard() + !((WildcardElement)wildcardType.typeArguments["E"]).isBounded() + !wildcardType.typeArguments["E"].isRawType() + + objectType.typeArguments["E"].type.name == "java.lang.Object" + !objectType.typeArguments["E"].isWildcard() + !objectType.typeArguments["E"].isRawType() + !objectType.typeArguments["E"].isGenericPlaceholder() + + genericType.typeArguments["E"].type.name == "java.lang.String" + !genericType.typeArguments["E"].isWildcard() + !genericType.typeArguments["E"].isRawType() + genericType.typeArguments["E"].isGenericPlaceholder() + def resolved = (genericType.typeArguments["E"] as GenericPlaceholderElement).getResolved().get() + resolved.name == "java.lang.String" + !resolved.isWildcard() + !resolved.isRawType() + !resolved.isGenericPlaceholder() + } } diff --git a/inject-groovy/src/test/groovy/io/micronaut/inject/annotation/modify/AnnotateFieldSpec.groovy b/inject-groovy/src/test/groovy/io/micronaut/inject/annotation/modify/AnnotateFieldSpec.groovy new file mode 100644 index 00000000000..54d90eb6125 --- /dev/null +++ b/inject-groovy/src/test/groovy/io/micronaut/inject/annotation/modify/AnnotateFieldSpec.groovy @@ -0,0 +1,95 @@ +package io.micronaut.inject.annotation.modify + +import io.micronaut.ast.groovy.TypeElementVisitorStart +import io.micronaut.ast.transform.test.AbstractBeanDefinitionSpec +import io.micronaut.core.annotation.AnnotationUtil +import io.micronaut.inject.ast.ClassElement +import io.micronaut.inject.visitor.AllElementsVisitor +import io.micronaut.inject.visitor.TypeElementVisitor +import io.micronaut.inject.visitor.VisitorContext + +class AnnotateFieldSpec extends AbstractBeanDefinitionSpec { + + def setup() { + System.setProperty(TypeElementVisitorStart.ELEMENT_VISITORS_PROPERTY, AnnotationFieldVisitor.name) + } + + def cleanup() { + System.setProperty(TypeElementVisitorStart.ELEMENT_VISITORS_PROPERTY, "") + AllElementsVisitor.clearVisited() + } + + void 'test annotating'() { + when: + def definition = buildBeanDefinition('addann.AnnotationFieldClass', ''' +package addann; + +import io.micronaut.context.annotation.Bean; +import jakarta.inject.Inject; + +@Bean +class AnnotationFieldClass { + + @Inject + public MyBean1 myField1; + + @Inject + public MyBean1 myField2; + +} + +class MyBean1 { +} + +''') + then: + def myField1 = definition.getInjectedFields()[0] + myField1.name == "myField1" + myField1.asArgument().getAnnotationMetadata().hasAnnotation(MyAnnotation) + and: + def myField2 = definition.getInjectedFields()[1] + myField2.name == "myField2" + !myField2.asArgument().getAnnotationMetadata().hasAnnotation(MyAnnotation) + } + + static class AnnotationFieldVisitor implements TypeElementVisitor { + @Override + void visitClass(ClassElement element, VisitorContext context) { + if (element.getSimpleName() == "AnnotationFieldClass") { + def myField1 = element.findField("myField1").get() + assert myField1.getAnnotationMetadata().getAnnotationNames().asList() == [AnnotationUtil.INJECT] + myField1.annotate(MyAnnotation) + assert myField1.getAnnotationMetadata().getAnnotationNames().asList() == [AnnotationUtil.INJECT, MyAnnotation.class.name] + assert myField1.getAnnotationMetadata().getAnnotationNames().asList() == [AnnotationUtil.INJECT, MyAnnotation.class.name] + assert myField1.getType().getAnnotationNames().isEmpty() + assert myField1.getType().getTypeAnnotationMetadata().getAnnotationNames().isEmpty() + assert myField1.getType().getType().getAnnotationMetadata().isEmpty() + assert myField1.getGenericType().getAnnotationNames().isEmpty() + assert myField1.getGenericType().getTypeAnnotationMetadata().getAnnotationNames().isEmpty() + assert myField1.getGenericType().getType().getAnnotationMetadata().isEmpty() + + // Validate the cache is working + assert context.getClassElement("addann.AnnotationFieldClass").get() + .findField("myField1").get() + .getAnnotationMetadata().getAnnotationNames().asList() == [AnnotationUtil.INJECT, MyAnnotation.class.name] + + // Test the second method with the same type doesn't have the annotations + + def myField2 = element.findField("myField2").get() + assert myField2.getAnnotationMetadata().getAnnotationNames().asList() == [AnnotationUtil.INJECT] + assert myField2.getType().getAnnotationNames().isEmpty() + assert myField2.getType().getTypeAnnotationMetadata().getAnnotationNames().isEmpty() + assert myField2.getGenericType().getAnnotationNames().isEmpty() + assert myField2.getGenericType().getTypeAnnotationMetadata().getAnnotationNames().isEmpty() + + // Validate the cache is working + assert context.getClassElement("addann.AnnotationFieldClass").get() + .findField("myField2").get() + .getAnnotationMetadata().getAnnotationNames().asList() == [AnnotationUtil.INJECT] + + assert context.getClassElement("addann.MyBean1").get().getAnnotationMetadata().isEmpty() + } + } + } + +} diff --git a/inject-groovy/src/test/groovy/io/micronaut/inject/annotation/modify/AnnotateFieldTypeSpec.groovy b/inject-groovy/src/test/groovy/io/micronaut/inject/annotation/modify/AnnotateFieldTypeSpec.groovy new file mode 100644 index 00000000000..d1b75524d47 --- /dev/null +++ b/inject-groovy/src/test/groovy/io/micronaut/inject/annotation/modify/AnnotateFieldTypeSpec.groovy @@ -0,0 +1,210 @@ +package io.micronaut.inject.annotation.modify + +import io.micronaut.ast.groovy.TypeElementVisitorStart +import io.micronaut.ast.transform.test.AbstractBeanDefinitionSpec +import io.micronaut.inject.BeanDefinition +import io.micronaut.inject.ast.ClassElement +import io.micronaut.inject.ast.GenericPlaceholderElement +import io.micronaut.inject.visitor.AllElementsVisitor +import io.micronaut.inject.visitor.TypeElementVisitor +import io.micronaut.inject.visitor.VisitorContext + +class AnnotateFieldTypeSpec extends AbstractBeanDefinitionSpec { + + def setup() { + System.setProperty(TypeElementVisitorStart.ELEMENT_VISITORS_PROPERTY, AnnotateFieldTypeVisitor.name) + } + + def cleanup() { + System.setProperty(TypeElementVisitorStart.ELEMENT_VISITORS_PROPERTY, "") + AllElementsVisitor.clearVisited() + } + + void 'test annotating'() { + when: + def definition = buildBeanDefinition('addann.AnnotateFieldTypeClass', ''' +package addann; + +import io.micronaut.context.annotation.Bean; +import jakarta.inject.Inject; + +@Bean +class AnnotateFieldTypeClass { + + @Inject + public MyBean1 myField1; + + @Inject + public MyBean1 myField2; + +} + +class MyBean1 { +} +''') + then: + validate(definition) + } + + void 'test annotating 2'() { + when: + def definition = buildBeanDefinition('addann.AnnotateFieldTypeClass', ''' +package addann; + +import io.micronaut.context.annotation.Executable; +import io.micronaut.context.annotation.Bean; +import jakarta.inject.Inject; + +@Bean +class AnnotateFieldTypeClass { + + @Inject + public T myField1; + + @Inject + public T myField2; + +} + +class MyBean1 { +} +''') + then: + validate(definition) + } + + void 'test annotating 3'() { + when: + def definition = buildBeanDefinition('addann.AnnotateFieldTypeClass', ''' +package addann; + +import io.micronaut.context.annotation.Bean; +import jakarta.inject.Inject; + +abstract class BaseAnnotateFieldTypeClass { + + @Inject + public S myField1; + + @Inject + public S myField2; + +} + +@Bean +class AnnotateFieldTypeClass extends BaseAnnotateFieldTypeClass { +} + +class MyBean1 { +} +''') + then: + validate(definition) + } + + void 'test annotating 4'() { + when: + def definition = buildBeanDefinition('addann.AnnotateFieldTypeClass', ''' +package addann; + +import io.micronaut.context.annotation.Bean; +import jakarta.inject.Inject; + +abstract class BaseAnnotateFieldTypeClass { + + @Inject + public S myField1; + + @Inject + public S myField2; + +} + +@Bean +class AnnotateFieldTypeClass extends BaseAnnotateFieldTypeClass { +} + +class MyBean1 { +} +''') + then: + validate(definition) + } + + void validate(BeanDefinition definition) { + def myField1 = definition.getInjectedFields()[0] + myField1.name == "myField1" + myField1.asArgument().getAnnotationMetadata().hasAnnotation(MyAnnotation) + + def myField2 = definition.getInjectedFields()[1] + myField2.name == "myField2" + !myField2.asArgument().getAnnotationMetadata().hasAnnotation(MyAnnotation) + } + + static class AnnotateFieldTypeVisitor implements TypeElementVisitor { + @Override + void visitClass(ClassElement classElement, VisitorContext context) { + if (classElement.getSimpleName() == "AnnotateFieldTypeClass") { + + def myField1 = classElement.findField("myField1").get() + def type = myField1.getType() + def genericType = myField1.getGenericType() + if (type instanceof GenericPlaceholderElement) { + assert genericType instanceof GenericPlaceholderElement + def placeholderElement = type as GenericPlaceholderElement + def genericPlaceholderElement = genericType as GenericPlaceholderElement + assert placeholderElement.getGenericNativeType() == genericPlaceholderElement.getGenericNativeType() + assert placeholderElement.variableName == genericPlaceholderElement.variableName + } + + assert type.getAnnotationMetadata().getAnnotationNames().isEmpty() + assert genericType.getAnnotationMetadata().getAnnotationNames().isEmpty() + + type.annotate(MyAnnotation) + + assert type.getAnnotationMetadata().getAnnotationNames().asList() == [MyAnnotation.class.name] + assert genericType.getAnnotationMetadata().getAnnotationNames().asList() == [MyAnnotation.class.name] + // The annotation should be added to type annotations + assert type.getTypeAnnotationMetadata().getAnnotationNames().asList() == [MyAnnotation.class.name] + assert genericType.getTypeAnnotationMetadata().getAnnotationNames().asList() == [MyAnnotation.class.name] + // The annotation is not added to the actual type + assert type.getType().isEmpty() + assert genericType.getType().isEmpty() + + // Validate the cache is working + + def newClassElement = context.getClassElement("addann.AnnotateFieldTypeClass").get() + def newField = newClassElement.findField("myField1").get() + def newType = newField.getType() + def newGenericType = newField.getGenericType() + + assert newType.getAnnotationMetadata().getAnnotationNames().asList() == [MyAnnotation.class.name] + assert newType.getTypeAnnotationMetadata().getAnnotationNames().asList() == [MyAnnotation.class.name] + assert newGenericType.getAnnotationMetadata().getAnnotationNames().asList() == [MyAnnotation.class.name] + assert newGenericType.getTypeAnnotationMetadata().getAnnotationNames().asList() == [MyAnnotation.class.name] + + assert context.getClassElement("addann.MyBean1").get().getAnnotationMetadata().isEmpty() + assert context.getClassElement("addann.MyBean1").get().getTypeAnnotationMetadata().isEmpty() + + // Validate the annotation is not added to the return class type of myMethod2 + + def field2Type = newClassElement.findField("myField2").get().getType() + def field2GenericType = newClassElement.findField("myField2").get().getGenericType() + + assert field2Type.getAnnotationMetadata().isEmpty() + assert field2Type.getTypeAnnotationMetadata().isEmpty() + + assert field2GenericType.getAnnotationMetadata().isEmpty() + assert field2GenericType.getTypeAnnotationMetadata().isEmpty() + + assert field2Type.getTypeAnnotationMetadata().isEmpty() + assert field2Type.getAnnotationMetadata().isEmpty() + + assert field2GenericType.getTypeAnnotationMetadata().isEmpty() + assert field2GenericType.getAnnotationMetadata().isEmpty() + + } + } + } + +} diff --git a/inject-groovy/src/test/groovy/io/micronaut/inject/annotation/modify/AnnotateMethodParameterSpec.groovy b/inject-groovy/src/test/groovy/io/micronaut/inject/annotation/modify/AnnotateMethodParameterSpec.groovy new file mode 100644 index 00000000000..e89a95a49f8 --- /dev/null +++ b/inject-groovy/src/test/groovy/io/micronaut/inject/annotation/modify/AnnotateMethodParameterSpec.groovy @@ -0,0 +1,270 @@ +package io.micronaut.inject.annotation.modify + +import io.micronaut.ast.groovy.TypeElementVisitorStart +import io.micronaut.ast.transform.test.AbstractBeanDefinitionSpec +import io.micronaut.inject.BeanDefinition +import io.micronaut.inject.ast.ClassElement +import io.micronaut.inject.ast.GenericPlaceholderElement +import io.micronaut.inject.visitor.AllElementsVisitor +import io.micronaut.inject.visitor.TypeElementVisitor +import io.micronaut.inject.visitor.VisitorContext + +class AnnotateMethodParameterSpec extends AbstractBeanDefinitionSpec { + + def setup() { + System.setProperty(TypeElementVisitorStart.ELEMENT_VISITORS_PROPERTY, AnnotateMethodParameterVisitor.name) + } + + def cleanup() { + System.setProperty(TypeElementVisitorStart.ELEMENT_VISITORS_PROPERTY, "") + AllElementsVisitor.clearVisited() + } + + void 'test annotating'() { + when: + def definition = buildBeanDefinition('addann.AnnotateMethodParameterClass', ''' +package addann; + +import io.micronaut.context.annotation.Executable; +import io.micronaut.context.annotation.Bean; + +@Bean +class AnnotateMethodParameterClass { + + @Executable + public MyBean1 myMethod1(MyBean1 param) { + return null; + } + + @Executable + public MyBean1 myMethod2(MyBean1 param) { + return null; + } + +} + +class MyBean1 { +} +''') + then: + validate(definition) + } + + void 'test annotating 2'() { + when: + def definition = buildBeanDefinition('addann.AnnotateMethodParameterClass', ''' +package addann; + +import io.micronaut.context.annotation.Executable; +import io.micronaut.context.annotation.Bean; + +@Bean +class AnnotateMethodParameterClass { + + @Executable + public T myMethod1(T param) { + return null; + } + + @Executable + public T myMethod2(T param) { + return null; + } + +} + +class MyBean1 { +} +''') + then: + validate(definition) + } + + void 'test annotating 3'() { + when: + def definition = buildBeanDefinition('addann.AnnotateMethodParameterClass', ''' +package addann; + +import io.micronaut.context.annotation.Executable; +import io.micronaut.context.annotation.Bean; + +@Bean +class AnnotateMethodParameterClass { + + @Executable + public K myMethod1(K param) { + return null; + } + + @Executable + public K myMethod2(K param) { + return null; + } + +} + +class MyBean1 { +} +''') + then: + validate(definition) + } + + void 'test annotating 4'() { + when: + def definition = buildBeanDefinition('addann.AnnotateMethodParameterClass', ''' +package addann; + +import io.micronaut.context.annotation.Executable; +import io.micronaut.context.annotation.Bean; + +abstract class Base { + + @Executable + public S myMethod1(S param) { + return null; + } + + @Executable + public S myMethod2(S param) { + return null; + } + +} + +@Bean +class AnnotateMethodParameterClass extends Base { +} + +class MyBean1 { +} +''') + then: + validate(definition) + } + + void 'test annotating 6'() { + when: + def definition = buildBeanDefinition('addann.AnnotateMethodParameterClass', ''' +package addann; + +import io.micronaut.context.annotation.Executable; +import io.micronaut.context.annotation.Bean; + +abstract class Base { + + @Executable + public S myMethod1(S param) { + return null; + } + + @Executable + public S myMethod2(S param) { + return null; + } + +} + +@Bean +class AnnotateMethodParameterClass extends Base { +} + +class MyBean1 { +} +''') + then: + validate(definition) + } + + void validate(BeanDefinition definition) { + def method1 = definition.findPossibleMethods("myMethod1").findAny().get() + def method1ParameterType = method1.getArguments()[0] + def method1ReturnType = method1.getReturnType() + + assert method1ParameterType.simpleName == "MyBean1" + assert method1ReturnType.simpleName == "MyBean1" + assert method1ParameterType.getAnnotationMetadata().hasAnnotation(MyAnnotation) + assert !method1ReturnType.getAnnotationMetadata().hasAnnotation(MyAnnotation) + assert !method1ReturnType.asArgument().getAnnotationMetadata().hasAnnotation(MyAnnotation) + assert !method1.hasAnnotation(MyAnnotation) + + def method2 = definition.findPossibleMethods("myMethod2").findAny().get() + def method2ParameterType = method2.getArguments()[0] + def method2ReturnType = method2.getReturnType() + + assert !method2ParameterType.getAnnotationMetadata().hasAnnotation(MyAnnotation) + assert !method2ReturnType.getAnnotationMetadata().hasAnnotation(MyAnnotation) + assert !method2ReturnType.asArgument().getAnnotationMetadata().hasAnnotation(MyAnnotation) + assert !method2.hasAnnotation(MyAnnotation) + assert !method2.hasAnnotation(MyAnnotation) + } + + static class AnnotateMethodParameterVisitor implements TypeElementVisitor { + @Override + void visitClass(ClassElement classElement, VisitorContext context) { + if (classElement.getSimpleName() == "AnnotateMethodParameterClass") { + + def myMethod1 = classElement.findMethod("myMethod1").get() + def type = myMethod1.getParameters()[0].getType() + def genericType = myMethod1.getParameters()[0].getGenericType() + if (type instanceof GenericPlaceholderElement) { + assert genericType instanceof GenericPlaceholderElement + def placeholderElement = type as GenericPlaceholderElement + def genericPlaceholderElement = genericType as GenericPlaceholderElement + assert placeholderElement.getGenericNativeType() == genericPlaceholderElement.getGenericNativeType() + assert placeholderElement.variableName == genericPlaceholderElement.variableName + } + + assert type.getAnnotationMetadata().getAnnotationNames().isEmpty() + assert genericType.getAnnotationMetadata().getAnnotationNames().isEmpty() + + type.annotate(MyAnnotation) + + assert type.getAnnotationMetadata().getAnnotationNames().asList() == [MyAnnotation.class.name] + assert genericType.getAnnotationMetadata().getAnnotationNames().asList() == [MyAnnotation.class.name] + // The annotation should be added to type annotations + assert type.getTypeAnnotationMetadata().getAnnotationNames().asList() == [MyAnnotation.class.name] + assert genericType.getTypeAnnotationMetadata().getAnnotationNames().asList() == [MyAnnotation.class.name] + // The annotation is not added to the actual type + assert type.getType().isEmpty() + assert genericType.getType().isEmpty() + myMethod1.getReturnType().getAnnotationMetadata().isEmpty() + myMethod1.getGenericReturnType().getAnnotationMetadata().isEmpty() + + // Validate the cache is working + + def newClassElement = context.getClassElement("addann.AnnotateMethodParameterClass").get() + def newMethod = newClassElement.findMethod("myMethod1").get() + def newType = newMethod.getParameters()[0].getType() + def newGenericType =newMethod.getParameters()[0].getGenericType() + + assert newType.getAnnotationMetadata().getAnnotationNames().asList() == [MyAnnotation.class.name] + assert newType.getTypeAnnotationMetadata().getAnnotationNames().asList() == [MyAnnotation.class.name] + assert newGenericType.getAnnotationMetadata().getAnnotationNames().asList() == [MyAnnotation.class.name] + assert newGenericType.getTypeAnnotationMetadata().getAnnotationNames().asList() == [MyAnnotation.class.name] + + assert context.getClassElement("addann.MyBean1").get().getAnnotationMetadata().isEmpty() + assert context.getClassElement("addann.MyBean1").get().getTypeAnnotationMetadata().isEmpty() + + // Validate the annotation is not added to the return class type of myMethod2 + + def method2Type = newClassElement.findMethod("myMethod2").get().getParameters()[0].getType() + def method2GenericType = newClassElement.findMethod("myMethod2").get().getParameters()[0].getGenericType() + + assert method2Type.getAnnotationMetadata().isEmpty() + assert method2Type.getTypeAnnotationMetadata().isEmpty() + + assert method2GenericType.getAnnotationMetadata().isEmpty() + assert method2GenericType.getTypeAnnotationMetadata().isEmpty() + + assert method2Type.getTypeAnnotationMetadata().isEmpty() + assert method2Type.getAnnotationMetadata().isEmpty() + + assert method2GenericType.getTypeAnnotationMetadata().isEmpty() + assert method2GenericType.getAnnotationMetadata().isEmpty() + + } + } + } + +} diff --git a/inject-groovy/src/test/groovy/io/micronaut/inject/annotation/modify/AnnotateMethodReturnSpec.groovy b/inject-groovy/src/test/groovy/io/micronaut/inject/annotation/modify/AnnotateMethodReturnSpec.groovy new file mode 100644 index 00000000000..e9f0825e3c3 --- /dev/null +++ b/inject-groovy/src/test/groovy/io/micronaut/inject/annotation/modify/AnnotateMethodReturnSpec.groovy @@ -0,0 +1,270 @@ +package io.micronaut.inject.annotation.modify + +import io.micronaut.ast.groovy.TypeElementVisitorStart +import io.micronaut.ast.transform.test.AbstractBeanDefinitionSpec +import io.micronaut.inject.BeanDefinition +import io.micronaut.inject.ast.ClassElement +import io.micronaut.inject.ast.GenericPlaceholderElement +import io.micronaut.inject.visitor.AllElementsVisitor +import io.micronaut.inject.visitor.TypeElementVisitor +import io.micronaut.inject.visitor.VisitorContext + +class AnnotateMethodReturnSpec extends AbstractBeanDefinitionSpec { + + def setup() { + System.setProperty(TypeElementVisitorStart.ELEMENT_VISITORS_PROPERTY, AnnotateMethodReturnVisitor.name) + } + + def cleanup() { + System.setProperty(TypeElementVisitorStart.ELEMENT_VISITORS_PROPERTY, "") + AllElementsVisitor.clearVisited() + } + + void 'test annotating'() { + when: + def definition = buildBeanDefinition('addann.AnnotateMethodReturnClass', ''' +package addann; + +import io.micronaut.context.annotation.Executable; +import io.micronaut.context.annotation.Bean; + +@Bean +class AnnotateMethodReturnClass { + + @Executable + public MyBean1 myMethod1() { + return null; + } + + @Executable + public MyBean1 myMethod2() { + return null; + } + +} + +class MyBean1 { +} +''') + then: + validate(definition) + } + + void 'test annotating 2'() { + when: + def definition = buildBeanDefinition('addann.AnnotateMethodReturnClass', ''' +package addann; + +import io.micronaut.context.annotation.Executable; +import io.micronaut.context.annotation.Bean; + +@Bean +class AnnotateMethodReturnClass { + + @Executable + public T myMethod1() { + return null; + } + + @Executable + public T myMethod2() { + return null; + } + +} + +class MyBean1 { +} +''') + then: + validate(definition) + } + + void 'test annotating 3'() { + when: + def definition = buildBeanDefinition('addann.AnnotateMethodReturnClass', ''' +package addann; + +import io.micronaut.context.annotation.Executable; +import io.micronaut.context.annotation.Bean; + +@Bean +class AnnotateMethodReturnClass { + + @Executable + public K myMethod1() { + return null; + } + + @Executable + public K myMethod2() { + return null; + } + +} + +class MyBean1 { +} +''') + then: + validate(definition) + } + + void 'test annotating 4'() { + when: + def definition = buildBeanDefinition('addann.AnnotateMethodReturnClass', ''' +package addann; + +import io.micronaut.context.annotation.Executable; +import io.micronaut.context.annotation.Bean; + +abstract class BaseAnnotateMethodReturnClass { + + @Executable + public S myMethod1() { + return null; + } + + @Executable + public S myMethod2() { + return null; + } + +} + +@Bean +class AnnotateMethodReturnClass extends BaseAnnotateMethodReturnClass { +} + +class MyBean1 { +} +''') + then: + validate(definition) + } + + void 'test annotating 6'() { + when: + def definition = buildBeanDefinition('addann.AnnotateMethodReturnClass', ''' +package addann; + +import io.micronaut.context.annotation.Executable; +import io.micronaut.context.annotation.Bean; + +abstract class BaseAnnotateMethodReturnClass { + + @Executable + public S myMethod1() { + return null; + } + + @Executable + public S myMethod2() { + return null; + } + +} + +@Bean +class AnnotateMethodReturnClass extends BaseAnnotateMethodReturnClass { +} + +class MyBean1 { +} +''') + then: + validate(definition) + } + + void validate(BeanDefinition definition) { + def method1 = definition.getRequiredMethod("myMethod1") + def method1ReturnType = method1.getReturnType() + + assert method1ReturnType.simpleName == "MyBean1" + assert method1ReturnType.getAnnotationMetadata().hasAnnotation(MyAnnotation) + assert method1ReturnType.asArgument().getAnnotationMetadata().hasAnnotation(MyAnnotation) + assert !method1.hasAnnotation(MyAnnotation) + + def method2 = definition.getRequiredMethod("myMethod2") + def method2ReturnType = method2.getReturnType() + + assert !method2ReturnType.getAnnotationMetadata().hasAnnotation(MyAnnotation) + assert !method2ReturnType.asArgument().getAnnotationMetadata().hasAnnotation(MyAnnotation) + assert !method2.hasAnnotation(MyAnnotation) + assert !method2.hasAnnotation(MyAnnotation) + } + + static class AnnotateMethodReturnVisitor implements TypeElementVisitor { + @Override + void visitClass(ClassElement classElement, VisitorContext context) { + if (classElement.getSimpleName() == "AnnotateMethodReturnClass") { + + def myMethod1 = classElement.findMethod("myMethod1").get() + def returnType = myMethod1.getReturnType() + def genericReturnType = myMethod1.getGenericReturnType() + if (returnType instanceof GenericPlaceholderElement) { + assert genericReturnType instanceof GenericPlaceholderElement + def placeholderElement = returnType as GenericPlaceholderElement + def genericPlaceholderElement = genericReturnType as GenericPlaceholderElement + assert placeholderElement.getGenericNativeType() == genericPlaceholderElement.getGenericNativeType() + assert placeholderElement.variableName == genericPlaceholderElement.variableName +// if ("K" == placeholderElement.variableName) { +// assert placeholderElement.declaringElement.get() == myMethod1 +// assert genericPlaceholderElement.declaringElement.get() == myMethod1 +// } else { +// assert placeholderElement.declaringElement.get() == classElement +// assert genericPlaceholderElement.declaringElement.get() == classElement +// } + } + + assert returnType.getAnnotationMetadata().getAnnotationNames().isEmpty() + assert genericReturnType.getAnnotationMetadata().getAnnotationNames().isEmpty() + + returnType.annotate(MyAnnotation) + + assert returnType.getAnnotationMetadata().getAnnotationNames().asList() == [MyAnnotation.class.name] + assert genericReturnType.getAnnotationMetadata().getAnnotationNames().asList() == [MyAnnotation.class.name] + // The annotation should be added to type annotations + assert returnType.getTypeAnnotationMetadata().getAnnotationNames().asList() == [MyAnnotation.class.name] + assert genericReturnType.getTypeAnnotationMetadata().getAnnotationNames().asList() == [MyAnnotation.class.name] + // The annotation is not added to the actual type + assert returnType.getType().isEmpty() + assert genericReturnType.getType().isEmpty() + + // Validate the cache is working + + def newClassElement = context.getClassElement("addann.AnnotateMethodReturnClass").get() + def newMethod = newClassElement.findMethod("myMethod1").get() + def newReturnType = newMethod.getReturnType() + def newGenericReturnType = newMethod.getGenericReturnType() + + assert newReturnType.getAnnotationMetadata().getAnnotationNames().asList() == [MyAnnotation.class.name] + assert newReturnType.getTypeAnnotationMetadata().getAnnotationNames().asList() == [MyAnnotation.class.name] + assert newGenericReturnType.getAnnotationMetadata().getAnnotationNames().asList() == [MyAnnotation.class.name] + assert newGenericReturnType.getTypeAnnotationMetadata().getAnnotationNames().asList() == [MyAnnotation.class.name] + + assert context.getClassElement("addann.MyBean1").get().getAnnotationMetadata().isEmpty() + assert context.getClassElement("addann.MyBean1").get().getTypeAnnotationMetadata().isEmpty() + + // Validate the annotation is not added to the return class type of myMethod2 + + def method2ReturnType = newClassElement.findMethod("myMethod2").get().getReturnType() + def method2GenericReturnType = newClassElement.findMethod("myMethod2").get().getGenericReturnType() + + assert method2ReturnType.getAnnotationMetadata().isEmpty() + assert method2ReturnType.getTypeAnnotationMetadata().isEmpty() + + assert method2GenericReturnType.getAnnotationMetadata().isEmpty() + assert method2GenericReturnType.getTypeAnnotationMetadata().isEmpty() + + assert method2ReturnType.getTypeAnnotationMetadata().isEmpty() + assert method2ReturnType.getAnnotationMetadata().isEmpty() + + assert method2GenericReturnType.getTypeAnnotationMetadata().isEmpty() + assert method2GenericReturnType.getAnnotationMetadata().isEmpty() + + } + } + } + +} diff --git a/inject-groovy/src/test/groovy/io/micronaut/inject/annotation/modify/AnnotateMethodSpec.groovy b/inject-groovy/src/test/groovy/io/micronaut/inject/annotation/modify/AnnotateMethodSpec.groovy new file mode 100644 index 00000000000..7c9d656c943 --- /dev/null +++ b/inject-groovy/src/test/groovy/io/micronaut/inject/annotation/modify/AnnotateMethodSpec.groovy @@ -0,0 +1,96 @@ +package io.micronaut.inject.annotation.modify + +import io.micronaut.ast.groovy.TypeElementVisitorStart +import io.micronaut.ast.transform.test.AbstractBeanDefinitionSpec +import io.micronaut.context.annotation.Bean +import io.micronaut.context.annotation.Executable +import io.micronaut.inject.ast.ClassElement +import io.micronaut.inject.visitor.AllElementsVisitor +import io.micronaut.inject.visitor.TypeElementVisitor +import io.micronaut.inject.visitor.VisitorContext + +class AnnotateMethodSpec extends AbstractBeanDefinitionSpec { + + def setup() { + System.setProperty(TypeElementVisitorStart.ELEMENT_VISITORS_PROPERTY, AnnotationMethodVisitor.name) + } + + def cleanup() { + System.setProperty(TypeElementVisitorStart.ELEMENT_VISITORS_PROPERTY, "") + AllElementsVisitor.clearVisited() + } + + void 'test annotating'() { + when: + def definition = buildBeanDefinition('addann.AnnotationMethodClass', ''' +package addann; + +import io.micronaut.context.annotation.Executable; +import io.micronaut.context.annotation.Bean; + +@Bean +class AnnotationMethodClass { + + @Executable + public MyBean1 myMethod1() { + return null; + } + + @Executable + public MyBean1 myMethod2() { + return null; + } + +} + +class MyBean1 { +} + +''') + then: "myMethod1 has added annotation on the method and it's seen on the return type" + definition.getRequiredMethod("myMethod1").hasAnnotation(MyAnnotation) + definition.getRequiredMethod("myMethod1").getReturnType().getAnnotationMetadata().hasAnnotation(MyAnnotation) + definition.getRequiredMethod("myMethod1").getReturnType().asArgument().getAnnotationMetadata().hasAnnotation(MyAnnotation) + + and: "myMethod2 doesn't have the same annotation on the same type" + !definition.getRequiredMethod("myMethod2").hasAnnotation(MyAnnotation) + !definition.getRequiredMethod("myMethod2").getReturnType().getAnnotationMetadata().hasAnnotation(MyAnnotation) + !definition.getRequiredMethod("myMethod2").getReturnType().asArgument().getAnnotationMetadata().hasAnnotation(MyAnnotation) + } + + static class AnnotationMethodVisitor implements TypeElementVisitor { + @Override + void visitClass(ClassElement element, VisitorContext context) { + if (element.getSimpleName() == "AnnotationMethodClass") { + def myMethod1 = element.findMethod("myMethod1").get() + assert myMethod1.getAnnotationMetadata().getAnnotationNames().asList() == [Executable.class.name, Bean.class.name] + myMethod1.annotate(MyAnnotation) + assert myMethod1.getAnnotationMetadata().getAnnotationNames().asList() == [Executable.class.name, Bean.class.name, MyAnnotation.class.name] + assert myMethod1.getAnnotationMetadata().getAnnotationNames().asList() == [Executable.class.name, Bean.class.name, MyAnnotation.class.name] + assert myMethod1.getReturnType().getAnnotationNames().isEmpty() + assert myMethod1.getReturnType().getTypeAnnotationMetadata().getAnnotationNames().isEmpty() + assert myMethod1.getReturnType().getType().getAnnotationMetadata().isEmpty() + + // Validate the cache is working + assert context.getClassElement("addann.AnnotationMethodClass").get() + .findMethod("myMethod1").get() + .getAnnotationMetadata().getAnnotationNames().asList() == [Executable.class.name, Bean.class.name, MyAnnotation.class.name] + + // Test the second method with the same type doesn't have the annotations + + def myMethod2 = element.findMethod("myMethod2").get() + assert myMethod2.getAnnotationMetadata().getAnnotationNames().asList() == [Executable.class.name, Bean.class.name] + assert myMethod2.getReturnType().getAnnotationNames().isEmpty() + assert myMethod2.getReturnType().getTypeAnnotationMetadata().getAnnotationNames().isEmpty() + + // Validate the cache is working + assert context.getClassElement("addann.AnnotationMethodClass").get() + .findMethod("myMethod2").get() + .getAnnotationMetadata().getAnnotationNames().asList() == [Executable.class.name, Bean.class.name] + + assert context.getClassElement("addann.MyBean1").get().getAnnotationMetadata().isEmpty() + } + } + } + +} diff --git a/inject-groovy/src/test/groovy/io/micronaut/inject/annotation/modify/MyAnnotation.java b/inject-groovy/src/test/groovy/io/micronaut/inject/annotation/modify/MyAnnotation.java new file mode 100644 index 00000000000..7bcb7839599 --- /dev/null +++ b/inject-groovy/src/test/groovy/io/micronaut/inject/annotation/modify/MyAnnotation.java @@ -0,0 +1,28 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.inject.annotation.modify; + +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; + +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.PACKAGE, ElementType.TYPE, ElementType.ANNOTATION_TYPE, ElementType.METHOD, ElementType.FIELD}) +public @interface MyAnnotation { +} diff --git a/inject-groovy/src/test/groovy/io/micronaut/inject/beans/BeanDefinitionSpec.groovy b/inject-groovy/src/test/groovy/io/micronaut/inject/beans/BeanDefinitionSpec.groovy index dbc9567e623..77a4f724b00 100644 --- a/inject-groovy/src/test/groovy/io/micronaut/inject/beans/BeanDefinitionSpec.groovy +++ b/inject-groovy/src/test/groovy/io/micronaut/inject/beans/BeanDefinitionSpec.groovy @@ -2,6 +2,8 @@ package io.micronaut.inject.beans import io.micronaut.ast.transform.test.AbstractBeanDefinitionSpec import io.micronaut.core.annotation.Order +import io.micronaut.core.type.GenericPlaceholder +import io.micronaut.inject.BeanDefinition class BeanDefinitionSpec extends AbstractBeanDefinitionSpec { @@ -64,10 +66,10 @@ class Test { } interface X { - + } class Y implements X { - + } ''') @@ -207,12 +209,12 @@ class OuterBean { static interface OrderedBean { } - + @Requires(property = "spec.name", value = "BeanDefinitionDelegateSpec") @Singleton @Order(value = 10) static class TestOrder implements OrderedBean { - + } } @@ -237,4 +239,83 @@ class TestBean { definition != null } + void "test isTypeVariable"() { + given: + BeanDefinition definition = buildBeanDefinition('test', 'Test', ''' +package test; +import javax.validation.constraints.*; +import java.util.List; + +@jakarta.inject.Singleton +public class Test implements Serde { +} + +interface Serde extends Serializer, Deserializer { +} + +interface Serializer { +} + +interface Deserializer { +} + + + ''') + + when: "Micronaut Serialization use-case" + def serdeTypeParam = definition.getTypeArguments("test.Serde")[0] + def serializerTypeParam = definition.getTypeArguments("test.Serializer")[0] + def deserializerTypeParam = definition.getTypeArguments("test.Deserializer")[0] + + then: "The first is a placeholder" + serdeTypeParam.isTypeVariable() // + (serdeTypeParam instanceof GenericPlaceholder) + and: + serializerTypeParam.isTypeVariable() + (serializerTypeParam instanceof GenericPlaceholder) + deserializerTypeParam.isTypeVariable() + (deserializerTypeParam instanceof GenericPlaceholder) + } + + void "test isTypeVariable array"() { + given: + BeanDefinition definition = buildBeanDefinition('test', 'Test', ''' +package test; +import javax.validation.constraints.*; +import java.util.List; + +@jakarta.inject.Singleton +public class Test implements Serde { +} + +interface Serde extends Serializer, Deserializer { +} + +interface Serializer { +} + +interface Deserializer { +} + + + ''') + + when: "Micronaut Serialization use-case" + def serdeTypeParam = definition.getTypeArguments("test.Serde")[0] + def serializerTypeParam = definition.getTypeArguments("test.Serializer")[0] + def deserializerTypeParam = definition.getTypeArguments("test.Deserializer")[0] + // Arrays are not resolved as GroovyClassElements or placeholders + then: "The first is a placeholder" + serdeTypeParam.simpleName == "String[]" + !serdeTypeParam.isTypeVariable() + !(serdeTypeParam instanceof GenericPlaceholder) + and: "threat resolved placeholder as not a type variable" + serializerTypeParam.simpleName == "String[]" + !serializerTypeParam.isTypeVariable() + !(serializerTypeParam instanceof GenericPlaceholder) + deserializerTypeParam.simpleName == "String[]" + !deserializerTypeParam.isTypeVariable() + !(deserializerTypeParam instanceof GenericPlaceholder) + } + } diff --git a/inject-groovy/src/test/groovy/io/micronaut/inject/executable/Book.java b/inject-groovy/src/test/groovy/io/micronaut/inject/executable/Book.java new file mode 100644 index 00000000000..a882ce4950e --- /dev/null +++ b/inject-groovy/src/test/groovy/io/micronaut/inject/executable/Book.java @@ -0,0 +1,8 @@ +package io.micronaut.inject.executable; + +import io.micronaut.core.annotation.Introspected; + +@MyEntity +@Introspected +public class Book { +} diff --git a/inject-groovy/src/test/groovy/io/micronaut/inject/executable/ExecutableBeanSpec.groovy b/inject-groovy/src/test/groovy/io/micronaut/inject/executable/ExecutableBeanSpec.groovy index 5d6140c9457..48a1354b324 100644 --- a/inject-groovy/src/test/groovy/io/micronaut/inject/executable/ExecutableBeanSpec.groovy +++ b/inject-groovy/src/test/groovy/io/micronaut/inject/executable/ExecutableBeanSpec.groovy @@ -16,7 +16,12 @@ package io.micronaut.inject.executable import io.micronaut.ast.transform.test.AbstractBeanDefinitionSpec +import io.micronaut.context.annotation.BeanProperties +import io.micronaut.core.annotation.Introspected +import io.micronaut.core.type.Argument import io.micronaut.inject.BeanDefinition +import io.micronaut.inject.validation.RequiresValidation +import spock.lang.PendingFeature class ExecutableBeanSpec extends AbstractBeanDefinitionSpec { @@ -187,4 +192,342 @@ class MyBean { definition.findMethod("run2").isPresent() definition.findMethod("run").isPresent() } + + void "test how annotations are preserved"() { + given: + BeanDefinition definition = buildBeanDefinition('test.MyBean','''\ +package test; + +import io.micronaut.inject.annotation.*; +import io.micronaut.context.annotation.*; +import javax.validation.Valid; +import java.util.List; +import io.micronaut.inject.executable.Book; + +@jakarta.inject.Singleton +class MyBean { + + @Executable + public void saveAll(@Valid List books) { + } + + @Executable + public void saveAll2(@Valid List book) { + } + + @Executable + public void saveAll3(@Valid List book) { + } + + @Executable + public void save2(@Valid Book book) { + } + + @Executable + public void save3(@Valid T book) { + } + + @Executable + public Book get() { + return null; + } +} + +''') + when: + def saveAll = definition.findMethod("saveAll", List.class).get() + def listTypeArgument = saveAll.getArguments()[0].getTypeParameters()[0] + then: + !saveAll.hasAnnotation(RequiresValidation) + !saveAll.hasStereotype(RequiresValidation) + !listTypeArgument.getAnnotationMetadata().hasAnnotation(MyEntity.class) + !listTypeArgument.getAnnotationMetadata().hasAnnotation(Introspected.class) + !listTypeArgument.getAnnotationMetadata().hasStereotype(Introspected.class) + !listTypeArgument.getAnnotationMetadata().hasAnnotation(BeanProperties.class) + !listTypeArgument.getAnnotationMetadata().hasStereotype(BeanProperties.class) + + when: + def saveAll2 = definition.findMethod("saveAll2", List.class).get() + def listTypeArgument2 = saveAll2.getArguments()[0].getTypeParameters()[0] + then: + !saveAll2.hasAnnotation(RequiresValidation) + !saveAll2.hasStereotype(RequiresValidation) + !listTypeArgument2.getAnnotationMetadata().hasAnnotation(MyEntity.class) + !listTypeArgument2.getAnnotationMetadata().hasAnnotation(Introspected.class) + !listTypeArgument2.getAnnotationMetadata().hasStereotype(Introspected.class) + !listTypeArgument2.getAnnotationMetadata().hasAnnotation(BeanProperties.class) + !listTypeArgument2.getAnnotationMetadata().hasStereotype(BeanProperties.class) + + when: + def saveAll3 = definition.findMethod("saveAll3", List.class).get() + def listTypeArgument3 = saveAll3.getArguments()[0].getTypeParameters()[0] + then: + !saveAll3.hasAnnotation(RequiresValidation) + !saveAll3.hasStereotype(RequiresValidation) + !listTypeArgument3.getAnnotationMetadata().hasAnnotation(MyEntity.class) + !listTypeArgument3.getAnnotationMetadata().hasAnnotation(Introspected.class) + !listTypeArgument3.getAnnotationMetadata().hasStereotype(Introspected.class) + !listTypeArgument3.getAnnotationMetadata().hasAnnotation(BeanProperties.class) + !listTypeArgument3.getAnnotationMetadata().hasStereotype(BeanProperties.class) + + when: + def save2 = definition.findMethod("save2", Book.class).get() + def parameter2 = save2.getArguments()[0] + then: + !save2.hasAnnotation(RequiresValidation) + !save2.hasStereotype(RequiresValidation) + !parameter2.getAnnotationMetadata().hasAnnotation(MyEntity.class) + !parameter2.getAnnotationMetadata().hasAnnotation(Introspected.class) + !parameter2.getAnnotationMetadata().hasStereotype(Introspected.class) + !parameter2.getAnnotationMetadata().hasAnnotation(BeanProperties.class) + !parameter2.getAnnotationMetadata().hasStereotype(BeanProperties.class) + + when: + def save3 = definition.findMethod("save3", Book.class).get() + def parameter3 = save3.getArguments()[0] + then: + !save3.hasAnnotation(RequiresValidation) + !save3.hasStereotype(RequiresValidation) + !parameter3.getAnnotationMetadata().hasAnnotation(MyEntity.class) + !parameter3.getAnnotationMetadata().hasAnnotation(Introspected.class) + !parameter3.getAnnotationMetadata().hasStereotype(Introspected.class) + !parameter3.getAnnotationMetadata().hasAnnotation(BeanProperties.class) + !parameter3.getAnnotationMetadata().hasStereotype(BeanProperties.class) + + when: + def get = definition.findMethod("get").get() + def returnType = get.getReturnType() + then: + !returnType.getAnnotationMetadata().hasAnnotation(MyEntity.class) + !returnType.getAnnotationMetadata().hasAnnotation(Introspected.class) + !returnType.getAnnotationMetadata().hasStereotype(Introspected.class) + !returnType.getAnnotationMetadata().hasAnnotation(BeanProperties.class) + !returnType.getAnnotationMetadata().hasStereotype(BeanProperties.class) + } + + void "test how the type annotations from the type are preserved 2"() { + given: + BeanDefinition bd = buildBeanDefinition('test.MyBean', '''\ +package test; + +import io.micronaut.inject.annotation.*; +import io.micronaut.context.annotation.*; +import java.util.List; +import io.micronaut.inject.executable.Book; +import io.micronaut.inject.executable.TypeUseRuntimeAnn; + +@jakarta.inject.Singleton +class MyBean { + + @Executable + public void saveAll(List<@TypeUseRuntimeAnn Book> books) { + } + + @Executable + public <@TypeUseRuntimeAnn T extends Book> void saveAll2(List book) { + } + + @Executable + public <@TypeUseRuntimeAnn T extends Book> void saveAll3(List book) { + } + + @Executable + public void saveAll4(List<@TypeUseRuntimeAnn ? extends T> book) { + } + + @Executable + public void saveAll5(List book) { + } + + @Executable + public void save2(@TypeUseRuntimeAnn Book book) { + } + + @Executable + public <@TypeUseRuntimeAnn T extends Book> void save3(T book) { + } + + @Executable + public void save4(T book) { + } + + @Executable + public void save5(@TypeUseRuntimeAnn T book) { + } + + @TypeUseRuntimeAnn + @Executable + public Book get() { + return null; + } +} + +''') + when: + def saveAll = bd.findMethod("saveAll", List).get() + def listTypeArgument = saveAll.getArguments()[0].getTypeParameters()[0] + then: + validateBookArgument(listTypeArgument) + + when: + def saveAll2 = bd.findMethod("saveAll2", List).get() + def listTypeArgument2 = saveAll2.getArguments()[0].getTypeParameters()[0] + then: + validateBookArgument(listTypeArgument2) + +// when: +// def saveAll3 = bd.findMethod("saveAll3", List).get() +// def listTypeArgument3 = saveAll3.getArguments()[0].getTypeParameters()[0] +// then: +// validateBookArgument(listTypeArgument3) + + when: + def saveAll4 = bd.findMethod("saveAll4", List).get() + def listTypeArgument4 = saveAll4.getArguments()[0].getTypeParameters()[0] + then: + validateBookArgument(listTypeArgument4) + +// when: +// def saveAll5 = bd.findMethod("saveAll5", List).get() +// def listTypeArgument5 = saveAll5.getArguments()[0].getTypeParameters()[0] +// then: +// validateBookArgument(listTypeArgument5) + + when: + def save2 = bd.findMethod("save2", Book).get() + def parameter2 = save2.getArguments()[0] + then: + validateBookArgument(parameter2) + + when: + def save3 = bd.findMethod("save3", Book).get() + def parameter3 = save3.getArguments()[0] + then: + validateBookArgument(parameter3) + + when: + def save4 = bd.findMethod("save4", Book).get() + def parameter4 = save4.getArguments()[0] + then: + validateBookArgument(parameter4) + +// when: +// def save5 = bd.findMethod("save5", Book).get() +// def parameter5 = save5.getArguments()[0] +// then: +// validateBookArgument(parameter5) + +// when: +// def get = bd.findMethod("get").get() +// def returnType = get.getReturnType().asArgument() +// then: +// validateBookArgument(returnType) + } + + @PendingFeature + void "test how the type annotations from the type are preserved - not working case"() { + given: + BeanDefinition bd = buildBeanDefinition('test.MyBean', '''\ +package test; + +import io.micronaut.inject.annotation.*; +import io.micronaut.context.annotation.*; +import java.util.List; +import io.micronaut.inject.executable.Book; +import io.micronaut.inject.executable.TypeUseRuntimeAnn; + +@jakarta.inject.Singleton +class MyBean { + + @Executable + public <@TypeUseRuntimeAnn T extends Book> void saveAll3(List book) { + } +} + +''') + when: + def saveAll3 = bd.findMethod("saveAll3", List).get() + def listTypeArgument3 = saveAll3.getArguments()[0].getTypeParameters()[0] + then: + validateBookArgument(listTypeArgument3) + } + + @PendingFeature // Groovy doesn't add the annotation to the return type + void "test how the type annotations are preserved on a method"() { + given: + BeanDefinition bd = buildBeanDefinition('test.MyBean', '''\ +package test; + +import io.micronaut.inject.annotation.*; +import io.micronaut.context.annotation.*; +import java.util.List; +import io.micronaut.inject.executable.Book; +import io.micronaut.inject.executable.TypeUseRuntimeAnn; + +@jakarta.inject.Singleton +class MyBean { + + @TypeUseRuntimeAnn + @Executable + public Book get() { + return null; + } +} + +''') + + when: + def get = bd.findMethod("get").get() + def returnType = get.getReturnType().asArgument() + then: + validateBookArgument(returnType) + } + + @PendingFeature // The actual placeholder with annotations is replaced by typeArguments one + void "test how the type annotations from the type are preserved 3"() { + given: + BeanDefinition bd = buildBeanDefinition('test.MyBean', '''\ +package test; + +import io.micronaut.inject.annotation.*; +import io.micronaut.context.annotation.*; +import java.util.List; +import io.micronaut.inject.executable.Book; +import io.micronaut.inject.executable.TypeUseRuntimeAnn; + +@jakarta.inject.Singleton +class MyBean { + + @Executable + public void saveAll5(List book) { + } + + @Executable + public void save5(@TypeUseRuntimeAnn T book) { + } + +} + +''') + when: + def saveAll5 = bd.findMethod("saveAll5", List).get() + def listTypeArgument5 = saveAll5.getArguments()[0].getTypeParameters()[0] + then: + validateBookArgument(listTypeArgument5) + + when: + def save5 = bd.findMethod("save5", Book).get() + def parameter5 = save5.getArguments()[0] + then: + validateBookArgument(parameter5) + } + + void validateBookArgument(Argument argument) { + // The argument should only have type annotations + def am = argument.getAnnotationMetadata(); + assert am.hasAnnotation(TypeUseRuntimeAnn.class) + assert !am.hasAnnotation(MyEntity.class) + assert !am.hasAnnotation(Introspected.class) + assert am.getAnnotationNames().size() == 1 + } } diff --git a/inject-groovy/src/test/groovy/io/micronaut/inject/executable/MyEntity.java b/inject-groovy/src/test/groovy/io/micronaut/inject/executable/MyEntity.java new file mode 100644 index 00000000000..80b37b9c634 --- /dev/null +++ b/inject-groovy/src/test/groovy/io/micronaut/inject/executable/MyEntity.java @@ -0,0 +1,15 @@ +package io.micronaut.inject.executable; + +import io.micronaut.core.annotation.Internal; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +@Retention(RUNTIME) +@Target({ElementType.METHOD, ElementType.TYPE, ElementType.FIELD, ElementType.PARAMETER}) +@Internal +public @interface MyEntity { +} diff --git a/inject-groovy/src/test/groovy/io/micronaut/inject/executable/TypeUseRuntimeAnn.java b/inject-groovy/src/test/groovy/io/micronaut/inject/executable/TypeUseRuntimeAnn.java new file mode 100644 index 00000000000..df59de6058b --- /dev/null +++ b/inject-groovy/src/test/groovy/io/micronaut/inject/executable/TypeUseRuntimeAnn.java @@ -0,0 +1,11 @@ +package io.micronaut.inject.executable; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.TYPE_USE) +@Retention(RetentionPolicy.RUNTIME) +public @interface TypeUseRuntimeAnn { +} diff --git a/inject-groovy/src/test/groovy/io/micronaut/inject/visitor/BeanIntrospectionSpec.groovy b/inject-groovy/src/test/groovy/io/micronaut/inject/visitor/BeanIntrospectionSpec.groovy index c61db467007..8812d57ced6 100644 --- a/inject-groovy/src/test/groovy/io/micronaut/inject/visitor/BeanIntrospectionSpec.groovy +++ b/inject-groovy/src/test/groovy/io/micronaut/inject/visitor/BeanIntrospectionSpec.groovy @@ -12,6 +12,7 @@ import io.micronaut.core.beans.BeanMethod import io.micronaut.core.beans.BeanProperty import io.micronaut.core.beans.UnsafeBeanProperty import io.micronaut.core.reflect.exception.InstantiationException +import io.micronaut.core.type.GenericPlaceholder import io.micronaut.inject.beans.visitor.IntrospectedTypeElementVisitor import io.micronaut.inject.visitor.introspections.Person import spock.lang.Issue @@ -2317,4 +2318,51 @@ class Test { !secondNameField.hasStereotype("io.micronaut.inject.visitor.TypeUseClassAnn") } + void "test subtypes"() { + given: + BeanIntrospection introspection = buildBeanIntrospection('test.Holder', ''' +package test; +import io.micronaut.core.annotation.Introspected; +import java.util.List; +import java.util.Collections; + +@Introspected +class Animal { +} + +@Introspected +class Cat extends Animal { + final public int lives; + Cat(int lives) { + this.lives = lives; + } +} + +@Introspected(accessKind = Introspected.AccessKind.FIELD) +class Holder { + public final Animal animalNonGeneric; + public final List animalsNonGeneric; + public final A animal; + public final List animals; + Holder(A animal) { + this.animal = animal; + this.animals = Collections.singletonList(animal); + this.animalNonGeneric = animal; + this.animalsNonGeneric = Collections.singletonList(animal); + } +} + + + ''') + + expect: + def animalListArgument = introspection.getProperty("animals").get().asArgument().getTypeParameters()[0] + animalListArgument instanceof GenericPlaceholder + animalListArgument.isTypeVariable() + + def animal = introspection.getProperty("animal").get().asArgument() + animal instanceof GenericPlaceholder + animal.isTypeVariable() + } + } diff --git a/inject-groovy/src/test/groovy/io/micronaut/inject/visitor/ClassElementSpec.groovy b/inject-groovy/src/test/groovy/io/micronaut/inject/visitor/ClassElementSpec.groovy index 7ca5952f4d8..4955b5e23f7 100644 --- a/inject-groovy/src/test/groovy/io/micronaut/inject/visitor/ClassElementSpec.groovy +++ b/inject-groovy/src/test/groovy/io/micronaut/inject/visitor/ClassElementSpec.groovy @@ -1996,6 +1996,261 @@ class MyBean { returnType.hasAnnotation(Introspected.class) } + void "test how the type annotations from the type are propagated"() { + given: + ClassElement ce = buildClassElement('''\ +package test; + +import io.micronaut.inject.annotation.*; +import io.micronaut.context.annotation.*; +import java.util.List; +import io.micronaut.inject.visitor.Book; +import io.micronaut.inject.visitor.TypeUseRuntimeAnn; + +@jakarta.inject.Singleton +class MyBean { + + @Executable + public void saveAll(List<@TypeUseRuntimeAnn Book> books) { + } + + @Executable + public <@TypeUseRuntimeAnn T extends Book> void saveAll2(List book) { + } + + @Executable + public <@TypeUseRuntimeAnn T extends Book> void saveAll3(List book) { + } + + @Executable + public void saveAll4(List<@TypeUseRuntimeAnn ? extends T> book) { + } + + @Executable + public void saveAll5(List book) { + } + + @Executable + public void save2(@TypeUseRuntimeAnn Book book) { + } + + @Executable + public <@TypeUseRuntimeAnn T extends Book> void save3(T book) { + } + + @Executable + public void save4(T book) { + } + + @Executable + public void save5(@TypeUseRuntimeAnn T book) { + } + + @TypeUseRuntimeAnn + @Executable + public Book get() { + return null; + } +} + +''') + when: + def saveAll = ce.findMethod("saveAll").get() + def listTypeArgument = saveAll.getParameters()[0].getGenericType().getTypeArguments(List).get("E") + then: + validateBookArgument(listTypeArgument) + + when: + def saveAll2 = ce.findMethod("saveAll2").get() + def listTypeArgument2 = saveAll2.getParameters()[0].getGenericType().getTypeArguments(List).get("E") + then: + validateBookArgument(listTypeArgument2) + +// when: +// def saveAll3 = ce.findMethod("saveAll3").get() +// def listTypeArgument3 = saveAll3.getParameters()[0].getGenericType().getTypeArguments(List).get("E") +// then: +// validateBookArgument(listTypeArgument3) + + when: + def saveAll4 = ce.findMethod("saveAll4").get() + def listTypeArgument4 = saveAll4.getParameters()[0].getGenericType().getTypeArguments(List).get("E") + then: + validateBookArgument(listTypeArgument4) + +// when: +// def saveAll5 = ce.findMethod("saveAll5").get() +// def listTypeArgument5 = saveAll5.getParameters()[0].getGenericType().getTypeArguments(List).get("E") +// then: +// validateBookArgument(listTypeArgument5) + + when: + def save2 = ce.findMethod("save2").get() + def parameter2 = save2.getParameters()[0].getGenericType() + then: + validateBookArgument(parameter2) + + when: + def save3 = ce.findMethod("save3").get() + def parameter3 = save3.getParameters()[0].getGenericType() + then: + validateBookArgument(parameter3) + + when: + def save4 = ce.findMethod("save4").get() + def parameter4 = save4.getParameters()[0].getGenericType() + then: + validateBookArgument(parameter4) + +// when: +// def save5 = ce.findMethod("save5").get() +// def parameter5 = save5.getParameters()[0].getGenericType() +// then: +// validateBookArgument(parameter5) + + when: + def get = ce.findMethod("get").get() + def returnType = get.getGenericReturnType() + then: + validateBookArgument(returnType) + } + + @PendingFeature + void "test how the type annotations from the type are propagated - not working case"() { + given: + ClassElement ce = buildClassElement('''\ +package test; + +import io.micronaut.inject.annotation.*; +import io.micronaut.context.annotation.*; +import java.util.List; +import io.micronaut.inject.visitor.Book; +import io.micronaut.inject.visitor.TypeUseRuntimeAnn; + +@jakarta.inject.Singleton +class MyBean { + + @Executable + public <@TypeUseRuntimeAnn T extends Book> void saveAll3(List book) { + } +} + +''') + + when: + def saveAll3 = ce.findMethod("saveAll3").get() + def listTypeArgument3 = saveAll3.getParameters()[0].getGenericType().getTypeArguments(List).get("E") + then: + validateBookArgument(listTypeArgument3) + } + + @PendingFeature + void "test how the type annotations from the type are propagated 2"() { + given: + ClassElement ce = buildClassElement('''\ +package test; + +import io.micronaut.inject.annotation.*; +import io.micronaut.context.annotation.*; +import java.util.List; +import io.micronaut.inject.visitor.Book; +import io.micronaut.inject.visitor.TypeUseRuntimeAnn; + +@jakarta.inject.Singleton +class MyBean { + + @Executable + public void saveAll5(List book) { + } + + @Executable + public void save5(@TypeUseRuntimeAnn T book) { + } + +} + +''') + when: + def saveAll5 = ce.findMethod("saveAll5").get() + def listTypeArgument5 = saveAll5.getParameters()[0].getGenericType().getTypeArguments(List).get("E") + then: + validateBookArgument(listTypeArgument5) + + when: + def save5 = ce.findMethod("save5").get() + def parameter5 = save5.getParameters()[0].getGenericType() + then: + validateBookArgument(parameter5) + } + + void "test interface placeholder"() { + ClassElement ce = buildClassElement(''' +package test; +import java.util.List; + +interface Repo extends GenericRepository { +} + +interface GenericRepository { +} + + +class MyBean { + private String name; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } +} + +class MyRepo implements Repo { +} + +''') + + when: + def repo = ce.getTypeArguments("test.Repo") + then: + repo.get("E").simpleName == "MyBean" + repo.get("E").getMethods().size() == 2 + repo.get("E").getFields().size() == 1 + when: + def genRepo = ce.getTypeArguments("test.GenericRepository") + then: + genRepo.get("E").simpleName == "MyBean" + genRepo.get("E").getMethods().size() == 2 + genRepo.get("E").getFields().size() == 1 + when: + def interfaces = ce.getInterfaces() + then: + interfaces.size() == 1 + interfaces[0].simpleName == "Repo" + } + + void validateBookArgument(ClassElement classElement) { + // The class element should have all the annotations present + assert classElement.hasAnnotation(TypeUseRuntimeAnn.class) + assert classElement.hasAnnotation(MyEntity.class) + assert classElement.hasAnnotation(Introspected.class) + + def typeAnnotationMetadata = classElement.getTypeAnnotationMetadata() + // The type annotations should have only type annotations + assert typeAnnotationMetadata.hasAnnotation(TypeUseRuntimeAnn.class) + assert !typeAnnotationMetadata.hasAnnotation(MyEntity.class) + assert !typeAnnotationMetadata.hasAnnotation(Introspected.class) + + // Get the actual type -> the type shouldn't have any type annotations + def type = classElement.getType() + assert !type.hasAnnotation(TypeUseRuntimeAnn.class) + assert type.hasAnnotation(MyEntity.class) + assert type.hasAnnotation(Introspected.class) + assert type.getTypeAnnotationMetadata().isEmpty() + } + private void assertListGenericArgument(ClassElement type, Closure cl) { def arg1 = type.getAllTypeArguments().get(List.class.name).get("E") def arg2 = type.getAllTypeArguments().get(Collection.class.name).get("E") diff --git a/inject-groovy/src/test/groovy/io/micronaut/inject/visitor/TypeUseRuntimeAnn.java b/inject-groovy/src/test/groovy/io/micronaut/inject/visitor/TypeUseRuntimeAnn.java index 40f18a22baa..adcb01e5ba0 100644 --- a/inject-groovy/src/test/groovy/io/micronaut/inject/visitor/TypeUseRuntimeAnn.java +++ b/inject-groovy/src/test/groovy/io/micronaut/inject/visitor/TypeUseRuntimeAnn.java @@ -5,7 +5,7 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; -@Target(ElementType.TYPE_USE) +@Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) @Retention(RetentionPolicy.RUNTIME) @interface TypeUseRuntimeAnn { } diff --git a/inject-java-test/src/main/groovy/io/micronaut/annotation/processing/test/AbstractTypeElementSpec.groovy b/inject-java-test/src/main/groovy/io/micronaut/annotation/processing/test/AbstractTypeElementSpec.groovy index 802a6188940..380b2aa56d0 100644 --- a/inject-java-test/src/main/groovy/io/micronaut/annotation/processing/test/AbstractTypeElementSpec.groovy +++ b/inject-java-test/src/main/groovy/io/micronaut/annotation/processing/test/AbstractTypeElementSpec.groovy @@ -239,13 +239,14 @@ class Test { * @param cls The class data * @return The context. Should be shutdown after use */ - ApplicationContext buildContext(String className, @Language("java") String cls, boolean includeAllBeans = false) { + ApplicationContext buildContext(String className, @Language("java") String cls, boolean includeAllBeans = false, Map properties = [:]) { def files = newJavaParser().generate(className, cls) ClassLoader classLoader = new JavaFileObjectClassLoader(files) def builder = ApplicationContext.builder() builder.classLoader(classLoader) builder.environments("test") + builder.properties(properties) configureContext(builder) def env = builder.build().environment def context = new DefaultApplicationContext((ApplicationContextConfiguration) builder) { @@ -567,13 +568,15 @@ class Test { if (classElement.isArray()) { return reconstructTypeSignature(classElement.fromArray()) + "[]" } else if (classElement.isGenericPlaceholder()) { - def freeVar = (GenericPlaceholderElement) classElement - def name = freeVar.variableName + def genericPlaceholderElement = (GenericPlaceholderElement) classElement + def name = genericPlaceholderElement.variableName if (typeVarsAsDeclarations) { - def bounds = freeVar.bounds - if ( reconstructTypeSignature(bounds[0]) != 'Object') { + def bounds = genericPlaceholderElement.bounds + if (reconstructTypeSignature(bounds[0]) != 'Object') { name += bounds.stream().map(AbstractTypeElementSpec::reconstructTypeSignature).collect(Collectors.joining(" & ", " extends ", "")) } + } else if (genericPlaceholderElement.resolved) { + return reconstructTypeSignature(genericPlaceholderElement.resolved.get()) } return name } else if (classElement.isWildcard()) { diff --git a/inject-java-test/src/test/groovy/io/micronaut/inject/visitor/beans/BeanIntrospectionSpec.groovy b/inject-java-test/src/test/groovy/io/micronaut/inject/visitor/beans/BeanIntrospectionSpec.groovy index a489ae49199..ad0b2f07c2b 100644 --- a/inject-java-test/src/test/groovy/io/micronaut/inject/visitor/beans/BeanIntrospectionSpec.groovy +++ b/inject-java-test/src/test/groovy/io/micronaut/inject/visitor/beans/BeanIntrospectionSpec.groovy @@ -24,6 +24,7 @@ import io.micronaut.core.convert.TypeConverter import io.micronaut.core.reflect.InstantiationUtils import io.micronaut.core.reflect.exception.InstantiationException import io.micronaut.core.type.Argument +import io.micronaut.core.type.GenericPlaceholder import io.micronaut.inject.ExecutableMethod import io.micronaut.inject.beans.visitor.IntrospectedTypeElementVisitor import io.micronaut.inject.visitor.TypeElementVisitor @@ -4524,6 +4525,93 @@ class Book { property.asArgument().getTypeParameters()[0].annotationMetadata.hasStereotype("javax.validation.Valid") } + void "test type_use annotations"() { + given: + def introspection = buildBeanIntrospection('test.Test', ''' +package test; +import io.micronaut.core.annotation.Introspected; +import io.micronaut.context.annotation.*; +import io.micronaut.inject.visitor.*; +@Introspected +class Test { + @io.micronaut.inject.visitor.beans.TypeUseRuntimeAnn + private String name; + @io.micronaut.inject.visitor.beans.TypeUseClassAnn + private String secondName; + public String getName() { + return name; + } + public void setName(String name) { + this.name = name; + } + public String getSecondName() { + return name; + } + public void setSecondName(String secondName) { + this.secondName = secondName; + } +} +''') + def nameField = introspection.getProperty("name").orElse(null) + def secondNameField = introspection.getProperty("secondName").orElse(null) + + expect: + nameField + secondNameField + + nameField.hasStereotype(TypeUseRuntimeAnn) + nameField.hasStereotype("io.micronaut.inject.visitor.beans.TypeUseRuntimeAnn") + !secondNameField.hasStereotype(TypeUseClassAnn) + !secondNameField.hasStereotype("io.micronaut.inject.visitor.beans.TypeUseClassAnn") + } + + void "test subtypes"() { + given: + BeanIntrospection introspection = buildBeanIntrospection('test.Holder', ''' +package test; +import io.micronaut.core.annotation.Introspected; +import java.util.List; +import java.util.Collections; + +@Introspected +class Animal { +} + +@Introspected +class Cat extends Animal { + final public int lives; + Cat(int lives) { + this.lives = lives; + } +} + +@Introspected(accessKind = Introspected.AccessKind.FIELD) +class Holder { + public final Animal animalNonGeneric; + public final List animalsNonGeneric; + public final A animal; + public final List animals; + Holder(A animal) { + this.animal = animal; + this.animals = Collections.singletonList(animal); + this.animalNonGeneric = animal; + this.animalsNonGeneric = Collections.singletonList(animal); + } +} + + + ''') + + expect: + def animalListArgument = introspection.getProperty("animals").get().asArgument().getTypeParameters()[0] + animalListArgument instanceof GenericPlaceholder + animalListArgument.isTypeVariable() + + def animal = introspection.getProperty("animal").get().asArgument() + animal instanceof GenericPlaceholder + animal.isTypeVariable() + } + @Override protected JavaParser newJavaParser() { return new JavaParser() { diff --git a/inject-java-test/src/test/groovy/io/micronaut/inject/visitor/beans/TypeUseClassAnn.java b/inject-java-test/src/test/groovy/io/micronaut/inject/visitor/beans/TypeUseClassAnn.java new file mode 100644 index 00000000000..06c1d62851f --- /dev/null +++ b/inject-java-test/src/test/groovy/io/micronaut/inject/visitor/beans/TypeUseClassAnn.java @@ -0,0 +1,11 @@ +package io.micronaut.inject.visitor.beans; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.TYPE_USE) +@Retention(RetentionPolicy.CLASS) +public @interface TypeUseClassAnn { +} diff --git a/inject-java-test/src/test/groovy/io/micronaut/inject/visitor/beans/TypeUseRuntimeAnn.java b/inject-java-test/src/test/groovy/io/micronaut/inject/visitor/beans/TypeUseRuntimeAnn.java new file mode 100644 index 00000000000..20c0948736e --- /dev/null +++ b/inject-java-test/src/test/groovy/io/micronaut/inject/visitor/beans/TypeUseRuntimeAnn.java @@ -0,0 +1,11 @@ +package io.micronaut.inject.visitor.beans; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.TYPE_USE) +@Retention(RetentionPolicy.RUNTIME) +public @interface TypeUseRuntimeAnn { +} diff --git a/inject-java/build.gradle b/inject-java/build.gradle index 0eb0cf4169d..1e4f20c564c 100644 --- a/inject-java/build.gradle +++ b/inject-java/build.gradle @@ -57,7 +57,7 @@ dependencies { tasks.withType(Test).configureEach { forkEvery = 100 - maxParallelForks = 2 + maxParallelForks = 4 useJUnitPlatform() } diff --git a/inject-java/src/main/java/io/micronaut/annotation/processing/AnnotationProcessingOutputVisitor.java b/inject-java/src/main/java/io/micronaut/annotation/processing/AnnotationProcessingOutputVisitor.java index e779558e577..44f0525bc86 100644 --- a/inject-java/src/main/java/io/micronaut/annotation/processing/AnnotationProcessingOutputVisitor.java +++ b/inject-java/src/main/java/io/micronaut/annotation/processing/AnnotationProcessingOutputVisitor.java @@ -15,6 +15,7 @@ */ package io.micronaut.annotation.processing; +import io.micronaut.annotation.processing.visitor.JavaNativeElement; import io.micronaut.core.annotation.Nullable; import io.micronaut.core.util.ArrayUtils; import io.micronaut.inject.writer.AbstractClassWriterOutputVisitor; @@ -96,8 +97,8 @@ public OutputStream visitClass(String classname, io.micronaut.inject.ast.Element List list = new ArrayList<>(originatingElements.length); for (io.micronaut.inject.ast.Element originatingElement : originatingElements) { Object nativeType = originatingElement.getNativeType(); - if (nativeType instanceof Element) { - list.add((Element) nativeType); + if (nativeType instanceof JavaNativeElement javaNativeElement) { + list.add(javaNativeElement.element()); } } nativeOriginatingElements = list.toArray(new Element[0]); @@ -118,7 +119,7 @@ public void visitServiceDescriptor(String type, String classname, io.micronaut.i StandardLocation.CLASS_OUTPUT, "", path, - (Element) originatingElement.getNativeType() + ((JavaNativeElement) originatingElement.getNativeType()).element() ); try (Writer w = fileObject.openWriter()) { w.write(""); @@ -133,7 +134,7 @@ public Optional visitMetaInfFile(String path, io.micronaut.inject return metaInfFiles.computeIfAbsent(path, s -> { String finalPath = "META-INF/" + path; Element[] nativeOriginatingElements = Arrays.stream(originatingElements) - .map(e -> (Element) e.getNativeType()).toArray(Element[]::new); + .map(e -> ((JavaNativeElement) e.getNativeType()).element()).toArray(Element[]::new); return Optional.of(new GeneratedFileObject(finalPath, nativeOriginatingElements)); }); } diff --git a/inject-java/src/main/java/io/micronaut/annotation/processing/AnnotationsElement.java b/inject-java/src/main/java/io/micronaut/annotation/processing/AnnotationsElement.java new file mode 100644 index 00000000000..536a6bc6434 --- /dev/null +++ b/inject-java/src/main/java/io/micronaut/annotation/processing/AnnotationsElement.java @@ -0,0 +1,115 @@ +/* + * Copyright 2017-2023 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.annotation.processing; + +import io.micronaut.core.annotation.Internal; + +import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.element.Element; +import javax.lang.model.element.ElementKind; +import javax.lang.model.element.ElementVisitor; +import javax.lang.model.element.Modifier; +import javax.lang.model.element.Name; +import javax.lang.model.type.TypeMirror; +import java.lang.annotation.Annotation; +import java.util.List; +import java.util.Objects; +import java.util.Set; + +/** + * A custom Java Lang Model element that provides the annotations from the type mirror. + * @author Denis Stepanov + * @since 4.0.0 + */ +@Internal +final class AnnotationsElement implements Element { + private final TypeMirror typeMirror; + + public AnnotationsElement(TypeMirror clazz) { + this.typeMirror = clazz; + } + + @Override + public TypeMirror asType() { + return typeMirror; + } + + @Override + public ElementKind getKind() { + return ElementKind.OTHER; + } + + @Override + public Set getModifiers() { + throw notSupportedMethod(); + } + + @Override + public Name getSimpleName() { + throw notSupportedMethod(); + } + + @Override + public Element getEnclosingElement() { + throw notSupportedMethod(); + } + + @Override + public List getEnclosedElements() { + throw notSupportedMethod(); + } + + @Override + public List getAnnotationMirrors() { + return typeMirror.getAnnotationMirrors(); + } + + @Override + public A getAnnotation(Class annotationType) { + return typeMirror.getAnnotation(annotationType); + } + + @Override + public A[] getAnnotationsByType(Class annotationType) { + return typeMirror.getAnnotationsByType(annotationType); + } + + @Override + public R accept(ElementVisitor v, P p) { + throw notSupportedMethod(); + } + + private static IllegalStateException notSupportedMethod() { + return new IllegalStateException("Not supported method"); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + AnnotationsElement that = (AnnotationsElement) o; + return Objects.equals(typeMirror, that.typeMirror); + } + + @Override + public int hashCode() { + return typeMirror.hashCode(); + } +} diff --git a/inject-java/src/main/java/io/micronaut/annotation/processing/BeanDefinitionInjectProcessor.java b/inject-java/src/main/java/io/micronaut/annotation/processing/BeanDefinitionInjectProcessor.java index a8ace115984..c976fde94a7 100644 --- a/inject-java/src/main/java/io/micronaut/annotation/processing/BeanDefinitionInjectProcessor.java +++ b/inject-java/src/main/java/io/micronaut/annotation/processing/BeanDefinitionInjectProcessor.java @@ -16,6 +16,7 @@ package io.micronaut.annotation.processing; import io.micronaut.annotation.processing.visitor.JavaClassElement; +import io.micronaut.annotation.processing.visitor.JavaNativeElement; import io.micronaut.annotation.processing.visitor.JavaVisitorContext; import io.micronaut.context.annotation.ConfigurationReader; import io.micronaut.context.annotation.Context; @@ -33,7 +34,6 @@ import io.micronaut.inject.processing.JavaModelUtils; import io.micronaut.inject.processing.ProcessingException; import io.micronaut.inject.visitor.BeanElementVisitor; -import io.micronaut.inject.visitor.VisitorConfiguration; import io.micronaut.inject.writer.AbstractBeanDefinitionBuilder; import io.micronaut.inject.writer.BeanDefinitionReferenceWriter; import io.micronaut.inject.writer.BeanDefinitionVisitor; @@ -131,18 +131,7 @@ protected JavaVisitorContext newVisitorContext(@NonNull ProcessingEnvironment pr filer, visitorAttributes, getVisitorKind() - ) { - @NonNull - @Override - public VisitorConfiguration getConfiguration() { - return new VisitorConfiguration() { - @Override - public boolean includeTypeLevelAnnotationsInGenericArguments() { - return false; - } - }; - } - }; + ); } @Override @@ -235,7 +224,7 @@ public final boolean process(Set annotations, RoundEnviro } } } catch (ProcessingException ex) { - error((Element) ex.getOriginatingElement(), ex.getMessage()); + error(((JavaNativeElement) ex.getOriginatingElement()).element(), ex.getMessage()); } catch (PostponeToNextRoundException e) { processed.remove(className); postponed.put(className, e); diff --git a/inject-java/src/main/java/io/micronaut/annotation/processing/JavaElementAnnotationMetadataFactory.java b/inject-java/src/main/java/io/micronaut/annotation/processing/JavaElementAnnotationMetadataFactory.java index 09b9ab90a8f..7e3f6bd6b87 100644 --- a/inject-java/src/main/java/io/micronaut/annotation/processing/JavaElementAnnotationMetadataFactory.java +++ b/inject-java/src/main/java/io/micronaut/annotation/processing/JavaElementAnnotationMetadataFactory.java @@ -16,13 +16,21 @@ package io.micronaut.annotation.processing; import io.micronaut.annotation.processing.visitor.AbstractJavaElement; +import io.micronaut.annotation.processing.visitor.JavaNativeElement; import io.micronaut.core.annotation.AnnotationMetadata; +import io.micronaut.inject.annotation.AbstractAnnotationMetadataBuilder; +import io.micronaut.inject.ast.ClassElement; +import io.micronaut.inject.ast.GenericPlaceholderElement; +import io.micronaut.inject.ast.WildcardElement; import io.micronaut.inject.ast.annotation.AbstractElementAnnotationMetadataFactory; import io.micronaut.inject.ast.annotation.ElementAnnotationMetadata; import io.micronaut.inject.ast.annotation.ElementAnnotationMetadataFactory; import javax.lang.model.element.AnnotationMirror; import javax.lang.model.element.Element; +import javax.lang.model.type.TypeMirror; +import javax.lang.model.type.TypeVariable; +import javax.lang.model.type.WildcardType; /** * Java element annotation metadata factory. @@ -47,24 +55,59 @@ public ElementAnnotationMetadataFactory readOnly() { @Override public ElementAnnotationMetadata build(io.micronaut.inject.ast.Element element) { AbstractJavaElement javaElement = (AbstractJavaElement) element; - if (notAllowedAnnotations(javaElement)) { + if (!allowedAnnotations(javaElement)) { return EMPTY; } return super.build(element); } - private static boolean notAllowedAnnotations(AbstractJavaElement javaElement) { - return !(javaElement.getNativeType() instanceof Element); + private static boolean allowedAnnotations(AbstractJavaElement javaElement) { + return javaElement.getNativeType().element() != null; } @Override public ElementAnnotationMetadata build(io.micronaut.inject.ast.Element element, AnnotationMetadata defaultAnnotationMetadata) { if (defaultAnnotationMetadata == null) { AbstractJavaElement javaElement = (AbstractJavaElement) element; - if (notAllowedAnnotations(javaElement)) { + if (!allowedAnnotations(javaElement)) { return EMPTY; } } return super.build(element, defaultAnnotationMetadata); } + + @Override + protected Element getNativeElement(io.micronaut.inject.ast.Element element) { + return ((AbstractJavaElement) element).getNativeType().element(); + } + + @Override + protected AbstractAnnotationMetadataBuilder.CachedAnnotationMetadata lookupTypeAnnotationsForClass(ClassElement classElement) { + JavaNativeElement.Class clazz = (JavaNativeElement.Class) classElement.getNativeType(); + TypeMirror typeMirror = clazz.typeMirror(); + if (typeMirror == null) { + return super.lookupTypeAnnotationsForClass(classElement); + } + return metadataBuilder.lookupOrBuild(clazz, new AnnotationsElement(typeMirror), true); + } + + @Override + protected AbstractAnnotationMetadataBuilder.CachedAnnotationMetadata lookupTypeAnnotationsForGenericPlaceholder(GenericPlaceholderElement placeholderElement) { + JavaNativeElement.Placeholder genericNativeType = (JavaNativeElement.Placeholder) placeholderElement.getGenericNativeType(); + Element placeholderJavaElement; + TypeVariable placeholderTypeVariable = genericNativeType.typeVariable(); + if (placeholderTypeVariable.getAnnotationMirrors().size() > 0) { + placeholderJavaElement = new AnnotationsElement(placeholderTypeVariable); + } else { + placeholderJavaElement = genericNativeType.element(); + } + return metadataBuilder.lookupOrBuild(genericNativeType, placeholderJavaElement); + } + + @Override + protected AbstractAnnotationMetadataBuilder.CachedAnnotationMetadata lookupTypeAnnotationsForWildcard(WildcardElement wildcardElement) { + WildcardType wildcard = (WildcardType) wildcardElement.getGenericNativeType(); + return metadataBuilder.lookupOrBuild(wildcard, new AnnotationsElement(wildcard), true); + } + } diff --git a/inject-java/src/main/java/io/micronaut/annotation/processing/TypeElementVisitorProcessor.java b/inject-java/src/main/java/io/micronaut/annotation/processing/TypeElementVisitorProcessor.java index 49394e2fb27..6c32424e222 100644 --- a/inject-java/src/main/java/io/micronaut/annotation/processing/TypeElementVisitorProcessor.java +++ b/inject-java/src/main/java/io/micronaut/annotation/processing/TypeElementVisitorProcessor.java @@ -17,6 +17,7 @@ import io.micronaut.annotation.processing.visitor.JavaClassElement; import io.micronaut.annotation.processing.visitor.JavaElementFactory; +import io.micronaut.annotation.processing.visitor.JavaNativeElement; import io.micronaut.annotation.processing.visitor.LoadedVisitor; import io.micronaut.aop.Introduction; import io.micronaut.context.annotation.Requires; @@ -251,20 +252,25 @@ public boolean process(Set annotations, RoundEnvironment // Micronaut Data use-case: EntityMapper with a higher priority needs to process entities first // before RepositoryMapper is going to process repositories and read entities + List javaClassElements = elements.stream() + .map(typeElement -> elementFactory.newSourceClassElement(typeElement, elementAnnotationMetadataFactory)) + .toList(); + for (LoadedVisitor loadedVisitor : loadedVisitors) { - for (TypeElement typeElement : elements) { + for (JavaClassElement javaClassElement : javaClassElements) { try { - JavaClassElement javaClassElement = elementFactory.newSourceClassElement( - typeElement, - elementAnnotationMetadataFactory - ); if (!loadedVisitor.matchesClass(javaClassElement)) { continue; } + TypeElement typeElement = javaClassElement.getNativeType().element(); String className = typeElement.getQualifiedName().toString(); typeElement.accept(new ElementVisitor(javaClassElement, typeElement, Collections.singletonList(loadedVisitor)), className); } catch (ProcessingException e) { - error((Element) e.getOriginatingElement(), e.getMessage()); + JavaNativeElement originatingElement = (JavaNativeElement) e.getOriginatingElement(); + if (originatingElement == null) { + originatingElement = javaClassElement.getNativeType(); + } + error(originatingElement.element(), e.getMessage()); } catch (PostponeToNextRoundException e) { // Ignore continue; @@ -392,7 +398,7 @@ public Object visitUnknown(Element e, Object o) { @Override public Object visitType(TypeElement classElement, Object o) { - if (!classElement.equals(javaClassElement.getNativeTypeElement())) { + if (!classElement.equals(javaClassElement.getNativeType().element())) { javaClassElement = javaVisitorContext.getElementFactory().newSourceClassElement( classElement, javaVisitorContext.getElementAnnotationMetadataFactory() diff --git a/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/AbstractJavaElement.java b/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/AbstractJavaElement.java index 3a7dce472e8..ef521546ebf 100644 --- a/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/AbstractJavaElement.java +++ b/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/AbstractJavaElement.java @@ -15,10 +15,10 @@ */ package io.micronaut.annotation.processing.visitor; -import io.micronaut.annotation.processing.AnnotationUtils; import io.micronaut.core.annotation.AnnotationMetadata; import io.micronaut.core.annotation.AnnotationValue; import io.micronaut.core.annotation.AnnotationValueBuilder; +import io.micronaut.core.annotation.Internal; import io.micronaut.core.annotation.NonNull; import io.micronaut.core.annotation.Nullable; import io.micronaut.core.util.CollectionUtils; @@ -29,12 +29,11 @@ import io.micronaut.inject.ast.WildcardElement; import io.micronaut.inject.ast.annotation.ElementAnnotationMetadata; import io.micronaut.inject.ast.annotation.ElementAnnotationMetadataFactory; -import io.micronaut.inject.ast.annotation.ElementMutableAnnotationMetadataDelegate; import io.micronaut.inject.ast.annotation.MutableAnnotationMetadataDelegate; -import javax.lang.model.element.AnnotationMirror; import javax.lang.model.element.Element; import javax.lang.model.element.ElementKind; +import javax.lang.model.element.ExecutableElement; import javax.lang.model.element.Modifier; import javax.lang.model.element.TypeElement; import javax.lang.model.element.TypeParameterElement; @@ -72,42 +71,31 @@ * @author graemerocher * @since 1.0 */ -public abstract class AbstractJavaElement implements io.micronaut.inject.ast.Element, ElementMutableAnnotationMetadataDelegate { +@Internal +public abstract class AbstractJavaElement implements io.micronaut.inject.ast.Element { protected final JavaVisitorContext visitorContext; protected final ElementAnnotationMetadataFactory elementAnnotationMetadataFactory; @Nullable protected AnnotationMetadata presetAnnotationMetadata; - private final Element element; + private final JavaNativeElement nativeElement; @Nullable private ElementAnnotationMetadata elementAnnotationMetadata; /** - * @param element The {@link Element} + * @param nativeElement The {@link Element} * @param annotationMetadataFactory The annotation metadata factory * @param visitorContext The Java visitor context */ - AbstractJavaElement(Element element, ElementAnnotationMetadataFactory annotationMetadataFactory, JavaVisitorContext visitorContext) { - this.element = element; + AbstractJavaElement(JavaNativeElement nativeElement, ElementAnnotationMetadataFactory annotationMetadataFactory, JavaVisitorContext visitorContext) { + this.nativeElement = nativeElement; this.elementAnnotationMetadataFactory = annotationMetadataFactory; this.visitorContext = visitorContext; } @Override - public io.micronaut.inject.ast.Element getReturnInstance() { - return this; - } - - @Override - public MutableAnnotationMetadataDelegate getAnnotationMetadata() { - if (elementAnnotationMetadata == null) { - if (presetAnnotationMetadata == null) { - elementAnnotationMetadata = elementAnnotationMetadataFactory.build(this); - } else { - elementAnnotationMetadata = elementAnnotationMetadataFactory.build(this, presetAnnotationMetadata); - } - } - return elementAnnotationMetadata; + public AnnotationMetadata getAnnotationMetadata() { + return getElementAnnotationMetadata(); } /** @@ -135,59 +123,92 @@ public io.micronaut.inject.ast.Element withAnnotationMetadata(AnnotationMetadata return abstractJavaElement; } + /** + * @return The element's annotation metadata + */ + protected ElementAnnotationMetadata getElementAnnotationMetadata() { + if (elementAnnotationMetadata == null) { + if (presetAnnotationMetadata == null) { + elementAnnotationMetadata = elementAnnotationMetadataFactory.build(this); + } else { + elementAnnotationMetadata = elementAnnotationMetadataFactory.build(this, presetAnnotationMetadata); + } + } + return elementAnnotationMetadata; + } + + /** + * Get annotation metadata to add or remove annotations. + * + * @return The annotation metadata to write + */ + protected MutableAnnotationMetadataDelegate getAnnotationMetadataToWrite() { + return getElementAnnotationMetadata(); + } + @Override public io.micronaut.inject.ast.Element annotate(String annotationType, Consumer> consumer) { - return ElementMutableAnnotationMetadataDelegate.super.annotate(annotationType, consumer); + getAnnotationMetadataToWrite().annotate(annotationType, consumer); + return this; } @Override public io.micronaut.inject.ast.Element removeAnnotation(String annotationType) { - return ElementMutableAnnotationMetadataDelegate.super.removeAnnotation(annotationType); + getAnnotationMetadataToWrite().removeAnnotation(annotationType); + return this; } @Override public io.micronaut.inject.ast.Element removeAnnotation(Class annotationType) { - return ElementMutableAnnotationMetadataDelegate.super.removeAnnotation(annotationType); + getAnnotationMetadataToWrite().removeAnnotation(annotationType); + return this; } @Override public io.micronaut.inject.ast.Element removeAnnotationIf(Predicate> predicate) { - return ElementMutableAnnotationMetadataDelegate.super.removeAnnotationIf(predicate); + getAnnotationMetadataToWrite().removeAnnotationIf(predicate); + return this; } @Override public io.micronaut.inject.ast.Element removeStereotype(String annotationType) { - return ElementMutableAnnotationMetadataDelegate.super.removeStereotype(annotationType); + getAnnotationMetadataToWrite().removeStereotype(annotationType); + return this; } @Override public io.micronaut.inject.ast.Element removeStereotype(Class annotationType) { - return ElementMutableAnnotationMetadataDelegate.super.removeStereotype(annotationType); + getAnnotationMetadataToWrite().removeStereotype(annotationType); + return this; } @Override public io.micronaut.inject.ast.Element annotate(String annotationType) { - return ElementMutableAnnotationMetadataDelegate.super.annotate(annotationType); + getAnnotationMetadataToWrite().annotate(annotationType); + return this; } @Override public io.micronaut.inject.ast.Element annotate(Class annotationType, Consumer> consumer) { - return ElementMutableAnnotationMetadataDelegate.super.annotate(annotationType, consumer); + getAnnotationMetadataToWrite().annotate(annotationType, consumer); + return this; } @Override public io.micronaut.inject.ast.Element annotate(Class annotationType) { - return ElementMutableAnnotationMetadataDelegate.super.annotate(annotationType); + getAnnotationMetadataToWrite().annotate(annotationType); + return this; } @Override public io.micronaut.inject.ast.Element annotate(AnnotationValue annotationValue) { - return ElementMutableAnnotationMetadataDelegate.super.annotate(annotationValue); + getAnnotationMetadataToWrite().annotate(annotationValue); + return this; } @Override public boolean isPackagePrivate() { - Set modifiers = element.getModifiers(); + Set modifiers = nativeElement.element().getModifiers(); return !(modifiers.contains(PUBLIC) || modifiers.contains(PROTECTED) || modifiers.contains(PRIVATE)); @@ -195,12 +216,12 @@ public boolean isPackagePrivate() { @Override public String getName() { - return element.getSimpleName().toString(); + return nativeElement.element().getSimpleName().toString(); } @Override public Set getModifiers() { - return element + return nativeElement.element() .getModifiers().stream() .map(m -> ElementModifier.valueOf(m.name())) .collect(Collectors.toSet()); @@ -208,7 +229,7 @@ public Set getModifiers() { @Override public Optional getDocumentation() { - String doc = visitorContext.getElements().getDocComment(element); + String doc = visitorContext.getElements().getDocComment(nativeElement.element()); return Optional.ofNullable(doc != null ? doc.trim() : null); } @@ -243,13 +264,28 @@ public boolean isProtected() { } @Override - public Object getNativeType() { - return element; + public JavaNativeElement getNativeType() { + return nativeElement; } @Override public String toString() { - return element.toString(); + return nativeElement.element().toString(); + } + + /** + * Obtain the ClassElement for the given mirror. + * + * @param owner The owner + * @param type The type + * @param declaredElementTypeArguments The type arguments of the declaring element (method, class) + * @return The class element + */ + @NonNull + protected final ClassElement newClassElement(JavaNativeElement owner, + TypeMirror type, + Map declaredElementTypeArguments) { + return newClassElement(owner, type, declaredElementTypeArguments, new HashSet<>(), false); } /** @@ -262,23 +298,23 @@ public String toString() { @NonNull protected final ClassElement newClassElement(TypeMirror type, Map declaredElementTypeArguments) { - return newClassElement(type, declaredElementTypeArguments, new HashSet<>(), true, false); + return newClassElement(null, type, declaredElementTypeArguments, new HashSet<>(), false); } @NonNull - private ClassElement newClassElement(TypeMirror type, + private ClassElement newClassElement(JavaNativeElement owner, + TypeMirror type, Map declaredTypeArguments, Set visitedTypes, - boolean includeTypeAnnotations, boolean isTypeVariable) { - return newClassElement(type, declaredTypeArguments, visitedTypes, includeTypeAnnotations, isTypeVariable, false, null); + return newClassElement(owner, type, declaredTypeArguments, visitedTypes, isTypeVariable, false, null); } @NonNull - private ClassElement newClassElement(TypeMirror type, + private ClassElement newClassElement(JavaNativeElement owner, + TypeMirror type, Map declaredTypeArguments, Set visitedTypes, - boolean includeTypeAnnotations, boolean isTypeVariable, boolean isRawTypeParameter, @Nullable @@ -290,15 +326,15 @@ private ClassElement newClassElement(TypeMirror type, return PrimitiveElement.VOID; } if (type instanceof DeclaredType dt) { - Element e = dt.asElement(); + Element element = dt.asElement(); // Declared types can wrap other types, like primitives - if (!(e.asType() instanceof DeclaredType)) { - return newClassElement(e.asType(), declaredTypeArguments, visitedTypes, includeTypeAnnotations, isTypeVariable); + if (!(element.asType() instanceof DeclaredType)) { + return newClassElement(owner, element.asType(), declaredTypeArguments, visitedTypes, isTypeVariable); } - if (e instanceof TypeElement typeElement) { + if (element instanceof TypeElement typeElement) { List typeMirrorArguments = dt.getTypeArguments(); Map resolvedTypeArguments; - if (visitedTypes.contains(dt) || typeElement.equals(element)) { + if (visitedTypes.contains(dt) || typeElement.equals(nativeElement.element())) { ClassElement objectElement = visitorContext.getClassElement("java.lang.Object").get(); List typeParameters = typeElement.getTypeParameters(); Map resolved = CollectionUtils.newHashMap(typeMirrorArguments.size()); @@ -309,48 +345,47 @@ private ClassElement newClassElement(TypeMirror type, resolvedTypeArguments = resolved; } else { visitedTypes.add(dt); - resolvedTypeArguments = resolveTypeArguments(typeElement, typeMirrorArguments, declaredTypeArguments, visitedTypes); + resolvedTypeArguments = resolveTypeArguments(typeElement.getTypeParameters(), typeMirrorArguments, declaredTypeArguments, visitedTypes); } if (visitorContext.getModelUtils().resolveKind(typeElement, ElementKind.ENUM).isPresent()) { return new JavaEnumElement( - typeElement, + new JavaNativeElement.Class(typeElement, type, owner), elementAnnotationMetadataFactory, visitorContext - ).withAnnotationMetadata(createAnnotationMetadata(typeElement, dt, includeTypeAnnotations)); + ); } return new JavaClassElement( - typeElement, + new JavaNativeElement.Class(typeElement, type, owner), elementAnnotationMetadataFactory, visitorContext, typeMirrorArguments, resolvedTypeArguments, 0, isTypeVariable - ).withAnnotationMetadata(createAnnotationMetadata(typeElement, dt, includeTypeAnnotations)); + ); } return PrimitiveElement.VOID; } if (type instanceof TypeVariable tv) { - return resolveTypeVariable(declaredTypeArguments, visitedTypes, includeTypeAnnotations, tv, isRawTypeParameter); + return resolveTypeVariable(owner, declaredTypeArguments, visitedTypes, tv, isRawTypeParameter); } if (type instanceof ArrayType at) { TypeMirror componentType = at.getComponentType(); - return newClassElement(componentType, declaredTypeArguments, visitedTypes, includeTypeAnnotations, isTypeVariable) + return newClassElement(owner, componentType, declaredTypeArguments, visitedTypes, isTypeVariable) .toArray(); } if (type instanceof PrimitiveType pt) { return PrimitiveElement.valueOf(pt.getKind().name()); } if (type instanceof WildcardType wt) { - return resolveWildcard(visitorContext, declaredTypeArguments, visitedTypes, includeTypeAnnotations, representedTypeParameter, wt); + return resolveWildcard(owner, declaredTypeArguments, visitedTypes, representedTypeParameter, wt); } return PrimitiveElement.VOID; } - private ClassElement resolveWildcard(JavaVisitorContext visitorContext, + private ClassElement resolveWildcard(JavaNativeElement owner, Map declaredTypeArguments, Set visitedTypes, - boolean includeTypeAnnotations, TypeParameterElement representedTypeParameter, WildcardType wt) { TypeMirror superBound = wt.getSuperBound(); @@ -370,16 +405,16 @@ private ClassElement resolveWildcard(JavaVisitorContext visitorContext, upperBounds = Stream.of(extendsBound); } List upperBoundsAsElements = upperBounds - .map(tm -> newClassElement(tm, declaredTypeArguments, visitedTypes, includeTypeAnnotations, true)) + .map(tm -> newClassElement(owner, tm, declaredTypeArguments, visitedTypes, true)) .toList(); List lowerBoundsAsElements = lowerBounds - .map(tm -> newClassElement(tm, declaredTypeArguments, visitedTypes, includeTypeAnnotations, true)) + .map(tm -> newClassElement(owner, tm, declaredTypeArguments, visitedTypes, true)) .toList(); ClassElement upperType = WildcardElement.findUpperType(upperBoundsAsElements, lowerBoundsAsElements); if (upperType.getType().getName().equals("java.lang.Object")) { // Not bounded wildcard: if (representedTypeParameter != null) { - ClassElement definedTypeBound = newClassElement(representedTypeParameter.asType(), declaredTypeArguments, visitedTypes, includeTypeAnnotations, true); + ClassElement definedTypeBound = newClassElement(owner, representedTypeParameter.asType(), declaredTypeArguments, visitedTypes, true); // Use originating parameter to extract the bound defined if (definedTypeBound instanceof JavaGenericPlaceholderElement javaGenericPlaceholderElement) { upperType = WildcardElement.findUpperType(javaGenericPlaceholderElement.getBounds(), Collections.emptyList()); @@ -401,10 +436,19 @@ private ClassElement resolveWildcard(JavaVisitorContext visitorContext, protected final Map resolveTypeArguments(TypeElement typeElement, @Nullable - List typeMirrorArguments, - Map declaredElementTypeArguments, - Set visitedTypes) { - List typeParameters = typeElement.getTypeParameters(); + List typeMirrorArguments) { + return resolveTypeArguments(typeElement.getTypeParameters(), typeMirrorArguments, Collections.emptyMap(), new HashSet<>()); + } + + protected final Map resolveTypeArguments(ExecutableElement executableElement, Map parentTypeArguments) { + return resolveTypeArguments(executableElement.getTypeParameters(), null, parentTypeArguments, new HashSet<>()); + } + + private Map resolveTypeArguments(List typeParameters, + @Nullable + List typeMirrorArguments, + Map parentTypeArguments, + Set visitedTypes) { if (typeParameters.isEmpty()) { return Collections.emptyMap(); } @@ -416,7 +460,7 @@ protected final Map resolveTypeArguments(TypeElement typeE String variableName = typeParameter.getSimpleName().toString(); resolved.put( variableName, - newClassElement(typeParameterMirror, declaredElementTypeArguments, visitedTypes, true, true, false, typeParameter) + newClassElement(getNativeType(), typeParameterMirror, parentTypeArguments, visitedTypes, true, false, typeParameter) ); } } else { @@ -427,56 +471,60 @@ protected final Map resolveTypeArguments(TypeElement typeE String variableName = typeParameter.getSimpleName().toString(); resolved.put( variableName, - newClassElement(typeParameter.asType(), declaredElementTypeArguments, visitedTypes, true, true, isRaw, null) + newClassElement(getNativeType(), typeParameter.asType(), parentTypeArguments, visitedTypes, true, isRaw, null) ); } } return resolved; } - private ClassElement resolveTypeVariable(Map genericsInfo, + private ClassElement resolveTypeVariable(JavaNativeElement owner, + Map parentTypeArguments, Set visitedTypes, - boolean includeTypeAnnotations, TypeVariable tv, boolean isRawType) { String variableName = tv.toString(); - ClassElement b = genericsInfo.get(variableName); - if (b != null) { - if (b instanceof WildcardElement wildcardElement) { + ClassElement resolvedBound = parentTypeArguments.get(variableName); + List bounds = null; + io.micronaut.inject.ast.Element declaredElement = this; + JavaClassElement resolved = null; + int arrayDimensions = 0; + if (resolvedBound != null) { + if (resolvedBound instanceof WildcardElement wildcardElement) { if (wildcardElement.isBounded()) { return wildcardElement; } + } else if (resolvedBound instanceof JavaGenericPlaceholderElement javaGenericPlaceholderElement) { + bounds = javaGenericPlaceholderElement.getBounds(); + declaredElement = javaGenericPlaceholderElement.getRequiredDeclaringElement(); + resolved = javaGenericPlaceholderElement.getResolvedInternal(); + isRawType = javaGenericPlaceholderElement.isRawType(); + arrayDimensions = javaGenericPlaceholderElement.getArrayDimensions(); + } else if (resolvedBound instanceof JavaClassElement resolvedClassElement) { + resolved = resolvedClassElement; + isRawType = resolvedClassElement.isRawType(); + arrayDimensions = resolvedClassElement.getArrayDimensions(); } else { - return b; + // Most likely primitive array + return resolvedBound; } } - List bounds = new ArrayList<>(); - TypeMirror upperBound = tv.getUpperBound(); - // type variable is still free. - List boundsUnresolved = upperBound instanceof IntersectionType ? - ((IntersectionType) upperBound).getBounds() : - Collections.singletonList(upperBound); - boundsUnresolved.stream() - .map(tm -> (JavaClassElement) newClassElement(tm, genericsInfo, visitedTypes, includeTypeAnnotations, true)) - .forEach(bounds::add); - return new JavaGenericPlaceholderElement(tv, bounds, elementAnnotationMetadataFactory, 0, isRawType); - } - - private AnnotationMetadata createAnnotationMetadata(TypeElement typeElement, DeclaredType dt, boolean includeTypeAnnotations) { - AnnotationUtils annotationUtils = visitorContext - .getAnnotationUtils(); - AnnotationMetadata newAnnotationMetadata; - List annotationMirrors = dt.getAnnotationMirrors(); - if (!annotationMirrors.isEmpty()) { - newAnnotationMetadata = annotationUtils.newAnnotationBuilder().buildDeclared(typeElement, annotationMirrors, includeTypeAnnotations); - } else { - newAnnotationMetadata = includeTypeAnnotations ? annotationUtils.newAnnotationBuilder().lookupOrBuildForType(typeElement).copyAnnotationMetadata() : AnnotationMetadata.EMPTY_METADATA; + if (bounds == null) { + bounds = new ArrayList<>(); + TypeMirror upperBound = tv.getUpperBound(); + // type variable is still free. + List boundsUnresolved = upperBound instanceof IntersectionType ? + ((IntersectionType) upperBound).getBounds() : + Collections.singletonList(upperBound); + boundsUnresolved.stream() + .map(tm -> (JavaClassElement) newClassElement(owner, tm, parentTypeArguments, visitedTypes, true)) + .forEach(bounds::add); } - return newAnnotationMetadata; + return new JavaGenericPlaceholderElement(new JavaNativeElement.Placeholder(tv.asElement(), tv, getNativeType()), tv, declaredElement, resolved, bounds, elementAnnotationMetadataFactory, arrayDimensions, isRawType); } private boolean hasModifier(Modifier modifier) { - return element.getModifiers().contains(modifier); + return nativeElement.element().getModifiers().contains(modifier); } @Override @@ -484,7 +532,6 @@ public boolean equals(Object o) { if (this == o) { return true; } - // Do not check if classes match, sometimes it's an anonymous one if (o == null) { return false; } @@ -492,11 +539,16 @@ public boolean equals(Object o) { if (that instanceof TypedElement && ((TypedElement) that).isPrimitive()) { return false; } - return element.equals(that.getNativeType()); + // Do not check if classes match, sometimes it's an anonymous one + if (!(that instanceof AbstractJavaElement abstractJavaElement)) { + return false; + } + // We allow to match different sub classes like JavaClassElement, JavaPlaceholder, JavaWildcard etc + return nativeElement.element().equals(abstractJavaElement.getNativeType().element()); } @Override public int hashCode() { - return element.hashCode(); + return nativeElement.element().hashCode(); } } diff --git a/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaAnnotationElement.java b/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaAnnotationElement.java index 0b78fbd8979..4e22deb659d 100644 --- a/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaAnnotationElement.java +++ b/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaAnnotationElement.java @@ -15,24 +15,27 @@ */ package io.micronaut.annotation.processing.visitor; +import io.micronaut.core.annotation.Internal; import io.micronaut.inject.ast.AnnotationElement; import io.micronaut.inject.ast.annotation.ElementAnnotationMetadataFactory; -import javax.lang.model.element.TypeElement; - /** * Represents an annotation in the AST for Java. * * @author graemerocher * @since 3.1.0 */ +@Internal final class JavaAnnotationElement extends JavaClassElement implements AnnotationElement { + /** - * @param classElement The {@link javax.lang.model.element.TypeElement} + * @param nativeElement The native element * @param annotationMetadataFactory The annotation metadata factory * @param visitorContext The visitor context */ - JavaAnnotationElement(TypeElement classElement, ElementAnnotationMetadataFactory annotationMetadataFactory, JavaVisitorContext visitorContext) { - super(classElement, annotationMetadataFactory, visitorContext); + JavaAnnotationElement(JavaNativeElement.Class nativeElement, + ElementAnnotationMetadataFactory annotationMetadataFactory, + JavaVisitorContext visitorContext) { + super(nativeElement, annotationMetadataFactory, visitorContext); } } diff --git a/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaBeanDefinitionBuilder.java b/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaBeanDefinitionBuilder.java index ca3864777db..656aeb5ca95 100644 --- a/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaBeanDefinitionBuilder.java +++ b/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaBeanDefinitionBuilder.java @@ -23,6 +23,7 @@ import io.micronaut.core.annotation.AnnotationMetadata; import io.micronaut.core.annotation.AnnotationValue; import io.micronaut.core.annotation.AnnotationValueBuilder; +import io.micronaut.core.annotation.Internal; import io.micronaut.core.util.ArgumentUtils; import io.micronaut.inject.annotation.AnnotationMetadataHierarchy; import io.micronaut.inject.ast.ClassElement; @@ -49,6 +50,7 @@ * @author graemerocher * @since 3.0.0 */ +@Internal class JavaBeanDefinitionBuilder extends AbstractBeanDefinitionBuilder { private final JavaVisitorContext javaVisitorContext; diff --git a/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaClassElement.java b/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaClassElement.java index da8b74c172b..a665a0a10aa 100644 --- a/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaClassElement.java +++ b/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaClassElement.java @@ -24,6 +24,7 @@ import io.micronaut.core.annotation.Nullable; import io.micronaut.core.naming.NameUtils; import io.micronaut.core.reflect.ClassUtils; +import io.micronaut.inject.annotation.AnnotationMetadataHierarchy; import io.micronaut.inject.ast.ArrayableClassElement; import io.micronaut.inject.ast.ClassElement; import io.micronaut.inject.ast.ConstructorElement; @@ -36,7 +37,9 @@ import io.micronaut.inject.ast.ParameterElement; import io.micronaut.inject.ast.PropertyElement; import io.micronaut.inject.ast.PropertyElementQuery; +import io.micronaut.inject.ast.annotation.ElementAnnotationMetadata; import io.micronaut.inject.ast.annotation.ElementAnnotationMetadataFactory; +import io.micronaut.inject.ast.annotation.MutableAnnotationMetadataDelegate; import io.micronaut.inject.ast.utils.AstBeanPropertiesUtils; import io.micronaut.inject.ast.utils.EnclosedElementsQuery; import io.micronaut.inject.processing.JavaModelUtils; @@ -96,92 +99,79 @@ public class JavaClassElement extends AbstractJavaElement implements ArrayableCl private ClassElement resolvedSuperType; private final JavaEnclosedElementsQuery enclosedElementsQuery = new JavaEnclosedElementsQuery(); @Nullable + private ElementAnnotationMetadata elementTypeAnnotationMetadata; + @Nullable + private ClassElement theType; + @Nullable + private AnnotationMetadata annotationMetadata; + @Nullable // Not null means raw type definition: "List myMethod()" // Null value means a class definition: "class List {}" final List typeArguments; /** - * @param classElement The {@link TypeElement} + * @param nativeType The native type * @param annotationMetadataFactory The annotation metadata factory * @param visitorContext The visitor context */ @Internal - public JavaClassElement(TypeElement classElement, ElementAnnotationMetadataFactory annotationMetadataFactory, JavaVisitorContext visitorContext) { - this(classElement, annotationMetadataFactory, visitorContext, null, null, 0, false); + public JavaClassElement(JavaNativeElement.Class nativeType, ElementAnnotationMetadataFactory annotationMetadataFactory, JavaVisitorContext visitorContext) { + this(nativeType, annotationMetadataFactory, visitorContext, null, null, 0, false); } /** - * @param classElement The {@link TypeElement} + * @param nativeType The native type * @param annotationMetadataFactory The annotation metadata factory * @param visitorContext The visitor context * @param typeArguments The declared type arguments * @param resolvedTypeArguments The resolvedTypeArguments */ - JavaClassElement(TypeElement classElement, + JavaClassElement(JavaNativeElement.Class nativeType, ElementAnnotationMetadataFactory annotationMetadataFactory, JavaVisitorContext visitorContext, List typeArguments, @Nullable Map resolvedTypeArguments) { - this(classElement, annotationMetadataFactory, visitorContext, typeArguments, resolvedTypeArguments, 0, false); + this(nativeType, annotationMetadataFactory, visitorContext, typeArguments, resolvedTypeArguments, 0, false); } /** - * @param classElement The {@link TypeElement} + * @param nativeType The native type * @param annotationMetadataFactory The annotation metadata factory * @param visitorContext The visitor context * @param typeArguments The declared type arguments * @param resolvedTypeArguments The resolvedTypeArguments * @param arrayDimensions The number of array dimensions */ - JavaClassElement(TypeElement classElement, + JavaClassElement(JavaNativeElement.Class nativeType, ElementAnnotationMetadataFactory annotationMetadataFactory, JavaVisitorContext visitorContext, List typeArguments, @Nullable Map resolvedTypeArguments, int arrayDimensions) { - this(classElement, annotationMetadataFactory, visitorContext, typeArguments, resolvedTypeArguments, arrayDimensions, false); + this(nativeType, annotationMetadataFactory, visitorContext, typeArguments, resolvedTypeArguments, arrayDimensions, false); } /** - * @param classElement The {@link TypeElement} + * @param nativeType The {@link TypeElement} * @param annotationMetadataFactory The annotation metadata factory * @param visitorContext The visitor context * @param typeArguments The declared type arguments * @param resolvedTypeArguments The resolvedTypeArguments - * @param isTypeVariable Is the class element a type variable + * @param arrayDimensions The number of array dimensions + * @param isTypeVariable Is the type a type variable */ - JavaClassElement(TypeElement classElement, + JavaClassElement(JavaNativeElement.Class nativeType, ElementAnnotationMetadataFactory annotationMetadataFactory, JavaVisitorContext visitorContext, List typeArguments, @Nullable Map resolvedTypeArguments, + int arrayDimensions, boolean isTypeVariable) { - this(classElement, annotationMetadataFactory, visitorContext, typeArguments, resolvedTypeArguments, 0, isTypeVariable); - } - - /** - * @param classElement The {@link TypeElement} - * @param annotationMetadataFactory The annotation metadata factory - * @param visitorContext The visitor context - * @param typeArguments The declared type arguments - * @param resolvedTypeArguments The resolvedTypeArguments - * @param arrayDimensions The number of array dimensions - * @param isTypeVariable Is the type a type variable - */ - JavaClassElement( - TypeElement classElement, - ElementAnnotationMetadataFactory annotationMetadataFactory, - JavaVisitorContext visitorContext, - List typeArguments, - @Nullable - Map resolvedTypeArguments, - int arrayDimensions, - boolean isTypeVariable) { - super(classElement, annotationMetadataFactory, visitorContext); - this.classElement = classElement; + super(nativeType, annotationMetadataFactory, visitorContext); + this.classElement = nativeType.element(); this.typeArguments = typeArguments; this.resolvedTypeArguments = resolvedTypeArguments; this.arrayDimensions = arrayDimensions; @@ -189,21 +179,18 @@ public JavaClassElement(TypeElement classElement, ElementAnnotationMetadataFacto } @Override - protected JavaClassElement copyThis() { - return new JavaClassElement(classElement, elementAnnotationMetadataFactory, visitorContext, typeArguments, resolvedTypeArguments); + public JavaNativeElement.Class getNativeType() { + return (JavaNativeElement.Class) super.getNativeType(); } @Override - protected void copyValues(AbstractJavaElement element) { - super.copyValues(element); - ((JavaClassElement) element).resolvedTypeArguments = resolvedTypeArguments; + protected JavaClassElement copyThis() { + return new JavaClassElement(getNativeType(), elementAnnotationMetadataFactory, visitorContext, typeArguments, resolvedTypeArguments, arrayDimensions); } @Override public ClassElement withTypeArguments(Map newTypeArguments) { - JavaClassElement javaClassElement = (JavaClassElement) makeCopy(); - javaClassElement.resolvedTypeArguments = newTypeArguments; - return javaClassElement; + return new JavaClassElement(getNativeType(), elementAnnotationMetadataFactory, visitorContext, typeArguments, newTypeArguments, arrayDimensions); } @Override @@ -211,6 +198,26 @@ public ClassElement withAnnotationMetadata(AnnotationMetadata annotationMetadata return (ClassElement) super.withAnnotationMetadata(annotationMetadata); } + @Override + protected MutableAnnotationMetadataDelegate getAnnotationMetadataToWrite() { + if (getNativeType().typeMirror() == null) { + return super.getAnnotationMetadataToWrite(); + } + return getTypeAnnotationMetadata(); + } + + @Override + public AnnotationMetadata getAnnotationMetadata() { + if (annotationMetadata == null) { + if (getNativeType().typeMirror() == null) { + annotationMetadata = super.getAnnotationMetadata(); + } else { + annotationMetadata = new AnnotationMetadataHierarchy(true, super.getAnnotationMetadata(), getTypeAnnotationMetadata()); + } + } + return annotationMetadata; + } + @Override public boolean isTypeVariable() { return isTypeVariable; @@ -231,10 +238,6 @@ public boolean isRecord() { return JavaModelUtils.isRecord(classElement); } - public final TypeElement getNativeTypeElement() { - return classElement; - } - @Override public boolean isPrimitive() { return ClassUtils.getPrimitiveType(getName()).isPresent(); @@ -285,23 +288,23 @@ public List getBeanProperties() { public List getBeanProperties(PropertyElementQuery propertyElementQuery) { if (isRecord()) { return AstBeanPropertiesUtils.resolveBeanProperties(propertyElementQuery, - this, - this::getRecordMethods, - this::getRecordFields, - true, - Collections.emptySet(), - methodElement -> Optional.empty(), - methodElement -> Optional.empty(), - this::mapToPropertyElement); + this, + this::getRecordMethods, + this::getRecordFields, + true, + Collections.emptySet(), + methodElement -> Optional.empty(), + methodElement -> Optional.empty(), + this::mapToPropertyElement); } Function> customReaderPropertyNameResolver = methodElement -> Optional.empty(); Function> customWriterPropertyNameResolver = methodElement -> Optional.empty(); - if (isKotlinClass(getNativeTypeElement())) { + if (isKotlinClass(getNativeType().element())) { Set isProperties = getEnclosedElements(ElementQuery.ALL_METHODS) - .stream() - .map(io.micronaut.inject.ast.Element::getName) - .filter(method -> method.startsWith(PREFIX_IS)) - .collect(Collectors.toSet()); + .stream() + .map(io.micronaut.inject.ast.Element::getName) + .filter(method -> method.startsWith(PREFIX_IS)) + .collect(Collectors.toSet()); if (!isProperties.isEmpty()) { customReaderPropertyNameResolver = methodElement -> { String methodName = methodElement.getSimpleName(); @@ -322,34 +325,34 @@ public List getBeanProperties(PropertyElementQuery propertyElem } } return AstBeanPropertiesUtils.resolveBeanProperties(propertyElementQuery, - this, - () -> getEnclosedElements(ElementQuery.ALL_METHODS), - () -> getEnclosedElements(ElementQuery.ALL_FIELDS), - false, - Collections.emptySet(), - customReaderPropertyNameResolver, - customWriterPropertyNameResolver, - this::mapToPropertyElement); + this, + () -> getEnclosedElements(ElementQuery.ALL_METHODS), + () -> getEnclosedElements(ElementQuery.ALL_FIELDS), + false, + Collections.emptySet(), + customReaderPropertyNameResolver, + customWriterPropertyNameResolver, + this::mapToPropertyElement); } private JavaPropertyElement mapToPropertyElement(AstBeanPropertiesUtils.BeanPropertyData value) { return new JavaPropertyElement( - JavaClassElement.this, - value.type, - value.readAccessKind == null ? null : value.getter, - value.writeAccessKind == null ? null : value.setter, - value.field, - elementAnnotationMetadataFactory, - value.propertyName, - value.readAccessKind == null ? PropertyElement.AccessKind.METHOD : PropertyElement.AccessKind.valueOf(value.readAccessKind.name()), - value.writeAccessKind == null ? PropertyElement.AccessKind.METHOD : PropertyElement.AccessKind.valueOf(value.writeAccessKind.name()), - value.isExcluded, - visitorContext); + JavaClassElement.this, + value.type, + value.readAccessKind == null ? null : value.getter, + value.writeAccessKind == null ? null : value.setter, + value.field, + elementAnnotationMetadataFactory, + value.propertyName, + value.readAccessKind == null ? PropertyElement.AccessKind.METHOD : PropertyElement.AccessKind.valueOf(value.readAccessKind.name()), + value.writeAccessKind == null ? PropertyElement.AccessKind.METHOD : PropertyElement.AccessKind.valueOf(value.writeAccessKind.name()), + value.isExcluded, + visitorContext); } private List getRecordMethods() { List methodElements = new ArrayList<>(); - classElement.asType().accept(new SuperclassAwareTypeVisitor(visitorContext) { + classElement.asType().accept(new SuperclassAwareTypeVisitor<>(visitorContext) { private final Set recordComponents = new HashSet<>(); @@ -376,10 +379,14 @@ public Object visitDeclared(DeclaredType type, Object o) { @Override protected void accept(DeclaredType type, Element element, Object o) { String name = element.getSimpleName().toString(); - if (element instanceof ExecutableElement) { + if (element instanceof ExecutableElement executableElement) { if (recordComponents.contains(name)) { methodElements.add( - new JavaMethodElement(JavaClassElement.this, (ExecutableElement) element, elementAnnotationMetadataFactory, visitorContext) + new JavaMethodElement( + JavaClassElement.this, + new JavaNativeElement.Method(executableElement), + elementAnnotationMetadataFactory, + visitorContext) ); } } else if (element instanceof VariableElement) { @@ -393,7 +400,7 @@ protected void accept(DeclaredType type, Element element, Object o) { private List getRecordFields() { List fieldElements = new ArrayList<>(); - classElement.asType().accept(new SuperclassAwareTypeVisitor(visitorContext) { + classElement.asType().accept(new SuperclassAwareTypeVisitor<>(visitorContext) { @Override protected boolean isAcceptable(Element element) { @@ -407,8 +414,8 @@ public Object visitDeclared(DeclaredType type, Object o) { List enclosedElements = element.getEnclosedElements(); for (Element enclosedElement : enclosedElements) { if ((JavaModelUtils.isRecordComponent(enclosedElement) - || enclosedElement instanceof ExecutableElement) - && enclosedElement.getKind() != ElementKind.CONSTRUCTOR) { + || enclosedElement instanceof ExecutableElement) + && enclosedElement.getKind() != ElementKind.CONSTRUCTOR) { accept(type, enclosedElement, o); } } @@ -418,9 +425,13 @@ public Object visitDeclared(DeclaredType type, Object o) { @Override protected void accept(DeclaredType type, Element element, Object o) { - if (element instanceof VariableElement) { + if (element instanceof VariableElement variableElement) { fieldElements.add( - new JavaFieldElement(JavaClassElement.this, (VariableElement) element, elementAnnotationMetadataFactory, visitorContext) + new JavaFieldElement( + JavaClassElement.this, + new JavaNativeElement.Variable(variableElement), + elementAnnotationMetadataFactory, + visitorContext) ); } } @@ -435,17 +446,7 @@ private boolean isKotlinClass(Element element) { @Override public List getEnclosedElements(@NonNull ElementQuery query) { - ClassElement classElementToInspect; - if (this instanceof GenericPlaceholderElement genericPlaceholderElement) { - List bounds = genericPlaceholderElement.getBounds(); - if (bounds.isEmpty()) { - return Collections.emptyList(); - } - classElementToInspect = bounds.get(0); - } else { - classElementToInspect = this; - } - return enclosedElementsQuery.getEnclosedElements(classElementToInspect, query); + return enclosedElementsQuery.getEnclosedElements(this, query); } @Override @@ -463,7 +464,7 @@ public ClassElement withArrayDimensions(int arrayDimensions) { if (arrayDimensions == this.arrayDimensions) { return this; } - return new JavaClassElement(classElement, elementAnnotationMetadataFactory, visitorContext, typeArguments, resolvedTypeArguments, arrayDimensions, false); + return new JavaClassElement(getNativeType(), elementAnnotationMetadataFactory, visitorContext, typeArguments, resolvedTypeArguments, arrayDimensions, false); } @Override @@ -498,9 +499,9 @@ public PackageElement getPackage() { } if (enclosingElement instanceof javax.lang.model.element.PackageElement packageElement) { return new JavaPackageElement( - packageElement, - elementAnnotationMetadataFactory, - visitorContext + packageElement, + elementAnnotationMetadataFactory, + visitorContext ); } else { return PackageElement.DEFAULT_PACKAGE; @@ -521,9 +522,8 @@ public boolean isAssignable(ClassElement type) { if (type.isPrimitive()) { return isAssignable(type.getName()); } - Object nativeType = type.getNativeType(); - if (nativeType instanceof TypeElement) { - return isAssignable((TypeElement) nativeType); + if (type instanceof JavaClassElement javaClassElement) { + return isAssignable(javaClassElement.getNativeType().element()); } return isAssignable(type.getName()); } @@ -567,8 +567,8 @@ public Optional getPrimaryConstructor() { } List constructors = getAccessibleConstructors(); Optional annotatedConstructor = constructors.stream() - .filter(c -> c.hasStereotype(AnnotationUtil.INJECT) || c.hasStereotype(Creator.class)) - .findFirst(); + .filter(c -> c.hasStereotype(AnnotationUtil.INJECT) || c.hasStereotype(Creator.class)) + .findFirst(); if (annotatedConstructor.isPresent()) { return annotatedConstructor.map(c -> c); } @@ -581,7 +581,7 @@ public Optional getPrimaryConstructor() { for (int i = 0; i < parameters.length; i++) { ParameterElement parameter = parameters[i]; RecordComponentElement rce = recordComponents.get(i); - VariableElement ve = (VariableElement) parameter.getNativeType(); + VariableElement ve = ((JavaNativeElement.Variable) parameter.getNativeType()).element(); TypeMirror leftType = visitorContext.getTypes().erasure(ve.asType()); TypeMirror rightType = visitorContext.getTypes().erasure(rce.asType()); if (!leftType.equals(rightType)) { @@ -604,12 +604,12 @@ public List getAccessibleStaticCreators() { return staticCreators; } return visitorContext.getClassElement(getName() + "$Companion", elementAnnotationMetadataFactory) - .filter(io.micronaut.inject.ast.Element::isStatic) - .flatMap(typeElement -> typeElement.getEnclosedElements(ElementQuery.ALL_METHODS - .annotated(annotationMetadata -> annotationMetadata.hasStereotype(Creator.class))).stream().findFirst() - ) - .filter(method -> !method.isPrivate() && method.getReturnType().equals(this)) - .stream().toList(); + .filter(io.micronaut.inject.ast.Element::isStatic) + .flatMap(typeElement -> typeElement.getEnclosedElements(ElementQuery.ALL_METHODS + .annotated(am -> am.hasStereotype(Creator.class))).stream().findFirst() + ) + .filter(method -> !method.isPrivate() && method.getReturnType().equals(this)) + .stream().toList(); } @Override @@ -618,8 +618,8 @@ public Optional getEnclosingType() { Element enclosingElement = this.classElement.getEnclosingElement(); if (enclosingElement instanceof TypeElement typeElement) { return Optional.of(visitorContext.getElementFactory().newClassElement( - typeElement, - elementAnnotationMetadataFactory + typeElement, + elementAnnotationMetadataFactory )); } } @@ -633,24 +633,24 @@ public List getBoundGenericTypes() { return Collections.emptyList(); } return typeArguments.stream() - .map(tm -> newClassElement(tm, getTypeArguments())) - .toList(); + .map(tm -> newClassElement(tm, getTypeArguments())) + .toList(); } @NonNull @Override public List getDeclaredGenericPlaceholders() { return classElement.getTypeParameters().stream() - // we want the *declared* variables, so we don't pass in our genericsInfo. - .map(tpe -> (GenericPlaceholderElement) newClassElement(tpe.asType(), Collections.emptyMap())) - .toList(); + // we want the *declared* variables, so we don't pass in our genericsInfo. + .map(tpe -> (GenericPlaceholderElement) newClassElement(tpe.asType(), Collections.emptyMap())) + .toList(); } @NonNull @Override public ClassElement getRawClassElement() { return visitorContext.getElementFactory().newClassElement(classElement, elementAnnotationMetadataFactory) - .withArrayDimensions(getArrayDimensions()); + .withArrayDimensions(getArrayDimensions()); } @NonNull @@ -677,7 +677,7 @@ public ClassElement withTypeArguments(@NonNull Collection typeArgu @NonNull public Map getTypeArguments() { if (resolvedTypeArguments == null) { - resolvedTypeArguments = resolveTypeArguments(classElement, typeArguments, Collections.emptyMap(), new HashSet<>()); + resolvedTypeArguments = resolveTypeArguments(classElement, typeArguments); } return resolvedTypeArguments; } @@ -691,18 +691,50 @@ public Map> getAllTypeArguments() { return resolvedAllTypeArguments; } + @Override + public ClassElement getType() { + if (theType == null) { + if (getNativeType().typeMirror() == null) { + theType = this; + } else { + // Strip the type mirror + // This should eliminate type annotations + theType = new JavaClassElement(new JavaNativeElement.Class(getNativeType().element()), elementAnnotationMetadataFactory, visitorContext, typeArguments, resolvedTypeArguments, arrayDimensions); + } + } + return theType; + } + + @Override + public MutableAnnotationMetadataDelegate getTypeAnnotationMetadata() { + if (elementTypeAnnotationMetadata == null) { + elementTypeAnnotationMetadata = elementAnnotationMetadataFactory.buildTypeAnnotations(this); + } + return elementTypeAnnotationMetadata; + } + private final class JavaEnclosedElementsQuery extends EnclosedElementsQuery { private List enclosedElements; + @Override + protected TypeElement getNativeClassType(ClassElement classElement) { + return ((JavaClassElement) classElement).getNativeType().element(); + } + + @Override + protected Element getNativeType(io.micronaut.inject.ast.Element element) { + return ((AbstractJavaElement) element).getNativeType().element(); + } + @Override protected Set getExcludedNativeElements(ElementQuery.Result result) { if (result.isExcludePropertyElements()) { Set excludeElements = new HashSet<>(); for (PropertyElement excludePropertyElement : getBeanProperties()) { - excludePropertyElement.getReadMethod().ifPresent(methodElement -> excludeElements.add((Element) methodElement.getNativeType())); - excludePropertyElement.getWriteMethod().ifPresent(methodElement -> excludeElements.add((Element) methodElement.getNativeType())); - excludePropertyElement.getField().ifPresent(fieldElement -> excludeElements.add((Element) fieldElement.getNativeType())); + excludePropertyElement.getReadMethod().ifPresent(methodElement -> excludeElements.add(((JavaNativeElement) methodElement.getNativeType()).element())); + excludePropertyElement.getWriteMethod().ifPresent(methodElement -> excludeElements.add(((JavaNativeElement) methodElement.getNativeType()).element())); + excludePropertyElement.getField().ifPresent(methodElement -> excludeElements.add(((JavaNativeElement) methodElement.getNativeType()).element())); } return excludeElements; } @@ -749,7 +781,7 @@ protected List getEnclosedElements(TypeElement classNode, ElementQuery. @Override protected boolean excludeClass(TypeElement classNode) { return classNode.getQualifiedName().toString().equals(Object.class.getName()) - || classNode.getQualifiedName().toString().equals(Enum.class.getName()); + || classNode.getQualifiedName().toString().equals(Enum.class.getName()); } @Override @@ -757,28 +789,28 @@ protected io.micronaut.inject.ast.Element toAstElement(Element enclosedElement, final JavaElementFactory elementFactory = visitorContext.getElementFactory(); return switch (enclosedElement.getKind()) { case METHOD -> elementFactory.newMethodElement( - JavaClassElement.this, - (ExecutableElement) enclosedElement, - elementAnnotationMetadataFactory + JavaClassElement.this, + (ExecutableElement) enclosedElement, + elementAnnotationMetadataFactory ); case FIELD -> elementFactory.newFieldElement( - JavaClassElement.this, - (VariableElement) enclosedElement, - elementAnnotationMetadataFactory + JavaClassElement.this, + (VariableElement) enclosedElement, + elementAnnotationMetadataFactory ); case ENUM_CONSTANT -> elementFactory.newEnumConstantElement( - JavaClassElement.this, - (VariableElement) enclosedElement, - elementAnnotationMetadataFactory + JavaClassElement.this, + (VariableElement) enclosedElement, + elementAnnotationMetadataFactory ); case CONSTRUCTOR -> elementFactory.newConstructorElement( - JavaClassElement.this, - (ExecutableElement) enclosedElement, - elementAnnotationMetadataFactory + JavaClassElement.this, + (ExecutableElement) enclosedElement, + elementAnnotationMetadataFactory ); case CLASS, ENUM -> elementFactory.newClassElement( - (TypeElement) enclosedElement, - elementAnnotationMetadataFactory + (TypeElement) enclosedElement, + elementAnnotationMetadataFactory ); default -> throw new IllegalStateException("Unknown element: " + enclosedElement); }; diff --git a/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaConstructorElement.java b/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaConstructorElement.java index aca4f2ba497..b6e5fda61e6 100644 --- a/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaConstructorElement.java +++ b/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaConstructorElement.java @@ -17,12 +17,9 @@ import io.micronaut.core.annotation.Internal; import io.micronaut.inject.ast.ConstructorElement; -import io.micronaut.inject.ast.annotation.ElementAnnotationMetadataFactory; import io.micronaut.inject.ast.MemberElement; import io.micronaut.inject.ast.MethodElement; -import io.micronaut.inject.ast.ParameterElement; - -import javax.lang.model.element.ExecutableElement; +import io.micronaut.inject.ast.annotation.ElementAnnotationMetadataFactory; /** * A {@link ConstructorElement} for Java. @@ -35,25 +32,20 @@ class JavaConstructorElement extends JavaMethodElement implements ConstructorEle /** * @param declaringClass The declaring class - * @param executableElement The {@link ExecutableElement} + * @param nativeElement The native element * @param annotationMetadataFactory The annotation metadata factory * @param visitorContext The visitor context */ JavaConstructorElement(JavaClassElement declaringClass, - ExecutableElement executableElement, + JavaNativeElement.Method nativeElement, ElementAnnotationMetadataFactory annotationMetadataFactory, JavaVisitorContext visitorContext) { - super(declaringClass, executableElement, annotationMetadataFactory, visitorContext); + super(declaringClass, nativeElement, annotationMetadataFactory, visitorContext); } @Override - public MethodElement withParameters(ParameterElement... newParameters) { - return new JavaConstructorElement(owningType, executableElement, elementAnnotationMetadataFactory, visitorContext) { - @Override - public ParameterElement[] getParameters() { - return newParameters; - } - }; + protected AbstractJavaElement copyThis() { + return new JavaConstructorElement(getDeclaringType(), getNativeType(), elementAnnotationMetadataFactory, visitorContext); } @Override diff --git a/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaElementFactory.java b/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaElementFactory.java index efc83edba98..407cef51fb1 100644 --- a/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaElementFactory.java +++ b/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaElementFactory.java @@ -16,6 +16,7 @@ package io.micronaut.annotation.processing.visitor; import io.micronaut.annotation.processing.PostponeToNextRoundException; +import io.micronaut.core.annotation.Internal; import io.micronaut.core.annotation.NonNull; import io.micronaut.inject.ast.ClassElement; import io.micronaut.inject.ast.ElementFactory; @@ -41,6 +42,7 @@ * @author graemerocher * @since 2.3.0 */ +@Internal public class JavaElementFactory implements ElementFactory { private final JavaVisitorContext visitorContext; @@ -56,19 +58,19 @@ public JavaClassElement newClassElement(@NonNull TypeElement type, ElementKind kind = type.getKind(); return switch (kind) { case ENUM -> new JavaEnumElement( - type, - annotationMetadataFactory, - visitorContext + new JavaNativeElement.Class(type), + annotationMetadataFactory, + visitorContext ); case ANNOTATION_TYPE -> new JavaAnnotationElement( - type, - annotationMetadataFactory, - visitorContext + new JavaNativeElement.Class(type), + annotationMetadataFactory, + visitorContext ); default -> new JavaClassElement( - type, - annotationMetadataFactory, - visitorContext + new JavaNativeElement.Class(type), + annotationMetadataFactory, + visitorContext ); }; } @@ -90,7 +92,7 @@ public JavaClassElement newSourceClassElement(@NonNull TypeElement type, @NonNul ElementKind kind = type.getKind(); if (kind == ElementKind.ENUM) { return new JavaEnumElement( - type, + new JavaNativeElement.Class(type), annotationMetadataFactory, visitorContext ) { @@ -108,7 +110,7 @@ public BeanElementBuilder addAssociatedBean(@NonNull ClassElement type) { }; } else { return new JavaClassElement( - type, + new JavaNativeElement.Class(type), annotationMetadataFactory, visitorContext ) { @@ -136,7 +138,7 @@ public JavaMethodElement newSourceMethodElement(ClassElement declaringClass, failIfPostponeIsNeeded(declaringClass, method); return new JavaMethodElement( (JavaClassElement) declaringClass, - method, + new JavaNativeElement.Method(method), annotationMetadataFactory, visitorContext ) { @@ -163,7 +165,7 @@ public JavaMethodElement newMethodElement(ClassElement owningType, failIfPostponeIsNeeded(owningType, method); return new JavaMethodElement( (JavaClassElement) owningType, - method, + new JavaNativeElement.Method(method), annotationMetadataFactory, visitorContext ); @@ -178,7 +180,7 @@ public JavaConstructorElement newConstructorElement(ClassElement owningType, failIfPostponeIsNeeded(owningType, constructor); return new JavaConstructorElement( (JavaClassElement) owningType, - constructor, + new JavaNativeElement.Method(constructor), annotationMetadataFactory, visitorContext ); @@ -195,7 +197,7 @@ public JavaEnumConstantElement newEnumConstantElement(ClassElement owningType, failIfPostponeIsNeeded(owningType, enumConstant); return new JavaEnumConstantElement( (JavaEnumElement) owningType, - enumConstant, + new JavaNativeElement.Variable(enumConstant), annotationMetadataFactory, visitorContext ); @@ -209,7 +211,7 @@ public JavaFieldElement newFieldElement(ClassElement owningType, failIfPostponeIsNeeded(owningType, field); return new JavaFieldElement( (JavaClassElement) owningType, - field, + new JavaNativeElement.Variable(field), annotationMetadataFactory, visitorContext ); diff --git a/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaEnumConstantElement.java b/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaEnumConstantElement.java index 89460e0736d..d06e7b18384 100644 --- a/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaEnumConstantElement.java +++ b/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaEnumConstantElement.java @@ -18,12 +18,11 @@ import io.micronaut.core.annotation.AnnotationMetadata; import io.micronaut.core.annotation.Internal; import io.micronaut.inject.ast.ClassElement; -import io.micronaut.inject.ast.annotation.ElementAnnotationMetadataFactory; import io.micronaut.inject.ast.ElementModifier; import io.micronaut.inject.ast.EnumConstantElement; import io.micronaut.inject.ast.FieldElement; +import io.micronaut.inject.ast.annotation.ElementAnnotationMetadataFactory; -import javax.lang.model.element.VariableElement; import java.util.Set; /** @@ -34,28 +33,30 @@ @Internal final class JavaEnumConstantElement extends AbstractJavaElement implements EnumConstantElement { - private final VariableElement variableElement; private final JavaEnumElement declaringEnum; /** * @param declaringEnum The declaring enum element - * @param variableElement The {@link javax.lang.model.element.ExecutableElement} + * @param nativeElement The native element * @param annotationMetadataFactory The annotation metadata factory * @param visitorContext The visitor context */ JavaEnumConstantElement(JavaEnumElement declaringEnum, - VariableElement variableElement, + JavaNativeElement.Variable nativeElement, ElementAnnotationMetadataFactory annotationMetadataFactory, JavaVisitorContext visitorContext) { - super(variableElement, annotationMetadataFactory, visitorContext); - + super(nativeElement, annotationMetadataFactory, visitorContext); this.declaringEnum = declaringEnum; - this.variableElement = variableElement; + } + + @Override + public JavaNativeElement.Variable getNativeType() { + return (JavaNativeElement.Variable) super.getNativeType(); } @Override protected AbstractJavaElement copyThis() { - return new JavaEnumConstantElement(declaringEnum, variableElement, elementAnnotationMetadataFactory, visitorContext); + return new JavaEnumConstantElement(declaringEnum, getNativeType(), elementAnnotationMetadataFactory, visitorContext); } @Override diff --git a/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaEnumElement.java b/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaEnumElement.java index 1cc92b4722f..0986ed4bb49 100644 --- a/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaEnumElement.java +++ b/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaEnumElement.java @@ -42,32 +42,32 @@ class JavaEnumElement extends JavaClassElement implements EnumElement { protected List values; /** - * @param classElement The {@link TypeElement} + * @param nativeElement The native element * @param annotationMetadataFactory The annotation metadata factory * @param visitorContext The visitor context */ - JavaEnumElement(TypeElement classElement, + JavaEnumElement(JavaNativeElement.Class nativeElement, ElementAnnotationMetadataFactory annotationMetadataFactory, JavaVisitorContext visitorContext) { - this(classElement, annotationMetadataFactory, visitorContext, 0); + this(nativeElement, annotationMetadataFactory, visitorContext, 0); } /** - * @param classElement The {@link TypeElement} + * @param nativeElement The native element * @param annotationMetadataFactory The annotation metadata factory * @param visitorContext The visitor context * @param arrayDimensions The number of array dimensions */ - JavaEnumElement(TypeElement classElement, + JavaEnumElement(JavaNativeElement.Class nativeElement, ElementAnnotationMetadataFactory annotationMetadataFactory, JavaVisitorContext visitorContext, int arrayDimensions) { - super(classElement, annotationMetadataFactory, visitorContext, Collections.emptyList(), Collections.emptyMap(), arrayDimensions, false); + super(nativeElement, annotationMetadataFactory, visitorContext, Collections.emptyList(), Collections.emptyMap(), arrayDimensions, false); } @Override protected JavaClassElement copyThis() { - return new JavaEnumElement(classElement, elementAnnotationMetadataFactory, visitorContext, arrayDimensions); + return new JavaEnumElement(getNativeType(), elementAnnotationMetadataFactory, visitorContext, arrayDimensions); } @Override @@ -91,14 +91,14 @@ public List elements() { private void initEnum() { values = new ArrayList<>(); enumConstants = new ArrayList<>(); - TypeElement nativeType = (TypeElement) getNativeType(); + TypeElement nativeType = getNativeType().element(); for (Element element : nativeType.getEnclosedElements()) { if (element.getKind() == ElementKind.ENUM_CONSTANT) { values.add(element.getSimpleName().toString()); enumConstants.add( new JavaEnumConstantElement( this, - (VariableElement) element, + new JavaNativeElement.Variable((VariableElement) element), elementAnnotationMetadataFactory, visitorContext) ); @@ -110,7 +110,7 @@ private void initEnum() { @Override public ClassElement withArrayDimensions(int arrayDimensions) { - return new JavaEnumElement(classElement, elementAnnotationMetadataFactory, visitorContext, arrayDimensions); + return new JavaEnumElement(getNativeType(), elementAnnotationMetadataFactory, visitorContext, arrayDimensions); } } diff --git a/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaFieldElement.java b/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaFieldElement.java index f984dff837e..d319ed0a66c 100644 --- a/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaFieldElement.java +++ b/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaFieldElement.java @@ -41,39 +41,44 @@ class JavaFieldElement extends AbstractJavaElement implements FieldElement { private final VariableElement variableElement; private JavaClassElement owningType; - private ClassElement typeElement; + private ClassElement type; private ClassElement genericType; private ClassElement resolvedDeclaringClass; /** - * @param variableElement The {@link VariableElement} + * @param nativeElement The native element * @param annotationMetadataFactory The annotation metadata factory * @param visitorContext The visitor context */ - JavaFieldElement(VariableElement variableElement, + JavaFieldElement(JavaNativeElement.Variable nativeElement, ElementAnnotationMetadataFactory annotationMetadataFactory, JavaVisitorContext visitorContext) { - super(variableElement, annotationMetadataFactory, visitorContext); - this.variableElement = variableElement; + super(nativeElement, annotationMetadataFactory, visitorContext); + this.variableElement = nativeElement.element(); } /** * @param owningType The declaring element - * @param variableElement The {@link VariableElement} + * @param nativeElement The native element * @param annotationMetadataFactory The annotation metadata factory * @param visitorContext The visitor context */ JavaFieldElement(JavaClassElement owningType, - VariableElement variableElement, + JavaNativeElement.Variable nativeElement, ElementAnnotationMetadataFactory annotationMetadataFactory, JavaVisitorContext visitorContext) { - this(variableElement, annotationMetadataFactory, visitorContext); + this(nativeElement, annotationMetadataFactory, visitorContext); this.owningType = owningType; } + @Override + public JavaNativeElement.Variable getNativeType() { + return (JavaNativeElement.Variable) super.getNativeType(); + } + @Override protected AbstractJavaElement copyThis() { - return new JavaFieldElement(owningType, variableElement, elementAnnotationMetadataFactory, visitorContext); + return new JavaFieldElement(owningType, getNativeType(), elementAnnotationMetadataFactory, visitorContext); } @Override @@ -81,15 +86,19 @@ public FieldElement withAnnotationMetadata(AnnotationMetadata annotationMetadata return (FieldElement) super.withAnnotationMetadata(annotationMetadata); } + @NonNull + @Override + public ClassElement getType() { + if (type == null) { + type = newClassElement(getNativeType(), variableElement.asType(), Collections.emptyMap()); + } + return type; + } + @Override public ClassElement getGenericType() { - if (this.genericType == null) { - if (owningType == null) { - this.genericType = getType(); - } else { - ClassElement declaringType = getDeclaringType(); - this.genericType = newClassElement(variableElement.asType(), declaringType.getTypeArguments()); - } + if (genericType == null) { + genericType = newClassElement(getNativeType(), variableElement.asType(), getDeclaringType().getTypeArguments()); } return this.genericType; } @@ -109,15 +118,6 @@ public int getArrayDimensions() { return getType().getArrayDimensions(); } - @NonNull - @Override - public ClassElement getType() { - if (this.typeElement == null) { - this.typeElement = newClassElement(variableElement.asType(), Collections.emptyMap()); - } - return this.typeElement; - } - @Override public ClassElement getDeclaringType() { if (resolvedDeclaringClass == null) { @@ -143,7 +143,7 @@ public boolean hides(MemberElement hidden) { if (isStatic() && getDeclaringType().isInterface()) { return false; } - return visitorContext.getElements().hides((Element) getNativeType(), (Element) hidden.getNativeType()); + return visitorContext.getElements().hides(getNativeType().element(), ((JavaNativeElement.Variable) hidden.getNativeType()).element()); } @Override diff --git a/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaGenericPlaceholderElement.java b/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaGenericPlaceholderElement.java index d1c40907fe9..17d2c7ff47b 100644 --- a/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaGenericPlaceholderElement.java +++ b/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaGenericPlaceholderElement.java @@ -15,17 +15,23 @@ */ package io.micronaut.annotation.processing.visitor; +import io.micronaut.core.annotation.AnnotationMetadata; import io.micronaut.core.annotation.Internal; import io.micronaut.core.annotation.NonNull; +import io.micronaut.core.annotation.Nullable; +import io.micronaut.inject.annotation.AnnotationMetadataHierarchy; import io.micronaut.inject.ast.ClassElement; import io.micronaut.inject.ast.Element; import io.micronaut.inject.ast.GenericPlaceholderElement; +import io.micronaut.inject.ast.WildcardElement; +import io.micronaut.inject.ast.annotation.AbstractElementAnnotationMetadata; +import io.micronaut.inject.ast.annotation.ElementAnnotationMetadata; import io.micronaut.inject.ast.annotation.ElementAnnotationMetadataFactory; import io.micronaut.inject.ast.annotation.MutableAnnotationMetadataDelegate; import javax.lang.model.element.TypeParameterElement; -import javax.lang.model.type.TypeMirror; import javax.lang.model.type.TypeVariable; +import java.util.ArrayList; import java.util.List; import java.util.Objects; import java.util.Optional; @@ -41,51 +47,128 @@ @Internal final class JavaGenericPlaceholderElement extends JavaClassElement implements GenericPlaceholderElement { final TypeVariable realTypeVariable; + private final JavaNativeElement.Placeholder genericNativeType; + private final Element declaredElement; + @Nullable + private final JavaClassElement resolved; private final List bounds; private final boolean isRawType; + private final ElementAnnotationMetadata typeAnnotationMetadata; + @Nullable + private ElementAnnotationMetadata genericTypeAnnotationMetadata; + + JavaGenericPlaceholderElement(JavaNativeElement.Placeholder genericNativeType, + TypeVariable realTypeVariable, + @NonNull Element declaredElement, + @Nullable JavaClassElement resolved, + @NonNull List bounds, + @NonNull ElementAnnotationMetadataFactory annotationMetadataFactory, + int arrayDimensions, + boolean isRawType) { + this(genericNativeType, + realTypeVariable, + declaredElement, + resolved, + bounds, + selectClassElementRepresentingThisPlaceholder(resolved, bounds), + annotationMetadataFactory, + arrayDimensions, isRawType); + } - JavaGenericPlaceholderElement(@NonNull TypeVariable realTypeVariable, + JavaGenericPlaceholderElement(JavaNativeElement.Placeholder genericNativeType, + TypeVariable realTypeVariable, + @NonNull Element declaredElement, + @Nullable JavaClassElement resolved, @NonNull List bounds, + JavaClassElement classElementRepresentingThisPlaceholder, @NonNull ElementAnnotationMetadataFactory annotationMetadataFactory, int arrayDimensions, boolean isRawType) { super( - bounds.get(0).classElement, - annotationMetadataFactory, - bounds.get(0).visitorContext, - bounds.get(0).typeArguments, - bounds.get(0).getTypeArguments(), - arrayDimensions + classElementRepresentingThisPlaceholder.getNativeType(), + annotationMetadataFactory, + classElementRepresentingThisPlaceholder.visitorContext, + classElementRepresentingThisPlaceholder.typeArguments, + classElementRepresentingThisPlaceholder.getTypeArguments(), + arrayDimensions ); + this.genericNativeType = genericNativeType; + this.declaredElement = declaredElement; this.realTypeVariable = realTypeVariable; + this.resolved = resolved; this.bounds = bounds; this.isRawType = isRawType; + typeAnnotationMetadata = new AbstractElementAnnotationMetadata() { + + AnnotationMetadata annotationMetadata; + + public AnnotationMetadata getReturnInstance() { + return getAnnotationMetadata(); + } + + @Override + protected MutableAnnotationMetadataDelegate getAnnotationMetadataToWrite() { + return getGenericTypeAnnotationMetadata(); + } + + @Override + public AnnotationMetadata getAnnotationMetadata() { + if (annotationMetadata == null) { + List allAnnotationMetadata = new ArrayList<>(); + getBounds().forEach(ce -> allAnnotationMetadata.add(ce.getTypeAnnotationMetadata())); + allAnnotationMetadata.add(JavaGenericPlaceholderElement.super.getTypeAnnotationMetadata()); + allAnnotationMetadata.add(getGenericTypeAnnotationMetadata()); + annotationMetadata = new AnnotationMetadataHierarchy(true, allAnnotationMetadata.toArray(AnnotationMetadata[]::new)); + } + return annotationMetadata; + } + }; + } + + private static JavaClassElement selectClassElementRepresentingThisPlaceholder(@Nullable JavaClassElement resolved, + @NonNull List bounds) { + if (resolved != null) { + return resolved; + } + return WildcardElement.findUpperType(bounds, bounds); } @Override - public Object getGenericNativeType() { - return realTypeVariable; + protected MutableAnnotationMetadataDelegate getAnnotationMetadataToWrite() { + return getGenericTypeAnnotationMetadata(); } @Override - public boolean isTypeVariable() { - return true; + public MutableAnnotationMetadataDelegate getGenericTypeAnnotationMetadata() { + if (genericTypeAnnotationMetadata == null) { + genericTypeAnnotationMetadata = elementAnnotationMetadataFactory.buildGenericTypeAnnotations(this); + } + return genericTypeAnnotationMetadata; } @Override - public boolean isRawType() { - return isRawType; + public MutableAnnotationMetadataDelegate getTypeAnnotationMetadata() { + return typeAnnotationMetadata; } @Override - public MutableAnnotationMetadataDelegate getAnnotationMetadata() { - return bounds.get(0).getAnnotationMetadata(); + public AnnotationMetadata getAnnotationMetadata() { + return new AnnotationMetadataHierarchy(true, super.getAnnotationMetadata(), getGenericTypeAnnotationMetadata()); } @Override - public Object getNativeType() { - // Native types should be always Element - return getParameterElement(); + public JavaNativeElement.Placeholder getGenericNativeType() { + return genericNativeType; + } + + @Override + public boolean isTypeVariable() { + return true; + } + + @Override + public boolean isRawType() { + return isRawType; } @NonNull @@ -106,13 +189,12 @@ public String getVariableName() { @Override public Optional getDeclaringElement() { - TypeMirror returnType = getParameterElement().getGenericElement().asType(); - return Optional.of(newClassElement(returnType, getTypeArguments())); + return Optional.of(declaredElement); } @Override public ClassElement withArrayDimensions(int arrayDimensions) { - return new JavaGenericPlaceholderElement(realTypeVariable, bounds, elementAnnotationMetadataFactory, arrayDimensions, isRawType); + return new JavaGenericPlaceholderElement(genericNativeType, realTypeVariable, declaredElement, resolved, bounds, elementAnnotationMetadataFactory, arrayDimensions, isRawType); } @Override @@ -121,4 +203,13 @@ public ClassElement foldBoundGenericTypes(@NonNull Function getResolved() { + return Optional.ofNullable(resolved); + } + + @Nullable + public JavaClassElement getResolvedInternal() { + return resolved; + } } diff --git a/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaMethodElement.java b/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaMethodElement.java index 7d6a7950535..f789cde4d80 100644 --- a/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaMethodElement.java +++ b/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaMethodElement.java @@ -59,25 +59,32 @@ public class JavaMethodElement extends AbstractJavaElement implements MethodElem private ParameterElement continuationParameter; private ClassElement genericReturnType; private ClassElement returnType; + private Map typeArguments; + private Map declaredTypeArguments; /** * @param owningType The declaring class - * @param executableElement The {@link ExecutableElement} + * @param nativeElement The native element * @param annotationMetadataFactory The annotation metadata factory * @param visitorContext The visitor context */ public JavaMethodElement(JavaClassElement owningType, - ExecutableElement executableElement, + JavaNativeElement.Method nativeElement, ElementAnnotationMetadataFactory annotationMetadataFactory, JavaVisitorContext visitorContext) { - super(executableElement, annotationMetadataFactory, visitorContext); - this.executableElement = executableElement; + super(nativeElement, annotationMetadataFactory, visitorContext); + this.executableElement = nativeElement.element(); this.owningType = owningType; } + @Override + public JavaNativeElement.Method getNativeType() { + return (JavaNativeElement.Method) super.getNativeType(); + } + @Override protected AbstractJavaElement copyThis() { - return new JavaMethodElement(owningType, executableElement, elementAnnotationMetadataFactory, visitorContext); + return new JavaMethodElement(owningType, getNativeType(), elementAnnotationMetadataFactory, visitorContext); } @Override @@ -117,7 +124,7 @@ public ClassElement[] getThrownTypes() { if (!thrownTypes.isEmpty()) { return thrownTypes.stream() .map(tm -> newClassElement(tm, getDeclaringType().getTypeArguments())) - .toArray(ClassElement[]::new); + .toArray(ClassElement[]::new); } return ClassElement.ZERO_CLASS_ELEMENTS; @@ -133,7 +140,7 @@ public boolean hides(MemberElement hidden) { if (isStatic() && getDeclaringType().isInterface()) { return false; } - return visitorContext.getElements().hides((Element) getNativeType(), (Element) hidden.getNativeType()); + return visitorContext.getElements().hides(getNativeType().element(), ((JavaNativeElement.Method) hidden.getNativeType()).element()); } @NonNull @@ -161,6 +168,22 @@ public List getDeclaredTypeVariables() { .toList(); } + @Override + public Map getTypeArguments() { + if (typeArguments == null) { + typeArguments = MethodElement.super.getTypeArguments(); + } + return typeArguments; + } + + @Override + public Map getDeclaredTypeArguments() { + if (declaredTypeArguments == null) { + declaredTypeArguments = resolveTypeArguments(executableElement, getDeclaringType().getTypeArguments()); + } + return declaredTypeArguments; + } + @Override public boolean isSuspend() { getParameters(); @@ -187,7 +210,7 @@ public ParameterElement[] getParameters() { @Override public MethodElement withNewOwningType(ClassElement owningType) { - JavaMethodElement javaMethodElement = new JavaMethodElement((JavaClassElement) owningType, executableElement, elementAnnotationMetadataFactory, visitorContext); + JavaMethodElement javaMethodElement = new JavaMethodElement((JavaClassElement) owningType, getNativeType(), elementAnnotationMetadataFactory, visitorContext); copyValues(javaMethodElement); return javaMethodElement; } @@ -211,7 +234,7 @@ public ParameterElement[] getSuspendParameters() { */ @NonNull protected JavaParameterElement newParameterElement(@NonNull MethodElement methodElement, @NonNull VariableElement variableElement) { - return new JavaParameterElement(owningType, methodElement, variableElement, elementAnnotationMetadataFactory, visitorContext); + return new JavaParameterElement(owningType, methodElement, new JavaNativeElement.Variable(variableElement), elementAnnotationMetadataFactory, visitorContext); } @Override @@ -223,8 +246,8 @@ public JavaClassElement getDeclaringType() { if (owningType.getName().equals(typeName)) { resolvedDeclaringClass = owningType; } else { - Map typeArguments = owningType.getTypeArguments(typeName); - resolvedDeclaringClass = (JavaClassElement) newClassElement(te.asType(), typeArguments); + Map parentTypeArguments = owningType.getTypeArguments(typeName); + resolvedDeclaringClass = (JavaClassElement) newClassElement(te.asType(), parentTypeArguments); } } else { return owningType; @@ -254,7 +277,7 @@ private ClassElement returnType(Map genericInfo) { } } final TypeMirror returnType = executableElement.getReturnType(); - return newClassElement(returnType, genericInfo); + return newClassElement(getNativeType(), returnType, genericInfo); } private static boolean sameType(String type, DeclaredType dt) { @@ -263,8 +286,7 @@ private static boolean sameType(String type, DeclaredType dt) { } private boolean isSuspend(VariableElement ve) { - if (ve != null && ve.asType() instanceof DeclaredType) { - DeclaredType dt = (DeclaredType) ve.asType(); + if (ve != null && ve.asType() instanceof DeclaredType dt) { return sameType("kotlin.coroutines.Continuation", dt); } return false; diff --git a/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaNativeElement.java b/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaNativeElement.java new file mode 100644 index 00000000000..c80041c00c4 --- /dev/null +++ b/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaNativeElement.java @@ -0,0 +1,94 @@ +/* + * Copyright 2017-2023 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.annotation.processing.visitor; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.Nullable; + +import javax.lang.model.element.Element; +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.element.PackageElement; +import javax.lang.model.element.TypeElement; +import javax.lang.model.element.VariableElement; +import javax.lang.model.type.TypeMirror; +import javax.lang.model.type.TypeVariable; + +/** + * The Java native element. + * + * @author Denis Stepanov + * @since 4.0.0 + */ +@Internal +public sealed interface JavaNativeElement { + + /** + * @return The native element. + */ + @Nullable + Element element(); + + /** + * The class native element. + * @param element The element + * @param typeMirror The type mirror + * @param owner The owner + */ + record Class(TypeElement element, @Nullable TypeMirror typeMirror, @Nullable JavaNativeElement owner) implements JavaNativeElement { + + Class(TypeElement element) { + this(element, null, null); + } + + Class(TypeElement element, @Nullable TypeMirror typeMirror) { + this(element, typeMirror, null); + } + + } + + /** + * The class native element. + * @param element The element + * @param typeVariable The type variable + * @param owner The owner + */ + record Placeholder(Element element, + TypeVariable typeVariable, + JavaNativeElement owner) implements JavaNativeElement { + } + + /** + * The method native element. + * @param element The element + */ + record Method(ExecutableElement element) implements JavaNativeElement { + } + + /** + * The variable native element. + * @param element The element + */ + record Variable(VariableElement element) implements JavaNativeElement { + } + + /** + * The package native element. + * @param element The element + */ + record Package(PackageElement element) implements JavaNativeElement { + } + +} diff --git a/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaPackageElement.java b/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaPackageElement.java index 92adfaf466a..7b717c78211 100644 --- a/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaPackageElement.java +++ b/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaPackageElement.java @@ -39,7 +39,7 @@ public class JavaPackageElement extends AbstractJavaElement implements io.micron public JavaPackageElement(PackageElement element, ElementAnnotationMetadataFactory annotationMetadataFactory, JavaVisitorContext visitorContext) { - super(element, annotationMetadataFactory, visitorContext); + super(new JavaNativeElement.Package(element), annotationMetadataFactory, visitorContext); this.element = element; } diff --git a/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaParameterElement.java b/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaParameterElement.java index 40638a35c03..d26e83dca8b 100644 --- a/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaParameterElement.java +++ b/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaParameterElement.java @@ -23,7 +23,6 @@ import io.micronaut.inject.ast.ParameterElement; import io.micronaut.inject.ast.annotation.ElementAnnotationMetadataFactory; -import javax.lang.model.element.VariableElement; import java.util.Collections; /** @@ -37,7 +36,6 @@ final class JavaParameterElement extends AbstractJavaElement implements Paramete private final JavaClassElement owningType; private final MethodElement methodElement; - private final VariableElement variableElement; private ClassElement typeElement; private ClassElement genericTypeElement; @@ -46,24 +44,28 @@ final class JavaParameterElement extends AbstractJavaElement implements Paramete * * @param owningType The owning class * @param methodElement The method element - * @param element The variable element + * @param nativeElement The native element * @param annotationMetadataFactory The annotation metadata factory * @param visitorContext The visitor context */ JavaParameterElement(JavaClassElement owningType, MethodElement methodElement, - VariableElement element, + JavaNativeElement.Variable nativeElement, ElementAnnotationMetadataFactory annotationMetadataFactory, JavaVisitorContext visitorContext) { - super(element, annotationMetadataFactory, visitorContext); + super(nativeElement, annotationMetadataFactory, visitorContext); this.owningType = owningType; this.methodElement = methodElement; - this.variableElement = element; + } + + @Override + public JavaNativeElement.Variable getNativeType() { + return (JavaNativeElement.Variable) super.getNativeType(); } @Override protected AbstractJavaElement copyThis() { - return new JavaParameterElement(owningType, methodElement, variableElement, elementAnnotationMetadataFactory, visitorContext); + return new JavaParameterElement(owningType, methodElement, getNativeType(), elementAnnotationMetadataFactory, visitorContext); } @Override @@ -90,7 +92,7 @@ public int getArrayDimensions() { @NonNull public ClassElement getType() { if (typeElement == null) { - typeElement = newClassElement(getNativeType().asType(), Collections.emptyMap()); + typeElement = newClassElement(getNativeType(), getNativeType().element().asType(), Collections.emptyMap()); } return typeElement; } @@ -99,7 +101,7 @@ public ClassElement getType() { @Override public ClassElement getGenericType() { if (genericTypeElement == null) { - genericTypeElement = newClassElement(getNativeType().asType(), methodElement.getDeclaringType().getTypeArguments()); + genericTypeElement = newClassElement(getNativeType(), getNativeType().element().asType(), methodElement.getTypeArguments()); } return genericTypeElement; } @@ -109,9 +111,4 @@ public MethodElement getMethodElement() { return methodElement; } - @Override - public VariableElement getNativeType() { - return (VariableElement) super.getNativeType(); - } - } diff --git a/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaPropertyElement.java b/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaPropertyElement.java index 09380fd1e16..c0e97b7b28d 100644 --- a/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaPropertyElement.java +++ b/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaPropertyElement.java @@ -23,14 +23,14 @@ import io.micronaut.core.annotation.Nullable; import io.micronaut.inject.annotation.AnnotationMetadataHierarchy; import io.micronaut.inject.ast.ClassElement; -import io.micronaut.inject.ast.annotation.ElementAnnotationMetadataFactory; import io.micronaut.inject.ast.FieldElement; -import io.micronaut.inject.ast.MemberElement; import io.micronaut.inject.ast.MethodElement; -import io.micronaut.inject.ast.annotation.MutableAnnotationMetadataDelegate; +import io.micronaut.inject.ast.ParameterElement; import io.micronaut.inject.ast.PropertyElement; +import io.micronaut.inject.ast.annotation.ElementAnnotationMetadata; +import io.micronaut.inject.ast.annotation.ElementAnnotationMetadataFactory; +import io.micronaut.inject.ast.annotation.MutableAnnotationMetadataDelegate; -import javax.lang.model.element.Element; import java.lang.annotation.Annotation; import java.util.ArrayList; import java.util.List; @@ -59,7 +59,7 @@ final class JavaPropertyElement extends AbstractJavaElement implements PropertyE @Nullable private final FieldElement field; private final boolean excluded; - private final MutableAnnotationMetadataDelegate annotationMetadata; + private final ElementAnnotationMetadata annotationMetadata; JavaPropertyElement(ClassElement owningElement, ClassElement type, @@ -82,15 +82,31 @@ final class JavaPropertyElement extends AbstractJavaElement implements PropertyE this.writeAccessKind = writeAccessKind; this.owningElement = owningElement; this.excluded = excluded; - List elements = new ArrayList<>(3); + List> elements = new ArrayList<>(3); if (setter != null) { elements.add(setter); + ParameterElement[] parameters = setter.getParameters(); + if (parameters.length > 0) { + ParameterElement parameter = parameters[0]; + MutableAnnotationMetadataDelegate typeAnnotationMetadata = parameter.getType().getTypeAnnotationMetadata(); + if (!typeAnnotationMetadata.isEmpty()) { + elements.add(typeAnnotationMetadata); + } + } } if (field != null) { elements.add(field); + MutableAnnotationMetadataDelegate typeAnnotationMetadata = field.getType().getTypeAnnotationMetadata(); + if (!typeAnnotationMetadata.isEmpty()) { + elements.add(typeAnnotationMetadata); + } } if (getter != null) { elements.add(getter); + MutableAnnotationMetadataDelegate typeAnnotationMetadata = getter.getReturnType().getTypeAnnotationMetadata(); + if (!typeAnnotationMetadata.isEmpty()) { + elements.add(typeAnnotationMetadata); + } } // The instance AnnotationMetadata of each element can change after a modification // Set annotation metadata as actual elements so the changes are reflected @@ -101,12 +117,12 @@ final class JavaPropertyElement extends AbstractJavaElement implements PropertyE propertyAnnotationMetadata = new AnnotationMetadataHierarchy( true, elements.stream().map(e -> { - if (e instanceof MethodElement) { + if (e instanceof MethodElement methodElement) { return new AnnotationMetadataDelegate() { @Override public AnnotationMetadata getAnnotationMetadata() { // Exclude type metadata - return e.getAnnotationMetadata().getDeclaredMetadata(); + return methodElement.getAnnotationMetadata().getDeclaredMetadata(); } }; } @@ -114,60 +130,60 @@ public AnnotationMetadata getAnnotationMetadata() { }).toArray(AnnotationMetadata[]::new) ); } - annotationMetadata = new MutableAnnotationMetadataDelegate() { + annotationMetadata = new ElementAnnotationMetadata() { @Override public io.micronaut.inject.ast.Element annotate(AnnotationValue annotationValue) { - for (MemberElement memberElement : elements) { - memberElement.annotate(annotationValue); + for (MutableAnnotationMetadataDelegate am : elements) { + am.annotate(annotationValue); } return JavaPropertyElement.this; } @Override public io.micronaut.inject.ast.Element annotate(String annotationType, Consumer> consumer) { - for (MemberElement memberElement : elements) { - memberElement.annotate(annotationType, consumer); + for (MutableAnnotationMetadataDelegate am : elements) { + am.annotate(annotationType, consumer); } return JavaPropertyElement.this; } @Override public io.micronaut.inject.ast.Element annotate(Class annotationType) { - for (MemberElement memberElement : elements) { - memberElement.annotate(annotationType); + for (MutableAnnotationMetadataDelegate am : elements) { + am.annotate(annotationType); } return JavaPropertyElement.this; } @Override public io.micronaut.inject.ast.Element annotate(String annotationType) { - for (MemberElement memberElement : elements) { - memberElement.annotate(annotationType); + for (MutableAnnotationMetadataDelegate am : elements) { + am.annotate(annotationType); } return JavaPropertyElement.this; } @Override public io.micronaut.inject.ast.Element annotate(Class annotationType, Consumer> consumer) { - for (MemberElement memberElement : elements) { - memberElement.annotate(annotationType, consumer); + for (MutableAnnotationMetadataDelegate am : elements) { + am.annotate(annotationType, consumer); } return JavaPropertyElement.this; } @Override public io.micronaut.inject.ast.Element removeAnnotation(String annotationType) { - for (MemberElement memberElement : elements) { - memberElement.removeAnnotation(annotationType); + for (MutableAnnotationMetadataDelegate am : elements) { + am.removeAnnotation(annotationType); } return JavaPropertyElement.this; } @Override public io.micronaut.inject.ast.Element removeAnnotationIf(Predicate> predicate) { - for (MemberElement memberElement : elements) { - memberElement.removeAnnotationIf(predicate); + for (MutableAnnotationMetadataDelegate am : elements) { + am.removeAnnotationIf(predicate); } return JavaPropertyElement.this; } @@ -189,17 +205,17 @@ public PropertyElement withAnnotationMetadata(AnnotationMetadata annotationMetad return (PropertyElement) super.withAnnotationMetadata(annotationMetadata); } - private static Element selectNativeType(MethodElement getter, + private static JavaNativeElement selectNativeType(MethodElement getter, MethodElement setter, FieldElement field) { if (getter != null) { - return (Element) getter.getNativeType(); + return (JavaNativeElement) getter.getNativeType(); } if (setter != null) { - return (Element) setter.getNativeType(); + return (JavaNativeElement) setter.getNativeType(); } if (field != null) { - return (Element) field.getNativeType(); + return (JavaNativeElement) field.getNativeType(); } throw new IllegalStateException(); } @@ -210,7 +226,7 @@ public boolean isExcluded() { } @Override - public MutableAnnotationMetadataDelegate getAnnotationMetadata() { + public ElementAnnotationMetadata getElementAnnotationMetadata() { return annotationMetadata; } diff --git a/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaVisitorContext.java b/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaVisitorContext.java index 0f1f4fc60ba..497bb98c0c6 100644 --- a/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaVisitorContext.java +++ b/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaVisitorContext.java @@ -76,7 +76,7 @@ * @since 1.0 */ @Internal -public class JavaVisitorContext implements VisitorContext, BeanElementVisitorContext { +public final class JavaVisitorContext implements VisitorContext, BeanElementVisitorContext { private final Messager messager; private final Elements elements; @@ -262,8 +262,8 @@ private void printMessage(String message, Diagnostic.Kind kind, @Nullable io.mic if (element instanceof BeanElement) { element = ((BeanElement) element).getDeclaringClass(); } - if (element instanceof AbstractJavaElement) { - Element el = (Element) element.getNativeType(); + if (element instanceof AbstractJavaElement abstractJavaElement) { + Element el = abstractJavaElement.getNativeType().element(); messager.printMessage(kind, message, el); } else { messager.printMessage(kind, message); diff --git a/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaWildcardElement.java b/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaWildcardElement.java index 5b75cc6d911..31eb1c51493 100644 --- a/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaWildcardElement.java +++ b/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaWildcardElement.java @@ -15,15 +15,21 @@ */ package io.micronaut.annotation.processing.visitor; +import io.micronaut.core.annotation.AnnotationMetadata; import io.micronaut.core.annotation.Internal; import io.micronaut.core.annotation.NonNull; +import io.micronaut.core.annotation.Nullable; +import io.micronaut.inject.annotation.AnnotationMetadataHierarchy; import io.micronaut.inject.ast.ArrayableClassElement; import io.micronaut.inject.ast.ClassElement; import io.micronaut.inject.ast.WildcardElement; +import io.micronaut.inject.ast.annotation.AbstractElementAnnotationMetadata; +import io.micronaut.inject.ast.annotation.ElementAnnotationMetadata; import io.micronaut.inject.ast.annotation.ElementAnnotationMetadataFactory; import io.micronaut.inject.ast.annotation.MutableAnnotationMetadataDelegate; import javax.lang.model.type.WildcardType; +import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.function.Function; @@ -40,6 +46,9 @@ final class JavaWildcardElement extends JavaClassElement implements WildcardElem private final JavaClassElement upperBound; private final List upperBounds; private final List lowerBounds; + private final ElementAnnotationMetadata typeAnnotationMetadata; + @Nullable + private ElementAnnotationMetadata genericTypeAnnotationMetadata; JavaWildcardElement(ElementAnnotationMetadataFactory elementAnnotationMetadataFactory, @NonNull WildcardType wildcardType, @@ -47,7 +56,7 @@ final class JavaWildcardElement extends JavaClassElement implements WildcardElem @NonNull List upperBounds, @NonNull List lowerBounds) { super( - mostUpper.classElement, + mostUpper.getNativeType(), elementAnnotationMetadataFactory, mostUpper.visitorContext, mostUpper.typeArguments, @@ -57,41 +66,65 @@ final class JavaWildcardElement extends JavaClassElement implements WildcardElem this.upperBound = mostUpper; this.upperBounds = upperBounds; this.lowerBounds = lowerBounds; + typeAnnotationMetadata = new AbstractElementAnnotationMetadata() { + + AnnotationMetadata annotationMetadata; + + public AnnotationMetadata getReturnInstance() { + return getAnnotationMetadata(); + } + + @Override + protected MutableAnnotationMetadataDelegate getAnnotationMetadataToWrite() { + return getGenericTypeAnnotationMetadata(); + } + + @Override + public AnnotationMetadata getAnnotationMetadata() { + if (annotationMetadata == null) { + List allAnnotationMetadata = new ArrayList<>(); + getLowerBounds().forEach(ce -> allAnnotationMetadata.add(ce.getTypeAnnotationMetadata())); + getUpperBounds().forEach(ce -> allAnnotationMetadata.add(ce.getTypeAnnotationMetadata())); + allAnnotationMetadata.add(JavaWildcardElement.super.getTypeAnnotationMetadata()); + allAnnotationMetadata.add(getGenericTypeAnnotationMetadata()); + annotationMetadata = new AnnotationMetadataHierarchy(true, allAnnotationMetadata.toArray(AnnotationMetadata[]::new)); + } + return annotationMetadata; + } + }; } @Override - public MutableAnnotationMetadataDelegate getAnnotationMetadata() { - return upperBound.getAnnotationMetadata(); + public MutableAnnotationMetadataDelegate getGenericTypeAnnotationMetadata() { + if (genericTypeAnnotationMetadata == null) { + genericTypeAnnotationMetadata = elementAnnotationMetadataFactory.buildGenericTypeAnnotations(this); + } + return genericTypeAnnotationMetadata; } @Override - public boolean isTypeVariable() { - return true; + protected MutableAnnotationMetadataDelegate getAnnotationMetadataToWrite() { + return getGenericTypeAnnotationMetadata(); } @Override - public Object getNativeType() { - return wildcardType; + public MutableAnnotationMetadataDelegate getTypeAnnotationMetadata() { + return typeAnnotationMetadata; } @Override - public int hashCode() { - return wildcardType.hashCode(); + public AnnotationMetadata getAnnotationMetadata() { + return new AnnotationMetadataHierarchy(true, super.getAnnotationMetadata(), getGenericTypeAnnotationMetadata()); } @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null) { - return false; - } - io.micronaut.inject.ast.Element that = (io.micronaut.inject.ast.Element) o; - if (that instanceof JavaWildcardElement wildcardElement) { - return wildcardElement.wildcardType.equals(wildcardType); - } - return false; + public Object getGenericNativeType() { + return wildcardType; + } + + @Override + public boolean isTypeVariable() { + return true; } @NonNull diff --git a/inject-java/src/test/groovy/io/micronaut/annotation/AnnotateFieldSpec.groovy b/inject-java/src/test/groovy/io/micronaut/annotation/AnnotateFieldSpec.groovy new file mode 100644 index 00000000000..ea881d777d1 --- /dev/null +++ b/inject-java/src/test/groovy/io/micronaut/annotation/AnnotateFieldSpec.groovy @@ -0,0 +1,84 @@ +package io.micronaut.annotation + +import io.micronaut.annotation.processing.test.AbstractTypeElementSpec +import io.micronaut.core.annotation.AnnotationUtil +import io.micronaut.inject.ast.ClassElement +import io.micronaut.inject.visitor.TypeElementVisitor +import io.micronaut.inject.visitor.VisitorContext + +class AnnotateFieldSpec extends AbstractTypeElementSpec { + + void 'test annotating'() { + when: + def definition = buildBeanDefinition('addann.AnnotationFieldClass', ''' +package addann; + +import io.micronaut.context.annotation.Bean; +import jakarta.inject.Inject; + +@Bean +class AnnotationFieldClass { + + @Inject + public MyBean1 myField1; + + @Inject + public MyBean1 myField2; + +} + +class MyBean1 { +} + +''') + then: + def myField1 = definition.getInjectedFields()[0] + myField1.name == "myField1" + myField1.asArgument().getAnnotationMetadata().hasAnnotation(MyAnnotation) + and: + def myField2 = definition.getInjectedFields()[1] + myField2.name == "myField2" + !myField2.asArgument().getAnnotationMetadata().hasAnnotation(MyAnnotation) + } + + static class AnnotationFieldVisitor implements TypeElementVisitor { + @Override + void visitClass(ClassElement element, VisitorContext context) { + if (element.getSimpleName() == "AnnotationFieldClass") { + def myField1 = element.findField("myField1").get() + assert myField1.getAnnotationMetadata().getAnnotationNames().asList() == [AnnotationUtil.INJECT] + myField1.annotate(MyAnnotation) + assert myField1.getAnnotationMetadata().getAnnotationNames().asList() == [AnnotationUtil.INJECT, MyAnnotation.class.name] + assert myField1.getAnnotationMetadata().getAnnotationNames().asList() == [AnnotationUtil.INJECT, MyAnnotation.class.name] + assert myField1.getType().getAnnotationNames().isEmpty() + assert myField1.getType().getTypeAnnotationMetadata().getAnnotationNames().isEmpty() + assert myField1.getType().getType().getAnnotationMetadata().isEmpty() + assert myField1.getGenericType().getAnnotationNames().isEmpty() + assert myField1.getGenericType().getTypeAnnotationMetadata().getAnnotationNames().isEmpty() + assert myField1.getGenericType().getType().getAnnotationMetadata().isEmpty() + + // Validate the cache is working + assert context.getClassElement("addann.AnnotationFieldClass").get() + .findField("myField1").get() + .getAnnotationMetadata().getAnnotationNames().asList() == [AnnotationUtil.INJECT, MyAnnotation.class.name] + + // Test the second method with the same type doesn't have the annotations + + def myField2 = element.findField("myField2").get() + assert myField2.getAnnotationMetadata().getAnnotationNames().asList() == [AnnotationUtil.INJECT] + assert myField2.getType().getAnnotationNames().isEmpty() + assert myField2.getType().getTypeAnnotationMetadata().getAnnotationNames().isEmpty() + assert myField2.getGenericType().getAnnotationNames().isEmpty() + assert myField2.getGenericType().getTypeAnnotationMetadata().getAnnotationNames().isEmpty() + + // Validate the cache is working + assert context.getClassElement("addann.AnnotationFieldClass").get() + .findField("myField2").get() + .getAnnotationMetadata().getAnnotationNames().asList() == [AnnotationUtil.INJECT] + + assert context.getClassElement("addann.MyBean1").get().getAnnotationMetadata().isEmpty() + } + } + } + +} diff --git a/inject-java/src/test/groovy/io/micronaut/annotation/AnnotateFieldTypeSpec.groovy b/inject-java/src/test/groovy/io/micronaut/annotation/AnnotateFieldTypeSpec.groovy new file mode 100644 index 00000000000..e7eac7f161e --- /dev/null +++ b/inject-java/src/test/groovy/io/micronaut/annotation/AnnotateFieldTypeSpec.groovy @@ -0,0 +1,254 @@ +package io.micronaut.annotation + +import io.micronaut.annotation.processing.test.AbstractTypeElementSpec +import io.micronaut.inject.BeanDefinition +import io.micronaut.inject.ast.ClassElement +import io.micronaut.inject.ast.GenericPlaceholderElement +import io.micronaut.inject.visitor.TypeElementVisitor +import io.micronaut.inject.visitor.VisitorContext + +class AnnotateFieldTypeSpec extends AbstractTypeElementSpec { + + void 'test annotating'() { + when: + def definition = buildBeanDefinition('addann.AnnotateFieldTypeClass', ''' +package addann; + +import io.micronaut.context.annotation.Bean; +import jakarta.inject.Inject; + +@Bean +class AnnotateFieldTypeClass { + + @Inject + public MyBean1 myField1; + + @Inject + public MyBean1 myField2; + +} + +class MyBean1 { + + private String name; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + +} +''') + then: + validate(definition) + } + + void 'test annotating 2'() { + when: + def definition = buildBeanDefinition('addann.AnnotateFieldTypeClass', ''' +package addann; + +import io.micronaut.context.annotation.Executable; +import io.micronaut.context.annotation.Bean; +import jakarta.inject.Inject; + +@Bean +class AnnotateFieldTypeClass { + + @Inject + public T myField1; + + @Inject + public T myField2; + +} + +class MyBean1 { + + private String name; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + +} +''') + then: + validate(definition) + } + + void 'test annotating 3'() { + when: + def definition = buildBeanDefinition('addann.AnnotateFieldTypeClass', ''' +package addann; + +import io.micronaut.context.annotation.Bean; +import jakarta.inject.Inject; + +abstract class BaseAnnotateFieldTypeClass { + + @Inject + public S myField1; + + @Inject + public S myField2; + +} + +@Bean +class AnnotateFieldTypeClass extends BaseAnnotateFieldTypeClass { +} + +class MyBean1 { + + private String name; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + +} +''') + then: + validate(definition) + } + + void 'test annotating 4'() { + when: + def definition = buildBeanDefinition('addann.AnnotateFieldTypeClass', ''' +package addann; + +import io.micronaut.context.annotation.Bean; +import jakarta.inject.Inject; + +abstract class BaseAnnotateFieldTypeClass { + + @Inject + public S myField1; + + @Inject + public S myField2; + +} + +@Bean +class AnnotateFieldTypeClass extends BaseAnnotateFieldTypeClass { +} + +class MyBean1 { + + private String name; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + +} +''') + then: + validate(definition) + } + + void validate(BeanDefinition definition) { + def myField1 = definition.getInjectedFields()[0] + myField1.name == "myField1" + myField1.asArgument().getAnnotationMetadata().hasAnnotation(MyAnnotation) + + def myField2 = definition.getInjectedFields()[1] + myField2.name == "myField2" + !myField2.asArgument().getAnnotationMetadata().hasAnnotation(MyAnnotation) + } + + static class AnnotateFieldTypeVisitor implements TypeElementVisitor { + @Override + void visitClass(ClassElement classElement, VisitorContext context) { + if (classElement.getSimpleName() == "AnnotateFieldTypeClass") { + + def myField1 = classElement.findField("myField1").get() + def type = myField1.getType() + def genericType = myField1.getGenericType() + if (type instanceof GenericPlaceholderElement) { + assert genericType instanceof GenericPlaceholderElement + def placeholderElement = type as GenericPlaceholderElement + def genericPlaceholderElement = genericType as GenericPlaceholderElement + assert placeholderElement.getGenericNativeType() == genericPlaceholderElement.getGenericNativeType() + assert placeholderElement.variableName == genericPlaceholderElement.variableName + } + + assert type.getAnnotationMetadata().getAnnotationNames().isEmpty() + assert genericType.getAnnotationMetadata().getAnnotationNames().isEmpty() + + type.annotate(MyAnnotation) + + assert type.getAnnotationMetadata().getAnnotationNames().asList() == [MyAnnotation.class.name] + assert genericType.getAnnotationMetadata().getAnnotationNames().asList() == [MyAnnotation.class.name] + // The annotation should be added to type annotations + assert type.getTypeAnnotationMetadata().getAnnotationNames().asList() == [MyAnnotation.class.name] + assert genericType.getTypeAnnotationMetadata().getAnnotationNames().asList() == [MyAnnotation.class.name] + // The annotation is not added to the actual type + assert type.getType().isEmpty() + assert genericType.getType().isEmpty() + + // Validate the cache is working + + def newClassElement = context.getClassElement("addann.AnnotateFieldTypeClass").get() + def newField = newClassElement.findField("myField1").get() + def newType = newField.getType() + def newGenericType = newField.getGenericType() + + validateBeanType(newGenericType.getType()) + + assert newType.getAnnotationMetadata().getAnnotationNames().asList() == [MyAnnotation.class.name] + assert newType.getTypeAnnotationMetadata().getAnnotationNames().asList() == [MyAnnotation.class.name] + assert newGenericType.getAnnotationMetadata().getAnnotationNames().asList() == [MyAnnotation.class.name] + assert newGenericType.getTypeAnnotationMetadata().getAnnotationNames().asList() == [MyAnnotation.class.name] + + // Validate the annotation is not added to the return class type of myMethod2 + + def field2Type = newClassElement.findField("myField2").get().getType() + def field2GenericType = newClassElement.findField("myField2").get().getGenericType() + + validateBeanType(field2GenericType.getType()) + + assert field2Type.getAnnotationMetadata().isEmpty() + assert field2Type.getTypeAnnotationMetadata().isEmpty() + + assert field2GenericType.getAnnotationMetadata().isEmpty() + assert field2GenericType.getTypeAnnotationMetadata().isEmpty() + + assert field2Type.getTypeAnnotationMetadata().isEmpty() + assert field2Type.getAnnotationMetadata().isEmpty() + + assert field2GenericType.getTypeAnnotationMetadata().isEmpty() + assert field2GenericType.getAnnotationMetadata().isEmpty() + + def bean = context.getClassElement("addann.MyBean1").get() + validateBeanType(bean) + } + + } + + private static void validateBeanType(ClassElement bean) { + assert bean.getAnnotationMetadata().isEmpty() + assert bean.getTypeAnnotationMetadata().isEmpty() + assert bean.getMethods().size() == 2 + assert bean.getFields().size() == 1 + } + } + +} diff --git a/inject-java/src/test/groovy/io/micronaut/annotation/AnnotateMethodParameterSpec.groovy b/inject-java/src/test/groovy/io/micronaut/annotation/AnnotateMethodParameterSpec.groovy new file mode 100644 index 00000000000..c82279b1501 --- /dev/null +++ b/inject-java/src/test/groovy/io/micronaut/annotation/AnnotateMethodParameterSpec.groovy @@ -0,0 +1,259 @@ +package io.micronaut.annotation + +import io.micronaut.annotation.processing.test.AbstractTypeElementSpec +import io.micronaut.inject.BeanDefinition +import io.micronaut.inject.ast.ClassElement +import io.micronaut.inject.ast.GenericPlaceholderElement +import io.micronaut.inject.visitor.TypeElementVisitor +import io.micronaut.inject.visitor.VisitorContext + +class AnnotateMethodParameterSpec extends AbstractTypeElementSpec { + + void 'test annotating'() { + when: + def definition = buildBeanDefinition('addann.AnnotateMethodParameterClass', ''' +package addann; + +import io.micronaut.context.annotation.Executable; +import io.micronaut.context.annotation.Bean; + +@Bean +class AnnotateMethodParameterClass { + + @Executable + public MyBean1 myMethod1(MyBean1 param) { + return null; + } + + @Executable + public MyBean1 myMethod2(MyBean1 param) { + return null; + } + +} + +class MyBean1 { +} +''') + then: + validate(definition) + } + + void 'test annotating 2'() { + when: + def definition = buildBeanDefinition('addann.AnnotateMethodParameterClass', ''' +package addann; + +import io.micronaut.context.annotation.Executable; +import io.micronaut.context.annotation.Bean; + +@Bean +class AnnotateMethodParameterClass { + + @Executable + public T myMethod1(T param) { + return null; + } + + @Executable + public T myMethod2(T param) { + return null; + } + +} + +class MyBean1 { +} +''') + then: + validate(definition) + } + + void 'test annotating 3'() { + when: + def definition = buildBeanDefinition('addann.AnnotateMethodParameterClass', ''' +package addann; + +import io.micronaut.context.annotation.Executable; +import io.micronaut.context.annotation.Bean; + +@Bean +class AnnotateMethodParameterClass { + + @Executable + public K myMethod1(K param) { + return null; + } + + @Executable + public K myMethod2(K param) { + return null; + } + +} + +class MyBean1 { +} +''') + then: + validate(definition) + } + + void 'test annotating 4'() { + when: + def definition = buildBeanDefinition('addann.AnnotateMethodParameterClass', ''' +package addann; + +import io.micronaut.context.annotation.Executable; +import io.micronaut.context.annotation.Bean; + +abstract class Base { + + @Executable + public S myMethod1(S param) { + return null; + } + + @Executable + public S myMethod2(S param) { + return null; + } + +} + +@Bean +class AnnotateMethodParameterClass extends Base { +} + +class MyBean1 { +} +''') + then: + validate(definition) + } + + void 'test annotating 6'() { + when: + def definition = buildBeanDefinition('addann.AnnotateMethodParameterClass', ''' +package addann; + +import io.micronaut.context.annotation.Executable; +import io.micronaut.context.annotation.Bean; + +abstract class Base { + + @Executable + public S myMethod1(S param) { + return null; + } + + @Executable + public S myMethod2(S param) { + return null; + } + +} + +@Bean +class AnnotateMethodParameterClass extends Base { +} + +class MyBean1 { +} +''') + then: + validate(definition) + } + + void validate(BeanDefinition definition) { + def method1 = definition.findPossibleMethods("myMethod1").findAny().get() + def method1ParameterType = method1.getArguments()[0] + def method1ReturnType = method1.getReturnType() + + assert method1ParameterType.simpleName == "MyBean1" + assert method1ReturnType.simpleName == "MyBean1" + assert method1ParameterType.getAnnotationMetadata().hasAnnotation(MyAnnotation) + assert !method1ReturnType.getAnnotationMetadata().hasAnnotation(MyAnnotation) + assert !method1ReturnType.asArgument().getAnnotationMetadata().hasAnnotation(MyAnnotation) + assert !method1.hasAnnotation(MyAnnotation) + + def method2 = definition.findPossibleMethods("myMethod2").findAny().get() + def method2ParameterType = method2.getArguments()[0] + def method2ReturnType = method2.getReturnType() + + assert !method2ParameterType.getAnnotationMetadata().hasAnnotation(MyAnnotation) + assert !method2ReturnType.getAnnotationMetadata().hasAnnotation(MyAnnotation) + assert !method2ReturnType.asArgument().getAnnotationMetadata().hasAnnotation(MyAnnotation) + assert !method2.hasAnnotation(MyAnnotation) + assert !method2.hasAnnotation(MyAnnotation) + } + + static class AnnotateMethodParameterVisitor implements TypeElementVisitor { + @Override + void visitClass(ClassElement classElement, VisitorContext context) { + if (classElement.getSimpleName() == "AnnotateMethodParameterClass") { + + def myMethod1 = classElement.findMethod("myMethod1").get() + def type = myMethod1.getParameters()[0].getType() + def genericType = myMethod1.getParameters()[0].getGenericType() + if (type instanceof GenericPlaceholderElement) { + assert genericType instanceof GenericPlaceholderElement + def placeholderElement = type as GenericPlaceholderElement + def genericPlaceholderElement = genericType as GenericPlaceholderElement + assert placeholderElement.getGenericNativeType() == genericPlaceholderElement.getGenericNativeType() + assert placeholderElement.variableName == genericPlaceholderElement.variableName + } + + assert type.getAnnotationMetadata().getAnnotationNames().isEmpty() + assert genericType.getAnnotationMetadata().getAnnotationNames().isEmpty() + + type.annotate(MyAnnotation) + + assert type.getAnnotationMetadata().getAnnotationNames().asList() == [MyAnnotation.class.name] + assert genericType.getAnnotationMetadata().getAnnotationNames().asList() == [MyAnnotation.class.name] + // The annotation should be added to type annotations + assert type.getTypeAnnotationMetadata().getAnnotationNames().asList() == [MyAnnotation.class.name] + assert genericType.getTypeAnnotationMetadata().getAnnotationNames().asList() == [MyAnnotation.class.name] + // The annotation is not added to the actual type + assert type.getType().isEmpty() + assert genericType.getType().isEmpty() + myMethod1.getReturnType().getAnnotationMetadata().isEmpty() + myMethod1.getGenericReturnType().getAnnotationMetadata().isEmpty() + + // Validate the cache is working + + def newClassElement = context.getClassElement("addann.AnnotateMethodParameterClass").get() + def newMethod = newClassElement.findMethod("myMethod1").get() + def newType = newMethod.getParameters()[0].getType() + def newGenericType =newMethod.getParameters()[0].getGenericType() + + assert newType.getAnnotationMetadata().getAnnotationNames().asList() == [MyAnnotation.class.name] + assert newType.getTypeAnnotationMetadata().getAnnotationNames().asList() == [MyAnnotation.class.name] + assert newGenericType.getAnnotationMetadata().getAnnotationNames().asList() == [MyAnnotation.class.name] + assert newGenericType.getTypeAnnotationMetadata().getAnnotationNames().asList() == [MyAnnotation.class.name] + + assert context.getClassElement("addann.MyBean1").get().getAnnotationMetadata().isEmpty() + assert context.getClassElement("addann.MyBean1").get().getTypeAnnotationMetadata().isEmpty() + + // Validate the annotation is not added to the return class type of myMethod2 + + def method2Type = newClassElement.findMethod("myMethod2").get().getParameters()[0].getType() + def method2GenericType = newClassElement.findMethod("myMethod2").get().getParameters()[0].getGenericType() + + assert method2Type.getAnnotationMetadata().isEmpty() + assert method2Type.getTypeAnnotationMetadata().isEmpty() + + assert method2GenericType.getAnnotationMetadata().isEmpty() + assert method2GenericType.getTypeAnnotationMetadata().isEmpty() + + assert method2Type.getTypeAnnotationMetadata().isEmpty() + assert method2Type.getAnnotationMetadata().isEmpty() + + assert method2GenericType.getTypeAnnotationMetadata().isEmpty() + assert method2GenericType.getAnnotationMetadata().isEmpty() + + } + } + } + +} diff --git a/inject-java/src/test/groovy/io/micronaut/annotation/AnnotateMethodReturnSpec.groovy b/inject-java/src/test/groovy/io/micronaut/annotation/AnnotateMethodReturnSpec.groovy new file mode 100644 index 00000000000..e01c3ed368a --- /dev/null +++ b/inject-java/src/test/groovy/io/micronaut/annotation/AnnotateMethodReturnSpec.groovy @@ -0,0 +1,324 @@ +package io.micronaut.annotation + +import io.micronaut.annotation.processing.test.AbstractTypeElementSpec +import io.micronaut.inject.BeanDefinition +import io.micronaut.inject.ast.ClassElement +import io.micronaut.inject.ast.GenericPlaceholderElement +import io.micronaut.inject.visitor.TypeElementVisitor +import io.micronaut.inject.visitor.VisitorContext + +class AnnotateMethodReturnSpec extends AbstractTypeElementSpec { + + void 'test annotating'() { + when: + def definition = buildBeanDefinition('addann.AnnotateMethodReturnClass', ''' +package addann; + +import io.micronaut.context.annotation.Executable; +import io.micronaut.context.annotation.Bean; + +@Bean +class AnnotateMethodReturnClass { + + @Executable + public MyBean1 myMethod1() { + return null; + } + + @Executable + public MyBean1 myMethod2() { + return null; + } + +} + +class MyBean1 { + + private String name; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + +} +''') + then: + validate(definition) + } + + void 'test annotating 2'() { + when: + def definition = buildBeanDefinition('addann.AnnotateMethodReturnClass', ''' +package addann; + +import io.micronaut.context.annotation.Executable; +import io.micronaut.context.annotation.Bean; + +@Bean +class AnnotateMethodReturnClass { + + @Executable + public T myMethod1() { + return null; + } + + @Executable + public T myMethod2() { + return null; + } + +} + +class MyBean1 { + + private String name; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + +} +''') + then: + validate(definition) + } + + void 'test annotating 3'() { + when: + def definition = buildBeanDefinition('addann.AnnotateMethodReturnClass', ''' +package addann; + +import io.micronaut.context.annotation.Executable; +import io.micronaut.context.annotation.Bean; + +@Bean +class AnnotateMethodReturnClass { + + @Executable + public K myMethod1() { + return null; + } + + @Executable + public K myMethod2() { + return null; + } + +} + +class MyBean1 { + + private String name; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + +} +''') + then: + validate(definition) + } + + void 'test annotating 4'() { + when: + def definition = buildBeanDefinition('addann.AnnotateMethodReturnClass', ''' +package addann; + +import io.micronaut.context.annotation.Executable; +import io.micronaut.context.annotation.Bean; + +abstract class BaseAnnotateMethodReturnClass { + + @Executable + public S myMethod1() { + return null; + } + + @Executable + public S myMethod2() { + return null; + } + +} + +@Bean +class AnnotateMethodReturnClass extends BaseAnnotateMethodReturnClass { +} + +class MyBean1 { + + private String name; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + +} +''') + then: + validate(definition) + } + + void 'test annotating 6'() { + when: + def definition = buildBeanDefinition('addann.AnnotateMethodReturnClass', ''' +package addann; + +import io.micronaut.context.annotation.Executable; +import io.micronaut.context.annotation.Bean; + +abstract class BaseAnnotateMethodReturnClass { + + @Executable + public S myMethod1() { + return null; + } + + @Executable + public S myMethod2() { + return null; + } + +} + +@Bean +class AnnotateMethodReturnClass extends BaseAnnotateMethodReturnClass { +} + +class MyBean1 { + + private String name; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + +} +''') + then: + validate(definition) + } + + void validate(BeanDefinition definition) { + def method1 = definition.getRequiredMethod("myMethod1") + def method1ReturnType = method1.getReturnType() + + assert method1ReturnType.simpleName == "MyBean1" + assert method1ReturnType.getAnnotationMetadata().hasAnnotation(MyAnnotation) + assert method1ReturnType.asArgument().getAnnotationMetadata().hasAnnotation(MyAnnotation) + assert !method1.hasAnnotation(MyAnnotation) + + def method2 = definition.getRequiredMethod("myMethod2") + def method2ReturnType = method2.getReturnType() + + assert !method2ReturnType.getAnnotationMetadata().hasAnnotation(MyAnnotation) + assert !method2ReturnType.asArgument().getAnnotationMetadata().hasAnnotation(MyAnnotation) + assert !method2.hasAnnotation(MyAnnotation) + assert !method2.hasAnnotation(MyAnnotation) + } + + static class AnnotateMethodReturnVisitor implements TypeElementVisitor { + @Override + void visitClass(ClassElement classElement, VisitorContext context) { + if (classElement.getSimpleName() == "AnnotateMethodReturnClass") { + + def myMethod1 = classElement.findMethod("myMethod1").get() + def returnType = myMethod1.getReturnType() + def genericReturnType = myMethod1.getGenericReturnType() + if (returnType instanceof GenericPlaceholderElement) { + assert genericReturnType instanceof GenericPlaceholderElement + def placeholderElement = returnType as GenericPlaceholderElement + def genericPlaceholderElement = genericReturnType as GenericPlaceholderElement + assert placeholderElement.getGenericNativeType() == genericPlaceholderElement.getGenericNativeType() + assert placeholderElement.variableName == genericPlaceholderElement.variableName +// if ("K" == placeholderElement.variableName) { +// assert placeholderElement.declaringElement.get() == myMethod1 +// assert genericPlaceholderElement.declaringElement.get() == myMethod1 +// } else { +// assert placeholderElement.declaringElement.get() == classElement +// assert genericPlaceholderElement.declaringElement.get() == classElement +// } + } + + assert returnType.getAnnotationMetadata().getAnnotationNames().isEmpty() + assert genericReturnType.getAnnotationMetadata().getAnnotationNames().isEmpty() + + returnType.annotate(MyAnnotation) + + assert returnType.getAnnotationMetadata().getAnnotationNames().asList() == [MyAnnotation.class.name] + assert genericReturnType.getAnnotationMetadata().getAnnotationNames().asList() == [MyAnnotation.class.name] + // The annotation should be added to type annotations + assert returnType.getTypeAnnotationMetadata().getAnnotationNames().asList() == [MyAnnotation.class.name] + assert genericReturnType.getTypeAnnotationMetadata().getAnnotationNames().asList() == [MyAnnotation.class.name] + // The annotation is not added to the actual type + assert returnType.getType().isEmpty() + assert genericReturnType.getType().isEmpty() + + // Validate the cache is working + + def newClassElement = context.getClassElement("addann.AnnotateMethodReturnClass").get() + def newMethod = newClassElement.findMethod("myMethod1").get() + def newReturnType = newMethod.getReturnType() + def newGenericReturnType = newMethod.getGenericReturnType() + + validateBeanType(newGenericReturnType.getType()) + + assert newReturnType.getAnnotationMetadata().getAnnotationNames().asList() == [MyAnnotation.class.name] + assert newReturnType.getTypeAnnotationMetadata().getAnnotationNames().asList() == [MyAnnotation.class.name] + assert newGenericReturnType.getAnnotationMetadata().getAnnotationNames().asList() == [MyAnnotation.class.name] + assert newGenericReturnType.getTypeAnnotationMetadata().getAnnotationNames().asList() == [MyAnnotation.class.name] + + // Validate the annotation is not added to the return class type of myMethod2 + + def method2ReturnType = newClassElement.findMethod("myMethod2").get().getReturnType() + def method2GenericReturnType = newClassElement.findMethod("myMethod2").get().getGenericReturnType() + + validateBeanType(method2GenericReturnType.getType()) + + assert method2ReturnType.getAnnotationMetadata().isEmpty() + assert method2ReturnType.getTypeAnnotationMetadata().isEmpty() + + assert method2GenericReturnType.getAnnotationMetadata().isEmpty() + assert method2GenericReturnType.getTypeAnnotationMetadata().isEmpty() + + assert method2ReturnType.getTypeAnnotationMetadata().isEmpty() + assert method2ReturnType.getAnnotationMetadata().isEmpty() + + assert method2GenericReturnType.getTypeAnnotationMetadata().isEmpty() + assert method2GenericReturnType.getAnnotationMetadata().isEmpty() + + def bean = context.getClassElement("addann.MyBean1").get() + validateBeanType(bean) + } + } + + private static void validateBeanType(ClassElement bean) { + assert bean.getAnnotationMetadata().isEmpty() + assert bean.getTypeAnnotationMetadata().isEmpty() + assert bean.getMethods().size() == 2 + assert bean.getFields().size() == 1 + } + } + +} diff --git a/inject-java/src/test/groovy/io/micronaut/annotation/AnnotateMethodSpec.groovy b/inject-java/src/test/groovy/io/micronaut/annotation/AnnotateMethodSpec.groovy new file mode 100644 index 00000000000..24f433eaaf4 --- /dev/null +++ b/inject-java/src/test/groovy/io/micronaut/annotation/AnnotateMethodSpec.groovy @@ -0,0 +1,85 @@ +package io.micronaut.annotation + +import io.micronaut.annotation.processing.test.AbstractTypeElementSpec +import io.micronaut.context.annotation.Bean +import io.micronaut.context.annotation.Executable +import io.micronaut.inject.ast.ClassElement +import io.micronaut.inject.visitor.TypeElementVisitor +import io.micronaut.inject.visitor.VisitorContext + +class AnnotateMethodSpec extends AbstractTypeElementSpec { + + void 'test annotating'() { + when: + def definition = buildBeanDefinition('addann.AnnotationMethodClass', ''' +package addann; + +import io.micronaut.context.annotation.Executable; +import io.micronaut.context.annotation.Bean; + +@Bean +class AnnotationMethodClass { + + @Executable + public MyBean1 myMethod1() { + return null; + } + + @Executable + public MyBean1 myMethod2() { + return null; + } + +} + +class MyBean1 { +} + +''') + then: "myMethod1 has added annotation on the method and it's seen on the return type" + definition.getRequiredMethod("myMethod1").hasAnnotation(MyAnnotation) + definition.getRequiredMethod("myMethod1").getReturnType().getAnnotationMetadata().hasAnnotation(MyAnnotation) + definition.getRequiredMethod("myMethod1").getReturnType().asArgument().getAnnotationMetadata().hasAnnotation(MyAnnotation) + + and: "myMethod2 doesn't have the same annotation on the same type" + !definition.getRequiredMethod("myMethod2").hasAnnotation(MyAnnotation) + !definition.getRequiredMethod("myMethod2").getReturnType().getAnnotationMetadata().hasAnnotation(MyAnnotation) + !definition.getRequiredMethod("myMethod2").getReturnType().asArgument().getAnnotationMetadata().hasAnnotation(MyAnnotation) + } + + static class AnnotationMethodVisitor implements TypeElementVisitor { + @Override + void visitClass(ClassElement element, VisitorContext context) { + if (element.getSimpleName() == "AnnotationMethodClass") { + def myMethod1 = element.findMethod("myMethod1").get() + assert myMethod1.getAnnotationMetadata().getAnnotationNames().asList() == [Executable.class.name, Bean.class.name] + myMethod1.annotate(MyAnnotation) + assert myMethod1.getAnnotationMetadata().getAnnotationNames().asList() == [Executable.class.name, Bean.class.name, MyAnnotation.class.name] + assert myMethod1.getAnnotationMetadata().getAnnotationNames().asList() == [Executable.class.name, Bean.class.name, MyAnnotation.class.name] + assert myMethod1.getReturnType().getAnnotationNames().isEmpty() + assert myMethod1.getReturnType().getTypeAnnotationMetadata().getAnnotationNames().isEmpty() + assert myMethod1.getReturnType().getType().getAnnotationMetadata().isEmpty() + + // Validate the cache is working + assert context.getClassElement("addann.AnnotationMethodClass").get() + .findMethod("myMethod1").get() + .getAnnotationMetadata().getAnnotationNames().asList() == [Executable.class.name, Bean.class.name, MyAnnotation.class.name] + + // Test the second method with the same type doesn't have the annotations + + def myMethod2 = element.findMethod("myMethod2").get() + assert myMethod2.getAnnotationMetadata().getAnnotationNames().asList() == [Executable.class.name, Bean.class.name] + assert myMethod2.getReturnType().getAnnotationNames().isEmpty() + assert myMethod2.getReturnType().getTypeAnnotationMetadata().getAnnotationNames().isEmpty() + + // Validate the cache is working + assert context.getClassElement("addann.AnnotationMethodClass").get() + .findMethod("myMethod2").get() + .getAnnotationMetadata().getAnnotationNames().asList() == [Executable.class.name, Bean.class.name] + + assert context.getClassElement("addann.MyBean1").get().getAnnotationMetadata().isEmpty() + } + } + } + +} diff --git a/inject-java/src/test/groovy/io/micronaut/annotation/MyAnnotation.java b/inject-java/src/test/groovy/io/micronaut/annotation/MyAnnotation.java new file mode 100644 index 00000000000..783e98f383e --- /dev/null +++ b/inject-java/src/test/groovy/io/micronaut/annotation/MyAnnotation.java @@ -0,0 +1,28 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.annotation; + +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; + +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.PACKAGE, ElementType.TYPE, ElementType.ANNOTATION_TYPE, ElementType.METHOD, ElementType.FIELD}) +public @interface MyAnnotation { +} diff --git a/inject-java/src/test/groovy/io/micronaut/annotation/processing/visitor/JavaReconstructionSpec.groovy b/inject-java/src/test/groovy/io/micronaut/annotation/processing/visitor/JavaReconstructionSpec.groovy index b9627a65b89..c73830c4325 100644 --- a/inject-java/src/test/groovy/io/micronaut/annotation/processing/visitor/JavaReconstructionSpec.groovy +++ b/inject-java/src/test/groovy/io/micronaut/annotation/processing/visitor/JavaReconstructionSpec.groovy @@ -283,12 +283,12 @@ class Test { fieldType | expectedType 'String' | 'String' 'List' | 'List' - 'List' | 'List' - 'List' | 'List' + 'List' | 'List' + 'List' | 'List' 'List' | 'List' 'List' | 'List' - 'List' | 'List' - 'List[]>' | 'List[]>' + 'List' | 'List' + 'List[]>' | 'List[]>' 'List' | 'List' 'List>' | 'List>' } @@ -317,12 +317,12 @@ class Test { fieldType | expectedType 'String' | 'String' 'List' | 'List' - 'List' | 'List' - 'List' | 'List' + 'List' | 'List' + 'List' | 'List' 'List' | 'List' 'List' | 'List' - 'List' | 'List' - 'List[]>' | 'List[]>' + 'List' | 'List' + 'List[]>' | 'List[]>' 'List' | 'List' 'List>' | 'List>' } @@ -448,8 +448,7 @@ class Test { List placeholders = fieldType.getDeclaredGenericPlaceholders() expect: - // Native types should be Element if possible - placeholders.every { it.nativeType.class.simpleName == "TypeVariableSymbol" } + placeholders.every { it.genericNativeType.typeVariable.class.simpleName == "TypeVar" } reconstructTypeSignature(fieldType.foldBoundGenericTypes { if (it.isGenericPlaceholder() && ((GenericPlaceholderElement) it).variableName == 'T') { return ClassElement.of(String) @@ -526,7 +525,7 @@ class Test { wildcardType.boundGenericTypes.size() == 1 wildcardType.boundGenericTypes[0].isWildcard() - wildcardType.boundGenericTypes[0].getNativeType().class.name == 'com.sun.tools.javac.code.Type$WildcardType' + wildcardType.boundGenericTypes[0].genericNativeType.class.name == 'com.sun.tools.javac.code.Type$WildcardType' wildcardType.typeArguments["E"].type.name == "java.lang.Object" wildcardType.typeArguments["E"].isWildcard() !wildcardType.typeArguments["E"].isRawType() @@ -587,4 +586,102 @@ class Test { ((WildcardElement)numberType.typeArguments["E"]).isBounded() !numberType.typeArguments["E"].isRawType() } + + def 'distinguish base list type'() { + given: + def classElement = buildClassElement(""" +package example; + +import java.util.*; +import java.lang.Number; + +class Test extends Base { +} + +abstract class Base { + List field1; + List field2; + List field3; + List field4; +} + +""") + def rawType = classElement.fields[0].type + def wildcardType = classElement.fields[1].type + def objectType = classElement.fields[2].type + def genericType = classElement.fields[3].type + + expect: + rawType.typeArguments["E"].type.name == "java.lang.Object" + rawType.typeArguments["E"].isRawType() + !rawType.typeArguments["E"].isWildcard() + rawType.typeArguments["E"].isGenericPlaceholder() + + wildcardType.typeArguments["E"].type.name == "java.lang.Object" + wildcardType.typeArguments["E"].isWildcard() + !((WildcardElement)wildcardType.typeArguments["E"]).isBounded() + !wildcardType.typeArguments["E"].isRawType() + + objectType.typeArguments["E"].type.name == "java.lang.Object" + !objectType.typeArguments["E"].isWildcard() + !objectType.typeArguments["E"].isRawType() + !objectType.typeArguments["E"].isGenericPlaceholder() + + genericType.typeArguments["E"].type.name == "java.lang.Object" + !genericType.typeArguments["E"].isWildcard() + !genericType.typeArguments["E"].isRawType() + genericType.typeArguments["E"].isGenericPlaceholder() + (genericType.typeArguments["E"] as GenericPlaceholderElement).getResolved().isEmpty() + } + + def 'distinguish base list generic type'() { + given: + def classElement = buildClassElement(""" +package example; + +import java.util.*; +import java.lang.Number; + +class Test extends Base { +} + +abstract class Base { + List field1; + List field2; + List field3; + List field4; +} + +""") + def rawType = classElement.fields[0].genericType + def wildcardType = classElement.fields[1].genericType + def objectType = classElement.fields[2].genericType + def genericType = classElement.fields[3].genericType + + expect: + rawType.typeArguments["E"].type.name == "java.lang.Object" + rawType.typeArguments["E"].isRawType() + !rawType.typeArguments["E"].isWildcard() + rawType.typeArguments["E"].isGenericPlaceholder() + + wildcardType.typeArguments["E"].type.name == "java.lang.Object" + wildcardType.typeArguments["E"].isWildcard() + !((WildcardElement)wildcardType.typeArguments["E"]).isBounded() + !wildcardType.typeArguments["E"].isRawType() + + objectType.typeArguments["E"].type.name == "java.lang.Object" + !objectType.typeArguments["E"].isWildcard() + !objectType.typeArguments["E"].isRawType() + !objectType.typeArguments["E"].isGenericPlaceholder() + + genericType.typeArguments["E"].type.name == "java.lang.String" + !genericType.typeArguments["E"].isWildcard() + !genericType.typeArguments["E"].isRawType() + genericType.typeArguments["E"].isGenericPlaceholder() + def resolved = (genericType.typeArguments["E"] as GenericPlaceholderElement).getResolved().get() + resolved.name == "java.lang.String" + !resolved.isWildcard() + !resolved.isRawType() + !resolved.isGenericPlaceholder() + } } diff --git a/inject-java/src/test/groovy/io/micronaut/aop/named2/NamedAopAdviceSpec.groovy b/inject-java/src/test/groovy/io/micronaut/aop/named2/NamedAopAdviceSpec.groovy new file mode 100644 index 00000000000..305426f7d37 --- /dev/null +++ b/inject-java/src/test/groovy/io/micronaut/aop/named2/NamedAopAdviceSpec.groovy @@ -0,0 +1,103 @@ +package io.micronaut.aop.named2 + +import io.micronaut.annotation.processing.test.AbstractTypeElementSpec +import io.micronaut.aop.Intercepted +import io.micronaut.inject.qualifiers.Qualifiers + +class NamedAopAdviceSpec extends AbstractTypeElementSpec { + + void "test that named beans that have AOP advice applied lookup the correct target named bean - primary included"() { + given: + def context = buildContext('test.java', ''' +package test; + +import io.micronaut.context.annotation.ConfigurationProperties; +import io.micronaut.aop.Logged; +import io.micronaut.context.annotation.EachProperty; +import io.micronaut.context.annotation.Factory; +import io.micronaut.context.annotation.Parameter; +import io.micronaut.runtime.context.scope.Refreshable; +import jakarta.inject.Inject; +import jakarta.inject.Named; +import jakarta.inject.Singleton; + +interface OtherInterface { + String doStuff(); +} + +@Singleton +class OtherBean { + + @Inject @Named("first") public OtherInterface first; + @Inject @Named("second") public OtherInterface second; +} + + +interface NamedInterface { + String doStuff(); +} + +@ConfigurationProperties("config") +class Config { + + public Config(Inner inner) { + + } + + @ConfigurationProperties("inner") + public static class Inner { + } +} + + +@Factory +class NamedFactory { + + @EachProperty(value = "aop.test.named", primary = "default") + @Refreshable + NamedInterface namedInterface(@Parameter String name) { + return () -> name; + } + + + @Named("first") + @Logged + @Singleton + OtherInterface first() { + return () -> "first"; + } + + @Named("second") + @Logged + @Singleton + OtherInterface second() { + return () -> "second"; + } + + @EachProperty("other.interfaces") + OtherInterface third(Config config, @Parameter String name) { + return () -> name; + } +} + + + +''', false, ['aop.test.named.default': 0, + 'aop.test.named.one': 1, + 'aop.test.named.two': 2,]) + + def namedInterfaceClass = context.getClassLoader().loadClass('test.NamedInterface') + + expect: + context.getBean(namedInterfaceClass) instanceof Intercepted + context.getBean(namedInterfaceClass).doStuff() == 'default' + context.getBean(namedInterfaceClass, Qualifiers.byName("one")).doStuff() == 'one' + context.getBean(namedInterfaceClass, Qualifiers.byName("two")).doStuff() == 'two' + context.getBeansOfType(namedInterfaceClass).size() == 3 + context.getBeansOfType(namedInterfaceClass).every({ it instanceof Intercepted }) + + cleanup: + context.close() + } + +} diff --git a/inject-java/src/test/groovy/io/micronaut/inject/beans/BeanDefinitionSpec.groovy b/inject-java/src/test/groovy/io/micronaut/inject/beans/BeanDefinitionSpec.groovy index d9ecc8c735c..53277e265ce 100644 --- a/inject-java/src/test/groovy/io/micronaut/inject/beans/BeanDefinitionSpec.groovy +++ b/inject-java/src/test/groovy/io/micronaut/inject/beans/BeanDefinitionSpec.groovy @@ -3,6 +3,7 @@ package io.micronaut.inject.beans import io.micronaut.annotation.processing.test.AbstractTypeElementSpec import io.micronaut.core.annotation.AnnotationUtil import io.micronaut.core.annotation.Order +import io.micronaut.core.type.GenericPlaceholder import io.micronaut.inject.BeanDefinition import io.micronaut.inject.qualifiers.Qualifiers import spock.lang.Issue @@ -492,4 +493,83 @@ public class Test { param3.getAnnotationMetadata().getAnnotationNames().size() == 1 param3.getAnnotationMetadata().getAnnotationNames().asList() == ['javax.validation.constraints.NotNull$List'] } + + void "test isTypeVariable"() { + given: + BeanDefinition definition = buildBeanDefinition('test', 'Test', ''' +package test; +import javax.validation.constraints.*; +import java.util.List; + +@jakarta.inject.Singleton +public class Test implements Serde { +} + +interface Serde extends Serializer, Deserializer { +} + +interface Serializer { +} + +interface Deserializer { +} + + + ''') + + when: "Micronaut Serialization use-case" + def serdeTypeParam = definition.getTypeArguments("test.Serde")[0] + def serializerTypeParam = definition.getTypeArguments("test.Serializer")[0] + def deserializerTypeParam = definition.getTypeArguments("test.Deserializer")[0] + + then: "The first is a placeholder" + serdeTypeParam.isTypeVariable() // + (serdeTypeParam instanceof GenericPlaceholder) + and: "threat resolved placeholder as not a type variable" + serializerTypeParam.isTypeVariable() + (serializerTypeParam instanceof GenericPlaceholder) + deserializerTypeParam.isTypeVariable() + (deserializerTypeParam instanceof GenericPlaceholder) + } + + void "test isTypeVariable array"() { + given: + BeanDefinition definition = buildBeanDefinition('test', 'Test', ''' +package test; +import javax.validation.constraints.*; +import java.util.List; + +@jakarta.inject.Singleton +public class Test implements Serde { +} + +interface Serde extends Serializer, Deserializer { +} + +interface Serializer { +} + +interface Deserializer { +} + + + ''') + + when: "Micronaut Serialization use-case" + def serdeTypeParam = definition.getTypeArguments("test.Serde")[0] + def serializerTypeParam = definition.getTypeArguments("test.Serializer")[0] + def deserializerTypeParam = definition.getTypeArguments("test.Deserializer")[0] + // Arrays are not resolved as JavaClassElements or placeholders + then: "The first is a placeholder" + serdeTypeParam.simpleName == "String[]" + !serdeTypeParam.isTypeVariable() + !(serdeTypeParam instanceof GenericPlaceholder) + and: "threat resolved placeholder as not a type variable" + serializerTypeParam.simpleName == "String[]" + !serializerTypeParam.isTypeVariable() + !(serializerTypeParam instanceof GenericPlaceholder) + deserializerTypeParam.simpleName == "String[]" + !deserializerTypeParam.isTypeVariable() + !(deserializerTypeParam instanceof GenericPlaceholder) + } } diff --git a/inject-java/src/test/groovy/io/micronaut/inject/executable/ExecutableBeanSpec.groovy b/inject-java/src/test/groovy/io/micronaut/inject/executable/ExecutableBeanSpec.groovy index 3e48c263f37..d369ff98ce1 100644 --- a/inject-java/src/test/groovy/io/micronaut/inject/executable/ExecutableBeanSpec.groovy +++ b/inject-java/src/test/groovy/io/micronaut/inject/executable/ExecutableBeanSpec.groovy @@ -17,7 +17,10 @@ package io.micronaut.inject.executable import io.micronaut.annotation.processing.test.AbstractTypeElementSpec import io.micronaut.context.annotation.BeanProperties +import io.micronaut.context.annotation.Executable +import io.micronaut.core.annotation.AnnotationUtil import io.micronaut.core.annotation.Introspected +import io.micronaut.core.type.Argument import io.micronaut.inject.BeanDefinition import io.micronaut.inject.validation.RequiresValidation import spock.lang.Issue @@ -151,9 +154,9 @@ class MyBean { then: !saveAll.hasAnnotation(RequiresValidation) !saveAll.hasStereotype(RequiresValidation) - listTypeArgument.getAnnotationMetadata().hasAnnotation(MyEntity.class) - listTypeArgument.getAnnotationMetadata().hasAnnotation(Introspected.class) - listTypeArgument.getAnnotationMetadata().hasStereotype(Introspected.class) + !listTypeArgument.getAnnotationMetadata().hasAnnotation(MyEntity.class) + !listTypeArgument.getAnnotationMetadata().hasAnnotation(Introspected.class) + !listTypeArgument.getAnnotationMetadata().hasStereotype(Introspected.class) !listTypeArgument.getAnnotationMetadata().hasAnnotation(BeanProperties.class) !listTypeArgument.getAnnotationMetadata().hasStereotype(BeanProperties.class) @@ -163,9 +166,9 @@ class MyBean { then: !saveAll2.hasAnnotation(RequiresValidation) !saveAll2.hasStereotype(RequiresValidation) - listTypeArgument2.getAnnotationMetadata().hasAnnotation(MyEntity.class) - listTypeArgument2.getAnnotationMetadata().hasAnnotation(Introspected.class) - listTypeArgument2.getAnnotationMetadata().hasStereotype(Introspected.class) + !listTypeArgument2.getAnnotationMetadata().hasAnnotation(MyEntity.class) + !listTypeArgument2.getAnnotationMetadata().hasAnnotation(Introspected.class) + !listTypeArgument2.getAnnotationMetadata().hasStereotype(Introspected.class) !listTypeArgument2.getAnnotationMetadata().hasAnnotation(BeanProperties.class) !listTypeArgument2.getAnnotationMetadata().hasStereotype(BeanProperties.class) @@ -175,46 +178,305 @@ class MyBean { then: !saveAll3.hasAnnotation(RequiresValidation) !saveAll3.hasStereotype(RequiresValidation) - listTypeArgument3.getAnnotationMetadata().hasAnnotation(MyEntity.class) - listTypeArgument3.getAnnotationMetadata().hasAnnotation(Introspected.class) - listTypeArgument3.getAnnotationMetadata().hasStereotype(Introspected.class) + !listTypeArgument3.getAnnotationMetadata().hasAnnotation(MyEntity.class) + !listTypeArgument3.getAnnotationMetadata().hasAnnotation(Introspected.class) + !listTypeArgument3.getAnnotationMetadata().hasStereotype(Introspected.class) !listTypeArgument3.getAnnotationMetadata().hasAnnotation(BeanProperties.class) !listTypeArgument3.getAnnotationMetadata().hasStereotype(BeanProperties.class) -// TODO: validate this behaviour -// -// when: -// def save2 = definition.findMethod("save2", Book.class).get() -// def parameter2 = save2.getArguments()[0] -// then: -// !save2.hasAnnotation(RequiresValidation) -// !save2.hasStereotype(RequiresValidation) -// parameter2.getAnnotationMetadata().hasAnnotation(MyEntity.class) -// parameter2.getAnnotationMetadata().hasAnnotation(Introspected.class) -// parameter2.getAnnotationMetadata().hasStereotype(Introspected.class) -// !parameter2.getAnnotationMetadata().hasAnnotation(BeanProperties.class) -// !parameter2.getAnnotationMetadata().hasStereotype(BeanProperties.class) -// -// when: -// def save3 = definition.findMethod("save3", Book.class).get() -// def parameter3 = save3.getArguments()[0] -// then: -// !save3.hasAnnotation(RequiresValidation) -// !save3.hasStereotype(RequiresValidation) -// parameter3.getAnnotationMetadata().hasAnnotation(MyEntity.class) -// parameter3.getAnnotationMetadata().hasAnnotation(Introspected.class) -// parameter3.getAnnotationMetadata().hasStereotype(Introspected.class) -// !parameter3.getAnnotationMetadata().hasAnnotation(BeanProperties.class) -// !parameter3.getAnnotationMetadata().hasStereotype(BeanProperties.class) -// -// when: -// def get = definition.findMethod("get").get() -// def returnType = get.getReturnType() -// then: -// returnType.getAnnotationMetadata().hasAnnotation(MyEntity.class) -// returnType.getAnnotationMetadata().hasAnnotation(Introspected.class) -// returnType.getAnnotationMetadata().hasStereotype(Introspected.class) -// !returnType.getAnnotationMetadata().hasAnnotation(BeanProperties.class) -// !returnType.getAnnotationMetadata().hasStereotype(BeanProperties.class) + + when: + def save2 = definition.findMethod("save2", Book.class).get() + def parameter2 = save2.getArguments()[0] + then: + !save2.hasAnnotation(RequiresValidation) + !save2.hasStereotype(RequiresValidation) + !parameter2.getAnnotationMetadata().hasAnnotation(MyEntity.class) + !parameter2.getAnnotationMetadata().hasAnnotation(Introspected.class) + !parameter2.getAnnotationMetadata().hasStereotype(Introspected.class) + !parameter2.getAnnotationMetadata().hasAnnotation(BeanProperties.class) + !parameter2.getAnnotationMetadata().hasStereotype(BeanProperties.class) + + when: + def save3 = definition.findMethod("save3", Book.class).get() + def parameter3 = save3.getArguments()[0] + then: + !save3.hasAnnotation(RequiresValidation) + !save3.hasStereotype(RequiresValidation) + !parameter3.getAnnotationMetadata().hasAnnotation(MyEntity.class) + !parameter3.getAnnotationMetadata().hasAnnotation(Introspected.class) + !parameter3.getAnnotationMetadata().hasStereotype(Introspected.class) + !parameter3.getAnnotationMetadata().hasAnnotation(BeanProperties.class) + !parameter3.getAnnotationMetadata().hasStereotype(BeanProperties.class) + + when: + def get = definition.findMethod("get").get() + def returnType = get.getReturnType() + then: + !returnType.getAnnotationMetadata().hasAnnotation(MyEntity.class) + !returnType.getAnnotationMetadata().hasAnnotation(Introspected.class) + !returnType.getAnnotationMetadata().hasStereotype(Introspected.class) + !returnType.getAnnotationMetadata().hasAnnotation(BeanProperties.class) + !returnType.getAnnotationMetadata().hasStereotype(BeanProperties.class) + } + + void "test how type annotations are preserved 2"() { + given: + BeanDefinition definition = buildBeanDefinition('test.MyBean','''\ +package test; + +import io.micronaut.inject.annotation.*; +import io.micronaut.context.annotation.*; +import javax.validation.Valid; +import java.util.List; +import io.micronaut.inject.executable.Book; +import io.micronaut.inject.executable.TypeUseRuntimeAnn; + +@jakarta.inject.Singleton +class MyBean { + + @Executable + public void saveAll(@Valid List<@TypeUseRuntimeAnn Book> books) { + } + + @Executable + public <@TypeUseRuntimeAnn T extends Book> void saveAll2(@Valid List book) { + } + + @Executable + public <@TypeUseRuntimeAnn T extends Book> void saveAll3(@Valid List book) { + } + + @Executable + public void save2(@Valid @TypeUseRuntimeAnn Book book) { + } + + @Executable + public <@TypeUseRuntimeAnn T extends Book> void save3(@Valid T book) { + } + + @TypeUseRuntimeAnn + @Executable + public Book get() { + return null; + } +} + +''') + when: + def saveAll = definition.findMethod("saveAll", List.class).get() + def listTypeArgument = saveAll.getArguments()[0].getTypeParameters()[0] + then: + listTypeArgument.getAnnotationMetadata().hasAnnotation(TypeUseRuntimeAnn.class) + + !saveAll.hasAnnotation(RequiresValidation) + !saveAll.hasStereotype(RequiresValidation) + !listTypeArgument.getAnnotationMetadata().hasAnnotation(MyEntity.class) + !listTypeArgument.getAnnotationMetadata().hasAnnotation(Introspected.class) + !listTypeArgument.getAnnotationMetadata().hasStereotype(Introspected.class) + !listTypeArgument.getAnnotationMetadata().hasAnnotation(BeanProperties.class) + !listTypeArgument.getAnnotationMetadata().hasStereotype(BeanProperties.class) + + when: + def saveAll2 = definition.findMethod("saveAll2", List.class).get() + def listTypeArgument2 = saveAll2.getArguments()[0].getTypeParameters()[0] + then: + listTypeArgument2.getAnnotationMetadata().hasAnnotation(TypeUseRuntimeAnn.class) + + !saveAll2.hasAnnotation(RequiresValidation) + !saveAll2.hasStereotype(RequiresValidation) + !listTypeArgument2.getAnnotationMetadata().hasAnnotation(MyEntity.class) + !listTypeArgument2.getAnnotationMetadata().hasAnnotation(Introspected.class) + !listTypeArgument2.getAnnotationMetadata().hasStereotype(Introspected.class) + !listTypeArgument2.getAnnotationMetadata().hasAnnotation(BeanProperties.class) + !listTypeArgument2.getAnnotationMetadata().hasStereotype(BeanProperties.class) + + when: + def saveAll3 = definition.findMethod("saveAll3", List.class).get() + def listTypeArgument3 = saveAll3.getArguments()[0].getTypeParameters()[0] + then: + listTypeArgument3.getAnnotationMetadata().hasAnnotation(TypeUseRuntimeAnn.class) + + !saveAll3.hasAnnotation(RequiresValidation) + !saveAll3.hasStereotype(RequiresValidation) + !listTypeArgument3.getAnnotationMetadata().hasAnnotation(MyEntity.class) + !listTypeArgument3.getAnnotationMetadata().hasAnnotation(Introspected.class) + !listTypeArgument3.getAnnotationMetadata().hasStereotype(Introspected.class) + !listTypeArgument3.getAnnotationMetadata().hasAnnotation(BeanProperties.class) + !listTypeArgument3.getAnnotationMetadata().hasStereotype(BeanProperties.class) + + when: + def save2 = definition.findMethod("save2", Book.class).get() + def parameter2 = save2.getArguments()[0] + then: + parameter2.getAnnotationMetadata().hasAnnotation(TypeUseRuntimeAnn.class) + + !save2.hasAnnotation(RequiresValidation) + !save2.hasStereotype(RequiresValidation) + !parameter2.getAnnotationMetadata().hasAnnotation(MyEntity.class) + !parameter2.getAnnotationMetadata().hasAnnotation(Introspected.class) + !parameter2.getAnnotationMetadata().hasStereotype(Introspected.class) + !parameter2.getAnnotationMetadata().hasAnnotation(BeanProperties.class) + !parameter2.getAnnotationMetadata().hasStereotype(BeanProperties.class) + + when: + def save3 = definition.findMethod("save3", Book.class).get() + def parameter3 = save3.getArguments()[0] + then: + parameter3.getAnnotationMetadata().hasAnnotation(TypeUseRuntimeAnn.class) + + !save3.hasAnnotation(RequiresValidation) + !save3.hasStereotype(RequiresValidation) + !parameter3.getAnnotationMetadata().hasAnnotation(MyEntity.class) + !parameter3.getAnnotationMetadata().hasAnnotation(Introspected.class) + !parameter3.getAnnotationMetadata().hasStereotype(Introspected.class) + !parameter3.getAnnotationMetadata().hasAnnotation(BeanProperties.class) + !parameter3.getAnnotationMetadata().hasStereotype(BeanProperties.class) + + when: + def get = definition.findMethod("get").get() + def returnType = get.getReturnType() + then: + returnType.getAnnotationMetadata().hasAnnotation(TypeUseRuntimeAnn.class) + + !returnType.getAnnotationMetadata().hasAnnotation(MyEntity.class) + !returnType.getAnnotationMetadata().hasAnnotation(Introspected.class) + !returnType.getAnnotationMetadata().hasStereotype(Introspected.class) + !returnType.getAnnotationMetadata().hasAnnotation(BeanProperties.class) + !returnType.getAnnotationMetadata().hasStereotype(BeanProperties.class) + } + + void "test how the type annotations from the type are preserved 2"() { + given: + BeanDefinition bd = buildBeanDefinition('test.MyBean', '''\ +package test; + +import io.micronaut.inject.annotation.*; +import io.micronaut.context.annotation.*; +import java.util.List; +import io.micronaut.inject.executable.Book; +import io.micronaut.inject.executable.TypeUseRuntimeAnn; + +@jakarta.inject.Singleton +class MyBean { + + @Executable + public void saveAll(List<@TypeUseRuntimeAnn Book> books) { + } + + @Executable + public <@TypeUseRuntimeAnn T extends Book> void saveAll2(List book) { + } + + @Executable + public <@TypeUseRuntimeAnn T extends Book> void saveAll3(List book) { + } + + @Executable + public void saveAll4(List<@TypeUseRuntimeAnn ? extends T> book) { + } + + @Executable + public void saveAll5(List book) { + } + + @Executable + public void save2(@TypeUseRuntimeAnn Book book) { + } + + @Executable + public <@TypeUseRuntimeAnn T extends Book> void save3(T book) { + } + + @Executable + public void save4(T book) { + } + + @Executable + public void save5(@TypeUseRuntimeAnn T book) { + } + + @TypeUseRuntimeAnn + @Executable + public Book get() { + return null; + } +} + +''') + when: + def saveAll = bd.findMethod("saveAll", List).get() + def listTypeArgument = saveAll.getArguments()[0].getTypeParameters()[0] + then: + validateBookArgument(listTypeArgument) + + when: + def saveAll2 = bd.findMethod("saveAll2", List).get() + def listTypeArgument2 = saveAll2.getArguments()[0].getTypeParameters()[0] + then: + validateBookArgument(listTypeArgument2) + + when: + def saveAll3 = bd.findMethod("saveAll3", List).get() + def listTypeArgument3 = saveAll3.getArguments()[0].getTypeParameters()[0] + then: + validateBookArgument(listTypeArgument3) + + when: + def saveAll4 = bd.findMethod("saveAll4", List).get() + def listTypeArgument4 = saveAll4.getArguments()[0].getTypeParameters()[0] + then: + validateBookArgument(listTypeArgument4) + + when: + def saveAll5 = bd.findMethod("saveAll5", List).get() + def listTypeArgument5 = saveAll5.getArguments()[0].getTypeParameters()[0] + then: + validateBookArgument(listTypeArgument5) + + when: + def save2 = bd.findMethod("save2", Book).get() + def parameter2 = save2.getArguments()[0] + then: + validateBookArgument(parameter2) + + when: + def save3 = bd.findMethod("save3", Book).get() + def parameter3 = save3.getArguments()[0] + then: + validateBookArgument(parameter3) + + when: + def save4 = bd.findMethod("save4", Book).get() + def parameter4 = save4.getArguments()[0] + then: + validateBookArgument(parameter4) + + when: + def save5 = bd.findMethod("save5", Book).get() + def parameter5 = save5.getArguments()[0] + then: + validateBookArgument(parameter5) + + when: + def get = bd.findMethod("get").get() + def returnType = get.getReturnType().asArgument() + then: + def am = returnType.getAnnotationMetadata(); + assert am.hasAnnotation(TypeUseRuntimeAnn.class) + assert !am.hasAnnotation(MyEntity.class) + assert !am.hasAnnotation(Introspected.class) + // + Class annotations + assert am.hasStereotype(AnnotationUtil.SINGLETON) + assert am.hasStereotype(Executable) + } + + void validateBookArgument(Argument argument) { + // The argument should only have type annotations + def am = argument.getAnnotationMetadata(); + assert am.hasAnnotation(TypeUseRuntimeAnn.class) + assert !am.hasAnnotation(MyEntity.class) + assert !am.hasAnnotation(Introspected.class) + assert am.getAnnotationNames().size() == 1 } void "test multiple executable annotations on a method"() { diff --git a/inject-java/src/test/groovy/io/micronaut/inject/executable/MyBean.java b/inject-java/src/test/groovy/io/micronaut/inject/executable/MyBean.java new file mode 100644 index 00000000000..94f26fcad38 --- /dev/null +++ b/inject-java/src/test/groovy/io/micronaut/inject/executable/MyBean.java @@ -0,0 +1,35 @@ +package io.micronaut.inject.executable; + +import io.micronaut.context.annotation.Executable; + +import javax.validation.Valid; +import java.util.List; + +@jakarta.inject.Singleton +class MyBean { + + @Executable + public void saveAll(@Valid List<@TypeUseRuntimeAnn Book> books) { + } + + @Executable + public void saveAll2(@Valid List book) { + } + + @Executable + public void saveAll3(@Valid List book) { + } + + @Executable + public void save2(@Valid io.micronaut.inject.executable.Book book) { + } + + @Executable + public void save3(@Valid T book) { + } + + @Executable + public io.micronaut.inject.executable.Book get() { + return null; + } +} diff --git a/inject-java/src/test/groovy/io/micronaut/inject/executable/TypeUseRuntimeAnn.java b/inject-java/src/test/groovy/io/micronaut/inject/executable/TypeUseRuntimeAnn.java new file mode 100644 index 00000000000..78ed6327baa --- /dev/null +++ b/inject-java/src/test/groovy/io/micronaut/inject/executable/TypeUseRuntimeAnn.java @@ -0,0 +1,11 @@ +package io.micronaut.inject.executable; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target({ElementType.TYPE_USE}) +@Retention(RetentionPolicy.RUNTIME) +public @interface TypeUseRuntimeAnn { +} diff --git a/inject-java/src/test/groovy/io/micronaut/visitors/AllElementsVisitor.java b/inject-java/src/test/groovy/io/micronaut/visitors/AllElementsVisitor.java index 4c4f167535d..b9271e46ce2 100644 --- a/inject-java/src/test/groovy/io/micronaut/visitors/AllElementsVisitor.java +++ b/inject-java/src/test/groovy/io/micronaut/visitors/AllElementsVisitor.java @@ -44,7 +44,8 @@ public void start(VisitorContext visitorContext) { @Override public void visitClass(ClassElement element, VisitorContext context) { visit(element); - element.getBeanProperties(); // Preload properties for tests otherwise it fails because the compiler is done + // Preload annotations and elements for tests otherwise it fails because the compiler is done + element.getBeanProperties().forEach(this::initialize); element.getAnnotationMetadata(); element.getSuperType().ifPresent(superType -> { superType.getAllTypeArguments(); @@ -75,9 +76,9 @@ public void visitMethod(MethodElement element, VisitorContext context) { } private void initialize(TypedElement typedElement) { - typedElement.getAnnotationMetadata(); - typedElement.getType().getAnnotationMetadata(); - typedElement.getGenericType().getAnnotationMetadata(); + typedElement.getAnnotationMetadata().getAnnotationNames(); + typedElement.getType().getAnnotationMetadata().getAnnotationNames(); + typedElement.getGenericType().getAnnotationMetadata().getAnnotationNames(); } @Override diff --git a/inject-java/src/test/groovy/io/micronaut/visitors/ClassElementSpec.groovy b/inject-java/src/test/groovy/io/micronaut/visitors/ClassElementSpec.groovy index 2ff266996f7..61fa2b68b18 100644 --- a/inject-java/src/test/groovy/io/micronaut/visitors/ClassElementSpec.groovy +++ b/inject-java/src/test/groovy/io/micronaut/visitors/ClassElementSpec.groovy @@ -27,11 +27,13 @@ import io.micronaut.inject.ast.ElementModifier import io.micronaut.inject.ast.ElementQuery import io.micronaut.inject.ast.EnumElement import io.micronaut.inject.ast.FieldElement +import io.micronaut.inject.ast.GenericPlaceholderElement import io.micronaut.inject.ast.MemberElement import io.micronaut.inject.ast.MethodElement import io.micronaut.inject.ast.PackageElement import io.micronaut.inject.ast.PrimitiveElement import io.micronaut.inject.ast.PropertyElement +import io.micronaut.inject.ast.WildcardElement import jakarta.inject.Singleton import spock.lang.IgnoreIf import spock.lang.Issue @@ -43,6 +45,7 @@ import java.sql.SQLException import java.util.function.Supplier class ClassElementSpec extends AbstractTypeElementSpec { + void "test class element generics"() { given: ClassElement classElement = buildClassElement(''' @@ -1760,6 +1763,68 @@ class Test { level3.getAnnotationMetadata().getAnnotationNames().asList() == ['javax.validation.constraints.NotNull$List'] } + void "test type annotations on a method and a field"() { + ClassElement ce = buildClassElement(''' +package test; + +class Test { + @io.micronaut.visitors.TypeUseRuntimeAnn + @io.micronaut.visitors.TypeUseClassAnn + String myField; + + @io.micronaut.visitors.TypeUseRuntimeAnn + @io.micronaut.visitors.TypeUseClassAnn + String myMethod() { + return null; + } +} +''') + expect: + def field = ce.findField("myField").get() + def method = ce.findMethod("myMethod").get() + + // Type annotations shouldn't appear on the field + field.getAnnotationMetadata().getAnnotationNames().asList() == [] + field.getType().getAnnotationMetadata().getAnnotationNames().asList() == ['io.micronaut.visitors.TypeUseRuntimeAnn', 'io.micronaut.visitors.TypeUseClassAnn' ] + field.getGenericType().getAnnotationMetadata().getAnnotationNames().asList() == ['io.micronaut.visitors.TypeUseRuntimeAnn', 'io.micronaut.visitors.TypeUseClassAnn' ] + // Type annotations shouldn't appear on the method + method.getAnnotationMetadata().getAnnotationNames().asList() == [] + method.getReturnType().getAnnotationMetadata().getAnnotationNames().asList() == ['io.micronaut.visitors.TypeUseRuntimeAnn', 'io.micronaut.visitors.TypeUseClassAnn' ] + method.getGenericReturnType().getAnnotationMetadata().getAnnotationNames().asList() == ['io.micronaut.visitors.TypeUseRuntimeAnn', 'io.micronaut.visitors.TypeUseClassAnn' ] + } + + void "test type annotations on a method and a field 2"() { + ClassElement ce = buildClassElement(''' +package test; + +class Test { + @io.micronaut.visitors.TypeFieldRuntimeAnn + @io.micronaut.visitors.TypeUseRuntimeAnn + @io.micronaut.visitors.TypeUseClassAnn + String myField; + + @io.micronaut.visitors.TypeMethodRuntimeAnn + @io.micronaut.visitors.TypeUseRuntimeAnn + @io.micronaut.visitors.TypeUseClassAnn + String myMethod() { + return null; + } +} +''') + expect: + def field = ce.findField("myField").get() + def method = ce.findMethod("myMethod").get() + + // Type annotations shouldn't appear on the field + field.getAnnotationMetadata().getAnnotationNames().asList() == ['io.micronaut.visitors.TypeFieldRuntimeAnn'] + field.getType().getAnnotationMetadata().getAnnotationNames().asList() == ['io.micronaut.visitors.TypeUseRuntimeAnn', 'io.micronaut.visitors.TypeUseClassAnn' ] + field.getGenericType().getAnnotationMetadata().getAnnotationNames().asList() == ['io.micronaut.visitors.TypeUseRuntimeAnn', 'io.micronaut.visitors.TypeUseClassAnn' ] + // Type annotations shouldn't appear on the method + method.getAnnotationMetadata().getAnnotationNames().asList() == ['io.micronaut.visitors.TypeMethodRuntimeAnn'] + method.getReturnType().getAnnotationMetadata().getAnnotationNames().asList() == ['io.micronaut.visitors.TypeUseRuntimeAnn', 'io.micronaut.visitors.TypeUseClassAnn' ] + method.getGenericReturnType().getAnnotationMetadata().getAnnotationNames().asList() == ['io.micronaut.visitors.TypeUseRuntimeAnn', 'io.micronaut.visitors.TypeUseClassAnn' ] + } + void "test recursive generic type parameter"() { given: ClassElement ce = buildClassElement('''\ @@ -1782,6 +1847,56 @@ final class TrackedSortedSet> { nextNextTypeArgument.name == "java.lang.Object" } + void "test annotation metadata present on deep type parameters for method 2"() { + ClassElement ce = buildClassElement(''' +package test; +import io.micronaut.core.annotation.*; +import javax.validation.constraints.*; +import java.util.List; + +class Test { + List>> deepList() { + return null; + } +} +''') + expect: + def method = ce.getEnclosedElement(ElementQuery.ALL_METHODS.named("deepList")).get() + def theType = method.getGenericReturnType() + + theType.getAnnotationMetadata().getAnnotationNames().size() == 0 + + assertListGenericArgument(theType, { ClassElement listArg1 -> + assertListGenericArgument(listArg1, { ClassElement listArg2 -> + assertListGenericArgument(listArg2, { ClassElement listArg3 -> + assert listArg3.getAnnotationMetadata().getAnnotationNames().asList() == ['io.micronaut.visitors.TypeUseRuntimeAnn', 'io.micronaut.visitors.TypeUseClassAnn'] + }) + }) + }) + + def level1 = theType.getTypeArguments()["E"] + def level2 = level1.getTypeArguments()["E"] + def level3 = level2.getTypeArguments()["E"] + level3.getAnnotationMetadata().getAnnotationNames().asList() == ['io.micronaut.visitors.TypeUseRuntimeAnn', 'io.micronaut.visitors.TypeUseClassAnn' ] + } + + void "test annotations on recursive generic type parameter 1"() { + given: + ClassElement ce = buildClassElement('''\ +package test; + +final class TrackedSortedSet<@io.micronaut.visitors.TypeUseRuntimeAnn T extends java.lang.Comparable> { +} + +''') + expect: + def typeArguments = ce.getTypeArguments() + typeArguments.size() == 1 + def typeArgument = typeArguments.get("T") + typeArgument.name == "java.lang.Comparable" + typeArgument.getAnnotationNames().asList() == ['io.micronaut.visitors.TypeUseRuntimeAnn'] + } + void "test recursive generic type parameter 2"() { given: ClassElement ce = buildClassElement('''\ @@ -2187,6 +2302,548 @@ class MyBean { returnType.hasAnnotation(Introspected.class) } + void "test how the type annotations from the type are propagated"() { + given: + ClassElement ce = buildClassElement('''\ +package test; + +import io.micronaut.inject.annotation.*; +import io.micronaut.context.annotation.*; +import java.util.List; +import io.micronaut.visitors.Book; +import io.micronaut.visitors.TypeUseRuntimeAnn; + +@jakarta.inject.Singleton +class MyBean { + + @Executable + public void saveAll(List<@TypeUseRuntimeAnn Book> books) { + } + + @Executable + public <@TypeUseRuntimeAnn T extends Book> void saveAll2(List book) { + } + + @Executable + public <@TypeUseRuntimeAnn T extends Book> void saveAll3(List book) { + } + + @Executable + public void saveAll4(List<@TypeUseRuntimeAnn ? extends T> book) { + } + + @Executable + public void saveAll5(List book) { + } + + @Executable + public void save2(@TypeUseRuntimeAnn Book book) { + } + + @Executable + public <@TypeUseRuntimeAnn T extends Book> void save3(T book) { + } + + @Executable + public void save4(T book) { + } + + @Executable + public void save5(@TypeUseRuntimeAnn T book) { + } + + @TypeUseRuntimeAnn + @Executable + public Book get() { + return null; + } +} + +''') + when: + def saveAll = ce.findMethod("saveAll").get() + def listTypeArgument = saveAll.getParameters()[0].getType().getTypeArguments(List).get("E") + then: + validateBookArgument(listTypeArgument) + + when: + def saveAll2 = ce.findMethod("saveAll2").get() + def listTypeArgument2 = saveAll2.getParameters()[0].getType().getTypeArguments(List).get("E") + then: + validateBookArgument(listTypeArgument2) + + when: + def saveAll3 = ce.findMethod("saveAll3").get() + def listTypeArgument3 = saveAll3.getParameters()[0].getType().getTypeArguments(List).get("E") + then: + validateBookArgument(listTypeArgument3) + + when: + def saveAll4 = ce.findMethod("saveAll4").get() + def listTypeArgument4 = saveAll4.getParameters()[0].getType().getTypeArguments(List).get("E") + then: + validateBookArgument(listTypeArgument4) + + when: + def saveAll5 = ce.findMethod("saveAll5").get() + def listTypeArgument5 = saveAll5.getParameters()[0].getType().getTypeArguments(List).get("E") + then: + validateBookArgument(listTypeArgument5) + + when: + def save2 = ce.findMethod("save2").get() + def parameter2 = save2.getParameters()[0].getType() + then: + validateBookArgument(parameter2) + + when: + def save3 = ce.findMethod("save3").get() + def parameter3 = save3.getParameters()[0].getType() + then: + validateBookArgument(parameter3) + + when: + def save4 = ce.findMethod("save4").get() + def parameter4 = save4.getParameters()[0].getType() + then: + validateBookArgument(parameter4) + + when: + def save5 = ce.findMethod("save5").get() + def parameter5 = save5.getParameters()[0].getType() + then: + validateBookArgument(parameter5) + + when: + def get = ce.findMethod("get").get() + def returnType = get.getReturnType() + then: + validateBookArgument(returnType) + } + + void validateBookArgument(ClassElement classElement) { + // The class element should have all the annotations present + assert classElement.hasAnnotation(TypeUseRuntimeAnn.class) + assert classElement.hasAnnotation(MyEntity.class) + assert classElement.hasAnnotation(Introspected.class) + + def typeAnnotationMetadata = classElement.getTypeAnnotationMetadata() + // The type annotations should have only type annotations + assert typeAnnotationMetadata.hasAnnotation(TypeUseRuntimeAnn.class) + assert !typeAnnotationMetadata.hasAnnotation(MyEntity.class) + assert !typeAnnotationMetadata.hasAnnotation(Introspected.class) + + // Get the actual type -> the type shouldn't have any type annotations + def type = classElement.getType() + assert !type.hasAnnotation(TypeUseRuntimeAnn.class) + assert type.hasAnnotation(MyEntity.class) + assert type.hasAnnotation(Introspected.class) + assert type.getTypeAnnotationMetadata().isEmpty() + } + + void "test generics model"() { + ClassElement ce = buildClassElement(''' +package test; +import java.util.List; + +class Test { + List>> method1() { + return null; + } +} +''') + expect: + def method1 = ce.getEnclosedElement(ElementQuery.ALL_METHODS.named("method1")).get() + def genericType = method1.getGenericReturnType() + def genericTypeLevel1 = genericType.getTypeArguments()["E"] + !genericTypeLevel1.isGenericPlaceholder() + !genericTypeLevel1.isWildcard() + def genericTypeLevel2 = genericTypeLevel1.getTypeArguments()["E"] + !genericTypeLevel2.isGenericPlaceholder() + !genericTypeLevel2.isWildcard() + def genericTypeLevel3 = genericTypeLevel2.getTypeArguments()["E"] + !genericTypeLevel3.isGenericPlaceholder() + !genericTypeLevel3.isWildcard() + + def type = method1.getReturnType() + def typeLevel1 = type.getTypeArguments()["E"] + !typeLevel1.isGenericPlaceholder() + !typeLevel1.isWildcard() + def typeLevel2 = typeLevel1.getTypeArguments()["E"] + !typeLevel2.isGenericPlaceholder() + !typeLevel2.isWildcard() + def typeLevel3 = typeLevel2.getTypeArguments()["E"] + !typeLevel3.isGenericPlaceholder() + !typeLevel3.isWildcard() + } + + void "test generics model for wildcard"() { + ClassElement ce = buildClassElement(''' +package test; +import java.util.List; + +class Test { + + List method() { + return null; + } +} +''') + expect: + def method = ce.getEnclosedElement(ElementQuery.ALL_METHODS.named("method")).get() + def genericTypeArgument = method.getGenericReturnType().getTypeArguments()["E"] + !genericTypeArgument.isGenericPlaceholder() + !genericTypeArgument.isRawType() + genericTypeArgument.isWildcard() + + def typeArgument = method.getReturnType().getTypeArguments()["E"] + !typeArgument.isGenericPlaceholder() + !typeArgument.isRawType() + typeArgument.isWildcard() + } + + void "test generics model for placeholder"() { + ClassElement ce = buildClassElement(''' +package test; +import java.util.List; + +class Test { + + List method() { + return null; + } +} +''') + expect: + def method = ce.getEnclosedElement(ElementQuery.ALL_METHODS.named("method")).get() + def genericTypeArgument = method.getGenericReturnType().getTypeArguments()["E"] + genericTypeArgument.isGenericPlaceholder() + !genericTypeArgument.isRawType() + !genericTypeArgument.isWildcard() + + def typeArgument = method.getReturnType().getTypeArguments()["E"] + typeArgument.isGenericPlaceholder() + !typeArgument.isRawType() + !typeArgument.isWildcard() + } + + void "test generics model for class placeholder wildcard"() { + ClassElement ce = buildClassElement(''' +package test; +import java.util.List; + +class Test { + + List method() { + return null; + } +} +''') + expect: + def method = ce.getEnclosedElement(ElementQuery.ALL_METHODS.named("method")).get() + def genericTypeArgument = method.getGenericReturnType().getTypeArguments()["E"] + !genericTypeArgument.isGenericPlaceholder() + !genericTypeArgument.isRawType() + genericTypeArgument.isWildcard() + + def genericWildcard = genericTypeArgument as WildcardElement + !genericWildcard.lowerBounds + genericWildcard.upperBounds.size() == 1 + def genericUpperBound = genericWildcard.upperBounds[0] + genericUpperBound.name == "java.lang.Object" + genericUpperBound.isGenericPlaceholder() + !genericUpperBound.isWildcard() + !genericUpperBound.isRawType() + def genericPlaceholderUpperBound = genericUpperBound as GenericPlaceholderElement + genericPlaceholderUpperBound.variableName == "T" + genericPlaceholderUpperBound.declaringElement.get() == ce + + def typeArgument = method.getReturnType().getTypeArguments()["E"] + !typeArgument.isGenericPlaceholder() + !typeArgument.isRawType() + typeArgument.isWildcard() + + def wildcard = genericTypeArgument as WildcardElement + !wildcard.lowerBounds + wildcard.upperBounds.size() == 1 + def upperBound = wildcard.upperBounds[0] + upperBound.name == "java.lang.Object" + upperBound.isGenericPlaceholder() + !upperBound.isWildcard() + !upperBound.isRawType() + def placeholderUpperBound = upperBound as GenericPlaceholderElement + placeholderUpperBound.variableName == "T" + placeholderUpperBound.declaringElement.get() == ce + } + + void "test generics model for method placeholder wildcard"() { + ClassElement ce = buildClassElement(''' +package test; +import java.util.List; + +class Test { + + List method() { + return null; + } +} +''') + expect: + def method = ce.getEnclosedElement(ElementQuery.ALL_METHODS.named("method")).get() + method.getDeclaredTypeVariables().size() == 1 + method.getDeclaredTypeVariables()[0].declaringElement.get() == method + method.getDeclaredTypeVariables()[0].variableName == "T" + method.getDeclaredTypeArguments().size() == 1 + def placeholder = method.getDeclaredTypeArguments()["T"] as GenericPlaceholderElement + placeholder.declaringElement.get() == method + placeholder.variableName == "T" + def genericTypeArgument = method.getGenericReturnType().getTypeArguments()["E"] + !genericTypeArgument.isGenericPlaceholder() + !genericTypeArgument.isRawType() + genericTypeArgument.isWildcard() + + def genericWildcard = genericTypeArgument as WildcardElement + !genericWildcard.lowerBounds + genericWildcard.upperBounds.size() == 1 + def genericUpperBound = genericWildcard.upperBounds[0] + genericUpperBound.name == "java.lang.Object" + genericUpperBound.isGenericPlaceholder() + !genericUpperBound.isWildcard() + !genericUpperBound.isRawType() + def genericPlaceholderUpperBound = genericUpperBound as GenericPlaceholderElement + genericPlaceholderUpperBound.variableName == "T" + genericPlaceholderUpperBound.declaringElement.get() == method + + def typeArgument = method.getReturnType().getTypeArguments()["E"] + !typeArgument.isGenericPlaceholder() + !typeArgument.isRawType() + typeArgument.isWildcard() + + def wildcard = genericTypeArgument as WildcardElement + !wildcard.lowerBounds + wildcard.upperBounds.size() == 1 + def upperBound = wildcard.upperBounds[0] + upperBound.name == "java.lang.Object" + upperBound.isGenericPlaceholder() + !upperBound.isWildcard() + !upperBound.isRawType() + def placeholderUpperBound = upperBound as GenericPlaceholderElement + placeholderUpperBound.variableName == "T" + placeholderUpperBound.declaringElement.get() == method + } + + void "test generics model for constructor placeholder wildcard"() { + ClassElement ce = buildClassElement(''' +package test; +import java.util.List; + +class Test { + + Test(List list) { + } +} +''') + expect: + def method = ce.getPrimaryConstructor().get() + method.getDeclaredTypeVariables().size() == 1 + method.getDeclaredTypeVariables()[0].declaringElement.get() == method + method.getDeclaredTypeVariables()[0].variableName == "T" + method.getDeclaredTypeArguments().size() == 1 + def placeholder = method.getDeclaredTypeArguments()["T"] as GenericPlaceholderElement + placeholder.declaringElement.get() == method + placeholder.variableName == "T" + def genericTypeArgument = method.getParameters()[0].getGenericType().getTypeArguments()["E"] + !genericTypeArgument.isGenericPlaceholder() + !genericTypeArgument.isRawType() + genericTypeArgument.isWildcard() + + def genericWildcard = genericTypeArgument as WildcardElement + !genericWildcard.lowerBounds + genericWildcard.upperBounds.size() == 1 + def genericUpperBound = genericWildcard.upperBounds[0] + genericUpperBound.name == "java.lang.Object" + genericUpperBound.isGenericPlaceholder() + !genericUpperBound.isWildcard() + !genericUpperBound.isRawType() + def genericPlaceholderUpperBound = genericUpperBound as GenericPlaceholderElement + genericPlaceholderUpperBound.variableName == "T" + genericPlaceholderUpperBound.declaringElement.get() == method + + def typeArgument = method.getParameters()[0].getType().getTypeArguments()["E"] + !typeArgument.isGenericPlaceholder() + !typeArgument.isRawType() + typeArgument.isWildcard() + + def wildcard = genericTypeArgument as WildcardElement + !wildcard.lowerBounds + wildcard.upperBounds.size() == 1 + def upperBound = wildcard.upperBounds[0] + upperBound.name == "java.lang.Object" + upperBound.isGenericPlaceholder() + !upperBound.isWildcard() + !upperBound.isRawType() + def placeholderUpperBound = upperBound as GenericPlaceholderElement + placeholderUpperBound.variableName == "T" + placeholderUpperBound.declaringElement.get() == method + } + + void "test generics equality"() { + ClassElement ce = buildClassElement(''' +package test; +import java.util.List; + +class Test { + + Number number; + + Test(List list) { + } + + List method1() { + return null; + } + + List method2() { + return null; + } + + T method3() { + return null; + } + + List> method4() { + return null; + } + + List> method5() { + return null; + } + + Test method6() { + return null; + } + + Test method7() { + return null; + } + + Test method8() { + return null; + } + + Test method9() { + return null; + } + + Test method10() { + return null; + } +} +''') + expect: + def numberType = ce.getFields()[0].getType() + def constructor = ce.getPrimaryConstructor().get() + constructor.getParameters()[0].getGenericType().getTypeArguments(List).get("E") == numberType + constructor.getParameters()[0].getType().getTypeArguments(List).get("E") == numberType + + ce.findMethod("method1").get().getGenericReturnType().getTypeArguments(List).get("E") == numberType + ce.findMethod("method1").get().getReturnType().getTypeArguments(List).get("E") == numberType + + ce.findMethod("method2").get().getGenericReturnType().getTypeArguments(List).get("E") == numberType + ce.findMethod("method2").get().getReturnType().getTypeArguments(List).get("E") == numberType + + ce.findMethod("method3").get().getGenericReturnType() == numberType + ce.findMethod("method3").get().getReturnType() == numberType + + ce.findMethod("method4").get().getGenericReturnType().getTypeArguments(List).get("E").getTypeArguments(List).get("E") == numberType + ce.findMethod("method4").get().getReturnType().getTypeArguments(List).get("E").getTypeArguments(List).get("E") == numberType + + ce.findMethod("method5").get().getGenericReturnType().getTypeArguments(List).get("E").getTypeArguments(List).get("E") == numberType + ce.findMethod("method5").get().getReturnType().getTypeArguments(List).get("E").getTypeArguments(List).get("E") == numberType + + ce.findMethod("method6").get().getGenericReturnType().getTypeArguments("test.Test").get("T") == numberType + ce.findMethod("method6").get().getReturnType().getTypeArguments("test.Test").get("T") == numberType + + ce.findMethod("method7").get().getGenericReturnType().getTypeArguments("test.Test").get("T") == numberType + ce.findMethod("method7").get().getReturnType().getTypeArguments("test.Test").get("T") == numberType + + ce.findMethod("method8").get().getGenericReturnType().getTypeArguments("test.Test").get("T") == numberType + ce.findMethod("method8").get().getReturnType().getTypeArguments("test.Test").get("T") == numberType + + ce.findMethod("method9").get().getGenericReturnType().getTypeArguments("test.Test").get("T") == numberType + ce.findMethod("method9").get().getReturnType().getTypeArguments("test.Test").get("T") == numberType + + ce.findMethod("method10").get().getGenericReturnType().getTypeArguments("test.Test").get("T") == numberType + ce.findMethod("method10").get().getReturnType().getTypeArguments("test.Test").get("T") == numberType + } + + void "test inherit parameter annotation"() { + ClassElement ce = buildClassElement(''' +package test; +import java.util.List; + +interface MyApi { + + String get(@io.micronaut.visitors.MyParameter("X-username") String username); +} + +class UserController implements MyApi { + + @Override + public String get(String username) { + return null; + } + +} + +''') + expect: + ce.findMethod("get").get().getParameters()[0].hasAnnotation(MyParameter) + } + + void "test interface placeholder"() { + ClassElement ce = buildClassElement(''' +package test; +import java.util.List; + +class MyRepo implements Repo { +} + +interface Repo extends GenericRepository { +} + +interface GenericRepository { +} + + +class MyBean { + private String name; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } +} + +''') + + when: + def repo = ce.getTypeArguments("test.Repo") + then: + repo.get("E").simpleName == "MyBean" + repo.get("E").getMethods().size() == 2 + repo.get("E").getFields().size() == 1 + when: + def genRepo = ce.getTypeArguments("test.GenericRepository") + then: + genRepo.get("E").simpleName == "MyBean" + genRepo.get("E").getMethods().size() == 2 + genRepo.get("E").getFields().size() == 1 + } + private void assertListGenericArgument(ClassElement type, Closure cl) { def arg1 = type.getAllTypeArguments().get(List.class.name).get("E") def arg2 = type.getAllTypeArguments().get(Collection.class.name).get("E") diff --git a/inject-java/src/test/groovy/io/micronaut/visitors/MyBean.java b/inject-java/src/test/groovy/io/micronaut/visitors/MyBean.java new file mode 100644 index 00000000000..23fc57f81c2 --- /dev/null +++ b/inject-java/src/test/groovy/io/micronaut/visitors/MyBean.java @@ -0,0 +1,39 @@ +package io.micronaut.visitors; + +import io.micronaut.context.annotation.Executable; + +import java.util.List; + +@jakarta.inject.Singleton +class MyBean { + + @Executable + public void saveAll(List<@TypeUseRuntimeAnn Book> books) { + } + + @Executable + public <@TypeUseRuntimeAnn T extends Book> void saveAll2(List book) { + } + + @Executable + public <@TypeUseRuntimeAnn T extends Book> void saveAll3(List book) { + } + + @Executable + public void saveAll4(List book) { + } + + @Executable + public void save2(@TypeUseRuntimeAnn Book book) { + } + + @Executable + public <@TypeUseRuntimeAnn T extends Book> void save3(T book) { + } + + @TypeUseRuntimeAnn + @Executable + public Book get() { + return null; + } +} diff --git a/inject-java/src/test/groovy/io/micronaut/visitors/MyParameter.java b/inject-java/src/test/groovy/io/micronaut/visitors/MyParameter.java new file mode 100644 index 00000000000..22e4ddd26d1 --- /dev/null +++ b/inject-java/src/test/groovy/io/micronaut/visitors/MyParameter.java @@ -0,0 +1,20 @@ +package io.micronaut.visitors; + +import jakarta.inject.Qualifier; + +import java.lang.annotation.Documented; +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; + +@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.ANNOTATION_TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Qualifier +@Inherited +public @interface MyParameter { + + String value() default ""; +} diff --git a/inject-java/src/test/groovy/io/micronaut/visitors/TypeFieldRuntimeAnn.java b/inject-java/src/test/groovy/io/micronaut/visitors/TypeFieldRuntimeAnn.java new file mode 100644 index 00000000000..f15fce79ac8 --- /dev/null +++ b/inject-java/src/test/groovy/io/micronaut/visitors/TypeFieldRuntimeAnn.java @@ -0,0 +1,11 @@ +package io.micronaut.visitors; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.FIELD) +@Retention(RetentionPolicy.RUNTIME) +public @interface TypeFieldRuntimeAnn { +} diff --git a/inject-java/src/test/groovy/io/micronaut/visitors/TypeMethodRuntimeAnn.java b/inject-java/src/test/groovy/io/micronaut/visitors/TypeMethodRuntimeAnn.java new file mode 100644 index 00000000000..547fc129c13 --- /dev/null +++ b/inject-java/src/test/groovy/io/micronaut/visitors/TypeMethodRuntimeAnn.java @@ -0,0 +1,11 @@ +package io.micronaut.visitors; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface TypeMethodRuntimeAnn { +} diff --git a/inject-java/src/test/groovy/io/micronaut/visitors/TypeParameterRuntimeAnn.java b/inject-java/src/test/groovy/io/micronaut/visitors/TypeParameterRuntimeAnn.java new file mode 100644 index 00000000000..d71f935a9c2 --- /dev/null +++ b/inject-java/src/test/groovy/io/micronaut/visitors/TypeParameterRuntimeAnn.java @@ -0,0 +1,11 @@ +package io.micronaut.visitors; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +public @interface TypeParameterRuntimeAnn { +} diff --git a/inject-java/src/test/groovy/io/micronaut/visitors/TypeUseClassAnn.java b/inject-java/src/test/groovy/io/micronaut/visitors/TypeUseClassAnn.java new file mode 100644 index 00000000000..e984a6f9657 --- /dev/null +++ b/inject-java/src/test/groovy/io/micronaut/visitors/TypeUseClassAnn.java @@ -0,0 +1,11 @@ +package io.micronaut.visitors; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.TYPE_USE) +@Retention(RetentionPolicy.CLASS) +public @interface TypeUseClassAnn { +} diff --git a/inject-java/src/test/groovy/io/micronaut/visitors/TypeUseRuntimeAnn.java b/inject-java/src/test/groovy/io/micronaut/visitors/TypeUseRuntimeAnn.java new file mode 100644 index 00000000000..07d27122e6d --- /dev/null +++ b/inject-java/src/test/groovy/io/micronaut/visitors/TypeUseRuntimeAnn.java @@ -0,0 +1,11 @@ +package io.micronaut.visitors; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.TYPE_USE) +@Retention(RetentionPolicy.RUNTIME) +public @interface TypeUseRuntimeAnn { +} diff --git a/inject-java/src/test/resources/META-INF/services/io.micronaut.inject.visitor.TypeElementVisitor b/inject-java/src/test/resources/META-INF/services/io.micronaut.inject.visitor.TypeElementVisitor index f8c16c41471..1fbf5b74cf0 100644 --- a/inject-java/src/test/resources/META-INF/services/io.micronaut.inject.visitor.TypeElementVisitor +++ b/inject-java/src/test/resources/META-INF/services/io.micronaut.inject.visitor.TypeElementVisitor @@ -10,3 +10,8 @@ io.micronaut.annotation.mapping.ReplacesRepeatableAnnotationSpec$ReplacesRepeata io.micronaut.annotation.mapping.AddsUnseenRepeatableAnnotationSpec$AddUnseenRepeatableTypeElementVisitor io.micronaut.annotation.mapping.AddsUnseenInnerRepeatableAnnotationSpec$AddUnseenRepeatableTypeElementVisitor io.micronaut.annotation.mapping.SourceAnnotationHasDefaultsSpec$TheVisitor +io.micronaut.annotation.AnnotateMethodSpec$AnnotationMethodVisitor +io.micronaut.annotation.AnnotateMethodReturnSpec$AnnotateMethodReturnVisitor +io.micronaut.annotation.AnnotateFieldSpec$AnnotationFieldVisitor +io.micronaut.annotation.AnnotateFieldTypeSpec$AnnotateFieldTypeVisitor +io.micronaut.annotation.AnnotateMethodParameterSpec$AnnotateMethodParameterVisitor diff --git a/inject-kotlin/build.gradle b/inject-kotlin/build.gradle index 13d11b2098b..8e697424cee 100644 --- a/inject-kotlin/build.gradle +++ b/inject-kotlin/build.gradle @@ -62,6 +62,6 @@ tasks.named("test") { // } maxHeapSize("1G") forkEvery = 40 - maxParallelForks = 2 + maxParallelForks = 4 } diff --git a/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/beans/BeanDefinitionProcessor.kt b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/beans/BeanDefinitionProcessor.kt index c727f00069a..006f8bb4d45 100644 --- a/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/beans/BeanDefinitionProcessor.kt +++ b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/beans/BeanDefinitionProcessor.kt @@ -39,15 +39,7 @@ class BeanDefinitionProcessor(private val environment: SymbolProcessorEnvironmen private val beanDefinitionMap = mutableMapOf() override fun process(resolver: Resolver): List { - val visitorContext = object : KotlinVisitorContext(environment, resolver) { - override fun getConfiguration(): VisitorConfiguration { - return object : VisitorConfiguration { - override fun includeTypeLevelAnnotationsInGenericArguments(): Boolean { - return false - } - } - } - } + val visitorContext = KotlinVisitorContext(environment, resolver) val elements = resolver.getAllFiles() .flatMap { file: KSFile -> diff --git a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/beans/BeanDefinitionSpec.groovy b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/beans/BeanDefinitionSpec.groovy index 33f3c2230a9..c110b58ca2c 100644 --- a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/beans/BeanDefinitionSpec.groovy +++ b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/beans/BeanDefinitionSpec.groovy @@ -1,7 +1,6 @@ package io.micronaut.kotlin.processing.beans import io.micronaut.annotation.processing.test.KotlinCompiler -import io.micronaut.context.ApplicationContext import io.micronaut.context.exceptions.NoSuchBeanException import io.micronaut.core.annotation.AnnotationUtil import io.micronaut.core.annotation.Introspected @@ -10,13 +9,16 @@ import io.micronaut.core.bind.annotation.Bindable import io.micronaut.http.annotation.Header import io.micronaut.http.annotation.HttpMethodMapping import io.micronaut.http.client.annotation.Client -import io.micronaut.inject.BeanDefinition import io.micronaut.inject.qualifiers.Qualifiers import io.micronaut.inject.writer.BeanDefinitionVisitor import spock.lang.PendingFeature import spock.lang.Specification -import static io.micronaut.annotation.processing.test.KotlinCompiler.* +import static io.micronaut.annotation.processing.test.KotlinCompiler.buildBeanDefinition +import static io.micronaut.annotation.processing.test.KotlinCompiler.buildBeanDefinitionReference +import static io.micronaut.annotation.processing.test.KotlinCompiler.buildContext +import static io.micronaut.annotation.processing.test.KotlinCompiler.getBean +import static io.micronaut.annotation.processing.test.KotlinCompiler.getBeanDefinition class BeanDefinitionSpec extends Specification { @@ -252,7 +254,7 @@ class RepeatedTest { ''') expect: definition.getRequiredMethod("test").getAnnotationValuesByType(Header).size() == 4 - definition.getRequiredMethod("test").getAnnotationNamesByStereotype(Bindable).size() == 2 + definition.getRequiredMethod("test").getAnnotationNamesByStereotype(Bindable) == [Header.class.name] } void "test repeated annotations"() { diff --git a/inject/src/main/java/io/micronaut/context/AbstractExecutableMethodsDefinition.java b/inject/src/main/java/io/micronaut/context/AbstractExecutableMethodsDefinition.java index 97b026c8727..56c249d9d8e 100644 --- a/inject/src/main/java/io/micronaut/context/AbstractExecutableMethodsDefinition.java +++ b/inject/src/main/java/io/micronaut/context/AbstractExecutableMethodsDefinition.java @@ -25,6 +25,7 @@ import io.micronaut.inject.ExecutableMethod; import io.micronaut.inject.ExecutableMethodsDefinition; import io.micronaut.inject.annotation.AbstractEnvironmentAnnotationMetadata; +import io.micronaut.inject.annotation.AnnotationMetadataHierarchy; import java.lang.reflect.Method; import java.util.*; @@ -227,18 +228,22 @@ private boolean argumentsTypesMatch(Class[] argumentTypes, Argument[] argu /** * Internal class representing method's metadata. + * + * @param declaringType The declaring type + * @param annotationMetadata The metadata + * @param methodName The method name + * @param returnArgument The return argument + * @param arguments The arguments + * @param isAbstract Is abstract + * @param isSuspend Is suspend */ - @Internal - public static final class MethodReference { - final AnnotationMetadata annotationMetadata; - final Class declaringType; - final String methodName; - @Nullable - final Argument returnArgument; - final Argument[] arguments; - final boolean isAbstract; - final boolean isSuspend; - + public record MethodReference(Class declaringType, + AnnotationMetadata annotationMetadata, + String methodName, + @Nullable Argument returnArgument, + Argument[] arguments, + boolean isAbstract, + boolean isSuspend) { /** * The constructor. * @@ -250,7 +255,13 @@ public static final class MethodReference { * @param isAbstract Is abstract * @param isSuspend Is suspend */ - public MethodReference(Class declaringType, AnnotationMetadata annotationMetadata, String methodName, Argument returnArgument, Argument[] arguments, boolean isAbstract, boolean isSuspend) { + public MethodReference(Class declaringType, + AnnotationMetadata annotationMetadata, + String methodName, + Argument returnArgument, + Argument[] arguments, + boolean isAbstract, + boolean isSuspend) { this.declaringType = declaringType; this.annotationMetadata = annotationMetadata == null ? AnnotationMetadata.EMPTY_METADATA : annotationMetadata; this.methodName = methodName; @@ -259,6 +270,37 @@ public MethodReference(Class declaringType, AnnotationMetadata annotationMeta this.isAbstract = isAbstract; this.isSuspend = isSuspend; } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + MethodReference that = (MethodReference) o; + return isAbstract == that.isAbstract && isSuspend == that.isSuspend && Objects.equals(declaringType, that.declaringType) && Objects.equals(annotationMetadata, that.annotationMetadata) && Objects.equals(methodName, that.methodName) && Objects.equals(returnArgument, that.returnArgument) && Arrays.equals(arguments, that.arguments); + } + + @Override + public int hashCode() { + return methodName.hashCode(); + } + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder("MethodReference{"); + sb.append("declaringType=").append(declaringType); + sb.append(", annotationMetadata=").append(annotationMetadata); + sb.append(", methodName='").append(methodName).append('\''); + sb.append(", returnArgument=").append(returnArgument); + sb.append(", arguments=").append(Arrays.toString(arguments)); + sb.append(", isAbstract=").append(isAbstract); + sb.append(", isSuspend=").append(isSuspend); + sb.append('}'); + return sb.toString(); + } } /** @@ -267,15 +309,18 @@ public MethodReference(Class declaringType, AnnotationMetadata annotationMeta * @param The type * @param The result type */ - private static final class DispatchedExecutableMethod implements ExecutableMethod, ReturnType, EnvironmentConfigurable { + private static final class DispatchedExecutableMethod implements ExecutableMethod, EnvironmentConfigurable { private final AbstractExecutableMethodsDefinition dispatcher; private final int index; private final MethodReference methodReference; private AnnotationMetadata annotationMetadata; + private ReturnType returnType; - private DispatchedExecutableMethod(AbstractExecutableMethodsDefinition dispatcher, int index, - MethodReference methodReference, AnnotationMetadata annotationMetadata) { + private DispatchedExecutableMethod(AbstractExecutableMethodsDefinition dispatcher, + int index, + MethodReference methodReference, + AnnotationMetadata annotationMetadata) { this.dispatcher = dispatcher; this.index = index; this.methodReference = methodReference; @@ -326,20 +371,31 @@ public Method getTargetMethod() { @Override public ReturnType getReturnType() { - return this; - } - - @Override - public Class getType() { - if (methodReference.returnArgument == null) { - return (Class) void.class; + if (returnType == null) { + // Return type also contains method annotations (Micronaut 3) + Argument returnTypeArgument = methodReference.returnArgument == null ? (Argument) Argument.VOID : (Argument) methodReference.returnArgument; + AnnotationMetadata returnTypeAnnotationMetadata; + if (!returnTypeArgument.getAnnotationMetadata().isEmpty() && !annotationMetadata.isEmpty()) { + returnTypeAnnotationMetadata = new AnnotationMetadataHierarchy(returnTypeArgument.getAnnotationMetadata(), annotationMetadata); + } else if (!returnTypeArgument.getAnnotationMetadata().isEmpty()) { + returnTypeAnnotationMetadata = returnTypeArgument.getAnnotationMetadata(); + } else { + returnTypeAnnotationMetadata = annotationMetadata; + } + if (returnTypeArgument.getAnnotationMetadata() != returnTypeAnnotationMetadata) { + returnTypeArgument = Argument.of( + returnTypeArgument.getType(), + returnTypeAnnotationMetadata, + returnTypeArgument.getTypeParameters() + ); + } + returnType = new DefaultReturnType<>( + returnTypeArgument, + returnTypeAnnotationMetadata, + methodReference.isSuspend + ); } - return (Class) methodReference.returnArgument.getType(); - } - - @Override - public boolean isSuspended() { - return methodReference.isSuspend; + return returnType; } @NonNull @@ -348,31 +404,6 @@ public AnnotationMetadata getAnnotationMetadata() { return annotationMetadata; } - @Override - public Argument[] getTypeParameters() { - if (methodReference.returnArgument != null) { - return methodReference.returnArgument.getTypeParameters(); - } - return Argument.ZERO_ARGUMENTS; - } - - @Override - public Map> getTypeVariables() { - if (methodReference.returnArgument != null) { - return methodReference.returnArgument.getTypeVariables(); - } - return Collections.emptyMap(); - } - - @Override - @NonNull - public Argument asArgument() { - Map> typeVariables = getTypeVariables(); - Collection> values = typeVariables.values(); - final AnnotationMetadata annotationMetadata = getAnnotationMetadata(); - return Argument.of(getType(), annotationMetadata, values.toArray(Argument.ZERO_ARGUMENTS)); - } - @Override public R invoke(T instance, Object... arguments) { ArgumentUtils.validateArguments(this, methodReference.arguments, arguments); @@ -384,10 +415,9 @@ public boolean equals(Object o) { if (this == o) { return true; } - if (!(o instanceof AbstractExecutableMethodsDefinition.DispatchedExecutableMethod)) { + if (!(o instanceof AbstractExecutableMethodsDefinition.DispatchedExecutableMethod that)) { return false; } - DispatchedExecutableMethod that = (DispatchedExecutableMethod) o; return Objects.equals(methodReference.declaringType, that.methodReference.declaringType) && Objects.equals(methodReference.methodName, that.methodReference.methodName) && Arrays.equals(methodReference.arguments, that.methodReference.arguments); @@ -407,6 +437,51 @@ public String toString() { String text = Argument.toString(getArguments()); return getReturnType().getType().getSimpleName() + " " + getMethodName() + "(" + text + ")"; } + + } + + /** + * The default return type implementation. + * + * @param returnArgument The return argument + * @param annotationMetadata The annotation metadata + * @param isSuspend Is suspended + * @param The return type + */ + private record DefaultReturnType(Argument returnArgument, + AnnotationMetadata annotationMetadata, + boolean isSuspend) implements ReturnType { + + @Override + public AnnotationMetadata getAnnotationMetadata() { + return annotationMetadata; + } + + @Override + public Class getType() { + return returnArgument.getType(); + } + + @Override + public boolean isSuspended() { + return isSuspend; + } + + @Override + public Argument[] getTypeParameters() { + return returnArgument.getTypeParameters(); + } + + @Override + public Map> getTypeVariables() { + return returnArgument.getTypeVariables(); + } + + @Override + @NonNull + public Argument asArgument() { + return returnArgument; + } } private static final class MethodAnnotationMetadata extends AbstractEnvironmentAnnotationMetadata { diff --git a/inject/src/main/java/io/micronaut/inject/annotation/AnnotationMetadataHierarchy.java b/inject/src/main/java/io/micronaut/inject/annotation/AnnotationMetadataHierarchy.java index 61f0c640887..c6298f573fa 100644 --- a/inject/src/main/java/io/micronaut/inject/annotation/AnnotationMetadataHierarchy.java +++ b/inject/src/main/java/io/micronaut/inject/annotation/AnnotationMetadataHierarchy.java @@ -33,6 +33,7 @@ import java.util.Collections; import java.util.HashSet; import java.util.Iterator; +import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Optional; @@ -424,20 +425,20 @@ public Optional classValue(@NonNull String annotation, @NonNull String me @NonNull @Override public List getAnnotationNamesByStereotype(@Nullable String stereotype) { - List list = new ArrayList<>(); + Set list = new LinkedHashSet<>(); for (AnnotationMetadata am : hierarchy) { list.addAll(am.getAnnotationNamesByStereotype(stereotype)); } - return list; + return new ArrayList<>(list); } @Override public List> getAnnotationValuesByStereotype(String stereotype) { - List> list = new ArrayList<>(); + Set> list = new LinkedHashSet<>(); for (AnnotationMetadata am : hierarchy) { list.addAll(am.getAnnotationValuesByStereotype(stereotype)); } - return list; + return new ArrayList<>(list); } @NonNull From c2c2d680e22d50fbbb0afb794a320437b17a2812 Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Wed, 15 Feb 2023 17:58:28 +0100 Subject: [PATCH 484/743] build: Netty 4.1.87.Final (#8774) --- gradle/libs.versions.toml | 2 +- src/main/docs/guide/httpServer/http2Server.adoc | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 4a957974b95..2915988406b 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -126,7 +126,7 @@ managed-micronaut-views = "3.8.1" managed-micronaut-xml = "3.2.0" managed-neo4j = "3.5.35" managed-neo4j-java-driver = "4.4.9" -managed-netty = "4.1.86.Final" +managed-netty = "4.1.87.Final" managed-reactive-pg-client = "0.11.4" managed-reactive-streams = "1.0.4" # This should be kept aligned with https://github.com/micronaut-projects/micronaut-reactor/blob/master/gradle.properties from the BOM diff --git a/src/main/docs/guide/httpServer/http2Server.adoc b/src/main/docs/guide/httpServer/http2Server.adoc index 106baae965e..1eb3a5bdbc1 100644 --- a/src/main/docs/guide/httpServer/http2Server.adoc +++ b/src/main/docs/guide/httpServer/http2Server.adoc @@ -31,16 +31,16 @@ For production, see the <> section of the documentatio Note that if your deployment environment uses JDK 8, or for improved support for OpenSSL, define the following dependencies on Netty Tomcat Native: -dependency:io.netty:netty-tcnative:2.0.46.Final[scope="runtimeOnly"] +dependency:io.netty:netty-tcnative:2.0.58.Final[scope="runtimeOnly"] -dependency:io.netty:netty-tcnative-boringssl-static:2.0.46.Final[scope="runtimeOnly"] +dependency:io.netty:netty-tcnative-boringssl-static:2.0.58.Final[scope="runtimeOnly"] In addition to a dependency on the appropriate native library for your architecture. For example: .Configuring Tomcat Native [source,groovy] ---- -runtimeOnly "io.netty:netty-tcnative-boringssl-static:2.0.46.Final:${Os.isFamily(Os.FAMILY_MAC) ? (Os.isArch("aarch64") ? "osx-aarch_64" : "osx-x86_64") : 'linux-x86_64'}" +runtimeOnly "io.netty:netty-tcnative-boringssl-static:2.0.58.Final:${Os.isFamily(Os.FAMILY_MAC) ? (Os.isArch("aarch64") ? "osx-aarch_64" : "osx-x86_64") : 'linux-x86_64'}" ---- See the documentation on https://netty.io/wiki/forked-tomcat-native.html[Tomcat Native] for more information. From a2fec0e157266b5667e699d1ed31384cf405436b Mon Sep 17 00:00:00 2001 From: Adam Wilder Date: Wed, 15 Feb 2023 11:11:27 -0600 Subject: [PATCH 485/743] Issues 8714: HttpClientIntroductionAdvice Subscriber may not have complete called by some Publisher implementations (#8715) --- .../HttpClientIntroductionAdvice.java | 15 +++++--- test-suite-kotlin/build.gradle | 1 + .../http/client/SuspendClientFilter.kt | 35 +++++++++++++++++++ .../http/client/SuspendClientSpec.kt | 26 +++++++++++--- 4 files changed, 68 insertions(+), 9 deletions(-) create mode 100644 test-suite-kotlin/src/test/kotlin/io/micronaut/http/client/SuspendClientFilter.kt diff --git a/http-client-core/src/main/java/io/micronaut/http/client/interceptor/HttpClientIntroductionAdvice.java b/http-client-core/src/main/java/io/micronaut/http/client/interceptor/HttpClientIntroductionAdvice.java index 6d400b58a7e..28765b807b4 100644 --- a/http-client-core/src/main/java/io/micronaut/http/client/interceptor/HttpClientIntroductionAdvice.java +++ b/http-client-core/src/main/java/io/micronaut/http/client/interceptor/HttpClientIntroductionAdvice.java @@ -88,7 +88,6 @@ import java.util.Map; import java.util.Optional; import java.util.concurrent.CompletableFuture; -import java.util.concurrent.atomic.AtomicReference; import java.util.function.Function; import java.util.function.Supplier; @@ -343,18 +342,23 @@ public Object intercept(MethodInvocationContext context) { Publisher csPublisher = httpClientResponsePublisher(httpClient, request, returnType, errorType, valueType); CompletableFuture future = new CompletableFuture<>(); csPublisher.subscribe(new CompletionAwareSubscriber() { - AtomicReference reference = new AtomicReference<>(); + Object message; + Subscription subscription; @Override protected void doOnSubscribe(Subscription subscription) { - subscription.request(1); + this.subscription = subscription; + subscription.request(Long.MAX_VALUE); } @Override protected void doOnNext(Object message) { if (Void.class != reactiveValueType) { - reference.set(message); + this.message = message; } + // we only want the first item + subscription.cancel(); + doOnComplete(); } @Override @@ -380,7 +384,8 @@ protected void doOnError(Throwable t) { @Override protected void doOnComplete() { - future.complete(reference.get()); + // can be called twice + future.complete(message); } }); return interceptedMethod.handleResult(future); diff --git a/test-suite-kotlin/build.gradle b/test-suite-kotlin/build.gradle index a6cefd28057..fe81a0c2773 100644 --- a/test-suite-kotlin/build.gradle +++ b/test-suite-kotlin/build.gradle @@ -28,6 +28,7 @@ dependencies { testImplementation libs.kotlinx.coroutines.rx2 testImplementation libs.kotlinx.coroutines.slf4j testImplementation libs.kotlinx.coroutines.reactor + testImplementation libs.kotlinx.coroutines.reactive // Adding these for now since micronaut-test isnt resolving correctly ... probably need to upgrade gradle there too testImplementation libs.junit.jupiter.api diff --git a/test-suite-kotlin/src/test/kotlin/io/micronaut/http/client/SuspendClientFilter.kt b/test-suite-kotlin/src/test/kotlin/io/micronaut/http/client/SuspendClientFilter.kt new file mode 100644 index 00000000000..eb76823a4ca --- /dev/null +++ b/test-suite-kotlin/src/test/kotlin/io/micronaut/http/client/SuspendClientFilter.kt @@ -0,0 +1,35 @@ +package io.micronaut.http.client + +import io.micronaut.http.HttpResponse +import io.micronaut.http.MutableHttpRequest +import io.micronaut.http.annotation.Filter +import io.micronaut.http.filter.ClientFilterChain +import io.micronaut.http.filter.HttpClientFilter +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.flatMapMerge +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.reactive.asFlow +import kotlinx.coroutines.reactive.asPublisher +import kotlinx.coroutines.withContext +import org.reactivestreams.Publisher + +@Filter +class SuspendClientFilter : HttpClientFilter { + override fun doFilter(request: MutableHttpRequest<*>, chain: ClientFilterChain): Publisher> { + // if request contains filterCheck, then do flow step, else proceed + return if ((request.body.orElse(null) as? Map<*, *>)?.containsValue(filterCheck) == true) { + flow { emit(getValue()) }.flatMapMerge { + chain.proceed(request).asFlow() + }.asPublisher() + } else { + chain.proceed(request) + } + } + + companion object { + val filterCheck = java.util.UUID.randomUUID().toString() + suspend fun getValue() = withContext(Dispatchers.Default) { "testString" } + } +} + + diff --git a/test-suite-kotlin/src/test/kotlin/io/micronaut/http/client/SuspendClientSpec.kt b/test-suite-kotlin/src/test/kotlin/io/micronaut/http/client/SuspendClientSpec.kt index 606cee07790..9da5a8b4896 100644 --- a/test-suite-kotlin/src/test/kotlin/io/micronaut/http/client/SuspendClientSpec.kt +++ b/test-suite-kotlin/src/test/kotlin/io/micronaut/http/client/SuspendClientSpec.kt @@ -2,6 +2,7 @@ package io.micronaut.http.client import io.micronaut.context.ApplicationContext import io.micronaut.http.HttpStatus +import io.micronaut.http.client.SuspendClientFilter import io.micronaut.runtime.server.EmbeddedServer import kotlinx.coroutines.runBlocking import org.junit.jupiter.api.Assertions @@ -11,35 +12,52 @@ class SuspendClientSpec { @Test fun testSuspendClientBody() { - val server = ApplicationContext.run(EmbeddedServer::class.java, mapOf("spec.name" to "SuspendClientSpec")) + val server = createServer() val ctx = server.applicationContext val response = runBlocking { ctx.getBean(SuspendClient::class.java).call("test") } - Assertions.assertEquals(response, "{\"newState\":\"test\"}") + server.close() } @Test fun testNotFound() { - val server = ApplicationContext.run(EmbeddedServer::class.java, mapOf("spec.name" to "SuspendClientSpec")) + val server = createServer() val ctx = server.applicationContext val response = runBlocking { ctx.getBean(SuspendClient::class.java).notFound() } Assertions.assertEquals(response.status, HttpStatus.NOT_FOUND) + server.close() } @Test fun testNotFoundWithoutHttpResponseWrapper() { - val server = ApplicationContext.run(EmbeddedServer::class.java, mapOf("spec.name" to "SuspendClientSpec")) + val server = createServer() val ctx = server.applicationContext val response = runBlocking { ctx.getBean(SuspendClient::class.java).notFoundWithoutHttpResponseWrapper() } Assertions.assertNull(response) + server.close() + } + + @Test + fun testFlowAsPublisherInFilterStep() { + val server = createServer() + val ctx = server.applicationContext + val response = runBlocking { + ctx.getBean(SuspendClient::class.java).call(SuspendClientFilter.filterCheck) + } + Assertions.assertEquals(response, "{\"newState\":\"${SuspendClientFilter.filterCheck}\"}") + server.close() + } + + fun createServer() : EmbeddedServer { + return ApplicationContext.run(EmbeddedServer::class.java, mapOf("spec.name" to "SuspendClientSpec")) } } From 80a8749e92a13bb21561e1ad3876cb88fa97ae82 Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Thu, 16 Feb 2023 10:38:21 +0100 Subject: [PATCH 486/743] build: Apache groovy 4.0.9 (#8784) without `@CompileDynamic` ``` [Static type checking] - Non-static method java.lang.Object#hashCode cannot be called from static context @ line 99, column 16. return visitor.getClass().hashCode() ^ ``` --- gradle/libs.versions.toml | 2 +- .../io/micronaut/ast/groovy/visitor/LoadedVisitor.groovy | 2 ++ src/main/docs/guide/introduction/whatsNew.adoc | 5 +++++ 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index c0a630744df..0ed30578232 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -60,7 +60,7 @@ wiremock = "2.33.2" # Versions which start with managed- are managed by Micronaut in the sense # that they will appear in the Micronaut BOM as # -managed-groovy = "4.0.6" +managed-groovy = "4.0.9" managed-jakarta-annotation-api = "2.1.1" managed-jackson = "2.14.0" managed-jackson-databind = "2.14.1" diff --git a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/LoadedVisitor.groovy b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/LoadedVisitor.groovy index 07c373cfd5b..5e1f61f5bff 100644 --- a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/LoadedVisitor.groovy +++ b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/LoadedVisitor.groovy @@ -15,6 +15,7 @@ */ package io.micronaut.ast.groovy.visitor +import groovy.transform.CompileDynamic import groovy.transform.CompileStatic import io.micronaut.core.annotation.AnnotationMetadata import io.micronaut.core.annotation.Internal @@ -94,6 +95,7 @@ class LoadedVisitor implements Ordered { return true } + @CompileDynamic @Override int hashCode() { return visitor.getClass().hashCode() diff --git a/src/main/docs/guide/introduction/whatsNew.adoc b/src/main/docs/guide/introduction/whatsNew.adoc index e136bc6f6dd..2cbadb937bd 100644 --- a/src/main/docs/guide/introduction/whatsNew.adoc +++ b/src/main/docs/guide/introduction/whatsNew.adoc @@ -1,8 +1,13 @@ //Micronaut {version} includes the following changes: == 4.0.0 +=== Apache Groovy 4.0 + +Micronaut Framework 4.x supports https://groovy-lang.org/releasenotes/groovy-4.0.html[Apache Groovy 4.0]. + === Core Changes + * <> ==== Injection of Maps From 587f3e51df37d833a0ab8c997d6ce0ec939ab0c0 Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Thu, 16 Feb 2023 10:55:05 +0100 Subject: [PATCH 487/743] build: update Micronaut build plugin to 5.4.6 --- settings.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/settings.gradle b/settings.gradle index 4bdc2a553f5..1eebe9ed7b6 100644 --- a/settings.gradle +++ b/settings.gradle @@ -14,7 +14,7 @@ pluginManagement { } plugins { - id 'io.micronaut.build.shared.settings' version '5.4.5' + id 'io.micronaut.build.shared.settings' version '5.4.6' } enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") From e114ba068ff3ff1c69a22fef9b97a7be3a53d2be Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Thu, 16 Feb 2023 11:23:25 +0100 Subject: [PATCH 488/743] Merge branch '3.9.x' into 4.0.x --- .github/workflows/corretto.yml | 73 ++++++++++++------- .github/workflows/release-notes.yml | 2 +- .github/workflows/release.yml | 2 +- .../visitor/BeanIntrospectionWriter.java | 3 +- .../inject/processing/JavaModelUtils.java | 14 ++-- .../inject/writer/DispatchWriter.java | 22 ++++-- gradle/libs.versions.toml | 2 +- .../HttpClientIntroductionAdvice.java | 15 ++-- .../native-image.properties | 1 + .../websocket/BinaryChatClientWebSocket.java | 4 +- .../websocket/BinaryChatServerWebSocket.java | 6 +- .../websocket/BinaryWebSocketSpec.groovy | 7 -- .../tck/tests/cors/CorsSimpleRequestTest.java | 19 +++++ .../SimpleRequestWithCorsNotEnabledTest.java | 24 ++++++ .../http/server/HttpServerConfiguration.java | 21 ++++++ .../http/server/cors/CorsFilter.java | 6 ++ .../inject/beanimport/BeanImportSpec.groovy | 43 +++++++++++ .../inject/visitor/beans/Auditable.java | 20 +++++ .../beans/BeanIntrospectionSpec.groovy | 39 +++++++++- .../AbstractExecutableMethodsDefinition.java | 15 +--- ...bstractInitializableBeanIntrospection.java | 31 +++++++- .../micronaut-inject/native-image.properties | 1 + settings.gradle | 2 +- ...ributedConfigurationAwsParameterStore.adoc | 1 - .../distributedConfigurationSpringCloud.adoc | 7 +- .../serviceDiscoveryManual.adoc | 12 +-- .../serviceDiscoveryRoute53.adoc | 3 +- src/main/docs/guide/config/eachProperty.adoc | 2 +- .../docs/guide/config/valueAnnotation.adoc | 1 - .../clientAnnotation/clientHeaders.adoc | 2 +- .../clientConfiguration.adoc | 11 ++- .../docs/guide/httpServer/apiVersioning.adoc | 24 +++--- .../docs/guide/httpServer/http2Server.adoc | 6 +- .../guide/httpServer/runningSpecificPort.adoc | 4 +- .../guide/httpServer/serverConfiguration.adoc | 16 ++-- .../serverConfiguration/accessLogger.adoc | 21 ++++-- .../cors/corsAllowCredentials.adoc | 2 +- .../cors/corsAllowedHeaders.adoc | 2 +- .../cors/corsAllowedMethods.adoc | 2 +- .../cors/corsAllowedOrigins.adoc | 2 +- .../cors/corsConfiguration.adoc | 6 +- .../cors/corsExposedHeaders.adoc | 2 +- .../serverConfiguration/cors/corsMaxAge.adoc | 2 +- .../cors/corsMultipleHeaderValues.adoc | 2 +- .../serverConfiguration/dualProtocol.adoc | 15 ++-- .../httpServer/serverConfiguration/https.adoc | 14 ++-- .../serverConfiguration/listener.adoc | 17 +++-- .../threadPools/blockingOperations.adoc | 1 - .../httpServer/websocket/websocketServer.adoc | 4 +- .../guide/logging/loggingConfiguration.adoc | 8 +- .../environmentEndpoint.adoc | 6 +- .../providedEndpoints/healthEndpoint.adoc | 11 ++- test-suite-kotlin/build.gradle | 1 + .../http/client/SuspendClientFilter.kt | 35 +++++++++ .../http/client/SuspendClientSpec.kt | 26 ++++++- 55 files changed, 471 insertions(+), 169 deletions(-) create mode 100644 http-netty/src/main/resources/META-INF/native-image/io.micronaut/micronaut-http-netty/native-image.properties create mode 100644 inject-java-test/src/test/groovy/io/micronaut/inject/visitor/beans/Auditable.java create mode 100644 test-suite-kotlin/src/test/kotlin/io/micronaut/http/client/SuspendClientFilter.kt diff --git a/.github/workflows/corretto.yml b/.github/workflows/corretto.yml index 9629db537e8..8fb5984fad0 100644 --- a/.github/workflows/corretto.yml +++ b/.github/workflows/corretto.yml @@ -2,7 +2,7 @@ name: Corretto CI on: push: branches: - - master + - '[1-9]+.[0-9]+.x' jobs: build: runs-on: ubuntu-latest @@ -11,28 +11,49 @@ jobs: java: ['17'] container: amazoncorretto:${{ matrix.java }} steps: - - name: Display Java and Linux version - run: java -version && cat /etc/system-release - - name: Install tar && gzip - run: yum install -y tar gzip - - uses: actions/checkout@v3 - - uses: actions/cache@v3.0.2 - with: - path: ~/.gradle/caches - key: ${{ runner.os }}-micronaut-core-gradle-${{ hashFiles('**/*.gradle') }} - restore-keys: | - ${{ runner.os }}-micronaut-core-gradle- - - uses: actions/cache@v3.0.2 - with: - path: ~/.gradle/wrapper - key: ${{ runner.os }}-micronaut-core-wrapper-${{ hashFiles('**/*.gradle') }} - restore-keys: | - ${{ runner.os }}-micronaut-core-wrapper- - - name: Build with Gradle - env: - GH_TOKEN_PUBLIC_REPOS_READONLY: ${{ secrets.GH_TOKEN_PUBLIC_REPOS_READONLY }} - GH_USERNAME: ${{ secrets.GH_USERNAME }} - GRADLE_ENTERPRISE_ACCESS_KEY: ${{ secrets.GRADLE_ENTERPRISE_ACCESS_KEY }} - GRADLE_ENTERPRISE_CACHE_USERNAME: ${{ secrets.GRADLE_ENTERPRISE_CACHE_USERNAME }} - GRADLE_ENTERPRISE_CACHE_PASSWORD: ${{ secrets.GRADLE_ENTERPRISE_CACHE_PASSWORD }} - run: unset HOSTNAME ; LANG=en_US.utf-8 LC_ALL=en_US.utf-8 ./gradlew check --no-daemon --parallel --continue + # https://github.com/actions/virtual-environments/issues/709 + - name: Free disk space + run: | + sudo rm -rf "/usr/local/share/boost" + sudo rm -rf "$AGENT_TOOLSDIRECTORY" + sudo apt-get clean + df -h + - uses: actions/checkout@v3 + - name: Set up JDK + uses: actions/setup-java@v3 + with: + distribution: 'corretto' + java-version: ${{ matrix.java }} + - name: Setup Gradle + uses: gradle/gradle-build-action@v2.3.3 + - name: Optional setup step + env: + GRADLE_ENTERPRISE_ACCESS_KEY: ${{ secrets.GRADLE_ENTERPRISE_ACCESS_KEY }} + GRADLE_ENTERPRISE_CACHE_USERNAME: ${{ secrets.GRADLE_ENTERPRISE_CACHE_USERNAME }} + GRADLE_ENTERPRISE_CACHE_PASSWORD: ${{ secrets.GRADLE_ENTERPRISE_CACHE_PASSWORD }} + run: | + [ -f ./setup.sh ] && ./setup.sh || true + - name: Build with Gradle + id: gradle + run: | + ./gradlew check --no-daemon --parallel --continue + env: + GH_TOKEN_PUBLIC_REPOS_READONLY: ${{ secrets.GH_TOKEN_PUBLIC_REPOS_READONLY }} + GH_USERNAME: ${{ secrets.GH_USERNAME }} + TESTCONTAINERS_RYUK_DISABLED: true + GRADLE_ENTERPRISE_ACCESS_KEY: ${{ secrets.GRADLE_ENTERPRISE_ACCESS_KEY }} + GRADLE_ENTERPRISE_CACHE_USERNAME: ${{ secrets.GRADLE_ENTERPRISE_CACHE_USERNAME }} + GRADLE_ENTERPRISE_CACHE_PASSWORD: ${{ secrets.GRADLE_ENTERPRISE_CACHE_PASSWORD }} + PREDICTIVE_TEST_SELECTION: "${{ github.event_name == 'pull_request' && 'true' || 'false' }}" + - name: Add build scan URL as PR comment + uses: actions/github-script@v5 + if: github.event_name == 'pull_request' && failure() + with: + github-token: ${{secrets.GITHUB_TOKEN}} + script: | + github.rest.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body: '❌ ${{ github.workflow }} failed: ${{ steps.gradle.outputs.build-scan-url }}' + }) diff --git a/.github/workflows/release-notes.yml b/.github/workflows/release-notes.yml index 35c3bf7a7a9..ecb1f667c65 100644 --- a/.github/workflows/release-notes.yml +++ b/.github/workflows/release-notes.yml @@ -21,7 +21,7 @@ jobs: id: check_release_drafter run: | has_release_drafter=$([ -f .github/release-drafter.yml ] && echo "true" || echo "false") - echo ::set-output name=has_release_drafter::${has_release_drafter} + echo "has_release_drafter=${has_release_drafter}" >> $GITHUB_OUTPUT # If it has release drafter: - uses: release-drafter/release-drafter@v5 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 908ba288e13..b6b487f4a90 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -25,7 +25,7 @@ jobs: java-version: '17' - name: Set the current release version id: release_version - run: echo ::set-output name=release_version::${GITHUB_REF:11} + run: echo "release_version=${GITHUB_REF:11}" >> $GITHUB_OUTPUT - name: Run pre-release uses: micronaut-projects/github-actions/pre-release@master env: diff --git a/core-processor/src/main/java/io/micronaut/inject/beans/visitor/BeanIntrospectionWriter.java b/core-processor/src/main/java/io/micronaut/inject/beans/visitor/BeanIntrospectionWriter.java index 9175be0b417..01f62bab50f 100644 --- a/core-processor/src/main/java/io/micronaut/inject/beans/visitor/BeanIntrospectionWriter.java +++ b/core-processor/src/main/java/io/micronaut/inject/beans/visitor/BeanIntrospectionWriter.java @@ -135,7 +135,7 @@ final class BeanIntrospectionWriter extends AbstractAnnotationMetadataWriter { this.introspectionName = computeIntrospectionName(name); this.introspectionType = getTypeReferenceForName(introspectionName); this.beanType = getTypeReferenceForName(name); - this.dispatchWriter = new DispatchWriter(introspectionType); + this.dispatchWriter = new DispatchWriter(introspectionType, Type.getType(AbstractInitializableBeanIntrospection.class)); } /** @@ -574,6 +574,7 @@ private void writeIntrospectionClass(ClassWriterOutputVisitor classWriterOutputV dispatchWriter.buildDispatchOneMethod(classWriter); dispatchWriter.buildDispatchMethod(classWriter); + dispatchWriter.buildGetTargetMethodByIndex(classWriter); buildPropertyIndexOfMethod(classWriter); buildFindIndexedProperty(classWriter); buildGetIndexedProperties(classWriter); diff --git a/core-processor/src/main/java/io/micronaut/inject/processing/JavaModelUtils.java b/core-processor/src/main/java/io/micronaut/inject/processing/JavaModelUtils.java index 3f503fdda31..f14a34db08d 100644 --- a/core-processor/src/main/java/io/micronaut/inject/processing/JavaModelUtils.java +++ b/core-processor/src/main/java/io/micronaut/inject/processing/JavaModelUtils.java @@ -267,11 +267,11 @@ public static boolean isRecordComponent(Element e) { */ public static Type getTypeReference(TypedElement type) { ClassElement classElement = type.getType(); - if (type.isPrimitive()) { + if (classElement.isPrimitive()) { String internalName = NAME_TO_TYPE_MAP.get(classElement.getName()); - if (type.isArray()) { + if (classElement.isArray()) { StringBuilder name = new StringBuilder(internalName); - for (int i = 0; i < type.getArrayDimensions(); i++) { + for (int i = 0; i < classElement.getArrayDimensions(); i++) { name.insert(0, "["); } return Type.getObjectType(name.toString()); @@ -279,18 +279,18 @@ public static Type getTypeReference(TypedElement type) { return Type.getType(internalName); } } else { - Object nativeType = type.getNativeType(); + Object nativeType = classElement.getNativeType(); if (nativeType instanceof Class t) { return Type.getType(t); } else { - String internalName = type.getType().getName().replace('.', '/'); + String internalName = classElement.getName().replace('.', '/'); if (internalName.isEmpty()) { return Type.getType(Object.class); } - if (type.isArray()) { + if (classElement.isArray()) { StringBuilder name = new StringBuilder(internalName); name.insert(0, "L"); - for (int i = 0; i < type.getArrayDimensions(); i++) { + for (int i = 0; i < classElement.getArrayDimensions(); i++) { name.insert(0, "["); } name.append(";"); diff --git a/core-processor/src/main/java/io/micronaut/inject/writer/DispatchWriter.java b/core-processor/src/main/java/io/micronaut/inject/writer/DispatchWriter.java index 23b9af20570..b9a07bd8772 100644 --- a/core-processor/src/main/java/io/micronaut/inject/writer/DispatchWriter.java +++ b/core-processor/src/main/java/io/micronaut/inject/writer/DispatchWriter.java @@ -71,11 +71,19 @@ public final class DispatchWriter extends AbstractClassFileWriter implements Opc private final List dispatchTargets = new ArrayList<>(); private final Type thisType; + + private final Type dispatchSuperType; + private boolean hasInterceptedMethod; public DispatchWriter(Type thisType) { + this(thisType, ExecutableMethodsDefinitionWriter.SUPER_TYPE); + } + + public DispatchWriter(Type thisType, Type dispatchSuperType) { super(); this.thisType = thisType; + this.dispatchSuperType = dispatchSuperType; } /** @@ -118,7 +126,7 @@ public int addMethod(TypedElement declaringType, MethodElement methodElement) { * @return the target index */ public int addMethod(TypedElement declaringType, MethodElement methodElement, boolean useOneDispatch) { - return addDispatchTarget(new MethodDispatchTarget(declaringType, methodElement, useOneDispatch, !useOneDispatch)); + return addDispatchTarget(new MethodDispatchTarget(dispatchSuperType, declaringType, methodElement, useOneDispatch, !useOneDispatch)); } /** @@ -136,6 +144,7 @@ public int addInterceptedMethod(TypedElement declaringType, String interceptedProxyBridgeMethodName) { hasInterceptedMethod = true; return addDispatchTarget(new InterceptableMethodDispatchTarget( + dispatchSuperType, declaringType, methodElement, interceptedProxyClassName, @@ -475,15 +484,17 @@ public FieldElement getField() { @Internal @SuppressWarnings("FinalClass") public static class MethodDispatchTarget implements DispatchTarget { + final Type dispatchSuperType; final TypedElement declaringType; final MethodElement methodElement; final boolean oneDispatch; final boolean multiDispatch; - private MethodDispatchTarget(TypedElement declaringType, + private MethodDispatchTarget(Type dispatchSuperType, TypedElement declaringType, MethodElement methodElement, boolean oneDispatch, boolean multiDispatch) { + this.dispatchSuperType = dispatchSuperType; this.declaringType = declaringType; this.methodElement = methodElement; this.oneDispatch = oneDispatch; @@ -528,7 +539,7 @@ public void writeDispatchMulti(GeneratorAdapter writer, int methodIndex) { } writer.loadThis(); writer.push(methodIndex); - writer.invokeVirtual(ExecutableMethodsDefinitionWriter.SUPER_TYPE, GET_ACCESSIBLE_TARGET_METHOD); + writer.invokeVirtual(dispatchSuperType, GET_ACCESSIBLE_TARGET_METHOD); if (hasArgs) { writer.loadArg(2); } else { @@ -607,12 +618,13 @@ public static final class InterceptableMethodDispatchTarget extends MethodDispat final String interceptedProxyBridgeMethodName; final Type thisType; - private InterceptableMethodDispatchTarget(TypedElement declaringType, + private InterceptableMethodDispatchTarget(Type dispatchSuperType, + TypedElement declaringType, MethodElement methodElement, String interceptedProxyClassName, String interceptedProxyBridgeMethodName, Type thisType) { - super(declaringType, methodElement, false, true); + super(dispatchSuperType, declaringType, methodElement, false, true); this.interceptedProxyClassName = interceptedProxyClassName; this.interceptedProxyBridgeMethodName = interceptedProxyBridgeMethodName; this.thisType = thisType; diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 0ed30578232..481f7f443fb 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -66,7 +66,7 @@ managed-jackson = "2.14.0" managed-jackson-databind = "2.14.1" managed-maven-native-plugin = "0.9.13" managed-methvin-directory-watcher = "0.16.1" -managed-netty = "4.1.86.Final" +managed-netty = "4.1.87.Final" managed-reactive-streams = "1.0.4" # This should be kept aligned with https://github.com/micronaut-projects/micronaut-reactor/blob/master/gradle.properties from the BOM managed-reactor = "3.4.24" diff --git a/http-client-core/src/main/java/io/micronaut/http/client/interceptor/HttpClientIntroductionAdvice.java b/http-client-core/src/main/java/io/micronaut/http/client/interceptor/HttpClientIntroductionAdvice.java index 3b709fe96b5..52a9bfb79f3 100644 --- a/http-client-core/src/main/java/io/micronaut/http/client/interceptor/HttpClientIntroductionAdvice.java +++ b/http-client-core/src/main/java/io/micronaut/http/client/interceptor/HttpClientIntroductionAdvice.java @@ -88,7 +88,6 @@ import java.util.Map; import java.util.Optional; import java.util.concurrent.CompletableFuture; -import java.util.concurrent.atomic.AtomicReference; import java.util.function.Function; import java.util.function.Supplier; @@ -343,18 +342,23 @@ public Object intercept(MethodInvocationContext context) { Publisher csPublisher = httpClientResponsePublisher(httpClient, request, returnType, errorType, valueType); CompletableFuture future = new CompletableFuture<>(); csPublisher.subscribe(new CompletionAwareSubscriber() { - AtomicReference reference = new AtomicReference<>(); + Object message; + Subscription subscription; @Override protected void doOnSubscribe(Subscription subscription) { - subscription.request(1); + this.subscription = subscription; + subscription.request(Long.MAX_VALUE); } @Override protected void doOnNext(Object message) { if (Void.class != reactiveValueType) { - reference.set(message); + this.message = message; } + // we only want the first item + subscription.cancel(); + doOnComplete(); } @Override @@ -380,7 +384,8 @@ protected void doOnError(Throwable t) { @Override protected void doOnComplete() { - future.complete(reference.get()); + // can be called twice + future.complete(message); } }); return interceptedMethod.handleResult(future); diff --git a/http-netty/src/main/resources/META-INF/native-image/io.micronaut/micronaut-http-netty/native-image.properties b/http-netty/src/main/resources/META-INF/native-image/io.micronaut/micronaut-http-netty/native-image.properties new file mode 100644 index 00000000000..52279101dc6 --- /dev/null +++ b/http-netty/src/main/resources/META-INF/native-image/io.micronaut/micronaut-http-netty/native-image.properties @@ -0,0 +1 @@ +Args = --features=io.micronaut.http.netty.graal.HttpNettyFeature diff --git a/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/websocket/BinaryChatClientWebSocket.java b/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/websocket/BinaryChatClientWebSocket.java index 533483b0142..4d2ecf914dc 100644 --- a/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/websocket/BinaryChatClientWebSocket.java +++ b/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/websocket/BinaryChatClientWebSocket.java @@ -46,7 +46,7 @@ public void onOpen(String topic, String username, WebSocketSession session) { this.topic = topic; this.username = username; this.session = session; - System.out.println("Client session opened for username = " + username); + System.out.println("Client session " + session.getId() + " opened for username = " + username); } public String getTopic() { @@ -72,7 +72,7 @@ public WebSocketSession getSession() { @OnMessage public void onMessage( byte[] message) { - System.out.println("Client received message = " + new String(message)); + System.out.println("Client " + username + " received message = " + new String(message)); replies.add(new String(message)); } diff --git a/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/websocket/BinaryChatServerWebSocket.java b/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/websocket/BinaryChatServerWebSocket.java index 49b581b7d64..141e417db33 100644 --- a/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/websocket/BinaryChatServerWebSocket.java +++ b/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/websocket/BinaryChatServerWebSocket.java @@ -36,7 +36,7 @@ public void onOpen(String topic, String username, WebSocketSession session) { if(isValid(topic, session, openSession)) { String msg = "[" + username + "] Joined!"; System.out.println("Server sending msg = " + msg); - openSession.sendSync(msg.getBytes()); + openSession.sendAsync(msg.getBytes()); } } } @@ -55,7 +55,7 @@ public void onMessage( if(isValid(topic, session, openSession)) { String msg = "[" + username + "] " + new String(message); System.out.println("Server sending msg = " + msg); - openSession.sendSync(msg.getBytes()); + openSession.sendAsync(msg.getBytes()); } } } @@ -72,7 +72,7 @@ public void onClose( if(isValid(topic, session, openSession)) { String msg = "[" + username + "] Disconnected!"; System.out.println("Server sending msg = " + msg); - openSession.sendSync(msg.getBytes()); + openSession.sendAsync(msg.getBytes()); } } } diff --git a/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/websocket/BinaryWebSocketSpec.groovy b/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/websocket/BinaryWebSocketSpec.groovy index 4fc32dac55c..66c59ced00c 100644 --- a/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/websocket/BinaryWebSocketSpec.groovy +++ b/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/websocket/BinaryWebSocketSpec.groovy @@ -34,17 +34,14 @@ import jakarta.inject.Singleton import reactor.core.publisher.Flux import reactor.core.publisher.Mono import spock.lang.Issue -import spock.lang.Retry import spock.lang.Specification import spock.util.concurrent.PollingConditions import java.nio.ByteBuffer import java.nio.charset.StandardCharsets -@Retry class BinaryWebSocketSpec extends Specification { - @Retry void "test binary websocket exchange"() { given: EmbeddedServer embeddedServer = ApplicationContext.builder('micronaut.server.netty.log-level':'TRACE').run(EmbeddedServer) @@ -71,7 +68,6 @@ class BinaryWebSocketSpec extends Specification { fred.replies.size() == 1 } - when:"A message is sent" fred.send("Hello bob!".bytes) @@ -88,7 +84,6 @@ class BinaryWebSocketSpec extends Specification { then: conditions.eventually { - fred.replies.contains("[bob] Hi fred. How are things?") fred.replies.size() == 2 bob.replies.contains("[fred] Hello bob!") @@ -101,8 +96,6 @@ class BinaryWebSocketSpec extends Specification { when: bob.close() - sleep(1000) - then: conditions.eventually { diff --git a/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/cors/CorsSimpleRequestTest.java b/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/cors/CorsSimpleRequestTest.java index bed3249894c..aff8053c321 100644 --- a/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/cors/CorsSimpleRequestTest.java +++ b/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/cors/CorsSimpleRequestTest.java @@ -52,6 +52,7 @@ public class CorsSimpleRequestTest { private static final String SPECNAME = "CorsSimpleRequestTest"; private static final String PROPERTY_MICRONAUT_SERVER_CORS_ENABLED = "micronaut.server.cors.enabled"; + private static final String PROPERTY_MICRONAUT_SERVER_CORS_LOCALHOST_PASS_THROUGH = "micronaut.server.cors.localhost-pass-through"; /** * @see GHSA-583g-g682-crxf @@ -75,6 +76,24 @@ void corsSimpleRequestNotAllowedForLocalhostAndAny() throws IOException { ); } + /** + * Test that a simple request is allowed for localhost and origin:any when specifically turned off. + * @see PR-8751 + * + * @throws IOException + */ + @Test + void corsSimpleRequestAllowedForLocalhostAndAnyWhenSpecificallyTurnedOff() throws IOException { + asserts(SPECNAME, + CollectionUtils.mapOf( + PROPERTY_MICRONAUT_SERVER_CORS_ENABLED, StringUtils.TRUE, + PROPERTY_MICRONAUT_SERVER_CORS_LOCALHOST_PASS_THROUGH, StringUtils.TRUE + ), + createRequest("https://foo.com"), + CorsSimpleRequestTest::isSuccessful + ); + } + /** * @see GHSA-583g-g682-crxf * diff --git a/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/cors/SimpleRequestWithCorsNotEnabledTest.java b/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/cors/SimpleRequestWithCorsNotEnabledTest.java index 618b37bfcaa..285663ce22a 100644 --- a/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/cors/SimpleRequestWithCorsNotEnabledTest.java +++ b/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/cors/SimpleRequestWithCorsNotEnabledTest.java @@ -18,6 +18,7 @@ import io.micronaut.context.annotation.Requires; import io.micronaut.context.event.ApplicationEventListener; import io.micronaut.context.event.ApplicationEventPublisher; +import io.micronaut.core.util.StringUtils; import io.micronaut.http.HttpRequest; import io.micronaut.http.HttpStatus; import io.micronaut.http.annotation.Controller; @@ -43,7 +44,9 @@ "checkstyle:MissingJavadocType", }) public class SimpleRequestWithCorsNotEnabledTest { + private static final String SPECNAME = "SimpleRequestWithCorsNotEnabledTest"; + private static final String PROPERTY_MICRONAUT_SERVER_CORS_LOCALHOST_PASS_THROUGH = "micronaut.server.cors.localhost-pass-through"; /** * @see GHSA-583g-g682-crxf @@ -66,6 +69,27 @@ void corsSimpleRequestNotAllowedForLocalhostAndAny() throws IOException { }); } + /** + * This test verifies a CORS simple request is allowed when invoked against a Micronaut application running in localhost without cors enabled but with localhost-pass-through switched on. + * @see PR-8751 + * + * @throws IOException + */ + @Test + void corsSimpleRequestAllowedForLocalhostAndAnyWhenConfiguredToAllowIt() throws IOException { + asserts(SPECNAME, + Collections.singletonMap(PROPERTY_MICRONAUT_SERVER_CORS_LOCALHOST_PASS_THROUGH, StringUtils.TRUE), + createRequest("https://sdelamo.github.io"), + (server, request) -> { + RefreshCounter refreshCounter = server.getApplicationContext().getBean(RefreshCounter.class); + assertEquals(0, refreshCounter.getRefreshCount()); + AssertionUtils.assertDoesNotThrow(server, request, HttpResponseAssertion.builder() + .status(HttpStatus.OK) + .build()); + assertEquals(1, refreshCounter.getRefreshCount()); + }); + } + /** * It should not deny a cors request coming from a localhost origin if the micronaut application resolved host is localhost. * @throws IOException scenario step fails diff --git a/http-server/src/main/java/io/micronaut/http/server/HttpServerConfiguration.java b/http-server/src/main/java/io/micronaut/http/server/HttpServerConfiguration.java index 18f515beefa..53a90d61ac8 100644 --- a/http-server/src/main/java/io/micronaut/http/server/HttpServerConfiguration.java +++ b/http-server/src/main/java/io/micronaut/http/server/HttpServerConfiguration.java @@ -664,9 +664,11 @@ public static class CorsConfiguration implements Toggleable { public static final boolean DEFAULT_ENABLED = false; public static final boolean DEFAULT_SINGLE_HEADER = false; + public static final boolean DEFAULT_LOCALHOST_PASS_THROUGH = false; private boolean enabled = DEFAULT_ENABLED; private boolean singleHeader = DEFAULT_SINGLE_HEADER; + private boolean localhostPassThrough = DEFAULT_LOCALHOST_PASS_THROUGH; private Map configurations = Collections.emptyMap(); @@ -680,6 +682,14 @@ public boolean isEnabled() { return enabled; } + /** + * @return Whether localhost pass-through is enabled. Defaults to {@value #DEFAULT_LOCALHOST_PASS_THROUGH}. + * @since 3.8.5 + */ + public boolean isLocalhostPassThrough() { + return localhostPassThrough; + } + /** * @return The cors configurations */ @@ -708,6 +718,17 @@ public void setEnabled(boolean enabled) { this.enabled = enabled; } + /** + * Sets whether localhost pass-through is enabled. Default value {@value #DEFAULT_LOCALHOST_PASS_THROUGH}. + * Setting this to true will allow requests to be made to localhost from any origin. + * + * @param localhostPassThrough True if localhost pass-through is enabled + * @since 3.8.5 + */ + public void setLocalhostPassThrough(boolean localhostPassThrough) { + this.localhostPassThrough = localhostPassThrough; + } + /** * Sets the CORS configurations. * @param configurations The CORS configurations diff --git a/http-server/src/main/java/io/micronaut/http/server/cors/CorsFilter.java b/http-server/src/main/java/io/micronaut/http/server/cors/CorsFilter.java index 10c3dbdd0ca..e8f2304b865 100644 --- a/http-server/src/main/java/io/micronaut/http/server/cors/CorsFilter.java +++ b/http-server/src/main/java/io/micronaut/http/server/cors/CorsFilter.java @@ -115,6 +115,9 @@ public Publisher> doFilter(HttpRequest request, Server */ protected boolean shouldDenyToPreventDriveByLocalhostAttack(@NonNull CorsOriginConfiguration corsOriginConfiguration, @NonNull HttpRequest request) { + if (corsConfiguration.isLocalhostPassThrough()) { + return false; + } if (httpHostResolver == null) { return false; } @@ -137,6 +140,9 @@ protected boolean shouldDenyToPreventDriveByLocalhostAttack(@NonNull CorsOriginC */ protected boolean shouldDenyToPreventDriveByLocalhostAttack(@NonNull String origin, @NonNull HttpRequest request) { + if (corsConfiguration.isLocalhostPassThrough()) { + return false; + } if (httpHostResolver == null) { return false; } diff --git a/inject-java-test/src/test/groovy/io/micronaut/inject/beanimport/BeanImportSpec.groovy b/inject-java-test/src/test/groovy/io/micronaut/inject/beanimport/BeanImportSpec.groovy index 51175c8f5f1..2243e3e8385 100644 --- a/inject-java-test/src/test/groovy/io/micronaut/inject/beanimport/BeanImportSpec.groovy +++ b/inject-java-test/src/test/groovy/io/micronaut/inject/beanimport/BeanImportSpec.groovy @@ -9,9 +9,39 @@ import io.smallrye.faulttolerance.DefaultExistingCircuitBreakerNames import io.smallrye.faulttolerance.DefaultFallbackHandlerProvider import io.smallrye.faulttolerance.DefaultFaultToleranceOperationProvider import io.smallrye.faulttolerance.ExecutorHolder +import jakarta.inject.Named + +import java.nio.charset.StandardCharsets class BeanImportSpec extends AbstractTypeElementSpec { + void "test bean import with primitive array constructor"() { + given: + ApplicationContext context = buildContext(''' +package beanimporttest1; + +import io.micronaut.context.annotation.*; +import jakarta.inject.Named; +import java.nio.charset.StandardCharsets; + +@Import(classes=io.micronaut.inject.beanimport.UpstreamByteConstructorBean.class) +class Application {} + +@Factory +class BytesFactory { + @Bean + @Named("some-bytes") + byte[] myBytes() { + return "test".getBytes(StandardCharsets.UTF_8); + } +} +''') + def bean = context.getBean(UpstreamByteConstructorBean) + + expect: + bean.toString() == 'test' + } + void 'test bean import for package'() { given: ApplicationContext context = buildContext(''' @@ -76,3 +106,16 @@ class Application {} context.close() } } +class UpstreamByteConstructorBean { + + private final byte[] bytes; + + public UpstreamByteConstructorBean(@Named("some-bytes") byte[] bytes) { + this.bytes = bytes; + } + + @Override + public String toString() { + return new String(bytes, StandardCharsets.UTF_8); + } +} diff --git a/inject-java-test/src/test/groovy/io/micronaut/inject/visitor/beans/Auditable.java b/inject-java-test/src/test/groovy/io/micronaut/inject/visitor/beans/Auditable.java new file mode 100644 index 00000000000..ec718a37647 --- /dev/null +++ b/inject-java-test/src/test/groovy/io/micronaut/inject/visitor/beans/Auditable.java @@ -0,0 +1,20 @@ +package io.micronaut.inject.visitor.beans; + +import io.micronaut.context.annotation.Executable; + +import java.time.Instant; + +public abstract class Auditable { + + private Instant updatedAt; + + @Executable(processOnStartup = true) + protected void beforeInsert() { + updatedAt = Instant.now(); + } + + public Instant getUpdatedAt() { + return updatedAt; + } +} + diff --git a/inject-java-test/src/test/groovy/io/micronaut/inject/visitor/beans/BeanIntrospectionSpec.groovy b/inject-java-test/src/test/groovy/io/micronaut/inject/visitor/beans/BeanIntrospectionSpec.groovy index ad0b2f07c2b..6d571bc0dd8 100644 --- a/inject-java-test/src/test/groovy/io/micronaut/inject/visitor/beans/BeanIntrospectionSpec.groovy +++ b/inject-java-test/src/test/groovy/io/micronaut/inject/visitor/beans/BeanIntrospectionSpec.groovy @@ -5,6 +5,7 @@ import com.fasterxml.jackson.annotation.JsonClassDescription import com.fasterxml.jackson.annotation.JsonCreator import com.fasterxml.jackson.annotation.JsonProperty import com.fasterxml.jackson.databind.ObjectMapper +import groovy.transform.PackageScope import io.micronaut.annotation.processing.TypeElementVisitorProcessor import io.micronaut.annotation.processing.test.AbstractTypeElementSpec import io.micronaut.annotation.processing.test.JavaParser @@ -44,6 +45,7 @@ import javax.validation.constraints.Min import javax.validation.constraints.NotBlank import javax.validation.constraints.Size import java.lang.reflect.Field +import java.time.Instant class BeanIntrospectionSpec extends AbstractTypeElementSpec { @@ -121,12 +123,47 @@ class Test { def prop = introspection.getRequiredProperty("foo", Optional) test.foo = 'value' - then:'the write method is not considered to match the getter/setter pair' + then: 'the write method is not considered to match the getter/setter pair' prop.get(test).get() == 'value' prop.type == Optional prop.isReadOnly() } + @Issue("https://github.com/micronaut-projects/micronaut-core/issues/8657") + void "test executable method on abstract class with introspection"() { + when: + def introspection = buildBeanIntrospection('issue8657.Test', ''' +package issue8657; + +import io.micronaut.core.annotation.Introspected; +import io.micronaut.context.annotation.Executable; +import io.micronaut.inject.visitor.beans.Auditable; + +@Introspected +class Test extends Auditable { + private String name; + public void setName(String name) { + this.name = name; + } + public String getName() { + return name; + } +} + +''') + + then: + introspection.beanMethods.size() == 1 + + when: + def bean = introspection.instantiate() + def method = introspection.beanMethods.first() + method.invoke(bean) + + then: + bean.updatedAt != null + } + void "test generics in arrays don't stack overflow"() { given: def introspection = buildBeanIntrospection('arraygenerics.Test', ''' diff --git a/inject/src/main/java/io/micronaut/context/AbstractExecutableMethodsDefinition.java b/inject/src/main/java/io/micronaut/context/AbstractExecutableMethodsDefinition.java index 56c249d9d8e..5984ac1e81f 100644 --- a/inject/src/main/java/io/micronaut/context/AbstractExecutableMethodsDefinition.java +++ b/inject/src/main/java/io/micronaut/context/AbstractExecutableMethodsDefinition.java @@ -46,7 +46,6 @@ public abstract class AbstractExecutableMethodsDefinition implements Executab private final DispatchedExecutableMethod[] executableMethods; private Environment environment; private List> executableMethodsList; - private Method[] reflectiveMethods; protected AbstractExecutableMethodsDefinition(MethodReference[] methodsReferences) { this.methodsReferences = methodsReferences; @@ -163,17 +162,11 @@ protected Object dispatch(int index, T target, Object[] args) { // this logic must allow reflection @SuppressWarnings("java:S3011") protected final Method getAccessibleTargetMethodByIndex(int index) { - if (reflectiveMethods == null) { - reflectiveMethods = new Method[methodsReferences.length]; - } - Method method = reflectiveMethods[index]; - if (method == null) { - method = getTargetMethodByIndex(index); - if (ClassUtils.REFLECTION_LOGGER.isDebugEnabled()) { - ClassUtils.REFLECTION_LOGGER.debug("Reflectively accessing method {} of type {}", method, method.getDeclaringClass()); - } - method.setAccessible(true); + Method method = getTargetMethodByIndex(index); + if (ClassUtils.REFLECTION_LOGGER.isDebugEnabled()) { + ClassUtils.REFLECTION_LOGGER.debug("Reflectively accessing method {} of type {}", method, method.getDeclaringClass()); } + method.setAccessible(true); return method; } diff --git a/inject/src/main/java/io/micronaut/inject/beans/AbstractInitializableBeanIntrospection.java b/inject/src/main/java/io/micronaut/inject/beans/AbstractInitializableBeanIntrospection.java index 9cc9019c321..fff93d3135e 100644 --- a/inject/src/main/java/io/micronaut/inject/beans/AbstractInitializableBeanIntrospection.java +++ b/inject/src/main/java/io/micronaut/inject/beans/AbstractInitializableBeanIntrospection.java @@ -119,6 +119,35 @@ protected BeanProperty getPropertyByIndex(int index) { return beanProperties.get(index); } + /** + * Find {@link Method} representation at the method by index. Used by {@link ExecutableMethod#getTargetMethod()}. + * + * @param index The index + * @return The method + */ + @UsedByGeneratedCode + @Internal + protected abstract Method getTargetMethodByIndex(int index); + + /** + * Find {@link Method} representation at the method by index. Used by {@link ExecutableMethod#getTargetMethod()}. + * + * @param index The index + * @return The method + * @since 3.8.5 + */ + @UsedByGeneratedCode + // this logic must allow reflection + @SuppressWarnings("java:S3011") + protected final Method getAccessibleTargetMethodByIndex(int index) { + Method method = getTargetMethodByIndex(index); + if (ClassUtils.REFLECTION_LOGGER.isDebugEnabled()) { + ClassUtils.REFLECTION_LOGGER.debug("Reflectively accessing method {} of type {}", method, method.getDeclaringClass()); + } + method.setAccessible(true); + return method; + } + /** * Triggers the invocation of the method at index. * @@ -577,7 +606,7 @@ public Method getTargetMethod() { if (ClassUtils.REFLECTION_LOGGER.isWarnEnabled()) { ClassUtils.REFLECTION_LOGGER.warn("Using getTargetMethod for method {} on type {} requires the use of reflection. GraalVM configuration necessary", getName(), getDeclaringType()); } - return ReflectionUtils.getRequiredMethod(getDeclaringType(), getMethodName(), getArgumentTypes()); + return getTargetMethodByIndex(ref.methodIndex); } @Override diff --git a/inject/src/main/resources/META-INF/native-image/io.micronaut/micronaut-inject/native-image.properties b/inject/src/main/resources/META-INF/native-image/io.micronaut/micronaut-inject/native-image.properties index 034ea152e0a..1d57fe9cf7e 100644 --- a/inject/src/main/resources/META-INF/native-image/io.micronaut/micronaut-inject/native-image.properties +++ b/inject/src/main/resources/META-INF/native-image/io.micronaut/micronaut-inject/native-image.properties @@ -18,4 +18,5 @@ Args = -H:EnableURLProtocols=http,https \ --initialize-at-build-time=io.micronaut.context.annotation \ --initialize-at-build-time=io.micronaut.inject.annotation \ --initialize-at-build-time=io.micronaut.runtime.converters.time \ + --initialize-at-run-time=io.micronaut.inject.provider.JakartaProviderBeanDefinition \ --initialize-at-run-time=io.micronaut.context.env.CachedEnvironment diff --git a/settings.gradle b/settings.gradle index 9fc71b3bc00..bb5ffa56320 100644 --- a/settings.gradle +++ b/settings.gradle @@ -14,7 +14,7 @@ pluginManagement { } plugins { - id 'io.micronaut.build.shared.settings' version '6.2.1' + id 'io.micronaut.build.shared.settings' version '6.2.2' } enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") diff --git a/src/main/docs/guide/cloud/cloudConfiguration/distributedConfigurationAwsParameterStore.adoc b/src/main/docs/guide/cloud/cloudConfiguration/distributedConfigurationAwsParameterStore.adoc index fdcd3e4ad10..2ba77b40ebb 100644 --- a/src/main/docs/guide/cloud/cloudConfiguration/distributedConfigurationAwsParameterStore.adoc +++ b/src/main/docs/guide/cloud/cloudConfiguration/distributedConfigurationAwsParameterStore.adoc @@ -4,7 +4,6 @@ dependency:io.micronaut.aws:micronaut-aws-parameter-store[] To enable distributed configuration, make sure <> is enabled and create a `src/main/resources/bootstrap.yml` file with the following configuration: -.bootstrap.yml [configuration] ---- micronaut: diff --git a/src/main/docs/guide/cloud/cloudConfiguration/distributedConfigurationSpringCloud.adoc b/src/main/docs/guide/cloud/cloudConfiguration/distributedConfigurationSpringCloud.adoc index ac469b78382..5cccfd2de9d 100644 --- a/src/main/docs/guide/cloud/cloudConfiguration/distributedConfigurationSpringCloud.adoc +++ b/src/main/docs/guide/cloud/cloudConfiguration/distributedConfigurationSpringCloud.adoc @@ -15,10 +15,13 @@ spring: config: enabled: true uri: http://localhost:8888/ - retry-attempts: 4 # optional, number of times to retry - retry-delay: 2s # optional, delay between retries + retry-attempts: 4 + retry-delay: 2s ---- +- `retry-attempts` is optional, and specifies the number of times to retry +- `retry-delay` is optional, and specifies the delay between retries + Micronaut uses the configured `micronaut.application.name` to look up property sources for the application from Spring Cloud config server configured via `spring.cloud.config.uri`. See the https://spring.io/projects/spring-cloud-config#learn[Documentation for Spring Cloud Config Server] for more information on how to set up the server. diff --git a/src/main/docs/guide/cloud/serviceDiscovery/serviceDiscoveryManual.adoc b/src/main/docs/guide/cloud/serviceDiscovery/serviceDiscoveryManual.adoc index 8b8ef5e260a..d9f16e610d8 100644 --- a/src/main/docs/guide/cloud/serviceDiscovery/serviceDiscoveryManual.adoc +++ b/src/main/docs/guide/cloud/serviceDiscovery/serviceDiscoveryManual.adoc @@ -29,13 +29,13 @@ micronaut: http: services: foo: - health-check: true # <1> - health-check-interval: 15s # <2> - health-check-uri: /health # <3> + health-check: true + health-check-interval: 15s + health-check-uri: /health ---- -<1> Whether to health check the service -<2> The interval between checks -<3> The URI of the health check request +- `health-check` indicates whether to health check the service +- `health-check-interval` is the interval between checks +- `health-check-uri` specifies the endpoint URI of the health check request Micronaut starts a background thread to check the health status of the service and if any of the configured services respond with an error code, they are removed from the list of available services. diff --git a/src/main/docs/guide/cloud/serviceDiscovery/serviceDiscoveryRoute53.adoc b/src/main/docs/guide/cloud/serviceDiscovery/serviceDiscoveryRoute53.adoc index 0abbf27b53c..44c72040c42 100644 --- a/src/main/docs/guide/cloud/serviceDiscovery/serviceDiscoveryRoute53.adoc +++ b/src/main/docs/guide/cloud/serviceDiscovery/serviceDiscoveryRoute53.adoc @@ -13,7 +13,7 @@ Here are the steps: 3. Add health checks or custom health checks (optional) 4. Add Service ID to your application configuration file like so: -.Sample application.yml +.Sample application configuration [configuration] ---- aws: @@ -33,7 +33,6 @@ dependency:io.micronaut.aws:micronaut-aws-route53[] 6. On the client side, you need the same dependencies and fewer configuration options: -.Sample application.yml [configuration] ---- aws: diff --git a/src/main/docs/guide/config/eachProperty.adoc b/src/main/docs/guide/config/eachProperty.adoc index 5d178d7d6ed..23d71b373b1 100644 --- a/src/main/docs/guide/config/eachProperty.adoc +++ b/src/main/docs/guide/config/eachProperty.adoc @@ -1,6 +1,6 @@ The link:{api}/io/micronaut/context/annotation/ConfigurationProperties.html[@ConfigurationProperties] annotation is great for a single configuration class, but sometimes you want multiple instances, each with its own distinct configuration. That is where link:{api}/io/micronaut/context/annotation/EachProperty.html[EachProperty] comes in. -The ann:context.annotation.EachProperty[] annotation creates a `ConfigurationProperties` bean for each sub-property within the given property. As an example consider the following class: +The ann:context.annotation.EachProperty[] annotation creates a `ConfigurationProperties` bean for each sub-property within the given name. As an example consider the following class: snippet::io.micronaut.docs.config.env.DataSourceConfiguration[tags="eachProperty", indent=0, title="Using @EachProperty"] diff --git a/src/main/docs/guide/config/valueAnnotation.adoc b/src/main/docs/guide/config/valueAnnotation.adoc index 3a5c892767a..14ff27ba646 100644 --- a/src/main/docs/guide/config/valueAnnotation.adoc +++ b/src/main/docs/guide/config/valueAnnotation.adoc @@ -91,7 +91,6 @@ The above instead injects the value of the `my.url` property resolved from appli You can also use this feature to resolve sub maps. For example, consider the following configuration: -.Example `application.yml` configuration [configuration] ---- datasources: diff --git a/src/main/docs/guide/httpClient/clientAnnotation/clientHeaders.adoc b/src/main/docs/guide/httpClient/clientAnnotation/clientHeaders.adoc index fe22c573b4b..df59fc0ba07 100644 --- a/src/main/docs/guide/httpClient/clientAnnotation/clientHeaders.adoc +++ b/src/main/docs/guide/httpClient/clientAnnotation/clientHeaders.adoc @@ -12,7 +12,7 @@ The above example defines a ann:http.annotation.Header[] annotation on the `PetC Then set the following in your configuration file (e.g `application.yml`) to populate the value: -.Configuring Headers in YAML +.Configuring Headers [configuration] ---- pet: diff --git a/src/main/docs/guide/httpClient/lowLevelHttpClient/clientConfiguration.adoc b/src/main/docs/guide/httpClient/lowLevelHttpClient/clientConfiguration.adoc index 1f93d67f502..6e67ad3ee8e 100644 --- a/src/main/docs/guide/httpClient/lowLevelHttpClient/clientConfiguration.adoc +++ b/src/main/docs/guide/httpClient/lowLevelHttpClient/clientConfiguration.adoc @@ -27,10 +27,10 @@ micronaut: urls: - http://foo1 - http://foo2 - read-timeout: 5s # <1> + read-timeout: 5s ---- -<1> The read timeout is applied to the `foo` client. +- The `read-timeout` is applied to the `foo` client. WARN: This client configuration can be used in conjunction with the `@Client` annotation, either by injecting an `HttpClient` directly or use on a client interface. In any case, all other attributes on the annotation *will be ignored* except the service id. @@ -98,10 +98,13 @@ micronaut: - http://foo1 - http://foo2 pool: - max-concurrent-http1-connections: 50 # <1> + max-concurrent-http1-connections: 50 + enabled: true + max-connections: 50 ---- -<1> Limit maximum concurrent HTTP/1.1 connections +- `max-concurrent-http1-connections` limits the maximum concurrent HTTP/1.1 connections to 50. +- `pool` enables the pool and sets the maximum number of connections for it See the API for link:{api}/io/micronaut/http/client/HttpClientConfiguration.ConnectionPoolConfiguration.html[ConnectionPoolConfiguration] for details on available pool configuration options. diff --git a/src/main/docs/guide/httpServer/apiVersioning.adoc b/src/main/docs/guide/httpServer/apiVersioning.adoc index 966fecb30ae..b2ae1ce9520 100644 --- a/src/main/docs/guide/httpServer/apiVersioning.adoc +++ b/src/main/docs/guide/httpServer/apiVersioning.adoc @@ -26,22 +26,22 @@ By default Micronaut has two strategies for resolving the version based on an HT micronaut: router: versioning: - enabled: true <1> + enabled: true parameter: - enabled: false # <2> - names: 'v,api-version' # <3> + enabled: false + names: 'v,api-version' header: - enabled: true # <4> - names: # <5> + enabled: true + names: - 'X-API-VERSION' - 'Accept-Version' ---- -<1> Enables versioning -<2> Enables or disables parameter-based versioning -<3> Specify the parameter names as a comma-separated list -<4> Enables or disables header-based versioning -<5> Specify the header names as a list +- This example enables versioning +- `parameter.enabled` enables or disables parameter-based versioning +- `parameter.names` specifies the parameter names as a comma-separated list +- `header.enabled` enables or disables header-based versioning +- `header.names` specifies the header names as a list If this is not enough you can also implement the api:web.router.version.resolution.RequestVersionResolver[] interface which receives the api:http.HttpRequest[] and can implement any strategy you choose. @@ -56,10 +56,10 @@ micronaut: router: versioning: enabled: true - default-version: 3 <1> + default-version: 3 ---- -<1> Sets the default version +- This example enables versioning and sets the default version A route is *not* matched if the following conditions are met: diff --git a/src/main/docs/guide/httpServer/http2Server.adoc b/src/main/docs/guide/httpServer/http2Server.adoc index 106baae965e..1eb3a5bdbc1 100644 --- a/src/main/docs/guide/httpServer/http2Server.adoc +++ b/src/main/docs/guide/httpServer/http2Server.adoc @@ -31,16 +31,16 @@ For production, see the <> section of the documentatio Note that if your deployment environment uses JDK 8, or for improved support for OpenSSL, define the following dependencies on Netty Tomcat Native: -dependency:io.netty:netty-tcnative:2.0.46.Final[scope="runtimeOnly"] +dependency:io.netty:netty-tcnative:2.0.58.Final[scope="runtimeOnly"] -dependency:io.netty:netty-tcnative-boringssl-static:2.0.46.Final[scope="runtimeOnly"] +dependency:io.netty:netty-tcnative-boringssl-static:2.0.58.Final[scope="runtimeOnly"] In addition to a dependency on the appropriate native library for your architecture. For example: .Configuring Tomcat Native [source,groovy] ---- -runtimeOnly "io.netty:netty-tcnative-boringssl-static:2.0.46.Final:${Os.isFamily(Os.FAMILY_MAC) ? (Os.isArch("aarch64") ? "osx-aarch_64" : "osx-x86_64") : 'linux-x86_64'}" +runtimeOnly "io.netty:netty-tcnative-boringssl-static:2.0.58.Final:${Os.isFamily(Os.FAMILY_MAC) ? (Os.isArch("aarch64") ? "osx-aarch_64" : "osx-x86_64") : 'linux-x86_64'}" ---- See the documentation on https://netty.io/wiki/forked-tomcat-native.html[Tomcat Native] for more information. diff --git a/src/main/docs/guide/httpServer/runningSpecificPort.adoc b/src/main/docs/guide/httpServer/runningSpecificPort.adoc index 83ff4816656..137dda39e8c 100644 --- a/src/main/docs/guide/httpServer/runningSpecificPort.adoc +++ b/src/main/docs/guide/httpServer/runningSpecificPort.adoc @@ -1,6 +1,6 @@ By default the server runs on port 8080. However, you can set the server to run on a specific port: -[source, yaml] +[configuration] ---- micronaut: server: @@ -11,7 +11,7 @@ TIP: This is also configurable from an environment variable, e.g. `MICRONAUT_SER To run on a random port: -[source, yaml] +[configuration] ---- micronaut: server: diff --git a/src/main/docs/guide/httpServer/serverConfiguration.adoc b/src/main/docs/guide/httpServer/serverConfiguration.adoc index 1a9fc0404ee..16971d46536 100644 --- a/src/main/docs/guide/httpServer/serverConfiguration.adoc +++ b/src/main/docs/guide/httpServer/serverConfiguration.adoc @@ -8,18 +8,18 @@ The following example shows how to tweak configuration options for the server vi micronaut: server: maxRequestSize: 1MB - host: localhost # <1> + host: localhost netty: - maxHeaderSize: 500KB # <2> + maxHeaderSize: 500KB worker: - threads: 8 # <3> + threads: 8 childOptions: - autoRead: true # <4> + autoRead: true ---- -<1> By default Micronaut binds to all network interfaces. Use `localhost` to bind only to loopback network interface -<2> Maximum size for headers -<3> Number of Netty worker threads -<4> Auto read request body +- By default Micronaut binds to all network interfaces. Use `localhost` to bind only to loopback network interface +- `maxHeaderSize` sets the maximum size for headers +- `worker.threads` specifies the number of Netty worker threads +- `autoRead` enables request body auto read include::{includedir}configurationProperties/io.micronaut.http.server.netty.configuration.NettyHttpServerConfiguration.adoc[] diff --git a/src/main/docs/guide/httpServer/serverConfiguration/accessLogger.adoc b/src/main/docs/guide/httpServer/serverConfiguration/accessLogger.adoc index 9e6f8fec89f..34da0cf2cc9 100644 --- a/src/main/docs/guide/httpServer/serverConfiguration/accessLogger.adoc +++ b/src/main/docs/guide/httpServer/serverConfiguration/accessLogger.adoc @@ -9,11 +9,15 @@ micronaut: server: netty: access-logger: - enabled: true # Enables the access logger - logger-name: my-access-logger # A logger name, optional, default is `HTTP_ACCESS_LOGGER` - log-format: common # A log format, optional, default is Common Log Format + enabled: true + logger-name: my-access-logger + log-format: common ---- +- `enabled` Enables the access logger +- optionally specify a `logger-name`, which defaults to `HTTP_ACCESS_LOGGER` +- optionally specify a `log-format`, which defaults to the Common Log Format + ==== Filtering access logs If you wish to not log access to certain paths, you can specify regular expression filters in the configuration: @@ -25,14 +29,19 @@ micronaut: server: netty: access-logger: - enabled: true # Enables the access logger - logger-name: my-access-logger # A logger name, optional, default is `HTTP_ACCESS_LOGGER` - log-format: common # A log format, optional, default is Common Log Format + enabled: true + logger-name: my-access-logger + log-format: common exclusions: - /health - /path/.+ ---- +- `enabled` Enables the access logger +- optionally specify a `logger-name`, which defaults to `HTTP_ACCESS_LOGGER` +- optionally specify a `log-format`, which defaults to the Common Log Format + + ==== Logback Configuration In addition to enabling the access logger, you must add a logger for the specified or default logger name. For instance using the default logger name for logback: diff --git a/src/main/docs/guide/httpServer/serverConfiguration/cors/corsAllowCredentials.adoc b/src/main/docs/guide/httpServer/serverConfiguration/cors/corsAllowCredentials.adoc index f976c613a40..65c5b6b121e 100644 --- a/src/main/docs/guide/httpServer/serverConfiguration/cors/corsAllowCredentials.adoc +++ b/src/main/docs/guide/httpServer/serverConfiguration/cors/corsAllowCredentials.adoc @@ -1,7 +1,7 @@ Credentials are allowed by default for CORS requests. To disallow credentials, set the `allowCredentials` option to `false`. .Example CORS Configuration -[source,yaml] +[configuration] ---- micronaut: server: diff --git a/src/main/docs/guide/httpServer/serverConfiguration/cors/corsAllowedHeaders.adoc b/src/main/docs/guide/httpServer/serverConfiguration/cors/corsAllowedHeaders.adoc index f4c215d0472..ea08df88177 100644 --- a/src/main/docs/guide/httpServer/serverConfiguration/cors/corsAllowedHeaders.adoc +++ b/src/main/docs/guide/httpServer/serverConfiguration/cors/corsAllowedHeaders.adoc @@ -3,7 +3,7 @@ To allow any request header for a given configuration, don't include the `allowe For multiple allowed headers, set the `allowedHeaders` key of the configuration to a list of strings. .Example CORS Configuration -[source,yaml] +[configuration] ---- micronaut: server: diff --git a/src/main/docs/guide/httpServer/serverConfiguration/cors/corsAllowedMethods.adoc b/src/main/docs/guide/httpServer/serverConfiguration/cors/corsAllowedMethods.adoc index 1d4c963efd1..a2b033ae3d5 100644 --- a/src/main/docs/guide/httpServer/serverConfiguration/cors/corsAllowedMethods.adoc +++ b/src/main/docs/guide/httpServer/serverConfiguration/cors/corsAllowedMethods.adoc @@ -3,7 +3,7 @@ To allow any request method for a given configuration, don't include the `allowe For multiple allowed methods, set the `allowedMethods` key of the configuration to a list of strings. .Example CORS Configuration -[source,yaml] +[configuration] ---- micronaut: server: diff --git a/src/main/docs/guide/httpServer/serverConfiguration/cors/corsAllowedOrigins.adoc b/src/main/docs/guide/httpServer/serverConfiguration/cors/corsAllowedOrigins.adoc index 50dbf256c61..a1a10186380 100644 --- a/src/main/docs/guide/httpServer/serverConfiguration/cors/corsAllowedOrigins.adoc +++ b/src/main/docs/guide/httpServer/serverConfiguration/cors/corsAllowedOrigins.adoc @@ -5,7 +5,7 @@ For multiple valid origins, set the `allowedOrigins` key of the configuration to Regular expressions are passed to link:{javase}java/util/regex/Pattern.html#compile-java.lang.String-[Pattern#compile] and compared to the request origin with link:{javase}java/util/regex/Matcher.html#matches--[Matcher#matches]. .Example CORS Configuration -[source,yaml] +[configuration] ---- micronaut: server: diff --git a/src/main/docs/guide/httpServer/serverConfiguration/cors/corsConfiguration.adoc b/src/main/docs/guide/httpServer/serverConfiguration/cors/corsConfiguration.adoc index ccd7bd4f75b..96862b8e19e 100644 --- a/src/main/docs/guide/httpServer/serverConfiguration/cors/corsConfiguration.adoc +++ b/src/main/docs/guide/httpServer/serverConfiguration/cors/corsConfiguration.adoc @@ -1,7 +1,7 @@ -To enable processing of CORS requests, modify your configuration. For example with `application.yml`: +To enable processing of CORS requests, modify your configuration in the application configuration file: .CORS Configuration Example -[source,yaml] +[configuration] ---- micronaut: server: @@ -14,7 +14,7 @@ By only enabling CORS processing, a "wide open" strategy is adopted that allows To change the settings for all origins or a specific origin, change the configuration to provide one or more "configurations". By providing any configuration, the default "wide open" configuration is not configured. .CORS Configurations -[source,yaml] +[configuration] ---- micronaut: server: diff --git a/src/main/docs/guide/httpServer/serverConfiguration/cors/corsExposedHeaders.adoc b/src/main/docs/guide/httpServer/serverConfiguration/cors/corsExposedHeaders.adoc index 11a931a294d..ed2aeb6df5f 100644 --- a/src/main/docs/guide/httpServer/serverConfiguration/cors/corsExposedHeaders.adoc +++ b/src/main/docs/guide/httpServer/serverConfiguration/cors/corsExposedHeaders.adoc @@ -1,7 +1,7 @@ To configure the headers that are sent in the response to a CORS request through the `Access-Control-Expose-Headers` header, include a list of strings for the `exposedHeaders` key in your configuration. None are exposed by default. .Example CORS Configuration -[source,yaml] +[configuration] ---- micronaut: server: diff --git a/src/main/docs/guide/httpServer/serverConfiguration/cors/corsMaxAge.adoc b/src/main/docs/guide/httpServer/serverConfiguration/cors/corsMaxAge.adoc index 27a9cafe456..93f2c2557f9 100644 --- a/src/main/docs/guide/httpServer/serverConfiguration/cors/corsMaxAge.adoc +++ b/src/main/docs/guide/httpServer/serverConfiguration/cors/corsMaxAge.adoc @@ -1,7 +1,7 @@ The default maximum age that preflight requests can be cached is 30 minutes. To change that behavior, specify a value in seconds. .Example CORS Configuration -[source,yaml] +[configuration] ---- micronaut: server: diff --git a/src/main/docs/guide/httpServer/serverConfiguration/cors/corsMultipleHeaderValues.adoc b/src/main/docs/guide/httpServer/serverConfiguration/cors/corsMultipleHeaderValues.adoc index f90ed768539..81d929a8e18 100644 --- a/src/main/docs/guide/httpServer/serverConfiguration/cors/corsMultipleHeaderValues.adoc +++ b/src/main/docs/guide/httpServer/serverConfiguration/cors/corsMultipleHeaderValues.adoc @@ -1,6 +1,6 @@ By default, when a header has multiple values, multiple headers are sent, each with a single value. It is possible to change the behavior to send a single header with a comma-separated list of values by setting a configuration option. -[source,yaml] +[configuration] ---- micronaut: server: diff --git a/src/main/docs/guide/httpServer/serverConfiguration/dualProtocol.adoc b/src/main/docs/guide/httpServer/serverConfiguration/dualProtocol.adoc index 4c71faf3336..5b620dd2f39 100644 --- a/src/main/docs/guide/httpServer/serverConfiguration/dualProtocol.adoc +++ b/src/main/docs/guide/httpServer/serverConfiguration/dualProtocol.adoc @@ -1,17 +1,17 @@ Micronaut supports binding both HTTP and HTTPS. To enable dual protocol support, modify your configuration. For example: -.Dual Protocol Configuration Example +.Enable HTTP to HTTPS Redirects [configuration] ---- micronaut: server: ssl: enabled: true - build-self-signed: true # <1> - dual-protocol : true #<2> + build-self-signed: true + dual-protocol : true ---- -<1> You must configure SSL for HTTPS to work. In this example we are just using a self-signed certificate, but see <> for other configurations -<2> Enabling both HTTP and HTTPS is an opt-in feature - setting the `dualProtocol` flag enables it. By default Micronaut only enables one +- You must configure SSL for HTTPS to work. In this example we are just using a self-signed certificate with `build-self-signed`, but see <> for other configurations +- `dual-protocol` enables both HTTP and HTTPS is an opt-in feature - setting the `dualProtocol` flag enables it. By default Micronaut only enables one It is also possible to redirect automatically all HTTP request to HTTPS. Besides the previous configuration, you need to enable this option. For example: @@ -25,6 +25,7 @@ micronaut: enabled: true build-self-signed: true dual-protocol : true - http-to-https-redirect: true # <1> + http-to-https-redirect: true ---- -<1> Enable HTTP to HTTPS redirects + +- `http-to-https-redirect` enables HTTP to HTTPS redirects diff --git a/src/main/docs/guide/httpServer/serverConfiguration/https.adoc b/src/main/docs/guide/httpServer/serverConfiguration/https.adoc index db21c83d1a5..4660633ed59 100644 --- a/src/main/docs/guide/httpServer/serverConfiguration/https.adoc +++ b/src/main/docs/guide/httpServer/serverConfiguration/https.adoc @@ -7,11 +7,11 @@ micronaut: server: ssl: enabled: true - buildSelfSigned: true # <1> + buildSelfSigned: true ---- -<1> Micronaut will create a self-signed certificate. +- Micronaut will create a self-signed certificate. -TIP: By default Micronaut with HTTPS support starts on port `8443` but you can change the port with the property `micronaut.server.ssl.port`. +TIP: By default, Micronaut with HTTPS support starts on port `8443` but you can change the port with the property `micronaut.server.ssl.port`. For generating self-signed certificates, the Micronaut HTTP server will use netty. Netty uses one of two approaches to generate the certificate. @@ -55,12 +55,12 @@ micronaut: ssl: enabled: true key-store: - path: classpath:server.p12 # <1> - password: mypassword # <2> + path: classpath:server.p12 + password: mypassword type: PKCS12 ---- -<1> The `p12` file. It can also be referenced as `file:/path/to/the/file` -<2> The password defined during the export +- Specify the `p12` file path. It can also be referenced as `file:/path/to/the/file` +- Also provide the `password` defined during the export With this configuration, if we start Micronaut and connect to `https://localhost:8443` we still see the warning in the browser, but if we inspect the certificate we can check that it is the one generated by Let's Encrypt. diff --git a/src/main/docs/guide/httpServer/serverConfiguration/listener.adoc b/src/main/docs/guide/httpServer/serverConfiguration/listener.adoc index de69117950a..2f4f21988be 100644 --- a/src/main/docs/guide/httpServer/serverConfiguration/listener.adoc +++ b/src/main/docs/guide/httpServer/serverConfiguration/listener.adoc @@ -1,13 +1,13 @@ Instead of configuring a single port, you can also specify each listener manually. -[source, yaml] +[configuration] ---- micronaut: server: netty: listeners: - httpListener: # listener name can be an arbitrary value - host: 127.0.0.1 # optional, by default binds to all interfaces + httpListener: + host: 127.0.0.1 port: 8086 ssl: false httpsListener: @@ -15,6 +15,9 @@ micronaut: ssl: true ---- +- `httpListener` is a listener name, and can be an arbitrary value +- `host` is optional, and by default binds to all interfaces + WARNING: If you specify listeners manually, other configuration such as `micronaut.server.port` will be ignored. SSL can be enabled or disabled for each listener individually. When enabled, the SSL will be configured <>. @@ -25,16 +28,18 @@ dependency:netty-transport-native-unix-common[groupId="io.netty",artifactId="net The server must also be configured to <> (epoll or kqueue). -[source, yaml] +[configuration] ---- micronaut: server: netty: listeners: - unixListener: # listener name can be an arbitrary value + unixListener: family: UNIX path: /run/micronaut.socket ssl: true ---- -NOTE: To use an abstract domain socket instead of a normal one, prefix the path with a NUL character, like `"\0/run/micronaut.socket"` \ No newline at end of file +- `unixListener` is a listener name, and can be an arbitrary value + +NOTE: To use an abstract domain socket instead of a normal one, prefix the path with a NUL character, like `"\0/run/micronaut.socket"` diff --git a/src/main/docs/guide/httpServer/serverConfiguration/threadPools/blockingOperations.adoc b/src/main/docs/guide/httpServer/serverConfiguration/threadPools/blockingOperations.adoc index 640007c0a64..845e4317797 100644 --- a/src/main/docs/guide/httpServer/serverConfiguration/threadPools/blockingOperations.adoc +++ b/src/main/docs/guide/httpServer/serverConfiguration/threadPools/blockingOperations.adoc @@ -11,4 +11,3 @@ micronaut: ---- The above configuration creates a fixed thread pool with 75 threads. - diff --git a/src/main/docs/guide/httpServer/websocket/websocketServer.adoc b/src/main/docs/guide/httpServer/websocket/websocketServer.adoc index b2adb2a7a13..9ce0b803bfa 100644 --- a/src/main/docs/guide/httpServer/websocket/websocketServer.adoc +++ b/src/main/docs/guide/httpServer/websocket/websocketServer.adoc @@ -75,7 +75,7 @@ By default, Micronaut times out idle connections with no activity after five min ---- micronaut: server: - idle-timeout: 30m # 30 minutes + idle-timeout: 30m ---- If you use Micronaut's WebSocket client you may also wish to set the timeout on the client: @@ -86,5 +86,5 @@ If you use Micronaut's WebSocket client you may also wish to set the timeout on micronaut: http: client: - read-idle-timeout: 30m # 30 minutes + read-idle-timeout: 30m ---- diff --git a/src/main/docs/guide/logging/loggingConfiguration.adoc b/src/main/docs/guide/logging/loggingConfiguration.adoc index c624348225c..84fa68a8a77 100644 --- a/src/main/docs/guide/logging/loggingConfiguration.adoc +++ b/src/main/docs/guide/logging/loggingConfiguration.adoc @@ -11,11 +11,11 @@ The same configuration can be achieved by setting the environment variable `LOGG ==== Custom Logback XML Configuration -[source,yaml] +[configuration] ---- logger: config: custom-logback.xml ----- +---- You can also set a custom Logback XML configuration file to be used via `logger.config`. Be aware that **the referenced file should be an accessible resource on your classpath**! @@ -27,9 +27,9 @@ To disable a logger, you need to set the logger level to `OFF`: ---- logger: levels: - io.verbose.logger.who.CriedWolf: OFF <1> + io.verbose.logger.who.CriedWolf: OFF ---- -1. This will disable ALL logging for the class `io.verbose.logger.who.CriedWolf` +- This will disable ALL logging for the class `io.verbose.logger.who.CriedWolf` Note that the ability to control log levels via config is controlled via the api:logging.LoggingSystem[] interface. Currently, Micronaut includes a single implementation that allows setting log levels for the Logback library. If you use another library, you should provide a bean that implements this interface. diff --git a/src/main/docs/guide/management/providedEndpoints/environmentEndpoint.adoc b/src/main/docs/guide/management/providedEndpoints/environmentEndpoint.adoc index 00db56fadc0..0c494d3442c 100644 --- a/src/main/docs/guide/management/providedEndpoints/environmentEndpoint.adoc +++ b/src/main/docs/guide/management/providedEndpoints/environmentEndpoint.adoc @@ -9,10 +9,12 @@ To enable and configure the environment endpoint, supply configuration through ` ---- endpoints: env: - enabled: Boolean # default: false - sensitive: Boolean # default: true + enabled: Boolean + sensitive: Boolean ---- +- defaults are false for `enabled` and true for `sensitive` + By default the endpoint will mask all values. To customize this masking you need to supply a Bean that implements api:management.endpoint.env.EnvironmentEndpointFilter[]. diff --git a/src/main/docs/guide/management/providedEndpoints/healthEndpoint.adoc b/src/main/docs/guide/management/providedEndpoints/healthEndpoint.adoc index 50b3ae4e64f..40b3bbe09f2 100644 --- a/src/main/docs/guide/management/providedEndpoints/healthEndpoint.adoc +++ b/src/main/docs/guide/management/providedEndpoints/healthEndpoint.adoc @@ -13,12 +13,12 @@ endpoints: health: enabled: Boolean sensitive: Boolean - details-visible: String <1> + details-visible: String status: http-mapping: Map ---- -<1> One of api:management.endpoint.health.DetailsVisibility[] +- `details-visible` is one of api:management.endpoint.health.DetailsVisibility[] The `details-visible` setting controls whether health detail will be exposed to users who are not authenticated. @@ -96,10 +96,13 @@ endpoints: health: disk-space: enabled: Boolean - path: String #The file path used to determine the disk space - threshold: String | Long #The minimum amount of free space + path: String + threshold: String | Long ---- +- `path` specifies the path used to determine the disk space +- `threshold` specifies the minimum amount of free space + The threshold can be provided as a string like "10MB" or "200KB", or the number of bytes. === JDBC diff --git a/test-suite-kotlin/build.gradle b/test-suite-kotlin/build.gradle index 065f03fa159..40eaaa11fd6 100644 --- a/test-suite-kotlin/build.gradle +++ b/test-suite-kotlin/build.gradle @@ -39,6 +39,7 @@ dependencies { testImplementation libs.kotlinx.coroutines.rx2 testImplementation libs.kotlinx.coroutines.slf4j testImplementation libs.kotlinx.coroutines.reactor + testImplementation libs.kotlinx.coroutines.reactive // Adding these for now since micronaut-test isnt resolving correctly ... probably need to upgrade gradle there too testImplementation libs.junit.jupiter.api diff --git a/test-suite-kotlin/src/test/kotlin/io/micronaut/http/client/SuspendClientFilter.kt b/test-suite-kotlin/src/test/kotlin/io/micronaut/http/client/SuspendClientFilter.kt new file mode 100644 index 00000000000..eb76823a4ca --- /dev/null +++ b/test-suite-kotlin/src/test/kotlin/io/micronaut/http/client/SuspendClientFilter.kt @@ -0,0 +1,35 @@ +package io.micronaut.http.client + +import io.micronaut.http.HttpResponse +import io.micronaut.http.MutableHttpRequest +import io.micronaut.http.annotation.Filter +import io.micronaut.http.filter.ClientFilterChain +import io.micronaut.http.filter.HttpClientFilter +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.flatMapMerge +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.reactive.asFlow +import kotlinx.coroutines.reactive.asPublisher +import kotlinx.coroutines.withContext +import org.reactivestreams.Publisher + +@Filter +class SuspendClientFilter : HttpClientFilter { + override fun doFilter(request: MutableHttpRequest<*>, chain: ClientFilterChain): Publisher> { + // if request contains filterCheck, then do flow step, else proceed + return if ((request.body.orElse(null) as? Map<*, *>)?.containsValue(filterCheck) == true) { + flow { emit(getValue()) }.flatMapMerge { + chain.proceed(request).asFlow() + }.asPublisher() + } else { + chain.proceed(request) + } + } + + companion object { + val filterCheck = java.util.UUID.randomUUID().toString() + suspend fun getValue() = withContext(Dispatchers.Default) { "testString" } + } +} + + diff --git a/test-suite-kotlin/src/test/kotlin/io/micronaut/http/client/SuspendClientSpec.kt b/test-suite-kotlin/src/test/kotlin/io/micronaut/http/client/SuspendClientSpec.kt index 606cee07790..9da5a8b4896 100644 --- a/test-suite-kotlin/src/test/kotlin/io/micronaut/http/client/SuspendClientSpec.kt +++ b/test-suite-kotlin/src/test/kotlin/io/micronaut/http/client/SuspendClientSpec.kt @@ -2,6 +2,7 @@ package io.micronaut.http.client import io.micronaut.context.ApplicationContext import io.micronaut.http.HttpStatus +import io.micronaut.http.client.SuspendClientFilter import io.micronaut.runtime.server.EmbeddedServer import kotlinx.coroutines.runBlocking import org.junit.jupiter.api.Assertions @@ -11,35 +12,52 @@ class SuspendClientSpec { @Test fun testSuspendClientBody() { - val server = ApplicationContext.run(EmbeddedServer::class.java, mapOf("spec.name" to "SuspendClientSpec")) + val server = createServer() val ctx = server.applicationContext val response = runBlocking { ctx.getBean(SuspendClient::class.java).call("test") } - Assertions.assertEquals(response, "{\"newState\":\"test\"}") + server.close() } @Test fun testNotFound() { - val server = ApplicationContext.run(EmbeddedServer::class.java, mapOf("spec.name" to "SuspendClientSpec")) + val server = createServer() val ctx = server.applicationContext val response = runBlocking { ctx.getBean(SuspendClient::class.java).notFound() } Assertions.assertEquals(response.status, HttpStatus.NOT_FOUND) + server.close() } @Test fun testNotFoundWithoutHttpResponseWrapper() { - val server = ApplicationContext.run(EmbeddedServer::class.java, mapOf("spec.name" to "SuspendClientSpec")) + val server = createServer() val ctx = server.applicationContext val response = runBlocking { ctx.getBean(SuspendClient::class.java).notFoundWithoutHttpResponseWrapper() } Assertions.assertNull(response) + server.close() + } + + @Test + fun testFlowAsPublisherInFilterStep() { + val server = createServer() + val ctx = server.applicationContext + val response = runBlocking { + ctx.getBean(SuspendClient::class.java).call(SuspendClientFilter.filterCheck) + } + Assertions.assertEquals(response, "{\"newState\":\"${SuspendClientFilter.filterCheck}\"}") + server.close() + } + + fun createServer() : EmbeddedServer { + return ApplicationContext.run(EmbeddedServer::class.java, mapOf("spec.name" to "SuspendClientSpec")) } } From 0d9d9e06ba1f07d45b68709412cdd4ccce1c3485 Mon Sep 17 00:00:00 2001 From: Graeme Rocher Date: Fri, 17 Feb 2023 08:43:11 +0100 Subject: [PATCH 489/743] Fix regression where AliasFor is no longer recursive (#8746) --------- Co-authored-by: Denis Stepanov --- .../AbstractAnnotationMetadataBuilder.java | 26 ++++++---- .../MultiValuesConverterFactory.java | 14 ++--- http-client/src/test/resources/logback.xml | 4 +- .../GroovyAnnotationMetadataBuilder.java | 19 +++++-- ...eritedConfigurationReaderPrefixSpec.groovy | 4 +- .../inject/visitor/ClassElementSpec.groovy | 51 ++++++++++++++++++ .../visitors/ClassElementSpec.groovy | 52 +++++++++++++++++++ .../micronaut/context/DefaultBeanContext.java | 1 - .../context/annotation/Replaces.java | 1 + .../replacesbug/MathInnerServiceSpec.groovy | 46 ++++++++++++++++ 10 files changed, 193 insertions(+), 25 deletions(-) create mode 100644 test-suite/src/test/groovy/io/micronaut/test/replacesbug/MathInnerServiceSpec.groovy diff --git a/core-processor/src/main/java/io/micronaut/inject/annotation/AbstractAnnotationMetadataBuilder.java b/core-processor/src/main/java/io/micronaut/inject/annotation/AbstractAnnotationMetadataBuilder.java index 5744d000586..6c134a8cd34 100644 --- a/core-processor/src/main/java/io/micronaut/inject/annotation/AbstractAnnotationMetadataBuilder.java +++ b/core-processor/src/main/java/io/micronaut/inject/annotation/AbstractAnnotationMetadataBuilder.java @@ -38,7 +38,6 @@ import io.micronaut.inject.qualifiers.InterceptorBindingQualifier; import io.micronaut.inject.visitor.VisitorContext; import jakarta.inject.Qualifier; -import org.jetbrains.annotations.NotNull; import java.lang.annotation.Annotation; import java.lang.annotation.RetentionPolicy; @@ -670,13 +669,16 @@ private void processAnnotationAlias(Map annotationValues, aliasedAnnotation = aliasAnnotation.orElseGet(aliasAnnotationName::get); String aliasedMemberName = aliasMember.get(); if (annotationValue != null) { - introducedAnnotations.add( - toProcessedAnnotation( + ProcessedAnnotation newAnnotation = toProcessedAnnotation( AnnotationValue.builder(aliasedAnnotation, getRetentionPolicy(aliasedAnnotation)) - .members(Collections.singletonMap(aliasedMemberName, annotationValue)) - .build() - ) + .members(Collections.singletonMap(aliasedMemberName, annotationValue)) + .build() ); + introducedAnnotations.add(newAnnotation); + ProcessedAnnotation newNewAnnotation = processAliases(newAnnotation, introducedAnnotations); + if (newNewAnnotation != newAnnotation) { + introducedAnnotations.set(introducedAnnotations.indexOf(newAnnotation), newNewAnnotation); + } } } } else if (aliasMember.isPresent()) { @@ -771,7 +773,7 @@ private void addAnnotations(MutableAnnotationMetadata annotationMetadata, addAnnotations(annotationMetadata, annotationValues, isDeclared, parentAnnotations); } - @NotNull + @NonNull private Stream annotationMirrorToAnnotationValue(Stream stream, T element, boolean originatingElementIsSameParent, @@ -868,8 +870,14 @@ private ProcessedAnnotation createAnnotationValue(T originatingElement, ); } - @NotNull - private Map getCachedAnnotationDefaults(String annotationName, T annotationType) { + /** + * Get the cached annotation defaults. + * @param annotationName The annotation name + * @param annotationType The annotation type + * @return The defaults + */ + @NonNull + protected Map getCachedAnnotationDefaults(String annotationName, T annotationType) { Map defaultValues; final Map defaults = ANNOTATION_DEFAULTS.get(annotationName); if (defaults != null) { diff --git a/core/src/main/java/io/micronaut/core/convert/converters/MultiValuesConverterFactory.java b/core/src/main/java/io/micronaut/core/convert/converters/MultiValuesConverterFactory.java index 59305ccd3ae..c011a4a4e5d 100644 --- a/core/src/main/java/io/micronaut/core/convert/converters/MultiValuesConverterFactory.java +++ b/core/src/main/java/io/micronaut/core/convert/converters/MultiValuesConverterFactory.java @@ -257,15 +257,15 @@ public Optional convert( ArgumentConversionContext context = (ArgumentConversionContext) conversionContext; String format = conversionContext.getAnnotationMetadata() - .getValue(Format.class, String.class).orElse(null); + .stringValue(Format.class).orElse(null); if (format == null) { return Optional.empty(); } - String name = conversionContext.getAnnotationMetadata().getValue(Bindable.class, String.class) + String name = conversionContext.getAnnotationMetadata().stringValue(Bindable.class) .orElse(context.getArgument().getName()); String defaultValue = conversionContext.getAnnotationMetadata() - .getValue(Bindable.class, "defaultValue", String.class) + .stringValue(Bindable.class, "defaultValue") .orElse(null); switch (normalizeFormatName(format)) { @@ -524,7 +524,7 @@ private Optional convertValues(ArgumentConversionContext context Object[] constructorParameters = new Object[constructorArguments.length]; for (int i = 0; i < constructorArguments.length; ++i) { Argument argument = constructorArguments[i]; - String name = argument.getAnnotationMetadata().getValue(Bindable.class, String.class) + String name = argument.getAnnotationMetadata().stringValue(Bindable.class) .orElse(argument.getName()); constructorParameters[i] = conversionService.convert(values.get(name), ConversionContext.of(argument)) .orElse(null); @@ -575,12 +575,12 @@ public Optional convert( // noinspection unchecked ArgumentConversionContext context = (ArgumentConversionContext) conversionContext; - String format = conversionContext.getAnnotationMetadata().getValue(Format.class, String.class).orElse(null); + String format = conversionContext.getAnnotationMetadata().stringValue(Format.class).orElse(null); if (format == null) { return Optional.empty(); } - String name = conversionContext.getAnnotationMetadata().getValue(Bindable.class, String.class) + String name = conversionContext.getAnnotationMetadata().stringValue(Bindable.class) .orElse(context.getArgument().getName()); MutableConvertibleMultiValuesMap parameters = new MutableConvertibleMultiValuesMap<>(); @@ -775,7 +775,7 @@ private void processValues(ArgumentConversionContext context, } for (BeanProperty property: beanWrapper.getBeanProperties()) { - String key = property.getValue(Bindable.class, String.class).orElse(property.getName()); + String key = property.stringValue(Bindable.class).orElse(property.getName()); ArgumentConversionContext conversionContext = ConversionContext.STRING.with(property.getAnnotationMetadata()); conversionService.convert(property.get(object), conversionContext).ifPresent(value -> { diff --git a/http-client/src/test/resources/logback.xml b/http-client/src/test/resources/logback.xml index d15761dd727..f0acf1096e5 100644 --- a/http-client/src/test/resources/logback.xml +++ b/http-client/src/test/resources/logback.xml @@ -12,4 +12,6 @@ - \ No newline at end of file + + + diff --git a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/annotation/GroovyAnnotationMetadataBuilder.java b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/annotation/GroovyAnnotationMetadataBuilder.java index 914a5d089e4..c69c7979073 100644 --- a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/annotation/GroovyAnnotationMetadataBuilder.java +++ b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/annotation/GroovyAnnotationMetadataBuilder.java @@ -31,7 +31,6 @@ import io.micronaut.core.reflect.ClassUtils; import io.micronaut.core.reflect.ReflectionUtils; import io.micronaut.core.util.CollectionUtils; -import io.micronaut.core.util.StringUtils; import io.micronaut.inject.annotation.AbstractAnnotationMetadataBuilder; import io.micronaut.inject.annotation.AnnotatedElementValidator; import io.micronaut.inject.visitor.VisitorContext; @@ -376,9 +375,7 @@ protected void readAnnotationRawValues( if (expression instanceof ConstantExpression constantExpression) { final Object v = constantExpression.getValue(); if (v instanceof String s) { - if (StringUtils.isNotEmpty(s)) { - defaultValues.put(method, new ConstantExpression(v)); - } + defaultValues.put(method, new ConstantExpression(s)); } else if (v != null) { defaultValues.put(method, expression); } @@ -597,7 +594,8 @@ private Object convertConstantValue(Object value) { @Override protected Optional> getAnnotationValues(AnnotatedNode originatingElement, AnnotatedNode member, Class annotationType) { if (member != null) { - final List anns = member.getAnnotations(ClassHelper.make(annotationType)); + ClassNode annotationTypeNode = ClassHelper.make(annotationType); + final List anns = member.getAnnotations(annotationTypeNode); if (CollectionUtils.isNotEmpty(anns)) { AnnotationNode ann = anns.get(0); Map converted = new LinkedHashMap<>(); @@ -608,6 +606,17 @@ protected Optional> getAnnotationValue AnnotatedNode annotationMember = annotationNode.getMethod(key, new Parameter[0]); readAnnotationRawValues(originatingElement, annotationType.getName(), annotationMember, key, value, converted); } + Map annotationDefaults = getCachedAnnotationDefaults(annotationType.getName(), annotationTypeNode); + if (!annotationDefaults.isEmpty()) { + Iterator> i = converted.entrySet().iterator(); + while (i.hasNext()) { + Map.Entry next = i.next(); + Object v = annotationDefaults.get(next.getKey()); + if (v != null && v.equals(next.getValue())) { + i.remove(); + } + } + } return Optional.of(AnnotationValue.builder(annotationType).members(converted).build()); } } diff --git a/inject-groovy/src/test/groovy/io/micronaut/inject/configproperties/InheritedConfigurationReaderPrefixSpec.groovy b/inject-groovy/src/test/groovy/io/micronaut/inject/configproperties/InheritedConfigurationReaderPrefixSpec.groovy index e2b7209837e..66ea10f7753 100644 --- a/inject-groovy/src/test/groovy/io/micronaut/inject/configproperties/InheritedConfigurationReaderPrefixSpec.groovy +++ b/inject-groovy/src/test/groovy/io/micronaut/inject/configproperties/InheritedConfigurationReaderPrefixSpec.groovy @@ -37,7 +37,7 @@ class MyBean { beanDefinition.getInjectedMethods()[0].name == 'setMyValue' def metadata = beanDefinition.getInjectedMethods()[0].getAnnotationMetadata() metadata.hasAnnotation(Property) - metadata.getValue(Property, "name", String).get() == 'endpoints.my-value' + metadata.getValue(Property, "name", String).get() == 'simple.my-value' } void "property path is overriding the existing one without base prefix"() { @@ -75,7 +75,7 @@ class MyBean { beanDefinition.getInjectedMethods()[0].name == 'setMyValue' def metadata = beanDefinition.getInjectedMethods()[0].getAnnotationMetadata() metadata.hasAnnotation(Property) - metadata.getValue(Property, "name", String).get() == 'endpoints.my-value' + metadata.getValue(Property, "name", String).get() == 'endpoints.simple.my-value' } void "property path is overriding the existing one"() { diff --git a/inject-groovy/src/test/groovy/io/micronaut/inject/visitor/ClassElementSpec.groovy b/inject-groovy/src/test/groovy/io/micronaut/inject/visitor/ClassElementSpec.groovy index 4955b5e23f7..995f8a0a7c3 100644 --- a/inject-groovy/src/test/groovy/io/micronaut/inject/visitor/ClassElementSpec.groovy +++ b/inject-groovy/src/test/groovy/io/micronaut/inject/visitor/ClassElementSpec.groovy @@ -17,6 +17,7 @@ package io.micronaut.inject.visitor import io.micronaut.ast.groovy.TypeElementVisitorStart import io.micronaut.ast.transform.test.AbstractBeanDefinitionSpec +import io.micronaut.context.annotation.Replaces import io.micronaut.context.exceptions.BeanContextException import io.micronaut.core.annotation.Introspected import io.micronaut.inject.ast.ClassElement @@ -1996,6 +1997,56 @@ class MyBean { returnType.hasAnnotation(Introspected.class) } + void "test alias for recursion"() { + given: + ClassElement ce = buildClassElement('''\ +package test; + +import io.micronaut.inject.annotation.*; +import io.micronaut.context.annotation.* +import io.micronaut.test.annotation.MockBean; +import jakarta.inject.Singleton; +import jakarta.inject.Inject; +import java.util.List; +import java.lang.Integer; + +interface MathService { + + Integer compute(Integer num); +} + +@Singleton +class MathServiceImpl implements MathService { + + @Override + Integer compute(Integer num) { + return num * 4 // should never be called + } +} + +@Singleton +class MathInnerServiceSpec { + + @Inject + MathService mathService + + @MockBean(MathService) + static class MyMock implements MathService { + + @Override + Integer compute(Integer num) { + return 50 + } + } +} + +''') + when: + def replaces = ce.getAnnotation(Replaces) + then: + replaces.stringValue("bean").get() == "test.MathService" + } + void "test how the type annotations from the type are propagated"() { given: ClassElement ce = buildClassElement('''\ diff --git a/inject-java/src/test/groovy/io/micronaut/visitors/ClassElementSpec.groovy b/inject-java/src/test/groovy/io/micronaut/visitors/ClassElementSpec.groovy index 61fa2b68b18..56a0fae819f 100644 --- a/inject-java/src/test/groovy/io/micronaut/visitors/ClassElementSpec.groovy +++ b/inject-java/src/test/groovy/io/micronaut/visitors/ClassElementSpec.groovy @@ -17,6 +17,7 @@ package io.micronaut.visitors import io.micronaut.annotation.processing.test.AbstractTypeElementSpec import io.micronaut.annotation.processing.visitor.JavaClassElement +import io.micronaut.context.annotation.Replaces import io.micronaut.context.exceptions.BeanContextException import io.micronaut.core.annotation.AnnotationUtil import io.micronaut.core.annotation.Introspected @@ -2302,6 +2303,57 @@ class MyBean { returnType.hasAnnotation(Introspected.class) } + void "test alias for recursion"() { + given: + ClassElement ce = buildClassElement('''\ +package test; + +import io.micronaut.inject.annotation.*; +import io.micronaut.context.annotation.*; +import io.micronaut.test.annotation.MockBean; +import jakarta.inject.Singleton; +import jakarta.inject.Inject; +import java.util.List; +import java.lang.Integer; + +@Singleton +class MathInnerServiceSpec { + + @Inject + MathService mathService; + + @MockBean(MathService.class) + static class MyMock implements MathService { + + @Override + public Integer compute(Integer num) { + return 50; + } + } +} + +interface MathService { + + Integer compute(Integer num); +} + + +@Singleton +class MathServiceImpl implements MathService { + + @Override + public Integer compute(Integer num) { + return num * 4; // should never be called + } +} + +''') + when: + def replaces = ce.getEnclosedElements(ElementQuery.ALL_INNER_CLASSES).get(0).getAnnotation(Replaces) + then: + replaces.stringValue("bean").get() == "test.MathService" + } + void "test how the type annotations from the type are propagated"() { given: ClassElement ce = buildClassElement('''\ diff --git a/inject/src/main/java/io/micronaut/context/DefaultBeanContext.java b/inject/src/main/java/io/micronaut/context/DefaultBeanContext.java index 2173c0fd3ac..58826ef7943 100644 --- a/inject/src/main/java/io/micronaut/context/DefaultBeanContext.java +++ b/inject/src/main/java/io/micronaut/context/DefaultBeanContext.java @@ -110,7 +110,6 @@ import io.micronaut.inject.QualifiedBeanType; import io.micronaut.inject.ValidatedBeanDefinition; import io.micronaut.inject.provider.AbstractProviderDefinition; -import io.micronaut.inject.provider.BeanProviderDefinition; import io.micronaut.inject.proxy.InterceptedBeanProxy; import io.micronaut.inject.qualifiers.AnyQualifier; import io.micronaut.inject.qualifiers.Qualified; diff --git a/inject/src/main/java/io/micronaut/context/annotation/Replaces.java b/inject/src/main/java/io/micronaut/context/annotation/Replaces.java index e4fca881933..ad30072fef5 100644 --- a/inject/src/main/java/io/micronaut/context/annotation/Replaces.java +++ b/inject/src/main/java/io/micronaut/context/annotation/Replaces.java @@ -46,6 +46,7 @@ /** * @return The bean type that this bean replaces */ + @AliasFor(member = "value") Class bean() default void.class; /** diff --git a/test-suite/src/test/groovy/io/micronaut/test/replacesbug/MathInnerServiceSpec.groovy b/test-suite/src/test/groovy/io/micronaut/test/replacesbug/MathInnerServiceSpec.groovy new file mode 100644 index 00000000000..93c5b1c2d66 --- /dev/null +++ b/test-suite/src/test/groovy/io/micronaut/test/replacesbug/MathInnerServiceSpec.groovy @@ -0,0 +1,46 @@ +package io.micronaut.test.replacesbug + +import io.micronaut.test.extensions.spock.annotation.MicronautTest +import io.micronaut.test.annotation.MockBean +import jakarta.inject.Singleton +import spock.lang.Specification + +import jakarta.inject.Inject + +@MicronautTest +class MathInnerServiceSpec extends Specification { + + @Inject + MathService mathService + + void "should compute use inner mock"() { + when: + def result = mathService.compute(10) + + then: + result == 50 + } + + @MockBean(MathService) + static class MyMock implements MathService { + + @Override + Integer compute(Integer num) { + return 50 + } + } +} + +interface MathService { + + Integer compute(Integer num); +} + +@Singleton +class MathServiceImpl implements MathService { + + @Override + Integer compute(Integer num) { + return num * 4 // should never be called + } +} From 6e3f273d49ddc417191ce604187d6bbfd85d9a6e Mon Sep 17 00:00:00 2001 From: Graeme Rocher Date: Fri, 17 Feb 2023 08:47:30 +0100 Subject: [PATCH 490/743] fix support for executable methods using reflection --- .../inject/beans/visitor/IntrospectedTypeElementVisitor.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/core-processor/src/main/java/io/micronaut/inject/beans/visitor/IntrospectedTypeElementVisitor.java b/core-processor/src/main/java/io/micronaut/inject/beans/visitor/IntrospectedTypeElementVisitor.java index 2b95d97d51e..19df8e68599 100644 --- a/core-processor/src/main/java/io/micronaut/inject/beans/visitor/IntrospectedTypeElementVisitor.java +++ b/core-processor/src/main/java/io/micronaut/inject/beans/visitor/IntrospectedTypeElementVisitor.java @@ -261,9 +261,8 @@ private void addExecutableMethods(ClassElement ce, BeanIntrospectionWriter write }); } ElementQuery query = ElementQuery.of(MethodElement.class) - .onlyAccessible() - .modifiers((modifiers) -> !modifiers.contains(ElementModifier.STATIC)) - .annotated((am) -> am.hasStereotype(Executable.class)); + .modifiers(modifiers -> !modifiers.contains(ElementModifier.STATIC)) + .annotated(am -> am.hasStereotype(Executable.class)); List executableMethods = ce.getEnclosedElements(query); for (MethodElement executableMethod : executableMethods) { if (added.contains(executableMethod)) { From 071eb28162325f4c8079d9f4b64a9bfe20ecc059 Mon Sep 17 00:00:00 2001 From: micronaut-build Date: Fri, 17 Feb 2023 09:54:23 +0000 Subject: [PATCH 491/743] [skip ci] Release v3.8.5 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index b43d3cc7ff6..eb784bdac09 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -projectVersion=3.8.5-SNAPSHOT +projectVersion=3.8.5 projectGroupId=io.micronaut title=Micronaut projectDesc=Natively Cloud Native From 42331574ada3c6cf5fea24df1133591924112009 Mon Sep 17 00:00:00 2001 From: Graeme Rocher Date: Fri, 17 Feb 2023 11:03:01 +0100 Subject: [PATCH 492/743] Fix Object lookup with Groovy. Fixes #8782 (#8791) --- .../groovy/visitor/AbstractGroovyElement.java | 2 +- .../groovy/visitor/GroovyVisitorContext.java | 27 ++++++++++++------- .../inject/visitor/AllElementsVisitor.java | 3 +++ 3 files changed, 21 insertions(+), 11 deletions(-) diff --git a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/AbstractGroovyElement.java b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/AbstractGroovyElement.java index d4499df2cc0..f5492693bbc 100644 --- a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/AbstractGroovyElement.java +++ b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/AbstractGroovyElement.java @@ -425,7 +425,7 @@ private static void addBounds(GenericsType genericsType, List classNo @NonNull private ClassElement getObjectClassElement() { - return visitorContext.getClassElement("java.lang.Object").get(); + return visitorContext.getClassElement(Object.class).get(); } @NonNull diff --git a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyVisitorContext.java b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyVisitorContext.java index 8b6df759e99..d168cf7521d 100644 --- a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyVisitorContext.java +++ b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyVisitorContext.java @@ -116,23 +116,30 @@ public Optional getClassElement(String name) { @Override public Optional getClassElement(String name, ElementAnnotationMetadataFactory annotationMetadataFactory) { - if (name == null || compilationUnit == null) { + if (name == null) { return Optional.empty(); + } else if (compilationUnit == null) { + return Optional.ofNullable(classNodeFromClassLoader(name)).map(cn -> + groovyElementFactory.newClassElement(cn, annotationMetadataFactory) + ); } ClassNode classNode = Optional.ofNullable(compilationUnit.getClassNode(name)) - .orElseGet(() -> { - if (sourceUnit != null) { - GroovyClassLoader classLoader = sourceUnit.getClassLoader(); - if (classLoader != null) { - return ClassUtils.forName(name, classLoader).map(ClassHelper::make).orElse(null); - } - } - return null; - }); + .orElseGet(() -> classNodeFromClassLoader(name)); return Optional.ofNullable(classNode).map(cn -> groovyElementFactory.newClassElement(cn, annotationMetadataFactory)); } + private ClassNode classNodeFromClassLoader(String name) { + ClassNode cn = null; + if (sourceUnit != null) { + GroovyClassLoader classLoader = sourceUnit.getClassLoader(); + if (classLoader != null) { + cn = ClassUtils.forName(name, classLoader).map(ClassHelper::make).orElse(null); + } + } + return cn; + } + @Override public Optional getClassElement(Class type) { final ClassNode classNode = ClassHelper.makeCached(type); diff --git a/inject-groovy/src/test/groovy/io/micronaut/inject/visitor/AllElementsVisitor.java b/inject-groovy/src/test/groovy/io/micronaut/inject/visitor/AllElementsVisitor.java index dd4ccbd0ba3..e8efba78d8c 100644 --- a/inject-groovy/src/test/groovy/io/micronaut/inject/visitor/AllElementsVisitor.java +++ b/inject-groovy/src/test/groovy/io/micronaut/inject/visitor/AllElementsVisitor.java @@ -22,6 +22,7 @@ import io.micronaut.inject.ast.FieldElement; import io.micronaut.inject.ast.MethodElement; import io.micronaut.inject.ast.TypedElement; +import org.junit.jupiter.api.Assertions; import java.util.*; @@ -65,6 +66,8 @@ public void finish(VisitorContext visitorContext) { @Override public void visitClass(ClassElement element, VisitorContext context) { visit(element); + Assertions.assertTrue(context.getClassElement(Object.class.getName()).isPresent()); + Assertions.assertTrue(context.getClassElement(Object.class).isPresent()); element.getBeanProperties(); // Preload properties for tests otherwise it fails because the compiler is done element.getAnnotationMetadata(); VISITED_CLASS_ELEMENTS.add(element); From dd2e634cb9beed7108b3f10e323c928b0284e87a Mon Sep 17 00:00:00 2001 From: micronaut-build Date: Fri, 17 Feb 2023 10:07:40 +0000 Subject: [PATCH 493/743] Back to 3.8.6-SNAPSHOT --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index eb784bdac09..ea86da9d5eb 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -projectVersion=3.8.5 +projectVersion=3.8.6-SNAPSHOT projectGroupId=io.micronaut title=Micronaut projectDesc=Natively Cloud Native From 13ba56310a73736f5861c80104778c9067fe19ca Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Fri, 17 Feb 2023 11:20:27 +0100 Subject: [PATCH 494/743] doc: links to session module (#8788) * Link to breaking changes * add dependencies macros for retry & discovery-core * fix link * Update src/main/docs/guide/appendix/breaks.adoc --- src/main/docs/guide/appendix/breaks.adoc | 20 ++++++++++++++----- .../docs/guide/introduction/whatsNew.adoc | 2 ++ 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/src/main/docs/guide/appendix/breaks.adoc b/src/main/docs/guide/appendix/breaks.adoc index 4ea541ec9e6..150659b9588 100644 --- a/src/main/docs/guide/appendix/breaks.adoc +++ b/src/main/docs/guide/appendix/breaks.adoc @@ -8,11 +8,22 @@ This section documents breaking changes between Micronaut versions The `micronaut-runtime` module has been split into separate modules depending on the application's use case: -* `micronaut-retry` - The retry implementation including annotations such as ann:retry.annotation.Retryable[] is now a separate module that can be optionally included in a Micronaut application. -* `micronaut-discovery-core` - The base service discovery features are now a separate module. If you application listens for events such as api:discovery.event.ServiceReadyEvent[] or api:discovery.heartbeat.HeartBeatEvent[] this module should be added to the application classpath. +===== Micronaut Discovery Core + +`micronaut-discovery-core` - The base service discovery features are now a separate module. If you application listens for events such as api:discovery.event.ServiceReadyEvent[] or api:health.HeartBeatEvent[] this module should be added to the application classpath. + +dependency::micronaut-discovery[] + +===== Micronaut Retry + +`micronaut-retry` - The retry implementation including annotations such as ann:retry.annotation.Retryable[] is now a separate module that can be optionally included in a Micronaut application. In addition, since `micronaut-retry` is now optional declarative clients annotated with ann:http.client.annotation.Client[] no longer invoke fallbacks by default. To restore the previous behaviour add `micronaut-retry` to your classpath and annotate any declarative clients with ann:retry.annotation.Recoverable[]. +To use the Retry functionality, add the following dependency: + +dependency::micronaut-retry[] + ==== Calling `registerSingleton(bean)` no longer overrides existing beans If you call `registerSingleton(bean)` on the api:context.BeanContext[] this will no longer override existing beans if the type and qualifier match, instead two beans will now exist which may lead to a api:context.exceptions.NonUniqueBeanException[]. @@ -32,7 +43,7 @@ context.registerBeanDefinition( ==== WebSocket No Longer Required -The `micronaut-websocket` API is no longer a required dependency of the HTTP server. If you are using annotations such as ann:websocket.annotation.ServerWebSocket[] you should add the `micronaut-websocket` dependency to your application classpath: +`io.micronaut:micronaut-http-server` no longer exposes `micronaut-websocket` transitively. If you are using annotations such as ann:websocket.annotation.ServerWebSocket[], you should add the `micronaut-websocket` dependency to your application classpath: dependency::micronaut-websocket[] @@ -44,8 +55,7 @@ dependency:micronaut-reactor[groupId="io.micronaut.reactor"] ==== Session Support Moved to Session Module -The Session handling features have been moved to a new `micronaut-session` module. -If you require session support, you should make your application depend on +The Session handling features https://micronaut-projects.github.io/micronaut-session/snapshot/guide/[have been moved to its own module]. If you use the HTTP session module, change the maven coordinates from `io.micronaut:micronaut-session` to `io.micronaut.session:micronaut-session`. dependency:micronaut-session[groupId="io.micronaut.session"] diff --git a/src/main/docs/guide/introduction/whatsNew.adoc b/src/main/docs/guide/introduction/whatsNew.adoc index 2cbadb937bd..926aca5f6c2 100644 --- a/src/main/docs/guide/introduction/whatsNew.adoc +++ b/src/main/docs/guide/introduction/whatsNew.adoc @@ -22,6 +22,8 @@ When a bean annotated with ann:context.annotation.EachProperty[] or ann:context. - Kotlin 1.7.10 +<> + == 3.8.0 Key features: From 26ec171a2889db7a5427c863792c89fe0b37ea72 Mon Sep 17 00:00:00 2001 From: Graeme Rocher Date: Fri, 17 Feb 2023 13:42:38 +0100 Subject: [PATCH 495/743] Fix GraalVM native image support (#8793) * Delete invalid graal config introduced by merge * fix invalid graal config --- .../io.micronaut/micronaut-http-netty/native-image.properties | 1 - .../io.micronaut/micronaut-inject/native-image.properties | 1 - 2 files changed, 2 deletions(-) delete mode 100644 http-netty/src/main/resources/META-INF/native-image/io.micronaut/micronaut-http-netty/native-image.properties diff --git a/http-netty/src/main/resources/META-INF/native-image/io.micronaut/micronaut-http-netty/native-image.properties b/http-netty/src/main/resources/META-INF/native-image/io.micronaut/micronaut-http-netty/native-image.properties deleted file mode 100644 index 52279101dc6..00000000000 --- a/http-netty/src/main/resources/META-INF/native-image/io.micronaut/micronaut-http-netty/native-image.properties +++ /dev/null @@ -1 +0,0 @@ -Args = --features=io.micronaut.http.netty.graal.HttpNettyFeature diff --git a/inject/src/main/resources/META-INF/native-image/io.micronaut/micronaut-inject/native-image.properties b/inject/src/main/resources/META-INF/native-image/io.micronaut/micronaut-inject/native-image.properties index 1d57fe9cf7e..034ea152e0a 100644 --- a/inject/src/main/resources/META-INF/native-image/io.micronaut/micronaut-inject/native-image.properties +++ b/inject/src/main/resources/META-INF/native-image/io.micronaut/micronaut-inject/native-image.properties @@ -18,5 +18,4 @@ Args = -H:EnableURLProtocols=http,https \ --initialize-at-build-time=io.micronaut.context.annotation \ --initialize-at-build-time=io.micronaut.inject.annotation \ --initialize-at-build-time=io.micronaut.runtime.converters.time \ - --initialize-at-run-time=io.micronaut.inject.provider.JakartaProviderBeanDefinition \ --initialize-at-run-time=io.micronaut.context.env.CachedEnvironment From 5515eba077202a65143b2711fb0e5a35704b3c41 Mon Sep 17 00:00:00 2001 From: Tim Yates Date: Fri, 17 Feb 2023 13:57:20 +0000 Subject: [PATCH 496/743] Remove bad merge (#8797) --- .github/workflows/corretto.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/corretto.yml b/.github/workflows/corretto.yml index 8fb5984fad0..df13f0294fa 100644 --- a/.github/workflows/corretto.yml +++ b/.github/workflows/corretto.yml @@ -9,7 +9,6 @@ jobs: strategy: matrix: java: ['17'] - container: amazoncorretto:${{ matrix.java }} steps: # https://github.com/actions/virtual-environments/issues/709 - name: Free disk space From 98f2638280b73ad786469c445c5aeb6760900926 Mon Sep 17 00:00:00 2001 From: Graeme Rocher Date: Fri, 17 Feb 2023 15:28:05 +0100 Subject: [PATCH 497/743] try fix some sonar issues (#8796) --- build.gradle | 4 +++- .../writer/AbstractClassFileWriter.java | 1 + .../inject/writer/BeanDefinitionWriter.java | 9 +++++---- .../core/annotation/AnnotationValue.java | 20 ++++++++++--------- .../GroovyAnnotationMetadataBuilder.java | 1 + .../groovy/visitor/AbstractGroovyElement.java | 3 ++- .../visitor/AbstractJavaElement.java | 7 ++++--- .../AbstractBeanResolutionContext.java | 7 ++++--- .../AbstractInitializableBeanDefinition.java | 7 ++++++- .../annotation/MutableAnnotationMetadata.java | 18 +---------------- 10 files changed, 38 insertions(+), 39 deletions(-) diff --git a/build.gradle b/build.gradle index 8469a250e77..410ef92a049 100644 --- a/build.gradle +++ b/build.gradle @@ -29,7 +29,9 @@ if (System.getenv("SONAR_TOKEN") != null) { "**/graal/ServiceLoaderInitialization.java", "**/graal/ServiceLoaderInitialization.java", "**/DirectoryClassWriterOutputVisitor.java", - "**/GroovyClassWriterOutputVisitor.java" + "**/GroovyClassWriterOutputVisitor.java", + "**/tck/**", + "**/test/support/**" ] sonarqube { properties { diff --git a/core-processor/src/main/java/io/micronaut/inject/writer/AbstractClassFileWriter.java b/core-processor/src/main/java/io/micronaut/inject/writer/AbstractClassFileWriter.java index 597e2a4786a..ec4395b9ac0 100644 --- a/core-processor/src/main/java/io/micronaut/inject/writer/AbstractClassFileWriter.java +++ b/core-processor/src/main/java/io/micronaut/inject/writer/AbstractClassFileWriter.java @@ -239,6 +239,7 @@ protected static void pushTypeArgumentElements( pushTypeArgumentElements(owningType, owningTypeWriter, generatorAdapter, declaringElementName, null, types, new HashSet<>(5), defaults, loadTypeMethods); } + @SuppressWarnings("java:S1872") private static void pushTypeArgumentElements( Type owningType, ClassWriter declaringClassWriter, diff --git a/core-processor/src/main/java/io/micronaut/inject/writer/BeanDefinitionWriter.java b/core-processor/src/main/java/io/micronaut/inject/writer/BeanDefinitionWriter.java index 0105936fd91..da25123993d 100644 --- a/core-processor/src/main/java/io/micronaut/inject/writer/BeanDefinitionWriter.java +++ b/core-processor/src/main/java/io/micronaut/inject/writer/BeanDefinitionWriter.java @@ -350,9 +350,10 @@ public class BeanDefinitionWriter extends AbstractClassFileWriter implements Bea private static final org.objectweb.asm.commons.Method METHOD_OPTIONAL_OF = org.objectweb.asm.commons.Method.getMethod( ReflectionUtils.getRequiredMethod(Optional.class, "of", Object.class) ); + private static final String METHOD_NAME_INSTANTIATE = "instantiate"; private static final org.objectweb.asm.commons.Method METHOD_BEAN_CONSTRUCTOR_INSTANTIATE = org.objectweb.asm.commons.Method.getMethod(ReflectionUtils.getRequiredMethod( BeanConstructor.class, - "instantiate", + METHOD_NAME_INSTANTIATE, Object[].class )); private static final String METHOD_DESCRIPTOR_CONSTRUCTOR_INSTANTIATE = getMethodDescriptor(Object.class, Arrays.asList( @@ -3272,7 +3273,7 @@ private void visitBuildMethodDefinition(MethodElement constructor, boolean requi org.objectweb.asm.commons.Method.getMethod( ReflectionUtils.getRequiredInternalMethod( InstantiationUtils.class, - "instantiate", + METHOD_NAME_INSTANTIATE, Class.class, Class[].class, Object[].class @@ -3360,7 +3361,7 @@ private void invokeConstructorChain(GeneratorAdapter generatorAdapter, int const generatorAdapter.visitMethodInsn( INVOKESTATIC, "io/micronaut/aop/chain/ConstructorInterceptorChain", - "instantiate", + METHOD_NAME_INSTANTIATE, METHOD_DESCRIPTOR_CONSTRUCTOR_INSTANTIATE, false ); @@ -3964,7 +3965,7 @@ private void defineBuilderMethod(boolean isParametrized) { ); } - String methodName = isParametrized ? "doInstantiate" : "instantiate"; + String methodName = isParametrized ? "doInstantiate" : METHOD_NAME_INSTANTIATE; this.buildMethodVisitor = new GeneratorAdapter(classWriter.visitMethod( ACC_PUBLIC, methodName, diff --git a/core/src/main/java/io/micronaut/core/annotation/AnnotationValue.java b/core/src/main/java/io/micronaut/core/annotation/AnnotationValue.java index 006a8b34239..fc9efb1e69c 100644 --- a/core/src/main/java/io/micronaut/core/annotation/AnnotationValue.java +++ b/core/src/main/java/io/micronaut/core/annotation/AnnotationValue.java @@ -1161,6 +1161,7 @@ public final T getRequiredValue(String member, Class type) { * @throws IllegalStateException If no member is available that conforms to the given name and type */ @NonNull + @SuppressWarnings("java:S2259") // false positive public List> getAnnotations(String member, Class type) { ArgumentUtils.requireNonNull("type", type); String typeName = type.getName(); @@ -1183,17 +1184,18 @@ public List> getAnnotations(String mem } if (CollectionUtils.isEmpty(values)) { return Collections.emptyList(); - } - List> list = new ArrayList<>(values.size()); - for (AnnotationValue value : values) { - if (value == null) { - continue; - } - if (value.getAnnotationName().equals(typeName)) { - list.add((AnnotationValue) value); + } else { + List> list = new ArrayList<>(values.size()); + for (AnnotationValue value : values) { + if (value == null) { + continue; + } + if (value.getAnnotationName().equals(typeName)) { + list.add((AnnotationValue) value); + } } + return list; } - return list; } /** diff --git a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/annotation/GroovyAnnotationMetadataBuilder.java b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/annotation/GroovyAnnotationMetadataBuilder.java index c69c7979073..890ff58b395 100644 --- a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/annotation/GroovyAnnotationMetadataBuilder.java +++ b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/annotation/GroovyAnnotationMetadataBuilder.java @@ -552,6 +552,7 @@ private Object readConstantExpression(AnnotatedNode originatingElement, Annotate } } + @SuppressWarnings("java:S1872") private Object convertConstantValue(Object value) { if (value instanceof ClassNode classNode) { return new AnnotationClassValue<>(classNode.getName()); diff --git a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/AbstractGroovyElement.java b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/AbstractGroovyElement.java index f5492693bbc..bce795af3bc 100644 --- a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/AbstractGroovyElement.java +++ b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/AbstractGroovyElement.java @@ -425,7 +425,8 @@ private static void addBounds(GenericsType genericsType, List classNo @NonNull private ClassElement getObjectClassElement() { - return visitorContext.getClassElement(Object.class).get(); + return visitorContext.getClassElement(Object.class) + .orElseThrow(() -> new IllegalStateException("java.lang.Object element not found")); } @NonNull diff --git a/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/AbstractJavaElement.java b/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/AbstractJavaElement.java index ef521546ebf..cd6abd665bd 100644 --- a/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/AbstractJavaElement.java +++ b/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/AbstractJavaElement.java @@ -335,7 +335,8 @@ private ClassElement newClassElement(JavaNativeElement owner, List typeMirrorArguments = dt.getTypeArguments(); Map resolvedTypeArguments; if (visitedTypes.contains(dt) || typeElement.equals(nativeElement.element())) { - ClassElement objectElement = visitorContext.getClassElement("java.lang.Object").get(); + ClassElement objectElement = visitorContext.getClassElement(Object.class.getName()) + .orElseThrow(() -> new IllegalStateException("java.lang.Object element not found")); List typeParameters = typeElement.getTypeParameters(); Map resolved = CollectionUtils.newHashMap(typeMirrorArguments.size()); for (TypeParameterElement typeParameter : typeParameters) { @@ -400,7 +401,7 @@ private ClassElement resolveWildcard(JavaNativeElement owner, if (extendsBound instanceof IntersectionType it) { upperBounds = it.getBounds().stream(); } else if (extendsBound == null) { - upperBounds = Stream.of(visitorContext.getElements().getTypeElement("java.lang.Object").asType()); + upperBounds = Stream.of(visitorContext.getElements().getTypeElement(Object.class.getName()).asType()); } else { upperBounds = Stream.of(extendsBound); } @@ -411,7 +412,7 @@ private ClassElement resolveWildcard(JavaNativeElement owner, .map(tm -> newClassElement(owner, tm, declaredTypeArguments, visitedTypes, true)) .toList(); ClassElement upperType = WildcardElement.findUpperType(upperBoundsAsElements, lowerBoundsAsElements); - if (upperType.getType().getName().equals("java.lang.Object")) { + if (upperType.getType().getName().equals(Object.class.getName())) { // Not bounded wildcard: if (representedTypeParameter != null) { ClassElement definedTypeBound = newClassElement(owner, representedTypeParameter.asType(), declaredTypeArguments, visitedTypes, true); diff --git a/inject/src/main/java/io/micronaut/context/AbstractBeanResolutionContext.java b/inject/src/main/java/io/micronaut/context/AbstractBeanResolutionContext.java index 3a68b7de0db..569c4ab3e45 100644 --- a/inject/src/main/java/io/micronaut/context/AbstractBeanResolutionContext.java +++ b/inject/src/main/java/io/micronaut/context/AbstractBeanResolutionContext.java @@ -44,6 +44,7 @@ @Internal public abstract class AbstractBeanResolutionContext implements BeanResolutionContext { + private static final String CONSTRUCTOR_METHOD_NAME = ""; protected final DefaultBeanContext context; protected final BeanDefinition rootDefinition; protected final Path path; @@ -361,12 +362,12 @@ public Path pushConstructorResolve(BeanDefinition declaringType, Argument argume if (constructor instanceof MethodInjectionPoint methodInjectionPoint) { return pushConstructorResolve(declaringType, methodInjectionPoint.getName(), argument, constructor.getArguments()); } - return pushConstructorResolve(declaringType, "", argument, constructor.getArguments()); + return pushConstructorResolve(declaringType, CONSTRUCTOR_METHOD_NAME, argument, constructor.getArguments()); } @Override public Path pushConstructorResolve(BeanDefinition declaringType, String methodName, Argument argument, Argument[] arguments) { - if ("".equals(methodName)) { + if (CONSTRUCTOR_METHOD_NAME.equals(methodName)) { ConstructorSegment constructorSegment = new ConstructorArgumentSegment(declaringType, methodName, argument, arguments); detectCircularDependency(declaringType, argument, constructorSegment); } else { @@ -533,7 +534,7 @@ public static class ConstructorSegment extends AbstractSegment { @Override public String toString() { StringBuilder baseString; - if ("".equals(methodName)) { + if (CONSTRUCTOR_METHOD_NAME.equals(methodName)) { baseString = new StringBuilder("new "); baseString.append(getDeclaringType().getBeanType().getSimpleName()); } else { diff --git a/inject/src/main/java/io/micronaut/context/AbstractInitializableBeanDefinition.java b/inject/src/main/java/io/micronaut/context/AbstractInitializableBeanDefinition.java index 9e8731c8fcc..3f1a899a5ba 100644 --- a/inject/src/main/java/io/micronaut/context/AbstractInitializableBeanDefinition.java +++ b/inject/src/main/java/io/micronaut/context/AbstractInitializableBeanDefinition.java @@ -674,12 +674,17 @@ public final Argument[] getRequiredArguments() { if (requiredParametrizedArguments != null) { return requiredParametrizedArguments; } - requiredParametrizedArguments = Arrays.stream(getConstructor().getArguments()) + ConstructorInjectionPoint ctor = getConstructor(); + if (ctor != null) { + requiredParametrizedArguments = Arrays.stream(ctor.getArguments()) .filter(arg -> { Optional qualifierType = arg.getAnnotationMetadata().getAnnotationNameByStereotype(AnnotationUtil.QUALIFIER); return qualifierType.isPresent() && qualifierType.get().equals(Parameter.class.getName()); }) .toArray(Argument[]::new); + } else { + requiredParametrizedArguments = Argument.ZERO_ARGUMENTS; + } return requiredParametrizedArguments; } diff --git a/inject/src/main/java/io/micronaut/inject/annotation/MutableAnnotationMetadata.java b/inject/src/main/java/io/micronaut/inject/annotation/MutableAnnotationMetadata.java index f26a097a892..b16298f0172 100644 --- a/inject/src/main/java/io/micronaut/inject/annotation/MutableAnnotationMetadata.java +++ b/inject/src/main/java/io/micronaut/inject/annotation/MutableAnnotationMetadata.java @@ -562,6 +562,7 @@ private void addSourceRetentionAnnotation(String annotation) { sourceRetentionAnnotations.add(annotation); } + @SuppressWarnings("java:S2259") private void putValues(String annotation, Map values, Map> currentAnnotationValues) { Map existing = currentAnnotationValues.get(annotation); boolean hasValues = CollectionUtils.isNotEmpty(values); @@ -640,23 +641,6 @@ private Map> getAnnotationsByStereotypeInternal() { return annotations; } - @Nullable - private Object getRawValue(@NonNull String annotation, @NonNull String member) { - Object rawValue = null; - if (allAnnotations != null && StringUtils.isNotEmpty(annotation)) { - Map values = allAnnotations.get(annotation); - if (values != null) { - rawValue = values.get(member); - } else if (allStereotypes != null) { - values = allStereotypes.get(annotation); - if (values != null) { - rawValue = values.get(member); - } - } - } - return rawValue; - } - private void addRepeatableInternal(String repeatableAnnotationContainer, AnnotationValue annotationValue, Map> allAnnotations, From 9e11b61fc3abe4449528c8c1e496cf0a9d60ffa9 Mon Sep 17 00:00:00 2001 From: Graeme Rocher Date: Wed, 22 Feb 2023 05:24:16 +0100 Subject: [PATCH 498/743] Refactor: Remove unnecessary use of reactive APIs in routes endpoint (#8795) --- .../endpoint/routes/RouteDataCollector.java | 4 +--- .../endpoint/routes/RoutesEndpoint.java | 12 +++++------ .../impl/DefaultRouteDataCollector.java | 21 +++++++++++-------- .../endpoint/routes/RoutesEndpointSpec.groovy | 2 +- 4 files changed, 19 insertions(+), 20 deletions(-) diff --git a/management/src/main/java/io/micronaut/management/endpoint/routes/RouteDataCollector.java b/management/src/main/java/io/micronaut/management/endpoint/routes/RouteDataCollector.java index c7b898dd7fa..62a7d83668f 100644 --- a/management/src/main/java/io/micronaut/management/endpoint/routes/RouteDataCollector.java +++ b/management/src/main/java/io/micronaut/management/endpoint/routes/RouteDataCollector.java @@ -16,8 +16,6 @@ package io.micronaut.management.endpoint.routes; import io.micronaut.web.router.UriRoute; -import org.reactivestreams.Publisher; - import java.util.stream.Stream; /** @@ -34,5 +32,5 @@ public interface RouteDataCollector { * @return A publisher that returns data representing all of * the given routes. */ - Publisher getData(Stream routes); + T getData(Stream routes); } diff --git a/management/src/main/java/io/micronaut/management/endpoint/routes/RoutesEndpoint.java b/management/src/main/java/io/micronaut/management/endpoint/routes/RoutesEndpoint.java index 1ea2fab6428..2a7da46e282 100644 --- a/management/src/main/java/io/micronaut/management/endpoint/routes/RoutesEndpoint.java +++ b/management/src/main/java/io/micronaut/management/endpoint/routes/RoutesEndpoint.java @@ -20,8 +20,6 @@ import io.micronaut.management.endpoint.annotation.Read; import io.micronaut.web.router.Router; import io.micronaut.web.router.UriRoute; -import org.reactivestreams.Publisher; -import reactor.core.publisher.Mono; import java.util.Comparator; import java.util.stream.Stream; @@ -37,27 +35,27 @@ public class RoutesEndpoint { private final Router router; - private final RouteDataCollector routeDataCollector; + private final RouteDataCollector routeDataCollector; /** * @param router The {@link Router} * @param routeDataCollector The {@link RouteDataCollector} */ - public RoutesEndpoint(Router router, RouteDataCollector routeDataCollector) { + public RoutesEndpoint(Router router, RouteDataCollector routeDataCollector) { this.router = router; this.routeDataCollector = routeDataCollector; } /** - * @return The routes as a {@link Mono} + * @return The routes data representing the routes. */ @Read @SingleResult - public Publisher getRoutes() { + public Object getRoutes() { Stream uriRoutes = router.uriRoutes() .sorted(Comparator .comparing((UriRoute r) -> r.getUriMatchTemplate().toPathString()) .thenComparing(UriRoute::getHttpMethodName)); - return Mono.from(routeDataCollector.getData(uriRoutes)); + return routeDataCollector.getData(uriRoutes); } } diff --git a/management/src/main/java/io/micronaut/management/endpoint/routes/impl/DefaultRouteDataCollector.java b/management/src/main/java/io/micronaut/management/endpoint/routes/impl/DefaultRouteDataCollector.java index b3b8871f275..1f438c994d7 100644 --- a/management/src/main/java/io/micronaut/management/endpoint/routes/impl/DefaultRouteDataCollector.java +++ b/management/src/main/java/io/micronaut/management/endpoint/routes/impl/DefaultRouteDataCollector.java @@ -22,9 +22,8 @@ import io.micronaut.management.endpoint.routes.RoutesEndpoint; import io.micronaut.web.router.UriRoute; import jakarta.inject.Singleton; -import org.reactivestreams.Publisher; -import reactor.core.publisher.Flux; -import java.util.List; + +import java.util.LinkedHashMap; import java.util.Map; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -39,20 +38,24 @@ @Requires(beans = RoutesEndpoint.class) public class DefaultRouteDataCollector implements RouteDataCollector> { - private final RouteData routeData; + private final RouteData routeData; /** * @param routeData The RouteData */ - public DefaultRouteDataCollector(RouteData routeData) { + public DefaultRouteDataCollector(RouteData routeData) { this.routeData = routeData; } @Override - public Publisher> getData(Stream routes) { - List routeList = routes.collect(Collectors.toList()); - return Flux.fromIterable(routeList) - .collectMap(this::getRouteKey, routeData::getData); + public Map getData(Stream routes) { + return routes + .collect(Collectors.toMap( + this::getRouteKey, + routeData::getData, + (e1, e2) -> e1, + LinkedHashMap::new + )); } /** diff --git a/management/src/test/groovy/io/micronaut/management/endpoint/routes/RoutesEndpointSpec.groovy b/management/src/test/groovy/io/micronaut/management/endpoint/routes/RoutesEndpointSpec.groovy index d0052c2f022..726928e8c96 100644 --- a/management/src/test/groovy/io/micronaut/management/endpoint/routes/RoutesEndpointSpec.groovy +++ b/management/src/test/groovy/io/micronaut/management/endpoint/routes/RoutesEndpointSpec.groovy @@ -47,7 +47,7 @@ class RoutesEndpointSpec extends Specification { result['{[/refresh],method=[POST],produces=[application/json]}']['method'] == "[Ljava.lang.String; io.micronaut.management.endpoint.refresh.RefreshEndpoint.refresh(java.lang.Boolean force)" result['{[/test],method=[GET],produces=[application/json]}']['method'] == "java.lang.String io.micronaut.management.endpoint.routes.RoutesEndpointSpec\$TestController.index()" result['{[/test/generics],method=[PUT],produces=[application/json]}']['method'] == "java.util.Map io.micronaut.management.endpoint.routes.RoutesEndpointSpec\$TestController.generics()" - result['{[/routes],method=[GET],produces=[application/json]}']['method'] == "org.reactivestreams.Publisher io.micronaut.management.endpoint.routes.RoutesEndpoint.getRoutes()" + result['{[/routes],method=[GET],produces=[application/json]}']['method'] == "java.lang.Object io.micronaut.management.endpoint.routes.RoutesEndpoint.getRoutes()" result['{[/test/post],method=[POST],produces=[application/json]}']['method'] == "io.micronaut.http.HttpResponse io.micronaut.management.endpoint.routes.RoutesEndpointSpec\$TestController.post(java.lang.Integer number, java.lang.String text)" cleanup: From 6eb89164fcbeff6277c32ed7d987641dc32de3ba Mon Sep 17 00:00:00 2001 From: Jonas Konrad Date: Wed, 22 Feb 2023 06:24:09 +0100 Subject: [PATCH 499/743] Deprecate FilterOrderProvider (#8624) For #8422 --- .../main/java/io/micronaut/http/filter/FilterOrderProvider.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/http/src/main/java/io/micronaut/http/filter/FilterOrderProvider.java b/http/src/main/java/io/micronaut/http/filter/FilterOrderProvider.java index 78a8c277c64..b81d6d4430d 100644 --- a/http/src/main/java/io/micronaut/http/filter/FilterOrderProvider.java +++ b/http/src/main/java/io/micronaut/http/filter/FilterOrderProvider.java @@ -23,7 +23,9 @@ * * @author James Kleeh * @since 1.0 + * @deprecated Deprecated without replacement. This class is unused and will be removed in 4.0.0. */ +@Deprecated public interface FilterOrderProvider extends Ordered { } From 825943b47ec702b056c92dd494a608016e564c77 Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Wed, 22 Feb 2023 13:10:09 +0100 Subject: [PATCH 500/743] javadoc: remove {@code @NonNullApi} (#8814) --- core/src/main/java/io/micronaut/core/annotation/NonNull.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/main/java/io/micronaut/core/annotation/NonNull.java b/core/src/main/java/io/micronaut/core/annotation/NonNull.java index 2835e08068f..a140e3e0168 100644 --- a/core/src/main/java/io/micronaut/core/annotation/NonNull.java +++ b/core/src/main/java/io/micronaut/core/annotation/NonNull.java @@ -32,7 +32,7 @@ *

Should be used at parameter, return value, and field level. Method overrides should repeat parent {@code @NonNull} annotations unless * they behave differently.

* - *

Use {@code @NonNullApi} (scope = parameters + return values) to set the default behavior to non-nullable in order to avoid annotating + *

Use {@code @NonNull} Api (scope = parameters + return values) to set the default behavior to non-nullable in order to avoid annotating * your whole codebase with {@code @NonNull}.

* * @author graemerocher From 0e960468daf5488f02ac054b380cad3099e9cdea Mon Sep 17 00:00:00 2001 From: Andrii Abramov Date: Wed, 22 Feb 2023 16:06:42 +0200 Subject: [PATCH 501/743] Add missing backtick in configurationProperties.adoc (#8819) --- src/main/docs/guide/config/configurationProperties.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/docs/guide/config/configurationProperties.adoc b/src/main/docs/guide/config/configurationProperties.adoc index ebb31f7b329..d8c48a3451f 100644 --- a/src/main/docs/guide/config/configurationProperties.adoc +++ b/src/main/docs/guide/config/configurationProperties.adoc @@ -84,7 +84,7 @@ public class EngineConfig { <1> Micronaut will use an empty prefix for getters and setters. <2> Define the getters and setters with an empty prefix. -Now you can inject `EngineConfig` and use it with `engineConfig.manufacturer()` and `engineConfig.cylinders() to retrieve the values from configuration. +Now you can inject `EngineConfig` and use it with `engineConfig.manufacturer()` and `engineConfig.cylinders()` to retrieve the values from configuration. == Property Type Conversion From 3adad0002a19c6288f508ae00b01c8b2596203fe Mon Sep 17 00:00:00 2001 From: Graeme Rocher Date: Wed, 22 Feb 2023 16:13:33 +0100 Subject: [PATCH 502/743] Fix behaviour is isTypeVariable() which differs from 3.x (#8817) The behaviour of `Argument.isTypeVariable()` should be the inverse of what it is to be consistent with 3.x. This changes fixes the test failures in the Micronaut Serialization project. --- .../java/io/micronaut/core/type/Argument.java | 8 ++-- .../groovy/visitor/AbstractGroovyElement.java | 2 +- .../inject/beans/BeanDefinitionSpec.groovy | 12 +++--- .../visitor/AbstractJavaElement.java | 2 +- .../inject/beans/BeanDefinitionSpec.groovy | 37 ++++++++++++++----- 5 files changed, 40 insertions(+), 21 deletions(-) diff --git a/core/src/main/java/io/micronaut/core/type/Argument.java b/core/src/main/java/io/micronaut/core/type/Argument.java index 0745db09768..1757999991e 100644 --- a/core/src/main/java/io/micronaut/core/type/Argument.java +++ b/core/src/main/java/io/micronaut/core/type/Argument.java @@ -549,7 +549,7 @@ static Argument of(@NonNull Class type, @Nullable AnnotationMetadata a */ @NonNull static Argument> listOf(@NonNull Class type) { - return listOf(Argument.ofTypeVariable(type, "E")); + return listOf(Argument.of(type, "E")); } /** @@ -575,7 +575,7 @@ static Argument> listOf(@NonNull Argument type) { */ @NonNull static Argument> setOf(@NonNull Class type) { - return setOf(Argument.ofTypeVariable(type, "E")); + return setOf(Argument.of(type, "E")); } /** @@ -603,7 +603,7 @@ static Argument> setOf(@NonNull Argument type) { */ @NonNull static Argument> mapOf(@NonNull Class keyType, @NonNull Class valueType) { - return mapOf(Argument.ofTypeVariable(keyType, "K"), Argument.ofTypeVariable(valueType, "V")); + return mapOf(Argument.of(keyType, "K"), Argument.of(valueType, "V")); } /** @@ -632,7 +632,7 @@ static Argument> mapOf(@NonNull Argument keyType, @NonNull A */ @NonNull static Argument> optionalOf(@NonNull Class optionalValueClass) { - return optionalOf(Argument.ofTypeVariable(optionalValueClass, "T")); + return optionalOf(Argument.of(optionalValueClass, "T")); } /** diff --git a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/AbstractGroovyElement.java b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/AbstractGroovyElement.java index bce795af3bc..4c420354dc7 100644 --- a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/AbstractGroovyElement.java +++ b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/AbstractGroovyElement.java @@ -263,7 +263,7 @@ private ClassElement newClassElement(@Nullable GroovyNativeElement declaredEleme if (genericsType.isPlaceholder()) { return resolvePlaceholder(declaredElement, genericsOwner, genericsType, redirectType, parentTypeArguments, visitedTypes, isRawType); } - return newClassElement(declaredElement, genericsType.getType(), parentTypeArguments, visitedTypes, true, isRawType); + return newClassElement(declaredElement, genericsType.getType(), parentTypeArguments, visitedTypes, genericsType.isPlaceholder(), isRawType); } @NonNull diff --git a/inject-groovy/src/test/groovy/io/micronaut/inject/beans/BeanDefinitionSpec.groovy b/inject-groovy/src/test/groovy/io/micronaut/inject/beans/BeanDefinitionSpec.groovy index 77a4f724b00..83d1080788c 100644 --- a/inject-groovy/src/test/groovy/io/micronaut/inject/beans/BeanDefinitionSpec.groovy +++ b/inject-groovy/src/test/groovy/io/micronaut/inject/beans/BeanDefinitionSpec.groovy @@ -268,13 +268,13 @@ interface Deserializer { def deserializerTypeParam = definition.getTypeArguments("test.Deserializer")[0] then: "The first is a placeholder" - serdeTypeParam.isTypeVariable() // - (serdeTypeParam instanceof GenericPlaceholder) + !serdeTypeParam.isTypeVariable() // + !(serdeTypeParam instanceof GenericPlaceholder) and: - serializerTypeParam.isTypeVariable() - (serializerTypeParam instanceof GenericPlaceholder) - deserializerTypeParam.isTypeVariable() - (deserializerTypeParam instanceof GenericPlaceholder) + !serializerTypeParam.isTypeVariable() + !(serializerTypeParam instanceof GenericPlaceholder) + !deserializerTypeParam.isTypeVariable() + !(deserializerTypeParam instanceof GenericPlaceholder) } void "test isTypeVariable array"() { diff --git a/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/AbstractJavaElement.java b/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/AbstractJavaElement.java index cd6abd665bd..f15e5de0364 100644 --- a/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/AbstractJavaElement.java +++ b/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/AbstractJavaElement.java @@ -461,7 +461,7 @@ private Map resolveTypeArguments(List { +class Test implements Serde { } interface Serde extends Serializer, Deserializer { @@ -514,22 +518,37 @@ interface Serializer { interface Deserializer { } +@jakarta.inject.Singleton +@Order(-100) +class ArrayListTest implements Serde> { +} + +@jakarta.inject.Singleton +class SetTest implements Serde> { +} ''') + BeanDefinition definition = getBeanDefinition(context, 'test.Test') + + when: "Micronaut Serialization use-case" def serdeTypeParam = definition.getTypeArguments("test.Serde")[0] def serializerTypeParam = definition.getTypeArguments("test.Serializer")[0] def deserializerTypeParam = definition.getTypeArguments("test.Deserializer")[0] + def listDeser = context.getBean(Argument.of(context.classLoader.loadClass('test.Deserializer'), Argument.listOf(String))) + def collectionDeser = context.getBean(Argument.of(context.classLoader.loadClass('test.Deserializer'), Argument.of(Collection.class, String))) then: "The first is a placeholder" - serdeTypeParam.isTypeVariable() // - (serdeTypeParam instanceof GenericPlaceholder) + listDeser.getClass().name == 'test.ArrayListTest' + listDeser.is(collectionDeser) + !serdeTypeParam.isTypeVariable() // + !(serdeTypeParam instanceof GenericPlaceholder) and: "threat resolved placeholder as not a type variable" - serializerTypeParam.isTypeVariable() - (serializerTypeParam instanceof GenericPlaceholder) - deserializerTypeParam.isTypeVariable() - (deserializerTypeParam instanceof GenericPlaceholder) + !serializerTypeParam.isTypeVariable() + !(serializerTypeParam instanceof GenericPlaceholder) + !deserializerTypeParam.isTypeVariable() + !(deserializerTypeParam instanceof GenericPlaceholder) } void "test isTypeVariable array"() { From e3a50878ac6a8e78169e323898f9060309e9fcc5 Mon Sep 17 00:00:00 2001 From: Dean Wette Date: Wed, 22 Feb 2023 23:51:14 -0600 Subject: [PATCH 503/743] Adding test for headers to verify conformance to HTTP/1.1 4.2 Message Headers specification (#8806) * Adding test for headers to verify conformance to HTTP/1.1 4.2 Message Headers specification (https://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.2) closes #8687 --- .../http/server/tck/tests/HeadersTest.java | 77 +++++++++++++++++++ 1 file changed, 77 insertions(+) create mode 100644 http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/HeadersTest.java diff --git a/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/HeadersTest.java b/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/HeadersTest.java new file mode 100644 index 00000000000..c08fd2e82c9 --- /dev/null +++ b/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/HeadersTest.java @@ -0,0 +1,77 @@ +/* + * Copyright 2017-2023 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.http.server.tck.tests; + +import io.micronaut.context.annotation.Requires; +import io.micronaut.http.HttpRequest; +import io.micronaut.http.HttpStatus; +import io.micronaut.http.MediaType; +import io.micronaut.http.annotation.Controller; +import io.micronaut.http.annotation.Get; +import io.micronaut.http.annotation.Header; +import io.micronaut.http.server.tck.AssertionUtils; +import io.micronaut.http.server.tck.HttpResponseAssertion; +import org.junit.jupiter.api.Test; + +import java.io.IOException; + +import static io.micronaut.http.server.tck.TestScenario.asserts; + +@SuppressWarnings({ + "java:S5960", // We're allowed assertions, as these are used in tests only + "checkstyle:MissingJavadocType", + "checkstyle:DesignForExtension" +}) +public class HeadersTest { + public static final String SPEC_NAME = "HeadersTest"; + + /** + * Message header field names are case-insensitive + * + * @see HTTP/1.1 Message Headers + */ + @Test + void headersAreCaseInsensitiveAsPerMessageHeadersSpecification() throws IOException { + // standard header name with mixed case + asserts(SPEC_NAME, + HttpRequest.GET("/foo/ok").header("aCcEpT", MediaType.APPLICATION_JSON), + (server, request) -> AssertionUtils.assertDoesNotThrow(server, request, HttpResponseAssertion.builder() + .status(HttpStatus.OK) + .body("{\"status\":\"ok\"}") + .build())); + // custom header name with mixed case + asserts(SPEC_NAME, + HttpRequest.GET("/foo/bar").header("fOO", "ok"), + (server, request) -> AssertionUtils.assertDoesNotThrow(server, request, HttpResponseAssertion.builder() + .status(HttpStatus.OK) + .body("{\"status\":\"ok\"}") + .build())); + } + + @Controller("/foo") + @Requires(property = "spec.name", value = SPEC_NAME) + static class ProduceController { + @Get(value = "/ok", produces = MediaType.APPLICATION_JSON) + String getOkAsJson() { + return "{\"status\":\"ok\"}"; + } + + @Get(value = "/bar", produces = MediaType.APPLICATION_JSON) + String getFooAsJson(@Header("Foo") String foo) { + return "{\"status\":\"" + foo + "\"}"; + } + } +} From aec63f6eaa3dbcbe3cd58f6631726fcdd1e74175 Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Thu, 23 Feb 2023 09:33:18 +0100 Subject: [PATCH 504/743] doc: close asciidoc table (#8816) --- src/main/docs/guide/httpServer/filters.adoc | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/docs/guide/httpServer/filters.adoc b/src/main/docs/guide/httpServer/filters.adoc index 77daa2b85c0..d87c18196d6 100644 --- a/src/main/docs/guide/httpServer/filters.adoc +++ b/src/main/docs/guide/httpServer/filters.adoc @@ -76,6 +76,7 @@ The api:http.annotation.Filter[] can use different styles of pattern for path ma |`customer/**/*.html` |customer/index.html, customer/adam/profile.html, customer/adam/job/description.html +|=== The other option is regular expression based matching. To use regular expressions, set `patternStyle = FilterPatternStyle.REGEX`. The `pattern` attribute is expected to contain a regular expression which will be expected to match the provided URLs exactly (using link:{jdkapi}/java/util/regex/Matcher.html#matches--[Matcher#matches]). From 7091b4e2bd3aa8c6f30f20da5ea10c084c1834b3 Mon Sep 17 00:00:00 2001 From: Dean Wette Date: Fri, 24 Feb 2023 00:16:04 -0600 Subject: [PATCH 505/743] doc: asciidoc macro migration follow-on from #8613, plus some extra cleanup (#8745) --- .../cloudConfiguration/distributedConfigurationConsul.adoc | 1 - .../httpClient/lowLevelHttpClient/clientConfiguration.adoc | 2 +- .../docs/guide/httpServer/serverConfiguration/accessLogger.adoc | 1 - 3 files changed, 1 insertion(+), 3 deletions(-) diff --git a/src/main/docs/guide/cloud/cloudConfiguration/distributedConfigurationConsul.adoc b/src/main/docs/guide/cloud/cloudConfiguration/distributedConfigurationConsul.adoc index eaa2fec477a..1f1bfffef9d 100644 --- a/src/main/docs/guide/cloud/cloudConfiguration/distributedConfigurationConsul.adoc +++ b/src/main/docs/guide/cloud/cloudConfiguration/distributedConfigurationConsul.adoc @@ -25,7 +25,6 @@ $ mn create-app my-app --features config-consul To enable distributed configuration make sure <> is enabled and create a `src/main/resources/bootstrap.[yml/toml/properties]` file with the following configuration: -.bootstrap configuration [configuration] ---- micronaut: diff --git a/src/main/docs/guide/httpClient/lowLevelHttpClient/clientConfiguration.adoc b/src/main/docs/guide/httpClient/lowLevelHttpClient/clientConfiguration.adoc index 6e67ad3ee8e..64ee258c9f8 100644 --- a/src/main/docs/guide/httpClient/lowLevelHttpClient/clientConfiguration.adoc +++ b/src/main/docs/guide/httpClient/lowLevelHttpClient/clientConfiguration.adoc @@ -102,7 +102,7 @@ micronaut: enabled: true max-connections: 50 ---- - +- Limit maximum concurrent HTTP/1.1 connections - `max-concurrent-http1-connections` limits the maximum concurrent HTTP/1.1 connections to 50. - `pool` enables the pool and sets the maximum number of connections for it diff --git a/src/main/docs/guide/httpServer/serverConfiguration/accessLogger.adoc b/src/main/docs/guide/httpServer/serverConfiguration/accessLogger.adoc index 34da0cf2cc9..35da646e012 100644 --- a/src/main/docs/guide/httpServer/serverConfiguration/accessLogger.adoc +++ b/src/main/docs/guide/httpServer/serverConfiguration/accessLogger.adoc @@ -41,7 +41,6 @@ micronaut: - optionally specify a `logger-name`, which defaults to `HTTP_ACCESS_LOGGER` - optionally specify a `log-format`, which defaults to the Common Log Format - ==== Logback Configuration In addition to enabling the access logger, you must add a logger for the specified or default logger name. For instance using the default logger name for logback: From 4398d51c7dcf858975d335738c869bea0843fb66 Mon Sep 17 00:00:00 2001 From: Graeme Rocher Date: Fri, 24 Feb 2023 10:00:32 +0100 Subject: [PATCH 506/743] Cleanup GraalVM metadata and used Metadata Repository (#8821) With the GraalVM Metadata Repository now active we no longer need metadata for Netty and Logback so this PR removes that metadata. In addition this PR removes the use of all GraalVM internal APIs from core and upgrades to the latest version of SVM. Finally, this PR deprecates AutomaticFeatureUtils because in future it should not be necessary to use this class with metadata living the repository. --- .github/workflows/graalvm.yml | 8 +- .../micronaut/buffer/netty/NettyFeature.java | 100 ------------------ .../native-image.properties | 1 - buildSrc/build.gradle | 2 +- .../core/graal/AutomaticFeatureUtils.java | 27 ++--- .../micronaut/core/graal/LoggingFeature.java | 68 ------------ .../service}/ServiceLoaderInitialization.java | 37 ++----- .../core/io/service/ServiceScanner.java | 69 +++++++++--- .../micronaut-core/native-image.properties | 3 +- .../micronaut-core/reflect-config.json | 30 ++++++ gradle/libs.versions.toml | 3 +- .../netty/graal/MicronautSubstitutions.java | 47 -------- test-suite-graal/build.gradle | 3 + 13 files changed, 113 insertions(+), 285 deletions(-) delete mode 100644 buffer-netty/src/main/java/io/micronaut/buffer/netty/NettyFeature.java delete mode 100644 buffer-netty/src/main/resources/META-INF/native-image/io.micronaut/micronaut-buffer-netty/native-image.properties delete mode 100644 core/src/main/java/io/micronaut/core/graal/LoggingFeature.java rename core/src/main/java/io/micronaut/core/{graal => io/service}/ServiceLoaderInitialization.java (89%) create mode 100644 core/src/main/resources/META-INF/native-image/io.micronaut/micronaut-core/reflect-config.json delete mode 100644 http-netty/src/main/java/io/micronaut/http/netty/graal/MicronautSubstitutions.java diff --git a/.github/workflows/graalvm.yml b/.github/workflows/graalvm.yml index d03c43c702d..c06db472f01 100644 --- a/.github/workflows/graalvm.yml +++ b/.github/workflows/graalvm.yml @@ -19,14 +19,8 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - graalvm: [ 'latest', 'dev' ] + graalvm: [ 'latest'] java: [ '17' ] - include: - - graalvm: 'dev' - java: '19' - exclude: - - graalvm: 'dev' - java: '17' steps: # https://github.com/actions/virtual-environments/issues/709 - name: Free disk space diff --git a/buffer-netty/src/main/java/io/micronaut/buffer/netty/NettyFeature.java b/buffer-netty/src/main/java/io/micronaut/buffer/netty/NettyFeature.java deleted file mode 100644 index dc320f7c73f..00000000000 --- a/buffer-netty/src/main/java/io/micronaut/buffer/netty/NettyFeature.java +++ /dev/null @@ -1,100 +0,0 @@ -/* - * Copyright 2017-2020 original authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.micronaut.buffer.netty; - -import com.oracle.svm.core.jdk.SystemPropertiesSupport; -import io.micronaut.core.annotation.Internal; -import io.micronaut.core.graal.AutomaticFeatureUtils; -import io.netty.util.internal.logging.InternalLoggerFactory; -import io.netty.util.internal.logging.Slf4JLoggerFactory; -import org.graalvm.nativeimage.ImageSingletons; -import org.graalvm.nativeimage.hosted.Feature; -import org.graalvm.nativeimage.hosted.RuntimeClassInitialization; -import org.graalvm.nativeimage.hosted.RuntimeReflection; - -import java.lang.reflect.Method; -import java.util.Arrays; - -/** - * A native image feature that configures the netty library. - * - * @author Jonas Konrad - * @since 3.3.0 - */ -@Internal -final class NettyFeature implements Feature { - @Override - public void beforeAnalysis(BeforeAnalysisAccess access) { - RuntimeClassInitialization.initializeAtRunTime("io.netty"); - RuntimeClassInitialization.initializeAtBuildTime( - "io.netty.util.internal.shaded.org.jctools", - "io.netty.util.internal.logging.InternalLoggerFactory", - "io.netty.util.internal.logging.Slf4JLoggerFactory", - "io.netty.util.internal.logging.LocationAwareSlf4JLogger" - ); - - // force netty to use slf4j logging - try { - InternalLoggerFactory.setDefaultFactory(Slf4JLoggerFactory.INSTANCE); - } catch (Throwable e) { - // can fail if no-op logger is on the classpath - } - - registerClasses(access, - "io.netty.channel.kqueue.KQueueChannelOption", "io.netty.channel.epoll.EpollChannelOption"); - - registerMethods(access, "io.netty.buffer.AbstractByteBufAllocator", "toLeakAwareBuffer"); - registerMethods(access, "io.netty.buffer.AdvancedLeakAwareByteBuf", "touch", "recordLeakNonRefCountingOperation"); - registerMethods(access, "io.netty.util.ReferenceCountUtil", "touch"); - - System.setProperty("io.netty.tryReflectionSetAccessible", "true"); - ImageSingletons.lookup(SystemPropertiesSupport.class).initializeProperty("io.netty.tryReflectionSetAccessible", "true"); - try { - RuntimeReflection.register(access.findClassByName("java.nio.DirectByteBuffer").getDeclaredConstructor(long.class, int.class)); - } catch (NoSuchMethodException e) { - throw new RuntimeException(e); - } - Class unsafeOld = access.findClassByName("sun.misc.Unsafe"); - if (unsafeOld != null) { - try { - RuntimeReflection.register(unsafeOld.getDeclaredMethod("allocateUninitializedArray", Class.class, int.class)); - } catch (NoSuchMethodException ignored) { - } - } - Class unsafeNew = access.findClassByName("jdk.internal.misc.Unsafe"); - if (unsafeNew != null) { - try { - RuntimeReflection.register(unsafeNew.getDeclaredMethod("allocateUninitializedArray", Class.class, int.class)); - } catch (NoSuchMethodException ignored) { - } - } - } - - private void registerClasses(BeforeAnalysisAccess access, String... classes) { - for (String clazz : classes) { - AutomaticFeatureUtils.registerClassForRuntimeReflection(access, clazz); - AutomaticFeatureUtils.registerFieldsForRuntimeReflection(access, clazz); - } - } - - private void registerMethods(BeforeAnalysisAccess access, String clz, String... methods) { - for (Method declaredMethod : access.findClassByName(clz).getDeclaredMethods()) { - if (Arrays.asList(methods).contains(declaredMethod.getName())) { - RuntimeReflection.register(declaredMethod); - } - } - } -} diff --git a/buffer-netty/src/main/resources/META-INF/native-image/io.micronaut/micronaut-buffer-netty/native-image.properties b/buffer-netty/src/main/resources/META-INF/native-image/io.micronaut/micronaut-buffer-netty/native-image.properties deleted file mode 100644 index 7552ba4d263..00000000000 --- a/buffer-netty/src/main/resources/META-INF/native-image/io.micronaut/micronaut-buffer-netty/native-image.properties +++ /dev/null @@ -1 +0,0 @@ -Args = --features=io.micronaut.buffer.netty.NettyFeature diff --git a/buildSrc/build.gradle b/buildSrc/build.gradle index b0e94ba8091..d75793b323a 100644 --- a/buildSrc/build.gradle +++ b/buildSrc/build.gradle @@ -13,5 +13,5 @@ dependencies { implementation "org.tomlj:tomlj:1.1.0" implementation "me.champeau.gradle:japicmp-gradle-plugin:0.4.1" - implementation "org.graalvm.buildtools.native:org.graalvm.buildtools.native.gradle.plugin:0.9.14" + implementation "org.graalvm.buildtools.native:org.graalvm.buildtools.native.gradle.plugin:0.9.20" } diff --git a/core/src/main/java/io/micronaut/core/graal/AutomaticFeatureUtils.java b/core/src/main/java/io/micronaut/core/graal/AutomaticFeatureUtils.java index b46c68d4a64..5cf1d3311f6 100644 --- a/core/src/main/java/io/micronaut/core/graal/AutomaticFeatureUtils.java +++ b/core/src/main/java/io/micronaut/core/graal/AutomaticFeatureUtils.java @@ -15,13 +15,12 @@ */ package io.micronaut.core.graal; -import com.oracle.svm.core.configure.ResourcesRegistry; -import com.oracle.svm.core.jdk.proxy.DynamicProxyRegistry; import io.micronaut.core.util.ArrayUtils; -import org.graalvm.nativeimage.ImageSingletons; import org.graalvm.nativeimage.hosted.Feature.BeforeAnalysisAccess; import org.graalvm.nativeimage.hosted.RuntimeClassInitialization; +import org.graalvm.nativeimage.hosted.RuntimeProxyCreation; import org.graalvm.nativeimage.hosted.RuntimeReflection; +import org.graalvm.nativeimage.hosted.RuntimeResourceAccess; import java.lang.reflect.Constructor; import java.lang.reflect.Field; @@ -36,7 +35,9 @@ * @author Álvaro Sánchez-Mariscal * @author graemerocher * @since 2.0.0 + * @deprecated Use GraalVM's own public API under {@code org.graalvm} or conditional metadata in JSON format instead */ +@Deprecated public final class AutomaticFeatureUtils { /** @@ -176,7 +177,7 @@ public static void addProxyClass(BeforeAnalysisAccess access, String... interfac } } if (classList.size() == interfaces.length) { - ImageSingletons.lookup(DynamicProxyRegistry.class).addProxyClass(classList.toArray(new Class[interfaces.length])); + RuntimeProxyCreation.register(classList.toArray(new Class[interfaces.length])); } } @@ -187,12 +188,12 @@ public static void addProxyClass(BeforeAnalysisAccess access, String... interfac */ public static void addResourcePatterns(String... patterns) { if (ArrayUtils.isNotEmpty(patterns)) { - ResourcesRegistry resourcesRegistry = ImageSingletons.lookup(ResourcesRegistry.class); - if (resourcesRegistry != null) { for (String resource : patterns) { - resourcesRegistry.addResources(resource); + RuntimeResourceAccess.addResource( + AutomaticFeatureUtils.class.getClassLoader().getUnnamedModule(), + resource + ); } - } } } @@ -203,11 +204,11 @@ public static void addResourcePatterns(String... patterns) { */ public static void addResourceBundles(String... bundles) { if (ArrayUtils.isNotEmpty(bundles)) { - ResourcesRegistry resourcesRegistry = ImageSingletons.lookup(ResourcesRegistry.class); - if (resourcesRegistry != null) { - for (String resource : bundles) { - resourcesRegistry.addResourceBundles(resource); - } + for (String resource : bundles) { + RuntimeResourceAccess.addResourceBundle( + AutomaticFeatureUtils.class.getClassLoader().getUnnamedModule(), + resource + ); } } } diff --git a/core/src/main/java/io/micronaut/core/graal/LoggingFeature.java b/core/src/main/java/io/micronaut/core/graal/LoggingFeature.java deleted file mode 100644 index 3d89677e5bf..00000000000 --- a/core/src/main/java/io/micronaut/core/graal/LoggingFeature.java +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Copyright 2017-2022 original authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.micronaut.core.graal; - -import io.micronaut.core.annotation.Internal; -import org.graalvm.nativeimage.hosted.Feature; -import org.graalvm.nativeimage.hosted.RuntimeReflection; - -import java.lang.reflect.Constructor; -import java.lang.reflect.Method; -import java.util.Objects; -import java.util.stream.Stream; - -/** - * Configures logback for runtime reflection if required. - * - * @since 4.0.0 - * @author graemerocher - */ -@Internal -public final class LoggingFeature implements Feature { - - @Override - public void beforeAnalysis(BeforeAnalysisAccess access) { - Stream.of("ch.qos.logback.classic.encoder.PatternLayoutEncoder", - "ch.qos.logback.classic.pattern.DateConverter", - "ch.qos.logback.classic.pattern.LevelConverter", - "ch.qos.logback.classic.pattern.LineSeparatorConverter", - "ch.qos.logback.classic.pattern.LoggerConverter", - "ch.qos.logback.classic.pattern.MessageConverter", - "ch.qos.logback.classic.pattern.ThreadConverter", - "ch.qos.logback.classic.pattern.color.HighlightingCompositeConverter", - "ch.qos.logback.core.ConsoleAppender", - "ch.qos.logback.core.OutputStreamAppender", - "ch.qos.logback.core.encoder.LayoutWrappingEncoder", - "ch.qos.logback.core.pattern.PatternLayoutEncoderBase", - "ch.qos.logback.core.pattern.color.CyanCompositeConverter", - "ch.qos.logback.core.pattern.color.GrayCompositeConverter", - "ch.qos.logback.core.pattern.color.MagentaCompositeConverter") - .map(access::findClassByName) - .filter(Objects::nonNull) - .forEach(t -> { - RuntimeReflection.registerForReflectiveInstantiation(t); - RuntimeReflection.register(t); - Constructor[] declaredConstructors = t.getConstructors(); - for (Constructor c : declaredConstructors) { - RuntimeReflection.register(c); - } - Method[] methods = t.getMethods(); - for (Method method : methods) { - RuntimeReflection.register(method); - } - }); - } -} diff --git a/core/src/main/java/io/micronaut/core/graal/ServiceLoaderInitialization.java b/core/src/main/java/io/micronaut/core/io/service/ServiceLoaderInitialization.java similarity index 89% rename from core/src/main/java/io/micronaut/core/graal/ServiceLoaderInitialization.java rename to core/src/main/java/io/micronaut/core/io/service/ServiceLoaderInitialization.java index 9580646436f..dfe17ac7ca1 100644 --- a/core/src/main/java/io/micronaut/core/graal/ServiceLoaderInitialization.java +++ b/core/src/main/java/io/micronaut/core/io/service/ServiceLoaderInitialization.java @@ -13,16 +13,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.micronaut.core.graal; +package io.micronaut.core.io.service; -import com.oracle.svm.core.annotate.Substitute; -import com.oracle.svm.core.annotate.TargetClass; import io.micronaut.core.annotation.AnnotationValue; -import io.micronaut.core.annotation.Internal; import io.micronaut.core.annotation.NonNull; import io.micronaut.core.beans.BeanInfo; +import io.micronaut.core.graal.GraalReflectionConfigurer; import io.micronaut.core.io.IOUtils; -import io.micronaut.core.io.service.SoftServiceLoader; +import io.micronaut.core.io.service.ServiceScanner.StaticServiceDefinitions; import io.micronaut.core.reflect.exception.InstantiationException; import io.micronaut.core.util.ArrayUtils; import org.graalvm.nativeimage.ImageSingletons; @@ -43,13 +41,10 @@ import java.nio.file.Paths; import java.util.ArrayList; import java.util.Collection; -import java.util.Collections; import java.util.Enumeration; -import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; -import java.util.Map; import java.util.Set; /** @@ -67,7 +62,7 @@ public void beforeAnalysis(BeforeAnalysisAccess access) { configureForReflection(access); StaticServiceDefinitions staticServiceDefinitions = buildStaticServiceDefinitions(access); - final Collection> allTypeNames = staticServiceDefinitions.serviceTypeMap.values(); + final Collection> allTypeNames = staticServiceDefinitions.serviceTypeMap().values(); for (Set typeNameSet : allTypeNames) { Iterator i = typeNameSet.iterator(); serviceLoop: while (i.hasNext()) { @@ -119,7 +114,7 @@ public void beforeAnalysis(BeforeAnalysisAccess access) { @NonNull private StaticServiceDefinitions buildStaticServiceDefinitions(BeforeAnalysisAccess access) { - StaticServiceDefinitions staticServiceDefinitions = new StaticServiceDefinitions(); + StaticServiceDefinitions staticServiceDefinitions = new StaticServiceDefinitions(null); final String path = "META-INF/micronaut/"; try { final Enumeration micronautResources = access.getApplicationClassLoader().getResources(path); @@ -160,7 +155,7 @@ private StaticServiceDefinitions buildStaticServiceDefinitions(BeforeAnalysisAcc servicePath, serviceTypePath -> { if (Files.isRegularFile(serviceTypePath)) { - final Set serviceTypeNames = staticServiceDefinitions.serviceTypeMap + final Set serviceTypeNames = staticServiceDefinitions.serviceTypeMap() .computeIfAbsent(servicePath, key -> new HashSet<>()); final String serviceTypeName = serviceTypePath.getFileName().toString(); @@ -215,24 +210,4 @@ public void register(Constructor... constructors) { } } -@Internal -final class StaticServiceDefinitions { - final Map> serviceTypeMap = new HashMap<>(); -} - -@SuppressWarnings("unused") -@TargetClass(className = "io.micronaut.core.io.service.ServiceScanner") -@Internal -final class ServiceLoaderInitialization { - private ServiceLoaderInitialization() { - } - @Substitute - private static Set computeMicronautServiceTypeNames(URI uri, String path) { - final StaticServiceDefinitions ssd = ImageSingletons.lookup(StaticServiceDefinitions.class); - return ssd.serviceTypeMap.getOrDefault( - path, - Collections.emptySet() - ); - } -} diff --git a/core/src/main/java/io/micronaut/core/io/service/ServiceScanner.java b/core/src/main/java/io/micronaut/core/io/service/ServiceScanner.java index 2bde13fd5f5..8ca26ebfc54 100644 --- a/core/src/main/java/io/micronaut/core/io/service/ServiceScanner.java +++ b/core/src/main/java/io/micronaut/core/io/service/ServiceScanner.java @@ -16,7 +16,9 @@ package io.micronaut.core.io.service; import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.Nullable; import io.micronaut.core.io.IOUtils; +import org.graalvm.nativeimage.ImageSingletons; import java.io.BufferedReader; import java.io.IOException; @@ -30,10 +32,13 @@ import java.nio.file.Paths; import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; import java.util.Enumeration; +import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashSet; import java.util.List; +import java.util.Map; import java.util.ServiceConfigurationError; import java.util.Set; import java.util.concurrent.ForkJoinPool; @@ -79,20 +84,48 @@ private static URI normalizeFilePath(String path, URI uri) { */ @SuppressWarnings("java:S3398") private static Set computeMicronautServiceTypeNames(URI uri, String path) { - Set typeNames = new HashSet<>(); - // Keep the anonymous class instead of Lambda to reduce the Lambda invocation overhead during the startup - Consumer consumer = new Consumer<>() { - - @Override - public void accept(Path currentPath) { - if (Files.isRegularFile(currentPath)) { - final String typeName = currentPath.getFileName().toString(); - typeNames.add(typeName); + final StaticServiceDefinitions ssd = findStaticServiceDefinitions(); + if (ssd != null) { + return ssd.serviceTypeMap.getOrDefault( + path, + Collections.emptySet() + ); + } else { + Set typeNames = new HashSet<>(); + // Keep the anonymous class instead of Lambda to reduce the Lambda invocation overhead during the startup + @SuppressWarnings({"Convert2Lambda", "java:S1604"}) Consumer consumer = new Consumer<>() { + + @Override + public void accept(Path currentPath) { + if (Files.isRegularFile(currentPath)) { + final String typeName = currentPath.getFileName().toString(); + typeNames.add(typeName); + } } - } - }; - IOUtils.eachFile(uri, path, consumer); - return typeNames; + }; + IOUtils.eachFile(uri, path, consumer); + return typeNames; + } + } + + @Nullable + private static StaticServiceDefinitions findStaticServiceDefinitions() { + if (hasImageSingletons()) { + return ImageSingletons.contains(StaticServiceDefinitions.class) ? ImageSingletons.lookup(StaticServiceDefinitions.class) : null; + } else { + return null; + } + } + + @SuppressWarnings("java:S1181") + private static boolean hasImageSingletons() { + try { + //noinspection ConstantValue + return ImageSingletons.class != null; + } catch (Throwable e) { + // not present or not a GraalVM JDK + return false; + } } @SuppressWarnings("java:S3398") @@ -333,4 +366,14 @@ private abstract static class RecursiveActionValuesCollector extends Recursiv public abstract void collect(Collection values); } + + @Internal + record StaticServiceDefinitions(Map> serviceTypeMap) { + StaticServiceDefinitions { + if (serviceTypeMap == null) { + serviceTypeMap = new HashMap<>(); + } + } + } + } diff --git a/core/src/main/resources/META-INF/native-image/io.micronaut/micronaut-core/native-image.properties b/core/src/main/resources/META-INF/native-image/io.micronaut/micronaut-core/native-image.properties index 3e9d3606c01..1384544a982 100644 --- a/core/src/main/resources/META-INF/native-image/io.micronaut/micronaut-core/native-image.properties +++ b/core/src/main/resources/META-INF/native-image/io.micronaut/micronaut-core/native-image.properties @@ -21,5 +21,4 @@ Args = --initialize-at-run-time=io.micronaut.core.io.socket.SocketUtils \ --initialize-at-build-time=io.micronaut.core.convert \ --initialize-at-build-time=io.micronaut.core.type \ --initialize-at-build-time=io.micronaut.core.annotation \ - --features=io.micronaut.core.graal.LoggingFeature \ - --features=io.micronaut.core.graal.ServiceLoaderFeature + --features=io.micronaut.core.io.service.ServiceLoaderFeature diff --git a/core/src/main/resources/META-INF/native-image/io.micronaut/micronaut-core/reflect-config.json b/core/src/main/resources/META-INF/native-image/io.micronaut/micronaut-core/reflect-config.json new file mode 100644 index 00000000000..32ac2eac8e9 --- /dev/null +++ b/core/src/main/resources/META-INF/native-image/io.micronaut/micronaut-core/reflect-config.json @@ -0,0 +1,30 @@ +[ + { + "name": "ch.qos.logback.core.ConsoleAppender", + "condition": { + "typeReachable": "ch.qos.logback.core.ConsoleAppender" + }, + "methods": [ + { + "name": "setWithJansi", + "parameterTypes": [ + "boolean" + ] + } + ] + }, + { + "name": "ch.qos.logback.core.encoder.LayoutWrappingEncoder", + "condition": { + "typeReachable": "ch.qos.logback.core.ConsoleAppender" + }, + "methods": [ + { + "name": "setParent", + "parameterTypes": [ + "ch.qos.logback.core.spi.ContextAware" + ] + } + ] + } +] diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 481f7f443fb..b89b6381952 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -9,8 +9,7 @@ compile-testing = "0.19" geb = "7.0" gorm = "7.3.2" # be sure to update graal version in gradle.properties as well -# Intentionally pin to 22.0.0.2 see https://github.com/micronaut-projects/micronaut-kafka/pull/564 and https://github.com/micronaut-projects/micronaut-core/pull/7663 -graal-svm = "22.0.0.2" +graal-svm = "22.3.1" h2 = "2.1.210" hibernate = "5.5.9.Final" hibernate-validator = "6.1.6.Final" diff --git a/http-netty/src/main/java/io/micronaut/http/netty/graal/MicronautSubstitutions.java b/http-netty/src/main/java/io/micronaut/http/netty/graal/MicronautSubstitutions.java deleted file mode 100644 index 61a569608b7..00000000000 --- a/http-netty/src/main/java/io/micronaut/http/netty/graal/MicronautSubstitutions.java +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright 2017-2020 original authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.micronaut.http.netty.graal; - -import com.oracle.svm.core.annotate.Substitute; -import com.oracle.svm.core.annotate.TargetClass; -import io.micronaut.core.annotation.Experimental; -import io.micronaut.core.annotation.Internal; -import io.netty.util.internal.logging.InternalLoggerFactory; -import io.netty.util.internal.logging.JdkLoggerFactory; - -/** - * Micronaut substitutions for GraalVM. - * - * @since 1.1 - * @author graemerocher - */ -@Experimental -@Internal -public class MicronautSubstitutions { -} - -//CHECKSTYLE:OFF -/** - * Substitutions for Netty logging. - */ -@TargetClass(io.netty.util.internal.logging.InternalLoggerFactory.class) -final class Target_io_netty_util_internal_logging_InternalLoggerFactory { - @Substitute - private static InternalLoggerFactory newDefaultFactory(String name) { - return JdkLoggerFactory.INSTANCE; - } -} -//CHECKSTYLE:ON diff --git a/test-suite-graal/build.gradle b/test-suite-graal/build.gradle index 1e614d8c828..2a8cfa4d740 100644 --- a/test-suite-graal/build.gradle +++ b/test-suite-graal/build.gradle @@ -65,6 +65,9 @@ def openGraalModules = [ graalvmNative { toolchainDetection = false + metadataRepository { + enabled = true + } binaries { all { openGraalModules.each { module -> From 7658ca202839fddf76206b39ad6ab5ff1a2838dd Mon Sep 17 00:00:00 2001 From: Jonas Konrad Date: Fri, 24 Feb 2023 15:58:49 +0100 Subject: [PATCH 507/743] Buffered JSON parsing (#8803) Currently, we use a non-blocking parser to transform input JSON to a JsonNode, then send it downstream, and finally convert it to the desired request type when the route arguments are bound. This patch delays parsing until the conversion to the route type. It consists of these parts: - A change to JsonContentProcessor. The JsonContentProcessor has to handle certain scenarios (`application/x-json-stream`, or our support for streaming a json array as a Publisher) where the input contains multiple json tokens, Instead of forwarding the input bytes to an async parser, they are first split into individual json values, that are then forwarded or parsed in bulk. - A new JsonCounter class. This class handles splitting the input into its individual json nodes. It is basically a limited JSON parser that counts braces. It is already very optimized, but it also has a special optimization when the content type is `application/json`, in which case it assumes only a single json value in the input (this is technically a breaking change, but breaks no tests). I also wrote fuzz tests for JsonCounter, I will submit them in a separate PR to https://github.com/micronaut-projects/micronaut-fuzzing . - A benchmark. It's not run by the build tools, but it is good to have around. - An option in NettyHttpServerConfiguration to re-enable eager parsing to JsonNode. JsonCounter is still used, so it's not 100% the old behavior, but it should be more compatible in some respects because it doesn't change the conversion logic anymore. - A new LazyJsonNode class. It contains a ByteBuffer with the actual unparsed bytes. It's a bit complicated because it contains a reference counted buffer that has to be released after conversion, but also sometimes multiple converters are called for the same LazyJsonNode (once to JsonNode, once to a user-defined type). To solve this, when there is a conversion to JsonNode, it keeps that JsonNode around (and releases the buffer). If there are future conversions to a specific type, it will use the JsonNode as the source instead. - A new JsonSyntaxException class. JsonMappers can throw this to signal a syntax error in the JSON, which will be reported differently than a data binding error. - Changes to JsonMapper API: The asynchronous parser is deprecated. There is a new readValue method that takes a ByteBuffer. The jackson implementation has an optimization for netty ByteBuffers. - New converters for LazyJsonNode. Also removed some old superfluous converters for JsonNode. - A change to RequestLifecycle to show JSON syntax error messages like we did previously, instead of the opaque 400 that we usually send for conversion errors. - Some test changes to reflect changes in error messages. Because we now parse the input in bulk, in some cases jackson can decorate errors with short snippets of the failing input data. Is this an issue? There are a few potential incompatibilities with this change: - Non-standard JSON features when combined with JsonCounter (i.e. when streaming a JSON array, or when using `application/x-json-stream`) will break even when the JSON parser is configured to support them. e.g. if the user configured jackson to ignore comments, that may fail now. - When the input body is never bound, JSON syntax errors may not be caught. - JsonCounter only supports UTF-8, by design. This is permitted by the JSON standard, however Jackson also supports UTF-16 and UTF-32. imo this is a net benefit, to avoid potential parser differential vulnerabilities. I also made a jackson feature to match this, though JsonCounter is sufficient now: https://github.com/FasterXML/jackson-core/pull/921 - JsonMapper implementations that do not use JsonSyntaxException yet will have less verbose HTTP errors until they switch to JsonSyntaxException. --- .../buffer/netty/NettyByteBuffer.java | 2 +- .../core/io/buffer/ReferenceCounted.java | 2 +- http-server-netty/build.gradle | 4 + .../NettyHttpServerConfiguration.java | 63 +++ .../netty/jackson/JsonContentProcessor.java | 156 +++--- .../server/netty/jackson/JsonCounter.java | 509 ++++++++++++++++++ .../JsonHttpContentSubscriberFactory.java | 6 +- .../DefaultJsonErrorHandlingSpec.groovy | 4 +- .../netty/binding/JsonBodyBindingSpec.groovy | 14 +- .../netty/jackson/JsonCounterSpec.groovy | 194 +++++++ .../JsonContentProcessorBenchmark.java | 174 ++++++ .../http/server/RequestLifecycle.java | 14 +- .../exceptions/BaseJsonExceptionHandler.java | 54 ++ .../exceptions/JacksonExceptionHandler.java | 44 ++ .../exceptions/JsonExceptionHandler.java | 43 +- jackson-core/build.gradle | 1 + .../core/parser/JacksonCoreParserFactory.java | 74 +++ .../databind/JacksonDatabindMapper.java | 31 +- .../convert/JsonConverterRegistrarSpec.groovy | 3 +- .../java/io/micronaut/json/JsonMapper.java | 19 +- .../micronaut/json/JsonSyntaxException.java | 45 ++ .../json/convert/JsonConverterRegistrar.java | 133 +++-- .../micronaut/json/convert/LazyJsonNode.java | 209 +++++++ .../errorHandling/localErrorHandling.adoc | 2 +- .../docs/server/json/PersonController.groovy | 7 +- .../docs/server/json/PersonController.kt | 6 +- .../docs/server/json/PersonController.kt | 6 +- .../docs/server/json/PersonController.java | 7 +- 28 files changed, 1612 insertions(+), 214 deletions(-) create mode 100644 http-server-netty/src/main/java/io/micronaut/http/server/netty/jackson/JsonCounter.java create mode 100644 http-server-netty/src/test/groovy/io/micronaut/http/server/netty/jackson/JsonCounterSpec.groovy create mode 100644 http-server-netty/src/test/java/io/micronaut/http/server/netty/jackson/JsonContentProcessorBenchmark.java create mode 100644 http-server/src/main/java/io/micronaut/http/server/exceptions/BaseJsonExceptionHandler.java create mode 100644 http-server/src/main/java/io/micronaut/http/server/exceptions/JacksonExceptionHandler.java create mode 100644 jackson-core/src/main/java/io/micronaut/jackson/core/parser/JacksonCoreParserFactory.java create mode 100644 json-core/src/main/java/io/micronaut/json/JsonSyntaxException.java create mode 100644 json-core/src/main/java/io/micronaut/json/convert/LazyJsonNode.java diff --git a/buffer-netty/src/main/java/io/micronaut/buffer/netty/NettyByteBuffer.java b/buffer-netty/src/main/java/io/micronaut/buffer/netty/NettyByteBuffer.java index 0f43a4ff980..5910011fb87 100644 --- a/buffer-netty/src/main/java/io/micronaut/buffer/netty/NettyByteBuffer.java +++ b/buffer-netty/src/main/java/io/micronaut/buffer/netty/NettyByteBuffer.java @@ -50,7 +50,7 @@ class NettyByteBuffer implements ByteBuffer, ReferenceCounted { } @Override - public ByteBuffer retain() { + public NettyByteBuffer retain() { delegate.retain(); return this; } diff --git a/core/src/main/java/io/micronaut/core/io/buffer/ReferenceCounted.java b/core/src/main/java/io/micronaut/core/io/buffer/ReferenceCounted.java index 1418f7fe2e9..166b30199f1 100644 --- a/core/src/main/java/io/micronaut/core/io/buffer/ReferenceCounted.java +++ b/core/src/main/java/io/micronaut/core/io/buffer/ReferenceCounted.java @@ -25,7 +25,7 @@ public interface ReferenceCounted { * * @return this */ - ByteBuffer retain(); + ReferenceCounted retain(); /** * Release a reference to this object. diff --git a/http-server-netty/build.gradle b/http-server-netty/build.gradle index e772ca7a7ca..3b2e21c91d1 100644 --- a/http-server-netty/build.gradle +++ b/http-server-netty/build.gradle @@ -31,6 +31,10 @@ dependencies { compileOnly libs.kotlin.stdlib compileOnly libs.managed.netty.transport.native.unix.common + testImplementation 'org.openjdk.jmh:jmh-core:1.36' + testAnnotationProcessor 'org.openjdk.jmh:jmh-generator-annprocess:1.36' + + testCompileOnly project(":inject-groovy") testCompileOnly(libs.jetbrains.annotations) testAnnotationProcessor project(":inject-java") diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/configuration/NettyHttpServerConfiguration.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/configuration/NettyHttpServerConfiguration.java index e40f8092c1b..fd726a08c94 100644 --- a/http-server-netty/src/main/java/io/micronaut/http/server/netty/configuration/NettyHttpServerConfiguration.java +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/configuration/NettyHttpServerConfiguration.java @@ -114,6 +114,22 @@ public class NettyHttpServerConfiguration extends HttpServerConfiguration { @SuppressWarnings("WeakerAccess") public static final boolean DEFAULT_KEEP_ALIVE_ON_SERVER_ERROR = true; + /** + * The default value for eager parsing. + * + * @since 4.0.0 + */ + @SuppressWarnings("WeakerAccess") + public static final boolean DEFAULT_EAGER_PARSING = false; + + /** + * The default value for eager parsing. + * + * @since 4.0.0 + */ + @SuppressWarnings("WeakerAccess") + public static final int DEFAULT_JSON_BUFFER_MAX_COMPONENTS = 4096; + private static final Logger LOG = LoggerFactory.getLogger(NettyHttpServerConfiguration.class); private final List pipelineCustomizers; @@ -140,6 +156,8 @@ public class NettyHttpServerConfiguration extends HttpServerConfiguration { private boolean keepAliveOnServerError = DEFAULT_KEEP_ALIVE_ON_SERVER_ERROR; private String pcapLoggingPathPattern = null; private List listeners = null; + private boolean eagerParsing = DEFAULT_EAGER_PARSING; + private int jsonBufferMaxComponents = DEFAULT_JSON_BUFFER_MAX_COMPONENTS; /** * Default empty constructor. @@ -564,6 +582,51 @@ public void setListeners(List listeners) { this.listeners = listeners; } + /** + * Parse incoming JSON data eagerly, before route binding. Default value + * {@value DEFAULT_EAGER_PARSING}. + * + * @return Whether to parse incoming JSON data eagerly before route binding + * @since 4.0.0 + */ + public boolean isEagerParsing() { + return eagerParsing; + } + + /** + * Parse incoming JSON data eagerly, before route binding. Default value + * {@value DEFAULT_EAGER_PARSING}. + * + * @param eagerParsing Whether to parse incoming JSON data eagerly before route binding + * @since 4.0.0 + */ + public void setEagerParsing(boolean eagerParsing) { + this.eagerParsing = eagerParsing; + } + + /** + * Maximum number of buffers to keep around in JSON parsing before they should be consolidated. + * Defaults to {@value #DEFAULT_JSON_BUFFER_MAX_COMPONENTS}. + * + * @return The maximum number of components + * @since 4.0.0 + */ + public int getJsonBufferMaxComponents() { + return jsonBufferMaxComponents; + } + + + /** + * Maximum number of buffers to keep around in JSON parsing before they should be consolidated. + * Defaults to {@value #DEFAULT_JSON_BUFFER_MAX_COMPONENTS}. + * + * @param jsonBufferMaxComponents The maximum number of components + * @since 4.0.0 + */ + public void setJsonBufferMaxComponents(int jsonBufferMaxComponents) { + this.jsonBufferMaxComponents = jsonBufferMaxComponents; + } + /** * Http2 settings. */ diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/jackson/JsonContentProcessor.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/jackson/JsonContentProcessor.java index 62dcc54e530..9d3adf4f996 100644 --- a/http-server-netty/src/main/java/io/micronaut/http/server/netty/jackson/JsonContentProcessor.java +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/jackson/JsonContentProcessor.java @@ -15,24 +15,24 @@ */ package io.micronaut.http.server.netty.jackson; +import io.micronaut.buffer.netty.NettyByteBufferFactory; import io.micronaut.core.annotation.Internal; import io.micronaut.core.async.publisher.Publishers; -import io.micronaut.core.async.subscriber.CompletionAwareSubscriber; +import io.micronaut.core.io.buffer.ByteBuffer; import io.micronaut.core.type.Argument; import io.micronaut.http.MediaType; -import io.micronaut.http.server.HttpServerConfiguration; import io.micronaut.http.server.netty.AbstractHttpContentProcessor; import io.micronaut.http.server.netty.HttpContentProcessor; import io.micronaut.http.server.netty.NettyHttpRequest; +import io.micronaut.http.server.netty.configuration.NettyHttpServerConfiguration; import io.micronaut.json.JsonMapper; +import io.micronaut.json.convert.LazyJsonNode; import io.micronaut.json.tree.JsonNode; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufHolder; -import io.netty.buffer.ByteBufUtil; -import io.netty.util.ReferenceCountUtil; -import org.reactivestreams.Processor; -import org.reactivestreams.Subscription; +import io.netty.buffer.CompositeByteBuf; +import java.io.IOException; import java.util.Collection; import java.util.Optional; @@ -47,9 +47,8 @@ public class JsonContentProcessor extends AbstractHttpContentProcessor { private final JsonMapper jsonMapper; - private Processor jacksonProcessor; - private Collection out; - private Throwable failure = null; + private final JsonCounter counter = new JsonCounter(); + private CompositeByteBuf buffer; /** * @param nettyHttpRequest The Netty Http request @@ -58,19 +57,22 @@ public class JsonContentProcessor extends AbstractHttpContentProcessor { */ public JsonContentProcessor( NettyHttpRequest nettyHttpRequest, - HttpServerConfiguration configuration, + NettyHttpServerConfiguration configuration, JsonMapper jsonMapper) { super(nettyHttpRequest, configuration); this.jsonMapper = jsonMapper; + + if (hasContentType(MediaType.APPLICATION_JSON_TYPE)) { + + // if the content type is application/json, we can only have one root-level value + counter.noTokenization(); + } } @Override public HttpContentProcessor resultType(Argument type) { - boolean streamArray = false; - boolean isJsonStream = nettyHttpRequest.getContentType() - .map(mediaType -> mediaType.equals(MediaType.APPLICATION_JSON_STREAM_TYPE)) - .orElse(false); + boolean isJsonStream = hasContentType(MediaType.APPLICATION_JSON_STREAM_TYPE); if (type != null) { Class targetType = type.getType(); @@ -78,91 +80,83 @@ public HttpContentProcessor resultType(Argument type) { Optional> genericArgument = type.getFirstTypeVariable(); if (genericArgument.isPresent() && !Iterable.class.isAssignableFrom(genericArgument.get().getType()) && !isJsonStream) { // if the generic argument is not a iterable type them stream the array into the publisher - streamArray = true; + counter.unwrapTopLevelArray(); } } } + return this; + } - this.jacksonProcessor = jsonMapper.createReactiveParser(p -> { - }, streamArray); - this.jacksonProcessor.subscribe(new CompletionAwareSubscriber<>() { - - @Override - protected void doOnSubscribe(Subscription jsonSubscription) { - jsonSubscription.request(Long.MAX_VALUE); - } - - @Override - protected void doOnNext(JsonNode message) { - if (out == null) { - throw new IllegalStateException("Concurrent access not allowed"); - } - out.add(message); - } - - @Override - protected void doOnError(Throwable t) { - if (out == null) { - throw new IllegalStateException("Concurrent access not allowed"); - } - failure = t; - } + private boolean hasContentType(MediaType expected) { + Optional actual = nettyHttpRequest.getContentType(); + return actual.isPresent() && actual.get().equals(expected); + } - @Override - protected void doOnComplete() { - if (out == null) { - throw new IllegalStateException("Concurrent access not allowed"); - } - } - }); - this.jacksonProcessor.onSubscribe(new Subscription() { - @Override - public void request(long n) { + @Override + protected void onData(ByteBufHolder message, Collection out) throws Throwable { + ByteBuf content = message.content(); + try { + countLoop(out, content); + } catch (Exception e) { + if (this.buffer != null) { + this.buffer.release(); + this.buffer = null; } + throw e; + } finally { + content.release(); + } + } - @Override - public void cancel() { - // happens on error, ignore + private void countLoop(Collection out, ByteBuf content) throws IOException { + long initialPosition = counter.position(); + long bias = initialPosition - content.readerIndex(); + while (content.isReadable()) { + counter.feed(content); + JsonCounter.BufferRegion bufferRegion = counter.pollFlushedRegion(); + if (bufferRegion != null) { + long start = Math.max(initialPosition, bufferRegion.start()); + flush(out, content.retainedSlice( + Math.toIntExact(start - bias), + Math.toIntExact(bufferRegion.end() - start) + )); } - }); - return this; + } + if (counter.isBuffering()) { + int currentBufferStart = Math.toIntExact(Math.max(initialPosition, counter.bufferStart()) - bias); + bufferForNextRun(content.retainedSlice(currentBufferStart, content.writerIndex() - currentBufferStart)); + } } - @Override - protected void onData(ByteBufHolder message, Collection out) throws Throwable { - if (jacksonProcessor == null) { - resultType(null); + private void bufferForNextRun(ByteBuf buffer) { + if (this.buffer == null) { + // number of components should not be too small to avoid unnecessary consolidation + this.buffer = buffer.alloc().compositeBuffer(((NettyHttpServerConfiguration) configuration).getJsonBufferMaxComponents()); } + this.buffer.addComponent(true, buffer); + } - this.out = out; - ByteBuf content = message.content(); - try { - byte[] bytes = ByteBufUtil.getBytes(content); - jacksonProcessor.onNext(bytes); - } finally { - ReferenceCountUtil.release(content); - this.out = null; + private void flush(Collection out, ByteBuf completedNode) throws IOException { + if (this.buffer != null) { + completedNode = completedNode == null ? this.buffer : this.buffer.addComponent(true, completedNode); + this.buffer = null; } - Throwable f = failure; - if (f != null) { - failure = null; - throw f; + ByteBuffer wrapped = NettyByteBufferFactory.DEFAULT.wrap(completedNode); + if (((NettyHttpServerConfiguration) configuration).isEagerParsing()) { + try { + out.add(jsonMapper.readValue(wrapped, Argument.of(JsonNode.class))); + } finally { + completedNode.release(); + } + } else { + out.add(new LazyJsonNode(wrapped)); } } @Override public void complete(Collection out) throws Throwable { - if (jacksonProcessor == null) { - resultType(null); - } - - this.out = out; - jacksonProcessor.onComplete(); - this.out = null; - Throwable f = failure; - if (f != null) { - failure = null; - throw f; + if (this.buffer != null) { + flush(out, null); } } } diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/jackson/JsonCounter.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/jackson/JsonCounter.java new file mode 100644 index 00000000000..487c79f4802 --- /dev/null +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/jackson/JsonCounter.java @@ -0,0 +1,509 @@ +/* + * Copyright 2017-2023 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.http.server.netty.jackson; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.Nullable; +import io.micronaut.json.JsonSyntaxException; +import io.netty.buffer.ByteBuf; + +/** + * This class takes in JSON data and does simple parsing to detect boundaries between json nodes. + * For example, this class can recognize the separation between the two JSON objects in + * {@code {"foo":"bar"} {"bar":"baz"}}.
+ * Public for fuzzing. + */ +@SuppressWarnings({"BooleanMethodIsAlwaysInverted", "InnerAssignment"}) +@Internal +public final class JsonCounter { + /** + * Total number of bytes consumed. + */ + private long position; + /** + * Depth of nested structures. + */ + private int depth; + /** + * Current state of the parser. + */ + private State state = State.BASE; + + /** + * {@link #position} of the first byte of the current top-level JSON node. + */ + private long bufferStart = -1; + + /** + * Whether we are currently unwrapping a top-level array. + * + * @see #unwrapTopLevelArray() + */ + private boolean unwrappingArray; + /** + * Whether we are currently unwrapping a top-level array, and expect a comma next (or end of + * array). + * + * @see #unwrapTopLevelArray() + */ + private boolean allowUnwrappingArrayComma; + + /** + * The region of the last complete top-level JSON node we have visited. Polled by the user. + */ + @Nullable + private BufferRegion lastFlushedRegion; + + /** + * Parse some input data. If {@code buf} is readable, this method always advances (always + * consumes at least one byte). + * + * @param buf The input buffer + * @throws JsonSyntaxException If there is a syntax error in the JSON. Note that not all syntax + * errors are detected by this class. + */ + public void feed(ByteBuf buf) throws JsonSyntaxException { + if (position < 4) { + // RFC 4627 allows JSON to be encoded as UTF-8, UTF-16 or UTF-32. It also specifies a + // charset detection algorithm using 0x00 bytes. + // Later standards (RFC 8259) only permit UTF-8, but Jackson still allows other + // charsets. To avoid potential parser differential vulnerabilities, we forbid any 0x00 + // bytes in the input. They never appear in valid UTF-8 JSON. + + // If the input is utf-16 or utf-32, one of the first four bytes will be 0. Checking + // this separately and only for four bytes allows us to avoid the work in the hot loops + // below. + int r = buf.readableBytes(); + if ((r >= 1 && buf.getByte(0) == 0) + || (r >= 2 && buf.getByte(1) == 0) + || (r >= 3 && buf.getByte(2) == 0) + || (r >= 4 && buf.getByte(3) == 0)) { + + throw new JsonSyntaxException("Input must be legal UTF-8 JSON"); + } + } + if (!isBuffering()) { + proceedUntilBuffering(buf); + } + if (isBuffering()) { + proceedUntilNonBuffering(buf); + } + } + + /** + * Enable top-level array unwrapping: If the input starts with an array, that array's elements + * are returned as individual JSON nodes, not the array all at once.
+ * Must be called before any data is processed, but can be called after + * {@link #noTokenization()}. + */ + public void unwrapTopLevelArray() { + if (position != 0) { + throw new IllegalStateException("Already consumed input"); + } + state = State.BEFORE_UNWRAP_ARRAY; + bufferStart = -1; + } + + /** + * Do not perform any tokenization, assume that there is only one root-level value. There is + * still some basic validation (ensuring the input isn't utf-16 or utf-32). + */ + public void noTokenization() { + if (position != 0) { + throw new IllegalStateException("Already consumed input"); + } + state = State.BUFFER_ALL; + bufferStart = 0; + } + + /** + * Proceed until {@link #isBuffering()} becomes false. + */ + private void proceedUntilNonBuffering(ByteBuf buf) throws JsonSyntaxException { + assert isBuffering(); + int end = buf.writerIndex(); + + int i = buf.readerIndex(); + while (i < end && bufferStart != -1) { + int start = i; + if (state == State.BASE) { + assert depth > 0 : depth; + for (; i < end; i++) { + if (!skipBufferingBase(buf.getByte(i))) { + break; + } + } + this.position += i - start; + if (i < end) { + handleBufferingBaseSpecial(buf.getByte(i)); + i++; + position++; + } + } else if (state == State.STRING) { + for (; i < end; i++) { + if (!skipString(buf.getByte(i))) { + break; + } + } + this.position += i - start; + if (i < end) { + handleStringSpecial(buf.getByte(i)); + i++; + position++; + } + } else if (state == State.ESCAPE) { + handleEscape(buf.getByte(i)); + i++; + position++; + } else if (state == State.TOP_LEVEL_SCALAR) { + assert depth == 0 : depth; + for (; i < end; i++) { + if (!skipTopLevelScalar(buf.getByte(i))) { + break; + } + } + this.position += i - start; + if (i < end) { + handleTopLevelScalarSpecial(buf.getByte(i)); + i++; + position++; + } + } else if (state == State.BUFFER_ALL) { + i = end; + position += i - start; + } else { + throw new AssertionError(state); + } + } + buf.readerIndex(i); + } + + /** + * Consume some input until {@link #isBuffering()}. Sometimes this method returns before that + * is the case, to make the implementation simpler. + */ + private void proceedUntilBuffering(ByteBuf buf) throws JsonSyntaxException { + assert !isBuffering(); + assert depth == 0 : depth; + + int start = buf.readerIndex(); + int end = buf.writerIndex(); + int i = start; + + if (state == State.AFTER_UNWRAP_ARRAY) { + // top-level array consumed. reject further data + skipWs(buf, i, end); + if (i < end) { + throw new JsonSyntaxException("Superfluous data after top-level array in streaming mode"); + } + } else { + // normal path + assert state == State.BASE || state == State.BEFORE_UNWRAP_ARRAY : state; + + if (position == 0 && i < end && buf.getByte(i) == (byte) 0xef) { + throw new JsonSyntaxException("UTF-8 BOM not allowed"); + } + + // if we are unwrapping a top-level array, search for a comma + if (allowUnwrappingArrayComma) { + i = skipWs(buf, i, end); + if (i < end && buf.getByte(i) == ',') { + allowUnwrappingArrayComma = false; + i++; + } + } + i = skipWs(buf, i, end); + this.position += i - start; + + if (i < end) { + byte b = buf.getByte(i); + handleNonBufferingBase(b); + i++; + position++; + } + } + + buf.readerIndex(i); + } + + /** + * Skip any whitespace characters. + * + * @param i The start index + * @param end The maximum index + * @return The first non-whitespace character index, or {@code end} + */ + private static int skipWs(ByteBuf buf, int i, int end) { + for (; i < end; i++) { + if (!ws(buf.getByte(i))) { + break; + } + } + return i; + } + + /** + * Handle a special byte (anything but whitespace) in the base state, while not buffering. + */ + private void handleNonBufferingBase(byte b) throws JsonSyntaxException { + switch (b) { + case '}' -> failMismatchedBrackets(); + case ']' -> { + if (unwrappingArray) { + state = State.AFTER_UNWRAP_ARRAY; + } else { + failMismatchedBrackets(); + } + } + case '{' -> { + depth = 1; + bufferStart = position; + state = State.BASE; // we might be in BEFORE_UNWRAP_ARRAY + } + case '[' -> { + if (state == State.BEFORE_UNWRAP_ARRAY) { + state = State.BASE; + unwrappingArray = true; + } else { + depth = 1; + bufferStart = position; + } + } + case '"' -> { + state = State.STRING; + bufferStart = position; + } + default -> { + state = State.TOP_LEVEL_SCALAR; + bufferStart = position; + } + } + } + + /** + * @return {@code true} if this character does not end the top-level scalar + */ + private static boolean skipTopLevelScalar(byte b) { + return !ws(b) && b != '"' && b != '{' && b != '[' && b != ']' && b != '}' && b != ','; + } + + /** + * Handle a special byte (anything but {@link #skipTopLevelScalar}) in the + * {@link State#TOP_LEVEL_SCALAR} state. + */ + private void handleTopLevelScalarSpecial(byte b) throws JsonSyntaxException { + if (ws(b)) { + position--; + flushAfter(); + position++; + allowUnwrappingArrayComma = unwrappingArray; + state = State.BASE; + } else if (unwrappingArray && (b == ',' || b == ']')) { + position--; + flushAfter(); + position++; + if (b == ',') { + state = State.BASE; + } else { + state = State.AFTER_UNWRAP_ARRAY; + } + allowUnwrappingArrayComma = false; + } else { + failMissingWs(); + } + } + + /** + * Handle a byte in the {@link State#ESCAPE} state. + */ + private void handleEscape(byte b) throws JsonSyntaxException { + state = State.STRING; + } + + /** + * @return {@code true} if this character does not end the string + */ + private static boolean skipString(byte b) { + return b != '"' && b != '\\'; + } + + /** + * Handle a special byte (anything but {@link #skipString}) in the {@link State#STRING} state. + */ + private void handleStringSpecial(byte b) throws JsonSyntaxException { + switch (b) { + case '"' -> { + state = State.BASE; + if (depth == 0) { + flushAfter(); + } + } + case '\\' -> state = State.ESCAPE; + default -> throw new AssertionError(); + } + } + + /** + * @return {@code true} if this character does not change the state while in {@link State#BASE} + * and while not buffering + */ + private static boolean skipBufferingBase(byte b) { + return (b != '"') & (b != '{') & (b != '[') & (b != ']') & (b != '}'); + } + + /** + * Handle a special byte (anything but {@link #skipBufferingBase(byte)}) in the base state, + * while buffering. + */ + private void handleBufferingBaseSpecial(byte b) throws JsonSyntaxException { + switch (b) { + case '}', ']' -> { + depth--; + if (depth == 0) { + flushAfter(); + } + } + case '{', '[' -> depth = Math.incrementExact(depth); + case '"' -> state = State.STRING; + default -> throw new AssertionError(b); + } + } + + /** + * Flush the current JSON node, starting at {@link #bufferStart}, and ending after + * {@link #position}. Disables buffering. + */ + private void flushAfter() { + if (lastFlushedRegion != null) { + throw new IllegalStateException("Should have cleared last buffer region"); + } + assert bufferStart != -1; + assert position >= bufferStart; + lastFlushedRegion = new BufferRegion(bufferStart, position + 1); + bufferStart = -1; + allowUnwrappingArrayComma = unwrappingArray; + } + + /** + * Check for any new flushed data from the last {@link #feed(ByteBuf)} operation. + * + * @return The region that contains a JSON node, relative to {@link #position()}, or + * {@code null} if the JSON node has not completed yet. + */ + @Nullable + public BufferRegion pollFlushedRegion() { + BufferRegion r = lastFlushedRegion; + lastFlushedRegion = null; + return r; + } + + /** + * The current position counter of the parser. Increases by exactly one for each byte consumed + * by {@link #feed}. + * + * @return The current position + */ + public long position() { + return position; + } + + /** + * Whether we are currently in the buffering state, i.e. there is a JSON node, but it's not + * done yet or we can't know for sure that it's done (e.g. for numbers). This is used to flush + * any remaining buffering data when EOF is reached. + * + * @return {@code true} if we are currently buffering + */ + public boolean isBuffering() { + return bufferStart != -1; + } + + /** + * If we are {@link #isBuffering() buffering}, the start {@link #position()} of the region that + * is being buffered. + * + * @return The buffer region start + * @throws IllegalStateException if we aren't buffering + */ + public long bufferStart() { + if (bufferStart == -1) { + throw new IllegalStateException("Not buffering"); + } + return bufferStart; + } + + private static void failMismatchedBrackets() throws JsonSyntaxException { + throw new JsonSyntaxException("JSON has mismatched brackets"); + } + + private static void failMissingWs() throws JsonSyntaxException { + // we *could* support this, but jackson doesn't, and this makes the + // implementation a little easier (we can do with returning a boolean) + throw new JsonSyntaxException("After top-level scalars, there must be whitespace before the next node"); + } + + private static boolean ws(byte b) { + return b == ' ' || b == '\n' || b == '\r' || b == '\t'; + } + + private enum State { + /** + * Default state, anything that's not inside a string, not a top-level scalar (numbers, + * booleans, null), and not a special state for {@link #unwrapTopLevelArray() unwrapping}. + */ + BASE, + /** + * State inside a string. Braces are ignored, and escape sequences get special handling. + */ + STRING, + /** + * State inside a "top-level scalar", i.e. a boolean, number or {@code null} that is not + * part of an array or object. These are a bit special because unlike strings, which + * terminate on {@code "}, and structures, which terminate on a bracket, these terminate on + * whitespace. + */ + TOP_LEVEL_SCALAR, + /** + * State just after {@code \} inside a {@link #STRING}. The next byte is ignored, and then + * we return to {@link #STRING} state. + */ + ESCAPE, + /** + * Special state for {@link #unwrapTopLevelArray() unwrapping}, before the top-level array. + * At this point we don't know if there is a top-level array that we need to unwrap or not. + */ + BEFORE_UNWRAP_ARRAY, + /** + * Special state for {@link #unwrapTopLevelArray() unwrapping}, after the closing brace of + * a top-level array. Any further tokens after this are an error. + */ + AFTER_UNWRAP_ARRAY, + /** + * Special state for {@link #noTokenization()}. The input is not visited at all, we just + * assume everything is part of one root-level token and buffer it all. + */ + BUFFER_ALL, + } + + /** + * A region that contains a JSON node. Positions are relative to {@link #position()}. + * + * @param start First byte position of this node + * @param end Position after the last byte of this node (i.e. it's exclusive) + */ + public record BufferRegion(long start, long end) { + } +} diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/jackson/JsonHttpContentSubscriberFactory.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/jackson/JsonHttpContentSubscriberFactory.java index f5d3c64dcfe..36a3ae887e6 100644 --- a/http-server-netty/src/main/java/io/micronaut/http/server/netty/jackson/JsonHttpContentSubscriberFactory.java +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/jackson/JsonHttpContentSubscriberFactory.java @@ -18,10 +18,10 @@ import io.micronaut.core.annotation.Internal; import io.micronaut.http.MediaType; import io.micronaut.http.annotation.Consumes; -import io.micronaut.http.server.HttpServerConfiguration; import io.micronaut.http.server.netty.HttpContentProcessor; import io.micronaut.http.server.netty.HttpContentSubscriberFactory; import io.micronaut.http.server.netty.NettyHttpRequest; +import io.micronaut.http.server.netty.configuration.NettyHttpServerConfiguration; import io.micronaut.json.JsonMapper; import jakarta.inject.Singleton; @@ -36,7 +36,7 @@ @Internal public class JsonHttpContentSubscriberFactory implements HttpContentSubscriberFactory { - private final HttpServerConfiguration httpServerConfiguration; + private final NettyHttpServerConfiguration httpServerConfiguration; private final JsonMapper jsonMapper; /** @@ -45,7 +45,7 @@ public class JsonHttpContentSubscriberFactory implements HttpContentSubscriberFa */ public JsonHttpContentSubscriberFactory( JsonMapper jsonMapper, - HttpServerConfiguration httpServerConfiguration) { + NettyHttpServerConfiguration httpServerConfiguration) { this.httpServerConfiguration = httpServerConfiguration; this.jsonMapper = jsonMapper; } diff --git a/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/binding/DefaultJsonErrorHandlingSpec.groovy b/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/binding/DefaultJsonErrorHandlingSpec.groovy index 0a7fd54e339..a83902637fa 100644 --- a/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/binding/DefaultJsonErrorHandlingSpec.groovy +++ b/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/binding/DefaultJsonErrorHandlingSpec.groovy @@ -22,8 +22,8 @@ class DefaultJsonErrorHandlingSpec extends AbstractMicronautSpec { then: HttpClientResponseException e = thrown() - e.response.getBody(Map).get()._embedded.errors[0].message == """Invalid JSON: Unexpected end-of-input - at [Source: UNKNOWN; line: 1, column: 21]""" + e.response.getBody(Map).get()._embedded.errors[0].message == """Invalid JSON: Unexpected end-of-input: expected close marker for Object (start marker at [Source: (byte[])"{"title":"The Stand""; line: 1, column: 1]) + at [Source: (byte[])"{"title":"The Stand""; line: 1, column: 21]""" e.response.status == HttpStatus.BAD_REQUEST when: diff --git a/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/binding/JsonBodyBindingSpec.groovy b/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/binding/JsonBodyBindingSpec.groovy index e61e0399f7a..22d9a309b0e 100644 --- a/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/binding/JsonBodyBindingSpec.groovy +++ b/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/binding/JsonBodyBindingSpec.groovy @@ -1,10 +1,9 @@ package io.micronaut.http.server.netty.binding -import io.micronaut.core.async.annotation.SingleResult import com.fasterxml.jackson.annotation.JsonCreator -import com.fasterxml.jackson.core.JsonParseException import groovy.json.JsonSlurper import io.micronaut.core.annotation.Introspected +import io.micronaut.core.async.annotation.SingleResult import io.micronaut.http.HttpHeaders import io.micronaut.http.HttpRequest import io.micronaut.http.HttpResponse @@ -17,6 +16,7 @@ import io.micronaut.http.client.exceptions.HttpClientResponseException import io.micronaut.http.hateoas.JsonError import io.micronaut.http.hateoas.Link import io.micronaut.http.server.netty.AbstractMicronautSpec +import io.micronaut.json.JsonSyntaxException import org.reactivestreams.Publisher import reactor.core.publisher.Flux import reactor.core.scheduler.Schedulers @@ -72,8 +72,8 @@ class JsonBodyBindingSpec extends AbstractMicronautSpec { then: HttpClientResponseException e = thrown() - e.message == """Invalid JSON: Unexpected character ('T' (code 84)): expected a valid value (JSON String, Number, Array, Object or token 'null', 'true' or 'false') - at [Source: UNKNOWN; line: 1, column: 11]""" + e.message == """Invalid JSON: Unrecognized token 'The': was expecting (JSON String, Number, Array, Object or token 'null', 'true' or 'false') + at [Source: (byte[])"{"title":The Stand}"; line: 1, column: 14]""" e.response.status == HttpStatus.BAD_REQUEST when: @@ -405,10 +405,10 @@ class JsonBodyBindingSpec extends AbstractMicronautSpec { return myReqBody.items*.name } - @Error(JsonParseException) - HttpResponse jsonError(HttpRequest request, JsonParseException jsonParseException) { + @Error(JsonSyntaxException) + HttpResponse jsonError(HttpRequest request, JsonSyntaxException jsonSyntaxException) { def response = HttpResponse.status(HttpStatus.BAD_REQUEST, "No!! Invalid JSON") - def error = new JsonError("Invalid JSON: ${jsonParseException.message}") + def error = new JsonError("Invalid JSON: ${jsonSyntaxException.message}") error.link(Link.SELF, Link.of(request.getUri())) response.body(error) return response diff --git a/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/jackson/JsonCounterSpec.groovy b/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/jackson/JsonCounterSpec.groovy new file mode 100644 index 00000000000..8ebbfdc1e50 --- /dev/null +++ b/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/jackson/JsonCounterSpec.groovy @@ -0,0 +1,194 @@ +package io.micronaut.http.server.netty.jackson + +import com.fasterxml.jackson.core.JsonFactory +import com.fasterxml.jackson.core.JsonParser +import com.fasterxml.jackson.core.JsonToken +import io.micronaut.json.JsonSyntaxException +import io.netty.buffer.ByteBufUtil +import io.netty.buffer.Unpooled +import spock.lang.Specification + +import java.nio.charset.StandardCharsets + +class JsonCounterSpec extends Specification { + private static final JsonFactory FACTORY = new JsonFactory() + + static List toTokens(String input) { + return toTokens(input.getBytes(StandardCharsets.UTF_8)) + } + + static List toTokens(byte[] input) { + def list = [] + // try to parse fully + try (JsonParser parser = FACTORY.createParser(input)) { + //noinspection GroovyEmptyStatementBody + while (true) { + def token = parser.nextToken() + if (token == null) { + break + } + list.add(token) + } + } + return list + } + + def 'compare with jackson'(String input) { + given: + // try to parse fully + toTokens(input) + + when: + def counter = new JsonCounter() + counter.feed(Unpooled.wrappedBuffer(input.getBytes(StandardCharsets.UTF_8))) + then: + !counter.isBuffering() + counter.pollFlushedRegion() == new JsonCounter.BufferRegion(0, input.length()) + + where: + input << ['{}', '[]', '["foo]"]', '{"foo[":"{bar"}', '{"foo":{"bar":"baz"}}'] + } + + static List splitUtf8(byte[] s, boolean unwrapTopLevelArray = false, boolean skipOptional = false) { + def parts = [] + def counter = new JsonCounter() + if (unwrapTopLevelArray) { + counter.unwrapTopLevelArray() + } + int sectionStart = 0 + def buf = Unpooled.wrappedBuffer(s) + def bias = counter.position() + while (buf.isReadable()) { + counter.feed(buf) + def flushedRegion = counter.pollFlushedRegion() + if (flushedRegion != null) { + parts.add(ByteBufUtil.getBytes(buf.slice((int) (flushedRegion.start() - bias), (int) (flushedRegion.end() - flushedRegion.start())))) + } + } + if (counter.isBuffering()) { + def start = (int) (counter.bufferStart() - bias) + parts.add(ByteBufUtil.getBytes(buf.slice(start, buf.writerIndex() - start))) + } + return parts + } + + def 'split compare with jackson'(String stream, List expectedParts) { + given: + def fullTokens = toTokens(stream) + + when: + def parts = splitUtf8(stream.getBytes(StandardCharsets.UTF_8)) + .collect { new String(it, StandardCharsets.UTF_8) } + then: + parts == expectedParts + parts.collectMany { toTokens(it) } == fullTokens + + when: + def partsWithoutOptional = splitUtf8(stream.getBytes(StandardCharsets.UTF_8), false, true) + .collect { new String(it, StandardCharsets.UTF_8) } + then: + partsWithoutOptional.collectMany { toTokens(it) } == fullTokens + + where: + stream | expectedParts + '{}' | ['{}'] + '[{}]' | ['[{}]'] + '[42]' | ['[42]'] + '{}{}' | ['{}', '{}'] + '{}[]' | ['{}', '[]'] + '"foo"42' | ['"foo"', '42'] + '"foo" 42' | ['"foo"', '42'] + //'42"foo"' | ['42', '"foo"'] unsupported + '42 "foo"' | ['42', '"foo"'] + //'42{}' | ['42', '{}'] unsupported + '42 {}' | ['42', '{}'] + } + + def 'split compare with jackson, top level array'(String stream, List expectedParts) { + given: + def fullTokens = toTokens(stream) + if (fullTokens[0] == JsonToken.START_ARRAY) { + // unwrap top-level array + assert fullTokens.last() == JsonToken.END_ARRAY + fullTokens.remove(fullTokens.size() - 1) + fullTokens.remove(0) + } + + when: + def parts = splitUtf8(stream.getBytes(StandardCharsets.UTF_8), true) + .collect { new String(it, StandardCharsets.UTF_8) } + then: + parts == expectedParts + parts.collectMany { toTokens(it) } == fullTokens + + when: + def partsWithoutOptional = splitUtf8(stream.getBytes(StandardCharsets.UTF_8), true, true) + .collect { new String(it, StandardCharsets.UTF_8) } + then: + partsWithoutOptional.collectMany { toTokens(it) } == fullTokens + + where: + stream | expectedParts + '{}' | ['{}'] + '[{}]' | ['{}'] + '[42]' | ['42'] + '{}{}' | ['{}', '{}'] + '{}[]' | ['{}', '[]'] + '"foo"42' | ['"foo"', '42'] + '"foo" 42' | ['"foo"', '42'] + '42 "foo"' | ['42', '"foo"'] + '42 {}' | ['42', '{}'] + '42 []' | ['42', '[]'] + '[1,"foo" ,{}]' | ['1', '"foo"', '{}'] + '[6 ,6]' | ['6', '6'] + } + + def 'illegal inputs'(byte[] input) { + when: + splitUtf8(input, false, false) + then: + thrown JsonSyntaxException + when: + splitUtf8(input, true, false) + then: + thrown JsonSyntaxException + when: + splitUtf8(input, false, true) + then: + thrown JsonSyntaxException + when: + splitUtf8(input, true, true) + then: + thrown JsonSyntaxException + + where: + input << [ + // byte-order mark + new byte[]{0xef, 0xbb, 0xbf, 0x7b, 0x09, 0x7d, 0x09, 0x20, 0x7b, 0x09, 0x09, 0x7d}, + // no space after number + '42"foo"', + '42{}', + '42[]', + // utf-16 + new byte[]{0x22, 0x00, 0x22, 0x5b, 0x22, 0x00}, + ] + } + + def 'illegal inputs unwrapTopLevelArray'(byte[] input) { + when: + splitUtf8(input, true, false) + then: + thrown JsonSyntaxException + when: + splitUtf8(input, true, true) + then: + thrown JsonSyntaxException + + where: + input << [ + '[] 42', + '[{}] "foo"', + '[{}] true', + ] + } +} diff --git a/http-server-netty/src/test/java/io/micronaut/http/server/netty/jackson/JsonContentProcessorBenchmark.java b/http-server-netty/src/test/java/io/micronaut/http/server/netty/jackson/JsonContentProcessorBenchmark.java new file mode 100644 index 00000000000..e866fa8579b --- /dev/null +++ b/http-server-netty/src/test/java/io/micronaut/http/server/netty/jackson/JsonContentProcessorBenchmark.java @@ -0,0 +1,174 @@ +package io.micronaut.http.server.netty.jackson; + +import io.micronaut.context.ApplicationContext; +import io.micronaut.core.annotation.Introspected; +import io.micronaut.core.convert.ConversionService; +import io.micronaut.core.type.Argument; +import io.micronaut.http.server.netty.NettyHttpRequest; +import io.micronaut.http.server.netty.configuration.NettyHttpServerConfiguration; +import io.micronaut.json.JsonMapper; +import io.micronaut.json.JsonSyntaxException; +import io.micronaut.json.convert.LazyJsonNode; +import io.micronaut.json.tree.JsonNode; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.DefaultByteBufHolder; +import io.netty.buffer.PooledByteBufAllocator; +import io.netty.channel.ChannelHandlerAdapter; +import io.netty.channel.embedded.EmbeddedChannel; +import io.netty.handler.codec.http.DefaultHttpRequest; +import io.netty.handler.codec.http.HttpMethod; +import io.netty.handler.codec.http.HttpVersion; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.Param; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.TearDown; +import org.openjdk.jmh.runner.Runner; +import org.openjdk.jmh.runner.options.OptionsBuilder; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.TimeUnit; + +public class JsonContentProcessorBenchmark { + private static final Argument PAYLOAD_ARGUMENT = Argument.of(Payload.class); + + public static void main(String[] args) throws Throwable { + if (false) { + Input input = new Input(); + input.size = "6-100000"; + input.chunkSize = 1024; + input.direct = false; + input.setUp(); + new JsonContentProcessorBenchmark().benchmarkNew(input); + //new JsonContentProcessorBenchmark().benchmarkNew(input); + return; + } + + new Runner(new OptionsBuilder() + .include(JsonContentProcessorBenchmark.class.getSimpleName()) + .mode(Mode.AverageTime) + .timeUnit(TimeUnit.NANOSECONDS) + .forks(1) + .warmupIterations(3) + .measurementIterations(5) + //.addProfiler(LinuxPerfAsmProfiler.class) + //.addProfiler(AsyncProfiler.class, "libPath=/home/yawkat/bin/async-profiler-2.9-linux-x64/build/libasyncProfiler.so;output=flamegraph") + .build()).run(); + } + + @State(Scope.Benchmark) + public static class Input { + @Param({ + //"6", + //"1000", + //"1000000", + //"100000-6", + "6-100000", + }) + public String size; + + @Param({ + //"256", + "1024", + // "1000000", + }) + public int chunkSize; + + @Param({ + //"true", + "false" + }) + public boolean direct; + + byte[] bytes; + List bufs; + + JsonMapper jsonMapper; + NettyHttpRequest request; + NettyHttpServerConfiguration configuration; + + @Setup + public void setUp() throws IOException { + bufs = new ArrayList<>(); + bytes = Files.readAllBytes(Paths.get("/home/yawkat/dev/mn/micronaut-benchmark/test-body-" + size + ".json")); + for (int off = 0; off < bytes.length; off += chunkSize) { + ByteBuf buf = direct ? PooledByteBufAllocator.DEFAULT.directBuffer(chunkSize) : PooledByteBufAllocator.DEFAULT.heapBuffer(chunkSize); + buf.writeBytes(bytes, off, Math.min(chunkSize, bytes.length - off)); + bufs.add(buf); + } + + jsonMapper = ApplicationContext.run().getBean(JsonMapper.class); + configuration = new NettyHttpServerConfiguration(); + EmbeddedChannel ch = new EmbeddedChannel(); + ch.pipeline().addLast(new ChannelHandlerAdapter() { + }); + request = new NettyHttpRequest<>( + new DefaultHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, "/"), + ch.pipeline().firstContext(), + ConversionService.SHARED, + configuration + ); + } + + @TearDown + public void tearDown() { + for (ByteBuf buf : bufs) { + buf.release(); + } + } + } + + @Benchmark + public Payload benchmarkNew(Input input) throws Throwable { + var processor = new JsonContentProcessor(input.request, input.configuration, input.jsonMapper); + List out = new ArrayList<>(); + for (ByteBuf buf : input.bufs) { + processor.onData(new DefaultByteBufHolder(buf.retainedDuplicate()), out); + } + processor.complete(out); + if (out.size() != 1) { + throw new AssertionError(); + } + LazyJsonNode lazy = (LazyJsonNode) out.get(0); + try { + return lazy.parse(input.jsonMapper, PAYLOAD_ARGUMENT); + } finally { + lazy.release(); + } + } + + /* JsonContentProcessorOld isn't committed so this doesn't compile + @Benchmark + public Payload benchmarkOld(Input input) throws Throwable { + var processor = new JsonContentProcessorOld(input.request, input.configuration, input.jsonMapper); + List out = new ArrayList<>(); + for (ByteBuf buf : input.bufs) { + processor.onData(new DefaultByteBufHolder(buf.retain()), out); + } + processor.complete(out); + if (out.size() != 1) { + throw new AssertionError(); + } + JsonNode node = (JsonNode) out.get(0); + return input.jsonMapper.readValueFromTree(node, PAYLOAD_ARGUMENT); + } + */ + + //@Benchmark + public JsonCounter.BufferRegion count(Input input) throws JsonSyntaxException { + JsonCounter counter2 = new JsonCounter(); + for (ByteBuf buf : input.bufs) { + counter2.feed(buf.duplicate()); + } + return counter2.pollFlushedRegion(); + } + + @Introspected + record Payload(List haystack, String needle) {} +} diff --git a/http-server/src/main/java/io/micronaut/http/server/RequestLifecycle.java b/http-server/src/main/java/io/micronaut/http/server/RequestLifecycle.java index f17473c4b7f..b3f1b3c329f 100644 --- a/http-server/src/main/java/io/micronaut/http/server/RequestLifecycle.java +++ b/http-server/src/main/java/io/micronaut/http/server/RequestLifecycle.java @@ -16,6 +16,7 @@ package io.micronaut.http.server; import io.micronaut.core.annotation.Nullable; +import io.micronaut.core.convert.exceptions.ConversionErrorException; import io.micronaut.core.execution.ExecutionFlow; import io.micronaut.core.type.ReturnType; import io.micronaut.core.util.CollectionUtils; @@ -36,6 +37,7 @@ import io.micronaut.inject.BeanDefinition; import io.micronaut.inject.ExecutableMethod; import io.micronaut.inject.qualifiers.Qualifiers; +import io.micronaut.json.JsonSyntaxException; import io.micronaut.web.router.RouteInfo; import io.micronaut.web.router.RouteMatch; import io.micronaut.web.router.UriRouteMatch; @@ -161,13 +163,15 @@ final ExecutionFlow> onErrorNoFilter(Throwable t) { Optional previousRequestRouteInfo = request.getAttribute(HttpAttributes.ROUTE_INFO, RouteInfo.class); Class declaringType = previousRequestRouteInfo.map(RouteInfo::getDeclaringType).orElse(null); - final Throwable cause; - // top level exceptions returned by CompletableFutures. These always wrap the real exception thrown. if ((t instanceof CompletionException || t instanceof ExecutionException) && t.getCause() != null) { - cause = t.getCause(); - } else { - cause = t; + // top level exceptions returned by CompletableFutures. These always wrap the real exception thrown. + t = t.getCause(); + } + if (t instanceof ConversionErrorException cee && cee.getCause() instanceof JsonSyntaxException jse) { + // with delayed parsing, json syntax errors show up as conversion errors + t = jse; } + final Throwable cause = t; RouteMatch errorRoute = routeExecutor.findErrorRoute(cause, declaringType, request); if (errorRoute != null) { diff --git a/http-server/src/main/java/io/micronaut/http/server/exceptions/BaseJsonExceptionHandler.java b/http-server/src/main/java/io/micronaut/http/server/exceptions/BaseJsonExceptionHandler.java new file mode 100644 index 00000000000..d85069a9a9b --- /dev/null +++ b/http-server/src/main/java/io/micronaut/http/server/exceptions/BaseJsonExceptionHandler.java @@ -0,0 +1,54 @@ +/* + * Copyright 2017-2023 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.http.server.exceptions; + +import io.micronaut.http.HttpRequest; +import io.micronaut.http.HttpResponse; +import io.micronaut.http.HttpStatus; +import io.micronaut.http.MutableHttpResponse; +import io.micronaut.http.server.exceptions.response.Error; +import io.micronaut.http.server.exceptions.response.ErrorContext; +import io.micronaut.http.server.exceptions.response.ErrorResponseProcessor; + +import java.util.Optional; + +sealed class BaseJsonExceptionHandler implements ExceptionHandler permits JacksonExceptionHandler, JsonExceptionHandler { + + private final ErrorResponseProcessor responseProcessor; + + BaseJsonExceptionHandler(ErrorResponseProcessor responseProcessor) { + this.responseProcessor = responseProcessor; + } + + @Override + public Object handle(HttpRequest request, E exception) { + MutableHttpResponse response = HttpResponse.status(HttpStatus.BAD_REQUEST, "Invalid JSON"); + return responseProcessor.processResponse(ErrorContext.builder(request) + .cause(exception) + .error(new Error() { + @Override + public String getMessage() { + return "Invalid JSON: " + exception.getMessage(); + } + + @Override + public Optional getTitle() { + return Optional.of("Invalid JSON"); + } + }) + .build(), response); + } +} diff --git a/http-server/src/main/java/io/micronaut/http/server/exceptions/JacksonExceptionHandler.java b/http-server/src/main/java/io/micronaut/http/server/exceptions/JacksonExceptionHandler.java new file mode 100644 index 00000000000..5af5fce5035 --- /dev/null +++ b/http-server/src/main/java/io/micronaut/http/server/exceptions/JacksonExceptionHandler.java @@ -0,0 +1,44 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.http.server.exceptions; + +import com.fasterxml.jackson.core.JsonProcessingException; +import io.micronaut.context.annotation.Requires; +import io.micronaut.core.annotation.Internal; +import io.micronaut.http.annotation.Produces; +import io.micronaut.http.server.exceptions.response.ErrorResponseProcessor; +import jakarta.inject.Singleton; + +/** + * Default exception handler for jackson processing errors. + * + * @author Graeme Rocher + * @since 1.0 + */ +@Produces +@Singleton +@Requires(classes = JsonProcessingException.class) +@Internal +public final class JacksonExceptionHandler extends BaseJsonExceptionHandler implements ExceptionHandler { + /** + * Constructor. + * + * @param responseProcessor Error Response Processor + */ + public JacksonExceptionHandler(ErrorResponseProcessor responseProcessor) { + super(responseProcessor); + } +} diff --git a/http-server/src/main/java/io/micronaut/http/server/exceptions/JsonExceptionHandler.java b/http-server/src/main/java/io/micronaut/http/server/exceptions/JsonExceptionHandler.java index 75042c15d56..92bb422ee01 100644 --- a/http-server/src/main/java/io/micronaut/http/server/exceptions/JsonExceptionHandler.java +++ b/http-server/src/main/java/io/micronaut/http/server/exceptions/JsonExceptionHandler.java @@ -15,21 +15,12 @@ */ package io.micronaut.http.server.exceptions; -import com.fasterxml.jackson.core.JsonProcessingException; -import io.micronaut.context.annotation.Requires; -import io.micronaut.http.HttpRequest; -import io.micronaut.http.HttpResponse; -import io.micronaut.http.HttpStatus; -import io.micronaut.http.MutableHttpResponse; +import io.micronaut.core.annotation.Internal; import io.micronaut.http.annotation.Produces; -import io.micronaut.http.server.exceptions.response.Error; -import io.micronaut.http.server.exceptions.response.ErrorContext; import io.micronaut.http.server.exceptions.response.ErrorResponseProcessor; -import jakarta.inject.Inject; +import io.micronaut.json.JsonSyntaxException; import jakarta.inject.Singleton; -import java.util.Optional; - /** * Default exception handler for JSON processing errors. * @@ -38,36 +29,14 @@ */ @Produces @Singleton -@Requires(classes = JsonProcessingException.class) -public class JsonExceptionHandler implements ExceptionHandler { - - private final ErrorResponseProcessor responseProcessor; - +@Internal +public final class JsonExceptionHandler extends BaseJsonExceptionHandler implements ExceptionHandler { /** * Constructor. + * * @param responseProcessor Error Response Processor */ - @Inject public JsonExceptionHandler(ErrorResponseProcessor responseProcessor) { - this.responseProcessor = responseProcessor; - } - - @Override - public Object handle(HttpRequest request, JsonProcessingException exception) { - MutableHttpResponse response = HttpResponse.status(HttpStatus.BAD_REQUEST, "Invalid JSON"); - return responseProcessor.processResponse(ErrorContext.builder(request) - .cause(exception) - .error(new Error() { - @Override - public String getMessage() { - return "Invalid JSON: " + exception.getMessage(); - } - - @Override - public Optional getTitle() { - return Optional.of("Invalid JSON"); - } - }) - .build(), response); + super(responseProcessor); } } diff --git a/jackson-core/build.gradle b/jackson-core/build.gradle index 3854cc32cd6..da2f0b62af7 100644 --- a/jackson-core/build.gradle +++ b/jackson-core/build.gradle @@ -9,6 +9,7 @@ dependencies { api libs.managed.jackson.core api libs.managed.jackson.annotations + compileOnly libs.managed.netty.buffer testAnnotationProcessor project(":inject-java") testAnnotationProcessor project(":inject-groovy") diff --git a/jackson-core/src/main/java/io/micronaut/jackson/core/parser/JacksonCoreParserFactory.java b/jackson-core/src/main/java/io/micronaut/jackson/core/parser/JacksonCoreParserFactory.java new file mode 100644 index 00000000000..1e392970df7 --- /dev/null +++ b/jackson-core/src/main/java/io/micronaut/jackson/core/parser/JacksonCoreParserFactory.java @@ -0,0 +1,74 @@ +/* + * Copyright 2017-2023 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.jackson.core.parser; + +import com.fasterxml.jackson.core.JsonFactory; +import com.fasterxml.jackson.core.JsonParser; +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.io.buffer.ByteBuffer; +import io.micronaut.core.type.Argument; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufInputStream; + +import java.io.IOException; +import java.io.InputStream; + +/** + * Helper class for implementing + * {@link io.micronaut.json.JsonMapper#readValue(ByteBuffer, Argument)} with optimizations for + * netty ByteBufs. + * + * @author Jonas Konrad + * @since 4.0.0 + */ +@Internal +public final class JacksonCoreParserFactory { + private static final boolean HAS_NETTY_BUFFER; + + private JacksonCoreParserFactory() { + } + + static { + boolean hasNettyBuffer; + try { + Class.forName("io.netty.buffer.ByteBuf", false, null); + hasNettyBuffer = true; + } catch (ClassNotFoundException e) { + hasNettyBuffer = false; + } + HAS_NETTY_BUFFER = hasNettyBuffer; + } + + /** + * Create a jackson {@link JsonParser} for the given input bytes. + * + * @param factory The jackson {@link JsonFactory} for parse features + * @param buffer The input data + * @return The created parser + * @throws IOException On failure of jackson createParser methods + */ + public static JsonParser createJsonParser(JsonFactory factory, ByteBuffer buffer) throws IOException { + if (!HAS_NETTY_BUFFER || !(buffer.asNativeBuffer() instanceof ByteBuf byteBuf)) { + return factory.createParser(buffer.toByteArray()); + } + + if (byteBuf.hasArray()) { + return factory.createParser(byteBuf.array(), byteBuf.readerIndex() + byteBuf.arrayOffset(), byteBuf.readableBytes()); + } else { + return factory.createParser((InputStream) new ByteBufInputStream(byteBuf)); + } + } +} diff --git a/jackson-databind/src/main/java/io/micronaut/jackson/databind/JacksonDatabindMapper.java b/jackson-databind/src/main/java/io/micronaut/jackson/databind/JacksonDatabindMapper.java index fac4e02c1e1..5e84d5b621a 100644 --- a/jackson-databind/src/main/java/io/micronaut/jackson/databind/JacksonDatabindMapper.java +++ b/jackson-databind/src/main/java/io/micronaut/jackson/databind/JacksonDatabindMapper.java @@ -16,6 +16,7 @@ package io.micronaut.jackson.databind; import com.fasterxml.jackson.annotation.JsonView; +import com.fasterxml.jackson.core.JsonParseException; import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.JavaType; @@ -26,17 +27,20 @@ import io.micronaut.core.annotation.Internal; import io.micronaut.core.annotation.NonNull; import io.micronaut.core.annotation.Nullable; +import io.micronaut.core.io.buffer.ByteBuffer; import io.micronaut.core.reflect.InstantiationUtils; import io.micronaut.core.type.Argument; import io.micronaut.jackson.JacksonConfiguration; import io.micronaut.jackson.ObjectMapperFactory; import io.micronaut.jackson.codec.JacksonFeatures; +import io.micronaut.jackson.core.parser.JacksonCoreParserFactory; +import io.micronaut.jackson.core.parser.JacksonCoreProcessor; import io.micronaut.jackson.core.tree.JsonNodeTreeCodec; import io.micronaut.jackson.core.tree.TreeGenerator; -import io.micronaut.json.JsonStreamConfig; -import io.micronaut.json.JsonMapper; import io.micronaut.json.JsonFeatures; -import io.micronaut.jackson.core.parser.JacksonCoreProcessor; +import io.micronaut.json.JsonMapper; +import io.micronaut.json.JsonStreamConfig; +import io.micronaut.json.JsonSyntaxException; import io.micronaut.json.tree.JsonNode; import jakarta.inject.Inject; import jakarta.inject.Singleton; @@ -111,12 +115,29 @@ public JsonNode writeValueToTree(@NonNull Argument type, T value) throws @Override public T readValue(@NonNull InputStream inputStream, @NonNull Argument type) throws IOException { - return objectMapper.readValue(inputStream, JacksonConfiguration.constructType(type, objectMapper.getTypeFactory())); + try { + return objectMapper.readValue(inputStream, JacksonConfiguration.constructType(type, objectMapper.getTypeFactory())); + } catch (JsonParseException pe) { + throw new JsonSyntaxException(pe); + } } @Override public T readValue(@NonNull byte[] byteArray, @NonNull Argument type) throws IOException { - return objectMapper.readValue(byteArray, JacksonConfiguration.constructType(type, objectMapper.getTypeFactory())); + try { + return objectMapper.readValue(byteArray, JacksonConfiguration.constructType(type, objectMapper.getTypeFactory())); + } catch (JsonParseException pe) { + throw new JsonSyntaxException(pe); + } + } + + @Override + public T readValue(ByteBuffer byteBuffer, Argument type) throws IOException { + try (JsonParser parser = JacksonCoreParserFactory.createJsonParser(objectMapper.getFactory(), byteBuffer)) { + return objectMapper.readValue(parser, JacksonConfiguration.constructType(type, objectMapper.getTypeFactory())); + } catch (JsonParseException pe) { + throw new JsonSyntaxException(pe); + } } @Override diff --git a/jackson-databind/src/test/groovy/io/micronaut/jackson/convert/JsonConverterRegistrarSpec.groovy b/jackson-databind/src/test/groovy/io/micronaut/jackson/convert/JsonConverterRegistrarSpec.groovy index 05b919ca505..9285988472f 100644 --- a/jackson-databind/src/test/groovy/io/micronaut/jackson/convert/JsonConverterRegistrarSpec.groovy +++ b/jackson-databind/src/test/groovy/io/micronaut/jackson/convert/JsonConverterRegistrarSpec.groovy @@ -16,8 +16,7 @@ class JsonConverterRegistrarSpec extends Specification { expect: converter.convert(JsonNode.createArrayNode([JsonNode.createStringNode("foo")]), Argument.of(List, String)).get() == ['foo'] converter.convert(JsonNode.createArrayNode([JsonNode.createStringNode("foo")]), Argument.of(Set, String)).get() == new HashSet(['foo']) - // SortedSet not supported - !converter.convert(JsonNode.createArrayNode([JsonNode.createStringNode("foo")]), Argument.of(SortedSet, String)).isPresent() + converter.convert(JsonNode.createArrayNode([JsonNode.createStringNode("foo")]), Argument.of(SortedSet, String)).get() == new TreeSet(['foo']) } def 'json node to ConvertibleValues'() { diff --git a/json-core/src/main/java/io/micronaut/json/JsonMapper.java b/json-core/src/main/java/io/micronaut/json/JsonMapper.java index fa0bb1c9380..2609fc5ea62 100644 --- a/json-core/src/main/java/io/micronaut/json/JsonMapper.java +++ b/json-core/src/main/java/io/micronaut/json/JsonMapper.java @@ -19,6 +19,7 @@ import io.micronaut.core.annotation.Experimental; import io.micronaut.core.annotation.NonNull; import io.micronaut.core.annotation.Nullable; +import io.micronaut.core.io.buffer.ByteBuffer; import io.micronaut.core.order.OrderUtil; import io.micronaut.core.type.Argument; import io.micronaut.json.tree.JsonNode; @@ -87,6 +88,19 @@ default T readValueFromTree(@NonNull JsonNode tree, @NonNull Class type) */ T readValue(@NonNull byte[] byteArray, @NonNull Argument type) throws IOException; + /** + * Parse and map json from the given byte buffer. + * + * @param byteBuffer The input data. + * @param type The type to deserialize to. + * @param Type variable of the return type. + * @return The deserialized object. + * @throws IOException IOException + */ + default T readValue(@NonNull ByteBuffer byteBuffer, @NonNull Argument type) throws IOException { + return readValue(byteBuffer.toByteArray(), type); + } + /** * Parse and map json from the given string. * @@ -108,7 +122,10 @@ default T readValue(@NonNull String string, @NonNull Argument type) throw * @return The reactive processor. */ @NonNull - Processor createReactiveParser(@NonNull Consumer> onSubscribe, boolean streamArray); + @Deprecated + default Processor createReactiveParser(@NonNull Consumer> onSubscribe, boolean streamArray) { + throw new UnsupportedOperationException("Reactive parser not supported"); + } /** * Transform an object value to a json tree. diff --git a/json-core/src/main/java/io/micronaut/json/JsonSyntaxException.java b/json-core/src/main/java/io/micronaut/json/JsonSyntaxException.java new file mode 100644 index 00000000000..0c43c8acecd --- /dev/null +++ b/json-core/src/main/java/io/micronaut/json/JsonSyntaxException.java @@ -0,0 +1,45 @@ +/* + * Copyright 2017-2023 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.json; + +import java.io.IOException; + +/** + * Exception thrown when there is a syntax error in JSON (e.g. mismatched braces). + * + * @since 4.0.0 + * @author Jonas Konrad + */ +public final class JsonSyntaxException extends IOException { + /** + * Construct a syntax exception from a framework exception (e.g. jackson JsonParseException). + * + * @param cause The framework exception + */ + public JsonSyntaxException(Throwable cause) { + // copy the message so it's shown properly to the user + super(cause.getMessage(), cause); + } + + /** + * Construct a syntax exception with just a message. + * + * @param message The message + */ + public JsonSyntaxException(String message) { + super(message); + } +} diff --git a/json-core/src/main/java/io/micronaut/json/convert/JsonConverterRegistrar.java b/json-core/src/main/java/io/micronaut/json/convert/JsonConverterRegistrar.java index 769954dac5a..68796937191 100644 --- a/json-core/src/main/java/io/micronaut/json/convert/JsonConverterRegistrar.java +++ b/json-core/src/main/java/io/micronaut/json/convert/JsonConverterRegistrar.java @@ -18,7 +18,7 @@ import io.micronaut.context.BeanProvider; import io.micronaut.context.annotation.Prototype; import io.micronaut.core.annotation.Experimental; -import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.NonNull; import io.micronaut.core.bind.ArgumentBinder; import io.micronaut.core.bind.BeanPropertyBinder; import io.micronaut.core.convert.ArgumentConversionContext; @@ -31,16 +31,14 @@ import io.micronaut.core.naming.NameUtils; import io.micronaut.core.type.Argument; import io.micronaut.json.JsonMapper; -import io.micronaut.json.tree.JsonArray; +import io.micronaut.json.JsonSyntaxException; import io.micronaut.json.tree.JsonNode; import jakarta.inject.Inject; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.util.ArrayList; -import java.util.Collection; import java.util.LinkedHashMap; -import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Optional; @@ -71,26 +69,37 @@ public JsonConverterRegistrar( @Override public void register(MutableConversionService conversionService) { - conversionService.addConverter( - JsonArray.class, - Object[].class, - arrayNodeToObjectConverter() - ); conversionService.addConverter( JsonNode.class, ConvertibleValues.class, objectNodeToConvertibleValuesConverter() ); conversionService.addConverter( - JsonArray.class, - Iterable.class, - arrayNodeToIterableConverter() + LazyJsonNode.class, + ConvertibleValues.class, + unparsedNodeToConvertibleValuesConverter() ); conversionService.addConverter( JsonNode.class, Object.class, jsonNodeToObjectConverter() ); + conversionService.addConverter( + LazyJsonNode.class, + Object.class, + unparsedJsonNodeToObjectConverter() + ); + // need to register the Object[] conversions explicitly because there is also an Object->Object[] converter + conversionService.addConverter( + JsonNode.class, + Object[].class, + (TypeConverter) jsonNodeToObjectConverter() + ); + conversionService.addConverter( + LazyJsonNode.class, + Object[].class, + (TypeConverter) unparsedJsonNodeToObjectConverter() + ); conversionService.addConverter( Map.class, Object.class, @@ -106,8 +115,7 @@ public void register(MutableConversionService conversionService) { /** * @return A converter that converts object nodes to convertible values */ - @Internal - public TypeConverter objectNodeToConvertibleValuesConverter() { + private TypeConverter objectNodeToConvertibleValuesConverter() { return (object, targetType, context) -> { if (object.isObject()) { return Optional.of(new JsonNodeConvertibleValues<>(object, conversionService)); @@ -119,42 +127,28 @@ public TypeConverter objectNodeToConvertibleValuesC } /** - * @return Converts array nodes to iterables. + * @return A converter that converts object nodes to convertible values */ - public TypeConverter arrayNodeToIterableConverter() { + private TypeConverter unparsedNodeToConvertibleValuesConverter() { return (node, targetType, context) -> { - Collection results; - if (targetType.isAssignableFrom(ArrayList.class)) { - results = new ArrayList<>(); - } else if (targetType.isAssignableFrom(LinkedHashSet.class)) { - results = new LinkedHashSet<>(); - } else { - // don't know how to convert to that collection type + // this is a bit convoluted, only release if we can convert or there is an error + try { + if (!node.isObject()) { + // ConvertibleValues only works for objects + return Optional.empty(); + } + } catch (JsonSyntaxException e) { + node.tryRelease(); + context.reject(e); return Optional.empty(); } - Map> typeVariables = context.getTypeVariables(); - Class elementType = typeVariables.isEmpty() ? Map.class : typeVariables.values().iterator().next().getType(); - for (int i = 0; i < node.size(); i++) { - Optional converted = conversionService.convert(node.get(i), elementType, context); - converted.ifPresent(results::add); - } - return Optional.of(results); - }; - } - - /** - * @return Converts array nodes to objects. - */ - @Internal - public TypeConverter arrayNodeToObjectConverter() { - return (node, targetType, context) -> { try { - JsonMapper om = this.objectCodec.get(); - Object[] result = om.readValueFromTree(node, targetType); - return Optional.of(result); + return Optional.of(new JsonNodeConvertibleValues<>(node.toJsonNode(objectCodec.get()), conversionService)); } catch (IOException e) { context.reject(e); return Optional.empty(); + } finally { + node.tryRelease(); } }; } @@ -206,7 +200,7 @@ private Object correctKeys(Object o) { /** * @return A converter that converts an object to a json node */ - protected TypeConverter objectToJsonNodeConverter() { + private TypeConverter objectToJsonNodeConverter() { return (object, targetType, context) -> { try { return Optional.of(objectCodec.get().writeValueToTree(object)); @@ -217,31 +211,58 @@ protected TypeConverter objectToJsonNodeConverter() { }; } + @NonNull + private static Argument argument(Class targetType, ConversionContext context) { + Argument argument = null; + if (context instanceof ArgumentConversionContext) { + argument = ((ArgumentConversionContext) context).getArgument(); + if (targetType != argument.getType()) { + argument = null; + } + } + if (argument == null) { + argument = Argument.of(targetType); + } + return argument; + } + /** * @return The JSON node to object converter */ - protected TypeConverter jsonNodeToObjectConverter() { + private TypeConverter jsonNodeToObjectConverter() { return (node, targetType, context) -> { try { if (CharSequence.class.isAssignableFrom(targetType) && node.isObject()) { return Optional.of(new String(objectCodec.get().writeValueAsBytes(node), StandardCharsets.UTF_8)); } else { - Argument argument = null; - if (context instanceof ArgumentConversionContext) { - argument = ((ArgumentConversionContext) context).getArgument(); - if (targetType != argument.getType()) { - argument = null; - } - } - if (argument == null) { - argument = Argument.of(targetType); - } - JsonMapper om = this.objectCodec.get(); - return Optional.ofNullable(om.readValueFromTree(node, argument)); + return Optional.ofNullable(this.objectCodec.get().readValueFromTree(node, argument(targetType, context))); + } + } catch (IOException e) { + context.reject(e); + return Optional.empty(); + } + }; + } + + /** + * @return The JSON node to object converter + */ + private TypeConverter unparsedJsonNodeToObjectConverter() { + return (node, targetType, context) -> { + try { + JsonMapper mapper = objectCodec.get(); + if (CharSequence.class.isAssignableFrom(targetType) && node.isObject()) { + // parse once to JsonNode to ensure validity & sanitize the input + byte[] sanitized = mapper.writeValueAsBytes(node.toJsonNode(mapper)); + return Optional.of(new String(sanitized, StandardCharsets.UTF_8)); + } else { + return Optional.ofNullable(node.parse(mapper, argument(targetType, context))); } } catch (IOException e) { context.reject(e); return Optional.empty(); + } finally { + node.tryRelease(); } }; } diff --git a/json-core/src/main/java/io/micronaut/json/convert/LazyJsonNode.java b/json-core/src/main/java/io/micronaut/json/convert/LazyJsonNode.java new file mode 100644 index 00000000000..0cfe68b0566 --- /dev/null +++ b/json-core/src/main/java/io/micronaut/json/convert/LazyJsonNode.java @@ -0,0 +1,209 @@ +/* + * Copyright 2017-2023 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.json.convert; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.NonNull; +import io.micronaut.core.annotation.Nullable; +import io.micronaut.core.io.buffer.ByteBuffer; +import io.micronaut.core.io.buffer.ReferenceCounted; +import io.micronaut.core.type.Argument; +import io.micronaut.json.JsonMapper; +import io.micronaut.json.JsonSyntaxException; +import io.micronaut.json.tree.JsonNode; + +import java.io.IOException; +import java.util.Objects; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; + +/** + * Lazily parsed {@link JsonNode}. + */ +@Internal +public final class LazyJsonNode implements ReferenceCounted { + private final Lock lock = new ReentrantLock(); + @Nullable + private ByteBuffer buffer; + private int refCnt = 1; + @Nullable + private volatile JsonNode asNode; + @Nullable + private JsonSyntaxException syntaxException; + + public LazyJsonNode(@NonNull ByteBuffer buffer) { + this.buffer = Objects.requireNonNull(buffer, "buffer"); + } + + /** + * Parse this JSON to the given type. + * + * @param mapper The mapper to use for parsing + * @param type The target type + * @param The target type + * @return The parsed value + * @throws IOException A {@link JsonSyntaxException} or framework data binding exception + */ + public T parse(JsonMapper mapper, Argument type) throws IOException { + lock.lock(); + try { + if (asNode == null) { + try { + return mapper.readValue(buffer(), type); + } catch (JsonSyntaxException se) { + this.syntaxException = se; + discardBuffer(); + throw se; + } + } else { + return mapper.readValueFromTree(asNode, type); + } + } finally { + lock.unlock(); + } + } + + /** + * Check whether this node is an object. + * + * @return {@code true} if this node is an object + * @throws JsonSyntaxException If the JSON is malformed. Note that this method does not always + * do full parsing, so this exception is best-effort only + */ + boolean isObject() throws JsonSyntaxException { + JsonNode n = asNode; + if (n != null) { + return n.isObject(); + } + + lock.lock(); + try { + n = asNode; + if (n != null) { + return n.isObject(); + } + if (syntaxException != null) { + throw syntaxException; + } + + ByteBuffer buf = buffer(); + if (buf.readableBytes() == 0) { + return false; + } + byte b = buf.getByte(buf.readerIndex()); + if (b == ' ' || b == '\t' || b == '\n' || b == '\r' || b == (byte) 0xef) { + // this should have been handled by the JsonCounter + throw new IllegalStateException("JSON input is not properly trimmed"); + } + return b == '{'; + } finally { + lock.unlock(); + } + } + + /** + * Parse this JSON to a {@link JsonNode}. + * + * @param mapper The JSON mapper to use for parsing + * @return The parsed JSON node + * @throws IOException A {@link JsonSyntaxException} or framework data binding exception + */ + JsonNode toJsonNode(JsonMapper mapper) throws IOException { + if (asNode == null) { + lock.lock(); + try { + if (asNode == null) { + if (syntaxException != null) { + throw syntaxException; + } + + asNode = parse(mapper, Argument.of(JsonNode.class)); + } + discardBuffer(); + } finally { + lock.unlock(); + } + } + return asNode; + } + + @Override + public LazyJsonNode retain() { + lock.lock(); + try { + if (refCnt == 0) { + throw new IllegalStateException("Already released"); + } + refCnt++; + } finally { + lock.unlock(); + } + return this; + } + + private ByteBuffer buffer() { + ByteBuffer b = buffer; + if (b == null) { + throw new IllegalStateException("Buffer not available anymore"); + } + return b; + } + + @Override + public boolean release() { + lock.lock(); + try { + if (refCnt == 0) { + throw new IllegalStateException("Already released"); + } + refCnt--; + if (refCnt == 0) { + discardBuffer(); + return true; + } else { + return false; + } + } finally { + lock.unlock(); + } + } + + /** + * Try to release this node if it hasn't been released already. + */ + @Internal + void tryRelease() { + // this is a bit yikes but it's necessary so we can attempt conversion twice. + // it seems to work fine because the first conversion is to JsonNode, which we store + // locally. + lock.lock(); + try { + if (refCnt != 0) { + release(); + } + } finally { + lock.unlock(); + } + } + + private void discardBuffer() { + // implicit null check here + if (buffer instanceof ReferenceCounted rc) { + rc.release(); + } + buffer = null; + } +} diff --git a/src/main/docs/guide/httpServer/errorHandling/localErrorHandling.adoc b/src/main/docs/guide/httpServer/errorHandling/localErrorHandling.adoc index 0f1c5ffe2e7..16fad50cf82 100644 --- a/src/main/docs/guide/httpServer/errorHandling/localErrorHandling.adoc +++ b/src/main/docs/guide/httpServer/errorHandling/localErrorHandling.adoc @@ -2,7 +2,7 @@ For example, the following method handles JSON parse exceptions from Jackson for snippet::io.micronaut.docs.server.json.PersonController[tags="localError", indent=0, title="Local exception handler"] -<1> A method that explicitly handles `JsonParseException` is declared +<1> A method that explicitly handles `JsonSyntaxException` is declared <2> An instance of api:http.hateoas.JsonError[] is returned. <3> A custom response is returned to handle the error diff --git a/test-suite-groovy/src/test/groovy/io/micronaut/docs/server/json/PersonController.groovy b/test-suite-groovy/src/test/groovy/io/micronaut/docs/server/json/PersonController.groovy index d0f5953c56c..beab90987bf 100644 --- a/test-suite-groovy/src/test/groovy/io/micronaut/docs/server/json/PersonController.groovy +++ b/test-suite-groovy/src/test/groovy/io/micronaut/docs/server/json/PersonController.groovy @@ -15,8 +15,8 @@ */ package io.micronaut.docs.server.json -import com.fasterxml.jackson.core.JsonParseException import io.micronaut.context.annotation.Requires +import io.micronaut.core.async.annotation.SingleResult import io.micronaut.http.HttpRequest import io.micronaut.http.HttpResponse import io.micronaut.http.HttpStatus @@ -27,9 +27,10 @@ import io.micronaut.http.annotation.Get import io.micronaut.http.annotation.Post import io.micronaut.http.hateoas.JsonError import io.micronaut.http.hateoas.Link +import io.micronaut.json.JsonSyntaxException import org.reactivestreams.Publisher import reactor.core.publisher.Mono -import io.micronaut.core.async.annotation.SingleResult + import java.util.concurrent.CompletableFuture import java.util.concurrent.ConcurrentHashMap @@ -98,7 +99,7 @@ class PersonController { // tag::localError[] @Error - HttpResponse jsonError(HttpRequest request, JsonParseException e) { // <1> + HttpResponse jsonError(HttpRequest request, JsonSyntaxException e) { // <1> JsonError error = new JsonError("Invalid JSON: " + e.message) // <2> .link(Link.SELF, Link.of(request.uri)) diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/json/PersonController.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/json/PersonController.kt index 6ae2156ecd4..84ecd547773 100644 --- a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/json/PersonController.kt +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/json/PersonController.kt @@ -15,8 +15,8 @@ */ package io.micronaut.docs.server.json -import com.fasterxml.jackson.core.JsonParseException import io.micronaut.context.annotation.Requires +import io.micronaut.core.async.annotation.SingleResult import io.micronaut.http.HttpRequest import io.micronaut.http.HttpResponse import io.micronaut.http.HttpStatus @@ -27,12 +27,12 @@ import io.micronaut.http.annotation.Get import io.micronaut.http.annotation.Post import io.micronaut.http.hateoas.JsonError import io.micronaut.http.hateoas.Link +import io.micronaut.json.JsonSyntaxException import org.reactivestreams.Publisher import reactor.core.publisher.Mono import java.util.Optional import java.util.concurrent.CompletableFuture import java.util.concurrent.ConcurrentHashMap -import io.micronaut.core.async.annotation.SingleResult @Requires(property = "spec.name", value = "PersonControllerSpec") // tag::class[] @@ -96,7 +96,7 @@ class PersonController { // tag::localError[] @Error - fun jsonError(request: HttpRequest<*>, e: JsonParseException): HttpResponse { // <1> + fun jsonError(request: HttpRequest<*>, e: JsonSyntaxException): HttpResponse { // <1> val error = JsonError("Invalid JSON: ${e.message}") // <2> .link(Link.SELF, Link.of(request.uri)) diff --git a/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/server/json/PersonController.kt b/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/server/json/PersonController.kt index 6ae2156ecd4..84ecd547773 100644 --- a/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/server/json/PersonController.kt +++ b/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/server/json/PersonController.kt @@ -15,8 +15,8 @@ */ package io.micronaut.docs.server.json -import com.fasterxml.jackson.core.JsonParseException import io.micronaut.context.annotation.Requires +import io.micronaut.core.async.annotation.SingleResult import io.micronaut.http.HttpRequest import io.micronaut.http.HttpResponse import io.micronaut.http.HttpStatus @@ -27,12 +27,12 @@ import io.micronaut.http.annotation.Get import io.micronaut.http.annotation.Post import io.micronaut.http.hateoas.JsonError import io.micronaut.http.hateoas.Link +import io.micronaut.json.JsonSyntaxException import org.reactivestreams.Publisher import reactor.core.publisher.Mono import java.util.Optional import java.util.concurrent.CompletableFuture import java.util.concurrent.ConcurrentHashMap -import io.micronaut.core.async.annotation.SingleResult @Requires(property = "spec.name", value = "PersonControllerSpec") // tag::class[] @@ -96,7 +96,7 @@ class PersonController { // tag::localError[] @Error - fun jsonError(request: HttpRequest<*>, e: JsonParseException): HttpResponse { // <1> + fun jsonError(request: HttpRequest<*>, e: JsonSyntaxException): HttpResponse { // <1> val error = JsonError("Invalid JSON: ${e.message}") // <2> .link(Link.SELF, Link.of(request.uri)) diff --git a/test-suite/src/test/java/io/micronaut/docs/server/json/PersonController.java b/test-suite/src/test/java/io/micronaut/docs/server/json/PersonController.java index 17e2e2f85f2..25e06427bdc 100644 --- a/test-suite/src/test/java/io/micronaut/docs/server/json/PersonController.java +++ b/test-suite/src/test/java/io/micronaut/docs/server/json/PersonController.java @@ -15,8 +15,8 @@ */ package io.micronaut.docs.server.json; -import com.fasterxml.jackson.core.JsonParseException; import io.micronaut.context.annotation.Requires; +import io.micronaut.core.async.annotation.SingleResult; import io.micronaut.http.HttpRequest; import io.micronaut.http.HttpResponse; import io.micronaut.http.HttpStatus; @@ -27,14 +27,15 @@ import io.micronaut.http.annotation.Post; import io.micronaut.http.hateoas.JsonError; import io.micronaut.http.hateoas.Link; +import io.micronaut.json.JsonSyntaxException; import org.reactivestreams.Publisher; import reactor.core.publisher.Mono; + import java.util.Collection; import java.util.Map; import java.util.Optional; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; -import io.micronaut.core.async.annotation.SingleResult; @Requires(property = "spec.name", value = "PersonControllerSpec") // tag::class[] @@ -101,7 +102,7 @@ public HttpResponse save(@Body Person person) { // tag::localError[] @Error - public HttpResponse jsonError(HttpRequest request, JsonParseException e) { // <1> + public HttpResponse jsonError(HttpRequest request, JsonSyntaxException e) { // <1> JsonError error = new JsonError("Invalid JSON: " + e.getMessage()) // <2> .link(Link.SELF, Link.of(request.getUri())); From 0cf0a8e0176a3bad0790910efc9ed3dcf280b901 Mon Sep 17 00:00:00 2001 From: Graeme Rocher Date: Fri, 24 Feb 2023 16:36:46 +0100 Subject: [PATCH 508/743] fix validation of write-only config properties (#8826) This PR fixes the R2DBC tests which are failing. They fail because the validator tries to validate write-only properties by reading them via an introspection. This PR disables that and instead validates via the bean argument. Note that a further refinement would be to not generate an introspection here because it is not needed. In addition when the new validator module is integrated the same change will need to be made to the forked module. --- .../core/io/service/SoftServiceLoader.java | 2 +- .../writeonly/WriteOnlyConfigProperties.java | 19 ++++++++++ .../WriteOnlyConfigPropertiesSpec.groovy | 35 +++++++++++++++++++ .../AbstractInitializableBeanDefinition.java | 29 ++++++++++++--- .../validator/DefaultValidator.java | 3 ++ 5 files changed, 83 insertions(+), 5 deletions(-) create mode 100644 inject-java/src/test/groovy/io/micronaut/inject/configproperties/writeonly/WriteOnlyConfigProperties.java create mode 100644 inject-java/src/test/groovy/io/micronaut/inject/configproperties/writeonly/WriteOnlyConfigPropertiesSpec.groovy diff --git a/core/src/main/java/io/micronaut/core/io/service/SoftServiceLoader.java b/core/src/main/java/io/micronaut/core/io/service/SoftServiceLoader.java index 63a9a18e53b..622669b41c2 100644 --- a/core/src/main/java/io/micronaut/core/io/service/SoftServiceLoader.java +++ b/core/src/main/java/io/micronaut/core/io/service/SoftServiceLoader.java @@ -368,7 +368,7 @@ default List load(Predicate condition, Predicate predicate) { return findAll(condition) .map(ServiceDefinition::load) .filter(s -> predicate == null || predicate.test(s)) - .collect(Collectors.toList()); + .toList(); } } diff --git a/inject-java/src/test/groovy/io/micronaut/inject/configproperties/writeonly/WriteOnlyConfigProperties.java b/inject-java/src/test/groovy/io/micronaut/inject/configproperties/writeonly/WriteOnlyConfigProperties.java new file mode 100644 index 00000000000..c8430c5cb31 --- /dev/null +++ b/inject-java/src/test/groovy/io/micronaut/inject/configproperties/writeonly/WriteOnlyConfigProperties.java @@ -0,0 +1,19 @@ +package io.micronaut.inject.configproperties.writeonly; + + +import io.micronaut.context.annotation.ConfigurationProperties; + +import javax.validation.constraints.NotBlank; + +@ConfigurationProperties("test") +public class WriteOnlyConfigProperties { + private String name; + + public void setName(@NotBlank String name) { + this.name = name; + } + + public String name() { + return name; + } +} diff --git a/inject-java/src/test/groovy/io/micronaut/inject/configproperties/writeonly/WriteOnlyConfigPropertiesSpec.groovy b/inject-java/src/test/groovy/io/micronaut/inject/configproperties/writeonly/WriteOnlyConfigPropertiesSpec.groovy new file mode 100644 index 00000000000..5b82c2baac6 --- /dev/null +++ b/inject-java/src/test/groovy/io/micronaut/inject/configproperties/writeonly/WriteOnlyConfigPropertiesSpec.groovy @@ -0,0 +1,35 @@ +package io.micronaut.inject.configproperties.writeonly + +import io.micronaut.context.ApplicationContext +import io.micronaut.context.exceptions.BeanInstantiationException +import spock.lang.Specification + +class WriteOnlyConfigPropertiesSpec extends Specification { + + void "test write-only config properties - valid"() { + given: + def context = ApplicationContext.run('test.name':'test') + def bean = context.getBean(WriteOnlyConfigProperties) + + expect: + bean.name() == 'test' + + cleanup: + context.close() + } + + void "test write-only config properties - invalid"() { + given: + def context = ApplicationContext.run('test.name':' ') + + when: + def bean = context.getBean(WriteOnlyConfigProperties) + + then: + def e = thrown(BeanInstantiationException) + e.message.contains("Validation failed for bean definition") + + cleanup: + context.close() + } +} diff --git a/inject/src/main/java/io/micronaut/context/AbstractInitializableBeanDefinition.java b/inject/src/main/java/io/micronaut/context/AbstractInitializableBeanDefinition.java index 3f1a899a5ba..cf914ea86cd 100644 --- a/inject/src/main/java/io/micronaut/context/AbstractInitializableBeanDefinition.java +++ b/inject/src/main/java/io/micronaut/context/AbstractInitializableBeanDefinition.java @@ -75,6 +75,7 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Optional; import java.util.OptionalDouble; import java.util.OptionalInt; @@ -1038,9 +1039,19 @@ protected final Object getPropertyValueForMethodArgument(BeanResolutionContext r String cliProperty) { MethodReference methodRef = methodInjection[methodIndex]; Argument argument = methodRef.arguments[argIndex]; - try (BeanResolutionContext.Path ignored = resolutionContext.getPath() + try (BeanResolutionContext.Path path = resolutionContext.getPath() .pushMethodArgumentResolve(this, methodRef.methodName, argument, methodRef.arguments)) { - return resolvePropertyValue(resolutionContext, context, argument, propertyValue, cliProperty, false); + Object val = resolvePropertyValue(resolutionContext, context, argument, propertyValue, cliProperty, false); + if (this instanceof ValidatedBeanDefinition validatedBeanDefinition) { + validatedBeanDefinition.validateBeanArgument( + resolutionContext, + Objects.requireNonNull(path.peek()).getInjectionPoint(), + argument, + argIndex, + val + ); + } + return val; } } @@ -1088,9 +1099,19 @@ protected final Object getPropertyValueForSetter(BeanResolutionContext resolutio Argument argument, String propertyValue, String cliProperty) { - try (BeanResolutionContext.Path ignored = resolutionContext.getPath() + try (BeanResolutionContext.Path path = resolutionContext.getPath() .pushMethodArgumentResolve(this, setterName, argument, new Argument[]{argument})) { - return resolvePropertyValue(resolutionContext, context, argument, propertyValue, cliProperty, false); + Object val = resolvePropertyValue(resolutionContext, context, argument, propertyValue, cliProperty, false); + if (this instanceof ValidatedBeanDefinition validatedBeanDefinition) { + validatedBeanDefinition.validateBeanArgument( + resolutionContext, + Objects.requireNonNull(path.peek()).getInjectionPoint(), + argument, + 0, + val + ); + } + return val; } } diff --git a/validation/src/main/java/io/micronaut/validation/validator/DefaultValidator.java b/validation/src/main/java/io/micronaut/validation/validator/DefaultValidator.java index 88dbecffb8b..4569f30fe31 100644 --- a/validation/src/main/java/io/micronaut/validation/validator/DefaultValidator.java +++ b/validation/src/main/java/io/micronaut/validation/validator/DefaultValidator.java @@ -1115,6 +1115,9 @@ private Set> doValidate( @SuppressWarnings("unchecked") final Class rootBeanClass = (Class) rootBean.getClass(); for (BeanProperty constrainedProperty : constrainedProperties) { + if (constrainedProperty.isWriteOnly()) { + continue; + } final Object propertyValue = constrainedProperty.get(object); //noinspection unchecked validateConstrainedPropertyInternal( From c9bfd19b5a8f24a5dcbbc64c722ea5ad2c16bcc1 Mon Sep 17 00:00:00 2001 From: micronaut-build <65172877+micronaut-build@users.noreply.github.com> Date: Sat, 25 Feb 2023 16:26:45 +0100 Subject: [PATCH 509/743] Bump micronaut-aws to 3.14.0 (#8834) --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 6b2b22183a6..accbf9cb916 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -67,7 +67,7 @@ managed-methvin-directory-watcher = "0.16.1" managed-micrometer = "1.10.3" managed-micronaut-acme = "3.2.0" managed-micronaut-aot = "1.1.1" -managed-micronaut-aws = "3.13.1" +managed-micronaut-aws = "3.14.0" managed-micronaut-azure = "3.8.0" managed-micronaut-cache = "3.5.0" managed-micronaut-cassandra = "5.1.1" From 332b5d98d74487ffc64af3b895b744ffd7beeaa1 Mon Sep 17 00:00:00 2001 From: micronaut-build <65172877+micronaut-build@users.noreply.github.com> Date: Sat, 25 Feb 2023 17:40:06 +0100 Subject: [PATCH 510/743] Bump micronaut-aws to 3.10.8 (#8827) --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 2915988406b..a2eb91fadac 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -67,7 +67,7 @@ managed-methvin-directory-watcher = "0.16.1" managed-micrometer = "1.10.3" managed-micronaut-acme = "3.2.0" managed-micronaut-aot = "1.1.1" -managed-micronaut-aws = "3.10.5" +managed-micronaut-aws = "3.10.8" managed-micronaut-azure = "3.7.1" managed-micronaut-cache = "3.5.0" managed-micronaut-cassandra = "5.1.1" From 04581eb01debaef86801a32c050af9d9858a5c03 Mon Sep 17 00:00:00 2001 From: micronaut-build <65172877+micronaut-build@users.noreply.github.com> Date: Sun, 26 Feb 2023 22:46:45 +0100 Subject: [PATCH 511/743] Bump micronaut-aws to 3.10.9 (#8838) --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index a2eb91fadac..d197fe88fd7 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -67,7 +67,7 @@ managed-methvin-directory-watcher = "0.16.1" managed-micrometer = "1.10.3" managed-micronaut-acme = "3.2.0" managed-micronaut-aot = "1.1.1" -managed-micronaut-aws = "3.10.8" +managed-micronaut-aws = "3.10.9" managed-micronaut-azure = "3.7.1" managed-micronaut-cache = "3.5.0" managed-micronaut-cassandra = "5.1.1" From e9589a85a1c1089496670bf1c636d1269989ab17 Mon Sep 17 00:00:00 2001 From: micronaut-build <65172877+micronaut-build@users.noreply.github.com> Date: Mon, 27 Feb 2023 07:13:20 +0100 Subject: [PATCH 512/743] build: micronaut-test to 3.9.0 (#8839) --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index accbf9cb916..0737209cb3c 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -117,7 +117,7 @@ managed-micronaut-serialization = "1.5.0" managed-micronaut-servlet = "3.3.5" managed-micronaut-spring = "4.5.0" managed-micronaut-sql = "4.7.2" -managed-micronaut-test = "3.8.2" +managed-micronaut-test = "3.9.0" managed-micronaut-test-resources = "1.2.3" managed-micronaut-toml = "1.1.3" managed-micronaut-tracing = "4.5.0" From eb3f1cef07acde6d983697f699ecfb956ddb8f9d Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Mon, 27 Feb 2023 07:13:38 +0100 Subject: [PATCH 513/743] test: HttpServerFilter and ExceptionHandler (#8815) --- .../HttpServerFilterExceptionHandlerTest.java | 117 ++++++++++++++++++ 1 file changed, 117 insertions(+) create mode 100644 http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/filter/HttpServerFilterExceptionHandlerTest.java diff --git a/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/filter/HttpServerFilterExceptionHandlerTest.java b/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/filter/HttpServerFilterExceptionHandlerTest.java new file mode 100644 index 00000000000..672d83d2aec --- /dev/null +++ b/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/filter/HttpServerFilterExceptionHandlerTest.java @@ -0,0 +1,117 @@ +/* + * Copyright 2017-2023 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.http.server.tck.tests.filter; + +import io.micronaut.context.annotation.Requires; +import io.micronaut.http.HttpRequest; +import io.micronaut.http.HttpResponse; +import io.micronaut.http.HttpStatus; +import io.micronaut.http.MediaType; +import io.micronaut.http.MutableHttpResponse; +import io.micronaut.http.annotation.Controller; +import io.micronaut.http.annotation.Filter; +import io.micronaut.http.annotation.Get; +import io.micronaut.http.annotation.Produces; +import io.micronaut.http.filter.HttpServerFilter; +import io.micronaut.http.filter.ServerFilterChain; +import io.micronaut.http.server.exceptions.ExceptionHandler; +import io.micronaut.http.server.exceptions.response.ErrorContext; +import io.micronaut.http.server.exceptions.response.ErrorResponseProcessor; +import io.micronaut.http.server.tck.AssertionUtils; +import io.micronaut.http.server.tck.HttpResponseAssertion; +import io.micronaut.http.server.tck.ServerUnderTest; +import io.micronaut.http.server.tck.TestScenario; +import jakarta.inject.Singleton; +import org.junit.jupiter.api.Test; +import org.reactivestreams.Publisher; +import reactor.core.publisher.Mono; + +import java.io.IOException; +import java.util.function.BiConsumer; + +import static io.micronaut.http.annotation.Filter.MATCH_ALL_PATTERN; +@SuppressWarnings({ + "java:S5960", // We're allowed assertions, as these are used in tests only + "checkstyle:MissingJavadocType", + "checkstyle:DesignForExtension" +}) +public class HttpServerFilterExceptionHandlerTest { + private static final String SPEC_NAME = "FilterErrorHandlerTest"; + + @Test + public void exceptionHandlerTest() throws IOException { + assertion(HttpRequest.GET("/foo"), + throwsStatus(HttpStatus.UNPROCESSABLE_ENTITY)); + } + + private static BiConsumer> throwsStatus(HttpStatus status) { + return (server, request) -> AssertionUtils.assertThrows(server, request, HttpResponseAssertion.builder() + .status(status) + .build()); + } + + private static void assertion(HttpRequest request, BiConsumer> assertion) throws IOException { + TestScenario.builder() + .specName(SPEC_NAME) + .request(request) + .assertion(assertion) + .run(); + } + + + static class FooException extends RuntimeException { + + } + + @Filter(value = MATCH_ALL_PATTERN) + @Requires(property = "spec.name", value = SPEC_NAME) + static class ErrorThrowningFilter implements HttpServerFilter { + + @Override + public Publisher> doFilter(HttpRequest request, ServerFilterChain chain) { + return Mono.error(new FooException()); + } + } + + @Requires(property = "spec.name", value = SPEC_NAME) + @Controller("/foo") + static class FooController { + + @Produces(MediaType.TEXT_PLAIN) + @Get + String index() { + return "Hello World"; + } + } + + @Requires(property = "spec.name", value = SPEC_NAME) + @Singleton + static class FooExceptionHandler implements ExceptionHandler> { + + private final ErrorResponseProcessor errorResponseProcessor; + + public FooExceptionHandler(ErrorResponseProcessor errorResponseProcessor) { + this.errorResponseProcessor = errorResponseProcessor; + } + + @Override + public HttpResponse handle(HttpRequest request, FooException exception) { + return errorResponseProcessor.processResponse(ErrorContext.builder(request) + .cause(exception) + .build(), HttpResponse.unprocessableEntity()); + } + } +} From fbcd804696ec3192ffe2e088828f0a797b4e4d0b Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Mon, 27 Feb 2023 07:13:58 +0100 Subject: [PATCH 514/743] test: adds HttpServerFilterTest (#8813) --- .../tests/filter/HttpServerFilterTest.java | 135 ++++++++++++++++++ 1 file changed, 135 insertions(+) create mode 100644 http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/filter/HttpServerFilterTest.java diff --git a/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/filter/HttpServerFilterTest.java b/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/filter/HttpServerFilterTest.java new file mode 100644 index 00000000000..d02b06d90db --- /dev/null +++ b/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/filter/HttpServerFilterTest.java @@ -0,0 +1,135 @@ +/* + * Copyright 2017-2023 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.http.server.tck.tests.filter; + +import io.micronaut.context.annotation.Requires; +import io.micronaut.http.HttpAttributes; +import io.micronaut.http.HttpHeaders; +import io.micronaut.http.HttpRequest; +import io.micronaut.http.HttpResponse; +import io.micronaut.http.HttpStatus; +import io.micronaut.http.MutableHttpResponse; +import io.micronaut.http.annotation.Controller; +import io.micronaut.http.annotation.Filter; +import io.micronaut.http.annotation.Get; +import io.micronaut.http.filter.HttpServerFilter; +import io.micronaut.http.filter.ServerFilterChain; +import io.micronaut.http.server.tck.AssertionUtils; +import io.micronaut.http.server.tck.HttpResponseAssertion; +import io.micronaut.http.server.tck.ServerUnderTest; +import io.micronaut.http.server.tck.TestScenario; +import io.micronaut.web.router.MethodBasedRouteMatch; +import io.micronaut.web.router.RouteMatch; +import jakarta.annotation.security.RolesAllowed; +import org.junit.jupiter.api.Test; +import org.reactivestreams.Publisher; +import reactor.core.publisher.Mono; + +import java.io.IOException; +import java.util.Optional; +import java.util.function.BiConsumer; +import java.util.stream.Stream; + +import static io.micronaut.http.annotation.Filter.MATCH_ALL_PATTERN; + +@SuppressWarnings({ + "java:S5960", // We're allowed assertions, as these are used in tests only + "checkstyle:MissingJavadocType", + "checkstyle:DesignForExtension" +}) +public class HttpServerFilterTest { + private static final String PATH = "/http-server-filter-test"; + private static final String SPEC_NAME = "HttpServerFilterTest"; + + @Test + public void httpServerFilterTest() throws IOException { + assertion(HttpRequest.GET(PATH), + throwsStatus(HttpStatus.UNAUTHORIZED)); + + assertion(HttpRequest.GET(PATH).header(HttpHeaders.AUTHORIZATION, "ROLE_USER"), + throwsStatus(HttpStatus.FORBIDDEN)); + + BiConsumer> okAssertion = (server, request) -> + AssertionUtils.assertDoesNotThrow(server, request, HttpResponseAssertion.builder() + .status(HttpStatus.OK) + .body("foo") + .build()); + + assertion(HttpRequest.GET(PATH).header(HttpHeaders.AUTHORIZATION, "ROLE_ADMIN"), + okAssertion); + + assertion(HttpRequest.GET("/open"), + okAssertion); + } + + private static BiConsumer> throwsStatus(HttpStatus status) { + return (server, request) -> AssertionUtils.assertThrows(server, request, HttpResponseAssertion.builder() + .status(status) + .build()); + } + + private static void assertion(HttpRequest request, BiConsumer> assertion) throws IOException { + TestScenario.builder() + .specName(SPEC_NAME) + .request(request) + .assertion(assertion) + .run(); + } + + @Filter(value = MATCH_ALL_PATTERN) + @Requires(property = "spec.name", value = SPEC_NAME) + static class SecurityFilter implements HttpServerFilter { + + @Override + public Publisher> doFilter(HttpRequest request, ServerFilterChain chain) { + RouteMatch routeMatch = request.getAttribute(HttpAttributes.ROUTE_MATCH, RouteMatch.class).orElse(null); + if (routeMatch instanceof MethodBasedRouteMatch) { + MethodBasedRouteMatch methodRoute = ((MethodBasedRouteMatch) routeMatch); + if (methodRoute.hasAnnotation(RolesAllowed.class)) { + String role = request.getHeaders().get(HttpHeaders.AUTHORIZATION); + if (role == null) { + return Mono.fromCallable(() -> HttpResponse.status(HttpStatus.UNAUTHORIZED)); + } + Optional optionalValue = methodRoute.getValue(RolesAllowed.class, String[].class); + if (optionalValue.isPresent()) { + String[] roles = optionalValue.get(); + if (role != null && Stream.of(roles).anyMatch(r -> r.equals(role))) { + return chain.proceed(request); + } + } + return Mono.fromCallable(() -> HttpResponse.status(HttpStatus.FORBIDDEN)); + } + } + return chain.proceed(request); + } + } + + @Controller + @Requires(property = "spec.name", value = SPEC_NAME) + public static class MyController { + @RolesAllowed("ROLE_ADMIN") + @Get("/http-server-filter-test") + public String rolesAllowed(HttpRequest request) { + return "foo"; + } + + @Get("/open") + public String open(HttpRequest request) { + return "foo"; + } + } + +} From 2c67fb2e88bd4afae049a68d502a5064f5fe5ce7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Hrstka?= Date: Mon, 27 Feb 2023 11:01:49 +0100 Subject: [PATCH 515/743] fix fallback for suspended methods (#8825) Fixes #7101 --- .../retry/intercept/RecoveryInterceptor.java | 36 +++- .../kotlin/io/micronaut/retry/FallbackSpec.kt | 183 ++++++++++++++++++ 2 files changed, 217 insertions(+), 2 deletions(-) create mode 100644 test-suite-kotlin/src/test/kotlin/io/micronaut/retry/FallbackSpec.kt diff --git a/runtime/src/main/java/io/micronaut/retry/intercept/RecoveryInterceptor.java b/runtime/src/main/java/io/micronaut/retry/intercept/RecoveryInterceptor.java index 6a45a8d2430..03a1bf0576f 100644 --- a/runtime/src/main/java/io/micronaut/retry/intercept/RecoveryInterceptor.java +++ b/runtime/src/main/java/io/micronaut/retry/intercept/RecoveryInterceptor.java @@ -84,9 +84,15 @@ public Object intercept(MethodInvocationContext context) { fallbackForReactiveType(context, interceptedMethod.interceptResultAsPublisher()) ); case COMPLETION_STAGE: - return interceptedMethod.handleResult( + if (context.isSuspend()) { + return interceptedMethod.handleResult( + fallbackForSuspend(context, interceptedMethod.interceptResultAsCompletionStage()) + ); + } else { + return interceptedMethod.handleResult( fallbackForFuture(context, interceptedMethod.interceptResultAsCompletionStage()) - ); + ); + } case SYNCHRONOUS: try { return context.proceed(); @@ -193,6 +199,32 @@ private CompletionStage fallbackForFuture(MethodInvocationContext fallbackForSuspend(MethodInvocationContext context, CompletionStage result) { + CompletableFuture newFuture = new CompletableFuture<>(); + result.whenComplete((o, throwable) -> { + if (throwable == null) { + newFuture.complete(o); + } else { + Optional> fallbackMethod = findFallbackMethod(context); + if (fallbackMethod.isPresent()) { + MethodExecutionHandle fallbackHandle = fallbackMethod.get(); + if (LOG.isDebugEnabled()) { + LOG.debug("Type [{}] resolved fallback: {}", context.getTarget().getClass(), fallbackHandle); + } + try { + newFuture.complete(fallbackHandle.invoke(context.getParameterValues())); + } catch (Throwable t) { + newFuture.completeExceptionally(t); + } + } else { + newFuture.completeExceptionally(throwable); + } + } + }); + + return newFuture; + } + /** * Resolves a fallback for the given execution context and exception. * diff --git a/test-suite-kotlin/src/test/kotlin/io/micronaut/retry/FallbackSpec.kt b/test-suite-kotlin/src/test/kotlin/io/micronaut/retry/FallbackSpec.kt new file mode 100644 index 00000000000..1f623c3f7e1 --- /dev/null +++ b/test-suite-kotlin/src/test/kotlin/io/micronaut/retry/FallbackSpec.kt @@ -0,0 +1,183 @@ +package io.micronaut.retry + +import io.micronaut.context.ApplicationContext +import io.micronaut.context.annotation.Requires +import io.micronaut.http.HttpResponse +import io.micronaut.http.HttpStatus +import io.micronaut.http.annotation.Controller +import io.micronaut.http.annotation.Post +import io.micronaut.http.client.annotation.Client +import io.micronaut.retry.annotation.Fallback +import io.micronaut.retry.annotation.Recoverable +import io.micronaut.runtime.server.EmbeddedServer +import kotlinx.coroutines.runBlocking +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import kotlin.test.assertEquals +import kotlin.test.assertNull + +class FallbackSpec { + + lateinit var server: EmbeddedServer + lateinit var fallbackClient: FallbackClient + + @BeforeEach + fun setUp() { + server = ApplicationContext.run(EmbeddedServer::class.java, mapOf("spec.name" to "FallbackClientSpec")) + val context = server.applicationContext + fallbackClient = context.getBean(FallbackClient::class.java) + } + + @AfterEach + fun tearDown() { + server.close() + } + + @Test + fun `server ok with string output`() { + runBlocking { + val response = fallbackClient.stringOutput(false, false) + assertEquals("server ok", response) + } + } + + @Test + fun `server ok with HttpResponse output`() { + runBlocking { + val response = fallbackClient.httpResponseOutput(false, false) + assertEquals(HttpStatus.OK, response.status) + assertEquals("server ok", response.body()) + } + } + + @Test + fun `server ok with null`() { + runBlocking { + val response = fallbackClient.nullOutput(false, false) + assertNull(response) + } + } + + @Test + fun `server fail with string output`() { + runBlocking { + val response = fallbackClient.stringOutput(true, false) + assertEquals("fallback ok", response) + } + } + + @Test + fun `server fail with HttpResponse output`() { + runBlocking { + val response = fallbackClient.httpResponseOutput(true, false) + assertEquals(HttpStatus.OK, response.status) + assertEquals("fallback ok", response.body()) + } + } + + @Test + fun `server fail with null`() { + runBlocking { + val response = fallbackClient.nullOutput(true, false) + assertNull(response) + } + } + + @Test + fun `faillback fail with string output`() { + runBlocking { + val exception = assertThrows { fallbackClient.stringOutput(true, true) } + assertEquals("fallback fail", exception.message) + } + } + + @Test + fun `faillback fail with HttpResponse output`() { + runBlocking { + val exception = assertThrows { fallbackClient.httpResponseOutput(true, true) } + assertEquals("fallback fail", exception.message) + } + } + + @Test + fun `faillback fail with null`() { + runBlocking { + val exception = assertThrows { fallbackClient.nullOutput(true, true) } + assertEquals("fallback fail", exception.message) + } + } + + +} + + +@Requires(property = "spec.name", value = "FallbackClientSpec") +@Controller("/fallback") +class FallbackClientController { + + @Post("stringOutput") + fun stringOutput(serverFail: Boolean, fallbackFail: Boolean): HttpResponse { + return httpResponseOutput(serverFail, fallbackFail) + } + + @Post("httpResponseOutput") + fun httpResponseOutput(serverFail: Boolean, fallbackFail: Boolean): HttpResponse { + return if (serverFail) { + HttpResponse.serverError("server fail") + } else { + HttpResponse.ok("server ok") + } + } + + @Post("nullOutput") + fun nullOutput(serverFail: Boolean, fallbackFail: Boolean): HttpResponse { + return if (serverFail) { + HttpResponse.serverError() + } else { + HttpResponse.ok() + } + } +} + +@Client("/fallback") +@Recoverable(api = FallbackClientFallback::class) +interface FallbackClient { + + @Post("stringOutput") + suspend fun stringOutput(serverFail: Boolean, fallbackFail: Boolean): String + + @Post("httpResponseOutput") + suspend fun httpResponseOutput(serverFail: Boolean, fallbackFail: Boolean): HttpResponse + + @Post("nullOutput") + suspend fun nullOutput(serverFail: Boolean, fallbackFail: Boolean): String? +} + +@Fallback +open class FallbackClientFallback : FallbackClient { + override suspend fun stringOutput(serverFail: Boolean, fallbackFail: Boolean): String { + return if (fallbackFail) { + throw RuntimeException("fallback fail") + } else { + "fallback ok" + } + } + + override suspend fun httpResponseOutput(serverFail: Boolean, fallbackFail: Boolean): HttpResponse { + return if (fallbackFail) { + throw RuntimeException("fallback fail") + } else { + HttpResponse.ok("fallback ok") + } + } + + override suspend fun nullOutput(serverFail: Boolean, fallbackFail: Boolean): String? { + return if (fallbackFail) { + throw RuntimeException("fallback fail") + } else { + null + } + } +} From 28c03b94f70c18a2a760248f27136d5320101144 Mon Sep 17 00:00:00 2001 From: Jonas Konrad Date: Mon, 27 Feb 2023 12:52:17 +0100 Subject: [PATCH 516/743] Support annotation-based HTTP filter declarations (#8422) This PR replaces the filter processing logic with a central `FilterRunner` class, and adds support for annotation-based filter methods (similar to controllers). Changes: - Replace most uses of `HttpFilter` with a new sealed `GenericHttpFilter` interface. `GenericHttpFilter`s are opaque, they are only processed by the `FilterRunner`. A `GenericHttpFilter` can be a legacy filter, a new annotation-based filter method, or one of a few special internal types. - Implement new annotation-based filter parsing in static methods in `FilterRunner`. It scans & validates filter arguments, assigns binders, validates the return value, etc, and finally stuffs the filter metadata into a record that extends `GenericHttpFilter`. This code is also used by a processor that validates filter methods at compile time. - Implement new filter logic in `FilterRunner`. The filter runner has a coroutine-inspired approach and tries to process filters sequentially, avoiding deep reactive call stacks. Reactive code is avoided altogether where possible. --- .../core/async/publisher/Publishers.java | 80 +- .../CompletableFutureExecutionFlowImpl.java | 44 +- .../core/execution/ExecutionFlow.java | 19 +- .../execution/ImperativeExecutionFlow.java | 5 + gradle/libs.versions.toml | 2 +- .../DefaultHttpClientFilterResolver.java | 235 +++- .../http/client/netty/DefaultHttpClient.java | 105 +- .../http/client/aop/ClientFilterSpec.groovy | 32 + .../server/netty/filters/FiltersSpec.groovy | 16 +- .../netty/filters/ServerFilterSpec.groovy | 173 +++ .../netty/stack/InvocationStackSpec.groovy | 4 +- http-server-tck/build.gradle.kts | 1 + .../http/server/tck/AssertionUtils.java | 7 + .../tests/filter/ClientRequestFilterTest.java | 559 ++++++++ .../filter/ClientResponseFilterTest.java | 421 ++++++ .../HttpServerFilterExceptionHandlerTest.java | 111 ++ .../RequestFilterExceptionHandlerTest.java | 108 ++ .../tck/tests/filter/RequestFilterTest.java | 551 ++++++++ .../ResponseFilterExceptionHandlerTest.java | 106 ++ .../tck/tests/filter/ResponseFilterTest.java | 393 ++++++ .../http/server/RequestLifecycle.java | 59 +- .../micronaut/http/server/RouteExecutor.java | 3 +- .../validation/routes/FilterVisitor.java | 133 ++ ...icronaut.inject.visitor.TypeElementVisitor | 1 + .../routes/FilterVisitorSpec.groovy | 99 ++ .../http/annotation/ClientFilter.java | 88 ++ .../http/annotation/RequestFilter.java | 102 ++ .../http/annotation/ResponseFilter.java | 101 ++ .../http/annotation/ServerFilter.java | 74 + .../http/filter/BaseFilterProcessor.java | 192 +++ .../http/filter/DefaultFilterEntry.java | 11 +- .../http/filter/FilterContinuation.java | 64 + .../io/micronaut/http/filter/FilterOrder.java | 66 + .../micronaut/http/filter/FilterRunner.java | 1188 +++++++++++++++++ .../http/filter/GenericHttpFilter.java | 153 +++ .../http/filter/HttpClientFilterResolver.java | 2 +- .../http/filter/HttpFilterResolver.java | 52 +- .../http/filter/HttpServerFilterResolver.java | 2 +- .../execution/ReactiveExecutionFlow.java | 3 + .../execution/ReactorExecutionFlowImpl.java | 19 + .../filter/BaseFilterProcessorSpec.groovy | 17 + .../http/filter/FilterRunnerSpec.groovy | 736 ++++++++++ .../ReactorExecutionFlowImplSpec.groovy | 100 ++ .../router/AnnotatedFilterRouteBuilder.java | 6 +- .../web/router/BeanDefinitionFilterRoute.java | 9 +- .../web/router/DefaultFilterRoute.java | 36 +- .../web/router/DefaultRouteBuilder.java | 38 +- .../micronaut/web/router/DefaultRouter.java | 27 +- .../io/micronaut/web/router/FilterRoute.java | 7 +- .../io/micronaut/web/router/RouteBuilder.java | 14 +- .../java/io/micronaut/web/router/Router.java | 8 +- .../web/router/ServerFilterRouteBuilder.java | 111 ++ .../web/router/filter/FilteredRouter.java | 8 +- .../web/router/DefaultFilterRouteSpec.groovy | 26 +- .../docs/guide/httpClient/clientFilter.adoc | 8 +- src/main/docs/guide/httpServer/filters.adoc | 80 +- .../httpServer/filters/filterPatterns.adoc | 31 + .../httpServer/filters/filtermethods.adoc | 9 + .../filters/filtermethods/continuations.adoc | 10 + .../filters/filtermethods/errorStates.adoc | 20 + .../filtermethods/filtermethodsexample.adoc | 21 + .../httpServer/filters/httpServerFilter.adoc | 14 + .../httpServerFilterErrorStates.adoc | 1 + .../httpServerFilterExample.adoc | 26 + .../docs/guide/introduction/whatsNew.adoc | 1 + src/main/docs/guide/toc.yml | 13 +- .../client/ThirdPartyClientFilterSpec.groovy | 22 +- .../filter/BasicAuthClientFilter.groovy | 18 +- .../client/filter/GoogleAuthFilter.groovy | 39 +- .../docs/server/filters/TraceFilter.groovy | 21 +- .../server/filters/TraceFilterSpec.groovy | 1 + .../filters/filtermethods/TraceFilter.groovy | 56 + .../TraceFilterMethodsSpec.groovy | 35 + .../filters/filtermethods/TraceService.groovy | 40 + .../continuations/TraceFilter.groovy | 57 + .../TraceFilterContinuationSpec.groovy | 34 + .../docs/client/ThirdPartyClientFilterSpec.kt | 25 +- .../client/filter/BasicAuthClientFilter.kt | 17 +- .../docs/client/filter/GoogleAuthFilter.kt | 31 +- .../docs/server/filters/TraceFilter.kt | 18 +- .../docs/server/filters/TraceFilterSpec.kt | 4 +- .../docs/server/filters/TraceService.kt | 2 +- .../filters/filtermethods/TraceFilter.kt | 45 + .../filters/filtermethods/TraceFilterSpec.kt | 28 + .../filters/filtermethods/TraceService.kt | 39 + .../continuations/TraceFilter.kt | 49 + .../TraceFilterContinuationsSpec.kt | 28 + .../client/ThirdPartyClientFilterSpec.java | 22 +- .../client/filter/BasicAuthClientFilter.java | 18 +- .../docs/client/filter/GoogleAuthFilter.java | 30 +- .../docs/server/filters/TraceFilter.java | 19 +- .../docs/server/filters/TraceFilterSpec.java | 1 + .../docs/server/filters/TraceService.java | 4 +- .../filters/filtermethods/TraceFilter.java | 52 + .../filtermethods/TraceFilterMethodsSpec.java | 53 + .../filters/filtermethods/TraceService.java | 28 +- .../continuations/TraceFilter.java | 51 + .../TraceFilterContinuationSpec.java | 53 + 98 files changed, 7106 insertions(+), 595 deletions(-) create mode 100644 http-server-netty/src/test/groovy/io/micronaut/http/server/netty/filters/ServerFilterSpec.groovy create mode 100644 http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/filter/ClientRequestFilterTest.java create mode 100644 http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/filter/ClientResponseFilterTest.java create mode 100644 http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/filter/HttpServerFilterExceptionHandlerTest.java create mode 100644 http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/filter/RequestFilterExceptionHandlerTest.java create mode 100644 http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/filter/RequestFilterTest.java create mode 100644 http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/filter/ResponseFilterExceptionHandlerTest.java create mode 100644 http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/filter/ResponseFilterTest.java create mode 100644 http-validation/src/main/java/io/micronaut/validation/routes/FilterVisitor.java create mode 100644 http-validation/src/test/groovy/io/micronaut/validation/routes/FilterVisitorSpec.groovy create mode 100644 http/src/main/java/io/micronaut/http/annotation/ClientFilter.java create mode 100644 http/src/main/java/io/micronaut/http/annotation/RequestFilter.java create mode 100644 http/src/main/java/io/micronaut/http/annotation/ResponseFilter.java create mode 100644 http/src/main/java/io/micronaut/http/annotation/ServerFilter.java create mode 100644 http/src/main/java/io/micronaut/http/filter/BaseFilterProcessor.java create mode 100644 http/src/main/java/io/micronaut/http/filter/FilterContinuation.java create mode 100644 http/src/main/java/io/micronaut/http/filter/FilterOrder.java create mode 100644 http/src/main/java/io/micronaut/http/filter/FilterRunner.java create mode 100644 http/src/main/java/io/micronaut/http/filter/GenericHttpFilter.java create mode 100644 http/src/test/groovy/io/micronaut/http/filter/BaseFilterProcessorSpec.groovy create mode 100644 http/src/test/groovy/io/micronaut/http/filter/FilterRunnerSpec.groovy create mode 100644 http/src/test/groovy/io/micronaut/http/reactive/execution/ReactorExecutionFlowImplSpec.groovy create mode 100644 router/src/main/java/io/micronaut/web/router/ServerFilterRouteBuilder.java create mode 100644 src/main/docs/guide/httpServer/filters/filterPatterns.adoc create mode 100644 src/main/docs/guide/httpServer/filters/filtermethods.adoc create mode 100644 src/main/docs/guide/httpServer/filters/filtermethods/continuations.adoc create mode 100644 src/main/docs/guide/httpServer/filters/filtermethods/errorStates.adoc create mode 100644 src/main/docs/guide/httpServer/filters/filtermethods/filtermethodsexample.adoc create mode 100644 src/main/docs/guide/httpServer/filters/httpServerFilter.adoc create mode 100644 src/main/docs/guide/httpServer/filters/httpServerFilter/httpServerFilterErrorStates.adoc create mode 100644 src/main/docs/guide/httpServer/filters/httpServerFilter/httpServerFilterExample.adoc create mode 100644 test-suite-groovy/src/test/groovy/io/micronaut/docs/server/filters/filtermethods/TraceFilter.groovy create mode 100644 test-suite-groovy/src/test/groovy/io/micronaut/docs/server/filters/filtermethods/TraceFilterMethodsSpec.groovy create mode 100644 test-suite-groovy/src/test/groovy/io/micronaut/docs/server/filters/filtermethods/TraceService.groovy create mode 100644 test-suite-groovy/src/test/groovy/io/micronaut/docs/server/filters/filtermethods/continuations/TraceFilter.groovy create mode 100644 test-suite-groovy/src/test/groovy/io/micronaut/docs/server/filters/filtermethods/continuations/TraceFilterContinuationSpec.groovy create mode 100644 test-suite-kotlin/src/test/kotlin/io/micronaut/docs/server/filters/filtermethods/TraceFilter.kt create mode 100644 test-suite-kotlin/src/test/kotlin/io/micronaut/docs/server/filters/filtermethods/TraceFilterSpec.kt create mode 100644 test-suite-kotlin/src/test/kotlin/io/micronaut/docs/server/filters/filtermethods/TraceService.kt create mode 100644 test-suite-kotlin/src/test/kotlin/io/micronaut/docs/server/filters/filtermethods/continuations/TraceFilter.kt create mode 100644 test-suite-kotlin/src/test/kotlin/io/micronaut/docs/server/filters/filtermethods/continuations/TraceFilterContinuationsSpec.kt create mode 100644 test-suite/src/test/java/io/micronaut/docs/server/filters/filtermethods/TraceFilter.java create mode 100644 test-suite/src/test/java/io/micronaut/docs/server/filters/filtermethods/TraceFilterMethodsSpec.java rename http/src/main/java/io/micronaut/http/filter/FilterOrderProvider.java => test-suite/src/test/java/io/micronaut/docs/server/filters/filtermethods/TraceService.java (52%) create mode 100644 test-suite/src/test/java/io/micronaut/docs/server/filters/filtermethods/continuations/TraceFilter.java create mode 100644 test-suite/src/test/java/io/micronaut/docs/server/filters/filtermethods/continuations/TraceFilterContinuationSpec.java diff --git a/core-reactive/src/main/java/io/micronaut/core/async/publisher/Publishers.java b/core-reactive/src/main/java/io/micronaut/core/async/publisher/Publishers.java index 64d73110953..8be1f64e3c1 100644 --- a/core-reactive/src/main/java/io/micronaut/core/async/publisher/Publishers.java +++ b/core-reactive/src/main/java/io/micronaut/core/async/publisher/Publishers.java @@ -18,7 +18,6 @@ import io.micronaut.core.annotation.Internal; import io.micronaut.core.annotation.NonNull; import io.micronaut.core.annotation.TypeHint; -import io.micronaut.core.async.subscriber.Completable; import io.micronaut.core.async.subscriber.CompletionAwareSubscriber; import io.micronaut.core.convert.ConversionService; import io.micronaut.core.optim.StaticOptimizations; @@ -28,7 +27,7 @@ import org.reactivestreams.Subscription; import java.util.ArrayList; -import java.util.Arrays; +import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Objects; @@ -38,6 +37,7 @@ import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Supplier; +import java.util.stream.Stream; /** * Utilities for working with raw {@link Publisher} instances. Designed for internal use by Micronaut and @@ -50,15 +50,12 @@ @TypeHint(Publishers.class) public class Publishers { - @SuppressWarnings("ConstantName") private static final List> REACTIVE_TYPES; - @SuppressWarnings("ConstantName") private static final List> SINGLE_TYPES; - private static final List> COMPLETABLE_TYPES; static { - List> reactiveTypes ; + List> reactiveTypes; List> singleTypes; List> completableTypes; ClassLoader classLoader = Publishers.class.getClassLoader(); @@ -72,27 +69,11 @@ public class Publishers { reactiveTypes = new ArrayList<>(3); singleTypes = new ArrayList<>(3); completableTypes = new ArrayList<>(3); - singleTypes.add(CompletableFuturePublisher.class); - singleTypes.add(JustPublisher.class); - completableTypes.add(Completable.class); - List typeNames = Arrays.asList( - "io.reactivex.Observable", - "reactor.core.publisher.Flux", - "kotlinx.coroutines.flow.Flow", - "io.reactivex.rxjava3.core.Flowable", - "io.reactivex.rxjava3.core.Observable" - ); - for (String name : typeNames) { + for (String name : getNonSpecificReactiveTypeNames()) { Optional> aClass = ClassUtils.forName(name, classLoader); aClass.ifPresent(reactiveTypes::add); } - for (String name : Arrays.asList( - "io.reactivex.Single", - "reactor.core.publisher.Mono", - "io.reactivex.Maybe", - "io.reactivex.rxjava3.core.Single", - "io.reactivex.rxjava3.core.Maybe" - )) { + for (String name : getSingleTypeNames()) { Optional> aClass = ClassUtils.forName(name, classLoader); aClass.ifPresent(aClass1 -> { singleTypes.add(aClass1); @@ -100,7 +81,7 @@ public class Publishers { }); } - for (String name : Arrays.asList("io.reactivex.Completable", "io.reactivex.rxjava3.core.Completable")) { + for (String name : getCompletableTypeNames()) { Optional> aClass = ClassUtils.forName(name, classLoader); aClass.ifPresent(aClass1 -> { completableTypes.add(aClass1); @@ -113,6 +94,49 @@ public class Publishers { COMPLETABLE_TYPES = completableTypes; } + @NonNull + private static List getSingleTypeNames() { + return List.of( + "io.micronaut.core.async.publisher.CompletableFuturePublisher", + "io.micronaut.core.async.publisher.Publishers$JustPublisher", + "io.reactivex.Single", + "reactor.core.publisher.Mono", + "io.reactivex.Maybe", + "io.reactivex.rxjava3.core.Single", + "io.reactivex.rxjava3.core.Maybe" + ); + } + + @NonNull + private static List getCompletableTypeNames() { + return List.of( + "io.reactivex.Completable", + "io.reactivex.rxjava3.core.Completable", + "io.micronaut.core.async.subscriber.Completable" + ); + } + + @NonNull + private static List getNonSpecificReactiveTypeNames() { + return List.of( + "io.reactivex.Observable", + "reactor.core.publisher.Flux", + "kotlinx.coroutines.flow.Flow", + "io.reactivex.rxjava3.core.Flowable", + "io.reactivex.rxjava3.core.Observable" + ); + } + + @NonNull + public static List getReactiveTypeNames() { + return Stream.of( + getNonSpecificReactiveTypeNames(), + getSingleTypeNames(), + getCompletableTypeNames(), + List.of("org.reactivestreams.Publisher") + ).flatMap(Collection::stream).toList(); + } + /** * Registers an additional reactive type. Should be called during application static initialization. * @param type The type @@ -152,7 +176,7 @@ public static void registerReactiveCompletable(Class type) { * @return A list of known reactive types. */ public static List> getKnownReactiveTypes() { - return Collections.unmodifiableList(new ArrayList<>(REACTIVE_TYPES)); + return List.copyOf(REACTIVE_TYPES); } /** @@ -469,8 +493,8 @@ public static T convertPublisher(ConversionService conversionService, Object if (publisherType.isInstance(object)) { return (T) object; } - if (object instanceof CompletableFuture) { - @SuppressWarnings("unchecked") Publisher futurePublisher = Publishers.fromCompletableFuture(() -> ((CompletableFuture) object)); + if (object instanceof CompletableFuture cf && !(object instanceof Publisher)) { + @SuppressWarnings("unchecked") Publisher futurePublisher = Publishers.fromCompletableFuture(() -> cf); return conversionService.convert(futurePublisher, publisherType) .orElseThrow(() -> unconvertibleError(object, publisherType)); } diff --git a/core/src/main/java/io/micronaut/core/execution/CompletableFutureExecutionFlowImpl.java b/core/src/main/java/io/micronaut/core/execution/CompletableFutureExecutionFlowImpl.java index 546a7e24abb..c78cf5483c2 100644 --- a/core/src/main/java/io/micronaut/core/execution/CompletableFutureExecutionFlowImpl.java +++ b/core/src/main/java/io/micronaut/core/execution/CompletableFutureExecutionFlowImpl.java @@ -16,8 +16,10 @@ package io.micronaut.core.execution; import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.Nullable; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; import java.util.concurrent.CompletionStage; import java.util.function.BiConsumer; import java.util.function.Function; @@ -40,6 +42,10 @@ final class CompletableFutureExecutionFlowImpl implements CompletableFutureExecu @Override public ExecutionFlow flatMap(Function> transformer) { + ImperativeExecutionFlow completedFlow = tryComplete(); + if (completedFlow != null) { + return completedFlow.flatMap(transformer); + } stage = stage.thenCompose(value -> { if (value != null) { return (CompletionStage) transformer.apply(value).toCompletableFuture(); @@ -57,13 +63,22 @@ public ExecutionFlow then(Supplier> @Override public ExecutionFlow map(Function function) { - stage = stage.thenApply(function::apply); + stage = stage.thenApply(function); return (ExecutionFlow) this; } @Override public ExecutionFlow onErrorResume(Function> fallback) { - stage = stage.exceptionallyCompose(throwable -> (CompletionStage) fallback.apply(throwable).toCompletableFuture()); + ImperativeExecutionFlow completedFlow = tryComplete(); + if (completedFlow != null) { + return completedFlow.onErrorResume(fallback); + } + stage = stage.exceptionallyCompose(throwable -> { + if (throwable instanceof CompletionException completionException) { + throwable = completionException.getCause(); + } + return (CompletionStage) fallback.apply(throwable).toCompletableFuture(); + }); return this; } @@ -74,12 +89,37 @@ public ExecutionFlow putInContext(String key, Object value) { @Override public void onComplete(BiConsumer fn) { + ImperativeExecutionFlow completedFlow = tryComplete(); + if (completedFlow != null) { + completedFlow.onComplete(fn); + return; + } stage.handle((o, throwable) -> { + if (throwable instanceof CompletionException completionException) { + throwable = completionException.getCause(); + } fn.accept(o, throwable); return null; }); } + @Nullable + @Override + public ImperativeExecutionFlow tryComplete() { + if (stage.isDone()) { + try { + return new ImperativeExecutionFlowImpl(stage.getNow(null), null); + } catch (Throwable throwable) { + if (throwable instanceof CompletionException completionException) { + throwable = completionException.getCause(); + } + return new ImperativeExecutionFlowImpl(null, throwable); + } + } else { + return null; + } + } + @Override public CompletableFuture toCompletableFuture() { return stage; diff --git a/core/src/main/java/io/micronaut/core/execution/ExecutionFlow.java b/core/src/main/java/io/micronaut/core/execution/ExecutionFlow.java index 2dc2796258d..892bb361f91 100644 --- a/core/src/main/java/io/micronaut/core/execution/ExecutionFlow.java +++ b/core/src/main/java/io/micronaut/core/execution/ExecutionFlow.java @@ -20,6 +20,7 @@ import io.micronaut.core.annotation.Nullable; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; import java.util.concurrent.Executor; import java.util.function.BiConsumer; import java.util.function.Function; @@ -85,6 +86,9 @@ static ExecutionFlow async(@NonNull Executor executor, @NonNull Supplier< CompletableFuture completableFuture = new CompletableFuture<>(); executor.execute(() -> supplier.get().onComplete((t, throwable) -> { if (throwable != null) { + if (throwable instanceof CompletionException completionException) { + throwable = completionException.getCause(); + } completableFuture.completeExceptionally(throwable); } else { completableFuture.complete(t); @@ -143,12 +147,22 @@ static ExecutionFlow async(@NonNull Executor executor, @NonNull Supplier< ExecutionFlow putInContext(@NonNull String key, @NonNull Object value); /** - * Invokes a provided function when the flow is resolved. + * Invokes a provided function when the flow is resolved, or immediately if it is already done. * * @param fn The function */ void onComplete(@NonNull BiConsumer fn); + /** + * Create an {@link ImperativeExecutionFlow} from this execution flow, if possible. The flow + * will have its result immediately available. + * + * @return The imperative flow, or {@code null} if this flow is not complete or does not + * support this operation + */ + @Nullable + ImperativeExecutionFlow tryComplete(); + /** * Converts the existing flow into the completable future. * @@ -159,6 +173,9 @@ default CompletableFuture toCompletableFuture() { CompletableFuture completableFuture = new CompletableFuture<>(); onComplete((value, throwable) -> { if (throwable != null) { + if (throwable instanceof CompletionException completionException) { + throwable = completionException.getCause(); + } CompletableFuture.failedFuture(throwable); } CompletableFuture.completedFuture(value); diff --git a/core/src/main/java/io/micronaut/core/execution/ImperativeExecutionFlow.java b/core/src/main/java/io/micronaut/core/execution/ImperativeExecutionFlow.java index a676249e8e3..c33516dcd59 100644 --- a/core/src/main/java/io/micronaut/core/execution/ImperativeExecutionFlow.java +++ b/core/src/main/java/io/micronaut/core/execution/ImperativeExecutionFlow.java @@ -49,4 +49,9 @@ public interface ImperativeExecutionFlow extends ExecutionFlow { @NonNull Map getContext(); + @NonNull + @Override + default ImperativeExecutionFlow tryComplete() { + return this; + } } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index b89b6381952..5bd439ee5ea 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -43,7 +43,7 @@ micronaut-session = "1.0.0-SNAPSHOT" micronaut-sql = "4.7.2" micronaut-test = "4.0.0-SNAPSHOT" micronaut-serde = "2.0.0-SNAPSHOT" -micronaut-tracing = "4.5.0" +micronaut-tracing = "5.0.0-SNAPSHOT" micrometer = "1.10.2" neo4j-java-driver = "1.4.5" selenium = "4.7.2" diff --git a/http-client-core/src/main/java/io/micronaut/http/client/filter/DefaultHttpClientFilterResolver.java b/http-client-core/src/main/java/io/micronaut/http/client/filter/DefaultHttpClientFilterResolver.java index e861dea2d61..f0fbb2736b0 100644 --- a/http-client-core/src/main/java/io/micronaut/http/client/filter/DefaultHttpClientFilterResolver.java +++ b/http-client-core/src/main/java/io/micronaut/http/client/filter/DefaultHttpClientFilterResolver.java @@ -15,18 +15,26 @@ */ package io.micronaut.http.client.filter; +import io.micronaut.context.BeanContext; import io.micronaut.context.annotation.BootstrapContextCompatible; import io.micronaut.core.annotation.AnnotationMetadata; import io.micronaut.core.annotation.AnnotationMetadataResolver; import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.NonNull; +import io.micronaut.core.annotation.Nullable; +import io.micronaut.core.order.OrderUtil; import io.micronaut.core.util.ArrayUtils; +import io.micronaut.core.util.CollectionUtils; import io.micronaut.core.util.StringUtils; -import io.micronaut.core.util.Toggleable; import io.micronaut.http.HttpMethod; import io.micronaut.http.HttpRequest; +import io.micronaut.http.annotation.ClientFilter; import io.micronaut.http.annotation.Filter; import io.micronaut.http.annotation.FilterMatcher; +import io.micronaut.http.filter.BaseFilterProcessor; +import io.micronaut.http.filter.FilterOrder; import io.micronaut.http.filter.FilterPatternStyle; +import io.micronaut.http.filter.GenericHttpFilter; import io.micronaut.http.filter.HttpClientFilter; import io.micronaut.http.filter.HttpClientFilterResolver; import jakarta.inject.Singleton; @@ -34,9 +42,11 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; +import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Set; +import java.util.function.Supplier; import java.util.stream.Collectors; /** @@ -49,97 +59,53 @@ @Internal @Singleton @BootstrapContextCompatible -public class DefaultHttpClientFilterResolver implements HttpClientFilterResolver { +public class DefaultHttpClientFilterResolver extends BaseFilterProcessor implements HttpClientFilterResolver { - private final List clientFilters; - private final AnnotationMetadataResolver annotationMetadataResolver; + private final List clientFilters; /** * Default constructor. * + * @param beanContext The bean context * @param annotationMetadataResolver The annotation metadata resolver - * @param clientFilters All client filters + * @param legacyClientFilters All client filters */ public DefaultHttpClientFilterResolver( - AnnotationMetadataResolver annotationMetadataResolver, - List clientFilters) { - this.annotationMetadataResolver = annotationMetadataResolver; - this.clientFilters = clientFilters; + BeanContext beanContext, + AnnotationMetadataResolver annotationMetadataResolver, + List legacyClientFilters) { + super(beanContext, ClientFilter.class); + this.clientFilters = legacyClientFilters.stream() + .map(legacyClientFilter -> createClientFilterEntry(annotationMetadataResolver, legacyClientFilter)) + .collect(Collectors.toList()); } @Override - public List> resolveFilterEntries(ClientFilterResolutionContext context) { + public List resolveFilterEntries(ClientFilterResolutionContext context) { return clientFilters.stream() - .map(httpClientFilter -> { - AnnotationMetadata annotationMetadata = annotationMetadataResolver.resolveMetadata(httpClientFilter); - HttpMethod[] methods = annotationMetadata.enumValues(Filter.class, "methods", HttpMethod.class); - FilterPatternStyle patternStyle = annotationMetadata.enumValue(Filter.class, - "patternStyle", FilterPatternStyle.class).orElse(FilterPatternStyle.ANT); - final Set httpMethods = new HashSet<>(Arrays.asList(methods)); - if (annotationMetadata.hasStereotype(FilterMatcher.class)) { - httpMethods.addAll( - Arrays.asList(annotationMetadata.enumValues(FilterMatcher.class, "methods", HttpMethod.class)) - ); - } - - return FilterEntry.of( - httpClientFilter, - annotationMetadata, - httpMethods, - patternStyle, - annotationMetadata.stringValues(Filter.class) - ); - }).filter(entry -> { - AnnotationMetadata annotationMetadata = entry.getAnnotationMetadata(); - boolean matches = !annotationMetadata.hasStereotype(FilterMatcher.class); - String filterAnnotation = annotationMetadata.getAnnotationNameByStereotype(FilterMatcher.class).orElse(null); - if (filterAnnotation != null && !matches) { - matches = context.getAnnotationMetadata().hasStereotype(filterAnnotation); - } - - if (matches) { - String[] serviceIds = annotationMetadata.stringValues(Filter.class, "serviceId"); - if (ArrayUtils.isNotEmpty(serviceIds)) { - matches = containsIdentifier(context.getClientIds(), serviceIds); - } - } - if (matches) { - String[] serviceIdsExclude = annotationMetadata.stringValues(Filter.class, "excludeServiceId"); - if (ArrayUtils.isNotEmpty(serviceIdsExclude)) { - matches = !containsIdentifier(context.getClientIds(), serviceIdsExclude); - } - } - return matches; - }).collect(Collectors.toList()); + .filter(entry -> matchesClientFilterEntry(context, entry)) + .collect(Collectors.toList()); } @Override - public List resolveFilters(HttpRequest request, List> filterEntries) { + public List resolveFilters(HttpRequest request, List filterEntries) { String requestPath = StringUtils.prependUri("/", request.getUri().getPath()); io.micronaut.http.HttpMethod method = request.getMethod(); - List filterList = new ArrayList<>(filterEntries.size()); - for (FilterEntry filterEntry : filterEntries) { - final HttpClientFilter filter = filterEntry.getFilter(); - if (filter instanceof Toggleable && !((Toggleable) filter).isEnabled()) { + List filterList = new ArrayList<>(filterEntries.size()); + for (FilterEntry filterEntry : filterEntries) { + final GenericHttpFilter filter = filterEntry.getFilter(); + if (filter instanceof GenericHttpFilter.AroundLegacy al && !al.isEnabled()) { continue; } - boolean matches = true; - if (filterEntry.hasMethods()) { - matches = anyMethodMatches(method, filterEntry.getFilterMethods()); - } - if (filterEntry.hasPatterns()) { - matches = matches && anyPatternMatches(requestPath, filterEntry.getPatterns(), filterEntry.getPatternStyle()); - } - - if (matches) { + if (matchesFilterEntry(method, requestPath, filterEntry)) { filterList.add(filter); } } return filterList; } - private boolean containsIdentifier(Collection clientIdentifiers, String[] clients) { - return Arrays.stream(clients).anyMatch(clientIdentifiers::contains); + private boolean containsIdentifier(Collection clientIdentifiers, Collection clients) { + return clients.stream().anyMatch(clientIdentifiers::contains); } private boolean anyPatternMatches(String requestPath, String[] patterns, FilterPatternStyle patternStyle) { @@ -150,4 +116,139 @@ private boolean anyMethodMatches(HttpMethod requestMethod, Collection factory, AnnotationMetadata methodAnnotations, FilterMetadata metadata) { + clientFilters.add(new ClientFilterEntry( + factory.get(), + methodAnnotations, + metadata.methods() != null ? new HashSet(metadata.methods()) : Collections.emptySet(), + metadata.patternStyle(), + metadata.patterns(), + metadata.serviceId(), + metadata.excludeServiceId() + )); + } + + private boolean matchesFilterEntry(@NonNull HttpMethod method, + @NonNull String requestPath, + @NonNull FilterEntry filterEntry) { + boolean matches = true; + if (filterEntry.hasMethods()) { + matches = anyMethodMatches(method, filterEntry.getFilterMethods()); + } + if (filterEntry.hasPatterns()) { + matches = matches && anyPatternMatches(requestPath, filterEntry.getPatterns(), filterEntry.getPatternStyle()); + } + return matches; + } + + private boolean matchesClientFilterEntry(@NonNull ClientFilterResolutionContext context, + @NonNull ClientFilterEntry entry) { + AnnotationMetadata annotationMetadata = entry.getAnnotationMetadata(); + boolean matches = !annotationMetadata.hasStereotype(FilterMatcher.class); + String filterAnnotation = annotationMetadata.getAnnotationNameByStereotype(FilterMatcher.class).orElse(null); + if (filterAnnotation != null && !matches) { + matches = context.getAnnotationMetadata().hasStereotype(filterAnnotation); + } + + if (matches && entry.serviceIds != null) { + matches = containsIdentifier(context.getClientIds(), entry.serviceIds); + } + if (matches && entry.excludeServiceIds != null) { + matches = !containsIdentifier(context.getClientIds(), entry.excludeServiceIds); + } + return matches; + } + + @NonNull + private ClientFilterEntry createClientFilterEntry(@NonNull AnnotationMetadataResolver annotationMetadataResolver, + @NonNull HttpClientFilter httpClientFilter) { + AnnotationMetadata annotationMetadata = annotationMetadataResolver.resolveMetadata(httpClientFilter); + FilterPatternStyle patternStyle = annotationMetadata.enumValue(Filter.class, + "patternStyle", FilterPatternStyle.class).orElse(FilterPatternStyle.ANT); + return new ClientFilterEntry( + new GenericHttpFilter.AroundLegacy(httpClientFilter, new FilterOrder.Dynamic(OrderUtil.getOrder(annotationMetadata))), + annotationMetadata, + methodsForFilter(annotationMetadata), + patternStyle, + List.of(annotationMetadata.stringValues(Filter.class)), + serviceIdsForFilter(annotationMetadata), + excludeServiceIdsForFilter(annotationMetadata) + ); + } + + @Nullable + private static List excludeServiceIdsForFilter(@NonNull AnnotationMetadata annotationMetadata) { + return idsForFilter(annotationMetadata, "excludeServiceId"); + } + + @Nullable + private static List serviceIdsForFilter(@NonNull AnnotationMetadata annotationMetadata) { + return idsForFilter(annotationMetadata, "serviceId"); + } + + @Nullable + private static List idsForFilter(@NonNull AnnotationMetadata annotationMetadata, @NonNull String member) { + String[] ids = annotationMetadata.stringValues(Filter.class, member); + return ArrayUtils.isNotEmpty(ids) ? List.of(ids) : null; + } + + @NonNull + private static Set methodsForFilter(@NonNull AnnotationMetadata annotationMetadata) { + HttpMethod[] methods = annotationMetadata.enumValues(Filter.class, "methods", HttpMethod.class); + final Set httpMethods = new HashSet<>(Arrays.asList(methods)); + if (annotationMetadata.hasStereotype(FilterMatcher.class)) { + httpMethods.addAll( + Arrays.asList(annotationMetadata.enumValues(FilterMatcher.class, "methods", HttpMethod.class)) + ); + } + return httpMethods; + } + + private record ClientFilterEntry( + GenericHttpFilter filter, + AnnotationMetadata annotationMetadata, + Set httpMethods, + FilterPatternStyle patternStyle, + List patterns, + @Nullable List serviceIds, + @Nullable List excludeServiceIds + ) implements FilterEntry { + + @NonNull + @Override + public AnnotationMetadata getAnnotationMetadata() { + return annotationMetadata; + } + + @Override + public GenericHttpFilter getFilter() { + return filter; + } + + @Override + public Set getFilterMethods() { + return httpMethods; + } + + @Override + public String[] getPatterns() { + return patterns.toArray(String[]::new); + } + + @Override + public FilterPatternStyle getPatternStyle() { + return patternStyle; + } + + @Override + public boolean hasMethods() { + return CollectionUtils.isNotEmpty(httpMethods); + } + + @Override + public boolean hasPatterns() { + return CollectionUtils.isNotEmpty(patterns); + } + } } diff --git a/http-client/src/main/java/io/micronaut/http/client/netty/DefaultHttpClient.java b/http-client/src/main/java/io/micronaut/http/client/netty/DefaultHttpClient.java index 3a88ebeb85f..ed41f45b41a 100644 --- a/http-client/src/main/java/io/micronaut/http/client/netty/DefaultHttpClient.java +++ b/http-client/src/main/java/io/micronaut/http/client/netty/DefaultHttpClient.java @@ -25,11 +25,12 @@ import io.micronaut.core.beans.BeanMap; import io.micronaut.core.convert.ConversionService; import io.micronaut.core.convert.ConversionServiceAware; +import io.micronaut.core.execution.ExecutionFlow; import io.micronaut.core.io.ResourceResolver; import io.micronaut.core.io.buffer.ByteBuffer; import io.micronaut.core.io.buffer.ByteBufferFactory; import io.micronaut.core.io.buffer.ReferenceCounted; -import io.micronaut.core.order.OrderUtil; +import io.micronaut.core.order.Ordered; import io.micronaut.core.type.Argument; import io.micronaut.core.util.ArgumentUtils; import io.micronaut.core.util.ArrayUtils; @@ -74,7 +75,9 @@ import io.micronaut.http.codec.MediaTypeCodec; import io.micronaut.http.codec.MediaTypeCodecRegistry; import io.micronaut.http.context.ServerRequestContext; -import io.micronaut.http.filter.ClientFilterChain; +import io.micronaut.http.filter.FilterOrder; +import io.micronaut.http.filter.FilterRunner; +import io.micronaut.http.filter.GenericHttpFilter; import io.micronaut.http.filter.HttpClientFilter; import io.micronaut.http.filter.HttpClientFilterResolver; import io.micronaut.http.filter.HttpFilterResolver; @@ -89,6 +92,7 @@ import io.micronaut.http.netty.stream.JsonSubscriber; import io.micronaut.http.netty.stream.StreamedHttpRequest; import io.micronaut.http.netty.stream.StreamedHttpResponse; +import io.micronaut.http.reactive.execution.ReactiveExecutionFlow; import io.micronaut.http.sse.Event; import io.micronaut.http.uri.UriBuilder; import io.micronaut.http.uri.UriTemplate; @@ -164,6 +168,7 @@ import reactor.core.publisher.Flux; import reactor.core.publisher.FluxSink; import reactor.core.publisher.Mono; +import reactor.util.context.Context; import java.io.Closeable; import java.io.File; @@ -186,11 +191,9 @@ import java.util.Optional; import java.util.concurrent.ThreadFactory; import java.util.concurrent.TimeoutException; -import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; import java.util.function.Consumer; import java.util.function.Function; -import java.util.function.Supplier; import static io.micronaut.scheduling.instrument.InvocationInstrumenter.NOOP; @@ -243,7 +246,7 @@ public class DefaultHttpClient implements ConnectionManager connectionManager; - private final List> clientFilterEntries; + private final List clientFilterEntries; private final LoadBalancer loadBalancer; private final HttpClientConfiguration configuration; private final String contextPath; @@ -284,7 +287,7 @@ public DefaultHttpClient(@Nullable LoadBalancer loadBalancer, null, configuration, contextPath, - new DefaultHttpClientFilterResolver(annotationMetadataResolver, Arrays.asList(filters)), + new DefaultHttpClientFilterResolver(null, annotationMetadataResolver, Arrays.asList(filters)), null, threadFactory, nettyClientSslBuilder, @@ -324,7 +327,7 @@ public DefaultHttpClient(@Nullable LoadBalancer loadBalancer, @NonNull HttpClientConfiguration configuration, @Nullable String contextPath, @NonNull HttpClientFilterResolver filterResolver, - List> clientFilterEntries, + List clientFilterEntries, @Nullable ThreadFactory threadFactory, @NonNull NettyClientSslBuilder nettyClientSslBuilder, @NonNull MediaTypeCodecRegistry codecRegistry, @@ -1243,45 +1246,46 @@ private > Publisher applyFilte AtomicReference> requestWrapper, Publisher responsePublisher) { - if (request instanceof MutableHttpRequest) { - MutableHttpRequest mutRequest = (MutableHttpRequest) request; - mutRequest.uri(requestURI); - if (informationalServiceId != null && - !mutRequest.getAttribute(HttpAttributes.SERVICE_ID).isPresent()) { + if (!(request instanceof MutableHttpRequest mutRequest)) { + return responsePublisher; + } - mutRequest.setAttribute(HttpAttributes.SERVICE_ID, informationalServiceId); - } + mutRequest.uri(requestURI); + if (informationalServiceId != null && + !mutRequest.getAttribute(HttpAttributes.SERVICE_ID).isPresent()) { - List filters = - filterResolver.resolveFilters(request, clientFilterEntries); - if (parentRequest != null) { - filters.add(new ClientServerContextFilter(parentRequest)); - } + mutRequest.setAttribute(HttpAttributes.SERVICE_ID, informationalServiceId); + } - OrderUtil.reverseSort(filters); - Publisher finalResponsePublisher = responsePublisher; - filters.add((req, chain) -> finalResponsePublisher); + List filters = + filterResolver.resolveFilters(request, clientFilterEntries); + if (parentRequest != null) { + // todo: migrate to new filter + filters.add( + new GenericHttpFilter.AroundLegacy(new ClientServerContextFilter(parentRequest), + new FilterOrder.Fixed(Ordered.HIGHEST_PRECEDENCE))); + } - ClientFilterChain filterChain = buildChain(requestWrapper, filters); - if (parentRequest != null) { - responsePublisher = ServerRequestContext.with(parentRequest, (Supplier>) () -> { - try { - return Flux.from((Publisher) filters.get(0).doFilter(request, filterChain)) - .contextWrite(ctx -> ctx.put(ServerRequestContext.KEY, parentRequest)); - } catch (Throwable t) { - return Flux.error(t); - } - }); - } else { - try { - responsePublisher = (Publisher) filters.get(0).doFilter(request, filterChain); - } catch (Throwable t) { - responsePublisher = Flux.error(t); + FilterRunner.sortReverse(filters); + filters.add(new GenericHttpFilter.TerminalReactive(responsePublisher)); + + FilterRunner runner = new FilterRunner(conversionService, filters); + Mono responseMono = Mono.deferContextual(ctx -> { + runner.reactorContext(Context.of(ctx)); + return Mono.from(ReactiveExecutionFlow.fromFlow((ExecutionFlow) runner.run(request)).toPublisher()); + }); + if (parentRequest != null) { + responseMono = responseMono.contextWrite(c -> { + // existing entry takes precedence. The parentRequest is derived from a thread + // local, and is more likely to be wrong than any reactive context we are fed. + if (c.hasKey(ServerRequestContext.KEY)) { + return c; + } else { + return c.put(ServerRequestContext.KEY, parentRequest); } - } + }); } - - return responsePublisher; + return responseMono; } /** @@ -1703,27 +1707,6 @@ private void prepareHttpHeaders( } } - private ClientFilterChain buildChain(AtomicReference> requestWrapper, List filters) { - AtomicInteger integer = new AtomicInteger(); - int len = filters.size(); - return new ClientFilterChain() { - @Override - public Publisher> proceed(MutableHttpRequest request) { - - int pos = integer.incrementAndGet(); - if (pos >= len) { - throw new IllegalStateException("The FilterChain.proceed(..) method should be invoked exactly once per filter execution. The method has instead been invoked multiple times by an erroneous filter definition."); - } - HttpClientFilter httpFilter = filters.get(pos); - try { - return httpFilter.doFilter(requestWrapper.getAndSet(request), this); - } catch (Throwable t) { - return Flux.error(t); - } - } - }; - } - private HttpPostRequestEncoder buildFormDataRequest(MutableHttpRequest clientHttpRequest, Object bodyValue) throws HttpPostRequestEncoder.ErrorDataEncoderException { HttpPostRequestEncoder postRequestEncoder = new HttpPostRequestEncoder(NettyHttpRequestBuilder.toHttpRequest(clientHttpRequest), false); diff --git a/http-client/src/test/groovy/io/micronaut/http/client/aop/ClientFilterSpec.groovy b/http-client/src/test/groovy/io/micronaut/http/client/aop/ClientFilterSpec.groovy index d6efb6145ae..36965ed0588 100644 --- a/http-client/src/test/groovy/io/micronaut/http/client/aop/ClientFilterSpec.groovy +++ b/http-client/src/test/groovy/io/micronaut/http/client/aop/ClientFilterSpec.groovy @@ -22,10 +22,12 @@ import io.micronaut.http.HttpResponse import io.micronaut.http.HttpVersion import io.micronaut.http.MediaType import io.micronaut.http.MutableHttpRequest +import io.micronaut.http.annotation.ClientFilter import io.micronaut.http.annotation.Controller import io.micronaut.http.annotation.Filter import io.micronaut.http.annotation.Get import io.micronaut.http.annotation.Header +import io.micronaut.http.annotation.RequestFilter import io.micronaut.http.client.HttpClient import io.micronaut.http.client.HttpClientRegistry import io.micronaut.http.client.annotation.Client @@ -59,6 +61,14 @@ class ClientFilterSpec extends Specification{ myApi.name() == 'Fred' } + void "test method-based client filter includes header"() { + given: + MyMethodApi myApi = context.getBean(MyMethodApi) + + expect: + myApi.name() == 'Fred' + } + void "test a client with no service ids doesn't match a filter with a service id"() { given: HttpClient client = context.createBean(HttpClient, embeddedServer.getURL()) @@ -138,6 +148,11 @@ class ClientFilterSpec extends Specification{ return username + lastname.orElse('') } + @Get(value = '/method-filters/name', produces = MediaType.TEXT_PLAIN) + String name2(@Header('X-Auth-Username') String username, @Header('X-Auth-Lastname') Optional lastname) { + return username + lastname.orElse('') + } + @Get(value = '/excluded-filters/name', produces = MediaType.TEXT_PLAIN) String nameExcluded(@Header('X-Auth-Lastname') Optional lastname) { return lastname.orElse('') @@ -161,6 +176,13 @@ class ClientFilterSpec extends Specification{ String name() } + @Requires(property = 'spec.name', value = "ClientFilterSpec") + @Client('/method-filters') + static interface MyMethodApi { + @Get(value = '/name', consumes = MediaType.TEXT_PLAIN) + String name() + } + @Requires(property = 'spec.name', value = "ClientFilterSpec") @Client('/') static interface RootApi { @@ -189,6 +211,16 @@ class ClientFilterSpec extends Specification{ } } + // this filter should match + @Requires(property = 'spec.name', value = "ClientFilterSpec") + @ClientFilter('/method-filters/**') + static class MyMethodFilter { + @RequestFilter + void doFilter(MutableHttpRequest request) { + request.header("X-Auth-Username", "Fred") + } + } + // this filter should not match the test @Requires(property = 'spec.name', value = "ClientFilterSpec") @Filter('/surnames/**') diff --git a/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/filters/FiltersSpec.groovy b/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/filters/FiltersSpec.groovy index dcd2d4757fb..67ba1f7d53e 100644 --- a/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/filters/FiltersSpec.groovy +++ b/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/filters/FiltersSpec.groovy @@ -6,12 +6,14 @@ import io.micronaut.core.annotation.Order import io.micronaut.core.async.publisher.Publishers import io.micronaut.http.HttpRequest import io.micronaut.http.HttpResponse +import io.micronaut.http.HttpStatus import io.micronaut.http.MutableHttpResponse import io.micronaut.http.annotation.Controller import io.micronaut.http.annotation.Filter import io.micronaut.http.annotation.Get import io.micronaut.http.client.HttpClient import io.micronaut.http.client.annotation.Client +import io.micronaut.http.client.exceptions.HttpClientResponseException import io.micronaut.http.context.ServerRequestContext import io.micronaut.http.filter.HttpServerFilter import io.micronaut.http.filter.ServerFilterChain @@ -23,7 +25,6 @@ import org.reactivestreams.Publisher import reactor.core.publisher.Flux import reactor.core.publisher.Mono import reactor.core.scheduler.Schedulers -import spock.lang.Ignore import spock.lang.Specification import spock.lang.Unroll @@ -112,19 +113,19 @@ class FiltersSpec extends Specification { filter3.mapExecutedOn.startsWith "io-executor" filter3.filterOrder == 3 - filter4.doFilterExecutedOn.startsWith "default-nioEventLoopGroup" + filter4.doFilterExecutedOn.startsWith "io-executor" filter4.mapExecutedOn.startsWith "io-executor" filter4.filterOrder == 4 - filter5.doFilterExecutedOn.startsWith "default-nioEventLoopGroup" + filter5.doFilterExecutedOn.startsWith "io-executor" filter5.mapExecutedOn.startsWith "io-executor" filter5.filterOrder == 5 - filter6.doFilterExecutedOn.startsWith "default-nioEventLoopGroup" + filter6.doFilterExecutedOn.startsWith "io-executor" filter6.mapExecutedOn.startsWith "io-executor" filter6.filterOrder == 6 - filter7.doFilterExecutedOn.startsWith "default-nioEventLoopGroup" + filter7.doFilterExecutedOn.startsWith "io-executor" filter7.mapExecutedOn.startsWith "io-executor" filter7.filterOrder == 7 @@ -136,7 +137,6 @@ class FiltersSpec extends Specification { method << ["get", "getReactive"] } - @Ignore("Find a fix") void "test filter with exception"() { given: EmbeddedServer server = ApplicationContext.run(EmbeddedServer, ['spec.name': FiltersSpec.simpleName, 'badFilter': true]) @@ -146,8 +146,8 @@ class FiltersSpec extends Specification { when: def response = client.get() then: - // TODO - response == "OK" + def e = thrown HttpClientResponseException + e.status == HttpStatus.INTERNAL_SERVER_ERROR cleanup: server.close() diff --git a/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/filters/ServerFilterSpec.groovy b/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/filters/ServerFilterSpec.groovy new file mode 100644 index 00000000000..c795bd6600c --- /dev/null +++ b/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/filters/ServerFilterSpec.groovy @@ -0,0 +1,173 @@ +package io.micronaut.http.server.netty.filters + +import io.micronaut.context.ApplicationContext +import io.micronaut.context.annotation.Requires +import io.micronaut.core.annotation.Order +import io.micronaut.http.HttpRequest +import io.micronaut.http.HttpResponse +import io.micronaut.http.annotation.Controller +import io.micronaut.http.annotation.Get +import io.micronaut.http.annotation.RequestFilter +import io.micronaut.http.annotation.ResponseFilter +import io.micronaut.http.annotation.ServerFilter +import io.micronaut.http.client.HttpClient +import io.micronaut.http.filter.FilterContinuation +import io.micronaut.runtime.server.EmbeddedServer +import io.micronaut.scheduling.TaskExecutors +import io.micronaut.scheduling.annotation.ExecuteOn +import jakarta.inject.Singleton +import spock.lang.Specification + +class ServerFilterSpec extends Specification { + def 'simple filter'() { + given: + def ctx = ApplicationContext.run(['spec.name': 'ServerFilterSpec']) + def server = ctx.getBean(EmbeddedServer) + server.start() + def client = ctx.createBean(HttpClient, server.URI).toBlocking() + def filter = ctx.getBean(MyFilter) + + when: + def response = client.exchange("/my-filter/index", String) + then: + response.body() == "foo" + filter.events == ['request /my-filter/index', 'response /my-filter/index OK'] + + cleanup: + server.stop() + ctx.close() + } + + @Singleton + @Requires(property = "spec.name", value = "ServerFilterSpec") + @ServerFilter("/my-filter/**") + static class MyFilter { + def events = new ArrayList() + + @RequestFilter + void request(HttpRequest request) { + events.add("request " + request.uri) + } + + @ResponseFilter + void response(HttpRequest request, HttpResponse response) { + events.add("response " + request.uri + " " + response.status) + } + } + + def 'filter order'() { + given: + def ctx = ApplicationContext.run(['spec.name': 'ServerFilterSpec']) + def server = ctx.getBean(EmbeddedServer) + server.start() + def client = ctx.createBean(HttpClient, server.URI).toBlocking() + def filter = ctx.getBean(OrderFilter) + + when: + def response = client.exchange("/order-filter/index", String) + then: + response.body() == "foo" + filter.events == ['request 1', 'request 2', 'request 3', 'response 3', 'response 2', 'response 1'] + + cleanup: + server.stop() + ctx.close() + } + + @Singleton + @Requires(property = "spec.name", value = "ServerFilterSpec") + @ServerFilter("/order-filter/**") + static class OrderFilter { + def events = new ArrayList() + + @RequestFilter + @Order(1) + void requestA(HttpRequest request) { + events.add("request 1") + } + + @RequestFilter + @Order(3) + void requestB(HttpRequest request) { + events.add("request 3") + } + + @RequestFilter + @Order(2) + void requestC(HttpRequest request) { + events.add("request 2") + } + + @ResponseFilter + @Order(1) + void responseA(HttpRequest request, HttpResponse response) { + events.add("response 1") + } + + @ResponseFilter + @Order(3) + void responseB(HttpRequest request, HttpResponse response) { + events.add("response 3") + } + + @ResponseFilter + @Order(2) + void responseC(HttpRequest request, HttpResponse response) { + events.add("response 2") + } + } + + def 'blocking filter'() { + given: + def ctx = ApplicationContext.run(['spec.name': 'ServerFilterSpec']) + def server = ctx.getBean(EmbeddedServer) + server.start() + def client = ctx.createBean(HttpClient, server.URI).toBlocking() + def filter = ctx.getBean(BlockingFilter) + + when: + def response = client.exchange("/blocking-filter/index", String) + then: + response.body() == "foo" + filter.events == ['request /blocking-filter/index', 'response /blocking-filter/index OK'] + + cleanup: + server.stop() + ctx.close() + } + + @Singleton + @Requires(property = "spec.name", value = "ServerFilterSpec") + @ServerFilter("/blocking-filter/**") + @ExecuteOn(TaskExecutors.BLOCKING) + static class BlockingFilter { + def events = new ArrayList() + + @RequestFilter + void request(HttpRequest request, FilterContinuation> continuation) { + events.add("request " + request.uri) + def response = continuation.proceed() + events.add("response " + request.uri + " " + response.status) + } + } + + @Singleton + @Requires(property = "spec.name", value = "ServerFilterSpec") + @Controller + static class Ctrl { + @Get("/my-filter/index") + String myFilterIndex() { + return "foo" + } + + @Get("/order-filter/index") + String orderFilterIndex() { + return "foo" + } + + @Get("/blocking-filter/index") + String blockingFilterIndex() { + return "foo" + } + } +} diff --git a/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/stack/InvocationStackSpec.groovy b/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/stack/InvocationStackSpec.groovy index 3d7798859c4..11cc06dbc0d 100644 --- a/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/stack/InvocationStackSpec.groovy +++ b/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/stack/InvocationStackSpec.groovy @@ -193,8 +193,8 @@ class InvocationStackSpec extends Specification { if (className.startsWith("org.codehaus.groovy") || className.startsWith("org.apache.groovy")) { return true // Spock } - if (className == "reactor.core.publisher.MonoDeferContextual" || className == "reactor.core.publisher.Mono") { - return true // added for reactive filters + if (className == "reactor.core.publisher.Mono" || className == "reactor.core.publisher.MonoFromPublisher") { + return true // added for kotlin context filters } return false } diff --git a/http-server-tck/build.gradle.kts b/http-server-tck/build.gradle.kts index 03ad4d2a6df..2f984421050 100644 --- a/http-server-tck/build.gradle.kts +++ b/http-server-tck/build.gradle.kts @@ -4,6 +4,7 @@ plugins { dependencies { annotationProcessor(projects.injectJava) annotationProcessor(projects.validation) + annotationProcessor(projects.httpValidation) implementation(projects.validation) implementation(projects.runtime) implementation(projects.jacksonDatabind) diff --git a/http-server-tck/src/main/java/io/micronaut/http/server/tck/AssertionUtils.java b/http-server-tck/src/main/java/io/micronaut/http/server/tck/AssertionUtils.java index 873e083952e..efb31b39eed 100644 --- a/http-server-tck/src/main/java/io/micronaut/http/server/tck/AssertionUtils.java +++ b/http-server-tck/src/main/java/io/micronaut/http/server/tck/AssertionUtils.java @@ -29,6 +29,7 @@ import java.util.Map; import java.util.Optional; +import java.util.function.BiConsumer; import static org.junit.jupiter.api.Assertions.*; @@ -47,6 +48,12 @@ private AssertionUtils() { } + public static BiConsumer> assertThrowsStatus(@NonNull HttpStatus status) { + return (server, request) -> AssertionUtils.assertThrows(server, request, HttpResponseAssertion.builder() + .status(status) + .build()); + } + public static void assertThrows(@NonNull ServerUnderTest server, @NonNull HttpRequest request, @NonNull HttpResponseAssertion assertion) { diff --git a/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/filter/ClientRequestFilterTest.java b/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/filter/ClientRequestFilterTest.java new file mode 100644 index 00000000000..b931c69553f --- /dev/null +++ b/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/filter/ClientRequestFilterTest.java @@ -0,0 +1,559 @@ +/* + * Copyright 2017-2023 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.http.server.tck.tests.filter; + +import io.micronaut.context.annotation.Requires; +import io.micronaut.core.annotation.Nullable; +import io.micronaut.http.HttpRequest; +import io.micronaut.http.HttpResponse; +import io.micronaut.http.HttpStatus; +import io.micronaut.http.MutableHttpRequest; +import io.micronaut.http.MutableHttpResponse; +import io.micronaut.http.annotation.ClientFilter; +import io.micronaut.http.annotation.Controller; +import io.micronaut.http.annotation.Get; +import io.micronaut.http.annotation.RequestFilter; +import io.micronaut.http.filter.FilterContinuation; +import io.micronaut.http.server.tck.AssertionUtils; +import io.micronaut.http.server.tck.HttpResponseAssertion; +import io.micronaut.http.server.tck.TestScenario; +import io.micronaut.scheduling.TaskExecutors; +import io.micronaut.scheduling.annotation.ExecuteOn; +import jakarta.inject.Singleton; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.reactivestreams.Publisher; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; + +@SuppressWarnings({ + "java:S5960", // We're allowed assertions, as these are used in tests only + "checkstyle:MissingJavadocType", + "checkstyle:DesignForExtension" +}) +public class ClientRequestFilterTest { + public static final String SPEC_NAME = "ClientRequestFilterTest"; + + @Test + public void requestFilterImmediateRequestParameter() throws IOException { + TestScenario.builder() + .specName(SPEC_NAME) + .request(HttpRequest.GET("/request-filter/immediate-request-parameter")) + .assertion((server, request) -> { + AssertionUtils.assertDoesNotThrow(server, request, HttpResponseAssertion.builder() + .status(HttpStatus.OK) + .body("foo") + .build()); + Assertions.assertEquals( + List.of("requestFilterImmediateRequestParameter /request-filter/immediate-request-parameter"), + server.getApplicationContext().getBean(MyClientFilter.class).events + ); + }) + .run(); + } + + @Test + public void requestFilterImmediateMutableRequestParameter() throws IOException { + TestScenario.builder() + .specName(SPEC_NAME) + .request(HttpRequest.GET("/request-filter/immediate-mutable-request-parameter")) + .assertion((server, request) -> AssertionUtils.assertDoesNotThrow(server, request, HttpResponseAssertion.builder() + .status(HttpStatus.OK) + .body("bar") + .build())) + .run(); + } + + @Test + @Disabled // updating the request is not supported by http client atm + public void requestFilterReplaceRequest() throws IOException { + TestScenario.builder() + .specName(SPEC_NAME) + .request(HttpRequest.GET("/request-filter/replace-request")) + .assertion((server, request) -> { + AssertionUtils.assertDoesNotThrow(server, request, HttpResponseAssertion.builder() + .status(HttpStatus.OK) + .body("/request-filter/replace-request-2") + .build()); + }) + .run(); + } + + @Test + @Disabled // updating the request is not supported by http client atm + public void requestFilterReplaceMutableRequest() throws IOException { + TestScenario.builder() + .specName(SPEC_NAME) + .request(HttpRequest.GET("/request-filter/replace-mutable-request")) + .assertion((server, request) -> { + AssertionUtils.assertDoesNotThrow(server, request, HttpResponseAssertion.builder() + .status(HttpStatus.OK) + .body("/request-filter/replace-mutable-request-2") + .build()); + }) + .run(); + } + + @Test + public void requestFilterReplaceRequestNull() throws IOException { + TestScenario.builder() + .specName(SPEC_NAME) + .request(HttpRequest.GET("/request-filter/replace-request-null")) + .assertion((server, request) -> { + AssertionUtils.assertDoesNotThrow(server, request, HttpResponseAssertion.builder() + .status(HttpStatus.OK) + .body("/request-filter/replace-request-null") + .build()); + }) + .run(); + } + + @Test + public void requestFilterReplaceRequestEmpty() throws IOException { + TestScenario.builder() + .specName(SPEC_NAME) + .request(HttpRequest.GET("/request-filter/replace-request-empty")) + .assertion((server, request) -> { + AssertionUtils.assertDoesNotThrow(server, request, HttpResponseAssertion.builder() + .status(HttpStatus.OK) + .body("/request-filter/replace-request-empty") + .build()); + }) + .run(); + } + + @Test + @Disabled // updating the request is not supported by http client atm + public void requestFilterReplaceRequestPublisher() throws IOException { + TestScenario.builder() + .specName(SPEC_NAME) + .request(HttpRequest.GET("/request-filter/replace-request-publisher")) + .assertion((server, request) -> { + AssertionUtils.assertDoesNotThrow(server, request, HttpResponseAssertion.builder() + .status(HttpStatus.OK) + .body("/request-filter/replace-request-publisher-2") + .build()); + }) + .run(); + } + + @Test + @Disabled // updating the request is not supported by http client atm + public void requestFilterReplaceRequestMono() throws IOException { + TestScenario.builder() + .specName(SPEC_NAME) + .request(HttpRequest.GET("/request-filter/replace-request-mono")) + .assertion((server, request) -> { + AssertionUtils.assertDoesNotThrow(server, request, HttpResponseAssertion.builder() + .status(HttpStatus.OK) + .body("/request-filter/replace-request-mono-2") + .build()); + }) + .run(); + } + + @Test + @Disabled // updating the request is not supported by http client atm + public void requestFilterReplaceRequestCompletable() throws IOException { + TestScenario.builder() + .specName(SPEC_NAME) + .request(HttpRequest.GET("/request-filter/replace-request-completable")) + .assertion((server, request) -> { + AssertionUtils.assertDoesNotThrow(server, request, HttpResponseAssertion.builder() + .status(HttpStatus.OK) + .body("/request-filter/replace-request-completable-2") + .build()); + }) + .run(); + } + + @Test + @Disabled // updating the request is not supported by http client atm + public void requestFilterReplaceRequestCompletion() throws IOException { + TestScenario.builder() + .specName(SPEC_NAME) + .request(HttpRequest.GET("/request-filter/replace-request-completion")) + .assertion((server, request) -> { + AssertionUtils.assertDoesNotThrow(server, request, HttpResponseAssertion.builder() + .status(HttpStatus.OK) + .body("/request-filter/replace-request-completion-2") + .build()); + }) + .run(); + } + + @Test + public void requestFilterContinuationBlocking() throws IOException { + TestScenario.builder() + .specName(SPEC_NAME) + .request(HttpRequest.GET("/request-filter/continuation-blocking")) + .assertion((server, request) -> { + AssertionUtils.assertDoesNotThrow(server, request, HttpResponseAssertion.builder() + .status(HttpStatus.OK) + .body("bar") + .build()); + Assertions.assertEquals( + List.of("requestFilterContinuationBlocking bar"), + server.getApplicationContext().getBean(MyClientFilter.class).events + ); + }) + .run(); + } + + @Test + public void requestFilterContinuationReactivePublisher() throws IOException { + TestScenario.builder() + .specName(SPEC_NAME) + .request(HttpRequest.GET("/request-filter/continuation-reactive-publisher")) + .assertion((server, request) -> { + AssertionUtils.assertDoesNotThrow(server, request, HttpResponseAssertion.builder() + .status(HttpStatus.OK) + .body("bar") + .build()); + Assertions.assertEquals( + List.of("requestFilterContinuationReactivePublisher bar"), + server.getApplicationContext().getBean(MyClientFilter.class).events + ); + }) + .run(); + } + + @Test + @Disabled // updating the request is not supported by http client atm + public void requestFilterContinuationUpdateRequest() throws IOException { + TestScenario.builder() + .specName(SPEC_NAME) + .request(HttpRequest.GET("/request-filter/continuation-update-request")) + .assertion((server, request) -> { + AssertionUtils.assertDoesNotThrow(server, request, HttpResponseAssertion.builder() + .status(HttpStatus.OK) + .body("/request-filter/continuation-update-request-2") + .build()); + }) + .run(); + } + + @Test + public void requestFilterImmediateResponse() throws IOException { + TestScenario.builder() + .specName(SPEC_NAME) + .request(HttpRequest.GET("/request-filter/immediate-response")) + .assertion((server, request) -> { + AssertionUtils.assertDoesNotThrow(server, request, HttpResponseAssertion.builder() + .status(HttpStatus.OK) + .body("requestFilterImmediateResponse") + .build()); + }) + .run(); + } + + @Test + public void requestFilterNullResponse() throws IOException { + TestScenario.builder() + .specName(SPEC_NAME) + .request(HttpRequest.GET("/request-filter/null-response")) + .assertion((server, request) -> { + AssertionUtils.assertDoesNotThrow(server, request, HttpResponseAssertion.builder() + .status(HttpStatus.OK) + .body("foo") + .build()); + Assertions.assertEquals( + List.of("requestFilterNullResponse"), + server.getApplicationContext().getBean(MyClientFilter.class).events + ); + }) + .run(); + } + + @Test + public void requestFilterEmptyOptionalResponse() throws IOException { + TestScenario.builder() + .specName(SPEC_NAME) + .request(HttpRequest.GET("/request-filter/empty-optional-response")) + .assertion((server, request) -> { + AssertionUtils.assertDoesNotThrow(server, request, HttpResponseAssertion.builder() + .status(HttpStatus.OK) + .body("foo") + .build()); + Assertions.assertEquals( + List.of("requestFilterEmptyOptionalResponse"), + server.getApplicationContext().getBean(MyClientFilter.class).events + ); + }) + .run(); + } + + @Test + public void requestFilterPublisherResponse() throws IOException { + TestScenario.builder() + .specName(SPEC_NAME) + .request(HttpRequest.GET("/request-filter/publisher-response")) + .assertion((server, request) -> { + AssertionUtils.assertDoesNotThrow(server, request, HttpResponseAssertion.builder() + .status(HttpStatus.OK) + .body("requestFilterPublisherResponse") + .build()); + }) + .run(); + } + + @Test + public void requestFilterMonoResponse() throws IOException { + TestScenario.builder() + .specName(SPEC_NAME) + .request(HttpRequest.GET("/request-filter/mono-response")) + .assertion((server, request) -> { + AssertionUtils.assertDoesNotThrow(server, request, HttpResponseAssertion.builder() + .status(HttpStatus.OK) + .body("requestFilterMonoResponse") + .build()); + }) + .run(); + } + + @Test + public void requestFilterCompletableResponse() throws IOException { + TestScenario.builder() + .specName(SPEC_NAME) + .request(HttpRequest.GET("/request-filter/completable-response")) + .assertion((server, request) -> { + AssertionUtils.assertDoesNotThrow(server, request, HttpResponseAssertion.builder() + .status(HttpStatus.OK) + .body("requestFilterCompletableResponse") + .build()); + }) + .run(); + } + + @Test + public void requestFilterCompletionResponse() throws IOException { + TestScenario.builder() + .specName(SPEC_NAME) + .request(HttpRequest.GET("/request-filter/completion-response")) + .assertion((server, request) -> { + AssertionUtils.assertDoesNotThrow(server, request, HttpResponseAssertion.builder() + .status(HttpStatus.OK) + .body("requestFilterCompletionResponse") + .build()); + }) + .run(); + } + + @ClientFilter + @Singleton + @Requires(property = "spec.name", value = SPEC_NAME) + public static class MyClientFilter { + List events = new ArrayList<>(); + + @RequestFilter("/request-filter/immediate-request-parameter") + public void requestFilterImmediateRequestParameter(HttpRequest request) { + events.add("requestFilterImmediateRequestParameter " + request.getPath()); + } + + @RequestFilter("/request-filter/immediate-mutable-request-parameter") + public void requestFilterImmediateMutableRequestParameter(MutableHttpRequest request) { + request.header("foo", "bar"); + } + + @RequestFilter("/request-filter/replace-request") + public HttpRequest requestFilterReplaceRequest() { + return HttpRequest.GET("/request-filter/replace-request-2"); + } + + @RequestFilter("/request-filter/replace-mutable-request") + public MutableHttpRequest requestFilterReplaceMutableRequest() { + return HttpRequest.GET("/request-filter/replace-mutable-request-2"); + } + + @RequestFilter("/request-filter/replace-request-null") + @Nullable + public HttpRequest requestFilterReplaceRequestNull() { + return null; + } + + @RequestFilter("/request-filter/replace-request-empty") + public Optional> requestFilterReplaceRequestEmpty() { + return Optional.empty(); + } + + @RequestFilter("/request-filter/replace-request-publisher") + public Publisher> requestFilterReplaceRequestPublisher() { + return Flux.just(HttpRequest.GET("/request-filter/replace-request-publisher-2")); + } + + @RequestFilter("/request-filter/replace-request-mono") + public Mono> requestFilterReplaceRequestMono() { + return Mono.just(HttpRequest.GET("/request-filter/replace-request-mono-2")); + } + + @RequestFilter("/request-filter/replace-request-completable") + public CompletableFuture> requestFilterReplaceRequestCompletable() { + return CompletableFuture.completedFuture(HttpRequest.GET("/request-filter/replace-request-completable-2")); + } + + @RequestFilter("/request-filter/replace-request-completion") + public CompletionStage> requestFilterReplaceRequestCompletion() { + return CompletableFuture.completedStage(HttpRequest.GET("/request-filter/replace-request-completion-2")); + } + + @RequestFilter("/request-filter/continuation-blocking") + @ExecuteOn(TaskExecutors.BLOCKING) + public void requestFilterContinuationBlocking(MutableHttpRequest request, FilterContinuation> continuation) { + request.header("foo", "bar"); + HttpResponse r = continuation.proceed(); + events.add("requestFilterContinuationBlocking " + r.body()); + } + + @RequestFilter("/request-filter/continuation-reactive-publisher") + public Publisher> requestFilterContinuationReactivePublisher(MutableHttpRequest request, FilterContinuation>> continuation) { + request.header("foo", "bar"); + return Mono.from(continuation.proceed()).doOnNext(r -> events.add("requestFilterContinuationReactivePublisher " + r.body())); + } + + @RequestFilter("/request-filter/continuation-update-request") + @ExecuteOn(TaskExecutors.BLOCKING) + public void requestFilterContinuationUpdateRequest(FilterContinuation> continuation) { + // won't affect the routing decision, but will appear in the controller + continuation.request(HttpRequest.GET("/request-filter/continuation-update-request-2")); + continuation.proceed(); + } + + @RequestFilter("/request-filter/immediate-response") + public HttpResponse requestFilterImmediateResponse() { + return HttpResponse.ok("requestFilterImmediateResponse"); + } + + @RequestFilter("/request-filter/null-response") + @Nullable + public HttpResponse requestFilterNullResponse() { + events.add("requestFilterNullResponse"); + return null; + } + + @RequestFilter("/request-filter/empty-optional-response") + public Optional> requestFilterEmptyOptionalResponse() { + events.add("requestFilterEmptyOptionalResponse"); + return Optional.empty(); + } + + @RequestFilter("/request-filter/publisher-response") + public Publisher> requestFilterPublisherResponse() { + return Mono.fromCallable(() -> HttpResponse.ok("requestFilterPublisherResponse")); + } + + @RequestFilter("/request-filter/mono-response") + public Mono> requestFilterMonoResponse() { + return Mono.fromCallable(() -> HttpResponse.ok("requestFilterMonoResponse")); + } + + @RequestFilter("/request-filter/completable-response") + public CompletableFuture> requestFilterCompletableResponse() { + return CompletableFuture.completedFuture(HttpResponse.ok("requestFilterCompletableResponse")); + } + + @RequestFilter("/request-filter/completion-response") + public CompletionStage> requestFilterCompletionResponse() { + return CompletableFuture.completedStage(HttpResponse.ok("requestFilterCompletionResponse")); + } + } + + @Controller + @Requires(property = "spec.name", value = SPEC_NAME) + public static class MyController { + @Get("/request-filter/immediate-request-parameter") + public String requestFilterImmediateRequestParameter() { + return "foo"; + } + + @Get("/request-filter/immediate-mutable-request-parameter") + public String requestFilterImmediateMutableRequestParameter(HttpRequest request) { + return request.getHeaders().get("foo"); + } + + @Get("/request-filter/replace-request-2") + public String requestFilterReplaceRequest(HttpRequest request) { + return request.getPath(); + } + + @Get("/request-filter/replace-mutable-request-2") + public String requestFilterReplaceMutableRequest(HttpRequest request) { + return request.getPath(); + } + + @Get("/request-filter/replace-request-null") + public String requestFilterReplaceRequestNull(HttpRequest request) { + return request.getPath(); + } + + @Get("/request-filter/replace-request-empty") + public String requestFilterReplaceRequestEmpty(HttpRequest request) { + return request.getPath(); + } + + @Get("/request-filter/replace-request-publisher-2") + public String requestFilterReplaceRequestPublisher(HttpRequest request) { + return request.getPath(); + } + + @Get("/request-filter/replace-request-mono-2") + public String requestFilterReplaceRequestMono(HttpRequest request) { + return request.getPath(); + } + + @Get("/request-filter/replace-request-completable-2") + public String requestFilterReplaceRequestCompletable(HttpRequest request) { + return request.getPath(); + } + + @Get("/request-filter/replace-request-completion-2") + public String requestFilterReplaceRequestCompletion(HttpRequest request) { + return request.getPath(); + } + + @Get("/request-filter/continuation-blocking") + public String requestFilterContinuationBlocking(HttpRequest request) { + return request.getHeaders().get("foo"); + } + + @Get("/request-filter/continuation-reactive-publisher") + public String requestFilterContinuationReactivePublisher(HttpRequest request) { + return request.getHeaders().get("foo"); + } + + @Get("/request-filter/continuation-update-request") + public String requestFilterContinuationUpdateRequest(HttpRequest request) { + return request.getPath(); + } + + @Get("/request-filter/null-response") + public String requestFilterNullResponse(HttpRequest request) { + return "foo"; + } + + @Get("/request-filter/empty-optional-response") + public String requestFilterEmptyOptionalResponse(HttpRequest request) { + return "foo"; + } + } +} diff --git a/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/filter/ClientResponseFilterTest.java b/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/filter/ClientResponseFilterTest.java new file mode 100644 index 00000000000..85e976bd790 --- /dev/null +++ b/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/filter/ClientResponseFilterTest.java @@ -0,0 +1,421 @@ +/* + * Copyright 2017-2023 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.http.server.tck.tests.filter; + +import io.micronaut.context.annotation.Requires; +import io.micronaut.core.annotation.Nullable; +import io.micronaut.http.HttpRequest; +import io.micronaut.http.HttpResponse; +import io.micronaut.http.HttpStatus; +import io.micronaut.http.MutableHttpRequest; +import io.micronaut.http.MutableHttpResponse; +import io.micronaut.http.annotation.ClientFilter; +import io.micronaut.http.annotation.Controller; +import io.micronaut.http.annotation.Get; +import io.micronaut.http.annotation.ResponseFilter; +import io.micronaut.http.client.exceptions.HttpClientResponseException; +import io.micronaut.http.server.tck.AssertionUtils; +import io.micronaut.http.server.tck.HttpResponseAssertion; +import io.micronaut.http.server.tck.TestScenario; +import jakarta.inject.Singleton; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.reactivestreams.Publisher; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; + +@SuppressWarnings({ + "java:S5960", // We're allowed assertions, as these are used in tests only + "checkstyle:MissingJavadocType", + "checkstyle:DesignForExtension" +}) +public class ClientResponseFilterTest { + public static final String SPEC_NAME = "ClientResponseFilterTest"; + + @Test + public void responseFilterImmediateRequestParameter() throws IOException { + TestScenario.builder() + .specName(SPEC_NAME) + .request(HttpRequest.GET("/response-filter/immediate-request-parameter")) + .assertion((server, request) -> { + AssertionUtils.assertDoesNotThrow(server, request, HttpResponseAssertion.builder() + .status(HttpStatus.OK) + .body("foo") + .build()); + Assertions.assertEquals( + List.of("responseFilterImmediateRequestParameter /response-filter/immediate-request-parameter"), + server.getApplicationContext().getBean(MyClientFilter.class).events + ); + }) + .run(); + } + + @Test + public void responseFilterImmediateMutableRequestParameter() throws IOException { + TestScenario.builder() + .specName(SPEC_NAME) + .request(HttpRequest.GET("/response-filter/immediate-mutable-request-parameter")) + .assertion((server, request) -> { + AssertionUtils.assertDoesNotThrow(server, request, HttpResponseAssertion.builder() + .status(HttpStatus.OK) + .body("foo") + .build()); + Assertions.assertEquals( + List.of("responseFilterImmediateMutableRequestParameter /response-filter/immediate-mutable-request-parameter"), + server.getApplicationContext().getBean(MyClientFilter.class).events + ); + }) + .run(); + } + + @Test + public void responseFilterResponseParameter() throws IOException { + TestScenario.builder() + .specName(SPEC_NAME) + .request(HttpRequest.GET("/response-filter/response-parameter")) + .assertion((server, request) -> { + AssertionUtils.assertDoesNotThrow(server, request, HttpResponseAssertion.builder() + .status(HttpStatus.OK) + .body("foo") + .build()); + Assertions.assertEquals( + List.of("responseFilterResponseParameter foo"), + server.getApplicationContext().getBean(MyClientFilter.class).events + ); + }) + .run(); + } + + @Test + @Disabled // mutable response parameter is not currently supported by the client + public void responseFilterMutableResponseParameter() throws IOException { + TestScenario.builder() + .specName(SPEC_NAME) + .request(HttpRequest.GET("/response-filter/mutable-response-parameter")) + .assertion((server, request) -> { + AssertionUtils.assertDoesNotThrow(server, request, HttpResponseAssertion.builder() + .status(HttpStatus.OK) + .body("responseFilterMutableResponseParameter foo") + .build()); + }) + .run(); + } + + @Test + public void responseFilterThrowableParameter() throws IOException { + TestScenario.builder() + .specName(SPEC_NAME) + .request(HttpRequest.GET("/response-filter/throwable-parameter")) + .assertion((server, request) -> { + AssertionUtils.assertThrows(server, request, HttpResponseAssertion.builder() + .status(HttpStatus.INTERNAL_SERVER_ERROR) + .build()); + Assertions.assertEquals( + // don't care about the order + Set.of( + "responseFilterThrowableParameter Internal Server Error", + "responseFilterThrowableParameter HCRE Internal Server Error", + "responseFilterThrowableParameter NAE null" + ), + new HashSet<>(server.getApplicationContext().getBean(MyClientFilter.class).events) + ); + }) + .run(); + } + + @Test + public void responseFilterReplaceResponse() throws IOException { + TestScenario.builder() + .specName(SPEC_NAME) + .request(HttpRequest.GET("/response-filter/replace-response")) + .assertion((server, request) -> { + AssertionUtils.assertDoesNotThrow(server, request, HttpResponseAssertion.builder() + .status(HttpStatus.OK) + .body("responseFilterReplaceResponse foo") + .build()); + }) + .run(); + } + + @Test + public void responseFilterReplaceMutableResponse() throws IOException { + TestScenario.builder() + .specName(SPEC_NAME) + .request(HttpRequest.GET("/response-filter/replace-mutable-response")) + .assertion((server, request) -> { + AssertionUtils.assertDoesNotThrow(server, request, HttpResponseAssertion.builder() + .status(HttpStatus.OK) + .body("responseFilterReplaceMutableResponse foo") + .build()); + }) + .run(); + } + + @Test + public void responseFilterReplaceResponseNull() throws IOException { + TestScenario.builder() + .specName(SPEC_NAME) + .request(HttpRequest.GET("/response-filter/replace-response-null")) + .assertion((server, request) -> { + AssertionUtils.assertDoesNotThrow(server, request, HttpResponseAssertion.builder() + .status(HttpStatus.OK) + .body("foo") + .build()); + }) + .run(); + } + + @Test + public void responseFilterReplaceResponseEmpty() throws IOException { + TestScenario.builder() + .specName(SPEC_NAME) + .request(HttpRequest.GET("/response-filter/replace-response-empty")) + .assertion((server, request) -> { + AssertionUtils.assertDoesNotThrow(server, request, HttpResponseAssertion.builder() + .status(HttpStatus.OK) + .body("foo") + .build()); + }) + .run(); + } + + @Test + public void responseFilterReplacePublisherResponse() throws IOException { + TestScenario.builder() + .specName(SPEC_NAME) + .request(HttpRequest.GET("/response-filter/replace-publisher-response")) + .assertion((server, request) -> { + AssertionUtils.assertDoesNotThrow(server, request, HttpResponseAssertion.builder() + .status(HttpStatus.OK) + .body("responseFilterReplacePublisherResponse foo") + .build()); + }) + .run(); + } + + @Test + public void responseFilterReplaceMonoResponse() throws IOException { + TestScenario.builder() + .specName(SPEC_NAME) + .request(HttpRequest.GET("/response-filter/replace-mono-response")) + .assertion((server, request) -> { + AssertionUtils.assertDoesNotThrow(server, request, HttpResponseAssertion.builder() + .status(HttpStatus.OK) + .body("responseFilterReplaceMonoResponse foo") + .build()); + }) + .run(); + } + + @Test + public void responseFilterReplaceCompletableResponse() throws IOException { + TestScenario.builder() + .specName(SPEC_NAME) + .request(HttpRequest.GET("/response-filter/replace-completable-response")) + .assertion((server, request) -> { + AssertionUtils.assertDoesNotThrow(server, request, HttpResponseAssertion.builder() + .status(HttpStatus.OK) + .body("responseFilterReplaceCompletableResponse foo") + .build()); + }) + .run(); + } + + @Test + public void responseFilterReplaceCompletionResponse() throws IOException { + TestScenario.builder() + .specName(SPEC_NAME) + .request(HttpRequest.GET("/response-filter/replace-completion-response")) + .assertion((server, request) -> { + AssertionUtils.assertDoesNotThrow(server, request, HttpResponseAssertion.builder() + .status(HttpStatus.OK) + .body("responseFilterReplaceCompletionResponse foo") + .build()); + }) + .run(); + } + + @ClientFilter + @Singleton + @Requires(property = "spec.name", value = SPEC_NAME) + public static class MyClientFilter { + List events = new ArrayList<>(); + + @ResponseFilter("/response-filter/immediate-request-parameter") + public void responseFilterImmediateRequestParameter(HttpRequest request) { + events.add("responseFilterImmediateRequestParameter " + request.getPath()); + } + + @ResponseFilter("/response-filter/immediate-mutable-request-parameter") + public void responseFilterImmediateMutableRequestParameter(MutableHttpRequest request) { + events.add("responseFilterImmediateMutableRequestParameter " + request.getPath()); + } + + @ResponseFilter("/response-filter/response-parameter") + public void responseFilterResponseParameter(HttpResponse response) { + events.add("responseFilterResponseParameter " + response.body()); + } + + @ResponseFilter("/response-filter/mutable-response-parameter") + public void responseFilterMutableResponseParameter(MutableHttpResponse response) { + response.body("responseFilterMutableResponseParameter " + response.body()); + } + + @ResponseFilter("/response-filter/throwable-parameter") + public void responseFilterThrowableParameter(Throwable t) { + // called + events.add("responseFilterThrowableParameter " + t.getMessage()); + } + + @ResponseFilter("/response-filter/throwable-parameter") + public void responseFilterThrowableParameter(HttpClientResponseException t) { + // called + events.add("responseFilterThrowableParameter HCRE " + t.getMessage()); + } + + @ResponseFilter("/response-filter/throwable-parameter") + public void responseFilterThrowableParameter(AssertionError t) { + // not called + events.add("responseFilterThrowableParameter AE " + t.getMessage()); + } + + @ResponseFilter("/response-filter/throwable-parameter") + public void responseFilterThrowableParameterNullable(@Nullable AssertionError t) { + // called + events.add("responseFilterThrowableParameter NAE " + t); + } + + @ResponseFilter("/response-filter/replace-response") + public HttpResponse responseFilterReplaceResponse(HttpResponse response) { + return HttpResponse.ok("responseFilterReplaceResponse " + response.body()); + } + + @ResponseFilter("/response-filter/replace-mutable-response") + public MutableHttpResponse responseFilterReplaceMutableResponse(HttpResponse response) { + return HttpResponse.ok("responseFilterReplaceMutableResponse " + response.body()); + } + + @ResponseFilter("/response-filter/replace-response-null") + @Nullable + public HttpResponse responseFilterReplaceResponseNull() { + return null; + } + + @ResponseFilter("/response-filter/replace-response-empty") + public Optional> responseFilterReplaceResponseEmpty() { + return Optional.empty(); + } + + @ResponseFilter("/response-filter/replace-publisher-response") + public Publisher> responseFilterReplacePublisherResponse(HttpResponse response) { + return Flux.just(HttpResponse.ok("responseFilterReplacePublisherResponse " + response.body())); + } + + @ResponseFilter("/response-filter/replace-mono-response") + public Mono> responseFilterReplaceMonoResponse(HttpResponse response) { + return Mono.just(HttpResponse.ok("responseFilterReplaceMonoResponse " + response.body())); + } + + @ResponseFilter("/response-filter/replace-completable-response") + public CompletableFuture> responseFilterReplaceCompletableResponse(HttpResponse response) { + return CompletableFuture.completedFuture(HttpResponse.ok("responseFilterReplaceCompletableResponse " + response.body())); + } + + @ResponseFilter("/response-filter/replace-completion-response") + public CompletionStage> responseFilterReplaceCompletionResponse(HttpResponse response) { + return CompletableFuture.completedStage(HttpResponse.ok("responseFilterReplaceCompletionResponse " + response.body())); + } + } + + @Controller + @Requires(property = "spec.name", value = SPEC_NAME) + public static class MyController { + @Get("/response-filter/immediate-request-parameter") + public String responseFilterImmediateRequestParameter() { + return "foo"; + } + + @Get("/response-filter/immediate-mutable-request-parameter") + public String responseFilterImmediateMutableRequestParameter() { + return "foo"; + } + + @Get("/response-filter/response-parameter") + public String responseFilterResponseParameter() { + return "foo"; + } + + @Get("/response-filter/mutable-response-parameter") + public String responseFilterMutableResponseParameter() { + return "foo"; + } + + @Get("/response-filter/throwable-parameter") + public String responseFilterThrowableParameter() { + throw new RuntimeException("foo"); + } + + @Get("/response-filter/replace-response") + public String responseFilterReplaceResponse() { + return "foo"; + } + + @Get("/response-filter/replace-mutable-response") + public String responseFilterReplaceMutableResponse() { + return "foo"; + } + + @Get("/response-filter/replace-response-null") + public String responseFilterReplaceResponseNull() { + return "foo"; + } + + @Get("/response-filter/replace-response-empty") + public String responseFilterReplaceResponseEmpty() { + return "foo"; + } + + @Get("/response-filter/replace-publisher-response") + public String responseFilterReplacePublisherResponse() { + return "foo"; + } + + @Get("/response-filter/replace-mono-response") + public String responseFilterReplaceMonoResponse() { + return "foo"; + } + + @Get("/response-filter/replace-completable-response") + public String responseFilterReplaceCompletableResponse() { + return "foo"; + } + + @Get("/response-filter/replace-completion-response") + public String responseFilterReplaceCompletionResponse() { + return "foo"; + } + } +} diff --git a/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/filter/HttpServerFilterExceptionHandlerTest.java b/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/filter/HttpServerFilterExceptionHandlerTest.java new file mode 100644 index 00000000000..28761cdc813 --- /dev/null +++ b/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/filter/HttpServerFilterExceptionHandlerTest.java @@ -0,0 +1,111 @@ +/* + * Copyright 2017-2023 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.http.server.tck.tests.filter; + +import io.micronaut.context.annotation.Requires; +import io.micronaut.http.HttpRequest; +import io.micronaut.http.HttpResponse; +import io.micronaut.http.HttpStatus; +import io.micronaut.http.MediaType; +import io.micronaut.http.MutableHttpResponse; +import io.micronaut.http.annotation.Controller; +import io.micronaut.http.annotation.Filter; +import io.micronaut.http.annotation.Get; +import io.micronaut.http.annotation.Produces; +import io.micronaut.http.filter.HttpServerFilter; +import io.micronaut.http.filter.ServerFilterChain; +import io.micronaut.http.server.exceptions.ExceptionHandler; +import io.micronaut.http.server.exceptions.response.ErrorContext; +import io.micronaut.http.server.exceptions.response.ErrorResponseProcessor; +import io.micronaut.http.server.tck.AssertionUtils; +import io.micronaut.http.server.tck.ServerUnderTest; +import io.micronaut.http.server.tck.TestScenario; +import jakarta.inject.Singleton; +import org.junit.jupiter.api.Test; +import org.reactivestreams.Publisher; +import reactor.core.publisher.Mono; + +import java.io.IOException; +import java.util.function.BiConsumer; + +import static io.micronaut.http.annotation.Filter.MATCH_ALL_PATTERN; + +@SuppressWarnings({ + "java:S5960", // We're allowed assertions, as these are used in tests only + "checkstyle:MissingJavadocType", + "checkstyle:DesignForExtension" +}) +public class HttpServerFilterExceptionHandlerTest { + private static final String SPEC_NAME = "FilterErrorHandlerTest"; + + @Test + public void exceptionHandlerTest() throws IOException { + assertion(HttpRequest.GET("/foo"), + AssertionUtils.assertThrowsStatus(HttpStatus.UNPROCESSABLE_ENTITY)); + } + + private static void assertion(HttpRequest request, BiConsumer> assertion) throws IOException { + TestScenario.builder() + .specName(SPEC_NAME) + .request(request) + .assertion(assertion) + .run(); + } + + + static class FooException extends RuntimeException { + + } + + @Filter(value = MATCH_ALL_PATTERN) + @Requires(property = "spec.name", value = SPEC_NAME) + static class ErrorThrowningFilter implements HttpServerFilter { + + @Override + public Publisher> doFilter(HttpRequest request, ServerFilterChain chain) { + return Mono.error(new FooException()); + } + } + + @Requires(property = "spec.name", value = SPEC_NAME) + @Controller("/foo") + static class FooController { + + @Produces(MediaType.TEXT_PLAIN) + @Get + String index() { + return "Hello World"; + } + } + + @Requires(property = "spec.name", value = SPEC_NAME) + @Singleton + static class FooExceptionHandler implements ExceptionHandler> { + + private final ErrorResponseProcessor errorResponseProcessor; + + public FooExceptionHandler(ErrorResponseProcessor errorResponseProcessor) { + this.errorResponseProcessor = errorResponseProcessor; + } + + @Override + public HttpResponse handle(HttpRequest request, FooException exception) { + return errorResponseProcessor.processResponse(ErrorContext.builder(request) + .cause(exception) + .build(), HttpResponse.unprocessableEntity()); + } + } +} diff --git a/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/filter/RequestFilterExceptionHandlerTest.java b/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/filter/RequestFilterExceptionHandlerTest.java new file mode 100644 index 00000000000..13d3825af53 --- /dev/null +++ b/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/filter/RequestFilterExceptionHandlerTest.java @@ -0,0 +1,108 @@ +/* + * Copyright 2017-2023 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.http.server.tck.tests.filter; + +import io.micronaut.context.annotation.Requires; +import io.micronaut.http.HttpRequest; +import io.micronaut.http.HttpResponse; +import io.micronaut.http.HttpStatus; +import io.micronaut.http.MediaType; +import io.micronaut.http.MutableHttpRequest; +import io.micronaut.http.annotation.Controller; +import io.micronaut.http.annotation.Get; +import io.micronaut.http.annotation.Produces; +import io.micronaut.http.annotation.RequestFilter; +import io.micronaut.http.annotation.ServerFilter; +import io.micronaut.http.server.exceptions.ExceptionHandler; +import io.micronaut.http.server.exceptions.response.ErrorContext; +import io.micronaut.http.server.exceptions.response.ErrorResponseProcessor; +import io.micronaut.http.server.tck.AssertionUtils; +import io.micronaut.http.server.tck.ServerUnderTest; +import io.micronaut.http.server.tck.TestScenario; +import jakarta.inject.Singleton; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.util.function.BiConsumer; + +import static io.micronaut.http.annotation.Filter.MATCH_ALL_PATTERN; + +@SuppressWarnings({ + "java:S5960", // We're allowed assertions, as these are used in tests only + "checkstyle:MissingJavadocType", + "checkstyle:DesignForExtension" +}) +public class RequestFilterExceptionHandlerTest { + private static final String SPEC_NAME = "RequestFilterExceptionHandlerTest"; + + @Test + public void exceptionHandlerTest() throws IOException { + assertion(HttpRequest.GET("/foo"), + AssertionUtils.assertThrowsStatus(HttpStatus.UNPROCESSABLE_ENTITY)); + } + + private static void assertion(HttpRequest request, BiConsumer> assertion) throws IOException { + TestScenario.builder() + .specName(SPEC_NAME) + .request(request) + .assertion(assertion) + .run(); + } + + + static class FooException extends RuntimeException { + + } + + @ServerFilter(value = MATCH_ALL_PATTERN) + @Requires(property = "spec.name", value = SPEC_NAME) + static class ErrorThrowingFilter { + + @RequestFilter + public void doFilter(MutableHttpRequest request) { + throw new FooException(); + } + } + + @Requires(property = "spec.name", value = SPEC_NAME) + @Controller("/foo") + static class FooController { + + @Produces(MediaType.TEXT_PLAIN) + @Get + String index() { + return "Hello World"; + } + } + + @Requires(property = "spec.name", value = SPEC_NAME) + @Singleton + static class FooExceptionHandler implements ExceptionHandler> { + + private final ErrorResponseProcessor errorResponseProcessor; + + public FooExceptionHandler(ErrorResponseProcessor errorResponseProcessor) { + this.errorResponseProcessor = errorResponseProcessor; + } + + @Override + public HttpResponse handle(HttpRequest request, FooException exception) { + return errorResponseProcessor.processResponse(ErrorContext.builder(request) + .cause(exception) + .build(), HttpResponse.unprocessableEntity()); + } + } +} diff --git a/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/filter/RequestFilterTest.java b/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/filter/RequestFilterTest.java new file mode 100644 index 00000000000..aa6c5f99463 --- /dev/null +++ b/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/filter/RequestFilterTest.java @@ -0,0 +1,551 @@ +/* + * Copyright 2017-2023 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.http.server.tck.tests.filter; + +import io.micronaut.context.annotation.Requires; +import io.micronaut.core.annotation.Nullable; +import io.micronaut.http.HttpRequest; +import io.micronaut.http.HttpResponse; +import io.micronaut.http.HttpStatus; +import io.micronaut.http.MutableHttpRequest; +import io.micronaut.http.MutableHttpResponse; +import io.micronaut.http.annotation.Controller; +import io.micronaut.http.annotation.Get; +import io.micronaut.http.annotation.RequestFilter; +import io.micronaut.http.annotation.ServerFilter; +import io.micronaut.http.filter.FilterContinuation; +import io.micronaut.http.server.tck.AssertionUtils; +import io.micronaut.http.server.tck.HttpResponseAssertion; +import io.micronaut.http.server.tck.TestScenario; +import io.micronaut.scheduling.TaskExecutors; +import io.micronaut.scheduling.annotation.ExecuteOn; +import jakarta.inject.Singleton; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.reactivestreams.Publisher; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; + +@SuppressWarnings({ + "java:S5960", // We're allowed assertions, as these are used in tests only + "checkstyle:MissingJavadocType", + "checkstyle:DesignForExtension" +}) +public class RequestFilterTest { + public static final String SPEC_NAME = "RequestFilterTest"; + + @Test + public void requestFilterImmediateRequestParameter() throws IOException { + TestScenario.builder() + .specName(SPEC_NAME) + .request(HttpRequest.GET("/request-filter/immediate-request-parameter")) + .assertion((server, request) -> { + AssertionUtils.assertDoesNotThrow(server, request, HttpResponseAssertion.builder() + .status(HttpStatus.OK) + .body("foo") + .build()); + Assertions.assertEquals( + List.of("requestFilterImmediateRequestParameter /request-filter/immediate-request-parameter"), + server.getApplicationContext().getBean(MyServerFilter.class).events + ); + }) + .run(); + } + + @Test + public void requestFilterImmediateMutableRequestParameter() throws IOException { + TestScenario.builder() + .specName(SPEC_NAME) + .request(HttpRequest.GET("/request-filter/immediate-mutable-request-parameter")) + .assertion((server, request) -> AssertionUtils.assertDoesNotThrow(server, request, HttpResponseAssertion.builder() + .status(HttpStatus.OK) + .body("bar") + .build())) + .run(); + } + + @Test + public void requestFilterReplaceRequest() throws IOException { + TestScenario.builder() + .specName(SPEC_NAME) + .request(HttpRequest.GET("/request-filter/replace-request")) + .assertion((server, request) -> { + AssertionUtils.assertDoesNotThrow(server, request, HttpResponseAssertion.builder() + .status(HttpStatus.OK) + .body("/request-filter/replace-request-2") + .build()); + }) + .run(); + } + + @Test + public void requestFilterReplaceMutableRequest() throws IOException { + TestScenario.builder() + .specName(SPEC_NAME) + .request(HttpRequest.GET("/request-filter/replace-mutable-request")) + .assertion((server, request) -> { + AssertionUtils.assertDoesNotThrow(server, request, HttpResponseAssertion.builder() + .status(HttpStatus.OK) + .body("/request-filter/replace-mutable-request-2") + .build()); + }) + .run(); + } + + @Test + public void requestFilterReplaceRequestNull() throws IOException { + TestScenario.builder() + .specName(SPEC_NAME) + .request(HttpRequest.GET("/request-filter/replace-request-null")) + .assertion((server, request) -> { + AssertionUtils.assertDoesNotThrow(server, request, HttpResponseAssertion.builder() + .status(HttpStatus.OK) + .body("/request-filter/replace-request-null") + .build()); + }) + .run(); + } + + @Test + public void requestFilterReplaceRequestEmpty() throws IOException { + TestScenario.builder() + .specName(SPEC_NAME) + .request(HttpRequest.GET("/request-filter/replace-request-empty")) + .assertion((server, request) -> { + AssertionUtils.assertDoesNotThrow(server, request, HttpResponseAssertion.builder() + .status(HttpStatus.OK) + .body("/request-filter/replace-request-empty") + .build()); + }) + .run(); + } + + @Test + public void requestFilterReplaceRequestPublisher() throws IOException { + TestScenario.builder() + .specName(SPEC_NAME) + .request(HttpRequest.GET("/request-filter/replace-request-publisher")) + .assertion((server, request) -> { + AssertionUtils.assertDoesNotThrow(server, request, HttpResponseAssertion.builder() + .status(HttpStatus.OK) + .body("/request-filter/replace-request-publisher-2") + .build()); + }) + .run(); + } + + @Test + public void requestFilterReplaceRequestMono() throws IOException { + TestScenario.builder() + .specName(SPEC_NAME) + .request(HttpRequest.GET("/request-filter/replace-request-mono")) + .assertion((server, request) -> { + AssertionUtils.assertDoesNotThrow(server, request, HttpResponseAssertion.builder() + .status(HttpStatus.OK) + .body("/request-filter/replace-request-mono-2") + .build()); + }) + .run(); + } + + @Test + public void requestFilterReplaceRequestCompletable() throws IOException { + TestScenario.builder() + .specName(SPEC_NAME) + .request(HttpRequest.GET("/request-filter/replace-request-completable")) + .assertion((server, request) -> { + AssertionUtils.assertDoesNotThrow(server, request, HttpResponseAssertion.builder() + .status(HttpStatus.OK) + .body("/request-filter/replace-request-completable-2") + .build()); + }) + .run(); + } + + @Test + public void requestFilterReplaceRequestCompletion() throws IOException { + TestScenario.builder() + .specName(SPEC_NAME) + .request(HttpRequest.GET("/request-filter/replace-request-completion")) + .assertion((server, request) -> { + AssertionUtils.assertDoesNotThrow(server, request, HttpResponseAssertion.builder() + .status(HttpStatus.OK) + .body("/request-filter/replace-request-completion-2") + .build()); + }) + .run(); + } + + @Test + public void requestFilterContinuationBlocking() throws IOException { + TestScenario.builder() + .specName(SPEC_NAME) + .request(HttpRequest.GET("/request-filter/continuation-blocking")) + .assertion((server, request) -> { + AssertionUtils.assertDoesNotThrow(server, request, HttpResponseAssertion.builder() + .status(HttpStatus.OK) + .body("bar") + .build()); + Assertions.assertEquals( + List.of("requestFilterContinuationBlocking bar"), + server.getApplicationContext().getBean(MyServerFilter.class).events + ); + }) + .run(); + } + + @Test + public void requestFilterContinuationReactivePublisher() throws IOException { + TestScenario.builder() + .specName(SPEC_NAME) + .request(HttpRequest.GET("/request-filter/continuation-reactive-publisher")) + .assertion((server, request) -> { + AssertionUtils.assertDoesNotThrow(server, request, HttpResponseAssertion.builder() + .status(HttpStatus.OK) + .body("bar") + .build()); + Assertions.assertEquals( + List.of("requestFilterContinuationReactivePublisher bar"), + server.getApplicationContext().getBean(MyServerFilter.class).events + ); + }) + .run(); + } + + @Test + public void requestFilterContinuationUpdateRequest() throws IOException { + TestScenario.builder() + .specName(SPEC_NAME) + .request(HttpRequest.GET("/request-filter/continuation-update-request")) + .assertion((server, request) -> { + AssertionUtils.assertDoesNotThrow(server, request, HttpResponseAssertion.builder() + .status(HttpStatus.OK) + .body("/request-filter/continuation-update-request-2") + .build()); + }) + .run(); + } + + @Test + public void requestFilterImmediateResponse() throws IOException { + TestScenario.builder() + .specName(SPEC_NAME) + .request(HttpRequest.GET("/request-filter/immediate-response")) + .assertion((server, request) -> { + AssertionUtils.assertDoesNotThrow(server, request, HttpResponseAssertion.builder() + .status(HttpStatus.OK) + .body("requestFilterImmediateResponse") + .build()); + }) + .run(); + } + + @Test + public void requestFilterNullResponse() throws IOException { + TestScenario.builder() + .specName(SPEC_NAME) + .request(HttpRequest.GET("/request-filter/null-response")) + .assertion((server, request) -> { + AssertionUtils.assertDoesNotThrow(server, request, HttpResponseAssertion.builder() + .status(HttpStatus.OK) + .body("foo") + .build()); + Assertions.assertEquals( + List.of("requestFilterNullResponse"), + server.getApplicationContext().getBean(MyServerFilter.class).events + ); + }) + .run(); + } + + @Test + public void requestFilterEmptyOptionalResponse() throws IOException { + TestScenario.builder() + .specName(SPEC_NAME) + .request(HttpRequest.GET("/request-filter/empty-optional-response")) + .assertion((server, request) -> { + AssertionUtils.assertDoesNotThrow(server, request, HttpResponseAssertion.builder() + .status(HttpStatus.OK) + .body("foo") + .build()); + Assertions.assertEquals( + List.of("requestFilterEmptyOptionalResponse"), + server.getApplicationContext().getBean(MyServerFilter.class).events + ); + }) + .run(); + } + + @Test + public void requestFilterPublisherResponse() throws IOException { + TestScenario.builder() + .specName(SPEC_NAME) + .request(HttpRequest.GET("/request-filter/publisher-response")) + .assertion((server, request) -> { + AssertionUtils.assertDoesNotThrow(server, request, HttpResponseAssertion.builder() + .status(HttpStatus.OK) + .body("requestFilterPublisherResponse") + .build()); + }) + .run(); + } + + @Test + public void requestFilterMonoResponse() throws IOException { + TestScenario.builder() + .specName(SPEC_NAME) + .request(HttpRequest.GET("/request-filter/mono-response")) + .assertion((server, request) -> { + AssertionUtils.assertDoesNotThrow(server, request, HttpResponseAssertion.builder() + .status(HttpStatus.OK) + .body("requestFilterMonoResponse") + .build()); + }) + .run(); + } + + @Test + public void requestFilterCompletableResponse() throws IOException { + TestScenario.builder() + .specName(SPEC_NAME) + .request(HttpRequest.GET("/request-filter/completable-response")) + .assertion((server, request) -> { + AssertionUtils.assertDoesNotThrow(server, request, HttpResponseAssertion.builder() + .status(HttpStatus.OK) + .body("requestFilterCompletableResponse") + .build()); + }) + .run(); + } + + @Test + public void requestFilterCompletionResponse() throws IOException { + TestScenario.builder() + .specName(SPEC_NAME) + .request(HttpRequest.GET("/request-filter/completion-response")) + .assertion((server, request) -> { + AssertionUtils.assertDoesNotThrow(server, request, HttpResponseAssertion.builder() + .status(HttpStatus.OK) + .body("requestFilterCompletionResponse") + .build()); + }) + .run(); + } + + @ServerFilter + @Singleton + @Requires(property = "spec.name", value = SPEC_NAME) + public static class MyServerFilter { + List events = new ArrayList<>(); + + @RequestFilter("/request-filter/immediate-request-parameter") + public void requestFilterImmediateRequestParameter(HttpRequest request) { + events.add("requestFilterImmediateRequestParameter " + request.getPath()); + } + + @RequestFilter("/request-filter/immediate-mutable-request-parameter") + public void requestFilterImmediateMutableRequestParameter(MutableHttpRequest request) { + request.setAttribute("foo", "bar"); + } + + @RequestFilter("/request-filter/replace-request") + public HttpRequest requestFilterReplaceRequest() { + return HttpRequest.GET("/request-filter/replace-request-2"); + } + + @RequestFilter("/request-filter/replace-mutable-request") + public MutableHttpRequest requestFilterReplaceMutableRequest() { + return HttpRequest.GET("/request-filter/replace-mutable-request-2"); + } + + @RequestFilter("/request-filter/replace-request-null") + @Nullable + public HttpRequest requestFilterReplaceRequestNull() { + return null; + } + + @RequestFilter("/request-filter/replace-request-empty") + public Optional> requestFilterReplaceRequestEmpty() { + return Optional.empty(); + } + + @RequestFilter("/request-filter/replace-request-publisher") + public Publisher> requestFilterReplaceRequestPublisher() { + return Flux.just(HttpRequest.GET("/request-filter/replace-request-publisher-2")); + } + + @RequestFilter("/request-filter/replace-request-mono") + public Mono> requestFilterReplaceRequestMono() { + return Mono.just(HttpRequest.GET("/request-filter/replace-request-mono-2")); + } + + @RequestFilter("/request-filter/replace-request-completable") + public CompletableFuture> requestFilterReplaceRequestCompletable() { + return CompletableFuture.completedFuture(HttpRequest.GET("/request-filter/replace-request-completable-2")); + } + + @RequestFilter("/request-filter/replace-request-completion") + public CompletionStage> requestFilterReplaceRequestCompletion() { + return CompletableFuture.completedStage(HttpRequest.GET("/request-filter/replace-request-completion-2")); + } + + @RequestFilter("/request-filter/continuation-blocking") + @ExecuteOn(TaskExecutors.BLOCKING) + public void requestFilterContinuationBlocking(HttpRequest request, FilterContinuation> continuation) { + request.setAttribute("foo", "bar"); + HttpResponse r = continuation.proceed(); + events.add("requestFilterContinuationBlocking " + r.body()); + } + + @RequestFilter("/request-filter/continuation-reactive-publisher") + public Publisher> requestFilterContinuationReactivePublisher(HttpRequest request, FilterContinuation>> continuation) { + request.setAttribute("foo", "bar"); + return Mono.from(continuation.proceed()).doOnNext(r -> events.add("requestFilterContinuationReactivePublisher " + r.body())); + } + + @RequestFilter("/request-filter/continuation-update-request") + @ExecuteOn(TaskExecutors.BLOCKING) + public void requestFilterContinuationUpdateRequest(FilterContinuation> continuation) { + // won't affect the routing decision, but will appear in the controller + continuation.request(HttpRequest.GET("/request-filter/continuation-update-request-2")); + continuation.proceed(); + } + + @RequestFilter("/request-filter/immediate-response") + public HttpResponse requestFilterImmediateResponse() { + return HttpResponse.ok("requestFilterImmediateResponse"); + } + + @RequestFilter("/request-filter/null-response") + @Nullable + public HttpResponse requestFilterNullResponse() { + events.add("requestFilterNullResponse"); + return null; + } + + @RequestFilter("/request-filter/empty-optional-response") + public Optional> requestFilterEmptyOptionalResponse() { + events.add("requestFilterEmptyOptionalResponse"); + return Optional.empty(); + } + + @RequestFilter("/request-filter/publisher-response") + public Publisher> requestFilterPublisherResponse() { + return Mono.fromCallable(() -> HttpResponse.ok("requestFilterPublisherResponse")); + } + + @RequestFilter("/request-filter/mono-response") + public Mono> requestFilterMonoResponse() { + return Mono.fromCallable(() -> HttpResponse.ok("requestFilterMonoResponse")); + } + + @RequestFilter("/request-filter/completable-response") + public CompletableFuture> requestFilterCompletableResponse() { + return CompletableFuture.completedFuture(HttpResponse.ok("requestFilterCompletableResponse")); + } + + @RequestFilter("/request-filter/completion-response") + public CompletionStage> requestFilterCompletionResponse() { + return CompletableFuture.completedStage(HttpResponse.ok("requestFilterCompletionResponse")); + } + } + + @Controller + @Requires(property = "spec.name", value = SPEC_NAME) + public static class MyController { + @Get("/request-filter/immediate-request-parameter") + public String requestFilterImmediateRequestParameter() { + return "foo"; + } + + @Get("/request-filter/immediate-mutable-request-parameter") + public String requestFilterImmediateMutableRequestParameter(HttpRequest request) { + return request.getAttribute("foo").get().toString(); + } + + @Get("/request-filter/replace-request") + public String requestFilterReplaceRequest(HttpRequest request) { + return request.getPath(); + } + + @Get("/request-filter/replace-mutable-request") + public String requestFilterReplaceMutableRequest(HttpRequest request) { + return request.getPath(); + } + + @Get("/request-filter/replace-request-null") + public String requestFilterReplaceRequestNull(HttpRequest request) { + return request.getPath(); + } + + @Get("/request-filter/replace-request-empty") + public String requestFilterReplaceRequestEmpty(HttpRequest request) { + return request.getPath(); + } + + @Get("/request-filter/replace-request-publisher") + public String requestFilterReplaceRequestPublisher(HttpRequest request) { + return request.getPath(); + } + + @Get("/request-filter/replace-request-mono") + public String requestFilterReplaceRequestMono(HttpRequest request) { + return request.getPath(); + } + + @Get("/request-filter/replace-request-completable") + public String requestFilterReplaceRequestCompletable(HttpRequest request) { + return request.getPath(); + } + + @Get("/request-filter/replace-request-completion") + public String requestFilterReplaceRequestCompletion(HttpRequest request) { + return request.getPath(); + } + + @Get("/request-filter/continuation-blocking") + public String requestFilterContinuationBlocking(HttpRequest request) { + return request.getAttribute("foo").get().toString(); + } + + @Get("/request-filter/continuation-reactive-publisher") + public String requestFilterContinuationReactivePublisher(HttpRequest request) { + return request.getAttribute("foo").get().toString(); + } + + @Get("/request-filter/continuation-update-request") + public String requestFilterContinuationUpdateRequest(HttpRequest request) { + return request.getPath(); + } + + @Get("/request-filter/null-response") + public String requestFilterNullResponse(HttpRequest request) { + return "foo"; + } + + @Get("/request-filter/empty-optional-response") + public String requestFilterEmptyOptionalResponse(HttpRequest request) { + return "foo"; + } + } +} diff --git a/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/filter/ResponseFilterExceptionHandlerTest.java b/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/filter/ResponseFilterExceptionHandlerTest.java new file mode 100644 index 00000000000..0a94392eb68 --- /dev/null +++ b/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/filter/ResponseFilterExceptionHandlerTest.java @@ -0,0 +1,106 @@ +/* + * Copyright 2017-2023 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.http.server.tck.tests.filter; + +import io.micronaut.context.annotation.Requires; +import io.micronaut.http.HttpRequest; +import io.micronaut.http.HttpResponse; +import io.micronaut.http.HttpStatus; +import io.micronaut.http.MediaType; +import io.micronaut.http.MutableHttpResponse; +import io.micronaut.http.annotation.Controller; +import io.micronaut.http.annotation.Get; +import io.micronaut.http.annotation.Produces; +import io.micronaut.http.annotation.ResponseFilter; +import io.micronaut.http.annotation.ServerFilter; +import io.micronaut.http.server.exceptions.ExceptionHandler; +import io.micronaut.http.server.exceptions.response.ErrorContext; +import io.micronaut.http.server.exceptions.response.ErrorResponseProcessor; +import io.micronaut.http.server.tck.AssertionUtils; +import io.micronaut.http.server.tck.ServerUnderTest; +import io.micronaut.http.server.tck.TestScenario; +import jakarta.inject.Singleton; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.util.function.BiConsumer; + +import static io.micronaut.http.annotation.Filter.MATCH_ALL_PATTERN; + +@SuppressWarnings({ + "java:S5960", // We're allowed assertions, as these are used in tests only + "checkstyle:MissingJavadocType", + "checkstyle:DesignForExtension" +}) +public class ResponseFilterExceptionHandlerTest { + private static final String SPEC_NAME = "ResponseFilterExceptionHandlerTest"; + + @Test + public void exceptionHandlerTest() throws IOException { + assertion(HttpRequest.GET("/foo"), + AssertionUtils.assertThrowsStatus(HttpStatus.UNPROCESSABLE_ENTITY)); + } + + private static void assertion(HttpRequest request, BiConsumer> assertion) throws IOException { + TestScenario.builder() + .specName(SPEC_NAME) + .request(request) + .assertion(assertion) + .run(); + } + + + static class FooException extends RuntimeException { + + } + + @ServerFilter(value = MATCH_ALL_PATTERN) + @Requires(property = "spec.name", value = SPEC_NAME) + static class ErrorThrowingFilter { + @ResponseFilter + public void doFilter(MutableHttpResponse response) { + throw new FooException(); + } + } + + @Requires(property = "spec.name", value = SPEC_NAME) + @Controller("/foo") + static class FooController { + @Produces(MediaType.TEXT_PLAIN) + @Get + String index() { + return "Hello World"; + } + } + + @Requires(property = "spec.name", value = SPEC_NAME) + @Singleton + static class FooExceptionHandler implements ExceptionHandler> { + + private final ErrorResponseProcessor errorResponseProcessor; + + public FooExceptionHandler(ErrorResponseProcessor errorResponseProcessor) { + this.errorResponseProcessor = errorResponseProcessor; + } + + @Override + public HttpResponse handle(HttpRequest request, FooException exception) { + return errorResponseProcessor.processResponse(ErrorContext.builder(request) + .cause(exception) + .build(), HttpResponse.unprocessableEntity()); + } + } +} diff --git a/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/filter/ResponseFilterTest.java b/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/filter/ResponseFilterTest.java new file mode 100644 index 00000000000..f0d3fefc5e9 --- /dev/null +++ b/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/filter/ResponseFilterTest.java @@ -0,0 +1,393 @@ +/* + * Copyright 2017-2023 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.http.server.tck.tests.filter; + +import io.micronaut.context.annotation.Requires; +import io.micronaut.core.annotation.Nullable; +import io.micronaut.http.HttpRequest; +import io.micronaut.http.HttpResponse; +import io.micronaut.http.HttpStatus; +import io.micronaut.http.MutableHttpRequest; +import io.micronaut.http.MutableHttpResponse; +import io.micronaut.http.annotation.Controller; +import io.micronaut.http.annotation.Get; +import io.micronaut.http.annotation.ResponseFilter; +import io.micronaut.http.annotation.ServerFilter; +import io.micronaut.http.server.tck.AssertionUtils; +import io.micronaut.http.server.tck.HttpResponseAssertion; +import io.micronaut.http.server.tck.TestScenario; +import jakarta.inject.Singleton; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.reactivestreams.Publisher; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; + +@SuppressWarnings({ + "java:S5960", // We're allowed assertions, as these are used in tests only + "checkstyle:MissingJavadocType", + "checkstyle:DesignForExtension" +}) +public class ResponseFilterTest { + public static final String SPEC_NAME = "ResponseFilterTest"; + + @Test + public void responseFilterImmediateRequestParameter() throws IOException { + TestScenario.builder() + .specName(SPEC_NAME) + .request(HttpRequest.GET("/response-filter/immediate-request-parameter")) + .assertion((server, request) -> { + AssertionUtils.assertDoesNotThrow(server, request, HttpResponseAssertion.builder() + .status(HttpStatus.OK) + .body("foo") + .build()); + Assertions.assertEquals( + List.of("responseFilterImmediateRequestParameter /response-filter/immediate-request-parameter"), + server.getApplicationContext().getBean(MyServerFilter.class).events + ); + }) + .run(); + } + + @Test + public void responseFilterImmediateMutableRequestParameter() throws IOException { + TestScenario.builder() + .specName(SPEC_NAME) + .request(HttpRequest.GET("/response-filter/immediate-mutable-request-parameter")) + .assertion((server, request) -> { + AssertionUtils.assertDoesNotThrow(server, request, HttpResponseAssertion.builder() + .status(HttpStatus.OK) + .body("foo") + .build()); + Assertions.assertEquals( + List.of("responseFilterImmediateMutableRequestParameter /response-filter/immediate-mutable-request-parameter"), + server.getApplicationContext().getBean(MyServerFilter.class).events + ); + }) + .run(); + } + + @Test + public void responseFilterResponseParameter() throws IOException { + TestScenario.builder() + .specName(SPEC_NAME) + .request(HttpRequest.GET("/response-filter/response-parameter")) + .assertion((server, request) -> { + AssertionUtils.assertDoesNotThrow(server, request, HttpResponseAssertion.builder() + .status(HttpStatus.OK) + .body("foo") + .build()); + Assertions.assertEquals( + List.of("responseFilterResponseParameter foo"), + server.getApplicationContext().getBean(MyServerFilter.class).events + ); + }) + .run(); + } + + @Test + public void responseFilterMutableResponseParameter() throws IOException { + TestScenario.builder() + .specName(SPEC_NAME) + .request(HttpRequest.GET("/response-filter/mutable-response-parameter")) + .assertion((server, request) -> { + AssertionUtils.assertDoesNotThrow(server, request, HttpResponseAssertion.builder() + .status(HttpStatus.OK) + .body("responseFilterMutableResponseParameter foo") + .build()); + }) + .run(); + } + + @Test + public void responseFilterThrowableParameterNotCalledForControllerError() throws IOException { + TestScenario.builder() + .specName(SPEC_NAME) + .request(HttpRequest.GET("/response-filter/throwable-parameter")) + .assertion((server, request) -> { + AssertionUtils.assertThrows(server, request, HttpResponseAssertion.builder() + .status(HttpStatus.INTERNAL_SERVER_ERROR) + .build()); + // filter not called, the error is mapped to a response before filters are invoked + Assertions.assertEquals( + List.of(), + server.getApplicationContext().getBean(MyServerFilter.class).events + ); + }) + .run(); + } + + @Test + public void responseFilterReplaceResponse() throws IOException { + TestScenario.builder() + .specName(SPEC_NAME) + .request(HttpRequest.GET("/response-filter/replace-response")) + .assertion((server, request) -> { + AssertionUtils.assertDoesNotThrow(server, request, HttpResponseAssertion.builder() + .status(HttpStatus.OK) + .body("responseFilterReplaceResponse foo") + .build()); + }) + .run(); + } + + @Test + public void responseFilterReplaceMutableResponse() throws IOException { + TestScenario.builder() + .specName(SPEC_NAME) + .request(HttpRequest.GET("/response-filter/replace-mutable-response")) + .assertion((server, request) -> { + AssertionUtils.assertDoesNotThrow(server, request, HttpResponseAssertion.builder() + .status(HttpStatus.OK) + .body("responseFilterReplaceMutableResponse foo") + .build()); + }) + .run(); + } + + @Test + public void responseFilterReplaceResponseNull() throws IOException { + TestScenario.builder() + .specName(SPEC_NAME) + .request(HttpRequest.GET("/response-filter/replace-response-null")) + .assertion((server, request) -> { + AssertionUtils.assertDoesNotThrow(server, request, HttpResponseAssertion.builder() + .status(HttpStatus.OK) + .body("foo") + .build()); + }) + .run(); + } + + @Test + public void responseFilterReplaceResponseEmpty() throws IOException { + TestScenario.builder() + .specName(SPEC_NAME) + .request(HttpRequest.GET("/response-filter/replace-response-empty")) + .assertion((server, request) -> { + AssertionUtils.assertDoesNotThrow(server, request, HttpResponseAssertion.builder() + .status(HttpStatus.OK) + .body("foo") + .build()); + }) + .run(); + } + + @Test + public void responseFilterReplacePublisherResponse() throws IOException { + TestScenario.builder() + .specName(SPEC_NAME) + .request(HttpRequest.GET("/response-filter/replace-publisher-response")) + .assertion((server, request) -> { + AssertionUtils.assertDoesNotThrow(server, request, HttpResponseAssertion.builder() + .status(HttpStatus.OK) + .body("responseFilterReplacePublisherResponse foo") + .build()); + }) + .run(); + } + + @Test + public void responseFilterReplaceMonoResponse() throws IOException { + TestScenario.builder() + .specName(SPEC_NAME) + .request(HttpRequest.GET("/response-filter/replace-mono-response")) + .assertion((server, request) -> { + AssertionUtils.assertDoesNotThrow(server, request, HttpResponseAssertion.builder() + .status(HttpStatus.OK) + .body("responseFilterReplaceMonoResponse foo") + .build()); + }) + .run(); + } + + @Test + public void responseFilterReplaceCompletableResponse() throws IOException { + TestScenario.builder() + .specName(SPEC_NAME) + .request(HttpRequest.GET("/response-filter/replace-completable-response")) + .assertion((server, request) -> { + AssertionUtils.assertDoesNotThrow(server, request, HttpResponseAssertion.builder() + .status(HttpStatus.OK) + .body("responseFilterReplaceCompletableResponse foo") + .build()); + }) + .run(); + } + + @Test + public void responseFilterReplaceCompletionResponse() throws IOException { + TestScenario.builder() + .specName(SPEC_NAME) + .request(HttpRequest.GET("/response-filter/replace-completion-response")) + .assertion((server, request) -> { + AssertionUtils.assertDoesNotThrow(server, request, HttpResponseAssertion.builder() + .status(HttpStatus.OK) + .body("responseFilterReplaceCompletionResponse foo") + .build()); + }) + .run(); + } + + @ServerFilter + @Singleton + @Requires(property = "spec.name", value = SPEC_NAME) + public static class MyServerFilter { + List events = new ArrayList<>(); + + @ResponseFilter("/response-filter/immediate-request-parameter") + public void responseFilterImmediateRequestParameter(HttpRequest request) { + events.add("responseFilterImmediateRequestParameter " + request.getPath()); + } + + @ResponseFilter("/response-filter/immediate-mutable-request-parameter") + public void responseFilterImmediateMutableRequestParameter(MutableHttpRequest request) { + events.add("responseFilterImmediateMutableRequestParameter " + request.getPath()); + } + + @ResponseFilter("/response-filter/response-parameter") + public void responseFilterResponseParameter(HttpResponse response) { + events.add("responseFilterResponseParameter " + response.body()); + } + + @ResponseFilter("/response-filter/mutable-response-parameter") + public void responseFilterMutableResponseParameter(MutableHttpResponse response) { + response.body("responseFilterMutableResponseParameter " + response.body()); + } + + @ResponseFilter("/response-filter/throwable-parameter") + public void responseFilterThrowableParameter(Throwable t) { + events.add("responseFilterThrowableParameter " + t.getMessage()); + } + + @ResponseFilter("/response-filter/replace-response") + public HttpResponse responseFilterReplaceResponse(HttpResponse response) { + return HttpResponse.ok("responseFilterReplaceResponse " + response.body()); + } + + @ResponseFilter("/response-filter/replace-mutable-response") + public MutableHttpResponse responseFilterReplaceMutableResponse(HttpResponse response) { + return HttpResponse.ok("responseFilterReplaceMutableResponse " + response.body()); + } + + @ResponseFilter("/response-filter/replace-response-null") + @Nullable + public HttpResponse responseFilterReplaceResponseNull() { + return null; + } + + @ResponseFilter("/response-filter/replace-response-empty") + public Optional> responseFilterReplaceResponseEmpty() { + return Optional.empty(); + } + + @ResponseFilter("/response-filter/replace-publisher-response") + public Publisher> responseFilterReplacePublisherResponse(HttpResponse response) { + return Flux.just(HttpResponse.ok("responseFilterReplacePublisherResponse " + response.body())); + } + + @ResponseFilter("/response-filter/replace-mono-response") + public Mono> responseFilterReplaceMonoResponse(HttpResponse response) { + return Mono.just(HttpResponse.ok("responseFilterReplaceMonoResponse " + response.body())); + } + + @ResponseFilter("/response-filter/replace-completable-response") + public CompletableFuture> responseFilterReplaceCompletableResponse(HttpResponse response) { + return CompletableFuture.completedFuture(HttpResponse.ok("responseFilterReplaceCompletableResponse " + response.body())); + } + + @ResponseFilter("/response-filter/replace-completion-response") + public CompletionStage> responseFilterReplaceCompletionResponse(HttpResponse response) { + return CompletableFuture.completedStage(HttpResponse.ok("responseFilterReplaceCompletionResponse " + response.body())); + } + } + + @Controller + @Requires(property = "spec.name", value = SPEC_NAME) + public static class MyController { + @Get("/response-filter/immediate-request-parameter") + public String responseFilterImmediateRequestParameter() { + return "foo"; + } + + @Get("/response-filter/immediate-mutable-request-parameter") + public String responseFilterImmediateMutableRequestParameter() { + return "foo"; + } + + @Get("/response-filter/response-parameter") + public String responseFilterResponseParameter() { + return "foo"; + } + + @Get("/response-filter/mutable-response-parameter") + public String responseFilterMutableResponseParameter() { + return "foo"; + } + + @Get("/response-filter/throwable-parameter") + public String responseFilterThrowableParameter() { + throw new RuntimeException("foo"); + } + + @Get("/response-filter/replace-response") + public String responseFilterReplaceResponse() { + return "foo"; + } + + @Get("/response-filter/replace-mutable-response") + public String responseFilterReplaceMutableResponse() { + return "foo"; + } + + @Get("/response-filter/replace-response-null") + public String responseFilterReplaceResponseNull() { + return "foo"; + } + + @Get("/response-filter/replace-response-empty") + public String responseFilterReplaceResponseEmpty() { + return "foo"; + } + + @Get("/response-filter/replace-publisher-response") + public String responseFilterReplacePublisherResponse() { + return "foo"; + } + + @Get("/response-filter/replace-mono-response") + public String responseFilterReplaceMonoResponse() { + return "foo"; + } + + @Get("/response-filter/replace-completable-response") + public String responseFilterReplaceCompletableResponse() { + return "foo"; + } + + @Get("/response-filter/replace-completion-response") + public String responseFilterReplaceCompletionResponse() { + return "foo"; + } + } +} diff --git a/http-server/src/main/java/io/micronaut/http/server/RequestLifecycle.java b/http-server/src/main/java/io/micronaut/http/server/RequestLifecycle.java index b3f1b3c329f..bc1d98cf4d2 100644 --- a/http-server/src/main/java/io/micronaut/http/server/RequestLifecycle.java +++ b/http-server/src/main/java/io/micronaut/http/server/RequestLifecycle.java @@ -28,9 +28,8 @@ import io.micronaut.http.MediaType; import io.micronaut.http.MutableHttpResponse; import io.micronaut.http.context.ServerRequestContext; -import io.micronaut.http.filter.HttpFilter; -import io.micronaut.http.filter.ServerFilterChain; -import io.micronaut.http.reactive.execution.ReactiveExecutionFlow; +import io.micronaut.http.filter.FilterRunner; +import io.micronaut.http.filter.GenericHttpFilter; import io.micronaut.http.server.exceptions.ExceptionHandler; import io.micronaut.http.server.exceptions.response.ErrorContext; import io.micronaut.http.server.types.files.FileCustomizableResponseType; @@ -41,10 +40,8 @@ import io.micronaut.web.router.RouteInfo; import io.micronaut.web.router.RouteMatch; import io.micronaut.web.router.UriRouteMatch; -import org.reactivestreams.Publisher; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import reactor.core.publisher.Mono; import reactor.util.context.Context; import java.util.ArrayList; @@ -58,7 +55,6 @@ import java.util.concurrent.CompletionException; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; -import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Supplier; /** @@ -266,45 +262,30 @@ public List getProduces() { */ protected final ExecutionFlow> runWithFilters(Supplier>> downstream) { ServerRequestContext.set(request); - List httpFilters = routeExecutor.router.findFilters(request); - if (httpFilters.isEmpty()) { + List httpFilters = routeExecutor.router.findFilters(request); + + List filters = new ArrayList<>(httpFilters.size() + 1); + filters.addAll(httpFilters); + filters.add((GenericHttpFilter.TerminalWithReactorContext) (request, context) -> { + this.request = request; + this.context = context; return downstream.get(); - } - List filters = new ArrayList<>(httpFilters); - AtomicInteger integer = new AtomicInteger(); - int len = filters.size(); + }); + FilterRunner filterRunner = new FilterRunner(routeExecutor.beanContext.getConversionService(), filters) { + @Override + protected ExecutionFlow> processResponse(HttpRequest request, HttpResponse response) { + RequestLifecycle.this.request = request; + return handleStatusException((MutableHttpResponse) response) + .onErrorResume(throwable -> onErrorNoFilter(throwable)); + } - ServerFilterChain filterChain = new ServerFilterChain() { @Override - public Publisher> proceed(io.micronaut.http.HttpRequest request) { - int pos = integer.incrementAndGet(); - if (pos > len) { - throw new IllegalStateException("The FilterChain.proceed(..) method should be invoked exactly once per filter execution. The method has instead been invoked multiple times by an erroneous filter definition."); - } - if (pos == len) { - return Mono.deferContextual(ctx -> { - context = Context.of(ctx); - return Mono.from(ReactiveExecutionFlow.fromFlow(downstream.get()).toPublisher()); - }); - } - HttpFilter httpFilter = filters.get(pos); + protected ExecutionFlow> processFailure(HttpRequest request, Throwable failure) { RequestLifecycle.this.request = request; - return ReactiveExecutionFlow.fromFlow(triggerFilter(httpFilter, this)).toPublisher(); + return onErrorNoFilter(failure); } }; - return triggerFilter(filters.get(0), filterChain); - } - - private ExecutionFlow> triggerFilter(HttpFilter httpFilter, ServerFilterChain filterChain) { - try { - Publisher> publisher = (Publisher>) httpFilter.doFilter(request, filterChain); - publisher = Mono.from(publisher).contextWrite(context); - return ReactiveExecutionFlow.fromPublisher(publisher) - .flatMap(this::handleStatusException) - .onErrorResume(this::onErrorNoFilter); - } catch (Throwable t) { - return onErrorNoFilter(t); - } + return filterRunner.run(request); } private ExecutionFlow> handleStatusException(MutableHttpResponse response) { diff --git a/http-server/src/main/java/io/micronaut/http/server/RouteExecutor.java b/http-server/src/main/java/io/micronaut/http/server/RouteExecutor.java index e0aba9c2957..18877024f7a 100644 --- a/http-server/src/main/java/io/micronaut/http/server/RouteExecutor.java +++ b/http-server/src/main/java/io/micronaut/http/server/RouteExecutor.java @@ -500,7 +500,8 @@ ExecutionFlow> createResponseForBody(HttpRequest reque } } else { HttpStatus defaultHttpStatus = routeInfo.isErrorRoute() ? HttpStatus.INTERNAL_SERVER_ERROR : HttpStatus.OK; - boolean isReactive = routeInfo.isAsyncOrReactive() || Publishers.isConvertibleToPublisher(body); + // special case HttpResponse because FullNettyClientHttpResponse implements Completable... + boolean isReactive = routeInfo.isAsyncOrReactive() || (Publishers.isConvertibleToPublisher(body) && !(body instanceof HttpResponse)); if (isReactive) { outgoingResponse = ReactiveExecutionFlow.fromPublisher( fromReactiveExecute(request, body, routeInfo, defaultHttpStatus) diff --git a/http-validation/src/main/java/io/micronaut/validation/routes/FilterVisitor.java b/http-validation/src/main/java/io/micronaut/validation/routes/FilterVisitor.java new file mode 100644 index 00000000000..9754f84b7b5 --- /dev/null +++ b/http-validation/src/main/java/io/micronaut/validation/routes/FilterVisitor.java @@ -0,0 +1,133 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.validation.routes; + +import io.micronaut.core.annotation.AnnotationValue; +import io.micronaut.core.async.publisher.Publishers; +import io.micronaut.core.type.Argument; +import io.micronaut.http.HttpRequest; +import io.micronaut.http.HttpResponse; +import io.micronaut.http.MutableHttpRequest; +import io.micronaut.http.MutableHttpResponse; +import io.micronaut.http.annotation.ClientFilter; +import io.micronaut.http.annotation.RequestFilter; +import io.micronaut.http.annotation.ResponseFilter; +import io.micronaut.http.annotation.ServerFilter; +import io.micronaut.http.filter.FilterContinuation; +import io.micronaut.http.filter.FilterRunner; +import io.micronaut.inject.ast.ClassElement; +import io.micronaut.inject.ast.MethodElement; +import io.micronaut.inject.ast.ParameterElement; +import io.micronaut.inject.visitor.TypeElementVisitor; +import io.micronaut.inject.visitor.VisitorContext; +import org.reactivestreams.Publisher; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.CompletionStage; + +/** + * Visitor that checks validity of {@link ServerFilter} and {@link ClientFilter} classes. + */ +public final class FilterVisitor implements TypeElementVisitor { + private static final Set> PERMITTED_CLASSES = Set.of( + void.class, + HttpRequest.class, + MutableHttpRequest.class, + HttpResponse.class, + MutableHttpResponse.class, + FilterContinuation.class, + Optional.class + ); + + @Override + public Set getSupportedAnnotationNames() { + return Set.of( + RequestFilter.class.getName(), + ResponseFilter.class.getName() + ); + } + + @Override + public void visitMethod(MethodElement element, VisitorContext context) { + AnnotationValue requestFilterAnnotation = element.getAnnotation(RequestFilter.class); + AnnotationValue responseFilterAnnotation = element.getAnnotation(ResponseFilter.class); + if (requestFilterAnnotation == null && responseFilterAnnotation == null) { + return; + } + + if (!element.getDeclaringType().isAnnotationPresent(ServerFilter.class) && + !element.getDeclaringType().isAnnotationPresent(ClientFilter.class)) { + + context.fail("Filter method must be declared on a @ServerFilter or @ClientFilter bean", element); + return; + } + + try { + Argument[] args = toArguments(Arrays.stream(element.getParameters()).map(ParameterElement::getType).toList(), context); + Argument ret = toArgument(element.getReturnType(), context); + + if (requestFilterAnnotation != null) { + // will throw on validation error + FilterRunner.validateFilterMethod(args, ret, false); + } + if (responseFilterAnnotation != null) { + // will throw on validation error + FilterRunner.validateFilterMethod(args, ret, true); + } + } catch (IllegalArgumentException e) { + context.fail(e.getMessage(), element); + } + } + + private Argument[] toArguments(Collection classElements, VisitorContext context) { + return classElements.stream().map(arg -> toArgument(arg, context)).toArray(Argument[]::new); + } + + private Argument toArgument(ClassElement classElement, VisitorContext context) { + Class cl = toClass(classElement, context); + Argument[] parameters; + try { + parameters = toArguments(classElement.getTypeArguments().values(), context); + } catch (IllegalArgumentException e) { + // we don't always need the type params, try the erased type + return Argument.of(cl); + } + return Argument.of(cl, parameters); + } + + private Class toClass(ClassElement classElement, VisitorContext context) { + for (Class permittedClass : PERMITTED_CLASSES) { + if (classElement.getName().equals(permittedClass.getName())) { + return permittedClass; + } + } + if (classElement.isAssignable(CompletionStage.class)) { + return CompletionStage.class; + } + if (classElement.isAssignable(Throwable.class)) { + return Throwable.class; + } + for (String reactiveTypeName : Publishers.getReactiveTypeNames()) { + if (classElement.isAssignable(reactiveTypeName)) { + return Publisher.class; + } + } + throw new IllegalArgumentException("Unsupported type for filter method: " + classElement.getName()); + } +} diff --git a/http-validation/src/main/resources/META-INF/services/io.micronaut.inject.visitor.TypeElementVisitor b/http-validation/src/main/resources/META-INF/services/io.micronaut.inject.visitor.TypeElementVisitor index 3338a866a04..bfebcfbe57f 100644 --- a/http-validation/src/main/resources/META-INF/services/io.micronaut.inject.visitor.TypeElementVisitor +++ b/http-validation/src/main/resources/META-INF/services/io.micronaut.inject.visitor.TypeElementVisitor @@ -1,2 +1,3 @@ io.micronaut.validation.websocket.WebSocketVisitor io.micronaut.validation.routes.RouteValidationVisitor +io.micronaut.validation.routes.FilterVisitor diff --git a/http-validation/src/test/groovy/io/micronaut/validation/routes/FilterVisitorSpec.groovy b/http-validation/src/test/groovy/io/micronaut/validation/routes/FilterVisitorSpec.groovy new file mode 100644 index 00000000000..fd56f8c928b --- /dev/null +++ b/http-validation/src/test/groovy/io/micronaut/validation/routes/FilterVisitorSpec.groovy @@ -0,0 +1,99 @@ +package io.micronaut.validation.routes + +import io.micronaut.annotation.processing.test.AbstractTypeElementSpec + +class FilterVisitorSpec extends AbstractTypeElementSpec { + def 'unknown parameter type'() { + when: + buildTypeElement(""" + +package test; + +import io.micronaut.http.*; +import io.micronaut.http.annotation.*; + +@ServerFilter +class Foo { + @RequestFilter + void test(String foo) { + } +} + +""") + + then: + def ex = thrown(RuntimeException) + ex.message.contains("Unsupported type for filter method: java.lang.String") + } + + def 'unknown return type'() { + when: + buildTypeElement(""" + +package test; + +import io.micronaut.http.*; +import io.micronaut.http.annotation.*; + +@ServerFilter +class Foo { + @RequestFilter + String test() { + } +} + +""") + + then: + def ex = thrown(RuntimeException) + ex.message.contains("Unsupported type for filter method: java.lang.String") + } + + def 'response on request filter'() { + when: + buildTypeElement(""" + +package test; + +import io.micronaut.http.*; +import io.micronaut.http.annotation.*; + +@ServerFilter +class Foo { + @RequestFilter + void test(HttpResponse response) { + } +} + +""") + + then: + def ex = thrown(RuntimeException) + ex.message.contains("Filter is called before the response is known, can't have a response argument") + } + + def 'publisher request on response filter'() { + when: + buildTypeElement(""" + +package test; + +import io.micronaut.http.*; +import io.micronaut.http.annotation.*; +import org.reactivestreams.Publisher; + +@ServerFilter +class Foo { + @ResponseFilter + Publisher> test() { + return null; + } +} + +""") + + then: + def ex = thrown(RuntimeException) + ex.message.contains("Unsupported filter return type io.micronaut.http.HttpRequest") + } +} diff --git a/http/src/main/java/io/micronaut/http/annotation/ClientFilter.java b/http/src/main/java/io/micronaut/http/annotation/ClientFilter.java new file mode 100644 index 00000000000..791a7c2d4e9 --- /dev/null +++ b/http/src/main/java/io/micronaut/http/annotation/ClientFilter.java @@ -0,0 +1,88 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.http.annotation; + +import io.micronaut.context.annotation.AliasFor; +import io.micronaut.context.annotation.Bean; +import io.micronaut.context.annotation.DefaultScope; +import io.micronaut.core.annotation.Experimental; +import io.micronaut.http.HttpMethod; +import io.micronaut.http.filter.FilterPatternStyle; +import jakarta.inject.Singleton; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +/** + * Mark a bean as a filter for the HTTP client. The bean may declare {@link RequestFilter}s and + * {@link ResponseFilter}s. + * + * @since 4.0.0 + * @author Jonas Konrad + */ +@Documented +@Retention(RUNTIME) +@Target(ElementType.TYPE) +@Bean +@DefaultScope(Singleton.class) +@Experimental +public @interface ClientFilter { + /** + * Pattern used to match all requests. + */ + String MATCH_ALL_PATTERN = Filter.MATCH_ALL_PATTERN; + + /** + * @return The patterns this filter should match + */ + String[] value() default {}; + + /** + * @return The style of pattern this filter uses + */ + FilterPatternStyle patternStyle() default FilterPatternStyle.ANT; + + /** + * Same as {@link #value()}. + * + * @return The patterns + */ + @AliasFor(member = "value") + String[] patterns() default {}; + + /** + * @return The methods to match. Defaults to all + */ + HttpMethod[] methods() default {}; + + /** + * The service identifiers this filter applies to. + * + * @return The service identifiers + */ + String[] serviceId() default {}; + + /** + * The service identifiers this filter does not apply to. + * + * @return The service identifiers + */ + String[] excludeServiceId() default {}; +} diff --git a/http/src/main/java/io/micronaut/http/annotation/RequestFilter.java b/http/src/main/java/io/micronaut/http/annotation/RequestFilter.java new file mode 100644 index 00000000000..e1e18fc6f7f --- /dev/null +++ b/http/src/main/java/io/micronaut/http/annotation/RequestFilter.java @@ -0,0 +1,102 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.http.annotation; + +import io.micronaut.context.annotation.AliasFor; +import io.micronaut.context.annotation.Executable; +import io.micronaut.core.annotation.EntryPoint; +import io.micronaut.core.annotation.Experimental; +import io.micronaut.http.HttpMethod; +import io.micronaut.http.HttpRequest; +import io.micronaut.http.HttpResponse; +import io.micronaut.http.MutableHttpRequest; +import io.micronaut.http.filter.FilterContinuation; +import io.micronaut.http.filter.FilterPatternStyle; +import org.reactivestreams.Publisher; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +/** + * Method annotation for a request filter. A request filter is called before the request is sent + * out. Possible parameter types are: + * + *
    + *
  • {@link HttpRequest} or {@link MutableHttpRequest}, to access the request
  • + *
  • {@link FilterContinuation}<{@link HttpResponse}>, + * {@link FilterContinuation}<{@link Publisher}<{@link HttpResponse}>>. A call to + * the continuation (and, for the reactive variant, subscribing to the {@link Publisher}) will + * trigger execution of downstream filters, and finally perform the request. The response + * returned by the continuation will be the response produced by the downstream, and can be + * modified and returned. Note that if you call a non-reactive continuation, the call will + * block, which may block the netty event loop. For that reason, always mark such a filter with + * {@link io.micronaut.scheduling.annotation.ExecuteOn}.
  • + *
+ * + * The return value may be: + * + *
    + *
  • {@code void} to immediately continue execution
  • + *
  • An updated {@link HttpRequest}
  • + *
  • A {@link HttpResponse} to skip execution of the request
  • + *
  • A {@link Publisher} (or other reactive type) that produces any of these return types, to + * delay further execution
  • + *
+ * + * @since 4.0.0 + * @author Jonas Konrad + */ +@Documented +@Retention(RUNTIME) +@Target({ElementType.METHOD}) +@Inherited +@Executable +@EntryPoint +@Experimental +public @interface RequestFilter { + /** + * Pattern used to match all requests. + */ + String MATCH_ALL_PATTERN = Filter.MATCH_ALL_PATTERN; + + /** + * @return The patterns this filter should match + */ + String[] value() default {}; + + /** + * @return The style of pattern this filter uses + */ + FilterPatternStyle patternStyle() default FilterPatternStyle.ANT; + + /** + * Same as {@link #value()}. + * + * @return The patterns + */ + @AliasFor(member = "value") + String[] patterns() default {}; + + /** + * @return The methods to match. Defaults to all + */ + HttpMethod[] methods() default {}; +} diff --git a/http/src/main/java/io/micronaut/http/annotation/ResponseFilter.java b/http/src/main/java/io/micronaut/http/annotation/ResponseFilter.java new file mode 100644 index 00000000000..c236532eecd --- /dev/null +++ b/http/src/main/java/io/micronaut/http/annotation/ResponseFilter.java @@ -0,0 +1,101 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.http.annotation; + +import io.micronaut.context.annotation.AliasFor; +import io.micronaut.context.annotation.Executable; +import io.micronaut.core.annotation.EntryPoint; +import io.micronaut.core.annotation.Experimental; +import io.micronaut.http.HttpMethod; +import io.micronaut.http.HttpRequest; +import io.micronaut.http.HttpResponse; +import io.micronaut.http.MutableHttpRequest; +import io.micronaut.http.MutableHttpResponse; +import io.micronaut.http.filter.FilterPatternStyle; +import org.reactivestreams.Publisher; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +/** + * Method annotation for a request filter. A response filter is called after a request has been + * sent and the response received. Possible parameter types are: + * + *
    + *
  • {@link HttpRequest} or {@link MutableHttpRequest}, to access the request
  • + *
  • {@link HttpResponse} or {@link MutableHttpResponse}, to access the response
  • + *
  • A {@link Throwable} type, to handle an error. If there is an error in the downstream + * filters, it is processed by the upstream response filters. Any response filter that does not + * declare a {@link Throwable} parameter of a matching type is skipped. If instead there is no + * downstream error, those response filters with a {@link Throwable} parameter are + * skipped, unless the parameter is {@link io.micronaut.core.annotation.Nullable}. Note that + * for server filter execution, exceptions are transformed into non-exceptional responses with + * an error status code, between each filter.
  • + *
+ * + * The return value may be: + * + *
    + *
  • {@code void} to immediately continue execution
  • + *
  • An updated {@link HttpResponse}
  • + *
  • A {@link Publisher} (or other reactive type) that produces any of these return types, to + * delay further execution
  • + *
+ * + * @since 4.0.0 + * @author Jonas Konrad + */ +@Documented +@Retention(RUNTIME) +@Target({ElementType.METHOD}) +@Inherited +@Executable +@EntryPoint +@Experimental +public @interface ResponseFilter { + /** + * Pattern used to match all requests. + */ + String MATCH_ALL_PATTERN = Filter.MATCH_ALL_PATTERN; + + /** + * @return The patterns this filter should match + */ + String[] value() default {}; + + /** + * @return The style of pattern this filter uses + */ + FilterPatternStyle patternStyle() default FilterPatternStyle.ANT; + + /** + * Same as {@link #value()}. + * + * @return The patterns + */ + @AliasFor(member = "value") + String[] patterns() default {}; + + /** + * @return The methods to match. Defaults to all + */ + HttpMethod[] methods() default {}; +} diff --git a/http/src/main/java/io/micronaut/http/annotation/ServerFilter.java b/http/src/main/java/io/micronaut/http/annotation/ServerFilter.java new file mode 100644 index 00000000000..7aab579e154 --- /dev/null +++ b/http/src/main/java/io/micronaut/http/annotation/ServerFilter.java @@ -0,0 +1,74 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.http.annotation; + +import io.micronaut.context.annotation.AliasFor; +import io.micronaut.context.annotation.Bean; +import io.micronaut.context.annotation.DefaultScope; +import io.micronaut.core.annotation.Experimental; +import io.micronaut.http.HttpMethod; +import io.micronaut.http.filter.FilterPatternStyle; +import jakarta.inject.Singleton; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +/** + * Mark a bean as a filter for the HTTP server. The bean may declare {@link RequestFilter}s and + * {@link ResponseFilter}s. + * + * @since 4.0.0 + * @author Jonas Konrad + */ +@Documented +@Retention(RUNTIME) +@Target(ElementType.TYPE) +@Bean +@DefaultScope(Singleton.class) +@Experimental +public @interface ServerFilter { + /** + * Pattern used to match all requests. + */ + String MATCH_ALL_PATTERN = Filter.MATCH_ALL_PATTERN; + + /** + * @return The patterns this filter should match + */ + String[] value() default {}; + + /** + * @return The style of pattern this filter uses + */ + FilterPatternStyle patternStyle() default FilterPatternStyle.ANT; + + /** + * Same as {@link #value()}. + * + * @return The patterns + */ + @AliasFor(member = "value") + String[] patterns() default {}; + + /** + * @return The methods to match. Defaults to all + */ + HttpMethod[] methods() default {}; +} diff --git a/http/src/main/java/io/micronaut/http/filter/BaseFilterProcessor.java b/http/src/main/java/io/micronaut/http/filter/BaseFilterProcessor.java new file mode 100644 index 00000000000..192011c5389 --- /dev/null +++ b/http/src/main/java/io/micronaut/http/filter/BaseFilterProcessor.java @@ -0,0 +1,192 @@ +/* + * Copyright 2017-2023 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.http.filter; + +import io.micronaut.context.BeanContext; +import io.micronaut.context.processor.ExecutableMethodProcessor; +import io.micronaut.core.annotation.AnnotationMetadata; +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.NonNull; +import io.micronaut.core.annotation.Nullable; +import io.micronaut.core.annotation.Order; +import io.micronaut.core.order.Ordered; +import io.micronaut.core.util.AntPathMatcher; +import io.micronaut.core.util.ArrayUtils; +import io.micronaut.http.HttpMethod; +import io.micronaut.http.annotation.RequestFilter; +import io.micronaut.http.annotation.ResponseFilter; +import io.micronaut.inject.BeanDefinition; +import io.micronaut.inject.ExecutableMethod; +import io.micronaut.inject.qualifiers.Qualifiers; +import io.micronaut.scheduling.annotation.ExecuteOn; + +import java.lang.annotation.Annotation; +import java.util.Arrays; +import java.util.List; +import java.util.OptionalInt; +import java.util.concurrent.Executor; +import java.util.function.Supplier; + +/** + * Base class for processing {@link io.micronaut.http.annotation.ServerFilter} and + * {@link io.micronaut.http.annotation.ClientFilter} beans. + * + * @param Filter annotation type + * @author Jonas Konrad + * @since 4.0.0 + */ +@Internal +public abstract class BaseFilterProcessor implements ExecutableMethodProcessor { + private final BeanContext beanContext; + private final Class filterAnnotation; + + public BaseFilterProcessor(BeanContext beanContext, Class filterAnnotation) { + this.beanContext = beanContext; + this.filterAnnotation = filterAnnotation; + } + + @Override + public void process(BeanDefinition beanDefinition, ExecutableMethod method) { + //noinspection unchecked,rawtypes + process0(beanDefinition, (ExecutableMethod) method); + } + + /** + * Add a filter. Called during {@link #process(BeanDefinition, ExecutableMethod)}. + * + * @param factory Factory that will create the filter instance + * @param methodAnnotations Annotations on the filter method + * @param metadata Filter metadata from class and method annotations + */ + protected abstract void addFilter(Supplier factory, AnnotationMetadata methodAnnotations, FilterMetadata metadata); + + private void process0(BeanDefinition beanDefinition, ExecutableMethod method) { + FilterMetadata beanLevel = metadata(beanDefinition, filterAnnotation); + if (method.isAnnotationPresent(RequestFilter.class)) { + FilterMetadata methodLevel = metadata(method, RequestFilter.class); + FilterMetadata combined = combineMetadata(beanLevel, methodLevel); + addFilter(() -> withAsync(combined, FilterRunner.prepareFilterMethod(beanContext.getConversionService(), beanContext.getBean(beanDefinition), method, false, combined.order)), method, combined); + } + if (method.isAnnotationPresent(ResponseFilter.class)) { + FilterMetadata methodLevel = metadata(method, ResponseFilter.class); + FilterMetadata combined = combineMetadata(beanLevel, methodLevel); + addFilter(() -> withAsync(combined, FilterRunner.prepareFilterMethod(beanContext.getConversionService(), beanContext.getBean(beanDefinition), method, true, combined.order)), method, combined); + } + } + + private GenericHttpFilter withAsync(FilterMetadata metadata, GenericHttpFilter filter) { + if (metadata.executeOn != null) { + return new GenericHttpFilter.Async(filter, beanContext.getBean(Executor.class, Qualifiers.byName(metadata.executeOn))); + } else { + return filter; + } + } + + private FilterMetadata combineMetadata(FilterMetadata beanLevel, FilterMetadata methodLevel) { + List patterns; + if (beanLevel.patterns == null) { + patterns = methodLevel.patterns; + } else if (methodLevel.patterns == null) { + patterns = beanLevel.patterns; + } else { + if (beanLevel.patternStyle == FilterPatternStyle.REGEX || + methodLevel.patternStyle == FilterPatternStyle.REGEX) { + throw new UnsupportedOperationException("Concatenating regex filter patterns is " + + "not supported. Please declare the full pattern on the method instead."); + } + patterns = beanLevel.patterns.stream() + .flatMap(p1 -> methodLevel.patterns.stream().map(p2 -> concatAntPatterns(p1, p2))) + .toList(); + } + + if (patterns != null) { + patterns = prependContextPath(patterns); + } + + FilterOrder order; + if (methodLevel.order != null) { + order = methodLevel.order; + } else if (beanLevel.order != null) { + // allow overriding using Ordered.getOrder, where possible + order = new FilterOrder.Dynamic(((FilterOrder.Fixed) beanLevel.order).value()); + } else { + order = new FilterOrder.Dynamic(Ordered.LOWEST_PRECEDENCE); + } + + return new FilterMetadata( + methodLevel.patterns == null ? beanLevel.patternStyle : methodLevel.patternStyle, + patterns, + methodLevel.methods == null ? beanLevel.methods : methodLevel.methods, + order, + methodLevel.executeOn == null ? beanLevel.executeOn : methodLevel.executeOn, + beanLevel.serviceId, // only present on bean level + beanLevel.excludeServiceId // only present on bean level + ); + } + + /** + * Prepend server context path if necessary. + * + * @param patterns Input patterns + * @return Output patterns with server context path prepended + */ + @NonNull + protected List prependContextPath(@NonNull List patterns) { + return patterns; + } + + static String concatAntPatterns(String p1, String p2) { + StringBuilder combined = new StringBuilder(p1.length() + p2.length() + 1); + combined.append(p1); + if (!p1.endsWith(AntPathMatcher.DEFAULT_PATH_SEPARATOR)) { + combined.append(AntPathMatcher.DEFAULT_PATH_SEPARATOR); + } + if (p2.startsWith(AntPathMatcher.DEFAULT_PATH_SEPARATOR)) { + combined.append(p2, AntPathMatcher.DEFAULT_PATH_SEPARATOR.length(), p2.length()); + } else { + combined.append(p2); + } + return combined.toString(); + } + + private FilterMetadata metadata(AnnotationMetadata annotationMetadata, Class annotationType) { + HttpMethod[] methods = annotationMetadata.enumValues(annotationType, "methods", HttpMethod.class); + String[] patterns = annotationMetadata.stringValues(annotationType); + OptionalInt order = annotationMetadata.intValue(Order.class); + String[] serviceId = annotationMetadata.stringValues(annotationType, "serviceId"); // only on ClientFilter + String[] excludeServiceId = annotationMetadata.stringValues(annotationType, "excludeServiceId"); // only on ClientFilter + return new FilterMetadata( + annotationMetadata.enumValue(annotationType, "patternStyle", FilterPatternStyle.class).orElse(FilterPatternStyle.ANT), + ArrayUtils.isNotEmpty(patterns) ? Arrays.asList(patterns) : null, + ArrayUtils.isNotEmpty(methods) ? Arrays.asList(methods) : null, + order.isPresent() ? new FilterOrder.Fixed(order.getAsInt()) : null, + annotationMetadata.stringValue(ExecuteOn.class).orElse(null), + ArrayUtils.isNotEmpty(serviceId) ? Arrays.asList(serviceId) : null, + ArrayUtils.isNotEmpty(excludeServiceId) ? Arrays.asList(excludeServiceId) : null + ); + } + + protected record FilterMetadata( + FilterPatternStyle patternStyle, + @Nullable List patterns, + @Nullable List methods, + @Nullable FilterOrder order, + @Nullable String executeOn, + @Nullable List serviceId, + @Nullable List excludeServiceId + ) { + } +} diff --git a/http/src/main/java/io/micronaut/http/filter/DefaultFilterEntry.java b/http/src/main/java/io/micronaut/http/filter/DefaultFilterEntry.java index bfa448c0198..31c6648e329 100644 --- a/http/src/main/java/io/micronaut/http/filter/DefaultFilterEntry.java +++ b/http/src/main/java/io/micronaut/http/filter/DefaultFilterEntry.java @@ -15,8 +15,8 @@ */ package io.micronaut.http.filter; -import io.micronaut.core.annotation.NonNull; import io.micronaut.core.annotation.AnnotationMetadata; +import io.micronaut.core.annotation.NonNull; import io.micronaut.core.util.ArrayUtils; import io.micronaut.core.util.CollectionUtils; import io.micronaut.core.util.StringUtils; @@ -30,10 +30,9 @@ * * @author graemerocher * @since 2.0 - * @param The filter type */ -final class DefaultFilterEntry implements HttpFilterResolver.FilterEntry { - private final T httpFilter; +final class DefaultFilterEntry implements HttpFilterResolver.FilterEntry { + private final GenericHttpFilter httpFilter; private final AnnotationMetadata annotationMetadata; private final Set filterMethods; private final String[] patterns; @@ -50,7 +49,7 @@ final class DefaultFilterEntry implements HttpFilterResolv * @param patterns THe patterns */ DefaultFilterEntry( - T filter, + GenericHttpFilter filter, AnnotationMetadata annotationMetadata, Set httpMethods, FilterPatternStyle patternStyle, @@ -71,7 +70,7 @@ public AnnotationMetadata getAnnotationMetadata() { } @Override - public T getFilter() { + public GenericHttpFilter getFilter() { return httpFilter; } diff --git a/http/src/main/java/io/micronaut/http/filter/FilterContinuation.java b/http/src/main/java/io/micronaut/http/filter/FilterContinuation.java new file mode 100644 index 00000000000..9798bdb9d47 --- /dev/null +++ b/http/src/main/java/io/micronaut/http/filter/FilterContinuation.java @@ -0,0 +1,64 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.http.filter; + +import io.micronaut.core.annotation.Experimental; +import io.micronaut.core.annotation.NonNull; +import io.micronaut.http.HttpRequest; + +/** + * A filter continuation gives can be declared as a parameter on a + * {@link io.micronaut.http.annotation.RequestFilter filter method}. The filter method gets + * "delayed" access to the parameter it is requesting. For example, a request filter can declare a + * continuation that returns the response. When the filter calls the continuation, other downstream + * filters and the final request call will be executed. The continuation will return once the + * response has been received and processed by downstream filters.
+ * A continuation can either return the value immediately (e.g. + * {@code FilterContinuation>}), in which case the call to {@link #proceed()} will + * block, or it can return a reactive wrapper (e.g. + * {@code FilterContinuation>>}). With a reactive wrapper, + * {@link #proceed()} will not block, and downstream processing will happen asynchronously (after + * the reactive stream is subscribed to). + * + * @param The type to return in {@link #proceed()} + */ +@Experimental +public interface FilterContinuation { + /** + * Update the request for downstream processing. + * + * @param request The new request + * @return This continuation, for call chaining + */ + @NonNull + FilterContinuation request(@NonNull HttpRequest request); + + /** + * Proceed processing downstream of this filter. If {@link R} is not a reactive type, this + * method will block until downstream processing completes, and may throw an exception if there + * is a failure. Blocking netty event loop threads can lead to bugs, so any filter that + * may block in the netty HTTP server should use + * {@link io.micronaut.scheduling.annotation.ExecuteOn} to avoid running on the event loop. + *
+ * If {@link R} is a reactive type, this method will return immediately. Downstream processing + * will happen when the reactive stream is subscribed to, and the reactive stream will produce + * the downstream result when available. + * + * @return The downstream result, or reactive stream wrapper thereof + */ + @NonNull + R proceed(); +} diff --git a/http/src/main/java/io/micronaut/http/filter/FilterOrder.java b/http/src/main/java/io/micronaut/http/filter/FilterOrder.java new file mode 100644 index 00000000000..be74ddf048a --- /dev/null +++ b/http/src/main/java/io/micronaut/http/filter/FilterOrder.java @@ -0,0 +1,66 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.http.filter; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.order.Ordered; + +/** + * Different filter order heuristics, derived from annotations or {@link Ordered#getOrder() a + * bean method}. + * + * @since 4.0.0 + * @author Jonas Konrad + */ +@Internal +public sealed interface FilterOrder { + /** + * Compute the order value for the given bean. + * + * @param bean The bean to compute the order value for, potentially implementing + * {@link Ordered} + * @return The order value + */ + int getOrder(Object bean); + + /** + * Fixed order value. + * + * @param value The order value + */ + record Fixed(int value) implements FilterOrder { + @Override + public int getOrder(Object bean) { + return value; + } + } + + /** + * Dynamic order value (from {@link Ordered#getOrder()}). + * + * @param fallbackValue The order value to use if the bean does not implement {@link Ordered} + */ + record Dynamic(int fallbackValue) implements FilterOrder { + @Override + public int getOrder(Object bean) { + if (bean instanceof Ordered o) { + return o.getOrder(); + } else { + return fallbackValue; + } + } + } +} diff --git a/http/src/main/java/io/micronaut/http/filter/FilterRunner.java b/http/src/main/java/io/micronaut/http/filter/FilterRunner.java new file mode 100644 index 00000000000..e82881167c7 --- /dev/null +++ b/http/src/main/java/io/micronaut/http/filter/FilterRunner.java @@ -0,0 +1,1188 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.http.filter; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.NonNull; +import io.micronaut.core.annotation.Nullable; +import io.micronaut.core.async.publisher.Publishers; +import io.micronaut.core.convert.ConversionService; +import io.micronaut.core.execution.CompletableFutureExecutionFlow; +import io.micronaut.core.execution.ExecutionFlow; +import io.micronaut.core.execution.ImperativeExecutionFlow; +import io.micronaut.core.order.OrderUtil; +import io.micronaut.core.order.Ordered; +import io.micronaut.core.type.Argument; +import io.micronaut.core.type.Executable; +import io.micronaut.http.HttpRequest; +import io.micronaut.http.HttpResponse; +import io.micronaut.http.MutableHttpRequest; +import io.micronaut.http.MutableHttpResponse; +import io.micronaut.http.reactive.execution.ReactiveExecutionFlow; +import io.micronaut.inject.ExecutableMethod; +import org.reactivestreams.Publisher; +import org.reactivestreams.Subscriber; +import org.reactivestreams.Subscription; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import reactor.core.CorePublisher; +import reactor.core.CoreSubscriber; +import reactor.core.publisher.Mono; +import reactor.util.context.Context; + +import java.util.HashMap; +import java.util.List; +import java.util.ListIterator; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Executor; +import java.util.function.BiConsumer; +import java.util.function.Function; +import java.util.function.Predicate; + +/** + * The filter runner will start processing the filters in the forward order. + * All the request filters are executed till one of them returns a response (bypasses the route execution for controllers or the client invocation), + * or the terminal filter will produce the response from the route/client call. + * After that, the filters are processed in the opposite order so response filters can be processed, + * which can sometimes override the existing response. + * There is a special case of response filters that needs to process the response; for those cases, + * the filter needs to be suspended, and the next filter in the order needs to be executed. + * When the response is committed, the filter will be resumed when it's processed again. + * There is a special case for the client filters; those will process the exception, + * which needs to be tracked during the response filtering phase. + * + * @author Jonas Konrad + * @author Denis Stepanov + * @since 4.0.0 + */ +@Internal +public class FilterRunner { + private static final Logger LOG = LoggerFactory.getLogger(FilterRunner.class); + private static final Predicate FILTER_CONDITION_ALWAYS_TRUE = runner -> true; + + private final ConversionService conversionService; + /** + * All filters to run. Request filters are executed in order from first to last, response + * filters in the reverse order. + */ + private final List filters; + + private Context initialReactorContext = Context.empty(); + + /** + * Create a new filter runner, to be used only once. + * + * @param conversionService The conversion service + * @param filters The filters to run + */ + public FilterRunner(ConversionService conversionService, List filters) { + this.conversionService = conversionService; + this.filters = Objects.requireNonNull(filters, "filters"); + } + + private static void checkOrdered(List filters) { + if (!filters.stream().allMatch(f -> f instanceof Ordered)) { + throw new IllegalStateException("Some filters cannot be ordered: " + filters); + } + } + + /** + * Sort filters according to their declared order (e.g. annotation, + * {@link Ordered#getOrder()}...). List must not contain terminal filters. + * + * @param filters The list of filters to sort in place + */ + public static void sort(@NonNull List filters) { + checkOrdered(filters); + OrderUtil.sort(filters); + } + + /** + * Sort filters according to their declared order (e.g. annotation, + * {@link Ordered#getOrder()}...). List must not contain terminal filters. Reverse order. + * + * @param filters The list of filters to sort in place + */ + public static void sortReverse(@NonNull List filters) { + checkOrdered(filters); + OrderUtil.reverseSort(filters); + } + + /** + * Transform a response, e.g. by replacing an error response with an exception. Called before + * every filter. + * + * @param request The current request + * @param response The current response + * @return A flow that will be passed on to the next filter + */ + protected ExecutionFlow> processResponse(HttpRequest request, HttpResponse response) { + return ExecutionFlow.just(response); + } + + /** + * Transform a failure, e.g. by replacing an exception with an error response. Called before + * every filter. + * + * @param request The current request + * @param failure The failure + * @return A flow that will be passed on to the next filter + */ + protected ExecutionFlow> processFailure(HttpRequest request, Throwable failure) { + return ExecutionFlow.error(failure); + } + + /** + * Set the initial reactor context. This is passed on to every filter that requests a reactive + * type, and, if applicable, to the + * {@link io.micronaut.http.filter.GenericHttpFilter.TerminalWithReactorContext terminal}. + * + * @param reactorContext The reactor context, may be updated by filters + * @return This filter runner, for chaining + */ + public final FilterRunner reactorContext(Context reactorContext) { + this.initialReactorContext = reactorContext; + return this; + } + + /** + * Execute the filters for the given request. May only be called once + * + * @param request The request + * @return The flow that completes after all filters and the terminal operation, with the final + * response + */ + public final ExecutionFlow> run(HttpRequest request) { + return (ExecutionFlow) filterRequest(new FilterContext(request, initialReactorContext), filters.listIterator(), new HashMap<>()); + } + + private ExecutionFlow> filterRequest(FilterContext context, + ListIterator iterator, + Map, FilterContinuationImpl>> suspended) { + return filterRequest0(context, iterator, suspended) + .flatMap(newContext -> { + if (newContext.response != null) { + return filterResponse(newContext, iterator, null, suspended); + } + return ExecutionFlow.error(new IllegalStateException("Request filters didn't produce any response!")); + }); + } + + private ExecutionFlow filterRequest0(FilterContext context, + ListIterator iterator, + Map, FilterContinuationImpl>> suspended) { + if (context.response != null) { + return ExecutionFlow.just(context); + } + if (iterator.hasNext()) { + GenericHttpFilter filter = iterator.next(); + return processRequestFilter(filter, context, suspended) + .flatMap(newContext -> filterRequest0(newContext, iterator, suspended)) + .onErrorResume(throwable -> { + // Un-suspend possibly awaiting filter and exception filtering scenario of the http client + return filterResponse(context, iterator, throwable, suspended).map(context::withResponse); + }); + } else { + return ExecutionFlow.error(new IllegalStateException("Request filters didn't produce any response!")); + } + } + + private ExecutionFlow> filterResponse(FilterContext context, + ListIterator iterator, + @Nullable + Throwable exception, + Map, FilterContinuationImpl>> suspended) { + if (iterator.hasPrevious()) { + // Walk backwards and execute response filters or un-suspend request filters waiting for the response + GenericHttpFilter filter = iterator.previous(); + return processResponseFilter(filter, context, exception, suspended) + .flatMap(newContext -> { + if (context != newContext) { + return processResponse(newContext.request, newContext.response).map(context::withResponse); + } + return ExecutionFlow.just(newContext); + }) + .onErrorResume(throwable -> processFailure(context.request, throwable).map(context::withResponse)) + .flatMap(newContext -> filterResponse(newContext, iterator, newContext.response == null ? exception : null, suspended)); + } else if (context.response != null) { + return ExecutionFlow.just(context.response); + } else if (exception != null) { + // This scenario only applies for client filters + // Filters didn't remap the exception to any response + return ExecutionFlow.error(exception); + } else { + return ExecutionFlow.error(new IllegalStateException("No response after response filters completed!")); + } + } + + private ExecutionFlow processRequestFilter(GenericHttpFilter filter, + FilterContext context, + Map, + FilterContinuationImpl>> suspended) { + Executor executeOn; + if (filter instanceof GenericHttpFilter.Async async) { + executeOn = async.executor(); + filter = async.actual(); + } else { + executeOn = null; + } + + if (filter instanceof FilterMethod before) { + if (before.isResponseFilter) { + // skip filter, only used for response + return ExecutionFlow.just(context); + } + ExecutionFlow filterMethodFlow; + FilterContinuationImpl continuation = before.isSuspended() ? before.createContinuation(context) : null; + FilterMethodContext filterMethodContext = new FilterMethodContext( + context.request, + context.response, + null, + continuation); + if (executeOn == null) { + // possibly continue with next filter + filterMethodFlow = before.filter(context, filterMethodContext); + } else { + if (continuation != null) { + continuation.completeOn = executeOn; + } + filterMethodFlow = ExecutionFlow.async(executeOn, () -> before.filter(context, filterMethodContext)); + } + if (before.isSuspended()) { + if (continuation instanceof ReactiveResultAwareReactiveContinuationImpl) { + // Method consumes reactive continuation and returns reactive result + suspended.put(filter, Map.entry(continuation.filterProcessedFlow(), continuation)); + } else if (continuation instanceof ReactiveContinuationImpl) { + // Method consumes reactive continuation and doesn't return reactive result + throw new IllegalStateException("Not supported use-case with reactive continuation and non-reactive return type"); + } else { + // Method consumes blocking continuation + suspended.put(filter, Map.entry(filterMethodFlow, continuation)); + } + // Continue executing other filters while this one is suspended + return continuation.nextFilterFlow(); + } + return filterMethodFlow; + } else if (filter instanceof GenericHttpFilter.AroundLegacy around) { + FilterChainImpl chainSuspensionPoint = new FilterChainImpl(conversionService, context); + // Legacy `Publisher proceed(..)` filters are always suspended + suspended.put(around, Map.entry(chainSuspensionPoint.filterProcessedFlow(), chainSuspensionPoint)); + chainSuspensionPoint.completeOn = executeOn; + if (executeOn == null) { + try { + around.bean().doFilter(context.request, chainSuspensionPoint).subscribe(chainSuspensionPoint); + } catch (Throwable e) { + chainSuspensionPoint.triggerFilterProcessed(context, null, e); + } + return chainSuspensionPoint.nextFilterFlow(); + } else { + return ExecutionFlow.async(executeOn, () -> { + try { + around.bean().doFilter(context.request, chainSuspensionPoint).subscribe(chainSuspensionPoint); + } catch (Throwable e) { + chainSuspensionPoint.triggerFilterProcessed(context, null, e); + } + return chainSuspensionPoint.nextFilterFlow(); + }); + } + } else if (filter instanceof GenericHttpFilter.TerminalReactive || filter instanceof GenericHttpFilter.Terminal || filter instanceof GenericHttpFilter.TerminalWithReactorContext) { + if (executeOn != null) { + throw new IllegalStateException("Async terminal filters not supported"); + } + if (filter.isSuspended()) { + throw new IllegalStateException("Terminal filters cannot be suspended"); + } + ExecutionFlow> terminalFlow; + if (filter instanceof GenericHttpFilter.TerminalWithReactorContext t) { + try { + terminalFlow = t.execute(context.request, context.reactorContext); + } catch (Throwable e) { + terminalFlow = ExecutionFlow.error(e); + } + } else if (filter instanceof GenericHttpFilter.Terminal t) { + try { + terminalFlow = t.execute(context.request); + } catch (Throwable e) { + terminalFlow = ExecutionFlow.error(e); + } + } else { + terminalFlow = ReactiveExecutionFlow.fromPublisher(Mono.from(((GenericHttpFilter.TerminalReactive) filter).responsePublisher()) + .contextWrite(context.reactorContext)); + } + return terminalFlow.flatMap(response -> ExecutionFlow.just(context.withResponse(response))); + } else { + throw new IllegalStateException("Unknown filter type"); + } + } + + private ExecutionFlow processResponseFilter(GenericHttpFilter filter, + FilterContext filterContext, + Throwable exceptionToFilter, + Map, FilterContinuationImpl>> suspended) { + Executor executeOn; + if (filter instanceof GenericHttpFilter.Async async) { + executeOn = async.executor(); + filter = async.actual(); + } else { + executeOn = null; + } + + Map.Entry, FilterContinuationImpl> suspendedFilterData = suspended.get(filter); + if (suspendedFilterData != null) { + // This filter is suspended and awaiting to receive the response + ExecutionFlow filterProcessedFlow = suspendedFilterData.getKey(); + FilterContinuationImpl continuation = suspendedFilterData.getValue(); + // Resume suspended filter + continuation.resume(filterContext, exceptionToFilter); + // Filter flow might modify the context provided + return filterProcessedFlow; + } + + if (exceptionToFilter != null && !filter.isFiltersException()) { + return ExecutionFlow.just(filterContext); + } + + if (filter instanceof FilterMethod after && after.isResponseFilter) { + if (after.isSuspended()) { + return ExecutionFlow.error(new IllegalStateException("Response filter cannot have a continuation!")); + } + FilterMethodContext filterMethodContext = new FilterMethodContext( + filterContext.request, + filterContext.response, + exceptionToFilter, + null); + if (executeOn == null) { + return after.filter(filterContext, filterMethodContext); + } else { + return ExecutionFlow.async(executeOn, () -> after.filter(filterContext, filterMethodContext)); + } + } + return ExecutionFlow.just(filterContext); + } + + @Internal + public static FilterMethod prepareFilterMethod(ConversionService conversionService, + T bean, + ExecutableMethod method, + boolean isResponseFilter, + FilterOrder order) throws IllegalArgumentException { + return prepareFilterMethod(conversionService, bean, method, method.getArguments(), method.getReturnType().asArgument(), isResponseFilter, order); + } + + @Internal + public static void validateFilterMethod(Argument[] arguments, + Argument returnType, + boolean isResponseFilter) throws IllegalArgumentException { + prepareFilterMethod(ConversionService.SHARED, null, null, arguments, returnType, isResponseFilter, null); + } + + @Internal + public static FilterMethod prepareFilterMethod(ConversionService conversionService, + T bean, + ExecutableMethod method, + Argument[] arguments, + Argument returnType, + boolean isResponseFilter, + FilterOrder order) throws IllegalArgumentException { + FilterArgBinder[] fulfilled = new FilterArgBinder[arguments.length]; + Predicate filterCondition = FILTER_CONDITION_ALWAYS_TRUE; + boolean skipOnError = isResponseFilter; + boolean filtersException = false; + Function> continuationCreator = null; + for (int i = 0; i < arguments.length; i++) { + Argument argument = arguments[i]; + if (argument.getType().isAssignableFrom(HttpRequest.class)) { + fulfilled[i] = ctx -> ctx.request; + } else if (argument.getType().isAssignableFrom(MutableHttpRequest.class)) { + fulfilled[i] = ctx -> { + HttpRequest request = ctx.request; + if (!(ctx.request instanceof MutableHttpRequest)) { + request = ctx.request.mutate(); + } + return request; + }; + } else if (argument.getType().isAssignableFrom(MutableHttpResponse.class)) { + if (!isResponseFilter) { + throw new IllegalArgumentException("Filter is called before the response is known, can't have a response argument"); + } + fulfilled[i] = ctx -> ctx.response; + } else if (Throwable.class.isAssignableFrom(argument.getType())) { + if (!isResponseFilter) { + throw new IllegalArgumentException("Request filters cannot handle exceptions"); + } + if (!argument.isNullable()) { + filterCondition = filterCondition.and(ctx -> ctx.failure != null && argument.isInstance(ctx.failure)); + fulfilled[i] = ctx -> ctx.failure; + } else { + fulfilled[i] = ctx -> { + if (ctx.failure != null && argument.isInstance(ctx.failure)) { + return ctx.failure; + } + return null; + }; + } + filtersException = true; + skipOnError = false; + } else if (argument.getType() == FilterContinuation.class) { + if (isResponseFilter) { + throw new IllegalArgumentException("Response filters cannot use filter continuations"); + } + if (continuationCreator != null) { + throw new IllegalArgumentException("Only one continuation per filter is allowed"); + } + Argument continuationReturnType = argument.getFirstTypeVariable().orElseThrow(() -> new IllegalArgumentException("Continuations must specify generic type")); + if (isReactive(continuationReturnType) && continuationReturnType.getWrappedType().isAssignableFrom(MutableHttpResponse.class)) { + if (isReactive(returnType)) { + continuationCreator = ctx -> new ReactiveResultAwareReactiveContinuationImpl<>(conversionService, ctx); + } else { + continuationCreator = ctx -> new ReactiveContinuationImpl<>(conversionService, ctx, continuationReturnType.getType()); + } + fulfilled[i] = ctx -> ctx.continuation; + } else if (continuationReturnType.getType().isAssignableFrom(MutableHttpResponse.class)) { + continuationCreator = BlockingContinuationImpl::new; + fulfilled[i] = ctx -> ctx.continuation; + } else { + throw new IllegalArgumentException("Unsupported continuation type: " + continuationReturnType); + } + } else { + throw new IllegalArgumentException("Unsupported filter argument type: " + argument); + } + } + if (skipOnError) { + filterCondition = filterCondition.and(ctx -> ctx.failure == null); + } else if (filterCondition == FILTER_CONDITION_ALWAYS_TRUE) { + filterCondition = null; + } + FilterReturnHandler returnHandler = prepareReturnHandler(conversionService, returnType, isResponseFilter, continuationCreator != null, false); + return new FilterMethod<>( + order, + bean, + method, + isResponseFilter, + fulfilled, + filterCondition, + continuationCreator, + filtersException, + returnHandler + ); + } + + private static boolean isReactive(Argument continuationReturnType) { + // Argument.isReactive doesn't work in http-validation, this is a workaround + return continuationReturnType.isReactive() || continuationReturnType.getType() == Publisher.class; + } + + private static FilterReturnHandler prepareReturnHandler(ConversionService conversionService, + Argument type, + boolean isResponseFilter, + boolean hasContinuation, + boolean fromOptional) throws IllegalArgumentException { + if (type.isOptional()) { + FilterReturnHandler next = prepareReturnHandler(conversionService, type.getWrappedType(), isResponseFilter, hasContinuation, true); + return (r, o, c) -> next.handle(r, o == null ? null : ((Optional) o).orElse(null), c); + } + if (type.isVoid()) { + if (hasContinuation) { + return FilterReturnHandler.VOID_WITH_CONTINUATION; + } else { + return FilterReturnHandler.VOID; + } + } + boolean nullable = type.isNullable() || fromOptional; + if (!isResponseFilter) { + if (type.getType() == HttpRequest.class || type.getType() == MutableHttpRequest.class) { + if (hasContinuation) { + throw new IllegalArgumentException("Filter method that accepts a continuation cannot return an HttpRequest"); + } + if (nullable) { + return FilterReturnHandler.REQUEST_NULLABLE; + } else { + return FilterReturnHandler.REQUEST; + } + } else if (type.getType() == HttpResponse.class || type.getType() == MutableHttpResponse.class) { + if (hasContinuation) { + return FilterReturnHandler.FROM_REQUEST_RESPONSE_WITH_CONTINUATION; + } else { + if (nullable) { + return FilterReturnHandler.FROM_REQUEST_RESPONSE_NULLABLE; + } else { + return FilterReturnHandler.FROM_REQUEST_RESPONSE; + } + } + } + } else { + if (hasContinuation) { + throw new AssertionError(); + } + if (type.getType() == HttpResponse.class || type.getType() == MutableHttpResponse.class) { + if (nullable) { + return FilterReturnHandler.FROM_RESPONSE_RESPONSE_NULLABLE; + } else { + return FilterReturnHandler.FROM_RESPONSE_RESPONSE; + } + } + } + if (isReactive(type)) { + var next = prepareReturnHandler(conversionService, type.getWrappedType(), isResponseFilter, hasContinuation, false); + return new FilterReturnHandler() { + + @Override + public ExecutionFlow handle(FilterContext context, Object returnValue, FilterContinuationImpl continuation) throws Throwable { + if (returnValue == null && !nullable) { + return next.handle(context, null, continuation); + } + + Mono publisher = Mono.from(Publishers.convertPublisher(conversionService, returnValue, Publisher.class)) + .contextWrite(context.reactorContext()); + + if (continuation instanceof ReactiveResultAwareReactiveContinuationImpl reactiveContinuation) { + publisher.subscribe(reactiveContinuation); + return reactiveContinuation.nextFilterFlow(); + } + return ReactiveExecutionFlow.fromPublisher(publisher).flatMap(v -> { + try { + return next.handle(context, v, continuation); + } catch (Throwable e) { + return ExecutionFlow.error(e); + } + }); + } + + }; + } else if (type.isAsync()) { + var next = prepareReturnHandler(conversionService, type.getWrappedType(), isResponseFilter, hasContinuation, false); + return new DelayedFilterReturnHandler(isResponseFilter, next, nullable) { + @Override + protected ExecutionFlow toFlow(FilterContext context, Object returnValue, FilterContinuationImpl continuation) { + //noinspection unchecked + return CompletableFutureExecutionFlow.just(((CompletionStage) returnValue).toCompletableFuture()); + } + }; + } else { + throw new IllegalArgumentException("Unsupported filter return type " + type.getType().getName()); + } + } + + record FilterMethod(FilterOrder order, + T bean, + Executable method, + boolean isResponseFilter, + FilterArgBinder[] argBinders, + @Nullable + Predicate filterCondition, + Function> continuationCreator, + boolean filtersException, + FilterReturnHandler returnHandler + ) implements GenericHttpFilter, Ordered { + + @Override + public boolean isSuspended() { + return continuationCreator != null; + } + + @Override + public boolean isFiltersException() { + return filtersException; + } + + @Override + public int getOrder() { + return order.getOrder(bean); + } + + public FilterContinuationImpl createContinuation(FilterContext filterContext) { + return continuationCreator.apply(filterContext); + } + + private ExecutionFlow filter(FilterContext filterContext, + FilterMethodContext methodContext) { + try { + if (filterCondition != null && !filterCondition.test(methodContext)) { + return ExecutionFlow.just(filterContext); + } + Object[] args = bindArgs(methodContext); + Object returnValue = method.invoke(bean, args); + return returnHandler.handle(filterContext, returnValue, methodContext.continuation); + } catch (Throwable e) { + if (methodContext.continuation != null) { + return methodContext.continuation.afterMethodExecuted(e); + } + return ExecutionFlow.error(e); + } + } + + private Object[] bindArgs(FilterMethodContext context) { + Object[] args = new Object[argBinders.length]; + for (int i = 0; i < args.length; i++) { + args[i] = argBinders[i].bind(context); + } + return args; + } + + } + + private record FilterMethodContext( + HttpRequest request, + @Nullable HttpResponse response, + @Nullable Throwable failure, + @Nullable FilterContinuationImpl continuation) { + } + + private interface FilterArgBinder { + Object bind(FilterMethodContext context); + } + + private interface FilterReturnHandler { + /** + * Void method that accepts a continuation. + */ + FilterReturnHandler VOID_WITH_CONTINUATION = (filterContext, returnValue, continuation) -> continuation.afterMethodExecuted(); + /** + * Void method. + */ + FilterReturnHandler VOID = (filterContext, returnValue, continuation) -> ExecutionFlow.just(filterContext); + /** + * Request handler that returns a response but also accepts a continuation. + */ + FilterReturnHandler FROM_REQUEST_RESPONSE_WITH_CONTINUATION = (filterContext, returnValue, continuation) -> { + if (returnValue == null) { + return continuation.afterMethodExecuted(); + } else { + return continuation.afterMethodExecuted((HttpResponse) returnValue); + } + }; + /** + * Request handler that returns a new request. + */ + FilterReturnHandler REQUEST = (filterContext, returnValue, continuation) -> ExecutionFlow.just( + filterContext.withRequest( + (HttpRequest) Objects.requireNonNull(returnValue, "Returned request must not be null, or mark the method as @Nullable") + ) + ); + /** + * Request handler that returns a new request (nullable). + */ + FilterReturnHandler REQUEST_NULLABLE = (filterContext, returnValue, continuation) -> { + if (returnValue == null) { + return ExecutionFlow.just(filterContext); + } + return ExecutionFlow.just( + filterContext.withRequest((HttpRequest) returnValue) + ); + }; + /** + * Request handler that returns a response. + */ + FilterReturnHandler FROM_REQUEST_RESPONSE = (filterContext, returnValue, continuation) -> { + // cancel request pipeline, move immediately to response handling + return ExecutionFlow.just( + filterContext + .withResponse( + (HttpResponse) Objects.requireNonNull(returnValue, "Returned response must not be null, or mark the method as @Nullable") + ) + ); + }; + /** + * Request handler that returns a response (nullable). + */ + FilterReturnHandler FROM_REQUEST_RESPONSE_NULLABLE = (filterContext, returnValue, continuation) -> { + if (returnValue == null) { + return ExecutionFlow.just(filterContext); + } + // cancel request pipeline, move immediately to response handling + return ExecutionFlow.just( + filterContext.withResponse((HttpResponse) returnValue) + ); + }; + /** + * Response handler that returns a new response. + */ + FilterReturnHandler FROM_RESPONSE_RESPONSE = (filterContext, returnValue, continuation) -> { + // cancel request pipeline, move immediately to response handling + return ExecutionFlow.just( + filterContext + .withResponse( + (HttpResponse) Objects.requireNonNull(returnValue, "Returned response must not be null, or mark the method as @Nullable") + ) + ); + }; + /** + * Response handler that returns a new response (nullable). + */ + FilterReturnHandler FROM_RESPONSE_RESPONSE_NULLABLE = (filterContext, returnValue, continuation) -> { + if (returnValue == null) { + return ExecutionFlow.just(filterContext); + } + // cancel request pipeline, move immediately to response handling + return ExecutionFlow.just( + filterContext.withResponse((HttpResponse) returnValue) + ); + }; + + ExecutionFlow handle(FilterContext context, + @Nullable Object returnValue, + @Nullable FilterContinuationImpl passedOnContinuation) throws Throwable; + } + + private abstract static class DelayedFilterReturnHandler implements FilterReturnHandler { + final boolean isResponseFilter; + final FilterReturnHandler next; + final boolean nullable; + + private DelayedFilterReturnHandler(boolean isResponseFilter, FilterReturnHandler next, boolean nullable) { + this.isResponseFilter = isResponseFilter; + this.next = next; + this.nullable = nullable; + } + + protected abstract ExecutionFlow toFlow(FilterContext context, + Object returnValue, + @Nullable FilterContinuationImpl continuation); + + @Override + public ExecutionFlow handle(FilterContext context, + @Nullable Object returnValue, + FilterContinuationImpl continuation) throws Throwable { + if (returnValue == null && nullable) { + return next.handle(context, null, continuation); + } + + ExecutionFlow delayedFlow = toFlow(context, + Objects.requireNonNull(returnValue, "Returned value must not be null, or mark the method as @Nullable"), + continuation + ); + ImperativeExecutionFlow doneFlow = delayedFlow.tryComplete(); + if (doneFlow != null) { + if (doneFlow.getError() != null) { + throw doneFlow.getError(); + } + return next.handle(context, doneFlow.getValue(), continuation); + } else { + return delayedFlow.flatMap(v -> { + try { + return next.handle(context, v, continuation); + } catch (Throwable e) { + return ExecutionFlow.error(e); + } + }); + } + } + } + + /** + * This class implements the "continuation" request filter pattern. It is used by filters that + * accept a {@link FilterContinuation}, but also by legacy {@link HttpFilter}s.
+ * Continuations give the user the choice when to proceed with filter execution. + * After the proceed is triggered the filter is essentially suspended and the next filter in the chain should be executed. + * + * @param Return value of the continuation + */ + private abstract static class FilterContinuationImpl implements FilterContinuation { + + /** + * Executor to run any downstream reactive code on. Only used by some implementations, e.g. + * it doesn't make sense for a blocking continuation. + */ + @Nullable + Executor completeOn = null; + + FilterContext filterContext; + + /** + * The future indicating that the next filter should be executed. + */ + final CompletableFuture nextFilterProcessing = new CompletableFuture<>(); + /** + * The future representing the suspension point, completing it will resume this filter processing. + */ + final CompletableFuture suspensionPoint = new CompletableFuture<>(); + /** + * The future representing the filter return value and will be completed when the filter method is finally processed. + */ + final CompletableFuture filterProcessed = new CompletableFuture<>(); + + FilterContinuationImpl(FilterContext filterContext) { + this.filterContext = filterContext; + } + + @Override + public FilterContinuation request(HttpRequest request) { + filterContext = filterContext.withRequest(Objects.requireNonNull(request, "request")); + return this; + } + + protected final void proceedRequested() { + if (!nextFilterProcessing.isDone()) { + nextFilterProcessing.complete(filterContext); + } else { + throw new IllegalStateException("Already subscribed to proceed() publisher, or filter method threw an exception and was cancelled"); + } + } + + /** + * The filter is suspended. After this filter is ready returned flow will process a next filter. + */ + public ExecutionFlow nextFilterFlow() { + return CompletableFutureExecutionFlow.just(nextFilterProcessing); + } + + /** + * The flow to continue after the suspended filter is finished. + */ + public ExecutionFlow filterProcessedFlow() { + return CompletableFutureExecutionFlow.just(filterProcessed); + } + + /** + * Resume suspended method with a new context. + * + * @param filterContext The context to resume the suspend method. + * @param throwable The exception + */ + public void resume(FilterContext filterContext, Throwable throwable) { + if (!suspensionPoint.isDone()) { + if (throwable == null) { + suspensionPoint.complete(filterContext); + } else { + suspensionPoint.completeExceptionally(throwable); + } + } else { + if (throwable == null) { + LOG.warn("Two outcomes for one continuation, this one is swallowed: {}", filterContext.response); + } else { + LOG.warn("Two outcomes for one continuation, this one is swallowed:", throwable); + } + } + } + + /** + * The filter method completed without modifying response / failed status. + */ + private ExecutionFlow afterMethodExecuted() { + return afterMethodExecuted(null, null); + } + + /** + * The filter method completed with modified response. + */ + private ExecutionFlow afterMethodExecuted(@NonNull HttpResponse response) { + return afterMethodExecuted(response, null); + } + + /** + * The filter method completed with a failure. + */ + ExecutionFlow afterMethodExecuted(@NonNull Throwable throwable) { + return afterMethodExecuted(null, throwable); + } + + /** + * Forward a given response from this suspension point. If {@link #proceed} was already + * called, this waits for the downstream filters to finish. + */ + private ExecutionFlow afterMethodExecuted(@Nullable HttpResponse newResponse, + @Nullable Throwable newFailure) { + FilterContext newFilterContext; + if (suspensionPoint.isDone()) { + // If the method modifies the response / failure, extend its filter context for downstream + // This is blocking scenario + try { + newFilterContext = suspensionPoint.get(); + } catch (Exception e) { + return ExecutionFlow.error(new IllegalStateException("Failed to extract suspension point result", e)); + } + } else { + newFilterContext = filterContext; + } + return asFilterProcessed(newFilterContext, newResponse, newFailure); + } + + protected void triggerFilterProcessed(FilterContext filterContext, + @Nullable + HttpResponse newResponse, + @Nullable + Throwable newFailure) { + if (!nextFilterProcessing.isDone()) { + // Publish the error to the nextFilterProcessing as well + if (newFailure == null) { + nextFilterProcessing.complete(newResponse == null ? filterContext : filterContext.withResponse(newResponse)); + } else { + nextFilterProcessing.completeExceptionally(newFailure); + } + } + if (!filterProcessed.isDone()) { + if (newFailure == null) { + filterProcessed.complete(newResponse == null ? filterContext : filterContext.withResponse(newResponse)); + } else { + filterProcessed.completeExceptionally(newFailure); + } + } else { + if (newFailure == null) { + LOG.warn("Two outcomes for one continuation, this one is swallowed: {}", newResponse); + } else { + LOG.warn("Two outcomes for one continuation, this one is swallowed:", newFailure); + } + } + } + + @NonNull + private ExecutionFlow asFilterProcessed(FilterContext filterContext, + @Nullable + HttpResponse newResponse, + @Nullable + Throwable newFailure) { + triggerFilterProcessed(filterContext, newResponse, newFailure); + return CompletableFutureExecutionFlow.just(filterProcessed); + } + + } + + private record FilterContext(HttpRequest request, + @Nullable HttpResponse response, + Context reactorContext) { + + FilterContext(HttpRequest request, Context reactorContext) { + this(request, null, reactorContext); + } + + public FilterContext withRequest(@NonNull HttpRequest request) { + if (this.request == request) { + return this; + } + if (response != null) { + throw new IllegalStateException("Cannot modify the request after response is set!"); + } + Objects.requireNonNull(request); + return new FilterContext(request, response, reactorContext); + } + + public FilterContext withResponse(@NonNull HttpResponse response) { + if (this.response == response) { + return this; + } + Objects.requireNonNull(response); + // New response should remove the failure + return new FilterContext(request, response, reactorContext); + } + + public FilterContext withReactorContext(@NonNull Context reactorContext) { + if (this.reactorContext == reactorContext) { + return this; + } + Objects.requireNonNull(reactorContext); + return new FilterContext(request, response, reactorContext); + } + + } + + /** + * Continuation implementation that yields a reactive type.
+ * This class implements a bunch of interfaces that it would otherwise have to create lambdas + * for. + * + * @param The reactive type to return (e.g. Publisher, Mono, Flux...) + */ + private static class ReactiveContinuationImpl extends FilterContinuationImpl + implements CorePublisher>, Subscription, BiConsumer { + private final ConversionService conversionService; + private final Class reactiveType; + private Subscriber> subscriber = null; + private boolean addedListener = false; + + ReactiveContinuationImpl(ConversionService conversionService, FilterContext filterContext, Class reactiveType) { + super(filterContext); + this.conversionService = conversionService; + this.reactiveType = reactiveType; + } + + @Override + public R proceed() { + return Publishers.convertPublisher(conversionService, this, reactiveType); + } + + @SuppressWarnings("NullableProblems") + @Override + public void subscribe(@NonNull CoreSubscriber> subscriber) { + subscribe((Subscriber>) subscriber); + } + + @Override + public void subscribe(Subscriber> s) { + if (this.subscriber != null) { + throw new IllegalStateException("Only one subscriber allowed"); + } + this.subscriber = s; + + if (s instanceof CoreSubscriber cs) { + filterContext = filterContext.withReactorContext(cs.currentContext()); + } + + proceedRequested(); + s.onSubscribe(this); + } + + @Override + public void request(long n) { + if (n > 0 && !addedListener) { + addedListener = true; + if (completeOn == null) { + suspensionPoint.whenComplete(this); + } else { + suspensionPoint.whenCompleteAsync(this, completeOn); + } + } + } + + @Override + public void cancel() { + // ignored + } + + @Override + public void accept(FilterContext filterContext, Throwable throwable) { + // Suspension point resumed + try { + if (throwable == null) { + this.filterContext = filterContext; + subscriber.onNext(filterContext.response); + subscriber.onComplete(); + } else { + subscriber.onError(throwable); + } + } catch (Throwable t) { + LOG.warn("Subscriber threw exception", t); + } + } + } + + /** + * {@link FilterContinuationImpl} that is adapted for filters returning a reactive response . + * Implements the {@link Subscriber} that will subscribe to the method's return value. + * + * @param The published item type + */ + private static class ReactiveResultAwareReactiveContinuationImpl extends ReactiveContinuationImpl> + implements CoreSubscriber> { + + ReactiveResultAwareReactiveContinuationImpl(ConversionService conversionService, FilterContext filterContext) { + //noinspection unchecked,rawtypes + super(conversionService, filterContext, (Class) Publisher.class); + } + + @Override + public Publisher proceed() { + // HACK: kotlin coroutine context propagation only supports reactor types (see + // ReactorContextInjector). If we want to support our own type, we would need our own + // ContextInjector, but that interface is marked as internal. + // Another solution could be to PR kotlin to support all CorePublishers in + // ReactorContextInjector. + return Mono.from(super.proceed()); + } + + @SuppressWarnings("NullableProblems") + @Override + public void onSubscribe(@NonNull Subscription s) { + s.request(Long.MAX_VALUE); + } + + @Override + public void onNext(HttpResponse response) { + triggerFilterProcessed(filterContext, response, null); + } + + @Override + public void onError(Throwable t) { + triggerFilterProcessed(filterContext, null, t); + } + + @Override + public void onComplete() { + if (!suspensionPoint.isDone()) { + triggerFilterProcessed(filterContext, null, new IllegalStateException("Publisher did not return response")); + } + } + + @SuppressWarnings("NullableProblems") + @NonNull + @Override + public Context currentContext() { + return filterContext.reactorContext; + } + } + + /** + * {@link ReactiveResultAwareReactiveContinuationImpl} that is adapted for legacy filters: Implements {@link FilterChain}. + */ + private static final class FilterChainImpl extends ReactiveResultAwareReactiveContinuationImpl> + implements ClientFilterChain, ServerFilterChain { + FilterChainImpl(ConversionService conversionService, FilterContext filterContext) { + super(conversionService, filterContext); + } + + @Override + public Publisher> proceed(MutableHttpRequest request) { + return proceed((HttpRequest) request); + } + + @Override + public Publisher> proceed(HttpRequest request) { + request(request); + return proceed(); + } + + } + + /** + * Implementation of {@link FilterContinuation} for blocking calls. + */ + private static final class BlockingContinuationImpl extends FilterContinuationImpl> { + BlockingContinuationImpl(FilterContext filterContext) { + super(filterContext); + } + + @Override + public HttpResponse proceed() { + proceedRequested(); + + boolean interrupted = false; + while (true) { + try { + // todo: detect event loop thread + filterContext = suspensionPoint.get(); + if (interrupted) { + Thread.currentThread().interrupt(); + } + return filterContext.response; + } catch (InterruptedException e) { + interrupted = true; + } catch (ExecutionException e) { + Throwable cause = e.getCause(); + if (cause instanceof RuntimeException re) { + throw re; + } else { + throw new RuntimeException(cause); + } + } + } + } + } + +} diff --git a/http/src/main/java/io/micronaut/http/filter/GenericHttpFilter.java b/http/src/main/java/io/micronaut/http/filter/GenericHttpFilter.java new file mode 100644 index 00000000000..8259ad252c4 --- /dev/null +++ b/http/src/main/java/io/micronaut/http/filter/GenericHttpFilter.java @@ -0,0 +1,153 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.http.filter; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.execution.ExecutionFlow; +import io.micronaut.core.order.Ordered; +import io.micronaut.core.util.Toggleable; +import io.micronaut.http.HttpRequest; +import io.micronaut.http.HttpResponse; +import org.reactivestreams.Publisher; +import reactor.util.context.Context; + +import java.util.concurrent.Executor; + +/** + * Base interface for different filter types. Note that while the base interface is exposed, so you + * can pass around instances of these filters, the different implementations are internal only. + * Only the framework should construct or call instances of this interface. The exception is the + * {@link Terminal terminal filter}. + * + * @author Jonas Konrad + * @since 4.0.0 + */ +public sealed interface GenericHttpFilter + permits + FilterRunner.FilterMethod, + GenericHttpFilter.AroundLegacy, + GenericHttpFilter.Async, + GenericHttpFilter.Terminal, + GenericHttpFilter.TerminalReactive, + GenericHttpFilter.TerminalWithReactorContext { + + /** + * When the filter is using the continuation it needs to be suspended and wait for the response. + * @return true if suspended + */ + default boolean isSuspended() { + return false; + } + + /** + * @return true if the filter can receive the processing exception. + */ + default boolean isFiltersException() { + return false; + } + + /** + * Wrapper around a filter that signifies the filter should be run asynchronously on the given + * executor. Usually from an {@link io.micronaut.scheduling.annotation.ExecuteOn} annotation. + * + * @param actual Actual filter + * @param executor Executor to run the filter on + */ + @Internal + record Async( + GenericHttpFilter actual, + Executor executor + ) implements GenericHttpFilter, Ordered { + + @Override + public boolean isSuspended() { + return actual.isSuspended(); + } + + @Override + public boolean isFiltersException() { + return actual.isFiltersException(); + } + + @Override + public int getOrder() { + return ((Ordered) actual).getOrder(); + } + } + + /** + * "Legacy" filter, i.e. filter bean that implements {@link HttpFilter}. + * + * @param bean The filter bean + * @param order The filter order + */ + @Internal + record AroundLegacy( + HttpFilter bean, + FilterOrder order + ) implements GenericHttpFilter, Ordered { + + @Override + public boolean isSuspended() { + // Legacy filters are always suspended + return true; + } + + @Override + public boolean isFiltersException() { + // Legacy filters can filter the exception using the publisher + return false; + } + + public boolean isEnabled() { + return !(bean instanceof Toggleable t) || t.isEnabled(); + } + + @Override + public int getOrder() { + return order.getOrder(bean); + } + } + + /** + * Terminal filter that accepts a reactive type. Used as a temporary solution for the http + * client, until that is un-reactified. + * + * @param responsePublisher The response publisher + */ + @Internal + record TerminalReactive(Publisher> responsePublisher) implements GenericHttpFilter { + } + + /** + * Like {@link Terminal}, with an additional parameter for the reactive context. + */ + @Internal + @FunctionalInterface + non-sealed interface TerminalWithReactorContext extends GenericHttpFilter { + ExecutionFlow> execute(HttpRequest request, Context context) throws Exception; + } + + /** + * Last item in a filter chain, called when all other filters are done. Basically, this runs + * the actual request. + */ + @Internal + @FunctionalInterface + non-sealed interface Terminal extends GenericHttpFilter { + ExecutionFlow> execute(HttpRequest request) throws Exception; + } +} diff --git a/http/src/main/java/io/micronaut/http/filter/HttpClientFilterResolver.java b/http/src/main/java/io/micronaut/http/filter/HttpClientFilterResolver.java index a72f2064a09..92738fe1272 100644 --- a/http/src/main/java/io/micronaut/http/filter/HttpClientFilterResolver.java +++ b/http/src/main/java/io/micronaut/http/filter/HttpClientFilterResolver.java @@ -25,5 +25,5 @@ * @author graemerocher * @since 2.0 */ -public interface HttpClientFilterResolver extends HttpFilterResolver { +public interface HttpClientFilterResolver extends HttpFilterResolver { } diff --git a/http/src/main/java/io/micronaut/http/filter/HttpFilterResolver.java b/http/src/main/java/io/micronaut/http/filter/HttpFilterResolver.java index 00b379b7da0..52437e12d0c 100644 --- a/http/src/main/java/io/micronaut/http/filter/HttpFilterResolver.java +++ b/http/src/main/java/io/micronaut/http/filter/HttpFilterResolver.java @@ -15,10 +15,11 @@ */ package io.micronaut.http.filter; -import io.micronaut.core.annotation.NonNull; -import io.micronaut.core.annotation.Nullable; import io.micronaut.core.annotation.AnnotationMetadata; import io.micronaut.core.annotation.AnnotationMetadataProvider; +import io.micronaut.core.annotation.NonNull; +import io.micronaut.core.annotation.Nullable; +import io.micronaut.core.order.OrderUtil; import io.micronaut.core.util.ArrayUtils; import io.micronaut.core.util.CollectionUtils; import io.micronaut.http.HttpMethod; @@ -34,10 +35,9 @@ * @author James Kleeh * @author graemerocher * @since 1.3.0 - * @param The filter type * @param The resolution context type */ -public interface HttpFilterResolver { +public interface HttpFilterResolver { /** * Resolves the initial list of filters. @@ -45,7 +45,7 @@ public interface HttpFilterResolver> resolveFilterEntries(T context); + List resolveFilterEntries(T context); /** * Returns which filters should apply for the given request. @@ -54,17 +54,17 @@ public interface HttpFilterResolver resolveFilters(HttpRequest request, List> filterEntries); + List resolveFilters(HttpRequest request, List filterEntries); /** * A resolved filter entry. - * @param The filter type */ - interface FilterEntry extends AnnotationMetadataProvider { + interface FilterEntry extends AnnotationMetadataProvider { /** * @return The filter */ - @NonNull F getFilter(); + @NonNull + GenericHttpFilter getFilter(); /** * @return The filter methods. @@ -98,29 +98,6 @@ default boolean hasPatterns() { return ArrayUtils.isNotEmpty(getPatterns()); } - /** - * Creates a filter entry for the given arguments. - * @param filter The filter - * @param annotationMetadata The annotation metadata - * @param methods The methods - * @param patterns The patterns - * @return The filter entry - * @param the filter type - */ - static FilterEntry of( - @NonNull FT filter, - @Nullable AnnotationMetadata annotationMetadata, - @Nullable Set methods, - String... patterns) { - return new DefaultFilterEntry<>( - Objects.requireNonNull(filter, "Filter cannot be null"), - annotationMetadata != null ? annotationMetadata : AnnotationMetadata.EMPTY_METADATA, - methods, - null, - patterns - ); - } - /** * Creates a filter entry for the given arguments. * @param filter The filter @@ -129,15 +106,16 @@ static FilterEntry of( * @param patternStyle the pattern style * @param patterns The patterns * @return The filter entry - * @param the filter type */ - static FilterEntry of( - @NonNull FT filter, + static FilterEntry of( + @NonNull HttpFilter filter, @Nullable AnnotationMetadata annotationMetadata, @Nullable Set methods, @NonNull FilterPatternStyle patternStyle, String... patterns) { - return new DefaultFilterEntry<>( - Objects.requireNonNull(filter, "Filter cannot be null"), + return new DefaultFilterEntry( + new GenericHttpFilter.AroundLegacy( + Objects.requireNonNull(filter, "Filter cannot be null"), + new FilterOrder.Dynamic(OrderUtil.getOrder(annotationMetadata))), annotationMetadata != null ? annotationMetadata : AnnotationMetadata.EMPTY_METADATA, methods, patternStyle, diff --git a/http/src/main/java/io/micronaut/http/filter/HttpServerFilterResolver.java b/http/src/main/java/io/micronaut/http/filter/HttpServerFilterResolver.java index 2ecce6d1b39..f1f2722aa47 100644 --- a/http/src/main/java/io/micronaut/http/filter/HttpServerFilterResolver.java +++ b/http/src/main/java/io/micronaut/http/filter/HttpServerFilterResolver.java @@ -25,6 +25,6 @@ * @author graemerocher * @since 2.0 */ -public interface HttpServerFilterResolver extends HttpFilterResolver { +public interface HttpServerFilterResolver extends HttpFilterResolver { } diff --git a/http/src/main/java/io/micronaut/http/reactive/execution/ReactiveExecutionFlow.java b/http/src/main/java/io/micronaut/http/reactive/execution/ReactiveExecutionFlow.java index 5389eae5109..2ec0e628312 100644 --- a/http/src/main/java/io/micronaut/http/reactive/execution/ReactiveExecutionFlow.java +++ b/http/src/main/java/io/micronaut/http/reactive/execution/ReactiveExecutionFlow.java @@ -88,4 +88,7 @@ static ReactiveExecutionFlow fromFlow(@NonNull ExecutionFlow flow) { @NonNull Publisher toPublisher(); + static Publisher toPublisher(Supplier> flowSupplier) { + return (Publisher) ReactorExecutionFlowImpl.toMono(flowSupplier); + } } diff --git a/http/src/main/java/io/micronaut/http/reactive/execution/ReactorExecutionFlowImpl.java b/http/src/main/java/io/micronaut/http/reactive/execution/ReactorExecutionFlowImpl.java index 54a4b1b1114..74fb7d33722 100644 --- a/http/src/main/java/io/micronaut/http/reactive/execution/ReactorExecutionFlowImpl.java +++ b/http/src/main/java/io/micronaut/http/reactive/execution/ReactorExecutionFlowImpl.java @@ -16,12 +16,14 @@ package io.micronaut.http.reactive.execution; import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.Nullable; import io.micronaut.core.execution.CompletableFutureExecutionFlow; import io.micronaut.core.execution.ExecutionFlow; import io.micronaut.core.execution.ImperativeExecutionFlow; import org.reactivestreams.Publisher; import org.reactivestreams.Subscriber; import org.reactivestreams.Subscription; +import reactor.core.Fuseable; import reactor.core.publisher.Mono; import java.util.Map; @@ -111,6 +113,19 @@ public void onComplete() { }); } + @Nullable + @Override + public ImperativeExecutionFlow tryComplete() { + if (value instanceof Fuseable.ScalarCallable callable) { + try { + return (ImperativeExecutionFlow) ExecutionFlow.just(callable.call()); + } catch (Exception e) { + return (ImperativeExecutionFlow) ExecutionFlow.error(e); + } + } + return null; + } + static Mono toMono(ExecutionFlow next) { if (next instanceof ReactorExecutionFlowImpl reactiveFlowImpl) { return reactiveFlowImpl.value; @@ -139,6 +154,10 @@ static Mono toMono(ExecutionFlow next) { throw new IllegalStateException(); } + static Mono toMono(Supplier> next) { + return Mono.defer(() -> toMono(next.get())); + } + @Override public Publisher toPublisher() { return value; diff --git a/http/src/test/groovy/io/micronaut/http/filter/BaseFilterProcessorSpec.groovy b/http/src/test/groovy/io/micronaut/http/filter/BaseFilterProcessorSpec.groovy new file mode 100644 index 00000000000..b89aa6e4624 --- /dev/null +++ b/http/src/test/groovy/io/micronaut/http/filter/BaseFilterProcessorSpec.groovy @@ -0,0 +1,17 @@ +package io.micronaut.http.filter + +import spock.lang.Specification + +class BaseFilterProcessorSpec extends Specification { + def 'combine ant patterns'(String bean, String method, String combined) { + expect: + BaseFilterProcessor.concatAntPatterns(bean, method) == combined + + where: + bean | method | combined + '/foo' | 'bar' | '/foo/bar' + '/foo/**' | 'bar' | '/foo/**/bar' + '/foo/**/' | 'bar' | '/foo/**/bar' + '/foo/**/' | '/bar' | '/foo/**/bar' + } +} diff --git a/http/src/test/groovy/io/micronaut/http/filter/FilterRunnerSpec.groovy b/http/src/test/groovy/io/micronaut/http/filter/FilterRunnerSpec.groovy new file mode 100644 index 00000000000..8368b93d7a2 --- /dev/null +++ b/http/src/test/groovy/io/micronaut/http/filter/FilterRunnerSpec.groovy @@ -0,0 +1,736 @@ +package io.micronaut.http.filter + +import io.micronaut.core.annotation.Nullable +import io.micronaut.core.convert.ConversionService +import io.micronaut.core.execution.CompletableFutureExecutionFlow +import io.micronaut.core.execution.ExecutionFlow +import io.micronaut.core.execution.ImperativeExecutionFlow +import io.micronaut.core.type.Argument +import io.micronaut.core.type.ReturnType +import io.micronaut.http.HttpRequest +import io.micronaut.http.HttpResponse +import io.micronaut.http.HttpStatus +import io.micronaut.http.MutableHttpResponse +import io.micronaut.inject.ExecutableMethod +import org.reactivestreams.Publisher +import reactor.core.publisher.Flux +import reactor.util.context.Context +import spock.lang.Ignore +import spock.lang.Specification + +import java.lang.reflect.Method +import java.util.concurrent.CompletableFuture +import java.util.concurrent.ExecutionException +import java.util.concurrent.Executors +import java.util.concurrent.ThreadFactory + +class FilterRunnerSpec extends Specification { + private FilterRunner filterRunner(List filters) { + return new FilterRunner(ConversionService.SHARED, filters); + } + + def 'simple tasks should not suspend'() { + given: + def events = [] + List filters = [ + after(ReturnType.of(void)) { req, resp -> + events.add("after") + null + }, + before(ReturnType.of(void)) { req -> + events.add("before") + null + }, + (GenericHttpFilter.Terminal) (req -> { + events.add("terminal") + ExecutionFlow.just(HttpResponse.ok()) + }) + ] + + when: + def result = filterRunner(filters).run(HttpRequest.GET("/")).tryComplete().value + then: + result.status() == HttpStatus.OK + events == ["before", "terminal", "after"] + } + + def 'around filter'(boolean legacy) { + given: + def events = [] + def req1 = HttpRequest.GET("/req1") + def req2 = HttpRequest.GET("/req2") + def resp1 = HttpResponse.ok("resp1") + def resp2 = HttpResponse.ok("resp2") + List filters = [ + around(legacy) { request, chain -> + assert request == req1 + events.add("before") + return Flux.from(chain.proceed(req2)).map(resp -> { + assert resp == resp1 + events.add("after") + return resp2 + }) + }, + (GenericHttpFilter.Terminal) (req -> { + assert req == req2 + events.add("terminal") + ExecutionFlow.just(resp1) + }) + ] + + when: + def result = await(filterRunner(filters).run(req1)) + then: + result != null + events == ["before", "terminal", "after"] + + where: + legacy << [true, false] + } + + def 'around filter context propagation'(boolean legacy) { + given: + def events = [] + List filters = [ + around(legacy) { request, chain -> + return Flux.deferContextual { ctx -> + events.add('context 1: ' + ctx.get('value')) + Flux.from(chain.proceed(request)) + .contextWrite { it.put('value', 'around 1') } + } + }, + around(legacy) { request, chain -> + return Flux.deferContextual { ctx -> + events.add('context 2: ' + ctx.get('value')) + Flux.from(chain.proceed(request)) + .contextWrite { it.put('value', 'around 2') } + } + }, + (GenericHttpFilter.TerminalWithReactorContext) ((req, ctx) -> { + events.add('terminal: ' + ctx.get('value')) + ExecutionFlow.just(HttpResponse.ok("resp1")) + }) + ] + + when: + def runner = filterRunner(filters) + runner.reactorContext(Context.of('value', 'outer')) + def result = await(runner.run(HttpRequest.GET("/req1"))) + then: + result != null + events == ["context 1: outer", "context 2: around 1", "terminal: around 2"] + + where: + legacy << [false, true] + } + + def 'exception in before'() { + given: + def events = [] + def testExc = new Exception("Test exception") + List filters = [ + before(ReturnType.of(void)) { throw testExc }, + (GenericHttpFilter.Terminal) (req -> { + events.add("terminal") + ExecutionFlow.just(HttpResponse.ok()) + }) + ] + + when: + await(filterRunner(filters).run(HttpRequest.GET("/"))) + then: + def actual = thrown Exception + actual == testExc + events == [] + } + + def 'exception in after'() { + given: + def events = [] + def testExc = new Exception("Test exception") + List filters = [ + after(ReturnType.of(void)) { req, resp -> throw testExc }, + (GenericHttpFilter.Terminal) (req -> { + events.add("terminal") + ExecutionFlow.just(HttpResponse.ok()) + }) + ] + + when: + await(filterRunner(filters).run(HttpRequest.GET("/"))) + then: + def actual = thrown Exception + actual == testExc + events == ["terminal"] + } + + def 'exception in terminal: direct'() { + given: + def testExc = new RuntimeException("Test exception") + List filters = [ + (GenericHttpFilter.Terminal) (req -> { + throw testExc + }) + ] + + when: + await(filterRunner(filters).run(HttpRequest.GET("/"))) + then: + def actual = thrown Exception + actual == testExc + } + + def 'exception in terminal: flow'() { + given: + def testExc = new Exception("Test exception") + List filters = [ + (GenericHttpFilter.Terminal) (req -> { + return ExecutionFlow.error(testExc) + }) + ] + + when: + await(filterRunner(filters).run(HttpRequest.GET("/"))) + then: + def actual = thrown Exception + actual == testExc + } + + def 'exception in around: before proceed'(boolean legacy) { + given: + def events = [] + def testExc = new RuntimeException("Test exception") + List filters = [ + around(legacy) { request, chain -> + throw testExc + }, + (GenericHttpFilter.Terminal) (req -> { + events.add("terminal") + ExecutionFlow.just(HttpResponse.ok()) + }) + ] + + when: + await(filterRunner(filters).run(HttpRequest.GET("/"))) + then: + def actual = thrown Exception + actual == testExc + events == [] + + where: + legacy << [true, false] + } + + def 'exception in around: in proceed transform'(boolean legacy) { + given: + def events = [] + def testExc = new RuntimeException("Test exception") + List filters = [ + around(legacy) { request, chain -> + return Flux.from(chain.proceed(request)).map(r -> { throw testExc }) + }, + (GenericHttpFilter.Terminal) (req -> { + events.add("terminal") + ExecutionFlow.just(HttpResponse.ok()) + }) + ] + + when: + await(filterRunner(filters).run(HttpRequest.GET("/"))) + then: + def actual = thrown Exception + actual == testExc + events == ["terminal"] + + where: + legacy << [true, false] + } + + def 'exception in around: after proceed, downstream gives normal response'(boolean legacy) { + // don't do this at home + given: + def events = [] + def testExc = new RuntimeException("Test exception") + List filters = [ + around(legacy) { request, chain -> + Flux.from(chain.proceed(request)).subscribe() + throw testExc + }, + (GenericHttpFilter.Terminal) (req -> { + events.add("terminal") + ExecutionFlow.just(HttpResponse.ok()) + }) + ] + + when: + await(filterRunner(filters).run(HttpRequest.GET("/"))) + then: + def actual = thrown Exception + actual == testExc + events == ["terminal"] + + where: + legacy << [true, false] + } + + def 'exception in around: after proceed, downstream gives error'(boolean legacy) { + // don't do this at home + given: + def testExc = new RuntimeException("Test exception") + def terminalFuture = new CompletableFuture() + List filters = [ + around(legacy) { request, chain -> + Flux.from(chain.proceed(request)).subscribe() + throw testExc + }, + (GenericHttpFilter.Terminal) (req -> { + CompletableFutureExecutionFlow.just(terminalFuture) + }) + ] + + when: + def flow = filterRunner(filters).run(HttpRequest.GET("/")) + // after the run() call, we're suspended waiting for the terminal to finish + // this exception is logged and dropped + terminalFuture.completeExceptionally(new RuntimeException("Test exception 2")) + await(flow) + then: + def actual = thrown Exception + actual == testExc + + where: + legacy << [true, false] + } + + def 'around filter does not call proceed'(boolean legacy) { + given: + def events = [] + List filters = [ + around(legacy) { request, chain -> + events.add("around") + Flux.just(HttpResponse.ok("foo")) + }, + (GenericHttpFilter.Terminal) (req -> { + events.add("terminal") + ExecutionFlow.just(HttpResponse.ok()) + }) + ] + + when: + def resp = await(filterRunner(filters).run(HttpRequest.GET("/"))).value + then: + resp.status == HttpStatus.OK + events == ["around"] + + where: + legacy << [true, false] + } + + def 'before returns new request'() { + given: + def events = [] + def req1 = HttpRequest.GET("/req1") + def req2 = HttpRequest.GET("/req2") + List filters = [ + before(ReturnType.of(HttpRequest)) { req -> + assert req == req1 + events.add("before") + req2 + }, + (GenericHttpFilter.Terminal) (req -> { + assert req == req2 + events.add("terminal") + ExecutionFlow.just(HttpResponse.ok()) + }) + ] + + when: + await(filterRunner(filters).run(req1)) + then: + events == ["before", "terminal"] + } + + def 'before returns response'() { + given: + def events = [] + List filters = [ + before(ReturnType.of(HttpResponse)) { + events.add("before") + HttpResponse.ok() + }, + (GenericHttpFilter.Terminal) (req -> { + events.add("terminal") + ExecutionFlow.just(HttpResponse.ok()) + }) + ] + + when: + await(filterRunner(filters).run(HttpRequest.GET("/"))) + then: + events == ["before"] + } + + def 'before returns publisher request'() { + given: + def events = [] + def req1 = HttpRequest.GET("/req1") + def req2 = HttpRequest.GET("/req2") + List filters = [ + before(ReturnType.of(Flux, Argument.of(HttpRequest))) { req -> + assert req == req1 + events.add("before") + Flux.just(req2) + }, + (GenericHttpFilter.Terminal) (req -> { + assert req == req2 + events.add("terminal") + ExecutionFlow.just(HttpResponse.ok()) + }) + ] + + when: + await(filterRunner(filters).run(req1)) + then: + events == ["before", "terminal"] + } + + def 'before returns completablefuture request'() { + given: + def events = [] + def req1 = HttpRequest.GET("/req1") + def req2 = HttpRequest.GET("/req2") + List filters = [ + before(ReturnType.of(CompletableFuture, Argument.of(HttpRequest))) { req -> + assert req == req1 + events.add("before") + CompletableFuture.completedFuture(req2) + }, + (GenericHttpFilter.Terminal) (req -> { + assert req == req2 + events.add("terminal") + ExecutionFlow.just(HttpResponse.ok()) + }) + ] + + when: + await(filterRunner(filters).run(req1)) + then: + events == ["before", "terminal"] + } + + def 'before returns publisher response'() { + given: + def events = [] + List filters = [ + before(ReturnType.of(Flux, Argument.of(HttpResponse))) { + events.add("before") + Flux.just(HttpResponse.ok()) + }, + (GenericHttpFilter.Terminal) (req -> { + events.add("terminal") + ExecutionFlow.just(HttpResponse.ok()) + }) + ] + + when: + await(filterRunner(filters).run(HttpRequest.GET("/"))) + then: + events == ["before"] + } + + def 'after returns new response'() { + given: + def events = [] + def resp1 = HttpResponse.ok("resp1") + def resp2 = HttpResponse.ok("resp2") + List filters = [ + after(ReturnType.of(HttpResponse)) { HttpResponse resp -> + assert resp == resp1 + events.add("after") + resp2 + }, + (GenericHttpFilter.Terminal) (req -> { + events.add("terminal") + ExecutionFlow.just(resp1) + }) + ] + + when: + def resp = await(filterRunner(filters).run(HttpRequest.GET("/"))).value + then: + resp == resp2 + events == ["terminal", "after"] + } + + def 'after returns publisher response'() { + given: + def events = [] + def resp1 = HttpResponse.ok("resp1") + def resp2 = HttpResponse.ok("resp2") + List filters = [ + after(ReturnType.of(Flux, Argument.of(HttpResponse))) { HttpResponse resp -> + assert resp == resp1 + events.add("after") + Flux.just(resp2) + }, + (GenericHttpFilter.Terminal) (req -> { + events.add("terminal") + ExecutionFlow.just(resp1) + }) + ] + + when: + def resp = await(filterRunner(filters).run(HttpRequest.GET("/"))).value + then: + resp == resp2 + events == ["terminal", "after"] + } + + def 'after should not be called if there is an exception but it cannot handle exceptions'() { + given: + def events = [] + def testExc = new Exception("Test exception") + List filters = [ + after(ReturnType.of(void)) { + events.add("after") + null + }, + (GenericHttpFilter.Terminal) (req -> { + events.add("terminal") + ExecutionFlow.error(testExc) + }) + ] + + when: + await(filterRunner(filters).run(HttpRequest.GET("/"))) + then: + def actual = thrown Exception + actual == testExc + events == ["terminal"] + } + + def 'after should be called if there is an exception that it can handle'() { + given: + def events = [] + def testExc = new Exception("Test exception") + def resp1 = HttpResponse.ok("resp1") + List filters = [ + after(ReturnType.of(HttpResponse)) { Exception exc -> + assert exc == testExc + events.add("after") + resp1 + }, + (GenericHttpFilter.Terminal) (req -> { + events.add("terminal") + ExecutionFlow.error(testExc) + }) + ] + + when: + def resp = await(filterRunner(filters).run(HttpRequest.GET("/"))).value + then: + resp == resp1 + events == ["terminal", "after"] + } + + def 'after should not be called if there is an exception it cannot handle'() { + given: + def events = [] + def testExc = new Exception("Test exception") + List filters = [ + after(ReturnType.of(void)) { RuntimeException exc -> + events.add("after") + null + }, + (GenericHttpFilter.Terminal) (req -> { + events.add("terminal") + ExecutionFlow.error(testExc) + }) + ] + + when: + await(filterRunner(filters).run(HttpRequest.GET("/"))) + then: + def actual = thrown Exception + actual == testExc + events == ["terminal"] + } + + def 'async filter'() { + given: + def events = [] + List filters = [ + before(ReturnType.of(void)) { + events.add("before1 " + Thread.currentThread().name) + null + }, + new GenericHttpFilter.Async(before(ReturnType.of(void)) { + events.add("before2 " + Thread.currentThread().name) + null + }, Executors.newCachedThreadPool(new ThreadFactory() { + @Override + Thread newThread(Runnable r) { + return new Thread(r, "thread-before") + } + })), + before(ReturnType.of(void)) { + events.add("before3 " + Thread.currentThread().name) + null + }, + after(ReturnType.of(void)) { + events.add("after1 " + Thread.currentThread().name) + null + }, + new GenericHttpFilter.Async(after(ReturnType.of(void)) { + events.add("after2 " + Thread.currentThread().name) + null + }, Executors.newCachedThreadPool(new ThreadFactory() { + @Override + Thread newThread(Runnable r) { + return new Thread(r, "thread-after") + } + })), + after(ReturnType.of(void)) { + events.add("after3 " + Thread.currentThread().name) + null + }, + (GenericHttpFilter.Terminal) (req -> { + events.add("terminal " + Thread.currentThread().name) + ExecutionFlow.just(HttpResponse.ok()) + }) + ] + + when: + def response = await(ExecutionFlow.async(Executors.newCachedThreadPool(new ThreadFactory() { + @Override + Thread newThread(Runnable r) { + return new Thread(r, "thread-outside") + } + }), () -> filterRunner(filters).run(HttpRequest.GET("/")))).value + then: + response.status() == HttpStatus.OK + events == ["before1 thread-outside", "before2 thread-before", "before3 thread-before", "terminal thread-before", "after3 thread-before", "after2 thread-after", "after1 thread-after"] + } + + @Ignore + def 'around filter with blocking continuation'() { + given: + def events = [] + def req1 = HttpRequest.GET("/req1") + def req2 = HttpRequest.GET("/req2") + def resp1 = HttpResponse.ok("resp1") + def resp2 = HttpResponse.ok("resp2") + List filters = [ + before(ReturnType.of(HttpResponse), [Argument.of(HttpRequest), Argument.of(FilterContinuation, HttpResponse)]) { request, chain -> + assert request == req1 + events.add("before") + def resp = chain.request(req2).proceed() + assert resp == resp1 + events.add("after") + return resp2 + }, + (GenericHttpFilter.Terminal) (req -> { + assert req == req2 + events.add("terminal") + ExecutionFlow.just(resp1) + }) + ] + + when: + def result = await(filterRunner(filters).run(req1)) + then: + result != null + events == ["before", "terminal", "after"] + } + + private def after(ReturnType returnType, List arguments = closure.parameterTypes.collect { Argument.of(it) }, Closure closure) { + return FilterRunner.prepareFilterMethod(ConversionService.SHARED, null, new LambdaExecutable(closure, arguments.toArray(new Argument[0]), returnType), true, new FilterOrder.Fixed(0)) + } + + private def before(ReturnType returnType, List arguments = closure.parameterTypes.collect { Argument.of(it) }, Closure closure) { + return FilterRunner.prepareFilterMethod(ConversionService.SHARED, null, new LambdaExecutable(closure, arguments.toArray(new Argument[0]), returnType), false, new FilterOrder.Fixed(0)) + } + + private def around(boolean legacy, Closure>> closure) { + if (legacy) { + return new GenericHttpFilter.AroundLegacy( + new HttpServerFilter() { + @Override + Publisher> doFilter(HttpRequest request, ServerFilterChain chain) { + return closure(request, chain) + } + }, + new FilterOrder.Fixed(0) + ) + } else { + return before(ReturnType.of(Publisher, Argument.of(HttpResponse)), [Argument.of(HttpRequest), Argument.of(FilterContinuation, Publisher)]) { request, continuation -> + closure(request, new ServerFilterChain() { + @Override + Publisher> proceed(HttpRequest r) { + return continuation.request(r).proceed() + } + }) + } + } + } + + private ImperativeExecutionFlow await(ExecutionFlow flow) { + CompletableFuture future = new CompletableFuture<>() + flow.onComplete((v, e) -> { + if (e == null) { + future.complete(v) + } else { + assert !(e instanceof ExecutionException) + future.completeExceptionally(e) + } + }) + try { + future.get() + } catch (ExecutionException e) { + throw e.cause + } + return CompletableFutureExecutionFlow.just(future).tryComplete() + } + + private static class LambdaExecutable implements ExecutableMethod { + private final Closure closure + private final Argument[] arguments + private final ReturnType returnType + + LambdaExecutable(Closure closure, Argument[] arguments, ReturnType returnType) { + this.closure = closure + this.arguments = arguments + this.returnType = returnType + } + + @Override + Class getDeclaringType() { + return Object + } + + @Override + String getMethodName() { + throw new UnsupportedOperationException() + } + + @Override + Argument[] getArguments() { + return arguments + } + + @Override + Method getTargetMethod() { + throw new UnsupportedOperationException() + } + + @Override + ReturnType getReturnType() { + return returnType + } + + @Override + Object invoke(@Nullable Object instance, Object... arguments) { + return closure.curry(arguments)() + } + } +} diff --git a/http/src/test/groovy/io/micronaut/http/reactive/execution/ReactorExecutionFlowImplSpec.groovy b/http/src/test/groovy/io/micronaut/http/reactive/execution/ReactorExecutionFlowImplSpec.groovy new file mode 100644 index 00000000000..c81b94a5e9e --- /dev/null +++ b/http/src/test/groovy/io/micronaut/http/reactive/execution/ReactorExecutionFlowImplSpec.groovy @@ -0,0 +1,100 @@ +package io.micronaut.http.reactive.execution + +import org.reactivestreams.Publisher +import reactor.core.publisher.Mono +import reactor.core.scheduler.Schedulers +import spock.lang.Specification + +import java.time.Duration + +class ReactorExecutionFlowImplSpec extends Specification { + /* + I tried to improve the heuristic with the following code: + + if (value instanceof Scannable scannable && + scannable.scan(Scannable.Attr.RUN_STYLE) == Scannable.Attr.RunStyle.SYNC && + scannable.parents().allMatch(s -> s.scan(Scannable.Attr.RUN_STYLE) == Scannable.Attr.RunStyle.SYNC)) { + + ImmediateSubscriber immediateSubscriber = new ImmediateSubscriber(); + value.subscribe(immediateSubscriber); + if (!immediateSubscriber.done) { + throw new IllegalStateException("Scan showed the value would be synchronous, but it wasn't?"); + } + return immediateSubscriber.result; + } + + private static class ImmediateSubscriber implements CoreSubscriber { + ImperativeExecutionFlow result; + boolean done = false; + + @Override + public void onSubscribe(Subscription s) { + s.request(Long.MAX_VALUE); + } + + @Override + public void onNext(Object o) { + if (result != null) { + throw new IllegalStateException("Duplicate result"); + } + result = (ImperativeExecutionFlow) ExecutionFlow.just(o); + } + + @Override + public void onError(Throwable t) { + result = (ImperativeExecutionFlow) ExecutionFlow.error(t); + done = true; + } + + @Override + public void onComplete() { + if (result == null) { + result = (ImperativeExecutionFlow) ExecutionFlow.error(new NoSuchElementException("Mono was empty")); + } + done = true; + } + } + + However it turns out that some operators (e.g. the delaySubscription below) show up as "SYNC" even though they don't + yield an immediate result. + + There is also another api, OptimizableOperator, which could be used for this. However it is private, and it would + not give certainty whether an immediate execution is possible before actually subscribing, which is necessary for + the ExecutionFlow api (only one subscription allowed). + */ + + def 'test immediate'(Publisher publisher) { + given: + def flow = ReactiveExecutionFlow.fromPublisher(publisher) + + when: + def done = flow.tryComplete() + then: + done != null + done.value == 'foo' + + where: + publisher << [ + Mono.just("foo"), + // not supported by our current algorithm + // Mono.just("f").map { it + "oo" } + ] + } + + def 'test not immediate'(Publisher publisher) { + given: + def flow = ReactiveExecutionFlow.fromPublisher(publisher) + + when: + def done = flow.tryComplete() + then: + done == null + + where: + publisher << [ + Mono.just("foo").delayElement(Duration.ofSeconds(1)), + Mono.just("foo").delaySubscription(Duration.ofSeconds(1)), + Mono.just("foo").subscribeOn(Schedulers.immediate()), + ] + } +} diff --git a/router/src/main/java/io/micronaut/web/router/AnnotatedFilterRouteBuilder.java b/router/src/main/java/io/micronaut/web/router/AnnotatedFilterRouteBuilder.java index 70a83c13428..81b224a4a81 100644 --- a/router/src/main/java/io/micronaut/web/router/AnnotatedFilterRouteBuilder.java +++ b/router/src/main/java/io/micronaut/web/router/AnnotatedFilterRouteBuilder.java @@ -15,10 +15,10 @@ */ package io.micronaut.web.router; -import io.micronaut.core.annotation.Nullable; import io.micronaut.context.BeanContext; import io.micronaut.context.ExecutionHandleLocator; import io.micronaut.context.processor.BeanDefinitionProcessor; +import io.micronaut.core.annotation.Nullable; import io.micronaut.core.convert.ConversionService; import io.micronaut.core.util.ArrayUtils; import io.micronaut.core.util.StringUtils; @@ -46,7 +46,6 @@ public class AnnotatedFilterRouteBuilder extends DefaultRouteBuilder implements /** * Constructor. * - * @param beanContext The bean context * @param executionHandleLocator The execution handler locator * @param uriNamingStrategy The URI naming strategy * @param conversionService The conversion service @@ -54,7 +53,6 @@ public class AnnotatedFilterRouteBuilder extends DefaultRouteBuilder implements */ @Inject public AnnotatedFilterRouteBuilder( - BeanContext beanContext, ExecutionHandleLocator executionHandleLocator, UriNamingStrategy uriNamingStrategy, ConversionService conversionService, @@ -95,7 +93,7 @@ public void process(BeanDefinition beanDefinition, BeanContext beanContext) { * @return The array of patterns that should match request URLs for the bean to * be invoked. */ - protected String[] getPatterns(BeanDefinition beanDefinition) { + private String[] getPatterns(BeanDefinition beanDefinition) { String[] values = beanDefinition.stringValues(Filter.class); String contextPath = contextPathProvider != null ? contextPathProvider.getContextPath() : null; if (contextPath != null) { diff --git a/router/src/main/java/io/micronaut/web/router/BeanDefinitionFilterRoute.java b/router/src/main/java/io/micronaut/web/router/BeanDefinitionFilterRoute.java index b70e1d4c6f6..19ef5e2af19 100644 --- a/router/src/main/java/io/micronaut/web/router/BeanDefinitionFilterRoute.java +++ b/router/src/main/java/io/micronaut/web/router/BeanDefinitionFilterRoute.java @@ -15,10 +15,13 @@ */ package io.micronaut.web.router; -import io.micronaut.core.annotation.NonNull; import io.micronaut.context.BeanLocator; import io.micronaut.core.annotation.AnnotationMetadata; import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.NonNull; +import io.micronaut.core.order.OrderUtil; +import io.micronaut.http.filter.FilterOrder; +import io.micronaut.http.filter.GenericHttpFilter; import io.micronaut.http.filter.HttpFilter; import io.micronaut.inject.BeanDefinition; @@ -40,7 +43,9 @@ class BeanDefinitionFilterRoute extends DefaultFilterRoute { * @param definition The definition */ BeanDefinitionFilterRoute(String pattern, BeanLocator beanLocator, BeanDefinition definition) { - super(pattern, () -> beanLocator.getBean(definition)); + super(pattern, () -> new GenericHttpFilter.AroundLegacy( + beanLocator.getBean(definition), + new FilterOrder.Dynamic(OrderUtil.getOrder(definition)))); this.definition = definition; } diff --git a/router/src/main/java/io/micronaut/web/router/DefaultFilterRoute.java b/router/src/main/java/io/micronaut/web/router/DefaultFilterRoute.java index 552e1989ef7..347ec615cc5 100644 --- a/router/src/main/java/io/micronaut/web/router/DefaultFilterRoute.java +++ b/router/src/main/java/io/micronaut/web/router/DefaultFilterRoute.java @@ -15,13 +15,15 @@ */ package io.micronaut.web.router; -import io.micronaut.core.annotation.NonNull; import io.micronaut.core.annotation.AnnotationMetadata; import io.micronaut.core.annotation.AnnotationMetadataResolver; -import io.micronaut.core.util.*; +import io.micronaut.core.annotation.NonNull; +import io.micronaut.core.util.ArrayUtils; +import io.micronaut.core.util.PathMatcher; +import io.micronaut.core.util.StringUtils; import io.micronaut.http.HttpMethod; import io.micronaut.http.filter.FilterPatternStyle; -import io.micronaut.http.filter.HttpFilter; +import io.micronaut.http.filter.GenericHttpFilter; import java.net.URI; import java.util.ArrayList; @@ -42,31 +44,35 @@ class DefaultFilterRoute implements FilterRoute { private final List patterns = new ArrayList<>(1); - private final Supplier filterSupplier; + private final Supplier filterSupplier; private final AnnotationMetadataResolver annotationMetadataResolver; private Set httpMethods; private FilterPatternStyle patternStyle; - private HttpFilter filter; + private volatile GenericHttpFilter filter; private AnnotationMetadata annotationMetadata; + DefaultFilterRoute(Supplier filter, AnnotationMetadataResolver annotationMetadataResolver) { + Objects.requireNonNull(filter, "HttpFilter argument is required"); + this.filterSupplier = filter; + this.annotationMetadataResolver = annotationMetadataResolver; + } + /** * @param pattern A pattern * @param filter A {@link Supplier} for an HTTP filter * @param annotationMetadataResolver The annotation metadata resolver */ - DefaultFilterRoute(String pattern, Supplier filter, AnnotationMetadataResolver annotationMetadataResolver) { + DefaultFilterRoute(String pattern, Supplier filter, AnnotationMetadataResolver annotationMetadataResolver) { + this(filter, annotationMetadataResolver); Objects.requireNonNull(pattern, "Pattern argument is required"); - Objects.requireNonNull(pattern, "HttpFilter argument is required"); - this.filterSupplier = filter; this.patterns.add(pattern); - this.annotationMetadataResolver = annotationMetadataResolver; } /** * @param pattern A pattern * @param filter A {@link Supplier} for an HTTP filter */ - DefaultFilterRoute(String pattern, Supplier filter) { + DefaultFilterRoute(String pattern, Supplier filter) { this(pattern, filter, AnnotationMetadataResolver.DEFAULT); } @@ -87,8 +93,8 @@ public AnnotationMetadata getAnnotationMetadata() { } @Override - public HttpFilter getFilter() { - HttpFilter filter = this.filter; + public GenericHttpFilter getFilter() { + GenericHttpFilter filter = this.filter; if (filter == null) { synchronized (this) { // double check filter = this.filter; @@ -119,7 +125,7 @@ public FilterPatternStyle getPatternStyle() { } @Override - public Optional match(HttpMethod method, URI uri) { + public Optional match(HttpMethod method, URI uri) { if (httpMethods != null && !httpMethods.contains(method)) { return Optional.empty(); } @@ -127,8 +133,8 @@ public Optional match(HttpMethod method, URI uri) { PathMatcher matcher = getPatternStyle().getPathMatcher(); for (String pattern : patterns) { if (matcher.matches(pattern, uriStr)) { - HttpFilter filter = getFilter(); - if (filter instanceof Toggleable && !((Toggleable) filter).isEnabled()) { + GenericHttpFilter filter = getFilter(); + if (filter instanceof GenericHttpFilter.AroundLegacy al && !al.isEnabled()) { return Optional.empty(); } return Optional.of(filter); diff --git a/router/src/main/java/io/micronaut/web/router/DefaultRouteBuilder.java b/router/src/main/java/io/micronaut/web/router/DefaultRouteBuilder.java index c7c23d6ddbb..8e397aad297 100644 --- a/router/src/main/java/io/micronaut/web/router/DefaultRouteBuilder.java +++ b/router/src/main/java/io/micronaut/web/router/DefaultRouteBuilder.java @@ -15,14 +15,14 @@ */ package io.micronaut.web.router; -import io.micronaut.core.annotation.NonNull; -import io.micronaut.core.annotation.Nullable; import io.micronaut.context.ApplicationContext; import io.micronaut.context.BeanLocator; import io.micronaut.context.ExecutionHandleLocator; import io.micronaut.context.env.Environment; import io.micronaut.core.annotation.AnnotationMetadata; import io.micronaut.core.annotation.AnnotationMetadataResolver; +import io.micronaut.core.annotation.NonNull; +import io.micronaut.core.annotation.Nullable; import io.micronaut.core.bind.annotation.Bindable; import io.micronaut.core.convert.ConversionService; import io.micronaut.core.type.Argument; @@ -35,6 +35,7 @@ import io.micronaut.http.MediaType; import io.micronaut.http.annotation.Body; import io.micronaut.http.annotation.Status; +import io.micronaut.http.filter.GenericHttpFilter; import io.micronaut.http.filter.HttpFilter; import io.micronaut.http.uri.UriMatchInfo; import io.micronaut.http.uri.UriMatchTemplate; @@ -47,7 +48,16 @@ import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; -import java.util.*; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; import java.util.function.Predicate; import java.util.function.Supplier; @@ -123,17 +133,6 @@ public List getFilterRoutes() { return filterRoutes; } - @Override - public FilterRoute addFilter(String pathPattern, Supplier filter) { - DefaultFilterRoute route = new DefaultFilterRoute( - pathPattern, - filter, - (AnnotationMetadataResolver) executionHandleLocator - ); - filterRoutes.add(route); - return route; - } - @Override public FilterRoute addFilter(String pathPattern, BeanLocator beanLocator, BeanDefinition beanDefinition) { DefaultFilterRoute route = new BeanDefinitionFilterRoute( @@ -145,6 +144,17 @@ public FilterRoute addFilter(String pathPattern, BeanLocator beanLocator, BeanDe return route; } + final FilterRoute addFilter(Supplier internalFilter, AnnotationMetadata annotationMetadata) { + FilterRoute fr = new DefaultFilterRoute(internalFilter, AnnotationMetadataResolver.DEFAULT) { + @Override + public AnnotationMetadata getAnnotationMetadata() { + return annotationMetadata; + } + }; + filterRoutes.add(fr); + return fr; + } + @Override public List getStatusRoutes() { return Collections.unmodifiableList(statusRoutes); diff --git a/router/src/main/java/io/micronaut/web/router/DefaultRouter.java b/router/src/main/java/io/micronaut/web/router/DefaultRouter.java index a012d9b694c..a06b41f0d85 100644 --- a/router/src/main/java/io/micronaut/web/router/DefaultRouter.java +++ b/router/src/main/java/io/micronaut/web/router/DefaultRouter.java @@ -30,7 +30,8 @@ import io.micronaut.http.annotation.Filter; import io.micronaut.http.annotation.FilterMatcher; import io.micronaut.http.filter.FilterPatternStyle; -import io.micronaut.http.filter.HttpFilter; +import io.micronaut.http.filter.FilterRunner; +import io.micronaut.http.filter.GenericHttpFilter; import io.micronaut.http.filter.HttpServerFilterResolver; import io.micronaut.http.uri.UriMatchTemplate; import io.micronaut.web.router.exceptions.RoutingException; @@ -70,15 +71,15 @@ public class DefaultRouter implements Router, HttpServerFilterResolver exposedPorts; private final List alwaysMatchesFilterRoutes = new ArrayList<>(); private final List preconditionFilterRoutes = new ArrayList<>(); - private final Supplier> alwaysMatchesHttpFilters = SupplierUtil.memoized(() -> { + private final Supplier> alwaysMatchesHttpFilters = SupplierUtil.memoized(() -> { if (alwaysMatchesFilterRoutes.isEmpty()) { return Collections.emptyList(); } - List httpFilters = new ArrayList<>(alwaysMatchesFilterRoutes.size()); + List httpFilters = new ArrayList<>(alwaysMatchesFilterRoutes.size()); for (FilterRoute filterRoute : alwaysMatchesFilterRoutes) { httpFilters.add(filterRoute.getFilter()); } - httpFilters.sort(OrderUtil.COMPARATOR); + FilterRunner.sort(httpFilters); return httpFilters; }); @@ -446,11 +447,11 @@ public Optional> route(@NonNull Throwable error) { @NonNull @Override - public List findFilters(@NonNull HttpRequest request) { + public List findFilters(@NonNull HttpRequest request) { if (preconditionFilterRoutes.isEmpty()) { return alwaysMatchesHttpFilters.get(); } - List httpFilters = new ArrayList<>(alwaysMatchesFilterRoutes.size() + preconditionFilterRoutes.size()); + List httpFilters = new ArrayList<>(alwaysMatchesFilterRoutes.size() + preconditionFilterRoutes.size()); httpFilters.addAll(alwaysMatchesHttpFilters.get()); RouteMatch routeMatch = (RouteMatch) request.getAttribute(HttpAttributes.ROUTE_MATCH).filter(o -> o instanceof RouteMatch).orElse(null); HttpMethod method = request.getMethod(); @@ -463,7 +464,7 @@ public List findFilters(@NonNull HttpRequest request) { } filterRoute.match(method, uri).ifPresent(httpFilters::add); } - httpFilters.sort(OrderUtil.COMPARATOR); + FilterRunner.sort(httpFilters); return Collections.unmodifiableList(httpFilters); } @@ -541,11 +542,11 @@ private Optional> findRouteMatch(Map } @Override - public List> resolveFilterEntries(RouteMatch routeMatch) { + public List resolveFilterEntries(RouteMatch routeMatch) { if (preconditionFilterRoutes.isEmpty()) { - return (List) alwaysMatchesFilterRoutes; + return new ArrayList<>(alwaysMatchesFilterRoutes); } - List> filterEntries = new ArrayList<>(alwaysMatchesFilterRoutes.size() + preconditionFilterRoutes.size()); + List filterEntries = new ArrayList<>(alwaysMatchesFilterRoutes.size() + preconditionFilterRoutes.size()); filterEntries.addAll(alwaysMatchesFilterRoutes); for (FilterRoute filterRoute : preconditionFilterRoutes) { if (!matchesFilterMatcher(filterRoute, routeMatch)) { @@ -557,9 +558,9 @@ public List> resolveFilterEntries(RouteMatch routeMat } @Override - public List resolveFilters(HttpRequest request, List> filterEntries) { - List httpFilters = new ArrayList<>(filterEntries.size()); - for (FilterEntry entry : filterEntries) { + public List resolveFilters(HttpRequest request, List filterEntries) { + List httpFilters = new ArrayList<>(filterEntries.size()); + for (FilterEntry entry : filterEntries) { if (entry.hasMethods() && !entry.getFilterMethods().contains(request.getMethod())) { continue; } diff --git a/router/src/main/java/io/micronaut/web/router/FilterRoute.java b/router/src/main/java/io/micronaut/web/router/FilterRoute.java index 01363319830..bd9781ec57d 100644 --- a/router/src/main/java/io/micronaut/web/router/FilterRoute.java +++ b/router/src/main/java/io/micronaut/web/router/FilterRoute.java @@ -17,6 +17,7 @@ import io.micronaut.http.HttpMethod; import io.micronaut.http.filter.FilterPatternStyle; +import io.micronaut.http.filter.GenericHttpFilter; import io.micronaut.http.filter.HttpFilter; import io.micronaut.http.filter.HttpFilterResolver; @@ -29,13 +30,13 @@ * @author Graeme Rocher * @since 1.0 */ -public interface FilterRoute extends HttpFilterResolver.FilterEntry { +public interface FilterRoute extends HttpFilterResolver.FilterEntry { /** * @return The filter for this {@link FilterRoute} */ @Override - HttpFilter getFilter(); + GenericHttpFilter getFilter(); /** * Matches the given path to this filter route. @@ -44,7 +45,7 @@ public interface FilterRoute extends HttpFilterResolver.FilterEntry * @param uri The URI * @return An {@link Optional} of {@link HttpFilter} */ - Optional match(HttpMethod method, URI uri); + Optional match(HttpMethod method, URI uri); /** * Add an addition pattern to this filter route. diff --git a/router/src/main/java/io/micronaut/web/router/RouteBuilder.java b/router/src/main/java/io/micronaut/web/router/RouteBuilder.java index fa65f63fc7c..a139016ac9a 100644 --- a/router/src/main/java/io/micronaut/web/router/RouteBuilder.java +++ b/router/src/main/java/io/micronaut/web/router/RouteBuilder.java @@ -17,6 +17,8 @@ import io.micronaut.context.BeanLocator; import io.micronaut.core.annotation.Indexed; +import io.micronaut.core.annotation.NonNull; +import io.micronaut.core.annotation.Nullable; import io.micronaut.core.naming.NameUtils; import io.micronaut.core.naming.conventions.MethodConvention; import io.micronaut.core.naming.conventions.PropertyConvention; @@ -31,11 +33,8 @@ import io.micronaut.inject.ExecutableMethod; import io.micronaut.inject.ProxyBeanDefinition; -import io.micronaut.core.annotation.NonNull; -import io.micronaut.core.annotation.Nullable; import java.util.List; import java.util.Set; -import java.util.function.Supplier; /** *

An interface for classes capable of building HTTP routing information.

@@ -82,15 +81,6 @@ public interface RouteBuilder { */ UriNamingStrategy getUriNamingStrategy(); - /** - * Add a filter. - * - * @param pathPattern The path pattern for the filter - * @param filter The filter itself - * @return The {@link FilterRoute} - */ - FilterRoute addFilter(String pathPattern, Supplier filter); - /** * Add a filter. * diff --git a/router/src/main/java/io/micronaut/web/router/Router.java b/router/src/main/java/io/micronaut/web/router/Router.java index 111f28c2788..1a5b7eb444d 100644 --- a/router/src/main/java/io/micronaut/web/router/Router.java +++ b/router/src/main/java/io/micronaut/web/router/Router.java @@ -15,13 +15,13 @@ */ package io.micronaut.web.router; +import io.micronaut.core.annotation.NonNull; +import io.micronaut.core.annotation.Nullable; import io.micronaut.http.HttpMethod; import io.micronaut.http.HttpRequest; import io.micronaut.http.HttpStatus; -import io.micronaut.http.filter.HttpFilter; +import io.micronaut.http.filter.GenericHttpFilter; -import io.micronaut.core.annotation.NonNull; -import io.micronaut.core.annotation.Nullable; import java.net.URI; import java.util.List; import java.util.Optional; @@ -239,7 +239,7 @@ Optional> findStatusRoute( * @param request The request * @return A new filtered publisher */ - @NonNull List findFilters( + @NonNull List findFilters( @NonNull HttpRequest request ); diff --git a/router/src/main/java/io/micronaut/web/router/ServerFilterRouteBuilder.java b/router/src/main/java/io/micronaut/web/router/ServerFilterRouteBuilder.java new file mode 100644 index 00000000000..fc8fe37ad4c --- /dev/null +++ b/router/src/main/java/io/micronaut/web/router/ServerFilterRouteBuilder.java @@ -0,0 +1,111 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.web.router; + +import io.micronaut.context.BeanContext; +import io.micronaut.context.ExecutionHandleLocator; +import io.micronaut.context.processor.ExecutableMethodProcessor; +import io.micronaut.core.annotation.AnnotationMetadata; +import io.micronaut.core.annotation.Experimental; +import io.micronaut.core.annotation.Nullable; +import io.micronaut.core.convert.ConversionService; +import io.micronaut.core.util.StringUtils; +import io.micronaut.http.HttpMethod; +import io.micronaut.http.annotation.ServerFilter; +import io.micronaut.http.context.ServerContextPathProvider; +import io.micronaut.http.filter.BaseFilterProcessor; +import io.micronaut.http.filter.GenericHttpFilter; +import io.micronaut.inject.BeanDefinition; +import io.micronaut.inject.ExecutableMethod; +import jakarta.inject.Singleton; + +import java.util.List; +import java.util.function.Supplier; + +/** + * {@link RouteBuilder} for {@link ServerFilter}s. + * + * @since 4.0.0 + * @author Jonas Konrad + */ +@Singleton +@Experimental +public class ServerFilterRouteBuilder extends DefaultRouteBuilder implements ExecutableMethodProcessor { + private final BaseFilterProcessor delegate; + + /** + * @param executionHandleLocator The execution handler locator + * @param uriNamingStrategy The URI naming strategy + * @param conversionService The conversion service + * @param beanContext The bean context + * @param contextPathProvider The server context path provider + */ + public ServerFilterRouteBuilder( + ExecutionHandleLocator executionHandleLocator, + UriNamingStrategy uriNamingStrategy, + ConversionService conversionService, + BeanContext beanContext, + @Nullable ServerContextPathProvider contextPathProvider + ) { + super(executionHandleLocator, uriNamingStrategy, conversionService); + delegate = new BaseFilterProcessor<>(beanContext, ServerFilter.class) { + @Nullable + @Override + protected List prependContextPath(List patterns) { + String contextPath = contextPathProvider != null ? contextPathProvider.getContextPath() : null; + if (contextPath != null && patterns != null) { + patterns = patterns.stream() + .map(pattern -> { + if (!pattern.startsWith(contextPath)) { + String newValue = StringUtils.prependUri(contextPath, pattern); + if (newValue.charAt(0) != '/') { + newValue = "/" + newValue; + } + return newValue; + } else { + return pattern; + } + }) + .toList(); + } + return patterns; + } + + @Override + protected void addFilter(Supplier factory, AnnotationMetadata methodAnnotations, FilterMetadata metadata) { + applyMetadata(ServerFilterRouteBuilder.this.addFilter(factory, methodAnnotations), metadata); + } + + private void applyMetadata(FilterRoute route, FilterMetadata metadata) { + route.patternStyle(metadata.patternStyle()); + if (metadata.patterns() == null || metadata.patterns().isEmpty()) { + throw new IllegalArgumentException("A filter pattern is required"); + } + for (String pattern : metadata.patterns()) { + route.pattern(pattern); + } + if (metadata.methods() != null) { + route.methods(metadata.methods().toArray(new HttpMethod[0])); + } + } + }; + } + + @Override + public void process(BeanDefinition beanDefinition, ExecutableMethod method) { + delegate.process(beanDefinition, method); + } +} diff --git a/router/src/main/java/io/micronaut/web/router/filter/FilteredRouter.java b/router/src/main/java/io/micronaut/web/router/filter/FilteredRouter.java index 9745f363999..c44d9bb6dff 100644 --- a/router/src/main/java/io/micronaut/web/router/filter/FilteredRouter.java +++ b/router/src/main/java/io/micronaut/web/router/filter/FilteredRouter.java @@ -15,17 +15,17 @@ */ package io.micronaut.web.router.filter; +import io.micronaut.core.annotation.NonNull; +import io.micronaut.core.annotation.Nullable; import io.micronaut.http.HttpMethod; import io.micronaut.http.HttpRequest; import io.micronaut.http.HttpStatus; -import io.micronaut.http.filter.HttpFilter; +import io.micronaut.http.filter.GenericHttpFilter; import io.micronaut.web.router.RouteMatch; import io.micronaut.web.router.Router; import io.micronaut.web.router.UriRoute; import io.micronaut.web.router.UriRouteMatch; -import io.micronaut.core.annotation.NonNull; -import io.micronaut.core.annotation.Nullable; import java.util.List; import java.util.Optional; import java.util.Set; @@ -159,7 +159,7 @@ public Optional> findStatusRoute(@NonNull HttpStatus status, H @NonNull @Override - public List findFilters(@NonNull HttpRequest request) { + public List findFilters(@NonNull HttpRequest request) { return router.findFilters(request); } diff --git a/router/src/test/groovy/io/micronaut/web/router/DefaultFilterRouteSpec.groovy b/router/src/test/groovy/io/micronaut/web/router/DefaultFilterRouteSpec.groovy index 2fd8fa3768c..2bd7a4260af 100644 --- a/router/src/test/groovy/io/micronaut/web/router/DefaultFilterRouteSpec.groovy +++ b/router/src/test/groovy/io/micronaut/web/router/DefaultFilterRouteSpec.groovy @@ -19,7 +19,9 @@ import io.micronaut.http.HttpMethod import io.micronaut.http.HttpRequest import io.micronaut.http.HttpResponse import io.micronaut.http.filter.FilterChain +import io.micronaut.http.filter.FilterOrder import io.micronaut.http.filter.FilterPatternStyle +import io.micronaut.http.filter.GenericHttpFilter import io.micronaut.http.filter.HttpFilter import org.reactivestreams.Publisher import spock.lang.Specification @@ -30,17 +32,17 @@ class DefaultFilterRouteSpec extends Specification { void "test filter route matching with no methods specified"() { given: - def filter = new HttpFilter() { + def filter = new GenericHttpFilter.AroundLegacy(new HttpFilter() { @Override Publisher> doFilter(HttpRequest request, FilterChain chain) { return null } - } + }, new FilterOrder.Fixed(0)) when: - def route = new DefaultFilterRoute("/foo", new Supplier() { + def route = new DefaultFilterRoute("/foo", new Supplier() { @Override - HttpFilter get() { + GenericHttpFilter get() { return filter } }) @@ -53,17 +55,17 @@ class DefaultFilterRouteSpec extends Specification { void "test filter route matching with methods specified"() { given: - def filter = new HttpFilter() { + def filter = new GenericHttpFilter.AroundLegacy(new HttpFilter() { @Override Publisher> doFilter(HttpRequest request, FilterChain chain) { return null } - } + }, new FilterOrder.Fixed(0)) when: - def route = new DefaultFilterRoute("/foo", new Supplier() { + def route = new DefaultFilterRoute("/foo", new Supplier() { @Override - HttpFilter get() { + GenericHttpFilter get() { return filter } }).methods(HttpMethod.POST, HttpMethod.PUT) @@ -76,17 +78,17 @@ class DefaultFilterRouteSpec extends Specification { void "test filter route matching with regex pattern style specified"() { given: - def filter = new HttpFilter() { + def filter = new GenericHttpFilter.AroundLegacy(new HttpFilter() { @Override Publisher> doFilter(HttpRequest request, FilterChain chain) { return null } - } + }, new FilterOrder.Fixed(0)) when: - def route = new DefaultFilterRoute('/fo(a|o)$', new Supplier() { + def route = new DefaultFilterRoute('/fo(a|o)$', new Supplier() { @Override - HttpFilter get() { + GenericHttpFilter get() { return filter } }).patternStyle(FilterPatternStyle.REGEX) diff --git a/src/main/docs/guide/httpClient/clientFilter.adoc b/src/main/docs/guide/httpClient/clientFilter.adoc index aa90f4d99f0..9506f88fd66 100644 --- a/src/main/docs/guide/httpClient/clientFilter.adoc +++ b/src/main/docs/guide/httpClient/clientFilter.adoc @@ -1,6 +1,6 @@ Often, you need to include the same HTTP headers or URL parameters in a set of requests against a third-party API or when calling another Microservice. -To simplify this, you can define api:http.filter.HttpClientFilter[] classes that are applied to all matching HTTP client requests. +To simplify this, you can define api:http.annotation.ClientFilter[] classes that are applied to all matching HTTP client requests. The details of how filters worked are described in the <>. Here we will only show some client filters. As an example, say you want to build a client to communicate with the https://bintray.com/docs/api/[Bintray REST API]. It would be tedious to specify authentication for every single HTTP call. @@ -22,11 +22,11 @@ snippet::io.micronaut.docs.client.ThirdPartyClientFilterSpec[tags="bintrayFilter Now when you invoke the `bintrayService.fetchRepositories()` method, the `Authorization` HTTP header is included in the request. -=== Injecting Another Client into a HttpClientFilter +=== Injecting Another Client into a ClientFilter -To create an link:{micronautreactorapi}/io/micronaut/reactor/http/client/ReactorHttpClient.html[ReactorHttpClient], Micronaut needs to resolve all `HttpClientFilter` instances, which creates a circular dependency when injecting another link:{micronautreactorapi}/io/micronaut/reactor/http/client/ReactorHttpClient.html[ReactorHttpClient] or a `@Client` bean into an instance of a `HttpClientFilter`. +To create an link:{micronautreactorapi}/io/micronaut/reactor/http/client/ReactorHttpClient.html[ReactorHttpClient], Micronaut needs to resolve all `ClientFilter` beans, which creates a circular dependency when injecting another link:{micronautreactorapi}/io/micronaut/reactor/http/client/ReactorHttpClient.html[ReactorHttpClient] or a `@Client` bean into an instance of a `ClientFilter`. -To resolve this issue, use the api:context.BeanProvider[] interface to inject another link:{micronautreactorapi}/io/micronaut/reactor/http/client/ReactorHttpClient.html[ReactorHttpClient] or a `@Client` bean into an instance of `HttpClientFilter`. +To resolve this issue, use the api:context.BeanProvider[] interface to inject another link:{micronautreactorapi}/io/micronaut/reactor/http/client/ReactorHttpClient.html[ReactorHttpClient] or a `@Client` bean into an instance of `ClientFilter`. The following example which implements a filter allowing https://cloud.google.com/run/docs/authenticating/service-to-service[authentication between services on Google Cloud Run] demonstrates how to use api:context.BeanProvider[] to inject another client: diff --git a/src/main/docs/guide/httpServer/filters.adoc b/src/main/docs/guide/httpServer/filters.adoc index 77daa2b85c0..e21f056a12d 100644 --- a/src/main/docs/guide/httpServer/filters.adoc +++ b/src/main/docs/guide/httpServer/filters.adoc @@ -6,81 +6,9 @@ Filters support the following use cases: * Modification of the outgoing api:http.HttpResponse[] * Implementation of cross-cutting concerns such as security, tracing, etc. -For a server application, the api:http.filter.HttpServerFilter[] interface `doFilter` method can be implemented. +There are two ways to implement a filter: -The `doFilter` method accepts the api:http.HttpRequest[] and an instance of api:http.filter.ServerFilterChain[]. +- <>. +- By using <>. -The `ServerFilterChain` interface contains a resolved chain of filters where the final entry in the chain is the matched route. The api:http.filter.ServerFilterChain.proceed(io.micronaut.http.HttpRequest)[] method resumes processing of the request. - -The `proceed(..)` method returns a Reactive Streams rs:Publisher[] that emits the response to be returned to the client. Implementors of filters can subscribe to the rs:Publisher[] and mutate the emitted api:http.MutableHttpResponse[] to modify the response prior to returning the response to the client. - -To put these concepts into practice lets look at an example. - -IMPORTANT: Filters execute in the event loop, so blocking operations must be offloaded to another thread pool. - -== Writing a Filter - -Suppose you wish to trace each request to the Micronaut "Hello World" example using some external system. This system could be a database or a distributed tracing service, and may require I/O operations. - -You should not block the underlying Netty event loop in your filter; instead the filter should proceed with execution once any I/O is complete. - -As an example, consider this `TraceService` that uses https://projectreactor.io[Project Reactor] to compose an I/O operation: - -snippet::io.micronaut.docs.server.filters.TraceService[tags="imports,class", indent=0, title="A TraceService Example using Reactive Streams"] - -<1> The reactor:Mono[] type creates logic that executes potentially blocking operations to write the trace data from the request -<2> Since this is just an example, the logic does nothing yet -<3> The `Schedulers.boundedElastic` executes the logic - -You can then inject this implementation into your filter definition: - -snippet::io.micronaut.docs.server.filters.TraceFilter[tags="imports,class,endclass", indent=0, title="An Example HttpServerFilter"] - -<1> The api:http.annotation.Filter[] annotation defines the URI pattern(s) the filter matches -<2> The class implements the api:http.filter.HttpServerFilter[] interface -<3> The previously defined `TraceService` is injected via constructor - -The final step is to write the `doFilter` implementation of the api:http.filter.HttpServerFilter[] interface. - -snippet::io.micronaut.docs.server.filters.TraceFilter[tags="doFilter", indent=0, title="The doFilter implementation"] - -<1> `TraceService` is invoked to trace the request -<2> If the call succeeds, the filter resumes request processing using https://projectreactor.io[Project Reactor]'s `switchMap` method, which invokes the `proceed` method of the api:http.filter.ServerFilterChain[] -<3> Finally, the https://projectreactor.io[Project Reactor]'s `doOnNext` method adds a `X-Trace-Enabled` header to the response. - -The previous example demonstrates some key concepts such as executing logic in a non-blocking manner before proceeding with the request and modifying the outgoing response. - -TIP: The examples use https://projectreactor.io[Project Reactor], however you can use any reactive framework that supports the Reactive streams specifications - -The api:http.annotation.Filter[] can use different styles of pattern for path matching by setting `patternStyle`. By default, it uses api:core.util.AntPathMatcher[] for path matching. When using Ant, the mapping matches URLs using the following rules: - -* ? matches one character -* * matches zero or more characters -* ** matches zero or more subdirectories in a path - -.@Filter Annotation Path Matching Examples -|=== -|Pattern|Example Matched Paths - -|`/**` -|any path - -|`customer/j?y` -|customer/joy, customer/jay - -|`customer/*/id` -|customer/adam/id, com/amy/id - -|`customer/**` -|customer/adam, customer/adam/id, customer/adam/name - -|`customer/**/*.html` -|customer/index.html, customer/adam/profile.html, customer/adam/job/description.html - -The other option is regular expression based matching. To use regular expressions, set `patternStyle = FilterPatternStyle.REGEX`. The `pattern` attribute is expected to contain a regular expression which will be expected to match the provided URLs exactly (using link:{jdkapi}/java/util/regex/Matcher.html#matches--[Matcher#matches]). - -NOTE: Using `FilterPatternStyle.ANT` is preferred as the pattern matching is more performant than using regular expressions. `FilterPatternStyle.REGEX` should be used when your pattern cannot be written properly using Ant. - -== Error States - -The publisher returned from `chain.proceed` should never emit an error. In the cases where an upstream filter emitted an error or the route itself threw an exception, the error response should be emitted instead of the exception. In some cases it may be desirable to know the cause of the error response and for this purpose an attribute exists on the response if it was created as a result of an exception being emitted or thrown. The original cause is stored as the attribute api:http.HttpAttributes#EXCEPTION[]. +NOTE: We recommend Micronaut developers use <> introduced in Micronaut Framework 4.0 to implement filters. diff --git a/src/main/docs/guide/httpServer/filters/filterPatterns.adoc b/src/main/docs/guide/httpServer/filters/filterPatterns.adoc new file mode 100644 index 00000000000..d27b1f887e8 --- /dev/null +++ b/src/main/docs/guide/httpServer/filters/filterPatterns.adoc @@ -0,0 +1,31 @@ +Filter patterns can be defined on the filter class (in api:http.annotation.Filter[], the api:http.annotation.ServerFilter[] or api:http.annotation.ClientFilter[] annotation), or on the filter method (in the api:http.annotation.RequestFilter[] or api:http.annotation.ResponseFilter[] annotation). + +You can use different styles of pattern for path matching by setting `patternStyle`. By default, api:core.util.AntPathMatcher[] is used for path matching. When using Ant, the mapping matches URLs using the following rules: + +* ? matches one character +* * matches zero or more characters +* ** matches zero or more subdirectories in a path + +.@Filter Annotation Path Matching Examples +|=== +|Pattern|Example Matched Paths + +|`/**` +|any path + +|`customer/j?y` +|customer/joy, customer/jay + +|`customer/*/id` +|customer/adam/id, com/amy/id + +|`customer/**` +|customer/adam, customer/adam/id, customer/adam/name + +|`customer/**/*.html` +|customer/index.html, customer/adam/profile.html, customer/adam/job/description.html +|=== + +The other option is regular expression based matching. To use regular expressions, set `patternStyle = FilterPatternStyle.REGEX`. The `pattern` attribute is expected to contain a regular expression which will be expected to match the provided URLs exactly (using link:{jdkapi}/java/util/regex/Matcher.html#matches--[Matcher#matches]). + +NOTE: Using `FilterPatternStyle.ANT` is preferred as the pattern matching is more performant than using regular expressions. `FilterPatternStyle.REGEX` should be used when your pattern cannot be written properly using Ant. diff --git a/src/main/docs/guide/httpServer/filters/filtermethods.adoc b/src/main/docs/guide/httpServer/filters/filtermethods.adoc new file mode 100644 index 00000000000..4240194b1f4 --- /dev/null +++ b/src/main/docs/guide/httpServer/filters/filtermethods.adoc @@ -0,0 +1,9 @@ +A filter method must be declared in a bean annotated with api:http.annotation.ServerFilter[], or api:http.annotation.ClientFilter[] if it should instead intercept requests made by the HTTP client. Each filter method must also be annotated with api:http.annotation.RequestFilter[], to run before the request is processed, or api:http.annotation.ResponseFilter[], to run after the request has completed to process the response. + +A filter method can take various parameters, such as the api:http.HttpRequest[] and the api:http.HttpResponse[] (only for response filters). The return type can be `void` or an updated api:http.HttpRequest[] (only for request filters) or api:http.HttpResponse[]. The different supported parameter and return types are described in the documentation of api:http.annotation.RequestFilter[] and api:http.annotation.ResponseFilter[]. + +To write asynchronous filters, you can return a reactive publisher. + +To put these concepts into practice lets look at an example. + +IMPORTANT: Filter methods execute in the event loop by default. If you need to perform blocking operations, you can annotate the filter with api:scheduling.annotation.ExecuteOn[]. diff --git a/src/main/docs/guide/httpServer/filters/filtermethods/continuations.adoc b/src/main/docs/guide/httpServer/filters/filtermethods/continuations.adoc new file mode 100644 index 00000000000..02f79919da5 --- /dev/null +++ b/src/main/docs/guide/httpServer/filters/filtermethods/continuations.adoc @@ -0,0 +1,10 @@ +Request filters can define a special api:http.filter.FilterContinuation[] parameter to get more control of the downstream execution, and to be run further actions after it completes. For example, the above `TraceFilter` can be expressed using a single request filter: + +snippet::io.micronaut.docs.server.filters.filtermethods.continuations.TraceFilter[tags="doFilter", indent=0, title="Single request filter"] + +<1> The request filter declares a api:http.filter.FilterContinuation[] parameter. The continuation will return a api:http.MutableHttpResponse[] +<2> After the request processing is done, the filter calls the blocking `proceed` to run downstream filters and the controller +<3> When downstream processing completes, the filter adds a `X-Trace-Enabled` header to the response returned by the continuation +<4> The whole filter is executed on a worker thread to avoid blocking the event loop in the `proceed` call + +IMPORTANT: The call to `FilterContinuation.proceed` is blocking by default, so it should never be done on the event loop. Such filters should be run on a worker thread as described above. Alternatively, the continuation can also be declared to return a reactive type (`Publisher>`) to proceed in an asynchronous manner, similar to the old api:http.filter.FilterChain[] API. diff --git a/src/main/docs/guide/httpServer/filters/filtermethods/errorStates.adoc b/src/main/docs/guide/httpServer/filters/filtermethods/errorStates.adoc new file mode 100644 index 00000000000..1556a163c53 --- /dev/null +++ b/src/main/docs/guide/httpServer/filters/filtermethods/errorStates.adoc @@ -0,0 +1,20 @@ +In principle, downstream filters and controllers can produce exceptions, and response filters should be prepared to handle them. For a response filter to be called when there is an exception, it must declare the exception type as a parameter. + +.@Filter Response filter declaration +|=== +|Declaration|Called when? + +|`void responseFilter(HttpResponse response)` +|Only called on non-exception response + +|`void responseFilter(Throwable failure)` +|Only called on exception response + +|`void responseFilter(IOException failure)` +|Only called on exception response, if the exception is an `IOException` + +|`void responseFilter(HttpResponse response, @Nullable Throwable failure)` +|Always called. `failure` will be `null` if there was no error. If there was an error, `response` will be `null`. +|=== + +Whether errors appear as exceptions depends on the context of the filter. For the Micronaut HTTP server, any exception is mapped to a non-exceptional api:http.HttpResponse[] with an error status code. This mapping happens before each filter, so a server filter will never actually see an exception. If you still want to access the original cause of the response, it is stored as the attribute api:http.HttpAttributes#EXCEPTION[]. diff --git a/src/main/docs/guide/httpServer/filters/filtermethods/filtermethodsexample.adoc b/src/main/docs/guide/httpServer/filters/filtermethods/filtermethodsexample.adoc new file mode 100644 index 00000000000..0dbeb9fdd3e --- /dev/null +++ b/src/main/docs/guide/httpServer/filters/filtermethods/filtermethodsexample.adoc @@ -0,0 +1,21 @@ +Suppose you wish to trace each request to the Micronaut "Hello World" example using some external system. This system could be a database or a distributed tracing service, and may require I/O operations. + +You should not block the underlying Netty event loop in your filter; instead the filter should proceed with execution once any I/O is complete. + +As an example, consider this `TraceService` that performs an I/O operation: + +snippet::io.micronaut.docs.server.filters.filtermethods.TraceService[tags="imports,class", indent=0, title="A TraceService Example using Reactive Streams"] + +<1> Since this is just an example, the logic does nothing yet + +The following code sample shows how to write a filter using filter methods: + +snippet::io.micronaut.docs.server.filters.filtermethods.TraceFilter[tags="imports,clazz", indent=0, title="An Example ServerFilter"] + +<1> The api:http.annotation.ServerFilter[] annotation defines the URI pattern(s) the filter matches +<2> The previously defined `TraceService` is injected via constructor +<3> The request filter is marked to execute on a separate thread so that the blocking code in `TraceService` does not cause problems +<4> `TraceService` is invoked to trace the request +<5> Finally, a separate response filter method adds a `X-Trace-Enabled` header to the response. + +The previous example demonstrates some key concepts such as executing blocking logic in a worker thread before proceeding with the request and modifying the outgoing response. diff --git a/src/main/docs/guide/httpServer/filters/httpServerFilter.adoc b/src/main/docs/guide/httpServer/filters/httpServerFilter.adoc new file mode 100644 index 00000000000..72c375c5ed6 --- /dev/null +++ b/src/main/docs/guide/httpServer/filters/httpServerFilter.adoc @@ -0,0 +1,14 @@ +WARNING: We recommend Micronaut developers use <> instead of `HttpServerFilter` introduced in Micronaut Framework 4.0 to implement filters. + + +For a server application, the api:http.filter.HttpServerFilter[] interface `doFilter` method can be implemented. + +The `doFilter` method accepts the api:http.HttpRequest[] and an instance of api:http.filter.ServerFilterChain[]. + +The `ServerFilterChain` interface contains a resolved chain of filters where the final entry in the chain is the matched route. The api:http.filter.ServerFilterChain.proceed(io.micronaut.http.HttpRequest)[] method resumes processing of the request. + +The `proceed(..)` method returns a Reactive Streams rs:Publisher[] that emits the response to be returned to the client. Implementors of filters can subscribe to the rs:Publisher[] and mutate the emitted api:http.MutableHttpResponse[] to modify the response prior to returning the response to the client. + +To put these concepts into practice lets look at an example. + +IMPORTANT: Filters execute in the event loop, so blocking operations must be offloaded to another thread pool. diff --git a/src/main/docs/guide/httpServer/filters/httpServerFilter/httpServerFilterErrorStates.adoc b/src/main/docs/guide/httpServer/filters/httpServerFilter/httpServerFilterErrorStates.adoc new file mode 100644 index 00000000000..b9b2aedc00f --- /dev/null +++ b/src/main/docs/guide/httpServer/filters/httpServerFilter/httpServerFilterErrorStates.adoc @@ -0,0 +1 @@ +The publisher returned from `chain.proceed` should never emit an error. In the cases where an upstream filter emitted an error or the route itself threw an exception, the error response should be emitted instead of the exception. In some cases it may be desirable to know the cause of the error response and for this purpose an attribute exists on the response if it was created as a result of an exception being emitted or thrown. The original cause is stored as the attribute api:http.HttpAttributes#EXCEPTION[]. diff --git a/src/main/docs/guide/httpServer/filters/httpServerFilter/httpServerFilterExample.adoc b/src/main/docs/guide/httpServer/filters/httpServerFilter/httpServerFilterExample.adoc new file mode 100644 index 00000000000..14c19a28e59 --- /dev/null +++ b/src/main/docs/guide/httpServer/filters/httpServerFilter/httpServerFilterExample.adoc @@ -0,0 +1,26 @@ +Suppose you wish to trace each request to the Micronaut "Hello World" example using some external system. This system could be a database or a distributed tracing service, and may require I/O operations. + +You should not block the underlying Netty event loop in your filter; instead the filter should proceed with execution once any I/O is complete. + +As an example, consider this `TraceService` that uses https://projectreactor.io[Project Reactor] to compose an I/O operation: + +snippet::io.micronaut.docs.server.filters.TraceService[tags="imports,class", indent=0, title="A TraceService Example using Reactive Streams"] + +<1> The reactor:Mono[] type creates logic that executes potentially blocking operations to write the trace data from the request +<2> Since this is just an example, the logic does nothing yet +<3> The `Schedulers.boundedElastic` executes the logic + +The following code sample shows how to implement the api:http.filter.HttpServerFilter[] interface. + +snippet::io.micronaut.docs.server.filters.TraceFilter[tags="imports,clazz", indent=0, title="An Example HttpServerFilter"] + +<1> The api:http.annotation.Filter[] annotation defines the URI pattern(s) the filter matches +<2> The class implements the api:http.filter.HttpServerFilter[] interface +<3> The previously defined `TraceService` is injected via constructor +<4> `TraceService` is invoked to trace the request +<5> If the call succeeds, the filter resumes request processing using https://projectreactor.io[Project Reactor]'s `switchMap` method, which invokes the `proceed` method of the api:http.filter.ServerFilterChain[] +<6> Finally, the https://projectreactor.io[Project Reactor]'s `doOnNext` method adds a `X-Trace-Enabled` header to the response. + +The previous example demonstrates some key concepts such as executing logic in a non-blocking manner before proceeding with the request and modifying the outgoing response. + +TIP: The examples use https://projectreactor.io[Project Reactor], however you can use any reactive framework that supports the Reactive streams specifications diff --git a/src/main/docs/guide/introduction/whatsNew.adoc b/src/main/docs/guide/introduction/whatsNew.adoc index 926aca5f6c2..78934dbaace 100644 --- a/src/main/docs/guide/introduction/whatsNew.adoc +++ b/src/main/docs/guide/introduction/whatsNew.adoc @@ -9,6 +9,7 @@ Micronaut Framework 4.x supports https://groovy-lang.org/releasenotes/groovy-4.0 * <> +* <> ==== Injection of Maps diff --git a/src/main/docs/guide/toc.yml b/src/main/docs/guide/toc.yml index 56862d8826b..b2ac3db2ef0 100644 --- a/src/main/docs/guide/toc.yml +++ b/src/main/docs/guide/toc.yml @@ -110,7 +110,18 @@ httpServer: serverIO: Writing Response Data uploads: File Uploads transfers: File Transfers - filters: HTTP Filters + filters: + title: HTTP Filters + filterPatterns: Filter Patterns + filtermethods: + title: Filter Methods + filtermethodsexample: Server Filter with Filter Methods + errorStates: Error States + continuations: Continuations + httpServerFilter: + title: HttpServerFilter + httpServerFilterExample: HttpServerFilter Example + httpServerFilterErrorStates: HttpServerFilter Error States sessions: HTTP Sessions sse: Server Sent Events websocket: diff --git a/test-suite-groovy/src/test/groovy/io/micronaut/docs/client/ThirdPartyClientFilterSpec.groovy b/test-suite-groovy/src/test/groovy/io/micronaut/docs/client/ThirdPartyClientFilterSpec.groovy index 8bd6bdfa95a..bdca35888fe 100644 --- a/test-suite-groovy/src/test/groovy/io/micronaut/docs/client/ThirdPartyClientFilterSpec.groovy +++ b/test-suite-groovy/src/test/groovy/io/micronaut/docs/client/ThirdPartyClientFilterSpec.groovy @@ -6,16 +6,15 @@ import io.micronaut.context.annotation.Value import io.micronaut.http.HttpRequest import io.micronaut.http.HttpResponse import io.micronaut.http.MutableHttpRequest +import io.micronaut.http.annotation.ClientFilter import io.micronaut.http.annotation.Controller -import io.micronaut.http.annotation.Filter import io.micronaut.http.annotation.Get import io.micronaut.http.annotation.Header +import io.micronaut.http.annotation.RequestFilter import io.micronaut.http.client.HttpClient import io.micronaut.http.client.annotation.Client -import io.micronaut.http.filter.ClientFilterChain -import io.micronaut.http.filter.HttpClientFilter import io.micronaut.runtime.server.EmbeddedServer -import org.reactivestreams.Publisher +import jakarta.inject.Singleton import reactor.core.publisher.Flux import spock.lang.AutoCleanup import spock.lang.Retry @@ -23,8 +22,6 @@ import spock.lang.Shared import spock.lang.Specification import spock.util.concurrent.PollingConditions -import jakarta.inject.Singleton - @Retry class ThirdPartyClientFilterSpec extends Specification { @@ -94,8 +91,8 @@ class BintrayService { @Requires(property = "spec.name", value = "ThirdPartyClientFilterSpec") //tag::bintrayFilter[] -@Filter('/repos/**') // <1> -class BintrayFilter implements HttpClientFilter { +@ClientFilter('/repos/**') // <1> +class BintrayFilter { final String username final String token @@ -107,12 +104,9 @@ class BintrayFilter implements HttpClientFilter { this.token = token } - @Override - Publisher> doFilter(MutableHttpRequest request, - ClientFilterChain chain) { - chain.proceed( - request.basicAuth(username, token) // <3> - ) + @RequestFilter + void filter(MutableHttpRequest request) { + request.basicAuth(username, token) // <3> } } //end::bintrayFilter[] diff --git a/test-suite-groovy/src/test/groovy/io/micronaut/docs/client/filter/BasicAuthClientFilter.groovy b/test-suite-groovy/src/test/groovy/io/micronaut/docs/client/filter/BasicAuthClientFilter.groovy index 1004d518811..cbdab4f9866 100644 --- a/test-suite-groovy/src/test/groovy/io/micronaut/docs/client/filter/BasicAuthClientFilter.groovy +++ b/test-suite-groovy/src/test/groovy/io/micronaut/docs/client/filter/BasicAuthClientFilter.groovy @@ -16,22 +16,20 @@ package io.micronaut.docs.client.filter //tag::class[] -import io.micronaut.http.HttpResponse -import io.micronaut.http.MutableHttpRequest -import io.micronaut.http.filter.ClientFilterChain -import io.micronaut.http.filter.HttpClientFilter -import org.reactivestreams.Publisher +import io.micronaut.http.MutableHttpRequest +import io.micronaut.http.annotation.ClientFilter +import io.micronaut.http.annotation.RequestFilter import jakarta.inject.Singleton @BasicAuth // <1> @Singleton // <2> -class BasicAuthClientFilter implements HttpClientFilter { +@ClientFilter +class BasicAuthClientFilter { - @Override - Publisher> doFilter(MutableHttpRequest request, - ClientFilterChain chain) { - chain.proceed(request.basicAuth("user", "pass")) + @RequestFilter + void filter(MutableHttpRequest request) { + request.basicAuth("user", "pass") } } //end::class[] diff --git a/test-suite-groovy/src/test/groovy/io/micronaut/docs/client/filter/GoogleAuthFilter.groovy b/test-suite-groovy/src/test/groovy/io/micronaut/docs/client/filter/GoogleAuthFilter.groovy index 3ed6c0fa88b..c839cba9fbb 100644 --- a/test-suite-groovy/src/test/groovy/io/micronaut/docs/client/filter/GoogleAuthFilter.groovy +++ b/test-suite-groovy/src/test/groovy/io/micronaut/docs/client/filter/GoogleAuthFilter.groovy @@ -1,24 +1,21 @@ package io.micronaut.docs.client.filter -//tag::class[] +import io.micronaut.context.BeanProvider import io.micronaut.context.annotation.Requires import io.micronaut.context.env.Environment -import io.micronaut.context.BeanProvider -import io.micronaut.http.HttpResponse + +//tag::class[] + import io.micronaut.http.MutableHttpRequest -import io.micronaut.http.annotation.Filter +import io.micronaut.http.annotation.ClientFilter +import io.micronaut.http.annotation.RequestFilter import io.micronaut.http.client.HttpClient -import io.micronaut.http.filter.ClientFilterChain -import io.micronaut.http.filter.HttpClientFilter -import org.reactivestreams.Publisher -import reactor.core.publisher.Flux -import reactor.core.publisher.Mono - -import static io.micronaut.http.HttpRequest.GET +import io.micronaut.scheduling.TaskExecutors +import io.micronaut.scheduling.annotation.ExecuteOn @Requires(env = Environment.GOOGLE_COMPUTE) -@Filter(patterns = "/google-auth/api/**") -class GoogleAuthFilter implements HttpClientFilter { +@ClientFilter(patterns = "/google-auth/api/**") +class GoogleAuthFilter { private final BeanProvider authClientProvider @@ -26,15 +23,15 @@ class GoogleAuthFilter implements HttpClientFilter { this.authClientProvider = httpClientProvider } - @Override - Publisher> doFilter(MutableHttpRequest request, - ClientFilterChain chain) { - Flux token = Mono.fromCallable(() -> encodeURI(request)) - .flatMap(authURI -> authClientProvider.get().retrieve(GET(authURI).header( // <2> - "Metadata-Flavor", "Google" - ))) + @RequestFilter + @ExecuteOn(TaskExecutors.BLOCKING) + void filter(MutableHttpRequest request) { + String authURI = encodeURI(request) + String token = authClientProvider.get().toBlocking().retrieve(HttpRequest.GET(authURI).header( // <2> + "Metadata-Flavor", "Google" + )) - return token.flatMap(t -> chain.proceed(request.bearerAuth(t))) + request.bearerAuth(token) } private static String encodeURI(MutableHttpRequest request) { diff --git a/test-suite-groovy/src/test/groovy/io/micronaut/docs/server/filters/TraceFilter.groovy b/test-suite-groovy/src/test/groovy/io/micronaut/docs/server/filters/TraceFilter.groovy index e5231ab70fd..34c27a57c00 100644 --- a/test-suite-groovy/src/test/groovy/io/micronaut/docs/server/filters/TraceFilter.groovy +++ b/test-suite-groovy/src/test/groovy/io/micronaut/docs/server/filters/TraceFilter.groovy @@ -15,6 +15,8 @@ */ package io.micronaut.docs.server.filters +import io.micronaut.context.annotation.Requires + // tag::imports[] import io.micronaut.http.HttpRequest import io.micronaut.http.MutableHttpResponse @@ -24,11 +26,8 @@ import io.micronaut.http.filter.ServerFilterChain import org.reactivestreams.Publisher // end::imports[] -/** - * @author Graeme Rocher - * @since 1.0 - */ -// tag::class[] +@Requires(property = "spec.filter", value = "TraceFilter") +// tag::clazz[] @Filter("/hello/**") // <1> class TraceFilter implements HttpServerFilter { // <2> @@ -37,20 +36,16 @@ class TraceFilter implements HttpServerFilter { // <2> TraceFilter(TraceService traceService) { // <3> this.traceService = traceService } -// end::class[] - // tag::doFilter[] @Override Publisher> doFilter(HttpRequest request, ServerFilterChain chain) { traceService - .trace(request) // <1> - .switchMap({ aBoolean -> chain.proceed(request) }) // <2> + .trace(request) // <3> + .switchMap({ aBoolean -> chain.proceed(request) }) // <4> .doOnNext({ res -> - res.headers.add("X-Trace-Enabled", "true") // <3> + res.headers.add("X-Trace-Enabled", "true") // <5> }) } - // end::doFilter[] -// tag::endclass[] } -// end::endclass[] +// end::clazz[] diff --git a/test-suite-groovy/src/test/groovy/io/micronaut/docs/server/filters/TraceFilterSpec.groovy b/test-suite-groovy/src/test/groovy/io/micronaut/docs/server/filters/TraceFilterSpec.groovy index d1f174dca64..c982a22b48d 100644 --- a/test-suite-groovy/src/test/groovy/io/micronaut/docs/server/filters/TraceFilterSpec.groovy +++ b/test-suite-groovy/src/test/groovy/io/micronaut/docs/server/filters/TraceFilterSpec.groovy @@ -15,6 +15,7 @@ class TraceFilterSpec extends Specification { @Shared @AutoCleanup EmbeddedServer embeddedServer = ApplicationContext.run(EmbeddedServer,['spec.name': HelloControllerSpec.simpleName, + 'spec.filter': 'TraceFilter', 'spec.lang': 'java'], Environment.TEST) @Shared @AutoCleanup HttpClient httpClient = embeddedServer.getApplicationContext() diff --git a/test-suite-groovy/src/test/groovy/io/micronaut/docs/server/filters/filtermethods/TraceFilter.groovy b/test-suite-groovy/src/test/groovy/io/micronaut/docs/server/filters/filtermethods/TraceFilter.groovy new file mode 100644 index 00000000000..8ba056b3731 --- /dev/null +++ b/test-suite-groovy/src/test/groovy/io/micronaut/docs/server/filters/filtermethods/TraceFilter.groovy @@ -0,0 +1,56 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.server.filters.filtermethods + +// tag::imports[] +import io.micronaut.http.HttpRequest +import io.micronaut.context.annotation.Requires +import io.micronaut.http.MutableHttpResponse +import io.micronaut.http.annotation.RequestFilter +import io.micronaut.http.annotation.ResponseFilter +import io.micronaut.http.annotation.ServerFilter +import io.micronaut.scheduling.TaskExecutors +import io.micronaut.scheduling.annotation.ExecuteOn + +// end::imports[] + +/** + * @author Graeme Rocher + * @since 1.0 + */ +@Requires(property = "spec.filter", value = "TraceFilterMethods") +// tag::clazz[] +@ServerFilter("/hello/**") // <1> +class TraceFilter { + + private final TraceService traceService + + TraceFilter(TraceService traceService) { // <2> + this.traceService = traceService + } + + @RequestFilter + @ExecuteOn(TaskExecutors.BLOCKING) // <3> + void filterRequest(HttpRequest request) { + traceService.trace(request) // <4> + } + + @ResponseFilter // <5> + void filterResponse(MutableHttpResponse res) { + res.headers.add("X-Trace-Enabled", "true") + } +} +// end::clazz[] diff --git a/test-suite-groovy/src/test/groovy/io/micronaut/docs/server/filters/filtermethods/TraceFilterMethodsSpec.groovy b/test-suite-groovy/src/test/groovy/io/micronaut/docs/server/filters/filtermethods/TraceFilterMethodsSpec.groovy new file mode 100644 index 00000000000..bd59b4d85ec --- /dev/null +++ b/test-suite-groovy/src/test/groovy/io/micronaut/docs/server/filters/filtermethods/TraceFilterMethodsSpec.groovy @@ -0,0 +1,35 @@ +package io.micronaut.docs.server.filters.filtermethods + +import io.micronaut.context.ApplicationContext +import io.micronaut.context.env.Environment +import io.micronaut.docs.server.intro.HelloControllerSpec +import io.micronaut.http.HttpRequest +import io.micronaut.http.HttpResponse +import io.micronaut.http.client.HttpClient +import io.micronaut.runtime.server.EmbeddedServer +import spock.lang.Specification + +class TraceFilterMethodsSpec extends Specification { + + void "test trace filter"() { + given: + EmbeddedServer embeddedServer = + ApplicationContext.run(EmbeddedServer,['spec.name': HelloControllerSpec.simpleName, + 'spec.filter': 'TraceFilterMethods', + 'spec.lang': 'java'], Environment.TEST) + HttpClient httpClient = + embeddedServer.getApplicationContext() + .createBean(HttpClient, embeddedServer.getURL()) + HttpResponse response = httpClient.toBlocking() + .exchange(HttpRequest.GET('/hello')) + + + expect: + response.headers.get('X-Trace-Enabled') == 'true' + + cleanup: + embeddedServer.close() + httpClient.close() + } +} + diff --git a/test-suite-groovy/src/test/groovy/io/micronaut/docs/server/filters/filtermethods/TraceService.groovy b/test-suite-groovy/src/test/groovy/io/micronaut/docs/server/filters/filtermethods/TraceService.groovy new file mode 100644 index 00000000000..19c935fc7fc --- /dev/null +++ b/test-suite-groovy/src/test/groovy/io/micronaut/docs/server/filters/filtermethods/TraceService.groovy @@ -0,0 +1,40 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.server.filters.filtermethods + +// tag::imports[] +import io.micronaut.http.HttpRequest +import org.slf4j.Logger +import org.slf4j.LoggerFactory + +import jakarta.inject.Singleton +import reactor.core.publisher.Flux +import reactor.core.publisher.Mono +import reactor.core.scheduler.Schedulers + +// end::imports[] + +// tag::class[] +@Singleton +class TraceService { + private static final Logger LOG = LoggerFactory.getLogger(TraceService.class) + + void trace(HttpRequest request) { + LOG.debug('Tracing request: {}', request.uri) + // trace logic here, potentially performing I/O <2> + } +} +// end::class[] diff --git a/test-suite-groovy/src/test/groovy/io/micronaut/docs/server/filters/filtermethods/continuations/TraceFilter.groovy b/test-suite-groovy/src/test/groovy/io/micronaut/docs/server/filters/filtermethods/continuations/TraceFilter.groovy new file mode 100644 index 00000000000..ff9ade139cb --- /dev/null +++ b/test-suite-groovy/src/test/groovy/io/micronaut/docs/server/filters/filtermethods/continuations/TraceFilter.groovy @@ -0,0 +1,57 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.server.filters.filtermethods.continuations + +import io.micronaut.docs.server.filters.filtermethods.TraceService + +// tag::imports[] +import io.micronaut.context.annotation.Requires +import io.micronaut.http.HttpRequest +import io.micronaut.http.MutableHttpResponse +import io.micronaut.http.annotation.RequestFilter +import io.micronaut.http.annotation.ServerFilter +import io.micronaut.http.filter.FilterContinuation +import io.micronaut.scheduling.TaskExecutors +import io.micronaut.scheduling.annotation.ExecuteOn + +// end::imports[] + +/** + * @author Graeme Rocher + * @since 1.0 + */ +@Requires(property = "spec.filter", value = "TraceFilterContinuation") +@ServerFilter("/hello/**") +class TraceFilter { + + private final TraceService traceService + + TraceFilter(TraceService traceService) { + this.traceService = traceService + } + + // tag::doFilter[] + @RequestFilter + @ExecuteOn(TaskExecutors.BLOCKING) // <4> + void filterRequest(HttpRequest request, FilterContinuation> continuation) { // <1> + traceService.trace(request) + MutableHttpResponse res = continuation.proceed(); // <2> + res.headers.add("X-Trace-Enabled", "true") // <3> + } + // end::doFilter[] +// tag::endclass[] +} +// end::endclass[] diff --git a/test-suite-groovy/src/test/groovy/io/micronaut/docs/server/filters/filtermethods/continuations/TraceFilterContinuationSpec.groovy b/test-suite-groovy/src/test/groovy/io/micronaut/docs/server/filters/filtermethods/continuations/TraceFilterContinuationSpec.groovy new file mode 100644 index 00000000000..553e698b3c4 --- /dev/null +++ b/test-suite-groovy/src/test/groovy/io/micronaut/docs/server/filters/filtermethods/continuations/TraceFilterContinuationSpec.groovy @@ -0,0 +1,34 @@ +package io.micronaut.docs.server.filters.filtermethods.continuations + +import io.micronaut.context.ApplicationContext +import io.micronaut.context.env.Environment +import io.micronaut.docs.server.intro.HelloControllerSpec +import io.micronaut.http.HttpRequest +import io.micronaut.http.HttpResponse +import io.micronaut.http.client.HttpClient +import io.micronaut.runtime.server.EmbeddedServer +import spock.lang.Specification + +class TraceFilterContinuationSpec extends Specification { + void "test trace filter with continuations"() { + given: + EmbeddedServer embeddedServer = + ApplicationContext.run(EmbeddedServer,['spec.name': HelloControllerSpec.simpleName, + 'spec.filter': 'TraceFilterContinuation', + 'spec.lang': 'java'], Environment.TEST) + HttpClient httpClient = + embeddedServer.getApplicationContext() + .createBean(HttpClient, embeddedServer.getURL()) + HttpResponse response = httpClient.toBlocking() + .exchange(HttpRequest.GET('/hello')) + + + expect: + response.headers.get('X-Trace-Enabled') == 'true' + + cleanup: + embeddedServer.close() + httpClient.close() + } +} + diff --git a/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/client/ThirdPartyClientFilterSpec.kt b/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/client/ThirdPartyClientFilterSpec.kt index 09eaceaf383..3a8c6ed897c 100644 --- a/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/client/ThirdPartyClientFilterSpec.kt +++ b/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/client/ThirdPartyClientFilterSpec.kt @@ -1,26 +1,20 @@ package io.micronaut.docs.client -import io.kotest.matchers.shouldBe import io.kotest.core.spec.style.StringSpec +import io.kotest.matchers.shouldBe import io.micronaut.context.ApplicationContext import io.micronaut.context.annotation.Requires import io.micronaut.context.annotation.Value import io.micronaut.http.HttpRequest import io.micronaut.http.HttpResponse import io.micronaut.http.MutableHttpRequest -import io.micronaut.http.annotation.Controller -import io.micronaut.http.annotation.Filter -import io.micronaut.http.annotation.Get -import io.micronaut.http.annotation.Header +import io.micronaut.http.annotation.* import io.micronaut.http.client.HttpClient import io.micronaut.http.client.annotation.Client -import io.micronaut.http.filter.ClientFilterChain -import io.micronaut.http.filter.HttpClientFilter import io.micronaut.runtime.server.EmbeddedServer -import org.reactivestreams.Publisher -import java.util.Base64 import jakarta.inject.Singleton import reactor.core.publisher.Flux +import java.util.* class ThirdPartyClientFilterSpec: StringSpec() { private var result: String? = null @@ -81,16 +75,15 @@ internal class BintrayService( @Requires(property = "spec.name", value = "ThirdPartyClientFilterSpec") //tag::bintrayFilter[] -@Filter("/repos/**") // <1> +@ClientFilter("/repos/**") // <1> internal class BintrayFilter( @param:Value("\${bintray.username}") val username: String, // <2> - @param:Value("\${bintray.token}") val token: String)// <2> - : HttpClientFilter { + @param:Value("\${bintray.token}") val token: String // <2> +) { - override fun doFilter(request: MutableHttpRequest<*>, chain: ClientFilterChain): Publisher> { - return chain.proceed( - request.basicAuth(username, token) // <3> - ) + @RequestFilter + fun filter(request: MutableHttpRequest<*>) { + request.basicAuth(username, token) // <3> } } //end::bintrayFilter[] diff --git a/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/client/filter/BasicAuthClientFilter.kt b/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/client/filter/BasicAuthClientFilter.kt index 3f3acf1b51d..c7ac7b3dcbe 100644 --- a/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/client/filter/BasicAuthClientFilter.kt +++ b/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/client/filter/BasicAuthClientFilter.kt @@ -16,21 +16,20 @@ package io.micronaut.docs.client.filter //tag::class[] -import io.micronaut.http.HttpResponse -import io.micronaut.http.MutableHttpRequest -import io.micronaut.http.filter.ClientFilterChain -import io.micronaut.http.filter.HttpClientFilter -import org.reactivestreams.Publisher +import io.micronaut.http.MutableHttpRequest +import io.micronaut.http.annotation.ClientFilter +import io.micronaut.http.annotation.RequestFilter import jakarta.inject.Singleton @BasicAuth // <1> @Singleton // <2> -class BasicAuthClientFilter : HttpClientFilter { +@ClientFilter +class BasicAuthClientFilter { - override fun doFilter(request: MutableHttpRequest<*>, - chain: ClientFilterChain): Publisher> { - return chain.proceed(request.basicAuth("user", "pass")) + @RequestFilter + fun doFilter(request: MutableHttpRequest<*>) { + request.basicAuth("user", "pass") } } //end::class[] diff --git a/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/client/filter/GoogleAuthFilter.kt b/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/client/filter/GoogleAuthFilter.kt index 476f283b6a4..92cc14ffb9a 100644 --- a/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/client/filter/GoogleAuthFilter.kt +++ b/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/client/filter/GoogleAuthFilter.kt @@ -5,30 +5,27 @@ import io.micronaut.context.BeanProvider import io.micronaut.context.annotation.Requires import io.micronaut.context.env.Environment import io.micronaut.http.HttpRequest -import io.micronaut.http.HttpResponse import io.micronaut.http.MutableHttpRequest -import io.micronaut.http.annotation.Filter +import io.micronaut.http.annotation.ClientFilter +import io.micronaut.http.annotation.RequestFilter import io.micronaut.http.client.HttpClient -import io.micronaut.http.filter.ClientFilterChain -import io.micronaut.http.filter.HttpClientFilter -import org.reactivestreams.Publisher -import reactor.core.publisher.Mono +import io.micronaut.scheduling.TaskExecutors +import io.micronaut.scheduling.annotation.ExecuteOn import java.net.URLEncoder @Requires(env = [Environment.GOOGLE_COMPUTE]) -@Filter(patterns = ["/google-auth/api/**"]) +@ClientFilter(patterns = ["/google-auth/api/**"]) class GoogleAuthFilter ( - private val authClientProvider: BeanProvider) : HttpClientFilter { // <1> + private val authClientProvider: BeanProvider) { // <1> - override fun doFilter(request: MutableHttpRequest<*>, - chain: ClientFilterChain): Publisher?> { - return Mono.fromCallable { encodeURI(request) } - .flux() - .map { authURI: String -> - authClientProvider.get().retrieve(HttpRequest.GET(authURI) - .header("Metadata-Flavor", "Google") // <2> - ) - }.flatMap { t -> chain.proceed(request.bearerAuth(t.toString())) } + @RequestFilter + @ExecuteOn(TaskExecutors.BLOCKING) + fun filter(request: MutableHttpRequest<*>) { + val authURI = encodeURI(request) + val t = authClientProvider.get().toBlocking().retrieve(HttpRequest.GET(authURI) + .header("Metadata-Flavor", "Google") // <2> + ) + request.bearerAuth(t.toString()) } private fun encodeURI(request: MutableHttpRequest<*>): String { diff --git a/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/server/filters/TraceFilter.kt b/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/server/filters/TraceFilter.kt index 1f455669336..8acd2c1bc4e 100644 --- a/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/server/filters/TraceFilter.kt +++ b/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/server/filters/TraceFilter.kt @@ -16,6 +16,7 @@ package io.micronaut.docs.server.filters // tag::imports[] +import io.micronaut.context.annotation.Requires import io.micronaut.http.HttpRequest import io.micronaut.http.MutableHttpResponse import io.micronaut.http.annotation.Filter @@ -24,23 +25,20 @@ import io.micronaut.http.filter.ServerFilterChain import org.reactivestreams.Publisher // end::imports[] -// tag::class[] +@Requires(property = "spec.filter", value = "TraceFilter") +// tag::clazz[] @Filter("/hello/**") // <1> -class TraceFilter(// <2> +class TraceFilter( // <2> private val traceService: TraceService)// <3> : HttpServerFilter { - // end::class[] - // tag::doFilter[] override fun doFilter(request: HttpRequest<*>, chain: ServerFilterChain): Publisher> { - return traceService.trace(request) // <1> - .switchMap { aBoolean -> chain.proceed(request) } // <2> + return traceService.trace(request) // <4> + .switchMap { aBoolean -> chain.proceed(request) } // <5> .doOnNext { res -> - res.headers.add("X-Trace-Enabled", "true") // <3> + res.headers.add("X-Trace-Enabled", "true") // <6> } } - // end::doFilter[] -// tag::endclass[] } -// end::endclass[] +// end::clazz[] diff --git a/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/server/filters/TraceFilterSpec.kt b/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/server/filters/TraceFilterSpec.kt index 494d401af96..1113a951679 100644 --- a/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/server/filters/TraceFilterSpec.kt +++ b/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/server/filters/TraceFilterSpec.kt @@ -12,7 +12,9 @@ class TraceFilterSpec: StringSpec() { val embeddedServer = autoClose( ApplicationContext.run(EmbeddedServer::class.java, - mapOf("spec.name" to HelloControllerSpec::class.java.simpleName, "spec.lang" to "java")) + mapOf("spec.name" to HelloControllerSpec::class.java.simpleName, + "spec.filter" to "TraceFilter", + "spec.lang" to "java")) ) val client = autoClose( diff --git a/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/server/filters/TraceService.kt b/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/server/filters/TraceService.kt index c44230d32e5..5f897b0d6ae 100644 --- a/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/server/filters/TraceService.kt +++ b/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/server/filters/TraceService.kt @@ -16,13 +16,13 @@ package io.micronaut.docs.server.filters // tag::imports[] +import io.micronaut.context.annotation.Requires import io.micronaut.http.HttpRequest import org.slf4j.LoggerFactory import jakarta.inject.Singleton import reactor.core.publisher.Flux import reactor.core.publisher.Mono import reactor.core.scheduler.Schedulers - // end::imports[] // tag::class[] diff --git a/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/server/filters/filtermethods/TraceFilter.kt b/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/server/filters/filtermethods/TraceFilter.kt new file mode 100644 index 00000000000..f5750d5697c --- /dev/null +++ b/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/server/filters/filtermethods/TraceFilter.kt @@ -0,0 +1,45 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.server.filters.filtermethods + +// tag::imports[] +import io.micronaut.context.annotation.Requires +import io.micronaut.http.HttpRequest +import io.micronaut.http.MutableHttpResponse +import io.micronaut.http.annotation.RequestFilter +import io.micronaut.http.annotation.ResponseFilter +import io.micronaut.http.annotation.ServerFilter +import io.micronaut.scheduling.TaskExecutors +import io.micronaut.scheduling.annotation.ExecuteOn +// end::imports[] + +@Requires(property = "spec.filter", value = "TraceFilterMethods") +// tag::clazz[] +@ServerFilter("/hello/**") // <1> +class TraceFilter(private val traceService: TraceService) { // <2> + + @RequestFilter + @ExecuteOn(TaskExecutors.BLOCKING) // <3> + fun filterRequest(request: HttpRequest<*>) { + traceService.trace(request) // <4> + } + + @ResponseFilter // <5> + fun filterResponse(res: MutableHttpResponse<*>) { + res.headers.add("X-Trace-Enabled", "true") + } +} +// end::clazz[] diff --git a/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/server/filters/filtermethods/TraceFilterSpec.kt b/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/server/filters/filtermethods/TraceFilterSpec.kt new file mode 100644 index 00000000000..16bc9d1e929 --- /dev/null +++ b/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/server/filters/filtermethods/TraceFilterSpec.kt @@ -0,0 +1,28 @@ +package io.micronaut.docs.server.filters.filtermethods + +import io.kotest.core.spec.style.StringSpec +import io.kotest.matchers.shouldBe +import io.micronaut.context.ApplicationContext +import io.micronaut.docs.server.intro.HelloControllerSpec +import io.micronaut.http.HttpRequest +import io.micronaut.http.client.HttpClient +import io.micronaut.runtime.server.EmbeddedServer + +class TraceFilterSpec: StringSpec() { + + init { + "test trace filter" { + val embeddedServer = ApplicationContext.run(EmbeddedServer::class.java, + mapOf("spec.name" to HelloControllerSpec::class.java.simpleName, "spec.filter" to "TraceFilterMethods", "spec.lang" to "java")) + val client = embeddedServer.applicationContext.createBean(HttpClient::class.java, embeddedServer.url) + + val response = client.toBlocking().exchange(HttpRequest.GET("/hello")) + + response.headers.get("X-Trace-Enabled") shouldBe "true" + + embeddedServer.close() + client.close() + } + } +} + diff --git a/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/server/filters/filtermethods/TraceService.kt b/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/server/filters/filtermethods/TraceService.kt new file mode 100644 index 00000000000..18e297e27d7 --- /dev/null +++ b/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/server/filters/filtermethods/TraceService.kt @@ -0,0 +1,39 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.server.filters.filtermethods + +// tag::imports[] +import io.micronaut.http.HttpRequest +import org.slf4j.LoggerFactory +import jakarta.inject.Singleton +import reactor.core.publisher.Flux +import reactor.core.publisher.Mono +import reactor.core.scheduler.Schedulers + +// end::imports[] + +// tag::class[] +@Singleton +class TraceService { + + private val LOG = LoggerFactory.getLogger(TraceService::class.java) + + internal fun trace(request: HttpRequest<*>) { + LOG.debug("Tracing request: {}", request.uri) + // trace logic here, potentially performing I/O <2> + } +} +// end::class[] diff --git a/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/server/filters/filtermethods/continuations/TraceFilter.kt b/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/server/filters/filtermethods/continuations/TraceFilter.kt new file mode 100644 index 00000000000..538047ed5fd --- /dev/null +++ b/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/server/filters/filtermethods/continuations/TraceFilter.kt @@ -0,0 +1,49 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.server.filters.filtermethods.continuations + +import io.micronaut.docs.server.filters.filtermethods.TraceService +// tag::imports[] +import io.micronaut.context.annotation.Requires +import io.micronaut.http.HttpRequest +import io.micronaut.http.MutableHttpResponse +import io.micronaut.http.annotation.RequestFilter +import io.micronaut.http.annotation.ServerFilter +import io.micronaut.http.filter.FilterContinuation +import io.micronaut.scheduling.TaskExecutors +import io.micronaut.scheduling.annotation.ExecuteOn + +// end::imports[] + +@Requires(property = "spec.filter", value = "TraceFilterContinuation") +// tag::class[] +@ServerFilter("/hello/**") // <1> +class TraceFilter(private val traceService: TraceService) { // <2> + // end::class[] + + // tag::doFilter[] + // end::class[] + @RequestFilter + @ExecuteOn(TaskExecutors.BLOCKING) // <4> + fun filterRequest(request: HttpRequest<*>, continuation: FilterContinuation>) { // <1> + traceService.trace(request) + val res = continuation.proceed() // <2> + res.headers.add("X-Trace-Enabled", "true") // <3> + } + // end::doFilter[] +// tag::endclass[] +} +// end::endclass[] diff --git a/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/server/filters/filtermethods/continuations/TraceFilterContinuationsSpec.kt b/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/server/filters/filtermethods/continuations/TraceFilterContinuationsSpec.kt new file mode 100644 index 00000000000..db03ba131ec --- /dev/null +++ b/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/server/filters/filtermethods/continuations/TraceFilterContinuationsSpec.kt @@ -0,0 +1,28 @@ +package io.micronaut.docs.server.filters.filtermethods.continuations + +import io.kotest.core.spec.style.StringSpec +import io.kotest.matchers.shouldBe +import io.micronaut.context.ApplicationContext +import io.micronaut.docs.server.intro.HelloControllerSpec +import io.micronaut.http.HttpRequest +import io.micronaut.http.client.HttpClient +import io.micronaut.runtime.server.EmbeddedServer + +class TraceFilterContinuationsSpec: StringSpec() { + + init { + "test trace filter with continuations" { + val embeddedServer = ApplicationContext.run(EmbeddedServer::class.java, + mapOf("spec.name" to HelloControllerSpec::class.java.simpleName, "spec.filter" to "TraceFilterContinuation", "spec.lang" to "java")) + val client = embeddedServer.applicationContext.createBean(HttpClient::class.java, embeddedServer.url) + + val response = client.toBlocking().exchange(HttpRequest.GET("/hello")) + + response.headers.get("X-Trace-Enabled") shouldBe "true" + + embeddedServer.close() + client.close() + } + } +} + diff --git a/test-suite/src/test/java/io/micronaut/docs/client/ThirdPartyClientFilterSpec.java b/test-suite/src/test/java/io/micronaut/docs/client/ThirdPartyClientFilterSpec.java index 056b1ddb66d..b893fa10e7b 100644 --- a/test-suite/src/test/java/io/micronaut/docs/client/ThirdPartyClientFilterSpec.java +++ b/test-suite/src/test/java/io/micronaut/docs/client/ThirdPartyClientFilterSpec.java @@ -21,22 +21,21 @@ import io.micronaut.http.HttpRequest; import io.micronaut.http.HttpResponse; import io.micronaut.http.MutableHttpRequest; +import io.micronaut.http.annotation.ClientFilter; import io.micronaut.http.annotation.Controller; -import io.micronaut.http.annotation.Filter; import io.micronaut.http.annotation.Get; import io.micronaut.http.annotation.Header; +import io.micronaut.http.annotation.RequestFilter; import io.micronaut.http.client.HttpClient; import io.micronaut.http.client.annotation.Client; -import io.micronaut.http.filter.ClientFilterChain; -import io.micronaut.http.filter.HttpClientFilter; import io.micronaut.runtime.server.EmbeddedServer; +import jakarta.inject.Singleton; import org.junit.AfterClass; import org.junit.BeforeClass; import org.junit.Test; -import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; import spock.lang.Retry; -import jakarta.inject.Singleton; + import java.util.Base64; import java.util.HashMap; import java.util.Map; @@ -133,8 +132,8 @@ Flux> fetchPackages(String repo) { @Requires(property = "spec.name", value = "ThirdPartyClientFilterSpec") //tag::bintrayFilter[] -@Filter("/repos/**") // <1> -class BintrayFilter implements HttpClientFilter { +@ClientFilter("/repos/**") // <1> +class BintrayFilter { final String username; final String token; @@ -146,12 +145,9 @@ class BintrayFilter implements HttpClientFilter { this.token = token; } - @Override - public Publisher> doFilter(MutableHttpRequest request, - ClientFilterChain chain) { - return chain.proceed( - request.basicAuth(username, token) // <3> - ); + @RequestFilter + public void filter(MutableHttpRequest request) { + request.basicAuth(username, token); // <3> } } //end::bintrayFilter[] diff --git a/test-suite/src/test/java/io/micronaut/docs/client/filter/BasicAuthClientFilter.java b/test-suite/src/test/java/io/micronaut/docs/client/filter/BasicAuthClientFilter.java index 830bf7276b5..5fc282d7478 100644 --- a/test-suite/src/test/java/io/micronaut/docs/client/filter/BasicAuthClientFilter.java +++ b/test-suite/src/test/java/io/micronaut/docs/client/filter/BasicAuthClientFilter.java @@ -16,22 +16,20 @@ package io.micronaut.docs.client.filter; //tag::class[] -import io.micronaut.http.HttpResponse; -import io.micronaut.http.MutableHttpRequest; -import io.micronaut.http.filter.ClientFilterChain; -import io.micronaut.http.filter.HttpClientFilter; -import org.reactivestreams.Publisher; +import io.micronaut.http.MutableHttpRequest; +import io.micronaut.http.annotation.ClientFilter; +import io.micronaut.http.annotation.RequestFilter; import jakarta.inject.Singleton; @BasicAuth // <1> @Singleton // <2> -public class BasicAuthClientFilter implements HttpClientFilter { +@ClientFilter +public class BasicAuthClientFilter { - @Override - public Publisher> doFilter(MutableHttpRequest request, - ClientFilterChain chain) { - return chain.proceed(request.basicAuth("user", "pass")); + @RequestFilter + public void filter(MutableHttpRequest request) { + request.basicAuth("user", "pass"); } } //end::class[] diff --git a/test-suite/src/test/java/io/micronaut/docs/client/filter/GoogleAuthFilter.java b/test-suite/src/test/java/io/micronaut/docs/client/filter/GoogleAuthFilter.java index a5cd5cf9160..e7734f43f64 100644 --- a/test-suite/src/test/java/io/micronaut/docs/client/filter/GoogleAuthFilter.java +++ b/test-suite/src/test/java/io/micronaut/docs/client/filter/GoogleAuthFilter.java @@ -16,26 +16,25 @@ package io.micronaut.docs.client.filter; //tag::class[] + import io.micronaut.context.BeanProvider; import io.micronaut.context.annotation.Requires; import io.micronaut.context.env.Environment; import io.micronaut.http.HttpRequest; -import io.micronaut.http.HttpResponse; import io.micronaut.http.MutableHttpRequest; -import io.micronaut.http.annotation.Filter; +import io.micronaut.http.annotation.ClientFilter; +import io.micronaut.http.annotation.RequestFilter; import io.micronaut.http.client.HttpClient; -import io.micronaut.http.filter.ClientFilterChain; -import io.micronaut.http.filter.HttpClientFilter; -import org.reactivestreams.Publisher; -import reactor.core.publisher.Mono; +import io.micronaut.scheduling.TaskExecutors; +import io.micronaut.scheduling.annotation.ExecuteOn; import java.io.UnsupportedEncodingException; import java.net.URI; import java.net.URLEncoder; @Requires(env = Environment.GOOGLE_COMPUTE) -@Filter(patterns = "/google-auth/api/**") -public class GoogleAuthFilter implements HttpClientFilter { +@ClientFilter(patterns = "/google-auth/api/**") +public class GoogleAuthFilter { private final BeanProvider authClientProvider; @@ -43,14 +42,13 @@ public GoogleAuthFilter(BeanProvider httpClientProvider) { // <1> this.authClientProvider = httpClientProvider; } - @Override - public Publisher> doFilter(MutableHttpRequest request, - ClientFilterChain chain) { - return Mono.fromCallable(() -> encodeURI(request)) - .flux() - .flatMap(uri -> authClientProvider.get().retrieve(HttpRequest.GET(uri) // <2> - .header("Metadata-Flavor", "Google"))) - .flatMap(t -> chain.proceed(request.bearerAuth(t))); + @RequestFilter + @ExecuteOn(TaskExecutors.BLOCKING) + public void filter(MutableHttpRequest request) throws Exception { + String uri = encodeURI(request); + String t = authClientProvider.get().toBlocking().retrieve(HttpRequest.GET(uri) // <2> + .header("Metadata-Flavor", "Google")); + request.bearerAuth(t); } private String encodeURI(MutableHttpRequest request) throws UnsupportedEncodingException { diff --git a/test-suite/src/test/java/io/micronaut/docs/server/filters/TraceFilter.java b/test-suite/src/test/java/io/micronaut/docs/server/filters/TraceFilter.java index bc1719193fe..81bf5a85126 100644 --- a/test-suite/src/test/java/io/micronaut/docs/server/filters/TraceFilter.java +++ b/test-suite/src/test/java/io/micronaut/docs/server/filters/TraceFilter.java @@ -16,6 +16,7 @@ package io.micronaut.docs.server.filters; // tag::imports[] +import io.micronaut.context.annotation.Requires; import io.micronaut.http.HttpRequest; import io.micronaut.http.MutableHttpResponse; import io.micronaut.http.annotation.Filter; @@ -25,29 +26,23 @@ import reactor.core.publisher.Flux; // end::imports[] -// tag::class[] +@Requires(property = "spec.filter", value = "TraceFilter") +// tag::clazz[] @Filter("/hello/**") // <1> public class TraceFilter implements HttpServerFilter { // <2> - private final TraceService traceService; - public TraceFilter(TraceService traceService) { // <3> this.traceService = traceService; } -// end::class[] - - // tag::doFilter[] @Override public Publisher> doFilter(HttpRequest request, ServerFilterChain chain) { return Flux.from(traceService - .trace(request)) // <1> - .switchMap(aBoolean -> chain.proceed(request)) // <2> + .trace(request)) // <4> + .switchMap(aBoolean -> chain.proceed(request)) // <5> .doOnNext(res -> - res.getHeaders().add("X-Trace-Enabled", "true") // <3> + res.getHeaders().add("X-Trace-Enabled", "true") // <6> ); } - // end::doFilter[] -// tag::endclass[] } -// end::endclass[] +// end::clazz[] diff --git a/test-suite/src/test/java/io/micronaut/docs/server/filters/TraceFilterSpec.java b/test-suite/src/test/java/io/micronaut/docs/server/filters/TraceFilterSpec.java index bd5db636fdd..a793b4b39d0 100644 --- a/test-suite/src/test/java/io/micronaut/docs/server/filters/TraceFilterSpec.java +++ b/test-suite/src/test/java/io/micronaut/docs/server/filters/TraceFilterSpec.java @@ -38,6 +38,7 @@ public class TraceFilterSpec { public static void setupServer() { Map map = new HashMap<>(); map.put("spec.name", HelloControllerSpec.class.getSimpleName()); + map.put("spec.filter", "TraceFilter"); map.put("spec.lang", "java"); server = ApplicationContext.run(EmbeddedServer.class, map, Environment.TEST); diff --git a/test-suite/src/test/java/io/micronaut/docs/server/filters/TraceService.java b/test-suite/src/test/java/io/micronaut/docs/server/filters/TraceService.java index 5111a1b78d1..532cb1b1cdb 100644 --- a/test-suite/src/test/java/io/micronaut/docs/server/filters/TraceService.java +++ b/test-suite/src/test/java/io/micronaut/docs/server/filters/TraceService.java @@ -28,10 +28,8 @@ // tag::class[] @Singleton public class TraceService { - private static final Logger LOG = LoggerFactory.getLogger(TraceService.class); - - Publisher trace(HttpRequest request) { + public Publisher trace(HttpRequest request) { return Mono.fromCallable(() -> { // <1> LOG.debug("Tracing request: {}", request.getUri()); // trace logic here, potentially performing I/O <2> diff --git a/test-suite/src/test/java/io/micronaut/docs/server/filters/filtermethods/TraceFilter.java b/test-suite/src/test/java/io/micronaut/docs/server/filters/filtermethods/TraceFilter.java new file mode 100644 index 00000000000..0f93a04353b --- /dev/null +++ b/test-suite/src/test/java/io/micronaut/docs/server/filters/filtermethods/TraceFilter.java @@ -0,0 +1,52 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.server.filters.filtermethods; + +import io.micronaut.docs.server.filters.filtermethods.TraceService; +// tag::imports[] +import io.micronaut.context.annotation.Requires; +import io.micronaut.http.HttpRequest; +import io.micronaut.http.MutableHttpResponse; +import io.micronaut.http.annotation.RequestFilter; +import io.micronaut.http.annotation.ResponseFilter; +import io.micronaut.http.annotation.ServerFilter; +import io.micronaut.scheduling.TaskExecutors; +import io.micronaut.scheduling.annotation.ExecuteOn; +// end::imports[] + +@Requires(property = "spec.filter", value = "TraceFilterMethods") +// tag::clazz[] +@ServerFilter("/hello/**") // <1> +public class TraceFilter { + + private final TraceService traceService; + + public TraceFilter(TraceService traceService) { // <2> + this.traceService = traceService; + } + + @RequestFilter + @ExecuteOn(TaskExecutors.BLOCKING) // <3> + public void filterRequest(HttpRequest request) { + traceService.trace(request); // <4> + } + + @ResponseFilter // <5> + public void filterResponse(MutableHttpResponse res) { + res.getHeaders().add("X-Trace-Enabled", "true"); + } +} +// end::clazz[] diff --git a/test-suite/src/test/java/io/micronaut/docs/server/filters/filtermethods/TraceFilterMethodsSpec.java b/test-suite/src/test/java/io/micronaut/docs/server/filters/filtermethods/TraceFilterMethodsSpec.java new file mode 100644 index 00000000000..58217aa1992 --- /dev/null +++ b/test-suite/src/test/java/io/micronaut/docs/server/filters/filtermethods/TraceFilterMethodsSpec.java @@ -0,0 +1,53 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.server.filters.filtermethods; + +import io.micronaut.context.ApplicationContext; +import io.micronaut.context.env.Environment; +import io.micronaut.docs.server.intro.HelloControllerSpec; +import io.micronaut.http.HttpRequest; +import io.micronaut.http.HttpResponse; +import io.micronaut.http.client.HttpClient; +import io.micronaut.runtime.server.EmbeddedServer; +import org.junit.Test; + +import java.util.HashMap; +import java.util.Map; + +import static org.junit.Assert.assertEquals; + +public class TraceFilterMethodsSpec { + @Test + public void testTraceFilter() { + Map map = new HashMap<>(); + map.put("spec.name", HelloControllerSpec.class.getSimpleName()); + map.put("spec.filter", "TraceFilterMethods"); + map.put("spec.lang", "java"); + + EmbeddedServer server = ApplicationContext.run(EmbeddedServer.class, map, Environment.TEST); + HttpClient client = server + .getApplicationContext() + .createBean(HttpClient.class, server.getURL()); + + HttpResponse response = client.toBlocking().exchange(HttpRequest.GET("/hello")); + + assertEquals("true", response.getHeaders().get("X-Trace-Enabled")); + + server.stop(); + client.stop(); + } +} + diff --git a/http/src/main/java/io/micronaut/http/filter/FilterOrderProvider.java b/test-suite/src/test/java/io/micronaut/docs/server/filters/filtermethods/TraceService.java similarity index 52% rename from http/src/main/java/io/micronaut/http/filter/FilterOrderProvider.java rename to test-suite/src/test/java/io/micronaut/docs/server/filters/filtermethods/TraceService.java index 78a8c277c64..869f7139317 100644 --- a/http/src/main/java/io/micronaut/http/filter/FilterOrderProvider.java +++ b/test-suite/src/test/java/io/micronaut/docs/server/filters/filtermethods/TraceService.java @@ -13,17 +13,25 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.micronaut.http.filter; +package io.micronaut.docs.server.filters.filtermethods; -import io.micronaut.core.order.Ordered; +// tag::imports[] -/** - * Describes a bean that contains an order to define the - * order of a client or server filter. - * - * @author James Kleeh - * @since 1.0 - */ -public interface FilterOrderProvider extends Ordered { +import io.micronaut.http.HttpRequest; +import jakarta.inject.Singleton; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +// end::imports[] + +// tag::class[] +@Singleton +public class TraceService { + + private static final Logger LOG = LoggerFactory.getLogger(TraceService.class); + public void trace(HttpRequest request) { + LOG.debug("Tracing request: {}", request.getUri()); + // trace logic here, potentially performing I/O <1> + } } +// end::class[] diff --git a/test-suite/src/test/java/io/micronaut/docs/server/filters/filtermethods/continuations/TraceFilter.java b/test-suite/src/test/java/io/micronaut/docs/server/filters/filtermethods/continuations/TraceFilter.java new file mode 100644 index 00000000000..e40bc215e73 --- /dev/null +++ b/test-suite/src/test/java/io/micronaut/docs/server/filters/filtermethods/continuations/TraceFilter.java @@ -0,0 +1,51 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.server.filters.filtermethods.continuations; + +import io.micronaut.docs.server.filters.filtermethods.TraceService; +// tag::imports[] +import io.micronaut.context.annotation.Requires; +import io.micronaut.http.HttpRequest; +import io.micronaut.http.MutableHttpResponse; +import io.micronaut.http.annotation.RequestFilter; +import io.micronaut.http.annotation.ServerFilter; +import io.micronaut.http.filter.FilterContinuation; +import io.micronaut.scheduling.TaskExecutors; +import io.micronaut.scheduling.annotation.ExecuteOn; +// end::imports[] + +@Requires(property = "spec.filter", value = "TraceFilterContinuation") +@ServerFilter("/hello/**") +public class TraceFilter { + + private final TraceService traceService; + + public TraceFilter(TraceService traceService) { // <2> + this.traceService = traceService; + } + + // tag::doFilter[] + @RequestFilter + @ExecuteOn(TaskExecutors.BLOCKING) // <4> + public void filterRequest(HttpRequest request, FilterContinuation> continuation) { // <1> + traceService.trace(request); + MutableHttpResponse res = continuation.proceed(); // <2> + res.getHeaders().add("X-Trace-Enabled", "true"); // <3> + } + // end::doFilter[] +// tag::endclass[] +} +// end::endclass[] diff --git a/test-suite/src/test/java/io/micronaut/docs/server/filters/filtermethods/continuations/TraceFilterContinuationSpec.java b/test-suite/src/test/java/io/micronaut/docs/server/filters/filtermethods/continuations/TraceFilterContinuationSpec.java new file mode 100644 index 00000000000..fc9cdffe744 --- /dev/null +++ b/test-suite/src/test/java/io/micronaut/docs/server/filters/filtermethods/continuations/TraceFilterContinuationSpec.java @@ -0,0 +1,53 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.server.filters.filtermethods.continuations; + +import io.micronaut.context.ApplicationContext; +import io.micronaut.context.env.Environment; +import io.micronaut.docs.server.intro.HelloControllerSpec; +import io.micronaut.http.HttpRequest; +import io.micronaut.http.HttpResponse; +import io.micronaut.http.client.HttpClient; +import io.micronaut.runtime.server.EmbeddedServer; +import org.junit.Test; + +import java.util.HashMap; +import java.util.Map; + +import static org.junit.Assert.assertEquals; + +public class TraceFilterContinuationSpec { + @Test + public void testTraceFilterWithContinuations() { + Map map = new HashMap<>(); + map.put("spec.name", HelloControllerSpec.class.getSimpleName()); + map.put("spec.filter", "TraceFilterContinuation"); + map.put("spec.lang", "java"); + + EmbeddedServer server = ApplicationContext.run(EmbeddedServer.class, map, Environment.TEST); + HttpClient client = server + .getApplicationContext() + .createBean(HttpClient.class, server.getURL()); + + HttpResponse response = client.toBlocking().exchange(HttpRequest.GET("/hello")); + + assertEquals("true", response.getHeaders().get("X-Trace-Enabled")); + + server.stop(); + client.stop(); + } +} + From 35a0158f6c45a5fb4f1500cf049bc6410b2a2f16 Mon Sep 17 00:00:00 2001 From: Graeme Rocher Date: Mon, 27 Feb 2023 13:21:16 +0100 Subject: [PATCH 517/743] Remove more GraalVM build-time init flags (#8841) --- .../micronaut-context/native-image.properties | 4 +--- .../native-image.properties | 17 ----------------- 2 files changed, 1 insertion(+), 20 deletions(-) delete mode 100644 jackson-core/src/main/resources/META-INF/native-image/io.micronaut/micronaut-jackson-core/native-image.properties diff --git a/context/src/main/resources/META-INF/native-image/io.micronaut/micronaut-context/native-image.properties b/context/src/main/resources/META-INF/native-image/io.micronaut/micronaut-context/native-image.properties index 0809e7c396f..4ca2cfd4de3 100644 --- a/context/src/main/resources/META-INF/native-image/io.micronaut/micronaut-context/native-image.properties +++ b/context/src/main/resources/META-INF/native-image/io.micronaut/micronaut-context/native-image.properties @@ -14,6 +14,4 @@ # limitations under the License. # -Args = --install-exit-handlers \ - --initialize-at-build-time=org.reactivestreams,javax.xml \ - --initialize-at-build-time=com.sun.org.apache.xerces.internal.util,com.sun.org.apache.xerces.internal.impl,jdk.xml.internal,com.sun.xml.internal.stream.util,com.sun.org.apache.xerces.internal.xni,com.sun.org.apache.xerces.internal.utils +Args = --install-exit-handlers diff --git a/jackson-core/src/main/resources/META-INF/native-image/io.micronaut/micronaut-jackson-core/native-image.properties b/jackson-core/src/main/resources/META-INF/native-image/io.micronaut/micronaut-jackson-core/native-image.properties deleted file mode 100644 index 2f0b71f9c5f..00000000000 --- a/jackson-core/src/main/resources/META-INF/native-image/io.micronaut/micronaut-jackson-core/native-image.properties +++ /dev/null @@ -1,17 +0,0 @@ -# -# Copyright 2017-2021 original authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# - -Args = --initialize-at-build-time=com.fasterxml.jackson From b73f3ea8ffa84c894745de34dfc239752dabf106 Mon Sep 17 00:00:00 2001 From: Graeme Rocher Date: Mon, 27 Feb 2023 14:28:43 +0100 Subject: [PATCH 518/743] Fix broken MethodElement.overrides implementation (#8842) --- .../main/java/io/micronaut/inject/ast/MethodElement.java | 9 +++++++++ .../IntroductionInterfaceBeanElementCreator.java | 2 +- .../InterfaceConfigurationPropertiesSpec.groovy | 9 ++++++++- 3 files changed, 18 insertions(+), 2 deletions(-) diff --git a/core-processor/src/main/java/io/micronaut/inject/ast/MethodElement.java b/core-processor/src/main/java/io/micronaut/inject/ast/MethodElement.java index fc6353c7591..6b3880dc442 100644 --- a/core-processor/src/main/java/io/micronaut/inject/ast/MethodElement.java +++ b/core-processor/src/main/java/io/micronaut/inject/ast/MethodElement.java @@ -240,6 +240,15 @@ default boolean overrides(@NonNull MethodElement overridden) { if (this.equals(overridden) || isStatic() || overridden.isStatic()) { return false; } + ClassElement thisType = getDeclaringType(); + ClassElement thatType = overridden.getDeclaringType(); + if (thisType.getName().equals(thatType.getName())) { + return false; + } + if (!thisType.isAssignable(thatType)) { + // not a parent class + return false; + } MethodElement newMethod = this; if (newMethod.isAbstract() && !newMethod.isDefault() && (!overridden.isAbstract() || overridden.isDefault())) { return false; diff --git a/core-processor/src/main/java/io/micronaut/inject/processing/IntroductionInterfaceBeanElementCreator.java b/core-processor/src/main/java/io/micronaut/inject/processing/IntroductionInterfaceBeanElementCreator.java index d6b09bb1e81..21b1ad73bf9 100644 --- a/core-processor/src/main/java/io/micronaut/inject/processing/IntroductionInterfaceBeanElementCreator.java +++ b/core-processor/src/main/java/io/micronaut/inject/processing/IntroductionInterfaceBeanElementCreator.java @@ -67,7 +67,7 @@ public void buildInternal() { // The introduction will include overridden methods* (find(List) <- find(Iterable)*) but ordinary class introduction doesn't // Because of the caching we need to process declared methods first - List allMethods = new ArrayList<>(classElement.getEnclosedElements(ElementQuery.ALL_METHODS.includeOverriddenMethods().includeOverriddenMethods())); + List allMethods = new ArrayList<>(classElement.getEnclosedElements(ElementQuery.ALL_METHODS.includeOverriddenMethods())); List methods = new ArrayList<>(allMethods); List nonAbstractMethods = methods.stream().filter(m -> !m.isAbstract()).toList(); // Remove abstract methods overridden by non-abstract ones diff --git a/inject-java/src/test/groovy/io/micronaut/inject/configproperties/InterfaceConfigurationPropertiesSpec.groovy b/inject-java/src/test/groovy/io/micronaut/inject/configproperties/InterfaceConfigurationPropertiesSpec.groovy index f84c45f0e44..ae8f38873fc 100644 --- a/inject-java/src/test/groovy/io/micronaut/inject/configproperties/InterfaceConfigurationPropertiesSpec.groovy +++ b/inject-java/src/test/groovy/io/micronaut/inject/configproperties/InterfaceConfigurationPropertiesSpec.groovy @@ -19,15 +19,21 @@ class InterfaceConfigurationPropertiesSpec extends AbstractTypeElementSpec { package test; import io.micronaut.context.annotation.*; +import io.micronaut.core.bind.annotation.Bindable; +import io.micronaut.core.util.Toggleable; import java.time.Duration; @ConfigurationProperties("foo.bar") -interface MyConfig { +interface MyConfig extends Toggleable { @javax.validation.constraints.NotBlank String getHost(); @javax.validation.constraints.Min(10L) int getServerPort(); + + @Bindable(defaultValue = "true") + @Override + boolean isEnabled(); } ''') @@ -46,6 +52,7 @@ interface MyConfig { then: config.host == 'test' config.serverPort == 9999 + config.enabled cleanup: context.close() From be3f487b0013fe0492a758d352d7db905d395759 Mon Sep 17 00:00:00 2001 From: micronaut-build <65172877+micronaut-build@users.noreply.github.com> Date: Mon, 27 Feb 2023 15:49:44 +0100 Subject: [PATCH 519/743] build: micronaut-security to 3.9.3 (#8845) --- gradle.properties | 2 +- gradle/libs.versions.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle.properties b/gradle.properties index ea86da9d5eb..f3e4c40fdb0 100644 --- a/gradle.properties +++ b/gradle.properties @@ -52,7 +52,7 @@ kotlin.stdlib.default.dependency=false # For the docs graalVersion=22.0.0.2 -micronautSecurityVersion=3.9.1 +micronautSecurityVersion=3.9.3 org.gradle.caching=true org.gradle.parallel=true diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index d197fe88fd7..601599cd365 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -112,7 +112,7 @@ managed-micronaut-rss = "3.2.0" managed-micronaut-rxjava1 = "1.0.0" managed-micronaut-rxjava2 = "1.3.0" managed-micronaut-rxjava3 = "2.4.0" -managed-micronaut-security = "3.9.2" +managed-micronaut-security = "3.9.3" managed-micronaut-serialization = "1.5.0" managed-micronaut-servlet = "3.3.5" managed-micronaut-spring = "4.4.0" From d2fa30ac5e9c930e118da51dbb2c6d9bf7ba45ca Mon Sep 17 00:00:00 2001 From: Denis Stepanov Date: Mon, 27 Feb 2023 10:40:39 -0500 Subject: [PATCH 520/743] Support type annotations for KSP, generics improvements and bugfixes. (#8837) --- .editorconfig | 24 + .../visitor/ConfigurationReaderVisitor.java | 2 +- .../AbstractAnnotationMetadataBuilder.java | 2 +- .../io/micronaut/inject/ast/ClassElement.java | 1 + .../micronaut/inject/ast/ElementFactory.java | 8 +- .../io/micronaut/inject/ast/TypedElement.java | 9 + .../annotation/AbstractAnnotationElement.java | 141 + ...cPlaceholderElementAnnotationMetadata.java | 67 + .../PropertyElementAnnotationMetadata.java | 174 ++ .../WildcardElementAnnotationMetadata.java | 67 + .../ast/utils/EnclosedElementsQuery.java | 34 +- .../visitor/BeanIntrospectionWriter.java | 2 +- .../inject/processing/JavaModelUtils.java | 10 +- .../writer/AbstractClassFileWriter.java | 39 +- .../inject/writer/BeanDefinitionWriter.java | 7 +- inject-groovy/README.md | 3 - inject-groovy/build.gradle | 3 +- .../groovy/visitor/AbstractGroovyElement.java | 102 +- .../groovy/visitor/GroovyClassElement.java | 16 +- .../groovy/visitor/GroovyFieldElement.java | 37 +- .../GroovyGenericPlaceholderElement.java | 32 +- .../groovy/visitor/GroovyMethodElement.java | 28 +- .../groovy/visitor/GroovyPropertyElement.java | 131 +- .../groovy/visitor/GroovyWildcardElement.java | 32 +- .../modify/AnnotateFieldTypeSpec.groovy | 51 +- .../JavaElementAnnotationMetadataFactory.java | 4 +- .../visitor/AbstractJavaElement.java | 105 +- .../processing/visitor/JavaClassElement.java | 32 +- .../visitor/JavaConstructorElement.java | 6 +- .../JavaGenericPlaceholderElement.java | 30 +- .../visitor/JavaPropertyElement.java | 128 +- .../visitor/JavaVisitorContext.java | 3 +- .../visitor/JavaWildcardElement.java | 31 +- .../annotation/AnnotateFieldTypeSpec.groovy | 9 +- .../test/AbstractKotlinCompilerSpec.groovy | 51 +- inject-kotlin/build.gradle | 1 + .../kotlin/processing/KotlinOutputVisitor.kt | 4 +- .../KotlinAnnotationMetadataBuilder.kt | 137 +- .../annotation/KotlinAnnotations.kt | 55 + .../KotlinElementAnnotationMetadataFactory.kt | 164 +- .../beans/BeanDefinitionProcessor.kt | 15 +- .../beans/BeanDefinitionProcessorProvider.kt | 2 +- .../micronaut/kotlin/processing/extensions.kt | 28 +- .../visitor/AbstractKotlinElement.kt | 636 +++-- .../visitor/AbstractKotlinMethodElement.kt | 141 + ...ractKotlinPropertyAccessorMethodElement.kt | 62 + .../visitor/AbstractKotlinPropertyElement.kt | 126 + .../visitor/KSAnnotatedReference.kt | 103 - .../processing/visitor/KotlinClassElement.kt | 888 +++---- .../visitor/KotlinConstructorElement.kt | 22 +- .../visitor/KotlinElementFactory.kt | 193 +- .../visitor/KotlinEnumConstructorElement.kt | 3 +- .../processing/visitor/KotlinEnumElement.kt | 58 +- .../processing/visitor/KotlinFieldElement.kt | 104 +- .../KotlinGenericPlaceholderElement.kt | 156 +- .../processing/visitor/KotlinMethodElement.kt | 389 +-- .../processing/visitor/KotlinNativeElement.kt | 244 ++ .../visitor/KotlinParameterElement.kt | 87 +- .../visitor/KotlinPropertyElement.kt | 556 +--- .../KotlinPropertyGetterMethodElement.kt | 95 + .../KotlinPropertySetterMethodElement.kt | 113 + .../visitor/KotlinSimplePropertyElement.kt | 91 + .../visitor/KotlinTypeArgumentElement.kt | 95 + .../visitor/KotlinVisitorContext.kt | 66 +- .../visitor/KotlinWildcardElement.kt | 73 +- .../processing/visitor/LoadedVisitor.kt | 16 +- .../visitor/TypeElementSymbolProcessor.kt | 72 +- .../TypeElementSymbolProcessorProvider.kt | 2 +- .../annotations/AnnotateFieldSpec.groovy | 117 + .../annotations/AnnotateFieldTypeSpec.groovy | 207 ++ .../AnnotateMethodParameterSpec.groovy | 251 ++ .../AnnotateMethodReturnSpec.groovy | 255 ++ .../annotations/AnnotateMethodSpec.groovy | 86 + .../processing/annotations/MyAnnotation.java | 28 + .../beans/BeanDefinitionSpec.groovy | 82 + .../executable/ExecutableBeanSpec.groovy | 543 ++++ .../processing/beans/executable/MyBook.java | 8 + .../processing/beans/executable/MyEntity.java | 15 + .../beans/executable/TypeUseRuntimeAnn.java | 11 + .../InheritedExecutableSpec.groovy | 5 +- .../inject/ast/ClassElementSpec.groovy | 1433 ++++++++++- .../kotlin/processing/inject/ast/MyBook.java | 8 + .../processing/inject/ast/MyEntity.java | 15 + .../processing/inject/ast/MyParameter.java | 20 + .../inject/ast/TypeFieldRuntimeAnn.java | 11 + .../inject/ast/TypeMethodRuntimeAnn.java | 11 + .../inject/ast/TypeParameterRuntimeAnn.java | 11 + .../inject/ast/TypeUseClassAnn.java | 11 + .../inject/ast/TypeUseRuntimeAnn.java | 11 + .../ConfigPropertiesParseSpec.groovy | 2270 +++++++++-------- .../ConfigurationPropertiesBuilderSpec.groovy | 1560 +++++------ .../visitor/AllElementsVisitor.java | 122 + .../visitor/BeanIntrospectionSpec.groovy | 93 +- .../visitor/KotlinReconstructionSpec.groovy | 607 ++++- .../processing/visitor/TypeUseClassAnn.java | 11 + .../processing/visitor/TypeUseRuntimeAnn.java | 11 + .../kotlin/processing/TypeUseRuntimeAnn.java | 11 + ...icronaut.inject.visitor.TypeElementVisitor | 6 + 98 files changed, 9272 insertions(+), 4776 deletions(-) create mode 100644 core-processor/src/main/java/io/micronaut/inject/ast/annotation/AbstractAnnotationElement.java create mode 100644 core-processor/src/main/java/io/micronaut/inject/ast/annotation/GenericPlaceholderElementAnnotationMetadata.java create mode 100644 core-processor/src/main/java/io/micronaut/inject/ast/annotation/PropertyElementAnnotationMetadata.java create mode 100644 core-processor/src/main/java/io/micronaut/inject/ast/annotation/WildcardElementAnnotationMetadata.java delete mode 100644 inject-groovy/README.md create mode 100644 inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/annotation/KotlinAnnotations.kt create mode 100644 inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/AbstractKotlinMethodElement.kt create mode 100644 inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/AbstractKotlinPropertyAccessorMethodElement.kt create mode 100644 inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/AbstractKotlinPropertyElement.kt delete mode 100644 inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/KSAnnotatedReference.kt create mode 100644 inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/KotlinNativeElement.kt create mode 100644 inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/KotlinPropertyGetterMethodElement.kt create mode 100644 inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/KotlinPropertySetterMethodElement.kt create mode 100644 inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/KotlinSimplePropertyElement.kt create mode 100644 inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/KotlinTypeArgumentElement.kt create mode 100644 inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/annotations/AnnotateFieldSpec.groovy create mode 100644 inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/annotations/AnnotateFieldTypeSpec.groovy create mode 100644 inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/annotations/AnnotateMethodParameterSpec.groovy create mode 100644 inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/annotations/AnnotateMethodReturnSpec.groovy create mode 100644 inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/annotations/AnnotateMethodSpec.groovy create mode 100644 inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/annotations/MyAnnotation.java create mode 100644 inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/beans/executable/MyBook.java create mode 100644 inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/beans/executable/MyEntity.java create mode 100644 inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/beans/executable/TypeUseRuntimeAnn.java create mode 100644 inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/inject/ast/MyBook.java create mode 100644 inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/inject/ast/MyEntity.java create mode 100644 inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/inject/ast/MyParameter.java create mode 100644 inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/inject/ast/TypeFieldRuntimeAnn.java create mode 100644 inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/inject/ast/TypeMethodRuntimeAnn.java create mode 100644 inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/inject/ast/TypeParameterRuntimeAnn.java create mode 100644 inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/inject/ast/TypeUseClassAnn.java create mode 100644 inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/inject/ast/TypeUseRuntimeAnn.java create mode 100644 inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/visitor/AllElementsVisitor.java create mode 100644 inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/visitor/TypeUseClassAnn.java create mode 100644 inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/visitor/TypeUseRuntimeAnn.java create mode 100644 inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/TypeUseRuntimeAnn.java create mode 100644 inject-kotlin/src/test/resources/META-INF/services/io.micronaut.inject.visitor.TypeElementVisitor diff --git a/.editorconfig b/.editorconfig index a9a165bae9f..573a247281b 100644 --- a/.editorconfig +++ b/.editorconfig @@ -19,5 +19,29 @@ max_line_length = 100 # Import order can be configured with ij_java_imports_layout=... # See documentation https://youtrack.jetbrains.com/issue/IDEA-170643#focus=streamItem-27-3708697.0-0 +# don't use wildcard for imports +ij_java_class_count_to_use_import_on_demand = 9999 +ij_java_names_count_to_use_import_on_demand = 9999 + +[*.kt] +indent_size = 4 +tab_width = 4 +ij_continuation_indent_size = 8 +max_line_length = 100 + +# don't use wildcard for imports +ij_kotlin_name_count_to_use_star_import = 9999 +ij_kotlin_name_count_to_use_star_import_for_members = 9999 + +[*.groovy] +indent_size = 4 +tab_width = 4 +ij_continuation_indent_size = 8 +max_line_length = 100 + +# don't use wildcard for imports +ij_groovy_class_count_to_use_import_on_demand = 9999 +ij_groovy_names_count_to_use_import_on_demand = 9999 + [*.xml] indent_size = 4 diff --git a/core-processor/src/main/java/io/micronaut/context/visitor/ConfigurationReaderVisitor.java b/core-processor/src/main/java/io/micronaut/context/visitor/ConfigurationReaderVisitor.java index 29164f17f9e..1462a872996 100644 --- a/core-processor/src/main/java/io/micronaut/context/visitor/ConfigurationReaderVisitor.java +++ b/core-processor/src/main/java/io/micronaut/context/visitor/ConfigurationReaderVisitor.java @@ -140,7 +140,7 @@ private void visitAbstractMethod(MethodElement method, VisitorContext context) { context.fail("Only zero argument getter methods are allowed on @ConfigurationProperties interfaces: " + method, method); return; } - if ("void".equals(method.getReturnType().getName())) { + if (method.getReturnType().isVoid()) { context.fail("Getter methods must return a value @ConfigurationProperties interfaces: " + method, method); return; } diff --git a/core-processor/src/main/java/io/micronaut/inject/annotation/AbstractAnnotationMetadataBuilder.java b/core-processor/src/main/java/io/micronaut/inject/annotation/AbstractAnnotationMetadataBuilder.java index 6c134a8cd34..52d01147fd0 100644 --- a/core-processor/src/main/java/io/micronaut/inject/annotation/AbstractAnnotationMetadataBuilder.java +++ b/core-processor/src/main/java/io/micronaut/inject/annotation/AbstractAnnotationMetadataBuilder.java @@ -267,7 +267,7 @@ public CachedAnnotationMetadata lookupOrBuild(Object key, T element) { * @return The annotation metadata * @since 4.0.0 */ - public CachedAnnotationMetadata lookupOrBuild(Object key, T element, boolean includeTypeAnnotations) { + private CachedAnnotationMetadata lookupOrBuild(Object key, T element, boolean includeTypeAnnotations) { CachedAnnotationMetadata cachedAnnotationMetadata = MUTATED_ANNOTATION_METADATA.get(key); if (cachedAnnotationMetadata == null) { AnnotationMetadata annotationMetadata = buildInternal(includeTypeAnnotations, false, element); diff --git a/core-processor/src/main/java/io/micronaut/inject/ast/ClassElement.java b/core-processor/src/main/java/io/micronaut/inject/ast/ClassElement.java index 86fc079a052..28ce24ac268 100644 --- a/core-processor/src/main/java/io/micronaut/inject/ast/ClassElement.java +++ b/core-processor/src/main/java/io/micronaut/inject/ast/ClassElement.java @@ -646,6 +646,7 @@ default ClassElement withBoundGenericTypes(@NonNull List * @since 3.1.0 */ @Experimental + @Nullable default ClassElement foldBoundGenericTypes(@NonNull Function fold) { List typeArgs = getBoundGenericTypes().stream().map(arg -> arg.foldBoundGenericTypes(fold)).toList(); if (typeArgs.contains(null)) { diff --git a/core-processor/src/main/java/io/micronaut/inject/ast/ElementFactory.java b/core-processor/src/main/java/io/micronaut/inject/ast/ElementFactory.java index 6ffc5e37ec5..ccf385f82a6 100644 --- a/core-processor/src/main/java/io/micronaut/inject/ast/ElementFactory.java +++ b/core-processor/src/main/java/io/micronaut/inject/ast/ElementFactory.java @@ -48,16 +48,18 @@ public interface ElementFactory { * * @param type The type * @param annotationMetadataFactory The element annotation metadata factory - * @param resolvedGenerics The resolved generics + * @param typeArguments The resolved generics * @return The class element * @since 4.0.0 * @deprecated no longer used */ @NonNull @Deprecated - ClassElement newClassElement(@NonNull C type, + default ClassElement newClassElement(@NonNull C type, @NonNull ElementAnnotationMetadataFactory annotationMetadataFactory, - @NonNull Map resolvedGenerics); + @NonNull Map typeArguments) { + return newClassElement(type, annotationMetadataFactory).withTypeArguments(typeArguments); + } /** * Builds a new source class element for the given type. This method diff --git a/core-processor/src/main/java/io/micronaut/inject/ast/TypedElement.java b/core-processor/src/main/java/io/micronaut/inject/ast/TypedElement.java index 09b94594827..f069b45becf 100644 --- a/core-processor/src/main/java/io/micronaut/inject/ast/TypedElement.java +++ b/core-processor/src/main/java/io/micronaut/inject/ast/TypedElement.java @@ -55,6 +55,15 @@ default boolean isPrimitive() { return false; } + /** + * Whether the type is void. + * @return True if it is + * @since 4.0.0 + */ + default boolean isVoid() { + return this == PrimitiveElement.VOID; + } + /** * Is the type an array. * @return True if it is. diff --git a/core-processor/src/main/java/io/micronaut/inject/ast/annotation/AbstractAnnotationElement.java b/core-processor/src/main/java/io/micronaut/inject/ast/annotation/AbstractAnnotationElement.java new file mode 100644 index 00000000000..a2c573fe463 --- /dev/null +++ b/core-processor/src/main/java/io/micronaut/inject/ast/annotation/AbstractAnnotationElement.java @@ -0,0 +1,141 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.inject.ast.annotation; + +import io.micronaut.core.annotation.AnnotationMetadata; +import io.micronaut.core.annotation.AnnotationValue; +import io.micronaut.core.annotation.AnnotationValueBuilder; +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.Nullable; + +import java.lang.annotation.Annotation; +import java.util.function.Consumer; +import java.util.function.Predicate; + +/** + * An abstract element. + * + * @author Denis Stepanov + * @since 4.0.0 + */ +@Internal +public abstract class AbstractAnnotationElement implements io.micronaut.inject.ast.Element { + + protected final ElementAnnotationMetadataFactory elementAnnotationMetadataFactory; + @Nullable + protected AnnotationMetadata presetAnnotationMetadata; + @Nullable + private ElementAnnotationMetadata elementAnnotationMetadata; + + /** + * @param annotationMetadataFactory The annotation metadata factory + */ + protected AbstractAnnotationElement(ElementAnnotationMetadataFactory annotationMetadataFactory) { + this.elementAnnotationMetadataFactory = annotationMetadataFactory; + } + + @Override + public AnnotationMetadata getAnnotationMetadata() { + return getElementAnnotationMetadata(); + } + + public final ElementAnnotationMetadataFactory getElementAnnotationMetadataFactory() { + return elementAnnotationMetadataFactory; + } + + /** + * @return The element's annotation metadata + */ + protected ElementAnnotationMetadata getElementAnnotationMetadata() { + if (elementAnnotationMetadata == null) { + if (presetAnnotationMetadata == null) { + elementAnnotationMetadata = elementAnnotationMetadataFactory.build(this); + } else { + elementAnnotationMetadata = elementAnnotationMetadataFactory.build(this, presetAnnotationMetadata); + } + } + return elementAnnotationMetadata; + } + + /** + * Get annotation metadata to add or remove annotations. + * + * @return The annotation metadata to write + */ + protected MutableAnnotationMetadataDelegate getAnnotationMetadataToWrite() { + return getElementAnnotationMetadata(); + } + + @Override + public io.micronaut.inject.ast.Element annotate(String annotationType, Consumer> consumer) { + getAnnotationMetadataToWrite().annotate(annotationType, consumer); + return this; + } + + @Override + public io.micronaut.inject.ast.Element removeAnnotation(String annotationType) { + getAnnotationMetadataToWrite().removeAnnotation(annotationType); + return this; + } + + @Override + public io.micronaut.inject.ast.Element removeAnnotation(Class annotationType) { + getAnnotationMetadataToWrite().removeAnnotation(annotationType); + return this; + } + + @Override + public io.micronaut.inject.ast.Element removeAnnotationIf(Predicate> predicate) { + getAnnotationMetadataToWrite().removeAnnotationIf(predicate); + return this; + } + + @Override + public io.micronaut.inject.ast.Element removeStereotype(String annotationType) { + getAnnotationMetadataToWrite().removeStereotype(annotationType); + return this; + } + + @Override + public io.micronaut.inject.ast.Element removeStereotype(Class annotationType) { + getAnnotationMetadataToWrite().removeStereotype(annotationType); + return this; + } + + @Override + public io.micronaut.inject.ast.Element annotate(String annotationType) { + getAnnotationMetadataToWrite().annotate(annotationType); + return this; + } + + @Override + public io.micronaut.inject.ast.Element annotate(Class annotationType, Consumer> consumer) { + getAnnotationMetadataToWrite().annotate(annotationType, consumer); + return this; + } + + @Override + public io.micronaut.inject.ast.Element annotate(Class annotationType) { + getAnnotationMetadataToWrite().annotate(annotationType); + return this; + } + + @Override + public io.micronaut.inject.ast.Element annotate(AnnotationValue annotationValue) { + getAnnotationMetadataToWrite().annotate(annotationValue); + return this; + } +} diff --git a/core-processor/src/main/java/io/micronaut/inject/ast/annotation/GenericPlaceholderElementAnnotationMetadata.java b/core-processor/src/main/java/io/micronaut/inject/ast/annotation/GenericPlaceholderElementAnnotationMetadata.java new file mode 100644 index 00000000000..3b4a4417e97 --- /dev/null +++ b/core-processor/src/main/java/io/micronaut/inject/ast/annotation/GenericPlaceholderElementAnnotationMetadata.java @@ -0,0 +1,67 @@ +/* + * Copyright 2017-2023 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.inject.ast.annotation; + +import io.micronaut.core.annotation.AnnotationMetadata; +import io.micronaut.core.annotation.Internal; +import io.micronaut.inject.annotation.AnnotationMetadataHierarchy; +import io.micronaut.inject.ast.ClassElement; +import io.micronaut.inject.ast.GenericPlaceholderElement; + +import java.util.ArrayList; +import java.util.List; + +/** + * The element annotation metadata for generic placeholder element. + * + * @author Denis Stepanov + * @since 4.0.0 + */ +@Internal +public final class GenericPlaceholderElementAnnotationMetadata extends AbstractElementAnnotationMetadata { + + private final GenericPlaceholderElement genericPlaceholderElement; + private final ClassElement representingClassElement; + private AnnotationMetadata annotationMetadata; + + public GenericPlaceholderElementAnnotationMetadata(GenericPlaceholderElement genericPlaceholderElement, + ClassElement representingClassElement) { + this.genericPlaceholderElement = genericPlaceholderElement; + this.representingClassElement = representingClassElement; + } + + public AnnotationMetadata getReturnInstance() { + return getAnnotationMetadata(); + } + + @Override + protected MutableAnnotationMetadataDelegate getAnnotationMetadataToWrite() { + return genericPlaceholderElement.getGenericTypeAnnotationMetadata(); + } + + @Override + public AnnotationMetadata getAnnotationMetadata() { + if (annotationMetadata == null) { + List allAnnotationMetadata = new ArrayList<>(); + genericPlaceholderElement.getBounds().forEach(ce -> allAnnotationMetadata.add(ce.getTypeAnnotationMetadata())); + allAnnotationMetadata.add(representingClassElement.getTypeAnnotationMetadata()); + allAnnotationMetadata.add(genericPlaceholderElement.getGenericTypeAnnotationMetadata()); + genericPlaceholderElement.getResolved().ifPresent(ClassElement::getTypeAnnotationMetadata); + annotationMetadata = new AnnotationMetadataHierarchy(true, allAnnotationMetadata.toArray(AnnotationMetadata[]::new)); + } + return annotationMetadata; + } +} diff --git a/core-processor/src/main/java/io/micronaut/inject/ast/annotation/PropertyElementAnnotationMetadata.java b/core-processor/src/main/java/io/micronaut/inject/ast/annotation/PropertyElementAnnotationMetadata.java new file mode 100644 index 00000000000..7f45f56275a --- /dev/null +++ b/core-processor/src/main/java/io/micronaut/inject/ast/annotation/PropertyElementAnnotationMetadata.java @@ -0,0 +1,174 @@ +/* + * Copyright 2017-2023 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.inject.ast.annotation; + +import io.micronaut.core.annotation.AnnotationMetadata; +import io.micronaut.core.annotation.AnnotationMetadataDelegate; +import io.micronaut.core.annotation.AnnotationValue; +import io.micronaut.core.annotation.AnnotationValueBuilder; +import io.micronaut.core.annotation.NonNull; +import io.micronaut.core.annotation.Nullable; +import io.micronaut.inject.annotation.AnnotationMetadataHierarchy; +import io.micronaut.inject.ast.FieldElement; +import io.micronaut.inject.ast.MethodElement; +import io.micronaut.inject.ast.ParameterElement; + +import java.lang.annotation.Annotation; +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; +import java.util.function.Predicate; + +/** + * The element annotation metadata for property element. + * + * @author Denis Stepanov + * @since 4.0.0 + */ +public final class PropertyElementAnnotationMetadata implements ElementAnnotationMetadata { + + private final io.micronaut.inject.ast.Element thisElement; + private final List> elements; + private final AnnotationMetadata propertyAnnotationMetadata; + + public PropertyElementAnnotationMetadata(@NonNull + io.micronaut.inject.ast.Element thisElement, + @Nullable + MethodElement getter, + @Nullable + MethodElement setter, + @Nullable + FieldElement field, + @Nullable + ParameterElement constructorParameter, + boolean includeSynthetic) { + + this.thisElement = thisElement; + List> elements = new ArrayList<>(3); + if (setter != null && (!setter.isSynthetic() || includeSynthetic)) { + elements.add(setter); + ParameterElement[] parameters = setter.getParameters(); + if (parameters.length > 0) { + ParameterElement parameter = parameters[0]; + MutableAnnotationMetadataDelegate typeAnnotationMetadata = parameter.getType().getTypeAnnotationMetadata(); + if (!typeAnnotationMetadata.isEmpty()) { + elements.add(typeAnnotationMetadata); + } + } + } + if (constructorParameter != null) { + elements.add(constructorParameter); + MutableAnnotationMetadataDelegate typeAnnotationMetadata = constructorParameter.getType().getTypeAnnotationMetadata(); + if (!typeAnnotationMetadata.isEmpty()) { + elements.add(typeAnnotationMetadata); + } + } + if (field != null && (!field.isSynthetic() || includeSynthetic)) { + elements.add(field); + MutableAnnotationMetadataDelegate typeAnnotationMetadata = field.getType().getTypeAnnotationMetadata(); + if (!typeAnnotationMetadata.isEmpty()) { + elements.add(typeAnnotationMetadata); + } + } + if (getter != null && (!getter.isSynthetic() || includeSynthetic)) { + elements.add(getter); + MutableAnnotationMetadataDelegate typeAnnotationMetadata = getter.getReturnType().getTypeAnnotationMetadata(); + if (!typeAnnotationMetadata.isEmpty()) { + elements.add(typeAnnotationMetadata); + } + } + + // The instance AnnotationMetadata of each element can change after a modification + // Set annotation metadata as actual elements so the changes are reflected + AnnotationMetadata[] hierarchy = elements.stream().map(e -> { + if (e instanceof MethodElement methodElement) { + return new AnnotationMetadataDelegate() { + @Override + public AnnotationMetadata getAnnotationMetadata() { + AnnotationMetadata annotationMetadata = methodElement.getAnnotationMetadata(); + // Exclude type metadata + return annotationMetadata.getDeclaredMetadata(); + } + }; + } + return e; + }).toArray(AnnotationMetadata[]::new); + this.propertyAnnotationMetadata = + hierarchy.length == 1 ? hierarchy[0] : new AnnotationMetadataHierarchy(true, hierarchy); + this.elements = elements; + } + + @Override + public io.micronaut.inject.ast.Element annotate(AnnotationValue annotationValue) { + for (MutableAnnotationMetadataDelegate am : elements) { + am.annotate(annotationValue); + } + return thisElement; + } + + @Override + public io.micronaut.inject.ast.Element annotate(String annotationType, Consumer> consumer) { + for (MutableAnnotationMetadataDelegate am : elements) { + am.annotate(annotationType, consumer); + } + return thisElement; + } + + @Override + public io.micronaut.inject.ast.Element annotate(Class annotationType) { + for (MutableAnnotationMetadataDelegate am : elements) { + am.annotate(annotationType); + } + return thisElement; + } + + @Override + public io.micronaut.inject.ast.Element annotate(String annotationType) { + for (MutableAnnotationMetadataDelegate am : elements) { + am.annotate(annotationType); + } + return thisElement; + } + + @Override + public io.micronaut.inject.ast.Element annotate(Class annotationType, Consumer> consumer) { + for (MutableAnnotationMetadataDelegate am : elements) { + am.annotate(annotationType, consumer); + } + return thisElement; + } + + @Override + public io.micronaut.inject.ast.Element removeAnnotation(String annotationType) { + for (MutableAnnotationMetadataDelegate am : elements) { + am.removeAnnotation(annotationType); + } + return thisElement; + } + + @Override + public io.micronaut.inject.ast.Element removeAnnotationIf(Predicate> predicate) { + for (MutableAnnotationMetadataDelegate am : elements) { + am.removeAnnotationIf(predicate); + } + return thisElement; + } + + @Override + public AnnotationMetadata getAnnotationMetadata() { + return propertyAnnotationMetadata; + } +} diff --git a/core-processor/src/main/java/io/micronaut/inject/ast/annotation/WildcardElementAnnotationMetadata.java b/core-processor/src/main/java/io/micronaut/inject/ast/annotation/WildcardElementAnnotationMetadata.java new file mode 100644 index 00000000000..44fb25e67a7 --- /dev/null +++ b/core-processor/src/main/java/io/micronaut/inject/ast/annotation/WildcardElementAnnotationMetadata.java @@ -0,0 +1,67 @@ +/* + * Copyright 2017-2023 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.inject.ast.annotation; + +import io.micronaut.core.annotation.AnnotationMetadata; +import io.micronaut.core.annotation.Internal; +import io.micronaut.inject.annotation.AnnotationMetadataHierarchy; +import io.micronaut.inject.ast.ClassElement; +import io.micronaut.inject.ast.WildcardElement; + +import java.util.ArrayList; +import java.util.List; + +/** + * The element annotation metadata for wildcard element. + * + * @author Denis Stepanov + * @since 4.0.0 + */ +@Internal +public final class WildcardElementAnnotationMetadata extends AbstractElementAnnotationMetadata { + + private final WildcardElement wildcardElement; + private final ClassElement representingClassElement; + private AnnotationMetadata annotationMetadata; + + public WildcardElementAnnotationMetadata(WildcardElement wildcardElement, + ClassElement representingClassElement) { + this.wildcardElement = wildcardElement; + this.representingClassElement = representingClassElement; + } + + public AnnotationMetadata getReturnInstance() { + return getAnnotationMetadata(); + } + + @Override + protected MutableAnnotationMetadataDelegate getAnnotationMetadataToWrite() { + return wildcardElement.getGenericTypeAnnotationMetadata(); + } + + @Override + public AnnotationMetadata getAnnotationMetadata() { + if (annotationMetadata == null) { + List allAnnotationMetadata = new ArrayList<>(); + wildcardElement.getLowerBounds().forEach(ce -> allAnnotationMetadata.add(ce.getTypeAnnotationMetadata())); + wildcardElement.getUpperBounds().forEach(ce -> allAnnotationMetadata.add(ce.getTypeAnnotationMetadata())); + allAnnotationMetadata.add(representingClassElement.getTypeAnnotationMetadata()); + allAnnotationMetadata.add(wildcardElement.getGenericTypeAnnotationMetadata()); + annotationMetadata = new AnnotationMetadataHierarchy(true, allAnnotationMetadata.toArray(AnnotationMetadata[]::new)); + } + return annotationMetadata; + } +} diff --git a/core-processor/src/main/java/io/micronaut/inject/ast/utils/EnclosedElementsQuery.java b/core-processor/src/main/java/io/micronaut/inject/ast/utils/EnclosedElementsQuery.java index 0caa4467840..68a078ae24c 100644 --- a/core-processor/src/main/java/io/micronaut/inject/ast/utils/EnclosedElementsQuery.java +++ b/core-processor/src/main/java/io/micronaut/inject/ast/utils/EnclosedElementsQuery.java @@ -54,7 +54,7 @@ public abstract class EnclosedElementsQuery { private static final int MAX_ITEMS_IN_CACHE = 200; - private final Map elementsCache = new LinkedHashMap<>(); + private final Map elementsCache = new LinkedHashMap<>(); /** * Get native class element. @@ -214,17 +214,33 @@ private Collection getAllElements Set addedFromClassElements = new LinkedHashSet<>(); classElements: for (N element : classElements) { - N cacheKey = getCacheKey(element); - T newElement = (T) elementsCache.computeIfAbsent(cacheKey, e -> this.toAstElement(e, result.getElementType())); + N nativeType = getCacheKey(element); + CacheKey cacheKey = new CacheKey(result.getElementType(), nativeType); + T newElement = (T) elementsCache.computeIfAbsent(cacheKey, ck -> toAstElement(nativeType, result.getElementType())); + if (result.getElementType() == MemberElement.class) { + // Also cache members query results as it's original element type + if (newElement instanceof FieldElement) { + elementsCache.putIfAbsent(new CacheKey(FieldElement.class, nativeType), newElement); + } else if (newElement instanceof ConstructorElement) { + elementsCache.putIfAbsent(new CacheKey(ConstructorElement.class, nativeType), newElement); + elementsCache.putIfAbsent(new CacheKey(MethodElement.class, nativeType), newElement); + } else if (newElement instanceof MethodElement) { + elementsCache.putIfAbsent(new CacheKey(MethodElement.class, nativeType), newElement); + } else if (newElement instanceof PropertyElement) { + elementsCache.putIfAbsent(new CacheKey(PropertyElement.class, nativeType), newElement); + } + } else if (MemberElement.class.isAssignableFrom(result.getElementType())) { + elementsCache.putIfAbsent(new CacheKey(MemberElement.class, nativeType), newElement); + } if (elementsCache.size() == MAX_ITEMS_IN_CACHE) { - Iterator> iterator = elementsCache.entrySet().iterator(); + Iterator> iterator = elementsCache.entrySet().iterator(); iterator.next(); iterator.remove(); } if (!result.getElementType().isInstance(newElement)) { // dirty cache elementsCache.remove(cacheKey); - newElement = (T) elementsCache.computeIfAbsent(cacheKey, e -> this.toAstElement(e, result.getElementType())); + newElement = (T) elementsCache.computeIfAbsent(cacheKey, ck -> toAstElement(nativeType, result.getElementType())); } for (Iterator iterator = elements.iterator(); iterator.hasNext(); ) { T existingElement = iterator.next(); @@ -246,7 +262,7 @@ private Collection getAllElements } /** - * get the cache key. + * Get the cache key. * * @param element The element * @return The cache key @@ -325,11 +341,13 @@ protected Set getExcludedNativeElements(@NonNull ElementQuery.Result resul /** * Converts the native element to the AST element. * - * @param enclosedElement The native element. + * @param nativeType The native element. * @param elementType The result type * @return The AST element */ @NonNull - protected abstract io.micronaut.inject.ast.Element toAstElement(N enclosedElement, Class elementType); + protected abstract io.micronaut.inject.ast.Element toAstElement(N nativeType, Class elementType); + private record CacheKey(Class elementType, Object nativeType) { + } } diff --git a/core-processor/src/main/java/io/micronaut/inject/beans/visitor/BeanIntrospectionWriter.java b/core-processor/src/main/java/io/micronaut/inject/beans/visitor/BeanIntrospectionWriter.java index 01f62bab50f..358115b3331 100644 --- a/core-processor/src/main/java/io/micronaut/inject/beans/visitor/BeanIntrospectionWriter.java +++ b/core-processor/src/main/java/io/micronaut/inject/beans/visitor/BeanIntrospectionWriter.java @@ -1149,7 +1149,7 @@ public void writeDispatchOne(GeneratorAdapter writer) { if (writeDispatch instanceof DispatchWriter.MethodDispatchTarget) { MethodElement writeMethod = ((DispatchWriter.MethodDispatchTarget) writeDispatch).getMethodElement(); ClassElement writeReturnType = invokeMethod(writer, writeMethod); - if (!writeReturnType.getName().equals("void")) { + if (!writeReturnType.isVoid()) { writer.pop(); } } else if (writeDispatch instanceof DispatchWriter.FieldSetDispatchTarget) { diff --git a/core-processor/src/main/java/io/micronaut/inject/processing/JavaModelUtils.java b/core-processor/src/main/java/io/micronaut/inject/processing/JavaModelUtils.java index f14a34db08d..ff63bad19e3 100644 --- a/core-processor/src/main/java/io/micronaut/inject/processing/JavaModelUtils.java +++ b/core-processor/src/main/java/io/micronaut/inject/processing/JavaModelUtils.java @@ -268,7 +268,15 @@ public static boolean isRecordComponent(Element e) { public static Type getTypeReference(TypedElement type) { ClassElement classElement = type.getType(); if (classElement.isPrimitive()) { - String internalName = NAME_TO_TYPE_MAP.get(classElement.getName()); + String internalName; + if (classElement.isVoid()) { + internalName = NAME_TO_TYPE_MAP.get("void"); + } else { + internalName = NAME_TO_TYPE_MAP.get(classElement.getName()); + } + if (internalName == null) { + throw new IllegalStateException("Unrecognized primitive type: " + classElement.getName()); + } if (classElement.isArray()) { StringBuilder name = new StringBuilder(internalName); for (int i = 0; i < classElement.getArrayDimensions(); i++) { diff --git a/core-processor/src/main/java/io/micronaut/inject/writer/AbstractClassFileWriter.java b/core-processor/src/main/java/io/micronaut/inject/writer/AbstractClassFileWriter.java index ec4395b9ac0..e1923e42785 100644 --- a/core-processor/src/main/java/io/micronaut/inject/writer/AbstractClassFileWriter.java +++ b/core-processor/src/main/java/io/micronaut/inject/writer/AbstractClassFileWriter.java @@ -251,7 +251,7 @@ private static void pushTypeArgumentElements( Set visitedTypes, Map defaults, Map loadTypeMethods) { - if (element == null || element.getClass().getSimpleName().equals("KotlinClassElement")) { + if (element == null) { if (visitedTypes.contains(declaringElementName)) { generatorAdapter.getStatic( TYPE_ARGUMENT, @@ -403,10 +403,6 @@ protected static void buildArgumentWithGenerics( // Persist only type annotations added to the type argument AnnotationMetadata annotationMetadata = MutableAnnotationMetadata.of(classElement.getTypeAnnotationMetadata()); - if (classElement.getClass().getSimpleName().startsWith("Kotlin")) { - // Remove after KSP supports type annotations - annotationMetadata = MutableAnnotationMetadata.of(classElement.getAnnotationMetadata()); - } boolean hasAnnotationMetadata = !annotationMetadata.isEmpty(); boolean isRecursiveType = false; @@ -578,20 +574,27 @@ protected void pushReturnTypeArgument(Type owningType, // Persist only type annotations added AnnotationMetadata annotationMetadata = argument.getTypeAnnotationMetadata(); - if (annotationMetadata.isEmpty()) { + if (argument.isVoid()) { Type type = Type.getType(Argument.class); - if (argument.isPrimitive() && !argument.isArray()) { - String constantName = argument.getName().toUpperCase(Locale.ENGLISH); - // refer to constant for primitives - generatorAdapter.getStatic(type, constantName, type); - return; - } - if (!argument.isArray() && String.class.getName().equals(argument.getType().getName()) - && argument.getName().equals(argument.getType().getName()) - && argument.getAnnotationMetadata().isEmpty()) { - generatorAdapter.getStatic(type, "STRING", type); - return; - } + generatorAdapter.getStatic(type, "VOID", type); + return; + } + if (argument.isPrimitive() && !argument.isArray()) { + String constantName = argument.getName().toUpperCase(Locale.ENGLISH); + // refer to constant for primitives + Type type = Type.getType(Argument.class); + generatorAdapter.getStatic(type, constantName, type); + return; + } + + if (annotationMetadata.isEmpty() + && !argument.isArray() + && String.class.getName().equals(argument.getType().getName()) + && argument.getName().equals(argument.getType().getName()) + && argument.getAnnotationMetadata().isEmpty()) { + Type type = Type.getType(Argument.class); + generatorAdapter.getStatic(type, "STRING", type); + return; } pushCreateArgument( diff --git a/core-processor/src/main/java/io/micronaut/inject/writer/BeanDefinitionWriter.java b/core-processor/src/main/java/io/micronaut/inject/writer/BeanDefinitionWriter.java index da25123993d..6c32d880ba7 100644 --- a/core-processor/src/main/java/io/micronaut/inject/writer/BeanDefinitionWriter.java +++ b/core-processor/src/main/java/io/micronaut/inject/writer/BeanDefinitionWriter.java @@ -84,7 +84,6 @@ import io.micronaut.inject.ast.MemberElement; import io.micronaut.inject.ast.MethodElement; import io.micronaut.inject.ast.ParameterElement; -import io.micronaut.inject.ast.PrimitiveElement; import io.micronaut.inject.ast.PropertyElement; import io.micronaut.inject.ast.TypedElement; import io.micronaut.inject.ast.beans.BeanElement; @@ -1441,7 +1440,7 @@ public void visitSetterValue( declaringTypeRef.getInternalName(), methodElement.getName(), methodDescriptor, isInterface); - if (methodElement.getReturnType() != PrimitiveElement.VOID) { + if (!methodElement.getReturnType().isVoid()) { injectMethodVisitor.pop(); } @@ -2058,7 +2057,7 @@ private void visitConfigBuilderMethodInternal( new org.objectweb.asm.commons.Method(methodName, methodDescriptor)); } - if (returnType != PrimitiveElement.VOID) { + if (!returnType.isVoid()) { injectMethodVisitor.pop(); } injectMethodVisitor.visitJumpInsn(GOTO, tryEnd); @@ -2435,7 +2434,7 @@ private void visitMethodInjectionPointInternal(MethodVisitData methodVisitData, injectMethodVisitor.visitMethodInsn(isInterface ? INVOKEINTERFACE : INVOKEVIRTUAL, declaringTypeRef.getInternalName(), methodName, methodDescriptor, isInterface); - if (isConfigurationProperties && returnType != PrimitiveElement.VOID) { + if (isConfigurationProperties && !returnType.isVoid()) { injectMethodVisitor.pop(); } } else { diff --git a/inject-groovy/README.md b/inject-groovy/README.md deleted file mode 100644 index 9a399b37f74..00000000000 --- a/inject-groovy/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# Micronaut AST - -AST Transformations and Annotation Processors for enhancing Groovy and Java code. \ No newline at end of file diff --git a/inject-groovy/build.gradle b/inject-groovy/build.gradle index e9facfce7c9..26c8a865052 100644 --- a/inject-groovy/build.gradle +++ b/inject-groovy/build.gradle @@ -37,7 +37,8 @@ dependencies { tasks.named("test") { exclude '**/*$_closure*' - + forkEvery = 100 + maxParallelForks = 4 systemProperty "groovy.attach.groovydoc", true } diff --git a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/AbstractGroovyElement.java b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/AbstractGroovyElement.java index 4c420354dc7..ad2881dcf00 100644 --- a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/AbstractGroovyElement.java +++ b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/AbstractGroovyElement.java @@ -17,8 +17,6 @@ import groovy.transform.PackageScope; import io.micronaut.core.annotation.AnnotationMetadata; -import io.micronaut.core.annotation.AnnotationValue; -import io.micronaut.core.annotation.AnnotationValueBuilder; import io.micronaut.core.annotation.Internal; import io.micronaut.core.annotation.NonNull; import io.micronaut.core.annotation.Nullable; @@ -30,9 +28,8 @@ import io.micronaut.inject.ast.ElementModifier; import io.micronaut.inject.ast.PrimitiveElement; import io.micronaut.inject.ast.WildcardElement; -import io.micronaut.inject.ast.annotation.ElementAnnotationMetadata; +import io.micronaut.inject.ast.annotation.AbstractAnnotationElement; import io.micronaut.inject.ast.annotation.ElementAnnotationMetadataFactory; -import io.micronaut.inject.ast.annotation.MutableAnnotationMetadataDelegate; import org.codehaus.groovy.ast.AnnotatedNode; import org.codehaus.groovy.ast.ClassHelper; import org.codehaus.groovy.ast.ClassNode; @@ -42,7 +39,6 @@ import org.codehaus.groovy.control.CompilationUnit; import org.codehaus.groovy.control.SourceUnit; -import java.lang.annotation.Annotation; import java.lang.reflect.Modifier; import java.util.ArrayList; import java.util.Arrays; @@ -52,8 +48,6 @@ import java.util.Map; import java.util.Optional; import java.util.Set; -import java.util.function.Consumer; -import java.util.function.Predicate; import java.util.regex.Pattern; import java.util.stream.Stream; @@ -65,16 +59,13 @@ * @since 1.1 */ @Internal -public abstract class AbstractGroovyElement implements Element { +public abstract class AbstractGroovyElement extends AbstractAnnotationElement { private static final Pattern JAVADOC_PATTERN = Pattern.compile("(/\\s*\\*\\*)|\\s*\\*|(\\s*[*/])"); protected final SourceUnit sourceUnit; protected final CompilationUnit compilationUnit; protected final GroovyVisitorContext visitorContext; - protected final ElementAnnotationMetadataFactory elementAnnotationMetadataFactory; - protected AnnotationMetadata presetAnnotationMetadata; - private ElementAnnotationMetadata elementAnnotationMetadata; private final GroovyNativeElement nativeElement; /** @@ -87,100 +78,13 @@ public abstract class AbstractGroovyElement implements Element { protected AbstractGroovyElement(GroovyVisitorContext visitorContext, GroovyNativeElement nativeElement, ElementAnnotationMetadataFactory annotationMetadataFactory) { + super(annotationMetadataFactory); this.visitorContext = visitorContext; this.compilationUnit = visitorContext.getCompilationUnit(); this.nativeElement = nativeElement; - this.elementAnnotationMetadataFactory = annotationMetadataFactory; this.sourceUnit = visitorContext.getSourceUnit(); } - @Override - public io.micronaut.inject.ast.Element annotate(String annotationType, Consumer> consumer) { - getAnnotationMetadataToWrite().annotate(annotationType, consumer); - return this; - } - - @Override - public io.micronaut.inject.ast.Element removeAnnotation(String annotationType) { - getAnnotationMetadataToWrite().removeAnnotation(annotationType); - return this; - } - - @Override - public io.micronaut.inject.ast.Element removeAnnotation(Class annotationType) { - getAnnotationMetadataToWrite().removeAnnotation(annotationType); - return this; - } - - @Override - public io.micronaut.inject.ast.Element removeAnnotationIf(Predicate> predicate) { - getAnnotationMetadataToWrite().removeAnnotationIf(predicate); - return this; - } - - @Override - public io.micronaut.inject.ast.Element removeStereotype(String annotationType) { - getAnnotationMetadataToWrite().removeStereotype(annotationType); - return this; - } - - @Override - public io.micronaut.inject.ast.Element removeStereotype(Class annotationType) { - getAnnotationMetadataToWrite().removeStereotype(annotationType); - return this; - } - - @Override - public io.micronaut.inject.ast.Element annotate(String annotationType) { - getAnnotationMetadataToWrite().annotate(annotationType); - return this; - } - - @Override - public io.micronaut.inject.ast.Element annotate(Class annotationType, Consumer> consumer) { - getAnnotationMetadataToWrite().annotate(annotationType, consumer); - return this; - } - - @Override - public io.micronaut.inject.ast.Element annotate(Class annotationType) { - getAnnotationMetadataToWrite().annotate(annotationType); - return this; - } - - @Override - public io.micronaut.inject.ast.Element annotate(AnnotationValue annotationValue) { - getAnnotationMetadataToWrite().annotate(annotationValue); - return this; - } - - @Override - public AnnotationMetadata getAnnotationMetadata() { - return getElementAnnotationMetadata(); - } - - /** - * @return The element annotation metadata - */ - protected ElementAnnotationMetadata getElementAnnotationMetadata() { - if (elementAnnotationMetadata == null) { - if (presetAnnotationMetadata == null) { - elementAnnotationMetadata = elementAnnotationMetadataFactory.build(this); - } else { - elementAnnotationMetadata = elementAnnotationMetadataFactory.build(this, presetAnnotationMetadata); - } - } - return elementAnnotationMetadata; - } - - /** - * Get annotation metadata to add or remove annotations. - * @return The annotation metadata to write - */ - protected MutableAnnotationMetadataDelegate getAnnotationMetadataToWrite() { - return getElementAnnotationMetadata(); - } - /** * Constructs this element by invoking the constructor. * diff --git a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyClassElement.java b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyClassElement.java index 49cbc3edefa..bdc7784e851 100644 --- a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyClassElement.java +++ b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyClassElement.java @@ -754,38 +754,38 @@ protected boolean excludeClass(ClassNode classNode) { } @Override - protected Element toAstElement(AnnotatedNode enclosedElement, Class elementType) { + protected Element toAstElement(AnnotatedNode nativeType, Class elementType) { final GroovyElementFactory elementFactory = visitorContext.getElementFactory(); if (isSource) { - if (!(enclosedElement instanceof ConstructorNode) && enclosedElement instanceof MethodNode methodNode) { + if (!(nativeType instanceof ConstructorNode) && nativeType instanceof MethodNode methodNode) { return elementFactory.newSourceMethodElement( GroovyClassElement.this, methodNode, elementAnnotationMetadataFactory ); } - if (enclosedElement instanceof ClassNode cn) { + if (nativeType instanceof ClassNode cn) { return elementFactory.newSourceClassElement( cn, elementAnnotationMetadataFactory ); } } - if (enclosedElement instanceof ConstructorNode constructorNode) { + if (nativeType instanceof ConstructorNode constructorNode) { return elementFactory.newConstructorElement( GroovyClassElement.this, constructorNode, elementAnnotationMetadataFactory ); } - if (enclosedElement instanceof MethodNode methodNode) { + if (nativeType instanceof MethodNode methodNode) { return elementFactory.newMethodElement( GroovyClassElement.this, methodNode, elementAnnotationMetadataFactory ); } - if (enclosedElement instanceof FieldNode fieldNode) { + if (nativeType instanceof FieldNode fieldNode) { if (fieldNode.isEnum()) { return elementFactory.newEnumConstantElement( GroovyClassElement.this, @@ -799,13 +799,13 @@ protected Element toAstElement(AnnotatedNode enclosedElement, Class elementTy elementAnnotationMetadataFactory ); } - if (enclosedElement instanceof ClassNode cn) { + if (nativeType instanceof ClassNode cn) { return elementFactory.newClassElement( cn, elementAnnotationMetadataFactory ); } - throw new IllegalStateException("Unknown element: " + enclosedElement); + throw new IllegalStateException("Unknown element: " + nativeType); } } diff --git a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyFieldElement.java b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyFieldElement.java index 9cd00eb9894..403619a0653 100644 --- a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyFieldElement.java +++ b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyFieldElement.java @@ -18,6 +18,7 @@ import io.micronaut.core.annotation.AnnotationMetadata; import io.micronaut.core.annotation.Internal; import io.micronaut.core.annotation.NonNull; +import io.micronaut.core.annotation.Nullable; import io.micronaut.inject.ast.ClassElement; import io.micronaut.inject.ast.ElementModifier; import io.micronaut.inject.ast.FieldElement; @@ -41,6 +42,12 @@ public class GroovyFieldElement extends AbstractGroovyElement implements FieldEl private final GroovyClassElement owningType; private final FieldNode fieldNode; + @Nullable + private GroovyClassElement declaringType; + @Nullable + private ClassElement type; + @Nullable + private ClassElement genericType; /** * @param visitorContext The visitor context @@ -145,24 +152,34 @@ public boolean isPackagePrivate() { @NonNull @Override public ClassElement getType() { - return newClassElement(fieldNode.getType()); + if (type == null) { + type = newClassElement(fieldNode.getType()); + } + return type; } @Override public ClassElement getGenericType() { - return newClassElement(fieldNode.getType(), getDeclaringType().getTypeArguments()); + if (genericType == null) { + genericType = newClassElement(fieldNode.getType(), getDeclaringType().getTypeArguments()); + } + return genericType; } @Override public GroovyClassElement getDeclaringType() { - ClassNode declaringClass = fieldNode.getDeclaringClass(); - if (declaringClass == null) { - throw new IllegalStateException("Declaring class could not be established"); - } - if (owningType.getNativeType().annotatedNode().equals(declaringClass)) { - return owningType; + if (declaringType == null) { + ClassNode declaringClass = fieldNode.getDeclaringClass(); + if (declaringClass == null) { + throw new IllegalStateException("Declaring class could not be established"); + } + if (owningType.getNativeType().annotatedNode().equals(declaringClass)) { + declaringType = owningType; + } else { + Map typeArguments = getOwningType().getTypeArguments(declaringClass.getName()); + declaringType = (GroovyClassElement) newClassElement(declaringClass, typeArguments); + } } - Map typeArguments = getOwningType().getTypeArguments(declaringClass.getName()); - return (GroovyClassElement) newClassElement(declaringClass, typeArguments); + return declaringType; } } diff --git a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyGenericPlaceholderElement.java b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyGenericPlaceholderElement.java index a50cbb70b8d..426e10b4ad3 100644 --- a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyGenericPlaceholderElement.java +++ b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyGenericPlaceholderElement.java @@ -24,11 +24,10 @@ import io.micronaut.inject.ast.Element; import io.micronaut.inject.ast.GenericPlaceholderElement; import io.micronaut.inject.ast.WildcardElement; -import io.micronaut.inject.ast.annotation.AbstractElementAnnotationMetadata; import io.micronaut.inject.ast.annotation.ElementAnnotationMetadata; +import io.micronaut.inject.ast.annotation.GenericPlaceholderElementAnnotationMetadata; import io.micronaut.inject.ast.annotation.MutableAnnotationMetadataDelegate; -import java.util.ArrayList; import java.util.List; import java.util.Objects; import java.util.Optional; @@ -77,7 +76,7 @@ final class GroovyGenericPlaceholderElement extends GroovyClassElement implement boolean rawType) { super(visitorContext, classElementRepresentingThisPlaceholder.getNativeType(), - classElementRepresentingThisPlaceholder.elementAnnotationMetadataFactory, + classElementRepresentingThisPlaceholder.getElementAnnotationMetadataFactory(), classElementRepresentingThisPlaceholder.resolvedTypeArguments, arrayDimensions); this.declaringElement = declaringElement; @@ -86,31 +85,8 @@ final class GroovyGenericPlaceholderElement extends GroovyClassElement implement this.resolved = resolved; this.bounds = bounds; this.rawType = rawType; - typeAnnotationMetadata = new AbstractElementAnnotationMetadata() { - - AnnotationMetadata annotationMetadata; - - public AnnotationMetadata getReturnInstance() { - return getAnnotationMetadata(); - } - - @Override - protected MutableAnnotationMetadataDelegate getAnnotationMetadataToWrite() { - return getGenericTypeAnnotationMetadata(); - } - - @Override - public AnnotationMetadata getAnnotationMetadata() { - if (annotationMetadata == null) { - List allAnnotationMetadata = new ArrayList<>(); - getBounds().forEach(ce -> allAnnotationMetadata.add(ce.getTypeAnnotationMetadata())); - allAnnotationMetadata.add(GroovyGenericPlaceholderElement.super.getTypeAnnotationMetadata()); - allAnnotationMetadata.add(getGenericTypeAnnotationMetadata()); - annotationMetadata = new AnnotationMetadataHierarchy(true, allAnnotationMetadata.toArray(AnnotationMetadata[]::new)); - } - return annotationMetadata; - } - }; + typeAnnotationMetadata = new GenericPlaceholderElementAnnotationMetadata(this, classElementRepresentingThisPlaceholder); + } private static GroovyClassElement selectClassElementRepresentingThisPlaceholder(@Nullable GroovyClassElement resolved, diff --git a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyMethodElement.java b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyMethodElement.java index 8de53d114ff..b8554879445 100644 --- a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyMethodElement.java +++ b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyMethodElement.java @@ -108,8 +108,8 @@ public ClassElement[] getThrownTypes() { final ClassNode[] exceptions = methodNode.getExceptions(); if (ArrayUtils.isNotEmpty(exceptions)) { return Arrays.stream(exceptions) - .map(cn -> newClassElement(cn, getDeclaringType().getTypeArguments())) - .toArray(ClassElement[]::new); + .map(cn -> newClassElement(cn, getDeclaringType().getTypeArguments())) + .toArray(ClassElement[]::new); } return ClassElement.ZERO_CLASS_ELEMENTS; } @@ -192,13 +192,19 @@ public Map getTypeArguments() { @NonNull @Override public ClassElement getGenericReturnType() { - return newClassElement(methodNode.getReturnType(), getTypeArguments()); + if (genericReturnType == null) { + genericReturnType = newClassElement(methodNode.getReturnType(), getTypeArguments()); + } + return genericReturnType; } @Override @NonNull public ClassElement getReturnType() { - return newClassElement(methodNode.getReturnType()); + if (returnType == null) { + returnType = newClassElement(methodNode.getReturnType()); + } + return returnType; } @Override @@ -212,11 +218,11 @@ public ParameterElement[] getParameters() { private GroovyParameterElement newParameter(Parameter parameter) { return new GroovyParameterElement( - this, - visitorContext, - new GroovyNativeElement.Parameter(parameter, methodNode), - parameter, - elementAnnotationMetadataFactory + this, + visitorContext, + new GroovyNativeElement.Parameter(parameter, methodNode), + parameter, + elementAnnotationMetadataFactory ); } @@ -245,8 +251,8 @@ public List getDeclaredTypeVariables() { return Collections.emptyList(); } return Arrays.stream(genericsTypes) - .map(gt -> (GenericPlaceholderElement) newClassElement(gt)) - .toList(); + .map(gt -> (GenericPlaceholderElement) newClassElement(gt)) + .toList(); } } diff --git a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyPropertyElement.java b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyPropertyElement.java index b13c753ee41..790f91bbb1f 100644 --- a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyPropertyElement.java +++ b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyPropertyElement.java @@ -16,12 +16,8 @@ package io.micronaut.ast.groovy.visitor; import io.micronaut.core.annotation.AnnotationMetadata; -import io.micronaut.core.annotation.AnnotationMetadataDelegate; -import io.micronaut.core.annotation.AnnotationValue; -import io.micronaut.core.annotation.AnnotationValueBuilder; import io.micronaut.core.annotation.Internal; import io.micronaut.core.annotation.Nullable; -import io.micronaut.inject.annotation.AnnotationMetadataHierarchy; import io.micronaut.inject.ast.ClassElement; import io.micronaut.inject.ast.FieldElement; import io.micronaut.inject.ast.MemberElement; @@ -29,13 +25,9 @@ import io.micronaut.inject.ast.PropertyElement; import io.micronaut.inject.ast.annotation.ElementAnnotationMetadata; import io.micronaut.inject.ast.annotation.ElementAnnotationMetadataFactory; +import io.micronaut.inject.ast.annotation.PropertyElementAnnotationMetadata; -import java.lang.annotation.Annotation; -import java.util.ArrayList; -import java.util.List; import java.util.Optional; -import java.util.function.Consumer; -import java.util.function.Predicate; /** * Implementation of {@link PropertyElement} for Groovy. @@ -81,102 +73,7 @@ final class GroovyPropertyElement extends AbstractGroovyElement implements Prope this.writeAccessKind = writeAccessKind; this.owningElement = owningElement; this.excluded = excluded; - List elements = new ArrayList<>(3); - if (getter instanceof AbstractGroovyElement) { - elements.add(getter); - } - if (setter instanceof AbstractGroovyElement) { - elements.add(setter); - } - if (field instanceof AbstractGroovyElement) { - elements.add(field); - } - // The instance AnnotationMetadata of each element can change after a modification - // Set annotation metadata as actual elements so the changes are reflected - AnnotationMetadata propertyAnnotationMetadata; - if (elements.size() == 1) { - propertyAnnotationMetadata = elements.iterator().next(); - } else { - propertyAnnotationMetadata = new AnnotationMetadataHierarchy( - true, - elements.stream().map(e -> { - if (e instanceof MethodElement) { - return new AnnotationMetadataDelegate() { - @Override - public AnnotationMetadata getAnnotationMetadata() { - // Exclude type metadata - return e.getAnnotationMetadata().getDeclaredMetadata(); - } - }; - } - return e; - }).toArray(AnnotationMetadata[]::new) - ); - } - annotationMetadata = new ElementAnnotationMetadata() { - - @Override - public io.micronaut.inject.ast.Element annotate(AnnotationValue annotationValue) { - for (MemberElement memberElement : elements) { - memberElement.annotate(annotationValue); - } - return GroovyPropertyElement.this; - } - - @Override - public io.micronaut.inject.ast.Element annotate(String annotationType, Consumer> consumer) { - for (MemberElement memberElement : elements) { - memberElement.annotate(annotationType, consumer); - } - return GroovyPropertyElement.this; - } - - @Override - public io.micronaut.inject.ast.Element annotate(Class annotationType) { - for (MemberElement memberElement : elements) { - memberElement.annotate(annotationType); - } - return GroovyPropertyElement.this; - } - - @Override - public io.micronaut.inject.ast.Element annotate(String annotationType) { - for (MemberElement memberElement : elements) { - memberElement.annotate(annotationType); - } - return GroovyPropertyElement.this; - } - - @Override - public io.micronaut.inject.ast.Element annotate(Class annotationType, Consumer> consumer) { - for (MemberElement memberElement : elements) { - memberElement.annotate(annotationType, consumer); - } - return GroovyPropertyElement.this; - } - - @Override - public io.micronaut.inject.ast.Element removeAnnotation(String annotationType) { - for (MemberElement memberElement : elements) { - memberElement.removeAnnotation(annotationType); - } - return GroovyPropertyElement.this; - } - - @Override - public io.micronaut.inject.ast.Element removeAnnotationIf(Predicate> predicate) { - for (MemberElement memberElement : elements) { - memberElement.removeAnnotationIf(predicate); - } - return GroovyPropertyElement.this; - } - - @Override - public AnnotationMetadata getAnnotationMetadata() { - return propertyAnnotationMetadata; - } - - }; + this.annotationMetadata = new PropertyElementAnnotationMetadata(this, getter, setter, field, null,false); } @Override @@ -287,26 +184,18 @@ public AccessKind getWriteAccessKind() { @Override public boolean isReadOnly() { - switch (writeAccessKind) { - case METHOD: - return setter == null; - case FIELD: - return field == null || field.isFinal(); - default: - throw new IllegalStateException(); - } + return switch (writeAccessKind) { + case METHOD -> setter == null; + case FIELD -> field == null || field.isFinal(); + }; } @Override public boolean isWriteOnly() { - switch (readAccessKind) { - case METHOD: - return getter == null; - case FIELD: - return field == null; - default: - throw new IllegalStateException(); - } + return switch (readAccessKind) { + case METHOD -> getter == null; + case FIELD -> field == null; + }; } @Override diff --git a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyWildcardElement.java b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyWildcardElement.java index d36f585fd84..6471f5873fe 100644 --- a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyWildcardElement.java +++ b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyWildcardElement.java @@ -22,13 +22,12 @@ import io.micronaut.inject.annotation.AnnotationMetadataHierarchy; import io.micronaut.inject.ast.ArrayableClassElement; import io.micronaut.inject.ast.ClassElement; -import io.micronaut.inject.ast.annotation.AbstractElementAnnotationMetadata; +import io.micronaut.inject.ast.WildcardElement; import io.micronaut.inject.ast.annotation.ElementAnnotationMetadata; import io.micronaut.inject.ast.annotation.ElementAnnotationMetadataFactory; -import io.micronaut.inject.ast.WildcardElement; import io.micronaut.inject.ast.annotation.MutableAnnotationMetadataDelegate; +import io.micronaut.inject.ast.annotation.WildcardElementAnnotationMetadata; -import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.function.Function; @@ -66,32 +65,7 @@ final class GroovyWildcardElement extends GroovyClassElement implements Wildcard this.upperType = upperType; this.upperBounds = upperBounds; this.lowerBounds = lowerBounds; - typeAnnotationMetadata = new AbstractElementAnnotationMetadata() { - - AnnotationMetadata annotationMetadata; - - public AnnotationMetadata getReturnInstance() { - return getAnnotationMetadata(); - } - - @Override - protected MutableAnnotationMetadataDelegate getAnnotationMetadataToWrite() { - return getGenericTypeAnnotationMetadata(); - } - - @Override - public AnnotationMetadata getAnnotationMetadata() { - if (annotationMetadata == null) { - List allAnnotationMetadata = new ArrayList<>(); - getLowerBounds().forEach(ce -> allAnnotationMetadata.add(ce.getTypeAnnotationMetadata())); - getUpperBounds().forEach(ce -> allAnnotationMetadata.add(ce.getTypeAnnotationMetadata())); - allAnnotationMetadata.add(GroovyWildcardElement.super.getTypeAnnotationMetadata()); - allAnnotationMetadata.add(getGenericTypeAnnotationMetadata()); - annotationMetadata = new AnnotationMetadataHierarchy(true, allAnnotationMetadata.toArray(AnnotationMetadata[]::new)); - } - return annotationMetadata; - } - }; + typeAnnotationMetadata = new WildcardElementAnnotationMetadata(this, upperType); } @Override diff --git a/inject-groovy/src/test/groovy/io/micronaut/inject/annotation/modify/AnnotateFieldTypeSpec.groovy b/inject-groovy/src/test/groovy/io/micronaut/inject/annotation/modify/AnnotateFieldTypeSpec.groovy index d1b75524d47..5c98b197e86 100644 --- a/inject-groovy/src/test/groovy/io/micronaut/inject/annotation/modify/AnnotateFieldTypeSpec.groovy +++ b/inject-groovy/src/test/groovy/io/micronaut/inject/annotation/modify/AnnotateFieldTypeSpec.groovy @@ -9,6 +9,7 @@ import io.micronaut.inject.visitor.AllElementsVisitor import io.micronaut.inject.visitor.TypeElementVisitor import io.micronaut.inject.visitor.VisitorContext +// Annotating the field type doesn't add the annotation to the runtime inject field class AnnotateFieldTypeSpec extends AbstractBeanDefinitionSpec { def setup() { @@ -23,19 +24,19 @@ class AnnotateFieldTypeSpec extends AbstractBeanDefinitionSpec { void 'test annotating'() { when: def definition = buildBeanDefinition('addann.AnnotateFieldTypeClass', ''' -package addann; +package addann -import io.micronaut.context.annotation.Bean; -import jakarta.inject.Inject; +import io.micronaut.context.annotation.Bean +import jakarta.inject.Inject @Bean class AnnotateFieldTypeClass { @Inject - public MyBean1 myField1; + public MyBean1 myField1 @Inject - public MyBean1 myField2; + public MyBean1 myField2 } @@ -49,20 +50,20 @@ class MyBean1 { void 'test annotating 2'() { when: def definition = buildBeanDefinition('addann.AnnotateFieldTypeClass', ''' -package addann; +package addann -import io.micronaut.context.annotation.Executable; -import io.micronaut.context.annotation.Bean; -import jakarta.inject.Inject; +import io.micronaut.context.annotation.Executable +import io.micronaut.context.annotation.Bean +import jakarta.inject.Inject @Bean class AnnotateFieldTypeClass { @Inject - public T myField1; + public T myField1 @Inject - public T myField2; + public T myField2 } @@ -76,18 +77,18 @@ class MyBean1 { void 'test annotating 3'() { when: def definition = buildBeanDefinition('addann.AnnotateFieldTypeClass', ''' -package addann; +package addann -import io.micronaut.context.annotation.Bean; -import jakarta.inject.Inject; +import io.micronaut.context.annotation.Bean +import jakarta.inject.Inject abstract class BaseAnnotateFieldTypeClass { @Inject - public S myField1; + public S myField1 @Inject - public S myField2; + public S myField2 } @@ -105,18 +106,18 @@ class MyBean1 { void 'test annotating 4'() { when: def definition = buildBeanDefinition('addann.AnnotateFieldTypeClass', ''' -package addann; +package addann -import io.micronaut.context.annotation.Bean; -import jakarta.inject.Inject; +import io.micronaut.context.annotation.Bean +import jakarta.inject.Inject abstract class BaseAnnotateFieldTypeClass { @Inject - public S myField1; + public S myField1 @Inject - public S myField2; + public S myField2 } @@ -133,12 +134,12 @@ class MyBean1 { void validate(BeanDefinition definition) { def myField1 = definition.getInjectedFields()[0] - myField1.name == "myField1" - myField1.asArgument().getAnnotationMetadata().hasAnnotation(MyAnnotation) + assert myField1.name == "myField1" + assert !myField1.asArgument().getAnnotationMetadata().hasAnnotation(MyAnnotation) def myField2 = definition.getInjectedFields()[1] - myField2.name == "myField2" - !myField2.asArgument().getAnnotationMetadata().hasAnnotation(MyAnnotation) + assert myField2.name == "myField2" + assert !myField2.asArgument().getAnnotationMetadata().hasAnnotation(MyAnnotation) } static class AnnotateFieldTypeVisitor implements TypeElementVisitor { diff --git a/inject-java/src/main/java/io/micronaut/annotation/processing/JavaElementAnnotationMetadataFactory.java b/inject-java/src/main/java/io/micronaut/annotation/processing/JavaElementAnnotationMetadataFactory.java index 7e3f6bd6b87..cb0724413ed 100644 --- a/inject-java/src/main/java/io/micronaut/annotation/processing/JavaElementAnnotationMetadataFactory.java +++ b/inject-java/src/main/java/io/micronaut/annotation/processing/JavaElementAnnotationMetadataFactory.java @@ -88,7 +88,7 @@ protected AbstractAnnotationMetadataBuilder.CachedAnnotationMetadata lookupTypeA if (typeMirror == null) { return super.lookupTypeAnnotationsForClass(classElement); } - return metadataBuilder.lookupOrBuild(clazz, new AnnotationsElement(typeMirror), true); + return metadataBuilder.lookupOrBuild(clazz, new AnnotationsElement(typeMirror)); } @Override @@ -107,7 +107,7 @@ protected AbstractAnnotationMetadataBuilder.CachedAnnotationMetadata lookupTypeA @Override protected AbstractAnnotationMetadataBuilder.CachedAnnotationMetadata lookupTypeAnnotationsForWildcard(WildcardElement wildcardElement) { WildcardType wildcard = (WildcardType) wildcardElement.getGenericNativeType(); - return metadataBuilder.lookupOrBuild(wildcard, new AnnotationsElement(wildcard), true); + return metadataBuilder.lookupOrBuild(wildcard, new AnnotationsElement(wildcard)); } } diff --git a/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/AbstractJavaElement.java b/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/AbstractJavaElement.java index f15e5de0364..9863ddf9c02 100644 --- a/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/AbstractJavaElement.java +++ b/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/AbstractJavaElement.java @@ -16,20 +16,17 @@ package io.micronaut.annotation.processing.visitor; import io.micronaut.core.annotation.AnnotationMetadata; -import io.micronaut.core.annotation.AnnotationValue; -import io.micronaut.core.annotation.AnnotationValueBuilder; import io.micronaut.core.annotation.Internal; import io.micronaut.core.annotation.NonNull; import io.micronaut.core.annotation.Nullable; import io.micronaut.core.util.CollectionUtils; +import io.micronaut.inject.ast.annotation.AbstractAnnotationElement; import io.micronaut.inject.ast.ClassElement; import io.micronaut.inject.ast.ElementModifier; import io.micronaut.inject.ast.PrimitiveElement; import io.micronaut.inject.ast.TypedElement; import io.micronaut.inject.ast.WildcardElement; -import io.micronaut.inject.ast.annotation.ElementAnnotationMetadata; import io.micronaut.inject.ast.annotation.ElementAnnotationMetadataFactory; -import io.micronaut.inject.ast.annotation.MutableAnnotationMetadataDelegate; import javax.lang.model.element.Element; import javax.lang.model.element.ElementKind; @@ -46,7 +43,6 @@ import javax.lang.model.type.TypeVariable; import javax.lang.model.type.UnionType; import javax.lang.model.type.WildcardType; -import java.lang.annotation.Annotation; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; @@ -55,8 +51,6 @@ import java.util.Map; import java.util.Optional; import java.util.Set; -import java.util.function.Consumer; -import java.util.function.Predicate; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -72,15 +66,10 @@ * @since 1.0 */ @Internal -public abstract class AbstractJavaElement implements io.micronaut.inject.ast.Element { +public abstract class AbstractJavaElement extends AbstractAnnotationElement { protected final JavaVisitorContext visitorContext; - protected final ElementAnnotationMetadataFactory elementAnnotationMetadataFactory; - @Nullable - protected AnnotationMetadata presetAnnotationMetadata; private final JavaNativeElement nativeElement; - @Nullable - private ElementAnnotationMetadata elementAnnotationMetadata; /** * @param nativeElement The {@link Element} @@ -88,16 +77,11 @@ public abstract class AbstractJavaElement implements io.micronaut.inject.ast.Ele * @param visitorContext The Java visitor context */ AbstractJavaElement(JavaNativeElement nativeElement, ElementAnnotationMetadataFactory annotationMetadataFactory, JavaVisitorContext visitorContext) { + super(annotationMetadataFactory); this.nativeElement = nativeElement; - this.elementAnnotationMetadataFactory = annotationMetadataFactory; this.visitorContext = visitorContext; } - @Override - public AnnotationMetadata getAnnotationMetadata() { - return getElementAnnotationMetadata(); - } - /** * @return copy of this element */ @@ -123,89 +107,6 @@ public io.micronaut.inject.ast.Element withAnnotationMetadata(AnnotationMetadata return abstractJavaElement; } - /** - * @return The element's annotation metadata - */ - protected ElementAnnotationMetadata getElementAnnotationMetadata() { - if (elementAnnotationMetadata == null) { - if (presetAnnotationMetadata == null) { - elementAnnotationMetadata = elementAnnotationMetadataFactory.build(this); - } else { - elementAnnotationMetadata = elementAnnotationMetadataFactory.build(this, presetAnnotationMetadata); - } - } - return elementAnnotationMetadata; - } - - /** - * Get annotation metadata to add or remove annotations. - * - * @return The annotation metadata to write - */ - protected MutableAnnotationMetadataDelegate getAnnotationMetadataToWrite() { - return getElementAnnotationMetadata(); - } - - @Override - public io.micronaut.inject.ast.Element annotate(String annotationType, Consumer> consumer) { - getAnnotationMetadataToWrite().annotate(annotationType, consumer); - return this; - } - - @Override - public io.micronaut.inject.ast.Element removeAnnotation(String annotationType) { - getAnnotationMetadataToWrite().removeAnnotation(annotationType); - return this; - } - - @Override - public io.micronaut.inject.ast.Element removeAnnotation(Class annotationType) { - getAnnotationMetadataToWrite().removeAnnotation(annotationType); - return this; - } - - @Override - public io.micronaut.inject.ast.Element removeAnnotationIf(Predicate> predicate) { - getAnnotationMetadataToWrite().removeAnnotationIf(predicate); - return this; - } - - @Override - public io.micronaut.inject.ast.Element removeStereotype(String annotationType) { - getAnnotationMetadataToWrite().removeStereotype(annotationType); - return this; - } - - @Override - public io.micronaut.inject.ast.Element removeStereotype(Class annotationType) { - getAnnotationMetadataToWrite().removeStereotype(annotationType); - return this; - } - - @Override - public io.micronaut.inject.ast.Element annotate(String annotationType) { - getAnnotationMetadataToWrite().annotate(annotationType); - return this; - } - - @Override - public io.micronaut.inject.ast.Element annotate(Class annotationType, Consumer> consumer) { - getAnnotationMetadataToWrite().annotate(annotationType, consumer); - return this; - } - - @Override - public io.micronaut.inject.ast.Element annotate(Class annotationType) { - getAnnotationMetadataToWrite().annotate(annotationType); - return this; - } - - @Override - public io.micronaut.inject.ast.Element annotate(AnnotationValue annotationValue) { - getAnnotationMetadataToWrite().annotate(annotationValue); - return this; - } - @Override public boolean isPackagePrivate() { Set modifiers = nativeElement.element().getModifiers(); diff --git a/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaClassElement.java b/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaClassElement.java index a665a0a10aa..cc86001728b 100644 --- a/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaClassElement.java +++ b/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaClassElement.java @@ -218,6 +218,14 @@ public AnnotationMetadata getAnnotationMetadata() { return annotationMetadata; } + @Override + public MutableAnnotationMetadataDelegate getTypeAnnotationMetadata() { + if (elementTypeAnnotationMetadata == null) { + elementTypeAnnotationMetadata = elementAnnotationMetadataFactory.buildTypeAnnotations(this); + } + return elementTypeAnnotationMetadata; + } + @Override public boolean isTypeVariable() { return isTypeVariable; @@ -705,14 +713,6 @@ public ClassElement getType() { return theType; } - @Override - public MutableAnnotationMetadataDelegate getTypeAnnotationMetadata() { - if (elementTypeAnnotationMetadata == null) { - elementTypeAnnotationMetadata = elementAnnotationMetadataFactory.buildTypeAnnotations(this); - } - return elementTypeAnnotationMetadata; - } - private final class JavaEnclosedElementsQuery extends EnclosedElementsQuery { private List enclosedElements; @@ -785,34 +785,34 @@ protected boolean excludeClass(TypeElement classNode) { } @Override - protected io.micronaut.inject.ast.Element toAstElement(Element enclosedElement, Class elementType) { + protected io.micronaut.inject.ast.Element toAstElement(Element nativeType, Class elementType) { final JavaElementFactory elementFactory = visitorContext.getElementFactory(); - return switch (enclosedElement.getKind()) { + return switch (nativeType.getKind()) { case METHOD -> elementFactory.newMethodElement( JavaClassElement.this, - (ExecutableElement) enclosedElement, + (ExecutableElement) nativeType, elementAnnotationMetadataFactory ); case FIELD -> elementFactory.newFieldElement( JavaClassElement.this, - (VariableElement) enclosedElement, + (VariableElement) nativeType, elementAnnotationMetadataFactory ); case ENUM_CONSTANT -> elementFactory.newEnumConstantElement( JavaClassElement.this, - (VariableElement) enclosedElement, + (VariableElement) nativeType, elementAnnotationMetadataFactory ); case CONSTRUCTOR -> elementFactory.newConstructorElement( JavaClassElement.this, - (ExecutableElement) enclosedElement, + (ExecutableElement) nativeType, elementAnnotationMetadataFactory ); case CLASS, ENUM -> elementFactory.newClassElement( - (TypeElement) enclosedElement, + (TypeElement) nativeType, elementAnnotationMetadataFactory ); - default -> throw new IllegalStateException("Unknown element: " + enclosedElement); + default -> throw new IllegalStateException("Unknown element: " + nativeType); }; } diff --git a/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaConstructorElement.java b/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaConstructorElement.java index b6e5fda61e6..e51444a46ae 100644 --- a/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaConstructorElement.java +++ b/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaConstructorElement.java @@ -31,16 +31,16 @@ class JavaConstructorElement extends JavaMethodElement implements ConstructorElement { /** - * @param declaringClass The declaring class + * @param owningClass The declaring class * @param nativeElement The native element * @param annotationMetadataFactory The annotation metadata factory * @param visitorContext The visitor context */ - JavaConstructorElement(JavaClassElement declaringClass, + JavaConstructorElement(JavaClassElement owningClass, JavaNativeElement.Method nativeElement, ElementAnnotationMetadataFactory annotationMetadataFactory, JavaVisitorContext visitorContext) { - super(declaringClass, nativeElement, annotationMetadataFactory, visitorContext); + super(owningClass, nativeElement, annotationMetadataFactory, visitorContext); } @Override diff --git a/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaGenericPlaceholderElement.java b/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaGenericPlaceholderElement.java index 17d2c7ff47b..fd573798f0d 100644 --- a/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaGenericPlaceholderElement.java +++ b/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaGenericPlaceholderElement.java @@ -24,14 +24,13 @@ import io.micronaut.inject.ast.Element; import io.micronaut.inject.ast.GenericPlaceholderElement; import io.micronaut.inject.ast.WildcardElement; -import io.micronaut.inject.ast.annotation.AbstractElementAnnotationMetadata; import io.micronaut.inject.ast.annotation.ElementAnnotationMetadata; import io.micronaut.inject.ast.annotation.ElementAnnotationMetadataFactory; +import io.micronaut.inject.ast.annotation.GenericPlaceholderElementAnnotationMetadata; import io.micronaut.inject.ast.annotation.MutableAnnotationMetadataDelegate; import javax.lang.model.element.TypeParameterElement; import javax.lang.model.type.TypeVariable; -import java.util.ArrayList; import java.util.List; import java.util.Objects; import java.util.Optional; @@ -98,31 +97,7 @@ final class JavaGenericPlaceholderElement extends JavaClassElement implements Ge this.resolved = resolved; this.bounds = bounds; this.isRawType = isRawType; - typeAnnotationMetadata = new AbstractElementAnnotationMetadata() { - - AnnotationMetadata annotationMetadata; - - public AnnotationMetadata getReturnInstance() { - return getAnnotationMetadata(); - } - - @Override - protected MutableAnnotationMetadataDelegate getAnnotationMetadataToWrite() { - return getGenericTypeAnnotationMetadata(); - } - - @Override - public AnnotationMetadata getAnnotationMetadata() { - if (annotationMetadata == null) { - List allAnnotationMetadata = new ArrayList<>(); - getBounds().forEach(ce -> allAnnotationMetadata.add(ce.getTypeAnnotationMetadata())); - allAnnotationMetadata.add(JavaGenericPlaceholderElement.super.getTypeAnnotationMetadata()); - allAnnotationMetadata.add(getGenericTypeAnnotationMetadata()); - annotationMetadata = new AnnotationMetadataHierarchy(true, allAnnotationMetadata.toArray(AnnotationMetadata[]::new)); - } - return annotationMetadata; - } - }; + typeAnnotationMetadata = new GenericPlaceholderElementAnnotationMetadata(this, classElementRepresentingThisPlaceholder); } private static JavaClassElement selectClassElementRepresentingThisPlaceholder(@Nullable JavaClassElement resolved, @@ -212,4 +187,5 @@ public Optional getResolved() { public JavaClassElement getResolvedInternal() { return resolved; } + } diff --git a/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaPropertyElement.java b/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaPropertyElement.java index c0e97b7b28d..982d4b20e17 100644 --- a/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaPropertyElement.java +++ b/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaPropertyElement.java @@ -16,27 +16,17 @@ package io.micronaut.annotation.processing.visitor; import io.micronaut.core.annotation.AnnotationMetadata; -import io.micronaut.core.annotation.AnnotationMetadataDelegate; -import io.micronaut.core.annotation.AnnotationValue; -import io.micronaut.core.annotation.AnnotationValueBuilder; import io.micronaut.core.annotation.Internal; import io.micronaut.core.annotation.Nullable; -import io.micronaut.inject.annotation.AnnotationMetadataHierarchy; import io.micronaut.inject.ast.ClassElement; import io.micronaut.inject.ast.FieldElement; import io.micronaut.inject.ast.MethodElement; -import io.micronaut.inject.ast.ParameterElement; import io.micronaut.inject.ast.PropertyElement; import io.micronaut.inject.ast.annotation.ElementAnnotationMetadata; import io.micronaut.inject.ast.annotation.ElementAnnotationMetadataFactory; -import io.micronaut.inject.ast.annotation.MutableAnnotationMetadataDelegate; +import io.micronaut.inject.ast.annotation.PropertyElementAnnotationMetadata; -import java.lang.annotation.Annotation; -import java.util.ArrayList; -import java.util.List; import java.util.Optional; -import java.util.function.Consumer; -import java.util.function.Predicate; /** * Models a {@link PropertyElement} for Java. @@ -82,117 +72,7 @@ final class JavaPropertyElement extends AbstractJavaElement implements PropertyE this.writeAccessKind = writeAccessKind; this.owningElement = owningElement; this.excluded = excluded; - List> elements = new ArrayList<>(3); - if (setter != null) { - elements.add(setter); - ParameterElement[] parameters = setter.getParameters(); - if (parameters.length > 0) { - ParameterElement parameter = parameters[0]; - MutableAnnotationMetadataDelegate typeAnnotationMetadata = parameter.getType().getTypeAnnotationMetadata(); - if (!typeAnnotationMetadata.isEmpty()) { - elements.add(typeAnnotationMetadata); - } - } - } - if (field != null) { - elements.add(field); - MutableAnnotationMetadataDelegate typeAnnotationMetadata = field.getType().getTypeAnnotationMetadata(); - if (!typeAnnotationMetadata.isEmpty()) { - elements.add(typeAnnotationMetadata); - } - } - if (getter != null) { - elements.add(getter); - MutableAnnotationMetadataDelegate typeAnnotationMetadata = getter.getReturnType().getTypeAnnotationMetadata(); - if (!typeAnnotationMetadata.isEmpty()) { - elements.add(typeAnnotationMetadata); - } - } - // The instance AnnotationMetadata of each element can change after a modification - // Set annotation metadata as actual elements so the changes are reflected - AnnotationMetadata propertyAnnotationMetadata; - if (elements.size() == 1) { - propertyAnnotationMetadata = elements.iterator().next(); - } else { - propertyAnnotationMetadata = new AnnotationMetadataHierarchy( - true, - elements.stream().map(e -> { - if (e instanceof MethodElement methodElement) { - return new AnnotationMetadataDelegate() { - @Override - public AnnotationMetadata getAnnotationMetadata() { - // Exclude type metadata - return methodElement.getAnnotationMetadata().getDeclaredMetadata(); - } - }; - } - return e; - }).toArray(AnnotationMetadata[]::new) - ); - } - annotationMetadata = new ElementAnnotationMetadata() { - - @Override - public io.micronaut.inject.ast.Element annotate(AnnotationValue annotationValue) { - for (MutableAnnotationMetadataDelegate am : elements) { - am.annotate(annotationValue); - } - return JavaPropertyElement.this; - } - - @Override - public io.micronaut.inject.ast.Element annotate(String annotationType, Consumer> consumer) { - for (MutableAnnotationMetadataDelegate am : elements) { - am.annotate(annotationType, consumer); - } - return JavaPropertyElement.this; - } - - @Override - public io.micronaut.inject.ast.Element annotate(Class annotationType) { - for (MutableAnnotationMetadataDelegate am : elements) { - am.annotate(annotationType); - } - return JavaPropertyElement.this; - } - - @Override - public io.micronaut.inject.ast.Element annotate(String annotationType) { - for (MutableAnnotationMetadataDelegate am : elements) { - am.annotate(annotationType); - } - return JavaPropertyElement.this; - } - - @Override - public io.micronaut.inject.ast.Element annotate(Class annotationType, Consumer> consumer) { - for (MutableAnnotationMetadataDelegate am : elements) { - am.annotate(annotationType, consumer); - } - return JavaPropertyElement.this; - } - - @Override - public io.micronaut.inject.ast.Element removeAnnotation(String annotationType) { - for (MutableAnnotationMetadataDelegate am : elements) { - am.removeAnnotation(annotationType); - } - return JavaPropertyElement.this; - } - - @Override - public io.micronaut.inject.ast.Element removeAnnotationIf(Predicate> predicate) { - for (MutableAnnotationMetadataDelegate am : elements) { - am.removeAnnotationIf(predicate); - } - return JavaPropertyElement.this; - } - - @Override - public AnnotationMetadata getAnnotationMetadata() { - return propertyAnnotationMetadata; - } - }; + this.annotationMetadata = new PropertyElementAnnotationMetadata(this, getter, setter, field, null, false); } @Override @@ -206,8 +86,8 @@ public PropertyElement withAnnotationMetadata(AnnotationMetadata annotationMetad } private static JavaNativeElement selectNativeType(MethodElement getter, - MethodElement setter, - FieldElement field) { + MethodElement setter, + FieldElement field) { if (getter != null) { return (JavaNativeElement) getter.getNativeType(); } diff --git a/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaVisitorContext.java b/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaVisitorContext.java index 497bb98c0c6..300d4968292 100644 --- a/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaVisitorContext.java +++ b/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaVisitorContext.java @@ -31,6 +31,7 @@ import io.micronaut.core.util.CollectionUtils; import io.micronaut.core.util.StringUtils; import io.micronaut.inject.ast.ClassElement; +import io.micronaut.inject.ast.annotation.AbstractAnnotationElement; import io.micronaut.inject.ast.annotation.ElementAnnotationMetadataFactory; import io.micronaut.inject.ast.beans.BeanElement; import io.micronaut.inject.ast.beans.BeanElementBuilder; @@ -494,7 +495,7 @@ public BeanElementBuilder addAssociatedBean(io.micronaut.inject.ast.Element orig originatingElement, type, ConfigurationMetadataBuilder.INSTANCE, - type instanceof AbstractJavaElement ? ((AbstractJavaElement) type).elementAnnotationMetadataFactory : elementAnnotationMetadataFactory, + type instanceof AbstractAnnotationElement ? ((AbstractAnnotationElement) type).getElementAnnotationMetadataFactory() : elementAnnotationMetadataFactory, this ); } diff --git a/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaWildcardElement.java b/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaWildcardElement.java index 31eb1c51493..0e7e0777ec0 100644 --- a/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaWildcardElement.java +++ b/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaWildcardElement.java @@ -23,13 +23,12 @@ import io.micronaut.inject.ast.ArrayableClassElement; import io.micronaut.inject.ast.ClassElement; import io.micronaut.inject.ast.WildcardElement; -import io.micronaut.inject.ast.annotation.AbstractElementAnnotationMetadata; import io.micronaut.inject.ast.annotation.ElementAnnotationMetadata; import io.micronaut.inject.ast.annotation.ElementAnnotationMetadataFactory; import io.micronaut.inject.ast.annotation.MutableAnnotationMetadataDelegate; +import io.micronaut.inject.ast.annotation.WildcardElementAnnotationMetadata; import javax.lang.model.type.WildcardType; -import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.function.Function; @@ -66,32 +65,7 @@ final class JavaWildcardElement extends JavaClassElement implements WildcardElem this.upperBound = mostUpper; this.upperBounds = upperBounds; this.lowerBounds = lowerBounds; - typeAnnotationMetadata = new AbstractElementAnnotationMetadata() { - - AnnotationMetadata annotationMetadata; - - public AnnotationMetadata getReturnInstance() { - return getAnnotationMetadata(); - } - - @Override - protected MutableAnnotationMetadataDelegate getAnnotationMetadataToWrite() { - return getGenericTypeAnnotationMetadata(); - } - - @Override - public AnnotationMetadata getAnnotationMetadata() { - if (annotationMetadata == null) { - List allAnnotationMetadata = new ArrayList<>(); - getLowerBounds().forEach(ce -> allAnnotationMetadata.add(ce.getTypeAnnotationMetadata())); - getUpperBounds().forEach(ce -> allAnnotationMetadata.add(ce.getTypeAnnotationMetadata())); - allAnnotationMetadata.add(JavaWildcardElement.super.getTypeAnnotationMetadata()); - allAnnotationMetadata.add(getGenericTypeAnnotationMetadata()); - annotationMetadata = new AnnotationMetadataHierarchy(true, allAnnotationMetadata.toArray(AnnotationMetadata[]::new)); - } - return annotationMetadata; - } - }; + typeAnnotationMetadata = new WildcardElementAnnotationMetadata(this, upperBound); } @Override @@ -168,4 +142,5 @@ private JavaClassElement toJavaClassElement(ClassElement element) { } } } + } diff --git a/inject-java/src/test/groovy/io/micronaut/annotation/AnnotateFieldTypeSpec.groovy b/inject-java/src/test/groovy/io/micronaut/annotation/AnnotateFieldTypeSpec.groovy index e7eac7f161e..fe67d97c734 100644 --- a/inject-java/src/test/groovy/io/micronaut/annotation/AnnotateFieldTypeSpec.groovy +++ b/inject-java/src/test/groovy/io/micronaut/annotation/AnnotateFieldTypeSpec.groovy @@ -7,6 +7,7 @@ import io.micronaut.inject.ast.GenericPlaceholderElement import io.micronaut.inject.visitor.TypeElementVisitor import io.micronaut.inject.visitor.VisitorContext +// Annotating the field type doesn't add the annotation to the runtime inject field class AnnotateFieldTypeSpec extends AbstractTypeElementSpec { void 'test annotating'() { @@ -166,12 +167,12 @@ class MyBean1 { void validate(BeanDefinition definition) { def myField1 = definition.getInjectedFields()[0] - myField1.name == "myField1" - myField1.asArgument().getAnnotationMetadata().hasAnnotation(MyAnnotation) + assert myField1.name == "myField1" + assert !myField1.asArgument().getAnnotationMetadata().hasAnnotation(MyAnnotation) def myField2 = definition.getInjectedFields()[1] - myField2.name == "myField2" - !myField2.asArgument().getAnnotationMetadata().hasAnnotation(MyAnnotation) + assert myField2.name == "myField2" + assert !myField2.asArgument().getAnnotationMetadata().hasAnnotation(MyAnnotation) } static class AnnotateFieldTypeVisitor implements TypeElementVisitor { diff --git a/inject-kotlin-test/src/main/groovy/io/micronaut/annotation/processing/test/AbstractKotlinCompilerSpec.groovy b/inject-kotlin-test/src/main/groovy/io/micronaut/annotation/processing/test/AbstractKotlinCompilerSpec.groovy index 9b14f0c32b6..3234495d6e3 100644 --- a/inject-kotlin-test/src/main/groovy/io/micronaut/annotation/processing/test/AbstractKotlinCompilerSpec.groovy +++ b/inject-kotlin-test/src/main/groovy/io/micronaut/annotation/processing/test/AbstractKotlinCompilerSpec.groovy @@ -15,7 +15,6 @@ */ package io.micronaut.annotation.processing.test - import io.micronaut.context.ApplicationContext import io.micronaut.context.Qualifier import io.micronaut.core.annotation.Experimental @@ -30,6 +29,7 @@ import org.intellij.lang.annotations.Language import spock.lang.Specification import java.util.function.Consumer +import java.util.function.Function import java.util.stream.Collectors class AbstractKotlinCompilerSpec extends Specification { @@ -98,6 +98,21 @@ class AbstractKotlinCompilerSpec extends Specification { return true } + /** + * Builds a class element for the given source code. + * @param cls The source + * @return The class element + */ + ClassElement buildClassElementTransformed(String className, @Language("kotlin") String cls, @NonNull Function processor) { + List elements = [] + KotlinCompiler.compile(className, cls, { + if (it.name == className) { + elements.add(processor.apply(it)) + } + }) + return elements.first() + } + Object getBean(ApplicationContext context, String className, Qualifier qualifier = null) { context.getBean(context.classLoader.loadClass(className), qualifier) } @@ -130,32 +145,42 @@ class AbstractKotlinCompilerSpec extends Specification { if (classElement.isArray()) { return "Array<" + reconstructTypeSignature(classElement.fromArray()) + ">" } else if (classElement.isGenericPlaceholder()) { - def freeVar = (GenericPlaceholderElement) classElement - def name = freeVar.variableName + def genericPlaceholderElement = classElement as GenericPlaceholderElement + def name = genericPlaceholderElement.variableName if (typeVarsAsDeclarations) { - def bounds = freeVar.bounds + def bounds = genericPlaceholderElement.bounds if (reconstructTypeSignature(bounds[0]) != 'Object') { - name += bounds.stream().map(AbstractKotlinCompilerSpec::reconstructTypeSignature).collect(Collectors.joining(" & ", " out ", "")) + name += bounds.stream().map(AbstractKotlinCompilerSpec::reconstructTypeSignature).collect(Collectors.joining(" & ", " : ", "")) } + } else if (genericPlaceholderElement.resolved) { + return reconstructTypeSignature(genericPlaceholderElement.resolved.get()) } return name } else if (classElement.isWildcard()) { - def we = (WildcardElement) classElement + def we = classElement as WildcardElement + if (we.isRawType()) { + return "*" + } if (!we.lowerBounds.isEmpty()) { return we.lowerBounds.stream().map(AbstractKotlinCompilerSpec::reconstructTypeSignature).collect(Collectors.joining(" | ", "in ", "")) - } else if (we.upperBounds.size() == 1 && reconstructTypeSignature(we.upperBounds.get(0)) == "Object") { - return "*" } else { return we.upperBounds.stream().map(AbstractKotlinCompilerSpec::reconstructTypeSignature).collect(Collectors.joining(" & ", "out ", "")) } } else { - def boundTypeArguments = classElement.getBoundGenericTypes() - if (boundTypeArguments.isEmpty()) { - return classElement.getSimpleName() + def typeArguments = classElement.getTypeArguments().values() + def simpleName = getSimpleName(classElement) + if (typeArguments.isEmpty()) { + return simpleName } else { - return classElement.getSimpleName() + - boundTypeArguments.stream().map(AbstractKotlinCompilerSpec::reconstructTypeSignature).collect(Collectors.joining(", ", "<", ">")) + return simpleName + typeArguments.stream().map(AbstractKotlinCompilerSpec::reconstructTypeSignature).collect(Collectors.joining(", ", "<", ">")) } } } + + private static String getSimpleName(ClassElement classElement) { + if (classElement.getName() == Object.class.name) { + return "Any" + } + classElement.getSimpleName() + } } diff --git a/inject-kotlin/build.gradle b/inject-kotlin/build.gradle index 8e697424cee..3c9610e8d7d 100644 --- a/inject-kotlin/build.gradle +++ b/inject-kotlin/build.gradle @@ -47,6 +47,7 @@ afterEvaluate { tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all { kotlinOptions { + jvmTarget = "17" freeCompilerArgs = ['-Xjvm-default=all'] } } diff --git a/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/KotlinOutputVisitor.kt b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/KotlinOutputVisitor.kt index 4938996dd6e..1c779666228 100644 --- a/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/KotlinOutputVisitor.kt +++ b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/KotlinOutputVisitor.kt @@ -28,7 +28,7 @@ import java.io.File import java.io.OutputStream import java.util.* -class KotlinOutputVisitor(private val environment: SymbolProcessorEnvironment): AbstractClassWriterOutputVisitor(false) { +internal class KotlinOutputVisitor(private val environment: SymbolProcessorEnvironment): AbstractClassWriterOutputVisitor(false) { override fun visitClass(classname: String, vararg originatingElements: Element): OutputStream { return environment.codeGenerator.createNewFile( @@ -70,7 +70,7 @@ class KotlinOutputVisitor(private val environment: SymbolProcessorEnvironment): val originatingFiles: MutableList = ArrayList(originatingElements.size) for (originatingElement in originatingElements) { if (originatingElement is AbstractKotlinElement<*>) { - val nativeType = originatingElement.nativeType.unwrap().containingFile + val nativeType = originatingElement.nativeType.element.containingFile if (nativeType is KSFile) { originatingFiles.add(nativeType) } diff --git a/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/annotation/KotlinAnnotationMetadataBuilder.kt b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/annotation/KotlinAnnotationMetadataBuilder.kt index 5c8cf83d682..dcab23e5b9c 100644 --- a/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/annotation/KotlinAnnotationMetadataBuilder.kt +++ b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/annotation/KotlinAnnotationMetadataBuilder.kt @@ -40,7 +40,7 @@ import java.lang.annotation.RetentionPolicy import java.lang.reflect.Method import java.util.* -class KotlinAnnotationMetadataBuilder(private val symbolProcessorEnvironment: SymbolProcessorEnvironment, +internal class KotlinAnnotationMetadataBuilder(private val symbolProcessorEnvironment: SymbolProcessorEnvironment, private val resolver: Resolver, private val visitorContext: KotlinVisitorContext): AbstractAnnotationMetadataBuilder() { @@ -104,46 +104,45 @@ class KotlinAnnotationMetadataBuilder(private val symbolProcessorEnvironment: Sy override fun getAnnotationsForType(element: KSAnnotated): MutableList { val annotationMirrors : MutableList = mutableListOf() - if (element is KSValueParameter) { - // fuse annotations for setter and property - val parent = element.parent - if (parent is KSPropertySetter) { - val property = parent.parent + when (element) { + is KSValueParameter -> { + // fuse annotations for setter and property + val parent = element.parent + if (parent is KSPropertySetter) { + val property = parent.parent + if (property is KSPropertyDeclaration) { + annotationMirrors.addAll(property.annotations) + } + annotationMirrors.addAll(parent.annotations) + } + annotationMirrors.addAll(element.annotations) + } + + is KSPropertyGetter, is KSPropertySetter -> { + val property = element.parent if (property is KSPropertyDeclaration) { annotationMirrors.addAll(property.annotations) } - annotationMirrors.addAll(parent.annotations) - } - annotationMirrors.addAll(element.annotations) - } else if (element is KSPropertyGetter || element is KSPropertySetter) { - val property = element.parent - if (property is KSPropertyDeclaration) { - annotationMirrors.addAll(property.annotations) + annotationMirrors.addAll(element.annotations) } - annotationMirrors.addAll(element.annotations) - } else if (element is KSPropertyDeclaration) { - val parent : KSClassDeclaration? = findClassDeclaration(element) - if (parent is KSClassDeclaration) { - if (parent.classKind == ClassKind.ANNOTATION_CLASS) { - annotationMirrors.addAll(element.annotations) - val getter = element.getter - if (getter != null) { - annotationMirrors.addAll(getter.annotations) - } - } else if (parent.modifiers.contains(Modifier.DATA)) { - val parameter = parent.primaryConstructor?.parameters?.find { it.name == element.simpleName } - if (parameter != null) { - annotationMirrors.addAll(parameter.annotations) + + is KSPropertyDeclaration -> { + val parent : KSClassDeclaration? = findClassDeclaration(element) + if (parent is KSClassDeclaration) { + if (parent.classKind == ClassKind.ANNOTATION_CLASS) { + annotationMirrors.addAll(element.annotations) + val getter = element.getter + if (getter != null) { + annotationMirrors.addAll(getter.annotations) + } } - annotationMirrors.addAll(element.annotations) - } else { - annotationMirrors.addAll(element.annotations) } - } else { annotationMirrors.addAll(element.annotations) } - } else { - annotationMirrors.addAll(element.annotations) + + else -> { + annotationMirrors.addAll(element.annotations) + } } val expanded : MutableList = mutableListOf() for (ann in annotationMirrors) { @@ -216,32 +215,64 @@ class KotlinAnnotationMetadataBuilder(private val symbolProcessorEnvironment: Sy if (declaredOnly) { return mutableListOf(element) } - if (element is KSClassDeclaration) { - val hierarchy = mutableListOf() - hierarchy.add(element) - if (element.classKind == ClassKind.ANNOTATION_CLASS) { - return hierarchy + when (element) { + + is KSValueParameter -> { + val parent = element.parent + return if (parent is KSFunctionDeclaration) { + if (parent.isConstructor()) { + mutableListOf(element) + } else { + val parameters = parent.parameters + val parameterIndex = + parameters.indexOf(parameters.find { it.name == element.name }) + methodsHierarchy(parent) + .map { if (it == parent) element else it.parameters[parameterIndex] } + .toMutableList() + } + } else { // Setter + mutableListOf(element) + } } - populateTypeHierarchy(element, hierarchy) - hierarchy.reverse() - return hierarchy - } else if (element is KSFunctionDeclaration) { - return if (element.isConstructor()) { - mutableListOf(element) - } else { - val hierarchy = mutableListOf(element) - var overidden = element.findOverridee() - while (overidden != null) { - hierarchy.add(overidden) - overidden = (overidden as KSFunctionDeclaration).findOverridee() + + is KSClassDeclaration -> { + val hierarchy = mutableListOf() + hierarchy.add(element) + if (element.classKind == ClassKind.ANNOTATION_CLASS) { + return hierarchy } - hierarchy + populateTypeHierarchy(element, hierarchy) + hierarchy.reverse() + return hierarchy + } + + is KSFunctionDeclaration -> { + val methodsHierarchy = methodsHierarchy(element) + val hierarchy = mutableListOf() + hierarchy.addAll(methodsHierarchy) + return hierarchy + } + + else -> { + return mutableListOf(element) } - } else { - return mutableListOf(element) } } + private fun methodsHierarchy(element: KSFunctionDeclaration): List = + if (element.isConstructor()) { + listOf(element) + } else { + val hierarchy = mutableListOf(element) + var overriden = element.findOverridee() as KSFunctionDeclaration? + while (overriden != null) { + hierarchy.add(overriden) + overriden = overriden.findOverridee() as KSFunctionDeclaration? + } + hierarchy.reverse() + hierarchy + } + override fun readAnnotationRawValues( originatingElement: KSAnnotated, annotationName: String, diff --git a/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/annotation/KotlinAnnotations.kt b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/annotation/KotlinAnnotations.kt new file mode 100644 index 00000000000..b2356aa4228 --- /dev/null +++ b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/annotation/KotlinAnnotations.kt @@ -0,0 +1,55 @@ +/* + * Copyright 2017-2023 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.kotlin.processing.annotation + +import com.google.devtools.ksp.symbol.KSAnnotated +import com.google.devtools.ksp.symbol.KSAnnotation +import com.google.devtools.ksp.symbol.KSNode +import com.google.devtools.ksp.symbol.KSVisitor +import com.google.devtools.ksp.symbol.Location +import com.google.devtools.ksp.symbol.Origin + +/** + * A simple annotations container. + * + * @author Denis Stepanov + * @since 4.0.0 + */ +internal class KotlinAnnotations(override val annotations: Sequence) : KSAnnotated { + + override val location: Location + get() { + throw notSupportedMethod() + } + override val origin: Origin + get() { + throw notSupportedMethod() + } + override val parent: KSNode? + get() { + throw notSupportedMethod() + } + + override fun accept(visitor: KSVisitor, data: D): R { + throw notSupportedMethod() + } + + companion object { + private fun notSupportedMethod(): IllegalStateException { + return IllegalStateException("Not supported method") + } + } +} diff --git a/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/annotation/KotlinElementAnnotationMetadataFactory.kt b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/annotation/KotlinElementAnnotationMetadataFactory.kt index ffb636f20fa..822311a32e1 100644 --- a/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/annotation/KotlinElementAnnotationMetadataFactory.kt +++ b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/annotation/KotlinElementAnnotationMetadataFactory.kt @@ -17,14 +17,172 @@ package io.micronaut.kotlin.processing.annotation import com.google.devtools.ksp.symbol.KSAnnotated import com.google.devtools.ksp.symbol.KSAnnotation +import io.micronaut.inject.annotation.AbstractAnnotationMetadataBuilder.CachedAnnotationMetadata +import io.micronaut.inject.ast.ClassElement +import io.micronaut.inject.ast.Element +import io.micronaut.inject.ast.FieldElement +import io.micronaut.inject.ast.GenericElement +import io.micronaut.inject.ast.GenericPlaceholderElement +import io.micronaut.inject.ast.MethodElement +import io.micronaut.inject.ast.PackageElement +import io.micronaut.inject.ast.ParameterElement +import io.micronaut.inject.ast.WildcardElement import io.micronaut.inject.ast.annotation.AbstractElementAnnotationMetadataFactory +import io.micronaut.inject.ast.annotation.ElementAnnotationMetadata import io.micronaut.inject.ast.annotation.ElementAnnotationMetadataFactory +import io.micronaut.inject.processing.ProcessingException +import io.micronaut.kotlin.processing.visitor.AbstractKotlinElement +import io.micronaut.kotlin.processing.visitor.AbstractKotlinMethodElement +import io.micronaut.kotlin.processing.visitor.KotlinClassNativeElement +import io.micronaut.kotlin.processing.visitor.KotlinClassElement +import io.micronaut.kotlin.processing.visitor.KotlinFieldElement +import io.micronaut.kotlin.processing.visitor.KotlinGenericPlaceholderElement +import io.micronaut.kotlin.processing.visitor.KotlinParameterElement +import io.micronaut.kotlin.processing.visitor.KotlinTypeArgumentElement +import io.micronaut.kotlin.processing.visitor.KotlinWildcardElement -class KotlinElementAnnotationMetadataFactory( +internal class KotlinElementAnnotationMetadataFactory( isReadOnly: Boolean, metadataBuilder: KotlinAnnotationMetadataBuilder -) : AbstractElementAnnotationMetadataFactory(isReadOnly, metadataBuilder) { +) : AbstractElementAnnotationMetadataFactory( + isReadOnly, + metadataBuilder +) { + + private val empty: KSAnnotated = KotlinAnnotations(emptySequence()) + override fun readOnly(): ElementAnnotationMetadataFactory { - return KotlinElementAnnotationMetadataFactory(true, metadataBuilder as KotlinAnnotationMetadataBuilder) + return KotlinElementAnnotationMetadataFactory( + true, + metadataBuilder as KotlinAnnotationMetadataBuilder + ) + } + + override fun getNativeElement(element: Element?): KSAnnotated { + return if (element is AbstractKotlinElement<*>) element.nativeType.element else empty + } + + override fun buildGenericTypeAnnotations(element: GenericElement?): ElementAnnotationMetadata { + if (element is KotlinTypeArgumentElement) { + return buildTypeAnnotationsForTypeArgument(element) + } + return super.buildGenericTypeAnnotations(element) + } + + private fun buildTypeAnnotationsForTypeArgument( + element: KotlinTypeArgumentElement + ): AbstractElementAnnotationMetadata { + return object : AbstractElementAnnotationMetadata(null) { + + override fun lookup(): CachedAnnotationMetadata { + if (element.genericNativeType.owner == null) { + throw ProcessingException(element, "Type annotations require the owner element to be specified!") + } + return metadataBuilder.lookupOrBuild( + element.genericNativeType, + element.genericNativeType.declaration + ) + } + + override fun toString(): String { + return element.toString() + } + } + } + + override fun lookupForClass(classElement: ClassElement?): CachedAnnotationMetadata { + val kotlinClassElement = classElement as KotlinClassElement + return metadataBuilder.lookupOrBuild( + getClassDefinitionCacheKey(kotlinClassElement), + classElement.declaration + ) + } + + override fun lookupTypeAnnotationsForClass(classElement: ClassElement): CachedAnnotationMetadata? { + val kotlinClassElement = classElement as KotlinClassElement + if (kotlinClassElement.nativeType.type == null) { + throw ProcessingException(classElement, "Type annotations aren't supported!") + } + if (kotlinClassElement.nativeType.owner == null) { + throw ProcessingException(classElement, "Type annotations require the owner element to be specified!") + } + return metadataBuilder.lookupOrBuild( + kotlinClassElement.nativeType, + KotlinAnnotations(kotlinClassElement.kotlinType.annotations) + ) + } + + override fun lookupTypeAnnotationsForGenericPlaceholder(placeholderElement: GenericPlaceholderElement): CachedAnnotationMetadata? { + val kotlinPlaceholderElement = placeholderElement as KotlinGenericPlaceholderElement + if (kotlinPlaceholderElement.genericNativeType.owner == null) { + throw ProcessingException(placeholderElement, "Type annotations an a generic placeholder require the owner element to be specified!") + } + return metadataBuilder.lookupOrBuild( + kotlinPlaceholderElement.genericNativeType, + kotlinPlaceholderElement.genericNativeType.declaration + ) + } + + override fun lookupTypeAnnotationsForWildcard(wildcardElement: WildcardElement): CachedAnnotationMetadata? { + val kotlinWildcardElement = wildcardElement as KotlinWildcardElement + if (kotlinWildcardElement.genericNativeType.owner == null) { + throw ProcessingException(wildcardElement, "Type annotations on a wildcard require the owner element to be specified!") + } + return metadataBuilder.lookupOrBuild( + kotlinWildcardElement.genericNativeType, + kotlinWildcardElement.genericNativeType.declaration + ) + } + + override fun lookupForPackage(packageElement: PackageElement?): CachedAnnotationMetadata? { + return metadataBuilder.lookupOrBuildForType(getNativeElement(packageElement)) + } + + override fun lookupForParameter(parameterElement: ParameterElement): CachedAnnotationMetadata? { + val kotlinParameterElement = parameterElement as KotlinParameterElement + val owner = kotlinParameterElement.methodElement.owningType as KotlinClassElement + return metadataBuilder.lookupOrBuild( + Key3( + getClassDefinitionCacheKey(owner), + kotlinParameterElement.methodElement.nativeType, + kotlinParameterElement.nativeType + ), + kotlinParameterElement.nativeType.element + ) } + + override fun lookupForField(fieldElement: FieldElement): CachedAnnotationMetadata? { + val kotlinFieldElement = fieldElement as KotlinFieldElement + val owner = kotlinFieldElement.owningType as KotlinClassElement + return metadataBuilder.lookupOrBuild( + Key2( + getClassDefinitionCacheKey(owner), + kotlinFieldElement.nativeType, + ), + kotlinFieldElement.nativeType.element + ) + } + + override fun lookupForMethod(methodElement: MethodElement): CachedAnnotationMetadata? { + val kotlinMethodElement = methodElement as AbstractKotlinMethodElement<*> + val owner = kotlinMethodElement.owningType as KotlinClassElement + return metadataBuilder.lookupOrBuild( + Key2( + getClassDefinitionCacheKey(owner), + kotlinMethodElement.nativeType, + ), + kotlinMethodElement.nativeType.element + ) + } + + private fun getClassDefinitionCacheKey(kotlinClassElement: KotlinClassElement) = + KotlinClassNativeElement( + kotlinClassElement.nativeType.declaration, + null, // Strip the type for the cache key + ) + + data class Key2(private val v1: Any, private val v2: Any) + + data class Key3(private val v1: Any, private val v2: Any, private val v3: Any) + } diff --git a/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/beans/BeanDefinitionProcessor.kt b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/beans/BeanDefinitionProcessor.kt index 006f8bb4d45..0f83136adf3 100644 --- a/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/beans/BeanDefinitionProcessor.kt +++ b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/beans/BeanDefinitionProcessor.kt @@ -25,16 +25,15 @@ import io.micronaut.inject.annotation.AbstractAnnotationMetadataBuilder import io.micronaut.inject.processing.BeanDefinitionCreator import io.micronaut.inject.processing.BeanDefinitionCreatorFactory import io.micronaut.inject.processing.ProcessingException -import io.micronaut.inject.visitor.VisitorConfiguration import io.micronaut.inject.writer.BeanDefinitionReferenceWriter import io.micronaut.inject.writer.BeanDefinitionVisitor import io.micronaut.kotlin.processing.KotlinOutputVisitor -import io.micronaut.kotlin.processing.unwrap import io.micronaut.kotlin.processing.visitor.KotlinClassElement +import io.micronaut.kotlin.processing.visitor.KotlinNativeElement import io.micronaut.kotlin.processing.visitor.KotlinVisitorContext import java.io.IOException -class BeanDefinitionProcessor(private val environment: SymbolProcessorEnvironment): SymbolProcessor { +internal class BeanDefinitionProcessor(private val environment: SymbolProcessorEnvironment): SymbolProcessor { private val beanDefinitionMap = mutableMapOf() @@ -68,7 +67,7 @@ class BeanDefinitionProcessor(private val environment: SymbolProcessorEnvironmen for (classDeclaration in elements) { if (classDeclaration.classKind != ClassKind.ANNOTATION_CLASS) { val classElement = - visitorContext.elementFactory.newClassElement(classDeclaration.asStarProjectedType()) as KotlinClassElement + visitorContext.elementFactory.newClassElement(classDeclaration) as KotlinClassElement val innerClasses = classDeclaration.declarations .filter { it is KSClassDeclaration } @@ -109,15 +108,17 @@ class BeanDefinitionProcessor(private val environment: SymbolProcessorEnvironmen } } - - companion object Helper { fun handleProcessingException(environment: SymbolProcessorEnvironment, e: ProcessingException) { val message = e.message - val originatingNode = (e.originatingElement as KSNode).unwrap() + val originatingNode = (e.originatingElement as KotlinNativeElement).element if (message != null) { environment.logger.error("Originating element: $originatingNode") environment.logger.error(message, originatingNode) + val cause = e.cause + if (cause != null) { + environment.logger.exception(cause) + } } else { environment.logger.error("Unknown error processing element", originatingNode) val cause = e.cause diff --git a/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/beans/BeanDefinitionProcessorProvider.kt b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/beans/BeanDefinitionProcessorProvider.kt index ebcb347eb6a..68918d59135 100644 --- a/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/beans/BeanDefinitionProcessorProvider.kt +++ b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/beans/BeanDefinitionProcessorProvider.kt @@ -19,7 +19,7 @@ import com.google.devtools.ksp.processing.SymbolProcessor import com.google.devtools.ksp.processing.SymbolProcessorEnvironment import com.google.devtools.ksp.processing.SymbolProcessorProvider -class BeanDefinitionProcessorProvider: SymbolProcessorProvider { +internal class BeanDefinitionProcessorProvider: SymbolProcessorProvider { override fun create(environment: SymbolProcessorEnvironment): SymbolProcessor { return BeanDefinitionProcessor(environment) diff --git a/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/extensions.kt b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/extensions.kt index e0e674d4ab8..e863d7a6d47 100644 --- a/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/extensions.kt +++ b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/extensions.kt @@ -19,15 +19,11 @@ import com.google.devtools.ksp.KspExperimental import com.google.devtools.ksp.getJavaClassByName import com.google.devtools.ksp.processing.Resolver import com.google.devtools.ksp.symbol.* -import io.micronaut.inject.ast.Element -import io.micronaut.kotlin.processing.visitor.AbstractKotlinElement -import io.micronaut.kotlin.processing.visitor.KSAnnotatedReference import io.micronaut.kotlin.processing.visitor.KotlinVisitorContext -import java.lang.StringBuilder @OptIn(KspExperimental::class) -fun KSDeclaration.getBinaryName(resolver: Resolver, visitorContext: KotlinVisitorContext): String { - var declaration = unwrap() as KSDeclaration +internal fun KSDeclaration.getBinaryName(resolver: Resolver, visitorContext: KotlinVisitorContext): String { + var declaration = this if (declaration is KSFunctionDeclaration) { val parent = declaration.parentDeclaration if (parent != null) { @@ -60,23 +56,7 @@ private fun computeName(declaration: KSDeclaration): String { return className.toString() } -fun KSNode.unwrap() : KSNode { - return if (this is KSAnnotatedReference) { - this.node - } else { - this - } -} - -fun Element.kspNode() : Any { - return if (this is AbstractKotlinElement<*>) { - this.nativeType.unwrap() - } else { - this.nativeType - } -} - -fun KSPropertySetter.getVisibility(): Visibility { +internal fun KSPropertySetter.getVisibility(): Visibility { val modifierSet = try { this.modifiers } catch (e: IllegalStateException) { @@ -95,7 +75,7 @@ fun KSPropertySetter.getVisibility(): Visibility { } @OptIn(KspExperimental::class) -fun KSAnnotated.getClassDeclaration(visitorContext: KotlinVisitorContext) : KSClassDeclaration { +internal fun KSAnnotated.getClassDeclaration(visitorContext: KotlinVisitorContext) : KSClassDeclaration { when (this) { is KSType -> { return this.declaration.getClassDeclaration(visitorContext) diff --git a/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/AbstractKotlinElement.kt b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/AbstractKotlinElement.kt index 0106ab79c54..ae594f50c8c 100644 --- a/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/AbstractKotlinElement.kt +++ b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/AbstractKotlinElement.kt @@ -15,56 +15,43 @@ */ package io.micronaut.kotlin.processing.visitor -import com.google.devtools.ksp.KspExperimental -import com.google.devtools.ksp.getVisibility -import com.google.devtools.ksp.isJavaPackagePrivate -import com.google.devtools.ksp.isOpen +import com.google.devtools.ksp.* import com.google.devtools.ksp.symbol.* import io.micronaut.core.annotation.AnnotationMetadata -import io.micronaut.core.annotation.AnnotationValue -import io.micronaut.core.annotation.AnnotationValueBuilder import io.micronaut.inject.ast.ClassElement import io.micronaut.inject.ast.Element import io.micronaut.inject.ast.ElementModifier -import io.micronaut.inject.ast.GenericPlaceholderElement -import io.micronaut.inject.ast.annotation.ElementAnnotationMetadata +import io.micronaut.inject.ast.PrimitiveElement +import io.micronaut.inject.ast.WildcardElement +import io.micronaut.inject.ast.annotation.AbstractAnnotationElement import io.micronaut.inject.ast.annotation.ElementAnnotationMetadataFactory -import io.micronaut.inject.ast.annotation.ElementMutableAnnotationMetadataDelegate -import io.micronaut.inject.ast.annotation.MutableAnnotationMetadataDelegate import io.micronaut.kotlin.processing.getBinaryName -import io.micronaut.kotlin.processing.unwrap +import io.micronaut.kotlin.processing.getClassDeclaration import java.util.* -import java.util.function.Consumer -import java.util.function.Predicate -abstract class AbstractKotlinElement(val declaration: T, - protected val annotationMetadataFactory: ElementAnnotationMetadataFactory, - protected val visitorContext: KotlinVisitorContext) : Element, ElementMutableAnnotationMetadataDelegate { +internal abstract class AbstractKotlinElement( + private val nativeType: T, + annotationMetadataFactory: ElementAnnotationMetadataFactory, + protected val visitorContext: KotlinVisitorContext +) : AbstractAnnotationElement(annotationMetadataFactory) { - protected var presetAnnotationMetadata: AnnotationMetadata? = null - private var elementAnnotationMetadata: ElementAnnotationMetadata? = null + private val annotatedInfo = nativeType.element - override fun getNativeType(): T { - return declaration - } + override fun getNativeType(): T = nativeType - override fun isProtected(): Boolean { - return if (declaration is KSDeclaration) { - declaration.getVisibility() == Visibility.PROTECTED - } else { - false - } + override fun isProtected() = if (annotatedInfo is KSDeclaration) { + annotatedInfo.getVisibility() == Visibility.PROTECTED + } else { + false } - override fun isStatic(): Boolean { - return if (declaration is KSDeclaration) { - declaration.modifiers.contains(Modifier.JAVA_STATIC) - } else { - false - } + override fun isStatic() = if (annotatedInfo is KSDeclaration) { + annotatedInfo.modifiers.contains(Modifier.JAVA_STATIC) + } else { + false } - protected fun makeCopy(): AbstractKotlinElement { + private fun makeCopy(): AbstractKotlinElement { val element: AbstractKotlinElement = copyThis() copyValues(element) return element @@ -81,52 +68,40 @@ abstract class AbstractKotlinElement(val declaration: T, protected open fun copyValues(element: AbstractKotlinElement) { element.presetAnnotationMetadata = presetAnnotationMetadata } + override fun withAnnotationMetadata(annotationMetadata: AnnotationMetadata): Element? { val kotlinElement: AbstractKotlinElement = makeCopy() kotlinElement.presetAnnotationMetadata = annotationMetadata return kotlinElement } - override fun getAnnotationMetadata(): MutableAnnotationMetadataDelegate<*> { - if (elementAnnotationMetadata == null) { - - val factory = annotationMetadataFactory - if (presetAnnotationMetadata == null) { - elementAnnotationMetadata = factory.build(this) - } else { - elementAnnotationMetadata = factory.build(this, presetAnnotationMetadata) - } - } - return elementAnnotationMetadata!! + override fun isPublic() = if (annotatedInfo is KSDeclaration) { + annotatedInfo.getVisibility() == Visibility.PUBLIC + } else { + false } - override fun isPublic(): Boolean { - return if (declaration is KSDeclaration) { - declaration.getVisibility() == Visibility.PUBLIC - } else { - false - } + override fun isPrivate() = if (annotatedInfo is KSDeclaration) { + annotatedInfo.getVisibility() == Visibility.PRIVATE + } else { + false } - override fun isPrivate(): Boolean { - return if (declaration is KSDeclaration) { - declaration.getVisibility() == Visibility.PRIVATE - } else { - false - } + override fun isPackagePrivate() = if (annotatedInfo is KSDeclaration) { + annotatedInfo.isJavaPackagePrivate() + } else { + false } - override fun isFinal(): Boolean { - return if (declaration is KSDeclaration) { - !declaration.isOpen() || declaration.modifiers.contains(Modifier.FINAL) - } else { - false - } + override fun isFinal() = if (annotatedInfo is KSDeclaration) { + !annotatedInfo.isOpen() || annotatedInfo.modifiers.contains(Modifier.FINAL) + } else { + false } override fun isAbstract(): Boolean { - return if (declaration is KSModifierListOwner) { - declaration.modifiers.contains(Modifier.ABSTRACT) + return if (annotatedInfo is KSModifierListOwner) { + annotatedInfo.modifiers.contains(Modifier.ABSTRACT) } else { false } @@ -134,9 +109,8 @@ abstract class AbstractKotlinElement(val declaration: T, @OptIn(KspExperimental::class) override fun getModifiers(): MutableSet { - val dec = declaration.unwrap() - if (dec is KSDeclaration) { - val javaModifiers = visitorContext.resolver.effectiveJavaModifiers(dec) + if (annotatedInfo is KSDeclaration) { + val javaModifiers = visitorContext.resolver.effectiveJavaModifiers(annotatedInfo) return javaModifiers.mapNotNull { when (it) { Modifier.ABSTRACT -> ElementModifier.ABSTRACT @@ -158,150 +132,412 @@ abstract class AbstractKotlinElement(val declaration: T, return super.getModifiers() } - override fun annotate( - annotationType: String?, - consumer: Consumer>? - ): Element { - return super.annotate(annotationType, consumer) - } - - override fun annotate(annotationType: String?): Element { - return super.annotate(annotationType) + override fun getDocumentation(): Optional { + return if (annotatedInfo is KSDeclaration) { + Optional.ofNullable(annotatedInfo.docString) + } else { + Optional.empty() + } } - override fun annotate( - annotationType: Class?, - consumer: Consumer>? - ): Element { - return super.annotate(annotationType, consumer) + protected fun resolveDeclaringType( + declaration: KSDeclaration, + owningType: ClassElement + ): ClassElement { + var parent = declaration.parent + if (parent is KSPropertyDeclaration) { + parent = parent.parent + } + if (parent is KSFunctionDeclaration) { + parent = parent.parent + } + return if (parent is KSClassDeclaration) { + val className = parent.getBinaryName(visitorContext.resolver, visitorContext) + if (owningType.name.equals(className)) { + owningType + } else { + val parentTypeArguments = owningType.getTypeArguments(className) + newKotlinClassElement(parent, parentTypeArguments) + } + } else { + owningType + } } - override fun annotate(annotationType: Class?): Element? { - return super.annotate(annotationType) - } - override fun annotate(annotationValue: AnnotationValue?): Element { - return super.annotate(annotationValue) + protected fun resolveTypeArguments( + owner: KotlinNativeElement, + type: KSDeclaration, + parentTypeArguments: Map, + visitedTypes: MutableSet = HashSet() + ): Map { + val typeArguments = mutableMapOf() + val typeParameters = type.typeParameters + typeParameters.forEachIndexed { i, typeParameter -> + typeArguments[typeParameters[i].name.asString()] = + resolveTypeParameter(owner, typeParameter, parentTypeArguments, visitedTypes) + } + return typeArguments } - override fun removeAnnotation(annotationType: String?): Element { - return super.removeAnnotation(annotationType) + protected fun resolveTypeParameter( + owner: KotlinNativeElement, + typeParameter: KSTypeParameter, + parentTypeArguments: Map, + visitedTypes: MutableSet = HashSet() + ): ClassElement { + val variableName = typeParameter.name.asString() + val found = parentTypeArguments[variableName] + if (found is PrimitiveElement) { + return found + } + var bound = found as KotlinClassElement? + if (bound is WildcardElement && !bound.isBounded) { + bound = null + } + val parent = typeParameter.parent + val thisNode = annotatedInfo + val declaringElement = if (thisNode == parent) { + this + } else if (parent is KSClassDeclaration) { + newKotlinClassElement(parent, emptyMap(), visitedTypes, true) + } else { + null + } + val stripTypeArguments = !visitedTypes.add(typeParameter) + val bounds = typeParameter.bounds.map { + val argumentType = it.resolve() + newKotlinClassElement( + owner, + argumentType, + parentTypeArguments, + visitedTypes, + stripTypeArguments + ) + }.ifEmpty { + mutableListOf(getJavaObjectClassElement()).asSequence() + }.toList() + + return KotlinGenericPlaceholderElement( + KotlinTypeParameterNativeElement(typeParameter, owner), + bound, + bounds, + declaringElement, + elementAnnotationMetadataFactory, + visitorContext + ) } - override fun removeAnnotation(annotationType: Class?): Element { - return super.removeAnnotation(annotationType) + private fun getJavaObjectClassElement() = + visitorContext.getClassElement(Object::class.java.name).get() as KotlinClassElement + + protected fun resolveTypeArguments( + owner: KotlinNativeElement, + type: KSType, + parentTypeArguments: Map, + visitedTypes: MutableSet = HashSet() + ): Map { + val typeArguments = mutableMapOf() + val typeParameters = type.declaration.typeParameters + if (type.arguments.isEmpty()) { + typeParameters.forEach { + typeArguments[it.name.asString()] = + resolveTypeParameter(owner, it, parentTypeArguments, visitedTypes) + } + } else { + type.arguments.forEachIndexed { i, typeArgument -> + val variableName = typeParameters[i].name.asString() + typeArguments[variableName] = + resolveTypeArgument(owner, typeArgument, parentTypeArguments, visitedTypes) + } + } + return typeArguments } - override fun removeAnnotationIf(predicate: Predicate>?): Element { - return super.removeAnnotationIf(predicate) + private fun resolveEmptyTypeArguments(declaration: KSClassDeclaration): Map { + val objectElement = getJavaObjectClassElement() + val typeArguments = mutableMapOf() + val typeParameters = declaration.typeParameters + typeParameters.forEach { + typeArguments[it.name.asString()] = objectElement + } + return typeArguments } - override fun removeStereotype(annotationType: String?): Element { - return super.removeStereotype(annotationType) - } + private fun resolveTypeArgument( + owner: KotlinNativeElement, + typeArgument: KSTypeArgument, + parentTypeArguments: Map, + visitedTypes: MutableSet + ): ClassElement { - override fun removeStereotype(annotationType: Class?): Element { - return super.removeStereotype(annotationType) - } + return when (typeArgument.variance) { + Variance.STAR, Variance.COVARIANT, Variance.CONTRAVARIANT -> { + // example List<*>, IN, OUT + val type = typeArgument.type!! + val stripTypeArguments = !visitedTypes.add(type) + val upperBounds = + resolveUpperBounds( + owner, + typeArgument, + parentTypeArguments, + visitedTypes, + stripTypeArguments + ) + val lowerBounds = resolveLowerBounds( + owner, + typeArgument, + parentTypeArguments, + visitedTypes, + stripTypeArguments + ) + val upper = WildcardElement.findUpperType(upperBounds, lowerBounds)!! + KotlinWildcardElement( + KotlinTypeArgumentNativeElement(typeArgument, owner), + upper, + upperBounds, + lowerBounds, + elementAnnotationMetadataFactory, + visitorContext, + typeArgument.variance == Variance.STAR + ) + } - override fun isPackagePrivate(): Boolean { - return if (declaration is KSDeclaration) { - declaration.isJavaPackagePrivate() - } else { - false + // List + else -> { + resolveTypeArgumentType(owner, typeArgument, parentTypeArguments, visitedTypes) + } } } - override fun getDocumentation(): Optional { - return if (declaration is KSDeclaration) { - Optional.ofNullable(declaration.docString) + private fun resolveLowerBounds( + owner: KotlinNativeElement, + typeArgument: KSTypeArgument, + parentTypeArguments: Map, + visitedTypes: MutableSet, + stripTypeArguments: Boolean, + ): List { + return if (typeArgument.variance == Variance.CONTRAVARIANT) { + listOf( + resolveTypeArgumentType( + owner, + typeArgument, + parentTypeArguments, + visitedTypes, + stripTypeArguments + ) as KotlinClassElement + ) } else { - Optional.empty() + return emptyList() } } - override fun getReturnInstance(): Element { - return this + private fun resolveUpperBounds( + owner: KotlinNativeElement, + typeArgument: KSTypeArgument, + parentTypeArguments: Map = emptyMap(), + visitedTypes: MutableSet, + stripTypeArguments: Boolean + ): List { + return when (typeArgument.variance) { + Variance.COVARIANT, Variance.STAR -> { + listOf( + resolveTypeArgumentType( + owner, + typeArgument, + parentTypeArguments, + visitedTypes, + stripTypeArguments + ) as KotlinClassElement + ) + } + + else -> { + val objectType = + visitorContext.resolver.getClassDeclarationByName(Object::class.java.name)!! + listOf( + newKotlinClassElement(objectType, parentTypeArguments, visitedTypes) + ) + } + } } - protected fun resolveGeneric( - parent: KSNode?, - type: ClassElement, - owningClass: ClassElement, - visitorContext: KotlinVisitorContext + protected fun newKotlinClassElement( + declaration: KSClassDeclaration, + parentTypeArguments: Map = emptyMap(), + visitedTypes: MutableSet = HashSet(), + stripTypeArguments: Boolean = false, + ) = newClassElement( + null, + null, + declaration, + parentTypeArguments, + visitedTypes, + false, + stripTypeArguments + ) as KotlinClassElement + + protected fun newClassElement( + declaration: KSClassDeclaration, + parentTypeArguments: Map = emptyMap(), + visitedTypes: MutableSet = HashSet(), + stripTypeArguments: Boolean = false, + ) = newClassElement( + null, + null, + declaration, + parentTypeArguments, + visitedTypes, + true, + stripTypeArguments + ) + + private fun newKotlinClassElement( + owner: KotlinNativeElement?, + type: KSType, + parentTypeArguments: Map = emptyMap(), + visitedTypes: MutableSet = HashSet(), + stripTypeArguments: Boolean = false, + ) = newClassElement( + owner, + type, + type.declaration.getClassDeclaration(visitorContext), + parentTypeArguments, + visitedTypes, + false, + stripTypeArguments + ) as KotlinClassElement + + private fun resolveTypeArgumentType( + owner: KotlinNativeElement, + typeArgument: KSTypeArgument, + parentTypeArguments: Map, + visitedTypes: MutableSet = HashSet(), + stripTypeArguments: Boolean = false ): ClassElement { - var resolvedType = type - if (parent is KSDeclaration && owningClass is KotlinClassElement) { - if (type is GenericPlaceholderElement) { - - val variableName = type.variableName - val genericTypeInfo = owningClass.getGenericTypeInfo() - val boundInfo = genericTypeInfo[parent.getBinaryName(visitorContext.resolver, visitorContext)] - if (boundInfo != null) { - val ksType = boundInfo[variableName] - if (ksType != null) { - resolvedType = visitorContext.elementFactory.newClassElement( - ksType, - visitorContext.elementAnnotationMetadataFactory, - true - ) - if (type.isArray) { - resolvedType = resolvedType.toArray() - } - } - } - } else if (type.declaredGenericPlaceholders.isNotEmpty() && type is KotlinClassElement) { - val genericTypeInfo = owningClass.getGenericTypeInfo() - val kotlinType = type.kotlinType - val boundInfo = if (parent.qualifiedName != null) genericTypeInfo[parent.getBinaryName(visitorContext.resolver, visitorContext)] else null - resolvedType = if (boundInfo != null) { - val boundArgs = kotlinType.arguments.map { arg -> - resolveTypeArgument(arg, boundInfo, visitorContext) - }.toMutableList() - type.withBoundGenericTypes(boundArgs) - } else { - type - } - } + val type = typeArgument.type + val resolvedType = type!!.resolve() + val stripTypeArguments2 = stripTypeArguments || !visitedTypes.add(type) + + val resolved = newTypeArgument( + owner, + resolvedType, + parentTypeArguments, + visitedTypes, + stripTypeArguments2 + ) + if (resolved !is KotlinClassElement || resolved.isGenericPlaceholder) { + return resolved } - return resolvedType + + return KotlinTypeArgumentElement( + KotlinTypeArgumentNativeElement(typeArgument, owner), + resolved, + visitorContext + ) } - private fun resolveTypeArgument( - arg: KSTypeArgument, - boundInfo: Map, - visitorContext: KotlinVisitorContext + protected fun newClassElement( + owner: KotlinNativeElement?, + type: KSType, + parentTypeArguments: Map = emptyMap() + ) = newClassElement( + owner, + type, + type.declaration.getClassDeclaration(visitorContext), + parentTypeArguments, + HashSet() + ) + + private fun newTypeArgument( + owner: KotlinNativeElement?, + type: KSType, + parentTypeArguments: Map, + visitedTypes: MutableSet = HashSet(), + stripTypeArguments: Boolean = false, + ) = newClassElement( + owner, + type, + type.declaration.getClassDeclaration(visitorContext), + parentTypeArguments, + visitedTypes, + false, + stripTypeArguments + ) + + private fun newClassElement( + owner: KotlinNativeElement?, + type: KSType?, + declaration: KSClassDeclaration, + parentTypeArguments: Map, + visitedTypes: MutableSet, + allowPrimitive: Boolean = true, + stripTypeArguments: Boolean = false ): ClassElement { - val n = arg.type?.toString() - val resolved = boundInfo[n] - return if (resolved != null) { - visitorContext.elementFactory.newClassElement( - resolved, - annotationMetadataFactory, + if (type != null) { + val typeDeclaration = type.declaration + if (typeDeclaration is KSTypeParameter) { + return resolveTypeParameter( + owner!!, + typeDeclaration, + parentTypeArguments, + visitedTypes + ) + } + } + val qualifiedName = declaration.qualifiedName!!.asString() + val primitiveArray = primitiveArrays[qualifiedName] + if (primitiveArray != null) { + return primitiveArray + } + val canBePrimitive = + type == null || type.annotations.toList().isEmpty() && !type.isMarkedNullable + if (allowPrimitive && canBePrimitive) { + val element = primitives[qualifiedName] + if (element != null) { + return element + } + } + if (type != null && qualifiedName == "kotlin.Array") { + val component = type.arguments[0].type!!.resolve() + return newTypeArgument( + owner, + component, + parentTypeArguments, + visitedTypes, false + ).toArray() + } + val typeArguments = if (stripTypeArguments) { + resolveEmptyTypeArguments(declaration) + } else if (type == null) { + resolveTypeArguments( + nativeType, + declaration, + parentTypeArguments, + visitedTypes ) } else { - if (arg.type != null) { - val t = arg.type!!.resolve() - if (t.arguments.isNotEmpty()) { - visitorContext.elementFactory.newClassElement( - t, - annotationMetadataFactory, - false - ).withBoundGenericTypes( - t.arguments.map { - resolveTypeArgument(it, boundInfo, visitorContext) - } - ) - } else { - visitorContext.elementFactory.newClassElement( - t, - annotationMetadataFactory, - false - ) - } - } else { - visitorContext.getClassElement(Object::class.java.name).get() - } + resolveTypeArguments( + nativeType, + type, + parentTypeArguments, + visitedTypes + ) + } + return if (declaration.classKind == ClassKind.ENUM_CLASS) { + KotlinEnumElement( + KotlinClassNativeElement(declaration, type, owner), + elementAnnotationMetadataFactory, + visitorContext, + typeArguments + ) + } else { + KotlinClassElement( + KotlinClassNativeElement(declaration, type, owner), + elementAnnotationMetadataFactory, + typeArguments, + visitorContext + ) } } @@ -311,18 +547,42 @@ abstract class AbstractKotlinElement(val declaration: T, override fun equals(other: Any?): Boolean { if (this === other) return true - if (javaClass != other?.javaClass) return false - - other as AbstractKotlinElement<*> - - if (nativeType != other.nativeType) return false - + if (other !is AbstractKotlinElement<*>) return false + if (isAnyOrObject() && other.isAnyOrObject()) return true + if (nativeType.element != other.nativeType.element) return false return true } + private fun isAnyOrObject(): Boolean { + return name.equals(Object::class.java.name) || name.equals(Any::class.java.name) + } + override fun hashCode(): Int { - return nativeType.hashCode() + return nativeType.element.hashCode() } + companion object { + val primitives = mapOf( + "kotlin.Boolean" to PrimitiveElement.BOOLEAN, + "kotlin.Char" to PrimitiveElement.CHAR, + "kotlin.Short" to PrimitiveElement.SHORT, + "kotlin.Int" to PrimitiveElement.INT, + "kotlin.Long" to PrimitiveElement.LONG, + "kotlin.Float" to PrimitiveElement.FLOAT, + "kotlin.Double" to PrimitiveElement.DOUBLE, + "kotlin.Byte" to PrimitiveElement.BYTE, + "kotlin.Unit" to PrimitiveElement.VOID + ) + val primitiveArrays = mapOf( + "kotlin.BooleanArray" to PrimitiveElement.BOOLEAN.toArray(), + "kotlin.CharArray" to PrimitiveElement.CHAR.toArray(), + "kotlin.ShortArray" to PrimitiveElement.SHORT.toArray(), + "kotlin.IntArray" to PrimitiveElement.INT.toArray(), + "kotlin.LongArray" to PrimitiveElement.LONG.toArray(), + "kotlin.FloatArray" to PrimitiveElement.FLOAT.toArray(), + "kotlin.DoubleArray" to PrimitiveElement.DOUBLE.toArray(), + "kotlin.ByteArray" to PrimitiveElement.BYTE.toArray(), + ) + } } diff --git a/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/AbstractKotlinMethodElement.kt b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/AbstractKotlinMethodElement.kt new file mode 100644 index 00000000000..5bf8a1772ee --- /dev/null +++ b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/AbstractKotlinMethodElement.kt @@ -0,0 +1,141 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.kotlin.processing.visitor + +import com.google.devtools.ksp.symbol.KSFunctionDeclaration +import com.google.devtools.ksp.symbol.KSModifierListOwner +import com.google.devtools.ksp.symbol.KSPropertySetter +import com.google.devtools.ksp.symbol.Modifier +import io.micronaut.core.annotation.AnnotationMetadata +import io.micronaut.core.util.ArrayUtils +import io.micronaut.inject.ast.ClassElement +import io.micronaut.inject.ast.GenericPlaceholderElement +import io.micronaut.inject.ast.MemberElement +import io.micronaut.inject.ast.MethodElement +import io.micronaut.inject.ast.ParameterElement +import io.micronaut.inject.ast.annotation.ElementAnnotationMetadataFactory + +internal abstract class AbstractKotlinMethodElement( + private val nativeType: T, + private val name: String, + private val owningType: ClassElement, + annotationMetadataFactory: ElementAnnotationMetadataFactory, + visitorContext: KotlinVisitorContext +) : AbstractKotlinElement(nativeType, annotationMetadataFactory, visitorContext), MethodElement { + + abstract val internalDeclaringType: ClassElement + abstract val internalDeclaredTypeArguments: Map + abstract val internalReturnType: ClassElement + abstract val internalGenericReturnType: ClassElement + abstract val resolvedParameters: List + + override fun getModifiers() = super.getModifiers() + + override fun getDeclaredTypeArguments() = internalDeclaredTypeArguments + + override fun getDeclaredTypeVariables() = + declaredTypeArguments.values.map { it as GenericPlaceholderElement }.toMutableList() + + override fun isSuspend(): Boolean { + val nativeType = nativeType + return if (nativeType is KSModifierListOwner) { + nativeType.modifiers.contains(Modifier.SUSPEND) + } else { + false + } + } + + override fun getSuspendParameters(): Array { + return if (isSuspend) { + val continuationParameter = + visitorContext.getClassElement("kotlin.coroutines.Continuation") + .map { + var rt = internalGenericReturnType + if (rt.isPrimitive && rt.isVoid) { + rt = ClassElement.of(Unit::class.java) + } + val resolvedType = it.withTypeArguments(mapOf("T" to rt)) + ParameterElement.of( + resolvedType, + "continuation" + ) + }.orElse(null) + if (continuationParameter != null) { + ArrayUtils.concat(parameters, continuationParameter) + } else { + parameters + } + } else { + parameters + } + } + + override fun overrides(overridden: MethodElement): Boolean { + if (overridden !is AbstractKotlinElement<*>) { + return false + } + val nativeType = getNativeType().element + val overriddenNativeType = (overridden as AbstractKotlinElement<*>).nativeType.element + if (nativeType == overriddenNativeType) { + return false + } else if (nativeType is KSFunctionDeclaration) { + return overriddenNativeType == nativeType.findOverridee() + } else if (nativeType is KSPropertySetter && overriddenNativeType is KSPropertySetter) { + return overriddenNativeType.receiver == nativeType.receiver.findOverridee() + } + return false + } + + override fun hides(memberElement: MemberElement?) = + // not sure how to implement this correctly for Kotlin + false + + override fun getName() = name + + override fun getOwningType() = owningType + + override fun getDeclaringType() = internalDeclaringType + + override fun getReturnType() = internalReturnType + + override fun getGenericReturnType() = internalGenericReturnType + + override fun getParameters() = resolvedParameters.toTypedArray() + + override fun withAnnotationMetadata(annotationMetadata: AnnotationMetadata) = + super.withAnnotationMetadata(annotationMetadata) as MethodElement + + override fun toString(): String { + return "$simpleName(" + parameters.joinToString(",") { + if (it.type.isGenericPlaceholder) { + (it.type as GenericPlaceholderElement).variableName + } else { + it.genericType.name + } + } + ")" + } + + override fun getThrownTypes() = stringValues(Throws::class.java, "exceptionClasses") + .flatMap { + val ce = visitorContext.getClassElement(it).orElse(null) + if (ce != null) { + listOf(ce) + } else { + emptyList() + } + }.toTypedArray() + +} diff --git a/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/AbstractKotlinPropertyAccessorMethodElement.kt b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/AbstractKotlinPropertyAccessorMethodElement.kt new file mode 100644 index 00000000000..74db7e58164 --- /dev/null +++ b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/AbstractKotlinPropertyAccessorMethodElement.kt @@ -0,0 +1,62 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.kotlin.processing.visitor + +import com.google.devtools.ksp.* +import com.google.devtools.ksp.symbol.* +import io.micronaut.inject.ast.* +import io.micronaut.inject.ast.annotation.ElementAnnotationMetadataFactory +import java.util.* + +@OptIn(KspExperimental::class) +internal abstract class AbstractKotlinPropertyAccessorMethodElement( + nativeType: T, + private val accessor: KSPropertyAccessor, + private val visibility: Visibility, + owningType: ClassElement, + elementAnnotationMetadataFactory: ElementAnnotationMetadataFactory, + visitorContext: KotlinVisitorContext, +) : AbstractKotlinMethodElement( + nativeType, + visitorContext.resolver.getJvmName(accessor)!!, + owningType, + elementAnnotationMetadataFactory, + visitorContext +), MethodElement { + + override val internalDeclaringType: ClassElement by lazy { + resolveDeclaringType(accessor.receiver, owningType) + } + + override val internalDeclaredTypeArguments: Map = emptyMap() + + override fun isSynthetic() = true + + override fun isFinal() = true + + override fun isAbstract(): Boolean = accessor.receiver.isAbstract() + + override fun isPublic() = visibility == Visibility.PUBLIC + + override fun isProtected() = visibility == Visibility.PROTECTED + + override fun isPrivate() = visibility == Visibility.PRIVATE + + override fun hides(memberElement: MemberElement?) = + // not sure how to implement this correctly for Kotlin + false + +} diff --git a/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/AbstractKotlinPropertyElement.kt b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/AbstractKotlinPropertyElement.kt new file mode 100644 index 00000000000..071dc204120 --- /dev/null +++ b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/AbstractKotlinPropertyElement.kt @@ -0,0 +1,126 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.kotlin.processing.visitor + +import com.google.devtools.ksp.symbol.KSDeclaration +import com.google.devtools.ksp.symbol.KSPropertyDeclaration +import io.micronaut.core.annotation.AnnotationMetadata +import io.micronaut.inject.ast.ClassElement +import io.micronaut.inject.ast.FieldElement +import io.micronaut.inject.ast.MemberElement +import io.micronaut.inject.ast.MethodElement +import io.micronaut.inject.ast.ParameterElement +import io.micronaut.inject.ast.PropertyElement +import io.micronaut.inject.ast.annotation.ElementAnnotationMetadataFactory +import io.micronaut.inject.ast.annotation.MutableAnnotationMetadataDelegate +import io.micronaut.inject.ast.annotation.PropertyElementAnnotationMetadata +import java.util.* + +internal abstract class AbstractKotlinPropertyElement( + nativeTypeDef: T, + val ownerType: ClassElement, + private val name: String, + private val excluded: Boolean, + elementAnnotationMetadataFactory: ElementAnnotationMetadataFactory, + visitorContext: KotlinVisitorContext, +) : AbstractKotlinElement( + nativeTypeDef, + elementAnnotationMetadataFactory, + visitorContext +), PropertyElement { + + abstract val resolvedType: ClassElement + abstract val resolvedGenericType: ClassElement + abstract val setter: Optional + abstract val getter: Optional + abstract val fieldElement: Optional + open val constructorParameter: Optional = Optional.empty() + abstract val abstract: Boolean + abstract val declaration: KSDeclaration + + private val internalAnnotationMetadata: MutableAnnotationMetadataDelegate<*> by lazy { + PropertyElementAnnotationMetadata( + this, + getter.orElse(null), + setter.orElse(null), + field.orElse(null), + constructorParameter.orElse(null), + true + ) + } + + private val resolvedDeclaringType: ClassElement by lazy { + resolveDeclaringType(declaration, owningType) + } + + override fun overrides(overridden: PropertyElement?): Boolean { + if (overridden == null || overridden !is AbstractKotlinPropertyElement<*>) { + return false + } + val nativeType = nativeType.element + val overriddenNativeType = overridden.nativeType.element + if (nativeType == overriddenNativeType) { + return false + } else if (nativeType is KSPropertyDeclaration) { + return overriddenNativeType == nativeType.findOverridee() + } + return false + } + + override fun isExcluded() = excluded + + override fun getGenericType() = resolvedGenericType + + override fun getAnnotationMetadata() = internalAnnotationMetadata + + override fun getField() = fieldElement + + override fun getName() = name + + override fun getModifiers() = super.getModifiers() + + override fun getType() = resolvedType + + override fun getDeclaringType() = resolvedDeclaringType + + override fun getOwningType() = ownerType + + override fun getReadMethod() = getter + + override fun getWriteMethod() = setter + + override fun isAbstract() = abstract + + override fun withAnnotationMetadata(annotationMetadata: AnnotationMetadata) = + super.withAnnotationMetadata(annotationMetadata) as MemberElement + + override fun isPrimitive() = type.isPrimitive + + override fun isArray() = type.isArray + + override fun getArrayDimensions() = type.arrayDimensions + + override fun isDeclaredNullable(): Boolean { + val theType = resolvedType + return theType is KotlinClassElement && theType.kotlinType.isMarkedNullable + } + + override fun isNullable(): Boolean { + val theType = resolvedType + return theType is KotlinClassElement && theType.kotlinType.isMarkedNullable + } + +} diff --git a/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/KSAnnotatedReference.kt b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/KSAnnotatedReference.kt deleted file mode 100644 index 92005234a71..00000000000 --- a/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/KSAnnotatedReference.kt +++ /dev/null @@ -1,103 +0,0 @@ -/* - * Copyright 2017-2023 original authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.micronaut.kotlin.processing.visitor - -import com.google.devtools.ksp.symbol.* -import io.micronaut.core.reflect.ReflectionUtils.findMethod - -open class KSAnnotatedReference(open val nativeType: Any, val node: KSNode) { - - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (other !is KSAnnotatedReference) return false - - if (nativeType != other.nativeType) return false - - return true - } - - override fun hashCode(): Int { - return nativeType.hashCode() - } - - companion object Helper { - fun resolveNativeType(nativeType: Any, kind: String): Any { - val javaClass = nativeType.javaClass - val method = findMethod(javaClass, "getKt$kind") - .orElseGet { - findMethod(javaClass, "getPsi").orElseGet { - findMethod(javaClass, "getDescriptor").orElse(null) - } - } - - return if (method != null && method.canAccess(nativeType)) { - method.invoke(nativeType) - } else { - nativeType - } - } - } -} - -class KSClassReference( - private val nt: KSClassDeclaration -) : KSAnnotatedReference(resolveNativeType(nt, "ClassOrObject"), nt), KSClassDeclaration by nt { - override fun toString(): String { - return "Class(${nt.qualifiedName?.asString()})" - } -} - -class KSValueParameterReference( - private val nt: KSValueParameter -) : KSAnnotatedReference(resolveNativeType(nt, "Parameter"), nt), KSValueParameter by nt { - override fun toString(): String { - return "Parameter(${nt.name?.asString()})" - } -} - -class KSPropertyReference( - private val nt: KSPropertyDeclaration -) : KSAnnotatedReference(resolveNativeType(nt, "Property"), nt), KSPropertyDeclaration by nt { - override fun toString(): String { - return "Property(${nt.qualifiedName?.asString()})" - } -} - -class KSPropertySetterReference( - private val nt: KSPropertySetter -) : KSAnnotatedReference(resolveNativeType(nt, "PropertySetter"), nt), KSPropertySetter by nt { - override fun toString(): String { - return "PropertySetter(${nt.receiver.qualifiedName?.asString()})" - } -} - -class KSPropertyGetterReference( - private val nt: KSPropertyGetter -) : KSAnnotatedReference(resolveNativeType(nt, "PropertyGetter"), nt), KSPropertyGetter by nt { - override fun toString(): String { - return "PropertyGetter(${nt.receiver.qualifiedName?.asString()})" - } -} - - -class KSFunctionReference( - private val nt: KSFunctionDeclaration -) : KSAnnotatedReference(resolveNativeType(nt, "Function"), nt), KSFunctionDeclaration by nt { - override fun toString(): String { - return "Function(${nt.qualifiedName?.asString()})" - } -} - diff --git a/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/KotlinClassElement.kt b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/KotlinClassElement.kt index 21c51ed8e3b..3a21bae9d47 100644 --- a/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/KotlinClassElement.kt +++ b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/KotlinClassElement.kt @@ -23,326 +23,322 @@ import io.micronaut.context.annotation.ConfigurationReader import io.micronaut.core.annotation.AnnotationMetadata import io.micronaut.core.annotation.Creator import io.micronaut.core.annotation.NonNull +import io.micronaut.inject.annotation.AnnotationMetadataHierarchy import io.micronaut.inject.ast.* import io.micronaut.inject.ast.annotation.ElementAnnotationMetadataFactory +import io.micronaut.inject.ast.annotation.MutableAnnotationMetadataDelegate import io.micronaut.inject.ast.utils.AstBeanPropertiesUtils import io.micronaut.inject.ast.utils.EnclosedElementsQuery import io.micronaut.inject.processing.ProcessingException import io.micronaut.kotlin.processing.getBinaryName -import io.micronaut.kotlin.processing.getClassDeclaration import java.util.* import java.util.function.Function import java.util.stream.Stream -import kotlin.collections.LinkedHashMap - -open class KotlinClassElement(val kotlinType: KSType, - protected val classDeclaration: KSClassDeclaration, - private val annotationInfo: KSAnnotated, - protected val elementAnnotationMetadataFactory: ElementAnnotationMetadataFactory, - visitorContext: KotlinVisitorContext, - private val arrayDimensions: Int = 0, - private val typeVariable: Boolean = false): AbstractKotlinElement(annotationInfo, elementAnnotationMetadataFactory, visitorContext), ArrayableClassElement { - - constructor( - ref: KSAnnotated, - elementAnnotationMetadataFactory: ElementAnnotationMetadataFactory, - visitorContext: KotlinVisitorContext, - arrayDimensions: Int = 0, - typeVariable: Boolean = false - ) : this(getType(ref, visitorContext), ref.getClassDeclaration(visitorContext), ref, elementAnnotationMetadataFactory, visitorContext, arrayDimensions, typeVariable) - - constructor( - type: KSType, - elementAnnotationMetadataFactory: ElementAnnotationMetadataFactory, - visitorContext: KotlinVisitorContext, - arrayDimensions: Int = 0, - typeVariable: Boolean = false - ) : this(type, type.declaration.getClassDeclaration(visitorContext), type.declaration.getClassDeclaration(visitorContext), elementAnnotationMetadataFactory, visitorContext, arrayDimensions, typeVariable) - - - val outerType: KSType? by lazy { - val outerDecl = classDeclaration.parentDeclaration as? KSClassDeclaration - outerDecl?.asType(kotlinType.arguments.subList(classDeclaration.typeParameters.size, kotlinType.arguments.size)) - } - - private val resolvedProperties : List by lazy { - getBeanProperties(PropertyElementQuery.of(this)) - } - private val enclosedElementsQuery = KotlinEnclosedElementsQuery() - private val nativeProperties : List by lazy { - classDeclaration.getAllProperties() - .filter { !it.isPrivate() } - .map { KotlinPropertyElement( - this, - visitorContext.elementFactory.newClassElement(it.type.resolve(), elementAnnotationMetadataFactory), - it, - elementAnnotationMetadataFactory, visitorContext - ) } - .filter { !it.hasAnnotation(JvmField::class.java) } - .toList() - } - private val internalGenerics : Map> by lazy { - val boundMirrors : Map = getBoundTypeMirrors() - val data = mutableMapOf>() - if (boundMirrors.isNotEmpty()) { - data[this.name] = boundMirrors - } - val classDeclaration = classDeclaration - populateGenericInfo(classDeclaration, data, boundMirrors) - data - } - private val internalCanonicalName : String by lazy { - classDeclaration.qualifiedName!!.asString() - } - private val internalName : String by lazy { - classDeclaration.getBinaryName(visitorContext.resolver, visitorContext) + +internal open class KotlinClassElement( + private val nativeType: KotlinClassNativeElement, + elementAnnotationMetadataFactory: ElementAnnotationMetadataFactory, + var resolvedTypeArguments: Map?, + visitorContext: KotlinVisitorContext, + private val internalArrayDimensions: Int = 0, + private val typeVariable: Boolean = false +) : AbstractKotlinElement( + nativeType, + elementAnnotationMetadataFactory, + visitorContext +), + ArrayableClassElement { + + private val definedType: KSType? by lazy { + nativeType.type } - private var overrideBoundGenericTypes: MutableList? = null - private var resolvedTypeArguments : MutableMap? = null + val declaration: KSClassDeclaration by lazy { + nativeType.declaration + } - private val nt : KSAnnotated = if (annotationInfo is KSTypeArgument) annotationInfo else KSClassReference(classDeclaration) - override fun getNativeType(): KSAnnotated { - return nt + val kotlinType: KSType by lazy { + definedType ?: declaration.asStarProjectedType() } - companion object Helper { - fun getType(ref: KSAnnotated, visitorContext: KotlinVisitorContext) : KSType { - if (ref is KSType) { - return ref - } else if (ref is KSTypeReference) { - return ref.resolve() - } else if (ref is KSTypeParameter) { - return ref.bounds.firstOrNull()?.resolve() ?: visitorContext.resolver.builtIns.anyType - } else if (ref is KSClassDeclaration) { - return ref.asStarProjectedType() - } else if (ref is KSTypeArgument) { - val ksType = ref.type?.resolve() - if (ksType != null) { - return ksType - } else { - throw IllegalArgumentException("Unresolvable type argument $ref") - } - } else if (ref is KSTypeAlias) { - return ref.type.resolve() - } else { - throw IllegalArgumentException("Not a type $ref") - } + private val asType: KotlinClassElement by lazy { + if (definedType == null) { + this + } else { + KotlinClassElement( + KotlinClassNativeElement(declaration), // Strip the kotlin type and the owner + elementAnnotationMetadataFactory, + resolvedTypeArguments, + visitorContext, + arrayDimensions, + typeVariable + ) } + } + private val outerType: KSType? by lazy { + val outerDecl = declaration.parentDeclaration as? KSClassDeclaration + outerDecl?.asType( + kotlinType.arguments.subList( + declaration.typeParameters.size, + kotlinType.arguments.size + ) + ) + } + private val resolvedProperties: List by lazy { + getBeanProperties(PropertyElementQuery.of(this)) + } + private val internalDeclaredGenericPlaceholders: List by lazy { + kotlinType.declaration.typeParameters.map { + resolveTypeParameter(nativeType, it, emptyMap()) as GenericPlaceholderElement + }.toList() } - override fun getName(): String { - return internalName + private val internalFields: List by lazy { + super.getFields() } - override fun getCanonicalName(): String { - return internalCanonicalName + private val internalMethods: List by lazy { + super.getMethods() } - override fun getPackageName(): String { - return classDeclaration.packageName.asString() + private val enclosedElementsQuery = KotlinEnclosedElementsQuery() + + private val nativeProperties: List by lazy { + val properties: MutableList = ArrayList() + var clazz: KotlinClassElement? = this + while (clazz != null) { + // We need to aggregate all the hierarchy properties because + // getAllProperties doesn't return correct parent of the property + properties.addAll(clazz.getDeclaredSyntheticBeanProperties()) + clazz = clazz.superType.orElse(null) as KotlinClassElement? + } + properties } - override fun isDeclaredNullable(): Boolean { - return kotlinType.isMarkedNullable + private val declaredNativeProperties: List by lazy { + declaration.getDeclaredProperties() + .filter { !it.isPrivate() } + .map { + KotlinPropertyElement( + this, + it, + elementAnnotationMetadataFactory, + visitorContext + ) + } + .filter { !it.hasAnnotation(JvmField::class.java) } + .toList() } - override fun isNullable(): Boolean { - return kotlinType.isMarkedNullable + private val internalCanonicalName: String by lazy { + declaration.qualifiedName!!.asString() } - override fun getSyntheticBeanProperties(): List { - return nativeProperties + private val internalName: String by lazy { + declaration.getBinaryName(visitorContext.resolver, visitorContext) } - override fun getAccessibleStaticCreators(): MutableList { - val staticCreators: MutableList = mutableListOf() - staticCreators.addAll(super.getAccessibleStaticCreators()) - return staticCreators.ifEmpty { - val companion = classDeclaration.declarations.filter { - it is KSClassDeclaration && it.isCompanionObject - }.map { it as KSClassDeclaration } - .map { visitorContext.elementFactory.newClassElement(it, elementAnnotationMetadataFactory, false) } - .firstOrNull() - - if (companion != null) { - return companion.getEnclosedElements( - ElementQuery.ALL_METHODS - .annotated { it.hasStereotype( - Creator::class.java - )} - .modifiers { it.isEmpty() || it.contains(ElementModifier.PUBLIC) } - .filter { method -> - method.returnType.isAssignable(this) - } - ) + private val resolvedInterfaces: Collection by lazy { + declaration.superTypes.map { it.resolve() } + .filter { + it != visitorContext.resolver.builtIns.anyType + } + .filter { + val declaration = it.declaration + declaration is KSClassDeclaration && declaration.classKind == ClassKind.INTERFACE + }.map { + newClassElement(nativeType, it, typeArguments) + }.toList() + } + private val resolvedSuperType: Optional by lazy { + val superType = declaration.superTypes.firstOrNull { + val resolved = it.resolve() + if (resolved == visitorContext.resolver.builtIns.anyType) { + false } else { - return mutableListOf() + val declaration = resolved.declaration + declaration is KSClassDeclaration && declaration.classKind != ClassKind.INTERFACE } } + Optional.ofNullable(superType) + .map { + newClassElement(nativeType, it.resolve(), typeArguments) + } } - override fun getBeanProperties(): List { - return resolvedProperties + private val resolvedPrimaryConstructor: Optional by lazy { + val primaryConstructor = super.getPrimaryConstructor() + if (primaryConstructor.isPresent) { + primaryConstructor + } else { + Optional.ofNullable(declaration.primaryConstructor) + .filter { !it.isPrivate() } + .map { + visitorContext.elementFactory.newConstructorElement( + this, + it, + elementAnnotationMetadataFactory + ) + } + } } - override fun getDeclaredGenericPlaceholders(): MutableList { - return kotlinType.declaration.typeParameters.map { - KotlinGenericPlaceholderElement(it, annotationMetadataFactory, visitorContext) - }.toMutableList() + private val resolvedDefaultConstructor: Optional by lazy { + val defaultConstructor = super.getDefaultConstructor() + if (defaultConstructor.isPresent) { + defaultConstructor + } else { + Optional.ofNullable(declaration.primaryConstructor) + .filter { !it.isPrivate() && it.parameters.isEmpty() } + .map { + visitorContext.elementFactory.newConstructorElement( + this, + it, + elementAnnotationMetadataFactory + ) + } + } } - override fun withBoundGenericTypes(typeArguments: MutableList?): ClassElement { - if (typeArguments != null && typeArguments.size == kotlinType.declaration.typeParameters.size) { - val copy = copyThis() - copy.overrideBoundGenericTypes = typeArguments + private val resolvedAnnotationMetadataToWrite: MutableAnnotationMetadataDelegate<*> by lazy { + if (definedType != null) { + resolvedTypeAnnotationMetadata + } else { + super.getAnnotationMetadataToWrite() + } + } - val i = typeArguments.iterator() - copy.resolvedTypeArguments = kotlinType.declaration.typeParameters.associate { - it.name.asString() to i.next() - }.toMutableMap() - return copy + private val resolvedTypeAnnotationMetadata: MutableAnnotationMetadataDelegate by lazy { + if (definedType != null) { + elementAnnotationMetadataFactory.buildTypeAnnotations(this) + } else { + MutableAnnotationMetadataDelegate.EMPTY as MutableAnnotationMetadataDelegate } - return this } - override fun getBoundGenericTypes(): MutableList { - if (overrideBoundGenericTypes == null) { - val arguments = kotlinType.arguments - if (arguments.isEmpty()) { - return mutableListOf() - } else { - val elementFactory = visitorContext.elementFactory - this.overrideBoundGenericTypes = arguments.map { arg -> - when(arg.variance) { - Variance.STAR, Variance.COVARIANT, Variance.CONTRAVARIANT -> KotlinWildcardElement( // example List<*> - resolveUpperBounds(arg, elementFactory, visitorContext), - resolveLowerBounds(arg, elementFactory), - elementAnnotationMetadataFactory, visitorContext - ) - else -> elementFactory.newClassElement( // other cases - arg, - elementAnnotationMetadataFactory, - false - ) - } - }.toMutableList() - } + private val resolvedAnnotationMetadata: AnnotationMetadata by lazy { + if (definedType != null) { + AnnotationMetadataHierarchy( + true, + super.getAnnotationMetadata(), + typeAnnotationMetadata + ) + } else { + super.getAnnotationMetadata() } - return overrideBoundGenericTypes!! - } - - fun getGenericTypeInfo() : Map> { - return this.internalGenerics - } - - private fun populateGenericInfo( - classDeclaration: KSClassDeclaration, - data: MutableMap>, - boundMirrors: Map? - ) { - classDeclaration.superTypes.forEach { - val superType = it.resolve() - if (superType != visitorContext.resolver.builtIns.anyType) { - val declaration = superType.declaration - val name = declaration.qualifiedName?.asString() - val binaryName = declaration.getBinaryName(visitorContext.resolver, visitorContext) - if (name != null && !data.containsKey(name)) { - val typeParameters = declaration.typeParameters - if (typeParameters.isEmpty()) { - data[binaryName] = emptyMap() + } + + override fun getType() = asType + + companion object Helper { + fun getType(ref: KSAnnotated, visitorContext: KotlinVisitorContext): KSType { + when (ref) { + is KSType -> { + return ref + } + + is KSTypeReference -> { + return ref.resolve() + } + + is KSTypeParameter -> { + return ref.bounds.firstOrNull()?.resolve() + ?: visitorContext.resolver.builtIns.anyType + } + + is KSClassDeclaration -> { + return ref.asStarProjectedType() + } + + is KSTypeArgument -> { + val ksType = ref.type?.resolve() + if (ksType != null) { + return ksType } else { - val ksTypeArguments = superType.arguments - if (typeParameters.size == ksTypeArguments.size) { - val resolved = LinkedHashMap() - var i = 0 - typeParameters.forEach { typeParameter -> - val parameterName = typeParameter.name.asString() - val typeArgument = ksTypeArguments[i] - val argumentType = typeArgument.type?.resolve() - val argumentName = argumentType?.declaration?.simpleName?.asString() - val bound = if (argumentName != null ) boundMirrors?.get(argumentName) else null - if (bound != null) { - resolved[parameterName] = bound - } else { - resolved[parameterName] = argumentType ?: typeParameter.bounds.firstOrNull()?.resolve() - ?: visitorContext.resolver.builtIns.anyType - } - i++ - } - data[binaryName] = resolved - } - } - if (declaration is KSClassDeclaration) { - val newBounds = data[binaryName] - populateGenericInfo( - declaration, - data, - newBounds - ) + throw IllegalArgumentException("Unresolvable type argument $ref") } } - } - } - } + is KSTypeAlias -> { + return ref.type.resolve() + } - private fun getBoundTypeMirrors(): Map { - val typeParameters: List = kotlinType.arguments - val parameterIterator = classDeclaration.typeParameters.iterator() - val tpi = typeParameters.iterator() - val map: MutableMap = LinkedHashMap() - while (tpi.hasNext() && parameterIterator.hasNext()) { - val tpe = tpi.next() - val parameter = parameterIterator.next() - val resolvedType = tpe.type?.resolve() - if (resolvedType != null) { - map[parameter.name.asString()] = resolvedType - } else { - map[parameter.name.asString()] = visitorContext.resolver.builtIns.anyType + else -> { + throw IllegalArgumentException("Not a type $ref") + } } } - return Collections.unmodifiableMap(map) } - private fun resolveLowerBounds(arg: KSTypeArgument, elementFactory: KotlinElementFactory): List { - return if (arg.variance == Variance.CONTRAVARIANT) { - listOf( - elementFactory.newClassElement(arg.type?.resolve()!!, elementAnnotationMetadataFactory, false) as KotlinClassElement - ) - } else { - return emptyList() - } - } + override fun getName() = internalName - private fun resolveUpperBounds( - arg: KSTypeArgument, - elementFactory: KotlinElementFactory, - visitorContext: KotlinVisitorContext - ): List { - return if (arg.variance == Variance.COVARIANT) { - listOf( - elementFactory.newClassElement(arg.type?.resolve()!!, elementAnnotationMetadataFactory, false) as KotlinClassElement - ) - } else { - val objectType = visitorContext.resolver.getClassDeclarationByName(Object::class.java.name)!! - listOf( - elementFactory.newClassElement(objectType.asStarProjectedType(), elementAnnotationMetadataFactory, false) as KotlinClassElement + override fun getCanonicalName() = internalCanonicalName + + override fun getPackageName() = declaration.packageName.asString() + + override fun isDeclaredNullable() = kotlinType.isMarkedNullable + + override fun isNullable() = kotlinType.isMarkedNullable + + override fun getSyntheticBeanProperties() = nativeProperties + + private fun getDeclaredSyntheticBeanProperties() = declaredNativeProperties + + override fun getAccessibleStaticCreators(): List { + val staticCreators: MutableList = mutableListOf() + staticCreators.addAll(super.getAccessibleStaticCreators()) + return staticCreators.ifEmpty { + val companion = declaration.declarations + .filter { it is KSClassDeclaration && it.isCompanionObject } + .map { it as KSClassDeclaration } + .map { newKotlinClassElement(it, emptyMap()) } + .firstOrNull() ?: return emptyList() + + return companion.getEnclosedElements( + ElementQuery.ALL_METHODS + .annotated { + it.hasStereotype( + Creator::class.java + ) + } + .modifiers { it.isEmpty() || it.contains(ElementModifier.PUBLIC) } + .filter { method -> + method.returnType.isAssignable(this) + } ) } } + override fun getBeanProperties() = resolvedProperties + + override fun getDeclaredGenericPlaceholders() = internalDeclaredGenericPlaceholders + + override fun getFields() = internalFields + + override fun findField(name: String) = Optional.ofNullable( + internalFields.firstOrNull { it.name == name } + ) + + override fun getMethods() = internalMethods + + override fun findMethod(name: String?) = Optional.ofNullable( + internalMethods.firstOrNull { it.name == name } + ) + override fun getBeanProperties(propertyElementQuery: PropertyElementQuery): MutableList { val customReaderPropertyNameResolver = Function> { Optional.empty() } val customWriterPropertyNameResolver = Function> { Optional.empty() } val accessKinds = propertyElementQuery.accessKinds - val fieldAccess = accessKinds.contains(BeanProperties.AccessKind.FIELD) && !propertyElementQuery.accessKinds.contains(BeanProperties.AccessKind.METHOD) + val fieldAccess = + accessKinds.contains(BeanProperties.AccessKind.FIELD) && !propertyElementQuery.accessKinds.contains( + BeanProperties.AccessKind.METHOD + ) if (fieldAccess) { // all kotlin fields are private return mutableListOf() @@ -350,7 +346,11 @@ open class KotlinClassElement(val kotlinType: KSType, val eq = ElementQuery.of(PropertyElement::class.java) .named { n -> !propertyElementQuery.excludes.contains(n) } - .named { n -> propertyElementQuery.includes.isEmpty() || propertyElementQuery.includes.contains(n) } + .named { n -> + propertyElementQuery.includes.isEmpty() || propertyElementQuery.includes.contains( + n + ) + } .modifiers { val visibility = propertyElementQuery.visibility if (visibility == BeanProperties.Visibility.PUBLIC) { @@ -359,28 +359,33 @@ open class KotlinClassElement(val kotlinType: KSType, !it.contains(ElementModifier.PRIVATE) } }.annotated { prop -> - if(prop.hasAnnotation(JvmField::class.java)) { + if (prop.hasAnnotation(JvmField::class.java)) { false } else { val excludedAnnotations = propertyElementQuery.excludedAnnotations - excludedAnnotations.isEmpty() || !excludedAnnotations.any { prop.hasAnnotation(it) } + excludedAnnotations.isEmpty() || !excludedAnnotations.any { + prop.hasAnnotation( + it + ) + } } } - val allProperties : MutableList = mutableListOf() + val allProperties: MutableList = mutableListOf() // unfortunate hack since these are not excluded? if (hasDeclaredStereotype(ConfigurationReader::class.java)) { val configurationBuilderQuery = ElementQuery.of(PropertyElement::class.java) .annotated { it.hasDeclaredAnnotation(ConfigurationBuilder::class.java) } .onlyInstance() - val configBuilderProps = enclosedElementsQuery.getEnclosedElements(this, configurationBuilderQuery) + val configBuilderProps = + enclosedElementsQuery.getEnclosedElements(this, configurationBuilderQuery) allProperties.addAll(configBuilderProps) } allProperties.addAll(enclosedElementsQuery.getEnclosedElements(this, eq)) val propertyNames = allProperties.map { it.name }.toSet() - val resolvedProperties : MutableList = mutableListOf() + val resolvedProperties: MutableList = mutableListOf() val methodProperties = AstBeanPropertiesUtils.resolveBeanProperties(propertyElementQuery, this, { @@ -408,8 +413,8 @@ open class KotlinClassElement(val kotlinType: KSType, return resolvedProperties } - private fun mapToPropertyElement(value: AstBeanPropertiesUtils.BeanPropertyData): KotlinPropertyElement { - return KotlinPropertyElement( + private fun mapToPropertyElement(value: AstBeanPropertiesUtils.BeanPropertyData) = + KotlinSimplePropertyElement( this@KotlinClassElement, value.type, value.propertyName, @@ -420,20 +425,19 @@ open class KotlinClassElement(val kotlinType: KSType, visitorContext, value.isExcluded ) - } @OptIn(KspExperimental::class) override fun getSimpleName(): String { - var parentDeclaration = classDeclaration.parentDeclaration + var parentDeclaration = declaration.parentDeclaration return if (parentDeclaration == null) { - val qualifiedName = classDeclaration.qualifiedName + val qualifiedName = declaration.qualifiedName if (qualifiedName != null) { visitorContext.resolver.mapKotlinNameToJava(qualifiedName)?.getShortName() - ?: classDeclaration.simpleName.asString() + ?: declaration.simpleName.asString() } else - classDeclaration.simpleName.asString() + declaration.simpleName.asString() } else { - val builder = StringBuilder(classDeclaration.simpleName.asString()) + val builder = StringBuilder(declaration.simpleName.asString()) while (parentDeclaration != null) { builder.insert(0, '$') .insert(0, parentDeclaration.simpleName.asString()) @@ -443,50 +447,21 @@ open class KotlinClassElement(val kotlinType: KSType, } } - override fun getSuperType(): Optional { - val superType = classDeclaration.superTypes.firstOrNull { - val resolved = it.resolve() - if (resolved == visitorContext.resolver.builtIns.anyType) { - false - } else { - val declaration = resolved.declaration - declaration is KSClassDeclaration && declaration.classKind != ClassKind.INTERFACE - } - } - return Optional.ofNullable(superType) - .map { - visitorContext.elementFactory.newClassElement(it.resolve()) - } - } + override fun getSuperType() = resolvedSuperType - override fun getInterfaces(): Collection { - return classDeclaration.superTypes.map { it.resolve() } - .filter { - it != visitorContext.resolver.builtIns.anyType - } - .filter { - val declaration = it.declaration - declaration is KSClassDeclaration && declaration.classKind == ClassKind.INTERFACE - }.map { - visitorContext.elementFactory.newClassElement(it) - }.toList() - } + override fun getInterfaces() = resolvedInterfaces - override fun isStatic(): Boolean { - return if (isInner) { - // inner classes in Kotlin are by default static unless - // the 'inner' keyword is used - !classDeclaration.modifiers.contains(Modifier.INNER) - } else { - super.isStatic() - } + override fun isStatic() = if (isInner) { + // inner classes in Kotlin are by default static unless + // the 'inner' keyword is used + !declaration.modifiers.contains(Modifier.INNER) + } else { + super.isStatic() } - override fun isInterface(): Boolean { - return classDeclaration.classKind == ClassKind.INTERFACE - } + override fun isInterface() = declaration.classKind == ClassKind.INTERFACE - override fun isTypeVariable(): Boolean = typeVariable + override fun isTypeVariable() = typeVariable @OptIn(KspExperimental::class) override fun isAssignable(type: String): Boolean { @@ -496,9 +471,11 @@ open class KotlinClassElement(val kotlinType: KSType, return true } val kotlinName = visitorContext.resolver.mapJavaNameToKotlin( - visitorContext.resolver.getKSNameFromString(type)) + visitorContext.resolver.getKSNameFromString(type) + ) if (kotlinName != null) { - ksType = visitorContext.resolver.getKotlinClassByName(kotlinName)?.asStarProjectedType() + ksType = + visitorContext.resolver.getKotlinClassByName(kotlinName)?.asStarProjectedType() if (ksType != null && kotlinType.starProjection().isAssignableFrom(ksType)) { return true } @@ -515,178 +492,149 @@ open class KotlinClassElement(val kotlinType: KSType, return super.isAssignable(type) } - override fun copyThis(): KotlinClassElement { - val copy = KotlinClassElement( - kotlinType, classDeclaration, annotationInfo, elementAnnotationMetadataFactory, visitorContext, arrayDimensions, typeVariable - ) - copy.resolvedTypeArguments = resolvedTypeArguments - return copy + override fun isPrimitive(): Boolean { + return isVoid } - override fun withTypeArguments(typeArguments: MutableMap?): ClassElement { - val copy = copyThis() - copy.resolvedTypeArguments = typeArguments - return copy + override fun isVoid(): Boolean { + if (internalName == "kotlin.Unit") { + return true + } + return false } - override fun isAbstract(): Boolean { - return classDeclaration.isAbstract() + override fun copyThis() = KotlinClassElement( + nativeType, + elementAnnotationMetadataFactory, + resolvedTypeArguments, + visitorContext, + arrayDimensions, + typeVariable + ) + + override fun withTypeArguments(typeArguments: Map) = KotlinClassElement( + nativeType, + elementAnnotationMetadataFactory, + typeArguments, + visitorContext, + arrayDimensions, + typeVariable + ) + + @NonNull + override fun withTypeArguments(@NonNull typeArguments: Collection): ClassElement? { + if (getTypeArguments() == typeArguments) { + return this + } + if (typeArguments.isEmpty()) { + return withTypeArguments(emptyMap()) + } + val boundByName: MutableMap = LinkedHashMap() + val keys = getTypeArguments().keys + val variableNames: Iterator = keys.iterator() + val args = typeArguments.iterator() + while (variableNames.hasNext() && args.hasNext()) { + var next = args.next() + val nativeType = next.nativeType + if (nativeType is Class<*>) { + next = visitorContext.getClassElement(nativeType).orElse(next) + } + if (nativeType is String) { + next = visitorContext.getClassElement(nativeType).orElse(next) + } + boundByName[variableNames.next()] = next + } + return withTypeArguments(boundByName) } - override fun withAnnotationMetadata(annotationMetadata: AnnotationMetadata): ClassElement { - return super.withAnnotationMetadata(annotationMetadata) as ClassElement - } + override fun isAbstract(): Boolean = declaration.isAbstract() - override fun isArray(): Boolean { - return arrayDimensions > 0 - } + override fun withAnnotationMetadata(annotationMetadata: AnnotationMetadata) = + super.withAnnotationMetadata(annotationMetadata) as ClassElement - override fun getArrayDimensions(): Int { - return arrayDimensions - } + override fun isArray() = arrayDimensions > 0 - override fun withArrayDimensions(arrayDimensions: Int): ClassElement { - return KotlinClassElement(kotlinType, classDeclaration, annotationInfo, elementAnnotationMetadataFactory, visitorContext, arrayDimensions, typeVariable) - } + override fun getArrayDimensions() = internalArrayDimensions - override fun isInner(): Boolean { - return outerType != null - } + override fun withArrayDimensions(arrayDimensions: Int) = KotlinClassElement( + nativeType, + elementAnnotationMetadataFactory, + resolvedTypeArguments, + visitorContext, + arrayDimensions, + typeVariable + ) - override fun getPrimaryConstructor(): Optional { - val primaryConstructor = super.getPrimaryConstructor() - return if (primaryConstructor.isPresent) { - primaryConstructor - } else { - Optional.ofNullable(classDeclaration.primaryConstructor) - .filter { !it.isPrivate() } - .map { visitorContext.elementFactory.newConstructorElement( - this, - it, - elementAnnotationMetadataFactory - ) } - } - } + override fun isInner() = outerType != null - override fun getDefaultConstructor(): Optional { - val defaultConstructor = super.getDefaultConstructor() - return if (defaultConstructor.isPresent) { - defaultConstructor - } else { - Optional.ofNullable(classDeclaration.primaryConstructor) - .filter { !it.isPrivate() && it.parameters.isEmpty() } - .map { visitorContext.elementFactory.newConstructorElement( - this, - it, - elementAnnotationMetadataFactory - ) } - } - } + override fun getPrimaryConstructor() = resolvedPrimaryConstructor + + override fun getDefaultConstructor() = resolvedDefaultConstructor override fun getTypeArguments(): Map { if (resolvedTypeArguments == null) { - val typeArguments = mutableMapOf() - val elementFactory = visitorContext.elementFactory - val typeParameters = kotlinType.declaration.typeParameters - if (kotlinType.arguments.isEmpty()) { - typeParameters.forEach { - typeArguments[it.name.asString()] = KotlinGenericPlaceholderElement(it, annotationMetadataFactory, visitorContext) - } + val ksDeclaration = kotlinType.declaration + resolvedTypeArguments = if (ksDeclaration is KSTypeParameter) { + resolveTypeArguments( + nativeType, + ksDeclaration.bounds.toList()[0].resolve(), + emptyMap() + ) + } else if (definedType != null) { + resolveTypeArguments(nativeType, definedType!!, emptyMap()) } else { - kotlinType.arguments.forEachIndexed { i, argument -> - val typeElement = elementFactory.newClassElement( - argument, - annotationMetadataFactory, - false - ) - typeArguments[typeParameters[i].name.asString()] = typeElement - } + resolveTypeArguments(nativeType, declaration, emptyMap()) } - resolvedTypeArguments = typeArguments } return resolvedTypeArguments!! } - override fun getTypeArguments(type: String): Map { - return allTypeArguments.getOrElse(type) { emptyMap() } - } - - override fun getAllTypeArguments(): Map> { - val genericInfo = getGenericTypeInfo() - return genericInfo.mapValues { entry -> - entry.value.mapValues { data -> - visitorContext.elementFactory.newClassElement(data.value, elementAnnotationMetadataFactory, false) - } - } - } - override fun getEnclosingType(): Optional { if (isInner) { return Optional.of( - visitorContext.elementFactory.newClassElement( - outerType!!, - visitorContext.elementAnnotationMetadataFactory - ) + newClassElement(nativeType, outerType!!, emptyMap()) ) } return Optional.empty() } - override fun getEnclosedElements(@NonNull query: ElementQuery): MutableList { - val classElementToInspect: ClassElement = if (this is GenericPlaceholderElement) { - val bounds: List = this.bounds - if (bounds.isEmpty()) { - return mutableListOf() - } - bounds[0] - } else { - this - } - return enclosedElementsQuery.getEnclosedElements(classElementToInspect, query) + override fun getAnnotationMetadataToWrite() = resolvedAnnotationMetadataToWrite - } + override fun getAnnotationMetadata() = resolvedAnnotationMetadata - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (javaClass != other?.javaClass) return false + override fun getTypeAnnotationMetadata() = resolvedTypeAnnotationMetadata - other as KotlinClassElement + override fun getEnclosedElements(query: ElementQuery): List = + enclosedElementsQuery.getEnclosedElements(this, query) - if (arrayDimensions != other.arrayDimensions) return false - if (typeVariable != other.typeVariable) return false - if (internalCanonicalName != other.internalCanonicalName) return false - if (overrideBoundGenericTypes != other.overrideBoundGenericTypes) return false + private inner class KotlinEnclosedElementsQuery : + EnclosedElementsQuery() { - return true - } + override fun getNativeClassType(classElement: ClassElement): KSClassDeclaration { + return (classElement as KotlinClassElement).nativeType.declaration + } - override fun hashCode(): Int { - var result = arrayDimensions - result = 31 * result + typeVariable.hashCode() - result = 31 * result + internalCanonicalName.hashCode() - result = 31 * result + (overrideBoundGenericTypes?.hashCode() ?: 0) - return result - } + override fun getNativeType(element: Element): KSNode { + return (element as AbstractKotlinElement<*>).nativeType.element + } - private inner class KotlinEnclosedElementsQuery : - EnclosedElementsQuery() { override fun getExcludedNativeElements(result: ElementQuery.Result<*>): Set { if (result.isExcludePropertyElements) { val excludeElements: MutableSet = HashSet() for (excludePropertyElement in beanProperties) { excludePropertyElement.readMethod.ifPresent { methodElement: MethodElement -> excludeElements.add( - methodElement.nativeType as KSNode + getNativeType(methodElement) ) } excludePropertyElement.writeMethod.ifPresent { methodElement: MethodElement -> excludeElements.add( - methodElement.nativeType as KSNode + getNativeType(methodElement) ) } excludePropertyElement.field.ifPresent { fieldElement: FieldElement -> excludeElements.add( - fieldElement.nativeType as KSNode + getNativeType(fieldElement) ) } } @@ -695,18 +643,6 @@ open class KotlinClassElement(val kotlinType: KSType, return emptySet() } - override fun getCacheKey(element: KSNode): KSNode { - return when(element) { - is KSFunctionDeclaration -> KSFunctionReference(element) - is KSPropertyDeclaration -> KSPropertyReference(element) - is KSClassDeclaration -> KSClassReference(element) - is KSValueParameter -> KSValueParameterReference(element) - is KSPropertyGetter -> KSPropertyGetterReference(element) - is KSPropertySetter -> KSPropertySetterReference(element) - else -> element - } - } - override fun getSuperClass(classNode: KSClassDeclaration): KSClassDeclaration? { val superTypes = classNode.superTypes for (superclass in superTypes) { @@ -718,7 +654,6 @@ open class KotlinClassElement(val kotlinType: KSType, } } } - return null } @@ -757,35 +692,45 @@ open class KotlinClassElement(val kotlinType: KSType, getEnclosedElements(classNode, result, MethodElement::class.java).stream() ).toList() } + MethodElement::class.java -> { classNode.getDeclaredFunctions() .filter { func: KSFunctionDeclaration -> !func.isConstructor() && - func.origin != Origin.SYNTHETIC && - // this is a hack but no other way it seems - !listOf("hashCode", "toString", "equals").contains(func.simpleName.asString()) + func.origin != Origin.SYNTHETIC && + // this is a hack but no other way it seems + !listOf( + "hashCode", + "toString", + "equals" + ).contains(func.simpleName.asString()) } .toList() } + FieldElement::class.java -> { classNode.getDeclaredProperties() .filter { it.hasBackingField && - it.origin != Origin.SYNTHETIC + it.origin != Origin.SYNTHETIC } .toList() } + PropertyElement::class.java -> { classNode.getDeclaredProperties().toList() } + ConstructorElement::class.java -> { classNode.getConstructors().toList() } + ClassElement::class.java -> { classNode.declarations.filter { it is KSClassDeclaration }.toList() } + else -> { throw java.lang.IllegalStateException("Unknown result type: $elementType") } @@ -802,26 +747,23 @@ open class KotlinClassElement(val kotlinType: KSType, } override fun toAstElement( - enclosedElement: KSNode, + nativeType: KSNode, elementType: Class<*> ): Element { - var ee = enclosedElement - if (ee is KSAnnotatedReference) { - ee = ee.node - } val elementFactory: KotlinElementFactory = visitorContext.elementFactory - return when (ee) { + val owningClass = this@KotlinClassElement + return when (nativeType) { is KSFunctionDeclaration -> { - if (ee.isConstructor()) { + if (nativeType.isConstructor()) { return elementFactory.newConstructorElement( - this@KotlinClassElement, - ee, + owningClass, + nativeType, elementAnnotationMetadataFactory ) } else { return elementFactory.newMethodElement( - this@KotlinClassElement, - ee, + owningClass, + nativeType, elementAnnotationMetadataFactory ) } @@ -830,44 +772,34 @@ open class KotlinClassElement(val kotlinType: KSType, is KSPropertyDeclaration -> { if (elementType == PropertyElement::class.java) { val prop = KotlinPropertyElement( - this@KotlinClassElement, - visitorContext.elementFactory.newClassElement( - ee.type.resolve(), - elementAnnotationMetadataFactory - ), - ee, + owningClass, + nativeType, elementAnnotationMetadataFactory, visitorContext ) if (!prop.hasAnnotation(JvmField::class.java)) { return prop } else { return elementFactory.newFieldElement( - this@KotlinClassElement, - ee, + owningClass, + nativeType, elementAnnotationMetadataFactory ) } } else { return elementFactory.newFieldElement( - this@KotlinClassElement, - ee, + owningClass, + nativeType, elementAnnotationMetadataFactory ) } } - is KSType -> elementFactory.newClassElement( - ee, - elementAnnotationMetadataFactory - ) - - is KSClassDeclaration -> elementFactory.newClassElement( - ee, - elementAnnotationMetadataFactory, - false + is KSClassDeclaration -> newKotlinClassElement( + nativeType, + emptyMap() ) - else -> throw ProcessingException(this@KotlinClassElement, "Unknown element: $ee") + else -> throw ProcessingException(owningClass, "Unexpected element: $nativeType") } } } diff --git a/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/KotlinConstructorElement.kt b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/KotlinConstructorElement.kt index bb5735799ad..185f1cf94b8 100644 --- a/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/KotlinConstructorElement.kt +++ b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/KotlinConstructorElement.kt @@ -20,20 +20,24 @@ import com.google.devtools.ksp.symbol.KSFunctionDeclaration import com.google.devtools.ksp.symbol.Modifier import io.micronaut.context.annotation.ConfigurationInject import io.micronaut.context.annotation.ConfigurationReader -import io.micronaut.core.annotation.AnnotationMetadata -import io.micronaut.inject.ast.* +import io.micronaut.inject.ast.ClassElement +import io.micronaut.inject.ast.ConstructorElement +import io.micronaut.inject.ast.MemberElement +import io.micronaut.inject.ast.MethodElement import io.micronaut.inject.ast.annotation.ElementAnnotationMetadataFactory -class KotlinConstructorElement(method: KSFunctionDeclaration, - declaringType: ClassElement, - elementAnnotationMetadataFactory: ElementAnnotationMetadataFactory, - visitorContext: KotlinVisitorContext, - returnType: ClassElement -): ConstructorElement, KotlinMethodElement(method, declaringType, returnType, elementAnnotationMetadataFactory, visitorContext) { +internal class KotlinConstructorElement( + owningType: ClassElement, + method: KSFunctionDeclaration, + elementAnnotationMetadataFactory: ElementAnnotationMetadataFactory, + visitorContext: KotlinVisitorContext, +) : ConstructorElement, + KotlinMethodElement(owningType, method, elementAnnotationMetadataFactory, visitorContext) { init { if (method.closestClassDeclaration()?.modifiers?.contains(Modifier.DATA) == true && - declaringType.hasDeclaredStereotype(ConfigurationReader::class.java)) { + owningType.hasDeclaredStereotype(ConfigurationReader::class.java) + ) { annotate(ConfigurationInject::class.java) } } diff --git a/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/KotlinElementFactory.kt b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/KotlinElementFactory.kt index 8d261903dbf..b8807ea8c8e 100644 --- a/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/KotlinElementFactory.kt +++ b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/KotlinElementFactory.kt @@ -15,133 +15,57 @@ */ package io.micronaut.kotlin.processing.visitor -import com.google.devtools.ksp.symbol.* +import com.google.devtools.ksp.symbol.ClassKind +import com.google.devtools.ksp.symbol.KSClassDeclaration +import com.google.devtools.ksp.symbol.KSFunctionDeclaration +import com.google.devtools.ksp.symbol.KSPropertyDeclaration import io.micronaut.core.annotation.AnnotationUtil -import io.micronaut.inject.ast.* +import io.micronaut.inject.ast.ClassElement +import io.micronaut.inject.ast.ConstructorElement +import io.micronaut.inject.ast.ElementFactory +import io.micronaut.inject.ast.EnumConstantElement +import io.micronaut.inject.ast.FieldElement +import io.micronaut.inject.ast.MethodElement import io.micronaut.inject.ast.annotation.ElementAnnotationMetadataFactory -class KotlinElementFactory( - private val visitorContext: KotlinVisitorContext): ElementFactory { - - companion object { - val primitives = mapOf( - "kotlin.Boolean" to PrimitiveElement.BOOLEAN, - "kotlin.Char" to PrimitiveElement.CHAR, - "kotlin.Short" to PrimitiveElement.SHORT, - "kotlin.Int" to PrimitiveElement.INT, - "kotlin.Long" to PrimitiveElement.LONG, - "kotlin.Float" to PrimitiveElement.FLOAT, - "kotlin.Double" to PrimitiveElement.DOUBLE, - "kotlin.Byte" to PrimitiveElement.BYTE, - "kotlin.Unit" to PrimitiveElement.VOID - ) - val primitiveArrays = mapOf( - "kotlin.BooleanArray" to PrimitiveElement.BOOLEAN.toArray(), - "kotlin.CharArray" to PrimitiveElement.CHAR.toArray(), - "kotlin.ShortArray" to PrimitiveElement.SHORT.toArray(), - "kotlin.IntArray" to PrimitiveElement.INT.toArray(), - "kotlin.LongArray" to PrimitiveElement.LONG.toArray(), - "kotlin.FloatArray" to PrimitiveElement.FLOAT.toArray(), - "kotlin.DoubleArray" to PrimitiveElement.DOUBLE.toArray(), - "kotlin.ByteArray" to PrimitiveElement.BYTE.toArray(), - ) - } +internal class KotlinElementFactory( + private val visitorContext: KotlinVisitorContext +) : ElementFactory { fun newClassElement( - type: KSType + type: KSClassDeclaration ): ClassElement { return newClassElement(type, visitorContext.elementAnnotationMetadataFactory) } override fun newClassElement( - type: KSType, + declaration: KSClassDeclaration, annotationMetadataFactory: ElementAnnotationMetadataFactory ): ClassElement { - return newClassElement( - type, - annotationMetadataFactory, - true - ) - } - - override fun newClassElement( - type: KSType, - annotationMetadataFactory: ElementAnnotationMetadataFactory, - resolvedGenerics: Map - ): ClassElement { - return newClassElement( - type, - annotationMetadataFactory, - true - ) - } - - fun newClassElement(annotated: KSAnnotated, - elementAnnotationMetadataFactory: ElementAnnotationMetadataFactory, - allowPrimitive: Boolean): ClassElement { - val type = KotlinClassElement.getType(annotated, visitorContext) - val declaration = type.declaration - val qualifiedName = declaration.qualifiedName!!.asString() - val hasNoAnnotations = !annotated.annotations.iterator().hasNext() - var element = primitiveArrays[qualifiedName] - if (hasNoAnnotations && element != null) { - return element - } - if (qualifiedName == "kotlin.Array") { - val component = type.arguments[0].type!!.resolve() - val componentElement = newClassElement(component, elementAnnotationMetadataFactory, false) - return componentElement.toArray() - } else if (declaration is KSTypeParameter) { - return KotlinGenericPlaceholderElement(declaration, elementAnnotationMetadataFactory, visitorContext) - } - if (allowPrimitive && !type.isMarkedNullable) { - element = primitives[qualifiedName] - if (hasNoAnnotations && element != null ) { - return element - } - } - return if (declaration is KSClassDeclaration && declaration.classKind == ClassKind.ENUM_CLASS) { - KotlinEnumElement(type, elementAnnotationMetadataFactory, visitorContext) - } else { - KotlinClassElement(annotated, elementAnnotationMetadataFactory, visitorContext) - } - } - - fun newClassElement(type: KSType, - elementAnnotationMetadataFactory: ElementAnnotationMetadataFactory, - allowPrimitive: Boolean): ClassElement { - val declaration = type.declaration - val qualifiedName = declaration.qualifiedName!!.asString() - val hasNoAnnotations = !type.annotations.iterator().hasNext() - var element = primitiveArrays[qualifiedName] - if (hasNoAnnotations && element != null) { - return element - } - if (qualifiedName == "kotlin.Array") { - val component = type.arguments[0].type!!.resolve() - val componentElement = newClassElement(component, elementAnnotationMetadataFactory, false) - return componentElement.toArray() - } else if (declaration is KSTypeParameter) { - return KotlinGenericPlaceholderElement(declaration, elementAnnotationMetadataFactory, visitorContext) - } - if (allowPrimitive && !type.isMarkedNullable) { - element = primitives[qualifiedName] - if (hasNoAnnotations && element != null ) { - return element - } - } - return if (declaration is KSClassDeclaration && declaration.classKind == ClassKind.ENUM_CLASS) { - KotlinEnumElement(type, elementAnnotationMetadataFactory, visitorContext) + return if (declaration.classKind == ClassKind.ENUM_CLASS) { + KotlinEnumElement( + KotlinClassNativeElement(declaration), + annotationMetadataFactory, + visitorContext, + null + ) } else { - KotlinClassElement(type, elementAnnotationMetadataFactory, visitorContext) + KotlinClassElement( + KotlinClassNativeElement(declaration), + annotationMetadataFactory, + null, + visitorContext, + 0, + false + ) } } override fun newSourceClassElement( - type: KSType, + declaration: KSClassDeclaration, elementAnnotationMetadataFactory: ElementAnnotationMetadataFactory ): ClassElement { - return newClassElement(type, elementAnnotationMetadataFactory) + return newClassElement(declaration, elementAnnotationMetadataFactory) } override fun newSourceMethodElement( @@ -155,60 +79,34 @@ class KotlinElementFactory( } override fun newMethodElement( - declaringClass: ClassElement, + owningClass: ClassElement, method: KSFunctionDeclaration, elementAnnotationMetadataFactory: ElementAnnotationMetadataFactory - ): KotlinMethodElement { - val returnType = method.returnType!!.resolve() - - val returnTypeElement = newClassElement(returnType, elementAnnotationMetadataFactory) + ): MethodElement { val kotlinMethodElement = KotlinMethodElement( + owningClass, method, - declaringClass, - returnTypeElement, elementAnnotationMetadataFactory, visitorContext ) - if (returnType.isMarkedNullable && !kotlinMethodElement.returnType.isPrimitive) { + if (method.returnType!!.resolve().isMarkedNullable && !kotlinMethodElement.returnType.isPrimitive) { kotlinMethodElement.annotate(AnnotationUtil.NULLABLE) } return kotlinMethodElement } - fun newMethodElement( - declaringClass: ClassElement, - propertyElement: KotlinPropertyElement, - method: KSPropertyGetter, - type: ClassElement, - elementAnnotationMetadataFactory: ElementAnnotationMetadataFactory - ): MethodElement { - return KotlinMethodElement(propertyElement, method, declaringClass, type, elementAnnotationMetadataFactory, visitorContext) - } - - fun newMethodElement( - declaringClass: ClassElement, - propertyElement: KotlinPropertyElement, - method: KSPropertySetter, - type: ClassElement, - elementAnnotationMetadataFactory: ElementAnnotationMetadataFactory - ): MethodElement { - return KotlinMethodElement( - type, - propertyElement, - method, - declaringClass, - elementAnnotationMetadataFactory, - visitorContext - ) - } - override fun newConstructorElement( owningClass: ClassElement, constructor: KSFunctionDeclaration, elementAnnotationMetadataFactory: ElementAnnotationMetadataFactory ): ConstructorElement { - return KotlinConstructorElement(constructor, owningClass, elementAnnotationMetadataFactory, visitorContext, owningClass) + return KotlinConstructorElement( + owningClass, + constructor, + elementAnnotationMetadataFactory, + visitorContext + ) } override fun newFieldElement( @@ -216,7 +114,12 @@ class KotlinElementFactory( field: KSPropertyDeclaration, elementAnnotationMetadataFactory: ElementAnnotationMetadataFactory ): FieldElement { - return KotlinFieldElement(field, owningClass, elementAnnotationMetadataFactory, visitorContext) + return KotlinFieldElement( + owningClass, + field, + elementAnnotationMetadataFactory, + visitorContext + ) } override fun newEnumConstantElement( diff --git a/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/KotlinEnumConstructorElement.kt b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/KotlinEnumConstructorElement.kt index 7eac7e0ba80..6c321f84aa0 100644 --- a/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/KotlinEnumConstructorElement.kt +++ b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/KotlinEnumConstructorElement.kt @@ -19,7 +19,8 @@ import io.micronaut.inject.ast.ClassElement import io.micronaut.inject.ast.MethodElement import io.micronaut.inject.ast.ParameterElement -class KotlinEnumConstructorElement(private val classElement: ClassElement): MethodElement { +internal class KotlinEnumConstructorElement(private val classElement: ClassElement) : + MethodElement { override fun getName(): String = "valueOf" diff --git a/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/KotlinEnumElement.kt b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/KotlinEnumElement.kt index 5ed1963d172..17b8987b09c 100644 --- a/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/KotlinEnumElement.kt +++ b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/KotlinEnumElement.kt @@ -16,35 +16,41 @@ package io.micronaut.kotlin.processing.visitor import com.google.devtools.ksp.symbol.KSClassDeclaration -import com.google.devtools.ksp.symbol.KSType +import io.micronaut.inject.ast.ClassElement import io.micronaut.inject.ast.EnumElement import io.micronaut.inject.ast.MethodElement import io.micronaut.inject.ast.annotation.ElementAnnotationMetadataFactory import java.util.* -class KotlinEnumElement(private val type: KSType, elementAnnotationMetadataFactory: ElementAnnotationMetadataFactory, visitorContext: KotlinVisitorContext): - KotlinClassElement(type, elementAnnotationMetadataFactory, visitorContext), EnumElement { - - override fun values(): List { - return classDeclaration.declarations - .filterIsInstance() - .map { decl -> decl.simpleName.asString() } - .toList() - } - - override fun getDefaultConstructor(): Optional { - return Optional.empty() - } - - override fun copyThis(): KotlinEnumElement { - return KotlinEnumElement( - type, - annotationMetadataFactory, - visitorContext - ) - } - - override fun getPrimaryConstructor(): Optional { - return Optional.of(KotlinEnumConstructorElement(this)) - } +internal class KotlinEnumElement( + nativeType: KotlinClassNativeElement, + elementAnnotationMetadataFactory: ElementAnnotationMetadataFactory, + visitorContext: KotlinVisitorContext, + resolvedTypeArguments: Map? +) : + + KotlinClassElement( + nativeType, + elementAnnotationMetadataFactory, + resolvedTypeArguments, + visitorContext + ), EnumElement { + + override fun values() = declaration.declarations + .filterIsInstance() + .map { ksClassDeclaration -> ksClassDeclaration.simpleName.asString() } + .toList() + + override fun getDefaultConstructor(): Optional = Optional.empty() + + override fun getPrimaryConstructor(): Optional = + Optional.of(KotlinEnumConstructorElement(this)) + + override fun copyThis() = KotlinEnumElement( + nativeType, + elementAnnotationMetadataFactory, + visitorContext, + resolvedTypeArguments + ) + } diff --git a/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/KotlinFieldElement.kt b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/KotlinFieldElement.kt index 286873516ae..50d3ac58b61 100644 --- a/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/KotlinFieldElement.kt +++ b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/KotlinFieldElement.kt @@ -16,84 +16,78 @@ package io.micronaut.kotlin.processing.visitor import com.google.devtools.ksp.symbol.KSPropertyDeclaration +import com.google.devtools.ksp.symbol.KSType import io.micronaut.core.annotation.AnnotationMetadata import io.micronaut.inject.ast.ClassElement -import io.micronaut.inject.ast.ElementModifier import io.micronaut.inject.ast.FieldElement import io.micronaut.inject.ast.annotation.ElementAnnotationMetadataFactory -class KotlinFieldElement(declaration: KSPropertyDeclaration, - private val declaringType: ClassElement, - elementAnnotationMetadataFactory: ElementAnnotationMetadataFactory, - visitorContext: KotlinVisitorContext -) : AbstractKotlinElement(KSPropertyReference(declaration), elementAnnotationMetadataFactory, visitorContext), FieldElement { +internal class KotlinFieldElement( + private val owningType: ClassElement, + var declaration: KSPropertyDeclaration, + elementAnnotationMetadataFactory: ElementAnnotationMetadataFactory, + visitorContext: KotlinVisitorContext +) : AbstractKotlinElement( + KotlinFieldNativeElement(declaration), + elementAnnotationMetadataFactory, + visitorContext +), FieldElement { private val internalName = declaration.simpleName.asString() - private val internalType : ClassElement by lazy { - visitorContext.elementFactory.newClassElement(declaration.type.resolve()) + private val internalDeclaringType: ClassElement by lazy { + resolveDeclaringType(declaration, owningType) } - - private val internalGenericType : ClassElement by lazy { - resolveGeneric(declaration.parent, type, declaringType, visitorContext) + private val internalKSType: KSType by lazy { + declaration.type.resolve() } - - override fun isFinal(): Boolean { - return declaration.setter == null + private val internalType: ClassElement by lazy { + newClassElement(nativeType, internalKSType, emptyMap()) } - - override fun isReflectionRequired(): Boolean { - return true // all Kotlin fields are private + private val internalGenericType: ClassElement by lazy { + newClassElement(nativeType, internalKSType, declaringType.typeArguments) } - override fun isReflectionRequired(callingType: ClassElement?): Boolean { - return true // all Kotlin fields are private - } + override fun isFinal() = declaration.setter == null - override fun isPublic(): Boolean { - return if (hasDeclaredAnnotation(JvmField::class.java)) { - super.isPublic() - } else { - false // all Kotlin fields are private - } - } + override fun isReflectionRequired() = true // all Kotlin fields are private - override fun getType(): ClassElement { - return internalType - } + override fun isReflectionRequired(callingType: ClassElement?) = + true // all Kotlin fields are private - override fun getName(): String { - return internalName + override fun isPublic() = if (hasDeclaredAnnotation(JvmField::class.java)) { + super.isPublic() + } else { + false // all Kotlin fields are private } - override fun getGenericType(): ClassElement { - return internalGenericType - } + override fun getType() = internalType - override fun isPrimitive(): Boolean { - return type.isPrimitive - } + override fun getGenericType() = internalGenericType - override fun isArray(): Boolean { - return type.isArray - } + override fun getName() = internalName - override fun getArrayDimensions(): Int { - return type.arrayDimensions - } + override fun isPrimitive() = type.isPrimitive - override fun copyThis(): AbstractKotlinElement { - return KotlinFieldElement(declaration, declaringType, annotationMetadataFactory, visitorContext) - } + override fun isArray() = type.isArray - override fun isPrivate(): Boolean = true + override fun getArrayDimensions() = type.arrayDimensions - override fun withAnnotationMetadata(annotationMetadata: AnnotationMetadata): FieldElement { - return super.withAnnotationMetadata(annotationMetadata) as FieldElement - } + override fun copyThis() = + KotlinFieldElement( + owningType, + declaration, + elementAnnotationMetadataFactory, + visitorContext + ) - override fun getDeclaringType() = declaringType + override fun isPrivate() = true - override fun getModifiers(): MutableSet { - return super.getModifiers() - } + override fun withAnnotationMetadata(annotationMetadata: AnnotationMetadata) = + super.withAnnotationMetadata(annotationMetadata) as FieldElement + + override fun getOwningType() = owningType + + override fun getDeclaringType() = internalDeclaringType + + override fun getModifiers() = super.getModifiers() } diff --git a/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/KotlinGenericPlaceholderElement.kt b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/KotlinGenericPlaceholderElement.kt index 355dadee52d..221178d9efd 100644 --- a/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/KotlinGenericPlaceholderElement.kt +++ b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/KotlinGenericPlaceholderElement.kt @@ -15,99 +15,119 @@ */ package io.micronaut.kotlin.processing.visitor -import com.google.devtools.ksp.closestClassDeclaration -import com.google.devtools.ksp.symbol.KSTypeParameter import io.micronaut.core.annotation.AnnotationMetadata -import io.micronaut.inject.ast.ArrayableClassElement +import io.micronaut.inject.annotation.AnnotationMetadataHierarchy import io.micronaut.inject.ast.ClassElement import io.micronaut.inject.ast.Element import io.micronaut.inject.ast.GenericPlaceholderElement +import io.micronaut.inject.ast.WildcardElement +import io.micronaut.inject.ast.annotation.ElementAnnotationMetadata import io.micronaut.inject.ast.annotation.ElementAnnotationMetadataFactory -import io.micronaut.kotlin.processing.getBinaryName +import io.micronaut.inject.ast.annotation.GenericPlaceholderElementAnnotationMetadata import java.util.* import java.util.function.Function -class KotlinGenericPlaceholderElement( - private val parameter: KSTypeParameter, +internal class KotlinGenericPlaceholderElement( + private var internalGenericNativeType: KotlinTypeParameterNativeElement, + private var upper: KotlinClassElement, + private var resolved: KotlinClassElement?, + private var bounds: List, + private var declaringElement: Element?, elementAnnotationMetadataFactory: ElementAnnotationMetadataFactory, visitorContext: KotlinVisitorContext, - private val arrayDimensions: Int = 0 -) : KotlinClassElement(parameter, elementAnnotationMetadataFactory, visitorContext, arrayDimensions, true), ArrayableClassElement, GenericPlaceholderElement { - override fun copyThis(): KotlinGenericPlaceholderElement { - return KotlinGenericPlaceholderElement( - parameter, - annotationMetadataFactory, - visitorContext, - arrayDimensions + arrayDimensions: Int = resolved?.arrayDimensions ?: 0 +) : KotlinClassElement( + upper.nativeType, + elementAnnotationMetadataFactory, + upper.resolvedTypeArguments, + visitorContext, + arrayDimensions, + true +), GenericPlaceholderElement { + + constructor( + genericNativeType: KotlinTypeParameterNativeElement, + resolved: KotlinClassElement?, + bounds: List, + declaringElement: Element?, + elementAnnotationMetadataFactory: ElementAnnotationMetadataFactory, + visitorContext: KotlinVisitorContext + ) : this( + genericNativeType, + selectClassElementRepresentingThisPlaceholder(resolved, bounds), + resolved, + bounds, + declaringElement, + elementAnnotationMetadataFactory, + visitorContext + ) + + private val resolvedTypeAnnotationMetadata: ElementAnnotationMetadata by lazy { + GenericPlaceholderElementAnnotationMetadata(this, upper) + } + private val resolvedAnnotationMetadata: AnnotationMetadata by lazy { + AnnotationMetadataHierarchy( + true, + upper.annotationMetadata, + resolvedGenericTypeAnnotationMetadata ) } - - override fun getName(): String { - val bounds = parameter.bounds.firstOrNull() - if (bounds != null) { - return bounds.resolve().declaration.getBinaryName(visitorContext.resolver, visitorContext) - } - return "java.lang.Object" + private val resolvedGenericTypeAnnotationMetadata: ElementAnnotationMetadata by lazy { + elementAnnotationMetadataFactory.buildGenericTypeAnnotations(this) } - override fun withAnnotationMetadata(annotationMetadata: AnnotationMetadata): ClassElement { - return super.withAnnotationMetadata(annotationMetadata) as ClassElement - } + override fun copyThis() = KotlinGenericPlaceholderElement( + genericNativeType, + upper, + resolved, + bounds, + declaringElement, + elementAnnotationMetadataFactory, + visitorContext, + arrayDimensions + ) - override fun isArray(): Boolean = arrayDimensions > 0 + override fun isGenericPlaceholder() = true - override fun getArrayDimensions(): Int = arrayDimensions + override fun getAnnotationMetadataToWrite() = resolvedGenericTypeAnnotationMetadata - override fun withArrayDimensions(arrayDimensions: Int): ClassElement { - return KotlinGenericPlaceholderElement(parameter, annotationMetadataFactory, visitorContext, arrayDimensions) - } + override fun getGenericTypeAnnotationMetadata() = resolvedGenericTypeAnnotationMetadata - override fun getBounds(): MutableList { - val elementFactory = visitorContext.elementFactory - val resolved = parameter.bounds.map { - val argumentType = it.resolve() - elementFactory.newClassElement(argumentType, annotationMetadataFactory) - }.toMutableList() - return if (resolved.isEmpty()) { - mutableListOf(visitorContext.getClassElement(Object::class.java.name).get()) - } else { - resolved - } - } + override fun getTypeAnnotationMetadata() = resolvedTypeAnnotationMetadata - override fun getVariableName(): String { - return parameter.simpleName.asString() - } + override fun getAnnotationMetadata() = resolvedAnnotationMetadata - override fun getDeclaringElement(): Optional { - val classDeclaration = parameter.closestClassDeclaration() - return Optional.ofNullable(classDeclaration).map { - visitorContext.elementFactory.newClassElement( - classDeclaration!!.asStarProjectedType(), - visitorContext.elementAnnotationMetadataFactory) - } - } + override fun getGenericNativeType() = internalGenericNativeType - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (javaClass != other?.javaClass) return false - if (!super.equals(other)) return false + override fun withAnnotationMetadata(annotationMetadata: AnnotationMetadata) = + super.withAnnotationMetadata(annotationMetadata) - other as KotlinGenericPlaceholderElement + override fun withArrayDimensions(arrayDimensions: Int) = KotlinGenericPlaceholderElement( + genericNativeType, + upper, + resolved, + bounds, + declaringElement, + elementAnnotationMetadataFactory, + visitorContext, + arrayDimensions + ) - if (parameter.simpleName.asString() != other.parameter.simpleName.asString()) return false + override fun getBounds() = bounds - return true - } + override fun getVariableName() = genericNativeType.declaration.simpleName.asString() - override fun hashCode(): Int { - var result = super.hashCode() - result = 31 * result + parameter.simpleName.asString().hashCode() - return result - } + override fun getResolved(): Optional = Optional.ofNullable(resolved) + + override fun getDeclaringElement(): Optional = Optional.ofNullable(declaringElement) + + override fun foldBoundGenericTypes(fold: Function) = + fold.apply(this) - override fun foldBoundGenericTypes(fold: Function?): ClassElement { - Objects.requireNonNull(fold, "Function argument cannot be null") - return fold!!.apply(this) + companion object { + private fun selectClassElementRepresentingThisPlaceholder( + resolved: KotlinClassElement?, + bounds: List + ) = resolved ?: WildcardElement.findUpperType(bounds, bounds) } } diff --git a/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/KotlinMethodElement.kt b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/KotlinMethodElement.kt index 8c91c6da0b0..3155a223624 100644 --- a/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/KotlinMethodElement.kt +++ b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/KotlinMethodElement.kt @@ -17,372 +17,107 @@ package io.micronaut.kotlin.processing.visitor import com.google.devtools.ksp.* import com.google.devtools.ksp.symbol.* -import io.micronaut.core.annotation.AnnotationMetadata -import io.micronaut.core.util.ArrayUtils -import io.micronaut.inject.annotation.AnnotationMetadataHierarchy import io.micronaut.inject.ast.* import io.micronaut.inject.ast.annotation.ElementAnnotationMetadataFactory -import io.micronaut.kotlin.processing.getVisibility -import io.micronaut.kotlin.processing.kspNode -import io.micronaut.kotlin.processing.unwrap -import java.util.* -import java.util.function.Supplier -import kotlin.jvm.Throws @OptIn(KspExperimental::class) -open class KotlinMethodElement: AbstractKotlinElement, MethodElement { - - private val name: String - private val owningType: ClassElement - private val internalDeclaringType: ClassElement by lazy { - var parent = declaration.parent - if (parent is KSPropertyDeclaration) { - parent = parent.parent - } - val owner = getOwningType() - if (parent is KSClassDeclaration) { - if (owner.name.equals(parent.qualifiedName)) { - owner - } else { - visitorContext.elementFactory.newClassElement( - parent.asStarProjectedType() - ) - } - } else { - owner - } - } - - private var parameterInit : Supplier> = Supplier { emptyList() } - private val parameters: List by lazy { - parameterInit.get() - } - private val returnType: ClassElement - private val abstract: Boolean - private val public: Boolean - private val private: Boolean - private val protected: Boolean - private val internal: Boolean - private val propertyElement : KotlinPropertyElement? - - constructor(propertyType : ClassElement, - propertyElement: KotlinPropertyElement, - method: KSPropertySetter, - owningType: ClassElement, - elementAnnotationMetadataFactory: ElementAnnotationMetadataFactory, - visitorContext: KotlinVisitorContext - ) : super(KSPropertySetterReference(method), elementAnnotationMetadataFactory, visitorContext) { - this.name = visitorContext.resolver.getJvmName(method)!! - this.propertyElement = propertyElement - this.owningType = owningType - this.returnType = PrimitiveElement.VOID - this.abstract = method.receiver.isAbstract() - val visibility = method.getVisibility() - this.public = visibility == Visibility.PUBLIC - this.private = visibility == Visibility.PRIVATE - this.protected = visibility == Visibility.PROTECTED - this.internal = visibility == Visibility.INTERNAL - this.parameterInit = Supplier { - val parameterElement = KotlinParameterElement( - propertyType, this, method.parameter, elementAnnotationMetadataFactory, visitorContext - ) - listOf(parameterElement) - } - } +internal open class KotlinMethodElement( + owningType: ClassElement, + val declaration: KSFunctionDeclaration, + private val presetParameters: List?, + elementAnnotationMetadataFactory: ElementAnnotationMetadataFactory, + visitorContext: KotlinVisitorContext +) : AbstractKotlinMethodElement( + KotlinMethodNativeElement(declaration), + visitorContext.resolver.getJvmName(declaration)!!, + owningType, + elementAnnotationMetadataFactory, + visitorContext +), MethodElement { constructor( - propertyElement: KotlinPropertyElement, - method: KSPropertyGetter, owningType: ClassElement, - returnType: ClassElement, + declaration: KSFunctionDeclaration, elementAnnotationMetadataFactory: ElementAnnotationMetadataFactory, - visitorContext: KotlinVisitorContext, - ) : super(KSPropertyGetterReference(method), elementAnnotationMetadataFactory, visitorContext) { - this.name = visitorContext.resolver.getJvmName(method)!! - this.propertyElement = propertyElement - this.owningType = owningType - this.parameterInit = Supplier { emptyList() } - this.returnType = returnType - this.abstract = method.receiver.isAbstract() - this.public = method.receiver.isPublic() - this.private = method.receiver.isPrivate() - this.protected = method.receiver.isProtected() - this.internal = method.receiver.isInternal() + visitorContext: KotlinVisitorContext + ) : this( + owningType, + declaration, + null, + elementAnnotationMetadataFactory, + visitorContext + ) + + override val internalDeclaringType: ClassElement by lazy { + resolveDeclaringType(declaration, owningType) + } + + override val internalDeclaredTypeArguments: Map by lazy { + resolveTypeArguments(nativeType, declaration, emptyMap()) } - constructor(method: KSFunctionDeclaration, - owningType: ClassElement, - returnType: ClassElement, - elementAnnotationMetadataFactory: ElementAnnotationMetadataFactory, - visitorContext: KotlinVisitorContext - ) : super(KSFunctionReference(method), elementAnnotationMetadataFactory, visitorContext) { - this.name = visitorContext.resolver.getJvmName(method)!! - this.owningType = owningType - this.parameterInit = Supplier { - method.parameters.map { - val t = visitorContext.elementFactory.newClassElement( - it.type.resolve(), - elementAnnotationMetadataFactory) + override val resolvedParameters: List by lazy { + presetParameters + ?: declaration.parameters.map { KotlinParameterElement( - t, + null, this, it, elementAnnotationMetadataFactory, visitorContext ) } - } - this.propertyElement = null - this.returnType = returnType - this.abstract = method.isAbstract - this.public = method.isPublic() - this.private = method.isPrivate() - this.protected = method.isProtected() - this.internal = method.isInternal() - } - - protected constructor(method: KSAnnotated, - name: String, - owningType: ClassElement, - elementAnnotationMetadataFactory: ElementAnnotationMetadataFactory, - visitorContext: KotlinVisitorContext, - returnType: ClassElement, - parameters: List, - abstract: Boolean, - public: Boolean, - private: Boolean, - protected: Boolean, - internal: Boolean - ) : super(method, elementAnnotationMetadataFactory, visitorContext) { - this.name = name - this.owningType = owningType - this.parameterInit = Supplier { - parameters - } - this.propertyElement = null - this.returnType = returnType - this.abstract = abstract - this.public = public - this.private = private - this.protected = protected - this.internal = internal - } - - override fun getOwningType(): ClassElement { - return owningType - } - - override fun isSynthetic(): Boolean { - return if (declaration is KSPropertyGetter || declaration is KSPropertySetter) { - return true - } else { - if (declaration is KSFunctionDeclaration) { - return declaration.functionKind != FunctionKind.MEMBER && declaration.functionKind != FunctionKind.STATIC - } else { - return false - } - } } - override fun isFinal(): Boolean { - return if (declaration is KSPropertyGetter || declaration is KSPropertySetter) { - true - } else { - super.isFinal() - } + override val internalReturnType: ClassElement by lazy { + newClassElement(nativeType, declaration.returnType!!.resolve(), emptyMap()) } - override fun getModifiers(): MutableSet { - return super.getModifiers() + override val internalGenericReturnType: ClassElement by lazy { + newClassElement(nativeType, declaration.returnType!!.resolve(), declaringType.typeArguments) } - override fun getDeclaredTypeVariables(): MutableList { - val nativeType = kspNode() - return if (nativeType is KSDeclaration) { - nativeType.typeParameters.map { - KotlinGenericPlaceholderElement(it, annotationMetadataFactory, visitorContext) - }.toMutableList() - } else { - super.getDeclaredTypeVariables() - } - } + override fun isAbstract(): Boolean = declaration.isAbstract - override fun isSuspend(): Boolean { - val nativeType = nativeType - return if (nativeType is KSModifierListOwner) { - nativeType.modifiers.contains(Modifier.SUSPEND) - } else { - false - } - } + override fun isPublic(): Boolean = declaration.isPublic() - override fun getSuspendParameters(): Array { - val parameters = getParameters() - return if (isSuspend) { - val continuationParameter = visitorContext.getClassElement("kotlin.coroutines.Continuation") - .map { - var rt = genericReturnType - if (rt.isPrimitive && rt.name.equals("void")) { - rt = ClassElement.of(Unit::class.java) - } - val resolvedType = it.withTypeArguments(mapOf("T" to rt)) - ParameterElement.of( - resolvedType, - "continuation" - ) - }.orElse(null) - if (continuationParameter != null) { + override fun isProtected(): Boolean = declaration.isProtected() - ArrayUtils.concat(parameters, continuationParameter) - } else { - parameters - } - } else { - parameters - } - } + override fun isPrivate(): Boolean = declaration.isPrivate() - override fun overrides(overridden: MethodElement): Boolean { - val nativeType = kspNode() - val overriddenNativeType = overridden.kspNode() - if (nativeType == overriddenNativeType) { - return false - } else if (nativeType is KSFunctionDeclaration) { - return overriddenNativeType == nativeType.findOverridee() - } else if (nativeType is KSPropertySetter && overriddenNativeType is KSPropertySetter) { - return overriddenNativeType.receiver == nativeType.receiver.findOverridee() - } - return false - } + override fun isSynthetic() = + declaration.functionKind != FunctionKind.MEMBER && declaration.functionKind != FunctionKind.STATIC - override fun hides(memberElement: MemberElement?): Boolean { - // not sure how to implement this correctly for Kotlin - return false - } + override fun isSuspend() = declaration.modifiers.contains(Modifier.SUSPEND) override fun withNewOwningType(owningType: ClassElement): MethodElement { val newMethod = KotlinMethodElement( + owningType, declaration, - name, - owningType as KotlinClassElement, - annotationMetadataFactory, + presetParameters, + elementAnnotationMetadataFactory, visitorContext, - returnType, - parameters, - abstract, - public, - private, - protected, - internal ) copyValues(newMethod) return newMethod } - override fun getName(): String { - return name - } - - override fun getDeclaringType(): ClassElement { - return internalDeclaringType - } - - override fun getReturnType(): ClassElement { - return returnType - } - - override fun getGenericReturnType(): ClassElement { - return if (this is ConstructorElement) { - returnType - } else { - resolveGeneric(declaration.parent, returnType, owningType, visitorContext) - } - } - - override fun getParameters(): Array { - return parameters.toTypedArray() - } - - override fun isAbstract(): Boolean = abstract - - override fun isPublic(): Boolean = public - - override fun isProtected(): Boolean = protected override fun copyThis(): KotlinMethodElement { - if (declaration is KSPropertySetter) { - return KotlinMethodElement( - parameters[0].type, - propertyElement!!, - declaration.unwrap() as KSPropertySetter, - owningType, - annotationMetadataFactory, - visitorContext - ) - } else if (declaration is KSPropertyGetter) { - return KotlinMethodElement( - propertyElement!!, - declaration.unwrap() as KSPropertyGetter, - owningType, - returnType, - annotationMetadataFactory, - visitorContext - ) - } else if (declaration is KSFunctionDeclaration) { - return KotlinMethodElement( - declaration.unwrap() as KSFunctionDeclaration, - owningType, - returnType, - annotationMetadataFactory, - visitorContext - ) - } else { - - return KotlinMethodElement( - declaration, - name, - owningType, - annotationMetadataFactory, - visitorContext, - returnType, - parameters, - abstract, - public, - private, - protected, - internal - ) - } - } - - override fun isPrivate(): Boolean = private - override fun withAnnotationMetadata(annotationMetadata: AnnotationMetadata): MethodElement { - return super.withAnnotationMetadata(annotationMetadata) as MethodElement - } - - override fun toString(): String { - return "$simpleName(" + parameters.joinToString(",") { - if (it.type.isGenericPlaceholder) { - (it.type as GenericPlaceholderElement).variableName - } else { - it.genericType.name - } - } + ")" - } - - override fun withParameters(vararg newParameters: ParameterElement): MethodElement { - return KotlinMethodElement(declaration, name, owningType, annotationMetadataFactory, visitorContext, returnType, newParameters.toList(), abstract, public, private, protected, internal) - } - - override fun getThrownTypes(): Array { - return stringValues(Throws::class.java, "exceptionClasses") - .flatMap { - val ce = visitorContext.getClassElement(it).orElse(null) - if (ce != null) { - listOf(ce) - } else { - emptyList() - } - }.toTypedArray() + return KotlinMethodElement( + owningType, + declaration, + presetParameters, + elementAnnotationMetadataFactory, + visitorContext, + ) } + override fun withParameters(vararg newParameters: ParameterElement) = + KotlinMethodElement( + owningType, + declaration, + newParameters.toList(), + elementAnnotationMetadataFactory, + visitorContext, + ) } diff --git a/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/KotlinNativeElement.kt b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/KotlinNativeElement.kt new file mode 100644 index 00000000000..6e2b592edd7 --- /dev/null +++ b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/KotlinNativeElement.kt @@ -0,0 +1,244 @@ +/* + * Copyright 2017-2023 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.kotlin.processing.visitor + +import com.google.devtools.ksp.symbol.KSAnnotated +import com.google.devtools.ksp.symbol.KSClassDeclaration +import com.google.devtools.ksp.symbol.KSFunctionDeclaration +import com.google.devtools.ksp.symbol.KSPropertyDeclaration +import com.google.devtools.ksp.symbol.KSPropertyGetter +import com.google.devtools.ksp.symbol.KSPropertySetter +import com.google.devtools.ksp.symbol.KSType +import com.google.devtools.ksp.symbol.KSTypeArgument +import com.google.devtools.ksp.symbol.KSTypeParameter +import com.google.devtools.ksp.symbol.KSValueParameter +import io.micronaut.core.annotation.Internal +import io.micronaut.core.reflect.ReflectionUtils.findMethod +import io.micronaut.inject.ast.ClassElement +import io.micronaut.inject.ast.FieldElement +import io.micronaut.inject.ast.MethodElement + +@Internal +open class KotlinNativeElement( + val element: KSAnnotated, + val owner: KotlinNativeElement?, + internal val kotlinNativeType: Any +) { + + constructor(element: KSAnnotated) : this(element, null, resolveKotlinNativeType(element)) + + constructor(element: KSAnnotated, owner: KotlinNativeElement? = null) : this( + element, + owner, + resolveKotlinNativeType(element) + ) + + companion object Helper { + fun resolveKotlinNativeType(nativeType: Any): Any { + + val kind: String = when (nativeType) { + is KSClassDeclaration -> "ClassOrObject" + is KSValueParameter -> "Parameter" + is KSPropertyDeclaration -> "Property" + is KSPropertySetter -> "PropertySetter" + is KSPropertyGetter -> "PropertyGetter" + is KSFunctionDeclaration -> "Function" + is KSTypeArgument -> "TypeArgument" + is KSTypeParameter -> "TypeParameter" + else -> throw IllegalStateException("Unknown native type ${nativeType.javaClass}") + } + + val javaClass = nativeType.javaClass + val method = findMethod(javaClass, "getKt$kind") + .orElseGet { + findMethod(javaClass, "getPsi").orElseGet { + findMethod(javaClass, "getDescriptor").orElse(null) + } + } + + return if (method != null && method.canAccess(nativeType)) { + method.invoke(nativeType) + } else { + nativeType + } + } + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is KotlinNativeElement) return false + if (kotlinNativeType != other.kotlinNativeType) return false + if (owner != other.owner) return false + return true + } + + override fun hashCode(): Int { + return kotlinNativeType.hashCode() + } + +} + +internal class KotlinClassNativeElement( + val declaration: KSClassDeclaration, + val type: KSType? = null, + owner: KotlinNativeElement? = null +) : KotlinNativeElement(declaration, owner) { + + init { + if (type != null && owner == null) { + throw IllegalStateException("Missing") + } + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is KotlinClassNativeElement) return false + if (kotlinNativeType != other.kotlinNativeType) return false + if (owner != other.owner) return false + if (type != other.type) return false + return true + } + + override fun hashCode(): Int { + return kotlinNativeType.hashCode() + } + + override fun toString(): String { + return "KotlinClassNativeElement(declaration=$declaration, type=$type)" + } + +} + +class KotlinMethodNativeElement( + val declaration: KSFunctionDeclaration, +) : KotlinNativeElement(declaration) { + + override fun toString(): String { + return "KotlinMethodNativeElement(declaration=$declaration)" + } +} + +class KotlinFieldNativeElement( + val declaration: KSPropertyDeclaration, +) : KotlinNativeElement(declaration) { + + override fun toString(): String { + return "KotlinFieldNativeElement(declaration=$declaration)" + } +} + +class KotlinMethodParameterNativeElement( + val declaration: KSValueParameter, + val method: KotlinNativeElement +) : KotlinNativeElement(declaration, method) { + + override fun toString(): String { + return "KotlinMethodParameterNativeElement(declaration=$declaration, method=$method)" + } +} + +class KotlinPropertySetterNativeElement( + val declaration: KSPropertySetter, +) : KotlinNativeElement(declaration) { + + override fun toString(): String { + return "KotlinPropertySetterNativeElement(declaration=$declaration)" + } +} + +class KotlinPropertyGetterNativeElement( + val declaration: KSPropertyGetter, +) : KotlinNativeElement(declaration) { + + override fun toString(): String { + return "KotlinPropertyGetterNativeElement(declaration=$declaration)" + } +} + +class KotlinTypeParameterNativeElement( + val declaration: KSTypeParameter, + owner: KotlinNativeElement +) : KotlinNativeElement(declaration, owner) { + + override fun toString(): String { + return "KotlinTypeParameterNativeElement(declaration=$declaration)" + } +} + +class KotlinTypeArgumentNativeElement( + val declaration: KSTypeArgument, + owner: KotlinNativeElement +) : KotlinNativeElement(declaration, owner) { + + override fun toString(): String { + return "KotlinTypeArgumentNativeElement(declaration=$declaration)" + } +} + +class KotlinPropertyNativeElement( + val declaration: KSPropertyDeclaration +) : KotlinNativeElement(declaration) { + + override fun toString(): String { + return "KotlinPropertyNativeElement(declaration=$declaration)" + } +} + +class KotlinSimplePropertyNativeElement( + val declaration: KSAnnotated +) : KotlinNativeElement(declaration) { + + constructor( + type: ClassElement, + field: FieldElement?, + getter: MethodElement?, + setter: MethodElement? + ) : this(pickDeclaration(type, field, getter, setter)) + + + companion object Helper { + private fun pickDeclaration( + type: ClassElement, + field: FieldElement?, + getter: MethodElement?, + setter: MethodElement? + ): KSAnnotated { + return when { + field is AbstractKotlinElement<*> -> { + field.getNativeType().element + } + + getter is AbstractKotlinElement<*> -> { + getter.getNativeType().element + } + + setter is AbstractKotlinElement<*> -> { + setter.getNativeType().element + } + + else -> { + (type as AbstractKotlinElement<*>).nativeType.element + } + } + } + } + + override fun toString(): String { + return "KotlinSimplePropertyNativeElement(declaration=$declaration)" + } +} + + diff --git a/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/KotlinParameterElement.kt b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/KotlinParameterElement.kt index f53ab4e76b8..d34cfed95a1 100644 --- a/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/KotlinParameterElement.kt +++ b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/KotlinParameterElement.kt @@ -17,66 +17,69 @@ package io.micronaut.kotlin.processing.visitor import com.google.devtools.ksp.symbol.KSValueParameter import io.micronaut.core.annotation.AnnotationMetadata +import io.micronaut.inject.ast.ArrayableClassElement import io.micronaut.inject.ast.ClassElement -import io.micronaut.inject.ast.MethodElement import io.micronaut.inject.ast.ParameterElement import io.micronaut.inject.ast.annotation.ElementAnnotationMetadataFactory -class KotlinParameterElement( - private val parameterType: ClassElement, - private val methodElement: KotlinMethodElement, +internal class KotlinParameterElement( + private val presetType: ClassElement?, + private val methodElement: AbstractKotlinMethodElement<*>, private val parameter: KSValueParameter, elementAnnotationMetadataFactory: ElementAnnotationMetadataFactory, visitorContext: KotlinVisitorContext -) : AbstractKotlinElement(KSValueParameterReference(parameter), elementAnnotationMetadataFactory, visitorContext), ParameterElement { - private val internalName : String by lazy { +) : AbstractKotlinElement( + KotlinMethodParameterNativeElement(parameter, methodElement.nativeType), + elementAnnotationMetadataFactory, + visitorContext +), ParameterElement { + + private val internalName: String by lazy { parameter.name!!.asString() } - private val internalGenericType : ClassElement by lazy { - resolveGeneric( - methodElement.declaration.parent, - parameterType, - methodElement.owningType, - visitorContext - ) + private val internalType: ClassElement by lazy { + presetType ?: newClassElement(nativeType, parameter.type.resolve(), emptyMap()) } - - override fun isPrimitive(): Boolean { - return parameterType.isPrimitive + private val internalGenericType: ClassElement by lazy { + if (presetType != null) { + if (presetType is KotlinClassElement) { + val newCE = newClassElement( + nativeType, + presetType.kotlinType, + methodElement.typeArguments + ) as ArrayableClassElement + newCE.withArrayDimensions(presetType.arrayDimensions) + } else { + presetType + } + } else { + newClassElement(nativeType, parameter.type.resolve(), methodElement.typeArguments) + } } - override fun isArray(): Boolean { - return parameterType.isArray - } + override fun isPrimitive() = internalType.isPrimitive - override fun copyThis(): AbstractKotlinElement { - return KotlinParameterElement( - parameterType, - methodElement, - parameter, - annotationMetadataFactory, - visitorContext - ) - } + override fun isArray() = internalType.isArray - override fun getMethodElement(): MethodElement { - return methodElement - } + override fun copyThis() = KotlinParameterElement( + presetType, + methodElement, + parameter, + elementAnnotationMetadataFactory, + visitorContext + ) - override fun getName(): String { - return internalName - } + override fun getMethodElement() = methodElement - override fun withAnnotationMetadata(annotationMetadata: AnnotationMetadata): ParameterElement { - return super.withAnnotationMetadata(annotationMetadata) as ParameterElement - } + override fun getName() = internalName - override fun getType(): ClassElement = parameterType + override fun withAnnotationMetadata(annotationMetadata: AnnotationMetadata) = + super.withAnnotationMetadata(annotationMetadata) as ParameterElement - override fun getGenericType(): ClassElement { - return internalGenericType - } + override fun getType() = internalType + + override fun getGenericType() = internalGenericType - override fun getArrayDimensions(): Int = parameterType.arrayDimensions + override fun getArrayDimensions() = internalType.arrayDimensions } diff --git a/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/KotlinPropertyElement.kt b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/KotlinPropertyElement.kt index 15874b05664..0a3ebc2f48f 100644 --- a/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/KotlinPropertyElement.kt +++ b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/KotlinPropertyElement.kt @@ -16,61 +16,44 @@ package io.micronaut.kotlin.processing.visitor import com.google.devtools.ksp.isAbstract -import com.google.devtools.ksp.symbol.* -import io.micronaut.core.annotation.AnnotationMetadata -import io.micronaut.core.annotation.AnnotationMetadataDelegate -import io.micronaut.core.annotation.AnnotationValue -import io.micronaut.core.annotation.AnnotationValueBuilder -import io.micronaut.inject.annotation.AnnotationMetadataHierarchy -import io.micronaut.inject.ast.* +import com.google.devtools.ksp.symbol.KSFunctionDeclaration +import com.google.devtools.ksp.symbol.KSPropertyDeclaration +import com.google.devtools.ksp.symbol.Modifier +import io.micronaut.inject.ast.ClassElement +import io.micronaut.inject.ast.FieldElement +import io.micronaut.inject.ast.MethodElement +import io.micronaut.inject.ast.ParameterElement +import io.micronaut.inject.ast.PropertyElement import io.micronaut.inject.ast.annotation.ElementAnnotationMetadataFactory -import io.micronaut.inject.ast.annotation.MutableAnnotationMetadataDelegate -import io.micronaut.kotlin.processing.kspNode import java.util.* -import java.util.function.Consumer -import java.util.function.Predicate -class KotlinPropertyElement: AbstractKotlinElement, PropertyElement { +internal class KotlinPropertyElement( + ownerType: ClassElement, + val property: KSPropertyDeclaration, + elementAnnotationMetadataFactory: ElementAnnotationMetadataFactory, + visitorContext: KotlinVisitorContext, +) : AbstractKotlinPropertyElement( + KotlinPropertyNativeElement(property), + ownerType, + property.simpleName.asString(), + false, + elementAnnotationMetadataFactory, visitorContext +), PropertyElement { - private val name: String - private val classElement: ClassElement - private val type: ClassElement - private val setter: Optional - private val getter: Optional - private val field: Optional - private val abstract: Boolean - private val exc: Boolean - private var annotationMetadata: MutableAnnotationMetadataDelegate<*>? = null - private val internalDeclaringType: ClassElement by lazy { - var parent = declaration.parent - if (parent is KSPropertyDeclaration) { - parent = parent.parent - } - val owner = getOwningType() - if (parent is KSClassDeclaration) { - if (owner.name.equals(parent.qualifiedName)) { - owner - } else { - visitorContext.elementFactory.newClassElement( - parent.asStarProjectedType() - ) - } - } else { - owner - } + override val declaration = property + + override val abstract = property.isAbstract() + + override val resolvedType: ClassElement by lazy { + newClassElement(nativeType, property.type.resolve(), emptyMap()) + } + + override val resolvedGenericType: ClassElement by lazy { + newClassElement(nativeType, property.type.resolve(), declaringType.typeArguments) } - constructor(classElement: ClassElement, - type: ClassElement, - property: KSPropertyDeclaration, - elementAnnotationMetadataFactory: ElementAnnotationMetadataFactory, - visitorContext: KotlinVisitorContext, - excluded : Boolean = false) : super(KSPropertyReference(property), elementAnnotationMetadataFactory, visitorContext) { - this.name = property.simpleName.asString() - this.exc = excluded - this.type = type - this.classElement = classElement - this.setter = Optional.ofNullable(property.setter) + override val setter: Optional by lazy { + Optional.ofNullable(property.setter) .map { method -> val modifiers = try { method.modifiers @@ -81,445 +64,78 @@ class KotlinPropertyElement: AbstractKotlinElement, PropertyElement { return@map if (modifiers.contains(Modifier.PRIVATE)) { null } else { - visitorContext.elementFactory.newMethodElement(classElement, this, method, type, elementAnnotationMetadataFactory) + KotlinPropertySetterMethodElement( + ownerType, + this, + method, + elementAnnotationMetadataFactory, + visitorContext + ) } } - this.getter = Optional.ofNullable(property.getter) + } + + override val getter: Optional by lazy { + Optional.ofNullable(property.getter) .map { method -> - return@map visitorContext.elementFactory.newMethodElement(classElement, this, method, type, elementAnnotationMetadataFactory) + KotlinPropertyGetterMethodElement( + owningType, + method, + this, + elementAnnotationMetadataFactory, + visitorContext + ) } - this.abstract = property.isAbstract() + } + + override val fieldElement: Optional by lazy { if (property.hasBackingField) { val newFieldElement = visitorContext.elementFactory.newFieldElement( - classElement, + ownerType, property, elementAnnotationMetadataFactory ) - this.field = Optional.of(newFieldElement) + Optional.of(newFieldElement) } else { - this.field = Optional.empty() - } - - val elements: MutableList = ArrayList(3) - setter.ifPresent { elements.add(it) } - getter.ifPresent { elements.add(it) } - field.ifPresent { elements.add(it) } - - - // The instance AnnotationMetadata of each element can change after a modification - // Set annotation metadata as actual elements so the changes are reflected - val propertyAnnotationMetadata: AnnotationMetadata - propertyAnnotationMetadata = if (elements.size == 1) { - elements.iterator().next() - } else { - AnnotationMetadataHierarchy( - true, - *elements.map { e: MemberElement -> - if (e is MethodElement) { - return@map object : AnnotationMetadataDelegate { - override fun getAnnotationMetadata(): AnnotationMetadata { - // Exclude type metadata - return e.getAnnotationMetadata().declaredMetadata - } - } - } - e - }.toTypedArray() - ) - } - this.annotationMetadata = object : MutableAnnotationMetadataDelegate { - override fun annotate(annotationValue: AnnotationValue): Element { - for (memberElement in elements) { - memberElement.annotate(annotationValue) - } - return this@KotlinPropertyElement - } - - override fun annotate( - annotationType: String, - consumer: Consumer> - ): Element { - for (memberElement in elements) { - memberElement.annotate(annotationType, consumer) - } - return this@KotlinPropertyElement - } - - override fun annotate(annotationType: Class): Element { - for (memberElement in elements) { - memberElement.annotate(annotationType) - } - return this@KotlinPropertyElement - } - - override fun annotate(annotationType: String): Element { - for (memberElement in elements) { - memberElement.annotate(annotationType) - } - return this@KotlinPropertyElement - } - - override fun annotate( - annotationType: Class, - consumer: Consumer> - ): Element { - for (memberElement in elements) { - memberElement.annotate(annotationType, consumer) - } - return this@KotlinPropertyElement - } - - override fun removeAnnotation(annotationType: String): Element { - for (memberElement in elements) { - memberElement.removeAnnotation(annotationType) - } - return this@KotlinPropertyElement - } - - override fun removeAnnotationIf(predicate: Predicate>): Element { - for (memberElement in elements) { - memberElement.removeAnnotationIf(predicate) - } - return this@KotlinPropertyElement - } - - override fun getAnnotationMetadata(): AnnotationMetadata { - return propertyAnnotationMetadata - } + Optional.empty() } } - constructor(classElement: ClassElement, - type: ClassElement, - name: String, - getter: KSFunctionDeclaration, - setter: KSFunctionDeclaration?, - elementAnnotationMetadataFactory: ElementAnnotationMetadataFactory, - visitorContext: KotlinVisitorContext, - excluded : Boolean = false) : super(getter, elementAnnotationMetadataFactory, visitorContext) { - this.name = name - this.type = type - this.exc = excluded - this.classElement = classElement - this.setter = Optional.ofNullable(setter) - .map { method -> - visitorContext.elementFactory.newMethodElement(classElement, method, elementAnnotationMetadataFactory) - } - this.getter = Optional.of(visitorContext.elementFactory.newMethodElement(classElement, getter, elementAnnotationMetadataFactory)) - this.abstract = getter.isAbstract || setter?.isAbstract == true - this.field = Optional.empty() - val elements: MutableList = ArrayList(3) - this.setter.ifPresent { elements.add(it) } - this.getter.ifPresent { elements.add(it) } - field.ifPresent { elements.add(it) } - - // The instance AnnotationMetadata of each element can change after a modification - // Set annotation metadata as actual elements so the changes are reflected - val propertyAnnotationMetadata: AnnotationMetadata - propertyAnnotationMetadata = if (elements.size == 1) { - elements.iterator().next() - } else { - AnnotationMetadataHierarchy( - true, - *elements.stream().map { e: MemberElement -> - if (e is MethodElement) { - return@map object : AnnotationMetadataDelegate { - override fun getAnnotationMetadata(): AnnotationMetadata { - // Exclude type metadata - return e.getAnnotationMetadata().declaredMetadata - } - } - } - e - }.toList().toTypedArray() - ) - } - this.annotationMetadata = object : MutableAnnotationMetadataDelegate { - override fun annotate(annotationValue: AnnotationValue): Element { - for (memberElement in elements) { - memberElement.annotate(annotationValue) - } - return this@KotlinPropertyElement - } - - override fun annotate( - annotationType: String, - consumer: Consumer> - ): Element { - for (memberElement in elements) { - memberElement.annotate(annotationType, consumer) - } - return this@KotlinPropertyElement - } - override fun annotate(annotationType: Class): Element { - for (memberElement in elements) { - memberElement.annotate(annotationType) - } - return this@KotlinPropertyElement - } - - override fun annotate(annotationType: String): Element { - for (memberElement in elements) { - memberElement.annotate(annotationType) - } - return this@KotlinPropertyElement - } - - override fun annotate( - annotationType: Class, - consumer: Consumer> - ): Element { - for (memberElement in elements) { - memberElement.annotate(annotationType, consumer) - } - return this@KotlinPropertyElement - } - - override fun removeAnnotation(annotationType: String): Element { - for (memberElement in elements) { - memberElement.removeAnnotation(annotationType) - } - return this@KotlinPropertyElement - } - - override fun removeAnnotationIf(predicate: Predicate>): Element { - for (memberElement in elements) { - memberElement.removeAnnotationIf(predicate) - } - return this@KotlinPropertyElement - } - - override fun getAnnotationMetadata(): AnnotationMetadata { - return propertyAnnotationMetadata - } - } - } - - constructor(classElement: ClassElement, - type: ClassElement, - name: String, - field: FieldElement?, - getter: MethodElement?, - setter: MethodElement?, - elementAnnotationMetadataFactory: ElementAnnotationMetadataFactory, - visitorContext: KotlinVisitorContext, - excluded : Boolean = false) : super(pickDeclaration(type, field, getter, setter), elementAnnotationMetadataFactory, visitorContext) { - this.name = name - this.type = type - this.classElement = classElement - this.setter = Optional.ofNullable(setter) - this.getter = Optional.ofNullable(getter) - this.abstract = getter?.isAbstract == true || setter?.isAbstract == true - this.field = Optional.ofNullable(field) - val elements: MutableList = ArrayList(3) - this.setter.ifPresent { elements.add(it) } - this.getter.ifPresent { elements.add(it) } - this.field.ifPresent { elements.add(it) } - this.exc = excluded - - // The instance AnnotationMetadata of each element can change after a modification - // Set annotation metadata as actual elements so the changes are reflected - val propertyAnnotationMetadata: AnnotationMetadata - propertyAnnotationMetadata = if (elements.size == 1) { - elements.iterator().next().declaredMetadata - } else { - AnnotationMetadataHierarchy( - true, - *elements.stream().map { e: MemberElement -> - if (e is MethodElement) { - return@map object : AnnotationMetadataDelegate { - override fun getAnnotationMetadata(): AnnotationMetadata { - // Exclude type metadata - return e.getAnnotationMetadata().declaredMetadata - } - } - } - e - }.toList().toTypedArray() - ) - } - this.annotationMetadata = object : MutableAnnotationMetadataDelegate { - override fun annotate(annotationValue: AnnotationValue): Element { - for (memberElement in elements) { - memberElement.annotate(annotationValue) - } - return this@KotlinPropertyElement - } - - override fun annotate( - annotationType: String, - consumer: Consumer> - ): Element { - for (memberElement in elements) { - memberElement.annotate(annotationType, consumer) - } - return this@KotlinPropertyElement - } - - override fun annotate(annotationType: Class): Element { - for (memberElement in elements) { - memberElement.annotate(annotationType) - } - return this@KotlinPropertyElement - } - - override fun annotate(annotationType: String): Element { - for (memberElement in elements) { - memberElement.annotate(annotationType) - } - return this@KotlinPropertyElement - } - - override fun annotate( - annotationType: Class, - consumer: Consumer> - ): Element { - for (memberElement in elements) { - memberElement.annotate(annotationType, consumer) - } - return this@KotlinPropertyElement - } - - override fun removeAnnotation(annotationType: String): Element { - for (memberElement in elements) { - memberElement.removeAnnotation(annotationType) - } - return this@KotlinPropertyElement - } - - override fun removeAnnotationIf(predicate: Predicate>): Element { - for (memberElement in elements) { - memberElement.removeAnnotationIf(predicate) - } - return this@KotlinPropertyElement - } - - override fun getAnnotationMetadata(): AnnotationMetadata { - return propertyAnnotationMetadata - } - } - } - - companion object Helper { - private fun pickDeclaration( - type: ClassElement, - field: FieldElement?, - getter: MethodElement?, - setter: MethodElement? - ): KSNode { - return if (field?.nativeType != null) { - field.nativeType as KSNode - } else if (getter?.nativeType != null) { - getter.nativeType as KSNode - } else if (setter?.nativeType != null) { - setter.nativeType as KSNode + override val constructorParameter: Optional by lazy { + val kotlinDeclaringType = declaringType as KotlinClassElement + val kotlinDeclaration = kotlinDeclaringType.declaration + if (kotlinDeclaration.modifiers.contains(Modifier.DATA)) { + val parameter = + kotlinDeclaration.primaryConstructor?.parameters?.find { it.name?.asString() == name } + if (parameter == null) { + Optional.empty() } else { - type.nativeType as KSNode - } - } - } - - override fun overrides(overridden: PropertyElement?): Boolean { - if (overridden == null) { - return false - } else { - val nativeType = kspNode() - val overriddenNativeType = overridden.kspNode() - if (nativeType == overriddenNativeType) { - return false - } else if (nativeType is KSPropertyDeclaration) { - return overriddenNativeType == nativeType.findOverridee() + val constructor = KotlinMethodElement( + kotlinDeclaringType, + kotlinDeclaration.primaryConstructor as KSFunctionDeclaration, + elementAnnotationMetadataFactory, + visitorContext + ) + Optional.of( + KotlinParameterElement( + null, + constructor, + parameter, + elementAnnotationMetadataFactory, + visitorContext + ) + ) } - return false - } - } - - override fun isExcluded(): Boolean { - return this.exc - } - - override fun getGenericType(): ClassElement { - return resolveGeneric(declaration.parent, getType(), classElement, visitorContext) - } - - override fun getAnnotationMetadata(): MutableAnnotationMetadataDelegate<*> { - return this.annotationMetadata!! - } - - override fun getField(): Optional { - return this.field - } - - override fun getName(): String = name - override fun getModifiers(): MutableSet { - return super.getModifiers() - } - - override fun getType(): ClassElement = type - - override fun getDeclaringType(): ClassElement { - return internalDeclaringType - } - - override fun getOwningType(): ClassElement = classElement - - override fun getReadMethod(): Optional = getter - - override fun getWriteMethod(): Optional = setter - - override fun isReadOnly(): Boolean { - return !setter.isPresent || setter.get().isPrivate - } - - override fun copyThis(): AbstractKotlinElement { - if (nativeType is KSPropertyDeclaration) { - val property : KSPropertyDeclaration = nativeType as KSPropertyDeclaration - return KotlinPropertyElement( - classElement, - type, - property, - annotationMetadataFactory, - visitorContext, - exc - ) } else { - val getter : KSFunctionDeclaration = nativeType as KSFunctionDeclaration - return KotlinPropertyElement( - classElement, - type, - name, - getter, - setter.map { it.nativeType as KSFunctionDeclaration }.orElse(null), - annotationMetadataFactory, - visitorContext, - exc - ) + Optional.empty() } } - override fun isAbstract() = abstract - override fun withAnnotationMetadata(annotationMetadata: AnnotationMetadata): MemberElement { - return super.withAnnotationMetadata(annotationMetadata) as MemberElement - } - - override fun isPrimitive(): Boolean { - return type.isPrimitive - } - - override fun isArray(): Boolean { - return type.isArray - } - - override fun getArrayDimensions(): Int { - return type.arrayDimensions - } - - override fun isDeclaredNullable(): Boolean { - return type is KotlinClassElement && type.kotlinType.isMarkedNullable - } - - override fun isNullable(): Boolean { - return type is KotlinClassElement && type.kotlinType.isMarkedNullable - } + override fun copyThis() = KotlinPropertyElement( + owningType, + property, + elementAnnotationMetadataFactory, + visitorContext + ) } diff --git a/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/KotlinPropertyGetterMethodElement.kt b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/KotlinPropertyGetterMethodElement.kt new file mode 100644 index 00000000000..c2c561e985e --- /dev/null +++ b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/KotlinPropertyGetterMethodElement.kt @@ -0,0 +1,95 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.kotlin.processing.visitor + +import com.google.devtools.ksp.getVisibility +import com.google.devtools.ksp.symbol.KSPropertyGetter +import io.micronaut.inject.ast.ClassElement +import io.micronaut.inject.ast.MethodElement +import io.micronaut.inject.ast.ParameterElement +import io.micronaut.inject.ast.annotation.ElementAnnotationMetadataFactory + +internal class KotlinPropertyGetterMethodElement( + private val owningType: ClassElement, + private val propertyElement: KotlinPropertyElement, + private val propertyGetter: KSPropertyGetter, + private val presetParameters: List, + elementAnnotationMetadataFactory: ElementAnnotationMetadataFactory, + visitorContext: KotlinVisitorContext, +) : AbstractKotlinPropertyAccessorMethodElement( + KotlinPropertyGetterNativeElement(propertyGetter), + propertyGetter, + propertyGetter.receiver.getVisibility(), + owningType, + elementAnnotationMetadataFactory, + visitorContext +), MethodElement { + + constructor( + owningType: ClassElement, + method: KSPropertyGetter, + propertyElement: KotlinPropertyElement, + elementAnnotationMetadataFactory: ElementAnnotationMetadataFactory, + visitorContext: KotlinVisitorContext + ) : this( + owningType, + propertyElement, + method, + emptyList(), + elementAnnotationMetadataFactory, + visitorContext + ) + + override val internalReturnType: ClassElement = propertyElement.type + + override val internalGenericReturnType: ClassElement = propertyElement.genericType + + override val resolvedParameters: List = presetParameters + + override fun withNewOwningType(owningType: ClassElement): MethodElement { + val newMethod = KotlinPropertyGetterMethodElement( + owningType, + propertyElement, + propertyGetter, + presetParameters, + elementAnnotationMetadataFactory, + visitorContext, + ) + copyValues(newMethod) + return newMethod + } + + override fun copyThis() = + KotlinPropertyGetterMethodElement( + owningType, + propertyElement, + propertyGetter, + presetParameters, + elementAnnotationMetadataFactory, + visitorContext, + ) + + override fun withParameters(vararg newParameters: ParameterElement) = + KotlinPropertyGetterMethodElement( + owningType, + propertyElement, + propertyGetter, + newParameters.toList(), + elementAnnotationMetadataFactory, + visitorContext, + ) + +} diff --git a/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/KotlinPropertySetterMethodElement.kt b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/KotlinPropertySetterMethodElement.kt new file mode 100644 index 00000000000..683b8eabbd0 --- /dev/null +++ b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/KotlinPropertySetterMethodElement.kt @@ -0,0 +1,113 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.kotlin.processing.visitor + +import com.google.devtools.ksp.symbol.KSPropertySetter +import io.micronaut.inject.ast.ClassElement +import io.micronaut.inject.ast.MethodElement +import io.micronaut.inject.ast.ParameterElement +import io.micronaut.inject.ast.PrimitiveElement +import io.micronaut.inject.ast.annotation.ElementAnnotationMetadataFactory +import io.micronaut.kotlin.processing.getVisibility + +internal class KotlinPropertySetterMethodElement( + private val owningType: ClassElement, + private val propertyElement: KotlinPropertyElement, + private val propertySetter: KSPropertySetter, + private val presetParameters: List?, + elementAnnotationMetadataFactory: ElementAnnotationMetadataFactory, + visitorContext: KotlinVisitorContext, +) : AbstractKotlinPropertyAccessorMethodElement( + KotlinPropertySetterNativeElement(propertySetter), + propertySetter, + propertySetter.getVisibility(), + owningType, + elementAnnotationMetadataFactory, + visitorContext +), MethodElement { + + constructor( + owningType: ClassElement, + propertyElement: KotlinPropertyElement, + method: KSPropertySetter, + elementAnnotationMetadataFactory: ElementAnnotationMetadataFactory, + visitorContext: KotlinVisitorContext, + ) : this( + owningType, + propertyElement, + method, + null, + elementAnnotationMetadataFactory, + visitorContext + ) + + + override val internalDeclaredTypeArguments: Map = emptyMap() + + override val internalReturnType: ClassElement = PrimitiveElement.VOID + + override val internalGenericReturnType: ClassElement = PrimitiveElement.VOID + + override val resolvedParameters: List by lazy { + presetParameters + ?: listOf( + KotlinParameterElement( + propertyElement.type, + this, + propertySetter.parameter, + elementAnnotationMetadataFactory, + visitorContext + ) + ) + } + + override fun isSynthetic() = true + + override fun isFinal() = true + + override fun withNewOwningType(owningType: ClassElement): MethodElement { + val newMethod = KotlinPropertySetterMethodElement( + owningType, + propertyElement, + propertySetter, + presetParameters, + elementAnnotationMetadataFactory, + visitorContext, + ) + copyValues(newMethod) + return newMethod + } + + override fun copyThis() = KotlinPropertySetterMethodElement( + owningType, + propertyElement, + propertySetter, + presetParameters, + elementAnnotationMetadataFactory, + visitorContext, + ) + + override fun withParameters(vararg newParameters: ParameterElement) = + KotlinPropertySetterMethodElement( + owningType, + propertyElement, + propertySetter, + newParameters.toList(), + elementAnnotationMetadataFactory, + visitorContext, + ) + +} diff --git a/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/KotlinSimplePropertyElement.kt b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/KotlinSimplePropertyElement.kt new file mode 100644 index 00000000000..608ac4560d1 --- /dev/null +++ b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/KotlinSimplePropertyElement.kt @@ -0,0 +1,91 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.kotlin.processing.visitor + +import com.google.devtools.ksp.symbol.KSDeclaration +import io.micronaut.inject.ast.ArrayableClassElement +import io.micronaut.inject.ast.ClassElement +import io.micronaut.inject.ast.FieldElement +import io.micronaut.inject.ast.MethodElement +import io.micronaut.inject.ast.PropertyElement +import io.micronaut.inject.ast.annotation.ElementAnnotationMetadataFactory +import java.util.* + +internal class KotlinSimplePropertyElement( + ownerType: ClassElement, + private val type: ClassElement, + name: String, + private val internalFieldElement: FieldElement?, + private val getterMethod: MethodElement?, + private val setterMethod: MethodElement?, + elementAnnotationMetadataFactory: ElementAnnotationMetadataFactory, + visitorContext: KotlinVisitorContext, + excluded: Boolean = false +) : AbstractKotlinPropertyElement( + KotlinSimplePropertyNativeElement(type, internalFieldElement, getterMethod, setterMethod), + ownerType, + name, + excluded, + elementAnnotationMetadataFactory, + visitorContext +), PropertyElement { + + override val declaration: KSDeclaration by lazy { + val ksAnnotated = nativeType.element + if (ksAnnotated is KSDeclaration) { + ksAnnotated + } else { + throw IllegalStateException("Expected declaration got: $ksAnnotated") + } + } + + override val resolvedType = type + + override val resolvedGenericType: ClassElement by lazy { + if (type is KotlinClassElement) { + val newCE = newClassElement( + nativeType, + type.kotlinType, + declaringType.typeArguments + ) as ArrayableClassElement + newCE.withArrayDimensions(type.arrayDimensions) + } else { + type + } + } + override val setter = Optional.ofNullable(setterMethod) + + override val getter = Optional.ofNullable(getterMethod) + + override val fieldElement: Optional = Optional.ofNullable(internalFieldElement) + + override val abstract: Boolean by lazy { + getterMethod?.isAbstract == true || setterMethod?.isAbstract == true + } + + override fun copyThis() = KotlinSimplePropertyElement( + ownerType, + type, + name, + internalFieldElement, + getterMethod, + setterMethod, + elementAnnotationMetadataFactory, + visitorContext, + isExcluded + ) + +} diff --git a/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/KotlinTypeArgumentElement.kt b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/KotlinTypeArgumentElement.kt new file mode 100644 index 00000000000..cf0b6194c34 --- /dev/null +++ b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/KotlinTypeArgumentElement.kt @@ -0,0 +1,95 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.kotlin.processing.visitor + +import io.micronaut.core.annotation.AnnotationMetadata +import io.micronaut.inject.annotation.AnnotationMetadataHierarchy +import io.micronaut.inject.ast.ClassElement +import io.micronaut.inject.ast.GenericElement +import io.micronaut.inject.ast.annotation.AbstractElementAnnotationMetadata +import io.micronaut.inject.ast.annotation.ElementAnnotationMetadata +import io.micronaut.inject.ast.annotation.MutableAnnotationMetadataDelegate + +internal class KotlinTypeArgumentElement( + private var internalGenericNativeType: KotlinTypeArgumentNativeElement, + private var resolved: KotlinClassElement, + visitorContext: KotlinVisitorContext, + internalArrayDimensions: Int = resolved.arrayDimensions +) : KotlinClassElement( + resolved.nativeType, + resolved.elementAnnotationMetadataFactory, + resolved.resolvedTypeArguments, + visitorContext, + internalArrayDimensions, + true +), GenericElement { + + private val resolvedTypeAnnotationMetadata: ElementAnnotationMetadata by lazy { + class KotlinTypeArgumentElementAnnotationMetadata( + private val typeArgumentElement: KotlinTypeArgumentElement, + private val representingClassElement: ClassElement + ) : AbstractElementAnnotationMetadata() { + private var annotationMetadata: AnnotationMetadata? = null + public override fun getReturnInstance(): AnnotationMetadata { + return getAnnotationMetadata() + } + + override fun getAnnotationMetadataToWrite(): MutableAnnotationMetadataDelegate<*> { + return typeArgumentElement.genericTypeAnnotationMetadata + } + + override fun getAnnotationMetadata(): AnnotationMetadata { + if (annotationMetadata == null) { + val allAnnotationMetadata: MutableList = ArrayList() + allAnnotationMetadata.add(representingClassElement.typeAnnotationMetadata) + allAnnotationMetadata.add(typeArgumentElement.genericTypeAnnotationMetadata) + annotationMetadata = + AnnotationMetadataHierarchy(true, *allAnnotationMetadata.toTypedArray()) + } + return annotationMetadata!! + } + } + KotlinTypeArgumentElementAnnotationMetadata(this, resolved) + } + private val resolvedAnnotationMetadata: AnnotationMetadata by lazy { + AnnotationMetadataHierarchy( + true, + elementAnnotationMetadata, + resolvedGenericTypeAnnotationMetadata + ) + } + private val resolvedGenericTypeAnnotationMetadata: ElementAnnotationMetadata by lazy { + elementAnnotationMetadataFactory.buildGenericTypeAnnotations(this) + } + + override fun getGenericNativeType() = internalGenericNativeType + + override fun getAnnotationMetadataToWrite() = resolvedGenericTypeAnnotationMetadata + + override fun getGenericTypeAnnotationMetadata() = resolvedGenericTypeAnnotationMetadata + + override fun getTypeAnnotationMetadata() = resolvedTypeAnnotationMetadata + + override fun getAnnotationMetadata() = resolvedAnnotationMetadata + + override fun withArrayDimensions(arrayDimensions: Int) = KotlinTypeArgumentElement( + genericNativeType, + resolved, + visitorContext, + arrayDimensions + ) + +} diff --git a/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/KotlinVisitorContext.kt b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/KotlinVisitorContext.kt index 639236fe2d8..b42cb7217f4 100644 --- a/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/KotlinVisitorContext.kt +++ b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/KotlinVisitorContext.kt @@ -17,7 +17,6 @@ package io.micronaut.kotlin.processing.visitor import com.google.devtools.ksp.KspExperimental import com.google.devtools.ksp.getClassDeclarationByName -import com.google.devtools.ksp.getJavaClassByName import com.google.devtools.ksp.processing.Resolver import com.google.devtools.ksp.processing.SymbolProcessorEnvironment import com.google.devtools.ksp.symbol.KSClassDeclaration @@ -32,8 +31,8 @@ import io.micronaut.inject.ast.Element import io.micronaut.inject.ast.annotation.ElementAnnotationMetadataFactory import io.micronaut.inject.visitor.VisitorContext import io.micronaut.inject.writer.GeneratedFile -import io.micronaut.kotlin.processing.annotation.KotlinAnnotationMetadataBuilder import io.micronaut.kotlin.processing.KotlinOutputVisitor +import io.micronaut.kotlin.processing.annotation.KotlinAnnotationMetadataBuilder import io.micronaut.kotlin.processing.annotation.KotlinElementAnnotationMetadataFactory import java.io.* import java.net.URI @@ -42,8 +41,10 @@ import java.util.* import java.util.function.BiConsumer @OptIn(KspExperimental::class) -open class KotlinVisitorContext(private val environment: SymbolProcessorEnvironment, - val resolver: Resolver) : VisitorContext { +internal open class KotlinVisitorContext( + private val environment: SymbolProcessorEnvironment, + val resolver: Resolver +) : VisitorContext { private val visitorAttributes: MutableConvertibleValues private val elementFactory: KotlinElementFactory @@ -55,10 +56,14 @@ open class KotlinVisitorContext(private val environment: SymbolProcessorEnvironm visitorAttributes = MutableConvertibleValuesMap() annotationMetadataBuilder = KotlinAnnotationMetadataBuilder(environment, resolver, this) elementFactory = KotlinElementFactory(this) - elementAnnotationMetadataFactory = KotlinElementAnnotationMetadataFactory(false, annotationMetadataBuilder) + elementAnnotationMetadataFactory = + KotlinElementAnnotationMetadataFactory(false, annotationMetadataBuilder) } - override fun get(name: CharSequence?, conversionContext: ArgumentConversionContext?): Optional { + override fun get( + name: CharSequence?, + conversionContext: ArgumentConversionContext? + ): Optional { return visitorAttributes.get(name, conversionContext) } @@ -90,21 +95,29 @@ open class KotlinVisitorContext(private val environment: SymbolProcessorEnvironm if (declaration == null) { declaration = resolver.getClassDeclarationByName(name.replace('$', '.')) } - return Optional.ofNullable(declaration?.asStarProjectedType()) + return Optional.ofNullable(declaration) .map(elementFactory::newClassElement) } @OptIn(KspExperimental::class) - override fun getClassElements(aPackage: String, vararg stereotypes: String): Array { + override fun getClassElements( + aPackage: String, + vararg stereotypes: String + ): Array { return resolver.getDeclarationsFromPackage(aPackage) .filterIsInstance() .filter { declaration -> declaration.annotations.any { ann -> - stereotypes.contains(KotlinAnnotationMetadataBuilder.getAnnotationTypeName(ann, this)) + stereotypes.contains( + KotlinAnnotationMetadataBuilder.getAnnotationTypeName( + ann, + this + ) + ) } } .map { declaration -> - elementFactory.newClassElement(declaration.asStarProjectedType()) + elementFactory.newClassElement(declaration) } .toList() .toTypedArray() @@ -122,11 +135,18 @@ open class KotlinVisitorContext(private val environment: SymbolProcessorEnvironm outputVisitor.visitServiceDescriptor(type, classname) } - override fun visitServiceDescriptor(type: String, classname: String, originatingElement: Element) { + override fun visitServiceDescriptor( + type: String, + classname: String, + originatingElement: Element + ) { outputVisitor.visitServiceDescriptor(type, classname, originatingElement) } - override fun visitMetaInfFile(path: String, vararg originatingElements: Element): Optional { + override fun visitMetaInfFile( + path: String, + vararg originatingElements: Element + ): Optional { return outputVisitor.visitMetaInfFile(path, *originatingElements) } @@ -146,7 +166,7 @@ open class KotlinVisitorContext(private val environment: SymbolProcessorEnvironm if (declaration == null) { declaration = resolver.getClassDeclarationByName(name.replace('$', '.')) } - return Optional.ofNullable(declaration?.asStarProjectedType()) + return Optional.ofNullable(declaration) .map { elementFactory.newClassElement(it, annotationMetadataFactory) } } @@ -187,23 +207,33 @@ open class KotlinVisitorContext(private val environment: SymbolProcessorEnvironm printMessage(message, environment.logger::warn, element) } - private fun printMessage(message: String, logger: BiConsumer, element: Element?) { + private fun printMessage( + message: String, + logger: BiConsumer, + element: Element? + ) { if (element is AbstractKotlinElement<*>) { - val el = element.nativeType + val el = element.nativeType.element printMessage(message, logger, el) } else { printMessage(message, logger, null as KSNode?) } } - private fun printMessage(message: String, logger: BiConsumer, element: KSNode?) { + private fun printMessage( + message: String, + logger: BiConsumer, + element: KSNode? + ) { if (StringUtils.isNotEmpty(message)) { logger.accept(message, element) } } - class KspGeneratedFile(private val outputStream: OutputStream, - private val path: String) : GeneratedFile { + class KspGeneratedFile( + private val outputStream: OutputStream, + private val path: String + ) : GeneratedFile { private val file = File(path) diff --git a/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/KotlinWildcardElement.kt b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/KotlinWildcardElement.kt index 170201abe24..085b4a41f15 100644 --- a/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/KotlinWildcardElement.kt +++ b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/KotlinWildcardElement.kt @@ -15,28 +15,62 @@ */ package io.micronaut.kotlin.processing.visitor +import io.micronaut.core.annotation.AnnotationMetadata import io.micronaut.core.annotation.NonNull +import io.micronaut.inject.annotation.AnnotationMetadataHierarchy import io.micronaut.inject.ast.ArrayableClassElement import io.micronaut.inject.ast.ClassElement import io.micronaut.inject.ast.WildcardElement +import io.micronaut.inject.ast.annotation.ElementAnnotationMetadata import io.micronaut.inject.ast.annotation.ElementAnnotationMetadataFactory +import io.micronaut.inject.ast.annotation.WildcardElementAnnotationMetadata import java.util.function.Function -class KotlinWildcardElement( +internal class KotlinWildcardElement( + private val internalGenericNativeType: KotlinTypeArgumentNativeElement, + private var upper: KotlinClassElement, private val upperBounds: List, private val lowerBounds: List, elementAnnotationMetadataFactory: ElementAnnotationMetadataFactory, visitorContext: KotlinVisitorContext, + private val isRawType: Boolean, arrayDimensions: Int = 0 ) : KotlinClassElement( - upperBounds[0]!!.nativeType, + upper.nativeType, elementAnnotationMetadataFactory, + upper.resolvedTypeArguments, visitorContext, arrayDimensions, - false + true ), WildcardElement { - override fun foldBoundGenericTypes(@NonNull fold: Function): ClassElement? { + private val resolvedTypeAnnotationMetadata: ElementAnnotationMetadata by lazy { + WildcardElementAnnotationMetadata(this, upper) + } + private val resolvedAnnotationMetadata: AnnotationMetadata by lazy { + AnnotationMetadataHierarchy( + true, + upper.annotationMetadata, + resolvedGenericTypeAnnotationMetadata + ) + } + private val resolvedGenericTypeAnnotationMetadata: ElementAnnotationMetadata by lazy { + elementAnnotationMetadataFactory.buildGenericTypeAnnotations(this) + } + + override fun getAnnotationMetadataToWrite() = resolvedGenericTypeAnnotationMetadata + + override fun getGenericTypeAnnotationMetadata() = resolvedGenericTypeAnnotationMetadata + + override fun getTypeAnnotationMetadata() = resolvedTypeAnnotationMetadata + + override fun getAnnotationMetadata() = resolvedAnnotationMetadata + + override fun isRawType() = isRawType + + override fun getGenericNativeType() = internalGenericNativeType + + override fun foldBoundGenericTypes(@NonNull fold: Function): ClassElement? { val upperBounds: List = this.upperBounds .map { ele -> toKotlinClassElement( @@ -51,7 +85,14 @@ class KotlinWildcardElement( }.toList() return fold.apply( if (upperBounds.contains(null) || lowerBounds.contains(null)) null else KotlinWildcardElement( - upperBounds, lowerBounds, elementAnnotationMetadataFactory, visitorContext, arrayDimensions + genericNativeType, + upper, + upperBounds, + lowerBounds, + elementAnnotationMetadataFactory, + visitorContext, + isRawType, + arrayDimensions ) ) } @@ -68,13 +109,21 @@ class KotlinWildcardElement( return list } - private fun toKotlinClassElement(element: ClassElement?): KotlinClassElement { - return if (element == null || element is KotlinClassElement) { - element as KotlinClassElement - } else { - if (element.isWildcard || element.isGenericPlaceholder) { + private fun toKotlinClassElement(element: ClassElement?): KotlinClassElement? { + return when { + element == null -> { + null + } + + element is KotlinClassElement -> { + element + } + + element.isWildcard || element.isGenericPlaceholder -> { throw UnsupportedOperationException("Cannot convert wildcard / free type variable to JavaClassElement") - } else { + } + + else -> { (visitorContext.getClassElement(element.name, elementAnnotationMetadataFactory) .orElseThrow { UnsupportedOperationException( @@ -82,7 +131,7 @@ class KotlinWildcardElement( ) } as ArrayableClassElement) .withArrayDimensions(element.arrayDimensions) - .withBoundGenericTypes(element.boundGenericTypes) as KotlinClassElement + .withTypeArguments(element.typeArguments) as KotlinClassElement } } } diff --git a/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/LoadedVisitor.kt b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/LoadedVisitor.kt index 274955f379a..8efb856fc50 100644 --- a/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/LoadedVisitor.kt +++ b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/LoadedVisitor.kt @@ -24,15 +24,17 @@ import io.micronaut.core.reflect.GenericTypeUtils import io.micronaut.inject.visitor.TypeElementVisitor import java.util.* -class LoadedVisitor(val visitor: TypeElementVisitor<*, *>, - val visitorContext: KotlinVisitorContext): Ordered { +internal class LoadedVisitor( + val visitor: TypeElementVisitor<*, *>, + val visitorContext: KotlinVisitorContext +) : Ordered { companion object { const val ANY = "kotlin.Any" } - var classAnnotation: String = ANY - var elementAnnotation: String = ANY + private var classAnnotation: String = ANY + private var elementAnnotation: String = ANY init { val javaClass = visitor.javaClass @@ -47,7 +49,8 @@ class LoadedVisitor(val visitor: TypeElementVisitor<*, *>, it.declaration.qualifiedName?.asString() == tevClassName }!! classAnnotation = getType(reference.arguments[0].type!!.resolve(), visitor.classType) - elementAnnotation = getType(reference.arguments[1].type!!.resolve(), visitor.elementType) + elementAnnotation = + getType(reference.arguments[1].type!!.resolve(), visitor.elementType) } else { val classes = GenericTypeUtils.resolveInterfaceTypeArguments( javaClass, @@ -101,7 +104,8 @@ class LoadedVisitor(val visitor: TypeElementVisitor<*, *>, if (classAnnotation == "java.lang.Object") { return true } - val annotationMetadata = visitorContext.annotationMetadataBuilder.buildDeclared(classDeclaration) + val annotationMetadata = + visitorContext.annotationMetadataBuilder.buildDeclared(classDeclaration) return annotationMetadata.hasStereotype(classAnnotation) } diff --git a/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/TypeElementSymbolProcessor.kt b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/TypeElementSymbolProcessor.kt index c04364e6ee9..6b9f1c86545 100644 --- a/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/TypeElementSymbolProcessor.kt +++ b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/TypeElementSymbolProcessor.kt @@ -20,7 +20,12 @@ import com.google.devtools.ksp.isInternal import com.google.devtools.ksp.processing.Resolver import com.google.devtools.ksp.processing.SymbolProcessor import com.google.devtools.ksp.processing.SymbolProcessorEnvironment -import com.google.devtools.ksp.symbol.* +import com.google.devtools.ksp.symbol.ClassKind +import com.google.devtools.ksp.symbol.KSAnnotated +import com.google.devtools.ksp.symbol.KSClassDeclaration +import com.google.devtools.ksp.symbol.KSFile +import com.google.devtools.ksp.symbol.KSFunctionDeclaration +import com.google.devtools.ksp.symbol.KSNode import com.google.devtools.ksp.visitor.KSTopDownVisitor import io.micronaut.context.annotation.Requires import io.micronaut.context.annotation.Requires.Sdk @@ -29,28 +34,34 @@ import io.micronaut.core.annotation.NonNull import io.micronaut.core.order.OrderUtil import io.micronaut.core.util.StringUtils import io.micronaut.core.version.VersionUtils -import io.micronaut.inject.ast.* +import io.micronaut.inject.ast.ClassElement +import io.micronaut.inject.ast.ElementModifier +import io.micronaut.inject.ast.ElementQuery +import io.micronaut.inject.ast.FieldElement +import io.micronaut.inject.ast.MethodElement +import io.micronaut.inject.ast.PropertyElement import io.micronaut.inject.processing.ProcessingException import io.micronaut.inject.visitor.TypeElementVisitor import io.micronaut.inject.visitor.VisitorContext import io.micronaut.kotlin.processing.beans.BeanDefinitionProcessor -import java.util.* -open class TypeElementSymbolProcessor(private val environment: SymbolProcessorEnvironment): SymbolProcessor { +internal open class TypeElementSymbolProcessor(private val environment: SymbolProcessorEnvironment) : + SymbolProcessor { private lateinit var loadedVisitors: MutableList private lateinit var typeElementVisitors: Collection> private lateinit var visitorContext: KotlinVisitorContext companion object { - private val SERVICE_LOADER = io.micronaut.core.io.service.SoftServiceLoader.load(TypeElementVisitor::class.java) + private val SERVICE_LOADER = + io.micronaut.core.io.service.SoftServiceLoader.load(TypeElementVisitor::class.java) } open fun newClassElement( visitorContext: KotlinVisitorContext, classDeclaration: KSClassDeclaration ) = visitorContext.elementFactory.newClassElement( - classDeclaration.asStarProjectedType(), + classDeclaration, visitorContext.elementAnnotationMetadataFactory ) @@ -78,7 +89,6 @@ open class TypeElementSymbolProcessor(private val environment: SymbolProcessorEn if (loadedVisitors.isNotEmpty()) { - val elements = resolver.getAllFiles() .flatMap { file: KSFile -> file.declarations } .filterIsInstance() @@ -90,6 +100,7 @@ open class TypeElementSymbolProcessor(private val environment: SymbolProcessorEn .toList() if (elements.isNotEmpty()) { + val classElementsCache: MutableMap = HashMap() // The visitor X with a higher priority should process elements of A before // the visitor Y which is processing elements of B but also using elements A @@ -104,7 +115,13 @@ open class TypeElementSymbolProcessor(private val environment: SymbolProcessorEn if (typeElement.classKind != ClassKind.ANNOTATION_CLASS) { val className = typeElement.qualifiedName.toString() try { - typeElement.accept(ElementVisitor(loadedVisitor, typeElement), className) + typeElement.accept( + ElementVisitor( + loadedVisitor, + typeElement, + classElementsCache + ), className + ) } catch (e: ProcessingException) { BeanDefinitionProcessor.handleProcessingException(environment, e) } @@ -179,7 +196,10 @@ open class TypeElementSymbolProcessor(private val environment: SymbolProcessorEn val sdk: Sdk = requires.sdk if (sdk == Sdk.MICRONAUT) { val version: String = requires.version - if (StringUtils.isNotEmpty(version) && !VersionUtils.isAtLeastMicronautVersion(version)) { + if (StringUtils.isNotEmpty(version) && !VersionUtils.isAtLeastMicronautVersion( + version + ) + ) { try { environment.logger.warn("TypeElementVisitor [" + definition.name + "] will be ignored because Micronaut version [" + VersionUtils.MICRONAUT_VERSION + "] must be at least " + version) continue @@ -195,8 +215,11 @@ open class TypeElementSymbolProcessor(private val environment: SymbolProcessorEn return typeElementVisitors.values } - private inner class ElementVisitor(private val loadedVisitor: LoadedVisitor, - private val classDeclaration: KSClassDeclaration) : KSTopDownVisitor() { + private inner class ElementVisitor( + private val loadedVisitor: LoadedVisitor, + private val classDeclaration: KSClassDeclaration, + private val classElementsCache: MutableMap + ) : KSTopDownVisitor() { override fun visitClassDeclaration(classDeclaration: KSClassDeclaration, data: Any): Any { if (classDeclaration.qualifiedName!!.asString() == "kotlin.Any") { @@ -208,15 +231,19 @@ open class TypeElementSymbolProcessor(private val environment: SymbolProcessorEn if (classDeclaration == this.classDeclaration) { val visitorContext = loadedVisitor.visitorContext if (loadedVisitor.matches(classDeclaration)) { - val classElement = newClassElement(visitorContext, classDeclaration) + val classElement = classElementsCache.computeIfAbsent(classDeclaration) { cd -> + newClassElement( + visitorContext, + cd + ) + } try { loadedVisitor.visitor.visitClass(classElement, visitorContext) } catch (e: Exception) { - throw ProcessingException(classElement, e.message) + throw ProcessingException(classElement, e.message, e) } - classDeclaration.getAllFunctions() .filter { it.isConstructor() && !it.isInternal() } .forEach { @@ -225,13 +252,13 @@ open class TypeElementSymbolProcessor(private val environment: SymbolProcessorEn visitMembers(classElement) val innerClassQuery = - ElementQuery.ALL_INNER_CLASSES.onlyStatic().modifiers { it.contains(ElementModifier.PUBLIC) } + ElementQuery.ALL_INNER_CLASSES.onlyStatic() + .modifiers { it.contains(ElementModifier.PUBLIC) } val innerClasses = classElement.getEnclosedElements(innerClassQuery) innerClasses.forEach { val visitor = loadedVisitor.visitor - val visitorContext = loadedVisitor.visitorContext if (loadedVisitor.matches(it)) { - visitor.visitClass(it, visitorContext) + visitor.visitClass(it, loadedVisitor.visitorContext) visitMembers(it) } } @@ -249,7 +276,8 @@ open class TypeElementSymbolProcessor(private val environment: SymbolProcessorEn throw ProcessingException(property, e.message, e) } } - val memberElements = classElement.getEnclosedElements(ElementQuery.ALL_FIELD_AND_METHODS) + val memberElements = + classElement.getEnclosedElements(ElementQuery.ALL_FIELD_AND_METHODS) for (memberElement in memberElements) { when (memberElement) { is FieldElement -> { @@ -304,14 +332,14 @@ open class TypeElementSymbolProcessor(private val environment: SymbolProcessorEn } } - fun visitNativeProperty(propertyNode : PropertyElement) { + fun visitNativeProperty(propertyNode: PropertyElement) { val visitor = loadedVisitor.visitor val visitorContext = loadedVisitor.visitorContext if (loadedVisitor.matches(propertyNode)) { - propertyNode.field.ifPresent { visitor.visitField(it, visitorContext)} + propertyNode.field.ifPresent { visitor.visitField(it, visitorContext) } // visit synthetic getter/setter methods - propertyNode.writeMethod.ifPresent { visitor.visitMethod(it, visitorContext)} - propertyNode.readMethod.ifPresent{ visitor.visitMethod(it, visitorContext)} + propertyNode.writeMethod.ifPresent { visitor.visitMethod(it, visitorContext) } + propertyNode.readMethod.ifPresent { visitor.visitMethod(it, visitorContext) } } } diff --git a/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/TypeElementSymbolProcessorProvider.kt b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/TypeElementSymbolProcessorProvider.kt index 769d745762a..068c91ed092 100644 --- a/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/TypeElementSymbolProcessorProvider.kt +++ b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/TypeElementSymbolProcessorProvider.kt @@ -19,7 +19,7 @@ import com.google.devtools.ksp.processing.SymbolProcessor import com.google.devtools.ksp.processing.SymbolProcessorEnvironment import com.google.devtools.ksp.processing.SymbolProcessorProvider -open class TypeElementSymbolProcessorProvider: SymbolProcessorProvider { +internal open class TypeElementSymbolProcessorProvider : SymbolProcessorProvider { override fun create(environment: SymbolProcessorEnvironment): SymbolProcessor { return TypeElementSymbolProcessor(environment) diff --git a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/annotations/AnnotateFieldSpec.groovy b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/annotations/AnnotateFieldSpec.groovy new file mode 100644 index 00000000000..9c21e40a066 --- /dev/null +++ b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/annotations/AnnotateFieldSpec.groovy @@ -0,0 +1,117 @@ +package io.micronaut.kotlin.processing.annotations + +import io.micronaut.annotation.processing.test.AbstractKotlinCompilerSpec +import io.micronaut.core.annotation.AnnotationUtil +import io.micronaut.inject.ast.ClassElement +import io.micronaut.inject.visitor.TypeElementVisitor +import io.micronaut.inject.visitor.VisitorContext +import spock.lang.PendingFeature + +class AnnotateFieldSpec extends AbstractKotlinCompilerSpec { + + void 'test annotating'() { + when: + def definition = buildBeanDefinition('addann.AnnotationFieldClass', ''' +package addann; + +import io.micronaut.context.annotation.Bean; +import jakarta.inject.Inject; + +@Bean +class AnnotationFieldClass { + + @Inject + var myField1: MyBean1? = null + + @Inject + var myField2: MyBean1? = null + +} + +class MyBean1 + +''') + then: + def inject1 = definition.getInjectedMethods()[0] + inject1.name == "setMyField1" +// inject1.getAnnotationMetadata().hasAnnotation(MyAnnotation) + and: + def inject2 = definition.getInjectedMethods()[1] + inject2.name == "setMyField2" + !inject2.getAnnotationMetadata().hasAnnotation(MyAnnotation) + } + + @PendingFeature + void 'test annotating is preserved'() { + when: + def definition = buildBeanDefinition('addann.AnnotationFieldClass', ''' +package addann; + +import io.micronaut.context.annotation.Bean; +import jakarta.inject.Inject; + +@Bean +class AnnotationFieldClass { + + @Inject + var myField1: MyBean1? = null + + @Inject + var myField2: MyBean1? = null + +} + +class MyBean1 + +''') + then: + def inject1 = definition.getInjectedMethods()[0] + inject1.name == "setMyField1" + inject1.getAnnotationMetadata().hasAnnotation(MyAnnotation) + and: + def inject2 = definition.getInjectedMethods()[1] + inject2.name == "setMyField2" + !inject2.getAnnotationMetadata().hasAnnotation(MyAnnotation) + } + + static class AnnotationFieldVisitor implements TypeElementVisitor { + @Override + void visitClass(ClassElement element, VisitorContext context) { + if (element.getSimpleName() == "AnnotationFieldClass") { + def myField1 = element.findField("myField1").get() + assert myField1.getAnnotationMetadata().getAnnotationNames().asList() == [AnnotationUtil.INJECT, AnnotationUtil.NULLABLE] + myField1.annotate(MyAnnotation) + assert myField1.getAnnotationMetadata().getAnnotationNames().asList() == [AnnotationUtil.INJECT, AnnotationUtil.NULLABLE, MyAnnotation.class.name] + assert myField1.getAnnotationMetadata().getAnnotationNames().asList() == [AnnotationUtil.INJECT, AnnotationUtil.NULLABLE, MyAnnotation.class.name] + assert myField1.getType().getAnnotationNames().isEmpty() + assert myField1.getType().getTypeAnnotationMetadata().getAnnotationNames().isEmpty() + assert myField1.getType().getType().getAnnotationMetadata().isEmpty() + assert myField1.getGenericType().getAnnotationNames().isEmpty() + assert myField1.getGenericType().getTypeAnnotationMetadata().getAnnotationNames().isEmpty() + assert myField1.getGenericType().getType().getAnnotationMetadata().isEmpty() + + // Validate the cache is working + assert context.getClassElement("addann.AnnotationFieldClass").get() + .findField("myField1").get() + .getAnnotationMetadata().getAnnotationNames().asList() == [AnnotationUtil.INJECT, AnnotationUtil.NULLABLE, MyAnnotation.class.name] + + // Test the second method with the same type doesn't have the annotations + + def myField2 = element.findField("myField2").get() + assert myField2.getAnnotationMetadata().getAnnotationNames().asList() == [AnnotationUtil.INJECT, AnnotationUtil.NULLABLE] + assert myField2.getType().getAnnotationNames().isEmpty() + assert myField2.getType().getTypeAnnotationMetadata().getAnnotationNames().isEmpty() + assert myField2.getGenericType().getAnnotationNames().isEmpty() + assert myField2.getGenericType().getTypeAnnotationMetadata().getAnnotationNames().isEmpty() + + // Validate the cache is working + assert context.getClassElement("addann.AnnotationFieldClass").get() + .findField("myField2").get() + .getAnnotationMetadata().getAnnotationNames().asList() == [AnnotationUtil.INJECT, AnnotationUtil.NULLABLE] + + assert context.getClassElement("addann.MyBean1").get().getAnnotationMetadata().isEmpty() + } + } + } + +} diff --git a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/annotations/AnnotateFieldTypeSpec.groovy b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/annotations/AnnotateFieldTypeSpec.groovy new file mode 100644 index 00000000000..84153ebb535 --- /dev/null +++ b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/annotations/AnnotateFieldTypeSpec.groovy @@ -0,0 +1,207 @@ +package io.micronaut.kotlin.processing.annotations + +import io.micronaut.annotation.processing.test.AbstractKotlinCompilerSpec +import io.micronaut.inject.BeanDefinition +import io.micronaut.inject.ast.ClassElement +import io.micronaut.inject.ast.GenericPlaceholderElement +import io.micronaut.inject.visitor.TypeElementVisitor +import io.micronaut.inject.visitor.VisitorContext + +class AnnotateFieldTypeSpec extends AbstractKotlinCompilerSpec { + + void 'test annotating'() { + when: + def definition = buildBeanDefinition('addann.AnnotateFieldTypeClass', ''' +package addann; + +import io.micronaut.context.annotation.Bean; +import jakarta.inject.Inject; + +@Bean +class AnnotateFieldTypeClass { + + @Inject + var myField1: MyBean1? = null + + @Inject + var myField2: MyBean1? = null + +} + +class MyBean1(var name: String) + +''') + then: + validate(definition) + } + + void 'test annotating 2'() { + when: + def definition = buildBeanDefinition('addann.AnnotateFieldTypeClass', ''' +package addann; + +import io.micronaut.context.annotation.Executable; +import io.micronaut.context.annotation.Bean; +import jakarta.inject.Inject; + +@Bean +class AnnotateFieldTypeClass { + + @Inject + var myField1: T? = null + + @Inject + var myField2: T? = null + +} + +class MyBean1(var name: String) +''') + then: + validate(definition) + } + + void 'test annotating 3'() { + when: + def definition = buildBeanDefinition('addann.AnnotateFieldTypeClass', ''' +package addann; + +import io.micronaut.context.annotation.Bean; +import jakarta.inject.Inject; + +abstract class BaseAnnotateFieldTypeClass { + + @Inject + var myField1: S? = null + + @Inject + var myField2: S? = null + +} + +@Bean +class AnnotateFieldTypeClass : BaseAnnotateFieldTypeClass() + +class MyBean1(var name: String) + +''') + then: + validate(definition) + } + + void 'test annotating 4'() { + when: + def definition = buildBeanDefinition('addann.AnnotateFieldTypeClass', ''' +package addann; + +import io.micronaut.context.annotation.Bean; +import jakarta.inject.Inject; + +abstract class BaseAnnotateFieldTypeClass { + + @Inject + var myField1: S? = null + + @Inject + var myField2: S? = null + +} + +@Bean +class AnnotateFieldTypeClass : BaseAnnotateFieldTypeClass() + +class MyBean1(var name: String) +''') + then: + validate(definition) + } + + void validate(BeanDefinition definition) { + def inject1 = definition.getInjectedMethods()[0] + assert inject1.name == "setMyField1" + // TODO: support annotations propagation + assert !inject1.getAnnotationMetadata().hasAnnotation(MyAnnotation) + + def inject2 = definition.getInjectedMethods()[1] + assert inject2.name == "setMyField2" + assert !inject2.getAnnotationMetadata().hasAnnotation(MyAnnotation) + } + + static class AnnotateFieldTypeVisitor implements TypeElementVisitor { + @Override + void visitClass(ClassElement classElement, VisitorContext context) { + if (classElement.getSimpleName() == "AnnotateFieldTypeClass") { + + def myField1 = classElement.findField("myField1").get() + def type = myField1.getType() + def genericType = myField1.getGenericType() + if (type instanceof GenericPlaceholderElement) { + assert genericType instanceof GenericPlaceholderElement + def placeholderElement = type as GenericPlaceholderElement + def genericPlaceholderElement = genericType as GenericPlaceholderElement + assert placeholderElement.getGenericNativeType() == genericPlaceholderElement.getGenericNativeType() + assert placeholderElement.variableName == genericPlaceholderElement.variableName + } + + assert type.getAnnotationMetadata().getAnnotationNames().isEmpty() + assert genericType.getAnnotationMetadata().getAnnotationNames().isEmpty() + + type.annotate(MyAnnotation) + + assert type.getAnnotationMetadata().getAnnotationNames().asList() == [MyAnnotation.class.name] + assert genericType.getAnnotationMetadata().getAnnotationNames().asList() == [MyAnnotation.class.name] + // The annotation should be added to type annotations + assert type.getTypeAnnotationMetadata().getAnnotationNames().asList() == [MyAnnotation.class.name] + assert genericType.getTypeAnnotationMetadata().getAnnotationNames().asList() == [MyAnnotation.class.name] + // The annotation is not added to the actual type + assert type.getType().isEmpty() + assert genericType.getType().isEmpty() + + // Validate the cache is working + + def newClassElement = context.getClassElement("addann.AnnotateFieldTypeClass").get() + def newField = newClassElement.findField("myField1").get() + def newType = newField.getType() + def newGenericType = newField.getGenericType() + + validateBeanType(newGenericType.getType()) + + assert newType.getAnnotationMetadata().getAnnotationNames().asList() == [MyAnnotation.class.name] + assert newType.getTypeAnnotationMetadata().getAnnotationNames().asList() == [MyAnnotation.class.name] + assert newGenericType.getAnnotationMetadata().getAnnotationNames().asList() == [MyAnnotation.class.name] + assert newGenericType.getTypeAnnotationMetadata().getAnnotationNames().asList() == [MyAnnotation.class.name] + + // Validate the annotation is not added to the return class type of myMethod2 + + def field2Type = newClassElement.findField("myField2").get().getType() + def field2GenericType = newClassElement.findField("myField2").get().getGenericType() + + validateBeanType(field2GenericType.getType()) + + assert field2Type.getAnnotationMetadata().isEmpty() + assert field2Type.getTypeAnnotationMetadata().isEmpty() + + assert field2GenericType.getAnnotationMetadata().isEmpty() + assert field2GenericType.getTypeAnnotationMetadata().isEmpty() + + assert field2Type.getTypeAnnotationMetadata().isEmpty() + assert field2Type.getAnnotationMetadata().isEmpty() + + assert field2GenericType.getTypeAnnotationMetadata().isEmpty() + assert field2GenericType.getAnnotationMetadata().isEmpty() + + def bean = context.getClassElement("addann.MyBean1").get() + validateBeanType(bean) + } + + } + + private static void validateBeanType(ClassElement bean) { + assert bean.getAnnotationMetadata().isEmpty() + assert bean.getTypeAnnotationMetadata().isEmpty() + assert bean.getMethods().size() == 0 + assert bean.getFields().size() == 1 + } + } + +} diff --git a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/annotations/AnnotateMethodParameterSpec.groovy b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/annotations/AnnotateMethodParameterSpec.groovy new file mode 100644 index 00000000000..ab711043858 --- /dev/null +++ b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/annotations/AnnotateMethodParameterSpec.groovy @@ -0,0 +1,251 @@ +package io.micronaut.kotlin.processing.annotations + +import io.micronaut.annotation.processing.test.AbstractKotlinCompilerSpec +import io.micronaut.inject.BeanDefinition +import io.micronaut.inject.ast.ClassElement +import io.micronaut.inject.ast.GenericPlaceholderElement +import io.micronaut.inject.visitor.TypeElementVisitor +import io.micronaut.inject.visitor.VisitorContext + +class AnnotateMethodParameterSpec extends AbstractKotlinCompilerSpec { + + void 'test annotating'() { + when: + def definition = buildBeanDefinition('addann.AnnotateMethodParameterClass', ''' +package addann; + +import io.micronaut.context.annotation.Executable; +import io.micronaut.context.annotation.Bean; + +@Bean +class AnnotateMethodParameterClass { + + @Executable + fun myMethod1(param: MyBean1) : MyBean1? { + return null + } + + @Executable + fun myMethod2(param: MyBean1) : MyBean1? { + return null + } + +} + +class MyBean1 +''') + then: + validate(definition) + } + + void 'test annotating 2'() { + when: + def definition = buildBeanDefinition('addann.AnnotateMethodParameterClass', ''' +package addann; + +import io.micronaut.context.annotation.Executable; +import io.micronaut.context.annotation.Bean; + +@Bean +class AnnotateMethodParameterClass { + + @Executable + fun myMethod1(param: T) : T? { + return null + } + + @Executable + fun myMethod2(param: T) : T? { + return null + } + +} + +class MyBean1 +''') + then: + validate(definition) + } + + void 'test annotating 3'() { + when: + def definition = buildBeanDefinition('addann.AnnotateMethodParameterClass', ''' +package addann; + +import io.micronaut.context.annotation.Executable; +import io.micronaut.context.annotation.Bean; + +@Bean +class AnnotateMethodParameterClass { + + @Executable + fun myMethod1(param: K) : K? { + return null + } + + @Executable + fun myMethod2(param: K) : K? { + return null + } + +} + +class MyBean1 +''') + then: + validate(definition) + } + + void 'test annotating 4'() { + when: + def definition = buildBeanDefinition('addann.AnnotateMethodParameterClass', ''' +package addann; + +import io.micronaut.context.annotation.Executable; +import io.micronaut.context.annotation.Bean; + +abstract class BaseAnnotateMethodParameterClass { + + @Executable + fun myMethod1(param: S) : S? { + return null + } + + @Executable + fun myMethod2(param: S) : S? { + return null + } + +} + +@Bean +class AnnotateMethodParameterClass : BaseAnnotateMethodParameterClass() + +class MyBean1(var name: String) +''') + then: + validate(definition) + } + + void 'test annotating 6'() { + when: + def definition = buildBeanDefinition('addann.AnnotateMethodParameterClass', ''' +package addann; + +import io.micronaut.context.annotation.Executable; +import io.micronaut.context.annotation.Bean; + +abstract class BaseAnnotateMethodParameterClass { + + @Executable + fun myMethod1(param: S) : S? { + return null + } + + @Executable + fun myMethod2(param: S) : S? { + return null + } +} + +@Bean +class AnnotateMethodParameterClass : BaseAnnotateMethodParameterClass() + +class MyBean1(var name: String) +''') + then: + validate(definition) + } + + void validate(BeanDefinition definition) { + def method1 = definition.findPossibleMethods("myMethod1").findAny().get() + def method1ParameterType = method1.getArguments()[0] + def method1ReturnType = method1.getReturnType() + + assert method1ParameterType.simpleName == "MyBean1" + assert method1ReturnType.simpleName == "MyBean1" + assert method1ParameterType.getAnnotationMetadata().hasAnnotation(MyAnnotation) + assert !method1ReturnType.getAnnotationMetadata().hasAnnotation(MyAnnotation) + assert !method1ReturnType.asArgument().getAnnotationMetadata().hasAnnotation(MyAnnotation) + assert !method1.hasAnnotation(MyAnnotation) + + def method2 = definition.findPossibleMethods("myMethod2").findAny().get() + def method2ParameterType = method2.getArguments()[0] + def method2ReturnType = method2.getReturnType() + + assert !method2ParameterType.getAnnotationMetadata().hasAnnotation(MyAnnotation) + assert !method2ReturnType.getAnnotationMetadata().hasAnnotation(MyAnnotation) + assert !method2ReturnType.asArgument().getAnnotationMetadata().hasAnnotation(MyAnnotation) + assert !method2.hasAnnotation(MyAnnotation) + assert !method2.hasAnnotation(MyAnnotation) + } + + static class AnnotateMethodParameterVisitor implements TypeElementVisitor { + @Override + void visitClass(ClassElement classElement, VisitorContext context) { + if (classElement.getSimpleName() == "AnnotateMethodParameterClass") { + + def myMethod1 = classElement.findMethod("myMethod1").get() + def type = myMethod1.getParameters()[0].getType() + def genericType = myMethod1.getParameters()[0].getGenericType() + if (type instanceof GenericPlaceholderElement) { + assert genericType instanceof GenericPlaceholderElement + def placeholderElement = type as GenericPlaceholderElement + def genericPlaceholderElement = genericType as GenericPlaceholderElement + assert placeholderElement.getGenericNativeType() == genericPlaceholderElement.getGenericNativeType() + assert placeholderElement.variableName == genericPlaceholderElement.variableName + } + + assert type.getAnnotationMetadata().getAnnotationNames().isEmpty() + assert genericType.getAnnotationMetadata().getAnnotationNames().isEmpty() + + type.annotate(MyAnnotation) + + assert type.getAnnotationMetadata().getAnnotationNames().asList() == [MyAnnotation.class.name] + assert genericType.getAnnotationMetadata().getAnnotationNames().asList() == [MyAnnotation.class.name] + // The annotation should be added to type annotations + assert type.getTypeAnnotationMetadata().getAnnotationNames().asList() == [MyAnnotation.class.name] + assert genericType.getTypeAnnotationMetadata().getAnnotationNames().asList() == [MyAnnotation.class.name] + // The annotation is not added to the actual type + assert type.getType().isEmpty() + assert genericType.getType().isEmpty() + myMethod1.getReturnType().getAnnotationMetadata().isEmpty() + myMethod1.getGenericReturnType().getAnnotationMetadata().isEmpty() + + // Validate the cache is working + + def newClassElement = context.getClassElement("addann.AnnotateMethodParameterClass").get() + def newMethod = newClassElement.findMethod("myMethod1").get() + def newType = newMethod.getParameters()[0].getType() + def newGenericType =newMethod.getParameters()[0].getGenericType() + + assert newType.getAnnotationMetadata().getAnnotationNames().asList() == [MyAnnotation.class.name] + assert newType.getTypeAnnotationMetadata().getAnnotationNames().asList() == [MyAnnotation.class.name] + assert newGenericType.getAnnotationMetadata().getAnnotationNames().asList() == [MyAnnotation.class.name] + assert newGenericType.getTypeAnnotationMetadata().getAnnotationNames().asList() == [MyAnnotation.class.name] + + assert context.getClassElement("addann.MyBean1").get().getAnnotationMetadata().isEmpty() + assert context.getClassElement("addann.MyBean1").get().getTypeAnnotationMetadata().isEmpty() + + // Validate the annotation is not added to the return class type of myMethod2 + + def method2Type = newClassElement.findMethod("myMethod2").get().getParameters()[0].getType() + def method2GenericType = newClassElement.findMethod("myMethod2").get().getParameters()[0].getGenericType() + + assert method2Type.getAnnotationMetadata().isEmpty() + assert method2Type.getTypeAnnotationMetadata().isEmpty() + + assert method2GenericType.getAnnotationMetadata().isEmpty() + assert method2GenericType.getTypeAnnotationMetadata().isEmpty() + + assert method2Type.getTypeAnnotationMetadata().isEmpty() + assert method2Type.getAnnotationMetadata().isEmpty() + + assert method2GenericType.getTypeAnnotationMetadata().isEmpty() + assert method2GenericType.getAnnotationMetadata().isEmpty() + + } + } + } + +} diff --git a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/annotations/AnnotateMethodReturnSpec.groovy b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/annotations/AnnotateMethodReturnSpec.groovy new file mode 100644 index 00000000000..d35697766d8 --- /dev/null +++ b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/annotations/AnnotateMethodReturnSpec.groovy @@ -0,0 +1,255 @@ +package io.micronaut.kotlin.processing.annotations + +import io.micronaut.annotation.processing.test.AbstractKotlinCompilerSpec +import io.micronaut.inject.BeanDefinition +import io.micronaut.inject.ast.ClassElement +import io.micronaut.inject.ast.GenericPlaceholderElement +import io.micronaut.inject.visitor.TypeElementVisitor +import io.micronaut.inject.visitor.VisitorContext + +class AnnotateMethodReturnSpec extends AbstractKotlinCompilerSpec { + + void 'test annotating'() { + when: + def definition = buildBeanDefinition('addann.AnnotateMethodReturnClass', ''' +package addann; + +import io.micronaut.context.annotation.Executable; +import io.micronaut.context.annotation.Bean; + +@Bean +class AnnotateMethodReturnClass { + + @Executable + fun myMethod1() : MyBean1? { + return null + } + + @Executable + fun myMethod2() : MyBean1? { + return null + } + +} + +class MyBean1(var name: String) +''') + then: + validate(definition) + } + + void 'test annotating 2'() { + when: + def definition = buildBeanDefinition('addann.AnnotateMethodReturnClass', ''' +package addann; + +import io.micronaut.context.annotation.Executable; +import io.micronaut.context.annotation.Bean; + +@Bean +class AnnotateMethodReturnClass { + + @Executable + fun myMethod1() : T? { + return null + } + + @Executable + fun myMethod2() : T? { + return null + } + +} + +class MyBean1(var name: String) + +''') + then: + validate(definition) + } + + void 'test annotating 3'() { + when: + def definition = buildBeanDefinition('addann.AnnotateMethodReturnClass', ''' +package addann; + +import io.micronaut.context.annotation.Executable; +import io.micronaut.context.annotation.Bean; + +@Bean +class AnnotateMethodReturnClass { + + @Executable + fun myMethod1() : K? { + return null + } + + @Executable + fun myMethod2() : K? { + return null + } + +} + +class MyBean1(public var name: String) +''') + then: + validate(definition) + } + + void 'test annotating 4'() { + when: + def definition = buildBeanDefinition('addann.AnnotateMethodReturnClass', ''' +package addann; + +import io.micronaut.context.annotation.Executable; +import io.micronaut.context.annotation.Bean; + +abstract class BaseAnnotateMethodReturnClass { + + @Executable + fun myMethod1() : S? { + return null + } + + @Executable + fun myMethod2() : S? { + return null + } + +} + +@Bean +class AnnotateMethodReturnClass : BaseAnnotateMethodReturnClass() + +class MyBean1(var name: String) +''') + then: + validate(definition) + } + + void 'test annotating 6'() { + when: + def definition = buildBeanDefinition('addann.AnnotateMethodReturnClass', ''' +package addann; + +import io.micronaut.context.annotation.Executable; +import io.micronaut.context.annotation.Bean; + +abstract class BaseAnnotateMethodReturnClass { + + @Executable + fun myMethod1() : S? { + return null + } + + @Executable + fun myMethod2() : S? { + return null + } +} + +@Bean +class AnnotateMethodReturnClass : BaseAnnotateMethodReturnClass() + +class MyBean1(var name: String) +''') + then: + validate(definition) + } + + void validate(BeanDefinition definition) { + def method1 = definition.getRequiredMethod("myMethod1") + def method1ReturnType = method1.getReturnType() + + assert method1ReturnType.simpleName == "MyBean1" + assert method1ReturnType.getAnnotationMetadata().hasAnnotation(MyAnnotation) + assert method1ReturnType.asArgument().getAnnotationMetadata().hasAnnotation(MyAnnotation) + assert !method1.hasAnnotation(MyAnnotation) + + def method2 = definition.getRequiredMethod("myMethod2") + def method2ReturnType = method2.getReturnType() + + assert !method2ReturnType.getAnnotationMetadata().hasAnnotation(MyAnnotation) + assert !method2ReturnType.asArgument().getAnnotationMetadata().hasAnnotation(MyAnnotation) + assert !method2.hasAnnotation(MyAnnotation) + assert !method2.hasAnnotation(MyAnnotation) + } + + static class AnnotateMethodReturnVisitor implements TypeElementVisitor { + @Override + void visitClass(ClassElement classElement, VisitorContext context) { + if (classElement.getSimpleName() == "AnnotateMethodReturnClass") { + + def myMethod1 = classElement.findMethod("myMethod1").get() + def returnType = myMethod1.getReturnType() + def genericReturnType = myMethod1.getGenericReturnType() + if (returnType instanceof GenericPlaceholderElement) { + assert genericReturnType instanceof GenericPlaceholderElement + def placeholderElement = returnType as GenericPlaceholderElement + def genericPlaceholderElement = genericReturnType as GenericPlaceholderElement + assert placeholderElement.getGenericNativeType() == genericPlaceholderElement.getGenericNativeType() + assert placeholderElement.variableName == genericPlaceholderElement.variableName + } + + assert returnType.getAnnotationMetadata().getAnnotationNames().isEmpty() + assert genericReturnType.getAnnotationMetadata().getAnnotationNames().isEmpty() + + returnType.annotate(MyAnnotation) + + assert returnType.getAnnotationMetadata().getAnnotationNames().asList() == [MyAnnotation.class.name] + assert genericReturnType.getAnnotationMetadata().getAnnotationNames().asList() == [MyAnnotation.class.name] + // The annotation should be added to type annotations + assert returnType.getTypeAnnotationMetadata().getAnnotationNames().asList() == [MyAnnotation.class.name] + assert genericReturnType.getTypeAnnotationMetadata().getAnnotationNames().asList() == [MyAnnotation.class.name] + // The annotation is not added to the actual type + assert returnType.getType().isEmpty() + assert genericReturnType.getType().isEmpty() + + // Validate the cache is working + + def newClassElement = context.getClassElement("addann.AnnotateMethodReturnClass").get() + def newMethod = newClassElement.findMethod("myMethod1").get() + def newReturnType = newMethod.getReturnType() + def newGenericReturnType = newMethod.getGenericReturnType() + + validateBeanType(newGenericReturnType.getType()) + + assert newReturnType.getAnnotationMetadata().getAnnotationNames().asList() == [MyAnnotation.class.name] + assert newReturnType.getTypeAnnotationMetadata().getAnnotationNames().asList() == [MyAnnotation.class.name] + assert newGenericReturnType.getAnnotationMetadata().getAnnotationNames().asList() == [MyAnnotation.class.name] + assert newGenericReturnType.getTypeAnnotationMetadata().getAnnotationNames().asList() == [MyAnnotation.class.name] + + // Validate the annotation is not added to the return class type of myMethod2 + + def method2ReturnType = newClassElement.findMethod("myMethod2").get().getReturnType() + def method2GenericReturnType = newClassElement.findMethod("myMethod2").get().getGenericReturnType() + + validateBeanType(method2GenericReturnType.getType()) + + assert method2ReturnType.getAnnotationMetadata().getAnnotationNames().asList() == [] + assert method2ReturnType.getTypeAnnotationMetadata().getAnnotationNames().asList() == [] + + assert method2GenericReturnType.getAnnotationMetadata().getAnnotationNames().asList() == [] + assert method2GenericReturnType.getTypeAnnotationMetadata().getAnnotationNames().asList() == [] + + assert method2ReturnType.getTypeAnnotationMetadata().getAnnotationNames().asList() == [] + assert method2ReturnType.getAnnotationMetadata().getAnnotationNames().asList() == [] + + assert method2GenericReturnType.getTypeAnnotationMetadata().getAnnotationNames().asList() == [] + assert method2GenericReturnType.getAnnotationMetadata().getAnnotationNames().asList() == [] + + def bean = context.getClassElement("addann.MyBean1").get() + validateBeanType(bean) + } + } + + private static void validateBeanType(ClassElement bean) { + assert bean.getAnnotationMetadata().isEmpty() + assert bean.getTypeAnnotationMetadata().isEmpty() + assert bean.getMethods().size() == 0 + assert bean.getFields().size() == 1 + } + } + +} diff --git a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/annotations/AnnotateMethodSpec.groovy b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/annotations/AnnotateMethodSpec.groovy new file mode 100644 index 00000000000..639a64c03ab --- /dev/null +++ b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/annotations/AnnotateMethodSpec.groovy @@ -0,0 +1,86 @@ +package io.micronaut.kotlin.processing.annotations + +import io.micronaut.annotation.processing.test.AbstractKotlinCompilerSpec +import io.micronaut.context.annotation.Bean +import io.micronaut.context.annotation.Executable +import io.micronaut.core.annotation.AnnotationUtil +import io.micronaut.inject.ast.ClassElement +import io.micronaut.inject.visitor.TypeElementVisitor +import io.micronaut.inject.visitor.VisitorContext + +class AnnotateMethodSpec extends AbstractKotlinCompilerSpec { + + void 'test annotating'() { + when: + def definition = buildBeanDefinition('addann.AnnotationMethodClass', ''' +package addann; + +import io.micronaut.context.annotation.Executable; +import io.micronaut.context.annotation.Bean; + +@Bean +class AnnotationMethodClass { + + @Executable + fun myMethod1() : MyBean1? { + return null + } + + @Executable + fun myMethod2() : MyBean1? { + return null + } + +} + +class MyBean1 { +} + +''') + then: "myMethod1 has added annotation on the method and it's seen on the return type" + definition.getRequiredMethod("myMethod1").hasAnnotation(MyAnnotation) + definition.getRequiredMethod("myMethod1").getReturnType().getAnnotationMetadata().hasAnnotation(MyAnnotation) + definition.getRequiredMethod("myMethod1").getReturnType().asArgument().getAnnotationMetadata().hasAnnotation(MyAnnotation) + + and: "myMethod2 doesn't have the same annotation on the same type" + !definition.getRequiredMethod("myMethod2").hasAnnotation(MyAnnotation) + !definition.getRequiredMethod("myMethod2").getReturnType().getAnnotationMetadata().hasAnnotation(MyAnnotation) + !definition.getRequiredMethod("myMethod2").getReturnType().asArgument().getAnnotationMetadata().hasAnnotation(MyAnnotation) + } + + static class AnnotationMethodVisitor implements TypeElementVisitor { + @Override + void visitClass(ClassElement element, VisitorContext context) { + if (element.getSimpleName() == "AnnotationMethodClass") { + def myMethod1 = element.findMethod("myMethod1").get() + assert myMethod1.getAnnotationMetadata().getAnnotationNames().asList() == [Executable.class.name, Bean.class.name, AnnotationUtil.NULLABLE] + myMethod1.annotate(MyAnnotation) + assert myMethod1.getAnnotationMetadata().getAnnotationNames().asList() == [Executable.class.name, Bean.class.name, AnnotationUtil.NULLABLE, MyAnnotation.class.name] + assert myMethod1.getAnnotationMetadata().getAnnotationNames().asList() == [Executable.class.name, Bean.class.name, AnnotationUtil.NULLABLE, MyAnnotation.class.name] + assert myMethod1.getReturnType().getAnnotationNames().isEmpty() + assert myMethod1.getReturnType().getTypeAnnotationMetadata().getAnnotationNames().isEmpty() + assert myMethod1.getReturnType().getType().getAnnotationMetadata().isEmpty() + + // Validate the cache is working + assert context.getClassElement("addann.AnnotationMethodClass").get() + .findMethod("myMethod1").get() + .getAnnotationMetadata().getAnnotationNames().asList() == [Executable.class.name, Bean.class.name, AnnotationUtil.NULLABLE, MyAnnotation.class.name] + + // Test the second method with the same type doesn't have the annotations + + def myMethod2 = element.findMethod("myMethod2").get() + assert myMethod2.getAnnotationMetadata().getAnnotationNames().asList() == [Executable.class.name, Bean.class.name, AnnotationUtil.NULLABLE] + assert myMethod2.getReturnType().getAnnotationNames().isEmpty() + assert myMethod2.getReturnType().getTypeAnnotationMetadata().getAnnotationNames().isEmpty() + + // Validate the cache is working + assert context.getClassElement("addann.AnnotationMethodClass").get() + .findMethod("myMethod2").get() + .getAnnotationMetadata().getAnnotationNames().asList() == [Executable.class.name, Bean.class.name, AnnotationUtil.NULLABLE] + + assert context.getClassElement("addann.MyBean1").get().getAnnotationMetadata().isEmpty() + } + } + } + +} diff --git a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/annotations/MyAnnotation.java b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/annotations/MyAnnotation.java new file mode 100644 index 00000000000..aa1755bd700 --- /dev/null +++ b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/annotations/MyAnnotation.java @@ -0,0 +1,28 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.kotlin.processing.annotations; + +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; + +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.PACKAGE, ElementType.TYPE, ElementType.ANNOTATION_TYPE, ElementType.METHOD, ElementType.FIELD}) +public @interface MyAnnotation { +} diff --git a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/beans/BeanDefinitionSpec.groovy b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/beans/BeanDefinitionSpec.groovy index c110b58ca2c..90076e8e4fc 100644 --- a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/beans/BeanDefinitionSpec.groovy +++ b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/beans/BeanDefinitionSpec.groovy @@ -6,9 +6,11 @@ import io.micronaut.core.annotation.AnnotationUtil import io.micronaut.core.annotation.Introspected import io.micronaut.core.annotation.Order import io.micronaut.core.bind.annotation.Bindable +import io.micronaut.core.type.GenericPlaceholder import io.micronaut.http.annotation.Header import io.micronaut.http.annotation.HttpMethodMapping import io.micronaut.http.client.annotation.Client +import io.micronaut.inject.BeanDefinition import io.micronaut.inject.qualifiers.Qualifiers import io.micronaut.inject.writer.BeanDefinitionVisitor import spock.lang.PendingFeature @@ -878,4 +880,84 @@ class Other noExceptionThrown() definition.constructor.arguments[0].isDeclaredNullable() } + + void "test isTypeVariable"() { + given: + BeanDefinition definition = buildBeanDefinition('test', 'Test', ''' +package test; +import javax.validation.constraints.*; +import java.util.List; + +@jakarta.inject.Singleton +class Test : Serde { +} + +interface Serde : Serializer, Deserializer { +} + +interface Serializer { +} + +interface Deserializer { +} + + + ''') + + when: "Micronaut Serialization use-case" + def serdeTypeParam = definition.getTypeArguments("test.Serde")[0] + def serializerTypeParam = definition.getTypeArguments("test.Serializer")[0] + def deserializerTypeParam = definition.getTypeArguments("test.Deserializer")[0] + + then: "The first is a placeholder" + serdeTypeParam.isTypeVariable() // + (serdeTypeParam instanceof GenericPlaceholder) + and: "threat resolved placeholder as not a type variable" + serializerTypeParam.isTypeVariable() + (serializerTypeParam instanceof GenericPlaceholder) + deserializerTypeParam.isTypeVariable() + (deserializerTypeParam instanceof GenericPlaceholder) + } + + void "test isTypeVariable array"() { + given: + BeanDefinition definition = buildBeanDefinition('test', 'Test', ''' +package test; + +import javax.validation.constraints.*; +import java.util.List + +@jakarta.inject.Singleton +class Test : Serde> { +} + +interface Serde : Serializer, Deserializer { +} + +interface Serializer { +} + +interface Deserializer { +} + + + ''') + + when: "Micronaut Serialization use-case" + def serdeTypeParam = definition.getTypeArguments("test.Serde")[0] + def serializerTypeParam = definition.getTypeArguments("test.Serializer")[0] + def deserializerTypeParam = definition.getTypeArguments("test.Deserializer")[0] + // Arrays are not resolved as KotlinClassElements or placeholders + then: "The first is a placeholder" + serdeTypeParam.simpleName == "String[]" + serdeTypeParam.isTypeVariable() + (serdeTypeParam instanceof GenericPlaceholder) + and: "threat resolved placeholder as not a type variable" + serializerTypeParam.simpleName == "String[]" + serializerTypeParam.isTypeVariable() + (serializerTypeParam instanceof GenericPlaceholder) + deserializerTypeParam.simpleName == "String[]" + deserializerTypeParam.isTypeVariable() + (deserializerTypeParam instanceof GenericPlaceholder) + } } diff --git a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/beans/executable/ExecutableBeanSpec.groovy b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/beans/executable/ExecutableBeanSpec.groovy index 2ea6a1db23e..17c1eb42012 100644 --- a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/beans/executable/ExecutableBeanSpec.groovy +++ b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/beans/executable/ExecutableBeanSpec.groovy @@ -15,8 +15,15 @@ */ package io.micronaut.kotlin.processing.beans.executable +import io.micronaut.context.annotation.BeanProperties +import io.micronaut.context.annotation.Executable +import io.micronaut.core.annotation.AnnotationUtil +import io.micronaut.core.annotation.Introspected +import io.micronaut.core.type.Argument import io.micronaut.inject.BeanDefinition +import io.micronaut.inject.validation.RequiresValidation import spock.lang.Issue +import spock.lang.PendingFeature import spock.lang.Specification import static io.micronaut.annotation.processing.test.KotlinCompiler.* @@ -45,6 +52,116 @@ class ExecutableBean1 { definition.findMethod("round", float.class).get().returnType.type == int.class } + void "test executable method return nullable types"() { + given: + BeanDefinition definition = buildBeanDefinition('test.ExecutableBean1','''\ +package test + +import io.micronaut.context.annotation.Executable +import kotlin.math.roundToInt + +@jakarta.inject.Singleton +@Executable +class ExecutableBean1 { + + fun round(num: Float): Int? { + return null + } +} +''') + expect: + definition != null + definition.findMethod("round", float.class).get().returnType.type == Integer.class + } + + void "test executable method nullable parameter types"() { + given: + BeanDefinition definition = buildBeanDefinition('test.ExecutableBean1','''\ +package test + +import io.micronaut.context.annotation.Executable +import kotlin.math.roundToInt + +@jakarta.inject.Singleton +@Executable +class ExecutableBean1 { + + fun round(num: Float?): Array { + return emptyArray() + } +} +''') + expect: + definition != null + definition.findMethod("round", Float.class).get().returnType.type == Integer[].class + } + + void "test executable method nullable parameter types 2"() { + given: + BeanDefinition definition = buildBeanDefinition('test.ExecutableBean1','''\ +package test + +import io.micronaut.context.annotation.Executable +import kotlin.math.roundToInt + +@jakarta.inject.Singleton +@Executable +class ExecutableBean1 { + + fun round(num: Float?): IntArray? { + return intArrayOf(1, 2, 3) + } +} +''') + expect: + definition != null + definition.findMethod("round", Float.class).get().returnType.type == int[].class + } + + void "test executable method nullable parameter types 3"() { + given: + BeanDefinition definition = buildBeanDefinition('test.ExecutableBean1','''\ +package test + +import io.micronaut.context.annotation.Executable +import kotlin.math.roundToInt + +@jakarta.inject.Singleton +@Executable +class ExecutableBean1 { + + fun round(num: Float?): IntArray { + return intArrayOf(1, 2, 3) + } +} +''') + expect: + definition != null + definition.findMethod("round", Float.class).get().returnType.type == int[].class + } + + void "test executable method nullable return array type"() { + given: + BeanDefinition definition = buildBeanDefinition('test.ExecutableBean1','''\ +package test + +import io.micronaut.context.annotation.Executable +import kotlin.math.roundToInt + +@jakarta.inject.Singleton +@Executable +class ExecutableBean1 { + + fun round(num: Float): Array? { + return null + } +} +''') + expect: + definition != null + definition.findMethod("round", float.class).get().returnType.type == Integer[].class + } + @Issue('#2789') void "test don't generate executable methods for inherited protected or package private methods"() { given: @@ -124,5 +241,431 @@ class MyBean { definition != null definition.findMethod("run").isPresent() } + + void "test how annotations are preserved"() { + given: + BeanDefinition definition = buildBeanDefinition('test.MyBean','''\ +package test + +import io.micronaut.inject.annotation.* +import io.micronaut.context.annotation.* +import javax.validation.Valid +import java.util.List +import io.micronaut.kotlin.processing.beans.executable.* + +@jakarta.inject.Singleton +class MyBean { + @Executable + fun saveAll(books: @Valid MutableList) { + } + + @Executable + fun saveAll2(book: @Valid MutableList) { + } + + @Executable + fun saveAll3(book: @Valid MutableList) { + } + + @Executable + fun save2(book: @Valid MyBook) { + } + + @Executable + fun save3(book: @Valid T) { + } + + @Executable + fun get(): MyBook? { + return null + } +} + +''') + when: + def saveAll = definition.findMethod("saveAll", List.class).get() + def listTypeArgument = saveAll.getArguments()[0].getTypeParameters()[0] + then: + !saveAll.hasAnnotation(RequiresValidation) + !saveAll.hasStereotype(RequiresValidation) + !listTypeArgument.getAnnotationMetadata().hasAnnotation(MyEntity.class) + !listTypeArgument.getAnnotationMetadata().hasAnnotation(Introspected.class) + !listTypeArgument.getAnnotationMetadata().hasStereotype(Introspected.class) + !listTypeArgument.getAnnotationMetadata().hasAnnotation(BeanProperties.class) + !listTypeArgument.getAnnotationMetadata().hasStereotype(BeanProperties.class) + + when: + def saveAll2 = definition.findMethod("saveAll2", List.class).get() + def listTypeArgument2 = saveAll2.getArguments()[0].getTypeParameters()[0] + then: + !saveAll2.hasAnnotation(RequiresValidation) + !saveAll2.hasStereotype(RequiresValidation) + !listTypeArgument2.getAnnotationMetadata().hasAnnotation(MyEntity.class) + !listTypeArgument2.getAnnotationMetadata().hasAnnotation(Introspected.class) + !listTypeArgument2.getAnnotationMetadata().hasStereotype(Introspected.class) + !listTypeArgument2.getAnnotationMetadata().hasAnnotation(BeanProperties.class) + !listTypeArgument2.getAnnotationMetadata().hasStereotype(BeanProperties.class) + + when: + def saveAll3 = definition.findMethod("saveAll3", List.class).get() + def listTypeArgument3 = saveAll3.getArguments()[0].getTypeParameters()[0] + then: + !saveAll3.hasAnnotation(RequiresValidation) + !saveAll3.hasStereotype(RequiresValidation) + !listTypeArgument3.getAnnotationMetadata().hasAnnotation(MyEntity.class) + !listTypeArgument3.getAnnotationMetadata().hasAnnotation(Introspected.class) + !listTypeArgument3.getAnnotationMetadata().hasStereotype(Introspected.class) + !listTypeArgument3.getAnnotationMetadata().hasAnnotation(BeanProperties.class) + !listTypeArgument3.getAnnotationMetadata().hasStereotype(BeanProperties.class) + + when: + def save2 = definition.findMethod("save2", MyBook.class).get() + def parameter2 = save2.getArguments()[0] + then: + !save2.hasAnnotation(RequiresValidation) + !save2.hasStereotype(RequiresValidation) + !parameter2.getAnnotationMetadata().hasAnnotation(MyEntity.class) + !parameter2.getAnnotationMetadata().hasAnnotation(Introspected.class) + !parameter2.getAnnotationMetadata().hasStereotype(Introspected.class) + !parameter2.getAnnotationMetadata().hasAnnotation(BeanProperties.class) + !parameter2.getAnnotationMetadata().hasStereotype(BeanProperties.class) + + when: + def save3 = definition.findMethod("save3", MyBook.class).get() + def parameter3 = save3.getArguments()[0] + then: + !save3.hasAnnotation(RequiresValidation) + !save3.hasStereotype(RequiresValidation) + !parameter3.getAnnotationMetadata().hasAnnotation(MyEntity.class) + !parameter3.getAnnotationMetadata().hasAnnotation(Introspected.class) + !parameter3.getAnnotationMetadata().hasStereotype(Introspected.class) + !parameter3.getAnnotationMetadata().hasAnnotation(BeanProperties.class) + !parameter3.getAnnotationMetadata().hasStereotype(BeanProperties.class) + + when: + def get = definition.findMethod("get").get() + def returnType = get.getReturnType() + then: + !returnType.getAnnotationMetadata().hasAnnotation(MyEntity.class) + !returnType.getAnnotationMetadata().hasAnnotation(Introspected.class) + !returnType.getAnnotationMetadata().hasStereotype(Introspected.class) + !returnType.getAnnotationMetadata().hasAnnotation(BeanProperties.class) + !returnType.getAnnotationMetadata().hasStereotype(BeanProperties.class) + } + + void "test how type annotations are preserved 2"() { + given: + BeanDefinition definition = buildBeanDefinition('test.MyBean','''\ +package test + +import io.micronaut.inject.annotation.* +import io.micronaut.context.annotation.* +import io.micronaut.context.annotation.Executable +import javax.validation.Valid +import java.util.List +import io.micronaut.kotlin.processing.beans.executable.* + +@jakarta.inject.Singleton +internal class MyBean { + @Executable + fun saveAll(books: @Valid MutableList<@TypeUseRuntimeAnn MyBook>) { + } + + @Executable + fun <@TypeUseRuntimeAnn T : MyBook> saveAll2(book: @Valid MutableList) { + } + + @Executable + fun <@TypeUseRuntimeAnn T : MyBook> saveAll3(book: @Valid MutableList) { + } + + @Executable + fun save2(book: @Valid @TypeUseRuntimeAnn MyBook) { + } + + @Executable + fun <@TypeUseRuntimeAnn T : MyBook> save3(book: @Valid T) { + } + + @Executable + fun get(): @TypeUseRuntimeAnn MyBook? { + return null + } +} + +''') + when: + def saveAll = definition.findMethod("saveAll", List.class).get() + def listTypeArgument = saveAll.getArguments()[0].getTypeParameters()[0] + then: + listTypeArgument.getAnnotationMetadata().hasAnnotation(TypeUseRuntimeAnn.class) + + !saveAll.hasAnnotation(RequiresValidation) + !saveAll.hasStereotype(RequiresValidation) + !listTypeArgument.getAnnotationMetadata().hasAnnotation(MyEntity.class) + !listTypeArgument.getAnnotationMetadata().hasAnnotation(Introspected.class) + !listTypeArgument.getAnnotationMetadata().hasStereotype(Introspected.class) + !listTypeArgument.getAnnotationMetadata().hasAnnotation(BeanProperties.class) + !listTypeArgument.getAnnotationMetadata().hasStereotype(BeanProperties.class) + + when: + def saveAll2 = definition.findMethod("saveAll2", List.class).get() + def listTypeArgument2 = saveAll2.getArguments()[0].getTypeParameters()[0] + then: + listTypeArgument2.getAnnotationMetadata().hasAnnotation(TypeUseRuntimeAnn.class) + + !saveAll2.hasAnnotation(RequiresValidation) + !saveAll2.hasStereotype(RequiresValidation) + !listTypeArgument2.getAnnotationMetadata().hasAnnotation(MyEntity.class) + !listTypeArgument2.getAnnotationMetadata().hasAnnotation(Introspected.class) + !listTypeArgument2.getAnnotationMetadata().hasStereotype(Introspected.class) + !listTypeArgument2.getAnnotationMetadata().hasAnnotation(BeanProperties.class) + !listTypeArgument2.getAnnotationMetadata().hasStereotype(BeanProperties.class) + + when: + def saveAll3 = definition.findMethod("saveAll3", List.class).get() + def listTypeArgument3 = saveAll3.getArguments()[0].getTypeParameters()[0] + then: + listTypeArgument3.getAnnotationMetadata().hasAnnotation(TypeUseRuntimeAnn.class) + + !saveAll3.hasAnnotation(RequiresValidation) + !saveAll3.hasStereotype(RequiresValidation) + !listTypeArgument3.getAnnotationMetadata().hasAnnotation(MyEntity.class) + !listTypeArgument3.getAnnotationMetadata().hasAnnotation(Introspected.class) + !listTypeArgument3.getAnnotationMetadata().hasStereotype(Introspected.class) + !listTypeArgument3.getAnnotationMetadata().hasAnnotation(BeanProperties.class) + !listTypeArgument3.getAnnotationMetadata().hasStereotype(BeanProperties.class) + + when: + def save2 = definition.findMethod("save2", MyBook.class).get() + def parameter2 = save2.getArguments()[0] + then: + parameter2.getAnnotationMetadata().hasAnnotation(TypeUseRuntimeAnn.class) + + !save2.hasAnnotation(RequiresValidation) + !save2.hasStereotype(RequiresValidation) + !parameter2.getAnnotationMetadata().hasAnnotation(MyEntity.class) + !parameter2.getAnnotationMetadata().hasAnnotation(Introspected.class) + !parameter2.getAnnotationMetadata().hasStereotype(Introspected.class) + !parameter2.getAnnotationMetadata().hasAnnotation(BeanProperties.class) + !parameter2.getAnnotationMetadata().hasStereotype(BeanProperties.class) + + when: + def save3 = definition.findMethod("save3", MyBook.class).get() + def parameter3 = save3.getArguments()[0] + then: + parameter3.getAnnotationMetadata().hasAnnotation(TypeUseRuntimeAnn.class) + + !save3.hasAnnotation(RequiresValidation) + !save3.hasStereotype(RequiresValidation) + !parameter3.getAnnotationMetadata().hasAnnotation(MyEntity.class) + !parameter3.getAnnotationMetadata().hasAnnotation(Introspected.class) + !parameter3.getAnnotationMetadata().hasStereotype(Introspected.class) + !parameter3.getAnnotationMetadata().hasAnnotation(BeanProperties.class) + !parameter3.getAnnotationMetadata().hasStereotype(BeanProperties.class) + + when: + def get = definition.findMethod("get").get() + def returnType = get.getReturnType() + then: + returnType.getAnnotationMetadata().hasAnnotation(TypeUseRuntimeAnn.class) + + !returnType.getAnnotationMetadata().hasAnnotation(MyEntity.class) + !returnType.getAnnotationMetadata().hasAnnotation(Introspected.class) + !returnType.getAnnotationMetadata().hasStereotype(Introspected.class) + !returnType.getAnnotationMetadata().hasAnnotation(BeanProperties.class) + !returnType.getAnnotationMetadata().hasStereotype(BeanProperties.class) + } + + void "test how the type annotations from the type are preserved 2"() { + given: + BeanDefinition bd = buildBeanDefinition('test.MyBean', '''\ +package test + +import io.micronaut.inject.annotation.* +import io.micronaut.context.annotation.* +import io.micronaut.context.annotation.Executable +import java.util.List +import io.micronaut.kotlin.processing.beans.executable.* + +@jakarta.inject.Singleton +class MyBean { + @Executable + fun saveAll(books: List<@TypeUseRuntimeAnn MyBook>) { + } + + @Executable + fun <@TypeUseRuntimeAnn T : MyBook> saveAll2(book: List) { + } + + @Executable + fun <@TypeUseRuntimeAnn T : MyBook> saveAll3(book: List) { + } + + @Executable + fun saveAll4(book: List) { + } + + @Executable + fun saveAll5(book: List<@TypeUseRuntimeAnn T>) { + } + + @Executable + fun save2(book: @TypeUseRuntimeAnn MyBook) { + } + + @Executable + fun <@TypeUseRuntimeAnn T : MyBook> save3(book: T) { + } + + @Executable + fun save4(book: T) { + } + + @Executable + fun save5(book: @TypeUseRuntimeAnn T) { + } + + @Executable + fun get(): @TypeUseRuntimeAnn MyBook? { + return null + } +} + +''') + when: + def saveAll = bd.findMethod("saveAll", List).get() + def listTypeArgument = saveAll.getArguments()[0].getTypeParameters()[0] + then: + validateMyBookArgument(listTypeArgument) + + when: + def saveAll2 = bd.findMethod("saveAll2", List).get() + def listTypeArgument2 = saveAll2.getArguments()[0].getTypeParameters()[0] + then: + validateMyBookArgument(listTypeArgument2) + + when: + def saveAll3 = bd.findMethod("saveAll3", List).get() + def listTypeArgument3 = saveAll3.getArguments()[0].getTypeParameters()[0] + then: + validateMyBookArgument(listTypeArgument3) + + when: + def saveAll4 = bd.findMethod("saveAll4", List).get() + def listTypeArgument4 = saveAll4.getArguments()[0].getTypeParameters()[0] + then: + validateMyBookArgument(listTypeArgument4) + +// when: +// def saveAll5 = bd.findMethod("saveAll5", List).get() +// def listTypeArgument5 = saveAll5.getArguments()[0].getTypeParameters()[0] +// then: +// validateMyBookArgument(listTypeArgument5) + + when: + def save2 = bd.findMethod("save2", MyBook).get() + def parameter2 = save2.getArguments()[0] + then: + validateMyBookArgument(parameter2) + + when: + def save3 = bd.findMethod("save3", MyBook).get() + def parameter3 = save3.getArguments()[0] + then: + validateMyBookArgument(parameter3) + + when: + def save4 = bd.findMethod("save4", MyBook).get() + def parameter4 = save4.getArguments()[0] + then: + validateMyBookArgument(parameter4) + +// when: +// def save5 = bd.findMethod("save5", MyBook).get() +// def parameter5 = save5.getArguments()[0] +// then: +// validateMyBookArgument(parameter5) + + when: + def get = bd.findMethod("get").get() + def returnType = get.getReturnType().asArgument() + then: + def am = returnType.getAnnotationMetadata() + assert am.hasAnnotation(TypeUseRuntimeAnn.class) + assert !am.hasAnnotation(MyEntity.class) + assert !am.hasAnnotation(Introspected.class) + // + Class annotations + assert am.hasStereotype(AnnotationUtil.SINGLETON) + assert am.hasStereotype(Executable) + } + + @PendingFeature + void "test how the type annotations from the type are preserved - pending 1"() { + given: + BeanDefinition bd = buildBeanDefinition('test.MyBean', '''\ +package test + +import io.micronaut.inject.annotation.* +import io.micronaut.context.annotation.* +import io.micronaut.context.annotation.Executable +import java.util.List +import io.micronaut.kotlin.processing.beans.executable.* + +@jakarta.inject.Singleton +class MyBean { + + @Executable + fun saveAll5(book: List<@TypeUseRuntimeAnn T>) { + } +} + +''') + + when: + def saveAll5 = bd.findMethod("saveAll5", List).get() + def listTypeArgument5 = saveAll5.getArguments()[0].getTypeParameters()[0] + then: + validateMyBookArgument(listTypeArgument5) + + } + + @PendingFeature + void "test how the type annotations from the type are preserved - pending 2"() { + given: + BeanDefinition bd = buildBeanDefinition('test.MyBean', '''\ +package test + +import io.micronaut.inject.annotation.* +import io.micronaut.context.annotation.* +import io.micronaut.context.annotation.Executable +import java.util.List +import io.micronaut.kotlin.processing.beans.executable.* + +@jakarta.inject.Singleton +class MyBean { + + @Executable + fun save5(book: @TypeUseRuntimeAnn T) { + } +} + +''') + + when: + def save5 = bd.findMethod("save5", MyBook).get() + def parameter5 = save5.getArguments()[0] + then: + validateMyBookArgument(parameter5) + } + + void validateMyBookArgument(Argument argument) { + // The argument should only have type annotations + def am = argument.getAnnotationMetadata() + assert am.hasAnnotation(TypeUseRuntimeAnn.class) + assert !am.hasAnnotation(MyEntity.class) + assert !am.hasAnnotation(Introspected.class) + assert am.getAnnotationNames().size() == 1 + } } diff --git a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/beans/executable/MyBook.java b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/beans/executable/MyBook.java new file mode 100644 index 00000000000..a627827649a --- /dev/null +++ b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/beans/executable/MyBook.java @@ -0,0 +1,8 @@ +package io.micronaut.kotlin.processing.beans.executable; + +import io.micronaut.core.annotation.Introspected; + +@MyEntity +@Introspected +public class MyBook { +} diff --git a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/beans/executable/MyEntity.java b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/beans/executable/MyEntity.java new file mode 100644 index 00000000000..9ced3757788 --- /dev/null +++ b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/beans/executable/MyEntity.java @@ -0,0 +1,15 @@ +package io.micronaut.kotlin.processing.beans.executable; + +import io.micronaut.core.annotation.Internal; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +@Retention(RUNTIME) +@Target({ElementType.METHOD, ElementType.TYPE, ElementType.FIELD, ElementType.PARAMETER}) +@Internal +public @interface MyEntity { +} diff --git a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/beans/executable/TypeUseRuntimeAnn.java b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/beans/executable/TypeUseRuntimeAnn.java new file mode 100644 index 00000000000..d7366024e60 --- /dev/null +++ b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/beans/executable/TypeUseRuntimeAnn.java @@ -0,0 +1,11 @@ +package io.micronaut.kotlin.processing.beans.executable; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) +@Retention(RetentionPolicy.RUNTIME) +public @interface TypeUseRuntimeAnn { +} diff --git a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/beans/executable/inheritance/InheritedExecutableSpec.groovy b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/beans/executable/inheritance/InheritedExecutableSpec.groovy index 0899c96b11c..89537e18cec 100644 --- a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/beans/executable/inheritance/InheritedExecutableSpec.groovy +++ b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/beans/executable/inheritance/InheritedExecutableSpec.groovy @@ -1,10 +1,9 @@ package io.micronaut.kotlin.processing.beans.executable.inheritance import io.micronaut.inject.BeanDefinition -import spock.lang.PendingFeature import spock.lang.Specification -import static io.micronaut.annotation.processing.test.KotlinCompiler.* +import static io.micronaut.annotation.processing.test.KotlinCompiler.buildBeanDefinition class InheritedExecutableSpec extends Specification { @@ -111,7 +110,7 @@ class StatusController: GenericController() { definition != null definition.getExecutableMethods().any { it.methodName == "create" && it.argumentTypes == [int] as Class[] } definition.getExecutableMethods().any { it.methodName == "save" && it.argumentTypes == [String] as Class[] } - definition.getExecutableMethods().any { it.methodName == "find" && it.argumentTypes == [int] as Class[] } + definition.getExecutableMethods().any { it.methodName == "find" && it.argumentTypes == [Integer] as Class[] } definition.getExecutableMethods().size() == 3 } diff --git a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/inject/ast/ClassElementSpec.groovy b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/inject/ast/ClassElementSpec.groovy index 2e0a606c0b7..04fda28ebdf 100644 --- a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/inject/ast/ClassElementSpec.groovy +++ b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/inject/ast/ClassElementSpec.groovy @@ -1,12 +1,22 @@ package io.micronaut.kotlin.processing.inject.ast + import io.micronaut.annotation.processing.test.AbstractKotlinCompilerSpec +import io.micronaut.core.annotation.AnnotationUtil +import io.micronaut.core.annotation.Introspected +import io.micronaut.core.util.CollectionUtils import io.micronaut.inject.ast.ClassElement import io.micronaut.inject.ast.ConstructorElement +import io.micronaut.inject.ast.Element import io.micronaut.inject.ast.ElementModifier import io.micronaut.inject.ast.ElementQuery +import io.micronaut.inject.ast.FieldElement +import io.micronaut.inject.ast.GenericPlaceholderElement +import io.micronaut.inject.ast.MemberElement import io.micronaut.inject.ast.MethodElement import io.micronaut.inject.ast.PropertyElement +import io.micronaut.inject.ast.WildcardElement +import io.micronaut.kotlin.processing.visitor.KotlinClassElement import spock.lang.PendingFeature class ClassElementSpec extends AbstractKotlinCompilerSpec { @@ -215,8 +225,7 @@ package ast.test * * @param constructorProp construct prop */ -class Test( - val constructorProp : String) : Parent(constructorProp), One { +class Test(val constructorProp : String) : Parent(constructorProp), One { /** * Property doc */ @@ -287,6 +296,9 @@ interface Three Map propMap = propertyElements.collectEntries { [it.name, it] } + Map syntheticPropMap = syntheticProperties.collectEntries { + [it.name, it] + } assert classElement.documentation.isPresent() assert methodMap['add'].parameters[1].genericType.simpleName == 'String' @@ -302,8 +314,25 @@ interface Three assert propMap['conventionProp'].readMethod.get().genericReturnType.simpleName == 'String' assert propMap['conventionProp'].writeMethod.get().parameters[0].type.simpleName == 'CharSequence' assert propMap['conventionProp'].writeMethod.get().parameters[0].genericType.simpleName == 'String' + assert propMap['parentConstructorProp'].type.simpleName == 'CharSequence' assert propMap['parentConstructorProp'].genericType.simpleName == 'String' + + assert syntheticPropMap['parentConstructorProp'].type.simpleName == 'CharSequence' + assert syntheticPropMap['parentConstructorProp'].genericType.simpleName == 'String' + + assert propMap['constructorProp'].type.simpleName == 'String' + assert propMap['constructorProp'].genericType.simpleName == 'String' + + assert syntheticPropMap['constructorProp'].type.simpleName == 'String' + assert syntheticPropMap['constructorProp'].genericType.simpleName == 'String' + + assert propMap['parentProp'].type.simpleName == 'CharSequence' + assert propMap['parentProp'].genericType.simpleName == 'String' + + assert syntheticPropMap['parentProp'].type.simpleName == 'CharSequence' + assert syntheticPropMap['parentProp'].genericType.simpleName == 'String' + assert methodMap['publicFunc'].documentation.isPresent() assert methodMap['parentFunc'].returnType.simpleName == 'CharSequence' assert methodMap['parentFunc'].genericReturnType.simpleName == 'String' @@ -311,4 +340,1404 @@ interface Three assert methodMap['parentFunc'].parameters[0].genericType.simpleName == 'String' } } + + void "test annotation metadata present on deep type parameters for field"() { + ClassElement ce = buildClassElementTransformed('test.Test', ''' +package test; +import io.micronaut.core.annotation.*; +import javax.validation.constraints.*; +import java.util.List; + +class Test { + var deepList: List<@Size(min=1, max=2) List<@NotEmpty List<@NotNull String>>>? = null +} +''') { + def field = it.findField("deepList").get() + initializeAllTypeArguments(field.getType()) + initializeAllTypeArguments(field.getGenericType()) + it + } + expect: + def field = ce.findField("deepList").get() + def fieldType = field.getGenericType() + + fieldType.getAnnotationMetadata().getAnnotationNames().size() == 0 + + assertListGenericArgument(fieldType, { ClassElement listArg1 -> + assert listArg1.getAnnotationMetadata().getAnnotationNames().asList() == ['javax.validation.constraints.Size$List'] + assert listArg1.getTypeAnnotationMetadata().getAnnotationNames().asList() == ['javax.validation.constraints.Size$List'] + assertListGenericArgument(listArg1, { ClassElement listArg2 -> + assert listArg2.getAnnotationMetadata().getAnnotationNames().asList() == ['javax.validation.constraints.NotEmpty$List'] + assert listArg2.getTypeAnnotationMetadata().getAnnotationNames().asList() == ['javax.validation.constraints.NotEmpty$List'] + assertListGenericArgument(listArg2, { ClassElement listArg3 -> + assert listArg3.getAnnotationMetadata().getAnnotationNames().asList() == ['javax.validation.constraints.NotNull$List'] + assert listArg3.getTypeAnnotationMetadata().getAnnotationNames().asList() == ['javax.validation.constraints.NotNull$List'] + }) + }) + }) + + def level1 = fieldType.getTypeArguments()["E"] + level1.getAnnotationMetadata().getAnnotationNames().asList() == ['javax.validation.constraints.Size$List'] + level1.getTypeAnnotationMetadata().getAnnotationNames().asList() == ['javax.validation.constraints.Size$List'] + def level2 = level1.getTypeArguments()["E"] + level2.getAnnotationMetadata().getAnnotationNames().asList() == ['javax.validation.constraints.NotEmpty$List'] + level2.getTypeAnnotationMetadata().getAnnotationNames().asList() == ['javax.validation.constraints.NotEmpty$List'] + def level3 = level2.getTypeArguments()["E"] + level3.getAnnotationMetadata().getAnnotationNames().asList() == ['javax.validation.constraints.NotNull$List'] + level3.getTypeAnnotationMetadata().getAnnotationNames().asList() == ['javax.validation.constraints.NotNull$List'] + } + + void "test annotation metadata present on deep type parameters for method"() { + ClassElement ce = buildClassElementTransformed('test.Test', ''' +package test +import javax.validation.constraints.* +import java.util.List + +class Test { + fun deepList(): List<@Size(min=1, max=2) List<@NotEmpty List<@NotNull String>>>? { + return null + } +} +''') { + def method = it.findMethod("deepList").get() + initializeAllTypeArguments(method.getReturnType()) + initializeAllTypeArguments(method.getGenericReturnType()) + it + } + expect: + def method = ce.findMethod("deepList").get() + def theType = method.getGenericReturnType() + + theType.getAnnotationMetadata().getAnnotationNames().size() == 0 + + def level1 = theType.getTypeArguments()["E"] + level1.getAnnotationMetadata().getAnnotationNames().asList() == ['javax.validation.constraints.Size$List'] + level1.getTypeAnnotationMetadata().getAnnotationNames().asList() == ['javax.validation.constraints.Size$List'] + def level2 = level1.getTypeArguments()["E"] + level2.getAnnotationMetadata().getAnnotationNames().asList() == ['javax.validation.constraints.NotEmpty$List'] + level2.getTypeAnnotationMetadata().getAnnotationNames().asList() == ['javax.validation.constraints.NotEmpty$List'] + def level3 = level2.getTypeArguments()["E"] + level3.getAnnotationMetadata().getAnnotationNames().asList() == ['javax.validation.constraints.NotNull$List'] + level3.getTypeAnnotationMetadata().getAnnotationNames().asList() == ['javax.validation.constraints.NotNull$List'] + + assertListGenericArgument(theType, { ClassElement listArg1 -> + assert listArg1.getAnnotationMetadata().getAnnotationNames().asList() == ['javax.validation.constraints.Size$List'] + assert listArg1.getTypeAnnotationMetadata().getAnnotationNames().asList() == ['javax.validation.constraints.Size$List'] + assertListGenericArgument(listArg1, { ClassElement listArg2 -> + assert listArg2.getAnnotationMetadata().getAnnotationNames().asList() == ['javax.validation.constraints.NotEmpty$List'] + assert listArg2.getTypeAnnotationMetadata().getAnnotationNames().asList() == ['javax.validation.constraints.NotEmpty$List'] + assertListGenericArgument(listArg2, { ClassElement listArg3 -> + assert listArg3.getTypeAnnotationMetadata().getAnnotationNames().asList() == ['javax.validation.constraints.NotNull$List'] + assert listArg3.getAnnotationMetadata().getAnnotationNames().asList() == ['javax.validation.constraints.NotNull$List'] + }) + }) + }) + } + + void "test type annotations on a method and a field"() { + ClassElement ce = buildClassElement('test.Test', ''' +package test + +import io.micronaut.kotlin.processing.inject.ast.* + +class Test { + var myField: @TypeUseRuntimeAnn @TypeUseClassAnn Str? = null + + fun myMethod(): @TypeUseRuntimeAnn @TypeUseClassAnn Str? { + return null + } +} + +class Str +''') + expect: + def field = ce.findField("myField").get() + def method = ce.findMethod("myMethod").get() + + // Type annotations shouldn't appear on the field + field.getAnnotationMetadata().getAnnotationNames().asList() == [AnnotationUtil.NULLABLE] + field.getType().getAnnotationMetadata().getAnnotationNames().asList() == [TypeUseRuntimeAnn.name, TypeUseClassAnn.name ] + field.getGenericType().getAnnotationMetadata().getAnnotationNames().asList() == [TypeUseRuntimeAnn.name, TypeUseClassAnn.name ] + // Type annotations shouldn't appear on the method + method.getAnnotationMetadata().getAnnotationNames().asList() == [AnnotationUtil.NULLABLE] + method.getReturnType().getAnnotationMetadata().getAnnotationNames().asList() == [TypeUseRuntimeAnn.name, TypeUseClassAnn.name ] + method.getGenericReturnType().getAnnotationMetadata().getAnnotationNames().asList() == [TypeUseRuntimeAnn.name, TypeUseClassAnn.name ] + } + + void "test type annotations on a method and a field 2"() { + ClassElement ce = buildClassElement('test.Test', ''' +package test + +import io.micronaut.kotlin.processing.inject.ast.* + +class Test { + @TypeFieldRuntimeAnn + var myField: @TypeUseRuntimeAnn @TypeUseClassAnn Str? = null + + @TypeMethodRuntimeAnn + fun myMethod(): @TypeUseRuntimeAnn @TypeUseClassAnn Str? { + return null + } +} + +class Str +''') + expect: + def field = ce.findField("myField").get() + def method = ce.findMethod("myMethod").get() + + // Type annotations shouldn't appear on the field + field.getAnnotationMetadata().getAnnotationNames().asList() == [TypeFieldRuntimeAnn.name, AnnotationUtil.NULLABLE] + field.getType().getAnnotationMetadata().getAnnotationNames().asList() == [TypeUseRuntimeAnn.name, TypeUseClassAnn.name] + field.getGenericType().getAnnotationMetadata().getAnnotationNames().asList() == [TypeUseRuntimeAnn.name, TypeUseClassAnn.name] + // Type annotations shouldn't appear on the method + method.getAnnotationMetadata().getAnnotationNames().asList() == [TypeMethodRuntimeAnn.name, AnnotationUtil.NULLABLE] + method.getReturnType().getAnnotationMetadata().getAnnotationNames().asList() == [TypeUseRuntimeAnn.name, TypeUseClassAnn.name] + method.getGenericReturnType().getAnnotationMetadata().getAnnotationNames().asList() == [TypeUseRuntimeAnn.name, TypeUseClassAnn.name] + } + + void "test recursive generic type parameter"() { + given: + ClassElement ce = buildClassElement('test.TrackedSortedSet', '''\ +package test + +class TrackedSortedSet> + +''') + expect: + def typeArguments = ce.getTypeArguments() + typeArguments.size() == 1 + def typeArgument = typeArguments.get("T") + typeArgument.name == "java.lang.Comparable" + def nextTypeArguments = typeArgument.getTypeArguments() + def nextTypeArgument = nextTypeArguments.get("T") + nextTypeArgument.name == "java.lang.Comparable" + def nextNextTypeArguments = nextTypeArgument.getTypeArguments() + def nextNextTypeArgument = nextNextTypeArguments.get("T") + nextNextTypeArgument.name == "java.lang.Object" + } + + void "test annotation metadata present on deep type parameters for method 2"() { + ClassElement ce = buildClassElementTransformed('test.Test', ''' +package test +import io.micronaut.core.annotation.* +import javax.validation.constraints.* +import java.util.List +import io.micronaut.kotlin.processing.inject.ast.* + +class Test { + fun deepList() : Lst>>? { + return null + } +} + +class Lst +''') { + def method = it.findMethod("deepList").get() + def theType = method.getGenericReturnType() + def level1 = theType.getTypeArguments()["E"] + def level2 = level1.getTypeArguments()["E"] + def level3 = level2.getTypeArguments()["E"] + level3.getAnnotationNames() + level3 + } + expect: + ce.getAnnotationMetadata().getAnnotationNames().asList() == [TypeUseRuntimeAnn.name, TypeUseClassAnn.name] + } + + void "test annotations on recursive generic type parameter 1"() { + given: + ClassElement ce = buildClassElementTransformed('test.TrackedSortedSet', '''\ +package test + +import io.micronaut.kotlin.processing.inject.ast.TypeUseRuntimeAnn + +class TrackedSortedSet<@TypeUseRuntimeAnn T : java.lang.Comparable> { +} + +''') { + initializeAllTypeArguments(it) + it + } + expect: + def typeArguments = ce.getTypeArguments() + typeArguments.size() == 1 + def typeArgument = typeArguments.get("T") + typeArgument.name == "java.lang.Comparable" + typeArgument.getAnnotationNames().asList() == [TypeUseRuntimeAnn.name] + } + + void "test recursive generic type parameter 2"() { + when: + buildClassElement('test.Test', '''\ +package test + +class Test> + +''') + then: + def e = thrown(RuntimeException) + e.message.contains "This type parameter violates the Finite Bound Restriction" + } + + void "test recursive generic type parameter 3"() { + given: + ClassElement ce = buildClassElement('test.Test', '''\ +package test + +class Test> + +''') + expect: + def typeArguments = ce.getTypeArguments() + typeArguments.size() == 1 + def typeArgument = typeArguments.get("T") + typeArgument.name == "test.Test" + def nextTypeArguments = typeArgument.getTypeArguments() + def nextTypeArgument = nextTypeArguments.get("T") + nextTypeArgument.name == "test.Test" + def nextNextTypeArguments = nextTypeArgument.getTypeArguments() + def nextNextTypeArgument = nextNextTypeArguments.get("T") + nextNextTypeArgument.name == "java.lang.Object" + } + + void "test recursive generic method return"() { + given: + ClassElement ce = buildClassElementTransformed('test.MyFactory', '''\ +package test + +import org.hibernate.SessionFactory +import org.hibernate.engine.spi.SessionFactoryDelegatingImpl + +class MyFactory { + + fun sessionFactory() : SessionFactory { + return SessionFactoryDelegatingImpl(null) + } +} + +''') { + def sessionFactoryMethod = it.findMethod("sessionFactory").get() + def withOptionsMethod = sessionFactoryMethod.getReturnType().findMethod("withOptions").get() + withOptionsMethod.getReturnType().getTypeArguments() + return it + } + expect: + def sessionFactoryMethod = ce.findMethod("sessionFactory").get() + def withOptionsMethod = sessionFactoryMethod.getReturnType().findMethod("withOptions").get() + def typeArguments = withOptionsMethod.getReturnType().getTypeArguments() + typeArguments.size() == 1 + def typeArgument = typeArguments.get("T") + typeArgument.name == "org.hibernate.SessionBuilder" + def nextTypeArguments = typeArgument.getTypeArguments() + def nextTypeArgument = nextTypeArguments.get("T") + nextTypeArgument.name == "org.hibernate.SessionBuilder" + def nextNextTypeArguments = nextTypeArgument.getTypeArguments() + def nextNextTypeArgument = nextNextTypeArguments.get("T") + nextNextTypeArgument.name == "java.lang.Object" + } + + void "test recursive generic method return 2"() { + when: + buildClassElement('test.MyFactory', '''\ +package test; + +class MyFactory { + + fun myBean(): MyBean { + return MyBean() + } +} + +interface MyBuilder> { + fun build(): T +} + +class MyBean { + + fun myBuilder() : MyBuilder? { + return null + } + +} +''') + then: + def e = thrown(RuntimeException) + e.message.contains "This type parameter violates the Finite Bound Restriction" + } + + void "test recursive generic method return 3"() { + given: + ClassElement ce = buildClassElement('test.MyFactory', '''\ +package test + +class MyFactory { + + fun myBean(): MyBean { + return MyBean() + } +} + +interface MyBuilder> { + fun build(): T +} + +class MyBean { + + fun > myBuilder() : K? { + return null + } + +} +''') + expect: + def myBeanMethod = ce.findMethod("myBean").get() + def myBuilderMethod = myBeanMethod.getReturnType().findMethod("myBuilder").get() + def typeArguments = myBuilderMethod.getReturnType().getTypeArguments() + typeArguments.size() == 1 + def typeArgument = typeArguments.get("T") + typeArgument.name == "test.MyBuilder" + def nextTypeArguments = typeArgument.getTypeArguments() + def nextTypeArgument = nextTypeArguments.get("T") + nextTypeArgument.name == "java.lang.Object" + } + + void "test recursive generic method return 4"() { + given: + ClassElement ce = buildClassElement('test.MyFactory', '''\ +package test + +class MyFactory { + + fun myBean(): MyBean { + return MyBean() + } +} + +interface MyBuilder> { + fun build(): T +} + +class MyBean { + + fun myBuilder() : MyBuilder<*>? { + return null + } + +} +''') + expect: + def myBeanMethod = ce.findMethod("myBean").get() + def myBuilderMethod = myBeanMethod.getReturnType().findMethod("myBuilder").get() + def typeArguments = myBuilderMethod.getReturnType().getTypeArguments() + typeArguments.size() == 1 + def typeArgument = typeArguments.get("T") + typeArgument.name == "test.MyBuilder" + def nextTypeArguments = typeArgument.getTypeArguments() + def nextTypeArgument = nextTypeArguments.get("T") + nextTypeArgument.name == "java.lang.Object" + } + + void "test recursive generic method return 5"() { + given: + ClassElement ce = buildClassElement('test.MyFactory', '''\ +package test + +class MyFactory { + + fun myBean(): MyBean<*> { + return MyBean() + } +} + +interface MyBuilder> { + fun build(): T +} + +class MyBean> { + + fun myBuilder() : MyBuilder? { + return null + } + +} +''') + expect: + def myBeanMethod = ce.findMethod("myBean").get() + def myBuilderMethod = myBeanMethod.getReturnType().findMethod("myBuilder").get() + def typeArguments = myBuilderMethod.getReturnType().getTypeArguments() + typeArguments.size() == 1 + def typeArgument = typeArguments.get("T") + typeArgument.name == "test.MyBuilder" + def nextTypeArguments = typeArgument.getTypeArguments() + def nextTypeArgument = nextTypeArguments.get("T") + nextTypeArgument.name == "test.MyBuilder" + def nextNextTypeArguments = nextTypeArgument.getTypeArguments() + def nextNextTypeArgument = nextNextTypeArguments.get("T") + nextNextTypeArgument.name == "java.lang.Object" + } + + void "test how the annotations from the type are propagated"() { + given: + ClassElement ce = buildClassElementTransformed('test.MyBean', '''\ +package test + +import io.micronaut.inject.annotation.* +import io.micronaut.context.annotation.* +import java.util.List +import io.micronaut.kotlin.processing.inject.ast.* +import jakarta.inject.Singleton + +@Singleton +class MyBean { + @Executable + fun saveAll(books: List) { + } + + @Executable + fun saveAll2(book: List) { + } + + @Executable + fun saveAll3(book: List) { + } + + @Executable + fun save2(book: MyBook) { + } + + @Executable + fun save3(book: T) { + } + + @Executable + fun get(): MyBook? { + return null + } +} + + +''') { + it.getMethods().forEach { method -> + initializeAllTypeArguments(method.getReturnType()) + initializeAllTypeArguments(method.getGenericReturnType()) + method.getParameters().each { parameter -> + parameter.getAnnotationNames() + initializeAllTypeArguments(parameter.getType()) + initializeAllTypeArguments(parameter.getGenericType()) + } + } + it + } + when: + def saveAll = ce.findMethod("saveAll").get() + def listTypeArgument = saveAll.getParameters()[0].getType().getTypeArguments(List).get("E") + then: + if (!listTypeArgument.hasAnnotation(MyEntity.class)) { + def kotlinClassElement = listTypeArgument as KotlinClassElement + System.out.println("XXX1 "+ kotlinClassElement) + System.out.println("XXX2 "+ kotlinClassElement.class) + System.out.println("XXX3 "+ kotlinClassElement.nativeType) + + def iterator = kotlinClassElement.nativeType.type?.annotations?.iterator() + System.out.println("XXX4 "+ (iterator == null ? [] : CollectionUtils.iteratorToSet(iterator))) + System.out.println("XXX5 "+ CollectionUtils.iteratorToSet(kotlinClassElement.nativeType.declaration.annotations.iterator())) + System.out.println("XXX6 "+ kotlinClassElement.nativeType.declaration) + System.out.println("XXX7 "+ kotlinClassElement.nativeType.declaration.class) + + } + listTypeArgument.hasAnnotation(MyEntity.class) + listTypeArgument.hasAnnotation(Introspected.class) + + when: + def saveAll2 = ce.findMethod("saveAll2").get() + def listTypeArgument2 = saveAll2.getParameters()[0].getType().getTypeArguments(List).get("E") + then: + listTypeArgument2.hasAnnotation(MyEntity.class) + listTypeArgument2.hasAnnotation(Introspected.class) + + when: + def saveAll3 = ce.findMethod("saveAll3").get() + def listTypeArgument3 = saveAll3.getParameters()[0].getType().getTypeArguments(List).get("E") + then: + listTypeArgument3.hasAnnotation(MyEntity.class) + listTypeArgument3.hasAnnotation(Introspected.class) + + when: + def save2 = ce.findMethod("save2").get() + def parameter2 = save2.getParameters()[0].getType() + then: + parameter2.hasAnnotation(MyEntity.class) + parameter2.hasAnnotation(Introspected.class) + + when: + def save3 = ce.findMethod("save3").get() + def parameter3 = save3.getParameters()[0].getType() + then: + parameter3.hasAnnotation(MyEntity.class) + parameter3.hasAnnotation(Introspected.class) + + when: + def get = ce.findMethod("get").get() + def returnType = get.getReturnType() + then: + returnType.hasAnnotation(MyEntity.class) + returnType.hasAnnotation(Introspected.class) + } + + void "test how the type annotations from the type are propagated"() { + given: + ClassElement ce = buildClassElementTransformed('test.MyBean','''\ +package test; + +import io.micronaut.context.annotation.Executable +import jakarta.inject.Singleton +import io.micronaut.kotlin.processing.inject.ast.* + +@Singleton +class MyBean { + @Executable + fun saveAll(books: List<@TypeUseRuntimeAnn MyBook>) { + } + + @Executable + fun <@TypeUseRuntimeAnn T : MyBook> saveAll2(book: List) { + } + + @Executable + fun <@TypeUseRuntimeAnn T : MyBook> saveAll3(book: List) { + } + + @Executable + fun saveAll4(book: List) { + } + + @Executable + fun saveAll5(book: List<@TypeUseRuntimeAnn T>) { + } + + @Executable + fun save2(book: @TypeUseRuntimeAnn MyBook) { + } + + @Executable + fun save2x(book: @TypeUseRuntimeAnn MyBook) { + } + + @Executable + fun save2xx(book: MyBook) { + } + + @Executable + fun <@TypeUseRuntimeAnn T : MyBook> save3(book: T) { + } + + @Executable + fun save4(book: T) { + } + + @Executable + fun save5(book: @TypeUseRuntimeAnn T) { + } + + @Executable + fun get(): @TypeUseRuntimeAnn MyBook? { + return null + } +} + +'''){ + it.getMethods().forEach { method -> + initializeAllTypeArguments(method.getReturnType()) + initializeAllTypeArguments(method.getGenericReturnType()) + method.getParameters().each { parameter -> + parameter.getAnnotationNames() + initializeAllTypeArguments(parameter.getType()) + initializeAllTypeArguments(parameter.getGenericType()) + } + } + it + } + when: + def saveAll = ce.findMethod("saveAll").get() + def listTypeArgument = saveAll.getParameters()[0].getType().getTypeArguments(List).get("E") + then: + validateMyBookArgument(listTypeArgument) + + when: + def saveAll2 = ce.findMethod("saveAll2").get() + def listTypeArgument2 = saveAll2.getParameters()[0].getType().getTypeArguments(List).get("E") + then: + validateMyBookArgument(listTypeArgument2) + + when: + def saveAll3 = ce.findMethod("saveAll3").get() + def listTypeArgument3 = saveAll3.getParameters()[0].getType().getTypeArguments(List).get("E") + then: + validateMyBookArgument(listTypeArgument3) + + when: + def saveAll4 = ce.findMethod("saveAll4").get() + def listTypeArgument4 = saveAll4.getParameters()[0].getType().getTypeArguments(List).get("E") + then: + validateMyBookArgument(listTypeArgument4) + +// when: +// def saveAll5 = ce.findMethod("saveAll5").get() +// def listTypeArgument5 = saveAll5.getParameters()[0].getType().getTypeArguments(List).get("E") +// then: +// noExceptionThrown() +// validateMyBookArgument(listTypeArgument5) + + when: + def save2 = ce.findMethod("save2").get() + def parameter2 = save2.getParameters()[0].getType() + then: + validateMyBookArgument(parameter2) + + when: + def save3 = ce.findMethod("save3").get() + def parameter3 = save3.getParameters()[0].getType() + then: + validateMyBookArgument(parameter3) + + when: + def save4 = ce.findMethod("save4").get() + def parameter4 = save4.getParameters()[0].getType() + then: + validateMyBookArgument(parameter4) + +// when: +// def save5 = ce.findMethod("save5").get() +// def parameter5 = save5.getParameters()[0].getType() +// then: +// validateMyBookArgument(parameter5) + + when: + def get = ce.findMethod("get").get() + def returnType = get.getReturnType() + then: + validateMyBookArgument(returnType) + } + + @PendingFeature + void "test how the type annotations from the type are propagated - pending 1"() { + given: + ClassElement ce = buildClassElementTransformed('test.MyBean','''\ +package test; + +import io.micronaut.context.annotation.Executable +import jakarta.inject.Singleton +import io.micronaut.kotlin.processing.inject.ast.* + +@Singleton +class MyBean { + + @Executable + fun saveAll5(book: List<@TypeUseRuntimeAnn T>) { + } + + @Executable + fun save2(book: @TypeUseRuntimeAnn MyBook) { + } + + @Executable + fun save2x(book: @TypeUseRuntimeAnn MyBook) { + } + + @Executable + fun save2xx(book: MyBook) { + } + + @Executable + fun <@TypeUseRuntimeAnn T : MyBook> save3(book: T) { + } + + @Executable + fun save4(book: T) { + } + + @Executable + fun save5(book: @TypeUseRuntimeAnn T) { + } + + @Executable + fun get(): @TypeUseRuntimeAnn MyBook? { + return null + } +} + +'''){ + it.getMethods().forEach { method -> + initializeAllTypeArguments(method.getReturnType()) + initializeAllTypeArguments(method.getGenericReturnType()) + method.getParameters().each { parameter -> + parameter.getAnnotationNames() + initializeAllTypeArguments(parameter.getType()) + initializeAllTypeArguments(parameter.getGenericType()) + } + } + it + } + when: + def saveAll5 = ce.findMethod("saveAll5").get() + def listTypeArgument5 = saveAll5.getParameters()[0].getType().getTypeArguments(List).get("E") + then: + noExceptionThrown() + validateMyBookArgument(listTypeArgument5) + } + + @PendingFeature + void "test how the type annotations from the type are propagated - pending 2"() { + given: + ClassElement ce = buildClassElementTransformed('test.MyBean','''\ +package test; + +import io.micronaut.context.annotation.Executable +import jakarta.inject.Singleton +import io.micronaut.kotlin.processing.inject.ast.* + +@Singleton +class MyBean { + + @Executable + fun save5(book: @TypeUseRuntimeAnn T) { + } + +} + +'''){ + it.getMethods().forEach { method -> + initializeAllTypeArguments(method.getReturnType()) + initializeAllTypeArguments(method.getGenericReturnType()) + method.getParameters().each { parameter -> + parameter.getAnnotationNames() + initializeAllTypeArguments(parameter.getType()) + initializeAllTypeArguments(parameter.getGenericType()) + } + } + it + } + + when: + def save5 = ce.findMethod("save5").get() + def parameter5 = save5.getParameters()[0].getType() + then: + validateMyBookArgument(parameter5) + + } + + void validateMyBookArgument(ClassElement classElement) { + // The class element should have all the annotations present + assert classElement.hasAnnotation(TypeUseRuntimeAnn.class) + assert classElement.hasAnnotation(MyEntity.class) + assert classElement.hasAnnotation(Introspected.class) + + def typeAnnotationMetadata = classElement.getTypeAnnotationMetadata() + // The type annotations should have only type annotations + assert typeAnnotationMetadata.hasAnnotation(TypeUseRuntimeAnn.class) + assert !typeAnnotationMetadata.hasAnnotation(MyEntity.class) + assert !typeAnnotationMetadata.hasAnnotation(Introspected.class) + + // Get the actual type -> the type shouldn't have any type annotations + def type = classElement.getType() + assert !type.hasAnnotation(TypeUseRuntimeAnn.class) + assert type.hasAnnotation(MyEntity.class) + assert type.hasAnnotation(Introspected.class) + assert type.getTypeAnnotationMetadata().isEmpty() + } + + void "test type annotations cache"() { + given: + ClassElement ce = buildClassElementTransformed('test.MyBean','''\ +package test + +import io.micronaut.context.annotation.Executable +import jakarta.inject.Singleton +import io.micronaut.kotlin.processing.inject.ast.* + +@Singleton +class MyBean { + + var field1: MyBook? = null + var field2: @TypeUseRuntimeAnn MyBook? = null + + @Executable + fun save1(book: MyBook) { + } + + @Executable + fun save2(book: @TypeUseRuntimeAnn MyBook) { + } + + @Executable + fun get1(): MyBook? { + return null + } + + @Executable + fun get2(): @TypeUseRuntimeAnn MyBook? { + return null + } + +} + +'''){ + it.getMethods().forEach { method -> + initializeAllTypeArguments(method.getReturnType()) + initializeAllTypeArguments(method.getGenericReturnType()) + method.getParameters().each { parameter -> + parameter.getAnnotationNames() + initializeAllTypeArguments(parameter.getType()) + initializeAllTypeArguments(parameter.getGenericType()) + } + } + it.getFields().forEach { method -> + initializeAllTypeArguments(method.type) + initializeAllTypeArguments(method.genericType) + } + it + } + + when: + def save1 = ce.findMethod("save1").get() + def parameter1 = save1.getParameters()[0] + def save2 = ce.findMethod("save2").get() + def parameter2 = save2.getParameters()[0] + then: + parameter1.nativeType != parameter2.nativeType + parameter1.type.nativeType != parameter2.type.nativeType + parameter1.genericType.nativeType != parameter2.genericType.nativeType + parameter1.type.type.nativeType == parameter2.type.type.nativeType + parameter1.genericType.type.nativeType == parameter2.genericType.type.nativeType + + !parameter1.hasAnnotation(TypeUseRuntimeAnn) + !parameter1.type.hasAnnotation(TypeUseRuntimeAnn) + !parameter1.genericType.hasAnnotation(TypeUseRuntimeAnn) + + parameter2.type.hasAnnotation(TypeUseRuntimeAnn) + parameter2.genericType.hasAnnotation(TypeUseRuntimeAnn) + !parameter2.hasAnnotation(TypeUseRuntimeAnn) + + !parameter2.type.type.hasAnnotation(TypeUseRuntimeAnn) + !parameter2.genericType.type.hasAnnotation(TypeUseRuntimeAnn) + when: + def get1 = ce.findMethod("get1").get() + def get2 = ce.findMethod("get2").get() + then: + get1.nativeType != get2.nativeType + get1.returnType.nativeType != get2.returnType.nativeType + get1.genericReturnType.nativeType != get2.genericReturnType.nativeType + get1.returnType.type.nativeType == get2.returnType.type.nativeType + get1.genericReturnType.type.nativeType == get2.genericReturnType.type.nativeType + + when: + def field1 = ce.findField("field1").get() + def field2 = ce.findField("field2").get() + then: + field1.nativeType != field2.nativeType + field1.type.nativeType != field2.type.nativeType + field1.genericType.nativeType != field2.genericType.nativeType + field1.type.type.nativeType == field2.type.type.nativeType + field1.genericType.type.nativeType == field2.genericType.type.nativeType + + + !field1.hasAnnotation(TypeUseRuntimeAnn) + !field1.type.hasAnnotation(TypeUseRuntimeAnn) + !field1.genericType.hasAnnotation(TypeUseRuntimeAnn) + + !field2.hasAnnotation(TypeUseRuntimeAnn) + field2.type.hasAnnotation(TypeUseRuntimeAnn) + field2.genericType.hasAnnotation(TypeUseRuntimeAnn) + + !field2.type.type.hasAnnotation(TypeUseRuntimeAnn) + !field2.genericType.type.hasAnnotation(TypeUseRuntimeAnn) + } + + void "test generics model"() { + ClassElement ce = buildClassElement('test.Test', ''' +package test + +class Test { + fun method1() : Lst>>? { + return null + } +} + +class Lst + +''') + expect: + def method1 = ce.findMethod("method1").get() + def genericType = method1.getGenericReturnType() + def genericTypeLevel1 = genericType.getTypeArguments()["E"] + !genericTypeLevel1.isGenericPlaceholder() + !genericTypeLevel1.isWildcard() + def genericTypeLevel2 = genericTypeLevel1.getTypeArguments()["E"] + !genericTypeLevel2.isGenericPlaceholder() + !genericTypeLevel2.isWildcard() + def genericTypeLevel3 = genericTypeLevel2.getTypeArguments()["E"] + !genericTypeLevel3.isGenericPlaceholder() + !genericTypeLevel3.isWildcard() + + def type = method1.getReturnType() + def typeLevel1 = type.getTypeArguments()["E"] + !typeLevel1.isGenericPlaceholder() + !typeLevel1.isWildcard() + def typeLevel2 = typeLevel1.getTypeArguments()["E"] + !typeLevel2.isGenericPlaceholder() + !typeLevel2.isWildcard() + def typeLevel3 = typeLevel2.getTypeArguments()["E"] + !typeLevel3.isGenericPlaceholder() + !typeLevel3.isWildcard() + } + + void "test generics model for wildcard"() { + ClassElement ce = buildClassElement('test.Test', ''' +package test + +class Test { + + fun method(): Lst<*>? { + return null + } +} + +class Lst + +''') + expect: + def method = ce.getEnclosedElement(ElementQuery.ALL_METHODS.named("method")).get() + def genericTypeArgument = method.getGenericReturnType().getTypeArguments()["E"] + !genericTypeArgument.isGenericPlaceholder() + genericTypeArgument.isRawType() + genericTypeArgument.isWildcard() + + def typeArgument = method.getReturnType().getTypeArguments()["E"] + !typeArgument.isGenericPlaceholder() + typeArgument.isRawType() + typeArgument.isWildcard() + } + + void "test generics model for placeholder"() { + ClassElement ce = buildClassElement('test.Test', ''' +package test + +class Test { + + fun method(): Lst? { + return null + } +} + +class Lst + +''') + expect: + def method = ce.getEnclosedElement(ElementQuery.ALL_METHODS.named("method")).get() + def genericTypeArgument = method.getGenericReturnType().getTypeArguments()["E"] + genericTypeArgument.isGenericPlaceholder() + !genericTypeArgument.isRawType() + !genericTypeArgument.isWildcard() + + def typeArgument = method.getReturnType().getTypeArguments()["E"] + typeArgument.isGenericPlaceholder() + !typeArgument.isRawType() + !typeArgument.isWildcard() + } + + void "test generics model for class placeholder wildcard"() { + ClassElement ce = buildClassElement('test.Test', ''' +package test + +class Test { + + fun method() : Lst? { + return null + } +} + +class Lst + +''') + expect: + def method = ce.getEnclosedElement(ElementQuery.ALL_METHODS.named("method")).get() + def genericTypeArgument = method.getGenericReturnType().getTypeArguments()["E"] + !genericTypeArgument.isGenericPlaceholder() + !genericTypeArgument.isRawType() + genericTypeArgument.isWildcard() + + def genericWildcard = genericTypeArgument as WildcardElement + !genericWildcard.lowerBounds + genericWildcard.upperBounds.size() == 1 + def genericUpperBound = genericWildcard.upperBounds[0] + genericUpperBound.name == "java.lang.Object" + genericUpperBound.isGenericPlaceholder() + !genericUpperBound.isWildcard() + !genericUpperBound.isRawType() + def genericPlaceholderUpperBound = genericUpperBound as GenericPlaceholderElement + genericPlaceholderUpperBound.variableName == "T" + genericPlaceholderUpperBound.declaringElement.get() == ce + + def typeArgument = method.getReturnType().getTypeArguments()["E"] + !typeArgument.isGenericPlaceholder() + !typeArgument.isRawType() + typeArgument.isWildcard() + + def wildcard = genericTypeArgument as WildcardElement + !wildcard.lowerBounds + wildcard.upperBounds.size() == 1 + def upperBound = wildcard.upperBounds[0] + upperBound.name == "java.lang.Object" + upperBound.isGenericPlaceholder() + !upperBound.isWildcard() + !upperBound.isRawType() + def placeholderUpperBound = upperBound as GenericPlaceholderElement + placeholderUpperBound.variableName == "T" + placeholderUpperBound.declaringElement.get() == ce + } + + void "test generics model for method placeholder wildcard"() { + ClassElement ce = buildClassElement('test.Test', ''' +package test + +class Test { + + fun method(): Lst? { + return null + } +} + +class Lst + +''') + expect: + def method = ce.getEnclosedElement(ElementQuery.ALL_METHODS.named("method")).get() + method.getDeclaredTypeVariables().size() == 1 + method.getDeclaredTypeVariables()[0].declaringElement.get() == method + method.getDeclaredTypeVariables()[0].variableName == "T" + method.getDeclaredTypeArguments().size() == 1 + def placeholder = method.getDeclaredTypeArguments()["T"] as GenericPlaceholderElement + placeholder.declaringElement.get() == method + placeholder.variableName == "T" + def genericTypeArgument = method.getGenericReturnType().getTypeArguments()["E"] + !genericTypeArgument.isGenericPlaceholder() + !genericTypeArgument.isRawType() + genericTypeArgument.isWildcard() + + def genericWildcard = genericTypeArgument as WildcardElement + !genericWildcard.lowerBounds + genericWildcard.upperBounds.size() == 1 + def genericUpperBound = genericWildcard.upperBounds[0] + genericUpperBound.name == "java.lang.Object" + genericUpperBound.isGenericPlaceholder() + !genericUpperBound.isWildcard() + !genericUpperBound.isRawType() + def genericPlaceholderUpperBound = genericUpperBound as GenericPlaceholderElement + genericPlaceholderUpperBound.variableName == "T" + genericPlaceholderUpperBound.declaringElement.get() == method + + def typeArgument = method.getReturnType().getTypeArguments()["E"] + !typeArgument.isGenericPlaceholder() + !typeArgument.isRawType() + typeArgument.isWildcard() + + def wildcard = genericTypeArgument as WildcardElement + !wildcard.lowerBounds + wildcard.upperBounds.size() == 1 + def upperBound = wildcard.upperBounds[0] + upperBound.name == "java.lang.Object" + upperBound.isGenericPlaceholder() + !upperBound.isWildcard() + !upperBound.isRawType() + def placeholderUpperBound = upperBound as GenericPlaceholderElement + placeholderUpperBound.variableName == "T" + placeholderUpperBound.declaringElement.get() == method + } + +// void "test generics model for constructor placeholder wildcard"() { +// ClassElement ce = buildClassElement('test.Test', ''' +//package test; +//import java.util.List; +// +//class Test { +// +// Test(List list) { +// } +//} +//''') +// expect: +// def method = ce.getPrimaryConstructor().get() +// method.getDeclaredTypeVariables().size() == 1 +// method.getDeclaredTypeVariables()[0].declaringElement.get() == method +// method.getDeclaredTypeVariables()[0].variableName == "T" +// method.getDeclaredTypeArguments().size() == 1 +// def placeholder = method.getDeclaredTypeArguments()["T"] as GenericPlaceholderElement +// placeholder.declaringElement.get() == method +// placeholder.variableName == "T" +// def genericTypeArgument = method.getParameters()[0].getGenericType().getTypeArguments()["E"] +// !genericTypeArgument.isGenericPlaceholder() +// !genericTypeArgument.isRawType() +// genericTypeArgument.isWildcard() +// +// def genericWildcard = genericTypeArgument as WildcardElement +// !genericWildcard.lowerBounds +// genericWildcard.upperBounds.size() == 1 +// def genericUpperBound = genericWildcard.upperBounds[0] +// genericUpperBound.name == "java.lang.Object" +// genericUpperBound.isGenericPlaceholder() +// !genericUpperBound.isWildcard() +// !genericUpperBound.isRawType() +// def genericPlaceholderUpperBound = genericUpperBound as GenericPlaceholderElement +// genericPlaceholderUpperBound.variableName == "T" +// genericPlaceholderUpperBound.declaringElement.get() == method +// +// def typeArgument = method.getParameters()[0].getType().getTypeArguments()["E"] +// !typeArgument.isGenericPlaceholder() +// !typeArgument.isRawType() +// typeArgument.isWildcard() +// +// def wildcard = genericTypeArgument as WildcardElement +// !wildcard.lowerBounds +// wildcard.upperBounds.size() == 1 +// def upperBound = wildcard.upperBounds[0] +// upperBound.name == "java.lang.Object" +// upperBound.isGenericPlaceholder() +// !upperBound.isWildcard() +// !upperBound.isRawType() +// def placeholderUpperBound = upperBound as GenericPlaceholderElement +// placeholderUpperBound.variableName == "T" +// placeholderUpperBound.declaringElement.get() == method +// } + + void "test generics equality"() { + ClassElement ce = buildClassElementTransformed('test.Test', ''' +package test + +import java.util.List + +class Test(list: List) { + + var number: Number? = null + + var obj: Any? = null + + fun method1(): List? { + return null + } + + fun method2(): List? { + return null + } + + fun method3(): T? { + return null + } + + fun method4(): List>? { + return null + } + + fun method5(): List>? { + return null + } + + fun method6(): Test? { + return null + } + + fun method7(): Test<*>? { + return null + } + + fun method8(): Test<*>? { + return null + } + + fun method9(): Test? { + return null + } + + fun method10(): Test? { + return null + } +} +''') { + + it.getPrimaryConstructor().get().getParameters().each { + it.getGenericType().getAllTypeArguments().values().forEach { m -> m.values().forEach {it.getAllTypeArguments()}} + it.getType().getAllTypeArguments().values().forEach { m -> m.values().forEach {it.getAllTypeArguments()}} + } + it.getMethods().forEach { + it.getReturnType().getAllTypeArguments().values().forEach { m -> m.values().forEach {it.getAllTypeArguments()}} + it.getGenericReturnType().getAllTypeArguments().values().forEach { m -> m.values().forEach {it.getAllTypeArguments()}} + } + it + } + expect: + def numberType = ce.getFields()[0].getType() + + def constructor = ce.getPrimaryConstructor().get() + constructor.getParameters()[0].getGenericType().getTypeArguments(List).get("E") == numberType + constructor.getParameters()[0].getType().getTypeArguments(List).get("E") == numberType + + ce.findMethod("method1").get().getGenericReturnType().getTypeArguments(List).get("E") == numberType + ce.findMethod("method1").get().getReturnType().getTypeArguments(List).get("E") == numberType + + ce.findMethod("method2").get().getGenericReturnType().getTypeArguments(List).get("E") == numberType + ce.findMethod("method2").get().getReturnType().getTypeArguments(List).get("E") == numberType + + ce.findMethod("method3").get().getGenericReturnType() == numberType + ce.findMethod("method3").get().getReturnType() == numberType + + ce.findMethod("method4").get().getGenericReturnType().getTypeArguments(List).get("E").getTypeArguments(List).get("E") == numberType + ce.findMethod("method4").get().getReturnType().getTypeArguments(List).get("E").getTypeArguments(List).get("E") == numberType + + ce.findMethod("method5").get().getGenericReturnType().getTypeArguments(List).get("E").getTypeArguments(List).get("E") == numberType + ce.findMethod("method5").get().getReturnType().getTypeArguments(List).get("E").getTypeArguments(List).get("E") == numberType + + ce.findMethod("method6").get().getGenericReturnType().getTypeArguments("test.Test").get("T") == numberType + ce.findMethod("method6").get().getReturnType().getTypeArguments("test.Test").get("T") == numberType + + ce.findMethod("method7").get().getGenericReturnType().getTypeArguments("test.Test").get("T") == numberType + ce.findMethod("method7").get().getReturnType().getTypeArguments("test.Test").get("T") == numberType + + ce.findMethod("method8").get().getGenericReturnType().getTypeArguments("test.Test").get("T") == numberType + ce.findMethod("method8").get().getReturnType().getTypeArguments("test.Test").get("T") == numberType + + ce.findMethod("method9").get().getGenericReturnType().getTypeArguments("test.Test").get("T") == numberType + ce.findMethod("method9").get().getReturnType().getTypeArguments("test.Test").get("T") == numberType + + ce.findMethod("method10").get().getGenericReturnType().getTypeArguments("test.Test").get("T") == numberType + ce.findMethod("method10").get().getReturnType().getTypeArguments("test.Test").get("T") == numberType + } + + void "test inherit parameter annotation"() { + ClassElement ce = buildClassElement('test.UserController', ''' +package test + +import io.micronaut.kotlin.processing.inject.ast.MyParameter + +interface MyApi { + fun get(@MyParameter("X-username") username: String): String +} + +class UserController : MyApi { + override fun get(username: String): String { + return "Hello" + } +} + +''') + expect: + ce.findMethod("get").get().getParameters()[0].hasAnnotation(MyParameter) + } + + void "test interface placeholder"() { + ClassElement ce = buildClassElement('test.MyRepo', ''' +package test +import io.micronaut.context.annotation.Prototype +import java.util.List + +class MyRepo : Repo +interface Repo : GenericRepository +interface GenericRepository +@Prototype +class MyBean { + var name: String? = null +} + +''') + + when: + def repo = ce.getTypeArguments("test.Repo") + then: + repo.get("E").simpleName == "MyBean" + repo.get("E").getSyntheticBeanProperties().size() == 1 + repo.get("E").getMethods().size() == 0 + repo.get("E").getFields().size() == 1 + repo.get("E").getFields().get(0).name == "name" + when: + def genRepo = ce.getTypeArguments("test.GenericRepository") + then: + genRepo.get("E").simpleName == "MyBean" + genRepo.get("E").getSyntheticBeanProperties().size() == 1 + genRepo.get("E").getMethods().size() == 0 + genRepo.get("E").getFields().get(0).name == "name" + } + + void "test internal methods"() { + ClassElement ce = buildClassElement('test.Test', ''' +package test +import io.micronaut.context.annotation.Executable +import io.micronaut.context.annotation.Prototype +import jakarta.inject.Singleton +import java.util.List + +@Prototype +class Test { + @Executable + internal fun helloWorld() { + } +} + +''') + + expect: + ce.findMethod("helloWorld\$main").isPresent() + } + + + private void assertListGenericArgument(ClassElement type, Closure cl) { + def arg1 = type.getAllTypeArguments().get(List.class.name).get("E") + def arg2 = type.getAllTypeArguments().get(Collection.class.name).get("E") + def arg3 = type.getAllTypeArguments().get(Iterable.class.name).get("T") + cl.call(arg1) + cl.call(arg2) + cl.call(arg3) + } + + private void initializeAllTypeArguments(ClassElement type) { + initializeAllTypeArguments0(type, 0) + } + + private void initializeAllTypeArguments0(ClassElement type, int level) { + if (level == 4) { + return + } + type.getAnnotationNames() + initializeAllTypeArguments0(type.getType(), level + 1) + initializeAllTypeArguments0(type.getGenericType(), level + 1) + type.getAllTypeArguments().entrySet().forEach { e1 -> + e1.value.entrySet().forEach { e2 -> + initializeAllTypeArguments0(e2.value, level + 1) + } + } + } + + private void assertMethodsByName(List allMethods, String name, List expectedDeclaringTypeSimpleNames) { + Collection methods = collectElements(allMethods, name) + assert expectedDeclaringTypeSimpleNames.size() == methods.size() + for (String expectedDeclaringTypeSimpleName : expectedDeclaringTypeSimpleNames) { + assert oneElementPresentWithDeclaringType(methods, expectedDeclaringTypeSimpleName) + } + } + + private void assertFieldsByName(List allFields, String name, List expectedDeclaringTypeSimpleNames) { + Collection fields = collectElements(allFields, name) + assert expectedDeclaringTypeSimpleNames.size() == fields.size() + for (String expectedDeclaringTypeSimpleName : expectedDeclaringTypeSimpleNames) { + assert oneElementPresentWithDeclaringType(fields, expectedDeclaringTypeSimpleName) + } + } + + private boolean oneElementPresentWithDeclaringType(Collection elements, String declaringTypeSimpleName) { + elements.stream() + .filter { it -> it.getDeclaringType().getSimpleName() == declaringTypeSimpleName } + .count() == 1 + } + + static Collection collectElements(List allElements, String name) { + return allElements.findAll { it.name == name } + } } diff --git a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/inject/ast/MyBook.java b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/inject/ast/MyBook.java new file mode 100644 index 00000000000..73bf7ad231a --- /dev/null +++ b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/inject/ast/MyBook.java @@ -0,0 +1,8 @@ +package io.micronaut.kotlin.processing.inject.ast; + +import io.micronaut.core.annotation.Introspected; + +@MyEntity +@Introspected +public class MyBook { +} diff --git a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/inject/ast/MyEntity.java b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/inject/ast/MyEntity.java new file mode 100644 index 00000000000..f51d3f82dc1 --- /dev/null +++ b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/inject/ast/MyEntity.java @@ -0,0 +1,15 @@ +package io.micronaut.kotlin.processing.inject.ast; + +import io.micronaut.core.annotation.Internal; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +@Retention(RUNTIME) +@Target({ElementType.METHOD, ElementType.TYPE, ElementType.FIELD, ElementType.PARAMETER}) +@Internal +public @interface MyEntity { +} diff --git a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/inject/ast/MyParameter.java b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/inject/ast/MyParameter.java new file mode 100644 index 00000000000..a5747753929 --- /dev/null +++ b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/inject/ast/MyParameter.java @@ -0,0 +1,20 @@ +package io.micronaut.kotlin.processing.inject.ast; + +import jakarta.inject.Qualifier; + +import java.lang.annotation.Documented; +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; + +@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.ANNOTATION_TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Qualifier +@Inherited +public @interface MyParameter { + + String value() default ""; +} diff --git a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/inject/ast/TypeFieldRuntimeAnn.java b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/inject/ast/TypeFieldRuntimeAnn.java new file mode 100644 index 00000000000..2d63482e3ae --- /dev/null +++ b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/inject/ast/TypeFieldRuntimeAnn.java @@ -0,0 +1,11 @@ +package io.micronaut.kotlin.processing.inject.ast; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.FIELD) +@Retention(RetentionPolicy.RUNTIME) +public @interface TypeFieldRuntimeAnn { +} diff --git a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/inject/ast/TypeMethodRuntimeAnn.java b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/inject/ast/TypeMethodRuntimeAnn.java new file mode 100644 index 00000000000..328c52b2a12 --- /dev/null +++ b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/inject/ast/TypeMethodRuntimeAnn.java @@ -0,0 +1,11 @@ +package io.micronaut.kotlin.processing.inject.ast; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface TypeMethodRuntimeAnn { +} diff --git a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/inject/ast/TypeParameterRuntimeAnn.java b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/inject/ast/TypeParameterRuntimeAnn.java new file mode 100644 index 00000000000..49f0f3f3c00 --- /dev/null +++ b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/inject/ast/TypeParameterRuntimeAnn.java @@ -0,0 +1,11 @@ +package io.micronaut.kotlin.processing.inject.ast; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +public @interface TypeParameterRuntimeAnn { +} diff --git a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/inject/ast/TypeUseClassAnn.java b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/inject/ast/TypeUseClassAnn.java new file mode 100644 index 00000000000..aa117359694 --- /dev/null +++ b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/inject/ast/TypeUseClassAnn.java @@ -0,0 +1,11 @@ +package io.micronaut.kotlin.processing.inject.ast; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.TYPE_USE) +@Retention(RetentionPolicy.CLASS) +public @interface TypeUseClassAnn { +} diff --git a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/inject/ast/TypeUseRuntimeAnn.java b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/inject/ast/TypeUseRuntimeAnn.java new file mode 100644 index 00000000000..c79a1703143 --- /dev/null +++ b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/inject/ast/TypeUseRuntimeAnn.java @@ -0,0 +1,11 @@ +package io.micronaut.kotlin.processing.inject.ast; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) +@Retention(RetentionPolicy.RUNTIME) +public @interface TypeUseRuntimeAnn { +} diff --git a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/inject/configproperties/ConfigPropertiesParseSpec.groovy b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/inject/configproperties/ConfigPropertiesParseSpec.groovy index e95fcdb655d..05e13123360 100644 --- a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/inject/configproperties/ConfigPropertiesParseSpec.groovy +++ b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/inject/configproperties/ConfigPropertiesParseSpec.groovy @@ -1,1134 +1,1136 @@ -package io.micronaut.kotlin.processing.inject.configproperties - -import io.micronaut.annotation.processing.test.KotlinCompiler -import io.micronaut.context.ApplicationContext -import io.micronaut.context.BeanContext -import io.micronaut.context.annotation.ConfigurationReader -import io.micronaut.context.annotation.Property -import io.micronaut.core.convert.format.ReadableBytes -import io.micronaut.inject.BeanDefinition -import io.micronaut.inject.InstantiatableBeanDefinition -import io.micronaut.inject.MethodInjectionPoint -import io.micronaut.kotlin.processing.inject.configuration.Engine -import spock.lang.Specification - -import static io.micronaut.annotation.processing.test.KotlinCompiler.* - -class ConfigPropertiesParseSpec extends Specification { - - void "test default as a property name"() { - given: - - def config = ['foo.bar.host': 'test', 'foo.bar.default': true] - def context = buildContext(''' -package test - -import io.micronaut.context.annotation.* - -@ConfigurationProperties("foo.bar") -data class DefaultConfigTest(val host : String, val default: Boolean ) -''', false, config) - - def bean = getBean(context, 'test.DefaultConfigTest') - - expect: - bean.host == 'test' - bean.default - - cleanup: - context.close() - } - - void "test data classes that are configuration properties inject values"() { - given: - - def config = ['foo.bar.host': 'test', 'foo.bar.baz.stuff': "good"] - def context = buildContext(''' -package test - -import io.micronaut.context.annotation.* - -@ConfigurationProperties("foo.bar") -data class DataConfigTest(val host : String, val child: ChildConfig ) { - @ConfigurationProperties("baz") - data class ChildConfig(var stuff: String) -} -''', false, config) - - def bean = getBean(context, 'test.DataConfigTest') - - expect: - bean.host == 'test' - bean.child.stuff == 'good' - - cleanup: - context.close() - } - - void "test inner class paths - pojo inheritance"() { - when: - BeanDefinition beanDefinition = buildBeanDefinition('test.MyConfig$ChildConfig', ''' -package test - -import io.micronaut.context.annotation.* -import java.time.Duration - -@ConfigurationProperties("foo.bar") -class MyConfig { - var host: String? = null - - @ConfigurationProperties("baz") - open class ChildConfig: ParentConfig() { - protected var stuff: String? = null - } -} - -open class ParentConfig { - var foo: String? = null -} -''') - then: - beanDefinition.synthesize(ConfigurationReader).prefix() == 'foo.bar.baz' - beanDefinition.injectedMethods.size() == 2 - - def setStuff = beanDefinition.injectedMethods.find { it.name == 'setStuff'} - setStuff.getAnnotationMetadata().hasAnnotation(Property) - setStuff.getAnnotationMetadata().synthesize(Property).name() == 'foo.bar.baz.stuff' - setStuff.name == 'setStuff' - - - def setFooMethod = beanDefinition.injectedMethods.find { it.name == 'setFoo'} - setFooMethod.getAnnotationMetadata().hasAnnotation(Property) - setFooMethod.getAnnotationMetadata().synthesize(Property).name() == 'foo.bar.baz.foo' - setFooMethod.name == 'setFoo' - } - - void "test inner class paths - fields"() { - when: - BeanDefinition beanDefinition = buildBeanDefinition('test.MyConfig$ChildConfig', ''' -package test - -import io.micronaut.context.annotation.* - -@ConfigurationProperties("foo.bar") -class MyConfig { - - var host: String? = null - - @ConfigurationProperties("baz") - open class ChildConfig { - protected var stuff: String? = null - } -} -''') - then: - beanDefinition.synthesize(ConfigurationReader).prefix() == 'foo.bar.baz' - beanDefinition.injectedFields.size() == 0 - beanDefinition.injectedMethods.size() == 1 - beanDefinition.injectedMethods[0].getAnnotationMetadata().hasAnnotation(Property) - beanDefinition.injectedMethods[0].getAnnotationMetadata().synthesize(Property).name() == 'foo.bar.baz.stuff' - beanDefinition.injectedMethods[0].name == 'setStuff' - } - - void "test inner class paths - one level"() { - when: - BeanDefinition beanDefinition = buildBeanDefinition('test.MyConfig$ChildConfig', ''' -package test - -import io.micronaut.context.annotation.* - -@ConfigurationProperties("foo.bar") -class MyConfig { - var host: String? = null - - @ConfigurationProperties("baz") - class ChildConfig { - var stuff: String? = null - } -} -''') - then: - beanDefinition.injectedFields.size() == 0 - beanDefinition.injectedMethods.size() == 1 - beanDefinition.injectedMethods[0].getAnnotationMetadata().hasAnnotation(Property) - beanDefinition.injectedMethods[0].getAnnotationMetadata().synthesize(Property).name() == 'foo.bar.baz.stuff' - beanDefinition.injectedMethods[0].name == 'setStuff' - } - - - void "test inner class paths - two levels"() { - when: - BeanDefinition beanDefinition = buildBeanDefinition('test.MyConfig$ChildConfig$MoreConfig', ''' -package test - -import io.micronaut.context.annotation.* - -@ConfigurationProperties("foo.bar") -class MyConfig { - var host: String? = null - - @ConfigurationProperties("baz") - class ChildConfig { - var stuff: String? = null - - @ConfigurationProperties("more") - class MoreConfig { - var stuff: String? = null - } - } -} -''') - then: - beanDefinition.injectedFields.size() == 0 - beanDefinition.injectedMethods.size() == 1 - beanDefinition.injectedMethods[0].getAnnotationMetadata().hasAnnotation(Property) - beanDefinition.injectedMethods[0].getAnnotationMetadata().synthesize(Property).name() == 'foo.bar.baz.more.stuff' - beanDefinition.injectedMethods[0].name == 'setStuff' - } - - void "test inner class paths - with parent inheritance"() { - when: - BeanDefinition beanDefinition = buildBeanDefinition('test.MyConfig$ChildConfig', ''' -package test - -import io.micronaut.context.annotation.* - -@ConfigurationProperties("foo.bar") -class MyConfig: ParentConfig() { - var host: String? = null - - @ConfigurationProperties("baz") - class ChildConfig { - var stuff: String? = null - } -} - -@ConfigurationProperties("parent") -open class ParentConfig -''') - then: - beanDefinition.injectedFields.size() == 0 - beanDefinition.injectedMethods.size() == 1 - beanDefinition.injectedMethods[0].getAnnotationMetadata().hasAnnotation(Property) - beanDefinition.injectedMethods[0].getAnnotationMetadata().synthesize(Property).name() == 'parent.foo.bar.baz.stuff' - beanDefinition.injectedMethods[0].name == 'setStuff' - } - - void "test setters with two arguments are not injected"() { - when: - BeanDefinition beanDefinition = buildBeanDefinition('test.MyConfig', ''' -package test - -import io.micronaut.context.annotation.* - -@ConfigurationProperties("foo.bar") -class MyConfig { - - private var host: String = "localhost" - - fun getHost() = host - - fun setHost(host: String, port: Int) { - this.host = host - } -} -''') - then: - beanDefinition.injectedFields.size() == 0 - beanDefinition.injectedMethods.size() == 0 - } - - void "test setters with two arguments from abstract parent are not injected"() { - when: - BeanDefinition beanDefinition = buildBeanDefinition('test.ChildConfig', ''' -package test - -import io.micronaut.context.annotation.* - -abstract class MyConfig { - private var host: String = "localhost" - - fun getHost() = host - - fun setHost(host: String, port: Int) { - this.host = host - } -} - -@ConfigurationProperties("baz") -class ChildConfig: MyConfig() { - var stuff: String? = null -} -''') - then: - beanDefinition.injectedFields.size() == 0 - beanDefinition.injectedMethods.size() == 1 - beanDefinition.injectedMethods[0].name == 'setStuff' - } - - void "test inheritance with setters"() { - when: - BeanDefinition beanDefinition = buildBeanDefinition('test.ChildConfig', ''' -package test - -import io.micronaut.context.annotation.* - -@ConfigurationProperties("foo.bar") -open class MyConfig { - protected var port: Int = 0 - var host: String? = null -} - -@ConfigurationProperties("baz") -class ChildConfig: MyConfig() { - var stuff: String? = null -} -''') - then: - beanDefinition.injectedFields.size() == 0 - beanDefinition.injectedMethods.size() == 3 - - def stuffMethod = beanDefinition.injectedMethods.find { it.name == 'setStuff'} - stuffMethod.name == 'setStuff' - stuffMethod.getAnnotationMetadata().hasAnnotation(Property) - stuffMethod.getAnnotationMetadata().synthesize(Property).name() == 'foo.bar.baz.stuff' - - def setPortMethod = beanDefinition.injectedMethods.find { it.name == 'setPort'} - setPortMethod.name == 'setPort' - setPortMethod.getAnnotationMetadata().synthesize(Property).name() == 'foo.bar.port' - - def setHostMethod = beanDefinition.injectedMethods.find { it.name == 'setHost'} - setHostMethod.getAnnotationMetadata().hasAnnotation(Property) - setHostMethod.getAnnotationMetadata().synthesize(Property).name() == 'foo.bar.host' - setHostMethod.name == 'setHost' - - } - - void "test annotation on property"() { - when: - BeanDefinition beanDefinition = buildBeanDefinition('test.HttpClientConfiguration', ''' -package test - -import io.micronaut.core.convert.format.* -import io.micronaut.context.annotation.* - -@ConfigurationProperties("http.client") -class HttpClientConfiguration { - @ReadableBytes - var maxContentLength: Int = 1024 * 1024 * 10 // 10MB -} -''') - then: - beanDefinition.injectedFields.size() == 0 - beanDefinition.injectedMethods.size() == 1 - beanDefinition.injectedMethods[0].arguments[0].synthesize(ReadableBytes) - } - - void "test different inject types for config properties"() { - when: - BeanDefinition beanDefinition = buildBeanDefinition('test.MyProperties', ''' -package test - -import io.micronaut.context.annotation.* - -@ConfigurationProperties("foo") -open class MyProperties { - protected var fieldTest: String = "unconfigured" - private val privateFinal = true - protected val protectedFinal = true - private var anotherField: Boolean = false - private var internalField = "unconfigured" - - fun setSetterTest(s: String) { - this.internalField = s - } - - fun getSetter() = internalField -} -''') - then: - beanDefinition.injectedMethods.size() == 2 - beanDefinition.injectedMethods.find { it.name == 'setFieldTest' } - beanDefinition.injectedMethods.find {it.name == 'setSetterTest' } - - when: - InstantiatableBeanDefinition factory = beanDefinition - ApplicationContext applicationContext = ApplicationContext.builder().start() - def bean = factory.instantiate(applicationContext) - - then: - bean != null - bean.setter == "unconfigured" - bean.@fieldTest == "unconfigured" - - when: - applicationContext.environment.addPropertySource( - "test", - ['foo.setterTest' :'foo', - 'foo.fieldTest' :'bar'] - ) - bean = factory.instantiate(applicationContext) - - then: - bean != null - bean.setter == "foo" - bean.@fieldTest == "bar" - } - - void "test configuration properties inheritance from non-configuration properties"() { - when: - BeanDefinition beanDefinition = buildBeanDefinition('test.MyProperties', ''' -package test - -import io.micronaut.context.annotation.* - -@ConfigurationProperties("foo") -open class MyProperties: Parent() { - protected var fieldTest: String = "unconfigured" - private val privateFinal = true - protected val protectedFinal = true - private var anotherField: Boolean = false - private var internalField = "unconfigured" - - fun setSetterTest(s: String) { - this.internalField = s - } - - fun getSetter() = internalField -} - -open class Parent { - private var parentField: String? = null - - fun setParentTest(s: String) { - this.parentField = s - } - - fun getParentTest() = parentField -} -''') - then: - beanDefinition.injectedMethods.size() == 3 - - def fieldTest = beanDefinition.injectedMethods.find { it.name == 'setFieldTest'} - fieldTest.getAnnotationMetadata().hasAnnotation(Property) - fieldTest.getAnnotationMetadata().synthesize(Property).name() == 'foo.field-test' - fieldTest.name == 'setFieldTest' - - def setterTest = beanDefinition.injectedMethods.find { it.name == 'setSetterTest'} - setterTest.getAnnotationMetadata().hasAnnotation(Property) - setterTest.getAnnotationMetadata().synthesize(Property).name() == 'foo.setter-test' - setterTest.name == 'setSetterTest' - - def parentTest = beanDefinition.injectedMethods.find { it.name == 'setParentTest'} - parentTest.name == 'setParentTest' - parentTest.getAnnotationMetadata().hasAnnotation(Property) - parentTest.getAnnotationMetadata().synthesize(Property).name() == 'foo.parent-test' - - - when: - InstantiatableBeanDefinition factory = beanDefinition - ApplicationContext applicationContext = ApplicationContext.builder().start() - def bean = factory.instantiate(applicationContext) - - then: - bean != null - bean.setter == "unconfigured" - bean.@fieldTest == "unconfigured" - - when: - applicationContext.environment.addPropertySource( - "test", - ['foo.setterTest' :'foo', - 'foo.fieldTest' :'bar'] - ) - bean = factory.instantiate(applicationContext) - - then: - bean != null - bean.setter == "foo" - bean.@fieldTest == "bar" - } - - void "test boolean fields starting with is[A-Z] map to set methods"() { - when: - BeanDefinition beanDefinition = buildBeanDefinition("micronaut.issuer.FooConfigurationProperties", """ -package micronaut.issuer - -import io.micronaut.context.annotation.ConfigurationProperties - -@ConfigurationProperties("foo") -class FooConfigurationProperties { - - private var issuer: String? = null - private var isEnabled = false - - fun setIssuer(issuer: String) { - this.issuer = issuer - } - - //isEnabled field maps to setEnabled method - fun setEnabled(enabled: Boolean) { - this.isEnabled = enabled - } -} -""") - then: - noExceptionThrown() - beanDefinition.injectedMethods[0].name == "setIssuer" - beanDefinition.injectedMethods[1].name == "setEnabled" - } - - void "test includes on fields"() { - when: - BeanDefinition beanDefinition = buildBeanDefinition('test.MyProperties', ''' -package test - -import io.micronaut.context.annotation.* - -@ConfigurationProperties(value = "foo", includes = ["publicField", "parentPublicField"]) -class MyProperties: Parent() { - var publicField: String? = null - var anotherPublicField: String? = null -} - -open class Parent { - var parentPublicField: String? = null - var anotherParentPublicField: String? = null -} -''') - then: - noExceptionThrown() - beanDefinition.injectedMethods.size() == 2 - beanDefinition.injectedMethods.find { it.name == "setPublicField" } - beanDefinition.injectedMethods.find { it.name == "setParentPublicField" } - } - - void "test includes on methods"() { - when: - BeanDefinition beanDefinition = buildBeanDefinition('test.MyProperties', ''' -package test - -import io.micronaut.context.annotation.* - -@ConfigurationProperties(value = "foo", includes = ["publicMethod", "parentPublicMethod"]) -class MyProperties: Parent() { - - fun setPublicMethod(value: String) {} - fun setAnotherPublicMethod(value: String) {} -} - -open class Parent { - fun setParentPublicMethod(value: String) {} - fun setAnotherParentPublicMethod(value: String) {} -} -''') - then: - noExceptionThrown() - beanDefinition.injectedMethods.size() == 2 - beanDefinition.injectedMethods.find { it.name == "setParentPublicMethod" } - beanDefinition.injectedMethods.find { it.name == "setPublicMethod" } - } - - void "test excludes on fields"() { - when: - BeanDefinition beanDefinition = buildBeanDefinition('test.MyProperties', ''' -package test - -import io.micronaut.context.annotation.* - -@ConfigurationProperties(value = "foo", excludes = ["anotherPublicField", "anotherParentPublicField"]) -class MyProperties: Parent() { - var publicField: String? = null - var anotherPublicField: String? = null -} - -open class Parent { - var parentPublicField: String? = null - var anotherParentPublicField: String? = null -} -''') - then: - noExceptionThrown() - beanDefinition.injectedMethods.size() == 2 - beanDefinition.injectedMethods.find { it.name == "setParentPublicField" } - beanDefinition.injectedMethods.find { it.name == "setPublicField" } - } - - void "test excludes on methods"() { - when: - BeanDefinition beanDefinition = buildBeanDefinition('test.MyProperties', ''' -package test - -import io.micronaut.context.annotation.* - -@ConfigurationProperties(value = "foo", excludes = ["anotherPublicMethod", "anotherParentPublicMethod"]) -class MyProperties: Parent() { - - fun setPublicMethod(value: String) {} - fun setAnotherPublicMethod(value: String) {} -} - -open class Parent { - fun setParentPublicMethod(value: String) {} - fun setAnotherParentPublicMethod(value: String) {} -} -''') - then: - noExceptionThrown() - beanDefinition.injectedMethods.size() == 2 - beanDefinition.injectedMethods.find { it.name == "setParentPublicMethod" } - beanDefinition.injectedMethods.find { it.name == "setPublicMethod" } - } - - void "test excludes on configuration builder"() { - when: - BeanDefinition beanDefinition = buildBeanDefinition('test.MyProperties', ''' -package test - -import io.micronaut.context.annotation.* -import io.micronaut.kotlin.processing.inject.configuration.Engine - -@ConfigurationProperties(value = "foo", excludes = ["engine", "engine2"]) -class MyProperties: Parent() { - - @ConfigurationBuilder(prefixes = ["with"]) - val engine: Engine.Builder = Engine.builder() - - @ConfigurationBuilder(configurationPrefix = "two", prefixes = ["with"]) - var engine2: Engine.Builder = Engine.builder() -} - -open class Parent { - fun setEngine(engine: Engine.Builder) {} -} -''') - then: - noExceptionThrown() - beanDefinition.injectedMethods.isEmpty() - beanDefinition.injectedFields.isEmpty() - - when: - InstantiatableBeanDefinition factory = beanDefinition - ApplicationContext applicationContext = ApplicationContext.run( - 'foo.manufacturer':'Subaru', - 'foo.two.manufacturer':'Subaru' - ) - def bean = factory.instantiate(applicationContext) - - then: - ((Engine.Builder) bean.engine).build().manufacturer == 'Subaru' - ((Engine.Builder) bean.getEngine2()).build().manufacturer == 'Subaru' - } - - void "test name is correct with inner classes of non config props class"() { - when: - BeanDefinition beanDefinition = buildBeanDefinition("test.Test\$TestNestedConfig", ''' -package test - -import io.micronaut.context.annotation.* - -class Test { - - @ConfigurationProperties("test") - class TestNestedConfig { - var vall: String? = null - } -} -''') - - then: - noExceptionThrown() - beanDefinition.injectedMethods[0].annotationMetadata.getAnnotationValuesByType(Property.class).get(0).stringValue("name").get() == "test.vall" - } - - void "test property names with numbers"() { - when: - BeanDefinition beanDefinition = buildBeanDefinition('test.AwsConfig', ''' -package test - -import io.micronaut.context.annotation.ConfigurationProperties - -@ConfigurationProperties("aws") -class AwsConfig { - - var disableEc2Metadata: String? = null - var disableEcMetadata: String? = null - var disableEc2instanceMetadata: String? = null -} -''') - - then: - noExceptionThrown() - beanDefinition.injectedMethods[0].getAnnotationMetadata().getAnnotationValuesByType(Property.class).get(0).stringValue("name").get() == "aws.disable-ec2-metadata" - beanDefinition.injectedMethods[1].getAnnotationMetadata().getAnnotationValuesByType(Property.class).get(0).stringValue("name").get() == "aws.disable-ec-metadata" - beanDefinition.injectedMethods[2].getAnnotationMetadata().getAnnotationValuesByType(Property.class).get(0).stringValue("name").get() == "aws.disable-ec2instance-metadata" - } - - void "test inner interface EachProperty list = true"() { - when: - BeanDefinition beanDefinition = buildBeanDefinition('test.Parent$Child$Intercepted', ''' -package test - -import io.micronaut.context.annotation.ConfigurationProperties -import io.micronaut.context.annotation.EachProperty - -import jakarta.inject.Inject - -@ConfigurationProperties("parent") -class Parent @Inject constructor(val children: List) { - - @EachProperty(value = "children", list = true) - interface Child { - fun getPropA(): String - fun getPropB(): String - } -} -''') - - then: - noExceptionThrown() - beanDefinition != null - beanDefinition.getAnnotationMetadata().stringValue(ConfigurationReader.class, "prefix").get() == "parent.children[*]" - beanDefinition.getRequiredMethod("getPropA").getAnnotationMetadata().getAnnotationValuesByType(Property.class).get(0).stringValue("name").get() == "parent.children[*].prop-a" - } - - void "test config props with post construct first in file"() { - given: - BeanContext context = buildContext(""" -package test - -import io.micronaut.context.annotation.ConfigurationProperties -import jakarta.annotation.PostConstruct - -@ConfigurationProperties("app.entity") -class EntityProperties { - - @PostConstruct - fun init() { - println("prop = " + prop) - } - - var prop: String? = null -} -""") - - when: - context.getBean(context.classLoader.loadClass("test.EntityProperties")) - - then: - noExceptionThrown() - } - - void "test inner class paths - two levels"() { - when: - BeanDefinition beanDefinition = buildBeanDefinition('test.MyConfig$ChildConfig$MoreConfig', ''' -package test - -import io.micronaut.context.annotation.ConfigurationProperties - -@ConfigurationProperties("foo.bar") -class MyConfig { - var host: String? = null - - @ConfigurationProperties("baz") - class ChildConfig { - var stuff: String? = null - - @ConfigurationProperties("more") - class MoreConfig { - var stuff: String? = null - } - } -} -''') - then: - beanDefinition.injectedFields.size() == 0 - beanDefinition.injectedMethods.size() == 1 - beanDefinition.injectedMethods[0].getAnnotationMetadata().hasAnnotation(Property) - beanDefinition.injectedMethods[0].getAnnotationMetadata().synthesize(Property).name() == 'foo.bar.baz.more.stuff' - beanDefinition.injectedMethods[0].name == 'setStuff' - } - - void "test inner class paths - with parent inheritance"() { - when: - BeanDefinition beanDefinition = buildBeanDefinition('test.MyConfig$ChildConfig', ''' -package test - -import io.micronaut.context.annotation.ConfigurationProperties - -@ConfigurationProperties("foo.bar") -class MyConfig: ParentConfig() { - var host: String? = null - - @ConfigurationProperties("baz") - class ChildConfig { - var stuff: String? = null - } -} - -@ConfigurationProperties("parent") -open class ParentConfig -''') - then: - beanDefinition.injectedFields.size() == 0 - beanDefinition.injectedMethods.size() == 1 - beanDefinition.injectedMethods[0].getAnnotationMetadata().hasAnnotation(Property) - beanDefinition.injectedMethods[0].getAnnotationMetadata().synthesize(Property).name() == 'parent.foo.bar.baz.stuff' - beanDefinition.injectedMethods[0].name == 'setStuff' - } - - void "test annotation on setters arguments"() { - when: - BeanDefinition beanDefinition = buildBeanDefinition('test.HttpClientConfiguration', ''' -package test - -import io.micronaut.core.convert.format.ReadableBytes -import io.micronaut.context.annotation.ConfigurationProperties - -@ConfigurationProperties("http.client") -class HttpClientConfiguration { - - @ReadableBytes - var maxContentLength: Int = 1024 * 1024 * 10 - -} -''') - then: - beanDefinition.injectedFields.size() == 0 - beanDefinition.injectedMethods.size() == 1 - beanDefinition.injectedMethods[0].arguments[0].synthesize(ReadableBytes) - } - - void "test different inject types for config properties"() { - when: - BeanDefinition beanDefinition = buildBeanDefinition('test.MyProperties', ''' -package test - -import io.micronaut.context.annotation.ConfigurationProperties - -@ConfigurationProperties("foo") -open class MyProperties { - open var fieldTest: String = "unconfigured" - private val privateFinal = true - open val protectedFinal = true - private val anotherField = false - private var internalField = "unconfigured" - - fun setSetterTest(s: String) { - this.internalField = s - } - - fun getSetter() = internalField -} -''') - then: - beanDefinition != null - beanDefinition.injectedMethods.size() == 2 - beanDefinition.injectedMethods.find { it.name == 'setFieldTest' } - beanDefinition.injectedMethods.find { it.name == 'setSetterTest' } - - when: - InstantiatableBeanDefinition factory = beanDefinition - ApplicationContext applicationContext = ApplicationContext.builder().start() - def bean = factory.instantiate(applicationContext) - - then: - bean != null - bean.setter == "unconfigured" - bean.@fieldTest == "unconfigured" - - when: - applicationContext.environment.addPropertySource( - "test", - ['foo.setterTest' :'foo', - 'foo.fieldTest' :'bar'] - ) - bean = factory.instantiate(applicationContext) - - then: - bean != null - bean.setter == "foo" - bean.@fieldTest == "bar" - } - - void "test configuration properties inheritance from non-configuration properties"() { - when: - BeanDefinition beanDefinition = buildBeanDefinition('test.MyProperties', ''' -package test - -import io.micronaut.context.annotation.ConfigurationProperties - -@ConfigurationProperties("foo") -class MyProperties: Parent() { - - open var fieldTest: String = "unconfigured" - private val privateFinal = true - open val protectedFinal = true - private val anotherField = false - private var internalField = "unconfigured" - - fun setSetterTest(s: String) { - this.internalField = s - } - - fun getSetter() = internalField -} - -open class Parent { - var parentTest: String?= null -} -''') - then: - beanDefinition.injectedMethods.size() == 3 - - def setFieldMethod = beanDefinition.injectedMethods.find { it.name == 'setFieldTest'} - setFieldMethod.name == 'setFieldTest' - setFieldMethod.getAnnotationMetadata().hasAnnotation(Property) - setFieldMethod.getAnnotationMetadata().synthesize(Property).name() == 'foo.field-test' - - - def setParentMethod = beanDefinition.injectedMethods.find { it.name == 'setParentTest'} - setParentMethod.name == 'setParentTest' - setParentMethod.getAnnotationMetadata().hasAnnotation(Property) - setParentMethod.getAnnotationMetadata().synthesize(Property).name() == 'foo.parent-test' - - - def setSetterTest = beanDefinition.injectedMethods.find { it.name == 'setSetterTest'} - setSetterTest.name == 'setSetterTest' - setSetterTest.getAnnotationMetadata().hasAnnotation(Property) - setSetterTest.getAnnotationMetadata().synthesize(Property).name() == 'foo.setter-test' - - when: - InstantiatableBeanDefinition factory = beanDefinition - ApplicationContext applicationContext = ApplicationContext.builder().start() - def bean = factory.instantiate(applicationContext) - - then: - bean != null - bean.setter == "unconfigured" - bean.@fieldTest == "unconfigured" - bean.parentTest == null - - when: - applicationContext.environment.addPropertySource( - "test", - ['foo.setterTest' :'foo', - 'foo.fieldTest' :'bar', - 'foo.parentTest': 'baz'] - ) - bean = factory.instantiate(applicationContext) - - then: - bean != null - bean.setter == "foo" - bean.@fieldTest == "bar" - bean.parentTest == "baz" - } - - void "test includes on properties"() { - when: - BeanDefinition beanDefinition = buildBeanDefinition('test.MyProperties', ''' -package test - -import io.micronaut.context.annotation.ConfigurationProperties - -@ConfigurationProperties(value = "foo", includes = ["publicField", "parentPublicField"]) -class MyProperties: Parent() { - var publicField: String? = null - var anotherPublicField: String? = null -} - -open class Parent { - var parentPublicField: String? = null - var anotherParentPublicField: String? = null -} -''') - then: - noExceptionThrown() - beanDefinition.injectedMethods.size() == 2 - beanDefinition.injectedMethods.find { it.name == "setPublicField" } - beanDefinition.injectedMethods.find { it.name == "setParentPublicField" } - } - - void "test excludes on properties"() { - when: - BeanDefinition beanDefinition = buildBeanDefinition('test.MyProperties', ''' -package test - -import io.micronaut.context.annotation.ConfigurationProperties - -@ConfigurationProperties(value = "foo", excludes = ["anotherPublicField", "anotherParentPublicField"]) -class MyProperties: Parent() { - var publicField: String? = null - var anotherPublicField: String? = null -} - -open class Parent { - var parentPublicField: String? = null - var anotherParentPublicField: String? = null -} -''') - then: - noExceptionThrown() - beanDefinition.injectedMethods.size() == 2 - beanDefinition.injectedMethods.find { it.name == "setPublicField" } - beanDefinition.injectedMethods.find { it.name == "setParentPublicField" } - } - - void "test name is correct with inner classes of non config props class"() { - when: - BeanDefinition beanDefinition = buildBeanDefinition("test.Test\$TestNestedConfig", ''' -package test - -import io.micronaut.context.annotation.ConfigurationProperties - -class Test { - - @ConfigurationProperties("test") - class TestNestedConfig { - var x: String? = null - } - -} -''') - - then: - noExceptionThrown() - beanDefinition.injectedMethods[0].annotationMetadata.getAnnotationValuesByType(Property.class).get(0).stringValue("name").get() == "test.x" - } - - void "test property names with numbers"() { - when: - BeanDefinition beanDefinition = buildBeanDefinition('test.AwsConfig', ''' -package test - -import io.micronaut.context.annotation.ConfigurationProperties - -@ConfigurationProperties("aws") -class AwsConfig { - - var disableEc2Metadata: String? = null - var disableEcMetadata: String? = null - var disableEc2instanceMetadata: String? = null -} -''') - - then: - noExceptionThrown() - beanDefinition.injectedMethods[0].getAnnotationMetadata().getAnnotationValuesByType(Property.class).get(0).stringValue("name").get() == "aws.disable-ec2-metadata" - beanDefinition.injectedMethods[1].getAnnotationMetadata().getAnnotationValuesByType(Property.class).get(0).stringValue("name").get() == "aws.disable-ec-metadata" - beanDefinition.injectedMethods[2].getAnnotationMetadata().getAnnotationValuesByType(Property.class).get(0).stringValue("name").get() == "aws.disable-ec2instance-metadata" - } - - void "test inner class EachProperty list = true"() { - when: - BeanDefinition beanDefinition = buildBeanDefinition('test.Parent$Child', ''' -package test - -import io.micronaut.context.annotation.ConfigurationProperties -import io.micronaut.context.annotation.EachProperty - -import jakarta.inject.Inject - -@ConfigurationProperties("parent") -class Parent(val children: List) { - - @EachProperty(value = "children", list = true) - class Child { - var propA: String? = null - var propB: String? = null - } -} -''') - - then: - noExceptionThrown() - beanDefinition != null - beanDefinition.getAnnotationMetadata().stringValue(ConfigurationReader.class, "prefix").get() == "parent.children[*]" - beanDefinition.injectedMethods[0].getAnnotationMetadata().getAnnotationValuesByType(Property.class).get(0).stringValue("name").get() == "parent.children[*].prop-a" - } - - void "test config props with post construct first in file"() { - given: - BeanContext context = buildContext(""" -package test - -import io.micronaut.context.annotation.ConfigurationProperties -import jakarta.annotation.PostConstruct - -@ConfigurationProperties("app.entity") -class EntityProperties { - - @PostConstruct - fun init() { - println("prop = \$prop") - } - - var prop: String? = null -} -""") - - when: - getBean(context, "test.EntityProperties") - - then: - noExceptionThrown() - } - - void "test configuration properties inheriting config props class"() { - when: - BeanDefinition beanDefinition = buildBeanDefinition('test.MyProperties', ''' -package test - -import io.micronaut.context.annotation.ConfigurationProperties - -@ConfigurationProperties(value = "child") -class MyProperties: Parent() { - var childProp: String? = null -} - -@ConfigurationProperties(value = "parent") -open class Parent { - var prop: String? = null -} -''') - then: - noExceptionThrown() - beanDefinition.injectedMethods.size() == 2 - - def setChildProp = beanDefinition.injectedMethods.find { it.name == 'setChildProp'} - setChildProp.name == "setChildProp" - setChildProp.annotationMetadata.stringValue(Property, "name").get() == "parent.child.child-prop" - - def setProp = beanDefinition.injectedMethods.find { it.name == 'setProp'} - setProp.name == "setProp" - setProp.annotationMetadata.stringValue(Property, "name").get() == "parent.prop" - } - - void "test inner each bean internal constructor"() { - when: - BeanDefinition beanDefinition = buildBeanDefinition("test.ParentEachPropsCtor\$ManagerProps", """ -package test - -import io.micronaut.context.annotation.* - -@EachProperty("teams") -class ParentEachPropsCtor internal constructor( - @Parameter val name: String, - val manager: ManagerProps? -) { - var wins: Int? = null - - @ConfigurationProperties("manager") - class ManagerProps internal constructor(@Parameter val name: String) { - var age: Int? = null - } -} -""") - - then: - noExceptionThrown() - beanDefinition != null - } -} +//package io.micronaut.kotlin.processing.inject.configproperties +// +// +//import io.micronaut.context.ApplicationContext +//import io.micronaut.context.BeanContext +//import io.micronaut.context.annotation.ConfigurationReader +//import io.micronaut.context.annotation.Property +//import io.micronaut.core.convert.format.ReadableBytes +//import io.micronaut.inject.BeanDefinition +//import io.micronaut.inject.InstantiatableBeanDefinition +//import io.micronaut.kotlin.processing.inject.configuration.Engine +//import spock.lang.Ignore +//import spock.lang.Specification +// +//import static io.micronaut.annotation.processing.test.KotlinCompiler.buildBeanDefinition +//import static io.micronaut.annotation.processing.test.KotlinCompiler.buildContext +//import static io.micronaut.annotation.processing.test.KotlinCompiler.getBean +// +//class ConfigPropertiesParseSpec extends Specification { +// +// void "test default as a property name"() { +// given: +// +// def config = ['foo.bar.host': 'test', 'foo.bar.default': true] +// def context = buildContext(''' +//package test +// +//import io.micronaut.context.annotation.* +// +//@ConfigurationProperties("foo.bar") +//data class DefaultConfigTest(val host : String, val default: Boolean ) +//''', false, config) +// +// def bean = getBean(context, 'test.DefaultConfigTest') +// +// expect: +// bean.host == 'test' +// bean.default +// +// cleanup: +// context.close() +// } +// +// void "test data classes that are configuration properties inject values"() { +// given: +// +// def config = ['foo.bar.host': 'test', 'foo.bar.baz.stuff': "good"] +// def context = buildContext(''' +//package test +// +//import io.micronaut.context.annotation.* +// +//@ConfigurationProperties("foo.bar") +//data class DataConfigTest(val host : String, val child: ChildConfig ) { +// @ConfigurationProperties("baz") +// data class ChildConfig(var stuff: String) +//} +//''', false, config) +// +// def bean = getBean(context, 'test.DataConfigTest') +// +// expect: +// bean.host == 'test' +// bean.child.stuff == 'good' +// +// cleanup: +// context.close() +// } +// +// void "test inner class paths - pojo inheritance"() { +// when: +// BeanDefinition beanDefinition = buildBeanDefinition('test.MyConfig$ChildConfig', ''' +//package test +// +//import io.micronaut.context.annotation.* +//import java.time.Duration +// +//@ConfigurationProperties("foo.bar") +//class MyConfig { +// var host: String? = null +// +// @ConfigurationProperties("baz") +// open class ChildConfig: ParentConfig() { +// protected var stuff: String? = null +// } +//} +// +//open class ParentConfig { +// var foo: String? = null +//} +//''') +// then: +// beanDefinition.synthesize(ConfigurationReader).prefix() == 'foo.bar.baz' +// beanDefinition.injectedMethods.size() == 2 +// +// def setStuff = beanDefinition.injectedMethods.find { it.name == 'setStuff'} +// setStuff.getAnnotationMetadata().hasAnnotation(Property) +// setStuff.getAnnotationMetadata().synthesize(Property).name() == 'foo.bar.baz.stuff' +// setStuff.name == 'setStuff' +// +// +// def setFooMethod = beanDefinition.injectedMethods.find { it.name == 'setFoo'} +// setFooMethod.getAnnotationMetadata().hasAnnotation(Property) +// setFooMethod.getAnnotationMetadata().synthesize(Property).name() == 'foo.bar.baz.foo' +// setFooMethod.name == 'setFoo' +// } +// +// void "test inner class paths - fields"() { +// when: +// BeanDefinition beanDefinition = buildBeanDefinition('test.MyConfig$ChildConfig', ''' +//package test +// +//import io.micronaut.context.annotation.* +// +//@ConfigurationProperties("foo.bar") +//class MyConfig { +// +// var host: String? = null +// +// @ConfigurationProperties("baz") +// open class ChildConfig { +// protected var stuff: String? = null +// } +//} +//''') +// then: +// beanDefinition.synthesize(ConfigurationReader).prefix() == 'foo.bar.baz' +// beanDefinition.injectedFields.size() == 0 +// beanDefinition.injectedMethods.size() == 1 +// beanDefinition.injectedMethods[0].getAnnotationMetadata().hasAnnotation(Property) +// beanDefinition.injectedMethods[0].getAnnotationMetadata().synthesize(Property).name() == 'foo.bar.baz.stuff' +// beanDefinition.injectedMethods[0].name == 'setStuff' +// } +// +// void "test inner class paths - one level"() { +// when: +// BeanDefinition beanDefinition = buildBeanDefinition('test.MyConfig$ChildConfig', ''' +//package test +// +//import io.micronaut.context.annotation.* +// +//@ConfigurationProperties("foo.bar") +//class MyConfig { +// var host: String? = null +// +// @ConfigurationProperties("baz") +// class ChildConfig { +// var stuff: String? = null +// } +//} +//''') +// then: +// beanDefinition.injectedFields.size() == 0 +// beanDefinition.injectedMethods.size() == 1 +// beanDefinition.injectedMethods[0].getAnnotationMetadata().hasAnnotation(Property) +// beanDefinition.injectedMethods[0].getAnnotationMetadata().synthesize(Property).name() == 'foo.bar.baz.stuff' +// beanDefinition.injectedMethods[0].name == 'setStuff' +// } +// +// +// void "test inner class paths - two levels"() { +// when: +// BeanDefinition beanDefinition = buildBeanDefinition('test.MyConfig$ChildConfig$MoreConfig', ''' +//package test +// +//import io.micronaut.context.annotation.* +// +//@ConfigurationProperties("foo.bar") +//class MyConfig { +// var host: String? = null +// +// @ConfigurationProperties("baz") +// class ChildConfig { +// var stuff: String? = null +// +// @ConfigurationProperties("more") +// class MoreConfig { +// var stuff: String? = null +// } +// } +//} +//''') +// then: +// beanDefinition.injectedFields.size() == 0 +// beanDefinition.injectedMethods.size() == 1 +// beanDefinition.injectedMethods[0].getAnnotationMetadata().hasAnnotation(Property) +// beanDefinition.injectedMethods[0].getAnnotationMetadata().synthesize(Property).name() == 'foo.bar.baz.more.stuff' +// beanDefinition.injectedMethods[0].name == 'setStuff' +// } +// +// void "test inner class paths - with parent inheritance"() { +// when: +// BeanDefinition beanDefinition = buildBeanDefinition('test.MyConfig$ChildConfig', ''' +//package test +// +//import io.micronaut.context.annotation.* +// +//@ConfigurationProperties("foo.bar") +//class MyConfig: ParentConfig() { +// var host: String? = null +// +// @ConfigurationProperties("baz") +// class ChildConfig { +// var stuff: String? = null +// } +//} +// +//@ConfigurationProperties("parent") +//open class ParentConfig +//''') +// then: +// beanDefinition.injectedFields.size() == 0 +// beanDefinition.injectedMethods.size() == 1 +// beanDefinition.injectedMethods[0].getAnnotationMetadata().hasAnnotation(Property) +// beanDefinition.injectedMethods[0].getAnnotationMetadata().synthesize(Property).name() == 'parent.foo.bar.baz.stuff' +// beanDefinition.injectedMethods[0].name == 'setStuff' +// } +// +// void "test setters with two arguments are not injected"() { +// when: +// BeanDefinition beanDefinition = buildBeanDefinition('test.MyConfig', ''' +//package test +// +//import io.micronaut.context.annotation.* +// +//@ConfigurationProperties("foo.bar") +//class MyConfig { +// +// private var host: String = "localhost" +// +// fun getHost() = host +// +// fun setHost(host: String, port: Int) { +// this.host = host +// } +//} +//''') +// then: +// beanDefinition.injectedFields.size() == 0 +// beanDefinition.injectedMethods.size() == 0 +// } +// +// void "test setters with two arguments from abstract parent are not injected"() { +// when: +// BeanDefinition beanDefinition = buildBeanDefinition('test.ChildConfig', ''' +//package test +// +//import io.micronaut.context.annotation.* +// +//abstract class MyConfig { +// private var host: String = "localhost" +// +// fun getHost() = host +// +// fun setHost(host: String, port: Int) { +// this.host = host +// } +//} +// +//@ConfigurationProperties("baz") +//class ChildConfig: MyConfig() { +// var stuff: String? = null +//} +//''') +// then: +// beanDefinition.injectedFields.size() == 0 +// beanDefinition.injectedMethods.size() == 1 +// beanDefinition.injectedMethods[0].name == 'setStuff' +// } +// +// void "test inheritance with setters"() { +// when: +// BeanDefinition beanDefinition = buildBeanDefinition('test.ChildConfig', ''' +//package test +// +//import io.micronaut.context.annotation.* +// +//@ConfigurationProperties("foo.bar") +//open class MyConfig { +// protected var port: Int = 0 +// var host: String? = null +//} +// +//@ConfigurationProperties("baz") +//class ChildConfig: MyConfig() { +// var stuff: String? = null +//} +//''') +// then: +// beanDefinition.injectedFields.size() == 0 +// beanDefinition.injectedMethods.size() == 3 +// +// def stuffMethod = beanDefinition.injectedMethods.find { it.name == 'setStuff'} +// stuffMethod.name == 'setStuff' +// stuffMethod.getAnnotationMetadata().hasAnnotation(Property) +// stuffMethod.getAnnotationMetadata().synthesize(Property).name() == 'foo.bar.baz.stuff' +// +// def setPortMethod = beanDefinition.injectedMethods.find { it.name == 'setPort'} +// setPortMethod.name == 'setPort' +// setPortMethod.getAnnotationMetadata().synthesize(Property).name() == 'foo.bar.port' +// +// def setHostMethod = beanDefinition.injectedMethods.find { it.name == 'setHost'} +// setHostMethod.getAnnotationMetadata().hasAnnotation(Property) +// setHostMethod.getAnnotationMetadata().synthesize(Property).name() == 'foo.bar.host' +// setHostMethod.name == 'setHost' +// +// } +// +// void "test annotation on property"() { +// when: +// BeanDefinition beanDefinition = buildBeanDefinition('test.HttpClientConfiguration', ''' +//package test +// +//import io.micronaut.core.convert.format.* +//import io.micronaut.context.annotation.* +// +//@ConfigurationProperties("http.client") +//class HttpClientConfiguration { +// @ReadableBytes +// var maxContentLength: Int = 1024 * 1024 * 10 // 10MB +//} +//''') +// then: +// beanDefinition.injectedFields.size() == 0 +// beanDefinition.injectedMethods.size() == 1 +// beanDefinition.injectedMethods[0].arguments[0].synthesize(ReadableBytes) +// } +// +// void "test different inject types for config properties"() { +// when: +// BeanDefinition beanDefinition = buildBeanDefinition('test.MyProperties', ''' +//package test +// +//import io.micronaut.context.annotation.* +// +//@ConfigurationProperties("foo") +//open class MyProperties { +// protected var fieldTest: String = "unconfigured" +// private val privateFinal = true +// protected val protectedFinal = true +// private var anotherField: Boolean = false +// private var internalField = "unconfigured" +// +// fun setSetterTest(s: String) { +// this.internalField = s +// } +// +// fun getSetter() = internalField +//} +//''') +// then: +// beanDefinition.injectedMethods.size() == 2 +// beanDefinition.injectedMethods.find { it.name == 'setFieldTest' } +// beanDefinition.injectedMethods.find {it.name == 'setSetterTest' } +// +// when: +// InstantiatableBeanDefinition factory = beanDefinition +// ApplicationContext applicationContext = ApplicationContext.builder().start() +// def bean = factory.instantiate(applicationContext) +// +// then: +// bean != null +// bean.setter == "unconfigured" +// bean.@fieldTest == "unconfigured" +// +// when: +// applicationContext.environment.addPropertySource( +// "test", +// ['foo.setterTest' :'foo', +// 'foo.fieldTest' :'bar'] +// ) +// bean = factory.instantiate(applicationContext) +// +// then: +// bean != null +// bean.setter == "foo" +// bean.@fieldTest == "bar" +// } +// +// void "test configuration properties inheritance from non-configuration properties"() { +// when: +// BeanDefinition beanDefinition = buildBeanDefinition('test.MyProperties', ''' +//package test +// +//import io.micronaut.context.annotation.* +// +//@ConfigurationProperties("foo") +//open class MyProperties: Parent() { +// protected var fieldTest: String = "unconfigured" +// private val privateFinal = true +// protected val protectedFinal = true +// private var anotherField: Boolean = false +// private var internalField = "unconfigured" +// +// fun setSetterTest(s: String) { +// this.internalField = s +// } +// +// fun getSetter() = internalField +//} +// +//open class Parent { +// private var parentField: String? = null +// +// fun setParentTest(s: String) { +// this.parentField = s +// } +// +// fun getParentTest() = parentField +//} +//''') +// then: +// beanDefinition.injectedMethods.size() == 3 +// +// def fieldTest = beanDefinition.injectedMethods.find { it.name == 'setFieldTest'} +// fieldTest.getAnnotationMetadata().hasAnnotation(Property) +// fieldTest.getAnnotationMetadata().synthesize(Property).name() == 'foo.field-test' +// fieldTest.name == 'setFieldTest' +// +// def setterTest = beanDefinition.injectedMethods.find { it.name == 'setSetterTest'} +// setterTest.getAnnotationMetadata().hasAnnotation(Property) +// setterTest.getAnnotationMetadata().synthesize(Property).name() == 'foo.setter-test' +// setterTest.name == 'setSetterTest' +// +// def parentTest = beanDefinition.injectedMethods.find { it.name == 'setParentTest'} +// parentTest.name == 'setParentTest' +// parentTest.getAnnotationMetadata().hasAnnotation(Property) +// parentTest.getAnnotationMetadata().synthesize(Property).name() == 'foo.parent-test' +// +// +// when: +// InstantiatableBeanDefinition factory = beanDefinition +// ApplicationContext applicationContext = ApplicationContext.builder().start() +// def bean = factory.instantiate(applicationContext) +// +// then: +// bean != null +// bean.setter == "unconfigured" +// bean.@fieldTest == "unconfigured" +// +// when: +// applicationContext.environment.addPropertySource( +// "test", +// ['foo.setterTest' :'foo', +// 'foo.fieldTest' :'bar'] +// ) +// bean = factory.instantiate(applicationContext) +// +// then: +// bean != null +// bean.setter == "foo" +// bean.@fieldTest == "bar" +// } +// +// void "test boolean fields starting with is[A-Z] map to set methods"() { +// when: +// BeanDefinition beanDefinition = buildBeanDefinition("micronaut.issuer.FooConfigurationProperties", """ +//package micronaut.issuer +// +//import io.micronaut.context.annotation.ConfigurationProperties +// +//@ConfigurationProperties("foo") +//class FooConfigurationProperties { +// +// private var issuer: String? = null +// private var isEnabled = false +// +// fun setIssuer(issuer: String) { +// this.issuer = issuer +// } +// +// //isEnabled field maps to setEnabled method +// fun setEnabled(enabled: Boolean) { +// this.isEnabled = enabled +// } +//} +//""") +// then: +// noExceptionThrown() +// beanDefinition.injectedMethods[0].name == "setIssuer" +// beanDefinition.injectedMethods[1].name == "setEnabled" +// } +// +// void "test includes on fields"() { +// when: +// BeanDefinition beanDefinition = buildBeanDefinition('test.MyProperties', ''' +//package test +// +//import io.micronaut.context.annotation.* +// +//@ConfigurationProperties(value = "foo", includes = ["publicField", "parentPublicField"]) +//class MyProperties: Parent() { +// var publicField: String? = null +// var anotherPublicField: String? = null +//} +// +//open class Parent { +// var parentPublicField: String? = null +// var anotherParentPublicField: String? = null +//} +//''') +// then: +// noExceptionThrown() +// beanDefinition.injectedMethods.size() == 2 +// beanDefinition.injectedMethods.find { it.name == "setPublicField" } +// beanDefinition.injectedMethods.find { it.name == "setParentPublicField" } +// } +// +// void "test includes on methods"() { +// when: +// BeanDefinition beanDefinition = buildBeanDefinition('test.MyProperties', ''' +//package test +// +//import io.micronaut.context.annotation.* +// +//@ConfigurationProperties(value = "foo", includes = ["publicMethod", "parentPublicMethod"]) +//class MyProperties: Parent() { +// +// fun setPublicMethod(value: String) {} +// fun setAnotherPublicMethod(value: String) {} +//} +// +//open class Parent { +// fun setParentPublicMethod(value: String) {} +// fun setAnotherParentPublicMethod(value: String) {} +//} +//''') +// then: +// noExceptionThrown() +// beanDefinition.injectedMethods.size() == 2 +// beanDefinition.injectedMethods.find { it.name == "setParentPublicMethod" } +// beanDefinition.injectedMethods.find { it.name == "setPublicMethod" } +// } +// +// void "test excludes on fields"() { +// when: +// BeanDefinition beanDefinition = buildBeanDefinition('test.MyProperties', ''' +//package test +// +//import io.micronaut.context.annotation.* +// +//@ConfigurationProperties(value = "foo", excludes = ["anotherPublicField", "anotherParentPublicField"]) +//class MyProperties: Parent() { +// var publicField: String? = null +// var anotherPublicField: String? = null +//} +// +//open class Parent { +// var parentPublicField: String? = null +// var anotherParentPublicField: String? = null +//} +//''') +// then: +// noExceptionThrown() +// beanDefinition.injectedMethods.size() == 2 +// beanDefinition.injectedMethods.find { it.name == "setParentPublicField" } +// beanDefinition.injectedMethods.find { it.name == "setPublicField" } +// } +// +// void "test excludes on methods"() { +// when: +// BeanDefinition beanDefinition = buildBeanDefinition('test.MyProperties', ''' +//package test +// +//import io.micronaut.context.annotation.* +// +//@ConfigurationProperties(value = "foo", excludes = ["anotherPublicMethod", "anotherParentPublicMethod"]) +//class MyProperties: Parent() { +// +// fun setPublicMethod(value: String) {} +// fun setAnotherPublicMethod(value: String) {} +//} +// +//open class Parent { +// fun setParentPublicMethod(value: String) {} +// fun setAnotherParentPublicMethod(value: String) {} +//} +//''') +// then: +// noExceptionThrown() +// beanDefinition.injectedMethods.size() == 2 +// beanDefinition.injectedMethods.find { it.name == "setParentPublicMethod" } +// beanDefinition.injectedMethods.find { it.name == "setPublicMethod" } +// } +// +// void "test excludes on configuration builder"() { +// when: +// BeanDefinition beanDefinition = buildBeanDefinition('test.MyProperties', ''' +//package test +// +//import io.micronaut.context.annotation.* +//import io.micronaut.kotlin.processing.inject.configuration.Engine +// +//@ConfigurationProperties(value = "foo", excludes = ["engine", "engine2"]) +//class MyProperties: Parent() { +// +// @ConfigurationBuilder(prefixes = ["with"]) +// val engine: Engine.Builder = Engine.builder() +// +// @ConfigurationBuilder(configurationPrefix = "two", prefixes = ["with"]) +// var engine2: Engine.Builder = Engine.builder() +//} +// +//open class Parent { +// fun setEngine(engine: Engine.Builder) {} +//} +//''') +// then: +// noExceptionThrown() +// beanDefinition.injectedMethods.isEmpty() +// beanDefinition.injectedFields.isEmpty() +// +// when: +// InstantiatableBeanDefinition factory = beanDefinition +// ApplicationContext applicationContext = ApplicationContext.run( +// 'foo.manufacturer':'Subaru', +// 'foo.two.manufacturer':'Subaru' +// ) +// def bean = factory.instantiate(applicationContext) +// +// then: +// ((Engine.Builder) bean.engine).build().manufacturer == 'Subaru' +// ((Engine.Builder) bean.getEngine2()).build().manufacturer == 'Subaru' +// } +// +// void "test name is correct with inner classes of non config props class"() { +// when: +// BeanDefinition beanDefinition = buildBeanDefinition("test.Test\$TestNestedConfig", ''' +//package test +// +//import io.micronaut.context.annotation.* +// +//class Test { +// +// @ConfigurationProperties("test") +// class TestNestedConfig { +// var vall: String? = null +// } +//} +//''') +// +// then: +// noExceptionThrown() +// beanDefinition.injectedMethods[0].annotationMetadata.getAnnotationValuesByType(Property.class).get(0).stringValue("name").get() == "test.vall" +// } +// +// void "test property names with numbers"() { +// when: +// BeanDefinition beanDefinition = buildBeanDefinition('test.AwsConfig', ''' +//package test +// +//import io.micronaut.context.annotation.ConfigurationProperties +// +//@ConfigurationProperties("aws") +//class AwsConfig { +// +// var disableEc2Metadata: String? = null +// var disableEcMetadata: String? = null +// var disableEc2instanceMetadata: String? = null +//} +//''') +// +// then: +// noExceptionThrown() +// beanDefinition.injectedMethods[0].getAnnotationMetadata().getAnnotationValuesByType(Property.class).get(0).stringValue("name").get() == "aws.disable-ec2-metadata" +// beanDefinition.injectedMethods[1].getAnnotationMetadata().getAnnotationValuesByType(Property.class).get(0).stringValue("name").get() == "aws.disable-ec-metadata" +// beanDefinition.injectedMethods[2].getAnnotationMetadata().getAnnotationValuesByType(Property.class).get(0).stringValue("name").get() == "aws.disable-ec2instance-metadata" +// } +// +// void "test inner interface EachProperty list = true"() { +// when: +// BeanDefinition beanDefinition = buildBeanDefinition('test.Parent$Child$Intercepted', ''' +//package test +// +//import io.micronaut.context.annotation.ConfigurationProperties +//import io.micronaut.context.annotation.EachProperty +// +//import jakarta.inject.Inject +// +//@ConfigurationProperties("parent") +//class Parent @Inject constructor(val children: List) { +// +// @EachProperty(value = "children", list = true) +// interface Child { +// fun getPropA(): String +// fun getPropB(): String +// } +//} +//''') +// +// then: +// noExceptionThrown() +// beanDefinition != null +// beanDefinition.getAnnotationMetadata().stringValue(ConfigurationReader.class, "prefix").get() == "parent.children[*]" +// beanDefinition.getRequiredMethod("getPropA").getAnnotationMetadata().getAnnotationValuesByType(Property.class).get(0).stringValue("name").get() == "parent.children[*].prop-a" +// } +// +// void "test config props with post construct first in file"() { +// given: +// BeanContext context = buildContext(""" +//package test +// +//import io.micronaut.context.annotation.ConfigurationProperties +//import jakarta.annotation.PostConstruct +// +//@ConfigurationProperties("app.entity") +//class EntityProperties { +// +// @PostConstruct +// fun init() { +// println("prop = " + prop) +// } +// +// var prop: String? = null +//} +//""") +// +// when: +// context.getBean(context.classLoader.loadClass("test.EntityProperties")) +// +// then: +// noExceptionThrown() +// } +// +// void "test inner class paths - two levels"() { +// when: +// BeanDefinition beanDefinition = buildBeanDefinition('test.MyConfig$ChildConfig$MoreConfig', ''' +//package test +// +//import io.micronaut.context.annotation.ConfigurationProperties +// +//@ConfigurationProperties("foo.bar") +//class MyConfig { +// var host: String? = null +// +// @ConfigurationProperties("baz") +// class ChildConfig { +// var stuff: String? = null +// +// @ConfigurationProperties("more") +// class MoreConfig { +// var stuff: String? = null +// } +// } +//} +//''') +// then: +// beanDefinition.injectedFields.size() == 0 +// beanDefinition.injectedMethods.size() == 1 +// beanDefinition.injectedMethods[0].getAnnotationMetadata().hasAnnotation(Property) +// beanDefinition.injectedMethods[0].getAnnotationMetadata().synthesize(Property).name() == 'foo.bar.baz.more.stuff' +// beanDefinition.injectedMethods[0].name == 'setStuff' +// } +// +// void "test inner class paths - with parent inheritance"() { +// when: +// BeanDefinition beanDefinition = buildBeanDefinition('test.MyConfig$ChildConfig', ''' +//package test +// +//import io.micronaut.context.annotation.ConfigurationProperties +// +//@ConfigurationProperties("foo.bar") +//class MyConfig: ParentConfig() { +// var host: String? = null +// +// @ConfigurationProperties("baz") +// class ChildConfig { +// var stuff: String? = null +// } +//} +// +//@ConfigurationProperties("parent") +//open class ParentConfig +//''') +// then: +// beanDefinition.injectedFields.size() == 0 +// beanDefinition.injectedMethods.size() == 1 +// beanDefinition.injectedMethods[0].getAnnotationMetadata().hasAnnotation(Property) +// beanDefinition.injectedMethods[0].getAnnotationMetadata().synthesize(Property).name() == 'parent.foo.bar.baz.stuff' +// beanDefinition.injectedMethods[0].name == 'setStuff' +// } +// +// void "test annotation on setters arguments"() { +// when: +// BeanDefinition beanDefinition = buildBeanDefinition('test.HttpClientConfiguration', ''' +//package test +// +//import io.micronaut.core.convert.format.ReadableBytes +//import io.micronaut.context.annotation.ConfigurationProperties +// +//@ConfigurationProperties("http.client") +//class HttpClientConfiguration { +// +// @ReadableBytes +// var maxContentLength: Int = 1024 * 1024 * 10 +// +//} +//''') +// then: +// beanDefinition.injectedFields.size() == 0 +// beanDefinition.injectedMethods.size() == 1 +// beanDefinition.injectedMethods[0].arguments[0].synthesize(ReadableBytes) +// } +// +// void "test different inject types for config properties"() { +// when: +// BeanDefinition beanDefinition = buildBeanDefinition('test.MyProperties', ''' +//package test +// +//import io.micronaut.context.annotation.ConfigurationProperties +// +//@ConfigurationProperties("foo") +//open class MyProperties { +// open var fieldTest: String = "unconfigured" +// private val privateFinal = true +// open val protectedFinal = true +// private val anotherField = false +// private var internalField = "unconfigured" +// +// fun setSetterTest(s: String) { +// this.internalField = s +// } +// +// fun getSetter() = internalField +//} +//''') +// then: +// beanDefinition != null +// beanDefinition.injectedMethods.size() == 2 +// beanDefinition.injectedMethods.find { it.name == 'setFieldTest' } +// beanDefinition.injectedMethods.find { it.name == 'setSetterTest' } +// +// when: +// InstantiatableBeanDefinition factory = beanDefinition +// ApplicationContext applicationContext = ApplicationContext.builder().start() +// def bean = factory.instantiate(applicationContext) +// +// then: +// bean != null +// bean.setter == "unconfigured" +// bean.@fieldTest == "unconfigured" +// +// when: +// applicationContext.environment.addPropertySource( +// "test", +// ['foo.setterTest' :'foo', +// 'foo.fieldTest' :'bar'] +// ) +// bean = factory.instantiate(applicationContext) +// +// then: +// bean != null +// bean.setter == "foo" +// bean.@fieldTest == "bar" +// } +// +// void "test configuration properties inheritance from non-configuration properties"() { +// when: +// BeanDefinition beanDefinition = buildBeanDefinition('test.MyProperties', ''' +//package test +// +//import io.micronaut.context.annotation.ConfigurationProperties +// +//@ConfigurationProperties("foo") +//class MyProperties: Parent() { +// +// open var fieldTest: String = "unconfigured" +// private val privateFinal = true +// open val protectedFinal = true +// private val anotherField = false +// private var internalField = "unconfigured" +// +// fun setSetterTest(s: String) { +// this.internalField = s +// } +// +// fun getSetter() = internalField +//} +// +//open class Parent { +// var parentTest: String?= null +//} +//''') +// then: +// beanDefinition.injectedMethods.size() == 3 +// +// def setFieldMethod = beanDefinition.injectedMethods.find { it.name == 'setFieldTest'} +// setFieldMethod.name == 'setFieldTest' +// setFieldMethod.getAnnotationMetadata().hasAnnotation(Property) +// setFieldMethod.getAnnotationMetadata().synthesize(Property).name() == 'foo.field-test' +// +// +// def setParentMethod = beanDefinition.injectedMethods.find { it.name == 'setParentTest'} +// setParentMethod.name == 'setParentTest' +// setParentMethod.getAnnotationMetadata().hasAnnotation(Property) +// setParentMethod.getAnnotationMetadata().synthesize(Property).name() == 'foo.parent-test' +// +// +// def setSetterTest = beanDefinition.injectedMethods.find { it.name == 'setSetterTest'} +// setSetterTest.name == 'setSetterTest' +// setSetterTest.getAnnotationMetadata().hasAnnotation(Property) +// setSetterTest.getAnnotationMetadata().synthesize(Property).name() == 'foo.setter-test' +// +// when: +// InstantiatableBeanDefinition factory = beanDefinition +// ApplicationContext applicationContext = ApplicationContext.builder().start() +// def bean = factory.instantiate(applicationContext) +// +// then: +// bean != null +// bean.setter == "unconfigured" +// bean.@fieldTest == "unconfigured" +// bean.parentTest == null +// +// when: +// applicationContext.environment.addPropertySource( +// "test", +// ['foo.setterTest' :'foo', +// 'foo.fieldTest' :'bar', +// 'foo.parentTest': 'baz'] +// ) +// bean = factory.instantiate(applicationContext) +// +// then: +// bean != null +// bean.setter == "foo" +// bean.@fieldTest == "bar" +// bean.parentTest == "baz" +// } +// +// void "test includes on properties"() { +// when: +// BeanDefinition beanDefinition = buildBeanDefinition('test.MyProperties', ''' +//package test +// +//import io.micronaut.context.annotation.ConfigurationProperties +// +//@ConfigurationProperties(value = "foo", includes = ["publicField", "parentPublicField"]) +//class MyProperties: Parent() { +// var publicField: String? = null +// var anotherPublicField: String? = null +//} +// +//open class Parent { +// var parentPublicField: String? = null +// var anotherParentPublicField: String? = null +//} +//''') +// then: +// noExceptionThrown() +// beanDefinition.injectedMethods.size() == 2 +// beanDefinition.injectedMethods.find { it.name == "setPublicField" } +// beanDefinition.injectedMethods.find { it.name == "setParentPublicField" } +// } +// +// void "test excludes on properties"() { +// when: +// BeanDefinition beanDefinition = buildBeanDefinition('test.MyProperties', ''' +//package test +// +//import io.micronaut.context.annotation.ConfigurationProperties +// +//@ConfigurationProperties(value = "foo", excludes = ["anotherPublicField", "anotherParentPublicField"]) +//class MyProperties: Parent() { +// var publicField: String? = null +// var anotherPublicField: String? = null +//} +// +//open class Parent { +// var parentPublicField: String? = null +// var anotherParentPublicField: String? = null +//} +//''') +// then: +// noExceptionThrown() +// beanDefinition.injectedMethods.size() == 2 +// beanDefinition.injectedMethods.find { it.name == "setPublicField" } +// beanDefinition.injectedMethods.find { it.name == "setParentPublicField" } +// } +// +// void "test name is correct with inner classes of non config props class"() { +// when: +// BeanDefinition beanDefinition = buildBeanDefinition("test.Test\$TestNestedConfig", ''' +//package test +// +//import io.micronaut.context.annotation.ConfigurationProperties +// +//class Test { +// +// @ConfigurationProperties("test") +// class TestNestedConfig { +// var x: String? = null +// } +// +//} +//''') +// +// then: +// noExceptionThrown() +// beanDefinition.injectedMethods[0].annotationMetadata.getAnnotationValuesByType(Property.class).get(0).stringValue("name").get() == "test.x" +// } +// +// void "test property names with numbers"() { +// when: +// BeanDefinition beanDefinition = buildBeanDefinition('test.AwsConfig', ''' +//package test +// +//import io.micronaut.context.annotation.ConfigurationProperties +// +//@ConfigurationProperties("aws") +//class AwsConfig { +// +// var disableEc2Metadata: String? = null +// var disableEcMetadata: String? = null +// var disableEc2instanceMetadata: String? = null +//} +//''') +// +// then: +// noExceptionThrown() +// beanDefinition.injectedMethods[0].getAnnotationMetadata().getAnnotationValuesByType(Property.class).get(0).stringValue("name").get() == "aws.disable-ec2-metadata" +// beanDefinition.injectedMethods[1].getAnnotationMetadata().getAnnotationValuesByType(Property.class).get(0).stringValue("name").get() == "aws.disable-ec-metadata" +// beanDefinition.injectedMethods[2].getAnnotationMetadata().getAnnotationValuesByType(Property.class).get(0).stringValue("name").get() == "aws.disable-ec2instance-metadata" +// } +// +// void "test inner class EachProperty list = true"() { +// when: +// BeanDefinition beanDefinition = buildBeanDefinition('test.Parent$Child', ''' +//package test +// +//import io.micronaut.context.annotation.ConfigurationProperties +//import io.micronaut.context.annotation.EachProperty +// +//import jakarta.inject.Inject +// +//@ConfigurationProperties("parent") +//class Parent(val children: List) { +// +// @EachProperty(value = "children", list = true) +// class Child { +// var propA: String? = null +// var propB: String? = null +// } +//} +//''') +// +// then: +// noExceptionThrown() +// beanDefinition != null +// beanDefinition.getAnnotationMetadata().stringValue(ConfigurationReader.class, "prefix").get() == "parent.children[*]" +// beanDefinition.injectedMethods[0].getAnnotationMetadata().getAnnotationValuesByType(Property.class).get(0).stringValue("name").get() == "parent.children[*].prop-a" +// } +// +// void "test config props with post construct first in file"() { +// given: +// BeanContext context = buildContext(""" +//package test +// +//import io.micronaut.context.annotation.ConfigurationProperties +//import jakarta.annotation.PostConstruct +// +//@ConfigurationProperties("app.entity") +//class EntityProperties { +// +// @PostConstruct +// fun init() { +// println("prop = \$prop") +// } +// +// var prop: String? = null +//} +//""") +// +// when: +// getBean(context, "test.EntityProperties") +// +// then: +// noExceptionThrown() +// } +// +// void "test configuration properties inheriting config props class"() { +// when: +// BeanDefinition beanDefinition = buildBeanDefinition('test.MyProperties', ''' +//package test +// +//import io.micronaut.context.annotation.ConfigurationProperties +// +//@ConfigurationProperties(value = "child") +//class MyProperties: Parent() { +// var childProp: String? = null +//} +// +//@ConfigurationProperties(value = "parent") +//open class Parent { +// var prop: String? = null +//} +//''') +// then: +// noExceptionThrown() +// beanDefinition.injectedMethods.size() == 2 +// +// def setChildProp = beanDefinition.injectedMethods.find { it.name == 'setChildProp'} +// setChildProp.name == "setChildProp" +// setChildProp.annotationMetadata.stringValue(Property, "name").get() == "parent.child.child-prop" +// +// def setProp = beanDefinition.injectedMethods.find { it.name == 'setProp'} +// setProp.name == "setProp" +// setProp.annotationMetadata.stringValue(Property, "name").get() == "parent.prop" +// } +// +// void "test inner each bean internal constructor"() { +// when: +// BeanDefinition beanDefinition = buildBeanDefinition("test.ParentEachPropsCtor\$ManagerProps", """ +//package test +// +//import io.micronaut.context.annotation.* +// +//@EachProperty("teams") +//class ParentEachPropsCtor internal constructor( +// @Parameter val name: String, +// val manager: ManagerProps? +//) { +// var wins: Int? = null +// +// @ConfigurationProperties("manager") +// class ManagerProps internal constructor(@Parameter val name: String) { +// var age: Int? = null +// } +//} +//""") +// +// then: +// noExceptionThrown() +// beanDefinition != null +// } +//} diff --git a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/inject/configproperties/ConfigurationPropertiesBuilderSpec.groovy b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/inject/configproperties/ConfigurationPropertiesBuilderSpec.groovy index b20ef8d800e..2f02283904d 100644 --- a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/inject/configproperties/ConfigurationPropertiesBuilderSpec.groovy +++ b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/inject/configproperties/ConfigurationPropertiesBuilderSpec.groovy @@ -1,780 +1,780 @@ -package io.micronaut.kotlin.processing.inject.configproperties - -import io.micronaut.context.ApplicationContext -import io.micronaut.context.env.PropertySource -import io.micronaut.inject.BeanDefinition -import io.micronaut.inject.InstantiatableBeanDefinition -import org.neo4j.driver.v1.Config -import spock.lang.PendingFeature -import spock.lang.Specification -import static io.micronaut.annotation.processing.test.KotlinCompiler.* - -class ConfigurationPropertiesBuilderSpec extends Specification { - - void "test configuration builder on method"() { - given: - BeanDefinition beanDefinition = buildBeanDefinition('test.MyProperties', ''' -package test; - -import io.micronaut.context.annotation.*; - -@ConfigurationProperties("test") -class MyProperties { - - @ConfigurationBuilder(factoryMethod="build") - var test: Test? = null -} - -class Test private constructor() { - - var foo: String? = null - - companion object { - @JvmStatic - fun build(): Test { - return Test() - } - } -} -''') - - when:"The bean was built and a warning was logged" - InstantiatableBeanDefinition factory = beanDefinition - ApplicationContext applicationContext = ApplicationContext.run( - 'test.foo':'good' - ) - def bean = factory.instantiate(applicationContext) - - then: - bean.test.foo == 'good' - } - - void "test configuration builder with includes"() { - given: - BeanDefinition beanDefinition = buildBeanDefinition('test.MyProperties', ''' -package test; - -import io.micronaut.context.annotation.*; - -@ConfigurationProperties("test") -class MyProperties { - - @ConfigurationBuilder(factoryMethod="build", includes=["foo"]) - var test: Test? = null -} - -class Test private constructor() { - - var foo: String? = null - var bar: String? = null - - companion object { - @JvmStatic - fun build(): Test { - return Test() - } - } -} -''') - - when:"The bean was built and a warning was logged" - InstantiatableBeanDefinition factory = beanDefinition - ApplicationContext applicationContext = ApplicationContext.run( - 'test.foo':'good', - 'test.bar':'bad' - ) - def bean = factory.instantiate(applicationContext) - - then: - bean.test.foo == 'good' - bean.test.bar == null - } - - void "test catch and log NoSuchMethodError for when underlying builder changes"() { - given: - BeanDefinition beanDefinition = buildBeanDefinition('test.MyProperties', ''' -package test - -import io.micronaut.context.annotation.* - -@ConfigurationProperties("test") -class MyProperties { - - @ConfigurationBuilder - var test = Test() -} - -class Test { - fun setFoo(s: String) { - throw NoSuchMethodError("setFoo") - } -} -''') - - expect:"The bean was built and a warning was logged" - InstantiatableBeanDefinition factory = beanDefinition - ApplicationContext applicationContext = ApplicationContext.run( - 'test.foo':'good', - ) - factory.instantiate(applicationContext) - } - - void "test with setters that return void"() { - given: - BeanDefinition beanDefinition = buildBeanDefinition('test.MyProperties', ''' -package test - -import io.micronaut.context.annotation.* - -@ConfigurationProperties("test") -class MyProperties { - - @ConfigurationBuilder - var test = Test() -} - -class Test { - var foo: String? = null - var bar: Int = 0 - @Deprecated("message") - var baz: Long? = null -} -''') - - when: - InstantiatableBeanDefinition factory = beanDefinition - ApplicationContext applicationContext = ApplicationContext.run( - 'test.foo':'good', - 'test.bar': '10', - 'test.baz':'20' - ) - def bean = factory.instantiate(applicationContext) - - then: - bean != null - bean.test != null - - when: - def test = bean.test - - then: - test.foo == 'good' - test.bar == 10 - test.baz == null //deprecated properties not settable - } - - void "test different inject types for config properties"() { - when: - BeanDefinition beanDefinition = buildBeanDefinition('test.Neo4jProperties', ''' -package test - -import io.micronaut.context.annotation.* -import org.neo4j.driver.v1.* - -@ConfigurationProperties("neo4j.test") -class Neo4jProperties { - protected var uri: java.net.URI? = null - - @ConfigurationBuilder( - prefixes=["with"], - allowZeroArgs=true - ) - var options: Config.ConfigBuilder = Config.build() -} -''') - then: - beanDefinition.injectedMethods.size() == 1 - beanDefinition.injectedMethods.first().name == 'setUri' - - when: - InstantiatableBeanDefinition factory = beanDefinition - ApplicationContext applicationContext = ApplicationContext.run( - 'neo4j.test.encryptionLevel':'none', - 'neo4j.test.leakedSessionsLogging':true, - 'neo4j.test.maxIdleSessions':2 - ) - def bean = factory.instantiate(applicationContext) - - then: - bean != null - bean.options != null - - when: - Config config = bean.options.toConfig() - - then: - config.maxIdleConnectionPoolSize() == 2 - config.encrypted() == true // deprecated properties are ignored - config.logLeakedSessions() - } - - void "test specifying a configuration prefix"() { - when: - BeanDefinition beanDefinition = buildBeanDefinition('test.Neo4jProperties', ''' -package test - -import io.micronaut.context.annotation.* -import org.neo4j.driver.v1.* - -@ConfigurationProperties("neo4j.test") -class Neo4jProperties { - protected var uri: java.net.URI? = null - - @ConfigurationBuilder( - prefixes=["with"], - allowZeroArgs=true, - configurationPrefix="options" - ) - var options: Config.ConfigBuilder = Config.build() -} -''') - then: - beanDefinition.injectedMethods.size() == 1 - beanDefinition.injectedMethods.first().name == 'setUri' - - when: - InstantiatableBeanDefinition factory = beanDefinition - ApplicationContext applicationContext = ApplicationContext.run( - 'neo4j.test.options.encryptionLevel':'none', - 'neo4j.test.options.leakedSessionsLogging':true, - 'neo4j.test.options.maxIdleSessions':2 - ) - def bean = factory.instantiate(applicationContext) - - then: - bean != null - bean.options != null - - when: - Config config = bean.options.toConfig() - - then: - config.maxIdleConnectionPoolSize() == 2 - config.encrypted() == true // deprecated properties are ignored - config.logLeakedSessions() - } - - void "test specifying a configuration prefix with value"() { - when: - BeanDefinition beanDefinition = buildBeanDefinition('test.Neo4jProperties', ''' -package test - -import io.micronaut.context.annotation.* -import org.neo4j.driver.v1.* - -@ConfigurationProperties("neo4j.test") -class Neo4jProperties { - protected var uri: java.net.URI? = null - - @ConfigurationBuilder( - prefixes=["with"], - allowZeroArgs=true, - value="options" - ) - var options: Config.ConfigBuilder = Config.build() -} -''') - then: - beanDefinition.injectedMethods.size() == 1 - beanDefinition.injectedMethods.first().name == 'setUri' - - when: - InstantiatableBeanDefinition factory = beanDefinition - ApplicationContext applicationContext = ApplicationContext.run( - 'neo4j.test.options.encryptionLevel':'none', - 'neo4j.test.options.leakedSessionsLogging':true, - 'neo4j.test.options.maxIdleSessions':2 - ) - def bean = factory.instantiate(applicationContext) - - then: - bean != null - bean.options != null - - when: - Config config = bean.options.toConfig() - - then: - config.maxIdleConnectionPoolSize() == 2 - config.encrypted() == true // deprecated properties are ignored - config.logLeakedSessions() - } - - void "test specifying a configuration prefix with value using @AccessorsStyle"() { - when: - BeanDefinition beanDefinition = buildBeanDefinition('test.Neo4jProperties', ''' -package test; - -import io.micronaut.context.annotation.*; -import io.micronaut.core.annotation.AccessorsStyle; -import org.neo4j.driver.v1.*; - -@ConfigurationProperties("neo4j.test") -class Neo4jProperties { - protected var uri: java.net.URI? = null - - @ConfigurationBuilder( - allowZeroArgs = true, - value = "options" - ) - @AccessorsStyle(writePrefixes = ["with"]) - var options: Config.ConfigBuilder = Config.build() -} -''') - then: - beanDefinition.injectedMethods.size() == 1 - beanDefinition.injectedMethods.first().name == 'setUri' - - when: - InstantiatableBeanDefinition factory = beanDefinition - ApplicationContext applicationContext = ApplicationContext.run( - 'neo4j.test.options.encryptionLevel':'none', - 'neo4j.test.options.leakedSessionsLogging':true, - 'neo4j.test.options.maxIdleSessions':2 - ) - def bean = factory.instantiate(applicationContext) - - then: - bean != null - bean.options != null - - when: - Config config = bean.options.toConfig() - - then: - config.maxIdleConnectionPoolSize() == 2 - config.encrypted() == true // deprecated properties are ignored - config.logLeakedSessions() - } - - void "test builder method long and TimeUnit arguments"() { - when: - BeanDefinition beanDefinition = buildBeanDefinition('test.Neo4jProperties', ''' -package test - -import io.micronaut.context.annotation.* -import org.neo4j.driver.v1.* - -@ConfigurationProperties("neo4j.test") -class Neo4jProperties { - protected var uri: java.net.URI? = null - - @ConfigurationBuilder( - prefixes=["with"], - allowZeroArgs=true - ) - var options: Config.ConfigBuilder = Config.build() - -} -''') - then: - beanDefinition.injectedMethods.size() == 1 - beanDefinition.injectedMethods.first().name == 'setUri' - - when: - InstantiatableBeanDefinition factory = beanDefinition - ApplicationContext applicationContext = ApplicationContext.run( - 'neo4j.test.connectionLivenessCheckTimeout': '6s' - ) - def bean = factory.instantiate(applicationContext) - - then: - bean != null - bean.options != null - - when: - Config config = bean.options.toConfig() - - then: - config.idleTimeBeforeConnectionTest() == 6000 - } - - void "test using a builder that is marked final"() { - when: - BeanDefinition beanDefinition = buildBeanDefinition('test.Neo4jProperties', ''' -package test - -import io.micronaut.context.annotation.* -import org.neo4j.driver.v1.* - -@ConfigurationProperties("neo4j.test") -class Neo4jProperties { - - @ConfigurationBuilder( - prefixes=["with"], - allowZeroArgs=true - ) - val options: Config.ConfigBuilder = Config.build() - -} -''') - InstantiatableBeanDefinition factory = beanDefinition - ApplicationContext applicationContext = ApplicationContext.run( - 'neo4j.test.connectionLivenessCheckTimeout': '17s' - ) - def bean = factory.instantiate(applicationContext) - - then: - bean != null - bean.options != null - - when: - Config config = bean.options.toConfig() - - then: - config.idleTimeBeforeConnectionTest() == 17000 - } - - void "test with setter methods that return this"() { - given: - BeanDefinition beanDefinition = buildBeanDefinition('test.MyProperties', ''' -package test - -import io.micronaut.context.annotation.* - -@ConfigurationProperties("test") -class MyProperties { - - @ConfigurationBuilder(factoryMethod="build") - var test: Test? = null -} - -class Test private constructor() { - - private var foo: String? = null - - fun getFoo() = foo - - fun setFoo(foo: String): Test { - this.foo = foo - return this - } - - private var bar: Int = 0 - - fun getBar() = bar - - fun setBar(bar: Int): Test { - this.bar = bar - return this - } - - private var baz: Long? = null - - fun getBaz() = baz - - @Deprecated("do not use") - fun setBaz(baz: Long): Test { - this.baz = baz - return this - } - - companion object { - @JvmStatic fun build(): Test { - return Test() - } - } -} -''') - - when: - InstantiatableBeanDefinition factory = beanDefinition - ApplicationContext applicationContext = ApplicationContext.run( - 'test.foo':'good', - 'test.bar': '10', - 'test.baz':'20' - ) - def bean = factory.instantiate(applicationContext) - - then: - bean != null - bean.test != null - - when: - def test = bean.test - - then: - test.foo == 'good' - test.bar == 10 - test.baz == null //deprecated properties not settable - } - - void "test different inject types for config properties"() { - when: - BeanDefinition beanDefinition = buildBeanDefinition('test.Neo4jProperties', ''' -package test - -import io.micronaut.context.annotation.* -import org.neo4j.driver.v1.* - -@ConfigurationProperties("neo4j.test") -class Neo4jProperties { - internal var uri: java.net.URI? = null - - @ConfigurationBuilder( - prefixes=["with"], - allowZeroArgs=true - ) - val options: Config.ConfigBuilder = Config.build() -} -''') - InstantiatableBeanDefinition factory = beanDefinition - ApplicationContext applicationContext = ApplicationContext.run( - 'neo4j.test.encryptionLevel':'none', - 'neo4j.test.leakedSessionsLogging':true, - 'neo4j.test.maxIdleSessions':2 - ) - def bean = factory.instantiate(applicationContext) - - then: - bean != null - bean.options != null - - when: - Config config = bean.options.toConfig() - - then: - config.maxIdleConnectionPoolSize() == 2 - config.encrypted() == true // deprecated properties are ignored - config.logLeakedSessions() - } - - void "test specifying a configuration prefix"() { - when: - BeanDefinition beanDefinition = buildBeanDefinition('test.Neo4jProperties', ''' -package test - -import io.micronaut.context.annotation.* -import org.neo4j.driver.v1.* - -@ConfigurationProperties("neo4j.test") -class Neo4jProperties { - internal var uri: java.net.URI? = null - - @ConfigurationBuilder( - prefixes=["with"], - allowZeroArgs=true, - configurationPrefix="options" - ) - val options: Config.ConfigBuilder = Config.build() -} -''') - then: - beanDefinition.injectedMethods.size() == 1 - beanDefinition.injectedMethods.first().name == 'setUri$main' - - when: - InstantiatableBeanDefinition factory = beanDefinition - ApplicationContext applicationContext = ApplicationContext.run( - 'neo4j.test.options.encryptionLevel':'none', - 'neo4j.test.options.leakedSessionsLogging':true, - 'neo4j.test.options.maxIdleSessions':2 - ) - def bean = factory.instantiate(applicationContext) - - then: - bean != null - bean.options != null - - when: - Config config = bean.options.toConfig() - - then: - config.maxIdleConnectionPoolSize() == 2 - config.encrypted() == true // deprecated properties are ignored - config.logLeakedSessions() - } - - void "test specifying a configuration prefix with value"() { - when: - BeanDefinition beanDefinition = buildBeanDefinition('test.Neo4jProperties', ''' -package test; - -import io.micronaut.context.annotation.*; -import org.neo4j.driver.v1.*; - -@ConfigurationProperties("neo4j.test") -class Neo4jProperties { - internal var uri: java.net.URI? = null - - @ConfigurationBuilder( - prefixes=["with"], - allowZeroArgs=true, - value="options" - ) - val options: Config.ConfigBuilder = Config.build() - - -} -''') - InstantiatableBeanDefinition factory = beanDefinition - ApplicationContext applicationContext = ApplicationContext.run( - 'neo4j.test.options.encryptionLevel':'none', - 'neo4j.test.options.leakedSessionsLogging':true, - 'neo4j.test.options.maxIdleSessions':2 - ) - def bean = factory.instantiate(applicationContext) - - then: - bean != null - bean.options != null - - when: - Config config = bean.options.toConfig() - - then: - config.maxIdleConnectionPoolSize() == 2 - config.encrypted() == true // deprecated properties are ignored - config.logLeakedSessions() - } - - void "test specifying a configuration prefix with value using @AccessorsStyle"() { - when: - BeanDefinition beanDefinition = buildBeanDefinition('test.Neo4jProperties', ''' -package test - -import io.micronaut.context.annotation.* -import io.micronaut.core.annotation.AccessorsStyle -import org.neo4j.driver.v1.* - -@ConfigurationProperties("neo4j.test") -class Neo4jProperties { - internal var uri: java.net.URI? = null - - @ConfigurationBuilder( - allowZeroArgs = true, - value = "options" - ) - @AccessorsStyle(writePrefixes = ["with"]) - val options: Config.ConfigBuilder = Config.build() -} -''') - InstantiatableBeanDefinition factory = beanDefinition - ApplicationContext applicationContext = ApplicationContext.run( - 'neo4j.test.options.encryptionLevel':'none', - 'neo4j.test.options.leakedSessionsLogging':true, - 'neo4j.test.options.maxIdleSessions':2 - ) - def bean = factory.instantiate(applicationContext) - - then: - bean != null - bean.options != null - - when: - Config config = bean.options.toConfig() - - then: - config.maxIdleConnectionPoolSize() == 2 - config.encrypted() == true // deprecated properties are ignored - config.logLeakedSessions() - } - - void "test builder method long and TimeUnit arguments"() { - when: - BeanDefinition beanDefinition = buildBeanDefinition('test.Neo4jProperties', ''' -package test - -import io.micronaut.context.annotation.* -import org.neo4j.driver.v1.* - -@ConfigurationProperties("neo4j.test") -class Neo4jProperties { - internal var uri: java.net.URI? = null - - @ConfigurationBuilder( - prefixes=["with"], - allowZeroArgs=true - ) - val options: Config.ConfigBuilder = Config.build() - -} -''') - InstantiatableBeanDefinition factory = beanDefinition - ApplicationContext applicationContext = ApplicationContext.run( - 'neo4j.test.connectionLivenessCheckTimeout': '6s' - ) - def bean = factory.instantiate(applicationContext) - - then: - bean != null - bean.options != null - - when: - Config config = bean.options.toConfig() - - then: - config.idleTimeBeforeConnectionTest() == 6000 - } - - void "test configuration builder that are interfaces"() { - given: - ApplicationContext ctx = buildContext(''' -package test - -import io.micronaut.context.annotation.* -import io.micronaut.kotlin.processing.beans.configproperties.AnnWithClass - -@ConfigurationProperties("pool") -class PoolConfig { - - @ConfigurationBuilder(prefixes = [""]) - var builder: ConnectionPool.Builder = DefaultConnectionPool.builder() - -} - -interface ConnectionPool { - - interface Builder { - fun maxConcurrency(maxConcurrency: Int?): Builder - fun foo(foo: Foo): Builder - fun build(): ConnectionPool - } - - fun getMaxConcurrency(): Int? -} - -class DefaultConnectionPool(private val maxConcurrency: Int?): ConnectionPool { - - companion object { - @JvmStatic - fun builder(): ConnectionPool.Builder { - return DefaultBuilder() - } - } - - override fun getMaxConcurrency(): Int? = maxConcurrency - - private class DefaultBuilder: ConnectionPool.Builder { - - private var maxConcurrency: Int? = null - - override fun maxConcurrency(maxConcurrency: Int?): ConnectionPool.Builder{ - this.maxConcurrency = maxConcurrency - return this - } - - override fun foo(foo: Foo): ConnectionPool.Builder { - return this - } - - override fun build(): ConnectionPool{ - return DefaultConnectionPool(maxConcurrency) - } - } -} - -@AnnWithClass(String::class) -interface Foo -''') - ctx.getEnvironment().addPropertySource(PropertySource.of(["pool.max-concurrency": 123])) - - when: - Class testProps = ctx.classLoader.loadClass("test.PoolConfig") - def testPropBean = ctx.getBean(testProps) - - then: - noExceptionThrown() - testPropBean.builder.build().getMaxConcurrency() == 123 - } -} +//package io.micronaut.kotlin.processing.inject.configproperties +// +//import io.micronaut.context.ApplicationContext +//import io.micronaut.context.env.PropertySource +//import io.micronaut.inject.BeanDefinition +//import io.micronaut.inject.InstantiatableBeanDefinition +//import org.neo4j.driver.v1.Config +//import spock.lang.PendingFeature +//import spock.lang.Specification +//import static io.micronaut.annotation.processing.test.KotlinCompiler.* +// +//class ConfigurationPropertiesBuilderSpec extends Specification { +// +// void "test configuration builder on method"() { +// given: +// BeanDefinition beanDefinition = buildBeanDefinition('test.MyProperties', ''' +//package test; +// +//import io.micronaut.context.annotation.*; +// +//@ConfigurationProperties("test") +//class MyProperties { +// +// @ConfigurationBuilder(factoryMethod="build") +// var test: Test? = null +//} +// +//class Test private constructor() { +// +// var foo: String? = null +// +// companion object { +// @JvmStatic +// fun build(): Test { +// return Test() +// } +// } +//} +//''') +// +// when:"The bean was built and a warning was logged" +// InstantiatableBeanDefinition factory = beanDefinition +// ApplicationContext applicationContext = ApplicationContext.run( +// 'test.foo':'good' +// ) +// def bean = factory.instantiate(applicationContext) +// +// then: +// bean.test.foo == 'good' +// } +// +// void "test configuration builder with includes"() { +// given: +// BeanDefinition beanDefinition = buildBeanDefinition('test.MyProperties', ''' +//package test; +// +//import io.micronaut.context.annotation.*; +// +//@ConfigurationProperties("test") +//class MyProperties { +// +// @ConfigurationBuilder(factoryMethod="build", includes=["foo"]) +// var test: Test? = null +//} +// +//class Test private constructor() { +// +// var foo: String? = null +// var bar: String? = null +// +// companion object { +// @JvmStatic +// fun build(): Test { +// return Test() +// } +// } +//} +//''') +// +// when:"The bean was built and a warning was logged" +// InstantiatableBeanDefinition factory = beanDefinition +// ApplicationContext applicationContext = ApplicationContext.run( +// 'test.foo':'good', +// 'test.bar':'bad' +// ) +// def bean = factory.instantiate(applicationContext) +// +// then: +// bean.test.foo == 'good' +// bean.test.bar == null +// } +// +// void "test catch and log NoSuchMethodError for when underlying builder changes"() { +// given: +// BeanDefinition beanDefinition = buildBeanDefinition('test.MyProperties', ''' +//package test +// +//import io.micronaut.context.annotation.* +// +//@ConfigurationProperties("test") +//class MyProperties { +// +// @ConfigurationBuilder +// var test = Test() +//} +// +//class Test { +// fun setFoo(s: String) { +// throw NoSuchMethodError("setFoo") +// } +//} +//''') +// +// expect:"The bean was built and a warning was logged" +// InstantiatableBeanDefinition factory = beanDefinition +// ApplicationContext applicationContext = ApplicationContext.run( +// 'test.foo':'good', +// ) +// factory.instantiate(applicationContext) +// } +// +// void "test with setters that return void"() { +// given: +// BeanDefinition beanDefinition = buildBeanDefinition('test.MyProperties', ''' +//package test +// +//import io.micronaut.context.annotation.* +// +//@ConfigurationProperties("test") +//class MyProperties { +// +// @ConfigurationBuilder +// var test = Test() +//} +// +//class Test { +// var foo: String? = null +// var bar: Int = 0 +// @Deprecated("message") +// var baz: Long? = null +//} +//''') +// +// when: +// InstantiatableBeanDefinition factory = beanDefinition +// ApplicationContext applicationContext = ApplicationContext.run( +// 'test.foo':'good', +// 'test.bar': '10', +// 'test.baz':'20' +// ) +// def bean = factory.instantiate(applicationContext) +// +// then: +// bean != null +// bean.test != null +// +// when: +// def test = bean.test +// +// then: +// test.foo == 'good' +// test.bar == 10 +// test.baz == null //deprecated properties not settable +// } +// +// void "test different inject types for config properties"() { +// when: +// BeanDefinition beanDefinition = buildBeanDefinition('test.Neo4jProperties', ''' +//package test +// +//import io.micronaut.context.annotation.* +//import org.neo4j.driver.v1.* +// +//@ConfigurationProperties("neo4j.test") +//class Neo4jProperties { +// protected var uri: java.net.URI? = null +// +// @ConfigurationBuilder( +// prefixes=["with"], +// allowZeroArgs=true +// ) +// var options: Config.ConfigBuilder = Config.build() +//} +//''') +// then: +// beanDefinition.injectedMethods.size() == 1 +// beanDefinition.injectedMethods.first().name == 'setUri' +// +// when: +// InstantiatableBeanDefinition factory = beanDefinition +// ApplicationContext applicationContext = ApplicationContext.run( +// 'neo4j.test.encryptionLevel':'none', +// 'neo4j.test.leakedSessionsLogging':true, +// 'neo4j.test.maxIdleSessions':2 +// ) +// def bean = factory.instantiate(applicationContext) +// +// then: +// bean != null +// bean.options != null +// +// when: +// Config config = bean.options.toConfig() +// +// then: +// config.maxIdleConnectionPoolSize() == 2 +// config.encrypted() == true // deprecated properties are ignored +// config.logLeakedSessions() +// } +// +// void "test specifying a configuration prefix"() { +// when: +// BeanDefinition beanDefinition = buildBeanDefinition('test.Neo4jProperties', ''' +//package test +// +//import io.micronaut.context.annotation.* +//import org.neo4j.driver.v1.* +// +//@ConfigurationProperties("neo4j.test") +//class Neo4jProperties { +// protected var uri: java.net.URI? = null +// +// @ConfigurationBuilder( +// prefixes=["with"], +// allowZeroArgs=true, +// configurationPrefix="options" +// ) +// var options: Config.ConfigBuilder = Config.build() +//} +//''') +// then: +// beanDefinition.injectedMethods.size() == 1 +// beanDefinition.injectedMethods.first().name == 'setUri' +// +// when: +// InstantiatableBeanDefinition factory = beanDefinition +// ApplicationContext applicationContext = ApplicationContext.run( +// 'neo4j.test.options.encryptionLevel':'none', +// 'neo4j.test.options.leakedSessionsLogging':true, +// 'neo4j.test.options.maxIdleSessions':2 +// ) +// def bean = factory.instantiate(applicationContext) +// +// then: +// bean != null +// bean.options != null +// +// when: +// Config config = bean.options.toConfig() +// +// then: +// config.maxIdleConnectionPoolSize() == 2 +// config.encrypted() == true // deprecated properties are ignored +// config.logLeakedSessions() +// } +// +// void "test specifying a configuration prefix with value"() { +// when: +// BeanDefinition beanDefinition = buildBeanDefinition('test.Neo4jProperties', ''' +//package test +// +//import io.micronaut.context.annotation.* +//import org.neo4j.driver.v1.* +// +//@ConfigurationProperties("neo4j.test") +//class Neo4jProperties { +// protected var uri: java.net.URI? = null +// +// @ConfigurationBuilder( +// prefixes=["with"], +// allowZeroArgs=true, +// value="options" +// ) +// var options: Config.ConfigBuilder = Config.build() +//} +//''') +// then: +// beanDefinition.injectedMethods.size() == 1 +// beanDefinition.injectedMethods.first().name == 'setUri' +// +// when: +// InstantiatableBeanDefinition factory = beanDefinition +// ApplicationContext applicationContext = ApplicationContext.run( +// 'neo4j.test.options.encryptionLevel':'none', +// 'neo4j.test.options.leakedSessionsLogging':true, +// 'neo4j.test.options.maxIdleSessions':2 +// ) +// def bean = factory.instantiate(applicationContext) +// +// then: +// bean != null +// bean.options != null +// +// when: +// Config config = bean.options.toConfig() +// +// then: +// config.maxIdleConnectionPoolSize() == 2 +// config.encrypted() == true // deprecated properties are ignored +// config.logLeakedSessions() +// } +// +// void "test specifying a configuration prefix with value using @AccessorsStyle"() { +// when: +// BeanDefinition beanDefinition = buildBeanDefinition('test.Neo4jProperties', ''' +//package test; +// +//import io.micronaut.context.annotation.*; +//import io.micronaut.core.annotation.AccessorsStyle; +//import org.neo4j.driver.v1.*; +// +//@ConfigurationProperties("neo4j.test") +//class Neo4jProperties { +// protected var uri: java.net.URI? = null +// +// @ConfigurationBuilder( +// allowZeroArgs = true, +// value = "options" +// ) +// @AccessorsStyle(writePrefixes = ["with"]) +// var options: Config.ConfigBuilder = Config.build() +//} +//''') +// then: +// beanDefinition.injectedMethods.size() == 1 +// beanDefinition.injectedMethods.first().name == 'setUri' +// +// when: +// InstantiatableBeanDefinition factory = beanDefinition +// ApplicationContext applicationContext = ApplicationContext.run( +// 'neo4j.test.options.encryptionLevel':'none', +// 'neo4j.test.options.leakedSessionsLogging':true, +// 'neo4j.test.options.maxIdleSessions':2 +// ) +// def bean = factory.instantiate(applicationContext) +// +// then: +// bean != null +// bean.options != null +// +// when: +// Config config = bean.options.toConfig() +// +// then: +// config.maxIdleConnectionPoolSize() == 2 +// config.encrypted() == true // deprecated properties are ignored +// config.logLeakedSessions() +// } +// +// void "test builder method long and TimeUnit arguments"() { +// when: +// BeanDefinition beanDefinition = buildBeanDefinition('test.Neo4jProperties', ''' +//package test +// +//import io.micronaut.context.annotation.* +//import org.neo4j.driver.v1.* +// +//@ConfigurationProperties("neo4j.test") +//class Neo4jProperties { +// protected var uri: java.net.URI? = null +// +// @ConfigurationBuilder( +// prefixes=["with"], +// allowZeroArgs=true +// ) +// var options: Config.ConfigBuilder = Config.build() +// +//} +//''') +// then: +// beanDefinition.injectedMethods.size() == 1 +// beanDefinition.injectedMethods.first().name == 'setUri' +// +// when: +// InstantiatableBeanDefinition factory = beanDefinition +// ApplicationContext applicationContext = ApplicationContext.run( +// 'neo4j.test.connectionLivenessCheckTimeout': '6s' +// ) +// def bean = factory.instantiate(applicationContext) +// +// then: +// bean != null +// bean.options != null +// +// when: +// Config config = bean.options.toConfig() +// +// then: +// config.idleTimeBeforeConnectionTest() == 6000 +// } +// +// void "test using a builder that is marked final"() { +// when: +// BeanDefinition beanDefinition = buildBeanDefinition('test.Neo4jProperties', ''' +//package test +// +//import io.micronaut.context.annotation.* +//import org.neo4j.driver.v1.* +// +//@ConfigurationProperties("neo4j.test") +//class Neo4jProperties { +// +// @ConfigurationBuilder( +// prefixes=["with"], +// allowZeroArgs=true +// ) +// val options: Config.ConfigBuilder = Config.build() +// +//} +//''') +// InstantiatableBeanDefinition factory = beanDefinition +// ApplicationContext applicationContext = ApplicationContext.run( +// 'neo4j.test.connectionLivenessCheckTimeout': '17s' +// ) +// def bean = factory.instantiate(applicationContext) +// +// then: +// bean != null +// bean.options != null +// +// when: +// Config config = bean.options.toConfig() +// +// then: +// config.idleTimeBeforeConnectionTest() == 17000 +// } +// +// void "test with setter methods that return this"() { +// given: +// BeanDefinition beanDefinition = buildBeanDefinition('test.MyProperties', ''' +//package test +// +//import io.micronaut.context.annotation.* +// +//@ConfigurationProperties("test") +//class MyProperties { +// +// @ConfigurationBuilder(factoryMethod="build") +// var test: Test? = null +//} +// +//class Test private constructor() { +// +// private var foo: String? = null +// +// fun getFoo() = foo +// +// fun setFoo(foo: String): Test { +// this.foo = foo +// return this +// } +// +// private var bar: Int = 0 +// +// fun getBar() = bar +// +// fun setBar(bar: Int): Test { +// this.bar = bar +// return this +// } +// +// private var baz: Long? = null +// +// fun getBaz() = baz +// +// @Deprecated("do not use") +// fun setBaz(baz: Long): Test { +// this.baz = baz +// return this +// } +// +// companion object { +// @JvmStatic fun build(): Test { +// return Test() +// } +// } +//} +//''') +// +// when: +// InstantiatableBeanDefinition factory = beanDefinition +// ApplicationContext applicationContext = ApplicationContext.run( +// 'test.foo':'good', +// 'test.bar': '10', +// 'test.baz':'20' +// ) +// def bean = factory.instantiate(applicationContext) +// +// then: +// bean != null +// bean.test != null +// +// when: +// def test = bean.test +// +// then: +// test.foo == 'good' +// test.bar == 10 +// test.baz == null //deprecated properties not settable +// } +// +// void "test different inject types for config properties"() { +// when: +// BeanDefinition beanDefinition = buildBeanDefinition('test.Neo4jProperties', ''' +//package test +// +//import io.micronaut.context.annotation.* +//import org.neo4j.driver.v1.* +// +//@ConfigurationProperties("neo4j.test") +//class Neo4jProperties { +// internal var uri: java.net.URI? = null +// +// @ConfigurationBuilder( +// prefixes=["with"], +// allowZeroArgs=true +// ) +// val options: Config.ConfigBuilder = Config.build() +//} +//''') +// InstantiatableBeanDefinition factory = beanDefinition +// ApplicationContext applicationContext = ApplicationContext.run( +// 'neo4j.test.encryptionLevel':'none', +// 'neo4j.test.leakedSessionsLogging':true, +// 'neo4j.test.maxIdleSessions':2 +// ) +// def bean = factory.instantiate(applicationContext) +// +// then: +// bean != null +// bean.options != null +// +// when: +// Config config = bean.options.toConfig() +// +// then: +// config.maxIdleConnectionPoolSize() == 2 +// config.encrypted() == true // deprecated properties are ignored +// config.logLeakedSessions() +// } +// +// void "test specifying a configuration prefix"() { +// when: +// BeanDefinition beanDefinition = buildBeanDefinition('test.Neo4jProperties', ''' +//package test +// +//import io.micronaut.context.annotation.* +//import org.neo4j.driver.v1.* +// +//@ConfigurationProperties("neo4j.test") +//class Neo4jProperties { +// internal var uri: java.net.URI? = null +// +// @ConfigurationBuilder( +// prefixes=["with"], +// allowZeroArgs=true, +// configurationPrefix="options" +// ) +// val options: Config.ConfigBuilder = Config.build() +//} +//''') +// then: +// beanDefinition.injectedMethods.size() == 1 +// beanDefinition.injectedMethods.first().name == 'setUri$main' +// +// when: +// InstantiatableBeanDefinition factory = beanDefinition +// ApplicationContext applicationContext = ApplicationContext.run( +// 'neo4j.test.options.encryptionLevel':'none', +// 'neo4j.test.options.leakedSessionsLogging':true, +// 'neo4j.test.options.maxIdleSessions':2 +// ) +// def bean = factory.instantiate(applicationContext) +// +// then: +// bean != null +// bean.options != null +// +// when: +// Config config = bean.options.toConfig() +// +// then: +// config.maxIdleConnectionPoolSize() == 2 +// config.encrypted() == true // deprecated properties are ignored +// config.logLeakedSessions() +// } +// +// void "test specifying a configuration prefix with value"() { +// when: +// BeanDefinition beanDefinition = buildBeanDefinition('test.Neo4jProperties', ''' +//package test; +// +//import io.micronaut.context.annotation.*; +//import org.neo4j.driver.v1.*; +// +//@ConfigurationProperties("neo4j.test") +//class Neo4jProperties { +// internal var uri: java.net.URI? = null +// +// @ConfigurationBuilder( +// prefixes=["with"], +// allowZeroArgs=true, +// value="options" +// ) +// val options: Config.ConfigBuilder = Config.build() +// +// +//} +//''') +// InstantiatableBeanDefinition factory = beanDefinition +// ApplicationContext applicationContext = ApplicationContext.run( +// 'neo4j.test.options.encryptionLevel':'none', +// 'neo4j.test.options.leakedSessionsLogging':true, +// 'neo4j.test.options.maxIdleSessions':2 +// ) +// def bean = factory.instantiate(applicationContext) +// +// then: +// bean != null +// bean.options != null +// +// when: +// Config config = bean.options.toConfig() +// +// then: +// config.maxIdleConnectionPoolSize() == 2 +// config.encrypted() == true // deprecated properties are ignored +// config.logLeakedSessions() +// } +// +// void "test specifying a configuration prefix with value using @AccessorsStyle"() { +// when: +// BeanDefinition beanDefinition = buildBeanDefinition('test.Neo4jProperties', ''' +//package test +// +//import io.micronaut.context.annotation.* +//import io.micronaut.core.annotation.AccessorsStyle +//import org.neo4j.driver.v1.* +// +//@ConfigurationProperties("neo4j.test") +//class Neo4jProperties { +// internal var uri: java.net.URI? = null +// +// @ConfigurationBuilder( +// allowZeroArgs = true, +// value = "options" +// ) +// @AccessorsStyle(writePrefixes = ["with"]) +// val options: Config.ConfigBuilder = Config.build() +//} +//''') +// InstantiatableBeanDefinition factory = beanDefinition +// ApplicationContext applicationContext = ApplicationContext.run( +// 'neo4j.test.options.encryptionLevel':'none', +// 'neo4j.test.options.leakedSessionsLogging':true, +// 'neo4j.test.options.maxIdleSessions':2 +// ) +// def bean = factory.instantiate(applicationContext) +// +// then: +// bean != null +// bean.options != null +// +// when: +// Config config = bean.options.toConfig() +// +// then: +// config.maxIdleConnectionPoolSize() == 2 +// config.encrypted() == true // deprecated properties are ignored +// config.logLeakedSessions() +// } +// +// void "test builder method long and TimeUnit arguments"() { +// when: +// BeanDefinition beanDefinition = buildBeanDefinition('test.Neo4jProperties', ''' +//package test +// +//import io.micronaut.context.annotation.* +//import org.neo4j.driver.v1.* +// +//@ConfigurationProperties("neo4j.test") +//class Neo4jProperties { +// internal var uri: java.net.URI? = null +// +// @ConfigurationBuilder( +// prefixes=["with"], +// allowZeroArgs=true +// ) +// val options: Config.ConfigBuilder = Config.build() +// +//} +//''') +// InstantiatableBeanDefinition factory = beanDefinition +// ApplicationContext applicationContext = ApplicationContext.run( +// 'neo4j.test.connectionLivenessCheckTimeout': '6s' +// ) +// def bean = factory.instantiate(applicationContext) +// +// then: +// bean != null +// bean.options != null +// +// when: +// Config config = bean.options.toConfig() +// +// then: +// config.idleTimeBeforeConnectionTest() == 6000 +// } +// +// void "test configuration builder that are interfaces"() { +// given: +// ApplicationContext ctx = buildContext(''' +//package test +// +//import io.micronaut.context.annotation.* +//import io.micronaut.kotlin.processing.beans.configproperties.AnnWithClass +// +//@ConfigurationProperties("pool") +//class PoolConfig { +// +// @ConfigurationBuilder(prefixes = [""]) +// var builder: ConnectionPool.Builder = DefaultConnectionPool.builder() +// +//} +// +//interface ConnectionPool { +// +// interface Builder { +// fun maxConcurrency(maxConcurrency: Int?): Builder +// fun foo(foo: Foo): Builder +// fun build(): ConnectionPool +// } +// +// fun getMaxConcurrency(): Int? +//} +// +//class DefaultConnectionPool(private val maxConcurrency: Int?): ConnectionPool { +// +// companion object { +// @JvmStatic +// fun builder(): ConnectionPool.Builder { +// return DefaultBuilder() +// } +// } +// +// override fun getMaxConcurrency(): Int? = maxConcurrency +// +// private class DefaultBuilder: ConnectionPool.Builder { +// +// private var maxConcurrency: Int? = null +// +// override fun maxConcurrency(maxConcurrency: Int?): ConnectionPool.Builder{ +// this.maxConcurrency = maxConcurrency +// return this +// } +// +// override fun foo(foo: Foo): ConnectionPool.Builder { +// return this +// } +// +// override fun build(): ConnectionPool{ +// return DefaultConnectionPool(maxConcurrency) +// } +// } +//} +// +//@AnnWithClass(String::class) +//interface Foo +//''') +// ctx.getEnvironment().addPropertySource(PropertySource.of(["pool.max-concurrency": 123])) +// +// when: +// Class testProps = ctx.classLoader.loadClass("test.PoolConfig") +// def testPropBean = ctx.getBean(testProps) +// +// then: +// noExceptionThrown() +// testPropBean.builder.build().getMaxConcurrency() == 123 +// } +//} diff --git a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/visitor/AllElementsVisitor.java b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/visitor/AllElementsVisitor.java new file mode 100644 index 00000000000..9b3caa061c8 --- /dev/null +++ b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/visitor/AllElementsVisitor.java @@ -0,0 +1,122 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.kotlin.processing.visitor; + +import io.micronaut.core.annotation.AnnotationMetadataProvider; +import io.micronaut.inject.ast.ClassElement; +import io.micronaut.inject.ast.Element; +import io.micronaut.inject.ast.FieldElement; +import io.micronaut.inject.ast.MethodElement; +import io.micronaut.inject.ast.TypedElement; +import io.micronaut.inject.visitor.TypeElementVisitor; +import io.micronaut.inject.visitor.VisitorContext; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.IdentityHashMap; +import java.util.List; +import java.util.Set; + +public class AllElementsVisitor implements TypeElementVisitor { + public static List VISITED_ELEMENTS = new ArrayList<>(); + public static List VISITED_CLASS_ELEMENTS = new ArrayList<>(); + public static List VISITED_METHOD_ELEMENTS = new ArrayList<>(); + + public Set visited = Collections.newSetFromMap(new IdentityHashMap<>()); + + @Override + public void start(VisitorContext visitorContext) { + VISITED_ELEMENTS.clear(); + VISITED_CLASS_ELEMENTS.clear(); + VISITED_METHOD_ELEMENTS.clear(); + } + + @Override + public void visitClass(ClassElement element, VisitorContext context) { + visit(element); + // Preload annotations and elements for tests otherwise it fails because the compiler is done + initializeClassElement(element, 0); + VISITED_CLASS_ELEMENTS.add(element); + } + + @Override + public void visitMethod(MethodElement methodElement, VisitorContext context) { + VISITED_METHOD_ELEMENTS.add(methodElement); + // Preload + initializeMethodElement(methodElement, 0); + visit(methodElement); + } + + @Override + public void visitField(FieldElement element, VisitorContext context) { + initializeTypedElement(element, 0); + visit(element); + } + + private void initializeElement(Element typedElement) { + typedElement.getAnnotationMetadata().getAnnotationNames(); + } + + private void initializeTypedElement(TypedElement typedElement, int level) { + initializeElement(typedElement); + initializeClassElement(typedElement.getType(), level + 1); + initializeClassElement(typedElement.getGenericType(), level + 1); + } + + private void initializeClassElement(ClassElement classElement, int level) { + String name = classElement.getName(); + if (!name.startsWith("test.") && !name.startsWith(Object.class.getName()) && !name.startsWith("kotlin.Any")) { + return; + } + if (!visited.add(classElement)) { + return; + } + if (level > 5) { + return; + } + + initializeTypedElement(classElement, level + 1); + classElement.getTypeAnnotationMetadata().getAnnotationNames(); + classElement.getPrimaryConstructor().ifPresent(methodElement -> initializeMethodElement(methodElement, level + 1)); + classElement.getSuperType().ifPresent(superType -> initializeClassElement(superType, level + 1)); + classElement.getFields().forEach(field -> initializeTypedElement(field, level + 1)); + classElement.getMethods().forEach(method -> initializeMethodElement(method, level + 1)); + classElement.getDeclaredGenericPlaceholders(); + classElement.getSyntheticBeanProperties(); + classElement.getBeanProperties().forEach(AnnotationMetadataProvider::getAnnotationMetadata); + classElement.getBeanProperties().forEach(propertyElement -> { + initializeTypedElement(propertyElement, level + 1); + propertyElement.getField().ifPresent(f -> initializeTypedElement(f, level + 1)); + propertyElement.getWriteMethod().ifPresent(methodElement -> initializeMethodElement(methodElement, level + 1)); + propertyElement.getReadMethod().ifPresent(methodElement -> initializeMethodElement(methodElement, level + 1)); + }); + classElement.getAllTypeArguments().values().forEach(ta -> ta.values().forEach(ce -> initializeClassElement(ce, level + 1))); + } + + private void initializeMethodElement(MethodElement methodElement, int level) { + initializeElement(methodElement); + initializeClassElement(methodElement.getReturnType(), level + 1); + initializeClassElement(methodElement.getGenericReturnType(), level + 1); + Arrays.stream(methodElement.getParameters()).forEach(p -> initializeTypedElement(p, level + 1)); + methodElement.getDeclaredTypeArguments().values().forEach(c -> initializeClassElement(c, level + 1)); + methodElement.getTypeArguments().values().forEach(c -> initializeClassElement(c, level + 1)); + } + + private void visit(Element element) { + VISITED_ELEMENTS.add(element.getName()); + } +} diff --git a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/visitor/BeanIntrospectionSpec.groovy b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/visitor/BeanIntrospectionSpec.groovy index ec0123849fd..c9e5532887d 100644 --- a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/visitor/BeanIntrospectionSpec.groovy +++ b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/visitor/BeanIntrospectionSpec.groovy @@ -15,12 +15,11 @@ import io.micronaut.core.convert.TypeConverter import io.micronaut.core.reflect.InstantiationUtils import io.micronaut.core.reflect.exception.InstantiationException import io.micronaut.core.type.Argument +import io.micronaut.core.type.GenericPlaceholder import io.micronaut.inject.ExecutableMethod -import io.micronaut.inject.beans.visitor.IntrospectedTypeElementVisitor import io.micronaut.inject.beans.visitor.MappedSuperClassIntrospectionMapper import io.micronaut.kotlin.processing.elementapi.SomeEnum import io.micronaut.kotlin.processing.elementapi.TestClass -import spock.lang.Specification import javax.persistence.Column import javax.persistence.Entity @@ -117,7 +116,7 @@ class Test { void 'test favor field access'() { given: BeanIntrospection introspection = buildBeanIntrospection('fieldaccess.Test','''\ -package fieldaccess; +package fieldaccess import io.micronaut.core.annotation.* @@ -132,7 +131,7 @@ class Test { } var invoked = false } -'''); +''') when: def properties = introspection.getBeanProperties() def instance = introspection.instantiate() @@ -163,7 +162,7 @@ open class Test(val two: Integer?) { // read-only protected var four: String? = null // not included since protected private var five: String? = null // not included since private } -'''); +''') when: def properties = introspection.getBeanProperties() @@ -702,9 +701,9 @@ fun interface Foo { void "test bean introspection with property with static creator method on interface with generic type arguments"() { given: BeanIntrospection introspection = buildBeanIntrospection('test.Foo', ''' -package test; +package test -import io.micronaut.core.annotation.Creator; +import io.micronaut.core.annotation.Creator @io.micronaut.core.annotation.Introspected fun interface Foo { @@ -851,8 +850,8 @@ class Book(val title: String) { BeanIntrospector introspector = BeanIntrospector.SHARED Field introspectionMapField = introspector.getClass().getDeclaredField("introspectionMap") introspectionMapField.setAccessible(true) - introspectionMapField.set(introspector, new HashMap>()); - Map map = (Map) introspectionMapField.get(introspector) + introspectionMapField.set(introspector, new HashMap>()) + Map map = (Map) introspectionMapField.get(introspector) map.put(reference.getName(), reference) and: @@ -913,8 +912,8 @@ class Book { BeanIntrospector introspector = BeanIntrospector.SHARED Field introspectionMapField = introspector.getClass().getDeclaredField("introspectionMap") introspectionMapField.setAccessible(true) - introspectionMapField.set(introspector, new HashMap>()); - Map map = (Map) introspectionMapField.get(introspector) + introspectionMapField.set(introspector, new HashMap>()) + Map map = (Map) introspectionMapField.get(introspector) map.put(reference.getName(), reference) and: @@ -956,8 +955,8 @@ class Book { BeanIntrospector introspector = BeanIntrospector.SHARED Field introspectionMapField = introspector.getClass().getDeclaredField("introspectionMap") introspectionMapField.setAccessible(true) - introspectionMapField.set(introspector, new HashMap>()); - Map map = (Map) introspectionMapField.get(introspector) + introspectionMapField.set(introspector, new HashMap>()) + Map map = (Map) introspectionMapField.get(introspector) map.put(reference.getName(), reference) and: @@ -1155,7 +1154,7 @@ class Test( @Version fun getAnotherVersion(): Long? { - return v; + return v } fun setAnotherVersion(v: Long) { @@ -1937,13 +1936,12 @@ abstract class Test { void "test class loading is not shared between the introspection and the ref"() { when: BeanIntrospection beanIntrospection = buildBeanIntrospection("test.Test", """ -package test; +package test -import io.micronaut.core.annotation.Introspected; -import java.util.Set; +import io.micronaut.core.annotation.Introspected +import java.util.Set -@Introspected(excludedAnnotations = [Deprecated::class]) -public class Test { +@Introspected(excludedAnnotations = [Deprecated::class]) class Test { var authors: Set? = null } @@ -2062,8 +2060,7 @@ import io.micronaut.context.annotation.Executable @Introspected(classes = [MyInterface::class]) class Test -@JsonClassDescription -public interface MyInterface { +@JsonClassDescription interface MyInterface { fun getName(): String @Executable @@ -2116,4 +2113,58 @@ class MyImpl: MyInterface { introspection.beanProperties.size() == 0 introspection.beanMethods.size() == 1 } + + void "test type_use annotations"() { + given: + def introspection = buildBeanIntrospection('test.Test', ''' +package test +import io.micronaut.core.annotation.Introspected +import io.micronaut.kotlin.processing.visitor.* + +@Introspected +class Test(val name: @TypeUseRuntimeAnn String, val secondName: @TypeUseClassAnn String) +''') + def nameField = introspection.getProperty("name").orElse(null) + def secondNameField = introspection.getProperty("secondName").orElse(null) + + expect: + nameField + secondNameField + + nameField.hasStereotype(TypeUseRuntimeAnn.name) + !secondNameField.hasStereotype(TypeUseClassAnn.name) + } + + void "test subtypes"() { + given: + BeanIntrospection introspection = buildBeanIntrospection('test.Holder', ''' +package test +import io.micronaut.core.annotation.Introspected + +@Introspected +open class Animal +@Introspected +class Cat(val lives: Int) : Animal() + +@Introspected +class Holder( + var animalNonGeneric: Animal, + var animalsNonGeneric: List, + var animal: A, + var animals: List +) { + + constructor(animal: A) : this(animal, listOf(animal), animal, listOf(animal)) +} + ''') + + expect: + def animalListArgument = introspection.getProperty("animals").get().asArgument().getTypeParameters()[0] + animalListArgument instanceof GenericPlaceholder + animalListArgument.isTypeVariable() + + def animal = introspection.getProperty("animal").get().asArgument() + animal instanceof GenericPlaceholder + animal.isTypeVariable() + } } diff --git a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/visitor/KotlinReconstructionSpec.groovy b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/visitor/KotlinReconstructionSpec.groovy index 70c43a88bc3..001b505e668 100644 --- a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/visitor/KotlinReconstructionSpec.groovy +++ b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/visitor/KotlinReconstructionSpec.groovy @@ -1,49 +1,92 @@ package io.micronaut.kotlin.processing.visitor import io.micronaut.annotation.processing.test.AbstractKotlinCompilerSpec +import io.micronaut.inject.ast.ClassElement +import io.micronaut.inject.ast.ElementQuery import io.micronaut.inject.ast.GenericPlaceholderElement -import spock.lang.PendingFeature +import io.micronaut.inject.ast.MethodElement +import io.micronaut.inject.ast.WildcardElement import spock.lang.Unroll - class KotlinReconstructionSpec extends AbstractKotlinCompilerSpec { - @PendingFeature(reason = "Not yet implemented") + @Unroll("field type is #fieldType") def 'field type'() { given: - def element = buildClassElement("example.Test", """ -package example; + def element = buildClassElement("test.Test", """ +package test; import java.util.*; class Test { lateinit var field : $fieldType } + +class Lst { +} +""") + def field = element.getFields()[0] + + expect: + reconstructTypeSignature(field.genericType) == fieldType + + where: + fieldType << [ + 'String', + 'List', + 'List', + 'List', + 'List>', + 'List', + 'List>', + 'List>', + 'List>>>', + 'Lst', + 'Lst', + 'Lst>' + ] + } + + @Unroll("type var is #decl") + def 'type vars declared on method'() { + given: + def element = buildClassElement("test.Test", """ +package test; + +import java.util.*; + +abstract class Test { + fun <$decl> method() {} +} +class Lst { +} """) - def field = element.getFields()[0] + def method = element. getEnclosedElement(ElementQuery.ALL_METHODS.named(s -> s == 'method')).get() expect: - reconstructTypeSignature(field.genericType) == fieldType + reconstructTypeSignature(method.declaredTypeVariables[0], true) == decl where: - fieldType << [ - 'String', - 'List', - 'List', - 'List>', - 'List', -// 'List', // doesn't work? - 'List>', - 'List>>>' - ] - } - - @PendingFeature(reason = "Breaks because you can't use kotlin elements outside of a compilation execution") + decl << [ +// 'T', +'T : CharSequence', +'T : A', +'T : List<*>', +'T : List', +'T : List', +'T : List', +'T : List>', +'T : Lst', +'T : Lst', +'T : Lst>' + ] + } + @Unroll("super type is #superType") def 'super type'() { given: - def element = buildClassElement("example.Test", """ -package example; + def element = buildClassElement("test.Test", """ +package test; import java.util.*; @@ -52,27 +95,25 @@ abstract class Test : $superType() { """) expect: - reconstructTypeSignature(element.superType.get()) == superType + reconstructTypeSignature(element.superType.get()) == superType where: - superType << [ -// 'AbstractList', raw types not supported - 'AbstractList', - 'AbstractList', - 'AbstractList>', - 'AbstractList>', - 'AbstractList>>', - 'AbstractList>>>', - 'AbstractList>' - ] - } - - @PendingFeature(reason = "Breaks because you can't use kotlin elements outside of a compilation execution") + superType << [ + 'AbstractList', + 'AbstractList', + 'AbstractList>', + 'AbstractList>', + 'AbstractList>>', + 'AbstractList>>>', + 'AbstractList>' + ] + } + @Unroll("super interface is #superType") def 'super interface'() { given: - def element = buildClassElement("example.Test", """ -package example; + def element = buildClassElement("test.Test", """ +package test; import java.util.*; @@ -81,83 +122,483 @@ abstract class Test : $superType { """) expect: - reconstructTypeSignature(element.interfaces[0]) == superType + reconstructTypeSignature(element.interfaces[0]) == superType where: - superType << [ -// 'List', - 'List', - 'List', - 'List>', - 'List>', -// 'List>', - 'List>>', - 'List>>>', -// 'List', - 'List>', - ] + superType << [ + 'List', + 'List', + 'List>', + 'List>', + 'List>>', + 'List>>>', + 'List>', + ] } @Unroll("type var is #decl") - @PendingFeature def 'type vars declared on type'() { given: - def element = buildClassElement("example.Test", """ -package example; + def element = buildClassElement("test.Test", """ +package test; import java.util.*; abstract class Test { } + +class Lst { +} """) expect: - reconstructTypeSignature(element.declaredGenericPlaceholders[1], true) == decl + reconstructTypeSignature(element.declaredGenericPlaceholders[1], true) == decl where: - decl << [ - 'T', - 'out T : CharSequence', - 'T : A', -// 'T extends List', -// 'T extends List', -// 'T extends List', -// 'T extends List', -// 'T extends List', -// 'T extends List', - ] + decl << [ +// 'T', +'T : A', +'T : List<*>', +'T : List', +'T : List', +'T : List', +'T : List>', +'T : Lst', +'T : Lst', +'T : Lst>' + ] } @Unroll('declaration is #decl') - @PendingFeature(reason = "Not yet implemented") def 'fold type variable to null'() { given: - def classElement = buildClassElement("example.Test", """ -package example; + def classElement = buildClassElement("test.Test", """ +package test; import java.util.*; class Test { lateinit var field : $decl; } + +class Lst { +} """) - def fieldType = classElement.fields[0].type + def fieldType = classElement.fields[0].type expect: - reconstructTypeSignature(fieldType.foldBoundGenericTypes { - if (it != null && it.isGenericPlaceholder() && ((GenericPlaceholderElement) it).variableName == 'T') { - return null - } else { - return it - } - }) == expected + reconstructTypeSignature(fieldType.foldBoundGenericTypes { + if (it != null && it.isGenericPlaceholder() && ((GenericPlaceholderElement) it).variableName == 'T') { + return null + } else { + return it + } + }) == expected + + where: + decl | expected + 'String' | 'String' + 'List' | 'List' + 'Map' | 'Map' + 'List' | 'List' + 'Lst' | 'Lst' + } + + @Unroll("field type is #fieldType") + def 'bound field type'() { + given: + def element = buildClassElement("test.Wrapper", """ +package test; + +import java.util.*; + +class Wrapper { + var test: Test? = null; +} +class Test { + var field: $fieldType? = null; +} +class Lst { +} +""") + def field = element.getFields()[0].genericType.getFields()[0] + + expect: + reconstructTypeSignature(field.genericType) == expectedType where: - decl | expected - 'String' | 'String' - 'List' | 'List' - 'Map' | 'Map' - 'List' | 'List' -// 'List' | 'List' + fieldType | expectedType + 'String' | 'String' + 'List' | 'List' + 'List<*>' | 'List<*>' + 'List' | 'List' + 'List>' | 'List>' + 'List' | 'List' + 'Lst' | 'Lst' + 'List>' | 'List>' + 'List>>>' | 'List>>>' +// 'List>' | 'List>' + } + + + @Unroll("field type is #fieldType") + def 'bound field type to other variable'() { + given: + def element = buildClassElement("test.Wrapper", """ +package test; + +import java.util.*; + +class Wrapper { + var test: Test? = null; +} +class Test { + var field: $fieldType? = null; +} +class Lst { +} +""") + def field = element.getFields()[0].genericType.getFields()[0] + + expect: + reconstructTypeSignature(field.genericType) == expectedType + + where: + fieldType | expectedType + 'String' | 'String' + 'List' | 'List' + 'List<*>' | 'List<*>' + 'List' | 'List' + 'List>' | 'List>' + 'List' | 'List' + 'Lst' | 'Lst' + 'List>' | 'List>' + 'List>>>' | 'List>>>' +// 'List>' | 'List>' + } + + def 'unbound super type'() { + given: + def superElement = buildClassElement("test.Sub", """ +package test; + +import java.util.*; + +class Sub : Sup<$params>() { +} +open class Sup<$decl> { +} +class Lst { +} +""") + def interfaceElement = buildClassElement("test.Sub", """ +package test; + +import java.util.*; + +class Sub : Sup<$params> { +} +interface Sup<$decl> { +} +class Lst { +} +""") + + expect: + reconstructTypeSignature(superElement.getSuperType().get()) == expected + reconstructTypeSignature(interfaceElement.getInterfaces()[0]) == expected + + where: + decl | params | expected + 'T' | 'String' | 'Sup' + 'T' | 'List' | 'Sup>' + 'T' | 'List' | 'Sup>' + 'T' | 'Lst' | 'Sup>' + } + + def 'bound super type'() { + given: + def superElement = buildClassElementTransformed("test.Sub", """ +package test; + +class Sub : Sup<$params>() { +} +open class Sup<$decl> { +} +class MyList +class Lst +class Str +""", { ce -> + ce = ce.withTypeArguments([ClassElement.of("test.Str")]) + initializeAllTypeArguments(ce) + return ce + }) + def interfaceElement = buildClassElementTransformed("test.Sub", """ +package test; + +class Sub : Sup<$params> { +} +interface Sup<$decl> { +} +class MyList +class Lst +class Str +""", { ce -> + ce = ce.withTypeArguments([ClassElement.of("test.Str")]) + initializeAllTypeArguments(ce) + return ce + }) + + expect: + reconstructTypeSignature(superElement.getSuperType().get()) == expected + reconstructTypeSignature(interfaceElement.getInterfaces()[0]) == expected + + where: + decl | params | expected + 'T' | 'Str' | 'Sup' + 'T' | 'MyList' | 'Sup>' + 'T' | 'MyList' | 'Sup>' + 'T' | 'Lst' | 'Sup>' + } + + @Unroll('declaration is #decl') + def 'fold type variable'() { + given: + def fieldType = buildClassElementTransformed("test.Test", """ +package test; + +class Test { + var field : $decl? = null; +} +class MyMap +class MyList +class Lst +class Str +""", { + def fieldType = it.fields[0].type.foldBoundGenericTypes { + if (it.isGenericPlaceholder() && ((GenericPlaceholderElement) it).variableName == 'T') { + return ClassElement.of("test.Str") + } else { + return it + } + } + + initializeAllTypeArguments(fieldType) + return fieldType + }) + + expect: + reconstructTypeSignature(fieldType) == expected + + where: + decl | expected + 'Str' | 'Str' + 'T' | 'Str' + 'MyList' | 'MyList' + 'MyMap' | 'MyMap' + 'MyList' | 'MyList' + 'Lst' | 'Lst' + } + + def 'distinguish list types'() { + given: + def classElement = buildClassElement("test.Test", """ +package test; + +import java.util.*; + +class Test { + var field1: List<*>? = null + var field2: List<*>? = null + var field3: List? = null +} +""") + def rawType = classElement.fields[0].genericType + def wildcardType = classElement.fields[1].genericType + def objectType = classElement.fields[2].genericType + + expect: +// rawType.boundGenericTypes.isEmpty() + rawType.typeArguments["E"].type.name == "java.lang.Object" + rawType.typeArguments["E"].isRawType() + rawType.typeArguments["E"].isWildcard() + !rawType.typeArguments["E"].isGenericPlaceholder() + +// wildcardType.boundGenericTypes.size() == 1 +// wildcardType.boundGenericTypes[0].isWildcard() +// wildcardType.typeArguments["E"].type.name == "java.lang.Object" +// wildcardType.typeArguments["E"].isWildcard() +// !wildcardType.typeArguments["E"].isRawType() + + objectType.boundGenericTypes.size() == 1 + !objectType.boundGenericTypes[0].isWildcard() + objectType.typeArguments["E"].type.name == "java.lang.Object" + !objectType.typeArguments["E"].isWildcard() + !objectType.typeArguments["E"].isRawType() + !objectType.typeArguments["E"].isGenericPlaceholder() + } + + def 'distinguish list types 2'() { + given: + def classElement = buildClassElement("test.Test", """ +package test; + +import java.util.*; +import java.lang.Number; + +class Test { + var field1: List<*>? = null + var field2: List<*>? = null + var field3: List? = null + var field4: List? = null + var field5: List? = null +} +""") + def rawType = classElement.fields[0].type + def wildcardType = classElement.fields[1].type + def objectType = classElement.fields[2].type + def stringType = classElement.fields[3].type + def numberType = classElement.fields[4].type + + expect: + rawType.typeArguments["E"].type.name == "java.lang.Object" + rawType.typeArguments["E"].isRawType() + rawType.typeArguments["E"].isWildcard() + !rawType.typeArguments["E"].isGenericPlaceholder() + +// wildcardType.typeArguments["E"].type.name == "java.lang.Object" +// wildcardType.typeArguments["E"].isWildcard() +// !((WildcardElement)wildcardType.typeArguments["E"]).isBounded() +// !wildcardType.typeArguments["E"].isRawType() + + objectType.typeArguments["E"].type.name == "java.lang.Object" + !objectType.typeArguments["E"].isWildcard() + !objectType.typeArguments["E"].isRawType() + !objectType.typeArguments["E"].isGenericPlaceholder() + + stringType.typeArguments["E"].type.name == "java.lang.String" + !stringType.typeArguments["E"].isWildcard() + !stringType.typeArguments["E"].isRawType() + !stringType.typeArguments["E"].isGenericPlaceholder() + + numberType.typeArguments["E"].type.name == "java.lang.Number" + numberType.typeArguments["E"].isWildcard() + ((WildcardElement) numberType.typeArguments["E"]).isBounded() + !numberType.typeArguments["E"].isRawType() + } + + def 'distinguish base list type'() { + given: + def classElement = buildClassElement("test.Test", """ +package test; + +import java.util.*; +import java.lang.Number; + +class Test : Base() { +} + +abstract class Base { + var field1: List<*>? = null + var field2: List<*>? = null + var field3: List? = null + var field4: List? = null +} + +""") + def rawType = classElement.fields[0].type + def wildcardType = classElement.fields[1].type + def objectType = classElement.fields[2].type + def genericType = classElement.fields[3].type + + expect: + rawType.typeArguments["E"].type.name == "java.lang.Object" + rawType.typeArguments["E"].isRawType() + rawType.typeArguments["E"].isWildcard() + !rawType.typeArguments["E"].isGenericPlaceholder() + +// wildcardType.typeArguments["E"].type.name == "java.lang.Object" +// wildcardType.typeArguments["E"].isWildcard() +// !((WildcardElement)wildcardType.typeArguments["E"]).isBounded() +// !wildcardType.typeArguments["E"].isRawType() + + objectType.typeArguments["E"].type.name == "java.lang.Object" + !objectType.typeArguments["E"].isWildcard() + !objectType.typeArguments["E"].isRawType() + !objectType.typeArguments["E"].isGenericPlaceholder() + + genericType.typeArguments["E"].type.name == "java.lang.Object" + !genericType.typeArguments["E"].isWildcard() + !genericType.typeArguments["E"].isRawType() + genericType.typeArguments["E"].isGenericPlaceholder() + (genericType.typeArguments["E"] as GenericPlaceholderElement).getResolved().isEmpty() + } + + def 'distinguish base list generic type'() { + given: + def classElement = buildClassElement("test.Test", """ +package test; + +import java.util.*; +import java.lang.Number; + +class Test : Base() { +} + +abstract class Base { + var field1: List<*>? = null + var field2: List<*>? = null + var field3: List? = null + var field4: List? = null +} + +""") + def rawType = classElement.fields[0].genericType + def wildcardType = classElement.fields[1].genericType + def objectType = classElement.fields[2].genericType + def genericType = classElement.fields[3].genericType + + expect: + rawType.typeArguments["E"].type.name == "java.lang.Object" + rawType.typeArguments["E"].isRawType() + rawType.typeArguments["E"].isWildcard() + !rawType.typeArguments["E"].isGenericPlaceholder() + +// wildcardType.typeArguments["E"].type.name == "java.lang.Object" +// wildcardType.typeArguments["E"].isWildcard() +// !((WildcardElement)wildcardType.typeArguments["E"]).isBounded() +// !wildcardType.typeArguments["E"].isRawType() + + objectType.typeArguments["E"].type.name == "java.lang.Object" + !objectType.typeArguments["E"].isWildcard() + !objectType.typeArguments["E"].isRawType() + !objectType.typeArguments["E"].isGenericPlaceholder() + + genericType.typeArguments["E"].type.name == "java.lang.String" + !genericType.typeArguments["E"].isWildcard() + !genericType.typeArguments["E"].isRawType() + genericType.typeArguments["E"].isGenericPlaceholder() + def resolved = (genericType.typeArguments["E"] as GenericPlaceholderElement).getResolved().get() + resolved.name == "java.lang.String" + !resolved.isWildcard() + !resolved.isRawType() + resolved.isGenericPlaceholder() + (resolved as GenericPlaceholderElement).declaringElement.get().name == "test.Base" + } + + private void initializeAllTypeArguments(ClassElement type) { + initializeAllTypeArguments0(type, 0) + } + + private void initializeAllTypeArguments0(ClassElement type, int level) { + if (level == 4) { + return + } + type.getAnnotationNames() + type.getAllTypeArguments().entrySet().forEach { e1 -> + e1.value.entrySet().forEach { e2 -> + initializeAllTypeArguments0(e2.value, level + 1) + } + } } } diff --git a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/visitor/TypeUseClassAnn.java b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/visitor/TypeUseClassAnn.java new file mode 100644 index 00000000000..a424caac784 --- /dev/null +++ b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/visitor/TypeUseClassAnn.java @@ -0,0 +1,11 @@ +package io.micronaut.kotlin.processing.visitor; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.TYPE_USE) +@Retention(RetentionPolicy.CLASS) +public @interface TypeUseClassAnn { +} diff --git a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/visitor/TypeUseRuntimeAnn.java b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/visitor/TypeUseRuntimeAnn.java new file mode 100644 index 00000000000..25dd93254ea --- /dev/null +++ b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/visitor/TypeUseRuntimeAnn.java @@ -0,0 +1,11 @@ +package io.micronaut.kotlin.processing.visitor; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.TYPE_USE) +@Retention(RetentionPolicy.RUNTIME) +public @interface TypeUseRuntimeAnn { +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/TypeUseRuntimeAnn.java b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/TypeUseRuntimeAnn.java new file mode 100644 index 00000000000..5d026464d32 --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/TypeUseRuntimeAnn.java @@ -0,0 +1,11 @@ +package io.micronaut.kotlin.processing; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target({ElementType.TYPE_USE}) +@Retention(RetentionPolicy.RUNTIME) +public @interface TypeUseRuntimeAnn { +} diff --git a/inject-kotlin/src/test/resources/META-INF/services/io.micronaut.inject.visitor.TypeElementVisitor b/inject-kotlin/src/test/resources/META-INF/services/io.micronaut.inject.visitor.TypeElementVisitor new file mode 100644 index 00000000000..5c55c94a11b --- /dev/null +++ b/inject-kotlin/src/test/resources/META-INF/services/io.micronaut.inject.visitor.TypeElementVisitor @@ -0,0 +1,6 @@ +io.micronaut.kotlin.processing.visitor.AllElementsVisitor +io.micronaut.kotlin.processing.annotations.AnnotateFieldSpec$AnnotationFieldVisitor +io.micronaut.kotlin.processing.annotations.AnnotateFieldTypeSpec$AnnotateFieldTypeVisitor +io.micronaut.kotlin.processing.annotations.AnnotateMethodParameterSpec$AnnotateMethodParameterVisitor +io.micronaut.kotlin.processing.annotations.AnnotateMethodReturnSpec$AnnotateMethodReturnVisitor +io.micronaut.kotlin.processing.annotations.AnnotateMethodSpec$AnnotationMethodVisitor From 9ddfbe700dc163d60da2df17153e8da95e0a09af Mon Sep 17 00:00:00 2001 From: Graeme Rocher Date: Mon, 27 Feb 2023 17:32:55 +0100 Subject: [PATCH 521/743] cleanup unnecessary GraalVM flags (#8843) --- .../micronaut-aop/native-image.properties | 1 - .../native-image.properties | 17 ----------------- .../native-image.properties | 1 - .../micronaut-http/native-image.properties | 2 +- .../native-image.properties | 17 ----------------- .../native-image.properties | 17 ----------------- 6 files changed, 1 insertion(+), 54 deletions(-) delete mode 100644 aop/src/main/resources/META-INF/native-image/io.micronaut/micronaut-aop/native-image.properties delete mode 100644 core-reactive/src/main/resources/META-INF/native-image/io.micronaut/micronaut-core-reactive/native-image.properties delete mode 100644 http-server/src/main/resources/META-INF/native-image/io.micronaut/micronaut-http-server/native-image.properties delete mode 100644 management/src/main/resources/META-INF/native-image/io.micronaut.management/micronaut-management/native-image.properties delete mode 100644 validation/src/main/resources/META-INF/native-image/io.micronaut.validation/micronaut-validation/native-image.properties diff --git a/aop/src/main/resources/META-INF/native-image/io.micronaut/micronaut-aop/native-image.properties b/aop/src/main/resources/META-INF/native-image/io.micronaut/micronaut-aop/native-image.properties deleted file mode 100644 index bcbeb04f25a..00000000000 --- a/aop/src/main/resources/META-INF/native-image/io.micronaut/micronaut-aop/native-image.properties +++ /dev/null @@ -1 +0,0 @@ -Args = --initialize-at-run-time=io.micronaut.aop.internal.intercepted.PublisherInterceptedMethod \ No newline at end of file diff --git a/core-reactive/src/main/resources/META-INF/native-image/io.micronaut/micronaut-core-reactive/native-image.properties b/core-reactive/src/main/resources/META-INF/native-image/io.micronaut/micronaut-core-reactive/native-image.properties deleted file mode 100644 index 5070ec57c20..00000000000 --- a/core-reactive/src/main/resources/META-INF/native-image/io.micronaut/micronaut-core-reactive/native-image.properties +++ /dev/null @@ -1,17 +0,0 @@ -# -# Copyright 2017-2021 original authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# - -Args = --initialize-at-run-time=io.micronaut.core.async.publisher.Publishers diff --git a/http-server/src/main/resources/META-INF/native-image/io.micronaut/micronaut-http-server/native-image.properties b/http-server/src/main/resources/META-INF/native-image/io.micronaut/micronaut-http-server/native-image.properties deleted file mode 100644 index f161fdc340d..00000000000 --- a/http-server/src/main/resources/META-INF/native-image/io.micronaut/micronaut-http-server/native-image.properties +++ /dev/null @@ -1 +0,0 @@ -Args = --initialize-at-run-time=io.micronaut.http.server.exceptions.JsonExceptionHandler,io.micronaut.http.server.exceptions.$JsonExceptionHandler$Definition,io.micronaut.http.server.exceptions.$JsonExceptionHandler$Definition$Exec,io.micronaut.http.server.$CoroutineHelper$Definition \ No newline at end of file diff --git a/http/src/main/resources/META-INF/native-image/io.micronaut.http/micronaut-http/native-image.properties b/http/src/main/resources/META-INF/native-image/io.micronaut.http/micronaut-http/native-image.properties index e0af0c8cd2d..ebe23952d2d 100644 --- a/http/src/main/resources/META-INF/native-image/io.micronaut.http/micronaut-http/native-image.properties +++ b/http/src/main/resources/META-INF/native-image/io.micronaut.http/micronaut-http/native-image.properties @@ -14,4 +14,4 @@ # limitations under the License. # -Args = -H:IncludeResources=META-INF/http/mime.types --initialize-at-run-time=io.micronaut.http.bind.binders.ContinuationArgumentBinder,io.micronaut.http.server.CoroutineHelper, \ No newline at end of file +Args = -H:IncludeResources=META-INF/http/mime.types diff --git a/management/src/main/resources/META-INF/native-image/io.micronaut.management/micronaut-management/native-image.properties b/management/src/main/resources/META-INF/native-image/io.micronaut.management/micronaut-management/native-image.properties deleted file mode 100644 index 6cebe3b888c..00000000000 --- a/management/src/main/resources/META-INF/native-image/io.micronaut.management/micronaut-management/native-image.properties +++ /dev/null @@ -1,17 +0,0 @@ -# -# Copyright 2017-2021 original authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# - -Args = --initialize-at-run-time=io.micronaut.management.health.indicator.jdbc.$JdbcIndicator$Definition diff --git a/validation/src/main/resources/META-INF/native-image/io.micronaut.validation/micronaut-validation/native-image.properties b/validation/src/main/resources/META-INF/native-image/io.micronaut.validation/micronaut-validation/native-image.properties deleted file mode 100644 index 4b684dcb448..00000000000 --- a/validation/src/main/resources/META-INF/native-image/io.micronaut.validation/micronaut-validation/native-image.properties +++ /dev/null @@ -1,17 +0,0 @@ -# -# Copyright 2017-2020 original authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# - -Args = --initialize-at-run-time=io.micronaut.validation.exceptions.ValidationExceptionHandler,io.micronaut.validation.exceptions.$ValidationExceptionHandler$Definition,io.micronaut.validation.exceptions.$ValidationExceptionHandler$Definition$Exec,io.micronaut.validation.exceptions.$ConstraintExceptionHandler$Definition,io.micronaut.validation.exceptions.ConstraintExceptionHandler From b504d82328ad71db8bece15e33d4f3e6068ee9f0 Mon Sep 17 00:00:00 2001 From: Tim Yates Date: Mon, 27 Feb 2023 20:05:20 +0000 Subject: [PATCH 522/743] doc: add note about using logback 1.3.x+ (#8846) Logback 1.3.x breaks binary compatability, and cannot be used in some cases with Micronaut 3.8.x This commit adds this fact to the documentation --- src/main/docs/guide/logging/logback.adoc | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/docs/guide/logging/logback.adoc b/src/main/docs/guide/logging/logback.adoc index a44f5bc885a..356d4027e28 100644 --- a/src/main/docs/guide/logging/logback.adoc +++ b/src/main/docs/guide/logging/logback.adoc @@ -2,6 +2,8 @@ To use the logback library, add the following dependency to your build. dependency:ch.qos.logback:logback-classic[gradleScope="implementation"] +NOTE: Logback 1.3.x+ included a breaking binary change that may prevent it working with 3.8.x of the Micronaut framework. If you are using Logback 1.3.x+ and are experiencing issues, please downgrade to Logback 1.2.x. + If it does not exist yet, place a link:https://logback.qos.ch/manual/configuration.html[logback.xml] file in the resources folder and modify the content for your needs. For example: .src/main/resources/logback.xml From 9c30cb8bb64095b95e7745845a9c837e7c7da3ea Mon Sep 17 00:00:00 2001 From: micronaut-build Date: Mon, 27 Feb 2023 20:23:27 +0000 Subject: [PATCH 523/743] [skip ci] Release v3.8.6 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index f3e4c40fdb0..9f4e4a451a2 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -projectVersion=3.8.6-SNAPSHOT +projectVersion=3.8.6 projectGroupId=io.micronaut title=Micronaut projectDesc=Natively Cloud Native From e1f443eccccbd97f4dc62d7be573fe17be3c2f1f Mon Sep 17 00:00:00 2001 From: micronaut-build Date: Mon, 27 Feb 2023 20:42:48 +0000 Subject: [PATCH 524/743] Back to 3.8.7-SNAPSHOT --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 9f4e4a451a2..59f2faa0b19 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -projectVersion=3.8.6 +projectVersion=3.8.7-SNAPSHOT projectGroupId=io.micronaut title=Micronaut projectDesc=Natively Cloud Native From b4f86d7af6b2d30a191a5107e12e8e58d384532e Mon Sep 17 00:00:00 2001 From: micronaut-build <65172877+micronaut-build@users.noreply.github.com> Date: Tue, 28 Feb 2023 12:30:18 +0100 Subject: [PATCH 525/743] Bump micronaut-azure to 3.8.1 (#8851) --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 2962afcfa16..0f232afc4ec 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -68,7 +68,7 @@ managed-micrometer = "1.10.3" managed-micronaut-acme = "3.2.0" managed-micronaut-aot = "1.1.1" managed-micronaut-aws = "3.14.0" -managed-micronaut-azure = "3.8.0" +managed-micronaut-azure = "3.8.1" managed-micronaut-cache = "3.5.0" managed-micronaut-cassandra = "5.1.1" managed-micronaut-coherence = "3.7.2" From 113b39bbc0facd59f0c707e62669b7bfd87bb0a2 Mon Sep 17 00:00:00 2001 From: Miguel Ferreira Date: Tue, 28 Feb 2023 15:33:08 +0100 Subject: [PATCH 526/743] Update simple retry explanation to mention a linear progression of 1s (#8850) If the retry delay always increases by 1s then that's not an exponential progression, it's a linear progression where the delay time is calculated as 1s x n, and n is the number of retries. --- src/main/docs/guide/aop/retry.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/docs/guide/aop/retry.adoc b/src/main/docs/guide/aop/retry.adoc index 1150ab5671d..c75303286b9 100644 --- a/src/main/docs/guide/aop/retry.adoc +++ b/src/main/docs/guide/aop/retry.adoc @@ -4,7 +4,7 @@ With this in mind, Micronaut includes a api:retry.annotation.Retryable[] annotat == Simple Retry -The simplest form of retry is just to add the `@Retryable` annotation to a type or method. The default behaviour of `@Retryable` is to retry three times with an exponential delay of one second between each retry. (first attempt with 1s delay, second attempt with 2s delay, third attempt with 3s delay). +The simplest form of retry is just to add the `@Retryable` annotation to a type or method. The default behaviour of `@Retryable` is to retry three times with a linear delay of one second between each retry. (first attempt with 1s delay, second attempt with 2s delay, third attempt with 3s delay). For example: From fd5bfdbc6b3041010f9ec29dd2dafabc2bac727f Mon Sep 17 00:00:00 2001 From: Denis Stepanov Date: Tue, 28 Feb 2023 11:27:19 -0500 Subject: [PATCH 527/743] Correct annotating a property for KSP and improve method element annotations (#8847) --- .../micronaut/inject/ast/MethodElement.java | 86 +++++- .../annotation/AbstractAnnotationElement.java | 2 +- ...tractElementAnnotationMetadataFactory.java | 213 +++++++------- .../ElementAnnotationMetadataFactory.java | 6 +- .../MethodElementAnnotationMetadata.java | 58 ++++ .../MethodElementAnnotationsHelper.java | 111 ++++++++ ...utatedMethodElementAnnotationMetadata.java | 59 ++++ .../PropertyElementAnnotationMetadata.java | 19 +- .../AbstractBeanElementCreator.java | 12 +- .../DeclaredBeanElementCreator.java | 8 +- .../groovy/visitor/GroovyClassElement.java | 10 +- .../groovy/visitor/GroovyMethodElement.java | 20 ++ .../groovy/visitor/GroovyPropertyElement.java | 2 +- .../modify/AnnotatePropertySpec.groovy | 263 ++++++++++++++++++ .../JavaElementAnnotationMetadataFactory.java | 12 - .../processing/visitor/JavaMethodElement.java | 20 ++ .../annotation/AnnotatePropertySpec.groovy | 218 +++++++++++++++ ...icronaut.inject.visitor.TypeElementVisitor | 1 + .../KotlinElementAnnotationMetadataFactory.kt | 2 +- .../visitor/AbstractKotlinMethodElement.kt | 16 ++ .../visitor/AbstractKotlinPropertyElement.kt | 2 + .../processing/visitor/KotlinClassElement.kt | 40 ++- .../processing/visitor/KotlinMethodElement.kt | 2 +- .../processing/visitor/LoadedVisitor.kt | 19 +- .../AnnotateMethodReturnSpec.groovy | 1 + .../annotations/AnnotatePropertySpec.groovy | 174 ++++++++++++ .../inject/ast/ClassElementSpec.groovy | 175 +++++++++++- ...icronaut.inject.visitor.TypeElementVisitor | 1 + 28 files changed, 1365 insertions(+), 187 deletions(-) create mode 100644 core-processor/src/main/java/io/micronaut/inject/ast/annotation/MethodElementAnnotationMetadata.java create mode 100644 core-processor/src/main/java/io/micronaut/inject/ast/annotation/MethodElementAnnotationsHelper.java create mode 100644 core-processor/src/main/java/io/micronaut/inject/ast/annotation/MutatedMethodElementAnnotationMetadata.java create mode 100644 inject-groovy/src/test/groovy/io/micronaut/inject/annotation/modify/AnnotatePropertySpec.groovy create mode 100644 inject-java/src/test/groovy/io/micronaut/annotation/AnnotatePropertySpec.groovy create mode 100644 inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/annotations/AnnotatePropertySpec.groovy diff --git a/core-processor/src/main/java/io/micronaut/inject/ast/MethodElement.java b/core-processor/src/main/java/io/micronaut/inject/ast/MethodElement.java index 6b3880dc442..df3d078dbde 100644 --- a/core-processor/src/main/java/io/micronaut/inject/ast/MethodElement.java +++ b/core-processor/src/main/java/io/micronaut/inject/ast/MethodElement.java @@ -26,6 +26,7 @@ import io.micronaut.core.util.ArrayUtils; import io.micronaut.core.util.CollectionUtils; import io.micronaut.inject.annotation.AbstractAnnotationMetadataBuilder; +import io.micronaut.inject.ast.annotation.MutableAnnotationMetadataDelegate; import io.micronaut.inject.ast.beans.BeanElementBuilder; import java.lang.annotation.Annotation; @@ -46,6 +47,25 @@ */ public interface MethodElement extends MemberElement { + /** + * Returns the method annotations. + * The method will only return annotations defined on a method or inherited from the super methods, + * while {@link #getAnnotationMetadata()} for a method combines the class and the method annotations. + * NOTE: For a constructor {@link #getAnnotationMetadata()} will not combine the class annotations. + * + * @return The method annotation metadata + * @since 4.0.0 + */ + @NonNull + default MutableAnnotationMetadataDelegate getMethodAnnotationMetadata() { + return new MutableAnnotationMetadataDelegate<>() { + @Override + public AnnotationMetadata getAnnotationMetadata() { + return MethodElement.this.getAnnotationMetadata(); + } + }; + } + /** * @return The return type of the method */ @@ -414,22 +434,24 @@ public String toString() { /** * Creates a {@link MethodElement} for the given parameters. * - * @param owningType The owing type - * @param declaringType The declaring type - * @param annotationMetadataProvider The annotation metadata provider - * @param metadataBuilder The metadata builder - * @param returnType The return type - * @param genericReturnType The generic return type - * @param name The name - * @param isStatic Is static - * @param isFinal Is final - * @param parameterElements The parameter elements + * @param owningType The owing type + * @param declaringType The declaring type + * @param methodAnnotationMetadataProvider The method annotation metadata provider + * @param annotationMetadataProvider The annotation metadata provider + * @param metadataBuilder The metadata builder + * @param returnType The return type + * @param genericReturnType The generic return type + * @param name The name + * @param isStatic Is static + * @param isFinal Is final + * @param parameterElements The parameter elements * @return The method element * @since 4.0.0 */ static @NonNull MethodElement of( @NonNull ClassElement owningType, @NonNull ClassElement declaringType, + @NonNull AnnotationMetadataProvider methodAnnotationMetadataProvider, @NonNull AnnotationMetadataProvider annotationMetadataProvider, @NonNull AbstractAnnotationMetadataBuilder metadataBuilder, @NonNull ClassElement returnType, @@ -440,6 +462,7 @@ public String toString() { ParameterElement... parameterElements) { return new MethodElement() { + private @Nullable AnnotationMetadata methodAnnotationMetadata; private @Nullable AnnotationMetadata annotationMetadata; @Override @@ -469,7 +492,18 @@ public MethodElement withParameters(ParameterElement... newParameters) { return MethodElement.of( owningType, declaringType, - annotationMetadataProvider, + new AnnotationMetadataProvider() { + @Override + public AnnotationMetadata getAnnotationMetadata() { + return methodAnnotationMetadata; + } + }, + new AnnotationMetadataProvider() { + @Override + public AnnotationMetadata getAnnotationMetadata() { + return annotationMetadata; + } + }, metadataBuilder, returnType, genericReturnType, @@ -480,6 +514,24 @@ public MethodElement withParameters(ParameterElement... newParameters) { ); } + @Override + public MutableAnnotationMetadataDelegate getMethodAnnotationMetadata() { + return new MutableAnnotationMetadataDelegate() { + + @Override + public AnnotationMetadata getAnnotationMetadata() { + return getMethodAnnotationMetadata0(); + } + }; + } + + private AnnotationMetadata getMethodAnnotationMetadata0() { + if (methodAnnotationMetadata == null) { + methodAnnotationMetadata = methodAnnotationMetadataProvider.getAnnotationMetadata().copyAnnotationMetadata(); + } + return methodAnnotationMetadata; + } + @NonNull @Override public AnnotationMetadata getAnnotationMetadata() { @@ -540,6 +592,7 @@ public Element annotate(@NonNull String annotationType, @ consumer.accept(builder); AnnotationValue av = builder.build(); + this.methodAnnotationMetadata = metadataBuilder.annotate(getMethodAnnotationMetadata0(), av); this.annotationMetadata = metadataBuilder.annotate(getAnnotationMetadata(), av); } return this; @@ -548,6 +601,7 @@ public Element annotate(@NonNull String annotationType, @ @Override public Element annotate(AnnotationValue annotationValue) { ArgumentUtils.requireNonNull("annotationValue", annotationValue); + methodAnnotationMetadata = metadataBuilder.annotate(getMethodAnnotationMetadata0(), annotationValue); annotationMetadata = metadataBuilder.annotate(getAnnotationMetadata(), annotationValue); return this; } @@ -556,6 +610,7 @@ public Element annotate(AnnotationValue annotationValu @SuppressWarnings("java:S1192") public Element removeAnnotation(@NonNull String annotationType) { ArgumentUtils.requireNonNull("annotationType", annotationType); + methodAnnotationMetadata = metadataBuilder.removeAnnotation(getMethodAnnotationMetadata0(), annotationType); annotationMetadata = metadataBuilder.removeAnnotation(getAnnotationMetadata(), annotationType); return this; } @@ -563,6 +618,7 @@ public Element removeAnnotation(@NonNull String annotationType) { @Override public Element removeAnnotationIf(@NonNull Predicate> predicate) { ArgumentUtils.requireNonNull("predicate", predicate); + methodAnnotationMetadata = metadataBuilder.removeAnnotationIf(getMethodAnnotationMetadata0(), predicate); annotationMetadata = metadataBuilder.removeAnnotationIf(getAnnotationMetadata(), predicate); return this; @@ -572,6 +628,7 @@ public Element removeAnnotationIf(@NonNull Predicate 0) { - cacheAnnotationMetadata = new AnnotationMetadataHierarchy( - Stream.concat(Arrays.stream(parentAnnotationMetadata), Stream.of(annotationMetadata)).toArray(AnnotationMetadata[]::new) - ); - } else { - cacheAnnotationMetadata = annotationMetadata; - } - } - return cacheAnnotationMetadata; + if (annotationMetadata != null) { + return annotationMetadata; + } + return getCacheEntry(); } - private AnnotationMetadata getAnnotationMetadataToModify() { - if (preloadedAnnotationMetadata instanceof AnnotationMetadataHierarchy) { - return preloadedAnnotationMetadata.getDeclaredMetadata().copyAnnotationMetadata(); - } - if (preloadedAnnotationMetadata != null) { - return preloadedAnnotationMetadata; + @Override + protected AnnotationMetadata getAnnotationMetadataToModify() { + if (annotationMetadata != null) { + return annotationMetadata; } return getCacheEntry().copyAnnotationMetadata(); } - private AnnotationMetadata replaceAnnotationsInternal(AnnotationMetadata annotationMetadata) { + @Override + protected AnnotationMetadata replaceAnnotationsInternal(AnnotationMetadata annotationMetadata) { if (annotationMetadata instanceof AbstractAnnotationMetadataBuilder.CachedAnnotationMetadata) { throw new IllegalStateException(); } if (annotationMetadata instanceof MutableAnnotationMetadataDelegate) { throw new IllegalStateException(); } + if (annotationMetadata instanceof AnnotationMetadataHierarchy) { + throw new IllegalStateException("Not supported to cache AnnotationMetadataHierarchy"); + } if (annotationMetadata.isEmpty()) { annotationMetadata = EMPTY_METADATA; } - if (!readOnly) { - if (annotationMetadata instanceof AnnotationMetadataHierarchy) { - throw new IllegalStateException("Not supported to cache AnnotationMetadataHierarchy"); - } - getCacheEntry().update(annotationMetadata); - preloadedAnnotationMetadata = null; + if (readOnly) { + this.annotationMetadata = annotationMetadata; } else { - preloadedAnnotationMetadata = annotationMetadata; + getCacheEntry().update(annotationMetadata); } - cacheAnnotationMetadata = null; return getAnnotationMetadata(); } + } + + /** + * Abstract mutable implementation of {@link ElementAnnotationMetadata}. + * + * @author Denis Stepanov + * @since 4.0.0 + */ + protected abstract class MutableElementAnnotationMetadata implements ElementAnnotationMetadata { + + /** + * Return the annotation metadata to modify. + * + * @return The annotation metadata to modify + */ + protected abstract AnnotationMetadata getAnnotationMetadataToModify(); + + /** + * Replaces existing annotation metadata. + * + * @param annotationMetadata new annotation metadata + * @return The annotation metadata + */ + protected abstract AnnotationMetadata replaceAnnotationsInternal(AnnotationMetadata annotationMetadata); + @Override @SuppressWarnings("java:S1192") public AnnotationMetadata annotate(@NonNull String annotationType, @NonNull Consumer> consumer) { diff --git a/core-processor/src/main/java/io/micronaut/inject/ast/annotation/ElementAnnotationMetadataFactory.java b/core-processor/src/main/java/io/micronaut/inject/ast/annotation/ElementAnnotationMetadataFactory.java index 3e45fab8fea..8528058e2e2 100644 --- a/core-processor/src/main/java/io/micronaut/inject/ast/annotation/ElementAnnotationMetadataFactory.java +++ b/core-processor/src/main/java/io/micronaut/inject/ast/annotation/ElementAnnotationMetadataFactory.java @@ -57,15 +57,13 @@ public interface ElementAnnotationMetadataFactory { ElementAnnotationMetadata buildGenericTypeAnnotations(@NonNull GenericElement element); /** - * Build new element annotation metadata from the element with preloaded annotations. - * This method will avoid fetching default annotation metadata from cache. + * Build new mutable element annotation metadata. * - * @param element The element * @param annotationMetadata The preloaded annotation * @return the element's metadata */ @NonNull - ElementAnnotationMetadata build(@NonNull Element element, @NonNull AnnotationMetadata annotationMetadata); + ElementAnnotationMetadata buildMutable(@NonNull AnnotationMetadata annotationMetadata); /** * Makes this factory read-only. No modification to the annotation metadata should be persisted into the shared cache. diff --git a/core-processor/src/main/java/io/micronaut/inject/ast/annotation/MethodElementAnnotationMetadata.java b/core-processor/src/main/java/io/micronaut/inject/ast/annotation/MethodElementAnnotationMetadata.java new file mode 100644 index 00000000000..dcdff7b4492 --- /dev/null +++ b/core-processor/src/main/java/io/micronaut/inject/ast/annotation/MethodElementAnnotationMetadata.java @@ -0,0 +1,58 @@ +/* + * Copyright 2017-2023 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.inject.ast.annotation; + +import io.micronaut.core.annotation.AnnotationMetadata; +import io.micronaut.core.annotation.NonNull; +import io.micronaut.inject.annotation.AnnotationMetadataHierarchy; +import io.micronaut.inject.ast.MethodElement; + +/** + * The element annotation metadata for a method element. + * + * @author Denis Stepanov + * @since 4.0.0 + */ +public final class MethodElementAnnotationMetadata extends AbstractElementAnnotationMetadata { + + private final MethodElement methodElement; + private final MutableAnnotationMetadataDelegate writeAnnotationMetadata; + private final AnnotationMetadata readAnnotationMetadata; + + public MethodElementAnnotationMetadata(@NonNull MethodElement methodElement) { + this.methodElement = methodElement; + writeAnnotationMetadata = methodElement.getMethodAnnotationMetadata(); + readAnnotationMetadata = new AnnotationMetadataHierarchy( + methodElement.getOwningType(), + writeAnnotationMetadata + ); + } + + @Override + public AnnotationMetadata getAnnotationMetadata() { + return readAnnotationMetadata; + } + + @Override + protected MethodElement getReturnInstance() { + return methodElement; + } + + @Override + protected MutableAnnotationMetadataDelegate getAnnotationMetadataToWrite() { + return writeAnnotationMetadata; + } +} diff --git a/core-processor/src/main/java/io/micronaut/inject/ast/annotation/MethodElementAnnotationsHelper.java b/core-processor/src/main/java/io/micronaut/inject/ast/annotation/MethodElementAnnotationsHelper.java new file mode 100644 index 00000000000..af440881713 --- /dev/null +++ b/core-processor/src/main/java/io/micronaut/inject/ast/annotation/MethodElementAnnotationsHelper.java @@ -0,0 +1,111 @@ +/* + * Copyright 2017-2023 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.inject.ast.annotation; + +import io.micronaut.core.annotation.AnnotationMetadata; +import io.micronaut.core.annotation.NonNull; +import io.micronaut.core.annotation.Nullable; +import io.micronaut.inject.annotation.AnnotationMetadataHierarchy; +import io.micronaut.inject.ast.ConstructorElement; +import io.micronaut.inject.ast.MethodElement; + +/** + * The helper class to implement method element annotations. + * + * @author Denis Stepanov + * @since 4.0.0 + */ +public final class MethodElementAnnotationsHelper { + + private final AbstractAnnotationElement methodElement; + private final ElementAnnotationMetadataFactory elementAnnotationMetadataFactory; + + @Nullable + private ElementAnnotationMetadata resolvedMethodAnnotationMetadata; + @Nullable + private AnnotationMetadata resolvedAnnotationMetadata; + + /** + * The constructor. + * + * @param methodElement The method element + * @param elementAnnotationMetadataFactory The annotations factory + */ + public MethodElementAnnotationsHelper(AbstractAnnotationElement methodElement, + ElementAnnotationMetadataFactory elementAnnotationMetadataFactory) { + this.methodElement = methodElement; + this.elementAnnotationMetadataFactory = elementAnnotationMetadataFactory; + } + + /** + * Returns the method annotations. + * + * @param presetAnnotationMetadata The preset annotations + * @return The method annotations + */ + @NonNull + public ElementAnnotationMetadata getMethodAnnotationMetadata(@Nullable AnnotationMetadata presetAnnotationMetadata) { + if (resolvedMethodAnnotationMetadata == null) { + if (methodElement instanceof ConstructorElement) { + resolvedMethodAnnotationMetadata = methodElement.getElementAnnotationMetadata(); + } else if (presetAnnotationMetadata instanceof AnnotationMetadataHierarchy annotationMetadataHierarchy) { + // Preset overrides both class and method annotations + AnnotationMetadata declaredMetadata = annotationMetadataHierarchy.getDeclaredMetadata(); + resolvedMethodAnnotationMetadata = getBuildMutable(declaredMetadata); + } else if (presetAnnotationMetadata != null) { + // Preset overrides method annotation + resolvedMethodAnnotationMetadata = getBuildMutable(presetAnnotationMetadata); + } else { + resolvedMethodAnnotationMetadata = methodElement.getElementAnnotationMetadata(); + } + } + return resolvedMethodAnnotationMetadata; + } + + @NonNull + private ElementAnnotationMetadata getBuildMutable(@NonNull AnnotationMetadata declaredMetadata) { + if (declaredMetadata instanceof ElementAnnotationMetadata elementAnnotationMetadata) { + return elementAnnotationMetadata; + } + return elementAnnotationMetadataFactory.buildMutable(declaredMetadata); + } + + /** + * Returns the annotations. + * + * @param presetAnnotationMetadata The preset annotations + * @return The annotations + */ + @NonNull + public AnnotationMetadata getAnnotationMetadata(@Nullable AnnotationMetadata presetAnnotationMetadata) { + if (resolvedAnnotationMetadata == null) { + if (methodElement instanceof ConstructorElement) { + resolvedAnnotationMetadata = getMethodAnnotationMetadata(presetAnnotationMetadata); + } else if (presetAnnotationMetadata instanceof AnnotationMetadataHierarchy annotationMetadataHierarchy) { + // Preset overrides both class and method annotations + resolvedAnnotationMetadata = new AnnotationMetadataHierarchy(annotationMetadataHierarchy.getRootMetadata(), getMethodAnnotationMetadata(presetAnnotationMetadata)); + } else if (presetAnnotationMetadata != null) { + // Preset overrides method annotation + resolvedAnnotationMetadata = new MutatedMethodElementAnnotationMetadata((MethodElement) methodElement, getMethodAnnotationMetadata(presetAnnotationMetadata)); + } else { + // Combine class and method annotations + resolvedAnnotationMetadata = new MethodElementAnnotationMetadata((MethodElement) methodElement); + } + } + return resolvedAnnotationMetadata; + } + +} diff --git a/core-processor/src/main/java/io/micronaut/inject/ast/annotation/MutatedMethodElementAnnotationMetadata.java b/core-processor/src/main/java/io/micronaut/inject/ast/annotation/MutatedMethodElementAnnotationMetadata.java new file mode 100644 index 00000000000..320afa9daac --- /dev/null +++ b/core-processor/src/main/java/io/micronaut/inject/ast/annotation/MutatedMethodElementAnnotationMetadata.java @@ -0,0 +1,59 @@ +/* + * Copyright 2017-2023 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.inject.ast.annotation; + +import io.micronaut.core.annotation.AnnotationMetadata; +import io.micronaut.core.annotation.NonNull; +import io.micronaut.inject.annotation.AnnotationMetadataHierarchy; +import io.micronaut.inject.ast.MethodElement; + +/** + * The mutated element annotation metadata for a method element. + * + * @author Denis Stepanov + * @since 4.0.0 + */ +public final class MutatedMethodElementAnnotationMetadata extends AbstractElementAnnotationMetadata { + + private final MethodElement methodElement; + private final MutableAnnotationMetadataDelegate writeAnnotationMetadata; + private final AnnotationMetadata readAnnotationMetadata; + + public MutatedMethodElementAnnotationMetadata(@NonNull MethodElement methodElement, + MutableAnnotationMetadataDelegate methodAnnotationMetadata) { + this.methodElement = methodElement; + writeAnnotationMetadata = methodAnnotationMetadata; + readAnnotationMetadata = new AnnotationMetadataHierarchy( + methodElement.getOwningType(), + writeAnnotationMetadata + ); + } + + @Override + public AnnotationMetadata getAnnotationMetadata() { + return readAnnotationMetadata; + } + + @Override + protected MethodElement getReturnInstance() { + return methodElement; + } + + @Override + protected MutableAnnotationMetadataDelegate getAnnotationMetadataToWrite() { + return writeAnnotationMetadata; + } +} diff --git a/core-processor/src/main/java/io/micronaut/inject/ast/annotation/PropertyElementAnnotationMetadata.java b/core-processor/src/main/java/io/micronaut/inject/ast/annotation/PropertyElementAnnotationMetadata.java index 7f45f56275a..b2fad92609f 100644 --- a/core-processor/src/main/java/io/micronaut/inject/ast/annotation/PropertyElementAnnotationMetadata.java +++ b/core-processor/src/main/java/io/micronaut/inject/ast/annotation/PropertyElementAnnotationMetadata.java @@ -16,7 +16,6 @@ package io.micronaut.inject.ast.annotation; import io.micronaut.core.annotation.AnnotationMetadata; -import io.micronaut.core.annotation.AnnotationMetadataDelegate; import io.micronaut.core.annotation.AnnotationValue; import io.micronaut.core.annotation.AnnotationValueBuilder; import io.micronaut.core.annotation.NonNull; @@ -59,7 +58,7 @@ public PropertyElementAnnotationMetadata(@NonNull this.thisElement = thisElement; List> elements = new ArrayList<>(3); if (setter != null && (!setter.isSynthetic() || includeSynthetic)) { - elements.add(setter); + elements.add(setter.getMethodAnnotationMetadata()); ParameterElement[] parameters = setter.getParameters(); if (parameters.length > 0) { ParameterElement parameter = parameters[0]; @@ -84,7 +83,7 @@ public PropertyElementAnnotationMetadata(@NonNull } } if (getter != null && (!getter.isSynthetic() || includeSynthetic)) { - elements.add(getter); + elements.add(getter.getMethodAnnotationMetadata()); MutableAnnotationMetadataDelegate typeAnnotationMetadata = getter.getReturnType().getTypeAnnotationMetadata(); if (!typeAnnotationMetadata.isEmpty()) { elements.add(typeAnnotationMetadata); @@ -93,19 +92,7 @@ public PropertyElementAnnotationMetadata(@NonNull // The instance AnnotationMetadata of each element can change after a modification // Set annotation metadata as actual elements so the changes are reflected - AnnotationMetadata[] hierarchy = elements.stream().map(e -> { - if (e instanceof MethodElement methodElement) { - return new AnnotationMetadataDelegate() { - @Override - public AnnotationMetadata getAnnotationMetadata() { - AnnotationMetadata annotationMetadata = methodElement.getAnnotationMetadata(); - // Exclude type metadata - return annotationMetadata.getDeclaredMetadata(); - } - }; - } - return e; - }).toArray(AnnotationMetadata[]::new); + AnnotationMetadata[] hierarchy = elements.toArray(AnnotationMetadata[]::new); this.propertyAnnotationMetadata = hierarchy.length == 1 ? hierarchy[0] : new AnnotationMetadataHierarchy(true, hierarchy); this.elements = elements; diff --git a/core-processor/src/main/java/io/micronaut/inject/processing/AbstractBeanElementCreator.java b/core-processor/src/main/java/io/micronaut/inject/processing/AbstractBeanElementCreator.java index 04ce327a635..16468f49c43 100644 --- a/core-processor/src/main/java/io/micronaut/inject/processing/AbstractBeanElementCreator.java +++ b/core-processor/src/main/java/io/micronaut/inject/processing/AbstractBeanElementCreator.java @@ -28,7 +28,6 @@ import io.micronaut.core.annotation.Internal; import io.micronaut.core.util.ArrayUtils; import io.micronaut.core.value.OptionalValues; -import io.micronaut.inject.annotation.AnnotationMetadataHierarchy; import io.micronaut.inject.ast.ClassElement; import io.micronaut.inject.ast.ElementQuery; import io.micronaut.inject.ast.MemberElement; @@ -101,14 +100,11 @@ protected void visitAnnotationMetadata(BeanDefinitionVisitor writer, AnnotationM } } - public static AnnotationMetadata getElementAnnotationMetadata(MemberElement methodElement) { - // NOTE: if annotation processor modified the method's annotation - // annotationUtils.getAnnotationMetadata(method) will return AnnotationMetadataHierarchy of both method+class metadata - AnnotationMetadata annotationMetadata = methodElement.getTargetAnnotationMetadata(); - if (annotationMetadata instanceof AnnotationMetadataHierarchy) { - return annotationMetadata.getDeclaredMetadata(); + public static AnnotationMetadata getElementAnnotationMetadata(MemberElement memberElement) { + if (memberElement instanceof MethodElement methodElement) { + return methodElement.getMethodAnnotationMetadata(); } - return annotationMetadata; + return memberElement.getAnnotationMetadata(); } protected boolean visitIntrospectedMethod(BeanDefinitionVisitor visitor, ClassElement typeElement, MethodElement methodElement) { diff --git a/core-processor/src/main/java/io/micronaut/inject/processing/DeclaredBeanElementCreator.java b/core-processor/src/main/java/io/micronaut/inject/processing/DeclaredBeanElementCreator.java index 79846771093..3ea31d191cf 100644 --- a/core-processor/src/main/java/io/micronaut/inject/processing/DeclaredBeanElementCreator.java +++ b/core-processor/src/main/java/io/micronaut/inject/processing/DeclaredBeanElementCreator.java @@ -305,7 +305,7 @@ protected boolean visitPropertyWriteElement(BeanDefinitionVisitor visitor, if (visitInjectAndLifecycleMethod(visitor, writeElement)) { makeInterceptedForValidationIfNeeded(writeElement); return true; - } else if (!writeElement.isStatic() && getElementAnnotationMetadata(writeElement).hasStereotype(AnnotationUtil.QUALIFIER)) { + } else if (!writeElement.isStatic() && writeElement.getMethodAnnotationMetadata().hasStereotype(AnnotationUtil.QUALIFIER)) { if (propertyElement.getReadMethod().isPresent() && writeElement.hasStereotype(ANN_REQUIRES_VALIDATION)) { visitor.setValidated(true); } @@ -413,7 +413,7 @@ private boolean visitAopAndExecutableMethod(BeanDefinitionVisitor visitor, Metho */ protected boolean visitAopMethod(BeanDefinitionVisitor visitor, MethodElement methodElement) { boolean aopDefinedOnClassAndPublicMethod = isAopProxy && (methodElement.isPublic() || methodElement.isPackagePrivate()); - AnnotationMetadata methodAnnotationMetadata = getElementAnnotationMetadata(methodElement); + AnnotationMetadata methodAnnotationMetadata = methodElement.getMethodAnnotationMetadata(); if (aopDefinedOnClassAndPublicMethod || !isAopProxy && InterceptedMethodUtil.hasAroundStereotype(methodAnnotationMetadata) || @@ -481,7 +481,7 @@ private void failIfMethodNotAccessible(MethodElement methodElement) { } private static boolean isExplicitlyAnnotatedAsExecutable(MethodElement methodElement) { - return !getElementAnnotationMetadata(methodElement).hasDeclaredAnnotation(Executable.class); + return !methodElement.getMethodAnnotationMetadata().hasDeclaredAnnotation(Executable.class); } /** @@ -534,7 +534,7 @@ protected boolean visitExecutableMethod(BeanDefinitionVisitor visitor, MethodEle // Synthetic methods cannot be executable as @Executable cannot be put on a field return false; } - if (getElementAnnotationMetadata(methodElement).hasStereotype(Executable.class)) { + if (methodElement.getMethodAnnotationMetadata().hasStereotype(Executable.class)) { // @Executable annotated on the method // Throw error if it cannot be accessed without the reflection if (!methodElement.isAccessible()) { diff --git a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyClassElement.java b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyClassElement.java index bdc7784e851..9113f17e28d 100644 --- a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyClassElement.java +++ b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyClassElement.java @@ -415,6 +415,12 @@ private GroovyPropertyElement mapPropertyElement(Set nativeProps, } AtomicReference ref = new AtomicReference<>(); if (conf.getAccessKinds().contains(BeanProperties.AccessKind.METHOD) && nativeProps.remove(value.propertyName)) { + AnnotationMetadataProvider methodAnnotationMetadataProvider = new AnnotationMetadataProvider() { + @Override + public AnnotationMetadata getAnnotationMetadata() { + return ref.get().getAnnotationMetadata(); + } + }; AnnotationMetadataProvider annotationMetadataProvider = new AnnotationMetadataProvider() { @Override public AnnotationMetadata getAnnotationMetadata() { @@ -432,6 +438,7 @@ public AnnotationMetadata getAnnotationMetadata() { value.getter = MethodElement.of( this, value.field.getDeclaringType(), + methodAnnotationMetadataProvider, annotationMetadataProvider, visitorContext.getAnnotationMetadataBuilder(), value.field.getGenericType(), @@ -449,6 +456,7 @@ public AnnotationMetadata getAnnotationMetadata() { value.setter = MethodElement.of( this, value.field.getDeclaringType(), + methodAnnotationMetadataProvider, annotationMetadataProvider, visitorContext.getAnnotationMetadataBuilder(), PrimitiveElement.VOID, @@ -456,7 +464,7 @@ public AnnotationMetadata getAnnotationMetadata() { NameUtils.setterNameFor(value.propertyName), value.field.isStatic(), value.field.isFinal(), - ParameterElement.of(value.field.getGenericType(), value.propertyName, annotationMetadataProvider, visitorContext.getAnnotationMetadataBuilder()) + ParameterElement.of(value.field.getGenericType(), value.propertyName, methodAnnotationMetadataProvider, visitorContext.getAnnotationMetadataBuilder()) ); value.writeAccessKind = BeanProperties.AccessKind.METHOD; } else if (nativePropertiesOnly) { diff --git a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyMethodElement.java b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyMethodElement.java index b8554879445..69ff3e23e78 100644 --- a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyMethodElement.java +++ b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyMethodElement.java @@ -25,7 +25,10 @@ import io.micronaut.inject.ast.GenericPlaceholderElement; import io.micronaut.inject.ast.MethodElement; import io.micronaut.inject.ast.ParameterElement; +import io.micronaut.inject.ast.annotation.ElementAnnotationMetadata; import io.micronaut.inject.ast.annotation.ElementAnnotationMetadataFactory; +import io.micronaut.inject.ast.annotation.MethodElementAnnotationsHelper; +import io.micronaut.inject.ast.annotation.MutableAnnotationMetadataDelegate; import org.codehaus.groovy.ast.ClassNode; import org.codehaus.groovy.ast.GenericsType; import org.codehaus.groovy.ast.MethodNode; @@ -56,6 +59,7 @@ public class GroovyMethodElement extends AbstractGroovyElement implements Method private ClassElement returnType; @Nullable private ClassElement genericReturnType; + private final MethodElementAnnotationsHelper helper; /** * @param owningType The owning type @@ -72,6 +76,22 @@ public class GroovyMethodElement extends AbstractGroovyElement implements Method super(visitorContext, nativeElement, annotationMetadata); this.methodNode = methodNode; this.owningType = owningType; + this.helper = new MethodElementAnnotationsHelper(this, annotationMetadata); + } + + @Override + protected MutableAnnotationMetadataDelegate getAnnotationMetadataToWrite() { + return helper.getMethodAnnotationMetadata(presetAnnotationMetadata); + } + + @Override + public ElementAnnotationMetadata getMethodAnnotationMetadata() { + return helper.getMethodAnnotationMetadata(presetAnnotationMetadata); + } + + @Override + public AnnotationMetadata getAnnotationMetadata() { + return helper.getAnnotationMetadata(presetAnnotationMetadata); } @Override diff --git a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyPropertyElement.java b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyPropertyElement.java index 790f91bbb1f..83d4d687731 100644 --- a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyPropertyElement.java +++ b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyPropertyElement.java @@ -73,7 +73,7 @@ final class GroovyPropertyElement extends AbstractGroovyElement implements Prope this.writeAccessKind = writeAccessKind; this.owningElement = owningElement; this.excluded = excluded; - this.annotationMetadata = new PropertyElementAnnotationMetadata(this, getter, setter, field, null,false); + this.annotationMetadata = new PropertyElementAnnotationMetadata(this, getter, setter, field, null, false); } @Override diff --git a/inject-groovy/src/test/groovy/io/micronaut/inject/annotation/modify/AnnotatePropertySpec.groovy b/inject-groovy/src/test/groovy/io/micronaut/inject/annotation/modify/AnnotatePropertySpec.groovy new file mode 100644 index 00000000000..2b0cccec46d --- /dev/null +++ b/inject-groovy/src/test/groovy/io/micronaut/inject/annotation/modify/AnnotatePropertySpec.groovy @@ -0,0 +1,263 @@ +package io.micronaut.inject.annotation.modify + +import io.micronaut.ast.groovy.TypeElementVisitorStart +import io.micronaut.ast.transform.test.AbstractBeanDefinitionSpec +import io.micronaut.core.beans.BeanIntrospection +import io.micronaut.inject.ast.ClassElement +import io.micronaut.inject.ast.MethodElement +import io.micronaut.inject.visitor.TypeElementVisitor +import io.micronaut.inject.visitor.VisitorContext + +class AnnotatePropertySpec extends AbstractBeanDefinitionSpec { + + def setup() { + System.setProperty(TypeElementVisitorStart.ELEMENT_VISITORS_PROPERTY, AnnotatePropertyVisitor.name) + } + + void 'test annotating 1'() { + when: + def introspection = buildBeanIntrospection('annotateprop.AnnotatePropertyBean1', ''' +package annotateprop; + +import io.micronaut.core.annotation.Introspected; + +@Introspected +class AnnotatePropertyBean1 { + + private String firstName; + private String lastName; + + public void setFirstName(String firstName) { + this.firstName = firstName; + } + + public String getFirstName() { + return firstName; + } + + public void setLastName(String lastName) { + this.lastName = lastName; + } + + public String getLastName() { + return lastName; + } + +} + +''') + then: + validate(introspection) + } + + void 'test annotating 2'() { + when: + def introspection = buildBeanIntrospection('annotateprop.AnnotatePropertyBean2', ''' +package annotateprop; + +import io.micronaut.core.annotation.Introspected; + +@Introspected +class AnnotatePropertyBean2 { + + private final String firstName; + private final String lastName; + + public AnnotatePropertyBean2(String firstName, String lastName) { + this.firstName = firstName; + this.lastName = lastName; + } + + public String getFirstName() { + return firstName; + } + + public String getLastName() { + return lastName; + } + +} + +''') + then: + validate(introspection) + } + + void 'test annotating 3'() { + when: + def introspection = buildBeanIntrospection('annotateprop.AnnotatePropertyBean2', ''' +package annotateprop; + +import io.micronaut.core.annotation.Introspected; + +@Introspected +class AnnotatePropertyBean2 { + + String firstName + String lastName + +} + +''') + then: + validate(introspection) + } + + void 'test annotating 4'() { + when: + def introspection = buildBeanIntrospection('annotateprop.AnnotatePropertyBean2', ''' +package annotateprop; + +import io.micronaut.core.annotation.Introspected; + +@Introspected +class AnnotatePropertyBean2 { + + final String firstName + final String lastName + +} + +''') + then: + validate(introspection) + } + + void validate(BeanIntrospection introspection) { + def property1 = introspection.getRequiredProperty("firstName", String) + + assert property1.name == "firstName" + assert property1.hasAnnotation(MyAnnotation) + assert property1.asArgument().getAnnotationMetadata().hasAnnotation(MyAnnotation) + def property2 = introspection.getRequiredProperty("lastName", String) + + assert property2.name == "lastName" + assert !property2.hasAnnotation(MyAnnotation) + assert !property2.asArgument().getAnnotationMetadata().hasAnnotation(MyAnnotation) + } + + static class AnnotatePropertyVisitor implements TypeElementVisitor { + + @Override + VisitorKind getVisitorKind() { + return VisitorKind.ISOLATING; + } + + @Override + void visitClass(ClassElement classElement, VisitorContext context) { + if (classElement.getSimpleName().startsWith("AnnotatePropertyBean")) { + + def properties = classElement.getBeanProperties() + assert properties.size() == 2 + + def property1 = properties[0] + assert property1.name == "firstName" + + assert property1.getAnnotationMetadata().getAnnotationNames().isEmpty() + assert property1.type.getAnnotationMetadata().getAnnotationNames().isEmpty() + assert property1.genericType.getAnnotationMetadata().getAnnotationNames().isEmpty() + + property1.annotate(MyAnnotation) + + assert property1.hasAnnotation(MyAnnotation.class.name) + assert property1.getAnnotationMetadata().getAnnotationNames().asList() == [MyAnnotation.class.name] + assert property1.type.getAnnotationMetadata().getAnnotationNames().isEmpty() + assert property1.genericType.getAnnotationMetadata().getAnnotationNames().isEmpty() + + def field1 = property1.getField().orElse(null) + if (field1) { + assert field1.getAnnotationNames().asList() == [MyAnnotation.class.name] + assert field1.type.getAnnotationMetadata().getAnnotationNames().isEmpty() + assert field1.genericType.getAnnotationMetadata().getAnnotationNames().isEmpty() + } + def getter1 = property1.getReadMember().orElse(null) as MethodElement + if (getter1) { + assert getter1.getMethodAnnotationMetadata().getAnnotationNames().asList() == [MyAnnotation.class.name] + assert getter1.hasAnnotation(MyAnnotation.class.name) + assert getter1.returnType.getAnnotationMetadata().getAnnotationNames().isEmpty() + assert getter1.genericReturnType.getAnnotationMetadata().getAnnotationNames().isEmpty() + } + def setter1 = property1.getWriteMember().orElse(null) as MethodElement + if (setter1) { + assert setter1.getMethodAnnotationMetadata().getAnnotationNames().asList() == [MyAnnotation.class.name] + assert setter1.hasAnnotation(MyAnnotation.class.name) + assert setter1.returnType.getAnnotationMetadata().getAnnotationNames().isEmpty() + assert setter1.genericReturnType.getAnnotationMetadata().getAnnotationNames().isEmpty() + + def parameter = setter1.parameters[0] + // TODO: delegate to the parameter +// assert parameter.getAnnotationNames().asList() == [MyAnnotation.class.name] +// assert parameter.getAnnotationNames().isEmpty() + assert parameter.type.getAnnotationMetadata().getAnnotationNames().isEmpty() + assert parameter.genericType.getAnnotationMetadata().getAnnotationNames().isEmpty() + } + + def property2 = properties[1] + assert property2.name == "lastName" + assert property2.getAnnotationMetadata().getAnnotationNames().isEmpty() + assert property2.type.getAnnotationMetadata().getAnnotationNames().isEmpty() + assert property2.genericType.getAnnotationMetadata().getAnnotationNames().isEmpty() + + def field2 = property2.getField().orElse(null) + if (field2) { + assert field2.getAnnotationMetadata().getAnnotationNames().isEmpty() + assert field2.type.getAnnotationMetadata().getAnnotationNames().isEmpty() + assert field2.genericType.getAnnotationMetadata().getAnnotationNames().isEmpty() + } + def getter2 = property2.getReadMember().orElse(null) as MethodElement + if (getter2) { + assert getter2.getMethodAnnotationMetadata().getAnnotationNames().isEmpty() + assert getter2.returnType.getAnnotationMetadata().getAnnotationNames().isEmpty() + assert getter2.genericReturnType.getAnnotationMetadata().getAnnotationNames().isEmpty() + } + def setter2 = property2.getWriteMember().orElse(null) as MethodElement + if (setter2) { + assert setter2.getMethodAnnotationMetadata().isEmpty() + assert setter2.returnType.getAnnotationMetadata().getAnnotationNames().isEmpty() + assert setter2.genericReturnType.getAnnotationMetadata().getAnnotationNames().isEmpty() + + def parameter = setter2.parameters[0] + assert parameter.getAnnotationNames().isEmpty() + assert parameter.type.getAnnotationMetadata().getAnnotationNames().isEmpty() + assert parameter.genericType.getAnnotationMetadata().getAnnotationNames().isEmpty() + } + + // Validate the cache is working + + def newClassElement = context.getClassElement(classElement.getName()).get() + def newProperty = newClassElement.getBeanProperties()[0] + assert newProperty.getAnnotationMetadata().getAnnotationNames().asList() == [MyAnnotation.class.name] + assert newProperty.type.getAnnotationMetadata().getAnnotationNames().isEmpty() + assert newProperty.genericType.getAnnotationMetadata().getAnnotationNames().isEmpty() + def field1new = newProperty.getField().orElse(null) + if (field1new) { + assert field1new.getAnnotationMetadata().getAnnotationNames().asList() == [MyAnnotation.class.name] + assert field1new.type.getAnnotationMetadata().getAnnotationNames().isEmpty() + assert field1new.genericType.getAnnotationMetadata().getAnnotationNames().isEmpty() + } + def getter1new = newProperty.getReadMember().orElse(null) as MethodElement + if (getter1new) { + assert getter1new.getMethodAnnotationMetadata().getAnnotationNames().asList() == [MyAnnotation.class.name] + assert getter1new.hasAnnotation(MyAnnotation.class.name) + assert getter1new.returnType.getAnnotationMetadata().getAnnotationNames().isEmpty() + assert getter1new.genericReturnType.getAnnotationMetadata().getAnnotationNames().isEmpty() + } + def setter1new = newProperty.getWriteMember().orElse(null) as MethodElement + if (setter1new) { + assert setter1new.getMethodAnnotationMetadata().getAnnotationNames().asList() == [MyAnnotation.class.name] + assert setter1new.hasAnnotation(MyAnnotation.class.name) + assert setter1new.returnType.getAnnotationMetadata().getAnnotationNames().isEmpty() + assert setter1new.genericReturnType.getAnnotationMetadata().getAnnotationNames().isEmpty() + + def parameter = setter1new.parameters[0] + // TODO: delegate to the parameter +// assert parameter.getAnnotationNames().asList() == [MyAnnotation.class.name] +// assert parameter.getAnnotationNames().isEmpty() + assert parameter.type.getAnnotationMetadata().getAnnotationNames().isEmpty() + assert parameter.genericType.getAnnotationMetadata().getAnnotationNames().isEmpty() + } + } + } + } + +} diff --git a/inject-java/src/main/java/io/micronaut/annotation/processing/JavaElementAnnotationMetadataFactory.java b/inject-java/src/main/java/io/micronaut/annotation/processing/JavaElementAnnotationMetadataFactory.java index cb0724413ed..6f29406d78e 100644 --- a/inject-java/src/main/java/io/micronaut/annotation/processing/JavaElementAnnotationMetadataFactory.java +++ b/inject-java/src/main/java/io/micronaut/annotation/processing/JavaElementAnnotationMetadataFactory.java @@ -17,7 +17,6 @@ import io.micronaut.annotation.processing.visitor.AbstractJavaElement; import io.micronaut.annotation.processing.visitor.JavaNativeElement; -import io.micronaut.core.annotation.AnnotationMetadata; import io.micronaut.inject.annotation.AbstractAnnotationMetadataBuilder; import io.micronaut.inject.ast.ClassElement; import io.micronaut.inject.ast.GenericPlaceholderElement; @@ -65,17 +64,6 @@ private static boolean allowedAnnotations(AbstractJavaElement javaElement) { return javaElement.getNativeType().element() != null; } - @Override - public ElementAnnotationMetadata build(io.micronaut.inject.ast.Element element, AnnotationMetadata defaultAnnotationMetadata) { - if (defaultAnnotationMetadata == null) { - AbstractJavaElement javaElement = (AbstractJavaElement) element; - if (!allowedAnnotations(javaElement)) { - return EMPTY; - } - } - return super.build(element, defaultAnnotationMetadata); - } - @Override protected Element getNativeElement(io.micronaut.inject.ast.Element element) { return ((AbstractJavaElement) element).getNativeType().element(); diff --git a/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaMethodElement.java b/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaMethodElement.java index f789cde4d80..99c9c109a16 100644 --- a/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaMethodElement.java +++ b/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaMethodElement.java @@ -26,7 +26,10 @@ import io.micronaut.inject.ast.MethodElement; import io.micronaut.inject.ast.ParameterElement; import io.micronaut.inject.ast.PrimitiveElement; +import io.micronaut.inject.ast.annotation.ElementAnnotationMetadata; import io.micronaut.inject.ast.annotation.ElementAnnotationMetadataFactory; +import io.micronaut.inject.ast.annotation.MethodElementAnnotationsHelper; +import io.micronaut.inject.ast.annotation.MutableAnnotationMetadataDelegate; import javax.lang.model.element.Element; import javax.lang.model.element.ExecutableElement; @@ -61,6 +64,7 @@ public class JavaMethodElement extends AbstractJavaElement implements MethodElem private ClassElement returnType; private Map typeArguments; private Map declaredTypeArguments; + private final MethodElementAnnotationsHelper helper; /** * @param owningType The declaring class @@ -75,6 +79,22 @@ public JavaMethodElement(JavaClassElement owningType, super(nativeElement, annotationMetadataFactory, visitorContext); this.executableElement = nativeElement.element(); this.owningType = owningType; + this.helper = new MethodElementAnnotationsHelper(this, annotationMetadataFactory); + } + + @Override + protected MutableAnnotationMetadataDelegate getAnnotationMetadataToWrite() { + return helper.getMethodAnnotationMetadata(presetAnnotationMetadata); + } + + @Override + public ElementAnnotationMetadata getMethodAnnotationMetadata() { + return helper.getMethodAnnotationMetadata(presetAnnotationMetadata); + } + + @Override + public AnnotationMetadata getAnnotationMetadata() { + return helper.getAnnotationMetadata(presetAnnotationMetadata); } @Override diff --git a/inject-java/src/test/groovy/io/micronaut/annotation/AnnotatePropertySpec.groovy b/inject-java/src/test/groovy/io/micronaut/annotation/AnnotatePropertySpec.groovy new file mode 100644 index 00000000000..20f6d626707 --- /dev/null +++ b/inject-java/src/test/groovy/io/micronaut/annotation/AnnotatePropertySpec.groovy @@ -0,0 +1,218 @@ +package io.micronaut.annotation + +import io.micronaut.annotation.processing.test.AbstractTypeElementSpec +import io.micronaut.core.beans.BeanIntrospection +import io.micronaut.inject.ast.ClassElement +import io.micronaut.inject.ast.MethodElement +import io.micronaut.inject.visitor.TypeElementVisitor +import io.micronaut.inject.visitor.VisitorContext + +class AnnotatePropertySpec extends AbstractTypeElementSpec { + + void 'test annotating 1'() { + when: + def introspection = buildBeanIntrospection('annotateprop.AnnotatePropertyBean1', ''' +package annotateprop; + +import io.micronaut.core.annotation.Introspected; + +@Introspected +class AnnotatePropertyBean1 { + + private String firstName; + private String lastName; + + public void setFirstName(String firstName) { + this.firstName = firstName; + } + + public String getFirstName() { + return firstName; + } + + public void setLastName(String lastName) { + this.lastName = lastName; + } + + public String getLastName() { + return lastName; + } + +} + +''') + then: + validate(introspection) + } + + void 'test annotating 2'() { + when: + def introspection = buildBeanIntrospection('annotateprop.AnnotatePropertyBean2', ''' +package annotateprop; + +import io.micronaut.core.annotation.Introspected; + +@Introspected +class AnnotatePropertyBean2 { + + private final String firstName; + private final String lastName; + + public AnnotatePropertyBean2(String firstName, String lastName) { + this.firstName = firstName; + this.lastName = lastName; + } + + public String getFirstName() { + return firstName; + } + + public String getLastName() { + return lastName; + } + +} + +''') + then: + validate(introspection) + } + + void validate(BeanIntrospection introspection) { + def property1 = introspection.getRequiredProperty("firstName", String) + + assert property1.name == "firstName" + assert property1.hasAnnotation(MyAnnotation) + assert property1.asArgument().getAnnotationMetadata().hasAnnotation(MyAnnotation) + def property2 = introspection.getRequiredProperty("lastName", String) + + assert property2.name == "lastName" + assert !property2.hasAnnotation(MyAnnotation) + assert !property2.asArgument().getAnnotationMetadata().hasAnnotation(MyAnnotation) + } + + static class AnnotatePropertyVisitor implements TypeElementVisitor { + + @Override + VisitorKind getVisitorKind() { + return VisitorKind.ISOLATING; + } + + @Override + void visitClass(ClassElement classElement, VisitorContext context) { + if (classElement.getSimpleName().startsWith("AnnotatePropertyBean")) { + + def properties = classElement.getBeanProperties() + assert properties.size() == 2 + + def property1 = properties[0] + assert property1.name == "firstName" + + assert property1.getAnnotationMetadata().getAnnotationNames().isEmpty() + assert property1.type.getAnnotationMetadata().getAnnotationNames().isEmpty() + assert property1.genericType.getAnnotationMetadata().getAnnotationNames().isEmpty() + + property1.annotate(MyAnnotation) + + assert property1.hasAnnotation(MyAnnotation.class.name) + assert property1.getAnnotationMetadata().getAnnotationNames().asList() == [MyAnnotation.class.name] + assert property1.type.getAnnotationMetadata().getAnnotationNames().isEmpty() + assert property1.genericType.getAnnotationMetadata().getAnnotationNames().isEmpty() + + def field1 = property1.getField().orElse(null) + if (field1) { + assert field1.getAnnotationNames().asList() == [MyAnnotation.class.name] + assert field1.type.getAnnotationMetadata().getAnnotationNames().isEmpty() + assert field1.genericType.getAnnotationMetadata().getAnnotationNames().isEmpty() + } + def getter1 = property1.getReadMember().orElse(null) as MethodElement + if (getter1) { + assert getter1.getMethodAnnotationMetadata().getAnnotationNames().asList() == [MyAnnotation.class.name] + assert getter1.hasAnnotation(MyAnnotation.class.name) + assert getter1.returnType.getAnnotationMetadata().getAnnotationNames().isEmpty() + assert getter1.genericReturnType.getAnnotationMetadata().getAnnotationNames().isEmpty() + } + def setter1 = property1.getWriteMember().orElse(null) as MethodElement + if (setter1) { + assert setter1.getMethodAnnotationMetadata().getAnnotationNames().asList() == [MyAnnotation.class.name] + assert setter1.hasAnnotation(MyAnnotation.class.name) + assert setter1.returnType.getAnnotationMetadata().getAnnotationNames().isEmpty() + assert setter1.genericReturnType.getAnnotationMetadata().getAnnotationNames().isEmpty() + + def parameter = setter1.parameters[0] + // TODO: delegate to the parameter +// assert parameter.getAnnotationNames().asList() == [MyAnnotation.class.name] + assert parameter.getAnnotationNames().isEmpty() + assert parameter.type.getAnnotationMetadata().getAnnotationNames().isEmpty() + assert parameter.genericType.getAnnotationMetadata().getAnnotationNames().isEmpty() + } + + def property2 = properties[1] + assert property2.name == "lastName" + assert property2.getAnnotationMetadata().getAnnotationNames().isEmpty() + assert property2.type.getAnnotationMetadata().getAnnotationNames().isEmpty() + assert property2.genericType.getAnnotationMetadata().getAnnotationNames().isEmpty() + + def field2 = property2.getField().orElse(null) + if (field2) { + assert field2.getAnnotationMetadata().getAnnotationNames().isEmpty() + assert field2.type.getAnnotationMetadata().getAnnotationNames().isEmpty() + assert field2.genericType.getAnnotationMetadata().getAnnotationNames().isEmpty() + } + def getter2 = property2.getReadMember().orElse(null) as MethodElement + if (getter2) { + assert getter2.getMethodAnnotationMetadata().getAnnotationNames().isEmpty() + assert getter2.returnType.getAnnotationMetadata().getAnnotationNames().isEmpty() + assert getter2.genericReturnType.getAnnotationMetadata().getAnnotationNames().isEmpty() + } + def setter2 = property2.getWriteMember().orElse(null) as MethodElement + if (setter2) { + assert setter2.getMethodAnnotationMetadata().isEmpty() + assert setter2.returnType.getAnnotationMetadata().getAnnotationNames().isEmpty() + assert setter2.genericReturnType.getAnnotationMetadata().getAnnotationNames().isEmpty() + + def parameter = setter2.parameters[0] + assert parameter.getAnnotationNames().isEmpty() + assert parameter.type.getAnnotationMetadata().getAnnotationNames().isEmpty() + assert parameter.genericType.getAnnotationMetadata().getAnnotationNames().isEmpty() + } + + // Validate the cache is working + + def newClassElement = context.getClassElement(classElement.getName()).get() + def newProperty = newClassElement.getBeanProperties()[0] + assert newProperty.getAnnotationMetadata().getAnnotationNames().asList() == [MyAnnotation.class.name] + assert newProperty.type.getAnnotationMetadata().getAnnotationNames().isEmpty() + assert newProperty.genericType.getAnnotationMetadata().getAnnotationNames().isEmpty() + def field1new = newProperty.getField().orElse(null) + if (field1new) { + assert field1new.getAnnotationMetadata().getAnnotationNames().asList() == [MyAnnotation.class.name] + assert field1new.type.getAnnotationMetadata().getAnnotationNames().isEmpty() + assert field1new.genericType.getAnnotationMetadata().getAnnotationNames().isEmpty() + } + def getter1new = newProperty.getReadMember().orElse(null) as MethodElement + if (getter1new) { + assert getter1new.getMethodAnnotationMetadata().getAnnotationNames().asList() == [MyAnnotation.class.name] + assert getter1new.hasAnnotation(MyAnnotation.class.name) + assert getter1new.returnType.getAnnotationMetadata().getAnnotationNames().isEmpty() + assert getter1new.genericReturnType.getAnnotationMetadata().getAnnotationNames().isEmpty() + } + def setter1new = newProperty.getWriteMember().orElse(null) as MethodElement + if (setter1new) { + assert setter1new.getMethodAnnotationMetadata().getAnnotationNames().asList() == [MyAnnotation.class.name] + assert setter1new.hasAnnotation(MyAnnotation.class.name) + assert setter1new.returnType.getAnnotationMetadata().getAnnotationNames().isEmpty() + assert setter1new.genericReturnType.getAnnotationMetadata().getAnnotationNames().isEmpty() + + def parameter = setter1new.parameters[0] + // TODO: delegate to the parameter +// assert parameter.getAnnotationNames().asList() == [MyAnnotation.class.name] + assert parameter.getAnnotationNames().isEmpty() + assert parameter.type.getAnnotationMetadata().getAnnotationNames().isEmpty() + assert parameter.genericType.getAnnotationMetadata().getAnnotationNames().isEmpty() + } + } + } + } + +} diff --git a/inject-java/src/test/resources/META-INF/services/io.micronaut.inject.visitor.TypeElementVisitor b/inject-java/src/test/resources/META-INF/services/io.micronaut.inject.visitor.TypeElementVisitor index 1fbf5b74cf0..825b5a97fe8 100644 --- a/inject-java/src/test/resources/META-INF/services/io.micronaut.inject.visitor.TypeElementVisitor +++ b/inject-java/src/test/resources/META-INF/services/io.micronaut.inject.visitor.TypeElementVisitor @@ -15,3 +15,4 @@ io.micronaut.annotation.AnnotateMethodReturnSpec$AnnotateMethodReturnVisitor io.micronaut.annotation.AnnotateFieldSpec$AnnotationFieldVisitor io.micronaut.annotation.AnnotateFieldTypeSpec$AnnotateFieldTypeVisitor io.micronaut.annotation.AnnotateMethodParameterSpec$AnnotateMethodParameterVisitor +io.micronaut.annotation.AnnotatePropertySpec$AnnotatePropertyVisitor diff --git a/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/annotation/KotlinElementAnnotationMetadataFactory.kt b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/annotation/KotlinElementAnnotationMetadataFactory.kt index 822311a32e1..756b6224ace 100644 --- a/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/annotation/KotlinElementAnnotationMetadataFactory.kt +++ b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/annotation/KotlinElementAnnotationMetadataFactory.kt @@ -72,7 +72,7 @@ internal class KotlinElementAnnotationMetadataFactory( private fun buildTypeAnnotationsForTypeArgument( element: KotlinTypeArgumentElement ): AbstractElementAnnotationMetadata { - return object : AbstractElementAnnotationMetadata(null) { + return object : AbstractElementAnnotationMetadata() { override fun lookup(): CachedAnnotationMetadata { if (element.genericNativeType.owner == null) { diff --git a/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/AbstractKotlinMethodElement.kt b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/AbstractKotlinMethodElement.kt index 5bf8a1772ee..8ad2e7a7115 100644 --- a/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/AbstractKotlinMethodElement.kt +++ b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/AbstractKotlinMethodElement.kt @@ -26,7 +26,10 @@ import io.micronaut.inject.ast.GenericPlaceholderElement import io.micronaut.inject.ast.MemberElement import io.micronaut.inject.ast.MethodElement import io.micronaut.inject.ast.ParameterElement +import io.micronaut.inject.ast.annotation.ElementAnnotationMetadata import io.micronaut.inject.ast.annotation.ElementAnnotationMetadataFactory +import io.micronaut.inject.ast.annotation.MethodElementAnnotationsHelper +import io.micronaut.inject.ast.annotation.MutableAnnotationMetadataDelegate internal abstract class AbstractKotlinMethodElement( private val nativeType: T, @@ -42,6 +45,19 @@ internal abstract class AbstractKotlinMethodElement( abstract val internalGenericReturnType: ClassElement abstract val resolvedParameters: List + private val methodHelper by lazy { + MethodElementAnnotationsHelper(this, annotationMetadataFactory) + } + + override fun getMethodAnnotationMetadata(): ElementAnnotationMetadata = + methodHelper.getMethodAnnotationMetadata(presetAnnotationMetadata) + + override fun getAnnotationMetadataToWrite(): MutableAnnotationMetadataDelegate<*> = + methodHelper.getMethodAnnotationMetadata(presetAnnotationMetadata) + + override fun getAnnotationMetadata(): AnnotationMetadata = + methodHelper.getAnnotationMetadata(presetAnnotationMetadata) + override fun getModifiers() = super.getModifiers() override fun getDeclaredTypeArguments() = internalDeclaredTypeArguments diff --git a/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/AbstractKotlinPropertyElement.kt b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/AbstractKotlinPropertyElement.kt index 071dc204120..7c9c4b5e836 100644 --- a/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/AbstractKotlinPropertyElement.kt +++ b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/AbstractKotlinPropertyElement.kt @@ -86,6 +86,8 @@ internal abstract class AbstractKotlinPropertyElement( override fun getAnnotationMetadata() = internalAnnotationMetadata + override fun getAnnotationMetadataToWrite() = internalAnnotationMetadata + override fun getField() = fieldElement override fun getName() = name diff --git a/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/KotlinClassElement.kt b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/KotlinClassElement.kt index 3a21bae9d47..165bb981fb2 100644 --- a/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/KotlinClassElement.kt +++ b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/KotlinClassElement.kt @@ -465,22 +465,38 @@ internal open class KotlinClassElement( @OptIn(KspExperimental::class) override fun isAssignable(type: String): Boolean { - var ksType = visitorContext.resolver.getClassDeclarationByName(type)?.asStarProjectedType() - if (ksType != null) { - if (ksType.isAssignableFrom(kotlinType)) { + val otherDeclaration = visitorContext.resolver.getClassDeclarationByName(type) + if (otherDeclaration != null) { + if (declaration == otherDeclaration) { return true } - val kotlinName = visitorContext.resolver.mapJavaNameToKotlin( - visitorContext.resolver.getKSNameFromString(type) + val thisFullName = declaration.getBinaryName( + visitorContext.resolver, + visitorContext ) - if (kotlinName != null) { - ksType = - visitorContext.resolver.getKotlinClassByName(kotlinName)?.asStarProjectedType() - if (ksType != null && kotlinType.starProjection().isAssignableFrom(ksType)) { - return true - } + val otherFullName = otherDeclaration.getBinaryName( + visitorContext.resolver, + visitorContext + ) + if (thisFullName == otherFullName) { + return true + } + val otherKotlinType = otherDeclaration.asStarProjectedType() + if (otherKotlinType == kotlinType) { + return true + } + if (otherKotlinType.isAssignableFrom(kotlinType)) { + return true + } + } + val kotlinName = visitorContext.resolver.mapJavaNameToKotlin( + visitorContext.resolver.getKSNameFromString(type) + ) + if (kotlinName != null) { + val kotlinClassByName = visitorContext.resolver.getKotlinClassByName(kotlinName) + if (kotlinClassByName != null && kotlinType.starProjection().isAssignableFrom(kotlinClassByName.asStarProjectedType())) { + return true } - return false } return false } diff --git a/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/KotlinMethodElement.kt b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/KotlinMethodElement.kt index 3155a223624..2d231199453 100644 --- a/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/KotlinMethodElement.kt +++ b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/KotlinMethodElement.kt @@ -53,7 +53,7 @@ internal open class KotlinMethodElement( } override val internalDeclaredTypeArguments: Map by lazy { - resolveTypeArguments(nativeType, declaration, emptyMap()) + resolveTypeArguments(nativeType, declaration, declaringType.typeArguments) } override val resolvedParameters: List by lazy { diff --git a/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/LoadedVisitor.kt b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/LoadedVisitor.kt index 8efb856fc50..5087670c9fa 100644 --- a/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/LoadedVisitor.kt +++ b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/LoadedVisitor.kt @@ -21,6 +21,7 @@ import com.google.devtools.ksp.symbol.KSType import io.micronaut.core.annotation.AnnotationMetadata import io.micronaut.core.order.Ordered import io.micronaut.core.reflect.GenericTypeUtils +import io.micronaut.inject.processing.ProcessingException import io.micronaut.inject.visitor.TypeElementVisitor import java.util.* @@ -47,10 +48,20 @@ internal class LoadedVisitor( .map { it.resolve() } .find { it.declaration.qualifiedName?.asString() == tevClassName - }!! - classAnnotation = getType(reference.arguments[0].type!!.resolve(), visitor.classType) - elementAnnotation = - getType(reference.arguments[1].type!!.resolve(), visitor.elementType) + } ?: throw ProcessingException( + visitorContext.elementFactory.newClassElement(declaration), + "The visitor [$declaration] doesn't implement $tevClassName" + ) + val classArgument = reference.arguments[0].type ?: throw ProcessingException( + visitorContext.elementFactory.newClassElement(declaration), + "Cannot determine the class type argument of the visitor: $declaration" + ) + val elementArgument = reference.arguments[1].type ?: throw ProcessingException( + visitorContext.elementFactory.newClassElement(declaration), + "Cannot determine the element type argument of the visitor: $declaration" + ) + classAnnotation = getType(classArgument.resolve(), visitor.classType) + elementAnnotation = getType(elementArgument.resolve(), visitor.elementType) } else { val classes = GenericTypeUtils.resolveInterfaceTypeArguments( javaClass, diff --git a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/annotations/AnnotateMethodReturnSpec.groovy b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/annotations/AnnotateMethodReturnSpec.groovy index d35697766d8..ed8ce11b601 100644 --- a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/annotations/AnnotateMethodReturnSpec.groovy +++ b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/annotations/AnnotateMethodReturnSpec.groovy @@ -245,6 +245,7 @@ class MyBean1(var name: String) } private static void validateBeanType(ClassElement bean) { + assert bean.isAssignable("addann.MyBean1") assert bean.getAnnotationMetadata().isEmpty() assert bean.getTypeAnnotationMetadata().isEmpty() assert bean.getMethods().size() == 0 diff --git a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/annotations/AnnotatePropertySpec.groovy b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/annotations/AnnotatePropertySpec.groovy new file mode 100644 index 00000000000..ea34a4da1b6 --- /dev/null +++ b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/annotations/AnnotatePropertySpec.groovy @@ -0,0 +1,174 @@ +package io.micronaut.kotlin.processing.annotations + +import io.micronaut.annotation.processing.test.AbstractKotlinCompilerSpec +import io.micronaut.core.beans.BeanIntrospection +import io.micronaut.inject.ast.ClassElement +import io.micronaut.inject.ast.MethodElement +import io.micronaut.inject.visitor.TypeElementVisitor +import io.micronaut.inject.visitor.VisitorContext + +class AnnotatePropertySpec extends AbstractKotlinCompilerSpec { + + void 'test annotating 1'() { + when: + def introspection = buildBeanIntrospection('annotateprop.AnnotatePropertyBean1', ''' +package annotateprop; + +import io.micronaut.core.annotation.Introspected + + +@Introspected +class AnnotatePropertyBean1(val firstName: String, val lastName: String) + +''') + then: + validate(introspection) + } + + void 'test annotating 2'() { + when: + def introspection = buildBeanIntrospection('annotateprop.AnnotatePropertyBean2', ''' +package annotateprop; + +import io.micronaut.core.annotation.Introspected + + +@Introspected +class AnnotatePropertyBean2(var firstName: String, var lastName: String) + +''') + then: + validate(introspection) + } + + void validate(BeanIntrospection introspection) { + def property1 = introspection.getRequiredProperty("firstName", String) + + assert property1.name == "firstName" + assert property1.hasAnnotation(MyAnnotation) + assert property1.asArgument().getAnnotationMetadata().hasAnnotation(MyAnnotation) + def property2 = introspection.getRequiredProperty("lastName", String) + + assert property2.name == "lastName" + assert !property2.hasAnnotation(MyAnnotation) + assert !property2.asArgument().getAnnotationMetadata().hasAnnotation(MyAnnotation) + } + + static class AnnotatePropertyVisitor implements TypeElementVisitor { + @Override + void visitClass(ClassElement classElement, VisitorContext context) { + if (classElement.getSimpleName().startsWith("AnnotatePropertyBean")) { + + def properties = classElement.getBeanProperties() + assert properties.size() == 2 + + def property1 = properties[0] + assert property1.name == "firstName" + + assert property1.getAnnotationMetadata().getAnnotationNames().isEmpty() + assert property1.type.getAnnotationMetadata().getAnnotationNames().isEmpty() + assert property1.genericType.getAnnotationMetadata().getAnnotationNames().isEmpty() + + property1.annotate(MyAnnotation) + + assert property1.getAnnotationMetadata().getAnnotationNames().asList() == [MyAnnotation.class.name] + assert property1.type.getAnnotationMetadata().getAnnotationNames().isEmpty() + assert property1.genericType.getAnnotationMetadata().getAnnotationNames().isEmpty() + + def field1 = property1.getField().orElse(null) + if (field1) { + assert field1.getAnnotationNames().asList() == [MyAnnotation.class.name] + assert field1.type.getAnnotationMetadata().getAnnotationNames().isEmpty() + assert field1.genericType.getAnnotationMetadata().getAnnotationNames().isEmpty() + } + def getter1 = property1.getReadMember().orElse(null) as MethodElement + if (getter1) { + assert getter1.getMethodAnnotationMetadata().getAnnotationNames().asList() == [MyAnnotation.class.name] + assert getter1.hasAnnotation(MyAnnotation.class.name) + assert getter1.returnType.getAnnotationMetadata().getAnnotationNames().isEmpty() + assert getter1.genericReturnType.getAnnotationMetadata().getAnnotationNames().isEmpty() + } + def setter1 = property1.getWriteMember().orElse(null) as MethodElement + if (setter1) { + assert setter1.getMethodAnnotationMetadata().getAnnotationNames().asList() == [MyAnnotation.class.name] + assert setter1.hasAnnotation(MyAnnotation.class.name) + assert setter1.returnType.getAnnotationMetadata().getAnnotationNames().isEmpty() + assert setter1.genericReturnType.getAnnotationMetadata().getAnnotationNames().isEmpty() + + def parameter = setter1.parameters[0] + // TODO: delegate to the parameter +// assert parameter.getAnnotationNames().asList() == [MyAnnotation.class.name] + assert parameter.getAnnotationNames().isEmpty() + assert parameter.type.getAnnotationMetadata().getAnnotationNames().isEmpty() + assert parameter.genericType.getAnnotationMetadata().getAnnotationNames().isEmpty() + } + + def property2 = properties[1] + assert property2.name == "lastName" + assert property2.getAnnotationMetadata().getAnnotationNames().isEmpty() + assert property2.type.getAnnotationMetadata().getAnnotationNames().isEmpty() + assert property2.genericType.getAnnotationMetadata().getAnnotationNames().isEmpty() + + def field2 = property2.getField().orElse(null) + if (field2) { + assert field2.getAnnotationMetadata().getAnnotationNames().isEmpty() + assert field2.type.getAnnotationMetadata().getAnnotationNames().isEmpty() + assert field2.genericType.getAnnotationMetadata().getAnnotationNames().isEmpty() + } + def getter2 = property2.getReadMember().orElse(null) as MethodElement + if (getter2) { + assert getter2.getMethodAnnotationMetadata().getAnnotationNames().isEmpty() + assert getter2.returnType.getAnnotationMetadata().getAnnotationNames().isEmpty() + assert getter2.genericReturnType.getAnnotationMetadata().getAnnotationNames().isEmpty() + } + def setter2 = property2.getWriteMember().orElse(null) as MethodElement + if (setter2) { + assert setter2.getMethodAnnotationMetadata().isEmpty() + assert setter2.returnType.getAnnotationMetadata().getAnnotationNames().isEmpty() + assert setter2.genericReturnType.getAnnotationMetadata().getAnnotationNames().isEmpty() + + def parameter = setter2.parameters[0] + assert parameter.getAnnotationNames().isEmpty() + assert parameter.type.getAnnotationMetadata().getAnnotationNames().isEmpty() + assert parameter.genericType.getAnnotationMetadata().getAnnotationNames().isEmpty() + } + + // Validate the cache is working + + def newClassElement = context.getClassElement(classElement.getName()).get() + def newProperty = newClassElement.getBeanProperties()[0] + assert newProperty.getAnnotationMetadata().getAnnotationNames().asList() == [MyAnnotation.class.name] + assert newProperty.type.getAnnotationMetadata().getAnnotationNames().isEmpty() + assert newProperty.genericType.getAnnotationMetadata().getAnnotationNames().isEmpty() + def field1new = newProperty.getField().orElse(null) + if (field1new) { + assert field1new.getAnnotationMetadata().getAnnotationNames().asList() == [MyAnnotation.class.name] + assert field1new.type.getAnnotationMetadata().getAnnotationNames().isEmpty() + assert field1new.genericType.getAnnotationMetadata().getAnnotationNames().isEmpty() + } + def getter1new = newProperty.getReadMember().orElse(null) as MethodElement + if (getter1new) { + assert getter1new.getMethodAnnotationMetadata().getAnnotationNames().asList() == [MyAnnotation.class.name] + assert getter1new.hasAnnotation(MyAnnotation.class.name) + assert getter1new.returnType.getAnnotationMetadata().getAnnotationNames().isEmpty() + assert getter1new.genericReturnType.getAnnotationMetadata().getAnnotationNames().isEmpty() + } + def setter1new = newProperty.getWriteMember().orElse(null) as MethodElement + if (setter1new) { + assert setter1new.getMethodAnnotationMetadata().getAnnotationNames().asList() == [MyAnnotation.class.name] + assert setter1new.hasAnnotation(MyAnnotation.class.name) + assert setter1new.returnType.getAnnotationMetadata().getAnnotationNames().isEmpty() + assert setter1new.genericReturnType.getAnnotationMetadata().getAnnotationNames().isEmpty() + + def parameter = setter1new.parameters[0] + // TODO: delegate to the parameter +// assert parameter.getAnnotationNames().asList() == [MyAnnotation.class.name] + assert parameter.getAnnotationNames().isEmpty() + assert parameter.type.getAnnotationMetadata().getAnnotationNames().isEmpty() + assert parameter.genericType.getAnnotationMetadata().getAnnotationNames().isEmpty() + } + } + } + } + +} diff --git a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/inject/ast/ClassElementSpec.groovy b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/inject/ast/ClassElementSpec.groovy index 04fda28ebdf..ca02ec6758b 100644 --- a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/inject/ast/ClassElementSpec.groovy +++ b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/inject/ast/ClassElementSpec.groovy @@ -1,6 +1,5 @@ package io.micronaut.kotlin.processing.inject.ast - import io.micronaut.annotation.processing.test.AbstractKotlinCompilerSpec import io.micronaut.core.annotation.AnnotationUtil import io.micronaut.core.annotation.Introspected @@ -1639,8 +1638,10 @@ package test import io.micronaut.context.annotation.Prototype import java.util.List -class MyRepo : Repo -interface Repo : GenericRepository +interface MyRepo : Repo +interface Repo : GenericRepository { + fun save(ent: E) +} interface GenericRepository @Prototype class MyBean { @@ -1657,10 +1658,178 @@ class MyBean { repo.get("E").getMethods().size() == 0 repo.get("E").getFields().size() == 1 repo.get("E").getFields().get(0).name == "name" + def element = ce.findMethod("save").get().getParameters()[0] + element.getGenericType().simpleName == "MyBean" + element.getType().simpleName == "Object" + when: + def genRepo = ce.getTypeArguments("test.GenericRepository") + then: + genRepo.get("E").simpleName == "MyBean" + genRepo.get("E").getSyntheticBeanProperties().size() == 1 + genRepo.get("E").getMethods().size() == 0 + genRepo.get("E").getFields().get(0).name == "name" + } + + void "test interface placeholder 2"() { + ClassElement ce = buildClassElement('test.MyRepoX', ''' +package test +import io.micronaut.context.annotation.Prototype +import java.util.List + +interface MyRepoX : RepoX +interface RepoX : GenericRepository { + fun save(ent: S) +} +interface GenericRepository +@Prototype +class MyBeanX { + var name: String? = null +} + +''') + + when: + def repo = ce.getTypeArguments("test.RepoX") + then: + repo.get("E").simpleName == "MyBeanX" + repo.get("E").getSyntheticBeanProperties().size() == 1 + repo.get("E").getMethods().size() == 0 + repo.get("E").getFields().size() == 1 + repo.get("E").getFields().get(0).name == "name" + def element = ce.findMethod("save").get().getParameters()[0] + element.getGenericType().simpleName == "MyBeanX" + element.getType().simpleName == "Object" + element.getGenericType().isAssignable("test.MyBeanX") + when: + def genRepo = ce.getTypeArguments("test.GenericRepository") + then: + genRepo.get("E").simpleName == "MyBeanX" + genRepo.get("E").getSyntheticBeanProperties().size() == 1 + genRepo.get("E").getMethods().size() == 0 + genRepo.get("E").getFields().get(0).name == "name" + } + + void "test abstract placeholder"() { + ClassElement ce = buildClassElement('test.MyRepo2', ''' +package test +import io.micronaut.context.annotation.Prototype +import java.util.List + +abstract class MyRepo2 : Repo +interface Repo : GenericRepository { + fun save(ent: E) +} +interface GenericRepository +@Prototype +@io.micronaut.kotlin.processing.inject.ast.MyEntity +class MyBean { + var name: String? = null +} + +''') + + when: + def repo = ce.getTypeArguments("test.Repo") + then: + repo.get("E").simpleName == "MyBean" + repo.get("E").hasAnnotation(MyEntity) + repo.get("E").getSyntheticBeanProperties().size() == 1 + repo.get("E").getMethods().size() == 0 + repo.get("E").getFields().size() == 1 + repo.get("E").getFields().get(0).name == "name" + + def element = ce.findMethod("save").get().getParameters()[0] + element.getGenericType().simpleName == "MyBean" + element.getType().simpleName == "Object" + when: + def genRepo = ce.getTypeArguments("test.GenericRepository") + then: + genRepo.get("E").simpleName == "MyBean" + genRepo.get("E").hasAnnotation(MyEntity) + genRepo.get("E").getSyntheticBeanProperties().size() == 1 + genRepo.get("E").getMethods().size() == 0 + genRepo.get("E").getFields().get(0).name == "name" + } + + void "test abstract placeholder 2"() { + ClassElement ce = buildClassElement('test.MyRepo2', ''' +package test +import io.micronaut.context.annotation.Prototype +import java.util.List + +abstract class MyRepo2 : Repo() +abstract class Repo : GenericRepository { + abstract fun save(ent: E) +} +interface GenericRepository +@Prototype +@io.micronaut.kotlin.processing.inject.ast.MyEntity +class MyBean { + var name: String? = null +} + +''') + + when: + def repo = ce.getTypeArguments("test.Repo") + then: + repo.get("E").simpleName == "MyBean" + repo.get("E").hasAnnotation(MyEntity) + repo.get("E").getSyntheticBeanProperties().size() == 1 + repo.get("E").getMethods().size() == 0 + repo.get("E").getFields().size() == 1 + repo.get("E").getFields().get(0).name == "name" + + def element = ce.findMethod("save").get().getParameters()[0] + element.getGenericType().simpleName == "MyBean" + element.getType().simpleName == "Object" + when: + def genRepo = ce.getTypeArguments("test.GenericRepository") + then: + genRepo.get("E").simpleName == "MyBean" + genRepo.get("E").hasAnnotation(MyEntity) + genRepo.get("E").getSyntheticBeanProperties().size() == 1 + genRepo.get("E").getMethods().size() == 0 + genRepo.get("E").getFields().get(0).name == "name" + } + + void "test abstract placeholder 3"() { + ClassElement ce = buildClassElement('test.MyRepo2', ''' +package test +import io.micronaut.context.annotation.Prototype +import java.util.List + +abstract class MyRepo2 : Repo() +abstract class Repo : GenericRepository { + abstract fun save(ent: S) +} +interface GenericRepository +@Prototype +@io.micronaut.kotlin.processing.inject.ast.MyEntity +class MyBean { + var name: String? = null +} + +''') + + when: + def repo = ce.getTypeArguments("test.Repo") + then: + repo.get("E").simpleName == "MyBean" + repo.get("E").hasAnnotation(MyEntity) + repo.get("E").getSyntheticBeanProperties().size() == 1 + repo.get("E").getMethods().size() == 0 + repo.get("E").getFields().size() == 1 + repo.get("E").getFields().get(0).name == "name" + + def element = ce.findMethod("save").get().getParameters()[0] + element.getGenericType().simpleName == "MyBean" + element.getType().simpleName == "Object" when: def genRepo = ce.getTypeArguments("test.GenericRepository") then: genRepo.get("E").simpleName == "MyBean" + genRepo.get("E").hasAnnotation(MyEntity) genRepo.get("E").getSyntheticBeanProperties().size() == 1 genRepo.get("E").getMethods().size() == 0 genRepo.get("E").getFields().get(0).name == "name" diff --git a/inject-kotlin/src/test/resources/META-INF/services/io.micronaut.inject.visitor.TypeElementVisitor b/inject-kotlin/src/test/resources/META-INF/services/io.micronaut.inject.visitor.TypeElementVisitor index 5c55c94a11b..46102e6a19c 100644 --- a/inject-kotlin/src/test/resources/META-INF/services/io.micronaut.inject.visitor.TypeElementVisitor +++ b/inject-kotlin/src/test/resources/META-INF/services/io.micronaut.inject.visitor.TypeElementVisitor @@ -4,3 +4,4 @@ io.micronaut.kotlin.processing.annotations.AnnotateFieldTypeSpec$AnnotateFieldTy io.micronaut.kotlin.processing.annotations.AnnotateMethodParameterSpec$AnnotateMethodParameterVisitor io.micronaut.kotlin.processing.annotations.AnnotateMethodReturnSpec$AnnotateMethodReturnVisitor io.micronaut.kotlin.processing.annotations.AnnotateMethodSpec$AnnotationMethodVisitor +io.micronaut.kotlin.processing.annotations.AnnotatePropertySpec$AnnotatePropertyVisitor From 0c91bff8d150940ca5c5013a015be3fc9860d604 Mon Sep 17 00:00:00 2001 From: Graeme Rocher Date: Tue, 28 Feb 2023 17:28:11 +0100 Subject: [PATCH 528/743] Include disabled bean info in beans endpoint (#8792) - Refactor beans endpoint removing unnecessary reactive APIs - Include disabled bean info in beans endpoint --- .../AbstractInitializableBeanDefinition.java | 8 ++- .../context/BeanDefinitionRegistry.java | 11 ++++ .../micronaut/context/DefaultBeanContext.java | 10 ++- .../io/micronaut/context/DisabledBean.java | 4 +- .../beans/BeanDefinitionDataCollector.java | 10 +-- .../endpoint/beans/BeansEndpoint.java | 25 ++------ .../beans/impl/DefaultBeanDefinitionData.java | 30 +++++++-- .../DefaultBeanDefinitionDataCollector.java | 63 +++++++++++++------ .../endpoint/beans/BeansEndpointSpec.groovy | 4 +- .../docs/guide/introduction/whatsNew.adoc | 6 ++ 10 files changed, 111 insertions(+), 60 deletions(-) diff --git a/inject/src/main/java/io/micronaut/context/AbstractInitializableBeanDefinition.java b/inject/src/main/java/io/micronaut/context/AbstractInitializableBeanDefinition.java index cf914ea86cd..5afa2ff4a62 100644 --- a/inject/src/main/java/io/micronaut/context/AbstractInitializableBeanDefinition.java +++ b/inject/src/main/java/io/micronaut/context/AbstractInitializableBeanDefinition.java @@ -462,9 +462,11 @@ public final Collection> getRequiredComponents() { } if (methodInjection != null) { for (MethodReference methodReference : methodInjection) { - if (methodReference.arguments != null && methodReference.arguments.length > 0) { - for (Argument argument : methodReference.arguments) { - argumentConsumer.accept(argument); + if (methodReference.annotationMetadata.hasDeclaredAnnotation(AnnotationUtil.INJECT)) { + if (methodReference.arguments != null && methodReference.arguments.length > 0) { + for (Argument argument : methodReference.arguments) { + argumentConsumer.accept(argument); + } } } } diff --git a/inject/src/main/java/io/micronaut/context/BeanDefinitionRegistry.java b/inject/src/main/java/io/micronaut/context/BeanDefinitionRegistry.java index c8d2e662880..ad5fd40ad2e 100644 --- a/inject/src/main/java/io/micronaut/context/BeanDefinitionRegistry.java +++ b/inject/src/main/java/io/micronaut/context/BeanDefinitionRegistry.java @@ -28,6 +28,7 @@ import io.micronaut.inject.ProxyBeanDefinition; import java.util.Collection; +import java.util.Collections; import java.util.Objects; import java.util.Optional; @@ -264,6 +265,16 @@ default BeanDefinitionRegistry registerBeanDefinition(@NonNull RuntimeBeanDe */ @NonNull Collection> getBeanDefinitionReferences(); + /** + * Get all of the disabled {@link DisabledBean}. + * + * @return The disabled bean definitions + * @since 4.0.0 + */ + default @NonNull Collection> getDisabledBeans() { + return Collections.emptyList(); + } + /** * Find active {@link javax.inject.Singleton} beans for the given qualifier. Note that * this method can return multiple registrations for a given singleton bean instance since each bean may have multiple qualifiers. diff --git a/inject/src/main/java/io/micronaut/context/DefaultBeanContext.java b/inject/src/main/java/io/micronaut/context/DefaultBeanContext.java index 58826ef7943..bc09b9357bd 100644 --- a/inject/src/main/java/io/micronaut/context/DefaultBeanContext.java +++ b/inject/src/main/java/io/micronaut/context/DefaultBeanContext.java @@ -1629,6 +1629,14 @@ public Collection> getAllBeanDefinitions() { return (Collection>) Collections.emptyMap(); } + @Override + public Collection> getDisabledBeans() { + return disabledBeans.values().stream() + .map(producer -> (DisabledBean) producer.reference) + .filter(Objects::nonNull) + .collect(Collectors.toList()); + } + @SuppressWarnings("unchecked") @NonNull @Override @@ -4098,7 +4106,7 @@ private static final class CollectionHolder { /** * The class adds the caching of the enabled decision + the definition instance. - * NOTE: The class can be accesed in multiple threads, we do allow for the fields to be possibly intitialized concurrently - multiple times. + * NOTE: The class can be accessed in multiple threads, we do allow for the fields to be possibly initialized concurrently - multiple times. * * @since 4.0.0 */ diff --git a/inject/src/main/java/io/micronaut/context/DisabledBean.java b/inject/src/main/java/io/micronaut/context/DisabledBean.java index 5890229afd3..26d7b4232dc 100644 --- a/inject/src/main/java/io/micronaut/context/DisabledBean.java +++ b/inject/src/main/java/io/micronaut/context/DisabledBean.java @@ -15,7 +15,6 @@ */ package io.micronaut.context; -import io.micronaut.core.annotation.Internal; import io.micronaut.core.annotation.NonNull; import io.micronaut.core.annotation.Nullable; import io.micronaut.core.type.Argument; @@ -32,8 +31,7 @@ * @param reasons The reasons the bean is disabled * @param The bean type */ -@Internal -record DisabledBean( +public record DisabledBean( @NonNull Argument type, @Nullable Qualifier qualifier, @NonNull List reasons) diff --git a/management/src/main/java/io/micronaut/management/endpoint/beans/BeanDefinitionDataCollector.java b/management/src/main/java/io/micronaut/management/endpoint/beans/BeanDefinitionDataCollector.java index a99932c8d09..16ba2640335 100644 --- a/management/src/main/java/io/micronaut/management/endpoint/beans/BeanDefinitionDataCollector.java +++ b/management/src/main/java/io/micronaut/management/endpoint/beans/BeanDefinitionDataCollector.java @@ -15,11 +15,6 @@ */ package io.micronaut.management.endpoint.beans; -import io.micronaut.inject.BeanDefinition; -import org.reactivestreams.Publisher; - -import java.util.Collection; - /** *

Used to respond with bean information used for the {@link BeansEndpoint}.

* @@ -30,9 +25,8 @@ public interface BeanDefinitionDataCollector { /** - * @param beanDefinitions A collection of bean definitions - * @return A publisher that returns data representing all of + * @return The raw data representing information about the available beans; * the given bean definitions */ - Publisher getData(Collection> beanDefinitions); + T getData(); } diff --git a/management/src/main/java/io/micronaut/management/endpoint/beans/BeansEndpoint.java b/management/src/main/java/io/micronaut/management/endpoint/beans/BeansEndpoint.java index 5372fbf0eb4..aefb6e72c0c 100644 --- a/management/src/main/java/io/micronaut/management/endpoint/beans/BeansEndpoint.java +++ b/management/src/main/java/io/micronaut/management/endpoint/beans/BeansEndpoint.java @@ -15,18 +15,9 @@ */ package io.micronaut.management.endpoint.beans; -import io.micronaut.context.BeanContext; import io.micronaut.core.async.annotation.SingleResult; -import io.micronaut.inject.BeanDefinition; import io.micronaut.management.endpoint.annotation.Endpoint; import io.micronaut.management.endpoint.annotation.Read; -import org.reactivestreams.Publisher; -import reactor.core.publisher.Mono; - -import java.util.Collections; -import java.util.Comparator; -import java.util.List; -import java.util.stream.Collectors; /** *

Exposes an {@link Endpoint} to provide information about the beans of the application.

@@ -37,15 +28,12 @@ @Endpoint("beans") public class BeansEndpoint { - private BeanContext beanContext; - private BeanDefinitionDataCollector beanDefinitionDataCollector; + private final BeanDefinitionDataCollector beanDefinitionDataCollector; /** - * @param beanContext The {@link BeanContext} * @param beanDefinitionDataCollector The {@link BeanDefinitionDataCollector} */ - public BeansEndpoint(BeanContext beanContext, BeanDefinitionDataCollector beanDefinitionDataCollector) { - this.beanContext = beanContext; + public BeansEndpoint(BeanDefinitionDataCollector beanDefinitionDataCollector) { this.beanDefinitionDataCollector = beanDefinitionDataCollector; } @@ -54,12 +42,7 @@ public BeansEndpoint(BeanContext beanContext, BeanDefinitionDataCollector beanDe */ @Read @SingleResult - public Publisher getBeans() { - List> beanDefinitions = beanContext.getAllBeanDefinitions() - .stream() - .sorted(Comparator.comparing((BeanDefinition bd) -> bd.getClass().getName())) - .collect(Collectors.toList()); - return Mono.from(beanDefinitionDataCollector.getData(beanDefinitions)) - .defaultIfEmpty(Collections.emptyMap()); + public Object getBeans() { + return beanDefinitionDataCollector.getData(); } } diff --git a/management/src/main/java/io/micronaut/management/endpoint/beans/impl/DefaultBeanDefinitionData.java b/management/src/main/java/io/micronaut/management/endpoint/beans/impl/DefaultBeanDefinitionData.java index 476a3c559cc..3360bc60f45 100644 --- a/management/src/main/java/io/micronaut/management/endpoint/beans/impl/DefaultBeanDefinitionData.java +++ b/management/src/main/java/io/micronaut/management/endpoint/beans/impl/DefaultBeanDefinitionData.java @@ -15,7 +15,10 @@ */ package io.micronaut.management.endpoint.beans.impl; +import io.micronaut.context.Qualifier; import io.micronaut.context.annotation.Requires; +import io.micronaut.core.annotation.NonNull; +import io.micronaut.core.annotation.Nullable; import io.micronaut.inject.BeanDefinition; import io.micronaut.management.endpoint.beans.BeanDefinitionData; import io.micronaut.management.endpoint.beans.BeansEndpoint; @@ -24,7 +27,6 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; -import java.util.stream.Collectors; /** * The default {@link BeanDefinitionData} implementation. Returns a {@link Map} with @@ -51,23 +53,40 @@ public Map getData(BeanDefinition beanDefinition) { beanData.put("dependencies", getDependencies(beanDefinition)); beanData.put("scope", getScope(beanDefinition)); beanData.put("type", getType(beanDefinition)); + beanData.put("qualifier", getQualifier(beanDefinition)); return beanData; } + /** + * Obtains the qualifier. + * @param beanDefinition The bean definition. + * @return The qualifier + */ + @Nullable + protected String getQualifier(@NonNull BeanDefinition beanDefinition) { + Qualifier q = beanDefinition.getDeclaredQualifier(); + if (q != null) { + return q.toString(); + } + return null; + } + /** * @param beanDefinition A bean definition * @return A list of dependencies for the bean definition */ - protected List getDependencies(BeanDefinition beanDefinition) { - return beanDefinition.getRequiredComponents().stream().map(Class::getName).sorted().collect(Collectors.toList()); + @NonNull + protected List getDependencies(@NonNull BeanDefinition beanDefinition) { + return beanDefinition.getRequiredComponents().stream().map(Class::getName).sorted().toList(); } /** * @param beanDefinition A bean definition * @return The scope for the bean */ - protected String getScope(BeanDefinition beanDefinition) { + @Nullable + protected String getScope(@NonNull BeanDefinition beanDefinition) { return beanDefinition.getScopeName().orElse(null); } @@ -75,7 +94,8 @@ protected String getScope(BeanDefinition beanDefinition) { * @param beanDefinition A bean definition * @return The type of the bean as String */ - protected String getType(BeanDefinition beanDefinition) { + @NonNull + protected String getType(@NonNull BeanDefinition beanDefinition) { return beanDefinition.getBeanType().getName(); } } diff --git a/management/src/main/java/io/micronaut/management/endpoint/beans/impl/DefaultBeanDefinitionDataCollector.java b/management/src/main/java/io/micronaut/management/endpoint/beans/impl/DefaultBeanDefinitionDataCollector.java index 2e2f83e68a6..fc7e9f38b5c 100644 --- a/management/src/main/java/io/micronaut/management/endpoint/beans/impl/DefaultBeanDefinitionDataCollector.java +++ b/management/src/main/java/io/micronaut/management/endpoint/beans/impl/DefaultBeanDefinitionDataCollector.java @@ -15,18 +15,22 @@ */ package io.micronaut.management.endpoint.beans.impl; +import io.micronaut.context.BeanContext; +import io.micronaut.context.DisabledBean; +import io.micronaut.context.Qualifier; import io.micronaut.context.annotation.Requires; import io.micronaut.inject.BeanDefinition; import io.micronaut.management.endpoint.beans.BeanDefinitionData; import io.micronaut.management.endpoint.beans.BeanDefinitionDataCollector; import io.micronaut.management.endpoint.beans.BeansEndpoint; import jakarta.inject.Singleton; -import org.reactivestreams.Publisher; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; + import java.util.Collection; +import java.util.Comparator; import java.util.LinkedHashMap; +import java.util.List; import java.util.Map; +import java.util.stream.Collectors; /** * The default {@link BeanDefinitionDataCollector} implementation. Returns a {@link Map} with @@ -39,33 +43,56 @@ @Requires(beans = BeansEndpoint.class) public class DefaultBeanDefinitionDataCollector implements BeanDefinitionDataCollector> { - private BeanDefinitionData beanDefinitionData; + private final BeanContext beanContext; + private final BeanDefinitionData> beanDefinitionData; /** + * @param beanContext The bean context * @param beanDefinitionData The {@link BeanDefinitionData} */ - DefaultBeanDefinitionDataCollector(BeanDefinitionData beanDefinitionData) { + DefaultBeanDefinitionDataCollector(BeanContext beanContext, BeanDefinitionData> beanDefinitionData) { + this.beanContext = beanContext; this.beanDefinitionData = beanDefinitionData; } @Override - public Publisher> getData(Collection> beanDefinitions) { - return Mono.from(getBeans(beanDefinitions)).map(beans -> { - Map beanData = new LinkedHashMap<>(1); - beanData.put("beans", beans); - return beanData; - }); + public Map getData() { + Map beanData = new LinkedHashMap<>(1); + List> beanDefinitions = beanContext.getAllBeanDefinitions() + .stream() + .sorted(Comparator.comparing((BeanDefinition bd) -> bd.getClass().getName())) + .toList(); + + beanData.put("beans", getBeans(beanDefinitions)); + beanData.put("disabled", getDisabledBeans()); + return beanData; } /** * @param definitions The bean definitions - * @return A {@link Publisher} that wraps a Map + * @return A map of bean information. */ - protected Publisher> getBeans(Collection> definitions) { - return Flux.fromIterable(definitions) - .collectMap(definition -> definition.getClass().getName(), - definition -> { - return beanDefinitionData.getData(definition); - }); + protected Map> getBeans(Collection> definitions) { + return definitions.stream() + .collect(Collectors.toMap(definition -> definition.getClass().getName(), beanDefinitionData::getData)); + } + + /** + * @return Information about the disabled beans. + */ + protected List> getDisabledBeans() { + Collection> disabledBeans = beanContext.getDisabledBeans(); + + return disabledBeans.stream() + .map(disabledBean -> { + Map data = new LinkedHashMap<>(); + data.put("type", disabledBean.type().getTypeName()); + Qualifier q = disabledBean.qualifier(); + if (q != null) { + data.put("qualifier", q.toString()); + } + data.put("reasons", disabledBean.reasons()); + return data; + }).toList(); } } diff --git a/management/src/test/groovy/io/micronaut/management/endpoint/beans/BeansEndpointSpec.groovy b/management/src/test/groovy/io/micronaut/management/endpoint/beans/BeansEndpointSpec.groovy index 47c203caae4..255a37d5431 100644 --- a/management/src/test/groovy/io/micronaut/management/endpoint/beans/BeansEndpointSpec.groovy +++ b/management/src/test/groovy/io/micronaut/management/endpoint/beans/BeansEndpointSpec.groovy @@ -43,10 +43,12 @@ class BeansEndpointSpec extends Specification { then: response.code() == HttpStatus.OK.code - beans["io.micronaut.management.endpoint.beans.\$BeansEndpoint" + BeanDefinitionWriter.CLASS_SUFFIX].dependencies.contains("io.micronaut.context.BeanContext") beans["io.micronaut.management.endpoint.beans.\$BeansEndpoint" + BeanDefinitionWriter.CLASS_SUFFIX].dependencies.contains("io.micronaut.management.endpoint.beans.BeanDefinitionDataCollector") beans["io.micronaut.management.endpoint.beans.\$BeansEndpoint" + BeanDefinitionWriter.CLASS_SUFFIX].scope == AnnotationUtil.SINGLETON beans["io.micronaut.management.endpoint.beans.\$BeansEndpoint" + BeanDefinitionWriter.CLASS_SUFFIX].type == "io.micronaut.management.endpoint.beans.BeansEndpoint" + result.disabled.find { + it.type == 'io.micronaut.logging.PropertiesLoggingLevelsConfigurer' + }.reasons == ["Required property [logger.levels] with value [null] not present"] cleanup: rxClient.close() diff --git a/src/main/docs/guide/introduction/whatsNew.adoc b/src/main/docs/guide/introduction/whatsNew.adoc index 78934dbaace..6e9f8364b23 100644 --- a/src/main/docs/guide/introduction/whatsNew.adoc +++ b/src/main/docs/guide/introduction/whatsNew.adoc @@ -19,6 +19,12 @@ It is now possible to inject a `java.util.Map` of beans where the key is the bea When a bean annotated with ann:context.annotation.EachProperty[] or ann:context.annotation.Bean[] is not found due to missing configuration an error is thrown showing the configuration prefix necessary to resolve the issue. +==== Tracking of Disabled Beans + +Beans that are disabled via <> are now tracked and an appropriate error thrown if a bean has been disabled. + +The disabled beans are also now visible via the <> in the <> aiding in understanding the state of your application configuration. + === Other Dependency Upgrades - Kotlin 1.7.10 From d7e5585aff618d38977ff3ebdb9988fa8b799f5b Mon Sep 17 00:00:00 2001 From: micronaut-build <65172877+micronaut-build@users.noreply.github.com> Date: Wed, 1 Mar 2023 11:40:41 +0100 Subject: [PATCH 529/743] Bump micronaut-test to 3.9.1 (#8857) --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 0f232afc4ec..f961103bcae 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -117,7 +117,7 @@ managed-micronaut-serialization = "1.5.0" managed-micronaut-servlet = "3.3.5" managed-micronaut-spring = "4.5.0" managed-micronaut-sql = "4.7.2" -managed-micronaut-test = "3.9.0" +managed-micronaut-test = "3.9.1" managed-micronaut-test-resources = "1.2.3" managed-micronaut-toml = "1.1.3" managed-micronaut-tracing = "4.5.0" From 206efc0b65248c375692c57c7da2323f24dfa896 Mon Sep 17 00:00:00 2001 From: micronaut-build <65172877+micronaut-build@users.noreply.github.com> Date: Wed, 1 Mar 2023 11:42:11 +0100 Subject: [PATCH 530/743] Bump micronaut-micrometer to 4.8.1 (#8854) * Bump micronaut-micrometer to 4.8.1 * Update libs.versions.toml --------- Co-authored-by: Sergio del Amo --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index f961103bcae..ad10259ab98 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -90,7 +90,7 @@ managed-micronaut-jmx = "3.2.0" managed-micronaut-kafka = "4.5.1" managed-micronaut-kotlin = "3.2.2" managed-micronaut-kubernetes = "3.4.0" -managed-micronaut-micrometer = "4.7.2" +managed-micronaut-micrometer = "4.8.1" managed-micronaut-microstream = "1.3.0" managed-micronaut-liquibase = "5.6.0" managed-micronaut-mongo = "4.6.0" From 986da009c92ec25da79c642fe82acf8fc04eb4ef Mon Sep 17 00:00:00 2001 From: micronaut-build <65172877+micronaut-build@users.noreply.github.com> Date: Wed, 1 Mar 2023 11:50:54 +0100 Subject: [PATCH 531/743] Bump micronaut-serialization to 1.5.1 (#8856) --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 601599cd365..c4159fe60fc 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -113,7 +113,7 @@ managed-micronaut-rxjava1 = "1.0.0" managed-micronaut-rxjava2 = "1.3.0" managed-micronaut-rxjava3 = "2.4.0" managed-micronaut-security = "3.9.3" -managed-micronaut-serialization = "1.5.0" +managed-micronaut-serialization = "1.5.1" managed-micronaut-servlet = "3.3.5" managed-micronaut-spring = "4.4.0" managed-micronaut-sql = "4.7.2" From e549e1ea5ed3dfd685f735a9087bddcd29b9e87e Mon Sep 17 00:00:00 2001 From: Denis Stepanov Date: Wed, 1 Mar 2023 09:54:37 -0500 Subject: [PATCH 532/743] Remove incorrect visitor reference (#8858) --- .../services/io.micronaut.inject.visitor.TypeElementVisitor | 2 -- 1 file changed, 2 deletions(-) diff --git a/core-processor/src/main/resources/META-INF/services/io.micronaut.inject.visitor.TypeElementVisitor b/core-processor/src/main/resources/META-INF/services/io.micronaut.inject.visitor.TypeElementVisitor index cc461fe73db..8d781117e40 100644 --- a/core-processor/src/main/resources/META-INF/services/io.micronaut.inject.visitor.TypeElementVisitor +++ b/core-processor/src/main/resources/META-INF/services/io.micronaut.inject.visitor.TypeElementVisitor @@ -5,5 +5,3 @@ io.micronaut.context.visitor.ExecutableVisitor io.micronaut.context.visitor.ValidationVisitor io.micronaut.context.visitor.InternalApiTypeElementVisitor io.micronaut.context.visitor.ConfigurationReaderVisitor -io.micronaut.scheduling.async.validation.AsyncTypeElementVisitor - From 579e9ddca7d27ec7f35c1d3f6e1d21653a884e1d Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Wed, 1 Mar 2023 16:54:26 +0100 Subject: [PATCH 533/743] fix: allow to disable disable `ServiceReadyHealthIndicator` via configuration (#8836) --- .../endpoint/health/HealthEndpoint.java | 18 ++++++++++++ .../service/ServiceReadyHealthIndicator.java | 3 ++ ...entHealthIndicatorConfigurationSpec.groovy | 19 ++++++++++++ .../ServiceReadyHealthIndicatorSpec.groovy | 29 +++++++++++++++++++ 4 files changed, 69 insertions(+) create mode 100644 management/src/test/groovy/io/micronaut/management/health/indicator/discovery/DiscoveryClientHealthIndicatorConfigurationSpec.groovy create mode 100644 management/src/test/groovy/io/micronaut/management/health/indicator/service/ServiceReadyHealthIndicatorSpec.groovy diff --git a/management/src/main/java/io/micronaut/management/endpoint/health/HealthEndpoint.java b/management/src/main/java/io/micronaut/management/endpoint/health/HealthEndpoint.java index b82c2a53505..fe98ff413ec 100644 --- a/management/src/main/java/io/micronaut/management/endpoint/health/HealthEndpoint.java +++ b/management/src/main/java/io/micronaut/management/endpoint/health/HealthEndpoint.java @@ -71,6 +71,8 @@ public class HealthEndpoint { private DetailsVisibility detailsVisible = DetailsVisibility.AUTHENTICATED; private StatusConfiguration statusConfiguration; + private boolean serviceReadyIndicatorEnabled = true; + /** * @param healthAggregator The {@link HealthAggregator} * @param healthIndicators The {@link HealthIndicator} @@ -137,6 +139,22 @@ public Publisher getHealth(@Nullable Principal principal, @Selecto ); } + /** + * Whether the {@link io.micronaut.management.health.indicator.service.ServiceReadyHealthIndicator} is enabled. Defaults to {@code true}. + * @return True if it is enabled. + */ + public boolean isServiceReadyIndicatorEnabled() { + return serviceReadyIndicatorEnabled; + } + + /** + * Set whether the {@link io.micronaut.management.health.indicator.service.ServiceReadyHealthIndicator} is enabled. Defaults to {@code true}. + * @param serviceReadyIndicatorEnabled True if the service ready indicator should be enabled. + */ + public void setServiceReadyIndicatorEnabled(boolean serviceReadyIndicatorEnabled) { + this.serviceReadyIndicatorEnabled = serviceReadyIndicatorEnabled; + } + /** * @return The visibility policy for health information. */ diff --git a/management/src/main/java/io/micronaut/management/health/indicator/service/ServiceReadyHealthIndicator.java b/management/src/main/java/io/micronaut/management/health/indicator/service/ServiceReadyHealthIndicator.java index e774a02f56d..a5c1b92919b 100644 --- a/management/src/main/java/io/micronaut/management/health/indicator/service/ServiceReadyHealthIndicator.java +++ b/management/src/main/java/io/micronaut/management/health/indicator/service/ServiceReadyHealthIndicator.java @@ -18,6 +18,7 @@ import io.micronaut.context.annotation.Requires; import io.micronaut.core.annotation.Internal; import io.micronaut.core.order.Ordered; +import io.micronaut.core.util.StringUtils; import io.micronaut.discovery.event.ServiceReadyEvent; import io.micronaut.health.HealthStatus; import io.micronaut.management.endpoint.health.HealthEndpoint; @@ -40,9 +41,11 @@ */ @Singleton @Requires(beans = HealthEndpoint.class) +@Requires(property = ServiceReadyHealthIndicator.ENABLED, value = StringUtils.TRUE, defaultValue = StringUtils.TRUE) @Readiness public class ServiceReadyHealthIndicator implements HealthIndicator { + public static final String ENABLED = HealthEndpoint.PREFIX + ".service-ready-indicator-enabled"; private static final String NAME = "service"; private final boolean isService; diff --git a/management/src/test/groovy/io/micronaut/management/health/indicator/discovery/DiscoveryClientHealthIndicatorConfigurationSpec.groovy b/management/src/test/groovy/io/micronaut/management/health/indicator/discovery/DiscoveryClientHealthIndicatorConfigurationSpec.groovy new file mode 100644 index 00000000000..59554c49375 --- /dev/null +++ b/management/src/test/groovy/io/micronaut/management/health/indicator/discovery/DiscoveryClientHealthIndicatorConfigurationSpec.groovy @@ -0,0 +1,19 @@ +package io.micronaut.management.health.indicator.discovery + +import io.micronaut.context.ApplicationContext +import spock.lang.Specification + +class DiscoveryClientHealthIndicatorConfigurationSpec extends Specification { + + void "bean of type DiscoveryClientHealthIndicatorConfiguration does not exist if you set endpoints.health.discovery-client.enabled=false"() { + given: + ApplicationContext applicationContext = ApplicationContext.run(['endpoints.health.discovery-client.enabled': 'false']) + + expect: + !applicationContext.containsBean(DiscoveryClientHealthIndicatorConfiguration) + !applicationContext.containsBean(DiscoveryClientHealthIndicator) + + cleanup: + applicationContext.close() + } +} diff --git a/management/src/test/groovy/io/micronaut/management/health/indicator/service/ServiceReadyHealthIndicatorSpec.groovy b/management/src/test/groovy/io/micronaut/management/health/indicator/service/ServiceReadyHealthIndicatorSpec.groovy new file mode 100644 index 00000000000..3d8e01acf0f --- /dev/null +++ b/management/src/test/groovy/io/micronaut/management/health/indicator/service/ServiceReadyHealthIndicatorSpec.groovy @@ -0,0 +1,29 @@ +package io.micronaut.management.health.indicator.service + +import io.micronaut.context.ApplicationContext +import io.micronaut.management.endpoint.health.HealthEndpoint +import spock.lang.Specification + +class ServiceReadyHealthIndicatorSpec extends Specification { + void "bean of type ServiceReadyHealthIndicatorConfiguration does not exist if you set endpoints.health.service.enabled=false"() { + given: + ApplicationContext applicationContext = ApplicationContext.run([(ServiceReadyHealthIndicator.ENABLED): 'false']) + + expect: + !applicationContext.containsBean(ServiceReadyHealthIndicator) + + cleanup: + applicationContext.close() + } + + void "bean of type ServiceReadyHealthIndicatorConfiguration exists by default"() { + given: + ApplicationContext applicationContext = ApplicationContext.run() + + expect: + applicationContext.containsBean(ServiceReadyHealthIndicator) + + cleanup: + applicationContext.close() + } +} From dd65c4529525a424c3b82997ae0d262eb9cad3ef Mon Sep 17 00:00:00 2001 From: Denis Stepanov Date: Wed, 1 Mar 2023 12:40:05 -0500 Subject: [PATCH 534/743] Replace the validation module with Micronaut Validation dependency (#8863) --- benchmarks/build.gradle | 18 +- context/build.gradle | 7 +- core-processor/build.gradle | 1 - .../context/visitor/ValidationVisitor.java | 120 - .../inject/writer/DispatchWriter.java | 10 +- ...icronaut.inject.visitor.TypeElementVisitor | 1 - .../io/micronaut/health/HealthStatus.java | 4 +- gradle/libs.versions.toml | 8 +- http-client/build.gradle | 14 +- http-server-netty/build.gradle | 25 +- http-server-tck/build.gradle.kts | 13 +- inject-groovy/build.gradle | 9 +- inject-java-test/build.gradle | 9 +- .../beans/BeanIntrospectionSpec.groovy | 4 +- inject-java/build.gradle | 24 +- inject-kotlin-test/build.gradle | 1 - inject-kotlin/build.gradle | 18 +- inject/build.gradle | 7 +- .../io/micronaut/context/ProviderUtils.java | 5 +- management/build.gradle | 12 +- .../env/EnvironmentEndpointFilter.java | 4 +- .../env/EnvironmentFilterSpecification.java | 14 +- retry/build.gradle | 8 + runtime/build.gradle | 6 +- settings.gradle | 1 - test-suite-graal/build.gradle | 11 +- test-suite-groovy/build.gradle | 12 +- test-suite-kotlin-ksp/build.gradle | 20 +- test-suite-kotlin/build.gradle | 21 +- test-suite/build.gradle | 26 +- validation/build.gradle | 45 - .../io/micronaut/validation/Validated.java | 51 - .../validation/ValidatingInterceptor.java | 186 -- .../ConstraintExceptionHandler.java | 113 - .../validation/exceptions/package-info.java | 23 - .../io/micronaut/validation/package-info.java | 23 - .../DefaultAnnotatedElementValidator.java | 101 - .../validator/DefaultClockProvider.java | 35 - .../DefaultConstraintDescriptor.java | 125 - .../validator/DefaultValidator.java | 2452 ----------------- .../DefaultValidatorConfiguration.java | 265 -- .../validator/DefaultValidatorFactory.java | 105 - .../validator/ExecutableMethodValidator.java | 140 - .../validator/IntrospectedBeanDescriptor.java | 205 -- .../validator/ReactiveValidator.java | 53 - .../validation/validator/Validator.java | 101 - .../validator/ValidatorConfiguration.java | 81 - .../constraints/AbstractPatternValidator.java | 118 - .../constraints/ConstraintValidator.java | 92 - .../ConstraintValidatorContext.java | 62 - .../ConstraintValidatorRegistry.java | 62 - .../constraints/DecimalMaxValidator.java | 68 - .../constraints/DecimalMinValidator.java | 68 - .../DefaultConstraintValidators.java | 1045 ------- .../constraints/DigitsValidator.java | 70 - .../validator/constraints/DomainNameUtil.java | 110 - .../validator/constraints/EmailValidator.java | 101 - .../constraints/PatternValidator.java | 43 - .../validator/constraints/SizeValidator.java | 50 - .../extractors/DefaultValueExtractors.java | 322 --- .../extractors/SimpleValueReceiver.java | 42 - .../UnwrapByDefaultValueExtractor.java | 29 - .../extractors/ValueExtractorRegistry.java | 64 - .../messages/DefaultValidationMessages.java | 68 - .../CompositeTraversableResolver.java | 68 - ...nject.annotation.AnnotatedElementValidator | 1 - .../validation/BookmarkController.java | 44 - .../validation/BookmarkControllerSpec.groovy | 128 - .../groovy/io/micronaut/validation/Foo.java | 80 - .../io/micronaut/validation/JavaClient.java | 29 - .../validation/PaginationCommand.java | 90 - .../groovy/io/micronaut/validation/Pojo.java | 47 - .../validation/PojoNoIntrospection.java | 44 - .../validation/ValidatedController.java | 67 - .../validation/ValidatedParseSpec.groovy | 119 - .../micronaut/validation/ValidatedSpec.groovy | 770 ------ .../validation/ValidationVisitorSpec.groovy | 163 -- .../WebSocketClientValidationSpec.groovy | 81 - .../WebSocketValidationServerHandler.groovy | 36 - .../executable/NullablePrimitiveSpec.groovy | 138 - .../internal/InteralApiRuleSpec.groovy | 52 - .../properties/MixedCasePropertySpec.groovy | 329 --- .../ConstraintValidatorRegistrySpec.groovy | 22 - .../validator/ValidatorGroupsSpec.groovy | 162 -- .../validation/validator/ValidatorSpec.groovy | 692 ----- .../validation/validator/ast/SomeAnn.java | 30 - .../ast/ValidateAnnotationMetadataSpec.groovy | 142 - .../constraints/ConstraintMessagesSpec.groovy | 104 - .../constraints/ConstraintsSpec.groovy | 313 --- .../custom/AlwaysInvalidConstraint.java | 22 - .../custom/CustomConstraintsSpec.groovy | 179 -- .../CustomConstraintsValidatorFactory.java | 21 - .../custom/CustomMessageConstraint.java | 12 - .../EmployeeExperienceConstraint.java | 13 - ...EmployeeExperienceConstraintValidator.java | 21 - .../EmployeeValidationsSpec.groovy | 194 -- .../validator/map/MapValidatorSpec.groovy | 48 - .../pojo/ContextValidationSpec.groovy | 65 - .../validator/pojo/FavoriteWebsSpec.groovy | 66 - .../pojo/NameAndLastNameValidator.java | 29 - .../pojo/PojoBodyParameterSpec.groovy | 250 -- .../validator/pojo/PojoConfigProps.java | 23 - .../PojoConfigurationPropertiesSpec.groovy | 23 - .../validator/pojo/PojoValidatorSpec.groovy | 118 - .../validation/validator/pojo/ValidURLs.java | 29 - .../ReactiveMethodValidationSpec.groovy | 148 - validation/src/test/resources/logback.xml | 14 - 107 files changed, 220 insertions(+), 11757 deletions(-) delete mode 100644 core-processor/src/main/java/io/micronaut/context/visitor/ValidationVisitor.java delete mode 100644 validation/build.gradle delete mode 100644 validation/src/main/java/io/micronaut/validation/Validated.java delete mode 100644 validation/src/main/java/io/micronaut/validation/ValidatingInterceptor.java delete mode 100644 validation/src/main/java/io/micronaut/validation/exceptions/ConstraintExceptionHandler.java delete mode 100644 validation/src/main/java/io/micronaut/validation/exceptions/package-info.java delete mode 100644 validation/src/main/java/io/micronaut/validation/package-info.java delete mode 100644 validation/src/main/java/io/micronaut/validation/validator/DefaultAnnotatedElementValidator.java delete mode 100644 validation/src/main/java/io/micronaut/validation/validator/DefaultClockProvider.java delete mode 100644 validation/src/main/java/io/micronaut/validation/validator/DefaultConstraintDescriptor.java delete mode 100644 validation/src/main/java/io/micronaut/validation/validator/DefaultValidator.java delete mode 100644 validation/src/main/java/io/micronaut/validation/validator/DefaultValidatorConfiguration.java delete mode 100644 validation/src/main/java/io/micronaut/validation/validator/DefaultValidatorFactory.java delete mode 100644 validation/src/main/java/io/micronaut/validation/validator/ExecutableMethodValidator.java delete mode 100644 validation/src/main/java/io/micronaut/validation/validator/IntrospectedBeanDescriptor.java delete mode 100644 validation/src/main/java/io/micronaut/validation/validator/ReactiveValidator.java delete mode 100644 validation/src/main/java/io/micronaut/validation/validator/Validator.java delete mode 100644 validation/src/main/java/io/micronaut/validation/validator/ValidatorConfiguration.java delete mode 100644 validation/src/main/java/io/micronaut/validation/validator/constraints/AbstractPatternValidator.java delete mode 100644 validation/src/main/java/io/micronaut/validation/validator/constraints/ConstraintValidator.java delete mode 100644 validation/src/main/java/io/micronaut/validation/validator/constraints/ConstraintValidatorContext.java delete mode 100644 validation/src/main/java/io/micronaut/validation/validator/constraints/ConstraintValidatorRegistry.java delete mode 100644 validation/src/main/java/io/micronaut/validation/validator/constraints/DecimalMaxValidator.java delete mode 100644 validation/src/main/java/io/micronaut/validation/validator/constraints/DecimalMinValidator.java delete mode 100644 validation/src/main/java/io/micronaut/validation/validator/constraints/DefaultConstraintValidators.java delete mode 100644 validation/src/main/java/io/micronaut/validation/validator/constraints/DigitsValidator.java delete mode 100644 validation/src/main/java/io/micronaut/validation/validator/constraints/DomainNameUtil.java delete mode 100644 validation/src/main/java/io/micronaut/validation/validator/constraints/EmailValidator.java delete mode 100644 validation/src/main/java/io/micronaut/validation/validator/constraints/PatternValidator.java delete mode 100644 validation/src/main/java/io/micronaut/validation/validator/constraints/SizeValidator.java delete mode 100644 validation/src/main/java/io/micronaut/validation/validator/extractors/DefaultValueExtractors.java delete mode 100644 validation/src/main/java/io/micronaut/validation/validator/extractors/SimpleValueReceiver.java delete mode 100644 validation/src/main/java/io/micronaut/validation/validator/extractors/UnwrapByDefaultValueExtractor.java delete mode 100644 validation/src/main/java/io/micronaut/validation/validator/extractors/ValueExtractorRegistry.java delete mode 100644 validation/src/main/java/io/micronaut/validation/validator/messages/DefaultValidationMessages.java delete mode 100644 validation/src/main/java/io/micronaut/validation/validator/resolver/CompositeTraversableResolver.java delete mode 100644 validation/src/main/resources/META-INF/services/io.micronaut.inject.annotation.AnnotatedElementValidator delete mode 100644 validation/src/test/groovy/io/micronaut/validation/BookmarkController.java delete mode 100644 validation/src/test/groovy/io/micronaut/validation/BookmarkControllerSpec.groovy delete mode 100644 validation/src/test/groovy/io/micronaut/validation/Foo.java delete mode 100644 validation/src/test/groovy/io/micronaut/validation/JavaClient.java delete mode 100644 validation/src/test/groovy/io/micronaut/validation/PaginationCommand.java delete mode 100644 validation/src/test/groovy/io/micronaut/validation/Pojo.java delete mode 100644 validation/src/test/groovy/io/micronaut/validation/PojoNoIntrospection.java delete mode 100644 validation/src/test/groovy/io/micronaut/validation/ValidatedController.java delete mode 100644 validation/src/test/groovy/io/micronaut/validation/ValidatedParseSpec.groovy delete mode 100644 validation/src/test/groovy/io/micronaut/validation/ValidatedSpec.groovy delete mode 100644 validation/src/test/groovy/io/micronaut/validation/ValidationVisitorSpec.groovy delete mode 100644 validation/src/test/groovy/io/micronaut/validation/WebSocketClientValidationSpec.groovy delete mode 100644 validation/src/test/groovy/io/micronaut/validation/WebSocketValidationServerHandler.groovy delete mode 100644 validation/src/test/groovy/io/micronaut/validation/executable/NullablePrimitiveSpec.groovy delete mode 100644 validation/src/test/groovy/io/micronaut/validation/internal/InteralApiRuleSpec.groovy delete mode 100644 validation/src/test/groovy/io/micronaut/validation/properties/MixedCasePropertySpec.groovy delete mode 100644 validation/src/test/groovy/io/micronaut/validation/validator/ConstraintValidatorRegistrySpec.groovy delete mode 100644 validation/src/test/groovy/io/micronaut/validation/validator/ValidatorGroupsSpec.groovy delete mode 100644 validation/src/test/groovy/io/micronaut/validation/validator/ValidatorSpec.groovy delete mode 100644 validation/src/test/groovy/io/micronaut/validation/validator/ast/SomeAnn.java delete mode 100644 validation/src/test/groovy/io/micronaut/validation/validator/ast/ValidateAnnotationMetadataSpec.groovy delete mode 100644 validation/src/test/groovy/io/micronaut/validation/validator/constraints/ConstraintMessagesSpec.groovy delete mode 100644 validation/src/test/groovy/io/micronaut/validation/validator/constraints/ConstraintsSpec.groovy delete mode 100644 validation/src/test/groovy/io/micronaut/validation/validator/constraints/custom/AlwaysInvalidConstraint.java delete mode 100644 validation/src/test/groovy/io/micronaut/validation/validator/constraints/custom/CustomConstraintsSpec.groovy delete mode 100644 validation/src/test/groovy/io/micronaut/validation/validator/constraints/custom/CustomConstraintsValidatorFactory.java delete mode 100644 validation/src/test/groovy/io/micronaut/validation/validator/constraints/custom/CustomMessageConstraint.java delete mode 100644 validation/src/test/groovy/io/micronaut/validation/validator/customwithdefaultconstraints/EmployeeExperienceConstraint.java delete mode 100644 validation/src/test/groovy/io/micronaut/validation/validator/customwithdefaultconstraints/EmployeeExperienceConstraintValidator.java delete mode 100644 validation/src/test/groovy/io/micronaut/validation/validator/customwithdefaultconstraints/EmployeeValidationsSpec.groovy delete mode 100644 validation/src/test/groovy/io/micronaut/validation/validator/map/MapValidatorSpec.groovy delete mode 100644 validation/src/test/groovy/io/micronaut/validation/validator/pojo/ContextValidationSpec.groovy delete mode 100644 validation/src/test/groovy/io/micronaut/validation/validator/pojo/FavoriteWebsSpec.groovy delete mode 100644 validation/src/test/groovy/io/micronaut/validation/validator/pojo/NameAndLastNameValidator.java delete mode 100644 validation/src/test/groovy/io/micronaut/validation/validator/pojo/PojoBodyParameterSpec.groovy delete mode 100644 validation/src/test/groovy/io/micronaut/validation/validator/pojo/PojoConfigProps.java delete mode 100644 validation/src/test/groovy/io/micronaut/validation/validator/pojo/PojoConfigurationPropertiesSpec.groovy delete mode 100644 validation/src/test/groovy/io/micronaut/validation/validator/pojo/PojoValidatorSpec.groovy delete mode 100644 validation/src/test/groovy/io/micronaut/validation/validator/pojo/ValidURLs.java delete mode 100644 validation/src/test/groovy/io/micronaut/validation/validator/reactive/ReactiveMethodValidationSpec.groovy delete mode 100644 validation/src/test/resources/logback.xml diff --git a/benchmarks/build.gradle b/benchmarks/build.gradle index f2c38f5d344..4da2b63b600 100644 --- a/benchmarks/build.gradle +++ b/benchmarks/build.gradle @@ -9,15 +9,27 @@ dependencies { jmhAnnotationProcessor libs.bundles.asm jmhAnnotationProcessor libs.jmh.generator.annprocess - annotationProcessor project(":validation") - compileOnly project(":validation") + annotationProcessor platform(libs.test.boms.micronaut.validation) + annotationProcessor (libs.micronaut.validation.processor) { + exclude group: 'io.micronaut' + } + + compileOnly platform(libs.test.boms.micronaut.validation) + compileOnly (libs.micronaut.validation) { + exclude group: 'io.micronaut' + } + api project(":inject") api project(":inject-java-test") - api project(":validation") api project(":http-server") api project(":router") api project(":runtime") + api platform(libs.test.boms.micronaut.validation) + api (libs.micronaut.validation) { + exclude group: 'io.micronaut' + } + jmh libs.jmh.core } jmh { diff --git a/context/build.gradle b/context/build.gradle index c527529d173..988876bedba 100644 --- a/context/build.gradle +++ b/context/build.gradle @@ -7,13 +7,18 @@ dependencies { annotationProcessor project(":graal") api project(':inject') api project(':aop') - api libs.managed.validation compileOnly project(':core-reactive') compileOnly project(':core-processor') compileOnly libs.log4j compileOnly libs.managed.logback.classic + // Support validation annotations + compileOnly platform(libs.test.boms.micronaut.validation) + compileOnly (libs.micronaut.validation) { + exclude group: 'io.micronaut' + } + testCompileOnly project(":inject-groovy") testAnnotationProcessor project(":inject-java") testImplementation project(":core-reactive") diff --git a/core-processor/build.gradle b/core-processor/build.gradle index 4f5698c7530..7667825f58a 100644 --- a/core-processor/build.gradle +++ b/core-processor/build.gradle @@ -13,5 +13,4 @@ dependencies { } compileOnly libs.kotlin.stdlib.jdk8 - compileOnly libs.managed.validation } diff --git a/core-processor/src/main/java/io/micronaut/context/visitor/ValidationVisitor.java b/core-processor/src/main/java/io/micronaut/context/visitor/ValidationVisitor.java deleted file mode 100644 index d10f41d2376..00000000000 --- a/core-processor/src/main/java/io/micronaut/context/visitor/ValidationVisitor.java +++ /dev/null @@ -1,120 +0,0 @@ -/* - * Copyright 2017-2020 original authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.micronaut.context.visitor; - -import io.micronaut.core.annotation.Internal; -import io.micronaut.core.annotation.NextMajorVersion; -import io.micronaut.core.annotation.NonNull; -import io.micronaut.inject.ast.ClassElement; -import io.micronaut.inject.ast.ConstructorElement; -import io.micronaut.inject.ast.Element; -import io.micronaut.inject.ast.FieldElement; -import io.micronaut.inject.ast.MethodElement; -import io.micronaut.inject.processing.ProcessingException; -import io.micronaut.inject.validation.RequiresValidation; -import io.micronaut.inject.visitor.TypeElementVisitor; -import io.micronaut.inject.visitor.VisitorContext; - -import java.util.Arrays; -import java.util.HashSet; -import java.util.Set; - -/** - * The visitor adds Validated annotation if one of the parameters is a constraint or @Valid. - * - * @author Denis Stepanov - * @since 4.0.0 - */ -@Internal -@NextMajorVersion("This class needs to be moved to the validation module") -public class ValidationVisitor implements TypeElementVisitor { - - private static final String ANN_CONSTRAINT = "javax.validation.Constraint"; - private static final String ANN_VALID = "javax.validation.Valid"; - - private ClassElement classElement; - - @Override - public Set getSupportedAnnotationNames() { - return new HashSet<>(Arrays.asList(ANN_CONSTRAINT, ANN_VALID)); - } - - @Override - public int getOrder() { - return 10; // Should run before ConfigurationReaderVisitor - } - - @NonNull - @Override - public VisitorKind getVisitorKind() { - return VisitorKind.ISOLATING; - } - - @Override - public void visitClass(ClassElement element, VisitorContext context) { - classElement = element; - } - - @Override - public void visitConstructor(ConstructorElement element, VisitorContext context) { - if (classElement == null) { - return; - } - if (requiresValidation(element, true) || parametersRequireValidation(element, true)) { - element.annotate(RequiresValidation.class); - classElement.annotate(RequiresValidation.class); - } - } - - @Override - public void visitMethod(MethodElement element, VisitorContext context) { - if (classElement == null) { - return; - } - boolean isPrivate = element.isPrivate(); - boolean isAbstract = element.getOwningType().isInterface() || element.getOwningType().isAbstract(); - boolean requireOnConstraint = isAbstract || !isPrivate; - if (requiresValidation(element, requireOnConstraint) || parametersRequireValidation(element, requireOnConstraint)) { - if (isPrivate) { - throw new ProcessingException(element, "Method annotated for validation but is declared private. Change the method to be non-private in order for AOP advice to be applied."); - } - element.annotate(RequiresValidation.class); - classElement.annotate(RequiresValidation.class); - } - } - - @Override - public void visitField(FieldElement element, VisitorContext context) { - if (classElement == null) { - return; - } - if (requiresValidation(element, true)) { - element.annotate(RequiresValidation.class); - classElement.annotate(RequiresValidation.class); - } - } - - private boolean parametersRequireValidation(MethodElement element, boolean requireOnConstraint) { - return Arrays.stream(element.getParameters()).anyMatch(e -> requiresValidation(e, requireOnConstraint)); - } - - private boolean requiresValidation(Element e, boolean requireOnConstraint) { - if (requireOnConstraint && e.hasStereotype(ANN_CONSTRAINT)) { - return true; - } - return e.hasStereotype(ANN_VALID); - } -} diff --git a/core-processor/src/main/java/io/micronaut/inject/writer/DispatchWriter.java b/core-processor/src/main/java/io/micronaut/inject/writer/DispatchWriter.java index b9a07bd8772..e9a74547875 100644 --- a/core-processor/src/main/java/io/micronaut/inject/writer/DispatchWriter.java +++ b/core-processor/src/main/java/io/micronaut/inject/writer/DispatchWriter.java @@ -16,6 +16,7 @@ package io.micronaut.inject.writer; import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.NonNull; import io.micronaut.core.reflect.ReflectionUtils; import io.micronaut.core.util.ArrayUtils; import io.micronaut.inject.ast.ClassElement; @@ -32,7 +33,6 @@ import org.objectweb.asm.commons.Method; import org.objectweb.asm.commons.TableSwitchGenerator; -import javax.validation.constraints.NotNull; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; @@ -386,7 +386,7 @@ default void writeDispatchMulti(GeneratorAdapter writer, int methodIndex) { */ @Internal public static final class FieldGetDispatchTarget implements DispatchTarget { - @NotNull + @NonNull final FieldElement beanField; public FieldGetDispatchTarget(FieldElement beanField) { @@ -421,7 +421,7 @@ public void writeDispatchOne(GeneratorAdapter writer) { pushBoxPrimitiveIfNecessary(propertyType, writer); } - @NotNull + @NonNull public FieldElement getField() { return beanField; } @@ -432,7 +432,7 @@ public FieldElement getField() { */ @Internal public static final class FieldSetDispatchTarget implements DispatchTarget { - @NotNull + @NonNull final FieldElement beanField; public FieldSetDispatchTarget(FieldElement beanField) { @@ -472,7 +472,7 @@ public void writeDispatchOne(GeneratorAdapter writer) { writer.push((String) null); } - @NotNull + @NonNull public FieldElement getField() { return beanField; } diff --git a/core-processor/src/main/resources/META-INF/services/io.micronaut.inject.visitor.TypeElementVisitor b/core-processor/src/main/resources/META-INF/services/io.micronaut.inject.visitor.TypeElementVisitor index 8d781117e40..46ee8919429 100644 --- a/core-processor/src/main/resources/META-INF/services/io.micronaut.inject.visitor.TypeElementVisitor +++ b/core-processor/src/main/resources/META-INF/services/io.micronaut.inject.visitor.TypeElementVisitor @@ -2,6 +2,5 @@ io.micronaut.inject.beans.visitor.IntrospectedTypeElementVisitor io.micronaut.context.visitor.BeanImportVisitor io.micronaut.context.visitor.ContextConfigurerVisitor io.micronaut.context.visitor.ExecutableVisitor -io.micronaut.context.visitor.ValidationVisitor io.micronaut.context.visitor.InternalApiTypeElementVisitor io.micronaut.context.visitor.ConfigurationReaderVisitor diff --git a/discovery-core/src/main/java/io/micronaut/health/HealthStatus.java b/discovery-core/src/main/java/io/micronaut/health/HealthStatus.java index 868baa29402..c1ecdb75132 100644 --- a/discovery-core/src/main/java/io/micronaut/health/HealthStatus.java +++ b/discovery-core/src/main/java/io/micronaut/health/HealthStatus.java @@ -17,9 +17,9 @@ import com.fasterxml.jackson.annotation.JsonValue; import io.micronaut.core.annotation.Introspected; +import io.micronaut.core.annotation.NonNull; import io.micronaut.core.annotation.ReflectiveAccess; -import javax.validation.constraints.NotNull; import java.util.Optional; /** @@ -86,7 +86,7 @@ public HealthStatus( /** * @param name The name of the status */ - public HealthStatus(@NotNull String name) { + public HealthStatus(@NonNull String name) { this(name, null, null, null); } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 5bd439ee5ea..811f5db9f82 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -44,6 +44,7 @@ micronaut-sql = "4.7.2" micronaut-test = "4.0.0-SNAPSHOT" micronaut-serde = "2.0.0-SNAPSHOT" micronaut-tracing = "5.0.0-SNAPSHOT" +micronaut-validation = "4.0.0-SNAPSHOT" micrometer = "1.10.2" neo4j-java-driver = "1.4.5" selenium = "4.7.2" @@ -71,7 +72,6 @@ managed-reactive-streams = "1.0.4" managed-reactor = "3.4.24" managed-slf4j = "2.0.4" managed-snakeyaml = "1.33" -managed-validation = "2.0.1.Final" managed-java-parser-core = "3.24.9" managed-ksp = "1.8.0-1.0.9" micronaut-docs = "2.0.0" @@ -81,6 +81,7 @@ micronaut-docs = "2.0.0" test-boms-micronaut-aws = { module = "io.micronaut.aws:micronaut-aws-bom", version.ref = "micronaut-aws" } test-boms-micronaut-sql = { module = "io.micronaut.sql:micronaut-sql-bom", version.ref = "micronaut-sql" } test-boms-micronaut-tracing = { module = "io.micronaut.tracing:micronaut-tracing-bom", version.ref = "micronaut-tracing" } +test-boms-micronaut-validation = { module = "io.micronaut.validation:micronaut-validation-bom", version.ref = "micronaut-validation" } boms-groovy = { module = "org.apache.groovy:groovy-bom", version.ref = "managed-groovy" } boms-netty = { module = "io.netty:netty-bom", version.ref = "managed-netty" } @@ -131,8 +132,6 @@ managed-logback-classic = { module = "ch.qos.logback:logback-classic", version.r managed-snakeyaml = { module = "org.yaml:snakeyaml", version.ref = "managed-snakeyaml" } -managed-validation = { module = "javax.validation:validation-api", version.ref = "managed-validation" } - # # Other libraries are used by Micronaut but will not appear in the BOM # @@ -248,6 +247,9 @@ systemlambda = { module = "com.github.stefanbirkner:system-lambda", version.ref micronaut-tracing-jaeger = { module = "io.micronaut.tracing:micronaut-tracing-jaeger" } micronaut-tracing-zipkin = { module = "io.micronaut.tracing:micronaut-tracing-zipkin" } +micronaut-validation = { module = "io.micronaut.validation:micronaut-validation" } +micronaut-validation-processor = { module = "io.micronaut.validation:micronaut-validation-processor" } + testcontainers-spock = { module = "org.testcontainers:spock", version.ref = "testcontainers" } vertx = { module = "io.vertx:vertx-core", version.ref = "vertx" } diff --git a/http-client/build.gradle b/http-client/build.gradle index b174dfb9e7f..82f794893d9 100644 --- a/http-client/build.gradle +++ b/http-client/build.gradle @@ -17,13 +17,23 @@ dependencies { api project(":http-netty") api libs.managed.netty.handler.proxy - testAnnotationProcessor project(":validation") + testAnnotationProcessor platform(libs.test.boms.micronaut.validation) + testAnnotationProcessor (libs.micronaut.validation.processor) { + exclude group: 'io.micronaut' + } testAnnotationProcessor project(":inject-java") testCompileOnly project(":inject-groovy") - testImplementation project(":validation") testImplementation project(":inject") + testImplementation platform(libs.test.boms.micronaut.validation) + testImplementation (libs.micronaut.validation) { + exclude group: 'io.micronaut' + } + testImplementation (libs.micronaut.validation.processor) { // For Groovy + exclude group: 'io.micronaut' + } + implementation libs.managed.reactor testImplementation project(":retry") diff --git a/http-server-netty/build.gradle b/http-server-netty/build.gradle index 3b2e21c91d1..df3c79a65aa 100644 --- a/http-server-netty/build.gradle +++ b/http-server-netty/build.gradle @@ -31,13 +31,26 @@ dependencies { compileOnly libs.kotlin.stdlib compileOnly libs.managed.netty.transport.native.unix.common - testImplementation 'org.openjdk.jmh:jmh-core:1.36' - testAnnotationProcessor 'org.openjdk.jmh:jmh-generator-annprocess:1.36' - + testImplementation libs.jmh.core + testAnnotationProcessor libs.jmh.generator.annprocess testCompileOnly project(":inject-groovy") testCompileOnly(libs.jetbrains.annotations) + testAnnotationProcessor project(":inject-java") + testAnnotationProcessor platform(libs.test.boms.micronaut.validation) + testAnnotationProcessor (libs.micronaut.validation.processor) { + exclude group: 'io.micronaut' + } + + testImplementation platform(libs.test.boms.micronaut.validation) + testImplementation (libs.micronaut.validation) { + exclude group: 'io.micronaut' + } + testImplementation (libs.micronaut.validation.processor) { // For Groovy + exclude group: 'io.micronaut' + } + testImplementation project(":inject") testImplementation project(":inject-java-test") testImplementation project(":http-client") @@ -81,6 +94,12 @@ dependencies { testImplementation project(":websocket") } +tasks.withType(Test).configureEach { + forkEvery = 100 + maxParallelForks = 4 + useJUnitPlatform() +} + //tasks.withType(Test).configureEach { // testLogging { // showStandardStreams = true diff --git a/http-server-tck/build.gradle.kts b/http-server-tck/build.gradle.kts index 2f984421050..c4db82b7203 100644 --- a/http-server-tck/build.gradle.kts +++ b/http-server-tck/build.gradle.kts @@ -3,12 +3,21 @@ plugins { } dependencies { annotationProcessor(projects.injectJava) - annotationProcessor(projects.validation) + + annotationProcessor(platform(libs.test.boms.micronaut.validation)) + annotationProcessor(libs.micronaut.validation.processor) { + exclude(group = "io.micronaut") + } annotationProcessor(projects.httpValidation) - implementation(projects.validation) + + implementation(platform(libs.test.boms.micronaut.validation)) + implementation(libs.micronaut.validation) { + exclude(group = "io.micronaut") + } implementation(projects.runtime) implementation(projects.jacksonDatabind) implementation(projects.inject) + api(projects.httpServer) api(projects.httpClientCore) api(libs.junit.jupiter.api) diff --git a/inject-groovy/build.gradle b/inject-groovy/build.gradle index 26c8a865052..b28b54e2b63 100644 --- a/inject-groovy/build.gradle +++ b/inject-groovy/build.gradle @@ -12,7 +12,6 @@ micronautBuild { dependencies { api project(":core-processor") api libs.managed.groovy -// testImplementation 'javax.validation:validation-api:1.1.0.Final' testImplementation project(":context") testImplementation libs.javax.inject testImplementation libs.spotbugs @@ -25,7 +24,13 @@ dependencies { testImplementation project(":jackson-databind") testImplementation project(":inject-test-utils") testImplementation project(":inject-groovy-test") - testImplementation project(":validation") + testImplementation platform(libs.test.boms.micronaut.validation) + testImplementation (libs.micronaut.validation) { + exclude group: 'io.micronaut' + } + testImplementation (libs.micronaut.validation.processor) { + exclude group: 'io.micronaut' + } testImplementation(libs.neo4j.bolt) testImplementation libs.managed.groovy.json testImplementation libs.blaze.persistence.core diff --git a/inject-java-test/build.gradle b/inject-java-test/build.gradle index b016798680c..f7fa8f89d6f 100644 --- a/inject-java-test/build.gradle +++ b/inject-java-test/build.gradle @@ -22,12 +22,19 @@ dependencies { testAnnotationProcessor project(":inject-java") testCompileOnly project(":inject-groovy") testImplementation project(":jackson-databind") - testImplementation libs.managed.validation testImplementation libs.javax.persistence testImplementation project(":runtime") testImplementation libs.javax.inject testImplementation libs.blaze.persistence.core testImplementation libs.smallrye + + testImplementation platform(libs.test.boms.micronaut.validation) + testImplementation (libs.micronaut.validation.processor) { + exclude group: 'io.micronaut' + } + testImplementation (libs.micronaut.validation) { + exclude group: 'io.micronaut' + } } tasks.named("sourcesJar") { diff --git a/inject-java-test/src/test/groovy/io/micronaut/inject/visitor/beans/BeanIntrospectionSpec.groovy b/inject-java-test/src/test/groovy/io/micronaut/inject/visitor/beans/BeanIntrospectionSpec.groovy index 6d571bc0dd8..490bb811d94 100644 --- a/inject-java-test/src/test/groovy/io/micronaut/inject/visitor/beans/BeanIntrospectionSpec.groovy +++ b/inject-java-test/src/test/groovy/io/micronaut/inject/visitor/beans/BeanIntrospectionSpec.groovy @@ -5,7 +5,6 @@ import com.fasterxml.jackson.annotation.JsonClassDescription import com.fasterxml.jackson.annotation.JsonCreator import com.fasterxml.jackson.annotation.JsonProperty import com.fasterxml.jackson.databind.ObjectMapper -import groovy.transform.PackageScope import io.micronaut.annotation.processing.TypeElementVisitorProcessor import io.micronaut.annotation.processing.test.AbstractTypeElementSpec import io.micronaut.annotation.processing.test.JavaParser @@ -13,7 +12,6 @@ import io.micronaut.context.ApplicationContext import io.micronaut.context.annotation.Executable import io.micronaut.context.annotation.Replaces import io.micronaut.context.visitor.ConfigurationReaderVisitor -import io.micronaut.context.visitor.ValidationVisitor import io.micronaut.core.annotation.Introspected import io.micronaut.core.beans.BeanIntrospection import io.micronaut.core.beans.BeanIntrospectionReference @@ -30,6 +28,7 @@ import io.micronaut.inject.ExecutableMethod import io.micronaut.inject.beans.visitor.IntrospectedTypeElementVisitor import io.micronaut.inject.visitor.TypeElementVisitor import io.micronaut.jackson.modules.BeanIntrospectionModule +import io.micronaut.validation.visitor.ValidationVisitor import jakarta.inject.Singleton import spock.lang.IgnoreIf import spock.lang.Issue @@ -45,7 +44,6 @@ import javax.validation.constraints.Min import javax.validation.constraints.NotBlank import javax.validation.constraints.Size import java.lang.reflect.Field -import java.time.Instant class BeanIntrospectionSpec extends AbstractTypeElementSpec { diff --git a/inject-java/build.gradle b/inject-java/build.gradle index 1e4f20c564c..7b17e4e0579 100644 --- a/inject-java/build.gradle +++ b/inject-java/build.gradle @@ -14,11 +14,16 @@ dependencies { if (!JavaVersion.current().isJava9Compatible()) { compileOnly files(org.gradle.internal.jvm.Jvm.current().toolsJar) } - compileOnly libs.managed.validation testImplementation project(":context") testImplementation project(':aop') + testAnnotationProcessor project(":inject-java") + testAnnotationProcessor platform(libs.test.boms.micronaut.validation) + testAnnotationProcessor (libs.micronaut.validation.processor) { + exclude group: 'io.micronaut' + } + testImplementation project(":inject-java-test") testImplementation project(":inject-test-utils") testImplementation project(":runtime") @@ -34,17 +39,22 @@ dependencies { testImplementation files(org.gradle.internal.jvm.Jvm.current().toolsJar) } testImplementation libs.micrometer.core - testImplementation(libs.micronaut.session) + testImplementation (libs.micronaut.session) { + exclude group: 'io.micronaut' + } testImplementation(project(":http-server")) - testImplementation project(":validation") + testImplementation platform(libs.test.boms.micronaut.validation) + testImplementation (libs.micronaut.validation) { + exclude group: 'io.micronaut' + } + testImplementation (libs.micronaut.validation.processor) { + exclude group: 'io.micronaut' + } testImplementation project(":jackson-databind") testImplementation libs.junit.jupiter.api testImplementation(platform(libs.test.boms.micronaut.tracing)) testImplementation(libs.micronaut.tracing.zipkin) { - exclude module: 'micronaut-bom' - exclude module: 'micronaut-http-client' - exclude module: 'micronaut-inject' - exclude module: 'micronaut-runtime' + exclude group: 'io.micronaut' } testImplementation libs.javax.annotation.api testImplementation libs.managed.snakeyaml diff --git a/inject-kotlin-test/build.gradle b/inject-kotlin-test/build.gradle index 5bd45de21c1..e84b043181e 100644 --- a/inject-kotlin-test/build.gradle +++ b/inject-kotlin-test/build.gradle @@ -17,7 +17,6 @@ dependencies { implementation(libs.kotlin.compiler.embeddable) implementation "com.squareup.okio:okio:3.2.0" implementation "io.github.classgraph:classgraph:4.8.149" - testImplementation libs.managed.validation testImplementation libs.javax.persistence testImplementation project(":runtime") api libs.blaze.persistence.core diff --git a/inject-kotlin/build.gradle b/inject-kotlin/build.gradle index 3c9610e8d7d..070c68aa5d4 100644 --- a/inject-kotlin/build.gradle +++ b/inject-kotlin/build.gradle @@ -20,22 +20,34 @@ dependencies { api files(org.gradle.internal.jvm.Jvm.current().toolsJar) } kspTest(project) + kspTest platform(libs.test.boms.micronaut.validation) + kspTest (libs.micronaut.validation.processor) { + exclude group: 'io.micronaut' + } + testImplementation project(":jackson-databind") testImplementation project(":inject-kotlin-test") testImplementation libs.kotlin.stdlib testImplementation project(':http-client') testImplementation libs.managed.jackson.annotations - testImplementation libs.managed.validation testImplementation libs.managed.reactor testImplementation libs.hibernate - testImplementation project(":validation") + testImplementation platform(libs.test.boms.micronaut.validation) + testImplementation (libs.micronaut.validation) { + exclude group: 'io.micronaut' + } + testImplementation (libs.micronaut.validation.processor) { + exclude group: 'io.micronaut' + } testImplementation libs.javax.persistence testImplementation project(":runtime") testImplementation(libs.neo4j.bolt) testImplementation libs.kotlinx.coroutines.core testImplementation libs.kotlinx.coroutines.jdk8 testImplementation libs.kotlinx.coroutines.rx2 - testImplementation libs.micronaut.test.junit5 + testImplementation (libs.micronaut.test.junit5) { + exclude group: 'io.micronaut' + } testImplementation libs.kotlin.kotest.junit5 } diff --git a/inject/build.gradle b/inject/build.gradle index 4de0ce71d31..8a108a1b950 100644 --- a/inject/build.gradle +++ b/inject/build.gradle @@ -19,20 +19,17 @@ dependencies { compileOnly libs.managed.snakeyaml compileOnly libs.managed.groovy compileOnly libs.kotlin.stdlib.jdk8 - compileOnly libs.managed.validation -// testImplementation libs.managed.validation testImplementation project(":context") testImplementation project(":inject-groovy") testImplementation project(":inject-test-utils") testImplementation libs.systemlambda testImplementation libs.managed.snakeyaml testRuntimeOnly libs.junit.jupiter.engine - } -tasks.withType(Test) { - if(JavaVersion.current().majorVersion.toInteger() >= 17) { +tasks.withType(Test).configureEach { + if (JavaVersion.current().majorVersion.toInteger() >= 17) { logger.warn("Opening java.util, so SystemLambda can work") jvmArgs += ['--add-opens', 'java.base/java.util=ALL-UNNAMED'] } diff --git a/inject/src/main/java/io/micronaut/context/ProviderUtils.java b/inject/src/main/java/io/micronaut/context/ProviderUtils.java index df8c0268787..bc00f6a55fb 100644 --- a/inject/src/main/java/io/micronaut/context/ProviderUtils.java +++ b/inject/src/main/java/io/micronaut/context/ProviderUtils.java @@ -15,10 +15,9 @@ */ package io.micronaut.context; +import io.micronaut.core.annotation.NonNull; import jakarta.inject.Provider; -import javax.validation.constraints.NotNull; - /** * Helper methods for dealing with {@link javax.inject.Provider}. * @@ -52,7 +51,7 @@ private static final class MemoizingProvider implements Provider { private Provider delegate = this::initialize; private boolean initialized; - MemoizingProvider(@NotNull Provider actual) { + MemoizingProvider(@NonNull Provider actual) { this.actual = actual; } diff --git a/management/build.gradle b/management/build.gradle index 6b7ba33bf80..d217ed00903 100644 --- a/management/build.gradle +++ b/management/build.gradle @@ -10,8 +10,13 @@ dependencies { api project(":discovery-core") compileOnly project(":jackson-databind") compileOnly(libs.micronaut.sql.jdbc) { - exclude module:'micronaut-inject' - exclude module:'micronaut-bom' + exclude group: 'io.micronaut' + } + + // Support validation annotations + compileOnly platform(libs.test.boms.micronaut.validation) + compileOnly (libs.micronaut.validation) { + exclude group: 'io.micronaut' } implementation libs.managed.reactor @@ -21,8 +26,7 @@ dependencies { testImplementation project(":http-server-netty") testImplementation project(":jackson-databind") testImplementation(libs.micronaut.sql.jdbc.tomcat) { - exclude module:'micronaut-inject' - exclude module:'micronaut-bom' + exclude group: 'io.micronaut' } testImplementation libs.managed.groovy.json diff --git a/management/src/main/java/io/micronaut/management/endpoint/env/EnvironmentEndpointFilter.java b/management/src/main/java/io/micronaut/management/endpoint/env/EnvironmentEndpointFilter.java index 5d459bfa109..9b48f09af3d 100644 --- a/management/src/main/java/io/micronaut/management/endpoint/env/EnvironmentEndpointFilter.java +++ b/management/src/main/java/io/micronaut/management/endpoint/env/EnvironmentEndpointFilter.java @@ -15,7 +15,7 @@ */ package io.micronaut.management.endpoint.env; -import javax.validation.constraints.NotNull; +import io.micronaut.core.annotation.NonNull; /** * A bean interface that allows hiding or masking of parts of the environment and its property sources when they are @@ -31,5 +31,5 @@ public interface EnvironmentEndpointFilter { * * @param specification a specification of which properties are masked or hidden from the endpoint. */ - void specifyFiltering(@NotNull EnvironmentFilterSpecification specification); + void specifyFiltering(@NonNull EnvironmentFilterSpecification specification); } diff --git a/management/src/main/java/io/micronaut/management/endpoint/env/EnvironmentFilterSpecification.java b/management/src/main/java/io/micronaut/management/endpoint/env/EnvironmentFilterSpecification.java index ac8e47ef847..9ed385aef34 100644 --- a/management/src/main/java/io/micronaut/management/endpoint/env/EnvironmentFilterSpecification.java +++ b/management/src/main/java/io/micronaut/management/endpoint/env/EnvironmentFilterSpecification.java @@ -15,9 +15,9 @@ */ package io.micronaut.management.endpoint.env; +import io.micronaut.core.annotation.NonNull; import io.micronaut.core.util.SupplierUtil; -import javax.validation.constraints.NotNull; import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -54,7 +54,7 @@ public final class EnvironmentFilterSpecification { * * @return this */ - public @NotNull EnvironmentFilterSpecification maskAll() { + public @NonNull EnvironmentFilterSpecification maskAll() { allMasked = true; return this; } @@ -64,7 +64,7 @@ public final class EnvironmentFilterSpecification { * * @return this */ - public @NotNull EnvironmentFilterSpecification maskNone() { + public @NonNull EnvironmentFilterSpecification maskNone() { allMasked = false; return this; } @@ -77,7 +77,7 @@ public final class EnvironmentFilterSpecification { * @param keyPredicate A predicate to match against property keys for masking * @return this */ - public @NotNull EnvironmentFilterSpecification exclude(@NotNull Predicate keyPredicate) { + public @NonNull EnvironmentFilterSpecification exclude(@NonNull Predicate keyPredicate) { exclusions.add(keyPredicate); return this; } @@ -90,7 +90,7 @@ public final class EnvironmentFilterSpecification { * @param keys Literal keys that should be excluded * @return this */ - public @NotNull EnvironmentFilterSpecification exclude(@NotNull String... keys) { + public @NonNull EnvironmentFilterSpecification exclude(@NonNull String... keys) { if (keys.length > 0) { if (keys.length == 1) { exclusions.add(name -> keys[0].equals(name)); @@ -111,7 +111,7 @@ public final class EnvironmentFilterSpecification { * @param keyPatterns The patterns used to compare keys to exclude them * @return this */ - public @NotNull EnvironmentFilterSpecification exclude(@NotNull Pattern... keyPatterns) { + public @NonNull EnvironmentFilterSpecification exclude(@NonNull Pattern... keyPatterns) { if (keyPatterns.length > 0) { if (keyPatterns.length == 1) { exclusions.add(name -> keyPatterns[0].matcher(name).matches()); @@ -127,7 +127,7 @@ public final class EnvironmentFilterSpecification { * * @return this */ - public @NotNull EnvironmentFilterSpecification legacyMasking() { + public @NonNull EnvironmentFilterSpecification legacyMasking() { allMasked = false; for (Pattern pattern : LEGACY_MASK_PATTERNS.get()) { exclude(pattern); diff --git a/retry/build.gradle b/retry/build.gradle index 2b10ed67ead..d84ee4e6dd3 100644 --- a/retry/build.gradle +++ b/retry/build.gradle @@ -7,7 +7,15 @@ dependencies { api project(':context') api project(':core-reactive') + + // Support validation annotations + compileOnly platform(libs.test.boms.micronaut.validation) + compileOnly (libs.micronaut.validation) { + exclude group: 'io.micronaut' + } + implementation libs.managed.reactor + testImplementation project(":jackson-databind") testImplementation project(":discovery-core") } diff --git a/runtime/build.gradle b/runtime/build.gradle index 41d927146f0..ef36489b9ea 100644 --- a/runtime/build.gradle +++ b/runtime/build.gradle @@ -13,8 +13,6 @@ dependencies { api project(':inject') api project(':retry') - api libs.managed.validation - implementation libs.managed.reactor compileOnly libs.graal @@ -49,8 +47,8 @@ spotless { } } -tasks.withType(Test) { - if(JavaVersion.current().majorVersion.toInteger() >= 17) { +tasks.withType(Test).configureEach { + if (JavaVersion.current().majorVersion.toInteger() >= 17) { logger.warn("Opening java.util, so SystemLambda can work") jvmArgs += ['--add-opens', 'java.base/java.util=ALL-UNNAMED'] } diff --git a/settings.gradle b/settings.gradle index bb5ffa56320..77a71f34595 100644 --- a/settings.gradle +++ b/settings.gradle @@ -58,7 +58,6 @@ include "retry" include "router" include "runtime" include "runtime-osx" -include "validation" include "websocket" // test suites diff --git a/test-suite-graal/build.gradle b/test-suite-graal/build.gradle index 2a8cfa4d740..f0da892954d 100644 --- a/test-suite-graal/build.gradle +++ b/test-suite-graal/build.gradle @@ -22,13 +22,18 @@ dependencies { implementation project(":graal") implementation project(":http-server-netty") implementation project(":http-client") - implementation project(":validation") + implementation platform(libs.test.boms.micronaut.validation) + implementation(libs.micronaut.validation) { + exclude group: 'io.micronaut' + } implementation project(":inject-java") testAnnotationProcessor libs.bundles.asm testAnnotationProcessor project(":inject-java") - testImplementation(libs.micronaut.serde.jackson) - testImplementation libs.micronaut.test.junit5 + testImplementation (libs.micronaut.serde.jackson) + testImplementation (libs.micronaut.test.junit5) { + exclude group: 'io.micronaut' + } } tasks.withType(Test).configureEach { diff --git a/test-suite-groovy/build.gradle b/test-suite-groovy/build.gradle index 69eb884039c..5561a1eee2f 100644 --- a/test-suite-groovy/build.gradle +++ b/test-suite-groovy/build.gradle @@ -26,10 +26,18 @@ dependencies { testImplementation project(":http-server-netty") testImplementation project(":jackson-databind") testImplementation project(":runtime") - testImplementation project(":validation") + testImplementation platform(libs.test.boms.micronaut.validation) + testImplementation (libs.micronaut.validation) { + exclude group: 'io.micronaut' + } + testImplementation (libs.micronaut.validation.processor) { + exclude group: 'io.micronaut' + } testImplementation project(":inject") testImplementation project(":management") - testImplementation(libs.micronaut.session) + testImplementation (libs.micronaut.session) { + exclude group: 'io.micronaut' + } testImplementation libs.jcache testImplementation libs.managed.groovy.sql testImplementation libs.managed.groovy.templates diff --git a/test-suite-kotlin-ksp/build.gradle b/test-suite-kotlin-ksp/build.gradle index 69159ecedc5..cf99dd09659 100644 --- a/test-suite-kotlin-ksp/build.gradle +++ b/test-suite-kotlin-ksp/build.gradle @@ -43,14 +43,18 @@ dependencies { // Adding these for now since micronaut-test isnt resolving correctly ... probably need to upgrade gradle there too testImplementation libs.junit.jupiter.api - testImplementation project(":validation") + testImplementation platform(libs.test.boms.micronaut.validation) + testImplementation (libs.micronaut.validation) { + exclude group: 'io.micronaut' + } testImplementation project(":management") testImplementation project(':inject-java') testImplementation project(":inject") testImplementation libs.jcache - testImplementation project(':validation') testImplementation project(":http-client") - testImplementation(libs.micronaut.session) + testImplementation (libs.micronaut.session) { + exclude group: 'io.micronaut' + } testImplementation project(":jackson-databind") testImplementation libs.managed.groovy.templates @@ -59,14 +63,14 @@ dependencies { testImplementation libs.kotlin.kotest.junit5 testImplementation libs.logbook.netty kspTest project(':inject-kotlin') - kspTest project(':validation') + kspTest platform(libs.test.boms.micronaut.validation) + kspTest (libs.micronaut.validation.processor) { + exclude group: 'io.micronaut' + } testImplementation libs.javax.inject testImplementation(platform(libs.test.boms.micronaut.tracing)) testImplementation(libs.micronaut.tracing.zipkin) { - exclude module: 'micronaut-bom' - exclude module: 'micronaut-http-client' - exclude module: 'micronaut-inject' - exclude module: 'micronaut-runtime' + exclude group: 'io.micronaut' } testRuntimeOnly libs.junit.jupiter.engine diff --git a/test-suite-kotlin/build.gradle b/test-suite-kotlin/build.gradle index 40eaaa11fd6..fc5634cf5a7 100644 --- a/test-suite-kotlin/build.gradle +++ b/test-suite-kotlin/build.gradle @@ -44,14 +44,18 @@ dependencies { // Adding these for now since micronaut-test isnt resolving correctly ... probably need to upgrade gradle there too testImplementation libs.junit.jupiter.api - testImplementation project(":validation") + testImplementation platform(libs.test.boms.micronaut.validation) + testImplementation (libs.micronaut.validation) { + exclude group: 'io.micronaut' + } testImplementation project(":management") testImplementation project(':inject-java') testImplementation project(":inject") testImplementation libs.jcache - testImplementation project(':validation') testImplementation project(":http-client") - testImplementation(libs.micronaut.session) + testImplementation (libs.micronaut.session) { + exclude group: 'io.micronaut' + } testImplementation project(":jackson-databind") testImplementation libs.managed.groovy.templates @@ -60,14 +64,15 @@ dependencies { testImplementation libs.kotlin.kotest.junit5 testImplementation libs.logbook.netty kaptTest project(':inject-java') - kaptTest project(':validation') + kaptTest platform(libs.test.boms.micronaut.validation) + kaptTest (libs.micronaut.validation.processor) { + exclude group: 'io.micronaut' + } + testImplementation libs.javax.inject testImplementation(platform(libs.test.boms.micronaut.tracing)) testImplementation(libs.micronaut.tracing.zipkin) { - exclude module: 'micronaut-bom' - exclude module: 'micronaut-http-client' - exclude module: 'micronaut-inject' - exclude module: 'micronaut-runtime' + exclude group: 'io.micronaut' } testRuntimeOnly libs.junit.jupiter.engine diff --git a/test-suite/build.gradle b/test-suite/build.gradle index 177a22e58a7..7111eff23ba 100644 --- a/test-suite/build.gradle +++ b/test-suite/build.gradle @@ -31,6 +31,11 @@ repositories { dependencies { annotationProcessor project(":inject-java") + annotationProcessor platform(libs.test.boms.micronaut.validation) + annotationProcessor (libs.micronaut.validation.processor) { + exclude group: 'io.micronaut' + } + api project(":core-processor") testImplementation project(":context") @@ -38,7 +43,13 @@ dependencies { testImplementation project(":http-server-netty") testImplementation project(":jackson-databind") testImplementation project(":http-client") - testImplementation project(":validation") + testImplementation platform(libs.test.boms.micronaut.validation) + testImplementation (libs.micronaut.validation) { + exclude group: 'io.micronaut' + } + testImplementation (libs.micronaut.validation.processor) { // For Groovy + exclude group: 'io.micronaut' + } testImplementation project(":inject-groovy") testImplementation project(":inject-java") testImplementation project(":inject-java-test") @@ -47,7 +58,9 @@ dependencies { testImplementation project(":inject") testImplementation project(":function-client") testImplementation project(":function-web") - testImplementation(libs.micronaut.session) + testImplementation (libs.micronaut.session) { + exclude group: 'io.micronaut' + } // Adding these for now since micronaut-test isnt resolving correctly ... probably need to upgrade gradle there too testImplementation libs.junit.jupiter.api @@ -55,10 +68,7 @@ dependencies { testImplementation libs.jcache testImplementation(platform(libs.test.boms.micronaut.tracing)) testImplementation(libs.micronaut.tracing.jaeger) { - exclude module: 'micronaut-bom' - exclude module: 'micronaut-http-client' - exclude module: 'micronaut-inject' - exclude module: 'micronaut-runtime' + exclude group: 'io.micronaut' } testImplementation libs.managed.groovy.json testImplementation libs.managed.groovy.templates @@ -70,6 +80,10 @@ dependencies { testAnnotationProcessor project(":test-suite-helper") testAnnotationProcessor project(":inject-java") testAnnotationProcessor project(":test-suite") + testAnnotationProcessor platform(libs.test.boms.micronaut.validation) + testAnnotationProcessor (libs.micronaut.validation.processor) { + exclude group: 'io.micronaut' + } testRuntimeOnly(platform(libs.test.boms.micronaut.aws)) testRuntimeOnly libs.h2 diff --git a/validation/build.gradle b/validation/build.gradle deleted file mode 100644 index a2100d9e15e..00000000000 --- a/validation/build.gradle +++ /dev/null @@ -1,45 +0,0 @@ -plugins { - id "io.micronaut.build.internal.convention-library" -} - -micronautBuild { - core { - usesMicronautTestSpock() - } -} - -dependencies { - annotationProcessor project(":inject-java") - - api project(":inject") - api project(":core-reactive") - api libs.managed.validation - - compileOnly(libs.gorm) { - exclude(module: 'groovy') - } - compileOnly project(":http-server") - - implementation libs.managed.reactor - - testImplementation libs.spotbugs - testAnnotationProcessor project(":inject-java") - testCompileOnly project(":inject-groovy") - testImplementation project(":inject") - - testImplementation project(":http-client") - testImplementation project(":jackson-databind") - testImplementation project(":http-server-netty") - testImplementation libs.managed.groovy.json - testImplementation project(":inject-java-test") - -} -//compileTestGroovy.groovyOptions.forkOptions.jvmArgs = ['-Xdebug', '-Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=5005'] -//compileTestGroovy.groovyOptions.fork = true - - -spotless { - java { - targetExclude '**/io/micronaut/validation/validator/constraints/EmailValidator.java' - } -} diff --git a/validation/src/main/java/io/micronaut/validation/Validated.java b/validation/src/main/java/io/micronaut/validation/Validated.java deleted file mode 100644 index 4be5b158a3d..00000000000 --- a/validation/src/main/java/io/micronaut/validation/Validated.java +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Copyright 2017-2020 original authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.micronaut.validation; - -import io.micronaut.aop.Around; -import io.micronaut.context.annotation.Type; - -import java.lang.annotation.Documented; -import java.lang.annotation.ElementType; -import java.lang.annotation.Inherited; -import java.lang.annotation.Retention; -import java.lang.annotation.Target; - -import static java.lang.annotation.RetentionPolicy.RUNTIME; - -/** - * {@link Around} advice that ensures an objects methods are validated. - * - * @author Graeme Rocher - * @since 1.0 - */ -@Documented -@Retention(RUNTIME) -@Target({ElementType.TYPE, ElementType.METHOD}) -@Around -@Inherited -@Type(ValidatingInterceptor.class) -public @interface Validated { - - /** - * The validation groups that will be used for validation. - * - * @return The validation groups - * @since 3.5.0 - */ - Class[] groups() default {}; - -} diff --git a/validation/src/main/java/io/micronaut/validation/ValidatingInterceptor.java b/validation/src/main/java/io/micronaut/validation/ValidatingInterceptor.java deleted file mode 100644 index 86f066b582c..00000000000 --- a/validation/src/main/java/io/micronaut/validation/ValidatingInterceptor.java +++ /dev/null @@ -1,186 +0,0 @@ -/* - * Copyright 2017-2020 original authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.micronaut.validation; - -import io.micronaut.aop.InterceptPhase; -import io.micronaut.aop.InterceptedMethod; -import io.micronaut.aop.MethodInterceptor; -import io.micronaut.aop.MethodInvocationContext; -import io.micronaut.core.annotation.Nullable; -import io.micronaut.core.convert.ConversionService; -import io.micronaut.inject.ExecutableMethod; -import io.micronaut.validation.validator.ExecutableMethodValidator; -import io.micronaut.validation.validator.ReactiveValidator; -import io.micronaut.validation.validator.Validator; -import jakarta.inject.Singleton; - -import javax.validation.ConstraintViolation; -import javax.validation.ConstraintViolationException; -import javax.validation.ValidatorFactory; -import javax.validation.executable.ExecutableValidator; -import java.lang.reflect.Method; -import java.util.Set; - -/** - * A {@link MethodInterceptor} that validates method invocations. - * - * @author Graeme Rocher - * @since 1.0 - */ -@Singleton -public class ValidatingInterceptor implements MethodInterceptor { - - /** - * The position of the interceptor. See {@link io.micronaut.core.order.Ordered} - */ - public static final int POSITION = InterceptPhase.VALIDATE.getPosition(); - - private final ConversionService conversionService; - private final @Nullable ExecutableValidator executableValidator; - private final @Nullable ExecutableMethodValidator micronautValidator; - - /** - * Creates ValidatingInterceptor from the validatorFactory. - * - * @param micronautValidator The micronaut validator use if no factory is available - * @param validatorFactory Factory returning initialized {@code Validator} instances - * @param conversionService The conversion service - */ - public ValidatingInterceptor(@Nullable Validator micronautValidator, - @Nullable ValidatorFactory validatorFactory, - ConversionService conversionService) { - this.conversionService = conversionService; - - if (validatorFactory != null) { - javax.validation.Validator validator = validatorFactory.getValidator(); - if (validator instanceof Validator) { - this.micronautValidator = (ExecutableMethodValidator) validator; - this.executableValidator = null; - } else { - this.micronautValidator = null; - this.executableValidator = validator.forExecutables(); - } - } else if (micronautValidator != null) { - this.micronautValidator = micronautValidator.forExecutables(); - this.executableValidator = null; - } else { - this.micronautValidator = null; - this.executableValidator = null; - } - } - - @Override - public int getOrder() { - return POSITION; - } - - @Nullable - @Override - public Object intercept(MethodInvocationContext context) { - if (executableValidator != null) { - Method targetMethod = context.getTargetMethod(); - if (targetMethod.getParameterTypes().length != 0) { - Set> constraintViolations = executableValidator - .validateParameters( - context.getTarget(), - targetMethod, - context.getParameterValues(), - getValidationGroups(context) - ); - if (!constraintViolations.isEmpty()) { - throw new ConstraintViolationException(constraintViolations); - } - } - return validateReturnExecutableValidator(context, targetMethod); - } else if (micronautValidator != null) { - ExecutableMethod executableMethod = context.getExecutableMethod(); - if (executableMethod.getArguments().length != 0) { - Set> constraintViolations = micronautValidator.validateParameters( - context.getTarget(), - executableMethod, - context.getParameterValues(), - getValidationGroups(context)); - if (!constraintViolations.isEmpty()) { - throw new ConstraintViolationException(constraintViolations); - } - } - if (hasValidationAnnotation(context)) { - if (micronautValidator instanceof ReactiveValidator) { - InterceptedMethod interceptedMethod = InterceptedMethod.of(context, conversionService); - try { - return switch (interceptedMethod.resultType()) { - case PUBLISHER -> interceptedMethod.handleResult( - ((ReactiveValidator) micronautValidator).validatePublisher( - interceptedMethod.interceptResultAsPublisher(), - getValidationGroups(context)) - ); - case COMPLETION_STAGE -> interceptedMethod.handleResult( - ((ReactiveValidator) micronautValidator).validateCompletionStage( - interceptedMethod.interceptResultAsCompletionStage(), - getValidationGroups(context)) - ); - case SYNCHRONOUS -> validateReturnMicronautValidator(context, executableMethod); - default -> interceptedMethod.unsupported(); - }; - } catch (Exception e) { - return interceptedMethod.handleException(e); - } - } else { - return validateReturnMicronautValidator(context, executableMethod); - } - } - return context.proceed(); - } - return context.proceed(); - } - - private Object validateReturnMicronautValidator(MethodInvocationContext context, ExecutableMethod executableMethod) { - Object result = context.proceed(); - Set> constraintViolations = micronautValidator.validateReturnValue( - context.getTarget(), - executableMethod, - result, - getValidationGroups(context)); - if (!constraintViolations.isEmpty()) { - throw new ConstraintViolationException(constraintViolations); - } - return result; - } - - private Object validateReturnExecutableValidator(MethodInvocationContext context, Method targetMethod) { - final Object result = context.proceed(); - if (hasValidationAnnotation(context)) { - Set> constraintViolations = executableValidator.validateReturnValue( - context.getTarget(), - targetMethod, - result, - getValidationGroups(context) - ); - if (!constraintViolations.isEmpty()) { - throw new ConstraintViolationException(constraintViolations); - } - } - return result; - } - - private boolean hasValidationAnnotation(MethodInvocationContext context) { - return context.hasStereotype(Validator.ANN_VALID) || context.hasStereotype(Validator.ANN_CONSTRAINT); - } - - private Class[] getValidationGroups(MethodInvocationContext context) { - return context.classValues(Validated.class, "groups"); - } -} diff --git a/validation/src/main/java/io/micronaut/validation/exceptions/ConstraintExceptionHandler.java b/validation/src/main/java/io/micronaut/validation/exceptions/ConstraintExceptionHandler.java deleted file mode 100644 index 883a68958ba..00000000000 --- a/validation/src/main/java/io/micronaut/validation/exceptions/ConstraintExceptionHandler.java +++ /dev/null @@ -1,113 +0,0 @@ -/* - * Copyright 2017-2020 original authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.micronaut.validation.exceptions; - -import io.micronaut.context.annotation.Requires; -import io.micronaut.http.HttpRequest; -import io.micronaut.http.HttpResponse; -import io.micronaut.http.HttpStatus; -import io.micronaut.http.MutableHttpResponse; -import io.micronaut.http.annotation.Produces; -import io.micronaut.http.server.exceptions.ExceptionHandler; -import io.micronaut.http.server.exceptions.response.ErrorContext; -import io.micronaut.http.server.exceptions.response.ErrorResponseProcessor; -import jakarta.inject.Inject; -import jakarta.inject.Singleton; - -import javax.validation.ConstraintViolation; -import javax.validation.ConstraintViolationException; -import javax.validation.ElementKind; -import javax.validation.Path; -import java.util.Iterator; -import java.util.Set; -import java.util.stream.Collectors; - -/** - * Default {@link ExceptionHandler} for {@link ConstraintViolationException}. - * - * @author Graeme Rocher - * @since 1.0 - */ -@Produces -@Singleton -@Requires(classes = {ConstraintViolationException.class, ExceptionHandler.class}) -public class ConstraintExceptionHandler implements ExceptionHandler> { - - private final ErrorResponseProcessor responseProcessor; - - /** - * Constructor. - * @param responseProcessor Error Response Processor - */ - @Inject - public ConstraintExceptionHandler(ErrorResponseProcessor responseProcessor) { - this.responseProcessor = responseProcessor; - } - - @Override - public HttpResponse handle(HttpRequest request, ConstraintViolationException exception) { - Set> constraintViolations = exception.getConstraintViolations(); - MutableHttpResponse response = HttpResponse.badRequest(); - final ErrorContext.Builder contextBuilder = ErrorContext.builder(request).cause(exception); - if (constraintViolations == null || constraintViolations.isEmpty()) { - return responseProcessor.processResponse(contextBuilder.errorMessage( - exception.getMessage() == null ? HttpStatus.BAD_REQUEST.getReason() : exception.getMessage() - ).build(), response); - } else { - return responseProcessor.processResponse(contextBuilder.errorMessages( - exception.getConstraintViolations() - .stream() - .map(this::buildMessage) - .sorted() - .collect(Collectors.toList()) - ).build(), response); - } - } - - /** - * Builds a message based on the provided violation. - * - * @param violation The constraint violation - * @return The violation message - */ - protected String buildMessage(ConstraintViolation violation) { - Path propertyPath = violation.getPropertyPath(); - StringBuilder message = new StringBuilder(); - Iterator i = propertyPath.iterator(); - - while (i.hasNext()) { - Path.Node node = i.next(); - - if (node.getKind() == ElementKind.METHOD || node.getKind() == ElementKind.CONSTRUCTOR) { - continue; - } - - message.append(node.getName()); - - if (node.getIndex() != null) { - message.append(String.format("[%d]", node.getIndex())); - } - - if (i.hasNext()) { - message.append('.'); - } - } - - message.append(": ").append(violation.getMessage()); - - return message.toString(); - } -} diff --git a/validation/src/main/java/io/micronaut/validation/exceptions/package-info.java b/validation/src/main/java/io/micronaut/validation/exceptions/package-info.java deleted file mode 100644 index e4730be8571..00000000000 --- a/validation/src/main/java/io/micronaut/validation/exceptions/package-info.java +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Copyright 2017-2020 original authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -/** - * Validation exceptions. - * - * @author graemerocher - * @since 1.0 - */ -package io.micronaut.validation.exceptions; - diff --git a/validation/src/main/java/io/micronaut/validation/package-info.java b/validation/src/main/java/io/micronaut/validation/package-info.java deleted file mode 100644 index 752faf92f0b..00000000000 --- a/validation/src/main/java/io/micronaut/validation/package-info.java +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Copyright 2017-2020 original authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -/** - * Validation advice and interceptors. - * - * @author graemerocher - * @since 1.0 - */ -package io.micronaut.validation; - diff --git a/validation/src/main/java/io/micronaut/validation/validator/DefaultAnnotatedElementValidator.java b/validation/src/main/java/io/micronaut/validation/validator/DefaultAnnotatedElementValidator.java deleted file mode 100644 index af78e7f53b1..00000000000 --- a/validation/src/main/java/io/micronaut/validation/validator/DefaultAnnotatedElementValidator.java +++ /dev/null @@ -1,101 +0,0 @@ -/* - * Copyright 2017-2020 original authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.micronaut.validation.validator; - -import io.micronaut.core.annotation.Internal; -import io.micronaut.core.annotation.NonNull; -import io.micronaut.core.convert.ConversionService; -import io.micronaut.core.io.service.SoftServiceLoader; -import io.micronaut.core.reflect.GenericTypeUtils; -import io.micronaut.core.util.ArrayUtils; -import io.micronaut.inject.annotation.AnnotatedElementValidator; -import io.micronaut.inject.qualifiers.TypeArgumentQualifier; -import io.micronaut.validation.validator.constraints.ConstraintValidator; -import io.micronaut.validation.validator.constraints.DefaultConstraintValidators; - -import java.lang.annotation.Annotation; -import java.util.Arrays; -import java.util.HashMap; -import java.util.Map; -import java.util.Optional; - -/** - * Default implementation of {@link AnnotatedElementValidator}. Used for discovery via - * service loader and not for direct public consumption. Considered internal. - * - * @author graemerocher - * @since 1.2 - */ -@Internal -public class DefaultAnnotatedElementValidator extends DefaultValidator implements AnnotatedElementValidator { - - /** - * Default constructor. - */ - public DefaultAnnotatedElementValidator() { - super(new DefaultValidatorConfiguration(ConversionService.SHARED) - .setConstraintValidatorRegistry(new LocalConstraintValidators()), ConversionService.SHARED); - } - - /** - * Local constraint validator lookup using service loader. - */ - private static final class LocalConstraintValidators extends DefaultConstraintValidators { - - private Map validatorMap; - - @Override - protected Optional findLocalConstraintValidator(@NonNull Class constraintType, @NonNull Class targetType) { - return findConstraintValidatorFromServiceLoader(constraintType, targetType); - } - - private Optional findConstraintValidatorFromServiceLoader(Class constraintType, Class targetType) { - if (validatorMap == null) { - validatorMap = initializeValidatorMap(); - } - return validatorMap.entrySet().stream() - .filter(entry -> { - final ValidatorKey key = entry.getKey(); - final Class[] left = {constraintType, targetType}; - return TypeArgumentQualifier.areTypesCompatible( - left, - Arrays.asList(key.getConstraintType(), key.getTargetType()) - ); - }) - .findFirst().map(Map.Entry::getValue); - } - - private Map initializeValidatorMap() { - validatorMap = new HashMap<>(); - for (ConstraintValidator validator : SoftServiceLoader.load(ConstraintValidator.class).collectAll()) { - try { - final Class[] typeArgs = GenericTypeUtils.resolveInterfaceTypeArguments(validator.getClass(), ConstraintValidator.class); - if (ArrayUtils.isNotEmpty(typeArgs) && typeArgs.length == 2) { - validatorMap.put( - new ValidatorKey(typeArgs[0], typeArgs[1]), - validator - ); - } - } catch (Exception e) { - // as this will occur in the compiler, we print a warning and not log it - System.err.println("WARNING: Could not load validator [" + validator.getClass().getName() + "]: " + e.getMessage()); - } - } - - return validatorMap; - } - } -} diff --git a/validation/src/main/java/io/micronaut/validation/validator/DefaultClockProvider.java b/validation/src/main/java/io/micronaut/validation/validator/DefaultClockProvider.java deleted file mode 100644 index df2f3ab2e08..00000000000 --- a/validation/src/main/java/io/micronaut/validation/validator/DefaultClockProvider.java +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright 2017-2020 original authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.micronaut.validation.validator; - -import jakarta.inject.Singleton; - -import javax.validation.ClockProvider; -import java.time.Clock; - -/** - * The default clock provider. - * - * @author graemerocher - * @since 1.2 - */ -@Singleton -public class DefaultClockProvider implements ClockProvider { - @Override - public Clock getClock() { - return Clock.systemDefaultZone(); - } -} diff --git a/validation/src/main/java/io/micronaut/validation/validator/DefaultConstraintDescriptor.java b/validation/src/main/java/io/micronaut/validation/validator/DefaultConstraintDescriptor.java deleted file mode 100644 index 5454efb8706..00000000000 --- a/validation/src/main/java/io/micronaut/validation/validator/DefaultConstraintDescriptor.java +++ /dev/null @@ -1,125 +0,0 @@ -/* - * Copyright 2017-2020 original authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.micronaut.validation.validator; - -import io.micronaut.core.annotation.AnnotationMetadata; -import io.micronaut.core.annotation.AnnotationValue; -import io.micronaut.core.annotation.Internal; -import io.micronaut.core.type.Argument; - -import javax.validation.ConstraintTarget; -import javax.validation.ConstraintValidator; -import javax.validation.Payload; -import javax.validation.metadata.ConstraintDescriptor; -import javax.validation.metadata.ValidateUnwrappedValue; -import java.lang.annotation.Annotation; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.stream.Collectors; - -/** - * Default constraint descriptor implementation. - * - * @param The constraint type - * @author graemerocher - * @since 1.2 - */ -@Internal -class DefaultConstraintDescriptor implements ConstraintDescriptor { - - private final AnnotationValue annotationValue; - private final AnnotationMetadata annotationMetadata; - private final Class type; - - /** - * Default constructor. - * @param annotationMetadata annotation metadata - * @param type constraint type - * @param annotationValue annotation value - */ - DefaultConstraintDescriptor( - AnnotationMetadata annotationMetadata, - Class type, - AnnotationValue annotationValue) { - this.annotationValue = annotationValue; - this.annotationMetadata = annotationMetadata; - this.type = type; - } - - @Override - public T getAnnotation() { - return annotationMetadata.synthesize(type); - } - - @Override - public String getMessageTemplate() { - return annotationValue.get("groups", String.class).orElse(null); - } - - @Override - public Set> getGroups() { - Set groups = annotationValue.get("groups", Argument.setOf(Class.class)).orElse(Collections.emptySet()); - //noinspection unchecked - return groups; - } - - @Override - public Set> getPayload() { - Set payload = annotationValue.get("payload", Argument.setOf(Class.class)).orElse(Collections.emptySet()); - //noinspection unchecked - return payload; - } - - @Override - public ConstraintTarget getValidationAppliesTo() { - return ConstraintTarget.IMPLICIT; - } - - @Override - public List>> getConstraintValidatorClasses() { - return Collections.emptyList(); - } - - @Override - public Map getAttributes() { - return annotationValue.getValues().entrySet().stream().collect(Collectors.toMap( - entry -> entry.getKey().toString(), - Map.Entry::getValue - )); - } - - @Override - public Set> getComposingConstraints() { - return Collections.emptySet(); - } - - @Override - public boolean isReportAsSingleViolation() { - return false; - } - - @Override - public ValidateUnwrappedValue getValueUnwrapping() { - return ValidateUnwrappedValue.DEFAULT; - } - - @Override - public Object unwrap(Class type) { - throw new UnsupportedOperationException("Unwrapping unsupported"); - } -} diff --git a/validation/src/main/java/io/micronaut/validation/validator/DefaultValidator.java b/validation/src/main/java/io/micronaut/validation/validator/DefaultValidator.java deleted file mode 100644 index 4569f30fe31..00000000000 --- a/validation/src/main/java/io/micronaut/validation/validator/DefaultValidator.java +++ /dev/null @@ -1,2452 +0,0 @@ -/* - * Copyright 2017-2020 original authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.micronaut.validation.validator; - -import io.micronaut.aop.Intercepted; -import io.micronaut.context.BeanResolutionContext; -import io.micronaut.context.ExecutionHandleLocator; -import io.micronaut.context.MessageSource; -import io.micronaut.context.annotation.ConfigurationReader; -import io.micronaut.context.annotation.Primary; -import io.micronaut.context.annotation.Property; -import io.micronaut.context.annotation.Requires; -import io.micronaut.context.exceptions.BeanInstantiationException; -import io.micronaut.core.annotation.AnnotatedElement; -import io.micronaut.core.annotation.AnnotationMetadata; -import io.micronaut.core.annotation.AnnotationValue; -import io.micronaut.core.annotation.Introspected; -import io.micronaut.core.annotation.NonNull; -import io.micronaut.core.annotation.Nullable; -import io.micronaut.core.async.publisher.Publishers; -import io.micronaut.core.beans.BeanIntrospection; -import io.micronaut.core.beans.BeanIntrospector; -import io.micronaut.core.beans.BeanProperty; -import io.micronaut.core.convert.ConversionService; -import io.micronaut.core.reflect.ClassUtils; -import io.micronaut.core.type.Argument; -import io.micronaut.core.type.ArgumentValue; -import io.micronaut.core.type.MutableArgumentValue; -import io.micronaut.core.type.ReturnType; -import io.micronaut.core.util.ArgumentUtils; -import io.micronaut.core.util.ArrayUtils; -import io.micronaut.core.util.CollectionUtils; -import io.micronaut.core.util.StringUtils; -import io.micronaut.inject.BeanDefinition; -import io.micronaut.inject.ExecutableMethod; -import io.micronaut.inject.InjectionPoint; -import io.micronaut.inject.MethodReference; -import io.micronaut.inject.ProxyBeanDefinition; -import io.micronaut.inject.annotation.AnnotatedElementValidator; -import io.micronaut.inject.validation.BeanDefinitionValidator; -import io.micronaut.validation.validator.constraints.ConstraintValidator; -import io.micronaut.validation.validator.constraints.ConstraintValidatorContext; -import io.micronaut.validation.validator.constraints.ConstraintValidatorRegistry; -import io.micronaut.validation.validator.extractors.SimpleValueReceiver; -import io.micronaut.validation.validator.extractors.ValueExtractorRegistry; -import jakarta.inject.Singleton; -import org.reactivestreams.Publisher; -import reactor.core.publisher.Flux; - -import javax.validation.ClockProvider; -import javax.validation.Constraint; -import javax.validation.ConstraintViolation; -import javax.validation.ConstraintViolationException; -import javax.validation.ElementKind; -import javax.validation.Path; -import javax.validation.TraversableResolver; -import javax.validation.Valid; -import javax.validation.ValidationException; -import javax.validation.groups.Default; -import javax.validation.metadata.BeanDescriptor; -import javax.validation.metadata.ConstraintDescriptor; -import javax.validation.metadata.ConstructorDescriptor; -import javax.validation.metadata.ElementDescriptor; -import javax.validation.metadata.MethodDescriptor; -import javax.validation.metadata.MethodType; -import javax.validation.metadata.PropertyDescriptor; -import javax.validation.metadata.Scope; -import javax.validation.valueextraction.ValueExtractor; -import java.lang.annotation.Annotation; -import java.lang.annotation.ElementType; -import java.lang.reflect.Constructor; -import java.lang.reflect.Method; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.Deque; -import java.util.HashSet; -import java.util.Iterator; -import java.util.LinkedHashMap; -import java.util.LinkedList; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.Set; -import java.util.concurrent.CompletionStage; -import java.util.function.Function; -import java.util.stream.Collectors; - -/** - * Default implementation of the {@link Validator} interface. - * - * @author graemerocher - * @since 1.2 - */ -@Singleton -@Primary -@Requires(property = ValidatorConfiguration.ENABLED, value = StringUtils.TRUE, defaultValue = StringUtils.TRUE) -public class DefaultValidator implements Validator, ExecutableMethodValidator, ReactiveValidator, AnnotatedElementValidator, BeanDefinitionValidator { - - private static final List> DEFAULT_GROUPS = Collections.singletonList(Default.class); - private final ConstraintValidatorRegistry constraintValidatorRegistry; - private final ClockProvider clockProvider; - private final ValueExtractorRegistry valueExtractorRegistry; - private final TraversableResolver traversableResolver; - private final ExecutionHandleLocator executionHandleLocator; - private final MessageSource messageSource; - private final ConversionService conversionService; - - /** - * Default constructor. - * - * @param configuration The validator configuration - * @param conversionService The conversion service - */ - protected DefaultValidator(@NonNull ValidatorConfiguration configuration, ConversionService conversionService) { - this.conversionService = conversionService; - ArgumentUtils.requireNonNull("configuration", configuration); - this.constraintValidatorRegistry = configuration.getConstraintValidatorRegistry(); - this.clockProvider = configuration.getClockProvider(); - this.valueExtractorRegistry = configuration.getValueExtractorRegistry(); - this.traversableResolver = configuration.getTraversableResolver(); - this.executionHandleLocator = configuration.getExecutionHandleLocator(); - this.messageSource = configuration.getMessageSource(); - } - - @SuppressWarnings("unchecked") - @NonNull - @Override - public Set> validate(@NonNull T object, @Nullable Class... groups) { - ArgumentUtils.requireNonNull("object", object); - final BeanIntrospection introspection = (BeanIntrospection) getBeanIntrospection(object); - if (introspection == null) { - return Collections.emptySet(); - } - return validate(introspection, object, groups); - } - - /** - * Validate the given introspection and object. - * @param introspection The introspection - * @param object The object - * @param groups The groups - * @param The object type - * @return The constraint violations - */ - @Override - @SuppressWarnings("ConstantConditions") - @NonNull - public Set> validate(@NonNull BeanIntrospection introspection, @NonNull T object, @Nullable Class... groups) { - if (introspection == null) { - throw new ValidationException("Passed object [" + object + "] cannot be introspected. Please annotate with @Introspected"); - } - @SuppressWarnings("unchecked") - final Collection> constrainedProperties = - ((BeanIntrospection) introspection).getIndexedProperties(Constraint.class); - @SuppressWarnings("unchecked") - final Collection> cascadeProperties = - ((BeanIntrospection) introspection).getIndexedProperties(Valid.class); - - final List> pojoConstraints = introspection.getAnnotationTypesByStereotype(Constraint.class); - - if (CollectionUtils.isNotEmpty(constrainedProperties) - || CollectionUtils.isNotEmpty(cascadeProperties) - || CollectionUtils.isNotEmpty(pojoConstraints)) { - - DefaultConstraintValidatorContext context = new DefaultConstraintValidatorContext(object, groups); - Set> overallViolations = new HashSet<>(5); - return doValidate( - introspection, - object, - object, - constrainedProperties, - cascadeProperties, - context, - overallViolations, - pojoConstraints - ); - } - return Collections.emptySet(); - } - - @NonNull - @Override - public Set> validateProperty( - @NonNull T object, - @NonNull String propertyName, - @Nullable Class... groups) { - ArgumentUtils.requireNonNull("object", object); - ArgumentUtils.requireNonNull("propertyName", propertyName); - final BeanIntrospection introspection = getBeanIntrospection(object); - if (introspection == null) { - throw new ValidationException("Passed object [" + object + "] cannot be introspected. Please annotate with @Introspected"); - } - - final Optional> property = introspection.getProperty(propertyName); - - if (property.isPresent()) { - final BeanProperty constrainedProperty = property.get(); - DefaultConstraintValidatorContext context = new DefaultConstraintValidatorContext(object, groups); - Set overallViolations = new HashSet<>(5); - final Object propertyValue = constrainedProperty.get(object); - - @SuppressWarnings("unchecked") - final Class rootBeanClass = (Class) object.getClass(); - //noinspection unchecked - validateConstrainedPropertyInternal( - rootBeanClass, - object, - object, - constrainedProperty, - constrainedProperty.getType(), - propertyValue, - context, - overallViolations, - null); - - //noinspection unchecked - return Collections.unmodifiableSet(overallViolations); - } - - return Collections.emptySet(); - } - - @NonNull - @Override - public Set> validateValue( - @NonNull Class beanType, - @NonNull String propertyName, - @Nullable Object value, - @Nullable Class... groups) { - ArgumentUtils.requireNonNull("beanType", beanType); - ArgumentUtils.requireNonNull("propertyName", propertyName); - - final BeanIntrospection introspection = getBeanIntrospection(beanType); - if (introspection == null) { - throw new ValidationException("Passed bean type [" + beanType + "] cannot be introspected. Please annotate with @Introspected"); - } - - final BeanProperty beanProperty = introspection.getProperty(propertyName) - .orElseThrow(() -> new ValidationException("No property [" + propertyName + "] found on type: " + beanType)); - - - final HashSet overallViolations = new HashSet<>(5); - final DefaultConstraintValidatorContext context = new DefaultConstraintValidatorContext(groups); - try { - context.addPropertyNode(propertyName, null); - //noinspection unchecked - validatePropertyInternal( - beanType, - null, - null, - context, - overallViolations, - beanProperty.getType(), - beanProperty, - value); - } finally { - context.removeLast(); - } - - //noinspection unchecked - return Collections.unmodifiableSet(overallViolations); - } - - @NonNull - @Override - public Set validatedAnnotatedElement(@NonNull AnnotatedElement element, @Nullable Object value) { - ArgumentUtils.requireNonNull("element", element); - if (!element.getAnnotationMetadata().hasStereotype(Constraint.class)) { - return Collections.emptySet(); - } - - final Set> overallViolations = new HashSet<>(5); - final DefaultConstraintValidatorContext context = new DefaultConstraintValidatorContext(); - try { - context.addPropertyNode(element.getName(), null); - validatePropertyInternal( - null, - element, - element, - context, - overallViolations, - value != null ? value.getClass() : Object.class, - element, - value); - } finally { - context.removeLast(); - } - - return Collections.unmodifiableSet(overallViolations.stream() - .map(ConstraintViolation::getMessage).collect(Collectors.toSet())); - } - - @NonNull - @Override - public T createValid(@NonNull Class beanType, Object... arguments) throws ConstraintViolationException { - ArgumentUtils.requireNonNull("type", beanType); - - @SuppressWarnings("unchecked") - final BeanIntrospection introspection = (BeanIntrospection) getBeanIntrospection(beanType); - if (introspection == null) { - throw new ValidationException("Passed bean type [" + beanType + "] cannot be introspected. Please annotate with @Introspected"); - } - - final Set> constraintViolations = validateConstructorParameters(introspection, arguments); - - if (constraintViolations.isEmpty()) { - final T instance = introspection.instantiate(arguments); - final Set> errors = validate(introspection, instance); - if (errors.isEmpty()) { - return instance; - } else { - throw new ConstraintViolationException(errors); - } - } - - throw new ConstraintViolationException(constraintViolations); - } - - @Override - public BeanDescriptor getConstraintsForClass(Class clazz) { - return BeanIntrospector.SHARED.findIntrospection(clazz) - .map((Function, BeanDescriptor>) IntrospectedBeanDescriptor::new) - .orElseGet(() -> new EmptyDescriptor(clazz)); - } - - @Override - public T unwrap(Class type) { - throw new UnsupportedOperationException("Validator unwrapping not supported by this implementation"); - } - - @Override - @NonNull - public ExecutableMethodValidator forExecutables() { - return this; - } - - @NonNull - @Override - public Set> validateParameters( - @NonNull T object, - @NonNull ExecutableMethod method, - @NonNull Object[] parameterValues, - @Nullable Class... groups) { - ArgumentUtils.requireNonNull("parameterValues", parameterValues); - ArgumentUtils.requireNonNull("object", object); - ArgumentUtils.requireNonNull("method", method); - final Argument[] arguments = method.getArguments(); - final int argLen = arguments.length; - if (argLen != parameterValues.length) { - throw new IllegalArgumentException("The method parameter array must have exactly " + argLen + " elements."); - } - - DefaultConstraintValidatorContext context = new DefaultConstraintValidatorContext(object, groups); - Set overallViolations = new HashSet<>(5); - - final Path.Node node = context.addMethodNode(method); - try { - @SuppressWarnings("unchecked") - final Class rootClass = (Class) object.getClass(); - validateParametersInternal( - rootClass, - object, - parameterValues, - arguments, - argLen, - context, - overallViolations, - node - ); - } finally { - context.removeLast(); - } - //noinspection unchecked - return Collections.unmodifiableSet(overallViolations); - } - - @NonNull - @Override - public Set> validateParameters( - @NonNull T object, @NonNull - ExecutableMethod method, - @NonNull Collection> argumentValues, - @Nullable Class... groups) { - ArgumentUtils.requireNonNull("object", object); - ArgumentUtils.requireNonNull("method", method); - ArgumentUtils.requireNonNull("parameterValues", argumentValues); - final Argument[] arguments = method.getArguments(); - final int argLen = arguments.length; - if (argLen != argumentValues.size()) { - throw new IllegalArgumentException("The method parameter array must have exactly " + argLen + " elements."); - } - - DefaultConstraintValidatorContext context = new DefaultConstraintValidatorContext(object, groups); - Set overallViolations = new HashSet<>(5); - - final Path.Node node = context.addMethodNode(method); - try { - @SuppressWarnings("unchecked") - final Class rootClass = (Class) object.getClass(); - validateParametersInternal( - rootClass, - object, - argumentValues.stream().map(ArgumentValue::getValue).toArray(), - arguments, - argLen, - context, - overallViolations, - node - ); - } finally { - context.removeLast(); - } - //noinspection unchecked - return Collections.unmodifiableSet(overallViolations); - } - - @NonNull - @Override - public Set> validateParameters( - @NonNull T object, - @NonNull Method method, - @NonNull Object[] parameterValues, - @Nullable Class... groups) { - ArgumentUtils.requireNonNull("method", method); - return executionHandleLocator.findExecutableMethod( - method.getDeclaringClass(), - method.getName(), - method.getParameterTypes() - ).map(executableMethod -> - validateParameters(object, executableMethod, parameterValues, groups) - ).orElse(Collections.emptySet()); - } - - @NonNull - @Override - public Set> validateReturnValue( - @NonNull T object, - @NonNull Method method, - @Nullable Object returnValue, - @Nullable Class... groups) { - ArgumentUtils.requireNonNull("method", method); - ArgumentUtils.requireNonNull("object", object); - return executionHandleLocator.findExecutableMethod( - method.getDeclaringClass(), - method.getName(), - method.getParameterTypes() - ).map(executableMethod -> - validateReturnValue(object, executableMethod, returnValue, groups) - ).orElse(Collections.emptySet()); - } - - @Override - public @NonNull Set> validateReturnValue( - @NonNull T object, - @NonNull ExecutableMethod executableMethod, - @Nullable Object returnValue, - @Nullable Class... groups) { - final ReturnType returnType = executableMethod.getReturnType(); - final Argument returnTypeArgument = returnType.asArgument(); - final HashSet overallViolations = new HashSet(3); - @SuppressWarnings("unchecked") - final Class rootBeanClass = (Class) object.getClass(); - final DefaultConstraintValidatorContext context = new DefaultConstraintValidatorContext(object, groups); - //noinspection unchecked - validateConstrainedPropertyInternal( - rootBeanClass, - object, - object, - returnTypeArgument, - returnType.getType(), - returnValue, - context, - overallViolations, - null - ); - - final AnnotationMetadata annotationMetadata = returnTypeArgument.getAnnotationMetadata(); - final boolean hasValid = annotationMetadata.isAnnotationPresent(Valid.class); - - if (hasValid) { - validateCascadePropertyInternal(context, - rootBeanClass, - object, - object, - returnTypeArgument, - returnValue, - overallViolations); - } - - //noinspection unchecked - return overallViolations; - } - - private void validateCascadePropertyInternal(DefaultConstraintValidatorContext context, - @NonNull Class rootBeanClass, - @Nullable T rootBean, - Object object, - @NonNull Argument cascadeProperty, - @Nullable Object propertyValue, - Set overallViolations) { - if (propertyValue != null) { - @SuppressWarnings("unchecked") final Optional> opt = valueExtractorRegistry - .findValueExtractor((Class) cascadeProperty.getType()); - - opt.ifPresent(valueExtractor -> valueExtractor.extractValues(propertyValue, new ValueExtractor.ValueReceiver() { - @Override - public void value(String nodeName, Object object1) { - - } - - @Override - public void iterableValue(String nodeName, Object iterableValue) { - if (iterableValue != null && context.validatedObjects.contains(iterableValue)) { - return; - } - cascadeToIterableValue( - context, - rootBeanClass, - rootBean, - object, - null, - cascadeProperty, - iterableValue, - overallViolations, - null, - null, - true); - } - - @Override - public void indexedValue(String nodeName, int i, Object iterableValue) { - if (iterableValue != null && context.validatedObjects.contains(iterableValue)) { - return; - } - cascadeToIterableValue( - context, - rootBeanClass, - rootBean, - object, - null, - cascadeProperty, - iterableValue, - overallViolations, - i, - null, - true); - } - - @Override - public void keyedValue(String nodeName, Object key, Object keyedValue) { - if (keyedValue != null && context.validatedObjects.contains(keyedValue)) { - return; - } - cascadeToIterableValue( - context, - rootBeanClass, - rootBean, - object, - null, - cascadeProperty, - keyedValue, - overallViolations, - null, - key, - false - ); - } - })); - - if (!opt.isPresent() && !context.validatedObjects.contains(propertyValue)) { - try { - // maybe a bean - final Path.Node node = context.addReturnValueNode(cascadeProperty.getName()); - final boolean canCascade = canCascade(rootBeanClass, context, propertyValue, node); - if (canCascade) { - cascadeToOne( - rootBeanClass, - rootBean, - object, - context, - overallViolations, - cascadeProperty, - cascadeProperty.getType(), - propertyValue, - null); - } - } finally { - context.removeLast(); - } - } - } - } - - @NonNull - @Override - public Set> validateConstructorParameters( - @NonNull Constructor constructor, - @NonNull Object[] parameterValues, - @Nullable Class... groups) { - ArgumentUtils.requireNonNull("constructor", constructor); - final Class declaringClass = constructor.getDeclaringClass(); - final BeanIntrospection introspection = BeanIntrospection.getIntrospection(declaringClass); - return validateConstructorParameters(introspection, parameterValues); - } - - @Override - @NonNull - public Set> validateConstructorParameters( - @NonNull BeanIntrospection introspection, - @NonNull Object[] parameterValues, - @Nullable Class... groups) { - ArgumentUtils.requireNonNull("introspection", introspection); - final Class beanType = introspection.getBeanType(); - final Argument[] constructorArguments = introspection.getConstructorArguments(); - return validateConstructorParameters(beanType, constructorArguments, parameterValues, groups); - } - - @Override - public Set> validateConstructorParameters(Class beanType, Argument[] constructorArguments, @NonNull Object[] parameterValues, @Nullable Class[] groups) { - //noinspection ConstantConditions - parameterValues = parameterValues != null ? parameterValues : ArrayUtils.EMPTY_OBJECT_ARRAY; - final int argLength = constructorArguments.length; - if (parameterValues.length != argLength) { - throw new IllegalArgumentException("Expected exactly [" + argLength + "] constructor arguments"); - } - DefaultConstraintValidatorContext context = new DefaultConstraintValidatorContext(groups); - Set overallViolations = new HashSet<>(5); - - final Path.Node node = context.addConstructorNode(beanType.getSimpleName(), constructorArguments); - try { - validateParametersInternal( - beanType, - null, - parameterValues, - constructorArguments, - argLength, - context, - overallViolations, - node); - } finally { - context.removeLast(); - } - //noinspection unchecked - return Collections.unmodifiableSet(overallViolations); - } - - @NonNull - @Override - public Set> validateConstructorReturnValue(@NonNull Constructor constructor, @NonNull T createdObject, @Nullable Class... groups) { - return validate(createdObject, groups); - } - - - /** - * looks up a bean introspection for the given object by instance's class or defined class. - * - * @param object The object, never null - * @param definedClass The defined class of the object, never null - * @return The introspection or null - */ - @SuppressWarnings({"WeakerAccess", "unchecked"}) - protected @Nullable BeanIntrospection getBeanIntrospection(@NonNull Object object, @NonNull Class definedClass) { - //noinspection ConstantConditions - if (object == null) { - return null; - } - return BeanIntrospector.SHARED.findIntrospection((Class) object.getClass()) - .orElseGet(() -> BeanIntrospector.SHARED.findIntrospection((Class) definedClass).orElse(null)); - } - - /** - * looks up a bean introspection for the given object. - * - * @param object The object, never null - * @return The introspection or null - */ - @SuppressWarnings({"WeakerAccess", "unchecked"}) - protected @Nullable BeanIntrospection getBeanIntrospection(@NonNull Object object) { - //noinspection ConstantConditions - if (object == null) { - return null; - } - if (object instanceof Class) { - return BeanIntrospector.SHARED.findIntrospection((Class) object).orElse(null); - } - return BeanIntrospector.SHARED.findIntrospection((Class) object.getClass()).orElse(null); - } - - private void validateParametersInternal( - @NonNull Class rootClass, - @Nullable T object, - @NonNull Object[] parameters, - Argument[] arguments, - int argLen, - DefaultConstraintValidatorContext context, - Set overallViolations, - Path.Node parentNode) { - for (int i = 0; i < argLen; i++) { - Argument argument = arguments[i]; - final Class parameterType = argument.getType(); - - final AnnotationMetadata annotationMetadata = argument.getAnnotationMetadata(); - - final boolean hasValid = annotationMetadata.hasStereotype(Validator.ANN_VALID); - final boolean hasConstraint = annotationMetadata.hasStereotype(Validator.ANN_CONSTRAINT); - - - if (!hasValid && !hasConstraint) { - continue; - } - - Object parameterValue = parameters[i]; - - ValueExtractor valueExtractor = null; - final boolean hasValue = parameterValue != null; - final boolean isValid = hasValue && hasValid; - final boolean isPublisher = hasValue && Publishers.isConvertibleToPublisher(parameterType); - if (isPublisher) { - instrumentPublisherArgumentWithValidation( - rootClass, - object, - parameters, - context, - i, - argument, - parameterType, - annotationMetadata, - parameterValue, - isValid - ); - } else { - final boolean isCompletionStage = hasValue && CompletionStage.class.isAssignableFrom(parameterType); - if (isCompletionStage) { - instrumentCompletionStageArgumentWithValidation( - rootClass, - object, - parameters, - context, - i, - argument, - annotationMetadata, - parameterValue, - isValid - ); - } else { - if (hasValue) { - //noinspection unchecked - valueExtractor = (ValueExtractor) valueExtractorRegistry.findUnwrapValueExtractor(parameterType).orElse(null); - } - - int finalIndex = i; - if (valueExtractor != null) { - valueExtractor.extractValues(parameterValue, (SimpleValueReceiver) (nodeName, unwrappedValue) -> validateParameterInternal( - rootClass, - object, - parameters, - context, - overallViolations, - argument.getName(), - unwrappedValue == null ? Object.class : unwrappedValue.getClass(), - finalIndex, - annotationMetadata, - unwrappedValue - )); - } else { - validateParameterInternal( - rootClass, - object, - parameters, - context, - overallViolations, - argument.getName(), - parameterType, - finalIndex, - annotationMetadata, - parameterValue - ); - } - - - if (isValid) { - if (context.validatedObjects.contains(parameterValue)) { - // already validated - continue; - } - // cascade to bean - //noinspection unchecked - valueExtractor = (ValueExtractor) valueExtractorRegistry.findValueExtractor(parameterType).orElse(null); - if (valueExtractor != null) { - valueExtractor.extractValues(parameterValue, new ValueExtractor.ValueReceiver() { - @Override - public void value(String nodeName, Object object1) { - } - - @Override - public void iterableValue(String nodeName, Object iterableValue) { - if (iterableValue != null && context.validatedObjects.contains(iterableValue)) { - return; - } - cascadeToIterableValue( - context, - rootClass, - object, - parameterValue, - parentNode, - argument, - iterableValue, - overallViolations, - null, - null, - true); - } - - @Override - public void indexedValue(String nodeName, int i, Object iterableValue) { - if (iterableValue != null && context.validatedObjects.contains(iterableValue)) { - return; - } - cascadeToIterableValue( - context, - rootClass, - object, - parameterValue, - parentNode, - argument, - iterableValue, - overallViolations, - i, - null, - true); - } - - @Override - public void keyedValue(String nodeName, Object key, Object keyedValue) { - if (keyedValue != null && context.validatedObjects.contains(keyedValue)) { - return; - } - cascadeToIterableValue( - context, - rootClass, - object, - parameterValue, - parentNode, - argument, - keyedValue, - overallViolations, - null, - key, - false); - } - }); - } else { - final BeanIntrospection beanIntrospection = getBeanIntrospection(parameterValue, parameterType); - if (beanIntrospection != null) { - try { - context.addParameterNode(argument.getName(), i); - cascadeToOneIntrospection( - context, - object, - parameterValue, - beanIntrospection, - overallViolations - ); - } finally { - context.removeLast(); - } - } else { - context.addParameterNode(argument.getName(), i); - overallViolations.add(createIntrospectionConstraintViolation(rootClass, object, context, - parameterType, parameterValue, parameters)); - context.removeLast(); - } - } - } - } - } - - } - } - - private void instrumentPublisherArgumentWithValidation( - @NonNull Class rootClass, - @Nullable T object, - @NonNull Object[] argumentValues, - DefaultConstraintValidatorContext context, - int argumentIndex, - Argument argument, - Class parameterType, - AnnotationMetadata annotationMetadata, - Object parameterValue, - boolean isValid) { - final Publisher publisher = Publishers.convertPublisher(conversionService, parameterValue, Publisher.class); - PathImpl copied = new PathImpl(context.currentPath); - final Flux finalFlowable = Flux.from(publisher).flatMap(o -> { - DefaultConstraintValidatorContext newContext = - new DefaultConstraintValidatorContext( - object, - copied - ); - Set newViolations = new HashSet(); - final BeanIntrospection beanIntrospection = !isValid || o == null || ClassUtils.isJavaBasicType(o.getClass()) ? null : getBeanIntrospection(o); - if (beanIntrospection != null) { - try { - context.addParameterNode(argument.getName(), argumentIndex); - cascadeToOneIntrospection( - newContext, - object, - o, - beanIntrospection, - newViolations - ); - } finally { - context.removeLast(); - } - } else { - - final Class t = argument.getFirstTypeVariable().map(Argument::getType).orElse(null); - validateParameterInternal( - rootClass, - object, - argumentValues, - newContext, - newViolations, - argument.getName(), - t != null ? t : Object.class, - argumentIndex, - annotationMetadata, - o - ); - } - - if (!newViolations.isEmpty()) { - return Flux.error( - new ConstraintViolationException(newViolations) - ); - } - - return Flux.just(o); - }); - argumentValues[argumentIndex] = Publishers.convertPublisher(conversionService, finalFlowable, parameterType); - } - - private void instrumentCompletionStageArgumentWithValidation( - @NonNull Class rootClass, - @Nullable T object, - @NonNull Object[] argumentValues, - DefaultConstraintValidatorContext context, - int argumentIndex, - Argument argument, - AnnotationMetadata annotationMetadata, - Object parameterValue, - boolean isValid) { - final CompletionStage publisher = (CompletionStage) parameterValue; - PathImpl copied = new PathImpl(context.currentPath); - final CompletionStage validatedStage = publisher.thenApply(o -> { - DefaultConstraintValidatorContext newContext = - new DefaultConstraintValidatorContext( - object, - copied - ); - Set newViolations = new HashSet(); - final BeanIntrospection beanIntrospection = !isValid || o == null || ClassUtils.isJavaBasicType(o.getClass()) ? null : getBeanIntrospection(o); - if (beanIntrospection != null) { - try { - context.addParameterNode(argument.getName(), argumentIndex); - cascadeToOneIntrospection( - newContext, - object, - o, - beanIntrospection, - newViolations - ); - } finally { - context.removeLast(); - } - } else { - - final Class t = argument.getFirstTypeVariable().map(Argument::getType).orElse(null); - validateParameterInternal( - rootClass, - object, - argumentValues, - newContext, - newViolations, - argument.getName(), - t != null ? t : Object.class, - argumentIndex, - annotationMetadata, - o - ); - } - - if (!newViolations.isEmpty()) { - throw new ConstraintViolationException(newViolations); - } - - return o; - }); - argumentValues[argumentIndex] = validatedStage; - } - - @SuppressWarnings("unchecked") - private void validateParameterInternal( - @NonNull Class rootClass, - @Nullable T object, - @NonNull Object[] argumentValues, - @NonNull DefaultConstraintValidatorContext context, - @NonNull Set overallViolations, - @NonNull String parameterName, - @NonNull Class parameterType, - int parameterIndex, - @NonNull AnnotationMetadata annotationMetadata, - @Nullable Object parameterValue) { - - final String currentMessageTemplate = context.getMessageTemplate().orElse(null); - try { - context.addParameterNode(parameterName, parameterIndex); - final List> constraintTypes = - annotationMetadata.getAnnotationTypesByStereotype(Constraint.class); - - // Constraints applied to the parameter - for (Class constraintType : constraintTypes) { - final ConstraintValidator constraintValidator = constraintValidatorRegistry - .findConstraintValidator(constraintType, parameterType).orElse(null); - if (constraintValidator != null) { - final AnnotationValue annotationValue = - annotationMetadata.getAnnotation(constraintType); - if (annotationValue != null && !constraintValidator.isValid(parameterValue, annotationValue, context)) { - final String messageTemplate = buildMessageTemplate(context, annotationValue, annotationMetadata); - final Map variables = newConstraintVariables(annotationValue, parameterValue, annotationMetadata); - overallViolations.add(new DefaultConstraintViolation( - object, - rootClass, - object, - parameterValue, - messageSource.interpolate(messageTemplate, MessageSource.MessageContext.of(variables)), - messageTemplate, - new PathImpl(context.currentPath), - new DefaultConstraintDescriptor(annotationMetadata, constraintType, annotationValue), - argumentValues)); - } - } - } - } finally { - context.removeLast(); - context.messageTemplate(currentMessageTemplate); - } - } - - @SuppressWarnings("unchecked") - private void validatePojoInternal(@NonNull Class rootClass, - @Nullable T object, - @Nullable Object[] argumentValues, - @NonNull DefaultConstraintValidatorContext context, - @NonNull Set overallViolations, - @NonNull Class parameterType, - @NonNull Object parameterValue, - Class pojoConstraint, - AnnotationValue constraintAnnotation) { - final ConstraintValidator constraintValidator = constraintValidatorRegistry - .findConstraintValidator(pojoConstraint, parameterType).orElse(null); - - if (constraintValidator != null) { - final String currentMessageTemplate = context.getMessageTemplate().orElse(null); - if (!constraintValidator.isValid(parameterValue, constraintAnnotation, context)) { - BeanIntrospection beanIntrospection = getBeanIntrospection(parameterValue); - if (beanIntrospection == null) { - throw new ValidationException("Passed object [" + parameterValue + "] cannot be introspected. Please annotate with @Introspected"); - } - AnnotationMetadata beanAnnotationMetadata = beanIntrospection.getAnnotationMetadata(); - AnnotationValue annotationValue = beanAnnotationMetadata.getAnnotation(pojoConstraint); - - final String propertyValue = ""; - final String messageTemplate = buildMessageTemplate(context, annotationValue, beanAnnotationMetadata); - final Map variables = newConstraintVariables(annotationValue, propertyValue, beanAnnotationMetadata); - overallViolations.add(new DefaultConstraintViolation( - object, - rootClass, - object, - parameterValue, - messageSource.interpolate(messageTemplate, MessageSource.MessageContext.of(variables)), - messageTemplate, - new PathImpl(context.currentPath), - new DefaultConstraintDescriptor(beanAnnotationMetadata, pojoConstraint, annotationValue), - argumentValues)); - } - context.messageTemplate(currentMessageTemplate); - } - } - - private Set> doValidate( - BeanIntrospection introspection, - @NonNull T rootBean, - @NonNull Object object, - Collection> constrainedProperties, - Collection> cascadeProperties, - DefaultConstraintValidatorContext context, - Set overallViolations, - List> pojoConstraints) { - @SuppressWarnings("unchecked") - final Class rootBeanClass = (Class) rootBean.getClass(); - for (BeanProperty constrainedProperty : constrainedProperties) { - if (constrainedProperty.isWriteOnly()) { - continue; - } - final Object propertyValue = constrainedProperty.get(object); - //noinspection unchecked - validateConstrainedPropertyInternal( - rootBeanClass, - rootBean, - object, - constrainedProperty, - constrainedProperty.getType(), - propertyValue, - context, - overallViolations, - null); - } - - for (Class pojoConstraint : pojoConstraints) { - validatePojoInternal( - rootBeanClass, - rootBean, - null, - context, - overallViolations, - object.getClass(), - object, - pojoConstraint, - introspection.getAnnotation(pojoConstraint)); - } - - // now handle cascading validation - for (BeanProperty cascadeProperty : cascadeProperties) { - final Object propertyValue = cascadeProperty.get(object); - if (propertyValue != null) { - @SuppressWarnings("unchecked") - final Optional> opt = valueExtractorRegistry - .findValueExtractor((Class) propertyValue.getClass()); - - opt.ifPresent(valueExtractor -> valueExtractor.extractValues(propertyValue, new ValueExtractor.ValueReceiver() { - @Override - public void value(String nodeName, Object object1) { - - } - - @Override - public void iterableValue(String nodeName, Object iterableValue) { - if (iterableValue != null && context.validatedObjects.contains(iterableValue)) { - return; - } - cascadeToIterableValue( - context, - rootBeanClass, - rootBean, - object, - cascadeProperty, - iterableValue, - overallViolations, - null, - null, - true); - } - - @Override - public void indexedValue(String nodeName, int i, Object iterableValue) { - if (iterableValue != null && context.validatedObjects.contains(iterableValue)) { - return; - } - cascadeToIterableValue( - context, - rootBeanClass, - rootBean, - object, - cascadeProperty, - iterableValue, - overallViolations, - i, - null, - true); - } - - @Override - public void keyedValue(String nodeName, Object key, Object keyedValue) { - if (keyedValue != null && context.validatedObjects.contains(keyedValue)) { - return; - } - cascadeToIterableValue( - context, - rootBeanClass, - rootBean, - object, - cascadeProperty, - keyedValue, - overallViolations, - null, - key, - false - ); - } - })); - - if (!opt.isPresent() && !context.validatedObjects.contains(propertyValue)) { - // maybe a bean - final Path.Node node = context.addPropertyNode(cascadeProperty.getName(), null); - - try { - final boolean canCascade = canCascade(rootBeanClass, context, propertyValue, node); - if (canCascade) { - cascadeToOne( - rootBeanClass, - rootBean, - object, - context, - overallViolations, - cascadeProperty, - cascadeProperty.getType(), - propertyValue, - null); - } - } finally { - context.removeLast(); - } - } - } - } - //noinspection unchecked - return Collections.unmodifiableSet(overallViolations); - } - - private boolean canCascade( - Class rootBeanClass, - DefaultConstraintValidatorContext context, - Object propertyValue, - Path.Node node) { - final boolean canCascade = traversableResolver.isCascadable( - propertyValue, - node, - rootBeanClass, - context.currentPath, - ElementType.FIELD - ); - final boolean isReachable = traversableResolver.isReachable( - propertyValue, - node, - rootBeanClass, - context.currentPath, - ElementType.FIELD - ); - return canCascade && isReachable; - } - - private void cascadeToIterableValue( - DefaultConstraintValidatorContext context, - @NonNull Class rootClass, - @Nullable T rootBean, - Object object, - BeanProperty cascadeProperty, - Object iterableValue, - Set overallViolations, - Integer index, - Object key, - boolean isIterable) { - final DefaultPropertyNode container = new DefaultPropertyNode( - cascadeProperty.getName(), - cascadeProperty.getType(), - index, - key, - ElementKind.CONTAINER_ELEMENT, - isIterable - ); - cascadeToOne( - rootClass, - rootBean, - object, - context, - overallViolations, - cascadeProperty, - cascadeProperty.getType(), - iterableValue, - container - ); - } - - private void cascadeToIterableValue( - DefaultConstraintValidatorContext context, - @NonNull Class rootClass, - @Nullable T rootBean, - @Nullable Object object, - Path.Node node, - Argument methodArgument, - Object iterableValue, - Set overallViolations, - Integer index, - Object key, - boolean isIterable) { - if (canCascade(rootClass, context, iterableValue, node)) { - DefaultPropertyNode currentContainerNode = new DefaultPropertyNode( - methodArgument.getName(), - methodArgument.getClass(), - index, - key, - ElementKind.CONTAINER_ELEMENT, - isIterable - ); - - cascadeToOne( - rootClass, - rootBean, - object, - context, - overallViolations, - methodArgument, - methodArgument.getType(), - iterableValue, - - currentContainerNode); - } - } - - private void cascadeToOne( - @NonNull Class rootClass, - @Nullable T rootBean, - Object object, - DefaultConstraintValidatorContext context, - Set overallViolations, - AnnotatedElement cascadeProperty, - Class propertyType, - Object propertyValue, - @Nullable DefaultPropertyNode container) { - - Class beanType = Object.class; - if (propertyValue != null) { - beanType = propertyValue.getClass(); - } else if (cascadeProperty instanceof BeanProperty) { - Argument argument = ((BeanProperty) cascadeProperty).asArgument(); - if (Map.class.isAssignableFrom(argument.getType())) { - Argument[] typeParameters = argument.getTypeParameters(); - if (typeParameters.length == 2) { - beanType = typeParameters[1].getType(); - } - } else { - - beanType = argument - .getFirstTypeVariable() - .map(Argument::getType) - .orElse(null); - } - } - - final BeanIntrospection beanIntrospection = getBeanIntrospection(beanType); - AnnotationMetadata annotationMetadata = cascadeProperty.getAnnotationMetadata(); - if (beanIntrospection == null && !annotationMetadata.hasStereotype(Constraint.class)) { - // error: only has @Valid but the propertyValue class is not @Introspected - overallViolations.add(createIntrospectionConstraintViolation( - rootClass, rootBean, context, beanType, propertyValue)); - return; - } - - if (beanIntrospection != null) { - if (container != null) { - context.addPropertyNode(container.getName(), container); - } - try { - cascadeToOneIntrospection( - context, - rootBean, - propertyValue, - beanIntrospection, - overallViolations); - } finally { - if (container != null) { - context.removeLast(); - } - - } - - } else { - // try apply cascade rules to actual property - //noinspection unchecked - validateConstrainedPropertyInternal( - rootClass, - rootBean, - object, - cascadeProperty, - propertyType, - propertyValue, - context, - overallViolations, - container - ); - } - } - - private void cascadeToOneIntrospection(DefaultConstraintValidatorContext context, T rootBean, Object bean, BeanIntrospection beanIntrospection, Set overallViolations) { - context.validatedObjects.add(bean); - final Collection> cascadeConstraints = - beanIntrospection.getIndexedProperties(Constraint.class); - final Collection> cascadeNestedProperties = - beanIntrospection.getIndexedProperties(Valid.class); - final List> pojoConstraints = - beanIntrospection.getAnnotationMetadata().getAnnotationTypesByStereotype(Constraint.class); - - if (CollectionUtils.isNotEmpty(cascadeConstraints) || - CollectionUtils.isNotEmpty(cascadeNestedProperties) || - CollectionUtils.isNotEmpty(pojoConstraints) - ) { - doValidate( - beanIntrospection, - rootBean, - bean, - cascadeConstraints, - cascadeNestedProperties, - context, - overallViolations, - pojoConstraints - ); - } - } - - private void validateConstrainedPropertyInternal( - @NonNull Class rootBeanClass, - @Nullable T rootBean, - @NonNull Object object, - @NonNull AnnotatedElement constrainedProperty, - @NonNull Class propertyType, - @Nullable Object propertyValue, - DefaultConstraintValidatorContext context, - Set> overallViolations, - @Nullable DefaultPropertyNode container) { - context.addPropertyNode( - constrainedProperty.getName(), container - ); - - final String currentMessageTemplate = context.getMessageTemplate().orElse(null); - validatePropertyInternal( - rootBeanClass, - rootBean, - object, - context, - overallViolations, - propertyType, - constrainedProperty, - propertyValue - ); - context.removeLast(); - context.messageTemplate(currentMessageTemplate); - } - - private void validatePropertyInternal( - @Nullable Class rootBeanClass, - @Nullable T rootBean, - @Nullable Object object, - @NonNull DefaultConstraintValidatorContext context, - @NonNull Set> overallViolations, - @NonNull Class propertyType, - @NonNull AnnotatedElement constrainedProperty, - @Nullable Object propertyValue) { - final AnnotationMetadata annotationMetadata = constrainedProperty.getAnnotationMetadata(); - final List> constraintTypes = annotationMetadata.getAnnotationTypesByStereotype(Constraint.class); - for (Class constraintType : constraintTypes) { - - ValueExtractor valueExtractor = null; - if (propertyValue != null && !annotationMetadata.hasAnnotation(Valid.class)) { - //noinspection unchecked - valueExtractor = valueExtractorRegistry.findUnwrapValueExtractor((Class) propertyValue.getClass()) - .orElse(null); - } - - if (valueExtractor != null) { - valueExtractor.extractValues(propertyValue, (SimpleValueReceiver) (nodeName, extractedValue) -> valueConstraintOnProperty( - rootBeanClass, - rootBean, - object, - context, - overallViolations, - constrainedProperty, - propertyType, - extractedValue, - constraintType - )); - } else { - valueConstraintOnProperty( - rootBeanClass, - rootBean, - object, - context, - overallViolations, - constrainedProperty, - propertyType, - propertyValue, - constraintType - ); - } - } - } - - @SuppressWarnings("unchecked") - private void valueConstraintOnProperty( - @Nullable Class rootBeanClass, - @Nullable T rootBean, - @Nullable Object object, - DefaultConstraintValidatorContext context, - Set> overallViolations, - AnnotatedElement constrainedProperty, - Class propertyType, - @Nullable Object propertyValue, - Class constraintType) { - final AnnotationMetadata annotationMetadata = constrainedProperty - .getAnnotationMetadata(); - final List> annotationValues = annotationMetadata - .getAnnotationValuesByType(constraintType); - - Set> constraints = new HashSet<>(3); - for (Class group : context.groups) { - for (AnnotationValue annotationValue : annotationValues) { - final Class[] classValues = annotationValue.classValues("groups"); - if (ArrayUtils.isEmpty(classValues)) { - if (context.groups == DEFAULT_GROUPS || group == Default.class) { - constraints.add(annotationValue); - } - } else { - final List> constraintGroups = Arrays.asList(classValues); - if (constraintGroups.contains(group)) { - constraints.add(annotationValue); - } - } - } - } - - @SuppressWarnings("unchecked") final Class targetType = propertyValue != null ? (Class) propertyValue.getClass() : (Class) propertyType; - final ConstraintValidator validator = constraintValidatorRegistry - .findConstraintValidator(constraintType, targetType).orElse(null); - if (validator != null) { - for (AnnotationValue annotationValue : constraints) { - //noinspection unchecked - if (!validator.isValid(propertyValue, annotationValue, context)) { - - final String messageTemplate = buildMessageTemplate(context, annotationValue, annotationMetadata); - Map variables = newConstraintVariables(annotationValue, propertyValue, annotationMetadata); - //noinspection unchecked - overallViolations.add( - new DefaultConstraintViolation( - rootBean, - rootBeanClass, - object, - propertyValue, - messageSource.interpolate(messageTemplate, MessageSource.MessageContext.of(variables)), - messageTemplate, - new PathImpl(context.currentPath), - new DefaultConstraintDescriptor(annotationMetadata, constraintType, annotationValue)) - - ); - } - } - } - } - - private Map newConstraintVariables(AnnotationValue annotationValue, @Nullable Object propertyValue, AnnotationMetadata annotationMetadata) { - final Map values = annotationValue.getValues(); - int initSize = (int) Math.ceil(values.size() / 0.75); - Map variables = new LinkedHashMap<>(initSize); - for (Map.Entry entry : values.entrySet()) { - variables.put(entry.getKey().toString(), entry.getValue()); - } - variables.put("validatedValue", propertyValue); - final Map defaultValues = annotationMetadata.getDefaultValues(annotationValue.getAnnotationName()); - for (Map.Entry entry : defaultValues.entrySet()) { - final String n = entry.getKey().toString(); - if (!variables.containsKey(n)) { - final Object v = entry.getValue(); - if (v != null) { - variables.put(n, v); - } - } - } - return variables; - } - - private String buildMessageTemplate(final DefaultConstraintValidatorContext context, final AnnotationValue annotationValue, - final AnnotationMetadata annotationMetadata) { - return context.getMessageTemplate() - .orElseGet(() -> annotationValue.stringValue("message") - .orElseGet(() -> annotationMetadata.getDefaultValue(annotationValue.getAnnotationName(), "message", String.class) - .orElse("{" + annotationValue.getAnnotationName() + ".message}"))); - } - - @NonNull - @Override - public Publisher validatePublisher(@NonNull Publisher publisher, Class... groups) { - ArgumentUtils.requireNonNull("publisher", publisher); - final Publisher reactiveSequence = Publishers.convertPublisher(conversionService, publisher, Publisher.class); - return Flux.from(reactiveSequence).flatMap(object -> { - final Set> constraintViolations = validate(object, groups); - if (!constraintViolations.isEmpty()) { - return Flux.error(new ConstraintViolationException(constraintViolations)); - } - return Flux.just(object); - }); - } - - @NonNull - @Override - public CompletionStage validateCompletionStage(@NonNull CompletionStage completionStage, Class... groups) { - ArgumentUtils.requireNonNull("completionStage", completionStage); - return completionStage.thenApply(t -> { - final Set> constraintViolations = validate(t, groups); - if (!constraintViolations.isEmpty()) { - throw new ConstraintViolationException(constraintViolations); - } - return t; - }); - } - - @Override - public void validateBeanArgument( - @NonNull BeanResolutionContext resolutionContext, - @NonNull InjectionPoint injectionPoint, - @NonNull Argument argument, - int index, - @Nullable T value) throws BeanInstantiationException { - final AnnotationMetadata annotationMetadata = argument.getAnnotationMetadata(); - final boolean hasValid = annotationMetadata.hasStereotype(Valid.class); - final boolean hasConstraint = annotationMetadata.hasStereotype(Constraint.class); - final Class parameterType = argument.getType(); - final Class rootClass = injectionPoint.getDeclaringBean().getBeanType(); - DefaultConstraintValidatorContext context = new DefaultConstraintValidatorContext(value); - Set overallViolations = new HashSet<>(5); - if (hasConstraint) { - final Path.Node parentNode = context.addConstructorNode(rootClass.getName(), injectionPoint.getDeclaringBean().getConstructor().getArguments()); - ValueExtractor valueExtractor = (ValueExtractor) valueExtractorRegistry.findValueExtractor(parameterType).orElse(null); - if (valueExtractor != null) { - valueExtractor.extractValues(value, new ValueExtractor.ValueReceiver() { - @Override - public void value(String nodeName, Object object1) { - } - - @Override - public void iterableValue(String nodeName, Object iterableValue) { - if (iterableValue != null && context.validatedObjects.contains(iterableValue)) { - return; - } - cascadeToIterableValue( - context, - rootClass, - null, - value, - parentNode, - argument, - iterableValue, - overallViolations, - null, - null, - true); - } - - @Override - public void indexedValue(String nodeName, int i, Object iterableValue) { - if (iterableValue != null && context.validatedObjects.contains(iterableValue)) { - return; - } - cascadeToIterableValue( - context, - rootClass, - null, - value, - parentNode, - argument, - iterableValue, - overallViolations, - i, - null, - true); - } - - @Override - public void keyedValue(String nodeName, Object key, Object keyedValue) { - if (keyedValue != null && context.validatedObjects.contains(keyedValue)) { - return; - } - cascadeToIterableValue( - context, - rootClass, - null, - value, - parentNode, - argument, - keyedValue, - overallViolations, - null, - key, - false); - } - }); - } else { - validateParameterInternal( - rootClass, - null, - ArrayUtils.EMPTY_OBJECT_ARRAY, - context, - overallViolations, - argument.getName(), - parameterType, - index, - annotationMetadata, - value - ); - } - context.removeLast(); - } else if (hasValid && value != null) { - final BeanIntrospection beanIntrospection = getBeanIntrospection(value, parameterType); - if (beanIntrospection != null) { - try { - context.addParameterNode(argument.getName(), index); - cascadeToOneIntrospection( - context, - null, - value, - beanIntrospection, - overallViolations - ); - } finally { - context.removeLast(); - } - } - } - - failOnError(resolutionContext, overallViolations, rootClass); - } - - @Override - public void validateBean(@NonNull BeanResolutionContext resolutionContext, @NonNull BeanDefinition definition, @NonNull T bean) throws BeanInstantiationException { - Class beanType; - if (definition instanceof ProxyBeanDefinition proxyBeanDefinition) { - beanType = (Class) proxyBeanDefinition.getTargetType(); - } else { - beanType = definition.getBeanType(); - } - final BeanIntrospection introspection = (BeanIntrospection) getBeanIntrospection(bean, beanType); - if (introspection != null) { - Set> errors = validate(introspection, bean); - failOnError(resolutionContext, errors, beanType); - } else if (bean instanceof Intercepted && definition.hasStereotype(ConfigurationReader.class)) { - final Collection> executableMethods = definition.getExecutableMethods(); - if (CollectionUtils.isNotEmpty(executableMethods)) { - Set> errors = new HashSet<>(); - final DefaultConstraintValidatorContext context = new DefaultConstraintValidatorContext(bean); - final Class[] interfaces = beanType.getInterfaces(); - if (ArrayUtils.isNotEmpty(interfaces)) { - context.addConstructorNode(interfaces[0].getSimpleName()); - } else { - context.addConstructorNode(beanType.getSimpleName()); - } - for (ExecutableMethod executableMethod : executableMethods) { - if (executableMethod.hasAnnotation(Property.class)) { - final boolean hasConstraint = executableMethod.hasStereotype(Constraint.class); - final boolean isValid = executableMethod.hasStereotype(Valid.class); - if (hasConstraint || isValid) { - final Object value = executableMethod.invoke(bean); - validateConstrainedPropertyInternal( - beanType, - bean, - bean, - executableMethod, - executableMethod.getReturnType().getType(), - value, - context, - errors, - null - ); - } - } - } - - failOnError(resolutionContext, errors, beanType); - } - } else { - throw new BeanInstantiationException(resolutionContext, "Cannot validate bean [" + beanType.getName() + "]. No bean introspection present. Please add @Introspected."); - } - } - - private void failOnError(@NonNull BeanResolutionContext resolutionContext, Set> errors, Class beanType) { - if (!errors.isEmpty()) { - StringBuilder builder = new StringBuilder() - .append("Validation failed for bean definition [") - .append(beanType.getName()) - .append("]\nList of constraint violations:[\n"); - for (ConstraintViolation violation : errors) { - builder.append('\t').append(violation.getPropertyPath()).append(" - ").append(violation.getMessage()).append('\n'); - } - builder.append(']'); - throw new BeanInstantiationException(resolutionContext, builder.toString()); - } - } - - @NonNull - private DefaultConstraintViolation createIntrospectionConstraintViolation( - @NonNull Class rootClass, - T object, - DefaultConstraintValidatorContext context, - Class parameterType, - Object parameterValue, - Object... parameters - ) { - final String messageTemplate = context.getMessageTemplate() - .orElseGet(() -> "{" + Introspected.class.getName() + ".message}"); - return new DefaultConstraintViolation<>(object, rootClass, object, parameterValue, - messageSource.interpolate(messageTemplate, MessageSource.MessageContext.of(Collections.singletonMap("type", parameterType.getName()))), - messageTemplate, new PathImpl(context.currentPath), null, parameters); - } - - /** - * The context object. - */ - private final class DefaultConstraintValidatorContext implements ConstraintValidatorContext { - final Set validatedObjects = new HashSet<>(20); - final PathImpl currentPath; - final List> groups; - String messageTemplate = null; - - private DefaultConstraintValidatorContext(T object, Class... groups) { - this(object, new PathImpl(), groups); - } - - private DefaultConstraintValidatorContext(T object, PathImpl path, Class... groups) { - if (object != null) { - validatedObjects.add(object); - } - if (ArrayUtils.isNotEmpty(groups)) { - sanityCheckGroups(groups); - - List> groupList = new ArrayList<>(); - for (Class group: groups) { - addInheritedGroups(group, groupList); - } - this.groups = Collections.unmodifiableList(groupList); - } else { - this.groups = DEFAULT_GROUPS; - } - - this.currentPath = path != null ? path : new PathImpl(); - } - - private DefaultConstraintValidatorContext(Class... groups) { - this(null, groups); - } - - private void sanityCheckGroups(Class[] groups) { - ArgumentUtils.requireNonNull("groups", groups); - - for (Class clazz : groups) { - if (clazz == null) { - throw new IllegalArgumentException("Validation groups must be non-null"); - } - if (!clazz.isInterface()) { - throw new IllegalArgumentException( - "Validation groups must be interfaces. " + clazz.getName() + " is not."); - } - } - } - - private void addInheritedGroups(Class group, List> groups) { - if (!groups.contains(group)) { - groups.add(group); - } - - for (Class inheritedGroup : group.getInterfaces()) { - addInheritedGroups(inheritedGroup, groups); - } - } - - @NonNull - @Override - public ClockProvider getClockProvider() { - return clockProvider; - } - - @Nullable - @Override - public Object getRootBean() { - return validatedObjects.isEmpty() ? null : validatedObjects.iterator().next(); - } - - @Override - public void messageTemplate(@Nullable final String messageTemplate) { - this.messageTemplate = messageTemplate; - } - - Optional getMessageTemplate() { - return Optional.ofNullable(messageTemplate); - } - - Path.Node addPropertyNode(String name, @Nullable DefaultPropertyNode container) { - final DefaultPropertyNode node; - if (container != null) { - node = new DefaultPropertyNode( - name, container - ); - } else { - node = new DefaultPropertyNode(name, null, null, null, ElementKind.PROPERTY, false); - } - currentPath.nodes.add(node); - return node; - } - - Path.Node addReturnValueNode(String name) { - final DefaultReturnValueNode returnValueNode; - returnValueNode = new DefaultReturnValueNode(name); - currentPath.nodes.add(returnValueNode); - return returnValueNode; - } - - void removeLast() { - currentPath.nodes.removeLast(); - } - - Path.Node addMethodNode(MethodReference reference) { - final DefaultMethodNode methodNode = new DefaultMethodNode(reference); - currentPath.nodes.add(methodNode); - return methodNode; - } - - void addParameterNode(String name, int index) { - final DefaultParameterNode node; - node = new DefaultParameterNode( - name, index - ); - currentPath.nodes.add(node); - } - - Path.Node addConstructorNode(String simpleName, Argument... constructorArguments) { - final DefaultConstructorNode node = new DefaultConstructorNode(new MethodReference() { - - @Override - public Argument[] getArguments() { - return constructorArguments; - } - - @Override - public Method getTargetMethod() { - return null; - } - - @Override - public ReturnType getReturnType() { - return null; - } - - @Override - public Class getDeclaringType() { - return null; - } - - @Override - public String getMethodName() { - return simpleName; - } - }); - currentPath.nodes.add(node); - return node; - } - } - - /** - * Path implementation. - */ - private final class PathImpl implements Path { - - final Deque nodes; - - /** - * Copy constructor. - * - * @param nodes The nodes - */ - private PathImpl(PathImpl nodes) { - this.nodes = new LinkedList<>(nodes.nodes); - } - - private PathImpl() { - this.nodes = new LinkedList<>(); - } - - @Override - public Iterator iterator() { - return nodes.iterator(); - } - - @Override - public String toString() { - StringBuilder builder = new StringBuilder(); - final Iterator i = nodes.iterator(); - while (i.hasNext()) { - final Node node = i.next(); - builder.append(node.getName()); - if (node.getKind() == ElementKind.CONTAINER_ELEMENT) { - final Integer index = node.getIndex(); - if (index != null) { - builder.append('[').append(index).append(']'); - } else { - final Object key = node.getKey(); - if (key != null) { - builder.append('[').append(key).append(']'); - } else { - builder.append("[]"); - } - } - - } - - if (i.hasNext()) { - builder.append('.'); - } - - } - return builder.toString(); - } - } - - /** - * Constructor node. - */ - private final class DefaultConstructorNode extends DefaultMethodNode implements Path.ConstructorNode { - public DefaultConstructorNode(MethodReference methodReference) { - super(methodReference); - } - - @Override - public ElementKind getKind() { - return ElementKind.CONSTRUCTOR; - } - } - - /** - * Method node implementation. - */ - private class DefaultMethodNode implements Path.MethodNode { - - private final MethodReference methodReference; - - public DefaultMethodNode(MethodReference methodReference) { - this.methodReference = methodReference; - } - - @Override - public List> getParameterTypes() { - return Arrays.asList(methodReference.getArgumentTypes()); - } - - @Override - public String getName() { - return methodReference.getMethodName(); - } - - @Override - public boolean isInIterable() { - return false; - } - - @Override - public Integer getIndex() { - return null; - } - - @Override - public Object getKey() { - return null; - } - - @Override - public ElementKind getKind() { - return ElementKind.METHOD; - } - - @Override - public String toString() { - return getName(); - } - - @Override - public T as(Class nodeType) { - throw new UnsupportedOperationException("Unwrapping is unsupported by this implementation"); - } - } - - /** - * Method node implementation. - */ - private final class DefaultParameterNode extends DefaultPropertyNode implements Path.ParameterNode { - - private final int parameterIndex; - - DefaultParameterNode(@NonNull String name, int parameterIndex) { - super(name, null, null, null, ElementKind.PARAMETER, false); - this.parameterIndex = parameterIndex; - } - - @Override - public ElementKind getKind() { - return ElementKind.PARAMETER; - } - - @Override - public T as(Class nodeType) { - throw new UnsupportedOperationException("Unwrapping is unsupported by this implementation"); - } - - @Override - public int getParameterIndex() { - return parameterIndex; - } - } - - /** - * Default Return value node implementation. - */ - private class DefaultReturnValueNode implements Path.ReturnValueNode { - private final String name; - private final Integer index; - private final Object key; - private final ElementKind kind; - private final boolean isInIterable; - - public DefaultReturnValueNode(String name, - Integer index, - Object key, - ElementKind kind, - boolean isInIterable) { - this.name = name; - this.index = index; - this.key = key; - this.kind = kind; - this.isInIterable = isInIterable; - } - - public DefaultReturnValueNode(String name) { - this(name, null, null, ElementKind.RETURN_VALUE, false); - } - - @Override - public String getName() { - return name; - } - - @Override - public Integer getIndex() { - return index; - } - - @Override - public Object getKey() { - return key; - } - - @Override - public ElementKind getKind() { - return kind; - } - - @Override - public boolean isInIterable() { - return isInIterable; - } - - @Override - public T as(Class nodeType) { - throw new UnsupportedOperationException("Unwrapping is unsupported by this implementation"); - } - } - - /** - * Default property node impl. - */ - private class DefaultPropertyNode implements Path.PropertyNode { - private final Class containerClass; - private final String name; - private final Integer index; - private final Object key; - private final ElementKind kind; - private final boolean isIterable; - - DefaultPropertyNode( - @NonNull String name, - @Nullable Class containerClass, - @Nullable Integer index, - @Nullable Object key, - @NonNull ElementKind kind, - boolean isIterable) { - this.containerClass = containerClass; - this.name = name; - this.index = index; - this.key = key; - this.kind = kind; - this.isIterable = isIterable || index != null; - } - - DefaultPropertyNode( - @NonNull String name, - @NonNull DefaultPropertyNode parent - ) { - this(name, parent.containerClass, parent.getIndex(), parent.getKey(), ElementKind.CONTAINER_ELEMENT, parent.isInIterable()); - } - - @Override - public Class getContainerClass() { - return containerClass; - } - - @Override - public Integer getTypeArgumentIndex() { - return null; - } - - @Override - public String getName() { - return name; - } - - @Override - public boolean isInIterable() { - return isIterable; - } - - @Override - public Integer getIndex() { - return index; - } - - @Override - public Object getKey() { - return key; - } - - @Override - public ElementKind getKind() { - return kind; - } - - @Override - public String toString() { - return getName(); - } - - @Override - public T as(Class nodeType) { - throw new UnsupportedOperationException("Unwrapping is unsupported by this implementation"); - } - } - - /** - * Default implementation of {@link ConstraintViolation}. - * - * @param The bean type. - */ - private final class DefaultConstraintViolation implements ConstraintViolation { - - private final T rootBean; - private final Object invalidValue; - private final String message; - private final String messageTemplate; - private final Path path; - private final Class rootBeanClass; - private final Object leafBean; - private final ConstraintDescriptor constraintDescriptor; - private final Object[] executableParams; - - private DefaultConstraintViolation( - @Nullable T rootBean, - @Nullable Class rootBeanClass, - Object leafBean, - Object invalidValue, - String message, - String messageTemplate, - Path path, - ConstraintDescriptor constraintDescriptor, - Object... executableParams) { - this.rootBean = rootBean; - this.rootBeanClass = rootBeanClass; - this.invalidValue = invalidValue; - this.message = message; - this.messageTemplate = messageTemplate; - this.path = path; - this.leafBean = leafBean; - this.constraintDescriptor = constraintDescriptor; - this.executableParams = executableParams; - } - - @Override - public String getMessage() { - return message; - } - - @Override - public String getMessageTemplate() { - return messageTemplate; - } - - @Override - public T getRootBean() { - return rootBean; - } - - @Override - public Class getRootBeanClass() { - return rootBeanClass; - } - - @Override - public Object getLeafBean() { - return leafBean; - } - - @Override - public Object[] getExecutableParameters() { - if (executableParams != null) { - return executableParams; - } else { - return ArrayUtils.EMPTY_OBJECT_ARRAY; - } - } - - @Override - public Object getExecutableReturnValue() { - return null; - } - - @Override - public Path getPropertyPath() { - return path; - } - - @Override - public Object getInvalidValue() { - return invalidValue; - } - - @Override - public ConstraintDescriptor getConstraintDescriptor() { - return constraintDescriptor; - } - - @Override - public U unwrap(Class type) { - throw new UnsupportedOperationException("Unwrapping is unsupported by this implementation"); - } - - @Override - public String toString() { - return "DefaultConstraintViolation{" + - "rootBean=" + rootBeanClass + - ", invalidValue=" + invalidValue + - ", path=" + path + - '}'; - } - } - - /** - * An empty descriptor with no constraints. - */ - private final class EmptyDescriptor implements BeanDescriptor, ElementDescriptor.ConstraintFinder { - private final Class elementClass; - - EmptyDescriptor(Class elementClass) { - this.elementClass = elementClass; - } - - @Override - public boolean isBeanConstrained() { - return false; - } - - @Override - public PropertyDescriptor getConstraintsForProperty(String propertyName) { - return null; - } - - @Override - public Set getConstrainedProperties() { - return Collections.emptySet(); - } - - @Override - public MethodDescriptor getConstraintsForMethod(String methodName, Class... parameterTypes) { - return null; - } - - @Override - public Set getConstrainedMethods(MethodType methodType, MethodType... methodTypes) { - return Collections.emptySet(); - } - - @Override - public ConstructorDescriptor getConstraintsForConstructor(Class... parameterTypes) { - return null; - } - - @Override - public Set getConstrainedConstructors() { - return Collections.emptySet(); - } - - @Override - public boolean hasConstraints() { - return false; - } - - @Override - public Class getElementClass() { - return elementClass; - } - - @Override - public ConstraintFinder unorderedAndMatchingGroups(Class... groups) { - return this; - } - - @Override - public ConstraintFinder lookingAt(Scope scope) { - return this; - } - - @Override - public ConstraintFinder declaredOn(ElementType... types) { - return this; - } - - @Override - public Set> getConstraintDescriptors() { - return Collections.emptySet(); - } - - @Override - public ConstraintFinder findConstraints() { - return this; - } - } -} diff --git a/validation/src/main/java/io/micronaut/validation/validator/DefaultValidatorConfiguration.java b/validation/src/main/java/io/micronaut/validation/validator/DefaultValidatorConfiguration.java deleted file mode 100644 index a146080cd82..00000000000 --- a/validation/src/main/java/io/micronaut/validation/validator/DefaultValidatorConfiguration.java +++ /dev/null @@ -1,265 +0,0 @@ -/* - * Copyright 2017-2020 original authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.micronaut.validation.validator; - -import io.micronaut.context.ExecutionHandleLocator; -import io.micronaut.context.MessageSource; -import io.micronaut.context.annotation.ConfigurationProperties; -import io.micronaut.core.annotation.NonNull; -import io.micronaut.core.annotation.Nullable; -import io.micronaut.core.convert.ConversionService; -import io.micronaut.core.util.Toggleable; -import io.micronaut.validation.validator.constraints.ConstraintValidatorRegistry; -import io.micronaut.validation.validator.constraints.DefaultConstraintValidators; -import io.micronaut.validation.validator.extractors.DefaultValueExtractors; -import io.micronaut.validation.validator.extractors.ValueExtractorRegistry; -import io.micronaut.validation.validator.messages.DefaultValidationMessages; -import jakarta.inject.Inject; - -import javax.validation.ClockProvider; -import javax.validation.ConstraintValidatorFactory; -import javax.validation.MessageInterpolator; -import javax.validation.ParameterNameProvider; -import javax.validation.Path; -import javax.validation.TraversableResolver; -import javax.validation.Validator; -import javax.validation.ValidatorContext; -import javax.validation.valueextraction.ValueExtractor; -import java.lang.annotation.ElementType; - -/** - * The default configuration for the validator. - * - * @author graemerocher - * @since 1.2 - */ -@ConfigurationProperties(ValidatorConfiguration.PREFIX) -public class DefaultValidatorConfiguration implements ValidatorConfiguration, Toggleable, ValidatorContext { - - private final ConversionService conversionService; - - @Nullable - private ConstraintValidatorRegistry constraintValidatorRegistry; - - @Nullable - private ValueExtractorRegistry valueExtractorRegistry; - - @Nullable - private ClockProvider clockProvider; - - @Nullable - private TraversableResolver traversableResolver; - - @Nullable - private MessageSource messageSource; - - @Nullable - private ExecutionHandleLocator executionHandleLocator; - - private boolean enabled = true; - - public DefaultValidatorConfiguration(ConversionService conversionService) { - this.conversionService = conversionService; - } - - @Override - @NonNull - public ConstraintValidatorRegistry getConstraintValidatorRegistry() { - if (constraintValidatorRegistry != null) { - return constraintValidatorRegistry; - } - return new DefaultConstraintValidators(); - } - - @Override - public boolean isEnabled() { - return enabled; - } - - /** - * Sets whether Micronaut's validator is enabled. - * - * @param enabled True if it is - * @return this configuration - */ - public DefaultValidatorConfiguration setEnabled(boolean enabled) { - this.enabled = enabled; - return this; - } - - /** - * Sets the constraint validator registry to use. - * @param constraintValidatorRegistry The registry to use - * @return this configuration - */ - @Inject - public DefaultValidatorConfiguration setConstraintValidatorRegistry(@Nullable ConstraintValidatorRegistry constraintValidatorRegistry) { - this.constraintValidatorRegistry = constraintValidatorRegistry; - return this; - } - - @Override - @NonNull - public ValueExtractorRegistry getValueExtractorRegistry() { - if (valueExtractorRegistry != null) { - return valueExtractorRegistry; - } - return new DefaultValueExtractors(); - } - - /** - * Sets the value extractor registry use. - * @param valueExtractorRegistry The registry - * @return this configuration - */ - @Inject - public DefaultValidatorConfiguration setValueExtractorRegistry(@Nullable ValueExtractorRegistry valueExtractorRegistry) { - this.valueExtractorRegistry = valueExtractorRegistry; - return this; - } - - @Override - @NonNull - public ClockProvider getClockProvider() { - if (clockProvider != null) { - return clockProvider; - } else { - return new DefaultClockProvider(); - } - } - - /** - * Sets the clock provider to use. - * @param clockProvider The clock provider - * @return this configuration - */ - @Inject - public DefaultValidatorConfiguration setClockProvider(@Nullable ClockProvider clockProvider) { - this.clockProvider = clockProvider; - return this; - } - - @Override - @NonNull - public TraversableResolver getTraversableResolver() { - if (traversableResolver != null) { - return traversableResolver; - } else { - return new TraversableResolver() { - @Override - public boolean isReachable(Object object, Path.Node node, Class rootType, Path path, ElementType elementType) { - return true; - } - - @Override - public boolean isCascadable(Object object, Path.Node node, Class rootType, Path path, ElementType elementType) { - return true; - } - }; - } - } - - /** - * Sets the traversable resolver to use. - * @param traversableResolver The resolver - * @return This configuration - */ - @Inject - public DefaultValidatorConfiguration setTraversableResolver(@Nullable TraversableResolver traversableResolver) { - this.traversableResolver = traversableResolver; - return this; - } - - @Override - @NonNull - public MessageSource getMessageSource() { - if (messageSource != null) { - return messageSource; - } - return new DefaultValidationMessages(); - } - - /** - * Sets the message source to use. - * - * @param messageSource The message source - * @return this configuration - */ - @Inject - public DefaultValidatorConfiguration setMessageSource(@Nullable MessageSource messageSource) { - this.messageSource = messageSource; - return this; - } - - @Override - @NonNull - public ExecutionHandleLocator getExecutionHandleLocator() { - if (executionHandleLocator != null) { - return executionHandleLocator; - } else { - return ExecutionHandleLocator.EMPTY; - } - } - - /** - * Sets the execution handler locator to use. - * - * @param executionHandleLocator The locator - * @return this configuration - */ - @Inject - public DefaultValidatorConfiguration setExecutionHandleLocator(@Nullable ExecutionHandleLocator executionHandleLocator) { - this.executionHandleLocator = executionHandleLocator; - return this; - } - - @Override - public ValidatorContext messageInterpolator(MessageInterpolator messageInterpolator) { - throw new UnsupportedOperationException("Method messageInterpolator(..) not supported"); - } - - @Override - public ValidatorContext traversableResolver(TraversableResolver traversableResolver) { - this.traversableResolver = traversableResolver; - return this; - } - - @Override - public ValidatorContext constraintValidatorFactory(ConstraintValidatorFactory factory) { - throw new UnsupportedOperationException("Method constraintValidatorFactory(..) not supported"); - } - - @Override - public ValidatorContext parameterNameProvider(ParameterNameProvider parameterNameProvider) { - throw new UnsupportedOperationException("Method parameterNameProvider(..) not supported"); - } - - @Override - public ValidatorContext clockProvider(ClockProvider clockProvider) { - this.clockProvider = clockProvider; - return this; - } - - @Override - public ValidatorContext addValueExtractor(ValueExtractor extractor) { - throw new UnsupportedOperationException("Method addValueExtractor(..) not supported"); - } - - @Override - public Validator getValidator() { - return new DefaultValidator(this, conversionService); - } -} diff --git a/validation/src/main/java/io/micronaut/validation/validator/DefaultValidatorFactory.java b/validation/src/main/java/io/micronaut/validation/validator/DefaultValidatorFactory.java deleted file mode 100644 index 9ea62c996da..00000000000 --- a/validation/src/main/java/io/micronaut/validation/validator/DefaultValidatorFactory.java +++ /dev/null @@ -1,105 +0,0 @@ -/* - * Copyright 2017-2020 original authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.micronaut.validation.validator; - -import io.micronaut.context.annotation.Requires; -import io.micronaut.core.annotation.Internal; -import io.micronaut.core.convert.ConversionService; -import jakarta.inject.Singleton; - -import javax.validation.ClockProvider; -import javax.validation.ConstraintValidatorFactory; -import javax.validation.MessageInterpolator; -import javax.validation.ParameterNameProvider; -import javax.validation.TraversableResolver; -import javax.validation.ValidatorContext; -import javax.validation.ValidatorFactory; - -/** - * Default validator factory implementation. - * - * @author graemerocher - * @since 1.2.0 - */ -@Requires(missingBeans = ValidatorFactory.class) -@Internal -@Singleton -public class DefaultValidatorFactory implements ValidatorFactory { - - private final ConversionService conversionService; - private final Validator validator; - private final ValidatorConfiguration configuration; - - /** - * Default constructor. - * - * @param conversionService The conversion service - * @param validator The validator. - * @param configuration The configuration. - */ - protected DefaultValidatorFactory(ConversionService conversionService, - Validator validator, - ValidatorConfiguration configuration) { - this.conversionService = conversionService; - this.validator = validator; - this.configuration = configuration; - } - - @Override - public javax.validation.Validator getValidator() { - return validator; - } - - @Override - public ValidatorContext usingContext() { - return new DefaultValidatorConfiguration(conversionService); - } - - @Override - public MessageInterpolator getMessageInterpolator() { - throw new UnsupportedOperationException("Method getMessageInterpolator() not supported"); - } - - @Override - public TraversableResolver getTraversableResolver() { - return configuration.getTraversableResolver(); - } - - @Override - public ConstraintValidatorFactory getConstraintValidatorFactory() { - throw new UnsupportedOperationException("Method getConstraintValidatorFactory() not supported"); - } - - @Override - public ParameterNameProvider getParameterNameProvider() { - throw new UnsupportedOperationException("Method getParameterNameProvider() not supported"); - } - - @Override - public ClockProvider getClockProvider() { - return configuration.getClockProvider(); - } - - @Override - public T unwrap(Class type) { - throw new UnsupportedOperationException("Method unwrap(..) not supported"); - } - - @Override - public void close() { - // no-op - } -} diff --git a/validation/src/main/java/io/micronaut/validation/validator/ExecutableMethodValidator.java b/validation/src/main/java/io/micronaut/validation/validator/ExecutableMethodValidator.java deleted file mode 100644 index 6670fa0383f..00000000000 --- a/validation/src/main/java/io/micronaut/validation/validator/ExecutableMethodValidator.java +++ /dev/null @@ -1,140 +0,0 @@ -/* - * Copyright 2017-2020 original authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.micronaut.validation.validator; - -import io.micronaut.core.annotation.NonNull; -import io.micronaut.core.annotation.Nullable; -import io.micronaut.core.beans.BeanIntrospection; -import io.micronaut.core.type.Argument; -import io.micronaut.core.type.MutableArgumentValue; -import io.micronaut.inject.ExecutableMethod; - -import javax.validation.ConstraintViolation; -import javax.validation.ConstraintViolationException; -import javax.validation.executable.ExecutableValidator; -import java.lang.reflect.Constructor; -import java.lang.reflect.Method; -import java.util.Collection; -import java.util.Set; - -/** - * Extended version of {@link ExecutableValidator} that operates on {@link io.micronaut.inject.ExecutableMethod} instances. - * - * @author graemerocher - * @since 1.2 - */ -public interface ExecutableMethodValidator extends ExecutableValidator { - - - /** - * Create a new valid instance. - * - * @param type The type - * @param arguments The arguments - * @param the generic type - * @return The instance - * @throws ConstraintViolationException If a valid instance couldn't be constructed - * @throws IllegalArgumentException If an argument is invalid - */ - @NonNull T createValid(@NonNull Class type, Object... arguments) throws ConstraintViolationException; - - /** - * Validate the parameter values of the given {@link ExecutableMethod}. - * @param object The object - * @param method The method - * @param parameterValues The values - * @param groups The groups - * @param The object type - * @return The constraint violations. - */ - @NonNull Set> validateParameters( - @NonNull T object, - @NonNull ExecutableMethod method, - @NonNull Object[] parameterValues, - @Nullable Class... groups); - - /** - * Validate the parameter values of the given {@link ExecutableMethod}. - * @param object The object - * @param method The method - * @param argumentValues The values - * @param groups The groups - * @param The object type - * @return The constraint violations. - */ - @NonNull Set> validateParameters( - @NonNull T object, - @NonNull ExecutableMethod method, - @NonNull Collection> argumentValues, - @Nullable Class... groups); - - /** - * Validates the return value of a {@link ExecutableMethod}. - * @param object The object - * @param executableMethod The method - * @param returnValue The return value - * @param groups The groups - * @param The object type - * @return A set of contstraint violations - */ - @NonNull Set> validateReturnValue( - @NonNull T object, - @NonNull ExecutableMethod executableMethod, - @Nullable Object returnValue, - @Nullable Class... groups); - - /** - * Validates parameters for the given introspection and values. - * @param introspection The introspection - * @param parameterValues The parameter values - * @param groups The groups - * @param The bean type. - * @return The constraint violations - */ - @NonNull - Set> validateConstructorParameters( - @NonNull BeanIntrospection introspection, - @NonNull Object[] parameterValues, - @Nullable Class... groups); - - /** - * Validates arguments for the given bean type and constructor arguments. - * @param beanType The bean type - * @param constructorArguments The constructor arguments - * @param parameterValues The parameter values - * @param groups The validation groups - * @param The generic type of the bean - * @return A set of constraint violations, if any - */ - Set> validateConstructorParameters( - @NonNull Class beanType, - @NonNull Argument[] constructorArguments, - @NonNull Object[] parameterValues, - @Nullable Class[] groups - ); - - @Override - @NonNull Set> validateParameters(@NonNull T object, @NonNull Method method, @NonNull Object[] parameterValues, @Nullable Class... groups); - - @Override - @NonNull Set> validateReturnValue(@NonNull T object, @NonNull Method method, @Nullable Object returnValue, @Nullable Class... groups); - - @Override - @NonNull Set> validateConstructorParameters(@NonNull Constructor constructor, @NonNull Object[] parameterValues, @Nullable Class... groups); - - @Override - @NonNull Set> validateConstructorReturnValue(@NonNull Constructor constructor, @NonNull T createdObject, @Nullable Class... groups); -} diff --git a/validation/src/main/java/io/micronaut/validation/validator/IntrospectedBeanDescriptor.java b/validation/src/main/java/io/micronaut/validation/validator/IntrospectedBeanDescriptor.java deleted file mode 100644 index 7692b922501..00000000000 --- a/validation/src/main/java/io/micronaut/validation/validator/IntrospectedBeanDescriptor.java +++ /dev/null @@ -1,205 +0,0 @@ -/* - * Copyright 2017-2020 original authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.micronaut.validation.validator; - -import io.micronaut.core.annotation.AnnotationValue; -import io.micronaut.core.annotation.Internal; -import io.micronaut.core.beans.BeanIntrospection; -import io.micronaut.core.beans.BeanProperty; -import io.micronaut.core.util.ArgumentUtils; - -import javax.validation.Constraint; -import javax.validation.Valid; -import javax.validation.metadata.*; -import java.lang.annotation.Annotation; -import java.lang.annotation.ElementType; -import java.util.Collections; -import java.util.Set; -import java.util.stream.Collectors; - -/** - * Basic implementation of {@link BeanDescriptor} that uses bean introspection metadata. - * - * @author graemerocher - * @since 1.2.0 - */ -@Internal -class IntrospectedBeanDescriptor implements BeanDescriptor, ElementDescriptor.ConstraintFinder { - - private final BeanIntrospection beanIntrospection; - - /** - * Default constructor. - * - * @param beanIntrospection The bean introspection - */ - IntrospectedBeanDescriptor(BeanIntrospection beanIntrospection) { - ArgumentUtils.requireNonNull("beanIntrospection", beanIntrospection); - this.beanIntrospection = beanIntrospection; - } - - @Override - public boolean isBeanConstrained() { - return hasConstraints(); - } - - @Override - public PropertyDescriptor getConstraintsForProperty(String propertyName) { - return beanIntrospection.getProperty(propertyName) - .map(IntrospectedPropertyDescriptor::new) - .orElse(null); - } - - @Override - public Set getConstrainedProperties() { - return beanIntrospection.getIndexedProperties(Constraint.class) - .stream() - .map(IntrospectedPropertyDescriptor::new) - .collect(Collectors.toSet()); - } - - @Override - public MethodDescriptor getConstraintsForMethod(String methodName, Class... parameterTypes) { - return null; - } - - @Override - public Set getConstrainedMethods(MethodType methodType, MethodType... methodTypes) { - return Collections.emptySet(); - } - - @Override - public ConstructorDescriptor getConstraintsForConstructor(Class... parameterTypes) { - return null; - } - - @Override - public Set getConstrainedConstructors() { - return Collections.emptySet(); - } - - @Override - public boolean hasConstraints() { - return beanIntrospection.getIndexedProperty(Constraint.class).isPresent(); - } - - @Override - public Class getElementClass() { - return beanIntrospection.getBeanType(); - } - - @Override - public ConstraintFinder unorderedAndMatchingGroups(Class... groups) { - return this; - } - - @Override - public ConstraintFinder lookingAt(Scope scope) { - return this; - } - - @Override - public ConstraintFinder declaredOn(ElementType... types) { - return this; - } - - @Override - public Set> getConstraintDescriptors() { - return Collections.emptySet(); - } - - @Override - public ConstraintFinder findConstraints() { - return this; - } - - /** - * Internal implementation of {@link PropertyDescriptor}. - */ - private final class IntrospectedPropertyDescriptor implements PropertyDescriptor, ConstraintFinder { - - private final BeanProperty beanProperty; - - IntrospectedPropertyDescriptor(BeanProperty beanProperty) { - this.beanProperty = beanProperty; - } - - @Override - public String getPropertyName() { - return beanProperty.getName(); - } - - @Override - public boolean isCascaded() { - return beanProperty.hasAnnotation(Valid.class); - } - - @Override - public Set getGroupConversions() { - return Collections.emptySet(); - } - - @Override - public Set getConstrainedContainerElementTypes() { - return Collections.emptySet(); - } - - @Override - public boolean hasConstraints() { - return beanProperty.hasStereotype(Constraint.class); - } - - @Override - public Class getElementClass() { - return beanProperty.getType(); - } - - @Override - public ConstraintFinder unorderedAndMatchingGroups(Class... groups) { - return this; - } - - @Override - public ConstraintFinder lookingAt(Scope scope) { - return this; - } - - @Override - public ConstraintFinder declaredOn(ElementType... types) { - return this; - } - - @SuppressWarnings("unchecked") - @Override - public Set> getConstraintDescriptors() { - return beanProperty.getAnnotationTypesByStereotype(Constraint.class) - .stream().map(type -> { - AnnotationValue annotation = beanProperty.getAnnotation(type); - DefaultConstraintDescriptor descriptor = new DefaultConstraintDescriptor( - beanProperty.getAnnotationMetadata(), - type, - annotation - ); - return descriptor; - }).collect(Collectors.toSet()); - } - - @Override - public ConstraintFinder findConstraints() { - return this; - } - } -} diff --git a/validation/src/main/java/io/micronaut/validation/validator/ReactiveValidator.java b/validation/src/main/java/io/micronaut/validation/validator/ReactiveValidator.java deleted file mode 100644 index 99b71b5691b..00000000000 --- a/validation/src/main/java/io/micronaut/validation/validator/ReactiveValidator.java +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Copyright 2017-2020 original authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.micronaut.validation.validator; - -import io.micronaut.core.annotation.NonNull; -import org.reactivestreams.Publisher; - -import java.util.concurrent.CompletionStage; - -/** - * Interface for reactive bean validation. - * - * @author graemerocher - * @since 1.2 - */ -public interface ReactiveValidator { - - /** - * Validate the given publisher by returning a new Publisher that validates each emitted value. If a - * constraint violation error occurs a {@link javax.validation.ConstraintViolationException} will be thrown. - * - * @param publisher The publisher - * @param groups The groups - * @param The generic type - * @return The publisher - */ - @NonNull Publisher validatePublisher(@NonNull Publisher publisher, Class... groups); - - - /** - * Validate the given CompletionStage by returning a new CompletionStage that validates the emitted value. If a - * constraint violation error occurs a {@link javax.validation.ConstraintViolationException} will be thrown. - * - * @param completionStage The completion stage - * @param groups The groups - * @param The generic type - * @return The publisher - */ - @NonNull CompletionStage validateCompletionStage(@NonNull CompletionStage completionStage, Class... groups); -} diff --git a/validation/src/main/java/io/micronaut/validation/validator/Validator.java b/validation/src/main/java/io/micronaut/validation/validator/Validator.java deleted file mode 100644 index 54a175d01c4..00000000000 --- a/validation/src/main/java/io/micronaut/validation/validator/Validator.java +++ /dev/null @@ -1,101 +0,0 @@ -/* - * Copyright 2017-2020 original authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.micronaut.validation.validator; - -import io.micronaut.core.annotation.NonNull; -import io.micronaut.core.annotation.Nullable; -import io.micronaut.core.beans.BeanIntrospection; -import io.micronaut.core.convert.ConversionService; - -import javax.validation.Constraint; -import javax.validation.ConstraintViolation; -import javax.validation.Valid; -import java.util.Set; - -/** - * Extended version of the {@link javax.validation.Valid} interface for Micronaut's implementation. - * - *

The {@link #getConstraintsForClass(Class)} method is not supported by the implementation.

- * - * @author graemerocher - * @since 1.2 - */ -public interface Validator extends javax.validation.Validator { - - /** - * Annotation used to define an object as valid. - */ - String ANN_VALID = Valid.class.getName(); - /** - * Annotation used to define a constraint. - */ - String ANN_CONSTRAINT = Constraint.class.getName(); - - /** - * Overridden variation that returns a {@link ExecutableMethodValidator}. - * @return The validator - */ - @Override - @NonNull ExecutableMethodValidator forExecutables(); - - @Override - @NonNull Set> validate( - @NonNull T object, - Class... groups - ); - - /** - * Validate the given introspection and object. - * @param introspection The introspection - * @param object The object - * @param groups The groups - * @param The object type - * @return The constraint violations - */ - @NonNull - Set> validate( - @NonNull BeanIntrospection introspection, - @NonNull T object, @Nullable Class... groups); - - @Override - @NonNull Set> validateProperty( - @NonNull T object, - @NonNull String propertyName, - Class... groups - ); - - @Override - @NonNull Set> validateValue( - @NonNull Class beanType, - @NonNull String propertyName, - @Nullable Object value, - Class... groups - ); - - /** - * Constructs a new default instance. Note that the returned instance will not contain - * managed {@link io.micronaut.validation.validator.constraints.ConstraintValidator} instances and using - * {@link jakarta.inject.Inject} should be preferred. - * - * @return The validator. - */ - static @NonNull Validator getInstance() { - ConversionService conversionService = ConversionService.SHARED; - return new DefaultValidator( - new DefaultValidatorConfiguration(conversionService), - conversionService); - } -} diff --git a/validation/src/main/java/io/micronaut/validation/validator/ValidatorConfiguration.java b/validation/src/main/java/io/micronaut/validation/validator/ValidatorConfiguration.java deleted file mode 100644 index e9bea54a158..00000000000 --- a/validation/src/main/java/io/micronaut/validation/validator/ValidatorConfiguration.java +++ /dev/null @@ -1,81 +0,0 @@ -/* - * Copyright 2017-2020 original authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.micronaut.validation.validator; - -import io.micronaut.context.ExecutionHandleLocator; -import io.micronaut.context.MessageSource; -import io.micronaut.core.annotation.NonNull; -import io.micronaut.validation.validator.constraints.ConstraintValidatorRegistry; -import io.micronaut.validation.validator.extractors.ValueExtractorRegistry; - -import javax.validation.ClockProvider; -import javax.validation.TraversableResolver; - -/** - * Configuration for the {@link Validator}. - * - * @author graemerocher - * @since 1.2 - */ -public interface ValidatorConfiguration { - - /** - * The prefix to use for config. - */ - String PREFIX = "micronaut.validator"; - - /** - * Whether the validator is enabled. - */ - String ENABLED = PREFIX + ".enabled"; - - /** - * @return The constraint registry to use. - */ - @NonNull - ConstraintValidatorRegistry getConstraintValidatorRegistry(); - - /** - * @return The value extractor registry - */ - @NonNull - ValueExtractorRegistry getValueExtractorRegistry(); - - /** - * @return The clock provider - */ - @NonNull - ClockProvider getClockProvider(); - - /** - * @return The traversable resolver to use - */ - @NonNull - TraversableResolver getTraversableResolver(); - - /** - * @return The message source - */ - @NonNull - MessageSource getMessageSource(); - - /** - * The execution handler locator to use. - * @return The locator - */ - @NonNull - ExecutionHandleLocator getExecutionHandleLocator(); -} diff --git a/validation/src/main/java/io/micronaut/validation/validator/constraints/AbstractPatternValidator.java b/validation/src/main/java/io/micronaut/validation/validator/constraints/AbstractPatternValidator.java deleted file mode 100644 index e2b4b59091a..00000000000 --- a/validation/src/main/java/io/micronaut/validation/validator/constraints/AbstractPatternValidator.java +++ /dev/null @@ -1,118 +0,0 @@ -/* - * Copyright 2017-2020 original authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.micronaut.validation.validator.constraints; - -import io.micronaut.core.annotation.AnnotationValue; -import io.micronaut.core.annotation.NonNull; - -import javax.validation.ValidationException; -import javax.validation.constraints.Pattern; -import java.lang.annotation.Annotation; -import java.util.Map; -import java.util.Objects; -import java.util.Optional; -import java.util.concurrent.ConcurrentHashMap; -import java.util.regex.PatternSyntaxException; - -/** - * Abstract pattern validator. - * - * @param The annotation type. - * - * @author graemerocher - * @since 1.2 - */ -abstract class AbstractPatternValidator implements ConstraintValidator { - private static final Pattern.Flag[] ZERO_FLAGS = new Pattern.Flag[0]; - private static final Map COMPUTED_PATTERNS = new ConcurrentHashMap<>(10); - - /** - * Gets the pattern for the given annotation metadata. - * @param annotationMetadata The metadata - * @param isOptional Whether the pattern is required to be returned - * @return The pattern - */ - java.util.regex.Pattern getPattern( - @NonNull AnnotationValue annotationMetadata, - boolean isOptional) { - final Optional regexp = annotationMetadata.get("regexp", String.class); - final String pattern; - - if (isOptional) { - pattern = regexp.orElse(".*"); - } else { - pattern = regexp - .orElseThrow(() -> new ValidationException("No pattern specified")); - } - - final Pattern.Flag[] flags = annotationMetadata.get("flags", Pattern.Flag[].class).orElse(ZERO_FLAGS); - if (isOptional && pattern.equals(".*") && flags.length == 0) { - return null; - } - - int computedFlag = 0; - for (Pattern.Flag flag : flags) { - computedFlag = computedFlag | flag.getValue(); - } - - final PatternKey key = new PatternKey(pattern, computedFlag); - java.util.regex.Pattern regex = COMPUTED_PATTERNS.get(key); - if (regex == null) { - try { - if (computedFlag != 0) { - regex = java.util.regex.Pattern.compile(pattern, computedFlag); - } else { - regex = java.util.regex.Pattern.compile(pattern); - } - } catch (PatternSyntaxException e) { - throw new IllegalArgumentException("Invalid regular expression", e); - } - COMPUTED_PATTERNS.put(key, regex); - } - return regex; - } - - /** - * Key used to cache patterns. - */ - private static final class PatternKey { - final String pattern; - final int flags; - - PatternKey(String pattern, int flags) { - this.pattern = pattern; - this.flags = flags; - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - PatternKey that = (PatternKey) o; - return flags == that.flags && - pattern.equals(that.pattern); - } - - @Override - public int hashCode() { - return Objects.hash(pattern, flags); - } - } -} diff --git a/validation/src/main/java/io/micronaut/validation/validator/constraints/ConstraintValidator.java b/validation/src/main/java/io/micronaut/validation/validator/constraints/ConstraintValidator.java deleted file mode 100644 index ab54c262709..00000000000 --- a/validation/src/main/java/io/micronaut/validation/validator/constraints/ConstraintValidator.java +++ /dev/null @@ -1,92 +0,0 @@ -/* - * Copyright 2017-2020 original authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.micronaut.validation.validator.constraints; - -import io.micronaut.core.annotation.AnnotationValue; -import io.micronaut.core.annotation.Indexed; -import io.micronaut.core.annotation.NonNull; -import io.micronaut.core.annotation.Nullable; - -import javax.validation.ClockProvider; -import javax.validation.Constraint; -import java.lang.annotation.Annotation; -import java.util.Optional; - -/** - * Constraint validator that can be used at either runtime or compilation time and - * is capable of validation {@link javax.validation.Constraint} instances. Allows defining validators that work with both Hibernate validator and Micronaut's validator. - * - *

Unlike the specification's interface this one can uses as a functional interface. Implementor should not implement the {@link #initialize(Annotation)} method and should instead read the passed {@link AnnotationValue}.

- * - * @param
The annotation type - * @param The supported validation types - */ -@Indexed(ConstraintValidator.class) -@FunctionalInterface -public interface ConstraintValidator extends javax.validation.ConstraintValidator { - - /** - * A constraint validator that just returns the object as being valid. - */ - ConstraintValidator VALID = (value, annotationMetadata, context) -> true; - - /** - * Implements the validation logic. - * - *

Implementations should be thread-safe and immutable.

- * - * @param value object to validate - * @param annotationMetadata The annotation metadata - * @param context The context object - * - * @return {@code false} if {@code value} does not pass the constraint - */ - boolean isValid( - @Nullable T value, - @NonNull AnnotationValue
annotationMetadata, - @NonNull ConstraintValidatorContext context); - - @Override - default boolean isValid(T value, javax.validation.ConstraintValidatorContext context) { - // simply adapt the interfaces for now. - return isValid(value, new AnnotationValue<>(Constraint.class.getName()), new ConstraintValidatorContext() { - - private String messageTemplate = context.getDefaultConstraintMessageTemplate(); - - @NonNull - @Override - public ClockProvider getClockProvider() { - return context.getClockProvider(); - } - - @Nullable - @Override - public Object getRootBean() { - return null; - } - - @Override - public void messageTemplate(@Nullable final String messageTemplate) { - this.messageTemplate = messageTemplate; - } - - public Optional getMessageTemplate() { - return Optional.ofNullable(messageTemplate); - } - - }); - } -} diff --git a/validation/src/main/java/io/micronaut/validation/validator/constraints/ConstraintValidatorContext.java b/validation/src/main/java/io/micronaut/validation/validator/constraints/ConstraintValidatorContext.java deleted file mode 100644 index 1c5eab7955b..00000000000 --- a/validation/src/main/java/io/micronaut/validation/validator/constraints/ConstraintValidatorContext.java +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Copyright 2017-2020 original authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.micronaut.validation.validator.constraints; - -import io.micronaut.core.annotation.NonNull; -import io.micronaut.core.annotation.Nullable; - -import javax.validation.ClockProvider; - -/** - * Subset of the {@link javax.validation.ConstraintValidatorContext} interface without the unnecessary parts. - * - * @author graemerocher - * @since 1.2 - */ -public interface ConstraintValidatorContext { - - /** - * Returns the provider for obtaining the current time in the form of a {@link java.time.Clock}, - * e.g. when validating the {@code Future} and {@code Past} constraints. - * - * @return the provider for obtaining the current time, never {@code null}. If no - * specific provider has been configured during bootstrap, a default implementation using - * the current system time and the current default time zone as returned by - * {@link java.time.Clock#systemDefaultZone()} will be returned. - * - * @since 2.0 - */ - @NonNull ClockProvider getClockProvider(); - - /** - * In case of using this constraint validator with {@code javax.validation.ConstraintValidator} returns null, because JRS-303 doesn't - * support passing a root bean in their validation context. - * - * @return The root bean under validation. - */ - @Nullable Object getRootBean(); - - /** - * Sets a message template to be used for the validation error message. - * - * @param messageTemplate the message template - * @since 2.5.0 - */ - default void messageTemplate(@Nullable final String messageTemplate) { - throw new UnsupportedOperationException("not implemented"); - } - -} diff --git a/validation/src/main/java/io/micronaut/validation/validator/constraints/ConstraintValidatorRegistry.java b/validation/src/main/java/io/micronaut/validation/validator/constraints/ConstraintValidatorRegistry.java deleted file mode 100644 index 81a41e45f8f..00000000000 --- a/validation/src/main/java/io/micronaut/validation/validator/constraints/ConstraintValidatorRegistry.java +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Copyright 2017-2020 original authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.micronaut.validation.validator.constraints; - -import io.micronaut.core.annotation.NonNull; - -import javax.validation.ValidationException; -import java.lang.annotation.Annotation; -import java.util.Optional; - -/** - * Interface for a class that is a registry of contraint validator. - * - * @author graemerocher - * @since 1.2 - */ -public interface ConstraintValidatorRegistry { - - /** - * Finds a constraint validator for the given type and target type. - * @param constraintType The annotation type of the constraint. - * @param targetType The type being validated. - * @param The annotation type - * @param The target type - * @return The validator - */ - @NonNull - Optional> findConstraintValidator( - @NonNull Class constraintType, - @NonNull Class targetType); - - /** - * Finds a constraint validator for the given type and target type. - * @param constraintType The annotation type of the constraint. - * @param targetType The type being validated. - * @param The annotation type - * @param The target type - * @return The validator - * @throws ValidationException if no validator is present - */ - @NonNull - default ConstraintValidator getConstraintValidator( - @NonNull Class constraintType, - @NonNull Class targetType) { - return findConstraintValidator(constraintType, targetType) - .orElseThrow(() -> new ValidationException("No constraint validator present able to validate constraint [" + constraintType + "] on type: " + targetType)); - } - -} diff --git a/validation/src/main/java/io/micronaut/validation/validator/constraints/DecimalMaxValidator.java b/validation/src/main/java/io/micronaut/validation/validator/constraints/DecimalMaxValidator.java deleted file mode 100644 index bcfb60d8cf3..00000000000 --- a/validation/src/main/java/io/micronaut/validation/validator/constraints/DecimalMaxValidator.java +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Copyright 2017-2020 original authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.micronaut.validation.validator.constraints; - -import io.micronaut.core.annotation.AnnotationValue; -import io.micronaut.core.annotation.NonNull; -import io.micronaut.core.annotation.Nullable; -import io.micronaut.core.convert.ConversionService; - -import javax.validation.ValidationException; -import javax.validation.constraints.DecimalMax; -import java.math.BigDecimal; - -/** - * Abstract implementation of a validator for {@link DecimalMax}. - * @param The type to constrain - * - * @author graemerocher - * @since 1.2 - */ -public interface DecimalMaxValidator extends ConstraintValidator { - - @Override - default boolean isValid(@Nullable T value, @NonNull AnnotationValue annotationMetadata, @NonNull ConstraintValidatorContext context) { - if (value == null) { - // null considered valid according to spec - return true; - } - - final BigDecimal bigDecimal = annotationMetadata.getValue(String.class) - .map(s -> - ConversionService.SHARED.convert(s, BigDecimal.class) - .orElseThrow(() -> new ValidationException(s + " does not represent a valid BigDecimal format."))) - .orElseThrow(() -> new ValidationException("null does not represent a valid BigDecimal format.")); - - - int result; - try { - result = doComparison(value, bigDecimal); - } catch (NumberFormatException nfe) { - return false; - } - final boolean inclusive = annotationMetadata.get("inclusive", boolean.class).orElse(true); - return inclusive ? result <= 0 : result < 0; - } - - - /** - * Perform the comparison for the given value. - * @param value The value - * @param bigDecimal The big decimal - * @return The result - */ - int doComparison(@NonNull T value, @NonNull BigDecimal bigDecimal); -} diff --git a/validation/src/main/java/io/micronaut/validation/validator/constraints/DecimalMinValidator.java b/validation/src/main/java/io/micronaut/validation/validator/constraints/DecimalMinValidator.java deleted file mode 100644 index 52d87375e26..00000000000 --- a/validation/src/main/java/io/micronaut/validation/validator/constraints/DecimalMinValidator.java +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Copyright 2017-2020 original authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.micronaut.validation.validator.constraints; - -import io.micronaut.core.annotation.AnnotationValue; -import io.micronaut.core.annotation.NonNull; -import io.micronaut.core.annotation.Nullable; -import io.micronaut.core.convert.ConversionService; - -import javax.validation.ValidationException; -import javax.validation.constraints.DecimalMin; -import java.math.BigDecimal; - -/** - * Abstract implementation of a validator for {@link DecimalMin}. - * - * @param The target type. - * @since 1.2 - * @author graemerocher - */ -@FunctionalInterface -public interface DecimalMinValidator extends ConstraintValidator { - - @Override - default boolean isValid(@Nullable T value, @NonNull AnnotationValue annotationMetadata, @NonNull ConstraintValidatorContext context) { - if (value == null) { - // null considered valid according to spec - return true; - } - - final BigDecimal bigDecimal = annotationMetadata.getValue(String.class) - .map(s -> - ConversionService.SHARED.convert(s, BigDecimal.class) - .orElseThrow(() -> new ValidationException(s + " does not represent a valid BigDecimal format."))) - .orElseThrow(() -> new ValidationException("null does not represent a valid BigDecimal format.")); - - - int result; - try { - result = doComparison(value, bigDecimal); - } catch (NumberFormatException nfe) { - return false; - } - final boolean inclusive = annotationMetadata.get("inclusive", boolean.class).orElse(true); - return inclusive ? result >= 0 : result > 0; - } - - /** - * Perform the comparison for the given value. - * @param value The value - * @param bigDecimal The big decimal - * @return The result - */ - int doComparison(@NonNull T value, @NonNull BigDecimal bigDecimal); -} diff --git a/validation/src/main/java/io/micronaut/validation/validator/constraints/DefaultConstraintValidators.java b/validation/src/main/java/io/micronaut/validation/validator/constraints/DefaultConstraintValidators.java deleted file mode 100644 index bfa9c255e7d..00000000000 --- a/validation/src/main/java/io/micronaut/validation/validator/constraints/DefaultConstraintValidators.java +++ /dev/null @@ -1,1045 +0,0 @@ -/* - * Copyright 2017-2020 original authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.micronaut.validation.validator.constraints; - -import io.micronaut.context.BeanContext; -import io.micronaut.context.Qualifier; -import io.micronaut.core.annotation.Introspected; -import io.micronaut.core.annotation.NonNull; -import io.micronaut.core.annotation.Nullable; -import io.micronaut.core.beans.BeanProperty; -import io.micronaut.core.beans.BeanWrapper; -import io.micronaut.core.reflect.ReflectionUtils; -import io.micronaut.core.type.Argument; -import io.micronaut.core.util.ArgumentUtils; -import io.micronaut.core.util.ArrayUtils; -import io.micronaut.core.util.CollectionUtils; -import io.micronaut.core.util.StringUtils; -import io.micronaut.core.util.clhm.ConcurrentLinkedHashMap; -import io.micronaut.inject.qualifiers.Qualifiers; -import io.micronaut.inject.qualifiers.TypeArgumentQualifier; -import jakarta.inject.Inject; -import jakarta.inject.Singleton; - -import javax.validation.ValidationException; -import javax.validation.constraints.*; -import java.lang.annotation.Annotation; -import java.math.BigDecimal; -import java.math.BigInteger; -import java.time.*; -import java.time.chrono.HijrahDate; -import java.time.chrono.JapaneseDate; -import java.time.chrono.MinguoDate; -import java.time.chrono.ThaiBuddhistDate; -import java.time.temporal.TemporalAccessor; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; -import java.util.Map; -import java.util.Objects; -import java.util.Optional; -import java.util.Date; -import java.util.concurrent.atomic.DoubleAccumulator; -import java.util.concurrent.atomic.DoubleAdder; - -/** - * A factory bean that contains implementation for many of the default validations. - * This approach is preferred as it generates less classes and smaller byte code than defining a - * validator class for each case. - * - * @author graemerocher - * @since 1.2 - */ -@Singleton -@Introspected -public class DefaultConstraintValidators implements ConstraintValidatorRegistry { - - private final Map validatorCache = new ConcurrentLinkedHashMap.Builder().initialCapacity(10).maximumWeightedCapacity(40).build(); - - private final ConstraintValidator assertFalseValidator = - (value, annotationMetadata, context) -> value == null || !value; - - private final ConstraintValidator assertTrueValidator = - (value, annotationMetadata, context) -> value == null || value; - - private final DecimalMaxValidator decimalMaxValidatorCharSequence = - (value, bigDecimal) -> new BigDecimal(value.toString()).compareTo(bigDecimal); - - private final DecimalMaxValidator decimalMaxValidatorNumber = DefaultConstraintValidators::compareNumber; - - private final DecimalMinValidator decimalMinValidatorCharSequence = - (value, bigDecimal) -> new BigDecimal(value.toString()).compareTo(bigDecimal); - - private final DecimalMinValidator decimalMinValidatorNumber = DefaultConstraintValidators::compareNumber; - - private final DigitsValidator digitsValidatorNumber = value -> { - if (value instanceof BigDecimal) { - return (BigDecimal) value; - } - return new BigDecimal(value.toString()); - }; - - private final DigitsValidator digitsValidatorCharSequence = - value -> new BigDecimal(value.toString()); - - private final ConstraintValidator maxNumberValidator = - (value, annotationMetadata, context) -> { - if (value == null) { - return true; // nulls are allowed according to spec - } - final Long max = annotationMetadata.getValue(Long.class).orElseThrow(() -> - new ValidationException("@Max annotation specified without value") - ); - - if (value instanceof BigInteger) { - return ((BigInteger) value).compareTo(BigInteger.valueOf(max)) <= 0; - } else if (value instanceof BigDecimal) { - return ((BigDecimal) value).compareTo(BigDecimal.valueOf(max)) <= 0; - } - return value.longValue() <= max; - }; - - private final ConstraintValidator minNumberValidator = - (value, annotationMetadata, context) -> { - if (value == null) { - return true; // nulls are allowed according to spec - } - final Long max = annotationMetadata.getValue(Long.class).orElseThrow(() -> - new ValidationException("@Min annotation specified without value") - ); - - if (value instanceof BigInteger) { - return ((BigInteger) value).compareTo(BigInteger.valueOf(max)) >= 0; - } else if (value instanceof BigDecimal) { - return ((BigDecimal) value).compareTo(BigDecimal.valueOf(max)) >= 0; - } - return value.longValue() >= max; - }; - - private final ConstraintValidator negativeNumberValidator = - (value, annotationMetadata, context) -> { - // null is allowed according to spec - if (value == null) { - return true; - } - if (value instanceof BigDecimal) { - return ((BigDecimal) value).signum() < 0; - } - if (value instanceof BigInteger) { - return ((BigInteger) value).signum() < 0; - } - if (value instanceof Double || - value instanceof Float || - value instanceof DoubleAdder || - value instanceof DoubleAccumulator) { - return value.doubleValue() < 0; - } - return value.longValue() < 0; - }; - - private final ConstraintValidator negativeOrZeroNumberValidator = - (value, annotationMetadata, context) -> { - // null is allowed according to spec - if (value == null) { - return true; - } - if (value instanceof BigDecimal) { - return ((BigDecimal) value).signum() <= 0; - } - if (value instanceof BigInteger) { - return ((BigInteger) value).signum() <= 0; - } - if (value instanceof Double || - value instanceof Float || - value instanceof DoubleAdder || - value instanceof DoubleAccumulator) { - return value.doubleValue() <= 0; - } - return value.longValue() <= 0; - }; - - private final ConstraintValidator positiveNumberValidator = - (value, annotationMetadata, context) -> { - // null is allowed according to spec - if (value == null) { - return true; - } - if (value instanceof BigDecimal) { - return ((BigDecimal) value).signum() > 0; - } - if (value instanceof BigInteger) { - return ((BigInteger) value).signum() > 0; - } - if (value instanceof Double || - value instanceof Float || - value instanceof DoubleAdder || - value instanceof DoubleAccumulator) { - return value.doubleValue() > 0; - } - return value.longValue() > 0; - }; - - private final ConstraintValidator positiveOrZeroNumberValidator = - (value, annotationMetadata, context) -> { - // null is allowed according to spec - if (value == null) { - return true; - } - if (value instanceof BigDecimal) { - return ((BigDecimal) value).signum() >= 0; - } - if (value instanceof BigInteger) { - return ((BigInteger) value).signum() >= 0; - } - if (value instanceof Double || - value instanceof Float || - value instanceof DoubleAdder || - value instanceof DoubleAccumulator) { - return value.doubleValue() >= 0; - } - return value.longValue() >= 0; - }; - - private final ConstraintValidator notBlankValidator = - (value, annotationMetadata, context) -> - StringUtils.isNotEmpty(value) && value.toString().trim().length() > 0; - - private final ConstraintValidator notNullValidator = - (value, annotationMetadata, context) -> value != null; - - private final ConstraintValidator nullValidator = - (value, annotationMetadata, context) -> value == null; - - private final ConstraintValidator notEmptyByteArrayValidator = - (value, annotationMetadata, context) -> value != null && value.length > 0; - - private final ConstraintValidator notEmptyCharArrayValidator = - (value, annotationMetadata, context) -> value != null && value.length > 0; - - private final ConstraintValidator notEmptyBooleanArrayValidator = - (value, annotationMetadata, context) -> value != null && value.length > 0; - - private final ConstraintValidator notEmptyDoubleArrayValidator = - (value, annotationMetadata, context) -> value != null && value.length > 0; - - private final ConstraintValidator notEmptyFloatArrayValidator = - (value, annotationMetadata, context) -> value != null && value.length > 0; - - private final ConstraintValidator notEmptyIntArrayValidator = - (value, annotationMetadata, context) -> value != null && value.length > 0; - - private final ConstraintValidator notEmptyLongArrayValidator = - (value, annotationMetadata, context) -> value != null && value.length > 0; - - private final ConstraintValidator notEmptyObjectArrayValidator = (value, annotationMetadata, context) -> value != null && value.length > 0; - - private final ConstraintValidator notEmptyShortArrayValidator = - (value, annotationMetadata, context) -> value != null && value.length > 0; - - private final ConstraintValidator notEmptyCharSequenceValidator = - (value, annotationMetadata, context) -> StringUtils.isNotEmpty(value); - - private final ConstraintValidator notEmptyCollectionValidator = - (value, annotationMetadata, context) -> CollectionUtils.isNotEmpty(value); - - private final ConstraintValidator notEmptyMapValidator = - (value, annotationMetadata, context) -> CollectionUtils.isNotEmpty(value); - - private final SizeValidator sizeObjectArrayValidator = value -> value.length; - - private final SizeValidator sizeByteArrayValidator = value -> value.length; - - private final SizeValidator sizeCharArrayValidator = value -> value.length; - - private final SizeValidator sizeBooleanArrayValidator = value -> value.length; - - private final SizeValidator sizeDoubleArrayValidator = value -> value.length; - - private final SizeValidator sizeFloatArrayValidator = value -> value.length; - - private final SizeValidator sizeIntArrayValidator = value -> value.length; - - private final SizeValidator sizeLongArrayValidator = value -> value.length; - - private final SizeValidator sizeShortArrayValidator = value -> value.length; - - private final SizeValidator sizeCharSequenceValidator = CharSequence::length; - - private final SizeValidator sizeCollectionValidator = Collection::size; - - private final SizeValidator sizeMapValidator = Map::size; - - private final ConstraintValidator pastTemporalAccessorConstraintValidator = - (value, annotationMetadata, context) -> { - if (value == null) { - // null is valid according to spec - return true; - } - Comparable comparable = getNow(value, context.getClockProvider().getClock()); - return comparable.compareTo(value) > 0; - }; - - private final ConstraintValidator pastDateConstraintValidator = - (value, annotationMetadata, context) -> { - if (value == null) { - // null is valid according to spec - return true; - } - Comparable comparable = Date.from(context.getClockProvider().getClock().instant()); - return comparable.compareTo(value) > 0; - }; - - private final ConstraintValidator pastOrPresentTemporalAccessorConstraintValidator = - (value, annotationMetadata, context) -> { - if (value == null) { - // null is valid according to spec - return true; - } - Comparable comparable = getNow(value, context.getClockProvider().getClock()); - return comparable.compareTo(value) >= 0; - }; - - private final ConstraintValidator pastOrPresentDateConstraintValidator = - (value, annotationMetadata, context) -> { - if (value == null) { - // null is valid according to spec - return true; - } - Comparable comparable = Date.from(context.getClockProvider().getClock().instant()); - return comparable.compareTo(value) >= 0; - }; - - private final ConstraintValidator futureTemporalAccessorConstraintValidator = (value, annotationMetadata, context) -> { - if (value == null) { - // null is valid according to spec - return true; - } - Comparable comparable = getNow(value, context.getClockProvider().getClock()); - return comparable.compareTo(value) < 0; - }; - - private final ConstraintValidator futureDateConstraintValidator = (value, annotationMetadata, context) -> { - if (value == null) { - // null is valid according to spec - return true; - } - Comparable comparable = Date.from(context.getClockProvider().getClock().instant()); - return comparable.compareTo(value) < 0; - }; - - private final ConstraintValidator futureOrPresentTemporalAccessorConstraintValidator = (value, annotationMetadata, context) -> { - if (value == null) { - // null is valid according to spec - return true; - } - Comparable comparable = getNow(value, context.getClockProvider().getClock()); - return comparable.compareTo(value) <= 0; - }; - - private final ConstraintValidator futureOrPresentDateConstraintValidator = (value, annotationMetadata, context) -> { - if (value == null) { - // null is valid according to spec - return true; - } - Comparable comparable = Date.from(context.getClockProvider().getClock().instant()); - return comparable.compareTo(value) <= 0; - }; - - private final @Nullable BeanContext beanContext; - private final Map localValidators; - - /** - * Default constructor. - */ - public DefaultConstraintValidators() { - this(null); - } - - /** - * Constructor used for DI. - * - * @param beanContext The bean context - */ - @Inject - protected DefaultConstraintValidators(@Nullable BeanContext beanContext) { - this.beanContext = beanContext; - BeanWrapper wrapper = BeanWrapper.findWrapper(DefaultConstraintValidators.class, this).orElse(null); - if (wrapper != null) { - - final Collection> beanProperties = wrapper.getBeanProperties(); - Map validatorMap = new HashMap<>(beanProperties.size()); - for (BeanProperty property : beanProperties) { - if (ConstraintValidator.class.isAssignableFrom(property.getType())) { - final Argument[] typeParameters = property.asArgument().getTypeParameters(); - if (ArrayUtils.isNotEmpty(typeParameters)) { - final int len = typeParameters.length; - - wrapper.getProperty(property.getName(), ConstraintValidator.class).ifPresent(constraintValidator -> { - if (len == 2) { - final Class targetType = ReflectionUtils.getWrapperType(typeParameters[1].getType()); - final ValidatorKey key = new ValidatorKey(typeParameters[0].getType(), targetType); - validatorMap.put(key, constraintValidator); - } else if (len == 1) { - if (constraintValidator instanceof SizeValidator) { - final ValidatorKey key = new ValidatorKey(Size.class, typeParameters[0].getType()); - validatorMap.put(key, constraintValidator); - } else if (constraintValidator instanceof DigitsValidator) { - final ValidatorKey key = new ValidatorKey(Digits.class, typeParameters[0].getType()); - validatorMap.put(key, constraintValidator); - } else if (constraintValidator instanceof DecimalMaxValidator) { - final ValidatorKey key = new ValidatorKey(DecimalMax.class, typeParameters[0].getType()); - validatorMap.put(key, constraintValidator); - } else if (constraintValidator instanceof DecimalMinValidator) { - final ValidatorKey key = new ValidatorKey(DecimalMin.class, typeParameters[0].getType()); - validatorMap.put(key, constraintValidator); - } - } - }); - } - } - } - validatorMap.put( - new ValidatorKey(Pattern.class, CharSequence.class), - new PatternValidator() - ); - validatorMap.put( - new ValidatorKey(Email.class, CharSequence.class), - new EmailValidator() - ); - this.localValidators = validatorMap; - } else { - this.localValidators = Collections.emptyMap(); - } - } - - @SuppressWarnings("unchecked") - @NonNull - @Override - public Optional> findConstraintValidator(@NonNull Class constraintType, @NonNull Class targetType) { - ArgumentUtils.requireNonNull("constraintType", constraintType); - ArgumentUtils.requireNonNull("targetType", targetType); - final ValidatorKey key = new ValidatorKey(constraintType, targetType); - targetType = (Class) ReflectionUtils.getWrapperType(targetType); - ConstraintValidator constraintValidator = localValidators.get(key); - if (constraintValidator != null) { - return Optional.of(constraintValidator); - } else { - constraintValidator = validatorCache.get(key); - if (constraintValidator != null) { - return Optional.of(constraintValidator); - } else { - final Qualifier qualifier = Qualifiers.byTypeArguments( - constraintType, - ReflectionUtils.getWrapperType(targetType) - ); - Class finalTargetType = targetType; - final Class[] finalTypeArguments = {constraintType, finalTargetType}; - final Optional local = localValidators.entrySet().stream().filter(entry -> { - final ValidatorKey k = entry.getKey(); - return TypeArgumentQualifier.areTypesCompatible( - finalTypeArguments, Arrays.asList(k.constraintType, k.targetType) - ); - } - ).map(Map.Entry::getValue).findFirst(); - - if (local.isPresent()) { - validatorCache.put(key, local.get()); - return (Optional) local; - } else if (beanContext != null) { - Optional bean = beanContext - .findBean(ConstraintValidator.class, qualifier); - final ConstraintValidator cv = bean.orElse(ConstraintValidator.VALID); - validatorCache.put(key, cv); - if (cv != ConstraintValidator.VALID) { - return Optional.of(cv); - } - } else { - // last chance lookup - final ConstraintValidator cv = findLocalConstraintValidator(constraintType, targetType) - .orElse(ConstraintValidator.VALID); - validatorCache.put(key, cv); - if (cv != ConstraintValidator.VALID) { - return Optional.of(cv); - } - } - } - } - return Optional.empty(); - } - - /** - * The {@link AssertFalse} validator. - * - * @return The validator - */ - public ConstraintValidator getAssertFalseValidator() { - return assertFalseValidator; - } - - /** - * The {@link AssertTrue} validator. - * - * @return The validator - */ - public ConstraintValidator getAssertTrueValidator() { - return assertTrueValidator; - } - - /** - * The {@link DecimalMax} validator for char sequences. - * - * @return The validator - */ - public DecimalMaxValidator getDecimalMaxValidatorCharSequence() { - return decimalMaxValidatorCharSequence; - } - - /** - * The {@link DecimalMax} validator for number. - * - * @return The validator - */ - public DecimalMaxValidator getDecimalMaxValidatorNumber() { - return decimalMaxValidatorNumber; - } - - /** - * The {@link DecimalMin} validator for char sequences. - * - * @return The validator - */ - public DecimalMinValidator getDecimalMinValidatorCharSequence() { - return decimalMinValidatorCharSequence; - } - - /** - * The {@link DecimalMin} validator for number. - * - * @return The validator - */ - public DecimalMinValidator getDecimalMinValidatorNumber() { - return decimalMinValidatorNumber; - } - - /** - * The {@link Digits} validator for number. - * - * @return The validator - */ - public DigitsValidator getDigitsValidatorNumber() { - return digitsValidatorNumber; - } - - /** - * The {@link Digits} validator for char sequence. - * - * @return The validator - */ - public DigitsValidator getDigitsValidatorCharSequence() { - return digitsValidatorCharSequence; - } - - /** - * The {@link Max} validator for numbers. - * - * @return The validator - */ - public ConstraintValidator getMaxNumberValidator() { - return maxNumberValidator; - } - - /** - * The {@link Min} validator for numbers. - * - * @return The validator - */ - public ConstraintValidator getMinNumberValidator() { - return minNumberValidator; - } - - /** - * The {@link Negative} validator for numbers. - * - * @return The validator - */ - public ConstraintValidator getNegativeNumberValidator() { - return negativeNumberValidator; - } - - /** - * The {@link NegativeOrZero} validator for numbers. - * - * @return The validator - */ - public ConstraintValidator getNegativeOrZeroNumberValidator() { - return negativeOrZeroNumberValidator; - } - - /** - * The {@link Positive} validator for numbers. - * - * @return The validator - */ - public ConstraintValidator getPositiveNumberValidator() { - return positiveNumberValidator; - } - - /** - * The {@link PositiveOrZero} validator for numbers. - * - * @return The validator - */ - public ConstraintValidator getPositiveOrZeroNumberValidator() { - return positiveOrZeroNumberValidator; - } - - /** - * The {@link NotBlank} validator for char sequences. - * - * @return The validator - */ - public ConstraintValidator getNotBlankValidator() { - return notBlankValidator; - } - - /** - * The {@link NotNull} validator. - * - * @return The validator - */ - public ConstraintValidator getNotNullValidator() { - return notNullValidator; - } - - /** - * The {@link Null} validator. - * - * @return The validator - */ - public ConstraintValidator getNullValidator() { - return nullValidator; - } - - /** - * The {@link NotEmpty} validator for byte[]. - * - * @return The validator - */ - public ConstraintValidator getNotEmptyByteArrayValidator() { - return notEmptyByteArrayValidator; - } - - /** - * The {@link NotEmpty} validator for char[]. - * - * @return The validator - */ - public ConstraintValidator getNotEmptyCharArrayValidator() { - return notEmptyCharArrayValidator; - } - - /** - * The {@link NotEmpty} validator for boolean[]. - * - * @return The validator - */ - public ConstraintValidator getNotEmptyBooleanArrayValidator() { - return notEmptyBooleanArrayValidator; - } - - /** - * The {@link NotEmpty} validator for double[]. - * - * @return The validator - */ - public ConstraintValidator getNotEmptyDoubleArrayValidator() { - return notEmptyDoubleArrayValidator; - } - - /** - * The {@link NotEmpty} validator for float[]. - * - * @return The validator - */ - public ConstraintValidator getNotEmptyFloatArrayValidator() { - return notEmptyFloatArrayValidator; - } - - /** - * The {@link NotEmpty} validator for int[]. - * - * @return The validator - */ - public ConstraintValidator getNotEmptyIntArrayValidator() { - return notEmptyIntArrayValidator; - } - - /** - * The {@link NotEmpty} validator for long[]. - * - * @return The validator - */ - public ConstraintValidator getNotEmptyLongArrayValidator() { - return notEmptyLongArrayValidator; - } - - /** - * The {@link NotEmpty} validator for Object[]. - * - * @return The validator - */ - public ConstraintValidator getNotEmptyObjectArrayValidator() { - return notEmptyObjectArrayValidator; - } - - /** - * The {@link NotEmpty} validator for short[]. - * - * @return The validator - */ - public ConstraintValidator getNotEmptyShortArrayValidator() { - return notEmptyShortArrayValidator; - } - - /** - * The {@link NotEmpty} validator for char sequence. - * - * @return The validator - */ - public ConstraintValidator getNotEmptyCharSequenceValidator() { - return notEmptyCharSequenceValidator; - } - - /** - * The {@link NotEmpty} validator for collection. - * - * @return The validator - */ - public ConstraintValidator getNotEmptyCollectionValidator() { - return notEmptyCollectionValidator; - } - - /** - * The {@link NotEmpty} validator for map. - * - * @return The validator - */ - public ConstraintValidator getNotEmptyMapValidator() { - return notEmptyMapValidator; - } - - /** - * The {@link Size} validator for Object[]. - * - * @return The validator - */ - public SizeValidator getSizeObjectArrayValidator() { - return sizeObjectArrayValidator; - } - - /** - * The {@link Size} validator for byte[]. - * - * @return The validator - */ - public SizeValidator getSizeByteArrayValidator() { - return sizeByteArrayValidator; - } - - /** - * The {@link Size} validator for char[]. - * - * @return The validator - */ - public SizeValidator getSizeCharArrayValidator() { - return sizeCharArrayValidator; - } - - /** - * The {@link Size} validator for boolean[]. - * - * @return The validator - */ - public SizeValidator getSizeBooleanArrayValidator() { - return sizeBooleanArrayValidator; - } - - /** - * The {@link Size} validator for double[]. - * - * @return The validator - */ - public SizeValidator getSizeDoubleArrayValidator() { - return sizeDoubleArrayValidator; - } - - /** - * The {@link Size} validator for float[]. - * - * @return The validator - */ - public SizeValidator getSizeFloatArrayValidator() { - return sizeFloatArrayValidator; - } - - /** - * The {@link Size} validator for int[]. - * - * @return The validator - */ - public SizeValidator getSizeIntArrayValidator() { - return sizeIntArrayValidator; - } - - /** - * The {@link Size} validator for long[]. - * - * @return The validator - */ - public SizeValidator getSizeLongArrayValidator() { - return sizeLongArrayValidator; - } - - /** - * The {@link Size} validator for short[]. - * - * @return The validator - */ - public SizeValidator getSizeShortArrayValidator() { - return sizeShortArrayValidator; - } - - /** - * The {@link Size} validator for CharSequence. - * - * @return The validator - */ - public SizeValidator getSizeCharSequenceValidator() { - return sizeCharSequenceValidator; - } - - /** - * The {@link Size} validator for Collection. - * - * @return The validator - */ - public SizeValidator getSizeCollectionValidator() { - return sizeCollectionValidator; - } - - /** - * The {@link Size} validator for Map. - * - * @return The validator - */ - public SizeValidator getSizeMapValidator() { - return sizeMapValidator; - } - - /** - * The {@link Past} validator for temporal accessor. - * - * @return The validator - */ - public ConstraintValidator getPastTemporalAccessorConstraintValidator() { - return pastTemporalAccessorConstraintValidator; - } - - /** - * The {@link Past} validator for Date accessor. - * - * @return The validator - */ - public ConstraintValidator getPastDateConstraintValidator() { - return pastDateConstraintValidator; - } - - /** - * The {@link PastOrPresent} validator for temporal accessor. - * - * @return The validator - */ - public ConstraintValidator getPastOrPresentTemporalAccessorConstraintValidator() { - return pastOrPresentTemporalAccessorConstraintValidator; - } - - /** - * The {@link PastOrPresent} validator for Date accessor. - * - * @return The validator - */ - public ConstraintValidator getPastOrPresentDateConstraintValidator() { - return pastOrPresentDateConstraintValidator; - } - - /** - * The {@link Future} validator for temporal accessor. - * - * @return The validator - */ - public ConstraintValidator getFutureTemporalAccessorConstraintValidator() { - return futureTemporalAccessorConstraintValidator; - } - - /** - * The {@link Future} validator for Date accessor. - * - * @return The validator - */ - public ConstraintValidator getFutureDateConstraintValidator() { - return futureDateConstraintValidator; - } - - /** - * The {@link FutureOrPresent} validator for temporal accessor. - * - * @return The validator - */ - public ConstraintValidator getFutureOrPresentTemporalAccessorConstraintValidator() { - return futureOrPresentTemporalAccessorConstraintValidator; - } - - /** - * The {@link FutureOrPresent} validator for Date accessor. - * - * @return The validator - */ - public ConstraintValidator getFutureOrPresentDateConstraintValidator() { - return futureOrPresentDateConstraintValidator; - } - - /** - * Last chance resolve for constraint validator. - * @param constraintType The constraint type - * @param targetType The target type - * @param The annotation type - * @param The target type - * @return The validator if present - */ - protected Optional findLocalConstraintValidator( - @NonNull Class constraintType, @NonNull Class targetType) { - return Optional.empty(); - } - - private Comparable getNow(TemporalAccessor value, Clock clock) { - if (!(value instanceof Comparable)) { - throw new IllegalArgumentException("TemporalAccessor value must be comparable"); - } - - if (value instanceof LocalDateTime) { - return LocalDateTime.now(clock); - } else if (value instanceof Instant) { - return Instant.now(clock); - } else if (value instanceof ZonedDateTime) { - return ZonedDateTime.now(clock); - } else if (value instanceof OffsetDateTime) { - return OffsetDateTime.now(clock); - } else if (value instanceof LocalDate) { - return LocalDate.now(clock); - } else if (value instanceof LocalTime) { - return LocalTime.now(clock); - } else if (value instanceof OffsetTime) { - return OffsetTime.now(clock); - } else if (value instanceof MonthDay) { - return MonthDay.now(clock); - } else if (value instanceof Year) { - return Year.now(clock); - } else if (value instanceof YearMonth) { - return YearMonth.now(clock); - } else if (value instanceof HijrahDate) { - return HijrahDate.now(clock); - } else if (value instanceof JapaneseDate) { - return JapaneseDate.now(clock); - } else if (value instanceof ThaiBuddhistDate) { - return ThaiBuddhistDate.now(clock); - } else if (value instanceof MinguoDate) { - return MinguoDate.now(clock); - } - throw new IllegalArgumentException("TemporalAccessor value type not supported: " + value.getClass()); - } - - /** - * Performs the comparision for number. - * @param value The value - * @param bigDecimal The big decimal - * @return The result - */ - private static int compareNumber(@NonNull Number value, @NonNull BigDecimal bigDecimal) { - int result; - if (value instanceof BigDecimal) { - result = ((BigDecimal) value).compareTo(bigDecimal); - } else if (value instanceof BigInteger) { - result = new BigDecimal((BigInteger) value).compareTo(bigDecimal); - } else { - result = BigDecimal.valueOf(value.doubleValue()).compareTo(bigDecimal); - } - return result; - } - - /** - * Key for caching validators. - * @param The annotation type - * @param The target type. - */ - protected final class ValidatorKey { - private final Class constraintType; - private final Class targetType; - - /** - * The key to lookup the validator. - * @param constraintType THe constraint type - * @param targetType The target type - */ - public ValidatorKey(@NonNull Class constraintType, @NonNull Class targetType) { - this.constraintType = constraintType; - this.targetType = targetType; - } - - /** - * @return The constraint type - */ - public Class getConstraintType() { - return constraintType; - } - - /** - * @return The target type - */ - public Class getTargetType() { - return targetType; - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - ValidatorKey key = (ValidatorKey) o; - return constraintType.equals(key.constraintType) && - targetType.equals(key.targetType); - } - - @Override - public int hashCode() { - return Objects.hash(constraintType, targetType); - } - } - -} diff --git a/validation/src/main/java/io/micronaut/validation/validator/constraints/DigitsValidator.java b/validation/src/main/java/io/micronaut/validation/validator/constraints/DigitsValidator.java deleted file mode 100644 index 80a6fbbe721..00000000000 --- a/validation/src/main/java/io/micronaut/validation/validator/constraints/DigitsValidator.java +++ /dev/null @@ -1,70 +0,0 @@ -/* - * Copyright 2017-2020 original authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.micronaut.validation.validator.constraints; - -import io.micronaut.core.annotation.AnnotationValue; -import io.micronaut.core.annotation.NonNull; -import io.micronaut.core.annotation.Nullable; - -import javax.validation.constraints.Digits; -import java.math.BigDecimal; - -/** - * Abstract {@link Digits} validator implementation. - * @param The target type - * - * @author graemerocher - * @since 1.2 - */ -@FunctionalInterface -public interface DigitsValidator extends ConstraintValidator { - @Override - default boolean isValid(@Nullable T value, @NonNull AnnotationValue annotationMetadata, @NonNull ConstraintValidatorContext context) { - if (value == null) { - // null valid according to spec - return true; - } - final int intMax = annotationMetadata.get("integer", int.class).orElse(0); - - if (intMax < 0) { - throw new IllegalArgumentException("The length of the integer part cannot be negative."); - } - - final int fracMax = annotationMetadata.get("fraction", int.class).orElse(0); - if (fracMax < 0) { - throw new IllegalArgumentException("The length of the fraction part cannot be negative."); - } - - BigDecimal bigDecimal; - try { - bigDecimal = getBigDecimal(value); - } catch (NumberFormatException e) { - return false; - } - - int intLen = bigDecimal.precision() - bigDecimal.scale(); - int fracLen = bigDecimal.scale() < 0 ? 0 : bigDecimal.scale(); - - return intMax >= intLen && fracMax >= fracLen; - } - - /** - * Resolve a big decimal for the given value. - * @param value The value - * @return The big decimal - */ - BigDecimal getBigDecimal(@NonNull T value); -} diff --git a/validation/src/main/java/io/micronaut/validation/validator/constraints/DomainNameUtil.java b/validation/src/main/java/io/micronaut/validation/validator/constraints/DomainNameUtil.java deleted file mode 100644 index 91204f9b225..00000000000 --- a/validation/src/main/java/io/micronaut/validation/validator/constraints/DomainNameUtil.java +++ /dev/null @@ -1,110 +0,0 @@ -/* - * Copyright 2017-2020 original authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.micronaut.validation.validator.constraints; - -import java.net.IDN; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import static java.util.regex.Pattern.CASE_INSENSITIVE; - -/** - * Forked from Hibernate Validator. - * - * @author Marko Bekhta - * @author Guillaume Smet - */ -public final class DomainNameUtil { - - /** - * This is the maximum length of a domain name. But be aware that each label (parts separated by a dot) of the - * domain name must be at most 63 characters long. This is verified by {@link IDN#toASCII(String)}. - */ - private static final int MAX_DOMAIN_PART_LENGTH = 255; - - private static final String DOMAIN_CHARS_WITHOUT_DASH = "[a-z\u0080-\uFFFF0-9!#$%&'*+/=?^_`{|}~]"; - private static final String DOMAIN_LABEL = "(" + DOMAIN_CHARS_WITHOUT_DASH + "-*)*" + DOMAIN_CHARS_WITHOUT_DASH + "+"; - private static final String DOMAIN = DOMAIN_LABEL + "+(\\." + DOMAIN_LABEL + "+)*"; - - private static final String IP_DOMAIN = "[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}"; - //IP v6 regex taken from https://stackoverflow.com/questions/53497/regular-expression-that-matches-valid-ipv6-addresses - private static final String IP_V6_DOMAIN = "(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))"; - - /** - * Regular expression for the domain part of an URL - *

- * A host string must be a domain string, an IPv4 address string, or "[", followed by an IPv6 address string, - * followed by "]". - */ - private static final Pattern DOMAIN_PATTERN = Pattern.compile( - DOMAIN + "|\\[" + IP_V6_DOMAIN + "\\]", CASE_INSENSITIVE - ); - - /** - * Regular expression for the domain part of an email address (everything after '@'). - */ - private static final Pattern EMAIL_DOMAIN_PATTERN = Pattern.compile( - DOMAIN + "|\\[" + IP_DOMAIN + "\\]|" + "\\[IPv6:" + IP_V6_DOMAIN + "\\]", CASE_INSENSITIVE - ); - - private DomainNameUtil() { - } - - /** - * Checks the validity of the domain name used in an email. To be valid it should be either a valid host name, or an - * IP address wrapped in []. - * - * @param domain domain to check for validity - * @return {@code true} if the provided string is a valid domain, {@code false} otherwise - */ - public static boolean isValidEmailDomainAddress(String domain) { - return isValidDomainAddress(domain, EMAIL_DOMAIN_PATTERN); - } - - /** - * Checks validity of a domain name. - * - * @param domain the domain to check for validity - * @return {@code true} if the provided string is a valid domain, {@code false} otherwise - */ - public static boolean isValidDomainAddress(String domain) { - return isValidDomainAddress(domain, DOMAIN_PATTERN); - } - - private static boolean isValidDomainAddress(String domain, Pattern pattern) { - // if we have a trailing dot the domain part we have an invalid email address. - // the regular expression match would take care of this, but IDN.toASCII drops the trailing '.' - if (domain.endsWith(".")) { - return false; - } - - Matcher matcher = pattern.matcher(domain); - if (!matcher.matches()) { - return false; - } - - String asciiString; - try { - asciiString = IDN.toASCII(domain); - } catch (IllegalArgumentException e) { - return false; - } - - return asciiString.length() <= MAX_DOMAIN_PART_LENGTH; - } - -} - diff --git a/validation/src/main/java/io/micronaut/validation/validator/constraints/EmailValidator.java b/validation/src/main/java/io/micronaut/validation/validator/constraints/EmailValidator.java deleted file mode 100644 index 98bfe8ea106..00000000000 --- a/validation/src/main/java/io/micronaut/validation/validator/constraints/EmailValidator.java +++ /dev/null @@ -1,101 +0,0 @@ -/* - * Copyright 2017-2020 original authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -/* - * Hibernate Validator, declare and validate application constraints - * - * License: Apache License, Version 2.0 - * See the license.txt file in the root directory or . - */ -package io.micronaut.validation.validator.constraints; - -import io.micronaut.core.annotation.AnnotationValue; -import io.micronaut.core.annotation.NonNull; -import io.micronaut.core.annotation.Nullable; -import jakarta.inject.Singleton; - -import javax.validation.constraints.Email; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import static java.util.regex.Pattern.CASE_INSENSITIVE; - -/** - * Provides Email validation. Largely based off the Hibernate validator implementation. - * - * @author Emmanuel Bernard - * @author Hardy Ferentschik - * @author Guillaume Smet - * @author graemerocher - */ -@Singleton -public class EmailValidator extends AbstractPatternValidator { - private static final int MAX_LOCAL_PART_LENGTH = 64; - - private static final String LOCAL_PART_ATOM = "[a-z0-9!#$%&'*+/=?^_`{|}~\u0080-\uFFFF-]"; - private static final String LOCAL_PART_INSIDE_QUOTES_ATOM = "([a-z0-9!#$%&'*.(),<>\\[\\]:; @+/=?^_`{|}~\u0080-\uFFFF-]|\\\\\\\\|\\\\\\\")"; - - /** - * Regular expression for the local part of an email address (everything before '@'). - */ - private static final Pattern LOCAL_PART_PATTERN = Pattern.compile( - "(" + LOCAL_PART_ATOM + "+|\"" + LOCAL_PART_INSIDE_QUOTES_ATOM + "+\")" + - "(\\." + "(" + LOCAL_PART_ATOM + "+|\"" + LOCAL_PART_INSIDE_QUOTES_ATOM + "+\")" + ")*", CASE_INSENSITIVE - ); - - @Override - public boolean isValid( - @Nullable CharSequence value, - @NonNull AnnotationValue annotationMetadata, - @NonNull ConstraintValidatorContext context) { - if (value == null) { - return true; - } - - String stringValue = value.toString(); - int i = stringValue.lastIndexOf('@'); - - // no @ character - if (i < 0) { - return false; - } - - String localPart = stringValue.substring(0, i); - String domainPart = stringValue.substring(i + 1); - - boolean isValid; - if (!isValidEmailLocalPart(localPart)) { - isValid = false; - } else { - isValid = DomainNameUtil.isValidEmailDomainAddress(domainPart); - } - - final Pattern pattern = getPattern(annotationMetadata, true); - if (pattern == null || !isValid) { - return isValid; - } - - Matcher m = pattern.matcher(value); - return m.matches(); - } - - private boolean isValidEmailLocalPart(String localPart) { - if (localPart.length() > MAX_LOCAL_PART_LENGTH) { - return false; - } - Matcher matcher = LOCAL_PART_PATTERN.matcher(localPart); - return matcher.matches(); - } -} diff --git a/validation/src/main/java/io/micronaut/validation/validator/constraints/PatternValidator.java b/validation/src/main/java/io/micronaut/validation/validator/constraints/PatternValidator.java deleted file mode 100644 index 267ba113568..00000000000 --- a/validation/src/main/java/io/micronaut/validation/validator/constraints/PatternValidator.java +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright 2017-2020 original authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.micronaut.validation.validator.constraints; - -import io.micronaut.core.annotation.AnnotationValue; -import io.micronaut.core.annotation.NonNull; -import io.micronaut.core.annotation.Nullable; -import jakarta.inject.Singleton; - -import javax.validation.constraints.Pattern; - -/** - * Validator for the {@link Pattern} annotation. - * - * @author graemerocher - * @since 1.2 - */ -@Singleton -public class PatternValidator extends AbstractPatternValidator { - - @Override - public boolean isValid(@Nullable CharSequence value, @NonNull AnnotationValue annotationMetadata, @NonNull ConstraintValidatorContext context) { - if (value == null) { - // null valid according to spec - return true; - } - java.util.regex.Pattern regex = getPattern(annotationMetadata, false); - return regex.matcher(value).matches(); - } -} diff --git a/validation/src/main/java/io/micronaut/validation/validator/constraints/SizeValidator.java b/validation/src/main/java/io/micronaut/validation/validator/constraints/SizeValidator.java deleted file mode 100644 index 5c3f9b403c7..00000000000 --- a/validation/src/main/java/io/micronaut/validation/validator/constraints/SizeValidator.java +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Copyright 2017-2020 original authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.micronaut.validation.validator.constraints; - -import io.micronaut.core.annotation.AnnotationValue; -import io.micronaut.core.annotation.NonNull; -import io.micronaut.core.annotation.Nullable; - -import javax.validation.constraints.Size; - -/** - * Abstract implementation of a {@link Size} validator. - * @param The type to constrain - * - * @author graemerocher - * @since 1.2 - */ -@FunctionalInterface -public interface SizeValidator extends ConstraintValidator { - @Override - default boolean isValid(@Nullable T value, @NonNull AnnotationValue annotationMetadata, @NonNull ConstraintValidatorContext context) { - if (value == null) { - return true; // null considered valid according to spec - } - final int len = getSize(value); - final int max = annotationMetadata.get("max", Integer.class).orElse(Integer.MAX_VALUE); - final int min = annotationMetadata.get("min", Integer.class).orElse(0); - return len <= max && len >= min; - } - - /** - * Evaluate the size for the given value. - * @param value The value - * @return The size - */ - int getSize(@NonNull T value); -} diff --git a/validation/src/main/java/io/micronaut/validation/validator/extractors/DefaultValueExtractors.java b/validation/src/main/java/io/micronaut/validation/validator/extractors/DefaultValueExtractors.java deleted file mode 100644 index 8b0759b6a14..00000000000 --- a/validation/src/main/java/io/micronaut/validation/validator/extractors/DefaultValueExtractors.java +++ /dev/null @@ -1,322 +0,0 @@ -/* - * Copyright 2017-2020 original authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.micronaut.validation.validator.extractors; - -import io.micronaut.context.BeanContext; -import io.micronaut.context.BeanRegistration; -import io.micronaut.core.annotation.Introspected; -import io.micronaut.core.annotation.NonNull; -import io.micronaut.core.annotation.Nullable; -import io.micronaut.core.beans.BeanProperty; -import io.micronaut.core.beans.BeanWrapper; -import io.micronaut.core.type.Argument; -import io.micronaut.core.util.ArrayUtils; -import io.micronaut.core.util.CollectionUtils; -import jakarta.inject.Inject; -import jakarta.inject.Singleton; - -import javax.validation.valueextraction.UnwrapByDefault; -import javax.validation.valueextraction.ValueExtractor; -import java.util.*; - -/** - * The default value extractors. - * - * @author graemerocher - * @since 1.2 - */ -@Singleton -@Introspected -public class DefaultValueExtractors implements ValueExtractorRegistry { - - public static final String ITERABLE_ELEMENT_NODE_NAME = ""; - public static final String LIST_ELEMENT_NODE_NAME = ""; - public static final String MAP_VALUE_NODE_NAME = ""; - - private final UnwrapByDefaultValueExtractor optionalValueExtractor = - (originalValue, receiver) -> receiver.value(null, originalValue.orElse(null)); - private final UnwrapByDefaultValueExtractor optionalIntValueExtractor = - (originalValue, receiver) -> receiver.value(null, originalValue.isPresent() ? originalValue.getAsInt() : null); - private final UnwrapByDefaultValueExtractor optionalLongValueExtractor = - (originalValue, receiver) -> receiver.value(null, originalValue.isPresent() ? originalValue.getAsLong() : null); - private final UnwrapByDefaultValueExtractor optionalDoubleValueExtractor = - (originalValue, receiver) -> receiver.value(null, originalValue.isPresent() ? originalValue.getAsDouble() : null); - - private final ValueExtractor iterableValueExtractor = (originalValue, receiver) -> { - if (originalValue instanceof List) { - int i = 0; - for (Object o : originalValue) { - receiver.indexedValue(LIST_ELEMENT_NODE_NAME, i++, o); - } - } else { - for (Object o : originalValue) { - receiver.iterableValue(ITERABLE_ELEMENT_NODE_NAME, o); - } - } - }; - private final ValueExtractor> mapValueExtractor = (originalValue, receiver) -> { - for (Map.Entry entry : originalValue.entrySet()) { - receiver.keyedValue(MAP_VALUE_NODE_NAME, entry.getKey(), entry.getValue()); - } - }; - private final ValueExtractor objectArrayValueExtractor = (originalValue, receiver) -> { - for (int i = 0; i < originalValue.length; i++) { - receiver.indexedValue(LIST_ELEMENT_NODE_NAME, i, originalValue[i]); - } - }; - - private final ValueExtractor intArrayValueExtractor = (originalValue, receiver) -> { - for (int i = 0; i < originalValue.length; i++) { - receiver.indexedValue(LIST_ELEMENT_NODE_NAME, i, originalValue[i]); - } - }; - - private final ValueExtractor byteArrayValueExtractor = (originalValue, receiver) -> { - for (int i = 0; i < originalValue.length; i++) { - receiver.indexedValue(LIST_ELEMENT_NODE_NAME, i, originalValue[i]); - } - }; - - private final ValueExtractor booleanArrayValueExtractor = (originalValue, receiver) -> { - for (int i = 0; i < originalValue.length; i++) { - receiver.indexedValue(LIST_ELEMENT_NODE_NAME, i, originalValue[i]); - } - }; - - private final ValueExtractor doubleArrayValueExtractor = (originalValue, receiver) -> { - for (int i = 0; i < originalValue.length; i++) { - receiver.indexedValue(LIST_ELEMENT_NODE_NAME, i, originalValue[i]); - } - }; - - private final ValueExtractor charArrayValueExtractor = (originalValue, receiver) -> { - for (int i = 0; i < originalValue.length; i++) { - receiver.indexedValue(LIST_ELEMENT_NODE_NAME, i, originalValue[i]); - } - }; - - private final ValueExtractor floatArrayValueExtractor = (originalValue, receiver) -> { - for (int i = 0; i < originalValue.length; i++) { - receiver.indexedValue(LIST_ELEMENT_NODE_NAME, i, originalValue[i]); - } - }; - - private final ValueExtractor shortArrayValueExtractor = (originalValue, receiver) -> { - for (int i = 0; i < originalValue.length; i++) { - receiver.indexedValue(LIST_ELEMENT_NODE_NAME, i, originalValue[i]); - } - }; - - private final Map, ValueExtractor> valueExtractors; - private final Set> unwrapByDefaultTypes = new HashSet<>(5); - - /** - * Default constructor. - */ - public DefaultValueExtractors() { - this(null); - } - - /** - * Constructor used during DI. - * - * @param beanContext The bean context - */ - @Inject - protected DefaultValueExtractors(@Nullable BeanContext beanContext) { - BeanWrapper wrapper = BeanWrapper.findWrapper(this).orElse(null); - Map, ValueExtractor> extractorMap = new HashMap<>(); - - if (beanContext != null && beanContext.containsBean(ValueExtractor.class)) { - final Collection> valueExtractors = - beanContext.getBeanRegistrations(ValueExtractor.class); - if (CollectionUtils.isNotEmpty(valueExtractors)) { - for (BeanRegistration reg : valueExtractors) { - final ValueExtractor valueExtractor = reg.getBean(); - final Class[] typeParameters = reg.getBeanDefinition().getTypeParameters(ValueExtractor.class); - if (ArrayUtils.isNotEmpty(typeParameters)) { - final Class targetType = typeParameters[0]; - extractorMap.put(targetType, valueExtractor); - if (valueExtractor instanceof UnwrapByDefaultValueExtractor || valueExtractor.getClass().isAnnotationPresent(UnwrapByDefault.class)) { - unwrapByDefaultTypes.add(targetType); - } - } - } - } - } - - if (wrapper != null) { - - final Collection> properties = wrapper.getBeanProperties(); - for (BeanProperty property : properties) { - if (ValueExtractor.class.isAssignableFrom(property.getType())) { - final ValueExtractor valueExtractor = wrapper - .getProperty(property.getName(), ValueExtractor.class).orElse(null); - final Class targetType = property.asArgument().getFirstTypeVariable().map(Argument::getType).orElse(null); - extractorMap.put(targetType, valueExtractor); - if (valueExtractor instanceof UnwrapByDefaultValueExtractor || valueExtractor.getClass().isAnnotationPresent(UnwrapByDefault.class)) { - unwrapByDefaultTypes.add(targetType); - } - } - } - this.valueExtractors = new HashMap<>(extractorMap.size()); - this.valueExtractors.putAll(extractorMap); - } else { - this.valueExtractors = Collections.emptyMap(); - } - } - - /** - * Value extractor for optional. - * - * @return The value extractor. - */ - public UnwrapByDefaultValueExtractor getOptionalValueExtractor() { - return optionalValueExtractor; - } - - /** - * Value extractor for {@link OptionalInt}. - * - * @return The value extractor - */ - public UnwrapByDefaultValueExtractor getOptionalIntValueExtractor() { - return optionalIntValueExtractor; - } - - /** - * Value extractor for {@link OptionalLong}. - * - * @return The value extractor - */ - public UnwrapByDefaultValueExtractor getOptionalLongValueExtractor() { - return optionalLongValueExtractor; - } - - /** - * Value extractor for {@link OptionalDouble}. - * - * @return The value extractor - */ - public UnwrapByDefaultValueExtractor getOptionalDoubleValueExtractor() { - return optionalDoubleValueExtractor; - } - - /** - * Value extractor for iterable. - * @return The value extractor - */ - public ValueExtractor getIterableValueExtractor() { - return iterableValueExtractor; - } - - /** - * Value extractor for iterable. - * @return The value extractor - */ - public ValueExtractor> getMapValueExtractor() { - return mapValueExtractor; - } - - /** - * Value extractor for Object[]. - * @return The object[] extractor - */ - public ValueExtractor getObjectArrayValueExtractor() { - return objectArrayValueExtractor; - } - - /** - * Value extractor for int[]. - * @return The int[] extractor - */ - public ValueExtractor getIntArrayValueExtractor() { - return intArrayValueExtractor; - } - - /** - * Value extractor for byte[]. - * @return The byte[] extractor - */ - public ValueExtractor getByteArrayValueExtractor() { - return byteArrayValueExtractor; - } - - /** - * Value extractor for char[]. - * @return The char[] extractor - */ - public ValueExtractor getCharArrayValueExtractor() { - return charArrayValueExtractor; - } - - /** - * Value extractor for boolean[]. - * @return The boolean[] extractor - */ - public ValueExtractor getBooleanArrayValueExtractor() { - return booleanArrayValueExtractor; - } - - /** - * Value extractor for double[]. - * @return The double[] extractor - */ - public ValueExtractor getDoubleArrayValueExtractor() { - return doubleArrayValueExtractor; - } - - /** - * Value extractor for float[]. - * @return The float[] extractor - */ - public ValueExtractor getFloatArrayValueExtractor() { - return floatArrayValueExtractor; - } - - /** - * Value extractor for short[]. - * @return The short[] extractor - */ - public ValueExtractor getShortArrayValueExtractor() { - return shortArrayValueExtractor; - } - - @SuppressWarnings("unchecked") - @NonNull - @Override - public Optional> findValueExtractor(@NonNull Class targetType) { - final ValueExtractor valueExtractor = valueExtractors.get(targetType); - if (valueExtractor != null) { - return Optional.of(valueExtractor); - } else { - return (Optional) valueExtractors.entrySet().stream() - .filter(entry -> entry.getKey().isAssignableFrom(targetType)) - .map(Map.Entry::getValue) - .findFirst(); - } - } - - @SuppressWarnings("unchecked") - @NonNull - @Override - public Optional> findUnwrapValueExtractor(@NonNull Class targetType) { - if (unwrapByDefaultTypes.contains(targetType)) { - return Optional.ofNullable(valueExtractors.get(targetType)); - } - return Optional.empty(); - } -} diff --git a/validation/src/main/java/io/micronaut/validation/validator/extractors/SimpleValueReceiver.java b/validation/src/main/java/io/micronaut/validation/validator/extractors/SimpleValueReceiver.java deleted file mode 100644 index 11fd107b58f..00000000000 --- a/validation/src/main/java/io/micronaut/validation/validator/extractors/SimpleValueReceiver.java +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright 2017-2020 original authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.micronaut.validation.validator.extractors; - -import javax.validation.valueextraction.ValueExtractor; - -/** - * No-op implementation that makes it easier to use with Lambdas. - * - * @author graemerocher - * @since 1.2 - */ -@FunctionalInterface -public interface SimpleValueReceiver extends ValueExtractor.ValueReceiver { - @Override - default void iterableValue(String nodeName, Object object) { - - } - - @Override - default void indexedValue(String nodeName, int i, Object object) { - - } - - @Override - default void keyedValue(String nodeName, Object key, Object object) { - - } -} diff --git a/validation/src/main/java/io/micronaut/validation/validator/extractors/UnwrapByDefaultValueExtractor.java b/validation/src/main/java/io/micronaut/validation/validator/extractors/UnwrapByDefaultValueExtractor.java deleted file mode 100644 index a17ad5a4741..00000000000 --- a/validation/src/main/java/io/micronaut/validation/validator/extractors/UnwrapByDefaultValueExtractor.java +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright 2017-2020 original authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.micronaut.validation.validator.extractors; - -import javax.validation.valueextraction.ValueExtractor; - -/** - * Interface based alternative for unwrap by default semantics. - * - * @author graemerocher - * @since 1.2 - * @param the extracted type - */ -@SuppressWarnings("WeakerAccess") -public interface UnwrapByDefaultValueExtractor extends ValueExtractor { -} diff --git a/validation/src/main/java/io/micronaut/validation/validator/extractors/ValueExtractorRegistry.java b/validation/src/main/java/io/micronaut/validation/validator/extractors/ValueExtractorRegistry.java deleted file mode 100644 index 0a8325e030f..00000000000 --- a/validation/src/main/java/io/micronaut/validation/validator/extractors/ValueExtractorRegistry.java +++ /dev/null @@ -1,64 +0,0 @@ -/* - * Copyright 2017-2020 original authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.micronaut.validation.validator.extractors; - -import io.micronaut.core.annotation.NonNull; - -import javax.validation.ValidationException; -import javax.validation.valueextraction.ValueExtractor; -import java.util.Optional; - -/** - * Registry of value extractors. - * - * @author graemerocher - * @since 1.2 - */ -public interface ValueExtractorRegistry { - /** - * Finds a a {@link ValueExtractor} for the given type. - * @param targetType The target type of the value - * @param The target type - * @return The extractor - */ - @NonNull - Optional> findValueExtractor( - @NonNull Class targetType); - - /** - * Finds a concrete {@link ValueExtractor} without searching the hierarchy. - * @param targetType The target type of the value - * @param The target type - * @return The extractor - */ - @NonNull - Optional> findUnwrapValueExtractor( - @NonNull Class targetType); - - /** - * Gets a a {@link ValueExtractor} for the given type. - * @param targetType The target type of the value - * @param The target type - * @return The extractor - * @throws ValidationException if no extractor is present - */ - @NonNull - default ValueExtractor getValueExtractor( - @NonNull Class targetType) { - return findValueExtractor(targetType) - .orElseThrow(() -> new ValidationException("No value extractor for target type [" + targetType + "]")); - } -} diff --git a/validation/src/main/java/io/micronaut/validation/validator/messages/DefaultValidationMessages.java b/validation/src/main/java/io/micronaut/validation/validator/messages/DefaultValidationMessages.java deleted file mode 100644 index a33b8d09daf..00000000000 --- a/validation/src/main/java/io/micronaut/validation/validator/messages/DefaultValidationMessages.java +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Copyright 2017-2020 original authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.micronaut.validation.validator.messages; - -import io.micronaut.context.StaticMessageSource; -import io.micronaut.core.annotation.Introspected; -import jakarta.inject.Singleton; - -import javax.validation.constraints.*; - -/** - * The default error messages. - * - * @author graemerocher - * @since 1.2 - */ -@Singleton -public class DefaultValidationMessages extends StaticMessageSource { - - /** - * The message suffix to use. - */ - private static final String MESSAGE_SUFFIX = ".message"; - - /** - * Constructs the default error messages. - */ - public DefaultValidationMessages() { - addMessage(AssertTrue.class.getName() + MESSAGE_SUFFIX, "must be true"); - addMessage(AssertFalse.class.getName() + MESSAGE_SUFFIX, "must be false"); - addMessage(DecimalMax.class.getName() + MESSAGE_SUFFIX, "must be less than or equal to {value}"); - addMessage(DecimalMin.class.getName() + MESSAGE_SUFFIX, "must be greater than or equal to {value}"); - addMessage(Digits.class.getName() + MESSAGE_SUFFIX, "numeric value out of bounds (<{integer} digits>.<{fraction} digits> expected)"); - addMessage(Email.class.getName() + MESSAGE_SUFFIX, "must be a well-formed email address"); - addMessage(Future.class.getName() + MESSAGE_SUFFIX, "must be a future date"); - addMessage(FutureOrPresent.class.getName() + MESSAGE_SUFFIX, "must be a date in the present or in the future"); - addMessage(Max.class.getName() + MESSAGE_SUFFIX, "must be less than or equal to {value}"); - addMessage(Min.class.getName() + MESSAGE_SUFFIX, "must be greater than or equal to {value}"); - addMessage(Negative.class.getName() + MESSAGE_SUFFIX, "must be less than 0"); - addMessage(NegativeOrZero.class.getName() + MESSAGE_SUFFIX, "must be less than or equal to 0"); - addMessage(NotBlank.class.getName() + MESSAGE_SUFFIX, "must not be blank"); - addMessage(NotEmpty.class.getName() + MESSAGE_SUFFIX, "must not be empty"); - addMessage(NotNull.class.getName() + MESSAGE_SUFFIX, "must not be null"); - addMessage(Null.class.getName() + MESSAGE_SUFFIX, "must be null"); - addMessage(Past.class.getName() + MESSAGE_SUFFIX, "must be a past date"); - addMessage(PastOrPresent.class.getName() + MESSAGE_SUFFIX, "must be a date in the past or in the present"); - addMessage(Pattern.class.getName() + MESSAGE_SUFFIX, "must match \"{regexp}\""); - - addMessage(Positive.class.getName() + MESSAGE_SUFFIX, "must be greater than 0"); - addMessage(PositiveOrZero.class.getName() + MESSAGE_SUFFIX, "must be greater than or equal to 0"); - addMessage(Size.class.getName() + MESSAGE_SUFFIX, "size must be between {min} and {max}"); - - addMessage(Introspected.class.getName() + MESSAGE_SUFFIX, "Cannot validate {type}. No bean introspection present. Please add @Introspected to the class and ensure Micronaut annotation processing is enabled"); - } -} diff --git a/validation/src/main/java/io/micronaut/validation/validator/resolver/CompositeTraversableResolver.java b/validation/src/main/java/io/micronaut/validation/validator/resolver/CompositeTraversableResolver.java deleted file mode 100644 index 7a04ebfae4f..00000000000 --- a/validation/src/main/java/io/micronaut/validation/validator/resolver/CompositeTraversableResolver.java +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Copyright 2017-2020 original authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.micronaut.validation.validator.resolver; - -import io.micronaut.context.annotation.Primary; -import io.micronaut.core.annotation.Internal; -import io.micronaut.core.util.CollectionUtils; -import jakarta.inject.Singleton; - -import javax.validation.Path; -import javax.validation.TraversableResolver; -import java.lang.annotation.ElementType; -import java.util.List; - -/** - * Primary {@link TraversableResolver} that takes into account all configured {@link TraversableResolver} instances. - * - * @author graemerocher - * @since 1.2.0 - */ -@Primary -@Singleton -@Internal -public class CompositeTraversableResolver implements TraversableResolver { - - private final List traversableResolvers; - - /** - * Default constructor. - * @param traversableResolvers The traversable resolvers - */ - public CompositeTraversableResolver(List traversableResolvers) { - this.traversableResolvers = CollectionUtils.isEmpty(traversableResolvers) ? null : traversableResolvers; - } - - @Override - public boolean isReachable(Object traversableObject, Path.Node traversableProperty, Class rootBeanType, Path pathToTraversableObject, ElementType elementType) { - if (traversableResolvers == null) { - return true; - } - return traversableResolvers.stream().allMatch(r -> - r.isReachable(traversableObject, traversableProperty, rootBeanType, pathToTraversableObject, elementType) - ); - } - - @Override - public boolean isCascadable(Object traversableObject, Path.Node traversableProperty, Class rootBeanType, Path pathToTraversableObject, ElementType elementType) { - if (traversableResolvers == null) { - return true; - } - return traversableResolvers.stream().allMatch(r -> - r.isCascadable(traversableObject, traversableProperty, rootBeanType, pathToTraversableObject, elementType) - ); - } -} diff --git a/validation/src/main/resources/META-INF/services/io.micronaut.inject.annotation.AnnotatedElementValidator b/validation/src/main/resources/META-INF/services/io.micronaut.inject.annotation.AnnotatedElementValidator deleted file mode 100644 index 3ba3f563c35..00000000000 --- a/validation/src/main/resources/META-INF/services/io.micronaut.inject.annotation.AnnotatedElementValidator +++ /dev/null @@ -1 +0,0 @@ -io.micronaut.validation.validator.DefaultAnnotatedElementValidator \ No newline at end of file diff --git a/validation/src/test/groovy/io/micronaut/validation/BookmarkController.java b/validation/src/test/groovy/io/micronaut/validation/BookmarkController.java deleted file mode 100644 index ac4acc45e96..00000000000 --- a/validation/src/test/groovy/io/micronaut/validation/BookmarkController.java +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright 2017-2020 original authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.micronaut.validation; - -import io.micronaut.core.annotation.Nullable; -import io.micronaut.http.HttpStatus; -import io.micronaut.http.annotation.Controller; -import io.micronaut.http.annotation.Get; - -import javax.validation.Valid; -import javax.validation.constraints.Pattern; -import javax.validation.constraints.Positive; -import javax.validation.constraints.PositiveOrZero; - -@Controller("/api") -public class BookmarkController { - - @Get("/bookmarks{?offset,max,sort,order}") - public HttpStatus index(@PositiveOrZero @Nullable Integer offset, - @Positive @Nullable Integer max, - @Nullable @Pattern(regexp = "name|href|title") String sort, - @Nullable @Pattern(regexp = "asc|desc|ASC|DESC") String order) { - return HttpStatus.OK; - } - - @Get("/bookmarks/list{?paginationCommand*}") - public PaginationCommand list(@Valid @Nullable PaginationCommand paginationCommand) { - return paginationCommand; - } - -} diff --git a/validation/src/test/groovy/io/micronaut/validation/BookmarkControllerSpec.groovy b/validation/src/test/groovy/io/micronaut/validation/BookmarkControllerSpec.groovy deleted file mode 100644 index e73cf6730c9..00000000000 --- a/validation/src/test/groovy/io/micronaut/validation/BookmarkControllerSpec.groovy +++ /dev/null @@ -1,128 +0,0 @@ -/* - * Copyright 2017-2019 original authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.micronaut.validation - -import io.micronaut.context.ApplicationContext -import io.micronaut.http.HttpRequest -import io.micronaut.http.HttpResponse -import io.micronaut.http.HttpStatus -import io.micronaut.http.client.HttpClient -import io.micronaut.http.client.exceptions.HttpClientResponseException -import io.micronaut.http.uri.UriTemplate -import io.micronaut.runtime.server.EmbeddedServer -import spock.lang.AutoCleanup -import spock.lang.Shared -import spock.lang.Specification - -class BookmarkControllerSpec extends Specification { - - @Shared @AutoCleanup EmbeddedServer embeddedServer = ApplicationContext.run(EmbeddedServer) - @Shared @AutoCleanup HttpClient client = embeddedServer.applicationContext.createBean(HttpClient, embeddedServer.getURL()) - - - void "test parameters binding"() { - when: - UriTemplate template = new UriTemplate("/api/bookmarks{?offset,max,sort,order}") - String uri = template.expand([offset: -1]) - - then: - uri == '/api/bookmarks?offset=-1' - - when: - HttpRequest request = HttpRequest.GET(uri) - client.toBlocking().exchange(request) - - then: - def e = thrown(HttpClientResponseException) - e.response.status == HttpStatus.BAD_REQUEST - } - - void "test parameters binding allows nullable"() { - when: - UriTemplate template = new UriTemplate("/api/bookmarks{?offset,max,sort,order}") - String uri = template.expand([:]) - - then: - uri == '/api/bookmarks' - - when: - HttpRequest request = HttpRequest.GET(uri) - HttpResponse rsp = client.toBlocking().exchange(request) - - then: - rsp.status == HttpStatus.OK - } - - void "test POJO binding allows nullable"() { - when: - UriTemplate template = new UriTemplate("/api/bookmarks/list{?offset,max,sort,order}") - String uri = template.expand([:]) - - then: - uri == '/api/bookmarks/list' - - when: - HttpRequest request = HttpRequest.GET(uri) - HttpResponse rsp = client.toBlocking().exchange(request) - - then: - rsp.status == HttpStatus.OK - } - - void "test POJO binding"() { - when: - UriTemplate template = new UriTemplate("/api/bookmarks/list{?offset,max,sort,order}") - String uri = template.expand([offset: -1, max: 10]) - - then: - uri == '/api/bookmarks/list?offset=-1&max=10' - - when: - HttpRequest request = HttpRequest.GET(uri) - client.toBlocking().exchange(request) - - then: - def e = thrown(HttpClientResponseException) - e.response.status == HttpStatus.BAD_REQUEST - } - - void "test POJO binding with valid values"() { - when: - UriTemplate template = new UriTemplate("/api/bookmarks/list{?sort,order,max,offset,ids*}") - String uri = template.expand([sort: 'href', order: 'desc', max: 2, offset: 0, ids: [1]]) // , 2 - - then: - uri == '/api/bookmarks/list?sort=href&order=desc&max=2&offset=0&ids=1' - - when: - HttpRequest request = HttpRequest.GET(uri) - HttpResponse rsp = client.toBlocking().exchange(request, Map) - - then: - noExceptionThrown() - rsp.status() == HttpStatus.OK - - when: - Map m = rsp.body() - - then: - m.offset == 0 - m.max == 2 - m.order == 'desc' - m.sort == 'href' - m.ids == [1] - } -} diff --git a/validation/src/test/groovy/io/micronaut/validation/Foo.java b/validation/src/test/groovy/io/micronaut/validation/Foo.java deleted file mode 100644 index 26fe3bd1465..00000000000 --- a/validation/src/test/groovy/io/micronaut/validation/Foo.java +++ /dev/null @@ -1,80 +0,0 @@ -/* - * Copyright 2017-2020 original authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.micronaut.validation; - -import groovy.lang.Singleton; -import io.micronaut.core.annotation.Introspected; - -import javax.validation.Valid; -import javax.validation.constraints.Digits; -import javax.validation.constraints.NotNull; -import java.util.Collections; -import java.util.List; -import java.util.Map; - -/** - * @author Graeme Rocher - * @since 1.0 - */ -@Singleton // Groovy @Singleton!!! -@Validated -public class Foo { - - public String testMe(@Digits(integer = 3, fraction = 2) String number) { - return '$' + number; - } - - @NotNull - public String notNull() { - return null; - } - - @NotNull - public Bar notNullBar() { - return null; - } - - @Valid - public Bar cascadeValidateReturnValue() { - return new Bar(); - } - - @Valid - public List validateReturnList() { - return Collections.singletonList(new Bar()); - } - - @Valid - public Map validateMap() { - return Collections.singletonMap("barObj", new Bar()); - } -} - -@Introspected -class Bar { - - @NotNull - private String prop; - - @NotNull - public String getProp() { - return prop; - } - - public void setProp(@NotNull String prop) { - this.prop = prop; - } -} diff --git a/validation/src/test/groovy/io/micronaut/validation/JavaClient.java b/validation/src/test/groovy/io/micronaut/validation/JavaClient.java deleted file mode 100644 index 1df62749de5..00000000000 --- a/validation/src/test/groovy/io/micronaut/validation/JavaClient.java +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright 2017-2020 original authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.micronaut.validation; - -import io.micronaut.http.annotation.Get; -import io.micronaut.http.annotation.Header; -import io.micronaut.http.client.annotation.Client; - -import javax.validation.constraints.NotBlank; - -@Client("/validated/tests") -interface JavaClient { - - @Get(value = "/validated") - String test5(@NotBlank @Header String header); -} diff --git a/validation/src/test/groovy/io/micronaut/validation/PaginationCommand.java b/validation/src/test/groovy/io/micronaut/validation/PaginationCommand.java deleted file mode 100644 index f6e1d3beed5..00000000000 --- a/validation/src/test/groovy/io/micronaut/validation/PaginationCommand.java +++ /dev/null @@ -1,90 +0,0 @@ -/* - * Copyright 2017-2020 original authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.micronaut.validation; - -import io.micronaut.core.annotation.Introspected; -import io.micronaut.core.annotation.Nullable; - -import javax.validation.constraints.Pattern; -import javax.validation.constraints.Positive; -import javax.validation.constraints.PositiveOrZero; -import java.util.List; - -@Introspected -public class PaginationCommand { - - @PositiveOrZero - @Nullable - private Integer offset; - - @Positive - @Nullable - private Integer max; - - @Nullable - @Pattern(regexp = "name|href|title") - private String sort; - - @Nullable - @Pattern(regexp = "asc|desc|ASC|DESC") - private String order; - - @Nullable - private List ids; - - @Nullable - public Integer getOffset() { - return offset; - } - - public void setOffset(@Nullable Integer offset) { - this.offset = offset; - } - - @Nullable - public Integer getMax() { - return max; - } - - public void setMax(@Nullable Integer max) { - this.max = max; - } - - @Nullable - public String getSort() { - return sort; - } - - public void setSort(@Nullable String sort) { - this.sort = sort; - } - - @Nullable - public String getOrder() { - return order; - } - - public void setOrder(@Nullable String order) { - this.order = order; - } - - @Nullable - public List getIds() { return ids; } - - public void setIds(@Nullable List ids) { - this.ids = ids; - } -} diff --git a/validation/src/test/groovy/io/micronaut/validation/Pojo.java b/validation/src/test/groovy/io/micronaut/validation/Pojo.java deleted file mode 100644 index 5701bff92b0..00000000000 --- a/validation/src/test/groovy/io/micronaut/validation/Pojo.java +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright 2017-2020 original authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.micronaut.validation; - -import io.micronaut.core.annotation.Introspected; - -import javax.validation.constraints.Email; -import javax.validation.constraints.NotBlank; - -@Introspected -public class Pojo { - - @Email(message = "Email should be valid") - private String email; - - @NotBlank - private String name; - - public String getEmail() { - return email; - } - - public void setEmail(String email) { - this.email = email; - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } -} diff --git a/validation/src/test/groovy/io/micronaut/validation/PojoNoIntrospection.java b/validation/src/test/groovy/io/micronaut/validation/PojoNoIntrospection.java deleted file mode 100644 index 565bce97b94..00000000000 --- a/validation/src/test/groovy/io/micronaut/validation/PojoNoIntrospection.java +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright 2017-2020 original authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.micronaut.validation; - -import javax.validation.constraints.Email; -import javax.validation.constraints.NotBlank; - -public class PojoNoIntrospection { - - @Email(message = "Email should be valid") - private String email; - - @NotBlank - private String name; - - public String getEmail() { - return email; - } - - public void setEmail(String email) { - this.email = email; - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } -} diff --git a/validation/src/test/groovy/io/micronaut/validation/ValidatedController.java b/validation/src/test/groovy/io/micronaut/validation/ValidatedController.java deleted file mode 100644 index 2907d4ea473..00000000000 --- a/validation/src/test/groovy/io/micronaut/validation/ValidatedController.java +++ /dev/null @@ -1,67 +0,0 @@ -/* - * Copyright 2017-2020 original authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.micronaut.validation; - -import io.micronaut.http.annotation.Body; -import io.micronaut.http.annotation.Controller; -import io.micronaut.http.annotation.Get; -import io.micronaut.http.annotation.Post; -import io.micronaut.http.annotation.QueryValue; - -import javax.validation.Valid; -import javax.validation.constraints.Digits; -import javax.validation.constraints.Min; -import javax.validation.constraints.NotNull; -import java.util.List; -import java.util.Optional; - -/** - * @author Graeme Rocher - * @since 1.0 - */ -@Controller("/validated") -public class ValidatedController { - - @Post("/args") - public String args(@Digits(integer = 3, fraction = 2) String amount) { - return "$" + amount; - } - - @Post("/pojo") - public Pojo pojo(@Body @Valid Pojo pojo) { - return pojo; - } - - @Post("/pojos") - public List pojos(@Body @Valid List pojos) { - return pojos; - } - - @Post("/no-introspection") - public PojoNoIntrospection pojo(@Body @Valid PojoNoIntrospection pojo) { - return pojo; - } - - @Get("/optional") - public boolean optional(@QueryValue @Min(1) Optional limit) { - return limit.map(l -> l >= 1).orElse(true); - } - - @Get("/optional/notNull") - public boolean optionalNotNull(@QueryValue @NotNull Optional limit) { - return limit.map(l -> l >= 1).orElse(true); - } -} diff --git a/validation/src/test/groovy/io/micronaut/validation/ValidatedParseSpec.groovy b/validation/src/test/groovy/io/micronaut/validation/ValidatedParseSpec.groovy deleted file mode 100644 index ec5a1d0dbed..00000000000 --- a/validation/src/test/groovy/io/micronaut/validation/ValidatedParseSpec.groovy +++ /dev/null @@ -1,119 +0,0 @@ -package io.micronaut.validation - -import io.micronaut.annotation.processing.test.AbstractTypeElementSpec -import io.micronaut.aop.Around -import io.micronaut.inject.ProxyBeanDefinition -import io.micronaut.inject.writer.BeanDefinitionVisitor -import io.micronaut.inject.writer.BeanDefinitionWriter -import spock.lang.PendingFeature - -import java.time.LocalDate - -class ValidatedParseSpec extends AbstractTypeElementSpec { - void "test constraints on beans make them @Validated"() { - given: - def definition = buildBeanDefinition('test.$Test' + BeanDefinitionWriter.CLASS_SUFFIX + BeanDefinitionVisitor.PROXY_SUFFIX,''' -package test; - -@jakarta.inject.Singleton -class Test { - - @io.micronaut.context.annotation.Executable - public void setName(@javax.validation.constraints.NotBlank String name) { - - } - - @io.micronaut.context.annotation.Executable - public void setName2(@javax.validation.Valid String name) { - - } -} -''') - - expect: - definition instanceof ProxyBeanDefinition - definition.findMethod("setName", String).get().hasStereotype(Validated) - definition.findMethod("setName2", String).get().getAnnotationTypesByStereotype(Around).contains(Validated) - } - - void "test constraints on a declarative client makes it @Validated"() { - given: - def definition = buildBeanDefinition('test.ExchangeRates' + BeanDefinitionVisitor.PROXY_SUFFIX,''' -package test; - -import io.micronaut.http.annotation.Get; -import io.micronaut.http.client.annotation.Client; - -import javax.validation.constraints.PastOrPresent; -import java.time.LocalDate; - -@Client("https://exchangeratesapi.io") -interface ExchangeRates { - - @Get("{date}") - String rate(@PastOrPresent LocalDate date); -} -''') - - expect: - definition.findMethod("rate", LocalDate).get().hasStereotype(Validated) - } - - - void "test a default method constraints on a declarative client makes it @Validated"() { - given: - def definition = buildBeanDefinition('validateparse3.ExchangeRates' + BeanDefinitionVisitor.PROXY_SUFFIX,''' -package validateparse3; - -import io.micronaut.http.annotation.Get; -import io.micronaut.http.client.annotation.Client; - -import javax.validation.constraints.PastOrPresent; -import java.time.LocalDate; - -@Client("https://exchangeratesapi.io") -interface ExchangeRates { - - @Get("{date}") - String rate(@PastOrPresent LocalDate date); - - default String rate2(@PastOrPresent LocalDate date) { - return null; - } -} -''') - - expect: - definition.findMethod("rate", LocalDate).get().hasStereotype(Validated) - definition.findMethod("rate2", LocalDate).get().hasStereotype(Validated) - } - - @PendingFeature - void "test a default method only constraints on a declarative client makes it @Validated"() { - given: - def definition = buildBeanDefinition('validateparse3.ExchangeRates' + BeanDefinitionVisitor.PROXY_SUFFIX,''' -package validateparse3; - -import io.micronaut.http.annotation.Get; -import io.micronaut.http.client.annotation.Client; - -import javax.validation.constraints.PastOrPresent; -import java.time.LocalDate; - -@Client("https://exchangeratesapi.io") -interface ExchangeRates { - - @Get("{date}") - String rate(LocalDate date); - - default String rate2(@PastOrPresent LocalDate date) { - return rate(date); - } -} -''') - - expect: - !definition.findMethod("rate", LocalDate).get().hasStereotype(Validated) - definition.findMethod("rate2", LocalDate).get().hasStereotype(Validated) - } -} diff --git a/validation/src/test/groovy/io/micronaut/validation/ValidatedSpec.groovy b/validation/src/test/groovy/io/micronaut/validation/ValidatedSpec.groovy deleted file mode 100644 index 87bb6894f35..00000000000 --- a/validation/src/test/groovy/io/micronaut/validation/ValidatedSpec.groovy +++ /dev/null @@ -1,770 +0,0 @@ -/* - * Copyright 2017-2019 original authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.micronaut.validation - -import groovy.json.JsonSlurper -import io.micronaut.aop.InterceptPhase -import io.micronaut.aop.InvocationContext -import io.micronaut.aop.MethodInterceptor -import io.micronaut.aop.MethodInvocationContext -import io.micronaut.context.ApplicationContext -import io.micronaut.context.annotation.ConfigurationProperties -import io.micronaut.context.exceptions.BeanInstantiationException -import io.micronaut.core.annotation.Nullable -import io.micronaut.core.convert.ConversionService -import io.micronaut.core.order.OrderUtil -import io.micronaut.core.type.Argument -import io.micronaut.http.HttpRequest -import io.micronaut.http.HttpResponse -import io.micronaut.http.HttpStatus -import io.micronaut.http.MediaType -import io.micronaut.http.annotation.Consumes -import io.micronaut.http.annotation.Controller -import io.micronaut.http.annotation.Get -import io.micronaut.http.annotation.Header -import io.micronaut.http.client.HttpClient -import io.micronaut.http.client.annotation.Client -import io.micronaut.http.client.exceptions.HttpClientResponseException -import io.micronaut.runtime.server.EmbeddedServer -import reactor.core.publisher.Flux -import spock.lang.Specification - -import javax.validation.ConstraintViolationException -import javax.validation.constraints.* - -/** - * @author Graeme Rocher - * @since 1.0 - */ -class ValidatedSpec extends Specification { - - def "test order"() { - given: - def list = [new MethodInterceptor() { - - @Override - int getOrder() { - return InterceptPhase.CACHE.getPosition() - } - - @Nullable - @Override - Object intercept(MethodInvocationContext context) { - return null - } - - @Override - @Nullable - Object intercept(InvocationContext context) { - return null - } - }, new ValidatingInterceptor(null, null, ConversionService.SHARED)] - OrderUtil.sort(list) - - expect: - list[0] instanceof ValidatingInterceptor - list[1] instanceof MethodInterceptor - } - - def "test validated annotation validates beans"() { - given: - ApplicationContext beanContext = ApplicationContext.run() - Foo foo = beanContext.getBean(Foo) - - when: "invalid data is passed" - - foo.testMe("aaa") - - then: - def e = thrown(ConstraintViolationException) - e.message == 'testMe.number: numeric value out of bounds (<3 digits>.<2 digits> expected)' - - when: "valid data is passed" - def result = foo.testMe("100.00") - - then: - result == "\$100.00" - - cleanup: - beanContext.close() - } - - def "test validated return value"() { - given: - ApplicationContext beanContext = ApplicationContext.run() - Foo foo = beanContext.getBean(Foo) - - when: - foo.notNull() - - then: - def e = thrown(ConstraintViolationException) - e.message == "string: must not be null" - - cleanup: - beanContext.close() - } - - def "test validated return value without cascade"() { - given: - ApplicationContext beanContext = ApplicationContext.run() - Foo foo = beanContext.getBean(Foo) - - when: - foo.notNullBar() - - then: - def e = thrown(ConstraintViolationException) - e.message == "bar: must not be null" - - cleanup: - beanContext.close() - } - - def "test validate return value with cascading"() { - given: - ApplicationContext beanContext = ApplicationContext.run() - Foo foo = beanContext.getBean(Foo) - - when: - foo.cascadeValidateReturnValue() - - then: - def e = thrown(ConstraintViolationException) - e.message == "bar.prop: must not be null" - - cleanup: - beanContext.close() - } - - def "test validate list return value with cascading"() { - given: - ApplicationContext beanContext = ApplicationContext.run() - Foo foo = beanContext.getBean(Foo) - - when: - foo.validateReturnList() - - then: - def e = thrown(ConstraintViolationException) - e.message == "list[0].prop: must not be null" - - cleanup: - beanContext.close() - } - - def "test validate map return value with cascading"() { - given: - ApplicationContext beanContext = ApplicationContext.run() - Foo foo = beanContext.getBean(Foo) - - when: - foo.validateMap() - - then: - def e = thrown(ConstraintViolationException) - e.message == "map[barObj].prop: must not be null" - - cleanup: - beanContext.close() - } - - def "test validated controller validates @Valid classes"() { - given: - ApplicationContext context = ApplicationContext.run([ - 'spec.name': getClass().simpleName - ]) - EmbeddedServer embeddedServer = context.getBean(EmbeddedServer).start() - HttpClient client = context.createBean(HttpClient, embeddedServer.getURL()) - EmbeddedServer server = ApplicationContext.run(EmbeddedServer) - - when: - HttpResponse response = client.toBlocking().exchange( - HttpRequest.POST("/validated/pojo", '{"email":"abc","name":"Micronaut"}') - .contentType(MediaType.APPLICATION_JSON_TYPE), - String - ) - - then: - def e = thrown(HttpClientResponseException) - e.response.code() == HttpStatus.BAD_REQUEST.code - - when: - def result = new JsonSlurper().parseText((String) e.response.getBody().get()) - - then: - result._embedded.errors[0].message == 'pojo.email: Email should be valid' - - cleanup: - server.close() - } - - def "test validated controller validates @Valid classes with standard embedded errors"() { - given: - ApplicationContext context = ApplicationContext.run([ - 'spec.name': getClass().simpleName - ]) - EmbeddedServer embeddedServer = context.getBean(EmbeddedServer).start() - HttpClient client = context.createBean(HttpClient, embeddedServer.getURL()) - EmbeddedServer server = ApplicationContext.run(EmbeddedServer) - - when: - HttpResponse response = client.toBlocking().exchange( - HttpRequest.POST("/validated/pojo", '{"email":"abc","name":"Micronaut"}') - .contentType(MediaType.APPLICATION_JSON_TYPE), - String - ) - - then: - def e = thrown(HttpClientResponseException) - e.response.code() == HttpStatus.BAD_REQUEST.code - - when: - def result = new JsonSlurper().parseText((String) e.response.getBody().get()) - - then: - result.message == 'Bad Request' - result._embedded.errors.size() == 1 - result._embedded.errors.find { it.message == 'pojo.email: Email should be valid' } - - cleanup: - server.close() - } - - def "test validated controller validates @Valid collection with standard embedded errors"() { - given: - ApplicationContext context = ApplicationContext.run([ - 'spec.name': getClass().simpleName - ]) - EmbeddedServer embeddedServer = context.getBean(EmbeddedServer).start() - HttpClient client = context.createBean(HttpClient, embeddedServer.getURL()) - EmbeddedServer server = ApplicationContext.run(EmbeddedServer) - - when: - HttpResponse response = client.toBlocking().exchange( - HttpRequest.POST("/validated/pojos", '[{"email":"abc"},{"email":"a@b.c","name":"Micronaut"},{"email":"a@b.c"}]') - .contentType(MediaType.APPLICATION_JSON_TYPE), - String - ) - - then: - def e = thrown(HttpClientResponseException) - e.response.code() == HttpStatus.BAD_REQUEST.code - - when: - def result = new JsonSlurper().parseText((String) e.response.getBody().get()) - - then: - result.message == 'Bad Request' - result._embedded.errors.size() == 3 - result._embedded.errors.find { it.message == 'pojos[0].email: Email should be valid' } - result._embedded.errors.find { it.message == 'pojos[0].name: must not be blank' } - result._embedded.errors.find { it.message == 'pojos[2].name: must not be blank' } - - cleanup: - server.close() - } - - def "test validated controller with multiple violations"() { - given: - ApplicationContext context = ApplicationContext.run([ - 'spec.name': getClass().simpleName - ]) - EmbeddedServer embeddedServer = context.getBean(EmbeddedServer).start() - HttpClient client = context.createBean(HttpClient, embeddedServer.getURL()) - EmbeddedServer server = ApplicationContext.run(EmbeddedServer) - - when: - HttpResponse response = client.toBlocking().exchange( - HttpRequest.POST("/validated/pojo", '{"email":"abc"}') - .contentType(MediaType.APPLICATION_JSON_TYPE), - String - ) - - then: - def e = thrown(HttpClientResponseException) - e.response.code() == HttpStatus.BAD_REQUEST.code - - when: - def result = new JsonSlurper().parseText((String) e.response.getBody().get()) - - then: - result.message == 'Bad Request' - result._embedded.errors.size() == 2 - result._embedded.errors.find { it.message == 'pojo.email: Email should be valid' } - result._embedded.errors.find { it.message == 'pojo.name: must not be blank' } - - cleanup: - server.close() - } - - def "test validated controller args"() { - given: - ApplicationContext context = ApplicationContext.run([ - 'spec.name': getClass().simpleName - ]) - EmbeddedServer embeddedServer = context.getBean(EmbeddedServer).start() - HttpClient client = context.createBean(HttpClient, embeddedServer.getURL()) - EmbeddedServer server = ApplicationContext.run(EmbeddedServer) - - when: - Flux> flowable = Flux.from(client.exchange( - HttpRequest.POST("/validated/args", '{"amount":"xxx"}') - .contentType(MediaType.APPLICATION_JSON_TYPE), - String - )) - flowable.blockFirst() - - then: - def e = thrown(HttpClientResponseException) - e.response.code() == HttpStatus.BAD_REQUEST.code - - when: - def result = new JsonSlurper().parseText((String) e.response.getBody().get()) - - then: - result._embedded.errors[0].message == 'amount: numeric value out of bounds (<3 digits>.<2 digits> expected)' - - cleanup: - server.close() - } - - def "test validated controller args with standard embedded errors"() { - given: - ApplicationContext context = ApplicationContext.run([ - 'spec.name': getClass().simpleName - ]) - EmbeddedServer embeddedServer = context.getBean(EmbeddedServer).start() - HttpClient client = context.createBean(HttpClient, embeddedServer.getURL()) - EmbeddedServer server = ApplicationContext.run(EmbeddedServer) - - when: - Flux> flowable = Flux.from(client.exchange( - HttpRequest.POST("/validated/args", '{"amount":"xxx"}') - .contentType(MediaType.APPLICATION_JSON_TYPE), - String - )) - flowable.blockFirst() - - then: - def e = thrown(HttpClientResponseException) - e.response.code() == HttpStatus.BAD_REQUEST.code - - when: - def result = new JsonSlurper().parseText((String) e.response.getBody().get()) - - then: - result.message == 'Bad Request' - result._embedded.errors.size() == 1 - result._embedded.errors.find { it.message == 'amount: numeric value out of bounds (<3 digits>.<2 digits> expected)' } - - cleanup: - server.close() - } - - void "test validated controller optional query param"() { - given: - ApplicationContext context = ApplicationContext.run([ - 'spec.name': getClass().simpleName - ]) - EmbeddedServer embeddedServer = context.getBean(EmbeddedServer).start() - HttpClient client = context.createBean(HttpClient, embeddedServer.getURL()) - EmbeddedServer server = ApplicationContext.run(EmbeddedServer) - - when: - Flux> flowable = Flux.from(client.exchange( - HttpRequest.GET("/validated/optional?limit=0"), - Argument.STRING, - Argument.of(Map, String, Object) - )) - flowable.blockFirst() - - then: - def e = thrown(HttpClientResponseException) - e.response.code() == HttpStatus.BAD_REQUEST.code - - when: - Map result = e.response.getBody(Argument.of(Map, String, Object)).get() - - then: - result._embedded.errors[0].message == 'limit: must be greater than or equal to 1' - - cleanup: - server.close() - } - - void "test validated controller optional query param with standard embedded errors"() { - given: - ApplicationContext context = ApplicationContext.run([ - 'spec.name': getClass().simpleName - ]) - EmbeddedServer embeddedServer = context.getBean(EmbeddedServer).start() - HttpClient client = context.createBean(HttpClient, embeddedServer.getURL()) - EmbeddedServer server = ApplicationContext.run(EmbeddedServer) - - when: - Flux> flowable = Flux.from(client.exchange( - HttpRequest.GET("/validated/optional?limit=0"), - Argument.STRING, - Argument.of(Map, String, Object) - )) - flowable.blockFirst() - - then: - def e = thrown(HttpClientResponseException) - e.response.code() == HttpStatus.BAD_REQUEST.code - - when: - Map result = e.response.getBody(Argument.of(Map, String, Object)).get() - - then: - result.message == 'Bad Request' - result._embedded.errors.size() == 1 - result._embedded.errors.find { it.message == 'limit: must be greater than or equal to 1' } - - cleanup: - server.close() - } - - void "test validated controller empty optional query param"() { - given: - EmbeddedServer server = ApplicationContext.run(EmbeddedServer, [ - 'spec.name': getClass().simpleName - ]) - HttpClient client = server.applicationContext.createBean(HttpClient, server.getURL()) - - when: - Flux> flowable = Flux.from(client.exchange( - HttpRequest.GET("/validated/optional"), - Argument.STRING, - Argument.of(Map, String, Object) - )) - def resp = flowable.blockFirst() - - then: - noExceptionThrown() - resp.body() == "true" - - cleanup: - client.close() - server.close() - } - - void "test validated controller empty optional query param with not null"() { - given: - EmbeddedServer server = ApplicationContext.run(EmbeddedServer, [ - 'spec.name': getClass().simpleName - ]) - HttpClient client = server.applicationContext.createBean(HttpClient, server.getURL()) - - when: - Flux> flowable = Flux.from(client.exchange( - HttpRequest.GET("/validated/optional/notNull"), - Argument.STRING, - Argument.of(Map, String, Object) - )) - flowable.blockFirst() - - then: - def e = thrown(HttpClientResponseException) - e.response.code() == HttpStatus.BAD_REQUEST.code - - when: - Map result = e.response.getBody(Argument.of(Map, String, Object)).get() - - then: - result._embedded.errors[0].message == 'limit: must not be null' - - cleanup: - server.close() - } - - void "test validated controller empty optional query param with not null with standard embedded errors"() { - given: - EmbeddedServer server = ApplicationContext.run(EmbeddedServer, [ - 'spec.name': getClass().simpleName - ]) - HttpClient client = server.applicationContext.createBean(HttpClient, server.getURL()) - - when: - Flux> flowable = Flux.from(client.exchange( - HttpRequest.GET("/validated/optional/notNull"), - Argument.STRING, - Argument.of(Map, String, Object) - )) - flowable.blockFirst() - - then: - def e = thrown(HttpClientResponseException) - e.response.code() == HttpStatus.BAD_REQUEST.code - - when: - Map result = e.response.getBody(Argument.of(Map, String, Object)).get() - - then: - result.message == 'Bad Request' - result._embedded.errors.size() == 1 - result._embedded.errors.find { it.message == 'limit: must not be null' } - - cleanup: - server.close() - } - - void "test validated response with annotation"() { - given: - EmbeddedServer server = ApplicationContext.run(EmbeddedServer) - TestClient client = server.applicationContext.getBean(TestClient) - - when: - client.test1("x") - - then: - def e = thrown(HttpClientResponseException) - e.response.getBody(Map).get()._embedded.errors[0].message == 'value: size must be between 2 and 2147483647' - - cleanup: - server.close() - } - - void "test validated response with annotation with standard embedded errors"() { - given: - EmbeddedServer server = ApplicationContext.run(EmbeddedServer) - TestClient client = server.applicationContext.getBean(TestClient) - - when: - client.test1("x") - - then: - def e = thrown(HttpClientResponseException) - - when: - def result = new JsonSlurper().parseText((String) e.response.getBody().get()) - - then: - result.message == 'Bad Request' - result._embedded.errors.size() == 1 - result._embedded.errors.find { it.message == 'value: size must be between 2 and 2147483647' } - - cleanup: - server.close() - } - - void "test validated response raw exception creation and null"() { - given: - EmbeddedServer server = ApplicationContext.run(EmbeddedServer) - TestClient client = server.applicationContext.getBean(TestClient) - - when: - client.test3() - - then: - def e = thrown(HttpClientResponseException) - e.response.getBody(Map).get()._embedded.errors[0].message == 'something is invalid' - - cleanup: - server.close() - } - - void "test validated response raw exception creation and empty"() { - given: - EmbeddedServer server = ApplicationContext.run(EmbeddedServer) - TestClient client = server.applicationContext.getBean(TestClient) - - when: - client.test4() - - then: - def e = thrown(HttpClientResponseException) - e.response.getBody(Map).get()._embedded.errors[0].message == 'another thing is invalid' - - cleanup: - server.close() - } - - void "test a client can be validated"() { - given: - EmbeddedServer server = ApplicationContext.run(EmbeddedServer) - TestClient client = server.applicationContext.getBean(TestClient) - - when: - client.test5("") - - then: - thrown(ConstraintViolationException) - - cleanup: - server.close() - } - - void "test a java client can be validated"() { - given: - EmbeddedServer server = ApplicationContext.run(EmbeddedServer) - JavaClient client = server.applicationContext.getBean(JavaClient) - - when: - client.test5("") - - then: - thrown(ConstraintViolationException) - } - - void "test validated controller with non introspected pojo"() { - given: - ApplicationContext context = ApplicationContext.run([ - 'spec.name': getClass().simpleName - ]) - EmbeddedServer embeddedServer = context.getBean(EmbeddedServer).start() - HttpClient client = context.createBean(HttpClient, embeddedServer.getURL()) - EmbeddedServer server = ApplicationContext.run(EmbeddedServer) - - when: - HttpResponse response = client.toBlocking().exchange( - HttpRequest.POST("/validated/no-introspection", '{"email":"a@a.com","name":"Micronaut"}') - .contentType(MediaType.APPLICATION_JSON_TYPE), - String - ) - - then: - def e = thrown(HttpClientResponseException) - e.response.code() == HttpStatus.BAD_REQUEST.code - - when: - def result = new JsonSlurper().parseText((String) e.response.getBody().get()) - - then: - result._embedded.errors[0].message == 'pojo: Cannot validate io.micronaut.validation.PojoNoIntrospection. No bean introspection present. Please add @Introspected to the class and ensure Micronaut annotation processing is enabled' - - cleanup: - server.close() - } - - void "test validated controller with non introspected pojo with standard embedded errors"() { - given: - ApplicationContext context = ApplicationContext.run([ - 'spec.name': getClass().simpleName - ]) - EmbeddedServer embeddedServer = context.getBean(EmbeddedServer).start() - HttpClient client = context.createBean(HttpClient, embeddedServer.getURL()) - EmbeddedServer server = ApplicationContext.run(EmbeddedServer) - - when: - HttpResponse response = client.toBlocking().exchange( - HttpRequest.POST("/validated/no-introspection", '{"email":"a@a.com","name":"Micronaut"}') - .contentType(MediaType.APPLICATION_JSON_TYPE), - String - ) - - then: - def e = thrown(HttpClientResponseException) - e.response.code() == HttpStatus.BAD_REQUEST.code - - when: - def result = new JsonSlurper().parseText((String) e.response.getBody().get()) - - then: - result.message == 'Bad Request' - result._embedded.errors.size() == 1 - result._embedded.errors.find { it.message == 'pojo: Cannot validate io.micronaut.validation.PojoNoIntrospection. No bean introspection present. Please add @Introspected to the class and ensure Micronaut annotation processing is enabled' } - - cleanup: - server.close() - } - - void "test validated config props with annotations in abstract class"() { - ApplicationContext context = ApplicationContext.run([ - 'spec.name': getClass().simpleName - ]) - - when: - context.getBean(Config) - - then: - def ex = thrown(BeanInstantiationException) - ex.message.contains("count - must be greater than or equal to 1") - } - - @Client("/validated/tests") - @Consumes(MediaType.TEXT_PLAIN) - static interface TestClient { - - @Get(value = "/test1/{value}", produces = MediaType.TEXT_PLAIN) - String test1(String value) - - @Get(value = "/test2/{value}", produces = MediaType.TEXT_PLAIN) - String test2(String value) - - @Get(value = "/test3", produces = MediaType.TEXT_PLAIN) - String test3() - - @Get(value = "/test4", produces = MediaType.TEXT_PLAIN) - String test4() - - @Get(value = "/validated") - String test5(@NotBlank @Header String header) - } - - @Controller("/validated/tests") - @Validated - static class TestController { - - @Get(value = "/test1/{value}", produces = MediaType.TEXT_PLAIN) - String test1(@Size(min = 2) String value) { - return "got some " + value - } - - @Get(value = "/test3", produces = MediaType.TEXT_PLAIN) - String test3() { - throw new ConstraintViolationException("something is invalid", null) - } - - @Get(value = "/test4", produces = MediaType.TEXT_PLAIN) - String test4() { - throw new ConstraintViolationException("another thing is invalid", Collections.emptySet()) - } - - - static class Some { - - private String thing - - @NotNull - String getThing() { - return thing - } - - void setThing(String thing) { - this.thing = thing - } - } - } - - @ConfigurationProperties("my.config") - static class Config { - - @NotNull - @Min(1L) - @Max(10L) - Integer count = 0 - - } - - abstract static class AbstractConfig { - @NotNull - @Min(1L) - @Max(10L) - Integer count = 0 - } -} diff --git a/validation/src/test/groovy/io/micronaut/validation/ValidationVisitorSpec.groovy b/validation/src/test/groovy/io/micronaut/validation/ValidationVisitorSpec.groovy deleted file mode 100644 index 74e4c1fd648..00000000000 --- a/validation/src/test/groovy/io/micronaut/validation/ValidationVisitorSpec.groovy +++ /dev/null @@ -1,163 +0,0 @@ -/* - * Copyright 2017-2019 original authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.micronaut.validation - -import io.micronaut.annotation.processing.test.AbstractTypeElementSpec -import io.micronaut.inject.BeanDefinition - -class ValidationVisitorSpec extends AbstractTypeElementSpec { - - void "test requires validation beans"() { - given: - BeanDefinition definition = buildBeanDefinition('test.FooService','''\ -package test; - -import io.micronaut.core.annotation.NonNull; -import jakarta.inject.Singleton; -import javax.annotation.Nonnull; -import javax.validation.Valid; -import javax.validation.constraints.NotBlank; -import javax.validation.constraints.NotNull; - -@Singleton -class FooService { - - public Pojo bar(@Nonnull @NotNull @Valid Pojo pojo) { - return foo(pojo); - } - - @NonNull - private Pojo foo(@NonNull Pojo pojo) { - return pojo; - } - -} - -@Valid -class Pojo { - @NotBlank - private final String name; - - public Pojo(String name) { - this.name = name; - } - - public String getName() { - return name; - } -} - -''') - expect: - definition != null - definition.findPossibleMethods("bar").findFirst().get().hasAnnotation(Validated) - } - - void "test fails when @Valid is defined for the parameter on a private method"() { - when: - buildBeanDefinition('test.FooService','''\ -package test; - -import io.micronaut.core.annotation.NonNull; -import jakarta.inject.Singleton; -import javax.annotation.Nonnull; -import javax.validation.Valid; -import javax.validation.constraints.NotBlank; -import javax.validation.constraints.NotNull; - -@Singleton -class FooService { - - public Pojo bar(@Nonnull @NotNull @Valid Pojo pojo) { - return foo(pojo); - } - - @NonNull - private Pojo foo(@NonNull @Valid Pojo pojo) { - return pojo; - } - -} - -@Valid -class Pojo { - @NotBlank - private final String name; - - public Pojo(String name) { - this.name = name; - } - - public String getName() { - return name; - } -} - -''') - then: - Throwable t = thrown() - t.message.contains 'Method annotated for validation but is declared private' - } - - void "test fails when @Valid is defined on a private method"() { - when: - buildBeanDefinition('test.FooService','''\ -package test; - -import io.micronaut.core.annotation.NonNull; -import jakarta.inject.Singleton; -import javax.annotation.Nonnull; -import javax.validation.Valid; -import javax.validation.constraints.NotBlank; -import javax.validation.constraints.NotNull; - -@Singleton -class FooService { - - public Pojo bar(@Nonnull @NotNull @Valid Pojo pojo) { - return foo(pojo); - } - - @Valid - @NonNull - private Pojo foo(@NonNull Pojo pojo) { - return pojo; - } - -} - -@Valid -class Pojo { - @NotBlank - private final String name; - - public Pojo(String name) { - this.name = name; - } - - public String getName() { - return name; - } -} - -''') - then: - Throwable t = thrown() - t.message.contains 'Method annotated for validation but is declared private' - } - -} - diff --git a/validation/src/test/groovy/io/micronaut/validation/WebSocketClientValidationSpec.groovy b/validation/src/test/groovy/io/micronaut/validation/WebSocketClientValidationSpec.groovy deleted file mode 100644 index c323ea43f05..00000000000 --- a/validation/src/test/groovy/io/micronaut/validation/WebSocketClientValidationSpec.groovy +++ /dev/null @@ -1,81 +0,0 @@ -package io.micronaut.validation - -import io.micronaut.context.ApplicationContext -import io.micronaut.context.annotation.Requires -import io.micronaut.core.annotation.Introspected -import io.micronaut.runtime.server.EmbeddedServer -import io.micronaut.websocket.WebSocketClient -import io.micronaut.websocket.annotation.ClientWebSocket -import io.micronaut.websocket.annotation.OnMessage -import jakarta.inject.Singleton -import reactor.core.publisher.Flux -import spock.lang.Issue -import spock.lang.Specification -import spock.util.concurrent.PollingConditions - -import javax.validation.ConstraintViolationException -import javax.validation.Valid -import javax.validation.constraints.Pattern - -class WebSocketClientValidationSpec extends Specification { - - @Issue('https://github.com/micronaut-projects/micronaut-core/issues/5332') - def 'test validation of request bean'() { - given: - EmbeddedServer embeddedServer = ApplicationContext.builder('spec.name':'WebSocketClientValidationSpec').run(EmbeddedServer) - def client = embeddedServer.applicationContext.createBean(WebSocketClient, new URI(embeddedServer.URI.toString())) - def holderBean = embeddedServer.applicationContext.getBean(HolderBean) - def conditions = new PollingConditions(timeout: 5) - - expect: - holderBean.seenData == null - holderBean.seenError == null - - when: - Flux.from(client.connect(ClientHandler, '/validated?foo=bar')).blockFirst().close() - then: - conditions.eventually { - assert holderBean.seenData != null - assert holderBean.seenData.foo == 'bar' - assert holderBean.seenError == null - } - - when: - Flux.from(client.connect(ClientHandler, '/validated?foo=baz')).blockFirst().close() - then: - conditions.eventually { - assert holderBean.seenData != null - assert holderBean.seenData.foo == 'bar' // unchanged - assert holderBean.seenError != null - } - } - - @Singleton - @Requires(property = 'spec.name', value = 'WebSocketClientValidationSpec') - static class HolderBean { - ValidatedData seenData = null - ConstraintViolationException seenError = null - } - - @ClientWebSocket('/validated') - @Requires(property = 'spec.name', value = 'WebSocketClientValidationSpec') - static abstract class ClientHandler implements Closeable { - @OnMessage - void onMessage(byte[] message) { - } - } - - @Introspected - static class ValidatedData { - private String foo - - void setFoo(String foo) { - this.foo = foo - } - - @Pattern(regexp = 'bar') - String getFoo() { - return foo - } - } -} diff --git a/validation/src/test/groovy/io/micronaut/validation/WebSocketValidationServerHandler.groovy b/validation/src/test/groovy/io/micronaut/validation/WebSocketValidationServerHandler.groovy deleted file mode 100644 index a7d5bfcbde6..00000000000 --- a/validation/src/test/groovy/io/micronaut/validation/WebSocketValidationServerHandler.groovy +++ /dev/null @@ -1,36 +0,0 @@ -package io.micronaut.validation - -import io.micronaut.context.annotation.Requires -import io.micronaut.http.annotation.RequestBean -import io.micronaut.websocket.WebSocketSession -import io.micronaut.websocket.annotation.OnError -import io.micronaut.websocket.annotation.OnMessage -import io.micronaut.websocket.annotation.OnOpen -import io.micronaut.websocket.annotation.ServerWebSocket -import jakarta.inject.Inject - -import javax.validation.ConstraintViolationException -import javax.validation.Valid - -// this has to be a top-level class because groovy -@Requires(property = 'spec.name', value = 'WebSocketClientValidationSpec') -@ServerWebSocket('/validated') -class WebSocketValidationServerHandler { - @Inject - WebSocketClientValidationSpec.HolderBean holderBean - - @OnOpen - @Validated - def onOpen(@RequestBean @Valid WebSocketClientValidationSpec.ValidatedData data, WebSocketSession session) { - holderBean.seenData = data - } - - @OnMessage - void onMessage(byte[] message) { - } - - @OnError - def onError(ConstraintViolationException e) { - holderBean.seenError = e - } -} diff --git a/validation/src/test/groovy/io/micronaut/validation/executable/NullablePrimitiveSpec.groovy b/validation/src/test/groovy/io/micronaut/validation/executable/NullablePrimitiveSpec.groovy deleted file mode 100644 index 00bd2b6f40e..00000000000 --- a/validation/src/test/groovy/io/micronaut/validation/executable/NullablePrimitiveSpec.groovy +++ /dev/null @@ -1,138 +0,0 @@ -package io.micronaut.validation.executable - -import io.micronaut.annotation.processing.test.AbstractTypeElementSpec -import spock.lang.Shared -import spock.lang.Unroll - -class NullablePrimitiveSpec extends AbstractTypeElementSpec { - - @Shared - String warning = "@Nullable on primitive types will allow the method to be executed at runtime with null values" - - PrintStream old - ByteArrayOutputStream out - - void setup() { - old = System.out - out = new ByteArrayOutputStream() - System.out = new PrintStream(out) - } - - void cleanup() { - System.out = old - } - - @Unroll - void 'test deprecation warning printed for applicable annotation #annotation'() { - when: - buildTypeElement(""" - package test; - - import io.micronaut.http.annotation.*; - - @Controller("/foo") - class Foo { - - @Get() - String abc(@${annotation} String str) { - return ""; - } - } - """.stripIndent() - ) - String output = out.toString("UTF8") - String deprecationWarning = "Usages of deprecated annotation $annotation found." - then: - noExceptionThrown() - (deprecated && output.contains(deprecationWarning)) || (!deprecated && !output.contains(deprecationWarning)) - - where: - deprecated | annotation - false | 'javax.annotation.Nullable' - false | 'io.micronaut.core.annotation.Nullable' - false | 'edu.umd.cs.findbugs.annotations.Nullable' - false | 'javax.annotation.Nonnull' - false | 'io.micronaut.core.annotation.NonNull' - false | 'edu.umd.cs.findbugs.annotations.NonNull' - } - - @Unroll - void 'test #annotation on primitive params will show a warning'() { - when: - buildTypeElement(""" - package test; - - import io.micronaut.http.annotation.*; - - @Controller("/foo") - class Foo { - - @Get() - String abc(${annotation} boolean boolPrimitive) { - return ""; - } - } - """.stripIndent() - ) - String output = out.toString("UTF8") - - then: - noExceptionThrown() - output.contains(warning) - - where: - annotation << ['@javax.annotation.Nullable', '@io.micronaut.core.annotation.Nullable', '@edu.umd.cs.findbugs.annotations.Nullable'] - } - - @Unroll - void 'test #annotation is allowed on non-primitive parameters'() { - when: - buildTypeElement(""" - package test; - - import io.micronaut.http.annotation.*; - - @Controller("/foo") - class Foo { - - @Get() - String abc(${annotation} Boolean boolType) { - return ""; - } - } - """.stripIndent() - ) - String output = out.toString("UTF8") - - then: - noExceptionThrown() - !output.contains(warning) - - where: - annotation << ['@javax.annotation.Nullable', '@io.micronaut.core.annotation.Nullable', '@edu.umd.cs.findbugs.annotations.Nullable'] - } - - @Unroll - void 'test #annotation is allowed primitive parameters and not @Executable classes'() { - when: - buildTypeElement(""" - package test; - - @jakarta.inject.Singleton - class Foo { - void bar(${annotation} int n) { - } - } - """.stripIndent() - ) - String output = out.toString("UTF8") - - then: - noExceptionThrown() - !output.contains(warning) - - where: - annotation << ['@javax.annotation.Nullable', '@io.micronaut.core.annotation.Nullable', '@edu.umd.cs.findbugs.annotations.Nullable'] - } - -} diff --git a/validation/src/test/groovy/io/micronaut/validation/internal/InteralApiRuleSpec.groovy b/validation/src/test/groovy/io/micronaut/validation/internal/InteralApiRuleSpec.groovy deleted file mode 100644 index d234099af7b..00000000000 --- a/validation/src/test/groovy/io/micronaut/validation/internal/InteralApiRuleSpec.groovy +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Copyright 2017-2019 original authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.micronaut.validation.internal - -import io.micronaut.annotation.processing.test.AbstractTypeElementSpec - -class InteralApiRuleSpec extends AbstractTypeElementSpec { - - void "test missing parameter"() { - given: - def oldOut = System.out - def out = new ByteArrayOutputStream() - System.out = new PrintStream(out) - - when: - buildTypeElement(""" - -package test; - -import io.micronaut.core.io.scan.*; - -class Foo extends BeanIntrospectionScanner { - -} - -""") - String output = out.toString("UTF8") - - then: - noExceptionThrown() - output.contains("warning: Element extends or implements an internal or experimental Micronaut API\n" + - "class Foo extends BeanIntrospectionScanner {") - output.contains("Overriding an internal Micronaut API may result in breaking changes in minor or patch versions") - - cleanup: - System.out = oldOut - } - -} \ No newline at end of file diff --git a/validation/src/test/groovy/io/micronaut/validation/properties/MixedCasePropertySpec.groovy b/validation/src/test/groovy/io/micronaut/validation/properties/MixedCasePropertySpec.groovy deleted file mode 100644 index ad5dec07f97..00000000000 --- a/validation/src/test/groovy/io/micronaut/validation/properties/MixedCasePropertySpec.groovy +++ /dev/null @@ -1,329 +0,0 @@ -/* - * Copyright 2017-2019 original authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.micronaut.validation.properties - -import io.micronaut.annotation.processing.test.AbstractTypeElementSpec -import spock.lang.Ignore - -@Ignore("Mixed case properties are now allowed") -class MixedCasePropertySpec extends AbstractTypeElementSpec { - - void "test wrong property name in @Property"() { - when: - buildTypeElement(""" -package test; - -import io.micronaut.context.annotation.Property; -import jakarta.inject.Singleton; - -@Singleton -class MyService { - - @Property(name = "fooBar") - private String property; -} - -""") - then: - def e = thrown(RuntimeException) - e.message.contains("Value 'fooBar' is not valid property placeholder. Please use kebab-case notation, for example 'foo-bar'.") - } - - void "test wrong property name in @Value"() { - when: - buildTypeElement(""" -package test; - -import io.micronaut.context.annotation.Value; -import jakarta.inject.Singleton; - -@Singleton -class MyService { - - @Value(\"Hello \${userName:John}\") - private String property; -} - -""") - - then: - def e = thrown(RuntimeException) - e.message.contains("Value 'userName' is not valid property placeholder. Please use kebab-case notation, for example 'user-name'.") - } - - void "test more than one property in @Value"() { - when: - buildTypeElement(""" -package test; - -import io.micronaut.context.annotation.Value; -import jakarta.inject.Singleton; - -@Singleton -class MyService { - - @Value(\"Hello \${user-name:John} \${lastName:Doe}\") - private String property; -} - -""") - - then: - def e = thrown(RuntimeException) - e.message.contains("Value 'lastName' is not valid property placeholder. Please use kebab-case notation, for example 'last-name'.") - } - - void "test wrong property name in @Controller"() { - when: - buildTypeElement(""" -package test; - -import io.micronaut.http.annotation.Controller; - -@Controller(value = \"\${controllerPath}\") -class MyController { - -} - -""") - - then: - def e = thrown(RuntimeException) - e.message.contains("Value 'controllerPath' is not valid property placeholder. Please use kebab-case notation, for example 'controller-path'.") - } - - void "test wrong property name in @Controller with 'produces' property"() { - when: - buildTypeElement(""" -package test; - -import io.micronaut.http.annotation.Controller; - -@Controller(value = \"\${controller-path}\", produces = {\"\${app.produces1}\", \"\${app.myWrongValue}\"}) -class MyController { - -} - -""") - - then: - def e = thrown(RuntimeException) - e.message.contains("Value 'app.myWrongValue' is not valid property placeholder. Please use kebab-case notation, for example 'app.my-wrong-value'.") - } - - void "test wrong property name in @Named in a constructor"() { - when: - buildTypeElement(""" -package test; - -import io.micronaut.http.annotation.Controller; -import io.micronaut.http.annotation.Get; -import io.micronaut.context.annotation.Value; -import jakarta.inject.Named; - -@Controller() -class VehicleController { - - private final Engine engine; - - public VehicleController(@Value(\"\${vehicleCylinders}\") Engine engine) { - this.engine = engine; - } -} - -interface Engine { - int getCylinders(); -} - -""") - - then: - def e = thrown(RuntimeException) - e.message.contains("Value 'vehicleCylinders' is not valid property placeholder. Please use kebab-case notation, for example 'vehicle-cylinders'.") - } - - void "test that environment-style variables are supported"() { - when: - buildTypeElement(""" -package test; - -import io.micronaut.context.annotation.Value; -import jakarta.inject.Singleton; - -@Singleton -class MyService { - - @Value(\"Hello \${USER_NAME}\") - private String msg; -} - -""") - - then: - notThrown(Exception) - } - - void "test that with defaults the last value is not checked"() { - when: - buildTypeElement(""" -package test; - -import io.micronaut.context.annotation.Value; -import jakarta.inject.Singleton; - -@Singleton -class MyService { - - @Value(\"\${some-value:another-thing:someValue2:doesntMaTTeR}\") - private String property; -} - -""") - - then: - def e = thrown(RuntimeException) - e.message.contains("Value 'someValue2' is not valid property placeholder. Please use kebab-case notation, for example 'some-value2'.") - } - - void "test that escaping : works with right property name"() { - when: - buildTypeElement(""" -package test; - -import io.micronaut.context.annotation.Value; -import jakarta.inject.Singleton; - -@Singleton -class MyService { - - @Value(\"\${server-address:`http://localhost:8080`}\") - private String serverAddress; -} - -""") - - then: - notThrown(Exception) - } - - void "test that escaping : works with wrong property name"() { - when: - buildTypeElement(""" -package test; - -import io.micronaut.context.annotation.Value; -import jakarta.inject.Singleton; - -@Singleton -class MyService { - - @Value(\"\${serverAddress:`http://localhost:8080`}\") - private String serverAddress; -} - -""") - - then: - def e = thrown(RuntimeException) - e.message.contains("Value 'serverAddress' is not valid property placeholder. Please use kebab-case notation, for example 'server-address'.") - } - - void "test that escaping : works with more than one property"() { - when: - buildTypeElement(""" -package test; - -import io.micronaut.context.annotation.Value; -import jakarta.inject.Singleton; - -@Singleton -class MyService { - - @Value(\"\${some-value:anotherThing:`this:goes:together`}\") - private String foo; -} - -""") - - then: - def e = thrown(RuntimeException) - e.message.contains("Value 'some-value:anotherThing' is not valid property placeholder. Please use kebab-case notation, for example 'some-value:another-thing'.") - } - - void "test that escaping : works with more than one property when all property names are correct"() { - when: - buildTypeElement(""" -package test; - -import io.micronaut.context.annotation.Value; -import jakarta.inject.Singleton; - -@Singleton -class MyService { - - @Value(\"\${some-value:another-thing:foo-bar:`this:goes:together`}\") - private String foo; -} - -""") - - then: - notThrown(Exception) - } - - void "test that escaping : works with the last property name"() { - when: - buildTypeElement(""" -package test; - -import io.micronaut.context.annotation.Value; -import jakarta.inject.Singleton; - -@Singleton -class MyService { - - @Value(\"\${some-value:another-thing:fooBar:`this:goes:together`}\") - private String foo; -} - -""") - - then: - def e = thrown(RuntimeException) - e.message.contains("Value 'some-value:another-thing:fooBar' is not valid property placeholder. Please use kebab-case notation, for example 'some-value:another-thing:foo-bar'.") - } - - void "test that escaping : works with more than one property when all property names are correct and default value is not checked"() { - when: - buildTypeElement(""" -package test; - -import io.micronaut.context.annotation.Value; -import jakarta.inject.Singleton; - -@Singleton -class MyService { - - @Value(\"\${some-value:another-thing:foo-bar:`this:goes:together:AndDoesNtMATtteR`}\") - private String foo; -} - -""") - - then: - notThrown(Exception) - } -} diff --git a/validation/src/test/groovy/io/micronaut/validation/validator/ConstraintValidatorRegistrySpec.groovy b/validation/src/test/groovy/io/micronaut/validation/validator/ConstraintValidatorRegistrySpec.groovy deleted file mode 100644 index 09819721c00..00000000000 --- a/validation/src/test/groovy/io/micronaut/validation/validator/ConstraintValidatorRegistrySpec.groovy +++ /dev/null @@ -1,22 +0,0 @@ -package io.micronaut.validation.validator - -import io.micronaut.context.ApplicationContext -import io.micronaut.validation.validator.constraints.ConstraintValidatorRegistry -import spock.lang.AutoCleanup -import spock.lang.Shared -import spock.lang.Specification - -import javax.validation.constraints.NotBlank - -class ConstraintValidatorRegistrySpec extends Specification { - - @Shared @AutoCleanup ApplicationContext context = ApplicationContext.run() - @Shared ConstraintValidatorRegistry reg = context.getBean(ConstraintValidatorRegistry) - - void "test find constraint validators"() { - expect: - reg.getConstraintValidator(NotBlank, String) - reg.getConstraintValidator(NotBlank, String).is(reg.getConstraintValidator(NotBlank, StringBuffer)) - reg.getConstraintValidator(NotBlank, String).is(reg.getConstraintValidator(NotBlank, CharSequence)) - } -} diff --git a/validation/src/test/groovy/io/micronaut/validation/validator/ValidatorGroupsSpec.groovy b/validation/src/test/groovy/io/micronaut/validation/validator/ValidatorGroupsSpec.groovy deleted file mode 100644 index 6245d530afc..00000000000 --- a/validation/src/test/groovy/io/micronaut/validation/validator/ValidatorGroupsSpec.groovy +++ /dev/null @@ -1,162 +0,0 @@ -package io.micronaut.validation.validator - -import io.micronaut.annotation.processing.TypeElementVisitorProcessor -import io.micronaut.annotation.processing.test.AbstractTypeElementSpec -import io.micronaut.annotation.processing.test.JavaParser -import io.micronaut.context.ApplicationContext -import io.micronaut.core.annotation.Introspected -import io.micronaut.inject.beans.visitor.IntrospectedTypeElementVisitor -import io.micronaut.inject.visitor.TypeElementVisitor -import spock.lang.AutoCleanup -import spock.lang.Shared - -import javax.annotation.processing.SupportedAnnotationTypes -import javax.validation.constraints.NotBlank -import javax.validation.constraints.NotEmpty -import javax.validation.constraints.Size -import javax.validation.groups.Default - -class ValidatorGroupsSpec extends AbstractTypeElementSpec { - - @Shared - @AutoCleanup - ApplicationContext applicationContext = ApplicationContext.run() - @Shared - Validator validator = applicationContext.getBean(Validator) - - void "test validate groups"() { - given: - def address = new Address(street: "") - - when: - def violations = validator.validate(address, GroupThree) - - then: - violations.size() == 1 - violations.iterator().next().message == 'different message' - - when: - violations = validator.validate(address, GroupThree, GroupTwo) - List messageTemplates = violations*.messageTemplate - - then: - violations.size() == 2 - messageTemplates.contains('{javax.validation.constraints.Size.message}') - messageTemplates.contains('different message') - - when: - violations = validator.validate(address, InheritedGroup) - messageTemplates = violations*.messageTemplate - - then: - violations.size() == 2 - messageTemplates.contains('{javax.validation.constraints.Size.message}') - messageTemplates.contains('message for default') - } - - void "test validate with default group"() { - def address = new AddressTwo(street: "", city: "", zipCode: "") - - when: - def violations = validator.validate(address) - List properties = violations*.propertyPath*.toString() - - then: - violations.size() == 2 - properties.contains("zipCode") - properties.contains("city") - - when: - violations = validator.validate(address, GroupOne) - properties = violations*.propertyPath*.toString() - - then: - violations.size() == 2 - properties.contains("zipCode") - properties.contains("street") - - when: - violations = validator.validate(address, GroupOne, Default) - properties = violations*.propertyPath*.toString() - - then: - violations.size() == 3 - properties.contains("zipCode") - properties.contains("street") - properties.contains("city") - } - - void "test build introspection"() { - given: - def introspection = buildBeanIntrospection('test.Address', ''' -package test; - -import javax.validation.constraints.*; - - -@io.micronaut.core.annotation.Introspected -class Address { - @NotBlank(groups = GroupOne.class) - @NotBlank(groups = GroupThree.class, message = "different message") - @Size(min = 5, max = 20, groups = GroupTwo.class) - private String street; - - public String getStreet() { - return this.street; - } -} - -interface GroupOne {} -interface GroupTwo {} -interface GroupThree {} -''') - expect: - introspection != null - } - - - @Override - protected JavaParser newJavaParser() { - return new JavaParser() { - @Override - protected TypeElementVisitorProcessor getTypeElementVisitorProcessor() { - return new MyTypeElementVisitorProcessor() - } - } - } - - @SupportedAnnotationTypes("*") - static class MyTypeElementVisitorProcessor extends TypeElementVisitorProcessor { - @Override - protected Collection findTypeElementVisitors() { - return [new IntrospectedTypeElementVisitor()] - } - } -} - -@Introspected -class Address { - @NotBlank(groups = GroupOne) - @NotBlank(groups = GroupThree, message = "different message") - @NotBlank(message = "message for default") - @Size(min = 5, max = 20, groups = GroupTwo) - String street -} - -interface GroupOne {} -interface GroupTwo {} -interface GroupThree {} -interface InheritedGroup extends Default, GroupTwo {} - -@Introspected -class AddressTwo { - - @NotEmpty(groups = GroupOne.class) - String street - - @NotEmpty - String city - - @NotEmpty(groups = [GroupOne.class, Default.class]) - String zipCode -} diff --git a/validation/src/test/groovy/io/micronaut/validation/validator/ValidatorSpec.groovy b/validation/src/test/groovy/io/micronaut/validation/validator/ValidatorSpec.groovy deleted file mode 100644 index 83f1d9d31d1..00000000000 --- a/validation/src/test/groovy/io/micronaut/validation/validator/ValidatorSpec.groovy +++ /dev/null @@ -1,692 +0,0 @@ -package io.micronaut.validation.validator - -import io.micronaut.context.ApplicationContext -import io.micronaut.context.annotation.Executable -import io.micronaut.context.annotation.Prototype -import io.micronaut.context.annotation.Value -import io.micronaut.context.exceptions.BeanInstantiationException -import io.micronaut.core.annotation.Introspected -import io.micronaut.core.reflect.ClassUtils -import io.micronaut.validation.validator.resolver.CompositeTraversableResolver -import jakarta.inject.Singleton -import spock.lang.AutoCleanup -import spock.lang.PendingFeature -import spock.lang.Shared -import spock.lang.Specification - -import javax.validation.ConstraintViolationException -import javax.validation.ElementKind -import javax.validation.Valid -import javax.validation.ValidatorFactory -import javax.validation.constraints.Max -import javax.validation.constraints.Min -import javax.validation.constraints.NotBlank -import javax.validation.constraints.NotNull -import javax.validation.constraints.Size -import javax.validation.metadata.BeanDescriptor - -class ValidatorSpec extends Specification { - - @Shared - @AutoCleanup - ApplicationContext applicationContext = ApplicationContext.run(["a.number": 40]) - @Shared - Validator validator = applicationContext.getBean(Validator) - - void "test validator config"() { - given: - ValidatorConfiguration config = applicationContext.getBean(ValidatorConfiguration) - ValidatorFactory factory = applicationContext.getBean(ValidatorFactory) - - expect: - config.traversableResolver instanceof CompositeTraversableResolver - factory instanceof DefaultValidatorFactory - } - - void "test simple bean validation"() { - given: - Book b = new Book(title: "", pages: 50) - def violations = validator.validate(b).sort { it.propertyPath.iterator().next().name } - - expect: - violations.size() == 4 - violations[0].invalidValue == [] - violations[0].messageTemplate == '{javax.validation.constraints.Size.message}' - violations[0].constraintDescriptor != null - violations[0].constraintDescriptor.annotation instanceof Size - violations[0].constraintDescriptor.annotation.min() == 1 - violations[0].constraintDescriptor.annotation.max() == 10 - violations[1].invalidValue == 50 - violations[1].propertyPath.iterator().next().name == 'pages' - violations[1].rootBean == b - violations[1].rootBeanClass == Book - violations[1].messageTemplate == '{javax.validation.constraints.Min.message}' - violations[1].constraintDescriptor != null - violations[1].constraintDescriptor.annotation instanceof Min - violations[1].constraintDescriptor.annotation.value() == 100l - violations[2].invalidValue == null - violations[2].propertyPath.iterator().next().name == 'primaryAuthor' - violations[2].constraintDescriptor != null - violations[2].constraintDescriptor.annotation instanceof NotNull - violations[3].invalidValue == '' - violations[3].propertyPath.iterator().next().name == 'title' - violations[3].constraintDescriptor != null - violations[3].constraintDescriptor.annotation instanceof NotBlank - - } - - void "test validate bean property"() { - given: - Book b = new Book(title: "", pages: 50) - def violations = validator.validateProperty(b, "title").sort { it.propertyPath.iterator().next().name } - - expect: - violations.size() == 1 - violations[0].invalidValue == '' - violations[0].propertyPath.iterator().next().name == 'title' - violations[0].constraintDescriptor != null - violations[0].constraintDescriptor.annotation instanceof NotBlank - - } - - void "test validate value"() { - given: - def violations = validator.validateValue(Book, "title", "").sort { it.propertyPath.iterator().next().name } - - expect: - violations.size() == 1 - violations[0].invalidValue == '' - violations[0].propertyPath.iterator().next().name == 'title' - violations[0].constraintDescriptor != null - violations[0].constraintDescriptor.annotation instanceof NotBlank - } - - void "test cascade to bean"() { - given: - Book b = new Book(title: "The Stand", pages: 1000, primaryAuthor: new Author(age: 150), authors: [new Author(name: "Stephen King", age: 50)]) - def violations = validator.validate(b).sort { it.propertyPath.iterator().next().name } - - def v1 = violations.find { it.propertyPath.toString() == 'primaryAuthor.age'} - def v2 = violations.find { it.propertyPath.toString() == 'primaryAuthor.name'} - expect: - violations.size() == 2 - v1.messageTemplate == '{javax.validation.constraints.Max.message}' - v1.propertyPath.toString() == 'primaryAuthor.age' - v1.invalidValue == 150 - v1.rootBean.is(b) - v1.leafBean instanceof Author - v1.constraintDescriptor != null - v1.constraintDescriptor.annotation instanceof Max - v1.constraintDescriptor.annotation.value() == 100l - v2.messageTemplate == '{javax.validation.constraints.NotBlank.message}' - v2.propertyPath.toString() == 'primaryAuthor.name' - v2.constraintDescriptor != null - v2.constraintDescriptor.annotation instanceof NotBlank - } - - void "test cascade to bean - no cycle"() { - given: - Book b = new Book( - title: "The Stand", - pages: 1000, - primaryAuthor: new Author(age: 150), - authors: [new Author(name: "Stephen King", age: 50)] - ) - b.primaryAuthor.favouriteBook = b // create cycle - def violations = validator.validate(b).sort { it.propertyPath.iterator().next().name } - - def v1 = violations.find { it.propertyPath.toString() == 'primaryAuthor.age'} - def v2 = violations.find { it.propertyPath.toString() == 'primaryAuthor.name'} - - expect: - violations.size() == 2 - v1.messageTemplate == '{javax.validation.constraints.Max.message}' - v1.propertyPath.toString() == 'primaryAuthor.age' - v1.invalidValue == 150 - v1.rootBean.is(b) - v1.leafBean instanceof Author - v1.constraintDescriptor != null - v1.constraintDescriptor.annotation instanceof Max - v1.constraintDescriptor.annotation.value() == 100l - v2.messageTemplate == '{javax.validation.constraints.NotBlank.message}' - v2.propertyPath.toString() == 'primaryAuthor.name' - v2.constraintDescriptor != null - v2.constraintDescriptor.annotation instanceof NotBlank - } - - void "test cascade to list - no cycle"() { - given: - Book b = new Book( - title: "The Stand", - pages: 1000, - primaryAuthor: new Author(age: 50, name: "Stephen King"), - authors: [new Author(age: 150)] - ) - b.authors[0].favouriteBook = b // create cycle - def violations = validator.validate(b).sort { it.propertyPath.iterator().next().name } - - def v1 = violations.find { it.propertyPath.toString() == 'authors[0].age'} - def v2 = violations.find { it.propertyPath.toString() == 'authors[0].name'} - - expect: - violations.size() == 2 - v1.messageTemplate == '{javax.validation.constraints.Max.message}' - v1.invalidValue == 150 - v1.propertyPath[0].kind == ElementKind.CONTAINER_ELEMENT - v1.propertyPath[0].isInIterable() - v1.propertyPath[0].index == 0 - !v1.propertyPath[1].isInIterable() - v1.propertyPath[1].index == null - v1.propertyPath[1].kind == ElementKind.PROPERTY - v1.rootBean.is(b) - v1.leafBean instanceof Author - v1.constraintDescriptor != null - v1.constraintDescriptor.annotation instanceof Max - v1.constraintDescriptor.annotation.value() == 100l - v2.messageTemplate == '{javax.validation.constraints.NotBlank.message}' - v2.constraintDescriptor != null - v2.constraintDescriptor.annotation instanceof NotBlank - } - - void "test array elements"() { - given: - ObjectArray arrayTest = new ObjectArray(strings: [] as String[]) - def violations = validator.validate(arrayTest) - - expect: - violations.size() == 1 - violations[0].invalidValue == [] - violations[0].messageTemplate == '{javax.validation.constraints.Size.message}' - violations[0].constraintDescriptor != null - violations[0].constraintDescriptor.annotation instanceof Size - violations[0].constraintDescriptor.annotation.min() == 1 - violations[0].constraintDescriptor.annotation.max() == 2 - - when: - arrayTest = new ObjectArray(strings: ["a", "b", "c"] as String[]) - violations = validator.validate(arrayTest) - - then: - violations.size() == 1 - violations[0].invalidValue == ["a", "b", "c"] - violations[0].messageTemplate == '{javax.validation.constraints.Size.message}' - violations[0].constraintDescriptor != null - violations[0].constraintDescriptor.annotation instanceof Size - violations[0].constraintDescriptor.annotation.min() == 1 - violations[0].constraintDescriptor.annotation.max() == 2 - - when: - arrayTest = new ObjectArray(numbers: [] as Long[]) - violations = validator.validate(arrayTest) - - then: - violations.size() == 1 - violations[0].invalidValue == [] - violations[0].messageTemplate == '{javax.validation.constraints.Size.message}' - violations[0].constraintDescriptor != null - violations[0].constraintDescriptor.annotation instanceof Size - violations[0].constraintDescriptor.annotation.min() == 1 - violations[0].constraintDescriptor.annotation.max() == 2 - - when: - arrayTest = new ObjectArray(numbers: [1L, 2L, 3L] as long[]) - violations = validator.validate(arrayTest) - - then: - violations.size() == 1 - violations[0].invalidValue == [1L, 2L, 3L] - violations[0].messageTemplate == '{javax.validation.constraints.Size.message}' - violations[0].constraintDescriptor != null - violations[0].constraintDescriptor.annotation instanceof Size - violations[0].constraintDescriptor.annotation.min() == 1 - violations[0].constraintDescriptor.annotation.max() == 2 - } - - void "test cascade to array elements"() { - given: - ArrayTest arrayTest = new ArrayTest(integers: [30, 10, 60] as int[]) - def violations = validator.validate(arrayTest) - - expect: - violations.size() == 2 - def v1 = violations.find { - it.invalidValue == 30 && - it.propertyPath.toList()[0].index == 0 && - it.propertyPath.toString() =='integers[0]'} - v1.constraintDescriptor != null - v1.constraintDescriptor.annotation instanceof Max - - def v2 = violations.find { it.invalidValue == 60 && it.propertyPath.toList()[0].index == 2 } - v2.constraintDescriptor != null - v2.constraintDescriptor.annotation instanceof Max - } - - void "test cascade to array elements - nested"() { - when: - ArrayTest arrayTest = new ArrayTest(integers: [10, 15] as int[], child: new ArrayTest(integers: [10, 60] as int[])) - def violations = validator.validate(arrayTest).toList() - - then: - violations.size() == 1 - violations[0].propertyPath.toString() == 'child.integers[1]' - violations[0].constraintDescriptor != null - violations[0].constraintDescriptor.annotation instanceof Max - - - when: - arrayTest = new ArrayTest( - integers: [10, 15] as int[], - child: new ArrayTest(integers: [10, 15] as int[], child: new ArrayTest(integers: [10, 60] as int[]))) - violations = validator.validate(arrayTest).toList() - - then: - violations.size() == 1 - violations[0].propertyPath.toString() == 'child.child.integers[1]' - violations[0].constraintDescriptor != null - violations[0].constraintDescriptor.annotation instanceof Max - } - - - void "test cascade to array elements null"() { - given: - ArrayTest arrayTest = new ArrayTest(integers: null) - def violations = validator.validate(arrayTest) - - expect: - violations.size() == 1 - violations.first().messageTemplate == '{javax.validation.constraints.NotNull.message}' - violations.first().propertyPath.toString() == 'integers' - violations.first().constraintDescriptor != null - violations.first().constraintDescriptor.annotation instanceof NotNull - } - - void "test executable validator"() { - given: - BookService bookService = applicationContext.getBean(BookService) - def constraintViolations = validator.forExecutables().validateParameters( - bookService, - BookService.getDeclaredMethod("saveBook", String, int.class), - ["", 50] as Object[] - ).toList().sort({ it.propertyPath.toString() }) - - expect: - constraintViolations.size() == 2 - constraintViolations[0].invalidValue == 50 - constraintViolations[0].propertyPath.toString() == 'saveBook.pages' - constraintViolations[0].constraintDescriptor != null - constraintViolations[0].constraintDescriptor.annotation instanceof Min - constraintViolations[0].constraintDescriptor.annotation.value() == 100l - constraintViolations[1].constraintDescriptor != null - constraintViolations[1].constraintDescriptor.annotation instanceof NotBlank - - } - - void "test executable validator - cascade to array"() { - when: - ArrayTest arrayTest = applicationContext.getBean(ArrayTest) - def constraintViolations = validator.forExecutables().validateParameters( - arrayTest, - ArrayTest.getDeclaredMethod("saveIntArray", int[].class), - [[30, 10, 60] as int[]] as Object[] - ).toList().sort({ it.propertyPath.toString() }) - - - then: - constraintViolations.size() == 2 - constraintViolations[0].propertyPath.toString() == 'saveIntArray.integers[0]' - constraintViolations[0].constraintDescriptor != null - constraintViolations[0].constraintDescriptor.annotation instanceof Max - constraintViolations[1].propertyPath.toString() == 'saveIntArray.integers[2]' - constraintViolations[1].constraintDescriptor != null - constraintViolations[1].constraintDescriptor.annotation instanceof Max - - when: - arrayTest = applicationContext.createBean(ArrayTest) - arrayTest.integers = [30, 10, 60] as int[] // Groovy property method access and validation - - then: - def e = thrown(ConstraintViolationException) - e.message.contains "setIntegers.integers[0]: must be less than or equal to 20" - e.message.contains "setIntegers.integers[2]: must be less than or equal to 20" - - when: - arrayTest = new ArrayTest() - arrayTest.integers = [30, 10, 60] as int[] // No interceptor - - def violations = validator.forExecutables().validateParameters( - new ArrayTest(), - ArrayTest.getDeclaredMethod("saveChild", ArrayTest.class), - [arrayTest] as Object[] - ).toList().sort({ it -> it.propertyPath.toString() }) - - then: - violations.size() == 2 - violations[0].propertyPath.toString() == 'saveChild.arrayTest.integers[0]' - violations[0].constraintDescriptor != null - violations[0].constraintDescriptor.annotation instanceof Max - violations[1].propertyPath.toString() == 'saveChild.arrayTest.integers[2]' - violations[1].constraintDescriptor != null - violations[1].constraintDescriptor.annotation instanceof Max - - } - - void "test bean descriptor"() { - given: - BeanDescriptor beanDescriptor = validator.getConstraintsForClass(Book) - - def descriptors = beanDescriptor.getConstraintsForProperty("authors") - .getConstraintDescriptors() - - expect: - beanDescriptor.isBeanConstrained() - beanDescriptor.getConstrainedProperties().size() == 4 - descriptors.size() == 1 - descriptors.first().annotation instanceof Size - descriptors.first().annotation.min() == 1 - descriptors.first().annotation.max() == 10 - } - - void "test empty bean descriptor"() { - given: - BeanDescriptor beanDescriptor = validator.getConstraintsForClass(String) - - - expect: - !beanDescriptor.isBeanConstrained() - beanDescriptor.getConstrainedProperties().size() == 0 - } - - void "test cascade to container of non-introspected class" () { - when: - Bee notIntrospected = new Bee(name: "") - HiveOfBeeList beeHive = new HiveOfBeeList(bees: [notIntrospected]) - def violations = validator.validate(beeHive) - - then: - violations.size() == 1 - !violations[0].constraintDescriptor - violations[0].message == "Cannot validate io.micronaut.validation.validator.Bee. No bean introspection present. " + - "Please add @Introspected to the class and ensure Micronaut annotation processing is enabled" - - when: - beeHive = new HiveOfBeeList(bees: [null]) - violations = validator.validate(beeHive) - - then: - violations.size() == 1 - !violations[0].constraintDescriptor - violations[0].message == "Cannot validate io.micronaut.validation.validator.Bee. No bean introspection present. " + - "Please add @Introspected to the class and ensure Micronaut annotation processing is enabled" - } - - void "test cascade to map of non-introspected value class" () { - when: - Bee notIntrospected = new Bee(name: "") - HiveOfBeeMap beeHive = new HiveOfBeeMap(bees: ["blank" : notIntrospected]) - def violations = validator.validate(beeHive) - - then: - violations.size() == 1 - !violations[0].constraintDescriptor - violations[0].message == "Cannot validate io.micronaut.validation.validator.Bee. No bean introspection present. " + - "Please add @Introspected to the class and ensure Micronaut annotation processing is enabled" - - when: - Map map = [:] - map.put("blank", null) - beeHive = new HiveOfBeeMap(bees: map) - violations = validator.validate(beeHive) - - then: - violations.size() == 1 - !violations[0].constraintDescriptor - violations[0].message == "Cannot validate io.micronaut.validation.validator.Bee. No bean introspection present. " + - "Please add @Introspected to the class and ensure Micronaut annotation processing is enabled" - } - - @PendingFeature(reason = "FIXME: https://github.com/micronaut-projects/micronaut-core/issues/4410") - void "test element validation in String collection" () { - when: - ListOfStrings strings = new ListOfStrings(strings: ["", null, "a string that's too long"]) - def violations = validator.validate(strings) - - then: - // should be: two for violating element size, and one null string violation - violations.size() == 3 - violations[0].constraintDescriptor - violations[0].constraintDescriptor.annotation instanceof Size - violations[1].constraintDescriptor - violations[1].constraintDescriptor.annotation instanceof NotNull - violations[2].constraintDescriptor - violations[2].constraintDescriptor.annotation instanceof Size - } - - void "test cascade to bean - enum"() { - given: - EnumList b = new EnumList( - enums: [null] - ) - - def violations = validator.validate(b) - - expect: - violations.size() == 1 - violations.first().message == "must not be null" - } - - void "test helpful toString() message for constraintViolation"() { - given: - BookService bookService = applicationContext.getBean(BookService) - def constraintViolations = validator.forExecutables().validateParameters( - bookService, - BookService.getDeclaredMethod("saveBook", String, int.class), - ["", 50] as Object[] - ).toList().sort({ it.propertyPath.toString() }) - - expect: - constraintViolations.size() == 2 - constraintViolations[0].toString() == 'DefaultConstraintViolation{rootBean=class io.micronaut.validation.validator.$BookService$Definition$Intercepted, invalidValue=50, path=saveBook.pages}' - constraintViolations[1].toString() == 'DefaultConstraintViolation{rootBean=class io.micronaut.validation.validator.$BookService$Definition$Intercepted, invalidValue=, path=saveBook.title}' - } - - void "test @Introspected is required to validate the bean"() { - when: - applicationContext.getBean(A) - then: - BeanInstantiationException e = thrown() - e.message.contains('''Cannot validate bean [io.micronaut.validation.validator.A]. No bean introspection present. Please add @Introspected.''') - and: - ClassUtils.forName('io.micronaut.validation.validator.$A$Definition', getClass().getClassLoader()).isPresent() - ClassUtils.forName('io.micronaut.validation.validator.$A$Definition$Intercepted', getClass().getClassLoader()).isEmpty() - } - - void "test @Introspected is required to validate the bean and it's intercepted if one of the methods requires validation"() { - when: - def beanB = applicationContext.getBean(B) - then: - BeanInstantiationException e = thrown() - e.message.contains('''number - must be less than or equal to 20''') - and: - ClassUtils.forName('io.micronaut.validation.validator.$B$Definition', getClass().getClassLoader()).isPresent() - ClassUtils.forName('io.micronaut.validation.validator.$B$Definition$Intercepted', getClass().getClassLoader()).isPresent() - } - - void "test @Introspected is required to validate the bean and it's intercepted if one of the methods requires validation 2"() { - when: - def beanC = applicationContext.getBean(C) - then: - beanC.number == 40 - when: - beanC.updateNumber(100) - then: - Exception e = thrown() - e.message.contains('''updateNumber.number: must be less than or equal to 50''') - beanC.number == 40 -// when: -// beanC.number = 100 -// then: -// e = thrown() -// e.message.contains('''updateNumber.number: must be less than or equal to 50''') -// beanC.number == 40 - and: - ClassUtils.forName('io.micronaut.validation.validator.$C$Definition', getClass().getClassLoader()).isPresent() - ClassUtils.forName('io.micronaut.validation.validator.$C$Definition$Intercepted', getClass().getClassLoader()).isPresent() - } -} - -@Introspected -class HiveOfBeeMap { - @Valid - Map bees -} - -@Introspected -class HiveOfBeeList { - @Valid - List bees -} - -// not introspected, expect validation failure -class Bee { - @NotBlank - String name -} - -// FIXME see https://github.com/micronaut-projects/micronaut-core/issues/4410 -// demonstrated by "test fail elements in String list" -// List, Set and array all work same -// 1) With Valid and constraints, validation is broken because it also applies @Size constraint to the container itself -// 2) Without @Valid only the container itself is validated for given constraints (makes sense) -@Introspected -class ListOfStrings { - @Valid - @Size(min=1, max=2) - @NotNull - List strings -} - -@Introspected -class ObjectArray { - @Size(min = 1, max = 2) - String[] strings - - @Size(min = 1, max = 2) - Long[] numbers -} - -@Introspected -class Book { - @NotBlank - String title - - @Min(100l) - int pages - - @Valid - @NotNull - Author primaryAuthor - - @Size(min = 1, max = 10) - @Valid - List authors = [] -} - -@Introspected -class Author { - @NotBlank - String name - @Max(100l) - Integer age - - @Valid - Book favouriteBook -} - -@Introspected -@Prototype -class ArrayTest { - @Valid - @Max(20l) - @NotNull - int[] integers - - @Valid - ArrayTest child - - @Executable - void saveChild(@Valid ArrayTest arrayTest) { - - } - - @Executable - ArrayTest saveIntArray(@Valid - @Max(20l) - @NotNull int[] integers) { - new ArrayTest(integers: integers) - } -} - -@Singleton -class A { - - @Valid - @Max(20l) - @NotNull - @Value('${a.number}') - Integer number -} - -@Introspected -@Singleton -class B { - - @Valid - @Max(20l) - @NotNull - @Value('${a.number}') - Integer number - - void updateNumber(@Max(20l) - @NotNull - Integer number) { - this.number = number - } -} - -@Introspected -@Singleton -class C { - - @Valid - @Max(50l) - @NotNull - @Value('${a.number}') - Integer number - - void updateNumber(@Max(50l) - @NotNull - Integer number) { - this.number = number - } -} - -@Singleton -class BookService { - @Executable - Book saveBook(@NotBlank String title, @Min(100l) int pages) { - new Book(title: title, pages: pages) - } -} - -@Introspected -class EnumList { - - @Valid - @NotNull - List enums -} - -enum AuthorState { - PUBLISHED, - DRAFT -} diff --git a/validation/src/test/groovy/io/micronaut/validation/validator/ast/SomeAnn.java b/validation/src/test/groovy/io/micronaut/validation/validator/ast/SomeAnn.java deleted file mode 100644 index 6aa5d3f146b..00000000000 --- a/validation/src/test/groovy/io/micronaut/validation/validator/ast/SomeAnn.java +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright 2017-2020 original authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.micronaut.validation.validator.ast; - -import javax.validation.constraints.NotBlank; -import java.lang.annotation.Documented; -import java.lang.annotation.Retention; - -import static java.lang.annotation.RetentionPolicy.RUNTIME; - -@Documented -@Retention(RUNTIME) -public @interface SomeAnn { - @NotBlank - @SomeAnn("good") - String value(); -} diff --git a/validation/src/test/groovy/io/micronaut/validation/validator/ast/ValidateAnnotationMetadataSpec.groovy b/validation/src/test/groovy/io/micronaut/validation/validator/ast/ValidateAnnotationMetadataSpec.groovy deleted file mode 100644 index 34a95a360c3..00000000000 --- a/validation/src/test/groovy/io/micronaut/validation/validator/ast/ValidateAnnotationMetadataSpec.groovy +++ /dev/null @@ -1,142 +0,0 @@ -package io.micronaut.validation.validator.ast - -import io.micronaut.annotation.processing.TypeElementVisitorProcessor -import io.micronaut.annotation.processing.test.AbstractTypeElementSpec -import io.micronaut.annotation.processing.test.JavaParser -import io.micronaut.inject.beans.visitor.IntrospectedTypeElementVisitor -import io.micronaut.inject.visitor.TypeElementVisitor - -import javax.annotation.processing.SupportedAnnotationTypes - -class ValidateAnnotationMetadataSpec extends AbstractTypeElementSpec { - - void "test validate annotation values on type with invalid constant"() { - when: - buildBeanIntrospection('test.Test', ''' -package test; -@io.micronaut.validation.validator.ast.SomeAnn(Test.VAL) // blank not allowed -@io.micronaut.core.annotation.Introspected -class Test { - public static final String VAL = ""; -} -''') - - then: - def e = thrown(RuntimeException) - e.message == '''test/Test.java:5: error: @SomeAnn.value: must not be blank -class Test { -^''' - - } - - void "test validate annotation values on type"() { - when: - buildBeanIntrospection('test.Test', ''' -package test; -@io.micronaut.validation.validator.ast.SomeAnn("") // blank not allowed -@io.micronaut.core.annotation.Introspected -class Test { - private String name; - public String getName() { - return this.name; - } - public Test setName(String n) { - this.name = n; - return this; - } -} -''') - - then: - def e = thrown(RuntimeException) - e.message == '''test/Test.java:5: error: @SomeAnn.value: must not be blank -class Test { -^''' - - } - - void "test validate annotation values on field"() { - when: - buildBeanIntrospection('test.Test', ''' -package test; - -@io.micronaut.core.annotation.Introspected -class Test { - @io.micronaut.validation.validator.ast.SomeAnn("") // blank not allowed - private String name; -} -''') - - then: - def e = thrown(RuntimeException) - e.message == '''test/Test.java:7: error: @SomeAnn.value: must not be blank - private String name; - ^''' - - } - - void "test validate annotation values on method"() { - when: - buildBeanIntrospection('test.Test', ''' -package test; - -@io.micronaut.core.annotation.Introspected -class Test { - @io.micronaut.validation.validator.ast.SomeAnn("") // blank not allowed - String getName() { - return null; - }; -} -''') - - then: - def e = thrown(RuntimeException) - e.message == '''test/Test.java:7: error: @SomeAnn.value: must not be blank - String getName() { - ^''' - - } - - void "test validate annotation values on parameter"() { - when: - buildBeanDefinition('test.Test', ''' -package test; - -import io.micronaut.validation.validator.ast.*; - -@jakarta.inject.Singleton -class Test { - - @io.micronaut.context.annotation.Executable - String getName(@SomeAnn("") String n) { - return null; - } -} -''') - - then: - def e = thrown(RuntimeException) - e.message == '''test/Test.java:10: error: @SomeAnn.value: must not be blank - String getName(@SomeAnn("") String n) { - ^''' - - } - - @Override - protected JavaParser newJavaParser() { - return new JavaParser() { - @Override - protected TypeElementVisitorProcessor getTypeElementVisitorProcessor() { - return new MyTypeElementVisitorProcessor() - } - } - } - - @SupportedAnnotationTypes("*") - static class MyTypeElementVisitorProcessor extends TypeElementVisitorProcessor { - @Override - protected Collection findTypeElementVisitors() { - return [new IntrospectedTypeElementVisitor()] - } - } -} diff --git a/validation/src/test/groovy/io/micronaut/validation/validator/constraints/ConstraintMessagesSpec.groovy b/validation/src/test/groovy/io/micronaut/validation/validator/constraints/ConstraintMessagesSpec.groovy deleted file mode 100644 index f4a00b21a34..00000000000 --- a/validation/src/test/groovy/io/micronaut/validation/validator/constraints/ConstraintMessagesSpec.groovy +++ /dev/null @@ -1,104 +0,0 @@ -package io.micronaut.validation.validator.constraints - -import io.micronaut.annotation.processing.TypeElementVisitorProcessor -import io.micronaut.annotation.processing.test.AbstractTypeElementSpec -import io.micronaut.annotation.processing.test.JavaParser -import io.micronaut.context.ApplicationContext -import io.micronaut.inject.beans.visitor.IntrospectedTypeElementVisitor -import io.micronaut.inject.visitor.TypeElementVisitor -import io.micronaut.validation.validator.Validator -import spock.lang.AutoCleanup -import spock.lang.Shared -import spock.lang.Unroll - -import javax.annotation.processing.SupportedAnnotationTypes -import javax.validation.constraints.* - -class ConstraintMessagesSpec extends AbstractTypeElementSpec { - - @Shared - @AutoCleanup - ApplicationContext context = ApplicationContext.run() - - @Shared - Validator validator = context.getBean(Validator) - - - @Unroll - void "test validation message for #annotation"() { - given: - def introspection = buildBeanIntrospection('test.Test', """ -package test; - -@io.micronaut.core.annotation.Introspected -class Test { - @${annotation.name}(${ - attributes ? attributes.entrySet().collect { it.key + '=' + getValString(it) }.join(',') : '' - }) - private ${type.name} field; - - public ${type.name} getField() { - return field; - } - - public void setField(${type.name} f) { - this.field = f; - } -} -""") - def instance = introspection.instantiate() - def prop = introspection.getProperty("field", type).get() - prop.set(instance, value) - def constraintViolations = validator.validate(introspection, instance) - - expect: - constraintViolations.size() == 1 - constraintViolations.iterator().next().message == message - - - where: - annotation | attributes | type | value | message - AssertFalse | null | Boolean | true | "must be false" - AssertFalse | [message: "should be false!!"] | Boolean | true | "should be false!!" - AssertTrue | null | Boolean | false | "must be true" - DecimalMax | [value: '1.0'] | String | '1.1' | "must be less than or equal to 1.0" - DecimalMax | [value: '1.0', message: "{validatedValue} exceeds max: {value}"] | String | '1.1' | "1.1 exceeds max: 1.0" - DecimalMin | [value: '1.0'] | String | '0.9' | "must be greater than or equal to 1.0" - Digits | [integer: 2, fraction: 2] | String | '110.20' | "numeric value out of bounds (<2 digits>.<2 digits> expected)" - Email | null | String | 'junk' | "must be a well-formed email address" - Max | [value: 10] | Integer | 20 | "must be less than or equal to 10" - Max | [value: 10, message: "{validatedValue} is too big! max: {value}"] | Integer | 20 | "20 is too big! max: 10" - Min | [value: 10] | Integer | 5 | "must be greater than or equal to 10" - Size | [min: 10, max: 20] | List | [1] | "size must be between 10 and 20" - NotBlank | null | String | '' | "must not be blank" - NotEmpty | null | List | [] | "must not be empty" - NotNull | null | List | null | "must not be null" - Null | null | List | [] | "must be null" - } - - private String getValString(Map.Entry it) { - def v = it.value - if (v instanceof String) { - return "\"$v\"" - } - return v.inspect() - } - - @Override - protected JavaParser newJavaParser() { - return new JavaParser() { - @Override - protected TypeElementVisitorProcessor getTypeElementVisitorProcessor() { - return new MyTypeElementVisitorProcessor() - } - } - } - - @SupportedAnnotationTypes("*") - static class MyTypeElementVisitorProcessor extends TypeElementVisitorProcessor { - @Override - protected Collection findTypeElementVisitors() { - return [new IntrospectedTypeElementVisitor()] - } - } -} diff --git a/validation/src/test/groovy/io/micronaut/validation/validator/constraints/ConstraintsSpec.groovy b/validation/src/test/groovy/io/micronaut/validation/validator/constraints/ConstraintsSpec.groovy deleted file mode 100644 index ad9490bd4cf..00000000000 --- a/validation/src/test/groovy/io/micronaut/validation/validator/constraints/ConstraintsSpec.groovy +++ /dev/null @@ -1,313 +0,0 @@ -package io.micronaut.validation.validator.constraints - -import io.micronaut.annotation.processing.test.AbstractTypeElementSpec -import io.micronaut.context.ApplicationContext -import io.micronaut.core.annotation.AnnotationValue -import io.micronaut.validation.validator.DefaultClockProvider -import spock.lang.AutoCleanup -import spock.lang.Shared -import spock.lang.Unroll - -import javax.validation.ClockProvider -import javax.validation.constraints.* -import java.time.Clock -import java.time.Instant -import java.time.LocalDateTime -import java.time.ZoneId -import java.time.ZoneOffset - -import static java.math.BigInteger.ONE - -class ConstraintsSpec extends AbstractTypeElementSpec { - @Shared - @AutoCleanup - ApplicationContext context = ApplicationContext.run() - @Shared - ConstraintValidatorRegistry reg = context.getBean(ConstraintValidatorRegistry) - - @Unroll - void "test #constraint constraint for value [#value]"() { - given: - def context = Mock(ConstraintValidatorContext) - context.getClockProvider() >> new DefaultClockProvider() - def validator = reg.getConstraintValidator(constraint, value?.getClass() ?: Object) - - expect: - validator.isValid(value, metadata, context) == isValid - - where: - constraint | value | isValid | metadata - AssertTrue | null | true | new AnnotationValue<>(constraint.getName()) - AssertTrue | true | true | new AnnotationValue<>(constraint.getName()) - AssertTrue | false | false | new AnnotationValue<>(constraint.getName()) - AssertFalse | null | true | new AnnotationValue<>(constraint.getName()) - AssertFalse | true | false | new AnnotationValue<>(constraint.getName()) - AssertFalse | false | true | new AnnotationValue<>(constraint.getName()) - NotNull | null | false | new AnnotationValue<>(constraint.getName()) - NotNull | "" | true | new AnnotationValue<>(constraint.getName()) - Null | null | true | new AnnotationValue<>(constraint.getName()) - Null | "" | false | new AnnotationValue<>(constraint.getName()) - NotBlank | "" | false | new AnnotationValue<>(constraint.getName()) - NotBlank | null | false | new AnnotationValue<>(constraint.getName()) - NotBlank | " " | false | new AnnotationValue<>(constraint.getName()) - NotBlank | "foo" | true | new AnnotationValue<>(constraint.getName()) - NotEmpty | "" | false | new AnnotationValue<>(constraint.getName()) - NotEmpty | [] | false | new AnnotationValue<>(constraint.getName()) - NotEmpty | null | false | new AnnotationValue<>(constraint.getName()) - NotEmpty | [] as String[] | false | new AnnotationValue<>(constraint.getName()) - NotEmpty | [] as int[] | false | new AnnotationValue<>(constraint.getName()) - NotEmpty | [1] as int[] | true | new AnnotationValue<>(constraint.getName()) - NotEmpty | [1] as short[] | true | new AnnotationValue<>(constraint.getName()) - NotEmpty | [] as short[] | false | new AnnotationValue<>(constraint.getName()) - NotEmpty | [] as byte[] | false | new AnnotationValue<>(constraint.getName()) - NotEmpty | [1] as byte[] | true | new AnnotationValue<>(constraint.getName()) - NotEmpty | [] as long[] | false | new AnnotationValue<>(constraint.getName()) - NotEmpty | [1] as long[] | true | new AnnotationValue<>(constraint.getName()) - NotEmpty | [] as double[] | false | new AnnotationValue<>(constraint.getName()) - NotEmpty | [1] as double[] | true | new AnnotationValue<>(constraint.getName()) - NotEmpty | [] as float[] | false | new AnnotationValue<>(constraint.getName()) - NotEmpty | [1] as float[] | true | new AnnotationValue<>(constraint.getName()) - NotEmpty | [] as char[] | false | new AnnotationValue<>(constraint.getName()) - NotEmpty | [1] as char[] | true | new AnnotationValue<>(constraint.getName()) - NotEmpty | [:] | false | new AnnotationValue<>(constraint.getName()) - NotEmpty | [foo: 'bar'] | true | new AnnotationValue<>(constraint.getName()) - NotEmpty | [1] | true | new AnnotationValue<>(constraint.getName()) - Negative | -1 | true | new AnnotationValue<>(constraint.getName()) - Negative | -100 | true | new AnnotationValue<>(constraint.getName()) - Negative | -100 as double | true | new AnnotationValue<>(constraint.getName()) - Negative | -0.001 as double | true | new AnnotationValue<>(constraint.getName()) - Negative | -100 as long | true | new AnnotationValue<>(constraint.getName()) - Negative | Integer.MIN_VALUE - 1L as long | true | new AnnotationValue<>(constraint.getName()) - Negative | Integer.MAX_VALUE + 1L as long | false | new AnnotationValue<>(constraint.getName()) - Negative | -100 as float | true | new AnnotationValue<>(constraint.getName()) - Negative | -100 as short | true | new AnnotationValue<>(constraint.getName()) - Negative | -100 as byte | true | new AnnotationValue<>(constraint.getName()) - Negative | new BigInteger("-100") | true | new AnnotationValue<>(constraint.getName()) - Negative | BigInteger.valueOf(Long.MIN_VALUE).subtract(ONE) | true | new AnnotationValue<>(constraint.getName()) - Negative | new BigDecimal("-100") | true | new AnnotationValue<>(constraint.getName()) - Negative | new BigDecimal("-0.01") | true | new AnnotationValue<>(constraint.getName()) - Negative | new BigInteger("100") | false | new AnnotationValue<>(constraint.getName()) - Negative | BigInteger.valueOf(Long.MAX_VALUE).add(ONE) | false | new AnnotationValue<>(constraint.getName()) - Negative | new BigDecimal("100") | false | new AnnotationValue<>(constraint.getName()) - Negative | BigDecimal.ZERO | false | new AnnotationValue<>(constraint.getName()) - Negative | 0 | false | new AnnotationValue<>(constraint.getName()) - NegativeOrZero | -1 | true | new AnnotationValue<>(constraint.getName()) - NegativeOrZero | -100 | true | new AnnotationValue<>(constraint.getName()) - NegativeOrZero | -100 as double | true | new AnnotationValue<>(constraint.getName()) - NegativeOrZero | -0.001 as double | true | new AnnotationValue<>(constraint.getName()) - NegativeOrZero | 0.001 as double | false | new AnnotationValue<>(constraint.getName()) - NegativeOrZero | -100 as long | true | new AnnotationValue<>(constraint.getName()) - NegativeOrZero | Integer.MIN_VALUE - 1L as long | true | new AnnotationValue<>(constraint.getName()) - NegativeOrZero | Integer.MAX_VALUE + 1L as long | false | new AnnotationValue<>(constraint.getName()) - NegativeOrZero | -100 as float | true | new AnnotationValue<>(constraint.getName()) - NegativeOrZero | -100 as short | true | new AnnotationValue<>(constraint.getName()) - NegativeOrZero | -100 as byte | true | new AnnotationValue<>(constraint.getName()) - NegativeOrZero | new BigInteger("-100") | true | new AnnotationValue<>(constraint.getName()) - NegativeOrZero | BigInteger.valueOf(Long.MIN_VALUE).subtract(ONE) | true | new AnnotationValue<>(constraint.getName()) - NegativeOrZero | new BigDecimal("-100") | true | new AnnotationValue<>(constraint.getName()) - NegativeOrZero | BigDecimal.ZERO | true | new AnnotationValue<>(constraint.getName()) - NegativeOrZero | new BigInteger("100") | false | new AnnotationValue<>(constraint.getName()) - NegativeOrZero | BigInteger.valueOf(Long.MAX_VALUE).add(ONE) | false | new AnnotationValue<>(constraint.getName()) - NegativeOrZero | new BigDecimal("100") | false | new AnnotationValue<>(constraint.getName()) - NegativeOrZero | new BigDecimal("0.01") | false | new AnnotationValue<>(constraint.getName()) - NegativeOrZero | 0 | true | new AnnotationValue<>(constraint.getName()) - // Positive - Positive | 1 | true | new AnnotationValue<>(constraint.getName()) - Positive | 100 | true | new AnnotationValue<>(constraint.getName()) - Positive | 100 as double | true | new AnnotationValue<>(constraint.getName()) - Positive | 0.001 as double | true | new AnnotationValue<>(constraint.getName()) - Positive | 100 as long | true | new AnnotationValue<>(constraint.getName()) - Positive | Integer.MAX_VALUE + 1L as long | true | new AnnotationValue<>(constraint.getName()) - Positive | 100 as float | true | new AnnotationValue<>(constraint.getName()) - Positive | 100 as short | true | new AnnotationValue<>(constraint.getName()) - Positive | 100 as byte | true | new AnnotationValue<>(constraint.getName()) - Positive | new BigInteger("100") | true | new AnnotationValue<>(constraint.getName()) - Positive | BigInteger.valueOf(Long.MAX_VALUE).add(ONE) | true | new AnnotationValue<>(constraint.getName()) - Positive | new BigDecimal("100") | true | new AnnotationValue<>(constraint.getName()) - Positive | new BigDecimal("0.01") | true | new AnnotationValue<>(constraint.getName()) - Positive | new BigInteger("-100") | false | new AnnotationValue<>(constraint.getName()) - Positive | new BigDecimal("-100") | false | new AnnotationValue<>(constraint.getName()) - Positive | BigDecimal.ZERO | false | new AnnotationValue<>(constraint.getName()) - Positive | 0 | false | new AnnotationValue<>(constraint.getName()) - Positive | -100 as double | false | new AnnotationValue<>(constraint.getName()) - Positive | -100 as long | false | new AnnotationValue<>(constraint.getName()) - Positive | Integer.MIN_VALUE - 1L as long | false | new AnnotationValue<>(constraint.getName()) - Positive | BigInteger.valueOf(Long.MIN_VALUE).subtract(ONE) | false | new AnnotationValue<>(constraint.getName()) - Positive | -100 as float | false | new AnnotationValue<>(constraint.getName()) - Positive | -100 as short | false | new AnnotationValue<>(constraint.getName()) - Positive | -100 as byte | false | new AnnotationValue<>(constraint.getName()) - // PositiveOrZero - PositiveOrZero | 1 | true | new AnnotationValue<>(constraint.getName()) - PositiveOrZero | null | true | new AnnotationValue<>(constraint.getName()) - PositiveOrZero | 0 | true | new AnnotationValue<>(constraint.getName()) - PositiveOrZero | -1 | false | new AnnotationValue<>(constraint.getName()) - PositiveOrZero | 100 | true | new AnnotationValue<>(constraint.getName()) - PositiveOrZero | 100 as double | true | new AnnotationValue<>(constraint.getName()) - PositiveOrZero | 100 as long | true | new AnnotationValue<>(constraint.getName()) - PositiveOrZero | Integer.MAX_VALUE + 1L as long | true | new AnnotationValue<>(constraint.getName()) - PositiveOrZero | 100 as float | true | new AnnotationValue<>(constraint.getName()) - PositiveOrZero | 100 as short | true | new AnnotationValue<>(constraint.getName()) - PositiveOrZero | 100 as byte | true | new AnnotationValue<>(constraint.getName()) - PositiveOrZero | new BigInteger("100") | true | new AnnotationValue<>(constraint.getName()) - PositiveOrZero | BigInteger.valueOf(Long.MAX_VALUE).add(ONE) | true | new AnnotationValue<>(constraint.getName()) - PositiveOrZero | new BigDecimal("100") | true | new AnnotationValue<>(constraint.getName()) - PositiveOrZero | BigDecimal.ZERO | true | new AnnotationValue<>(constraint.getName()) - PositiveOrZero | new BigInteger("-100") | false | new AnnotationValue<>(constraint.getName()) - PositiveOrZero | BigInteger.valueOf(Long.MIN_VALUE).subtract(ONE) | false | new AnnotationValue<>(constraint.getName()) - PositiveOrZero | new BigDecimal("-100") | false | new AnnotationValue<>(constraint.getName()) - PositiveOrZero | new BigDecimal("-0.01") | false | new AnnotationValue<>(constraint.getName()) - PositiveOrZero | -100 as double | false | new AnnotationValue<>(constraint.getName()) - PositiveOrZero | -0.001 as double | false | new AnnotationValue<>(constraint.getName()) - PositiveOrZero | -100 as long | false | new AnnotationValue<>(constraint.getName()) - PositiveOrZero | Integer.MIN_VALUE - 1L as long | false | new AnnotationValue<>(constraint.getName()) - PositiveOrZero | -100 as float | false | new AnnotationValue<>(constraint.getName()) - PositiveOrZero | -100 as short | false | new AnnotationValue<>(constraint.getName()) - PositiveOrZero | -100 as byte | false | new AnnotationValue<>(constraint.getName()) - // Max - Max | 10 | false | constraintMetadata(constraint, "@Max(5)") - Max | 5 | true | constraintMetadata(constraint, "@Max(5)") - Max | new BigInteger("6") | false | constraintMetadata(constraint, "@Max(5)") - Max | new BigDecimal("6") | false | constraintMetadata(constraint, "@Max(5)") - Max | new BigInteger("5") | true | constraintMetadata(constraint, "@Max(5)") - Max | new BigDecimal("5") | true | constraintMetadata(constraint, "@Max(5)") - Max | 0 | true | constraintMetadata(constraint, "@Max(5)") - Max | null | true | constraintMetadata(constraint, "@Max(5)") - // Min - Min | 10 | true | constraintMetadata(constraint, "@Min(5)") - Min | 5 | true | constraintMetadata(constraint, "@Min(5)") - Min | new BigInteger("10") | true | constraintMetadata(constraint, "@Min(5)") - Min | new BigDecimal("10") | true | constraintMetadata(constraint, "@Min(5)") - Min | 0 | false | constraintMetadata(constraint, "@Min(5)") - Min | null | true | constraintMetadata(constraint, "@Min(5)") - // Size - Size | null | true | constraintMetadata(constraint, "@Size(min=2, max=5)") - Size | "test" | true | constraintMetadata(constraint, "@Size(min=2, max=5)") - Size | "t" | false | constraintMetadata(constraint, "@Size(min=2, max=5)") - Size | "te" | true | constraintMetadata(constraint, "@Size(min=2, max=5)") - Size | "test1" | true | constraintMetadata(constraint, "@Size(min=2, max=5)") - Size | "test12" | false | constraintMetadata(constraint, "@Size(min=2, max=5)") - // Size Collection - Size | [1, 2, 3] | true | constraintMetadata(constraint, "@Size(min=2, max=5)") - Size | [1] | false | constraintMetadata(constraint, "@Size(min=2, max=5)") - Size | [1, 2] | true | constraintMetadata(constraint, "@Size(min=2, max=5)") - Size | [1, 2, 3, 4, 5] | true | constraintMetadata(constraint, "@Size(min=2, max=5)") - Size | [1, 2, 3, 4, 5, 6] | false | constraintMetadata(constraint, "@Size(min=2, max=5)") - // Size Map - Size | [a: 1, b: 2, c: 3] | true | constraintMetadata(constraint, "@Size(min=2, max=5)") - Size | [a: 1] | false | constraintMetadata(constraint, "@Size(min=2, max=5)") - Size | [a: 1, b: 2] | true | constraintMetadata(constraint, "@Size(min=2, max=5)") - Size | [a: 1, b: 2, c: 3, d: 4, e: 5] | true | constraintMetadata(constraint, "@Size(min=2, max=5)") - Size | [a: 1, b: 2, c: 3, d: 4, e: 5, f: 6] | false | constraintMetadata(constraint, "@Size(min=2, max=5)") - // DecimalMax - DecimalMax | null | true | constraintMetadata(constraint, "@DecimalMax(\"5\")") - DecimalMax | 10 | false | constraintMetadata(constraint, "@DecimalMax(\"5\")") - DecimalMax | new BigDecimal("10") | false | constraintMetadata(constraint, "@DecimalMax(\"5\")") - DecimalMax | 5 | true | constraintMetadata(constraint, "@DecimalMax(\"5\")") - DecimalMax | "10" | false | constraintMetadata(constraint, "@DecimalMax(\"5\")") - DecimalMax | "5" | true | constraintMetadata(constraint, "@DecimalMax(\"5\")") - DecimalMax | "5" | false | constraintMetadata(constraint, "@DecimalMax(value=\"5\",inclusive=false)") - // DecimalMin - DecimalMin | null | true | constraintMetadata(constraint, "@DecimalMin(\"5\")") - DecimalMin | 10 | true | constraintMetadata(constraint, "@DecimalMin(\"5\")") - DecimalMin | 3 | false | constraintMetadata(constraint, "@DecimalMin(\"5\")") - DecimalMin | new BigDecimal("10") | true | constraintMetadata(constraint, "@DecimalMin(\"5\")") - DecimalMin | 5 | true | constraintMetadata(constraint, "@DecimalMin(\"5\")") - DecimalMin | "10" | true | constraintMetadata(constraint, "@DecimalMin(\"5\")") - DecimalMin | "3" | false | constraintMetadata(constraint, "@DecimalMin(\"5\")") - DecimalMin | "5" | false | constraintMetadata(constraint, "@DecimalMin(value=\"5\",inclusive=false)") - // Pattern - Pattern | null | true | constraintMetadata(constraint, /@Pattern(regexp="\\d+")/) - Pattern | 'abc' | false | constraintMetadata(constraint, /@Pattern(regexp="\\d+")/) - Pattern | '123' | true | constraintMetadata(constraint, /@Pattern(regexp="\\d+")/) - // Digits - Digits | null | true | constraintMetadata(constraint, /@Digits(integer=2, fraction=2)/) - Digits | "10.15" | true | constraintMetadata(constraint, /@Digits(integer=2, fraction=2)/) - Digits | "110.15" | false | constraintMetadata(constraint, /@Digits(integer=2, fraction=2)/) - Digits | "10.150" | false | constraintMetadata(constraint, /@Digits(integer=2, fraction=2)/) - Digits | "110.150" | false | constraintMetadata(constraint, /@Digits(integer=2, fraction=2)/) - Digits | 10.15 | true | constraintMetadata(constraint, /@Digits(integer=2, fraction=2)/) - Digits | 110.15 | false | constraintMetadata(constraint, /@Digits(integer=2, fraction=2)/) - Digits | 10.150 | false | constraintMetadata(constraint, /@Digits(integer=2, fraction=2)/) - Digits | 110.150 | false | constraintMetadata(constraint, /@Digits(integer=2, fraction=2)/) - Digits | 10.15d | true | constraintMetadata(constraint, /@Digits(integer=2, fraction=2)/) - Digits | 110.15d | false | constraintMetadata(constraint, /@Digits(integer=2, fraction=2)/) - Digits | 10.150d | true | constraintMetadata(constraint, /@Digits(integer=2, fraction=2)/) - Digits | 110.150d | false | constraintMetadata(constraint, /@Digits(integer=2, fraction=2)/) - Digits | 10 | true | constraintMetadata(constraint, /@Digits(integer=2, fraction=2)/) - Digits | 110 | false | constraintMetadata(constraint, /@Digits(integer=2, fraction=2)/) - - // Email - Email | null | true | constraintMetadata(constraint, /@Email/) - Email | "junk" | false | constraintMetadata(constraint, /@Email/) - Email | "junk@junk.com" | true | constraintMetadata(constraint, /@Email/) - } - - @Unroll - void "test #constraint constraint for value dates [#value]"() { - given: - def context = Mock(ConstraintValidatorContext) - def fixedInstant = LocalDateTime.parse("2020-01-01T00:00:00").toInstant(ZoneOffset.UTC) - context.getClockProvider() >> new TestClockProvider(fixedInstant) - - def validator = reg.getConstraintValidator(constraint, value?.getClass() ?: Object) - - expect: - validator.isValid(value, metadata, context) == isValid - - where: - constraint | value | isValid | metadata - - // Past - Past | Date.from(LocalDateTime.parse("2019-12-30T00:00:00").toInstant(ZoneOffset.UTC)) | true | constraintMetadata(constraint, /@Past/) - Past | Date.from(LocalDateTime.parse("2020-02-01T00:00:00").toInstant(ZoneOffset.UTC)) | false | constraintMetadata(constraint, /@Past/) - Past | LocalDateTime.parse("2019-12-30T00:00:00").toInstant(ZoneOffset.UTC) | true | constraintMetadata(constraint, /@Past/) - Past | LocalDateTime.parse("2020-02-01T00:00:00").toInstant(ZoneOffset.UTC) | false | constraintMetadata(constraint, /@Past/) - Past | LocalDateTime.parse("2019-12-30T00:00:00") | true | constraintMetadata(constraint, /@Past/) - Past | LocalDateTime.parse("2020-02-01T00:00:00") | false | constraintMetadata(constraint, /@Past/) - Past | null | true | constraintMetadata(constraint, /@Past/) - - // Future - Future | Date.from(LocalDateTime.parse("2019-12-30T00:00:00").toInstant(ZoneOffset.UTC)) | false | constraintMetadata(constraint, /@Future/) - Future | Date.from(LocalDateTime.parse("2020-02-01T00:00:00").toInstant(ZoneOffset.UTC)) | true | constraintMetadata(constraint, /@Future/) - Future | LocalDateTime.parse("2019-12-30T00:00:00").toInstant(ZoneOffset.UTC) | false | constraintMetadata(constraint, /@Future/) - Future | LocalDateTime.parse("2020-02-01T00:00:00").toInstant(ZoneOffset.UTC) | true | constraintMetadata(constraint, /@Future/) - Future | LocalDateTime.parse("2019-12-30T00:00:00") | false | constraintMetadata(constraint, /@Future/) - Future | LocalDateTime.parse("2020-02-01T00:00:00") | true | constraintMetadata(constraint, /@Future/) - Future | null | true | constraintMetadata(constraint, /@Future/) - - // FutureOrPresent - FutureOrPresent | Date.from(LocalDateTime.parse("2019-12-30T00:00:00").toInstant(ZoneOffset.UTC)) | false| constraintMetadata(constraint, /@FutureOrPresent/) - FutureOrPresent | Date.from(LocalDateTime.parse("2020-01-01T00:00:00").toInstant(ZoneOffset.UTC)) | true | constraintMetadata(constraint, /@FutureOrPresent/) - FutureOrPresent | Date.from(LocalDateTime.parse("2020-01-02T00:00:00").toInstant(ZoneOffset.UTC)) | true | constraintMetadata(constraint, /@FutureOrPresent/) - FutureOrPresent | LocalDateTime.parse("2019-12-30T00:00:00").toInstant(ZoneOffset.UTC) | false| constraintMetadata(constraint, /@FutureOrPresent/) - FutureOrPresent | LocalDateTime.parse("2020-01-01T00:00:00").toInstant(ZoneOffset.UTC) | true | constraintMetadata(constraint, /@FutureOrPresent/) - FutureOrPresent | LocalDateTime.parse("2020-01-02T00:00:00").toInstant(ZoneOffset.UTC) | true | constraintMetadata(constraint, /@FutureOrPresent/) - FutureOrPresent | null | true | constraintMetadata(constraint, /@FutureOrPresent/) - - // PastOrPresent - PastOrPresent | Date.from(LocalDateTime.parse("2019-12-30T00:00:00").toInstant(ZoneOffset.UTC)) | true | constraintMetadata(constraint, /@PastOrPresent/) - PastOrPresent | Date.from(LocalDateTime.parse("2020-01-01T00:00:00").toInstant(ZoneOffset.UTC)) | true | constraintMetadata(constraint, /@PastOrPresent/) - PastOrPresent | Date.from(LocalDateTime.parse("2020-01-02T00:00:00").toInstant(ZoneOffset.UTC)) | false| constraintMetadata(constraint, /@PastOrPresent/) - PastOrPresent | LocalDateTime.parse("2019-12-30T00:00:00").toInstant(ZoneOffset.UTC) | true | constraintMetadata(constraint, /@PastOrPresent/) - PastOrPresent | LocalDateTime.parse("2020-01-01T00:00:00").toInstant(ZoneOffset.UTC) | true | constraintMetadata(constraint, /@PastOrPresent/) - PastOrPresent | LocalDateTime.parse("2020-01-02T00:00:00").toInstant(ZoneOffset.UTC) | false| constraintMetadata(constraint, /@PastOrPresent/) - PastOrPresent | null | true | constraintMetadata(constraint, /@PastOrPresent/) - } - - private class TestClockProvider implements ClockProvider { - final Instant instant - - TestClockProvider(Instant instant) { - this.instant = instant - } - - @Override - Clock getClock() { - return Clock.fixed(instant, ZoneId.of("UTC")) - } - } - - private AnnotationValue constraintMetadata(Class annotation, String ann) { - buildAnnotationMetadata(ann, "javax.validation.constraints").getAnnotation(annotation) - } -} diff --git a/validation/src/test/groovy/io/micronaut/validation/validator/constraints/custom/AlwaysInvalidConstraint.java b/validation/src/test/groovy/io/micronaut/validation/validator/constraints/custom/AlwaysInvalidConstraint.java deleted file mode 100644 index 959136e8efd..00000000000 --- a/validation/src/test/groovy/io/micronaut/validation/validator/constraints/custom/AlwaysInvalidConstraint.java +++ /dev/null @@ -1,22 +0,0 @@ -package io.micronaut.validation.validator.constraints.custom; - -import javax.validation.Constraint; -import java.lang.annotation.Documented; -import java.lang.annotation.Retention; -import java.lang.annotation.Target; - -import static java.lang.annotation.ElementType.TYPE; -import static java.lang.annotation.RetentionPolicy.RUNTIME; - -@Retention(RUNTIME) -@Constraint(validatedBy = { }) -@interface AlwaysInvalidConstraint { - String message() default "invalid"; - - @Target(TYPE) - @Retention(RUNTIME) - @Documented - @interface List { - AlwaysInvalidConstraint[] value(); - } -} diff --git a/validation/src/test/groovy/io/micronaut/validation/validator/constraints/custom/CustomConstraintsSpec.groovy b/validation/src/test/groovy/io/micronaut/validation/validator/constraints/custom/CustomConstraintsSpec.groovy deleted file mode 100644 index 487efdea150..00000000000 --- a/validation/src/test/groovy/io/micronaut/validation/validator/constraints/custom/CustomConstraintsSpec.groovy +++ /dev/null @@ -1,179 +0,0 @@ -package io.micronaut.validation.validator.constraints.custom - -import io.micronaut.context.ApplicationContext -import io.micronaut.core.annotation.Introspected -import io.micronaut.validation.validator.Validator -import spock.lang.AutoCleanup -import spock.lang.Shared -import spock.lang.Specification - -import javax.validation.Valid - -class CustomConstraintsSpec extends Specification { - - @Shared - @AutoCleanup - ApplicationContext applicationContext = ApplicationContext.run() - @Shared - Validator validator = applicationContext.getBean(Validator) - - void "test validation where pojo with inner custom constraint fails"() { - given: - TestInvalid testInvalid = new TestInvalid(invalidInner: new TestInvalid.InvalidInner()) - - when: - def violations = validator.validate(testInvalid) - - then: - violations.size() == 1 - violations[0].message == "invalid" - } - - void "test validation where pojo with outer custom constraint fails"() { - given: - TestInvalid testInvalid = new TestInvalid(invalidOuter: new InvalidOuter()) - - when: - def violations = validator.validate(testInvalid) - - then: - violations.size() == 1 - violations[0].message == "invalid" - } - - void "test validation where pojo with inner and outer custom constraint both fail"() { - given: - TestInvalid testInvalid = new TestInvalid( - invalidInner: new TestInvalid.InvalidInner(), - invalidOuter: new InvalidOuter()) - - when: - def violations = validator.validate(testInvalid) - - then: - violations.size() == 2 - violations[0].message == "invalid" - violations[1].message == "invalid" - } - - void "test validation where inner custom constraint fails"() { - given: - TestInvalid.InvalidInner invalidInner = new TestInvalid.InvalidInner() - - when: - def violations = validator.validate(invalidInner) - - then: - violations.size() == 1 - violations[0].message == "invalid" - } - - void "test validation where outer custom constraint fails"() { - given: - InvalidOuter invalidOuter = new InvalidOuter() - - when: - def violations = validator.validate(invalidOuter) - - then: - violations.size() == 1 - violations[0].message == "invalid" - } - - void "test validation where pojo with inner custom message constraint fails"() { - given: - CustomTestInvalid testInvalid = new CustomTestInvalid(invalidInner: new CustomTestInvalid.CustomInvalidInner()) - - when: - def violations = validator.validate(testInvalid) - - then: - violations.size() == 1 - violations[0].message == "custom invalid" - } - - void "test validation where pojo with outer custom message constraint fails"() { - given: - CustomTestInvalid testInvalid = new CustomTestInvalid(invalidOuter: new CustomInvalidOuter()) - - when: - def violations = validator.validate(testInvalid) - - then: - violations.size() == 1 - violations[0].message == "custom invalid" - } - - void "test validation where pojo with inner and outer custom message constraint both fail"() { - given: - CustomTestInvalid testInvalid = new CustomTestInvalid( - invalidInner: new CustomTestInvalid.CustomInvalidInner(), - invalidOuter: new CustomInvalidOuter()) - - when: - def violations = validator.validate(testInvalid) - - then: - violations.size() == 2 - violations[0].message == "custom invalid" - violations[1].message == "custom invalid" - } - - void "test validation where inner custom message constraint fails"() { - given: - CustomTestInvalid.CustomInvalidInner invalidInner = new CustomTestInvalid.CustomInvalidInner() - - when: - def violations = validator.validate(invalidInner) - - then: - violations.size() == 1 - violations[0].message == "custom invalid" - } - - void "test validation where outer custom message constraint fails"() { - given: - CustomInvalidOuter invalidOuter = new CustomInvalidOuter() - - when: - def violations = validator.validate(invalidOuter) - - then: - violations.size() == 1 - violations[0].message == "custom invalid" - } -} - -@Introspected -class TestInvalid { - @Valid - InvalidInner invalidInner - - @Valid - InvalidOuter invalidOuter - - @Introspected - @AlwaysInvalidConstraint - static class InvalidInner {} -} - -@Introspected -@AlwaysInvalidConstraint -class InvalidOuter {} - -@Introspected -class CustomTestInvalid { - @Valid - CustomInvalidInner invalidInner - - @Valid - CustomInvalidOuter invalidOuter - - @Introspected - @CustomMessageConstraint - static class CustomInvalidInner {} -} - -@Introspected -@CustomMessageConstraint -class CustomInvalidOuter {} \ No newline at end of file diff --git a/validation/src/test/groovy/io/micronaut/validation/validator/constraints/custom/CustomConstraintsValidatorFactory.java b/validation/src/test/groovy/io/micronaut/validation/validator/constraints/custom/CustomConstraintsValidatorFactory.java deleted file mode 100644 index c4385e5c3a7..00000000000 --- a/validation/src/test/groovy/io/micronaut/validation/validator/constraints/custom/CustomConstraintsValidatorFactory.java +++ /dev/null @@ -1,21 +0,0 @@ -package io.micronaut.validation.validator.constraints.custom; - -import io.micronaut.context.annotation.Factory; -import io.micronaut.validation.validator.constraints.ConstraintValidator; -import jakarta.inject.Singleton; - -@Factory -class CustomConstraintsValidatorFactory { - @Singleton - ConstraintValidator alwaysInvalidConstraintValidator() { - return (value, annotationMetadata, context) -> false; - } - - @Singleton - ConstraintValidator customMessageConstraintValidator() { - return (value, annotationMetadata, context) -> { - context.messageTemplate("custom invalid"); - return false; - }; - } -} diff --git a/validation/src/test/groovy/io/micronaut/validation/validator/constraints/custom/CustomMessageConstraint.java b/validation/src/test/groovy/io/micronaut/validation/validator/constraints/custom/CustomMessageConstraint.java deleted file mode 100644 index 126d976c301..00000000000 --- a/validation/src/test/groovy/io/micronaut/validation/validator/constraints/custom/CustomMessageConstraint.java +++ /dev/null @@ -1,12 +0,0 @@ -package io.micronaut.validation.validator.constraints.custom; - -import javax.validation.Constraint; -import java.lang.annotation.Retention; - -import static java.lang.annotation.RetentionPolicy.RUNTIME; - -@Retention(RUNTIME) -@Constraint(validatedBy = { }) -@interface CustomMessageConstraint { - String message() default "invalid"; -} diff --git a/validation/src/test/groovy/io/micronaut/validation/validator/customwithdefaultconstraints/EmployeeExperienceConstraint.java b/validation/src/test/groovy/io/micronaut/validation/validator/customwithdefaultconstraints/EmployeeExperienceConstraint.java deleted file mode 100644 index bf6a755bfb9..00000000000 --- a/validation/src/test/groovy/io/micronaut/validation/validator/customwithdefaultconstraints/EmployeeExperienceConstraint.java +++ /dev/null @@ -1,13 +0,0 @@ -package io.micronaut.validation.validator.customwithdefaultconstraints; - -import javax.validation.Constraint; -import java.lang.annotation.Retention; - -import static java.lang.annotation.RetentionPolicy.RUNTIME; - -@Retention(RUNTIME) -@Constraint(validatedBy = {}) -@interface EmployeeExperienceConstraint { - - String message() default "invalid"; -} diff --git a/validation/src/test/groovy/io/micronaut/validation/validator/customwithdefaultconstraints/EmployeeExperienceConstraintValidator.java b/validation/src/test/groovy/io/micronaut/validation/validator/customwithdefaultconstraints/EmployeeExperienceConstraintValidator.java deleted file mode 100644 index 02db044e23d..00000000000 --- a/validation/src/test/groovy/io/micronaut/validation/validator/customwithdefaultconstraints/EmployeeExperienceConstraintValidator.java +++ /dev/null @@ -1,21 +0,0 @@ -package io.micronaut.validation.validator.customwithdefaultconstraints; - -import io.micronaut.core.annotation.AnnotationValue; -import io.micronaut.validation.validator.constraints.ConstraintValidator; -import io.micronaut.validation.validator.constraints.ConstraintValidatorContext; -import jakarta.inject.Singleton; - -import java.util.Objects; - -@Singleton -public class EmployeeExperienceConstraintValidator implements ConstraintValidator { - - @Override - public boolean isValid(Employee value, AnnotationValue annotationMetadata, ConstraintValidatorContext context) { - if (Objects.nonNull(value) && value.getExperience() <= 20) { - context.messageTemplate("Experience Ineligible"); - return false; - } - return true; - } -} diff --git a/validation/src/test/groovy/io/micronaut/validation/validator/customwithdefaultconstraints/EmployeeValidationsSpec.groovy b/validation/src/test/groovy/io/micronaut/validation/validator/customwithdefaultconstraints/EmployeeValidationsSpec.groovy deleted file mode 100644 index 7dd85ed3873..00000000000 --- a/validation/src/test/groovy/io/micronaut/validation/validator/customwithdefaultconstraints/EmployeeValidationsSpec.groovy +++ /dev/null @@ -1,194 +0,0 @@ -package io.micronaut.validation.validator.customwithdefaultconstraints - -import io.micronaut.context.ApplicationContext -import io.micronaut.context.annotation.Executable -import io.micronaut.core.annotation.Introspected -import io.micronaut.core.annotation.Nullable -import io.micronaut.validation.validator.Validator -import jakarta.inject.Singleton -import spock.lang.AutoCleanup -import spock.lang.Issue -import spock.lang.Shared -import spock.lang.Specification - -import javax.validation.ConstraintViolation -import javax.validation.ConstraintViolationException -import javax.validation.Valid -import javax.validation.constraints.NotBlank -import javax.validation.constraints.NotEmpty -import javax.validation.constraints.NotNull -import java.util.stream.Collectors - -import static org.junit.jupiter.api.Assertions.assertEquals -import static org.junit.jupiter.api.Assertions.assertIterableEquals -import static org.junit.jupiter.api.Assertions.assertThrows - -@Issue("https://github.com/micronaut-projects/micronaut-core/issues/6519") -class EmployeeValidationsSpec extends Specification { - - @Shared - @AutoCleanup - ApplicationContext applicationContext = ApplicationContext.run() - - - void "test validations where both custom message constraint default validations fail"() { - given: - Validator validator = applicationContext.getBean(Validator.class) - Employee emp = new Employee(); - emp.setName(""); - emp.setExperience(10); - - Set messages = new HashSet<>(); - messages.add("must not be blank"); - messages.add("Experience Ineligible"); - messages.add("must not be null"); - - when: - final Set> constraintViolations = validator.validate(emp) - - then: - Set violationMessages = constraintViolations.stream().map(ConstraintViolation::getMessage).collect(Collectors.toSet()); - assertEquals(3, constraintViolations.size()); - assertIterableEquals(messages, violationMessages); - } - - void "test whether exceptions thrown when both custom message constraint default validations fail"() { - given: - EmployeeService employeeService = applicationContext.getBean(EmployeeService.class) - - Designation designation = new Designation(); - designation.setName(""); - - Employee emp = new Employee() - emp.setName("") - emp.setExperience(10) - emp.setDesignation(designation); - - when: - final ConstraintViolationException exception = - assertThrows(ConstraintViolationException.class, () -> - employeeService.startHoliday(emp) - ) - then: - String notBlankValidated = exception.getConstraintViolations().stream().filter(constraintViolation -> Objects.equals(constraintViolation.getPropertyPath().toString(), "startHoliday.emp.name")).map(ConstraintViolation::getMessage).findFirst().get(); - String experienceValidated = exception.getConstraintViolations().stream().filter(constraintViolation -> Objects.equals(constraintViolation.getPropertyPath().toString(), "startHoliday.emp")).map(ConstraintViolation::getMessage).findFirst().get(); - String notNullValidated = exception.getConstraintViolations().stream().filter(constraintViolation -> Objects.equals(constraintViolation.getPropertyPath().toString(), "startHoliday.emp.designation.name")).map(ConstraintViolation::getMessage).findFirst().get(); - assertEquals("must not be blank", notBlankValidated); - assertEquals("Experience Ineligible", experienceValidated); - assertEquals("must not be empty", notNullValidated); - - } - - void "test whether exceptions thrown when both custom message cascade constraint default validations fail"() { - given: - EmployeeService employeeService = applicationContext.getBean(EmployeeService.class) - - Employee alternateRepresentativeToBeContacted = new Employee() - alternateRepresentativeToBeContacted.setName("") - alternateRepresentativeToBeContacted.setExperience(20) - alternateRepresentativeToBeContacted.setDesignation(null); - - Designation designation = new Designation(); - designation.setName("Senior Manager"); - - Employee emp = new Employee() - emp.setName("") - emp.setExperience(20) - emp.setDesignation(designation); - emp.setAlternateRepresentative(alternateRepresentativeToBeContacted); - - when: - final ConstraintViolationException exception = - assertThrows(ConstraintViolationException.class, () -> - employeeService.startHoliday(emp) - ) - then: - String notBlankValidated = exception.getConstraintViolations().stream().filter(constraintViolation -> Objects.equals(constraintViolation.getPropertyPath().toString(), "startHoliday.emp.name")).map(ConstraintViolation::getMessage).findFirst().get(); - String experienceValidated = exception.getConstraintViolations().stream().filter(constraintViolation -> Objects.equals(constraintViolation.getPropertyPath().toString(), "startHoliday.emp")).map(ConstraintViolation::getMessage).findFirst().get(); - String alternateRepresentativeToBeContactedDesignationValidated = exception.getConstraintViolations().stream().filter(constraintViolation -> Objects.equals(constraintViolation.getPropertyPath().toString(), "startHoliday.emp.alternateRepresentative")).map(ConstraintViolation::getMessage).findFirst().get(); - assertEquals("must not be blank", notBlankValidated); - assertEquals("Experience Ineligible", experienceValidated); - assertEquals("Experience Ineligible", alternateRepresentativeToBeContactedDesignationValidated); - - } - -} - - -@Singleton -class EmployeeService { - @Executable - String startHoliday(@Valid Employee emp) { - return "Person " + emp.getName() - +" is eligible for sabbatical holiday as the person is of " + emp.getExperience() - +" years experienced. Please ensure his alternate representative contact " - +emp.getAlternateRepresentative().getName() - +" of designation " + emp.getAlternateRepresentative().getDesignation().getName() + " is aware of it"; - } -} - -@Introspected -@EmployeeExperienceConstraint -class Employee { - - private String name; - - private int experience; - - private Employee alternateRepresentative; - - @Valid - @NotNull - private Designation designation; - - @EmployeeExperienceConstraint - @Nullable - Employee getAlternateRepresentative() { - return alternateRepresentative - } - - void setAlternateRepresentative(Employee alternateRepresentative) { - this.alternateRepresentative = alternateRepresentative - } - - int getExperience() { - return experience; - } - - void setExperience(int experience) { - this.experience = experience; - } - - @NotBlank - String getName() { - return name; - } - - void setName(String name) { - this.name = name; - } - - Designation getDesignation() { - return designation - } - - void setDesignation(Designation designation) { - this.designation = designation - } -} - -@Introspected -class Designation { - - @NotEmpty - private String name; - - String getName() { - return name - } - - void setName(String name) { - this.name = name - } - -} diff --git a/validation/src/test/groovy/io/micronaut/validation/validator/map/MapValidatorSpec.groovy b/validation/src/test/groovy/io/micronaut/validation/validator/map/MapValidatorSpec.groovy deleted file mode 100644 index 5a5f9dd47b0..00000000000 --- a/validation/src/test/groovy/io/micronaut/validation/validator/map/MapValidatorSpec.groovy +++ /dev/null @@ -1,48 +0,0 @@ -package io.micronaut.validation.validator.map - -import io.micronaut.context.ApplicationContext -import io.micronaut.core.annotation.Introspected -import io.micronaut.validation.validator.Validator -import spock.lang.AutoCleanup -import spock.lang.Shared -import spock.lang.Specification - -import javax.validation.Valid -import javax.validation.constraints.NotBlank - -class MapValidatorSpec extends Specification { - @Shared - @AutoCleanup - ApplicationContext applicationContext = ApplicationContext.run() - @Shared - Validator validator = applicationContext.getBean(Validator) - - void "test cascade validate to map"() { - given: - Author a = new Author( - name: "Stephen King", - books: [it:new Book(title: "")] - ) - - when: - def constraintViolations = validator.validate(a) - - then: - constraintViolations.size() == 1 - constraintViolations.first().propertyPath.toString() == 'books[it].title' - } -} - -@Introspected -class Author { - String name - - @Valid - Map books -} - -@Introspected -class Book { - @NotBlank - String title -} diff --git a/validation/src/test/groovy/io/micronaut/validation/validator/pojo/ContextValidationSpec.groovy b/validation/src/test/groovy/io/micronaut/validation/validator/pojo/ContextValidationSpec.groovy deleted file mode 100644 index 12615332d7c..00000000000 --- a/validation/src/test/groovy/io/micronaut/validation/validator/pojo/ContextValidationSpec.groovy +++ /dev/null @@ -1,65 +0,0 @@ -package io.micronaut.validation.validator.pojo - -import io.micronaut.context.ApplicationContext -import io.micronaut.context.annotation.Factory -import io.micronaut.context.annotation.Requires -import io.micronaut.context.env.Environment -import io.micronaut.core.annotation.Introspected -import io.micronaut.validation.validator.Validator -import io.micronaut.validation.validator.constraints.ConstraintValidator -import io.micronaut.validation.validator.constraints.ConstraintValidatorContext -import jakarta.inject.Singleton -import spock.lang.AutoCleanup -import spock.lang.Shared -import spock.lang.Specification - -class ContextValidationSpec extends Specification { - - @Shared - @AutoCleanup - ApplicationContext applicationContext = ApplicationContext.run( - ['spec.name': 'contextValidationSpec'], - Environment.TEST - ) - - @Shared - Validator validator = applicationContext.getBean(Validator) - - void "test whether ConstraintValidator has access to validated object"() { - given: - FavoriteWebs2 favoriteWebs = new FavoriteWebs2(webs: ['aaaa']) - - expect: - !favoriteWebs.touched - def constraintViolations = validator.validate(favoriteWebs) - constraintViolations.size() == 0 - favoriteWebs.touched - } -} - - -@Introspected -class FavoriteWebs2 { - - boolean touched = false - - @ValidURLs - List webs - -} - -@Factory -@Requires(property = "spec.name", value = "contextValidationSpec") -class ValidURLsValidatorFactory2 { - - @Singleton - ConstraintValidator> validURLValidator() { - return { value, annotationMetadata, ConstraintValidatorContext context -> - if (context.rootBean instanceof FavoriteWebs2) { - (context.rootBean as FavoriteWebs2).touched = true - return true - } - return false - } - } -} diff --git a/validation/src/test/groovy/io/micronaut/validation/validator/pojo/FavoriteWebsSpec.groovy b/validation/src/test/groovy/io/micronaut/validation/validator/pojo/FavoriteWebsSpec.groovy deleted file mode 100644 index 7bbad3db4c6..00000000000 --- a/validation/src/test/groovy/io/micronaut/validation/validator/pojo/FavoriteWebsSpec.groovy +++ /dev/null @@ -1,66 +0,0 @@ -package io.micronaut.validation.validator.pojo - -import io.micronaut.context.ApplicationContext -import io.micronaut.context.annotation.Factory -import io.micronaut.context.annotation.Requires -import io.micronaut.context.env.Environment -import io.micronaut.core.annotation.Introspected -import io.micronaut.validation.validator.Validator -import io.micronaut.validation.validator.constraints.ConstraintValidator -import jakarta.inject.Singleton -import spock.lang.AutoCleanup -import spock.lang.Shared -import spock.lang.Specification - -import javax.validation.constraints.NotEmpty -import javax.validation.constraints.NotNull -import java.util.regex.Pattern - -class FavoriteWebsSpec extends Specification { - - @Shared - @AutoCleanup - ApplicationContext applicationContext = ApplicationContext.run( - ['spec.name': 'customValidatorField'], - Environment.TEST - ) - - @Shared - Validator validator = applicationContext.getBean(Validator) - - void "test custom constraint validator in a Field"() { - given: - FavoriteWebs favoriteWebs = new FavoriteWebs(webs: ['aaaa']) - - when: - def constraintViolations = validator.validate(favoriteWebs) - - then: - constraintViolations.size() == 1 - constraintViolations.first().message == "invalid url" - } -} - - -@Introspected -class FavoriteWebs { - - @NotNull - @NotEmpty - @ValidURLs - List webs -} - -@Factory -@Requires(property = "spec.name", value = "customValidatorField") -class ValidURLsValidatorFactory { - - private static final Pattern URL = Pattern.compile("(http:\\/\\/|https:\\/\\/)?(www.)?([a-zA-Z0-9]+).[a-zA-Z0-9]*.[a-z]{3}.?([a-z]+)?"); - - @Singleton - ConstraintValidator> validURLValidator() { - return { value, annotationMetadata, context -> - value != null && value.stream().allMatch { u -> URL.matcher(u).matches() } - } - } -} diff --git a/validation/src/test/groovy/io/micronaut/validation/validator/pojo/NameAndLastNameValidator.java b/validation/src/test/groovy/io/micronaut/validation/validator/pojo/NameAndLastNameValidator.java deleted file mode 100644 index 0bd87ec269d..00000000000 --- a/validation/src/test/groovy/io/micronaut/validation/validator/pojo/NameAndLastNameValidator.java +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright 2017-2020 original authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.micronaut.validation.validator.pojo; - -import javax.validation.Constraint; -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -@Retention(RetentionPolicy.RUNTIME) -@Target(ElementType.TYPE) -@Constraint(validatedBy = {}) -@interface NameAndLastNameValidator { - String message() default "Both name and lastName can''t be null"; -} diff --git a/validation/src/test/groovy/io/micronaut/validation/validator/pojo/PojoBodyParameterSpec.groovy b/validation/src/test/groovy/io/micronaut/validation/validator/pojo/PojoBodyParameterSpec.groovy deleted file mode 100644 index 406119fd17d..00000000000 --- a/validation/src/test/groovy/io/micronaut/validation/validator/pojo/PojoBodyParameterSpec.groovy +++ /dev/null @@ -1,250 +0,0 @@ -/* - * Copyright 2017-2019 original authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.micronaut.validation.validator.pojo - -import com.fasterxml.jackson.annotation.JsonSubTypes -import com.fasterxml.jackson.annotation.JsonTypeInfo -import io.micronaut.context.ApplicationContext -import io.micronaut.context.annotation.Requires -import io.micronaut.context.env.Environment -import io.micronaut.core.annotation.Introspected -import io.micronaut.core.type.Argument -import io.micronaut.http.HttpRequest -import io.micronaut.http.HttpResponse -import io.micronaut.http.HttpStatus -import io.micronaut.http.annotation.* -import io.micronaut.http.client.HttpClient -import io.micronaut.http.client.exceptions.HttpClientResponseException -import io.micronaut.runtime.server.EmbeddedServer -import io.micronaut.validation.Validated -import javax.validation.groups.Default -import spock.lang.AutoCleanup -import spock.lang.Shared -import spock.lang.Specification - -import javax.annotation.Nullable -import javax.validation.ConstraintViolationException -import javax.validation.Valid -import javax.validation.constraints.NotEmpty -import javax.validation.constraints.NotNull - -class PojoBodyParameterSpec extends Specification { - - @Shared - @AutoCleanup - EmbeddedServer embeddedServer = ApplicationContext.run(EmbeddedServer, - ['spec.name': 'customValidatorPOJO'], - Environment.TEST) - @Shared - @AutoCleanup - HttpClient client = embeddedServer.getApplicationContext().createBean(HttpClient, embeddedServer.getURL()) - - void 'test custom constraints in a Pojo are taken into account when it is used as a controller parameter'() { - given: - HttpRequest req = HttpRequest.POST("/search/", new Search()) - - when: - client.toBlocking().exchange(req, Argument.of(HttpResponse), Argument.of(HttpResponse)) - - then: - HttpClientResponseException e = thrown() - e.response.status() == HttpStatus.BAD_REQUEST - } - - void "test only sub properties are bound"() { - HttpRequest req = HttpRequest.POST("/search/sub", '{"name":"X","search":{"lastName":"Jones"}}') - - when: - Search search = client.toBlocking().retrieve(req, Search) - - then: - search.name == null - search.lastName == "Jones" - } - - void "should fail on missing name"() { - given: - HttpRequest req = HttpRequest.POST("/search/extended", '{"type":"NAME","requiredVal":"xxx"}') - - when: - client.toBlocking().exchange(req) - - then: - def e = thrown(HttpClientResponseException) - e.status == HttpStatus.BAD_REQUEST - } - - void "should not fail on missing name"() { - given: - HttpRequest req = HttpRequest.POST("/search/extended", '{"type":"NAME","requiredVal":"xxx", "name": "MyName"}') - - when: - def response = client.toBlocking().exchange(req) - - then: - response.status() == HttpStatus.OK - } - - void "should fail on missing requiredVal"() { - given: - HttpRequest req = HttpRequest.POST("/search/extended", '{"type":"NAME", "name": "MyName"}') - - when: - client.toBlocking().exchange(req) - - then: - def e = thrown(HttpClientResponseException) - e.status == HttpStatus.BAD_REQUEST - } - - void "should not fail on default validation group with missing nullable value"() { - given: - HttpRequest req = HttpRequest.POST("/search/extended", '{"type":"NULLABLE", "requiredVal":"xxx"}') - - when: - def response = client.toBlocking().exchange(req) - - then: - response.status() == HttpStatus.OK - } - - void "should fail on on default validation group with missing requiredVal"() { - given: - HttpRequest req = HttpRequest.POST("/search/extended", '{"type":"NULLABLE", "nullableValue": "value"}') - - when: - client.toBlocking().exchange(req) - - then: - def e = thrown(HttpClientResponseException) - e.status == HttpStatus.BAD_REQUEST - } - - void "should not fail on on default validation group with nothing missing"() { - given: - HttpRequest req = HttpRequest.POST("/search/extended", '{"type":"NULLABLE", "requiredVal":"xxx", "nullableValue": "value"}') - - when: - def response = client.toBlocking().exchange(req) - - then: - response.status() == HttpStatus.OK - } - - void "should fail on custom validation group with missing nullable value"() { - given: - HttpRequest req = HttpRequest.POST("/search/extended-group", '{"type":"NULLABLE", "requiredVal":"xxx"}') - - when: - client.toBlocking().exchange(req) - - then: - def e = thrown(HttpClientResponseException) - e.status == HttpStatus.BAD_REQUEST - } - - void "should fail on custom validation group with missing requiredVal"() { - given: - HttpRequest req = HttpRequest.POST("/search/extended-group", '{"type":"NULLABLE", "nullableValue": "value"}') - - when: - client.toBlocking().exchange(req) - - then: - def e = thrown(HttpClientResponseException) - e.status == HttpStatus.BAD_REQUEST - } - - void "should not fail on custom validation group with nothing missing"() { - given: - HttpRequest req = HttpRequest.POST("/search/extended-group", '{"type":"NULLABLE", "requiredVal":"xxx", "nullableValue": "value"}') - - when: - def response = client.toBlocking().exchange(req) - - then: - response.status() == HttpStatus.OK - } -} - - -@JsonTypeInfo( - use = JsonTypeInfo.Id.NAME, - property = "type" -) - -@Introspected -@JsonSubTypes([ - @JsonSubTypes.Type(value = ByName.class, name = "NAME"), - @JsonSubTypes.Type(value = ByAge.class, name = "AGE"), - @JsonSubTypes.Type(value = ByNullableValue.class, name = "NULLABLE")]) -abstract class SearchBy { - @NotEmpty - String requiredVal; - -} - -@Introspected -class ByName extends SearchBy { - @NotNull - String name -} - -@Introspected -class ByAge extends SearchBy { - @NotNull - Integer age; -} - -@Introspected -class ByNullableValue extends SearchBy { - @NotNull(groups = TestGroup) - String nullableValue; -} - -interface TestGroup extends Default {} - -@Controller("/search") -@Requires(property = "spec.name", value = "customValidatorPOJO") -class SearchController { - - @Post("/") - HttpResponse search(@Nullable @Header("X-Temp") String temp, @Body @NotNull @Valid Search search) { - return HttpResponse.ok() - } - - @Post("/sub") - HttpResponse search(@Body("search") Search search) { - return HttpResponse.ok(search) - } - - @Post("/extended") - HttpResponse extendedSearch(@Valid @Body SearchBy search) { - return HttpResponse.ok(search) - } - - @Post("/extended-group") - @Validated(groups = TestGroup) - HttpResponse extendedSearchWithValidationGroup(@Valid @Body SearchBy search) { - return HttpResponse.ok(search) - } - - @Error(exception = ConstraintViolationException.class) - HttpResponse validationError(ConstraintViolationException ex) { - return HttpResponse.badRequest() - } -} diff --git a/validation/src/test/groovy/io/micronaut/validation/validator/pojo/PojoConfigProps.java b/validation/src/test/groovy/io/micronaut/validation/validator/pojo/PojoConfigProps.java deleted file mode 100644 index 401ee9f4a0e..00000000000 --- a/validation/src/test/groovy/io/micronaut/validation/validator/pojo/PojoConfigProps.java +++ /dev/null @@ -1,23 +0,0 @@ -package io.micronaut.validation.validator.pojo; - -import io.micronaut.context.annotation.ConfigurationProperties; -import io.micronaut.validation.Pojo; - -import javax.validation.Valid; -import java.util.List; - -@ConfigurationProperties("test.valid") -public class PojoConfigProps { - - @Valid - private List pojos; - - public List getPojos() { - return pojos; - } - - public void setPojos(List pojos) { - this.pojos = pojos; - } - -} diff --git a/validation/src/test/groovy/io/micronaut/validation/validator/pojo/PojoConfigurationPropertiesSpec.groovy b/validation/src/test/groovy/io/micronaut/validation/validator/pojo/PojoConfigurationPropertiesSpec.groovy deleted file mode 100644 index fc4ae17a7cf..00000000000 --- a/validation/src/test/groovy/io/micronaut/validation/validator/pojo/PojoConfigurationPropertiesSpec.groovy +++ /dev/null @@ -1,23 +0,0 @@ -package io.micronaut.validation.validator.pojo - -import io.micronaut.context.ApplicationContext -import io.micronaut.context.exceptions.BeanInstantiationException -import spock.lang.Specification - -class PojoConfigurationPropertiesSpec extends Specification { - - void "test @Valid on config props property"() { - ApplicationContext context = ApplicationContext.run([ - 'test.valid.pojos': [ - [name: ''] - ] - ]) - - when: - context.getBean(PojoConfigProps) - - then: - def ex = thrown(BeanInstantiationException) - ex.message.contains("pojos[0].name - must not be blank") - } -} diff --git a/validation/src/test/groovy/io/micronaut/validation/validator/pojo/PojoValidatorSpec.groovy b/validation/src/test/groovy/io/micronaut/validation/validator/pojo/PojoValidatorSpec.groovy deleted file mode 100644 index 7f4fdbf8d5d..00000000000 --- a/validation/src/test/groovy/io/micronaut/validation/validator/pojo/PojoValidatorSpec.groovy +++ /dev/null @@ -1,118 +0,0 @@ -/* - * Copyright 2017-2019 original authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.micronaut.validation.validator.pojo - -import io.micronaut.context.ApplicationContext -import io.micronaut.context.annotation.Factory -import io.micronaut.context.annotation.Requires -import io.micronaut.context.env.Environment -import io.micronaut.core.annotation.Introspected -import io.micronaut.validation.validator.Validator -import io.micronaut.validation.validator.constraints.ConstraintValidator -import jakarta.inject.Singleton -import spock.lang.AutoCleanup -import spock.lang.Shared -import spock.lang.Specification - -import javax.validation.ConstraintViolationException -import javax.validation.Valid -import javax.validation.constraints.NotNull - -class PojoValidatorSpec extends Specification { - - @Shared - @AutoCleanup - ApplicationContext applicationContext = ApplicationContext.run( - ['spec.name': 'customValidatorPOJO'], - Environment.TEST - ) - - @Shared - Validator validator = applicationContext.getBean(Validator) - - void "test custom constraint validator on a Pojo"() { - given: - Search search = new Search() - - when: - def constraintViolations = validator.validate(search) - - then: - constraintViolations.size() == 1 - constraintViolations.first().message == "Both name and lastName can't be null" - } - - void "test custom constraint validator on a nested Pojo"() { - given: - SearchAny search = new SearchAny(new Search()) - - when: - def constraintViolations = validator.validate(search) - - then: - constraintViolations.size() == 1 - constraintViolations.first().message == "Both name and lastName can't be null" - } - - void "test custom constraint validator on pojo method argument"() { - when: - applicationContext.getBean(SearchAny2).validate(new Search()) - - then: - def ex = thrown(ConstraintViolationException) - ex.constraintViolations.size() == 1 - } -} - -@Introspected -@NameAndLastNameValidator -class Search { - String name - String lastName -} - -@Introspected -class SearchAny { - @Valid - List searches; - SearchAny(Search... searches) { - this.searches = searches; - } -} - -@Singleton -class SearchAny2 { - - void validate(@NotNull @Valid Search search) { - - } -} - -@Factory -@Requires(property = "spec.name", value = "customValidatorPOJO") -class NameAndLastNameValidatorFactory { - - @Singleton - ConstraintValidator nameAndLastNameValidator() { - return { value, annotationMetadata, context -> - Objects.requireNonNull(annotationMetadata) - Objects.requireNonNull(context) - value != null && (value.getName() != null || value.getLastName() != null) - } - } -} - diff --git a/validation/src/test/groovy/io/micronaut/validation/validator/pojo/ValidURLs.java b/validation/src/test/groovy/io/micronaut/validation/validator/pojo/ValidURLs.java deleted file mode 100644 index c1aeaf2a0ea..00000000000 --- a/validation/src/test/groovy/io/micronaut/validation/validator/pojo/ValidURLs.java +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright 2017-2020 original authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.micronaut.validation.validator.pojo; - -import javax.validation.Constraint; -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -@Constraint(validatedBy = {}) -@Retention(RetentionPolicy.RUNTIME) -@Target({ElementType.FIELD}) -public @interface ValidURLs { - String message() default "invalid url"; -} diff --git a/validation/src/test/groovy/io/micronaut/validation/validator/reactive/ReactiveMethodValidationSpec.groovy b/validation/src/test/groovy/io/micronaut/validation/validator/reactive/ReactiveMethodValidationSpec.groovy deleted file mode 100644 index d24f42902b9..00000000000 --- a/validation/src/test/groovy/io/micronaut/validation/validator/reactive/ReactiveMethodValidationSpec.groovy +++ /dev/null @@ -1,148 +0,0 @@ -package io.micronaut.validation.validator.reactive - -import io.micronaut.context.ApplicationContext -import io.micronaut.context.annotation.Executable -import io.micronaut.core.annotation.Introspected -import jakarta.inject.Singleton -import org.reactivestreams.Publisher -import reactor.core.publisher.Flux -import reactor.core.publisher.Mono -import spock.lang.AutoCleanup -import spock.lang.Shared -import spock.lang.Specification -import io.micronaut.core.async.annotation.SingleResult -import javax.validation.ConstraintViolationException -import javax.validation.Valid -import javax.validation.constraints.NotBlank -import java.util.concurrent.CompletableFuture -import java.util.concurrent.CompletionStage -import java.util.concurrent.ExecutionException - -class ReactiveMethodValidationSpec extends Specification { - - @Shared - @AutoCleanup - ApplicationContext applicationContext = ApplicationContext.run() - - void "test reactive return type validation"() { - given: - BookService bookService = applicationContext.getBean(BookService) - - when: - Mono.from(bookService.rxReturnInvalid(Mono.just(new Book(title: "It")))).block() - - then: - ConstraintViolationException e = thrown() - e.message == 'title: must not be blank' - e.getConstraintViolations().first().propertyPath.toString() == 'title' - } - - void "test reactive validation with invalid argument"() { - given: - BookService bookService = applicationContext.getBean(BookService) - - when: - Mono.from(bookService.rxValid(Mono.just(new Book(title: "")))).block() - - then: - ConstraintViolationException e = thrown() - e.message == 'rxValid.title: must not be blank' - e.getConstraintViolations().first().propertyPath.toString() == 'rxValid.title' - } - - void "test reactive validation with invalid simple argument"() { - given: - BookService bookService = applicationContext.getBean(BookService) - - when: - Mono.from(bookService.rxSimple(Mono.just(""))).block() - - then: - ConstraintViolationException e = thrown() - e.message == 'rxSimple.title: must not be blank' - e.getConstraintViolations().first().propertyPath.toString() == 'rxSimple.title' - } - - void "test future validation with invalid simple argument"() { - given: - BookService bookService = applicationContext.getBean(BookService) - - when: - bookService.futureSimple(CompletableFuture.completedFuture("")).get() - - then: - ExecutionException e = thrown() - - e.cause.message == 'futureSimple.title: must not be blank' - e.cause.getConstraintViolations().first().propertyPath.toString() == 'futureSimple.title' - } - - void "test future validation with invalid argument"() { - given: - BookService bookService = applicationContext.getBean(BookService) - - when: - bookService.futureValid(CompletableFuture.completedFuture(new Book(title: ""))).get() - - then: - ExecutionException e = thrown() - - e.cause.message == 'futureValid.title: must not be blank' - e.cause.getConstraintViolations().first().propertyPath.toString() == 'futureValid.title' - } - - void "test reactive validation with valid argument"() { - given: - BookService bookService = applicationContext.getBean(BookService) - - when: - Book book = Mono.from(bookService.rxValid(Mono.just(new Book(title: "It")))).block() - - then: - book.title == 'It' - } -} - -@Singleton -class BookService { - @Executable - @Valid - CompletionStage futureSimple(@NotBlank CompletionStage title) { - return title.thenApply({ String t -> new Book(title: t)}) - } - - @Executable - @Valid - CompletableFuture futureValid(@Valid CompletableFuture book) { - return book - } - - @Executable - @Valid - @SingleResult - Publisher rxSimple(@NotBlank Publisher title) { - return Flux.from(title).map({ String t -> new Book(title: t)}) - } - - @Executable - @Valid - @SingleResult - Publisher rxValid(@Valid Publisher book) { - return book - } - - @Executable - @Valid - @SingleResult - Publisher rxReturnInvalid(@Valid Publisher book) { - return Flux.from(book).map({ b -> b.title =''; return b}) - } - - -} - -@Introspected -class Book { - @NotBlank - String title -} diff --git a/validation/src/test/resources/logback.xml b/validation/src/test/resources/logback.xml deleted file mode 100644 index afaebf8e17d..00000000000 --- a/validation/src/test/resources/logback.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n - - - - - - - \ No newline at end of file From 87079a5ed05022d5bc41218fe9bff91606cf9307 Mon Sep 17 00:00:00 2001 From: micronaut-build <65172877+micronaut-build@users.noreply.github.com> Date: Wed, 1 Mar 2023 19:28:19 +0100 Subject: [PATCH 535/743] Bump micronaut-aws to 3.14.1 (#8867) --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index ad10259ab98..d7809c1d06c 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -67,7 +67,7 @@ managed-methvin-directory-watcher = "0.16.1" managed-micrometer = "1.10.3" managed-micronaut-acme = "3.2.0" managed-micronaut-aot = "1.1.1" -managed-micronaut-aws = "3.14.0" +managed-micronaut-aws = "3.14.1" managed-micronaut-azure = "3.8.1" managed-micronaut-cache = "3.5.0" managed-micronaut-cassandra = "5.1.1" From 479057543b8fe61935d91d04e7c566db3d5f1cb4 Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Wed, 1 Mar 2023 19:28:34 +0100 Subject: [PATCH 536/743] test: HTTP Server TCK Health endpoint (#8862) * build: add management dependency * test: add HealthTest --- http-server-tck/build.gradle.kts | 1 + .../tests/endpoints/health/HealthTest.java | 50 +++++++++++++++++++ 2 files changed, 51 insertions(+) create mode 100644 http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/endpoints/health/HealthTest.java diff --git a/http-server-tck/build.gradle.kts b/http-server-tck/build.gradle.kts index f8a0ff7a35b..5423c7997a6 100644 --- a/http-server-tck/build.gradle.kts +++ b/http-server-tck/build.gradle.kts @@ -11,6 +11,7 @@ dependencies { implementation(projects.validation) implementation(projects.runtime) implementation(projects.inject) + implementation(projects.management) api(projects.httpServer) api(libs.junit.jupiter.api) api(libs.junit.jupiter.params) diff --git a/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/endpoints/health/HealthTest.java b/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/endpoints/health/HealthTest.java new file mode 100644 index 00000000000..7593d99def8 --- /dev/null +++ b/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/endpoints/health/HealthTest.java @@ -0,0 +1,50 @@ +/* + * Copyright 2017-2023 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.http.server.tck.tests.endpoints.health; + +import io.micronaut.http.HttpRequest; +import io.micronaut.http.HttpStatus; +import io.micronaut.http.server.tck.AssertionUtils; +import io.micronaut.http.server.tck.HttpResponseAssertion; +import org.junit.jupiter.api.Test; + +import java.io.IOException; + +import static io.micronaut.http.server.tck.TestScenario.asserts; + +@SuppressWarnings({ + "java:S5960", // We're allowed assertions, as these are used in tests only + "checkstyle:MissingJavadocType", + "checkstyle:DesignForExtension" +}) +public class HealthTest { + private static final String SPEC_NAME = "HealthTest"; + + /** + * This test verifies health endpoint is exposed. The server under test needs to publish the {@link io.micronaut.discovery.event.ServiceReadyEvent} or {@link io.micronaut.runtime.server.event.ServerStartupEvent} for {@link io.micronaut.management.health.indicator.service.ServiceReadyHealthIndicator} to be UP. + * @throws IOException Exception thrown while getting the server under test. + */ + @Test + public void healthEndpointExposed() throws IOException { + // standard header name with mixed case + asserts(SPEC_NAME, + HttpRequest.GET("/health"), + (server, request) -> AssertionUtils.assertDoesNotThrow(server, request, HttpResponseAssertion.builder() + .status(HttpStatus.OK) + .body("{\"status\":\"UP\"}") + .build())); + } +} From 85d948c72f3399adfcc3460e52e5814fb95f59ff Mon Sep 17 00:00:00 2001 From: Graeme Rocher Date: Thu, 2 Mar 2023 11:32:56 +0100 Subject: [PATCH 537/743] fix IncompatibleClassChangeError that occurs with inherited methods from interfaces (#8864) --- .../inject/writer/BeanDefinitionWriter.java | 5 ++- .../ConfigPropertiesParseSpec.groovy | 42 +++++++++++++++++++ 2 files changed, 45 insertions(+), 2 deletions(-) diff --git a/core-processor/src/main/java/io/micronaut/inject/writer/BeanDefinitionWriter.java b/core-processor/src/main/java/io/micronaut/inject/writer/BeanDefinitionWriter.java index 6c32d880ba7..10366b64563 100644 --- a/core-processor/src/main/java/io/micronaut/inject/writer/BeanDefinitionWriter.java +++ b/core-processor/src/main/java/io/micronaut/inject/writer/BeanDefinitionWriter.java @@ -1436,9 +1436,10 @@ public void visitSetterValue( Type declaringTypeRef = JavaModelUtils.getTypeReference(declaringType); String methodDescriptor = getMethodDescriptor(methodElement.getReturnType(), Arrays.asList(methodElement.getParameters())); - injectMethodVisitor.visitMethodInsn(isInterface ? INVOKEINTERFACE : INVOKEVIRTUAL, + boolean isDeclaringTypeInterface = declaringType.getType().isInterface(); + injectMethodVisitor.visitMethodInsn(isDeclaringTypeInterface ? INVOKEINTERFACE : INVOKEVIRTUAL, declaringTypeRef.getInternalName(), methodElement.getName(), - methodDescriptor, isInterface); + methodDescriptor, isDeclaringTypeInterface); if (!methodElement.getReturnType().isVoid()) { injectMethodVisitor.pop(); diff --git a/inject-java/src/test/groovy/io/micronaut/inject/configproperties/ConfigPropertiesParseSpec.groovy b/inject-java/src/test/groovy/io/micronaut/inject/configproperties/ConfigPropertiesParseSpec.groovy index 463df10e813..1f77862619a 100644 --- a/inject-java/src/test/groovy/io/micronaut/inject/configproperties/ConfigPropertiesParseSpec.groovy +++ b/inject-java/src/test/groovy/io/micronaut/inject/configproperties/ConfigPropertiesParseSpec.groovy @@ -17,6 +17,46 @@ import java.time.Duration class ConfigPropertiesParseSpec extends AbstractTypeElementSpec { + void "test configuration properties implementing interface"() { + when: + def context = buildContext(''' +package jdbctest; + +import io.micronaut.context.annotation.ConfigurationProperties; + +@ConfigurationProperties("jdbc") +class TestConfiguration extends AbstractConfiguration implements BasicJdbcConfiguration { + private String url; + @Override public void setUrl(String url) { + this.url = url; + } + @Override public String getUrl() { + return url; + } +} +class AbstractConfiguration { + private String username; + public String getUsername() { + return username; + } + public void setUsername(String username) { + this.username = username; + } +} +interface BasicJdbcConfiguration { + String getUrl(); + void setUrl(String url); + String getUsername(); + void setUsername(String username); +} +''') + def bean = getBean(context, 'jdbctest.TestConfiguration') + + then: + bean.url == 'test' + bean.username == 'foo' + } + @Issue("https://github.com/micronaut-projects/micronaut-core/issues/8574") void "test configuration properties inherited from parent with multiple overloads"() { when: @@ -208,6 +248,8 @@ class MyConfig { protected void configureContext(ApplicationContextBuilder contextBuilder) { contextBuilder.properties( 'foo.bar.host':'bar', + 'jdbc.url':'test', + 'jdbc.username':'foo', "micronaut.session.http.test.write-mode": "test", "micronaut.session.http.test.uri": "http://localhost:9999", "micronaut.session.http.test.uris": "http://localhost:9999", From b5d29c7b7382d24ea88fd2c93aad8e4b27643c65 Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Thu, 2 Mar 2023 11:33:42 +0100 Subject: [PATCH 538/743] test: HTTP Server TCK LOG body and contains string (#8861) * test: HTTP Server TCK LOG body and contains string * add message to assertTrue --- .../io/micronaut/http/server/tck/BodyAssertion.java | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/http-server-tck/src/main/java/io/micronaut/http/server/tck/BodyAssertion.java b/http-server-tck/src/main/java/io/micronaut/http/server/tck/BodyAssertion.java index e37021054ad..d31266333b8 100644 --- a/http-server-tck/src/main/java/io/micronaut/http/server/tck/BodyAssertion.java +++ b/http-server-tck/src/main/java/io/micronaut/http/server/tck/BodyAssertion.java @@ -16,6 +16,8 @@ package io.micronaut.http.server.tck; import io.micronaut.core.annotation.Experimental; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.util.Arrays; import java.util.function.BiPredicate; @@ -29,6 +31,7 @@ */ @Experimental public final class BodyAssertion { + private static final Logger LOG = LoggerFactory.getLogger(BodyAssertion.class); private final Class bodyType; private final T expected; @@ -54,7 +57,7 @@ public static BodyAssertion.Builder builder() { */ @SuppressWarnings("java:S5960") // Assertion is the whole point of this method public void evaluate(T body) { - assertTrue(this.evaluator.test(expected, body)); + assertTrue(this.evaluator.test(expected, body), "Expected [" + expected + "] but was [" + body + "]"); } /** @@ -119,7 +122,13 @@ public StringBodyAssertionBuilder(String expected) { * @return a body assertion which verifiers the HTTP Response's body contains the expected body */ public BodyAssertion contains() { - return new BodyAssertion<>(String.class, this.body, (required, received) -> received.contains(required)); + return new BodyAssertion<>(String.class, this.body, (required, received) -> { + boolean result = received.contains(required); + if (!result) { + LOG.warn("The following body does not contains {}.\n{}\n", required, received); + } + return result; + }); } /** From 3de6371db9e3ba2e15757d267956232f3cd8f2e5 Mon Sep 17 00:00:00 2001 From: Graeme Rocher Date: Thu, 2 Mar 2023 14:27:40 +0100 Subject: [PATCH 539/743] Run the HTTP TCK against Netty natively (#8865) This PR deletes the GraalVM specific test suite project and just runs the TCK natively for Netty. --- .../http/server/tck/tests/BodyTest.java | 2 + .../http/server/tck/tests/ConsumesTest.java | 2 + settings.gradle | 1 - test-suite-graal/gradle.properties | 1 - .../micronaut/test/graal/HomeController.java | 30 ------------- .../test/graal/HomeControllerTest.java | 25 ----------- .../build.gradle | 45 ++++++++----------- .../build.gradle.kts | 9 ---- .../src/test/resources/logback.xml | 1 + 9 files changed, 24 insertions(+), 92 deletions(-) delete mode 100644 test-suite-graal/gradle.properties delete mode 100644 test-suite-graal/src/main/java/io/micronaut/test/graal/HomeController.java delete mode 100644 test-suite-graal/src/test/java/io/micronaut/test/graal/HomeControllerTest.java rename {test-suite-graal => test-suite-http-server-tck-netty}/build.gradle (63%) delete mode 100644 test-suite-http-server-tck-netty/build.gradle.kts diff --git a/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/BodyTest.java b/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/BodyTest.java index 48bc82bf294..0b8f4f4286d 100644 --- a/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/BodyTest.java +++ b/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/BodyTest.java @@ -16,6 +16,7 @@ package io.micronaut.http.server.tck.tests; import io.micronaut.context.annotation.Requires; +import io.micronaut.core.annotation.Introspected; import io.micronaut.core.async.annotation.SingleResult; import io.micronaut.http.HttpHeaders; import io.micronaut.http.HttpRequest; @@ -138,6 +139,7 @@ String postBytes(@Body byte[] bytes) { } } + @Introspected static class Point { private Integer x; private Integer y; diff --git a/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/ConsumesTest.java b/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/ConsumesTest.java index b3218f5c305..770070381e8 100644 --- a/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/ConsumesTest.java +++ b/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/ConsumesTest.java @@ -16,6 +16,7 @@ package io.micronaut.http.server.tck.tests; import io.micronaut.context.annotation.Requires; +import io.micronaut.core.annotation.Introspected; import io.micronaut.http.HttpHeaders; import io.micronaut.http.HttpRequest; import io.micronaut.http.HttpStatus; @@ -59,6 +60,7 @@ Pojo save(@Body Pojo pojo) { } } + @Introspected static class Pojo { private String name; diff --git a/settings.gradle b/settings.gradle index 77a71f34595..450f9e520af 100644 --- a/settings.gradle +++ b/settings.gradle @@ -69,7 +69,6 @@ include "test-suite-jakarta-inject-bean-import" include "test-suite-http-server-tck-netty" include "test-suite-kotlin" include "test-suite-kotlin-ksp" -include "test-suite-graal" include "test-suite-groovy" include "test-suite-groovy" include "test-suite-logback" diff --git a/test-suite-graal/gradle.properties b/test-suite-graal/gradle.properties deleted file mode 100644 index 53493a23825..00000000000 --- a/test-suite-graal/gradle.properties +++ /dev/null @@ -1 +0,0 @@ -skipDocumentation=true \ No newline at end of file diff --git a/test-suite-graal/src/main/java/io/micronaut/test/graal/HomeController.java b/test-suite-graal/src/main/java/io/micronaut/test/graal/HomeController.java deleted file mode 100644 index 9151a0a14fa..00000000000 --- a/test-suite-graal/src/main/java/io/micronaut/test/graal/HomeController.java +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright 2017-2022 original authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.micronaut.test.graal; - -import io.micronaut.http.annotation.Controller; -import io.micronaut.http.annotation.Get; -import java.util.Collections; -import java.util.Map; - -@Controller -public class HomeController { - - @Get - Map index() { - return Collections.singletonMap("message", "Hello World"); - } -} diff --git a/test-suite-graal/src/test/java/io/micronaut/test/graal/HomeControllerTest.java b/test-suite-graal/src/test/java/io/micronaut/test/graal/HomeControllerTest.java deleted file mode 100644 index 4197d27d599..00000000000 --- a/test-suite-graal/src/test/java/io/micronaut/test/graal/HomeControllerTest.java +++ /dev/null @@ -1,25 +0,0 @@ -package io.micronaut.test.graal; - -import io.micronaut.http.client.HttpClient; -import io.micronaut.http.client.annotation.Client; -import io.micronaut.test.extensions.junit5.annotation.MicronautTest; -import jakarta.inject.Inject; -import org.junit.jupiter.api.Test; - -import java.util.Map; - -import static org.junit.jupiter.api.Assertions.*; - -@MicronautTest -class HomeControllerTest { - - @Inject - @Client("/") - HttpClient httpClient; - - @Test - void helloWorld() { - assertEquals("Hello World", - httpClient.toBlocking().retrieve("/", Map.class).get("message")); - } -} diff --git a/test-suite-graal/build.gradle b/test-suite-http-server-tck-netty/build.gradle similarity index 63% rename from test-suite-graal/build.gradle rename to test-suite-http-server-tck-netty/build.gradle index f0da892954d..16f03d2df62 100644 --- a/test-suite-graal/build.gradle +++ b/test-suite-http-server-tck-netty/build.gradle @@ -1,43 +1,35 @@ plugins { - id "io.micronaut.build.internal.common" - id 'org.graalvm.buildtools.native' + id "java" + id("org.graalvm.buildtools.native") } -micronautBuild { - enableBom = false - enableProcessing = false -} repositories { mavenCentral() - maven { url "https://s01.oss.sonatype.org/content/repositories/snapshots/" } + maven { + url "https://s01.oss.sonatype.org/content/repositories/snapshots/" + mavenContent { + snapshotsOnly() + } + } +} + +micronautBuild { + enableBom = false + enableProcessing = false } dependencies { - annotationProcessor libs.bundles.asm - annotationProcessor project(":inject-java") - implementation project(":context") - implementation project(":core") - implementation project(":inject") - implementation project(":graal") - implementation project(":http-server-netty") - implementation project(":http-client") + implementation(projects.httpServerTck) + testImplementation(projects.httpServerNetty) + testImplementation(projects.httpClient) + testImplementation(libs.junit.platform.engine) + testImplementation(libs.managed.logback.classic) implementation platform(libs.test.boms.micronaut.validation) implementation(libs.micronaut.validation) { exclude group: 'io.micronaut' } - implementation project(":inject-java") - - testAnnotationProcessor libs.bundles.asm - testAnnotationProcessor project(":inject-java") - testImplementation (libs.micronaut.serde.jackson) - testImplementation (libs.micronaut.test.junit5) { - exclude group: 'io.micronaut' - } -} -tasks.withType(Test).configureEach { - useJUnitPlatform() } configurations { @@ -75,6 +67,7 @@ graalvmNative { } binaries { all { + resources.autodetect() openGraalModules.each { module -> jvmArgs.add("--add-exports=" + module + "=ALL-UNNAMED") } diff --git a/test-suite-http-server-tck-netty/build.gradle.kts b/test-suite-http-server-tck-netty/build.gradle.kts deleted file mode 100644 index 1e36c4d3594..00000000000 --- a/test-suite-http-server-tck-netty/build.gradle.kts +++ /dev/null @@ -1,9 +0,0 @@ -plugins { - id("io.micronaut.build.internal.convention-test-library") -} -dependencies { - testImplementation(projects.httpServerNetty) - testImplementation(projects.httpClient) - testImplementation(projects.httpServerTck) - testImplementation(libs.junit.platform.engine) -} diff --git a/test-suite-http-server-tck-netty/src/test/resources/logback.xml b/test-suite-http-server-tck-netty/src/test/resources/logback.xml index 8eb8c3a8170..f6fa1cb1607 100644 --- a/test-suite-http-server-tck-netty/src/test/resources/logback.xml +++ b/test-suite-http-server-tck-netty/src/test/resources/logback.xml @@ -8,3 +8,4 @@ + From d2c556fecb8fbe35c1f22955ee44d5f9e2c0adc7 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 2 Mar 2023 15:19:56 +0100 Subject: [PATCH 540/743] chore(deps): update dependency gradle to v7.6.1 (#8832) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/wrapper/gradle-wrapper.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index f398c33c4b0..508322917bd 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.6-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.1-bin.zip networkTimeout=10000 zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists From 3b8631dc888e8142b4a8958cae123f6e2a82c2c4 Mon Sep 17 00:00:00 2001 From: Graeme Rocher Date: Thu, 2 Mar 2023 17:55:15 +0100 Subject: [PATCH 541/743] Fallback to trying to load visitor annotations from type (#8870) --- .../processing/visitor/LoadedVisitor.kt | 70 ++++++++++--------- 1 file changed, 37 insertions(+), 33 deletions(-) diff --git a/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/LoadedVisitor.kt b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/LoadedVisitor.kt index 5087670c9fa..5a5887ca9fa 100644 --- a/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/LoadedVisitor.kt +++ b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/LoadedVisitor.kt @@ -48,42 +48,22 @@ internal class LoadedVisitor( .map { it.resolve() } .find { it.declaration.qualifiedName?.asString() == tevClassName - } ?: throw ProcessingException( - visitorContext.elementFactory.newClassElement(declaration), - "The visitor [$declaration] doesn't implement $tevClassName" - ) - val classArgument = reference.arguments[0].type ?: throw ProcessingException( - visitorContext.elementFactory.newClassElement(declaration), - "Cannot determine the class type argument of the visitor: $declaration" - ) - val elementArgument = reference.arguments[1].type ?: throw ProcessingException( - visitorContext.elementFactory.newClassElement(declaration), - "Cannot determine the element type argument of the visitor: $declaration" - ) - classAnnotation = getType(classArgument.resolve(), visitor.classType) - elementAnnotation = getType(elementArgument.resolve(), visitor.elementType) - } else { - val classes = GenericTypeUtils.resolveInterfaceTypeArguments( - javaClass, - TypeElementVisitor::class.java - ) - if (classes != null && classes.size == 2) { - val classGeneric = classes[0] - classAnnotation = if (classGeneric == Any::class.java) { - visitor.classType - } else { - classGeneric.name - } - val elementGeneric = classes[1] - elementAnnotation = if (elementGeneric == Any::class.java) { - visitor.elementType + } + if (reference == null) { + resolveFromClassDeclaration(javaClass) + } else { + + val classArgument = reference.arguments[0].type + val elementArgument = reference.arguments[1].type + if (classArgument != null && elementArgument != null) { + classAnnotation = getType(classArgument.resolve(), visitor.classType) + elementAnnotation = getType(elementArgument.resolve(), visitor.elementType) } else { - elementGeneric.name + resolveFromClassDeclaration(javaClass) } - } else { - classAnnotation = Any::class.java.name - elementAnnotation = Any::class.java.name } + } else { + resolveFromClassDeclaration(javaClass) } if (classAnnotation == ANY) { classAnnotation = Object::class.java.name @@ -93,6 +73,30 @@ internal class LoadedVisitor( } } + private fun resolveFromClassDeclaration(javaClass: Class>) { + val classes = GenericTypeUtils.resolveInterfaceTypeArguments( + javaClass, + TypeElementVisitor::class.java + ) + if (classes != null && classes.size == 2) { + val classGeneric = classes[0] + classAnnotation = if (classGeneric == Any::class.java) { + visitor.classType + } else { + classGeneric.name + } + val elementGeneric = classes[1] + elementAnnotation = if (elementGeneric == Any::class.java) { + visitor.elementType + } else { + elementGeneric.name + } + } else { + classAnnotation = Any::class.java.name + elementAnnotation = Any::class.java.name + } + } + override fun getOrder(): Int { return visitor.order } From ad657d5a0d29127fc624c6b46244faec7bf5a78a Mon Sep 17 00:00:00 2001 From: Graeme Rocher Date: Thu, 2 Mar 2023 18:28:00 +0100 Subject: [PATCH 542/743] Remove jackson databind as a transtive of GraalVM module (#8869) --- graal/build.gradle | 1 - 1 file changed, 1 deletion(-) diff --git a/graal/build.gradle b/graal/build.gradle index 26667cb75c1..d2307e2ec76 100644 --- a/graal/build.gradle +++ b/graal/build.gradle @@ -4,7 +4,6 @@ plugins { dependencies { annotationProcessor project(":inject-java") api project(":core-processor") - implementation libs.managed.jackson.databind testImplementation project(":inject") testImplementation project(":http") From 615b68a1136e204dc404c4e1b7c7dde8abca8221 Mon Sep 17 00:00:00 2001 From: micronaut-build <65172877+micronaut-build@users.noreply.github.com> Date: Fri, 3 Mar 2023 11:41:16 +0100 Subject: [PATCH 543/743] Bump micronaut-gcp to 4.8.1 (#8875) --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index c4159fe60fc..0e35d0506d9 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -78,7 +78,7 @@ managed-micronaut-discovery = "3.2.0" managed-micronaut-elasticsearch = "4.3.0" managed-micronaut-email = "1.5.0" managed-micronaut-flyway = "5.4.1" -managed-micronaut-gcp = "4.8.0" +managed-micronaut-gcp = "4.8.1" managed-micronaut-graphql = "3.2.0" managed-micronaut-groovy = "3.4.0" managed-micronaut-grpc = "3.5.0" From d2cf5a29ba7f62a45dc243ce5ad6f1cf9d4d3ad1 Mon Sep 17 00:00:00 2001 From: micronaut-build <65172877+micronaut-build@users.noreply.github.com> Date: Fri, 3 Mar 2023 12:11:38 +0100 Subject: [PATCH 544/743] Bump micronaut-sql to 4.7.3 (#8873) --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 3370dcbb159..8c697cc642d 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -116,7 +116,7 @@ managed-micronaut-security = "3.8.3" managed-micronaut-serialization = "1.3.3" managed-micronaut-servlet = "3.3.5" managed-micronaut-spring = "4.3.1" -managed-micronaut-sql = "4.7.2" +managed-micronaut-sql = "4.7.3" managed-micronaut-test = "3.6.2" managed-micronaut-test-resources = "1.1.3" managed-micronaut-toml = "1.1.1" From 01f4f67e5a0b630f1dd78649cbc4d52cae676fb6 Mon Sep 17 00:00:00 2001 From: micronaut-build <65172877+micronaut-build@users.noreply.github.com> Date: Fri, 3 Mar 2023 12:13:38 +0100 Subject: [PATCH 545/743] Bump micronaut-aot to 1.1.2 (#8874) --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 0e35d0506d9..abbf2dc7b1c 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -66,7 +66,7 @@ managed-maven-native-plugin = "0.9.19" managed-methvin-directory-watcher = "0.16.1" managed-micrometer = "1.10.3" managed-micronaut-acme = "3.2.0" -managed-micronaut-aot = "1.1.1" +managed-micronaut-aot = "1.1.2" managed-micronaut-aws = "3.10.9" managed-micronaut-azure = "3.7.1" managed-micronaut-cache = "3.5.0" From cf3691ed8c72ec8423fa941f3dca136bb83dd51b Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Fri, 3 Mar 2023 14:18:06 +0100 Subject: [PATCH 546/743] docs: fix Route Compile-Time validation (#8877) The annotation processor is micronaut-http-validation This was changed in Micronaut Framework 3.0 --- src/main/docs/guide/httpServer/routing.adoc | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/src/main/docs/guide/httpServer/routing.adoc b/src/main/docs/guide/httpServer/routing.adoc index fdf7a90c8f9..32325fe2198 100644 --- a/src/main/docs/guide/httpServer/routing.adoc +++ b/src/main/docs/guide/httpServer/routing.adoc @@ -140,22 +140,13 @@ snippet::io.micronaut.docs.server.routes.MyRoutes[tags="imports,class", indent=0 <2> Use `@Inject` to inject a method with the controller to route to <3> Use methods such as link:{api}/io/micronaut/web/router/RouteBuilder.html[`RouteBuilder::GET(String,Class,String,Class...)`] to route to controller methods. Note that even though the issues controller is used, the route has no knowledge of its `@Controller` annotation and thus the full path must be specified. - - - TIP: Unfortunately due to type erasure, a Java method lambda reference cannot be used with the API. For Groovy there is a `GroovyRouteBuilder` class which can be subclassed that allows passing Groovy method references. == Route Compile-Time Validation -Micronaut supports validating route arguments at compile time with the validation library. To get started, add the `validation` dependency to your build: +Micronaut supports validating route arguments at compile time with the validation library. To get started, add the `micronaut-http-validation` dependency to your build: -[source,groovy] -.build.gradle ----- -annotationProcessor "io.micronaut:micronaut-validation" // Java only -kapt "io.micronaut:micronaut-validation" // Kotlin only -implementation "io.micronaut:micronaut-validation" ----- +dependency:io.micronaut:micronaut-http-validation[scope='annotationProcessor'] With the correct dependency on your classpath, route arguments will automatically be checked at compile time. Compilation will fail if any of the following conditions are met: @@ -167,7 +158,7 @@ An optional variable is one that allows the route to match a URI even if the val * {blank} The URI template contains a variable that is missing from the method arguments. -NOTE: To disable route compile-time validation, set the system property `-Dmicronaut.route.validation=false`. For Java and Kotlin users using Gradle, the same effect can be achieved by removing the `validation` dependency from the `annotationProcessor`/`kapt` scope. +NOTE: To disable route compile-time validation, set the system property `-Dmicronaut.route.validation=false`. For Java and Kotlin users using Gradle, the same effect can be achieved by removing the `micronaut-http-validation` dependency from the `annotationProcessor`/`kapt` scope. == Routing non-standard HTTP methods From 56746d3e79bedcd4d9ab4e1de08946c4939d2390 Mon Sep 17 00:00:00 2001 From: Denis Stepanov Date: Fri, 3 Mar 2023 10:12:02 -0500 Subject: [PATCH 547/743] Prepare for Jakarta validation (#8871) --- .../IntrospectedTypeElementVisitor.java | 25 ++++--------------- .../GroovyAnnotationMetadataBuilder.java | 4 ++- .../beans/BeanIntrospectionSpec.groovy | 2 +- .../JavaAnnotationMetadataBuilder.java | 2 +- 4 files changed, 10 insertions(+), 23 deletions(-) diff --git a/core-processor/src/main/java/io/micronaut/inject/beans/visitor/IntrospectedTypeElementVisitor.java b/core-processor/src/main/java/io/micronaut/inject/beans/visitor/IntrospectedTypeElementVisitor.java index 19df8e68599..e647878d945 100644 --- a/core-processor/src/main/java/io/micronaut/inject/beans/visitor/IntrospectedTypeElementVisitor.java +++ b/core-processor/src/main/java/io/micronaut/inject/beans/visitor/IntrospectedTypeElementVisitor.java @@ -36,6 +36,7 @@ import io.micronaut.inject.writer.ClassGenerationException; import java.io.IOException; +import java.lang.annotation.Annotation; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; @@ -59,15 +60,6 @@ public class IntrospectedTypeElementVisitor implements TypeElementVisitor ANN_CONSTRAINT = AnnotationValue.builder(Introspected.IndexedAnnotation.class) - .member("annotation", new AnnotationClassValue<>(JAVAX_VALIDATION_CONSTRAINT)) - .build(); - private static final String JAVAX_VALIDATION_VALID = "javax.validation.Valid"; - private static final AnnotationValue ANN_VALID = AnnotationValue.builder(Introspected.IndexedAnnotation.class) - .member("annotation", new AnnotationClassValue<>(JAVAX_VALIDATION_VALID)) - .build(); - private final Map writers = new LinkedHashMap<>(10); @Override @@ -92,21 +84,14 @@ private boolean isIntrospected(VisitorContext context, ClassElement c) { private void processIntrospected(ClassElement element, VisitorContext context, AnnotationValue introspected) { final String[] packages = introspected.stringValues("packages"); - final AnnotationClassValue[] classes = introspected.get("classes", AnnotationClassValue[].class, new AnnotationClassValue[0]); + final AnnotationClassValue[] classes = introspected.get("classes", AnnotationClassValue[].class, new AnnotationClassValue[0]); final boolean metadata = introspected.booleanValue("annotationMetadata").orElse(true); final Set includedAnnotations = CollectionUtils.setOf(introspected.stringValues("includedAnnotations")); - final Set toIndex = CollectionUtils.setOf(introspected.get("indexed", AnnotationValue[].class, new AnnotationValue[0])); - final Set indexedAnnotations; - if (CollectionUtils.isEmpty(toIndex)) { - indexedAnnotations = CollectionUtils.setOf(ANN_CONSTRAINT, ANN_VALID); - } else { - toIndex.addAll(CollectionUtils.setOf(ANN_CONSTRAINT, ANN_VALID)); - indexedAnnotations = toIndex; - } + final Set> indexedAnnotations = CollectionUtils.setOf(introspected.get("indexed", AnnotationValue[].class, new AnnotationValue[0])); if (ArrayUtils.isNotEmpty(classes)) { AtomicInteger index = new AtomicInteger(0); - for (AnnotationClassValue aClass : classes) { + for (AnnotationClassValue aClass : classes) { context.getClassElement(aClass.getName()).ifPresent(ce -> { if (ce.isPublic() && !isIntrospected(context, ce)) { final AnnotationMetadata typeMetadata = ce.getAnnotationMetadata(); @@ -186,7 +171,7 @@ public void finish(VisitorContext visitorContext) { } private void processElement(boolean metadata, - Set indexedAnnotations, + Set> indexedAnnotations, ClassElement ce, BeanIntrospectionWriter writer) { PropertyElementQuery query = PropertyElementQuery.of(ce).ignoreSettersWithDifferingType(true); diff --git a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/annotation/GroovyAnnotationMetadataBuilder.java b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/annotation/GroovyAnnotationMetadataBuilder.java index 890ff58b395..796c3cb99b0 100644 --- a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/annotation/GroovyAnnotationMetadataBuilder.java +++ b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/annotation/GroovyAnnotationMetadataBuilder.java @@ -120,7 +120,9 @@ protected boolean isValidationRequired(AnnotatedNode member) { if (member != null) { final List annotations = member.getAnnotations(); if (CollectionUtils.isNotEmpty(annotations)) { - return annotations.stream().anyMatch((it) -> it.getClassNode().getName().startsWith("javax.validation")); + return annotations.stream().anyMatch((it) -> + it.getClassNode().getName().startsWith("javax.validation") + || it.getClassNode().getName().startsWith("jakarta.validation")); } } return false; diff --git a/inject-java-test/src/test/groovy/io/micronaut/inject/visitor/beans/BeanIntrospectionSpec.groovy b/inject-java-test/src/test/groovy/io/micronaut/inject/visitor/beans/BeanIntrospectionSpec.groovy index 490bb811d94..4a6981eec7b 100644 --- a/inject-java-test/src/test/groovy/io/micronaut/inject/visitor/beans/BeanIntrospectionSpec.groovy +++ b/inject-java-test/src/test/groovy/io/micronaut/inject/visitor/beans/BeanIntrospectionSpec.groovy @@ -4661,7 +4661,7 @@ class Holder { static class MyTypeElementVisitorProcessor extends TypeElementVisitorProcessor { @Override protected Collection findTypeElementVisitors() { - return [new ValidationVisitor(), new ConfigurationReaderVisitor(), new IntrospectedTypeElementVisitor()] + return [new ValidationVisitor(), new ConfigurationReaderVisitor(), new io.micronaut.validation.visitor.IntrospectedValidationIndexesVisitor(), new IntrospectedTypeElementVisitor()] } } diff --git a/inject-java/src/main/java/io/micronaut/annotation/processing/JavaAnnotationMetadataBuilder.java b/inject-java/src/main/java/io/micronaut/annotation/processing/JavaAnnotationMetadataBuilder.java index 3b3310051e6..9138dbec95c 100644 --- a/inject-java/src/main/java/io/micronaut/annotation/processing/JavaAnnotationMetadataBuilder.java +++ b/inject-java/src/main/java/io/micronaut/annotation/processing/JavaAnnotationMetadataBuilder.java @@ -391,7 +391,7 @@ protected boolean isValidationRequired(Element member) { private boolean isValidationRequired(List annotationMirrors) { for (AnnotationMirror annotationMirror : annotationMirrors) { final String annotationName = getAnnotationTypeName(annotationMirror); - if (annotationName.startsWith("javax.validation")) { + if (annotationName.startsWith("javax.validation") || annotationName.startsWith("jakarta.validation")) { return true; } else if (!AnnotationUtil.INTERNAL_ANNOTATION_NAMES.contains(annotationName)) { final Element element = getAnnotationMirror(annotationName).orElse(null); From 2f50d163ae646f93a6a3a758fa10ecfafba550d1 Mon Sep 17 00:00:00 2001 From: micronaut-build <65172877+micronaut-build@users.noreply.github.com> Date: Fri, 3 Mar 2023 16:52:08 +0100 Subject: [PATCH 548/743] Bump micronaut-kafka to 4.5.2 (#8878) --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index abbf2dc7b1c..ceb52f354b7 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -87,7 +87,7 @@ managed-micronaut-ignite = "1.0.0.RC1" managed-micronaut-jaxrs = "3.4.0" managed-micronaut-jms = "2.1.0" managed-micronaut-jmx = "3.2.0" -managed-micronaut-kafka = "4.5.1" +managed-micronaut-kafka = "4.5.2" managed-micronaut-kotlin = "3.2.2" managed-micronaut-kubernetes = "3.4.0" managed-micronaut-micrometer = "4.7.2" From d72b31f5129030dcadfff4fc2827278bdfbe0a59 Mon Sep 17 00:00:00 2001 From: Dean Wette Date: Sun, 5 Mar 2023 09:25:25 -0600 Subject: [PATCH 549/743] doc: del session docs & link to micronaut-session (#8737) * Remove docs for HTTP sessions and provide link to `micronaut-session` docs * docs: move sessions docs url to gradle.properties closes #8735 --- gradle.properties | 1 + src/main/docs/guide/appendix/breaks.adoc | 2 +- src/main/docs/guide/httpServer/sessions.adoc | 132 +------------------ 3 files changed, 3 insertions(+), 132 deletions(-) diff --git a/gradle.properties b/gradle.properties index 27d00b5a362..4581dfcb29c 100644 --- a/gradle.properties +++ b/gradle.properties @@ -35,6 +35,7 @@ micronautribbonapi=https://micronaut-projects.github.io/micronaut-netflix/latest micronautcacheapi=https://micronaut-projects.github.io/micronaut-cache/latest/api micronautreactorapi=https://micronaut-projects.github.io/micronaut-reactor/latest/api micronautsessionapi=https://micronaut-projects.github.io/micronaut-session/snapshot/api +micronautsessiondocs=https://micronaut-projects.github.io/micronaut-session/snapshot/guide micronautspringapi=https://micronaut-projects.github.io/micronaut-spring/latest/api micronauttracingapi=https://micronaut-projects.github.io/micronaut-tracing/latest/api hibernateapi=http://docs.jboss.org/hibernate/orm/current/javadocs diff --git a/src/main/docs/guide/appendix/breaks.adoc b/src/main/docs/guide/appendix/breaks.adoc index 150659b9588..2d7baf0f6a0 100644 --- a/src/main/docs/guide/appendix/breaks.adoc +++ b/src/main/docs/guide/appendix/breaks.adoc @@ -55,7 +55,7 @@ dependency:micronaut-reactor[groupId="io.micronaut.reactor"] ==== Session Support Moved to Session Module -The Session handling features https://micronaut-projects.github.io/micronaut-session/snapshot/guide/[have been moved to its own module]. If you use the HTTP session module, change the maven coordinates from `io.micronaut:micronaut-session` to `io.micronaut.session:micronaut-session`. +The Session handling features link:{micronautsessiondocs}[have been moved to its own module]. If you use the HTTP session module, change the maven coordinates from `io.micronaut:micronaut-session` to `io.micronaut.session:micronaut-session`. dependency:micronaut-session[groupId="io.micronaut.session"] diff --git a/src/main/docs/guide/httpServer/sessions.adoc b/src/main/docs/guide/httpServer/sessions.adoc index fbcb0822415..12cc50c1d3a 100644 --- a/src/main/docs/guide/httpServer/sessions.adoc +++ b/src/main/docs/guide/httpServer/sessions.adoc @@ -1,131 +1 @@ -By default Micronaut is a stateless HTTP server, however depending on your application requirements you may need the notion of HTTP sessions. - -Micronaut includes a `session` module inspired by https://projects.spring.io/spring-session/[Spring Session] that enables this which currently has two implementations: - -* In-Memory sessions - which you should combine with an a sticky session proxy if you plan to run multiple instances. -* Redis sessions - In this case https://redis.io[Redis] stores sessions, and non-blocking I/O is used to read/write sessions to Redis. - -== Enabling Sessions - -To enable support for in-memory sessions you just need the `session` dependency: - -dependency:micronaut-session[groupId=io.micronaut.session] - -=== Redis Sessions - -To store link:{micronautsessionapi}/io/micronaut/session/Session.html[Session] instances in Redis, use the https://micronaut-projects.github.io/micronaut-redis/latest/guide/#sessions[Micronaut Redis] module which includes detailed instructions. - -To quickly get up and running with Redis sessions you must also have the `redis-lettuce` dependency in your build: - -.build.gradle -[source,groovy] ----- -implementation "io.micronaut-session:micronaut-session" -implementation "io.micronaut.redis:micronaut-redis-lettuce" ----- - -And enable Redis sessions via configuration in your configuration file (e.g `application.yml`): - -.Enabling Redis Sessions -[configuration] ----- -redis: - uri: redis://localhost:6379 -micronaut: - session: - http: - redis: - enabled: true ----- - -== Configuring Session Resolution - -link:{micronautsessionapi}/io/micronaut/session/Session.html[Session] resolution can be configured with link:{micronautsessionapi}/io/micronaut/session/http/HttpSessionConfiguration.html[HttpSessionConfiguration]. - -By default, sessions are resolved using link:{micronautsessionapi}/io/micronaut/session/http/HttpSessionFilter.html[HttpSessionFilter] that looks for session identifiers via either an HTTP header (using the `Authorization-Info` or `X-Auth-Token` headers) or via a Cookie named `SESSION`. - -You can disable either header resolution or cookie resolution via configuration in your configuration file (e.g `application.yml`): - -.Disabling Cookie Resolution -[configuration] ----- -micronaut: - session: - http: - cookie: false - header: true ----- - -The above configuration enables header resolution, but disables cookie resolution. You can also configure header and cookie names. - -== Working with Sessions - -A link:{micronautsessionapi}/io/micronaut/session/Session.html[Session] can be retrieved with a parameter of type link:{micronautsessionapi}/io/micronaut/session/Session.html[Session] in a controller method. For example consider the following controller: - -snippet::io.micronaut.docs.session.ShoppingController[tags="imports,class,add,endclass", indent=0] - -<1> `ShoppingController` declares a link:{micronautsessionapi}/io/micronaut/session/Session.html[Session] attribute named `cart` -<2> The link:{micronautsessionapi}/io/micronaut/session/Session.html[Session] is declared as a method parameter -<3> The `cart` attribute is retrieved -<4> Otherwise a new `Cart` instance is created and stored in the session - -Note that because the link:{micronautsessionapi}/io/micronaut/session/Session.html[Session] is declared as a required parameter, to execute the controller action a link:{micronautsessionapi}/io/micronaut/session/Session.html[Session] will be created and saved to the link:{micronautsessionapi}/io/micronaut/session/SessionStore.html[SessionStore]. - -If you don't want to create unnecessary sessions, declare the link:{micronautsessionapi}/io/micronaut/session/Session.html[Session] as `@Nullable` in which case a session will not be created and saved unnecessarily. For example: - -snippet::io.micronaut.docs.session.ShoppingController[tags="clear", indent=0,title="Using @Nullable with Sessions"] - -The above method only injects a new link:{micronautsessionapi}/io/micronaut/session/Session.html[Session] if one already exists. - -== Session Clients - -If the client is a web browser, sessions should work if cookies are enabled. However, for programmatic HTTP clients you need to propagate the session ID between HTTP calls. - -For example, when invoking the `viewCart` method of the `StoreController` in the previous example, the HTTP client receives by default a `AUTHORIZATION_INFO` header. The following example, using a Spock test, demonstrates this: - -.Retrieving the AUTHORIZATION_INFO header -snippet::io.micronaut.docs.session.ShoppingControllerSpec[tags="view", indent=0] - -<1> A request is made to `/shopping/cart` -<2> The `AUTHORIZATION_INFO` header is present in the response - -You can then pass this `AUTHORIZATION_INFO` in subsequent requests to reuse the existing link:{micronautsessionapi}/io/micronaut/session/Session.html[Session]: - -.Sending the AUTHORIZATION_INFO header -snippet::io.micronaut.docs.session.ShoppingControllerSpec[tags="add", indent=0] - -<1> The `AUTHORIZATION_INFO` is retrieved from the response -<2> And then sent as a header in the subsequent request - -== Using @SessionValue - -Rather than explicitly injecting the link:{micronautsessionapi}/io/micronaut/session/Session.html[Session] into a controller method, you can instead use link:{micronautsessionapi}/io/micronaut/session/annotation/SessionValue.html[@SessionValue]. For example: - -snippet::io.micronaut.docs.session.ShoppingController[tags="view", indent=0,title="Using @SessionValue"] - -<1> link:{micronautsessionapi}/io/micronaut/session/annotation/SessionValue.html[@SessionValue] is declared on the method resulting in the return value being stored in the link:{micronautsessionapi}/io/micronaut/session/Session.html[Session]. Note that you must specify the attribute name when used on a return value -<2> link:{micronautsessionapi}/io/micronaut/session/annotation/SessionValue.html[@SessionValue] is used on a `@Nullable` parameter which results in looking up the value from the link:{micronautsessionapi}/io/micronaut/session/Session.html[Session] in a non-blocking way and supplying it if present. In the case a value is not specified to link:{micronautsessionapi}/io/micronaut/session/annotation/SessionValue.html[@SessionValue] resulting in the parameter name being used to lookup the attribute. - -== Session Events - -You can register api:context.event.ApplicationEventListener[] beans to listen for link:{micronautsessionapi}/io/micronaut/session/Session.html[Session] related events located in the link:{micronautsessionapi}/io/micronaut/session/event/package-summary.html[session.event] package. - -The following table summarizes the events: - -.Session Events -|=== -|Type|Description - -|link:{micronautsessionapi}/io/micronaut/session/event/SessionCreatedEvent.html[SessionCreatedEvent] -|Fired when a link:{micronautsessionapi}/io/micronaut/session/Session.html[Session] is created - -|link:{micronautsessionapi}/io/micronaut/session/event/SessionDeletedEvent.html[SessionDeletedEvent] -|Fired when a link:{micronautsessionapi}/io/micronaut/session/Session.html[Session] is deleted - -|link:{micronautsessionapi}/io/micronaut/session/event/SessionExpiredEvent.html[SessionExpiredEvent] -|Fired when a link:{micronautsessionapi}/io/micronaut/session/Session.html[Session] expires - -|link:{micronautsessionapi}/io/micronaut/session/event/SessionDestroyedEvent.html[SessionDestroyedEvent] -|Parent of both `SessionDeletedEvent` and `SessionExpiredEvent` - -|=== +See the documentation for link:{micronautsessiondocs}[Micronaut Session] for information about supporting HTTP sessions in your applications. From f557117febbcc95e2b6505d0cde4f69581d1063e Mon Sep 17 00:00:00 2001 From: micronaut-build <65172877+micronaut-build@users.noreply.github.com> Date: Sun, 5 Mar 2023 23:29:36 +0100 Subject: [PATCH 550/743] Bump micronaut-sql to 4.8.0 (#8883) --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index cca4d338d2a..9c6f2488d9f 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -116,7 +116,7 @@ managed-micronaut-security = "3.9.3" managed-micronaut-serialization = "1.5.1" managed-micronaut-servlet = "3.3.5" managed-micronaut-spring = "4.5.0" -managed-micronaut-sql = "4.7.2" +managed-micronaut-sql = "4.8.0" managed-micronaut-test = "3.9.1" managed-micronaut-test-resources = "1.2.3" managed-micronaut-toml = "1.1.3" From efefc4d7d1aaaf7477bcc590502ed0b825342124 Mon Sep 17 00:00:00 2001 From: micronaut-build <65172877+micronaut-build@users.noreply.github.com> Date: Mon, 6 Mar 2023 09:16:24 +0100 Subject: [PATCH 551/743] build: micronaut-gcp to 4.9.0 (#8886) --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 9c6f2488d9f..70ed927da44 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -78,7 +78,7 @@ managed-micronaut-discovery = "3.3.0" managed-micronaut-elasticsearch = "4.3.0" managed-micronaut-email = "1.5.0" managed-micronaut-flyway = "5.4.1" -managed-micronaut-gcp = "4.8.1" +managed-micronaut-gcp = "4.9.0" managed-micronaut-graphql = "3.2.0" managed-micronaut-groovy = "3.4.0" managed-micronaut-grpc = "3.5.0" From d71d1d09aa407c8f745e6f85563f2407824ef347 Mon Sep 17 00:00:00 2001 From: Denis Stepanov Date: Mon, 6 Mar 2023 03:20:30 -0500 Subject: [PATCH 552/743] Correct processing of the inner classes (#8879) --- .../BeanDefinitionInjectProcessor.java | 20 +-- .../annotation/processing/ModelUtils.java | 32 +++- .../TypeElementVisitorProcessor.java | 24 +-- .../annotation/AnnotateClassSpec.groovy | 153 ++++++++++++++++++ ...icronaut.inject.visitor.TypeElementVisitor | 1 + 5 files changed, 200 insertions(+), 30 deletions(-) create mode 100644 inject-java/src/test/groovy/io/micronaut/annotation/AnnotateClassSpec.groovy diff --git a/inject-java/src/main/java/io/micronaut/annotation/processing/BeanDefinitionInjectProcessor.java b/inject-java/src/main/java/io/micronaut/annotation/processing/BeanDefinitionInjectProcessor.java index c976fde94a7..60f39b11e52 100644 --- a/inject-java/src/main/java/io/micronaut/annotation/processing/BeanDefinitionInjectProcessor.java +++ b/inject-java/src/main/java/io/micronaut/annotation/processing/BeanDefinitionInjectProcessor.java @@ -56,7 +56,6 @@ import java.util.Set; import java.util.stream.Collectors; -import static javax.lang.model.element.ElementKind.ANNOTATION_TYPE; import static javax.lang.model.element.ElementKind.ENUM; /** @@ -154,19 +153,14 @@ public final boolean process(Set annotations, RoundEnviro TypeElement groovyObjectTypeElement = elementUtils.getTypeElement("groovy.lang.GroovyObject"); TypeMirror groovyObjectType = groovyObjectTypeElement != null ? groovyObjectTypeElement.asType() : null; // accumulate all the class elements for all annotated elements - annotations.forEach(annotation -> roundEnv.getElementsAnnotatedWith(annotation) - .stream() - // filtering annotation definitions, which are not processed - .filter(element -> element.getKind() != ANNOTATION_TYPE) - .forEach(element -> { - TypeElement typeElement = modelUtils.classElementFor(element); - if (typeElement == null) { - return; - } - if (element.getKind() == ENUM) { - final AnnotationMetadata am = annotationMetadataBuilder.lookupOrBuildForType(element); + annotations.forEach(annotation -> modelUtils.resolveTypeElements( + roundEnv.getElementsAnnotatedWith(annotation) + ) + .forEach(typeElement -> { + if (typeElement.getKind() == ENUM) { + final AnnotationMetadata am = annotationMetadataBuilder.lookupOrBuildForType(typeElement); if (BeanDefinitionCreatorFactory.isDeclaredBeanInMetadata(am)) { - error(element, "Enum types cannot be defined as beans"); + error(typeElement, "Enum types cannot be defined as beans"); } return; } diff --git a/inject-java/src/main/java/io/micronaut/annotation/processing/ModelUtils.java b/inject-java/src/main/java/io/micronaut/annotation/processing/ModelUtils.java index 0981459d9dd..281484e6d1d 100644 --- a/inject-java/src/main/java/io/micronaut/annotation/processing/ModelUtils.java +++ b/inject-java/src/main/java/io/micronaut/annotation/processing/ModelUtils.java @@ -15,24 +15,30 @@ */ package io.micronaut.annotation.processing; +import io.micronaut.core.annotation.Generated; import io.micronaut.core.annotation.Internal; import io.micronaut.core.annotation.Nullable; import io.micronaut.inject.processing.JavaModelUtils; import javax.lang.model.element.Element; import javax.lang.model.element.ElementKind; +import javax.lang.model.element.ExecutableElement; import javax.lang.model.element.TypeElement; +import javax.lang.model.element.VariableElement; import javax.lang.model.type.DeclaredType; import javax.lang.model.type.TypeKind; import javax.lang.model.type.TypeMirror; import javax.lang.model.util.Elements; import javax.lang.model.util.Types; +import java.util.Objects; import java.util.Optional; import java.util.Set; import java.util.SortedSet; import java.util.TreeSet; +import java.util.stream.Stream; -import static javax.lang.model.element.Modifier.*; +import static javax.lang.model.element.Modifier.ABSTRACT; +import static javax.lang.model.element.Modifier.STATIC; import static javax.lang.model.type.TypeKind.NONE; /** @@ -63,6 +69,30 @@ public Types getTypeUtils() { return typeUtils; } + /** + * Resolves type elements from the provided annotated elements. + * + * @param annotatedElements The elements to process + * @return the type elements + */ + public Stream resolveTypeElements(Set annotatedElements) { + return annotatedElements + .stream() + .map(element -> { + if (element instanceof ExecutableElement executableElement) { + return executableElement.getEnclosingElement(); + } + if (element instanceof VariableElement variableElement) { + return variableElement.getEnclosingElement(); + } + return element; + }) + .filter(element -> JavaModelUtils.isClassOrInterface(element) || JavaModelUtils.isEnum(element) || JavaModelUtils.isRecord(element)) + .map(this::classElementFor) + .filter(Objects::nonNull) + .filter(element -> element.getAnnotation(Generated.class) == null); + } + /** * Obtains the {@link TypeElement} for an given element. * diff --git a/inject-java/src/main/java/io/micronaut/annotation/processing/TypeElementVisitorProcessor.java b/inject-java/src/main/java/io/micronaut/annotation/processing/TypeElementVisitorProcessor.java index 6c32424e222..28ad0f9168c 100644 --- a/inject-java/src/main/java/io/micronaut/annotation/processing/TypeElementVisitorProcessor.java +++ b/inject-java/src/main/java/io/micronaut/annotation/processing/TypeElementVisitorProcessor.java @@ -61,6 +61,7 @@ import java.util.List; import java.util.Objects; import java.util.Set; +import java.util.function.Predicate; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -228,18 +229,21 @@ public boolean process(Set annotations, RoundEnvironment TypeElement groovyObjectTypeElement = elementUtils.getTypeElement("groovy.lang.GroovyObject"); TypeMirror groovyObjectType = groovyObjectTypeElement != null ? groovyObjectTypeElement.asType() : null; + Predicate notGroovyObject = typeElement -> groovyObjectType == null || !typeUtils.isAssignable(typeElement.asType(), groovyObjectType); Set elements = new LinkedHashSet<>(); for (TypeElement annotation : annotations) { - final Set annotatedElements = roundEnv.getElementsAnnotatedWith(annotation); - includeElements(elements, annotatedElements, groovyObjectType); + modelUtils.resolveTypeElements( + roundEnv.getElementsAnnotatedWith(annotation) + ).filter(notGroovyObject).forEach(elements::add); } // This call to getRootElements() should be removed in Micronaut 4. It should not be possible // to process elements without at least one annotation present and this call breaks that assumption. - final Set rootElements = roundEnv.getRootElements(); - includeElements(elements, rootElements, groovyObjectType); + modelUtils.resolveTypeElements( + roundEnv.getRootElements() + ).filter(notGroovyObject).forEach(elements::add); if (!elements.isEmpty()) { @@ -306,18 +310,6 @@ public boolean process(Set annotations, RoundEnvironment return false; } - private void includeElements(Set target, - Set annotatedElements, TypeMirror groovyObjectType) { - annotatedElements - .stream() - .filter(element -> JavaModelUtils.isClassOrInterface(element) || JavaModelUtils.isEnum(element) || JavaModelUtils.isRecord(element)) - .map(modelUtils::classElementFor) - .filter(Objects::nonNull) - .filter(element -> element.getAnnotation(Generated.class) == null) - .filter(typeElement -> groovyObjectType == null || !typeUtils.isAssignable(typeElement.asType(), groovyObjectType)) - .forEach(target::add); - } - /** * Discovers the {@link TypeElementVisitor} instances that are available. * diff --git a/inject-java/src/test/groovy/io/micronaut/annotation/AnnotateClassSpec.groovy b/inject-java/src/test/groovy/io/micronaut/annotation/AnnotateClassSpec.groovy new file mode 100644 index 00000000000..24fa615185d --- /dev/null +++ b/inject-java/src/test/groovy/io/micronaut/annotation/AnnotateClassSpec.groovy @@ -0,0 +1,153 @@ +package io.micronaut.annotation + +import io.micronaut.annotation.processing.test.AbstractTypeElementSpec +import io.micronaut.context.annotation.Prototype +import io.micronaut.inject.ast.ClassElement +import io.micronaut.inject.visitor.TypeElementVisitor +import io.micronaut.inject.visitor.VisitorContext + +class AnnotateClassSpec extends AbstractTypeElementSpec { + + void 'test annotating 1'() { + when: + def definition = buildBeanDefinition('addann.AnnotateClass', ''' +package addann; + +import io.micronaut.context.annotation.Executable; + +class AnnotateClass { + + @Executable + public String myMethod1() { + return null; + } + +} + +''') + then: + definition.hasAnnotation(MyAnnotation) + } + + void 'test annotating 2'() { + when: + def definition = buildBeanDefinition('addann.Foobar1$AnnotateClass', ''' +package addann; + +import io.micronaut.context.annotation.Executable; + +class Foobar1 { + + @Executable + public String myMethod1() { + return null; + } + + static class AnnotateClass { + + @Executable + public String myMethod2() { + return null; + } + + } + +} + +''') + then: + definition.hasAnnotation(MyAnnotation) + } + + void 'test annotating 3'() { + when: + def definition = buildBeanDefinition('addann.Foobar2$AnnotateClass', ''' +package addann; + +import io.micronaut.context.annotation.Executable; + +class Foobar2 { + + static class AnnotateClass { + + @Executable + public String myMethod2() { + return null; + } + + } + +} + +''') + then: + definition.hasAnnotation(MyAnnotation) + } + + void 'test annotating 4'() { + when: + def definition = buildBeanDefinition('addann.Foobar3$AnnotateClass', ''' +package addann; + +import io.micronaut.context.annotation.Executable; +import io.micronaut.core.annotation.Nullable;import io.micronaut.core.annotation.ReflectiveAccess; + +class Foobar3 { + + static class AnnotateClass { + + @Executable + @ReflectiveAccess + private String myMethod2() { + return null; + } + + } + +} + +''') + then: + definition.hasAnnotation(MyAnnotation) + } + + void 'test annotating 5'() { + when: + def definition = buildBeanDefinition('addann.Foobar4$AnnotateClass', ''' +package addann; + +import io.micronaut.context.annotation.Property; + +class Foobar4 { + + static class AnnotateClass { + + @Property(name = "xyz") // Make the BeanDefinitionInjectProcessor to see the class + private String myField; + + } + +} + +''') + then: + definition.hasAnnotation(MyAnnotation) + } + + static class AnnotateClassVisitor implements TypeElementVisitor { + @Override + void visitClass(ClassElement element, VisitorContext context) { + if (element.getSimpleName().endsWith("AnnotateClass")) { + element.annotate(MyAnnotation) + element.annotate(Prototype) + + // Validate the cache is working + + def newClassElement = context.getClassElement(element.name).get() + assert newClassElement.hasAnnotation(MyAnnotation) + assert newClassElement.hasAnnotation(Prototype) + } + } + } + +} diff --git a/inject-java/src/test/resources/META-INF/services/io.micronaut.inject.visitor.TypeElementVisitor b/inject-java/src/test/resources/META-INF/services/io.micronaut.inject.visitor.TypeElementVisitor index 825b5a97fe8..8af31fade39 100644 --- a/inject-java/src/test/resources/META-INF/services/io.micronaut.inject.visitor.TypeElementVisitor +++ b/inject-java/src/test/resources/META-INF/services/io.micronaut.inject.visitor.TypeElementVisitor @@ -16,3 +16,4 @@ io.micronaut.annotation.AnnotateFieldSpec$AnnotationFieldVisitor io.micronaut.annotation.AnnotateFieldTypeSpec$AnnotateFieldTypeVisitor io.micronaut.annotation.AnnotateMethodParameterSpec$AnnotateMethodParameterVisitor io.micronaut.annotation.AnnotatePropertySpec$AnnotatePropertyVisitor +io.micronaut.annotation.AnnotateClassSpec$AnnotateClassVisitor From dff9fefe9fecaa982b4d31c0ec85ba59978d9f48 Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Mon, 6 Mar 2023 09:43:44 +0100 Subject: [PATCH 553/743] build: Micronaut Flyway 5.5.0 (#8887) --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 70ed927da44..ef9deba63ad 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -77,7 +77,7 @@ managed-micronaut-data = "3.9.6" managed-micronaut-discovery = "3.3.0" managed-micronaut-elasticsearch = "4.3.0" managed-micronaut-email = "1.5.0" -managed-micronaut-flyway = "5.4.1" +managed-micronaut-flyway = "5.5.0" managed-micronaut-gcp = "4.9.0" managed-micronaut-graphql = "3.2.0" managed-micronaut-groovy = "3.4.0" From d2ae9becdf3171f390b3cdf2b7710cb8123611dc Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Mon, 6 Mar 2023 10:51:16 +0100 Subject: [PATCH 554/743] build: Micronaut CRaC 1.2.0 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index ef9deba63ad..ecbd578e32f 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -72,7 +72,7 @@ managed-micronaut-azure = "3.8.1" managed-micronaut-cache = "3.5.0" managed-micronaut-cassandra = "5.1.1" managed-micronaut-coherence = "3.7.2" -managed-micronaut-crac = "1.1.1" +managed-micronaut-crac = "1.2.0" managed-micronaut-data = "3.9.6" managed-micronaut-discovery = "3.3.0" managed-micronaut-elasticsearch = "4.3.0" From 2ac5bddf70ec640321643988a7e66b2c9d55e276 Mon Sep 17 00:00:00 2001 From: micronaut-build <65172877+micronaut-build@users.noreply.github.com> Date: Mon, 6 Mar 2023 11:02:00 +0100 Subject: [PATCH 555/743] Bump micronaut-liquibase to 5.7.0 (#8888) --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index ecbd578e32f..ca9c61a2d38 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -92,7 +92,7 @@ managed-micronaut-kotlin = "3.2.2" managed-micronaut-kubernetes = "3.4.0" managed-micronaut-micrometer = "4.8.1" managed-micronaut-microstream = "1.3.0" -managed-micronaut-liquibase = "5.6.0" +managed-micronaut-liquibase = "5.7.0" managed-micronaut-mongo = "4.6.0" managed-micronaut-mqtt = "2.3.0" managed-micronaut-multitenancy = "4.2.0" From dfbc1f337807b5b01c5f504e004bcb2c93d7e415 Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Mon, 6 Mar 2023 17:03:45 +0100 Subject: [PATCH 556/743] build: Micronaut Azure 3.9.0 (#8889) --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index ca9c61a2d38..fd8979e75e4 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -68,7 +68,7 @@ managed-micrometer = "1.10.3" managed-micronaut-acme = "3.2.0" managed-micronaut-aot = "1.1.2" managed-micronaut-aws = "3.14.1" -managed-micronaut-azure = "3.8.1" +managed-micronaut-azure = "3.9.0" managed-micronaut-cache = "3.5.0" managed-micronaut-cassandra = "5.1.1" managed-micronaut-coherence = "3.7.2" From 5c7a9047d83f6ccea251b2df279acba0414d0de2 Mon Sep 17 00:00:00 2001 From: micronaut-build <65172877+micronaut-build@users.noreply.github.com> Date: Tue, 7 Mar 2023 07:12:45 +0100 Subject: [PATCH 557/743] Bump micronaut-aws to 3.15.0 (#8892) --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index fd8979e75e4..504162bd23f 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -67,7 +67,7 @@ managed-methvin-directory-watcher = "0.16.1" managed-micrometer = "1.10.3" managed-micronaut-acme = "3.2.0" managed-micronaut-aot = "1.1.2" -managed-micronaut-aws = "3.14.1" +managed-micronaut-aws = "3.15.0" managed-micronaut-azure = "3.9.0" managed-micronaut-cache = "3.5.0" managed-micronaut-cassandra = "5.1.1" From da54ede48bf22503daf1c624ce306e3d706def80 Mon Sep 17 00:00:00 2001 From: Denis Stepanov Date: Tue, 7 Mar 2023 01:14:40 -0500 Subject: [PATCH 558/743] Support private access for introspected beans (#8890) * Support private access for introspected beans * Correct primitives --- .../ast/utils/AstBeanPropertiesUtils.java | 1 + .../visitor/BeanIntrospectionWriter.java | 4 +- .../IntrospectedTypeElementVisitor.java | 2 +- .../inject/writer/DispatchWriter.java | 128 ++++++++++-------- .../core/annotation/Introspected.java | 17 ++- .../core/reflect/ReflectionUtils.java | 24 ++++ .../beans/BeanIntrospectionSpec.groovy | 52 +++++++ .../visitor/beans/BeanIntrospectorSpec.groovy | 3 +- .../beans/OptionalValueExtractorTest.java | 94 +++++++++++++ .../reflection/OptionalDoubleHolder.java | 16 +++ .../beans/reflection/OptionalHolder.java | 16 +++ .../beans/reflection/OptionalIntHolder.java | 16 +++ .../beans/reflection/OptionalLongHolder.java | 16 +++ .../beans/reflection/PrivateAccessTest.java | 87 ++++++++++++ .../beans/reflection/PrivateFieldBean.java | 8 ++ .../beans/reflection/PrivateFieldBean2.java | 8 ++ .../beans/reflection/PrivateMethodsBean.java | 16 +++ ...nterfaceConfigurationPropertiesSpec.groovy | 4 +- .../context/annotation/BeanProperties.java | 8 +- 19 files changed, 455 insertions(+), 65 deletions(-) create mode 100644 inject-java-test/src/test/groovy/io/micronaut/inject/visitor/beans/OptionalValueExtractorTest.java create mode 100644 inject-java-test/src/test/groovy/io/micronaut/inject/visitor/beans/reflection/OptionalDoubleHolder.java create mode 100644 inject-java-test/src/test/groovy/io/micronaut/inject/visitor/beans/reflection/OptionalHolder.java create mode 100644 inject-java-test/src/test/groovy/io/micronaut/inject/visitor/beans/reflection/OptionalIntHolder.java create mode 100644 inject-java-test/src/test/groovy/io/micronaut/inject/visitor/beans/reflection/OptionalLongHolder.java create mode 100644 inject-java-test/src/test/groovy/io/micronaut/inject/visitor/beans/reflection/PrivateAccessTest.java create mode 100644 inject-java-test/src/test/groovy/io/micronaut/inject/visitor/beans/reflection/PrivateFieldBean.java create mode 100644 inject-java-test/src/test/groovy/io/micronaut/inject/visitor/beans/reflection/PrivateFieldBean2.java create mode 100644 inject-java-test/src/test/groovy/io/micronaut/inject/visitor/beans/reflection/PrivateMethodsBean.java diff --git a/core-processor/src/main/java/io/micronaut/inject/ast/utils/AstBeanPropertiesUtils.java b/core-processor/src/main/java/io/micronaut/inject/ast/utils/AstBeanPropertiesUtils.java index 5a0dfeb942f..cf2c0b09d95 100644 --- a/core-processor/src/main/java/io/micronaut/inject/ast/utils/AstBeanPropertiesUtils.java +++ b/core-processor/src/main/java/io/micronaut/inject/ast/utils/AstBeanPropertiesUtils.java @@ -353,6 +353,7 @@ private static boolean isAccessible(MemberElement memberElement, BeanProperties. case DEFAULT -> !memberElement.isPrivate() && (memberElement.isAccessible() || memberElement.getDeclaringType().hasDeclaredStereotype(BeanProperties.class)); case PUBLIC -> memberElement.isPublic(); + case ANY -> true; }; } diff --git a/core-processor/src/main/java/io/micronaut/inject/beans/visitor/BeanIntrospectionWriter.java b/core-processor/src/main/java/io/micronaut/inject/beans/visitor/BeanIntrospectionWriter.java index 358115b3331..e58e9cf1ebb 100644 --- a/core-processor/src/main/java/io/micronaut/inject/beans/visitor/BeanIntrospectionWriter.java +++ b/core-processor/src/main/java/io/micronaut/inject/beans/visitor/BeanIntrospectionWriter.java @@ -1015,7 +1015,7 @@ public boolean supportsDispatchOne() { } @Override - public void writeDispatchOne(GeneratorAdapter writer) { + public void writeDispatchOne(GeneratorAdapter writer, int index) { writer.throwException(Type.getType(exceptionType), message); } } @@ -1039,7 +1039,7 @@ public boolean supportsDispatchOne() { } @Override - public void writeDispatchOne(GeneratorAdapter writer) { + public void writeDispatchOne(GeneratorAdapter writer, int index) { // In this case we have to do the copy constructor approach Set constructorProps = new HashSet<>(); diff --git a/core-processor/src/main/java/io/micronaut/inject/beans/visitor/IntrospectedTypeElementVisitor.java b/core-processor/src/main/java/io/micronaut/inject/beans/visitor/IntrospectedTypeElementVisitor.java index e647878d945..2b23750a8c1 100644 --- a/core-processor/src/main/java/io/micronaut/inject/beans/visitor/IntrospectedTypeElementVisitor.java +++ b/core-processor/src/main/java/io/micronaut/inject/beans/visitor/IntrospectedTypeElementVisitor.java @@ -70,7 +70,7 @@ public int getOrder() { @Override public void visitClass(ClassElement element, VisitorContext context) { - if (!element.isPrivate() && element.hasStereotype(Introspected.class)) { + if (element.hasStereotype(Introspected.class)) { final AnnotationValue introspected = element.getAnnotation(Introspected.class); if (introspected != null && !writers.containsKey(element.getName())) { processIntrospected(element, context, introspected); diff --git a/core-processor/src/main/java/io/micronaut/inject/writer/DispatchWriter.java b/core-processor/src/main/java/io/micronaut/inject/writer/DispatchWriter.java index e9a74547875..5dfcf06439b 100644 --- a/core-processor/src/main/java/io/micronaut/inject/writer/DispatchWriter.java +++ b/core-processor/src/main/java/io/micronaut/inject/writer/DispatchWriter.java @@ -69,6 +69,12 @@ public final class DispatchWriter extends AbstractClassFileWriter implements Opc private static final org.objectweb.asm.commons.Method METHOD_INVOKE_METHOD = org.objectweb.asm.commons.Method.getMethod( ReflectionUtils.getRequiredInternalMethod(ReflectionUtils.class, "invokeMethod", Object.class, java.lang.reflect.Method.class, Object[].class)); + private static final org.objectweb.asm.commons.Method METHOD_GET_FIELD_VALUE = org.objectweb.asm.commons.Method.getMethod( + ReflectionUtils.getRequiredInternalMethod(ReflectionUtils.class, "getField", Class.class, String.class, Object.class)); + + private static final org.objectweb.asm.commons.Method METHOD_SET_FIELD_VALUE = org.objectweb.asm.commons.Method.getMethod( + ReflectionUtils.getRequiredInternalMethod(ReflectionUtils.class, "setField", Class.class, String.class, Object.class, Object.class)); + private final List dispatchTargets = new ArrayList<>(); private final Type thisType; @@ -236,7 +242,7 @@ public void buildDispatchOneMethod(ClassWriter classWriter) { @Override public void generateCase(int key, Label end) { DispatchTarget method = dispatchTargets.get(key); - method.writeDispatchOne(dispatchMethod); + method.writeDispatchOne(dispatchMethod, key); dispatchMethod.returnValue(); } @@ -355,10 +361,11 @@ default boolean supportsDispatchOne() { /** * Generate dispatch one. + * @param methodIndex The method index * * @param writer The writer */ - default void writeDispatchOne(GeneratorAdapter writer) { + default void writeDispatchOne(GeneratorAdapter writer, int methodIndex) { throw new IllegalStateException("Not supported"); } @@ -404,19 +411,29 @@ public boolean supportsDispatchMulti() { } @Override - public void writeDispatchOne(GeneratorAdapter writer) { + public void writeDispatchOne(GeneratorAdapter writer, int fieldIndex) { final Type propertyType = JavaModelUtils.getTypeReference(beanField.getType()); final Type beanType = JavaModelUtils.getTypeReference(beanField.getOwningType()); - // load this - writer.loadArg(1); - pushCastToType(writer, beanType); + if (beanField.isReflectionRequired()) { + writer.push(beanType); // Bean class + writer.push(beanField.getName()); // Field name + writer.loadArg(1); // Bean instance + writer.invokeStatic(TYPE_REFLECTION_UTILS, METHOD_GET_FIELD_VALUE); + if (beanField.isPrimitive()) { + pushCastToType(writer, propertyType); + } + } else { + // load this + writer.loadArg(1); + pushCastToType(writer, beanType); - // get field value - writer.getField( + // get field value + writer.getField( JavaModelUtils.getTypeReference(beanField.getOwningType()), beanField.getName(), propertyType); + } pushBoxPrimitiveIfNecessary(propertyType, writer); } @@ -450,24 +467,32 @@ public boolean supportsDispatchMulti() { } @Override - public void writeDispatchOne(GeneratorAdapter writer) { + public void writeDispatchOne(GeneratorAdapter writer, int fieldIndex) { final Type propertyType = JavaModelUtils.getTypeReference(beanField.getType()); final Type beanType = JavaModelUtils.getTypeReference(beanField.getOwningType()); - // load this - writer.loadArg(1); - pushCastToType(writer, beanType); + if (beanField.isReflectionRequired()) { + writer.push(beanType); // Bean class + writer.push(beanField.getName()); // Field name + writer.loadArg(1); // Bean instance + writer.loadArg(2); // Field value + writer.invokeStatic(TYPE_REFLECTION_UTILS, METHOD_SET_FIELD_VALUE); + } else { + // load this + writer.loadArg(1); + pushCastToType(writer, beanType); - // load value - writer.loadArg(2); - pushCastToType(writer, propertyType); + // load value + writer.loadArg(2); + pushCastToType(writer, propertyType); - // get field value - writer.putField( + // get field value + writer.putField( beanType, beanField.getName(), propertyType); + } // push null return type writer.push((String) null); } @@ -517,6 +542,15 @@ public boolean supportsDispatchMulti() { @Override public void writeDispatchMulti(GeneratorAdapter writer, int methodIndex) { + writeDispatch(writer, methodIndex, true); + } + + @Override + public void writeDispatchOne(GeneratorAdapter writer, int methodIndex) { + writeDispatch(writer, methodIndex, false); + } + + private void writeDispatch(GeneratorAdapter writer, int methodIndex, boolean isMulti) { String methodName = methodElement.getName(); List argumentTypes = Arrays.asList(methodElement.getSuspendParameters()); @@ -541,7 +575,16 @@ public void writeDispatchMulti(GeneratorAdapter writer, int methodIndex) { writer.push(methodIndex); writer.invokeVirtual(dispatchSuperType, GET_ACCESSIBLE_TARGET_METHOD); if (hasArgs) { - writer.loadArg(2); + if (isMulti) { + writer.loadArg(2); + } else { + writer.push(1); + writer.newArray(Type.getType(Object.class)); // new Object[1] + writer.dup(); // one ref to store and one to return + writer.push(0); + writer.loadArg(2); + writer.visitInsn(AASTORE); // objects[0] = argumentAtIndex2 + } } else { writer.getStatic(Type.getType(ArrayUtils.class), "EMPTY_OBJECT_ARRAY", Type.getType(Object[].class)); } @@ -551,14 +594,20 @@ public void writeDispatchMulti(GeneratorAdapter writer, int methodIndex) { pushCastToType(writer, declaringTypeObject); } if (hasArgs) { - int argCount = argumentTypes.size(); - Iterator argIterator = argumentTypes.iterator(); - for (int i = 0; i < argCount; i++) { + if (isMulti) { + int argCount = argumentTypes.size(); + Iterator argIterator = argumentTypes.iterator(); + for (int i = 0; i < argCount; i++) { + writer.loadArg(2); + writer.push(i); + writer.visitInsn(AALOAD); + // cast the argument value to the correct type + pushCastToType(writer, argIterator.next()); + } + } else { writer.loadArg(2); - writer.push(i); - writer.visitInsn(AALOAD); - // cast the return value to the correct type - pushCastToType(writer, argIterator.next()); + // cast the argument value to the correct type + pushCastToType(writer, argumentTypes.iterator().next()); } } String methodDescriptor = getMethodDescriptor(returnType, argumentTypes); @@ -578,35 +627,6 @@ public void writeDispatchMulti(GeneratorAdapter writer, int methodIndex) { } } - @Override - public void writeDispatchOne(GeneratorAdapter writer) { - String methodName = methodElement.getName(); - - List argumentTypes = Arrays.asList(methodElement.getSuspendParameters()); - Type declaringTypeObject = JavaModelUtils.getTypeReference(declaringType); - - ClassElement returnType = methodElement.isSuspend() ? ClassElement.of(Object.class) : methodElement.getReturnType(); - boolean isInterface = declaringType.getType().isInterface(); - Type returnTypeObject = JavaModelUtils.getTypeReference(returnType); - - writer.loadArg(1); - pushCastToType(writer, declaringType); - boolean hasArgs = !argumentTypes.isEmpty(); - if (hasArgs) { - writer.loadArg(2); - pushCastToType(writer, argumentTypes.get(0)); - } - - writer.visitMethodInsn(isInterface ? INVOKEINTERFACE : INVOKEVIRTUAL, - declaringTypeObject.getInternalName(), methodName, - getMethodDescriptor(returnType, argumentTypes), isInterface); - - if (returnTypeObject.equals(Type.VOID_TYPE)) { - writer.visitInsn(ACONST_NULL); - } else { - pushBoxPrimitiveIfNecessary(returnType, writer); - } - } } /** diff --git a/core/src/main/java/io/micronaut/core/annotation/Introspected.java b/core/src/main/java/io/micronaut/core/annotation/Introspected.java index 6767e6ac0f7..a3f1c29c8fe 100644 --- a/core/src/main/java/io/micronaut/core/annotation/Introspected.java +++ b/core/src/main/java/io/micronaut/core/annotation/Introspected.java @@ -15,9 +15,13 @@ */ package io.micronaut.core.annotation; -import java.lang.annotation.*; +import java.lang.annotation.Annotation; +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; -import static java.lang.annotation.RetentionPolicy.CLASS; import static java.lang.annotation.RetentionPolicy.RUNTIME; /** @@ -219,6 +223,13 @@ enum Visibility { * The default behaviour which in addition to public getters and setters will also include package protected fields if an {@link io.micronaut.core.annotation.Introspected.AccessKind} of {@link io.micronaut.core.annotation.Introspected.AccessKind#FIELD} is specified. * */ - DEFAULT + DEFAULT, + + /** + * All methods and/or fields are included. + * + * @since 4.0.0 + */ + ANY } } diff --git a/core/src/main/java/io/micronaut/core/reflect/ReflectionUtils.java b/core/src/main/java/io/micronaut/core/reflect/ReflectionUtils.java index 2642e9523fd..470cc7b1263 100644 --- a/core/src/main/java/io/micronaut/core/reflect/ReflectionUtils.java +++ b/core/src/main/java/io/micronaut/core/reflect/ReflectionUtils.java @@ -468,4 +468,28 @@ public static Object getField(@NonNull Class clazz, @NonNull String fieldName throw new InvocationException("Exception occurred getting a field [" + fieldName + "] of class [" + clazz + "]: " + e.getMessage(), e); } } + + /** + * Sets the value of the given field reflectively. + * @param clazz The class + * @param fieldName The fieldName + * @param instance The instance + * @param value The value + * @since 4.0.0 + */ + public static void setField(@NonNull Class clazz, + @NonNull String fieldName, + @NonNull Object instance, + @Nullable Object value) { + try { + Field field = findField(clazz, fieldName) + .orElseThrow(() -> new IllegalStateException("Field with name: " + fieldName + " not found in class: " + clazz)); + ClassUtils.REFLECTION_LOGGER.debug("Reflectively setting field {} to value {} on object {}", field, value, value); + field.setAccessible(true); + field.set(instance, value); + } catch (Throwable e) { + throw new InvocationException("Exception occurred setting field [" + fieldName + "]: " + e.getMessage(), e); + } + } + } diff --git a/inject-java-test/src/test/groovy/io/micronaut/inject/visitor/beans/BeanIntrospectionSpec.groovy b/inject-java-test/src/test/groovy/io/micronaut/inject/visitor/beans/BeanIntrospectionSpec.groovy index 4a6981eec7b..d7a0a2adab6 100644 --- a/inject-java-test/src/test/groovy/io/micronaut/inject/visitor/beans/BeanIntrospectionSpec.groovy +++ b/inject-java-test/src/test/groovy/io/micronaut/inject/visitor/beans/BeanIntrospectionSpec.groovy @@ -40,6 +40,7 @@ import javax.persistence.Entity import javax.persistence.Id import javax.persistence.Version import javax.validation.Constraint +import javax.validation.constraints.DecimalMin import javax.validation.constraints.Min import javax.validation.constraints.NotBlank import javax.validation.constraints.Size @@ -4647,6 +4648,57 @@ class Holder { animal.isTypeVariable() } + void "test private property 1"() { + given: + BeanIntrospection introspection = buildBeanIntrospection('test.OptionalDoubleHolder', ''' +package test; +import io.micronaut.core.annotation.Introspected; +import javax.validation.constraints.DecimalMin; +import java.util.List; +import java.util.Collections; +import java.util.OptionalDouble; + +@Introspected(accessKind = Introspected.AccessKind.FIELD, visibility = Introspected.Visibility.ANY) +class OptionalDoubleHolder { + @DecimalMin("5") + private final OptionalDouble optionalDouble; + + private OptionalDoubleHolder(OptionalDouble optionalDouble) { + this.optionalDouble = optionalDouble; + } +} + ''') + + expect: + introspection.getProperty("optionalDouble").get().getType() == OptionalDouble.class + introspection.getProperty("optionalDouble").get().hasAnnotation(DecimalMin) + } + void "test private property 2"() { + given: + BeanIntrospection introspection = buildBeanIntrospection('test.OptionalStringHolder', ''' +package test; +import io.micronaut.core.annotation.Introspected; +import javax.validation.constraints.NotBlank; +import java.util.List; +import java.util.Collections; +import java.util.Optional; + +@Introspected(accessKind = Introspected.AccessKind.FIELD, visibility = Introspected.Visibility.ANY) +class OptionalStringHolder { + private final Optional<@NotBlank String> optionalString; + + private OptionalStringHolder(Optional optionalString) { + this.optionalString = optionalString; + } +} + ''') + + expect: + introspection.getProperty("optionalString").get().getType() == Optional.class + introspection.getProperty("optionalString").get().asArgument().getFirstTypeVariable().get().getAnnotationMetadata().hasAnnotation(NotBlank) + + } + @Override protected JavaParser newJavaParser() { return new JavaParser() { diff --git a/inject-java-test/src/test/groovy/io/micronaut/inject/visitor/beans/BeanIntrospectorSpec.groovy b/inject-java-test/src/test/groovy/io/micronaut/inject/visitor/beans/BeanIntrospectorSpec.groovy index f7254d36675..ecd21bfad77 100644 --- a/inject-java-test/src/test/groovy/io/micronaut/inject/visitor/beans/BeanIntrospectorSpec.groovy +++ b/inject-java-test/src/test/groovy/io/micronaut/inject/visitor/beans/BeanIntrospectorSpec.groovy @@ -7,7 +7,6 @@ import io.micronaut.core.beans.BeanIntrospector import spock.lang.PendingFeature import spock.lang.Specification -import jakarta.inject.Singleton import javax.persistence.Entity import javax.persistence.Id import javax.persistence.Version @@ -56,7 +55,7 @@ class BeanIntrospectorSpec extends Specification { void "test find introspections"() { expect: BeanIntrospector.SHARED.findIntrospections(Introspected).size() > 0 - BeanIntrospector.SHARED.findIntrospections(Introspected, "io.micronaut.inject.visitor.beans").size() == 5 + BeanIntrospector.SHARED.findIntrospections(Introspected, "io.micronaut.inject.visitor.beans").size() > 0 BeanIntrospector.SHARED.findIntrospections(Introspected, "blah").size() == 0 } diff --git a/inject-java-test/src/test/groovy/io/micronaut/inject/visitor/beans/OptionalValueExtractorTest.java b/inject-java-test/src/test/groovy/io/micronaut/inject/visitor/beans/OptionalValueExtractorTest.java new file mode 100644 index 00000000000..7c1cbdcbbb6 --- /dev/null +++ b/inject-java-test/src/test/groovy/io/micronaut/inject/visitor/beans/OptionalValueExtractorTest.java @@ -0,0 +1,94 @@ +package io.micronaut.inject.visitor.beans; + +import io.micronaut.core.annotation.Introspected; +import io.micronaut.core.beans.BeanIntrospection; +import io.micronaut.core.beans.BeanIntrospector; +import io.micronaut.core.beans.BeanProperty; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import javax.validation.constraints.DecimalMin; +import javax.validation.constraints.Min; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; +import java.util.Optional; +import java.util.OptionalDouble; +import java.util.OptionalInt; +import java.util.OptionalLong; + +public class OptionalValueExtractorTest { + public OptionalValueExtractorTest() { + } + + @Test + public void optionalValueExtractor() { + BeanIntrospection introspection = BeanIntrospector.SHARED.getIntrospection(OptionalHolder.class); + BeanProperty property = introspection.getProperty("optional").get(); + Assertions.assertNotNull(property.asArgument().getFirstTypeVariable().get().getAnnotationMetadata().getAnnotation(NotBlank.class)); + Assertions.assertEquals(property.getType(), Optional.class); + Assertions.assertEquals(Optional.of("hello"), property.get(new OptionalHolder(Optional.of("hello")))); + } + + @Test + public void optionalIntValueExtractor() { + BeanIntrospection introspection = BeanIntrospector.SHARED.getIntrospection(OptionalIntHolder.class); + BeanProperty property = introspection.getProperty("optionalInt").get(); + Assertions.assertNotNull(property.asArgument().getAnnotation(Min.class)); + Assertions.assertEquals(property.getType(), OptionalInt.class); + Assertions.assertEquals(OptionalInt.of(123), property.get(new OptionalIntHolder(OptionalInt.of(123)))); + } + + @Test + public void optionalLongValueExtractor() { + BeanIntrospection introspection = BeanIntrospector.SHARED.getIntrospection(OptionalLongHolder.class); + BeanProperty property = introspection.getProperty("optionalLong").get(); + Assertions.assertNotNull(property.asArgument().getAnnotation(Min.class)); + Assertions.assertEquals(property.getType(), OptionalLong.class); + Assertions.assertEquals(OptionalLong.of(123L), property.get(new OptionalLongHolder(OptionalLong.of(123L)))); + } + + @Test + public void optionalDoubleValueExtractor() { + BeanIntrospection introspection = BeanIntrospector.SHARED.getIntrospection(OptionalDoubleHolder.class); + BeanProperty property = introspection.getProperty("optionalDouble").get(); + Assertions.assertNotNull(property.asArgument().getAnnotation(DecimalMin.class)); + Assertions.assertEquals(property.getType(), OptionalDouble.class); + Assertions.assertEquals(OptionalDouble.of(12.3), property.get(new OptionalDoubleHolder(OptionalDouble.of(12.3)))); + } + + @Introspected(accessKind = Introspected.AccessKind.FIELD, visibility = Introspected.Visibility.ANY) + private static class OptionalDoubleHolder { + private final @NotNull @DecimalMin("5") OptionalDouble optionalDouble; + + private OptionalDoubleHolder(OptionalDouble optionalDouble) { + this.optionalDouble = optionalDouble; + } + } + + @Introspected(accessKind = Introspected.AccessKind.FIELD, visibility = Introspected.Visibility.ANY) + private static class OptionalLongHolder { + private final @NotNull @Min(5L) OptionalLong optionalLong; + + private OptionalLongHolder(OptionalLong optionalLong) { + this.optionalLong = optionalLong; + } + } + + @Introspected(accessKind = Introspected.AccessKind.FIELD, visibility = Introspected.Visibility.ANY) + private static class OptionalIntHolder { + private final @NotNull @Min(5L) OptionalInt optionalInt; + + private OptionalIntHolder(OptionalInt optionalInt) { + this.optionalInt = optionalInt; + } + } + + @Introspected(accessKind = Introspected.AccessKind.FIELD, visibility = Introspected.Visibility.ANY) + private static class OptionalHolder { + private final Optional<@NotNull @NotBlank String> optional; + + private OptionalHolder(Optional optional) { + this.optional = optional; + } + } +} diff --git a/inject-java-test/src/test/groovy/io/micronaut/inject/visitor/beans/reflection/OptionalDoubleHolder.java b/inject-java-test/src/test/groovy/io/micronaut/inject/visitor/beans/reflection/OptionalDoubleHolder.java new file mode 100644 index 00000000000..97a9da6f552 --- /dev/null +++ b/inject-java-test/src/test/groovy/io/micronaut/inject/visitor/beans/reflection/OptionalDoubleHolder.java @@ -0,0 +1,16 @@ +package io.micronaut.inject.visitor.beans.reflection; + +import io.micronaut.core.annotation.Introspected; + +import javax.validation.constraints.DecimalMin; +import javax.validation.constraints.NotNull; +import java.util.OptionalDouble; + +@Introspected(accessKind = Introspected.AccessKind.FIELD, visibility = Introspected.Visibility.ANY) +class OptionalDoubleHolder { + private final @NotNull @DecimalMin("5") OptionalDouble optionalDouble; + + OptionalDoubleHolder(OptionalDouble optionalDouble) { + this.optionalDouble = optionalDouble; + } +} diff --git a/inject-java-test/src/test/groovy/io/micronaut/inject/visitor/beans/reflection/OptionalHolder.java b/inject-java-test/src/test/groovy/io/micronaut/inject/visitor/beans/reflection/OptionalHolder.java new file mode 100644 index 00000000000..4b871ad0b21 --- /dev/null +++ b/inject-java-test/src/test/groovy/io/micronaut/inject/visitor/beans/reflection/OptionalHolder.java @@ -0,0 +1,16 @@ +package io.micronaut.inject.visitor.beans.reflection; + +import io.micronaut.core.annotation.Introspected; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; +import java.util.Optional; + +@Introspected(accessKind = Introspected.AccessKind.FIELD, visibility = Introspected.Visibility.ANY) +class OptionalHolder { + private final Optional<@NotNull @NotBlank String> optional; + + OptionalHolder(Optional optional) { + this.optional = optional; + } +} diff --git a/inject-java-test/src/test/groovy/io/micronaut/inject/visitor/beans/reflection/OptionalIntHolder.java b/inject-java-test/src/test/groovy/io/micronaut/inject/visitor/beans/reflection/OptionalIntHolder.java new file mode 100644 index 00000000000..9be2987d951 --- /dev/null +++ b/inject-java-test/src/test/groovy/io/micronaut/inject/visitor/beans/reflection/OptionalIntHolder.java @@ -0,0 +1,16 @@ +package io.micronaut.inject.visitor.beans.reflection; + +import io.micronaut.core.annotation.Introspected; + +import javax.validation.constraints.Min; +import javax.validation.constraints.NotNull; +import java.util.OptionalInt; + +@Introspected(accessKind = Introspected.AccessKind.FIELD, visibility = Introspected.Visibility.ANY) +class OptionalIntHolder { + private final @NotNull @Min(5L) OptionalInt optionalInt; + + OptionalIntHolder(OptionalInt optionalInt) { + this.optionalInt = optionalInt; + } +} diff --git a/inject-java-test/src/test/groovy/io/micronaut/inject/visitor/beans/reflection/OptionalLongHolder.java b/inject-java-test/src/test/groovy/io/micronaut/inject/visitor/beans/reflection/OptionalLongHolder.java new file mode 100644 index 00000000000..b3eadeea835 --- /dev/null +++ b/inject-java-test/src/test/groovy/io/micronaut/inject/visitor/beans/reflection/OptionalLongHolder.java @@ -0,0 +1,16 @@ +package io.micronaut.inject.visitor.beans.reflection; + +import io.micronaut.core.annotation.Introspected; + +import javax.validation.constraints.Min; +import javax.validation.constraints.NotNull; +import java.util.OptionalLong; + +@Introspected(accessKind = Introspected.AccessKind.FIELD, visibility = Introspected.Visibility.ANY) +class OptionalLongHolder { + private final @NotNull @Min(5L) OptionalLong optionalLong; + + OptionalLongHolder(OptionalLong optionalLong) { + this.optionalLong = optionalLong; + } +} diff --git a/inject-java-test/src/test/groovy/io/micronaut/inject/visitor/beans/reflection/PrivateAccessTest.java b/inject-java-test/src/test/groovy/io/micronaut/inject/visitor/beans/reflection/PrivateAccessTest.java new file mode 100644 index 00000000000..ba121dd85f9 --- /dev/null +++ b/inject-java-test/src/test/groovy/io/micronaut/inject/visitor/beans/reflection/PrivateAccessTest.java @@ -0,0 +1,87 @@ +package io.micronaut.inject.visitor.beans.reflection; + +import io.micronaut.core.beans.BeanIntrospection; +import io.micronaut.core.beans.BeanIntrospector; +import io.micronaut.core.beans.BeanProperty; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import javax.validation.constraints.DecimalMin; +import javax.validation.constraints.Min; +import javax.validation.constraints.NotBlank; +import java.util.Optional; +import java.util.OptionalDouble; +import java.util.OptionalInt; +import java.util.OptionalLong; + +public class PrivateAccessTest { + public PrivateAccessTest() { + } + + @Test + public void optionalValueExtractor() { + BeanIntrospection introspection = BeanIntrospector.SHARED.getIntrospection(OptionalHolder.class); + BeanProperty property = introspection.getProperty("optional").get(); + Assertions.assertNotNull(property.asArgument().getFirstTypeVariable().get().getAnnotationMetadata().getAnnotation(NotBlank.class)); + Assertions.assertEquals(property.getType(), Optional.class); + Assertions.assertEquals(Optional.of("hello"), property.get(new OptionalHolder(Optional.of("hello")))); + } + + @Test + public void optionalIntValueExtractor() { + BeanIntrospection introspection = BeanIntrospector.SHARED.getIntrospection(OptionalIntHolder.class); + BeanProperty property = introspection.getProperty("optionalInt").get(); + Assertions.assertNotNull(property.asArgument().getAnnotation(Min.class)); + Assertions.assertEquals(property.getType(), OptionalInt.class); + Assertions.assertEquals(OptionalInt.of(123), property.get(new OptionalIntHolder(OptionalInt.of(123)))); + } + + @Test + public void optionalLongValueExtractor() { + BeanIntrospection introspection = BeanIntrospector.SHARED.getIntrospection(OptionalLongHolder.class); + BeanProperty property = introspection.getProperty("optionalLong").get(); + Assertions.assertNotNull(property.asArgument().getAnnotation(Min.class)); + Assertions.assertEquals(property.getType(), OptionalLong.class); + Assertions.assertEquals(OptionalLong.of(123L), property.get(new OptionalLongHolder(OptionalLong.of(123L)))); + } + + @Test + public void optionalDoubleValueExtractor() { + BeanIntrospection introspection = BeanIntrospector.SHARED.getIntrospection(OptionalDoubleHolder.class); + BeanProperty property = introspection.getProperty("optionalDouble").get(); + Assertions.assertNotNull(property.asArgument().getAnnotation(DecimalMin.class)); + Assertions.assertEquals(property.getType(), OptionalDouble.class); + Assertions.assertEquals(OptionalDouble.of(12.3), property.get(new OptionalDoubleHolder(OptionalDouble.of(12.3)))); + } + + @Test + public void privateFieldWrite() { + PrivateFieldBean bean = new PrivateFieldBean(); + BeanIntrospection introspection = BeanIntrospector.SHARED.getIntrospection(PrivateFieldBean.class); + BeanProperty property = introspection.getProperty("name").get(); + Assertions.assertNull(property.get(bean)); + property.set(bean, "hello"); + Assertions.assertEquals("hello", property.get(bean)); + } + + @Test + public void privateFieldWrite2() { + PrivateFieldBean2 bean = new PrivateFieldBean2(); + BeanIntrospection introspection = BeanIntrospector.SHARED.getIntrospection(PrivateFieldBean2.class); + BeanProperty property = introspection.getProperty("abc").get(); + Assertions.assertEquals(0, property.get(bean)); + property.set(bean, 123); + Assertions.assertEquals(123, property.get(bean)); + } + + @Test + public void privateMethodWrite() { + PrivateMethodsBean bean = new PrivateMethodsBean(); + BeanIntrospection introspection = BeanIntrospector.SHARED.getIntrospection(PrivateMethodsBean.class); + BeanProperty property = introspection.getProperty("name").get(); + Assertions.assertNull(property.get(bean)); + property.set(bean, "hello"); + Assertions.assertEquals("hello", property.get(bean)); + } + +} diff --git a/inject-java-test/src/test/groovy/io/micronaut/inject/visitor/beans/reflection/PrivateFieldBean.java b/inject-java-test/src/test/groovy/io/micronaut/inject/visitor/beans/reflection/PrivateFieldBean.java new file mode 100644 index 00000000000..332a686fc49 --- /dev/null +++ b/inject-java-test/src/test/groovy/io/micronaut/inject/visitor/beans/reflection/PrivateFieldBean.java @@ -0,0 +1,8 @@ +package io.micronaut.inject.visitor.beans.reflection; + +import io.micronaut.core.annotation.Introspected; + +@Introspected(accessKind = Introspected.AccessKind.FIELD, visibility = Introspected.Visibility.ANY) +class PrivateFieldBean { + private String name; +} diff --git a/inject-java-test/src/test/groovy/io/micronaut/inject/visitor/beans/reflection/PrivateFieldBean2.java b/inject-java-test/src/test/groovy/io/micronaut/inject/visitor/beans/reflection/PrivateFieldBean2.java new file mode 100644 index 00000000000..92019e8c958 --- /dev/null +++ b/inject-java-test/src/test/groovy/io/micronaut/inject/visitor/beans/reflection/PrivateFieldBean2.java @@ -0,0 +1,8 @@ +package io.micronaut.inject.visitor.beans.reflection; + +import io.micronaut.core.annotation.Introspected; + +@Introspected(accessKind = Introspected.AccessKind.FIELD, visibility = Introspected.Visibility.ANY) +class PrivateFieldBean2 { + private int abc; +} diff --git a/inject-java-test/src/test/groovy/io/micronaut/inject/visitor/beans/reflection/PrivateMethodsBean.java b/inject-java-test/src/test/groovy/io/micronaut/inject/visitor/beans/reflection/PrivateMethodsBean.java new file mode 100644 index 00000000000..6c602a3ef18 --- /dev/null +++ b/inject-java-test/src/test/groovy/io/micronaut/inject/visitor/beans/reflection/PrivateMethodsBean.java @@ -0,0 +1,16 @@ +package io.micronaut.inject.visitor.beans.reflection; + +import io.micronaut.core.annotation.Introspected; + +@Introspected(accessKind = Introspected.AccessKind.METHOD, visibility = Introspected.Visibility.ANY) +class PrivateMethodsBean { + private String name; + + private String getName() { + return name; + } + + private void setName(String name) { + this.name = name; + } +} diff --git a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/inject/configproperties/InterfaceConfigurationPropertiesSpec.groovy b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/inject/configproperties/InterfaceConfigurationPropertiesSpec.groovy index bc475ab685c..af1c96216be 100644 --- a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/inject/configproperties/InterfaceConfigurationPropertiesSpec.groovy +++ b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/inject/configproperties/InterfaceConfigurationPropertiesSpec.groovy @@ -8,10 +8,10 @@ import io.micronaut.inject.InstantiatableBeanDefinition import io.micronaut.inject.ValidatedBeanDefinition import io.micronaut.runtime.context.env.ConfigurationAdvice import spock.lang.Specification -import static io.micronaut.annotation.processing.test.KotlinCompiler.* -class InterfaceConfigurationPropertiesSpec extends Specification { +import static io.micronaut.annotation.processing.test.KotlinCompiler.buildBeanDefinition +class InterfaceConfigurationPropertiesSpec extends Specification { void "test simple interface config props"() { when: diff --git a/inject/src/main/java/io/micronaut/context/annotation/BeanProperties.java b/inject/src/main/java/io/micronaut/context/annotation/BeanProperties.java index 122535caa96..b57c9c48827 100644 --- a/inject/src/main/java/io/micronaut/context/annotation/BeanProperties.java +++ b/inject/src/main/java/io/micronaut/context/annotation/BeanProperties.java @@ -132,6 +132,12 @@ enum Visibility { /** * The default behaviour which in addition to public getters and setters will also include package protected fields if an {@link BeanProperties.AccessKind} of {@link BeanProperties.AccessKind#FIELD} is specified. */ - DEFAULT + DEFAULT, + + /** + * All methods and/or fields are included. + * + */ + ANY } } From 90fae9bb0f27e7255466de5d2315fa9aa3506236 Mon Sep 17 00:00:00 2001 From: James Kleeh Date: Tue, 7 Mar 2023 03:44:25 -0500 Subject: [PATCH 559/743] Prevent duplicate Date headers when returning files from controllers (#8872) Co-authored-by: Sergio del Amo --- .../http/server/netty/types/files/FileTypeHandler.java | 4 +++- .../http/server/netty/types/FileTypeHandlerSpec.groovy | 1 + 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/types/files/FileTypeHandler.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/types/files/FileTypeHandler.java index 0be16a28715..b8a1b03c2d6 100644 --- a/http-server-netty/src/main/java/io/micronaut/http/server/netty/types/files/FileTypeHandler.java +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/types/files/FileTypeHandler.java @@ -119,7 +119,9 @@ protected void setDateAndCacheHeaders(MutableHttpResponse response, long lastMod // Date header MutableHttpHeaders headers = response.getHeaders(); LocalDateTime now = LocalDateTime.now(); - headers.date(now); + if (!headers.contains(HttpHeaders.DATE)) { + headers.date(now); + } // Add cache headers LocalDateTime cacheSeconds = now.plus(configuration.getCacheSeconds(), ChronoUnit.SECONDS); diff --git a/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/types/FileTypeHandlerSpec.groovy b/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/types/FileTypeHandlerSpec.groovy index c3900daae9e..d00e6681301 100644 --- a/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/types/FileTypeHandlerSpec.groovy +++ b/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/types/FileTypeHandlerSpec.groovy @@ -75,6 +75,7 @@ class FileTypeHandlerSpec extends AbstractMicronautSpec { response.header(CONTENT_TYPE) == "text/html" Integer.parseInt(response.header(CONTENT_LENGTH)) > 0 response.headers.getDate(DATE) < response.headers.getDate(EXPIRES) + response.headers.getAll(DATE).size() == 1 response.header(CACHE_CONTROL) == "private, max-age=60" response.headers.getDate(LAST_MODIFIED) == ZonedDateTime.ofInstant(Instant.ofEpochMilli(tempFile.lastModified()), ZoneId.of("GMT")).truncatedTo(ChronoUnit.SECONDS) response.body() == tempFileContents From c8b9489ef0c9e5892207dd4fa12c3c6142c17086 Mon Sep 17 00:00:00 2001 From: micronaut-build <65172877+micronaut-build@users.noreply.github.com> Date: Tue, 7 Mar 2023 10:54:37 +0100 Subject: [PATCH 560/743] Bump micronaut-reactor to 2.6.0 (#8894) --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 504162bd23f..db68cbbe378 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -106,7 +106,7 @@ managed-micronaut-picocli = "4.3.0" managed-micronaut-problem = "2.6.0" managed-micronaut-rabbitmq = "3.4.1" managed-micronaut-r2dbc = "4.0.0" -managed-micronaut-reactor = "2.5.0" +managed-micronaut-reactor = "2.6.0" managed-micronaut-redis = "5.3.2" managed-micronaut-rss = "3.2.0" managed-micronaut-rxjava1 = "1.0.0" From 70f67ac44c2c542b209c8754ba7f85facc9f950f Mon Sep 17 00:00:00 2001 From: micronaut-build <65172877+micronaut-build@users.noreply.github.com> Date: Tue, 7 Mar 2023 10:55:13 +0100 Subject: [PATCH 561/743] Bump micronaut-serialization to 1.5.2 (#8895) --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index ceb52f354b7..6a455c22e34 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -113,7 +113,7 @@ managed-micronaut-rxjava1 = "1.0.0" managed-micronaut-rxjava2 = "1.3.0" managed-micronaut-rxjava3 = "2.4.0" managed-micronaut-security = "3.9.3" -managed-micronaut-serialization = "1.5.1" +managed-micronaut-serialization = "1.5.2" managed-micronaut-servlet = "3.3.5" managed-micronaut-spring = "4.4.0" managed-micronaut-sql = "4.7.2" From 7f62cc5abba52c934204a37d10743d84e96ebbc0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Champeau?= Date: Tue, 7 Mar 2023 15:08:07 +0100 Subject: [PATCH 562/743] Fix version parsing (#8897) The previous check assumed that we could only find `-SNAPSHOT`, but for a release, we may have `-M1` or `-beta-1`, etc. Fixes error seen at https://ge.micronaut.io/s/aenwsqygoz6ik --- core-bom/build.gradle | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/core-bom/build.gradle b/core-bom/build.gradle index 9ad103622b5..c014b8bd4ce 100644 --- a/core-bom/build.gradle +++ b/core-bom/build.gradle @@ -15,7 +15,9 @@ micronautBom { micronautBuild { binaryCompatibility { - def (major, minor, patch) = (version - '-SNAPSHOT').split('[.]').collect { it.toInteger() } + def dash = version.indexOf('-') + def v = dash > 0 ? version.substring(0, dash) : version + def (major, minor, patch) = v.split('[.]').collect { it.toInteger() } enabled = major > 4 || (major == 4 && minor > 0) || (major == 4 && minor == 0 && patch > 0) } } From 11e60e716b9094486515d1164262d4a68f17207f Mon Sep 17 00:00:00 2001 From: micronaut-build <65172877+micronaut-build@users.noreply.github.com> Date: Tue, 7 Mar 2023 15:16:51 +0100 Subject: [PATCH 563/743] Update common files (#8689) --- .editorconfig | 24 ----------- .github/workflows/graalvm.yml | 40 +++++++----------- .github/workflows/gradle.yml | 42 +++++++------------ .github/workflows/release-notes.yml | 50 ----------------------- .github/workflows/release.yml | 2 +- .github/workflows/sonarqube.yml | 12 +++--- gradle/wrapper/gradle-wrapper.jar | Bin 61574 -> 61608 bytes gradle/wrapper/gradle-wrapper.properties | 2 +- gradlew | 4 +- 9 files changed, 40 insertions(+), 136 deletions(-) delete mode 100644 .github/workflows/release-notes.yml diff --git a/.editorconfig b/.editorconfig index 573a247281b..a9a165bae9f 100644 --- a/.editorconfig +++ b/.editorconfig @@ -19,29 +19,5 @@ max_line_length = 100 # Import order can be configured with ij_java_imports_layout=... # See documentation https://youtrack.jetbrains.com/issue/IDEA-170643#focus=streamItem-27-3708697.0-0 -# don't use wildcard for imports -ij_java_class_count_to_use_import_on_demand = 9999 -ij_java_names_count_to_use_import_on_demand = 9999 - -[*.kt] -indent_size = 4 -tab_width = 4 -ij_continuation_indent_size = 8 -max_line_length = 100 - -# don't use wildcard for imports -ij_kotlin_name_count_to_use_star_import = 9999 -ij_kotlin_name_count_to_use_star_import_for_members = 9999 - -[*.groovy] -indent_size = 4 -tab_width = 4 -ij_continuation_indent_size = 8 -max_line_length = 100 - -# don't use wildcard for imports -ij_groovy_class_count_to_use_import_on_demand = 9999 -ij_groovy_names_count_to_use_import_on_demand = 9999 - [*.xml] indent_size = 4 diff --git a/.github/workflows/graalvm.yml b/.github/workflows/graalvm.yml index c06db472f01..4708c913038 100644 --- a/.github/workflows/graalvm.yml +++ b/.github/workflows/graalvm.yml @@ -22,13 +22,13 @@ jobs: graalvm: [ 'latest'] java: [ '17' ] steps: - # https://github.com/actions/virtual-environments/issues/709 + # https://github.com/actions/virtual-environments/issues/709 - name: Free disk space run: | - sudo rm -rf "/usr/local/share/boost" - sudo rm -rf "$AGENT_TOOLSDIRECTORY" - sudo apt-get clean - df -h + sudo rm -rf "/usr/local/share/boost" + sudo rm -rf "$AGENT_TOOLSDIRECTORY" + sudo apt-get clean + df -h - uses: actions/checkout@v3 - uses: actions/cache@v3 with: @@ -44,7 +44,7 @@ jobs: components: 'native-image' github-token: ${{ secrets.GITHUB_TOKEN }} - name: Setup Gradle - uses: gradle/gradle-build-action@v2.3.3 + uses: gradle/gradle-build-action@v2.4.0 - name: Build with Gradle id: gradle run: | @@ -55,28 +55,16 @@ jobs: ./gradlew check --continue --no-daemon fi env: - TESTCONTAINERS_RYUK_DISABLED: true - GH_TOKEN_PUBLIC_REPOS_READONLY: ${{ secrets.GH_TOKEN_PUBLIC_REPOS_READONLY }} - GH_USERNAME: ${{ secrets.GH_USERNAME }} - GRADLE_ENTERPRISE_ACCESS_KEY: ${{ secrets.GRADLE_ENTERPRISE_ACCESS_KEY }} - GRADLE_ENTERPRISE_CACHE_USERNAME: ${{ secrets.GRADLE_ENTERPRISE_CACHE_USERNAME }} - GRADLE_ENTERPRISE_CACHE_PASSWORD: ${{ secrets.GRADLE_ENTERPRISE_CACHE_PASSWORD }} - PREDICTIVE_TEST_SELECTION: "${{ github.event_name == 'pull_request' && 'true' || 'false' }}" - - name: Add build scan URL as PR comment - uses: actions/github-script@v5 - if: github.event_name == 'pull_request' && failure() - with: - github-token: ${{secrets.GITHUB_TOKEN}} - script: | - github.rest.issues.createComment({ - issue_number: context.issue.number, - owner: context.repo.owner, - repo: context.repo.repo, - body: '❌ ${{ github.workflow }} ${{ matrix.java }} ${{ matrix.graalvm }} failed: ${{ steps.gradle.outputs.build-scan-url }}' - }) + TESTCONTAINERS_RYUK_DISABLED: true + GH_TOKEN_PUBLIC_REPOS_READONLY: ${{ secrets.GH_TOKEN_PUBLIC_REPOS_READONLY }} + GH_USERNAME: ${{ secrets.GH_USERNAME }} + GRADLE_ENTERPRISE_ACCESS_KEY: ${{ secrets.GRADLE_ENTERPRISE_ACCESS_KEY }} + GRADLE_ENTERPRISE_CACHE_USERNAME: ${{ secrets.GRADLE_ENTERPRISE_CACHE_USERNAME }} + GRADLE_ENTERPRISE_CACHE_PASSWORD: ${{ secrets.GRADLE_ENTERPRISE_CACHE_PASSWORD }} + PREDICTIVE_TEST_SELECTION: "${{ github.event_name == 'pull_request' && 'true' || 'false' }}" - name: Publish Test Report if: always() - uses: mikepenz/action-junit-report@v3.7.1 + uses: mikepenz/action-junit-report@v3.7.5 with: check_name: GraalVM CE CI / Test Report (Java ${{ matrix.java }}) report_paths: '**/build/test-results/test/TEST-*.xml' diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index 1ae4689dc35..ed53f091054 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -21,13 +21,13 @@ jobs: matrix: java: ['17'] steps: - # https://github.com/actions/virtual-environments/issues/709 + # https://github.com/actions/virtual-environments/issues/709 - name: Free disk space run: | - sudo rm -rf "/usr/local/share/boost" - sudo rm -rf "$AGENT_TOOLSDIRECTORY" - sudo apt-get clean - df -h + sudo rm -rf "/usr/local/share/boost" + sudo rm -rf "$AGENT_TOOLSDIRECTORY" + sudo apt-get clean + df -h - uses: actions/checkout@v3 - name: Set up JDK uses: actions/setup-java@v3 @@ -35,7 +35,7 @@ jobs: distribution: 'temurin' java-version: ${{ matrix.java }} - name: Setup Gradle - uses: gradle/gradle-build-action@v2.3.3 + uses: gradle/gradle-build-action@v2.4.0 - name: Optional setup step env: GRADLE_ENTERPRISE_ACCESS_KEY: ${{ secrets.GRADLE_ENTERPRISE_ACCESS_KEY }} @@ -48,28 +48,16 @@ jobs: run: | ./gradlew check --no-daemon --parallel --continue env: - GH_TOKEN_PUBLIC_REPOS_READONLY: ${{ secrets.GH_TOKEN_PUBLIC_REPOS_READONLY }} - GH_USERNAME: ${{ secrets.GH_USERNAME }} - TESTCONTAINERS_RYUK_DISABLED: true - GRADLE_ENTERPRISE_ACCESS_KEY: ${{ secrets.GRADLE_ENTERPRISE_ACCESS_KEY }} - GRADLE_ENTERPRISE_CACHE_USERNAME: ${{ secrets.GRADLE_ENTERPRISE_CACHE_USERNAME }} - GRADLE_ENTERPRISE_CACHE_PASSWORD: ${{ secrets.GRADLE_ENTERPRISE_CACHE_PASSWORD }} - PREDICTIVE_TEST_SELECTION: "${{ github.event_name == 'pull_request' && 'true' || 'false' }}" - - name: Add build scan URL as PR comment - uses: actions/github-script@v5 - if: github.event_name == 'pull_request' && failure() - with: - github-token: ${{secrets.GITHUB_TOKEN}} - script: | - github.rest.issues.createComment({ - issue_number: context.issue.number, - owner: context.repo.owner, - repo: context.repo.repo, - body: '❌ ${{ github.workflow }} failed: ${{ steps.gradle.outputs.build-scan-url }}' - }) + GH_TOKEN_PUBLIC_REPOS_READONLY: ${{ secrets.GH_TOKEN_PUBLIC_REPOS_READONLY }} + GH_USERNAME: ${{ secrets.GH_USERNAME }} + TESTCONTAINERS_RYUK_DISABLED: true + GRADLE_ENTERPRISE_ACCESS_KEY: ${{ secrets.GRADLE_ENTERPRISE_ACCESS_KEY }} + GRADLE_ENTERPRISE_CACHE_USERNAME: ${{ secrets.GRADLE_ENTERPRISE_CACHE_USERNAME }} + GRADLE_ENTERPRISE_CACHE_PASSWORD: ${{ secrets.GRADLE_ENTERPRISE_CACHE_PASSWORD }} + PREDICTIVE_TEST_SELECTION: "${{ github.event_name == 'pull_request' && 'true' || 'false' }}" - name: Publish Test Report if: always() - uses: mikepenz/action-junit-report@v3.7.1 + uses: mikepenz/action-junit-report@v3.7.5 with: check_name: Java CI / Test Report (${{ matrix.java }}) report_paths: '**/build/test-results/test/TEST-*.xml' @@ -84,7 +72,7 @@ jobs: if: success() && github.event_name == 'push' && matrix.java == '17' env: GH_TOKEN_PUBLIC_REPOS_READONLY: ${{ secrets.GH_TOKEN_PUBLIC_REPOS_READONLY }} - GH_USERNAME: ${{ secrets.GH_USERNAME }} + GH_USERNAME: ${{ secrets.GH_USERNAME }} SONATYPE_USERNAME: ${{ secrets.SONATYPE_USERNAME }} SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }} GRADLE_ENTERPRISE_ACCESS_KEY: ${{ secrets.GRADLE_ENTERPRISE_ACCESS_KEY }} diff --git a/.github/workflows/release-notes.yml b/.github/workflows/release-notes.yml deleted file mode 100644 index ecb1f667c65..00000000000 --- a/.github/workflows/release-notes.yml +++ /dev/null @@ -1,50 +0,0 @@ -# WARNING: Do not edit this file directly. Instead, go to: -# -# https://github.com/micronaut-projects/micronaut-project-template/tree/master/.github/workflows -# -# and edit them there. Note that it will be sync'ed to all the Micronaut repos -name: Changelog -on: - issues: - types: [closed,reopened] - push: - branches: - - master - - '[1-9]+.[0-9]+.x' -jobs: - release_notes: - if: github.repository != 'micronaut-projects/micronaut-project-template' - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - name: Check if it has release drafter config file - id: check_release_drafter - run: | - has_release_drafter=$([ -f .github/release-drafter.yml ] && echo "true" || echo "false") - echo "has_release_drafter=${has_release_drafter}" >> $GITHUB_OUTPUT - - # If it has release drafter: - - uses: release-drafter/release-drafter@v5 - if: steps.check_release_drafter.outputs.has_release_drafter == 'true' - env: - GITHUB_TOKEN: ${{ secrets.GH_TOKEN }} - - # Otherwise: - - name: Export Gradle Properties - if: steps.check_release_drafter.outputs.has_release_drafter == 'false' - uses: micronaut-projects/github-actions/export-gradle-properties@master - - uses: micronaut-projects/github-actions/release-notes@master - if: steps.check_release_drafter.outputs.has_release_drafter == 'false' - id: release_notes - with: - token: ${{ secrets.GH_TOKEN }} - - uses: ncipollo/release-action@v1 - if: steps.check_release_drafter.outputs.has_release_drafter == 'false' && steps.release_notes.outputs.generated_changelog == 'true' - with: - allowUpdates: true - commit: ${{ steps.release_notes.outputs.current_branch }} - draft: true - name: ${{ env.title }} ${{ steps.release_notes.outputs.next_version }} - tag: v${{ steps.release_notes.outputs.next_version }} - bodyFile: CHANGELOG.md - token: ${{ secrets.GH_TOKEN }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b6b487f4a90..a079b9d8928 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -149,7 +149,7 @@ jobs: actions: read # To read the workflow path. id-token: write # To sign the provenance. contents: write # To add assets to a release. - uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v1.4.0 + uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v1.5.0 with: base64-subjects: "${{ needs.provenance-subject.outputs.artifacts-sha256 }}" upload-assets: true # Upload to a new release. diff --git a/.github/workflows/sonarqube.yml b/.github/workflows/sonarqube.yml index ac6029e85cf..37231c88d10 100644 --- a/.github/workflows/sonarqube.yml +++ b/.github/workflows/sonarqube.yml @@ -18,13 +18,13 @@ jobs: if: github.repository != 'micronaut-projects/micronaut-project-template' runs-on: ubuntu-latest steps: - # https://github.com/actions/virtual-environments/issues/709 + # https://github.com/actions/virtual-environments/issues/709 - name: Free disk space run: | - sudo rm -rf "/usr/local/share/boost" - sudo rm -rf "$AGENT_TOOLSDIRECTORY" - sudo apt-get clean - df -h + sudo rm -rf "/usr/local/share/boost" + sudo rm -rf "$AGENT_TOOLSDIRECTORY" + sudo apt-get clean + df -h - uses: actions/checkout@v3 with: fetch-depth: 0 @@ -56,3 +56,5 @@ jobs: GRADLE_ENTERPRISE_CACHE_PASSWORD: ${{ secrets.GRADLE_ENTERPRISE_CACHE_PASSWORD }} SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_TOKEN_PUBLIC_REPOS_READONLY: ${{ secrets.GH_TOKEN_PUBLIC_REPOS_READONLY }} + GH_USERNAME: ${{ secrets.GH_USERNAME }} diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 943f0cbfa754578e88a3dae77fce6e3dea56edbf..ccebba7710deaf9f98673a68957ea02138b60d0a 100644 GIT binary patch delta 5094 zcmZu#c|6qH|DG9RA4`noBZNWrC2N)tSqjO%%aX0^O4dPAB*iC6_9R<`apl^#h-_oY z)(k_0v8Fxp{fyi9-uwN%e)GpU&v~BrS>~KG^PF=MNmQjIDr&QHR7f-kM{%U_u*1=5 zGC}ae5(^Rrg9QY8$x^}oiJ0d2O9YW{J~$dD1ovlvh&0B4L)!4S=z;Hac>K{#9q9cKq;>>BtKo1!+gw`yqE zSK8x^jC|B!qmSW#uyb@T^CkB9qRd{N3V-rEi}AEgoU_J27lw_0X`}c0&m9JhxM;RK z54_gdZ(u?R5`B3}NeVal2NTHqlktM`2eTF28%6BZCWW$-shf0l-BOVSm)hU58MTPy zDcY-5777j;ccU!Yba8wH=X6OdPJ8O5Kp^3gUNo>!b=xb6T2F&LiC2eBJj8KuLPW!4 zw3V^NnAKZm^D?tmliCvzi>UtoDH%V#%SM0d*NS+m%4}qO<)M1E{OpQ(v&ZNc`vdi| zEGlVi$Dgxy1p6+k0qGLQt(JwxZxLCZ4>wJ=sb0v%Ki?*+!ic_2exumn{%Co|| z-axdK#RUC;P|vqbe?L`K!j;sUo=uuR_#ZkRvBf%Txo6{OL&I(?dz?47Z(DcX3KTw> zGY%A=kX;fBkq$F^sX|-)1Qkg##+n-Ci{qJVPj@P?l_1Y`nD^v>fZ3HMX%(4p-TlD(>yWwJij!6Jw}l7h>CIm@Ou5B@$Wy`Ky*814%Mdi1GfG1zDG9NogaoVHHr4gannv4?w6g&10!j=lKM zFW;@=Z0}vAPAxA=R4)|`J??*$|Fh`5=ks*V7TapX`+=4n*{aXxRhh-EGX_Xrzjb4r zn0vO7Cc~wtyeM_8{**~9y7>+}1JV8Buhg%*hy|PUc#!vw#W(HFTL|BpM)U0>JxG6S zLnqn1!0++RyyJ>5VU<4mDv8>Q#{EtgS3mj7Hx}Zkr0tz1}h8Kn6q`MiwC z{Y#;D!-ndlImST(C@(*i5f0U(jD29G7g#nkiPX zki6M$QYX_fNH=E4_eg9*FFZ3wF9YAKC}CP89Kl(GNS(Ag994)0$OL4-fj_1EdR}ARB#-vP_$bWF`Qk58+ z4Jq*-YkcmCuo9U%oxGeYe7Be=?n}pX+x>ob(8oPLDUPiIryT8v*N4@0{s_VYALi;lzj19ivLJKaXt7~UfU|mu9zjbhPnIhG2`uI34urWWA9IO{ z_1zJ)lwSs{qt3*UnD}3qB^kcRZ?``>IDn>qp8L96bRaZH)Zl`!neewt(wjSk1i#zf zb8_{x_{WRBm9+0CF4+nE)NRe6K8d|wOWN)&-3jCDiK5mj>77=s+TonlH5j`nb@rB5 z5NX?Z1dk`E#$BF{`(D>zISrMo4&}^wmUIyYL-$PWmEEfEn-U0tx_vy$H6|+ zi{ytv2@JXBsot|%I5s74>W1K{-cvj0BYdNiRJz*&jrV9>ZXYZhEMULcM=fCmxkN&l zEoi=)b)Vazc5TQC&Q$oEZETy@!`Gnj`qoXl7mcwdY@3a-!SpS2Mau|uK#++@>H8QC zr2ld8;<_8We%@E?S=E?=e9c$BL^9X?bj*4W;<+B&OOe+3{<`6~*fC(=`TO>o^A(Y! zA`Qc1ky?*6xjVfR?ugE~oY`Gtzhw^{Z@E6vZ`mMRAp>Odpa!m zzWmtjT|Lj^qiZMfj%%un-o$Eu>*v12qF{$kCKai^?DF=$^tfyV%m9;W@pm-BZn_6b z{jsXY3!U`%9hzk6n7YyHY%48NhjI6jjuUn?Xfxe0`ARD_Q+T_QBZ{ zUK@!63_Wr`%9q_rh`N4=J=m;v>T{Y=ZLKN^m?(KZQ2J%|3`hV0iogMHJ} zY6&-nXirq$Yhh*CHY&Qf*b@@>LPTMf z(cMorwW?M11RN{H#~ApKT)F!;R#fBHahZGhmy>Sox`rk>>q&Y)RG$-QwH$_TWk^hS zTq2TC+D-cB21|$g4D=@T`-ATtJ?C=aXS4Q}^`~XjiIRszCB^cvW0OHe5;e~9D%D10 zl4yP4O=s-~HbL7*4>#W52eiG7*^Hi)?@-#*7C^X5@kGwK+paI>_a2qxtW zU=xV7>QQROWQqVfPcJ$4GSx`Y23Z&qnS?N;%mjHL*EVg3pBT{V7bQUI60jtBTS?i~ zycZ4xqJ<*3FSC6_^*6f)N|sgB5Bep(^%)$=0cczl>j&n~KR!7WC|3;Zoh_^GuOzRP zo2Hxf50w9?_4Qe368fZ0=J|fR*jO_EwFB1I^g~i)roB|KWKf49-)!N%Ggb%w=kB8)(+_%kE~G!(73aF=yCmM3Cfb9lV$G!b zoDIxqY{dH>`SILGHEJwq%rwh46_i`wkZS-NY95qdNE)O*y^+k#JlTEij8NT(Y_J!W zFd+YFoZB|auOz~A@A{V*c)o7E(a=wHvb@8g5PnVJ&7D+Fp8ABV z5`&LD-<$jPy{-y*V^SqM)9!#_Pj2-x{m$z+9Z*o|JTBGgXYYVM;g|VbitDUfnVn$o zO)6?CZcDklDoODzj+ti@i#WcqPoZ!|IPB98LW!$-p+a4xBVM@%GEGZKmNjQMhh)zv z7D){Gpe-Dv=~>c9f|1vANF&boD=Nb1Dv>4~eD636Lldh?#zD5{6JlcR_b*C_Enw&~ z5l2(w(`{+01xb1FCRfD2ap$u(h1U1B6e&8tQrnC}Cy0GR=i^Uue26Rc6Dx}!4#K*0 zaxt`a+px7-Z!^(U1WN2#kdN#OeR|2z+C@b@w+L67VEi&ZpAdg+8`HJT=wIMJqibhT ztb3PFzsq&7jzQuod3xp7uL?h-7rYao&0MiT_Bux;U*N#ebGv92o(jM2?`1!N2W_M* zeo9$%hEtIy;=`8z1c|kL&ZPn0y`N)i$Y1R9>K!el{moiy)014448YC#9=K zwO3weN|8!`5bU_#f(+ZrVd*9`7Uw?!q?yo&7sk&DJ;#-^tcCtqt5*A(V;&LdHq7Hg zI6sC@!ly9p$^@v&XDsgIuv;9#w^!C1n5+10-tEw~ZdO1kqMDYyDl!5__o}f3hYe2M zCeO)~m&&=JZn%cVH3HzPlcE`9^@``2u+!Y}Remn)DLMHc-h5A9ATgs;7F7=u2=vBlDRbjeYvyNby=TvpI{5nb2@J_YTEEEj4q<@zaGSC_i&xxD!6)d zG{1??({Ma<=Wd4JL%bnEXoBOU_0bbNy3p%mFrMW>#c zzPEvryBevZVUvT^2P&Zobk#9j>vSIW_t?AHy>(^x-Bx~(mvNYb_%$ZFg(s5~oka+Kp(GU68I$h(Vq|fZ zC_u1FM|S)=ldt#5q>&p4r%%p)*7|Rf0}B#-FwHDTo*|P6HB_rz%R;{==hpl#xTt@VLdSrrf~g^ z`IA8ZV1b`UazYpnkn28h&U)$(gdZ*f{n`&kH%Oy54&Z;ebjlh4x?JmnjFAALu}EG} zfGmQ$5vEMJMH`a=+*src#dWK&N1^LFxK9Sa#q_rja$JWra09we<2oL9Q9Sx)?kZFW z$jhOFGE~VcihYlkaZv8?uA7v$*}?2h6i%Qmgc4n~3E(O_`YCRGy~}`NFaj@(?Wz;GS_?T+RqU{S)eD1j$1Gr;C^m z7zDK=xaJ^6``=#Y-2ssNfdRqh0ntJrutGV5Nv&WI%3k1wmD5n+0aRe{0k^!>LFReN zx1g*E>nbyx03KU~UT6->+rG%(owLF=beJxK&a0F;ie1GZ^eKg-VEZb&=s&ajKS#6w zjvC6J#?b|U_(%@uq$c#Q@V_me0S1%)pKz9--{EKwyM}_gOj*Og-NEWLDF_oFtPjG; zXCZ7%#=s}RKr&_5RFN@=H(015AGl4XRN9Bc51`;WWt%vzQvzexDI2BZ@xP~^2$I&7 zA(ndsgLsmA*su8p-~IS q+ZJUZM}`4#Zi@l2F-#HCw*??ha2ta#9s8?H3%YId(*zJG6aF78h1yF1 delta 5107 zcmY*d1zc0@|J{HQlai7V5+f#EN-H%&UP4MFm6QgFfuJK4DG4u#ARsbQL4i>MB1q|w zmWd#pqd~BR-yN@ieE-|$^W1aKIZtf&-p_fyw{(Uwc7_sWYDh^12cY!qXvcPQ!qF;q@b0nYU7 zP&ht}K7j%}P%%|ffm;4F0^i3P0R`a!2wm89L5P3Kfu;tTZJre<{N5}AzsH+E3DS`Q zJLIl`LRMf`JOTBLf(;IV(9(h{(}dXK!cPoSLm(o@fz8vRz}6fOw%3}3VYOsCczLF` za2RTsCWa2sS-uw(6|HLJg)Xf@S8#|+(Z5Y)ER+v+8;btfB3&9sWH6<=U}0)o-jIts zsi?Nko;No&JyZI%@1G&zsG5kKo^Zd7rk_9VIUao9;fC~nv(T0F&Af0&Rp`?x94EIS zUBPyBe5R5#okNiB1Xe--q4|hPyGzhJ?Lurt#Ci09BQ+}rlHpBhm;EmfLw{EbCz)sg zgseAE#f$met1jo;`Z6ihk?O1be3aa$IGV69{nzagziA!M*~E5lMc(Sp+NGm2IUjmn zql((DU9QP~Tn1pt6L`}|$Na-v(P+Zg&?6bAN@2u%KiB*Gmf}Z)R zMENRJgjKMqVbMpzPO{`!J~2Jyu7&xXnTDW?V?IJgy+-35q1)-J8T**?@_-2H`%X+6f5 zIRv`uLp&*?g7L~6+3O*saXT~gWsmhF*FNKw4X$29ePKi02G*)ysenhHv{u9-y?_do ztT(Cu04pk>51n}zu~=wgToY5Cx|MTlNw}GR>+`|6CAhQn=bh@S<7N)`w};;KTywDU z=QWO@RBj$WKOXSgCWg{BD`xl&DS!G}`Mm3$)=%3jzO_C+s+mfTFH5JL>}*(JKs@MqX|o2b#ZBX5P;p7;c)$F1y4HwvJ?KA938$rd)gn_U^CcUtmdaBW57 zlPph>Fz&L`cSScFjcj+7Jif3vxb20Ag~FPstm?9#OrD$e?Y~#1osDB0CFZ9Mu&%iE zSj~wZpFqu6!k%BT)}$F@Z%(d-Pqy07`N8ch2F7z^=S-!r-@j{#&{SM@a8O$P#SySx zZLD_z=I300OCA1YmKV0^lo@>^)THfZvW}s<$^w^#^Ce=kO5ymAnk>H7pK!+NJ-+F7 z1Bb6Y=r)0nZ+hRXUyD+BKAyecZxb+$JTHK5k(nWv*5%2a+u*GDt|rpReYQ}vft zXrIt#!kGO85o^~|9Oc-M5A!S@9Q)O$$&g8u>1=ew?T35h8B{-Z_S78oe=E(-YZhBPe@Y1sUt63A-Cdv>D1nIT~=Rub6$?8g>meFb7Ic@w^%@RN2z72oPZ#Ta%b(P1|&6I z61iO<8hT*)p19Bgd0JgXP{^c{P2~K@^DIXv=dF(u|DFfqD^dMIl8-x)xKIpJRZru@ zDxicyYJG}mh}=1Dfg%B$#H`CiAxPTj^;f4KRMZHUz-_x6)lEq!^mu%72*PI=t$6{Uql#dqm4 zClgaN63!&?v*enz4k1sbaM+yCqUf+i9rw$(YrY%ir1+%cWRB<;r}$8si!6QcNAk~J zk3?dejBaC`>=T<=y=>QVt*4kL>SwYwn$(4ES793qaH)>n(axyV3R5jdXDh#e-N0K- zuUgk|N^|3*D1!Wlz-!M*b}Zc5=;K6I+>1N$&Q%)&8LWUiTYi&aQIj(luA< zN5R<8Y8L#*i0xBio$jWcaiZ4S2w3#R@CGemesy~akKP)2GojQF6!$}!_RdUJPBevX zG#~uz%Yirb0@1wgQ;ayb=qD}6{=QXxjuZQ@@kxbN!QWhtEvuhS2yAZe8fZy6*4Inr zdSyR9Dec4HrE|I=z-U;IlH;_h#7e^Hq}gaJ<-z^}{*s!m^66wu2=(*EM0UaV*&u1q zJrq!K23TO8a(ecSQFdD$y+`xu)Xk36Z*;1i{hS=H2E<8<5yHuHG~22-S+Jq|3HMAw z%qBz3auT=M!=5F|Wqke|I^E8pmJ-}>_DwX5w%d3MSdC>xW%$ocm8w8HRdZ|^#cEt1 zM*I7S6sLQq;;Mecet(Q()+?s+&MeVLOvx}(MkvytkvLHl7h*N0AT1#AqC&(he(^%przH`KqA$z_dAvJJb409@F)fYwD$JW_{_Oie8!@VdJE zU>D$@B?LawAf5$;`AZ1E!krn=aAC%4+YQrzL!59yl1;|T2)u=RBYA8lk0Ek&gS!Rb zt0&hVuyhSa0}rpZGjTA>Gz}>Uv*4)F zf7S%D2nfA7x?gPEXZWk8DZimQs#xi0?So_k`2zb!UVQEAcbvjPLK9v>J~!awnxGpq zEh$EPOc4q&jywmglnC&D)1-P0DH!@)x;uJwMHdhPh>ZLWDw+p1pf52{X2dk{_|UOmakJa4MHu?CY`6Hhv!!d7=aNwiB5z zb*Wlq1zf^3iDlPf)b_SzI*{JCx2jN;*s~ra8NeB!PghqP!0po-ZL?0Jk;2~*~sCQ<%wU`mRImd)~!23RS?XJu|{u( ztFPy3*F=ZhJmBugTv48WX)4U*pNmm~4oD4}$*-92&<)n=R)5lT z-VpbEDk>(C1hoo#-H_u0`#%L6L$ zln(}h2*Cl(5(JtVM{YZ26@Fwmp;?Qt}9$_F%`?+-JHbC;bPZj8PLq9 zWo-KFw!i&r8WuA-!3F_m9!24Z(RhalAUR~_H#Ln=$%b5GY z)oB)zO%J5TY}&BXq^7#M>euVL%01Tzj4$6^ZOjT*7@zr~q@6GEjGi)nbwzSL`TiLN z{DVG~I$w@%^#tD{>1Ap@%=XogG_^Hvy_xiRn4yy?LKsC+ zU!S79X8orh&D%>1S`x2iyi&(iG&r#YT{}~iy(FIOo8?MZU#eo*c*(RjAGj@uDi zARJur)-*{n0PgW~&mFeg`MJ?(Kr;NUom)jh?ozZtyywN9bea6ikQlh}953Oul~N%4 z@Sx!@>?l1e7V*@HZMJx!gMo0TeXdU~#W6^n?YVQJ$)nuFRkvKbfwv_s*2g(!wPO|@ zvuXF=2MiPIX)A7x!|BthSa$GB%ECnuZe_Scx&AlnC z!~6C_SF24#@^VMIw)a-7{00}}Cr5NImPbW8OTIHoo6@NcxLVTna8<<;uy~YaaeMnd z;k_ynYc_8jQn9vW_W8QLkgaHtmwGC}wRcgZ^I^GPbz{lW)p#YYoinez1MjkY%6LBd z+Vr>j&^!?b-*Vk>8I!28o`r3w&^Lal8@=50zV4&9V9oXI{^r8;JmVeos&wf?O!;_o zk))^k*1fvYw9?WrS!sG2TcX`hH@Y3mF&@{i05;_AV{>Umi8{uZP_0W5_1V2yHU<)E z+qviK*7SJtnL;76{WK!?Pv$-!w$08<%8Qy|sB|P%GiV1<+dHw*sj!C~SjsB6+1L@so+Q~n# z+Uc5+Uz+mGmkR@>H7D*c?mm8WQz;3VOpktU_DeBi>3#@z zmLe;3gP<7KPy>~k47nEeT?G?7e2g6316Xdb_y+ja5C9Ayg6QTNr~&Kbs(1>7zp|f@le;9B z1e(+Ga%jPWR7oc}=XcB4$z?YD)l;%#U;}~gZzGViI=fwu9OAPCCK!0w>Ay^#$b49k zT&|M?JaIyRT<;@*t_jp1ifWPvL;{maf6o0T#X!#9YX;0Q;LTQ0}0tg^_Ru4pkSr4#P zmnW|D0`A#Ie6pEfBDv39=jN2;kiUoT6I&kChsbI!jMuY6zuZql5!&i%5!c zjsHlXtjT;NV?jAb`%vy)JOK_j1rponLqc>(2qgYlLPEs>|0QV<=Pw~C`fLFKJJitt zyC6003{rxCsmtGKjhB%W2W~*%vKH8l$pZoOFT*K@uL9%CD^3rh=ZtuTU1 zJpf4|%n^yjh#dKSSCJI8;YU*CD!8Wv20*e5`-fya^75@ADLU^RdHDg3Bk3k6)dGi7 z!!z;|O1h$8q!vO*w6 I6Xdi10eY*&F8}}l diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 508322917bd..bdc9a83b1e6 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.0.2-bin.zip networkTimeout=10000 zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew index 65dcd68d65c..79a61d421cc 100755 --- a/gradlew +++ b/gradlew @@ -144,7 +144,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then case $MAX_FD in #( max*) # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC3045 + # shellcheck disable=SC3045 MAX_FD=$( ulimit -H -n ) || warn "Could not query maximum file descriptor limit" esac @@ -152,7 +152,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then '' | soft) :;; #( *) # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC3045 + # shellcheck disable=SC3045 ulimit -n "$MAX_FD" || warn "Could not set maximum file descriptor limit to $MAX_FD" esac From 96b31e99e0507ffbb2633a87a8c8660684da672e Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Tue, 7 Mar 2023 17:33:25 +0100 Subject: [PATCH 564/743] feat: HTTP Client Implementation with Java HTTP Client (#8441) --- gradle/libs.versions.toml | 1 + .../client/AbstractHttpClientFactory.java | 94 +++++ .../http/client/HttpClientConfiguration.java | 38 +- .../exceptions/HttpClientExceptionUtils.java | 53 +++ .../HttpClientExceptionUtilsSpec.groovy | 21 ++ http-client-jdk/build.gradle.kts | 25 ++ .../client/jdk/AbstractJdkHttpClient.java | 298 +++++++++++++++ .../http/client/jdk/DefaultJdkHttpClient.java | 166 +++++++++ .../jdk/DefaultJdkHttpClientRegistry.java | 338 ++++++++++++++++++ .../http/client/jdk/HttpHeadersAdapter.java | 75 ++++ .../http/client/jdk/HttpRequestFactory.java | 116 ++++++ .../http/client/jdk/HttpResponseAdapter.java | 113 ++++++ .../client/jdk/JdkBlockingHttpClient.java | 104 ++++++ .../http/client/jdk/JdkClientSslBuilder.java | 120 +++++++ .../http/client/jdk/JdkHttpClient.java | 29 ++ .../http/client/jdk/JdkHttpClientFactory.java | 48 +++ .../jdk/cookie/CompositeCookieDecoder.java | 56 +++ .../http/client/jdk/cookie/CookieDecoder.java | 41 +++ .../jdk/cookie/DefaultCookieDecoder.java | 46 +++ .../client/jdk/cookie/NettyCookieDecoder.java | 89 +++++ ...io.micronaut.http.client.HttpClientFactory | 1 + .../client/jdk/ClientLoggerNameSpec.groovy | 79 ++++ .../http/client/jdk/ClientProxySpec.groovy | 117 ++++++ .../client/jdk/ClientVersioningSpec.groovy | 139 +++++++ .../micronaut/http/client/jdk/H2CSpec.groovy | 28 ++ .../http/client/jdk/Http2Spec.groovy | 47 +++ .../http/client/jdk/HttpProxySpec.groovy | 81 +++++ .../jdk/OptionsRequestAttributesSpec.groovy | 61 ++++ .../jdk/RequestAttributeBindingSpec.groovy | 112 ++++++ .../client/jdk/RequestAttributeSpec.groovy | 94 +++++ .../http/client/jdk/SslSelfSignedSpec.groovy | 68 ++++ .../micronaut/http/client/jdk/SslSpec.groovy | 106 ++++++ .../src/test/resources/logback.xml | 17 + http-client-jdk/src/test/resources/squid.conf | 2 + http-client-tck/build.gradle.kts | 15 + .../http/client/tck/tests/AuthTest.java | 61 ++++ .../http/client/tck/tests/CookieTest.java | 111 ++++++ .../tck/tests/DontFollowRedirectsTest.java | 72 ++++ .../tck/tests/ExceptionOnErrorStatusTest.java | 65 ++++ .../tck/tests/HttpMethodDeleteTest.java | 130 +++++++ .../client/tck/tests/HttpMethodPostTest.java | 67 ++++ .../http/client/tck/tests/Person.java | 60 ++++ .../http/client/tck/tests/RedirectTest.java | 249 +++++++++++++ .../http/client/tck/tests/StatusTest.java | 146 ++++++++ .../http/client/netty/ConnectionManager.java | 20 +- .../http/client/netty/DefaultHttpClient.java | 94 +++-- .../client/netty/NettyClientHttpRequest.java | 2 +- .../http/client/ClientRedirectSpec.groovy | 4 +- .../RequestAttributeBindingSpec.groovy | 3 +- http-server-tck/build.gradle.kts | 1 + .../server/tck/tests/BodyArgumentTest.java | 8 +- .../http/server/tck/tests/BodyTest.java | 9 +- .../http/server/tck/tests/ConsumesTest.java | 8 +- .../http/server/tck/tests/CookiesTest.java | 7 +- .../tck/tests/DeleteWithoutBodyTest.java | 2 +- .../server/tck/tests/ErrorHandlerTest.java | 11 +- .../server/tck/tests/FilterErrorTest.java | 6 +- .../http/server/tck/tests/FiltersTest.java | 9 +- .../http/server/tck/tests/FluxTest.java | 7 +- .../http/server/tck/tests/HelloWorldTest.java | 4 +- .../http/server/tck/tests/MiscTest.java | 7 +- .../http/server/tck/tests/OctetTest.java | 8 +- .../http/server/tck/tests/ParameterTest.java | 7 +- .../server/tck/tests/RemoteAddressTest.java | 6 +- .../server/tck/tests/ResponseStatusTest.java | 6 +- .../http/server/tck/tests/StatusTest.java | 7 +- .../http/server/tck/tests/VersionTest.java | 7 +- .../tests/cors/CorsDisabledByDefaultTest.java | 8 +- .../tck/tests/cors/CorsSimpleRequestTest.java | 19 +- .../SimpleRequestWithCorsNotEnabledTest.java | 6 +- .../tests/filter/ClientRequestFilterTest.java | 6 +- .../filter/ClientResponseFilterTest.java | 6 +- .../HttpServerFilterExceptionHandlerTest.java | 6 +- .../RequestFilterExceptionHandlerTest.java | 6 +- .../tck/tests/filter/RequestFilterTest.java | 6 +- .../ResponseFilterExceptionHandlerTest.java | 6 +- .../tck/tests/filter/ResponseFilterTest.java | 6 +- http-tck/build.gradle.kts | 26 ++ .../micronaut/http}/tck/AssertionUtils.java | 23 +- .../io/micronaut/http}/tck/BodyAssertion.java | 48 ++- .../http}/tck/EmbeddedServerUnderTest.java | 29 +- .../tck/EmbeddedServerUnderTestProvider.java | 3 +- .../http}/tck/HttpResponseAssertion.java | 12 +- .../micronaut/http}/tck/RequestSupplier.java | 2 +- .../micronaut/http}/tck/ServerUnderTest.java | 41 ++- .../http}/tck/ServerUnderTestProvider.java | 2 +- .../tck/ServerUnderTestProviderUtils.java | 2 +- .../io/micronaut/http}/tck/TestScenario.java | 4 +- .../context/ClientContextPathProvider.java | 1 + .../http/context/ContextPathUtils.java | 52 +++ .../context/ServerContextPathProvider.java | 1 + .../http/context/ContextPathUtilsSpec.groovy | 32 ++ settings.gradle | 7 + src/main/docs/guide/httpClient.adoc | 12 - .../httpClient/httpClientImplementations.adoc | 1 + .../jdkHttpClient.adoc | 14 + .../nettyHttpClient.adoc | 3 + .../guide/httpClient/lowLevelOrHighLevel.adoc | 1 + .../docs/guide/introduction/whatsNew.adoc | 2 + src/main/docs/guide/toc.yml | 6 + test-suite-groovy/build.gradle | 1 + .../docs/client/MultiClientSpec.groovy | 55 +++ .../build.gradle.kts | 19 + .../http/client/jdk/SslRefreshSpec.groovy | 103 ++++++ .../http/client/jdk/SslStaticCertSpec.groovy | 80 +++++ .../src/test/resources/certs/client1.p12 | Bin 0 -> 2646 bytes .../src/test/resources/certs/server.p12 | Bin 0 -> 2606 bytes .../src/test/resources/certs/truststore | Bin 0 -> 1074 bytes .../src/test/resources/keystore.p12 | Bin 0 -> 2485 bytes .../src/test/resources/logback.xml | 16 + .../build.gradle.kts | 15 + .../tck/jdk/tests/JdkHttpMethodTests.java | 16 + ...micronaut.http.tck.ServerUnderTestProvider | 1 + .../src/test/resources/logback.xml | 16 + .../build.gradle.kts | 14 + .../tck/netty/tests/NettyHttpMethodTests.java | 12 + ...micronaut.http.tck.ServerUnderTestProvider | 1 + .../src/test/resources/logback.xml | 16 + .../build.gradle.kts | 16 + .../netty/tests/JdkHttpServerTestSuite.java | 15 + ...micronaut.http.tck.ServerUnderTestProvider | 1 + .../src/test/resources/logback.xml | 16 + ...ut.http.server.tck.ServerUnderTestProvider | 1 - ...micronaut.http.tck.ServerUnderTestProvider | 1 + .../src/test/resources/logback.xml | 6 + test-suite-kotlin/build.gradle | 1 + .../micronaut/docs/client/MultiClientSpec.kt | 50 +++ test-suite/build.gradle | 1 + .../docs/client/MultiClientSpec.java | 56 +++ 129 files changed, 4762 insertions(+), 208 deletions(-) create mode 100644 http-client-core/src/main/java/io/micronaut/http/client/AbstractHttpClientFactory.java create mode 100644 http-client-core/src/main/java/io/micronaut/http/client/exceptions/HttpClientExceptionUtils.java create mode 100644 http-client-core/src/test/groovy/io/micronaut/http/client/exceptions/HttpClientExceptionUtilsSpec.groovy create mode 100644 http-client-jdk/build.gradle.kts create mode 100644 http-client-jdk/src/main/java/io/micronaut/http/client/jdk/AbstractJdkHttpClient.java create mode 100644 http-client-jdk/src/main/java/io/micronaut/http/client/jdk/DefaultJdkHttpClient.java create mode 100644 http-client-jdk/src/main/java/io/micronaut/http/client/jdk/DefaultJdkHttpClientRegistry.java create mode 100644 http-client-jdk/src/main/java/io/micronaut/http/client/jdk/HttpHeadersAdapter.java create mode 100644 http-client-jdk/src/main/java/io/micronaut/http/client/jdk/HttpRequestFactory.java create mode 100644 http-client-jdk/src/main/java/io/micronaut/http/client/jdk/HttpResponseAdapter.java create mode 100644 http-client-jdk/src/main/java/io/micronaut/http/client/jdk/JdkBlockingHttpClient.java create mode 100644 http-client-jdk/src/main/java/io/micronaut/http/client/jdk/JdkClientSslBuilder.java create mode 100644 http-client-jdk/src/main/java/io/micronaut/http/client/jdk/JdkHttpClient.java create mode 100644 http-client-jdk/src/main/java/io/micronaut/http/client/jdk/JdkHttpClientFactory.java create mode 100644 http-client-jdk/src/main/java/io/micronaut/http/client/jdk/cookie/CompositeCookieDecoder.java create mode 100644 http-client-jdk/src/main/java/io/micronaut/http/client/jdk/cookie/CookieDecoder.java create mode 100644 http-client-jdk/src/main/java/io/micronaut/http/client/jdk/cookie/DefaultCookieDecoder.java create mode 100644 http-client-jdk/src/main/java/io/micronaut/http/client/jdk/cookie/NettyCookieDecoder.java create mode 100644 http-client-jdk/src/main/resources/META-INF/services/io.micronaut.http.client.HttpClientFactory create mode 100644 http-client-jdk/src/test/groovy/io/micronaut/http/client/jdk/ClientLoggerNameSpec.groovy create mode 100644 http-client-jdk/src/test/groovy/io/micronaut/http/client/jdk/ClientProxySpec.groovy create mode 100644 http-client-jdk/src/test/groovy/io/micronaut/http/client/jdk/ClientVersioningSpec.groovy create mode 100644 http-client-jdk/src/test/groovy/io/micronaut/http/client/jdk/H2CSpec.groovy create mode 100644 http-client-jdk/src/test/groovy/io/micronaut/http/client/jdk/Http2Spec.groovy create mode 100644 http-client-jdk/src/test/groovy/io/micronaut/http/client/jdk/HttpProxySpec.groovy create mode 100644 http-client-jdk/src/test/groovy/io/micronaut/http/client/jdk/OptionsRequestAttributesSpec.groovy create mode 100644 http-client-jdk/src/test/groovy/io/micronaut/http/client/jdk/RequestAttributeBindingSpec.groovy create mode 100644 http-client-jdk/src/test/groovy/io/micronaut/http/client/jdk/RequestAttributeSpec.groovy create mode 100644 http-client-jdk/src/test/groovy/io/micronaut/http/client/jdk/SslSelfSignedSpec.groovy create mode 100644 http-client-jdk/src/test/groovy/io/micronaut/http/client/jdk/SslSpec.groovy create mode 100644 http-client-jdk/src/test/resources/logback.xml create mode 100644 http-client-jdk/src/test/resources/squid.conf create mode 100644 http-client-tck/build.gradle.kts create mode 100644 http-client-tck/src/main/java/io/micronaut/http/client/tck/tests/AuthTest.java create mode 100644 http-client-tck/src/main/java/io/micronaut/http/client/tck/tests/CookieTest.java create mode 100644 http-client-tck/src/main/java/io/micronaut/http/client/tck/tests/DontFollowRedirectsTest.java create mode 100644 http-client-tck/src/main/java/io/micronaut/http/client/tck/tests/ExceptionOnErrorStatusTest.java create mode 100644 http-client-tck/src/main/java/io/micronaut/http/client/tck/tests/HttpMethodDeleteTest.java create mode 100644 http-client-tck/src/main/java/io/micronaut/http/client/tck/tests/HttpMethodPostTest.java create mode 100644 http-client-tck/src/main/java/io/micronaut/http/client/tck/tests/Person.java create mode 100644 http-client-tck/src/main/java/io/micronaut/http/client/tck/tests/RedirectTest.java create mode 100644 http-client-tck/src/main/java/io/micronaut/http/client/tck/tests/StatusTest.java create mode 100644 http-tck/build.gradle.kts rename {http-server-tck/src/main/java/io/micronaut/http/server => http-tck/src/main/java/io/micronaut/http}/tck/AssertionUtils.java (87%) rename {http-server-tck/src/main/java/io/micronaut/http/server => http-tck/src/main/java/io/micronaut/http}/tck/BodyAssertion.java (70%) rename {http-server-tck/src/main/java/io/micronaut/http/server => http-tck/src/main/java/io/micronaut/http}/tck/EmbeddedServerUnderTest.java (71%) rename {http-server-tck/src/main/java/io/micronaut/http/server => http-tck/src/main/java/io/micronaut/http}/tck/EmbeddedServerUnderTestProvider.java (96%) rename {http-server-tck/src/main/java/io/micronaut/http/server => http-tck/src/main/java/io/micronaut/http}/tck/HttpResponseAssertion.java (93%) rename {http-server-tck/src/main/java/io/micronaut/http/server => http-tck/src/main/java/io/micronaut/http}/tck/RequestSupplier.java (96%) rename {http-server-tck/src/main/java/io/micronaut/http/server => http-tck/src/main/java/io/micronaut/http}/tck/ServerUnderTest.java (64%) rename {http-server-tck/src/main/java/io/micronaut/http/server => http-tck/src/main/java/io/micronaut/http}/tck/ServerUnderTestProvider.java (98%) rename {http-server-tck/src/main/java/io/micronaut/http/server => http-tck/src/main/java/io/micronaut/http}/tck/ServerUnderTestProviderUtils.java (97%) rename {http-server-tck/src/main/java/io/micronaut/http/server => http-tck/src/main/java/io/micronaut/http}/tck/TestScenario.java (99%) create mode 100644 http/src/main/java/io/micronaut/http/context/ContextPathUtils.java create mode 100644 http/src/test/groovy/io/micronaut/http/context/ContextPathUtilsSpec.groovy create mode 100644 src/main/docs/guide/httpClient/httpClientImplementations.adoc create mode 100644 src/main/docs/guide/httpClient/httpClientImplementations/jdkHttpClient.adoc create mode 100644 src/main/docs/guide/httpClient/httpClientImplementations/nettyHttpClient.adoc create mode 100644 src/main/docs/guide/httpClient/lowLevelOrHighLevel.adoc create mode 100644 test-suite-groovy/src/test/groovy/io/micronaut/docs/client/MultiClientSpec.groovy create mode 100644 test-suite-http-client-jdk-ssl/build.gradle.kts create mode 100644 test-suite-http-client-jdk-ssl/src/test/groovy/io/micronaut/http/client/jdk/SslRefreshSpec.groovy create mode 100644 test-suite-http-client-jdk-ssl/src/test/groovy/io/micronaut/http/client/jdk/SslStaticCertSpec.groovy create mode 100644 test-suite-http-client-jdk-ssl/src/test/resources/certs/client1.p12 create mode 100644 test-suite-http-client-jdk-ssl/src/test/resources/certs/server.p12 create mode 100644 test-suite-http-client-jdk-ssl/src/test/resources/certs/truststore create mode 100644 test-suite-http-client-jdk-ssl/src/test/resources/keystore.p12 create mode 100644 test-suite-http-client-jdk-ssl/src/test/resources/logback.xml create mode 100644 test-suite-http-client-tck-jdk/build.gradle.kts create mode 100644 test-suite-http-client-tck-jdk/src/test/java/io/micronaut/http/client/tck/jdk/tests/JdkHttpMethodTests.java create mode 100644 test-suite-http-client-tck-jdk/src/test/resources/META-INF/services/io.micronaut.http.tck.ServerUnderTestProvider create mode 100644 test-suite-http-client-tck-jdk/src/test/resources/logback.xml create mode 100644 test-suite-http-client-tck-netty/build.gradle.kts create mode 100644 test-suite-http-client-tck-netty/src/test/java/io/micronaut/http/client/tck/netty/tests/NettyHttpMethodTests.java create mode 100644 test-suite-http-client-tck-netty/src/test/resources/META-INF/services/io.micronaut.http.tck.ServerUnderTestProvider create mode 100644 test-suite-http-client-tck-netty/src/test/resources/logback.xml create mode 100644 test-suite-http-server-tck-jdk/build.gradle.kts create mode 100644 test-suite-http-server-tck-jdk/src/test/java/io/micronaut/http/server/tck/netty/tests/JdkHttpServerTestSuite.java create mode 100644 test-suite-http-server-tck-jdk/src/test/resources/META-INF/services/io.micronaut.http.tck.ServerUnderTestProvider create mode 100644 test-suite-http-server-tck-jdk/src/test/resources/logback.xml delete mode 100644 test-suite-http-server-tck-netty/src/test/resources/META-INF/services/io.micronaut.http.server.tck.ServerUnderTestProvider create mode 100644 test-suite-http-server-tck-netty/src/test/resources/META-INF/services/io.micronaut.http.tck.ServerUnderTestProvider create mode 100644 test-suite-kotlin/src/test/kotlin/io/micronaut/docs/client/MultiClientSpec.kt create mode 100644 test-suite/src/test/java/io/micronaut/docs/client/MultiClientSpec.java diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 811f5db9f82..83b5c465b5a 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -181,6 +181,7 @@ jmh-generator-annprocess = { module = "org.openjdk.jmh:jmh-generator-annprocess" jsr107 = { module = "org.jsr107.ri:cache-ri-impl", version.ref = "jsr107" } jsr305 = { module = "com.google.code.findbugs:jsr305", version.ref = "jsr305" } +junit-jupiter = { module = "org.junit.jupiter:junit-jupiter", version.ref = "junit5" } junit-jupiter-api = { module = "org.junit.jupiter:junit-jupiter-api", version.ref = "junit5" } junit-jupiter-engine = { module = "org.junit.jupiter:junit-jupiter-engine", version.ref = "junit5" } junit-jupiter-params = { module = "org.junit.jupiter:junit-jupiter-params", version.ref = "junit5" } diff --git a/http-client-core/src/main/java/io/micronaut/http/client/AbstractHttpClientFactory.java b/http-client-core/src/main/java/io/micronaut/http/client/AbstractHttpClientFactory.java new file mode 100644 index 00000000000..107d109d584 --- /dev/null +++ b/http-client-core/src/main/java/io/micronaut/http/client/AbstractHttpClientFactory.java @@ -0,0 +1,94 @@ +/* + * Copyright 2017-2023 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.http.client; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.NonNull; +import io.micronaut.core.annotation.Nullable; +import io.micronaut.core.convert.ConversionService; +import io.micronaut.http.codec.MediaTypeCodecRegistry; + +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; + +/** + * Abstract class implementation of {@link HttpClientFactory}. + * + * @param The type of {@link HttpClient} created by this factory + * @author Sergio del Amo + * @since 4.0.0 + */ +@Internal +public abstract class AbstractHttpClientFactory implements HttpClientFactory { + + protected final MediaTypeCodecRegistry mediaTypeCodecRegistry; + protected final ConversionService conversionService; + + protected AbstractHttpClientFactory( + @Nullable MediaTypeCodecRegistry mediaTypeCodecRegistry, + ConversionService conversionService + ) { + this.mediaTypeCodecRegistry = mediaTypeCodecRegistry; + this.conversionService = conversionService; + } + + /** + * Creates a new {@link HttpClient} instance for a given URI. + * @param uri The URI + * @return The client + */ + @NonNull + protected abstract T createHttpClient(@Nullable URI uri); + + /** + * Creates a new {@link HttpClient} instance for a given URI and configuration. + * @param uri The URI + * @param configuration The configuration + * @return The client + */ + @NonNull + protected abstract T createHttpClient(@Nullable URI uri, @NonNull HttpClientConfiguration configuration); + + @Override + @NonNull + public HttpClient createClient(URL url) { + return createHttpClient(url); + } + + @Override + @NonNull + public HttpClient createClient(URL url, @NonNull HttpClientConfiguration configuration) { + return createHttpClient(url, configuration); + } + + private T createHttpClient(URL url) { + try { + return createHttpClient(url != null ? url.toURI() : null); + } catch (URISyntaxException e) { + throw new IllegalArgumentException(e); + } + } + + @NonNull + private T createHttpClient(@Nullable URL url, @NonNull HttpClientConfiguration configuration) { + try { + return createHttpClient(url != null ? url.toURI() : null, configuration); + } catch (URISyntaxException e) { + throw new IllegalArgumentException(e); + } + } +} diff --git a/http-client-core/src/main/java/io/micronaut/http/client/HttpClientConfiguration.java b/http-client-core/src/main/java/io/micronaut/http/client/HttpClientConfiguration.java index 29a284cc561..59c5315c670 100644 --- a/http-client-core/src/main/java/io/micronaut/http/client/HttpClientConfiguration.java +++ b/http-client-core/src/main/java/io/micronaut/http/client/HttpClientConfiguration.java @@ -237,6 +237,8 @@ public void setHttpVersion(HttpVersion httpVersion) { } /** + * [available in the Netty HTTP client]. + * * @return The trace logging level */ public Optional getLogLevel() { @@ -253,6 +255,8 @@ public void setLogLevel(@Nullable LogLevel logLevel) { } /** + * [available in the Netty HTTP client]. + * * @return The event loop group to use. */ public String getEventLoopGroup() { @@ -339,6 +343,8 @@ public void setFollowRedirects(boolean followRedirects) { } /** + * [available in the Netty HTTP client]. + * * @return The default charset to use */ public Charset getDefaultCharset() { @@ -355,6 +361,8 @@ public void setDefaultCharset(Charset defaultCharset) { } /** + * [available in the Netty HTTP client]. + * * @return The Client channel options. */ public Map getChannelOptions() { @@ -378,6 +386,7 @@ public Optional getReadTimeout() { /** * For streaming requests and WebSockets, the {@link #getReadTimeout()} method does not apply instead a configurable * idle timeout is applied. + * [available in the Netty HTTP client] * * @return The default amount of time to allow read operation connections to remain idle */ @@ -386,6 +395,8 @@ public Optional getReadIdleTimeout() { } /** + * [available in the Netty HTTP client]. + * * @return The idle timeout for connection in the client connection pool. Defaults to 0. */ public Optional getConnectionPoolIdleTimeout() { @@ -400,6 +411,8 @@ public Optional getConnectTimeout() { } /** + * [available in the Netty HTTP client]. + * * @return The connectTtl. */ public Optional getConnectTtl() { @@ -408,6 +421,7 @@ public Optional getConnectTtl() { /** * The amount of quiet period for shutdown. + * [available in the Netty HTTP client] * * @return The shutdown timeout */ @@ -417,6 +431,7 @@ public Optional getShutdownQuietPeriod() { /** * The amount of time to wait for shutdown. + * [available in the Netty HTTP client] * * @return The shutdown timeout */ @@ -490,6 +505,8 @@ public void setConnectTtl(@Nullable Duration connectTtl) { } /** + * [available in the Netty HTTP client]. + * * @return The number of threads the client should use for requests */ public OptionalInt getNumOfThreads() { @@ -506,6 +523,8 @@ public void setNumOfThreads(@Nullable Integer numOfThreads) { } /** + * [available in the Netty HTTP client]. + * * @return An {@link Optional} {@code ThreadFactory} */ public Optional> getThreadFactory() { @@ -522,6 +541,8 @@ public void setThreadFactory(Class threadFactory) { } /** + * [available in the Netty HTTP client]. + * * @return The maximum content length the client can consume */ public int getMaxContentLength() { @@ -662,6 +683,8 @@ public Proxy resolveProxy(boolean isSsl, String host, int port) { *
* Note: If {@link #httpVersion} is set, this setting is ignored! * + * [available in the Netty HTTP client]. + * * @return The plaintext connection mode. * @since 4.0.0 */ @@ -687,6 +710,7 @@ public void setPlaintextMode(@NonNull HttpVersionSelection.PlaintextMode plainte * TLS cipher suites to those supported by the HTTP 2 standard. *
* Note: If {@link #httpVersion} is set, this setting is ignored! + * [available in the Netty HTTP client]. * * @return The supported ALPN protocols. * @since 4.0.0 @@ -738,7 +762,7 @@ public static class ConnectionPoolConfiguration implements Toggleable { /** * Whether connection pooling is enabled. - * + * [available in the Netty HTTP client] * @return True if connection pooling is enabled */ @Override @@ -757,7 +781,7 @@ public void setEnabled(boolean enabled) { /** * Maximum number of futures awaiting connection acquisition. Defaults to no maximum. - * + * [available in the Netty HTTP client] * @return The max pending requires */ public int getMaxPendingAcquires() { @@ -775,7 +799,7 @@ public void setMaxPendingAcquires(int maxPendingAcquires) { /** * The time to wait to acquire a connection. - * + * [available in the Netty HTTP client] * @return The timeout as a duration. */ public Optional getAcquireTimeout() { @@ -794,7 +818,7 @@ public void setAcquireTimeout(@Nullable Duration acquireTimeout) { /** * The maximum number of pending (new) connections before they are assigned to a * pool. - * + * [available in the Netty HTTP client] * @return The maximum number of pending connections * @since 4.0.0 */ @@ -816,7 +840,7 @@ public void setMaxPendingConnections(int maxPendingConnections) { /** * The maximum number of requests (streams) that can run concurrently on one HTTP2 * connection. - * + * [available in the Netty HTTP client] * @return The maximum concurrent request count * @since 4.0.0 */ @@ -837,7 +861,7 @@ public void setMaxConcurrentRequestsPerHttp2Connection(int maxConcurrentRequests /** * The maximum number of concurrent HTTP1 connections in the pool. - * + * [available in the Netty HTTP client] * @return The maximum concurrent connection count * @since 4.0.0 */ @@ -857,7 +881,7 @@ public void setMaxConcurrentHttp1Connections(int maxConcurrentHttp1Connections) /** * The maximum number of concurrent HTTP2 connections in the pool. - * + * [available in the Netty HTTP client] * @return The maximum concurrent connection count * @since 4.0.0 */ diff --git a/http-client-core/src/main/java/io/micronaut/http/client/exceptions/HttpClientExceptionUtils.java b/http-client-core/src/main/java/io/micronaut/http/client/exceptions/HttpClientExceptionUtils.java new file mode 100644 index 00000000000..4a1639c94f6 --- /dev/null +++ b/http-client-core/src/main/java/io/micronaut/http/client/exceptions/HttpClientExceptionUtils.java @@ -0,0 +1,53 @@ +/* + * Copyright 2017-2023 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.http.client.exceptions; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.Nullable; +import io.micronaut.http.client.HttpClientConfiguration; +import io.micronaut.http.client.ServiceHttpClientConfiguration; + +/** + * Utility Class to work with {@link HttpClientException}. + * @author Sergio del Amo + * @since 4.0.0 + */ +@Internal +public final class HttpClientExceptionUtils { + + private HttpClientExceptionUtils() { + + } + + /** + * Sets {@link HttpClientException#setServiceId(String)} for a {@link HttpClientException}. + * @param exc HTTP Client Exception + * @param clientId Client Identifier + * @param configuration HttpClientConfiguration + * @return an HTTP Client Exception + * @param HTTP Client Exception + */ + public static E populateServiceId(E exc, + @Nullable String clientId, + @Nullable HttpClientConfiguration configuration) { + if (clientId != null) { + exc.setServiceId(clientId); + } else if (configuration instanceof ServiceHttpClientConfiguration clientConfiguration) { + exc.setServiceId(clientConfiguration.getServiceId()); + } + return exc; + } +} diff --git a/http-client-core/src/test/groovy/io/micronaut/http/client/exceptions/HttpClientExceptionUtilsSpec.groovy b/http-client-core/src/test/groovy/io/micronaut/http/client/exceptions/HttpClientExceptionUtilsSpec.groovy new file mode 100644 index 00000000000..f9d888e3ecb --- /dev/null +++ b/http-client-core/src/test/groovy/io/micronaut/http/client/exceptions/HttpClientExceptionUtilsSpec.groovy @@ -0,0 +1,21 @@ +package io.micronaut.http.client.exceptions + +import io.micronaut.http.client.ServiceHttpClientConfiguration +import spock.lang.Specification + +class HttpClientExceptionUtilsSpec extends Specification { + + void "verify serviceId gets populated"() { + given: + def config = Stub(ServiceHttpClientConfiguration) { + getServiceId() >> 'bar' + } + expect: + "bar" == HttpClientExceptionUtils.populateServiceId(new HttpClientException("foo"), "bar", null).serviceId + "bar" == HttpClientExceptionUtils.populateServiceId(new HttpClientException("foo"), null, config).serviceId + null == HttpClientExceptionUtils.populateServiceId(new HttpClientException("foo"), null, null).serviceId + + and: 'client id takes precedence' + "fii" == HttpClientExceptionUtils.populateServiceId(new HttpClientException("foo"), "fii", config).serviceId + } +} diff --git a/http-client-jdk/build.gradle.kts b/http-client-jdk/build.gradle.kts new file mode 100644 index 00000000000..23543389173 --- /dev/null +++ b/http-client-jdk/build.gradle.kts @@ -0,0 +1,25 @@ +plugins { + id("io.micronaut.build.internal.convention-library") +} + +micronautBuild { + core { + usesMicronautTestSpock() + } +} + +dependencies { + annotationProcessor(projects.injectJava) + api(projects.httpClientCore) + compileOnly(projects.httpClient) + implementation(libs.managed.reactor) + testImplementation(projects.jacksonDatabind) + testImplementation(projects.httpServerNetty) + testImplementation(libs.bcpkix) + testImplementation(libs.testcontainers.spock) +} + +tasks.named("test") { + useJUnitPlatform() + // systemProperty("jdk.httpclient.HttpClient.log", "all") // Uncomment to enable logging +} diff --git a/http-client-jdk/src/main/java/io/micronaut/http/client/jdk/AbstractJdkHttpClient.java b/http-client-jdk/src/main/java/io/micronaut/http/client/jdk/AbstractJdkHttpClient.java new file mode 100644 index 00000000000..8a61bb3dd1c --- /dev/null +++ b/http-client-jdk/src/main/java/io/micronaut/http/client/jdk/AbstractJdkHttpClient.java @@ -0,0 +1,298 @@ +/* + * Copyright 2017-2023 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.http.client.jdk; + +import io.micronaut.context.exceptions.ConfigurationException; +import io.micronaut.core.annotation.Experimental; +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.NonNull; +import io.micronaut.core.annotation.Nullable; +import io.micronaut.core.convert.ConversionService; +import io.micronaut.core.type.Argument; +import io.micronaut.core.util.StringUtils; +import io.micronaut.http.HttpResponse; +import io.micronaut.http.MutableHttpRequest; +import io.micronaut.http.bind.RequestBinderRegistry; +import io.micronaut.http.client.HttpClientConfiguration; +import io.micronaut.http.client.HttpVersionSelection; +import io.micronaut.http.client.LoadBalancer; +import io.micronaut.http.client.exceptions.HttpClientException; +import io.micronaut.http.client.exceptions.NoHostException; +import io.micronaut.http.client.jdk.cookie.CookieDecoder; +import io.micronaut.http.codec.MediaTypeCodecRegistry; +import io.micronaut.http.context.ContextPathUtils; +import io.micronaut.http.cookie.Cookie; +import io.micronaut.http.ssl.ClientAuthentication; +import io.micronaut.http.ssl.ClientSslConfiguration; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import reactor.core.publisher.Mono; + +import javax.net.ssl.SSLParameters; +import java.net.Authenticator; +import java.net.CookieManager; +import java.net.HttpCookie; +import java.net.InetSocketAddress; +import java.net.PasswordAuthentication; +import java.net.ProxySelector; +import java.net.SocketAddress; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.util.Optional; + +import static io.micronaut.http.client.exceptions.HttpClientExceptionUtils.populateServiceId; + +/** + * Abstract implementation of {@link DefaultJdkHttpClient} that provides common functionality. + * + * @author Sergio del Amo + * @author Tim Yates + * @since 4.0.0 + */ +@Internal +@Experimental +abstract class AbstractJdkHttpClient { + + public static final String H2C_ERROR_MESSAGE = "H2C is not supported by the JDK HTTP client"; + + protected final LoadBalancer loadBalancer; + protected final HttpVersionSelection httpVersion; + protected final HttpClientConfiguration configuration; + protected final String contextPath; + protected final HttpClient client; + protected final CookieManager cookieManager; + protected final RequestBinderRegistry requestBinderRegistry; + protected final String clientId; + protected final ConversionService conversionService; + protected final JdkClientSslBuilder sslBuilder; + protected final Logger log; + protected final CookieDecoder cookieDecoder; + protected MediaTypeCodecRegistry mediaTypeCodecRegistry; + + /** + * @param log the logger to use + * @param loadBalancer The {@link LoadBalancer} to use for selecting servers + * @param httpVersion The {@link HttpVersionSelection} to prefer + * @param configuration The {@link HttpClientConfiguration} to use + * @param contextPath The base URI to prepend to request uris + * @param mediaTypeCodecRegistry The {@link MediaTypeCodecRegistry} to use for encoding and decoding objects + * @param requestBinderRegistry The request binder registry + * @param clientId The client id + * @param conversionService The {@link ConversionService} + * @param sslBuilder The {@link JdkClientSslBuilder} for creating an {@link javax.net.ssl.SSLContext} + */ + protected AbstractJdkHttpClient( + Logger log, + LoadBalancer loadBalancer, + HttpVersionSelection httpVersion, + HttpClientConfiguration configuration, + String contextPath, + MediaTypeCodecRegistry mediaTypeCodecRegistry, + RequestBinderRegistry requestBinderRegistry, + String clientId, + ConversionService conversionService, + JdkClientSslBuilder sslBuilder, + CookieDecoder cookieDecoder + ) { + this.cookieDecoder = cookieDecoder; + this.log = configuration.getLoggerName().map(LoggerFactory::getLogger).orElse(log); + this.loadBalancer = loadBalancer; + this.httpVersion = httpVersion; + this.configuration = configuration; + this.mediaTypeCodecRegistry = mediaTypeCodecRegistry; + this.requestBinderRegistry = requestBinderRegistry; + this.clientId = clientId; + this.conversionService = conversionService; + this.cookieManager = new CookieManager(); + this.sslBuilder = sslBuilder; + + if (System.getProperty("jdk.internal.httpclient.disableHostnameVerification") != null && log.isWarnEnabled()) { + log.warn("The jdk.internal.httpclient.disableHostnameVerification system property is set. This is not recommended for production use as it prevents proper certificate validation and may allow man-in-the-middle attacks."); + } + + if (StringUtils.isNotEmpty(contextPath)) { + if (contextPath.charAt(0) != '/') { + contextPath = '/' + contextPath; + } + this.contextPath = contextPath; + } else { + this.contextPath = null; + } + + HttpClient.Builder builder = HttpClient.newBuilder(); + configuration.getConnectTimeout().ifPresent(builder::connectTimeout); + + HttpVersionSelection httpVersionSelection = HttpVersionSelection.forClientConfiguration(configuration); + + if (httpVersionSelection.getPlaintextMode() == HttpVersionSelection.PlaintextMode.H2C) { + throw new ConfigurationException(H2C_ERROR_MESSAGE); + } + + if (httpVersionSelection.isAlpn() && httpVersionSelection.isHttp2CipherSuites()) { + builder.version(HttpClient.Version.HTTP_2); + } else { + builder.version(HttpClient.Version.HTTP_1_1); + } + + builder + .followRedirects(configuration.isFollowRedirects() ? HttpClient.Redirect.NORMAL : HttpClient.Redirect.NEVER) + .cookieHandler(cookieManager); + + Optional proxyAddress = configuration.getProxyAddress(); + if (proxyAddress.isPresent()) { + SocketAddress socketAddress = proxyAddress.get(); + builder = configureProxy(builder, socketAddress, configuration.getProxyUsername().orElse(null), configuration.getProxyPassword().orElse(null)); + } + + if (configuration.getSslConfiguration() instanceof ClientSslConfiguration clientSslConfiguration) { + configureSsl(builder, clientSslConfiguration); + } + + this.client = builder.build(); + } + + private static HttpCookie toJdkCookie(@NonNull Cookie cookie, + @NonNull io.micronaut.http.HttpRequest request, + @NonNull String host) { + HttpCookie newCookie = new HttpCookie(cookie.getName(), cookie.getValue()); + newCookie.setMaxAge(cookie.getMaxAge()); + newCookie.setDomain(host); + newCookie.setHttpOnly(cookie.isHttpOnly()); + newCookie.setSecure(cookie.isSecure()); + newCookie.setPath(cookie.getPath() == null ? request.getPath() : cookie.getPath()); + return newCookie; + } + + private HttpClient.Builder configureProxy( + @NonNull HttpClient.Builder builder, + @NonNull SocketAddress address, + @Nullable String username, + @Nullable String password + ) { + if (log.isDebugEnabled()) { + log.debug("Configuring proxy: {} with username: {}", address, username); + } + if (address instanceof InetSocketAddress inetSocketAddress) { + builder = builder.proxy(ProxySelector.of(inetSocketAddress)); + if (username != null && password != null) { + builder = builder.authenticator(new Authenticator() { + @Override + protected PasswordAuthentication getPasswordAuthentication() { + return new PasswordAuthentication(username, password.toCharArray()); + } + }); + } + } else { + throw new IllegalArgumentException("Unsupported proxy address type: " + address.getClass().getName()); + } + return builder; + } + + private void configureSsl(HttpClient.Builder builder, ClientSslConfiguration clientSslConfiguration) { + sslBuilder.build(clientSslConfiguration).ifPresent(builder::sslContext); + SSLParameters sslParameters = new SSLParameters(); + clientSslConfiguration.getClientAuthentication().ifPresent(a -> { + if (a == ClientAuthentication.WANT) { + sslParameters.setWantClientAuth(true); + } else if (a == ClientAuthentication.NEED) { + sslParameters.setNeedClientAuth(true); + } + }); + clientSslConfiguration.getProtocols().ifPresent(sslParameters::setProtocols); + clientSslConfiguration.getCiphers().ifPresent(sslParameters::setCipherSuites); + builder.sslParameters(sslParameters); + } + + /** + * @return The {@link MediaTypeCodecRegistry} + */ + public MediaTypeCodecRegistry getMediaTypeCodecRegistry() { + return mediaTypeCodecRegistry; + } + + /** + * @param mediaTypeCodecRegistry The {@link MediaTypeCodecRegistry} + */ + public void setMediaTypeCodecRegistry(MediaTypeCodecRegistry mediaTypeCodecRegistry) { + this.mediaTypeCodecRegistry = mediaTypeCodecRegistry; + } + + /** + * Convert the Micronaut request to a JDK request. + * + * @param request The Micronaut request object + * @param bodyType The body type + * @param The body type + * @return A JDK request object + */ + protected Mono mapToHttpRequest(io.micronaut.http.HttpRequest request, Argument bodyType) { + return resolveRequestUri(request) + .map(uri -> { + cookieDecoder.decode(request).ifPresent(cookies -> cookies.getAll().forEach(cookie -> { + HttpCookie newCookie = toJdkCookie(cookie, request, uri.getHost()); + cookieManager.getCookieStore().add(uri, newCookie); + })); + + return HttpRequestFactory.builder(uri, request, configuration, bodyType, mediaTypeCodecRegistry).build(); + }); + } + + private Mono resolveRequestUri(io.micronaut.http.HttpRequest request) { + if (request.getUri().getScheme() != null) { + // Full request URI, so use that + return Mono.just(request.getUri()); + } + + // Otherwise, go and look it up via the LoadBalancer + return resolveURI(request); + } + + private Mono resolveURI(io.micronaut.http.HttpRequest request) { + URI requestURI = request.getUri(); + if (loadBalancer == null) { + return Mono.error(populateServiceId(new NoHostException("Request URI specifies no host to connect to"), clientId, configuration)); + } + + return Mono.from(loadBalancer.select(request)).map(server -> { + Optional authInfo = server.getMetadata().get(io.micronaut.http.HttpHeaders.AUTHORIZATION_INFO, String.class); + if (request instanceof MutableHttpRequest mutableRequest && authInfo.isPresent()) { + mutableRequest.getHeaders().auth(authInfo.get()); + } + + try { + return server.resolve(ContextPathUtils.prepend(requestURI, contextPath)); + } catch (URISyntaxException e) { + throw populateServiceId(new HttpClientException("Failed to construct the request URI", e), clientId, configuration); + } + } + ); + } + + /** + * Convert the JDK response to a Micronaut response. + * + * @param netResponse The JDK response + * @param bodyType The body type + * @param The body type + * @return A Micronaut response + */ + @NonNull + protected HttpResponse response(@NonNull java.net.http.HttpResponse netResponse, @NonNull Argument bodyType) { + return new HttpResponseAdapter<>(netResponse, bodyType, conversionService, mediaTypeCodecRegistry); + } +} diff --git a/http-client-jdk/src/main/java/io/micronaut/http/client/jdk/DefaultJdkHttpClient.java b/http-client-jdk/src/main/java/io/micronaut/http/client/jdk/DefaultJdkHttpClient.java new file mode 100644 index 00000000000..f37ae65bfe0 --- /dev/null +++ b/http-client-jdk/src/main/java/io/micronaut/http/client/jdk/DefaultJdkHttpClient.java @@ -0,0 +1,166 @@ +/* + * Copyright 2017-2023 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.http.client.jdk; + +import io.micronaut.core.annotation.Experimental; +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.NonNull; +import io.micronaut.core.annotation.Nullable; +import io.micronaut.core.convert.ConversionService; +import io.micronaut.core.io.ResourceResolver; +import io.micronaut.core.type.Argument; +import io.micronaut.http.HttpRequest; +import io.micronaut.http.HttpResponse; +import io.micronaut.http.HttpStatus; +import io.micronaut.http.bind.DefaultRequestBinderRegistry; +import io.micronaut.http.bind.RequestBinderRegistry; +import io.micronaut.http.client.BlockingHttpClient; +import io.micronaut.http.client.DefaultHttpClientConfiguration; +import io.micronaut.http.client.HttpClient; +import io.micronaut.http.client.HttpClientConfiguration; +import io.micronaut.http.client.HttpVersionSelection; +import io.micronaut.http.client.LoadBalancer; +import io.micronaut.http.client.exceptions.HttpClientExceptionUtils; +import io.micronaut.http.client.exceptions.HttpClientResponseException; +import io.micronaut.http.client.jdk.cookie.CompositeCookieDecoder; +import io.micronaut.http.client.jdk.cookie.CookieDecoder; +import io.micronaut.http.client.jdk.cookie.DefaultCookieDecoder; +import io.micronaut.http.codec.MediaTypeCodecRegistry; +import io.micronaut.json.JsonMapper; +import io.micronaut.json.codec.JsonMediaTypeCodec; +import io.micronaut.json.codec.JsonStreamMediaTypeCodec; +import io.micronaut.runtime.ApplicationConfiguration; +import org.reactivestreams.Publisher; +import org.slf4j.LoggerFactory; +import reactor.core.publisher.Mono; + +import java.net.URI; +import java.util.List; + +/** + * {@link HttpClient} implementation for {@literal java.net.http.*} HTTP Client. + * @author Sergio del Amo + * @since 4.0.0 + */ +@Internal +@Experimental +public class DefaultJdkHttpClient extends AbstractJdkHttpClient implements JdkHttpClient { + + public DefaultJdkHttpClient( + @Nullable LoadBalancer loadBalancer, + HttpVersionSelection httpVersion, + @NonNull HttpClientConfiguration configuration, + @Nullable String contextPath, + MediaTypeCodecRegistry mediaTypeCodecRegistry, + RequestBinderRegistry requestBinderRegistry, + String clientId, + ConversionService conversionService, + JdkClientSslBuilder sslBuilder, + CookieDecoder cookieDecoder + ) { + super( + configuration.getLoggerName().map(LoggerFactory::getLogger).orElseGet(() -> LoggerFactory.getLogger(DefaultJdkHttpClient.class)), + loadBalancer, + httpVersion, + configuration, + contextPath, + mediaTypeCodecRegistry, + requestBinderRegistry, + clientId, + conversionService, + sslBuilder, + cookieDecoder + ); + } + + public DefaultJdkHttpClient(URI uri, ConversionService conversionService) { + this( + uri == null ? null : LoadBalancer.fixed(uri), + null, + new DefaultHttpClientConfiguration(), + null, + createDefaultMediaTypeRegistry(), + new DefaultRequestBinderRegistry(conversionService), + null, + conversionService, + new JdkClientSslBuilder(new ResourceResolver()), + new CompositeCookieDecoder(List.of(new DefaultCookieDecoder())) + ); + } + + public DefaultJdkHttpClient( + URI uri, + HttpClientConfiguration configuration, + MediaTypeCodecRegistry mediaTypeCodecRegistry, + ConversionService conversionService + ) { + this( + uri == null ? null : LoadBalancer.fixed(uri), + null, + configuration, + null, + mediaTypeCodecRegistry, + new DefaultRequestBinderRegistry(conversionService), + null, + conversionService, + new JdkClientSslBuilder(new ResourceResolver()), + new CompositeCookieDecoder(List.of(new DefaultCookieDecoder())) + ); + } + + private static MediaTypeCodecRegistry createDefaultMediaTypeRegistry() { + JsonMapper mapper = JsonMapper.createDefault(); + ApplicationConfiguration configuration = new ApplicationConfiguration(); + return MediaTypeCodecRegistry.of( + new JsonMediaTypeCodec(mapper, configuration, null), + new JsonStreamMediaTypeCodec(mapper, configuration, null) + ); + } + + @Override + public BlockingHttpClient toBlocking() { + return new JdkBlockingHttpClient(loadBalancer, httpVersion, configuration, contextPath, mediaTypeCodecRegistry, requestBinderRegistry, clientId, conversionService, sslBuilder, cookieDecoder); + } + + @Override + public Publisher> exchange(@NonNull HttpRequest request, @NonNull Argument bodyType, @NonNull Argument errorType) { + return mapToHttpRequest(request, bodyType) + .map(httpRequest -> { + if (log.isDebugEnabled()) { + log.debug("Client {} Sending HTTP Request: {}", clientId, httpRequest); + } + if (log.isTraceEnabled()) { + httpRequest.headers().map().forEach((k, v) -> log.trace("Client {} Sending HTTP Request Header: {}={}", clientId, k, v)); + } + return client.sendAsync(httpRequest, java.net.http.HttpResponse.BodyHandlers.ofByteArray()); + }) + .flatMap(Mono::fromCompletionStage) + .map(netResponse -> { + log.error("Client {} Received HTTP Response: {} {}", clientId, netResponse.statusCode(), netResponse.uri()); + boolean errorStatus = netResponse.statusCode() >= 400; + if (errorStatus && configuration.isExceptionOnErrorStatus()) { + throw HttpClientExceptionUtils.populateServiceId(new HttpClientResponseException(HttpStatus.valueOf(netResponse.statusCode()).getReason(), + response(netResponse, bodyType)), clientId, configuration); + } + return response(netResponse, bodyType); + }); + } + + @Override + public boolean isRunning() { + return false; + } +} diff --git a/http-client-jdk/src/main/java/io/micronaut/http/client/jdk/DefaultJdkHttpClientRegistry.java b/http-client-jdk/src/main/java/io/micronaut/http/client/jdk/DefaultJdkHttpClientRegistry.java new file mode 100644 index 00000000000..98922d43793 --- /dev/null +++ b/http-client-jdk/src/main/java/io/micronaut/http/client/jdk/DefaultJdkHttpClientRegistry.java @@ -0,0 +1,338 @@ +/* + * Copyright 2017-2023 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.http.client.jdk; + +import io.micronaut.context.BeanContext; +import io.micronaut.context.BeanProvider; +import io.micronaut.context.annotation.Bean; +import io.micronaut.context.annotation.BootstrapContextCompatible; +import io.micronaut.context.annotation.Factory; +import io.micronaut.context.annotation.Parameter; +import io.micronaut.context.annotation.Primary; +import io.micronaut.core.annotation.AnnotationMetadata; +import io.micronaut.core.annotation.Experimental; +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.Nullable; +import io.micronaut.core.annotation.Order; +import io.micronaut.core.convert.ConversionService; +import io.micronaut.core.io.ResourceResolver; +import io.micronaut.core.util.StringUtils; +import io.micronaut.http.MediaType; +import io.micronaut.http.annotation.FilterMatcher; +import io.micronaut.http.bind.DefaultRequestBinderRegistry; +import io.micronaut.http.bind.RequestBinderRegistry; +import io.micronaut.http.client.HttpClient; +import io.micronaut.http.client.HttpClientConfiguration; +import io.micronaut.http.client.HttpClientRegistry; +import io.micronaut.http.client.HttpVersionSelection; +import io.micronaut.http.client.LoadBalancer; +import io.micronaut.http.client.LoadBalancerResolver; +import io.micronaut.http.client.annotation.Client; +import io.micronaut.http.client.exceptions.HttpClientException; +import io.micronaut.http.client.jdk.cookie.CompositeCookieDecoder; +import io.micronaut.http.client.jdk.cookie.CookieDecoder; +import io.micronaut.http.client.jdk.cookie.DefaultCookieDecoder; +import io.micronaut.http.codec.MediaTypeCodec; +import io.micronaut.http.codec.MediaTypeCodecRegistry; +import io.micronaut.inject.InjectionPoint; +import io.micronaut.inject.qualifiers.Qualifiers; +import io.micronaut.json.JsonFeatures; +import io.micronaut.json.JsonMapper; +import io.micronaut.json.codec.MapperMediaTypeCodec; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Factory to create {@literal java.net.http.*} HTTP Clients. + * + * @author Sergio del Amo + * @author Tim Yates + * @since 4.0.0 + */ +@Factory +@BootstrapContextCompatible +@Order(2) // If both this and the netty client are present, netty is the default. +@Internal +@Experimental +public final class DefaultJdkHttpClientRegistry implements AutoCloseable, HttpClientRegistry { + + private static final Logger LOG = LoggerFactory.getLogger(DefaultJdkHttpClientRegistry.class); + + private final Map clients = new ConcurrentHashMap<>(10); + private final BeanContext beanContext; + private final LoadBalancerResolver loadBalancerResolver; + private final HttpClientConfiguration defaultHttpClientConfiguration; + private final JsonMapper jsonMapper; + @Nullable + private final MediaTypeCodecRegistry mediaTypeCodecRegistry; + private final BeanProvider requestBinderRegistryProvider; + private final JdkClientSslBuilder jdkClientSslBuilder; + private final CookieDecoder cookieDecoder; + + public DefaultJdkHttpClientRegistry( + BeanContext beanContext, + LoadBalancerResolver loadBalancerResolver, HttpClientConfiguration defaultHttpClientConfiguration, + JsonMapper jsonMapper, + @Nullable MediaTypeCodecRegistry mediaTypeCodecRegistry, + BeanProvider requestBinderRegistryProvider, + BeanProvider sslBuilderBeanProvider, + BeanProvider cookieDecoderBeanProvider + ) { + this.beanContext = beanContext; + this.loadBalancerResolver = loadBalancerResolver; + this.defaultHttpClientConfiguration = defaultHttpClientConfiguration; + this.jsonMapper = jsonMapper; + this.mediaTypeCodecRegistry = mediaTypeCodecRegistry; + this.requestBinderRegistryProvider = requestBinderRegistryProvider; + this.jdkClientSslBuilder = sslBuilderBeanProvider.orElse(new JdkClientSslBuilder(new ResourceResolver())); + this.cookieDecoder = cookieDecoderBeanProvider.orElse(new CompositeCookieDecoder(List.of(new DefaultCookieDecoder()))); + } + + private static MediaTypeCodec createNewJsonCodec(BeanContext beanContext, JsonFeatures jsonFeatures) { + return getJsonCodec(beanContext).cloneWithFeatures(jsonFeatures); + } + + private static MapperMediaTypeCodec getJsonCodec(BeanContext beanContext) { + return beanContext.getBean(MapperMediaTypeCodec.class, Qualifiers.byName(MapperMediaTypeCodec.REGULAR_JSON_MEDIA_TYPE_CODEC_NAME)); + } + + /** + * Creates a {@literal java.net.http.*} HTTP Client. + * + * @param injectionPoint + * @param loadBalancer + * @param configuration + * @param beanContext + * @return A {@literal java.net.http.*} HTTP Client + */ + @Bean + @BootstrapContextCompatible + @Primary + @Order(2) // If both this and the netty client are present, netty is the default. + protected DefaultJdkHttpClient httpClient( + @Nullable InjectionPoint injectionPoint, + @Parameter @Nullable LoadBalancer loadBalancer, + @Parameter @Nullable HttpClientConfiguration configuration, + BeanContext beanContext + ) { + return resolveDefaultHttpClient(injectionPoint, loadBalancer, configuration, beanContext); + } + + private DefaultJdkHttpClient resolveDefaultHttpClient( + @Nullable InjectionPoint injectionPoint, + @Nullable LoadBalancer loadBalancer, + @Nullable HttpClientConfiguration configuration, + BeanContext beanContext + ) { + if (loadBalancer != null) { + if (configuration == null) { + configuration = defaultHttpClientConfiguration; + } + return buildClient( + loadBalancer, + null, + configuration, + null, + loadBalancer.getContextPath().orElse(null), + beanContext + ); + } else { + return getClient(injectionPoint != null ? injectionPoint.getAnnotationMetadata() : AnnotationMetadata.EMPTY_METADATA); + } + } + + @Override + public DefaultJdkHttpClient getClient(AnnotationMetadata annotationMetadata) { + final ClientKey key = getClientKey(annotationMetadata); + return getClient(key); + } + + private ClientKey getClientKey(AnnotationMetadata metadata) { + HttpVersionSelection httpVersionSelection = HttpVersionSelection.forClientAnnotation(metadata); + String clientId = metadata.stringValue(Client.class).orElse(null); + String path = metadata.stringValue(Client.class, "path").orElse(null); + List filterAnnotation = metadata + .getAnnotationNamesByStereotype(FilterMatcher.class); + final Class configurationClass = + metadata.classValue(Client.class, "configuration").orElse(null); + JsonFeatures jsonFeatures = jsonMapper.detectFeatures(metadata).orElse(null); + + return new ClientKey(httpVersionSelection, clientId, filterAnnotation, path, configurationClass, jsonFeatures); + } + + private DefaultJdkHttpClient getClient(ClientKey key) { + return clients.computeIfAbsent(key, clientKey -> { + DefaultJdkHttpClient clientBean = null; + final String clientId = clientKey.clientId; + final Class configurationClass = clientKey.configurationClass; + + if (clientId != null) { + clientBean = (DefaultJdkHttpClient) this.beanContext.findBean(HttpClient.class, Qualifiers.byName(clientId)).orElse(null); + } + + if (configurationClass != null && !HttpClientConfiguration.class.isAssignableFrom(configurationClass)) { + throw new IllegalStateException("Referenced HTTP client configuration class must be an instance of HttpClientConfiguration for injection point: " + configurationClass); + } + + final List filterAnnotations = clientKey.filterAnnotations; + final String path = clientKey.path; + if (clientBean != null && path == null && configurationClass == null && filterAnnotations.isEmpty()) { + return clientBean; + } + + LoadBalancer loadBalancer = null; + final HttpClientConfiguration configuration; + if (configurationClass != null) { + configuration = (HttpClientConfiguration) this.beanContext.getBean(configurationClass); + } else if (clientId != null) { + configuration = this.beanContext.findBean( + HttpClientConfiguration.class, + Qualifiers.byName(clientId) + ).orElse(defaultHttpClientConfiguration); + } else { + configuration = defaultHttpClientConfiguration; + } + + if (clientId != null) { + loadBalancer = loadBalancerResolver.resolve(clientId) + .orElseThrow(() -> + new HttpClientException("Invalid service reference [" + clientId + "] specified to @Client")); + } + + String contextPath = null; + if (StringUtils.isNotEmpty(path)) { + contextPath = path; + } else if (StringUtils.isNotEmpty(clientId) && clientId.startsWith("/")) { + contextPath = clientId; + } else { + if (loadBalancer != null) { + contextPath = loadBalancer.getContextPath().orElse(null); + } + } + + DefaultJdkHttpClient client = buildClient(loadBalancer, clientKey.httpVersion, configuration, clientId, contextPath, beanContext); + + final JsonFeatures jsonFeatures = clientKey.jsonFeatures; + if (jsonFeatures != null) { + List codecs = new ArrayList<>(2); + MediaTypeCodecRegistry codecRegistry = client.getMediaTypeCodecRegistry(); + for (MediaTypeCodec codec : codecRegistry.getCodecs()) { + if (codec instanceof MapperMediaTypeCodec mapper) { + codecs.add(mapper.cloneWithFeatures(jsonFeatures)); + } else { + codecs.add(codec); + } + } + if (codecRegistry.findCodec(MediaType.APPLICATION_JSON_TYPE).isEmpty()) { + codecs.add(createNewJsonCodec(this.beanContext, jsonFeatures)); + } + client.setMediaTypeCodecRegistry(MediaTypeCodecRegistry.of(codecs)); + } + return client; + }); + } + + private DefaultJdkHttpClient buildClient( + LoadBalancer loadBalancer, + HttpVersionSelection httpVersion, + HttpClientConfiguration configuration, + String clientId, + String contextPath, + BeanContext beanContext + ) { + ConversionService conversionService = beanContext.getBean(ConversionService.class); + return new DefaultJdkHttpClient( + loadBalancer, + httpVersion, + configuration, + contextPath, + mediaTypeCodecRegistry, + requestBinderRegistryProvider.orElse(new DefaultRequestBinderRegistry(conversionService)), + clientId, + conversionService, + jdkClientSslBuilder, + cookieDecoder + ); + } + + @Override + public HttpClient getClient(HttpVersionSelection httpVersion, String clientId, String path) { + final ClientKey key = new ClientKey( + httpVersion, + clientId, + null, + path, + null, + null + ); + return getClient(key); + } + + @Override + public HttpClient resolveClient(InjectionPoint injectionPoint, LoadBalancer loadBalancer, HttpClientConfiguration configuration, BeanContext beanContext) { + return resolveDefaultHttpClient(injectionPoint, loadBalancer, configuration, beanContext); + } + + @Override + public void disposeClient(AnnotationMetadata annotationMetadata) { + final ClientKey key = getClientKey(annotationMetadata); + HttpClient client = clients.get(key); + if (client != null && client.isRunning()) { + client.close(); + clients.remove(key); + } + } + + @Override + public void close() throws Exception { + for (HttpClient httpClient : clients.values()) { + try { + httpClient.close(); + } catch (Throwable e) { + if (LOG.isWarnEnabled()) { + LOG.warn("Error shutting down HTTP client: " + e.getMessage(), e); + } + } + } + clients.clear(); + } + + /** + * Client key. + * + * @param httpVersion The HTTP version + * @param clientId The client ID + * @param filterAnnotations The filter annotations + * @param path The path + * @param configurationClass The configuration class + * @param jsonFeatures The JSON features + */ + @Internal + private record ClientKey( + HttpVersionSelection httpVersion, + String clientId, + List filterAnnotations, + String path, + Class configurationClass, + JsonFeatures jsonFeatures + ) { + } +} diff --git a/http-client-jdk/src/main/java/io/micronaut/http/client/jdk/HttpHeadersAdapter.java b/http-client-jdk/src/main/java/io/micronaut/http/client/jdk/HttpHeadersAdapter.java new file mode 100644 index 00000000000..2d320633587 --- /dev/null +++ b/http-client-jdk/src/main/java/io/micronaut/http/client/jdk/HttpHeadersAdapter.java @@ -0,0 +1,75 @@ +/* + * Copyright 2017-2023 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.http.client.jdk; + +import io.micronaut.core.annotation.Experimental; +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.convert.ArgumentConversionContext; +import io.micronaut.core.convert.ConversionService; +import io.micronaut.http.HttpHeaders; + +import java.util.Collection; +import java.util.List; +import java.util.Optional; +import java.util.Set; + +/** + * Adapter from {@link java.net.http.HttpHeaders} into {@link HttpHeaders}. + * @author Sergio del Amo + * @since 4.0.0 + */ +@Internal +@Experimental +public class HttpHeadersAdapter implements HttpHeaders { + + private final java.net.http.HttpHeaders httpHeaders; + private final ConversionService conversionService; + + /** + * + * @param httpHeaders HTTP Headers. + * @param conversionService Conversion Service. + */ + public HttpHeadersAdapter(java.net.http.HttpHeaders httpHeaders, ConversionService conversionService) { + this.httpHeaders = httpHeaders; + this.conversionService = conversionService; + } + + @Override + public List getAll(CharSequence name) { + return httpHeaders.allValues(name.toString()); + } + + @Override + public String get(CharSequence name) { + return httpHeaders.firstValue(name.toString()).orElse(null); + } + + @Override + public Set names() { + return httpHeaders.map().keySet(); + } + + @Override + public Collection> values() { + return httpHeaders.map().values(); + } + + @Override + public Optional get(CharSequence name, ArgumentConversionContext conversionContext) { + return conversionService.convert(get(name), conversionContext); + } +} diff --git a/http-client-jdk/src/main/java/io/micronaut/http/client/jdk/HttpRequestFactory.java b/http-client-jdk/src/main/java/io/micronaut/http/client/jdk/HttpRequestFactory.java new file mode 100644 index 00000000000..13ece7a3a7f --- /dev/null +++ b/http-client-jdk/src/main/java/io/micronaut/http/client/jdk/HttpRequestFactory.java @@ -0,0 +1,116 @@ +/* + * Copyright 2017-2023 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.http.client.jdk; + +import io.micronaut.core.annotation.Experimental; +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.NonNull; +import io.micronaut.core.async.publisher.Publishers; +import io.micronaut.core.type.Argument; +import io.micronaut.http.HttpHeaders; +import io.micronaut.http.MediaType; +import io.micronaut.http.client.HttpClientConfiguration; +import io.micronaut.http.codec.MediaTypeCodec; +import io.micronaut.http.codec.MediaTypeCodecRegistry; + +import java.net.URI; +import java.net.http.HttpRequest; +import java.util.Optional; + +/** + * Utility class to create {@link HttpRequest.Builder} from a Micronaut HTTP Request. + * + * @author Sergio del Amo + * @since 4.0.0 + */ +@Internal +@Experimental +public final class HttpRequestFactory { + + private HttpRequestFactory() { + } + + @NonNull + public static HttpRequest.Builder builder( + @NonNull URI uri, io.micronaut.http.HttpRequest request, + @NonNull HttpClientConfiguration configuration, + Argument bodyType, + MediaTypeCodecRegistry mediaTypeCodecRegistry + ) { + final HttpRequest.Builder builder = HttpRequest.newBuilder().uri(uri); + configuration.getReadTimeout().ifPresent(builder::timeout); + HttpRequest.BodyPublisher bodyPublisher = publisherForRequest(request, bodyType, mediaTypeCodecRegistry); + builder.method(request.getMethod().toString(), bodyPublisher); + request.getHeaders().forEach((name, values) -> values.forEach(value -> builder.header(name, value))); + if (request.getContentType().isEmpty()) { + builder.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON); + } + configuration.getReadTimeout().ifPresent(builder::timeout); + return builder; + } + + private static HttpRequest.BodyPublisher publisherForRequest( + io.micronaut.http.HttpRequest request, + Argument bodyType, + MediaTypeCodecRegistry mediaTypeCodecRegistry + ) { + if (io.micronaut.http.HttpMethod.permitsRequestBody(request.getMethod())) { + Optional body = request.getBody(); + boolean hasBody = body.isPresent(); + MediaType requestContentType = request.getContentType().orElseGet(() -> MediaType.APPLICATION_JSON_TYPE); + if (requestContentType.equals(MediaType.APPLICATION_FORM_URLENCODED_TYPE) && hasBody) { + Object bodyValue = body.get(); + if (bodyValue instanceof CharSequence) { + return HttpRequest.BodyPublishers.ofString(bodyValue.toString()); + } else { + throw unsupportedBodyType(bodyValue.getClass(), requestContentType.toString()); + } + } else if (requestContentType.equals(MediaType.MULTIPART_FORM_DATA_TYPE) && hasBody) { + Object bodyValue = body.get(); + throw unsupportedBodyType(bodyValue.getClass(), requestContentType.toString()); + } else { + if (hasBody) { + Object bodyValue = body.get(); + if (Publishers.isConvertibleToPublisher(bodyValue)) { + throw unsupportedBodyType(bodyValue.getClass(), requestContentType.toString()); + } else if (bodyValue instanceof CharSequence) { + return HttpRequest.BodyPublishers.ofString(bodyValue.toString()); + } else if (mediaTypeCodecRegistry != null) { + Optional registeredCodec = mediaTypeCodecRegistry.findCodec(requestContentType); + var encoded = registeredCodec.map(codec -> { + if (bodyType != null && bodyType.isInstance(bodyValue)) { + return codec.encode((Argument) bodyType, bodyValue); + } else { + return codec.encode(bodyValue); + } + }) + .orElse(null); + if (encoded != null) { + return HttpRequest.BodyPublishers.ofByteArray(encoded); + } else { + return HttpRequest.BodyPublishers.noBody(); + } + } + } + } + } + return HttpRequest.BodyPublishers.noBody(); + } + + private static UnsupportedOperationException unsupportedBodyType(Class clazz, String contentType) { + return new UnsupportedOperationException("Body of type [" + clazz + "] as " + contentType + " is not yet supported"); + } +} diff --git a/http-client-jdk/src/main/java/io/micronaut/http/client/jdk/HttpResponseAdapter.java b/http-client-jdk/src/main/java/io/micronaut/http/client/jdk/HttpResponseAdapter.java new file mode 100644 index 00000000000..77a802f8c26 --- /dev/null +++ b/http-client-jdk/src/main/java/io/micronaut/http/client/jdk/HttpResponseAdapter.java @@ -0,0 +1,113 @@ +/* + * Copyright 2017-2023 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.http.client.jdk; + +import io.micronaut.core.annotation.Experimental; +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.NonNull; +import io.micronaut.core.annotation.Nullable; +import io.micronaut.core.convert.ConversionContext; +import io.micronaut.core.convert.ConversionService; +import io.micronaut.core.convert.value.MutableConvertibleValues; +import io.micronaut.core.convert.value.MutableConvertibleValuesMap; +import io.micronaut.core.type.Argument; +import io.micronaut.http.HttpHeaders; +import io.micronaut.http.HttpResponse; +import io.micronaut.http.HttpStatus; +import io.micronaut.http.MediaType; +import io.micronaut.http.codec.MediaTypeCodec; +import io.micronaut.http.codec.MediaTypeCodecRegistry; + +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.util.Optional; + +/** + * Adapter from {@link java.net.http.HttpResponse} to {@link HttpResponse}. + * @author Sergio del Amo + * @since 4.0.0 + * @param Body Type + */ +@Internal +@Experimental +public class HttpResponseAdapter implements HttpResponse { + + private final java.net.http.HttpResponse httpResponse; + @NonNull + private final Argument bodyType; + private final ConversionService conversionService; + private final MutableConvertibleValues attributes = new MutableConvertibleValuesMap<>(); + + private final MediaTypeCodecRegistry mediaTypeCodecRegistry; + + public HttpResponseAdapter(java.net.http.HttpResponse httpResponse, + @NonNull Argument bodyType, + ConversionService conversionService, + MediaTypeCodecRegistry mediaTypeCodecRegistry) { + this.httpResponse = httpResponse; + this.bodyType = bodyType; + this.conversionService = conversionService; + this.mediaTypeCodecRegistry = mediaTypeCodecRegistry; + } + + @Override + public HttpStatus getStatus() { + return HttpStatus.valueOf(httpResponse.statusCode()); + } + + @Override + public int code() { + return httpResponse.statusCode(); + } + + @Override + public String reason() { + return getStatus().getReason(); + } + + @Override + public HttpHeaders getHeaders() { + return new HttpHeadersAdapter(httpResponse.headers(), conversionService); + } + + @Override + public MutableConvertibleValues getAttributes() { + return attributes; + } + + @Override + public Optional getBody() { + return convertBytes(getContentType().orElse(null), httpResponse.body(), bodyType); + } + + private Optional convertBytes(@Nullable MediaType contentType, byte[] bytes, Argument type) { + if (type != null && mediaTypeCodecRegistry != null && contentType != null) { + if (CharSequence.class.isAssignableFrom(type.getType())) { + Charset charset = contentType.getCharset().orElse(StandardCharsets.UTF_8); + return Optional.of(new String(bytes, charset)); + } else if (type.getType() == byte[].class) { + return Optional.of(bytes); + } else { + Optional foundCodec = mediaTypeCodecRegistry.findCodec(contentType); + if (foundCodec.isPresent()) { + return foundCodec.map(codec -> codec.decode(type, bytes)); + } + } + } + // last chance, try type conversion + return type != null ? conversionService.convert(bytes, ConversionContext.of(type)) : Optional.empty(); + } +} diff --git a/http-client-jdk/src/main/java/io/micronaut/http/client/jdk/JdkBlockingHttpClient.java b/http-client-jdk/src/main/java/io/micronaut/http/client/jdk/JdkBlockingHttpClient.java new file mode 100644 index 00000000000..4565a383215 --- /dev/null +++ b/http-client-jdk/src/main/java/io/micronaut/http/client/jdk/JdkBlockingHttpClient.java @@ -0,0 +1,104 @@ +/* + * Copyright 2017-2023 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.http.client.jdk; + +import io.micronaut.core.annotation.Experimental; +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.convert.ConversionService; +import io.micronaut.core.type.Argument; +import io.micronaut.http.HttpStatus; +import io.micronaut.http.bind.RequestBinderRegistry; +import io.micronaut.http.client.BlockingHttpClient; +import io.micronaut.http.client.HttpClientConfiguration; +import io.micronaut.http.client.HttpVersionSelection; +import io.micronaut.http.client.LoadBalancer; +import io.micronaut.http.client.exceptions.HttpClientException; +import io.micronaut.http.client.exceptions.HttpClientExceptionUtils; +import io.micronaut.http.client.exceptions.HttpClientResponseException; +import io.micronaut.http.client.jdk.cookie.CookieDecoder; +import io.micronaut.http.codec.MediaTypeCodecRegistry; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.net.http.HttpResponse; + +/** + * {@link io.micronaut.http.client.HttpClient} implementation for {@literal java.net.http.*} HTTP Client. + * @author Sergio del Amo + * @since 4.0.0 + */ +@Internal +@Experimental +public class JdkBlockingHttpClient extends AbstractJdkHttpClient implements BlockingHttpClient { + + public JdkBlockingHttpClient( + LoadBalancer loadBalancer, + HttpVersionSelection httpVersion, + HttpClientConfiguration configuration, + String contextPath, + MediaTypeCodecRegistry mediaTypeCodecRegistry, + RequestBinderRegistry requestBinderRegistry, + String clientId, + ConversionService conversionService, + JdkClientSslBuilder sslBuilder, + CookieDecoder cookieDecoder + ) { + super( + configuration.getLoggerName().map(LoggerFactory::getLogger).orElseGet(() -> LoggerFactory.getLogger(JdkBlockingHttpClient.class)), + loadBalancer, + httpVersion, + configuration, + contextPath, + mediaTypeCodecRegistry, + requestBinderRegistry, + clientId, + conversionService, + sslBuilder, + cookieDecoder + ); + } + + @Override + public io.micronaut.http.HttpResponse exchange(io.micronaut.http.HttpRequest request, + Argument bodyType, + Argument errorType) { + var httpRequest = mapToHttpRequest(request, bodyType).block(); + try { + if (log.isDebugEnabled()) { + log.debug("Client {} Sending HTTP Request: {}", clientId, httpRequest); + } + HttpResponse httpResponse = client.send(httpRequest, HttpResponse.BodyHandlers.ofByteArray()); + boolean errorStatus = httpResponse.statusCode() >= 400; + if (errorStatus && configuration.isExceptionOnErrorStatus()) { + if (log.isErrorEnabled()) { + log.error("Client {} Received HTTP Response: {} {}", clientId, httpResponse.statusCode(), httpResponse.uri()); + } + throw HttpClientExceptionUtils.populateServiceId(new HttpClientResponseException(HttpStatus.valueOf(httpResponse.statusCode()).getReason(), response(httpResponse, bodyType)), clientId, configuration); + } + return response(httpResponse, bodyType); + } catch (IOException e) { + throw new HttpClientException("Error sending request: " + e.getMessage(), e); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new HttpClientException("Error sending request: " + e.getMessage(), e); + } + } + + @Override + public void close() throws IOException { + // Nothing to do here, we do not need to close clients + } +} diff --git a/http-client-jdk/src/main/java/io/micronaut/http/client/jdk/JdkClientSslBuilder.java b/http-client-jdk/src/main/java/io/micronaut/http/client/jdk/JdkClientSslBuilder.java new file mode 100644 index 00000000000..6fcbf1b8026 --- /dev/null +++ b/http-client-jdk/src/main/java/io/micronaut/http/client/jdk/JdkClientSslBuilder.java @@ -0,0 +1,120 @@ +/* + * Copyright 2017-2023 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.http.client.jdk; + +import io.micronaut.context.annotation.BootstrapContextCompatible; +import io.micronaut.core.annotation.Experimental; +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.Nullable; +import io.micronaut.core.io.ResourceResolver; +import io.micronaut.http.HttpVersion; +import io.micronaut.http.client.HttpVersionSelection; +import io.micronaut.http.ssl.ClientSslConfiguration; +import io.micronaut.http.ssl.SslBuilder; +import io.micronaut.http.ssl.SslConfiguration; +import io.micronaut.http.ssl.SslConfigurationException; +import jakarta.inject.Singleton; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.SSLContext; +import javax.net.ssl.TrustManager; +import javax.net.ssl.TrustManagerFactory; +import javax.net.ssl.X509TrustManager; +import java.security.KeyManagementException; +import java.security.NoSuchAlgorithmException; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.util.Optional; + +/** + * The Javanet implementation of {@link SslBuilder} that generates an {@link SSLContext} to create a client that + * supports SSL. + * + * @author Tim Yates + * @since 4.0.0 + */ +@Singleton +@Internal +@Experimental +@BootstrapContextCompatible +public final class JdkClientSslBuilder extends SslBuilder { + + private static final Logger LOG = LoggerFactory.getLogger(JdkClientSslBuilder.class); + + /** + * @param resourceResolver The resource resolver + */ + public JdkClientSslBuilder(ResourceResolver resourceResolver) { + super(resourceResolver); + } + + @Override + public Optional build(SslConfiguration ssl) { + return build(ssl, HttpVersion.HTTP_1_1); + } + + @Override + public Optional build(SslConfiguration ssl, HttpVersion httpVersion) { + return Optional.ofNullable(build(ssl, HttpVersionSelection.forLegacyVersion(httpVersion))); + } + + @Nullable + public SSLContext build(SslConfiguration ssl, HttpVersionSelection versionSelection) { + if (!ssl.isEnabled()) { + return null; + } + TrustManagerFactory trustManagerFactory = getTrustManagerFactory(ssl); + KeyManagerFactory keyManagerFactory = getKeyManagerFactory(ssl); + try { + SSLContext tls = SSLContext.getInstance(ssl.getProtocol().orElse("TLS")); + if (trustManagerFactory == null) { + trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + } + TrustManager[] trustManagers = trustManagerFactory.getTrustManagers(); + if (ssl instanceof ClientSslConfiguration clientSslConfiguration && clientSslConfiguration.isInsecureTrustAllCertificates()) { + if (LOG.isWarnEnabled()) { + LOG.warn("Trust all certificates is enabled. This is insecure and should not be used in production"); + } + trustManagers = new TrustManager[] { new TrustAllTrustManager() }; + } + tls.init(keyManagerFactory.getKeyManagers(), trustManagers, null); + return tls; + } catch (NoSuchAlgorithmException | KeyManagementException e) { + throw new SslConfigurationException("Error initializing SSL context: " + e.getMessage(), e); + } + } + + @SuppressWarnings("java:S4830") // This is explicitly to turn security off when isInsecureTrustAllCertificates + private static class TrustAllTrustManager implements X509TrustManager { + + @Override + public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException { + // trust everything + } + + @Override + public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException { + // trust everything + } + + @Override + public X509Certificate[] getAcceptedIssuers() { + return new X509Certificate[0]; + } + } +} diff --git a/http-client-jdk/src/main/java/io/micronaut/http/client/jdk/JdkHttpClient.java b/http-client-jdk/src/main/java/io/micronaut/http/client/jdk/JdkHttpClient.java new file mode 100644 index 00000000000..2eea802d1fc --- /dev/null +++ b/http-client-jdk/src/main/java/io/micronaut/http/client/jdk/JdkHttpClient.java @@ -0,0 +1,29 @@ +/* + * Copyright 2017-2023 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.http.client.jdk; + +import io.micronaut.core.annotation.Experimental; +import io.micronaut.http.client.HttpClient; + +/** + * Marker interface for {@link io.micronaut.http.client.HttpClient} implementations that use the {@literal java.net.http.*} HTTP Client. + * + * @since 4.0.0 + * @author Tim Yates + */ +@Experimental +public interface JdkHttpClient extends HttpClient { +} diff --git a/http-client-jdk/src/main/java/io/micronaut/http/client/jdk/JdkHttpClientFactory.java b/http-client-jdk/src/main/java/io/micronaut/http/client/jdk/JdkHttpClientFactory.java new file mode 100644 index 00000000000..3da749ca636 --- /dev/null +++ b/http-client-jdk/src/main/java/io/micronaut/http/client/jdk/JdkHttpClientFactory.java @@ -0,0 +1,48 @@ +/* + * Copyright 2017-2023 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.http.client.jdk; + +import io.micronaut.core.annotation.Experimental; +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.convert.ConversionService; +import io.micronaut.http.client.AbstractHttpClientFactory; +import io.micronaut.http.client.HttpClientConfiguration; + +import java.net.URI; + +/** + * Factory to create {@literal java.net.http.*} HTTP Clients. + * @author Sergio del Amo + * @since 4.0.0 + */ +@Internal +@Experimental +public class JdkHttpClientFactory extends AbstractHttpClientFactory { + + public JdkHttpClientFactory() { + super(null, ConversionService.SHARED); + } + + @Override + protected DefaultJdkHttpClient createHttpClient(URI uri) { + return new DefaultJdkHttpClient(uri, conversionService); + } + + @Override + protected DefaultJdkHttpClient createHttpClient(URI uri, HttpClientConfiguration configuration) { + return new DefaultJdkHttpClient(uri, configuration, mediaTypeCodecRegistry, conversionService); + } +} diff --git a/http-client-jdk/src/main/java/io/micronaut/http/client/jdk/cookie/CompositeCookieDecoder.java b/http-client-jdk/src/main/java/io/micronaut/http/client/jdk/cookie/CompositeCookieDecoder.java new file mode 100644 index 00000000000..9235198e756 --- /dev/null +++ b/http-client-jdk/src/main/java/io/micronaut/http/client/jdk/cookie/CompositeCookieDecoder.java @@ -0,0 +1,56 @@ +/* + * Copyright 2017-2023 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.http.client.jdk.cookie; + +import io.micronaut.context.annotation.Primary; +import io.micronaut.core.annotation.Experimental; +import io.micronaut.core.annotation.Internal; +import io.micronaut.http.HttpRequest; +import io.micronaut.http.cookie.Cookies; +import jakarta.inject.Inject; +import jakarta.inject.Singleton; + +import java.util.List; +import java.util.Optional; + +/** + * Iterate the cookieDecoders and return the first one that returns cookies. + * + * @since 4.0.0 + * @author Tim Yates + */ +@Primary +@Singleton +@Experimental +@Internal +public class CompositeCookieDecoder implements CookieDecoder { + + private final List cookieDecoders; + + @Inject + public CompositeCookieDecoder(List cookieDecoders) { + this.cookieDecoders = cookieDecoders; + } + + @Override + public Optional decode(HttpRequest request) { + return cookieDecoders.stream() + .map(d -> d.decode(request)) + .filter(Optional::isPresent) + .findFirst() + .orElseGet(Optional::empty); + } +} diff --git a/http-client-jdk/src/main/java/io/micronaut/http/client/jdk/cookie/CookieDecoder.java b/http-client-jdk/src/main/java/io/micronaut/http/client/jdk/cookie/CookieDecoder.java new file mode 100644 index 00000000000..c58bc16a9b0 --- /dev/null +++ b/http-client-jdk/src/main/java/io/micronaut/http/client/jdk/cookie/CookieDecoder.java @@ -0,0 +1,41 @@ +/* + * Copyright 2017-2023 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.http.client.jdk.cookie; + +import io.micronaut.core.annotation.NonNull; +import io.micronaut.core.order.Ordered; +import io.micronaut.http.HttpRequest; +import io.micronaut.http.cookie.Cookies; + +import java.util.Optional; + +/** + * Interface to allow cookie decoding. + * + * @since 4.0.0 + * @author Tim Yates + */ +public interface CookieDecoder extends Ordered { + + /** + * Decode the cookies from the request. + * + * @param request the request + * @return the cookies or empty if none + */ + @NonNull + Optional decode(HttpRequest request); +} diff --git a/http-client-jdk/src/main/java/io/micronaut/http/client/jdk/cookie/DefaultCookieDecoder.java b/http-client-jdk/src/main/java/io/micronaut/http/client/jdk/cookie/DefaultCookieDecoder.java new file mode 100644 index 00000000000..0f44122dfbe --- /dev/null +++ b/http-client-jdk/src/main/java/io/micronaut/http/client/jdk/cookie/DefaultCookieDecoder.java @@ -0,0 +1,46 @@ +/* + * Copyright 2017-2023 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.http.client.jdk.cookie; + +import io.micronaut.core.annotation.Experimental; +import io.micronaut.core.annotation.Internal; +import io.micronaut.http.HttpRequest; +import io.micronaut.http.cookie.Cookies; +import jakarta.inject.Singleton; + +import java.util.Optional; + +/** + * Default implementation of {@link CookieDecoder} that returns the cookies from the request. + * + * @since 4.0.0 + * @author Tim Yates + */ +@Singleton +@Experimental +@Internal +public class DefaultCookieDecoder implements CookieDecoder { + + @Override + public Optional decode(HttpRequest request) { + return Optional.of(request.getCookies()); + } + + @Override + public int getOrder() { + return NettyCookieDecoder.ORDER + 1; + } +} diff --git a/http-client-jdk/src/main/java/io/micronaut/http/client/jdk/cookie/NettyCookieDecoder.java b/http-client-jdk/src/main/java/io/micronaut/http/client/jdk/cookie/NettyCookieDecoder.java new file mode 100644 index 00000000000..c09be9812c1 --- /dev/null +++ b/http-client-jdk/src/main/java/io/micronaut/http/client/jdk/cookie/NettyCookieDecoder.java @@ -0,0 +1,89 @@ +/* + * Copyright 2017-2023 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.http.client.jdk.cookie; + +import io.micronaut.context.annotation.Requires; +import io.micronaut.core.annotation.Experimental; +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.NonNull; +import io.micronaut.core.convert.ConversionService; +import io.micronaut.http.HttpHeaders; +import io.micronaut.http.HttpRequest; +import io.micronaut.http.client.netty.NettyClientHttpRequest; +import io.micronaut.http.cookie.Cookie; +import io.micronaut.http.cookie.Cookies; +import io.micronaut.http.simple.cookies.SimpleCookie; +import io.micronaut.http.simple.cookies.SimpleCookies; +import jakarta.inject.Singleton; + +import java.net.HttpCookie; +import java.util.Collection; +import java.util.List; +import java.util.Optional; + +/** + * A cookie decoder that extracts cookies from the {@link NettyClientHttpRequest} if it is present. + * Required as {@link NettyClientHttpRequest} does not implement {@link HttpRequest#getCookies()}. + * + * @since 4.0.0 + * @author Tim Yates + */ +@Singleton +@Experimental +@Internal +@Requires(classes = NettyClientHttpRequest.class) +public class NettyCookieDecoder implements CookieDecoder { + + public static final int ORDER = 1; + private final ConversionService conversionService; + + public NettyCookieDecoder(ConversionService conversionService) { + this.conversionService = conversionService; + } + + @NonNull + @Override + public Optional decode(HttpRequest request) { + if (request instanceof NettyClientHttpRequest nettyClientHttpRequest) { + SimpleCookies entries = new SimpleCookies(conversionService); + + List headerCookies = nettyClientHttpRequest + .getHeaders() + .getAll(HttpHeaders.COOKIE) + .stream() + .map(HttpCookie::parse) + .flatMap(Collection::stream) + .toList(); + + headerCookies.forEach(cookie -> { + Cookie c = new SimpleCookie(cookie.getName(), cookie.getValue()); + c.maxAge(cookie.getMaxAge()); + c.domain(cookie.getDomain()); + c.httpOnly(cookie.isHttpOnly()); + c.path(cookie.getPath()); + c.secure(cookie.getSecure()); + entries.put(cookie.getName(), c); + }); + return Optional.of(entries); + } + return Optional.empty(); + } + + @Override + public int getOrder() { + return ORDER; + } +} diff --git a/http-client-jdk/src/main/resources/META-INF/services/io.micronaut.http.client.HttpClientFactory b/http-client-jdk/src/main/resources/META-INF/services/io.micronaut.http.client.HttpClientFactory new file mode 100644 index 00000000000..b0dd611e313 --- /dev/null +++ b/http-client-jdk/src/main/resources/META-INF/services/io.micronaut.http.client.HttpClientFactory @@ -0,0 +1 @@ +io.micronaut.http.client.jdk.JdkHttpClientFactory diff --git a/http-client-jdk/src/test/groovy/io/micronaut/http/client/jdk/ClientLoggerNameSpec.groovy b/http-client-jdk/src/test/groovy/io/micronaut/http/client/jdk/ClientLoggerNameSpec.groovy new file mode 100644 index 00000000000..cd209222c13 --- /dev/null +++ b/http-client-jdk/src/test/groovy/io/micronaut/http/client/jdk/ClientLoggerNameSpec.groovy @@ -0,0 +1,79 @@ +package io.micronaut.http.client.jdk + +import io.micronaut.context.annotation.Property +import io.micronaut.context.annotation.Requires +import io.micronaut.http.annotation.Controller +import io.micronaut.http.annotation.Get +import io.micronaut.http.client.HttpClient +import io.micronaut.http.client.HttpClientConfiguration +import io.micronaut.http.client.annotation.Client +import io.micronaut.test.extensions.spock.annotation.MicronautTest +import jakarta.inject.Inject +import jakarta.inject.Singleton +import spock.lang.Specification + +@MicronautTest +@Property(name = "spec.name", value = "ClientLoggerNameSpec") +class ClientLoggerNameSpec extends Specification { + + @Inject + @Client(value = "/one", configuration = OneConfig) + HttpClient oneClient + + @Inject + @Client(value = "/two", configuration = TwoConfig) + HttpClient twoClient + + def "test"() { + when: + def one = oneClient.toBlocking().retrieve("/") + def two = twoClient.toBlocking().retrieve("/") + + then: + one == "one" + two == "two" + } + + @Requires(property = "spec.name", value = "ClientLoggerNameSpec") + @Controller + static class SpecController { + + @Get("/one") + String one() { + "one" + } + + @Get("/two") + String two() { + "two" + } + } + + @Singleton + @Requires(property = "spec.name", value = "ClientLoggerNameSpec") + static final class OneConfig extends HttpClientConfiguration { + + OneConfig() { + setLoggerName("named.client.one") + } + + @Override + ConnectionPoolConfiguration getConnectionPoolConfiguration() { + return null + } + } + + @Singleton + @Requires(property = "spec.name", value = "ClientLoggerNameSpec") + static final class TwoConfig extends HttpClientConfiguration { + + TwoConfig() { + setLoggerName("named.client.two") + } + + @Override + ConnectionPoolConfiguration getConnectionPoolConfiguration() { + return null + } + } +} diff --git a/http-client-jdk/src/test/groovy/io/micronaut/http/client/jdk/ClientProxySpec.groovy b/http-client-jdk/src/test/groovy/io/micronaut/http/client/jdk/ClientProxySpec.groovy new file mode 100644 index 00000000000..dbf02dbb8bd --- /dev/null +++ b/http-client-jdk/src/test/groovy/io/micronaut/http/client/jdk/ClientProxySpec.groovy @@ -0,0 +1,117 @@ +package io.micronaut.http.client.jdk + +import io.micronaut.context.ApplicationContext +import io.micronaut.context.env.Environment +import io.micronaut.http.HttpResponse +import io.micronaut.http.HttpStatus +import io.micronaut.http.client.HttpClient +import io.micronaut.http.client.exceptions.HttpClientException +import io.micronaut.runtime.server.EmbeddedServer +import org.testcontainers.DockerClientFactory +import org.testcontainers.containers.GenericContainer +import org.testcontainers.containers.wait.strategy.HostPortWaitStrategy +import org.testcontainers.utility.MountableFile +import spock.lang.AutoCleanup +import spock.lang.Requires +import spock.lang.Specification +import spock.util.environment.RestoreSystemProperties + +@Requires({ DockerClientFactory.instance().isDockerAvailable() }) +class ClientProxySpec extends Specification { + + static final int PROXY_PORT = 3128 + static final String URL = "https://micronaut.io/" + static final String HTML_FRAGMENT = 'Home - Micronaut Framework' + + @AutoCleanup + GenericContainer proxyContainer = + new GenericContainer('sameersbn/squid:latest') + .withCopyFileToContainer(MountableFile.forClasspathResource('/squid.conf'), '/etc/squid/squid.conf') + .withExposedPorts(PROXY_PORT) + .withLogConsumer { outputFrame -> print("SQUID::\t${outputFrame.getUtf8String()}") } + .waitingFor(new HostPortWaitStrategy()) + + @AutoCleanup + EmbeddedServer embeddedServer + + def setup() { + proxyContainer.start() + } + + void "test downloading via http proxy using proxy-address"() { + given: + startServer([ + 'micronaut.http.client.exception-on-error-status': false, + 'micronaut.http.client.proxy-type' : 'http', + 'micronaut.http.client.proxy-address' : "${proxyHost}:${proxyPort}" + ]) + + when: 'download page via proxy' + def response = downloadPage() + + then: 'page is downloaded' + response.status == HttpStatus.OK + response.body().contains(HTML_FRAGMENT) + + when: 'proxy container is stopped' + proxyContainer.stop() + + then: 'page download fails' + downloadFailsWithException() instanceof HttpClientException + } + + @RestoreSystemProperties + void "test downloading via http proxy using default proxy-selector"() { + given: + System.setProperty('https.proxyHost', proxyHost) + System.setProperty('https.proxyPort', proxyPort.toString()) + startServer([ + 'micronaut.http.client.exception-on-error-status': false, + 'micronaut.http.client.proxy-selector' : 'default' + ]) + + when: 'download page via proxy' + def response = downloadPage() + + then: 'page is downloaded' + response.status == HttpStatus.OK + response.body().contains(HTML_FRAGMENT) + + when: 'proxy container is stopped' + proxyContainer.stop() + + then: 'page download fails' + downloadFailsWithException() instanceof HttpClientException + } + + private String getProxyHost() { + proxyContainer.host + } + + private int getProxyPort() { + def port = proxyContainer.getMappedPort(PROXY_PORT) + println("Proxy port: ${port}") + port + } + + private void startServer(Map config) { + embeddedServer = ApplicationContext.run(EmbeddedServer, config, Environment.TEST) + } + + private HttpClient getClient() { + embeddedServer.applicationContext.getBean(HttpClient) + } + + private HttpResponse downloadPage() { + client.toBlocking().exchange(URL, String) + } + + private Throwable downloadFailsWithException() { + try { + downloadPage() + return null + } catch (Throwable ex) { + return ex + } + } +} diff --git a/http-client-jdk/src/test/groovy/io/micronaut/http/client/jdk/ClientVersioningSpec.groovy b/http-client-jdk/src/test/groovy/io/micronaut/http/client/jdk/ClientVersioningSpec.groovy new file mode 100644 index 00000000000..bfc4f03f39f --- /dev/null +++ b/http-client-jdk/src/test/groovy/io/micronaut/http/client/jdk/ClientVersioningSpec.groovy @@ -0,0 +1,139 @@ +package io.micronaut.http.client.jdk + +import io.micronaut.context.ApplicationContext +import io.micronaut.context.annotation.Requires +import io.micronaut.core.version.annotation.Version +import io.micronaut.http.HttpRequest +import io.micronaut.http.annotation.Controller +import io.micronaut.http.annotation.Get +import io.micronaut.http.client.annotation.Client +import io.micronaut.runtime.server.EmbeddedServer +import spock.lang.AutoCleanup +import spock.lang.Shared +import spock.lang.Specification + +class ClientVersioningSpec extends Specification { + + @Shared + @AutoCleanup + EmbeddedServer server = ApplicationContext.run(EmbeddedServer,[ + 'spec.name': 'ClientVersioningSpec', + "micronaut.http.client.versioning./.headers" : ['X-API-VERSION'], + "micronaut.http.client.versioning./.parameters" : ['api-version'], + "micronaut.http.client.versioning.default.parameters": ['version'], + ]) + + def "should has 'api-version' parameter inside client request"() { + when: + def client = server.applicationContext.getBean(VersionedClient) + def response = client.withParameter() + then: + response == '1' + } + + def "should has 'X-API-VERSION' header inside client request"() { + when: + def client = server.applicationContext.getBean(VersionedClient) + def response = client.withHeader() + then: + response == '1' + } + + def "should has 'X-API-VERSION' header from class level '@Version'"() { + when: + def client = server.applicationContext.getBean(VersionedClient) + def response = client.overrideWithClass() + then: + response == '0' + } + + def "should populate with method level '@Version'"() { + when: + def client = server.applicationContext.getBean(VersionedClient) + def response = client.overrideWithClass() + then: + response == '0' + } + + def "should fallback to default configuration"() { + when: + def client = server.applicationContext.getBean(DefaulConfVersionedClient) + def response = client.request1() + then: + response == '1' + } + + def "should ignore empty version string"() { + when: + def client = server.applicationContext.getBean(VersionedClient) + def response = client.withoutVersion() + then: + response == null + } + + @Requires(property = 'spec.name', value = 'ClientVersioningSpec') + @Version("0") + @Client("/") + static interface VersionedClient { + + @Version("1") + @Get("param") + String withParameter(); + + @Version("1") + @Get("header") + String withHeader(); + + @Get("override") + String overrideWithClass(); + + @Version("") + @Get("empty") + String withoutVersion(); + + } + + @Requires(property = 'spec.name', value = 'ClientVersioningSpec') + @Client("/notconfigured") + static interface DefaulConfVersionedClient { + + @Version("1") + @Get() + String request1(); + + @Version("2") + @Get() + String request2(); + } + + @Requires(property = 'spec.name', value = 'ClientVersioningSpec') + @Controller("/") + static class TestController { + + @Get("param") + String param(HttpRequest request) { + return request.parameters.get("api-version") + } + + @Get("header") + String header(HttpRequest request) { + return request.headers.get("X-API-VERSION") + } + + @Get("override") + String override(HttpRequest request) { + return request.headers.get("X-API-VERSION") + } + + @Get("notconfigured") + String notConfigured(HttpRequest request) { + return request.parameters.get("version") + } + + @Get("empty") + String empty(HttpRequest request) { + return request.parameters.get("version") + } + } + +} diff --git a/http-client-jdk/src/test/groovy/io/micronaut/http/client/jdk/H2CSpec.groovy b/http-client-jdk/src/test/groovy/io/micronaut/http/client/jdk/H2CSpec.groovy new file mode 100644 index 00000000000..1990b2f51cc --- /dev/null +++ b/http-client-jdk/src/test/groovy/io/micronaut/http/client/jdk/H2CSpec.groovy @@ -0,0 +1,28 @@ +package io.micronaut.http.client.jdk + +import io.micronaut.context.ApplicationContext +import io.micronaut.context.exceptions.BeanInstantiationException +import io.micronaut.context.exceptions.ConfigurationException +import io.micronaut.http.client.HttpClient +import spock.lang.Specification + +class H2CSpec extends Specification { + + def "h2c is not supported"() { + given: + def ctx = ApplicationContext.run( + 'micronaut.http.client.plaintext-mode': 'h2c' + ) + + when: + ctx.getBean(HttpClient) + + then: + def ex = thrown(BeanInstantiationException) + ex.cause instanceof ConfigurationException + ex.cause.message == AbstractJdkHttpClient.H2C_ERROR_MESSAGE + + cleanup: + ctx.close() + } +} diff --git a/http-client-jdk/src/test/groovy/io/micronaut/http/client/jdk/Http2Spec.groovy b/http-client-jdk/src/test/groovy/io/micronaut/http/client/jdk/Http2Spec.groovy new file mode 100644 index 00000000000..1899afd19e5 --- /dev/null +++ b/http-client-jdk/src/test/groovy/io/micronaut/http/client/jdk/Http2Spec.groovy @@ -0,0 +1,47 @@ +package io.micronaut.http.client.jdk + +import io.micronaut.context.annotation.Property +import io.micronaut.context.annotation.Requires +import io.micronaut.http.HttpRequest +import io.micronaut.http.MediaType +import io.micronaut.http.annotation.Controller +import io.micronaut.http.annotation.Get +import io.micronaut.http.annotation.Produces +import io.micronaut.http.client.HttpClient +import io.micronaut.http.client.HttpVersionSelection +import io.micronaut.http.client.annotation.Client +import io.micronaut.test.extensions.spock.annotation.MicronautTest +import jakarta.inject.Inject +import spock.lang.Specification + +@MicronautTest +@Property(name = "spec.name", value = "Http2Spec") +@Property(name = "micronaut.server.http-version", value = "HTTP_2_0") +@Property(name = "micronaut.server.ssl.build-self-signed", value = "true") +@Property(name = "micronaut.server.ssl.enabled", value = "true") +@Property(name = "micronaut.http.client.ssl.insecure-trust-all-certificates", value = "true") +class Http2Spec extends Specification { + + @Inject + @Client(value = "/", alpnModes = HttpVersionSelection.ALPN_HTTP_2) + HttpClient client + + def "test http2"() { + when: + def response = client.toBlocking().retrieve(HttpRequest.GET("/http2")) + + then: + response == "hello" + } + + @Controller("/http2") + @Requires(property = "spec.name", value = "Http2Spec") + static class SpecController { + + @Get + @Produces(MediaType.TEXT_PLAIN) + String get() { + "hello" + } + } +} diff --git a/http-client-jdk/src/test/groovy/io/micronaut/http/client/jdk/HttpProxySpec.groovy b/http-client-jdk/src/test/groovy/io/micronaut/http/client/jdk/HttpProxySpec.groovy new file mode 100644 index 00000000000..3466452eaf9 --- /dev/null +++ b/http-client-jdk/src/test/groovy/io/micronaut/http/client/jdk/HttpProxySpec.groovy @@ -0,0 +1,81 @@ +package io.micronaut.http.client.jdk + +import groovy.transform.Canonical +import groovy.transform.ToString +import io.micronaut.context.ApplicationContext +import io.micronaut.context.BeanContext +import io.micronaut.context.annotation.Requires +import io.micronaut.http.HttpRequest +import io.micronaut.http.annotation.Controller +import io.micronaut.http.annotation.Get +import io.micronaut.http.annotation.Produces +import io.micronaut.http.client.HttpClient +import io.micronaut.runtime.server.EmbeddedServer +import spock.lang.Specification + +class HttpProxySpec extends Specification { + + def "test http proxy"() { + given: 'a proxy server' + EmbeddedServer proxy = ApplicationContext.run(EmbeddedServer, [ + 'spec.name': 'HttpProxySpec_Proxy' + ]) + + and: 'a target server configured to use the proxy' + EmbeddedServer target = ApplicationContext.run(EmbeddedServer, [ + 'spec.name': 'HttpProxySpec', + 'micronaut.http.client.proxy-address': "localhost:${proxy.getURI().port}", + ]) + + and: 'the proxy knows where the target server is' + proxy.applicationContext.registerSingleton(new TargetPort(target.getPort())) + + and: 'a client for the target app' + def client = target.applicationContext.createBean(HttpClient, target.URL) + + when: 'we make a request' + def response = client.toBlocking().retrieve("/test/1") + + then: 'the response is proxied' + response == "Proxied hello" + + cleanup: + proxy.stop() + target.stop() + } + + @Controller("/test/{id}") + @Requires(property = "spec.name", value = "HttpProxySpec") + static class MyController { + + @Get + @Produces("text/plain") + String get() { + "hello" + } + } + + @Controller("/{whatever}/{id}") + @Requires(property = "spec.name", value = "HttpProxySpec_Proxy") + static class MyProxy { + + final BeanContext ctx + + MyProxy(BeanContext ctx) { + this.ctx = ctx + } + + @Get + String get(HttpRequest request) { + "Proxied " + ctx.createBean(HttpClient, "http://localhost:${ctx.getBean(TargetPort).port}".toURI()) + .toBlocking() + .retrieve(request.getUri().toASCIIString()) + } + } + + @Canonical + @ToString + static class TargetPort { + int port + } +} diff --git a/http-client-jdk/src/test/groovy/io/micronaut/http/client/jdk/OptionsRequestAttributesSpec.groovy b/http-client-jdk/src/test/groovy/io/micronaut/http/client/jdk/OptionsRequestAttributesSpec.groovy new file mode 100644 index 00000000000..5baef541258 --- /dev/null +++ b/http-client-jdk/src/test/groovy/io/micronaut/http/client/jdk/OptionsRequestAttributesSpec.groovy @@ -0,0 +1,61 @@ +package io.micronaut.http.client.jdk + +import io.micronaut.context.ApplicationContext +import io.micronaut.context.annotation.Requires +import io.micronaut.http.HttpAttributes +import io.micronaut.http.HttpRequest +import io.micronaut.http.HttpStatus +import io.micronaut.http.MutableHttpResponse +import io.micronaut.http.annotation.Controller +import io.micronaut.http.annotation.Filter +import io.micronaut.http.annotation.Get +import io.micronaut.http.client.HttpClient +import io.micronaut.http.client.exceptions.HttpClientResponseException +import io.micronaut.http.filter.HttpServerFilter +import io.micronaut.http.filter.ServerFilterChain +import io.micronaut.runtime.server.EmbeddedServer +import jakarta.inject.Singleton +import org.reactivestreams.Publisher +import org.spockframework.util.Assert +import spock.lang.Specification + +class OptionsRequestAttributesSpec extends Specification { + + def 'test OPTIONS requests attributes'() { + EmbeddedServer server = ApplicationContext.run(EmbeddedServer, ['spec.name': 'OptionsRequestAttributesSpec']) + def ctx = server.applicationContext + HttpClient client = ctx.createBean(HttpClient, server.getURL()) + + when: + client.toBlocking().exchange(HttpRequest.OPTIONS('/foo'), String) + + then: + HttpClientResponseException e = thrown() + e.response.status == HttpStatus.METHOD_NOT_ALLOWED + } + + @Singleton + @Controller + @Requires(property = 'spec.name', value = 'OptionsRequestAttributesSpec') + static class SimpleController { + @Get('/foo') + public String foo() { + return "bar" + } + } + + @Requires(property = "spec.name", value = "OptionsRequestAttributesSpec") + @Singleton + @Filter("/**") + static class MyFilter implements HttpServerFilter { + + @Override + Publisher> doFilter(HttpRequest request, ServerFilterChain chain) { + Assert.that(request.getAttributes().contains(HttpAttributes.ROUTE.toString())) + Assert.that(request.getAttributes().contains(HttpAttributes.ROUTE_MATCH.toString())) + Assert.that(request.getAttributes().contains(HttpAttributes.ROUTE_INFO.toString())) + Assert.that(request.getAttributes().contains(HttpAttributes.URI_TEMPLATE.toString())) + return chain.proceed(request) + } + } +} diff --git a/http-client-jdk/src/test/groovy/io/micronaut/http/client/jdk/RequestAttributeBindingSpec.groovy b/http-client-jdk/src/test/groovy/io/micronaut/http/client/jdk/RequestAttributeBindingSpec.groovy new file mode 100644 index 00000000000..cda0ad6cd6c --- /dev/null +++ b/http-client-jdk/src/test/groovy/io/micronaut/http/client/jdk/RequestAttributeBindingSpec.groovy @@ -0,0 +1,112 @@ +package io.micronaut.http.client.jdk + +import io.micronaut.context.annotation.Property +import io.micronaut.context.annotation.Requires +import io.micronaut.core.annotation.Nullable +import io.micronaut.http.HttpRequest +import io.micronaut.http.HttpStatus +import io.micronaut.http.MutableHttpResponse +import io.micronaut.http.annotation.Controller +import io.micronaut.http.annotation.Filter +import io.micronaut.http.annotation.Get +import io.micronaut.http.annotation.RequestAttribute +import io.micronaut.http.client.BlockingHttpClient +import io.micronaut.http.client.HttpClient +import io.micronaut.http.client.annotation.Client +import io.micronaut.http.client.exceptions.HttpClientResponseException +import io.micronaut.http.filter.HttpServerFilter +import io.micronaut.http.filter.ServerFilterChain +import io.micronaut.test.extensions.spock.annotation.MicronautTest +import jakarta.inject.Inject +import org.reactivestreams.Publisher +import spock.lang.Specification + +@MicronautTest +@Property(name = "spec.name", value = "RequestAttributeBindingSpec") +class RequestAttributeBindingSpec extends Specification { + + @Inject + @Client("/") + HttpClient rxClient + + void "test request attribute binding from a filter"() { + given: + BlockingHttpClient client = rxClient.toBlocking() + + expect: + client.retrieve("/attribute/filter/implicit") == "Sally" + client.retrieve("/attribute/filter/implicit/nonnull") == "Sally" + client.retrieve("/attribute/filter/annotation") == "Sally" + client.retrieve("/attribute/filter/annotation/nonnull") == "Sally" + + when: + client.retrieve("/attribute/filter/implicit?foo=false") + + then: + def ex = thrown(HttpClientResponseException) + ex.status == HttpStatus.NOT_FOUND + + when: + client.retrieve("/attribute/filter/implicit/nonnull?foo=false") + + then: + ex = thrown(HttpClientResponseException) + ex.status == HttpStatus.BAD_REQUEST + + when: + client.retrieve("/attribute/filter/annotation?foo=false") + + then: + ex = thrown(HttpClientResponseException) + ex.status == HttpStatus.NOT_FOUND + + when: + client.retrieve("/attribute/filter/annotation/nonnull?foo=false") + + then: + ex = thrown(HttpClientResponseException) + ex.status == HttpStatus.BAD_REQUEST + } + + static class Foo { + String name + } + + @Requires(property = "spec.name", value = "RequestAttributeBindingSpec") + @Controller("/attribute") + static class MyController { + + @Get("/filter/implicit") + String implicit(@Nullable Foo foo) { + return foo?.name + } + + @Get("/filter/implicit/nonnull") + String implicitNotNull(Foo foo) { + return foo?.name + } + + @Get("/filter/annotation") + String annotation(@RequestAttribute @Nullable Foo foo) { + return foo?.name + } + + @Get("/filter/annotation/nonnull") + String annotationNotNull(@RequestAttribute Foo foo) { + return foo?.name + } + } + + @Requires(property = "spec.name", value = "RequestAttributeBindingSpec") + @Filter("/**") + static class MyFilter implements HttpServerFilter { + @Override + Publisher> doFilter(HttpRequest request, ServerFilterChain chain) { + if (!request.getParameters().contains("foo")) { + chain.proceed(request.setAttribute("foo", new Foo(name: "Sally"))) + } else { + chain.proceed(request) + } + } + } +} diff --git a/http-client-jdk/src/test/groovy/io/micronaut/http/client/jdk/RequestAttributeSpec.groovy b/http-client-jdk/src/test/groovy/io/micronaut/http/client/jdk/RequestAttributeSpec.groovy new file mode 100644 index 00000000000..08e76acf6d8 --- /dev/null +++ b/http-client-jdk/src/test/groovy/io/micronaut/http/client/jdk/RequestAttributeSpec.groovy @@ -0,0 +1,94 @@ +package io.micronaut.http.client.jdk + +import io.micronaut.context.annotation.Property +import io.micronaut.context.annotation.Requires +import io.micronaut.http.HttpRequest +import io.micronaut.http.MutableHttpResponse +import io.micronaut.http.annotation.Controller +import io.micronaut.http.annotation.Filter +import io.micronaut.http.annotation.Get +import io.micronaut.http.annotation.RequestAttribute +import io.micronaut.http.client.annotation.Client +import io.micronaut.http.filter.HttpServerFilter +import io.micronaut.http.filter.ServerFilterChain +import io.micronaut.test.extensions.spock.annotation.MicronautTest +import jakarta.inject.Inject +import org.reactivestreams.Publisher +import spock.lang.Specification + +@MicronautTest +@Property(name = 'spec.name', value = 'RequestAttributeSpec') +class RequestAttributeSpec extends Specification { + + @Inject + StoryClient storyClient + + @Inject + ReceiveClient receiveClient + + void "test send and receive request attribute"() { + given: + Story story = storyClient.get() + + expect: + story.storyId == "ABC123" + story.title == "The Hungry Caterpillar" + } + + def 'request attributes should not be forwarded'() { + when: + def uri = receiveClient.get('foo') + then: + uri.path == '/request_attribute_not_forwarded' + uri.query == null + } + + @Client('/request_attribute') + @Requires(property = 'spec.name', value = 'RequestAttributeSpec') + static interface StoryClient { + @Get('/story') + Story get() + } + + @Controller('/request_attribute') + @Requires(property = 'spec.name', value = 'RequestAttributeSpec') + static class StoryController { + + @Get("/story") + Story get(@RequestAttribute("x-story-id") Object storyId) { + return new Story(storyId: storyId, title: 'The Hungry Caterpillar') + } + } + + static class Story { + String storyId + String title + } + + @Filter("/**") + @Requires(property = 'spec.name', value = 'RequestAttributeSpec') + static class ServerFilter implements HttpServerFilter { + + @Override + Publisher> doFilter(HttpRequest request, ServerFilterChain chain) { + request.setAttribute("x-story-id", "ABC123") + return chain.proceed(request) + } + } + + @Client('/request_attribute_not_forwarded') + @Requires(property = 'spec.name', value = 'RequestAttributeSpec') + static interface ReceiveClient { + @Get + URI get(@RequestAttribute("example") String value) + } + + @Controller('/request_attribute_not_forwarded') + @Requires(property = 'spec.name', value = 'RequestAttributeSpec') + static class ReceiveController { + @Get + URI get(HttpRequest request) { + return request.uri + } + } +} diff --git a/http-client-jdk/src/test/groovy/io/micronaut/http/client/jdk/SslSelfSignedSpec.groovy b/http-client-jdk/src/test/groovy/io/micronaut/http/client/jdk/SslSelfSignedSpec.groovy new file mode 100644 index 00000000000..645fe510a3e --- /dev/null +++ b/http-client-jdk/src/test/groovy/io/micronaut/http/client/jdk/SslSelfSignedSpec.groovy @@ -0,0 +1,68 @@ +package io.micronaut.http.client.jdk + +import io.micronaut.context.ApplicationContext +import io.micronaut.context.annotation.Requires +import io.micronaut.context.env.Environment +import io.micronaut.core.io.socket.SocketUtils +import io.micronaut.http.HttpRequest +import io.micronaut.http.HttpResponse +import io.micronaut.http.annotation.Controller +import io.micronaut.http.annotation.Get +import io.micronaut.http.client.HttpClient +import io.micronaut.runtime.server.EmbeddedServer +import reactor.core.publisher.Flux +import spock.lang.Shared +import spock.lang.Specification + +class SslSelfSignedSpec extends Specification { + + @Shared + String host = Optional.ofNullable(System.getenv(Environment.HOSTNAME)).orElse(SocketUtils.LOCALHOST) + + ApplicationContext context + EmbeddedServer embeddedServer + HttpClient client + + void setup() { + context = ApplicationContext.run([ + 'spec.name': 'SslSelfSignedSpec', + 'micronaut.ssl.enabled': true, + 'micronaut.server.ssl.buildSelfSigned': true, + 'micronaut.server.ssl.port': -1, + 'micronaut.http.client.ssl.insecure-trust-all-certificates': true, + ]) + embeddedServer = context.getBean(EmbeddedServer).start() + client = context.createBean(HttpClient, embeddedServer.getURL()) + } + + void cleanup() { + client.close() + context.close() + } + + void "expect the url to be https"() { + expect: + embeddedServer.getURL().toString().startsWith("https://${host}:") + } + + void "test send https request"() { + when: + Flux> flowable = Flux.from(client.exchange( + HttpRequest.GET("/ssl"), String + )) + HttpResponse response = flowable.blockFirst() + + then: + response.body() == "Hello" + } + + @Requires(property = 'spec.name', value = 'SslSelfSignedSpec') + @Controller('/') + static class SslSelfSignedController { + + @Get('/ssl') + String simple() { + return "Hello" + } + } +} diff --git a/http-client-jdk/src/test/groovy/io/micronaut/http/client/jdk/SslSpec.groovy b/http-client-jdk/src/test/groovy/io/micronaut/http/client/jdk/SslSpec.groovy new file mode 100644 index 00000000000..de8bcaad13e --- /dev/null +++ b/http-client-jdk/src/test/groovy/io/micronaut/http/client/jdk/SslSpec.groovy @@ -0,0 +1,106 @@ +package io.micronaut.http.client.jdk + +import io.micronaut.http.client.DefaultHttpClientConfiguration +import io.micronaut.http.client.HttpClient +import io.micronaut.http.ssl.ClientSslConfiguration +import spock.lang.PendingFeature +import spock.lang.Specification + +import javax.net.ssl.SSLHandshakeException +import java.security.GeneralSecurityException + +// See http-client/src/test/groovy/io/micronaut/http/client/SslSpec.groovy +class SslSpec extends Specification { + + void 'bad server ssl cert'() { + given: + def client = HttpClient.create(new URL(url)) + + when: + client.toBlocking().exchange('/') + + then: + def e = thrown RuntimeException + e.printStackTrace() + e.cause instanceof GeneralSecurityException || e.cause instanceof SSLHandshakeException + + cleanup: + client.stop() + + where: + url << [ + 'https://expired.badssl.com/', + 'https://wrong.host.badssl.com/', + 'https://self-signed.badssl.com/', + 'https://untrusted-root.badssl.com/', + 'https://revoked.badssl.com/', + //'https://pinning-test.badssl.com/', // not implemented + 'https://no-subject.badssl.com/', + 'https://reversed-chain.badssl.com/', + 'https://rc4-md5.badssl.com/', + 'https://rc4.badssl.com/', + 'https://3des.badssl.com/', + 'https://null.badssl.com/', + 'https://dh480.badssl.com/', + 'https://dh512.badssl.com/', + // 'https://dh1024.badssl.com/', // passes + // 'https://dh-small-subgroup.badssl.com/', // passes + // 'https://dh-composite.badssl.com/', // times out + ] + } + + @PendingFeature + void 'bad server ssl cert currently unsupported'() { + given: + def client = HttpClient.create(new URL(url)) + + when: + client.toBlocking().exchange('/') + + then: + def e = thrown RuntimeException + e.printStackTrace() + e.cause instanceof GeneralSecurityException || e.cause instanceof SSLHandshakeException + + cleanup: + client.stop() + + where: + url << [ + 'https://pinning-test.badssl.com/', // not implemented + 'https://dh1024.badssl.com/', // passes + 'https://dh-small-subgroup.badssl.com/', // passes + 'https://dh-composite.badssl.com/', // times out + ] + } + + void 'self-signed allowed with config'() { + given: + def cfg = new DefaultHttpClientConfiguration() + ((ClientSslConfiguration) cfg.getSslConfiguration()).setInsecureTrustAllCertificates(true) + def client = HttpClient.create(new URL('https://self-signed.badssl.com/'), cfg) + + when: + client.toBlocking().exchange('/') + + then: + noExceptionThrown() + + cleanup: + client.stop() + } + + void 'normal ssl host allowed'() { + given: + def client = HttpClient.create(new URL('https://www.google.com/')) + + when: + client.toBlocking().exchange('/') + + then: + noExceptionThrown() + + cleanup: + client.stop() + } +} diff --git a/http-client-jdk/src/test/resources/logback.xml b/http-client-jdk/src/test/resources/logback.xml new file mode 100644 index 00000000000..962af34177d --- /dev/null +++ b/http-client-jdk/src/test/resources/logback.xml @@ -0,0 +1,17 @@ + + + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + + + + diff --git a/http-client-jdk/src/test/resources/squid.conf b/http-client-jdk/src/test/resources/squid.conf new file mode 100644 index 00000000000..8181d109513 --- /dev/null +++ b/http-client-jdk/src/test/resources/squid.conf @@ -0,0 +1,2 @@ +http_port 3128 +http_access allow all diff --git a/http-client-tck/build.gradle.kts b/http-client-tck/build.gradle.kts new file mode 100644 index 00000000000..5d9ce3317ee --- /dev/null +++ b/http-client-tck/build.gradle.kts @@ -0,0 +1,15 @@ +plugins { + id("io.micronaut.build.internal.convention-library") +} +dependencies { + annotationProcessor(project(":inject-java")) + api(libs.junit.jupiter) + api(projects.httpTck) + implementation(libs.managed.reactor) + implementation(project(":context")) + implementation(project(":http-server-netty")) + implementation(project(":http-client-core")) +} +tasks.named("test") { + useJUnitPlatform() +} diff --git a/http-client-tck/src/main/java/io/micronaut/http/client/tck/tests/AuthTest.java b/http-client-tck/src/main/java/io/micronaut/http/client/tck/tests/AuthTest.java new file mode 100644 index 00000000000..6d49a693c69 --- /dev/null +++ b/http-client-tck/src/main/java/io/micronaut/http/client/tck/tests/AuthTest.java @@ -0,0 +1,61 @@ +/* + * Copyright 2017-2023 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.http.client.tck.tests; + +import io.micronaut.context.annotation.Requires; +import io.micronaut.http.BasicAuth; +import io.micronaut.http.HttpRequest; +import io.micronaut.http.HttpStatus; +import io.micronaut.http.annotation.Controller; +import io.micronaut.http.annotation.Get; +import io.micronaut.http.tck.AssertionUtils; +import io.micronaut.http.tck.HttpResponseAssertion; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import java.io.IOException; +import java.util.Map; + +import static io.micronaut.http.tck.ServerUnderTest.BLOCKING_CLIENT_PROPERTY; +import static io.micronaut.http.tck.TestScenario.asserts; + +class AuthTest { + + private static final String SPEC_NAME = "AuthTest"; + + @ParameterizedTest(name = "blocking={0}") + @ValueSource(booleans = {true, false}) + void authTest(boolean blocking) throws IOException { + asserts(SPEC_NAME, + Map.of(BLOCKING_CLIENT_PROPERTY, blocking), + HttpRequest.GET("/auth-test").basicAuth("Tim", "Yates"), + (server, request) -> AssertionUtils.assertDoesNotThrow(server, request, + HttpResponseAssertion.builder() + .status(HttpStatus.OK) + .body("Tim:Yates") + .build())); + } + + @Controller("/auth-test") + @Requires(property = "spec.name", value = SPEC_NAME) + static class AuthController { + + @Get + String get(BasicAuth auth) { + return auth.getUsername() + ":" + auth.getPassword(); + } + } +} diff --git a/http-client-tck/src/main/java/io/micronaut/http/client/tck/tests/CookieTest.java b/http-client-tck/src/main/java/io/micronaut/http/client/tck/tests/CookieTest.java new file mode 100644 index 00000000000..cd3ede51211 --- /dev/null +++ b/http-client-tck/src/main/java/io/micronaut/http/client/tck/tests/CookieTest.java @@ -0,0 +1,111 @@ +/* + * Copyright 2017-2023 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.http.client.tck.tests; + +import io.micronaut.context.annotation.Requires; +import io.micronaut.core.util.CollectionUtils; +import io.micronaut.http.HttpRequest; +import io.micronaut.http.HttpStatus; +import io.micronaut.http.annotation.Controller; +import io.micronaut.http.annotation.CookieValue; +import io.micronaut.http.annotation.Get; +import io.micronaut.http.cookie.Cookie; +import io.micronaut.http.tck.AssertionUtils; +import io.micronaut.http.tck.BodyAssertion; +import io.micronaut.http.tck.HttpResponseAssertion; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +import static io.micronaut.http.tck.ServerUnderTest.BLOCKING_CLIENT_PROPERTY; +import static io.micronaut.http.tck.TestScenario.asserts; + +class CookieTest { + + private static final String SPEC_NAME = "CookieTest"; + + @ParameterizedTest(name = "blocking={0}") + @ValueSource(booleans = {true, false}) + void cookieBinding(boolean blocking) throws IOException { + asserts(SPEC_NAME, + Map.of(BLOCKING_CLIENT_PROPERTY, blocking), + HttpRequest.GET("/cookies-test/bind") + .cookie(Cookie.of("one", "foo")) + .cookie(Cookie.of("two", "bar")), + (server, request) -> AssertionUtils.assertDoesNotThrow(server, request, + HttpResponseAssertion.builder() + .status(HttpStatus.OK) + .body(BodyAssertion.builder().body("{\"one\":\"foo\",\"two\":\"bar\"}").equals()) + .build()) + ); + } + + @ParameterizedTest(name = "blocking={0}") + @ValueSource(booleans = {true, false}) + void getCookiesFromRequest(boolean blocking) throws IOException { + asserts(SPEC_NAME, + Map.of(BLOCKING_CLIENT_PROPERTY, blocking), + HttpRequest.GET("/cookies-test/all") + .cookie(Cookie.of("one", "foo")) + .cookie(Cookie.of("two", "bar")), + (server, request) -> AssertionUtils.assertDoesNotThrow(server, request, + HttpResponseAssertion.builder() + .status(HttpStatus.OK) + .body(BodyAssertion.builder().body("{\"one\":\"foo\",\"two\":\"bar\"}").equals()) + .build()) + ); + } + + @ParameterizedTest(name = "blocking={0}") + @ValueSource(booleans = {true, false}) + void testNoCookies(boolean blocking) throws IOException { + asserts(SPEC_NAME, + Map.of(BLOCKING_CLIENT_PROPERTY, blocking), + HttpRequest.GET("/cookies-test/all"), + (server, request) -> AssertionUtils.assertDoesNotThrow(server, request, + HttpResponseAssertion.builder() + .status(HttpStatus.OK) + .body(BodyAssertion.builder().body("{}").equals()) + .build()) + ); + } + + + @Controller("/cookies-test") + @Requires(property = "spec.name", value = SPEC_NAME) + static class CookieController { + + @Get(uri = "/all") + Map all(HttpRequest request) { + Map map = new HashMap<>(); + for (String cookieName : request.getCookies().names()) { + map.put(cookieName, request.getCookies().get(cookieName).getValue()); + } + return map; + } + + @Get(uri = "/bind") + Map all(@CookieValue String one, @CookieValue String two) { + return CollectionUtils.mapOf( + "one", one, + "two", two + ); + } + } +} diff --git a/http-client-tck/src/main/java/io/micronaut/http/client/tck/tests/DontFollowRedirectsTest.java b/http-client-tck/src/main/java/io/micronaut/http/client/tck/tests/DontFollowRedirectsTest.java new file mode 100644 index 00000000000..4b99a1814b8 --- /dev/null +++ b/http-client-tck/src/main/java/io/micronaut/http/client/tck/tests/DontFollowRedirectsTest.java @@ -0,0 +1,72 @@ +/* + * Copyright 2017-2023 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.http.client.tck.tests; + +import io.micronaut.context.annotation.Requires; +import io.micronaut.core.util.StringUtils; +import io.micronaut.http.HttpRequest; +import io.micronaut.http.HttpResponse; +import io.micronaut.http.HttpStatus; +import io.micronaut.http.annotation.Controller; +import io.micronaut.http.annotation.Get; +import io.micronaut.http.tck.AssertionUtils; +import io.micronaut.http.tck.HttpResponseAssertion; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import java.io.IOException; +import java.net.URI; +import java.util.Map; + +import static io.micronaut.http.tck.ServerUnderTest.BLOCKING_CLIENT_PROPERTY; +import static io.micronaut.http.tck.TestScenario.asserts; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +class DontFollowRedirectsTest { + + private static final String SPEC_NAME = "DisableRedirectTest"; + + @ParameterizedTest(name = "blocking={0}") + @ValueSource(booleans = {true, false}) + void dontFollowRedirects(boolean blocking) throws IOException { + asserts(SPEC_NAME, + Map.of( + "micronaut.http.client.follow-redirects", StringUtils.FALSE, + BLOCKING_CLIENT_PROPERTY, blocking + ), + HttpRequest.GET("/redirect/redirect"), + (server, request) -> AssertionUtils.assertDoesNotThrow(server, request, HttpResponseAssertion.builder() + .status(HttpStatus.SEE_OTHER) + .assertResponse(response -> { + assertNotNull(response.getHeaders().get("Location")); + assertEquals("/redirect/direct", response.getHeaders().get("Location")); + }) + .build())); + } + + @Requires(property = "spec.name", value = SPEC_NAME) + @Controller("/redirect") + @SuppressWarnings("checkstyle:MissingJavadocType") + static class RedirectTestController { + + @Get("/redirect") + HttpResponse redirect() { + return HttpResponse.seeOther(URI.create("/redirect/direct")); + } + } + +} diff --git a/http-client-tck/src/main/java/io/micronaut/http/client/tck/tests/ExceptionOnErrorStatusTest.java b/http-client-tck/src/main/java/io/micronaut/http/client/tck/tests/ExceptionOnErrorStatusTest.java new file mode 100644 index 00000000000..d0dabb58fea --- /dev/null +++ b/http-client-tck/src/main/java/io/micronaut/http/client/tck/tests/ExceptionOnErrorStatusTest.java @@ -0,0 +1,65 @@ +/* + * Copyright 2017-2023 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.http.client.tck.tests; + +import io.micronaut.context.annotation.Requires; +import io.micronaut.core.util.StringUtils; +import io.micronaut.http.HttpRequest; +import io.micronaut.http.HttpResponse; +import io.micronaut.http.HttpStatus; +import io.micronaut.http.annotation.Controller; +import io.micronaut.http.annotation.Get; +import io.micronaut.http.tck.AssertionUtils; +import io.micronaut.http.tck.HttpResponseAssertion; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import java.io.IOException; +import java.util.Map; + +import static io.micronaut.http.tck.ServerUnderTest.BLOCKING_CLIENT_PROPERTY; +import static io.micronaut.http.tck.TestScenario.asserts; + +class ExceptionOnErrorStatusTest { + + private static final String SPEC_NAME = "ExceptionOnErrorStatusTest"; + + @ParameterizedTest(name = "blocking={0}") + @ValueSource(booleans = {true, false}) + void exceptionOnErrorStatus(boolean blocking) throws IOException { + asserts(SPEC_NAME, + Map.of( + "micronaut.http.client.exception-on-error-status", StringUtils.FALSE, + BLOCKING_CLIENT_PROPERTY, blocking + ), + HttpRequest.GET("/unprocessable"), + (server, request) -> AssertionUtils.assertDoesNotThrow(server, request, HttpResponseAssertion.builder() + .status(HttpStatus.UNPROCESSABLE_ENTITY) + .body("{\"message\":\"Cannot make it\"}") + .build())); + } + + @Requires(property = "spec.name", value = SPEC_NAME) + @Controller("/unprocessable") + @SuppressWarnings("checkstyle:MissingJavadocType") + static class RedirectTestController { + + @Get + HttpResponse index() { + return HttpResponse.unprocessableEntity().body("{\"message\":\"Cannot make it\"}"); + } + } +} diff --git a/http-client-tck/src/main/java/io/micronaut/http/client/tck/tests/HttpMethodDeleteTest.java b/http-client-tck/src/main/java/io/micronaut/http/client/tck/tests/HttpMethodDeleteTest.java new file mode 100644 index 00000000000..3ec5c40503f --- /dev/null +++ b/http-client-tck/src/main/java/io/micronaut/http/client/tck/tests/HttpMethodDeleteTest.java @@ -0,0 +1,130 @@ +/* + * Copyright 2017-2023 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.http.client.tck.tests; + +import io.micronaut.context.annotation.Requires; +import io.micronaut.http.HttpRequest; +import io.micronaut.http.HttpResponse; +import io.micronaut.http.HttpStatus; +import io.micronaut.http.annotation.Controller; +import io.micronaut.http.annotation.Delete; +import io.micronaut.http.annotation.Status; +import io.micronaut.http.client.annotation.Client; +import io.micronaut.http.tck.AssertionUtils; +import io.micronaut.http.tck.BodyAssertion; +import io.micronaut.http.tck.HttpResponseAssertion; +import io.micronaut.http.tck.ServerUnderTest; +import io.micronaut.http.tck.ServerUnderTestProviderUtils; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import java.io.IOException; +import java.util.Map; + +import static io.micronaut.http.tck.ServerUnderTest.BLOCKING_CLIENT_PROPERTY; +import static io.micronaut.http.tck.TestScenario.asserts; +import static org.junit.jupiter.api.Assertions.assertEquals; + +class HttpMethodDeleteTest { + + private static final String SPEC_NAME = "HttpMethodDeleteTest"; + + @ParameterizedTest(name = "blocking={0}") + @ValueSource(booleans = {true, false}) + void deleteMethodMapping(boolean blocking) throws IOException { + asserts(SPEC_NAME, + Map.of(BLOCKING_CLIENT_PROPERTY, blocking), + HttpRequest.DELETE("/delete"), (server, request) -> + AssertionUtils.assertDoesNotThrow(server, request, + HttpResponseAssertion.builder() + .status(HttpStatus.NO_CONTENT) + .build()) + ); + } + + @Test + void deleteMethodClientMappingWithStringResponse() throws IOException { + try (ServerUnderTest server = ServerUnderTestProviderUtils.getServerUnderTestProvider().getServer(SPEC_NAME)) { + HttpMethodDeleteClient client = server.getApplicationContext().getBean(HttpMethodDeleteClient.class); + assertEquals("ok", client.response()); + } + } + + @ParameterizedTest(name = "blocking={0}") + @ValueSource(booleans = {true, false}) + void deleteMethodMappingWithStringResponse(boolean blocking) throws IOException { + asserts(SPEC_NAME, + Map.of(BLOCKING_CLIENT_PROPERTY, blocking), + HttpRequest.DELETE("/delete/string-response"), + (server, request) -> + AssertionUtils.assertDoesNotThrow(server, request, + HttpResponseAssertion.builder() + .status(HttpStatus.OK) + .body("ok") + .build()) + ); + } + + @ParameterizedTest(name = "blocking={0}") + @ValueSource(booleans = {true, false}) + void deleteMethodMappingWithObjectResponse(boolean blocking) throws IOException { + asserts(SPEC_NAME, + Map.of(BLOCKING_CLIENT_PROPERTY, blocking), + HttpRequest.DELETE("/delete/object-response"), + (server, request) -> + AssertionUtils.assertDoesNotThrow(server, request, + HttpResponseAssertion.builder() + .status(HttpStatus.OK) + .body(BodyAssertion.builder().body("{\"name\":\"Tim\",\"age\":49}").equals()) + .build()) + ); + } + + @Requires(property = "spec.name", value = SPEC_NAME) + @Controller("/delete") + static class HttpMethodDeleteTestController { + + @Delete + @Status(HttpStatus.NO_CONTENT) + void index() { + // no-op + } + + @Delete("/string-response") + String response() { + return "ok"; + } + + @Delete("/object-response") + Person person() { + return new Person("Tim", 49); + } + } + + @Requires(property = "spec.name", value = SPEC_NAME) + @Client("/delete") + interface HttpMethodDeleteClient { + + HttpResponse index(); + + @Delete("/string-response") + String response(); + + @Delete("/object-response") + Person person(); + } +} diff --git a/http-client-tck/src/main/java/io/micronaut/http/client/tck/tests/HttpMethodPostTest.java b/http-client-tck/src/main/java/io/micronaut/http/client/tck/tests/HttpMethodPostTest.java new file mode 100644 index 00000000000..b196e666787 --- /dev/null +++ b/http-client-tck/src/main/java/io/micronaut/http/client/tck/tests/HttpMethodPostTest.java @@ -0,0 +1,67 @@ +/* + * Copyright 2017-2023 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.http.client.tck.tests; + +import io.micronaut.context.annotation.Requires; +import io.micronaut.http.HttpRequest; +import io.micronaut.http.HttpStatus; +import io.micronaut.http.annotation.Controller; +import io.micronaut.http.annotation.Post; +import io.micronaut.http.tck.AssertionUtils; +import io.micronaut.http.tck.HttpResponseAssertion; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import java.io.IOException; +import java.util.Map; + +import static io.micronaut.http.tck.ServerUnderTest.BLOCKING_CLIENT_PROPERTY; +import static io.micronaut.http.tck.TestScenario.asserts; + +class HttpMethodPostTest { + + private static final String SPEC_NAME = "HttpMethodPostTest"; + + @ParameterizedTest(name = "blocking={0}") + @ValueSource(booleans = {true, false}) + void postBody(boolean blocking) throws IOException { + asserts(SPEC_NAME, + Map.of(BLOCKING_CLIENT_PROPERTY, blocking), + HttpRequest.POST("/post/object-body", new Person("Tim", 49)), + (server, request) -> + AssertionUtils.assertDoesNotThrow(server, request, + HttpResponseAssertion.builder() + .status(HttpStatus.OK) + .body("Tim:49") + .build()) + ); + } + + @Requires(property = "spec.name", value = SPEC_NAME) + @Controller("/post") + static class HttpMethodPostTestController { + + @Post() + String response() { + return "ok"; + } + + @Post("/object-body") + String person(Person person) { + return person.getName() + ":" + person.getAge(); + } + } +} diff --git a/http-client-tck/src/main/java/io/micronaut/http/client/tck/tests/Person.java b/http-client-tck/src/main/java/io/micronaut/http/client/tck/tests/Person.java new file mode 100644 index 00000000000..342aadf0b68 --- /dev/null +++ b/http-client-tck/src/main/java/io/micronaut/http/client/tck/tests/Person.java @@ -0,0 +1,60 @@ +/* + * Copyright 2017-2023 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.http.client.tck.tests; + +import io.micronaut.core.annotation.Introspected; +import io.micronaut.core.annotation.NonNull; + +import java.util.Objects; + +@Introspected +class Person { + + @NonNull + private final String name; + + private final int age; + + Person(String name, int age) { + this.name = name; + this.age = age; + } + + public String getName() { + return name; + } + + public int getAge() { + return age; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Person person = (Person) o; + return age == person.age && Objects.equals(name, person.name); + } + + @Override + public int hashCode() { + return Objects.hash(name, age); + } +} diff --git a/http-client-tck/src/main/java/io/micronaut/http/client/tck/tests/RedirectTest.java b/http-client-tck/src/main/java/io/micronaut/http/client/tck/tests/RedirectTest.java new file mode 100644 index 00000000000..5507d7bd365 --- /dev/null +++ b/http-client-tck/src/main/java/io/micronaut/http/client/tck/tests/RedirectTest.java @@ -0,0 +1,249 @@ +/* + * Copyright 2017-2023 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.http.client.tck.tests; + +import io.micronaut.context.annotation.Requires; +import io.micronaut.core.annotation.Nullable; +import io.micronaut.core.async.publisher.Publishers; +import io.micronaut.core.util.StringUtils; +import io.micronaut.discovery.ServiceInstance; +import io.micronaut.http.HttpRequest; +import io.micronaut.http.HttpResponse; +import io.micronaut.http.HttpStatus; +import io.micronaut.http.annotation.Consumes; +import io.micronaut.http.annotation.Controller; +import io.micronaut.http.annotation.Get; +import io.micronaut.http.annotation.Header; +import io.micronaut.http.annotation.Produces; +import io.micronaut.http.client.HttpClient; +import io.micronaut.http.client.LoadBalancer; +import io.micronaut.http.client.annotation.Client; +import io.micronaut.http.tck.AssertionUtils; +import io.micronaut.http.tck.BodyAssertion; +import io.micronaut.http.tck.HttpResponseAssertion; +import io.micronaut.http.tck.ServerUnderTest; +import io.micronaut.http.tck.ServerUnderTestProviderUtils; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.reactivestreams.Publisher; +import reactor.core.publisher.Flux; + +import java.io.IOException; +import java.net.URI; +import java.net.URL; +import java.util.Collections; +import java.util.Map; +import java.util.Optional; + +import static io.micronaut.http.tck.ServerUnderTest.BLOCKING_CLIENT_PROPERTY; +import static io.micronaut.http.tck.TestScenario.asserts; +import static org.junit.jupiter.api.Assertions.assertEquals; + +@SuppressWarnings({ + "java:S2259", // The tests will show if it's null + "java:S5960", // We're allowed assertions, as these are used in tests only + "java:S1192", // It's more readable without the constant +}) +class RedirectTest { + + private static final String SPEC_NAME = "RedirectTest"; + private static final String BODY = "It works"; + private static final BodyAssertion EXPECTED_BODY = BodyAssertion.builder().body(BODY).equals(); + private static final String REDIRECT = "redirect"; + + @ParameterizedTest(name = "blocking={0}") + @ValueSource(booleans = {true, false}) + void absoluteRedirection(boolean blocking) throws IOException { + asserts(SPEC_NAME, + Map.of(BLOCKING_CLIENT_PROPERTY, blocking), + HttpRequest.GET("/redirect/redirect"), + (server, request) -> AssertionUtils.assertDoesNotThrow(server, request, + HttpResponseAssertion.builder() + .status(HttpStatus.OK) + .body(EXPECTED_BODY) + .build()) + ); + } + + @Test + void clientRedirection() throws IOException { + try (ServerUnderTest server = ServerUnderTestProviderUtils.getServerUnderTestProvider().getServer(SPEC_NAME)) { + RedirectClient client = server.getApplicationContext().getBean(RedirectClient.class); + assertEquals(BODY, client.redirect()); + } + } + + @Test + void clientRelativeUriDirect() throws IOException { + try (ServerUnderTest server = ServerUnderTestProviderUtils.getServerUnderTestProvider().getServer(SPEC_NAME); + HttpClient client = server.getApplicationContext().createBean(HttpClient.class, relativeLoadBalancer(server, "/redirect"))) { + var exchange = Flux.from(client.exchange(HttpRequest.GET("direct"), String.class)).blockFirst(); + assertEquals(HttpStatus.OK, exchange.getStatus()); + assertEquals(BODY, exchange.body()); + } + } + + @Test + void blockingClientRelativeUriDirect() throws IOException { + try (ServerUnderTest server = ServerUnderTestProviderUtils.getServerUnderTestProvider().getServer(SPEC_NAME); + HttpClient client = server.getApplicationContext().createBean(HttpClient.class, relativeLoadBalancer(server, "/redirect"))) { + var exchange = client.toBlocking().exchange(HttpRequest.GET("direct"), String.class); + assertEquals(HttpStatus.OK, exchange.getStatus()); + assertEquals(BODY, exchange.body()); + } + } + + @Test + void clientRelativeUriNoSlash() throws IOException { + try (ServerUnderTest server = ServerUnderTestProviderUtils.getServerUnderTestProvider().getServer(SPEC_NAME); + HttpClient client = server.getApplicationContext().createBean(HttpClient.class, relativeLoadBalancer(server, REDIRECT))) { + var exchange = Flux.from(client.exchange(HttpRequest.GET("direct"), String.class)).blockFirst(); + assertEquals(HttpStatus.OK, exchange.getStatus()); + assertEquals(BODY, exchange.body()); + } + } + + @Test + void blockingClientRelativeUriNoSlash() throws IOException { + try (ServerUnderTest server = ServerUnderTestProviderUtils.getServerUnderTestProvider().getServer(SPEC_NAME); + HttpClient client = server.getApplicationContext().createBean(HttpClient.class, relativeLoadBalancer(server, REDIRECT))) { + var exchange = client.toBlocking().exchange(HttpRequest.GET("direct"), String.class); + assertEquals(HttpStatus.OK, exchange.getStatus()); + assertEquals(BODY, exchange.body()); + } + } + + @Test + @SuppressWarnings("java:S3655") + void clientRelativeUriRedirectAbsolute() throws IOException { + try (ServerUnderTest server = ServerUnderTestProviderUtils.getServerUnderTestProvider().getServer(SPEC_NAME); + HttpClient client = server.getApplicationContext().createBean(HttpClient.class, server.getURL().get() + "/redirect")) { + var response = Flux.from(client.exchange(HttpRequest.GET(REDIRECT), String.class)).blockFirst(); + assertEquals(HttpStatus.OK, response.getStatus()); + assertEquals(BODY, response.body()); + } + } + + @Test + @SuppressWarnings("java:S3655") + void blockingClientRelativeUriRedirectAbsolute() throws IOException { + try (ServerUnderTest server = ServerUnderTestProviderUtils.getServerUnderTestProvider().getServer(SPEC_NAME); + HttpClient client = server.getApplicationContext().createBean(HttpClient.class, server.getURL().get() + "/redirect")) { + var response = client.toBlocking().exchange(HttpRequest.GET(REDIRECT), String.class); + assertEquals(HttpStatus.OK, response.getStatus()); + assertEquals(BODY, response.body()); + } + } + + @ParameterizedTest(name = "blocking={0}") + @ValueSource(booleans = {true, false}) + @SuppressWarnings("java:S3655") + void hostHeaderIsCorrectForRedirect(boolean blocking) throws IOException { + try (ServerUnderTest otherServer = ServerUnderTestProviderUtils.getServerUnderTestProvider().getServer(SPEC_NAME, Collections.singletonMap("redirect.server", "true"))) { + int otherPort = otherServer.getPort().get(); + asserts(SPEC_NAME, + Map.of(BLOCKING_CLIENT_PROPERTY, blocking), + HttpRequest.GET("/redirect/redirect-host").header(REDIRECT, "http://localhost:" + otherPort + "/redirect/host-header"), + (server, request) -> AssertionUtils.assertDoesNotThrow(server, request, + HttpResponseAssertion.builder() + .status(HttpStatus.OK) + .body(BodyAssertion.builder().body("localhost:" + otherPort).equals()) + .build()) + ); + } + } + + @Test + @Disabled("not supported, see -- io.micronaut.http.client.ClientRedirectSpec#test - client: full uri, redirect: relative") + void relativeRedirection() throws IOException { + asserts(SPEC_NAME, + HttpRequest.GET("/redirect/redirect-relative"), + (server, request) -> AssertionUtils.assertDoesNotThrow(server, request, + HttpResponseAssertion.builder() + .status(HttpStatus.OK) + .body(EXPECTED_BODY) + .build()) + ); + } + + @SuppressWarnings("java:S3655") + private LoadBalancer relativeLoadBalancer(ServerUnderTest server, String path) { + return new LoadBalancer() { + @Override + public Publisher select(@Nullable Object discriminator) { + URL url = server.getURL().get(); + return Publishers.just(ServiceInstance.of(url.getHost(), url)); + } + + @Override + public Optional getContextPath() { + return Optional.of(path); + } + }; + } + + @Requires(property = "spec.name", value = SPEC_NAME) + @Controller("/redirect") + @SuppressWarnings("checkstyle:MissingJavadocType") + static class RedirectTestController { + + @Get("/redirect") + HttpResponse redirect() { + return HttpResponse.redirect(URI.create("/redirect/direct")); + } + + @Get("/redirect-relative") + HttpResponse redirectRelative() { + return HttpResponse.redirect(URI.create("./direct")); + } + + @Get("/redirect-host") + HttpResponse redirectHost(@Header String redirect) { + return HttpResponse.redirect(URI.create(redirect)); + } + + @Get("/direct") + @Produces("text/plain") + HttpResponse direct() { + return HttpResponse.ok(BODY); + } + } + + @Requires(property = "spec.name", value = SPEC_NAME) + @Requires(property = "redirect.server", value = StringUtils.TRUE) + @Controller("/redirect") + @SuppressWarnings("checkstyle:MissingJavadocType") + static class RedirectHostHeaderController { + + @Get("/host-header") + @Produces("text/plain") + HttpResponse hostHeader(@Header String host) { + return HttpResponse.ok(host); + } + } + + @SuppressWarnings("checkstyle:MissingJavadocType") + @Requires(property = "spec.name", value = SPEC_NAME) + @Client("/redirect") + interface RedirectClient { + + @Get("/redirect") + @Consumes({"text/plain", "application/json"}) + String redirect(); + } +} diff --git a/http-client-tck/src/main/java/io/micronaut/http/client/tck/tests/StatusTest.java b/http-client-tck/src/main/java/io/micronaut/http/client/tck/tests/StatusTest.java new file mode 100644 index 00000000000..366645a56b4 --- /dev/null +++ b/http-client-tck/src/main/java/io/micronaut/http/client/tck/tests/StatusTest.java @@ -0,0 +1,146 @@ +/* + * Copyright 2017-2023 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.http.client.tck.tests; + +import io.micronaut.context.annotation.Requires; +import io.micronaut.http.HttpRequest; +import io.micronaut.http.HttpResponse; +import io.micronaut.http.HttpStatus; +import io.micronaut.http.annotation.Controller; +import io.micronaut.http.annotation.Get; +import io.micronaut.http.annotation.Produces; +import io.micronaut.http.annotation.Status; +import io.micronaut.http.server.exceptions.ExceptionHandler; +import io.micronaut.http.server.exceptions.response.ErrorContext; +import io.micronaut.http.server.exceptions.response.ErrorResponseProcessor; +import io.micronaut.http.tck.AssertionUtils; +import io.micronaut.http.tck.HttpResponseAssertion; +import jakarta.inject.Singleton; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import java.io.IOException; +import java.util.Map; + +import static io.micronaut.http.tck.ServerUnderTest.BLOCKING_CLIENT_PROPERTY; +import static io.micronaut.http.tck.TestScenario.asserts; + +class StatusTest { + + private static final String SPEC_NAME = "StatusTest"; + + @ParameterizedTest(name = "blocking={0}") + @ValueSource(booleans = {true, false}) + void returnStatus(boolean blocking) throws IOException { + asserts(SPEC_NAME, + Map.of(BLOCKING_CLIENT_PROPERTY, blocking), + HttpRequest.GET("/status/http-status"), + (server, request) -> + AssertionUtils.assertThrows(server, request, + HttpResponseAssertion.builder() + .status(HttpStatus.I_AM_A_TEAPOT) + .build()) + ); + } + + @ParameterizedTest(name = "blocking={0}") + @ValueSource(booleans = {true, false}) + void responseStatus(boolean blocking) throws IOException { + asserts(SPEC_NAME, + Map.of(BLOCKING_CLIENT_PROPERTY, blocking), + HttpRequest.GET("/status/response-status"), + (server, request) -> + AssertionUtils.assertThrows(server, request, + HttpResponseAssertion.builder() + .status(HttpStatus.I_AM_A_TEAPOT) + .build()) + ); + } + + @ParameterizedTest(name = "blocking={0}") + @ValueSource(booleans = {true, false}) + void atStatus(boolean blocking) throws IOException { + asserts(SPEC_NAME, + Map.of(BLOCKING_CLIENT_PROPERTY, blocking), + HttpRequest.GET("/status/at-status"), + (server, request) -> + AssertionUtils.assertThrows(server, request, + HttpResponseAssertion.builder() + .status(HttpStatus.I_AM_A_TEAPOT) + .build()) + ); + } + + @ParameterizedTest(name = "blocking={0}") + @ValueSource(booleans = {true, false}) + void exceptionStatus(boolean blocking) throws IOException { + asserts(SPEC_NAME, + Map.of(BLOCKING_CLIENT_PROPERTY, blocking), + HttpRequest.GET("/status/exception-status"), + (server, request) -> + AssertionUtils.assertThrows(server, request, + HttpResponseAssertion.builder() + .status(HttpStatus.I_AM_A_TEAPOT) + .build()) + ); + } + + @Requires(property = "spec.name", value = SPEC_NAME) + @Controller("/status") + static class HttpStatusController { + @Get("/http-status") + HttpStatus status() { + return HttpStatus.I_AM_A_TEAPOT; + } + + @Get("/at-status") + @Status(HttpStatus.I_AM_A_TEAPOT) + void atstatus() { + // Does nothing, just returns a status + } + + @Get("/response-status") + HttpResponse response() { + return HttpResponse.status(HttpStatus.I_AM_A_TEAPOT); + } + + @Get("/exception-status") + HttpResponse exception() { + throw new TeapotException(); + } + } + + static class TeapotException extends RuntimeException { + } + + @Produces + @Singleton + @Requires(property = "spec.name", value = SPEC_NAME) + static class TeapotExceptionHandler implements ExceptionHandler> { + private final ErrorResponseProcessor errorResponseProcessor; + + TeapotExceptionHandler(ErrorResponseProcessor errorResponseProcessor) { + this.errorResponseProcessor = errorResponseProcessor; + } + + @Override + public HttpResponse handle(HttpRequest request, TeapotException e) { + return errorResponseProcessor.processResponse(ErrorContext.builder(request) + .cause(e) + .build(), HttpResponse.status(HttpStatus.I_AM_A_TEAPOT)); + } + } +} diff --git a/http-client/src/main/java/io/micronaut/http/client/netty/ConnectionManager.java b/http-client/src/main/java/io/micronaut/http/client/netty/ConnectionManager.java index 41957a50319..73464086430 100644 --- a/http-client/src/main/java/io/micronaut/http/client/netty/ConnectionManager.java +++ b/http-client/src/main/java/io/micronaut/http/client/netty/ConnectionManager.java @@ -24,6 +24,7 @@ import io.micronaut.http.client.HttpClientConfiguration; import io.micronaut.http.client.HttpVersionSelection; import io.micronaut.http.client.exceptions.HttpClientException; +import io.micronaut.http.client.exceptions.HttpClientExceptionUtils; import io.micronaut.http.client.netty.ssl.NettyClientSslBuilder; import io.micronaut.http.netty.channel.ChannelPipelineCustomizer; import io.micronaut.http.netty.channel.NettyThreadFactory; @@ -357,7 +358,7 @@ private SslContext buildSslContext(DefaultHttpClient.RequestKey requestKey) { sslCtx = sslContext; //Allow https requests to be sent if SSL is disabled but a proxy is present if (sslCtx == null && !configuration.getProxyAddress().isPresent()) { - throw customizeException(new HttpClientException("Cannot send HTTPS request. SSL is disabled")); + throw decorate(new HttpClientException("Cannot send HTTPS request. SSL is disabled")); } } else { sslCtx = null; @@ -485,11 +486,6 @@ > void addInstrumentedListener( }); } - private E customizeException(E exc) { - DefaultHttpClient.customizeException0(configuration, informationalServiceId, exc); - return exc; - } - private Http2FrameCodec makeFrameCodec() { Http2FrameCodecBuilder builder = Http2FrameCodecBuilder.forClient(); configuration.getLogLevel().ifPresent(logLevel -> { @@ -498,7 +494,7 @@ private Http2FrameCodec makeFrameCodec() { io.netty.handler.logging.LogLevel.valueOf(logLevel.name()); builder.frameLogger(new Http2FrameLogger(nettyLevel, DefaultHttpClient.class)); } catch (IllegalArgumentException e) { - throw customizeException(new HttpClientException("Unsupported log level: " + logLevel)); + throw decorate(new HttpClientException("Unsupported log level: " + logLevel)); } }); return builder.build(); @@ -533,7 +529,7 @@ private void addLogHandler(Channel ch) { io.netty.handler.logging.LogLevel.valueOf(logLevel.name()); ch.pipeline().addLast(new LoggingHandler(DefaultHttpClient.class, nettyLevel)); } catch (IllegalArgumentException e) { - throw customizeException(new HttpClientException("Unsupported log level: " + logLevel)); + throw decorate(new HttpClientException("Unsupported log level: " + logLevel)); } }); } @@ -598,6 +594,10 @@ public void channelRead(@NonNull ChannelHandlerContext ctx, @NonNull Object msg) }); } + private E decorate(E exc) { + return HttpClientExceptionUtils.populateServiceId(exc, informationalServiceId, configuration); + } + /** * Initializer for TLS channels. After ALPN we will proceed either with * {@link #initHttp1(Channel)} or {@link #initHttp2(Pool, Channel, NettyClientCustomizer)}. @@ -645,7 +645,7 @@ protected void configurePipeline(ChannelHandlerContext ctx, String protocol) { ctx.pipeline().remove(ChannelPipelineCustomizer.HANDLER_INITIAL_ERROR); } else { ctx.close(); - throw customizeException(new HttpClientException("Unknown Protocol: " + protocol)); + throw decorate(new HttpClientException("Unknown Protocol: " + protocol)); } } @@ -835,7 +835,7 @@ void onNewConnectionFailure(@Nullable Throwable error) throws Exception { } else { wrapped = new HttpClientException("Connect Error: " + error.getMessage(), error); } - if (pending.tryEmitError(customizeException(wrapped)) == Sinks.EmitResult.OK) { + if (pending.tryEmitError(decorate(wrapped)) == Sinks.EmitResult.OK) { // no need to log return; } diff --git a/http-client/src/main/java/io/micronaut/http/client/netty/DefaultHttpClient.java b/http-client/src/main/java/io/micronaut/http/client/netty/DefaultHttpClient.java index ed41f45b41a..c051abacfd3 100644 --- a/http-client/src/main/java/io/micronaut/http/client/netty/DefaultHttpClient.java +++ b/http-client/src/main/java/io/micronaut/http/client/netty/DefaultHttpClient.java @@ -55,11 +55,11 @@ import io.micronaut.http.client.LoadBalancer; import io.micronaut.http.client.ProxyHttpClient; import io.micronaut.http.client.ProxyRequestOptions; -import io.micronaut.http.client.ServiceHttpClientConfiguration; import io.micronaut.http.client.StreamingHttpClient; import io.micronaut.http.client.exceptions.ContentLengthExceededException; import io.micronaut.http.client.exceptions.HttpClientErrorDecoder; import io.micronaut.http.client.exceptions.HttpClientException; +import io.micronaut.http.client.exceptions.HttpClientExceptionUtils; import io.micronaut.http.client.exceptions.HttpClientResponseException; import io.micronaut.http.client.exceptions.NoHostException; import io.micronaut.http.client.exceptions.ReadTimeoutException; @@ -74,6 +74,7 @@ import io.micronaut.http.codec.CodecException; import io.micronaut.http.codec.MediaTypeCodec; import io.micronaut.http.codec.MediaTypeCodecRegistry; +import io.micronaut.http.context.ContextPathUtils; import io.micronaut.http.context.ServerRequestContext; import io.micronaut.http.filter.FilterOrder; import io.micronaut.http.filter.FilterRunner; @@ -525,12 +526,12 @@ public O retrieve(io.micronaut.http.HttpRequest request, Argument body = response.getBody(); if (!body.isPresent() && response.getBody(Argument.of(byte[].class)).isPresent()) { - throw customizeException(new HttpClientResponseException( + throw decorate(new HttpClientResponseException( String.format("Failed to decode the body for the given content type [%s]", response.getContentType().orElse(null)), response )); } else { - return body.orElseThrow(() -> customizeException(new HttpClientResponseException( + return body.orElseThrow(() -> decorate(new HttpClientResponseException( "Empty body", response ))); @@ -667,7 +668,7 @@ public void onError(Throwable t) { if (t instanceof HttpClientException) { emitter.error(t); } else { - emitter.error(customizeException(new HttpClientException("Error consuming Server Sent Events: " + t.getMessage(), t))); + emitter.error(decorate(new HttpClientException("Error consuming Server Sent Events: " + t.getMessage(), t))); } } @@ -804,12 +805,12 @@ public Publisher retrieve(io.micronaut.http.HttpRequest request, } else { Optional body = response.getBody(); if (!body.isPresent() && response.getBody(byte[].class).isPresent()) { - throw customizeException(new HttpClientResponseException( + throw decorate(new HttpClientResponseException( String.format("Failed to decode the body for the given content type [%s]", response.getContentType().orElse(null)), response )); } else { - return body.orElseThrow(() -> customizeException(new HttpClientResponseException( + return body.orElseThrow(() -> decorate(new HttpClientResponseException( "Empty body", response ))); @@ -1217,21 +1218,6 @@ protected Publisher resolveRedirectURI(io.micronaut.http.HttpRequest } } - /** - * @param requestURI The request URI - * @return A URI that is prepended with the contextPath, if set - */ - protected URI prependContextPath(URI requestURI) { - if (StringUtils.isNotEmpty(contextPath)) { - try { - return new URI(StringUtils.prependUri(contextPath, requestURI.toString())); - } catch (URISyntaxException e) { - throw customizeException(new HttpClientException("Failed to construct the request URI", e)); - } - } - return requestURI; - } - /** * @return The discriminator to use when selecting a server for the purposes of load balancing (defaults to null) */ @@ -1415,7 +1401,7 @@ protected NettyRequestWriter buildNettyRequest( } if (bodyContent == null) { bodyContent = conversionService.convert(bodyValue, ByteBuf.class).orElseThrow(() -> - customizeException(new HttpClientException("Body [" + bodyValue + "] cannot be encoded to content type [" + requestContentType + "]. No possible codecs or converters found.")) + decorate(new HttpClientException("Body [" + bodyValue + "] cannot be encoded to content type [" + requestContentType + "]. No possible codecs or converters found.")) ); } } @@ -1474,7 +1460,7 @@ public void onComplete() { FullHttpResponse fullHttpResponse = new DefaultFullHttpResponse(nettyResponse.protocolVersion(), nettyResponse.status(), buffer, nettyResponse.headers(), new DefaultHttpHeaders(true)); final FullNettyClientHttpResponse fullNettyClientHttpResponse = new FullNettyClientHttpResponse<>(fullHttpResponse, mediaTypeCodecRegistry, byteBufferFactory, (Argument) errorType, true, conversionService); fullNettyClientHttpResponse.onComplete(); - emitter.error(customizeException(new HttpClientResponseException( + emitter.error(decorate(new HttpClientResponseException( fullHttpResponse.status().reasonPhrase(), null, fullNettyClientHttpResponse, @@ -1502,7 +1488,7 @@ public Argument getErrorType(MediaType mediaType) { private Publisher resolveURI(io.micronaut.http.HttpRequest request, boolean includeContextPath) { URI requestURI = request.getUri(); if (loadBalancer == null) { - return Flux.error(customizeException(new NoHostException("Request URI specifies no host to connect to"))); + return Flux.error(decorate(new NoHostException("Request URI specifies no host to connect to"))); } return Flux.from(loadBalancer.select(getLoadBalancerDiscriminator())).map(server -> { @@ -1510,7 +1496,12 @@ private Publisher resolveURI(io.micronaut.http.HttpRequest request, if (request instanceof MutableHttpRequest && authInfo.isPresent()) { ((MutableHttpRequest) request).getHeaders().auth(authInfo.get()); } - return server.resolve(includeContextPath ? prependContextPath(requestURI) : requestURI); + + try { + return server.resolve(includeContextPath ? ContextPathUtils.prepend(requestURI, contextPath) : requestURI); + } catch (URISyntaxException e) { + throw decorate(new HttpClientException("Failed to construct the request URI", e)); + } } ); } @@ -1591,7 +1582,7 @@ private > Flux handleStreamHttpError( ) { boolean errorStatus = response.code() >= 400; if (errorStatus && failOnError) { - return Flux.error(customizeException(new HttpClientResponseException(response.reason(), response))); + return Flux.error(decorate(new HttpClientResponseException(response.reason(), response))); } else { return Flux.just(response); } @@ -1850,24 +1841,15 @@ static boolean isSecureScheme(String scheme) { return io.micronaut.http.HttpRequest.SCHEME_HTTPS.equalsIgnoreCase(scheme) || SCHEME_WSS.equalsIgnoreCase(scheme); } - private E customizeException(E exc) { - customizeException0(configuration, informationalServiceId, exc); - return exc; - } - - static void customizeException0(HttpClientConfiguration configuration, String informationalServiceId, HttpClientException exc) { - if (informationalServiceId != null) { - exc.setServiceId(informationalServiceId); - } else if (configuration instanceof ServiceHttpClientConfiguration) { - exc.setServiceId(((ServiceHttpClientConfiguration) configuration).getServiceId()); - } - } - @FunctionalInterface interface ThrowingBiConsumer { void accept(T1 t1, T2 t2) throws Exception; } + private E decorate(E exc) { + return HttpClientExceptionUtils.populateServiceId(exc, informationalServiceId, configuration); + } + /** * Key used for connection pooling and determining host/port. */ @@ -1888,7 +1870,7 @@ public RequestKey(DefaultHttpClient ctx, URI requestURI) { if (host == null) { host = requestURI.getAuthority(); if (host == null) { - throw ctx.customizeException(new NoHostException("URI specifies no host to connect to")); + throw decorate(ctx, new NoHostException("URI specifies no host to connect to")); } final int i = host.indexOf(':'); @@ -1898,7 +1880,7 @@ public RequestKey(DefaultHttpClient ctx, URI requestURI) { try { port = Integer.parseInt(portStr); } catch (NumberFormatException e) { - throw ctx.customizeException(new HttpClientException("URI specifies an invalid port: " + portStr)); + throw decorate(ctx, new HttpClientException("URI specifies an invalid port: " + portStr)); } } else { port = requestURI.getPort() > -1 ? requestURI.getPort() : secure ? DEFAULT_HTTPS_PORT : DEFAULT_HTTP_PORT; @@ -1945,6 +1927,10 @@ public boolean equals(Object o) { public int hashCode() { return ObjectUtils.hash(host, port, secure); } + + private E decorate(DefaultHttpClient ctx, E exc) { + return HttpClientExceptionUtils.populateServiceId(exc, ctx.informationalServiceId, ctx.configuration); + } } /** @@ -2056,11 +2042,11 @@ public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { HttpClientException result; if (cause instanceof TooLongFrameException) { - result = customizeException(new ContentLengthExceededException(configuration.getMaxContentLength())); + result = decorate(new ContentLengthExceededException(configuration.getMaxContentLength())); } else if (cause instanceof io.netty.handler.timeout.ReadTimeoutException) { result = ReadTimeoutException.TIMEOUT_EXCEPTION; } else { - result = customizeException(new HttpClientException("Error occurred reading HTTP response: " + message, cause)); + result = decorate(new HttpClientException("Error occurred reading HTTP response: " + message, cause)); } responsePromise.tryFailure(result); } @@ -2200,8 +2186,7 @@ protected void buildResponse(Promise> promise, FullHttpR msg.headers().remove(HttpHeaderNames.CONTENT_LENGTH); } - boolean convertBodyWithBodyType = msg.status().code() < 400 || - (!DefaultHttpClient.this.configuration.isExceptionOnErrorStatus() && bodyType.equalsType(errorType)); + boolean convertBodyWithBodyType = shouldConvertWithBodyType(msg, DefaultHttpClient.this.configuration, bodyType, errorType); FullNettyClientHttpResponse response = new FullNettyClientHttpResponse<>(msg, mediaTypeCodecRegistry, byteBufferFactory, bodyType, convertBodyWithBodyType, conversionService); @@ -2231,12 +2216,23 @@ protected void buildResponse(Promise> promise, FullHttpR } } + private static boolean shouldConvertWithBodyType(FullHttpResponse msg, + HttpClientConfiguration configuration, + Argument bodyType, + Argument errorType) { + if (msg.status().code() < 400) { + return true; + } + return !configuration.isExceptionOnErrorStatus() && bodyType.equalsType(errorType); + + } + /** * Create a {@link HttpClientResponseException} from a response with a failed HTTP status. */ private HttpClientResponseException makeErrorFromRequestBody(HttpResponseStatus status, FullNettyClientHttpResponse response) { if (errorType != null && errorType != HttpClient.DEFAULT_ERROR_TYPE) { - return customizeException(new HttpClientResponseException( + return decorate(new HttpClientResponseException( status.reasonPhrase(), null, response, @@ -2248,7 +2244,7 @@ public Argument getErrorType(MediaType mediaType) { } )); } else { - return customizeException(new HttpClientResponseException(status.reasonPhrase(), response)); + return decorate(new HttpClientResponseException(status.reasonPhrase(), response)); } } @@ -2266,7 +2262,7 @@ private HttpClientResponseException makeErrorBodyParseError(FullHttpResponse ful ); // this onComplete call disables further parsing by HttpClientResponseException errorResponse.onComplete(); - return customizeException(new HttpClientResponseException( + return decorate(new HttpClientResponseException( "Error decoding HTTP error response body: " + t.getMessage(), t, errorResponse, @@ -2283,7 +2279,7 @@ private void makeNormalBodyParseError(FullHttpResponse fullResponse, Throwable t false, conversionService ); - HttpClientResponseException clientResponseError = customizeException(new HttpClientResponseException( + HttpClientResponseException clientResponseError = decorate(new HttpClientResponseException( "Error decoding HTTP response body: " + t.getMessage(), t, response, diff --git a/http-client/src/main/java/io/micronaut/http/client/netty/NettyClientHttpRequest.java b/http-client/src/main/java/io/micronaut/http/client/netty/NettyClientHttpRequest.java index 95095d4ea1c..7db2b822bc3 100644 --- a/http-client/src/main/java/io/micronaut/http/client/netty/NettyClientHttpRequest.java +++ b/http-client/src/main/java/io/micronaut/http/client/netty/NettyClientHttpRequest.java @@ -65,7 +65,7 @@ * @since 1.0 */ @Internal -class NettyClientHttpRequest implements MutableHttpRequest, NettyHttpRequestBuilder { +public class NettyClientHttpRequest implements MutableHttpRequest, NettyHttpRequestBuilder { static final CharSequence CHANNEL = "netty_channel"; private final NettyHttpHeaders headers = new NettyHttpHeaders(); diff --git a/http-client/src/test/groovy/io/micronaut/http/client/ClientRedirectSpec.groovy b/http-client/src/test/groovy/io/micronaut/http/client/ClientRedirectSpec.groovy index 84cfd0b8d9e..bdd6ce1b1ad 100644 --- a/http-client/src/test/groovy/io/micronaut/http/client/ClientRedirectSpec.groovy +++ b/http-client/src/test/groovy/io/micronaut/http/client/ClientRedirectSpec.groovy @@ -67,8 +67,8 @@ class ClientRedirectSpec extends Specification { when: HttpResponse response = client.toBlocking().exchange('/test/redirect-relative', String) - then: "Client should correctly redirect relatively to /test/direct the same way as " - "# curl localhost:17320/test/redirect-relative -vvv -L" + then: "Client should correctly redirect relatively to /test/direct the same way as curl localhost:17320/test/redirect-relative -vvv -L" + noExceptionThrown() response.status() == HttpStatus.OK response.body() == "It works!" diff --git a/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/binding/RequestAttributeBindingSpec.groovy b/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/binding/RequestAttributeBindingSpec.groovy index 1375895688c..ca816a9fdc0 100644 --- a/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/binding/RequestAttributeBindingSpec.groovy +++ b/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/binding/RequestAttributeBindingSpec.groovy @@ -20,11 +20,10 @@ import javax.annotation.Nullable class RequestAttributeBindingSpec extends AbstractMicronautSpec { - @Issue("https://github.com/micronaut-projects/micronaut-core/issues/2846") void "test request attribute binding from a filter"() { given: BlockingHttpClient client = rxClient.toBlocking() - + expect: client.retrieve("/attribute/filter/implicit") == "Sally" client.retrieve("/attribute/filter/implicit/nonnull") == "Sally" diff --git a/http-server-tck/build.gradle.kts b/http-server-tck/build.gradle.kts index c4db82b7203..7b67e1976d0 100644 --- a/http-server-tck/build.gradle.kts +++ b/http-server-tck/build.gradle.kts @@ -18,6 +18,7 @@ dependencies { implementation(projects.jacksonDatabind) implementation(projects.inject) + api(projects.httpTck) api(projects.httpServer) api(projects.httpClientCore) api(libs.junit.jupiter.api) diff --git a/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/BodyArgumentTest.java b/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/BodyArgumentTest.java index 1923451f202..3fd97655691 100644 --- a/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/BodyArgumentTest.java +++ b/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/BodyArgumentTest.java @@ -23,11 +23,13 @@ import io.micronaut.http.annotation.Controller; import io.micronaut.http.annotation.Post; import io.micronaut.http.annotation.Produces; -import io.micronaut.http.server.tck.AssertionUtils; -import io.micronaut.http.server.tck.HttpResponseAssertion; +import io.micronaut.http.tck.AssertionUtils; +import io.micronaut.http.tck.HttpResponseAssertion; import org.junit.jupiter.api.Test; + import java.io.IOException; -import static io.micronaut.http.server.tck.TestScenario.asserts; + +import static io.micronaut.http.tck.TestScenario.asserts; @SuppressWarnings({ "java:S5960", // We're allowed assertions, as these are used in tests only diff --git a/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/BodyTest.java b/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/BodyTest.java index 0b8f4f4286d..1367822ebdf 100644 --- a/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/BodyTest.java +++ b/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/BodyTest.java @@ -26,16 +26,17 @@ import io.micronaut.http.annotation.Controller; import io.micronaut.http.annotation.Post; import io.micronaut.http.annotation.Status; -import io.micronaut.http.server.tck.AssertionUtils; -import io.micronaut.http.server.tck.BodyAssertion; -import io.micronaut.http.server.tck.HttpResponseAssertion; -import static io.micronaut.http.server.tck.TestScenario.asserts; +import io.micronaut.http.tck.AssertionUtils; +import io.micronaut.http.tck.BodyAssertion; +import io.micronaut.http.tck.HttpResponseAssertion; import org.junit.jupiter.api.Test; import org.reactivestreams.Publisher; import java.io.IOException; import java.util.Objects; +import static io.micronaut.http.tck.TestScenario.asserts; + @SuppressWarnings({ "java:S5960", // We're allowed assertions, as these are used in tests only "checkstyle:MissingJavadocType", diff --git a/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/ConsumesTest.java b/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/ConsumesTest.java index 770070381e8..ec03f2ffd1f 100644 --- a/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/ConsumesTest.java +++ b/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/ConsumesTest.java @@ -25,12 +25,14 @@ import io.micronaut.http.annotation.Consumes; import io.micronaut.http.annotation.Controller; import io.micronaut.http.annotation.Post; -import io.micronaut.http.server.tck.AssertionUtils; -import io.micronaut.http.server.tck.HttpResponseAssertion; -import static io.micronaut.http.server.tck.TestScenario.asserts; +import io.micronaut.http.tck.AssertionUtils; +import io.micronaut.http.tck.HttpResponseAssertion; import org.junit.jupiter.api.Test; + import java.io.IOException; +import static io.micronaut.http.tck.TestScenario.asserts; + @SuppressWarnings({ "java:S5960", // We're allowed assertions, as these are used in tests only "checkstyle:MissingJavadocType", diff --git a/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/CookiesTest.java b/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/CookiesTest.java index c11013a46d5..52421c90b27 100644 --- a/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/CookiesTest.java +++ b/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/CookiesTest.java @@ -23,15 +23,16 @@ import io.micronaut.http.annotation.CookieValue; import io.micronaut.http.annotation.Get; import io.micronaut.http.cookie.Cookie; -import io.micronaut.http.server.tck.AssertionUtils; -import io.micronaut.http.server.tck.HttpResponseAssertion; -import static io.micronaut.http.server.tck.TestScenario.asserts; +import io.micronaut.http.tck.AssertionUtils; +import io.micronaut.http.tck.HttpResponseAssertion; import org.junit.jupiter.api.Test; import java.io.IOException; import java.util.HashMap; import java.util.Map; +import static io.micronaut.http.tck.TestScenario.asserts; + @SuppressWarnings({ "java:S5960", // We're allowed assertions, as these are used in tests only "checkstyle:MissingJavadocType", diff --git a/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/DeleteWithoutBodyTest.java b/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/DeleteWithoutBodyTest.java index c48393b448e..7de46ff14c6 100644 --- a/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/DeleteWithoutBodyTest.java +++ b/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/DeleteWithoutBodyTest.java @@ -26,11 +26,11 @@ import io.micronaut.http.annotation.Header; import io.micronaut.http.annotation.PathVariable; import io.micronaut.http.annotation.Status; -import static io.micronaut.http.server.tck.TestScenario.asserts; import org.junit.jupiter.api.Test; import java.io.IOException; +import static io.micronaut.http.tck.TestScenario.asserts; import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertEquals; diff --git a/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/ErrorHandlerTest.java b/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/ErrorHandlerTest.java index 05d55dded1c..310dbd9edc3 100644 --- a/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/ErrorHandlerTest.java +++ b/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/ErrorHandlerTest.java @@ -36,11 +36,10 @@ import io.micronaut.http.hateoas.JsonError; import io.micronaut.http.hateoas.Link; import io.micronaut.http.server.exceptions.ExceptionHandler; -import io.micronaut.http.server.tck.AssertionUtils; -import io.micronaut.http.server.tck.HttpResponseAssertion; -import io.micronaut.http.server.tck.ServerUnderTest; -import io.micronaut.http.server.tck.ServerUnderTestProviderUtils; -import static io.micronaut.http.server.tck.TestScenario.asserts; +import io.micronaut.http.tck.AssertionUtils; +import io.micronaut.http.tck.HttpResponseAssertion; +import io.micronaut.http.tck.ServerUnderTest; +import io.micronaut.http.tck.ServerUnderTestProviderUtils; import jakarta.inject.Singleton; import org.junit.jupiter.api.Test; @@ -50,6 +49,8 @@ import java.util.Collections; import java.util.Map; +import static io.micronaut.http.tck.TestScenario.asserts; + @SuppressWarnings({ "java:S5960", // We're allowed assertions, as these are used in tests only diff --git a/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/FilterErrorTest.java b/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/FilterErrorTest.java index a4ba93082eb..010dc7d1d41 100644 --- a/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/FilterErrorTest.java +++ b/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/FilterErrorTest.java @@ -35,9 +35,8 @@ import io.micronaut.http.filter.HttpServerFilter; import io.micronaut.http.filter.ServerFilterChain; import io.micronaut.http.server.exceptions.ExceptionHandler; -import io.micronaut.http.server.tck.AssertionUtils; -import io.micronaut.http.server.tck.HttpResponseAssertion; -import static io.micronaut.http.server.tck.TestScenario.asserts; +import io.micronaut.http.tck.AssertionUtils; +import io.micronaut.http.tck.HttpResponseAssertion; import io.micronaut.web.router.MethodBasedRouteMatch; import io.micronaut.web.router.RouteMatch; import jakarta.inject.Singleton; @@ -48,6 +47,7 @@ import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; +import static io.micronaut.http.tck.TestScenario.asserts; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; diff --git a/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/FiltersTest.java b/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/FiltersTest.java index 64a52d674bf..7d9e358d8b0 100644 --- a/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/FiltersTest.java +++ b/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/FiltersTest.java @@ -31,10 +31,10 @@ import io.micronaut.http.filter.HttpServerFilter; import io.micronaut.http.filter.ServerFilterChain; import io.micronaut.http.server.exceptions.ExceptionHandler; -import io.micronaut.http.server.tck.AssertionUtils; -import io.micronaut.http.server.tck.HttpResponseAssertion; -import io.micronaut.http.server.tck.ServerUnderTest; -import io.micronaut.http.server.tck.ServerUnderTestProviderUtils; +import io.micronaut.http.tck.AssertionUtils; +import io.micronaut.http.tck.HttpResponseAssertion; +import io.micronaut.http.tck.ServerUnderTest; +import io.micronaut.http.tck.ServerUnderTestProviderUtils; import jakarta.inject.Singleton; import org.junit.jupiter.api.Test; import org.reactivestreams.Publisher; @@ -42,6 +42,7 @@ import java.io.IOException; import java.util.Collections; import java.util.Map; + @SuppressWarnings({ "java:S5960", // We're allowed assertions, as these are used in tests only "checkstyle:MissingJavadocType", diff --git a/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/FluxTest.java b/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/FluxTest.java index 887caeebcb2..fae866d70db 100644 --- a/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/FluxTest.java +++ b/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/FluxTest.java @@ -20,9 +20,8 @@ import io.micronaut.http.HttpStatus; import io.micronaut.http.annotation.Controller; import io.micronaut.http.annotation.Get; -import io.micronaut.http.server.tck.AssertionUtils; -import io.micronaut.http.server.tck.HttpResponseAssertion; -import static io.micronaut.http.server.tck.TestScenario.asserts; +import io.micronaut.http.tck.AssertionUtils; +import io.micronaut.http.tck.HttpResponseAssertion; import org.junit.jupiter.api.Test; import reactor.core.publisher.Flux; @@ -31,6 +30,8 @@ import java.util.Collections; import java.util.Map; +import static io.micronaut.http.tck.TestScenario.asserts; + @SuppressWarnings({ "java:S5960", // We're allowed assertions, as these are used in tests only "checkstyle:MissingJavadocType", diff --git a/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/HelloWorldTest.java b/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/HelloWorldTest.java index 86089cb5d28..7045fdab6dc 100644 --- a/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/HelloWorldTest.java +++ b/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/HelloWorldTest.java @@ -23,14 +23,14 @@ import io.micronaut.http.annotation.Controller; import io.micronaut.http.annotation.Get; import io.micronaut.http.annotation.Produces; -import io.micronaut.http.server.tck.AssertionUtils; -import static io.micronaut.http.server.tck.TestScenario.asserts; +import io.micronaut.http.tck.AssertionUtils; import io.micronaut.http.uri.UriBuilder; import org.junit.jupiter.api.Test; import java.io.IOException; import java.util.Collections; +import static io.micronaut.http.tck.TestScenario.asserts; @SuppressWarnings({ "java:S5960", // We're allowed assertions, as these are used in tests only diff --git a/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/MiscTest.java b/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/MiscTest.java index a7104d96e4f..05d8ff5d2a9 100644 --- a/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/MiscTest.java +++ b/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/MiscTest.java @@ -28,9 +28,8 @@ import io.micronaut.http.annotation.Controller; import io.micronaut.http.annotation.Get; import io.micronaut.http.annotation.Post; -import io.micronaut.http.server.tck.AssertionUtils; -import io.micronaut.http.server.tck.HttpResponseAssertion; -import static io.micronaut.http.server.tck.TestScenario.asserts; +import io.micronaut.http.tck.AssertionUtils; +import io.micronaut.http.tck.HttpResponseAssertion; import org.junit.jupiter.api.Test; import javax.validation.constraints.NotBlank; @@ -38,6 +37,8 @@ import java.util.Collections; import java.util.Map; +import static io.micronaut.http.tck.TestScenario.asserts; + @SuppressWarnings({ "java:S5960", // We're allowed assertions, as these are used in tests only "checkstyle:MissingJavadocType", diff --git a/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/OctetTest.java b/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/OctetTest.java index 5778c8c1e42..985de32da02 100644 --- a/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/OctetTest.java +++ b/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/OctetTest.java @@ -22,16 +22,16 @@ import io.micronaut.http.MediaType; import io.micronaut.http.annotation.Controller; import io.micronaut.http.annotation.Get; -import io.micronaut.http.server.tck.AssertionUtils; -import io.micronaut.http.server.tck.BodyAssertion; -import io.micronaut.http.server.tck.HttpResponseAssertion; +import io.micronaut.http.tck.AssertionUtils; +import io.micronaut.http.tck.BodyAssertion; +import io.micronaut.http.tck.HttpResponseAssertion; import org.junit.jupiter.api.Test; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.util.stream.IntStream; -import static io.micronaut.http.server.tck.TestScenario.asserts; +import static io.micronaut.http.tck.TestScenario.asserts; @SuppressWarnings({ "java:S5960", // We're allowed assertions, as these are used in tests only diff --git a/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/ParameterTest.java b/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/ParameterTest.java index 4ad42b4659f..267b37d82b2 100644 --- a/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/ParameterTest.java +++ b/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/ParameterTest.java @@ -20,15 +20,16 @@ import io.micronaut.http.HttpStatus; import io.micronaut.http.annotation.Controller; import io.micronaut.http.annotation.Get; -import io.micronaut.http.server.tck.AssertionUtils; -import io.micronaut.http.server.tck.HttpResponseAssertion; -import static io.micronaut.http.server.tck.TestScenario.asserts; +import io.micronaut.http.tck.AssertionUtils; +import io.micronaut.http.tck.HttpResponseAssertion; import io.micronaut.http.uri.UriBuilder; import org.junit.jupiter.api.Test; import java.io.IOException; import java.util.List; +import static io.micronaut.http.tck.TestScenario.asserts; + @SuppressWarnings({ "java:S5960", // We're allowed assertions, as these are used in tests only diff --git a/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/RemoteAddressTest.java b/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/RemoteAddressTest.java index 49b94b05ba4..c445f5be9d5 100644 --- a/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/RemoteAddressTest.java +++ b/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/RemoteAddressTest.java @@ -28,9 +28,8 @@ import io.micronaut.http.filter.HttpServerFilter; import io.micronaut.http.filter.ServerFilterChain; import io.micronaut.http.server.exceptions.ExceptionHandler; -import io.micronaut.http.server.tck.AssertionUtils; -import io.micronaut.http.server.tck.HttpResponseAssertion; -import static io.micronaut.http.server.tck.TestScenario.asserts; +import io.micronaut.http.tck.AssertionUtils; +import io.micronaut.http.tck.HttpResponseAssertion; import jakarta.inject.Singleton; import org.junit.jupiter.api.Test; import org.reactivestreams.Publisher; @@ -38,6 +37,7 @@ import java.io.IOException; import java.util.Collections; +import static io.micronaut.http.tck.TestScenario.asserts; @SuppressWarnings({ "java:S5960", // We're allowed assertions, as these are used in tests only diff --git a/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/ResponseStatusTest.java b/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/ResponseStatusTest.java index 69959731609..d2d937d6454 100644 --- a/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/ResponseStatusTest.java +++ b/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/ResponseStatusTest.java @@ -26,9 +26,8 @@ import io.micronaut.http.annotation.Get; import io.micronaut.http.annotation.Post; import io.micronaut.http.annotation.Status; -import io.micronaut.http.server.tck.AssertionUtils; -import io.micronaut.http.server.tck.HttpResponseAssertion; -import static io.micronaut.http.server.tck.TestScenario.asserts; +import io.micronaut.http.tck.AssertionUtils; +import io.micronaut.http.tck.HttpResponseAssertion; import org.junit.jupiter.api.Test; import javax.validation.ConstraintViolationException; @@ -36,6 +35,7 @@ import java.util.Collections; import java.util.Optional; +import static io.micronaut.http.tck.TestScenario.asserts; @SuppressWarnings({ "java:S5960", // We're allowed assertions, as these are used in tests only diff --git a/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/StatusTest.java b/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/StatusTest.java index 4f4d0a0f6a5..8c57108a9a8 100644 --- a/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/StatusTest.java +++ b/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/StatusTest.java @@ -25,15 +25,16 @@ import io.micronaut.http.server.exceptions.ExceptionHandler; import io.micronaut.http.server.exceptions.response.ErrorContext; import io.micronaut.http.server.exceptions.response.ErrorResponseProcessor; -import io.micronaut.http.server.tck.AssertionUtils; -import io.micronaut.http.server.tck.HttpResponseAssertion; -import static io.micronaut.http.server.tck.TestScenario.asserts; +import io.micronaut.http.tck.AssertionUtils; +import io.micronaut.http.tck.HttpResponseAssertion; import jakarta.inject.Singleton; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; import java.io.IOException; +import static io.micronaut.http.tck.TestScenario.asserts; + @SuppressWarnings({ "java:S5960", // We're allowed assertions, as these are used in tests only "checkstyle:MissingJavadocType", diff --git a/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/VersionTest.java b/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/VersionTest.java index 7665853c62f..d7b5345c08f 100644 --- a/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/VersionTest.java +++ b/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/VersionTest.java @@ -23,12 +23,13 @@ import io.micronaut.http.HttpStatus; import io.micronaut.http.annotation.Controller; import io.micronaut.http.annotation.Get; -import io.micronaut.http.server.tck.AssertionUtils; -import io.micronaut.http.server.tck.HttpResponseAssertion; -import static io.micronaut.http.server.tck.TestScenario.asserts; +import io.micronaut.http.tck.AssertionUtils; +import io.micronaut.http.tck.HttpResponseAssertion; import org.junit.jupiter.api.Test; import java.io.IOException; +import static io.micronaut.http.tck.TestScenario.asserts; + @SuppressWarnings({ "java:S5960", // We're allowed assertions, as these are used in tests only "checkstyle:MissingJavadocType", diff --git a/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/cors/CorsDisabledByDefaultTest.java b/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/cors/CorsDisabledByDefaultTest.java index cc52de62568..f09933ae89b 100644 --- a/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/cors/CorsDisabledByDefaultTest.java +++ b/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/cors/CorsDisabledByDefaultTest.java @@ -24,16 +24,16 @@ import io.micronaut.http.annotation.Controller; import io.micronaut.http.annotation.Post; import io.micronaut.http.annotation.Status; -import io.micronaut.http.server.tck.AssertionUtils; -import io.micronaut.http.server.tck.HttpResponseAssertion; import io.micronaut.http.server.util.HttpHostResolver; +import io.micronaut.http.tck.AssertionUtils; +import io.micronaut.http.tck.HttpResponseAssertion; import jakarta.inject.Singleton; import org.junit.jupiter.api.Test; import java.io.IOException; import java.util.Collections; -import static io.micronaut.http.server.tck.TestScenario.asserts; +import static io.micronaut.http.tck.TestScenario.asserts; import static org.junit.jupiter.api.Assertions.assertNull; @SuppressWarnings({ @@ -46,7 +46,7 @@ public class CorsDisabledByDefaultTest { private static final String SPECNAME = "CorsDisabledByDefaultTest"; /** - * By default CORS is disabled no cors headers are present in response. + * By default, CORS is disabled no cors headers are present in response. * @throws IOException may throw the try for resources */ @Test diff --git a/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/cors/CorsSimpleRequestTest.java b/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/cors/CorsSimpleRequestTest.java index aff8053c321..db8de4dd07a 100644 --- a/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/cors/CorsSimpleRequestTest.java +++ b/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/cors/CorsSimpleRequestTest.java @@ -28,19 +28,20 @@ import io.micronaut.http.annotation.Post; import io.micronaut.http.annotation.Status; import io.micronaut.http.client.multipart.MultipartBody; -import io.micronaut.http.server.tck.AssertionUtils; -import io.micronaut.http.server.tck.HttpResponseAssertion; -import io.micronaut.http.server.tck.RequestSupplier; -import io.micronaut.http.server.tck.ServerUnderTest; +import io.micronaut.http.tck.AssertionUtils; +import io.micronaut.http.tck.HttpResponseAssertion; +import io.micronaut.http.tck.RequestSupplier; +import io.micronaut.http.tck.ServerUnderTest; import io.micronaut.runtime.context.scope.refresh.RefreshEvent; import jakarta.inject.Inject; import jakarta.inject.Singleton; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import java.io.IOException; import java.util.Collections; -import static io.micronaut.http.server.tck.TestScenario.asserts; +import static io.micronaut.http.tck.TestScenario.asserts; import static org.junit.jupiter.api.Assertions.*; @SuppressWarnings({ @@ -68,6 +69,7 @@ public class CorsSimpleRequestTest { * @throws IOException may throw the try for resources */ @Test + @Tag("multipart") void corsSimpleRequestNotAllowedForLocalhostAndAny() throws IOException { asserts(SPECNAME, Collections.singletonMap(PROPERTY_MICRONAUT_SERVER_CORS_ENABLED, StringUtils.TRUE), @@ -83,6 +85,7 @@ void corsSimpleRequestNotAllowedForLocalhostAndAny() throws IOException { * @throws IOException */ @Test + @Tag("multipart") void corsSimpleRequestAllowedForLocalhostAndAnyWhenSpecificallyTurnedOff() throws IOException { asserts(SPECNAME, CollectionUtils.mapOf( @@ -108,6 +111,7 @@ void corsSimpleRequestAllowedForLocalhostAndAnyWhenSpecificallyTurnedOff() throw * @throws IOException may throw the try for resources */ @Test + @Tag("multipart") void corsSimpleRequestNotAllowedFor127AndAny() throws IOException { asserts(SPECNAME, Collections.singletonMap(PROPERTY_MICRONAUT_SERVER_CORS_ENABLED, StringUtils.TRUE), @@ -121,6 +125,7 @@ void corsSimpleRequestNotAllowedFor127AndAny() throws IOException { * @throws IOException scenario step fails */ @Test + @Tag("multipart") void corsSimpleRequestAllowedForLocalhostAndOriginLocalhost() throws IOException { asserts(SPECNAME, Collections.singletonMap(PROPERTY_MICRONAUT_SERVER_CORS_ENABLED, StringUtils.TRUE), @@ -135,6 +140,7 @@ void corsSimpleRequestAllowedForLocalhostAndOriginLocalhost() throws IOException * @throws IOException */ @Test + @Tag("multipart") void corsSimpleRequestAllowedForLocalhostAnd127Origin() throws IOException { asserts(SPECNAME, Collections.singletonMap(PROPERTY_MICRONAUT_SERVER_CORS_ENABLED, StringUtils.TRUE), @@ -149,6 +155,7 @@ void corsSimpleRequestAllowedForLocalhostAnd127Origin() throws IOException { * @throws IOException */ @Test + @Tag("multipart") void corsSimpleRequestFailsForLocalhostAndSpoofed127Origin() throws IOException { asserts(SPECNAME, Collections.singletonMap(PROPERTY_MICRONAUT_SERVER_CORS_ENABLED, StringUtils.TRUE), @@ -163,6 +170,7 @@ void corsSimpleRequestFailsForLocalhostAndSpoofed127Origin() throws IOException * @throws IOException */ @Test + @Tag("multipart") void corsSimpleRequestAllowedFor127RequestAndLocalhostOrigin() throws IOException { asserts(SPECNAME, Collections.singletonMap(PROPERTY_MICRONAUT_SERVER_CORS_ENABLED, StringUtils.TRUE), @@ -176,6 +184,7 @@ void corsSimpleRequestAllowedFor127RequestAndLocalhostOrigin() throws IOExceptio * @throws IOException may throw the try for resources */ @Test + @Tag("multipart") void corsSimpleRequestForLocalhostCanBeAllowedViaConfiguration() throws IOException { asserts(SPECNAME, CollectionUtils.mapOf( diff --git a/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/cors/SimpleRequestWithCorsNotEnabledTest.java b/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/cors/SimpleRequestWithCorsNotEnabledTest.java index 285663ce22a..572f98cbf90 100644 --- a/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/cors/SimpleRequestWithCorsNotEnabledTest.java +++ b/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/cors/SimpleRequestWithCorsNotEnabledTest.java @@ -24,8 +24,8 @@ import io.micronaut.http.annotation.Controller; import io.micronaut.http.annotation.Post; import io.micronaut.http.annotation.Status; -import io.micronaut.http.server.tck.AssertionUtils; -import io.micronaut.http.server.tck.HttpResponseAssertion; +import io.micronaut.http.tck.AssertionUtils; +import io.micronaut.http.tck.HttpResponseAssertion; import io.micronaut.runtime.context.scope.refresh.RefreshEvent; import jakarta.inject.Inject; import jakarta.inject.Singleton; @@ -34,7 +34,7 @@ import java.io.IOException; import java.util.Collections; -import static io.micronaut.http.server.tck.TestScenario.asserts; +import static io.micronaut.http.tck.TestScenario.asserts; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; diff --git a/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/filter/ClientRequestFilterTest.java b/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/filter/ClientRequestFilterTest.java index b931c69553f..485b22c11e3 100644 --- a/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/filter/ClientRequestFilterTest.java +++ b/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/filter/ClientRequestFilterTest.java @@ -27,9 +27,9 @@ import io.micronaut.http.annotation.Get; import io.micronaut.http.annotation.RequestFilter; import io.micronaut.http.filter.FilterContinuation; -import io.micronaut.http.server.tck.AssertionUtils; -import io.micronaut.http.server.tck.HttpResponseAssertion; -import io.micronaut.http.server.tck.TestScenario; +import io.micronaut.http.tck.AssertionUtils; +import io.micronaut.http.tck.HttpResponseAssertion; +import io.micronaut.http.tck.TestScenario; import io.micronaut.scheduling.TaskExecutors; import io.micronaut.scheduling.annotation.ExecuteOn; import jakarta.inject.Singleton; diff --git a/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/filter/ClientResponseFilterTest.java b/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/filter/ClientResponseFilterTest.java index 85e976bd790..cdb7079a524 100644 --- a/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/filter/ClientResponseFilterTest.java +++ b/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/filter/ClientResponseFilterTest.java @@ -27,9 +27,9 @@ import io.micronaut.http.annotation.Get; import io.micronaut.http.annotation.ResponseFilter; import io.micronaut.http.client.exceptions.HttpClientResponseException; -import io.micronaut.http.server.tck.AssertionUtils; -import io.micronaut.http.server.tck.HttpResponseAssertion; -import io.micronaut.http.server.tck.TestScenario; +import io.micronaut.http.tck.AssertionUtils; +import io.micronaut.http.tck.HttpResponseAssertion; +import io.micronaut.http.tck.TestScenario; import jakarta.inject.Singleton; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Disabled; diff --git a/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/filter/HttpServerFilterExceptionHandlerTest.java b/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/filter/HttpServerFilterExceptionHandlerTest.java index 28761cdc813..5fb71f6692e 100644 --- a/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/filter/HttpServerFilterExceptionHandlerTest.java +++ b/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/filter/HttpServerFilterExceptionHandlerTest.java @@ -30,9 +30,9 @@ import io.micronaut.http.server.exceptions.ExceptionHandler; import io.micronaut.http.server.exceptions.response.ErrorContext; import io.micronaut.http.server.exceptions.response.ErrorResponseProcessor; -import io.micronaut.http.server.tck.AssertionUtils; -import io.micronaut.http.server.tck.ServerUnderTest; -import io.micronaut.http.server.tck.TestScenario; +import io.micronaut.http.tck.AssertionUtils; +import io.micronaut.http.tck.ServerUnderTest; +import io.micronaut.http.tck.TestScenario; import jakarta.inject.Singleton; import org.junit.jupiter.api.Test; import org.reactivestreams.Publisher; diff --git a/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/filter/RequestFilterExceptionHandlerTest.java b/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/filter/RequestFilterExceptionHandlerTest.java index 13d3825af53..19f5f51e9fb 100644 --- a/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/filter/RequestFilterExceptionHandlerTest.java +++ b/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/filter/RequestFilterExceptionHandlerTest.java @@ -29,9 +29,9 @@ import io.micronaut.http.server.exceptions.ExceptionHandler; import io.micronaut.http.server.exceptions.response.ErrorContext; import io.micronaut.http.server.exceptions.response.ErrorResponseProcessor; -import io.micronaut.http.server.tck.AssertionUtils; -import io.micronaut.http.server.tck.ServerUnderTest; -import io.micronaut.http.server.tck.TestScenario; +import io.micronaut.http.tck.AssertionUtils; +import io.micronaut.http.tck.ServerUnderTest; +import io.micronaut.http.tck.TestScenario; import jakarta.inject.Singleton; import org.junit.jupiter.api.Test; diff --git a/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/filter/RequestFilterTest.java b/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/filter/RequestFilterTest.java index aa6c5f99463..3427e33ad9e 100644 --- a/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/filter/RequestFilterTest.java +++ b/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/filter/RequestFilterTest.java @@ -27,9 +27,9 @@ import io.micronaut.http.annotation.RequestFilter; import io.micronaut.http.annotation.ServerFilter; import io.micronaut.http.filter.FilterContinuation; -import io.micronaut.http.server.tck.AssertionUtils; -import io.micronaut.http.server.tck.HttpResponseAssertion; -import io.micronaut.http.server.tck.TestScenario; +import io.micronaut.http.tck.AssertionUtils; +import io.micronaut.http.tck.HttpResponseAssertion; +import io.micronaut.http.tck.TestScenario; import io.micronaut.scheduling.TaskExecutors; import io.micronaut.scheduling.annotation.ExecuteOn; import jakarta.inject.Singleton; diff --git a/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/filter/ResponseFilterExceptionHandlerTest.java b/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/filter/ResponseFilterExceptionHandlerTest.java index 0a94392eb68..2b2ba56d042 100644 --- a/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/filter/ResponseFilterExceptionHandlerTest.java +++ b/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/filter/ResponseFilterExceptionHandlerTest.java @@ -29,9 +29,9 @@ import io.micronaut.http.server.exceptions.ExceptionHandler; import io.micronaut.http.server.exceptions.response.ErrorContext; import io.micronaut.http.server.exceptions.response.ErrorResponseProcessor; -import io.micronaut.http.server.tck.AssertionUtils; -import io.micronaut.http.server.tck.ServerUnderTest; -import io.micronaut.http.server.tck.TestScenario; +import io.micronaut.http.tck.AssertionUtils; +import io.micronaut.http.tck.ServerUnderTest; +import io.micronaut.http.tck.TestScenario; import jakarta.inject.Singleton; import org.junit.jupiter.api.Test; diff --git a/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/filter/ResponseFilterTest.java b/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/filter/ResponseFilterTest.java index f0d3fefc5e9..220ec0a4d0d 100644 --- a/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/filter/ResponseFilterTest.java +++ b/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/filter/ResponseFilterTest.java @@ -26,9 +26,9 @@ import io.micronaut.http.annotation.Get; import io.micronaut.http.annotation.ResponseFilter; import io.micronaut.http.annotation.ServerFilter; -import io.micronaut.http.server.tck.AssertionUtils; -import io.micronaut.http.server.tck.HttpResponseAssertion; -import io.micronaut.http.server.tck.TestScenario; +import io.micronaut.http.tck.AssertionUtils; +import io.micronaut.http.tck.HttpResponseAssertion; +import io.micronaut.http.tck.TestScenario; import jakarta.inject.Singleton; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; diff --git a/http-tck/build.gradle.kts b/http-tck/build.gradle.kts new file mode 100644 index 00000000000..3d93011d827 --- /dev/null +++ b/http-tck/build.gradle.kts @@ -0,0 +1,26 @@ +import org.gradle.internal.impldep.org.junit.experimental.categories.Categories.CategoryFilter.exclude + +plugins { + id("io.micronaut.build.internal.convention-library") +} +dependencies { + annotationProcessor(projects.injectJava) + annotationProcessor(platform(libs.test.boms.micronaut.validation)) + annotationProcessor(libs.micronaut.validation.processor) { + exclude(group = "io.micronaut") + } + annotationProcessor(projects.httpValidation) + + implementation(platform(libs.test.boms.micronaut.validation)) + implementation(libs.micronaut.validation) { + exclude(group = "io.micronaut") + } + implementation(projects.runtime) + implementation(projects.jacksonDatabind) + implementation(projects.inject) + api(projects.httpServer) + api(projects.httpClientCore) + api(libs.junit.jupiter.api) + api(libs.junit.jupiter.params) + api(libs.managed.reactor) +} diff --git a/http-server-tck/src/main/java/io/micronaut/http/server/tck/AssertionUtils.java b/http-tck/src/main/java/io/micronaut/http/tck/AssertionUtils.java similarity index 87% rename from http-server-tck/src/main/java/io/micronaut/http/server/tck/AssertionUtils.java rename to http-tck/src/main/java/io/micronaut/http/tck/AssertionUtils.java index efb31b39eed..3b0fc772462 100644 --- a/http-server-tck/src/main/java/io/micronaut/http/server/tck/AssertionUtils.java +++ b/http-tck/src/main/java/io/micronaut/http/tck/AssertionUtils.java @@ -13,15 +13,17 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.micronaut.http.server.tck; +package io.micronaut.http.tck; import io.micronaut.core.annotation.Experimental; import io.micronaut.core.annotation.NonNull; import io.micronaut.core.annotation.Nullable; +import io.micronaut.core.type.Argument; import io.micronaut.http.HttpHeaders; import io.micronaut.http.HttpRequest; import io.micronaut.http.HttpResponse; import io.micronaut.http.HttpStatus; +import io.micronaut.http.client.HttpClient; import io.micronaut.http.client.exceptions.HttpClientResponseException; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.function.Executable; @@ -58,7 +60,7 @@ public static void assertThrows(@NonNull ServerUnderTest server, @NonNull HttpRequest request, @NonNull HttpResponseAssertion assertion) { Executable e = assertion.getBody() != null ? - () -> server.exchange(request, String.class) : + () -> server.exchange(request, Argument.of(assertion.getBody().getBodyType()), errorType(assertion)) : () -> server.exchange(request); HttpClientResponseException thrown = Assertions.assertThrows(HttpClientResponseException.class, e); HttpResponse response = thrown.getResponse(); @@ -68,6 +70,18 @@ public static void assertThrows(@NonNull ServerUnderTest server, assertion.getResponseConsumer().ifPresent(httpResponseConsumer -> httpResponseConsumer.accept(response)); } + @Nullable + private static Argument errorType(HttpResponseAssertion assertion) { + if (assertion.getBody() == null) { + return HttpClient.DEFAULT_ERROR_TYPE; + } + if (assertion.getBody().getErrorType() == null) { + return HttpClient.DEFAULT_ERROR_TYPE; + } + return Argument.of(assertion.getBody().getErrorType()); + + } + public static void assertThrows(@NonNull ServerUnderTest server, @NonNull HttpRequest request, @NonNull HttpStatus expectedStatus, @@ -96,7 +110,7 @@ public static void assertDoesNotThrow(@NonNull ServerUnderTest server, @NonNull HttpRequest request, @NonNull HttpResponseAssertion assertion) { ThrowingSupplier> executable = assertion.getBody() != null ? - () -> server.exchange(request, String.class) : + () -> server.exchange(request, Argument.of(assertion.getBody().getBodyType()), errorType(assertion)) : () -> server.exchange(request); HttpResponse response = Assertions.assertDoesNotThrow(executable); assertEquals(assertion.getHttpStatus(), response.getStatus()); @@ -105,7 +119,7 @@ public static void assertDoesNotThrow(@NonNull ServerUnderTest server, assertion.getResponseConsumer().ifPresent(httpResponseConsumer -> httpResponseConsumer.accept(response)); } - private static void assertBody(@NonNull HttpResponse response, @Nullable BodyAssertion bodyAssertion) { + private static void assertBody(@NonNull HttpResponse response, @Nullable BodyAssertion bodyAssertion) { if (bodyAssertion != null) { Optional bodyOptional = response.getBody(bodyAssertion.getBodyType()); assertTrue(bodyOptional.isPresent()); @@ -136,5 +150,4 @@ private static void assertHeaders(@NonNull HttpResponse response, @Nullable } } - } diff --git a/http-server-tck/src/main/java/io/micronaut/http/server/tck/BodyAssertion.java b/http-tck/src/main/java/io/micronaut/http/tck/BodyAssertion.java similarity index 70% rename from http-server-tck/src/main/java/io/micronaut/http/server/tck/BodyAssertion.java rename to http-tck/src/main/java/io/micronaut/http/tck/BodyAssertion.java index e37021054ad..85053bbd44e 100644 --- a/http-server-tck/src/main/java/io/micronaut/http/server/tck/BodyAssertion.java +++ b/http-tck/src/main/java/io/micronaut/http/tck/BodyAssertion.java @@ -13,9 +13,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.micronaut.http.server.tck; +package io.micronaut.http.tck; import io.micronaut.core.annotation.Experimental; +import io.micronaut.core.annotation.Nullable; import java.util.Arrays; import java.util.function.BiPredicate; @@ -26,16 +27,21 @@ * HTTP Response's body assertions. * * @param The body type + * @param The error type */ @Experimental -public final class BodyAssertion { +public final class BodyAssertion { private final Class bodyType; + private final Class errorType; private final T expected; private final BiPredicate evaluator; - private BodyAssertion(Class bodyType, T expected, BiPredicate evaluator) { + private BodyAssertion(Class bodyType, + Class errorType, + T expected, BiPredicate evaluator) { this.bodyType = bodyType; + this.errorType = errorType; this.expected = expected; this.evaluator = evaluator; } @@ -64,22 +70,28 @@ public Class getBodyType() { return bodyType; } + @Nullable + public Class getErrorType() { + return errorType; + } + /** * The interface for typed BodyAssertion Builders. * * @param The body type + * @param The error type */ - public interface AssertionBuilder { + public interface AssertionBuilder { /** * @return a body assertion which verifiers the HTTP Response's body contains the expected body */ - BodyAssertion contains(); + BodyAssertion contains(); /** * @return a body assertion which verifiers the HTTP Response's body is equals to the expected body */ - BodyAssertion equals(); + BodyAssertion equals(); } /** @@ -91,7 +103,7 @@ public static class Builder { * @param expected Expected Body * @return The Builder */ - public AssertionBuilder body(String expected) { + public AssertionBuilder body(String expected) { return new StringBodyAssertionBuilder(expected); } @@ -99,7 +111,7 @@ public AssertionBuilder body(String expected) { * @param expected Expected Body * @return The Builder */ - public AssertionBuilder body(byte[] expected) { + public AssertionBuilder body(byte[] expected) { return new ByteArrayBodyAssertionBuilder(expected); } } @@ -107,7 +119,7 @@ public AssertionBuilder body(byte[] expected) { /** * String BodyAssertion Builder. */ - public static class StringBodyAssertionBuilder extends BodyAssertion.Builder implements AssertionBuilder { + public static class StringBodyAssertionBuilder extends BodyAssertion.Builder implements AssertionBuilder { private final String body; @@ -118,22 +130,22 @@ public StringBodyAssertionBuilder(String expected) { /** * @return a body assertion which verifiers the HTTP Response's body contains the expected body */ - public BodyAssertion contains() { - return new BodyAssertion<>(String.class, this.body, (required, received) -> received.contains(required)); + public BodyAssertion contains() { + return new BodyAssertion<>(String.class, String.class, this.body, (required, received) -> received.contains(required)); } /** * @return a body assertion which verifiers the HTTP Response's body is equals to the expected body */ - public BodyAssertion equals() { - return new BodyAssertion<>(String.class, this.body, (required, received) -> received.equals(required)); + public BodyAssertion equals() { + return new BodyAssertion<>(String.class, String.class, this.body, (required, received) -> received.equals(required)); } } /** * Byte Array BodyAssertion Builder. */ - public static class ByteArrayBodyAssertionBuilder extends BodyAssertion.Builder implements BodyAssertion.AssertionBuilder { + public static class ByteArrayBodyAssertionBuilder extends BodyAssertion.Builder implements BodyAssertion.AssertionBuilder { private final byte[] body; @@ -144,8 +156,8 @@ public ByteArrayBodyAssertionBuilder(byte[] expected) { /** * @return a body assertion which verifiers the HTTP Response's body contains the expected body */ - public BodyAssertion contains() { - return new BodyAssertion<>(byte[].class, this.body, (required, received) -> { + public BodyAssertion contains() { + return new BodyAssertion<>(byte[].class, byte[].class, this.body, (required, received) -> { throw new AssertionError("Not implemented yet!"); }); } @@ -153,8 +165,8 @@ public BodyAssertion contains() { /** * @return a body assertion which verifiers the HTTP Response's body is equals to the expected body */ - public BodyAssertion equals() { - return new BodyAssertion<>(byte[].class, this.body, (required, received) -> Arrays.equals(received, required)); + public BodyAssertion equals() { + return new BodyAssertion<>(byte[].class, byte[].class, this.body, (required, received) -> Arrays.equals(received, required)); } } } diff --git a/http-server-tck/src/main/java/io/micronaut/http/server/tck/EmbeddedServerUnderTest.java b/http-tck/src/main/java/io/micronaut/http/tck/EmbeddedServerUnderTest.java similarity index 71% rename from http-server-tck/src/main/java/io/micronaut/http/server/tck/EmbeddedServerUnderTest.java rename to http-tck/src/main/java/io/micronaut/http/tck/EmbeddedServerUnderTest.java index 115feccf611..f15d01f51a4 100644 --- a/http-server-tck/src/main/java/io/micronaut/http/server/tck/EmbeddedServerUnderTest.java +++ b/http-tck/src/main/java/io/micronaut/http/tck/EmbeddedServerUnderTest.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.micronaut.http.server.tck; +package io.micronaut.http.tck; import io.micronaut.context.ApplicationContext; import io.micronaut.core.annotation.Experimental; @@ -24,8 +24,10 @@ import io.micronaut.http.client.BlockingHttpClient; import io.micronaut.http.client.HttpClient; import io.micronaut.runtime.server.EmbeddedServer; +import reactor.core.publisher.Flux; import java.io.IOException; +import java.net.URL; import java.util.Map; import java.util.Optional; @@ -37,17 +39,32 @@ @Experimental public class EmbeddedServerUnderTest implements ServerUnderTest { - private EmbeddedServer embeddedServer; + private final boolean isBlockingClient; + private final EmbeddedServer embeddedServer; private HttpClient httpClient; private BlockingHttpClient client; public EmbeddedServerUnderTest(@NonNull Map properties) { this.embeddedServer = ApplicationContext.run(EmbeddedServer.class, properties); + this.isBlockingClient = (boolean) properties.getOrDefault(BLOCKING_CLIENT_PROPERTY, true); } @Override public HttpResponse exchange(HttpRequest request, Argument bodyType) { - return getBlockingHttpClient().exchange(request, bodyType); + if (isBlockingClient) { + return getBlockingHttpClient().exchange(request, bodyType); + } else { + return Flux.from(getHttpClient().exchange(request, bodyType)).blockFirst(); + } + } + + @Override + public HttpResponse exchange(HttpRequest request, Argument bodyType, Argument errorType) { + if (isBlockingClient) { + return getBlockingHttpClient().exchange(request, bodyType, errorType); + } else { + return Flux.from(getHttpClient().exchange(request, bodyType, errorType)).blockFirst(); + } } @Override @@ -71,6 +88,12 @@ public Optional getPort() { return Optional.ofNullable(embeddedServer).map(EmbeddedServer::getPort); } + @Override + @NonNull + public Optional getURL() { + return Optional.ofNullable(embeddedServer).map(EmbeddedServer::getURL); + } + @NonNull private HttpClient getHttpClient() { if (httpClient == null) { diff --git a/http-server-tck/src/main/java/io/micronaut/http/server/tck/EmbeddedServerUnderTestProvider.java b/http-tck/src/main/java/io/micronaut/http/tck/EmbeddedServerUnderTestProvider.java similarity index 96% rename from http-server-tck/src/main/java/io/micronaut/http/server/tck/EmbeddedServerUnderTestProvider.java rename to http-tck/src/main/java/io/micronaut/http/tck/EmbeddedServerUnderTestProvider.java index f6708f64620..e00296a5b94 100644 --- a/http-server-tck/src/main/java/io/micronaut/http/server/tck/EmbeddedServerUnderTestProvider.java +++ b/http-tck/src/main/java/io/micronaut/http/tck/EmbeddedServerUnderTestProvider.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.micronaut.http.server.tck; +package io.micronaut.http.tck; import io.micronaut.core.annotation.Experimental; import io.micronaut.core.annotation.NonNull; @@ -27,6 +27,7 @@ */ @Experimental public class EmbeddedServerUnderTestProvider implements ServerUnderTestProvider { + @Override @NonNull public ServerUnderTest getServer(@NonNull Map properties) { diff --git a/http-server-tck/src/main/java/io/micronaut/http/server/tck/HttpResponseAssertion.java b/http-tck/src/main/java/io/micronaut/http/tck/HttpResponseAssertion.java similarity index 93% rename from http-server-tck/src/main/java/io/micronaut/http/server/tck/HttpResponseAssertion.java rename to http-tck/src/main/java/io/micronaut/http/tck/HttpResponseAssertion.java index 1b39cdfb938..1ce187026bb 100644 --- a/http-server-tck/src/main/java/io/micronaut/http/server/tck/HttpResponseAssertion.java +++ b/http-tck/src/main/java/io/micronaut/http/tck/HttpResponseAssertion.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.micronaut.http.server.tck; +package io.micronaut.http.tck; import io.micronaut.core.annotation.Experimental; import io.micronaut.core.annotation.NonNull; @@ -36,14 +36,14 @@ public final class HttpResponseAssertion { private final HttpStatus httpStatus; private final Map headers; - private final BodyAssertion bodyAssertion; + private final BodyAssertion bodyAssertion; @Nullable private final Consumer> responseConsumer; private HttpResponseAssertion(HttpStatus httpStatus, Map headers, - BodyAssertion bodyAssertion, + BodyAssertion bodyAssertion, @Nullable Consumer> responseConsumer) { this.httpStatus = httpStatus; this.headers = headers; @@ -77,7 +77,7 @@ public Map getHeaders() { * @return Expected HTTP Response body */ - public BodyAssertion getBody() { + public BodyAssertion getBody() { return bodyAssertion; } @@ -95,7 +95,7 @@ public static HttpResponseAssertion.Builder builder() { public static class Builder { private HttpStatus httpStatus; private Map headers; - private BodyAssertion bodyAssertion; + private BodyAssertion bodyAssertion; private Consumer> responseConsumer; @@ -148,7 +148,7 @@ public Builder body(String containsBody) { * @param bodyAssertion Response Body Assertion * @return HTTP Response Assertion Builder */ - public Builder body(BodyAssertion bodyAssertion) { + public Builder body(BodyAssertion bodyAssertion) { this.bodyAssertion = bodyAssertion; return this; } diff --git a/http-server-tck/src/main/java/io/micronaut/http/server/tck/RequestSupplier.java b/http-tck/src/main/java/io/micronaut/http/tck/RequestSupplier.java similarity index 96% rename from http-server-tck/src/main/java/io/micronaut/http/server/tck/RequestSupplier.java rename to http-tck/src/main/java/io/micronaut/http/tck/RequestSupplier.java index c58dad9356f..d6ffa0ba680 100644 --- a/http-server-tck/src/main/java/io/micronaut/http/server/tck/RequestSupplier.java +++ b/http-tck/src/main/java/io/micronaut/http/tck/RequestSupplier.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.micronaut.http.server.tck; +package io.micronaut.http.tck; import io.micronaut.http.HttpRequest; diff --git a/http-server-tck/src/main/java/io/micronaut/http/server/tck/ServerUnderTest.java b/http-tck/src/main/java/io/micronaut/http/tck/ServerUnderTest.java similarity index 64% rename from http-server-tck/src/main/java/io/micronaut/http/server/tck/ServerUnderTest.java rename to http-tck/src/main/java/io/micronaut/http/tck/ServerUnderTest.java index 0712fc9633b..af0d0d97a8e 100644 --- a/http-server-tck/src/main/java/io/micronaut/http/server/tck/ServerUnderTest.java +++ b/http-tck/src/main/java/io/micronaut/http/tck/ServerUnderTest.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.micronaut.http.server.tck; +package io.micronaut.http.tck; import io.micronaut.context.ApplicationContextProvider; import io.micronaut.core.annotation.Experimental; @@ -23,6 +23,7 @@ import io.micronaut.http.HttpResponse; import java.io.Closeable; +import java.net.URL; import java.util.Optional; /** @@ -33,6 +34,13 @@ @Experimental public interface ServerUnderTest extends ApplicationContextProvider, Closeable, AutoCloseable { + /** + * The property name used to signify we want to use a non-blocking client. + * + * This is used as the implementation varies for the javanet client. + */ + String BLOCKING_CLIENT_PROPERTY = "use.blocking.client"; + /* * Perform an HTTP request for the given request against the server under test and returns the the full HTTP response * @param request The {@link HttpRequest} to execute @@ -58,6 +66,21 @@ default HttpResponse exchange(HttpRequest request, Class bodyTyp return exchange(request, Argument.of(bodyType)); } + /* + * Perform an HTTP request for the given request against the server under test and returns the full HTTP response + * @param request The {@link HttpRequest} to execute + * @param bodyType The body type + * @param errorType The error type + * @param The request body type + * @param The response body type + * @param The error body type + * @return The full {@link HttpResponse} object + * @throws HttpClientResponseException when an error status is returned + */ + default HttpResponse exchange(HttpRequest request, Class bodyType, Class errorType) { + return exchange(request, Argument.of(bodyType), Argument.of(errorType)); + } + /* * Perform an HTTP request for the given request against the server under test and returns the full HTTP response * @param request The {@link HttpRequest} to execute @@ -69,8 +92,24 @@ default HttpResponse exchange(HttpRequest request, Class bodyTyp */ HttpResponse exchange(HttpRequest request, Argument bodyType); + /* + * Perform an HTTP request for the given request against the server under test and returns the full HTTP response + * @param request The {@link HttpRequest} to execute + * @param bodyType The body type + * @param The request body type + * @param The response body type + * @return The full {@link HttpResponse} object + * @throws HttpClientResponseException when an error status is returned + */ + HttpResponse exchange(HttpRequest request, Argument bodyType, Argument errorType); + @NonNull default Optional getPort() { return Optional.empty(); } + + @NonNull + default Optional getURL() { + return Optional.empty(); + } } diff --git a/http-server-tck/src/main/java/io/micronaut/http/server/tck/ServerUnderTestProvider.java b/http-tck/src/main/java/io/micronaut/http/tck/ServerUnderTestProvider.java similarity index 98% rename from http-server-tck/src/main/java/io/micronaut/http/server/tck/ServerUnderTestProvider.java rename to http-tck/src/main/java/io/micronaut/http/tck/ServerUnderTestProvider.java index 2ea500fb918..56ff92f2cd0 100644 --- a/http-server-tck/src/main/java/io/micronaut/http/server/tck/ServerUnderTestProvider.java +++ b/http-tck/src/main/java/io/micronaut/http/tck/ServerUnderTestProvider.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.micronaut.http.server.tck; +package io.micronaut.http.tck; import io.micronaut.core.annotation.Experimental; import io.micronaut.core.annotation.NonNull; diff --git a/http-server-tck/src/main/java/io/micronaut/http/server/tck/ServerUnderTestProviderUtils.java b/http-tck/src/main/java/io/micronaut/http/tck/ServerUnderTestProviderUtils.java similarity index 97% rename from http-server-tck/src/main/java/io/micronaut/http/server/tck/ServerUnderTestProviderUtils.java rename to http-tck/src/main/java/io/micronaut/http/tck/ServerUnderTestProviderUtils.java index ff9bdede0a8..f0ba5518268 100644 --- a/http-server-tck/src/main/java/io/micronaut/http/server/tck/ServerUnderTestProviderUtils.java +++ b/http-tck/src/main/java/io/micronaut/http/tck/ServerUnderTestProviderUtils.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.micronaut.http.server.tck; +package io.micronaut.http.tck; import io.micronaut.context.exceptions.ConfigurationException; import io.micronaut.core.annotation.Experimental; diff --git a/http-server-tck/src/main/java/io/micronaut/http/server/tck/TestScenario.java b/http-tck/src/main/java/io/micronaut/http/tck/TestScenario.java similarity index 99% rename from http-server-tck/src/main/java/io/micronaut/http/server/tck/TestScenario.java rename to http-tck/src/main/java/io/micronaut/http/tck/TestScenario.java index f86e1010c2f..5540eae6fa7 100644 --- a/http-server-tck/src/main/java/io/micronaut/http/server/tck/TestScenario.java +++ b/http-tck/src/main/java/io/micronaut/http/tck/TestScenario.java @@ -13,10 +13,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.micronaut.http.server.tck; +package io.micronaut.http.tck; import io.micronaut.core.annotation.Experimental; import io.micronaut.http.HttpRequest; + import java.io.IOException; import java.util.Map; import java.util.Objects; @@ -29,6 +30,7 @@ */ @Experimental public final class TestScenario { + private final String specName; private final Map configuration; diff --git a/http/src/main/java/io/micronaut/http/context/ClientContextPathProvider.java b/http/src/main/java/io/micronaut/http/context/ClientContextPathProvider.java index 9e91da355d4..0aec8c84bc7 100644 --- a/http/src/main/java/io/micronaut/http/context/ClientContextPathProvider.java +++ b/http/src/main/java/io/micronaut/http/context/ClientContextPathProvider.java @@ -23,6 +23,7 @@ * @author James Kleeh * @since 1.2.8 */ +@FunctionalInterface public interface ClientContextPathProvider { /** diff --git a/http/src/main/java/io/micronaut/http/context/ContextPathUtils.java b/http/src/main/java/io/micronaut/http/context/ContextPathUtils.java new file mode 100644 index 00000000000..b79cc7dfc6d --- /dev/null +++ b/http/src/main/java/io/micronaut/http/context/ContextPathUtils.java @@ -0,0 +1,52 @@ +/* + * Copyright 2017-2023 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.http.context; + +import io.micronaut.core.annotation.NonNull; +import io.micronaut.core.annotation.Nullable; +import io.micronaut.core.util.StringUtils; + +import java.net.URI; +import java.net.URISyntaxException; + +/** + * Utility class to work with context paths and URIs. + * @author Sergio del Amo + * @since 4.0.0 + */ +public final class ContextPathUtils { + + private ContextPathUtils() { + } + + @NonNull + public static URI prepend(@NonNull URI requestURI, @Nullable ServerContextPathProvider serverContextPathProvider) throws URISyntaxException { + return prepend(requestURI, serverContextPathProvider.getContextPath()); + } + + @NonNull + public static URI prepend(@NonNull URI requestURI, @Nullable ClientContextPathProvider clientContextPathProvider) throws URISyntaxException { + return prepend(requestURI, clientContextPathProvider.getContextPath().orElse(null)); + } + + @NonNull + public static URI prepend(@NonNull URI requestURI, @Nullable String contextPath) throws URISyntaxException { + if (StringUtils.isNotEmpty(contextPath)) { + return new URI(StringUtils.prependUri(contextPath, requestURI.toString())); + } + return requestURI; + } +} diff --git a/http/src/main/java/io/micronaut/http/context/ServerContextPathProvider.java b/http/src/main/java/io/micronaut/http/context/ServerContextPathProvider.java index e5cdb623833..059df28384c 100644 --- a/http/src/main/java/io/micronaut/http/context/ServerContextPathProvider.java +++ b/http/src/main/java/io/micronaut/http/context/ServerContextPathProvider.java @@ -23,6 +23,7 @@ * @author James Kleeh * @since 1.2.8 */ +@FunctionalInterface public interface ServerContextPathProvider { /** diff --git a/http/src/test/groovy/io/micronaut/http/context/ContextPathUtilsSpec.groovy b/http/src/test/groovy/io/micronaut/http/context/ContextPathUtilsSpec.groovy new file mode 100644 index 00000000000..91d96156186 --- /dev/null +++ b/http/src/test/groovy/io/micronaut/http/context/ContextPathUtilsSpec.groovy @@ -0,0 +1,32 @@ +package io.micronaut.http.context + +import spock.lang.Specification + +class ContextPathUtilsSpec extends Specification { + + void "ContextPathUtils.prependContextPath with serverContextPathProvider"() { + expect: + '/bar/foo' == ContextPathUtils.prepend(URI.create("/foo"), new ServerContextPathProvider() { + @Override + String getContextPath() { + "/bar" + } + }).toString() + } + + void "ContextPathUtils.prependContextPath with ClientContextPathProvider"() { + expect: + '/bar/foo' == ContextPathUtils.prepend(URI.create("/foo"), new ClientContextPathProvider() { + @Override + Optional getContextPath() { + Optional.of("/bar") + } + }).toString() + } + + void "ContextPathUtils.prependContextPath with path"() { + expect: + '/bar/foo' == ContextPathUtils.prepend(URI.create("/foo"),"/bar").toString() + '/foo' == ContextPathUtils.prepend(URI.create("/foo"), null as String).toString() + } +} diff --git a/settings.gradle b/settings.gradle index 450f9e520af..56ec53bfc70 100644 --- a/settings.gradle +++ b/settings.gradle @@ -36,10 +36,13 @@ include "graal" include "http" include "http-client-core" include "http-client" +include "http-client-jdk" +include "http-client-tck" include "http-netty" include "http-server" include "http-server-tck" include "http-server-netty" +include "http-tck" include "http-validation" include "inject" include "inject-groovy" @@ -64,8 +67,12 @@ include "websocket" include "test-suite" include "test-suite-geb" include "test-suite-helper" +include "test-suite-http-client-jdk-ssl" +include "test-suite-http-client-tck-netty" +include "test-suite-http-client-tck-jdk" include "test-suite-javax-inject" include "test-suite-jakarta-inject-bean-import" +include "test-suite-http-server-tck-jdk" include "test-suite-http-server-tck-netty" include "test-suite-kotlin" include "test-suite-kotlin-ksp" diff --git a/src/main/docs/guide/httpClient.adoc b/src/main/docs/guide/httpClient.adoc index 39e66d50c93..7ad8aa99148 100644 --- a/src/main/docs/guide/httpClient.adoc +++ b/src/main/docs/guide/httpClient.adoc @@ -1,15 +1,3 @@ -[TIP] -.Using the CLI -==== -If you create your project using the Micronaut CLI, the `http-client` dependency is included by default. -==== - Client communication between Microservices is a critical component of any Microservice architecture. With that in mind, Micronaut includes an HTTP client that has both a low-level API and a higher-level AOP-driven API. TIP: Regardless whether you choose to use Micronaut's HTTP server, you may wish to use the Micronaut HTTP client in your application since it is a feature-rich client implementation. - -To use the HTTP client, add the `http-client` dependency to your build: - -dependency:micronaut-http-client[] - -Since the higher level API is built on the low-level HTTP client, we first introduce the low-level client. diff --git a/src/main/docs/guide/httpClient/httpClientImplementations.adoc b/src/main/docs/guide/httpClient/httpClientImplementations.adoc new file mode 100644 index 00000000000..1ca30803a79 --- /dev/null +++ b/src/main/docs/guide/httpClient/httpClientImplementations.adoc @@ -0,0 +1 @@ +There are several implementations of the Micronaut HTTP Client. diff --git a/src/main/docs/guide/httpClient/httpClientImplementations/jdkHttpClient.adoc b/src/main/docs/guide/httpClient/httpClientImplementations/jdkHttpClient.adoc new file mode 100644 index 00000000000..24120a6a002 --- /dev/null +++ b/src/main/docs/guide/httpClient/httpClientImplementations/jdkHttpClient.adoc @@ -0,0 +1,14 @@ +To use an implementation based on https://openjdk.org/groups/net/httpclient/intro.html[Java HTTP Client], add the following dependency to your build: + +dependency:micronaut-http-client-jdk[] + +NOTE: This implementation of the Micronaut HTTP Client is available since Micronaut Framework 4.0. + +The implementation based on https://openjdk.org/groups/net/httpclient/intro.html[Java HTTP Client] does not support the following features: + +* <> support (we do support <>). +* Client Filters. +* Streaming support. +* Multipart requests. + +If you require any of these, we recommend you use the <>. diff --git a/src/main/docs/guide/httpClient/httpClientImplementations/nettyHttpClient.adoc b/src/main/docs/guide/httpClient/httpClientImplementations/nettyHttpClient.adoc new file mode 100644 index 00000000000..cfc9e66a1d7 --- /dev/null +++ b/src/main/docs/guide/httpClient/httpClientImplementations/nettyHttpClient.adoc @@ -0,0 +1,3 @@ +To use an implementation based on https://netty.io[Netty], add the following dependency to your build: + +dependency:micronaut-http-client[] diff --git a/src/main/docs/guide/httpClient/lowLevelOrHighLevel.adoc b/src/main/docs/guide/httpClient/lowLevelOrHighLevel.adoc new file mode 100644 index 00000000000..a469afb7b3c --- /dev/null +++ b/src/main/docs/guide/httpClient/lowLevelOrHighLevel.adoc @@ -0,0 +1 @@ +Since the higher level API is built on the low-level HTTP client, we first introduce the low-level client. diff --git a/src/main/docs/guide/introduction/whatsNew.adoc b/src/main/docs/guide/introduction/whatsNew.adoc index 6e9f8364b23..901f8c6fdc2 100644 --- a/src/main/docs/guide/introduction/whatsNew.adoc +++ b/src/main/docs/guide/introduction/whatsNew.adoc @@ -11,6 +11,8 @@ Micronaut Framework 4.x supports https://groovy-lang.org/releasenotes/groovy-4.0 * <> * <> +* <> + ==== Injection of Maps It is now possible to inject a `java.util.Map` of beans where the key is the bean name. The name of the bean is derived from the <> or (if not present) the simple name of the class. diff --git a/src/main/docs/guide/toc.yml b/src/main/docs/guide/toc.yml index b2ac3db2ef0..fd9e464d418 100644 --- a/src/main/docs/guide/toc.yml +++ b/src/main/docs/guide/toc.yml @@ -160,6 +160,12 @@ httpServer: graphql: GraphQL Support httpClient: title: The HTTP Client + httpClientImplementations: + title: HTTP Client Implementations + nettyHttpClient: HTTP Client based on Netty + jdkHttpClient: + title: HTTP Client based on the Java HTTP Client + lowLevelOrHighLevel: Low-Level and High-Level APIs lowLevelHttpClient: title: Using the Low-Level HTTP Client clientBasics: Sending your first HTTP request diff --git a/test-suite-groovy/build.gradle b/test-suite-groovy/build.gradle index 5561a1eee2f..9961729f7f4 100644 --- a/test-suite-groovy/build.gradle +++ b/test-suite-groovy/build.gradle @@ -22,6 +22,7 @@ repositories { dependencies { testImplementation libs.managed.netty.codec.http testImplementation project(":http-client") + testImplementation project(":http-client-jdk") testImplementation project(":inject-groovy") testImplementation project(":http-server-netty") testImplementation project(":jackson-databind") diff --git a/test-suite-groovy/src/test/groovy/io/micronaut/docs/client/MultiClientSpec.groovy b/test-suite-groovy/src/test/groovy/io/micronaut/docs/client/MultiClientSpec.groovy new file mode 100644 index 00000000000..f08efbc1237 --- /dev/null +++ b/test-suite-groovy/src/test/groovy/io/micronaut/docs/client/MultiClientSpec.groovy @@ -0,0 +1,55 @@ +package io.micronaut.docs.client + +import io.micronaut.context.annotation.Property +import io.micronaut.context.annotation.Requires +import io.micronaut.http.HttpRequest +import io.micronaut.http.MediaType +import io.micronaut.http.MutableHttpRequest +import io.micronaut.http.annotation.Controller +import io.micronaut.http.annotation.CookieValue +import io.micronaut.http.annotation.Get +import io.micronaut.http.client.HttpClient +import io.micronaut.http.client.annotation.Client +import io.micronaut.http.client.jdk.DefaultJdkHttpClient +import io.micronaut.http.client.jdk.JdkHttpClient +import io.micronaut.http.client.netty.DefaultHttpClient +import io.micronaut.http.cookie.Cookie +import io.micronaut.test.extensions.spock.annotation.MicronautTest +import jakarta.inject.Inject +import spock.lang.Specification + + +@MicronautTest +@Property(name = "spec.name", value = "MultiClientSpec") +class MultiClientSpec extends Specification { + + @Inject + @Client("/") + HttpClient nettyClient + + @Inject + @Client("/") + JdkHttpClient jdkClient + + void "can specify client implementation if both are on the classpath"() { + expect: + nettyClient.class == DefaultHttpClient + jdkClient.class == DefaultJdkHttpClient + nettyClient.toBlocking().retrieve(getRequest()) == "ok bar" + jdkClient.toBlocking().retrieve(getRequest()) == "ok bar" + } + + private static MutableHttpRequest getRequest() { + HttpRequest.GET("/multi-client").cookie(Cookie.of("foo", "bar")) + } + + @Controller + @Requires(property = "spec.name", value = "MultiClientSpec") + static class MultiClientController { + + @Get(uri = "/multi-client", produces = MediaType.TEXT_PLAIN) + String multiClient(@CookieValue("foo") String foo) { + return "ok " + foo + } + } +} diff --git a/test-suite-http-client-jdk-ssl/build.gradle.kts b/test-suite-http-client-jdk-ssl/build.gradle.kts new file mode 100644 index 00000000000..d113890fff9 --- /dev/null +++ b/test-suite-http-client-jdk-ssl/build.gradle.kts @@ -0,0 +1,19 @@ +plugins { + id("io.micronaut.build.internal.convention-test-library") +} + +description = "Test suite for the Java.net HTTP client with SSL where hostname resolution is disabled" + +dependencies { + testImplementation(projects.httpServerNetty) + testImplementation(projects.httpClientJdk) + testImplementation(libs.spock) + testImplementation(libs.managed.reactor) + testImplementation(projects.jacksonDatabind) +} + +tasks.named("test") { + useJUnitPlatform() + systemProperty("jdk.httpclient.HttpClient.log", "all") + systemProperty("jdk.internal.httpclient.disableHostnameVerification", "true") +} diff --git a/test-suite-http-client-jdk-ssl/src/test/groovy/io/micronaut/http/client/jdk/SslRefreshSpec.groovy b/test-suite-http-client-jdk-ssl/src/test/groovy/io/micronaut/http/client/jdk/SslRefreshSpec.groovy new file mode 100644 index 00000000000..d04df8f43a3 --- /dev/null +++ b/test-suite-http-client-jdk-ssl/src/test/groovy/io/micronaut/http/client/jdk/SslRefreshSpec.groovy @@ -0,0 +1,103 @@ +package io.micronaut.http.client.jdk + +import io.micronaut.context.ApplicationContext +import io.micronaut.context.annotation.Requires +import io.micronaut.context.env.PropertySource +import io.micronaut.context.event.ApplicationEventPublisher +import io.micronaut.core.type.Argument +import io.micronaut.http.HttpRequest +import io.micronaut.http.HttpResponse +import io.micronaut.http.HttpStatus +import io.micronaut.http.annotation.Controller +import io.micronaut.http.annotation.Get +import io.micronaut.http.client.HttpClient +import io.micronaut.http.server.netty.NettyHttpRequest +import io.micronaut.runtime.context.scope.refresh.RefreshEvent +import io.micronaut.runtime.server.EmbeddedServer +import io.netty.handler.ssl.SslHandler +import spock.lang.Shared +import spock.lang.Specification + +import java.security.cert.X509Certificate + +class SslRefreshSpec extends Specification { + + @Shared List ciphers = ['TLS_RSA_WITH_AES_128_CBC_SHA', + 'TLS_RSA_WITH_AES_256_CBC_SHA', + 'TLS_RSA_WITH_AES_128_GCM_SHA256', + 'TLS_RSA_WITH_AES_256_GCM_SHA384', + 'TLS_DHE_RSA_WITH_AES_128_GCM_SHA256', + 'TLS_DHE_RSA_WITH_AES_256_GCM_SHA384', + 'TLS_DHE_DSS_WITH_AES_128_GCM_SHA256', + 'TLS_DHE_DSS_WITH_AES_256_GCM_SHA384'] + @Shared Map config = [ + 'spec.name': 'SslRefreshSpec', + 'micronaut.ssl.enabled': true, + 'micronaut.server.ssl.port':-1, + 'micronaut.server.ssl.client-authentication': 'NEED', + 'micronaut.server.ssl.key-store.path': 'classpath:certs/server.p12', + 'micronaut.server.ssl.key-store.password': 'secret', + 'micronaut.server.ssl.trust-store.path': 'classpath:certs/truststore', + 'micronaut.server.ssl.trust-store.password': 'secret', + 'micronaut.server.ssl.ciphers': ciphers, + 'micronaut.http.client.ssl.client-authentication': 'NEED', + 'micronaut.http.client.ssl.key-store.path': 'classpath:certs/client1.p12', + 'micronaut.http.client.ssl.key-store.password': 'secret', + 'micronaut.http.client.ssl.insecure-trust-all-certificates': true, + ] + + void "test ssl refresh"() { + EmbeddedServer embeddedServer = ApplicationContext + .builder() + .propertySources(PropertySource.of(config)) + .run(EmbeddedServer) + + HttpClient client = embeddedServer.applicationContext + .createBean(HttpClient, embeddedServer.getURI()) + + when: + def response = client.toBlocking().exchange(HttpRequest.GET('/ssl/refresh'), Map) + + then: + response.status() == HttpStatus.OK + response.body().ciphers == ciphers + response.body().subjectDN == 'CN=test.example.com, OU=IT, O=Whatever, L=Munich, ST=Bavaria, C=DE, EMAILADDRESS=info@example.com' + + when: + config.putAll( 'micronaut.server.ssl.key-store.path': 'classpath:keystore.p12', + 'micronaut.server.ssl.key-store.password': 'foobar', + 'micronaut.server.ssl.key-store.type': 'PKCS12', + 'micronaut.server.ssl.ciphers': ciphers[0..4]) + def diff = embeddedServer.applicationContext.environment.refreshAndDiff() + embeddedServer.applicationContext + .getBean(Argument.of(ApplicationEventPublisher, RefreshEvent)) + .publishEvent(new RefreshEvent(diff)) + response = client.toBlocking().exchange(HttpRequest.GET('/ssl/refresh'), Map) + + then: + response.status() == HttpStatus.OK + response.body().ciphers == ciphers[0..4] + response.body().subjectDN == 'CN=example.local, OU=IT Department, O=Global Security, L=London, ST=London, C=GB' + + cleanup: + client.close() + embeddedServer.close() + } + + @Controller("/ssl/refresh") + @Requires(property = 'spec.name',value = 'SslRefreshSpec') + static class TestSslRefresh { + @Get + HttpResponse> test(HttpRequest request) { + def pipeline = (request as NettyHttpRequest).getChannelHandlerContext().pipeline() + def sslHandler = pipeline.get(SslHandler) + + def engine = sslHandler.engine() + X509Certificate cert = engine.getSession().getLocalCertificates()[0] + return HttpResponse.ok([ + ciphers: engine.enabledCipherSuites, + subjectDN: cert.subjectDN.toString() + ] as Map) + } + } +} diff --git a/test-suite-http-client-jdk-ssl/src/test/groovy/io/micronaut/http/client/jdk/SslStaticCertSpec.groovy b/test-suite-http-client-jdk-ssl/src/test/groovy/io/micronaut/http/client/jdk/SslStaticCertSpec.groovy new file mode 100644 index 00000000000..fffc7190c23 --- /dev/null +++ b/test-suite-http-client-jdk-ssl/src/test/groovy/io/micronaut/http/client/jdk/SslStaticCertSpec.groovy @@ -0,0 +1,80 @@ +package io.micronaut.http.client.jdk + +import io.micronaut.context.ApplicationContext +import io.micronaut.context.annotation.Requires +import io.micronaut.context.env.Environment +import io.micronaut.core.io.socket.SocketUtils +import io.micronaut.http.HttpRequest +import io.micronaut.http.HttpResponse +import io.micronaut.http.annotation.Controller +import io.micronaut.http.annotation.Get +import io.micronaut.http.client.HttpClient +import io.micronaut.runtime.server.EmbeddedServer +import reactor.core.publisher.Flux +import spock.lang.Shared +import spock.lang.Specification + +class SslStaticCertSpec extends Specification { + + @Shared + String host = Optional.ofNullable(System.getenv(Environment.HOSTNAME)).orElse(SocketUtils.LOCALHOST) + + ApplicationContext context + EmbeddedServer embeddedServer + HttpClient client + + void setup() { + context = ApplicationContext.run([ + 'spec.name': 'SslStaticCertSpec', + 'micronaut.ssl.enabled': true, + 'micronaut.ssl.keyStore.path': 'classpath:keystore.p12', + 'micronaut.ssl.keyStore.password': 'foobar', + 'micronaut.ssl.keyStore.type': 'PKCS12', + 'micronaut.ssl.protocols': ['TLSv1.2'], + 'micronaut.server.ssl.port': -1, + 'micronaut.ssl.ciphers': ['TLS_RSA_WITH_AES_128_CBC_SHA', + 'TLS_RSA_WITH_AES_256_CBC_SHA', + 'TLS_RSA_WITH_AES_128_GCM_SHA256', + 'TLS_RSA_WITH_AES_256_GCM_SHA384', + 'TLS_DHE_RSA_WITH_AES_128_GCM_SHA256', + 'TLS_DHE_RSA_WITH_AES_256_GCM_SHA384', + 'TLS_DHE_DSS_WITH_AES_128_GCM_SHA256', + 'TLS_DHE_DSS_WITH_AES_256_GCM_SHA384'], + 'micronaut.http.client.ssl.insecure-trust-all-certificates': true + ]) + embeddedServer = context.getBean(EmbeddedServer).start() + client = context.createBean(HttpClient, embeddedServer.getURL()) + } + + void cleanup() { + client.close() + context.close() + } + + void "expect the url to be https"() { + expect: + embeddedServer.getURL().toString() == "https://${host}:${embeddedServer.port}" + } + + void "test send https request"() { + when: + Flux> reactiveSequence = Flux.from(client.exchange( + HttpRequest.GET("/ssl/static"), String + )) + HttpResponse response = reactiveSequence.blockFirst() + + then: + response.body() == "Hello" + } + + @Requires(property = 'spec.name', value = 'SslStaticCertSpec') + @Controller('/') + static class SslStaticController { + + @Get('/ssl/static') + String simple() { + return "Hello" + } + + } +} diff --git a/test-suite-http-client-jdk-ssl/src/test/resources/certs/client1.p12 b/test-suite-http-client-jdk-ssl/src/test/resources/certs/client1.p12 new file mode 100644 index 0000000000000000000000000000000000000000..ef6e965c1663375d0bbd2c03fe8b6e712dea1ccc GIT binary patch literal 2646 zcmY+^XH?T`5(nUaAPGGnM0%tKrAR`g2?Ajw8m>|JRmmE z6A~u}n8XP=ia}@+IQX9t^a%;l;7(XzG9(?opJxFi&csuH8mPUsP#uj`P<6e+9>E< za65ki8!0APV`8}|#iv5#B|UXqVn111V`|~Hx7^@d&qBwyA527!TRhDwdiAE5mVAnC zuJ7@5J5iD6fl0*GwTk3crv4;8`o3b&(c=4-@$kXxcp$c`FODz$RTUCbYxG z*Ayu`Md_U;_Rn8mC`XrWO)TfhmuoVEUg&5~o^3&eYJL8#RhOEV+pfSZaXE<5XjS37 zhhzj8S-uU8#El(@n!khaFU^~n`^O_xj=9rntDA3HfAEuRjiORJ+oOVwj2=!4WMzLP zTV$p7@n1SdiAY9_%R`;rA6o+PY&G5YSxf0$`-)SkHm%JOI3%Sw;e}Ubtn8{z3$k|= z1E`kem|qzQ{n~r_3^uhBz5<1-tM84ZY&O~_iLq!##)vE*gR>%w6;#|@Wbzp!TKWirB^>YMz@e;(O&Jz zofO#2U@W2BQVo@!A#V2vtbL>lzAE-`) zo~b-JXR|+Xwr7B|+c{7biz~wzR@8I>c_T=IStRaKZCOcKm7A?1uB6Dj)=kP4KCZb{ zX4$v_?YB>B@ z?fCrcL)^H8a3YG>FK*UlxSGW(&d*;j#WYCBCp8(ITK#S^seRr;$=rG{0Q`=e{&TW_PE6Bq+>}+@X6=HUa7vPFK&!qP}TVEulNQiSywuMy$4BK8qL{SQm5fJ~Sd! z#m(9iqQnBk?YnwJP}Zd%r%Zk0Sa4#3iB4Lctu96da!bDn)4veIrpo4OKEo(+0yW>n zS**&owx4Zh@hOvQ40n&Q!(}Do>(uD2Qg_|^;!m8T%yb7LIuUaFW!Wy-p0CrKw=eyr z^F^-LFRJCF3t;ab#3bSc#TYYHuumxZD21SP(bVlYeMrdjsc-h9HPC?-!$9p3y>W4- zZwi{Zs%XZ46SHC&AVV0DF}{JZG}N;*A3F^39=ibDI&9-gL6ab){{iL+3E~eXLA;OR zn@7(T%Jm;^I6*8&F8w$HY5D&YV*OhoNez658pDA7Z-u}la3?rtc=I=23hOUQ_$_Pq zPKoMLgI0<3O%*b1b7$}|HR}qHaa$LF@Y&$BoHjJBbw$PngC(*q5Cp zOr)UW0wc_Ofj=SilcYJj@ca)e^7{d>)~cvqd8Zx3OyoH7MC&mU##-kbupQ`<$s3xr z>4&i*It_cedDI-@;WkyTH7)B`$|nV9tn_}v27caW-w8n2L<*@8zia^RH27qE%9O`6 z?YK>@4#bj>ky;JQe+z5QSe|Cp7>y2c!&kHR24Iqt(3GKR>+lt@epOL_+`Oy zZQ(H6s>o0Mqu}A;>Ydh*o~&z_2QOo{HJs0msCBbmTv>IiPp_qGI z!mk-&+e^O8MXAV+C!Eb;pv6|9b!N`n6yW|d>5R$ReMV+KAJ)a~Gdr2LpG>h0Rq+pO zTMTeb?i^+R;cRQ`h!5HEnai<9hmGC_OB8Cor@0p1U(CLn81Kq$x=#O4(LEp}Kuz2~ zcegI1Kjq5B-+9pms%dNT-UCXd!6@62v0N7lBGYTsvNEM6s;P@omU8mVxJE4Yyji z{GX-(-xJMRi2D~p?`fpts=DWAR#r>V0@*aX!9-udS#kCurQ=ABtoo#H(vR!C)`cVK zv_DJr0xK%R_{&MC6OQuD*3OAACih%+iiPJ7%C|T;*f>8fvNGlD%s1kft&<<}JLD0@Xxh^%_OJ7!n|a$;N&yljn>uh)nz>{^;11Uh&`T^J6+ z_XE^j62G4F%Z~{2yqPZhbTx-(^fDAKr4b^}iHAXWuWD_k+nW@R47MZpC^ZVrvWAmh zZP|Hm2F*(y6a9Kz(?sA6Rz$l4yrGrCn^%|KwrTQt2km@$jD+3eRbrtUBk&&X)yjtA z25X;0OgAHzQnoX3FATm<1yvq$Csbpqpa|WdTXIAVRpYU~{iq&TxXlE@8w#AK6_Z0w z(XtEf46ljroO#(GrJAd8B?6XE^r{wCAcX1f9XZIEmv59A-C$Ju55;UC)Q&l zgq!fv#NtG*%cRQYkIgfk>}yF6LKIT<8Fds+N$J83{!X81LPC3}S+xOvo_wPQ<`$Bm zS8=BOmYU(CIq~$0AU%T+r)7?x@{Hr|Y?XR4V~}ohoz`_D8&;wnZhhn7)WEFc%50At z{$Gu638QSchuoN#1YSGmCdIyvfm3EwmCMwid1ny%lubIj^iqVVEB?WFNAhI715bH)TizmOb!^O{zmKlvjkVcoInFk zfCu0X5CJbB08jyxk17%HKdQ<{Jp^z7JOQ7h4dT(?3GfCy(JE*uGzS!-2xDatg#ci< lWvE5GL_)q%@i=C*TjQ#{`1(`~n0+IjLnG%#O7`E({so_f)F1!= literal 0 HcmV?d00001 diff --git a/test-suite-http-client-jdk-ssl/src/test/resources/certs/server.p12 b/test-suite-http-client-jdk-ssl/src/test/resources/certs/server.p12 new file mode 100644 index 0000000000000000000000000000000000000000..bf0f7946486d83b5d98755a5f8e45c5e7f17f7b1 GIT binary patch literal 2606 zcmY+^cQ_mR8VB%%2(c;%rKnYV#H`jTLNrEew3Mn*BesOrED?%h?_FZ6)oQG2Y}In? z)ZR*|)#7Mv*Lj|M@45Gn_xJvu@B2RQpC34$R*V8j3CBZzf+1oNx)Hw_fYd+&9C(W)L}*#d>uDDqXwWv%AY;2IQb~ z{rY2Ir5nf4(l_H+$VnaS#WXddoSAF9=TH6W=rvu+T}p1y4>fL*AEi~N<>}!n#q-2+ z-2!Sp+5V22WL>vHQG>ZrY#|s2Y4CAFAfrrweC+5MptEA|krY0I4wPxKwZq za_q#85=Re!-?~rFIy(;hqfa8lKUQ{$xl~vz-1&eT*@{Y(}X})TJLNQzO z4p;wk#?BYdyyfmfRrx#!7<|`a{CBXRh}IO?K&oq1S^rDH0V?P?f0IHXi20M-lRw?L zhU|M4D(rd}k9_xyBv5Ep58mGu?s_B{ZdBqJ?~=!y^08npR}@brEo@ZpXU*`B8*zGA zPGsh^{_Ch`50rf}U2`I*5)0oXebv6*NP2U(NG0-$xeh7eyK;Yq6%|C9*QOt{>fx*| zf}QpY9o}ZF>1nHzdYA;5QW6WyJdqc$ww)t7#XGl}YI4E>|Y7SA9oRHHh z_uTFP{qk%Ari!FzMd@N;WlBEzTX7PeWLM%MRxC|jS5yy4jF=m6d~sR>xs_ZQ>?^*m z&ZPo62;4VuYfwf9&%PuXy?)GB^h9FtWwBS~ z1@>6BB6DA5J6!6O$)21a@ijeo8KBM-H83Igw1juaV=lYj3b|b1e}6EGTHo4jg+m$L zu6q0yVM)+||DaL@qgM-{;f^ua9Dr^ zrF_@l+JNw&&+^l=fGWosyAE!r<*SUY0#YH%dg&XtcF_YnR!yYTE zUwKyx=k#xgwT*OoF=FI5TKSq^ETQD%sRVpR=0*-lJFW;zE^SdXi97Dv`mgMaimwW@A(pjC~#u(|isF8sZs3(OoPfVy&CCt}p{NC`H zySt&iA2Hl(KcqZ9l~i3+^b;9(;v9p>3^ZvQ_knrV2M(6oL~U?9?uzFQ^w0UD1)$=! zUG5S!q~9#Va1qN>Y6F<$)$) zB3?QdIeqi9Zp_b=E(fcr!qqiVNWQA{4@24hWEeC213))R=7PkQ_7H#xZvFC_s24SnFLwQ=xwWp7LlR2T9hWBtD4Dd2(Nr9-8{`P$N(+YczXz z*Z+%!#bMq_+~MKF=9crnaz9TD5p`w(=F^F#pBu5kvoVT+DuE-SkJJmknhk@E8BUy} z{f|4suE$Q83to`T;@|lRw#xv2H-&1sG|RnvKC;k07Sy(ejAYcMWGS6GudU5~l&qs4 z@N2~@0^sQTL`uPCs-vab>xJZm;-DBuf)F0K3H3p2avi-1%9ZVzTCv0=z0UUGdGU~;cITM)nc`M@oKf9`rV~-;i(JwBb^*})Ys7(K z9B&NS_jOCoV%4ao}_YnONGpgR}Y;`0iMX^nb^ zWvJ4;NQ-n@xR}LADz)t|tT`clqr#qiC0A8t3^#IWQ!PxcWi4SxJN)1Q66uBuG1cF7 zYmzVxM%RUXyv8Ei7Btp6mXM@}yvMPX8ez_HH;;rEgl4&;ktepy#oA+@SH?6qbLckG z&7ujr)S9_UMhr9>nIyY{9lC;hOmMlYioJpHgF`$Un>)!p<3gX!?fmL#u12*B;pc5Z z%w)#>!oE*1J8`6MD?%*^L-e}rL&uj;#{TfC9fJY$+N!K{X-#)zFM1_!&EIL zmfSkZ0A|^fePu&^f$bG>!~I-yk=r)TG)+~s=S!3y&=zTHZ6N9!z#o7r?+XaosLBNY zRUQoankKn0tX~0s*D`L~u6Fn#$b|=P07@i7)u4(gw>}G7wGz&6&ohi2ndXt*>m$@Q z7i*~*r7z4|$+MN^?W zqP(u4ak#2kEAVt(D&x#QK!>%YL~Y%kHVSOJJ{v+~(xFs8opsv2z2y z87ZZh&5sm~=~ZsS#y&ec)F=KQIB9xmZ(H7!orj9oA;5CFw3w#98*fZxe1Rqc7{LSjW0C6+4 Aq5uE@ literal 0 HcmV?d00001 diff --git a/test-suite-http-client-jdk-ssl/src/test/resources/certs/truststore b/test-suite-http-client-jdk-ssl/src/test/resources/certs/truststore new file mode 100644 index 0000000000000000000000000000000000000000..7f89f28be9963df42bd99789eb2f56091ee56754 GIT binary patch literal 1074 zcmezO_TO6u1_mY|W(3nL={W^PKu+$iiigt}SR?dI4J;WLn7^y9~ zrFogj8HR!e{2&1?9**#g#FEsq)FPM>+&oO4A%-FbLLhl&9&RLC4CKUl4J{1~3=Itp zOf3z}qr`cQfLsF$D0dLKw24s(IT#sP8JL?G`56qF7`d357#SIMs4bAJl=ZJ@cqsiU zugKf{THlsgHXE$VC(S*Ue_2r6&BHKm?mJnvra4@u{1KhnT8V5aV$+M)Om5z4#9XKt z-m?0kg4*n?SN9{$vtGQK?BcB`zF^X6hSj@;^9^FAZ=C(0qD$y*?A!MXr}7rr?b`F> zyp3(>EceAq*4J%=KdzSyQMEaD`jzaFe0AUKso_UwUKi<>j-GpPPOSRlSu?zNR~zi9 z?($rruDaxT_!TB~1$&cJ^R3e+{rsBn{<-Hao+s-aZPF9h@4dPF%4~0orz$h$L>4YD zj`kJ}*{gh*bN!X|{?Ze5`k!^)cI;mC-&M%#hsn#D*?->mOqHE;owI26$r$5|Rwiaf z2FAs~27$nY#T+Wj$0Eiea*(_7PJi~*l}1ODS$74#*Zs&>ddNT?B(2OMVIbCkT>&T^ z$qF+v{%2t|U+i1dJ`GvUw*T_y zlrtP}mPN3XZqQk|;8qpuWB--Omr|eezY3MRTVQtoef<2%!v2yQ^CX2kcbXl|$~(H< z&ODEMw|bK1>V{2;ilyz2@0P^hS(crxK1Cw#xirVhY>}gNMai4?8Xjiq+{CYYt*+@e zL%7q&&GSnHyua&gub;MZMndXPBZM|e2#Q!WO01ekZRs&$)UGOGby=Y{Evn{esZoTgJ!;Qb zm6)XxHLLb4s^zxNbMHO(zWBfS{+{Rm@`J*IDro_9C_E^L0VEcs7j*;yFamP%Aa@`h z;Gx`dI3w6sh# zVR$-{6wk8M>{^}mV?#QDt2%N*z?C@9`@QBK1I!1L=pf+O2XtJU)~J!NdG7uv52qn> z-LflRH1|`R3$kR4NiEjWx<=73O*LAW zYNN2YadFqrV9Ia^7V9eEA+amyQhX)3>HsdQ%e1I49s$xEbre1oRD_Eg3=H0H<~U ze*Q5p5JIOh5qt8{WDL5Mzfpie#=IMJ5_fN#|5znFlg!*)-^G3JykLdiq=W}%aki^8 z?F!UJ$&@uE+`{s(uZz6na~aV(4taIp=F&g0`A2n>C|e^z&CP)9tYeF~W2!SMU<(aK zl6l&FQ&jinMzF&lKMqL}-1%_)+m}+;4!ItM?<7-E^hUh;mdH4Q=gE<)#gdw5bAm#D z?aJiHK>esx1gy{(M6r9bhP#JF^l6pU9A0Ibns>C}4LL|8#k~u`hRBivr;(p}cHM$8 z1nck?xAZpWJ%SNrwhFV&vtgySV+=U&vOUmXjJN52IXEVilk~PM$L09#(f4clZ0TNE zemQRvL+r%Nh(28Y$(JS{$?DlJSu_YOPxZFi6zEaU+((MZv%L|BT&T3nSo~M2JB4xB zcWtKDsb(V3?2j47uID3`VNeEY&fSbP+&&-IYzk}IAG$P8+Px`^8GIPQssZVROBRy1 z1VDOc%JoADln^O}l;~Uhv*THoj+XJKEaus*gpt_=gW#z41n_afDM|2YX0kRbi_GkO zo#$ZM5aXLWqNfYRLF_($bWQPSe2>C-s}^oB+DDS zwBPU2ujdULF>VY-b`t^Pj9pyZu%Xjv`?;_5rr!S^IUKXuxmzQg+!A-FPS7TQeao`+kP3LT9P}w~WWPKFwC6n7H@qOCqho zA@D=QsjWH2d>o@bNm@!%c97N>D(ir%3TD5Ixb8s}WSr?6j>W6|RR8eFdwi(M`8ny4 zq1&^U$$VEN&*huLGAmpIvRidfHAanP#wi*rN#?XOJu;V3-4#{U42i>H?X z;^`%SWwBo;%)tB~4!{7~Uv6yua%1iPl%V^q#M^$&iv2a}!fz#jcpx#Y(LQ6k8C#L` zV+yKvR42rJmWu91NaiWwIU5KnwRF72gs(@DREBH4pXHIBa z5I}?SkaY&p2ahe4sP*FViP|cKYfW@G#bvKAxY&N_$J+Vg@}`H45rA{TliRpvS>*?w zKQ@<=O{Rjw&;ji?w2~puFaVyc$#cWHV?M@wRn8^hUkFanaF~`*x$a9eXbW{F-8MK z(26BXx+S=QRb|*pPQbaVz>4ECNIw4BEcg2DY84f|t+mz4eFKDcK! zGR$-xGlkq1JqTt8WLK(l^-)stq1pWN`U7XLH9~1B}gS%8*H_%7GPY&pPk+yO-zfNn+YsJfJ6Tc#5OC;y}iS`_uHxYwR4yG2&f5LMM z@r%~VNXpgix+k(h_#%)R8(?~6Y_|jl{-H- zpkAF~Hfw7?D*tkX?{jzls5t&(WNMAhJW3Fk-L?yM`)PLCuoYXq+Gb_uo@cnSH^M|`vc?}1t=_bD0X5B5#L;zk6HF@62I^LOp*`x?_tN{FupTg54bVxn~~)(wo#C2<&KpPOjaxiecWlRxacBYp~>gZU4P|ME%%uH&_8M3SX_+p615G*sL!<7QL>tI)YyYxsYE73w-d~PhJ|s1KiRlPBIU + + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + + + diff --git a/test-suite-http-client-tck-jdk/build.gradle.kts b/test-suite-http-client-tck-jdk/build.gradle.kts new file mode 100644 index 00000000000..6c83d3bb603 --- /dev/null +++ b/test-suite-http-client-tck-jdk/build.gradle.kts @@ -0,0 +1,15 @@ +plugins { + id("io.micronaut.build.internal.convention-test-library") +} + +dependencies { + testImplementation(projects.httpServerNetty) + testImplementation(projects.httpClientJdk) + testImplementation(projects.httpClientTck) + testImplementation(libs.junit.platform.engine) +} + +tasks.named("test") { + useJUnitPlatform() + // systemProperty("jdk.httpclient.HttpClient.log", "all") // Uncomment to enable logging +} diff --git a/test-suite-http-client-tck-jdk/src/test/java/io/micronaut/http/client/tck/jdk/tests/JdkHttpMethodTests.java b/test-suite-http-client-tck-jdk/src/test/java/io/micronaut/http/client/tck/jdk/tests/JdkHttpMethodTests.java new file mode 100644 index 00000000000..2e9b5876bb2 --- /dev/null +++ b/test-suite-http-client-tck-jdk/src/test/java/io/micronaut/http/client/tck/jdk/tests/JdkHttpMethodTests.java @@ -0,0 +1,16 @@ +package io.micronaut.http.client.tck.jdk.tests; + +import org.junit.platform.suite.api.SelectPackages; +import org.junit.platform.suite.api.Suite; +import org.junit.platform.suite.api.SuiteDisplayName; + +/** + * Java HTTP Client + */ +@Suite +@SelectPackages("io.micronaut.http.client.tck.tests") +@SuiteDisplayName("HTTP Client TCK for the HTTP Client Implementation based on Java HTTP Client") +@SuppressWarnings("java:S2187") // This runs a suite of tests, but has no tests of its own +public class JdkHttpMethodTests { +} + diff --git a/test-suite-http-client-tck-jdk/src/test/resources/META-INF/services/io.micronaut.http.tck.ServerUnderTestProvider b/test-suite-http-client-tck-jdk/src/test/resources/META-INF/services/io.micronaut.http.tck.ServerUnderTestProvider new file mode 100644 index 00000000000..32597c3d661 --- /dev/null +++ b/test-suite-http-client-tck-jdk/src/test/resources/META-INF/services/io.micronaut.http.tck.ServerUnderTestProvider @@ -0,0 +1 @@ +io.micronaut.http.tck.EmbeddedServerUnderTestProvider diff --git a/test-suite-http-client-tck-jdk/src/test/resources/logback.xml b/test-suite-http-client-tck-jdk/src/test/resources/logback.xml new file mode 100644 index 00000000000..8fef0ca4384 --- /dev/null +++ b/test-suite-http-client-tck-jdk/src/test/resources/logback.xml @@ -0,0 +1,16 @@ + + + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + + + diff --git a/test-suite-http-client-tck-netty/build.gradle.kts b/test-suite-http-client-tck-netty/build.gradle.kts new file mode 100644 index 00000000000..09bfd775ca4 --- /dev/null +++ b/test-suite-http-client-tck-netty/build.gradle.kts @@ -0,0 +1,14 @@ +plugins { + id("io.micronaut.build.internal.convention-test-library") +} + +dependencies { + testImplementation(projects.httpServerNetty) + testImplementation(projects.httpClient) + testImplementation(projects.httpClientTck) + testImplementation(libs.junit.platform.engine) +} + +tasks.named("test") { + useJUnitPlatform() +} diff --git a/test-suite-http-client-tck-netty/src/test/java/io/micronaut/http/client/tck/netty/tests/NettyHttpMethodTests.java b/test-suite-http-client-tck-netty/src/test/java/io/micronaut/http/client/tck/netty/tests/NettyHttpMethodTests.java new file mode 100644 index 00000000000..42e4c82e3ad --- /dev/null +++ b/test-suite-http-client-tck-netty/src/test/java/io/micronaut/http/client/tck/netty/tests/NettyHttpMethodTests.java @@ -0,0 +1,12 @@ +package io.micronaut.http.client.tck.netty.tests; + +import org.junit.platform.suite.api.SelectPackages; +import org.junit.platform.suite.api.Suite; +import org.junit.platform.suite.api.SuiteDisplayName; + +@Suite +@SelectPackages("io.micronaut.http.client.tck.tests") +@SuiteDisplayName("HTTP Client TCK for the HTTP Client Implementation based on Netty") +@SuppressWarnings("java:S2187") // This runs a suite of tests, but has no tests of its own +public class NettyHttpMethodTests { +} diff --git a/test-suite-http-client-tck-netty/src/test/resources/META-INF/services/io.micronaut.http.tck.ServerUnderTestProvider b/test-suite-http-client-tck-netty/src/test/resources/META-INF/services/io.micronaut.http.tck.ServerUnderTestProvider new file mode 100644 index 00000000000..32597c3d661 --- /dev/null +++ b/test-suite-http-client-tck-netty/src/test/resources/META-INF/services/io.micronaut.http.tck.ServerUnderTestProvider @@ -0,0 +1 @@ +io.micronaut.http.tck.EmbeddedServerUnderTestProvider diff --git a/test-suite-http-client-tck-netty/src/test/resources/logback.xml b/test-suite-http-client-tck-netty/src/test/resources/logback.xml new file mode 100644 index 00000000000..d9c96b90d72 --- /dev/null +++ b/test-suite-http-client-tck-netty/src/test/resources/logback.xml @@ -0,0 +1,16 @@ + + + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + + + diff --git a/test-suite-http-server-tck-jdk/build.gradle.kts b/test-suite-http-server-tck-jdk/build.gradle.kts new file mode 100644 index 00000000000..ef1175d830d --- /dev/null +++ b/test-suite-http-server-tck-jdk/build.gradle.kts @@ -0,0 +1,16 @@ +plugins { + id("io.micronaut.build.internal.convention-test-library") +} +dependencies { + testImplementation(projects.httpServerNetty) + testImplementation(projects.httpClientJdk) + testImplementation(projects.httpServerTck) + testImplementation(libs.junit.platform.engine) +} + +tasks.withType(Test::class) { + // systemProperty("jdk.httpclient.HttpClient.log", "all") // Uncomment to enable logging + + // These restricted headers are set in the server TCK + systemProperty("jdk.httpclient.allowRestrictedHeaders", "Host,Connection,Content-Length") +} diff --git a/test-suite-http-server-tck-jdk/src/test/java/io/micronaut/http/server/tck/netty/tests/JdkHttpServerTestSuite.java b/test-suite-http-server-tck-jdk/src/test/java/io/micronaut/http/server/tck/netty/tests/JdkHttpServerTestSuite.java new file mode 100644 index 00000000000..78dcdc8f75e --- /dev/null +++ b/test-suite-http-server-tck-jdk/src/test/java/io/micronaut/http/server/tck/netty/tests/JdkHttpServerTestSuite.java @@ -0,0 +1,15 @@ +package io.micronaut.http.server.tck.netty.tests; + +import org.junit.platform.suite.api.ExcludeClassNamePatterns; +import org.junit.platform.suite.api.ExcludeTags; +import org.junit.platform.suite.api.SelectPackages; +import org.junit.platform.suite.api.Suite; +import org.junit.platform.suite.api.SuiteDisplayName; + +@Suite +@SelectPackages("io.micronaut.http.server.tck.tests") +@SuiteDisplayName("HTTP Server TCK for Javanet client") +@ExcludeClassNamePatterns("io.micronaut.http.server.tck.tests.filter.Client.*FilterTest") +@ExcludeTags("multipart") // Multipart not supported by HttpClient +public class JdkHttpServerTestSuite { +} diff --git a/test-suite-http-server-tck-jdk/src/test/resources/META-INF/services/io.micronaut.http.tck.ServerUnderTestProvider b/test-suite-http-server-tck-jdk/src/test/resources/META-INF/services/io.micronaut.http.tck.ServerUnderTestProvider new file mode 100644 index 00000000000..32597c3d661 --- /dev/null +++ b/test-suite-http-server-tck-jdk/src/test/resources/META-INF/services/io.micronaut.http.tck.ServerUnderTestProvider @@ -0,0 +1 @@ +io.micronaut.http.tck.EmbeddedServerUnderTestProvider diff --git a/test-suite-http-server-tck-jdk/src/test/resources/logback.xml b/test-suite-http-server-tck-jdk/src/test/resources/logback.xml new file mode 100644 index 00000000000..d9c96b90d72 --- /dev/null +++ b/test-suite-http-server-tck-jdk/src/test/resources/logback.xml @@ -0,0 +1,16 @@ + + + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + + + diff --git a/test-suite-http-server-tck-netty/src/test/resources/META-INF/services/io.micronaut.http.server.tck.ServerUnderTestProvider b/test-suite-http-server-tck-netty/src/test/resources/META-INF/services/io.micronaut.http.server.tck.ServerUnderTestProvider deleted file mode 100644 index adf15625293..00000000000 --- a/test-suite-http-server-tck-netty/src/test/resources/META-INF/services/io.micronaut.http.server.tck.ServerUnderTestProvider +++ /dev/null @@ -1 +0,0 @@ -io.micronaut.http.server.tck.EmbeddedServerUnderTestProvider diff --git a/test-suite-http-server-tck-netty/src/test/resources/META-INF/services/io.micronaut.http.tck.ServerUnderTestProvider b/test-suite-http-server-tck-netty/src/test/resources/META-INF/services/io.micronaut.http.tck.ServerUnderTestProvider new file mode 100644 index 00000000000..32597c3d661 --- /dev/null +++ b/test-suite-http-server-tck-netty/src/test/resources/META-INF/services/io.micronaut.http.tck.ServerUnderTestProvider @@ -0,0 +1 @@ +io.micronaut.http.tck.EmbeddedServerUnderTestProvider diff --git a/test-suite-http-server-tck-netty/src/test/resources/logback.xml b/test-suite-http-server-tck-netty/src/test/resources/logback.xml index f6fa1cb1607..d0d20829204 100644 --- a/test-suite-http-server-tck-netty/src/test/resources/logback.xml +++ b/test-suite-http-server-tck-netty/src/test/resources/logback.xml @@ -1,11 +1,17 @@ + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + diff --git a/test-suite-kotlin/build.gradle b/test-suite-kotlin/build.gradle index fc5634cf5a7..d54d5d3ae4b 100644 --- a/test-suite-kotlin/build.gradle +++ b/test-suite-kotlin/build.gradle @@ -53,6 +53,7 @@ dependencies { testImplementation project(":inject") testImplementation libs.jcache testImplementation project(":http-client") + testImplementation project(":http-client-jdk") testImplementation (libs.micronaut.session) { exclude group: 'io.micronaut' } diff --git a/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/client/MultiClientSpec.kt b/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/client/MultiClientSpec.kt new file mode 100644 index 00000000000..5d96df29ff6 --- /dev/null +++ b/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/client/MultiClientSpec.kt @@ -0,0 +1,50 @@ +package io.micronaut.docs.client + +import io.micronaut.context.annotation.Property +import io.micronaut.context.annotation.Requires +import io.micronaut.http.HttpRequest +import io.micronaut.http.MediaType +import io.micronaut.http.annotation.Controller +import io.micronaut.http.annotation.CookieValue +import io.micronaut.http.annotation.Get +import io.micronaut.http.client.HttpClient +import io.micronaut.http.client.annotation.Client +import io.micronaut.http.client.jdk.DefaultJdkHttpClient +import io.micronaut.http.client.jdk.JdkHttpClient +import io.micronaut.http.client.netty.DefaultHttpClient +import io.micronaut.http.cookie.Cookie +import io.micronaut.test.extensions.junit5.annotation.MicronautTest +import jakarta.inject.Inject +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test + +@MicronautTest +@Property(name = "spec.name", value = "MultiClientSpec") +class MultiClientSpec { + + @field:Client("/") + @Inject + lateinit var nettyClient: HttpClient + + @field:Client("/") + @Inject + lateinit var jdkClient: JdkHttpClient + + @Test + fun testMultiClient() { + assertEquals(DefaultHttpClient::class.java, nettyClient.javaClass) + assertEquals(DefaultJdkHttpClient::class.java, jdkClient.javaClass) + assertEquals("ok bar", nettyClient.toBlocking().retrieve(getRequest())) + assertEquals("ok bar", jdkClient.toBlocking().retrieve(getRequest())) + } + + private fun getRequest() = HttpRequest.GET("/multi-client").cookie(Cookie.of("foo", "bar")) + + @Controller + @Requires(property = "spec.name", value = "MultiClientSpec") + internal class MultiClientController { + + @Get(uri = "/multi-client", produces = [MediaType.TEXT_PLAIN]) + fun multiClient(@CookieValue("foo") foo: String) = "ok $foo" + } +} diff --git a/test-suite/build.gradle b/test-suite/build.gradle index 7111eff23ba..6874af17f7c 100644 --- a/test-suite/build.gradle +++ b/test-suite/build.gradle @@ -43,6 +43,7 @@ dependencies { testImplementation project(":http-server-netty") testImplementation project(":jackson-databind") testImplementation project(":http-client") + testImplementation project(":http-client-jdk") testImplementation platform(libs.test.boms.micronaut.validation) testImplementation (libs.micronaut.validation) { exclude group: 'io.micronaut' diff --git a/test-suite/src/test/java/io/micronaut/docs/client/MultiClientSpec.java b/test-suite/src/test/java/io/micronaut/docs/client/MultiClientSpec.java new file mode 100644 index 00000000000..337d3d2bf06 --- /dev/null +++ b/test-suite/src/test/java/io/micronaut/docs/client/MultiClientSpec.java @@ -0,0 +1,56 @@ +package io.micronaut.docs.client; + +import io.micronaut.context.annotation.Property; +import io.micronaut.context.annotation.Requires; +import io.micronaut.http.HttpRequest; +import io.micronaut.http.MediaType; +import io.micronaut.http.MutableHttpRequest; +import io.micronaut.http.annotation.Controller; +import io.micronaut.http.annotation.CookieValue; +import io.micronaut.http.annotation.Get; +import io.micronaut.http.client.HttpClient; +import io.micronaut.http.client.annotation.Client; +import io.micronaut.http.client.jdk.DefaultJdkHttpClient; +import io.micronaut.http.client.jdk.JdkHttpClient; +import io.micronaut.http.client.netty.DefaultHttpClient; +import io.micronaut.http.cookie.Cookie; +import io.micronaut.test.extensions.junit5.annotation.MicronautTest; +import jakarta.inject.Inject; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +@MicronautTest +@Property(name = "spec.name", value = "MultiClientSpec") +class MultiClientSpec { + + @Inject + @Client("/") + HttpClient nettyClient; + + @Inject + @Client("/") + JdkHttpClient jdkClient; + + @Test + void testMultiClient() { + assertEquals(DefaultHttpClient.class, nettyClient.getClass()); + assertEquals(DefaultJdkHttpClient.class, jdkClient.getClass()); + assertEquals("ok bar", nettyClient.toBlocking().retrieve(getRequest())); + assertEquals("ok bar", jdkClient.toBlocking().retrieve(getRequest())); + } + + private static MutableHttpRequest getRequest() { + return HttpRequest.GET("/multi-client").cookie(Cookie.of("foo", "bar")); + } + + @Controller + @Requires(property = "spec.name", value = "MultiClientSpec") + static class MultiClientController { + + @Get(uri = "/multi-client", produces = MediaType.TEXT_PLAIN) + String multiClient(@CookieValue("foo") String foo) { + return "ok " + foo; + } + } +} From f2b9cd1b00443ef5571d7f134696de755db657c7 Mon Sep 17 00:00:00 2001 From: Tim Yates Date: Tue, 7 Mar 2023 20:57:45 +0000 Subject: [PATCH 565/743] Add back the platform for the server-tck tests (#8898) --- settings.gradle | 2 +- test-suite-http-server-tck-jdk/build.gradle.kts | 4 ++++ test-suite-http-server-tck-netty/build.gradle | 4 ++++ 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/settings.gradle b/settings.gradle index 56ec53bfc70..9a20f8f800c 100644 --- a/settings.gradle +++ b/settings.gradle @@ -14,7 +14,7 @@ pluginManagement { } plugins { - id 'io.micronaut.build.shared.settings' version '6.2.2' + id 'io.micronaut.build.shared.settings' version '6.3.3' } enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") diff --git a/test-suite-http-server-tck-jdk/build.gradle.kts b/test-suite-http-server-tck-jdk/build.gradle.kts index ef1175d830d..2f7d7926dd4 100644 --- a/test-suite-http-server-tck-jdk/build.gradle.kts +++ b/test-suite-http-server-tck-jdk/build.gradle.kts @@ -14,3 +14,7 @@ tasks.withType(Test::class) { // These restricted headers are set in the server TCK systemProperty("jdk.httpclient.allowRestrictedHeaders", "Host,Connection,Content-Length") } + +tasks.named("test") { + useJUnitPlatform() +} diff --git a/test-suite-http-server-tck-netty/build.gradle b/test-suite-http-server-tck-netty/build.gradle index 16f03d2df62..694b268670e 100644 --- a/test-suite-http-server-tck-netty/build.gradle +++ b/test-suite-http-server-tck-netty/build.gradle @@ -54,6 +54,10 @@ tasks.named("check") { task -> } } +tasks.named("test") { + useJUnitPlatform() +} + def openGraalModules = [ "org.graalvm.nativeimage.builder/com.oracle.svm.core.jdk", "org.graalvm.nativeimage.builder/com.oracle.svm.core.configure", From bb12352bb1e0260f2818f06d1e01ab83095bb346 Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Tue, 7 Mar 2023 21:58:45 +0100 Subject: [PATCH 566/743] build: Micronaut CRaC 1.1.2 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 6a455c22e34..2cd8b9a4bb0 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -72,7 +72,7 @@ managed-micronaut-azure = "3.7.1" managed-micronaut-cache = "3.5.0" managed-micronaut-cassandra = "5.1.1" managed-micronaut-coherence = "3.7.2" -managed-micronaut-crac = "1.1.1" +managed-micronaut-crac = "1.1.2" managed-micronaut-data = "3.9.6" managed-micronaut-discovery = "3.2.0" managed-micronaut-elasticsearch = "4.3.0" From 34a72bf0aed54b55c7df213702f36a169e190b5e Mon Sep 17 00:00:00 2001 From: Denis Stepanov Date: Wed, 8 Mar 2023 04:14:22 -0500 Subject: [PATCH 567/743] Allow to introspect classes by names (#8899) --- .../IntrospectedTypeElementVisitor.java | 82 +++++++---- .../core/annotation/Introspected.java | 7 + .../test/AbstractTypeElementSpec.groovy | 8 +- .../annotation/AnnotateTypeArgSpec.groovy | 135 ++++++++++++++++++ ...icronaut.inject.visitor.TypeElementVisitor | 1 + 5 files changed, 202 insertions(+), 31 deletions(-) create mode 100644 inject-java/src/test/groovy/io/micronaut/annotation/AnnotateTypeArgSpec.groovy diff --git a/core-processor/src/main/java/io/micronaut/inject/beans/visitor/IntrospectedTypeElementVisitor.java b/core-processor/src/main/java/io/micronaut/inject/beans/visitor/IntrospectedTypeElementVisitor.java index 2b23750a8c1..d7217278c61 100644 --- a/core-processor/src/main/java/io/micronaut/inject/beans/visitor/IntrospectedTypeElementVisitor.java +++ b/core-processor/src/main/java/io/micronaut/inject/beans/visitor/IntrospectedTypeElementVisitor.java @@ -37,6 +37,7 @@ import java.io.IOException; import java.lang.annotation.Annotation; +import java.util.Arrays; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; @@ -44,6 +45,7 @@ import java.util.Optional; import java.util.Set; import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.Stream; /** * A {@link TypeElementVisitor} that visits classes annotated with {@link Introspected} and produces @@ -84,37 +86,40 @@ private boolean isIntrospected(VisitorContext context, ClassElement c) { private void processIntrospected(ClassElement element, VisitorContext context, AnnotationValue introspected) { final String[] packages = introspected.stringValues("packages"); - final AnnotationClassValue[] classes = introspected.get("classes", AnnotationClassValue[].class, new AnnotationClassValue[0]); + final List classes = Stream.concat( + Arrays.stream(introspected.annotationClassValues("classes")).map(AnnotationClassValue::getName), + Arrays.stream(introspected.stringValues("classNames")) + ).toList(); final boolean metadata = introspected.booleanValue("annotationMetadata").orElse(true); final Set includedAnnotations = CollectionUtils.setOf(introspected.stringValues("includedAnnotations")); final Set> indexedAnnotations = CollectionUtils.setOf(introspected.get("indexed", AnnotationValue[].class, new AnnotationValue[0])); - if (ArrayUtils.isNotEmpty(classes)) { + if (!classes.isEmpty()) { AtomicInteger index = new AtomicInteger(0); - for (AnnotationClassValue aClass : classes) { - context.getClassElement(aClass.getName()).ifPresent(ce -> { - if (ce.isPublic() && !isIntrospected(context, ce)) { - final AnnotationMetadata typeMetadata = ce.getAnnotationMetadata(); - final AnnotationMetadata resolvedMetadata = typeMetadata == AnnotationMetadata.EMPTY_METADATA - ? element.getAnnotationMetadata() - : new AnnotationMetadataHierarchy(element.getAnnotationMetadata(), typeMetadata); - final BeanIntrospectionWriter writer = new BeanIntrospectionWriter( - element.getName(), - index.getAndIncrement(), - element, - ce, - metadata ? resolvedMetadata : null - ); + classes.stream().flatMap(className -> context.getClassElement(className).stream()).forEach(ce -> { + if (isIntrospected(context, ce)) { + return; + } + final AnnotationMetadata typeMetadata = ce.getAnnotationMetadata(); + final AnnotationMetadata resolvedMetadata = typeMetadata == AnnotationMetadata.EMPTY_METADATA + ? element.getAnnotationMetadata() + : new AnnotationMetadataHierarchy(element.getAnnotationMetadata(), typeMetadata); + final BeanIntrospectionWriter writer = new BeanIntrospectionWriter( + element.getName(), + index.getAndIncrement(), + element, + ce, + metadata ? resolvedMetadata : null + ); - processElement( - metadata, - indexedAnnotations, - ce, - writer - ); - } - }); - } + processElement( + metadata, + indexedAnnotations, + getExternalPropertyElementQuery(element, ce), + ce, + writer + ); + }); } else if (ArrayUtils.isNotEmpty(packages)) { if (includedAnnotations.isEmpty()) { context.fail("When specifying 'packages' you must also specify 'includedAnnotations' to limit scanning", element); @@ -134,7 +139,11 @@ private void processIntrospected(ClassElement element, VisitorContext context, A metadata ? element.getAnnotationMetadata() : null ); - processElement(metadata, indexedAnnotations, classElement, writer); + processElement(metadata, + indexedAnnotations, + getExternalPropertyElementQuery(element, classElement), + classElement, + writer); } } } @@ -147,6 +156,12 @@ private void processIntrospected(ClassElement element, VisitorContext context, A } } + @NonNull + private static PropertyElementQuery getExternalPropertyElementQuery(ClassElement defined, ClassElement current) { + AnnotationMetadataHierarchy hierarchy = new AnnotationMetadataHierarchy(defined, current); + return PropertyElementQuery.of(hierarchy).ignoreSettersWithDifferingType(true); + } + @NonNull @Override public VisitorKind getVisitorKind() { @@ -174,8 +189,19 @@ private void processElement(boolean metadata, Set> indexedAnnotations, ClassElement ce, BeanIntrospectionWriter writer) { - PropertyElementQuery query = PropertyElementQuery.of(ce).ignoreSettersWithDifferingType(true); - List beanProperties = ce.getBeanProperties(query).stream() + processElement(metadata, + indexedAnnotations, + PropertyElementQuery.of(ce).ignoreSettersWithDifferingType(true), + ce, + writer); + } + + private void processElement(boolean metadata, + Set> indexedAnnotations, + PropertyElementQuery propertyElementQuery, + ClassElement ce, + BeanIntrospectionWriter writer) { + List beanProperties = ce.getBeanProperties(propertyElementQuery).stream() .filter(p -> !p.isExcluded()) .toList(); Optional constructorElement = ce.getPrimaryConstructor(); diff --git a/core/src/main/java/io/micronaut/core/annotation/Introspected.java b/core/src/main/java/io/micronaut/core/annotation/Introspected.java index a3f1c29c8fe..6cd213e4fc6 100644 --- a/core/src/main/java/io/micronaut/core/annotation/Introspected.java +++ b/core/src/main/java/io/micronaut/core/annotation/Introspected.java @@ -75,6 +75,13 @@ */ Class[] classes() default {}; + /** + * Alternative way to specify the value for `classes` when the class cannot be referenced. + * + * @return The class names to generate introspections for + */ + String[] classNames() default {}; + /** *

The default access type is {@link AccessKind#METHOD} which treats only public JavaBean getters or Java record components as properties. By specifying {@link AccessKind#FIELD}, public or package-protected fields will be used instead.

* diff --git a/inject-java-test/src/main/groovy/io/micronaut/annotation/processing/test/AbstractTypeElementSpec.groovy b/inject-java-test/src/main/groovy/io/micronaut/annotation/processing/test/AbstractTypeElementSpec.groovy index 380b2aa56d0..574da2da42f 100644 --- a/inject-java-test/src/main/groovy/io/micronaut/annotation/processing/test/AbstractTypeElementSpec.groovy +++ b/inject-java-test/src/main/groovy/io/micronaut/annotation/processing/test/AbstractTypeElementSpec.groovy @@ -145,7 +145,8 @@ abstract class AbstractTypeElementSpec extends Specification { * @return the introspection if it is correct **/ protected BeanIntrospection buildBeanIntrospection(String className, @Language("java") String cls) { - def beanDefName= (className.startsWith('$') ? '' : '$') + NameUtils.getSimpleName(className) + '$Introspection' + def simpleName = NameUtils.getSimpleName(className) + def beanDefName = (simpleName.startsWith('$') ? '' : '$') + simpleName + '$Introspection' def packageName = NameUtils.getPackageName(className) String beanFullName = "${packageName}.${beanDefName}" @@ -472,8 +473,9 @@ class Test { return new JavaFileObjectClassLoader(files) } + @CompileStatic protected AnnotationMetadata writeAndLoadMetadata(String className, AnnotationMetadata toWrite) { - def stream = new ByteArrayOutputStream() + ByteArrayOutputStream stream = new ByteArrayOutputStream() new AnnotationMetadataWriter(className, null, toWrite, true) .writeTo(stream) className = className + AnnotationMetadata.CLASS_NAME_SUFFIX @@ -481,7 +483,7 @@ class Test { @Override protected Class findClass(String name) throws ClassNotFoundException { if (name == className) { - def bytes = stream.toByteArray() + byte[] bytes = stream.toByteArray() return defineClass(name, bytes, 0, bytes.length) } return super.findClass(name) diff --git a/inject-java/src/test/groovy/io/micronaut/annotation/AnnotateTypeArgSpec.groovy b/inject-java/src/test/groovy/io/micronaut/annotation/AnnotateTypeArgSpec.groovy new file mode 100644 index 00000000000..7115bd2b22c --- /dev/null +++ b/inject-java/src/test/groovy/io/micronaut/annotation/AnnotateTypeArgSpec.groovy @@ -0,0 +1,135 @@ +package io.micronaut.annotation + +import io.micronaut.annotation.processing.test.AbstractTypeElementSpec +import io.micronaut.core.annotation.Introspected +import io.micronaut.inject.ast.ClassElement +import io.micronaut.inject.visitor.TypeElementVisitor +import io.micronaut.inject.visitor.VisitorContext +import spock.lang.PendingFeature + +class AnnotateTypeArgSpec extends AbstractTypeElementSpec { + + void 'is always triggered without annotations'() { + when: + def introspection = buildBeanIntrospection('addann.AnnotateTypeArg0', ''' +package addann; + +import java.util.Map; + +class AnnotateTypeArg0 { + + private Map nameMap; + +} + +''') + then: + introspection.hasAnnotation(MyAnnotation) + } + + void 'test annotating 1'() { + when: + def introspection = buildBeanIntrospection('addann.AnnotateTypeArg1', ''' +package addann; + +import javax.validation.constraints.NotBlank; +import java.util.Map; + +class AnnotateTypeArg1 { + + private Map<@NotBlank String, String> nameMap; + +} + +''') + then: + introspection.hasAnnotation(MyAnnotation) + } + + @PendingFeature(reason = "Annotation processor doesn't trigger for inner classes with type argument only annotation") + void 'test annotating 2'() { + when: + def introspection = buildBeanIntrospection('addann.Outer1$AnnotateTypeArg2', ''' +package addann; + +import javax.validation.constraints.NotBlank; +import java.util.Map; + +class Outer1 { + static class AnnotateTypeArg2 { + + private Map<@NotBlank String, String> nameMap; + + } +} + +''') + then: + introspection.hasAnnotation(MyAnnotation) + } + + void 'test annotating 3'() { + when: + def introspection = buildBeanIntrospection('addann.$addann_Outer2$NotProcessedByVisitor', ''' +package addann; + +import io.micronaut.core.annotation.Introspected; +import javax.validation.constraints.NotBlank; +import java.util.Map; + +@Introspected(classes = addann.Outer2.NotProcessedByVisitor.class, accessKind = Introspected.AccessKind.FIELD, visibility = Introspected.Visibility.ANY) +class Outer2 { + static class NotProcessedByVisitor { + + private Map<@NotBlank String, String> hnameMap; + + } +} + +''') + then: + introspection + introspection.getPropertyNames().toList() == ["hnameMap"] + } + + void 'test annotating 4'() { + when: + def introspection = buildBeanIntrospection('addann.$addann_Outer3$NotProcessedByVisitor2', ''' +package addann; + +import io.micronaut.core.annotation.Introspected; +import javax.validation.constraints.NotBlank; +import java.util.Map; + +@Introspected(classNames = "addann.Outer3.NotProcessedByVisitor2", accessKind = Introspected.AccessKind.FIELD, visibility = Introspected.Visibility.ANY) +class Outer3 { + static class NotProcessedByVisitor2 { + + private Map<@NotBlank String, String> hnameMap; + + } +} + +''') + then: + introspection + introspection.getPropertyNames().toList() == ["hnameMap"] + } + + static class AnnotateTypeArgVisitor implements TypeElementVisitor { + + @Override + VisitorKind getVisitorKind() { + return VisitorKind.ISOLATING + } + + @Override + void visitClass(ClassElement element, VisitorContext context) { + if (element.getSimpleName().contains("AnnotateTypeArg")) { + element.annotate(Introspected) + element.annotate(MyAnnotation) + } + } + } + +} diff --git a/inject-java/src/test/resources/META-INF/services/io.micronaut.inject.visitor.TypeElementVisitor b/inject-java/src/test/resources/META-INF/services/io.micronaut.inject.visitor.TypeElementVisitor index 8af31fade39..6f2fdc69e3a 100644 --- a/inject-java/src/test/resources/META-INF/services/io.micronaut.inject.visitor.TypeElementVisitor +++ b/inject-java/src/test/resources/META-INF/services/io.micronaut.inject.visitor.TypeElementVisitor @@ -17,3 +17,4 @@ io.micronaut.annotation.AnnotateFieldTypeSpec$AnnotateFieldTypeVisitor io.micronaut.annotation.AnnotateMethodParameterSpec$AnnotateMethodParameterVisitor io.micronaut.annotation.AnnotatePropertySpec$AnnotatePropertyVisitor io.micronaut.annotation.AnnotateClassSpec$AnnotateClassVisitor +io.micronaut.annotation.AnnotateTypeArgSpec$AnnotateTypeArgVisitor From 82658f2cdcaad772b3d342e05316d8b24c975a17 Mon Sep 17 00:00:00 2001 From: Tim Yates Date: Wed, 8 Mar 2023 12:19:08 +0000 Subject: [PATCH 568/743] Allow Tag annotation at build time for the TCK (#8904) Using the Tag annotation in the TCK tests means that `org.junit.platform.engine.TestTag` is initialized at build time --- .../server/tck/tests/ErrorHandlerTest.java | 2 +- .../io/micronaut/http/tck/BodyAssertion.java | 54 +++++++++++++++---- .../native-image.properties | 1 + 3 files changed, 47 insertions(+), 10 deletions(-) create mode 100644 http-tck/src/main/resources/META-INF/native-image/io.micronaut.http.tck/micronaut-http-tck/native-image.properties diff --git a/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/ErrorHandlerTest.java b/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/ErrorHandlerTest.java index 310dbd9edc3..6b79e214516 100644 --- a/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/ErrorHandlerTest.java +++ b/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/ErrorHandlerTest.java @@ -104,7 +104,7 @@ void testCustomGlobalExceptionHandlersForPOSTWithBody() throws IOException { .header(HttpHeaders.CONTENT_TYPE, io.micronaut.http.MediaType.APPLICATION_JSON); AssertionUtils.assertDoesNotThrow(server, request, HttpStatus.OK, - "{\"message\":\"Error: bad things when post and body in request\",\"", + "\"message\":\"Error: bad things when post and body in request\"", Collections.singletonMap(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON)); } } diff --git a/http-tck/src/main/java/io/micronaut/http/tck/BodyAssertion.java b/http-tck/src/main/java/io/micronaut/http/tck/BodyAssertion.java index 85053bbd44e..e9e9879dcf8 100644 --- a/http-tck/src/main/java/io/micronaut/http/tck/BodyAssertion.java +++ b/http-tck/src/main/java/io/micronaut/http/tck/BodyAssertion.java @@ -35,11 +35,13 @@ public final class BodyAssertion { private final Class bodyType; private final Class errorType; private final T expected; - private final BiPredicate evaluator; + private final BodyEvaluator evaluator; private BodyAssertion(Class bodyType, Class errorType, - T expected, BiPredicate evaluator) { + T expected, + BodyEvaluator evaluator + ) { this.bodyType = bodyType; this.errorType = errorType; this.expected = expected; @@ -60,7 +62,7 @@ public static BodyAssertion.Builder builder() { */ @SuppressWarnings("java:S5960") // Assertion is the whole point of this method public void evaluate(T body) { - assertTrue(this.evaluator.test(expected, body)); + assertTrue(this.evaluator.test(expected, body), () -> this.evaluator.message(expected, body)); } /** @@ -75,6 +77,11 @@ public Class getErrorType() { return errorType; } + private static enum EvaluatorType { + EQUAL, + CONTAIN, + } + /** * The interface for typed BodyAssertion Builders. * @@ -94,6 +101,15 @@ public interface AssertionBuilder { BodyAssertion equals(); } + private interface BodyEvaluator extends BiPredicate { + + EvaluatorType type(); + + default String message(T expected, T actual) { + return "Expected received body of '" + actual + "' to " + type().name().toLowerCase() + " '" + expected + "'"; + } + } + /** * BodyAssertion Builder. */ @@ -131,14 +147,14 @@ public StringBodyAssertionBuilder(String expected) { * @return a body assertion which verifiers the HTTP Response's body contains the expected body */ public BodyAssertion contains() { - return new BodyAssertion<>(String.class, String.class, this.body, (required, received) -> received.contains(required)); + return new BodyAssertion<>(String.class, String.class, this.body, new StringEvaluator(EvaluatorType.CONTAIN)); } /** * @return a body assertion which verifiers the HTTP Response's body is equals to the expected body */ public BodyAssertion equals() { - return new BodyAssertion<>(String.class, String.class, this.body, (required, received) -> received.equals(required)); + return new BodyAssertion<>(String.class, String.class, this.body, new StringEvaluator(EvaluatorType.EQUAL)); } } @@ -157,16 +173,36 @@ public ByteArrayBodyAssertionBuilder(byte[] expected) { * @return a body assertion which verifiers the HTTP Response's body contains the expected body */ public BodyAssertion contains() { - return new BodyAssertion<>(byte[].class, byte[].class, this.body, (required, received) -> { - throw new AssertionError("Not implemented yet!"); - }); + return new BodyAssertion<>(byte[].class, byte[].class, this.body, new ByteArrayEvaluator(EvaluatorType.CONTAIN)); } /** * @return a body assertion which verifiers the HTTP Response's body is equals to the expected body */ public BodyAssertion equals() { - return new BodyAssertion<>(byte[].class, byte[].class, this.body, (required, received) -> Arrays.equals(received, required)); + return new BodyAssertion<>(byte[].class, byte[].class, this.body, new ByteArrayEvaluator(EvaluatorType.EQUAL)); + } + } + + private record StringEvaluator(EvaluatorType type) implements BodyEvaluator { + + @Override + public boolean test(String expected, String received) { + return switch (type) { + case EQUAL -> received.equals(expected); + case CONTAIN -> received.contains(expected); + }; + } + } + + private record ByteArrayEvaluator(EvaluatorType type) implements BodyEvaluator { + + @Override + public boolean test(byte[] expected, byte[] received) { + return switch (type) { + case EQUAL -> Arrays.equals(received, expected); + case CONTAIN -> throw new AssertionError("Not implemented yet!"); + }; } } } diff --git a/http-tck/src/main/resources/META-INF/native-image/io.micronaut.http.tck/micronaut-http-tck/native-image.properties b/http-tck/src/main/resources/META-INF/native-image/io.micronaut.http.tck/micronaut-http-tck/native-image.properties new file mode 100644 index 00000000000..094f0a9bb7f --- /dev/null +++ b/http-tck/src/main/resources/META-INF/native-image/io.micronaut.http.tck/micronaut-http-tck/native-image.properties @@ -0,0 +1 @@ +Args = --initialize-at-build-time=org.junit.platform.engine.TestTag From 9b526387ed49cdd1734d3fa235c4bed609075578 Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Wed, 8 Mar 2023 13:20:31 +0100 Subject: [PATCH 569/743] imp: Add STANDARD_HEADERS list (#8903) It contains every HttpHeader constant Co-authored-by: Graeme Rocher --------- Co-authored-by: Graeme Rocher --- .../java/io/micronaut/http/HttpHeaders.java | 100 +++++++++++++++++ .../io/micronaut/http/HttpHeadersSpec.groovy | 105 +++++++++++++++++- 2 files changed, 204 insertions(+), 1 deletion(-) diff --git a/http/src/main/java/io/micronaut/http/HttpHeaders.java b/http/src/main/java/io/micronaut/http/HttpHeaders.java index 3c1df8839c8..7a614001a54 100644 --- a/http/src/main/java/io/micronaut/http/HttpHeaders.java +++ b/http/src/main/java/io/micronaut/http/HttpHeaders.java @@ -499,6 +499,106 @@ public interface HttpHeaders extends Headers { */ String X_AUTH_TOKEN = "X-Auth-Token"; + /** + * Unmodifiable List of every header constant defined in {@link HttpHeaders}. + */ + List STANDARD_HEADERS = Collections.unmodifiableList(Arrays.asList( + ACCEPT, + ACCEPT, + ACCEPT_CH, + ACCEPT_CH_LIFETIME, + ACCEPT_CHARSET, + ACCEPT_ENCODING, + ACCEPT_LANGUAGE, + ACCEPT_RANGES, + ACCEPT_PATCH, + ACCESS_CONTROL_ALLOW_CREDENTIALS, + ACCESS_CONTROL_ALLOW_HEADERS, + ACCESS_CONTROL_ALLOW_METHODS, + ACCESS_CONTROL_ALLOW_ORIGIN, + ACCESS_CONTROL_EXPOSE_HEADERS, + ACCESS_CONTROL_MAX_AGE, + ACCESS_CONTROL_REQUEST_HEADERS, + ACCESS_CONTROL_REQUEST_METHOD, + AGE, + ALLOW, + AUTHORIZATION, + AUTHORIZATION_INFO, + CACHE_CONTROL, + CONNECTION, + CONTENT_BASE, + CONTENT_DISPOSITION, + CONTENT_DPR, + CONTENT_ENCODING, + CONTENT_LANGUAGE, + CONTENT_LENGTH, + CONTENT_LOCATION, + CONTENT_TRANSFER_ENCODING, + CONTENT_MD5, + CONTENT_RANGE, + CONTENT_TYPE, + COOKIE, + CROSS_ORIGIN_RESOURCE_POLICY, + DATE, + DEVICE_MEMORY, + DOWNLINK, + DPR, + ECT, + ETAG, + EXPECT, + EXPIRES, + FEATURE_POLICY, + FORWARDED, + FROM, + HOST, + IF_MATCH, + IF_MODIFIED_SINCE, + IF_NONE_MATCH, + IF_RANGE, + IF_UNMODIFIED_SINCE, + LAST_MODIFIED, + LINK, + LOCATION, + MAX_FORWARDS, + ORIGIN, + PRAGMA, + PROXY_AUTHENTICATE, + PROXY_AUTHORIZATION, + RANGE, + REFERER, + REFERRER_POLICY, + RETRY_AFTER, + RTT, + SAVE_DATA, + SEC_WEBSOCKET_KEY1, + SEC_WEBSOCKET_KEY2, + SEC_WEBSOCKET_LOCATION, + SEC_WEBSOCKET_ORIGIN, + SEC_WEBSOCKET_PROTOCOL, + SEC_WEBSOCKET_VERSION, + SEC_WEBSOCKET_KEY, + SEC_WEBSOCKET_ACCEPT, + SERVER, + SET_COOKIE, + SET_COOKIE2, + SOURCE_MAP, + TE, + TRAILER, + TRANSFER_ENCODING, + UPGRADE, + USER_AGENT, + VARY, + VIA, + VIEWPORT_WIDTH, + WARNING, + WEBSOCKET_LOCATION, + WEBSOCKET_ORIGIN, + WEBSOCKET_PROTOCOL, + WIDTH, + WWW_AUTHENTICATE, + X_AUTH_TOKEN + )); + /** * Obtain the date header. * diff --git a/http/src/test/groovy/io/micronaut/http/HttpHeadersSpec.groovy b/http/src/test/groovy/io/micronaut/http/HttpHeadersSpec.groovy index a3c0fae68d5..243b69fb94b 100644 --- a/http/src/test/groovy/io/micronaut/http/HttpHeadersSpec.groovy +++ b/http/src/test/groovy/io/micronaut/http/HttpHeadersSpec.groovy @@ -16,6 +16,7 @@ package io.micronaut.http import spock.lang.Specification +import spock.lang.Unroll class HttpHeadersSpec extends Specification { @@ -46,4 +47,106 @@ class HttpHeadersSpec extends Specification { mediaTypeList.find { it.name == 'application/json' && it.qualityAsNumber == 1.0 } } -} \ No newline at end of file + @Unroll + void "HttpHeader.STANDARD_NAMES contains HTTP Header #httpHeaderName"(String httpHeaderName) { + expect: + HttpHeaders.STANDARD_HEADERS.contains(httpHeaderName) + + where: + httpHeaderName << [ + HttpHeaders.ACCEPT, + HttpHeaders.ACCEPT_CH, + HttpHeaders.ACCEPT_CH_LIFETIME, + HttpHeaders.ACCEPT_CHARSET, + HttpHeaders.ACCEPT_ENCODING, + HttpHeaders.ACCEPT_LANGUAGE, + HttpHeaders.ACCEPT_RANGES, + HttpHeaders.ACCEPT_PATCH, + HttpHeaders.ACCESS_CONTROL_ALLOW_CREDENTIALS, + HttpHeaders.ACCESS_CONTROL_ALLOW_HEADERS, + HttpHeaders.ACCESS_CONTROL_ALLOW_METHODS, + HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN, + HttpHeaders.ACCESS_CONTROL_EXPOSE_HEADERS, + HttpHeaders.ACCESS_CONTROL_MAX_AGE, + HttpHeaders.ACCESS_CONTROL_REQUEST_HEADERS, + HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, + HttpHeaders.AGE, + HttpHeaders.ALLOW, + HttpHeaders.AUTHORIZATION, + HttpHeaders.AUTHORIZATION_INFO, + HttpHeaders.CACHE_CONTROL, + HttpHeaders.CONNECTION, + HttpHeaders.CONTENT_BASE, + HttpHeaders.CONTENT_DISPOSITION, + HttpHeaders.CONTENT_DPR, + HttpHeaders.CONTENT_ENCODING, + HttpHeaders.CONTENT_LANGUAGE, + HttpHeaders.CONTENT_LENGTH, + HttpHeaders.CONTENT_LOCATION, + HttpHeaders.CONTENT_TRANSFER_ENCODING, + HttpHeaders.CONTENT_MD5, + HttpHeaders.CONTENT_RANGE, + HttpHeaders.CONTENT_TYPE, + HttpHeaders.COOKIE, + HttpHeaders.CROSS_ORIGIN_RESOURCE_POLICY, + HttpHeaders.DATE, + HttpHeaders.DEVICE_MEMORY, + HttpHeaders.DOWNLINK, + HttpHeaders.DPR, + HttpHeaders.ECT, + HttpHeaders.ETAG, + HttpHeaders.EXPECT, + HttpHeaders.EXPIRES, + HttpHeaders.FEATURE_POLICY, + HttpHeaders.FORWARDED, + HttpHeaders.FROM, + HttpHeaders.HOST, + HttpHeaders.IF_MATCH, + HttpHeaders.IF_MODIFIED_SINCE, + HttpHeaders.IF_NONE_MATCH, + HttpHeaders.IF_RANGE, + HttpHeaders.IF_UNMODIFIED_SINCE, + HttpHeaders.LAST_MODIFIED, + HttpHeaders.LINK, + HttpHeaders.LOCATION, + HttpHeaders.MAX_FORWARDS, + HttpHeaders.ORIGIN, + HttpHeaders.PRAGMA, + HttpHeaders.PROXY_AUTHENTICATE, + HttpHeaders.PROXY_AUTHORIZATION, + HttpHeaders.RANGE, + HttpHeaders.REFERER, + HttpHeaders.REFERRER_POLICY, + HttpHeaders.RETRY_AFTER, + HttpHeaders.RTT, + HttpHeaders.SAVE_DATA, + HttpHeaders.SEC_WEBSOCKET_KEY1, + HttpHeaders.SEC_WEBSOCKET_KEY2, + HttpHeaders.SEC_WEBSOCKET_LOCATION, + HttpHeaders.SEC_WEBSOCKET_ORIGIN, + HttpHeaders.SEC_WEBSOCKET_PROTOCOL, + HttpHeaders.SEC_WEBSOCKET_VERSION, + HttpHeaders.SEC_WEBSOCKET_KEY, + HttpHeaders.SEC_WEBSOCKET_ACCEPT, + HttpHeaders.SERVER, + HttpHeaders.SET_COOKIE, + HttpHeaders.SET_COOKIE2, + HttpHeaders.SOURCE_MAP, + HttpHeaders.TE, + HttpHeaders.TRAILER, + HttpHeaders.TRANSFER_ENCODING, + HttpHeaders.UPGRADE, + HttpHeaders.USER_AGENT, + HttpHeaders.VARY, + HttpHeaders.VIA, + HttpHeaders.VIEWPORT_WIDTH, + HttpHeaders.WARNING, + HttpHeaders.WEBSOCKET_LOCATION, + HttpHeaders.WEBSOCKET_ORIGIN, + HttpHeaders.WEBSOCKET_PROTOCOL, + HttpHeaders.WIDTH, + HttpHeaders.WWW_AUTHENTICATE, + HttpHeaders.X_AUTH_TOKEN, + ] + } +} From d3bce80e9884a713a53bbc728dd752c8b4b84647 Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Wed, 8 Mar 2023 13:21:40 +0100 Subject: [PATCH 570/743] fix(deps): update dependency org.yaml:snakeyaml to v2 (#8905) https://bitbucket.org/snakeyaml/snakeyaml/wiki/Changes Relates to: https://nvd.nist.gov/vuln/detail/CVE-2022-1471 Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- .../context/env/yaml/CustomSafeConstructor.java | 2 ++ src/main/docs/guide/appendix/breaks.adoc | 9 +++++++++ 3 files changed, 12 insertions(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 2cd8b9a4bb0..084b3c12786 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -142,7 +142,7 @@ managed-springboot = "2.7.0" managed-swagger = "2.2.7" managed-validation = "2.0.1.Final" managed-testcontainers = "1.17.5" -managed-snakeyaml = "1.33" +managed-snakeyaml = "2.0" micronaut-docs = "2.0.0" [libraries] diff --git a/inject/src/main/java/io/micronaut/context/env/yaml/CustomSafeConstructor.java b/inject/src/main/java/io/micronaut/context/env/yaml/CustomSafeConstructor.java index 3c1a2d9cfa5..c43dd24919f 100644 --- a/inject/src/main/java/io/micronaut/context/env/yaml/CustomSafeConstructor.java +++ b/inject/src/main/java/io/micronaut/context/env/yaml/CustomSafeConstructor.java @@ -16,6 +16,7 @@ package io.micronaut.context.env.yaml; import io.micronaut.core.annotation.Internal; +import org.yaml.snakeyaml.LoaderOptions; import org.yaml.snakeyaml.constructor.SafeConstructor; import org.yaml.snakeyaml.nodes.MappingNode; import org.yaml.snakeyaml.nodes.SequenceNode; @@ -34,6 +35,7 @@ @Internal class CustomSafeConstructor extends SafeConstructor { CustomSafeConstructor() { + super(new LoaderOptions()); yamlConstructors.put(Tag.TIMESTAMP, new ConstructIsoTimestampString()); } diff --git a/src/main/docs/guide/appendix/breaks.adoc b/src/main/docs/guide/appendix/breaks.adoc index 7781d29da25..e5129b3aaeb 100644 --- a/src/main/docs/guide/appendix/breaks.adoc +++ b/src/main/docs/guide/appendix/breaks.adoc @@ -1,5 +1,14 @@ This section documents breaking changes between Micronaut versions +== 3.8.7 + +Micronaut Framework 3.8.7 updates to https://bitbucket.org/snakeyaml/snakeyaml/wiki/Changes[SnakeYAML 2.0] which addresses https://nvd.nist.gov/vuln/detail/CVE-2022-1471[CVE-2022-1471]. Many organizations' policies forbid their teams to use Micronaut Framework if the framework depends on a vulnerable dependency, even if the framework is unaffected. Micronaut Framework is not affected by https://nvd.nist.gov/vuln/detail/CVE-2022-1471[CVE-2022-1471]. +Micronaut Framework uses SnakeYAML to load configuration in Micronaut applications. There is only one instance of https://github.com/micronaut-projects/micronaut-core/blob/3.7.x/inject/src/main/java/io/micronaut/context/env/yaml/YamlPropertySourceLoader.java#L56[SnakeYAML instantiation] which uses the https://github.com/micronaut-projects/micronaut-core/blob/3.8.x/inject/src/main/java/io/micronaut/context/env/yaml/CustomSafeConstructor.java[Safe Constructor]. Using SnakeYaml's SafeConstructor which is the recommended way to prevent this issue: + +____ +We recommend using SnakeYaml's SafeConsturctor when parsing untrusted content to restrict deserialization. +____ + == 3.3.0 - The <> is now disabled by default. To enable it, you must update your endpoint config: From 959ea40b4efccf6dc84bfe5728c4259d96aa26f1 Mon Sep 17 00:00:00 2001 From: Graeme Rocher Date: Wed, 8 Mar 2023 16:31:37 +0100 Subject: [PATCH 571/743] remove paralleism from default methods (#8906) --- .../main/java/io/micronaut/core/io/scan/AnnotationScanner.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/core/src/main/java/io/micronaut/core/io/scan/AnnotationScanner.java b/core/src/main/java/io/micronaut/core/io/scan/AnnotationScanner.java index 40d9252d57d..f4c39e39ca4 100644 --- a/core/src/main/java/io/micronaut/core/io/scan/AnnotationScanner.java +++ b/core/src/main/java/io/micronaut/core/io/scan/AnnotationScanner.java @@ -119,7 +119,7 @@ public interface AnnotationScanner { default @NonNull Stream> scan(@NonNull Class annotation, @NonNull Collection packages) { Objects.requireNonNull(annotation, "Annotation type cannot be null"); Objects.requireNonNull(packages, "Packages to scan cannot be null"); - return scan(annotation.getName(), packages.parallelStream()); + return scan(annotation.getName(), packages.stream()); } /** @@ -134,7 +134,6 @@ public interface AnnotationScanner { Objects.requireNonNull(packages, "Packages to scan cannot be null"); return packages - .parallel() .flatMap(pkg -> scan(annotation, pkg)); } From ba28f62c2539c63e0b6278ab1e3a8b7bb9b19e3a Mon Sep 17 00:00:00 2001 From: Graeme Rocher Date: Wed, 8 Mar 2023 16:44:09 +0100 Subject: [PATCH 572/743] Upgrade to Micronaut Spring 4.5.1 (#8910) --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index ee6326b3c58..a3c4d74a621 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -115,7 +115,7 @@ managed-micronaut-rxjava3 = "2.4.0" managed-micronaut-security = "3.9.3" managed-micronaut-serialization = "1.5.2" managed-micronaut-servlet = "3.3.5" -managed-micronaut-spring = "4.5.0" +managed-micronaut-spring = "4.5.1" managed-micronaut-sql = "4.8.0" managed-micronaut-test = "3.9.1" managed-micronaut-test-resources = "1.2.3" From 30d493cd6e79b74f70f8f889704cb729eae2cba9 Mon Sep 17 00:00:00 2001 From: micronaut-build Date: Wed, 8 Mar 2023 18:58:29 +0000 Subject: [PATCH 573/743] [skip ci] Release v3.8.7 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 59f2faa0b19..b92cbbf8859 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -projectVersion=3.8.7-SNAPSHOT +projectVersion=3.8.7 projectGroupId=io.micronaut title=Micronaut projectDesc=Natively Cloud Native From e22c6e427f7469b77153929649c212b224b49520 Mon Sep 17 00:00:00 2001 From: micronaut-build Date: Wed, 8 Mar 2023 19:17:23 +0000 Subject: [PATCH 574/743] Back to 3.8.8-SNAPSHOT --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index b92cbbf8859..6df7eb03e0b 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -projectVersion=3.8.7 +projectVersion=3.8.8-SNAPSHOT projectGroupId=io.micronaut title=Micronaut projectDesc=Natively Cloud Native From 887aef4fe85678078761cacc282acf76fd2299b2 Mon Sep 17 00:00:00 2001 From: Jonas Konrad Date: Thu, 9 Mar 2023 14:59:12 +0100 Subject: [PATCH 575/743] add experimental support for HTTP/3 (#8559) This PR adds HTTP/3 support for the server and client based on the experimental https://github.com/netty/netty-incubator-codec-http3 . The code is very similar to the existing HTTP/2 stack. The major differences relate to UDP vs TCP. The HTTP/3 server is activated through a special listener protocol family QUIC (maybe this should be called UDP?). The client is activated by setting the alpn-modes to [h3] (switching to udp based on this property is a bit weird, but ALPN still exists with HTTP/3. eventually, falling back on tcp would be nice). Server push is currently not supported. The test case for it in particular (netty-based) would need to be rewritten for UDP. --- gradle/libs.versions.toml | 2 + .../http/client/HttpVersionSelection.java | 15 ++ .../client/jdk/AbstractJdkHttpClient.java | 21 +- .../micronaut/http/client/jdk/H2CSpec.groovy | 36 +++ http-client/build.gradle | 2 + .../http/client/netty/ConnectionManager.java | 254 ++++++++++++++++-- .../http/client/netty/DefaultHttpClient.java | 22 +- .../netty/DefaultNettyHttpClientRegistry.java | 11 +- .../netty/ssl/NettyClientSslBuilder.java | 21 ++ .../http/netty/AbstractNettyHttpRequest.java | 13 - .../channel/DefaultEventLoopGroupFactory.java | 16 ++ .../channel/EpollEventLoopGroupFactory.java | 26 ++ .../netty/channel/EventLoopGroupFactory.java | 51 ++++ .../channel/KQueueEventLoopGroupFactory.java | 26 ++ .../http/netty/channel/NettyChannelType.java | 40 +++ .../channel/NioEventLoopGroupFactory.java | 27 ++ .../netty/reactive/HandlerSubscriber.java | 8 +- .../http/netty/stream/HttpStreamsHandler.java | 2 +- http-server-netty/build.gradle | 2 + .../DefaultNettyEmbeddedServerFactory.java | 27 +- .../server/netty/HttpPipelineBuilder.java | 124 +++++++-- .../server/netty/NettyEmbeddedServices.java | 25 +- .../http/server/netty/NettyHttpRequest.java | 10 + .../http/server/netty/NettyHttpServer.java | 146 +++++----- .../server/netty/QuicTokenHandlerImpl.java | 164 +++++++++++ .../NettyHttpServerConfiguration.java | 144 +++++++++- .../ssl/CertificateProvidedSslBuilder.java | 19 ++ .../netty/ssl/SelfSignedSslBuilder.java | 16 ++ .../server/netty/ssl/ServerSslBuilder.java | 5 + ...ttySystemFileCustomizableResponseType.java | 10 +- .../netty/QuicTokenHandlerImplSpec.groovy | 106 ++++++++ .../http/server/netty/http2/Http3Spec.groovy | 79 ++++++ .../Http2StaticResourceCacheSpec.groovy | 5 +- .../docs/guide/httpClient/clientHttp3.adoc | 16 ++ .../jdkHttpClient.adoc | 1 + .../docs/guide/httpServer/http3Server.adoc | 23 ++ src/main/docs/guide/toc.yml | 2 + test-suite/build.gradle | 1 + .../http/client/http2/Http2RequestSpec.groovy | 26 +- .../http/client/http2/Http3RequestSpec.groovy | 11 + 40 files changed, 1382 insertions(+), 173 deletions(-) create mode 100644 http-netty/src/main/java/io/micronaut/http/netty/channel/NettyChannelType.java create mode 100644 http-server-netty/src/main/java/io/micronaut/http/server/netty/QuicTokenHandlerImpl.java create mode 100644 http-server-netty/src/test/groovy/io/micronaut/http/server/netty/QuicTokenHandlerImplSpec.groovy create mode 100644 http-server-netty/src/test/groovy/io/micronaut/http/server/netty/http2/Http3Spec.groovy create mode 100644 src/main/docs/guide/httpClient/clientHttp3.adoc create mode 100644 src/main/docs/guide/httpServer/http3Server.adoc create mode 100644 test-suite/src/test/groovy/io/micronaut/http/client/http2/Http3RequestSpec.groovy diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 83b5c465b5a..4bee199dc5b 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -67,6 +67,7 @@ managed-jackson-databind = "2.14.1" managed-maven-native-plugin = "0.9.13" managed-methvin-directory-watcher = "0.16.1" managed-netty = "4.1.87.Final" +managed-netty-http3 = "0.0.16.Final" managed-reactive-streams = "1.0.4" # This should be kept aligned with https://github.com/micronaut-projects/micronaut-reactor/blob/master/gradle.properties from the BOM managed-reactor = "3.4.24" @@ -115,6 +116,7 @@ managed-methvin-directoryWatcher = { module = "io.methvin:directory-watcher", ve managed-netty-buffer = { module = "io.netty:netty-buffer", version.ref = "managed-netty" } managed-netty-codec-http = { module = "io.netty:netty-codec-http", version.ref = "managed-netty" } managed-netty-codec-http2 = { module = "io.netty:netty-codec-http2", version.ref = "managed-netty" } +managed-netty-incubator-codec-http3 = { module = "io.netty.incubator:netty-incubator-codec-http3", version.ref = "managed-netty-http3" } managed-netty-handler = { module = "io.netty:netty-handler", version.ref = "managed-netty" } managed-netty-handler-proxy = { module = "io.netty:netty-handler-proxy", version.ref = "managed-netty" } managed-netty-transport-native-epoll = { module = "io.netty:netty-transport-native-epoll", version.ref = "managed-netty" } diff --git a/http-client-core/src/main/java/io/micronaut/http/client/HttpVersionSelection.java b/http-client-core/src/main/java/io/micronaut/http/client/HttpVersionSelection.java index 4d8d8afce7a..5a1a243df49 100644 --- a/http-client-core/src/main/java/io/micronaut/http/client/HttpVersionSelection.java +++ b/http-client-core/src/main/java/io/micronaut/http/client/HttpVersionSelection.java @@ -40,6 +40,11 @@ public final class HttpVersionSelection { * ALPN protocol ID for HTTP/2. */ public static final String ALPN_HTTP_2 = "h2"; + /** + * ALPN protocol ID for HTTP/3. When this is selected, it must be the only ALPN ID, since we + * will connect via UDP. + */ + public static final String ALPN_HTTP_3 = "h3"; private static final HttpVersionSelection LEGACY_1 = new HttpVersionSelection( PlaintextMode.HTTP_1, @@ -59,12 +64,17 @@ public final class HttpVersionSelection { private final boolean alpn; private final String[] alpnSupportedProtocols; private final boolean http2CipherSuites; + private final boolean http3; private HttpVersionSelection(@NonNull PlaintextMode plaintextMode, boolean alpn, @NonNull String[] alpnSupportedProtocols, boolean http2CipherSuites) { this.plaintextMode = plaintextMode; this.alpn = alpn; this.alpnSupportedProtocols = alpnSupportedProtocols; this.http2CipherSuites = http2CipherSuites; + this.http3 = Arrays.asList(alpnSupportedProtocols).contains(ALPN_HTTP_3); + if (http3 && alpnSupportedProtocols.length != 1) { + throw new IllegalArgumentException("When using HTTP 3, h3 must be the only ALPN protocol"); + } } /** @@ -181,6 +191,11 @@ public boolean isHttp2CipherSuites() { return http2CipherSuites; } + @Internal + public boolean isHttp3() { + return http3; + } + /** * The connection mode to use for plaintext (non-TLS) connections. */ diff --git a/http-client-jdk/src/main/java/io/micronaut/http/client/jdk/AbstractJdkHttpClient.java b/http-client-jdk/src/main/java/io/micronaut/http/client/jdk/AbstractJdkHttpClient.java index 8a61bb3dd1c..2e125351625 100644 --- a/http-client-jdk/src/main/java/io/micronaut/http/client/jdk/AbstractJdkHttpClient.java +++ b/http-client-jdk/src/main/java/io/micronaut/http/client/jdk/AbstractJdkHttpClient.java @@ -53,6 +53,8 @@ import java.net.URISyntaxException; import java.net.http.HttpClient; import java.net.http.HttpRequest; +import java.util.Arrays; +import java.util.List; import java.util.Optional; import static io.micronaut.http.client.exceptions.HttpClientExceptionUtils.populateServiceId; @@ -69,6 +71,8 @@ abstract class AbstractJdkHttpClient { public static final String H2C_ERROR_MESSAGE = "H2C is not supported by the JDK HTTP client"; + public static final String H3_ERROR_MESSAGE = "HTTP/3 is not supported by the JDK HTTP client"; + public static final String WEIRD_ALPN_ERROR_MESSAGE = "The only supported ALPN modes are [" + HttpVersionSelection.ALPN_HTTP_1 + "] or [" + HttpVersionSelection.ALPN_HTTP_1 + "," + HttpVersionSelection.ALPN_HTTP_2 + "]"; protected final LoadBalancer loadBalancer; protected final HttpVersionSelection httpVersion; @@ -142,9 +146,22 @@ protected AbstractJdkHttpClient( if (httpVersionSelection.getPlaintextMode() == HttpVersionSelection.PlaintextMode.H2C) { throw new ConfigurationException(H2C_ERROR_MESSAGE); } + if (httpVersionSelection.isHttp3()) { + throw new ConfigurationException(H3_ERROR_MESSAGE); + } - if (httpVersionSelection.isAlpn() && httpVersionSelection.isHttp2CipherSuites()) { - builder.version(HttpClient.Version.HTTP_2); + if (httpVersionSelection.isAlpn()) { + List supportedProtocols = Arrays.asList(httpVersionSelection.getAlpnSupportedProtocols()); + if (supportedProtocols.size() == 2 && + supportedProtocols.contains(HttpVersionSelection.ALPN_HTTP_1) && + supportedProtocols.contains(HttpVersionSelection.ALPN_HTTP_2)) { + builder.version(HttpClient.Version.HTTP_2); + } else if (supportedProtocols.size() == 1 && + supportedProtocols.get(0).equals(HttpVersionSelection.ALPN_HTTP_1)) { + builder.version(HttpClient.Version.HTTP_1_1); + } else { + throw new ConfigurationException(WEIRD_ALPN_ERROR_MESSAGE); + } } else { builder.version(HttpClient.Version.HTTP_1_1); } diff --git a/http-client-jdk/src/test/groovy/io/micronaut/http/client/jdk/H2CSpec.groovy b/http-client-jdk/src/test/groovy/io/micronaut/http/client/jdk/H2CSpec.groovy index 1990b2f51cc..71bd8fc5726 100644 --- a/http-client-jdk/src/test/groovy/io/micronaut/http/client/jdk/H2CSpec.groovy +++ b/http-client-jdk/src/test/groovy/io/micronaut/http/client/jdk/H2CSpec.groovy @@ -25,4 +25,40 @@ class H2CSpec extends Specification { cleanup: ctx.close() } + + def "h3 is not supported"() { + given: + def ctx = ApplicationContext.run( + 'micronaut.http.client.alpn-modes': ['h3'] + ) + + when: + ctx.getBean(HttpClient) + + then: + def ex = thrown(BeanInstantiationException) + ex.cause instanceof ConfigurationException + ex.cause.message == AbstractJdkHttpClient.H3_ERROR_MESSAGE + + cleanup: + ctx.close() + } + + def "http2-only is not supported"() { + given: + def ctx = ApplicationContext.run( + 'micronaut.http.client.alpn-modes': ['h2'] + ) + + when: + ctx.getBean(HttpClient) + + then: + def ex = thrown(BeanInstantiationException) + ex.cause instanceof ConfigurationException + ex.cause.message == AbstractJdkHttpClient.WEIRD_ALPN_ERROR_MESSAGE + + cleanup: + ctx.close() + } } diff --git a/http-client/build.gradle b/http-client/build.gradle index 82f794893d9..edab284666d 100644 --- a/http-client/build.gradle +++ b/http-client/build.gradle @@ -17,6 +17,8 @@ dependencies { api project(":http-netty") api libs.managed.netty.handler.proxy + compileOnly libs.managed.netty.incubator.codec.http3 + testAnnotationProcessor platform(libs.test.boms.micronaut.validation) testAnnotationProcessor (libs.micronaut.validation.processor) { exclude group: 'io.micronaut' diff --git a/http-client/src/main/java/io/micronaut/http/client/netty/ConnectionManager.java b/http-client/src/main/java/io/micronaut/http/client/netty/ConnectionManager.java index 73464086430..88371f83c92 100644 --- a/http-client/src/main/java/io/micronaut/http/client/netty/ConnectionManager.java +++ b/http-client/src/main/java/io/micronaut/http/client/netty/ConnectionManager.java @@ -41,7 +41,9 @@ import io.netty.channel.ChannelInboundHandlerAdapter; import io.netty.channel.ChannelInitializer; import io.netty.channel.ChannelOption; +import io.netty.channel.ChannelOutboundHandlerAdapter; import io.netty.channel.ChannelPipeline; +import io.netty.channel.ChannelPromise; import io.netty.channel.EventLoopGroup; import io.netty.channel.nio.NioEventLoopGroup; import io.netty.handler.codec.DecoderException; @@ -52,11 +54,13 @@ import io.netty.handler.codec.http.HttpHeaderNames; import io.netty.handler.codec.http.HttpMethod; import io.netty.handler.codec.http.HttpObjectAggregator; +import io.netty.handler.codec.http.HttpScheme; import io.netty.handler.codec.http.websocketx.extensions.compression.WebSocketClientCompressionHandler; import io.netty.handler.codec.http2.Http2ClientUpgradeCodec; import io.netty.handler.codec.http2.Http2FrameCodec; import io.netty.handler.codec.http2.Http2FrameCodecBuilder; import io.netty.handler.codec.http2.Http2FrameLogger; +import io.netty.handler.codec.http2.Http2HeadersFrame; import io.netty.handler.codec.http2.Http2MultiplexHandler; import io.netty.handler.codec.http2.Http2SettingsAckFrame; import io.netty.handler.codec.http2.Http2SettingsFrame; @@ -74,6 +78,15 @@ import io.netty.handler.timeout.IdleStateHandler; import io.netty.handler.timeout.ReadTimeoutException; import io.netty.handler.timeout.ReadTimeoutHandler; +import io.netty.incubator.codec.http3.Http3; +import io.netty.incubator.codec.http3.Http3ClientConnectionHandler; +import io.netty.incubator.codec.http3.Http3FrameToHttpObjectCodec; +import io.netty.incubator.codec.http3.Http3HeadersFrame; +import io.netty.incubator.codec.http3.Http3RequestStreamInitializer; +import io.netty.incubator.codec.http3.Http3SettingsFrame; +import io.netty.incubator.codec.quic.QuicChannel; +import io.netty.incubator.codec.quic.QuicSslContext; +import io.netty.incubator.codec.quic.QuicStreamChannel; import io.netty.resolver.NoopAddressResolverGroup; import io.netty.util.ReferenceCountUtil; import io.netty.util.ResourceLeakDetector; @@ -123,9 +136,12 @@ class ConnectionManager { private final boolean shutdownGroup; private final ThreadFactory threadFactory; private final ChannelFactory socketChannelFactory; + private final ChannelFactory udpChannelFactory; private Bootstrap bootstrap; + private Bootstrap udpBootstrap; private final HttpClientConfiguration configuration; private final SslContext sslContext; + private final /* QuicSslContext */ Object http3SslContext; private final NettyClientCustomizer clientCustomizer; private final String informationalServiceId; @@ -142,9 +158,12 @@ class ConnectionManager { this.shutdownGroup = from.shutdownGroup; this.threadFactory = from.threadFactory; this.socketChannelFactory = from.socketChannelFactory; + this.udpChannelFactory = from.udpChannelFactory; this.bootstrap = from.bootstrap; + this.udpBootstrap = from.udpBootstrap; this.configuration = from.configuration; this.sslContext = from.sslContext; + this.http3SslContext = from.http3SslContext; this.clientCustomizer = from.clientCustomizer; this.informationalServiceId = from.informationalServiceId; } @@ -154,9 +173,10 @@ class ConnectionManager { @Nullable EventLoopGroup eventLoopGroup, @Nullable ThreadFactory threadFactory, HttpClientConfiguration configuration, - @Nullable HttpVersionSelection httpVersion, + @Nullable HttpVersionSelection httpVersion, InvocationInstrumenter instrumenter, ChannelFactory socketChannelFactory, + ChannelFactory udpChannelFactory, NettyClientSslBuilder nettyClientSslBuilder, NettyClientCustomizer clientCustomizer, String informationalServiceId) { @@ -169,12 +189,18 @@ class ConnectionManager { this.httpVersion = httpVersion; this.threadFactory = threadFactory; this.socketChannelFactory = socketChannelFactory; + this.udpChannelFactory = udpChannelFactory; this.configuration = configuration; this.instrumenter = instrumenter; this.clientCustomizer = clientCustomizer; this.informationalServiceId = informationalServiceId; this.sslContext = nettyClientSslBuilder.build(configuration.getSslConfiguration(), httpVersion); + if (httpVersion.isHttp3()) { + this.http3SslContext = nettyClientSslBuilder.buildHttp3(configuration.getSslConfiguration()); + } else { + this.http3SslContext = null; + } if (eventLoopGroup != null) { group = eventLoopGroup; @@ -284,10 +310,15 @@ public void start() { } private void initBootstrap() { - this.bootstrap = new Bootstrap(); - this.bootstrap.group(group) + this.bootstrap = new Bootstrap() + .group(group) .channelFactory(socketChannelFactory) .option(ChannelOption.SO_KEEPALIVE, true); + if (httpVersion.isHttp3()) { + this.udpBootstrap = new Bootstrap() + .group(group) + .channelFactory(udpChannelFactory); + } } /** @@ -477,7 +508,7 @@ private void configureProxy(ChannelPipeline pipeline, boolean secure, String hos } > void addInstrumentedListener( - Future channelFuture, GenericFutureListener listener) { + Future channelFuture, GenericFutureListener listener) { channelFuture.addListener(f -> { try (Instrumentation ignored = instrumenter.newInstrumentation()) { //noinspection unchecked @@ -729,6 +760,95 @@ public void channelActive(@NonNull ChannelHandlerContext ctx) throws Exception { } } + private final class Http3ChannelInitializer extends ChannelOutboundHandlerAdapter { + private final Pool pool; + + private final String host; + private final int port; + + Http3ChannelInitializer(Pool pool, String host, int port) { + this.pool = pool; + this.host = host; + this.port = port; + } + + // delay channel initialization until bind is complete. This is required so that we can see + // the local address + @Override + public void bind(ChannelHandlerContext ctx, SocketAddress localAddress, ChannelPromise promise) throws Exception { + ChannelPromise downstreamPromise = ctx.newPromise(); + super.bind(ctx, localAddress, downstreamPromise); + downstreamPromise.addListener(future -> { + if (future.isSuccess()) { + try { + initChannel(promise.channel()); + ctx.pipeline().remove(this); + promise.setSuccess(); + } catch (Exception e) { + promise.setFailure(e); + } + } else { + promise.setFailure(future.cause()); + } + }); + } + + private void initChannel(Channel ch) { + NettyClientCustomizer channelCustomizer = clientCustomizer.specializeForChannel(ch, NettyClientCustomizer.ChannelRole.CONNECTION); + + ch.pipeline() + .addLast(Http3.newQuicClientCodecBuilder() + .sslEngineProvider(c -> ((QuicSslContext) http3SslContext).newEngine(c.alloc(), host, port)) + .initialMaxData(10000000) + .initialMaxStreamDataBidirectionalLocal(1000000) + .build()) + .addLast(ChannelPipelineCustomizer.HANDLER_INITIAL_ERROR, pool.initialErrorHandler); + + channelCustomizer.onInitialPipelineBuilt(); + + QuicChannel.newBootstrap(ch) + .handler(new ChannelInboundHandlerAdapter() { + @Override + public void handlerAdded(ChannelHandlerContext ctx) throws Exception { + QuicChannel quicChannel = (QuicChannel) ctx.channel(); + ctx.pipeline().addLast(ChannelPipelineCustomizer.HANDLER_HTTP2_CONNECTION, new Http3ClientConnectionHandler( + // control stream handler + new ChannelInboundHandlerAdapter() { + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { + if (msg instanceof Http3SettingsFrame) { + ch.pipeline().remove(ChannelPipelineCustomizer.HANDLER_INITIAL_ERROR); + pool.new Http3ConnectionHolder(ch, quicChannel, channelCustomizer).init(); + } + super.channelRead(ctx, msg); + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { + ch.pipeline().remove(ChannelPipelineCustomizer.HANDLER_INITIAL_ERROR); + ch.close(); + pool.onNewConnectionFailure(cause); + } + }, + null, + null, + null, + false + )); + ctx.pipeline().remove(this); + } + }) + .remoteAddress(new InetSocketAddress(this.host, this.port)) + .localAddress(ch.localAddress()) + .connect() + .addListener((GenericFutureListener>) future -> { + if (!future.isSuccess()) { + pool.onNewConnectionFailure(future.cause()); + } + }); + } + } + /** * Handle for a pooled connection. One pool handle generally corresponds to one request, and * once the request and response are done, the handle is {@link #release() released} and a new @@ -846,8 +966,28 @@ void onNewConnectionFailure(@Nullable Throwable error) throws Exception { @Override void openNewConnection(@Nullable BlockHint blockHint) throws Exception { // open a new connection + ChannelFuture channelFuture = openConnectionFuture(); + if (blockHint != null && blockHint.blocks(channelFuture.channel().eventLoop())) { + channelFuture.channel().close(); + onNewConnectionFailure(BlockHint.createException()); + return; + } + addInstrumentedListener(channelFuture, future -> { + if (!future.isSuccess()) { + onNewConnectionFailure(future.cause()); + } + }); + } + + private ChannelFuture openConnectionFuture() { ChannelInitializer initializer; if (requestKey.isSecure()) { + if (httpVersion.isHttp3()) { + return udpBootstrap.clone() + .handler(new Http3ChannelInitializer(this, requestKey.getHost(), requestKey.getPort())) + .bind(0); + } + initializer = new AdaptiveAlpnChannelInitializer( this, buildSslContext(requestKey), @@ -881,17 +1021,7 @@ public void channelActive(@NonNull ChannelHandlerContext ctx) throws Exception { throw new AssertionError("Unknown plaintext mode"); } } - ChannelFuture channelFuture = doConnect(requestKey, initializer); - if (blockHint != null && blockHint.blocks(channelFuture.channel().eventLoop())) { - channelFuture.channel().close(); - onNewConnectionFailure(BlockHint.createException()); - return; - } - addInstrumentedListener(channelFuture, future -> { - if (!future.isSuccess()) { - onNewConnectionFailure(future.cause()); - } - }); + return doConnect(requestKey, initializer); } public void shutdown() { @@ -1158,7 +1288,7 @@ void onInactive() { } } - final class Http2ConnectionHolder extends ConnectionHolder { + class Http2ConnectionHolder extends ConnectionHolder { private final AtomicInteger liveRequests = new AtomicInteger(0); private final Set liveStreamChannels = new HashSet<>(); // todo: https://github.com/netty/netty/pull/12830 @@ -1167,15 +1297,19 @@ final class Http2ConnectionHolder extends ConnectionHolder { } void init() { + addTimeoutHandlers(); + + connectionCustomizer.onStreamPipelineBuilt(); + + onNewConnectionEstablished2(this); + } + + void addTimeoutHandlers() { addTimeoutHandlers( requestKey.isSecure() ? ChannelPipelineCustomizer.HANDLER_SSL : ChannelPipelineCustomizer.HANDLER_HTTP2_CONNECTION ); - - connectionCustomizer.onStreamPipelineBuilt(); - - onNewConnectionEstablished2(this); } @Override @@ -1201,11 +1335,18 @@ void dispatch0(PoolSink sink) { returnPendingRequest(sink); return; } - addInstrumentedListener(new Http2StreamChannelBootstrap(channel).open(), (Future future) -> { + addInstrumentedListener(openStreamChannel(), (Future future) -> { if (future.isSuccess()) { - Http2StreamChannel streamChannel = future.get(); + Channel streamChannel = future.get(); streamChannel.pipeline() - .addLast(new Http2StreamFrameToHttpObjectCodec(false)) + .addLast(new ChannelOutboundHandlerAdapter() { + @Override + public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception { + adaptHeaders(msg); + super.write(ctx, msg, promise); + } + }) + .addLast(createFrameToHttpObjectCodec()) .addLast(ChannelPipelineCustomizer.HANDLER_HTTP_DECOMPRESSOR, new HttpContentDecompressor()); NettyClientCustomizer streamCustomizer = connectionCustomizer.specializeForChannel(streamChannel, NettyClientCustomizer.ChannelRole.HTTP2_STREAM); PoolHandle ph = new PoolHandle(true, streamChannel) { @@ -1246,6 +1387,25 @@ void notifyRequestPipelineBuilt() { }); } + @NonNull + ChannelHandler createFrameToHttpObjectCodec() { + return new Http2StreamFrameToHttpObjectCodec(false); + } + + Future openStreamChannel() { + return new Http2StreamChannelBootstrap(channel).open(); + } + + void adaptHeaders(Object msg) { + if (msg instanceof Http2HeadersFrame hf) { + if (requestKey.isSecure()) { + hf.headers().scheme(HttpScheme.HTTPS.name()); + } else { + hf.headers().scheme(HttpScheme.HTTP.name()); + } + } + } + private void returnPendingRequest(PoolSink sink) { // failed, but the pending request may still work on another connection. addPendingRequest(sink); @@ -1266,5 +1426,53 @@ void onInactive() { onConnectionInactive2(this); } } + + final class Http3ConnectionHolder extends Http2ConnectionHolder { + private final Channel udpChannel; + private final QuicChannel quicChannel; + + Http3ConnectionHolder(Channel channel, QuicChannel quicChannel, NettyClientCustomizer customizer) { + super(quicChannel, customizer); + this.udpChannel = channel; + this.quicChannel = quicChannel; + } + + @Override + void adaptHeaders(Object msg) { + if (msg instanceof Http3HeadersFrame hf) { + if (requestKey.isSecure()) { + hf.headers().scheme(HttpScheme.HTTPS.name()); + } else { + hf.headers().scheme(HttpScheme.HTTP.name()); + } + } + } + + @Override + void addTimeoutHandlers() { + addTimeoutHandlers(ChannelPipelineCustomizer.HANDLER_HTTP2_CONNECTION); + } + + @Override + ChannelHandler createFrameToHttpObjectCodec() { + return new Http3FrameToHttpObjectCodec(false); + } + + @Override + Future openStreamChannel() { + return Http3.newRequestStream(quicChannel, new Http3RequestStreamInitializer() { + @Override + protected void initRequestStream(QuicStreamChannel ch) { + // do nothing, channel is initialized in the future handler + } + }); + } + + @Override + void onInactive() { + super.onInactive(); + udpChannel.close(); + } + } } } diff --git a/http-client/src/main/java/io/micronaut/http/client/netty/DefaultHttpClient.java b/http-client/src/main/java/io/micronaut/http/client/netty/DefaultHttpClient.java index c051abacfd3..50036f2c2a7 100644 --- a/http-client/src/main/java/io/micronaut/http/client/netty/DefaultHttpClient.java +++ b/http-client/src/main/java/io/micronaut/http/client/netty/DefaultHttpClient.java @@ -83,7 +83,6 @@ import io.micronaut.http.filter.HttpClientFilterResolver; import io.micronaut.http.filter.HttpFilterResolver; import io.micronaut.http.multipart.MultipartException; -import io.micronaut.http.netty.AbstractNettyHttpRequest; import io.micronaut.http.netty.NettyHttpHeaders; import io.micronaut.http.netty.NettyHttpRequestBuilder; import io.micronaut.http.netty.NettyHttpResponseBuilder; @@ -125,6 +124,9 @@ import io.netty.channel.ChannelPipeline; import io.netty.channel.EventLoopGroup; import io.netty.channel.MultithreadEventLoopGroup; +import io.netty.channel.socket.DatagramChannel; +import io.netty.channel.socket.SocketChannel; +import io.netty.channel.socket.nio.NioDatagramChannel; import io.netty.channel.socket.nio.NioSocketChannel; import io.netty.handler.codec.TooLongFrameException; import io.netty.handler.codec.http.DefaultFullHttpResponse; @@ -142,7 +144,6 @@ import io.netty.handler.codec.http.HttpObjectAggregator; import io.netty.handler.codec.http.HttpRequest; import io.netty.handler.codec.http.HttpResponseStatus; -import io.netty.handler.codec.http.HttpScheme; import io.netty.handler.codec.http.HttpUtil; import io.netty.handler.codec.http.LastHttpContent; import io.netty.handler.codec.http.multipart.DefaultHttpDataFactory; @@ -297,6 +298,7 @@ public DefaultHttpClient(@Nullable LoadBalancer loadBalancer, new DefaultRequestBinderRegistry(conversionService), null, NioSocketChannel::new, + NioDatagramChannel::new, CompositeNettyClientCustomizer.EMPTY, invocationInstrumenterFactories, null, @@ -305,7 +307,7 @@ public DefaultHttpClient(@Nullable LoadBalancer loadBalancer, /** * Construct a client for the given arguments. - * @param loadBalancer The {@link LoadBalancer} to use for selecting servers + * @param loadBalancer The {@link LoadBalancer} to use for selecting servers * @param explicitHttpVersion The HTTP version to use. Can be null and defaults to {@link io.micronaut.http.HttpVersion#HTTP_1_1} * @param configuration The {@link HttpClientConfiguration} object * @param contextPath The base URI to prepend to request uris @@ -318,6 +320,7 @@ public DefaultHttpClient(@Nullable LoadBalancer loadBalancer, * @param requestBinderRegistry The request binder registry * @param eventLoopGroup The event loop group to use * @param socketChannelFactory The socket channel factory + * @param udpChannelFactory The UDP channel factory * @param clientCustomizer The pipeline customizer * @param invocationInstrumenterFactories The invocation instrumeter factories to instrument netty handlers execution with * @param informationalServiceId Optional service ID that will be passed to exceptions created by this client @@ -335,7 +338,8 @@ public DefaultHttpClient(@Nullable LoadBalancer loadBalancer, @NonNull WebSocketBeanRegistry webSocketBeanRegistry, @NonNull RequestBinderRegistry requestBinderRegistry, @Nullable EventLoopGroup eventLoopGroup, - @NonNull ChannelFactory socketChannelFactory, + @NonNull ChannelFactory socketChannelFactory, + @NonNull ChannelFactory udpChannelFactory, NettyClientCustomizer clientCustomizer, List invocationInstrumenterFactories, @Nullable String informationalServiceId, @@ -386,6 +390,7 @@ public DefaultHttpClient(@Nullable LoadBalancer loadBalancer, explicitHttpVersion, combineFactories(), socketChannelFactory, + udpChannelFactory, nettyClientSslBuilder, clientCustomizer, informationalServiceId); @@ -1956,15 +1961,6 @@ private class NettyRequestWriter { * @param emitter The emitter */ protected void write(ConnectionManager.PoolHandle poolHandle, boolean isSecure, FluxSink emitter) { - if (poolHandle.http2) { - // todo: move to ConnectionManager, DefaultHttpClient shouldn't care about the scheme - if (isSecure) { - nettyRequest.headers().add(AbstractNettyHttpRequest.HTTP2_SCHEME, HttpScheme.HTTPS); - } else { - nettyRequest.headers().add(AbstractNettyHttpRequest.HTTP2_SCHEME, HttpScheme.HTTP); - } - } - Channel channel = poolHandle.channel; ChannelFuture writeFuture; if (encoder != null && encoder.isChunked()) { diff --git a/http-client/src/main/java/io/micronaut/http/client/netty/DefaultNettyHttpClientRegistry.java b/http-client/src/main/java/io/micronaut/http/client/netty/DefaultNettyHttpClientRegistry.java index e7391e0ccb7..4642c26b993 100644 --- a/http-client/src/main/java/io/micronaut/http/client/netty/DefaultNettyHttpClientRegistry.java +++ b/http-client/src/main/java/io/micronaut/http/client/netty/DefaultNettyHttpClientRegistry.java @@ -56,6 +56,7 @@ import io.micronaut.http.netty.channel.EventLoopGroupConfiguration; import io.micronaut.http.netty.channel.EventLoopGroupFactory; import io.micronaut.http.netty.channel.EventLoopGroupRegistry; +import io.micronaut.http.netty.channel.NettyChannelType; import io.micronaut.inject.InjectionPoint; import io.micronaut.inject.qualifiers.Qualifiers; import io.micronaut.json.JsonFeatures; @@ -65,8 +66,11 @@ import io.micronaut.websocket.WebSocketClient; import io.micronaut.websocket.WebSocketClientRegistry; import io.micronaut.websocket.context.WebSocketBeanRegistry; +import io.netty.channel.Channel; import io.netty.channel.ChannelFactory; import io.netty.channel.EventLoopGroup; +import io.netty.channel.socket.DatagramChannel; +import io.netty.channel.socket.SocketChannel; import jakarta.annotation.PreDestroy; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -418,7 +422,8 @@ private DefaultHttpClient buildClient( new DefaultRequestBinderRegistry(conversionService) ), eventLoopGroup, - resolveSocketChannelFactory(configuration, beanContext), + resolveSocketChannelFactory(NettyChannelType.CLIENT_SOCKET, SocketChannel.class, configuration, beanContext), + resolveSocketChannelFactory(NettyChannelType.DATAGRAM_SOCKET, DatagramChannel.class, configuration, beanContext), clientCustomizer, invocationInstrumenterFactories, clientId, @@ -461,7 +466,7 @@ private DefaultHttpClient resolveDefaultHttpClient( } } - private ChannelFactory resolveSocketChannelFactory(HttpClientConfiguration configuration, BeanContext beanContext) { + private ChannelFactory resolveSocketChannelFactory(NettyChannelType type, Class channelClass, HttpClientConfiguration configuration, BeanContext beanContext) { final String eventLoopGroup = configuration.getEventLoopGroup(); final EventLoopGroupConfiguration eventLoopGroupConfiguration = beanContext.findBean(EventLoopGroupConfiguration.class, Qualifiers.byName(eventLoopGroup)) @@ -473,7 +478,7 @@ private ChannelFactory resolveSocketChannelFactory(HttpClientConfiguration confi } }); - return () -> eventLoopGroupFactory.clientSocketChannelInstance(eventLoopGroupConfiguration); + return () -> channelClass.cast(eventLoopGroupFactory.channelInstance(type, eventLoopGroupConfiguration)); } private ClientKey getClientKey(AnnotationMetadata metadata) { diff --git a/http-client/src/main/java/io/micronaut/http/client/netty/ssl/NettyClientSslBuilder.java b/http-client/src/main/java/io/micronaut/http/client/netty/ssl/NettyClientSslBuilder.java index 6bcab80613c..8abb19398aa 100644 --- a/http-client/src/main/java/io/micronaut/http/client/netty/ssl/NettyClientSslBuilder.java +++ b/http-client/src/main/java/io/micronaut/http/client/netty/ssl/NettyClientSslBuilder.java @@ -34,6 +34,9 @@ import io.netty.handler.ssl.SslProvider; import io.netty.handler.ssl.SupportedCipherSuiteFilter; import io.netty.handler.ssl.util.InsecureTrustManagerFactory; +import io.netty.incubator.codec.http3.Http3; +import io.netty.incubator.codec.quic.QuicSslContext; +import io.netty.incubator.codec.quic.QuicSslContextBuilder; import jakarta.inject.Singleton; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -121,6 +124,24 @@ public SslContext build(SslConfiguration ssl, HttpVersionSelection versionSelect } } + public QuicSslContext buildHttp3(SslConfiguration ssl) { + QuicSslContextBuilder sslBuilder = QuicSslContextBuilder.forClient() + .keyManager(getKeyManagerFactory(ssl), ssl.getKeyStore().getPassword().orElse(null)) + .trustManager(getTrustManagerFactory(ssl)) + .applicationProtocols(Http3.supportedApplicationProtocols()); + Optional clientAuthentication = ssl.getClientAuthentication(); + if (clientAuthentication.isPresent()) { + ClientAuthentication clientAuth = clientAuthentication.get(); + if (clientAuth == ClientAuthentication.NEED) { + sslBuilder.clientAuth(ClientAuth.REQUIRE); + } else if (clientAuth == ClientAuthentication.WANT) { + sslBuilder.clientAuth(ClientAuth.OPTIONAL); + } + } + + return sslBuilder.build(); + } + @Override protected KeyManagerFactory getKeyManagerFactory(SslConfiguration ssl) { try { diff --git a/http-netty/src/main/java/io/micronaut/http/netty/AbstractNettyHttpRequest.java b/http-netty/src/main/java/io/micronaut/http/netty/AbstractNettyHttpRequest.java index ce2e3b28dc5..3371ec42cca 100644 --- a/http-netty/src/main/java/io/micronaut/http/netty/AbstractNettyHttpRequest.java +++ b/http-netty/src/main/java/io/micronaut/http/netty/AbstractNettyHttpRequest.java @@ -22,7 +22,6 @@ import io.micronaut.http.HttpMethod; import io.micronaut.http.HttpParameters; import io.micronaut.http.HttpRequest; -import io.micronaut.http.HttpVersion; import io.micronaut.http.MediaType; import io.micronaut.http.netty.stream.DefaultStreamedHttpRequest; import io.micronaut.http.netty.stream.StreamedHttpRequest; @@ -30,8 +29,6 @@ import io.netty.handler.codec.http.DefaultLastHttpContent; import io.netty.handler.codec.http.LastHttpContent; import io.netty.handler.codec.http.QueryStringDecoder; -import io.netty.handler.codec.http2.HttpConversionUtil; -import io.netty.util.AsciiString; import io.netty.util.DefaultAttributeMap; import java.net.URI; @@ -51,8 +48,6 @@ @Internal public abstract class AbstractNettyHttpRequest extends DefaultAttributeMap implements HttpRequest, NettyHttpRequestBuilder { - public static final AsciiString STREAM_ID = HttpConversionUtil.ExtensionHeaderNames.STREAM_ID.text(); - public static final AsciiString HTTP2_SCHEME = HttpConversionUtil.ExtensionHeaderNames.SCHEME.text(); protected final io.netty.handler.codec.http.HttpRequest nettyRequest; protected final ConversionService conversionService; protected final HttpMethod httpMethod; @@ -146,14 +141,6 @@ public boolean isStream() { return this.nettyRequest instanceof StreamedHttpRequest; } - @Override - public HttpVersion getHttpVersion() { - if (nettyRequest.headers().contains(HTTP2_SCHEME)) { - return HttpVersion.HTTP_2_0; - } - return HttpVersion.HTTP_1_1; - } - /** * @return The native netty request */ diff --git a/http-netty/src/main/java/io/micronaut/http/netty/channel/DefaultEventLoopGroupFactory.java b/http-netty/src/main/java/io/micronaut/http/netty/channel/DefaultEventLoopGroupFactory.java index da2ad48c3f5..727882e416a 100644 --- a/http-netty/src/main/java/io/micronaut/http/netty/channel/DefaultEventLoopGroupFactory.java +++ b/http-netty/src/main/java/io/micronaut/http/netty/channel/DefaultEventLoopGroupFactory.java @@ -21,6 +21,7 @@ import io.micronaut.core.annotation.Nullable; import io.micronaut.core.util.ArgumentUtils; import io.micronaut.http.netty.configuration.NettyGlobalConfiguration; +import io.netty.channel.Channel; import io.netty.channel.EventLoopGroup; import io.netty.channel.ServerChannel; import io.netty.channel.socket.ServerSocketChannel; @@ -105,6 +106,11 @@ public Class domainServerSocketChannelClass return nativeFactory.domainServerSocketChannelClass(); } + @Override + public Class channelClass(NettyChannelType type) throws UnsupportedOperationException { + return nativeFactory.channelClass(type); + } + @NonNull @Override public Class serverSocketChannelClass(EventLoopGroupConfiguration configuration) { @@ -117,6 +123,11 @@ public Class domainServerSocketChannelClass return getFactory(configuration).domainServerSocketChannelClass(configuration); } + @Override + public Class channelClass(NettyChannelType type, @Nullable EventLoopGroupConfiguration configuration) { + return getFactory(configuration).channelClass(type, configuration); + } + @Override public ServerSocketChannel serverSocketChannelInstance(EventLoopGroupConfiguration configuration) { return getFactory(configuration).serverSocketChannelInstance(configuration); @@ -127,6 +138,11 @@ public ServerChannel domainServerSocketChannelInstance(@Nullable EventLoopGroupC return getFactory(configuration).domainServerSocketChannelInstance(configuration); } + @Override + public Channel channelInstance(NettyChannelType type, @Nullable EventLoopGroupConfiguration configuration) { + return getFactory(configuration).channelInstance(type, configuration); + } + @NonNull @Override public Class clientSocketChannelClass(@Nullable EventLoopGroupConfiguration configuration) { diff --git a/http-netty/src/main/java/io/micronaut/http/netty/channel/EpollEventLoopGroupFactory.java b/http-netty/src/main/java/io/micronaut/http/netty/channel/EpollEventLoopGroupFactory.java index cc95e32635a..dcba8f904aa 100644 --- a/http-netty/src/main/java/io/micronaut/http/netty/channel/EpollEventLoopGroupFactory.java +++ b/http-netty/src/main/java/io/micronaut/http/netty/channel/EpollEventLoopGroupFactory.java @@ -20,9 +20,11 @@ import io.micronaut.core.annotation.Internal; import io.micronaut.core.annotation.NonNull; import io.micronaut.core.annotation.Nullable; +import io.netty.channel.Channel; import io.netty.channel.EventLoopGroup; import io.netty.channel.ServerChannel; import io.netty.channel.epoll.Epoll; +import io.netty.channel.epoll.EpollDatagramChannel; import io.netty.channel.epoll.EpollEventLoopGroup; import io.netty.channel.epoll.EpollServerDomainSocketChannel; import io.netty.channel.epoll.EpollServerSocketChannel; @@ -124,4 +126,28 @@ public boolean isNative() { return true; } + @Override + public Class channelClass(NettyChannelType type) throws UnsupportedOperationException { + return switch (type) { + case SERVER_SOCKET -> EpollServerSocketChannel.class; + case CLIENT_SOCKET -> EpollSocketChannel.class; + case DOMAIN_SERVER_SOCKET -> EpollServerDomainSocketChannel.class; + case DATAGRAM_SOCKET -> EpollDatagramChannel.class; + }; + } + + @Override + public Class channelClass(NettyChannelType type, @Nullable EventLoopGroupConfiguration configuration) { + return channelClass(type); + } + + @Override + public Channel channelInstance(NettyChannelType type, @Nullable EventLoopGroupConfiguration configuration) { + return switch (type) { + case SERVER_SOCKET -> new EpollServerSocketChannel(); + case CLIENT_SOCKET -> new EpollSocketChannel(); + case DOMAIN_SERVER_SOCKET -> new EpollServerDomainSocketChannel(); + case DATAGRAM_SOCKET -> new EpollDatagramChannel(); + }; + } } diff --git a/http-netty/src/main/java/io/micronaut/http/netty/channel/EventLoopGroupFactory.java b/http-netty/src/main/java/io/micronaut/http/netty/channel/EventLoopGroupFactory.java index b63a04a4e53..8591e399c50 100644 --- a/http-netty/src/main/java/io/micronaut/http/netty/channel/EventLoopGroupFactory.java +++ b/http-netty/src/main/java/io/micronaut/http/netty/channel/EventLoopGroupFactory.java @@ -18,6 +18,7 @@ import io.micronaut.core.annotation.NonNull; import io.micronaut.core.annotation.Nullable; import io.micronaut.core.util.ArgumentUtils; +import io.netty.channel.Channel; import io.netty.channel.EventLoopGroup; import io.netty.channel.ServerChannel; import io.netty.channel.socket.ServerSocketChannel; @@ -121,6 +122,22 @@ default Class domainServerSocketChannelClas throw new UnsupportedOperationException("Domain server socket channels not supported by this transport"); } + /** + * Returns the domain socket server channel class. + * + * @param type Type of the channel to return + * @return A channel class. + * @throws UnsupportedOperationException if domain sockets are not supported. + */ + @NonNull + default Class channelClass(NettyChannelType type) throws UnsupportedOperationException { + return switch (type) { + case SERVER_SOCKET -> serverSocketChannelClass(); + case DOMAIN_SERVER_SOCKET -> domainServerSocketChannelClass(); + default -> throw new UnsupportedOperationException("Channel type not supported"); + }; + } + /** * Returns the server channel class. * @@ -142,6 +159,23 @@ default Class domainServerSocketChannelClas return domainServerSocketChannelClass(); } + /** + * Returns the domain socket server channel class. + * + * @param type Type of the channel to return + * @param configuration The configuration + * @return A channel implementation. + * @throws UnsupportedOperationException if domain sockets are not supported. + */ + default @NonNull Class channelClass(NettyChannelType type, @Nullable EventLoopGroupConfiguration configuration) { + return switch (type) { + case SERVER_SOCKET -> serverSocketChannelClass(configuration); + case CLIENT_SOCKET -> clientSocketChannelClass(configuration); + case DOMAIN_SERVER_SOCKET -> domainServerSocketChannelClass(configuration); + default -> throw new UnsupportedOperationException("Channel type not supported"); + }; + } + /** * Returns the server channel class instance. * @@ -171,6 +205,23 @@ default Class domainServerSocketChannelClas } } + /** + * Returns the domain socket server channel class. + * + * @param type Type of the channel to return + * @param configuration The configuration + * @return A ServerDomainSocketChannel implementation. + * @throws UnsupportedOperationException if domain sockets are not supported. + */ + default @NonNull Channel channelInstance(NettyChannelType type, @Nullable EventLoopGroupConfiguration configuration) { + return switch (type) { + case SERVER_SOCKET -> serverSocketChannelInstance(configuration); + case CLIENT_SOCKET -> clientSocketChannelInstance(configuration); + case DOMAIN_SERVER_SOCKET -> domainServerSocketChannelInstance(configuration); + default -> throw new UnsupportedOperationException("Channel type not supported"); + }; + } + /** * Returns the client channel class. * diff --git a/http-netty/src/main/java/io/micronaut/http/netty/channel/KQueueEventLoopGroupFactory.java b/http-netty/src/main/java/io/micronaut/http/netty/channel/KQueueEventLoopGroupFactory.java index cf2fd6c016a..5f90b9ee4ae 100644 --- a/http-netty/src/main/java/io/micronaut/http/netty/channel/KQueueEventLoopGroupFactory.java +++ b/http-netty/src/main/java/io/micronaut/http/netty/channel/KQueueEventLoopGroupFactory.java @@ -20,9 +20,11 @@ import io.micronaut.core.annotation.Internal; import io.micronaut.core.annotation.NonNull; import io.micronaut.core.annotation.Nullable; +import io.netty.channel.Channel; import io.netty.channel.EventLoopGroup; import io.netty.channel.ServerChannel; import io.netty.channel.kqueue.KQueue; +import io.netty.channel.kqueue.KQueueDatagramChannel; import io.netty.channel.kqueue.KQueueEventLoopGroup; import io.netty.channel.kqueue.KQueueServerDomainSocketChannel; import io.netty.channel.kqueue.KQueueServerSocketChannel; @@ -130,4 +132,28 @@ private static KQueueEventLoopGroup withIoRatio(KQueueEventLoopGroup group, @Nul return group; } + @Override + public Class channelClass(NettyChannelType type) throws UnsupportedOperationException { + return switch (type) { + case SERVER_SOCKET -> KQueueServerSocketChannel.class; + case CLIENT_SOCKET -> KQueueSocketChannel.class; + case DOMAIN_SERVER_SOCKET -> KQueueServerDomainSocketChannel.class; + case DATAGRAM_SOCKET -> KQueueDatagramChannel.class; + }; + } + + @Override + public Class channelClass(NettyChannelType type, @Nullable EventLoopGroupConfiguration configuration) { + return channelClass(type); + } + + @Override + public Channel channelInstance(NettyChannelType type, @Nullable EventLoopGroupConfiguration configuration) { + return switch (type) { + case SERVER_SOCKET -> new KQueueServerSocketChannel(); + case CLIENT_SOCKET -> new KQueueSocketChannel(); + case DOMAIN_SERVER_SOCKET -> new KQueueServerDomainSocketChannel(); + case DATAGRAM_SOCKET -> new KQueueDatagramChannel(); + }; + } } diff --git a/http-netty/src/main/java/io/micronaut/http/netty/channel/NettyChannelType.java b/http-netty/src/main/java/io/micronaut/http/netty/channel/NettyChannelType.java new file mode 100644 index 00000000000..85ffe985786 --- /dev/null +++ b/http-netty/src/main/java/io/micronaut/http/netty/channel/NettyChannelType.java @@ -0,0 +1,40 @@ +/* + * Copyright 2017-2023 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.http.netty.channel; + +/** + * Different netty channel types. + * + * @since 4.0.0 + */ +public enum NettyChannelType { + /** + * @see io.netty.channel.socket.ServerSocketChannel + */ + SERVER_SOCKET, + /** + * @see io.netty.channel.socket.SocketChannel + */ + CLIENT_SOCKET, + /** + * @see io.netty.channel.unix.ServerDomainSocketChannel + */ + DOMAIN_SERVER_SOCKET, + /** + * @see io.netty.channel.socket.DatagramChannel + */ + DATAGRAM_SOCKET, +} diff --git a/http-netty/src/main/java/io/micronaut/http/netty/channel/NioEventLoopGroupFactory.java b/http-netty/src/main/java/io/micronaut/http/netty/channel/NioEventLoopGroupFactory.java index 800d736a666..53ef7a692af 100644 --- a/http-netty/src/main/java/io/micronaut/http/netty/channel/NioEventLoopGroupFactory.java +++ b/http-netty/src/main/java/io/micronaut/http/netty/channel/NioEventLoopGroupFactory.java @@ -19,10 +19,12 @@ import io.micronaut.core.annotation.Internal; import io.micronaut.core.annotation.NonNull; import io.micronaut.core.annotation.Nullable; +import io.netty.channel.Channel; import io.netty.channel.EventLoopGroup; import io.netty.channel.nio.NioEventLoopGroup; import io.netty.channel.socket.ServerSocketChannel; import io.netty.channel.socket.SocketChannel; +import io.netty.channel.socket.nio.NioDatagramChannel; import io.netty.channel.socket.nio.NioServerSocketChannel; import io.netty.channel.socket.nio.NioSocketChannel; import io.netty.channel.unix.ServerDomainSocketChannel; @@ -104,4 +106,29 @@ private static NioEventLoopGroup withIoRatio(NioEventLoopGroup group, @Nullable } return group; } + + @Override + public Class channelClass(NettyChannelType type) throws UnsupportedOperationException { + return switch (type) { + case SERVER_SOCKET -> NioServerSocketChannel.class; + case CLIENT_SOCKET -> NioSocketChannel.class; + case DOMAIN_SERVER_SOCKET -> throw new UnsupportedOperationException("NIO does not support domain sockets"); + case DATAGRAM_SOCKET -> NioDatagramChannel.class; + }; + } + + @Override + public Class channelClass(NettyChannelType type, @Nullable EventLoopGroupConfiguration configuration) { + return channelClass(type); + } + + @Override + public Channel channelInstance(NettyChannelType type, @Nullable EventLoopGroupConfiguration configuration) { + return switch (type) { + case SERVER_SOCKET -> new NioServerSocketChannel(); + case CLIENT_SOCKET -> new NioSocketChannel(); + case DOMAIN_SERVER_SOCKET -> throw new UnsupportedOperationException("NIO does not support domain sockets"); + case DATAGRAM_SOCKET -> new NioDatagramChannel(); + }; + } } diff --git a/http-netty/src/main/java/io/micronaut/http/netty/reactive/HandlerSubscriber.java b/http-netty/src/main/java/io/micronaut/http/netty/reactive/HandlerSubscriber.java index ff201bedd0f..b3085719266 100644 --- a/http-netty/src/main/java/io/micronaut/http/netty/reactive/HandlerSubscriber.java +++ b/http-netty/src/main/java/io/micronaut/http/netty/reactive/HandlerSubscriber.java @@ -26,7 +26,13 @@ import java.util.concurrent.atomic.AtomicBoolean; -import static io.micronaut.http.netty.reactive.HandlerSubscriber.State.*; +import static io.micronaut.http.netty.reactive.HandlerSubscriber.State.CANCELLED; +import static io.micronaut.http.netty.reactive.HandlerSubscriber.State.COMPLETE; +import static io.micronaut.http.netty.reactive.HandlerSubscriber.State.INACTIVE; +import static io.micronaut.http.netty.reactive.HandlerSubscriber.State.NO_CONTEXT; +import static io.micronaut.http.netty.reactive.HandlerSubscriber.State.NO_SUBSCRIPTION; +import static io.micronaut.http.netty.reactive.HandlerSubscriber.State.NO_SUBSCRIPTION_OR_CONTEXT; +import static io.micronaut.http.netty.reactive.HandlerSubscriber.State.RUNNING; /** * Subscriber that publishes received messages to the handler pipeline. diff --git a/http-netty/src/main/java/io/micronaut/http/netty/stream/HttpStreamsHandler.java b/http-netty/src/main/java/io/micronaut/http/netty/stream/HttpStreamsHandler.java index e3b56b348e6..2a6a659ec82 100644 --- a/http-netty/src/main/java/io/micronaut/http/netty/stream/HttpStreamsHandler.java +++ b/http-netty/src/main/java/io/micronaut/http/netty/stream/HttpStreamsHandler.java @@ -359,7 +359,7 @@ public void onNext(HttpContent httpContent) { //if oncomplete gets called before the message is written the promise //set to lastWriteFuture shouldn't complete until the first content is written lastWriteFuture = messageWritePromise; - ctx.writeAndFlush(message).addListener(f -> super.onNext(httpContent, messageWritePromise)); + ctx.writeAndFlush(message).addListener(f -> onNext(httpContent, messageWritePromise)); } else { super.onNext(httpContent); } diff --git a/http-server-netty/build.gradle b/http-server-netty/build.gradle index df3c79a65aa..b2dd9cf9229 100644 --- a/http-server-netty/build.gradle +++ b/http-server-netty/build.gradle @@ -30,6 +30,7 @@ dependencies { compileOnly project(":websocket") compileOnly libs.kotlin.stdlib compileOnly libs.managed.netty.transport.native.unix.common + compileOnly libs.managed.netty.incubator.codec.http3 testImplementation libs.jmh.core testAnnotationProcessor libs.jmh.generator.annprocess @@ -55,6 +56,7 @@ dependencies { testImplementation project(":inject-java-test") testImplementation project(":http-client") testImplementation libs.spotbugs + testImplementation libs.managed.netty.incubator.codec.http3 if (JavaVersion.current().isCompatibleWith(JavaVersion.VERSION_15)) { testImplementation libs.bcpkix } diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/DefaultNettyEmbeddedServerFactory.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/DefaultNettyEmbeddedServerFactory.java index 28cec74dda1..4cc4fe98e27 100644 --- a/http-server-netty/src/main/java/io/micronaut/http/server/netty/DefaultNettyEmbeddedServerFactory.java +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/DefaultNettyEmbeddedServerFactory.java @@ -15,16 +15,6 @@ */ package io.micronaut.http.server.netty; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Optional; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.ThreadFactory; - import io.micronaut.context.ApplicationContext; import io.micronaut.context.annotation.Bean; import io.micronaut.context.annotation.Factory; @@ -40,6 +30,7 @@ import io.micronaut.http.netty.channel.EventLoopGroupConfiguration; import io.micronaut.http.netty.channel.EventLoopGroupFactory; import io.micronaut.http.netty.channel.EventLoopGroupRegistry; +import io.micronaut.http.netty.channel.NettyChannelType; import io.micronaut.http.netty.channel.NettyThreadFactory; import io.micronaut.http.netty.channel.converters.ChannelOptionFactory; import io.micronaut.http.netty.channel.converters.DefaultChannelOptionFactory; @@ -56,6 +47,7 @@ import io.micronaut.http.ssl.ServerSslConfiguration; import io.micronaut.scheduling.executor.ExecutorSelector; import io.micronaut.web.router.resource.StaticResourceResolver; +import io.netty.channel.Channel; import io.netty.channel.ChannelOutboundHandler; import io.netty.channel.EventLoopGroup; import io.netty.channel.ServerChannel; @@ -65,6 +57,16 @@ import jakarta.inject.Named; import jakarta.inject.Singleton; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.ThreadFactory; + /** * Default implementation of {@link io.micronaut.http.server.netty.NettyEmbeddedServerFactory}. * @@ -295,6 +297,11 @@ public ServerChannel getDomainServerChannelInstance(EventLoopGroupConfiguration return eventLoopGroupFactory.domainServerSocketChannelInstance(workerConfig); } + @Override + public Channel getChannelInstance(NettyChannelType type, EventLoopGroupConfiguration workerConfig) { + return eventLoopGroupFactory.channelInstance(type, workerConfig); + } + @SuppressWarnings("unchecked") @Override public ApplicationEventPublisher getEventPublisher(Class eventClass) { diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/HttpPipelineBuilder.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/HttpPipelineBuilder.java index 3e1bb834ebf..e8c66825ecc 100644 --- a/http-server-netty/src/main/java/io/micronaut/http/server/netty/HttpPipelineBuilder.java +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/HttpPipelineBuilder.java @@ -19,6 +19,7 @@ import io.micronaut.core.annotation.Nullable; import io.micronaut.core.naming.Named; import io.micronaut.core.util.SupplierUtil; +import io.micronaut.http.HttpVersion; import io.micronaut.http.context.event.HttpRequestReceivedEvent; import io.micronaut.http.netty.channel.ChannelPipelineCustomizer; import io.micronaut.http.netty.stream.HttpStreamsServerHandler; @@ -27,6 +28,7 @@ import io.micronaut.http.server.netty.encoders.HttpResponseEncoder; import io.micronaut.http.server.netty.handler.accesslog.HttpAccessLogHandler; import io.micronaut.http.server.netty.ssl.HttpRequestCertificateHandler; +import io.micronaut.http.server.netty.types.files.NettySystemFileCustomizableResponseType; import io.micronaut.http.server.netty.websocket.NettyServerWebSocketUpgradeHandler; import io.micronaut.http.server.util.HttpHostResolver; import io.micronaut.http.ssl.ServerSslConfiguration; @@ -63,6 +65,11 @@ import io.netty.handler.ssl.SslHandshakeCompletionEvent; import io.netty.handler.stream.ChunkedWriteHandler; import io.netty.handler.timeout.IdleStateHandler; +import io.netty.incubator.codec.http3.Http3; +import io.netty.incubator.codec.http3.Http3FrameToHttpObjectCodec; +import io.netty.incubator.codec.http3.Http3ServerConnectionHandler; +import io.netty.incubator.codec.quic.QuicSslContext; +import io.netty.incubator.codec.quic.QuicStreamChannel; import io.netty.util.AsciiString; import io.netty.util.AttributeKey; import io.netty.util.ReferenceCountUtil; @@ -103,23 +110,28 @@ final class HttpPipelineBuilder { private final LoggingHandler loggingHandler; private final SslContext sslContext; + private final QuicSslContext quicSslContext; private final HttpAccessLogHandler accessLogHandler; private final HttpRequestDecoder requestDecoder; private final HttpResponseEncoder responseEncoder; private final NettyServerCustomizer serverCustomizer; - HttpPipelineBuilder(NettyHttpServer server, NettyEmbeddedServices embeddedServices, ServerSslConfiguration sslConfiguration, RoutingInBoundHandler routingInBoundHandler, HttpHostResolver hostResolver, NettyServerCustomizer serverCustomizer) { + private final boolean quic; + + HttpPipelineBuilder(NettyHttpServer server, NettyEmbeddedServices embeddedServices, ServerSslConfiguration sslConfiguration, RoutingInBoundHandler routingInBoundHandler, HttpHostResolver hostResolver, NettyServerCustomizer serverCustomizer, boolean quic) { this.server = server; this.embeddedServices = embeddedServices; this.sslConfiguration = sslConfiguration; this.routingInBoundHandler = routingInBoundHandler; this.hostResolver = hostResolver; this.serverCustomizer = serverCustomizer; + this.quic = quic; Optional logLevel = server.getServerConfiguration().getLogLevel(); loggingHandler = logLevel.map(level -> new LoggingHandler(NettyHttpServer.class, level)).orElse(null); - sslContext = embeddedServices.getServerSslBuilder() != null ? embeddedServices.getServerSslBuilder().build().orElse(null) : null; + sslContext = embeddedServices.getServerSslBuilder() != null && !quic ? embeddedServices.getServerSslBuilder().build().orElse(null) : null; + quicSslContext = quic ? embeddedServices.getServerSslBuilder().buildQuic().orElse(null) : null; NettyHttpServerConfiguration.AccessLogger accessLogger = server.getServerConfiguration().getAccessLogger(); if (accessLogger != null && accessLogger.isEnabled()) { @@ -129,9 +141,9 @@ final class HttpPipelineBuilder { } requestDecoder = new HttpRequestDecoder(server, - server.getEnvironment(), - server.getServerConfiguration(), - embeddedServices.getEventPublisher(HttpRequestReceivedEvent.class)); + server.getEnvironment(), + server.getServerConfiguration(), + embeddedServices.getEventPublisher(HttpRequestReceivedEvent.class)); responseEncoder = new HttpResponseEncoder( embeddedServices.getMediaTypeCodecRegistry(), server.getServerConfiguration(), @@ -148,17 +160,24 @@ final class ConnectionPipeline { @Nullable private final SslHandler sslHandler; + private final boolean https; private final NettyServerCustomizer connectionCustomizer; - ConnectionPipeline(Channel channel, boolean ssl) { + /** + * @param channel The channel of this connection + * @param tls Whether to add a TLS handler + * @param https Whether this connection is HTTPS (usually same as {@code tls}, except for HTTP/3) + */ + ConnectionPipeline(Channel channel, boolean tls, boolean https) { this.channel = channel; this.pipeline = channel.pipeline(); - this.sslHandler = ssl ? sslContext.newHandler(channel.alloc()) : null; + this.sslHandler = tls ? sslContext.newHandler(channel.alloc()) : null; + this.https = https; this.connectionCustomizer = serverCustomizer.specializeForChannel(channel, NettyServerCustomizer.ChannelRole.CONNECTION); } - void insertPcapLoggingHandler(String qualifier) { + void insertPcapLoggingHandler(Channel ch, String qualifier) { String pattern = server.getServerConfiguration().getPcapLoggingPathPattern(); if (pattern == null) { return; @@ -166,8 +185,16 @@ void insertPcapLoggingHandler(String qualifier) { String path = pattern; path = path.replace("{qualifier}", qualifier); - path = path.replace("{localAddress}", resolveIfNecessary(pipeline.channel().localAddress())); - path = path.replace("{remoteAddress}", resolveIfNecessary(pipeline.channel().remoteAddress())); + if (ch.localAddress() != null) { + path = path.replace("{localAddress}", resolveIfNecessary(ch.localAddress())); + } + if (ch.remoteAddress() != null) { + path = path.replace("{remoteAddress}", resolveIfNecessary(ch.remoteAddress())); + } + if (quic && ch instanceof QuicStreamChannel qsc) { + path = path.replace("{localAddress}", resolveIfNecessary(qsc.parent().localAddress())); + path = path.replace("{remoteAddress}", resolveIfNecessary(qsc.parent().remoteAddress())); + } path = path.replace("{random}", Long.toHexString(ThreadLocalRandom.current().nextLong())); path = path.replace("{timestamp}", Instant.now().toString()); @@ -176,7 +203,13 @@ void insertPcapLoggingHandler(String qualifier) { LOG.warn("Logging *full* request data, as configured. This will contain sensitive information! Path: '{}'", path); try { - pipeline.addLast(new PcapWriteHandler(new FileOutputStream(path))); + PcapWriteHandler.Builder builder = PcapWriteHandler.builder(); + + if (quic && ch instanceof QuicStreamChannel qsc) { + builder.forceTcpChannel((InetSocketAddress) qsc.parent().localAddress(), (InetSocketAddress) qsc.parent().remoteAddress(), true); + } + + ch.pipeline().addLast(builder.build(new FileOutputStream(path))); } catch (FileNotFoundException e) { LOG.warn("Failed to create target pcap at '{}', not logging.", path, e); } @@ -219,17 +252,44 @@ void initChannel() { } } + void initHttp3Channel() { + insertPcapLoggingHandler(channel, "udp-encapsulated"); + + pipeline.addLast(Http3.newQuicServerCodecBuilder() + .sslContext(quicSslContext) + .initialMaxData(server.getServerConfiguration().getHttp3().getInitialMaxData()) + .initialMaxStreamDataBidirectionalLocal(server.getServerConfiguration().getHttp3().getInitialMaxStreamDataBidirectionalLocal()) + .initialMaxStreamDataBidirectionalRemote(server.getServerConfiguration().getHttp3().getInitialMaxStreamDataBidirectionalRemote()) + .initialMaxStreamsBidirectional(server.getServerConfiguration().getHttp3().getInitialMaxStreamsBidirectional()) + .tokenHandler(QuicTokenHandlerImpl.create(channel.alloc())) + .handler(new ChannelInitializer() { + @Override + protected void initChannel(@NonNull Channel ch) throws Exception { + insertPcapLoggingHandler(ch, "quic-decapsulated"); + ch.pipeline().addLast(new Http3ServerConnectionHandler(new ChannelInitializer() { + @Override + protected void initChannel(@NonNull QuicStreamChannel ch) throws Exception { + StreamPipeline streamPipeline = new StreamPipeline(ch, sslHandler, https, connectionCustomizer.specializeForChannel(ch, NettyServerCustomizer.ChannelRole.REQUEST_STREAM)); + streamPipeline.insertHttp3FrameHandlers(); + streamPipeline.streamCustomizer.onStreamPipelineBuilt(); + } + })); + } + }) + .build()); + } + /** * Insert handlers that wrap the outermost TCP stream. This is SSL and potentially packet capture. */ void insertOuterTcpHandlers() { - insertPcapLoggingHandler("encapsulated"); + insertPcapLoggingHandler(channel, "encapsulated"); if (sslHandler != null) { sslHandler.setHandshakeTimeoutMillis(sslConfiguration.getHandshakeTimeout().toMillis()); pipeline.addLast(ChannelPipelineCustomizer.HANDLER_SSL, sslHandler); - insertPcapLoggingHandler("ssl-decapsulated"); + insertPcapLoggingHandler(channel, "ssl-decapsulated"); } if (loggingHandler != null) { @@ -263,7 +323,7 @@ void configureForHttp1() { pipeline.addLast(ChannelPipelineCustomizer.HANDLER_HTTP_SERVER_CODEC, createServerCodec()); - new StreamPipeline(channel, sslHandler, connectionCustomizer).insertHttp1DownstreamHandlers(); + new StreamPipeline(channel, sslHandler, https, connectionCustomizer).insertHttp1DownstreamHandlers(); connectionCustomizer.onInitialPipelineBuilt(); connectionCustomizer.onStreamPipelineBuilt(); @@ -280,7 +340,7 @@ private void configureForHttp2() { pipeline.addLast(new Http2MultiplexHandler(new ChannelInitializer() { @Override protected void initChannel(@NonNull Channel ch) { - StreamPipeline streamPipeline = new StreamPipeline(ch, sslHandler, connectionCustomizer.specializeForChannel(ch, NettyServerCustomizer.ChannelRole.REQUEST_STREAM)); + StreamPipeline streamPipeline = new StreamPipeline(ch, sslHandler, https, connectionCustomizer.specializeForChannel(ch, NettyServerCustomizer.ChannelRole.REQUEST_STREAM)); streamPipeline.insertHttp2FrameHandlers(); streamPipeline.streamCustomizer.onStreamPipelineBuilt(); } @@ -363,7 +423,7 @@ void configureForH2cSupport() { return new Http2ServerUpgradeCodec(connectionHandler, new Http2MultiplexHandler(new ChannelInitializer() { @Override protected void initChannel(@NonNull Http2StreamChannel ch) { - StreamPipeline streamPipeline = new StreamPipeline(ch, sslHandler, connectionCustomizer.specializeForChannel(ch, NettyServerCustomizer.ChannelRole.REQUEST_STREAM)); + StreamPipeline streamPipeline = new StreamPipeline(ch, sslHandler, https, connectionCustomizer.specializeForChannel(ch, NettyServerCustomizer.ChannelRole.REQUEST_STREAM)); streamPipeline.insertHttp2FrameHandlers(); streamPipeline.streamCustomizer.onStreamPipelineBuilt(); } @@ -402,7 +462,7 @@ protected void channelRead0(ChannelHandlerContext ctx, HttpMessage msg) { // reconfigure for http1 // note: we have to reuse the serverCodec in case it still has some data buffered - new StreamPipeline(channel, sslHandler, connectionCustomizer).insertHttp1DownstreamHandlers(); + new StreamPipeline(channel, sslHandler, https, connectionCustomizer).insertHttp1DownstreamHandlers(); connectionCustomizer.onStreamPipelineBuilt(); onRequestPipelineBuilt(); cp.fireChannelRead(ReferenceCountUtil.retain(msg)); @@ -424,22 +484,25 @@ private HttpServerCodec createServerCodec() { } final class StreamPipeline { + HttpVersion httpVersion = HttpVersion.HTTP_1_1; private final Channel channel; private final ChannelPipeline pipeline; @Nullable private final SslHandler sslHandler; + private final boolean https; private final NettyServerCustomizer streamCustomizer; - private StreamPipeline(Channel channel, @Nullable SslHandler sslHandler, NettyServerCustomizer streamCustomizer) { + private StreamPipeline(Channel channel, @Nullable SslHandler sslHandler, boolean https, NettyServerCustomizer streamCustomizer) { this.channel = channel; this.pipeline = channel.pipeline(); this.sslHandler = sslHandler; + this.https = https; this.streamCustomizer = streamCustomizer; } void initializeChildPipelineForPushPromise(Channel childChannel) { - StreamPipeline promisePipeline = new StreamPipeline(childChannel, sslHandler, streamCustomizer.specializeForChannel(childChannel, NettyServerCustomizer.ChannelRole.PUSH_PROMISE_STREAM)); + StreamPipeline promisePipeline = new StreamPipeline(childChannel, sslHandler, https, streamCustomizer.specializeForChannel(childChannel, NettyServerCustomizer.ChannelRole.PUSH_PROMISE_STREAM)); promisePipeline.insertHttp2FrameHandlers(); promisePipeline.streamCustomizer.onStreamPipelineBuilt(); } @@ -450,11 +513,19 @@ private void insertHttp2FrameHandlers() { insertHttp2DownstreamHandlers(); } + private void insertHttp3FrameHandlers() { + pipeline.addLast(ChannelPipelineCustomizer.HANDLER_HTTP_DECODER, new Http3FrameToHttpObjectCodec(true, server.getServerConfiguration().isValidateHeaders())); + + insertHttp2DownstreamHandlers(); + } + /** * Insert the handlers downstream of the {@value ChannelPipelineCustomizer#HANDLER_HTTP2_CONNECTION}. Used both * for ALPN HTTP 2 and h2c. */ private void insertHttp2DownstreamHandlers() { + httpVersion = HttpVersion.HTTP_2_0; + pipeline.addLast(ChannelPipelineCustomizer.HANDLER_FLOW_CONTROL, new FlowControlHandler()); if (accessLogHandler != null) { pipeline.addLast(ChannelPipelineCustomizer.HANDLER_ACCESS_LOGGER, accessLogHandler); @@ -462,24 +533,28 @@ private void insertHttp2DownstreamHandlers() { registerMicronautChannelHandlers(); - insertMicronautHandlers(); + insertMicronautHandlers(false); } /** * Insert the handlers that manage the micronaut message handling, e.g. conversion between micronaut requests * and netty requests, and routing. */ - private void insertMicronautHandlers() { + private void insertMicronautHandlers(boolean zeroCopySupported) { channel.attr(STREAM_PIPELINE_ATTRIBUTE.get()).set(this); - pipeline.addLast(ChannelPipelineCustomizer.HANDLER_HTTP_COMPRESSOR, new SmartHttpContentCompressor(embeddedServices.getHttpCompressionStrategy())); + SmartHttpContentCompressor contentCompressor = new SmartHttpContentCompressor(embeddedServices.getHttpCompressionStrategy()); + if (zeroCopySupported) { + channel.attr(NettySystemFileCustomizableResponseType.ZERO_COPY_PREDICATE.get()).set(contentCompressor); + } + pipeline.addLast(ChannelPipelineCustomizer.HANDLER_HTTP_COMPRESSOR, contentCompressor); pipeline.addLast(ChannelPipelineCustomizer.HANDLER_HTTP_DECOMPRESSOR, new HttpContentDecompressor()); pipeline.addLast(NettyServerWebSocketUpgradeHandler.COMPRESSION_HANDLER, new WebSocketServerCompressionHandler()); pipeline.addLast(ChannelPipelineCustomizer.HANDLER_HTTP_STREAM, new HttpStreamsServerHandler()); pipeline.addLast(ChannelPipelineCustomizer.HANDLER_HTTP_CHUNK, new ChunkedWriteHandler()); pipeline.addLast(HttpRequestDecoder.ID, requestDecoder); - if (server.getServerConfiguration().isDualProtocol() && server.getServerConfiguration().isHttpToHttpsRedirect() && sslHandler == null) { + if (server.getServerConfiguration().isDualProtocol() && server.getServerConfiguration().isHttpToHttpsRedirect() && !https) { pipeline.addLast(ChannelPipelineCustomizer.HANDLER_HTTP_TO_HTTPS_REDIRECT, new HttpToHttpsRedirectHandler(sslConfiguration, hostResolver)); } if (sslHandler != null) { @@ -499,6 +574,7 @@ private void insertMicronautHandlers() { * after a H2C negotiation failure. */ private void insertHttp1DownstreamHandlers() { + httpVersion = HttpVersion.HTTP_1_1; if (accessLogHandler != null) { pipeline.addLast(ChannelPipelineCustomizer.HANDLER_ACCESS_LOGGER, accessLogHandler); } @@ -506,7 +582,7 @@ private void insertHttp1DownstreamHandlers() { pipeline.addLast(ChannelPipelineCustomizer.HANDLER_FLOW_CONTROL, new FlowControlHandler()); pipeline.addLast(ChannelPipelineCustomizer.HANDLER_HTTP_KEEP_ALIVE, new HttpServerKeepAliveHandler()); - insertMicronautHandlers(); + insertMicronautHandlers(sslHandler == null && !https); } /** diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/NettyEmbeddedServices.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/NettyEmbeddedServices.java index 7657b6afb89..9d1d73036aa 100644 --- a/http-server-netty/src/main/java/io/micronaut/http/server/netty/NettyEmbeddedServices.java +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/NettyEmbeddedServices.java @@ -15,10 +15,6 @@ */ package io.micronaut.http.server.netty; -import java.util.List; -import java.util.Optional; -import java.util.concurrent.ExecutorService; - import io.micronaut.context.ApplicationContext; import io.micronaut.context.event.ApplicationEventPublisher; import io.micronaut.core.annotation.Internal; @@ -27,6 +23,7 @@ import io.micronaut.http.codec.MediaTypeCodecRegistry; import io.micronaut.http.netty.channel.EventLoopGroupConfiguration; import io.micronaut.http.netty.channel.EventLoopGroupRegistry; +import io.micronaut.http.netty.channel.NettyChannelType; import io.micronaut.http.netty.channel.converters.ChannelOptionFactory; import io.micronaut.http.server.RouteExecutor; import io.micronaut.http.server.binding.RequestArgumentSatisfier; @@ -34,12 +31,17 @@ import io.micronaut.scheduling.executor.ExecutorSelector; import io.micronaut.web.router.Router; import io.micronaut.web.router.resource.StaticResourceResolver; +import io.netty.channel.Channel; import io.netty.channel.ChannelOutboundHandler; import io.netty.channel.EventLoopGroup; import io.netty.channel.ServerChannel; import io.netty.channel.SimpleChannelInboundHandler; import io.netty.channel.socket.ServerSocketChannel; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.ExecutorService; + /** * Internal interface with services required by the {@link io.micronaut.http.server.netty.NettyHttpServer}. *e @@ -170,6 +172,21 @@ default ExecutorSelector getExecutorSelector() { throw new UnsupportedOperationException("Domain sockets not supported"); } + /** + * Gets the domain server socket channel instance. + * @param type The channel type to return + * @param workerConfig The worker config + * @return The channel + * @throws UnsupportedOperationException if domain sockets are not supported. + */ + @NonNull default Channel getChannelInstance(NettyChannelType type, @NonNull EventLoopGroupConfiguration workerConfig) { + return switch (type) { + case SERVER_SOCKET -> getServerSocketChannelInstance(workerConfig); + case DOMAIN_SERVER_SOCKET -> getDomainServerChannelInstance(workerConfig); + default -> throw new UnsupportedOperationException("Unsupported netty channel type"); + }; + } + /** * Get an event publisher for the server for the given type. * @param eventClass The event publisher diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/NettyHttpRequest.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/NettyHttpRequest.java index 9cd44eee6bc..119a831120b 100644 --- a/http-server-netty/src/main/java/io/micronaut/http/server/netty/NettyHttpRequest.java +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/NettyHttpRequest.java @@ -28,6 +28,7 @@ import io.micronaut.http.HttpHeaders; import io.micronaut.http.HttpMethod; import io.micronaut.http.HttpRequest; +import io.micronaut.http.HttpVersion; import io.micronaut.http.MediaType; import io.micronaut.http.MutableHttpHeaders; import io.micronaut.http.MutableHttpParameters; @@ -206,6 +207,15 @@ public Optional getAttribute(CharSequence name) { return Optional.ofNullable(getAttributes().getValue(Objects.requireNonNull(name, "Name cannot be null").toString())); } + @Override + public HttpVersion getHttpVersion() { + HttpPipelineBuilder.StreamPipeline pipeline = channelHandlerContext.channel().attr(HttpPipelineBuilder.STREAM_PIPELINE_ATTRIBUTE.get()).get(); + if (pipeline != null) { + return pipeline.httpVersion; + } + return HttpVersion.HTTP_1_1; + } + @Override public String toString() { return getMethodName() + " " + getUri(); diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/NettyHttpServer.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/NettyHttpServer.java index 57723f57d07..09940709caa 100644 --- a/http-server-netty/src/main/java/io/micronaut/http/server/netty/NettyHttpServer.java +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/NettyHttpServer.java @@ -32,6 +32,7 @@ import io.micronaut.http.netty.channel.ChannelPipelineListener; import io.micronaut.http.netty.channel.DefaultEventLoopGroupConfiguration; import io.micronaut.http.netty.channel.EventLoopGroupConfiguration; +import io.micronaut.http.netty.channel.NettyChannelType; import io.micronaut.http.netty.channel.converters.ChannelOptionFactory; import io.micronaut.http.netty.websocket.WebSocketSessionRepository; import io.micronaut.http.server.HttpServerConfiguration; @@ -49,6 +50,7 @@ import io.micronaut.runtime.server.event.ServerStartupEvent; import io.micronaut.scheduling.TaskExecutors; import io.micronaut.web.router.Router; +import io.netty.bootstrap.Bootstrap; import io.netty.bootstrap.ServerBootstrap; import io.netty.channel.Channel; import io.netty.channel.ChannelFuture; @@ -273,6 +275,7 @@ public synchronized NettyEmbeddedServer start() { workerGroup = createWorkerEventLoopGroup(workerConfig); parentGroup = createParentEventLoopGroup(); ServerBootstrap serverBootstrap = createServerBootstrap(); + Bootstrap udpBootstrap = null; // create lazily processOptions(serverConfiguration.getOptions(), serverBootstrap::option); processOptions(serverConfiguration.getChildOptions(), serverBootstrap::childOption); @@ -280,7 +283,12 @@ public synchronized NettyEmbeddedServer start() { List listeners = new ArrayList<>(); for (NettyHttpServerConfiguration.NettyListenerConfiguration listenerConfiguration : listenerConfigurations) { - Listener listener = bind(serverBootstrap, listenerConfiguration, workerConfig); + if (listenerConfiguration.getFamily() == NettyHttpServerConfiguration.NettyListenerConfiguration.Family.QUIC && udpBootstrap == null) { + udpBootstrap = new Bootstrap(); + processOptions(serverConfiguration.getOptions(), udpBootstrap::option); + udpBootstrap.group(workerGroup); + } + Listener listener = bind(serverBootstrap, udpBootstrap, listenerConfiguration, workerConfig); listeners.add(listener); } this.activeListeners = Collections.unmodifiableList(listeners); @@ -348,6 +356,7 @@ public void register(@NonNull NettyServerCustomizer customizer) { } @Override + @SuppressWarnings("InnerAssignmentCheck") public int getPort() { List listenersLocal = this.activeListeners; @@ -358,19 +367,18 @@ public int getPort() { // not started, try to infer from config for (NettyHttpServerConfiguration.NettyListenerConfiguration listenerCfg : listenerConfigurations) { switch (listenerCfg.getFamily()) { - case TCP: + case TCP, QUIC -> { if (listenerCfg.getPort() == -1) { hasRandom = true; } else { // found one \o/ return listenerCfg.getPort(); } - break; - case UNIX: - hasUnix = true; - break; - default: + } + case UNIX -> hasUnix = true; + default -> { // unknown + } } } } else { @@ -489,41 +497,58 @@ protected ServerBootstrap createServerBootstrap() { return new ServerBootstrap(); } - private Listener bind(ServerBootstrap bootstrap, NettyHttpServerConfiguration.NettyListenerConfiguration cfg, EventLoopGroupConfiguration workerConfig) { + private Listener bind(ServerBootstrap bootstrap, Bootstrap udpBootstrap, NettyHttpServerConfiguration.NettyListenerConfiguration cfg, EventLoopGroupConfiguration workerConfig) { logBind(cfg); try { - Listener listener = new Listener(cfg); - ServerBootstrap listenerBootstrap = bootstrap.clone() - // this initializer runs before the actual bind operation, so we can be sure - // setServerChannel has been called by the time bind runs. - .handler(new ChannelInitializer() { - @Override - protected void initChannel(@NonNull Channel ch) { - listener.setServerChannel(ch); - } - }) - .childHandler(listener); ChannelFuture future; - switch (cfg.getFamily()) { - case TCP: - listenerBootstrap.channelFactory(() -> nettyEmbeddedServices.getServerSocketChannelInstance(workerConfig)); - int port = cfg.getPort(); - if (port == -1) { - port = 0; - } - if (cfg.getHost() == null) { - future = listenerBootstrap.bind(port); - } else { - future = listenerBootstrap.bind(cfg.getHost(), port); - } - break; - case UNIX: - listenerBootstrap.channelFactory(() -> nettyEmbeddedServices.getDomainServerChannelInstance(workerConfig)); - future = listenerBootstrap.bind(DomainSocketHolder.makeDomainSocketAddress(cfg.getPath())); - break; - default: - throw new UnsupportedOperationException("Unsupported family: " + cfg.getFamily()); + Listener listener; + if (cfg.getFamily() == NettyHttpServerConfiguration.NettyListenerConfiguration.Family.QUIC) { + listener = new UdpListener(cfg); + Bootstrap listenerBootstrap = udpBootstrap.clone() + .handler(listener) + .channelFactory(() -> nettyEmbeddedServices.getChannelInstance(NettyChannelType.DATAGRAM_SOCKET, workerConfig)); + int port = cfg.getPort(); + if (port == -1) { + port = 0; + } + if (cfg.getHost() == null) { + future = listenerBootstrap.bind(port); + } else { + future = listenerBootstrap.bind(cfg.getHost(), port); + } + } else { + listener = new Listener(cfg); + ServerBootstrap listenerBootstrap = bootstrap.clone() + // this initializer runs before the actual bind operation, so we can be sure + // setServerChannel has been called by the time bind runs. + .handler(new ChannelInitializer() { + @Override + protected void initChannel(@NonNull Channel ch) { + listener.setServerChannel(ch); + } + }) + .childHandler(listener); + switch (cfg.getFamily()) { + case TCP: + listenerBootstrap.channelFactory(() -> nettyEmbeddedServices.getServerSocketChannelInstance(workerConfig)); + int port = cfg.getPort(); + if (port == -1) { + port = 0; + } + if (cfg.getHost() == null) { + future = listenerBootstrap.bind(port); + } else { + future = listenerBootstrap.bind(cfg.getHost(), port); + } + break; + case UNIX: + listenerBootstrap.channelFactory(() -> nettyEmbeddedServices.getDomainServerChannelInstance(workerConfig)); + future = listenerBootstrap.bind(DomainSocketHolder.makeDomainSocketAddress(cfg.getPath())); + break; + default: + throw new UnsupportedOperationException("Unsupported family: " + cfg.getFamily()); + } } future.syncUninterruptibly(); return listener; @@ -558,22 +583,10 @@ private void logBind(NettyHttpServerConfiguration.NettyListenerConfiguration cfg } private static String displayAddress(NettyHttpServerConfiguration.NettyListenerConfiguration cfg) { - switch (cfg.getFamily()) { - case TCP: - if (cfg.getHost() == null) { - return "*:" + cfg.getPort(); - } else { - return cfg.getHost() + ":" + cfg.getPort(); - } - case UNIX: - if (cfg.getPath().startsWith("\0")) { - return "unix:@" + cfg.getPath().substring(1); - } else { - return "unix:" + cfg.getPath(); - } - default: - throw new UnsupportedOperationException("Unsupported family: " + cfg.getFamily()); - } + return switch (cfg.getFamily()) { + case TCP, QUIC -> cfg.getHost() == null ? "*:" + cfg.getPort() : cfg.getHost() + ":" + cfg.getPort(); + case UNIX -> cfg.getPath().startsWith("\0") ? "unix:@" + cfg.getPath().substring(1) : "unix:" + cfg.getPath(); + }; } private void fireStartupEvents() { @@ -707,9 +720,9 @@ final void triggerPipelineListeners(ChannelPipeline pipeline) { } } - private HttpPipelineBuilder createPipelineBuilder(NettyServerCustomizer customizer) { + private HttpPipelineBuilder createPipelineBuilder(NettyServerCustomizer customizer, boolean quic) { Objects.requireNonNull(customizer, "customizer"); - return new HttpPipelineBuilder(NettyHttpServer.this, nettyEmbeddedServices, sslConfiguration, routingHandler, hostResolver, customizer); + return new HttpPipelineBuilder(NettyHttpServer.this, nettyEmbeddedServices, sslConfiguration, routingHandler, hostResolver, customizer, quic); } /** @@ -733,7 +746,7 @@ public EmbeddedChannel buildEmbeddedChannel(boolean ssl) { */ @Internal public void buildEmbeddedChannel(EmbeddedChannel prototype, boolean ssl) { - createPipelineBuilder(rootCustomizer).new ConnectionPipeline(prototype, ssl).initChannel(); + createPipelineBuilder(rootCustomizer, false).new ConnectionPipeline(prototype, ssl, ssl).initChannel(); } static Predicate inclusionPredicate(NettyHttpServerConfiguration.AccessLogger config) { @@ -752,14 +765,14 @@ private class Listener extends ChannelInitializer { NettyServerCustomizer listenerCustomizer; NettyHttpServerConfiguration.NettyListenerConfiguration config; - private volatile HttpPipelineBuilder httpPipelineBuilder; + volatile HttpPipelineBuilder httpPipelineBuilder; Listener(NettyHttpServerConfiguration.NettyListenerConfiguration config) { this.config = config; } void refresh() { - httpPipelineBuilder = createPipelineBuilder(listenerCustomizer); + httpPipelineBuilder = createPipelineBuilder(listenerCustomizer, config.getFamily() == NettyHttpServerConfiguration.NettyListenerConfiguration.Family.QUIC); if (config.isSsl() && !httpPipelineBuilder.supportsSsl()) { throw new IllegalStateException("Listener configured for SSL, but no SSL context available"); } @@ -773,7 +786,20 @@ void setServerChannel(Channel serverChannel) { @Override protected void initChannel(@NonNull Channel ch) throws Exception { - httpPipelineBuilder.new ConnectionPipeline(ch, config.isSsl()).initChannel(); + httpPipelineBuilder.new ConnectionPipeline(ch, config.isSsl(), config.isSsl()).initChannel(); + } + } + + private class UdpListener extends Listener { + UdpListener(NettyHttpServerConfiguration.NettyListenerConfiguration config) { + super(config); + } + + @Override + protected void initChannel(Channel ch) throws Exception { + // udp does not have connection channels + setServerChannel(ch); + httpPipelineBuilder.new ConnectionPipeline(ch, false, true).initHttp3Channel(); } } diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/QuicTokenHandlerImpl.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/QuicTokenHandlerImpl.java new file mode 100644 index 00000000000..349f6794f9f --- /dev/null +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/QuicTokenHandlerImpl.java @@ -0,0 +1,164 @@ +/* + * Copyright 2017-2023 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.http.server.netty; + +import io.micronaut.core.annotation.Internal; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.incubator.codec.quic.QuicTokenHandler; +import io.netty.util.concurrent.FastThreadLocal; + +import javax.crypto.KeyGenerator; +import javax.crypto.Mac; +import java.net.InetSocketAddress; +import java.security.Key; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +/** + * Secure {@link QuicTokenHandler} implementation based on a MAC. + * + * @since 4.0.0 + * @author Jonas Konrad + */ +@Internal +class QuicTokenHandlerImpl implements QuicTokenHandler { + private static final int MAC_LENGTH = 256 / 8; + private static final int MAX_CONNECTION_ID_LENGTH = 20; + /** + * Timestamp will be modulo'd by this window size, and included in the MAC. In verification, we + * check the last two windows, so a given token expires in roughly this time frame. + */ + private static final long TIMESTAMP_WINDOW_SIZE = 5 * 60 * 1000; + + private final Key key; + /** + * Making this non-static is not ideal, but it allows initializing the mac with the key only + * once. + */ + private final FastThreadLocal macCache = new FastThreadLocal<>() { + @Override + protected Mac initialValue() throws Exception { + Mac mac = Mac.getInstance("HmacSHA256"); + mac.init(key); + return mac; + } + }; + + private final ByteBufAllocator alloc; + + QuicTokenHandlerImpl(ByteBufAllocator alloc) { + this.alloc = alloc; + try { + key = KeyGenerator.getInstance("HmacSHA256").generateKey(); + } catch (NoSuchAlgorithmException e) { + throw new AssertionError(e); + } + } + + /** + * Alias for {@link #QuicTokenHandlerImpl}, avoids a {@link NoClassDefFoundError} when quic is + * missing from the classpath. + */ + static QuicTokenHandler create(ByteBufAllocator alloc) { + return new QuicTokenHandlerImpl(alloc); + } + + /** + * Write the validation token. The output buffer contains first the token, then the + * {@code dcid}, the same format that is passed into {@link #validateToken}. + * + * @param out {@link ByteBuf} into which the token will be written. + * @param dcid the destination connection id. + * @param address the {@link InetSocketAddress} of the sender. + * @return {@code true} + */ + @Override + public boolean writeToken(ByteBuf out, ByteBuf dcid, InetSocketAddress address) { + byte[] hash = hash(address, dcid, currentWindowId()); + out.writeBytes(hash); + out.writeBytes(dcid, dcid.readerIndex(), dcid.readableBytes()); + return true; + } + + /** + * Verify the validation token. The input buffer is user-controlled, but should contain the + * same format returned by {@link #writeToken} (first token, then dcid). This method extracts + * the dcid from the input, computes the MAC, and validates it against the token. + * + * @param token the {@link ByteBuf} that contains the token. The ownership is not transferred. + * @param address the {@link InetSocketAddress} of the sender. + * @return The offset of the dcid in the input buffer. This is used by netty. -1 on validation + * failure. + */ + @Override + public int validateToken(ByteBuf token, InetSocketAddress address) { + byte[] actual = new byte[MAC_LENGTH]; + token.getBytes(token.readerIndex(), actual); + ByteBuf dcid = token.slice(token.readerIndex() + MAC_LENGTH, token.readableBytes() - MAC_LENGTH); + + long windowId = currentWindowId(); + byte[] expectedHashNow = hash(address, dcid, windowId); + byte[] expectedHashPrev = hash(address, dcid, windowId - 1); + // constant-time comparison + boolean equalNow = MessageDigest.isEqual(expectedHashNow, actual); + boolean equalPrev = MessageDigest.isEqual(expectedHashPrev, actual); + if (equalNow | equalPrev) { + return MAC_LENGTH; + } else { + return -1; + } + } + + private byte[] hash(InetSocketAddress address, ByteBuf dcid, long windowId) { + ByteBuf textToVerify = buildTextToVerify(address, dcid, windowId); + byte[] hash; + try { + Mac mac = macCache.get(); + mac.update(textToVerify.array(), textToVerify.arrayOffset() + textToVerify.readerIndex(), textToVerify.readableBytes()); + hash = mac.doFinal(); + } finally { + textToVerify.release(); + } + assert hash.length == MAC_LENGTH; + return hash; + } + + private ByteBuf buildTextToVerify(InetSocketAddress address, ByteBuf dcid, long windowId) { + if (dcid.readableBytes() > MAX_CONNECTION_ID_LENGTH) { + throw new IllegalArgumentException("Connection ID may not exceed 20 bytes length"); + } + ByteBuf textToVerify = alloc.heapBuffer(); + byte[] addressBytes = address.getAddress().getAddress(); + textToVerify.writeByte(addressBytes.length); + textToVerify.writeBytes(addressBytes); + textToVerify.writeShort(address.getPort()); + textToVerify.writeByte(dcid.readableBytes()); + textToVerify.writeBytes(dcid, dcid.readerIndex(), dcid.readableBytes()); + textToVerify.writeLong(windowId); + return textToVerify; + } + + @Override + public int maxTokenLength() { + return MAC_LENGTH + MAX_CONNECTION_ID_LENGTH; + } + + // overridden in test + long currentWindowId() { + return System.currentTimeMillis() / TIMESTAMP_WINDOW_SIZE; + } +} diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/configuration/NettyHttpServerConfiguration.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/configuration/NettyHttpServerConfiguration.java index fd726a08c94..152f8549ba7 100644 --- a/http-server-netty/src/main/java/io/micronaut/http/server/netty/configuration/NettyHttpServerConfiguration.java +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/configuration/NettyHttpServerConfiguration.java @@ -20,6 +20,7 @@ import io.micronaut.context.annotation.Property; import io.micronaut.context.annotation.Replaces; import io.micronaut.context.annotation.Requires; +import io.micronaut.core.annotation.Experimental; import io.micronaut.core.annotation.Internal; import io.micronaut.core.annotation.NonNull; import io.micronaut.core.annotation.Nullable; @@ -129,6 +130,38 @@ public class NettyHttpServerConfiguration extends HttpServerConfiguration { */ @SuppressWarnings("WeakerAccess") public static final int DEFAULT_JSON_BUFFER_MAX_COMPONENTS = 4096; + /** + * Default value for {@link Http3Settings#getInitialMaxData()}. + * + * @since 4.0.0 + */ + @Experimental + @SuppressWarnings("WeakerAccess") + public static final int DEFAULT_HTTP3_INITIAL_MAX_DATA = 10000000; + /** + * Default value for {@link Http3Settings#getInitialMaxStreamDataBidirectionalLocal()}. + * + * @since 4.0.0 + */ + @Experimental + @SuppressWarnings("WeakerAccess") + public static final int DEFAULT_HTTP3_INITIAL_MAX_STREAM_DATA_BIDIRECTIONAL_LOCAL = 1000000; + /** + * Default value for {@link Http3Settings#getInitialMaxStreamDataBidirectionalRemote()}. + * + * @since 4.0.0 + */ + @Experimental + @SuppressWarnings("WeakerAccess") + public static final int DEFAULT_HTTP3_INITIAL_MAX_STREAM_DATA_BIDIRECTIONAL_REMOTE = 1000000; + /** + * Default value for {@link Http3Settings#getInitialMaxStreamsBidirectional()}. + * + * @since 4.0.0 + */ + @Experimental + @SuppressWarnings("WeakerAccess") + public static final int DEFAULT_HTTP3_INITIAL_MAX_STREAMS_BIDIRECTIONAL = 100; private static final Logger LOG = LoggerFactory.getLogger(NettyHttpServerConfiguration.class); @@ -153,6 +186,7 @@ public class NettyHttpServerConfiguration extends HttpServerConfiguration { private String fallbackProtocol = ApplicationProtocolNames.HTTP_1_1; private AccessLogger accessLogger; private Http2Settings http2Settings = new Http2Settings(); + private Http3Settings http3Settings = new Http3Settings(); private boolean keepAliveOnServerError = DEFAULT_KEEP_ALIVE_ON_SERVER_ERROR; private String pcapLoggingPathPattern = null; private List listeners = null; @@ -219,6 +253,26 @@ public void setHttp2(Http2Settings http2) { } } + /** + * Returns the Http3Settings. + * @return The Http3Settings. + */ + @Experimental + public Http3Settings getHttp3() { + return http3Settings; + } + + /** + * Sets the Http3Settings. + * @param http3Settings The Http3Settings. + */ + @Experimental + public void setHttp3Settings(Http3Settings http3Settings) { + if (http3Settings != null) { + this.http3Settings = http3Settings; + } + } + /** * @return The pipeline customizers */ @@ -615,7 +669,6 @@ public int getJsonBufferMaxComponents() { return jsonBufferMaxComponents; } - /** * Maximum number of buffers to keep around in JSON parsing before they should be consolidated. * Defaults to {@value #DEFAULT_JSON_BUFFER_MAX_COMPONENTS}. @@ -776,6 +829,90 @@ public void setMaxHeaderListSize(Long value) { } } + /** + * Configuration for the experimental HTTP/3 server. + */ + @ConfigurationProperties("http3") + @Experimental + public static final class Http3Settings { + private int initialMaxData = DEFAULT_HTTP3_INITIAL_MAX_DATA; + private int initialMaxStreamDataBidirectionalLocal = DEFAULT_HTTP3_INITIAL_MAX_STREAM_DATA_BIDIRECTIONAL_LOCAL; + private int initialMaxStreamDataBidirectionalRemote = DEFAULT_HTTP3_INITIAL_MAX_STREAM_DATA_BIDIRECTIONAL_REMOTE; + private int initialMaxStreamsBidirectional = DEFAULT_HTTP3_INITIAL_MAX_STREAMS_BIDIRECTIONAL; + + /** + * QUIC initial_max_data setting, see RFC 9000. + * + * @return The initial_max_data setting + */ + public int getInitialMaxData() { + return initialMaxData; + } + + /** + * QUIC initial_max_data setting, see RFC 9000. + * + * @param initialMaxData The initial_max_data setting + */ + public void setInitialMaxData(int initialMaxData) { + this.initialMaxData = initialMaxData; + } + + /** + * QUIC initial_max_stream_data_bidi_local setting, see RFC 9000. + * + * @return The initial_max_stream_data_bidi_local setting + */ + public int getInitialMaxStreamDataBidirectionalLocal() { + return initialMaxStreamDataBidirectionalLocal; + } + + /** + * QUIC initial_max_stream_data_bidi_local setting, see RFC 9000. + * + * @param initialMaxStreamDataBidirectionalLocal The initial_max_stream_data_bidi_local setting + */ + public void setInitialMaxStreamDataBidirectionalLocal(int initialMaxStreamDataBidirectionalLocal) { + this.initialMaxStreamDataBidirectionalLocal = initialMaxStreamDataBidirectionalLocal; + } + + /** + * QUIC initial_max_stream_data_bidi_remote setting, see RFC 9000. + * + * @return The initial_max_stream_data_bidi_remote setting + */ + public int getInitialMaxStreamDataBidirectionalRemote() { + return initialMaxStreamDataBidirectionalRemote; + } + + /** + * QUIC initial_max_stream_data_bidi_remote setting, see RFC 9000. + * + * @param initialMaxStreamDataBidirectionalRemote The initial_max_stream_data_bidi_remote setting + */ + public void setInitialMaxStreamDataBidirectionalRemote(int initialMaxStreamDataBidirectionalRemote) { + this.initialMaxStreamDataBidirectionalRemote = initialMaxStreamDataBidirectionalRemote; + } + + /** + * QUIC initial_max_streams_bidi setting, see RFC 9000. + * + * @return The initial_max_streams_bidi setting + */ + public int getInitialMaxStreamsBidirectional() { + return initialMaxStreamsBidirectional; + } + + /** + * QUIC initial_max_streams_bidi setting, see RFC 9000. + * + * @param initialMaxStreamsBidirectional The initial_max_streams_bidi setting + */ + public void setInitialMaxStreamsBidirectional(int initialMaxStreamsBidirectional) { + this.initialMaxStreamsBidirectional = initialMaxStreamsBidirectional; + } + } + /** * Access logger configuration. */ @@ -1272,6 +1409,11 @@ public enum Family { * UNIX domain socket. */ UNIX, + /** + * QUIC (HTTP/3) listener. + */ + @Experimental + QUIC, } } } diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/ssl/CertificateProvidedSslBuilder.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/ssl/CertificateProvidedSslBuilder.java index a927e02151e..d7d2a7defa8 100644 --- a/http-server-netty/src/main/java/io/micronaut/http/server/netty/ssl/CertificateProvidedSslBuilder.java +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/ssl/CertificateProvidedSslBuilder.java @@ -38,6 +38,9 @@ import io.netty.handler.ssl.SslContextBuilder; import io.netty.handler.ssl.SslProvider; import io.netty.handler.ssl.SupportedCipherSuiteFilter; +import io.netty.incubator.codec.http3.Http3; +import io.netty.incubator.codec.quic.QuicSslContext; +import io.netty.incubator.codec.quic.QuicSslContextBuilder; import jakarta.inject.Singleton; import javax.net.ssl.SSLException; @@ -141,6 +144,22 @@ static void setupSslBuilder(SslContextBuilder sslBuilder, SslConfiguration ssl, } } + @Override + public Optional buildQuic() { + QuicSslContextBuilder sslBuilder = QuicSslContextBuilder.forServer(getKeyManagerFactory(ssl), ssl.getKeyStore().getPassword().orElse(null)) + .applicationProtocols(Http3.supportedApplicationProtocols()); + Optional clientAuthentication = ssl.getClientAuthentication(); + if (clientAuthentication.isPresent()) { + ClientAuthentication clientAuth = clientAuthentication.get(); + if (clientAuth == ClientAuthentication.NEED) { + sslBuilder.clientAuth(ClientAuth.REQUIRE); + } else if (clientAuth == ClientAuthentication.WANT) { + sslBuilder.clientAuth(ClientAuth.OPTIONAL); + } + } + return Optional.of(sslBuilder.build()); + } + @Override protected Optional getTrustStore(SslConfiguration ssl) throws Exception { if (trustStoreCache == null) { diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/ssl/SelfSignedSslBuilder.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/ssl/SelfSignedSslBuilder.java index 14f55bd2da7..ae0d1dafd47 100644 --- a/http-server-netty/src/main/java/io/micronaut/http/server/netty/ssl/SelfSignedSslBuilder.java +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/ssl/SelfSignedSslBuilder.java @@ -28,6 +28,9 @@ import io.netty.handler.ssl.SslContext; import io.netty.handler.ssl.SslContextBuilder; import io.netty.handler.ssl.util.SelfSignedCertificate; +import io.netty.incubator.codec.http3.Http3; +import io.netty.incubator.codec.quic.QuicSslContext; +import io.netty.incubator.codec.quic.QuicSslContextBuilder; import jakarta.inject.Singleton; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -95,6 +98,19 @@ public Optional build(SslConfiguration ssl, HttpVersion httpVersion) } } + @Override + public Optional buildQuic() { + SelfSignedCertificate ssc; + try { + ssc = new SelfSignedCertificate(); + } catch (CertificateException e) { + throw new SslConfigurationException("Encountered an error while building a self signed certificate", e); + } + return Optional.of(QuicSslContextBuilder.forServer(ssc.privateKey(), null, ssc.certificate()) + .applicationProtocols(Http3.supportedApplicationProtocols()) + .build()); + } + static class SelfSignedConfigured extends BuildSelfSignedCondition { @Override protected boolean validate(ConditionContext context, boolean deprecatedPropertyFound, boolean newPropertyFound) { diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/ssl/ServerSslBuilder.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/ssl/ServerSslBuilder.java index 968b63fceda..d0204b38842 100644 --- a/http-server-netty/src/main/java/io/micronaut/http/server/netty/ssl/ServerSslBuilder.java +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/ssl/ServerSslBuilder.java @@ -17,6 +17,7 @@ import io.micronaut.http.ssl.ServerSslConfiguration; import io.netty.handler.ssl.SslContext; +import io.netty.incubator.codec.quic.QuicSslContext; import java.util.Optional; @@ -35,4 +36,8 @@ public interface ServerSslBuilder { * @return Builds the SSL configuration wrapped inside an optional */ Optional build(); + + default Optional buildQuic() { + throw new UnsupportedOperationException("QUIC not supported"); + } } diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/types/files/NettySystemFileCustomizableResponseType.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/types/files/NettySystemFileCustomizableResponseType.java index 138f37d65a0..3f95327b5c2 100644 --- a/http-server-netty/src/main/java/io/micronaut/http/server/netty/types/files/NettySystemFileCustomizableResponseType.java +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/types/files/NettySystemFileCustomizableResponseType.java @@ -38,9 +38,8 @@ import io.netty.handler.codec.http.DefaultHttpResponse; import io.netty.handler.codec.http.HttpChunkedInput; import io.netty.handler.codec.http.LastHttpContent; -import io.netty.handler.codec.http2.Http2StreamChannel; -import io.netty.handler.ssl.SslHandler; import io.netty.handler.stream.ChunkedFile; +import io.netty.util.AttributeKey; import io.netty.util.ResourceLeakDetector; import io.netty.util.ResourceLeakDetectorFactory; import io.netty.util.ResourceLeakTracker; @@ -65,6 +64,8 @@ */ @Internal public class NettySystemFileCustomizableResponseType extends SystemFile implements NettyFileCustomizableResponseType { + public static final Supplier> ZERO_COPY_PREDICATE = + SupplierUtil.memoized(() -> AttributeKey.newInstance("zero-copy-predicate")); private static final int LENGTH_8K = 8192; private static final String UNIT_BYTES = "bytes"; @@ -150,9 +151,8 @@ public ChannelFuture write(HttpRequest request, MutableHttpResponse respon FileHolder file = new FileHolder(getFile()); // Write the content. - if (context.pipeline().get(SslHandler.class) == null && - context.pipeline().get(SmartHttpContentCompressor.class).shouldSkip(finalResponse) && - !(context.channel() instanceof Http2StreamChannel)) { + SmartHttpContentCompressor predicate = context.channel().attr(ZERO_COPY_PREDICATE.get()).get(); + if (predicate != null && predicate.shouldSkip(finalResponse)) { // SSL not enabled - can use zero-copy file transfer. context.write(new DefaultFileRegion(file.raf.getChannel(), position, contentLength), context.newProgressivePromise()) .addListener(file); diff --git a/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/QuicTokenHandlerImplSpec.groovy b/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/QuicTokenHandlerImplSpec.groovy new file mode 100644 index 00000000000..7471402be6d --- /dev/null +++ b/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/QuicTokenHandlerImplSpec.groovy @@ -0,0 +1,106 @@ +package io.micronaut.http.server.netty + +import io.netty.buffer.ByteBuf +import io.netty.buffer.ByteBufAllocator +import io.netty.buffer.Unpooled +import spock.lang.Specification + +class QuicTokenHandlerImplSpec extends Specification { + def 'successful validation round-trip'() { + given: + def handler = new QuicTokenHandlerImpl(ByteBufAllocator.DEFAULT) + def cid = Unpooled.wrappedBuffer("foobar".getBytes()) + + when: + ByteBuf token = Unpooled.buffer() + handler.writeToken(token, cid, new InetSocketAddress("1.2.3.4", 1234)) + def offset = handler.validateToken(token, new InetSocketAddress("1.2.3.4", 1234)) + then: + offset > 0 + token.skipBytes(offset) == cid + } + + def 'validation failure: wrong token'() { + given: + def handler = new QuicTokenHandlerImpl(ByteBufAllocator.DEFAULT) + def cid = Unpooled.wrappedBuffer("foobar".getBytes()) + + when: + ByteBuf token = Unpooled.buffer() + handler.writeToken(token, cid, new InetSocketAddress("1.2.3.4", 1234)) + token.setByte(token.readerIndex(), token.getByte(token.readerIndex()) + 1) + then: + handler.validateToken(token, new InetSocketAddress("1.2.3.4", 1234)) == -1 + } + + def 'validation failure: wrong port'() { + given: + def handler = new QuicTokenHandlerImpl(ByteBufAllocator.DEFAULT) + def cid = Unpooled.wrappedBuffer("foobar".getBytes()) + + when: + ByteBuf token = Unpooled.buffer() + handler.writeToken(token, cid, new InetSocketAddress("1.2.3.4", 1235)) + then: + handler.validateToken(token, new InetSocketAddress("1.2.3.4", 1234)) == -1 + } + + def 'validation failure: wrong address'() { + given: + def handler = new QuicTokenHandlerImpl(ByteBufAllocator.DEFAULT) + def cid = Unpooled.wrappedBuffer("foobar".getBytes()) + + when: + ByteBuf token = Unpooled.buffer() + handler.writeToken(token, cid, new InetSocketAddress("1.2.3.5", 1234)) + then: + handler.validateToken(token, new InetSocketAddress("1.2.3.4", 1234)) == -1 + } + + def 'validation failure: wrong cid'() { + given: + def handler = new QuicTokenHandlerImpl(ByteBufAllocator.DEFAULT) + def cid = Unpooled.wrappedBuffer("foobar".getBytes()) + + when: + ByteBuf token = Unpooled.buffer() + handler.writeToken(token, cid, new InetSocketAddress("1.2.3.4", 1234)) + def changeIndex = token.readerIndex() + token.readableBytes() - 1 + token.setByte(changeIndex, token.getByte(changeIndex) + 1) + then: + handler.validateToken(token, new InetSocketAddress("1.2.3.4", 1234)) == -1 + } + + def 'validation window id'() { + given: + def handler = new QuicTokenHandlerImpl(ByteBufAllocator.DEFAULT) { + def windowId = 0 + + @Override + long currentWindowId() { + return windowId + } + } + def cid = Unpooled.wrappedBuffer("foobar".getBytes()) + + when: + ByteBuf token = Unpooled.buffer() + handler.writeToken(token, cid, new InetSocketAddress("1.2.3.4", 1234)) + def offset = handler.validateToken(token, new InetSocketAddress("1.2.3.4", 1234)) + then: + offset > 0 + token.slice().skipBytes(offset) == cid + + when: + handler.windowId++ + offset = handler.validateToken(token, new InetSocketAddress("1.2.3.4", 1234)) + then: + offset > 0 + token.slice().skipBytes(offset) == cid + + when: + handler.windowId++ + then: + handler.validateToken(token, new InetSocketAddress("1.2.3.4", 1234)) == -1 + } +} diff --git a/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/http2/Http3Spec.groovy b/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/http2/Http3Spec.groovy new file mode 100644 index 00000000000..9ec5575b5ec --- /dev/null +++ b/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/http2/Http3Spec.groovy @@ -0,0 +1,79 @@ +package io.micronaut.http.server.netty.http2 + +import io.micronaut.context.ApplicationContext +import io.micronaut.context.annotation.Requires +import io.micronaut.http.HttpStatus +import io.micronaut.http.annotation.Controller +import io.micronaut.http.annotation.Get +import io.micronaut.http.client.HttpClient +import io.micronaut.http.client.exceptions.HttpClientResponseException +import io.micronaut.runtime.server.EmbeddedServer +import org.reactivestreams.Publisher +import reactor.core.publisher.Flux +import spock.lang.Specification + +class Http3Spec extends Specification { + def 'simple request'() { + given: + def ctx = ApplicationContext.run([ + 'micronaut.server.ssl.enabled': true, + 'micronaut.server.ssl.buildSelfSigned': true, + 'micronaut.server.netty.listeners.a.family': 'QUIC', + + "micronaut.http.client.alpn-modes" : ["h3"], + 'micronaut.http.client.ssl.insecure-trust-all-certificates': true, + ]) + def server = ctx.getBean(EmbeddedServer) + server.start() + def client = ctx.createBean(HttpClient, server.URI) + + when: + client.toBlocking().exchange("/") + then: + def e = thrown HttpClientResponseException + e.status == HttpStatus.NOT_FOUND + + when: + client.toBlocking().exchange("/") + then: + e = thrown HttpClientResponseException + e.status == HttpStatus.NOT_FOUND + + cleanup: + server.close() + client.close() + ctx.close() + } + + def 'streaming response'() { + given: + def ctx = ApplicationContext.run([ + 'spec.name': 'Http3Spec', + + 'micronaut.server.ssl.enabled': true, + 'micronaut.server.ssl.buildSelfSigned': true, + 'micronaut.server.netty.listeners.a.family': 'QUIC', + 'micronaut.server.netty.listeners.a.port': -1, + + "micronaut.http.client.alpn-modes" : ["h3"], + 'micronaut.http.client.ssl.insecure-trust-all-certificates': true, + ]) + def server = ctx.getBean(EmbeddedServer) + server.start() + def client = ctx.createBean(HttpClient, server.URI) + + when: + def resp = client.toBlocking().exchange("/h3/stream", String) + then: + resp.body() == '["foo","bar"]' + } + + @Controller + @Requires(property = "spec.name", value = "Http3Spec") + static class Ctrl { + @Get('/h3/stream') + Publisher stream() { + return Flux.fromIterable(['"foo"', '"bar"']) + } + } +} diff --git a/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/resources/Http2StaticResourceCacheSpec.groovy b/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/resources/Http2StaticResourceCacheSpec.groovy index faa3f6e6bf0..b8cf8e85897 100644 --- a/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/resources/Http2StaticResourceCacheSpec.groovy +++ b/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/resources/Http2StaticResourceCacheSpec.groovy @@ -3,7 +3,6 @@ package io.micronaut.http.server.netty.resources import io.micronaut.context.ApplicationContext import io.micronaut.core.annotation.NonNull import io.micronaut.http.HttpHeaders -import io.micronaut.http.netty.AbstractNettyHttpRequest import io.micronaut.runtime.server.EmbeddedServer import io.netty.bootstrap.Bootstrap import io.netty.channel.ChannelHandlerContext @@ -102,7 +101,7 @@ class Http2StaticResourceCacheSpec extends Specification { def request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, '/' + tempFile.getName()) request.headers().add(HttpConversionUtil.ExtensionHeaderNames.SCHEME.text(), 'https') - request.headers().add(AbstractNettyHttpRequest.STREAM_ID, 3) + request.headers().add(HttpConversionUtil.ExtensionHeaderNames.STREAM_ID.text(), 3) request.headers().add(HttpHeaders.IF_MODIFIED_SINCE, Instant.ofEpochMilli(tempFile.lastModified()).atZone(ZoneOffset.UTC).format(DateTimeFormatter.RFC_1123_DATE_TIME)) ctx.channel().writeAndFlush(request) } @@ -115,7 +114,7 @@ class Http2StaticResourceCacheSpec extends Specification { def response = completion.get() then: response.status() == HttpResponseStatus.NOT_MODIFIED - response.headers().get(AbstractNettyHttpRequest.STREAM_ID) == '3' + response.headers().get(HttpConversionUtil.ExtensionHeaderNames.STREAM_ID.text()) == '3' cleanup: tempFile.delete() diff --git a/src/main/docs/guide/httpClient/clientHttp3.adoc b/src/main/docs/guide/httpClient/clientHttp3.adoc new file mode 100644 index 00000000000..ba98f2e449b --- /dev/null +++ b/src/main/docs/guide/httpClient/clientHttp3.adoc @@ -0,0 +1,16 @@ +Since Micronaut 4.x, Micronaut's Netty-based HTTP client can be configured to support HTTP/3. This support is experimental and may change without notice. + +Instead of the TCP used for HTTP/1.1 and HTTP/2, HTTP/3 runs on UDP. If the client is configured with the special `h3` value for the `alpn-modes` property, the client will automatically use HTTP/3 over UDP instead of HTTP/1.1 or HTTP/2 over TCP. At this time, the client cannot fall back to TCP if the server does not support HTTP/3. + +.Enabling HTTP/3 in Clients +[source,yaml] +---- +micronaut: + http: + client: + alpn-modes: [h3] +---- + +Additionally, the netty HTTP/3 codec needs to be present on the classpath: + +dependency:netty-incubator-codec-http3[groupId="io.netty.incubator",artifactId="netty-incubator-codec-http3"] diff --git a/src/main/docs/guide/httpClient/httpClientImplementations/jdkHttpClient.adoc b/src/main/docs/guide/httpClient/httpClientImplementations/jdkHttpClient.adoc index 24120a6a002..fb8030c9161 100644 --- a/src/main/docs/guide/httpClient/httpClientImplementations/jdkHttpClient.adoc +++ b/src/main/docs/guide/httpClient/httpClientImplementations/jdkHttpClient.adoc @@ -10,5 +10,6 @@ The implementation based on https://openjdk.org/groups/net/httpclient/intro.html * Client Filters. * Streaming support. * Multipart requests. +* H2C and HTTP/3. If you require any of these, we recommend you use the <>. diff --git a/src/main/docs/guide/httpServer/http3Server.adoc b/src/main/docs/guide/httpServer/http3Server.adoc new file mode 100644 index 00000000000..9af5699ec7c --- /dev/null +++ b/src/main/docs/guide/httpServer/http3Server.adoc @@ -0,0 +1,23 @@ +Since Micronaut 4.x, Micronaut's Netty-based HTTP server can be configured to support HTTP/3. This support is experimental and may change without notice. + +==== Configuring the Server for HTTP/3 + +Instead of the TCP used for HTTP/1.1 and HTTP/2, HTTP/3 runs on UDP. To expose a HTTP/3 server, you need to define a <> with the special `QUIC` protocol family: + +.Enabling HTTP/3 Support +[source,yaml] +---- +micronaut: + server: + netty: + listeners: + http3Listener: + family: QUIC + port: 8443 +---- + +NOTE: that defining this listener will disable the implicit TCP listeners. You can add them manually as described in the <>. + +Additionally, the netty HTTP/3 codec needs to be present on the classpath: + +dependency:netty-incubator-codec-http3[groupId="io.netty.incubator",artifactId="netty-incubator-codec-http3"] diff --git a/src/main/docs/guide/toc.yml b/src/main/docs/guide/toc.yml index fd9e464d418..43ffd1eb780 100644 --- a/src/main/docs/guide/toc.yml +++ b/src/main/docs/guide/toc.yml @@ -129,6 +129,7 @@ httpServer: websocketServer: Using @ServerWebSocket websocketClient: Using @ClientWebSocket http2Server: HTTP/2 Support + http3Server: HTTP/3 Support serverEvents: Server Events serverConfiguration: title: Configuring the HTTP Server @@ -188,6 +189,7 @@ httpClient: netflixHystrix: Netflix Hystrix Support clientFilter: HTTP Client Filters clientHttp2: HTTP/2 Support + clientHttp3: HTTP/3 Support clientSample: HTTP Client Sample cloud: title: Cloud Native Features diff --git a/test-suite/build.gradle b/test-suite/build.gradle index 6874af17f7c..d0b1dec8366 100644 --- a/test-suite/build.gradle +++ b/test-suite/build.gradle @@ -101,6 +101,7 @@ dependencies { classifier = Os.isFamily(Os.FAMILY_MAC) ? (Os.isArch("aarch64") ? "osx-aarch_64" : "osx-x86_64") : 'linux-x86_64' } } + testImplementation libs.managed.netty.incubator.codec.http3 testImplementation libs.logbook.netty testImplementation libs.managed.logback.classic diff --git a/test-suite/src/test/groovy/io/micronaut/http/client/http2/Http2RequestSpec.groovy b/test-suite/src/test/groovy/io/micronaut/http/client/http2/Http2RequestSpec.groovy index 561bb44d1e8..3c630772ee7 100644 --- a/test-suite/src/test/groovy/io/micronaut/http/client/http2/Http2RequestSpec.groovy +++ b/test-suite/src/test/groovy/io/micronaut/http/client/http2/Http2RequestSpec.groovy @@ -36,18 +36,24 @@ import java.util.function.Consumer // which is not included in this test suite //@IgnoreIf({ !Jvm.current.isJava9Compatible() }) class Http2RequestSpec extends Specification { - @Shared @AutoCleanup EmbeddedServer server = ApplicationContext.run(EmbeddedServer, [ - 'micronaut.server.ssl.enabled': true, - "micronaut.server.http-version" : "2.0", - "micronaut.http.client.http-version" : "2.0", - 'micronaut.server.ssl.buildSelfSigned': true, - 'micronaut.server.ssl.port': -1, - "micronaut.http.client.log-level" : "TRACE", - "micronaut.server.netty.log-level" : "TRACE", - 'micronaut.http.client.ssl.insecure-trust-all-certificates': true - ]) + @Shared @AutoCleanup EmbeddedServer server = ApplicationContext.run(EmbeddedServer, config()) HttpClient client = server.getApplicationContext().getBean(HttpClient) + Map config() { + // overridden for Http3RequestSpec + return [ + 'micronaut.server.ssl.enabled': true, + "micronaut.http.client.plaintext-mode" : "h2c", + "micronaut.http.client.alpn-modes" : ["h2"], + 'micronaut.server.ssl.buildSelfSigned': true, + 'micronaut.server.ssl.port': -1, + 'micronaut.server.http-version': '2.0', + "micronaut.http.client.log-level" : "TRACE", + "micronaut.server.netty.log-level" : "TRACE", + 'micronaut.http.client.ssl.insecure-trust-all-certificates': true + ] + } + void "test make HTTP/2 stream request"() { when:"A non stream request is executed" List people = Flux.from(((StreamingHttpClient)client).jsonStream(HttpRequest.GET("${server.URL}/http2/personStream"), Person)) diff --git a/test-suite/src/test/groovy/io/micronaut/http/client/http2/Http3RequestSpec.groovy b/test-suite/src/test/groovy/io/micronaut/http/client/http2/Http3RequestSpec.groovy new file mode 100644 index 00000000000..bae2a1cd922 --- /dev/null +++ b/test-suite/src/test/groovy/io/micronaut/http/client/http2/Http3RequestSpec.groovy @@ -0,0 +1,11 @@ +package io.micronaut.http.client.http2 + +class Http3RequestSpec extends Http2RequestSpec { + @Override + Map config() { + return super.config() + [ + "micronaut.http.client.alpn-modes" : ["h3"], + 'micronaut.server.netty.listeners.a.family': 'QUIC', + ] + } +} From 74d98faa10e16ed9757abc324217330484f5093b Mon Sep 17 00:00:00 2001 From: Grady Johnson <41174775+Gradsta@users.noreply.github.com> Date: Thu, 9 Mar 2023 07:59:54 -0600 Subject: [PATCH 576/743] Fix bug preventing scheduled jobs from being ran in the specified time zone (#8911) Fixes #8085 --- .../main/java/io/micronaut/scheduling/NextFireTime.java | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/context/src/main/java/io/micronaut/scheduling/NextFireTime.java b/context/src/main/java/io/micronaut/scheduling/NextFireTime.java index 6f1d8eea3b2..5735523a136 100644 --- a/context/src/main/java/io/micronaut/scheduling/NextFireTime.java +++ b/context/src/main/java/io/micronaut/scheduling/NextFireTime.java @@ -34,6 +34,7 @@ final class NextFireTime implements Supplier { private Duration duration; private ZonedDateTime nextFireTime; private final CronExpression cron; + private final ZoneId zoneId; /** * Default constructor. @@ -41,8 +42,7 @@ final class NextFireTime implements Supplier { * @param cron A cron expression */ NextFireTime(CronExpression cron) { - this.cron = cron; - nextFireTime = ZonedDateTime.now(); + this(cron, ZoneId.systemDefault()); } /** @@ -51,12 +51,13 @@ final class NextFireTime implements Supplier { */ NextFireTime(CronExpression cron, ZoneId zoneId) { this.cron = cron; + this.zoneId = zoneId; nextFireTime = ZonedDateTime.now(zoneId); } @Override public Duration get() { - ZonedDateTime now = ZonedDateTime.now(); + ZonedDateTime now = ZonedDateTime.now(zoneId); // check if the task have fired too early computeNextFireTime(now.isAfter(nextFireTime) ? now : nextFireTime); return duration; @@ -64,6 +65,6 @@ public Duration get() { private void computeNextFireTime(ZonedDateTime currentFireTime) { nextFireTime = cron.nextTimeAfter(currentFireTime); - duration = Duration.ofMillis(nextFireTime.toInstant().toEpochMilli() - ZonedDateTime.now().toInstant().toEpochMilli()); + duration = Duration.ofMillis(nextFireTime.toInstant().toEpochMilli() - ZonedDateTime.now(zoneId).toInstant().toEpochMilli()); } } From 248dbd8bc61df0f559f96484c8bbc1c8c9206bd4 Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Thu, 9 Mar 2023 16:01:43 +0100 Subject: [PATCH 577/743] ci: remove build scan url step (#8909) To avoid extra github api calls --- .github/workflows/corretto.yml | 12 ------------ .github/workflows/graalvm.yml | 12 ------------ .github/workflows/gradle.yml | 12 ------------ 3 files changed, 36 deletions(-) diff --git a/.github/workflows/corretto.yml b/.github/workflows/corretto.yml index 7cd1b0dfbf0..24539093881 100644 --- a/.github/workflows/corretto.yml +++ b/.github/workflows/corretto.yml @@ -44,15 +44,3 @@ jobs: GRADLE_ENTERPRISE_CACHE_USERNAME: ${{ secrets.GRADLE_ENTERPRISE_CACHE_USERNAME }} GRADLE_ENTERPRISE_CACHE_PASSWORD: ${{ secrets.GRADLE_ENTERPRISE_CACHE_PASSWORD }} PREDICTIVE_TEST_SELECTION: "${{ github.event_name == 'pull_request' && 'true' || 'false' }}" - - name: Add build scan URL as PR comment - uses: actions/github-script@v5 - if: github.event_name == 'pull_request' && failure() - with: - github-token: ${{secrets.GITHUB_TOKEN}} - script: | - github.rest.issues.createComment({ - issue_number: context.issue.number, - owner: context.repo.owner, - repo: context.repo.repo, - body: '❌ ${{ github.workflow }} failed: ${{ steps.gradle.outputs.build-scan-url }}' - }) diff --git a/.github/workflows/graalvm.yml b/.github/workflows/graalvm.yml index c06db472f01..03d9045f491 100644 --- a/.github/workflows/graalvm.yml +++ b/.github/workflows/graalvm.yml @@ -62,18 +62,6 @@ jobs: GRADLE_ENTERPRISE_CACHE_USERNAME: ${{ secrets.GRADLE_ENTERPRISE_CACHE_USERNAME }} GRADLE_ENTERPRISE_CACHE_PASSWORD: ${{ secrets.GRADLE_ENTERPRISE_CACHE_PASSWORD }} PREDICTIVE_TEST_SELECTION: "${{ github.event_name == 'pull_request' && 'true' || 'false' }}" - - name: Add build scan URL as PR comment - uses: actions/github-script@v5 - if: github.event_name == 'pull_request' && failure() - with: - github-token: ${{secrets.GITHUB_TOKEN}} - script: | - github.rest.issues.createComment({ - issue_number: context.issue.number, - owner: context.repo.owner, - repo: context.repo.repo, - body: '❌ ${{ github.workflow }} ${{ matrix.java }} ${{ matrix.graalvm }} failed: ${{ steps.gradle.outputs.build-scan-url }}' - }) - name: Publish Test Report if: always() uses: mikepenz/action-junit-report@v3.7.1 diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index 69a7471dd8d..081cbd0dab5 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -55,18 +55,6 @@ jobs: GRADLE_ENTERPRISE_CACHE_USERNAME: ${{ secrets.GRADLE_ENTERPRISE_CACHE_USERNAME }} GRADLE_ENTERPRISE_CACHE_PASSWORD: ${{ secrets.GRADLE_ENTERPRISE_CACHE_PASSWORD }} PREDICTIVE_TEST_SELECTION: "${{ github.event_name == 'pull_request' && 'true' || 'false' }}" - - name: Add build scan URL as PR comment - uses: actions/github-script@v5 - if: github.event_name == 'pull_request' && failure() - with: - github-token: ${{secrets.GITHUB_TOKEN}} - script: | - github.rest.issues.createComment({ - issue_number: context.issue.number, - owner: context.repo.owner, - repo: context.repo.repo, - body: '❌ ${{ github.workflow }} failed: ${{ steps.gradle.outputs.build-scan-url }}' - }) - name: Publish Test Report if: always() uses: mikepenz/action-junit-report@v3.7.1 From 4b0b7c43bda2ae1a8c9c9056c0499ea79493021e Mon Sep 17 00:00:00 2001 From: micronaut-build <65172877+micronaut-build@users.noreply.github.com> Date: Fri, 10 Mar 2023 09:38:45 +0100 Subject: [PATCH 578/743] Update common files (#8915) --- .github/renovate.json | 2 +- .github/workflows/graalvm.yml | 71 --------------------------------- .github/workflows/gradle.yml | 28 ++++++++++--- .github/workflows/release.yml | 10 ++--- .github/workflows/sonarqube.yml | 60 ---------------------------- 5 files changed, 28 insertions(+), 143 deletions(-) delete mode 100644 .github/workflows/graalvm.yml delete mode 100644 .github/workflows/sonarqube.yml diff --git a/.github/renovate.json b/.github/renovate.json index aaa729b515d..ccd88d42ba8 100644 --- a/.github/renovate.json +++ b/.github/renovate.json @@ -2,7 +2,7 @@ "extends": [ "config:base" ], - "addLabels": ["dependency-upgrade"], + "addLabels": ["type: dependency-upgrade"], "schedule": [ "every weekend" ], diff --git a/.github/workflows/graalvm.yml b/.github/workflows/graalvm.yml deleted file mode 100644 index 4708c913038..00000000000 --- a/.github/workflows/graalvm.yml +++ /dev/null @@ -1,71 +0,0 @@ -# WARNING: Do not edit this file directly. Instead, go to: -# -# https://github.com/micronaut-projects/micronaut-project-template/tree/master/.github/workflows -# -# and edit them there. Note that it will be sync'ed to all the Micronaut repos -name: GraalVM CE CI -on: - push: - branches: - - master - - '[1-9]+.[0-9]+.x' - pull_request: - branches: - - master - - '[1-9]+.[0-9]+.x' -jobs: - build: - if: github.repository != 'micronaut-projects/micronaut-project-template' - runs-on: ubuntu-latest - strategy: - matrix: - graalvm: [ 'latest'] - java: [ '17' ] - steps: - # https://github.com/actions/virtual-environments/issues/709 - - name: Free disk space - run: | - sudo rm -rf "/usr/local/share/boost" - sudo rm -rf "$AGENT_TOOLSDIRECTORY" - sudo apt-get clean - df -h - - uses: actions/checkout@v3 - - uses: actions/cache@v3 - with: - path: ~/.gradle/caches - key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle') }} - restore-keys: | - ${{ runner.os }}-gradle- - - name: Setup GraalVM CE - uses: graalvm/setup-graalvm@v1 - with: - version: ${{ matrix.graalvm }} - java-version: ${{ matrix.java }} - components: 'native-image' - github-token: ${{ secrets.GITHUB_TOKEN }} - - name: Setup Gradle - uses: gradle/gradle-build-action@v2.4.0 - - name: Build with Gradle - id: gradle - run: | - if ./gradlew tasks --no-daemon --all | grep -w "testNativeImage" - then - ./gradlew check testNativeImage --continue --no-daemon - else - ./gradlew check --continue --no-daemon - fi - env: - TESTCONTAINERS_RYUK_DISABLED: true - GH_TOKEN_PUBLIC_REPOS_READONLY: ${{ secrets.GH_TOKEN_PUBLIC_REPOS_READONLY }} - GH_USERNAME: ${{ secrets.GH_USERNAME }} - GRADLE_ENTERPRISE_ACCESS_KEY: ${{ secrets.GRADLE_ENTERPRISE_ACCESS_KEY }} - GRADLE_ENTERPRISE_CACHE_USERNAME: ${{ secrets.GRADLE_ENTERPRISE_CACHE_USERNAME }} - GRADLE_ENTERPRISE_CACHE_PASSWORD: ${{ secrets.GRADLE_ENTERPRISE_CACHE_PASSWORD }} - PREDICTIVE_TEST_SELECTION: "${{ github.event_name == 'pull_request' && 'true' || 'false' }}" - - name: Publish Test Report - if: always() - uses: mikepenz/action-junit-report@v3.7.5 - with: - check_name: GraalVM CE CI / Test Report (Java ${{ matrix.java }}) - report_paths: '**/build/test-results/test/TEST-*.xml' - check_retries: 'true' diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index ed53f091054..4dc98860301 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -19,6 +19,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: + graalvm: [ 'latest'] java: ['17'] steps: # https://github.com/actions/virtual-environments/issues/709 @@ -29,13 +30,15 @@ jobs: sudo apt-get clean df -h - uses: actions/checkout@v3 - - name: Set up JDK - uses: actions/setup-java@v3 + - name: Setup GraalVM CE + uses: graalvm/setup-graalvm@v1 with: - distribution: 'temurin' + version: ${{ matrix.graalvm }} java-version: ${{ matrix.java }} + components: 'native-image' + github-token: ${{ secrets.GITHUB_TOKEN }} - name: Setup Gradle - uses: gradle/gradle-build-action@v2.4.0 + uses: gradle/gradle-build-action@v2 - name: Optional setup step env: GRADLE_ENTERPRISE_ACCESS_KEY: ${{ secrets.GRADLE_ENTERPRISE_ACCESS_KEY }} @@ -55,9 +58,22 @@ jobs: GRADLE_ENTERPRISE_CACHE_USERNAME: ${{ secrets.GRADLE_ENTERPRISE_CACHE_USERNAME }} GRADLE_ENTERPRISE_CACHE_PASSWORD: ${{ secrets.GRADLE_ENTERPRISE_CACHE_PASSWORD }} PREDICTIVE_TEST_SELECTION: "${{ github.event_name == 'pull_request' && 'true' || 'false' }}" + - name: Run static analysis + if: github.repository_owner == 'micronaut-projects' + run: | + ./gradlew sonarqube + env: + TESTCONTAINERS_RYUK_DISABLED: true + GRADLE_ENTERPRISE_ACCESS_KEY: ${{ secrets.GRADLE_ENTERPRISE_ACCESS_KEY }} + GRADLE_ENTERPRISE_CACHE_USERNAME: ${{ secrets.GRADLE_ENTERPRISE_CACHE_USERNAME }} + GRADLE_ENTERPRISE_CACHE_PASSWORD: ${{ secrets.GRADLE_ENTERPRISE_CACHE_PASSWORD }} + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_TOKEN_PUBLIC_REPOS_READONLY: ${{ secrets.GH_TOKEN_PUBLIC_REPOS_READONLY }} + GH_USERNAME: ${{ secrets.GH_USERNAME }} - name: Publish Test Report if: always() - uses: mikepenz/action-junit-report@v3.7.5 + uses: mikepenz/action-junit-report@v3 with: check_name: Java CI / Test Report (${{ matrix.java }}) report_paths: '**/build/test-results/test/TEST-*.xml' @@ -72,7 +88,7 @@ jobs: if: success() && github.event_name == 'push' && matrix.java == '17' env: GH_TOKEN_PUBLIC_REPOS_READONLY: ${{ secrets.GH_TOKEN_PUBLIC_REPOS_READONLY }} - GH_USERNAME: ${{ secrets.GH_USERNAME }} + GH_USERNAME: ${{ secrets.GH_USERNAME }} SONATYPE_USERNAME: ${{ secrets.SONATYPE_USERNAME }} SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }} GRADLE_ENTERPRISE_ACCESS_KEY: ${{ secrets.GRADLE_ENTERPRISE_ACCESS_KEY }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a079b9d8928..120e5a6d329 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -66,13 +66,13 @@ jobs: # Store the hash in a file, which is uploaded as a workflow artifact. echo $(sha256sum $ARTIFACTS | base64 -w0) > artifacts-sha256 - name: Upload build artifacts - uses: actions/upload-artifact@3cea5372237819ed00197afe530f5a7ea3e805c8 # v3.1.0 + uses: actions/upload-artifact@v3 with: name: gradle-build-outputs path: build/repo/${{ steps.publish.outputs.group }}/*/${{ steps.publish.outputs.version }}/* retention-days: 5 - name: Upload artifacts-sha256 - uses: actions/upload-artifact@3cea5372237819ed00197afe530f5a7ea3e805c8 # v3.1.0 + uses: actions/upload-artifact@v3 with: name: artifacts-sha256 path: artifacts-sha256 @@ -130,7 +130,7 @@ jobs: artifacts-sha256: ${{ steps.set-hash.outputs.artifacts-sha256 }} steps: - name: Download artifacts-sha256 - uses: actions/download-artifact@9782bd6a9848b53b110e712e20e42d89988822b7 # v3.0.1 + uses: actions/download-artifact@v3 with: name: artifacts-sha256 # The SLSA provenance generator expects the hash digest of artifacts to be passed as a job @@ -161,9 +161,9 @@ jobs: if: startsWith(github.ref, 'refs/tags/') steps: - name: Checkout repository - uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b # v3.0.2 + uses: actions/checkout@v3 - name: Download artifacts - uses: actions/download-artifact@9782bd6a9848b53b110e712e20e42d89988822b7 # v3.0.1 + uses: actions/download-artifact@v3 with: name: gradle-build-outputs path: build/repo diff --git a/.github/workflows/sonarqube.yml b/.github/workflows/sonarqube.yml deleted file mode 100644 index 37231c88d10..00000000000 --- a/.github/workflows/sonarqube.yml +++ /dev/null @@ -1,60 +0,0 @@ -# WARNING: Do not edit this file directly. Instead, go to: -# -# https://github.com/micronaut-projects/micronaut-project-template/tree/master/.github/workflows -# -# and edit them there. Note that it will be sync'ed to all the Micronaut repos -name: Static Analysis -on: - push: - branches: - - master - - '[1-9]+.[0-9]+.x' - pull_request: - branches: - - master - - '[1-9]+.[0-9]+.x' -jobs: - build: - if: github.repository != 'micronaut-projects/micronaut-project-template' - runs-on: ubuntu-latest - steps: - # https://github.com/actions/virtual-environments/issues/709 - - name: Free disk space - run: | - sudo rm -rf "/usr/local/share/boost" - sudo rm -rf "$AGENT_TOOLSDIRECTORY" - sudo apt-get clean - df -h - - uses: actions/checkout@v3 - with: - fetch-depth: 0 - - uses: actions/cache@v3 - with: - path: ~/.gradle/caches - key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle') }} - restore-keys: | - ${{ runner.os }}-gradle- - - name: Set up JDK - uses: actions/setup-java@v3 - with: - distribution: 'temurin' - java-version: 17 - - name: Optional setup step - env: - GRADLE_ENTERPRISE_ACCESS_KEY: ${{ secrets.GRADLE_ENTERPRISE_ACCESS_KEY }} - GRADLE_ENTERPRISE_CACHE_USERNAME: ${{ secrets.GRADLE_ENTERPRISE_CACHE_USERNAME }} - GRADLE_ENTERPRISE_CACHE_PASSWORD: ${{ secrets.GRADLE_ENTERPRISE_CACHE_PASSWORD }} - run: | - [ -f ./setup.sh ] && ./setup.sh || true - - name: Analyse with Gradle - run: | - ./gradlew check sonarqube --no-daemon --parallel --continue - env: - TESTCONTAINERS_RYUK_DISABLED: true - GRADLE_ENTERPRISE_ACCESS_KEY: ${{ secrets.GRADLE_ENTERPRISE_ACCESS_KEY }} - GRADLE_ENTERPRISE_CACHE_USERNAME: ${{ secrets.GRADLE_ENTERPRISE_CACHE_USERNAME }} - GRADLE_ENTERPRISE_CACHE_PASSWORD: ${{ secrets.GRADLE_ENTERPRISE_CACHE_PASSWORD }} - SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - GH_TOKEN_PUBLIC_REPOS_READONLY: ${{ secrets.GH_TOKEN_PUBLIC_REPOS_READONLY }} - GH_USERNAME: ${{ secrets.GH_USERNAME }} From 4b2bbdfce4a1cb56842e3c5d9ecb55d5095348fc Mon Sep 17 00:00:00 2001 From: micronaut-build <65172877+micronaut-build@users.noreply.github.com> Date: Fri, 10 Mar 2023 14:52:04 +0100 Subject: [PATCH 579/743] Bump micronaut-test to 3.9.2 (#8922) --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index a3c4d74a621..7cf7741733c 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -117,7 +117,7 @@ managed-micronaut-serialization = "1.5.2" managed-micronaut-servlet = "3.3.5" managed-micronaut-spring = "4.5.1" managed-micronaut-sql = "4.8.0" -managed-micronaut-test = "3.9.1" +managed-micronaut-test = "3.9.2" managed-micronaut-test-resources = "1.2.3" managed-micronaut-toml = "1.1.3" managed-micronaut-tracing = "4.5.0" From 71efba6e264eb01d0288145f400aaa65fdeccbcb Mon Sep 17 00:00:00 2001 From: micronaut-build <65172877+micronaut-build@users.noreply.github.com> Date: Fri, 10 Mar 2023 14:54:20 +0100 Subject: [PATCH 580/743] Bump micronaut-openapi to 4.8.5 (#8921) --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 32146f71d2c..aada27d0e49 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -100,7 +100,7 @@ managed-micronaut-neo4j = "5.2.0" managed-micronaut-nats = "3.1.0" managed-micronaut-netflix = "2.1.0" managed-micronaut-object-storage = "1.1.0" -managed-micronaut-openapi = "4.8.4" +managed-micronaut-openapi = "4.8.5" managed-micronaut-oraclecloud = "2.3.4" managed-micronaut-picocli = "4.3.0" managed-micronaut-problem = "2.6.0" From 9559a1c0c8a618ae1616eab275f9265b150c70c8 Mon Sep 17 00:00:00 2001 From: micronaut-build <65172877+micronaut-build@users.noreply.github.com> Date: Fri, 10 Mar 2023 14:55:38 +0100 Subject: [PATCH 581/743] Bump micronaut-micrometer to 4.8.2 (#8920) --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 7cf7741733c..ae3a20bfe96 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -90,7 +90,7 @@ managed-micronaut-jmx = "3.2.0" managed-micronaut-kafka = "4.5.2" managed-micronaut-kotlin = "3.2.2" managed-micronaut-kubernetes = "3.4.0" -managed-micronaut-micrometer = "4.8.1" +managed-micronaut-micrometer = "4.8.2" managed-micronaut-microstream = "1.3.0" managed-micronaut-liquibase = "5.7.0" managed-micronaut-mongo = "4.6.0" From af9c3fd2c95497e67d517e5ff7798638f1f76292 Mon Sep 17 00:00:00 2001 From: Tim Yates Date: Fri, 10 Mar 2023 15:48:15 +0000 Subject: [PATCH 582/743] Run Corretto nightly instead of every merge (#8924) This also introduces an environment variable so the tests should not come from the cache https://github.com/micronaut-projects/micronaut-build/pull/519 --- .github/workflows/corretto.yml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/corretto.yml b/.github/workflows/corretto.yml index 24539093881..fd15f1f8b97 100644 --- a/.github/workflows/corretto.yml +++ b/.github/workflows/corretto.yml @@ -1,8 +1,8 @@ name: Corretto CI on: - push: - branches: - - '[1-9]+.[0-9]+.x' + schedule: + - cron: "0 1 * * 1-5" # Mon-Fri at 1am UTC + workflow_dispatch: jobs: build: runs-on: ubuntu-latest @@ -39,6 +39,7 @@ jobs: env: GH_TOKEN_PUBLIC_REPOS_READONLY: ${{ secrets.GH_TOKEN_PUBLIC_REPOS_READONLY }} GH_USERNAME: ${{ secrets.GH_USERNAME }} + MICRONAUT_TEST_USE_VENDOR: true TESTCONTAINERS_RYUK_DISABLED: true GRADLE_ENTERPRISE_ACCESS_KEY: ${{ secrets.GRADLE_ENTERPRISE_ACCESS_KEY }} GRADLE_ENTERPRISE_CACHE_USERNAME: ${{ secrets.GRADLE_ENTERPRISE_CACHE_USERNAME }} From 3ea5f95d1f2024ba862d396e8a6b40f3b474e508 Mon Sep 17 00:00:00 2001 From: Denis Stepanov Date: Sat, 11 Mar 2023 02:45:31 -0500 Subject: [PATCH 583/743] Migrate to Jakarta validation (#8916) --- .../io/micronaut/logging/LoggingSystem.java | 4 +- .../executor/ExecutorConfiguration.java | 2 +- .../executor/UserExecutorConfiguration.java | 2 +- .../inject/writer/BeanDefinitionVisitor.java | 2 +- .../http/client/aop/RequestBeanSpec.groovy | 4 +- .../convert/DateTimeConversionSpec.groovy | 2 +- .../docs/annotation/PetControllerTest.java | 2 +- .../client/docs/annotation/PetOperations.java | 4 +- .../http/client/stream/StreamPostSpec.groovy | 4 +- ...atMapAndRequestInReactorContextSpec.groovy | 2 +- .../server/tck/tests/ErrorHandlerTest.java | 4 +- .../http/server/tck/tests/MiscTest.java | 2 +- .../server/tck/tests/ResponseStatusTest.java | 2 +- .../test/AbstractBeanDefinitionSpec.groovy | 2 +- .../GroovyAnnotationMetadataBuilder.java | 3 +- .../compile/IntroductionWithAroundSpec.groovy | 2 +- .../aop/compile/PropertyAdviceSpec.groovy | 2 +- .../aop/compile/ValidatedNonBeanSpec.groovy | 2 +- .../visitor/GroovyBeanPropertiesSpec.groovy | 6 +- .../visitor/GroovyDocumentationSpec.groovy | 4 +- .../ast/groovy/visitor/SuperClass.groovy | 4 +- .../inject/beans/BeanDefinitionSpec.groovy | 4 +- ...mmutableConfigurationPropertiesSpec.groovy | 2 +- ...nterfaceConfigurationPropertiesSpec.groovy | 22 ++-- .../inject/configproperties/Pojo.groovy | 4 +- .../ValidatedConfigurationSpec.groovy | 14 +- .../ValidatedGetterConfigurationSpec.groovy | 6 +- .../executable/ExecutableBeanSpec.groovy | 2 +- .../generics/GenericTypeArgumentsSpec.groovy | 4 +- .../visitor/BeanIntrospectionSpec.groovy | 40 +++--- .../inject/visitor/ClassElementSpec.groovy | 32 ++--- .../inject/visitor/ElementAnnotateSpec.groovy | 2 +- .../inject/visitor/InterfaceWithGenerics.java | 4 +- .../validation/ValidatedParseSpec.groovy | 14 +- .../inject/visitor/ElementAnnotateSpec.groovy | 2 +- .../visitor/InheritanceVisitorSpec.groovy | 2 +- .../beans/AnnotatedIntrospectedSpec.groovy | 2 +- .../beans/BeanIntrospectionSpec.groovy | 124 +++++++++--------- .../beans/OptionalValueExtractorTest.java | 8 +- .../inject/visitor/beans/TestEntity.java | 2 +- .../reflection/OptionalDoubleHolder.java | 4 +- .../beans/reflection/OptionalHolder.java | 4 +- .../beans/reflection/OptionalIntHolder.java | 4 +- .../beans/reflection/OptionalLongHolder.java | 4 +- .../beans/reflection/PrivateAccessTest.java | 6 +- .../JavaAnnotationMetadataBuilder.java | 2 +- .../annotation/AnnotateTypeArgSpec.groovy | 8 +- .../compile/IntroductionAnnotationSpec.groovy | 6 +- .../compile/IntroductionWithAroundSpec.groovy | 2 +- .../compile/OriginatingElementsSpec.groovy | 6 +- .../aop/compile/ValidatedNonBeanSpec.groovy | 2 +- .../aop/introduction/DeleteByIdCrudRepo.java | 2 +- ...roductionAdviceWithNewInterfaceSpec.groovy | 2 +- .../micronaut/aop/introduction/MyRepo2.java | 2 +- ...roducedWithRepeatableAnnotationSpec.groovy | 4 +- .../AnnotationsOnGenericTypesSpec.groovy | 24 ++-- .../ArgumentAnnotationMetadataSpec.groovy | 12 +- .../inject/beans/BeanDefinitionSpec.groovy | 15 ++- ...mmutableConfigurationPropertiesSpec.groovy | 13 +- ...nterfaceConfigurationPropertiesSpec.groovy | 22 ++-- .../inject/configproperties/Pojo.java | 4 +- .../configproperties/ValidatedConfig.java | 4 +- .../ValidatedConfigurationSpec.groovy | 8 +- .../ValidatedGetterConfig.java | 4 +- .../ValidatedGetterConfigurationSpec.groovy | 2 +- .../configproperties/itfce/MyConfig.java | 2 +- .../configproperties/itfce/MyEachConfig.java | 2 +- .../writeonly/WriteOnlyConfigProperties.java | 2 +- .../executable/ExecutableBeanSpec.groovy | 4 +- .../micronaut/inject/executable/MyBean.java | 2 +- .../generics/GenericTypeArgumentsSpec.groovy | 6 +- .../io/micronaut/inject/records/Test.java | 2 +- .../io/micronaut/inject/records/Test2.java | 2 +- .../RequiresBeanPropertiesSpec.groovy | 2 +- .../micronaut/inject/validation/Account1.java | 2 +- .../micronaut/inject/validation/Account2.java | 2 +- .../micronaut/inject/validation/Account3.java | 2 +- .../ClassElementAnnotationsRetaining.groovy | 30 ++--- .../visitors/ClassElementSpec.groovy | 30 ++--- .../visitors/DocumentationSpec.groovy | 4 +- .../visitors/InterfaceWithGenerics.java | 4 +- .../visitors/PropertyElementSpec.groovy | 12 +- .../KotlinAnnotationMetadataBuilder.kt | 2 +- .../compile/IntroductionAnnotationSpec.groovy | 8 +- .../compile/IntroductionWithAroundSpec.groovy | 2 +- .../aop/compile/ValidatedNonBeanSpec.groovy | 2 +- ...roductionAdviceWithNewInterfaceSpec.groovy | 2 +- .../beans/BeanDefinitionSpec.groovy | 6 +- .../executable/ExecutableBeanSpec.groovy | 4 +- .../inject/ast/ClassElementSpec.groovy | 54 ++++---- ...mmutableConfigurationPropertiesSpec.groovy | 10 +- ...nterfaceConfigurationPropertiesSpec.groovy | 22 ++-- .../ValidatedConfigurationSpec.groovy | 2 +- .../generics/GenericTypeArgumentsSpec.groovy | 4 +- .../visitor/BeanIntrospectionSpec.groovy | 30 ++--- .../aop/introduction/DeleteByIdCrudRepo.kt | 2 +- .../processing/aop/introduction/MyRepo2.kt | 2 +- .../processing/beans/configproperties/Pojo.kt | 4 +- .../beans/configproperties/ValidatedConfig.kt | 4 +- .../processing/elementapi/TestEntity.kt | 2 +- .../inject/configproperties/Pojo.kt | 4 +- .../configproperties/ValidatedConfig.kt | 4 +- .../inject/configproperties/itfce/MyConfig.kt | 2 +- .../configproperties/itfce/MyEachConfig.kt | 2 +- .../inject/ValidatedBeanDefinition.java | 2 +- .../yaml/YamlPropertySourceLoaderSpec.groovy | 6 +- .../yaml/YamlPropertySourceLoaderSpec2.groovy | 1 - .../env/JsonPropertySourceLoaderSpec.groovy | 16 +-- .../endpoint/loggers/LoggersEndpoint.java | 2 +- .../endpoint/loggers/LoggersManager.java | 4 +- .../loggers/ManagedLoggingSystem.java | 2 +- .../loggers/impl/DefaultLoggersManager.java | 4 +- .../health/indicator/HealthResult.java | 2 +- .../retry/annotation/CircuitBreaker.java | 2 +- .../micronaut/retry/annotation/Retryable.java | 2 +- settings.gradle | 2 +- src/main/docs/guide/aop/validation.adoc | 4 +- src/main/docs/guide/appendix/breaks.adoc | 2 +- .../guide/config/configurationProperties.adoc | 2 +- .../docs/guide/config/immutableConfig.adoc | 6 +- .../guide/httpClient/clientAnnotation.adoc | 4 +- .../docs/guide/httpServer/datavalidation.adoc | 8 +- .../builtInExceptionHandlers.adoc | 2 +- .../reactiveServer/bodyAnnotation.adoc | 2 +- src/main/docs/guide/ioc/beanValidation.adoc | 14 +- .../languageSupport/kotlin/openandaop.adoc | 2 +- .../docs/annotation/PetControllerSpec.groovy | 2 +- .../docs/annotation/PetOperations.groovy | 4 +- .../docs/annotation/headers/PetClient.groovy | 4 +- .../docs/config/immutable/EngineConfig.groovy | 6 +- .../docs/config/itfce/EngineConfig.groovy | 6 +- .../docs/config/mapFormat/EngineConfig.groovy | 2 +- .../config/properties/EngineConfig.groovy | 4 +- .../docs/datavalidation/groups/Email.groovy | 2 +- .../groups/EmailController.groovy | 2 +- .../groups/FinalValidation.groovy | 2 +- .../params/EmailController.groovy | 2 +- .../docs/datavalidation/pogo/Email.groovy | 2 +- .../pogo/EmailController.groovy | 2 +- .../nullable/EngineConfiguration.groovy | 2 +- .../docs/ioc/validation/Person.groovy | 4 +- .../docs/ioc/validation/PersonService.groovy | 2 +- .../ioc/validation/PersonServiceSpec.groovy | 2 +- .../validation/custom/DurationPattern.groovy | 2 +- .../DurationPatternValidatorSpec.groovy | 2 +- .../validation/custom/HolidayService.groovy | 2 +- .../ioc/validation/pojo/PersonService.groovy | 2 +- .../validation/pojo/PersonServiceSpec.groovy | 2 +- .../server/binding/BookmarkController.groovy | 2 +- .../server/binding/MovieTicketBean.groovy | 2 +- .../binding/MovieTicketController.groovy | 2 +- .../server/binding/PaginationCommand.groovy | 6 +- .../docs/server/body/MessageController.groovy | 2 +- .../docs/session/ShoppingController.groovy | 2 +- .../docs/annotation/PetControllerSpec.kt | 2 +- .../docs/annotation/PetOperations.kt | 4 +- .../docs/config/immutable/EngineConfig.kt | 6 +- .../docs/config/itfce/EngineConfig.kt | 6 +- .../docs/config/mapFormat/EngineConfig.kt | 2 +- .../docs/config/properties/EngineConfig.kt | 4 +- .../docs/datavalidation/groups/Email.kt | 2 +- .../datavalidation/groups/EmailController.kt | 2 +- .../datavalidation/groups/FinalValidation.kt | 2 +- .../datavalidation/params/EmailController.kt | 2 +- .../docs/datavalidation/pogo/Email.kt | 2 +- .../datavalidation/pogo/EmailController.kt | 2 +- .../factories/nullable/EngineConfiguration.kt | 2 +- .../micronaut/docs/ioc/validation/Person.kt | 6 +- .../docs/ioc/validation/PersonService.kt | 2 +- .../docs/ioc/validation/PersonServiceSpec.kt | 2 +- .../ioc/validation/custom/DurationPattern.kt | 2 +- .../custom/DurationPatternValidatorSpec.kt | 2 +- .../ioc/validation/custom/HolidayService.kt | 2 +- .../docs/ioc/validation/pojo/PersonService.kt | 2 +- .../ioc/validation/pojo/PersonServiceSpec.kt | 2 +- .../docs/server/binding/BookmarkController.kt | 2 +- .../docs/server/binding/MovieTicketBean.kt | 2 +- .../server/binding/MovieTicketController.kt | 4 +- .../docs/server/binding/PaginationCommand.kt | 6 +- .../docs/server/body/MessageController.kt | 2 +- .../micronaut/validation/validator/Person.kt | 6 +- .../validation/validator/ValidatorSpec.kt | 4 +- .../docs/annotation/PetControllerSpec.kt | 2 +- .../docs/annotation/PetOperations.kt | 4 +- .../docs/config/immutable/EngineConfig.kt | 6 +- .../docs/config/itfce/EngineConfig.kt | 6 +- .../docs/config/mapFormat/EngineConfig.kt | 2 +- .../docs/config/properties/EngineConfig.kt | 4 +- .../docs/datavalidation/groups/Email.kt | 2 +- .../datavalidation/groups/EmailController.kt | 2 +- .../datavalidation/groups/FinalValidation.kt | 2 +- .../datavalidation/params/EmailController.kt | 2 +- .../docs/datavalidation/pogo/Email.kt | 2 +- .../datavalidation/pogo/EmailController.kt | 2 +- .../factories/nullable/EngineConfiguration.kt | 2 +- .../micronaut/docs/ioc/validation/Person.kt | 6 +- .../docs/ioc/validation/PersonService.kt | 2 +- .../docs/ioc/validation/PersonServiceSpec.kt | 2 +- .../ioc/validation/custom/DurationPattern.kt | 2 +- .../custom/DurationPatternValidatorSpec.kt | 2 +- .../ioc/validation/custom/HolidayService.kt | 2 +- .../docs/ioc/validation/pojo/PersonService.kt | 2 +- .../ioc/validation/pojo/PersonServiceSpec.kt | 2 +- .../docs/server/binding/BookmarkController.kt | 2 +- .../docs/server/binding/MovieTicketBean.kt | 2 +- .../server/binding/MovieTicketController.kt | 4 +- .../docs/server/binding/PaginationCommand.kt | 6 +- .../docs/server/body/MessageController.kt | 2 +- .../micronaut/validation/validator/Person.kt | 6 +- .../validation/validator/ValidatorSpec.kt | 4 +- .../docs/aop/validation/BookService.java | 2 +- .../aop/validation/BookServiceSpec.groovy | 2 +- ...idatedWithJavaxAnnoationNonNullSpec.groovy | 4 +- .../http/hateoas/JsonErrorEmbeddedSpec.groovy | 2 +- .../micronaut/docs/annotation/PetClient.java | 4 +- .../docs/annotation/PetControllerSpec.java | 2 +- .../docs/annotation/PetOperations.java | 4 +- .../docs/config/immutable/EngineConfig.java | 6 +- .../docs/config/itfce/EngineConfig.java | 6 +- .../docs/config/mapFormat/EngineConfig.java | 2 +- .../docs/config/properties/EngineConfig.java | 4 +- .../docs/datavalidation/groups/Email.java | 2 +- .../groups/EmailController.java | 2 +- .../groups/FinalValidation.java | 2 +- .../params/EmailController.java | 2 +- .../docs/datavalidation/pogo/Email.java | 2 +- .../datavalidation/pogo/EmailController.java | 2 +- .../nullable/EngineConfiguration.java | 2 +- .../micronaut/docs/ioc/validation/Person.java | 4 +- .../docs/ioc/validation/PersonService.java | 2 +- .../ioc/validation/PersonServiceSpec.java | 2 +- .../validation/custom/DurationPattern.java | 2 +- .../custom/DurationPatternValidatorSpec.java | 4 +- .../ioc/validation/custom/HolidayService.java | 2 +- .../ioc/validation/pojo/PersonService.java | 2 +- .../validation/pojo/PersonServiceSpec.java | 4 +- .../server/binding/BookmarkController.java | 2 +- .../docs/server/binding/MovieTicketBean.java | 2 +- .../server/binding/MovieTicketController.java | 2 +- .../server/binding/PaginationCommand.java | 6 +- .../docs/server/body/MessageController.java | 2 +- .../docs/session/ShoppingController.java | 2 +- 242 files changed, 616 insertions(+), 610 deletions(-) diff --git a/context/src/main/java/io/micronaut/logging/LoggingSystem.java b/context/src/main/java/io/micronaut/logging/LoggingSystem.java index 6a490aaea23..e35ed958da2 100644 --- a/context/src/main/java/io/micronaut/logging/LoggingSystem.java +++ b/context/src/main/java/io/micronaut/logging/LoggingSystem.java @@ -17,8 +17,8 @@ import io.micronaut.core.annotation.Indexed; -import javax.validation.constraints.NotBlank; -import javax.validation.constraints.NotNull; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; /** * Abstraction for a logging system. diff --git a/context/src/main/java/io/micronaut/scheduling/executor/ExecutorConfiguration.java b/context/src/main/java/io/micronaut/scheduling/executor/ExecutorConfiguration.java index 53ea4bde03a..303d962df0d 100644 --- a/context/src/main/java/io/micronaut/scheduling/executor/ExecutorConfiguration.java +++ b/context/src/main/java/io/micronaut/scheduling/executor/ExecutorConfiguration.java @@ -17,7 +17,7 @@ import io.micronaut.core.annotation.Nullable; -import javax.validation.constraints.Min; +import jakarta.validation.constraints.Min; import java.util.Optional; import java.util.concurrent.ThreadFactory; diff --git a/context/src/main/java/io/micronaut/scheduling/executor/UserExecutorConfiguration.java b/context/src/main/java/io/micronaut/scheduling/executor/UserExecutorConfiguration.java index 79e84af8184..2fcc6548512 100644 --- a/context/src/main/java/io/micronaut/scheduling/executor/UserExecutorConfiguration.java +++ b/context/src/main/java/io/micronaut/scheduling/executor/UserExecutorConfiguration.java @@ -22,7 +22,7 @@ import io.micronaut.core.annotation.Nullable; import io.micronaut.core.util.ArgumentUtils; -import javax.validation.constraints.Min; +import jakarta.validation.constraints.Min; import java.util.Optional; import java.util.concurrent.ThreadFactory; diff --git a/core-processor/src/main/java/io/micronaut/inject/writer/BeanDefinitionVisitor.java b/core-processor/src/main/java/io/micronaut/inject/writer/BeanDefinitionVisitor.java index e6534e5b26b..e1e08cf1824 100644 --- a/core-processor/src/main/java/io/micronaut/inject/writer/BeanDefinitionVisitor.java +++ b/core-processor/src/main/java/io/micronaut/inject/writer/BeanDefinitionVisitor.java @@ -168,7 +168,7 @@ void visitDefaultConstructor( Type getProvidedType(); /** - * Make the bean definition as validated by javax.validation. + * Make the bean definition as validated by jakarta.validation. * * @param validated Whether the bean definition is validated */ diff --git a/http-client/src/test/groovy/io/micronaut/http/client/aop/RequestBeanSpec.groovy b/http-client/src/test/groovy/io/micronaut/http/client/aop/RequestBeanSpec.groovy index 9a530b57a2d..2d14297d14a 100644 --- a/http-client/src/test/groovy/io/micronaut/http/client/aop/RequestBeanSpec.groovy +++ b/http-client/src/test/groovy/io/micronaut/http/client/aop/RequestBeanSpec.groovy @@ -21,8 +21,8 @@ import spock.lang.Shared import spock.lang.Specification import javax.annotation.Nullable -import javax.validation.Valid -import javax.validation.constraints.Pattern +import jakarta.validation.Valid +import jakarta.validation.constraints.Pattern class RequestBeanSpec extends Specification { diff --git a/http-client/src/test/groovy/io/micronaut/http/client/convert/DateTimeConversionSpec.groovy b/http-client/src/test/groovy/io/micronaut/http/client/convert/DateTimeConversionSpec.groovy index 3d8fa578bbf..9cc620fff5f 100644 --- a/http-client/src/test/groovy/io/micronaut/http/client/convert/DateTimeConversionSpec.groovy +++ b/http-client/src/test/groovy/io/micronaut/http/client/convert/DateTimeConversionSpec.groovy @@ -26,7 +26,7 @@ import spock.lang.AutoCleanup import spock.lang.Shared import spock.lang.Specification -import javax.validation.constraints.NotNull +import jakarta.validation.constraints.NotNull import java.time.OffsetDateTime import java.time.format.DateTimeFormatter diff --git a/http-client/src/test/groovy/io/micronaut/http/client/docs/annotation/PetControllerTest.java b/http-client/src/test/groovy/io/micronaut/http/client/docs/annotation/PetControllerTest.java index 4b466b863d4..94cd5f461d7 100644 --- a/http-client/src/test/groovy/io/micronaut/http/client/docs/annotation/PetControllerTest.java +++ b/http-client/src/test/groovy/io/micronaut/http/client/docs/annotation/PetControllerTest.java @@ -22,7 +22,7 @@ import org.junit.rules.ExpectedException; import reactor.core.publisher.Mono; -import javax.validation.ConstraintViolationException; +import jakarta.validation.ConstraintViolationException; import static org.junit.jupiter.api.Assertions.assertEquals; diff --git a/http-client/src/test/groovy/io/micronaut/http/client/docs/annotation/PetOperations.java b/http-client/src/test/groovy/io/micronaut/http/client/docs/annotation/PetOperations.java index de858c42f67..e8cfb5e1e31 100644 --- a/http-client/src/test/groovy/io/micronaut/http/client/docs/annotation/PetOperations.java +++ b/http-client/src/test/groovy/io/micronaut/http/client/docs/annotation/PetOperations.java @@ -21,8 +21,8 @@ import io.micronaut.validation.Validated; import org.reactivestreams.Publisher; import io.micronaut.core.async.annotation.SingleResult; -import javax.validation.constraints.Min; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotBlank; // end::imports[] /** diff --git a/http-client/src/test/groovy/io/micronaut/http/client/stream/StreamPostSpec.groovy b/http-client/src/test/groovy/io/micronaut/http/client/stream/StreamPostSpec.groovy index f7af81f049b..af07b27f57c 100644 --- a/http-client/src/test/groovy/io/micronaut/http/client/stream/StreamPostSpec.groovy +++ b/http-client/src/test/groovy/io/micronaut/http/client/stream/StreamPostSpec.groovy @@ -44,8 +44,8 @@ import spock.lang.IgnoreIf import spock.lang.Shared import spock.lang.Specification -import javax.validation.Valid -import javax.validation.constraints.NotNull +import jakarta.validation.Valid +import jakarta.validation.constraints.NotNull import java.util.function.Function /** diff --git a/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/context/FlatMapAndRequestInReactorContextSpec.groovy b/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/context/FlatMapAndRequestInReactorContextSpec.groovy index 0646a882e7b..3bbf4af7e21 100644 --- a/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/context/FlatMapAndRequestInReactorContextSpec.groovy +++ b/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/context/FlatMapAndRequestInReactorContextSpec.groovy @@ -36,7 +36,7 @@ import spock.lang.PendingFeature import spock.lang.Shared import spock.lang.Specification import spock.lang.Unroll -import javax.validation.constraints.NotBlank +import jakarta.validation.constraints.NotBlank import java.util.stream.Stream class FlatMapAndRequestInReactorContextSpec extends Specification { diff --git a/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/ErrorHandlerTest.java b/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/ErrorHandlerTest.java index 6b79e214516..935620a030a 100644 --- a/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/ErrorHandlerTest.java +++ b/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/ErrorHandlerTest.java @@ -43,8 +43,8 @@ import jakarta.inject.Singleton; import org.junit.jupiter.api.Test; -import javax.validation.Valid; -import javax.validation.constraints.Min; +import jakarta.validation.Valid; +import jakarta.validation.constraints.Min; import java.io.IOException; import java.util.Collections; import java.util.Map; diff --git a/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/MiscTest.java b/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/MiscTest.java index 05d8ff5d2a9..f043f0856eb 100644 --- a/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/MiscTest.java +++ b/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/MiscTest.java @@ -32,7 +32,7 @@ import io.micronaut.http.tck.HttpResponseAssertion; import org.junit.jupiter.api.Test; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotBlank; import java.io.IOException; import java.util.Collections; import java.util.Map; diff --git a/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/ResponseStatusTest.java b/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/ResponseStatusTest.java index d2d937d6454..fac27bac64e 100644 --- a/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/ResponseStatusTest.java +++ b/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/ResponseStatusTest.java @@ -30,7 +30,7 @@ import io.micronaut.http.tck.HttpResponseAssertion; import org.junit.jupiter.api.Test; -import javax.validation.ConstraintViolationException; +import jakarta.validation.ConstraintViolationException; import java.io.IOException; import java.util.Collections; import java.util.Optional; diff --git a/inject-groovy-test/src/main/groovy/io/micronaut/ast/transform/test/AbstractBeanDefinitionSpec.groovy b/inject-groovy-test/src/main/groovy/io/micronaut/ast/transform/test/AbstractBeanDefinitionSpec.groovy index b461fa4a46e..ed4f8eb798d 100644 --- a/inject-groovy-test/src/main/groovy/io/micronaut/ast/transform/test/AbstractBeanDefinitionSpec.groovy +++ b/inject-groovy-test/src/main/groovy/io/micronaut/ast/transform/test/AbstractBeanDefinitionSpec.groovy @@ -229,7 +229,7 @@ abstract class AbstractBeanDefinitionSpec extends Specification { protected Class findClass(String name) throws ClassNotFoundException { if (name == className) { def bytes = stream.toByteArray() - return defineClass(name, bytes, 0, bytes.length) + return super.defineClass(name, bytes, 0, bytes.length) } return super.findClass(name) } diff --git a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/annotation/GroovyAnnotationMetadataBuilder.java b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/annotation/GroovyAnnotationMetadataBuilder.java index 796c3cb99b0..2cddaa913c5 100644 --- a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/annotation/GroovyAnnotationMetadataBuilder.java +++ b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/annotation/GroovyAnnotationMetadataBuilder.java @@ -121,8 +121,7 @@ protected boolean isValidationRequired(AnnotatedNode member) { final List annotations = member.getAnnotations(); if (CollectionUtils.isNotEmpty(annotations)) { return annotations.stream().anyMatch((it) -> - it.getClassNode().getName().startsWith("javax.validation") - || it.getClassNode().getName().startsWith("jakarta.validation")); + it.getClassNode().getName().startsWith("jakarta.validation")); } } return false; diff --git a/inject-groovy/src/test/groovy/io/micronaut/aop/compile/IntroductionWithAroundSpec.groovy b/inject-groovy/src/test/groovy/io/micronaut/aop/compile/IntroductionWithAroundSpec.groovy index bcad7f933ab..0cae44f495b 100644 --- a/inject-groovy/src/test/groovy/io/micronaut/aop/compile/IntroductionWithAroundSpec.groovy +++ b/inject-groovy/src/test/groovy/io/micronaut/aop/compile/IntroductionWithAroundSpec.groovy @@ -16,7 +16,7 @@ package introaroundtest; import io.micronaut.aop.introduction.*; import io.micronaut.context.annotation.*; import java.net.*; -import javax.validation.constraints.*; +import jakarta.validation.constraints.*; import jakarta.inject.Singleton; @Stub diff --git a/inject-groovy/src/test/groovy/io/micronaut/aop/compile/PropertyAdviceSpec.groovy b/inject-groovy/src/test/groovy/io/micronaut/aop/compile/PropertyAdviceSpec.groovy index b79483bd964..6694a462ec0 100644 --- a/inject-groovy/src/test/groovy/io/micronaut/aop/compile/PropertyAdviceSpec.groovy +++ b/inject-groovy/src/test/groovy/io/micronaut/aop/compile/PropertyAdviceSpec.groovy @@ -14,7 +14,7 @@ package test; import io.micronaut.aop.interceptors.*; import io.micronaut.context.annotation.*; -import javax.validation.constraints.*; +import jakarta.validation.constraints.*; import javax.inject.Singleton; @Mutating("name") diff --git a/inject-groovy/src/test/groovy/io/micronaut/aop/compile/ValidatedNonBeanSpec.groovy b/inject-groovy/src/test/groovy/io/micronaut/aop/compile/ValidatedNonBeanSpec.groovy index 2ba5a427b2b..ad9f8d86c61 100644 --- a/inject-groovy/src/test/groovy/io/micronaut/aop/compile/ValidatedNonBeanSpec.groovy +++ b/inject-groovy/src/test/groovy/io/micronaut/aop/compile/ValidatedNonBeanSpec.groovy @@ -10,7 +10,7 @@ class ValidatedNonBeanSpec extends AbstractBeanDefinitionSpec { BeanDefinition beanDefinition = buildBeanDefinition("test.DefaultContract", """ package test -import javax.validation.constraints.NotNull +import jakarta.validation.constraints.NotNull import io.micronaut.context.annotation.* import jakarta.inject.Singleton diff --git a/inject-groovy/src/test/groovy/io/micronaut/ast/groovy/visitor/GroovyBeanPropertiesSpec.groovy b/inject-groovy/src/test/groovy/io/micronaut/ast/groovy/visitor/GroovyBeanPropertiesSpec.groovy index 7254d23bcf1..bfd381f163f 100644 --- a/inject-groovy/src/test/groovy/io/micronaut/ast/groovy/visitor/GroovyBeanPropertiesSpec.groovy +++ b/inject-groovy/src/test/groovy/io/micronaut/ast/groovy/visitor/GroovyBeanPropertiesSpec.groovy @@ -2,7 +2,7 @@ package io.micronaut.ast.groovy.visitor import io.micronaut.ast.transform.test.AbstractBeanDefinitionSpec -import javax.validation.constraints.NotBlank +import jakarta.validation.constraints.NotBlank class GroovyBeanPropertiesSpec extends AbstractBeanDefinitionSpec { @@ -10,8 +10,8 @@ class GroovyBeanPropertiesSpec extends AbstractBeanDefinitionSpec { def classElement = buildClassElement(""" package test -import javax.validation.constraints.NotBlank -import javax.validation.constraints.NotNull +import jakarta.validation.constraints.NotBlank +import jakarta.validation.constraints.NotNull import io.micronaut.ast.groovy.visitor.SuperClass class Test extends SuperClass { diff --git a/inject-groovy/src/test/groovy/io/micronaut/ast/groovy/visitor/GroovyDocumentationSpec.groovy b/inject-groovy/src/test/groovy/io/micronaut/ast/groovy/visitor/GroovyDocumentationSpec.groovy index 70d2e7a07cd..631b289583b 100644 --- a/inject-groovy/src/test/groovy/io/micronaut/ast/groovy/visitor/GroovyDocumentationSpec.groovy +++ b/inject-groovy/src/test/groovy/io/micronaut/ast/groovy/visitor/GroovyDocumentationSpec.groovy @@ -10,8 +10,8 @@ class GroovyDocumentationSpec extends AbstractBeanDefinitionSpec { def classElement = buildClassElement(""" package test -import javax.validation.constraints.NotBlank -import javax.validation.constraints.NotNull +import jakarta.validation.constraints.NotBlank +import jakarta.validation.constraints.NotNull import io.micronaut.ast.groovy.visitor.SuperClass /** diff --git a/inject-groovy/src/test/groovy/io/micronaut/ast/groovy/visitor/SuperClass.groovy b/inject-groovy/src/test/groovy/io/micronaut/ast/groovy/visitor/SuperClass.groovy index 83d9bb0f4da..24d340e9119 100644 --- a/inject-groovy/src/test/groovy/io/micronaut/ast/groovy/visitor/SuperClass.groovy +++ b/inject-groovy/src/test/groovy/io/micronaut/ast/groovy/visitor/SuperClass.groovy @@ -1,7 +1,7 @@ package io.micronaut.ast.groovy.visitor -import javax.validation.constraints.NotBlank -import javax.validation.constraints.NotNull +import jakarta.validation.constraints.NotBlank +import jakarta.validation.constraints.NotNull class SuperClass { diff --git a/inject-groovy/src/test/groovy/io/micronaut/inject/beans/BeanDefinitionSpec.groovy b/inject-groovy/src/test/groovy/io/micronaut/inject/beans/BeanDefinitionSpec.groovy index 83d1080788c..2dbdae3404e 100644 --- a/inject-groovy/src/test/groovy/io/micronaut/inject/beans/BeanDefinitionSpec.groovy +++ b/inject-groovy/src/test/groovy/io/micronaut/inject/beans/BeanDefinitionSpec.groovy @@ -243,7 +243,7 @@ class TestBean { given: BeanDefinition definition = buildBeanDefinition('test', 'Test', ''' package test; -import javax.validation.constraints.*; +import jakarta.validation.constraints.*; import java.util.List; @jakarta.inject.Singleton @@ -281,7 +281,7 @@ interface Deserializer { given: BeanDefinition definition = buildBeanDefinition('test', 'Test', ''' package test; -import javax.validation.constraints.*; +import jakarta.validation.constraints.*; import java.util.List; @jakarta.inject.Singleton diff --git a/inject-groovy/src/test/groovy/io/micronaut/inject/configproperties/ImmutableConfigurationPropertiesSpec.groovy b/inject-groovy/src/test/groovy/io/micronaut/inject/configproperties/ImmutableConfigurationPropertiesSpec.groovy index 7e7f5ed92ed..83f84bccbc7 100644 --- a/inject-groovy/src/test/groovy/io/micronaut/inject/configproperties/ImmutableConfigurationPropertiesSpec.groovy +++ b/inject-groovy/src/test/groovy/io/micronaut/inject/configproperties/ImmutableConfigurationPropertiesSpec.groovy @@ -23,7 +23,7 @@ class MyConfig { private int serverPort; @ConfigurationInject - MyConfig(@javax.validation.constraints.NotBlank String host, int serverPort, @io.micronaut.core.annotation.Nullable String nullable) { + MyConfig(@jakarta.validation.constraints.NotBlank String host, int serverPort, @io.micronaut.core.annotation.Nullable String nullable) { this.host = host; this.serverPort = serverPort; } diff --git a/inject-groovy/src/test/groovy/io/micronaut/inject/configproperties/InterfaceConfigurationPropertiesSpec.groovy b/inject-groovy/src/test/groovy/io/micronaut/inject/configproperties/InterfaceConfigurationPropertiesSpec.groovy index daa4e634b24..8abf04deecc 100644 --- a/inject-groovy/src/test/groovy/io/micronaut/inject/configproperties/InterfaceConfigurationPropertiesSpec.groovy +++ b/inject-groovy/src/test/groovy/io/micronaut/inject/configproperties/InterfaceConfigurationPropertiesSpec.groovy @@ -23,10 +23,10 @@ import java.time.Duration; @ConfigurationProperties("foo.bar") interface MyConfig { - @javax.validation.constraints.NotBlank + @jakarta.validation.constraints.NotBlank String getHost(); - @javax.validation.constraints.Min(10L) + @jakarta.validation.constraints.Min(10L) int getServerPort(); } @@ -67,7 +67,7 @@ interface MyConfig { @javax.annotation.Nullable String getHost(); - @javax.validation.constraints.Min(10L) + @jakarta.validation.constraints.Min(10L) Optional getServerPort(); @io.micronaut.core.bind.annotation.Bindable(defaultValue = "http://default") @@ -122,14 +122,14 @@ import java.time.Duration; @Executable interface MyConfig extends ParentConfig { - @javax.validation.constraints.Min(10L) + @jakarta.validation.constraints.Min(10L) int getServerPort(); } @ConfigurationProperties("foo") @Executable interface ParentConfig { - @javax.validation.constraints.NotBlank + @jakarta.validation.constraints.NotBlank String getHost(); } @@ -167,10 +167,10 @@ import java.net.URL; @ConfigurationProperties("foo.bar") @Executable interface MyConfig { - @javax.validation.constraints.NotBlank + @jakarta.validation.constraints.NotBlank String getHost(); - @javax.validation.constraints.Min(10L) + @jakarta.validation.constraints.Min(10L) int getServerPort(); @ConfigurationProperties("child") @@ -204,10 +204,10 @@ import java.net.URL; @ConfigurationProperties("foo.bar") @Executable interface MyConfig { - @javax.validation.constraints.NotBlank + @jakarta.validation.constraints.NotBlank String getHost(); - @javax.validation.constraints.Min(10L) + @jakarta.validation.constraints.Min(10L) int getServerPort(); @Executable @@ -251,10 +251,10 @@ import java.time.Duration; @ConfigurationProperties("foo.bar") interface MyConfig { - @javax.validation.constraints.NotBlank + @jakarta.validation.constraints.NotBlank String junk(String s); - @javax.validation.constraints.Min(10L) + @jakarta.validation.constraints.Min(10L) int getServerPort(); } diff --git a/inject-groovy/src/test/groovy/io/micronaut/inject/configproperties/Pojo.groovy b/inject-groovy/src/test/groovy/io/micronaut/inject/configproperties/Pojo.groovy index 470931bce39..175a5c74f9d 100644 --- a/inject-groovy/src/test/groovy/io/micronaut/inject/configproperties/Pojo.groovy +++ b/inject-groovy/src/test/groovy/io/micronaut/inject/configproperties/Pojo.groovy @@ -2,8 +2,8 @@ package io.micronaut.inject.configproperties import io.micronaut.core.annotation.Introspected -import javax.validation.constraints.Email -import javax.validation.constraints.NotBlank +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.NotBlank @Introspected class Pojo { diff --git a/inject-groovy/src/test/groovy/io/micronaut/inject/configproperties/ValidatedConfigurationSpec.groovy b/inject-groovy/src/test/groovy/io/micronaut/inject/configproperties/ValidatedConfigurationSpec.groovy index 73f7ea18caf..e97c6b21051 100644 --- a/inject-groovy/src/test/groovy/io/micronaut/inject/configproperties/ValidatedConfigurationSpec.groovy +++ b/inject-groovy/src/test/groovy/io/micronaut/inject/configproperties/ValidatedConfigurationSpec.groovy @@ -24,8 +24,8 @@ import io.micronaut.context.exceptions.BeanInstantiationException import io.micronaut.inject.BeanDefinition import io.micronaut.inject.ValidatedBeanDefinition -import javax.validation.constraints.NotBlank -import javax.validation.constraints.NotNull +import jakarta.validation.constraints.NotBlank +import jakarta.validation.constraints.NotNull class ValidatedConfigurationSpec extends AbstractBeanDefinitionSpec { @@ -75,7 +75,7 @@ package test import io.micronaut.context.annotation.ConfigurationProperties import io.micronaut.inject.configproperties.Pojo -import javax.validation.Valid +import jakarta.validation.Valid import java.util.List @ConfigurationProperties("test.valid") @@ -107,12 +107,12 @@ package test import io.micronaut.context.annotation.ConfigurationProperties import io.micronaut.inject.configproperties.Pojo -import javax.validation.Valid +import jakarta.validation.Valid import java.util.List @ConfigurationProperties("test.valid") class MyConfig { - + private List pojos @Valid @@ -139,12 +139,12 @@ package test import io.micronaut.context.annotation.ConfigurationProperties import io.micronaut.inject.configproperties.Pojo -import javax.validation.Valid +import jakarta.validation.Valid import java.util.List @ConfigurationProperties("test.valid") class MyConfig { - + @Valid List pojos } diff --git a/inject-groovy/src/test/groovy/io/micronaut/inject/configproperties/ValidatedGetterConfigurationSpec.groovy b/inject-groovy/src/test/groovy/io/micronaut/inject/configproperties/ValidatedGetterConfigurationSpec.groovy index fd6f489be16..f03e79f9df5 100644 --- a/inject-groovy/src/test/groovy/io/micronaut/inject/configproperties/ValidatedGetterConfigurationSpec.groovy +++ b/inject-groovy/src/test/groovy/io/micronaut/inject/configproperties/ValidatedGetterConfigurationSpec.groovy @@ -22,9 +22,9 @@ import io.micronaut.context.env.PropertySource import io.micronaut.context.exceptions.BeanInstantiationException import spock.lang.Specification -import javax.validation.Validation -import javax.validation.constraints.NotBlank -import javax.validation.constraints.NotNull +import jakarta.validation.Validation +import jakarta.validation.constraints.NotBlank +import jakarta.validation.constraints.NotNull /** * Created by graemerocher on 15/06/2017. diff --git a/inject-groovy/src/test/groovy/io/micronaut/inject/executable/ExecutableBeanSpec.groovy b/inject-groovy/src/test/groovy/io/micronaut/inject/executable/ExecutableBeanSpec.groovy index 48a1354b324..dbaf5e78138 100644 --- a/inject-groovy/src/test/groovy/io/micronaut/inject/executable/ExecutableBeanSpec.groovy +++ b/inject-groovy/src/test/groovy/io/micronaut/inject/executable/ExecutableBeanSpec.groovy @@ -200,7 +200,7 @@ package test; import io.micronaut.inject.annotation.*; import io.micronaut.context.annotation.*; -import javax.validation.Valid; +import jakarta.validation.Valid; import java.util.List; import io.micronaut.inject.executable.Book; diff --git a/inject-groovy/src/test/groovy/io/micronaut/inject/generics/GenericTypeArgumentsSpec.groovy b/inject-groovy/src/test/groovy/io/micronaut/inject/generics/GenericTypeArgumentsSpec.groovy index 76c50cb3066..45cc262494a 100644 --- a/inject-groovy/src/test/groovy/io/micronaut/inject/generics/GenericTypeArgumentsSpec.groovy +++ b/inject-groovy/src/test/groovy/io/micronaut/inject/generics/GenericTypeArgumentsSpec.groovy @@ -22,7 +22,7 @@ import io.micronaut.inject.ExecutableMethod import io.micronaut.inject.writer.BeanDefinitionWriter import spock.lang.Unroll -import javax.validation.ConstraintViolationException +import jakarta.validation.ConstraintViolationException import java.util.function.Function import java.util.function.Supplier @@ -35,7 +35,7 @@ package exceptionhandler; import io.micronaut.inject.annotation.*; import io.micronaut.context.annotation.*; -import javax.validation.ConstraintViolationException; +import jakarta.validation.ConstraintViolationException; @Context class Test implements ExceptionHandler> { diff --git a/inject-groovy/src/test/groovy/io/micronaut/inject/visitor/BeanIntrospectionSpec.groovy b/inject-groovy/src/test/groovy/io/micronaut/inject/visitor/BeanIntrospectionSpec.groovy index 8812d57ced6..230554beecf 100644 --- a/inject-groovy/src/test/groovy/io/micronaut/inject/visitor/BeanIntrospectionSpec.groovy +++ b/inject-groovy/src/test/groovy/io/micronaut/inject/visitor/BeanIntrospectionSpec.groovy @@ -18,7 +18,7 @@ import io.micronaut.inject.visitor.introspections.Person import spock.lang.Issue import spock.util.environment.RestoreSystemProperties -import javax.validation.constraints.Size +import jakarta.validation.constraints.Size @RestoreSystemProperties class BeanIntrospectionSpec extends AbstractBeanDefinitionSpec { @@ -532,7 +532,7 @@ interface Test extends io.micronaut.core.naming.Named { package test; import io.micronaut.core.annotation.*; -import javax.validation.constraints.*; +import jakarta.validation.constraints.*; import java.util.*; import com.fasterxml.jackson.annotation.*; @@ -678,7 +678,7 @@ class Test { package test; import io.micronaut.core.annotation.*; -import javax.validation.constraints.*; +import jakarta.validation.constraints.*; import java.util.*; @Introspected @@ -779,7 +779,7 @@ class Test { package test; import io.micronaut.core.annotation.*; -import javax.validation.constraints.*; +import jakarta.validation.constraints.*; import java.util.*; @Introspected @@ -872,7 +872,7 @@ class Test { package test; import io.micronaut.core.annotation.*; -import javax.validation.constraints.*; +import jakarta.validation.constraints.*; import javax.persistence.*; import java.util.*; @@ -955,7 +955,7 @@ class Test { package test; import io.micronaut.core.annotation.*; -import javax.validation.constraints.*; +import jakarta.validation.constraints.*; import java.util.*; import io.micronaut.core.convert.TypeConverter; @@ -1123,7 +1123,7 @@ class ParentBean { package test import io.micronaut.core.annotation.* -import javax.validation.constraints.* +import jakarta.validation.constraints.* import io.micronaut.core.convert.TypeConverter @Introspected @@ -1631,8 +1631,8 @@ package test; import io.micronaut.context.annotation.ConfigurationProperties; -import javax.validation.constraints.NotNull; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.NotBlank; import java.net.URL; @ConfigurationProperties("foo.bar") @@ -1672,7 +1672,7 @@ package test import io.micronaut.context.annotation.* import io.micronaut.core.annotation.* -import javax.validation.constraints.* +import jakarta.validation.constraints.* @ConfigurationProperties("foo.bar") @AccessorsStyle(readPrefixes = "read") @@ -1713,8 +1713,8 @@ package test import io.micronaut.context.annotation.ConfigurationProperties -import javax.validation.constraints.NotNull -import javax.validation.constraints.NotBlank +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.NotBlank import java.net.URL @ConfigurationProperties("foo.bar") @@ -1739,8 +1739,8 @@ package test import io.micronaut.context.annotation.ConfigurationProperties -import javax.validation.constraints.NotNull -import javax.validation.constraints.NotBlank +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.NotBlank import java.net.URL @ConfigurationProperties("foo.bar") @@ -1772,7 +1772,7 @@ class MyConfig { private int serverPort @ConfigurationInject - MyConfig(@javax.validation.constraints.NotBlank String host, int serverPort) { + MyConfig(@jakarta.validation.constraints.NotBlank String host, int serverPort) { this.host = host this.serverPort = serverPort } @@ -1810,7 +1810,7 @@ class MyConfig { private int serverPort @ConfigurationInject - MyConfig(@javax.validation.constraints.NotBlank String host, int serverPort) { + MyConfig(@jakarta.validation.constraints.NotBlank String host, int serverPort) { this.host = host this.serverPort = serverPort } @@ -1836,8 +1836,8 @@ package test import io.micronaut.context.annotation.ConfigurationProperties -import javax.validation.constraints.NotNull -import javax.validation.constraints.NotBlank +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.NotBlank import java.net.URL @ConfigurationProperties("foo.bar") @@ -1868,8 +1868,8 @@ package test import io.micronaut.context.annotation.ConfigurationProperties import io.micronaut.core.annotation.AccessorsStyle -import javax.validation.constraints.NotNull -import javax.validation.constraints.NotBlank +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.NotBlank import java.net.URL @ConfigurationProperties("foo.bar") diff --git a/inject-groovy/src/test/groovy/io/micronaut/inject/visitor/ClassElementSpec.groovy b/inject-groovy/src/test/groovy/io/micronaut/inject/visitor/ClassElementSpec.groovy index 995f8a0a7c3..371466f00ac 100644 --- a/inject-groovy/src/test/groovy/io/micronaut/inject/visitor/ClassElementSpec.groovy +++ b/inject-groovy/src/test/groovy/io/micronaut/inject/visitor/ClassElementSpec.groovy @@ -87,7 +87,7 @@ import io.micronaut.core.annotation.Introspected import io.micronaut.http.annotation.Controller import io.micronaut.http.annotation.Get -import javax.validation.constraints.NotNull +import jakarta.validation.constraints.NotNull @Controller class TestController { @@ -1047,7 +1047,7 @@ class Pet { ClassElement ce = buildClassElement(''' package test; import io.micronaut.core.annotation.*; -import javax.validation.constraints.*; +import jakarta.validation.constraints.*; import java.util.List; class Test { @@ -1061,28 +1061,28 @@ class Test { fieldType.getAnnotationMetadata().getAnnotationNames().size() == 0 assertListGenericArgument(fieldType, { ClassElement listArg1 -> - assert listArg1.getAnnotationMetadata().getAnnotationNames().asList() == ['javax.validation.constraints.Size$List'] + assert listArg1.getAnnotationMetadata().getAnnotationNames().asList() == ['jakarta.validation.constraints.Size$List'] assertListGenericArgument(listArg1, { ClassElement listArg2 -> - assert listArg2.getAnnotationMetadata().getAnnotationNames().asList() == ['javax.validation.constraints.NotEmpty$List'] + assert listArg2.getAnnotationMetadata().getAnnotationNames().asList() == ['jakarta.validation.constraints.NotEmpty$List'] assertListGenericArgument(listArg2, { ClassElement listArg3 -> - assert listArg3.getAnnotationMetadata().getAnnotationNames().asList() == ['javax.validation.constraints.NotNull$List'] + assert listArg3.getAnnotationMetadata().getAnnotationNames().asList() == ['jakarta.validation.constraints.NotNull$List'] }) }) }) def level1 = fieldType.getTypeArguments()["E"] - level1.getAnnotationMetadata().getAnnotationNames().asList() == ['javax.validation.constraints.Size$List'] + level1.getAnnotationMetadata().getAnnotationNames().asList() == ['jakarta.validation.constraints.Size$List'] def level2 = level1.getTypeArguments()["E"] - level2.getAnnotationMetadata().getAnnotationNames().asList() == ['javax.validation.constraints.NotEmpty$List'] + level2.getAnnotationMetadata().getAnnotationNames().asList() == ['jakarta.validation.constraints.NotEmpty$List'] def level3 = level2.getTypeArguments()["E"] - level3.getAnnotationMetadata().getAnnotationNames().asList() == ['javax.validation.constraints.NotNull$List'] + level3.getAnnotationMetadata().getAnnotationNames().asList() == ['jakarta.validation.constraints.NotNull$List'] } void "test annotation metadata present on deep type parameters for method"() { ClassElement ce = buildClassElement(''' package test; import io.micronaut.core.annotation.*; -import javax.validation.constraints.*; +import jakarta.validation.constraints.*; import java.util.List; class Test { @@ -1098,28 +1098,28 @@ class Test { theType.getAnnotationMetadata().getAnnotationNames().size() == 0 assertListGenericArgument(theType, { ClassElement listArg1 -> - assert listArg1.getAnnotationMetadata().getAnnotationNames().asList() == ['javax.validation.constraints.Size$List'] + assert listArg1.getAnnotationMetadata().getAnnotationNames().asList() == ['jakarta.validation.constraints.Size$List'] assertListGenericArgument(listArg1, { ClassElement listArg2 -> - assert listArg2.getAnnotationMetadata().getAnnotationNames().asList() == ['javax.validation.constraints.NotEmpty$List'] + assert listArg2.getAnnotationMetadata().getAnnotationNames().asList() == ['jakarta.validation.constraints.NotEmpty$List'] assertListGenericArgument(listArg2, { ClassElement listArg3 -> - assert listArg3.getAnnotationMetadata().getAnnotationNames().asList() == ['javax.validation.constraints.NotNull$List'] + assert listArg3.getAnnotationMetadata().getAnnotationNames().asList() == ['jakarta.validation.constraints.NotNull$List'] }) }) }) def level1 = theType.getTypeArguments()["E"] - level1.getAnnotationMetadata().getAnnotationNames().asList() == ['javax.validation.constraints.Size$List'] + level1.getAnnotationMetadata().getAnnotationNames().asList() == ['jakarta.validation.constraints.Size$List'] def level2 = level1.getTypeArguments()["E"] - level2.getAnnotationMetadata().getAnnotationNames().asList() == ['javax.validation.constraints.NotEmpty$List'] + level2.getAnnotationMetadata().getAnnotationNames().asList() == ['jakarta.validation.constraints.NotEmpty$List'] def level3 = level2.getTypeArguments()["E"] - level3.getAnnotationMetadata().getAnnotationNames().asList() == ['javax.validation.constraints.NotNull$List'] + level3.getAnnotationMetadata().getAnnotationNames().asList() == ['jakarta.validation.constraints.NotNull$List'] } void "test annotation metadata present on deep type parameters for method 2"() { ClassElement ce = buildClassElement(''' package test; import io.micronaut.core.annotation.*; -import javax.validation.constraints.*; +import jakarta.validation.constraints.*; import java.util.List; class Test { diff --git a/inject-groovy/src/test/groovy/io/micronaut/inject/visitor/ElementAnnotateSpec.groovy b/inject-groovy/src/test/groovy/io/micronaut/inject/visitor/ElementAnnotateSpec.groovy index 5a59ac8697e..db23e3c091f 100644 --- a/inject-groovy/src/test/groovy/io/micronaut/inject/visitor/ElementAnnotateSpec.groovy +++ b/inject-groovy/src/test/groovy/io/micronaut/inject/visitor/ElementAnnotateSpec.groovy @@ -29,7 +29,7 @@ package elemann1; import io.micronaut.context.annotation.*; import java.net.*; -import javax.validation.constraints.*; +import jakarta.validation.constraints.*; import io.micronaut.aop.introduction.Stub; @Stub diff --git a/inject-groovy/src/test/groovy/io/micronaut/inject/visitor/InterfaceWithGenerics.java b/inject-groovy/src/test/groovy/io/micronaut/inject/visitor/InterfaceWithGenerics.java index 69b543244b0..54888abb6e6 100644 --- a/inject-groovy/src/test/groovy/io/micronaut/inject/visitor/InterfaceWithGenerics.java +++ b/inject-groovy/src/test/groovy/io/micronaut/inject/visitor/InterfaceWithGenerics.java @@ -16,8 +16,8 @@ package io.micronaut.inject.visitor; import io.micronaut.core.annotation.NonNull; -import javax.validation.Valid; -import javax.validation.constraints.NotNull; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotNull; import java.util.Optional; public interface InterfaceWithGenerics { diff --git a/inject-groovy/src/test/groovy/io/micronaut/validation/ValidatedParseSpec.groovy b/inject-groovy/src/test/groovy/io/micronaut/validation/ValidatedParseSpec.groovy index 19abe2ea1c7..2c2121b470f 100644 --- a/inject-groovy/src/test/groovy/io/micronaut/validation/ValidatedParseSpec.groovy +++ b/inject-groovy/src/test/groovy/io/micronaut/validation/ValidatedParseSpec.groovy @@ -19,13 +19,13 @@ package validateparse1; class Test { @io.micronaut.context.annotation.Executable - public void setName(@javax.validation.constraints.NotBlank String name) { - + public void setName(@jakarta.validation.constraints.NotBlank String name) { + } - + @io.micronaut.context.annotation.Executable - public void setName2(@javax.validation.Valid String name) { - + public void setName2(@jakarta.validation.Valid String name) { + } } ''') @@ -42,7 +42,7 @@ class Test { package validateparse2; import io.micronaut.core.annotation.Introspected -import javax.validation.Constraint +import jakarta.validation.Constraint import java.lang.annotation.ElementType import java.lang.annotation.Retention import java.lang.annotation.RetentionPolicy @@ -77,7 +77,7 @@ package validateparse3 import io.micronaut.http.annotation.Get import io.micronaut.http.client.annotation.Client -import javax.validation.constraints.PastOrPresent +import jakarta.validation.constraints.PastOrPresent import java.time.LocalDate @Client("https://exchangeratesapi.io") diff --git a/inject-java-test/src/test/groovy/io/micronaut/inject/visitor/ElementAnnotateSpec.groovy b/inject-java-test/src/test/groovy/io/micronaut/inject/visitor/ElementAnnotateSpec.groovy index 66858cd1ddf..09a32ebf2fc 100644 --- a/inject-java-test/src/test/groovy/io/micronaut/inject/visitor/ElementAnnotateSpec.groovy +++ b/inject-java-test/src/test/groovy/io/micronaut/inject/visitor/ElementAnnotateSpec.groovy @@ -28,7 +28,7 @@ package test; import io.micronaut.inject.visitor.Stub; import io.micronaut.context.annotation.*; import java.net.*; -import javax.validation.constraints.*; +import jakarta.validation.constraints.*; @Stub interface MyInterface{ diff --git a/inject-java-test/src/test/groovy/io/micronaut/inject/visitor/InheritanceVisitorSpec.groovy b/inject-java-test/src/test/groovy/io/micronaut/inject/visitor/InheritanceVisitorSpec.groovy index c5a5f512e6a..f89d7ea761c 100644 --- a/inject-java-test/src/test/groovy/io/micronaut/inject/visitor/InheritanceVisitorSpec.groovy +++ b/inject-java-test/src/test/groovy/io/micronaut/inject/visitor/InheritanceVisitorSpec.groovy @@ -22,7 +22,7 @@ class InheritanceVisitorSpec extends AbstractTypeElementSpec { package test; import io.micronaut.core.annotation.*; -import javax.validation.constraints.*; +import jakarta.validation.constraints.*; import java.util.*; @Introspected diff --git a/inject-java-test/src/test/groovy/io/micronaut/inject/visitor/beans/AnnotatedIntrospectedSpec.groovy b/inject-java-test/src/test/groovy/io/micronaut/inject/visitor/beans/AnnotatedIntrospectedSpec.groovy index 575940a5a8f..1f9c1495a16 100644 --- a/inject-java-test/src/test/groovy/io/micronaut/inject/visitor/beans/AnnotatedIntrospectedSpec.groovy +++ b/inject-java-test/src/test/groovy/io/micronaut/inject/visitor/beans/AnnotatedIntrospectedSpec.groovy @@ -17,7 +17,7 @@ class AnnotatedIntrospectedSpec extends AbstractTypeElementSpec { package test; import io.micronaut.core.annotation.*; -import javax.validation.constraints.*; +import jakarta.validation.constraints.*; import java.util.*; @io.micronaut.inject.visitor.beans.MakeIntrospected diff --git a/inject-java-test/src/test/groovy/io/micronaut/inject/visitor/beans/BeanIntrospectionSpec.groovy b/inject-java-test/src/test/groovy/io/micronaut/inject/visitor/beans/BeanIntrospectionSpec.groovy index d7a0a2adab6..3f5f4022f45 100644 --- a/inject-java-test/src/test/groovy/io/micronaut/inject/visitor/beans/BeanIntrospectionSpec.groovy +++ b/inject-java-test/src/test/groovy/io/micronaut/inject/visitor/beans/BeanIntrospectionSpec.groovy @@ -39,11 +39,11 @@ import javax.persistence.Column import javax.persistence.Entity import javax.persistence.Id import javax.persistence.Version -import javax.validation.Constraint -import javax.validation.constraints.DecimalMin -import javax.validation.constraints.Min -import javax.validation.constraints.NotBlank -import javax.validation.constraints.Size +import jakarta.validation.Constraint +import jakarta.validation.constraints.DecimalMin +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotBlank +import jakarta.validation.constraints.Size import java.lang.reflect.Field class BeanIntrospectionSpec extends AbstractTypeElementSpec { @@ -859,7 +859,7 @@ package test; import io.micronaut.core.annotation.Creator; import java.util.List; -import javax.validation.constraints.Min; +import jakarta.validation.constraints.Min; @io.micronaut.core.annotation.Introspected public record Foo(int x, int y){ @@ -888,7 +888,7 @@ package test; import io.micronaut.core.annotation.Creator; import java.util.List; -import javax.validation.constraints.Min; +import jakarta.validation.constraints.Min; @io.micronaut.core.annotation.Introspected public record Foo(int x, int y){ @@ -921,7 +921,7 @@ package json.test; import io.micronaut.core.annotation.Creator; import java.util.List; -import javax.validation.constraints.Min; +import jakarta.validation.constraints.Min; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; @@ -947,7 +947,7 @@ package test; import io.micronaut.core.annotation.Creator; import java.util.List; -import javax.validation.constraints.Min; +import jakarta.validation.constraints.Min; @io.micronaut.core.annotation.Introspected public record Foo(int x, int y){ @@ -976,7 +976,7 @@ package test; import io.micronaut.core.annotation.Creator; import java.util.List; -import javax.validation.constraints.Min; +import jakarta.validation.constraints.Min; @io.micronaut.core.annotation.Introspected public record Foo(List<@Min(10) Long> value){ @@ -1001,7 +1001,7 @@ package test; import io.micronaut.core.annotation.Creator; import java.util.List; -import javax.validation.constraints.Min; +import jakarta.validation.constraints.Min; import java.lang.annotation.*; import static java.lang.annotation.RetentionPolicy.RUNTIME; import static java.lang.annotation.ElementType.*; @@ -1045,7 +1045,7 @@ package test; import io.micronaut.core.annotation.Creator; @io.micronaut.core.annotation.Introspected -public record Foo(@javax.validation.constraints.NotBlank String name, int age){ +public record Foo(@jakarta.validation.constraints.NotBlank String name, int age){ } ''') when: @@ -1082,7 +1082,7 @@ public record Foo(@javax.validation.constraints.NotBlank String name, int age){ package test; import io.micronaut.core.annotation.*; -import javax.validation.constraints.*; +import jakarta.validation.constraints.*; import java.util.*; import io.micronaut.inject.visitor.beans.*; @@ -1194,7 +1194,7 @@ class MyImpl implements MyInterface { package test; import io.micronaut.core.annotation.*; -import javax.validation.constraints.*; +import jakarta.validation.constraints.*; import java.util.*; import io.micronaut.inject.visitor.beans.*; @@ -1429,8 +1429,8 @@ package test; import io.micronaut.context.annotation.ConfigurationProperties; -import javax.validation.constraints.NotNull; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.NotBlank; import java.net.URL; @ConfigurationProperties("foo.bar") @@ -1454,8 +1454,8 @@ package test; import io.micronaut.context.annotation.ConfigurationProperties;import io.micronaut.core.annotation.AccessorsStyle; -import javax.validation.constraints.NotNull; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.NotBlank; import java.net.URL; @ConfigurationProperties("foo.bar") @@ -1527,8 +1527,8 @@ package test; import io.micronaut.context.annotation.ConfigurationProperties; -import javax.validation.constraints.NotNull; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.NotBlank; import java.net.URL; @ConfigurationProperties("foo.bar") @@ -1571,8 +1571,8 @@ package test; import io.micronaut.context.annotation.ConfigurationProperties; import io.micronaut.core.annotation.AccessorsStyle; -import javax.validation.constraints.NotNull; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.NotBlank; import java.net.URL; @ConfigurationProperties("foo.bar") @@ -1613,8 +1613,8 @@ package test; import io.micronaut.context.annotation.ConfigurationProperties; -import javax.validation.constraints.NotNull; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.NotBlank; import java.net.URL; @ConfigurationProperties("foo.bar") @@ -1655,8 +1655,8 @@ package test; import io.micronaut.context.annotation.ConfigurationProperties; -import javax.validation.constraints.NotNull; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.NotBlank; import java.net.URL; @ConfigurationProperties("foo.bar") @@ -1689,8 +1689,8 @@ package test; import io.micronaut.context.annotation.ConfigurationProperties; -import javax.validation.constraints.NotNull; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.NotBlank; import java.net.URL; @ConfigurationProperties("foo.bar") @@ -1722,8 +1722,8 @@ package test; import io.micronaut.context.annotation.ConfigurationProperties; -import javax.validation.constraints.NotNull; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.NotBlank; import java.net.URL; @ConfigurationProperties("foo.bar") @@ -1772,8 +1772,8 @@ package test; import io.micronaut.context.annotation.ConfigurationProperties; import io.micronaut.core.annotation.AccessorsStyle; -import javax.validation.constraints.NotNull; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.NotBlank; import java.net.URL; @ConfigurationProperties("foo.bar") @@ -1822,8 +1822,8 @@ package test; import io.micronaut.context.annotation.ConfigurationProperties; -import javax.validation.constraints.NotNull; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.NotBlank; import java.net.URL; @ConfigurationProperties("foo.bar") @@ -1866,8 +1866,8 @@ package test; import io.micronaut.context.annotation.ConfigurationProperties;import io.micronaut.core.annotation.AccessorsStyle; -import javax.validation.constraints.NotNull; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.NotBlank; import java.net.URL; @ConfigurationProperties("foo.bar") @@ -1919,7 +1919,7 @@ class MyConfig { private int serverPort; @ConfigurationInject - MyConfig(@javax.validation.constraints.NotBlank String host, int serverPort) { + MyConfig(@jakarta.validation.constraints.NotBlank String host, int serverPort) { this.host = host; this.serverPort = serverPort; } @@ -1952,7 +1952,7 @@ class MyConfig { private int serverPort; @ConfigurationInject - MyConfig(@javax.validation.constraints.NotBlank String host, int serverPort) { + MyConfig(@jakarta.validation.constraints.NotBlank String host, int serverPort) { this.host = host; this.serverPort = serverPort; } @@ -2007,7 +2007,7 @@ public class Test { BeanIntrospection introspection = buildBeanIntrospection('test.Test','''\ package test; import io.micronaut.core.annotation.*; -import javax.validation.constraints.*; +import jakarta.validation.constraints.*; import java.util.List; import java.util.Set; @@ -2033,11 +2033,11 @@ public class Test { property.getAnnotationMetadata().getAnnotationNames().size() == 0 param1.getAnnotationMetadata().getAnnotationNames().size() == 1 - param1.getAnnotationMetadata().getAnnotationNames().asList() == ['javax.validation.constraints.Size$List'] + param1.getAnnotationMetadata().getAnnotationNames().asList() == ['jakarta.validation.constraints.Size$List'] param2.getAnnotationMetadata().getAnnotationNames().size() == 1 - param2.getAnnotationMetadata().getAnnotationNames().asList() == ['javax.validation.constraints.NotEmpty$List'] + param2.getAnnotationMetadata().getAnnotationNames().asList() == ['jakarta.validation.constraints.NotEmpty$List'] param3.getAnnotationMetadata().getAnnotationNames().size() == 1 - param3.getAnnotationMetadata().getAnnotationNames().asList() == ['javax.validation.constraints.NotNull$List'] + param3.getAnnotationMetadata().getAnnotationNames().asList() == ['jakarta.validation.constraints.NotNull$List'] } @Issue('https://github.com/micronaut-projects/micronaut-core/issues/2083') @@ -2107,7 +2107,7 @@ class Test extends RecursiveGenerics { def context = buildContext('test.Address', ''' package test; -import javax.validation.constraints.*; +import jakarta.validation.constraints.*; @io.micronaut.core.annotation.Introspected @@ -2275,7 +2275,7 @@ class Book { package test; import io.micronaut.core.annotation.*; -import javax.validation.constraints.*; +import jakarta.validation.constraints.*; import java.util.*; import com.fasterxml.jackson.annotation.*; @@ -2346,7 +2346,7 @@ class Test { package test; import io.micronaut.core.annotation.*; -import javax.validation.constraints.*; +import jakarta.validation.constraints.*; import java.util.*; @Introspected @@ -2450,7 +2450,7 @@ class Test { package test; import io.micronaut.core.annotation.*; -import javax.validation.constraints.*; +import jakarta.validation.constraints.*; import java.util.*; @Introspected @@ -2500,7 +2500,7 @@ class Test { package test; import io.micronaut.core.annotation.*; -import javax.validation.constraints.*; +import jakarta.validation.constraints.*; import javax.persistence.*; import java.util.*; @@ -2616,7 +2616,7 @@ class Test { package test; import io.micronaut.core.annotation.*; -import javax.validation.constraints.*; +import jakarta.validation.constraints.*; import javax.persistence.*; import java.util.*; @@ -2688,7 +2688,7 @@ class Test { package test; import io.micronaut.core.annotation.*; -import javax.validation.constraints.*; +import jakarta.validation.constraints.*; import java.util.*; import io.micronaut.inject.visitor.beans.*; @@ -2722,7 +2722,7 @@ class Test {} package test; import io.micronaut.core.annotation.*; -import javax.validation.constraints.*; +import jakarta.validation.constraints.*; import java.util.*; import io.micronaut.inject.visitor.beans.*; @@ -2746,7 +2746,7 @@ class Test {} package test; import io.micronaut.core.annotation.*; -import javax.validation.constraints.*; +import jakarta.validation.constraints.*; import java.util.*; import io.micronaut.inject.visitor.beans.*; @@ -2771,7 +2771,7 @@ class Test {} package test; import io.micronaut.core.annotation.*; -import javax.validation.constraints.*; +import jakarta.validation.constraints.*; import java.util.*; import io.micronaut.core.convert.TypeConverter; @@ -2961,7 +2961,7 @@ class ParentBean { package test; import io.micronaut.core.annotation.*; -import javax.validation.constraints.*; +import jakarta.validation.constraints.*; import java.util.*; import io.micronaut.core.convert.TypeConverter; @@ -3150,7 +3150,7 @@ class ParentBean { package test; import io.micronaut.core.annotation.*; -import javax.validation.constraints.*; +import jakarta.validation.constraints.*; import java.util.*; import com.fasterxml.jackson.annotation.*; @@ -3822,7 +3822,7 @@ class Test { package test; import io.micronaut.core.annotation.Introspected; -import javax.validation.constraints.NotNull; +import jakarta.validation.constraints.NotNull; interface IEmail { String getEmail(); @@ -3853,7 +3853,7 @@ class Test extends SuperClass implements IEmail { package test; import io.micronaut.core.annotation.*; -import javax.validation.constraints.NotNull; +import jakarta.validation.constraints.NotNull; interface IEmail { String readEmail(); @@ -4503,7 +4503,7 @@ package test; import io.micronaut.core.annotation.Creator; import java.util.List; -import javax.validation.constraints.Min; +import jakarta.validation.constraints.Min; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; @@ -4519,8 +4519,8 @@ public record Foo(String name, String isSurname, boolean contains, Boolean purge BeanIntrospection beanIntrospection = buildBeanIntrospection('test.Book', ''' package test; -import javax.validation.Valid; -import javax.validation.constraints.Size; +import jakarta.validation.Valid; +import jakarta.validation.constraints.Size; import java.util.List; import io.micronaut.core.annotation.Introspected; @@ -4558,7 +4558,7 @@ class Book { then: property.name == "authors" - property.asArgument().getTypeParameters()[0].annotationMetadata.hasStereotype("javax.validation.Valid") + property.asArgument().getTypeParameters()[0].annotationMetadata.hasStereotype("jakarta.validation.Valid") } void "test type_use annotations"() { @@ -4653,7 +4653,7 @@ class Holder { BeanIntrospection introspection = buildBeanIntrospection('test.OptionalDoubleHolder', ''' package test; import io.micronaut.core.annotation.Introspected; -import javax.validation.constraints.DecimalMin; +import jakarta.validation.constraints.DecimalMin; import java.util.List; import java.util.Collections; import java.util.OptionalDouble; @@ -4678,7 +4678,7 @@ class OptionalDoubleHolder { BeanIntrospection introspection = buildBeanIntrospection('test.OptionalStringHolder', ''' package test; import io.micronaut.core.annotation.Introspected; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotBlank; import java.util.List; import java.util.Collections; import java.util.Optional; diff --git a/inject-java-test/src/test/groovy/io/micronaut/inject/visitor/beans/OptionalValueExtractorTest.java b/inject-java-test/src/test/groovy/io/micronaut/inject/visitor/beans/OptionalValueExtractorTest.java index 7c1cbdcbbb6..e74c821ee42 100644 --- a/inject-java-test/src/test/groovy/io/micronaut/inject/visitor/beans/OptionalValueExtractorTest.java +++ b/inject-java-test/src/test/groovy/io/micronaut/inject/visitor/beans/OptionalValueExtractorTest.java @@ -7,10 +7,10 @@ import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; -import javax.validation.constraints.DecimalMin; -import javax.validation.constraints.Min; -import javax.validation.constraints.NotBlank; -import javax.validation.constraints.NotNull; +import jakarta.validation.constraints.DecimalMin; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; import java.util.Optional; import java.util.OptionalDouble; import java.util.OptionalInt; diff --git a/inject-java-test/src/test/groovy/io/micronaut/inject/visitor/beans/TestEntity.java b/inject-java-test/src/test/groovy/io/micronaut/inject/visitor/beans/TestEntity.java index b940118edc8..dc5c37c8bfd 100644 --- a/inject-java-test/src/test/groovy/io/micronaut/inject/visitor/beans/TestEntity.java +++ b/inject-java-test/src/test/groovy/io/micronaut/inject/visitor/beans/TestEntity.java @@ -19,7 +19,7 @@ import javax.persistence.GeneratedValue; import javax.persistence.Id; import javax.persistence.Version; -import javax.validation.constraints.Size; +import jakarta.validation.constraints.Size; @Entity public class TestEntity { diff --git a/inject-java-test/src/test/groovy/io/micronaut/inject/visitor/beans/reflection/OptionalDoubleHolder.java b/inject-java-test/src/test/groovy/io/micronaut/inject/visitor/beans/reflection/OptionalDoubleHolder.java index 97a9da6f552..bcf85c40995 100644 --- a/inject-java-test/src/test/groovy/io/micronaut/inject/visitor/beans/reflection/OptionalDoubleHolder.java +++ b/inject-java-test/src/test/groovy/io/micronaut/inject/visitor/beans/reflection/OptionalDoubleHolder.java @@ -2,8 +2,8 @@ import io.micronaut.core.annotation.Introspected; -import javax.validation.constraints.DecimalMin; -import javax.validation.constraints.NotNull; +import jakarta.validation.constraints.DecimalMin; +import jakarta.validation.constraints.NotNull; import java.util.OptionalDouble; @Introspected(accessKind = Introspected.AccessKind.FIELD, visibility = Introspected.Visibility.ANY) diff --git a/inject-java-test/src/test/groovy/io/micronaut/inject/visitor/beans/reflection/OptionalHolder.java b/inject-java-test/src/test/groovy/io/micronaut/inject/visitor/beans/reflection/OptionalHolder.java index 4b871ad0b21..d6809b523ea 100644 --- a/inject-java-test/src/test/groovy/io/micronaut/inject/visitor/beans/reflection/OptionalHolder.java +++ b/inject-java-test/src/test/groovy/io/micronaut/inject/visitor/beans/reflection/OptionalHolder.java @@ -2,8 +2,8 @@ import io.micronaut.core.annotation.Introspected; -import javax.validation.constraints.NotBlank; -import javax.validation.constraints.NotNull; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; import java.util.Optional; @Introspected(accessKind = Introspected.AccessKind.FIELD, visibility = Introspected.Visibility.ANY) diff --git a/inject-java-test/src/test/groovy/io/micronaut/inject/visitor/beans/reflection/OptionalIntHolder.java b/inject-java-test/src/test/groovy/io/micronaut/inject/visitor/beans/reflection/OptionalIntHolder.java index 9be2987d951..fe7f704b80b 100644 --- a/inject-java-test/src/test/groovy/io/micronaut/inject/visitor/beans/reflection/OptionalIntHolder.java +++ b/inject-java-test/src/test/groovy/io/micronaut/inject/visitor/beans/reflection/OptionalIntHolder.java @@ -2,8 +2,8 @@ import io.micronaut.core.annotation.Introspected; -import javax.validation.constraints.Min; -import javax.validation.constraints.NotNull; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotNull; import java.util.OptionalInt; @Introspected(accessKind = Introspected.AccessKind.FIELD, visibility = Introspected.Visibility.ANY) diff --git a/inject-java-test/src/test/groovy/io/micronaut/inject/visitor/beans/reflection/OptionalLongHolder.java b/inject-java-test/src/test/groovy/io/micronaut/inject/visitor/beans/reflection/OptionalLongHolder.java index b3eadeea835..52295797bd2 100644 --- a/inject-java-test/src/test/groovy/io/micronaut/inject/visitor/beans/reflection/OptionalLongHolder.java +++ b/inject-java-test/src/test/groovy/io/micronaut/inject/visitor/beans/reflection/OptionalLongHolder.java @@ -2,8 +2,8 @@ import io.micronaut.core.annotation.Introspected; -import javax.validation.constraints.Min; -import javax.validation.constraints.NotNull; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotNull; import java.util.OptionalLong; @Introspected(accessKind = Introspected.AccessKind.FIELD, visibility = Introspected.Visibility.ANY) diff --git a/inject-java-test/src/test/groovy/io/micronaut/inject/visitor/beans/reflection/PrivateAccessTest.java b/inject-java-test/src/test/groovy/io/micronaut/inject/visitor/beans/reflection/PrivateAccessTest.java index ba121dd85f9..9ea9ac3d110 100644 --- a/inject-java-test/src/test/groovy/io/micronaut/inject/visitor/beans/reflection/PrivateAccessTest.java +++ b/inject-java-test/src/test/groovy/io/micronaut/inject/visitor/beans/reflection/PrivateAccessTest.java @@ -6,9 +6,9 @@ import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; -import javax.validation.constraints.DecimalMin; -import javax.validation.constraints.Min; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.DecimalMin; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotBlank; import java.util.Optional; import java.util.OptionalDouble; import java.util.OptionalInt; diff --git a/inject-java/src/main/java/io/micronaut/annotation/processing/JavaAnnotationMetadataBuilder.java b/inject-java/src/main/java/io/micronaut/annotation/processing/JavaAnnotationMetadataBuilder.java index 9138dbec95c..5be680c7618 100644 --- a/inject-java/src/main/java/io/micronaut/annotation/processing/JavaAnnotationMetadataBuilder.java +++ b/inject-java/src/main/java/io/micronaut/annotation/processing/JavaAnnotationMetadataBuilder.java @@ -391,7 +391,7 @@ protected boolean isValidationRequired(Element member) { private boolean isValidationRequired(List annotationMirrors) { for (AnnotationMirror annotationMirror : annotationMirrors) { final String annotationName = getAnnotationTypeName(annotationMirror); - if (annotationName.startsWith("javax.validation") || annotationName.startsWith("jakarta.validation")) { + if (annotationName.startsWith("jakarta.validation")) { return true; } else if (!AnnotationUtil.INTERNAL_ANNOTATION_NAMES.contains(annotationName)) { final Element element = getAnnotationMirror(annotationName).orElse(null); diff --git a/inject-java/src/test/groovy/io/micronaut/annotation/AnnotateTypeArgSpec.groovy b/inject-java/src/test/groovy/io/micronaut/annotation/AnnotateTypeArgSpec.groovy index 7115bd2b22c..3e13e5e23a6 100644 --- a/inject-java/src/test/groovy/io/micronaut/annotation/AnnotateTypeArgSpec.groovy +++ b/inject-java/src/test/groovy/io/micronaut/annotation/AnnotateTypeArgSpec.groovy @@ -32,7 +32,7 @@ class AnnotateTypeArg0 { def introspection = buildBeanIntrospection('addann.AnnotateTypeArg1', ''' package addann; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotBlank; import java.util.Map; class AnnotateTypeArg1 { @@ -52,7 +52,7 @@ class AnnotateTypeArg1 { def introspection = buildBeanIntrospection('addann.Outer1$AnnotateTypeArg2', ''' package addann; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotBlank; import java.util.Map; class Outer1 { @@ -74,7 +74,7 @@ class Outer1 { package addann; import io.micronaut.core.annotation.Introspected; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotBlank; import java.util.Map; @Introspected(classes = addann.Outer2.NotProcessedByVisitor.class, accessKind = Introspected.AccessKind.FIELD, visibility = Introspected.Visibility.ANY) @@ -98,7 +98,7 @@ class Outer2 { package addann; import io.micronaut.core.annotation.Introspected; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotBlank; import java.util.Map; @Introspected(classNames = "addann.Outer3.NotProcessedByVisitor2", accessKind = Introspected.AccessKind.FIELD, visibility = Introspected.Visibility.ANY) diff --git a/inject-java/src/test/groovy/io/micronaut/aop/compile/IntroductionAnnotationSpec.groovy b/inject-java/src/test/groovy/io/micronaut/aop/compile/IntroductionAnnotationSpec.groovy index b9ac21e1870..b2780d45c11 100644 --- a/inject-java/src/test/groovy/io/micronaut/aop/compile/IntroductionAnnotationSpec.groovy +++ b/inject-java/src/test/groovy/io/micronaut/aop/compile/IntroductionAnnotationSpec.groovy @@ -24,8 +24,8 @@ import io.micronaut.inject.BeanDefinition import io.micronaut.inject.InstantiatableBeanDefinition import io.micronaut.inject.writer.BeanDefinitionVisitor -import javax.validation.constraints.Min -import javax.validation.constraints.NotBlank +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotBlank /** * @author graemerocher * @since 1.0 @@ -120,7 +120,7 @@ package test; import io.micronaut.aop.introduction.*; import io.micronaut.context.annotation.*; import java.net.*; -import javax.validation.constraints.*; +import jakarta.validation.constraints.*; interface MyInterface{ @Executable diff --git a/inject-java/src/test/groovy/io/micronaut/aop/compile/IntroductionWithAroundSpec.groovy b/inject-java/src/test/groovy/io/micronaut/aop/compile/IntroductionWithAroundSpec.groovy index dbf535af636..e9ac9186f1d 100644 --- a/inject-java/src/test/groovy/io/micronaut/aop/compile/IntroductionWithAroundSpec.groovy +++ b/inject-java/src/test/groovy/io/micronaut/aop/compile/IntroductionWithAroundSpec.groovy @@ -16,7 +16,7 @@ package test; import io.micronaut.aop.introduction.*; import io.micronaut.context.annotation.*; import java.net.*; -import javax.validation.constraints.*; +import jakarta.validation.constraints.*; import jakarta.inject.Singleton; @Stub diff --git a/inject-java/src/test/groovy/io/micronaut/aop/compile/OriginatingElementsSpec.groovy b/inject-java/src/test/groovy/io/micronaut/aop/compile/OriginatingElementsSpec.groovy index 35bee8ace84..d02f09b9989 100644 --- a/inject-java/src/test/groovy/io/micronaut/aop/compile/OriginatingElementsSpec.groovy +++ b/inject-java/src/test/groovy/io/micronaut/aop/compile/OriginatingElementsSpec.groovy @@ -204,7 +204,7 @@ package test; import io.micronaut.aop.introduction.*; import io.micronaut.context.annotation.*; import java.net.*; -import javax.validation.constraints.*; +import jakarta.validation.constraints.*; interface MyInterface{ @Executable @@ -242,7 +242,7 @@ package test; import io.micronaut.aop.introduction.*; import io.micronaut.context.annotation.*; import java.net.*; -import javax.validation.constraints.*; +import jakarta.validation.constraints.*; interface MyInterface { @Executable @@ -280,7 +280,7 @@ package test; import io.micronaut.aop.introduction.*; import io.micronaut.context.annotation.*; import java.net.*; -import javax.validation.constraints.*; +import jakarta.validation.constraints.*; interface MyInterface { @Executable diff --git a/inject-java/src/test/groovy/io/micronaut/aop/compile/ValidatedNonBeanSpec.groovy b/inject-java/src/test/groovy/io/micronaut/aop/compile/ValidatedNonBeanSpec.groovy index 35a88fb19aa..d35422bf380 100644 --- a/inject-java/src/test/groovy/io/micronaut/aop/compile/ValidatedNonBeanSpec.groovy +++ b/inject-java/src/test/groovy/io/micronaut/aop/compile/ValidatedNonBeanSpec.groovy @@ -10,7 +10,7 @@ class ValidatedNonBeanSpec extends AbstractTypeElementSpec { BeanDefinition beanDefinition = buildBeanDefinition("test.DefaultContract", """ package test; -import javax.validation.constraints.NotNull; +import jakarta.validation.constraints.NotNull; import io.micronaut.context.annotation.*; import jakarta.inject.Singleton; diff --git a/inject-java/src/test/groovy/io/micronaut/aop/introduction/DeleteByIdCrudRepo.java b/inject-java/src/test/groovy/io/micronaut/aop/introduction/DeleteByIdCrudRepo.java index a27ad20f77a..5fd5a23e99e 100644 --- a/inject-java/src/test/groovy/io/micronaut/aop/introduction/DeleteByIdCrudRepo.java +++ b/inject-java/src/test/groovy/io/micronaut/aop/introduction/DeleteByIdCrudRepo.java @@ -17,7 +17,7 @@ import io.micronaut.core.annotation.NonNull; -import javax.validation.constraints.NotNull; +import jakarta.validation.constraints.NotNull; public interface DeleteByIdCrudRepo { diff --git a/inject-java/src/test/groovy/io/micronaut/aop/introduction/IntroductionAdviceWithNewInterfaceSpec.groovy b/inject-java/src/test/groovy/io/micronaut/aop/introduction/IntroductionAdviceWithNewInterfaceSpec.groovy index d13ae0989a2..bb23d01a5d2 100644 --- a/inject-java/src/test/groovy/io/micronaut/aop/introduction/IntroductionAdviceWithNewInterfaceSpec.groovy +++ b/inject-java/src/test/groovy/io/micronaut/aop/introduction/IntroductionAdviceWithNewInterfaceSpec.groovy @@ -35,7 +35,7 @@ class IntroductionAdviceWithNewInterfaceSpec extends AbstractTypeElementSpec { package test; import io.micronaut.aop.introduction.*; -import javax.validation.constraints.NotNull; +import jakarta.validation.constraints.NotNull; @RepoDef interface MyRepo extends DeleteByIdCrudRepo { diff --git a/inject-java/src/test/groovy/io/micronaut/aop/introduction/MyRepo2.java b/inject-java/src/test/groovy/io/micronaut/aop/introduction/MyRepo2.java index 31ab3ba1865..cdbbd9f0e0a 100644 --- a/inject-java/src/test/groovy/io/micronaut/aop/introduction/MyRepo2.java +++ b/inject-java/src/test/groovy/io/micronaut/aop/introduction/MyRepo2.java @@ -15,7 +15,7 @@ */ package io.micronaut.aop.introduction; -import javax.validation.constraints.NotNull; +import jakarta.validation.constraints.NotNull; @RepoDef public interface MyRepo2 extends DeleteByIdCrudRepo { diff --git a/inject-java/src/test/groovy/io/micronaut/aop/introduction/repeatable/IntroducedWithRepeatableAnnotationSpec.groovy b/inject-java/src/test/groovy/io/micronaut/aop/introduction/repeatable/IntroducedWithRepeatableAnnotationSpec.groovy index 3ea2911a685..2432e9e32ac 100644 --- a/inject-java/src/test/groovy/io/micronaut/aop/introduction/repeatable/IntroducedWithRepeatableAnnotationSpec.groovy +++ b/inject-java/src/test/groovy/io/micronaut/aop/introduction/repeatable/IntroducedWithRepeatableAnnotationSpec.groovy @@ -30,8 +30,8 @@ import java.lang.annotation.*; import io.micronaut.aop.Introduction; import io.micronaut.context.annotation.Type; import io.micronaut.core.annotation.NonNull; -import javax.validation.Valid; -import javax.validation.constraints.NotNull; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotNull; import io.micronaut.aop.MethodInterceptor; import io.micronaut.aop.MethodInvocationContext; import io.micronaut.core.annotation.Nullable; diff --git a/inject-java/src/test/groovy/io/micronaut/inject/annotation/AnnotationsOnGenericTypesSpec.groovy b/inject-java/src/test/groovy/io/micronaut/inject/annotation/AnnotationsOnGenericTypesSpec.groovy index d45e815ef66..a9a79dd51a1 100644 --- a/inject-java/src/test/groovy/io/micronaut/inject/annotation/AnnotationsOnGenericTypesSpec.groovy +++ b/inject-java/src/test/groovy/io/micronaut/inject/annotation/AnnotationsOnGenericTypesSpec.groovy @@ -9,8 +9,8 @@ import io.micronaut.inject.MethodInjectionPoint import spock.lang.Requires import jakarta.inject.Singleton -import javax.validation.Valid -import javax.validation.constraints.Min +import jakarta.validation.Valid +import jakarta.validation.constraints.Min class AnnotationsOnGenericTypesSpec extends AbstractTypeElementSpec { @@ -24,14 +24,14 @@ import io.micronaut.context.annotation.Executable; import jakarta.inject.Singleton; import java.util.List; import java.util.List; -import javax.validation.constraints.Min; +import jakarta.validation.constraints.Min; @Singleton class Test { @Executable void test(List<@Min(10) Long> values) { - + } } ''') @@ -52,14 +52,14 @@ import io.micronaut.context.annotation.Executable; import jakarta.inject.Singleton; import java.util.List; import java.util.List; -import javax.validation.Valid; +import jakarta.validation.Valid; @Singleton class Test { @Executable void test(List<@Valid Foo> values) { - + } } @@ -86,14 +86,14 @@ import io.micronaut.context.annotation.Executable; import jakarta.inject.Singleton; import java.util.List; import java.util.List; -import javax.validation.Valid; +import jakarta.validation.Valid; @Singleton class Test { @Executable void test(@Valid Foo value) { - + } } @@ -121,7 +121,7 @@ import io.micronaut.context.annotation.Executable; import jakarta.inject.Singleton; import java.util.List; import java.util.List; -import javax.validation.constraints.Min; +import jakarta.validation.constraints.Min; @Singleton class Test { @@ -149,7 +149,7 @@ import io.micronaut.context.annotation.*; import jakarta.inject.Singleton; import java.util.List; import java.util.List; -import javax.validation.constraints.Min; +import jakarta.validation.constraints.Min; @Singleton class Test { @@ -175,14 +175,14 @@ import io.micronaut.context.annotation.*; import jakarta.inject.*; import java.util.List; import java.util.List; -import javax.validation.constraints.Min; +import jakarta.validation.constraints.Min; @Singleton class Test { @Inject void test(List<@Min(10) Long> values) { - + } } ''') diff --git a/inject-java/src/test/groovy/io/micronaut/inject/annotation/ArgumentAnnotationMetadataSpec.groovy b/inject-java/src/test/groovy/io/micronaut/inject/annotation/ArgumentAnnotationMetadataSpec.groovy index 05f568450dc..694320fd8f9 100644 --- a/inject-java/src/test/groovy/io/micronaut/inject/annotation/ArgumentAnnotationMetadataSpec.groovy +++ b/inject-java/src/test/groovy/io/micronaut/inject/annotation/ArgumentAnnotationMetadataSpec.groovy @@ -19,7 +19,7 @@ import io.micronaut.core.annotation.AnnotationMetadata import io.micronaut.annotation.processing.test.AbstractTypeElementSpec import io.micronaut.core.annotation.AnnotationUtil import jakarta.inject.Named -import javax.validation.constraints.Size +import jakarta.validation.constraints.Size class ArgumentAnnotationMetadataSpec extends AbstractTypeElementSpec { @@ -32,7 +32,7 @@ package test; class Test { void test(@jakarta.inject.Named("foo") String id) { - + } } ''', 'test', 'id') @@ -53,8 +53,8 @@ package test; class Test { @io.micronaut.context.annotation.Executable - void test(@javax.validation.constraints.Size(max=1024) byte[] id) { - + void test(@jakarta.validation.constraints.Size(max=1024) byte[] id) { + } } ''', 'test', 'id') @@ -78,7 +78,7 @@ class Test implements TestApi { @jakarta.annotation.PostConstruct @java.lang.Override public void test(String id) { - + } } @@ -90,7 +90,7 @@ interface TestApi { @Inherited @Retention(RetentionPolicy.RUNTIME) -@jakarta.inject.Named("foo") +@jakarta.inject.Named("foo") @interface MyAnn {} ''', 'test', 'id') diff --git a/inject-java/src/test/groovy/io/micronaut/inject/beans/BeanDefinitionSpec.groovy b/inject-java/src/test/groovy/io/micronaut/inject/beans/BeanDefinitionSpec.groovy index ab873d26f9a..a45d5eaebce 100644 --- a/inject-java/src/test/groovy/io/micronaut/inject/beans/BeanDefinitionSpec.groovy +++ b/inject-java/src/test/groovy/io/micronaut/inject/beans/BeanDefinitionSpec.groovy @@ -469,7 +469,7 @@ public class Test { given: BeanDefinition definition = buildBeanDefinition('test','Test',''' package test; -import javax.validation.constraints.*; +import jakarta.validation.constraints.*; import java.util.List; @jakarta.inject.Singleton @@ -487,20 +487,21 @@ public class Test { def param3 = param2.getTypeParameters()[0] then: - param.getAnnotationMetadata().getAnnotationNames().size() == 0 + param.getAnnotationMetadata().getAnnotationNames().size() == 1 + param.getAnnotationMetadata().getAnnotationNames().asList() == ['io.micronaut.validation.annotation.ValidatedElement'] param1.getAnnotationMetadata().getAnnotationNames().size() == 1 - param1.getAnnotationMetadata().getAnnotationNames().asList() == ['javax.validation.constraints.Size$List'] + param1.getAnnotationMetadata().getAnnotationNames().asList() == ['jakarta.validation.constraints.Size$List'] param2.getAnnotationMetadata().getAnnotationNames().size() == 1 - param2.getAnnotationMetadata().getAnnotationNames().asList() == ['javax.validation.constraints.NotEmpty$List'] + param2.getAnnotationMetadata().getAnnotationNames().asList() == ['jakarta.validation.constraints.NotEmpty$List'] param3.getAnnotationMetadata().getAnnotationNames().size() == 1 - param3.getAnnotationMetadata().getAnnotationNames().asList() == ['javax.validation.constraints.NotNull$List'] + param3.getAnnotationMetadata().getAnnotationNames().asList() == ['jakarta.validation.constraints.NotNull$List'] } void "test isTypeVariable"() { given: ApplicationContext context = buildContext( ''' package test; -import javax.validation.constraints.*; +import jakarta.validation.constraints.*; import java.util.*; import io.micronaut.core.annotation.*; import io.micronaut.context.annotation.*; @@ -555,7 +556,7 @@ class SetTest implements Serde> { given: BeanDefinition definition = buildBeanDefinition('test', 'Test', ''' package test; -import javax.validation.constraints.*; +import jakarta.validation.constraints.*; import java.util.List; @jakarta.inject.Singleton diff --git a/inject-java/src/test/groovy/io/micronaut/inject/configproperties/ImmutableConfigurationPropertiesSpec.groovy b/inject-java/src/test/groovy/io/micronaut/inject/configproperties/ImmutableConfigurationPropertiesSpec.groovy index 58bf272053a..4590b1a933f 100644 --- a/inject-java/src/test/groovy/io/micronaut/inject/configproperties/ImmutableConfigurationPropertiesSpec.groovy +++ b/inject-java/src/test/groovy/io/micronaut/inject/configproperties/ImmutableConfigurationPropertiesSpec.groovy @@ -4,10 +4,13 @@ import io.micronaut.annotation.processing.test.AbstractTypeElementSpec import io.micronaut.context.ApplicationContext import io.micronaut.context.ApplicationContextBuilder import io.micronaut.context.annotation.Property +import io.micronaut.context.visitor.ConfigurationReaderVisitor import io.micronaut.inject.BeanDefinition import io.micronaut.inject.InstantiatableBeanDefinition import io.micronaut.inject.ValidatedBeanDefinition import io.micronaut.inject.qualifiers.Qualifiers +import io.micronaut.inject.visitor.TypeElementVisitor +import io.micronaut.validation.visitor.ValidationVisitor class ImmutableConfigurationPropertiesSpec extends AbstractTypeElementSpec { @@ -17,10 +20,11 @@ class ImmutableConfigurationPropertiesSpec extends AbstractTypeElementSpec { package interfaceprops; import io.micronaut.context.annotation.EachProperty; +import io.micronaut.context.annotation.Executable; @EachProperty("foo.bar") interface MyConfig { - @javax.validation.constraints.NotBlank + @jakarta.validation.constraints.NotBlank String getHost(); int getPort(); @@ -49,7 +53,7 @@ class MyConfig { private int serverPort; @ConfigurationInject - MyConfig(@javax.validation.constraints.NotBlank String host, int serverPort) { + MyConfig(@jakarta.validation.constraints.NotBlank String host, int serverPort) { this.host = host; this.serverPort = serverPort; } @@ -244,4 +248,9 @@ class MyConfig { context.close() } + + @Override + protected Collection getLocalTypeElementVisitors() { + [new ValidationVisitor(), new ConfigurationReaderVisitor()] + } } diff --git a/inject-java/src/test/groovy/io/micronaut/inject/configproperties/InterfaceConfigurationPropertiesSpec.groovy b/inject-java/src/test/groovy/io/micronaut/inject/configproperties/InterfaceConfigurationPropertiesSpec.groovy index ae8f38873fc..bc185389640 100644 --- a/inject-java/src/test/groovy/io/micronaut/inject/configproperties/InterfaceConfigurationPropertiesSpec.groovy +++ b/inject-java/src/test/groovy/io/micronaut/inject/configproperties/InterfaceConfigurationPropertiesSpec.groovy @@ -25,10 +25,10 @@ import java.time.Duration; @ConfigurationProperties("foo.bar") interface MyConfig extends Toggleable { - @javax.validation.constraints.NotBlank + @jakarta.validation.constraints.NotBlank String getHost(); - @javax.validation.constraints.Min(10L) + @jakarta.validation.constraints.Min(10L) int getServerPort(); @Bindable(defaultValue = "true") @@ -75,7 +75,7 @@ interface MyConfig { @javax.annotation.Nullable String getHost(); - @javax.validation.constraints.Min(10L) + @jakarta.validation.constraints.Min(10L) Optional getServerPort(); @io.micronaut.core.bind.annotation.Bindable(defaultValue = "http://default") @@ -130,14 +130,14 @@ import java.time.Duration; interface MyConfig extends ParentConfig { @Executable - @javax.validation.constraints.Min(10L) + @jakarta.validation.constraints.Min(10L) int getServerPort(); } @ConfigurationProperties("foo") interface ParentConfig { @Executable - @javax.validation.constraints.NotBlank + @jakarta.validation.constraints.NotBlank String getHost(); } @@ -175,11 +175,11 @@ import java.net.URL; @ConfigurationProperties("foo.bar") interface MyConfig { @Executable - @javax.validation.constraints.NotBlank + @jakarta.validation.constraints.NotBlank String getHost(); @Executable - @javax.validation.constraints.Min(10L) + @jakarta.validation.constraints.Min(10L) int getServerPort(); @ConfigurationProperties("child") @@ -219,11 +219,11 @@ import java.net.URL; @ConfigurationProperties("foo.bar") interface MyConfig { - @javax.validation.constraints.NotBlank + @jakarta.validation.constraints.NotBlank @Executable String getHost(); - @javax.validation.constraints.Min(10L) + @jakarta.validation.constraints.Min(10L) @Executable int getServerPort(); @@ -268,10 +268,10 @@ import java.time.Duration; @ConfigurationProperties("foo.bar") interface MyConfig { - @javax.validation.constraints.NotBlank + @jakarta.validation.constraints.NotBlank String junk(String s); - @javax.validation.constraints.Min(10L) + @jakarta.validation.constraints.Min(10L) int getServerPort(); } diff --git a/inject-java/src/test/groovy/io/micronaut/inject/configproperties/Pojo.java b/inject-java/src/test/groovy/io/micronaut/inject/configproperties/Pojo.java index 43b99de3407..5d020b98606 100644 --- a/inject-java/src/test/groovy/io/micronaut/inject/configproperties/Pojo.java +++ b/inject-java/src/test/groovy/io/micronaut/inject/configproperties/Pojo.java @@ -2,8 +2,8 @@ import io.micronaut.core.annotation.Introspected; -import javax.validation.constraints.Email; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; @Introspected public class Pojo { diff --git a/inject-java/src/test/groovy/io/micronaut/inject/configproperties/ValidatedConfig.java b/inject-java/src/test/groovy/io/micronaut/inject/configproperties/ValidatedConfig.java index 94daeb6dee6..c6407514ade 100644 --- a/inject-java/src/test/groovy/io/micronaut/inject/configproperties/ValidatedConfig.java +++ b/inject-java/src/test/groovy/io/micronaut/inject/configproperties/ValidatedConfig.java @@ -18,8 +18,8 @@ import io.micronaut.context.annotation.ConfigurationProperties; import io.micronaut.context.annotation.Requires; -import javax.validation.constraints.NotNull; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.NotBlank; import java.net.URL; @Requires(property = "spec.name", value = "ValidatedConfigurationSpec") diff --git a/inject-java/src/test/groovy/io/micronaut/inject/configproperties/ValidatedConfigurationSpec.groovy b/inject-java/src/test/groovy/io/micronaut/inject/configproperties/ValidatedConfigurationSpec.groovy index cc47f14299a..71132ee1d45 100644 --- a/inject-java/src/test/groovy/io/micronaut/inject/configproperties/ValidatedConfigurationSpec.groovy +++ b/inject-java/src/test/groovy/io/micronaut/inject/configproperties/ValidatedConfigurationSpec.groovy @@ -24,7 +24,7 @@ import io.micronaut.inject.BeanDefinition import io.micronaut.inject.ValidatedBeanDefinition import spock.lang.Specification -import javax.validation.Validation +import jakarta.validation.Validation class ValidatedConfigurationSpec extends AbstractTypeElementSpec { @@ -79,7 +79,7 @@ package test; import io.micronaut.context.annotation.ConfigurationProperties; import io.micronaut.inject.configproperties.Pojo; -import javax.validation.Valid; +import jakarta.validation.Valid; import java.util.List; @ConfigurationProperties("test.valid") @@ -111,12 +111,12 @@ package test; import io.micronaut.context.annotation.ConfigurationProperties; import io.micronaut.inject.configproperties.Pojo; -import javax.validation.Valid; +import jakarta.validation.Valid; import java.util.List; @ConfigurationProperties("test.valid") public class MyConfig { - + private List pojos; @Valid diff --git a/inject-java/src/test/groovy/io/micronaut/inject/configproperties/ValidatedGetterConfig.java b/inject-java/src/test/groovy/io/micronaut/inject/configproperties/ValidatedGetterConfig.java index ad5c9cedc74..2f343bf78a5 100644 --- a/inject-java/src/test/groovy/io/micronaut/inject/configproperties/ValidatedGetterConfig.java +++ b/inject-java/src/test/groovy/io/micronaut/inject/configproperties/ValidatedGetterConfig.java @@ -18,8 +18,8 @@ import io.micronaut.context.annotation.ConfigurationProperties; import io.micronaut.context.annotation.Requires; -import javax.validation.constraints.NotBlank; -import javax.validation.constraints.NotNull; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; import java.net.URL; @Requires(property = "spec.name", value = "ValidatedGetterConfigurationSpec") diff --git a/inject-java/src/test/groovy/io/micronaut/inject/configproperties/ValidatedGetterConfigurationSpec.groovy b/inject-java/src/test/groovy/io/micronaut/inject/configproperties/ValidatedGetterConfigurationSpec.groovy index 59b62c3965b..3dd698d48d4 100644 --- a/inject-java/src/test/groovy/io/micronaut/inject/configproperties/ValidatedGetterConfigurationSpec.groovy +++ b/inject-java/src/test/groovy/io/micronaut/inject/configproperties/ValidatedGetterConfigurationSpec.groovy @@ -21,7 +21,7 @@ import io.micronaut.context.env.PropertySource import io.micronaut.context.exceptions.BeanInstantiationException import spock.lang.Specification -import javax.validation.Validation +import jakarta.validation.Validation class ValidatedGetterConfigurationSpec extends Specification { diff --git a/inject-java/src/test/groovy/io/micronaut/inject/configproperties/itfce/MyConfig.java b/inject-java/src/test/groovy/io/micronaut/inject/configproperties/itfce/MyConfig.java index 4c93cfdbe15..046065ef37f 100644 --- a/inject-java/src/test/groovy/io/micronaut/inject/configproperties/itfce/MyConfig.java +++ b/inject-java/src/test/groovy/io/micronaut/inject/configproperties/itfce/MyConfig.java @@ -3,7 +3,7 @@ import io.micronaut.context.annotation.ConfigurationProperties; import io.micronaut.context.annotation.Requires; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotBlank; @ConfigurationProperties("my.config") @Requires(property = "my.config") diff --git a/inject-java/src/test/groovy/io/micronaut/inject/configproperties/itfce/MyEachConfig.java b/inject-java/src/test/groovy/io/micronaut/inject/configproperties/itfce/MyEachConfig.java index be2e9a2ac92..6dec0122f92 100644 --- a/inject-java/src/test/groovy/io/micronaut/inject/configproperties/itfce/MyEachConfig.java +++ b/inject-java/src/test/groovy/io/micronaut/inject/configproperties/itfce/MyEachConfig.java @@ -3,7 +3,7 @@ import io.micronaut.context.annotation.EachProperty; import io.micronaut.context.annotation.Requires; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotBlank; @EachProperty(value = "my.config", primary = "default") @Requires(property = "my.config") diff --git a/inject-java/src/test/groovy/io/micronaut/inject/configproperties/writeonly/WriteOnlyConfigProperties.java b/inject-java/src/test/groovy/io/micronaut/inject/configproperties/writeonly/WriteOnlyConfigProperties.java index c8430c5cb31..1ad3bb37b58 100644 --- a/inject-java/src/test/groovy/io/micronaut/inject/configproperties/writeonly/WriteOnlyConfigProperties.java +++ b/inject-java/src/test/groovy/io/micronaut/inject/configproperties/writeonly/WriteOnlyConfigProperties.java @@ -3,7 +3,7 @@ import io.micronaut.context.annotation.ConfigurationProperties; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotBlank; @ConfigurationProperties("test") public class WriteOnlyConfigProperties { diff --git a/inject-java/src/test/groovy/io/micronaut/inject/executable/ExecutableBeanSpec.groovy b/inject-java/src/test/groovy/io/micronaut/inject/executable/ExecutableBeanSpec.groovy index d369ff98ce1..e39ab4814a3 100644 --- a/inject-java/src/test/groovy/io/micronaut/inject/executable/ExecutableBeanSpec.groovy +++ b/inject-java/src/test/groovy/io/micronaut/inject/executable/ExecutableBeanSpec.groovy @@ -115,7 +115,7 @@ package test; import io.micronaut.inject.annotation.*; import io.micronaut.context.annotation.*; -import javax.validation.Valid; +import jakarta.validation.Valid; import java.util.List; @jakarta.inject.Singleton @@ -226,7 +226,7 @@ package test; import io.micronaut.inject.annotation.*; import io.micronaut.context.annotation.*; -import javax.validation.Valid; +import jakarta.validation.Valid; import java.util.List; import io.micronaut.inject.executable.Book; import io.micronaut.inject.executable.TypeUseRuntimeAnn; diff --git a/inject-java/src/test/groovy/io/micronaut/inject/executable/MyBean.java b/inject-java/src/test/groovy/io/micronaut/inject/executable/MyBean.java index 94f26fcad38..59dd62ba278 100644 --- a/inject-java/src/test/groovy/io/micronaut/inject/executable/MyBean.java +++ b/inject-java/src/test/groovy/io/micronaut/inject/executable/MyBean.java @@ -2,7 +2,7 @@ import io.micronaut.context.annotation.Executable; -import javax.validation.Valid; +import jakarta.validation.Valid; import java.util.List; @jakarta.inject.Singleton diff --git a/inject-java/src/test/groovy/io/micronaut/inject/generics/GenericTypeArgumentsSpec.groovy b/inject-java/src/test/groovy/io/micronaut/inject/generics/GenericTypeArgumentsSpec.groovy index d1f088562a3..acd3ec2ce7b 100644 --- a/inject-java/src/test/groovy/io/micronaut/inject/generics/GenericTypeArgumentsSpec.groovy +++ b/inject-java/src/test/groovy/io/micronaut/inject/generics/GenericTypeArgumentsSpec.groovy @@ -25,7 +25,7 @@ import zipkin2.Span import zipkin2.reporter.AsyncReporter import zipkin2.reporter.Reporter -import javax.validation.ConstraintViolationException +import jakarta.validation.ConstraintViolationException import java.util.function.Function import java.util.function.Supplier @@ -39,7 +39,7 @@ package innergenerics; class Outer { interface Foo {} - + @jakarta.inject.Singleton class FooImpl implements Foo {} } @@ -89,7 +89,7 @@ package exceptionhandler; import io.micronaut.inject.annotation.*; import io.micronaut.context.annotation.*; -import javax.validation.ConstraintViolationException; +import jakarta.validation.ConstraintViolationException; @Context class Test implements ExceptionHandler> { diff --git a/inject-java/src/test/groovy/io/micronaut/inject/records/Test.java b/inject-java/src/test/groovy/io/micronaut/inject/records/Test.java index 0ef953567fd..55bec0dc31e 100644 --- a/inject-java/src/test/groovy/io/micronaut/inject/records/Test.java +++ b/inject-java/src/test/groovy/io/micronaut/inject/records/Test.java @@ -6,7 +6,7 @@ import io.micronaut.core.convert.ConversionService; import jakarta.inject.Inject; -import javax.validation.constraints.Min; +import jakarta.validation.constraints.Min; @Requires(property = "spec.name", value = "RecordBeansSpec") @ConfigurationProperties("foo") diff --git a/inject-java/src/test/groovy/io/micronaut/inject/records/Test2.java b/inject-java/src/test/groovy/io/micronaut/inject/records/Test2.java index cfa19a06282..6858587d1bc 100644 --- a/inject-java/src/test/groovy/io/micronaut/inject/records/Test2.java +++ b/inject-java/src/test/groovy/io/micronaut/inject/records/Test2.java @@ -6,7 +6,7 @@ import io.micronaut.core.convert.ConversionService; import jakarta.inject.Inject; -import javax.validation.constraints.NotNull; +import jakarta.validation.constraints.NotNull; @Requires(property = "spec.name", value = "RecordBeansSpec") record Test2( diff --git a/inject-java/src/test/groovy/io/micronaut/inject/requires/RequiresBeanPropertiesSpec.groovy b/inject-java/src/test/groovy/io/micronaut/inject/requires/RequiresBeanPropertiesSpec.groovy index da05b6d7754..c4b3db692a5 100644 --- a/inject-java/src/test/groovy/io/micronaut/inject/requires/RequiresBeanPropertiesSpec.groovy +++ b/inject-java/src/test/groovy/io/micronaut/inject/requires/RequiresBeanPropertiesSpec.groovy @@ -673,7 +673,7 @@ class AccessorStyleBean { package test; import io.micronaut.context.annotation.*; import io.micronaut.core.convert.ConversionService; -import javax.validation.constraints.Min; +import jakarta.validation.constraints.Min; import jakarta.inject.Inject; import io.micronaut.context.BeanContext; import jakarta.inject.Singleton; diff --git a/inject-java/src/test/groovy/io/micronaut/inject/validation/Account1.java b/inject-java/src/test/groovy/io/micronaut/inject/validation/Account1.java index 40d7ed1e007..23565ffab20 100644 --- a/inject-java/src/test/groovy/io/micronaut/inject/validation/Account1.java +++ b/inject-java/src/test/groovy/io/micronaut/inject/validation/Account1.java @@ -2,7 +2,7 @@ import javax.annotation.Nullable; import javax.persistence.Entity; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotBlank; @Entity public class Account1 { diff --git a/inject-java/src/test/groovy/io/micronaut/inject/validation/Account2.java b/inject-java/src/test/groovy/io/micronaut/inject/validation/Account2.java index b6c8cdb633d..79620bde4c3 100644 --- a/inject-java/src/test/groovy/io/micronaut/inject/validation/Account2.java +++ b/inject-java/src/test/groovy/io/micronaut/inject/validation/Account2.java @@ -1,7 +1,7 @@ package io.micronaut.inject.validation; import javax.persistence.Entity; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotBlank; @Entity public class Account2 { diff --git a/inject-java/src/test/groovy/io/micronaut/inject/validation/Account3.java b/inject-java/src/test/groovy/io/micronaut/inject/validation/Account3.java index 07b9fb9db5d..669d702e612 100644 --- a/inject-java/src/test/groovy/io/micronaut/inject/validation/Account3.java +++ b/inject-java/src/test/groovy/io/micronaut/inject/validation/Account3.java @@ -2,7 +2,7 @@ import javax.annotation.Nullable; import javax.persistence.Entity; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotBlank; @Entity @Nullable diff --git a/inject-java/src/test/groovy/io/micronaut/visitors/ClassElementAnnotationsRetaining.groovy b/inject-java/src/test/groovy/io/micronaut/visitors/ClassElementAnnotationsRetaining.groovy index 47caef20eaf..0f8dabee72b 100644 --- a/inject-java/src/test/groovy/io/micronaut/visitors/ClassElementAnnotationsRetaining.groovy +++ b/inject-java/src/test/groovy/io/micronaut/visitors/ClassElementAnnotationsRetaining.groovy @@ -16,8 +16,8 @@ class ClassElementAnnotationsRetaining extends AbstractTypeElementSpec { def code = ''' package test; import java.util.List; -import javax.validation.Valid; -import javax.validation.constraints.NotBlank; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotBlank; import io.micronaut.core.annotation.Introspected; @Introspected class SaladWithSetter { @@ -38,7 +38,7 @@ class SaladWithSetter { then: def propertyTypeArgument = propertyElement.type.typeArguments.get("E") - propertyTypeArgument.annotationMetadata.hasStereotype("javax.validation.Valid") + propertyTypeArgument.annotationMetadata.hasStereotype("jakarta.validation.Valid") } void 'test type argument annotation on the getter'() { @@ -48,8 +48,8 @@ class SaladWithSetter { def code = ''' package test; import java.util.List; -import javax.validation.Valid; -import javax.validation.constraints.NotBlank; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotBlank; import io.micronaut.core.annotation.Introspected; @Introspected class SaladWithSetter { @@ -70,7 +70,7 @@ class SaladWithSetter { then: def propertyTypeArgument = propertyElement.type.typeArguments.get("E") - propertyTypeArgument.annotationMetadata.hasStereotype("javax.validation.Valid") + propertyTypeArgument.annotationMetadata.hasStereotype("jakarta.validation.Valid") } void 'test type argument annotation on the setter'() { @@ -79,8 +79,8 @@ class SaladWithSetter { def code = ''' package test; import java.util.List; -import javax.validation.Valid; -import javax.validation.constraints.NotBlank; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotBlank; import io.micronaut.core.annotation.Introspected; @Introspected class SaladWithSetter { @@ -104,7 +104,7 @@ class SaladWithSetter { then: def propertyTypeArgument = propertyElement.type.typeArguments.get("E") - propertyTypeArgument.annotationMetadata.hasStereotype("javax.validation.Valid") + propertyTypeArgument.annotationMetadata.hasStereotype("jakarta.validation.Valid") } void 'test type argument annotation on the property with a setter'() { @@ -114,8 +114,8 @@ class SaladWithSetter { def code = ''' package test; import java.util.List; -import javax.validation.Valid; -import javax.validation.constraints.NotBlank; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotBlank; import io.micronaut.core.annotation.Introspected; @Introspected class SaladWithSetter { @@ -139,7 +139,7 @@ class SaladWithSetter { then: def propertyTypeArgument = propertyElement.type.typeArguments.get("E") - propertyTypeArgument.annotationMetadata.hasStereotype("javax.validation.Valid") + propertyTypeArgument.annotationMetadata.hasStereotype("jakarta.validation.Valid") } void 'test annotation on the property with a setter'() { @@ -148,8 +148,8 @@ class SaladWithSetter { // Define a setter def code = ''' package test; -import javax.validation.Valid; -import javax.validation.constraints.NotBlank; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotBlank; import io.micronaut.core.annotation.Introspected; @Introspected class SaladWithSetter { @@ -172,7 +172,7 @@ class SaladWithSetter { PropertyElement propertyElement = classElement.getBeanProperties().iterator().next() then: - propertyElement.annotationMetadata.hasStereotype("javax.validation.Valid") + propertyElement.annotationMetadata.hasStereotype("jakarta.validation.Valid") } } diff --git a/inject-java/src/test/groovy/io/micronaut/visitors/ClassElementSpec.groovy b/inject-java/src/test/groovy/io/micronaut/visitors/ClassElementSpec.groovy index 56a0fae819f..79dfdcddb29 100644 --- a/inject-java/src/test/groovy/io/micronaut/visitors/ClassElementSpec.groovy +++ b/inject-java/src/test/groovy/io/micronaut/visitors/ClassElementSpec.groovy @@ -1696,7 +1696,7 @@ class Pet { ClassElement ce = buildClassElement(''' package test; import io.micronaut.core.annotation.*; -import javax.validation.constraints.*; +import jakarta.validation.constraints.*; import java.util.List; class Test { @@ -1710,28 +1710,28 @@ class Test { fieldType.getAnnotationMetadata().getAnnotationNames().size() == 0 assertListGenericArgument(fieldType, { ClassElement listArg1 -> - assert listArg1.getAnnotationMetadata().getAnnotationNames().asList() == ['javax.validation.constraints.Size$List'] + assert listArg1.getAnnotationMetadata().getAnnotationNames().asList() == ['jakarta.validation.constraints.Size$List'] assertListGenericArgument(listArg1, { ClassElement listArg2 -> - assert listArg2.getAnnotationMetadata().getAnnotationNames().asList() == ['javax.validation.constraints.NotEmpty$List'] + assert listArg2.getAnnotationMetadata().getAnnotationNames().asList() == ['jakarta.validation.constraints.NotEmpty$List'] assertListGenericArgument(listArg2, { ClassElement listArg3 -> - assert listArg3.getAnnotationMetadata().getAnnotationNames().asList() == ['javax.validation.constraints.NotNull$List'] + assert listArg3.getAnnotationMetadata().getAnnotationNames().asList() == ['jakarta.validation.constraints.NotNull$List'] }) }) }) def level1 = fieldType.getTypeArguments()["E"] - level1.getAnnotationMetadata().getAnnotationNames().asList() == ['javax.validation.constraints.Size$List'] + level1.getAnnotationMetadata().getAnnotationNames().asList() == ['jakarta.validation.constraints.Size$List'] def level2 = level1.getTypeArguments()["E"] - level2.getAnnotationMetadata().getAnnotationNames().asList() == ['javax.validation.constraints.NotEmpty$List'] + level2.getAnnotationMetadata().getAnnotationNames().asList() == ['jakarta.validation.constraints.NotEmpty$List'] def level3 = level2.getTypeArguments()["E"] - level3.getAnnotationMetadata().getAnnotationNames().asList() == ['javax.validation.constraints.NotNull$List'] + level3.getAnnotationMetadata().getAnnotationNames().asList() == ['jakarta.validation.constraints.NotNull$List'] } void "test annotation metadata present on deep type parameters for method"() { ClassElement ce = buildClassElement(''' package test; import io.micronaut.core.annotation.*; -import javax.validation.constraints.*; +import jakarta.validation.constraints.*; import java.util.List; class Test { @@ -1747,21 +1747,21 @@ class Test { theType.getAnnotationMetadata().getAnnotationNames().size() == 0 assertListGenericArgument(theType, { ClassElement listArg1 -> - assert listArg1.getAnnotationMetadata().getAnnotationNames().asList() == ['javax.validation.constraints.Size$List'] + assert listArg1.getAnnotationMetadata().getAnnotationNames().asList() == ['jakarta.validation.constraints.Size$List'] assertListGenericArgument(listArg1, { ClassElement listArg2 -> - assert listArg2.getAnnotationMetadata().getAnnotationNames().asList() == ['javax.validation.constraints.NotEmpty$List'] + assert listArg2.getAnnotationMetadata().getAnnotationNames().asList() == ['jakarta.validation.constraints.NotEmpty$List'] assertListGenericArgument(listArg2, { ClassElement listArg3 -> - assert listArg3.getAnnotationMetadata().getAnnotationNames().asList() == ['javax.validation.constraints.NotNull$List'] + assert listArg3.getAnnotationMetadata().getAnnotationNames().asList() == ['jakarta.validation.constraints.NotNull$List'] }) }) }) def level1 = theType.getTypeArguments()["E"] - level1.getAnnotationMetadata().getAnnotationNames().asList() == ['javax.validation.constraints.Size$List'] + level1.getAnnotationMetadata().getAnnotationNames().asList() == ['jakarta.validation.constraints.Size$List'] def level2 = level1.getTypeArguments()["E"] - level2.getAnnotationMetadata().getAnnotationNames().asList() == ['javax.validation.constraints.NotEmpty$List'] + level2.getAnnotationMetadata().getAnnotationNames().asList() == ['jakarta.validation.constraints.NotEmpty$List'] def level3 = level2.getTypeArguments()["E"] - level3.getAnnotationMetadata().getAnnotationNames().asList() == ['javax.validation.constraints.NotNull$List'] + level3.getAnnotationMetadata().getAnnotationNames().asList() == ['jakarta.validation.constraints.NotNull$List'] } void "test type annotations on a method and a field"() { @@ -1852,7 +1852,7 @@ final class TrackedSortedSet> { ClassElement ce = buildClassElement(''' package test; import io.micronaut.core.annotation.*; -import javax.validation.constraints.*; +import jakarta.validation.constraints.*; import java.util.List; class Test { diff --git a/inject-java/src/test/groovy/io/micronaut/visitors/DocumentationSpec.groovy b/inject-java/src/test/groovy/io/micronaut/visitors/DocumentationSpec.groovy index cc9a29ac02c..705c8b8d3f4 100644 --- a/inject-java/src/test/groovy/io/micronaut/visitors/DocumentationSpec.groovy +++ b/inject-java/src/test/groovy/io/micronaut/visitors/DocumentationSpec.groovy @@ -10,8 +10,8 @@ class DocumentationSpec extends AbstractTypeElementSpec { def classElement = buildClassElement(""" package test; -import javax.validation.constraints.NotBlank; -import javax.validation.constraints.NotNull; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; /** * This is class level docs diff --git a/inject-java/src/test/groovy/io/micronaut/visitors/InterfaceWithGenerics.java b/inject-java/src/test/groovy/io/micronaut/visitors/InterfaceWithGenerics.java index 4e9f7fffcf9..3d3cece1f0e 100644 --- a/inject-java/src/test/groovy/io/micronaut/visitors/InterfaceWithGenerics.java +++ b/inject-java/src/test/groovy/io/micronaut/visitors/InterfaceWithGenerics.java @@ -16,8 +16,8 @@ package io.micronaut.visitors; import io.micronaut.core.annotation.NonNull; -import javax.validation.Valid; -import javax.validation.constraints.NotNull; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotNull; import java.util.Optional; public interface InterfaceWithGenerics { diff --git a/inject-java/src/test/groovy/io/micronaut/visitors/PropertyElementSpec.groovy b/inject-java/src/test/groovy/io/micronaut/visitors/PropertyElementSpec.groovy index 0d0f45a3550..d3a5c75d84e 100644 --- a/inject-java/src/test/groovy/io/micronaut/visitors/PropertyElementSpec.groovy +++ b/inject-java/src/test/groovy/io/micronaut/visitors/PropertyElementSpec.groovy @@ -22,7 +22,7 @@ import spock.lang.IgnoreIf import spock.util.environment.Jvm import javax.annotation.Nullable -import javax.validation.constraints.NotBlank +import jakarta.validation.constraints.NotBlank class PropertyElementSpec extends AbstractTypeElementSpec { @IgnoreIf({ !jvm.isJava14Compatible() }) @@ -31,7 +31,7 @@ class PropertyElementSpec extends AbstractTypeElementSpec { ClassElement classElement = buildClassElement(''' package test; -record Book( @javax.validation.constraints.NotBlank String title, int pages) {} +record Book( @jakarta.validation.constraints.NotBlank String title, int pages) {} ''') def beanProperties = classElement.getBeanProperties() def titleProp = beanProperties.find { it.name == 'title' } @@ -75,7 +75,7 @@ public class TestController { * The age */ @Get("/getMethod/{age}") - public int getAge( @javax.validation.constraints.NotBlank int age) { + public int getAge( @jakarta.validation.constraints.NotBlank int age) { return age; } @@ -83,8 +83,8 @@ public class TestController { return name; } - @javax.validation.constraints.NotBlank - public void setName(@javax.validation.constraints.NotBlank String n) { + @jakarta.validation.constraints.NotBlank + public void setName(@jakarta.validation.constraints.NotBlank String n) { name = n; } @@ -95,7 +95,7 @@ public class TestController { return description; } - public void setDescription(@javax.validation.constraints.NotBlank String description) { + public void setDescription(@jakarta.validation.constraints.NotBlank String description) { this.description = description; } } diff --git a/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/annotation/KotlinAnnotationMetadataBuilder.kt b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/annotation/KotlinAnnotationMetadataBuilder.kt index dcab23e5b9c..d3ddeacc048 100644 --- a/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/annotation/KotlinAnnotationMetadataBuilder.kt +++ b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/annotation/KotlinAnnotationMetadataBuilder.kt @@ -295,7 +295,7 @@ internal class KotlinAnnotationMetadataBuilder(private val symbolProcessorEnviro return member.annotations.any { val name = it.annotationType.resolve().declaration.qualifiedName?.asString() if (name != null) { - return name.startsWith("javax.validation") || name.startsWith("jakarta.validation") + return name.startsWith("jakarta.validation") } else { return false } diff --git a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/compile/IntroductionAnnotationSpec.groovy b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/compile/IntroductionAnnotationSpec.groovy index ecc10da9d20..d2ae71a941f 100644 --- a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/compile/IntroductionAnnotationSpec.groovy +++ b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/compile/IntroductionAnnotationSpec.groovy @@ -9,8 +9,8 @@ import io.micronaut.inject.writer.BeanDefinitionVisitor import io.micronaut.kotlin.processing.aop.introduction.NotImplementedAdvice import spock.lang.Specification -import javax.validation.constraints.Min -import javax.validation.constraints.NotBlank +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotBlank import static io.micronaut.annotation.processing.test.KotlinCompiler.* @@ -97,8 +97,8 @@ package test import io.micronaut.kotlin.processing.aop.introduction.Stub import io.micronaut.context.annotation.Executable -import javax.validation.constraints.Min -import javax.validation.constraints.NotBlank +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotBlank interface MyInterface{ @Executable diff --git a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/compile/IntroductionWithAroundSpec.groovy b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/compile/IntroductionWithAroundSpec.groovy index c9c8d5625df..5f8054d5b1a 100644 --- a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/compile/IntroductionWithAroundSpec.groovy +++ b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/compile/IntroductionWithAroundSpec.groovy @@ -17,7 +17,7 @@ package test; import io.micronaut.kotlin.processing.aop.introduction.Stub import io.micronaut.kotlin.processing.aop.simple.Mutating -import javax.validation.constraints.* +import jakarta.validation.constraints.* import jakarta.inject.Singleton @Stub diff --git a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/compile/ValidatedNonBeanSpec.groovy b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/compile/ValidatedNonBeanSpec.groovy index 2a60ee60047..276486c9555 100644 --- a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/compile/ValidatedNonBeanSpec.groovy +++ b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/compile/ValidatedNonBeanSpec.groovy @@ -11,7 +11,7 @@ class ValidatedNonBeanSpec extends Specification { BeanDefinition beanDefinition = buildBeanDefinition("test.DefaultContract", """ package test -import javax.validation.constraints.NotNull +import jakarta.validation.constraints.NotNull import io.micronaut.context.annotation.* import jakarta.inject.Singleton diff --git a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/introduction/IntroductionAdviceWithNewInterfaceSpec.groovy b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/introduction/IntroductionAdviceWithNewInterfaceSpec.groovy index 4cd1000d9cb..1d8f4fe324d 100644 --- a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/introduction/IntroductionAdviceWithNewInterfaceSpec.groovy +++ b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/introduction/IntroductionAdviceWithNewInterfaceSpec.groovy @@ -93,7 +93,7 @@ interface MyBean { package test import io.micronaut.kotlin.processing.aop.introduction.* -import javax.validation.constraints.NotNull +import jakarta.validation.constraints.NotNull @RepoDef interface MyRepo : DeleteByIdCrudRepo { diff --git a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/beans/BeanDefinitionSpec.groovy b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/beans/BeanDefinitionSpec.groovy index 90076e8e4fc..d7384cd1b79 100644 --- a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/beans/BeanDefinitionSpec.groovy +++ b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/beans/BeanDefinitionSpec.groovy @@ -209,7 +209,7 @@ package test import io.micronaut.context.annotation.ConfigurationProperties import io.micronaut.core.convert.format.MapFormat -import javax.validation.constraints.Min +import jakarta.validation.constraints.Min // end::imports[] // tag::class[] @@ -885,7 +885,7 @@ class Other given: BeanDefinition definition = buildBeanDefinition('test', 'Test', ''' package test; -import javax.validation.constraints.*; +import jakarta.validation.constraints.*; import java.util.List; @jakarta.inject.Singleton @@ -924,7 +924,7 @@ interface Deserializer { BeanDefinition definition = buildBeanDefinition('test', 'Test', ''' package test; -import javax.validation.constraints.*; +import jakarta.validation.constraints.*; import java.util.List @jakarta.inject.Singleton diff --git a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/beans/executable/ExecutableBeanSpec.groovy b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/beans/executable/ExecutableBeanSpec.groovy index 17c1eb42012..82f918a0c99 100644 --- a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/beans/executable/ExecutableBeanSpec.groovy +++ b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/beans/executable/ExecutableBeanSpec.groovy @@ -249,7 +249,7 @@ package test import io.micronaut.inject.annotation.* import io.micronaut.context.annotation.* -import javax.validation.Valid +import jakarta.validation.Valid import java.util.List import io.micronaut.kotlin.processing.beans.executable.* @@ -361,7 +361,7 @@ package test import io.micronaut.inject.annotation.* import io.micronaut.context.annotation.* import io.micronaut.context.annotation.Executable -import javax.validation.Valid +import jakarta.validation.Valid import java.util.List import io.micronaut.kotlin.processing.beans.executable.* diff --git a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/inject/ast/ClassElementSpec.groovy b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/inject/ast/ClassElementSpec.groovy index ca02ec6758b..c69912ef184 100644 --- a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/inject/ast/ClassElementSpec.groovy +++ b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/inject/ast/ClassElementSpec.groovy @@ -344,7 +344,7 @@ interface Three ClassElement ce = buildClassElementTransformed('test.Test', ''' package test; import io.micronaut.core.annotation.*; -import javax.validation.constraints.*; +import jakarta.validation.constraints.*; import java.util.List; class Test { @@ -363,33 +363,33 @@ class Test { fieldType.getAnnotationMetadata().getAnnotationNames().size() == 0 assertListGenericArgument(fieldType, { ClassElement listArg1 -> - assert listArg1.getAnnotationMetadata().getAnnotationNames().asList() == ['javax.validation.constraints.Size$List'] - assert listArg1.getTypeAnnotationMetadata().getAnnotationNames().asList() == ['javax.validation.constraints.Size$List'] + assert listArg1.getAnnotationMetadata().getAnnotationNames().asList() == ['jakarta.validation.constraints.Size$List'] + assert listArg1.getTypeAnnotationMetadata().getAnnotationNames().asList() == ['jakarta.validation.constraints.Size$List'] assertListGenericArgument(listArg1, { ClassElement listArg2 -> - assert listArg2.getAnnotationMetadata().getAnnotationNames().asList() == ['javax.validation.constraints.NotEmpty$List'] - assert listArg2.getTypeAnnotationMetadata().getAnnotationNames().asList() == ['javax.validation.constraints.NotEmpty$List'] + assert listArg2.getAnnotationMetadata().getAnnotationNames().asList() == ['jakarta.validation.constraints.NotEmpty$List'] + assert listArg2.getTypeAnnotationMetadata().getAnnotationNames().asList() == ['jakarta.validation.constraints.NotEmpty$List'] assertListGenericArgument(listArg2, { ClassElement listArg3 -> - assert listArg3.getAnnotationMetadata().getAnnotationNames().asList() == ['javax.validation.constraints.NotNull$List'] - assert listArg3.getTypeAnnotationMetadata().getAnnotationNames().asList() == ['javax.validation.constraints.NotNull$List'] + assert listArg3.getAnnotationMetadata().getAnnotationNames().asList() == ['jakarta.validation.constraints.NotNull$List'] + assert listArg3.getTypeAnnotationMetadata().getAnnotationNames().asList() == ['jakarta.validation.constraints.NotNull$List'] }) }) }) def level1 = fieldType.getTypeArguments()["E"] - level1.getAnnotationMetadata().getAnnotationNames().asList() == ['javax.validation.constraints.Size$List'] - level1.getTypeAnnotationMetadata().getAnnotationNames().asList() == ['javax.validation.constraints.Size$List'] + level1.getAnnotationMetadata().getAnnotationNames().asList() == ['jakarta.validation.constraints.Size$List'] + level1.getTypeAnnotationMetadata().getAnnotationNames().asList() == ['jakarta.validation.constraints.Size$List'] def level2 = level1.getTypeArguments()["E"] - level2.getAnnotationMetadata().getAnnotationNames().asList() == ['javax.validation.constraints.NotEmpty$List'] - level2.getTypeAnnotationMetadata().getAnnotationNames().asList() == ['javax.validation.constraints.NotEmpty$List'] + level2.getAnnotationMetadata().getAnnotationNames().asList() == ['jakarta.validation.constraints.NotEmpty$List'] + level2.getTypeAnnotationMetadata().getAnnotationNames().asList() == ['jakarta.validation.constraints.NotEmpty$List'] def level3 = level2.getTypeArguments()["E"] - level3.getAnnotationMetadata().getAnnotationNames().asList() == ['javax.validation.constraints.NotNull$List'] - level3.getTypeAnnotationMetadata().getAnnotationNames().asList() == ['javax.validation.constraints.NotNull$List'] + level3.getAnnotationMetadata().getAnnotationNames().asList() == ['jakarta.validation.constraints.NotNull$List'] + level3.getTypeAnnotationMetadata().getAnnotationNames().asList() == ['jakarta.validation.constraints.NotNull$List'] } void "test annotation metadata present on deep type parameters for method"() { ClassElement ce = buildClassElementTransformed('test.Test', ''' package test -import javax.validation.constraints.* +import jakarta.validation.constraints.* import java.util.List class Test { @@ -410,24 +410,24 @@ class Test { theType.getAnnotationMetadata().getAnnotationNames().size() == 0 def level1 = theType.getTypeArguments()["E"] - level1.getAnnotationMetadata().getAnnotationNames().asList() == ['javax.validation.constraints.Size$List'] - level1.getTypeAnnotationMetadata().getAnnotationNames().asList() == ['javax.validation.constraints.Size$List'] + level1.getAnnotationMetadata().getAnnotationNames().asList() == ['jakarta.validation.constraints.Size$List'] + level1.getTypeAnnotationMetadata().getAnnotationNames().asList() == ['jakarta.validation.constraints.Size$List'] def level2 = level1.getTypeArguments()["E"] - level2.getAnnotationMetadata().getAnnotationNames().asList() == ['javax.validation.constraints.NotEmpty$List'] - level2.getTypeAnnotationMetadata().getAnnotationNames().asList() == ['javax.validation.constraints.NotEmpty$List'] + level2.getAnnotationMetadata().getAnnotationNames().asList() == ['jakarta.validation.constraints.NotEmpty$List'] + level2.getTypeAnnotationMetadata().getAnnotationNames().asList() == ['jakarta.validation.constraints.NotEmpty$List'] def level3 = level2.getTypeArguments()["E"] - level3.getAnnotationMetadata().getAnnotationNames().asList() == ['javax.validation.constraints.NotNull$List'] - level3.getTypeAnnotationMetadata().getAnnotationNames().asList() == ['javax.validation.constraints.NotNull$List'] + level3.getAnnotationMetadata().getAnnotationNames().asList() == ['jakarta.validation.constraints.NotNull$List'] + level3.getTypeAnnotationMetadata().getAnnotationNames().asList() == ['jakarta.validation.constraints.NotNull$List'] assertListGenericArgument(theType, { ClassElement listArg1 -> - assert listArg1.getAnnotationMetadata().getAnnotationNames().asList() == ['javax.validation.constraints.Size$List'] - assert listArg1.getTypeAnnotationMetadata().getAnnotationNames().asList() == ['javax.validation.constraints.Size$List'] + assert listArg1.getAnnotationMetadata().getAnnotationNames().asList() == ['jakarta.validation.constraints.Size$List'] + assert listArg1.getTypeAnnotationMetadata().getAnnotationNames().asList() == ['jakarta.validation.constraints.Size$List'] assertListGenericArgument(listArg1, { ClassElement listArg2 -> - assert listArg2.getAnnotationMetadata().getAnnotationNames().asList() == ['javax.validation.constraints.NotEmpty$List'] - assert listArg2.getTypeAnnotationMetadata().getAnnotationNames().asList() == ['javax.validation.constraints.NotEmpty$List'] + assert listArg2.getAnnotationMetadata().getAnnotationNames().asList() == ['jakarta.validation.constraints.NotEmpty$List'] + assert listArg2.getTypeAnnotationMetadata().getAnnotationNames().asList() == ['jakarta.validation.constraints.NotEmpty$List'] assertListGenericArgument(listArg2, { ClassElement listArg3 -> - assert listArg3.getTypeAnnotationMetadata().getAnnotationNames().asList() == ['javax.validation.constraints.NotNull$List'] - assert listArg3.getAnnotationMetadata().getAnnotationNames().asList() == ['javax.validation.constraints.NotNull$List'] + assert listArg3.getTypeAnnotationMetadata().getAnnotationNames().asList() == ['jakarta.validation.constraints.NotNull$List'] + assert listArg3.getAnnotationMetadata().getAnnotationNames().asList() == ['jakarta.validation.constraints.NotNull$List'] }) }) }) @@ -520,7 +520,7 @@ class TrackedSortedSet> ClassElement ce = buildClassElementTransformed('test.Test', ''' package test import io.micronaut.core.annotation.* -import javax.validation.constraints.* +import jakarta.validation.constraints.* import java.util.List import io.micronaut.kotlin.processing.inject.ast.* diff --git a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/inject/configproperties/ImmutableConfigurationPropertiesSpec.groovy b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/inject/configproperties/ImmutableConfigurationPropertiesSpec.groovy index 36fb65b58c6..037dc86d23a 100644 --- a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/inject/configproperties/ImmutableConfigurationPropertiesSpec.groovy +++ b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/inject/configproperties/ImmutableConfigurationPropertiesSpec.groovy @@ -9,7 +9,7 @@ import io.micronaut.inject.InstantiatableBeanDefinition import io.micronaut.inject.ValidatedBeanDefinition import spock.lang.Specification -import javax.validation.Constraint +import jakarta.validation.Constraint import static io.micronaut.annotation.processing.test.KotlinCompiler.* @@ -25,7 +25,7 @@ import io.micronaut.context.annotation.EachProperty @EachProperty("foo.bar") interface MyConfig { - @javax.validation.constraints.NotBlank + @jakarta.validation.constraints.NotBlank fun getHost(): String fun getPort(): Int @@ -48,7 +48,7 @@ package test import io.micronaut.context.annotation.* @ConfigurationProperties("foo.bar") -class MyConfig @ConfigurationInject constructor(@javax.validation.constraints.NotBlank val host: String, val serverPort: Int) +class MyConfig @ConfigurationInject constructor(@jakarta.validation.constraints.NotBlank val host: String, val serverPort: Int) ''') def arguments = beanDefinition.constructor.arguments @@ -80,7 +80,7 @@ package test import io.micronaut.context.annotation.* @ConfigurationProperties("foo.bar") -class MyConfig @ConfigurationInject constructor(@javax.validation.constraints.NotBlank val host: String, val serverPort: Int) { +class MyConfig @ConfigurationInject constructor(@jakarta.validation.constraints.NotBlank val host: String, val serverPort: Int) { @ConfigurationProperties("baz") class ChildConfig @ConfigurationInject constructor(val stuff: String) @@ -115,7 +115,7 @@ import io.micronaut.context.annotation.*; import java.time.Duration; @EachProperty("foo.bar") -class MyConfig @ConfigurationInject constructor(@javax.validation.constraints.NotBlank val host: String, val serverPort: Int) +class MyConfig @ConfigurationInject constructor(@jakarta.validation.constraints.NotBlank val host: String, val serverPort: Int) ''', false, ['foo.bar.one.host': 'test', 'foo.bar.one.server-port': '9999']) def config = getBean(context, 'test.MyConfig') diff --git a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/inject/configproperties/InterfaceConfigurationPropertiesSpec.groovy b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/inject/configproperties/InterfaceConfigurationPropertiesSpec.groovy index af1c96216be..68fa5bad8f9 100644 --- a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/inject/configproperties/InterfaceConfigurationPropertiesSpec.groovy +++ b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/inject/configproperties/InterfaceConfigurationPropertiesSpec.groovy @@ -22,10 +22,10 @@ import io.micronaut.context.annotation.* @ConfigurationProperties("foo.bar") interface MyConfig { - @javax.validation.constraints.NotBlank + @jakarta.validation.constraints.NotBlank fun getHost(): String? - @javax.validation.constraints.Min(10L) + @jakarta.validation.constraints.Min(10L) fun getServerPort(): Int } ''') @@ -65,7 +65,7 @@ interface MyConfig { fun getHost(): String? - @javax.validation.constraints.Min(10L) + @jakarta.validation.constraints.Min(10L) fun getServerPort(): Optional @io.micronaut.core.bind.annotation.Bindable(defaultValue = "http://default") @@ -117,7 +117,7 @@ import io.micronaut.context.annotation.* interface MyConfig: ParentConfig { @Executable - @javax.validation.constraints.Min(10L) + @jakarta.validation.constraints.Min(10L) fun getServerPort(): Int } @@ -125,7 +125,7 @@ interface MyConfig: ParentConfig { interface ParentConfig { @Executable - @javax.validation.constraints.NotBlank + @jakarta.validation.constraints.NotBlank fun getHost(): String? } @@ -162,11 +162,11 @@ import java.net.URL @ConfigurationProperties("foo.bar") interface MyConfig { @Executable - @javax.validation.constraints.NotBlank + @jakarta.validation.constraints.NotBlank fun getHost(): String? @Executable - @javax.validation.constraints.Min(10L) + @jakarta.validation.constraints.Min(10L) fun getServerPort(): Int @ConfigurationProperties("child") @@ -202,11 +202,11 @@ import java.net.URL @ConfigurationProperties("foo.bar") interface MyConfig { - @javax.validation.constraints.NotBlank + @jakarta.validation.constraints.NotBlank @Executable fun getHost(): String - @javax.validation.constraints.Min(10L) + @jakarta.validation.constraints.Min(10L) @Executable fun getServerPort(): Int @@ -248,10 +248,10 @@ import io.micronaut.context.annotation.* @ConfigurationProperties("foo.bar") interface MyConfig { - @javax.validation.constraints.NotBlank + @jakarta.validation.constraints.NotBlank fun junk(s: String): String - @javax.validation.constraints.Min(10L) + @jakarta.validation.constraints.Min(10L) fun getServerPort(): Int } diff --git a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/inject/configproperties/ValidatedConfigurationSpec.groovy b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/inject/configproperties/ValidatedConfigurationSpec.groovy index add763e9d71..b8da2f899e7 100644 --- a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/inject/configproperties/ValidatedConfigurationSpec.groovy +++ b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/inject/configproperties/ValidatedConfigurationSpec.groovy @@ -61,7 +61,7 @@ package test import io.micronaut.context.annotation.ConfigurationProperties import io.micronaut.kotlin.processing.inject.configproperties.Pojo -import javax.validation.Valid +import jakarta.validation.Valid @ConfigurationProperties("test.valid") class MyConfig { diff --git a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/inject/generics/GenericTypeArgumentsSpec.groovy b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/inject/generics/GenericTypeArgumentsSpec.groovy index d341757b9d9..673601d996e 100644 --- a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/inject/generics/GenericTypeArgumentsSpec.groovy +++ b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/inject/generics/GenericTypeArgumentsSpec.groovy @@ -6,7 +6,7 @@ import io.micronaut.context.event.BeanCreatedEventListener import io.micronaut.inject.BeanDefinition import spock.lang.Unroll -import javax.validation.ConstraintViolationException +import jakarta.validation.ConstraintViolationException import java.util.function.Function import java.util.function.Supplier @@ -71,7 +71,7 @@ package exceptionhandler import io.micronaut.inject.annotation.* import io.micronaut.context.annotation.* -import javax.validation.ConstraintViolationException +import jakarta.validation.ConstraintViolationException @Context class Test : ExceptionHandler?> { diff --git a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/visitor/BeanIntrospectionSpec.groovy b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/visitor/BeanIntrospectionSpec.groovy index c9e5532887d..ce6c16908c4 100644 --- a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/visitor/BeanIntrospectionSpec.groovy +++ b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/visitor/BeanIntrospectionSpec.groovy @@ -25,10 +25,10 @@ import javax.persistence.Column import javax.persistence.Entity import javax.persistence.Id import javax.persistence.Version -import javax.validation.Constraint -import javax.validation.constraints.Min -import javax.validation.constraints.NotBlank -import javax.validation.constraints.Size +import jakarta.validation.Constraint +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotBlank +import jakarta.validation.constraints.Size import java.lang.reflect.Field class BeanIntrospectionSpec extends AbstractKotlinCompilerSpec { @@ -412,7 +412,7 @@ data class Foo(val x: Int, val y: Int) { package test import io.micronaut.core.annotation.Creator -import javax.validation.constraints.Min +import jakarta.validation.constraints.Min @io.micronaut.core.annotation.Introspected data class Foo(val value: List<@Min(10) Long>) @@ -435,7 +435,7 @@ data class Foo(val value: List<@Min(10) Long>) BeanIntrospection introspection = buildBeanIntrospection('test.Foo', ''' package test -import javax.validation.constraints.Min +import jakarta.validation.constraints.Min import kotlin.annotation.AnnotationTarget.* @io.micronaut.core.annotation.Introspected @@ -464,7 +464,7 @@ annotation class SomeAnn package test @io.micronaut.core.annotation.Introspected -data class Foo(@javax.validation.constraints.NotBlank val name: String, val age: Int) +data class Foo(@jakarta.validation.constraints.NotBlank val name: String, val age: Int) ''') when: def test = introspection.instantiate("test", 20) @@ -788,7 +788,7 @@ interface Test : io.micronaut.core.naming.Named { def classLoader = buildClassLoader('test.Address', ''' package test -import javax.validation.constraints.* +import jakarta.validation.constraints.* @io.micronaut.core.annotation.Introspected class Address { @@ -976,7 +976,7 @@ class Book { package test import io.micronaut.core.annotation.* -import javax.validation.constraints.* +import jakarta.validation.constraints.* import java.util.* import com.fasterxml.jackson.annotation.* @@ -1036,7 +1036,7 @@ class Test { package test import io.micronaut.core.annotation.* -import javax.validation.constraints.* +import jakarta.validation.constraints.* import java.util.* @Introspected @@ -1085,7 +1085,7 @@ class Test { package test import io.micronaut.core.annotation.* -import javax.validation.constraints.* +import jakarta.validation.constraints.* import java.util.* @Introspected @@ -1132,7 +1132,7 @@ class Test { def classLoader = buildClassLoader('test.Test', ''' package test -import javax.validation.constraints.* +import jakarta.validation.constraints.* import javax.persistence.* @Entity @@ -1211,7 +1211,7 @@ class Test( def classLoader = buildClassLoader('test.Test', ''' package test -import javax.validation.constraints.* +import jakarta.validation.constraints.* import javax.persistence.* @Entity @@ -1342,7 +1342,7 @@ package test import io.micronaut.core.annotation.Introspected import io.micronaut.core.convert.TypeConverter -import javax.validation.constraints.Size +import jakarta.validation.constraints.Size @Introspected class Test: ParentBean() { @@ -1851,7 +1851,7 @@ class Test { package test import io.micronaut.core.annotation.Introspected -import javax.validation.constraints.NotNull +import jakarta.validation.constraints.NotNull interface IEmail { fun getEmail(): String? diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/DeleteByIdCrudRepo.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/DeleteByIdCrudRepo.kt index afa59aaef8b..8a0f73c92c2 100644 --- a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/DeleteByIdCrudRepo.kt +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/DeleteByIdCrudRepo.kt @@ -15,7 +15,7 @@ */ package io.micronaut.kotlin.processing.aop.introduction -import javax.validation.constraints.NotNull +import jakarta.validation.constraints.NotNull interface DeleteByIdCrudRepo { diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/MyRepo2.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/MyRepo2.kt index 651a31fd14f..ee4dc68bcd0 100644 --- a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/MyRepo2.kt +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/MyRepo2.kt @@ -1,6 +1,6 @@ package io.micronaut.kotlin.processing.aop.introduction -import javax.validation.constraints.NotNull +import jakarta.validation.constraints.NotNull @RepoDef interface MyRepo2 : DeleteByIdCrudRepo { diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/beans/configproperties/Pojo.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/beans/configproperties/Pojo.kt index aeee41d218a..43967128d88 100644 --- a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/beans/configproperties/Pojo.kt +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/beans/configproperties/Pojo.kt @@ -2,8 +2,8 @@ package io.micronaut.kotlin.processing.beans.configproperties import io.micronaut.core.annotation.Introspected -import javax.validation.constraints.Email -import javax.validation.constraints.NotBlank +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.NotBlank @Introspected class Pojo { diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/beans/configproperties/ValidatedConfig.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/beans/configproperties/ValidatedConfig.kt index 5c0a45549bd..e6f6e1f8da2 100644 --- a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/beans/configproperties/ValidatedConfig.kt +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/beans/configproperties/ValidatedConfig.kt @@ -18,8 +18,8 @@ package io.micronaut.kotlin.processing.beans.configproperties; import io.micronaut.context.annotation.ConfigurationProperties import io.micronaut.context.annotation.Requires -import javax.validation.constraints.NotBlank -import javax.validation.constraints.NotNull +import jakarta.validation.constraints.NotBlank +import jakarta.validation.constraints.NotNull import java.net.URL @Requires(property = "spec.name", value = "ValidatedConfigurationSpec") diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/elementapi/TestEntity.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/elementapi/TestEntity.kt index eec2a968e22..66db7873583 100644 --- a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/elementapi/TestEntity.kt +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/elementapi/TestEntity.kt @@ -1,6 +1,6 @@ package io.micronaut.kotlin.processing.elementapi -import javax.validation.constraints.* +import jakarta.validation.constraints.* import javax.persistence.* @Entity diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/inject/configproperties/Pojo.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/inject/configproperties/Pojo.kt index baa7b01a6f6..ea5e6d6c6e9 100644 --- a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/inject/configproperties/Pojo.kt +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/inject/configproperties/Pojo.kt @@ -1,8 +1,8 @@ package io.micronaut.kotlin.processing.inject.configproperties import io.micronaut.core.annotation.Introspected -import javax.validation.constraints.Email -import javax.validation.constraints.NotBlank +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.NotBlank @Introspected class Pojo { diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/inject/configproperties/ValidatedConfig.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/inject/configproperties/ValidatedConfig.kt index b8217e6b454..49180c6e15d 100644 --- a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/inject/configproperties/ValidatedConfig.kt +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/inject/configproperties/ValidatedConfig.kt @@ -3,8 +3,8 @@ package io.micronaut.kotlin.processing.inject.configproperties import io.micronaut.context.annotation.ConfigurationProperties import io.micronaut.context.annotation.Requires import io.micronaut.core.annotation.Introspected -import javax.validation.constraints.NotBlank -import javax.validation.constraints.NotNull +import jakarta.validation.constraints.NotBlank +import jakarta.validation.constraints.NotNull import java.net.URL @Requires(property = "spec.name", value = "ValidatedConfigurationSpec") diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/inject/configproperties/itfce/MyConfig.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/inject/configproperties/itfce/MyConfig.kt index 6b6542d6370..edc8e2632b6 100644 --- a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/inject/configproperties/itfce/MyConfig.kt +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/inject/configproperties/itfce/MyConfig.kt @@ -2,7 +2,7 @@ package io.micronaut.kotlin.processing.inject.configproperties.itfce import io.micronaut.context.annotation.ConfigurationProperties import io.micronaut.context.annotation.Requires -import javax.validation.constraints.NotBlank +import jakarta.validation.constraints.NotBlank @ConfigurationProperties("my.config") @Requires(property = "my.config") diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/inject/configproperties/itfce/MyEachConfig.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/inject/configproperties/itfce/MyEachConfig.kt index d4733248853..e525e9607ad 100644 --- a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/inject/configproperties/itfce/MyEachConfig.kt +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/inject/configproperties/itfce/MyEachConfig.kt @@ -2,7 +2,7 @@ package io.micronaut.kotlin.processing.inject.configproperties.itfce import io.micronaut.context.annotation.EachProperty import io.micronaut.context.annotation.Requires -import javax.validation.constraints.NotBlank +import jakarta.validation.constraints.NotBlank @EachProperty(value = "my.config", primary = "default") @Requires(property = "my.config") diff --git a/inject/src/main/java/io/micronaut/inject/ValidatedBeanDefinition.java b/inject/src/main/java/io/micronaut/inject/ValidatedBeanDefinition.java index 54967b05de2..14e51b187ca 100644 --- a/inject/src/main/java/io/micronaut/inject/ValidatedBeanDefinition.java +++ b/inject/src/main/java/io/micronaut/inject/ValidatedBeanDefinition.java @@ -24,7 +24,7 @@ import io.micronaut.core.annotation.Nullable; /** - * A bean definition that is validated with javax.validation. + * A bean definition that is validated with jakarta.validation. * * @param The bean definition type * @author Graeme Rocher diff --git a/inject/src/test/groovy/io/micronaut/context/env/yaml/YamlPropertySourceLoaderSpec.groovy b/inject/src/test/groovy/io/micronaut/context/env/yaml/YamlPropertySourceLoaderSpec.groovy index 07ca9649b93..8e197d5b0ef 100644 --- a/inject/src/test/groovy/io/micronaut/context/env/yaml/YamlPropertySourceLoaderSpec.groovy +++ b/inject/src/test/groovy/io/micronaut/context/env/yaml/YamlPropertySourceLoaderSpec.groovy @@ -46,7 +46,6 @@ class YamlPropertySourceLoaderSpec extends Specification { @Override protected SoftServiceLoader readPropertySourceLoaders() { GroovyClassLoader gcl = new GroovyClassLoader() - gcl.addClass(YamlPropertySourceLoader) gcl.addURL(YamlPropertySourceLoader.getResource("/META-INF/services/io.micronaut.context.env.PropertySourceLoader")) return new SoftServiceLoader(PropertySourceLoader, gcl) } @@ -70,7 +69,7 @@ dataSource: pooled: true driverClassName: org.h2.Driver username: sa - password: '' + password: '' '''.bytes)) } @@ -99,7 +98,6 @@ dataSource: @Override protected SoftServiceLoader readPropertySourceLoaders() { GroovyClassLoader gcl = new GroovyClassLoader() - gcl.addClass(YamlPropertySourceLoader) gcl.addURL(YamlPropertySourceLoader.getResource("/META-INF/services/io.micronaut.context.env.PropertySourceLoader")) return new SoftServiceLoader(PropertySourceLoader, gcl) } @@ -113,7 +111,7 @@ datasources.default: {} } else if(path.endsWith("application.yml")) { return Optional.of(new ByteArrayInputStream('''\ -datasources.default: {} +datasources.default: {} '''.bytes)) } diff --git a/inject/src/test/groovy/io/micronaut/context/env/yaml/YamlPropertySourceLoaderSpec2.groovy b/inject/src/test/groovy/io/micronaut/context/env/yaml/YamlPropertySourceLoaderSpec2.groovy index 0fcf44a6625..9396d0f1548 100644 --- a/inject/src/test/groovy/io/micronaut/context/env/yaml/YamlPropertySourceLoaderSpec2.groovy +++ b/inject/src/test/groovy/io/micronaut/context/env/yaml/YamlPropertySourceLoaderSpec2.groovy @@ -48,7 +48,6 @@ class YamlPropertySourceLoaderSpec2 extends Specification { @Override protected SoftServiceLoader readPropertySourceLoaders() { GroovyClassLoader gcl = new GroovyClassLoader() - gcl.addClass(YamlPropertySourceLoader) gcl.addURL(YamlPropertySourceLoader.getResource("/META-INF/services/io.micronaut.context.env.PropertySourceLoader")) return new SoftServiceLoader(PropertySourceLoader, gcl) } diff --git a/jackson-core/src/test/groovy/io/micronaut/jackson/core/env/JsonPropertySourceLoaderSpec.groovy b/jackson-core/src/test/groovy/io/micronaut/jackson/core/env/JsonPropertySourceLoaderSpec.groovy index 7987f50cece..a67664fc0a1 100644 --- a/jackson-core/src/test/groovy/io/micronaut/jackson/core/env/JsonPropertySourceLoaderSpec.groovy +++ b/jackson-core/src/test/groovy/io/micronaut/jackson/core/env/JsonPropertySourceLoaderSpec.groovy @@ -21,6 +21,7 @@ import io.micronaut.context.env.PropertySource import io.micronaut.context.env.PropertySourceLoader import io.micronaut.core.io.service.ServiceDefinition import io.micronaut.core.io.service.SoftServiceLoader +import io.micronaut.core.reflect.ReflectionUtils import io.micronaut.core.version.SemanticVersion import spock.lang.Requires import spock.lang.Specification @@ -50,10 +51,10 @@ class JsonPropertySourceLoaderSpec extends Specification { { "pooled": true, "driverClassName": "org.h2.Driver", "username": "sa", - "password": "", - "something": [1,2] + "password": "", + "something": [1,2] } -} +} ''' } } @@ -90,7 +91,6 @@ class JsonPropertySourceLoaderSpec extends Specification { @Override protected SoftServiceLoader readPropertySourceLoaders() { GroovyClassLoader gcl = new GroovyClassLoader() - gcl.addClass(JsonPropertySourceLoader) gcl.addURL(JsonPropertySourceLoader.getResource("/META-INF/services/io.micronaut.context.env.PropertySourceLoader")) return new SoftServiceLoader(PropertySourceLoader, gcl) } @@ -102,7 +102,7 @@ class JsonPropertySourceLoaderSpec extends Specification { { "dataSource": { "jmxExport": true, "username": "sa", - "password": "test" + "password": "test" } } '''.bytes)) @@ -117,10 +117,10 @@ class JsonPropertySourceLoaderSpec extends Specification { { "pooled": true, "driverClassName": "org.h2.Driver", "username": "sa", - "password": "", - "something": [1,2] + "password": "", + "something": [1,2] } -} +} '''.bytes)) } return Optional.empty() diff --git a/management/src/main/java/io/micronaut/management/endpoint/loggers/LoggersEndpoint.java b/management/src/main/java/io/micronaut/management/endpoint/loggers/LoggersEndpoint.java index 32290137038..edc84d80c64 100644 --- a/management/src/main/java/io/micronaut/management/endpoint/loggers/LoggersEndpoint.java +++ b/management/src/main/java/io/micronaut/management/endpoint/loggers/LoggersEndpoint.java @@ -28,7 +28,7 @@ import org.reactivestreams.Publisher; import reactor.core.publisher.Mono; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotBlank; import java.util.Map; /** diff --git a/management/src/main/java/io/micronaut/management/endpoint/loggers/LoggersManager.java b/management/src/main/java/io/micronaut/management/endpoint/loggers/LoggersManager.java index d4fa57ecf5d..af93f832aa3 100644 --- a/management/src/main/java/io/micronaut/management/endpoint/loggers/LoggersManager.java +++ b/management/src/main/java/io/micronaut/management/endpoint/loggers/LoggersManager.java @@ -17,8 +17,8 @@ import org.reactivestreams.Publisher; -import javax.validation.constraints.NotBlank; -import javax.validation.constraints.NotNull; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; /** * Used to retrieve and update logger information for the {@link LoggersEndpoint}. diff --git a/management/src/main/java/io/micronaut/management/endpoint/loggers/ManagedLoggingSystem.java b/management/src/main/java/io/micronaut/management/endpoint/loggers/ManagedLoggingSystem.java index 93b816114b2..9e588d5532b 100644 --- a/management/src/main/java/io/micronaut/management/endpoint/loggers/ManagedLoggingSystem.java +++ b/management/src/main/java/io/micronaut/management/endpoint/loggers/ManagedLoggingSystem.java @@ -17,7 +17,7 @@ import io.micronaut.core.annotation.NonNull; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotBlank; import java.util.Collection; /** diff --git a/management/src/main/java/io/micronaut/management/endpoint/loggers/impl/DefaultLoggersManager.java b/management/src/main/java/io/micronaut/management/endpoint/loggers/impl/DefaultLoggersManager.java index 03669ef7eb3..6cee17f2e48 100644 --- a/management/src/main/java/io/micronaut/management/endpoint/loggers/impl/DefaultLoggersManager.java +++ b/management/src/main/java/io/micronaut/management/endpoint/loggers/impl/DefaultLoggersManager.java @@ -24,8 +24,8 @@ import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; -import javax.validation.constraints.NotBlank; -import javax.validation.constraints.NotNull; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; import java.util.Arrays; import java.util.Collection; import java.util.LinkedHashMap; diff --git a/management/src/main/java/io/micronaut/management/health/indicator/HealthResult.java b/management/src/main/java/io/micronaut/management/health/indicator/HealthResult.java index a2413767b1b..8239fc94216 100644 --- a/management/src/main/java/io/micronaut/management/health/indicator/HealthResult.java +++ b/management/src/main/java/io/micronaut/management/health/indicator/HealthResult.java @@ -20,7 +20,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import javax.validation.constraints.NotNull; +import jakarta.validation.constraints.NotNull; import java.util.HashMap; import java.util.Map; diff --git a/retry/src/main/java/io/micronaut/retry/annotation/CircuitBreaker.java b/retry/src/main/java/io/micronaut/retry/annotation/CircuitBreaker.java index fbd028455f8..fb4d070c7d7 100644 --- a/retry/src/main/java/io/micronaut/retry/annotation/CircuitBreaker.java +++ b/retry/src/main/java/io/micronaut/retry/annotation/CircuitBreaker.java @@ -17,7 +17,7 @@ import io.micronaut.context.annotation.AliasFor; -import javax.validation.constraints.Digits; +import jakarta.validation.constraints.Digits; import java.lang.annotation.Documented; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; diff --git a/retry/src/main/java/io/micronaut/retry/annotation/Retryable.java b/retry/src/main/java/io/micronaut/retry/annotation/Retryable.java index a9c7f83791a..6ea52671fff 100644 --- a/retry/src/main/java/io/micronaut/retry/annotation/Retryable.java +++ b/retry/src/main/java/io/micronaut/retry/annotation/Retryable.java @@ -20,7 +20,7 @@ import io.micronaut.context.annotation.Type; import io.micronaut.retry.intercept.DefaultRetryInterceptor; -import javax.validation.constraints.Digits; +import jakarta.validation.constraints.Digits; import java.lang.annotation.Documented; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; diff --git a/settings.gradle b/settings.gradle index 9a20f8f800c..02f0b72f202 100644 --- a/settings.gradle +++ b/settings.gradle @@ -14,7 +14,7 @@ pluginManagement { } plugins { - id 'io.micronaut.build.shared.settings' version '6.3.3' + id 'io.micronaut.build.shared.settings' version '6.3.5' } enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") diff --git a/src/main/docs/guide/aop/validation.adoc b/src/main/docs/guide/aop/validation.adoc index 8f59f5334cc..e6a79739dac 100644 --- a/src/main/docs/guide/aop/validation.adoc +++ b/src/main/docs/guide/aop/validation.adoc @@ -1,8 +1,8 @@ Validation advice is one of the most common advice types you are likely to want to use in your application. -Validation advice is built on https://beanvalidation.org/2.0/spec/[Bean Validation JSR 380], a specification of the Java API for bean validation which ensures that the properties of a bean meet specific criteria, using `javax.validation` annotations such as `@NotNull`, `@Min`, and `@Max`. +Validation advice is built on https://beanvalidation.org/2.0/spec/[Bean Validation JSR 380], a specification of the Java API for bean validation which ensures that the properties of a bean meet specific criteria, using `jakarta.validation` annotations such as `@NotNull`, `@Min`, and `@Max`. -Micronaut provides native support for the `javax.validation` annotations with the `micronaut-validation` dependency: +Micronaut provides native support for the `jakarta.validation` annotations with the `micronaut-validation` dependency: dependency::micronaut-validation[] diff --git a/src/main/docs/guide/appendix/breaks.adoc b/src/main/docs/guide/appendix/breaks.adoc index 2d7baf0f6a0..a21b53ef4b0 100644 --- a/src/main/docs/guide/appendix/breaks.adoc +++ b/src/main/docs/guide/appendix/breaks.adoc @@ -322,7 +322,7 @@ The following table summarizes the core Micronaut annotations and which are inhe When upgrading an application you may need to take action if you implement an interface or subclass a superclass and override a method. -For example the annotations defined in `javax.validation` are not inherited by default, so they must be defined again in any overridden or implemented methods. +For example the annotations defined in `jakarta.validation` are not inherited by default, so they must be defined again in any overridden or implemented methods. This behaviour grants more flexibility if you need to redefine the validation rules. Note that it is still possible to inherit validation rules through meta-annotations. See the section on <> for more information. diff --git a/src/main/docs/guide/config/configurationProperties.adoc b/src/main/docs/guide/config/configurationProperties.adoc index ebb31f7b329..6f67c54fd97 100644 --- a/src/main/docs/guide/config/configurationProperties.adoc +++ b/src/main/docs/guide/config/configurationProperties.adoc @@ -8,7 +8,7 @@ For example: snippet::io.micronaut.docs.config.properties.EngineConfig[tags="imports,class",indent=0,title="@ConfigurationProperties Example"] <1> The `@ConfigurationProperties` annotation takes the configuration prefix -<2> You can use `javax.validation` annotations to validate the configuration +<2> You can use `jakarta.validation` annotations to validate the configuration <3> Default values can be assigned to the property <4> Static inner classes can provide nested configuration <5> Optional configuration values can be wrapped in `java.util.Optional` diff --git a/src/main/docs/guide/config/immutableConfig.adoc b/src/main/docs/guide/config/immutableConfig.adoc index ad3818e21d1..2da35216398 100644 --- a/src/main/docs/guide/config/immutableConfig.adoc +++ b/src/main/docs/guide/config/immutableConfig.adoc @@ -65,9 +65,9 @@ import io.micronaut.context.annotation.ConfigurationProperties; import io.micronaut.core.annotation.AccessorsStyle; import io.micronaut.core.bind.annotation.Bindable; -import javax.validation.constraints.Min; -import javax.validation.constraints.NotBlank; -import javax.validation.constraints.NotNull; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; import java.util.Optional; @ConfigurationProperties("my.engine") // <1> diff --git a/src/main/docs/guide/httpClient/clientAnnotation.adoc b/src/main/docs/guide/httpClient/clientAnnotation.adoc index 3ad3f0c5b80..efe93f829f5 100644 --- a/src/main/docs/guide/httpClient/clientAnnotation.adoc +++ b/src/main/docs/guide/httpClient/clientAnnotation.adoc @@ -10,11 +10,11 @@ You can define a common interface for saving new `Pet` instances: snippet::io.micronaut.docs.annotation.PetOperations[tags="imports, class", indent=0, title="PetOperations.java"] -Note how the interface uses Micronaut's HTTP annotations which are usable on both the server and client side. You can also use `javax.validation` constraints to validate arguments. +Note how the interface uses Micronaut's HTTP annotations which are usable on both the server and client side. You can also use `jakarta.validation` constraints to validate arguments. TIP: Be aware that some annotations, such as api:http.annotation.Produces[] and api:http.annotation.Consumes[], have different semantics between server and client side usage. For example, `@Produces` on a controller method (server side) indicates how the method's *return value* is formatted, while `@Produces` on a client indicates how the method's *parameters* are formatted when sent to the server. While this may seem a little confusing, it is logical considering the different semantics between a server producing/consuming vs a client: a server consumes an argument and *returns* a response to the client, whereas a client consumes an argument and *sends* output to a server. -Additionally, to use the `javax.validation` features, add the `validation` module to your build: +Additionally, to use the `jakarta.validation` features, add the `validation` module to your build: dependency:micronaut-validation[] diff --git a/src/main/docs/guide/httpServer/datavalidation.adoc b/src/main/docs/guide/httpServer/datavalidation.adoc index 1d2e08617c7..08ae3388cdd 100644 --- a/src/main/docs/guide/httpServer/datavalidation.adoc +++ b/src/main/docs/guide/httpServer/datavalidation.adoc @@ -1,6 +1,6 @@ It is easy to validate incoming data with Micronaut controllers using <>. -Micronaut provides native support for the `javax.validation` annotations with the `micronaut-validation` dependency: +Micronaut provides native support for the `jakarta.validation` annotations with the `micronaut-validation` dependency: dependency::micronaut-validation[] @@ -8,14 +8,14 @@ Or full JSR 380 compliance with the `micronaut-hibernate-validator` dependency: dependency:micronaut-hibernate-validator[groupId="io.micronaut.beanvalidation"] -We can validate parameters using `javax.validation` annotations and the api:validation.Validated[] annotation at the class level. +We can validate parameters using `jakarta.validation` annotations and the api:validation.Validated[] annotation at the class level. snippet::io.micronaut.docs.datavalidation.params.EmailController[tags="imports,clazz", indent=0,title="Example"] <1> Annotate controller with api:validation.Validated[] <2> `subject` and `recipient` cannot be blank. -If a validation error occurs a `javax.validation.ConstraintViolationException` is thrown. By default, the integrated `io.micronaut.validation.exception.ConstraintExceptionHandler` handles the exception, leading to a behaviour as shown in the following test: +If a validation error occurs a `jakarta.validation.ConstraintViolationException` is thrown. By default, the integrated `io.micronaut.validation.exception.ConstraintExceptionHandler` handles the exception, leading to a behaviour as shown in the following test: snippet::io.micronaut.docs.datavalidation.params.EmailControllerSpec[tags="paramsvalidated",indent=0,title="Example Test"] @@ -25,7 +25,7 @@ Often you may want to use POJOs as controller method parameters. snippet::io.micronaut.docs.datavalidation.pogo.Email[tags="clazz", indent=0] -<1> You can use `javax.validation` annotations in your POJOs. +<1> You can use `jakarta.validation` annotations in your POJOs. Annotate your controller with api:validation.Validated[], and annotate the binding POJO with `@Valid`. diff --git a/src/main/docs/guide/httpServer/errorHandling/exceptionHandler/builtInExceptionHandlers.adoc b/src/main/docs/guide/httpServer/errorHandling/exceptionHandler/builtInExceptionHandlers.adoc index fd70e9d0270..b7631328ab6 100644 --- a/src/main/docs/guide/httpServer/errorHandling/exceptionHandler/builtInExceptionHandlers.adoc +++ b/src/main/docs/guide/httpServer/errorHandling/exceptionHandler/builtInExceptionHandlers.adoc @@ -2,7 +2,7 @@ Micronaut ships with several built-in handlers: |=== |Exception|Handler -| `javax.validation.ConstraintViolationException` +| `jakarta.validation.ConstraintViolationException` | api:validation.exceptions.ConstraintExceptionHandler[] | api:http.exceptions.ContentLengthExceededException[] | api:http.server.exceptions.ContentLengthExceededHandler[] diff --git a/src/main/docs/guide/httpServer/reactiveServer/bodyAnnotation.adoc b/src/main/docs/guide/httpServer/reactiveServer/bodyAnnotation.adoc index 86cbac74193..02a6fb1e6d4 100644 --- a/src/main/docs/guide/httpServer/reactiveServer/bodyAnnotation.adoc +++ b/src/main/docs/guide/httpServer/reactiveServer/bodyAnnotation.adoc @@ -5,7 +5,7 @@ The following example implements a simple echo server that echoes the body sent snippet::io.micronaut.docs.server.body.MessageController[tags="imports,class,echo,endclass", indent=0, title="Using the @Body annotation"] <1> The api:io.micronaut.http.annotation.Post[] annotation is used with a api:http.MediaType[] of `text/plain` (the default is `application/json`). -<2> The api:http.annotation.Body[] annotation is used with a `javax.validation.constraints.Size` that limits the size of the body to at most 1KB. This constraint does *not* limit the amount of data read/buffered by the server. +<2> The api:http.annotation.Body[] annotation is used with a `jakarta.validation.constraints.Size` that limits the size of the body to at most 1KB. This constraint does *not* limit the amount of data read/buffered by the server. <3> The body is returned as the result of the method Note that reading the request body is done in a non-blocking manner in that the request contents are read as the data becomes available and accumulated into the String passed to the method. diff --git a/src/main/docs/guide/ioc/beanValidation.adoc b/src/main/docs/guide/ioc/beanValidation.adoc index 610b4c0f779..d1fea6f546d 100644 --- a/src/main/docs/guide/ioc/beanValidation.adoc +++ b/src/main/docs/guide/ioc/beanValidation.adoc @@ -1,4 +1,4 @@ -Since Micronaut 1.2, Micronaut has built-in support for validating beans annotated with `javax.validation` annotations. At a minimum include the `micronaut-validation` module as a compile dependency: +Since Micronaut 1.2, Micronaut has built-in support for validating beans annotated with `jakarta.validation` annotations. At a minimum include the `micronaut-validation` module as a compile dependency: dependency::micronaut-validation[] @@ -9,7 +9,7 @@ The following features are unsupported at this time: * Annotations on generic argument types, since only the Java language supports this feature. * Any interaction with the https://beanvalidation.org/2.0/spec/#constraintmetadata[constraint metadata API], since Micronaut uses compile-time generated metadata. * XML-based configuration -* Instead of using `javax.validation.ConstraintValidator`, use api:validation.validator.constraints.ConstraintValidator[] (io.micronaut.validation.validator.constraints.ConstraintValidator) to define custom constraints, which supports validating annotations at compile time. +* Instead of using `jakarta.validation.ConstraintValidator`, use api:validation.validator.constraints.ConstraintValidator[] (io.micronaut.validation.validator.constraints.ConstraintValidator) to define custom constraints, which supports validating annotations at compile time. Micronaut's implementation includes the following benefits: @@ -27,7 +27,7 @@ dependency:micronaut-hibernate-validator[groupId="io.micronaut.beanvalidation"] === Validating Bean Methods -You can validate methods of any class declared as a Micronaut bean by applying `javax.validation` annotations to arguments: +You can validate methods of any class declared as a Micronaut bean by applying `jakarta.validation` annotations to arguments: .Validating Methods snippet::io.micronaut.docs.ioc.validation.PersonService[tags="imports,class",indent=0] @@ -36,7 +36,7 @@ The above example declares that the `@NotBlank` annotation will be validated whe WARNING: If you use Kotlin, the class and method must be declared `open` so Micronaut can create a compile-time subclass. Alternatively you can annotate the class with ann:validation.Validated[] and configure the Kotlin `all-open` plugin to open classes annotated with this type. See the https://kotlinlang.org/docs/reference/compiler-plugins.html[Compiler plugins] section. -A `javax.validation.ConstraintViolationException` is thrown if a validation error occurs. For example: +A `jakarta.validation.ConstraintViolationException` is thrown if a validation error occurs. For example: .ConstraintViolationException Example snippet::io.micronaut.docs.ioc.validation.PersonServiceSpec[tags="imports,test",indent=0] @@ -63,7 +63,7 @@ snippet::io.micronaut.docs.ioc.validation.pojo.PersonServiceSpec[tags="validator <1> The validator validates the person <2> The constraint violations are verified -Alternatively on Bean methods you can use `javax.validation.Valid` to trigger cascading validation: +Alternatively on Bean methods you can use `jakarta.validation.Valid` to trigger cascading validation: .ConstraintViolationException Example snippet::io.micronaut.docs.ioc.validation.pojo.PersonService[tags="class",indent="0"] @@ -89,7 +89,7 @@ To define additional constraints, create a new annotation, for example: .Example Constraint Annotation snippet::io.micronaut.docs.ioc.validation.custom.DurationPattern[tags="imports,class", indent="0"] -<1> The annotation should be annotated with `javax.validation.Constraint` +<1> The annotation should be annotated with `jakarta.validation.Constraint` <2> A `message` template can be provided in a hard-coded manner as above. If none is specified, Micronaut tries to find a message using `ClassName.message` using the api:context.MessageSource[] interface (optional) <3> To support repeated annotations you can define an inner annotation (optional) @@ -127,7 +127,7 @@ You can use Micronaut's validator to validate annotation elements at compile tim dependency::micronaut-validation[scope="annotationProcessor"] -Then Micronaut will at compile time validate annotation values that are themselves annotated with `javax.validation`. For example consider the following annotation: +Then Micronaut will at compile time validate annotation values that are themselves annotated with `jakarta.validation`. For example consider the following annotation: .Annotation Validation snippet::io.micronaut.docs.ioc.validation.custom.TimeOff[tags="imports,class", indent="0"] diff --git a/src/main/docs/guide/languageSupport/kotlin/openandaop.adoc b/src/main/docs/guide/languageSupport/kotlin/openandaop.adoc index 7a85a50f482..ac5bc21733d 100644 --- a/src/main/docs/guide/languageSupport/kotlin/openandaop.adoc +++ b/src/main/docs/guide/languageSupport/kotlin/openandaop.adoc @@ -8,7 +8,7 @@ import io.micronaut.http.annotation.Controller import io.micronaut.http.annotation.Get import io.micronaut.http.HttpStatus import io.micronaut.validation.Validated -import javax.validation.constraints.NotBlank +import jakarta.validation.constraints.NotBlank @Validated @Controller("/email") diff --git a/test-suite-groovy/src/test/groovy/io/micronaut/docs/annotation/PetControllerSpec.groovy b/test-suite-groovy/src/test/groovy/io/micronaut/docs/annotation/PetControllerSpec.groovy index 2a5f3e3096d..61c028c25d8 100644 --- a/test-suite-groovy/src/test/groovy/io/micronaut/docs/annotation/PetControllerSpec.groovy +++ b/test-suite-groovy/src/test/groovy/io/micronaut/docs/annotation/PetControllerSpec.groovy @@ -7,7 +7,7 @@ import org.junit.rules.ExpectedException import reactor.core.publisher.Mono import spock.lang.Specification -import javax.validation.ConstraintViolationException +import jakarta.validation.ConstraintViolationException class PetControllerSpec extends Specification { diff --git a/test-suite-groovy/src/test/groovy/io/micronaut/docs/annotation/PetOperations.groovy b/test-suite-groovy/src/test/groovy/io/micronaut/docs/annotation/PetOperations.groovy index d8ed2dc34f9..b9c569cc8c5 100644 --- a/test-suite-groovy/src/test/groovy/io/micronaut/docs/annotation/PetOperations.groovy +++ b/test-suite-groovy/src/test/groovy/io/micronaut/docs/annotation/PetOperations.groovy @@ -20,8 +20,8 @@ import io.micronaut.http.annotation.Post import io.micronaut.validation.Validated import org.reactivestreams.Publisher import io.micronaut.core.async.annotation.SingleResult -import javax.validation.constraints.Min -import javax.validation.constraints.NotBlank +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotBlank // end::imports[] // tag::class[] diff --git a/test-suite-groovy/src/test/groovy/io/micronaut/docs/annotation/headers/PetClient.groovy b/test-suite-groovy/src/test/groovy/io/micronaut/docs/annotation/headers/PetClient.groovy index a1b4d50f635..d7c6783f241 100644 --- a/test-suite-groovy/src/test/groovy/io/micronaut/docs/annotation/headers/PetClient.groovy +++ b/test-suite-groovy/src/test/groovy/io/micronaut/docs/annotation/headers/PetClient.groovy @@ -23,8 +23,8 @@ import io.micronaut.http.client.annotation.Client import org.reactivestreams.Publisher import reactor.core.publisher.Mono import io.micronaut.core.async.annotation.SingleResult -import javax.validation.constraints.Min -import javax.validation.constraints.NotBlank +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotBlank // tag::class[] @Client("/pets") diff --git a/test-suite-groovy/src/test/groovy/io/micronaut/docs/config/immutable/EngineConfig.groovy b/test-suite-groovy/src/test/groovy/io/micronaut/docs/config/immutable/EngineConfig.groovy index 8a8eb8d7f95..1dbf8b34e77 100644 --- a/test-suite-groovy/src/test/groovy/io/micronaut/docs/config/immutable/EngineConfig.groovy +++ b/test-suite-groovy/src/test/groovy/io/micronaut/docs/config/immutable/EngineConfig.groovy @@ -21,9 +21,9 @@ import io.micronaut.context.annotation.ConfigurationProperties import io.micronaut.core.bind.annotation.Bindable import javax.annotation.Nullable -import javax.validation.constraints.Min -import javax.validation.constraints.NotBlank -import javax.validation.constraints.NotNull +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotBlank +import jakarta.validation.constraints.NotNull // end::imports[] // tag::class[] diff --git a/test-suite-groovy/src/test/groovy/io/micronaut/docs/config/itfce/EngineConfig.groovy b/test-suite-groovy/src/test/groovy/io/micronaut/docs/config/itfce/EngineConfig.groovy index a64890762c0..47fffe7757d 100644 --- a/test-suite-groovy/src/test/groovy/io/micronaut/docs/config/itfce/EngineConfig.groovy +++ b/test-suite-groovy/src/test/groovy/io/micronaut/docs/config/itfce/EngineConfig.groovy @@ -19,9 +19,9 @@ package io.micronaut.docs.config.itfce import io.micronaut.context.annotation.ConfigurationProperties import io.micronaut.core.bind.annotation.Bindable -import javax.validation.constraints.Min -import javax.validation.constraints.NotBlank -import javax.validation.constraints.NotNull +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotBlank +import jakarta.validation.constraints.NotNull // end::imports[] // tag::class[] diff --git a/test-suite-groovy/src/test/groovy/io/micronaut/docs/config/mapFormat/EngineConfig.groovy b/test-suite-groovy/src/test/groovy/io/micronaut/docs/config/mapFormat/EngineConfig.groovy index 29020b76d03..20797804f1c 100644 --- a/test-suite-groovy/src/test/groovy/io/micronaut/docs/config/mapFormat/EngineConfig.groovy +++ b/test-suite-groovy/src/test/groovy/io/micronaut/docs/config/mapFormat/EngineConfig.groovy @@ -19,7 +19,7 @@ package io.micronaut.docs.config.mapFormat import io.micronaut.context.annotation.ConfigurationProperties import io.micronaut.core.convert.format.MapFormat -import javax.validation.constraints.Min +import jakarta.validation.constraints.Min // end::imports[] // tag::class[] diff --git a/test-suite-groovy/src/test/groovy/io/micronaut/docs/config/properties/EngineConfig.groovy b/test-suite-groovy/src/test/groovy/io/micronaut/docs/config/properties/EngineConfig.groovy index df3d43af635..ec02d274f00 100644 --- a/test-suite-groovy/src/test/groovy/io/micronaut/docs/config/properties/EngineConfig.groovy +++ b/test-suite-groovy/src/test/groovy/io/micronaut/docs/config/properties/EngineConfig.groovy @@ -18,8 +18,8 @@ package io.micronaut.docs.config.properties // tag::imports[] import io.micronaut.context.annotation.ConfigurationProperties -import javax.validation.constraints.Min -import javax.validation.constraints.NotBlank +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotBlank // end::imports[] // tag::class[] diff --git a/test-suite-groovy/src/test/groovy/io/micronaut/docs/datavalidation/groups/Email.groovy b/test-suite-groovy/src/test/groovy/io/micronaut/docs/datavalidation/groups/Email.groovy index 5452f04d4ce..df130ceee87 100644 --- a/test-suite-groovy/src/test/groovy/io/micronaut/docs/datavalidation/groups/Email.groovy +++ b/test-suite-groovy/src/test/groovy/io/micronaut/docs/datavalidation/groups/Email.groovy @@ -18,7 +18,7 @@ package io.micronaut.docs.datavalidation.groups //tag::clazz[] import io.micronaut.core.annotation.Introspected -import javax.validation.constraints.NotBlank +import jakarta.validation.constraints.NotBlank @Introspected class Email { diff --git a/test-suite-groovy/src/test/groovy/io/micronaut/docs/datavalidation/groups/EmailController.groovy b/test-suite-groovy/src/test/groovy/io/micronaut/docs/datavalidation/groups/EmailController.groovy index 123a9fa47b8..f8b1aba8d4e 100644 --- a/test-suite-groovy/src/test/groovy/io/micronaut/docs/datavalidation/groups/EmailController.groovy +++ b/test-suite-groovy/src/test/groovy/io/micronaut/docs/datavalidation/groups/EmailController.groovy @@ -23,7 +23,7 @@ import io.micronaut.http.annotation.Controller import io.micronaut.http.annotation.Post import io.micronaut.validation.Validated -import javax.validation.Valid +import jakarta.validation.Valid //end::imports[] @Requires(property = "spec.name", value = "datavalidationgroups") diff --git a/test-suite-groovy/src/test/groovy/io/micronaut/docs/datavalidation/groups/FinalValidation.groovy b/test-suite-groovy/src/test/groovy/io/micronaut/docs/datavalidation/groups/FinalValidation.groovy index eee72ad2adb..92e57cc4583 100644 --- a/test-suite-groovy/src/test/groovy/io/micronaut/docs/datavalidation/groups/FinalValidation.groovy +++ b/test-suite-groovy/src/test/groovy/io/micronaut/docs/datavalidation/groups/FinalValidation.groovy @@ -17,7 +17,7 @@ package io.micronaut.docs.datavalidation.groups; //tag::clazz[] -import javax.validation.groups.Default +import jakarta.validation.groups.Default interface FinalValidation extends Default {} // <1> diff --git a/test-suite-groovy/src/test/groovy/io/micronaut/docs/datavalidation/params/EmailController.groovy b/test-suite-groovy/src/test/groovy/io/micronaut/docs/datavalidation/params/EmailController.groovy index d6abe54fcf7..ac4b007a292 100644 --- a/test-suite-groovy/src/test/groovy/io/micronaut/docs/datavalidation/params/EmailController.groovy +++ b/test-suite-groovy/src/test/groovy/io/micronaut/docs/datavalidation/params/EmailController.groovy @@ -22,7 +22,7 @@ import io.micronaut.http.annotation.Controller import io.micronaut.http.annotation.Get import io.micronaut.validation.Validated -import javax.validation.constraints.NotBlank +import jakarta.validation.constraints.NotBlank //end::imports[] @Requires(property = "spec.name", value = "datavalidationparams") diff --git a/test-suite-groovy/src/test/groovy/io/micronaut/docs/datavalidation/pogo/Email.groovy b/test-suite-groovy/src/test/groovy/io/micronaut/docs/datavalidation/pogo/Email.groovy index c5ee5293b3a..3346b38b46f 100644 --- a/test-suite-groovy/src/test/groovy/io/micronaut/docs/datavalidation/pogo/Email.groovy +++ b/test-suite-groovy/src/test/groovy/io/micronaut/docs/datavalidation/pogo/Email.groovy @@ -18,7 +18,7 @@ package io.micronaut.docs.datavalidation.pogo //tag::clazz[] import io.micronaut.core.annotation.Introspected -import javax.validation.constraints.NotBlank +import jakarta.validation.constraints.NotBlank @Introspected class Email { diff --git a/test-suite-groovy/src/test/groovy/io/micronaut/docs/datavalidation/pogo/EmailController.groovy b/test-suite-groovy/src/test/groovy/io/micronaut/docs/datavalidation/pogo/EmailController.groovy index 5c341fa2b5c..7d0b2c5ae1d 100644 --- a/test-suite-groovy/src/test/groovy/io/micronaut/docs/datavalidation/pogo/EmailController.groovy +++ b/test-suite-groovy/src/test/groovy/io/micronaut/docs/datavalidation/pogo/EmailController.groovy @@ -23,7 +23,7 @@ import io.micronaut.http.annotation.Controller import io.micronaut.http.annotation.Post import io.micronaut.validation.Validated -import javax.validation.Valid +import jakarta.validation.Valid //end::imports[] @Requires(property = "spec.name", value = "datavalidationpogo") diff --git a/test-suite-groovy/src/test/groovy/io/micronaut/docs/factories/nullable/EngineConfiguration.groovy b/test-suite-groovy/src/test/groovy/io/micronaut/docs/factories/nullable/EngineConfiguration.groovy index ad7968e5946..b5375067107 100644 --- a/test-suite-groovy/src/test/groovy/io/micronaut/docs/factories/nullable/EngineConfiguration.groovy +++ b/test-suite-groovy/src/test/groovy/io/micronaut/docs/factories/nullable/EngineConfiguration.groovy @@ -18,7 +18,7 @@ package io.micronaut.docs.factories.nullable import io.micronaut.context.annotation.EachProperty import io.micronaut.core.util.Toggleable -import javax.validation.constraints.NotNull +import jakarta.validation.constraints.NotNull // tag::class[] @EachProperty("engines") diff --git a/test-suite-groovy/src/test/groovy/io/micronaut/docs/ioc/validation/Person.groovy b/test-suite-groovy/src/test/groovy/io/micronaut/docs/ioc/validation/Person.groovy index 92c8a4c0e89..b2db905faea 100644 --- a/test-suite-groovy/src/test/groovy/io/micronaut/docs/ioc/validation/Person.groovy +++ b/test-suite-groovy/src/test/groovy/io/micronaut/docs/ioc/validation/Person.groovy @@ -18,8 +18,8 @@ package io.micronaut.docs.ioc.validation // tag::class[] import io.micronaut.core.annotation.Introspected -import javax.validation.constraints.Min -import javax.validation.constraints.NotBlank +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotBlank @Introspected class Person { diff --git a/test-suite-groovy/src/test/groovy/io/micronaut/docs/ioc/validation/PersonService.groovy b/test-suite-groovy/src/test/groovy/io/micronaut/docs/ioc/validation/PersonService.groovy index 60288cc104e..af9779a83d5 100644 --- a/test-suite-groovy/src/test/groovy/io/micronaut/docs/ioc/validation/PersonService.groovy +++ b/test-suite-groovy/src/test/groovy/io/micronaut/docs/ioc/validation/PersonService.groovy @@ -17,7 +17,7 @@ package io.micronaut.docs.ioc.validation // tag::imports[] import jakarta.inject.Singleton -import javax.validation.constraints.NotBlank +import jakarta.validation.constraints.NotBlank // end::imports[] // tag::class[] diff --git a/test-suite-groovy/src/test/groovy/io/micronaut/docs/ioc/validation/PersonServiceSpec.groovy b/test-suite-groovy/src/test/groovy/io/micronaut/docs/ioc/validation/PersonServiceSpec.groovy index ce2f18b3d28..df2c6a386ff 100644 --- a/test-suite-groovy/src/test/groovy/io/micronaut/docs/ioc/validation/PersonServiceSpec.groovy +++ b/test-suite-groovy/src/test/groovy/io/micronaut/docs/ioc/validation/PersonServiceSpec.groovy @@ -5,7 +5,7 @@ import io.micronaut.test.extensions.spock.annotation.MicronautTest import spock.lang.Specification import jakarta.inject.Inject -import javax.validation.ConstraintViolationException +import jakarta.validation.ConstraintViolationException // end::imports[] // tag::test[] diff --git a/test-suite-groovy/src/test/groovy/io/micronaut/docs/ioc/validation/custom/DurationPattern.groovy b/test-suite-groovy/src/test/groovy/io/micronaut/docs/ioc/validation/custom/DurationPattern.groovy index 7e3a6f020fc..20ea1e28e4a 100644 --- a/test-suite-groovy/src/test/groovy/io/micronaut/docs/ioc/validation/custom/DurationPattern.groovy +++ b/test-suite-groovy/src/test/groovy/io/micronaut/docs/ioc/validation/custom/DurationPattern.groovy @@ -16,7 +16,7 @@ package io.micronaut.docs.ioc.validation.custom // tag::imports[] -import javax.validation.Constraint +import jakarta.validation.Constraint import java.lang.annotation.Retention import static java.lang.annotation.RetentionPolicy.RUNTIME diff --git a/test-suite-groovy/src/test/groovy/io/micronaut/docs/ioc/validation/custom/DurationPatternValidatorSpec.groovy b/test-suite-groovy/src/test/groovy/io/micronaut/docs/ioc/validation/custom/DurationPatternValidatorSpec.groovy index 17ff66afe0b..5f397a85daa 100644 --- a/test-suite-groovy/src/test/groovy/io/micronaut/docs/ioc/validation/custom/DurationPatternValidatorSpec.groovy +++ b/test-suite-groovy/src/test/groovy/io/micronaut/docs/ioc/validation/custom/DurationPatternValidatorSpec.groovy @@ -4,7 +4,7 @@ import io.micronaut.test.extensions.spock.annotation.MicronautTest import spock.lang.Specification import jakarta.inject.Inject -import javax.validation.ConstraintViolationException +import jakarta.validation.ConstraintViolationException @MicronautTest class DurationPatternValidatorSpec extends Specification { diff --git a/test-suite-groovy/src/test/groovy/io/micronaut/docs/ioc/validation/custom/HolidayService.groovy b/test-suite-groovy/src/test/groovy/io/micronaut/docs/ioc/validation/custom/HolidayService.groovy index 3fcaf0a8e88..0f35a1c9fca 100644 --- a/test-suite-groovy/src/test/groovy/io/micronaut/docs/ioc/validation/custom/HolidayService.groovy +++ b/test-suite-groovy/src/test/groovy/io/micronaut/docs/ioc/validation/custom/HolidayService.groovy @@ -16,7 +16,7 @@ package io.micronaut.docs.ioc.validation.custom import jakarta.inject.Singleton -import javax.validation.constraints.NotBlank +import jakarta.validation.constraints.NotBlank import java.time.Duration // tag::class[] diff --git a/test-suite-groovy/src/test/groovy/io/micronaut/docs/ioc/validation/pojo/PersonService.groovy b/test-suite-groovy/src/test/groovy/io/micronaut/docs/ioc/validation/pojo/PersonService.groovy index c4042b9f7fc..0accc868dcf 100644 --- a/test-suite-groovy/src/test/groovy/io/micronaut/docs/ioc/validation/pojo/PersonService.groovy +++ b/test-suite-groovy/src/test/groovy/io/micronaut/docs/ioc/validation/pojo/PersonService.groovy @@ -19,7 +19,7 @@ package io.micronaut.docs.ioc.validation.pojo import io.micronaut.docs.ioc.validation.Person import jakarta.inject.Singleton -import javax.validation.Valid +import jakarta.validation.Valid // end::imports[] // tag::class[] diff --git a/test-suite-groovy/src/test/groovy/io/micronaut/docs/ioc/validation/pojo/PersonServiceSpec.groovy b/test-suite-groovy/src/test/groovy/io/micronaut/docs/ioc/validation/pojo/PersonServiceSpec.groovy index 9511a9df64c..e9849764b4c 100644 --- a/test-suite-groovy/src/test/groovy/io/micronaut/docs/ioc/validation/pojo/PersonServiceSpec.groovy +++ b/test-suite-groovy/src/test/groovy/io/micronaut/docs/ioc/validation/pojo/PersonServiceSpec.groovy @@ -8,7 +8,7 @@ import io.micronaut.validation.validator.Validator import spock.lang.Specification import jakarta.inject.Inject -import javax.validation.ConstraintViolationException +import jakarta.validation.ConstraintViolationException // end::imports[] // tag::test[] diff --git a/test-suite-groovy/src/test/groovy/io/micronaut/docs/server/binding/BookmarkController.groovy b/test-suite-groovy/src/test/groovy/io/micronaut/docs/server/binding/BookmarkController.groovy index 6d2490bdbf1..d5056e0354a 100644 --- a/test-suite-groovy/src/test/groovy/io/micronaut/docs/server/binding/BookmarkController.groovy +++ b/test-suite-groovy/src/test/groovy/io/micronaut/docs/server/binding/BookmarkController.groovy @@ -21,7 +21,7 @@ import io.micronaut.http.annotation.Controller import io.micronaut.http.annotation.Get import javax.annotation.Nullable -import javax.validation.Valid +import jakarta.validation.Valid // end::imports[] // tag::class[] diff --git a/test-suite-groovy/src/test/groovy/io/micronaut/docs/server/binding/MovieTicketBean.groovy b/test-suite-groovy/src/test/groovy/io/micronaut/docs/server/binding/MovieTicketBean.groovy index 94b71cabe57..90a06e9a006 100644 --- a/test-suite-groovy/src/test/groovy/io/micronaut/docs/server/binding/MovieTicketBean.groovy +++ b/test-suite-groovy/src/test/groovy/io/micronaut/docs/server/binding/MovieTicketBean.groovy @@ -22,7 +22,7 @@ import io.micronaut.http.annotation.PathVariable import io.micronaut.http.annotation.QueryValue import javax.annotation.Nullable -import javax.validation.constraints.PositiveOrZero +import jakarta.validation.constraints.PositiveOrZero // end::imports[] // tag::class[] diff --git a/test-suite-groovy/src/test/groovy/io/micronaut/docs/server/binding/MovieTicketController.groovy b/test-suite-groovy/src/test/groovy/io/micronaut/docs/server/binding/MovieTicketController.groovy index 08dc11112d0..663f101db3d 100644 --- a/test-suite-groovy/src/test/groovy/io/micronaut/docs/server/binding/MovieTicketController.groovy +++ b/test-suite-groovy/src/test/groovy/io/micronaut/docs/server/binding/MovieTicketController.groovy @@ -21,7 +21,7 @@ import io.micronaut.http.annotation.Controller import io.micronaut.http.annotation.Get import io.micronaut.http.annotation.RequestBean -import javax.validation.Valid +import jakarta.validation.Valid // end::imports[] // tag::class[] diff --git a/test-suite-groovy/src/test/groovy/io/micronaut/docs/server/binding/PaginationCommand.groovy b/test-suite-groovy/src/test/groovy/io/micronaut/docs/server/binding/PaginationCommand.groovy index e2379ee3530..c73bb99233b 100644 --- a/test-suite-groovy/src/test/groovy/io/micronaut/docs/server/binding/PaginationCommand.groovy +++ b/test-suite-groovy/src/test/groovy/io/micronaut/docs/server/binding/PaginationCommand.groovy @@ -18,9 +18,9 @@ package io.micronaut.docs.server.binding; import io.micronaut.core.annotation.Introspected import javax.annotation.Nullable -import javax.validation.constraints.Pattern -import javax.validation.constraints.Positive -import javax.validation.constraints.PositiveOrZero +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Positive +import jakarta.validation.constraints.PositiveOrZero @Introspected class PaginationCommand { diff --git a/test-suite-groovy/src/test/groovy/io/micronaut/docs/server/body/MessageController.groovy b/test-suite-groovy/src/test/groovy/io/micronaut/docs/server/body/MessageController.groovy index 4fe4ec28227..13c3d0a7a55 100644 --- a/test-suite-groovy/src/test/groovy/io/micronaut/docs/server/body/MessageController.groovy +++ b/test-suite-groovy/src/test/groovy/io/micronaut/docs/server/body/MessageController.groovy @@ -21,7 +21,7 @@ import io.micronaut.http.MediaType import io.micronaut.http.annotation.Body import io.micronaut.http.annotation.Controller import io.micronaut.http.annotation.Post -import javax.validation.constraints.Size +import jakarta.validation.constraints.Size // end::imports[] // tag::importsreactive[] import org.reactivestreams.Publisher diff --git a/test-suite-groovy/src/test/groovy/io/micronaut/docs/session/ShoppingController.groovy b/test-suite-groovy/src/test/groovy/io/micronaut/docs/session/ShoppingController.groovy index 5c5c9ff9f92..6c623cd9c9f 100644 --- a/test-suite-groovy/src/test/groovy/io/micronaut/docs/session/ShoppingController.groovy +++ b/test-suite-groovy/src/test/groovy/io/micronaut/docs/session/ShoppingController.groovy @@ -23,7 +23,7 @@ import io.micronaut.session.Session import io.micronaut.session.annotation.SessionValue import javax.annotation.Nullable -import javax.validation.constraints.NotBlank +import jakarta.validation.constraints.NotBlank // end::imports[] // tag::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/annotation/PetControllerSpec.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/annotation/PetControllerSpec.kt index 3e60c89d5da..35140f1f7e9 100644 --- a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/annotation/PetControllerSpec.kt +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/annotation/PetControllerSpec.kt @@ -7,7 +7,7 @@ import io.micronaut.http.client.HttpClient import io.micronaut.runtime.server.EmbeddedServer import reactor.core.publisher.Mono -import javax.validation.ConstraintViolationException +import jakarta.validation.ConstraintViolationException import java.lang.Exception diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/annotation/PetOperations.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/annotation/PetOperations.kt index 19816ea8a64..926cdbd3454 100644 --- a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/annotation/PetOperations.kt +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/annotation/PetOperations.kt @@ -18,8 +18,8 @@ package io.micronaut.docs.annotation // tag::imports[] import io.micronaut.http.annotation.Post import io.micronaut.validation.Validated -import javax.validation.constraints.Min -import javax.validation.constraints.NotBlank +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotBlank import io.micronaut.core.async.annotation.SingleResult import org.reactivestreams.Publisher // end::imports[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/immutable/EngineConfig.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/immutable/EngineConfig.kt index 04a8ecd1824..4b5f1dba68b 100644 --- a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/immutable/EngineConfig.kt +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/immutable/EngineConfig.kt @@ -20,9 +20,9 @@ import io.micronaut.context.annotation.ConfigurationInject import io.micronaut.context.annotation.ConfigurationProperties import io.micronaut.core.bind.annotation.Bindable import java.util.Optional -import javax.validation.constraints.Min -import javax.validation.constraints.NotBlank -import javax.validation.constraints.NotNull +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotBlank +import jakarta.validation.constraints.NotNull // end::imports[] // tag::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/itfce/EngineConfig.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/itfce/EngineConfig.kt index 2cc9e4d4961..45aca3f726a 100644 --- a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/itfce/EngineConfig.kt +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/itfce/EngineConfig.kt @@ -18,9 +18,9 @@ package io.micronaut.docs.config.itfce // tag::imports[] import io.micronaut.context.annotation.ConfigurationProperties import io.micronaut.core.bind.annotation.Bindable -import javax.validation.constraints.Min -import javax.validation.constraints.NotBlank -import javax.validation.constraints.NotNull +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotBlank +import jakarta.validation.constraints.NotNull // end::imports[] // tag::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/mapFormat/EngineConfig.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/mapFormat/EngineConfig.kt index ceb841f3c77..578e4d9850d 100644 --- a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/mapFormat/EngineConfig.kt +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/mapFormat/EngineConfig.kt @@ -18,7 +18,7 @@ package io.micronaut.docs.config.mapFormat // tag::imports[] import io.micronaut.context.annotation.ConfigurationProperties import io.micronaut.core.convert.format.MapFormat -import javax.validation.constraints.Min +import jakarta.validation.constraints.Min // end::imports[] // tag::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/properties/EngineConfig.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/properties/EngineConfig.kt index 198beb4f6ef..cd398ad4461 100644 --- a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/properties/EngineConfig.kt +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/properties/EngineConfig.kt @@ -18,8 +18,8 @@ package io.micronaut.docs.config.properties // tag::imports[] import io.micronaut.context.annotation.ConfigurationProperties import java.util.Optional -import javax.validation.constraints.Min -import javax.validation.constraints.NotBlank +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotBlank // end::imports[] // tag::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/datavalidation/groups/Email.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/datavalidation/groups/Email.kt index db0e6ef981f..384e3086aaf 100644 --- a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/datavalidation/groups/Email.kt +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/datavalidation/groups/Email.kt @@ -17,7 +17,7 @@ package io.micronaut.docs.datavalidation.groups //tag::clazz[] import io.micronaut.core.annotation.Introspected -import javax.validation.constraints.NotBlank +import jakarta.validation.constraints.NotBlank @Introspected open class Email { diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/datavalidation/groups/EmailController.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/datavalidation/groups/EmailController.kt index 3bb5439d8ed..515daadde22 100644 --- a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/datavalidation/groups/EmailController.kt +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/datavalidation/groups/EmailController.kt @@ -22,7 +22,7 @@ import io.micronaut.http.annotation.Body import io.micronaut.http.annotation.Controller import io.micronaut.http.annotation.Post import io.micronaut.validation.Validated -import javax.validation.Valid +import jakarta.validation.Valid //end::imports[] @Requires(property = "spec.name", value = "datavalidationgroups") diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/datavalidation/groups/FinalValidation.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/datavalidation/groups/FinalValidation.kt index a8fe62a3e1d..317bcfe73c1 100644 --- a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/datavalidation/groups/FinalValidation.kt +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/datavalidation/groups/FinalValidation.kt @@ -17,7 +17,7 @@ package io.micronaut.docs.datavalidation.groups //tag::clazz[] -import javax.validation.groups.Default +import jakarta.validation.groups.Default interface FinalValidation : Default {} // <1> diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/datavalidation/params/EmailController.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/datavalidation/params/EmailController.kt index 66bd525b9c9..1502a0aae23 100644 --- a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/datavalidation/params/EmailController.kt +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/datavalidation/params/EmailController.kt @@ -21,7 +21,7 @@ import io.micronaut.http.HttpResponse import io.micronaut.http.annotation.Controller import io.micronaut.http.annotation.Get import io.micronaut.validation.Validated -import javax.validation.constraints.NotBlank +import jakarta.validation.constraints.NotBlank //end::imports[] @Requires(property = "spec.name", value = "datavalidationparams") diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/datavalidation/pogo/Email.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/datavalidation/pogo/Email.kt index 970db1d7b7b..d6f023d5891 100644 --- a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/datavalidation/pogo/Email.kt +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/datavalidation/pogo/Email.kt @@ -17,7 +17,7 @@ package io.micronaut.docs.datavalidation.pogo //tag::clazz[] import io.micronaut.core.annotation.Introspected -import javax.validation.constraints.NotBlank +import jakarta.validation.constraints.NotBlank @Introspected open class Email { diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/datavalidation/pogo/EmailController.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/datavalidation/pogo/EmailController.kt index 7933d0c764a..f8161550bcc 100644 --- a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/datavalidation/pogo/EmailController.kt +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/datavalidation/pogo/EmailController.kt @@ -22,7 +22,7 @@ import io.micronaut.http.annotation.Body import io.micronaut.http.annotation.Controller import io.micronaut.http.annotation.Post import io.micronaut.validation.Validated -import javax.validation.Valid +import jakarta.validation.Valid //end::imports[] @Requires(property = "spec.name", value = "datavalidationpogo") diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/factories/nullable/EngineConfiguration.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/factories/nullable/EngineConfiguration.kt index 9f925b06b0b..4f6ce251606 100644 --- a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/factories/nullable/EngineConfiguration.kt +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/factories/nullable/EngineConfiguration.kt @@ -17,7 +17,7 @@ package io.micronaut.docs.factories.nullable import io.micronaut.context.annotation.EachProperty import io.micronaut.core.util.Toggleable -import javax.validation.constraints.NotNull +import jakarta.validation.constraints.NotNull // tag::class[] @EachProperty("engines") diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/ioc/validation/Person.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/ioc/validation/Person.kt index b869cf98c45..6af4f23b969 100644 --- a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/ioc/validation/Person.kt +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/ioc/validation/Person.kt @@ -17,12 +17,12 @@ package io.micronaut.docs.ioc.validation // tag::class[] import io.micronaut.core.annotation.Introspected -import javax.validation.constraints.Min -import javax.validation.constraints.NotBlank +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotBlank @Introspected data class Person( @field:NotBlank var name: String, @field:Min(18) var age: Int ) -// end::class[] \ No newline at end of file +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/ioc/validation/PersonService.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/ioc/validation/PersonService.kt index dd1e2e9c230..c767e1d3954 100644 --- a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/ioc/validation/PersonService.kt +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/ioc/validation/PersonService.kt @@ -17,7 +17,7 @@ package io.micronaut.docs.ioc.validation // tag::imports[] import jakarta.inject.Singleton -import javax.validation.constraints.NotBlank +import jakarta.validation.constraints.NotBlank // end::imports[] // tag::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/ioc/validation/PersonServiceSpec.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/ioc/validation/PersonServiceSpec.kt index c248f7c2949..c764e8e0493 100644 --- a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/ioc/validation/PersonServiceSpec.kt +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/ioc/validation/PersonServiceSpec.kt @@ -6,7 +6,7 @@ import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertThrows import org.junit.jupiter.api.Test import jakarta.inject.Inject -import javax.validation.ConstraintViolationException +import jakarta.validation.ConstraintViolationException // end::imports[] // tag::test[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/ioc/validation/custom/DurationPattern.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/ioc/validation/custom/DurationPattern.kt index f166300ac6b..0c474144612 100644 --- a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/ioc/validation/custom/DurationPattern.kt +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/ioc/validation/custom/DurationPattern.kt @@ -16,7 +16,7 @@ package io.micronaut.docs.ioc.validation.custom // tag::imports[] -import javax.validation.Constraint +import jakarta.validation.Constraint import kotlin.annotation.AnnotationRetention.RUNTIME // end::imports[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/ioc/validation/custom/DurationPatternValidatorSpec.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/ioc/validation/custom/DurationPatternValidatorSpec.kt index f8a6b0e2fc1..6c7ab8c1427 100644 --- a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/ioc/validation/custom/DurationPatternValidatorSpec.kt +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/ioc/validation/custom/DurationPatternValidatorSpec.kt @@ -5,7 +5,7 @@ import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertThrows import org.junit.jupiter.api.Test import jakarta.inject.Inject -import javax.validation.ConstraintViolationException +import jakarta.validation.ConstraintViolationException @MicronautTest internal class DurationPatternValidatorSpec { diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/ioc/validation/custom/HolidayService.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/ioc/validation/custom/HolidayService.kt index 8e70b21d637..e43d19ca1cf 100644 --- a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/ioc/validation/custom/HolidayService.kt +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/ioc/validation/custom/HolidayService.kt @@ -17,7 +17,7 @@ package io.micronaut.docs.ioc.validation.custom import java.time.Duration import jakarta.inject.Singleton -import javax.validation.constraints.NotBlank +import jakarta.validation.constraints.NotBlank // tag::class[] @Singleton diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/ioc/validation/pojo/PersonService.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/ioc/validation/pojo/PersonService.kt index 9e463115a99..3856aed9938 100644 --- a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/ioc/validation/pojo/PersonService.kt +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/ioc/validation/pojo/PersonService.kt @@ -19,7 +19,7 @@ package io.micronaut.docs.ioc.validation.pojo import io.micronaut.docs.ioc.validation.Person import jakarta.inject.Singleton -import javax.validation.Valid +import jakarta.validation.Valid // end::imports[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/ioc/validation/pojo/PersonServiceSpec.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/ioc/validation/pojo/PersonServiceSpec.kt index 7e9932adc1b..588b5db4d70 100644 --- a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/ioc/validation/pojo/PersonServiceSpec.kt +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/ioc/validation/pojo/PersonServiceSpec.kt @@ -8,7 +8,7 @@ import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertThrows import org.junit.jupiter.api.Test import jakarta.inject.Inject -import javax.validation.ConstraintViolationException +import jakarta.validation.ConstraintViolationException // end::imports[] // tag::test[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/binding/BookmarkController.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/binding/BookmarkController.kt index dc3f37b025e..3d34f0a3204 100644 --- a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/binding/BookmarkController.kt +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/binding/BookmarkController.kt @@ -19,7 +19,7 @@ package io.micronaut.docs.server.binding import io.micronaut.http.HttpStatus import io.micronaut.http.annotation.Controller import io.micronaut.http.annotation.Get -import javax.validation.Valid +import jakarta.validation.Valid // end::imports[] // tag::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/binding/MovieTicketBean.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/binding/MovieTicketBean.kt index e633ca5c60d..b30d691b348 100644 --- a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/binding/MovieTicketBean.kt +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/binding/MovieTicketBean.kt @@ -21,7 +21,7 @@ import io.micronaut.http.HttpRequest import io.micronaut.http.annotation.PathVariable import io.micronaut.http.annotation.QueryValue import javax.annotation.Nullable -import javax.validation.constraints.PositiveOrZero +import jakarta.validation.constraints.PositiveOrZero // end::imports[] // tag::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/binding/MovieTicketController.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/binding/MovieTicketController.kt index a93fe966291..4a06c3047f2 100644 --- a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/binding/MovieTicketController.kt +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/binding/MovieTicketController.kt @@ -20,7 +20,7 @@ import io.micronaut.http.HttpStatus import io.micronaut.http.annotation.Controller import io.micronaut.http.annotation.Get import io.micronaut.http.annotation.RequestBean -import javax.validation.Valid +import jakarta.validation.Valid // end::imports[] // tag::class[] @@ -35,4 +35,4 @@ open class MovieTicketController { } } -// end::class[] \ No newline at end of file +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/binding/PaginationCommand.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/binding/PaginationCommand.kt index 3396c8824d8..257c08c3e27 100644 --- a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/binding/PaginationCommand.kt +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/binding/PaginationCommand.kt @@ -17,9 +17,9 @@ package io.micronaut.docs.server.binding // tag::imports[] import io.micronaut.core.annotation.Introspected -import javax.validation.constraints.Pattern -import javax.validation.constraints.Positive -import javax.validation.constraints.PositiveOrZero +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Positive +import jakarta.validation.constraints.PositiveOrZero // end::imports[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/body/MessageController.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/body/MessageController.kt index b6079d18794..59e6fb6571d 100644 --- a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/body/MessageController.kt +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/body/MessageController.kt @@ -21,7 +21,7 @@ import io.micronaut.http.MediaType import io.micronaut.http.annotation.Body import io.micronaut.http.annotation.Controller import io.micronaut.http.annotation.Post -import javax.validation.constraints.Size +import jakarta.validation.constraints.Size // end::imports[] // tag::importsreactive[] import org.reactivestreams.Publisher diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/validation/validator/Person.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/validation/validator/Person.kt index 1b3da56f2c6..a920551581b 100644 --- a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/validation/validator/Person.kt +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/validation/validator/Person.kt @@ -16,11 +16,11 @@ package io.micronaut.validation.validator import io.micronaut.core.annotation.Introspected -import javax.validation.constraints.Min -import javax.validation.constraints.NotBlank +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotBlank @Introspected data class Person( @NotBlank var name: String, @Min(18) var age: Int -) \ No newline at end of file +) diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/validation/validator/ValidatorSpec.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/validation/validator/ValidatorSpec.kt index e29239dd2b6..8fd7b4f06e7 100644 --- a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/validation/validator/ValidatorSpec.kt +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/validation/validator/ValidatorSpec.kt @@ -4,7 +4,7 @@ import io.micronaut.context.ApplicationContext import org.junit.jupiter.api.Test import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.fail -import javax.validation.ConstraintViolationException +import jakarta.validation.ConstraintViolationException class ValidatorSpec { @@ -33,4 +33,4 @@ class ValidatorSpec { } context.close() } -} \ No newline at end of file +} diff --git a/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/annotation/PetControllerSpec.kt b/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/annotation/PetControllerSpec.kt index 3e60c89d5da..35140f1f7e9 100644 --- a/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/annotation/PetControllerSpec.kt +++ b/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/annotation/PetControllerSpec.kt @@ -7,7 +7,7 @@ import io.micronaut.http.client.HttpClient import io.micronaut.runtime.server.EmbeddedServer import reactor.core.publisher.Mono -import javax.validation.ConstraintViolationException +import jakarta.validation.ConstraintViolationException import java.lang.Exception diff --git a/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/annotation/PetOperations.kt b/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/annotation/PetOperations.kt index 19816ea8a64..926cdbd3454 100644 --- a/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/annotation/PetOperations.kt +++ b/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/annotation/PetOperations.kt @@ -18,8 +18,8 @@ package io.micronaut.docs.annotation // tag::imports[] import io.micronaut.http.annotation.Post import io.micronaut.validation.Validated -import javax.validation.constraints.Min -import javax.validation.constraints.NotBlank +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotBlank import io.micronaut.core.async.annotation.SingleResult import org.reactivestreams.Publisher // end::imports[] diff --git a/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/config/immutable/EngineConfig.kt b/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/config/immutable/EngineConfig.kt index 04a8ecd1824..4b5f1dba68b 100644 --- a/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/config/immutable/EngineConfig.kt +++ b/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/config/immutable/EngineConfig.kt @@ -20,9 +20,9 @@ import io.micronaut.context.annotation.ConfigurationInject import io.micronaut.context.annotation.ConfigurationProperties import io.micronaut.core.bind.annotation.Bindable import java.util.Optional -import javax.validation.constraints.Min -import javax.validation.constraints.NotBlank -import javax.validation.constraints.NotNull +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotBlank +import jakarta.validation.constraints.NotNull // end::imports[] // tag::class[] diff --git a/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/config/itfce/EngineConfig.kt b/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/config/itfce/EngineConfig.kt index 2cc9e4d4961..45aca3f726a 100644 --- a/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/config/itfce/EngineConfig.kt +++ b/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/config/itfce/EngineConfig.kt @@ -18,9 +18,9 @@ package io.micronaut.docs.config.itfce // tag::imports[] import io.micronaut.context.annotation.ConfigurationProperties import io.micronaut.core.bind.annotation.Bindable -import javax.validation.constraints.Min -import javax.validation.constraints.NotBlank -import javax.validation.constraints.NotNull +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotBlank +import jakarta.validation.constraints.NotNull // end::imports[] // tag::class[] diff --git a/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/config/mapFormat/EngineConfig.kt b/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/config/mapFormat/EngineConfig.kt index ceb841f3c77..578e4d9850d 100644 --- a/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/config/mapFormat/EngineConfig.kt +++ b/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/config/mapFormat/EngineConfig.kt @@ -18,7 +18,7 @@ package io.micronaut.docs.config.mapFormat // tag::imports[] import io.micronaut.context.annotation.ConfigurationProperties import io.micronaut.core.convert.format.MapFormat -import javax.validation.constraints.Min +import jakarta.validation.constraints.Min // end::imports[] // tag::class[] diff --git a/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/config/properties/EngineConfig.kt b/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/config/properties/EngineConfig.kt index 198beb4f6ef..cd398ad4461 100644 --- a/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/config/properties/EngineConfig.kt +++ b/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/config/properties/EngineConfig.kt @@ -18,8 +18,8 @@ package io.micronaut.docs.config.properties // tag::imports[] import io.micronaut.context.annotation.ConfigurationProperties import java.util.Optional -import javax.validation.constraints.Min -import javax.validation.constraints.NotBlank +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotBlank // end::imports[] // tag::class[] diff --git a/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/datavalidation/groups/Email.kt b/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/datavalidation/groups/Email.kt index db0e6ef981f..384e3086aaf 100644 --- a/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/datavalidation/groups/Email.kt +++ b/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/datavalidation/groups/Email.kt @@ -17,7 +17,7 @@ package io.micronaut.docs.datavalidation.groups //tag::clazz[] import io.micronaut.core.annotation.Introspected -import javax.validation.constraints.NotBlank +import jakarta.validation.constraints.NotBlank @Introspected open class Email { diff --git a/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/datavalidation/groups/EmailController.kt b/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/datavalidation/groups/EmailController.kt index 3bb5439d8ed..515daadde22 100644 --- a/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/datavalidation/groups/EmailController.kt +++ b/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/datavalidation/groups/EmailController.kt @@ -22,7 +22,7 @@ import io.micronaut.http.annotation.Body import io.micronaut.http.annotation.Controller import io.micronaut.http.annotation.Post import io.micronaut.validation.Validated -import javax.validation.Valid +import jakarta.validation.Valid //end::imports[] @Requires(property = "spec.name", value = "datavalidationgroups") diff --git a/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/datavalidation/groups/FinalValidation.kt b/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/datavalidation/groups/FinalValidation.kt index a8fe62a3e1d..317bcfe73c1 100644 --- a/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/datavalidation/groups/FinalValidation.kt +++ b/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/datavalidation/groups/FinalValidation.kt @@ -17,7 +17,7 @@ package io.micronaut.docs.datavalidation.groups //tag::clazz[] -import javax.validation.groups.Default +import jakarta.validation.groups.Default interface FinalValidation : Default {} // <1> diff --git a/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/datavalidation/params/EmailController.kt b/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/datavalidation/params/EmailController.kt index 66bd525b9c9..1502a0aae23 100644 --- a/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/datavalidation/params/EmailController.kt +++ b/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/datavalidation/params/EmailController.kt @@ -21,7 +21,7 @@ import io.micronaut.http.HttpResponse import io.micronaut.http.annotation.Controller import io.micronaut.http.annotation.Get import io.micronaut.validation.Validated -import javax.validation.constraints.NotBlank +import jakarta.validation.constraints.NotBlank //end::imports[] @Requires(property = "spec.name", value = "datavalidationparams") diff --git a/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/datavalidation/pogo/Email.kt b/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/datavalidation/pogo/Email.kt index 970db1d7b7b..d6f023d5891 100644 --- a/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/datavalidation/pogo/Email.kt +++ b/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/datavalidation/pogo/Email.kt @@ -17,7 +17,7 @@ package io.micronaut.docs.datavalidation.pogo //tag::clazz[] import io.micronaut.core.annotation.Introspected -import javax.validation.constraints.NotBlank +import jakarta.validation.constraints.NotBlank @Introspected open class Email { diff --git a/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/datavalidation/pogo/EmailController.kt b/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/datavalidation/pogo/EmailController.kt index 7933d0c764a..f8161550bcc 100644 --- a/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/datavalidation/pogo/EmailController.kt +++ b/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/datavalidation/pogo/EmailController.kt @@ -22,7 +22,7 @@ import io.micronaut.http.annotation.Body import io.micronaut.http.annotation.Controller import io.micronaut.http.annotation.Post import io.micronaut.validation.Validated -import javax.validation.Valid +import jakarta.validation.Valid //end::imports[] @Requires(property = "spec.name", value = "datavalidationpogo") diff --git a/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/factories/nullable/EngineConfiguration.kt b/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/factories/nullable/EngineConfiguration.kt index 9f925b06b0b..4f6ce251606 100644 --- a/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/factories/nullable/EngineConfiguration.kt +++ b/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/factories/nullable/EngineConfiguration.kt @@ -17,7 +17,7 @@ package io.micronaut.docs.factories.nullable import io.micronaut.context.annotation.EachProperty import io.micronaut.core.util.Toggleable -import javax.validation.constraints.NotNull +import jakarta.validation.constraints.NotNull // tag::class[] @EachProperty("engines") diff --git a/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/ioc/validation/Person.kt b/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/ioc/validation/Person.kt index b869cf98c45..6af4f23b969 100644 --- a/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/ioc/validation/Person.kt +++ b/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/ioc/validation/Person.kt @@ -17,12 +17,12 @@ package io.micronaut.docs.ioc.validation // tag::class[] import io.micronaut.core.annotation.Introspected -import javax.validation.constraints.Min -import javax.validation.constraints.NotBlank +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotBlank @Introspected data class Person( @field:NotBlank var name: String, @field:Min(18) var age: Int ) -// end::class[] \ No newline at end of file +// end::class[] diff --git a/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/ioc/validation/PersonService.kt b/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/ioc/validation/PersonService.kt index dd1e2e9c230..c767e1d3954 100644 --- a/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/ioc/validation/PersonService.kt +++ b/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/ioc/validation/PersonService.kt @@ -17,7 +17,7 @@ package io.micronaut.docs.ioc.validation // tag::imports[] import jakarta.inject.Singleton -import javax.validation.constraints.NotBlank +import jakarta.validation.constraints.NotBlank // end::imports[] // tag::class[] diff --git a/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/ioc/validation/PersonServiceSpec.kt b/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/ioc/validation/PersonServiceSpec.kt index c248f7c2949..c764e8e0493 100644 --- a/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/ioc/validation/PersonServiceSpec.kt +++ b/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/ioc/validation/PersonServiceSpec.kt @@ -6,7 +6,7 @@ import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertThrows import org.junit.jupiter.api.Test import jakarta.inject.Inject -import javax.validation.ConstraintViolationException +import jakarta.validation.ConstraintViolationException // end::imports[] // tag::test[] diff --git a/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/ioc/validation/custom/DurationPattern.kt b/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/ioc/validation/custom/DurationPattern.kt index f166300ac6b..0c474144612 100644 --- a/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/ioc/validation/custom/DurationPattern.kt +++ b/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/ioc/validation/custom/DurationPattern.kt @@ -16,7 +16,7 @@ package io.micronaut.docs.ioc.validation.custom // tag::imports[] -import javax.validation.Constraint +import jakarta.validation.Constraint import kotlin.annotation.AnnotationRetention.RUNTIME // end::imports[] diff --git a/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/ioc/validation/custom/DurationPatternValidatorSpec.kt b/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/ioc/validation/custom/DurationPatternValidatorSpec.kt index f8a6b0e2fc1..6c7ab8c1427 100644 --- a/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/ioc/validation/custom/DurationPatternValidatorSpec.kt +++ b/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/ioc/validation/custom/DurationPatternValidatorSpec.kt @@ -5,7 +5,7 @@ import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertThrows import org.junit.jupiter.api.Test import jakarta.inject.Inject -import javax.validation.ConstraintViolationException +import jakarta.validation.ConstraintViolationException @MicronautTest internal class DurationPatternValidatorSpec { diff --git a/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/ioc/validation/custom/HolidayService.kt b/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/ioc/validation/custom/HolidayService.kt index 8e70b21d637..e43d19ca1cf 100644 --- a/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/ioc/validation/custom/HolidayService.kt +++ b/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/ioc/validation/custom/HolidayService.kt @@ -17,7 +17,7 @@ package io.micronaut.docs.ioc.validation.custom import java.time.Duration import jakarta.inject.Singleton -import javax.validation.constraints.NotBlank +import jakarta.validation.constraints.NotBlank // tag::class[] @Singleton diff --git a/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/ioc/validation/pojo/PersonService.kt b/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/ioc/validation/pojo/PersonService.kt index 9e463115a99..3856aed9938 100644 --- a/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/ioc/validation/pojo/PersonService.kt +++ b/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/ioc/validation/pojo/PersonService.kt @@ -19,7 +19,7 @@ package io.micronaut.docs.ioc.validation.pojo import io.micronaut.docs.ioc.validation.Person import jakarta.inject.Singleton -import javax.validation.Valid +import jakarta.validation.Valid // end::imports[] diff --git a/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/ioc/validation/pojo/PersonServiceSpec.kt b/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/ioc/validation/pojo/PersonServiceSpec.kt index 7e9932adc1b..588b5db4d70 100644 --- a/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/ioc/validation/pojo/PersonServiceSpec.kt +++ b/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/ioc/validation/pojo/PersonServiceSpec.kt @@ -8,7 +8,7 @@ import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertThrows import org.junit.jupiter.api.Test import jakarta.inject.Inject -import javax.validation.ConstraintViolationException +import jakarta.validation.ConstraintViolationException // end::imports[] // tag::test[] diff --git a/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/server/binding/BookmarkController.kt b/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/server/binding/BookmarkController.kt index dc3f37b025e..3d34f0a3204 100644 --- a/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/server/binding/BookmarkController.kt +++ b/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/server/binding/BookmarkController.kt @@ -19,7 +19,7 @@ package io.micronaut.docs.server.binding import io.micronaut.http.HttpStatus import io.micronaut.http.annotation.Controller import io.micronaut.http.annotation.Get -import javax.validation.Valid +import jakarta.validation.Valid // end::imports[] // tag::class[] diff --git a/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/server/binding/MovieTicketBean.kt b/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/server/binding/MovieTicketBean.kt index e633ca5c60d..b30d691b348 100644 --- a/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/server/binding/MovieTicketBean.kt +++ b/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/server/binding/MovieTicketBean.kt @@ -21,7 +21,7 @@ import io.micronaut.http.HttpRequest import io.micronaut.http.annotation.PathVariable import io.micronaut.http.annotation.QueryValue import javax.annotation.Nullable -import javax.validation.constraints.PositiveOrZero +import jakarta.validation.constraints.PositiveOrZero // end::imports[] // tag::class[] diff --git a/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/server/binding/MovieTicketController.kt b/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/server/binding/MovieTicketController.kt index a93fe966291..4a06c3047f2 100644 --- a/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/server/binding/MovieTicketController.kt +++ b/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/server/binding/MovieTicketController.kt @@ -20,7 +20,7 @@ import io.micronaut.http.HttpStatus import io.micronaut.http.annotation.Controller import io.micronaut.http.annotation.Get import io.micronaut.http.annotation.RequestBean -import javax.validation.Valid +import jakarta.validation.Valid // end::imports[] // tag::class[] @@ -35,4 +35,4 @@ open class MovieTicketController { } } -// end::class[] \ No newline at end of file +// end::class[] diff --git a/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/server/binding/PaginationCommand.kt b/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/server/binding/PaginationCommand.kt index 3396c8824d8..257c08c3e27 100644 --- a/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/server/binding/PaginationCommand.kt +++ b/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/server/binding/PaginationCommand.kt @@ -17,9 +17,9 @@ package io.micronaut.docs.server.binding // tag::imports[] import io.micronaut.core.annotation.Introspected -import javax.validation.constraints.Pattern -import javax.validation.constraints.Positive -import javax.validation.constraints.PositiveOrZero +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Positive +import jakarta.validation.constraints.PositiveOrZero // end::imports[] diff --git a/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/server/body/MessageController.kt b/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/server/body/MessageController.kt index b6079d18794..59e6fb6571d 100644 --- a/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/server/body/MessageController.kt +++ b/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/server/body/MessageController.kt @@ -21,7 +21,7 @@ import io.micronaut.http.MediaType import io.micronaut.http.annotation.Body import io.micronaut.http.annotation.Controller import io.micronaut.http.annotation.Post -import javax.validation.constraints.Size +import jakarta.validation.constraints.Size // end::imports[] // tag::importsreactive[] import org.reactivestreams.Publisher diff --git a/test-suite-kotlin/src/test/kotlin/io/micronaut/validation/validator/Person.kt b/test-suite-kotlin/src/test/kotlin/io/micronaut/validation/validator/Person.kt index 1b3da56f2c6..a920551581b 100644 --- a/test-suite-kotlin/src/test/kotlin/io/micronaut/validation/validator/Person.kt +++ b/test-suite-kotlin/src/test/kotlin/io/micronaut/validation/validator/Person.kt @@ -16,11 +16,11 @@ package io.micronaut.validation.validator import io.micronaut.core.annotation.Introspected -import javax.validation.constraints.Min -import javax.validation.constraints.NotBlank +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotBlank @Introspected data class Person( @NotBlank var name: String, @Min(18) var age: Int -) \ No newline at end of file +) diff --git a/test-suite-kotlin/src/test/kotlin/io/micronaut/validation/validator/ValidatorSpec.kt b/test-suite-kotlin/src/test/kotlin/io/micronaut/validation/validator/ValidatorSpec.kt index e29239dd2b6..8fd7b4f06e7 100644 --- a/test-suite-kotlin/src/test/kotlin/io/micronaut/validation/validator/ValidatorSpec.kt +++ b/test-suite-kotlin/src/test/kotlin/io/micronaut/validation/validator/ValidatorSpec.kt @@ -4,7 +4,7 @@ import io.micronaut.context.ApplicationContext import org.junit.jupiter.api.Test import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.fail -import javax.validation.ConstraintViolationException +import jakarta.validation.ConstraintViolationException class ValidatorSpec { @@ -33,4 +33,4 @@ class ValidatorSpec { } context.close() } -} \ No newline at end of file +} diff --git a/test-suite/src/test/groovy/io/micronaut/docs/aop/validation/BookService.java b/test-suite/src/test/groovy/io/micronaut/docs/aop/validation/BookService.java index 67023ce00b6..1fdc97c2c91 100644 --- a/test-suite/src/test/groovy/io/micronaut/docs/aop/validation/BookService.java +++ b/test-suite/src/test/groovy/io/micronaut/docs/aop/validation/BookService.java @@ -18,7 +18,7 @@ // tag::imports[] import io.micronaut.validation.Validated; import jakarta.inject.Singleton; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotBlank; import java.util.*; // end::imports[] diff --git a/test-suite/src/test/groovy/io/micronaut/docs/aop/validation/BookServiceSpec.groovy b/test-suite/src/test/groovy/io/micronaut/docs/aop/validation/BookServiceSpec.groovy index afa6b4e5176..82aafb39e01 100644 --- a/test-suite/src/test/groovy/io/micronaut/docs/aop/validation/BookServiceSpec.groovy +++ b/test-suite/src/test/groovy/io/micronaut/docs/aop/validation/BookServiceSpec.groovy @@ -20,7 +20,7 @@ import spock.lang.AutoCleanup import spock.lang.Shared import spock.lang.Specification -import javax.validation.ConstraintViolationException +import jakarta.validation.ConstraintViolationException /** * @author graemerocher diff --git a/test-suite/src/test/groovy/io/micronaut/docs/aop/validation/ValidatedWithJavaxAnnoationNonNullSpec.groovy b/test-suite/src/test/groovy/io/micronaut/docs/aop/validation/ValidatedWithJavaxAnnoationNonNullSpec.groovy index 3ea20b220e5..0c3d0e0a77a 100644 --- a/test-suite/src/test/groovy/io/micronaut/docs/aop/validation/ValidatedWithJavaxAnnoationNonNullSpec.groovy +++ b/test-suite/src/test/groovy/io/micronaut/docs/aop/validation/ValidatedWithJavaxAnnoationNonNullSpec.groovy @@ -24,8 +24,8 @@ import spock.lang.AutoCleanup import spock.lang.Shared import spock.lang.Specification import jakarta.inject.Singleton -import javax.validation.ConstraintViolationException -import javax.validation.constraints.NotNull +import jakarta.validation.ConstraintViolationException +import jakarta.validation.constraints.NotNull class ValidatedWithJavaxAnnoationNonNullSpec extends Specification { diff --git a/test-suite/src/test/groovy/io/micronaut/http/hateoas/JsonErrorEmbeddedSpec.groovy b/test-suite/src/test/groovy/io/micronaut/http/hateoas/JsonErrorEmbeddedSpec.groovy index ae25dafc5d1..948ec703ead 100644 --- a/test-suite/src/test/groovy/io/micronaut/http/hateoas/JsonErrorEmbeddedSpec.groovy +++ b/test-suite/src/test/groovy/io/micronaut/http/hateoas/JsonErrorEmbeddedSpec.groovy @@ -17,7 +17,7 @@ import jakarta.inject.Inject import spock.lang.Issue import spock.lang.Specification -import javax.validation.constraints.Pattern +import jakarta.validation.constraints.Pattern @Issue("https://github.com/micronaut-projects/micronaut-core/issues/2863") @Property(name = "spec.name", value = "JsonErrorEmbeddedSpec") diff --git a/test-suite/src/test/java/io/micronaut/docs/annotation/PetClient.java b/test-suite/src/test/java/io/micronaut/docs/annotation/PetClient.java index 9a324554c24..83e0c411f5d 100644 --- a/test-suite/src/test/java/io/micronaut/docs/annotation/PetClient.java +++ b/test-suite/src/test/java/io/micronaut/docs/annotation/PetClient.java @@ -19,8 +19,8 @@ import io.micronaut.http.client.annotation.Client; import org.reactivestreams.Publisher; import io.micronaut.core.async.annotation.SingleResult; -import javax.validation.constraints.Min; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotBlank; // end::imports[] // tag::class[] diff --git a/test-suite/src/test/java/io/micronaut/docs/annotation/PetControllerSpec.java b/test-suite/src/test/java/io/micronaut/docs/annotation/PetControllerSpec.java index daaf7f95960..c120a221d9f 100644 --- a/test-suite/src/test/java/io/micronaut/docs/annotation/PetControllerSpec.java +++ b/test-suite/src/test/java/io/micronaut/docs/annotation/PetControllerSpec.java @@ -22,7 +22,7 @@ import org.junit.rules.ExpectedException; import reactor.core.publisher.Mono; -import javax.validation.ConstraintViolationException; +import jakarta.validation.ConstraintViolationException; import static org.junit.Assert.assertEquals; diff --git a/test-suite/src/test/java/io/micronaut/docs/annotation/PetOperations.java b/test-suite/src/test/java/io/micronaut/docs/annotation/PetOperations.java index 9ec481e6160..73f96496d85 100644 --- a/test-suite/src/test/java/io/micronaut/docs/annotation/PetOperations.java +++ b/test-suite/src/test/java/io/micronaut/docs/annotation/PetOperations.java @@ -20,8 +20,8 @@ import io.micronaut.validation.Validated; import org.reactivestreams.Publisher; import io.micronaut.core.async.annotation.SingleResult; -import javax.validation.constraints.Min; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotBlank; // end::imports[] // tag::class[] diff --git a/test-suite/src/test/java/io/micronaut/docs/config/immutable/EngineConfig.java b/test-suite/src/test/java/io/micronaut/docs/config/immutable/EngineConfig.java index 7db637308f0..63d532f9221 100644 --- a/test-suite/src/test/java/io/micronaut/docs/config/immutable/EngineConfig.java +++ b/test-suite/src/test/java/io/micronaut/docs/config/immutable/EngineConfig.java @@ -21,9 +21,9 @@ import io.micronaut.context.annotation.ConfigurationInject; import io.micronaut.context.annotation.ConfigurationProperties; -import javax.validation.constraints.Min; -import javax.validation.constraints.NotBlank; -import javax.validation.constraints.NotNull; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; import java.util.Optional; // end::imports[] diff --git a/test-suite/src/test/java/io/micronaut/docs/config/itfce/EngineConfig.java b/test-suite/src/test/java/io/micronaut/docs/config/itfce/EngineConfig.java index 1c98e102b11..7949e7f3dcc 100644 --- a/test-suite/src/test/java/io/micronaut/docs/config/itfce/EngineConfig.java +++ b/test-suite/src/test/java/io/micronaut/docs/config/itfce/EngineConfig.java @@ -19,9 +19,9 @@ import io.micronaut.context.annotation.ConfigurationProperties; import io.micronaut.core.bind.annotation.Bindable; -import javax.validation.constraints.Min; -import javax.validation.constraints.NotBlank; -import javax.validation.constraints.NotNull; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; import java.util.Optional; // end::imports[] diff --git a/test-suite/src/test/java/io/micronaut/docs/config/mapFormat/EngineConfig.java b/test-suite/src/test/java/io/micronaut/docs/config/mapFormat/EngineConfig.java index aaec46ea9d6..5195cb8833c 100644 --- a/test-suite/src/test/java/io/micronaut/docs/config/mapFormat/EngineConfig.java +++ b/test-suite/src/test/java/io/micronaut/docs/config/mapFormat/EngineConfig.java @@ -19,7 +19,7 @@ import io.micronaut.context.annotation.ConfigurationProperties; import io.micronaut.core.convert.format.MapFormat; -import javax.validation.constraints.Min; +import jakarta.validation.constraints.Min; import java.util.Map; // end::imports[] diff --git a/test-suite/src/test/java/io/micronaut/docs/config/properties/EngineConfig.java b/test-suite/src/test/java/io/micronaut/docs/config/properties/EngineConfig.java index 4207f28bae7..2dfae0be56d 100644 --- a/test-suite/src/test/java/io/micronaut/docs/config/properties/EngineConfig.java +++ b/test-suite/src/test/java/io/micronaut/docs/config/properties/EngineConfig.java @@ -18,8 +18,8 @@ // tag::imports[] import io.micronaut.context.annotation.ConfigurationProperties; -import javax.validation.constraints.Min; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotBlank; import java.util.Optional; // end::imports[] diff --git a/test-suite/src/test/java/io/micronaut/docs/datavalidation/groups/Email.java b/test-suite/src/test/java/io/micronaut/docs/datavalidation/groups/Email.java index bdd03b53fa9..7f2d1b72e57 100644 --- a/test-suite/src/test/java/io/micronaut/docs/datavalidation/groups/Email.java +++ b/test-suite/src/test/java/io/micronaut/docs/datavalidation/groups/Email.java @@ -18,7 +18,7 @@ //tag::clazz[] import io.micronaut.core.annotation.Introspected; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotBlank; @Introspected public class Email { diff --git a/test-suite/src/test/java/io/micronaut/docs/datavalidation/groups/EmailController.java b/test-suite/src/test/java/io/micronaut/docs/datavalidation/groups/EmailController.java index 08de95cbe37..3c1b4ea8a7b 100644 --- a/test-suite/src/test/java/io/micronaut/docs/datavalidation/groups/EmailController.java +++ b/test-suite/src/test/java/io/micronaut/docs/datavalidation/groups/EmailController.java @@ -23,7 +23,7 @@ import io.micronaut.http.annotation.Post; import io.micronaut.validation.Validated; -import javax.validation.Valid; +import jakarta.validation.Valid; import java.util.Collections; //end::imports[] diff --git a/test-suite/src/test/java/io/micronaut/docs/datavalidation/groups/FinalValidation.java b/test-suite/src/test/java/io/micronaut/docs/datavalidation/groups/FinalValidation.java index dae6a2d553c..3cfe7e231ff 100644 --- a/test-suite/src/test/java/io/micronaut/docs/datavalidation/groups/FinalValidation.java +++ b/test-suite/src/test/java/io/micronaut/docs/datavalidation/groups/FinalValidation.java @@ -17,7 +17,7 @@ //tag::clazz[] -import javax.validation.groups.Default; +import jakarta.validation.groups.Default; public interface FinalValidation extends Default {} // <1> diff --git a/test-suite/src/test/java/io/micronaut/docs/datavalidation/params/EmailController.java b/test-suite/src/test/java/io/micronaut/docs/datavalidation/params/EmailController.java index 17e34fcda0b..0c7cd24c438 100644 --- a/test-suite/src/test/java/io/micronaut/docs/datavalidation/params/EmailController.java +++ b/test-suite/src/test/java/io/micronaut/docs/datavalidation/params/EmailController.java @@ -22,7 +22,7 @@ import io.micronaut.http.annotation.Get; import io.micronaut.validation.Validated; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotBlank; import java.util.Collections; //end::imports[] diff --git a/test-suite/src/test/java/io/micronaut/docs/datavalidation/pogo/Email.java b/test-suite/src/test/java/io/micronaut/docs/datavalidation/pogo/Email.java index f75400bce34..ebc89534e3f 100644 --- a/test-suite/src/test/java/io/micronaut/docs/datavalidation/pogo/Email.java +++ b/test-suite/src/test/java/io/micronaut/docs/datavalidation/pogo/Email.java @@ -18,7 +18,7 @@ //tag::clazz[] import io.micronaut.core.annotation.Introspected; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotBlank; @Introspected public class Email { diff --git a/test-suite/src/test/java/io/micronaut/docs/datavalidation/pogo/EmailController.java b/test-suite/src/test/java/io/micronaut/docs/datavalidation/pogo/EmailController.java index 4c10334378c..bce26d7a94f 100644 --- a/test-suite/src/test/java/io/micronaut/docs/datavalidation/pogo/EmailController.java +++ b/test-suite/src/test/java/io/micronaut/docs/datavalidation/pogo/EmailController.java @@ -23,7 +23,7 @@ import io.micronaut.http.annotation.Post; import io.micronaut.validation.Validated; -import javax.validation.Valid; +import jakarta.validation.Valid; import java.util.Collections; //end::imports[] diff --git a/test-suite/src/test/java/io/micronaut/docs/factories/nullable/EngineConfiguration.java b/test-suite/src/test/java/io/micronaut/docs/factories/nullable/EngineConfiguration.java index e7ebbe14b28..5b2debe5d4b 100644 --- a/test-suite/src/test/java/io/micronaut/docs/factories/nullable/EngineConfiguration.java +++ b/test-suite/src/test/java/io/micronaut/docs/factories/nullable/EngineConfiguration.java @@ -18,7 +18,7 @@ import io.micronaut.context.annotation.EachProperty; import io.micronaut.core.util.Toggleable; -import javax.validation.constraints.NotNull; +import jakarta.validation.constraints.NotNull; // tag::class[] @EachProperty("engines") diff --git a/test-suite/src/test/java/io/micronaut/docs/ioc/validation/Person.java b/test-suite/src/test/java/io/micronaut/docs/ioc/validation/Person.java index c65027b97c2..08f43528135 100644 --- a/test-suite/src/test/java/io/micronaut/docs/ioc/validation/Person.java +++ b/test-suite/src/test/java/io/micronaut/docs/ioc/validation/Person.java @@ -18,8 +18,8 @@ // tag::class[] import io.micronaut.core.annotation.Introspected; -import javax.validation.constraints.Min; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotBlank; @Introspected public class Person { diff --git a/test-suite/src/test/java/io/micronaut/docs/ioc/validation/PersonService.java b/test-suite/src/test/java/io/micronaut/docs/ioc/validation/PersonService.java index f3e25819f1c..b44d10cfcce 100644 --- a/test-suite/src/test/java/io/micronaut/docs/ioc/validation/PersonService.java +++ b/test-suite/src/test/java/io/micronaut/docs/ioc/validation/PersonService.java @@ -17,7 +17,7 @@ // tag::imports[] import jakarta.inject.Singleton; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotBlank; // end::imports[] // tag::class[] diff --git a/test-suite/src/test/java/io/micronaut/docs/ioc/validation/PersonServiceSpec.java b/test-suite/src/test/java/io/micronaut/docs/ioc/validation/PersonServiceSpec.java index db867359e02..0af23b83bc3 100644 --- a/test-suite/src/test/java/io/micronaut/docs/ioc/validation/PersonServiceSpec.java +++ b/test-suite/src/test/java/io/micronaut/docs/ioc/validation/PersonServiceSpec.java @@ -20,7 +20,7 @@ import org.junit.jupiter.api.Test; import jakarta.inject.Inject; -import javax.validation.ConstraintViolationException; +import jakarta.validation.ConstraintViolationException; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; diff --git a/test-suite/src/test/java/io/micronaut/docs/ioc/validation/custom/DurationPattern.java b/test-suite/src/test/java/io/micronaut/docs/ioc/validation/custom/DurationPattern.java index f104e89ba3b..22c844676d1 100644 --- a/test-suite/src/test/java/io/micronaut/docs/ioc/validation/custom/DurationPattern.java +++ b/test-suite/src/test/java/io/micronaut/docs/ioc/validation/custom/DurationPattern.java @@ -16,7 +16,7 @@ package io.micronaut.docs.ioc.validation.custom; // tag::imports[] -import javax.validation.Constraint; +import jakarta.validation.Constraint; import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.Target; diff --git a/test-suite/src/test/java/io/micronaut/docs/ioc/validation/custom/DurationPatternValidatorSpec.java b/test-suite/src/test/java/io/micronaut/docs/ioc/validation/custom/DurationPatternValidatorSpec.java index 7476cd5b710..130868380f2 100644 --- a/test-suite/src/test/java/io/micronaut/docs/ioc/validation/custom/DurationPatternValidatorSpec.java +++ b/test-suite/src/test/java/io/micronaut/docs/ioc/validation/custom/DurationPatternValidatorSpec.java @@ -20,8 +20,8 @@ import jakarta.inject.Inject; -import javax.validation.ConstraintViolation; -import javax.validation.ConstraintViolationException; +import jakarta.validation.ConstraintViolation; +import jakarta.validation.ConstraintViolationException; import java.util.Objects; diff --git a/test-suite/src/test/java/io/micronaut/docs/ioc/validation/custom/HolidayService.java b/test-suite/src/test/java/io/micronaut/docs/ioc/validation/custom/HolidayService.java index bf1007c6979..69dae9e769f 100644 --- a/test-suite/src/test/java/io/micronaut/docs/ioc/validation/custom/HolidayService.java +++ b/test-suite/src/test/java/io/micronaut/docs/ioc/validation/custom/HolidayService.java @@ -16,7 +16,7 @@ package io.micronaut.docs.ioc.validation.custom; import jakarta.inject.Singleton; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotBlank; import java.time.Duration; // tag::class[] diff --git a/test-suite/src/test/java/io/micronaut/docs/ioc/validation/pojo/PersonService.java b/test-suite/src/test/java/io/micronaut/docs/ioc/validation/pojo/PersonService.java index ffcacfecd80..dd7f9db993a 100644 --- a/test-suite/src/test/java/io/micronaut/docs/ioc/validation/pojo/PersonService.java +++ b/test-suite/src/test/java/io/micronaut/docs/ioc/validation/pojo/PersonService.java @@ -20,7 +20,7 @@ import io.micronaut.docs.ioc.validation.Person; import jakarta.inject.Singleton; -import javax.validation.Valid; +import jakarta.validation.Valid; // end::imports[] // tag::class[] diff --git a/test-suite/src/test/java/io/micronaut/docs/ioc/validation/pojo/PersonServiceSpec.java b/test-suite/src/test/java/io/micronaut/docs/ioc/validation/pojo/PersonServiceSpec.java index 5783e78eac9..135a8dfbd47 100644 --- a/test-suite/src/test/java/io/micronaut/docs/ioc/validation/pojo/PersonServiceSpec.java +++ b/test-suite/src/test/java/io/micronaut/docs/ioc/validation/pojo/PersonServiceSpec.java @@ -22,8 +22,8 @@ import org.junit.jupiter.api.Test; import jakarta.inject.Inject; -import javax.validation.ConstraintViolation; -import javax.validation.ConstraintViolationException; +import jakarta.validation.ConstraintViolation; +import jakarta.validation.ConstraintViolationException; import java.util.Set; import static org.junit.jupiter.api.Assertions.assertEquals; diff --git a/test-suite/src/test/java/io/micronaut/docs/server/binding/BookmarkController.java b/test-suite/src/test/java/io/micronaut/docs/server/binding/BookmarkController.java index 2ff7110cf13..82c909b29a3 100644 --- a/test-suite/src/test/java/io/micronaut/docs/server/binding/BookmarkController.java +++ b/test-suite/src/test/java/io/micronaut/docs/server/binding/BookmarkController.java @@ -21,7 +21,7 @@ import io.micronaut.http.annotation.Get; import io.micronaut.core.annotation.Nullable; -import javax.validation.Valid; +import jakarta.validation.Valid; // end::imports[] // tag::class[] diff --git a/test-suite/src/test/java/io/micronaut/docs/server/binding/MovieTicketBean.java b/test-suite/src/test/java/io/micronaut/docs/server/binding/MovieTicketBean.java index 79b56c38bcc..e71fcf95057 100644 --- a/test-suite/src/test/java/io/micronaut/docs/server/binding/MovieTicketBean.java +++ b/test-suite/src/test/java/io/micronaut/docs/server/binding/MovieTicketBean.java @@ -21,7 +21,7 @@ import io.micronaut.http.HttpRequest; import io.micronaut.http.annotation.PathVariable; import io.micronaut.http.annotation.QueryValue; -import javax.validation.constraints.PositiveOrZero; +import jakarta.validation.constraints.PositiveOrZero; // end::imports[] // tag::class[] diff --git a/test-suite/src/test/java/io/micronaut/docs/server/binding/MovieTicketController.java b/test-suite/src/test/java/io/micronaut/docs/server/binding/MovieTicketController.java index f592d35350b..5b30d25de84 100644 --- a/test-suite/src/test/java/io/micronaut/docs/server/binding/MovieTicketController.java +++ b/test-suite/src/test/java/io/micronaut/docs/server/binding/MovieTicketController.java @@ -16,7 +16,7 @@ package io.micronaut.docs.server.binding; // tag::imports[] -import javax.validation.Valid; +import jakarta.validation.Valid; import io.micronaut.http.HttpStatus; import io.micronaut.http.annotation.Controller; import io.micronaut.http.annotation.Get; diff --git a/test-suite/src/test/java/io/micronaut/docs/server/binding/PaginationCommand.java b/test-suite/src/test/java/io/micronaut/docs/server/binding/PaginationCommand.java index 89ee288c516..8bf7d808205 100644 --- a/test-suite/src/test/java/io/micronaut/docs/server/binding/PaginationCommand.java +++ b/test-suite/src/test/java/io/micronaut/docs/server/binding/PaginationCommand.java @@ -18,9 +18,9 @@ import io.micronaut.core.annotation.Introspected; import io.micronaut.core.annotation.Nullable; -import javax.validation.constraints.Pattern; -import javax.validation.constraints.Positive; -import javax.validation.constraints.PositiveOrZero; +import jakarta.validation.constraints.Pattern; +import jakarta.validation.constraints.Positive; +import jakarta.validation.constraints.PositiveOrZero; @Introspected public class PaginationCommand { diff --git a/test-suite/src/test/java/io/micronaut/docs/server/body/MessageController.java b/test-suite/src/test/java/io/micronaut/docs/server/body/MessageController.java index 2f3cd7cdc3e..56cedeee8f7 100644 --- a/test-suite/src/test/java/io/micronaut/docs/server/body/MessageController.java +++ b/test-suite/src/test/java/io/micronaut/docs/server/body/MessageController.java @@ -21,7 +21,7 @@ import io.micronaut.http.annotation.Body; import io.micronaut.http.annotation.Controller; import io.micronaut.http.annotation.Post; -import javax.validation.constraints.Size; +import jakarta.validation.constraints.Size; // end::imports[] // tag::importsreactive[] import org.reactivestreams.Publisher; diff --git a/test-suite/src/test/java/io/micronaut/docs/session/ShoppingController.java b/test-suite/src/test/java/io/micronaut/docs/session/ShoppingController.java index 59e8f8b65d2..c1adbd26d3c 100644 --- a/test-suite/src/test/java/io/micronaut/docs/session/ShoppingController.java +++ b/test-suite/src/test/java/io/micronaut/docs/session/ShoppingController.java @@ -23,7 +23,7 @@ import io.micronaut.session.annotation.SessionValue; import io.micronaut.core.annotation.Nullable; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotBlank; // end::imports[] // tag::class[] From 0992ee7f42fe6a41e1690e9b7046e41afcc0d4c7 Mon Sep 17 00:00:00 2001 From: micronaut-build <65172877+micronaut-build@users.noreply.github.com> Date: Sat, 11 Mar 2023 09:57:05 +0100 Subject: [PATCH 584/743] Update common files (#8926) --- .github/workflows/gradle.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index 4dc98860301..c0142bc2c44 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -30,6 +30,8 @@ jobs: sudo apt-get clean df -h - uses: actions/checkout@v3 + with: + fetch-depth: 0 - name: Setup GraalVM CE uses: graalvm/setup-graalvm@v1 with: @@ -61,7 +63,7 @@ jobs: - name: Run static analysis if: github.repository_owner == 'micronaut-projects' run: | - ./gradlew sonarqube + ./gradlew sonar env: TESTCONTAINERS_RYUK_DISABLED: true GRADLE_ENTERPRISE_ACCESS_KEY: ${{ secrets.GRADLE_ENTERPRISE_ACCESS_KEY }} From a84fef2e939d4cde47d7c29c18ae68aaa62419a7 Mon Sep 17 00:00:00 2001 From: Graeme Rocher Date: Sat, 11 Mar 2023 12:02:58 +0100 Subject: [PATCH 585/743] address sonar issues (#8928) --- .../netty/jackson/JsonContentProcessor.java | 4 +- .../micronaut/http/filter/FilterRunner.java | 57 ++++++++++++------- .../visitor/TypeElementSymbolProcessor.kt | 2 +- .../AbstractInitializableBeanDefinition.java | 13 +++-- .../micronaut/context/DefaultBeanContext.java | 20 ++++--- .../exp/RandomPropertyExpressionResolver.java | 9 ++- .../annotation/MutableAnnotationMetadata.java | 1 + .../micronaut/json/convert/LazyJsonNode.java | 1 + 8 files changed, 69 insertions(+), 38 deletions(-) diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/jackson/JsonContentProcessor.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/jackson/JsonContentProcessor.java index 9d3adf4f996..00648973a61 100644 --- a/http-server-netty/src/main/java/io/micronaut/http/server/netty/jackson/JsonContentProcessor.java +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/jackson/JsonContentProcessor.java @@ -146,7 +146,9 @@ private void flush(Collection out, ByteBuf completedNode) throws IOExcep try { out.add(jsonMapper.readValue(wrapped, Argument.of(JsonNode.class))); } finally { - completedNode.release(); + if (completedNode != null) { + completedNode.release(); + } } } else { out.add(new LazyJsonNode(wrapped)); diff --git a/http/src/main/java/io/micronaut/http/filter/FilterRunner.java b/http/src/main/java/io/micronaut/http/filter/FilterRunner.java index e82881167c7..225e556d937 100644 --- a/http/src/main/java/io/micronaut/http/filter/FilterRunner.java +++ b/http/src/main/java/io/micronaut/http/filter/FilterRunner.java @@ -134,6 +134,7 @@ public static void sortReverse(@NonNull List filters) { * @param response The current response * @return A flow that will be passed on to the next filter */ + @SuppressWarnings("java:S1452") protected ExecutionFlow> processResponse(HttpRequest request, HttpResponse response) { return ExecutionFlow.just(response); } @@ -146,6 +147,7 @@ protected ExecutionFlow> processResponse(HttpRequest> processFailure(HttpRequest request, Throwable failure) { return ExecutionFlow.error(failure); } @@ -170,6 +172,7 @@ public final FilterRunner reactorContext(Context reactorContext) { * @return The flow that completes after all filters and the terminal operation, with the final * response */ + @SuppressWarnings("java:S1452") public final ExecutionFlow> run(HttpRequest request) { return (ExecutionFlow) filterRequest(new FilterContext(request, initialReactorContext), filters.listIterator(), new HashMap<>()); } @@ -233,6 +236,11 @@ private ExecutionFlow> filterResponse(FilterContext context, } } + @SuppressWarnings({ + "java:S3776", // performance + "java:S2259", // false positive + "java:S1181" // this is a framework not an application + }) private ExecutionFlow processRequestFilter(GenericHttpFilter filter, FilterContext context, Map, @@ -395,6 +403,7 @@ public static void validateFilterMethod(Argument[] arguments, } @Internal + @SuppressWarnings("java:S3776") // performance public static FilterMethod prepareFilterMethod(ConversionService conversionService, T bean, ExecutableMethod method, @@ -490,6 +499,7 @@ private static boolean isReactive(Argument continuationReturnType) { return continuationReturnType.isReactive() || continuationReturnType.getType() == Publisher.class; } + @SuppressWarnings({"java:S3776", "java:S3740"}) // performance private static FilterReturnHandler prepareReturnHandler(ConversionService conversionService, Argument type, boolean isResponseFilter, @@ -542,30 +552,25 @@ private static FilterReturnHandler prepareReturnHandler(ConversionService conver } if (isReactive(type)) { var next = prepareReturnHandler(conversionService, type.getWrappedType(), isResponseFilter, hasContinuation, false); - return new FilterReturnHandler() { - - @Override - public ExecutionFlow handle(FilterContext context, Object returnValue, FilterContinuationImpl continuation) throws Throwable { - if (returnValue == null && !nullable) { - return next.handle(context, null, continuation); - } + return (context, returnValue, continuation) -> { + if (returnValue == null && !nullable) { + return next.handle(context, null, continuation); + } - Mono publisher = Mono.from(Publishers.convertPublisher(conversionService, returnValue, Publisher.class)) - .contextWrite(context.reactorContext()); + Mono publisher = Mono.from(Publishers.convertPublisher(conversionService, returnValue, Publisher.class)) + .contextWrite(context.reactorContext()); - if (continuation instanceof ReactiveResultAwareReactiveContinuationImpl reactiveContinuation) { - publisher.subscribe(reactiveContinuation); - return reactiveContinuation.nextFilterFlow(); - } - return ReactiveExecutionFlow.fromPublisher(publisher).flatMap(v -> { - try { - return next.handle(context, v, continuation); - } catch (Throwable e) { - return ExecutionFlow.error(e); - } - }); + if (continuation instanceof ReactiveResultAwareReactiveContinuationImpl reactiveContinuation) { + publisher.subscribe(reactiveContinuation); + return reactiveContinuation.nextFilterFlow(); } - + return ReactiveExecutionFlow.fromPublisher(publisher).flatMap(v -> { + try { + return next.handle(context, v, continuation); + } catch (Throwable e) { + return ExecutionFlow.error(e); + } + }); }; } else if (type.isAsync()) { var next = prepareReturnHandler(conversionService, type.getWrappedType(), isResponseFilter, hasContinuation, false); @@ -581,6 +586,7 @@ protected ExecutionFlow toFlow(FilterContext context, Object returnValue, Fil } } + @SuppressWarnings("java:S6218") // equals/hashCode not used record FilterMethod(FilterOrder order, T bean, Executable method, @@ -608,6 +614,7 @@ public int getOrder() { return order.getOrder(bean); } + @SuppressWarnings("java:S1452") public FilterContinuationImpl createContinuation(FilterContext filterContext) { return continuationCreator.apply(filterContext); } @@ -737,6 +744,7 @@ private interface FilterReturnHandler { ); }; + @SuppressWarnings("java:S112") // internal interface ExecutionFlow handle(FilterContext context, @Nullable Object returnValue, @Nullable FilterContinuationImpl passedOnContinuation) throws Throwable; @@ -753,6 +761,7 @@ private DelayedFilterReturnHandler(boolean isResponseFilter, FilterReturnHandler this.nullable = nullable; } + @SuppressWarnings("java:S1452") protected abstract ExecutionFlow toFlow(FilterContext context, Object returnValue, @Nullable FilterContinuationImpl continuation); @@ -906,6 +915,9 @@ private ExecutionFlow afterMethodExecuted(@Nullable HttpResponse< // This is blocking scenario try { newFilterContext = suspensionPoint.get(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + return ExecutionFlow.error(new IllegalStateException("Failed to extract suspension point result", e)); } catch (Exception e) { return ExecutionFlow.error(new IllegalStateException("Failed to extract suspension point result", e)); } @@ -915,6 +927,7 @@ private ExecutionFlow afterMethodExecuted(@Nullable HttpResponse< return asFilterProcessed(newFilterContext, newResponse, newFailure); } + @SuppressWarnings("java:S3776") // performance protected void triggerFilterProcessed(FilterContext filterContext, @Nullable HttpResponse newResponse, @@ -1153,6 +1166,7 @@ public Publisher> proceed(HttpRequest request) { /** * Implementation of {@link FilterContinuation} for blocking calls. */ + @SuppressWarnings("java:S112") // framework code private static final class BlockingContinuationImpl extends FilterContinuationImpl> { BlockingContinuationImpl(FilterContext filterContext) { super(filterContext); @@ -1172,6 +1186,7 @@ public HttpResponse proceed() { } return filterContext.response; } catch (InterruptedException e) { + Thread.currentThread().interrupt(); interrupted = true; } catch (ExecutionException e) { Throwable cause = e.getCause(); diff --git a/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/TypeElementSymbolProcessor.kt b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/TypeElementSymbolProcessor.kt index 6b9f1c86545..62e041ec289 100644 --- a/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/TypeElementSymbolProcessor.kt +++ b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/TypeElementSymbolProcessor.kt @@ -147,7 +147,7 @@ internal open class TypeElementSymbolProcessor(private val environment: SymbolPr } override fun onError() { - + // do nothing } private fun start() { diff --git a/inject/src/main/java/io/micronaut/context/AbstractInitializableBeanDefinition.java b/inject/src/main/java/io/micronaut/context/AbstractInitializableBeanDefinition.java index 5afa2ff4a62..1453f786d4f 100644 --- a/inject/src/main/java/io/micronaut/context/AbstractInitializableBeanDefinition.java +++ b/inject/src/main/java/io/micronaut/context/AbstractInitializableBeanDefinition.java @@ -2258,13 +2258,18 @@ private String resolveCliOption(String name) { private > R resolveBeansOfType(BeanResolutionContext resolutionContext, BeanContext context, Argument returnType, Argument beanType, Qualifier qualifier) { if (beanType == null) { - throw new DependencyInjectionException(resolutionContext, "Type " + returnType.getType() + " has no generic argument"); + throw noGenericsError(resolutionContext, returnType); } qualifier = qualifier == null ? resolveQualifier(resolutionContext, beanType, returnType) : qualifier; Collection beansOfType = resolutionContext.getBeansOfType(resolveEnvironmentArgument(context, beanType), qualifier); return coerceCollectionToCorrectType(returnType.getType(), beansOfType, resolutionContext, returnType); } + @NonNull + private static DependencyInjectionException noGenericsError(BeanResolutionContext resolutionContext, Argument returnType) { + return new DependencyInjectionException(resolutionContext, "Type " + returnType.getType() + " has no generic argument"); + } + private boolean isInnerConfiguration(@Nullable Argument argument) { if (argument == null || !precalculatedInfo.isConfigurationProperties) { return false; @@ -2285,7 +2290,7 @@ private boolean isEachBeanParent(Argument argument) { private Stream resolveStreamOfType(BeanResolutionContext resolutionContext, Argument returnType, Argument beanType, Qualifier qualifier) { if (beanType == null) { - throw new DependencyInjectionException(resolutionContext, "Type " + returnType.getType() + " has no generic argument"); + throw noGenericsError(resolutionContext, returnType); } qualifier = qualifier == null ? resolveQualifier(resolutionContext, beanType, returnType) : qualifier; return resolutionContext.streamOfType(beanType, qualifier); @@ -2297,7 +2302,7 @@ private Map resolveMapOfType( Argument beanType, Qualifier qualifier) { if (beanType == null) { - throw new DependencyInjectionException(resolutionContext, "Type " + returnType.getType() + " has no generic returnType"); + throw noGenericsError(resolutionContext, returnType); } qualifier = qualifier == null ? resolveQualifier(resolutionContext, beanType, returnType) : qualifier; Map map = resolutionContext.mapOfType(beanType, qualifier); @@ -2309,7 +2314,7 @@ private Map resolveMapOfType( private Optional resolveOptionalBean(BeanResolutionContext resolutionContext, Argument returnType, Argument beanType, Qualifier qualifier) { if (beanType == null) { - throw new DependencyInjectionException(resolutionContext, "Type " + returnType.getType() + " has no generic argument"); + throw noGenericsError(resolutionContext, returnType); } qualifier = qualifier == null ? resolveQualifier(resolutionContext, beanType, returnType) : qualifier; return resolutionContext.findBean(beanType, qualifier); diff --git a/inject/src/main/java/io/micronaut/context/DefaultBeanContext.java b/inject/src/main/java/io/micronaut/context/DefaultBeanContext.java index bc09b9357bd..d5f2243bb92 100644 --- a/inject/src/main/java/io/micronaut/context/DefaultBeanContext.java +++ b/inject/src/main/java/io/micronaut/context/DefaultBeanContext.java @@ -169,6 +169,8 @@ public class DefaultBeanContext implements InitializableBeanContext { int order2 = OrderUtil.getOrder(o2.getBeanDefinition(), o2.getBean()); return Integer.compare(order1, order2); }; + private static final String MSG_COULD_NOT_BE_LOADED = "] could not be loaded: "; + public static final String MSG_BEAN_DEFINITION = "Bean definition ["; protected final AtomicBoolean running = new AtomicBoolean(false); @@ -2020,7 +2022,7 @@ protected void initializeContext( try { loadEagerBeans(contextScopeBean, eagerInit); } catch (Throwable e) { - throw new BeanInstantiationException("Bean definition [" + contextScopeBean.getReference().getName() + "] could not be loaded: " + e.getMessage(), e); + throw new BeanInstantiationException(MSG_BEAN_DEFINITION + contextScopeBean.getReference().getName() + MSG_COULD_NOT_BE_LOADED + e.getMessage(), e); } } filterReplacedBeans(null, eagerInit); @@ -2033,7 +2035,7 @@ protected void initializeContext( AbstractBeanContextConditional.ConditionLog.LOG.debug("Bean of type [{}] disabled for reason: {}", eagerInitDefinition.getBeanType().getSimpleName(), e.getMessage()); } } catch (Throwable e) { - throw new BeanInstantiationException("Bean definition [" + eagerInitDefinition.getName() + "] could not be loaded: " + e.getMessage(), e); + throw new BeanInstantiationException(MSG_BEAN_DEFINITION + eagerInitDefinition.getName() + MSG_COULD_NOT_BE_LOADED + e.getMessage(), e); } } } @@ -2513,7 +2515,7 @@ protected void processParallelBeans(List parallelBeans) loadEagerBeans(producer, parallelDefinitions); } catch (Throwable e) { BeanDefinitionReference beanDefinitionReference = producer.getReference(); - LOG.error("Parallel Bean definition [" + beanDefinitionReference.getName() + "] could not be loaded: " + e.getMessage(), e); + LOG.error("Parallel Bean definition [" + beanDefinitionReference.getName() + MSG_COULD_NOT_BE_LOADED + e.getMessage(), e); Boolean shutdownOnError = beanDefinitionReference.getAnnotationMetadata().booleanValue(Parallel.class, "shutdownOnError").orElse(true); if (shutdownOnError) { stop(); @@ -2527,7 +2529,7 @@ protected void processParallelBeans(List parallelBeans) try { initializeEagerBean(beanDefinition); } catch (Throwable e) { - LOG.error("Parallel Bean definition [" + beanDefinition.getName() + "] could not be loaded: " + e.getMessage(), e); + LOG.error("Parallel Bean definition [" + beanDefinition.getName() + MSG_COULD_NOT_BE_LOADED + e.getMessage(), e); Boolean shutdownOnError = beanDefinition.getAnnotationMetadata().booleanValue(Parallel.class, "shutdownOnError").orElse(true); if (shutdownOnError) { stop(); @@ -2705,7 +2707,7 @@ private void doInjectAndInitialize(BeanResolutionContext resolutionContext, initializingBeanDefinition.initialize(resolutionContext, this, instance); } } else { - throw new BeanContextException("Bean definition [" + beanDefinition + "] doesn't support injection!"); + throw new BeanContextException(MSG_BEAN_DEFINITION + beanDefinition + "] doesn't support injection!"); } } @@ -4018,8 +4020,8 @@ public int hashCode() { @Override public String getName() { - if (qualifier instanceof Named) { - return ((Named) qualifier).getName(); + if (qualifier instanceof Named named) { + return named.getName(); } return Primary.SIMPLE_NAME; } @@ -4114,8 +4116,10 @@ private static final class CollectionHolder { static final class BeanDefinitionProducer { @Nullable + @SuppressWarnings("java:S3077") private volatile BeanDefinitionReference reference; @Nullable + @SuppressWarnings("java:S3077") private volatile BeanDefinition definition; @Nullable private volatile Boolean referenceEnabled; @@ -4205,7 +4209,7 @@ public BeanDefinition getDefinition(BeanContext beanContext) { } return def; } catch (Throwable e) { - throw new BeanInstantiationException("Bean definition [" + reference.getName() + "] could not be loaded: " + e.getMessage(), e); + throw new BeanInstantiationException(MSG_BEAN_DEFINITION + reference.getName() + MSG_COULD_NOT_BE_LOADED + e.getMessage(), e); } } diff --git a/inject/src/main/java/io/micronaut/context/env/exp/RandomPropertyExpressionResolver.java b/inject/src/main/java/io/micronaut/context/env/exp/RandomPropertyExpressionResolver.java index 0e23dd01383..b3adc25059b 100644 --- a/inject/src/main/java/io/micronaut/context/env/exp/RandomPropertyExpressionResolver.java +++ b/inject/src/main/java/io/micronaut/context/env/exp/RandomPropertyExpressionResolver.java @@ -40,6 +40,7 @@ public final class RandomPropertyExpressionResolver implements PropertyExpressionResolver { private static final String RANDOM_PREFIX = "random."; + private static final String MSG_INVALID_RANGE = "Invalid range: `"; @Override public Optional resolve(PropertyResolver propertyResolver, ConversionService conversionService, String expression, Class requiredType) { @@ -91,6 +92,7 @@ private Object resolveRandomValue(String value, String expression) { case "float" -> { return getNextFloatInRange(range, expression); } + default -> getNextIntegerInRange(range, expression); } } } @@ -109,7 +111,7 @@ private int getNextIntegerInRange(String range, String expression) { int upperBound = Integer.parseInt(tokens[1]); return LazyInit.RANDOM.nextInt(lowerBound, upperBound); } catch (NumberFormatException ex) { - throw new ValueException("Invalid range: `" + range + "` found for type Integer for expression: " + expression, ex); + throw new ValueException(MSG_INVALID_RANGE + range + "` found for type Integer for expression: " + expression, ex); } } @@ -123,7 +125,7 @@ private long getNextLongInRange(String range, String expression) { long upperBound = Long.parseLong(tokens[1]); return LazyInit.RANDOM.nextLong(lowerBound, upperBound); } catch (NumberFormatException ex) { - throw new ValueException("Invalid range: `" + range + "` found for type Long for expression: " + expression, ex); + throw new ValueException(MSG_INVALID_RANGE + range + "` found for type Long for expression: " + expression, ex); } } @@ -137,11 +139,12 @@ private float getNextFloatInRange(String range, String expression) { float upperBound = Float.parseFloat(tokens[1]); return LazyInit.RANDOM.nextFloat(lowerBound, upperBound); } catch (NumberFormatException ex) { - throw new ValueException("Invalid range: `" + range + "` found for type Float for expression: " + expression, ex); + throw new ValueException(MSG_INVALID_RANGE + range + "` found for type Float for expression: " + expression, ex); } } static class LazyInit { + private LazyInit() {} private static final SecureRandom RANDOM = new SecureRandom(); private static final String RANDOM_UPPER_LIMIT = "(\\(-?\\d+(\\.\\d+)?\\))"; private static final String RANDOM_RANGE = "(\\[-?\\d+(\\.\\d+)?,\\s?-?\\d+(\\.\\d+)?])"; diff --git a/inject/src/main/java/io/micronaut/inject/annotation/MutableAnnotationMetadata.java b/inject/src/main/java/io/micronaut/inject/annotation/MutableAnnotationMetadata.java index b16298f0172..0683dc66cad 100644 --- a/inject/src/main/java/io/micronaut/inject/annotation/MutableAnnotationMetadata.java +++ b/inject/src/main/java/io/micronaut/inject/annotation/MutableAnnotationMetadata.java @@ -537,6 +537,7 @@ public void addDeclaredAnnotation(String annotation, Map v } } + @SuppressWarnings("java:S2259") // false positive private void addAnnotation(String annotation, Map values, Map> declaredAnnotations, diff --git a/json-core/src/main/java/io/micronaut/json/convert/LazyJsonNode.java b/json-core/src/main/java/io/micronaut/json/convert/LazyJsonNode.java index 0cfe68b0566..d074ee4e1ba 100644 --- a/json-core/src/main/java/io/micronaut/json/convert/LazyJsonNode.java +++ b/json-core/src/main/java/io/micronaut/json/convert/LazyJsonNode.java @@ -40,6 +40,7 @@ public final class LazyJsonNode implements ReferenceCounted { private ByteBuffer buffer; private int refCnt = 1; @Nullable + @SuppressWarnings("java:S3077") private volatile JsonNode asNode; @Nullable private JsonSyntaxException syntaxException; From a1bfa02074218022f4d079382ea7677fbdd8de35 Mon Sep 17 00:00:00 2001 From: Sorin Silaghi <116458+sorin-silaghi@users.noreply.github.com> Date: Sat, 11 Mar 2023 13:09:38 +0200 Subject: [PATCH 586/743] Removed mention of default null check behaviour from javadoc (#8820) --- core/src/main/java/io/micronaut/core/annotation/NonNull.java | 3 --- 1 file changed, 3 deletions(-) diff --git a/core/src/main/java/io/micronaut/core/annotation/NonNull.java b/core/src/main/java/io/micronaut/core/annotation/NonNull.java index a140e3e0168..32fca1f1ec6 100644 --- a/core/src/main/java/io/micronaut/core/annotation/NonNull.java +++ b/core/src/main/java/io/micronaut/core/annotation/NonNull.java @@ -32,9 +32,6 @@ *

Should be used at parameter, return value, and field level. Method overrides should repeat parent {@code @NonNull} annotations unless * they behave differently.

* - *

Use {@code @NonNull} Api (scope = parameters + return values) to set the default behavior to non-nullable in order to avoid annotating - * your whole codebase with {@code @NonNull}.

- * * @author graemerocher * @see Nullable * @since 2.4 From 2f5c857d5cd8ac23cb023384135f1d309a607792 Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Sat, 11 Mar 2023 12:49:12 +0100 Subject: [PATCH 587/743] build: remove dependency to validation dependency from core (#8914) This PR removes validation as a dependency of http-server-tck and http-tck --------- Co-authored-by: Graeme Rocher --- .github/workflows/gradle.yml | 2 +- http-server-tck/build.gradle.kts | 4 ++-- http-tck/build.gradle.kts | 4 ++-- test-suite-http-server-tck-jdk/build.gradle.kts | 9 +++++++++ 4 files changed, 14 insertions(+), 5 deletions(-) diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index c0142bc2c44..a8eea2a8faf 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -51,7 +51,7 @@ jobs: - name: Build with Gradle id: gradle run: | - ./gradlew check --no-daemon --parallel --continue + ./gradlew preReleaseCheck check --no-daemon --parallel --continue env: GH_TOKEN_PUBLIC_REPOS_READONLY: ${{ secrets.GH_TOKEN_PUBLIC_REPOS_READONLY }} GH_USERNAME: ${{ secrets.GH_USERNAME }} diff --git a/http-server-tck/build.gradle.kts b/http-server-tck/build.gradle.kts index 7b67e1976d0..992c63d1a54 100644 --- a/http-server-tck/build.gradle.kts +++ b/http-server-tck/build.gradle.kts @@ -10,8 +10,8 @@ dependencies { } annotationProcessor(projects.httpValidation) - implementation(platform(libs.test.boms.micronaut.validation)) - implementation(libs.micronaut.validation) { + compileOnly(platform(libs.test.boms.micronaut.validation)) + compileOnly(libs.micronaut.validation) { exclude(group = "io.micronaut") } implementation(projects.runtime) diff --git a/http-tck/build.gradle.kts b/http-tck/build.gradle.kts index 3d93011d827..335b4ad08b7 100644 --- a/http-tck/build.gradle.kts +++ b/http-tck/build.gradle.kts @@ -11,8 +11,8 @@ dependencies { } annotationProcessor(projects.httpValidation) - implementation(platform(libs.test.boms.micronaut.validation)) - implementation(libs.micronaut.validation) { + compileOnly(platform(libs.test.boms.micronaut.validation)) + compileOnly(libs.micronaut.validation) { exclude(group = "io.micronaut") } implementation(projects.runtime) diff --git a/test-suite-http-server-tck-jdk/build.gradle.kts b/test-suite-http-server-tck-jdk/build.gradle.kts index 2f7d7926dd4..efe469f579b 100644 --- a/test-suite-http-server-tck-jdk/build.gradle.kts +++ b/test-suite-http-server-tck-jdk/build.gradle.kts @@ -6,6 +6,15 @@ dependencies { testImplementation(projects.httpClientJdk) testImplementation(projects.httpServerTck) testImplementation(libs.junit.platform.engine) + testAnnotationProcessor(platform(libs.test.boms.micronaut.validation)) + testAnnotationProcessor(libs.micronaut.validation.processor) { + exclude(group = "io.micronaut") + } + testImplementation(platform(libs.test.boms.micronaut.validation)) + testImplementation(libs.micronaut.validation) { + exclude(group = "io.micronaut") + } + } tasks.withType(Test::class) { From 0bba46e36b0565b87a0e538b6185c4f4f628b152 Mon Sep 17 00:00:00 2001 From: Graeme Rocher Date: Sat, 11 Mar 2023 13:45:51 +0100 Subject: [PATCH 588/743] fix merge errors --- .github/workflows/gradle.yml | 2 +- gradle/libs.versions.toml | 2 +- http-server-tck/build.gradle.kts | 4 ++-- .../io/micronaut/http/server/tck/tests/HeadersTest.java | 7 ++++--- .../server/tck/tests/endpoints/health/HealthTest.java | 7 ++++--- .../server/tck/tests/filter/HttpServerFilterTest.java | 8 ++++---- http-tck/build.gradle.kts | 4 ++-- 7 files changed, 18 insertions(+), 16 deletions(-) diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index c0142bc2c44..a8eea2a8faf 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -51,7 +51,7 @@ jobs: - name: Build with Gradle id: gradle run: | - ./gradlew check --no-daemon --parallel --continue + ./gradlew preReleaseCheck check --no-daemon --parallel --continue env: GH_TOKEN_PUBLIC_REPOS_READONLY: ${{ secrets.GH_TOKEN_PUBLIC_REPOS_READONLY }} GH_USERNAME: ${{ secrets.GH_USERNAME }} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 4bee199dc5b..bf5f030dc2d 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -72,7 +72,7 @@ managed-reactive-streams = "1.0.4" # This should be kept aligned with https://github.com/micronaut-projects/micronaut-reactor/blob/master/gradle.properties from the BOM managed-reactor = "3.4.24" managed-slf4j = "2.0.4" -managed-snakeyaml = "1.33" +managed-snakeyaml = "2.0" managed-java-parser-core = "3.24.9" managed-ksp = "1.8.0-1.0.9" micronaut-docs = "2.0.0" diff --git a/http-server-tck/build.gradle.kts b/http-server-tck/build.gradle.kts index 63910776a36..9b096787673 100644 --- a/http-server-tck/build.gradle.kts +++ b/http-server-tck/build.gradle.kts @@ -10,8 +10,8 @@ dependencies { } annotationProcessor(projects.httpValidation) - implementation(platform(libs.test.boms.micronaut.validation)) - implementation(libs.micronaut.validation) { + compileOnly(platform(libs.test.boms.micronaut.validation)) + compileOnly(libs.micronaut.validation) { exclude(group = "io.micronaut") } implementation(projects.runtime) diff --git a/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/HeadersTest.java b/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/HeadersTest.java index c08fd2e82c9..1900e732085 100644 --- a/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/HeadersTest.java +++ b/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/HeadersTest.java @@ -22,13 +22,14 @@ import io.micronaut.http.annotation.Controller; import io.micronaut.http.annotation.Get; import io.micronaut.http.annotation.Header; -import io.micronaut.http.server.tck.AssertionUtils; -import io.micronaut.http.server.tck.HttpResponseAssertion; +import io.micronaut.http.tck.AssertionUtils; +import io.micronaut.http.tck.HttpResponseAssertion; import org.junit.jupiter.api.Test; import java.io.IOException; -import static io.micronaut.http.server.tck.TestScenario.asserts; +import static io.micronaut.http.tck.TestScenario.asserts; + @SuppressWarnings({ "java:S5960", // We're allowed assertions, as these are used in tests only diff --git a/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/endpoints/health/HealthTest.java b/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/endpoints/health/HealthTest.java index 7593d99def8..151ea94a03c 100644 --- a/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/endpoints/health/HealthTest.java +++ b/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/endpoints/health/HealthTest.java @@ -17,13 +17,14 @@ import io.micronaut.http.HttpRequest; import io.micronaut.http.HttpStatus; -import io.micronaut.http.server.tck.AssertionUtils; -import io.micronaut.http.server.tck.HttpResponseAssertion; +import io.micronaut.http.tck.AssertionUtils; +import io.micronaut.http.tck.HttpResponseAssertion; import org.junit.jupiter.api.Test; import java.io.IOException; -import static io.micronaut.http.server.tck.TestScenario.asserts; +import static io.micronaut.http.tck.TestScenario.asserts; + @SuppressWarnings({ "java:S5960", // We're allowed assertions, as these are used in tests only diff --git a/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/filter/HttpServerFilterTest.java b/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/filter/HttpServerFilterTest.java index d02b06d90db..fa83516a84c 100644 --- a/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/filter/HttpServerFilterTest.java +++ b/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/filter/HttpServerFilterTest.java @@ -27,10 +27,10 @@ import io.micronaut.http.annotation.Get; import io.micronaut.http.filter.HttpServerFilter; import io.micronaut.http.filter.ServerFilterChain; -import io.micronaut.http.server.tck.AssertionUtils; -import io.micronaut.http.server.tck.HttpResponseAssertion; -import io.micronaut.http.server.tck.ServerUnderTest; -import io.micronaut.http.server.tck.TestScenario; +import io.micronaut.http.tck.AssertionUtils; +import io.micronaut.http.tck.HttpResponseAssertion; +import io.micronaut.http.tck.ServerUnderTest; +import io.micronaut.http.tck.TestScenario; import io.micronaut.web.router.MethodBasedRouteMatch; import io.micronaut.web.router.RouteMatch; import jakarta.annotation.security.RolesAllowed; diff --git a/http-tck/build.gradle.kts b/http-tck/build.gradle.kts index 3d93011d827..335b4ad08b7 100644 --- a/http-tck/build.gradle.kts +++ b/http-tck/build.gradle.kts @@ -11,8 +11,8 @@ dependencies { } annotationProcessor(projects.httpValidation) - implementation(platform(libs.test.boms.micronaut.validation)) - implementation(libs.micronaut.validation) { + compileOnly(platform(libs.test.boms.micronaut.validation)) + compileOnly(libs.micronaut.validation) { exclude(group = "io.micronaut") } implementation(projects.runtime) From cf319f5115f47a6e712cb7ca9392e242b8df6d4a Mon Sep 17 00:00:00 2001 From: Graeme Rocher Date: Sat, 11 Mar 2023 13:51:06 +0100 Subject: [PATCH 589/743] fix merge errors --- .../micronaut-buffer-netty/native-image.properties | 1 - .../micronaut-http-netty/native-image.properties | 1 - test-suite-http-server-tck-jdk/build.gradle.kts | 4 ++++ test-suite-http-server-tck-netty/build.gradle | 6 +++--- 4 files changed, 7 insertions(+), 5 deletions(-) delete mode 100644 buffer-netty/src/main/resources/META-INF/native-image/io.micronaut/micronaut-buffer-netty/native-image.properties delete mode 100644 http-netty/src/main/resources/META-INF/native-image/io.micronaut/micronaut-http-netty/native-image.properties diff --git a/buffer-netty/src/main/resources/META-INF/native-image/io.micronaut/micronaut-buffer-netty/native-image.properties b/buffer-netty/src/main/resources/META-INF/native-image/io.micronaut/micronaut-buffer-netty/native-image.properties deleted file mode 100644 index 7552ba4d263..00000000000 --- a/buffer-netty/src/main/resources/META-INF/native-image/io.micronaut/micronaut-buffer-netty/native-image.properties +++ /dev/null @@ -1 +0,0 @@ -Args = --features=io.micronaut.buffer.netty.NettyFeature diff --git a/http-netty/src/main/resources/META-INF/native-image/io.micronaut/micronaut-http-netty/native-image.properties b/http-netty/src/main/resources/META-INF/native-image/io.micronaut/micronaut-http-netty/native-image.properties deleted file mode 100644 index 52279101dc6..00000000000 --- a/http-netty/src/main/resources/META-INF/native-image/io.micronaut/micronaut-http-netty/native-image.properties +++ /dev/null @@ -1 +0,0 @@ -Args = --features=io.micronaut.http.netty.graal.HttpNettyFeature diff --git a/test-suite-http-server-tck-jdk/build.gradle.kts b/test-suite-http-server-tck-jdk/build.gradle.kts index 2f7d7926dd4..b8ec71f929f 100644 --- a/test-suite-http-server-tck-jdk/build.gradle.kts +++ b/test-suite-http-server-tck-jdk/build.gradle.kts @@ -6,6 +6,10 @@ dependencies { testImplementation(projects.httpClientJdk) testImplementation(projects.httpServerTck) testImplementation(libs.junit.platform.engine) + testImplementation(platform(libs.test.boms.micronaut.validation)) + testImplementation(libs.micronaut.validation) { + exclude(group = "io.micronaut") + } } tasks.withType(Test::class) { diff --git a/test-suite-http-server-tck-netty/build.gradle b/test-suite-http-server-tck-netty/build.gradle index 694b268670e..2060d85e5f1 100644 --- a/test-suite-http-server-tck-netty/build.gradle +++ b/test-suite-http-server-tck-netty/build.gradle @@ -25,9 +25,9 @@ dependencies { testImplementation(projects.httpClient) testImplementation(libs.junit.platform.engine) testImplementation(libs.managed.logback.classic) - implementation platform(libs.test.boms.micronaut.validation) - implementation(libs.micronaut.validation) { - exclude group: 'io.micronaut' + testImplementation(platform(libs.test.boms.micronaut.validation)) + testImplementation(libs.micronaut.validation) { + exclude(group = "io.micronaut") } } From 85bf67abd16dee7fd4f46ecb7197f1e5dbd5777d Mon Sep 17 00:00:00 2001 From: Graeme Rocher Date: Sat, 11 Mar 2023 14:05:09 +0100 Subject: [PATCH 590/743] fix merge errors --- test-suite-http-server-tck-netty/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-suite-http-server-tck-netty/build.gradle b/test-suite-http-server-tck-netty/build.gradle index 2060d85e5f1..d83eb1726ce 100644 --- a/test-suite-http-server-tck-netty/build.gradle +++ b/test-suite-http-server-tck-netty/build.gradle @@ -27,7 +27,7 @@ dependencies { testImplementation(libs.managed.logback.classic) testImplementation(platform(libs.test.boms.micronaut.validation)) testImplementation(libs.micronaut.validation) { - exclude(group = "io.micronaut") + exclude(group: "io.micronaut") } } From 4632bcf684b9ccd47a0d2e9adf90ea0559fad434 Mon Sep 17 00:00:00 2001 From: Denis Stepanov Date: Sun, 12 Mar 2023 09:45:17 -0600 Subject: [PATCH 591/743] Include stereotypes into the mapped/remapped annotation value, keep `@Inherited` (#8932) This PR changes the way how annotations are processed. Now, we include stereotypes into the AnnotationValue and recursively build the tree of the annotations and their stereotypes before mappers/transformers are run, making them more powerful. That allowed me to extract some specific code related to qualifiers and interceptors into its own remappers, which also gained the ability to be executed on all annotations. Now we also include @Inherited stereotype into stereotypes of AnnotationValue and check if the annotation is inherited after the transformation, allowing the remappers to make the annotation inherited, something that is needed for JAX-RS and Validation. The change in the processing also returns the behavior of Micronaut 3, allowing remapping non-inherited annotation to an inherited one https://github.com/micronaut-projects/micronaut-serialization/blob/master/serde-jackson/src/test/groovy/io/micronaut/serde/jackson/annotation/JsonIgnoreSpec.groovy#L438 --- .../io/micronaut/aop/InterceptorBinding.java | 6 + .../aop/chain/DefaultInterceptorRegistry.java | 7 +- .../intercepted/InterceptedMethodUtil.java | 30 + .../AbstractAnnotationMetadataBuilder.java | 1086 ++++++++--------- .../inject/annotation/AnnotationRemapper.java | 9 + .../internal/InterceptorBindingMembers.java | 142 +++ .../internal/QualifierBindingMembers.java | 61 + .../BeanDefinitionCreatorFactory.java | 6 +- ...onaut.inject.annotation.AnnotationRemapper | 2 + .../core/annotation/AnnotationUtil.java | 34 +- .../core/annotation/AnnotationValue.java | 13 + .../annotation/AnnotationValueBuilder.java | 75 +- .../micronaut/core/util/CollectionUtils.java | 66 +- .../test/AbstractBeanDefinitionSpec.groovy | 6 +- .../GroovyAnnotationMetadataBuilder.java | 36 - ...eritedConfigurationReaderPrefixSpec.groovy | 4 +- .../BeanDefinitionInjectProcessor.java | 8 +- .../JavaAnnotationMetadataBuilder.java | 46 - .../micronaut/annotation/mapping/MyGet1.java | 14 + .../micronaut/annotation/mapping/MyGet2.java | 14 + ...TransformNotInheritedAnnotationSpec.groovy | 56 + .../TransformToInheritedAnnotationSpec.groovy | 59 + ...eritedConfigurationReaderPrefixSpec.groovy | 4 +- .../NonBindingQualifierSpec.groovy | 18 +- ...ut.inject.annotation.AnnotationTransformer | 2 + .../KotlinAnnotationMetadataBuilder.kt | 28 - .../annotation/AnnotationMetadataSupport.java | 13 + .../annotation/MutableAnnotationMetadata.java | 42 +- .../AnnotationMetadataQualifier.java | 6 +- .../InterceptorBindingQualifier.java | 145 ++- 30 files changed, 1200 insertions(+), 838 deletions(-) create mode 100644 core-processor/src/main/java/io/micronaut/inject/annotation/internal/InterceptorBindingMembers.java create mode 100644 core-processor/src/main/java/io/micronaut/inject/annotation/internal/QualifierBindingMembers.java create mode 100644 inject-java/src/test/groovy/io/micronaut/annotation/mapping/MyGet1.java create mode 100644 inject-java/src/test/groovy/io/micronaut/annotation/mapping/MyGet2.java create mode 100644 inject-java/src/test/groovy/io/micronaut/annotation/mapping/TransformNotInheritedAnnotationSpec.groovy create mode 100644 inject-java/src/test/groovy/io/micronaut/annotation/mapping/TransformToInheritedAnnotationSpec.groovy diff --git a/aop/src/main/java/io/micronaut/aop/InterceptorBinding.java b/aop/src/main/java/io/micronaut/aop/InterceptorBinding.java index 92e183a92aa..0086df38a9c 100644 --- a/aop/src/main/java/io/micronaut/aop/InterceptorBinding.java +++ b/aop/src/main/java/io/micronaut/aop/InterceptorBinding.java @@ -32,6 +32,12 @@ @Target({ElementType.ANNOTATION_TYPE, ElementType.TYPE}) @Repeatable(InterceptorBindingDefinitions.class) public @interface InterceptorBinding { + + /** + * The bind members name. + */ + String META_BIND_MEMBERS = "bindMembers"; + /** * When declared on an interceptor, the value of this annotation can be used to indicate the annotation the * {@link MethodInterceptor} binds to at runtime. diff --git a/aop/src/main/java/io/micronaut/aop/chain/DefaultInterceptorRegistry.java b/aop/src/main/java/io/micronaut/aop/chain/DefaultInterceptorRegistry.java index 44410c571b7..1f3005e0dfd 100644 --- a/aop/src/main/java/io/micronaut/aop/chain/DefaultInterceptorRegistry.java +++ b/aop/src/main/java/io/micronaut/aop/chain/DefaultInterceptorRegistry.java @@ -34,6 +34,7 @@ import io.micronaut.core.type.Argument; import io.micronaut.core.type.Executable; import io.micronaut.inject.ExecutableMethod; +import io.micronaut.inject.qualifiers.InterceptorBindingQualifier; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -42,8 +43,6 @@ import java.util.Collection; import java.util.List; -import static io.micronaut.inject.qualifiers.InterceptorBindingQualifier.META_MEMBER_MEMBERS; - /** * Default implementation of the interceptor registry interface. * @@ -179,7 +178,7 @@ private boolean selectInterceptor(Class declaringType, private boolean matches(AnnotationValue interceptorAnnotationValue, Collection> interceptPointBindings) { final AnnotationValue memberBinding = interceptorAnnotationValue - .getAnnotation(META_MEMBER_MEMBERS).orElse(null); + .getAnnotation(InterceptorBindingQualifier.META_BINDING_VALUES).orElse(null); final String annotationName = interceptorAnnotationValue.stringValue().orElse(null); if (annotationName == null) { // This shouldn't happen @@ -194,7 +193,7 @@ private boolean matches(AnnotationValue interceptorAnnotationValue, Collectio return true; } AnnotationValue otherMembers = - applicableValue.getAnnotation(META_MEMBER_MEMBERS).orElse(null); + applicableValue.getAnnotation(InterceptorBindingQualifier.META_BINDING_VALUES).orElse(null); if (!memberBinding.equals(otherMembers)) { continue; } diff --git a/aop/src/main/java/io/micronaut/aop/internal/intercepted/InterceptedMethodUtil.java b/aop/src/main/java/io/micronaut/aop/internal/intercepted/InterceptedMethodUtil.java index fc6a4967176..76a487f6a7a 100644 --- a/aop/src/main/java/io/micronaut/aop/internal/intercepted/InterceptedMethodUtil.java +++ b/aop/src/main/java/io/micronaut/aop/internal/intercepted/InterceptedMethodUtil.java @@ -19,6 +19,7 @@ import io.micronaut.aop.InterceptedMethod; import io.micronaut.aop.InterceptorBinding; import io.micronaut.aop.InterceptorKind; +import io.micronaut.aop.Introduction; import io.micronaut.aop.MethodInvocationContext; import io.micronaut.core.annotation.AnnotationMetadata; import io.micronaut.core.annotation.AnnotationUtil; @@ -113,6 +114,17 @@ public static boolean hasAroundStereotype(@Nullable AnnotationMetadata annotatio annMetdata -> annMetdata.getAnnotationValuesByType(InterceptorBinding.class)); } + /** + * Does the given metadata have introduction declared. + * @param annotationMetadata The annotation metadata + * @return True if it does + */ + public static boolean hasIntroductionStereotype(@Nullable AnnotationMetadata annotationMetadata) { + return hasIntroduction(annotationMetadata, + annMetadata -> annMetadata.hasStereotype(Introduction.class), + annMetdata -> annMetdata.getAnnotationValuesByType(InterceptorBinding.class)); + } + /** * Does the given metadata have declared AOP advice. * @param annotationMetadata The annotation metadata @@ -141,4 +153,22 @@ private static boolean hasAround(@Nullable AnnotationMetadata annotationMetadata return false; } + + private static boolean hasIntroduction(@Nullable AnnotationMetadata annotationMetadata, + @NonNull Predicate hasFunction, + @NonNull Function>> interceptorBindingsFunction) { + if (annotationMetadata == null) { + return false; + } + if (hasFunction.test(annotationMetadata)) { + return true; + } else if (annotationMetadata.hasDeclaredStereotype(AnnotationUtil.ANN_INTERCEPTOR_BINDINGS)) { + return interceptorBindingsFunction.apply(annotationMetadata) + .stream().anyMatch(av -> + av.enumValue("kind", InterceptorKind.class).orElse(InterceptorKind.AROUND) == InterceptorKind.INTRODUCTION + ); + } + + return false; + } } diff --git a/core-processor/src/main/java/io/micronaut/inject/annotation/AbstractAnnotationMetadataBuilder.java b/core-processor/src/main/java/io/micronaut/inject/annotation/AbstractAnnotationMetadataBuilder.java index 52d01147fd0..6af26a577be 100644 --- a/core-processor/src/main/java/io/micronaut/inject/annotation/AbstractAnnotationMetadataBuilder.java +++ b/core-processor/src/main/java/io/micronaut/inject/annotation/AbstractAnnotationMetadataBuilder.java @@ -19,14 +19,12 @@ import io.micronaut.context.annotation.Aliases; import io.micronaut.context.annotation.DefaultScope; import io.micronaut.context.annotation.NonBinding; -import io.micronaut.context.annotation.Type; import io.micronaut.core.annotation.AnnotatedElement; import io.micronaut.core.annotation.AnnotationClassValue; import io.micronaut.core.annotation.AnnotationMetadata; import io.micronaut.core.annotation.AnnotationMetadataDelegate; import io.micronaut.core.annotation.AnnotationUtil; import io.micronaut.core.annotation.AnnotationValue; -import io.micronaut.core.annotation.AnnotationValueBuilder; import io.micronaut.core.annotation.InstantiatedMember; import io.micronaut.core.annotation.Internal; import io.micronaut.core.annotation.NonNull; @@ -35,9 +33,7 @@ import io.micronaut.core.naming.NameUtils; import io.micronaut.core.util.CollectionUtils; import io.micronaut.core.util.StringUtils; -import io.micronaut.inject.qualifiers.InterceptorBindingQualifier; import io.micronaut.inject.visitor.VisitorContext; -import jakarta.inject.Qualifier; import java.lang.annotation.Annotation; import java.lang.annotation.RetentionPolicy; @@ -45,8 +41,9 @@ import java.util.Collections; import java.util.HashMap; import java.util.HashSet; +import java.util.Iterator; import java.util.LinkedHashMap; -import java.util.LinkedList; +import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Objects; @@ -75,13 +72,13 @@ public abstract class AbstractAnnotationMetadataBuilder { private static final Map>> ANNOTATION_MAPPERS = new HashMap<>(10); private static final Map>> ANNOTATION_TRANSFORMERS = new HashMap<>(5); private static final Map> ANNOTATION_REMAPPERS = new HashMap<>(5); + private static final List ALL_ANNOTATION_REMAPPERS = new ArrayList<>(5); private static final Map MUTATED_ANNOTATION_METADATA = new HashMap<>(100); - private static final Map> NON_BINDING_CACHE = new HashMap<>(50); private static final Map> ANNOTATION_DEFAULTS = new HashMap<>(20); static { for (AnnotationMapper mapper : SoftServiceLoader.load(AnnotationMapper.class, AbstractAnnotationMetadataBuilder.class.getClassLoader()) - .disableFork().collectAll()) { + .disableFork().collectAll()) { try { String name = null; if (mapper instanceof TypedAnnotationMapper typedAnnotationMapper) { @@ -98,7 +95,7 @@ public abstract class AbstractAnnotationMetadataBuilder { } for (AnnotationTransformer transformer : SoftServiceLoader.load(AnnotationTransformer.class, AbstractAnnotationMetadataBuilder.class.getClassLoader()) - .disableFork().collectAll()) { + .disableFork().collectAll()) { try { String name = null; if (transformer instanceof TypedAnnotationTransformer typedAnnotationTransformer) { @@ -115,10 +112,12 @@ public abstract class AbstractAnnotationMetadataBuilder { } for (AnnotationRemapper mapper : SoftServiceLoader.load(AnnotationRemapper.class, AbstractAnnotationMetadataBuilder.class.getClassLoader()) - .disableFork().collectAll()) { + .disableFork().collectAll()) { try { String name = mapper.getPackageName(); - if (StringUtils.isNotEmpty(name)) { + if (name.equals(AnnotationRemapper.ALL_PACKAGES)) { + ALL_ANNOTATION_REMAPPERS.add(mapper); + } else if (StringUtils.isNotEmpty(name)) { ANNOTATION_REMAPPERS.computeIfAbsent(name, s -> new ArrayList<>(2)).add(mapper); } } catch (Throwable e) { @@ -157,9 +156,9 @@ public AnnotationMetadata buildDeclared(T element) { MutableAnnotationMetadata annotationMetadata = new MutableAnnotationMetadata(); try { AnnotationMetadata metadata = buildInternalMulti( - Collections.emptyList(), - element, - annotationMetadata, true, true + Collections.emptyList(), + element, + annotationMetadata, true, true ); if (metadata.isEmpty()) { return AnnotationMetadata.EMPTY_METADATA; @@ -170,38 +169,6 @@ public AnnotationMetadata buildDeclared(T element) { } } - /** - * Build only metadata for declared annotations. - * - * @param element The element - * @param annotations The annotations - * @param includeTypeAnnotations Whether to include type level annotations in the metadata for the element - * @return The {@link AnnotationMetadata} - */ - public AnnotationMetadata buildDeclared(T element, List annotations, boolean includeTypeAnnotations) { - if (CollectionUtils.isEmpty(annotations)) { - return AnnotationMetadata.EMPTY_METADATA; - } - MutableAnnotationMetadata annotationMetadata = new MutableAnnotationMetadata(); - if (includeTypeAnnotations) { - buildInternalMulti( - Collections.emptyList(), - element, - annotationMetadata, false, true - ); - } - try { - addAnnotations(annotationMetadata, element, false, true, - annotations, Collections.emptyList()); - if (annotationMetadata.isEmpty()) { - return AnnotationMetadata.EMPTY_METADATA; - } - return annotationMetadata; - } catch (RuntimeException e) { - return metadataForError(e); - } - } - /** * Build the meta data for the given element. If the element is a method the class metadata will be included. * @@ -255,22 +222,9 @@ public CachedAnnotationMetadata lookupOrBuildForField(T owningType, T element) { * @since 4.0.0 */ public CachedAnnotationMetadata lookupOrBuild(Object key, T element) { - return lookupOrBuild(key, element, false); - } - - /** - * Lookup or build new annotation metadata. - * - * @param key The cache key - * @param element The type element - * @param includeTypeAnnotations Whether to include type level annotations in the metadata for the element - * @return The annotation metadata - * @since 4.0.0 - */ - private CachedAnnotationMetadata lookupOrBuild(Object key, T element, boolean includeTypeAnnotations) { CachedAnnotationMetadata cachedAnnotationMetadata = MUTATED_ANNOTATION_METADATA.get(key); if (cachedAnnotationMetadata == null) { - AnnotationMetadata annotationMetadata = buildInternal(includeTypeAnnotations, false, element); + AnnotationMetadata annotationMetadata = buildInternal(element); cachedAnnotationMetadata = new DefaultCachedAnnotationMetadata(annotationMetadata); // Don't use `computeIfAbsent` as it can lead to a concurrent exception because the cache is accessed during in `buildInternal` MUTATED_ANNOTATION_METADATA.put(key, cachedAnnotationMetadata); @@ -278,13 +232,13 @@ private CachedAnnotationMetadata lookupOrBuild(Object key, T element, boolean in return cachedAnnotationMetadata; } - private AnnotationMetadata buildInternal(boolean inheritTypeAnnotations, boolean declaredOnly, T element) { + private AnnotationMetadata buildInternal(T element) { MutableAnnotationMetadata annotationMetadata = new MutableAnnotationMetadata(); try { return buildInternalMulti( - Collections.emptyList(), - element, - annotationMetadata, inheritTypeAnnotations, declaredOnly + Collections.emptyList(), + element, + annotationMetadata, false, false ); } catch (RuntimeException e) { return metadataForError(e); @@ -375,12 +329,12 @@ private AnnotationMetadata buildInternal(boolean inheritTypeAnnotations, boolean * @param annotationValues The values to populate */ protected abstract void readAnnotationRawValues( - T originatingElement, - String annotationName, - T member, - String memberName, - Object annotationValue, - Map annotationValues); + T originatingElement, + String annotationName, + T member, + String memberName, + Object annotationValue, + Map annotationValues); /** * Validates an annotation value. @@ -403,7 +357,7 @@ protected void validateAnnotationValue(T originatingElement, final AnnotatedElementValidator elementValidator = getElementValidator(); if (elementValidator != null && !erroneousElements.contains(member)) { boolean shouldValidate = !(annotationName.equals(AliasFor.class.getName())) && - (!(resolvedValue instanceof String) || !resolvedValue.toString().contains("${")); + (!(resolvedValue instanceof String) || !resolvedValue.toString().contains("${")); if (shouldValidate) { shouldValidate = isValidationRequired(member); } @@ -564,20 +518,20 @@ protected AnnotationValue readNestedAnnotationValue(T annotationElement, A an if (aliasMember.isPresent() && !(aliasAnnotation.isPresent() || aliasAnnotationName.isPresent())) { String aliasedNamed = aliasMember.get(); readAnnotationRawValues(annotationElement, - annotationTypeName, - member, - aliasedNamed, - annotationValue, - resolvedValues); + annotationTypeName, + member, + aliasedNamed, + annotationValue, + resolvedValues); } } String memberName = getAnnotationMemberName(member); readAnnotationRawValues(annotationElement, - annotationTypeName, - member, - memberName, - annotationValue, - resolvedValues); + annotationTypeName, + member, + memberName, + annotationValue, + resolvedValues); } return new AnnotationValue<>(annotationTypeName, resolvedValues); } @@ -644,11 +598,11 @@ private Map getAnnotationDefaults(T originatingElement, if (!defaultValues.containsKey(memberName)) { Object annotationValue = entry.getValue(); readAnnotationRawValues(originatingElement, - annotationName, - member, - memberName, - annotationValue, - defaultValues); + annotationName, + member, + memberName, + annotationValue, + defaultValues); } } return defaultValues; @@ -710,11 +664,11 @@ public RetentionPolicy getRetentionPolicy(@NonNull String annotation) { } private AnnotationMetadata buildInternalMulti( - List parents, - T element, - MutableAnnotationMetadata annotationMetadata, - boolean inheritTypeAnnotations, - boolean declaredOnly) { + List parents, + T element, + MutableAnnotationMetadata annotationMetadata, + boolean inheritTypeAnnotations, + boolean declaredOnly) { List hierarchy = buildHierarchy(element, inheritTypeAnnotations, declaredOnly); for (T parent : parents) { final List parentHierarchy = buildHierarchy(parent, inheritTypeAnnotations, declaredOnly); @@ -738,17 +692,16 @@ private AnnotationMetadata buildInternalMulti( boolean originatingElementIsSameParent = parents.contains(currentElement); boolean isDeclared = currentElement == element; addAnnotations( - annotationMetadata, - currentElement, - originatingElementIsSameParent, - isDeclared, - annotationHierarchy, - Collections.emptyList() + annotationMetadata, + currentElement, + originatingElementIsSameParent, + isDeclared, + annotationHierarchy ); } if (!annotationMetadata.hasDeclaredStereotype(AnnotationUtil.SCOPE) && annotationMetadata.hasDeclaredStereotype( - DefaultScope.class)) { + DefaultScope.class)) { Optional value = annotationMetadata.stringValue(DefaultScope.class); value.ifPresent(name -> annotationMetadata.addDeclaredAnnotation(name, Collections.emptyMap())); } @@ -763,42 +716,37 @@ protected void postProcess(MutableAnnotationMetadata mutableAnnotationMetadata, private void addAnnotations(MutableAnnotationMetadata annotationMetadata, T element, - boolean originatingElementIsSameParent, + boolean alwaysIncludeAnnotation, boolean isDeclared, - List annotationHierarchy, - List parentAnnotations) { + List annotationHierarchy) { Stream stream = annotationHierarchy.stream(); - Stream annotationValues = annotationMirrorToAnnotationValue(stream, - element, originatingElementIsSameParent, annotationMetadata, isDeclared, false); - addAnnotations(annotationMetadata, annotationValues, isDeclared, parentAnnotations); + Stream annotationValues = annotationMirrorToAnnotationValue(stream, element); + addAnnotations(annotationMetadata, annotationValues, isDeclared, alwaysIncludeAnnotation); } @NonNull private Stream annotationMirrorToAnnotationValue(Stream stream, - T element, - boolean originatingElementIsSameParent, - MutableAnnotationMetadata annotationMetadata, - boolean isDeclared, - boolean isStereotype) { + T element) { return stream - .filter(annotationMirror -> { - String annotationName = getAnnotationTypeName(annotationMirror); - if (AnnotationUtil.INTERNAL_ANNOTATION_NAMES.contains(annotationName) - || isExcludedAnnotation(element, annotationName)) { - return false; - } - if (DEPRECATED_ANNOTATION_NAMES.containsKey(annotationName)) { - addWarning(element, - "Usages of deprecated annotation " + annotationName + " found. You should use " + DEPRECATED_ANNOTATION_NAMES.get( - annotationName) + " instead."); - } - return isStereotype || isDeclared || isInheritedAnnotation(annotationMirror) || originatingElementIsSameParent; - }).map(annotationMirror -> createAnnotationValue(element, annotationMirror, annotationMetadata)); + .filter(annotationMirror -> { + String annotationName = getAnnotationTypeName(annotationMirror); + if (!annotationName.equals(AnnotationUtil.ANN_INHERITED) + && (AnnotationUtil.INTERNAL_ANNOTATION_NAMES.contains(annotationName) || isExcludedAnnotation(element, annotationName))) { + return false; + } + if (DEPRECATED_ANNOTATION_NAMES.containsKey(annotationName)) { + addWarning(element, + "Usages of deprecated annotation " + annotationName + " found. You should use " + DEPRECATED_ANNOTATION_NAMES.get( + annotationName) + " instead."); + } + return true; + }).map(annotationMirror -> createAnnotationValue(element, annotationMirror)); } - private ProcessedAnnotation createAnnotationValue(T originatingElement, - A annotationMirror, - MutableAnnotationMetadata metadata) { + // The value of this method can be cached + @NonNull + private ProcessedAnnotation createAnnotationValue(@NonNull T originatingElement, + @NonNull A annotationMirror) { String annotationName = getAnnotationTypeName(annotationMirror); final T annotationType = getTypeForAnnotation(annotationMirror); RetentionPolicy retentionPolicy = getRetentionPolicy(annotationType); @@ -809,7 +757,7 @@ private ProcessedAnnotation createAnnotationValue(T originatingElement, annotationValues = new LinkedHashMap<>(3); } else { annotationValues = new LinkedHashMap<>(5); - Set nonBindingMembers = new HashSet<>(2); + Set nonBindingMembers = new LinkedHashSet<>(2); for (Map.Entry entry : elementValues.entrySet()) { T member = entry.getKey(); @@ -821,11 +769,11 @@ private ProcessedAnnotation createAnnotationValue(T originatingElement, if (hasAnnotations(member)) { final MutableAnnotationMetadata memberMetadata = new MutableAnnotationMetadata(); final List annotationsForMember = getAnnotationsForType(member) - .stream().filter((a) -> !getAnnotationTypeName(a).equals(annotationName)) - .toList(); + .stream().filter((a) -> !getAnnotationTypeName(a).equals(annotationName)) + .toList(); addAnnotations(memberMetadata, member, false, - true, annotationsForMember, Collections.emptyList()); + true, annotationsForMember); boolean isInstantiatedMember = memberMetadata.hasAnnotation(InstantiatedMember.class); @@ -843,35 +791,31 @@ private ProcessedAnnotation createAnnotationValue(T originatingElement, } readAnnotationRawValues(originatingElement, - annotationName, - member, - getAnnotationMemberName(member), - annotationValue, - annotationValues); + annotationName, + member, + getAnnotationMemberName(member), + annotationValue, + annotationValues); } if (!nonBindingMembers.isEmpty()) { - if (hasAnnotation(annotationType, AnnotationUtil.QUALIFIER) || hasAnnotation(annotationType, Qualifier.class)) { - metadata.addDeclaredStereotype( - Collections.singletonList(getAnnotationTypeName(annotationMirror)), - AnnotationUtil.QUALIFIER, - Collections.singletonMap("nonBinding", nonBindingMembers) - ); - } + nonBindingMembers.add(AnnotationUtil.NON_BINDING_ATTRIBUTE); + annotationValues.put(AnnotationUtil.NON_BINDING_ATTRIBUTE, nonBindingMembers.toArray(String[]::new)); } } Map defaultValues = getCachedAnnotationDefaults(annotationName, annotationType); return new ProcessedAnnotation( - annotationType, - new AnnotationValue<>(annotationName, annotationValues, defaultValues, retentionPolicy) + annotationType, + new AnnotationValue<>(annotationName, annotationValues, defaultValues, retentionPolicy) ); } /** * Get the cached annotation defaults. + * * @param annotationName The annotation name * @param annotationType The annotation type * @return The defaults @@ -904,173 +848,262 @@ private void handleAnnotationAlias(T originatingElement, if (aliases.isPresent()) { for (AnnotationValue av : aliases.get().getAnnotations(AnnotationMetadata.VALUE_MEMBER)) { processAnnotationAlias( - annotationValues, - annotationValue, - av, - introducedAnnotations + annotationValues, + annotationValue, + av, + introducedAnnotations ); } } else { Optional> aliasForValues = getAnnotationValues(originatingElement, annotationMember, AliasFor.class); if (aliasForValues.isPresent()) { processAnnotationAlias( - annotationValues, - annotationValue, - aliasForValues.get(), - introducedAnnotations + annotationValues, + annotationValue, + aliasForValues.get(), + introducedAnnotations ); } } } - private void addAnnotations(MutableAnnotationMetadata annotationMetadata, - Stream stream, + private void addAnnotations(@NonNull MutableAnnotationMetadata annotationMetadata, + @NonNull Stream stream, boolean isDeclared, - List parentAnnotations) { - stream = filterAndTransformAnnotations(stream, parentAnnotations); + boolean alwaysIncludeAnnotation) { - List introducedAliasForAnnotations = new ArrayList<>(); + ProcessingContext processingContext = new ProcessingContext(createVisitorContext()); - stream = stream.map(processedAnnotation -> processAliases(processedAnnotation, introducedAliasForAnnotations)); + List> annotationValues = stream + .flatMap(processedAnnotation -> processAnnotation(processingContext, processedAnnotation)) + .>map(ProcessedAnnotation::getAnnotationValue) + .toList(); - List processedAnnotations = addAnnotations(stream, annotationMetadata, isDeclared, false, parentAnnotations).toList(); + addAnnotations(annotationMetadata, isDeclared, false, alwaysIncludeAnnotation, List.of( + Map.entry( + List.of(), annotationValues + ) + )); + } + + private void addAnnotations(@NonNull MutableAnnotationMetadata annotationMetadata, + boolean isDeclared, + boolean isStereotype, + boolean alwaysIncludeAnnotation, + @NonNull List, List>>> annotations) { + + // We need to add annotations by their levels: + // 1. The annotation + // 2. Stereotypes defined on the annotation + // 3. The stereotypes of the stereotypes added in #2 + // 3. The stereotypes of the stereotypes added in #3 etc + + List, List>>> stereotypes = new ArrayList<>(annotations.size()); + + for (Map.Entry, List>> e : annotations) { + List parentAnnotations = e.getKey(); + for (AnnotationValue annotationValue : e.getValue()) { + if (annotationValue.getAnnotationName().equals(AnnotationUtil.ANN_INHERITED)) { + continue; + } + if (isDeclared || isStereotype || alwaysIncludeAnnotation || isInherited(annotationValue.getStereotypes())) { + + addAnnotation( + annotationMetadata, + parentAnnotations, + isDeclared, + isStereotype, + annotationValue); + + List newParentAnnotations = CollectionUtils.concat(parentAnnotations, annotationValue.getAnnotationName()); + + if (annotationValue.getStereotypes() != null) { + stereotypes.add(Map.entry( + newParentAnnotations, + annotationValue.getStereotypes() + )); + } + + } + } - if (CollectionUtils.isNotEmpty(introducedAliasForAnnotations)) { - // Add annotation created by @AliasFor - addStereotypeAnnotations( - introducedAliasForAnnotations.stream(), - null, - parentAnnotations, - annotationMetadata, - isDeclared - ); } - // After annotations are processes process their stereotypes - for (ProcessedAnnotation processedAnnotation : processedAnnotations) { - processStereotypes(annotationMetadata, isDeclared, parentAnnotations, processedAnnotation); + if (!stereotypes.isEmpty()) { + addAnnotations(annotationMetadata, isDeclared, true, alwaysIncludeAnnotation, stereotypes); } } - private Stream processInterceptors(Stream annotationValues, - MutableAnnotationMetadata annotationMetadata, - String lastParent, - LinkedList> interceptorBindings) { + private boolean isInherited(@Nullable List> stereotypes) { + if (stereotypes == null) { + return false; + } + return stereotypes.stream().anyMatch(av -> av.getAnnotationName().equals(AnnotationUtil.ANN_INHERITED)); + } - return annotationValues - .map(processedAnnotation -> { - AnnotationValue annotationValue = processedAnnotation.getAnnotationValue(); - String annotationName = annotationValue.getAnnotationName(); + @NonNull + private Stream processAnnotation(@NonNull ProcessingContext context, + @NonNull ProcessedAnnotation processedAnnotation) { - addToInterceptorBindingsIfNecessary(interceptorBindings, lastParent, annotationName); + AnnotationValue annotationValue = processedAnnotation.getAnnotationValue(); + if (AnnotationUtil.INTERNAL_ANNOTATION_NAMES.contains(annotationValue.getAnnotationName()) || context.isProcessed(annotationValue)) { + return Stream.empty(); + } + // The method is invoked recursively till the stereotypes are set. + // That will build an annotation value tree with annotations and it's stereotypes. + // After that we start transforming, starting from the stereotypes moving up in the hierarchy. + ProcessedAnnotation annotationWithStereotypes = addStereotypes(context, processedAnnotation); + return transform(context, annotationWithStereotypes) + .flatMap(this::flattenRepeatable) + .map(ann -> processAliases(context, ann)); + } - final boolean hasInterceptorBinding = !interceptorBindings.isEmpty(); - if (hasInterceptorBinding && AnnotationUtil.ANN_INTERCEPTOR_BINDING.equals(annotationName)) { - annotationValue = handleMemberBinding(annotationMetadata, lastParent, annotationValue); - interceptorBindings.getLast().members(annotationValue.getValues()); - return processedAnnotation.withAnnotationValue(annotationValue); - } - if (hasInterceptorBinding && Type.class.getName().equals(annotationName)) { - final Object o = annotationValue.getValues().get(AnnotationMetadata.VALUE_MEMBER); - AnnotationClassValue interceptorType = null; - if (o instanceof AnnotationClassValue annotationClassValue) { - interceptorType = annotationClassValue; - } else if (o instanceof AnnotationClassValue[] annotationClassValues) { - if (annotationClassValues.length > 0) { - interceptorType = annotationClassValues[0]; - } - } - if (interceptorType != null) { - for (AnnotationValueBuilder interceptorBinding : interceptorBindings) { - interceptorBinding.member("interceptorType", interceptorType); - } - } - } - return processedAnnotation; - }); + @NonNull + private ProcessedAnnotation processAliases(@NonNull ProcessingContext context, + @NonNull ProcessedAnnotation processedAnnotation) { + // Aliases produces by the annotations are added to the stereotypes collection + List introducedAliasForAnnotations = new ArrayList<>(); + ProcessedAnnotation newAnn = processAliases(processedAnnotation, introducedAliasForAnnotations); + if (!introducedAliasForAnnotations.isEmpty()) { + newAnn = newAnn.withAnnotationValue( + newAnn.getAnnotationValue().mutate() + .stereotypes( + introducedAliasForAnnotations.stream() + .flatMap(a -> processAnnotation(context, a)) + .>map(ProcessedAnnotation::getAnnotationValue) + .toList() + ).build() + ); + } + return newAnn; } - private Stream addAnnotations(Stream annotationValues, - MutableAnnotationMetadata annotationMetadata, - boolean isDeclared, - boolean isStereotype, - List parentAnnotations) { - return annotationValues - .peek(processedAnnotation -> { - addAnnotationDefaults(annotationMetadata, processedAnnotation); - addAnnotation(annotationMetadata, parentAnnotations, isDeclared, isStereotype, processedAnnotation); - }); + @NonNull + private ProcessedAnnotation addStereotypes(@NonNull ProcessingContext context, + @NonNull ProcessedAnnotation processedAnnotation) { + AnnotationValue annotationValue = processedAnnotation.getAnnotationValue(); + if (processedAnnotation.annotationType != null && annotationValue.getDefaultValues() == null) { + Map annotationDefaults = getCachedAnnotationDefaults( + annotationValue.getAnnotationName(), + processedAnnotation.annotationType + ); + processedAnnotation = processedAnnotation.withAnnotationValue( + annotationValue.mutate().defaultValues(annotationDefaults).build() + ); + } + List stereotypes = getStereotypes(context, processedAnnotation); + List addedStereotypes = getAddedStereotypes(context, processedAnnotation.annotationType); + if (!addedStereotypes.isEmpty()) { + stereotypes = CollectionUtils.concat(stereotypes, addedStereotypes); + } + + return processedAnnotation.withStereotypes( + stereotypes + ); } - private Stream filterAndTransformAnnotations(Stream annotationValues, List parentAnnotations) { - return annotationValues - .filter(processedAnnotation -> { - AnnotationValue annotationValue = processedAnnotation.getAnnotationValue(); - return !AnnotationUtil.INTERNAL_ANNOTATION_NAMES.contains(annotationValue.getAnnotationName()) - && !parentAnnotations.contains(annotationValue.getAnnotationName()); - }) - .flatMap(this::transform) - .flatMap(this::flattenRepeatable); + @NonNull + private List getStereotypes(@NonNull ProcessingContext context, + @NonNull ProcessedAnnotation processedAnnotation) { + AnnotationValue annotationValue = processedAnnotation.getAnnotationValue(); + ProcessingContext newContext = context.withParent(processedAnnotation.annotationValue); + + if (annotationValue.getStereotypes() != null) { + // The annotation has the stereotypes set manually + // Let's flatten repeatable + return annotationValue.getStereotypes().stream() + .map(this::toProcessedAnnotation) + .flatMap(this::flattenRepeatable) + .toList(); + } + + if (processedAnnotation.annotationType == null) { + // The annotation is not on the classpath + // We set an empty collection to mark that stereotypes are processed + return Collections.emptyList(); + } else if (annotationValue.getDefaultValues() == null) { + Map annotationDefaults = getCachedAnnotationDefaults( + annotationValue.getAnnotationName(), + processedAnnotation.annotationType + ); + processedAnnotation = processedAnnotation.withAnnotationValue( + annotationValue.mutate().defaultValues(annotationDefaults).build() + ); + } + List nativeStereotypes = getAnnotationsForType(processedAnnotation.annotationType); + if (nativeStereotypes.isEmpty()) { + // We set an empty collection to mark that stereotypes are processed + return Collections.emptyList(); + } + String annotationName = annotationValue.getAnnotationName(); + String packageName = NameUtils.getPackageName(annotationName); + boolean excludesStereotypes = AnnotationUtil.STEREOTYPE_EXCLUDES.contains(packageName) || annotationName.endsWith(".Nullable"); + return annotationMirrorToAnnotationValue(nativeStereotypes.stream(), processedAnnotation.annotationType) + .filter(stereotypeAnnotation -> { + AnnotationValue stereotypeAnnotationValue = stereotypeAnnotation.getAnnotationValue(); + String stereotypeName = stereotypeAnnotationValue.getAnnotationName(); + if (stereotypeName.equals(AnnotationUtil.ANN_INHERITED)) { + return true; + } + if (excludesStereotypes) { + return false; + } + // special case: don't add stereotype for @Nonnull when it's marked as UNKNOWN/MAYBE/NEVER. + // https://github.com/micronaut-projects/micronaut-core/issues/6795 + if (stereotypeName.equals("javax.annotation.Nonnull")) { + String when = Objects.toString(stereotypeAnnotationValue.getValues().get("when")); + return !(when.equals("UNKNOWN") || when.equals("MAYBE") || when.equals("NEVER")); + } + return true; + }).flatMap(stereotype -> processAnnotation(newContext, stereotype)).toList(); } - private Stream transform(ProcessedAnnotation toTransform) { + @NonNull + private Stream transform(@NonNull ProcessingContext context, + @NonNull ProcessedAnnotation toTransform) { // Transform annotation using: // - io.micronaut.inject.annotation.AnnotationMapper // - io.micronaut.inject.annotation.AnnotationRemapper // - io.micronaut.inject.annotation.AnnotationTransformer // Each result of the transformation will be also transformed - // To eliminate infinity loops "processedVisitors" will track and eliminate processed mappers/transformers - Set> processedVisitors = new HashSet<>(); - return transform(toTransform, processedVisitors); - } - - private Stream transform(ProcessedAnnotation toTransform, Set> processedVisitors) { - return processAnnotationMappers(toTransform, processedVisitors) - .flatMap(annotation -> processAnnotationRemappers(annotation, processedVisitors)) - .flatMap(annotation -> processAnnotationTransformers(annotation, processedVisitors)); + return processAnnotationMappers(context, toTransform) + .flatMap(annotation -> processAnnotationRemappers(context, annotation)) + .flatMap(annotation -> processAnnotationTransformers(context, annotation)); } - private Stream flattenRepeatable(ProcessedAnnotation processedAnnotation) { + @NonNull + private Stream flattenRepeatable(@NonNull ProcessedAnnotation processedAnnotation) { // In a case of a repeatable container process it as a stream of repeatable annotation values AnnotationValue annotationValue = processedAnnotation.getAnnotationValue(); List> repeatableAnnotations = annotationValue.getAnnotations(AnnotationMetadata.VALUE_MEMBER); boolean isRepeatableAnnotationContainer = !repeatableAnnotations.isEmpty() && repeatableAnnotations.stream() - .allMatch(value -> { - T annotationMirror = getAnnotationMirror(value.getAnnotationName()).orElse(null); - return annotationMirror != null && getRepeatableNameForType(annotationMirror) != null; - }); + .allMatch(value -> { + T annotationMirror = getAnnotationMirror(value.getAnnotationName()).orElse(null); + return annotationMirror != null && getRepeatableNameForType(annotationMirror) != null; + }); if (isRepeatableAnnotationContainer) { // Repeatable annotations container is being added with values // We will add every repeatable annotation separately to properly detect its container and run transformations Map containerValues = new LinkedHashMap<>(annotationValue.getValues()); containerValues.remove(AnnotationMetadata.VALUE_MEMBER); return Stream.concat( - Stream.of( - // Add repeatable container for possible stereotype annotation retrieval - // and additional members defined in the container annotation - toProcessedAnnotation(new AnnotationValue<>( - annotationValue.getAnnotationName(), - containerValues, - getRetentionPolicy(annotationValue.getAnnotationName()))) - ), - repeatableAnnotations.stream().map(this::toProcessedAnnotation) + Stream.of( + // Add repeatable container for possible stereotype annotation retrieval + // and additional members defined in the container annotation + toProcessedAnnotation(new AnnotationValue<>( + annotationValue.getAnnotationName(), + containerValues, + getRetentionPolicy(annotationValue.getAnnotationName()))) + ), + repeatableAnnotations.stream().map(this::toProcessedAnnotation) ); } return Stream.of(processedAnnotation); } - private void addAnnotationDefaults(MutableAnnotationMetadata annotationMetadata, ProcessedAnnotation processedAnnotation) { - AnnotationValue annotationValue = processedAnnotation.getAnnotationValue(); - String annotationName = annotationValue.getAnnotationName(); - T annotationType = processedAnnotation.getAnnotationType(); - Map annotationDefaults = annotationValue.getDefaultValues(); - if (annotationDefaults == null && annotationType != null) { - annotationDefaults = getCachedAnnotationDefaults(annotationName, annotationType); - } - annotationMetadata.addDefaultAnnotationValues(annotationName, annotationDefaults, annotationValue.getRetentionPolicy()); - } - - private ProcessedAnnotation processAliases(ProcessedAnnotation processedAnnotation, List introducedAnnotations) { + @NonNull + private ProcessedAnnotation processAliases(@NonNull ProcessedAnnotation processedAnnotation, + @NonNull List introducedAnnotations) { T annotationType = processedAnnotation.getAnnotationType(); if (annotationType == null) { return processedAnnotation; @@ -1083,11 +1116,11 @@ private ProcessedAnnotation processAliases(ProcessedAnnotation processedAnnotati T member = getAnnotationMember(annotationType, key); if (member != null) { handleAnnotationAlias( - annotationType, - newValues, - member, - value, - introducedAnnotations + annotationType, + newValues, + member, + value, + introducedAnnotations ); } } @@ -1097,77 +1130,53 @@ private ProcessedAnnotation processAliases(ProcessedAnnotation processedAnnotati return processedAnnotation; } return processedAnnotation.withAnnotationValue( - AnnotationValue.builder(annotationValue).members(newValues).build() + annotationValue.mutate().members(newValues).build() ); } - private void processStereotypes(MutableAnnotationMetadata annotationMetadata, - boolean isDeclared, - List parentAnnotations, - ProcessedAnnotation processedAnnotation) { - AnnotationValue annotationValue = processedAnnotation.getAnnotationValue(); - String annotationName = annotationValue.getAnnotationName(); - String packageName = NameUtils.getPackageName(annotationName); - if (AnnotationUtil.STEREOTYPE_EXCLUDES.contains(packageName) || annotationName.endsWith(".Nullable")) { - return; - } - T annotationType = processedAnnotation.getAnnotationType(); - List newParentAnnotations = new ArrayList<>(parentAnnotations); - newParentAnnotations.add(annotationName); + private void addAnnotation(@NonNull MutableAnnotationMetadata mutableAnnotationMetadata, + @NonNull List parentAnnotations, + boolean isDeclared, + boolean isStereotype, + @NonNull AnnotationValue annotationValue) { - Stream stereotypes; - if (annotationType == null || CollectionUtils.isNotEmpty(annotationValue.getStereotypes())) { - // Annotation is not on the classpath or a transformer/mapper provided a value with custom stereotypes - stereotypes = annotationValue.getStereotypes() == null ? Stream.empty() : annotationValue.getStereotypes().stream().map(this::toProcessedAnnotation); - } else { - stereotypes = annotationMirrorToAnnotationValue(getAnnotationsForType(annotationType).stream(), - annotationType, false, annotationMetadata, isDeclared, true); + String annotationName = annotationValue.getAnnotationName(); + Map annotationDefaults = annotationValue.getDefaultValues(); + if (annotationDefaults != null) { + mutableAnnotationMetadata.addDefaultAnnotationValues(annotationName, annotationDefaults, annotationValue.getRetentionPolicy()); } - addStereotypeAnnotations( - stereotypes, - annotationType, - newParentAnnotations, - annotationMetadata, - isDeclared - ); - } - private void addAnnotation(MutableAnnotationMetadata mutableAnnotationMetadata, - List parentAnnotations, - boolean isDeclared, - boolean isStereotype, - ProcessedAnnotation processedAnnotation) { - AnnotationValue annotationValue = processedAnnotation.getAnnotationValue(); - String repeatableContainer = processedAnnotation.getAnnotationType() == null ? null : getRepeatableNameForType(processedAnnotation.getAnnotationType()); + T annotationMirror = getAnnotationMirror(annotationName).orElse(null); + String repeatableContainer = annotationMirror != null ? getRepeatableNameForType(annotationMirror) : null; if (isStereotype) { if (repeatableContainer != null) { if (isDeclared) { mutableAnnotationMetadata.addDeclaredRepeatableStereotype( - parentAnnotations, - repeatableContainer, - annotationValue + parentAnnotations, + repeatableContainer, + annotationValue ); } else { mutableAnnotationMetadata.addRepeatableStereotype( - parentAnnotations, - repeatableContainer, - annotationValue + parentAnnotations, + repeatableContainer, + annotationValue ); } } else { if (isDeclared) { mutableAnnotationMetadata.addDeclaredStereotype( - parentAnnotations, - annotationValue.getAnnotationName(), - annotationValue.getValues(), - annotationValue.getRetentionPolicy() + parentAnnotations, + annotationValue.getAnnotationName(), + annotationValue.getValues(), + annotationValue.getRetentionPolicy() ); } else { mutableAnnotationMetadata.addStereotype( - parentAnnotations, - annotationValue.getAnnotationName(), - annotationValue.getValues(), - annotationValue.getRetentionPolicy() + parentAnnotations, + annotationValue.getAnnotationName(), + annotationValue.getValues(), + annotationValue.getRetentionPolicy() ); } } @@ -1181,15 +1190,15 @@ private void addAnnotation(MutableAnnotationMetadata mutableAnnotationMetadata, } else { if (isDeclared) { mutableAnnotationMetadata.addDeclaredAnnotation( - annotationValue.getAnnotationName(), - annotationValue.getValues(), - annotationValue.getRetentionPolicy() + annotationValue.getAnnotationName(), + annotationValue.getValues(), + annotationValue.getRetentionPolicy() ); } else { mutableAnnotationMetadata.addAnnotation( - annotationValue.getAnnotationName(), - annotationValue.getValues(), - annotationValue.getRetentionPolicy() + annotationValue.getAnnotationName(), + annotationValue.getValues(), + annotationValue.getRetentionPolicy() ); } } @@ -1207,299 +1216,177 @@ protected boolean isExcludedAnnotation(@NonNull T element, @NonNull String annot return AnnotationUtil.INTERNAL_ANNOTATION_NAMES.contains(annotationName); } - /** - * Test whether the annotation mirror is inherited. - * - * @param annotationMirror The mirror - * @return True if it is - */ - protected abstract boolean isInheritedAnnotation(@NonNull A annotationMirror); - - private void addStereotypeAnnotations(Stream stream, - @Nullable - T element, - List parentAnnotations, - MutableAnnotationMetadata metadata, - boolean isDeclared) { - - final String lastParent = CollectionUtils.last(parentAnnotations); - LinkedList> interceptorBindings = new LinkedList<>(); - - stream = filterAndTransformAnnotations(stream, parentAnnotations); - - stream = stream.filter(processedAnnotation -> { - AnnotationValue annotationValue = processedAnnotation.getAnnotationValue(); - - String annotationName = annotationValue.getAnnotationName(); - if (!AnnotationUtil.INTERNAL_ANNOTATION_NAMES.contains(annotationName) && !parentAnnotations.contains(annotationName)) { - if (AnnotationUtil.ADVICE_STEREOTYPES.contains(lastParent)) { - if (AnnotationUtil.ANN_INTERCEPTOR_BINDING.equals(annotationName)) { - // skip @InterceptorBinding stereotype handled in last round - return false; - } - } - } - - // special case: don't add stereotype for @Nonnull when it's marked as UNKNOWN/MAYBE/NEVER. - // https://github.com/micronaut-projects/micronaut-core/issues/6795 - if (annotationValue.getAnnotationName().equals("javax.annotation.Nonnull")) { - String when = Objects.toString(annotationValue.getValues().get("when")); - return !(when.equals("UNKNOWN") || when.equals("MAYBE") || when.equals("NEVER")); - } - return true; - }); - stream = processInterceptors(stream, metadata, lastParent, interceptorBindings); - - List introducedAliasForAnnotations = new ArrayList<>(); - - stream = stream.map(processedAnnotation -> processAliases(processedAnnotation, introducedAliasForAnnotations)); - - List processedAnnotations = addAnnotations(stream, metadata, isDeclared, true, parentAnnotations).toList(); - - if (CollectionUtils.isNotEmpty(introducedAliasForAnnotations)) { - // Add annotation created by @AliasFor - addStereotypeAnnotations( - introducedAliasForAnnotations.stream(), - null, - parentAnnotations, - metadata, - isDeclared - ); - } - - // After annotations are processes process their stereotypes - for (ProcessedAnnotation processedAnnotation : processedAnnotations) { - processStereotypes(metadata, isDeclared, parentAnnotations, processedAnnotation); + @NonNull + private List getAddedStereotypes(@NonNull ProcessingContext context, + @Nullable T element) { + if (element == null) { + return List.of(); } - - handleAnnotationsWithMutatedMetadata(element, parentAnnotations, metadata, isDeclared, lastParent); - - if (!interceptorBindings.isEmpty()) { - for (AnnotationValueBuilder interceptorBinding : interceptorBindings) { - if (isDeclared) { - metadata.addDeclaredRepeatable(AnnotationUtil.ANN_INTERCEPTOR_BINDINGS, interceptorBinding.build()); - } else { - metadata.addRepeatable(AnnotationUtil.ANN_INTERCEPTOR_BINDINGS, interceptorBinding.build()); - } - } + CachedAnnotationMetadata modifiedStereotypes = MUTATED_ANNOTATION_METADATA.get(element); + if (modifiedStereotypes == null || modifiedStereotypes.isEmpty() || !modifiedStereotypes.isMutated()) { + return List.of(); } - } - - private void handleAnnotationsWithMutatedMetadata(T element, - List parentAnnotations, - MutableAnnotationMetadata metadata, - boolean isDeclared, - String lastParent) { - if (lastParent != null && element != null) { - CachedAnnotationMetadata modifiedStereotypes = MUTATED_ANNOTATION_METADATA.get(element); - if (modifiedStereotypes != null && !modifiedStereotypes.isEmpty() && modifiedStereotypes.isMutated()) { - for (String stereotypeName : modifiedStereotypes.getStereotypeAnnotationNames()) { + return Stream.concat( + modifiedStereotypes.getStereotypeAnnotationNames().stream().flatMap(stereotypeName -> { final AnnotationValue a = modifiedStereotypes.getAnnotation(stereotypeName); if (a == null) { - continue; + return Stream.of(); } + AnnotationValue parent = null; final List stereotypeParents = modifiedStereotypes.getAnnotationNamesByStereotype(stereotypeName); - List newParentAnnotations = new ArrayList<>(parentAnnotations); - newParentAnnotations.addAll(stereotypeParents); - - addStereotypeAnnotations( - Stream.of(toProcessedAnnotation(a)), - null, - newParentAnnotations, - metadata, - isDeclared - ); - } + for (String stereotype : stereotypeParents) { + AnnotationValue annotationValue = AnnotationValue.builder(stereotype).build(); + if (parent == null) { + parent = annotationValue; + } else { + parent = parent.mutate().stereotype(annotationValue).build(); + } + } + if (parent == null) { + return processAnnotation( + context.withParents(stereotypeParents), + toProcessedAnnotation(a) + ); + } else { + return processAnnotation( + context.withParents(stereotypeParents), + toProcessedAnnotation(parent.mutate().stereotype(a).build()) + ); + } - for (String annotationName : modifiedStereotypes.getAnnotationNames()) { + }), + modifiedStereotypes.getAnnotationNames().stream().flatMap(annotationName -> { AnnotationValue a = modifiedStereotypes.getAnnotation(annotationName); if (a == null) { - continue; + return Stream.empty(); } - addStereotypeAnnotations( - Stream.of(toProcessedAnnotation(a)), - null, - parentAnnotations, - metadata, - isDeclared + return processAnnotation( + context, + toProcessedAnnotation(a) ); - } - - } - } + }) + ).toList(); } - private AnnotationValue handleMemberBinding(DefaultAnnotationMetadata metadata, String lastParent, AnnotationValue annotationValue) { - Map data = annotationValue.getValues(); - if (!data.containsKey(InterceptorBindingQualifier.META_MEMBER_MEMBERS)) { - return annotationValue; - } - data = new LinkedHashMap<>(data); - final Object o = data.remove(InterceptorBindingQualifier.META_MEMBER_MEMBERS); - if (o instanceof Boolean && ((Boolean) o)) { - Map values = metadata.getValues(lastParent); - if (!values.isEmpty()) { - Set nonBinding = NON_BINDING_CACHE.computeIfAbsent(lastParent, (annotationName) -> { - final HashSet nonBindingResult = new HashSet<>(5); - Map members = getAnnotationMembers(lastParent); - if (CollectionUtils.isNotEmpty(members)) { - members.forEach((name, ann) -> { - if (hasSimpleAnnotation(ann, NonBinding.class.getSimpleName())) { - nonBindingResult.add(name); - } - }); - } - return nonBindingResult.isEmpty() ? Collections.emptySet() : nonBindingResult; - }); - - if (!nonBinding.isEmpty()) { - values = new HashMap<>(values); - values.keySet().removeAll(nonBinding); - } - final AnnotationValueBuilder builder = - AnnotationValue - .builder(lastParent) - .members(values); - data.put( - InterceptorBindingQualifier.META_MEMBER_MEMBERS, - builder.build() - ); - - } - } - return AnnotationValue.builder(annotationValue).members(data).build(); - } - - /** - * Gets the annotation members for the given type. - * - * @param annotationType The annotation type - * @return The members - * @since 3.3.0 - */ @NonNull - protected abstract Map getAnnotationMembers(@NonNull String annotationType); - - /** - * Returns true if a simple meta annotation is present for the given element and annotation type. - * - * @param element The element - * @param simpleName The simple name, ie {@link Class#getSimpleName()} - * @return True an annotation with the given simple name exists on the element - */ - protected abstract boolean hasSimpleAnnotation(T element, String simpleName); - - private void addToInterceptorBindingsIfNecessary(List> interceptorBindings, - String lastParent, - String annotationName) { - if (lastParent != null) { - AnnotationValueBuilder interceptorBinding = null; - if (AnnotationUtil.ANN_AROUND.equals(annotationName) || AnnotationUtil.ANN_INTERCEPTOR_BINDING.equals(annotationName)) { - interceptorBinding = AnnotationValue.builder(AnnotationUtil.ANN_INTERCEPTOR_BINDING) - .member(AnnotationMetadata.VALUE_MEMBER, new AnnotationClassValue<>(lastParent)) - .member("kind", "AROUND"); - } else if (AnnotationUtil.ANN_INTRODUCTION.equals(annotationName)) { - interceptorBinding = AnnotationValue.builder(AnnotationUtil.ANN_INTERCEPTOR_BINDING) - .member(AnnotationMetadata.VALUE_MEMBER, new AnnotationClassValue<>(lastParent)) - .member("kind", "INTRODUCTION"); - } else if (AnnotationUtil.ANN_AROUND_CONSTRUCT.equals(annotationName)) { - interceptorBinding = AnnotationValue.builder(AnnotationUtil.ANN_INTERCEPTOR_BINDING) - .member(AnnotationMetadata.VALUE_MEMBER, new AnnotationClassValue<>(lastParent)) - .member("kind", "AROUND_CONSTRUCT"); - } - if (interceptorBinding != null) { - interceptorBindings.add(interceptorBinding); - } - } - } - - private List eliminateProcessed(List visitors, Set> processedVisitors) { + private List eliminateProcessed(@NonNull ProcessingContext context, @NonNull List visitors) { if (visitors == null) { - return null; + return Collections.emptyList(); } - return visitors.stream().filter(v -> !processedVisitors.contains(v.getClass())).toList(); + return visitors.stream().filter(v -> !context.processedVisitors.contains(v.getClass())).toList(); } - private Stream processAnnotationRemappers(ProcessedAnnotation processedAnnotation, - Set> processedVisitors) { + @NonNull + private Stream processAnnotationRemappers(@NonNull ProcessingContext context, + @NonNull ProcessedAnnotation processedAnnotation) { AnnotationValue annotationValue = processedAnnotation.getAnnotationValue(); String packageName = NameUtils.getPackageName(annotationValue.getAnnotationName()); List annotationRemappers = ANNOTATION_REMAPPERS.get(packageName); - annotationRemappers = eliminateProcessed(annotationRemappers, processedVisitors); - if (CollectionUtils.isEmpty(annotationRemappers)) { + if (annotationRemappers == null) { + annotationRemappers = ALL_ANNOTATION_REMAPPERS; + } else { + annotationRemappers = CollectionUtils.concat(annotationRemappers, ALL_ANNOTATION_REMAPPERS); + } + annotationRemappers = eliminateProcessed(context, annotationRemappers); + return remapAnnotation( + context, + processedAnnotation, + annotationValue, + annotationRemappers.iterator() + ); + } + + @NonNull + private Stream remapAnnotation(@NonNull ProcessingContext context, + @NonNull ProcessedAnnotation processedAnnotation, + @NonNull AnnotationValue annotationValue, + @NonNull Iterator remappers) { + if (!remappers.hasNext()) { return Stream.of(processedAnnotation); } - VisitorContext visitorContext = createVisitorContext(); - List result = new ArrayList<>(); - for (AnnotationRemapper annotationRemapper : annotationRemappers) { - processedVisitors.add(annotationRemapper.getClass()); - for (AnnotationValue newAnnotationValue : annotationRemapper.remap(annotationValue, visitorContext)) { - if (newAnnotationValue == annotationValue) { - result.add(processedAnnotation); // Retain the same value - } else { - result.add(toProcessedAnnotation(newAnnotationValue)); - } + AnnotationRemapper annotationRemapper = remappers.next(); + ProcessingContext newContext = context.withProcessedVisitor(annotationRemapper.getClass()); + return annotationRemapper.remap(annotationValue, context.visitorContext).stream().flatMap(newAnnotationValue -> { + if (newAnnotationValue == annotationValue) { + // Value didn't change, continue with other remappers + return remapAnnotation(newContext, processedAnnotation, annotationValue, remappers); } - } - // Transform new remapped annotations - return result.stream().flatMap(annotation -> transform(annotation, processedVisitors)); + if (annotationValue.getAnnotationName().equals(newAnnotationValue.getAnnotationName())) { + // Retain the same value native element + return processAnnotation(newContext, processedAnnotation.withAnnotationValue(newAnnotationValue)); + } + return processAnnotation(newContext, toProcessedAnnotation(newAnnotationValue)); + }); } - private Stream processAnnotationTransformers(ProcessedAnnotation processedAnnotation, - Set> processedVisitors) { + private Stream processAnnotationTransformers(@NonNull ProcessingContext context, + @NonNull ProcessedAnnotation processedAnnotation) { AnnotationValue annotationValue = (AnnotationValue) processedAnnotation.getAnnotationValue(); List> annotationTransformers = getAnnotationTransformers(annotationValue.getAnnotationName()); - annotationTransformers = eliminateProcessed(annotationTransformers, processedVisitors); + annotationTransformers = eliminateProcessed(context, annotationTransformers); if (CollectionUtils.isEmpty(annotationTransformers)) { return Stream.of(processedAnnotation); } - VisitorContext visitorContext = createVisitorContext(); - List result = new ArrayList<>(); - for (AnnotationTransformer annotationTransformer : annotationTransformers) { - processedVisitors.add(annotationTransformer.getClass()); - for (AnnotationValue newAnnotationValue : annotationTransformer.transform(annotationValue, visitorContext)) { - if (newAnnotationValue == annotationValue) { - result.add(processedAnnotation); // Retain the same value - } else { - result.add(toProcessedAnnotation(newAnnotationValue)); - } - } + Iterator> transformers = annotationTransformers.iterator(); + return transformAnnotation(context, processedAnnotation, annotationValue, transformers); + } + + @NonNull + private Stream transformAnnotation(@NonNull ProcessingContext context, + @NonNull ProcessedAnnotation processedAnnotation, + @NonNull AnnotationValue annotationValue, + @NonNull Iterator> transformers) { + if (!transformers.hasNext()) { + return Stream.of(processedAnnotation); } - // Transform new transformed annotations - return result.stream().flatMap(annotation -> transform(annotation, processedVisitors)); + AnnotationTransformer annotationTransformer = transformers.next(); + ProcessingContext newContext = context.withProcessedVisitor(annotationTransformer.getClass()); + return annotationTransformer.transform(annotationValue, context.visitorContext).stream().flatMap(newAnnotationValue -> { + if (newAnnotationValue == annotationValue) { + // Value didn't change, continue with other transformers + return transformAnnotation(newContext, processedAnnotation, annotationValue, transformers); + } + if (annotationValue.getAnnotationName().equals(newAnnotationValue.getAnnotationName())) { + // Retain the same value native element + return processAnnotation(newContext, processedAnnotation.withAnnotationValue(newAnnotationValue)); + } + return processAnnotation(newContext, toProcessedAnnotation(newAnnotationValue)); + }); } - private Stream processAnnotationMappers(ProcessedAnnotation processedAnnotation, - Set> processedVisitors) { + @NonNull + private Stream processAnnotationMappers(@NonNull ProcessingContext context, + @NonNull ProcessedAnnotation processedAnnotation) { AnnotationValue annotationValue = (AnnotationValue) processedAnnotation.getAnnotationValue(); List> mappers = getAnnotationMappers(annotationValue.getAnnotationName()); - mappers = eliminateProcessed(mappers, processedVisitors); + mappers = eliminateProcessed(context, mappers); if (CollectionUtils.isEmpty(mappers)) { return Stream.of(processedAnnotation); } - VisitorContext visitorContext = createVisitorContext(); - List result = new ArrayList<>(); - result.add(processedAnnotation); // Mapper retains the original value - for (AnnotationMapper mapper : mappers) { - processedVisitors.add(mapper.getClass()); - List> mappedToAnnotationValues = mapper.map(annotationValue, visitorContext); - if (mappedToAnnotationValues != null) { - for (AnnotationValue mappedToAnnotationValue : mappedToAnnotationValues) { - if (mappedToAnnotationValue != annotationValue) { - result.add(toProcessedAnnotation(mappedToAnnotationValue)); - } - // else: Mapper returned the same value, but it's already included - } + return mappers.stream().flatMap(mapper -> { + Stream mappedAnnotationsStream; + ProcessingContext newContext = context.withProcessedVisitor(mapper.getClass()); + List> mappedToAnnotationValues = mapper.map(annotationValue, context.visitorContext); + if (mappedToAnnotationValues == null) { + mappedAnnotationsStream = Stream.empty(); + } else { + mappedAnnotationsStream = mappedToAnnotationValues + .stream() + .filter(newAnnotationValue -> newAnnotationValue != annotationValue) + .flatMap(newAnnotationValue -> processAnnotation(newContext, toProcessedAnnotation(newAnnotationValue))); } - } - // Transform new mapped annotations - return result.stream().flatMap(annotation -> transform(annotation, processedVisitors)); + return Stream.concat( + Stream.of(processedAnnotation), // Mapper retains the original value + mappedAnnotationsStream + ); + }); } - private ProcessedAnnotation toProcessedAnnotation(AnnotationValue av) { + @NonNull + private ProcessedAnnotation toProcessedAnnotation(@NonNull AnnotationValue av) { return new ProcessedAnnotation( - getAnnotationMirror(av.getAnnotationName()).orElse(null), - av + getAnnotationMirror(av.getAnnotationName()).orElse(null), + av ); } @@ -1534,9 +1421,7 @@ public static void copyToRuntime() { */ @Internal public static Set getMappedAnnotationNames() { - final HashSet all = new HashSet<>(ANNOTATION_MAPPERS.keySet()); - all.addAll(ANNOTATION_TRANSFORMERS.keySet()); - return all; + return CollectionUtils.concat(ANNOTATION_MAPPERS.keySet(), ANNOTATION_TRANSFORMERS.keySet()); } /** @@ -1560,10 +1445,10 @@ public AnnotationMetadata annotate(@NonNull AnnotationMe @NonNull AnnotationValue annotationValue) { return modify(annotationMetadata, metadata -> { addAnnotations( - metadata, - Stream.of(toProcessedAnnotation(annotationValue)), - true, - Collections.emptyList() + metadata, + Stream.of(toProcessedAnnotation(annotationValue)), + true, + false ); }); } @@ -1657,6 +1542,45 @@ private AnnotationMetadata modify(AnnotationMetadata annotationMetadata, Consume return mutableAnnotationMetadata; } + /** + * The context of the annotation processing. + * + * @param visitorContext The visitor context + * @param parentAnnotations The parent annotations + * @param processedVisitors The processed visitors + * @since 4.0.0 + */ + private record ProcessingContext(@NonNull VisitorContext visitorContext, + @NonNull Set parentAnnotations, + @NonNull Set> processedVisitors) { + + ProcessingContext(@NonNull VisitorContext visitorContext) { + this(visitorContext, Collections.emptySet(), Collections.emptySet()); + } + + boolean isProcessed(@NonNull AnnotationValue annotationValue) { + return parentAnnotations.contains(annotationValue.getAnnotationName()); + } + + @NonNull + ProcessingContext withParent(@NonNull AnnotationValue parent) { + Set parents = CollectionUtils.concat(parentAnnotations, parent.getAnnotationName()); + return new ProcessingContext(visitorContext, Collections.unmodifiableSet(parents), processedVisitors); + } + + @NonNull + ProcessingContext withParents(@NonNull List newParents) { + Set parents = CollectionUtils.concat(parentAnnotations, newParents); + return new ProcessingContext(visitorContext, Collections.unmodifiableSet(parents), processedVisitors); + } + + @NonNull + public ProcessingContext withProcessedVisitor(@NonNull Class processedVisitor) { + Set> visitors = CollectionUtils.concat(processedVisitors, processedVisitor); + return new ProcessingContext(visitorContext, parentAnnotations, Collections.unmodifiableSet(visitors)); + } + } + /** * Simple tuple object combining the annotation value plus the native annotation type. * NOTE: Some implementation like Groovy don't return correct annotation native type with type hierarchies. @@ -1669,7 +1593,8 @@ private final class ProcessedAnnotation { private final T annotationType; private final AnnotationValue annotationValue; - private ProcessedAnnotation(@Nullable T annotationType, AnnotationValue annotationValue) { + private ProcessedAnnotation(@Nullable T annotationType, + AnnotationValue annotationValue) { this.annotationType = annotationType; this.annotationValue = annotationValue; } @@ -1678,6 +1603,14 @@ public ProcessedAnnotation withAnnotationValue(AnnotationValue annotationValu return new ProcessedAnnotation(annotationType, annotationValue); } + public ProcessedAnnotation withStereotypes(List stereotypes) { + return new ProcessedAnnotation(annotationType, + annotationValue.mutate() + .replaceStereotypes(stereotypes.stream().>map(ProcessedAnnotation::getAnnotationValue).toList()) + .build() + ); + } + @Nullable public T getAnnotationType() { return annotationType; @@ -1686,6 +1619,7 @@ public T getAnnotationType() { public AnnotationValue getAnnotationValue() { return annotationValue; } + } /** diff --git a/core-processor/src/main/java/io/micronaut/inject/annotation/AnnotationRemapper.java b/core-processor/src/main/java/io/micronaut/inject/annotation/AnnotationRemapper.java index 5979b167fa4..7569c3193d4 100644 --- a/core-processor/src/main/java/io/micronaut/inject/annotation/AnnotationRemapper.java +++ b/core-processor/src/main/java/io/micronaut/inject/annotation/AnnotationRemapper.java @@ -16,6 +16,7 @@ package io.micronaut.inject.annotation; import io.micronaut.core.annotation.AnnotationValue; +import io.micronaut.core.annotation.Experimental; import io.micronaut.inject.visitor.VisitorContext; import io.micronaut.core.annotation.NonNull; @@ -34,11 +35,19 @@ * similar in function, for example {@code javax.annotation.Nullable} and {@code io.micronaut.core.annotation.Nullable}. One can * remap these to a single annotation internally at compilation time.

* + * NOTE: Remapping all packages is an experimental feature and might be replaced in the future with more efficient way. + * * @author graemerocher * @since 1.2.0 */ public interface AnnotationRemapper { + /** + * Return this value in {@link #getPackageName()} to trigger remap on all annotations. + */ + @Experimental + String ALL_PACKAGES = "*"; + /** * @return The package name of the annotation. */ diff --git a/core-processor/src/main/java/io/micronaut/inject/annotation/internal/InterceptorBindingMembers.java b/core-processor/src/main/java/io/micronaut/inject/annotation/internal/InterceptorBindingMembers.java new file mode 100644 index 00000000000..ad500b2cd3c --- /dev/null +++ b/core-processor/src/main/java/io/micronaut/inject/annotation/internal/InterceptorBindingMembers.java @@ -0,0 +1,142 @@ +/* + * Copyright 2017-2023 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.inject.annotation.internal; + +import io.micronaut.aop.Around; +import io.micronaut.aop.AroundConstruct; +import io.micronaut.aop.InterceptorBinding; +import io.micronaut.aop.InterceptorKind; +import io.micronaut.aop.Introduction; +import io.micronaut.context.annotation.Type; +import io.micronaut.core.annotation.AnnotationClassValue; +import io.micronaut.core.annotation.AnnotationMetadata; +import io.micronaut.core.annotation.AnnotationUtil; +import io.micronaut.core.annotation.AnnotationValue; +import io.micronaut.core.annotation.AnnotationValueBuilder; +import io.micronaut.core.annotation.Internal; +import io.micronaut.inject.annotation.AnnotationRemapper; +import io.micronaut.inject.qualifiers.InterceptorBindingQualifier; +import io.micronaut.inject.visitor.VisitorContext; + +import java.lang.annotation.Annotation; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * The remapped for various interceptor annotation stereotypes. + * + * @author Denis Stepanov + * @since 4.0.0 + */ +@Internal +public final class InterceptorBindingMembers implements AnnotationRemapper { + + private static final Set SKIP_ANNOTATIONS = Set.of( + Around.class.getName(), AroundConstruct.class.getName(), InterceptorBinding.class.getName(), Introduction.class.getName() + ); + + @Override + public String getPackageName() { + return ALL_PACKAGES; + } + + @Override + public List> remap(AnnotationValue annotationValue, VisitorContext visitorContext) { + if (annotationValue.getStereotypes() == null) { + return List.of(annotationValue); + } + String annotationName = annotationValue.getAnnotationName(); + if (SKIP_ANNOTATIONS.contains(annotationName)) { + return List.of(annotationValue.mutate().replaceStereotypes(Collections.emptyList()).build()); + } + + List> interceptorBindings = new ArrayList<>(); + for (AnnotationValue stereotype : annotationValue.getStereotypes()) { + String stereotypeName = stereotype.getAnnotationName(); + AnnotationValueBuilder newInterceptorBinding = null; + if (InterceptorBinding.class.getName().equals(stereotypeName)) { + newInterceptorBinding = stereotype.mutate() + .member(AnnotationMetadata.VALUE_MEMBER, new AnnotationClassValue<>(annotationName)); + + if (stereotype.booleanValue(InterceptorBinding.META_BIND_MEMBERS).orElse(false)) { + String[] nonBinding = annotationValue.stringValues(AnnotationUtil.NON_BINDING_ATTRIBUTE); + Map bindingValues = annotationValue.getValues(); + bindingValues = new LinkedHashMap<>(bindingValues); + Arrays.asList(nonBinding).forEach(bindingValues.keySet()::remove); + + AnnotationValue binding = AnnotationValue.builder(annotationValue.getAnnotationName()) + .members(bindingValues) + .build(); + newInterceptorBinding.member(InterceptorBindingQualifier.META_BINDING_VALUES, binding); + } + } else if (Around.class.getName().equals(stereotypeName)) { + newInterceptorBinding = AnnotationValue.builder(InterceptorBinding.class) + .member(AnnotationMetadata.VALUE_MEMBER, new AnnotationClassValue<>(annotationName)) + .member("kind", InterceptorKind.AROUND); + } else if (Introduction.class.getName().equals(stereotypeName)) { + newInterceptorBinding = AnnotationValue.builder(InterceptorBinding.class) + .member(AnnotationMetadata.VALUE_MEMBER, new AnnotationClassValue<>(annotationName)) + .member("kind", InterceptorKind.INTRODUCTION); + } else if (AroundConstruct.class.getName().equals(stereotypeName)) { + newInterceptorBinding = AnnotationValue.builder(InterceptorBinding.class) + .member(AnnotationMetadata.VALUE_MEMBER, new AnnotationClassValue<>(annotationName)) + .member("kind", InterceptorKind.AROUND_CONSTRUCT); + } + if (newInterceptorBinding != null) { + interceptorBindings.add(newInterceptorBinding); + } + } + final boolean hasInterceptorBinding = !interceptorBindings.isEmpty(); + if (hasInterceptorBinding) { + for (AnnotationValue av : annotationValue.getStereotypes()) { + if (Type.class.getName().equals(av.getAnnotationName())) { + final Object o = av.getValues().get(AnnotationMetadata.VALUE_MEMBER); + AnnotationClassValue interceptorType = null; + if (o instanceof AnnotationClassValue annotationClassValue) { + interceptorType = annotationClassValue; + } else if (o instanceof AnnotationClassValue[] annotationClassValues) { + if (annotationClassValues.length > 0) { + interceptorType = annotationClassValues[0]; + } + } + if (interceptorType != null) { + for (AnnotationValueBuilder interceptorBinding : interceptorBindings) { + interceptorBinding.member("interceptorType", interceptorType); + } + } + break; + } + } + } + + if (!interceptorBindings.isEmpty()) { + AnnotationValue[] interceptors = interceptorBindings.stream().map(AnnotationValueBuilder::build).toArray(AnnotationValue[]::new); + AnnotationValue interceptorsContainer = AnnotationValue.builder(AnnotationUtil.ANN_INTERCEPTOR_BINDINGS) + .values(interceptors) + .build(); + annotationValue = annotationValue.mutate() + .stereotype(interceptorsContainer) + .build(); + } + return List.of(annotationValue); + } + +} diff --git a/core-processor/src/main/java/io/micronaut/inject/annotation/internal/QualifierBindingMembers.java b/core-processor/src/main/java/io/micronaut/inject/annotation/internal/QualifierBindingMembers.java new file mode 100644 index 00000000000..1ed6d6f2c9c --- /dev/null +++ b/core-processor/src/main/java/io/micronaut/inject/annotation/internal/QualifierBindingMembers.java @@ -0,0 +1,61 @@ +/* + * Copyright 2017-2023 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.inject.annotation.internal; + +import io.micronaut.core.annotation.AnnotationUtil; +import io.micronaut.core.annotation.AnnotationValue; +import io.micronaut.core.annotation.Internal; +import io.micronaut.inject.annotation.AnnotationRemapper; +import io.micronaut.inject.visitor.VisitorContext; +import jakarta.inject.Qualifier; + +import java.util.List; +import java.util.Optional; + +/** + * The remapped adds a non-binding attribute to any qualifiers that are stereotypes. + * + * @author Denis Stepanov + * @since 4.0.0 + */ +@Internal +public final class QualifierBindingMembers implements AnnotationRemapper { + + @Override + public String getPackageName() { + return ALL_PACKAGES; + } + + @Override + public List> remap(AnnotationValue annotation, VisitorContext visitorContext) { + if (annotation.getStereotypes() != null) { + String[] nonBindingMembers = annotation.stringValues(AnnotationUtil.NON_BINDING_ATTRIBUTE); + if (nonBindingMembers.length > 0) { + Optional> qualifier = annotation.getStereotypes() + .stream() + .filter(av -> av.getAnnotationName().equals(AnnotationUtil.QUALIFIER) || av.getAnnotationName().equals(Qualifier.class.getName())) + .findFirst(); + if (qualifier.isPresent()) { + AnnotationValue originalQualifier = qualifier.get(); + AnnotationValue newQualifier = originalQualifier.mutate() + .member(AnnotationUtil.NON_BINDING_ATTRIBUTE, nonBindingMembers).build(); + annotation = annotation.mutate().replaceStereotype(originalQualifier, newQualifier).build(); + } + } + } + return List.of(annotation); + } +} diff --git a/core-processor/src/main/java/io/micronaut/inject/processing/BeanDefinitionCreatorFactory.java b/core-processor/src/main/java/io/micronaut/inject/processing/BeanDefinitionCreatorFactory.java index 8b386174870..5298adce804 100644 --- a/core-processor/src/main/java/io/micronaut/inject/processing/BeanDefinitionCreatorFactory.java +++ b/core-processor/src/main/java/io/micronaut/inject/processing/BeanDefinitionCreatorFactory.java @@ -43,7 +43,7 @@ public abstract class BeanDefinitionCreatorFactory { @NonNull public static BeanDefinitionCreator produce(ClassElement classElement, VisitorContext visitorContext) { boolean isAbstract = classElement.isAbstract(); - boolean isIntroduction = classElement.hasStereotype(AnnotationUtil.ANN_INTRODUCTION); + boolean isIntroduction = isIntroduction(classElement); if (ConfigurationReaderBeanElementCreator.isConfigurationProperties(classElement)) { if (classElement.isInterface()) { return new IntroductionInterfaceBeanElementCreator(classElement, visitorContext); @@ -123,4 +123,8 @@ public static boolean isDeclaredBeanInMetadata(AnnotationMetadata concreteClassM concreteClassMetadata.hasStereotype(DefaultScope.class); } + public static boolean isIntroduction(AnnotationMetadata metadata) { + return InterceptedMethodUtil.hasIntroductionStereotype(metadata); + } + } diff --git a/core-processor/src/main/resources/META-INF/services/io.micronaut.inject.annotation.AnnotationRemapper b/core-processor/src/main/resources/META-INF/services/io.micronaut.inject.annotation.AnnotationRemapper index 548aa165be4..2e8b0c71a0a 100644 --- a/core-processor/src/main/resources/META-INF/services/io.micronaut.inject.annotation.AnnotationRemapper +++ b/core-processor/src/main/resources/META-INF/services/io.micronaut.inject.annotation.AnnotationRemapper @@ -1,3 +1,5 @@ io.micronaut.inject.annotation.internal.FindBugsRemapper io.micronaut.inject.annotation.internal.JakartaRemapper +io.micronaut.inject.annotation.internal.QualifierBindingMembers +io.micronaut.inject.annotation.internal.InterceptorBindingMembers diff --git a/core/src/main/java/io/micronaut/core/annotation/AnnotationUtil.java b/core/src/main/java/io/micronaut/core/annotation/AnnotationUtil.java index ec81d4b2ab6..41c1d2bfb15 100644 --- a/core/src/main/java/io/micronaut/core/annotation/AnnotationUtil.java +++ b/core/src/main/java/io/micronaut/core/annotation/AnnotationUtil.java @@ -15,12 +15,20 @@ */ package io.micronaut.core.annotation; -import io.micronaut.core.util.CollectionUtils; import io.micronaut.core.util.StringUtils; -import java.lang.annotation.*; +import java.lang.annotation.Annotation; +import java.lang.annotation.Documented; +import java.lang.annotation.Inherited; +import java.lang.annotation.Repeatable; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; import java.lang.reflect.AnnotatedElement; -import java.util.*; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Objects; import java.util.concurrent.ConcurrentHashMap; /** @@ -43,7 +51,6 @@ public class AnnotationUtil { "javax.annotation.meta.TypeQualifierNickname", "kotlin.annotation.Retention", "kotlin.Annotation", - Inherited.class.getName(), SuppressWarnings.class.getName(), Override.class.getName(), Repeatable.class.getName(), @@ -137,15 +144,6 @@ public Annotation[] getDeclaredAnnotations() { */ public static final String ANN_INTERCEPTOR_BINDING_QUALIFIER = "io.micronaut.inject.qualifiers.InterceptorBindingQualifier"; - /** - * The advice stereotypes. - */ - public static final Set ADVICE_STEREOTYPES = CollectionUtils.setOf( - ANN_AROUND, - ANN_AROUND_CONSTRUCT, - ANN_INTRODUCTION - ); - /** * Name of the repeatable interceptor bindings type. */ @@ -186,6 +184,16 @@ public Annotation[] getDeclaredAnnotations() { */ public static final String POST_CONSTRUCT = "javax.annotation.PostConstruct"; + /** + * The annotation attribute containing all the attributes marked as non binding. + */ + public static final String NON_BINDING_ATTRIBUTE = "$nonBinding"; + + /** + * The inherited annotation. + */ + public static final String ANN_INHERITED = Inherited.class.getName(); + private static final Map> INTERN_LIST_POOL = new ConcurrentHashMap<>(); private static final Map> INTERN_MAP_POOL = new ConcurrentHashMap<>(); diff --git a/core/src/main/java/io/micronaut/core/annotation/AnnotationValue.java b/core/src/main/java/io/micronaut/core/annotation/AnnotationValue.java index fc9efb1e69c..3a0c022f86b 100644 --- a/core/src/main/java/io/micronaut/core/annotation/AnnotationValue.java +++ b/core/src/main/java/io/micronaut/core/annotation/AnnotationValue.java @@ -43,6 +43,9 @@ *

If a member is not present then the methods of the class will attempt to resolve the default value for a given annotation member. In this sense the behaviour of this class is similar to how * a implementation of {@link Annotation} behaves.

* + * NOTE: During the mapping or remapping, nullable stereotypes value means that + * the stereotypes will be filled from the annotation definition, when empty collection will skip it. + * * @param The annotation type * @author Graeme Rocher * @since 1.0 @@ -179,6 +182,16 @@ protected AnnotationValue(AnnotationValue target, this.stereotypes = null; } + /** + * Creates a builder with the initial value of this annotation. + * + * @return The builder with this annotation value + * @since 4.0.0 + */ + public AnnotationValueBuilder mutate() { + return builder(this); + } + /** * @return The retention policy. */ diff --git a/core/src/main/java/io/micronaut/core/annotation/AnnotationValueBuilder.java b/core/src/main/java/io/micronaut/core/annotation/AnnotationValueBuilder.java index 2f34139521a..ad7098e790d 100644 --- a/core/src/main/java/io/micronaut/core/annotation/AnnotationValueBuilder.java +++ b/core/src/main/java/io/micronaut/core/annotation/AnnotationValueBuilder.java @@ -20,9 +20,11 @@ import java.lang.annotation.Annotation; import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; +import java.util.Collection; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Objects; /** * A build for annotation values. @@ -84,7 +86,7 @@ public class AnnotationValueBuilder { this.annotationName = value.getAnnotationName(); this.values.putAll(value.getValues()); this.defaultValues = value.getDefaultValues(); - this.stereotypes = value.getStereotypes(); + this.stereotypes = value.getStereotypes() == null ? null : new ArrayList<>(value.getStereotypes()); this.retentionPolicy = retentionPolicy != null ? retentionPolicy : RetentionPolicy.RUNTIME; } @@ -115,6 +117,77 @@ public AnnotationValueBuilder stereotype(AnnotationValue annotation) { return this; } + /** + * Replaces the stereotype annotation. + * + * @param originalAnnotationValue The original annotation value + * @param newAnnotationValue The new annotation value + * @return This builder + * @since 4.0.0 + */ + @NonNull + public AnnotationValueBuilder replaceStereotype(@NonNull AnnotationValue originalAnnotationValue, + @NonNull AnnotationValue newAnnotationValue) { + Objects.requireNonNull(stereotypes); + List> values = new ArrayList<>(stereotypes); + int index = values.indexOf(originalAnnotationValue); + if (index < 0) { + throw new IllegalArgumentException("Unknown original annotation value!"); + } + values.set(index, newAnnotationValue); + stereotypes = values; + return this; + } + + /** + * Removes the stereotype annotation. + * + * @param annotationValueToRemove The annotation value to remove + * @return This builder + * @since 4.0.0 + */ + @NonNull + public AnnotationValueBuilder removeStereotype(@NonNull AnnotationValue annotationValueToRemove) { + Objects.requireNonNull(stereotypes); + List> values = new ArrayList<>(stereotypes); + int index = values.indexOf(annotationValueToRemove); + if (index < 0) { + throw new IllegalArgumentException("Unknown annotation value!"); + } + values.remove(index); + stereotypes = values; + return this; + } + + /** + * Adds a stereotypes of the annotation. + * + * @param newStereotypes The stereotypes + * @return This builder + * @since 4.0.0 + */ + @NonNull + public AnnotationValueBuilder stereotypes(@NonNull Collection> newStereotypes) { + if (stereotypes == null) { + stereotypes = new ArrayList<>(10); + } + stereotypes.addAll(newStereotypes); + return this; + } + + /** + * Replaces stereotypes of the annotation. + * + * @param newStereotypes The stereotypes + * @return This builder + * @since 4.0.0 + */ + @NonNull + public AnnotationValueBuilder replaceStereotypes(@NonNull Collection> newStereotypes) { + stereotypes = new ArrayList<>(newStereotypes); + return this; + } + /** * Sets the default values of the annotation. * diff --git a/core/src/main/java/io/micronaut/core/util/CollectionUtils.java b/core/src/main/java/io/micronaut/core/util/CollectionUtils.java index 98fbc1d5f67..09872084e53 100644 --- a/core/src/main/java/io/micronaut/core/util/CollectionUtils.java +++ b/core/src/main/java/io/micronaut/core/util/CollectionUtils.java @@ -31,6 +31,70 @@ */ public class CollectionUtils { + /** + * The method will merge the set and element into a new set. + * + * @param set The set + * @param element The element + * @param The element type + * @return The new set + * @since 4.0.0 + */ + public static Set concat(Set set, E element) { + Set newList = CollectionUtils.newHashSet(set.size() + 1); + newList.addAll(set); + newList.add(element); + return newList; + } + + /** + * The method will merge two sets into a new set. + * + * @param set1 The first set + * @param collection The second collection + * @param The element type + * @return The new set + * @since 4.0.0 + */ + public static Set concat(Set set1, Collection collection) { + Set newSet = newHashSet(set1.size() + collection.size()); + newSet.addAll(set1); + newSet.addAll(collection); + return newSet; + } + + /** + * The method will merge the list and element into a new list. + * + * @param list The list + * @param element The element + * @param The element type + * @return The new list + * @since 4.0.0 + */ + public static List concat(List list, E element) { + List newList = new ArrayList<>(list.size() + 1); + newList.addAll(list); + newList.add(element); + return newList; + } + + /** + * The method will merge two list into a new list. + * + * @param list1 The first list + * @param collection The second collection + * @param The element type + * @return The new list + * @since 4.0.0 + */ + public static List concat(List list1, Collection collection) { + List newList = new ArrayList<>(list1.size() + collection.size()); + newList.addAll(list1); + newList.addAll(collection); + return newList; + } + /** * Create new {@link HashSet} sized to fit all the elements of the size provided. * @param size The size to fit all the elements @@ -279,7 +343,7 @@ public static String toString(String delimiter, Iterable iterable) { if (o == null) { continue; } else { - if (CharSequence.class.isInstance(o)) { + if (o instanceof CharSequence) { builder.append(o); } else { Optional converted = ConversionService.SHARED.convert(o, String.class); diff --git a/inject-groovy-test/src/main/groovy/io/micronaut/ast/transform/test/AbstractBeanDefinitionSpec.groovy b/inject-groovy-test/src/main/groovy/io/micronaut/ast/transform/test/AbstractBeanDefinitionSpec.groovy index ed4f8eb798d..64c26a7796b 100644 --- a/inject-groovy-test/src/main/groovy/io/micronaut/ast/transform/test/AbstractBeanDefinitionSpec.groovy +++ b/inject-groovy-test/src/main/groovy/io/micronaut/ast/transform/test/AbstractBeanDefinitionSpec.groovy @@ -218,9 +218,9 @@ abstract class AbstractBeanDefinitionSpec extends Specification { return element } - + @CompileStatic protected AnnotationMetadata writeAndLoadMetadata(String className, AnnotationMetadata toWrite) { - def stream = new ByteArrayOutputStream() + ByteArrayOutputStream stream = new ByteArrayOutputStream() new AnnotationMetadataWriter(className, null, toWrite, true) .writeTo(stream) className = className + AnnotationMetadata.CLASS_NAME_SUFFIX @@ -228,7 +228,7 @@ abstract class AbstractBeanDefinitionSpec extends Specification { @Override protected Class findClass(String name) throws ClassNotFoundException { if (name == className) { - def bytes = stream.toByteArray() + byte[] bytes = stream.toByteArray() return super.defineClass(name, bytes, 0, bytes.length) } return super.findClass(name) diff --git a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/annotation/GroovyAnnotationMetadataBuilder.java b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/annotation/GroovyAnnotationMetadataBuilder.java index 2cddaa913c5..3245cce1ae3 100644 --- a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/annotation/GroovyAnnotationMetadataBuilder.java +++ b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/annotation/GroovyAnnotationMetadataBuilder.java @@ -59,7 +59,6 @@ import org.codehaus.groovy.control.SourceUnit; import java.lang.annotation.Annotation; -import java.lang.annotation.Inherited; import java.lang.annotation.Repeatable; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -386,41 +385,6 @@ protected void readAnnotationRawValues( return defaultValues; } - @Override - protected boolean isInheritedAnnotation(@NonNull AnnotationNode annotationMirror) { - final List annotations = annotationMirror.getClassNode().getAnnotations(); - if (CollectionUtils.isNotEmpty(annotations)) { - return annotations.stream().anyMatch((ann) -> - ann.getClassNode().getName().equals(Inherited.class.getName()) - ); - } - return false; - } - - @Override - protected Map getAnnotationMembers(String annotationType) { - final AnnotatedNode node = getAnnotationMirror(annotationType).orElse(null); - if (node instanceof final ClassNode cn) { - if (cn.isAnnotationDefinition()) { - return cn.getDeclaredMethodsMap(); - } - } - return Collections.emptyMap(); - } - - @Override - protected boolean hasSimpleAnnotation(AnnotatedNode element, String simpleName) { - if (element != null) { - final List annotations = element.getAnnotations(); - for (AnnotationNode ann : annotations) { - if (ann.getClassNode().getNameWithoutPackage().equalsIgnoreCase(simpleName)) { - return true; - } - } - } - return false; - } - @Override protected Object readAnnotationValue(AnnotatedNode originatingElement, AnnotatedNode member, String memberName, Object annotationValue) { if (annotationValue instanceof ConstantExpression constantExpression) { diff --git a/inject-groovy/src/test/groovy/io/micronaut/inject/configproperties/InheritedConfigurationReaderPrefixSpec.groovy b/inject-groovy/src/test/groovy/io/micronaut/inject/configproperties/InheritedConfigurationReaderPrefixSpec.groovy index 66ea10f7753..538a963150a 100644 --- a/inject-groovy/src/test/groovy/io/micronaut/inject/configproperties/InheritedConfigurationReaderPrefixSpec.groovy +++ b/inject-groovy/src/test/groovy/io/micronaut/inject/configproperties/InheritedConfigurationReaderPrefixSpec.groovy @@ -37,7 +37,7 @@ class MyBean { beanDefinition.getInjectedMethods()[0].name == 'setMyValue' def metadata = beanDefinition.getInjectedMethods()[0].getAnnotationMetadata() metadata.hasAnnotation(Property) - metadata.getValue(Property, "name", String).get() == 'simple.my-value' + metadata.getValue(Property, "name", String).get() == 'endpoints.my-value' } void "property path is overriding the existing one without base prefix"() { @@ -56,7 +56,7 @@ class MyBean { beanDefinition.getInjectedMethods()[0].name == 'setMyValue' def metadata = beanDefinition.getInjectedMethods()[0].getAnnotationMetadata() metadata.hasAnnotation(Property) - metadata.getValue(Property, "name", String).get() == 'simple.my-value' + metadata.getValue(Property, "name", String).get() == 'endpoints.my-value' } void "property path is broken because alias is pointing to another alias 2"() { diff --git a/inject-java/src/main/java/io/micronaut/annotation/processing/BeanDefinitionInjectProcessor.java b/inject-java/src/main/java/io/micronaut/annotation/processing/BeanDefinitionInjectProcessor.java index 60f39b11e52..c2f80ba9d23 100644 --- a/inject-java/src/main/java/io/micronaut/annotation/processing/BeanDefinitionInjectProcessor.java +++ b/inject-java/src/main/java/io/micronaut/annotation/processing/BeanDefinitionInjectProcessor.java @@ -72,8 +72,6 @@ @SupportedOptions({AbstractInjectAnnotationProcessor.MICRONAUT_PROCESSING_INCREMENTAL, AbstractInjectAnnotationProcessor.MICRONAUT_PROCESSING_ANNOTATIONS, BeanDefinitionWriter.OMIT_CONFPROP_INJECTION_POINTS}) public class BeanDefinitionInjectProcessor extends AbstractInjectAnnotationProcessor { - private static final String AROUND_TYPE = AnnotationUtil.ANN_AROUND; - private static final String INTRODUCTION_TYPE = AnnotationUtil.ANN_INTRODUCTION; private static final String[] ANNOTATION_STEREOTYPES = new String[]{ AnnotationUtil.POST_CONSTRUCT, AnnotationUtil.PRE_DESTROY, @@ -90,10 +88,10 @@ public class BeanDefinitionInjectProcessor extends AbstractInjectAnnotationProce "io.micronaut.context.annotation.Value", "io.micronaut.context.annotation.Property", "io.micronaut.context.annotation.Executable", - AROUND_TYPE, + AnnotationUtil.ANN_AROUND, AnnotationUtil.ANN_INTERCEPTOR_BINDINGS, AnnotationUtil.ANN_INTERCEPTOR_BINDING, - INTRODUCTION_TYPE + AnnotationUtil.ANN_INTRODUCTION }; private Set beanDefinitions; @@ -178,7 +176,7 @@ public final boolean process(Set annotations, RoundEnviro beanDefinitions.add(name); } else { AnnotationMetadata annotationMetadata = annotationMetadataBuilder.lookupOrBuildForType(typeElement); - if (annotationMetadata.hasStereotype(INTRODUCTION_TYPE) || annotationMetadata.hasStereotype(ConfigurationReader.class)) { + if (BeanDefinitionCreatorFactory.isIntroduction(annotationMetadata) || annotationMetadata.hasStereotype(ConfigurationReader.class)) { beanDefinitions.add(name); } } diff --git a/inject-java/src/main/java/io/micronaut/annotation/processing/JavaAnnotationMetadataBuilder.java b/inject-java/src/main/java/io/micronaut/annotation/processing/JavaAnnotationMetadataBuilder.java index 5be680c7618..9b6551463b9 100644 --- a/inject-java/src/main/java/io/micronaut/annotation/processing/JavaAnnotationMetadataBuilder.java +++ b/inject-java/src/main/java/io/micronaut/annotation/processing/JavaAnnotationMetadataBuilder.java @@ -48,7 +48,6 @@ import javax.lang.model.util.Types; import javax.tools.Diagnostic; import java.lang.annotation.Annotation; -import java.lang.annotation.Inherited; import java.lang.annotation.Repeatable; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -185,51 +184,6 @@ protected RetentionPolicy getRetentionPolicy(@NonNull Element annotation) { return RetentionPolicy.RUNTIME; } - @Override - protected boolean isInheritedAnnotation(@NonNull AnnotationMirror annotationMirror) { - final List annotationMirrors = annotationMirror.getAnnotationType().asElement().getAnnotationMirrors(); - for (AnnotationMirror mirror : annotationMirrors) { - if (getAnnotationTypeName(mirror).equals(Inherited.class.getName())) { - return true; - } - } - return false; - } - - @Override - protected Map getAnnotationMembers(String annotationType) { - final Element element = getAnnotationMirror(annotationType).orElse(null); - if (element != null && element.getKind() == ElementKind.ANNOTATION_TYPE) { - final List elements = element.getEnclosedElements(); - if (elements.isEmpty()) { - return Collections.emptyMap(); - } else { - Map members = new LinkedHashMap<>(elements.size()); - for (Element method : elements) { - members.put(method.getSimpleName().toString(), method); - } - return Collections.unmodifiableMap(members); - } - } - return Collections.emptyMap(); - } - - @Override - protected boolean hasSimpleAnnotation(Element element, String simpleName) { - if (element != null) { - final List mirrors = element.getAnnotationMirrors(); - for (AnnotationMirror mirror : mirrors) { - final String s = mirror.getAnnotationType() - .asElement() - .getSimpleName().toString(); - if (s.equalsIgnoreCase(simpleName)) { - return true; - } - } - } - return false; - } - @Override protected Element getTypeForAnnotation(AnnotationMirror annotationMirror) { return annotationMirror.getAnnotationType().asElement(); diff --git a/inject-java/src/test/groovy/io/micronaut/annotation/mapping/MyGet1.java b/inject-java/src/test/groovy/io/micronaut/annotation/mapping/MyGet1.java new file mode 100644 index 00000000000..653207c84f4 --- /dev/null +++ b/inject-java/src/test/groovy/io/micronaut/annotation/mapping/MyGet1.java @@ -0,0 +1,14 @@ +package io.micronaut.annotation.mapping; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +@Documented +@Retention(RUNTIME) +@Target({ElementType.METHOD}) +public @interface MyGet1 { +} diff --git a/inject-java/src/test/groovy/io/micronaut/annotation/mapping/MyGet2.java b/inject-java/src/test/groovy/io/micronaut/annotation/mapping/MyGet2.java new file mode 100644 index 00000000000..70b84061bd1 --- /dev/null +++ b/inject-java/src/test/groovy/io/micronaut/annotation/mapping/MyGet2.java @@ -0,0 +1,14 @@ +package io.micronaut.annotation.mapping; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +@Documented +@Retention(RUNTIME) +@Target({ElementType.METHOD}) +public @interface MyGet2 { +} diff --git a/inject-java/src/test/groovy/io/micronaut/annotation/mapping/TransformNotInheritedAnnotationSpec.groovy b/inject-java/src/test/groovy/io/micronaut/annotation/mapping/TransformNotInheritedAnnotationSpec.groovy new file mode 100644 index 00000000000..2def834da71 --- /dev/null +++ b/inject-java/src/test/groovy/io/micronaut/annotation/mapping/TransformNotInheritedAnnotationSpec.groovy @@ -0,0 +1,56 @@ +package io.micronaut.annotation.mapping + +import io.micronaut.annotation.processing.test.AbstractTypeElementSpec +import io.micronaut.core.annotation.AnnotationValue +import io.micronaut.http.annotation.Get +import io.micronaut.http.annotation.HttpMethodMapping +import io.micronaut.inject.annotation.TypedAnnotationTransformer +import io.micronaut.inject.visitor.VisitorContext + +class TransformNotInheritedAnnotationSpec extends AbstractTypeElementSpec { + + void 'test transforming'() { + given: + def definition = buildBeanDefinition('addann.TransformNotInherited', ''' +package addann; + +import io.micronaut.context.annotation.Bean; + +interface MyInterface { + + @io.micronaut.annotation.mapping.MyGet1 + String getHelloWorld(); +} + +@Bean +class TransformNotInherited implements MyInterface { + + @Override + public String getHelloWorld() { + return "Hello world"; + } +} +''') + expect: + definition.getRequiredMethod("getHelloWorld").hasAnnotation(Get) + definition.getRequiredMethod("getHelloWorld").hasStereotype(HttpMethodMapping) + } + + static class TheAnnotationMapper implements TypedAnnotationTransformer { + + + @Override + List> transform(AnnotationValue annotation, VisitorContext visitorContext) { + return List.of( + AnnotationValue.builder(Get.class).build() + ) + } + + @Override + Class annotationType() { + return MyGet1.class + } + } + + +} diff --git a/inject-java/src/test/groovy/io/micronaut/annotation/mapping/TransformToInheritedAnnotationSpec.groovy b/inject-java/src/test/groovy/io/micronaut/annotation/mapping/TransformToInheritedAnnotationSpec.groovy new file mode 100644 index 00000000000..b33e2cf364b --- /dev/null +++ b/inject-java/src/test/groovy/io/micronaut/annotation/mapping/TransformToInheritedAnnotationSpec.groovy @@ -0,0 +1,59 @@ +package io.micronaut.annotation.mapping + +import io.micronaut.annotation.processing.test.AbstractTypeElementSpec +import io.micronaut.core.annotation.AnnotationValue +import io.micronaut.inject.annotation.TypedAnnotationTransformer +import io.micronaut.inject.visitor.VisitorContext + +import java.lang.annotation.Inherited + +class TransformToInheritedAnnotationSpec extends AbstractTypeElementSpec { + + void 'test transforming'() { + given: + def definition = buildBeanDefinition('addann.TransformToInherited', ''' +package addann; + +import io.micronaut.context.annotation.Bean; +import io.micronaut.context.annotation.Executable; + +interface MyInterfaceX { + + @io.micronaut.annotation.mapping.MyGet2 + @Executable + String getHelloWorld(); +} + +@Bean +class TransformToInherited implements MyInterfaceX { + + @Override + public String getHelloWorld() { + return "Hello world"; + } +} +''') + expect: + definition.getRequiredMethod("getHelloWorld").hasAnnotation(MyGet2) + } + + static class TheAnnotationMapper implements TypedAnnotationTransformer { + + + @Override + List> transform(AnnotationValue annotation, VisitorContext visitorContext) { + return List.of( + annotation.mutate().stereotype( + AnnotationValue.builder(Inherited).build() + ).build() + ) + } + + @Override + Class annotationType() { + return MyGet2.class + } + } + + +} diff --git a/inject-java/src/test/groovy/io/micronaut/inject/configproperties/InheritedConfigurationReaderPrefixSpec.groovy b/inject-java/src/test/groovy/io/micronaut/inject/configproperties/InheritedConfigurationReaderPrefixSpec.groovy index b8131792119..c7bd57d83ba 100644 --- a/inject-java/src/test/groovy/io/micronaut/inject/configproperties/InheritedConfigurationReaderPrefixSpec.groovy +++ b/inject-java/src/test/groovy/io/micronaut/inject/configproperties/InheritedConfigurationReaderPrefixSpec.groovy @@ -45,7 +45,7 @@ class MyBean { beanDefinition.getInjectedMethods()[0].name == 'setMyValue' def metadata = beanDefinition.getInjectedMethods()[0].getAnnotationMetadata() metadata.hasAnnotation(Property) - metadata.getValue(Property, "name", String).get() == 'simple.my-value' + metadata.getValue(Property, "name", String).get() == 'endpoints.my-value' } void "property path is overriding the existing one without base prefix"() { @@ -72,7 +72,7 @@ class MyBean { beanDefinition.getInjectedMethods()[0].name == 'setMyValue' def metadata = beanDefinition.getInjectedMethods()[0].getAnnotationMetadata() metadata.hasAnnotation(Property) - metadata.getValue(Property, "name", String).get() == 'simple.my-value' + metadata.getValue(Property, "name", String).get() == 'endpoints.my-value' } void "property path is broken because alias is pointing to another alias 2"() { diff --git a/inject-java/src/test/groovy/io/micronaut/inject/qualifiers/annotationmember/NonBindingQualifierSpec.groovy b/inject-java/src/test/groovy/io/micronaut/inject/qualifiers/annotationmember/NonBindingQualifierSpec.groovy index 1cbdc2ebce7..de3fa63dda2 100644 --- a/inject-java/src/test/groovy/io/micronaut/inject/qualifiers/annotationmember/NonBindingQualifierSpec.groovy +++ b/inject-java/src/test/groovy/io/micronaut/inject/qualifiers/annotationmember/NonBindingQualifierSpec.groovy @@ -1,11 +1,7 @@ package io.micronaut.inject.qualifiers.annotationmember import io.micronaut.annotation.processing.test.AbstractTypeElementSpec -import io.micronaut.context.ApplicationContext import io.micronaut.core.annotation.AnnotationUtil -import io.micronaut.inject.BeanDefinition - -import jakarta.inject.Qualifier class NonBindingQualifierSpec extends AbstractTypeElementSpec { @@ -28,7 +24,7 @@ class Test { @Singleton @Cylinders(value = 6, description = "6-cylinder V6 engine") -class V6Engine implements Engine { +class V6Engine implements Engine { @Override public int getCylinders() { @@ -37,8 +33,8 @@ class V6Engine implements Engine { } @Singleton -@Cylinders(value = 8, description = "8-cylinder V8 engine") -class V8Engine implements Engine { +@Cylinders(value = 8, description = "8-cylinder V8 engine") +class V8Engine implements Engine { @Override public int getCylinders() { return 8; @@ -49,12 +45,12 @@ interface Engine { int getCylinders(); } -@Qualifier +@Qualifier @Retention(RUNTIME) @interface Cylinders { int value(); - @NonBinding + @NonBinding String description() default ""; } ''') @@ -90,7 +86,7 @@ class Test { @NonBinding String description() default ""; - + } ''') @@ -100,7 +96,7 @@ class Test { definition .getAnnotationMetadata() .getAnnotation(AnnotationUtil.QUALIFIER) - .stringValues("nonBinding") as Set == ['description'] as Set + .stringValues(AnnotationUtil.NON_BINDING_ATTRIBUTE) as Set == ['description', AnnotationUtil.NON_BINDING_ATTRIBUTE] as Set definition .annotationMetadata .getAnnotationNameByStereotype(AnnotationUtil.QUALIFIER).get() == 'annotationmember.Cylinders' diff --git a/inject-java/src/test/resources/META-INF/services/io.micronaut.inject.annotation.AnnotationTransformer b/inject-java/src/test/resources/META-INF/services/io.micronaut.inject.annotation.AnnotationTransformer index 29d34b98218..f92f0b8dd26 100644 --- a/inject-java/src/test/resources/META-INF/services/io.micronaut.inject.annotation.AnnotationTransformer +++ b/inject-java/src/test/resources/META-INF/services/io.micronaut.inject.annotation.AnnotationTransformer @@ -2,3 +2,5 @@ io.micronaut.inject.beanbuilder.TestInterceptorBindingTransformer io.micronaut.aop.compile.AroundCompileSpec$TestStereotypeAnnTransformer io.micronaut.aop.compile.AroundConstructCompileSpec$TestStereotypeAnnTransformer io.micronaut.annotation.mapping.TransformsToRepeatableAnnotationSpec$TheAnnotationTransformer +io.micronaut.annotation.mapping.TransformNotInheritedAnnotationSpec$TheAnnotationMapper +io.micronaut.annotation.mapping.TransformToInheritedAnnotationSpec$TheAnnotationMapper diff --git a/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/annotation/KotlinAnnotationMetadataBuilder.kt b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/annotation/KotlinAnnotationMetadataBuilder.kt index d3ddeacc048..7c693b934e0 100644 --- a/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/annotation/KotlinAnnotationMetadataBuilder.kt +++ b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/annotation/KotlinAnnotationMetadataBuilder.kt @@ -35,7 +35,6 @@ import io.micronaut.inject.visitor.VisitorContext import io.micronaut.kotlin.processing.getBinaryName import io.micronaut.kotlin.processing.getClassDeclaration import io.micronaut.kotlin.processing.visitor.KotlinVisitorContext -import java.lang.annotation.Inherited import java.lang.annotation.RetentionPolicy import java.lang.reflect.Method import java.util.* @@ -542,12 +541,6 @@ internal class KotlinAnnotationMetadataBuilder(private val symbolProcessorEnviro } } - override fun isInheritedAnnotation(annotationMirror: KSAnnotation): Boolean { - return annotationMirror.annotationType.resolve().declaration.annotations.any { - it.annotationType.resolve().declaration.qualifiedName?.asString() == Inherited::class.qualifiedName - } - } - private fun populateTypeHierarchy(element: KSClassDeclaration, hierarchy: MutableList) { element.superTypes.forEach { val t = it.resolve() @@ -584,25 +577,4 @@ internal class KotlinAnnotationMetadataBuilder(private val symbolProcessorEnviro return value } - override fun getAnnotationMembers(annotationType: String): MutableMap { - val annotationMirror = getAnnotationMirror(annotationType) - val members = mutableMapOf() - if (annotationMirror.isPresent) { - (annotationMirror.get().getClassDeclaration(visitorContext)).getDeclaredProperties() - .forEach { - members[it.simpleName.asString()] = it - } - } - return members - } - - override fun hasSimpleAnnotation(element: KSAnnotated, simpleName: String): Boolean { - val annotationMirrors: MutableList = element.annotations.toMutableList() - if (element is KSPropertyDeclaration) { - annotationMirrors.addAll(element.getter!!.annotations) - } - return annotationMirrors.any { - it.annotationType.resolve().declaration.simpleName.asString() == simpleName - } - } } diff --git a/inject/src/main/java/io/micronaut/inject/annotation/AnnotationMetadataSupport.java b/inject/src/main/java/io/micronaut/inject/annotation/AnnotationMetadataSupport.java index 9f3c61f8fbe..fb95895521c 100644 --- a/inject/src/main/java/io/micronaut/inject/annotation/AnnotationMetadataSupport.java +++ b/inject/src/main/java/io/micronaut/inject/annotation/AnnotationMetadataSupport.java @@ -295,6 +295,19 @@ static void registerRepeatableAnnotations(Map repeatableAnnotati REPEATABLE_ANNOTATIONS_CONTAINERS.putAll(repeatableAnnotations); } + /** + * Registers repeatable annotation containers. + * + * @param repeatable the repeatable annotations + * @param repeatableContainer the repeatable annotation container + * @MyRepeatable -> @MyRepeatableContainer + * @since 4.0.0 + */ + @Internal + static void registerRepeatableAnnotation(@NonNull String repeatable, @NonNull String repeatableContainer) { + REPEATABLE_ANNOTATIONS_CONTAINERS.put(repeatable, repeatableContainer); + } + /** * @param annotation The annotation * @return The proxy class diff --git a/inject/src/main/java/io/micronaut/inject/annotation/MutableAnnotationMetadata.java b/inject/src/main/java/io/micronaut/inject/annotation/MutableAnnotationMetadata.java index 0683dc66cad..32159a724a2 100644 --- a/inject/src/main/java/io/micronaut/inject/annotation/MutableAnnotationMetadata.java +++ b/inject/src/main/java/io/micronaut/inject/annotation/MutableAnnotationMetadata.java @@ -17,7 +17,6 @@ import io.micronaut.context.env.DefaultPropertyPlaceholderResolver; import io.micronaut.core.annotation.AnnotationMetadata; -import io.micronaut.core.annotation.AnnotationUtil; import io.micronaut.core.annotation.AnnotationValue; import io.micronaut.core.annotation.Internal; import io.micronaut.core.annotation.NonNull; @@ -921,7 +920,7 @@ private void removeAnnotationsIf(@NonNull Predicate { final String annotationName = entry.getKey(); if (predicate.test(newAnnotationValue(annotationName, entry.getValue()))) { - removeFromStereotypes(annotationName, annotations); + removeFromStereotypes(annotationName); return true; } return false; @@ -946,7 +945,7 @@ public void removeAnnotation(String annotationType) { } if (declaredAnnotations != null) { declaredAnnotations.remove(annotationType); - removeFromStereotypes(annotationType, declaredAnnotations); + removeFromStereotypes(annotationType); } if (annotationRepeatableContainer != null) { annotationRepeatableContainer.remove(annotationType); @@ -982,19 +981,19 @@ public void removeStereotype(String annotationType) { } } - private void removeFromStereotypes(String annotationType, Map> declaredAnnotations) { - if (annotationsByStereotype == null) { + private void removeFromStereotypes(String annotationType) { + if (annotationsByStereotype == null || annotationsByStereotype.isEmpty()) { return; } final Iterator>> i = annotationsByStereotype.entrySet().iterator(); - Set toBeRemoved = CollectionUtils.setOf(annotationType); + Set removeNext = new LinkedHashSet<>(); while (i.hasNext()) { final Map.Entry> entry = i.next(); final String stereotypeName = entry.getKey(); final List value = entry.getValue(); - if (value.removeAll(toBeRemoved)) { + if (value.remove(annotationType)) { if (value.isEmpty()) { - toBeRemoved.add(stereotypeName); + removeNext.add(stereotypeName); i.remove(); if (allStereotypes != null) { this.allStereotypes.remove(stereotypeName); @@ -1005,33 +1004,12 @@ private void removeFromStereotypes(String annotationType, Map> declaredAnnotations, Set toBeRemoved) { - if (declaredAnnotations == null) { - return; - } - final Map v = declaredAnnotations.get(AnnotationUtil.ANN_INTERCEPTOR_BINDINGS); - if (v != null) { - final Object o = v.get(AnnotationMetadata.VALUE_MEMBER); - if (o instanceof Collection) { - Collection> col = (Collection) o; - col.removeIf(av -> Arrays.stream(av.annotationClassValues(AnnotationMetadata.VALUE_MEMBER)) - .anyMatch(acv -> toBeRemoved.contains(acv.getName()))); - - if (col.isEmpty()) { - declaredAnnotations.remove(AnnotationUtil.ANN_INTERCEPTOR_BINDINGS); - } - } + for (String stereotype : removeNext) { + removeFromStereotypes(stereotype); } } diff --git a/inject/src/main/java/io/micronaut/inject/qualifiers/AnnotationMetadataQualifier.java b/inject/src/main/java/io/micronaut/inject/qualifiers/AnnotationMetadataQualifier.java index f997d92c3d2..a0aaf59d5fe 100644 --- a/inject/src/main/java/io/micronaut/inject/qualifiers/AnnotationMetadataQualifier.java +++ b/inject/src/main/java/io/micronaut/inject/qualifiers/AnnotationMetadataQualifier.java @@ -34,7 +34,7 @@ import java.util.Arrays; import java.util.Collections; import java.util.HashMap; -import java.util.HashSet; +import java.util.LinkedHashSet; import java.util.Map; import java.util.Set; import java.util.stream.Collectors; @@ -166,8 +166,8 @@ private static Map resolveBindingValues(AnnotationMetadata @NonNull private static Set resolveNonBindingMembers(AnnotationMetadata annotationMetadata) { - final String[] nonBindingArray = annotationMetadata.stringValues(AnnotationUtil.QUALIFIER, "nonBinding"); - return ArrayUtils.isNotEmpty(nonBindingArray) ? new HashSet<>(Arrays.asList(nonBindingArray)) : Collections.emptySet(); + final String[] nonBindingArray = annotationMetadata.stringValues(AnnotationUtil.QUALIFIER, AnnotationUtil.NON_BINDING_ATTRIBUTE); + return ArrayUtils.isNotEmpty(nonBindingArray) ? new LinkedHashSet<>(Arrays.asList(nonBindingArray)) : Collections.emptySet(); } @Override diff --git a/inject/src/main/java/io/micronaut/inject/qualifiers/InterceptorBindingQualifier.java b/inject/src/main/java/io/micronaut/inject/qualifiers/InterceptorBindingQualifier.java index f0c280ae238..35dd4568d16 100644 --- a/inject/src/main/java/io/micronaut/inject/qualifiers/InterceptorBindingQualifier.java +++ b/inject/src/main/java/io/micronaut/inject/qualifiers/InterceptorBindingQualifier.java @@ -45,7 +45,7 @@ */ @Internal public final class InterceptorBindingQualifier implements Qualifier { - public static final String META_MEMBER_MEMBERS = "bindMembers"; + public static final String META_BINDING_VALUES = "$bindingValues"; private static final String META_MEMBER_INTERCEPTOR_TYPE = "interceptorType"; private final Map>> supportedAnnotationNames; private final Set> supportedInterceptorTypes; @@ -83,16 +83,16 @@ private static Map>> findSupportedAnnotations(Co final Map>> supportedAnnotationNames = CollectionUtils.newHashMap(annotationValues.size()); for (AnnotationValue annotationValue : annotationValues) { final String name = annotationValue.stringValue().orElse(null); - if (name != null) { - final AnnotationValue members = - annotationValue.getAnnotation(META_MEMBER_MEMBERS).orElse(null); - if (members != null) { - List> existing = supportedAnnotationNames - .computeIfAbsent(name, k -> new ArrayList<>(5)); - existing.add(members); - } else { - supportedAnnotationNames.put(name, null); - } + if (name == null) { + continue; + } + final AnnotationValue members = annotationValue.getAnnotation(META_BINDING_VALUES).orElse(null); + if (members != null) { + List> existing = supportedAnnotationNames + .computeIfAbsent(name, k -> new ArrayList<>(5)); + existing.add(members); + } else { + supportedAnnotationNames.put(name, null); } } return supportedAnnotationNames; @@ -105,58 +105,57 @@ public > Stream reduce(Class beanType, Stream return true; } final AnnotationMetadata annotationMetadata = candidate.getAnnotationMetadata(); - Collection> interceptorValues = resolveInterceptorAnnotationValues(annotationMetadata, null); - if (!interceptorValues.isEmpty()) { - if (interceptorValues.size() == 1) { - // single binding case, fast path - final AnnotationValue interceptorBinding = interceptorValues.iterator().next(); - final String annotationName = interceptorBinding.stringValue().orElse(null); - if (annotationName == null) { - return false; - } else { - final List> bindingList = supportedAnnotationNames.get(annotationName); - if (bindingList != null) { - final AnnotationValue otherBinding = - interceptorBinding.getAnnotation(META_MEMBER_MEMBERS).orElse(null); - boolean matched = true; - for (AnnotationValue binding : bindingList) { - matched = matched && (!binding.isPresent(META_MEMBER_MEMBERS) || binding.equals(otherBinding)); - } - return matched; - } else { - return supportedAnnotationNames.containsKey(annotationName); - } + Collection> interceptorValues = resolveInterceptorAnnotationValues(annotationMetadata, null); + if (interceptorValues.isEmpty()) { + return false; + } + if (interceptorValues.size() == 1) { + // single binding case, fast path + final AnnotationValue interceptorBinding = interceptorValues.iterator().next(); + final String annotationName = interceptorBinding.stringValue().orElse(null); + if (annotationName == null) { + return false; + } + final List> bindingList = supportedAnnotationNames.get(annotationName); + if (bindingList != null) { + final AnnotationValue otherBinding = + interceptorBinding.getAnnotation(META_BINDING_VALUES).orElse(null); + boolean matched = true; + for (AnnotationValue binding : bindingList) { + matched = matched && (!binding.isPresent(META_BINDING_VALUES) || binding.equals(otherBinding)); } + return matched; } else { - // multiple binding case - boolean matched = false; - for (AnnotationValue annotation : interceptorValues) { - final String annotationName = annotation.stringValue().orElse(null); - if (annotationName == null) { - continue; - } - final List> bindingList = supportedAnnotationNames.get(annotationName); - if (bindingList != null) { - final AnnotationValue otherBinding = - annotation.getAnnotation(META_MEMBER_MEMBERS).orElse(null); - for (AnnotationValue binding : bindingList) { - matched = (!binding.isPresent(META_MEMBER_MEMBERS) || binding.equals(otherBinding)); - if (matched) { - break; - } + return supportedAnnotationNames.containsKey(annotationName); + } + } else { + // multiple binding case + boolean matched = false; + for (AnnotationValue annotation : interceptorValues) { + final String annotationName = annotation.stringValue().orElse(null); + if (annotationName == null) { + continue; + } + final List> bindingList = supportedAnnotationNames.get(annotationName); + if (bindingList != null) { + final AnnotationValue otherBinding = + annotation.getAnnotation(META_BINDING_VALUES).orElse(null); + for (AnnotationValue binding : bindingList) { + matched = (!binding.isPresent(META_BINDING_VALUES) || binding.equals(otherBinding)); + if (matched) { + break; } - } else { - matched = supportedAnnotationNames.containsKey(annotationName); } + } else { + matched = supportedAnnotationNames.containsKey(annotationName); + } - if (matched) { - break; - } + if (matched) { + break; } - return matched; } + return matched; } - return false; }); } @@ -187,28 +186,26 @@ public String toString() { } } - private static @NonNull Collection> resolveInterceptorAnnotationValues( + private static @NonNull Collection> resolveInterceptorAnnotationValues( @NonNull AnnotationMetadata annotationMetadata, @Nullable String kind) { - List> bindings = annotationMetadata - .getAnnotationValuesByName(AnnotationUtil.ANN_INTERCEPTOR_BINDING); - if (CollectionUtils.isNotEmpty(bindings)) { - return bindings - .stream() - .filter(av -> { - if (av.stringValue().isEmpty()) { - return false; - } - if (kind == null) { - return true; - } else { - final String specifiedkind = av.stringValue("kind").orElse(null); - return specifiedkind == null || specifiedkind.equals(kind); - } - }) - .collect(Collectors.toList()); - } else { + List> bindings = annotationMetadata.getAnnotationValuesByName(AnnotationUtil.ANN_INTERCEPTOR_BINDING); + if (CollectionUtils.isEmpty(bindings)) { return Collections.emptyList(); } + return bindings + .stream() + .filter(av -> { + if (av.stringValue().isEmpty()) { + return false; + } + if (kind == null) { + return true; + } else { + final String specifiedkind = av.stringValue("kind").orElse(null); + return specifiedkind == null || specifiedkind.equals(kind); + } + }) + .toList(); } } From 52940903444c2aa7c273a231184e282abb3a31f3 Mon Sep 17 00:00:00 2001 From: Denis Stepanov Date: Mon, 13 Mar 2023 14:45:25 -0600 Subject: [PATCH 592/743] Allow for configuration beans to have interceptors (#8936) Fixes #8933 --- ...ConfigurationReaderBeanElementCreator.java | 4 +- .../DeclaredBeanElementCreator.java | 6 +- .../aop/compile/AroundCompileSpec.groovy | 62 +++++++++++++++++++ 3 files changed, 69 insertions(+), 3 deletions(-) diff --git a/core-processor/src/main/java/io/micronaut/inject/processing/ConfigurationReaderBeanElementCreator.java b/core-processor/src/main/java/io/micronaut/inject/processing/ConfigurationReaderBeanElementCreator.java index f32a432160b..ebbcfb1fb60 100644 --- a/core-processor/src/main/java/io/micronaut/inject/processing/ConfigurationReaderBeanElementCreator.java +++ b/core-processor/src/main/java/io/micronaut/inject/processing/ConfigurationReaderBeanElementCreator.java @@ -146,8 +146,8 @@ public static boolean isConfigurationProperties(ClassElement classElement) { } @Override - protected boolean visitAopMethod(BeanDefinitionVisitor visitor, MethodElement methodElement) { - return false; + protected void makeInterceptedForValidationIfNeeded(MethodElement element) { + // Configuration beans are validated by the introspection } @Override diff --git a/core-processor/src/main/java/io/micronaut/inject/processing/DeclaredBeanElementCreator.java b/core-processor/src/main/java/io/micronaut/inject/processing/DeclaredBeanElementCreator.java index 3ea31d191cf..8fef82f6e24 100644 --- a/core-processor/src/main/java/io/micronaut/inject/processing/DeclaredBeanElementCreator.java +++ b/core-processor/src/main/java/io/micronaut/inject/processing/DeclaredBeanElementCreator.java @@ -234,7 +234,11 @@ protected boolean visitProperty(BeanDefinitionVisitor visitor, PropertyElement p return claimed; } - private static void makeInterceptedForValidationIfNeeded(MethodElement element) { + /** + * Makes the method intercepted by the validation advice. + * @param element The method element + */ + protected void makeInterceptedForValidationIfNeeded(MethodElement element) { // The method with constrains should be intercepted with the validation interceptor if (element.hasDeclaredAnnotation(ANN_REQUIRES_VALIDATION)) { element.annotate(ANN_VALIDATED); diff --git a/inject-java/src/test/groovy/io/micronaut/aop/compile/AroundCompileSpec.groovy b/inject-java/src/test/groovy/io/micronaut/aop/compile/AroundCompileSpec.groovy index 7296d4f1803..596d1bb90f4 100644 --- a/inject-java/src/test/groovy/io/micronaut/aop/compile/AroundCompileSpec.groovy +++ b/inject-java/src/test/groovy/io/micronaut/aop/compile/AroundCompileSpec.groovy @@ -13,6 +13,7 @@ import io.micronaut.inject.BeanDefinition import io.micronaut.inject.BeanDefinitionReference import io.micronaut.inject.annotation.NamedAnnotationMapper import io.micronaut.inject.annotation.NamedAnnotationTransformer +import io.micronaut.inject.qualifiers.Qualifiers import io.micronaut.inject.visitor.VisitorContext import io.micronaut.inject.writer.BeanDefinitionWriter import spock.lang.Issue @@ -1101,6 +1102,67 @@ interface IBeanValidator { beanDefinition.getTypeArguments('test.BaseService')[0].type.name == 'test.BaseEntity' } + void 'test around on @EachProperty'() { + given: + ApplicationContext context = buildContext('justaround.MyBean', ''' +package justaround; + +import java.lang.annotation.*; +import io.micronaut.aop.*; +import io.micronaut.context.annotation.EachProperty; +import jakarta.inject.*; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +@EachProperty("somebeans.here") +class MyBean { + @TestAnn + void test() { + } +} + +@Retention(RUNTIME) +@Target({ElementType.METHOD, ElementType.TYPE}) +@Around +@interface TestAnn { +} + +@InterceptorBean(TestAnn.class) +class TestInterceptor implements Interceptor { + boolean invoked = false; + @Override + public Object intercept(InvocationContext context) { + invoked = true; + return context.proceed(); + } +} + +@Singleton +class AnotherInterceptor implements Interceptor { + boolean invoked = false; + @Override + public Object intercept(InvocationContext context) { + invoked = true; + return context.proceed(); + } +} +''', false, [ + "somebeans.here.abc": "123", + "somebeans.here.xyz": "123", +]) + def instance = getBean(context, 'justaround.MyBean', Qualifiers.byName("abc")) + def interceptor = getBean(context, 'justaround.TestInterceptor') + def anotherInterceptor = getBean(context, 'justaround.AnotherInterceptor') + instance.test() + + expect:"the interceptor was invoked" + instance instanceof Intercepted + interceptor.invoked + !anotherInterceptor.invoked + + cleanup: + context.close() + } + static class NamedTestAnnMapper implements NamedAnnotationMapper { @Override From 27121d9ed5d0ef56c4b15ff717381a82eccb14d1 Mon Sep 17 00:00:00 2001 From: Dean Wette Date: Tue, 14 Mar 2023 05:23:19 -0500 Subject: [PATCH 593/743] Add support for annotation-based CORS configuration (8558) (#8580) --- .../server/netty/cors/CorsFilterSpec.groovy | 18 +- .../netty/cors/CrossOriginUtilSpec.groovy | 178 +++++++ .../http/server/tck/CorsAssertion.java | 182 +++++++ .../micronaut/http/server/tck/CorsUtils.java | 95 ++++ .../http/server/tck/tests/HeadersTest.java | 2 +- .../tests/cors/CorsDisabledByDefaultTest.java | 11 +- .../tck/tests/cors/CorsSimpleRequestTest.java | 15 +- .../tck/tests/cors/CrossOriginTest.java | 448 ++++++++++++++++++ .../http/server/cors/CorsFilter.java | 36 +- .../server/cors/CorsOriginConfiguration.java | 26 +- .../http/server/cors/CorsOriginConverter.java | 5 + .../http/server/cors/CrossOrigin.java | 91 ++++ .../http/server/cors/CrossOriginUtil.java | 86 ++++ src/main/docs/guide/appendix/breaks.adoc | 4 + .../cors/annotationBasedCors.adoc | 27 ++ .../cors/corsAllowCredentials.adoc | 4 +- .../cors/corsAllowedHeaders.adoc | 6 +- .../cors/corsAllowedMethods.adoc | 6 +- .../cors/corsAllowedOrigins.adoc | 12 +- .../cors/corsExposedHeaders.adoc | 4 +- .../serverConfiguration/cors/corsMaxAge.adoc | 2 +- src/main/docs/guide/toc.yml | 1 + .../http/server/cors/CorsController.groovy | 29 ++ .../server/cors/CorsControllerSpec.groovy | 61 +++ .../docs/http/server/cors/CorsController.kt | 28 ++ .../http/server/cors/CorsControllerTest.kt | 47 ++ .../docs/http/server/cors/CorsController.java | 28 ++ .../http/server/cors/CorsControllerTest.java | 53 +++ 28 files changed, 1449 insertions(+), 56 deletions(-) create mode 100644 http-server-netty/src/test/groovy/io/micronaut/http/server/netty/cors/CrossOriginUtilSpec.groovy create mode 100644 http-server-tck/src/main/java/io/micronaut/http/server/tck/CorsAssertion.java create mode 100644 http-server-tck/src/main/java/io/micronaut/http/server/tck/CorsUtils.java create mode 100644 http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/cors/CrossOriginTest.java create mode 100644 http-server/src/main/java/io/micronaut/http/server/cors/CrossOrigin.java create mode 100644 http-server/src/main/java/io/micronaut/http/server/cors/CrossOriginUtil.java create mode 100644 src/main/docs/guide/httpServer/serverConfiguration/cors/annotationBasedCors.adoc create mode 100644 test-suite-groovy/src/test/groovy/io/micronaut/docs/http/server/cors/CorsController.groovy create mode 100644 test-suite-groovy/src/test/groovy/io/micronaut/docs/http/server/cors/CorsControllerSpec.groovy create mode 100644 test-suite-kotlin/src/test/kotlin/io/micronaut/docs/http/server/cors/CorsController.kt create mode 100644 test-suite-kotlin/src/test/kotlin/io/micronaut/docs/http/server/cors/CorsControllerTest.kt create mode 100644 test-suite/src/test/java/io/micronaut/docs/http/server/cors/CorsController.java create mode 100644 test-suite/src/test/java/io/micronaut/docs/http/server/cors/CorsControllerTest.java diff --git a/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/cors/CorsFilterSpec.groovy b/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/cors/CorsFilterSpec.groovy index d7b3be1f0db..69ffddb7728 100644 --- a/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/cors/CorsFilterSpec.groovy +++ b/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/cors/CorsFilterSpec.groovy @@ -93,13 +93,13 @@ class CorsFilterSpec extends Specification { } @Unroll - void "regex matching configuration"(List regex, String origin) { + void "regex matching configuration"(String regex, String origin) { given: HttpRequest request = createRequest(origin) request.getAttribute(HttpAttributes.ROUTE_MATCH, RouteMatch.class) >> Optional.empty() CorsOriginConfiguration originConfig = new CorsOriginConfiguration() - originConfig.allowedOrigins = regex + originConfig.allowedOriginsRegex = regex HttpServerConfiguration.CorsConfiguration config = enabledCorsConfiguration([foo: originConfig]) CorsFilter corsHandler = buildCorsHandler(config) @@ -124,13 +124,13 @@ class CorsFilterSpec extends Specification { where: regex | origin - ['.*'] | 'http://www.bar.com' - ['^http://www\\.(foo|bar)\\.com$'] | 'http://www.bar.com' - ['^http://www\\.(foo|bar)\\.com$'] | 'http://www.foo.com' - ['.*bar$', '.*foo$'] | 'asdfasdf foo' - ['.*bar$', '.*foo$'] | 'asdfasdf bar' - ['.*bar$', '.*foo$'] | 'http://asdfasdf.foo' - ['.*bar$', '.*foo$'] | 'http://asdfasdf.bar' + '.*' | 'http://www.bar.com' + '^http://www\\.(foo|bar)\\.com$' | 'http://www.bar.com' + '^http://www\\.(foo|bar)\\.com$' | 'http://www.foo.com' + '.*(bar|foo)$' | 'asdfasdf foo' + '.*(bar|foo)$' | 'asdfasdf bar' + '.*(bar|foo)$' | 'http://asdfasdf.foo' + '.*(bar|foo)$' | 'http://asdfasdf.bar' } void "test handleRequest with disallowed method"() { diff --git a/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/cors/CrossOriginUtilSpec.groovy b/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/cors/CrossOriginUtilSpec.groovy new file mode 100644 index 00000000000..0dffce3aee1 --- /dev/null +++ b/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/cors/CrossOriginUtilSpec.groovy @@ -0,0 +1,178 @@ +package io.micronaut.http.server.netty.cors + +import io.micronaut.context.ApplicationContext +import io.micronaut.context.annotation.Requires +import io.micronaut.core.util.CollectionUtils +import io.micronaut.http.HttpHeaders +import io.micronaut.http.HttpMethod +import io.micronaut.http.HttpRequest +import io.micronaut.http.MediaType +import io.micronaut.http.annotation.Controller +import io.micronaut.http.annotation.Get +import io.micronaut.http.annotation.Produces +import io.micronaut.http.client.HttpClient +import io.micronaut.http.server.cors.CorsOriginConfiguration +import io.micronaut.http.server.cors.CrossOrigin +import io.micronaut.http.server.cors.CrossOriginUtil +import io.micronaut.runtime.server.EmbeddedServer +import spock.lang.AutoCleanup +import spock.lang.Shared +import spock.lang.Specification + +class CrossOriginUtilSpec extends Specification { + + private static final String SPECNAME = "CrossOriginUtilSpec" + + @Shared + @AutoCleanup + EmbeddedServer embeddedServer = ApplicationContext.run(EmbeddedServer, ["spec.name": SPECNAME]) + + @Shared + @AutoCleanup + HttpClient client = embeddedServer.applicationContext.createBean(HttpClient, embeddedServer.URL) + + void "test CrossOrigin on method annotation maps to CorsOriginConfiguration"() { + when: + HttpRequest req = HttpRequest.GET("/methodexample").accept(MediaType.TEXT_PLAIN) + client.toBlocking().retrieve(req, String.class) + CorsOriginConfiguration config = embeddedServer.applicationContext.getBean(TestMethodController).config + + then: + config + config.allowedOrigins == [ "https://foo.com" ] + !config.allowedOriginsRegex.isPresent() + config.allowedHeaders == [ HttpHeaders.CONTENT_TYPE, HttpHeaders.AUTHORIZATION ] + config.exposedHeaders == [ HttpHeaders.CONTENT_TYPE, HttpHeaders.AUTHORIZATION ] + config.allowedMethods == [ HttpMethod.GET, HttpMethod.POST ] + !config.allowCredentials + config.maxAge == -1L + + cleanup: + embeddedServer.applicationContext.getBean(TestMethodController).config = null + } + + void "test CrossOrigin with value on method annotation maps to CorsOriginConfiguration allowedOrigin"() { + when: + HttpRequest req = HttpRequest.GET("/anothermethod").accept(MediaType.TEXT_PLAIN) + client.toBlocking().retrieve(req, String.class) + CorsOriginConfiguration config = embeddedServer.applicationContext.getBean(TestMethodController).config + + then: + config + config.allowedOrigins == [ "https://foo.com" ] + !config.allowedOriginsRegex + config.allowedHeaders == CorsOriginConfiguration.ANY + CollectionUtils.isEmpty(config.exposedHeaders) + config.allowedMethods == CorsOriginConfiguration.ANY_METHOD + config.allowCredentials + config.maxAge == 1800L + + cleanup: + embeddedServer.applicationContext.getBean(TestMethodController).config = null + } + + void "test CrossOrigin on class annotation maps to CorsOriginConfiguration"() { + when: + HttpRequest req = HttpRequest.GET("/classexample").accept(MediaType.TEXT_PLAIN) + client.toBlocking().retrieve(req, String.class) + CorsOriginConfiguration config = embeddedServer.applicationContext.getBean(TestClassController).config + + then: + config + config.allowedOrigins == [ "https://bar.com" ] + config.allowedHeaders == [ HttpHeaders.CONTENT_TYPE, HttpHeaders.AUTHORIZATION ] + config.exposedHeaders == [ HttpHeaders.CONTENT_TYPE, HttpHeaders.AUTHORIZATION ] + config.allowedMethods == [ HttpMethod.GET, HttpMethod.POST, HttpMethod.DELETE ] + !config.allowCredentials + config.maxAge == 3600L + + cleanup: + embeddedServer.applicationContext.getBean(TestMethodController).config = null + } + + void "test CrossOrigin with value on class annotation maps to CorsOriginConfiguration allowedOrigin"() { + when: + HttpRequest req = HttpRequest.GET("/anotherclass").accept(MediaType.TEXT_PLAIN) + client.toBlocking().retrieve(req, String.class) + CorsOriginConfiguration config = embeddedServer.applicationContext.getBean(TestAnotherClassController).config + + then: + config + config.allowedOrigins == [ "https://bar.com" ] + config.allowedHeaders == CorsOriginConfiguration.ANY + CollectionUtils.isEmpty(config.exposedHeaders) + config.allowedMethods == CorsOriginConfiguration.ANY_METHOD + config.allowCredentials + config.maxAge == 1800L + + cleanup: + embeddedServer.applicationContext.getBean(TestMethodController).config = null + } + + @Requires(property = 'spec.name', value = SPECNAME) + @Controller + static class TestMethodController { + CorsOriginConfiguration config + + @CrossOrigin( + allowedOrigins = "https://foo.com", + allowedHeaders = [ HttpHeaders.CONTENT_TYPE, HttpHeaders.AUTHORIZATION ], + exposedHeaders = [ HttpHeaders.CONTENT_TYPE, HttpHeaders.AUTHORIZATION ], + allowedMethods = [ HttpMethod.GET, HttpMethod.POST ], + allowCredentials = false, + maxAge = -1L + ) + @Produces(MediaType.TEXT_PLAIN) + @Get("/methodexample") + String method(HttpRequest req) { + this.config = CrossOriginUtil.getCorsOriginConfigurationForRequest(req).orElse(null) + return "method" + } + + @CrossOrigin( + "https://foo.com" + // allowedOriginsRegex = false - is the default + ) + @Produces(MediaType.TEXT_PLAIN) + @Get("/anothermethod") + String example(HttpRequest req) { + this.config = CrossOriginUtil.getCorsOriginConfigurationForRequest(req).orElse(null) + return "anothermethod" + } + } + + @Requires(property = 'spec.name', value = SPECNAME) + @Controller + @CrossOrigin( + allowedOrigins = "https://bar.com", + allowedHeaders = [ HttpHeaders.CONTENT_TYPE, HttpHeaders.AUTHORIZATION ], + exposedHeaders = [ HttpHeaders.CONTENT_TYPE, HttpHeaders.AUTHORIZATION ], + allowedMethods = [ HttpMethod.GET, HttpMethod.POST, HttpMethod.DELETE ], + allowCredentials = false, + maxAge = 3600L + ) + static class TestClassController{ + CorsOriginConfiguration config + + @Produces(MediaType.TEXT_PLAIN) + @Get("/classexample") + String example(HttpRequest req) { + this.config = CrossOriginUtil.getCorsOriginConfigurationForRequest(req).orElse(null) + return "class" + } + } + + @Requires(property = 'spec.name', value = SPECNAME) + @Controller + @CrossOrigin("https://bar.com") + static class TestAnotherClassController{ + CorsOriginConfiguration config + + @Produces(MediaType.TEXT_PLAIN) + @Get("/anotherclass") + String example(HttpRequest req) { + this.config = CrossOriginUtil.getCorsOriginConfigurationForRequest(req).orElse(null) + return "class" + } + } +} diff --git a/http-server-tck/src/main/java/io/micronaut/http/server/tck/CorsAssertion.java b/http-server-tck/src/main/java/io/micronaut/http/server/tck/CorsAssertion.java new file mode 100644 index 00000000000..4fd7c06ea71 --- /dev/null +++ b/http-server-tck/src/main/java/io/micronaut/http/server/tck/CorsAssertion.java @@ -0,0 +1,182 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.http.server.tck; + +import io.micronaut.core.util.CollectionUtils; +import io.micronaut.core.util.StringUtils; +import io.micronaut.http.HttpHeaders; +import io.micronaut.http.HttpMethod; +import io.micronaut.http.HttpResponse; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * CORS assertion. + * @author Sergio del Amo + * @since 3.9.0 + */ +public final class CorsAssertion { + + private final String vary; + private final String accessControlAllowCredentials; + + private final String origin; + + private final List allowMethods; + + private final String maxAge; + + private CorsAssertion(String vary, + String accessControlAllowCredentials, + String origin, + List allowMethods, + String maxAge) { + this.vary = vary; + this.accessControlAllowCredentials = accessControlAllowCredentials; + this.origin = origin; + this.allowMethods = allowMethods; + this.maxAge = maxAge; + } + + /** + * Validate the CORS assertions. + * @param response HTTP Response to run CORS assertions against it. + */ + public void validate(HttpResponse response) { + if (StringUtils.isNotEmpty(vary)) { + assertEquals(vary, response.getHeaders().get(HttpHeaders.VARY)); + } + if (StringUtils.isNotEmpty(accessControlAllowCredentials)) { + assertEquals(accessControlAllowCredentials, response.getHeaders().get(HttpHeaders.ACCESS_CONTROL_ALLOW_CREDENTIALS)); + } + if (StringUtils.isNotEmpty(origin)) { + assertEquals(origin, response.getHeaders().get(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN)); + } + if (CollectionUtils.isNotEmpty(allowMethods)) { + assertEquals(allowMethods.stream().map(HttpMethod::toString).collect(Collectors.joining(",")), + response.getHeaders().get(HttpHeaders.ACCESS_CONTROL_ALLOW_METHODS)); + } + if (StringUtils.isNotEmpty(maxAge)) { + assertEquals(maxAge, response.getHeaders().get(HttpHeaders.ACCESS_CONTROL_MAX_AGE)); + } + } + + /** + * + * @return a CORS Assertion Builder. + */ + public static Builder builder() { + return new Builder(); + } + + /** + * CORS Assertion Builder. + */ + public static class Builder { + private String vary; + private String accessControlAllowCredentials; + + private String origin; + + private List allowMethods; + + private String maxAge; + + /** + * + * @param varyValue The expected value for the HTTP Header {@value HttpHeaders#VARY}. + * @return The Builder + */ + public Builder vary(String varyValue) { + this.vary = varyValue; + return this; + } + + /** + * + * @param accessControlAllowCredentials The expected value for the HTTP Header {@value HttpHeaders#ACCESS_CONTROL_ALLOW_CREDENTIALS}. + * @return The Builder + */ + public Builder allowCredentials(String accessControlAllowCredentials) { + this.accessControlAllowCredentials = accessControlAllowCredentials; + return this; + } + + /** + * + * Set expectation of value for the HTTP Header {@value HttpHeaders#ACCESS_CONTROL_ALLOW_CREDENTIALS} to {@value StringUtils#TRUE}. + * @return The Builder + */ + public Builder allowCredentials() { + return allowCredentials(StringUtils.TRUE); + } + + /** + * + * Set expectation of value for the HTTP Header {@value HttpHeaders#ACCESS_CONTROL_ALLOW_CREDENTIALS} to {@value StringUtils#TRUE}. + * @param allowCredentials Set expectation of value for the HTTP Header {@value HttpHeaders#ACCESS_CONTROL_ALLOW_CREDENTIALS} + * @return The Builder + */ + public Builder allowCredentials(boolean allowCredentials) { + return allowCredentials ? allowCredentials(StringUtils.TRUE) : allowCredentials(""); + } + + /** + * + * @param origin The expected value for the HTTP Header {@value HttpHeaders#ACCESS_CONTROL_ALLOW_ORIGIN}. + * @return The Builder + */ + public Builder allowOrigin(String origin) { + this.origin = origin; + return this; + } + + /** + * + * @param method The expected value for the HTTP Header {@value HttpHeaders#ACCESS_CONTROL_ALLOW_METHODS}. + * @return The Builder + */ + public Builder allowMethods(HttpMethod method) { + if (allowMethods == null) { + this.allowMethods = new ArrayList<>(); + } + this.allowMethods.add(method); + return this; + } + + /** + * + * @param maxAge The expected value for the HTTP Header {@value HttpHeaders#ACCESS_CONTROL_MAX_AGE}. + * @return The Builder + */ + public Builder maxAge(String maxAge) { + this.maxAge = maxAge; + return this; + } + + /** + * + * @return A CORS assertion. + */ + public CorsAssertion build() { + return new CorsAssertion(vary, accessControlAllowCredentials, origin, allowMethods, maxAge); + } + } +} diff --git a/http-server-tck/src/main/java/io/micronaut/http/server/tck/CorsUtils.java b/http-server-tck/src/main/java/io/micronaut/http/server/tck/CorsUtils.java new file mode 100644 index 00000000000..b689b370eb5 --- /dev/null +++ b/http-server-tck/src/main/java/io/micronaut/http/server/tck/CorsUtils.java @@ -0,0 +1,95 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.http.server.tck; + +import io.micronaut.http.HttpHeaders; +import io.micronaut.http.HttpMethod; +import io.micronaut.http.HttpResponse; + +import static org.junit.jupiter.api.Assertions.assertFalse; + +/** + * Utility class to do CORS related assertions. + * @author Sergio del Amo + * @since 3.9.0 + */ +public final class CorsUtils { + private CorsUtils() { + + } + + /** + * @param response HTTP Response to run CORS assertions against it. + */ + public static void assertCorsHeadersNotPresent(HttpResponse response) { + assertFalse(response.getHeaders().names().contains(HttpHeaders.VARY)); + assertFalse(response.getHeaders().names().contains(HttpHeaders.ACCESS_CONTROL_ALLOW_CREDENTIALS)); + assertFalse(response.getHeaders().names().contains(HttpHeaders.ACCESS_CONTROL_MAX_AGE)); + assertFalse(response.getHeaders().names().contains(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN)); + assertFalse(response.getHeaders().names().contains(HttpHeaders.ACCESS_CONTROL_ALLOW_METHODS)); + assertFalse(response.getHeaders().names().contains(HttpHeaders.ACCESS_CONTROL_ALLOW_HEADERS)); + } + + /** + * @param response HTTP Response to run CORS assertions against it. + * @param origin The expected value for the HTTP Header {@value HttpHeaders#ACCESS_CONTROL_ALLOW_ORIGIN}. + * @param method The expected value for the HTTP Header {@value HttpHeaders#ACCESS_CONTROL_ALLOW_METHODS}. + */ + public static void assertCorsHeaders(HttpResponse response, String origin, HttpMethod method) { + CorsAssertion.builder() + .vary("Origin") + .allowCredentials() + .allowOrigin(origin) + .allowMethods(method) + .maxAge("1800") + .build() + .validate(response); + } + + /** + * @param response HTTP Response to run CORS assertions against it. + * @param origin The expected value for the HTTP Header {@value HttpHeaders#ACCESS_CONTROL_ALLOW_ORIGIN}. + * @param method The expected value for the HTTP Header {@value HttpHeaders#ACCESS_CONTROL_ALLOW_METHODS}. + * @param allowCredentials The expected value for the HTTP Header {@value HttpHeaders#ACCESS_CONTROL_ALLOW_CREDENTIALS}. + */ + public static void assertCorsHeaders(HttpResponse response, String origin, HttpMethod method, boolean allowCredentials) { + CorsAssertion.builder() + .vary("Origin") + .allowCredentials(allowCredentials) + .allowOrigin(origin) + .allowMethods(method) + .maxAge("1800") + .build() + .validate(response); + } + + /** + * @param response HTTP Response to run CORS assertions against it. + * @param origin The expected value for the HTTP Header {@value HttpHeaders#ACCESS_CONTROL_ALLOW_ORIGIN}. + * @param method The expected value for the HTTP Header {@value HttpHeaders#ACCESS_CONTROL_ALLOW_METHODS}. + * @param maxAge The expected value for the HTTP Header {@value HttpHeaders#ACCESS_CONTROL_MAX_AGE}. + */ + public static void assertCorsHeaders(HttpResponse response, String origin, HttpMethod method, String maxAge) { + CorsAssertion.builder() + .vary("Origin") + .allowCredentials() + .allowOrigin(origin) + .allowMethods(method) + .maxAge(maxAge) + .build() + .validate(response); + } +} diff --git a/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/HeadersTest.java b/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/HeadersTest.java index c08fd2e82c9..c2f3efe01bf 100644 --- a/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/HeadersTest.java +++ b/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/HeadersTest.java @@ -39,7 +39,7 @@ public class HeadersTest { public static final String SPEC_NAME = "HeadersTest"; /** - * Message header field names are case-insensitive + * Message header field names are case-insensitive. * * @see HTTP/1.1 Message Headers */ diff --git a/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/cors/CorsDisabledByDefaultTest.java b/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/cors/CorsDisabledByDefaultTest.java index cc52de62568..038b52236c6 100644 --- a/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/cors/CorsDisabledByDefaultTest.java +++ b/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/cors/CorsDisabledByDefaultTest.java @@ -25,6 +25,7 @@ import io.micronaut.http.annotation.Post; import io.micronaut.http.annotation.Status; import io.micronaut.http.server.tck.AssertionUtils; +import io.micronaut.http.server.tck.CorsUtils; import io.micronaut.http.server.tck.HttpResponseAssertion; import io.micronaut.http.server.util.HttpHostResolver; import jakarta.inject.Singleton; @@ -34,7 +35,6 @@ import java.util.Collections; import static io.micronaut.http.server.tck.TestScenario.asserts; -import static org.junit.jupiter.api.Assertions.assertNull; @SuppressWarnings({ "java:S2259", // The tests will show if it's null @@ -56,14 +56,7 @@ void corsDisabledByDefault() throws IOException { (server, request) -> { AssertionUtils.assertDoesNotThrow(server, request, HttpResponseAssertion.builder() .status(HttpStatus.OK) - .assertResponse(response -> { - assertNull(response.getHeaders().get("Access-Control-Allow-Origin")); - assertNull(response.getHeaders().get("Vary")); - assertNull(response.getHeaders().get("Access-Control-Allow-Credentials")); - assertNull(response.getHeaders().get("Access-Control-Allow-Methods")); - assertNull(response.getHeaders().get("Access-Control-Allow-Headers")); - assertNull(response.getHeaders().get("Access-Control-Max-Age")); - }) + .assertResponse(CorsUtils::assertCorsHeadersNotPresent) .build()); }); } diff --git a/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/cors/CorsSimpleRequestTest.java b/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/cors/CorsSimpleRequestTest.java index aff8053c321..6dc7342aad3 100644 --- a/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/cors/CorsSimpleRequestTest.java +++ b/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/cors/CorsSimpleRequestTest.java @@ -29,6 +29,7 @@ import io.micronaut.http.annotation.Status; import io.micronaut.http.client.multipart.MultipartBody; import io.micronaut.http.server.tck.AssertionUtils; +import io.micronaut.http.server.tck.CorsAssertion; import io.micronaut.http.server.tck.HttpResponseAssertion; import io.micronaut.http.server.tck.RequestSupplier; import io.micronaut.http.server.tck.ServerUnderTest; @@ -190,14 +191,12 @@ void corsSimpleRequestForLocalhostCanBeAllowedViaConfiguration() throws IOExcept AssertionUtils.assertDoesNotThrow(server, request, HttpResponseAssertion.builder() .status(HttpStatus.OK) - .assertResponse(response -> { - assertNotNull(response.getHeaders().get("Access-Control-Allow-Origin")); - assertNotNull(response.getHeaders().get("Vary")); - assertNotNull(response.getHeaders().get("Access-Control-Allow-Credentials")); - assertNull(response.getHeaders().get("Access-Control-Allow-Methods")); - assertNull(response.getHeaders().get("Access-Control-Allow-Headers")); - assertNull(response.getHeaders().get("Access-Control-Max-Age")); - }) + .assertResponse(response -> CorsAssertion.builder() + .vary("Origin") + .allowCredentials() + .allowOrigin("https://foo.com") + .build() + .validate(response)) .build()); assertEquals(1, refreshCounter.getRefreshCount()); }); diff --git a/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/cors/CrossOriginTest.java b/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/cors/CrossOriginTest.java new file mode 100644 index 00000000000..de678b9d434 --- /dev/null +++ b/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/cors/CrossOriginTest.java @@ -0,0 +1,448 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.http.server.tck.tests.cors; + +import io.micronaut.context.annotation.Replaces; +import io.micronaut.context.annotation.Requires; +import io.micronaut.core.annotation.Nullable; +import io.micronaut.core.util.CollectionUtils; +import io.micronaut.core.util.StringUtils; +import io.micronaut.http.HttpHeaders; +import io.micronaut.http.HttpMethod; +import io.micronaut.http.HttpRequest; +import io.micronaut.http.HttpStatus; +import io.micronaut.http.MediaType; +import io.micronaut.http.MutableHttpRequest; +import io.micronaut.http.annotation.Controller; +import io.micronaut.http.annotation.Delete; +import io.micronaut.http.annotation.Get; +import io.micronaut.http.annotation.PathVariable; +import io.micronaut.http.annotation.Post; +import io.micronaut.http.annotation.Produces; +import io.micronaut.http.server.cors.CrossOrigin; +import io.micronaut.http.server.tck.AssertionUtils; +import io.micronaut.http.server.tck.CorsUtils; +import io.micronaut.http.server.tck.HttpResponseAssertion; +import io.micronaut.http.server.tck.ServerUnderTest; +import io.micronaut.http.server.util.HttpHostResolver; +import io.micronaut.http.uri.UriBuilder; +import jakarta.inject.Singleton; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.net.URI; +import java.util.function.BiConsumer; + +import static io.micronaut.http.server.tck.CorsUtils.*; +import static io.micronaut.http.server.tck.TestScenario.asserts; +import static org.junit.jupiter.api.Assertions.assertAll; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@SuppressWarnings({ + "java:S2259", // The tests will show if it's null + "java:S5960", // We're allowed assertions, as these are used in tests only + "checkstyle:MissingJavadocType", + "checkstyle:DesignForExtension" +}) +public class CrossOriginTest { + + private static final String SPECNAME = "CrossOriginTest"; + + @Test + void crossOriginAnnotationWithMatchingOrigin() throws IOException { + asserts(SPECNAME, + preflight(UriBuilder.of("/foo").path("bar"), "https://foo.com", HttpMethod.GET), + (server, request) -> AssertionUtils.assertDoesNotThrow(server, request, HttpResponseAssertion.builder() + .status(HttpStatus.OK) + .assertResponse(response -> { + assertCorsHeaders(response, "https://foo.com", HttpMethod.GET); + assertFalse(response.getHeaders().names().contains(HttpHeaders.ACCESS_CONTROL_ALLOW_HEADERS)); + }) + .build())); + } + + @Test + void crossOriginAnnotationWithNoMatchingOrigin() throws IOException { + asserts(SPECNAME, + preflight(UriBuilder.of("/foo").path("bar"), "https://bar.com", HttpMethod.GET), + (server, request) -> AssertionUtils.assertThrows(server, request, HttpResponseAssertion.builder() + .status(HttpStatus.METHOD_NOT_ALLOWED) + .assertResponse(CorsUtils::assertCorsHeadersNotPresent) + .build())); + } + + @Test + void verifyHttpMethodIsValidatedInACorsRequest() { + assertAll( + () -> asserts(SPECNAME, + preflight(UriBuilder.of("/methods").path("getit"), "https://www.google.com", HttpMethod.GET), + (server, request) -> AssertionUtils.assertDoesNotThrow(server, request, HttpResponseAssertion.builder() + .status(HttpStatus.OK) + .assertResponse(response -> { + assertCorsHeaders(response, "https://www.google.com", HttpMethod.GET); + assertFalse(response.getHeaders().names().contains(HttpHeaders.ACCESS_CONTROL_ALLOW_HEADERS)); + }) + .build())), + () -> asserts(SPECNAME, + preflight(UriBuilder.of("/methods").path("postit").path("id"), "https://www.google.com", HttpMethod.POST), + (server, request) -> AssertionUtils.assertDoesNotThrow(server, request, HttpResponseAssertion.builder() + .status(HttpStatus.OK) + .assertResponse(response -> { + assertCorsHeaders(response, "https://www.google.com", HttpMethod.POST); + assertFalse(response.getHeaders().names().contains(HttpHeaders.ACCESS_CONTROL_ALLOW_HEADERS)); + }) + .build())), + () -> asserts(SPECNAME, + preflight(UriBuilder.of("/methods").path("deleteit").path("id"), "https://www.google.com", HttpMethod.DELETE), + (server, request) -> AssertionUtils.assertThrows(server, request, HttpResponseAssertion.builder() + .status(HttpStatus.FORBIDDEN) + .assertResponse(CorsUtils::assertCorsHeadersNotPresent) + .build())) + ); + } + + @Test + void allowedOriginsRegexHappyPath() throws IOException { + URI uri = UriBuilder.of("/allowedoriginsregex").path("foo").build(); + String origin = "https://foo.com"; + asserts(SPECNAME, preflight(uri, origin, HttpMethod.GET), happyPathAssertion(origin)); + origin = "http://foo.com"; + asserts(SPECNAME, preflight(uri, origin, HttpMethod.GET), happyPathAssertion(origin)); + } + + private static BiConsumer> happyPathAssertion(String origin) { + return (server, request) -> AssertionUtils.assertDoesNotThrow(server, request, HttpResponseAssertion.builder() + .status(HttpStatus.OK) + .assertResponse(response -> { + assertCorsHeaders(response, origin, HttpMethod.GET); + assertFalse(response.getHeaders().names().contains(HttpHeaders.ACCESS_CONTROL_ALLOW_HEADERS)); + }) + .build()); + } + + @Test + void allowedOriginsRegexFailure() throws IOException { + asserts(SPECNAME, + preflight(UriBuilder.of("/allowedoriginsregex").path("bar"), "https://foo.com", HttpMethod.GET), + (server, request) -> AssertionUtils.assertThrows(server, request, HttpResponseAssertion.builder() + .status(HttpStatus.METHOD_NOT_ALLOWED) + .assertResponse(CorsUtils::assertCorsHeadersNotPresent) + .build())); + } + + @Test + void allowedHeadersHappyPath() throws IOException { + asserts(SPECNAME, + preflight(UriBuilder.of("/allowedheaders").path("bar"), "https://foo.com", HttpMethod.GET) + .header(HttpHeaders.ACCESS_CONTROL_REQUEST_HEADERS, HttpHeaders.AUTHORIZATION + "," + HttpHeaders.CONTENT_TYPE), + (server, request) -> AssertionUtils.assertDoesNotThrow(server, request, HttpResponseAssertion.builder() + .status(HttpStatus.OK) + .assertResponse(response -> { + assertCorsHeaders(response, "https://foo.com", HttpMethod.GET); + assertTrue(response.getHeaders().names().contains(HttpHeaders.ACCESS_CONTROL_ALLOW_HEADERS)); + }) + .build())); + } + + /** + * Access-Control-Allow-Headers + * The Access-Control-Allow-Headers response header is used in response to a preflight request which includes the Access-Control-Request-Headers to indicate which HTTP headers can be used during the actual request. + */ + @Test + void allowedHeadersFailure() throws IOException { + asserts(SPECNAME, + preflight(UriBuilder.of("/allowedheaders").path("bar"), "https://foo.com", HttpMethod.GET) + .header(HttpHeaders.ACCESS_CONTROL_REQUEST_HEADERS, "foo"), + (server, request) -> AssertionUtils.assertThrows(server, request, HttpResponseAssertion.builder() + .status(HttpStatus.FORBIDDEN) + .assertResponse(CorsUtils::assertCorsHeadersNotPresent) + .build())); + } + + /** + * The Access-Control-Expose-Headers header adds the specified headers to the allowlist that JavaScript (such as getResponseHeader()) in browsers is allowed to access. + * @see Access-Control-Expose-Headers + */ + @Test + void defaultAccessControlExposeHeaderValueIsNotSet() throws IOException { + asserts(SPECNAME, + preflight(UriBuilder.of("/exposedheaders").path("foo"), "https://foo.com", HttpMethod.GET), + (server, request) -> AssertionUtils.assertDoesNotThrow(server, request, HttpResponseAssertion.builder() + .status(HttpStatus.OK) + .assertResponse(response -> { + assertCorsHeaders(response, "https://foo.com", HttpMethod.GET); + assertFalse(response.getHeaders().names().contains(HttpHeaders.ACCESS_CONTROL_EXPOSE_HEADERS)); + }) + .build())); + } + + /** + * The Access-Control-Expose-Headers header adds the specified headers to the allowlist that JavaScript (such as getResponseHeader()) in browsers is allowed to access. + * @see Access-Control-Expose-Headers + */ + @Test + void httHeaderValueAccessControlExposeHeaderValueCanBeSetViaCrossOriginAnnotation() throws IOException { + asserts(SPECNAME, + CollectionUtils.mapOf("micronaut.server.cors.single-header", StringUtils.TRUE), + preflight(UriBuilder.of("/exposedheaders").path("bar"), "https://foo.com", HttpMethod.GET) + .header(HttpHeaders.ACCESS_CONTROL_EXPOSE_HEADERS, "foo"), + (server, request) -> AssertionUtils.assertDoesNotThrow(server, request, HttpResponseAssertion.builder() + .status(HttpStatus.OK) + .assertResponse(response -> { + assertCorsHeaders(response, "https://foo.com", HttpMethod.GET); + assertTrue(response.getHeaders().names().contains(HttpHeaders.ACCESS_CONTROL_EXPOSE_HEADERS)); + assertEquals("Content-Encoding,Kuma-Revision", response.getHeaders().get(HttpHeaders.ACCESS_CONTROL_EXPOSE_HEADERS)); + }) + .build())); + } + + /** + * The Access-Control-Allow-Credentials response header tells browsers whether to expose the response to the frontend JavaScript code when the request's credentials mode (Request.credentials) is include. + * @see Access-Control-Allow-Credentials + */ + @Test + void defaultAccessControlAllowCredentialsValueIsNotSet() throws IOException { + asserts(SPECNAME, + preflight(UriBuilder.of("/credentials").path("foo"), "https://foo.com", HttpMethod.GET), + (server, request) -> AssertionUtils.assertDoesNotThrow(server, request, HttpResponseAssertion.builder() + .status(HttpStatus.OK) + .assertResponse(response -> { + assertCorsHeaders(response, "https://foo.com", HttpMethod.GET, false); + assertFalse(response.getHeaders().names().contains(HttpHeaders.ACCESS_CONTROL_ALLOW_CREDENTIALS)); + }) + .build())); + } + + /** + * The Access-Control-Allow-Credentials response header tells browsers whether to expose the response to the frontend JavaScript code when the request's credentials mode (Request.credentials) is include. + * @see Access-Control-Allow-Credentials + */ + @Test + void defaultAccessControlAllowCredentialsValueIsSet() throws IOException { + asserts(SPECNAME, + preflight(UriBuilder.of("/credentials").path("bar"), "https://foo.com", HttpMethod.GET), + (server, request) -> AssertionUtils.assertDoesNotThrow(server, request, HttpResponseAssertion.builder() + .status(HttpStatus.OK) + .assertResponse(response -> { + assertCorsHeaders(response, "https://foo.com", HttpMethod.GET); + assertTrue(response.getHeaders().names().contains(HttpHeaders.ACCESS_CONTROL_ALLOW_CREDENTIALS)); + assertEquals("true", response.getHeaders().get(HttpHeaders.ACCESS_CONTROL_ALLOW_CREDENTIALS)); + }) + .build())); + } + + /** + * The Access-Control-Max-Age response header indicates how long the results of a preflight request (that is the information contained in the Access-Control-Allow-Methods and Access-Control-Allow-Headers headers) can be cached. + * @see Access-Control-Max-Age + */ + @Test + void defaultAccessControlMaxAgeValueIsSet() throws IOException { + asserts(SPECNAME, + preflight(UriBuilder.of("/maxage").path("foo"), "https://foo.com", HttpMethod.GET), + (server, request) -> AssertionUtils.assertDoesNotThrow(server, request, HttpResponseAssertion.builder() + .status(HttpStatus.OK) + .assertResponse(response -> { + assertCorsHeaders(response, "https://foo.com", HttpMethod.GET); + assertTrue(response.getHeaders().names().contains(HttpHeaders.ACCESS_CONTROL_MAX_AGE)); + assertEquals("1800", response.getHeaders().get(HttpHeaders.ACCESS_CONTROL_MAX_AGE)); + }) + .build())); + } + + /** + * The Access-Control-Max-Age response header indicates how long the results of a preflight request (that is the information contained in the Access-Control-Allow-Methods and Access-Control-Allow-Headers headers) can be cached. + * @see Access-Control-Max-Age + */ + @Test + void accessControlMaxAgeValueIsSet() throws IOException { + asserts(SPECNAME, + preflight(UriBuilder.of("/maxage").path("bar"), "https://foo.com", HttpMethod.GET), + (server, request) -> AssertionUtils.assertDoesNotThrow(server, request, HttpResponseAssertion.builder() + .status(HttpStatus.OK) + .assertResponse(response -> { + assertCorsHeaders(response, "https://foo.com", HttpMethod.GET, "1000"); + assertTrue(response.getHeaders().names().contains(HttpHeaders.ACCESS_CONTROL_MAX_AGE)); + assertEquals("1000", response.getHeaders().get(HttpHeaders.ACCESS_CONTROL_MAX_AGE)); + }) + .build())); + } + + private static MutableHttpRequest preflight(UriBuilder uriBuilder, String originValue, HttpMethod method) { + return preflight(uriBuilder.build(), originValue, method); + } + + private static MutableHttpRequest preflight(URI uri, String originValue, HttpMethod method) { + return HttpRequest.OPTIONS(uri) + .header(HttpHeaders.ACCEPT, MediaType.TEXT_PLAIN) + .header(HttpHeaders.ORIGIN, originValue) + .header(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, method); + } + + @Requires(property = "spec.name", value = SPECNAME) + @Controller("/foo") + static class Foo { + @CrossOrigin("https://foo.com") + @Produces(MediaType.TEXT_PLAIN) + @Get("/bar") + String index() { + return "bar"; + } + } + + @Requires(property = "spec.name", value = SPECNAME) + @Controller("/allowedoriginsregex") + static class AllowedOriginsRegex { + + @CrossOrigin( + allowedOriginsRegex = "^http(|s):\\/\\/foo\\.com$" + ) + @Produces(MediaType.TEXT_PLAIN) + @Get("/foo") + String foo() { + return "foo"; + } + + @CrossOrigin( + value = "^http(|s):\\/\\/foo\\.com$" + // allowedOriginsRegex defaults to false + ) + @Produces(MediaType.TEXT_PLAIN) + @Get("/bar") + String bar() { + return "bar"; + } + } + + @Requires(property = "spec.name", value = SPECNAME) + @Controller("/methods") + @CrossOrigin( + allowedOrigins = "https://www.google.com", + allowedMethods = { HttpMethod.GET, HttpMethod.POST } + ) + static class AllowedMethods { + @Produces(MediaType.TEXT_PLAIN) + @Get("/getit") + String canGet() { + return "get"; + } + + @Produces(MediaType.TEXT_PLAIN) + @Post("/postit/{id}") + String canPost(@PathVariable String id) { + return id; + } + + @Delete("/deleteit/{id}") + String cantDelete(@PathVariable String id) { + return id; + } + } + + @Requires(property = "spec.name", value = SPECNAME) + @Controller("/allowedheaders") + @CrossOrigin( + value = "https://foo.com", + allowedHeaders = { HttpHeaders.CONTENT_TYPE, HttpHeaders.AUTHORIZATION } + ) + static class AllowedHeaders { + @Produces(MediaType.TEXT_PLAIN) + @Get("/bar") + String index() { + return "bar"; + } + } + + @Requires(property = "spec.name", value = SPECNAME) + @Controller("/exposedheaders") + static class ExposedHeaders { + @CrossOrigin( + value = "https://foo.com", + exposedHeaders = { "Content-Encoding", "Kuma-Revision" } + ) + @Produces(MediaType.TEXT_PLAIN) + @Get("/bar") + String bar() { + return "bar"; + } + + @CrossOrigin( + value = "https://foo.com" + ) + @Produces(MediaType.TEXT_PLAIN) + @Get("/foo") + String foo() { + return "foo"; + } + } + + @Requires(property = "spec.name", value = SPECNAME) + @Controller("/credentials") + static class Credentials { + @CrossOrigin( + value = "https://foo.com", + allowCredentials = false + ) + @Produces(MediaType.TEXT_PLAIN) + @Get("/foo") + String foo() { + return "foo"; + } + + @CrossOrigin( + value = "https://foo.com" + ) + @Produces(MediaType.TEXT_PLAIN) + @Get("/bar") + String bar() { + return "bar"; + } + } + + @Requires(property = "spec.name", value = SPECNAME) + @Controller("/maxage") + static class MaxAge { + @CrossOrigin( + value = "https://foo.com" + ) + @Produces(MediaType.TEXT_PLAIN) + @Get("/foo") + String foo() { + return "foo"; + } + + @CrossOrigin( + value = "https://foo.com", + maxAge = 1000L + ) + @Produces(MediaType.TEXT_PLAIN) + @Get("/bar") + String bar() { + return "bar"; + } + } + + @Requires(property = "spec.name", value = SPECNAME) + @Replaces(HttpHostResolver.class) + @Singleton + static class HttpHostResolverReplacement implements HttpHostResolver { + @Override + public String resolve(@Nullable HttpRequest request) { + return "https://micronautexample.com"; + } + } +} diff --git a/http-server/src/main/java/io/micronaut/http/server/cors/CorsFilter.java b/http-server/src/main/java/io/micronaut/http/server/cors/CorsFilter.java index 58eef88f473..7bf75191a32 100644 --- a/http-server/src/main/java/io/micronaut/http/server/cors/CorsFilter.java +++ b/http-server/src/main/java/io/micronaut/http/server/cors/CorsFilter.java @@ -97,7 +97,7 @@ public Publisher> doFilter(HttpRequest request, Server LOG.trace("Http Header " + HttpHeaders.ORIGIN + " not present. Proceeding with the request."); return chain.proceed(request); } - CorsOriginConfiguration corsOriginConfiguration = getConfiguration(origin).orElse(null); + CorsOriginConfiguration corsOriginConfiguration = getConfiguration(request).orElse(null); if (corsOriginConfiguration != null) { if (CorsUtil.isPreflightRequest(request)) { return handlePreflightRequest(request, chain, corsOriginConfiguration); @@ -317,31 +317,45 @@ protected void setMaxAge(long maxAge, MutableHttpResponse response) { } @NonNull - private Optional getConfiguration(@NonNull String requestOrigin) { + private Optional getConfiguration(@NonNull HttpRequest request) { + String requestOrigin = request.getHeaders().getOrigin().orElse(null); + if (requestOrigin == null) { + return Optional.empty(); + } + Optional originConfiguration = CrossOriginUtil.getCorsOriginConfigurationForRequest(request); + if (originConfiguration.isPresent() && matchesOrigin(originConfiguration.get(), requestOrigin)) { + return originConfiguration; + } if (!corsConfiguration.isEnabled()) { return Optional.empty(); } return corsConfiguration.getConfigurations().values().stream() - .filter(config -> { - List allowedOrigins = config.getAllowedOrigins(); - return !allowedOrigins.isEmpty() && (isAny(allowedOrigins) || allowedOrigins.stream().anyMatch(origin -> matchesOrigin(origin, requestOrigin))); - }).findFirst(); + .filter(config -> matchesOrigin(config, requestOrigin)) + .findFirst(); } - private boolean matchesOrigin(@NonNull String origin, @NonNull String requestOrigin) { - if (origin.equals(requestOrigin)) { + private static boolean matchesOrigin(@NonNull CorsOriginConfiguration config, String requestOrigin) { + if (config.getAllowedOriginsRegex().map(regex -> matchesOrigin(regex, requestOrigin)).orElse(false)) { return true; } - Pattern p = Pattern.compile(origin); + List allowedOrigins = config.getAllowedOrigins(); + return !allowedOrigins.isEmpty() && ( + (!config.getAllowedOriginsRegex().isPresent() && isAny(allowedOrigins)) || + allowedOrigins.stream().anyMatch(origin -> origin.equals(requestOrigin)) + ); + } + + private static boolean matchesOrigin(@NonNull String originRegex, @NonNull String requestOrigin) { + Pattern p = Pattern.compile(originRegex); Matcher m = p.matcher(requestOrigin); return m.matches(); } - private boolean isAny(List values) { + private static boolean isAny(List values) { return values == CorsOriginConfiguration.ANY; } - private boolean isAnyMethod(List allowedMethods) { + private static boolean isAnyMethod(List allowedMethods) { return allowedMethods == CorsOriginConfiguration.ANY_METHOD; } diff --git a/http-server/src/main/java/io/micronaut/http/server/cors/CorsOriginConfiguration.java b/http-server/src/main/java/io/micronaut/http/server/cors/CorsOriginConfiguration.java index 8632dc27e90..e0be0f806a4 100644 --- a/http-server/src/main/java/io/micronaut/http/server/cors/CorsOriginConfiguration.java +++ b/http-server/src/main/java/io/micronaut/http/server/cors/CorsOriginConfiguration.java @@ -15,11 +15,13 @@ */ package io.micronaut.http.server.cors; +import io.micronaut.core.annotation.NonNull; import io.micronaut.core.annotation.Nullable; +import io.micronaut.core.util.StringUtils; import io.micronaut.http.HttpMethod; - import java.util.Collections; import java.util.List; +import java.util.Optional; /** * Stores configuration for CORS. @@ -29,7 +31,6 @@ * @since 1.0 */ public class CorsOriginConfiguration { - /** * Constant to represent any value. */ @@ -41,6 +42,7 @@ public class CorsOriginConfiguration { public static final List ANY_METHOD = Collections.emptyList(); private List allowedOrigins = ANY; + private String allowedOriginsRegex; private List allowedMethods = ANY_METHOD; private List allowedHeaders = ANY; private List exposedHeaders = Collections.emptyList(); @@ -65,6 +67,26 @@ public void setAllowedOrigins(@Nullable List allowedOrigins) { } } + /** + * @return a regular expression for matching Allowed Origins. + */ + @NonNull + public Optional getAllowedOriginsRegex() { + if (allowedOriginsRegex == null || allowedOriginsRegex.equals(StringUtils.EMPTY_STRING)) { + return Optional.empty(); + } + return Optional.ofNullable(allowedOriginsRegex); + } + + /** + * Sets a regular expression for matching Allowed Origins. + * + * @param allowedOriginsRegex a regular expression for matching Allowed Origins. + */ + public void setAllowedOriginsRegex(String allowedOriginsRegex) { + this.allowedOriginsRegex = allowedOriginsRegex; + } + /** * @return The allowed methods */ diff --git a/http-server/src/main/java/io/micronaut/http/server/cors/CorsOriginConverter.java b/http-server/src/main/java/io/micronaut/http/server/cors/CorsOriginConverter.java index a5b4da4112b..baa6cd42f37 100644 --- a/http-server/src/main/java/io/micronaut/http/server/cors/CorsOriginConverter.java +++ b/http-server/src/main/java/io/micronaut/http/server/cors/CorsOriginConverter.java @@ -40,6 +40,7 @@ public class CorsOriginConverter implements TypeConverter, CorsOriginConfiguration> { private static final String ALLOWED_ORIGINS = "allowed-origins"; + private static final String ALLOWED_ORIGINS_REGEX = "allowed-origins-regex"; private static final String ALLOWED_METHODS = "allowed-methods"; private static final String ALLOWED_HEADERS = "allowed-headers"; private static final String EXPOSED_HEADERS = "exposed-headers"; @@ -57,6 +58,10 @@ public Optional convert(Map object, Cla .get(ALLOWED_ORIGINS, ConversionContext.LIST_OF_STRING) .ifPresent(configuration::setAllowedOrigins); + convertibleValues + .get(ALLOWED_ORIGINS_REGEX, ConversionContext.STRING) + .ifPresent(configuration::setAllowedOriginsRegex); + convertibleValues .get(ALLOWED_METHODS, CONVERSION_CONTEXT_LIST_OF_HTTP_METHOD) .ifPresent(configuration::setAllowedMethods); diff --git a/http-server/src/main/java/io/micronaut/http/server/cors/CrossOrigin.java b/http-server/src/main/java/io/micronaut/http/server/cors/CrossOrigin.java new file mode 100644 index 00000000000..4eeed2849b0 --- /dev/null +++ b/http-server/src/main/java/io/micronaut/http/server/cors/CrossOrigin.java @@ -0,0 +1,91 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.http.server.cors; + +import io.micronaut.context.annotation.AliasFor; +import io.micronaut.core.util.StringUtils; +import io.micronaut.http.HttpMethod; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +/** + * Support CORs configuration via annotation. For example, it will enable Micronaut developers only + * to allow CORS for a few routes in their applications. Thus, having more secure + * applications. + * @since 3.9.0 + */ +@Documented +@Inherited +@Retention(RUNTIME) +@Target({ElementType.TYPE, ElementType.METHOD}) +public @interface CrossOrigin { + + /** + * + * @return the origins for which cross-origin requests are allowed + */ + @AliasFor(member = "allowedOrigins") + String[] value() default {}; + + /** + * + * @return the origins for which cross-origin requests are allowed + */ + @AliasFor(member = "value") + String[] allowedOrigins() default {}; + + /** + * + * @return regular expression to match allowed origins + */ + String allowedOriginsRegex() default StringUtils.EMPTY_STRING; + + /** + * + * @return request headers permitted in requests + */ + String[] allowedHeaders() default {}; + + /** + * + * @return response headers that user-agent will allow client to access on actual response + */ + String[] exposedHeaders() default {}; + + /** + * + * @return supported HTTP request methods + */ + HttpMethod[] allowedMethods() default {}; + + /** + * + * @return whether the browser should send credentials + */ + boolean allowCredentials() default true; + + /** + * + * @return maximum age (in seconds) of the cache duration for preflight responses + */ + long maxAge() default 1800L; +} diff --git a/http-server/src/main/java/io/micronaut/http/server/cors/CrossOriginUtil.java b/http-server/src/main/java/io/micronaut/http/server/cors/CrossOriginUtil.java new file mode 100644 index 00000000000..4c7cf81bccd --- /dev/null +++ b/http-server/src/main/java/io/micronaut/http/server/cors/CrossOriginUtil.java @@ -0,0 +1,86 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.http.server.cors; + +import io.micronaut.core.annotation.AnnotationMetadata; +import io.micronaut.core.annotation.NonNull; +import io.micronaut.core.util.CollectionUtils; +import io.micronaut.http.HttpAttributes; +import io.micronaut.http.HttpMethod; +import io.micronaut.http.HttpRequest; + +import java.util.Arrays; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * Utility classes to work with {@link CrossOrigin}. + * @author Sergio del Amo + * @since 3.9.0 + */ +public final class CrossOriginUtil { + + public static final String MEMBER_ALLOWED_ORIGINS = "allowedOrigins"; + public static final String MEMBER_ALLOWED_ORIGINS_REGEX = "allowedOriginsRegex"; + public static final String MEMBER_ALLOWED_HEADERS = "allowedHeaders"; + public static final String MEMBER_EXPOSED_HEADERS = "exposedHeaders"; + public static final String MEMBER_ALLOWED_METHODS = "allowedMethods"; + public static final String MEMBER_ALLOW_CREDENTIALS = "allowCredentials"; + public static final String MEMBER_MAX_AGE = "maxAge"; + + private CrossOriginUtil() { + } + + /** + * @param request the HTTP request for the configuration + * @return the cors origin configuration for the given request + */ + @NonNull + public static Optional getCorsOriginConfigurationForRequest(@NonNull HttpRequest request) { + return request.getAttribute(HttpAttributes.ROUTE_MATCH, AnnotationMetadata.class) + .flatMap(CrossOriginUtil::getCorsOriginConfiguration); + } + + private static Optional getCorsOriginConfiguration(@NonNull AnnotationMetadata annotationMetadata) { + if (!annotationMetadata.hasAnnotation(CrossOrigin.class)) { + return Optional.empty(); + } + CorsOriginConfiguration config = new CorsOriginConfiguration(); + config.setAllowedOrigins(Arrays.asList(annotationMetadata.stringValues(CrossOrigin.class, MEMBER_ALLOWED_ORIGINS))); + annotationMetadata.stringValue(CrossOrigin.class, MEMBER_ALLOWED_ORIGINS_REGEX) + .ifPresent(config::setAllowedOriginsRegex); + + String[] allowedHeaders = annotationMetadata.stringValues(CrossOrigin.class, MEMBER_ALLOWED_HEADERS); + List allowedHeadersList = allowedHeaders.length == 0 ? CorsOriginConfiguration.ANY : Arrays.asList(allowedHeaders); + config.setAllowedHeaders(allowedHeadersList); + config.setExposedHeaders(Arrays.asList(annotationMetadata.stringValues(CrossOrigin.class, MEMBER_EXPOSED_HEADERS))); + + + List allowedMethods = Stream.of(annotationMetadata.stringValues(CrossOrigin.class, MEMBER_ALLOWED_METHODS)) + .map(HttpMethod::parse) + .filter(method -> method != HttpMethod.CUSTOM) + .collect(Collectors.toList()); + config.setAllowedMethods(CollectionUtils.isNotEmpty(allowedMethods) ? allowedMethods : CorsOriginConfiguration.ANY_METHOD); + + annotationMetadata.booleanValue(CrossOrigin.class, MEMBER_ALLOW_CREDENTIALS) + .ifPresent(config::setAllowCredentials); + annotationMetadata.longValue(CrossOrigin.class, MEMBER_MAX_AGE) + .ifPresent(config::setMaxAge); + return Optional.of(config); + } +} diff --git a/src/main/docs/guide/appendix/breaks.adoc b/src/main/docs/guide/appendix/breaks.adoc index e5129b3aaeb..93fcd101773 100644 --- a/src/main/docs/guide/appendix/breaks.adoc +++ b/src/main/docs/guide/appendix/breaks.adoc @@ -1,5 +1,9 @@ This section documents breaking changes between Micronaut versions +== 3.9.0 + +Since Micronaut Framework 3.9.0, CORS `allowed-origins` configuration does not support regular expressions to prevent accidentally exposing your API. You can use `allowed-origins-regex`, if you wish to support a regular expression. + == 3.8.7 Micronaut Framework 3.8.7 updates to https://bitbucket.org/snakeyaml/snakeyaml/wiki/Changes[SnakeYAML 2.0] which addresses https://nvd.nist.gov/vuln/detail/CVE-2022-1471[CVE-2022-1471]. Many organizations' policies forbid their teams to use Micronaut Framework if the framework depends on a vulnerable dependency, even if the framework is unaffected. Micronaut Framework is not affected by https://nvd.nist.gov/vuln/detail/CVE-2022-1471[CVE-2022-1471]. diff --git a/src/main/docs/guide/httpServer/serverConfiguration/cors/annotationBasedCors.adoc b/src/main/docs/guide/httpServer/serverConfiguration/cors/annotationBasedCors.adoc new file mode 100644 index 00000000000..62229e41cda --- /dev/null +++ b/src/main/docs/guide/httpServer/serverConfiguration/cors/annotationBasedCors.adoc @@ -0,0 +1,27 @@ +Micronaut CORS configuration applies by default to all endpoints in the running application. + +As an alternative, <> can be applied in a more fine-grained manner to specific routes using the +link:{api}/io/micronaut/http/server/cors/CrossOrigin.html[@CrossOrigin] annotation. This is applied to `@Controller` to apply the CORS configuration to all endpoints in the controller. Alternatively, the annotation can be applied to specific endpoints on a controller, for even more fine-grained control. + +The `@CrossOrigin` annotation maps with a one-to-one correspondence to application-wide link:{api}/io/micronaut/http/server/cors/CorsOriginConfiguration.html[CorsOriginConfiguration] configuration properties. To specify just an allowed origin, use `@CrossOrigin("https://foo.com")`. To specify additional configuration details, use a combination of annotation attributes the same as you would for specifying global `CorsOriginConfiguration` properties. + +[source,java] +---- +@CrossOrigin( + allowedOrigins = { "http://foo.com" }, + allowedOriginsRegex = "^http(|s):\\/\\/www\\.google\\.com$", + allowedMethods = { HttpMethod.POST, HttpMethod.PUT }, + allowedHeaders = { HttpHeaders.CONTENT_TYPE, HttpHeaders.AUTHORIZATION }, + exposedHeaders = { HttpHeaders.CONTENT_TYPE, HttpHeaders.AUTHORIZATION }, + allowCredentials = false, + maxAge = 3600 +) +---- + +The following example demonstrates how the annotation might be applied to a specific endpoint. To enable CORS for all endpoints in the controller, move the annotation to the class level and configure it appropriately. + +snippet::io.micronaut.docs.http.server.cors.CorsController[tags="imports,controller", indent=0, title="@CrossOrigin Example"] + +<1> The ann:http.server.cors.CrossOrigin[] annotation is applied to a specific endpoint, making the CORS configuration fine-grained. +<2> The `GET /hello` endpoint has "https://myui.com" as an allowed cross origin endpoint +<3> The `GET /hello/nocors` endpoint cannot use "https://myui.com" as an origin, since it doesn't have a CORS configuration that allows it. diff --git a/src/main/docs/guide/httpServer/serverConfiguration/cors/corsAllowCredentials.adoc b/src/main/docs/guide/httpServer/serverConfiguration/cors/corsAllowCredentials.adoc index 65c5b6b121e..af5b6b4ade8 100644 --- a/src/main/docs/guide/httpServer/serverConfiguration/cors/corsAllowCredentials.adoc +++ b/src/main/docs/guide/httpServer/serverConfiguration/cors/corsAllowCredentials.adoc @@ -1,4 +1,4 @@ -Credentials are allowed by default for CORS requests. To disallow credentials, set the `allowCredentials` option to `false`. +Credentials are allowed by default for CORS requests. To disallow credentials, set the `allow-credentials` option to `false`. .Example CORS Configuration [configuration] @@ -9,5 +9,5 @@ micronaut: enabled: true configurations: web: - allowCredentials: false + allow-credentials: false ---- diff --git a/src/main/docs/guide/httpServer/serverConfiguration/cors/corsAllowedHeaders.adoc b/src/main/docs/guide/httpServer/serverConfiguration/cors/corsAllowedHeaders.adoc index ea08df88177..fb6e540e8a7 100644 --- a/src/main/docs/guide/httpServer/serverConfiguration/cors/corsAllowedHeaders.adoc +++ b/src/main/docs/guide/httpServer/serverConfiguration/cors/corsAllowedHeaders.adoc @@ -1,6 +1,6 @@ -To allow any request header for a given configuration, don't include the `allowedHeaders` key in your configuration. +To allow any request header for a given configuration, don't include the `allowed-headers` key in your configuration. -For multiple allowed headers, set the `allowedHeaders` key of the configuration to a list of strings. +For multiple allowed headers, set the `allowed-headers` key of the configuration to a list of strings. .Example CORS Configuration [configuration] @@ -11,7 +11,7 @@ micronaut: enabled: true configurations: web: - allowedHeaders: + allowed-headers: - Content-Type - Authorization ---- diff --git a/src/main/docs/guide/httpServer/serverConfiguration/cors/corsAllowedMethods.adoc b/src/main/docs/guide/httpServer/serverConfiguration/cors/corsAllowedMethods.adoc index a2b033ae3d5..c34bc16f788 100644 --- a/src/main/docs/guide/httpServer/serverConfiguration/cors/corsAllowedMethods.adoc +++ b/src/main/docs/guide/httpServer/serverConfiguration/cors/corsAllowedMethods.adoc @@ -1,6 +1,6 @@ -To allow any request method for a given configuration, don't include the `allowedMethods` key in your configuration. +To allow any request method for a given configuration, don't include the `allowed-methods` key in your configuration. -For multiple allowed methods, set the `allowedMethods` key of the configuration to a list of strings. +For multiple allowed methods, set the `allowed-methods` key of the configuration to a list of strings. .Example CORS Configuration [configuration] @@ -11,7 +11,7 @@ micronaut: enabled: true configurations: web: - allowedMethods: + allowed-methods: - POST - PUT ---- diff --git a/src/main/docs/guide/httpServer/serverConfiguration/cors/corsAllowedOrigins.adoc b/src/main/docs/guide/httpServer/serverConfiguration/cors/corsAllowedOrigins.adoc index a1a10186380..580473c5a0e 100644 --- a/src/main/docs/guide/httpServer/serverConfiguration/cors/corsAllowedOrigins.adoc +++ b/src/main/docs/guide/httpServer/serverConfiguration/cors/corsAllowedOrigins.adoc @@ -1,8 +1,8 @@ -To allow any origin for a given configuration, don't include the `allowedOrigins` key in your configuration. +Don't define `allowed-origins` or `allowed-origins-regex` to allow any origin for a given configuration. -For multiple valid origins, set the `allowedOrigins` key of the configuration to a list of strings. Each value can either be a static value (`http://www.foo.com`) or a regular expression (`^http(|s)://www\.google\.com$`). +For multiple valid origins, set the `allowed-origins` key of the configuration to a list of strings. -Regular expressions are passed to link:{javase}java/util/regex/Pattern.html#compile-java.lang.String-[Pattern#compile] and compared to the request origin with link:{javase}java/util/regex/Matcher.html#matches--[Matcher#matches]. +You can also define via `allowed-origins-regex` a regular expression (`^http(|s)://www\.google\.com$`). The Regular expression is passed to link:{javase}java/util/regex/Pattern.html#compile-java.lang.String-[Pattern#compile] and compared to the request origin with link:{javase}java/util/regex/Matcher.html#matches--[Matcher#matches]. .Example CORS Configuration [configuration] @@ -13,7 +13,9 @@ micronaut: enabled: true configurations: web: - allowedOrigins: + allowed-origins-regex: '^http(|s):\/\/www\.google\.com$' + allowed-origins: - http://foo.com - - ^http(|s):\/\/www\.google\.com$ ---- + +WARNING: Use the `allowed-origins-regex` configuration judiciously. You may accidentally make an insecure configuration which could be targeted by an attacker registering domains targeting the regular expression. diff --git a/src/main/docs/guide/httpServer/serverConfiguration/cors/corsExposedHeaders.adoc b/src/main/docs/guide/httpServer/serverConfiguration/cors/corsExposedHeaders.adoc index ed2aeb6df5f..77bbf99c226 100644 --- a/src/main/docs/guide/httpServer/serverConfiguration/cors/corsExposedHeaders.adoc +++ b/src/main/docs/guide/httpServer/serverConfiguration/cors/corsExposedHeaders.adoc @@ -1,4 +1,4 @@ -To configure the headers that are sent in the response to a CORS request through the `Access-Control-Expose-Headers` header, include a list of strings for the `exposedHeaders` key in your configuration. None are exposed by default. +To configure the headers that are sent in the response to a CORS request through the `Access-Control-Expose-Headers` header, include a list of strings for the `exposed-headers` key in your configuration. None are exposed by default. .Example CORS Configuration [configuration] @@ -9,7 +9,7 @@ micronaut: enabled: true configurations: web: - exposedHeaders: + exposed-headers: - Content-Type - Authorization ---- diff --git a/src/main/docs/guide/httpServer/serverConfiguration/cors/corsMaxAge.adoc b/src/main/docs/guide/httpServer/serverConfiguration/cors/corsMaxAge.adoc index 93f2c2557f9..34523504b66 100644 --- a/src/main/docs/guide/httpServer/serverConfiguration/cors/corsMaxAge.adoc +++ b/src/main/docs/guide/httpServer/serverConfiguration/cors/corsMaxAge.adoc @@ -9,5 +9,5 @@ micronaut: enabled: true configurations: web: - maxAge: 3600 # 1 hour + max-age: 3600 # 1 hour ---- diff --git a/src/main/docs/guide/toc.yml b/src/main/docs/guide/toc.yml index f4f4d21ff63..f28965377d5 100644 --- a/src/main/docs/guide/toc.yml +++ b/src/main/docs/guide/toc.yml @@ -130,6 +130,7 @@ httpServer: listener: Advanced Listener Configuration cors: title: Configuring CORS + annotationBasedCors: Annotation-based CORS Configuration corsConfiguration: CORS via Configuration corsAllowedOrigins: Allowed Origins corsAllowedMethods: Allowed Methods diff --git a/test-suite-groovy/src/test/groovy/io/micronaut/docs/http/server/cors/CorsController.groovy b/test-suite-groovy/src/test/groovy/io/micronaut/docs/http/server/cors/CorsController.groovy new file mode 100644 index 00000000000..50f8d05c4f0 --- /dev/null +++ b/test-suite-groovy/src/test/groovy/io/micronaut/docs/http/server/cors/CorsController.groovy @@ -0,0 +1,29 @@ +package io.micronaut.docs.http.server.cors + +import io.micronaut.context.annotation.Requires + +// tag::imports[] +import io.micronaut.http.MediaType +import io.micronaut.http.annotation.Controller +import io.micronaut.http.annotation.Get +import io.micronaut.http.annotation.Produces +import io.micronaut.http.server.cors.CrossOrigin +// end::imports[] + +@Requires(property = "spec.name", value = "CorsControllerSpec") +// tag::controller[] +@Controller("/hello") +class CorsController { + @CrossOrigin("https://myui.com") // <1> + @Get(produces = MediaType.TEXT_PLAIN) // <2> + String cors() { + return "Welcome to the worlds of CORS" + } + + @Produces(MediaType.TEXT_PLAIN) + @Get("/nocors") // <3> + String nocorstoday() { + return "No more CORS for you" + } +} +// end::controller[] diff --git a/test-suite-groovy/src/test/groovy/io/micronaut/docs/http/server/cors/CorsControllerSpec.groovy b/test-suite-groovy/src/test/groovy/io/micronaut/docs/http/server/cors/CorsControllerSpec.groovy new file mode 100644 index 00000000000..0e147c50bd1 --- /dev/null +++ b/test-suite-groovy/src/test/groovy/io/micronaut/docs/http/server/cors/CorsControllerSpec.groovy @@ -0,0 +1,61 @@ +package io.micronaut.docs.http.server.cors + +import io.micronaut.context.ApplicationContext +import io.micronaut.core.util.CollectionUtils +import io.micronaut.http.HttpHeaders +import io.micronaut.http.HttpMethod +import io.micronaut.http.HttpRequest +import io.micronaut.http.MediaType +import io.micronaut.http.MutableHttpRequest +import io.micronaut.http.client.BlockingHttpClient +import io.micronaut.http.client.HttpClient +import io.micronaut.http.client.exceptions.HttpClientResponseException +import io.micronaut.http.uri.UriBuilder +import io.micronaut.runtime.server.EmbeddedServer +import spock.lang.Specification + +class CorsControllerSpec extends Specification { + + void "CrossOrigin with allowed Origin"() { + given: + EmbeddedServer embeddedServer = ApplicationContext.run(EmbeddedServer, CollectionUtils.mapOf("spec.name", "CorsControllerSpec")) + HttpRequest request = preflight(UriBuilder.of("/hello"), "https://myui.com", HttpMethod.GET) + HttpClient httpClient = embeddedServer.applicationContext.createBean(HttpClient, embeddedServer.URL) + BlockingHttpClient client = httpClient.toBlocking() + + when: + client.exchange(request) + + then: + noExceptionThrown() + + cleanup: + httpClient.close() + embeddedServer.close() + } + + void "CrossOrigin with not allowed Origin"() { + given: + EmbeddedServer embeddedServer = ApplicationContext.run(EmbeddedServer, CollectionUtils.mapOf("spec.name", "CorsControllerSpec")) + HttpRequest request = preflight(UriBuilder.of("/hello"), "https://google.com", HttpMethod.GET) + HttpClient httpClient = embeddedServer.applicationContext.createBean(HttpClient, embeddedServer.URL) + BlockingHttpClient client = httpClient.toBlocking() + + when: + client.exchange(request) + + then: + thrown(HttpClientResponseException) + + cleanup: + httpClient.close() + embeddedServer.close() + } + + private static MutableHttpRequest preflight(UriBuilder uriBuilder, String originValue, HttpMethod method) { + return HttpRequest.OPTIONS(uriBuilder.build()) + .header(HttpHeaders.ACCEPT, MediaType.TEXT_PLAIN) + .header(HttpHeaders.ORIGIN, originValue) + .header(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, method) + } +} diff --git a/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/http/server/cors/CorsController.kt b/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/http/server/cors/CorsController.kt new file mode 100644 index 00000000000..682b1be033b --- /dev/null +++ b/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/http/server/cors/CorsController.kt @@ -0,0 +1,28 @@ +package io.micronaut.docs.http.server.cors + +// tag::imports[] +import io.micronaut.context.annotation.Requires +import io.micronaut.http.MediaType +import io.micronaut.http.annotation.Controller +import io.micronaut.http.annotation.Get +import io.micronaut.http.annotation.Produces +import io.micronaut.http.server.cors.CrossOrigin +// end::imports[] + +@Requires(property = "spec.name", value = "CorsControllerSpec") +// tag::controller[] +@Controller("/hello") +class CorsController { + @CrossOrigin("https://myui.com") // <1> + @Get(produces = [MediaType.TEXT_PLAIN]) // <2> + fun cors(): String { + return "Welcome to the worlds of CORS" + } + + @Produces(MediaType.TEXT_PLAIN) + @Get("/nocors") // <3> + fun nocorstoday(): String { + return "No more CORS for you" + } +} +// end::controller[] diff --git a/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/http/server/cors/CorsControllerTest.kt b/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/http/server/cors/CorsControllerTest.kt new file mode 100644 index 00000000000..799c09ecc25 --- /dev/null +++ b/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/http/server/cors/CorsControllerTest.kt @@ -0,0 +1,47 @@ +package io.micronaut.docs.http.server.cors + +import io.micronaut.context.ApplicationContext +import io.micronaut.http.* +import io.micronaut.http.client.HttpClient +import io.micronaut.http.client.exceptions.HttpClientResponseException +import io.micronaut.http.uri.UriBuilder +import io.micronaut.runtime.server.EmbeddedServer +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Test + +class CorsControllerTest { + @Test + fun crossOriginWithAllowedOrigin() { + val embeddedServer = ApplicationContext.run(EmbeddedServer::class.java, mapOf("spec.name" to "CorsControllerSpec")) + val request: HttpRequest = preflight(UriBuilder.of("/hello"), "https://myui.com", HttpMethod.GET) + val httpClient = embeddedServer.applicationContext.createBean(HttpClient::class.java, embeddedServer.url) + val client = httpClient.toBlocking() + Assertions.assertDoesNotThrow> { + client.exchange(request) + } + httpClient.close() + embeddedServer.close() + } + + @Test + fun crossOriginWithNotAllowedOrigin() { + val embeddedServer = ApplicationContext.run(EmbeddedServer::class.java, mapOf("spec.name" to "CorsControllerSpec")) + val request: HttpRequest = preflight(UriBuilder.of("/hello"), "https://google.com", HttpMethod.GET) + val httpClient = embeddedServer.applicationContext.createBean( + HttpClient::class.java, embeddedServer.url + ) + val client = httpClient.toBlocking() + Assertions.assertThrows(HttpClientResponseException::class.java) { + val response: HttpResponse = client.exchange(request) + } + httpClient.close() + embeddedServer.close() + } + + fun preflight(uriBuilder: UriBuilder, originValue: String, method: HttpMethod): HttpRequest { + return HttpRequest.OPTIONS(uriBuilder.build()) + .header(HttpHeaders.ACCEPT, MediaType.TEXT_PLAIN) + .header(HttpHeaders.ORIGIN, originValue) + .header(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, method) + } +} diff --git a/test-suite/src/test/java/io/micronaut/docs/http/server/cors/CorsController.java b/test-suite/src/test/java/io/micronaut/docs/http/server/cors/CorsController.java new file mode 100644 index 00000000000..b84e3ab8bcc --- /dev/null +++ b/test-suite/src/test/java/io/micronaut/docs/http/server/cors/CorsController.java @@ -0,0 +1,28 @@ +package io.micronaut.docs.http.server.cors; + +// tag::imports[] +import io.micronaut.context.annotation.Requires; +import io.micronaut.http.MediaType; +import io.micronaut.http.annotation.Controller; +import io.micronaut.http.annotation.Get; +import io.micronaut.http.annotation.Produces; +import io.micronaut.http.server.cors.CrossOrigin; +// end::imports[] + +@Requires(property = "spec.name", value = "CorsControllerSpec") +// tag::controller[] +@Controller("/hello") +public class CorsController { + @CrossOrigin("https://myui.com") // <1> + @Get(produces = MediaType.TEXT_PLAIN) // <2> + public String cors() { + return "Welcome to the worlds of CORS"; + } + + @Produces(MediaType.TEXT_PLAIN) + @Get("/nocors") // <3> + public String nocorstoday() { + return "No more CORS for you"; + } +} +// end::controller[] diff --git a/test-suite/src/test/java/io/micronaut/docs/http/server/cors/CorsControllerTest.java b/test-suite/src/test/java/io/micronaut/docs/http/server/cors/CorsControllerTest.java new file mode 100644 index 00000000000..be05521708e --- /dev/null +++ b/test-suite/src/test/java/io/micronaut/docs/http/server/cors/CorsControllerTest.java @@ -0,0 +1,53 @@ +package io.micronaut.docs.http.server.cors; + +import io.micronaut.context.ApplicationContext; +import io.micronaut.core.util.CollectionUtils; +import io.micronaut.http.*; +import io.micronaut.http.client.BlockingHttpClient; +import io.micronaut.http.client.HttpClient; +import io.micronaut.http.client.exceptions.HttpClientResponseException; +import io.micronaut.http.uri.UriBuilder; +import io.micronaut.runtime.server.EmbeddedServer; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.function.Executable; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertThrows; + +class CorsControllerTest { + + @Test + void crossOriginWithAllowedOrigin() { + + EmbeddedServer embeddedServer = ApplicationContext.run(EmbeddedServer.class, CollectionUtils.mapOf("spec.name", "CorsControllerSpec")); + HttpRequest request = preflight(UriBuilder.of("/hello"), "https://myui.com", HttpMethod.GET); + HttpClient httpClient = embeddedServer.getApplicationContext().createBean(HttpClient.class, embeddedServer.getURL()); + BlockingHttpClient client = httpClient.toBlocking(); + + assertDoesNotThrow(() -> client.exchange(request)); + + httpClient.close(); + embeddedServer.close(); + } + + @Test + void crossOriginWithNotAllowedOrigin() { + EmbeddedServer embeddedServer = ApplicationContext.run(EmbeddedServer.class, CollectionUtils.mapOf("spec.name", "CorsControllerSpec")); + HttpRequest request = preflight(UriBuilder.of("/hello"), "https://google.com", HttpMethod.GET); + HttpClient httpClient = embeddedServer.getApplicationContext().createBean(HttpClient.class, embeddedServer.getURL()); + BlockingHttpClient client = httpClient.toBlocking(); + + Executable e = () -> client.exchange(request); + assertThrows(HttpClientResponseException.class, e); + + httpClient.close(); + embeddedServer.close(); + } + + private static MutableHttpRequest preflight(UriBuilder uriBuilder, String originValue, HttpMethod method) { + return HttpRequest.OPTIONS(uriBuilder.build()) + .header(HttpHeaders.ACCEPT, MediaType.TEXT_PLAIN) + .header(HttpHeaders.ORIGIN, originValue) + .header(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, method); + } +} From 29cad06edbbdc513022ef1b649ca9b729f99db93 Mon Sep 17 00:00:00 2001 From: Jonas Konrad Date: Tue, 14 Mar 2023 15:09:56 +0100 Subject: [PATCH 594/743] Enable test for streaming h2c requests (#8406) Also fixes the subscription, moves the config to the new plaintext-mode property, and changes a switch in ConnectionManager to java 17 switch expression. Fixes #6282 --- .../http/client/netty/ConnectionManager.java | 44 ++++++++----------- .../http/server/netty/http2/H2cSpec.groovy | 12 +---- 2 files changed, 21 insertions(+), 35 deletions(-) diff --git a/http-client/src/main/java/io/micronaut/http/client/netty/ConnectionManager.java b/http-client/src/main/java/io/micronaut/http/client/netty/ConnectionManager.java index 88371f83c92..7a0d1814349 100644 --- a/http-client/src/main/java/io/micronaut/http/client/netty/ConnectionManager.java +++ b/http-client/src/main/java/io/micronaut/http/client/netty/ConnectionManager.java @@ -995,31 +995,25 @@ private ChannelFuture openConnectionFuture() { requestKey.getPort() ); } else { - switch (httpVersion.getPlaintextMode()) { - case HTTP_1: - initializer = new ChannelInitializer() { - @Override - protected void initChannel(@NonNull Channel ch) throws Exception { - configureProxy(ch.pipeline(), false, requestKey.getHost(), requestKey.getPort()); - initHttp1(ch); - ch.pipeline().addLast(ChannelPipelineCustomizer.HANDLER_ACTIVITY_LISTENER, new ChannelInboundHandlerAdapter() { - @Override - public void channelActive(@NonNull ChannelHandlerContext ctx) throws Exception { - super.channelActive(ctx); - ctx.pipeline().remove(this); - NettyClientCustomizer channelCustomizer = clientCustomizer.specializeForChannel(ch, NettyClientCustomizer.ChannelRole.CONNECTION); - new Http1ConnectionHolder(ch, channelCustomizer).init(true); - } - }); - } - }; - break; - case H2C: - initializer = new Http2UpgradeInitializer(this); - break; - default: - throw new AssertionError("Unknown plaintext mode"); - } + initializer = switch (httpVersion.getPlaintextMode()) { + case HTTP_1 -> new ChannelInitializer<>() { + @Override + protected void initChannel(@NonNull Channel ch) throws Exception { + configureProxy(ch.pipeline(), false, requestKey.getHost(), requestKey.getPort()); + initHttp1(ch); + ch.pipeline().addLast(ChannelPipelineCustomizer.HANDLER_ACTIVITY_LISTENER, new ChannelInboundHandlerAdapter() { + @Override + public void channelActive(@NonNull ChannelHandlerContext ctx) throws Exception { + super.channelActive(ctx); + ctx.pipeline().remove(this); + NettyClientCustomizer channelCustomizer = clientCustomizer.specializeForChannel(ch, NettyClientCustomizer.ChannelRole.CONNECTION); + new Http1ConnectionHolder(ch, channelCustomizer).init(true); + } + }); + } + }; + case H2C -> new Http2UpgradeInitializer(this); + }; } return doConnect(requestKey, initializer); } diff --git a/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/http2/H2cSpec.groovy b/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/http2/H2cSpec.groovy index 92df66cf4dc..2ce3c3b33ab 100644 --- a/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/http2/H2cSpec.groovy +++ b/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/http2/H2cSpec.groovy @@ -40,9 +40,7 @@ import org.reactivestreams.Publisher import org.reactivestreams.Subscriber import org.reactivestreams.Subscription import reactor.core.publisher.Flux -import spock.lang.Ignore import spock.lang.Issue -import spock.lang.PendingFeature import spock.lang.Specification import java.nio.charset.StandardCharsets @@ -52,7 +50,7 @@ import java.util.concurrent.TimeUnit @MicronautTest @Property(name = "micronaut.server.http-version", value = "2.0") //@Property(name = "micronaut.server.port", value = "8912") -@Property(name = "micronaut.http.client.http-version", value = "2.0") +@Property(name = "micronaut.http.client.plaintext-mode", value = "h2c") @Property(name = "micronaut.server.ssl.enabled", value = "false") @Issue('https://github.com/micronaut-projects/micronaut-core/issues/5005') class H2cSpec extends Specification { @@ -150,6 +148,7 @@ class H2cSpec extends Specification { streamingHttpClient.dataStream(HttpRequest.GET(url)).subscribe(new Subscriber>() { @Override void onSubscribe(Subscription s) { + s.request(Long.MAX_VALUE) } @Override @@ -171,19 +170,12 @@ class H2cSpec extends Specification { return composed.toString(StandardCharsets.UTF_8) } - @PendingFeature - @Ignore - // todo: streaming h2c is currently broken. This is because addFinalHandler is called after the stream receivers - // have been registered to the pipeline. This means that http2 messages aren't transformed to http messages - // properly. void 'test using micronaut http client: stream'() { expect: stream("http://localhost:${embeddedServer.port}/h2c/test") == 'foo' stream("http://localhost:${embeddedServer.port}/h2c/testStream") == 'foo' } - @PendingFeature - @Ignore void 'test using micronaut http client: stream reverse'() { // order matters because the client reuses connections expect: From a18658afdabe7bb7429c1c0fd6f60c50a0ea2cfd Mon Sep 17 00:00:00 2001 From: Jonas Konrad Date: Wed, 15 Mar 2023 11:25:55 +0100 Subject: [PATCH 595/743] test: More mTLS test cases (#8944) This PR adds more test cases for the server side of mTLS. These came from an internal user that reported expired certs being accepted. The test cases check a normal cert, an expired cert, and an untrusted cert. The previous RequestCertificateSpec only tests the "happy path" with the valid cert. These tests will prevent issues similar to #4116. It turns out that the behavior for expired certs is correct. When a cert is directly added to the trust store (not just its CA), the JDK does not check expiry. I think we should match that behavior. Also contains a small change to SelfSignedSslBuilder to make it actually use the configured trust store. This has no security implications, it just makes the tests work. --- .../netty/ssl/SelfSignedSslBuilder.java | 3 +- .../netty/ssl/RequestCertificateSpec2.groovy | 206 ++++++++++++++++++ 2 files changed, 208 insertions(+), 1 deletion(-) create mode 100644 http-server-netty/src/test/groovy/io/micronaut/http/server/netty/ssl/RequestCertificateSpec2.groovy diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/ssl/SelfSignedSslBuilder.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/ssl/SelfSignedSslBuilder.java index 14f55bd2da7..e9d6fca960c 100644 --- a/http-server-netty/src/main/java/io/micronaut/http/server/netty/ssl/SelfSignedSslBuilder.java +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/ssl/SelfSignedSslBuilder.java @@ -87,7 +87,8 @@ public Optional build(SslConfiguration ssl, HttpVersion httpVersion) LOG.warn("HTTP Server is configured to use a self-signed certificate ('build-self-signed' is set to true). This configuration should not be used in a production environment as self-signed certificates are inherently insecure."); } SelfSignedCertificate ssc = new SelfSignedCertificate(); - final SslContextBuilder sslBuilder = SslContextBuilder.forServer(ssc.certificate(), ssc.privateKey()); + final SslContextBuilder sslBuilder = SslContextBuilder.forServer(ssc.certificate(), ssc.privateKey()) + .trustManager(getTrustManagerFactory(ssl)); CertificateProvidedSslBuilder.setupSslBuilder(sslBuilder, ssl, httpVersion); return Optional.of(sslBuilder.build()); } catch (CertificateException | SSLException e) { diff --git a/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/ssl/RequestCertificateSpec2.groovy b/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/ssl/RequestCertificateSpec2.groovy new file mode 100644 index 00000000000..5bfccea6154 --- /dev/null +++ b/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/ssl/RequestCertificateSpec2.groovy @@ -0,0 +1,206 @@ +package io.micronaut.http.server.netty.ssl + +import io.micronaut.context.ApplicationContext +import io.micronaut.context.annotation.Requires +import io.micronaut.http.HttpRequest +import io.micronaut.http.annotation.Controller +import io.micronaut.http.annotation.Get +import io.micronaut.runtime.server.EmbeddedServer +import io.netty.handler.ssl.util.SelfSignedCertificate +import io.vertx.core.Vertx +import io.vertx.core.net.JksOptions +import io.vertx.ext.web.client.HttpResponse +import io.vertx.ext.web.client.WebClient +import io.vertx.ext.web.client.WebClientOptions +import spock.lang.Specification + +import javax.net.ssl.SSLHandshakeException +import java.nio.file.Files +import java.nio.file.Path +import java.security.KeyStore +import java.security.cert.Certificate +import java.security.cert.X509Certificate +import java.time.Instant +import java.time.temporal.ChronoUnit +import java.util.concurrent.CompletableFuture +import java.util.concurrent.ExecutionException + +class RequestCertificateSpec2 extends Specification { + def normal() { + given: + def certificate = new SelfSignedCertificate() + + def keyStorePath = Files.createTempFile("micronaut-test-key-store", "pkcs12") + def trustStorePath = Files.createTempFile("micronaut-test-trust-store", "pkcs12") + + writeStores(certificate, keyStorePath, trustStorePath) + + def ctx = ApplicationContext.run([ + "spec.name" : "RequestCertificateSpec2", + "micronaut.http.client.read-timeout" : "15s", + 'micronaut.server.ssl.enabled' : true, + 'micronaut.server.ssl.port' : -1, + 'micronaut.server.ssl.buildSelfSigned': true, + 'micronaut.ssl.clientAuthentication' : "need", + 'micronaut.ssl.trust-store.path' : 'file://' + trustStorePath.toString(), + 'micronaut.ssl.trust-store.type' : 'JKS', + 'micronaut.ssl.trust-store.password' : '123456', + ]) + + def server = ctx.getBean(EmbeddedServer) + server.start() + + def vertx = Vertx.vertx() + def client = WebClient.create(vertx, new WebClientOptions() + .setTrustAll(true) + .setSsl(true) + .setKeyCertOptions(new JksOptions().setPath(keyStorePath.toString()).setPassword("")) + .setTrustStoreOptions(new JksOptions().setPath(trustStorePath.toString()).setPassword("123456")) + ) + + when: + def future = new CompletableFuture>() + client.get(server.port, "localhost", "/mtls").send { if (it.failed()) future.completeExceptionally(it.cause()) else future.complete(it.result()) } + def response = future.get() + then: + response.bodyAsString() == 'CN=localhost' + response.statusCode() == 200 + + cleanup: + vertx.close() + ctx.close() + Files.deleteIfExists(keyStorePath) + Files.deleteIfExists(trustStorePath) + } + + def expired() { + // this is intended behavior: an expired client cert DOES NOT lead to a handshake failure. This is JDK behavior: + // when a cert is directly in the trust store, expiry is not checked. expiry is only checked if the cert is + // signed by a CA that is in the trust store. + + given: + def certificate = new SelfSignedCertificate(Date.from(Instant.now().minus(5, ChronoUnit.HOURS)), Date.from(Instant.now().minus(1, ChronoUnit.HOURS))) + + def keyStorePath = Files.createTempFile("micronaut-test-key-store", "pkcs12") + def trustStorePath = Files.createTempFile("micronaut-test-trust-store", "pkcs12") + + writeStores(certificate, keyStorePath, trustStorePath) + + def ctx = ApplicationContext.run([ + "spec.name" : "RequestCertificateSpec2", + "micronaut.http.client.read-timeout" : "15s", + 'micronaut.server.ssl.enabled' : true, + 'micronaut.server.ssl.port' : -1, + 'micronaut.server.ssl.buildSelfSigned': true, + 'micronaut.ssl.clientAuthentication' : "need", + 'micronaut.ssl.trust-store.path' : 'file://' + trustStorePath.toString(), + 'micronaut.ssl.trust-store.type' : 'JKS', + 'micronaut.ssl.trust-store.password' : '123456', + ]) + + def server = ctx.getBean(EmbeddedServer) + server.start() + + def vertx = Vertx.vertx() + def client = WebClient.create(vertx, new WebClientOptions() + .setTrustAll(true) + .setSsl(true) + .setKeyCertOptions(new JksOptions().setPath(keyStorePath.toString()).setPassword("")) + .setTrustStoreOptions(new JksOptions().setPath(trustStorePath.toString()).setPassword("123456")) + ) + + when: + def future = new CompletableFuture>() + client.get(server.port, "localhost", "/mtls").send { if (it.failed()) future.completeExceptionally(it.cause()) else future.complete(it.result()) } + def response = future.get() + then: + response.bodyAsString() == 'CN=localhost' + response.statusCode() == 200 + + cleanup: + vertx.close() + ctx.close() + Files.deleteIfExists(keyStorePath) + Files.deleteIfExists(trustStorePath) + } + + def untrusted() { + given: + def clientCert = new SelfSignedCertificate() + // for the client to send the cert, we still need the same CN in the trust store + def serverExpectsCert = new SelfSignedCertificate() + + def keyStorePath = Files.createTempFile("micronaut-test-key-store", "pkcs12") + def trustStorePath = Files.createTempFile("micronaut-test-trust-store", "pkcs12") + + writeStores(clientCert, keyStorePath, null) + writeStores(serverExpectsCert, null, trustStorePath) + + def ctx = ApplicationContext.run([ + "spec.name" : "RequestCertificateSpec2", + "micronaut.http.client.read-timeout" : "15s", + 'micronaut.server.ssl.enabled' : true, + 'micronaut.server.ssl.port' : -1, + 'micronaut.server.ssl.buildSelfSigned': true, + 'micronaut.ssl.clientAuthentication' : "need", + 'micronaut.ssl.trust-store.path' : 'file://' + trustStorePath.toString(), + 'micronaut.ssl.trust-store.type' : 'JKS', + 'micronaut.ssl.trust-store.password' : '123456', + ]) + + def server = ctx.getBean(EmbeddedServer) + server.start() + + def vertx = Vertx.vertx() + def client = WebClient.create(vertx, new WebClientOptions() + .setTrustAll(true) + .setSsl(true) + .setKeyCertOptions(new JksOptions().setPath(keyStorePath.toString()).setPassword("")) + .setTrustStoreOptions(new JksOptions().setPath(trustStorePath.toString()).setPassword("123456")) + ) + + when: + def future = new CompletableFuture>() + client.get(server.port, "localhost", "/mtls").send { if (it.failed()) future.completeExceptionally(it.cause()) else future.complete(it.result()) } + def response = future.get() + then: + def e = thrown ExecutionException + e.cause instanceof SSLHandshakeException + + cleanup: + vertx.close() + ctx.close() + Files.deleteIfExists(keyStorePath) + Files.deleteIfExists(trustStorePath) + } + + private void writeStores(SelfSignedCertificate certificate, Path keyStorePath, Path trustStorePath) { + if (keyStorePath != null) { + KeyStore ks = KeyStore.getInstance("PKCS12") + ks.load(null, null) + ks.setKeyEntry("key", certificate.key(), "".toCharArray(), new Certificate[]{certificate.cert()}) + try (OutputStream os = Files.newOutputStream(keyStorePath)) { + ks.store(os, "".toCharArray()) + } + } + + if (trustStorePath != null) { + KeyStore ts = KeyStore.getInstance("JKS") + ts.load(null, null) + ts.setCertificateEntry("cert", certificate.cert()) + try (OutputStream os = Files.newOutputStream(trustStorePath)) { + ts.store(os, "123456".toCharArray()) + } + } + } + + @Controller + @Requires(property = "spec.name", value = "RequestCertificateSpec2") + static class TestController { + @Get('/mtls') + String name(HttpRequest request) { + def cert = request.getCertificate().get() as X509Certificate + cert.issuerX500Principal.name + } + } +} From bdf7b2ed69099fdff5cdacc09dcc2015504834b4 Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Wed, 15 Mar 2023 15:02:36 +0100 Subject: [PATCH 596/743] Merge branch '3.9.x' into '4.0.x' (#8947) --- .../io/micronaut/scheduling/NextFireTime.java | 9 +- .../netty/ssl/SelfSignedSslBuilder.java | 3 +- .../server/netty/cors/CorsFilterSpec.groovy | 18 +- .../netty/cors/CrossOriginUtilSpec.groovy | 178 +++++++ .../netty/ssl/RequestCertificateSpec2.groovy | 206 ++++++++ .../http/server/tck/CorsAssertion.java | 182 +++++++ .../micronaut/http/server/tck/CorsUtils.java | 95 ++++ .../http/server/tck/tests/HeadersTest.java | 2 +- .../tests/cors/CorsDisabledByDefaultTest.java | 18 +- .../tck/tests/cors/CorsSimpleRequestTest.java | 27 +- .../tck/tests/cors/CrossOriginTest.java | 448 ++++++++++++++++++ .../http/server/cors/CorsFilter.java | 36 +- .../server/cors/CorsOriginConfiguration.java | 26 +- .../http/server/cors/CorsOriginConverter.java | 5 + .../http/server/cors/CrossOrigin.java | 91 ++++ .../http/server/cors/CrossOriginUtil.java | 86 ++++ src/main/docs/guide/appendix/breaks.adoc | 4 + .../cors/annotationBasedCors.adoc | 27 ++ .../cors/corsAllowCredentials.adoc | 4 +- .../cors/corsAllowedHeaders.adoc | 6 +- .../cors/corsAllowedMethods.adoc | 6 +- .../cors/corsAllowedOrigins.adoc | 12 +- .../cors/corsExposedHeaders.adoc | 4 +- .../serverConfiguration/cors/corsMaxAge.adoc | 2 +- src/main/docs/guide/toc.yml | 1 + .../http/server/cors/CorsController.groovy | 29 ++ .../server/cors/CorsControllerSpec.groovy | 61 +++ .../docs/http/server/cors/CorsController.kt | 28 ++ .../http/server/cors/CorsControllerTest.kt | 47 ++ .../docs/http/server/cors/CorsController.java | 28 ++ .../http/server/cors/CorsControllerTest.java | 53 +++ 31 files changed, 1671 insertions(+), 71 deletions(-) create mode 100644 http-server-netty/src/test/groovy/io/micronaut/http/server/netty/cors/CrossOriginUtilSpec.groovy create mode 100644 http-server-netty/src/test/groovy/io/micronaut/http/server/netty/ssl/RequestCertificateSpec2.groovy create mode 100644 http-server-tck/src/main/java/io/micronaut/http/server/tck/CorsAssertion.java create mode 100644 http-server-tck/src/main/java/io/micronaut/http/server/tck/CorsUtils.java create mode 100644 http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/cors/CrossOriginTest.java create mode 100644 http-server/src/main/java/io/micronaut/http/server/cors/CrossOrigin.java create mode 100644 http-server/src/main/java/io/micronaut/http/server/cors/CrossOriginUtil.java create mode 100644 src/main/docs/guide/httpServer/serverConfiguration/cors/annotationBasedCors.adoc create mode 100644 test-suite-groovy/src/test/groovy/io/micronaut/docs/http/server/cors/CorsController.groovy create mode 100644 test-suite-groovy/src/test/groovy/io/micronaut/docs/http/server/cors/CorsControllerSpec.groovy create mode 100644 test-suite-kotlin/src/test/kotlin/io/micronaut/docs/http/server/cors/CorsController.kt create mode 100644 test-suite-kotlin/src/test/kotlin/io/micronaut/docs/http/server/cors/CorsControllerTest.kt create mode 100644 test-suite/src/test/java/io/micronaut/docs/http/server/cors/CorsController.java create mode 100644 test-suite/src/test/java/io/micronaut/docs/http/server/cors/CorsControllerTest.java diff --git a/context/src/main/java/io/micronaut/scheduling/NextFireTime.java b/context/src/main/java/io/micronaut/scheduling/NextFireTime.java index 6f1d8eea3b2..5735523a136 100644 --- a/context/src/main/java/io/micronaut/scheduling/NextFireTime.java +++ b/context/src/main/java/io/micronaut/scheduling/NextFireTime.java @@ -34,6 +34,7 @@ final class NextFireTime implements Supplier { private Duration duration; private ZonedDateTime nextFireTime; private final CronExpression cron; + private final ZoneId zoneId; /** * Default constructor. @@ -41,8 +42,7 @@ final class NextFireTime implements Supplier { * @param cron A cron expression */ NextFireTime(CronExpression cron) { - this.cron = cron; - nextFireTime = ZonedDateTime.now(); + this(cron, ZoneId.systemDefault()); } /** @@ -51,12 +51,13 @@ final class NextFireTime implements Supplier { */ NextFireTime(CronExpression cron, ZoneId zoneId) { this.cron = cron; + this.zoneId = zoneId; nextFireTime = ZonedDateTime.now(zoneId); } @Override public Duration get() { - ZonedDateTime now = ZonedDateTime.now(); + ZonedDateTime now = ZonedDateTime.now(zoneId); // check if the task have fired too early computeNextFireTime(now.isAfter(nextFireTime) ? now : nextFireTime); return duration; @@ -64,6 +65,6 @@ public Duration get() { private void computeNextFireTime(ZonedDateTime currentFireTime) { nextFireTime = cron.nextTimeAfter(currentFireTime); - duration = Duration.ofMillis(nextFireTime.toInstant().toEpochMilli() - ZonedDateTime.now().toInstant().toEpochMilli()); + duration = Duration.ofMillis(nextFireTime.toInstant().toEpochMilli() - ZonedDateTime.now(zoneId).toInstant().toEpochMilli()); } } diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/ssl/SelfSignedSslBuilder.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/ssl/SelfSignedSslBuilder.java index ae0d1dafd47..6854cfb3a94 100644 --- a/http-server-netty/src/main/java/io/micronaut/http/server/netty/ssl/SelfSignedSslBuilder.java +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/ssl/SelfSignedSslBuilder.java @@ -90,7 +90,8 @@ public Optional build(SslConfiguration ssl, HttpVersion httpVersion) LOG.warn("HTTP Server is configured to use a self-signed certificate ('build-self-signed' is set to true). This configuration should not be used in a production environment as self-signed certificates are inherently insecure."); } SelfSignedCertificate ssc = new SelfSignedCertificate(); - final SslContextBuilder sslBuilder = SslContextBuilder.forServer(ssc.certificate(), ssc.privateKey()); + final SslContextBuilder sslBuilder = SslContextBuilder.forServer(ssc.certificate(), ssc.privateKey()) + .trustManager(getTrustManagerFactory(ssl)); CertificateProvidedSslBuilder.setupSslBuilder(sslBuilder, ssl, httpVersion); return Optional.of(sslBuilder.build()); } catch (CertificateException | SSLException e) { diff --git a/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/cors/CorsFilterSpec.groovy b/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/cors/CorsFilterSpec.groovy index ef97090733b..ea793220e50 100644 --- a/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/cors/CorsFilterSpec.groovy +++ b/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/cors/CorsFilterSpec.groovy @@ -93,13 +93,13 @@ class CorsFilterSpec extends Specification { } @Unroll - void "regex matching configuration"(List regex, String origin) { + void "regex matching configuration"(String regex, String origin) { given: HttpRequest request = createRequest(origin) request.getAttribute(HttpAttributes.ROUTE_MATCH, RouteMatch.class) >> Optional.empty() CorsOriginConfiguration originConfig = new CorsOriginConfiguration() - originConfig.allowedOrigins = regex + originConfig.allowedOriginsRegex = regex HttpServerConfiguration.CorsConfiguration config = enabledCorsConfiguration([foo: originConfig]) CorsFilter corsHandler = buildCorsHandler(config) @@ -124,13 +124,13 @@ class CorsFilterSpec extends Specification { where: regex | origin - ['.*'] | 'http://www.bar.com' - ['^http://www\\.(foo|bar)\\.com$'] | 'http://www.bar.com' - ['^http://www\\.(foo|bar)\\.com$'] | 'http://www.foo.com' - ['.*bar$', '.*foo$'] | 'asdfasdf foo' - ['.*bar$', '.*foo$'] | 'asdfasdf bar' - ['.*bar$', '.*foo$'] | 'http://asdfasdf.foo' - ['.*bar$', '.*foo$'] | 'http://asdfasdf.bar' + '.*' | 'http://www.bar.com' + '^http://www\\.(foo|bar)\\.com$' | 'http://www.bar.com' + '^http://www\\.(foo|bar)\\.com$' | 'http://www.foo.com' + '.*(bar|foo)$' | 'asdfasdf foo' + '.*(bar|foo)$' | 'asdfasdf bar' + '.*(bar|foo)$' | 'http://asdfasdf.foo' + '.*(bar|foo)$' | 'http://asdfasdf.bar' } void "test handleRequest with disallowed method"() { diff --git a/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/cors/CrossOriginUtilSpec.groovy b/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/cors/CrossOriginUtilSpec.groovy new file mode 100644 index 00000000000..0dffce3aee1 --- /dev/null +++ b/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/cors/CrossOriginUtilSpec.groovy @@ -0,0 +1,178 @@ +package io.micronaut.http.server.netty.cors + +import io.micronaut.context.ApplicationContext +import io.micronaut.context.annotation.Requires +import io.micronaut.core.util.CollectionUtils +import io.micronaut.http.HttpHeaders +import io.micronaut.http.HttpMethod +import io.micronaut.http.HttpRequest +import io.micronaut.http.MediaType +import io.micronaut.http.annotation.Controller +import io.micronaut.http.annotation.Get +import io.micronaut.http.annotation.Produces +import io.micronaut.http.client.HttpClient +import io.micronaut.http.server.cors.CorsOriginConfiguration +import io.micronaut.http.server.cors.CrossOrigin +import io.micronaut.http.server.cors.CrossOriginUtil +import io.micronaut.runtime.server.EmbeddedServer +import spock.lang.AutoCleanup +import spock.lang.Shared +import spock.lang.Specification + +class CrossOriginUtilSpec extends Specification { + + private static final String SPECNAME = "CrossOriginUtilSpec" + + @Shared + @AutoCleanup + EmbeddedServer embeddedServer = ApplicationContext.run(EmbeddedServer, ["spec.name": SPECNAME]) + + @Shared + @AutoCleanup + HttpClient client = embeddedServer.applicationContext.createBean(HttpClient, embeddedServer.URL) + + void "test CrossOrigin on method annotation maps to CorsOriginConfiguration"() { + when: + HttpRequest req = HttpRequest.GET("/methodexample").accept(MediaType.TEXT_PLAIN) + client.toBlocking().retrieve(req, String.class) + CorsOriginConfiguration config = embeddedServer.applicationContext.getBean(TestMethodController).config + + then: + config + config.allowedOrigins == [ "https://foo.com" ] + !config.allowedOriginsRegex.isPresent() + config.allowedHeaders == [ HttpHeaders.CONTENT_TYPE, HttpHeaders.AUTHORIZATION ] + config.exposedHeaders == [ HttpHeaders.CONTENT_TYPE, HttpHeaders.AUTHORIZATION ] + config.allowedMethods == [ HttpMethod.GET, HttpMethod.POST ] + !config.allowCredentials + config.maxAge == -1L + + cleanup: + embeddedServer.applicationContext.getBean(TestMethodController).config = null + } + + void "test CrossOrigin with value on method annotation maps to CorsOriginConfiguration allowedOrigin"() { + when: + HttpRequest req = HttpRequest.GET("/anothermethod").accept(MediaType.TEXT_PLAIN) + client.toBlocking().retrieve(req, String.class) + CorsOriginConfiguration config = embeddedServer.applicationContext.getBean(TestMethodController).config + + then: + config + config.allowedOrigins == [ "https://foo.com" ] + !config.allowedOriginsRegex + config.allowedHeaders == CorsOriginConfiguration.ANY + CollectionUtils.isEmpty(config.exposedHeaders) + config.allowedMethods == CorsOriginConfiguration.ANY_METHOD + config.allowCredentials + config.maxAge == 1800L + + cleanup: + embeddedServer.applicationContext.getBean(TestMethodController).config = null + } + + void "test CrossOrigin on class annotation maps to CorsOriginConfiguration"() { + when: + HttpRequest req = HttpRequest.GET("/classexample").accept(MediaType.TEXT_PLAIN) + client.toBlocking().retrieve(req, String.class) + CorsOriginConfiguration config = embeddedServer.applicationContext.getBean(TestClassController).config + + then: + config + config.allowedOrigins == [ "https://bar.com" ] + config.allowedHeaders == [ HttpHeaders.CONTENT_TYPE, HttpHeaders.AUTHORIZATION ] + config.exposedHeaders == [ HttpHeaders.CONTENT_TYPE, HttpHeaders.AUTHORIZATION ] + config.allowedMethods == [ HttpMethod.GET, HttpMethod.POST, HttpMethod.DELETE ] + !config.allowCredentials + config.maxAge == 3600L + + cleanup: + embeddedServer.applicationContext.getBean(TestMethodController).config = null + } + + void "test CrossOrigin with value on class annotation maps to CorsOriginConfiguration allowedOrigin"() { + when: + HttpRequest req = HttpRequest.GET("/anotherclass").accept(MediaType.TEXT_PLAIN) + client.toBlocking().retrieve(req, String.class) + CorsOriginConfiguration config = embeddedServer.applicationContext.getBean(TestAnotherClassController).config + + then: + config + config.allowedOrigins == [ "https://bar.com" ] + config.allowedHeaders == CorsOriginConfiguration.ANY + CollectionUtils.isEmpty(config.exposedHeaders) + config.allowedMethods == CorsOriginConfiguration.ANY_METHOD + config.allowCredentials + config.maxAge == 1800L + + cleanup: + embeddedServer.applicationContext.getBean(TestMethodController).config = null + } + + @Requires(property = 'spec.name', value = SPECNAME) + @Controller + static class TestMethodController { + CorsOriginConfiguration config + + @CrossOrigin( + allowedOrigins = "https://foo.com", + allowedHeaders = [ HttpHeaders.CONTENT_TYPE, HttpHeaders.AUTHORIZATION ], + exposedHeaders = [ HttpHeaders.CONTENT_TYPE, HttpHeaders.AUTHORIZATION ], + allowedMethods = [ HttpMethod.GET, HttpMethod.POST ], + allowCredentials = false, + maxAge = -1L + ) + @Produces(MediaType.TEXT_PLAIN) + @Get("/methodexample") + String method(HttpRequest req) { + this.config = CrossOriginUtil.getCorsOriginConfigurationForRequest(req).orElse(null) + return "method" + } + + @CrossOrigin( + "https://foo.com" + // allowedOriginsRegex = false - is the default + ) + @Produces(MediaType.TEXT_PLAIN) + @Get("/anothermethod") + String example(HttpRequest req) { + this.config = CrossOriginUtil.getCorsOriginConfigurationForRequest(req).orElse(null) + return "anothermethod" + } + } + + @Requires(property = 'spec.name', value = SPECNAME) + @Controller + @CrossOrigin( + allowedOrigins = "https://bar.com", + allowedHeaders = [ HttpHeaders.CONTENT_TYPE, HttpHeaders.AUTHORIZATION ], + exposedHeaders = [ HttpHeaders.CONTENT_TYPE, HttpHeaders.AUTHORIZATION ], + allowedMethods = [ HttpMethod.GET, HttpMethod.POST, HttpMethod.DELETE ], + allowCredentials = false, + maxAge = 3600L + ) + static class TestClassController{ + CorsOriginConfiguration config + + @Produces(MediaType.TEXT_PLAIN) + @Get("/classexample") + String example(HttpRequest req) { + this.config = CrossOriginUtil.getCorsOriginConfigurationForRequest(req).orElse(null) + return "class" + } + } + + @Requires(property = 'spec.name', value = SPECNAME) + @Controller + @CrossOrigin("https://bar.com") + static class TestAnotherClassController{ + CorsOriginConfiguration config + + @Produces(MediaType.TEXT_PLAIN) + @Get("/anotherclass") + String example(HttpRequest req) { + this.config = CrossOriginUtil.getCorsOriginConfigurationForRequest(req).orElse(null) + return "class" + } + } +} diff --git a/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/ssl/RequestCertificateSpec2.groovy b/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/ssl/RequestCertificateSpec2.groovy new file mode 100644 index 00000000000..5bfccea6154 --- /dev/null +++ b/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/ssl/RequestCertificateSpec2.groovy @@ -0,0 +1,206 @@ +package io.micronaut.http.server.netty.ssl + +import io.micronaut.context.ApplicationContext +import io.micronaut.context.annotation.Requires +import io.micronaut.http.HttpRequest +import io.micronaut.http.annotation.Controller +import io.micronaut.http.annotation.Get +import io.micronaut.runtime.server.EmbeddedServer +import io.netty.handler.ssl.util.SelfSignedCertificate +import io.vertx.core.Vertx +import io.vertx.core.net.JksOptions +import io.vertx.ext.web.client.HttpResponse +import io.vertx.ext.web.client.WebClient +import io.vertx.ext.web.client.WebClientOptions +import spock.lang.Specification + +import javax.net.ssl.SSLHandshakeException +import java.nio.file.Files +import java.nio.file.Path +import java.security.KeyStore +import java.security.cert.Certificate +import java.security.cert.X509Certificate +import java.time.Instant +import java.time.temporal.ChronoUnit +import java.util.concurrent.CompletableFuture +import java.util.concurrent.ExecutionException + +class RequestCertificateSpec2 extends Specification { + def normal() { + given: + def certificate = new SelfSignedCertificate() + + def keyStorePath = Files.createTempFile("micronaut-test-key-store", "pkcs12") + def trustStorePath = Files.createTempFile("micronaut-test-trust-store", "pkcs12") + + writeStores(certificate, keyStorePath, trustStorePath) + + def ctx = ApplicationContext.run([ + "spec.name" : "RequestCertificateSpec2", + "micronaut.http.client.read-timeout" : "15s", + 'micronaut.server.ssl.enabled' : true, + 'micronaut.server.ssl.port' : -1, + 'micronaut.server.ssl.buildSelfSigned': true, + 'micronaut.ssl.clientAuthentication' : "need", + 'micronaut.ssl.trust-store.path' : 'file://' + trustStorePath.toString(), + 'micronaut.ssl.trust-store.type' : 'JKS', + 'micronaut.ssl.trust-store.password' : '123456', + ]) + + def server = ctx.getBean(EmbeddedServer) + server.start() + + def vertx = Vertx.vertx() + def client = WebClient.create(vertx, new WebClientOptions() + .setTrustAll(true) + .setSsl(true) + .setKeyCertOptions(new JksOptions().setPath(keyStorePath.toString()).setPassword("")) + .setTrustStoreOptions(new JksOptions().setPath(trustStorePath.toString()).setPassword("123456")) + ) + + when: + def future = new CompletableFuture>() + client.get(server.port, "localhost", "/mtls").send { if (it.failed()) future.completeExceptionally(it.cause()) else future.complete(it.result()) } + def response = future.get() + then: + response.bodyAsString() == 'CN=localhost' + response.statusCode() == 200 + + cleanup: + vertx.close() + ctx.close() + Files.deleteIfExists(keyStorePath) + Files.deleteIfExists(trustStorePath) + } + + def expired() { + // this is intended behavior: an expired client cert DOES NOT lead to a handshake failure. This is JDK behavior: + // when a cert is directly in the trust store, expiry is not checked. expiry is only checked if the cert is + // signed by a CA that is in the trust store. + + given: + def certificate = new SelfSignedCertificate(Date.from(Instant.now().minus(5, ChronoUnit.HOURS)), Date.from(Instant.now().minus(1, ChronoUnit.HOURS))) + + def keyStorePath = Files.createTempFile("micronaut-test-key-store", "pkcs12") + def trustStorePath = Files.createTempFile("micronaut-test-trust-store", "pkcs12") + + writeStores(certificate, keyStorePath, trustStorePath) + + def ctx = ApplicationContext.run([ + "spec.name" : "RequestCertificateSpec2", + "micronaut.http.client.read-timeout" : "15s", + 'micronaut.server.ssl.enabled' : true, + 'micronaut.server.ssl.port' : -1, + 'micronaut.server.ssl.buildSelfSigned': true, + 'micronaut.ssl.clientAuthentication' : "need", + 'micronaut.ssl.trust-store.path' : 'file://' + trustStorePath.toString(), + 'micronaut.ssl.trust-store.type' : 'JKS', + 'micronaut.ssl.trust-store.password' : '123456', + ]) + + def server = ctx.getBean(EmbeddedServer) + server.start() + + def vertx = Vertx.vertx() + def client = WebClient.create(vertx, new WebClientOptions() + .setTrustAll(true) + .setSsl(true) + .setKeyCertOptions(new JksOptions().setPath(keyStorePath.toString()).setPassword("")) + .setTrustStoreOptions(new JksOptions().setPath(trustStorePath.toString()).setPassword("123456")) + ) + + when: + def future = new CompletableFuture>() + client.get(server.port, "localhost", "/mtls").send { if (it.failed()) future.completeExceptionally(it.cause()) else future.complete(it.result()) } + def response = future.get() + then: + response.bodyAsString() == 'CN=localhost' + response.statusCode() == 200 + + cleanup: + vertx.close() + ctx.close() + Files.deleteIfExists(keyStorePath) + Files.deleteIfExists(trustStorePath) + } + + def untrusted() { + given: + def clientCert = new SelfSignedCertificate() + // for the client to send the cert, we still need the same CN in the trust store + def serverExpectsCert = new SelfSignedCertificate() + + def keyStorePath = Files.createTempFile("micronaut-test-key-store", "pkcs12") + def trustStorePath = Files.createTempFile("micronaut-test-trust-store", "pkcs12") + + writeStores(clientCert, keyStorePath, null) + writeStores(serverExpectsCert, null, trustStorePath) + + def ctx = ApplicationContext.run([ + "spec.name" : "RequestCertificateSpec2", + "micronaut.http.client.read-timeout" : "15s", + 'micronaut.server.ssl.enabled' : true, + 'micronaut.server.ssl.port' : -1, + 'micronaut.server.ssl.buildSelfSigned': true, + 'micronaut.ssl.clientAuthentication' : "need", + 'micronaut.ssl.trust-store.path' : 'file://' + trustStorePath.toString(), + 'micronaut.ssl.trust-store.type' : 'JKS', + 'micronaut.ssl.trust-store.password' : '123456', + ]) + + def server = ctx.getBean(EmbeddedServer) + server.start() + + def vertx = Vertx.vertx() + def client = WebClient.create(vertx, new WebClientOptions() + .setTrustAll(true) + .setSsl(true) + .setKeyCertOptions(new JksOptions().setPath(keyStorePath.toString()).setPassword("")) + .setTrustStoreOptions(new JksOptions().setPath(trustStorePath.toString()).setPassword("123456")) + ) + + when: + def future = new CompletableFuture>() + client.get(server.port, "localhost", "/mtls").send { if (it.failed()) future.completeExceptionally(it.cause()) else future.complete(it.result()) } + def response = future.get() + then: + def e = thrown ExecutionException + e.cause instanceof SSLHandshakeException + + cleanup: + vertx.close() + ctx.close() + Files.deleteIfExists(keyStorePath) + Files.deleteIfExists(trustStorePath) + } + + private void writeStores(SelfSignedCertificate certificate, Path keyStorePath, Path trustStorePath) { + if (keyStorePath != null) { + KeyStore ks = KeyStore.getInstance("PKCS12") + ks.load(null, null) + ks.setKeyEntry("key", certificate.key(), "".toCharArray(), new Certificate[]{certificate.cert()}) + try (OutputStream os = Files.newOutputStream(keyStorePath)) { + ks.store(os, "".toCharArray()) + } + } + + if (trustStorePath != null) { + KeyStore ts = KeyStore.getInstance("JKS") + ts.load(null, null) + ts.setCertificateEntry("cert", certificate.cert()) + try (OutputStream os = Files.newOutputStream(trustStorePath)) { + ts.store(os, "123456".toCharArray()) + } + } + } + + @Controller + @Requires(property = "spec.name", value = "RequestCertificateSpec2") + static class TestController { + @Get('/mtls') + String name(HttpRequest request) { + def cert = request.getCertificate().get() as X509Certificate + cert.issuerX500Principal.name + } + } +} diff --git a/http-server-tck/src/main/java/io/micronaut/http/server/tck/CorsAssertion.java b/http-server-tck/src/main/java/io/micronaut/http/server/tck/CorsAssertion.java new file mode 100644 index 00000000000..4fd7c06ea71 --- /dev/null +++ b/http-server-tck/src/main/java/io/micronaut/http/server/tck/CorsAssertion.java @@ -0,0 +1,182 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.http.server.tck; + +import io.micronaut.core.util.CollectionUtils; +import io.micronaut.core.util.StringUtils; +import io.micronaut.http.HttpHeaders; +import io.micronaut.http.HttpMethod; +import io.micronaut.http.HttpResponse; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * CORS assertion. + * @author Sergio del Amo + * @since 3.9.0 + */ +public final class CorsAssertion { + + private final String vary; + private final String accessControlAllowCredentials; + + private final String origin; + + private final List allowMethods; + + private final String maxAge; + + private CorsAssertion(String vary, + String accessControlAllowCredentials, + String origin, + List allowMethods, + String maxAge) { + this.vary = vary; + this.accessControlAllowCredentials = accessControlAllowCredentials; + this.origin = origin; + this.allowMethods = allowMethods; + this.maxAge = maxAge; + } + + /** + * Validate the CORS assertions. + * @param response HTTP Response to run CORS assertions against it. + */ + public void validate(HttpResponse response) { + if (StringUtils.isNotEmpty(vary)) { + assertEquals(vary, response.getHeaders().get(HttpHeaders.VARY)); + } + if (StringUtils.isNotEmpty(accessControlAllowCredentials)) { + assertEquals(accessControlAllowCredentials, response.getHeaders().get(HttpHeaders.ACCESS_CONTROL_ALLOW_CREDENTIALS)); + } + if (StringUtils.isNotEmpty(origin)) { + assertEquals(origin, response.getHeaders().get(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN)); + } + if (CollectionUtils.isNotEmpty(allowMethods)) { + assertEquals(allowMethods.stream().map(HttpMethod::toString).collect(Collectors.joining(",")), + response.getHeaders().get(HttpHeaders.ACCESS_CONTROL_ALLOW_METHODS)); + } + if (StringUtils.isNotEmpty(maxAge)) { + assertEquals(maxAge, response.getHeaders().get(HttpHeaders.ACCESS_CONTROL_MAX_AGE)); + } + } + + /** + * + * @return a CORS Assertion Builder. + */ + public static Builder builder() { + return new Builder(); + } + + /** + * CORS Assertion Builder. + */ + public static class Builder { + private String vary; + private String accessControlAllowCredentials; + + private String origin; + + private List allowMethods; + + private String maxAge; + + /** + * + * @param varyValue The expected value for the HTTP Header {@value HttpHeaders#VARY}. + * @return The Builder + */ + public Builder vary(String varyValue) { + this.vary = varyValue; + return this; + } + + /** + * + * @param accessControlAllowCredentials The expected value for the HTTP Header {@value HttpHeaders#ACCESS_CONTROL_ALLOW_CREDENTIALS}. + * @return The Builder + */ + public Builder allowCredentials(String accessControlAllowCredentials) { + this.accessControlAllowCredentials = accessControlAllowCredentials; + return this; + } + + /** + * + * Set expectation of value for the HTTP Header {@value HttpHeaders#ACCESS_CONTROL_ALLOW_CREDENTIALS} to {@value StringUtils#TRUE}. + * @return The Builder + */ + public Builder allowCredentials() { + return allowCredentials(StringUtils.TRUE); + } + + /** + * + * Set expectation of value for the HTTP Header {@value HttpHeaders#ACCESS_CONTROL_ALLOW_CREDENTIALS} to {@value StringUtils#TRUE}. + * @param allowCredentials Set expectation of value for the HTTP Header {@value HttpHeaders#ACCESS_CONTROL_ALLOW_CREDENTIALS} + * @return The Builder + */ + public Builder allowCredentials(boolean allowCredentials) { + return allowCredentials ? allowCredentials(StringUtils.TRUE) : allowCredentials(""); + } + + /** + * + * @param origin The expected value for the HTTP Header {@value HttpHeaders#ACCESS_CONTROL_ALLOW_ORIGIN}. + * @return The Builder + */ + public Builder allowOrigin(String origin) { + this.origin = origin; + return this; + } + + /** + * + * @param method The expected value for the HTTP Header {@value HttpHeaders#ACCESS_CONTROL_ALLOW_METHODS}. + * @return The Builder + */ + public Builder allowMethods(HttpMethod method) { + if (allowMethods == null) { + this.allowMethods = new ArrayList<>(); + } + this.allowMethods.add(method); + return this; + } + + /** + * + * @param maxAge The expected value for the HTTP Header {@value HttpHeaders#ACCESS_CONTROL_MAX_AGE}. + * @return The Builder + */ + public Builder maxAge(String maxAge) { + this.maxAge = maxAge; + return this; + } + + /** + * + * @return A CORS assertion. + */ + public CorsAssertion build() { + return new CorsAssertion(vary, accessControlAllowCredentials, origin, allowMethods, maxAge); + } + } +} diff --git a/http-server-tck/src/main/java/io/micronaut/http/server/tck/CorsUtils.java b/http-server-tck/src/main/java/io/micronaut/http/server/tck/CorsUtils.java new file mode 100644 index 00000000000..b689b370eb5 --- /dev/null +++ b/http-server-tck/src/main/java/io/micronaut/http/server/tck/CorsUtils.java @@ -0,0 +1,95 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.http.server.tck; + +import io.micronaut.http.HttpHeaders; +import io.micronaut.http.HttpMethod; +import io.micronaut.http.HttpResponse; + +import static org.junit.jupiter.api.Assertions.assertFalse; + +/** + * Utility class to do CORS related assertions. + * @author Sergio del Amo + * @since 3.9.0 + */ +public final class CorsUtils { + private CorsUtils() { + + } + + /** + * @param response HTTP Response to run CORS assertions against it. + */ + public static void assertCorsHeadersNotPresent(HttpResponse response) { + assertFalse(response.getHeaders().names().contains(HttpHeaders.VARY)); + assertFalse(response.getHeaders().names().contains(HttpHeaders.ACCESS_CONTROL_ALLOW_CREDENTIALS)); + assertFalse(response.getHeaders().names().contains(HttpHeaders.ACCESS_CONTROL_MAX_AGE)); + assertFalse(response.getHeaders().names().contains(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN)); + assertFalse(response.getHeaders().names().contains(HttpHeaders.ACCESS_CONTROL_ALLOW_METHODS)); + assertFalse(response.getHeaders().names().contains(HttpHeaders.ACCESS_CONTROL_ALLOW_HEADERS)); + } + + /** + * @param response HTTP Response to run CORS assertions against it. + * @param origin The expected value for the HTTP Header {@value HttpHeaders#ACCESS_CONTROL_ALLOW_ORIGIN}. + * @param method The expected value for the HTTP Header {@value HttpHeaders#ACCESS_CONTROL_ALLOW_METHODS}. + */ + public static void assertCorsHeaders(HttpResponse response, String origin, HttpMethod method) { + CorsAssertion.builder() + .vary("Origin") + .allowCredentials() + .allowOrigin(origin) + .allowMethods(method) + .maxAge("1800") + .build() + .validate(response); + } + + /** + * @param response HTTP Response to run CORS assertions against it. + * @param origin The expected value for the HTTP Header {@value HttpHeaders#ACCESS_CONTROL_ALLOW_ORIGIN}. + * @param method The expected value for the HTTP Header {@value HttpHeaders#ACCESS_CONTROL_ALLOW_METHODS}. + * @param allowCredentials The expected value for the HTTP Header {@value HttpHeaders#ACCESS_CONTROL_ALLOW_CREDENTIALS}. + */ + public static void assertCorsHeaders(HttpResponse response, String origin, HttpMethod method, boolean allowCredentials) { + CorsAssertion.builder() + .vary("Origin") + .allowCredentials(allowCredentials) + .allowOrigin(origin) + .allowMethods(method) + .maxAge("1800") + .build() + .validate(response); + } + + /** + * @param response HTTP Response to run CORS assertions against it. + * @param origin The expected value for the HTTP Header {@value HttpHeaders#ACCESS_CONTROL_ALLOW_ORIGIN}. + * @param method The expected value for the HTTP Header {@value HttpHeaders#ACCESS_CONTROL_ALLOW_METHODS}. + * @param maxAge The expected value for the HTTP Header {@value HttpHeaders#ACCESS_CONTROL_MAX_AGE}. + */ + public static void assertCorsHeaders(HttpResponse response, String origin, HttpMethod method, String maxAge) { + CorsAssertion.builder() + .vary("Origin") + .allowCredentials() + .allowOrigin(origin) + .allowMethods(method) + .maxAge(maxAge) + .build() + .validate(response); + } +} diff --git a/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/HeadersTest.java b/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/HeadersTest.java index 1900e732085..eb087256105 100644 --- a/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/HeadersTest.java +++ b/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/HeadersTest.java @@ -40,7 +40,7 @@ public class HeadersTest { public static final String SPEC_NAME = "HeadersTest"; /** - * Message header field names are case-insensitive + * Message header field names are case-insensitive. * * @see HTTP/1.1 Message Headers */ diff --git a/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/cors/CorsDisabledByDefaultTest.java b/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/cors/CorsDisabledByDefaultTest.java index f09933ae89b..aa4a1406c42 100644 --- a/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/cors/CorsDisabledByDefaultTest.java +++ b/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/cors/CorsDisabledByDefaultTest.java @@ -24,17 +24,16 @@ import io.micronaut.http.annotation.Controller; import io.micronaut.http.annotation.Post; import io.micronaut.http.annotation.Status; +import io.micronaut.http.server.tck.CorsUtils; import io.micronaut.http.server.util.HttpHostResolver; -import io.micronaut.http.tck.AssertionUtils; -import io.micronaut.http.tck.HttpResponseAssertion; import jakarta.inject.Singleton; import org.junit.jupiter.api.Test; - +import static io.micronaut.http.tck.TestScenario.asserts; +import io.micronaut.http.tck.AssertionUtils; +import io.micronaut.http.tck.HttpResponseAssertion; import java.io.IOException; import java.util.Collections; -import static io.micronaut.http.tck.TestScenario.asserts; -import static org.junit.jupiter.api.Assertions.assertNull; @SuppressWarnings({ "java:S2259", // The tests will show if it's null @@ -56,14 +55,7 @@ void corsDisabledByDefault() throws IOException { (server, request) -> { AssertionUtils.assertDoesNotThrow(server, request, HttpResponseAssertion.builder() .status(HttpStatus.OK) - .assertResponse(response -> { - assertNull(response.getHeaders().get("Access-Control-Allow-Origin")); - assertNull(response.getHeaders().get("Vary")); - assertNull(response.getHeaders().get("Access-Control-Allow-Credentials")); - assertNull(response.getHeaders().get("Access-Control-Allow-Methods")); - assertNull(response.getHeaders().get("Access-Control-Allow-Headers")); - assertNull(response.getHeaders().get("Access-Control-Max-Age")); - }) + .assertResponse(CorsUtils::assertCorsHeadersNotPresent) .build()); }); } diff --git a/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/cors/CorsSimpleRequestTest.java b/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/cors/CorsSimpleRequestTest.java index db8de4dd07a..8f6a8be051d 100644 --- a/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/cors/CorsSimpleRequestTest.java +++ b/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/cors/CorsSimpleRequestTest.java @@ -28,21 +28,22 @@ import io.micronaut.http.annotation.Post; import io.micronaut.http.annotation.Status; import io.micronaut.http.client.multipart.MultipartBody; -import io.micronaut.http.tck.AssertionUtils; -import io.micronaut.http.tck.HttpResponseAssertion; -import io.micronaut.http.tck.RequestSupplier; -import io.micronaut.http.tck.ServerUnderTest; +import io.micronaut.http.server.tck.CorsAssertion; import io.micronaut.runtime.context.scope.refresh.RefreshEvent; import jakarta.inject.Inject; import jakarta.inject.Singleton; import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; - +import io.micronaut.http.tck.ServerUnderTest; +import io.micronaut.http.tck.RequestSupplier; import java.io.IOException; import java.util.Collections; +import io.micronaut.http.tck.AssertionUtils; +import io.micronaut.http.tck.HttpResponseAssertion; import static io.micronaut.http.tck.TestScenario.asserts; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; @SuppressWarnings({ "java:S2259", // The tests will show if it's null @@ -199,14 +200,12 @@ void corsSimpleRequestForLocalhostCanBeAllowedViaConfiguration() throws IOExcept AssertionUtils.assertDoesNotThrow(server, request, HttpResponseAssertion.builder() .status(HttpStatus.OK) - .assertResponse(response -> { - assertNotNull(response.getHeaders().get("Access-Control-Allow-Origin")); - assertNotNull(response.getHeaders().get("Vary")); - assertNotNull(response.getHeaders().get("Access-Control-Allow-Credentials")); - assertNull(response.getHeaders().get("Access-Control-Allow-Methods")); - assertNull(response.getHeaders().get("Access-Control-Allow-Headers")); - assertNull(response.getHeaders().get("Access-Control-Max-Age")); - }) + .assertResponse(response -> CorsAssertion.builder() + .vary("Origin") + .allowCredentials() + .allowOrigin("https://foo.com") + .build() + .validate(response)) .build()); assertEquals(1, refreshCounter.getRefreshCount()); }); diff --git a/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/cors/CrossOriginTest.java b/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/cors/CrossOriginTest.java new file mode 100644 index 00000000000..247e4c93693 --- /dev/null +++ b/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/cors/CrossOriginTest.java @@ -0,0 +1,448 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.http.server.tck.tests.cors; + +import io.micronaut.context.annotation.Replaces; +import io.micronaut.context.annotation.Requires; +import io.micronaut.core.annotation.Nullable; +import io.micronaut.core.util.CollectionUtils; +import io.micronaut.core.util.StringUtils; +import io.micronaut.http.HttpHeaders; +import io.micronaut.http.HttpMethod; +import io.micronaut.http.HttpRequest; +import io.micronaut.http.HttpStatus; +import io.micronaut.http.MediaType; +import io.micronaut.http.MutableHttpRequest; +import io.micronaut.http.annotation.Controller; +import io.micronaut.http.annotation.Delete; +import io.micronaut.http.annotation.Get; +import io.micronaut.http.annotation.PathVariable; +import io.micronaut.http.annotation.Post; +import io.micronaut.http.annotation.Produces; +import io.micronaut.http.server.cors.CrossOrigin; +import io.micronaut.http.server.tck.CorsUtils; +import io.micronaut.http.server.util.HttpHostResolver; +import io.micronaut.http.uri.UriBuilder; +import jakarta.inject.Singleton; +import org.junit.jupiter.api.Test; +import io.micronaut.http.tck.ServerUnderTest; +import io.micronaut.http.tck.AssertionUtils; +import io.micronaut.http.tck.HttpResponseAssertion; + +import java.io.IOException; +import java.net.URI; +import java.util.function.BiConsumer; + +import static io.micronaut.http.tck.TestScenario.asserts; +import static io.micronaut.http.server.tck.CorsUtils.*; +import static org.junit.jupiter.api.Assertions.assertAll; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@SuppressWarnings({ + "java:S2259", // The tests will show if it's null + "java:S5960", // We're allowed assertions, as these are used in tests only + "checkstyle:MissingJavadocType", + "checkstyle:DesignForExtension" +}) +public class CrossOriginTest { + + private static final String SPECNAME = "CrossOriginTest"; + + @Test + void crossOriginAnnotationWithMatchingOrigin() throws IOException { + asserts(SPECNAME, + preflight(UriBuilder.of("/foo").path("bar"), "https://foo.com", HttpMethod.GET), + (server, request) -> AssertionUtils.assertDoesNotThrow(server, request, HttpResponseAssertion.builder() + .status(HttpStatus.OK) + .assertResponse(response -> { + assertCorsHeaders(response, "https://foo.com", HttpMethod.GET); + assertFalse(response.getHeaders().names().contains(HttpHeaders.ACCESS_CONTROL_ALLOW_HEADERS)); + }) + .build())); + } + + @Test + void crossOriginAnnotationWithNoMatchingOrigin() throws IOException { + asserts(SPECNAME, + preflight(UriBuilder.of("/foo").path("bar"), "https://bar.com", HttpMethod.GET), + (server, request) -> AssertionUtils.assertThrows(server, request, HttpResponseAssertion.builder() + .status(HttpStatus.METHOD_NOT_ALLOWED) + .assertResponse(CorsUtils::assertCorsHeadersNotPresent) + .build())); + } + + @Test + void verifyHttpMethodIsValidatedInACorsRequest() { + assertAll( + () -> asserts(SPECNAME, + preflight(UriBuilder.of("/methods").path("getit"), "https://www.google.com", HttpMethod.GET), + (server, request) -> AssertionUtils.assertDoesNotThrow(server, request, HttpResponseAssertion.builder() + .status(HttpStatus.OK) + .assertResponse(response -> { + assertCorsHeaders(response, "https://www.google.com", HttpMethod.GET); + assertFalse(response.getHeaders().names().contains(HttpHeaders.ACCESS_CONTROL_ALLOW_HEADERS)); + }) + .build())), + () -> asserts(SPECNAME, + preflight(UriBuilder.of("/methods").path("postit").path("id"), "https://www.google.com", HttpMethod.POST), + (server, request) -> AssertionUtils.assertDoesNotThrow(server, request, HttpResponseAssertion.builder() + .status(HttpStatus.OK) + .assertResponse(response -> { + assertCorsHeaders(response, "https://www.google.com", HttpMethod.POST); + assertFalse(response.getHeaders().names().contains(HttpHeaders.ACCESS_CONTROL_ALLOW_HEADERS)); + }) + .build())), + () -> asserts(SPECNAME, + preflight(UriBuilder.of("/methods").path("deleteit").path("id"), "https://www.google.com", HttpMethod.DELETE), + (server, request) -> AssertionUtils.assertThrows(server, request, HttpResponseAssertion.builder() + .status(HttpStatus.FORBIDDEN) + .assertResponse(CorsUtils::assertCorsHeadersNotPresent) + .build())) + ); + } + + @Test + void allowedOriginsRegexHappyPath() throws IOException { + URI uri = UriBuilder.of("/allowedoriginsregex").path("foo").build(); + String origin = "https://foo.com"; + asserts(SPECNAME, preflight(uri, origin, HttpMethod.GET), happyPathAssertion(origin)); + origin = "http://foo.com"; + asserts(SPECNAME, preflight(uri, origin, HttpMethod.GET), happyPathAssertion(origin)); + } + + private static BiConsumer> happyPathAssertion(String origin) { + return (server, request) -> AssertionUtils.assertDoesNotThrow(server, request, HttpResponseAssertion.builder() + .status(HttpStatus.OK) + .assertResponse(response -> { + assertCorsHeaders(response, origin, HttpMethod.GET); + assertFalse(response.getHeaders().names().contains(HttpHeaders.ACCESS_CONTROL_ALLOW_HEADERS)); + }) + .build()); + } + + @Test + void allowedOriginsRegexFailure() throws IOException { + asserts(SPECNAME, + preflight(UriBuilder.of("/allowedoriginsregex").path("bar"), "https://foo.com", HttpMethod.GET), + (server, request) -> AssertionUtils.assertThrows(server, request, HttpResponseAssertion.builder() + .status(HttpStatus.METHOD_NOT_ALLOWED) + .assertResponse(CorsUtils::assertCorsHeadersNotPresent) + .build())); + } + + @Test + void allowedHeadersHappyPath() throws IOException { + asserts(SPECNAME, + preflight(UriBuilder.of("/allowedheaders").path("bar"), "https://foo.com", HttpMethod.GET) + .header(HttpHeaders.ACCESS_CONTROL_REQUEST_HEADERS, HttpHeaders.AUTHORIZATION + "," + HttpHeaders.CONTENT_TYPE), + (server, request) -> AssertionUtils.assertDoesNotThrow(server, request, HttpResponseAssertion.builder() + .status(HttpStatus.OK) + .assertResponse(response -> { + assertCorsHeaders(response, "https://foo.com", HttpMethod.GET); + assertTrue(response.getHeaders().names().contains(HttpHeaders.ACCESS_CONTROL_ALLOW_HEADERS)); + }) + .build())); + } + + /** + * Access-Control-Allow-Headers + * The Access-Control-Allow-Headers response header is used in response to a preflight request which includes the Access-Control-Request-Headers to indicate which HTTP headers can be used during the actual request. + */ + @Test + void allowedHeadersFailure() throws IOException { + asserts(SPECNAME, + preflight(UriBuilder.of("/allowedheaders").path("bar"), "https://foo.com", HttpMethod.GET) + .header(HttpHeaders.ACCESS_CONTROL_REQUEST_HEADERS, "foo"), + (server, request) -> AssertionUtils.assertThrows(server, request, HttpResponseAssertion.builder() + .status(HttpStatus.FORBIDDEN) + .assertResponse(CorsUtils::assertCorsHeadersNotPresent) + .build())); + } + + /** + * The Access-Control-Expose-Headers header adds the specified headers to the allowlist that JavaScript (such as getResponseHeader()) in browsers is allowed to access. + * @see Access-Control-Expose-Headers + */ + @Test + void defaultAccessControlExposeHeaderValueIsNotSet() throws IOException { + asserts(SPECNAME, + preflight(UriBuilder.of("/exposedheaders").path("foo"), "https://foo.com", HttpMethod.GET), + (server, request) -> AssertionUtils.assertDoesNotThrow(server, request, HttpResponseAssertion.builder() + .status(HttpStatus.OK) + .assertResponse(response -> { + assertCorsHeaders(response, "https://foo.com", HttpMethod.GET); + assertFalse(response.getHeaders().names().contains(HttpHeaders.ACCESS_CONTROL_EXPOSE_HEADERS)); + }) + .build())); + } + + /** + * The Access-Control-Expose-Headers header adds the specified headers to the allowlist that JavaScript (such as getResponseHeader()) in browsers is allowed to access. + * @see Access-Control-Expose-Headers + */ + @Test + void httHeaderValueAccessControlExposeHeaderValueCanBeSetViaCrossOriginAnnotation() throws IOException { + asserts(SPECNAME, + CollectionUtils.mapOf("micronaut.server.cors.single-header", StringUtils.TRUE), + preflight(UriBuilder.of("/exposedheaders").path("bar"), "https://foo.com", HttpMethod.GET) + .header(HttpHeaders.ACCESS_CONTROL_EXPOSE_HEADERS, "foo"), + (server, request) -> AssertionUtils.assertDoesNotThrow(server, request, HttpResponseAssertion.builder() + .status(HttpStatus.OK) + .assertResponse(response -> { + assertCorsHeaders(response, "https://foo.com", HttpMethod.GET); + assertTrue(response.getHeaders().names().contains(HttpHeaders.ACCESS_CONTROL_EXPOSE_HEADERS)); + assertEquals("Content-Encoding,Kuma-Revision", response.getHeaders().get(HttpHeaders.ACCESS_CONTROL_EXPOSE_HEADERS)); + }) + .build())); + } + + /** + * The Access-Control-Allow-Credentials response header tells browsers whether to expose the response to the frontend JavaScript code when the request's credentials mode (Request.credentials) is include. + * @see Access-Control-Allow-Credentials + */ + @Test + void defaultAccessControlAllowCredentialsValueIsNotSet() throws IOException { + asserts(SPECNAME, + preflight(UriBuilder.of("/credentials").path("foo"), "https://foo.com", HttpMethod.GET), + (server, request) -> AssertionUtils.assertDoesNotThrow(server, request, HttpResponseAssertion.builder() + .status(HttpStatus.OK) + .assertResponse(response -> { + assertCorsHeaders(response, "https://foo.com", HttpMethod.GET, false); + assertFalse(response.getHeaders().names().contains(HttpHeaders.ACCESS_CONTROL_ALLOW_CREDENTIALS)); + }) + .build())); + } + + /** + * The Access-Control-Allow-Credentials response header tells browsers whether to expose the response to the frontend JavaScript code when the request's credentials mode (Request.credentials) is include. + * @see Access-Control-Allow-Credentials + */ + @Test + void defaultAccessControlAllowCredentialsValueIsSet() throws IOException { + asserts(SPECNAME, + preflight(UriBuilder.of("/credentials").path("bar"), "https://foo.com", HttpMethod.GET), + (server, request) -> AssertionUtils.assertDoesNotThrow(server, request, HttpResponseAssertion.builder() + .status(HttpStatus.OK) + .assertResponse(response -> { + assertCorsHeaders(response, "https://foo.com", HttpMethod.GET); + assertTrue(response.getHeaders().names().contains(HttpHeaders.ACCESS_CONTROL_ALLOW_CREDENTIALS)); + assertEquals("true", response.getHeaders().get(HttpHeaders.ACCESS_CONTROL_ALLOW_CREDENTIALS)); + }) + .build())); + } + + /** + * The Access-Control-Max-Age response header indicates how long the results of a preflight request (that is the information contained in the Access-Control-Allow-Methods and Access-Control-Allow-Headers headers) can be cached. + * @see Access-Control-Max-Age + */ + @Test + void defaultAccessControlMaxAgeValueIsSet() throws IOException { + asserts(SPECNAME, + preflight(UriBuilder.of("/maxage").path("foo"), "https://foo.com", HttpMethod.GET), + (server, request) -> AssertionUtils.assertDoesNotThrow(server, request, HttpResponseAssertion.builder() + .status(HttpStatus.OK) + .assertResponse(response -> { + assertCorsHeaders(response, "https://foo.com", HttpMethod.GET); + assertTrue(response.getHeaders().names().contains(HttpHeaders.ACCESS_CONTROL_MAX_AGE)); + assertEquals("1800", response.getHeaders().get(HttpHeaders.ACCESS_CONTROL_MAX_AGE)); + }) + .build())); + } + + /** + * The Access-Control-Max-Age response header indicates how long the results of a preflight request (that is the information contained in the Access-Control-Allow-Methods and Access-Control-Allow-Headers headers) can be cached. + * @see Access-Control-Max-Age + */ + @Test + void accessControlMaxAgeValueIsSet() throws IOException { + asserts(SPECNAME, + preflight(UriBuilder.of("/maxage").path("bar"), "https://foo.com", HttpMethod.GET), + (server, request) -> AssertionUtils.assertDoesNotThrow(server, request, HttpResponseAssertion.builder() + .status(HttpStatus.OK) + .assertResponse(response -> { + assertCorsHeaders(response, "https://foo.com", HttpMethod.GET, "1000"); + assertTrue(response.getHeaders().names().contains(HttpHeaders.ACCESS_CONTROL_MAX_AGE)); + assertEquals("1000", response.getHeaders().get(HttpHeaders.ACCESS_CONTROL_MAX_AGE)); + }) + .build())); + } + + private static MutableHttpRequest preflight(UriBuilder uriBuilder, String originValue, HttpMethod method) { + return preflight(uriBuilder.build(), originValue, method); + } + + private static MutableHttpRequest preflight(URI uri, String originValue, HttpMethod method) { + return HttpRequest.OPTIONS(uri) + .header(HttpHeaders.ACCEPT, MediaType.TEXT_PLAIN) + .header(HttpHeaders.ORIGIN, originValue) + .header(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, method); + } + + @Requires(property = "spec.name", value = SPECNAME) + @Controller("/foo") + static class Foo { + @CrossOrigin("https://foo.com") + @Produces(MediaType.TEXT_PLAIN) + @Get("/bar") + String index() { + return "bar"; + } + } + + @Requires(property = "spec.name", value = SPECNAME) + @Controller("/allowedoriginsregex") + static class AllowedOriginsRegex { + + @CrossOrigin( + allowedOriginsRegex = "^http(|s):\\/\\/foo\\.com$" + ) + @Produces(MediaType.TEXT_PLAIN) + @Get("/foo") + String foo() { + return "foo"; + } + + @CrossOrigin( + value = "^http(|s):\\/\\/foo\\.com$" + // allowedOriginsRegex defaults to false + ) + @Produces(MediaType.TEXT_PLAIN) + @Get("/bar") + String bar() { + return "bar"; + } + } + + @Requires(property = "spec.name", value = SPECNAME) + @Controller("/methods") + @CrossOrigin( + allowedOrigins = "https://www.google.com", + allowedMethods = { HttpMethod.GET, HttpMethod.POST } + ) + static class AllowedMethods { + @Produces(MediaType.TEXT_PLAIN) + @Get("/getit") + String canGet() { + return "get"; + } + + @Produces(MediaType.TEXT_PLAIN) + @Post("/postit/{id}") + String canPost(@PathVariable String id) { + return id; + } + + @Delete("/deleteit/{id}") + String cantDelete(@PathVariable String id) { + return id; + } + } + + @Requires(property = "spec.name", value = SPECNAME) + @Controller("/allowedheaders") + @CrossOrigin( + value = "https://foo.com", + allowedHeaders = { HttpHeaders.CONTENT_TYPE, HttpHeaders.AUTHORIZATION } + ) + static class AllowedHeaders { + @Produces(MediaType.TEXT_PLAIN) + @Get("/bar") + String index() { + return "bar"; + } + } + + @Requires(property = "spec.name", value = SPECNAME) + @Controller("/exposedheaders") + static class ExposedHeaders { + @CrossOrigin( + value = "https://foo.com", + exposedHeaders = { "Content-Encoding", "Kuma-Revision" } + ) + @Produces(MediaType.TEXT_PLAIN) + @Get("/bar") + String bar() { + return "bar"; + } + + @CrossOrigin( + value = "https://foo.com" + ) + @Produces(MediaType.TEXT_PLAIN) + @Get("/foo") + String foo() { + return "foo"; + } + } + + @Requires(property = "spec.name", value = SPECNAME) + @Controller("/credentials") + static class Credentials { + @CrossOrigin( + value = "https://foo.com", + allowCredentials = false + ) + @Produces(MediaType.TEXT_PLAIN) + @Get("/foo") + String foo() { + return "foo"; + } + + @CrossOrigin( + value = "https://foo.com" + ) + @Produces(MediaType.TEXT_PLAIN) + @Get("/bar") + String bar() { + return "bar"; + } + } + + @Requires(property = "spec.name", value = SPECNAME) + @Controller("/maxage") + static class MaxAge { + @CrossOrigin( + value = "https://foo.com" + ) + @Produces(MediaType.TEXT_PLAIN) + @Get("/foo") + String foo() { + return "foo"; + } + + @CrossOrigin( + value = "https://foo.com", + maxAge = 1000L + ) + @Produces(MediaType.TEXT_PLAIN) + @Get("/bar") + String bar() { + return "bar"; + } + } + + @Requires(property = "spec.name", value = SPECNAME) + @Replaces(HttpHostResolver.class) + @Singleton + static class HttpHostResolverReplacement implements HttpHostResolver { + @Override + public String resolve(@Nullable HttpRequest request) { + return "https://micronautexample.com"; + } + } +} diff --git a/http-server/src/main/java/io/micronaut/http/server/cors/CorsFilter.java b/http-server/src/main/java/io/micronaut/http/server/cors/CorsFilter.java index e8f2304b865..f8f16200434 100644 --- a/http-server/src/main/java/io/micronaut/http/server/cors/CorsFilter.java +++ b/http-server/src/main/java/io/micronaut/http/server/cors/CorsFilter.java @@ -86,7 +86,7 @@ public Publisher> doFilter(HttpRequest request, Server LOG.trace("Http Header " + HttpHeaders.ORIGIN + " not present. Proceeding with the request."); return chain.proceed(request); } - CorsOriginConfiguration corsOriginConfiguration = getConfiguration(origin).orElse(null); + CorsOriginConfiguration corsOriginConfiguration = getConfiguration(request).orElse(null); if (corsOriginConfiguration != null) { if (CorsUtil.isPreflightRequest(request)) { return handlePreflightRequest(request, chain, corsOriginConfiguration); @@ -283,31 +283,45 @@ protected void setMaxAge(long maxAge, MutableHttpResponse response) { } @NonNull - private Optional getConfiguration(@NonNull String requestOrigin) { + private Optional getConfiguration(@NonNull HttpRequest request) { + String requestOrigin = request.getHeaders().getOrigin().orElse(null); + if (requestOrigin == null) { + return Optional.empty(); + } + Optional originConfiguration = CrossOriginUtil.getCorsOriginConfigurationForRequest(request); + if (originConfiguration.isPresent() && matchesOrigin(originConfiguration.get(), requestOrigin)) { + return originConfiguration; + } if (!corsConfiguration.isEnabled()) { return Optional.empty(); } return corsConfiguration.getConfigurations().values().stream() - .filter(config -> { - List allowedOrigins = config.getAllowedOrigins(); - return !allowedOrigins.isEmpty() && (isAny(allowedOrigins) || allowedOrigins.stream().anyMatch(origin -> matchesOrigin(origin, requestOrigin))); - }).findFirst(); + .filter(config -> matchesOrigin(config, requestOrigin)) + .findFirst(); } - private boolean matchesOrigin(@NonNull String origin, @NonNull String requestOrigin) { - if (origin.equals(requestOrigin)) { + private static boolean matchesOrigin(@NonNull CorsOriginConfiguration config, String requestOrigin) { + if (config.getAllowedOriginsRegex().map(regex -> matchesOrigin(regex, requestOrigin)).orElse(false)) { return true; } - Pattern p = Pattern.compile(origin); + List allowedOrigins = config.getAllowedOrigins(); + return !allowedOrigins.isEmpty() && ( + (!config.getAllowedOriginsRegex().isPresent() && isAny(allowedOrigins)) || + allowedOrigins.stream().anyMatch(origin -> origin.equals(requestOrigin)) + ); + } + + private static boolean matchesOrigin(@NonNull String originRegex, @NonNull String requestOrigin) { + Pattern p = Pattern.compile(originRegex); Matcher m = p.matcher(requestOrigin); return m.matches(); } - private boolean isAny(List values) { + private static boolean isAny(List values) { return values == CorsOriginConfiguration.ANY; } - private boolean isAnyMethod(List allowedMethods) { + private static boolean isAnyMethod(List allowedMethods) { return allowedMethods == CorsOriginConfiguration.ANY_METHOD; } diff --git a/http-server/src/main/java/io/micronaut/http/server/cors/CorsOriginConfiguration.java b/http-server/src/main/java/io/micronaut/http/server/cors/CorsOriginConfiguration.java index 8632dc27e90..e0be0f806a4 100644 --- a/http-server/src/main/java/io/micronaut/http/server/cors/CorsOriginConfiguration.java +++ b/http-server/src/main/java/io/micronaut/http/server/cors/CorsOriginConfiguration.java @@ -15,11 +15,13 @@ */ package io.micronaut.http.server.cors; +import io.micronaut.core.annotation.NonNull; import io.micronaut.core.annotation.Nullable; +import io.micronaut.core.util.StringUtils; import io.micronaut.http.HttpMethod; - import java.util.Collections; import java.util.List; +import java.util.Optional; /** * Stores configuration for CORS. @@ -29,7 +31,6 @@ * @since 1.0 */ public class CorsOriginConfiguration { - /** * Constant to represent any value. */ @@ -41,6 +42,7 @@ public class CorsOriginConfiguration { public static final List ANY_METHOD = Collections.emptyList(); private List allowedOrigins = ANY; + private String allowedOriginsRegex; private List allowedMethods = ANY_METHOD; private List allowedHeaders = ANY; private List exposedHeaders = Collections.emptyList(); @@ -65,6 +67,26 @@ public void setAllowedOrigins(@Nullable List allowedOrigins) { } } + /** + * @return a regular expression for matching Allowed Origins. + */ + @NonNull + public Optional getAllowedOriginsRegex() { + if (allowedOriginsRegex == null || allowedOriginsRegex.equals(StringUtils.EMPTY_STRING)) { + return Optional.empty(); + } + return Optional.ofNullable(allowedOriginsRegex); + } + + /** + * Sets a regular expression for matching Allowed Origins. + * + * @param allowedOriginsRegex a regular expression for matching Allowed Origins. + */ + public void setAllowedOriginsRegex(String allowedOriginsRegex) { + this.allowedOriginsRegex = allowedOriginsRegex; + } + /** * @return The allowed methods */ diff --git a/http-server/src/main/java/io/micronaut/http/server/cors/CorsOriginConverter.java b/http-server/src/main/java/io/micronaut/http/server/cors/CorsOriginConverter.java index a5b4da4112b..baa6cd42f37 100644 --- a/http-server/src/main/java/io/micronaut/http/server/cors/CorsOriginConverter.java +++ b/http-server/src/main/java/io/micronaut/http/server/cors/CorsOriginConverter.java @@ -40,6 +40,7 @@ public class CorsOriginConverter implements TypeConverter, CorsOriginConfiguration> { private static final String ALLOWED_ORIGINS = "allowed-origins"; + private static final String ALLOWED_ORIGINS_REGEX = "allowed-origins-regex"; private static final String ALLOWED_METHODS = "allowed-methods"; private static final String ALLOWED_HEADERS = "allowed-headers"; private static final String EXPOSED_HEADERS = "exposed-headers"; @@ -57,6 +58,10 @@ public Optional convert(Map object, Cla .get(ALLOWED_ORIGINS, ConversionContext.LIST_OF_STRING) .ifPresent(configuration::setAllowedOrigins); + convertibleValues + .get(ALLOWED_ORIGINS_REGEX, ConversionContext.STRING) + .ifPresent(configuration::setAllowedOriginsRegex); + convertibleValues .get(ALLOWED_METHODS, CONVERSION_CONTEXT_LIST_OF_HTTP_METHOD) .ifPresent(configuration::setAllowedMethods); diff --git a/http-server/src/main/java/io/micronaut/http/server/cors/CrossOrigin.java b/http-server/src/main/java/io/micronaut/http/server/cors/CrossOrigin.java new file mode 100644 index 00000000000..4eeed2849b0 --- /dev/null +++ b/http-server/src/main/java/io/micronaut/http/server/cors/CrossOrigin.java @@ -0,0 +1,91 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.http.server.cors; + +import io.micronaut.context.annotation.AliasFor; +import io.micronaut.core.util.StringUtils; +import io.micronaut.http.HttpMethod; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +/** + * Support CORs configuration via annotation. For example, it will enable Micronaut developers only + * to allow CORS for a few routes in their applications. Thus, having more secure + * applications. + * @since 3.9.0 + */ +@Documented +@Inherited +@Retention(RUNTIME) +@Target({ElementType.TYPE, ElementType.METHOD}) +public @interface CrossOrigin { + + /** + * + * @return the origins for which cross-origin requests are allowed + */ + @AliasFor(member = "allowedOrigins") + String[] value() default {}; + + /** + * + * @return the origins for which cross-origin requests are allowed + */ + @AliasFor(member = "value") + String[] allowedOrigins() default {}; + + /** + * + * @return regular expression to match allowed origins + */ + String allowedOriginsRegex() default StringUtils.EMPTY_STRING; + + /** + * + * @return request headers permitted in requests + */ + String[] allowedHeaders() default {}; + + /** + * + * @return response headers that user-agent will allow client to access on actual response + */ + String[] exposedHeaders() default {}; + + /** + * + * @return supported HTTP request methods + */ + HttpMethod[] allowedMethods() default {}; + + /** + * + * @return whether the browser should send credentials + */ + boolean allowCredentials() default true; + + /** + * + * @return maximum age (in seconds) of the cache duration for preflight responses + */ + long maxAge() default 1800L; +} diff --git a/http-server/src/main/java/io/micronaut/http/server/cors/CrossOriginUtil.java b/http-server/src/main/java/io/micronaut/http/server/cors/CrossOriginUtil.java new file mode 100644 index 00000000000..4c7cf81bccd --- /dev/null +++ b/http-server/src/main/java/io/micronaut/http/server/cors/CrossOriginUtil.java @@ -0,0 +1,86 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.http.server.cors; + +import io.micronaut.core.annotation.AnnotationMetadata; +import io.micronaut.core.annotation.NonNull; +import io.micronaut.core.util.CollectionUtils; +import io.micronaut.http.HttpAttributes; +import io.micronaut.http.HttpMethod; +import io.micronaut.http.HttpRequest; + +import java.util.Arrays; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * Utility classes to work with {@link CrossOrigin}. + * @author Sergio del Amo + * @since 3.9.0 + */ +public final class CrossOriginUtil { + + public static final String MEMBER_ALLOWED_ORIGINS = "allowedOrigins"; + public static final String MEMBER_ALLOWED_ORIGINS_REGEX = "allowedOriginsRegex"; + public static final String MEMBER_ALLOWED_HEADERS = "allowedHeaders"; + public static final String MEMBER_EXPOSED_HEADERS = "exposedHeaders"; + public static final String MEMBER_ALLOWED_METHODS = "allowedMethods"; + public static final String MEMBER_ALLOW_CREDENTIALS = "allowCredentials"; + public static final String MEMBER_MAX_AGE = "maxAge"; + + private CrossOriginUtil() { + } + + /** + * @param request the HTTP request for the configuration + * @return the cors origin configuration for the given request + */ + @NonNull + public static Optional getCorsOriginConfigurationForRequest(@NonNull HttpRequest request) { + return request.getAttribute(HttpAttributes.ROUTE_MATCH, AnnotationMetadata.class) + .flatMap(CrossOriginUtil::getCorsOriginConfiguration); + } + + private static Optional getCorsOriginConfiguration(@NonNull AnnotationMetadata annotationMetadata) { + if (!annotationMetadata.hasAnnotation(CrossOrigin.class)) { + return Optional.empty(); + } + CorsOriginConfiguration config = new CorsOriginConfiguration(); + config.setAllowedOrigins(Arrays.asList(annotationMetadata.stringValues(CrossOrigin.class, MEMBER_ALLOWED_ORIGINS))); + annotationMetadata.stringValue(CrossOrigin.class, MEMBER_ALLOWED_ORIGINS_REGEX) + .ifPresent(config::setAllowedOriginsRegex); + + String[] allowedHeaders = annotationMetadata.stringValues(CrossOrigin.class, MEMBER_ALLOWED_HEADERS); + List allowedHeadersList = allowedHeaders.length == 0 ? CorsOriginConfiguration.ANY : Arrays.asList(allowedHeaders); + config.setAllowedHeaders(allowedHeadersList); + config.setExposedHeaders(Arrays.asList(annotationMetadata.stringValues(CrossOrigin.class, MEMBER_EXPOSED_HEADERS))); + + + List allowedMethods = Stream.of(annotationMetadata.stringValues(CrossOrigin.class, MEMBER_ALLOWED_METHODS)) + .map(HttpMethod::parse) + .filter(method -> method != HttpMethod.CUSTOM) + .collect(Collectors.toList()); + config.setAllowedMethods(CollectionUtils.isNotEmpty(allowedMethods) ? allowedMethods : CorsOriginConfiguration.ANY_METHOD); + + annotationMetadata.booleanValue(CrossOrigin.class, MEMBER_ALLOW_CREDENTIALS) + .ifPresent(config::setAllowCredentials); + annotationMetadata.longValue(CrossOrigin.class, MEMBER_MAX_AGE) + .ifPresent(config::setMaxAge); + return Optional.of(config); + } +} diff --git a/src/main/docs/guide/appendix/breaks.adoc b/src/main/docs/guide/appendix/breaks.adoc index bc34e517643..829809ec178 100644 --- a/src/main/docs/guide/appendix/breaks.adoc +++ b/src/main/docs/guide/appendix/breaks.adoc @@ -120,6 +120,10 @@ Interceptors with multiple interceptor bindings annotations now require the same New type converters can be added to api:core.convert.MutableConversionService[] retrieved from the bean context or by declaring a bean of type api:core.convert.TypeConverter[]. To register a type converter into `ConversionService.SHARED`, the registration needs to be done via the service loader. +== 3.9.0 + +Since Micronaut Framework 3.9.0, CORS `allowed-origins` configuration does not support regular expressions to prevent accidentally exposing your API. You can use `allowed-origins-regex`, if you wish to support a regular expression. + == 3.8.7 Micronaut Framework 3.8.7 updates to https://bitbucket.org/snakeyaml/snakeyaml/wiki/Changes[SnakeYAML 2.0] which addresses https://nvd.nist.gov/vuln/detail/CVE-2022-1471[CVE-2022-1471]. Many organizations' policies forbid their teams to use Micronaut Framework if the framework depends on a vulnerable dependency, even if the framework is unaffected. Micronaut Framework is not affected by https://nvd.nist.gov/vuln/detail/CVE-2022-1471[CVE-2022-1471]. diff --git a/src/main/docs/guide/httpServer/serverConfiguration/cors/annotationBasedCors.adoc b/src/main/docs/guide/httpServer/serverConfiguration/cors/annotationBasedCors.adoc new file mode 100644 index 00000000000..62229e41cda --- /dev/null +++ b/src/main/docs/guide/httpServer/serverConfiguration/cors/annotationBasedCors.adoc @@ -0,0 +1,27 @@ +Micronaut CORS configuration applies by default to all endpoints in the running application. + +As an alternative, <> can be applied in a more fine-grained manner to specific routes using the +link:{api}/io/micronaut/http/server/cors/CrossOrigin.html[@CrossOrigin] annotation. This is applied to `@Controller` to apply the CORS configuration to all endpoints in the controller. Alternatively, the annotation can be applied to specific endpoints on a controller, for even more fine-grained control. + +The `@CrossOrigin` annotation maps with a one-to-one correspondence to application-wide link:{api}/io/micronaut/http/server/cors/CorsOriginConfiguration.html[CorsOriginConfiguration] configuration properties. To specify just an allowed origin, use `@CrossOrigin("https://foo.com")`. To specify additional configuration details, use a combination of annotation attributes the same as you would for specifying global `CorsOriginConfiguration` properties. + +[source,java] +---- +@CrossOrigin( + allowedOrigins = { "http://foo.com" }, + allowedOriginsRegex = "^http(|s):\\/\\/www\\.google\\.com$", + allowedMethods = { HttpMethod.POST, HttpMethod.PUT }, + allowedHeaders = { HttpHeaders.CONTENT_TYPE, HttpHeaders.AUTHORIZATION }, + exposedHeaders = { HttpHeaders.CONTENT_TYPE, HttpHeaders.AUTHORIZATION }, + allowCredentials = false, + maxAge = 3600 +) +---- + +The following example demonstrates how the annotation might be applied to a specific endpoint. To enable CORS for all endpoints in the controller, move the annotation to the class level and configure it appropriately. + +snippet::io.micronaut.docs.http.server.cors.CorsController[tags="imports,controller", indent=0, title="@CrossOrigin Example"] + +<1> The ann:http.server.cors.CrossOrigin[] annotation is applied to a specific endpoint, making the CORS configuration fine-grained. +<2> The `GET /hello` endpoint has "https://myui.com" as an allowed cross origin endpoint +<3> The `GET /hello/nocors` endpoint cannot use "https://myui.com" as an origin, since it doesn't have a CORS configuration that allows it. diff --git a/src/main/docs/guide/httpServer/serverConfiguration/cors/corsAllowCredentials.adoc b/src/main/docs/guide/httpServer/serverConfiguration/cors/corsAllowCredentials.adoc index 65c5b6b121e..af5b6b4ade8 100644 --- a/src/main/docs/guide/httpServer/serverConfiguration/cors/corsAllowCredentials.adoc +++ b/src/main/docs/guide/httpServer/serverConfiguration/cors/corsAllowCredentials.adoc @@ -1,4 +1,4 @@ -Credentials are allowed by default for CORS requests. To disallow credentials, set the `allowCredentials` option to `false`. +Credentials are allowed by default for CORS requests. To disallow credentials, set the `allow-credentials` option to `false`. .Example CORS Configuration [configuration] @@ -9,5 +9,5 @@ micronaut: enabled: true configurations: web: - allowCredentials: false + allow-credentials: false ---- diff --git a/src/main/docs/guide/httpServer/serverConfiguration/cors/corsAllowedHeaders.adoc b/src/main/docs/guide/httpServer/serverConfiguration/cors/corsAllowedHeaders.adoc index ea08df88177..fb6e540e8a7 100644 --- a/src/main/docs/guide/httpServer/serverConfiguration/cors/corsAllowedHeaders.adoc +++ b/src/main/docs/guide/httpServer/serverConfiguration/cors/corsAllowedHeaders.adoc @@ -1,6 +1,6 @@ -To allow any request header for a given configuration, don't include the `allowedHeaders` key in your configuration. +To allow any request header for a given configuration, don't include the `allowed-headers` key in your configuration. -For multiple allowed headers, set the `allowedHeaders` key of the configuration to a list of strings. +For multiple allowed headers, set the `allowed-headers` key of the configuration to a list of strings. .Example CORS Configuration [configuration] @@ -11,7 +11,7 @@ micronaut: enabled: true configurations: web: - allowedHeaders: + allowed-headers: - Content-Type - Authorization ---- diff --git a/src/main/docs/guide/httpServer/serverConfiguration/cors/corsAllowedMethods.adoc b/src/main/docs/guide/httpServer/serverConfiguration/cors/corsAllowedMethods.adoc index a2b033ae3d5..c34bc16f788 100644 --- a/src/main/docs/guide/httpServer/serverConfiguration/cors/corsAllowedMethods.adoc +++ b/src/main/docs/guide/httpServer/serverConfiguration/cors/corsAllowedMethods.adoc @@ -1,6 +1,6 @@ -To allow any request method for a given configuration, don't include the `allowedMethods` key in your configuration. +To allow any request method for a given configuration, don't include the `allowed-methods` key in your configuration. -For multiple allowed methods, set the `allowedMethods` key of the configuration to a list of strings. +For multiple allowed methods, set the `allowed-methods` key of the configuration to a list of strings. .Example CORS Configuration [configuration] @@ -11,7 +11,7 @@ micronaut: enabled: true configurations: web: - allowedMethods: + allowed-methods: - POST - PUT ---- diff --git a/src/main/docs/guide/httpServer/serverConfiguration/cors/corsAllowedOrigins.adoc b/src/main/docs/guide/httpServer/serverConfiguration/cors/corsAllowedOrigins.adoc index a1a10186380..580473c5a0e 100644 --- a/src/main/docs/guide/httpServer/serverConfiguration/cors/corsAllowedOrigins.adoc +++ b/src/main/docs/guide/httpServer/serverConfiguration/cors/corsAllowedOrigins.adoc @@ -1,8 +1,8 @@ -To allow any origin for a given configuration, don't include the `allowedOrigins` key in your configuration. +Don't define `allowed-origins` or `allowed-origins-regex` to allow any origin for a given configuration. -For multiple valid origins, set the `allowedOrigins` key of the configuration to a list of strings. Each value can either be a static value (`http://www.foo.com`) or a regular expression (`^http(|s)://www\.google\.com$`). +For multiple valid origins, set the `allowed-origins` key of the configuration to a list of strings. -Regular expressions are passed to link:{javase}java/util/regex/Pattern.html#compile-java.lang.String-[Pattern#compile] and compared to the request origin with link:{javase}java/util/regex/Matcher.html#matches--[Matcher#matches]. +You can also define via `allowed-origins-regex` a regular expression (`^http(|s)://www\.google\.com$`). The Regular expression is passed to link:{javase}java/util/regex/Pattern.html#compile-java.lang.String-[Pattern#compile] and compared to the request origin with link:{javase}java/util/regex/Matcher.html#matches--[Matcher#matches]. .Example CORS Configuration [configuration] @@ -13,7 +13,9 @@ micronaut: enabled: true configurations: web: - allowedOrigins: + allowed-origins-regex: '^http(|s):\/\/www\.google\.com$' + allowed-origins: - http://foo.com - - ^http(|s):\/\/www\.google\.com$ ---- + +WARNING: Use the `allowed-origins-regex` configuration judiciously. You may accidentally make an insecure configuration which could be targeted by an attacker registering domains targeting the regular expression. diff --git a/src/main/docs/guide/httpServer/serverConfiguration/cors/corsExposedHeaders.adoc b/src/main/docs/guide/httpServer/serverConfiguration/cors/corsExposedHeaders.adoc index ed2aeb6df5f..77bbf99c226 100644 --- a/src/main/docs/guide/httpServer/serverConfiguration/cors/corsExposedHeaders.adoc +++ b/src/main/docs/guide/httpServer/serverConfiguration/cors/corsExposedHeaders.adoc @@ -1,4 +1,4 @@ -To configure the headers that are sent in the response to a CORS request through the `Access-Control-Expose-Headers` header, include a list of strings for the `exposedHeaders` key in your configuration. None are exposed by default. +To configure the headers that are sent in the response to a CORS request through the `Access-Control-Expose-Headers` header, include a list of strings for the `exposed-headers` key in your configuration. None are exposed by default. .Example CORS Configuration [configuration] @@ -9,7 +9,7 @@ micronaut: enabled: true configurations: web: - exposedHeaders: + exposed-headers: - Content-Type - Authorization ---- diff --git a/src/main/docs/guide/httpServer/serverConfiguration/cors/corsMaxAge.adoc b/src/main/docs/guide/httpServer/serverConfiguration/cors/corsMaxAge.adoc index 93f2c2557f9..34523504b66 100644 --- a/src/main/docs/guide/httpServer/serverConfiguration/cors/corsMaxAge.adoc +++ b/src/main/docs/guide/httpServer/serverConfiguration/cors/corsMaxAge.adoc @@ -9,5 +9,5 @@ micronaut: enabled: true configurations: web: - maxAge: 3600 # 1 hour + max-age: 3600 # 1 hour ---- diff --git a/src/main/docs/guide/toc.yml b/src/main/docs/guide/toc.yml index 43ffd1eb780..dbb8fb47e40 100644 --- a/src/main/docs/guide/toc.yml +++ b/src/main/docs/guide/toc.yml @@ -143,6 +143,7 @@ httpServer: listener: Advanced Listener Configuration cors: title: Configuring CORS + annotationBasedCors: Annotation-based CORS Configuration corsConfiguration: CORS via Configuration corsAllowedOrigins: Allowed Origins corsAllowedMethods: Allowed Methods diff --git a/test-suite-groovy/src/test/groovy/io/micronaut/docs/http/server/cors/CorsController.groovy b/test-suite-groovy/src/test/groovy/io/micronaut/docs/http/server/cors/CorsController.groovy new file mode 100644 index 00000000000..50f8d05c4f0 --- /dev/null +++ b/test-suite-groovy/src/test/groovy/io/micronaut/docs/http/server/cors/CorsController.groovy @@ -0,0 +1,29 @@ +package io.micronaut.docs.http.server.cors + +import io.micronaut.context.annotation.Requires + +// tag::imports[] +import io.micronaut.http.MediaType +import io.micronaut.http.annotation.Controller +import io.micronaut.http.annotation.Get +import io.micronaut.http.annotation.Produces +import io.micronaut.http.server.cors.CrossOrigin +// end::imports[] + +@Requires(property = "spec.name", value = "CorsControllerSpec") +// tag::controller[] +@Controller("/hello") +class CorsController { + @CrossOrigin("https://myui.com") // <1> + @Get(produces = MediaType.TEXT_PLAIN) // <2> + String cors() { + return "Welcome to the worlds of CORS" + } + + @Produces(MediaType.TEXT_PLAIN) + @Get("/nocors") // <3> + String nocorstoday() { + return "No more CORS for you" + } +} +// end::controller[] diff --git a/test-suite-groovy/src/test/groovy/io/micronaut/docs/http/server/cors/CorsControllerSpec.groovy b/test-suite-groovy/src/test/groovy/io/micronaut/docs/http/server/cors/CorsControllerSpec.groovy new file mode 100644 index 00000000000..0e147c50bd1 --- /dev/null +++ b/test-suite-groovy/src/test/groovy/io/micronaut/docs/http/server/cors/CorsControllerSpec.groovy @@ -0,0 +1,61 @@ +package io.micronaut.docs.http.server.cors + +import io.micronaut.context.ApplicationContext +import io.micronaut.core.util.CollectionUtils +import io.micronaut.http.HttpHeaders +import io.micronaut.http.HttpMethod +import io.micronaut.http.HttpRequest +import io.micronaut.http.MediaType +import io.micronaut.http.MutableHttpRequest +import io.micronaut.http.client.BlockingHttpClient +import io.micronaut.http.client.HttpClient +import io.micronaut.http.client.exceptions.HttpClientResponseException +import io.micronaut.http.uri.UriBuilder +import io.micronaut.runtime.server.EmbeddedServer +import spock.lang.Specification + +class CorsControllerSpec extends Specification { + + void "CrossOrigin with allowed Origin"() { + given: + EmbeddedServer embeddedServer = ApplicationContext.run(EmbeddedServer, CollectionUtils.mapOf("spec.name", "CorsControllerSpec")) + HttpRequest request = preflight(UriBuilder.of("/hello"), "https://myui.com", HttpMethod.GET) + HttpClient httpClient = embeddedServer.applicationContext.createBean(HttpClient, embeddedServer.URL) + BlockingHttpClient client = httpClient.toBlocking() + + when: + client.exchange(request) + + then: + noExceptionThrown() + + cleanup: + httpClient.close() + embeddedServer.close() + } + + void "CrossOrigin with not allowed Origin"() { + given: + EmbeddedServer embeddedServer = ApplicationContext.run(EmbeddedServer, CollectionUtils.mapOf("spec.name", "CorsControllerSpec")) + HttpRequest request = preflight(UriBuilder.of("/hello"), "https://google.com", HttpMethod.GET) + HttpClient httpClient = embeddedServer.applicationContext.createBean(HttpClient, embeddedServer.URL) + BlockingHttpClient client = httpClient.toBlocking() + + when: + client.exchange(request) + + then: + thrown(HttpClientResponseException) + + cleanup: + httpClient.close() + embeddedServer.close() + } + + private static MutableHttpRequest preflight(UriBuilder uriBuilder, String originValue, HttpMethod method) { + return HttpRequest.OPTIONS(uriBuilder.build()) + .header(HttpHeaders.ACCEPT, MediaType.TEXT_PLAIN) + .header(HttpHeaders.ORIGIN, originValue) + .header(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, method) + } +} diff --git a/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/http/server/cors/CorsController.kt b/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/http/server/cors/CorsController.kt new file mode 100644 index 00000000000..682b1be033b --- /dev/null +++ b/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/http/server/cors/CorsController.kt @@ -0,0 +1,28 @@ +package io.micronaut.docs.http.server.cors + +// tag::imports[] +import io.micronaut.context.annotation.Requires +import io.micronaut.http.MediaType +import io.micronaut.http.annotation.Controller +import io.micronaut.http.annotation.Get +import io.micronaut.http.annotation.Produces +import io.micronaut.http.server.cors.CrossOrigin +// end::imports[] + +@Requires(property = "spec.name", value = "CorsControllerSpec") +// tag::controller[] +@Controller("/hello") +class CorsController { + @CrossOrigin("https://myui.com") // <1> + @Get(produces = [MediaType.TEXT_PLAIN]) // <2> + fun cors(): String { + return "Welcome to the worlds of CORS" + } + + @Produces(MediaType.TEXT_PLAIN) + @Get("/nocors") // <3> + fun nocorstoday(): String { + return "No more CORS for you" + } +} +// end::controller[] diff --git a/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/http/server/cors/CorsControllerTest.kt b/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/http/server/cors/CorsControllerTest.kt new file mode 100644 index 00000000000..799c09ecc25 --- /dev/null +++ b/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/http/server/cors/CorsControllerTest.kt @@ -0,0 +1,47 @@ +package io.micronaut.docs.http.server.cors + +import io.micronaut.context.ApplicationContext +import io.micronaut.http.* +import io.micronaut.http.client.HttpClient +import io.micronaut.http.client.exceptions.HttpClientResponseException +import io.micronaut.http.uri.UriBuilder +import io.micronaut.runtime.server.EmbeddedServer +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Test + +class CorsControllerTest { + @Test + fun crossOriginWithAllowedOrigin() { + val embeddedServer = ApplicationContext.run(EmbeddedServer::class.java, mapOf("spec.name" to "CorsControllerSpec")) + val request: HttpRequest = preflight(UriBuilder.of("/hello"), "https://myui.com", HttpMethod.GET) + val httpClient = embeddedServer.applicationContext.createBean(HttpClient::class.java, embeddedServer.url) + val client = httpClient.toBlocking() + Assertions.assertDoesNotThrow> { + client.exchange(request) + } + httpClient.close() + embeddedServer.close() + } + + @Test + fun crossOriginWithNotAllowedOrigin() { + val embeddedServer = ApplicationContext.run(EmbeddedServer::class.java, mapOf("spec.name" to "CorsControllerSpec")) + val request: HttpRequest = preflight(UriBuilder.of("/hello"), "https://google.com", HttpMethod.GET) + val httpClient = embeddedServer.applicationContext.createBean( + HttpClient::class.java, embeddedServer.url + ) + val client = httpClient.toBlocking() + Assertions.assertThrows(HttpClientResponseException::class.java) { + val response: HttpResponse = client.exchange(request) + } + httpClient.close() + embeddedServer.close() + } + + fun preflight(uriBuilder: UriBuilder, originValue: String, method: HttpMethod): HttpRequest { + return HttpRequest.OPTIONS(uriBuilder.build()) + .header(HttpHeaders.ACCEPT, MediaType.TEXT_PLAIN) + .header(HttpHeaders.ORIGIN, originValue) + .header(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, method) + } +} diff --git a/test-suite/src/test/java/io/micronaut/docs/http/server/cors/CorsController.java b/test-suite/src/test/java/io/micronaut/docs/http/server/cors/CorsController.java new file mode 100644 index 00000000000..b84e3ab8bcc --- /dev/null +++ b/test-suite/src/test/java/io/micronaut/docs/http/server/cors/CorsController.java @@ -0,0 +1,28 @@ +package io.micronaut.docs.http.server.cors; + +// tag::imports[] +import io.micronaut.context.annotation.Requires; +import io.micronaut.http.MediaType; +import io.micronaut.http.annotation.Controller; +import io.micronaut.http.annotation.Get; +import io.micronaut.http.annotation.Produces; +import io.micronaut.http.server.cors.CrossOrigin; +// end::imports[] + +@Requires(property = "spec.name", value = "CorsControllerSpec") +// tag::controller[] +@Controller("/hello") +public class CorsController { + @CrossOrigin("https://myui.com") // <1> + @Get(produces = MediaType.TEXT_PLAIN) // <2> + public String cors() { + return "Welcome to the worlds of CORS"; + } + + @Produces(MediaType.TEXT_PLAIN) + @Get("/nocors") // <3> + public String nocorstoday() { + return "No more CORS for you"; + } +} +// end::controller[] diff --git a/test-suite/src/test/java/io/micronaut/docs/http/server/cors/CorsControllerTest.java b/test-suite/src/test/java/io/micronaut/docs/http/server/cors/CorsControllerTest.java new file mode 100644 index 00000000000..be05521708e --- /dev/null +++ b/test-suite/src/test/java/io/micronaut/docs/http/server/cors/CorsControllerTest.java @@ -0,0 +1,53 @@ +package io.micronaut.docs.http.server.cors; + +import io.micronaut.context.ApplicationContext; +import io.micronaut.core.util.CollectionUtils; +import io.micronaut.http.*; +import io.micronaut.http.client.BlockingHttpClient; +import io.micronaut.http.client.HttpClient; +import io.micronaut.http.client.exceptions.HttpClientResponseException; +import io.micronaut.http.uri.UriBuilder; +import io.micronaut.runtime.server.EmbeddedServer; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.function.Executable; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertThrows; + +class CorsControllerTest { + + @Test + void crossOriginWithAllowedOrigin() { + + EmbeddedServer embeddedServer = ApplicationContext.run(EmbeddedServer.class, CollectionUtils.mapOf("spec.name", "CorsControllerSpec")); + HttpRequest request = preflight(UriBuilder.of("/hello"), "https://myui.com", HttpMethod.GET); + HttpClient httpClient = embeddedServer.getApplicationContext().createBean(HttpClient.class, embeddedServer.getURL()); + BlockingHttpClient client = httpClient.toBlocking(); + + assertDoesNotThrow(() -> client.exchange(request)); + + httpClient.close(); + embeddedServer.close(); + } + + @Test + void crossOriginWithNotAllowedOrigin() { + EmbeddedServer embeddedServer = ApplicationContext.run(EmbeddedServer.class, CollectionUtils.mapOf("spec.name", "CorsControllerSpec")); + HttpRequest request = preflight(UriBuilder.of("/hello"), "https://google.com", HttpMethod.GET); + HttpClient httpClient = embeddedServer.getApplicationContext().createBean(HttpClient.class, embeddedServer.getURL()); + BlockingHttpClient client = httpClient.toBlocking(); + + Executable e = () -> client.exchange(request); + assertThrows(HttpClientResponseException.class, e); + + httpClient.close(); + embeddedServer.close(); + } + + private static MutableHttpRequest preflight(UriBuilder uriBuilder, String originValue, HttpMethod method) { + return HttpRequest.OPTIONS(uriBuilder.build()) + .header(HttpHeaders.ACCEPT, MediaType.TEXT_PLAIN) + .header(HttpHeaders.ORIGIN, originValue) + .header(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, method); + } +} From 0d1bcc275f76e7d72330f15b48beb65ade834ea0 Mon Sep 17 00:00:00 2001 From: Denis Stepanov Date: Thu, 16 Mar 2023 02:28:48 -0600 Subject: [PATCH 597/743] Allow to directly synthesize the annotation value (#8949) --- .../inject/annotation/AnnotationMetadataWriterSpec.groovy | 7 +++++++ .../inject/annotation/AnnotationMetadataSupport.java | 5 ++++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/inject-groovy/src/test/groovy/io/micronaut/inject/annotation/AnnotationMetadataWriterSpec.groovy b/inject-groovy/src/test/groovy/io/micronaut/inject/annotation/AnnotationMetadataWriterSpec.groovy index a79115db4f7..7720215c346 100644 --- a/inject-groovy/src/test/groovy/io/micronaut/inject/annotation/AnnotationMetadataWriterSpec.groovy +++ b/inject-groovy/src/test/groovy/io/micronaut/inject/annotation/AnnotationMetadataWriterSpec.groovy @@ -580,4 +580,11 @@ class Test { values["annotationsArray1"] == new AnnotationValue[0] values["annotationsArray2"] == new AnnotationValue[] { AnnotationValue.builder(MyAnnotation3).value("foo").build(), AnnotationValue.builder(MyAnnotation3).value("bar").build() } } + + void "test synthesize"() { + when: + def annotationValue = AnnotationValue.builder(jakarta.inject.Named.class).value("Denis").build() + then: + AnnotationMetadataSupport.buildAnnotation(jakarta.inject.Named.class, annotationValue).value() == "Denis" + } } diff --git a/inject/src/main/java/io/micronaut/inject/annotation/AnnotationMetadataSupport.java b/inject/src/main/java/io/micronaut/inject/annotation/AnnotationMetadataSupport.java index fb95895521c..f48a5028db2 100644 --- a/inject/src/main/java/io/micronaut/inject/annotation/AnnotationMetadataSupport.java +++ b/inject/src/main/java/io/micronaut/inject/annotation/AnnotationMetadataSupport.java @@ -321,12 +321,15 @@ static Optional> getProxyClass(Class The type * @return The annotation */ - static T buildAnnotation(Class annotationClass, @Nullable AnnotationValue annotationValue) { + @Internal + public static T buildAnnotation(Class annotationClass, @Nullable AnnotationValue annotationValue) { Optional> proxyClass = getProxyClass(annotationClass); if (proxyClass.isPresent()) { Map values = new HashMap<>(getDefaultValues(annotationClass)); From 6f956e7509fdb900c6fb6b732d65220ef2e1eaaa Mon Sep 17 00:00:00 2001 From: Denis Stepanov Date: Thu, 16 Mar 2023 06:39:22 -0600 Subject: [PATCH 598/743] Correctly propagate annotations default (#8943) --- .../visitor/BeanIntrospectionWriter.java | 75 +-- .../visitor/util/VisitorContextUtils.java | 27 - .../writer/AbstractClassFileWriter.java | 69 ++- .../writer/BeanDefinitionReferenceWriter.java | 1 + .../inject/writer/BeanDefinitionWriter.java | 75 +-- .../inject/writer/ExecutableMethodWriter.java | 509 ------------------ .../ExecutableMethodsDefinitionWriter.java | 42 +- .../visitor/beans/BeanIntrospectorSpec.groovy | 7 + .../MapOfListsWithAutomaticUnwrapping.java | 14 + .../micronaut/inject/visitor/beans/MyMin.java | 29 + .../inject/beans/BeanDefinitionSpec.groovy | 55 ++ .../inject/beans/MapOfListsBean1.java | 19 + .../inject/beans/MapOfListsBean2.java | 19 + .../inject/beans/MapOfListsBean3.java | 20 + .../inject/beans/MapOfListsBean4.java | 19 + .../inject/beans/MapOfListsBean5.java | 17 + .../io/micronaut/inject/beans/MyMin1.java | 29 + .../io/micronaut/inject/beans/MyMin2.java | 29 + .../io/micronaut/inject/beans/MyMin3.java | 29 + .../io/micronaut/inject/beans/MyMin4.java | 29 + .../io/micronaut/inject/beans/MyMin5.java | 29 + .../annotation/MutableAnnotationMetadata.java | 24 +- 22 files changed, 503 insertions(+), 663 deletions(-) delete mode 100644 core-processor/src/main/java/io/micronaut/inject/writer/ExecutableMethodWriter.java create mode 100644 inject-java-test/src/test/groovy/io/micronaut/inject/visitor/beans/MapOfListsWithAutomaticUnwrapping.java create mode 100644 inject-java-test/src/test/groovy/io/micronaut/inject/visitor/beans/MyMin.java create mode 100644 inject-java/src/test/groovy/io/micronaut/inject/beans/MapOfListsBean1.java create mode 100644 inject-java/src/test/groovy/io/micronaut/inject/beans/MapOfListsBean2.java create mode 100644 inject-java/src/test/groovy/io/micronaut/inject/beans/MapOfListsBean3.java create mode 100644 inject-java/src/test/groovy/io/micronaut/inject/beans/MapOfListsBean4.java create mode 100644 inject-java/src/test/groovy/io/micronaut/inject/beans/MapOfListsBean5.java create mode 100644 inject-java/src/test/groovy/io/micronaut/inject/beans/MyMin1.java create mode 100644 inject-java/src/test/groovy/io/micronaut/inject/beans/MyMin2.java create mode 100644 inject-java/src/test/groovy/io/micronaut/inject/beans/MyMin3.java create mode 100644 inject-java/src/test/groovy/io/micronaut/inject/beans/MyMin4.java create mode 100644 inject-java/src/test/groovy/io/micronaut/inject/beans/MyMin5.java diff --git a/core-processor/src/main/java/io/micronaut/inject/beans/visitor/BeanIntrospectionWriter.java b/core-processor/src/main/java/io/micronaut/inject/beans/visitor/BeanIntrospectionWriter.java index e58e9cf1ebb..b858224804d 100644 --- a/core-processor/src/main/java/io/micronaut/inject/beans/visitor/BeanIntrospectionWriter.java +++ b/core-processor/src/main/java/io/micronaut/inject/beans/visitor/BeanIntrospectionWriter.java @@ -41,7 +41,6 @@ import io.micronaut.inject.ast.TypedElement; import io.micronaut.inject.beans.AbstractInitializableBeanIntrospection; import io.micronaut.inject.processing.JavaModelUtils; -import io.micronaut.inject.visitor.util.VisitorContextUtils; import io.micronaut.inject.writer.AbstractAnnotationMetadataWriter; import io.micronaut.inject.writer.ClassWriterOutputVisitor; import io.micronaut.inject.writer.DispatchWriter; @@ -202,19 +201,6 @@ void visitProperty( @Nullable AnnotationMetadata annotationMetadata, @Nullable Map typeArguments) { - MutableAnnotationMetadata.contributeDefaults( - this.annotationMetadata, - annotationMetadata - ); - if (typeArguments != null) { - for (ClassElement element : typeArguments.values()) { - VisitorContextUtils.contributeRepeatable( - this.annotationMetadata, - element - ); - } - } - int readDispatchIndex = -1; if (readMember != null) { if (readMember instanceof MethodElement) { @@ -312,12 +298,12 @@ public void accept(ClassWriterOutputVisitor classWriterOutputVisitor) throws IOE // Run only once executed = true; - // write the reference - writeIntrospectionReference(classWriterOutputVisitor); - loadTypeMethods.clear(); - // write the introspection + // First write the introspection for the annotation metadata can be populated with defaults that reference will contain writeIntrospectionClass(classWriterOutputVisitor); + loadTypeMethods.clear(); + // Second write the reference + writeIntrospectionReference(classWriterOutputVisitor); } } @@ -334,6 +320,7 @@ private void buildStaticInit(ClassWriter classWriter) { Type args = Type.getType(Argument[].class); classWriter.visitField(ACC_PRIVATE | ACC_FINAL | ACC_STATIC, FIELD_CONSTRUCTOR_ARGUMENTS, args.getDescriptor(), null, null); pushBuildArgumentsForMethod( + annotationMetadata, introspectionType.getClassName(), introspectionType, classWriter, @@ -414,6 +401,7 @@ private void pushBeanPropertyReference(ClassWriter classWriter, staticInit.dup(); pushCreateArgument( + annotationMetadata, beanType.getClassName(), introspectionType, classWriter, @@ -449,7 +437,7 @@ private void pushBeanMethodReference(ClassWriter classWriter, staticInit.dup(); // 1: return argument ClassElement genericReturnType = beanMethodData.methodElement.getGenericReturnType(); - pushReturnTypeArgument(introspectionType, classWriter, staticInit, classElement.getName(), genericReturnType, defaults, loadTypeMethods); + pushReturnTypeArgument(annotationMetadata, introspectionType, classWriter, staticInit, classElement.getName(), genericReturnType, defaults, loadTypeMethods); // 2: name staticInit.push(beanMethodData.methodElement.getName()); // 3: annotation metadata @@ -459,6 +447,7 @@ private void pushBeanMethodReference(ClassWriter classWriter, staticInit.push((String) null); } else { pushBuildArgumentsForMethod( + annotationMetadata, beanType.getClassName(), introspectionType, classWriter, @@ -917,6 +906,11 @@ private ClassWriter generateClassBytes(ClassWriter classWriter) { } private void pushAnnotationMetadata(ClassWriter classWriter, GeneratorAdapter staticInit, AnnotationMetadata annotationMetadata) { + MutableAnnotationMetadata.contributeDefaults( + this.annotationMetadata, + annotationMetadata + ); + annotationMetadata = annotationMetadata.getTargetAnnotationMetadata(); if (annotationMetadata.isEmpty()) { staticInit.push((String) null); @@ -971,22 +965,6 @@ private static String computeIntrospectionName(String generatingName, String cla */ void visitConstructor(MethodElement constructor) { this.constructor = constructor; - processAnnotationDefaults(constructor); - } - - private void processAnnotationDefaults(MethodElement constructor) { - if (constructor != null) { - MutableAnnotationMetadata.contributeDefaults( - this.annotationMetadata, - constructor.getAnnotationMetadata().getTargetAnnotationMetadata() - ); - for (ParameterElement parameter : constructor.getParameters()) { - MutableAnnotationMetadata.contributeDefaults( - this.annotationMetadata, - parameter.getAnnotationMetadata().getTargetAnnotationMetadata() - ); - } - } } /** @@ -996,29 +974,20 @@ private void processAnnotationDefaults(MethodElement constructor) { */ void visitDefaultConstructor(MethodElement constructor) { this.defaultConstructor = constructor; - processAnnotationDefaults(constructor); } - private static final class ExceptionDispatchTarget implements DispatchWriter.DispatchTarget { + private record ExceptionDispatchTarget(Class exceptionType, String message) implements DispatchWriter.DispatchTarget { - private final Class exceptionType; - private final String message; - - private ExceptionDispatchTarget(Class exceptionType, String message) { - this.exceptionType = exceptionType; - this.message = message; - } - - @Override - public boolean supportsDispatchOne() { - return true; - } + @Override + public boolean supportsDispatchOne() { + return true; + } - @Override - public void writeDispatchOne(GeneratorAdapter writer, int index) { - writer.throwException(Type.getType(exceptionType), message); + @Override + public void writeDispatchOne(GeneratorAdapter writer, int index) { + writer.throwException(Type.getType(exceptionType), message); + } } - } /** * Copy constructor "with" method writer. diff --git a/core-processor/src/main/java/io/micronaut/inject/visitor/util/VisitorContextUtils.java b/core-processor/src/main/java/io/micronaut/inject/visitor/util/VisitorContextUtils.java index 5fef1e840c5..605e32658b9 100644 --- a/core-processor/src/main/java/io/micronaut/inject/visitor/util/VisitorContextUtils.java +++ b/core-processor/src/main/java/io/micronaut/inject/visitor/util/VisitorContextUtils.java @@ -16,16 +16,12 @@ package io.micronaut.inject.visitor.util; import io.micronaut.context.env.CachedEnvironment; -import io.micronaut.core.annotation.AnnotationMetadata; import io.micronaut.core.annotation.Internal; -import io.micronaut.inject.annotation.MutableAnnotationMetadata; -import io.micronaut.inject.ast.ClassElement; import io.micronaut.inject.visitor.VisitorContext; import javax.annotation.processing.ProcessingEnvironment; import java.util.AbstractMap; import java.util.Collections; -import java.util.HashSet; import java.util.Map; import java.util.Optional; import java.util.Set; @@ -78,27 +74,4 @@ public static Map getProcessorOptions(ProcessingEnvironment proc .orElse(Collections.emptyMap()); } - /** - * Contributes repeatable annotation metadata to the given class element. - * - *

WARNING: for internal use only be the framework

- * - * @param target The target - * @param classElement The source - */ - @Internal - public static void contributeRepeatable(AnnotationMetadata target, ClassElement classElement) { - contributeRepeatable(target, classElement, new HashSet<>()); - } - - private static void contributeRepeatable(AnnotationMetadata target, ClassElement classElement, Set alreadySeen) { - alreadySeen.add(classElement); - MutableAnnotationMetadata.contributeRepeatable(target, classElement.getAnnotationMetadata()); - for (ClassElement element : classElement.getTypeArguments().values()) { - if (alreadySeen.contains(classElement)) { - continue; - } - contributeRepeatable(target, element); - } - } } diff --git a/core-processor/src/main/java/io/micronaut/inject/writer/AbstractClassFileWriter.java b/core-processor/src/main/java/io/micronaut/inject/writer/AbstractClassFileWriter.java index e1923e42785..da4fd3e0274 100644 --- a/core-processor/src/main/java/io/micronaut/inject/writer/AbstractClassFileWriter.java +++ b/core-processor/src/main/java/io/micronaut/inject/writer/AbstractClassFileWriter.java @@ -216,6 +216,7 @@ public void addOriginatingElement(@NonNull Element element) { /** * Pushes type arguments onto the stack. * + * @param annotationMetadataWithDefaults The annotation metadata with defaults * @param owningType The owning type * @param owningTypeWriter The declaring class writer * @param generatorAdapter The generator adapter @@ -225,6 +226,7 @@ public void addOriginatingElement(@NonNull Element element) { * @param loadTypeMethods The load type methods */ protected static void pushTypeArgumentElements( + AnnotationMetadata annotationMetadataWithDefaults, Type owningType, ClassWriter owningTypeWriter, GeneratorAdapter generatorAdapter, @@ -236,11 +238,12 @@ protected static void pushTypeArgumentElements( generatorAdapter.visitInsn(ACONST_NULL); return; } - pushTypeArgumentElements(owningType, owningTypeWriter, generatorAdapter, declaringElementName, null, types, new HashSet<>(5), defaults, loadTypeMethods); + pushTypeArgumentElements(annotationMetadataWithDefaults, owningType, owningTypeWriter, generatorAdapter, declaringElementName, null, types, new HashSet<>(5), defaults, loadTypeMethods); } @SuppressWarnings("java:S1872") private static void pushTypeArgumentElements( + AnnotationMetadata annotationMetadataWithDefaults, Type owningType, ClassWriter declaringClassWriter, GeneratorAdapter generatorAdapter, @@ -277,6 +280,7 @@ private static void pushTypeArgumentElements( Map typeArguments = classElement.getTypeArguments(); if (CollectionUtils.isNotEmpty(typeArguments) || !classElement.getAnnotationMetadata().isEmpty()) { buildArgumentWithGenerics( + annotationMetadataWithDefaults, owningType, declaringClassWriter, generatorAdapter, @@ -369,6 +373,7 @@ protected static void buildArgument(GeneratorAdapter generatorAdapter, String ar /** * Builds generic type arguments recursively. * + * @param annotationMetadataWithDefaults The annotation metadata with defaults * @param owningType The owning type * @param owningClassWriter The declaring writer * @param generatorAdapter The generator adapter to use @@ -381,6 +386,7 @@ protected static void buildArgument(GeneratorAdapter generatorAdapter, String ar * @param loadTypeMethods The load type methods */ protected static void buildArgumentWithGenerics( + AnnotationMetadata annotationMetadataWithDefaults, Type owningType, ClassWriter owningClassWriter, GeneratorAdapter generatorAdapter, @@ -431,6 +437,11 @@ protected static void buildArgumentWithGenerics( if (!hasAnnotationMetadata) { generatorAdapter.visitInsn(ACONST_NULL); } else { + MutableAnnotationMetadata.contributeDefaults( + annotationMetadataWithDefaults, + annotationMetadata + ); + AnnotationMetadataWriter.instantiateNewMetadata( owningType, owningClassWriter, @@ -443,6 +454,7 @@ protected static void buildArgumentWithGenerics( // 4th argument, more generics pushTypeArgumentElements( + annotationMetadataWithDefaults, owningType, owningClassWriter, generatorAdapter, @@ -503,15 +515,17 @@ protected static void buildArgumentWithGenerics( } /** - * @param declaringElementName The declaring element name - * @param owningType The owning type - * @param declaringClassWriter The declaring class writer - * @param generatorAdapter The {@link GeneratorAdapter} - * @param argumentTypes The argument types - * @param defaults The annotation defaults - * @param loadTypeMethods The load type methods + * @param annotationMetadataWithDefaults The annotation metadata with defaults + * @param declaringElementName The declaring element name + * @param owningType The owning type + * @param declaringClassWriter The declaring class writer + * @param generatorAdapter The {@link GeneratorAdapter} + * @param argumentTypes The argument types + * @param defaults The annotation defaults + * @param loadTypeMethods The load type methods */ protected static void pushBuildArgumentsForMethod( + AnnotationMetadata annotationMetadataWithDefaults, String declaringElementName, Type owningType, ClassWriter declaringClassWriter, @@ -526,6 +540,15 @@ protected static void pushBuildArgumentsForMethod( // the array index position generatorAdapter.push(i); + MutableAnnotationMetadata.contributeDefaults( + annotationMetadataWithDefaults, + entry.getAnnotationMetadata() + ); + MutableAnnotationMetadata.contributeDefaults( + annotationMetadataWithDefaults, + entry.getType().getTypeAnnotationMetadata() + ); + ClassElement classElement = entry.getGenericType(); String argumentName = entry.getName(); AnnotationMetadata annotationMetadata = new AnnotationMetadataHierarchy( @@ -534,6 +557,7 @@ protected static void pushBuildArgumentsForMethod( ).merge(); Map typeArguments = classElement.getTypeArguments(); pushCreateArgument( + annotationMetadataWithDefaults, declaringElementName, owningType, declaringClassWriter, @@ -556,15 +580,17 @@ protected static void pushBuildArgumentsForMethod( /** * Pushes an argument. * - * @param owningType The owning type - * @param classWriter The declaring class writer - * @param generatorAdapter The generator adapter - * @param declaringTypeName The declaring type name - * @param argument The argument - * @param defaults The annotation defaults - * @param loadTypeMethods The load type methods - */ - protected void pushReturnTypeArgument(Type owningType, + * @param annotationMetadataWithDefaults The annotation metadata with defaults + * @param owningType The owning type + * @param classWriter The declaring class writer + * @param generatorAdapter The generator adapter + * @param declaringTypeName The declaring type name + * @param argument The argument + * @param defaults The annotation defaults + * @param loadTypeMethods The load type methods + */ + protected void pushReturnTypeArgument(AnnotationMetadata annotationMetadataWithDefaults, + Type owningType, ClassWriter classWriter, GeneratorAdapter generatorAdapter, String declaringTypeName, @@ -598,6 +624,7 @@ protected void pushReturnTypeArgument(Type owningType, } pushCreateArgument( + annotationMetadataWithDefaults, declaringTypeName, owningType, classWriter, @@ -614,6 +641,7 @@ protected void pushReturnTypeArgument(Type owningType, /** * Pushes a new Argument creation. * + * @param annotationMetadataWithDefaults The annotation metadata with defaults * @param declaringTypeName The declaring type name * @param owningType The owning type * @param declaringClassWriter The declaring class writer @@ -626,6 +654,7 @@ protected void pushReturnTypeArgument(Type owningType, * @param loadTypeMethods The load type methods */ protected static void pushCreateArgument( + AnnotationMetadata annotationMetadataWithDefaults, String declaringTypeName, Type owningType, ClassWriter declaringClassWriter, @@ -675,6 +704,11 @@ protected static void pushCreateArgument( // 3rd argument: The annotation metadata if (hasAnnotations) { + MutableAnnotationMetadata.contributeDefaults( + annotationMetadataWithDefaults, + annotationMetadata + ); + AnnotationMetadataWriter.instantiateNewMetadata( owningType, declaringClassWriter, @@ -690,6 +724,7 @@ protected static void pushCreateArgument( // 4th argument: The generic types if (hasTypeArguments) { pushTypeArgumentElements( + annotationMetadataWithDefaults, owningType, declaringClassWriter, generatorAdapter, diff --git a/core-processor/src/main/java/io/micronaut/inject/writer/BeanDefinitionReferenceWriter.java b/core-processor/src/main/java/io/micronaut/inject/writer/BeanDefinitionReferenceWriter.java index 867878056af..a7b0d2600aa 100644 --- a/core-processor/src/main/java/io/micronaut/inject/writer/BeanDefinitionReferenceWriter.java +++ b/core-processor/src/main/java/io/micronaut/inject/writer/BeanDefinitionReferenceWriter.java @@ -262,6 +262,7 @@ private ClassWriter generateClassBytes() { // start method: Argument getGenericBeanType() GeneratorAdapter getGenericType = startPublicMethodZeroArgs(classWriter, Argument.class, "getGenericBeanType"); pushCreateArgument( + annotationMetadata, beanDefinitionReferenceClassName, Type.getType(getTypeDescriptor(beanDefinitionReferenceClassName)), classWriter, diff --git a/core-processor/src/main/java/io/micronaut/inject/writer/BeanDefinitionWriter.java b/core-processor/src/main/java/io/micronaut/inject/writer/BeanDefinitionWriter.java index 10366b64563..80a60b6a669 100644 --- a/core-processor/src/main/java/io/micronaut/inject/writer/BeanDefinitionWriter.java +++ b/core-processor/src/main/java/io/micronaut/inject/writer/BeanDefinitionWriter.java @@ -95,7 +95,6 @@ import io.micronaut.inject.visitor.BeanElementVisitor; import io.micronaut.inject.visitor.BeanElementVisitorContext; import io.micronaut.inject.visitor.VisitorContext; -import io.micronaut.inject.visitor.util.VisitorContextUtils; import jakarta.inject.Singleton; import org.objectweb.asm.ClassVisitor; import org.objectweb.asm.ClassWriter; @@ -505,6 +504,9 @@ public class BeanDefinitionWriter extends AbstractClassFileWriter implements Bea ReflectionUtils.getRequiredMethod(ConversionServiceProvider.class, "getConversionService") ); + private static final org.objectweb.asm.commons.Method METHOD_INVOKE_INTERNAL = org.objectweb.asm.commons.Method.getMethod( + ReflectionUtils.getRequiredInternalMethod(AbstractExecutableMethod.class, "invokeInternal", Object.class, Object[].class)); + private final ClassWriter classWriter; private final String beanFullClassName; private final String beanDefinitionName; @@ -1000,6 +1002,11 @@ public void visitBeanDefinitionEnd() { throw new IllegalStateException("At least one called to visitBeanDefinitionConstructor(..) is required"); } + if (executableMethodsDefinitionWriter != null) { + // Make sure the methods are written and annotation defaults are contributed + executableMethodsDefinitionWriter.visitDefinitionEnd(); + } + processAllBeanElementVisitors(); if (constructor instanceof MethodElement methodElement) { @@ -1098,6 +1105,7 @@ public void visitBeanDefinitionEnd() { @Override public void accept(Map stringClassElementMap) { pushTypeArgumentElements( + annotationMetadata, beanDefinitionType, classWriter, staticInit, @@ -1553,23 +1561,8 @@ public int visitExecutableMethod(TypedElement declaringType, String interceptedProxyClassName, String interceptedProxyBridgeMethodName) { - MutableAnnotationMetadata.contributeDefaults( - this.annotationMetadata, - methodElement.getAnnotationMetadata() - ); - for (ParameterElement parameterElement : methodElement.getSuspendParameters()) { - MutableAnnotationMetadata.contributeDefaults( - this.annotationMetadata, - parameterElement.getAnnotationMetadata() - ); - VisitorContextUtils.contributeRepeatable( - this.annotationMetadata, - parameterElement.getGenericType() - ); - } - if (executableMethodsDefinitionWriter == null) { - executableMethodsDefinitionWriter = new ExecutableMethodsDefinitionWriter(beanDefinitionName, getBeanDefinitionReferenceClassName(), originatingElements); + executableMethodsDefinitionWriter = new ExecutableMethodsDefinitionWriter(annotationMetadata, beanDefinitionName, getBeanDefinitionReferenceClassName(), originatingElements); } return executableMethodsDefinitionWriter.visitExecutableMethod(declaringType, methodElement, interceptedProxyClassName, interceptedProxyBridgeMethodName); } @@ -1917,6 +1910,7 @@ private void pushInvokeGetPropertyValueForField(GeneratorAdapter injectMethodVis resolveFieldArgument(injectMethodVisitor, currentFieldIndex); } else { pushCreateArgument( + this.annotationMetadata, beanFullClassName, beanDefinitionType, classWriter, @@ -1955,6 +1949,7 @@ private void pushInvokeGetPropertyPlaceholderValueForField(GeneratorAdapter inje resolveFieldArgument(injectMethodVisitor, currentFieldIndex); } else { pushCreateArgument( + this.annotationMetadata, beanFullClassName, beanDefinitionType, classWriter, @@ -2090,6 +2085,7 @@ private int pushGetValueForPathCall(GeneratorAdapter injectMethodVisitor, ClassE ); } else { buildArgumentWithGenerics( + annotationMetadata, beanDefinitionType, classWriter, injectMethodVisitor, @@ -2148,8 +2144,6 @@ private void visitFieldInjectionPointInternal( boolean requiresGenericType) { autoApplyNamedIfPresent(fieldElement, annotationMetadata); - MutableAnnotationMetadata.contributeDefaults(this.annotationMetadata, annotationMetadata); - VisitorContextUtils.contributeRepeatable(this.annotationMetadata, fieldElement.getGenericField()); GeneratorAdapter injectMethodVisitor = this.injectMethodVisitor; @@ -2401,15 +2395,11 @@ private void visitMethodInjectionPointInternal(MethodVisitData methodVisitData, final String methodName = methodElement.getName(); final boolean requiresReflection = methodVisitData.requiresReflection; final ClassElement returnType = methodElement.getReturnType(); - MutableAnnotationMetadata.contributeDefaults(this.annotationMetadata, annotationMetadata); - VisitorContextUtils.contributeRepeatable(this.annotationMetadata, returnType); boolean hasArguments = methodElement.hasParameters(); int argCount = hasArguments ? argumentTypes.size() : 0; Type declaringTypeRef = JavaModelUtils.getTypeReference(declaringType); boolean hasInjectScope = false; for (ParameterElement value : argumentTypes) { - MutableAnnotationMetadata.contributeDefaults(this.annotationMetadata, value.getAnnotationMetadata()); - VisitorContextUtils.contributeRepeatable(this.annotationMetadata, value.getGenericType()); if (value.hasDeclaredAnnotation(InjectScope.class)) { hasInjectScope = true; } @@ -2610,6 +2600,7 @@ private void pushInvokeGetPropertyValueForSetter(GeneratorAdapter injectMethodVi resolveMethodArgument(injectMethodVisitor, currentMethodIndex, 0); } else { pushCreateArgument( + this.annotationMetadata, beanFullClassName, beanDefinitionType, classWriter, @@ -2650,6 +2641,7 @@ private void pushInvokeGetBeanForSetter(GeneratorAdapter injectMethodVisitor, St resolveMethodArgument(injectMethodVisitor, currentMethodIndex, 0); } else { pushCreateArgument( + this.annotationMetadata, beanFullClassName, beanDefinitionType, classWriter, @@ -2691,6 +2683,7 @@ private void pushInvokeGetBeansOfTypeForSetter(GeneratorAdapter injectMethodVisi resolveMethodArgument(injectMethodVisitor, currentMethodIndex, 0); } else { pushCreateArgument( + this.annotationMetadata, beanFullClassName, beanDefinitionType, classWriter, @@ -2742,6 +2735,7 @@ private void pushInvokeGetPropertyPlaceholderValueForSetter(GeneratorAdapter inj resolveMethodArgument(injectMethodVisitor, currentMethodIndex, 0); } else { pushCreateArgument( + this.annotationMetadata, beanFullClassName, beanDefinitionType, classWriter, @@ -2776,8 +2770,6 @@ private void removeAnnotations(AnnotationMetadata annotationMetadata, String... private void applyDefaultNamedToParameters(List argumentTypes) { for (ParameterElement parameterElement : argumentTypes) { final AnnotationMetadata annotationMetadata = parameterElement.getAnnotationMetadata(); - MutableAnnotationMetadata.contributeDefaults(this.annotationMetadata, annotationMetadata); - VisitorContextUtils.contributeRepeatable(this.annotationMetadata, parameterElement.getGenericType()); autoApplyNamedIfPresent(parameterElement, annotationMetadata); } } @@ -2950,7 +2942,7 @@ private void writeInterceptedLifecycleMethod( lookupReferenceAnnotationMetadata(getAnnotationMetadata); // now define the invokerInternal method - final GeneratorAdapter invokeMethod = startPublicMethod(postConstructInnerWriter, ExecutableMethodWriter.METHOD_INVOKE_INTERNAL); + final GeneratorAdapter invokeMethod = startPublicMethod(postConstructInnerWriter, METHOD_INVOKE_INTERNAL); invokeMethod.loadThis(); // load the bean definition field invokeMethod.getField(postConstructInnerClassType, fieldBeanDef, beanDefinitionType); @@ -3990,9 +3982,6 @@ private void pushBeanDefinitionMethodInvocation(GeneratorAdapter buildMethodVisi private void visitBeanDefinitionConstructorInternal(GeneratorAdapter staticInit, Object constructor) { if (constructor instanceof MethodElement methodElement) { - AnnotationMetadata constructorMetadata = methodElement.getAnnotationMetadata(); - MutableAnnotationMetadata.contributeDefaults(this.annotationMetadata, constructorMetadata); - VisitorContextUtils.contributeRepeatable(this.annotationMetadata, methodElement.getGenericReturnType()); ParameterElement[] parameters = methodElement.getParameters(); List parameterList = Arrays.asList(parameters); applyDefaultNamedToParameters(parameterList); @@ -4150,10 +4139,6 @@ private void pushNewMethodReference(GeneratorAdapter staticInit, boolean isPostConstructMethod, boolean isPreDestroyMethod) { annotationMetadata = annotationMetadata.getTargetAnnotationMetadata(); - for (ParameterElement value : methodElement.getParameters()) { - MutableAnnotationMetadata.contributeDefaults(this.annotationMetadata, value.getAnnotationMetadata()); - VisitorContextUtils.contributeRepeatable(this.annotationMetadata, value.getGenericType()); - } staticInit.newInstance(Type.getType(AbstractInitializableBeanDefinition.MethodReference.class)); staticInit.dup(); // 1: declaringType @@ -4165,6 +4150,7 @@ private void pushNewMethodReference(GeneratorAdapter staticInit, staticInit.visitInsn(ACONST_NULL); } else { pushBuildArgumentsForMethod( + this.annotationMetadata, beanFullClassName, beanDefinitionType, classWriter, @@ -4197,6 +4183,7 @@ private void pushNewFieldReference(GeneratorAdapter staticInit, Type declaringTy staticInit.push(declaringType); // 2: argument pushCreateArgument( + this.annotationMetadata, beanFullClassName, beanDefinitionType, classWriter, @@ -4227,8 +4214,15 @@ private void pushNewAnnotationReference(GeneratorAdapter staticInit, Type refere ANNOTATION_REFERENCE_CONSTRUCTOR); } - private void pushAnnotationMetadata(GeneratorAdapter staticInit, AnnotationMetadata annotationMetadata) { + private void pushAnnotationMetadata(GeneratorAdapter staticInit, + AnnotationMetadata annotationMetadata) { annotationMetadata = annotationMetadata.getTargetAnnotationMetadata(); +// +// MutableAnnotationMetadata.contributeDefaults( +// this.annotationMetadata, +// annotationMetadata +// ); + if (annotationMetadata == AnnotationMetadata.EMPTY_METADATA || annotationMetadata.isEmpty()) { staticInit.push((String) null); } else if (annotationMetadata instanceof AnnotationMetadataHierarchy annotationMetadataHierarchy) { @@ -4675,18 +4669,7 @@ public boolean isPreDestroy() { } - private static class FactoryMethodDef { - private final Type factoryType; - private final Element factoryMethod; - private final String methodDescriptor; - private final int factoryVar; - - public FactoryMethodDef(Type factoryType, Element factoryMethod, String methodDescriptor, int factoryVar) { - this.factoryType = factoryType; - this.factoryMethod = factoryMethod; - this.methodDescriptor = methodDescriptor; - this.factoryVar = factoryVar; - } + private record FactoryMethodDef(Type factoryType, Element factoryMethod, String methodDescriptor, int factoryVar) { } private static class InnerClassDef { diff --git a/core-processor/src/main/java/io/micronaut/inject/writer/ExecutableMethodWriter.java b/core-processor/src/main/java/io/micronaut/inject/writer/ExecutableMethodWriter.java deleted file mode 100644 index 86204bcb77a..00000000000 --- a/core-processor/src/main/java/io/micronaut/inject/writer/ExecutableMethodWriter.java +++ /dev/null @@ -1,509 +0,0 @@ -/* - * Copyright 2017-2020 original authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.micronaut.inject.writer; - -import io.micronaut.context.AbstractExecutableMethod; -import io.micronaut.core.annotation.AnnotationMetadata; -import io.micronaut.core.annotation.Internal; -import io.micronaut.core.annotation.NonNull; -import io.micronaut.core.reflect.ReflectionUtils; -import io.micronaut.core.type.Argument; -import io.micronaut.inject.ExecutableMethod; -import io.micronaut.inject.annotation.AnnotationMetadataReference; -import io.micronaut.inject.annotation.MutableAnnotationMetadata; -import io.micronaut.inject.ast.ClassElement; -import io.micronaut.inject.ast.MethodElement; -import io.micronaut.inject.ast.ParameterElement; -import io.micronaut.inject.ast.TypedElement; -import io.micronaut.inject.processing.JavaModelUtils; -import io.micronaut.inject.visitor.util.VisitorContextUtils; -import org.objectweb.asm.ClassWriter; -import org.objectweb.asm.Label; -import org.objectweb.asm.MethodVisitor; -import org.objectweb.asm.Opcodes; -import org.objectweb.asm.Type; -import org.objectweb.asm.commons.GeneratorAdapter; -import org.objectweb.asm.commons.Method; - -import java.io.IOException; -import java.io.OutputStream; -import java.util.Arrays; -import java.util.Collection; -import java.util.HashMap; -import java.util.Iterator; -import java.util.List; -import java.util.Locale; - -/** - * Writes out {@link io.micronaut.inject.ExecutableMethod} implementations. - * - * @author Graeme Rocher - * @since 1.0 - */ -@Internal -public class ExecutableMethodWriter extends AbstractAnnotationMetadataWriter implements Opcodes { - - public static final org.objectweb.asm.commons.Method METHOD_INVOKE_INTERNAL = org.objectweb.asm.commons.Method.getMethod( - ReflectionUtils.getRequiredInternalMethod(AbstractExecutableMethod.class, "invokeInternal", Object.class, Object[].class)); - protected static final org.objectweb.asm.commons.Method METHOD_IS_ABSTRACT = org.objectweb.asm.commons.Method.getMethod( - ReflectionUtils.getRequiredInternalMethod(ExecutableMethod.class, "isAbstract")); - protected static final org.objectweb.asm.commons.Method METHOD_IS_SUSPEND = org.objectweb.asm.commons.Method.getMethod( - ReflectionUtils.getRequiredInternalMethod(ExecutableMethod.class, "isSuspend")); - protected static final Method METHOD_GET_TARGET = Method.getMethod("java.lang.reflect.Method resolveTargetMethod()"); - private static final Type TYPE_REFLECTION_UTILS = Type.getType(ReflectionUtils.class); - private static final org.objectweb.asm.commons.Method METHOD_GET_REQUIRED_METHOD = org.objectweb.asm.commons.Method.getMethod( - ReflectionUtils.getRequiredInternalMethod(ReflectionUtils.class, "getRequiredMethod", Class.class, String.class, Class[].class)); - private static final String FIELD_INTERCEPTABLE = "$interceptable"; - - protected final Type methodType; - - private final ClassWriter classWriter; - private final String className; - private final String internalName; - private final boolean isInterface; - private final boolean isAbstract; - private final boolean isSuspend; - private final boolean isDefault; - private final String interceptedProxyClassName; - private final String interceptedProxyBridgeMethodName; - - /** - * @param methodClassName The method class name - * @param isInterface Whether is an interface - * @param isAbstract Whether the method is abstract - * @param isDefault Whether the method is a default method - * @param isSuspend Whether the method is Kotlin suspend function - * @param originatingElements The originating elements - * @param annotationMetadata The annotation metadata - * @param interceptedProxyClassName The intercepted proxy class name - * @param interceptedProxyBridgeMethodName The intercepted proxy bridge method name - */ - public ExecutableMethodWriter( - String methodClassName, - boolean isInterface, - boolean isAbstract, - boolean isDefault, - boolean isSuspend, - OriginatingElements originatingElements, - AnnotationMetadata annotationMetadata, - String interceptedProxyClassName, - String interceptedProxyBridgeMethodName) { - super(methodClassName, originatingElements, annotationMetadata, true); - this.classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS | ClassWriter.COMPUTE_FRAMES); - this.className = methodClassName; - this.internalName = getInternalName(methodClassName); - this.methodType = getObjectType(methodClassName); - this.isInterface = isInterface; - this.isAbstract = isAbstract; - this.isDefault = isDefault; - this.isSuspend = isSuspend; - this.interceptedProxyClassName = interceptedProxyClassName; - this.interceptedProxyBridgeMethodName = interceptedProxyBridgeMethodName; - } - - /** - * @return Is supports intercepted proxy. - */ - public boolean isSupportsInterceptedProxy() { - return interceptedProxyClassName != null; - } - - /** - * @return Is the method abstract. - */ - public boolean isAbstract() { - return isAbstract; - } - - /** - * @return Is the method in an interface. - */ - public boolean isInterface() { - return isInterface; - } - - /** - * @return Is the method a default method. - */ - public boolean isDefault() { - return isDefault; - } - - /** - * @return Is the method suspend. - */ - public boolean isSuspend() { - return isSuspend; - } - - /** - * @return The class name - */ - public String getClassName() { - return className; - } - - /** - * @return The internal name - */ - public String getInternalName() { - return internalName; - } - - /** - * Write the method. - * - * @param declaringType The declaring type - * @param methodElement The method element - */ - public void visitMethod(TypedElement declaringType, - MethodElement methodElement) { - String methodName = methodElement.getName(); - List argumentTypes = Arrays.asList(methodElement.getSuspendParameters()); - Type declaringTypeObject = JavaModelUtils.getTypeReference(declaringType); - boolean hasArgs = !argumentTypes.isEmpty(); - - classWriter.visit(V1_8, ACC_SYNTHETIC, - internalName, - null, - Type.getInternalName(AbstractExecutableMethod.class), - null); - - classWriter.visitAnnotation(TYPE_GENERATED.getDescriptor(), false); - - // initialize and write the annotation metadata - if (!(annotationMetadata instanceof AnnotationMetadataReference)) { - writeAnnotationMetadataStaticInitializer(classWriter); - } - writeGetAnnotationMetadataMethod(classWriter); - - MethodVisitor executorMethodConstructor; - GeneratorAdapter constructorWriter; - if (interceptedProxyBridgeMethodName != null) { - // Create default constructor call other one with 'false' - - String descriptor = Type.getDescriptor(boolean.class); - classWriter.visitField(ACC_FINAL | ACC_PRIVATE, FIELD_INTERCEPTABLE, descriptor, null, null); - - GeneratorAdapter defaultConstructorWriter = new GeneratorAdapter(startConstructor(classWriter), - Opcodes.ACC_PUBLIC, - CONSTRUCTOR_NAME, - DESCRIPTOR_DEFAULT_CONSTRUCTOR); - - String executorMethodConstructorDescriptor = getConstructorDescriptor(boolean.class); - executorMethodConstructor = startConstructor(classWriter, boolean.class); - constructorWriter = new GeneratorAdapter(executorMethodConstructor, - Opcodes.ACC_PUBLIC, - CONSTRUCTOR_NAME, - executorMethodConstructorDescriptor); - - defaultConstructorWriter.loadThis(); - defaultConstructorWriter.push(false); - defaultConstructorWriter.visitMethodInsn(INVOKESPECIAL, internalName, CONSTRUCTOR_NAME, executorMethodConstructorDescriptor, false); - defaultConstructorWriter.visitInsn(RETURN); - defaultConstructorWriter.visitMaxs(DEFAULT_MAX_STACK, 1); - - constructorWriter.loadThis(); - constructorWriter.loadArg(0); - constructorWriter.putField(Type.getObjectType(internalName), FIELD_INTERCEPTABLE, Type.getType(boolean.class)); - } else { - executorMethodConstructor = startConstructor(classWriter); - constructorWriter = new GeneratorAdapter(executorMethodConstructor, - Opcodes.ACC_PUBLIC, - CONSTRUCTOR_NAME, - DESCRIPTOR_DEFAULT_CONSTRUCTOR); - } - - // ALOAD 0 - constructorWriter.loadThis(); - - // load 'this' - constructorWriter.loadThis(); - - // 1st argument: the declaring class - constructorWriter.push(declaringTypeObject); - - // 2nd argument: the method name - constructorWriter.push(methodName); - - // 3rd argument the generic return type - ClassElement genericReturnType = methodElement.getGenericReturnType(); - if (genericReturnType.isPrimitive() && !genericReturnType.isArray()) { - String constantName = genericReturnType.getName().toUpperCase(Locale.ENGLISH); - - // refer to constant for primitives - Type type = Type.getType(Argument.class); - constructorWriter.getStatic(type, constantName, type); - - } else { - pushCreateArgument( - declaringType.getName(), - methodType, - classWriter, - constructorWriter, - genericReturnType.getName(), - genericReturnType, - genericReturnType.getAnnotationMetadata(), - genericReturnType.getTypeArguments(), - new HashMap<>(), - loadTypeMethods - ); - } - - if (hasArgs) { - // 4th argument: the generic types - pushBuildArgumentsForMethod( - methodType.getClassName(), - methodType, - classWriter, - constructorWriter, - argumentTypes, - new HashMap<>(), - loadTypeMethods - ); - - for (ParameterElement pe : argumentTypes) { - MutableAnnotationMetadata.contributeDefaults(this.annotationMetadata, pe.getAnnotationMetadata()); - VisitorContextUtils.contributeRepeatable(this.annotationMetadata, pe.getGenericType()); - } - // now invoke super(..) if no arg constructor - invokeConstructor( - executorMethodConstructor, - AbstractExecutableMethod.class, - Class.class, - String.class, - Argument.class, - Argument[].class); - } else { - invokeConstructor( - executorMethodConstructor, - AbstractExecutableMethod.class, - Class.class, - String.class, - Argument.class); - } - constructorWriter.visitInsn(RETURN); - constructorWriter.visitMaxs(DEFAULT_MAX_STACK, 1); - - // add isAbstract method - GeneratorAdapter isAbstractMethod = new GeneratorAdapter(classWriter.visitMethod( - ACC_PUBLIC | ACC_FINAL, - METHOD_IS_ABSTRACT.getName(), - METHOD_IS_ABSTRACT.getDescriptor(), - null, - null), - ACC_PUBLIC, - METHOD_IS_ABSTRACT.getName(), - METHOD_IS_ABSTRACT.getDescriptor() - ); - - isAbstractMethod.push(isAbstract()); - isAbstractMethod.returnValue(); - isAbstractMethod.visitMaxs(1, 1); - isAbstractMethod.endMethod(); - - // add isSuspend method - GeneratorAdapter isSuspendMethod = new GeneratorAdapter(classWriter.visitMethod( - ACC_PUBLIC | ACC_FINAL, - METHOD_IS_SUSPEND.getName(), - METHOD_IS_SUSPEND.getDescriptor(), - null, - null), - ACC_PUBLIC, - METHOD_IS_SUSPEND.getName(), - METHOD_IS_SUSPEND.getDescriptor() - ); - - isSuspendMethod.push(isSuspend()); - isSuspendMethod.returnValue(); - isSuspendMethod.visitMaxs(1, 1); - isSuspendMethod.endMethod(); - - // invoke the methods with the passed arguments - String invokeDescriptor = METHOD_INVOKE_INTERNAL.getDescriptor(); - String invokeInternalName = METHOD_INVOKE_INTERNAL.getName(); - GeneratorAdapter invokeMethod = new GeneratorAdapter(classWriter.visitMethod( - Opcodes.ACC_PUBLIC, - invokeInternalName, - invokeDescriptor, - null, - null), - ACC_PUBLIC, - invokeInternalName, - invokeDescriptor - ); - - ClassElement returnType = methodElement.isSuspend() ? ClassElement.of(Object.class) : methodElement.getReturnType(); - buildInvokeMethod(declaringTypeObject, methodName, returnType, argumentTypes, invokeMethod); - - buildResolveTargetMethod(methodName, declaringTypeObject, hasArgs, argumentTypes); - - for (GeneratorAdapter method : loadTypeMethods.values()) { - method.visitMaxs(3, 1); - method.visitEnd(); - } - } - - @Override - public void accept(ClassWriterOutputVisitor classWriterOutputVisitor) throws IOException { - try (OutputStream outputStream = classWriterOutputVisitor.visitClass(className, getOriginatingElements())) { - outputStream.write(classWriter.toByteArray()); - } - } - - @NonNull - @Override - protected final GeneratorAdapter beginAnnotationMetadataMethod(ClassWriter classWriter) { - return startProtectedMethod(classWriter, "resolveAnnotationMetadata", AnnotationMetadata.class.getName()); - } - - /** - * @param declaringTypeObject The declaring object type - * @param methodName The method name - * @param returnType The return type - * @param argumentTypes The argument types - * @param invokeMethodVisitor The invoke method visitor - */ - protected void buildInvokeMethod( - Type declaringTypeObject, - String methodName, - ClassElement returnType, - Collection argumentTypes, - GeneratorAdapter invokeMethodVisitor) { - Type returnTypeObject = JavaModelUtils.getTypeReference(returnType); - - // load this - invokeMethodVisitor.visitVarInsn(ALOAD, 1); - // duplicate target - invokeMethodVisitor.dup(); - - String methodDescriptor = getMethodDescriptor(returnType, argumentTypes); - if (interceptedProxyClassName != null) { - Label invokeTargetBlock = new Label(); - - Type interceptedProxyType = getObjectType(interceptedProxyClassName); - - // load this.$interceptable field value - invokeMethodVisitor.loadThis(); - invokeMethodVisitor.getField(Type.getObjectType(internalName), FIELD_INTERCEPTABLE, Type.getType(boolean.class)); - // check if it equals true - invokeMethodVisitor.push(true); - invokeMethodVisitor.ifCmp(Type.BOOLEAN_TYPE, GeneratorAdapter.NE, invokeTargetBlock); - - // target instanceOf intercepted proxy - invokeMethodVisitor.loadArg(0); - invokeMethodVisitor.instanceOf(interceptedProxyType); - // check if instanceOf - invokeMethodVisitor.push(true); - invokeMethodVisitor.ifCmp(Type.BOOLEAN_TYPE, GeneratorAdapter.NE, invokeTargetBlock); - - pushCastToType(invokeMethodVisitor, interceptedProxyType); - - // load arguments - Iterator iterator = argumentTypes.iterator(); - for (int i = 0; i < argumentTypes.size(); i++) { - invokeMethodVisitor.loadArg(1); - invokeMethodVisitor.push(i); - invokeMethodVisitor.visitInsn(AALOAD); - - pushCastToType(invokeMethodVisitor, iterator.next()); - } - - invokeMethodVisitor.visitMethodInsn(INVOKEVIRTUAL, - interceptedProxyType.getInternalName(), interceptedProxyBridgeMethodName, - methodDescriptor, false); - - if (returnTypeObject.equals(Type.VOID_TYPE)) { - invokeMethodVisitor.visitInsn(ACONST_NULL); - } else { - pushBoxPrimitiveIfNecessary(returnType, invokeMethodVisitor); - } - invokeMethodVisitor.visitInsn(ARETURN); - - invokeMethodVisitor.visitLabel(invokeTargetBlock); - - // remove parent - invokeMethodVisitor.pop(); - } - - pushCastToType(invokeMethodVisitor, declaringTypeObject); - boolean hasArgs = !argumentTypes.isEmpty(); - if (hasArgs) { - int argCount = argumentTypes.size(); - Iterator argIterator = argumentTypes.iterator(); - for (int i = 0; i < argCount; i++) { - invokeMethodVisitor.visitVarInsn(ALOAD, 2); - invokeMethodVisitor.push(i); - invokeMethodVisitor.visitInsn(AALOAD); - // cast the return value to the correct type - pushCastToType(invokeMethodVisitor, argIterator.next()); - } - } - - invokeMethodVisitor.visitMethodInsn(isInterface ? INVOKEINTERFACE : INVOKEVIRTUAL, - declaringTypeObject.getInternalName(), methodName, - methodDescriptor, isInterface); - - if (returnTypeObject.equals(Type.VOID_TYPE)) { - invokeMethodVisitor.visitInsn(ACONST_NULL); - } else { - pushBoxPrimitiveIfNecessary(returnType, invokeMethodVisitor); - } - invokeMethodVisitor.visitInsn(ARETURN); - - invokeMethodVisitor.visitMaxs(DEFAULT_MAX_STACK, 1); - invokeMethodVisitor.visitEnd(); - } - - private void buildResolveTargetMethod(String methodName, Type declaringTypeObject, boolean hasArgs, Collection argumentTypeClasses) { - String targetMethodInternalName = METHOD_GET_TARGET.getName(); - String targetMethodDescriptor = METHOD_GET_TARGET.getDescriptor(); - GeneratorAdapter getTargetMethod = new GeneratorAdapter(classWriter.visitMethod( - ACC_PUBLIC | ACC_FINAL, - targetMethodInternalName, - targetMethodDescriptor, - null, - null), - ACC_PUBLIC | ACC_FINAL, - targetMethodInternalName, - targetMethodDescriptor - ); - - getTargetMethod.push(declaringTypeObject); - getTargetMethod.push(methodName); - if (hasArgs) { - int len = argumentTypeClasses.size(); - Iterator iter = argumentTypeClasses.iterator(); - pushNewArray(getTargetMethod, Class.class, len); - for (int i = 0; i < len; i++) { - ParameterElement type = iter.next(); - pushStoreInArray( - getTargetMethod, - i, - len, - () -> getTargetMethod.push(JavaModelUtils.getTypeReference(type)) - ); - - } - } else { - getTargetMethod.getStatic(TYPE_REFLECTION_UTILS, "EMPTY_CLASS_ARRAY", Type.getType(Class[].class)); - } - getTargetMethod.invokeStatic(TYPE_REFLECTION_UTILS, METHOD_GET_REQUIRED_METHOD); - getTargetMethod.returnValue(); - getTargetMethod.visitMaxs(1, 1); - getTargetMethod.endMethod(); - } -} diff --git a/core-processor/src/main/java/io/micronaut/inject/writer/ExecutableMethodsDefinitionWriter.java b/core-processor/src/main/java/io/micronaut/inject/writer/ExecutableMethodsDefinitionWriter.java index 1e03bdfadcf..1cd18628e23 100644 --- a/core-processor/src/main/java/io/micronaut/inject/writer/ExecutableMethodsDefinitionWriter.java +++ b/core-processor/src/main/java/io/micronaut/inject/writer/ExecutableMethodsDefinitionWriter.java @@ -97,10 +97,16 @@ public class ExecutableMethodsDefinitionWriter extends AbstractClassFileWriter i private final Set methodNames = new HashSet<>(); - public ExecutableMethodsDefinitionWriter(String beanDefinitionClassName, + private final AnnotationMetadata annotationMetadataWithDefaults; + + private ClassWriter classWriter; + + public ExecutableMethodsDefinitionWriter(AnnotationMetadata annotationMetadataWithDefaults, + String beanDefinitionClassName, String beanDefinitionReferenceClassName, OriginatingElements originatingElements) { super(originatingElements); + this.annotationMetadataWithDefaults = annotationMetadataWithDefaults; this.className = beanDefinitionClassName + CLASS_SUFFIX; this.internalName = getInternalName(className); this.thisType = Type.getObjectType(internalName); @@ -212,7 +218,16 @@ public int visitExecutableMethod(TypedElement declaringType, @Override public void accept(ClassWriterOutputVisitor classWriterOutputVisitor) throws IOException { - ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS | ClassWriter.COMPUTE_FRAMES); + try (OutputStream outputStream = classWriterOutputVisitor.visitClass(className, getOriginatingElements())) { + outputStream.write(classWriter.toByteArray()); + } + } + + /** + * Invoke to build the class model. + */ + public final void visitDefinitionEnd() { + classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS | ClassWriter.COMPUTE_FRAMES); classWriter.visit(V1_8, ACC_SYNTHETIC | ACC_FINAL, internalName, null, @@ -236,10 +251,6 @@ public void accept(ClassWriterOutputVisitor classWriterOutputVisitor) throws IOE } classWriter.visitEnd(); - - try (OutputStream outputStream = classWriterOutputVisitor.visitClass(className, getOriginatingElements())) { - outputStream.write(classWriter.toByteArray()); - } } private void buildStaticInit(ClassWriter classWriter, Type methodsFieldType) { @@ -416,18 +427,19 @@ private void pushNewMethodReference0(ClassWriter classWriter, } } - pushAnnotationMetadata(classWriter, staticInit, annotationMetadata, defaultsStorage); + pushAnnotationMetadata(annotationMetadataWithDefaults, classWriter, staticInit, annotationMetadata, defaultsStorage); // 3: methodName staticInit.push(methodElement.getName()); // 4: return argument ClassElement genericReturnType = methodElement.getGenericReturnType(); - pushReturnTypeArgument(thisType, classWriter, staticInit, declaringType.getName(), genericReturnType, defaultsStorage, loadTypeMethods); + pushReturnTypeArgument(annotationMetadataWithDefaults, thisType, classWriter, staticInit, declaringType.getName(), genericReturnType, defaultsStorage, loadTypeMethods); // 5: arguments ParameterElement[] parameters = methodElement.getSuspendParameters(); if (parameters.length == 0) { staticInit.visitInsn(ACONST_NULL); } else { pushBuildArgumentsForMethod( + annotationMetadataWithDefaults, typeReference.getClassName(), thisType, classWriter, @@ -454,16 +466,23 @@ private void pushNewMethodReference0(ClassWriter classWriter, boolean.class); } - private void pushAnnotationMetadata(ClassWriter classWriter, + private void pushAnnotationMetadata(AnnotationMetadata annotationMetadataWithDefaults, + ClassWriter classWriter, GeneratorAdapter staticInit, AnnotationMetadata annotationMetadata, Map defaultsStorage) { + if (annotationMetadata == AnnotationMetadata.EMPTY_METADATA || annotationMetadata.isEmpty()) { staticInit.push((String) null); } else if (annotationMetadata instanceof AnnotationMetadataReference annotationMetadataReference) { String className = annotationMetadataReference.getClassName(); staticInit.getStatic(getTypeReferenceForName(className), AbstractAnnotationMetadataWriter.FIELD_ANNOTATION_METADATA, Type.getType(AnnotationMetadata.class)); } else if (annotationMetadata instanceof AnnotationMetadataHierarchy annotationMetadataHierarchy) { + MutableAnnotationMetadata.contributeDefaults( + annotationMetadataWithDefaults, + annotationMetadataHierarchy + ); + AnnotationMetadataWriter.instantiateNewMetadataHierarchy( thisType, classWriter, @@ -472,6 +491,11 @@ private void pushAnnotationMetadata(ClassWriter classWriter, defaultsStorage, loadTypeMethods); } else if (annotationMetadata instanceof MutableAnnotationMetadata mutableAnnotationMetadata) { + MutableAnnotationMetadata.contributeDefaults( + annotationMetadataWithDefaults, + annotationMetadata + ); + AnnotationMetadataWriter.instantiateNewMetadata( thisType, classWriter, diff --git a/inject-java-test/src/test/groovy/io/micronaut/inject/visitor/beans/BeanIntrospectorSpec.groovy b/inject-java-test/src/test/groovy/io/micronaut/inject/visitor/beans/BeanIntrospectorSpec.groovy index ecd21bfad77..67ea45423ad 100644 --- a/inject-java-test/src/test/groovy/io/micronaut/inject/visitor/beans/BeanIntrospectorSpec.groovy +++ b/inject-java-test/src/test/groovy/io/micronaut/inject/visitor/beans/BeanIntrospectorSpec.groovy @@ -69,4 +69,11 @@ class BeanIntrospectorSpec extends Specification { then: def ex = thrown(IllegalArgumentException) } + + void "test repeatable inner type annotation"() { + when: + BeanIntrospection introspection = BeanIntrospection.getIntrospection(MapOfListsWithAutomaticUnwrapping) + then: + introspection.getAnnotationMetadata().findRepeatableAnnotation(MyMin).isPresent() + } } diff --git a/inject-java-test/src/test/groovy/io/micronaut/inject/visitor/beans/MapOfListsWithAutomaticUnwrapping.java b/inject-java-test/src/test/groovy/io/micronaut/inject/visitor/beans/MapOfListsWithAutomaticUnwrapping.java new file mode 100644 index 00000000000..4f374cca866 --- /dev/null +++ b/inject-java-test/src/test/groovy/io/micronaut/inject/visitor/beans/MapOfListsWithAutomaticUnwrapping.java @@ -0,0 +1,14 @@ +package io.micronaut.inject.visitor.beans; + +import io.micronaut.core.annotation.Introspected; + +import java.util.List; +import java.util.Map; +import java.util.OptionalInt; + +@Introspected(accessKind = Introspected.AccessKind.FIELD, visibility = Introspected.Visibility.ANY) +class MapOfListsWithAutomaticUnwrapping { + + private Map> map; + +} diff --git a/inject-java-test/src/test/groovy/io/micronaut/inject/visitor/beans/MyMin.java b/inject-java-test/src/test/groovy/io/micronaut/inject/visitor/beans/MyMin.java new file mode 100644 index 00000000000..6eac03e98ab --- /dev/null +++ b/inject-java-test/src/test/groovy/io/micronaut/inject/visitor/beans/MyMin.java @@ -0,0 +1,29 @@ +package io.micronaut.inject.visitor.beans; + +import java.lang.annotation.Documented; +import java.lang.annotation.Repeatable; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.ANNOTATION_TYPE; +import static java.lang.annotation.ElementType.CONSTRUCTOR; +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_USE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE }) +@Retention(RUNTIME) +@Repeatable(MyMin.List.class) +@Documented +public @interface MyMin { + + @Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE }) + @Retention(RUNTIME) + @Documented + @interface List { + + MyMin[] value(); + } +} diff --git a/inject-java/src/test/groovy/io/micronaut/inject/beans/BeanDefinitionSpec.groovy b/inject-java/src/test/groovy/io/micronaut/inject/beans/BeanDefinitionSpec.groovy index a45d5eaebce..636d4c1e489 100644 --- a/inject-java/src/test/groovy/io/micronaut/inject/beans/BeanDefinitionSpec.groovy +++ b/inject-java/src/test/groovy/io/micronaut/inject/beans/BeanDefinitionSpec.groovy @@ -592,4 +592,59 @@ interface Deserializer { !deserializerTypeParam.isTypeVariable() !(deserializerTypeParam instanceof GenericPlaceholder) } + + void "test repeatable inner type annotation 1"() { + when: + def ctx = ApplicationContext.builder().properties(["repeatabletest": "true"]).build().start() + def beanDef = ctx.getBeanDefinition(MapOfListsBean1) + then: + beanDef.getAnnotationMetadata().findRepeatableAnnotation(MyMin1).isPresent() + + cleanup: + ctx.close() + } + + void "test repeatable inner type annotation 2"() { + when: + def ctx = ApplicationContext.builder().properties(["repeatabletest": "true"]).build().start() + def beanDef = ctx.getBeanDefinition(MapOfListsBean2) + then: + beanDef.getAnnotationMetadata().findRepeatableAnnotation(MyMin2).isPresent() + + cleanup: + ctx.close() + } + + void "test repeatable inner type annotation 3"() { + when: + def ctx = ApplicationContext.builder().properties(["repeatabletest": "true"]).build().start() + def beanDef = ctx.getBeanDefinition(MapOfListsBean3) + then: + beanDef.getAnnotationMetadata().findRepeatableAnnotation(MyMin3).isPresent() + + cleanup: + ctx.close() + } + + void "test repeatable inner type annotation 4"() { + when: + def ctx = ApplicationContext.builder().properties(["repeatabletest": "true"]).build().start() + def beanDef = ctx.getBeanDefinition(MapOfListsBean4) + then: + beanDef.getAnnotationMetadata().findRepeatableAnnotation(MyMin4).isPresent() + + cleanup: + ctx.close() + } + + void "test repeatable inner type annotation 5"() { + when: + def ctx = ApplicationContext.builder().properties(["repeatabletest": "true"]).build().start() + def beanDef = ctx.getBeanDefinition(MapOfListsBean5) + then: + beanDef.getAnnotationMetadata().findRepeatableAnnotation(MyMin5).isPresent() + + cleanup: + ctx.close() + } } diff --git a/inject-java/src/test/groovy/io/micronaut/inject/beans/MapOfListsBean1.java b/inject-java/src/test/groovy/io/micronaut/inject/beans/MapOfListsBean1.java new file mode 100644 index 00000000000..3c72a4abd1b --- /dev/null +++ b/inject-java/src/test/groovy/io/micronaut/inject/beans/MapOfListsBean1.java @@ -0,0 +1,19 @@ +package io.micronaut.inject.beans; + +import io.micronaut.context.annotation.Executable; +import io.micronaut.context.annotation.Requires; +import jakarta.inject.Singleton; + +import java.util.List; +import java.util.Map; +import java.util.OptionalInt; + +@Requires(property = "repeatabletest", value = "true") +@Singleton +class MapOfListsBean1 { + + @Executable + void myMethod(Map> map) { + } + +} diff --git a/inject-java/src/test/groovy/io/micronaut/inject/beans/MapOfListsBean2.java b/inject-java/src/test/groovy/io/micronaut/inject/beans/MapOfListsBean2.java new file mode 100644 index 00000000000..873c0964c04 --- /dev/null +++ b/inject-java/src/test/groovy/io/micronaut/inject/beans/MapOfListsBean2.java @@ -0,0 +1,19 @@ +package io.micronaut.inject.beans; + +import io.micronaut.context.annotation.Executable; +import io.micronaut.context.annotation.Requires; +import jakarta.inject.Singleton; + +import java.util.List; +import java.util.Map; +import java.util.OptionalInt; + +@Requires(property = "repeatabletest", value = "true") +@Singleton +class MapOfListsBean2 { + + @Executable + void myMethod(Map>>>>> map) { + } + +} diff --git a/inject-java/src/test/groovy/io/micronaut/inject/beans/MapOfListsBean3.java b/inject-java/src/test/groovy/io/micronaut/inject/beans/MapOfListsBean3.java new file mode 100644 index 00000000000..3bf11854dbc --- /dev/null +++ b/inject-java/src/test/groovy/io/micronaut/inject/beans/MapOfListsBean3.java @@ -0,0 +1,20 @@ +package io.micronaut.inject.beans; + +import io.micronaut.context.annotation.Executable; +import io.micronaut.context.annotation.Requires; +import jakarta.inject.Singleton; + +import java.util.List; +import java.util.Map; +import java.util.OptionalInt; + +@Requires(property = "repeatabletest", value = "true") +@Singleton +class MapOfListsBean3 { + + @Executable + Map>>>>> myMethod() { + return null; + } + +} diff --git a/inject-java/src/test/groovy/io/micronaut/inject/beans/MapOfListsBean4.java b/inject-java/src/test/groovy/io/micronaut/inject/beans/MapOfListsBean4.java new file mode 100644 index 00000000000..288123d49b0 --- /dev/null +++ b/inject-java/src/test/groovy/io/micronaut/inject/beans/MapOfListsBean4.java @@ -0,0 +1,19 @@ +package io.micronaut.inject.beans; + +import io.micronaut.context.annotation.Requires; +import jakarta.inject.Singleton; + +import java.util.List; +import java.util.Map; +import java.util.OptionalInt; + +@Requires(property = "repeatabletest", value = "true") +@Singleton +class MapOfListsBean4 { + + private final Map>>>>> map; + + MapOfListsBean4(Map>>>>> map) { + this.map = map; + } +} diff --git a/inject-java/src/test/groovy/io/micronaut/inject/beans/MapOfListsBean5.java b/inject-java/src/test/groovy/io/micronaut/inject/beans/MapOfListsBean5.java new file mode 100644 index 00000000000..adc457a1738 --- /dev/null +++ b/inject-java/src/test/groovy/io/micronaut/inject/beans/MapOfListsBean5.java @@ -0,0 +1,17 @@ +package io.micronaut.inject.beans; + +import io.micronaut.context.annotation.Requires; +import jakarta.inject.Inject; +import jakarta.inject.Singleton; + +import java.util.List; +import java.util.Map; +import java.util.OptionalInt; + +@Singleton +@Requires(property = "repeatabletest", value = "true") +class MapOfListsBean5 { + + @Inject + public Map>>>>> map; +} diff --git a/inject-java/src/test/groovy/io/micronaut/inject/beans/MyMin1.java b/inject-java/src/test/groovy/io/micronaut/inject/beans/MyMin1.java new file mode 100644 index 00000000000..856982aac34 --- /dev/null +++ b/inject-java/src/test/groovy/io/micronaut/inject/beans/MyMin1.java @@ -0,0 +1,29 @@ +package io.micronaut.inject.beans; + +import java.lang.annotation.Documented; +import java.lang.annotation.Repeatable; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.ANNOTATION_TYPE; +import static java.lang.annotation.ElementType.CONSTRUCTOR; +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_USE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE }) +@Retention(RUNTIME) +@Repeatable(MyMin1.List.class) +@Documented +public @interface MyMin1 { + + @Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE }) + @Retention(RUNTIME) + @Documented + @interface List { + + MyMin1[] value(); + } +} diff --git a/inject-java/src/test/groovy/io/micronaut/inject/beans/MyMin2.java b/inject-java/src/test/groovy/io/micronaut/inject/beans/MyMin2.java new file mode 100644 index 00000000000..12c4b1a9eef --- /dev/null +++ b/inject-java/src/test/groovy/io/micronaut/inject/beans/MyMin2.java @@ -0,0 +1,29 @@ +package io.micronaut.inject.beans; + +import java.lang.annotation.Documented; +import java.lang.annotation.Repeatable; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.ANNOTATION_TYPE; +import static java.lang.annotation.ElementType.CONSTRUCTOR; +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_USE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE }) +@Retention(RUNTIME) +@Repeatable(MyMin2.List.class) +@Documented +public @interface MyMin2 { + + @Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE }) + @Retention(RUNTIME) + @Documented + @interface List { + + MyMin2[] value(); + } +} diff --git a/inject-java/src/test/groovy/io/micronaut/inject/beans/MyMin3.java b/inject-java/src/test/groovy/io/micronaut/inject/beans/MyMin3.java new file mode 100644 index 00000000000..a5bdd399c0d --- /dev/null +++ b/inject-java/src/test/groovy/io/micronaut/inject/beans/MyMin3.java @@ -0,0 +1,29 @@ +package io.micronaut.inject.beans; + +import java.lang.annotation.Documented; +import java.lang.annotation.Repeatable; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.ANNOTATION_TYPE; +import static java.lang.annotation.ElementType.CONSTRUCTOR; +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_USE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE }) +@Retention(RUNTIME) +@Repeatable(MyMin3.List.class) +@Documented +public @interface MyMin3 { + + @Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE }) + @Retention(RUNTIME) + @Documented + @interface List { + + MyMin3[] value(); + } +} diff --git a/inject-java/src/test/groovy/io/micronaut/inject/beans/MyMin4.java b/inject-java/src/test/groovy/io/micronaut/inject/beans/MyMin4.java new file mode 100644 index 00000000000..f94405d02aa --- /dev/null +++ b/inject-java/src/test/groovy/io/micronaut/inject/beans/MyMin4.java @@ -0,0 +1,29 @@ +package io.micronaut.inject.beans; + +import java.lang.annotation.Documented; +import java.lang.annotation.Repeatable; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.ANNOTATION_TYPE; +import static java.lang.annotation.ElementType.CONSTRUCTOR; +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_USE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE }) +@Retention(RUNTIME) +@Repeatable(MyMin4.List.class) +@Documented +public @interface MyMin4 { + + @Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE }) + @Retention(RUNTIME) + @Documented + @interface List { + + MyMin4[] value(); + } +} diff --git a/inject-java/src/test/groovy/io/micronaut/inject/beans/MyMin5.java b/inject-java/src/test/groovy/io/micronaut/inject/beans/MyMin5.java new file mode 100644 index 00000000000..248bb36fc5f --- /dev/null +++ b/inject-java/src/test/groovy/io/micronaut/inject/beans/MyMin5.java @@ -0,0 +1,29 @@ +package io.micronaut.inject.beans; + +import java.lang.annotation.Documented; +import java.lang.annotation.Repeatable; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.ANNOTATION_TYPE; +import static java.lang.annotation.ElementType.CONSTRUCTOR; +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_USE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE }) +@Retention(RUNTIME) +@Repeatable(MyMin5.List.class) +@Documented +public @interface MyMin5 { + + @Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE }) + @Retention(RUNTIME) + @Documented + @interface List { + + MyMin5[] value(); + } +} diff --git a/inject/src/main/java/io/micronaut/inject/annotation/MutableAnnotationMetadata.java b/inject/src/main/java/io/micronaut/inject/annotation/MutableAnnotationMetadata.java index 32159a724a2..fd4a696037c 100644 --- a/inject/src/main/java/io/micronaut/inject/annotation/MutableAnnotationMetadata.java +++ b/inject/src/main/java/io/micronaut/inject/annotation/MutableAnnotationMetadata.java @@ -822,8 +822,9 @@ public void addAnnotationMetadata(MutableAnnotationMetadata annotationMetadata) @Internal public static void contributeDefaults(AnnotationMetadata target, AnnotationMetadata source) { source = source.getTargetAnnotationMetadata(); - if (source instanceof AnnotationMetadataHierarchy) { - source = ((AnnotationMetadataHierarchy) source).merge(); + if (source instanceof AnnotationMetadataHierarchy annotationMetadataHierarchy) { + contributeDefaults(target, annotationMetadataHierarchy); + return; } if (target instanceof MutableAnnotationMetadata damTarget && source instanceof MutableAnnotationMetadata damSource) { final Map> existingDefaults = damTarget.annotationDefaultValues; @@ -844,6 +845,25 @@ public static void contributeDefaults(AnnotationMetadata target, AnnotationMetad contributeRepeatable(target, source); } + /** + * Contributes defaults to the given target. + * + *

WARNING: for internal use only be the framework

+ * + * @param target The target + * @param source The source + * @since 4.0.0 + */ + @Internal + public static void contributeDefaults(AnnotationMetadata target, AnnotationMetadataHierarchy source) { + for (AnnotationMetadata annotationMetadata : source) { + if (annotationMetadata instanceof AnnotationMetadataReference) { + continue; + } + contributeDefaults(target, annotationMetadata); + } + } + /** * Contributes repeatable annotation metadata to the given target. * From 23bee74dccf73bf7fdb9afc14c1f53a3127e10b3 Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Thu, 16 Mar 2023 13:50:11 +0100 Subject: [PATCH 599/743] =?UTF-8?q?doc:=20delete=20what=E2=80=99s=20new=20?= =?UTF-8?q?and=20breaks=20prior=20to=204.0.0=20(#8952)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Moved to https://micronaut-projects.github.io/micronaut-upgrade/snapshot/guide/ --- src/main/docs/guide/appendix/breaks.adoc | 529 ------------- .../docs/guide/introduction/whatsNew.adoc | 725 ------------------ 2 files changed, 1254 deletions(-) diff --git a/src/main/docs/guide/appendix/breaks.adoc b/src/main/docs/guide/appendix/breaks.adoc index 829809ec178..a1a1772e703 100644 --- a/src/main/docs/guide/appendix/breaks.adoc +++ b/src/main/docs/guide/appendix/breaks.adoc @@ -119,532 +119,3 @@ Interceptors with multiple interceptor bindings annotations now require the same New type converters can be added to api:core.convert.MutableConversionService[] retrieved from the bean context or by declaring a bean of type api:core.convert.TypeConverter[]. To register a type converter into `ConversionService.SHARED`, the registration needs to be done via the service loader. - -== 3.9.0 - -Since Micronaut Framework 3.9.0, CORS `allowed-origins` configuration does not support regular expressions to prevent accidentally exposing your API. You can use `allowed-origins-regex`, if you wish to support a regular expression. - -== 3.8.7 - -Micronaut Framework 3.8.7 updates to https://bitbucket.org/snakeyaml/snakeyaml/wiki/Changes[SnakeYAML 2.0] which addresses https://nvd.nist.gov/vuln/detail/CVE-2022-1471[CVE-2022-1471]. Many organizations' policies forbid their teams to use Micronaut Framework if the framework depends on a vulnerable dependency, even if the framework is unaffected. Micronaut Framework is not affected by https://nvd.nist.gov/vuln/detail/CVE-2022-1471[CVE-2022-1471]. -Micronaut Framework uses SnakeYAML to load configuration in Micronaut applications. There is only one instance of https://github.com/micronaut-projects/micronaut-core/blob/3.7.x/inject/src/main/java/io/micronaut/context/env/yaml/YamlPropertySourceLoader.java#L56[SnakeYAML instantiation] which uses the https://github.com/micronaut-projects/micronaut-core/blob/3.8.x/inject/src/main/java/io/micronaut/context/env/yaml/CustomSafeConstructor.java[Safe Constructor]. Using SnakeYaml's SafeConstructor which is the recommended way to prevent this issue: - -____ -We recommend using SnakeYaml's SafeConsturctor when parsing untrusted content to restrict deserialization. -____ - -== 3.3.0 - -- The <> is now disabled by default. To enable it, you must update your endpoint config: - -[configuration] ----- -endpoints: - env: - enabled: true ----- - -This will then be available, but mask all values. To restore the previous functionality, you can add a bean that implements api:management.endpoint.env.EnvironmentEndpointFilter[]: - -.Legacy Environment Filtering Bean -[source,java] ----- -@Singleton -public class LegacyEnvEndpointFilter implements EnvironmentEndpointFilter { - @Override - public void specifyFiltering(@NotNull EnvironmentFilterSpecification specification) { - specification.legacyMasking(); - } -} ----- - -See the <> for more filtering options. - -== 3.2.4 - -- The link:{api}/io/micronaut/http/client/ProxyHttpClient.html[ProxyHttpClient] now sends the Host header of the proxied service https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.23[as per the RFC], instead of the originating service. - -== 3.2.0 - -- The HTTP client now does SSL certificate verification by default. The old insecure behavior can be re-enabled by setting the `micronaut.http.client.ssl.insecureTrustAllCertificates` property to `true`, but consider using a trust store instead if you're using self-signed certificates. - -- Maven GraalVM Native Image plugin has new GAV coordinates. If you have declared it in your `pom.xml` please update the coordinates to: - -[source,xml] ----- - - org.graalvm.buildtools - native-maven-plugin -... - ----- - -- `WebSocketClient.create` has been modified to accept a `URI` parameter instead of `URL`. The old `URL` methods still exist, but when called with `null` like `WebSocketClient.create(null)`, the method call is now ambiguous. Please insert a cast to `URI`: `WebSocketClient.create((URI) null)`. - The same applies for the `create` method that accepts an additional `HttpClientConfiguration` parameter. - -== 3.1.0 - -Retrieving the port from the Netty embedded server is no longer supported if the server is configured to bind to a random port and the server has not been started. - -== 3.0.0 - -=== Core Changes - -==== Annotation Inheritance - -Possibly the most important change in Micronaut 3.0 is how annotations are inherited from parent classes, methods and interfaces. - -Micronaut 2.x did not respect the rules defined in the jdk:java.lang.reflect.AnnotatedElement[], and inherited all annotations from parent interfaces and types regardless of the presence of the jdk:java.lang.annotation.Inherited[] annotation. - -With Micronaut 3.x and above only annotations that are explicitly meta-annotated with jdk:java.lang.annotation.Inherited[] are now inherited from parent classes and interfaces. -This applies to types in the case where one extends another, and methods in the case where one overrides another. - -Many of Micronaut's core annotations have been annotated with `@Inherited`, so no change will be required, but some annotations that are either outside Micronaut or defined by user code will need changes to code or the annotation. - -In general, behaviour which you wish to override is not inherited by default in Micronaut 3.x and above including <>, <>, <>, <> and so on. - -The following table summarizes the core Micronaut annotations and which are inherited and which are not: - -.Annotation Inheritance in Micronaut 3.x and above -[width="80%",frame="topbot",options="header"] -|====================== -|Annotation |Inherited -|ann:aop.Adapter[] | ✅ -|ann:aop.Around[] | ❌ -|ann:aop.AroundConstruct[] | ❌ -|ann:aop.InterceptorBean[] | ❌ -|ann:aop.InterceptorBinding[] | ❌ -|ann:aop.Introduction[] | ❌ -|ann:core.annotation.Blocking[] | ✅ -|ann:core.annotation.Creator[] | ❌ -|ann:core.annotation.EntryPoint[] | ✅ -|ann:core.annotation.Experimental[] (source level) | ❌ -|ann:core.annotation.Indexes[] & ann:core.annotation.Indexed[] | ✅ -|ann:core.annotation.Internal[] | ✅ -|ann:core.annotation.Introspected[] | ✅ -|ann:core.annotation.NonBlocking[] | ✅ -|ann:core.annotation.Nullable[] | ❌ -|ann:core.annotation.NonNull[] | ❌ -|ann:core.annotation.Order[] | ❌ -|ann:core.annotation.ReflectiveAccess[] | ❌ -|ann:core.annotation.TypeHint[] | ❌ -|ann:core.async.annotation.SingleResult[] | ✅ -|ann:core.bind.annotation.Bindable[] | ✅ -|ann:core.convert.format.Format[] | ✅ -|ann:core.convert.format.MapFormat[] | ✅ -|ann:core.convert.format.ReadableBytes[] | ✅ -|ann:core.version.annotation.Version[] | ❌ -|ann:context.annotation.AliasFor[] | ❌ -|ann:context.annotation.Any[] | ❌ -|ann:context.annotation.Bean[] | ❌ -|ann:context.annotation.BootstrapContextCompatible[] | ✅ -|ann:context.annotation.ConfigurationBuilder[] | ❌ -|ann:context.annotation.ConfigurationInject[] | ❌ -|ann:context.annotation.ConfigurationProperties[] | ❌ -|ann:context.annotation.ConfigurationReader[] | ❌ -|ann:context.annotation.Context[] | ❌ -|ann:context.annotation.DefaultImplementation[] | ✅ -|ann:context.annotation.DefaultScope[] | ❌ -|ann:context.annotation.EachBean[] | ❌ -|ann:context.annotation.Executable[] | ✅ -|ann:context.annotation.Factory[] | ❌ -|ann:context.annotation.NonBinding[] | ❌ -|ann:context.annotation.Parallel[] | ❌ -|ann:context.annotation.Parameter[] | ❌ -|ann:context.annotation.Primary[] | ❌ -|ann:context.annotation.Property[] | ❌ -|ann:context.annotation.PropertySource[] | ❌ -|ann:context.annotation.Prototype[] | ❌ -|ann:context.annotation.Replaces[] | ❌ -|ann:context.annotation.Requirements[] | ❌ -|ann:context.annotation.Requires[] | ❌ -|ann:context.annotation.Secondary[] | ❌ -|ann:context.annotation.Type[] | ❌ -|ann:context.annotation.Value[] | ❌ -|ann:http.annotation.Controller[] | ❌ -|ann:http.annotation.Body[] | ✅ -|ann:http.annotation.Consumes[] | ✅ -|ann:http.annotation.CookieValue[] | ✅ -|ann:http.annotation.CustomHttpMethod[] | ✅ -|ann:http.annotation.Delete[] | ✅ -|ann:http.annotation.Error[] | ✅ -|ann:http.annotation.Filter[] | ❌ -|ann:http.annotation.FilterMatcher[] | ❌ -|ann:http.annotation.Get[] | ✅ -|ann:http.annotation.Head[] | ✅ -|ann:http.annotation.Header[] | ✅ -|ann:http.annotation.Headers[] | ✅ -|ann:http.annotation.HttpMethodMapping[] | ✅ -|ann:http.annotation.Options[] | ✅ -|ann:http.annotation.Part[] | ✅ -|ann:http.annotation.Patch[] | ✅ -|ann:http.annotation.PathVariable[] | ✅ -|ann:http.annotation.Post[] | ✅ -|ann:http.annotation.Produces[] | ✅ -|ann:http.annotation.Put[] | ✅ -|ann:http.annotation.QueryValue[] | ✅ -|ann:http.annotation.RequestAttribute[] | ✅ -|ann:http.annotation.RequestAttributes[] | ✅ -|ann:http.annotation.RequestBean[] | ✅ -|ann:http.annotation.Status[] | ✅ -|ann:http.annotation.Trace[] | ✅ -|ann:http.annotation.UriMapping[] | ✅ -|ann:http.client.annotation.Client[] | ❌ -|ann:jackson.annotation.JacksonFeatures[] | ❌ -|ann:management.endpoint.annotation.Delete[] | ✅ -|ann:management.endpoint.annotation.Endpoint[] | ❌ -|ann:management.endpoint.annotation.Read[] | ✅ -|ann:management.endpoint.annotation.Sensitive[] | ✅ -|ann:management.endpoint.annotation.Selector[] | ✅ -|ann:management.endpoint.annotation.Write[] | ✅ -|ann:management.health.indicator.annotation.Liveness[] | ❌ -|ann:management.health.indicator.annotation.Readiness[] | ❌ -|ann:messaging.annotation.MessageBody[] | ✅ -|ann:messaging.annotation.MessageHeader[] | ✅ -|ann:messaging.annotation.MessageHeaders[] | ✅ -|ann:messaging.annotation.MessageListener[] | ❌ -|ann:messaging.annotation.MessageMapping[] | ✅ -|ann:messaging.annotation.MessageProducer[] | ❌ -|ann:messaging.annotation.SendTo[] | ✅ -|ann:retry.annotation.CircuitBreaker[] | ❌ -|ann:retry.annotation.Fallback[] | ❌ -|ann:retry.annotation.Recoverable[] | ❌ -|ann:retry.annotation.Retryable[] | ❌ -|ann:runtime.context.scope.Refreshable[] | ❌ -|ann:runtime.context.scope.ScopedProxy[] | ❌ -|ann:runtime.context.scope.ThreadLocal[] | ❌ -|ann:runtime.event.annotation.EventListener[] | ✅ -|ann:runtime.http.scope.RequestScope[] | ❌ -|ann:scheduling.annotation.Async[] | ❌ -|ann:scheduling.annotation.ExecuteOn[] | ❌ -|ann:scheduling.annotation.Scheduled[] | ❌ -|ann:session.annotation.SessionValue[] | ✅ -|link:{micronauttracingapi}/io/micronaut/tracing/annotation/ContinueSpan.html[@ContinueSpan] | ✅ -|link:{micronauttracingapi}/io/micronaut/tracing/annotation/NewSpan.html[@NewSpan] | ✅ -|link:{micronauttracingapi}/io/micronaut/tracing/annotation/SpanTag.html[@SpanTag] | ✅ -|ann:validation.Validated[] | ✅ -|ann:websocket.annotation.ClientWebSocket[] | ❌ -|ann:websocket.annotation.OnClose[] | ✅ -|ann:websocket.annotation.OnError[] | ✅ -|ann:websocket.annotation.OnMessage[] | ✅ -|ann:websocket.annotation.OnOpen[] | ✅ -|ann:websocket.annotation.ServerWebSocket[] | ❌ -|ann:websocket.annotation.WebSocketComponent[] | ❌ -|ann:websocket.annotation.WebSocketMapping[] | ✅ -|====================== - -When upgrading an application you may need to take action if you implement an interface or subclass a superclass and override a method. - -For example the annotations defined in `jakarta.validation` are not inherited by default, so they must be defined again in any overridden or implemented methods. - -This behaviour grants more flexibility if you need to redefine the validation rules. Note that it is still possible to inherit validation rules through meta-annotations. See the section on <> for more information. - -==== Error Response Format - -The default value of `jackson.always-serialize-errors-as-list` is now true. That means by default the Hateoas JSON errors will always be a list. For example: - -.Example error response ----- -{ - ... - "_embedded": { - "errors": [ - { - "message": "Person.name: must not be blank" - } - ] - } -} ----- - -To revert to the previous behavior where a singular error was populated in the message field instead of including `_embedded.errors`, set the configuration setting to false. - -==== Runtime Classpath Scanning Removed - -It is no longer possible to scan the classpath at runtime using the `scan` method of the api:context.env.Environment[] interface. - -This functionality has not been needed for some time as scanning is implemented at build time through <>. - -==== Inject Annotations - -Micronaut now provides the `jakarta.inject` annotations as a transitive dependency instead of the `javax.inject` annotations. -To continue using the old annotations, add the following dependency. - -dependency:javax.inject:javax.inject:1[] - -==== Nullable Annotations - -Micronaut no longer exports any third party dependency for nullability annotations. -Micronaut now provides its own annotations for this purpose (api:core.annotation.Nullable[] and api:core.annotation.NonNull[]) that are used for our APIs. -To continue using other nullability annotations, simply add the relevant dependency. - -Internally, Micronaut makes use of a third party annotation that may manifest as a warning in your project: -``` -warning: unknown enum constant When.MAYBE - reason: class file for javax.annotation.meta.When not found -``` - -This warning is harmless and can be ignored. To eliminate this warning, add the following dependency to your project's compile only classpath: - -dependency:com.google.code.findbugs:jsr305[gradleScope="compileOnly"] - -==== Server Filter Behavior - -In Micronaut 2 server filters could have been called multiple times in the case of an exception being thrown, or sometimes not at all if the error resulted before route execution. -This also allowed for filters to handle exceptions thrown from routes. -Filters have changed in Micronaut 3 to always be called exactly once for each request, under all conditions. -Exceptions are no longer propagated to filters and instead the resulting error response is passed through the reactive stream. - -In the case of a response being created as a result of an exception, the original cause is now stored as a response attribute (api:http.HttpAttributes#EXCEPTION[]). -That attribute can be read by filters to have context for the error HTTP response. - -The api:http.filter.OncePerRequestHttpServerFilter[] class is now deprecated and will be removed in the next major release. -The api:http.filter.OncePerRequestHttpServerFilter[] stores a request attribute when the filter is executed and some functionality may rely on that attribute existing. -The class will still create the attribute but it is recommended to instead create a custom attribute in your filter class and use that instead of the one created by api:http.filter.OncePerRequestHttpServerFilter[]. - -There is also a minor behavior change in when the response gets written. -Any modifications to the response after the underlying `onNext` call is made will not have any effect as the response has already been written. - -==== HTTP Compile Time Validation - -Compile time validation of HTTP related classes has been moved to its own module. To continue validating controllers, websocket server classes add `http-validation` to the annotation processor classpath. - -dependency:io.micronaut:micronaut-http-validation[gradleScope="annotationProcessor"] - -==== Decapitalization Strategy - -For many cases, one common one being introspections, getter names like `getXForwarded()` would result in the bean property being `XForwarded`. -The name will now be `xForwarded`. -This can affect many areas of the framework where names like `XForwarded` are used. - -==== @Order default - -Previously the default order value for the `@Order` annotation was the lowest precedence. -It is now 0. - -==== Classes Renaming - -* `RxJavaRouteDataCollector` has been renamed to `DefaultRouteDataCollector`. -* `RxJavaBeanDefinitionDataCollector.html` has been renamed to `DefaultBeanDefinitionDataCollector`. -* `RxJavaHealthAggregator` has been renamed to `DefaultHealthAggregator` - -==== Deprecation Removal - -Classes, constructors, etc. that have been deprecated in previous versions of Micronaut have been removed. - -==== Reflective Bean Map - -In several places in Micronaut, it is required to get a map representation of your object. -In previous versions, a reflection based strategy was used to retrieve that information if the class was not annotated with `@Introspected`. -That functionality has been removed and it is now required to annotate classes with `@Introspected` that are being used in this way. -Any class may be affected if it is passed as an argument or returned from any controller or client, among other use cases. - -==== Cookie Secure Configuration - -Previously the `secure` configuration for cookies was only respected if the request was determined to be sent over https. -Due to a number of factors including proxies, HTTPS requests can be presented to the server as if they are HTTP. -In those cases the setting was not having any effect. -The setting is now respected regardless of the status of the request. -If the setting is not set, cookies will be secure if the request is determined to be HTTPS. - -==== Server Error Route Priority - -Previously if a route could not be satisfied, or an `HttpStatusException` was thrown, routes for the relevant HTTP status was searched before routes that handled the specific exception. -In Micronaut 3 routes that handle the exception will be searched first, then routes that handle the HTTP status. - -==== Status Route Default Response Status - -Status error routes will now default to produce responses with the same HTTP status as specified in the `@Error` annotation. -In previous versions a 200 OK response was created. -For example: - -``` -@Error(status = HttpStatus.UNSUPPORTED_MEDIA_TYPE) -String unsupportedMediaTypeHandler() { - return "not supported"; -} -``` - -The above method will result in a response of HTTP status 415 with a body of "not supported". -Previously it would have been a response of HTTP status 200 with a body of "not supported". -To specify the desired response status, either annotate the method with `@Status` or return an `HttpResponse`. - -==== No Longer Possible to Inject a `List` of `Provider` - -In Micronaut 2.x it was possible to inject a `List`, although this was undocumented behaviour. -In Micronaut 3.x injecting a list of `Provider` instances is no longer possible and you should instead use the api:context.BeanProvider[] API which provides `stream()` and `iterator()` methods to provide the same functionality. - -==== Injecting ExecutorService - -In previous versions of Micronaut it was possible to inject an link:{jdkapi}/java/util/concurrent/ExecutorService.html[ExecutorService] without any qualifiers and the default Netty event loop group would be injected. -Because the event loop should not be used for general purpose use cases, the injection will now fail by default with a non unique bean exception. -The injection point should be qualified for which executor service is desired. - -==== Subclasses Returned From Factories Not Injectable - -It is no longer possible to inject the internal implementation type from beans produced via factories. The type returned from the factory or any of its super types are able to be injected. - -For example: - -[source,java] ----- -import java.util.concurrent.ForkJoinPool; -import java.util.concurrent.ExecutorService; -import javax.inject.Singleton; - -public class ExecutorFactory { - @Singleton - public ExecutorService executorService() { - return ForkJoinPool.commonPool(); - } -} ----- - -In the above case, if the `ExecutorService` had been already been retrieved from the context in previous logic, a call to `context.getBean(ForkJoinPool.class)` would locate the already created bean. -This behaviour was inconsistent because if the bean had not yet been created then this lookup would not work. -In Micronaut 3 for consistency this is no longer possible. - -You can however restore the behaviour by changing the factory to return the implementation type: - -[source,java] ----- -import java.util.concurrent.ForkJoinPool; -import java.util.concurrent.ExecutorService; -import javax.inject.Singleton; -public class ExecutorFactory { - - @Singleton - public ForkJoinPool executorService() { - return ForkJoinPool.commonPool(); - } -} ----- - -==== No Longer Possible to Define AOP Advice on a Bean Produced from a Factory with Constructor arguments - -In previous versions of Micronaut it was possible to define AOP advice to a factory method that returned a class that featured constructor arguments. -This could lead to undefined behaviour since the argument of the generated proxy which would be dependency injected by the framework may be different from manually constructed proxy target. - -The following definition is now invalid in Micronaut 3 and above and will lead to a compilation error: - -[source,java] ----- -import io.micronaut.context.annotation.*; -import io.micronaut.runtime.context.scope.*; - -@Factory -class ExampleFactory { - - @ThreadLocal - Test test() { - return new Test("foo"); - } -} - -class Test { - // illegally defines constructor arguments - Test(String name) {} -} ----- - -==== Implementations of `javax.inject.Provider` No Longer Generate Factories - -In Micronaut 2.x if you defined a bean that implemented the `javax.inject.Provider` interface then the return type of the `get` method also automatically became a bean. - -For example: - -[source,java] ----- -import javax.inject.Provider; -import javax.inject.Singleton; - -@Singleton -public class AProvider implements Provider { - @Override - public A get() { - return new AImpl(); - } -} ----- - -In the above example a bean of type `A` would automatically be exposed by Micronaut. -This behaviour is no longer supported and instead the ann:context.annotation.Factory[] annotation should be used to express the same behaviour. -For example: - -[source,java] ----- -import io.micronaut.context.annotation.Factory; -import javax.inject.Provider; -import javax.inject.Singleton; - -@Factory -public class AProvider implements Provider { - @Override - @Singleton - public A get() { - return new AImpl(); - } -} ----- - -==== Fewer Executable Methods Generated for Controllers and Message Listeners - -Previous versions of Micronaut specified the ann:context.annotation.Executable[] annotation as a meta-annotation on the ann:http.annotation.Controller[], ann:http.annotation.Filter[] and ann:messaging.annotation.MessageListener[] annotations. -This resulted in generating executable method all non-private methods of classes annotated with these annotations. - -In Micronaut 3.x and above the ann:context.annotation.Executable[] has been moved to a meta-annotation of ann:http.annotation.HttpMethodMapping[] and ann:messaging.annotation.MessageMapping[] instead to reduce memory consumption and improve efficiency. - -If you were relying on the presence of these executable methods you must explicitly annotate methods in your classes with ann:context.annotation.Executable[] to restore this behaviour. - -==== GraalVM changes - -In previous versions of Micronaut annotating a class with `@Introspected` automatically added it to the GraalVM `reflect-config.json` file. -The original intended usage of the annotation is to generate <> so Micronaut can instantiate the class and call getters and setters without using reflection. - -Starting in Micronaut 3.x the `@Introspected` annotation doesn't add the class to the GraalVM `reflect-config.json` file anymore, because in most of the cases is not really necessary. -If you need to declare a class to be accessed by reflection, use the `@ReflectiveAccess` annotation instead. - -Another change is regarding the GraalVM resources created at compile-time. In previous versions of Micronaut adding a dependency on `io.micronaut:micronaut-graal` triggered the generation of the GraalVM `resource-config.json` that included all the resources in `src/main/resources` so they were included in the native image. Starting in Micronaut 3.x that is done in either the Gradle or Maven plugins. - -=== Exception Handler Moves - -Two exception handlers that were in `micronaut-server-netty` have now been moved to `micronaut-server` since they were not specific to Netty. Their package has also changed as a result. - -.Package changes -|=== -|Old |New - -| http-server-netty/src/main/java/io/micronaut/http/server/netty/converters/DuplicateRouteHandler.java -| http-server/src/main/java/io/micronaut/http/server/exceptions/DuplicateRouteHandler.java - -| http-server-netty/src/main/java/io/micronaut/http/server/netty/converters/UnsatisfiedRouteHandler.java -| http-server/src/main/java/io/micronaut/http/server/exceptions/UnsatisfiedRouteHandler.java - -|=== - -=== Module Changes - -==== New package for Micronaut Cassandra - -The classes in Micronaut Cassandra have been moved from `io.micronaut.configuration.cassandra` to `io.micronaut.cassandra` package. - -==== Micronaut Security - -Many of the APIs in the Micronaut Security module have undergone changes. Please see the link:https://micronaut-projects.github.io/micronaut-security/{micronautSecurityVersion}/guide[Micronaut Security] documentation for the details. - -==== Groovy changes - -In previous version the missing property wouldn't set the field value to `null` as it would do for the Java code, in the version 3 it should behave in the same way. - -Please refactor to use the default value in the `@Value` annotation: - -[source,groovy] ----- -@Nullable -@Value('${greeting}') -protected String before = "Default greeting" - -@Nullable -@Value('${greeting:Default greeting}') -protected String after ----- diff --git a/src/main/docs/guide/introduction/whatsNew.adoc b/src/main/docs/guide/introduction/whatsNew.adoc index 901f8c6fdc2..64eda6513a7 100644 --- a/src/main/docs/guide/introduction/whatsNew.adoc +++ b/src/main/docs/guide/introduction/whatsNew.adoc @@ -33,728 +33,3 @@ The disabled beans are also now visible via the < <> -== 3.8.0 - -Key features: - -- https://www.graalvm.org/release-notes/22_3/[GraalVM 22.3 Support] -- With Micronaut `3.8.0`, you can use `@RequestBean` annotations with https://docs.oracle.com/en/java/javase/14/language/records.html[Records]. Before `3.8.0`, you could use a POJO as a controller method parameter and annotate the parameter with `@RequestBean` to bind any Bindable value (e.g., `HttpRequest`, `@PathVariable`, `@QueryValue` or `@Header` fields). -- If you enable CORS from any origin while running your app in localhost (e.g., test or development), since `3.8.0`, the `CorsFilter` returns 403 for non-localhost origins to protect you against drive-by localhost attacks. - -Please read the https://micronaut.io/2022/12/27/micronaut-framework-3-8-0-released/[Micronaut Framework 3.8.0 announcement blog post]. You will find a detailed overview of what’s new in Micronaut 3.8.0. - -== 3.7.0 - -Several improvements: - -- If you want complete control of where your application loads configuration from, for example, due to security restrictions, you can disable the default https://docs.micronaut.io/snapshot/guide/#propertySource[`PropertySourceLoader`] implementations by calling `ApplicationContextBuilder::enableDefaultPropertySources(false)` when starting your application. - -- Better `java.time` conversion for YAML configuration - -- Client SSL inner configuration is https://docs.micronaut.io/latest/guide/#bootstrap[Bootstrap] context compatible. - -- https://docs.micronaut.io/snapshot/api/io/micronaut/http/uri/UriBuilder.html[`UriBuilder`] methods `queryParam` and `replaceQueryParam` ignore null values. - -- It is possible to stop the Netty server without stopping the Application context. - -- You can declare beans at runtime using interfaces. - -- You can mark static methods as `@Executable`. - -- A big HTTP client refactor. - -**Spring integration improvements** - -- https://micronaut-projects.github.io/micronaut-spring/latest/guide/[Micronaut Spring] contains improvements for developers who want to use Micronaut modules with a Spring application or consume Spring libraries from a Micronaut application. - -**New modules**: - -- https://micronaut-projects.github.io/micronaut-object-storage/latest/guide/[Object Storage]. - -- https://micronaut-projects.github.io/micronaut-crac/latest/guide/[Micronaut CRaC]. - -Please read the https://micronaut.io/2022/09/21/micronaut-framework-3-7-0-released/[Micronaut Framework 3.7.0 announcement blog post]. You will find a detailed overview of what’s new in Micronaut 3.7.0. - -== 3.6.0 - -Key features: - -- https://micronaut-projects.github.io/micronaut-test-resources/latest/guide/#introduction[Test Resources] -- https://micronaut-projects.github.io/micronaut-sql/latest/guide/#hibernate-reactive[Hibernate Reactive] -- https://micronaut-projects.github.io/micronaut-tracing/latest/guide/#opentelemetry[OpenTelemetry] -- https://micronaut-projects.github.io/micronaut-azure/latest/guide/#azureKeyVault[Azure Vault] -- https://www.graalvm.org/release-notes/22_2/[GraalVM 22.2 Support] -- https://nubesgen.com/[NubesGen integration] - -Please read the https://micronaut.io/2022/08/04/micronaut-framework-3-6-0-released/[Micronaut Framework 3.6.0 announcement blog post]. You will find a detailed overview of what’s new in Micronaut 3.6.0. - -Micronaut Core features: - -=== Don't apply a @Filter for services - -It is possible to exclude services from an HTTP Client Filter with the member `excludeServiceId` of `@Filter.` - -```java -@Filter(patterns = '/**', excludeServiceId = 'authClient') -public class AppHttpClientFilter implements HttpClientFilter { -``` - -=== Netty runtime - -This version upgrades https://netty.io[Netty] from 4.1.77 to 4.1.79. Moreover, it contains improvements to the API to https://docs.micronaut.io/snapshot/guide/#nettyClientPipeline[configure the Netty Client Pipeline] and to https://docs.micronaut.io/snapshot/guide/#nettyServerPipeline[configure the Netty Server Pipeline]. - -=== Improvements to HttpClientException - -If present a `serviceId` field is populated in the `HttpClientException` and shown in the exception message. - -=== Modules Upgrades - -- Micronaut AWS 3.5.3 to 3.7.0 -- Micronaut Azure 3.2.3 to 3.3.0 -- Micronaut Cache 3.4.1 to 3.5.0 -- Micronaut Cassandra 4.0.0 to 5.1.1 -- Micronaut Coherence 3.4.1 to 3.5.1 -- Micronaut Data 3.4.3 to 3.7.2 -- Micronaut Elasticsearch 4.2.0 to 4.3.0 -- Micronaut Email 1.2.3 to 1.3.1 -- Micronaut Flyway 5.3.0 to 5.4.0 -- Micronaut GCP 4.2.1 to 4.4.0 -- Micronaut GraphQL 3.0.0 to 3.1.0 -- Micronaut Groovy 3.1.0 to 3.2.0 -- Micronaut JaxRS 3.3.0 to 3.4.0 -- Micronaut JMX 3.0.0 to 3.1.0 -- Micronaut Kafka 4.3.1 to 4.4.0 -- Micronaut Micrometer 4.3.0 to 4.4.0 -- Micronaut Microstream 1.0.0-M1 to 1.0.0 -- Micronaut Liquibase 5.3.0 to 5.4.1 -- Micronaut Mongo 4.2.0 to 4.4.0 -- Micronaut Neo4J 5.0.0 to 5.1.0 -- Micronaut Nats 3.0.0 to 3.1.0 -- Micronaut OpenAPI 4.2.2 to 4.4.3 -- Micronaut Picocli 4.2.1 to 4.3.0 -- Micronaut Problem 2.3.1 to 2.4.0 -- Micronaut RabbitMQ 3.1.0 to 3.3.0 -- Micronaut R2DBC 3.0.0 to 3.0.1 -- Micronaut Reactor 2.2.3 to 2.3.1 -- Micronaut Redis 5.2.0 to 5.3.0 -- Micronaut RxJava3 2.2.1 to 2.3.0 -- Micronaut Serialization 1.1.1 to 1.3.0 -- Micronaut Servlet 3.2.3 to 3.3.0 -- Micronaut Spring 4.1.1 to 4.2.1 -- Micronaut SQL 4.4.1 to 4.6.3 -- Micronaut Test 3.3.1 to 3.4.0 -- Micronaut TOML 1.0.0 to 1.1.1 -- Micronaut Tracing 4.1.1 to 4.2.1 -- Micronaut Views 3.4.0 to 3.5.0 -- Micronaut Jackson XML 3.0.1 to 3.1.0 - -== 3.5.0 - -=== GraalVM 22.1.0 - -Micronaut framework 3.5 supports https://www.graalvm.org/release-notes/22_1/[GraalVM 22.1.0]. - -https://micronaut-projects.github.io/micronaut-gradle-plugin/latest/[Micronaut Gradle Plugin v3.4.0] and https://github.com/micronaut-projects/micronaut-maven-plugin/releases/tag/v3.3.0[Micronaut Maven Plugin v3.3.0] support GraalVM 22.1.0. - -=== Incremental Compilation for Gradle Builds - -Micronaut framework 3.5 supports fully incremental compilation, including GraalVM metadata for Gradle Builds. - -=== Micronaut Data - -https://github.com/micronaut-projects/micronaut-data/releases/tag/v3.4.0[Micronaut Data 3.4.0] supports: - -- Postgres enums for JDBC. -- Pagination for reactive repositories and specifications. -- Pagination for async, coroutines repositories, and specifications. - -=== Turbo Integration - -Micronaut Views adds https://micronaut-projects.github.io/micronaut-views/latest/guide/#turbo[integration with Turbo] - -=== New Module - Micronaut Microstream - -https://micronaut-projects.github.io/micronaut-microstream/snapshot/guide/[Micronaut Microstream] eases working with https://microstream.one[MicroStream], a native Java object graph storage engine. - -=== @Scheduled with Time Zones - -Optionally, you can specify a time zone when using the <>. - -[source,java] ----- -@Scheduled(cron = '1/33 0/1 * 1/1 * ?', zoneId = "America/Chicago") -void runCron() { -... -.. ----- - -=== Support validation groups with `@Validated` - -You can enforce a subset of constraints using <> using groups on the `@Validated`. - -=== Advanced Listener Configuration - -Micronaut framework 3.5.0 offers more flexibility in configuring the HTTP Server. Instead of configuring a single port, you -<>. - -=== EPHEMERAL FACTORIES - -A <> has the default scope `@Singleton`, and it is destroyed with the context. Since Micronaut framework v3.5.0, you can dispose of the factory after producing a bean by annotating your factory class with `@Prototype` and `@Factory` - -=== Module upgrades - -- https://github.com/micronaut-projects/micronaut-test/releases/tag/v3.2.0[Micronaut Test 3.2.0] adds support for KoTest 5. -- https://github.com/micronaut-projects/micronaut-aws/releases/tag/v3.5.0[Micronaut AWS 3.5.0] adds a new module https://micronaut-projects.github.io/micronaut-aws/latest/guide/#cdk[Micronaut AWS CDK]. It also upgrades to the latest versions of the AWS SDKs. -- https://github.com/micronaut-projects/micronaut-micrometer/releases/tag/v4.3.0[Micronaut Micrometer 4.3.0] updates to Micrometer 1.9.0. -- https://github.com/micronaut-projects/micronaut-gcp/releases/tag/v4.2.0[Micronaut GCP 4.2.0] updates to `grpc-auth` -1.45.1 and `grpc-netty-shaded`. Moreover, we have clarified the documentation to support GraalVM Native Images when using the GCP libraries, and the Micronaut GCP Bom now includes the `com.google.cloud:native-image-support` dependency. -- https://github.com/micronaut-projects/micronaut-aot/releases/tag/v1.1.0[Micronaut AOT 1.1.0] -- https://github.com/micronaut-projects/micronaut-sql/releases/tag/v4.4.0[Micronaut SQL to 4.4.0] -- https://github.com/micronaut-projects/micronaut-problem-json/releases/tag/v2.3.0[Micronaut Problem JSON to 2.3.0] -- https://github.com/micronaut-projects/micronaut-grpc/releases/tag/v3.3.0[Micronaut GRPC to 3.3.0] allows exposing a gRPC Health Check for a grpc-server. -- https://github.com/micronaut-projects/micronaut-serialization/releases/tag/v1.1.0[Micronaut Serialization to 1.1.0]. It allows the serialization and deserialization of object arrays. -- https://github.com/micronaut-projects/micronaut-openapi/releases/tag/v4.1.0[Micronaut OpenAPI to 4.1.0] updates to Swagger 2.2.0. -- https://github.com/micronaut-projects/micronaut-r2dbc/releases/tag/v3.0.0[Micronaut R2DBC to 3.0.0] updates to R2DBC `1.0.0.RELEASE`. -- https://github.com/micronaut-projects/micronaut-security/releases/tag/v3.6.0[Micronaut Security to 3.6.0]. -- https://github.com/micronaut-projects/micronaut-cache/releases/tag/v3.4.1[Micronaut Cache to 3.4.1]. -- https://github.com/micronaut-projects/micronaut-coherence/releases/tag/v3.4.1[Micronaut Coherence to 3.4.1]. - -Several modules publish a BOM (Bill of Materials) or use a Gradle Version Catalogs: - -- https://github.com/micronaut-projects/micronaut-jaxrs/releases/tag/v3.3.0[Micronaut JAX-RS to 3.3.0] -- https://github.com/micronaut-projects/micronaut-picocli/releases/tag/v4.2.1[Micronaut Picocli to 4.2.1] -- https://github.com/micronaut-projects/micronaut-acme/releases/tag/v3.2.0[Micronaut ACME to 3.2.0]. -- https://github.com/micronaut-projects/micronaut-mongodb/releases/tag/v4.2.0[Micronaut MongoDB to 4.2.0] -- https://github.com/micronaut-projects/micronaut-mqtt/releases/tag/v2.2.0[Micronaut MQTT to 2.2.0]. -- https://github.com/micronaut-projects/micronaut-kafka/releases/tag/v4.3.0[Micronaut Kafka to 4.3.0]. - -=== Schema Migration Modules - -* https://github.com/micronaut-projects/micronaut-flyway/releases/tag/v5.3.0[Micronaut Flyway 5.3.0] updates Flyway to 8.5.8. -* https://github.com/micronaut-projects/micronaut-liquibase/releases/tag/v5.3.0[Micronaut Liquibase 5.3.0] updates Liquibase to 4.9.1 - - -== 3.4.0 - -=== Localized Message Source - -You can now inject <>, a `@RequestScope` bean, in your controllers to resolve localized messages for the current HTTP Request. It works in combination with <> capabilities. - -=== Referencing bean properties in @Requires. - -With 3.4.0, you can https://docs.micronaut.io/latest/guide/#_referencing_bean_properties_in_requires[reference other beans properties in `@Requires` to load beans conditionally]. - -[source, java] ----- -@Requires(bean=Config.class, beanProperty="foo", value="John") ----- - -=== Micronaut Data MongoDB - -https://github.com/micronaut-projects/micronaut-data/releases/tag/v3.3.0[Micronaut Data 3.3.0] includes https://micronaut-projects.github.io/micronaut-data/latest/guide/index.html#mongo[Micronaut Data MongoDB]. - -=== Micronaut AOT and Maven - -https://micronaut-projects.github.io/micronaut-aot/latest/guide/[Micronaut AOT] is now fully supported for Maven users. Enabling AOT is as simply as passing `-Dmicronaut.aot.enabled` when running, testing, or packaging your application. - -For more details, check the https://micronaut-projects.github.io/micronaut-maven-plugin/latest/examples/aot.html[Micronaut Maven Plugin documentation]. - -=== Micronaut TOML - -https://micronaut-projects.github.io/micronaut-toml/latest/guide/[Micronaut TOML] allows you to write your application configuration with https://toml.io/en/[TOML] in addition to `Properties`, `YAML`, `Groovy` or `Config4k`. - -=== Micronaut Security - -https://github.com/micronaut-projects/micronaut-security/releases/tag/v3.4.0[Micronaut Security 3.4.1] responds with an error when an authenticated user visits a sensitive endpoint. This forces the developer to define how they want their application to behave in that scenario. Read the https://github.com/micronaut-projects/micronaut-security/releases/tag/v3.4.0[release notes] and the https://micronaut-projects.github.io/micronaut-security/latest/guide/#builtInEndpointsAccess[documentation] to learn more. - -=== BOM Modules - -Several projects include a BOM (Bills of Materials) module: - -- https://github.com/micronaut-projects/micronaut-azure/releases/tag/v3.1.0[Micronaut Azure 3.1.0] -- https://github.com/micronaut-projects/micronaut-gcp/releases/tag/v4.1.0[Micronaut GCP 4.1.0]. It includes updates to the latest versions of Google Cloud dependencies. -- https://github.com/micronaut-projects/micronaut-kotlin/releases/tag/v3.2.0[Micronaut Kotlin 3.2.0] -- https://github.com/micronaut-projects/micronaut-mongodb/releases/tag/v4.1.0[Micronaut MongoDB 4.1.0] -- https://github.com/micronaut-projects/micronaut-mqtt/releases/tag/v2.1.0[Micronaut MQTT 2.1.0] -- https://github.com/micronaut-projects/micronaut-reactor/releases/tag/v2.2.1[Micronaut Reactor 2.2.1]. It includes updates to the Project Reactor dependencies. -- https://github.com/micronaut-projects/micronaut-redis/releases/tag/v5.2.0[Micronaut Redis 5.2.0] -- https://github.com/micronaut-projects/micronaut-rxjava2/releases/tag/v1.2.0[Micronaut RxJava2 1.2.0] -- https://github.com/micronaut-projects/micronaut-rxjava3/releases/tag/v2.2.0[Micronaut RxJava3 2.2.0] -- https://github.com/micronaut-projects/micronaut-security/releases/tag/v3.4.0[Micronaut Security 3.4.1] -- https://github.com/micronaut-projects/micronaut-servlet/releases/tag/v3.2.0[Micronaut Servlet 3.2.0]. It includes updates to Tomcat and Undertow dependencies. - -=== Other Module Upgrades - -- https://github.com/micronaut-projects/micronaut-aws/releases/tag/v3.2.0[Micronaut AWS 3.2.0] updates to the latest version of AWS SDK, ASK SDK and AWS Serverless Java Container. -- https://github.com/micronaut-projects/micronaut-email/releases/tag/v1.1.0[Micronaut Email 1.1.0] updates to the Sendgrid 4.8.3 and contains improvements for `javamail` module users. -- https://github.com/micronaut-projects/micronaut-test/releases/tag/v3.1.0[Micronaut Test 3.1.0] updates the underlying testing dependencies. - -== 3.3.0 - -=== GraalVM 22.0.0.2 - -Micronaut now supports the latest GraalVM 22.0.0.2 release. - -=== Environment Endpoint - -A new API api:management.endpoint.env.EnvironmentEndpointFilter[] has been created to allow applications to customize which keys should have their values masked and which keys should not have their values masked. See the <> for full details. - -=== AOP Interceptor Binding - -When binding an AOP annotation to an interceptor, only the presence of the annotation is used to determine if the interceptor should be applied. Now it's possible to also bind based on the values of the annotation. To enable this feature, set the `bindMembers` member of the ann:aop.InterceptorBinding[] annotation to `true`. - -=== Netty Buffer Allocation - -It is now possible to configure the default Netty buffer allocator. See the https://docs.micronaut.io/3.3.x/guide/configurationreference.html#io.micronaut.buffer.netty.DefaultByteBufAllocatorConfiguration[configuration reference]. - -=== Improved Flexibility in Class Style - -Many features of the Micronaut framework rely on the convention of getters and setters. Due to things like records and builders, the method names we look for are now configurable with the ann:core.annotation.AccessorsStyle[] annotation. For example, the annotation can be placed on ann:context.annotation.ConfigurationProperties[] beans to allow for binding configuration to methods that do not begin with `set`. It can also be used with classes annotated with ann:core.annotation.Introspected[]. - -=== Access Log Exclusions - -The Netty access logger now supports excluding requests based on a set of regular expression patterns that match against the URI. See the <>. - -=== New Serialization/Deserialization Module - -https://micronaut-projects.github.io/micronaut-serialization/1.0.x/guide/[Micronaut Serialization] is a new module created as an alternative to Jackson. It supports serializing and deserializing Java types (including Java 17 records) to and from JSON and other formats. - -Users now have the choice of an alternative implementation that's largely compatible with existing Jackson annotations but contains many benefits, including the elimination of reflection, compile-time validation, greater security because only explicit types are serializable, and reduction of native image build sizes, build times, and memory usage. - -=== New Email Module - -https://micronaut-projects.github.io/micronaut-email/latest/guide/[Micronaut Email] is a new module to ease sending emails from a Micronaut application. It provides integration with transactional email providers such as Amazon Simple Email Service, Postmark, Mailjet or SendGrid. - -=== Micronaut AOT - -During this minor cycle, we released a milestone release of a new module Micronaut AOT. You can use Micronaut AOT and use the build-time optimizations provided by the module to achieve faster startup times via the Micronaut Gradle Plugin. Please, read more about it in the https://micronaut.io/2021/12/20/micronaut-aot-build-time-optimizations-for-micronaut-applications/[announcement blog post]. - -=== Micronaut Kubernetes 3.3.0 - -Micronaut Kubernetes 3.3 adds support to easily create the Kubernetes Operator. The Kubernetes Operator is a known pattern used to extend the capabilities of Kubernetes by creating application specific controllers for both native and custom resources. See more on https://micronaut-projects.github.io/micronaut-kubernetes/latest/guide/#kubernetes-operator[Kubernetes Operator]. - -The version of Micronaut Kubernetes 3.3.0 also adds new Kubernetes reactive client for RxJava3. - -=== Other Module Upgrades - -- Micronaut Cache 3.1.0 -- Micronaut Discovery Client 3.1.0 -- Micronaut Elasticsearch 4.2.0 -- Micronaut Flyway 5.1.1 -- Micronaut Kafka 4.1.1 -- Micronaut Kotlin 3.1.0 -- Micronaut Liquibase 5.1.1 -- Micronaut Openapi 4.0.0 -- Micronaut Picocli 4.1.0 -- Micronaut Problem 2.2.0 -- Micronaut Security 3.3.0 -- Micronaut Sql 4.1.1 -- Micronaut Toml 1.0.0-M2 -- Micronaut Views 3.1.2 - -=== Other Dependency Upgrades - -- Apache Commons DBCP 2.9.0 -- Elasticsearch 7.16.3 -- Flyway 8.4.2 -- Hibernate 5.5.9.Final -- Kotlin 1.6.10 -- Liquibase 4.7.1 -- Logback 1.2.10 -- Swagger 2.1.12 - -== 3.2.0 - -=== GraalVM 21.3.0 - -Micronaut has been updated to support the latest GraalVM 21.3.0 release. Please keep in mind that starting with 21.3.0 GraalVM doesn't release a version based on JDK 8. If you still use Java 8 use the GraalVM JDK 11 distribution. - -The official GraalVM Maven plugin has new GAV coordinates so if you have declared it in your `pom.xml` update the coordinates to: - -[source,xml] ----- - - org.graalvm.buildtools - native-maven-plugin -... - ----- - -Please check https://graalvm.github.io/native-build-tools/0.9.7.1/maven-plugin.html[the official documentation] about how to customize the plugin. - -=== Gradle Plugin 3.0.0 - -A new major version of the Gradle plugin has been released, including internal changes to use Gradle's lazy configuration APIs. -In the process, https://micronaut-projects.github.io/micronaut-gradle-plugin/latest/[documentation] has been rewritten. - -Support for GraalVM now delegates to https://graalvm.github.io/native-build-tools/0.9.7.1/gradle-plugin.html[the official GraalVM plugin]. -We recommend to upgrade in order to get the latest bugfixes, but this constitutes a breaking change for some users: - -- the `nativeImage` task is now replaced with `nativeCompile` -- native image configuration happens in the `graalvmNative` DSL extension instead of the `nativeCompile` task -- native image building makes use of Gradle's toolchain support. Please refer to the https://micronaut-projects.github.io/micronaut-gradle-plugin/latest/[documentation] for help. - -NOTE: You can still build existing applications or libraries using the 2.x version of the Gradle plugin. Documentation for this version can be found https://github.com/micronaut-projects/micronaut-gradle-plugin/blob/2.0.x/README.md[here]. - -=== Kotlin 1.6.0 - -Micronaut 3.2.0 includes support for Kotlin 1.6.0. - -=== HTTP Features - -==== WebSocket Ping API - -WebSocket ann:websocket.annotation.OnMessage[] methods can now accept a api:websocket.WebSocketPongMessage[] parameter that will receive a WebSocket pong sent as a response to a ping submitted using the new `sendPingAsync` method on api:websocket.WebSocketSession[]. - -==== HTTP2 Server Push - -It is now possible to send resources, e.g. stylesheets required by a HTML page, to the client alongside the request for the page using the HTTP2 server push protocol. See the <> for information on how to use this feature. - -==== JsonView on request bodies - -You can now specify the Jackson `@JsonView` annotation on `@Body` parameters to controller methods. - -==== WebSocket ws/wss protocol support - -The WebSocket clients now support the ws/wss protocol. To implement this change, the api:websocket.WebSocketClient[] `create` methods now take a `URI` instead of a `URL`. The `URL` methods have been deprecated. - -Note: Should you be calling `WebSocketClient.create(null)`, the method call is now ambiguous. Insert a cast in that case: `WebSocketClient.create((URI) null)` - -==== SSL handshake timeout configuration - -The SSL handshake timeout can now be configured using the `micronaut.ssl.handshakeTimeout` and `micronaut.http.client.ssl.handshakeTimeout` configurations for the server and client respectively. - -=== Module Upgrades - -==== Micronaut Data 3.2.0 - -- Repositories with JPA Criteria API specification for Micronaut JDBC/R2DBC -- Expandable query parameters optimizations - -==== Reactive Modules - -- The RxJava2, RxJava3, and Reactor modules have been updated with the equivalent static `create` methods on their core counterparts. - -==== Micronaut Micrometer 4.1.0 - -- Adds support for metrics with gRPC - -==== Micronaut Security 3.2.0 - -- The way JSON Web Key Sets are being cached has been greatly improved for scenarios where there are multiple key sets. - -==== Other Module Upgrades - -- Elasticsearch 7.15.2 -- Flyway 8.0.2 -- gRPC 1.39.0 -- Liquibase 4.6.1 -- Micronaut Elasticsearch 4.0.0 -- Micronaut Flyway 5.0.0 -- Micronaut gRPC 3.1.1 -- Micronaut Liquibase 5.0.0 -- Micronaut OpenAPI 3.2.0 -- Micronaut Redis 5.1.0 -- Testcontainers 1.16.1 - -== 3.1.0 - -=== Core Features - -==== Primitive Beans - -<> can now create beans that are primitive types or primitive array types. - -See the section on <> in the documentation for more information. - -==== Repeatable Qualifiers - -<> can now be repeatable (an annotation annotated with `java.lang.annotation.Repeatable`) allowing narrowing bean resolution by a complete or partial match of the qualifiers declared on the injection point. - -==== InjectScope - -A new ann:context.annotation.InjectScope[] annotation has been added which destroys any beans with no defined scope and injected into a method or constructor annotated with `@Inject` after the method or constructor completes. - -==== More Build Time Optimizations - -Further build time metadata optimizations have been added included reducing the number and size of the classes generated to support <> and including knowledge of repeatable annotations in generated metadata avoiding further reflective calls and optimizing Micronaut's memory usage, in particular with GraalVM. - -==== Improvements to Context Propagation - -Support for <> has been further improved by inclusion of request context information in the https://projectreactor.io/docs/core/release/reference/#context[Reactor context] and <> when using Kotlin coroutines. - -==== Improvements to the Element API - -The build-time api:inject.ast.Element[] API has been improved in a number of ways: - -* New methods were added to the api:inject.ast.MethodElement[] API to resolve the retriever type and throws declaration -* A new experimental API has been added to the api:inject.ast.ClassElement[] API to resolve generic placeholders and resolve the generic bound to the element - -=== HTTP Features - -==== Filter By Regex - -HTTP filters now support matching URLs by a regular expression. Set the `patternStyle` member of the annotation to `REGEX` and the value will be treated as a regular expression. - -==== Random Port Binding - -The way the server binds to random ports has improved and should result in fewer port binding exceptions in tests. - -==== Client Data Formatting - -The ann:core.convert.format.Format[] annotation now supports several new values that can be used in conjunction with the declarative HTTP client to support formatting data in several new ways. See the <> documentation for more information. - -==== StreamingFileUpload - -The api:http.multipart.StreamingFileUpload[] API has been improved to support streaming directly to an output stream. As with the other `transferTo` methods, the write to the stream is offloaded to the IO pool automatically. - -==== Server SSL Configuration - -The SSL configuration for the Netty server now responds to refresh events. This allows for swapping out certificates without having to restart the server. See the <> for information on how to trigger the refresh. - -==== New Netty Server API - -If you wish to programmatically start additional Netty servers on different ports with potentially different configurations, new APIs have been added to do so including a new api:http.server.netty.NettyEmbeddedServerFactory[] interface. - -See the documentation on <> for more information. - -=== Deprecations - -The `netty.responses.file.\*` configuration is deprecated in favor of `micronaut.server.netty.responses.file.*`. The old configuration key will be removed in the next major version of the framework. - -=== Module Upgrades - -==== Micronaut Data 3.1.0 - -- Kotlin's coroutines support. New repository interface `CoroutineCrudRepository`. -- Support for `AttributeConverter` -- R2DBC upgraded to `Arabba-SR11` -- JPA Criteria specifications - -==== Micronaut JAX-RS 3.1 - -The https://micronaut-projects.github.io/micronaut-jaxrs/latest/guide/[JAX-RS module] now integrated with Micronaut Security allowing binding of the JAX-RS `SecurityContext` - -==== Micronaut Kubernetes 3.1.0 - -Micronaut Kubernetes 3.1 introduces new annotation https://micronaut-projects.github.io/micronaut-kubernetes/latest/api/io/micronaut/kubernetes/client/informer/Informer.html[@Informer]. By using the annotation on the https://javadoc.io/doc/io.kubernetes/client-java/latest/io/kubernetes/client/informer/ResourceEventHandler.html[ResourceEventHandler] the Micronaut will instantiate the https://javadoc.io/doc/io.kubernetes/client-java/latest/io/kubernetes/client/informer/SharedIndexInformer.html[SharedInformer] from the official https://github.com/kubernetes-client/java[Kubernetes Java SDK]. Then you only need to take care of handling the changes of the watched Kubernetes resource. See more on https://micronaut-projects.github.io/micronaut-kubernetes/latest/guide/#kubernetes-informer[Kubernetes Informer]. - -==== Micronaut Oracle Coherence 3.0.0 - -The https://micronaut-projects.github.io/micronaut-coherence/latest/guide/[Micronaut Oracle Coherence] module is now out of preview status and includes broad integration with Oracle Coherence including support for caching, messaging and Micronaut Data. - -== 3.0.0 - -=== Core Features - -==== Optimized Build-Time Metadata - -Micronaut 3.0 introduces a new build time metadata format that is more efficient in terms of startup and code size. - -The result is significant improvements to startup and native image sizes when building native images with GraalVM Native Image. - -It is recommended that users re-compile their applications and libraries with Micronaut 3.0 to benefit from these changes. - -==== Support for GraalVM 21.2 - -Micronaut has been updated to support the latest GraalVM 21.2 release. - -==== Jakarta Inject - -The `jakarta.inject` annotations are now the default injection annotations for Micronaut 3 - -==== Support for JSR-330 Bean Import - -Using the ann:context.annotation.Import[] annotation it is now possible to import bean definitions into your application where JSR-330 (either `javax.inject` or `jakarta.inject` annotations) are used in an external library. - -See the documentation on <> for more information. - -==== Support for Controlling Annotation Inheritance - -api:core.annotation.AnnotationMetadata[] inheritance can now be controlled via Java's `@Inherited` annotation. If an annotation is not explicitly annotated with `@Inherited` it will not be included in the metadata. See the <> section of the documentation for more information. - -NOTE: This is an important behavioural change from Micronaut 2.x, see the <> section for information on how to upgrade. - -==== Support Narrowing Injection by Generic Type Arguments - -Micronaut can now resolve the correct bean to inject based on the generic type arguments specified on the injection point: - -snippet::io.micronaut.docs.inject.generics.Vehicle[tags="constructor",indent=0] - -For more information see the section on <>. - -==== Support for using Annotation Members in Qualifiers - -You can now use annotation members in qualifiers and specify which members should be excluded with the new ann:context.annotation.NonBinding[] annotation. - -For more information see the section on <>. - -==== Support for Limiting the Injectable Types - -You can now limit the exposed types of a bean using the `typed` member of the ann:context.annotation.Bean[] annotation: - -snippet::io.micronaut.docs.inject.typed.V8Engine[tags="class",indent=0] - -For more information see the section on <>. - -==== Factories can produce bean from fields - -Beans defined with the ann:context.annotation.Factory[] annotation can now produce beans from public or package protected fields, for example: - -snippet::io.micronaut.docs.factories.VehicleMockSpec[tags="class",indent=0] - -For more information see the <> section of the documentation. - -==== Enhanced `BeanProvider` Interface - -The api:context.BeanProvider[] interface has been enhanced with new methods such as `iterator()` and `stream()` as well as methods to check for bean existence and uniqueness. - -==== New `@Any` Qualifier for use in Bean Factories - -A new ann:context.annotation.Any[] qualifier has been introduced to allow injecting any available instance into an injection point and can be used in combination with the new `BeanProvider` interface mentioned above to allow more dynamic behaviour. - -snippet::io.micronaut.docs.qualifiers.any.Vehicle[tags="imports,clazz", indent=0, title="Using BeanProvider with Any"] - -The annotation can also be used on ann:context.annotation.Factory[] methods to allow customization of how objects are injected via the api:inject.InjectionPoint[] API. - -==== Support for Fields in Bean Introspections - -Bean introspections on public or package protected fields are now supported: - -snippet::io.micronaut.docs.ioc.beans.User[tags="class", indent=0] - -For more information see the "Bean Fields" section of the <> documentation. - -==== `ApplicationEventPublisher` has now a generic event type - -For the performance reasons it's advised to inject an instance of `ApplicationEventPublisher` with a generic type parameter - `ApplicationEventPublisher`. - -=== AOP Features - -==== Support for Constructor Interception - -It is now possible to intercept bean construction invocations through the api:aop.ConstructorInterceptor[] interface and ann:aop.AroundConstruct[] annotation. - -See the section on <> for more information. - -==== Support for `@PostConstruct` & `@PreDestroy` Interception - -It is now possible to intercept `@PostConstruct` and `@PreDestroy` method invocations through the api:aop.MethodInterceptor[] interface and ann:aop.InterceptorBinding[] annotation. - -See the section on <> for more information. - - -==== Random Configuration Values - -It is now possible to set a max and a range for random numbers in configuration. For example to set an integer between 0 and 9, `${random.int(10)}` can be used as the configuration value. See the <> under "Using Random Properties" for more information. - -==== Project Reactor used internally instead of RxJava2 - -Micronaut 3 uses internally https://projectreactor.io[Project Reactor] instead https://github.com/ReactiveX/RxJava[RxJava 2]. Project Reactor allows -Micronaut 3 to simplify instrumentation, thanks to https://projectreactor.io/docs/core/release/api/reactor/util/context/Context.html[Reactor's Context], simplifies conversion login and eases the integration with R2DBC drivers. We recommend users to migrate to Reactor. However, it is possible to continue to use RxJava. See <>. - -=== Module Upgrades - -==== Micronaut Data 3.1.0 - -- Kotlin's coroutines support. New repository interface `CoroutineCrudRepository`. -- Support for `AttributeConverter` -- R2DBC upgraded to `Arabba-SR11` -- JPA Criteria specifications - -==== Micronaut Micrometer 4.0.0 - -The https://micronaut-projects.github.io/micronaut-micrometer/latest/guide/[Micrometer module] has been upgraded and now supports repeated definitions of the https://micrometer.io/?/docs/concepts#_the_timed_annotation[@Timed] annotation as well as also supporting the `@Counted` annotation for counters when you add the `micronaut-micrometer-annotation` dependency to your annotation processor classpath. - -==== Micronaut Oracle Cloud 2.0.0 - -Micronaut's https://micronaut-projects.github.io/micronaut-oracle-cloud/latest/guide/[Oracle Cloud Integration] has been updated with support for Cloud Monitoring and Tracing. - - -==== Micronaut Cassandra 4.0.0 - -The https://micronaut-projects.github.io/micronaut-cassandra/latest/guide/[Micronaut Cassandra] integration now includes support for GraalVM out of the box. - -==== Other Modules - -- Micronaut Acme 3.0.0 -- Micronaut Aws 3.0.0 -- Micronaut Azure 3.0.0 -- Micronaut Cache 3.0.0 -- Micronaut Discovery Client 3.0.0 -- Micronaut ElasticSearch 3.0.0 -- Micronaut Flyway 4.1.0 -- Micronaut GCP 4.0.0 -- Micronaut GraphQL 3.0.0 -- Micronaut Groovy 3.0.0 -- Micronaut Grpc 3.0.0 -- Micronaut Jackson XML 3.0.0 -- Micronaut Jaxrs 3.0.0 -- Micronaut JMX 3.0.0 -- Micronaut Kafka 4.0.0 -- Micronaut Kotlin 3.0.0 -- Micronaut Kubernetes 3.0.0 -- Micronaut Liquibase 4.0.2 -- Micronaut Mongo 4.0.0 -- Micronaut MQTT 2.0.0 -- Micronaut Multitenancy 4.0.0 -- Micronaut Nats Io 3.0.0 -- Micronaut Neo4j 5.0.0 -- Micronaut OpenApi 3.0.1 -- Micronaut Picocli 4.0.0 -- Micronaut Problem Json 2.0.0 -- Micronaut R2DBC 2.0.0 -- Micronaut RabbitMQ 3.0.0 -- Micronaut Reactor 2.0.0 -- Micronaut Redis 5.0.0 -- Micronaut RSS 3.0.0 -- Micronaut RxJava2 1.0.0 (new) -- Micronaut RxJava3 2.0.0 -- Micronaut Security 3.0.0 -- Micronaut Servlet 3.0.0 -- Micronaut Spring 4.0.0 -- Micronaut SQL 4.0.0 -- Micronaut Test 3.0.0 -- Micronaut Views 3.0.0 - -=== Dependency Upgrades - -- Caffeine 2.9.1 -- Cassandra 4.11.1 -- Elasticsearch 7.12.0 -- Flyway 7.12.1 -- GraalVM 21.2.0 -- H2 Database 1.4.200 -- Hazelcast 4.2.1 -- Hibernate 5.5.3.Final -- Hikari 4.0.3 -- Infinispan 12.1.6.Final -- Jackson 2.12.4 -- Jaeger 1.6.0 -- Jakarta Annotation API 2.0.0 -- JAsync 1.2.2 -- JDBI 3.20.1 -- JOOQ 3.14.12 -- JUnit 5.7.2 -- Kafka 2.8.0 -- Kotlin 1.5.21 -- Kotlin Coroutines 1.5.1 -- Ktor 1.6.1 -- Liquibase 4.4.3 -- MariaDB Driver 2.7.3 -- Micrometer 1.7.1 -- MongoDB 4.3.0 -- MS SQL Driver 9.2.1.jre8 -- MySQL Driver 8.0.25 -- Neo4j Driver 4.2.7 -- Postgres Driver 42.2.23 -- Reactor 3.4.8 -- RxJava3 3.0.13 -- SLF4J 1.7.29 -- Snake YAML 1.29 -- Spock 2.0-groovy-3.0 -- Spring 5.3.9 -- Spring Boot 2.5.3 -- Testcontainers 1.15.3 -- Tomcat JDBC 10.0.8 -- Vertx SQL Drivers 4.1.1 From dff2a455aa0b82e11c63873e95be27be58197c27 Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Thu, 16 Mar 2023 15:09:18 +0100 Subject: [PATCH 600/743] doc: add websocket dependency (#8953) * Micronaut Webscoket dependency * doc: add websocket dependency --- src/main/docs/guide/httpServer/websocket.adoc | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/main/docs/guide/httpServer/websocket.adoc b/src/main/docs/guide/httpServer/websocket.adoc index e07df7d1083..f865217fe8e 100644 --- a/src/main/docs/guide/httpServer/websocket.adoc +++ b/src/main/docs/guide/httpServer/websocket.adoc @@ -1 +1,5 @@ Micronaut features dedicated support for creating WebSocket clients and servers. The pkg:websocket.annotation[] package includes annotations for defining both clients and servers. + +WARNING: Since Micronaut Framework 4.0. `io.micronaut:micronaut-http-server` no longer exposes `micronaut-websocket` transitively. To use annotations such as ann:websocket.annotation.ServerWebSocket[], add the `micronaut-websocket` dependency to your application classpath: + +dependency::micronaut-websocket[] From 5e0e6767f3f1c39efc0c22ca3b6bdcbe768aa25b Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Thu, 16 Mar 2023 16:55:50 +0100 Subject: [PATCH 601/743] docs: what's new support Kotlin 1.8.0 (#8785) --- src/main/docs/guide/introduction/whatsNew.adoc | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/main/docs/guide/introduction/whatsNew.adoc b/src/main/docs/guide/introduction/whatsNew.adoc index 64eda6513a7..2ed9b4e0485 100644 --- a/src/main/docs/guide/introduction/whatsNew.adoc +++ b/src/main/docs/guide/introduction/whatsNew.adoc @@ -1,13 +1,16 @@ //Micronaut {version} includes the following changes: == 4.0.0 +=== Kotlin 1.8.0 + +Micronaut Framework 4.0 supports https://kotlinlang.org/docs/whatsnew18.html[Kotlin 1.8.0] + === Apache Groovy 4.0 Micronaut Framework 4.x supports https://groovy-lang.org/releasenotes/groovy-4.0.html[Apache Groovy 4.0]. === Core Changes - * <> * <> From 270734a035b8da357b2be9fb572466ac5eb208b0 Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Thu, 16 Mar 2023 21:11:52 +0100 Subject: [PATCH 602/743] doc: Add KSP section and mention it in what's new (#8786) --- .../docs/guide/introduction/whatsNew.adoc | 4 + .../docs/guide/languageSupport/kotlin.adoc | 44 +------ .../languageSupport/kotlin/controller.adoc | 18 +++ .../guide/languageSupport/kotlin/kapt.adoc | 26 ++++ .../languageSupport/kotlin/kaptOrKsp.adoc | 18 +++ .../guide/languageSupport/kotlin/ksp.adoc | 123 ++++++++++++++++++ src/main/docs/guide/toc.yml | 4 + 7 files changed, 194 insertions(+), 43 deletions(-) create mode 100644 src/main/docs/guide/languageSupport/kotlin/controller.adoc create mode 100644 src/main/docs/guide/languageSupport/kotlin/kapt.adoc create mode 100644 src/main/docs/guide/languageSupport/kotlin/kaptOrKsp.adoc create mode 100644 src/main/docs/guide/languageSupport/kotlin/ksp.adoc diff --git a/src/main/docs/guide/introduction/whatsNew.adoc b/src/main/docs/guide/introduction/whatsNew.adoc index 2ed9b4e0485..bb780eb8ef4 100644 --- a/src/main/docs/guide/introduction/whatsNew.adoc +++ b/src/main/docs/guide/introduction/whatsNew.adoc @@ -5,6 +5,10 @@ Micronaut Framework 4.0 supports https://kotlinlang.org/docs/whatsnew18.html[Kotlin 1.8.0] +=== Experimental Support for Kotlin Symbol Processing (KSP) + +Micronaut Framework has offered support for Kotlin via <>. With version 4.0, Micronaut Framework supports Kotlin also via <>. + === Apache Groovy 4.0 Micronaut Framework 4.x supports https://groovy-lang.org/releasenotes/groovy-4.0.html[Apache Groovy 4.0]. diff --git a/src/main/docs/guide/languageSupport/kotlin.adoc b/src/main/docs/guide/languageSupport/kotlin.adoc index 244f163c790..cee774c6563 100644 --- a/src/main/docs/guide/languageSupport/kotlin.adoc +++ b/src/main/docs/guide/languageSupport/kotlin.adoc @@ -6,49 +6,7 @@ TIP: The <> for Micronaut includes special support $ mn create-app hello-world --lang kotlin ---- -Support for Kotlin in Micronaut is built upon the https://kotlinlang.org/docs/reference/kapt.html[Kapt] compiler plugin, which includes support for Java annotation processors. To use Kotlin in your Micronaut application, add the proper dependencies to configure and run kapt on your `kt` source files. Kapt creates Java "stub" classes for your Kotlin classes, which can then be processed by Micronaut's Java annotation processor. The stubs are not included in the final compiled application. +Since the 4.0 release, Micronaut Framework offers support for Kotlin via https://kotlinlang.org/docs/reference/kapt.html[Kapt] or https://kotlinlang.org/docs/ksp-overview.html[Kotlin Symbol Processing API]. -TIP: Learn more about kapt and its features from the https://kotlinlang.org/docs/reference/kapt.html[official documentation.] -The Micronaut annotation processors are declared in the `kapt` scope when using Gradle. For example: -[source,groovy] -.Example build.gradle ----- -dependencies { - compile "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlinVersion" //<1> - compile "org.jetbrains.kotlin:kotlin-reflect:$kotlinVersion" - kapt "io.micronaut:micronaut-inject-java" //<2> - - kaptTest "io.micronaut:micronaut-inject-java" //<3> - ... -} ----- - -<1> Add the Kotlin standard libraries -<2> Add the `micronaut-inject-java` dependency under the `kapt` scope, so classes in `src/main` are processed -<3> Add the `micronaut-inject-java` dependency under the `kaptTest` scope, so classes in `src/test` are processed. - -With a `build.gradle` file similar to the above, you can now run your Micronaut application using the `run` task (provided by the Application plugin): - -[source,bash] -$ ./gradlew run - -An example controller written in Kotlin can be seen below: - -[source, kotlin] -.src/main/kotlin/example/HelloController.kt ----- -package example - -import io.micronaut.http.annotation.* - -@Controller("/") -class HelloController { - - @Get("/hello/{name}") - fun hello(name: String): String { - return "Hello $name" - } -} ----- diff --git a/src/main/docs/guide/languageSupport/kotlin/controller.adoc b/src/main/docs/guide/languageSupport/kotlin/controller.adoc new file mode 100644 index 00000000000..17e7c8a1cf9 --- /dev/null +++ b/src/main/docs/guide/languageSupport/kotlin/controller.adoc @@ -0,0 +1,18 @@ +An example controller written in Kotlin can be seen below: + +[source, kotlin] +.src/main/kotlin/example/HelloController.kt +---- +package example + +import io.micronaut.http.annotation.* + +@Controller("/") +class HelloController { + + @Get("/hello/{name}") + fun hello(name: String): String { + return "Hello $name" + } +} +---- diff --git a/src/main/docs/guide/languageSupport/kotlin/kapt.adoc b/src/main/docs/guide/languageSupport/kotlin/kapt.adoc new file mode 100644 index 00000000000..985ef4c63dc --- /dev/null +++ b/src/main/docs/guide/languageSupport/kotlin/kapt.adoc @@ -0,0 +1,26 @@ +The https://kotlinlang.org/docs/reference/kapt.html[Kapt] compiler plugin includes support for Java annotation processors. To use Kotlin in your Micronaut application, add the proper dependencies to configure and run kapt on your `kt` source files. Kapt creates Java "stub" classes for your Kotlin classes, which can then be processed by Micronaut's Java annotation processor. The stubs are not included in the final compiled application. + +TIP: Learn more about kapt and its features from the https://kotlinlang.org/docs/reference/kapt.html[official documentation.] + +The Micronaut annotation processors are declared in the `kapt` scope when using Gradle. For example: + +[source,groovy] +.Example build.gradle +---- +dependencies { + compile "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlinVersion" //<1> + compile "org.jetbrains.kotlin:kotlin-reflect:$kotlinVersion" + kapt "io.micronaut:micronaut-inject-java" //<2> + kaptTest "io.micronaut:micronaut-inject-java" //<3> + ... +} +---- + +<1> Add the Kotlin standard libraries +<2> Add the `micronaut-inject-java` dependency under the `kapt` scope, so classes in `src/main` are processed +<3> Add the `micronaut-inject-java` dependency under the `kaptTest` scope, so classes in `src/test` are processed. + +With a `build.gradle` file similar to the above, you can now run your Micronaut application using the `run` task (provided by the https://docs.gradle.org/current/userguide/application_plugin.html[Application plugin]): + +[source,bash] +$ ./gradlew run diff --git a/src/main/docs/guide/languageSupport/kotlin/kaptOrKsp.adoc b/src/main/docs/guide/languageSupport/kotlin/kaptOrKsp.adoc new file mode 100644 index 00000000000..20850c1cdff --- /dev/null +++ b/src/main/docs/guide/languageSupport/kotlin/kaptOrKsp.adoc @@ -0,0 +1,18 @@ +Micronaut Framework has offered support for Kotlin via https://kotlinlang.org/docs/reference/kapt.html[Kapt]. + +With version 4.0, Micronaut Framework supports Kotlin also via https://kotlinlang.org/docs/ksp-overview.html[Kotlin Symbol Processing (KSP) API]. + +Please note that KAPT is in maintenance mode. Micronaut framework 4 includes experimental support for KSP which Kotlin users should consider migrating in the future. + +____ +kapt is in maintenance mode. We are keeping it up-to-date with recent Kotlin and Java releases but have no plans to implement new features. +____ + +KAPT supports existing Java annotation processors by generating Java stubs and feeding them into the Java annotation processors. + +By skipping the generation of stubs, KSP offers several advantages: + +* Faster compilation. +* Better support Kotlin native syntax. + +WARNING: If you use other annotation processors besides the Micronaut annotation processors, they will not work with KSP. diff --git a/src/main/docs/guide/languageSupport/kotlin/ksp.adoc b/src/main/docs/guide/languageSupport/kotlin/ksp.adoc new file mode 100644 index 00000000000..7fa203a2117 --- /dev/null +++ b/src/main/docs/guide/languageSupport/kotlin/ksp.adoc @@ -0,0 +1,123 @@ +You can build Micronaut applications with Kotlin and https://kotlinlang.org/docs/ksp-overview.html[KSP]: + +____ +Kotlin Symbol Processing (KSP) is an API that you can use to develop lightweight compiler plugins. KSP provides a simplified compiler plugin API that leverages the power of Kotlin while keeping the learning curve at a minimum. Compared to kapt, annotation processors that use KSP can run up to 2 times faster. +____ + +If you use the https://micronaut-projects.github.io/micronaut-gradle-plugin/latest/[Micronaut Gradle Plugin], you can build Micronaut applications with Kotlin and https://kotlinlang.org/docs/ksp-overview.html[KSP]. You need to apply the `com.google.devtools.ksp` Gradle plugin. + +[source,kotlin] +.build.gradle.kts +---- +plugins { + id("org.jetbrains.kotlin.jvm") version "1.8.10" + id("com.google.devtools.ksp") version "1.8.10-1.0.9" + id("org.jetbrains.kotlin.plugin.allopen") version "1.8.10" + id("io.micronaut.application") version "4.0.0" +} +version = "0.1" +group = "example.micronaut" +repositories { + mavenCentral() +} +dependencies { + runtimeOnly("ch.qos.logback:logback-classic") + runtimeOnly("org.yaml:snakeyaml") + implementation("io.micronaut:micronaut-jackson-databind") + testImplementation("io.micronaut:micronaut-http-client") +} +application { + mainClass.set("example.micronaut.Application") +} +java { + sourceCompatibility = JavaVersion.toVersion("17") + targetCompatibility = JavaVersion.toVersion("17") +} +tasks { + compileKotlin { + kotlinOptions { + jvmTarget = "17" + } + } + compileTestKotlin { + kotlinOptions { + jvmTarget = "17" + } + } +} +graalvmNative.toolchainDetection.set(false) +micronaut { + runtime("netty") + testRuntime("junit5") + processing { + incremental(true) + annotations("example.micronaut.*") + } +} +---- + +If you don't use the https://micronaut-projects.github.io/micronaut-gradle-plugin/latest/[Micronaut Gradle Plugin], in addition to applying the `com.google.devtools.ksp` Gradle plugin, you have to add `micronaut-inject-kotlin` with the `ksp` configuration. + +[source, kotlin] +---- +plugins { + id("org.jetbrains.kotlin.jvm") version "1.8.10" + id("com.google.devtools.ksp") version "1.8.10-1.0.9" + id("org.jetbrains.kotlin.plugin.allopen") version "1.8.10" + application +} +version = "0.1" +group = "dockerisms" +repositories { + mavenCentral() + + maven { + url = uri("https://s01.oss.sonatype.org/content/repositories/snapshots/") + } +} +val micronautVersion by properties +dependencies { + runtimeOnly("ch.qos.logback:logback-classic") + runtimeOnly("org.yaml:snakeyaml") + implementation("io.micronaut:micronaut-jackson-databind") + + + implementation(platform("io.micronaut.platform:micronaut-platform:$micronautVersion")) + implementation("io.micronaut:micronaut-http-server-netty") + + ksp(platform("io.micronaut.platform:micronaut-platform:$micronautVersion")) + ksp("io.micronaut:micronaut-inject-kotlin") + kspTest(platform("io.micronaut.platform:micronaut-platform:$micronautVersion")) + kspTest("io.micronaut:micronaut-inject-kotlin") + + testImplementation(platform("io.micronaut.platform:micronaut-platform:$micronautVersion")) + testImplementation("io.micronaut:micronaut-http-client") + testImplementation("io.micronaut.test:micronaut-test-junit5") + testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine") +} + +application { + mainClass.set("dockerisms.Application") +} + +java { + sourceCompatibility = JavaVersion.toVersion("17") + targetCompatibility = JavaVersion.toVersion("17") +} + +tasks { + compileKotlin { + kotlinOptions { + jvmTarget = "17" + } + } + compileTestKotlin { + kotlinOptions { + jvmTarget = "17" + } + } + withType { + useJUnitPlatform() + } +} +---- diff --git a/src/main/docs/guide/toc.yml b/src/main/docs/guide/toc.yml index dbb8fb47e40..f90cca28647 100644 --- a/src/main/docs/guide/toc.yml +++ b/src/main/docs/guide/toc.yml @@ -267,6 +267,10 @@ languageSupport: groovy: Micronaut for Groovy kotlin: title: Micronaut for Kotlin + kaptOrKsp: Kotlin support via KAPT or KSP + kapt: Kotlin Annotation Processing (KAPT) + ksp: Kotlin Symbol Processing (KSP) + controller: Controller in Kotlin kaptintellij: Kotlin, Kapt and IntelliJ gradlekapt: Incremental Annotation Processing with Gradle and Kapt openandaop: Kotlin and AOP Advice From 9684680dd3bf812a3e38e2974ee70f73c967b300 Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Fri, 17 Mar 2023 14:39:12 +0100 Subject: [PATCH 603/743] doc: introduction mention server & client runtimes (#8960) --- gradle.properties | 2 ++ src/main/docs/guide/introduction.adoc | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 3835ab40810..11c9fe3ed92 100644 --- a/gradle.properties +++ b/gradle.properties @@ -23,6 +23,7 @@ testsviews=views/src/test testswebsocket=http-server-netty/src/test/groovy/io/micronaut/http/server/netty/websocket/ metricscore=configurations/micrometer-core/src/main/java/io/micronaut/configuration/metrics examples=examples +javaseapi17=https://docs.oracle.com/en/java/javase/17/docs/api jdkapi=https://docs.oracle.com/javase/8/docs/api jee7api=https://docs.oracle.com/javaee/7/api jeeapi=https://docs.oracle.com/javaee/6/api @@ -36,6 +37,7 @@ micronautcacheapi=https://micronaut-projects.github.io/micronaut-cache/latest/ap micronautreactorapi=https://micronaut-projects.github.io/micronaut-reactor/latest/api micronautsessionapi=https://micronaut-projects.github.io/micronaut-session/snapshot/api micronautsessiondocs=https://micronaut-projects.github.io/micronaut-session/snapshot/guide +micronautservletdocs=https://micronaut-projects.github.io/micronaut-servlet/latest/guide/ micronautspringapi=https://micronaut-projects.github.io/micronaut-spring/latest/api micronauttracingapi=https://micronaut-projects.github.io/micronaut-tracing/latest/api hibernateapi=http://docs.jboss.org/hibernate/orm/current/javadocs diff --git a/src/main/docs/guide/introduction.adoc b/src/main/docs/guide/introduction.adoc index 798f9de5e51..6abca8a22bb 100644 --- a/src/main/docs/guide/introduction.adoc +++ b/src/main/docs/guide/introduction.adoc @@ -26,6 +26,6 @@ At the same time Micronaut aims to avoid the downsides of frameworks like Spring Historically, frameworks such as Spring and Grails were not designed to run in scenarios such as serverless functions, Android apps, or low memory footprint microservices. In contrast, Micronaut is designed to be suitable for all of these scenarios. -This goal is achieved through the use of Java's https://docs.oracle.com/javase/8/docs/api/javax/annotation/processing/Processor.html[annotation processors], which are usable on any JVM language that supports them, as well as an HTTP Server and Client built on https://netty.io/[Netty]. To provide a similar programming model to Spring and Grails, these annotation processors precompile the necessary metadata to perform DI, define AOP proxies and configure your application to run in a low-memory environment. +This goal is achieved through the use of Java's link:{javaseapi17}/java.compiler/javax/annotation/processing/Processor.html[annotation processors], which are usable on any JVM language that supports them, as well as an HTTP Server (with several runtimes https://netty.io/[Netty], link:{micronautservletdocs}#jetty[Jetty], link:{micronautservletdocs}#tomcat[Tomcat], link:{micronautservletdocs}#undertow[Undertow]...) and an HTTP Client (with several runtimes <>, <>, ...). To provide a similar programming model to Spring and Grails, these annotation processors precompile the necessary metadata to perform DI, define AOP proxies and configure your application to run in a low-memory environment. Many APIs in Micronaut are heavily inspired by Spring and Grails. This is by design, and helps bring developers up to speed quickly. From 84852a75dc5bb9490d46aed90c92159cbfc391ef Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Fri, 17 Mar 2023 14:39:25 +0100 Subject: [PATCH 604/743] docs: validation module (#8957) Close #8876 --- gradle.properties | 1 + src/main/docs/guide/aop/validation.adoc | 8 +- src/main/docs/guide/appendix/breaks.adoc | 9 ++ .../docs/guide/httpServer/datavalidation.adoc | 6 +- src/main/docs/guide/ioc/beanValidation.adoc | 149 +----------------- .../guide/languageSupport/java/lombok.adoc | 6 +- 6 files changed, 23 insertions(+), 156 deletions(-) diff --git a/gradle.properties b/gradle.properties index 11c9fe3ed92..d4edec21c44 100644 --- a/gradle.properties +++ b/gradle.properties @@ -40,6 +40,7 @@ micronautsessiondocs=https://micronaut-projects.github.io/micronaut-session/snap micronautservletdocs=https://micronaut-projects.github.io/micronaut-servlet/latest/guide/ micronautspringapi=https://micronaut-projects.github.io/micronaut-spring/latest/api micronauttracingapi=https://micronaut-projects.github.io/micronaut-tracing/latest/api +micronautvalidationdocs=https://micronaut-projects.github.io/micronaut-validation/snapshot/guide/index.html hibernateapi=http://docs.jboss.org/hibernate/orm/current/javadocs rsapi=http://www.reactive-streams.org/reactive-streams-1.0.3-javadoc projectUrl=https://micronaut.io diff --git a/src/main/docs/guide/aop/validation.adoc b/src/main/docs/guide/aop/validation.adoc index e6a79739dac..3d434aac454 100644 --- a/src/main/docs/guide/aop/validation.adoc +++ b/src/main/docs/guide/aop/validation.adoc @@ -4,10 +4,12 @@ Validation advice is built on https://beanvalidation.org/2.0/spec/[Bean Validati Micronaut provides native support for the `jakarta.validation` annotations with the `micronaut-validation` dependency: -dependency::micronaut-validation[] +dependency:micronaut-validation-processor[groupId="io.micronaut.validation",scope="annotationProcessor"] -Or full JSR 380 compliance with the `micronaut-hibernate-validator` dependency: +dependency:micronaut-validation[groupId="io.micronaut.validation"] -dependency::micronaut-hibernate-validator[] +Or full https://beanvalidation.org/2.0/spec/[JSR 380] compliance with the `micronaut-hibernate-validator` dependency: + +dependency::micronaut-hibernate-validator[groupId="io.micronaut.beanvalidation"] See the section on <> for more information on how to apply validation rules to your bean classes. diff --git a/src/main/docs/guide/appendix/breaks.adoc b/src/main/docs/guide/appendix/breaks.adoc index a1a1772e703..92cf24c8ec2 100644 --- a/src/main/docs/guide/appendix/breaks.adoc +++ b/src/main/docs/guide/appendix/breaks.adoc @@ -53,6 +53,15 @@ The instrumentation features for Reactor have been moved to the `micronaut-react dependency:micronaut-reactor[groupId="io.micronaut.reactor"] + +==== Validation Support Moved to Validation Module + +The validation features link:{micronautvalidationdocs}[have been moved to a separate module]. Moreover, the new validation module requires you to use `micronaut-validation-processor` in the annotation processor classpath. + +dependency:micronaut-validation-processor[groupId="io.micronaut.validation",scope="annotationProcessor"] + +dependency:micronaut-validation[groupId="io.micronaut.validation"] + ==== Session Support Moved to Session Module The Session handling features link:{micronautsessiondocs}[have been moved to its own module]. If you use the HTTP session module, change the maven coordinates from `io.micronaut:micronaut-session` to `io.micronaut.session:micronaut-session`. diff --git a/src/main/docs/guide/httpServer/datavalidation.adoc b/src/main/docs/guide/httpServer/datavalidation.adoc index 08ae3388cdd..432a1c0eab0 100644 --- a/src/main/docs/guide/httpServer/datavalidation.adoc +++ b/src/main/docs/guide/httpServer/datavalidation.adoc @@ -2,9 +2,11 @@ It is easy to validate incoming data with Micronaut controllers using < The method is called with a blank string -<2> An exception occurs - -=== Validating Data Classes - -To validate data classes, e.g. POJOs (typically used in JSON interchange), the class must be annotated with ann:core.annotation.Introspected[] (see the previous section on <>) or, if the class is external, be imported by the `@Introspected` annotation. - -.POJO Validation Example -snippet::io.micronaut.docs.ioc.validation.Person[tags="class"] - -TIP: The ann:core.annotation.Introspected[] annotation can be used as a meta-annotation; common annotations like `@javax.persistence.Entity` are treated as `@Introspected` - -The above example defines a `Person` class that has two properties (`name` and `age`) that have constraints applied. Note that in Java the annotations can be on the field or the getter, and with Kotlin data classes, the annotation should target the field. - -To validate the class manually, inject an instance of api:validation.validator.Validator[]: - -.Manual Validation Example -snippet::io.micronaut.docs.ioc.validation.pojo.PersonServiceSpec[tags="validator", indent="0"] - -<1> The validator validates the person -<2> The constraint violations are verified - -Alternatively on Bean methods you can use `jakarta.validation.Valid` to trigger cascading validation: - -.ConstraintViolationException Example -snippet::io.micronaut.docs.ioc.validation.pojo.PersonService[tags="class",indent="0"] - -The `PersonService` now validates the `Person` class when invoked: - -.Manual Validation Example -snippet::io.micronaut.docs.ioc.validation.pojo.PersonServiceSpec[tags="validate-service",indent="0"] - -<1> A validated method is invoked -<2> The constraint violations are verified - -=== Validating Configuration Properties - -You can also validate the properties of classes that are annotated with ann:context.annotation.ConfigurationProperties[] to ensure configuration is correct. - -NOTE: It is recommended that you annotate ann:context.annotation.ConfigurationProperties[] that features validation with ann:context.annotation.Context[] to ensure that the validation occurs at startup. - -=== Defining Additional Constraints - -To define additional constraints, create a new annotation, for example: - -.Example Constraint Annotation -snippet::io.micronaut.docs.ioc.validation.custom.DurationPattern[tags="imports,class", indent="0"] - -<1> The annotation should be annotated with `jakarta.validation.Constraint` -<2> A `message` template can be provided in a hard-coded manner as above. If none is specified, Micronaut tries to find a message using `ClassName.message` using the api:context.MessageSource[] interface (optional) -<3> To support repeated annotations you can define an inner annotation (optional) - -TIP: You can add messages and message bundles using the api:context.MessageSource[] and api:context.i18n.ResourceBundleMessageSource[] classes. See <> documentation. - -Once you have defined the annotation, implement a api:validation.validator.constraints.ConstraintValidator[] that validates the annotation. You can either create a bean class that implements the interface directly or define a factory that returns one or more validators. - -The latter approach is recommended if you plan to define multiple validators: - -.Example Constraint Validator -snippet::io.micronaut.docs.ioc.validation.custom.MyValidatorFactory[tags="imports,class", indent="0"] - -<1> Override the default message template with an inline call for more control over the validation error message. (Since `2.5.0`) - -The above example implements a validator that validates any field, parameter etc. that is annotated with `DurationPattern`, ensuring that the string can be parsed with `java.time.Duration.parse`. - -NOTE: Generally `null` is regarded as valid and `@NotNull` is used to constrain a value as not being `null`. The example above regards `null` as a valid value. - -For example: - -.Example Custom Constraint Usage -snippet::io.micronaut.docs.ioc.validation.custom.HolidayService[tags="class", indent="0"] - -To verify the above examples validates the `duration` parameter, define a test: - -.Testing Example Custom Constraint Usage -snippet::io.micronaut.docs.ioc.validation.custom.DurationPatternValidatorSpec[tags="test", indent="0"] - -<1> A validated method is invoked -<2> THe constraint violations are verified - -=== Validating Annotations at Compile Time - -You can use Micronaut's validator to validate annotation elements at compile time by including `micronaut-validation` in the annotation processor classpath: - -dependency::micronaut-validation[scope="annotationProcessor"] - -Then Micronaut will at compile time validate annotation values that are themselves annotated with `jakarta.validation`. For example consider the following annotation: - -.Annotation Validation -snippet::io.micronaut.docs.ioc.validation.custom.TimeOff[tags="imports,class", indent="0"] - -If you attempt to use `@TimeOff(duration="junk")` in your source, Micronaut will fail compilation due to the `duration` value violating the `DurationPattern` constraint. - -NOTE: If `duration` is a property placeholder such as `@TimeOff(duration="${my.value}")`, validation is deferred until runtime. - -Note that to use a custom `ConstraintValidator` at compile time you must instead define the validator as a class: - -.Example Constraint Validator -snippet::io.micronaut.docs.ioc.validation.custom.DurationPatternValidator[tags="imports,class", indent="0"] - -Additionally: - -* Define a `META-INF/services/io.micronaut.validation.validator.constraints.ConstraintValidator` file that references the class. -* The class must be public and have a public no-argument constructor -* The class must be on the annotation processor classpath of the project to be validated. +See the link:{micronautvalidationdocs}[Micronaut Validation documentation]. diff --git a/src/main/docs/guide/languageSupport/java/lombok.adoc b/src/main/docs/guide/languageSupport/java/lombok.adoc index 0f9d56e480a..26550f90efa 100644 --- a/src/main/docs/guide/languageSupport/java/lombok.adoc +++ b/src/main/docs/guide/languageSupport/java/lombok.adoc @@ -40,9 +40,9 @@ Or if using Maven: micronaut-inject-java ${micronaut.version} - - io.micronaut - micronaut-validation + + io.micronaut.validation + micronaut-validation-processor ${micronaut.version} From 101feb709aa8b6d596555d3594f47b2f4292ea13 Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Fri, 17 Mar 2023 15:02:58 +0100 Subject: [PATCH 605/743] doc: retry dependency in Advice & HTTP client (#8961) --- src/main/docs/guide/aop/retry.adoc | 6 ++++++ .../docs/guide/httpClient/clientAnnotation/clientRetry.adoc | 4 ++++ 2 files changed, 10 insertions(+) diff --git a/src/main/docs/guide/aop/retry.adoc b/src/main/docs/guide/aop/retry.adoc index c75303286b9..1177e0687b8 100644 --- a/src/main/docs/guide/aop/retry.adoc +++ b/src/main/docs/guide/aop/retry.adoc @@ -2,6 +2,12 @@ In distributed systems and microservice environments, failure is something you h With this in mind, Micronaut includes a api:retry.annotation.Retryable[] annotation. +== Retry Dependency + +NOTE: Since Micronaut Framework 4.0 to use the Retry functionality you need to add the following dependency: + +dependency::micronaut-retry[] + == Simple Retry The simplest form of retry is just to add the `@Retryable` annotation to a type or method. The default behaviour of `@Retryable` is to retry three times with a linear delay of one second between each retry. (first attempt with 1s delay, second attempt with 2s delay, third attempt with 3s delay). diff --git a/src/main/docs/guide/httpClient/clientAnnotation/clientRetry.adoc b/src/main/docs/guide/httpClient/clientAnnotation/clientRetry.adoc index c1c4769c600..5f3b96638eb 100644 --- a/src/main/docs/guide/httpClient/clientAnnotation/clientRetry.adoc +++ b/src/main/docs/guide/httpClient/clientAnnotation/clientRetry.adoc @@ -1,5 +1,9 @@ Recovering from failure is critical for HTTP clients, and that is where Micronaut's integrated <> comes in handy. +NOTE: Since Micronaut Framework 4.0, declarative clients annotated with ann:http.client.annotation.Client[] no longer invoke fallbacks by default. To restore the previous behaviour add the following dependency and annotate any declarative clients with ann:retry.annotation.Recoverable[]. + +dependency::micronaut-retry[] + You can declare the ann:retry.annotation.Retryable[] or ann:retry.annotation.CircuitBreaker[] annotations on any ann:http.client.annotation.Client[] interface and the retry policy will be applied, for example: snippet::io.micronaut.docs.annotation.retry.PetClient[tags="class", indent=0, title="Declaring @Retryable"] From aa328891335d7f502a6ac09d814538db5b7a3143 Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Fri, 17 Mar 2023 22:13:27 +0100 Subject: [PATCH 606/743] fix: WebSocketMessageEncoder @Requires classes (#8962) WebSocketMessageEncoder should not be loaded unless classes WebSocketSessionException class is present --- .../micronaut/http/netty/websocket/WebSocketMessageEncoder.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/http-netty/src/main/java/io/micronaut/http/netty/websocket/WebSocketMessageEncoder.java b/http-netty/src/main/java/io/micronaut/http/netty/websocket/WebSocketMessageEncoder.java index a6e59f69d76..a2de807f1c7 100644 --- a/http-netty/src/main/java/io/micronaut/http/netty/websocket/WebSocketMessageEncoder.java +++ b/http-netty/src/main/java/io/micronaut/http/netty/websocket/WebSocketMessageEncoder.java @@ -16,6 +16,7 @@ package io.micronaut.http.netty.websocket; import io.micronaut.buffer.netty.NettyByteBufferFactory; +import io.micronaut.context.annotation.Requires; import io.micronaut.core.reflect.ClassUtils; import io.micronaut.http.MediaType; import io.micronaut.http.codec.MediaTypeCodec; @@ -38,6 +39,7 @@ * @author sdelamo * @since 1.0 */ +@Requires(classes = WebSocketSessionException.class) @Singleton public class WebSocketMessageEncoder { From 3824c39e240a94f9e6a6cebe354f8b60291f87d1 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 18 Mar 2023 09:33:13 +0100 Subject: [PATCH 607/743] fix(deps): update dependency ch.qos.logback:logback-classic to v1.4.6 (#8964) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index bf5f030dc2d..53fadd7c157 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -34,7 +34,7 @@ junit-platform="1.9.1" kotlin = "1.8.10" kotlin-coroutines = "1.6.4" ktor = "1.6.8" -managed-logback = "1.4.5" +managed-logback = "1.4.6" logbook-netty = "2.14.0" log4j = "2.19.0" micronaut-aws = "3.9.2" From d35e5a030411a14a6ff000de2f95d847f95d5ae8 Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Mon, 20 Mar 2023 11:44:11 +0100 Subject: [PATCH 608/743] ci: Github Actions Sync (#8970) see: https://github.com/micronaut-projects/micronaut-project-template/pull/346 --- .github/workflows/release.yml | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 120e5a6d329..3eb1a55d4f5 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -161,18 +161,18 @@ jobs: if: startsWith(github.ref, 'refs/tags/') steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0 - name: Download artifacts - uses: actions/download-artifact@v3 + uses: actions/download-artifact@9bc31d5ccc31df68ecc42ccf4149144866c47d8a # v3.0.2 with: name: gradle-build-outputs path: build/repo + - name: Create artifacts archive + shell: bash + run: find build/repo -regextype sed -regex '\(.*\.jar\|.*\.pom\|.*\.module\|.*\.toml\)' | xargs zip artifacts.zip - name: Upload assets - # Upload the artifacts and SLSA L3 provenance as assets to the existing - # release. Note that the provenance will attest to each artifact file and - # not the aggregated ZIP file. - run: | - find build/repo -regextype sed -regex '\(.*\.jar\|.*\.pom\|.*\.module\|.*\.toml\)' | xargs zip artifacts.zip - gh release upload ${{ github.ref_name }} artifacts.zip - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + # Upload the artifacts to the existing release. Note that the SLSA provenance will + # attest to each artifact file and not the aggregated ZIP file. + uses: softprops/action-gh-release@de2c0eb89ae2a093876385947365aca7b0e5f844 # v0.1.15 + with: + files: artifacts.zip From e3454f5d3b28646b05d1c35ee58656fef4d41575 Mon Sep 17 00:00:00 2001 From: micronaut-build Date: Mon, 20 Mar 2023 11:30:33 +0000 Subject: [PATCH 609/743] [skip ci] Release v4.0.0-M1 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index d4edec21c44..861773d18f0 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -projectVersion=4.0.0-SNAPSHOT +projectVersion=4.0.0-M1 projectGroupId=io.micronaut title=Micronaut projectDesc=Natively Cloud Native From b6514ddd51b32a7a2cb34506d92a19217e2f04e1 Mon Sep 17 00:00:00 2001 From: micronaut-build Date: Mon, 20 Mar 2023 11:44:58 +0000 Subject: [PATCH 610/743] Back to 4.0.0-SNAPSHOT --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 861773d18f0..d4edec21c44 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -projectVersion=4.0.0-M1 +projectVersion=4.0.0-SNAPSHOT projectGroupId=io.micronaut title=Micronaut projectDesc=Natively Cloud Native From 8ba4c2996ce2f47b81acafc4a36d2ea515613c64 Mon Sep 17 00:00:00 2001 From: altro3 Date: Mon, 20 Mar 2023 21:10:34 +0700 Subject: [PATCH 611/743] Move from `javax.el` to `jakarta.el`. (#8966) --- .gitattributes | 19 ++++++++++--------- gradle.properties | 1 - gradle/libs.versions.toml | 8 ++++---- inject-groovy/build.gradle | 4 ++-- inject-java/build.gradle | 4 ++-- .../PostConstructExceptionSpec.groovy | 11 +++++------ runtime/build.gradle | 2 +- 7 files changed, 24 insertions(+), 25 deletions(-) diff --git a/.gitattributes b/.gitattributes index 31628339ce7..2603050e864 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,18 +1,19 @@ # Auto detect text files and perform LF normalization * text=auto -*.java text -*.html text -*.kt text -*.kts text -*.md text diff=markdown +*.java text eol=lf +*.groovy text eol=lf +*.html text eol=lf +*.kt text eol=lf +*.kts text eol=lf +*.md text diff=markdown eol=lf *.py text diff=python executable *.pl text diff=perl executable *.pm text diff=perl -*.css text diff=css -*.js text -*.sql text -*.q text +*.css text diff=css eol=lf +*.js text eol=lf +*.sql text eol=lf +*.q text eol=lf *.sh text eol=lf gradlew text eol=lf diff --git a/gradle.properties b/gradle.properties index d4edec21c44..84f4dbacf93 100644 --- a/gradle.properties +++ b/gradle.properties @@ -45,7 +45,6 @@ hibernateapi=http://docs.jboss.org/hibernate/orm/current/javadocs rsapi=http://www.reactive-streams.org/reactive-streams-1.0.3-javadoc projectUrl=https://micronaut.io developers=Graeme Rocher -kapt.use.worker.api=true # Dependency Versions micronautMavenPluginVersion=3.5.2 diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 53fadd7c157..e318302f44b 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -26,8 +26,8 @@ jetty = "9.4.48.v20220622" jmh = "1.35" jsr107 = "1.1.1" jsr305 = "3.0.2" -javax-el = "3.0.1-b12" -javax-el-impl = "2.2.1-b05" +jakarta-el = "5.0.1" +jakarta-el-impl = "5.0.0-M1" jcache = "1.1.1" junit5 = "5.9.1" junit-platform="1.9.1" @@ -168,8 +168,8 @@ jakarta-inject-api = { module = "jakarta.inject:jakarta.inject-api", version.ref jakarta-inject-tck = { module = "jakarta.inject:jakarta.inject-tck", version.ref = "jakarta-inject-tck" } javax-annotation-api = { module = "javax.annotation:javax.annotation-api", version.ref = "javax-annotation-api" } -javax-el = { module = "org.glassfish:javax.el", version.ref = "javax-el" } -javax-el-impl = { module = "org.glassfish:javax.el", version.ref = "javax-el" } +jakarta-el = { module = "jakarta.el:jakarta.el-api", version.ref = "jakarta-el" } +jakarta-el-impl = { module = "org.glassfish:jakarta.el", version.ref = "jakarta-el-impl" } javax-inject = { module = "javax.inject:javax.inject", version.ref = "javax-inject" } javax-persistence = { module = "javax.persistence:javax.persistence-api", version.ref = "javax-persistence" } diff --git a/inject-groovy/build.gradle b/inject-groovy/build.gradle index b28b54e2b63..5303f00dc94 100644 --- a/inject-groovy/build.gradle +++ b/inject-groovy/build.gradle @@ -17,8 +17,8 @@ dependencies { testImplementation libs.spotbugs testImplementation libs.hibernate testImplementation libs.hibernate.validator - testRuntimeOnly libs.javax.el.impl - testRuntimeOnly libs.javax.el + testRuntimeOnly libs.jakarta.el.impl + testRuntimeOnly libs.jakarta.el testImplementation project(":http-server-netty") testImplementation project(":http-client") testImplementation project(":jackson-databind") diff --git a/inject-java/build.gradle b/inject-java/build.gradle index 7b17e4e0579..ac4e553556b 100644 --- a/inject-java/build.gradle +++ b/inject-java/build.gradle @@ -58,8 +58,8 @@ dependencies { } testImplementation libs.javax.annotation.api testImplementation libs.managed.snakeyaml - testRuntimeOnly libs.javax.el.impl - testRuntimeOnly libs.javax.el + testRuntimeOnly libs.jakarta.el.impl + testRuntimeOnly libs.jakarta.el } //compileTestJava.options.fork = true //compileTestJava.options.forkOptions.jvmArgs = ['-Xdebug', '-Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=5005'] diff --git a/inject-java/src/test/groovy/io/micronaut/inject/failures/postconstruct/PostConstructExceptionSpec.groovy b/inject-java/src/test/groovy/io/micronaut/inject/failures/postconstruct/PostConstructExceptionSpec.groovy index c603e35b5c9..65589e304af 100644 --- a/inject-java/src/test/groovy/io/micronaut/inject/failures/postconstruct/PostConstructExceptionSpec.groovy +++ b/inject-java/src/test/groovy/io/micronaut/inject/failures/postconstruct/PostConstructExceptionSpec.groovy @@ -16,8 +16,7 @@ package io.micronaut.inject.failures.postconstruct import io.micronaut.context.ApplicationContext -import io.micronaut.context.BeanContext -import io.micronaut.context.DefaultBeanContext +import io.micronaut.context.env.CachedEnvironment import io.micronaut.context.exceptions.BeanInstantiationException import spock.lang.Specification @@ -32,10 +31,10 @@ class PostConstructExceptionSpec extends Specification { then:"The implementation is injected" def e = thrown(BeanInstantiationException) - e.message == '''Error instantiating bean of type [io.micronaut.inject.failures.postconstruct.B] - -Message: bad -Path Taken: new B()''' + def ls = CachedEnvironment.getProperty("line.separator") + e.message == 'Error instantiating bean of type [io.micronaut.inject.failures.postconstruct.B]' + ls + ls + + 'Message: bad' + ls + + 'Path Taken: new B()' cleanup: context.close() diff --git a/runtime/build.gradle b/runtime/build.gradle index ef36489b9ea..445fdcc2b05 100644 --- a/runtime/build.gradle +++ b/runtime/build.gradle @@ -18,7 +18,7 @@ dependencies { compileOnly libs.graal compileOnly libs.jcache - compileOnly libs.javax.el + compileOnly libs.jakarta.el compileOnly libs.caffeine compileOnly libs.kotlinx.coroutines.core compileOnly libs.kotlinx.coroutines.reactive From fe828de1324a100173ceb24c62e0679a83f11a39 Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Tue, 21 Mar 2023 11:10:38 +0100 Subject: [PATCH 612/743] build: Apache Groovy 4.0.10 (#8975) --- gradle/libs.versions.toml | 2 +- .../groovy/io/micronaut/ast/groovy/visitor/LoadedVisitor.groovy | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index e318302f44b..ac7fba287ce 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -60,7 +60,7 @@ wiremock = "2.33.2" # Versions which start with managed- are managed by Micronaut in the sense # that they will appear in the Micronaut BOM as # -managed-groovy = "4.0.9" +managed-groovy = "4.0.10" managed-jakarta-annotation-api = "2.1.1" managed-jackson = "2.14.0" managed-jackson-databind = "2.14.1" diff --git a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/LoadedVisitor.groovy b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/LoadedVisitor.groovy index 5e1f61f5bff..07c373cfd5b 100644 --- a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/LoadedVisitor.groovy +++ b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/LoadedVisitor.groovy @@ -15,7 +15,6 @@ */ package io.micronaut.ast.groovy.visitor -import groovy.transform.CompileDynamic import groovy.transform.CompileStatic import io.micronaut.core.annotation.AnnotationMetadata import io.micronaut.core.annotation.Internal @@ -95,7 +94,6 @@ class LoadedVisitor implements Ordered { return true } - @CompileDynamic @Override int hashCode() { return visitor.getClass().hashCode() From b51b6594912e93b411e8e3e9fe71e2589175d9e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20S=C3=A1nchez-Mariscal?= Date: Tue, 21 Mar 2023 15:03:20 +0100 Subject: [PATCH 613/743] Bump Jib Maven Plugin version (#8980) Fixes https://github.com/micronaut-projects/micronaut-maven-plugin/issues/675 --- parent/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/parent/build.gradle b/parent/build.gradle index 6ba0d0da06c..cc551cabd8a 100644 --- a/parent/build.gradle +++ b/parent/build.gradle @@ -49,7 +49,7 @@ ext.extraPomInfo = { 'azure-functions-maven-plugin.version'('1.5.0') 'exec-maven-plugin.version'('1.6.0') 'function-maven-plugin.version'('0.9.8') - 'jib-maven-plugin.version'('3.1.4') + 'jib-maven-plugin.version'('3.3.1') 'maven-compiler-plugin.version'('3.10.1') // Override actual Maven compiler version (3.1) because some bugs cause annotation processors doesn't work well 'maven-deploy-plugin.version'('3.0.0') 'maven-failsafe-plugin.version'('2.22.2') // Override actual Maven surefire and failsafe version (2.12) to get native support for executing tests on the JUnit Platform (JUnit 5) From df72e8e996e7a5cc5a2b84fa796d01728f20ad79 Mon Sep 17 00:00:00 2001 From: Jonas Konrad Date: Tue, 21 Mar 2023 16:50:47 +0100 Subject: [PATCH 614/743] Embedded HTTP benchmark, various small perf improvements (#8974) * add benchmark for full http stack --- benchmarks/build.gradle | 2 + .../server/stack/FullHttpStackBenchmark.java | 195 ++++++++ .../stack/JmhFastThreadLocalExecutor.java | 26 + .../http/server/stack/RequestHandler.java | 120 +++++ .../http/server/stack/SearchController.java | 39 ++ .../core/execution/DelayedExecutionFlow.java | 44 ++ .../execution/DelayedExecutionFlowImpl.java | 449 ++++++++++++++++++ .../core/execution/ExecutionFlow.java | 9 +- .../execution/DelayedExecutionFlowSpec.groovy | 81 ++++ http-netty/build.gradle | 1 + .../http/netty/NettyHttpHeaders.java | 14 + .../http/netty/reactive/HandlerPublisher.java | 22 +- .../http/netty/stream/HttpStreamsHandler.java | 7 +- .../stream/HttpStreamsServerHandler.java | 7 +- .../reactive/HandlerPublisherSpec.groovy | 7 +- .../server/netty/HttpPipelineBuilder.java | 9 +- .../http/server/netty/NettyHttpRequest.java | 2 +- .../server/netty/NettyRequestLifecycle.java | 14 +- .../server/netty/cors/CorsFilterSpec.groovy | 61 ++- .../http/server/cors/CorsFilter.java | 67 ++- .../execution/ReactorExecutionFlowImpl.java | 6 +- 21 files changed, 1102 insertions(+), 80 deletions(-) create mode 100644 benchmarks/src/jmh/java/io/micronaut/http/server/stack/FullHttpStackBenchmark.java create mode 100644 benchmarks/src/jmh/java/io/micronaut/http/server/stack/JmhFastThreadLocalExecutor.java create mode 100644 benchmarks/src/jmh/java/io/micronaut/http/server/stack/RequestHandler.java create mode 100644 benchmarks/src/jmh/java/io/micronaut/http/server/stack/SearchController.java create mode 100644 core/src/main/java/io/micronaut/core/execution/DelayedExecutionFlow.java create mode 100644 core/src/main/java/io/micronaut/core/execution/DelayedExecutionFlowImpl.java create mode 100644 core/src/test/groovy/io/micronaut/core/execution/DelayedExecutionFlowSpec.groovy diff --git a/benchmarks/build.gradle b/benchmarks/build.gradle index 4da2b63b600..0d192ab3b11 100644 --- a/benchmarks/build.gradle +++ b/benchmarks/build.gradle @@ -22,6 +22,8 @@ dependencies { api project(":inject") api project(":inject-java-test") api project(":http-server") + api project(":http-server-netty") + api project(":jackson-databind") api project(":router") api project(":runtime") diff --git a/benchmarks/src/jmh/java/io/micronaut/http/server/stack/FullHttpStackBenchmark.java b/benchmarks/src/jmh/java/io/micronaut/http/server/stack/FullHttpStackBenchmark.java new file mode 100644 index 00000000000..ae33a657cd8 --- /dev/null +++ b/benchmarks/src/jmh/java/io/micronaut/http/server/stack/FullHttpStackBenchmark.java @@ -0,0 +1,195 @@ +package io.micronaut.http.server.stack; + +import io.micronaut.context.ApplicationContext; +import io.micronaut.http.server.netty.NettyHttpServer; +import io.micronaut.runtime.server.EmbeddedServer; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.CompositeByteBuf; +import io.netty.buffer.PooledByteBufAllocator; +import io.netty.buffer.Unpooled; +import io.netty.channel.embedded.EmbeddedChannel; +import io.netty.handler.codec.http.DefaultFullHttpRequest; +import io.netty.handler.codec.http.FullHttpRequest; +import io.netty.handler.codec.http.FullHttpResponse; +import io.netty.handler.codec.http.HttpClientCodec; +import io.netty.handler.codec.http.HttpHeaderNames; +import io.netty.handler.codec.http.HttpHeaderValues; +import io.netty.handler.codec.http.HttpMethod; +import io.netty.handler.codec.http.HttpObjectAggregator; +import io.netty.handler.codec.http.HttpResponseStatus; +import io.netty.handler.codec.http.HttpServerCodec; +import io.netty.handler.codec.http.HttpVersion; +import io.netty.util.concurrent.FastThreadLocalThread; +import org.junit.jupiter.api.Assertions; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.Param; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.TearDown; +import org.openjdk.jmh.profile.AsyncProfiler; +import org.openjdk.jmh.runner.Runner; +import org.openjdk.jmh.runner.options.Options; +import org.openjdk.jmh.runner.options.OptionsBuilder; + +import java.nio.charset.StandardCharsets; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +public class FullHttpStackBenchmark { + @Benchmark + public void test(Holder holder) { + ByteBuf response = holder.exchange(); + if (!holder.responseBytes.equals(response)) { + throw new AssertionError("Response did not match"); + } + response.release(); + } + + public static void main(String[] args) throws Exception { + JmhFastThreadLocalExecutor exec = new JmhFastThreadLocalExecutor(1, "init-test"); + exec.submit(() -> { + // simple test that everything works properly + for (StackFactory stack : StackFactory.values()) { + Holder holder = new Holder(); + holder.stack = stack; + holder.setUp(); + holder.tearDown(); + } + return null; + }).get(); + exec.shutdown(); + + Options opt = new OptionsBuilder() + .include(FullHttpStackBenchmark.class.getName() + ".*") + .warmupIterations(20) + .measurementIterations(30) + .mode(Mode.AverageTime) + .timeUnit(TimeUnit.NANOSECONDS) + .addProfiler(AsyncProfiler.class, "libPath=/home/yawkat/bin/async-profiler-2.9-linux-x64/build/libasyncProfiler.so;output=flamegraph") + .forks(1) + .jvmArgsAppend("-Djmh.executor=CUSTOM", "-Djmh.executor.class=" + JmhFastThreadLocalExecutor.class.getName()) + .build(); + + new Runner(opt).run(); + } + + @State(Scope.Thread) + public static class Holder { + @Param({"MICRONAUT"/*, "PURE_NETTY"*/}) + StackFactory stack = StackFactory.MICRONAUT; + + AutoCloseable ctx; + EmbeddedChannel channel; + ByteBuf requestBytes; + ByteBuf responseBytes; + + @Setup + public void setUp() { + if (!(Thread.currentThread() instanceof FastThreadLocalThread)) { + throw new IllegalStateException("Should run on a netty FTL thread"); + } + + Stack stack = this.stack.openChannel(); + ctx = stack.closeable; + channel = stack.serverChannel; + + channel.freezeTime(); + + EmbeddedChannel clientChannel = new EmbeddedChannel(); + clientChannel.pipeline().addLast(new HttpClientCodec()); + clientChannel.pipeline().addLast(new HttpObjectAggregator(1000)); + + FullHttpRequest request = new DefaultFullHttpRequest( + HttpVersion.HTTP_1_1, + HttpMethod.POST, + "/search/find", + Unpooled.wrappedBuffer("{\"haystack\": [\"xniomb\", \"seelzp\", \"nzogdq\", \"omblsg\", \"idgtlm\", \"ydonzo\"], \"needle\": \"idg\"}".getBytes(StandardCharsets.UTF_8)) + ); + request.headers().add(HttpHeaderNames.CONTENT_LENGTH, request.content().readableBytes()); + request.headers().add(HttpHeaderNames.CONTENT_TYPE, HttpHeaderValues.APPLICATION_JSON); + request.headers().add(HttpHeaderNames.ACCEPT, HttpHeaderValues.APPLICATION_JSON); + clientChannel.writeOutbound(request); + clientChannel.flushOutbound(); + + requestBytes = PooledByteBufAllocator.DEFAULT.buffer(); + while (true) { + ByteBuf part = clientChannel.readOutbound(); + if (part == null) { + break; + } + requestBytes.writeBytes(part); + } + + // sanity check: run req/resp once and see that the response is correct + responseBytes = exchange(); + clientChannel.writeInbound(responseBytes.retainedDuplicate()); + FullHttpResponse response = clientChannel.readInbound(); + //System.out.println(response); + //System.out.println(response.content().toString(StandardCharsets.UTF_8)); + Assertions.assertEquals(HttpResponseStatus.OK, response.status()); + Assertions.assertEquals("application/json", response.headers().get(HttpHeaderNames.CONTENT_TYPE)); + Assertions.assertEquals("keep-alive", response.headers().get(HttpHeaderNames.CONNECTION)); + String expectedResponseBody = "{\"listIndex\":4,\"stringIndex\":0}"; + Assertions.assertEquals(expectedResponseBody, response.content().toString(StandardCharsets.UTF_8)); + Assertions.assertEquals(expectedResponseBody.length(), response.headers().getInt(HttpHeaderNames.CONTENT_LENGTH)); + response.release(); + } + + private ByteBuf exchange() { + channel.writeInbound(requestBytes.retainedDuplicate()); + channel.runPendingTasks(); + CompositeByteBuf response = PooledByteBufAllocator.DEFAULT.compositeBuffer(); + while (true) { + ByteBuf part = channel.readOutbound(); + if (part == null) { + break; + } + response.addComponent(true, part); + } + return response; + } + + @TearDown + public void tearDown() throws Exception { + ctx.close(); + requestBytes.release(); + responseBytes.release(); + } + } + + public enum StackFactory { + MICRONAUT { + @Override + Stack openChannel() { + ApplicationContext ctx = ApplicationContext.run(Map.of( + "spec.name", "FullHttpStackBenchmark", + "micronaut.server.date-header", false // disabling this makes the response identical each time + )); + EmbeddedServer server = ctx.getBean(EmbeddedServer.class); + EmbeddedChannel channel = ((NettyHttpServer) server).buildEmbeddedChannel(false); + return new Stack(channel, ctx); + } + }, + PURE_NETTY { + @Override + Stack openChannel() { + HttpObjectAggregator aggregator = new HttpObjectAggregator(10_000_000); + aggregator.setMaxCumulationBufferComponents(100000); + EmbeddedChannel channel = new EmbeddedChannel(); + channel.pipeline().addLast(new HttpServerCodec()); + channel.pipeline().addLast(aggregator); + channel.pipeline().addLast(new RequestHandler()); + return new Stack(channel, () -> { + }); + } + }; + + abstract Stack openChannel(); + } + + private record Stack(EmbeddedChannel serverChannel, AutoCloseable closeable) { + } + +} diff --git a/benchmarks/src/jmh/java/io/micronaut/http/server/stack/JmhFastThreadLocalExecutor.java b/benchmarks/src/jmh/java/io/micronaut/http/server/stack/JmhFastThreadLocalExecutor.java new file mode 100644 index 00000000000..43850330c9b --- /dev/null +++ b/benchmarks/src/jmh/java/io/micronaut/http/server/stack/JmhFastThreadLocalExecutor.java @@ -0,0 +1,26 @@ +package io.micronaut.http.server.stack; + +import io.micronaut.core.annotation.NonNull; +import io.netty.util.concurrent.FastThreadLocalThread; + +import java.util.concurrent.SynchronousQueue; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +public final class JmhFastThreadLocalExecutor extends ThreadPoolExecutor { + public JmhFastThreadLocalExecutor(int maxThreads, String prefix) { + super(maxThreads, maxThreads, + 60L, TimeUnit.SECONDS, + new SynchronousQueue<>(), + new ThreadFactory() { + final AtomicInteger counter = new AtomicInteger(); + + @Override + public Thread newThread(@NonNull Runnable r) { + return new FastThreadLocalThread(r, prefix + "-jmh-worker-ftl-" + counter.incrementAndGet()); + } + }); + } +} diff --git a/benchmarks/src/jmh/java/io/micronaut/http/server/stack/RequestHandler.java b/benchmarks/src/jmh/java/io/micronaut/http/server/stack/RequestHandler.java new file mode 100644 index 00000000000..9354538a47b --- /dev/null +++ b/benchmarks/src/jmh/java/io/micronaut/http/server/stack/RequestHandler.java @@ -0,0 +1,120 @@ +package io.micronaut.http.server.stack; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.ObjectReader; +import com.fasterxml.jackson.databind.ObjectWriter; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufInputStream; +import io.netty.buffer.ByteBufOutputStream; +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.SimpleChannelInboundHandler; +import io.netty.handler.codec.http.DefaultFullHttpResponse; +import io.netty.handler.codec.http.FullHttpRequest; +import io.netty.handler.codec.http.FullHttpResponse; +import io.netty.handler.codec.http.HttpHeaderNames; +import io.netty.handler.codec.http.HttpHeaderValues; +import io.netty.handler.codec.http.HttpMethod; +import io.netty.handler.codec.http.HttpResponseStatus; +import io.netty.handler.codec.http.HttpVersion; +import io.netty.handler.ssl.SslContext; +import io.netty.handler.ssl.SslProvider; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.URI; +import java.util.List; + +@ChannelHandler.Sharable +final class RequestHandler extends SimpleChannelInboundHandler { + private final ObjectMapper objectMapper = new ObjectMapper(); + private final ObjectReader reader = objectMapper.readerFor(SearchController.Input.class); + private final ObjectWriter writerResult = objectMapper.writerFor(SearchController.Result.class); + private final ObjectWriter writerStatus = objectMapper.writerFor(Status.class); + + @Override + protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest msg) throws Exception { + FullHttpResponse response = computeResponse(ctx, msg); + response.headers().add(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE); + response.headers().add(HttpHeaderNames.CONTENT_LENGTH, response.content().readableBytes()); + ctx.writeAndFlush(response, ctx.voidPromise()); + ctx.read(); + } + + private FullHttpResponse computeResponse(ChannelHandlerContext ctx, FullHttpRequest msg) { + try { + String path = URI.create(msg.uri()).getPath(); + if (path.equals("/search/find")) { + return computeResponseSearch(ctx, msg); + } + if (path.equals("/status")) { + return computeResponseStatus(ctx, msg); + } + return new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.NOT_FOUND); + } catch (Exception e) { + e.printStackTrace(); + return new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.INTERNAL_SERVER_ERROR); + } + } + + private FullHttpResponse computeResponseSearch(ChannelHandlerContext ctx, FullHttpRequest msg) throws IOException { + if (!msg.method().equals(HttpMethod.POST)) { + return new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.METHOD_NOT_ALLOWED); + } + if (!msg.headers().contains(HttpHeaderNames.CONTENT_TYPE, HttpHeaderValues.APPLICATION_JSON, true)) { + return new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.UNSUPPORTED_MEDIA_TYPE); + } + + ByteBuf content = msg.content(); + SearchController.Input input; + if (content.hasArray()) { + input = reader.readValue(content.array(), content.readerIndex() + content.arrayOffset(), content.readableBytes()); + } else { + input = reader.readValue((InputStream) new ByteBufInputStream(content)); + } + + SearchController.Result result = find(input.haystack(), input.needle()); + if (result == null) { + return new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.NOT_FOUND); + } else { + return serialize(ctx, writerResult, result); + } + } + + private FullHttpResponse serialize(ChannelHandlerContext ctx, ObjectWriter writer, Object result) throws IOException { + ByteBuf buffer = ctx.alloc().buffer(); + writer.writeValue((OutputStream) new ByteBufOutputStream(buffer), result); + DefaultFullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK, buffer); + response.headers().add(HttpHeaderNames.CONTENT_TYPE, HttpHeaderValues.APPLICATION_JSON); + return response; + } + + private FullHttpResponse computeResponseStatus(ChannelHandlerContext ctx, FullHttpRequest msg) throws IOException { + if (!msg.method().equals(HttpMethod.GET)) { + return new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.METHOD_NOT_ALLOWED); + } + + Status status = new Status( + ctx.channel().getClass().getName(), + SslContext.defaultServerProvider() + ); + + return serialize(ctx, writerStatus, status); + } + + private static SearchController.Result find(List haystack, String needle) { + for (int listIndex = 0; listIndex < haystack.size(); listIndex++) { + String s = haystack.get(listIndex); + int stringIndex = s.indexOf(needle); + if (stringIndex != -1) { + return new SearchController.Result(listIndex, stringIndex); + } + } + return null; + } + + record Status(String channelImplementation, + SslProvider sslProvider) { + } +} diff --git a/benchmarks/src/jmh/java/io/micronaut/http/server/stack/SearchController.java b/benchmarks/src/jmh/java/io/micronaut/http/server/stack/SearchController.java new file mode 100644 index 00000000000..ac23997ad09 --- /dev/null +++ b/benchmarks/src/jmh/java/io/micronaut/http/server/stack/SearchController.java @@ -0,0 +1,39 @@ +package io.micronaut.http.server.stack; + +import io.micronaut.context.annotation.Requires; +import io.micronaut.core.annotation.Introspected; +import io.micronaut.http.HttpResponse; +import io.micronaut.http.MutableHttpResponse; +import io.micronaut.http.annotation.Body; +import io.micronaut.http.annotation.Controller; +import io.micronaut.http.annotation.Post; + +import java.util.List; + +@Controller("/search") +@Requires(property = "spec.name", value = "FullHttpStackBenchmark") +public class SearchController { + @Post("find") + public HttpResponse find(@Body Input input) { + return find(input.haystack, input.needle); + } + + private static MutableHttpResponse find(List haystack, String needle) { + for (int listIndex = 0; listIndex < haystack.size(); listIndex++) { + String s = haystack.get(listIndex); + int stringIndex = s.indexOf(needle); + if (stringIndex != -1) { + return HttpResponse.ok(new Result(listIndex, stringIndex)); + } + } + return HttpResponse.notFound(); + } + + @Introspected + record Input(List haystack, String needle) { + } + + @Introspected + record Result(int listIndex, int stringIndex) { + } +} diff --git a/core/src/main/java/io/micronaut/core/execution/DelayedExecutionFlow.java b/core/src/main/java/io/micronaut/core/execution/DelayedExecutionFlow.java new file mode 100644 index 00000000000..44490372deb --- /dev/null +++ b/core/src/main/java/io/micronaut/core/execution/DelayedExecutionFlow.java @@ -0,0 +1,44 @@ +/* + * Copyright 2017-2023 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.core.execution; + +import io.micronaut.core.annotation.Nullable; + +/** + * {@link ExecutionFlow} that can be completed similar to a + * {@link java.util.concurrent.CompletableFuture}. + * + * @param The type of this flow + */ +public sealed interface DelayedExecutionFlow extends ExecutionFlow permits DelayedExecutionFlowImpl { + static DelayedExecutionFlow create() { + return new DelayedExecutionFlowImpl<>(); + } + + /** + * Complete this flow normally. + * + * @param result The result value + */ + void complete(@Nullable T result); + + /** + * Complete this flow with an exception. + * + * @param exc The exception + */ + void completeExceptionally(Throwable exc); +} diff --git a/core/src/main/java/io/micronaut/core/execution/DelayedExecutionFlowImpl.java b/core/src/main/java/io/micronaut/core/execution/DelayedExecutionFlowImpl.java new file mode 100644 index 00000000000..10ccc9fe7b1 --- /dev/null +++ b/core/src/main/java/io/micronaut/core/execution/DelayedExecutionFlowImpl.java @@ -0,0 +1,449 @@ +/* + * Copyright 2017-2023 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.core.execution; + +import io.micronaut.core.annotation.NonNull; +import io.micronaut.core.annotation.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.function.BiConsumer; +import java.util.function.Function; +import java.util.function.Supplier; + +@SuppressWarnings("rawtypes") +final class DelayedExecutionFlowImpl implements DelayedExecutionFlow { + private static final Logger LOG = LoggerFactory.getLogger(DelayedExecutionFlowImpl.class); + + /** + * Object used as a stand-in for a {@code null} completion to distinguish it from the + * uncompleted state. + */ + private static final Object NULL = new Object(); + + /** + * The head of the linked list of steps in this flow. + */ + private final Head head = new Head(); + /** + * The tail of the linked list of steps in this flow. + */ + private Step tail = head; + + /** + * Perform the given step with the given item. Continue on until there is either no more steps, + * either because onComplete was hit or because the consumer is not finished adding all the + * steps, or until a step does not finish immediately, e.g. flatMap returning a non-immediate + * flow. + * + * @param step The step to execute first + * @param item The input item for the step + */ + private static void work(Step step, Object item) { + while (true) { + item = step.apply(item); + if (item == null) { + // step suspended + break; + } + step = step.atomicSetOutput(item); + if (step == null) { + break; + } + } + } + + /** + * Complete this flow with the given result. + * + * @param result The result object. May be a {@link Failure}, {@link #NULL}, or any other + * successful value. + */ + private void complete0(@NonNull Object result) { + Step immediateStep = head.atomicSetOutput(result); + if (immediateStep != null) { + work(immediateStep, result); + } + } + + @Override + public void complete(T result) { + complete0(result == null ? NULL : result); + } + + @Override + public void completeExceptionally(Throwable exc) { + complete0(new Failure(exc)); + } + + /** + * Add a new step to this flow. + * + * @param next The new step + * @param The return type of the flow for generics support + * @return This flow + */ + @SuppressWarnings("unchecked") + private ExecutionFlow next(Step next) { + Step oldTail = tail; + tail = next; + Object output = oldTail.atomicSetNext(next); + if (output != null) { + work(next, output); + } + return (ExecutionFlow) this; + } + + @Override + public ExecutionFlow map(Function transformer) { + return next(new Map(transformer)); + } + + @SuppressWarnings("unchecked") + @Override + public ExecutionFlow flatMap(Function> transformer) { + return next(new FlatMap((Function) transformer)); + } + + @Override + public ExecutionFlow then(Supplier> supplier) { + return next(new Then<>(supplier)); + } + + @Override + public ExecutionFlow onErrorResume(Function> fallback) { + return next(new OnErrorResume(fallback)); + } + + @Override + public ExecutionFlow putInContext(String key, Object value) { + return this; + } + + @Override + public void onComplete(BiConsumer fn) { + next(new OnComplete<>(fn)); + } + + @SuppressWarnings("unchecked") + @Nullable + @Override + public ImperativeExecutionFlow tryComplete() { + Object tailOutput = tail.output; + if (tailOutput != null) { + if (tailOutput instanceof Failure failure) { + return (ImperativeExecutionFlow) new ImperativeExecutionFlowImpl(null, failure.t); + } else if (tailOutput == NULL) { + return (ImperativeExecutionFlow) new ImperativeExecutionFlowImpl(null, null); + } else { + return (ImperativeExecutionFlow) new ImperativeExecutionFlowImpl(tailOutput, null); + } + } else { + return null; + } + } + + /** + * Special wrapper for exception results. + * + * @param t The exception of the failure + */ + private record Failure(Throwable t) { + } + + private abstract static class Step { + /** + * The next step to take, or {@code null} if there is no next step yet. + */ + private volatile Step next; + /** + * The output of this step, or {@code null} if this step has not completed yet. + */ + private volatile Object output; + + /** + * Apply this step. Must call one of {@link #returnImmediate}, {@link #returnFlow}, + * {@link #returnError} or {@link #returnUnchanged}. + * + * @param input The input for the step + * @return The return value of the {@code return*} method called + */ + abstract Object apply(Object input); + + /** + * Atomically set the output of this step. If this returns non-null, the caller must call + * {@link #work(Step, Object)} with the returned step. + * + * @param output The output of this step + * @return The next step to execute using {@link #work(Step, Object)}, or {@code null} if + * the next step will be executed later + */ + @Nullable + final Step atomicSetOutput(Object output) { + if (this.output != null) { + // this is a best-effort check, the output field isn't always set + throw new IllegalStateException("Already completed"); + } + Step next = this.next; + if (next != null) { + return next; + } + this.output = output; + next = this.next; + if (next != null) { + // another thread completed at the same time! one or both threads hit this sync + // block. + synchronized (this) { + // deconfliction path + next = this.next; + if (next != null) { + // our sync block was executed first, unset output so the other thread aborts + this.output = null; + return next; + } + } + } + // no next step yet. + return null; + } + + /** + * Atomically set the next step. If this returns non-null, the caller must call + * {@link #work(Step, Object)} with the returned output value. + * + * @param next The next step to execute + * @return The output value of this step, to be passed to {@link #work(Step, Object)}, or + * {@code null} if the output is not yet known and the given step will be executed later + */ + @Nullable + final Object atomicSetNext(Step next) { + if (this.next != null) { + // this is a best-effort check, the next field isn't always set + throw new IllegalStateException("Already added a next step"); + } + Object output = this.output; + if (output != null) { + return output; + } + this.next = next; + output = this.output; + if (output != null) { + // another thread completed at the same time! one or both threads hit this sync + // block. + synchronized (this) { + // deconfliction path + output = this.output; + if (output != null) { + // our sync block was executed first, unset next so the other thread aborts + this.next = null; + return output; + } + } + } + // no output yet. + return null; + } + + /** + * Return a flow from this step (e.g. from flatMap). + * + * @param outputFlow The flow to return + * @return The value to return from {@link #work} + */ + final Object returnFlow(ExecutionFlow outputFlow) { + ImperativeExecutionFlow complete = outputFlow.tryComplete(); + if (complete != null) { + Throwable error = complete.getError(); + if (error == null) { + return returnImmediate(complete.getValue()); + } else { + return returnError(error); + } + } + + outputFlow.onComplete((v, t) -> { + Object result; + if (t == null) { + result = v == null ? NULL : v; + } else { + result = new Failure(t); + } + Step step = atomicSetOutput(result); + if (step != null) { + work(step, result); + } + }); + return null; + } + + /** + * Return an immediate successful value from this step (e.g. from map). + * + * @param o The value to return + * @return The value to return from {@link #work} + */ + final Object returnImmediate(@Nullable Object o) { + return o == null ? NULL : o; + } + + /** + * Signal that this step made no change to the input (e.g. a {@code map} when the flow has + * an error). + * + * @param input The input passed to {@link #apply} + * @return The value to return from {@link #work} + */ + final Object returnUnchanged(Object input) { + return input; + } + + /** + * Return an immediate failed value from this step (e.g. from map). + * + * @param e The exception to return + * @return The value to return from {@link #work} + */ + final Object returnError(Throwable e) { + return new Failure(e); + } + } + + /** + * Mock step used as the head of the linked list of steps. + */ + private static final class Head extends Step { + @Override + Object apply(Object input) { + throw new UnsupportedOperationException(); + } + } + + private static final class Map extends Step { + private final Function transformer; + + private Map(Function transformer) { + this.transformer = transformer; + } + + @SuppressWarnings("unchecked") + @Override + Object apply(Object input) { + try { + if (input instanceof Failure) { + return returnUnchanged(input); + } else if (input == NULL) { + return returnImmediate(transformer.apply(null)); + } else { + return returnImmediate(transformer.apply(input)); + } + } catch (Exception e) { + return returnError(e); + } + } + } + + private static final class FlatMap extends Step { + private final Function transformer; + + private FlatMap(Function transformer) { + this.transformer = transformer; + } + + @Override + Object apply(Object input) { + if (input instanceof Failure) { + return returnUnchanged(input); + } else { + try { + if (input == NULL) { + return returnFlow(transformer.apply(null)); + } else { + return returnFlow(transformer.apply(input)); + } + } catch (Exception e) { + return returnError(e); + } + } + } + } + + private static final class Then extends Step { + private final Supplier> transformer; + + private Then(Supplier> transformer) { + this.transformer = transformer; + } + + @Override + Object apply(Object input) { + if (input instanceof Failure) { + return returnUnchanged(input); + } else { + try { + return returnFlow(transformer.get()); + } catch (Exception e) { + return returnError(e); + } + } + } + } + + private static final class OnErrorResume extends Step { + private final Function> fallback; + + private OnErrorResume(Function> fallback) { + this.fallback = fallback; + } + + @Override + Object apply(Object input) { + if (input instanceof Failure failure) { + try { + return returnFlow(fallback.apply(failure.t)); + } catch (Exception e) { + return returnError(e); + } + } else { + return returnUnchanged(input); + } + } + } + + private static final class OnComplete extends Step { + private final BiConsumer consumer; + + public OnComplete(BiConsumer consumer) { + this.consumer = consumer; + } + + @SuppressWarnings("unchecked") + @Override + Object apply(Object input) { + try { + if (input instanceof Failure failure) { + consumer.accept(null, failure.t); + } else if (input == NULL) { + consumer.accept(null, null); + } else { + consumer.accept((E) input, null); + } + } catch (Exception e) { + LOG.error("Failed to execute onComplete", e); + } + return null; + } + } +} diff --git a/core/src/main/java/io/micronaut/core/execution/ExecutionFlow.java b/core/src/main/java/io/micronaut/core/execution/ExecutionFlow.java index 892bb361f91..a9dea9633b2 100644 --- a/core/src/main/java/io/micronaut/core/execution/ExecutionFlow.java +++ b/core/src/main/java/io/micronaut/core/execution/ExecutionFlow.java @@ -83,7 +83,7 @@ static ExecutionFlow empty() { */ @NonNull static ExecutionFlow async(@NonNull Executor executor, @NonNull Supplier> supplier) { - CompletableFuture completableFuture = new CompletableFuture<>(); + DelayedExecutionFlow completableFuture = DelayedExecutionFlow.create(); executor.execute(() -> supplier.get().onComplete((t, throwable) -> { if (throwable != null) { if (throwable instanceof CompletionException completionException) { @@ -94,7 +94,7 @@ static ExecutionFlow async(@NonNull Executor executor, @NonNull Supplier< completableFuture.complete(t); } })); - return CompletableFutureExecutionFlow.just(completableFuture); + return completableFuture; } /** @@ -176,9 +176,10 @@ default CompletableFuture toCompletableFuture() { if (throwable instanceof CompletionException completionException) { throwable = completionException.getCause(); } - CompletableFuture.failedFuture(throwable); + completableFuture.completeExceptionally(throwable); + } else { + completableFuture.complete(value); } - CompletableFuture.completedFuture(value); }); return completableFuture; } diff --git a/core/src/test/groovy/io/micronaut/core/execution/DelayedExecutionFlowSpec.groovy b/core/src/test/groovy/io/micronaut/core/execution/DelayedExecutionFlowSpec.groovy new file mode 100644 index 00000000000..56c9c9a4dbe --- /dev/null +++ b/core/src/test/groovy/io/micronaut/core/execution/DelayedExecutionFlowSpec.groovy @@ -0,0 +1,81 @@ +package io.micronaut.core.execution + +import org.apache.groovy.internal.util.Function +import spock.lang.Specification + +import java.util.concurrent.CompletableFuture + +class DelayedExecutionFlowSpec extends Specification { + def "single thread permutations"(List orderOfCompletion) { + given: + List> futures = [null, new CompletableFuture(), new CompletableFuture(), new CompletableFuture()] + List results = ["step0", "step2", "step3", "step5"] + ExecutionFlow inputFlow = new DelayedExecutionFlowImpl<>() + ExecutionFlow flowStep2 = CompletableFutureExecutionFlow.just(futures[1]) + ExecutionFlow flowStep3 = CompletableFutureExecutionFlow.just(futures[2]) + ExecutionFlow flowStep5 = CompletableFutureExecutionFlow.just(futures[3]) + String output = null + List, ExecutionFlow>> permTestSteps = [ + (ExecutionFlow prev) -> prev.map { + assert it == "step0" + return "step1" + }, + (ExecutionFlow prev) -> prev.flatMap { + assert it == "step1" + return flowStep2 + }, + (ExecutionFlow prev) -> prev.then { + return flowStep3 + }, + (ExecutionFlow prev) -> prev.map { + assert it == "step3" + throw new RuntimeException("step4") + }, + (ExecutionFlow prev) -> prev.map { + throw new AssertionError("should not be called") + }, + (ExecutionFlow prev) -> prev.flatMap { + throw new AssertionError("should not be called") + }, + (ExecutionFlow prev) -> prev.then { + throw new AssertionError("should not be called") + }, + (ExecutionFlow prev) -> prev.onErrorResume { + assert it.message == "step4" + return flowStep5 + }, + (ExecutionFlow prev) -> prev.onComplete((s, t) -> output = s), + ] + + ExecutionFlow flow = inputFlow + for (int i = 0; i < permTestSteps.size(); i++) { + for (int j = 0; j < orderOfCompletion.size(); j++) { + if (orderOfCompletion[j] == i) { + if (j == 0) { + inputFlow.complete(results[j]) + } else { + futures[j].complete(results[j]) + } + } + } + flow = permTestSteps[i](flow) + } + + where: + orderOfCompletion << powerSet([0, 1, 2, 3, 4, 5, 6, 7, 8], 4) + } + + private static List> powerSet(List base, int exp) { + if (exp == 0) { + return [[]] + } + List> output = [] + List> next = powerSet(base, exp - 1) + for (T t : base) { + for (List head : next) { + output.add(head + t) + } + } + return output + } +} diff --git a/http-netty/build.gradle b/http-netty/build.gradle index d4edb612b48..3732901ad0d 100644 --- a/http-netty/build.gradle +++ b/http-netty/build.gradle @@ -21,6 +21,7 @@ dependencies { testImplementation project(":runtime") testImplementation project(":websocket") + testImplementation project(":jackson-databind") } spotless { diff --git a/http-netty/src/main/java/io/micronaut/http/netty/NettyHttpHeaders.java b/http-netty/src/main/java/io/micronaut/http/netty/NettyHttpHeaders.java index 0a46c1ebb34..e777fb3eb37 100644 --- a/http-netty/src/main/java/io/micronaut/http/netty/NettyHttpHeaders.java +++ b/http-netty/src/main/java/io/micronaut/http/netty/NettyHttpHeaders.java @@ -20,6 +20,7 @@ import io.micronaut.core.convert.ConversionService; import io.micronaut.core.type.MutableHeaders; import io.micronaut.http.HttpHeaderValues; +import io.micronaut.http.HttpHeaders; import io.micronaut.http.MediaType; import io.micronaut.http.MutableHttpHeaders; import io.netty.handler.codec.http.DefaultHttpHeaders; @@ -242,4 +243,17 @@ public ConversionService getConversionService() { public void setConversionService(ConversionService conversionService) { this.conversionService = conversionService; } + + @Override + public Optional contentType() { + // optimization to avoid ConversionService + String str = get(HttpHeaders.CONTENT_TYPE); + if (str != null) { + try { + return Optional.of(MediaType.of(str)); + } catch (IllegalArgumentException ignored) { + } + } + return Optional.empty(); + } } diff --git a/http-netty/src/main/java/io/micronaut/http/netty/reactive/HandlerPublisher.java b/http-netty/src/main/java/io/micronaut/http/netty/reactive/HandlerPublisher.java index 9fe95e86902..1c826cfb1c6 100644 --- a/http-netty/src/main/java/io/micronaut/http/netty/reactive/HandlerPublisher.java +++ b/http-netty/src/main/java/io/micronaut/http/netty/reactive/HandlerPublisher.java @@ -21,7 +21,6 @@ import io.netty.handler.codec.http.HttpContent; import io.netty.util.ReferenceCountUtil; import io.netty.util.concurrent.EventExecutor; -import io.netty.util.internal.TypeParameterMatcher; import org.reactivestreams.Subscriber; import org.reactivestreams.Subscription; import org.slf4j.Logger; @@ -32,7 +31,15 @@ import java.util.Queue; import java.util.concurrent.atomic.AtomicBoolean; -import static io.micronaut.http.netty.reactive.HandlerPublisher.State.*; +import static io.micronaut.http.netty.reactive.HandlerPublisher.State.BUFFERING; +import static io.micronaut.http.netty.reactive.HandlerPublisher.State.DEMANDING; +import static io.micronaut.http.netty.reactive.HandlerPublisher.State.DONE; +import static io.micronaut.http.netty.reactive.HandlerPublisher.State.DRAINING; +import static io.micronaut.http.netty.reactive.HandlerPublisher.State.IDLE; +import static io.micronaut.http.netty.reactive.HandlerPublisher.State.NO_CONTEXT; +import static io.micronaut.http.netty.reactive.HandlerPublisher.State.NO_SUBSCRIBER; +import static io.micronaut.http.netty.reactive.HandlerPublisher.State.NO_SUBSCRIBER_ERROR; +import static io.micronaut.http.netty.reactive.HandlerPublisher.State.NO_SUBSCRIBER_OR_CONTEXT; /** * Publisher for a Netty Handler. @@ -62,7 +69,7 @@ * @since 1.0 */ @Internal -public class HandlerPublisher extends ChannelDuplexHandler implements HotObservable { +public abstract class HandlerPublisher extends ChannelDuplexHandler implements HotObservable { private static final Logger LOG = LoggerFactory.getLogger(HandlerPublisher.class); /** * Used for buffering a completion signal. @@ -76,7 +83,6 @@ public String toString() { private final AtomicBoolean completed = new AtomicBoolean(false); private final EventExecutor executor; - private final TypeParameterMatcher matcher; private final Queue buffer = new LinkedList<>(); @@ -100,11 +106,9 @@ public String toString() { * with, if not, an exception will be thrown when the handler is registered. * * @param executor The executor to execute asynchronous events from the subscriber on. - * @param subscriberMessageType The type of message this publisher accepts. */ - public HandlerPublisher(EventExecutor executor, Class subscriberMessageType) { + public HandlerPublisher(EventExecutor executor) { this.executor = executor; - this.matcher = TypeParameterMatcher.get(subscriberMessageType); } @Override @@ -139,9 +143,7 @@ public void cancel() { * @param msg The message to check. * @return True if the message should be accepted. */ - protected boolean acceptInboundMessage(Object msg) { - return matcher.match(msg); - } + protected abstract boolean acceptInboundMessage(Object msg); /** * Override to handle when a subscriber cancels the subscription. diff --git a/http-netty/src/main/java/io/micronaut/http/netty/stream/HttpStreamsHandler.java b/http-netty/src/main/java/io/micronaut/http/netty/stream/HttpStreamsHandler.java index 2a6a659ec82..080d4c6638b 100644 --- a/http-netty/src/main/java/io/micronaut/http/netty/stream/HttpStreamsHandler.java +++ b/http-netty/src/main/java/io/micronaut/http/netty/stream/HttpStreamsHandler.java @@ -230,7 +230,12 @@ public void channelRead(final ChannelHandlerContext ctx, Object msg) throws Exce currentlyStreamedMessage = inMsg; // It has a body, stream it - HandlerPublisher publisher = new HandlerPublisher(ctx.executor(), HttpContent.class) { + HandlerPublisher publisher = new HandlerPublisher(ctx.executor()) { + @Override + protected boolean acceptInboundMessage(Object msg) { + return msg instanceof HttpContent; + } + @Override protected void cancelled() { if (ctx.executor().inEventLoop()) { diff --git a/http-netty/src/main/java/io/micronaut/http/netty/stream/HttpStreamsServerHandler.java b/http-netty/src/main/java/io/micronaut/http/netty/stream/HttpStreamsServerHandler.java index a2d4cf67584..d0baea2d300 100644 --- a/http-netty/src/main/java/io/micronaut/http/netty/stream/HttpStreamsServerHandler.java +++ b/http-netty/src/main/java/io/micronaut/http/netty/stream/HttpStreamsServerHandler.java @@ -246,7 +246,12 @@ private void handleWebSocketResponse(ChannelHandlerContext ctx, HttpResponse mes } else { // First, insert new handlers in the chain after us for handling the websocket ChannelPipeline pipeline = ctx.pipeline(); - HandlerPublisher publisher = new HandlerPublisher<>(ctx.executor(), WebSocketFrame.class); + HandlerPublisher publisher = new HandlerPublisher<>(ctx.executor()) { + @Override + protected boolean acceptInboundMessage(Object msg) { + return msg instanceof WebSocketFrame; + } + }; HandlerSubscriber subscriber = new HandlerSubscriber<>(ctx.executor()); pipeline.addAfter(ctx.executor(), ctx.name(), "websocket-subscriber", subscriber); pipeline.addAfter(ctx.executor(), ctx.name(), "websocket-publisher", publisher); diff --git a/http-netty/src/test/groovy/io/micronaut/http/netty/reactive/HandlerPublisherSpec.groovy b/http-netty/src/test/groovy/io/micronaut/http/netty/reactive/HandlerPublisherSpec.groovy index 5b2fb717a4f..34e046b6592 100644 --- a/http-netty/src/test/groovy/io/micronaut/http/netty/reactive/HandlerPublisherSpec.groovy +++ b/http-netty/src/test/groovy/io/micronaut/http/netty/reactive/HandlerPublisherSpec.groovy @@ -27,7 +27,12 @@ class HandlerPublisherSpec extends Specification { */ def embeddedChannel = new EmbeddedChannel() - def handlerPublisher = new HandlerPublisher(embeddedChannel.eventLoop(), Object) + def handlerPublisher = new HandlerPublisher(embeddedChannel.eventLoop()) { + @Override + protected boolean acceptInboundMessage(Object msg) { + return true + } + } boolean killOnNextRead = false embeddedChannel.pipeline().addLast(new ChannelDuplexHandler() { @Override diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/HttpPipelineBuilder.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/HttpPipelineBuilder.java index e8c66825ecc..1a4b638562b 100644 --- a/http-server-netty/src/main/java/io/micronaut/http/server/netty/HttpPipelineBuilder.java +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/HttpPipelineBuilder.java @@ -550,7 +550,10 @@ private void insertMicronautHandlers(boolean zeroCopySupported) { pipeline.addLast(ChannelPipelineCustomizer.HANDLER_HTTP_COMPRESSOR, contentCompressor); pipeline.addLast(ChannelPipelineCustomizer.HANDLER_HTTP_DECOMPRESSOR, new HttpContentDecompressor()); - pipeline.addLast(NettyServerWebSocketUpgradeHandler.COMPRESSION_HANDLER, new WebSocketServerCompressionHandler()); + Optional>> webSocketUpgradeHandler = embeddedServices.getWebSocketUpgradeHandler(server); + if (webSocketUpgradeHandler.isPresent()) { + pipeline.addLast(NettyServerWebSocketUpgradeHandler.COMPRESSION_HANDLER, new WebSocketServerCompressionHandler()); + } pipeline.addLast(ChannelPipelineCustomizer.HANDLER_HTTP_STREAM, new HttpStreamsServerHandler()); pipeline.addLast(ChannelPipelineCustomizer.HANDLER_HTTP_CHUNK, new ChunkedWriteHandler()); pipeline.addLast(HttpRequestDecoder.ID, requestDecoder); @@ -561,9 +564,7 @@ private void insertMicronautHandlers(boolean zeroCopySupported) { pipeline.addLast("request-certificate-handler", new HttpRequestCertificateHandler(sslHandler)); } pipeline.addLast(HttpResponseEncoder.ID, responseEncoder); - embeddedServices.getWebSocketUpgradeHandler(server).ifPresent(websocketHandler -> - pipeline.addLast(ChannelPipelineCustomizer.HANDLER_WEBSOCKET_UPGRADE, websocketHandler) - ); + webSocketUpgradeHandler.ifPresent(h -> pipeline.addLast(ChannelPipelineCustomizer.HANDLER_WEBSOCKET_UPGRADE, h)); pipeline.addLast(ChannelPipelineCustomizer.HANDLER_MICRONAUT_INBOUND, routingInBoundHandler); } diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/NettyHttpRequest.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/NettyHttpRequest.java index 119a831120b..96bca32ad2b 100644 --- a/http-server-netty/src/main/java/io/micronaut/http/server/netty/NettyHttpRequest.java +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/NettyHttpRequest.java @@ -287,7 +287,7 @@ public MutableConvertibleValues getAttributes() { synchronized (this) { // double check attributes = this.attributes; if (attributes == null) { - attributes = new MutableConvertibleValuesMap<>(new HashMap<>(4)); + attributes = new MutableConvertibleValuesMap<>(new HashMap<>(8)); this.attributes = attributes; } } diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/NettyRequestLifecycle.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/NettyRequestLifecycle.java index 46984fd23ac..3d56eda33cf 100644 --- a/http-server-netty/src/main/java/io/micronaut/http/server/netty/NettyRequestLifecycle.java +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/NettyRequestLifecycle.java @@ -17,7 +17,7 @@ import io.micronaut.core.annotation.Internal; import io.micronaut.core.annotation.Nullable; -import io.micronaut.core.execution.CompletableFutureExecutionFlow; +import io.micronaut.core.execution.DelayedExecutionFlow; import io.micronaut.core.execution.ExecutionFlow; import io.micronaut.core.type.Argument; import io.micronaut.http.HttpMethod; @@ -54,7 +54,6 @@ import java.util.Collection; import java.util.List; import java.util.Optional; -import java.util.concurrent.CompletableFuture; @Internal final class NettyRequestLifecycle extends RequestLifecycle { @@ -151,7 +150,7 @@ private ExecutionFlow> waitForBody(RouteMatch routeMatch) { HttpContentProcessor processor = rib.httpContentProcessorResolver.resolve(nettyRequest, routeMatch); StreamingDataSubscriber pr = new StreamingDataSubscriber(completer, processor); ((StreamedHttpRequest) nettyRequest.getNativeRequest()).subscribe(pr); - return CompletableFutureExecutionFlow.just(pr.completion); + return pr.completion; } void handleException(Throwable cause) { @@ -187,7 +186,8 @@ private boolean shouldReadBody(RouteMatch routeMatch) { } private static class StreamingDataSubscriber implements Subscriber { - final CompletableFuture> completion = new CompletableFuture<>(); + final DelayedExecutionFlow> completion = DelayedExecutionFlow.create(); + private boolean completed = false; private final List bufferList = new ArrayList<>(1); private final HttpContentProcessor contentProcessor; @@ -272,7 +272,10 @@ private void handleError(Throwable t) { // this may drop the exception if the route has already been executed. However, that is // only the case if there are publisher parameters, and those will still receive the // failure. Hopefully. - completion.completeExceptionally(t); + if (!completed) { + completion.completeExceptionally(t); + completed = true; + } downstreamDone = true; } @@ -298,6 +301,7 @@ public void onComplete() { private void executeRoute() { completion.complete(completer.routeMatch); + completed = true; } } } diff --git a/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/cors/CorsFilterSpec.groovy b/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/cors/CorsFilterSpec.groovy index ea793220e50..65ca22d9cee 100644 --- a/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/cors/CorsFilterSpec.groovy +++ b/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/cors/CorsFilterSpec.groovy @@ -17,12 +17,16 @@ package io.micronaut.http.server.netty.cors import io.micronaut.context.ApplicationContext import io.micronaut.core.annotation.Nullable -import io.micronaut.core.async.publisher.Publishers import io.micronaut.core.util.StringUtils -import io.micronaut.http.* +import io.micronaut.http.HttpAttributes +import io.micronaut.http.HttpHeaders +import io.micronaut.http.HttpMethod +import io.micronaut.http.HttpRequest +import io.micronaut.http.HttpResponse +import io.micronaut.http.HttpStatus +import io.micronaut.http.MutableHttpResponse import io.micronaut.http.annotation.Controller import io.micronaut.http.annotation.Get -import io.micronaut.http.filter.ServerFilterChain import io.micronaut.http.server.HttpServerConfiguration import io.micronaut.http.server.cors.CorsFilter import io.micronaut.http.server.cors.CorsOriginConfiguration @@ -32,8 +36,6 @@ import io.micronaut.web.router.RouteMatch import io.micronaut.web.router.Router import io.micronaut.web.router.UriRouteMatch import org.apache.http.client.utils.URIBuilder -import org.reactivestreams.Publisher -import reactor.core.publisher.Mono import spock.lang.AutoCleanup import spock.lang.Shared import spock.lang.Specification @@ -41,7 +43,15 @@ import spock.lang.Unroll import java.util.stream.Collectors -import static io.micronaut.http.HttpHeaders.* +import static io.micronaut.http.HttpHeaders.ACCESS_CONTROL_ALLOW_CREDENTIALS +import static io.micronaut.http.HttpHeaders.ACCESS_CONTROL_ALLOW_HEADERS +import static io.micronaut.http.HttpHeaders.ACCESS_CONTROL_ALLOW_METHODS +import static io.micronaut.http.HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN +import static io.micronaut.http.HttpHeaders.ACCESS_CONTROL_EXPOSE_HEADERS +import static io.micronaut.http.HttpHeaders.ACCESS_CONTROL_MAX_AGE +import static io.micronaut.http.HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD +import static io.micronaut.http.HttpHeaders.ACCESS_CONTROL_REQUEST_HEADERS +import static io.micronaut.http.HttpHeaders.VARY class CorsFilterSpec extends Specification { @@ -56,7 +66,7 @@ class CorsFilterSpec extends Specification { HttpRequest request = createRequest(null as String) when: - Optional> result = Mono.from(corsHandler.doFilter(request, okChain())).blockOptional() + Optional> result = filterOk(corsHandler, request) then: "the request is passed through" result.isPresent() @@ -79,7 +89,7 @@ class CorsFilterSpec extends Specification { CorsFilter corsHandler = buildCorsHandler(config) when: - Optional> result = Mono.from(corsHandler.doFilter(request, okChain())).blockOptional() + Optional> result = filterOk(corsHandler, request) then: result.isPresent() @@ -104,7 +114,7 @@ class CorsFilterSpec extends Specification { CorsFilter corsHandler = buildCorsHandler(config) when: - Optional> result = Mono.from(corsHandler.doFilter(request, okChain())).blockOptional() + Optional> result = filterOk(corsHandler, request) then: result.isPresent() @@ -146,7 +156,7 @@ class CorsFilterSpec extends Specification { CorsFilter corsHandler = buildCorsHandler(config) when: - Optional> result = Mono.from(corsHandler.doFilter(request, okChain())).blockOptional() + Optional> result = filterOk(corsHandler, request) then: result.isPresent() @@ -172,7 +182,7 @@ class CorsFilterSpec extends Specification { CorsFilter corsHandler = buildCorsHandler(config) when: - Optional> result = Mono.from(corsHandler.doFilter(request, okChain())).blockOptional() + Optional> result = filterOk(corsHandler, request) then: result.isPresent() @@ -219,7 +229,7 @@ class CorsFilterSpec extends Specification { CorsFilter corsHandler = buildCorsHandler(config) when: - Optional> result = Mono.from(corsHandler.doFilter(request, okChain())).blockOptional() + Optional> result = filterOk(corsHandler, request) then: result.isPresent() @@ -260,7 +270,7 @@ class CorsFilterSpec extends Specification { when: - Optional> result = Mono.from(corsHandler.doFilter(request, okChain())).blockOptional() + Optional> result = filterOk(corsHandler, request) then: result.isPresent() @@ -301,7 +311,7 @@ class CorsFilterSpec extends Specification { } when: - Optional> result = Mono.from(corsHandler.doFilter(request, okChain())).blockOptional() + Optional> result = filterOk(corsHandler, request) then: notThrown(NullPointerException) @@ -339,7 +349,7 @@ class CorsFilterSpec extends Specification { CorsFilter corsHandler = buildCorsHandler(config) when: - Optional> result = Mono.from(corsHandler.doFilter(request, okChain())).blockOptional() + Optional> result = filterOk(corsHandler, request) then: result.isPresent() @@ -383,7 +393,7 @@ class CorsFilterSpec extends Specification { CorsFilter corsHandler = buildCorsHandler(config) when: - Optional> result = Mono.from(corsHandler.doFilter(request, okChain())).blockOptional() + Optional> result = filterOk(corsHandler, request) then: result.isPresent() @@ -431,7 +441,7 @@ class CorsFilterSpec extends Specification { request.getAttribute(HttpAttributes.AVAILABLE_HTTP_METHODS, _) >> Optional.of(routes.stream().map(route->route.getHttpMethod()).collect(Collectors.toList())) when: - Optional> result = Mono.from(corsHandler.doFilter(request, okChain())).blockOptional() + Optional> result = filterOk(corsHandler, request) then: result.isPresent() @@ -481,7 +491,7 @@ class CorsFilterSpec extends Specification { CorsFilter corsHandler = buildCorsHandler(config) when: - Optional> result = Mono.from(corsHandler.doFilter(request, okChain())).blockOptional() + Optional> result = filterOk(corsHandler, request) then: result.isPresent() @@ -522,7 +532,7 @@ class CorsFilterSpec extends Specification { request.getAttribute(HttpAttributes.AVAILABLE_HTTP_METHODS, _) >> Optional.of(routes.stream().map(route->route.getHttpMethod()).collect(Collectors.toList())) when: - Optional> result = Mono.from(corsHandler.doFilter(request, okChain())).blockOptional() + Optional> result = filterOk(corsHandler, request) then: result.isPresent() @@ -554,13 +564,14 @@ class CorsFilterSpec extends Specification { } } - private ServerFilterChain okChain() { - new ServerFilterChain() { - @Override - Publisher> proceed(HttpRequest req) { - Publishers.just(HttpResponse.ok()) - } + private Optional> filterOk(CorsFilter filter, HttpRequest req) { + def earlyResponse = filter.filterRequest(req) + if (earlyResponse != null) { + return Optional.of(earlyResponse) } + MutableHttpResponse response = HttpResponse.ok() + filter.filterResponse(req, response) + return Optional.of(response) } private HttpServerConfiguration.CorsConfiguration enabledCorsConfiguration(Map corsConfigurationMap = null) { diff --git a/http-server/src/main/java/io/micronaut/http/server/cors/CorsFilter.java b/http-server/src/main/java/io/micronaut/http/server/cors/CorsFilter.java index f8f16200434..48408994e68 100644 --- a/http-server/src/main/java/io/micronaut/http/server/cors/CorsFilter.java +++ b/http-server/src/main/java/io/micronaut/http/server/cors/CorsFilter.java @@ -15,13 +15,14 @@ */ package io.micronaut.http.server.cors; +import io.micronaut.core.annotation.Internal; import io.micronaut.core.annotation.NonNull; import io.micronaut.core.annotation.Nullable; -import io.micronaut.core.async.publisher.Publishers; import io.micronaut.core.convert.ArgumentConversionContext; import io.micronaut.core.convert.ConversionContext; import io.micronaut.core.convert.ImmutableArgumentConversionContext; import io.micronaut.core.io.socket.SocketUtils; +import io.micronaut.core.order.Ordered; import io.micronaut.core.util.StringUtils; import io.micronaut.http.HttpHeaders; import io.micronaut.http.HttpMethod; @@ -29,14 +30,13 @@ import io.micronaut.http.HttpResponse; import io.micronaut.http.HttpStatus; import io.micronaut.http.MutableHttpResponse; -import io.micronaut.http.annotation.Filter; -import io.micronaut.http.filter.HttpServerFilter; -import io.micronaut.http.filter.ServerFilterChain; +import io.micronaut.http.annotation.RequestFilter; +import io.micronaut.http.annotation.ResponseFilter; +import io.micronaut.http.annotation.ServerFilter; import io.micronaut.http.filter.ServerFilterPhase; import io.micronaut.http.server.HttpServerConfiguration; import io.micronaut.http.server.util.HttpHostResolver; import org.jetbrains.annotations.NotNull; -import org.reactivestreams.Publisher; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -49,7 +49,16 @@ import java.util.stream.Collectors; import static io.micronaut.http.HttpAttributes.AVAILABLE_HTTP_METHODS; -import static io.micronaut.http.HttpHeaders.*; +import static io.micronaut.http.HttpHeaders.ACCESS_CONTROL_ALLOW_CREDENTIALS; +import static io.micronaut.http.HttpHeaders.ACCESS_CONTROL_ALLOW_HEADERS; +import static io.micronaut.http.HttpHeaders.ACCESS_CONTROL_ALLOW_METHODS; +import static io.micronaut.http.HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN; +import static io.micronaut.http.HttpHeaders.ACCESS_CONTROL_EXPOSE_HEADERS; +import static io.micronaut.http.HttpHeaders.ACCESS_CONTROL_MAX_AGE; +import static io.micronaut.http.HttpHeaders.ACCESS_CONTROL_REQUEST_HEADERS; +import static io.micronaut.http.HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD; +import static io.micronaut.http.HttpHeaders.ORIGIN; +import static io.micronaut.http.HttpHeaders.VARY; import static io.micronaut.http.annotation.Filter.MATCH_ALL_PATTERN; /** @@ -59,8 +68,8 @@ * @author Graeme Rocher * @since 1.0 */ -@Filter(MATCH_ALL_PATTERN) -public class CorsFilter implements HttpServerFilter { +@ServerFilter(MATCH_ALL_PATTERN) +public class CorsFilter implements Ordered { private static final Logger LOG = LoggerFactory.getLogger(CorsFilter.class); private static final ArgumentConversionContext CONVERSION_CONTEXT_HTTP_METHOD = ImmutableArgumentConversionContext.of(HttpMethod.class); @@ -79,17 +88,19 @@ public CorsFilter(HttpServerConfiguration.CorsConfiguration corsConfiguration, this.httpHostResolver = httpHostResolver; } - @Override - public Publisher> doFilter(HttpRequest request, ServerFilterChain chain) { + @RequestFilter + @Nullable + @Internal + public final HttpResponse filterRequest(HttpRequest request) { String origin = request.getHeaders().getOrigin().orElse(null); if (origin == null) { LOG.trace("Http Header " + HttpHeaders.ORIGIN + " not present. Proceeding with the request."); - return chain.proceed(request); + return null; // proceed } CorsOriginConfiguration corsOriginConfiguration = getConfiguration(request).orElse(null); if (corsOriginConfiguration != null) { if (CorsUtil.isPreflightRequest(request)) { - return handlePreflightRequest(request, chain, corsOriginConfiguration); + return handlePreflightRequest(request, corsOriginConfiguration); } if (!validateMethodToMatch(request, corsOriginConfiguration).isPresent()) { return forbidden(); @@ -98,13 +109,25 @@ public Publisher> doFilter(HttpRequest request, Server LOG.trace("The resolved configuration allows any origin. To prevent drive-by-localhost attacks the request is forbidden"); return forbidden(); } - return Publishers.then(chain.proceed(request), resp -> decorateResponseWithHeaders(request, resp, corsOriginConfiguration)); + return null; // proceed } else if (shouldDenyToPreventDriveByLocalhostAttack(origin, request)) { LOG.trace("the request specifies an origin different than localhost. To prevent drive-by-localhost attacks the request is forbidden"); return forbidden(); } LOG.trace("CORS configuration not found for {} origin", origin); - return chain.proceed(request); + return null; // proceed + } + + @ResponseFilter + @Internal + public final void filterResponse(HttpRequest request, MutableHttpResponse response) { + CorsOriginConfiguration corsOriginConfiguration = getConfiguration(request).orElse(null); + if (corsOriginConfiguration != null) { + if (CorsUtil.isPreflightRequest(request)) { + decorateResponseWithHeadersForPreflightRequest(request, response, corsOriginConfiguration); + } + decorateResponseWithHeaders(request, response, corsOriginConfiguration); + } } /** @@ -347,8 +370,8 @@ private boolean hasAllowedHeaders(@NonNull HttpRequest request, @NonNull Cors } @NotNull - private static Publisher> forbidden() { - return Publishers.just(HttpResponse.status(HttpStatus.FORBIDDEN)); + private static MutableHttpResponse forbidden() { + return HttpResponse.status(HttpStatus.FORBIDDEN); } @NonNull @@ -375,24 +398,20 @@ private void decorateResponseWithHeaders(@NonNull HttpRequest request, } @NonNull - private Publisher> handlePreflightRequest(@NonNull HttpRequest request, - @NonNull ServerFilterChain chain, + private MutableHttpResponse handlePreflightRequest(@NonNull HttpRequest request, @NonNull CorsOriginConfiguration corsOriginConfiguration) { Optional statusOptional = validatePreflightRequest(request, corsOriginConfiguration); if (statusOptional.isPresent()) { HttpStatus status = statusOptional.get(); if (status.getCode() >= 400) { - return Publishers.just(HttpResponse.status(status)); + return HttpResponse.status(status); } MutableHttpResponse resp = HttpResponse.status(status); decorateResponseWithHeadersForPreflightRequest(request, resp, corsOriginConfiguration); decorateResponseWithHeaders(request, resp, corsOriginConfiguration); - return Publishers.just(resp); + return resp; } - return Publishers.then(chain.proceed(request), resp -> { - decorateResponseWithHeadersForPreflightRequest(request, resp, corsOriginConfiguration); - decorateResponseWithHeaders(request, resp, corsOriginConfiguration); - }); + return null; } @NonNull diff --git a/http/src/main/java/io/micronaut/http/reactive/execution/ReactorExecutionFlowImpl.java b/http/src/main/java/io/micronaut/http/reactive/execution/ReactorExecutionFlowImpl.java index 74fb7d33722..7ece13d7075 100644 --- a/http/src/main/java/io/micronaut/http/reactive/execution/ReactorExecutionFlowImpl.java +++ b/http/src/main/java/io/micronaut/http/reactive/execution/ReactorExecutionFlowImpl.java @@ -17,7 +17,6 @@ import io.micronaut.core.annotation.Internal; import io.micronaut.core.annotation.Nullable; -import io.micronaut.core.execution.CompletableFutureExecutionFlow; import io.micronaut.core.execution.ExecutionFlow; import io.micronaut.core.execution.ImperativeExecutionFlow; import org.reactivestreams.Publisher; @@ -129,8 +128,6 @@ public ImperativeExecutionFlow tryComplete() { static Mono toMono(ExecutionFlow next) { if (next instanceof ReactorExecutionFlowImpl reactiveFlowImpl) { return reactiveFlowImpl.value; - } else if (next instanceof CompletableFutureExecutionFlow completableFutureFlow) { - return Mono.fromCompletionStage(completableFutureFlow.toCompletableFuture()); } else if (next instanceof ImperativeExecutionFlow imperativeFlow) { Mono m; if (imperativeFlow.getError() != null) { @@ -150,8 +147,9 @@ static Mono toMono(ExecutionFlow next) { }); } return m; + } else { + return Mono.fromCompletionStage(next.toCompletableFuture()); } - throw new IllegalStateException(); } static Mono toMono(Supplier> next) { From 05926b377c74434941ec824e6e7598cd6b9187ef Mon Sep 17 00:00:00 2001 From: Denis Stepanov Date: Tue, 21 Mar 2023 19:33:34 -0600 Subject: [PATCH 615/743] Fix validation tests (#8983) --- ...naut.build.internal.convention-base.gradle | 5 ++ .../http/client/aop/RequestBeanSpec.groovy | 4 +- .../docs/annotation/PetControllerTest.java | 21 ++++---- .../beans/BeanIntrospectionSpec.groovy | 10 ++-- .../inject/beans/BeanDefinitionSpec.groovy | 12 ++--- .../inject/ast/ClassElementSpec.groovy | 51 +++++++++---------- 6 files changed, 49 insertions(+), 54 deletions(-) diff --git a/buildSrc/src/main/groovy/io.micronaut.build.internal.convention-base.gradle b/buildSrc/src/main/groovy/io.micronaut.build.internal.convention-base.gradle index 6a3e6092838..2fdc01feb56 100644 --- a/buildSrc/src/main/groovy/io.micronaut.build.internal.convention-base.gradle +++ b/buildSrc/src/main/groovy/io.micronaut.build.internal.convention-base.gradle @@ -15,6 +15,11 @@ repositories { maven { url "https://s01.oss.sonatype.org/content/repositories/snapshots/" } } +configurations.all { + resolutionStrategy.cacheChangingModulesFor 0, 'seconds' + resolutionStrategy.cacheDynamicVersionsFor 0, 'seconds' +} + configurations { testRuntimeClasspath { // Use AOP from here, not from maven diff --git a/http-client/src/test/groovy/io/micronaut/http/client/aop/RequestBeanSpec.groovy b/http-client/src/test/groovy/io/micronaut/http/client/aop/RequestBeanSpec.groovy index 2d14297d14a..ed5518a73d0 100644 --- a/http-client/src/test/groovy/io/micronaut/http/client/aop/RequestBeanSpec.groovy +++ b/http-client/src/test/groovy/io/micronaut/http/client/aop/RequestBeanSpec.groovy @@ -68,7 +68,7 @@ class RequestBeanSpec extends Specification { then: def ex = thrown(HttpClientResponseException) - ex.response.getBody(Map).get()._embedded.errors[0].message.contains("Field must have value first or second.") + ex.response.getBody(Map).get()._embedded.errors[0].message == "bean.validatedValue: Field must have value 'first' or 'second'." } void "test validated value returns ok when valid"() { @@ -112,7 +112,7 @@ class RequestBeanSpec extends Specification { then: def ex = thrown(HttpClientResponseException) - ex.response.getBody(Map).get()._embedded.errors[0].message.contains("Field must have value first or second.") + ex.response.getBody(Map).get()._embedded.errors[0].message == "bean.validatedValue: Field must have value 'first' or 'second'." } void "test Immutable Bean gets injected Typed Value"() { diff --git a/http-client/src/test/groovy/io/micronaut/http/client/docs/annotation/PetControllerTest.java b/http-client/src/test/groovy/io/micronaut/http/client/docs/annotation/PetControllerTest.java index 94cd5f461d7..06436e04d99 100644 --- a/http-client/src/test/groovy/io/micronaut/http/client/docs/annotation/PetControllerTest.java +++ b/http-client/src/test/groovy/io/micronaut/http/client/docs/annotation/PetControllerTest.java @@ -17,13 +17,11 @@ import io.micronaut.context.ApplicationContext; import io.micronaut.runtime.server.EmbeddedServer; -import org.junit.Rule; +import jakarta.validation.ConstraintViolationException; +import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; -import org.junit.rules.ExpectedException; import reactor.core.publisher.Mono; -import jakarta.validation.ConstraintViolationException; - import static org.junit.jupiter.api.Assertions.assertEquals; /** @@ -32,9 +30,6 @@ */ public class PetControllerTest { - @Rule - public ExpectedException thrown = ExpectedException.none(); - @Test public void testPostPet() { EmbeddedServer embeddedServer = ApplicationContext.run(EmbeddedServer.class); @@ -53,10 +48,16 @@ public void testPostPetValidation() { EmbeddedServer embeddedServer = ApplicationContext.run(EmbeddedServer.class); PetClient client = embeddedServer.getApplicationContext().getBean(PetClient.class); - thrown.expect(ConstraintViolationException.class); - thrown.expectMessage("save.age: must be greater than or equal to 1"); - Mono.from(client.save("Fred", -1)).block(); + try { + Mono.from(client.save("Fred", -1)).block(); + } catch (ConstraintViolationException e) { + Assertions.assertEquals("save.age: must be greater than or equal to 1", e.getMessage()); + embeddedServer.stop(); + return; + } embeddedServer.stop(); + + Assertions.fail(); } } diff --git a/inject-java-test/src/test/groovy/io/micronaut/inject/visitor/beans/BeanIntrospectionSpec.groovy b/inject-java-test/src/test/groovy/io/micronaut/inject/visitor/beans/BeanIntrospectionSpec.groovy index e3ec7147ba8..621af43bc99 100644 --- a/inject-java-test/src/test/groovy/io/micronaut/inject/visitor/beans/BeanIntrospectionSpec.groovy +++ b/inject-java-test/src/test/groovy/io/micronaut/inject/visitor/beans/BeanIntrospectionSpec.groovy @@ -2033,13 +2033,9 @@ public class Test { param2.getTypeParameters().length == 1 def param3 = param2.getTypeParameters()[0] - property.getAnnotationMetadata().getAnnotationNames().size() == 0 - param1.getAnnotationMetadata().getAnnotationNames().size() == 1 - param1.getAnnotationMetadata().getAnnotationNames().asList() == ['jakarta.validation.constraints.Size$List'] - param2.getAnnotationMetadata().getAnnotationNames().size() == 1 - param2.getAnnotationMetadata().getAnnotationNames().asList() == ['jakarta.validation.constraints.NotEmpty$List'] - param3.getAnnotationMetadata().getAnnotationNames().size() == 1 - param3.getAnnotationMetadata().getAnnotationNames().asList() == ['jakarta.validation.constraints.NotNull$List'] + param1.getAnnotationMetadata().getAnnotationNames().contains('jakarta.validation.constraints.Size$List') + param2.getAnnotationMetadata().getAnnotationNames().contains('jakarta.validation.constraints.NotEmpty$List') + param3.getAnnotationMetadata().getAnnotationNames().contains('jakarta.validation.constraints.NotNull$List') } @Issue('https://github.com/micronaut-projects/micronaut-core/issues/2083') diff --git a/inject-java/src/test/groovy/io/micronaut/inject/beans/BeanDefinitionSpec.groovy b/inject-java/src/test/groovy/io/micronaut/inject/beans/BeanDefinitionSpec.groovy index 636d4c1e489..41177383ee3 100644 --- a/inject-java/src/test/groovy/io/micronaut/inject/beans/BeanDefinitionSpec.groovy +++ b/inject-java/src/test/groovy/io/micronaut/inject/beans/BeanDefinitionSpec.groovy @@ -487,14 +487,10 @@ public class Test { def param3 = param2.getTypeParameters()[0] then: - param.getAnnotationMetadata().getAnnotationNames().size() == 1 - param.getAnnotationMetadata().getAnnotationNames().asList() == ['io.micronaut.validation.annotation.ValidatedElement'] - param1.getAnnotationMetadata().getAnnotationNames().size() == 1 - param1.getAnnotationMetadata().getAnnotationNames().asList() == ['jakarta.validation.constraints.Size$List'] - param2.getAnnotationMetadata().getAnnotationNames().size() == 1 - param2.getAnnotationMetadata().getAnnotationNames().asList() == ['jakarta.validation.constraints.NotEmpty$List'] - param3.getAnnotationMetadata().getAnnotationNames().size() == 1 - param3.getAnnotationMetadata().getAnnotationNames().asList() == ['jakarta.validation.constraints.NotNull$List'] + param.getAnnotationMetadata().getAnnotationNames().contains('io.micronaut.validation.annotation.ValidatedElement') + param1.getAnnotationMetadata().getAnnotationNames().contains('jakarta.validation.constraints.Size$List') + param2.getAnnotationMetadata().getAnnotationNames().contains('jakarta.validation.constraints.NotEmpty$List') + param3.getAnnotationMetadata().getAnnotationNames().contains('jakarta.validation.constraints.NotNull$List') } void "test isTypeVariable"() { diff --git a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/inject/ast/ClassElementSpec.groovy b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/inject/ast/ClassElementSpec.groovy index c69912ef184..7eb7e96acca 100644 --- a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/inject/ast/ClassElementSpec.groovy +++ b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/inject/ast/ClassElementSpec.groovy @@ -360,30 +360,28 @@ class Test { def field = ce.findField("deepList").get() def fieldType = field.getGenericType() - fieldType.getAnnotationMetadata().getAnnotationNames().size() == 0 - assertListGenericArgument(fieldType, { ClassElement listArg1 -> - assert listArg1.getAnnotationMetadata().getAnnotationNames().asList() == ['jakarta.validation.constraints.Size$List'] - assert listArg1.getTypeAnnotationMetadata().getAnnotationNames().asList() == ['jakarta.validation.constraints.Size$List'] + assert listArg1.getAnnotationMetadata().getAnnotationNames().contains('jakarta.validation.constraints.Size$List') + assert listArg1.getTypeAnnotationMetadata().getAnnotationNames().contains('jakarta.validation.constraints.Size$List') assertListGenericArgument(listArg1, { ClassElement listArg2 -> - assert listArg2.getAnnotationMetadata().getAnnotationNames().asList() == ['jakarta.validation.constraints.NotEmpty$List'] - assert listArg2.getTypeAnnotationMetadata().getAnnotationNames().asList() == ['jakarta.validation.constraints.NotEmpty$List'] + assert listArg2.getAnnotationMetadata().getAnnotationNames().contains('jakarta.validation.constraints.NotEmpty$List') + assert listArg2.getTypeAnnotationMetadata().getAnnotationNames().contains('jakarta.validation.constraints.NotEmpty$List') assertListGenericArgument(listArg2, { ClassElement listArg3 -> - assert listArg3.getAnnotationMetadata().getAnnotationNames().asList() == ['jakarta.validation.constraints.NotNull$List'] - assert listArg3.getTypeAnnotationMetadata().getAnnotationNames().asList() == ['jakarta.validation.constraints.NotNull$List'] + assert listArg3.getAnnotationMetadata().getAnnotationNames().contains('jakarta.validation.constraints.NotNull$List') + assert listArg3.getTypeAnnotationMetadata().getAnnotationNames().contains('jakarta.validation.constraints.NotNull$List') }) }) }) def level1 = fieldType.getTypeArguments()["E"] - level1.getAnnotationMetadata().getAnnotationNames().asList() == ['jakarta.validation.constraints.Size$List'] - level1.getTypeAnnotationMetadata().getAnnotationNames().asList() == ['jakarta.validation.constraints.Size$List'] + level1.getAnnotationMetadata().getAnnotationNames().contains('jakarta.validation.constraints.Size$List') + level1.getTypeAnnotationMetadata().getAnnotationNames().contains('jakarta.validation.constraints.Size$List') def level2 = level1.getTypeArguments()["E"] - level2.getAnnotationMetadata().getAnnotationNames().asList() == ['jakarta.validation.constraints.NotEmpty$List'] - level2.getTypeAnnotationMetadata().getAnnotationNames().asList() == ['jakarta.validation.constraints.NotEmpty$List'] + level2.getAnnotationMetadata().getAnnotationNames().contains('jakarta.validation.constraints.NotEmpty$List') + level2.getTypeAnnotationMetadata().getAnnotationNames().contains('jakarta.validation.constraints.NotEmpty$List') def level3 = level2.getTypeArguments()["E"] - level3.getAnnotationMetadata().getAnnotationNames().asList() == ['jakarta.validation.constraints.NotNull$List'] - level3.getTypeAnnotationMetadata().getAnnotationNames().asList() == ['jakarta.validation.constraints.NotNull$List'] + level3.getAnnotationMetadata().getAnnotationNames().contains('jakarta.validation.constraints.NotNull$List') + level3.getTypeAnnotationMetadata().getAnnotationNames().contains('jakarta.validation.constraints.NotNull$List') } void "test annotation metadata present on deep type parameters for method"() { @@ -407,27 +405,26 @@ class Test { def method = ce.findMethod("deepList").get() def theType = method.getGenericReturnType() - theType.getAnnotationMetadata().getAnnotationNames().size() == 0 def level1 = theType.getTypeArguments()["E"] - level1.getAnnotationMetadata().getAnnotationNames().asList() == ['jakarta.validation.constraints.Size$List'] - level1.getTypeAnnotationMetadata().getAnnotationNames().asList() == ['jakarta.validation.constraints.Size$List'] + level1.getAnnotationMetadata().getAnnotationNames().contains('jakarta.validation.constraints.Size$List') + level1.getTypeAnnotationMetadata().getAnnotationNames().contains('jakarta.validation.constraints.Size$List') def level2 = level1.getTypeArguments()["E"] - level2.getAnnotationMetadata().getAnnotationNames().asList() == ['jakarta.validation.constraints.NotEmpty$List'] - level2.getTypeAnnotationMetadata().getAnnotationNames().asList() == ['jakarta.validation.constraints.NotEmpty$List'] + level2.getAnnotationMetadata().getAnnotationNames().contains('jakarta.validation.constraints.NotEmpty$List') + level2.getTypeAnnotationMetadata().getAnnotationNames().contains('jakarta.validation.constraints.NotEmpty$List') def level3 = level2.getTypeArguments()["E"] - level3.getAnnotationMetadata().getAnnotationNames().asList() == ['jakarta.validation.constraints.NotNull$List'] - level3.getTypeAnnotationMetadata().getAnnotationNames().asList() == ['jakarta.validation.constraints.NotNull$List'] + level3.getAnnotationMetadata().getAnnotationNames().contains('jakarta.validation.constraints.NotNull$List') + level3.getTypeAnnotationMetadata().getAnnotationNames().contains('jakarta.validation.constraints.NotNull$List') assertListGenericArgument(theType, { ClassElement listArg1 -> - assert listArg1.getAnnotationMetadata().getAnnotationNames().asList() == ['jakarta.validation.constraints.Size$List'] - assert listArg1.getTypeAnnotationMetadata().getAnnotationNames().asList() == ['jakarta.validation.constraints.Size$List'] + assert listArg1.getAnnotationMetadata().getAnnotationNames().contains('jakarta.validation.constraints.Size$List') + assert listArg1.getTypeAnnotationMetadata().getAnnotationNames().contains('jakarta.validation.constraints.Size$List') assertListGenericArgument(listArg1, { ClassElement listArg2 -> - assert listArg2.getAnnotationMetadata().getAnnotationNames().asList() == ['jakarta.validation.constraints.NotEmpty$List'] - assert listArg2.getTypeAnnotationMetadata().getAnnotationNames().asList() == ['jakarta.validation.constraints.NotEmpty$List'] + assert listArg2.getAnnotationMetadata().getAnnotationNames().contains('jakarta.validation.constraints.NotEmpty$List') + assert listArg2.getTypeAnnotationMetadata().getAnnotationNames().contains('jakarta.validation.constraints.NotEmpty$List') assertListGenericArgument(listArg2, { ClassElement listArg3 -> - assert listArg3.getTypeAnnotationMetadata().getAnnotationNames().asList() == ['jakarta.validation.constraints.NotNull$List'] - assert listArg3.getAnnotationMetadata().getAnnotationNames().asList() == ['jakarta.validation.constraints.NotNull$List'] + assert listArg3.getTypeAnnotationMetadata().getAnnotationNames().contains('jakarta.validation.constraints.NotNull$List') + assert listArg3.getAnnotationMetadata().getAnnotationNames().contains('jakarta.validation.constraints.NotNull$List') }) }) }) From 85f77f250a64e98b1a91adf4affa69e7e8ae9ab9 Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Wed, 22 Mar 2023 11:25:15 +0100 Subject: [PATCH 616/743] tck: static resource test (#8971) * test: static resource test see: https://github.com/micronaut-projects/micronaut-aws/issues/1361 --------- Co-authored-by: Graeme Rocher Co-authored-by: Tim Yates --- .../staticresources/StaticResourceTest.java | 52 +++++++++++++++++++ .../src/main/resources/assets/hello.txt | 1 + 2 files changed, 53 insertions(+) create mode 100644 http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/staticresources/StaticResourceTest.java create mode 100644 http-server-tck/src/main/resources/assets/hello.txt diff --git a/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/staticresources/StaticResourceTest.java b/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/staticresources/StaticResourceTest.java new file mode 100644 index 00000000000..853fb27ead9 --- /dev/null +++ b/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/staticresources/StaticResourceTest.java @@ -0,0 +1,52 @@ +/* + * Copyright 2017-2023 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.http.server.tck.tests.staticresources; + +import io.micronaut.core.util.CollectionUtils; +import io.micronaut.http.HttpHeaders; +import io.micronaut.http.HttpRequest; +import io.micronaut.http.HttpStatus; +import io.micronaut.http.MediaType; +import io.micronaut.http.server.tck.AssertionUtils; +import io.micronaut.http.uri.UriBuilder; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.util.Collections; + +import static io.micronaut.http.server.tck.TestScenario.asserts; + +@SuppressWarnings({ + "java:S5960", // We're allowed assertions, as these are used in tests only + "checkstyle:MissingJavadocType", + "checkstyle:DesignForExtension" +}) +public class StaticResourceTest { + public static final String SPEC_NAME = "StaticResourceTest"; + + @Test + public void staticResource() throws IOException { + asserts(SPEC_NAME, + CollectionUtils.mapOf( + "micronaut.router.static-resources.assets.mapping", "/assets/**", + "micronaut.router.static-resources.assets.paths", "classpath:assets"), + HttpRequest.GET(UriBuilder.of("/assets").path("hello.txt").build()).accept(MediaType.TEXT_PLAIN), + (server, request) -> AssertionUtils.assertDoesNotThrow(server, request, + HttpStatus.OK, + "Hello World", + Collections.singletonMap(HttpHeaders.CONTENT_TYPE, MediaType.TEXT_PLAIN))); + } +} diff --git a/http-server-tck/src/main/resources/assets/hello.txt b/http-server-tck/src/main/resources/assets/hello.txt new file mode 100644 index 00000000000..557db03de99 --- /dev/null +++ b/http-server-tck/src/main/resources/assets/hello.txt @@ -0,0 +1 @@ +Hello World From 691c87d8316430f04e4cda81c450a47631176360 Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Wed, 22 Mar 2023 16:13:46 +0100 Subject: [PATCH 617/743] doc: don't use Micronaut as a noun (#8958) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * doc: don't use Micronaut as a noun * doc: don't use Micronaut as a noun According to Trademark policy https://objectcomputing.com/files/6816/4867/5102/Micronaut_Trademark_Policy.pdf > § You may not use the word Micronaut as a verb or a noun. It should be used only as an adjective followed by a noun (e.g., “I love using the Micronaut® framework,” not “I love using Micronaut!”). --- gradle.properties | 2 +- src/main/docs/guide/aop.adoc | 2 +- src/main/docs/guide/aop/adapterAdvice.adoc | 4 ++-- src/main/docs/guide/aop/aroundAdvice.adoc | 4 ++-- src/main/docs/guide/aop/caching.adoc | 4 ++-- src/main/docs/guide/aop/introductionAdvice.adoc | 2 +- src/main/docs/guide/aop/lifecycleAdvice.adoc | 2 +- src/main/docs/guide/aop/retry.adoc | 2 +- src/main/docs/guide/aop/scheduling.adoc | 4 ++-- src/main/docs/guide/aop/springAop.adoc | 6 +++--- src/main/docs/guide/aop/validation.adoc | 2 +- src/main/docs/guide/appendix/architecture.adoc | 2 +- .../appendix/architecture/annotationArch.adoc | 2 +- .../docs/guide/appendix/architecture/iocArch.adoc | 2 +- src/main/docs/guide/cli.adoc | 4 ++-- src/main/docs/guide/cloud.adoc | 4 ++-- .../docs/guide/cloud/clientSideLoadBalancing.adoc | 2 +- .../clientSideLoadBalancing/netflixRibbon.adoc | 2 +- src/main/docs/guide/cloud/cloudConfiguration.adoc | 5 ++--- .../distributedConfiguration.adoc | 4 ++-- .../distributedConfigurationAwsParameterStore.adoc | 4 ++-- .../distributedConfigurationConsul.adoc | 6 +++--- .../distributedConfigurationSpringCloud.adoc | 4 ++-- .../distributedConfigurationVault.adoc | 4 ++-- src/main/docs/guide/cloud/serviceDiscovery.adoc | 2 +- .../serviceDiscoveryKubernetes.adoc | 2 +- .../serviceDiscovery/serviceDiscoveryManual.adoc | 2 +- .../serviceDiscovery/serviceDiscoveryRoute53.adoc | 2 +- src/main/docs/guide/config.adoc | 2 +- .../docs/guide/config/configurationProperties.adoc | 12 ++++++------ .../docs/guide/config/customTypeConverter.adoc | 2 +- src/main/docs/guide/config/eachProperty.adoc | 2 +- src/main/docs/guide/config/environments.adoc | 14 +++++++------- src/main/docs/guide/config/immutableConfig.adoc | 8 ++++---- src/main/docs/guide/config/jmx.adoc | 2 +- src/main/docs/guide/config/propertySource.adoc | 8 ++++---- src/main/docs/guide/config/valueAnnotation.adoc | 2 +- src/main/docs/guide/configurations.adoc | 2 +- src/main/docs/guide/configurations/dataAccess.adoc | 2 +- .../configurations/dataAccess/flywaySupport.adoc | 2 +- .../dataAccess/hibernateSupport.adoc | 4 ++-- .../dataAccess/liquibaseSupport.adoc | 2 +- .../configurations/dataAccess/mongoSupport.adoc | 2 +- .../configurations/dataAccess/mysqlSupport.adoc | 2 +- .../configurations/dataAccess/neo4jSupport.adoc | 2 +- .../configurations/dataAccess/postgresSupport.adoc | 2 +- .../configurations/dataAccess/redisSupport.adoc | 2 +- src/main/docs/guide/httpClient.adoc | 2 +- .../docs/guide/httpClient/clientAnnotation.adoc | 2 +- .../clientAnnotationStreaming.adoc | 2 +- .../clientAnnotation/clientFallback.adoc | 2 +- .../clientAnnotation/netflixHystrix.adoc | 2 +- src/main/docs/guide/httpClient/clientFilter.adoc | 2 +- src/main/docs/guide/httpClient/clientHttp3.adoc | 2 +- .../lowLevelHttpClient/clientConfiguration.adoc | 2 +- src/main/docs/guide/httpServer.adoc | 4 ++-- src/main/docs/guide/httpServer/apiVersioning.adoc | 4 ++-- src/main/docs/guide/httpServer/binding.adoc | 6 +++--- .../docs/guide/httpServer/clientIpAddress.adoc | 2 +- .../guide/httpServer/customArgumentBinding.adoc | 6 +++--- src/main/docs/guide/httpServer/datavalidation.adoc | 2 +- .../httpServer/errorHandling/errorFormatting.adoc | 2 +- .../exceptionHandler/builtInExceptionHandlers.adoc | 2 +- .../filtermethods/filtermethodsexample.adoc | 2 +- .../httpServerFilter/httpServerFilterExample.adoc | 2 +- src/main/docs/guide/httpServer/formData.adoc | 2 +- src/main/docs/guide/httpServer/hostResolution.adoc | 2 +- src/main/docs/guide/httpServer/http2Server.adoc | 4 ++-- src/main/docs/guide/httpServer/http3Server.adoc | 2 +- src/main/docs/guide/httpServer/jsonBinding.adoc | 6 +++--- .../docs/guide/httpServer/localeResolution.adoc | 2 +- src/main/docs/guide/httpServer/openapi.adoc | 2 +- src/main/docs/guide/httpServer/reactiveServer.adoc | 2 +- .../httpServer/reactiveServer/bodyAnnotation.adoc | 6 +++--- .../reactiveServer/reactiveResponses.adoc | 8 ++++---- src/main/docs/guide/httpServer/routing.adoc | 6 +++--- .../docs/guide/httpServer/serverConfiguration.adoc | 2 +- .../guide/httpServer/serverConfiguration/cors.adoc | 2 +- .../serverConfiguration/dualProtocol.adoc | 5 ++--- .../httpServer/serverConfiguration/https.adoc | 14 +++++++------- .../serverConfiguration/nettyClientPipeline.adoc | 2 +- .../serverConfiguration/nettyServerPipeline.adoc | 2 +- .../serverConfiguration/secondaryServers.adoc | 3 +-- .../threadPools/atBlocking.adoc | 4 ++-- .../threadPools/blockingOperations.adoc | 2 +- .../docs/guide/httpServer/staticResources.adoc | 2 +- src/main/docs/guide/httpServer/transfers.adoc | 6 +++--- src/main/docs/guide/httpServer/views.adoc | 2 +- src/main/docs/guide/httpServer/websocket.adoc | 2 +- .../httpServer/websocket/websocketClient.adoc | 2 +- .../httpServer/websocket/websocketServer.adoc | 4 ++-- src/main/docs/guide/introduction.adoc | 14 +++++++------- src/main/docs/guide/introduction/upgrading.adoc | 10 +++++----- src/main/docs/guide/introduction/whatsNew.adoc | 8 ++++---- src/main/docs/guide/ioc.adoc | 6 +++--- src/main/docs/guide/ioc/annotationMetadata.adoc | 6 +++--- src/main/docs/guide/ioc/beanConfigurations.adoc | 2 +- src/main/docs/guide/ioc/beanImport.adoc | 4 ++-- src/main/docs/guide/ioc/beans.adoc | 4 ++-- src/main/docs/guide/ioc/contextEvents.adoc | 4 ++-- src/main/docs/guide/ioc/factories.adoc | 4 ++-- src/main/docs/guide/ioc/how.adoc | 6 +++--- src/main/docs/guide/ioc/introspection.adoc | 2 +- src/main/docs/guide/ioc/lifecycle.adoc | 4 ++-- .../docs/guide/ioc/nullabilityAnnotations.adoc | 4 ++-- src/main/docs/guide/ioc/qualifiers.adoc | 10 +++++----- src/main/docs/guide/ioc/scopes.adoc | 2 +- .../guide/ioc/scopes/builtInScopes/eagerInit.adoc | 2 +- src/main/docs/guide/ioc/springBeans.adoc | 2 +- src/main/docs/guide/ioc/types.adoc | 4 ++-- src/main/docs/guide/languageSupport.adoc | 2 +- src/main/docs/guide/languageSupport/graal.adoc | 2 +- .../docs/guide/languageSupport/graal/graalFAQ.adoc | 6 +++--- .../guide/languageSupport/graal/graalServices.adoc | 14 +++++++------- src/main/docs/guide/languageSupport/groovy.adoc | 6 +++--- src/main/docs/guide/languageSupport/java.adoc | 4 ++-- .../java/incrementalannotationgradle.adoc | 4 ++-- .../docs/guide/languageSupport/java/java9.adoc | 2 +- .../docs/guide/languageSupport/java/lombok.adoc | 2 +- .../languageSupport/java/retainparameternames.adoc | 2 +- src/main/docs/guide/languageSupport/kotlin.adoc | 4 ++-- .../kotlin/coroutineTracingContextPropagation.adoc | 2 +- .../guide/languageSupport/kotlin/kaptOrKsp.adoc | 4 ++-- .../guide/languageSupport/kotlin/kaptintellij.adoc | 2 +- .../kotlin/kotlinContextPropagation.adoc | 2 +- .../kotlin/kotlinretainparamnames.adoc | 4 ++-- .../guide/languageSupport/kotlin/openandaop.adoc | 2 +- src/main/docs/guide/logging.adoc | 2 +- .../docs/guide/logging/loggingConfiguration.adoc | 2 +- src/main/docs/guide/logging/loggingSystem.adoc | 2 +- .../docs/guide/management/providedEndpoints.adoc | 2 +- .../providedEndpoints/healthEndpoint.adoc | 2 +- .../providedEndpoints/metricsEndpoint.adoc | 2 +- src/main/docs/guide/messaging/kafka.adoc | 2 +- src/main/docs/guide/messaging/nats.adoc | 2 +- src/main/docs/guide/messaging/rabbitmq.adoc | 4 ++-- src/main/docs/guide/quickStart.adoc | 2 +- src/main/docs/guide/quickStart/creatingClient.adoc | 4 ++-- .../guide/quickStart/ideSetup/eclipseSetup.adoc | 2 +- .../guide/quickStart/ideSetup/vsCodeSetup.adoc | 2 +- src/main/docs/guide/security.adoc | 2 +- src/main/docs/guide/serverlessFunctions.adoc | 2 +- .../docs/guide/serverlessFunctions/awsLambda.adoc | 2 +- .../guide/serverlessFunctions/azureFunction.adoc | 2 +- .../guide/serverlessFunctions/gcpFunction.adoc | 2 +- src/main/docs/guide/spring.adoc | 2 +- 146 files changed, 253 insertions(+), 256 deletions(-) diff --git a/gradle.properties b/gradle.properties index 84f4dbacf93..80d4ef1853b 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,7 +1,7 @@ projectVersion=4.0.0-SNAPSHOT projectGroupId=io.micronaut -title=Micronaut projectDesc=Natively Cloud Native +title=Micronaut Framework githubSlug=micronaut-projects/micronaut-core docsRepository=micronaut-projects/micronaut-docs testsdir=inject-groovy/src/test/groovy/io/micronaut/docs diff --git a/src/main/docs/guide/aop.adoc b/src/main/docs/guide/aop.adoc index 3a1dd921ea0..e31d17d94ab 100644 --- a/src/main/docs/guide/aop.adoc +++ b/src/main/docs/guide/aop.adoc @@ -9,4 +9,4 @@ In modern Java applications, declaring advice typically takes the form of an ann The disadvantage of traditional approaches to AOP is the heavy reliance on runtime proxy creation and reflection, which slows application performance, makes debugging harder and increases memory consumption. -Micronaut tries to address these concerns by providing a simple compile-time AOP API that does not use reflection. +Micronaut framework tries to address these concerns by providing a simple compile-time AOP API that does not use reflection. diff --git a/src/main/docs/guide/aop/adapterAdvice.adoc b/src/main/docs/guide/aop/adapterAdvice.adoc index a9cb738dea4..7dba9d59752 100644 --- a/src/main/docs/guide/aop/adapterAdvice.adoc +++ b/src/main/docs/guide/aop/adapterAdvice.adoc @@ -14,7 +14,7 @@ void onStartup(StartupEvent event) { } ---- -The presence of the ann:runtime.event.annotation.EventListener[] annotation causes Micronaut to create a new class that implements api:context.event.ApplicationEventListener[] and invokes the `onStartup` method defined in the bean above. +The presence of the ann:runtime.event.annotation.EventListener[] annotation causes the Micronaut framework to create a new class that implements api:context.event.ApplicationEventListener[] and invokes the `onStartup` method defined in the bean above. The actual implementation of the ann:runtime.event.annotation.EventListener[] is trivial; it simply uses the ann:aop.Adapter[] annotation to specify which SAM (single abstract method) type it adapts: @@ -27,6 +27,6 @@ include::context/src/main/java/io/micronaut/runtime/event/annotation/EventListen <1> The ann:aop.Adapter[] annotation indicates which SAM type to adapt, in this case api:context.event.ApplicationEventListener[]. -NOTE: Micronaut also automatically aligns the generic types for the SAM interface if they are specified. +NOTE: The Micronaut framework also automatically aligns the generic types for the SAM interface if they are specified. Using this mechanism you can define custom annotations that use the ann:aop.Adapter[] annotation and a SAM interface to automatically implement beans for you at compile time. diff --git a/src/main/docs/guide/aop/aroundAdvice.adoc b/src/main/docs/guide/aop/aroundAdvice.adoc index 73a72271f4d..f0dda9beb85 100644 --- a/src/main/docs/guide/aop/aroundAdvice.adoc +++ b/src/main/docs/guide/aop/aroundAdvice.adoc @@ -9,7 +9,7 @@ snippet::io.micronaut.docs.aop.around.NotNull[tags="imports,annotation", indent= <1> The retention policy of the annotation should be `RUNTIME` <2> Generally you want to be able to apply advice at the class or method level so the target types are `TYPE` and `METHOD` -<3> The ann:aop.Around[] annotation is added to tell Micronaut that the annotation is Around advice +<3> The ann:aop.Around[] annotation is added to tell the Micronaut framework that the annotation is Around advice The next step to defining Around advice is to implement a api:aop.MethodInterceptor[]. For example the following interceptor disallows parameters with `null` values: @@ -41,7 +41,7 @@ This behaviour is more efficient as only one instance of the bean is required, h * `proxyTarget` (defaults to `false`) - If set to `true`, instead of a subclass that calls `super`, the proxy delegates to the original bean instance * `hotswap` (defaults to `false`) - Same as `proxyTarget=true`, but in addition the proxy implements link:{api}/io/micronaut/aop/HotSwappableInterceptedProxy.html[HotSwappableInterceptedProxy] which wraps each method call in a `ReentrantReadWriteLock` and allows swapping the target instance at runtime. -* `lazy` (defaults to `false`) - By default Micronaut eagerly initializes the proxy target when the proxy is created. If set to `true` the proxy target is instead resolved lazily for each method call. +* `lazy` (defaults to `false`) - By default the Micronaut framework eagerly initializes the proxy target when the proxy is created. If set to `true` the proxy target is instead resolved lazily for each method call. == AOP Advice on @Factory Beans diff --git a/src/main/docs/guide/aop/caching.adoc b/src/main/docs/guide/aop/caching.adoc index bf27ba3a9ef..44e6c6b2ffa 100644 --- a/src/main/docs/guide/aop/caching.adoc +++ b/src/main/docs/guide/aop/caching.adoc @@ -1,4 +1,4 @@ -Like Spring and Grails, Micronaut provides caching annotations in the link:{micronautcacheapi}/io/micronaut/cache/package-summary.html[io.micronaut.cache] package. +Like Spring and Grails, the Micronaut framework provides caching annotations in the link:{micronautcacheapi}/io/micronaut/cache/package-summary.html[io.micronaut.cache] package. The link:{micronautcacheapi}/io/micronaut/cache/CacheManager.html[CacheManager] interface allows different cache implementations to be plugged in as necessary. @@ -46,7 +46,7 @@ See the https://micronaut-projects.github.io/micronaut-cache/latest/guide/config A {micronautcacheapi}/io/micronaut/cache/DynamicCacheManager.html[DynamicCacheManager] bean can be registered for use cases where caches cannot be configured ahead of time. When a cache is attempted to be retrieved that was not predefined, the dynamic cache manager is invoked to create and return a cache. -By default, if there is no other dynamic cache manager defined in the application, Micronaut registers an instance of {micronautcacheapi}/io/micronaut/cache/caffeine/DefaultDynamicCacheManager.html[DefaultDynamicCacheManager] that creates Caffeine caches with default values. +By default, if there is no other dynamic cache manager defined in the application, the Micronaut framework registers an instance of {micronautcacheapi}/io/micronaut/cache/caffeine/DefaultDynamicCacheManager.html[DefaultDynamicCacheManager] that creates Caffeine caches with default values. == Other Cache Implementations diff --git a/src/main/docs/guide/aop/introductionAdvice.adoc b/src/main/docs/guide/aop/introductionAdvice.adoc index d3472c4081d..8e8e9b263e3 100644 --- a/src/main/docs/guide/aop/introductionAdvice.adoc +++ b/src/main/docs/guide/aop/introductionAdvice.adoc @@ -2,7 +2,7 @@ Introduction advice is distinct from Around advice in that it involves providing Examples of introduction advice include https://gorm.grails.org[GORM] and https://projects.spring.io/spring-data[Spring Data] which implement persistence logic for you. -Micronaut's api:http.client.annotation.Client[] annotation is another example of introduction advice where Micronaut implements HTTP client interfaces for you at compile time. +Micronaut api:http.client.annotation.Client[] annotation is another example of introduction advice where the Micronaut framework implements HTTP client interfaces for you at compile time. The way you implement Introduction advice is very similar to how you implement Around advice. diff --git a/src/main/docs/guide/aop/lifecycleAdvice.adoc b/src/main/docs/guide/aop/lifecycleAdvice.adoc index 4e13bcf96c6..6c5f31794b2 100644 --- a/src/main/docs/guide/aop/lifecycleAdvice.adoc +++ b/src/main/docs/guide/aop/lifecycleAdvice.adoc @@ -4,7 +4,7 @@ Sometimes you may need to apply advice to a bean's lifecycle. There are 3 types * Interception of the bean's `@PostConstruct` invocation * Interception of a bean's `@PreDestroy` invocation -Micronaut supports these 3 use cases by allowing the definition of additional ann:aop.InterceptorBinding[] meta-annotations. +The Micronaut framework supports these 3 use cases by allowing the definition of additional ann:aop.InterceptorBinding[] meta-annotations. Consider the following annotation definition: diff --git a/src/main/docs/guide/aop/retry.adoc b/src/main/docs/guide/aop/retry.adoc index 1177e0687b8..ec44b78ab10 100644 --- a/src/main/docs/guide/aop/retry.adoc +++ b/src/main/docs/guide/aop/retry.adoc @@ -1,6 +1,6 @@ In distributed systems and microservice environments, failure is something you have to plan for, and it is common to want to attempt to retry an operation if it fails. If first you don't succeed try again! -With this in mind, Micronaut includes a api:retry.annotation.Retryable[] annotation. +With this in mind, the Micronaut framework includes a api:retry.annotation.Retryable[] annotation. == Retry Dependency diff --git a/src/main/docs/guide/aop/scheduling.adoc b/src/main/docs/guide/aop/scheduling.adoc index 84fdeb0ed64..f3bff043eb5 100644 --- a/src/main/docs/guide/aop/scheduling.adoc +++ b/src/main/docs/guide/aop/scheduling.adoc @@ -1,4 +1,4 @@ -Like Spring and Grails, Micronaut features a api:scheduling.annotation.Scheduled[] annotation for scheduling background tasks. +Like Spring and Grails, the Micronaut framework features a api:scheduling.annotation.Scheduled[] annotation for scheduling background tasks. == Using the @Scheduled Annotation @@ -71,6 +71,6 @@ include::{includedir}configurationProperties/io.micronaut.scheduling.executor.Us == Handling Exceptions -By default, Micronaut includes a api:io.micronaut.scheduling.DefaultTaskExceptionHandler[] bean that implements the api:io.micronaut.scheduling.TaskExceptionHandler[] interface and simply logs the exception if an error occurs invoking a scheduled task. +By default, the Micronaut framework includes a api:io.micronaut.scheduling.DefaultTaskExceptionHandler[] bean that implements the api:io.micronaut.scheduling.TaskExceptionHandler[] interface and simply logs the exception if an error occurs invoking a scheduled task. If you have custom requirements you can replace this bean with your own implementation (for example to send an email or shutdown the context to fail fast). To do so, write your own api:io.micronaut.scheduling.TaskExceptionHandler[] and annotate it with `@Replaces(DefaultTaskExceptionHandler.class)`. diff --git a/src/main/docs/guide/aop/springAop.adoc b/src/main/docs/guide/aop/springAop.adoc index fa4ab24cbd5..750b27949a2 100644 --- a/src/main/docs/guide/aop/springAop.adoc +++ b/src/main/docs/guide/aop/springAop.adoc @@ -1,10 +1,10 @@ -Although Micronaut's design is based on a compile-time approach and does not rely on Spring dependency injection, there is still a lot of value in the Spring ecosystem that does not depend directly on the Spring container. +Although the Micronaut framework's design is based on a compile-time approach and does not rely on Spring dependency injection, there is still a lot of value in the Spring ecosystem that does not depend directly on the Spring container. -You may wish to use existing Spring projects within Micronaut and configure beans to be used within Micronaut. +You may wish to use existing Spring projects within the Micronaut framework and configure beans to be used within the Micronaut framework. You may also wish to leverage existing AOP advice from Spring. One example of this is Spring's support for declarative transactions with `@Transactional`. -Micronaut provides support for Spring-based transaction management without requiring Spring itself. Simply add the `spring` module to your application dependencies: +The Micronaut framework provides support for Spring-based transaction management without requiring Spring itself. Simply add the `spring` module to your application dependencies: dependency:io.micronaut.spring:micronaut-spring[gradleScope="implementation"] diff --git a/src/main/docs/guide/aop/validation.adoc b/src/main/docs/guide/aop/validation.adoc index 3d434aac454..68c63c8e2ea 100644 --- a/src/main/docs/guide/aop/validation.adoc +++ b/src/main/docs/guide/aop/validation.adoc @@ -2,7 +2,7 @@ Validation advice is one of the most common advice types you are likely to want Validation advice is built on https://beanvalidation.org/2.0/spec/[Bean Validation JSR 380], a specification of the Java API for bean validation which ensures that the properties of a bean meet specific criteria, using `jakarta.validation` annotations such as `@NotNull`, `@Min`, and `@Max`. -Micronaut provides native support for the `jakarta.validation` annotations with the `micronaut-validation` dependency: +The Micronaut framework provides native support for the `jakarta.validation` annotations with the `micronaut-validation` dependency: dependency:micronaut-validation-processor[groupId="io.micronaut.validation",scope="annotationProcessor"] diff --git a/src/main/docs/guide/appendix/architecture.adoc b/src/main/docs/guide/appendix/architecture.adoc index 946603cfe36..261cb9068fc 100644 --- a/src/main/docs/guide/appendix/architecture.adoc +++ b/src/main/docs/guide/appendix/architecture.adoc @@ -1,4 +1,4 @@ -The following documentation describes Micronaut's architecture and is designed for those who are looking for information on the internal workings of Micronaut and how it is architected. This is not intended as end-user developer documentation, but for those interested in the inner workings of Micronaut. +The following documentation describes the Micronaut framework's architecture and is designed for those who are looking for information on the internal workings of the Micronaut framework and how it is architected. This is not intended as end-user developer documentation, but for those interested in the inner workings of the Micronaut framework. This documentation is divided into sections that describe the compiler, introspections, application container, dependency injection and so on. diff --git a/src/main/docs/guide/appendix/architecture/annotationArch.adoc b/src/main/docs/guide/appendix/architecture/annotationArch.adoc index f135dccf7b8..3accad8c55b 100644 --- a/src/main/docs/guide/appendix/architecture/annotationArch.adoc +++ b/src/main/docs/guide/appendix/architecture/annotationArch.adoc @@ -1,4 +1,4 @@ -Micronaut is an implementation of an annotation-based programming model. That is to say annotations form a fundamental part of the API design of the framework. +Micronaut framework is an implementation of an annotation-based programming model. That is to say annotations form a fundamental part of the API design of the framework. Given this design decision, a compilation-time model was formulated to address the challenges of evaluating annotations at runtime. diff --git a/src/main/docs/guide/appendix/architecture/iocArch.adoc b/src/main/docs/guide/appendix/architecture/iocArch.adoc index 24ba957cbe7..f013df37f67 100644 --- a/src/main/docs/guide/appendix/architecture/iocArch.adoc +++ b/src/main/docs/guide/appendix/architecture/iocArch.adoc @@ -1,4 +1,4 @@ -Micronaut is an implementation of the JSR-330 specification for Dependency Injection. +Micronaut framework is an implementation of the JSR-330 specification for Dependency Injection. Dependency Injection (or Inversion of Control) is a widely adopted and common pattern in Java that allows loosely decoupling components to allow applications to be easily extended and tested. diff --git a/src/main/docs/guide/cli.adoc b/src/main/docs/guide/cli.adoc index f7eed9218c3..2f0d44dc4af 100644 --- a/src/main/docs/guide/cli.adoc +++ b/src/main/docs/guide/cli.adoc @@ -2,14 +2,14 @@ The Micronaut CLI is the recommended way to create new Micronaut projects. The C TIP: We have a website that can be used to generate projects instead of the CLI. Check out https://micronaut.io/launch/[Micronaut Launch] to get started! -When <>, you can call the CLI with the `mn` command. +When <>, you can call the CLI with the `mn` command. [source,bash] ---- $ mn create-app my-app ---- -A Micronaut CLI project can be identified by the `micronaut-cli.yml` file, which is included at the project root if it was generated via the CLI. This file will include the project's profile, default package, and other variables. The project's default package is evaluated based on the project name. +A Micronaut framework CLI project can be identified by the `micronaut-cli.yml` file, which is included at the project root if it was generated via the CLI. This file will include the project's profile, default package, and other variables. The project's default package is evaluated based on the project name. [source,bash] ---- diff --git a/src/main/docs/guide/cloud.adoc b/src/main/docs/guide/cloud.adoc index cd929ded590..5ef3d008266 100644 --- a/src/main/docs/guide/cloud.adoc +++ b/src/main/docs/guide/cloud.adoc @@ -1,6 +1,6 @@ The majority of JVM frameworks in use today were designed before the rise of cloud deployments and microservice architectures. Applications built with these frameworks were intended to be deployed to traditional Java containers. As a result, cloud support in these frameworks typically comes as an add-on rather than as core design features. -Micronaut was designed from the ground up for building microservices for the cloud. As a result, many key features that typically require external libraries or services are available within your application itself. To override one of the industry's current favorite buzzwords, Micronaut applications are "natively cloud-native". +Micronaut framework was designed from the ground up for building microservices for the cloud. As a result, many key features that typically require external libraries or services are available within your application itself. To override one of the industry's current favorite buzzwords, Micronaut applications are "natively cloud-native". The following are some cloud-specific features that are integrated directly into the Micronaut runtime: @@ -10,6 +10,6 @@ The following are some cloud-specific features that are integrated directly into * Distributed Tracing * Serverless Functions -Many features in Micronaut are heavily inspired by features from Spring and Grails. This is by design and helps developers who are already familiar with systems such as Spring Cloud. +Many features in the Micronaut framework are heavily inspired by features from Spring and Grails. This is by design and helps developers who are already familiar with systems such as Spring Cloud. The following sections cover these features and how to use them. diff --git a/src/main/docs/guide/cloud/clientSideLoadBalancing.adoc b/src/main/docs/guide/cloud/clientSideLoadBalancing.adoc index df9e71adcfb..647dbbc44a9 100644 --- a/src/main/docs/guide/cloud/clientSideLoadBalancing.adoc +++ b/src/main/docs/guide/cloud/clientSideLoadBalancing.adoc @@ -1,6 +1,6 @@ When <> from Consul, Eureka, or other Service Discovery servers, the api:discovery.DiscoveryClient[] emits a list of available api:discovery.ServiceInstance[]. -Micronaut by default automatically performs Round Robin client-side load balancing using the servers in this list. This combined with <> adds extra resiliency to your Microservice infrastructure. +The Micronaut framework by default automatically performs Round Robin client-side load balancing using the servers in this list. This combined with <> adds extra resiliency to your Microservice infrastructure. The load balancing is handled by the api:http.client.LoadBalancer[] interface, which has a api:http.client.LoadBalancer.select()[] method that returns a `Publisher` which emits a api:discovery.ServiceInstance[]. diff --git a/src/main/docs/guide/cloud/clientSideLoadBalancing/netflixRibbon.adoc b/src/main/docs/guide/cloud/clientSideLoadBalancing/netflixRibbon.adoc index 5a5ca986de1..4b5c6ccfb71 100644 --- a/src/main/docs/guide/cloud/clientSideLoadBalancing/netflixRibbon.adoc +++ b/src/main/docs/guide/cloud/clientSideLoadBalancing/netflixRibbon.adoc @@ -39,4 +39,4 @@ ribbon: ServerListRefreshInterval: 2000 ---- -By default Micronaut registers a link:{micronautribbonapi}/io/micronaut/configuration/ribbon/DiscoveryClientServerList.html[DiscoveryClientServerList] for each client that integrates Ribbon with Micronaut's api:discovery.DiscoveryClient[]. +By default, the Micronaut framework registers a link:{micronautribbonapi}/io/micronaut/configuration/ribbon/DiscoveryClientServerList.html[DiscoveryClientServerList] for each client that integrates Ribbon with the Micronaut framework's api:discovery.DiscoveryClient[]. diff --git a/src/main/docs/guide/cloud/cloudConfiguration.adoc b/src/main/docs/guide/cloud/cloudConfiguration.adoc index f77cdd0a7c1..175b047d24b 100644 --- a/src/main/docs/guide/cloud/cloudConfiguration.adoc +++ b/src/main/docs/guide/cloud/cloudConfiguration.adoc @@ -92,13 +92,12 @@ TIP: Any configuration property in the api:context.env.Environment[] can also be == Using Cloud Instance Metadata -When Micronaut detects it is running on a supported cloud platform, on startup it populates the interface api:discovery.cloud.ComputeInstanceMetadata[]. +When the Micronaut framework detects it is running on a supported cloud platform, on startup it populates the interface api:discovery.cloud.ComputeInstanceMetadata[]. -TIP: As of Micronaut 2.1.x this logic depends on the presence of the appropriate core Cloud module for Oracle Cloud, AWS, or GCP. +TIP: As of Micronaut framework 2.1.x this logic depends on the presence of the appropriate core Cloud module for Oracle Cloud, AWS, or GCP. All this data is merged together into the `metadata` property for the running api:discovery.ServiceInstance[]. - To access the metadata for your application instance you can use the interface api:discovery.EmbeddedServerInstance[], and call `getMetadata()` which returns a Map of the metadata. If you connect remotely via a client, the instance metadata can be referenced once you have retrieved a api:io.micronaut.discovery.ServiceInstance[] from either the api:http.client.LoadBalancer[] or api:discovery.DiscoveryClient[] APIs. diff --git a/src/main/docs/guide/cloud/cloudConfiguration/distributedConfiguration.adoc b/src/main/docs/guide/cloud/cloudConfiguration/distributedConfiguration.adoc index 966fe10cba9..a32951929a3 100644 --- a/src/main/docs/guide/cloud/cloudConfiguration/distributedConfiguration.adoc +++ b/src/main/docs/guide/cloud/cloudConfiguration/distributedConfiguration.adoc @@ -1,6 +1,6 @@ -As you can see, Micronaut features a robust system for externalizing and adapting configuration to the environment inspired by similar approaches in Grails and Spring Boot. +As you can see, the Micronaut framework features a robust system for externalizing and adapting configuration to the environment inspired by similar approaches in Grails and Spring Boot. -However, what if you want multiple microservices to share configuration? Micronaut includes APIs for distributed configuration. +However, what if you want multiple microservices to share configuration? the Micronaut framework includes APIs for distributed configuration. The api:discovery.config.ConfigurationClient[] interface has a `getPropertySources` method that can be implemented to read and resolve configuration from distributed sources. diff --git a/src/main/docs/guide/cloud/cloudConfiguration/distributedConfigurationAwsParameterStore.adoc b/src/main/docs/guide/cloud/cloudConfiguration/distributedConfigurationAwsParameterStore.adoc index 2ba77b40ebb..c2e0287c49b 100644 --- a/src/main/docs/guide/cloud/cloudConfiguration/distributedConfigurationAwsParameterStore.adoc +++ b/src/main/docs/guide/cloud/cloudConfiguration/distributedConfigurationAwsParameterStore.adoc @@ -1,4 +1,4 @@ -Micronaut supports configuration sharing via AWS System Manager Parameter Store. You need the following dependencies configured: +The Micronaut framework supports configuration sharing via AWS System Manager Parameter Store. You need the following dependencies configured: dependency:io.micronaut.aws:micronaut-aws-parameter-store[] @@ -22,7 +22,7 @@ See the https://micronaut-projects.github.io/micronaut-aws/latest/guide/configur You can configure shared properties from the AWS Console -> System Manager -> Parameter Store. -Micronaut uses a hierarchy to read configuration values, and supports `String`, `StringList`, and `SecureString` types. +The Micronaut framework uses a hierarchy to read configuration values, and supports `String`, `StringList`, and `SecureString` types. You can create environment-specific configurations as well by including the environment name after an underscore `_`. For example if `micronaut.application.name` is set to `helloworld`, specifying configuration values under `helloworld_test` will be applied only to the `test` environment. diff --git a/src/main/docs/guide/cloud/cloudConfiguration/distributedConfigurationConsul.adoc b/src/main/docs/guide/cloud/cloudConfiguration/distributedConfigurationConsul.adoc index 1f1bfffef9d..25694b56dcb 100644 --- a/src/main/docs/guide/cloud/cloudConfiguration/distributedConfigurationConsul.adoc +++ b/src/main/docs/guide/cloud/cloudConfiguration/distributedConfigurationConsul.adoc @@ -1,4 +1,4 @@ -https://www.consul.io[Consul] is a popular Service Discovery and Distributed Configuration server provided by HashiCorp. Micronaut features a native link:{micronautdiscoveryapi}/io/micronaut/discovery/consul/client/v1/ConsulClient.html[ConsulClient] that uses Micronaut's support for <>. +https://www.consul.io[Consul] is a popular Service Discovery and Distributed Configuration server provided by HashiCorp. The Micronaut framework features a native link:{micronautdiscoveryapi}/io/micronaut/discovery/consul/client/v1/ConsulClient.html[ConsulClient] that uses Micronaut's support for <>. == Starting Consul @@ -41,7 +41,7 @@ After enabling distributed configuration, store the configuration to share in Co == Storing Configuration as Key/Value Pairs -One way is to store the keys and values directly in Consul. In this case by default Micronaut looks for configuration in the Consul `/config` directory. +One way is to store the keys and values directly in Consul. In this case by default the Micronaut framework looks for configuration in the Consul `/config` directory. TIP: You can alter the path searched for by setting `consul.client.config.path` @@ -79,7 +79,7 @@ If you now define a `@Value("${foo.bar}")` or call `environment.getProperty(..)` == Storing Configuration in YAML, JSON etc. -Some Consul users prefer storing configuration in blobs of a certain format, such as YAML. Micronaut supports this mode and supports storing configuration in either YAML, JSON, or Java properties format. +Some Consul users prefer storing configuration in blobs of a certain format, such as YAML. The Micronaut framework supports this mode and supports storing configuration in either YAML, JSON, or Java properties format. TIP: The api:discovery.config.ConfigDiscoveryConfiguration[] has a number of configuration options for configuring how distributed configuration is discovered. diff --git a/src/main/docs/guide/cloud/cloudConfiguration/distributedConfigurationSpringCloud.adoc b/src/main/docs/guide/cloud/cloudConfiguration/distributedConfigurationSpringCloud.adoc index 5cccfd2de9d..229eef0696b 100644 --- a/src/main/docs/guide/cloud/cloudConfiguration/distributedConfigurationSpringCloud.adoc +++ b/src/main/docs/guide/cloud/cloudConfiguration/distributedConfigurationSpringCloud.adoc @@ -1,4 +1,4 @@ -Since 1.1, Micronaut features a native https://spring.io/projects/spring-cloud-config[Spring Cloud Configuration] for those who have not switched to a dedicated more complete solution like Consul. +Since 1.1, the Micronaut framework features a native https://spring.io/projects/spring-cloud-config[Spring Cloud Configuration] for those who have not switched to a dedicated more complete solution like Consul. To enable distributed configuration make sure <> is enabled and create a `src/main/resources/bootstrap.[yml/toml/properties]` file with the following configuration: @@ -22,6 +22,6 @@ spring: - `retry-attempts` is optional, and specifies the number of times to retry - `retry-delay` is optional, and specifies the delay between retries -Micronaut uses the configured `micronaut.application.name` to look up property sources for the application from Spring Cloud config server configured via `spring.cloud.config.uri`. +The Micronaut framework uses the configured `micronaut.application.name` to look up property sources for the application from Spring Cloud config server configured via `spring.cloud.config.uri`. See the https://spring.io/projects/spring-cloud-config#learn[Documentation for Spring Cloud Config Server] for more information on how to set up the server. diff --git a/src/main/docs/guide/cloud/cloudConfiguration/distributedConfigurationVault.adoc b/src/main/docs/guide/cloud/cloudConfiguration/distributedConfigurationVault.adoc index b4d8f215164..0d5f28ee804 100644 --- a/src/main/docs/guide/cloud/cloudConfiguration/distributedConfigurationVault.adoc +++ b/src/main/docs/guide/cloud/cloudConfiguration/distributedConfigurationVault.adoc @@ -1,4 +1,4 @@ -Micronaut integrates with https://www.vaultproject.io/[HashiCorp Vault] as a distributed configuration source. +The Micronaut framework integrates with https://www.vaultproject.io/[HashiCorp Vault] as a distributed configuration source. To enable distributed configuration make sure <> is enabled and create a `src/main/resources/bootstrap.[yml/toml/properties]` file with the following configuration: @@ -19,7 +19,7 @@ vault: See the https://micronaut-projects.github.io/micronaut-discovery-client/latest/guide/configurationreference.html#io.micronaut.discovery.vault.config.VaultClientConfiguration[configuration reference] for all configuration options. -Micronaut uses the configured `micronaut.application.name` to lookup property sources for the application from Vault. +The Micronaut framework uses the configured `micronaut.application.name` to lookup property sources for the application from Vault. .Configuration Resolution Precedence |=== diff --git a/src/main/docs/guide/cloud/serviceDiscovery.adoc b/src/main/docs/guide/cloud/serviceDiscovery.adoc index 0e50c6e2ec7..3506e2665ac 100644 --- a/src/main/docs/guide/cloud/serviceDiscovery.adoc +++ b/src/main/docs/guide/cloud/serviceDiscovery.adoc @@ -1,5 +1,5 @@ Service Discovery enables Microservices to find each other without knowing the physical location or IP address of associated services. -Micronaut integrates with multiple tools and libraries. See +The Micronaut framework integrates with multiple tools and libraries. See https://micronaut-projects.github.io/micronaut-discovery-client/latest/guide/[Micronaut Service Discovery documentation] for more details. diff --git a/src/main/docs/guide/cloud/serviceDiscovery/serviceDiscoveryKubernetes.adoc b/src/main/docs/guide/cloud/serviceDiscovery/serviceDiscoveryKubernetes.adoc index ec285c8c83d..be9159e029d 100644 --- a/src/main/docs/guide/cloud/serviceDiscovery/serviceDiscoveryKubernetes.adoc +++ b/src/main/docs/guide/cloud/serviceDiscovery/serviceDiscoveryKubernetes.adoc @@ -1,3 +1,3 @@ Kubernetes is a container runtime with many features including integrated Service Discovery and Distributed Configuration. -Micronaut includes first-class integration with Kubernetes. See the https://micronaut-projects.github.io/micronaut-kubernetes/latest/guide/index.html[Micronaut Kubernetes documentation] for more details. +The Micronaut framework includes first-class integration with Kubernetes. See the https://micronaut-projects.github.io/micronaut-kubernetes/latest/guide/index.html[Micronaut Kubernetes documentation] for more details. diff --git a/src/main/docs/guide/cloud/serviceDiscovery/serviceDiscoveryManual.adoc b/src/main/docs/guide/cloud/serviceDiscovery/serviceDiscoveryManual.adoc index d9f16e610d8..f8270e385be 100644 --- a/src/main/docs/guide/cloud/serviceDiscovery/serviceDiscoveryManual.adoc +++ b/src/main/docs/guide/cloud/serviceDiscovery/serviceDiscoveryManual.adoc @@ -38,4 +38,4 @@ micronaut: - `health-check-interval` is the interval between checks - `health-check-uri` specifies the endpoint URI of the health check request -Micronaut starts a background thread to check the health status of the service and if any of the configured services respond with an error code, they are removed from the list of available services. +The Micronaut framework starts a background thread to check the health status of the service and if any of the configured services respond with an error code, they are removed from the list of available services. diff --git a/src/main/docs/guide/cloud/serviceDiscovery/serviceDiscoveryRoute53.adoc b/src/main/docs/guide/cloud/serviceDiscovery/serviceDiscoveryRoute53.adoc index 44c72040c42..8bad7c10cd3 100644 --- a/src/main/docs/guide/cloud/serviceDiscovery/serviceDiscoveryRoute53.adoc +++ b/src/main/docs/guide/cloud/serviceDiscovery/serviceDiscoveryRoute53.adoc @@ -106,7 +106,7 @@ Type can be 'HTTP','HTTPS', or 'TCP'. You can only use a standard health check o For a custom health check, you only need to specify `--health-check-custom-config FailureThreshold=integer` which works on private namespaces as well. -This is also good because Micronaut sends out pulsation commands to let AWS know the instance is still healthy. +This is also good because the Micronaut framework sends out pulsation commands to let AWS know the instance is still healthy. For more help run 'aws discoveryservice create-service help'. diff --git a/src/main/docs/guide/config.adoc b/src/main/docs/guide/config.adoc index e413c31e556..cb7c3ebf907 100644 --- a/src/main/docs/guide/config.adoc +++ b/src/main/docs/guide/config.adoc @@ -25,6 +25,6 @@ Configuration can by default be provided in Java properties files or https://www |=== -In addition, Micronaut allows overriding any property via system properties or environment variables. +In addition, Micronaut framework allows overriding any property via system properties or environment variables. Each source of configuration is modeled with the link:{api}/io/micronaut/context/env/PropertySource.html[PropertySource] interface and the mechanism is extensible, allowing the implementation of additional link:{api}/io/micronaut/context/env/PropertySourceLoader.html[PropertySourceLoader] implementations. diff --git a/src/main/docs/guide/config/configurationProperties.adoc b/src/main/docs/guide/config/configurationProperties.adoc index 5d0e0cca9ac..b71644be7e9 100644 --- a/src/main/docs/guide/config/configurationProperties.adoc +++ b/src/main/docs/guide/config/configurationProperties.adoc @@ -1,7 +1,7 @@ You can create type-safe configuration by creating classes that are annotated with ann:io.micronaut.context.annotation.ConfigurationProperties[]. -Micronaut will produce a reflection-free `@ConfigurationProperties` bean and will also at compile time calculate the property paths to evaluate, greatly improving the speed and efficiency of loading `@ConfigurationProperties`. +The Micronaut framework will produce a reflection-free `@ConfigurationProperties` bean and will also at compile time calculate the property paths to evaluate, greatly improving the speed and efficiency of loading `@ConfigurationProperties`. For example: @@ -41,7 +41,7 @@ The names supplied to the includes/excludes list must be the "property" name. Fo [[configurationPropertiesAccessorsStyle]] == Change accessors style -Since 3.3, Micronaut supports defining different accessors prefixes for getters and setter other than the default `get` and `set` defined for Java Beans. Annotate your POJO or `@ConfigurationProperties` class with the link:{api}/io/micronaut/core/annotation/AccessorsStyle.html[@AccessorsStyle] annotation. +Since 3.3, the Micronaut framework supports defining different accessors prefixes for getters and setter other than the default `get` and `set` defined for Java Beans. Annotate your POJO or `@ConfigurationProperties` class with the link:{api}/io/micronaut/core/annotation/AccessorsStyle.html[@AccessorsStyle] annotation. This is useful when you write the getters and setters in a fluent way. For example: @@ -81,16 +81,16 @@ public class EngineConfig { } ---- -<1> Micronaut will use an empty prefix for getters and setters. +<1> The Micronaut framework will use an empty prefix for getters and setters. <2> Define the getters and setters with an empty prefix. Now you can inject `EngineConfig` and use it with `engineConfig.manufacturer()` and `engineConfig.cylinders()` to retrieve the values from configuration. == Property Type Conversion -Micronaut uses the api:io.micronaut.core.convert.ConversionService[] bean to convert values when resolving properties. You can register additional converters for types not supported by Micronaut by defining beans that implement the api:io.micronaut.core.convert.TypeConverter[] interface. +The Micronaut framework uses the api:io.micronaut.core.convert.ConversionService[] bean to convert values when resolving properties. You can register additional converters for types not supported by Micronaut by defining beans that implement the api:io.micronaut.core.convert.TypeConverter[] interface. -Micronaut features some built-in conversions that are useful, which are detailed below. +The Micronaut framework features some built-in conversions that are useful, which are detailed below. === Duration Conversion @@ -196,7 +196,7 @@ You can use the link:{api}/io/micronaut/context/annotation/ConfigurationBuilder. Since there is no consistent way to define builders in the Java world, one or more method prefixes can be specified in the annotation to support builder methods like `withXxx` or `setXxx`. If the builder methods have no prefix, assign an empty string to the parameter. -A configuration prefix can also be specified to tell Micronaut where to look for configuration values. By default, builder methods use the configuration prefix specified in a class-level link:{api}/io/micronaut/context/annotation/ConfigurationProperties.html[@ConfigurationProperties] annotation. +A configuration prefix can also be specified to tell the Micronaut framework where to look for configuration values. By default, builder methods use the configuration prefix specified in a class-level link:{api}/io/micronaut/context/annotation/ConfigurationProperties.html[@ConfigurationProperties] annotation. For example: diff --git a/src/main/docs/guide/config/customTypeConverter.adoc b/src/main/docs/guide/config/customTypeConverter.adoc index 076ffd96b15..64ce5d5798b 100644 --- a/src/main/docs/guide/config/customTypeConverter.adoc +++ b/src/main/docs/guide/config/customTypeConverter.adoc @@ -1,4 +1,4 @@ -Micronaut includes an extensible type conversion mechanism. To add additional type converters you register beans of type api:core.convert.TypeConverter[]. +The Micronaut framework includes an extensible type conversion mechanism. To add additional type converters you register beans of type api:core.convert.TypeConverter[]. The following example shows how to use one of the built-in converters (Map to an Object) or create your own. diff --git a/src/main/docs/guide/config/eachProperty.adoc b/src/main/docs/guide/config/eachProperty.adoc index 23d71b373b1..d92f97942f6 100644 --- a/src/main/docs/guide/config/eachProperty.adoc +++ b/src/main/docs/guide/config/eachProperty.adoc @@ -21,7 +21,7 @@ snippet::io.micronaut.docs.config.env.EachPropertyTest[tags="beans", indent=0, t === List-Based Binding -The default behavior of ann:context.annotation.EachProperty[] is to bind from a map style of configuration, where the key is the named qualifier of the bean and the value is the data to bind from. For cases where map style configuration doesn't make sense, it is possible to inform Micronaut that the class is bound from a list. Simply set the `list` member on the annotation to true. +The default behavior of ann:context.annotation.EachProperty[] is to bind from a map style of configuration, where the key is the named qualifier of the bean and the value is the data to bind from. For cases where map style configuration doesn't make sense, it is possible to inform the Micronaut framework that the class is bound from a list. Simply set the `list` member on the annotation to true. snippet::io.micronaut.docs.config.env.RateLimitsConfiguration[tags="clazz", indent=0, title="@EachProperty List Example"] diff --git a/src/main/docs/guide/config/environments.adoc b/src/main/docs/guide/config/environments.adoc index 4544900f17b..d94de405e82 100644 --- a/src/main/docs/guide/config/environments.adoc +++ b/src/main/docs/guide/config/environments.adoc @@ -4,7 +4,7 @@ snippet::io.micronaut.docs.context.env.EnvironmentSpec[tags="env",indent=0,title The active environment names allow loading different configuration files depending on the environment, and also using the ann:context.annotation.Requires[] annotation to conditionally load beans or bean ann:context.annotation.Configuration[] packages. -In addition, Micronaut attempts to detect the current environments. For example within a Spock or JUnit test the api:context.env.Environment#TEST[] environment is automatically active. +In addition, the Micronaut framework attempts to detect the current environments. For example within a Spock or JUnit test the api:context.env.Environment#TEST[] environment is automatically active. Additional active environments can be specified using the `micronaut.environments` system property or the `MICRONAUT_ENVIRONMENTS` environment variable. These are specified as a comma-separated list. For example: @@ -16,13 +16,13 @@ $ java -Dmicronaut.environments=foo,bar -jar myapp.jar The above activates environments called `foo` and `bar`. -It is also possible to enable the detection of the Cloud environment the application is deployed to (this feature is disabled by default since Micronaut 4). See the section on <> for more information. +It is also possible to enable the detection of the Cloud environment the application is deployed to (this feature is disabled by default since Micronaut framework 4). See the section on <> for more information. == Environment Priority -Micronaut loads property sources based on the environments specified, and if the same property key exists in multiple property sources specific to an environment, the environment order determines which value to use. +The Micronaut framework loads property sources based on the environments specified, and if the same property key exists in multiple property sources specific to an environment, the environment order determines which value to use. -Micronaut uses the following hierarchy for environment processing (lowest to highest priority): +The Micronaut framework uses the following hierarchy for environment processing (lowest to highest priority): * Deduced environments * Environments from the `micronaut.environments` system property @@ -35,7 +35,7 @@ NOTE: This also applies to `@MicronautTest(environments = ...)` == Disabling Environment Detection -Automatic detection of environments can be disabled by setting the `micronaut.env.deduction` system property or the `MICRONAUT_ENV_DEDUCTION` environment variable to `false`. This prevents Micronaut from detecting current environments, while still using any environments that are specifically provided as shown above. +Automatic detection of environments can be disabled by setting the `micronaut.env.deduction` system property or the `MICRONAUT_ENV_DEDUCTION` environment variable to `false`. This prevents the Micronaut framework from detecting current environments, while still using any environments that are specifically provided as shown above. .Disabling environment detection via system property [source,bash] @@ -49,7 +49,7 @@ snippet::io.micronaut.docs.context.env.DefaultEnvironmentSpec[tags="disableEnvDe == Default Environment -Micronaut supports the concept of one or many default environments. +The Micronaut framework supports the concept of one or many default environments. A default environment is one that is only applied if no other environments are explicitly specified or deduced. Environments can be explicitly specified either through the application context builder `Micronaut.build().environments(...)`, through the `micronaut.environments` system property, or the `MICRONAUT_ENVIRONMENTS` environment variable. Environments can be deduced to automatically apply the environment appropriate for cloud deployments. @@ -79,7 +79,7 @@ NOTE: Previously, we recommended using `Micronaut.defaultEnvironments("dev")` ho == Micronaut Banner -Since Micronaut 2.3 a banner is shown when the application starts. It is enabled by default and it also shows the Micronaut version. +Since Micronaut framework 2.3 a banner is shown when the application starts. It is enabled by default and it also shows the Micronaut version. [source,shell,subs="attributes"] ---- diff --git a/src/main/docs/guide/config/immutableConfig.adoc b/src/main/docs/guide/config/immutableConfig.adoc index 2da35216398..5cfdd951702 100644 --- a/src/main/docs/guide/config/immutableConfig.adoc +++ b/src/main/docs/guide/config/immutableConfig.adoc @@ -1,4 +1,4 @@ -Since 1.3, Micronaut supports the definition of immutable configuration. +Since 1.3, Micronaut framework supports the definition of immutable configuration. There are two ways to define immutable configuration. The preferred way is to define an interface annotated with ann:context.annotation.ConfigurationProperties[]. For example: @@ -11,7 +11,7 @@ snippet::io.micronaut.docs.config.itfce.EngineConfig[tags="imports,class",indent <5> You can nest immutable configuration <6> Optional configuration can be indicated by returning an `Optional` or specifying `@Nullable` -In this case Micronaut provides a compile-time implementation that delegates all getters to call the `getProperty(..)` method of the api:context.env.Environment[] interface. +In this case the Micronaut framework provides a compile-time implementation that delegates all getters to call the `getProperty(..)` method of the api:context.env.Environment[] interface. This has the advantage that if the application configuration is <> (for example by invoking the `/refresh` endpoint), the injected interface automatically sees the new values. @@ -30,11 +30,11 @@ snippet::io.micronaut.docs.config.immutable.EngineConfig[tags="imports,class",in <5> You can nest immutable configuration <6> Optional configuration can be indicated with `@Nullable` -The ann:context.annotation.ConfigurationInject[] annotation provides a hint to Micronaut to prioritize binding values from configuration instead of injecting beans. +The ann:context.annotation.ConfigurationInject[] annotation provides a hint to the Micronaut framework to prioritize binding values from configuration instead of injecting beans. NOTE: Using this approach, to make the configuration refreshable, add the ann:runtime.context.scope.Refreshable[] annotation to the class as well. This allows the bean to be re-created in the case of a <>. -There are a few exceptions to this rule. Micronaut will not perform configuration binding for a parameter if any of these conditions is met: +There are a few exceptions to this rule. Micronaut framework will not perform configuration binding for a parameter if any of these conditions is met: * The parameter is annotated with `@Value` (explicit binding) * The parameter is annotated with `@Property` (explicit binding) diff --git a/src/main/docs/guide/config/jmx.adoc b/src/main/docs/guide/config/jmx.adoc index 64aa12d271d..e924d0f1593 100644 --- a/src/main/docs/guide/config/jmx.adoc +++ b/src/main/docs/guide/config/jmx.adoc @@ -1,3 +1,3 @@ -Micronaut provides basic support for JMX. +Micronaut framework provides basic support for JMX. For more information, see the https://micronaut-projects.github.io/micronaut-jmx/latest/guide/[documentation] for the micronaut-jmx project. diff --git a/src/main/docs/guide/config/propertySource.adoc b/src/main/docs/guide/config/propertySource.adoc index 51e479c724d..ca339747f5f 100644 --- a/src/main/docs/guide/config/propertySource.adoc +++ b/src/main/docs/guide/config/propertySource.adoc @@ -8,7 +8,7 @@ Alternatively one can register a link:{api}/io/micronaut/context/env/PropertySou === Included PropertySource Loaders -Micronaut by default contains `PropertySourceLoader` implementations that load properties from the given locations and priority: +Micronaut framework by default contains `PropertySourceLoader` implementations that load properties from the given locations and priority: . Command line arguments . Properties from `SPRING_APPLICATION_JSON` (for Spring compatibility) @@ -73,7 +73,7 @@ datasources: driverClassName: ${JDBC_DRIVER:com.mysql.cj.jdbc.Driver} ---- -To securely externalize configuration consider using a secrets manager system supported by Micronaut such as: +To securely externalize configuration consider using a secrets manager system supported by the Micronaut framework such as: * https://micronaut-projects.github.io/micronaut-aws/latest/guide/#secretsmanager[AWS Secrets Manager]. * https://micronaut-projects.github.io/micronaut-gcp/latest/guide/#secretManager[Google Cloud Secrets Manager]. @@ -83,7 +83,7 @@ To securely externalize configuration consider using a secrets manager system su === Property Value Placeholders -As mentioned in the previous section, Micronaut includes a property placeholder syntax to reference configuration properties both within configuration values and with any Micronaut annotation. See ann:io.micronaut.context.annotation.Value[] and the section on <>. +As mentioned in the previous section, the Micronaut framework includes a property placeholder syntax to reference configuration properties both within configuration values and with any Micronaut annotation. See ann:io.micronaut.context.annotation.Value[] and the section on <>. TIP: Programmatic usage is also possible via the api:io.micronaut.context.env.PropertyPlaceholderResolver[] interface. @@ -120,7 +120,7 @@ The above example looks for a `server.address` property and defaults to `http:// Note that these property references should be in kebab case (lowercase and hyphen-separated) when placing references in code or in placeholder values. For example, use `micronaut.server.default-charset` and not `micronaut.server.defaultCharset`. -Micronaut still allows specifying the latter in configuration, but normalizes the properties into kebab case form to optimize memory consumption and reduce complexity when resolving properties. The following table summarizes how properties are normalized from different sources: +The Micronaut framework still allows specifying the latter in configuration, but normalizes the properties into kebab case form to optimize memory consumption and reduce complexity when resolving properties. The following table summarizes how properties are normalized from different sources: .Property Value Normalization |=== diff --git a/src/main/docs/guide/config/valueAnnotation.adoc b/src/main/docs/guide/config/valueAnnotation.adoc index 14ff27ba646..a5978afd9c3 100644 --- a/src/main/docs/guide/config/valueAnnotation.adoc +++ b/src/main/docs/guide/config/valueAnnotation.adoc @@ -6,7 +6,7 @@ Consider the following example: snippet::io.micronaut.docs.config.value.EngineImpl[tags="imports,class",indent=0,title="@Value Example"] -<1> The `@Value` annotation accepts a string that can have embedded placeholder values (the default value can be provided by specifying a value after the colon `:` character). Also try to avoid setting the member visibility to `private`, since this requires Micronaut Framework to use reflection. Prefer to use `protected`. +<1> The `@Value` annotation accepts a string that can have embedded placeholder values (the default value can be provided by specifying a value after the colon `:` character). Also try to avoid setting the member visibility to `private`, since this requires the Micronaut Framework to use reflection. Prefer to use `protected`. <2> The injected value can then be used within code. Note that `@Value` can also be used to inject a static value. For example the following injects the number 10: diff --git a/src/main/docs/guide/configurations.adoc b/src/main/docs/guide/configurations.adoc index 66f4498dad9..e1021983277 100644 --- a/src/main/docs/guide/configurations.adoc +++ b/src/main/docs/guide/configurations.adoc @@ -1,2 +1,2 @@ -Micronaut features several built-in configurations that enable integration with common databases and other servers. +Micronaut framework features several built-in configurations that enable integration with common databases and other servers. diff --git a/src/main/docs/guide/configurations/dataAccess.adoc b/src/main/docs/guide/configurations/dataAccess.adoc index d3757d2b78c..02db4c6a0ee 100644 --- a/src/main/docs/guide/configurations/dataAccess.adoc +++ b/src/main/docs/guide/configurations/dataAccess.adoc @@ -53,7 +53,7 @@ For example, to add support for MongoDB, add the following dependency: compile "io.micronaut.mongodb:micronaut-mongo-reactive" ---- -For Groovy users, Micronaut provides special support for https://gorm.grails.org[GORM]. +For Groovy users, the Micronaut framework provides special support for https://gorm.grails.org[GORM]. WARNING: With GORM for Hibernate you cannot have both the `hibernate-jpa` and `hibernate-gorm` dependencies. diff --git a/src/main/docs/guide/configurations/dataAccess/flywaySupport.adoc b/src/main/docs/guide/configurations/dataAccess/flywaySupport.adoc index 46df4ab8afd..5142bc03aba 100644 --- a/src/main/docs/guide/configurations/dataAccess/flywaySupport.adoc +++ b/src/main/docs/guide/configurations/dataAccess/flywaySupport.adoc @@ -1,2 +1,2 @@ -To configure Micronaut integration with https://flywaydb.org/[Flyway], please follow +To configure the Micronaut integration with https://flywaydb.org/[Flyway], please follow https://micronaut-projects.github.io/micronaut-flyway/latest/guide/index.html[these instructions] diff --git a/src/main/docs/guide/configurations/dataAccess/hibernateSupport.adoc b/src/main/docs/guide/configurations/dataAccess/hibernateSupport.adoc index e3180da0475..ec2fcf1a2b9 100644 --- a/src/main/docs/guide/configurations/dataAccess/hibernateSupport.adoc +++ b/src/main/docs/guide/configurations/dataAccess/hibernateSupport.adoc @@ -9,7 +9,7 @@ $ mn create-app my-app --features hibernate-jpa ---- ==== -Micronaut includes support for configuring a https://hibernate.org[Hibernate] / JPA `EntityManager` that builds on the <>. +The Micronaut framework includes support for configuring a https://hibernate.org[Hibernate] / JPA `EntityManager` that builds on the <>. Once you have <> to use Hibernate, add the `hibernate-jpa` dependency to your build: @@ -30,4 +30,4 @@ $ mn create-app my-app --features hibernate-gorm ---- ==== -See the https://micronaut-projects.github.io/micronaut-groovy/latest/guide/#gorm[GORM Modules] section of the https://github.com/micronaut-projects/micronaut-groovy[Micronaut for Groovy ] user guide. +See the https://micronaut-projects.github.io/micronaut-groovy/latest/guide/#gorm[GORM Modules] section of the https://github.com/micronaut-projects/micronaut-groovy[Micronaut Groovy user guide]. diff --git a/src/main/docs/guide/configurations/dataAccess/liquibaseSupport.adoc b/src/main/docs/guide/configurations/dataAccess/liquibaseSupport.adoc index 6ced9b5937a..aa1f40282db 100644 --- a/src/main/docs/guide/configurations/dataAccess/liquibaseSupport.adoc +++ b/src/main/docs/guide/configurations/dataAccess/liquibaseSupport.adoc @@ -1,2 +1,2 @@ -To configure Micronaut integration with https://www.liquibase.org[Liquibase], please follow +To configure the Micronaut integration with https://www.liquibase.org[Liquibase], please follow https://micronaut-projects.github.io/micronaut-liquibase/latest/guide/index.html[these instructions]. diff --git a/src/main/docs/guide/configurations/dataAccess/mongoSupport.adoc b/src/main/docs/guide/configurations/dataAccess/mongoSupport.adoc index 00e9e6500ad..64dcb1c708a 100644 --- a/src/main/docs/guide/configurations/dataAccess/mongoSupport.adoc +++ b/src/main/docs/guide/configurations/dataAccess/mongoSupport.adoc @@ -9,7 +9,7 @@ $ mn create-app my-app --features mongo-reactive ---- ==== -Micronaut can automatically configure the native MongoDB Java driver. To use this, add the following dependency to your build: +The Micronaut framework can automatically configure the native MongoDB Java driver. To use this, add the following dependency to your build: dependency:micronaut-mongo-reactive[groupId="io.micronaut.mongodb"] diff --git a/src/main/docs/guide/configurations/dataAccess/mysqlSupport.adoc b/src/main/docs/guide/configurations/dataAccess/mysqlSupport.adoc index 41a567484a8..ec687f48c89 100644 --- a/src/main/docs/guide/configurations/dataAccess/mysqlSupport.adoc +++ b/src/main/docs/guide/configurations/dataAccess/mysqlSupport.adoc @@ -1,4 +1,4 @@ -Micronaut supports reactive and non-blocking client to connect to MySQL using https://github.com/eclipse-vertx/vertx-sql-client/tree/master/vertx-mysql-client[vertx-mysql-client], allowing to handle many database connections with a single thread. +The Micronaut framework supports reactive and non-blocking client to connect to MySQL using https://github.com/eclipse-vertx/vertx-sql-client/tree/master/vertx-mysql-client[vertx-mysql-client], allowing to handle many database connections with a single thread. == Configuring the Reactive MySQL Client diff --git a/src/main/docs/guide/configurations/dataAccess/neo4jSupport.adoc b/src/main/docs/guide/configurations/dataAccess/neo4jSupport.adoc index e1bc1e3ce7f..beae36049d3 100644 --- a/src/main/docs/guide/configurations/dataAccess/neo4jSupport.adoc +++ b/src/main/docs/guide/configurations/dataAccess/neo4jSupport.adoc @@ -1,4 +1,4 @@ -Micronaut features dedicated support for automatically configuring the https://neo4j.com/docs/developer-manual/current/drivers/[Neo4j Bolt Driver] for the popular https://neo4j.com/[Neo4j] Graph Database. +The Micronaut Framework features dedicated support for automatically configuring the https://neo4j.com/docs/developer-manual/current/drivers/[Neo4j Bolt Driver] for the popular https://neo4j.com/[Neo4j] Graph Database. [TIP] .Using the CLI diff --git a/src/main/docs/guide/configurations/dataAccess/postgresSupport.adoc b/src/main/docs/guide/configurations/dataAccess/postgresSupport.adoc index 2177c3c31d1..49d3357c59d 100644 --- a/src/main/docs/guide/configurations/dataAccess/postgresSupport.adoc +++ b/src/main/docs/guide/configurations/dataAccess/postgresSupport.adoc @@ -1,4 +1,4 @@ -Micronaut supports a reactive and non-blocking client to connect to Postgres using https://github.com/eclipse-vertx/vertx-sql-client/tree/master/vertx-pg-client[vertx-pg-client], which can handle many database connections with a single thread. +The Micronaut framework supports a reactive and non-blocking client to connect to Postgres using https://github.com/eclipse-vertx/vertx-sql-client/tree/master/vertx-pg-client[vertx-pg-client], which can handle many database connections with a single thread. == Configuring the Reactive Postgres Client diff --git a/src/main/docs/guide/configurations/dataAccess/redisSupport.adoc b/src/main/docs/guide/configurations/dataAccess/redisSupport.adoc index 1e2f9cd92a0..a190ba1ed42 100644 --- a/src/main/docs/guide/configurations/dataAccess/redisSupport.adoc +++ b/src/main/docs/guide/configurations/dataAccess/redisSupport.adoc @@ -1,4 +1,4 @@ -Micronaut features automatic configuration of the https://lettuce.io[Lettuce] driver for https://redis.io[Redis] via the `redis-lettuce` module. +The Micronaut framework features automatic configuration of the https://lettuce.io[Lettuce] driver for https://redis.io[Redis] via the `redis-lettuce` module. == Configuring Lettuce [TIP] diff --git a/src/main/docs/guide/httpClient.adoc b/src/main/docs/guide/httpClient.adoc index 7ad8aa99148..55eabc3e3a9 100644 --- a/src/main/docs/guide/httpClient.adoc +++ b/src/main/docs/guide/httpClient.adoc @@ -1,3 +1,3 @@ -Client communication between Microservices is a critical component of any Microservice architecture. With that in mind, Micronaut includes an HTTP client that has both a low-level API and a higher-level AOP-driven API. +Client communication between Microservices is a critical component of any Microservice architecture. With that in mind, Micronaut framework includes an HTTP client that has both a low-level API and a higher-level AOP-driven API. TIP: Regardless whether you choose to use Micronaut's HTTP server, you may wish to use the Micronaut HTTP client in your application since it is a feature-rich client implementation. diff --git a/src/main/docs/guide/httpClient/clientAnnotation.adoc b/src/main/docs/guide/httpClient/clientAnnotation.adoc index efe93f829f5..fb91badec4d 100644 --- a/src/main/docs/guide/httpClient/clientAnnotation.adoc +++ b/src/main/docs/guide/httpClient/clientAnnotation.adoc @@ -18,7 +18,7 @@ Additionally, to use the `jakarta.validation` features, add the `validation` mod dependency:micronaut-validation[] -On the server-side of Micronaut you can implement the `PetOperations` interface: +On the server-side of the Micronaut framework you can implement the `PetOperations` interface: snippet::io.micronaut.docs.annotation.PetController[tags="imports, class", indent=0, title="PetController.java"] diff --git a/src/main/docs/guide/httpClient/clientAnnotation/clientAnnotationStreaming.adoc b/src/main/docs/guide/httpClient/clientAnnotation/clientAnnotationStreaming.adoc index 865ebe9a8f1..755e35dd5bd 100644 --- a/src/main/docs/guide/httpClient/clientAnnotation/clientAnnotationStreaming.adoc +++ b/src/main/docs/guide/httpClient/clientAnnotation/clientAnnotationStreaming.adoc @@ -64,7 +64,7 @@ The above example sets the `readIdleTimeout` to ten minutes. === Streaming Server Sent Events -Micronaut features a native client for Server Sent Events (SSE) defined by the interface api:http.client.sse.SseClient[]. +The Micronaut framework features a native client for Server Sent Events (SSE) defined by the interface api:http.client.sse.SseClient[]. You can use this client to stream SSE events from any server that emits them. diff --git a/src/main/docs/guide/httpClient/clientAnnotation/clientFallback.adoc b/src/main/docs/guide/httpClient/clientAnnotation/clientFallback.adoc index 5de416b1961..68987b828b8 100644 --- a/src/main/docs/guide/httpClient/clientAnnotation/clientFallback.adoc +++ b/src/main/docs/guide/httpClient/clientAnnotation/clientFallback.adoc @@ -2,7 +2,7 @@ In distributed systems, failure happens and it is best to be prepared for it and In addition, when developing Microservices it is quite common to work on a single Microservice without other Microservices the project requires being available. -With that in mind, Micronaut features a native fallback mechanism that is integrated into <> that allows falling back to another implementation in the case of failure. +With that in mind, the Micronaut framework features a native fallback mechanism that is integrated into <> that allows falling back to another implementation in the case of failure. Using the ann:retry.annotation.Fallback[] annotation, you can declare a fallback implementation of a client to be used when all possible retries have been exhausted. diff --git a/src/main/docs/guide/httpClient/clientAnnotation/netflixHystrix.adoc b/src/main/docs/guide/httpClient/clientAnnotation/netflixHystrix.adoc index ebe3589a278..37291bac267 100644 --- a/src/main/docs/guide/httpClient/clientAnnotation/netflixHystrix.adoc +++ b/src/main/docs/guide/httpClient/clientAnnotation/netflixHystrix.adoc @@ -9,7 +9,7 @@ $ mn create-app my-app --features netflix-hystrix https://github.com/Netflix/Hystrix[Netflix Hystrix] is a fault tolerance library developed by the Netflix team and is designed to improve resilience of interprocess communication. -Micronaut integrates with Hystrix through the `netflix-hystrix` module, which you can add to your build: +The Micronaut framework integrates with Hystrix through the `netflix-hystrix` module, which you can add to your build: dependency:io.micronaut.netflix:micronaut-netflix-hystrix[] diff --git a/src/main/docs/guide/httpClient/clientFilter.adoc b/src/main/docs/guide/httpClient/clientFilter.adoc index 9506f88fd66..662a05a19a2 100644 --- a/src/main/docs/guide/httpClient/clientFilter.adoc +++ b/src/main/docs/guide/httpClient/clientFilter.adoc @@ -24,7 +24,7 @@ Now when you invoke the `bintrayService.fetchRepositories()` method, the `Author === Injecting Another Client into a ClientFilter -To create an link:{micronautreactorapi}/io/micronaut/reactor/http/client/ReactorHttpClient.html[ReactorHttpClient], Micronaut needs to resolve all `ClientFilter` beans, which creates a circular dependency when injecting another link:{micronautreactorapi}/io/micronaut/reactor/http/client/ReactorHttpClient.html[ReactorHttpClient] or a `@Client` bean into an instance of a `ClientFilter`. +To create an link:{micronautreactorapi}/io/micronaut/reactor/http/client/ReactorHttpClient.html[ReactorHttpClient], the Micronaut framework needs to resolve all `ClientFilter` beans, which creates a circular dependency when injecting another link:{micronautreactorapi}/io/micronaut/reactor/http/client/ReactorHttpClient.html[ReactorHttpClient] or a `@Client` bean into an instance of a `ClientFilter`. To resolve this issue, use the api:context.BeanProvider[] interface to inject another link:{micronautreactorapi}/io/micronaut/reactor/http/client/ReactorHttpClient.html[ReactorHttpClient] or a `@Client` bean into an instance of `ClientFilter`. diff --git a/src/main/docs/guide/httpClient/clientHttp3.adoc b/src/main/docs/guide/httpClient/clientHttp3.adoc index ba98f2e449b..3f76c4915e1 100644 --- a/src/main/docs/guide/httpClient/clientHttp3.adoc +++ b/src/main/docs/guide/httpClient/clientHttp3.adoc @@ -1,4 +1,4 @@ -Since Micronaut 4.x, Micronaut's Netty-based HTTP client can be configured to support HTTP/3. This support is experimental and may change without notice. +Since the Micronaut framework 4.x, Micronaut's Netty-based HTTP client can be configured to support HTTP/3. This support is experimental and may change without notice. Instead of the TCP used for HTTP/1.1 and HTTP/2, HTTP/3 runs on UDP. If the client is configured with the special `h3` value for the `alpn-modes` property, the client will automatically use HTTP/3 over UDP instead of HTTP/1.1 or HTTP/2 over TCP. At this time, the client cannot fall back to TCP if the server does not support HTTP/3. diff --git a/src/main/docs/guide/httpClient/lowLevelHttpClient/clientConfiguration.adoc b/src/main/docs/guide/httpClient/lowLevelHttpClient/clientConfiguration.adoc index 64ee258c9f8..02ba08acfb1 100644 --- a/src/main/docs/guide/httpClient/lowLevelHttpClient/clientConfiguration.adoc +++ b/src/main/docs/guide/httpClient/lowLevelHttpClient/clientConfiguration.adoc @@ -112,7 +112,7 @@ By setting the `pool.enabled` property to `false`, you can disable connection re === Configuring Event Loop Groups -By default, Micronaut shares a common Netty `EventLoopGroup` for worker threads and all HTTP client threads. +By default, the Micronaut framework shares a common Netty `EventLoopGroup` for worker threads and all HTTP client threads. This `EventLoopGroup` can be configured via the `micronaut.netty.event-loops.default` property: diff --git a/src/main/docs/guide/httpServer.adoc b/src/main/docs/guide/httpServer.adoc index c2ea7a75559..1c75662bb10 100644 --- a/src/main/docs/guide/httpServer.adoc +++ b/src/main/docs/guide/httpServer.adoc @@ -4,9 +4,9 @@ If you create your project using the Micronaut CLI `create-app` command, the `http-server` dependency is included by default. ==== -Micronaut includes both non-blocking HTTP server and client APIs based on https://netty.io[Netty]. +Micronaut framework includes both non-blocking HTTP server and client APIs based on https://netty.io[Netty]. -The design of the HTTP server in Micronaut is optimized for interchanging messages between Microservices, typically in JSON, and is not intended as a full server-side MVC framework. For example, there is currently no support for server-side views or features typical of a traditional server-side MVC framework. +The design of the HTTP server in the Micronaut framework is optimized for interchanging messages between Microservices, typically in JSON, and is not intended as a full server-side MVC framework. For example, there is currently no support for server-side views or features typical of a traditional server-side MVC framework. The goal of the HTTP server is to make it as easy as possible to expose APIs to be consumed by HTTP clients, regardless of the language they are written in. To use the HTTP server you need the `http-server-netty` dependency in your build: diff --git a/src/main/docs/guide/httpServer/apiVersioning.adoc b/src/main/docs/guide/httpServer/apiVersioning.adoc index b2ae1ce9520..6ba6c817cf3 100644 --- a/src/main/docs/guide/httpServer/apiVersioning.adoc +++ b/src/main/docs/guide/httpServer/apiVersioning.adoc @@ -1,4 +1,4 @@ -Since 1.1.x, Micronaut supports API versioning via a dedicated ann:core.version.annotation.Version[] annotation. +Since 1.1.x, the Micronaut framework supports API versioning via a dedicated ann:core.version.annotation.Version[] annotation. The following example demonstrates how to version an API: @@ -18,7 +18,7 @@ micronaut: enabled: true ---- -By default Micronaut has two strategies for resolving the version based on an HTTP header named `X-API-VERSION` or a request parameter named `api-version`, however this is configurable. A full configuration example can be seen below: +By default, the Micronaut framework has two strategies for resolving the version based on an HTTP header named `X-API-VERSION` or a request parameter named `api-version`, however this is configurable. A full configuration example can be seen below: .Configuring Versioning [configuration] diff --git a/src/main/docs/guide/httpServer/binding.adoc b/src/main/docs/guide/httpServer/binding.adoc index 11f6b608c18..09e37c97a78 100644 --- a/src/main/docs/guide/httpServer/binding.adoc +++ b/src/main/docs/guide/httpServer/binding.adoc @@ -1,4 +1,4 @@ -The examples in the previous section demonstrate how Micronaut lets you bind method parameters from URI path variables. This section shows how to bind arguments from other parts of the request. +The examples in the previous section demonstrate how the Micronaut framework lets you bind method parameters from URI path variables. This section shows how to bind arguments from other parts of the request. == Binding Annotations @@ -53,7 +53,7 @@ snippet::io.micronaut.docs.server.binding.BindingController[tags="header1,header == Stream Support -Micronaut also supports binding the body to an `InputStream`. If the method is reading the stream, the method execution must be offloaded to another thread pool to avoid blocking the event loop. +The Micronaut framework also supports binding the body to an `InputStream`. If the method is reading the stream, the method execution must be offloaded to another thread pool to avoid blocking the event loop. snippet::io.micronaut.docs.http.server.stream.StreamController[tags="read", indent=0, title="Performing Blocking I/O With InputStream"] @@ -125,7 +125,7 @@ Some parameters are recognized by their type instead of their annotation. The fo == Variable resolution -Micronaut tries to populate method arguments in the following order: +The Micronaut framework tries to populate method arguments in the following order: . URI variables like `/{id}`. . From query parameters if the request is a `GET` request (e.g. `?foo=bar`). diff --git a/src/main/docs/guide/httpServer/clientIpAddress.adoc b/src/main/docs/guide/httpServer/clientIpAddress.adoc index d9d966068f3..781cce43e6f 100644 --- a/src/main/docs/guide/httpServer/clientIpAddress.adoc +++ b/src/main/docs/guide/httpServer/clientIpAddress.adoc @@ -1,4 +1,4 @@ -You may need to resolve the originating IP address of an HTTP Request. Micronaut includes an implementation of api:http.server.util.HttpClientAddressResolver[]. +You may need to resolve the originating IP address of an HTTP Request. The Micronaut framework includes an implementation of api:http.server.util.HttpClientAddressResolver[]. The default implementation resolves the client address in the following places in order: diff --git a/src/main/docs/guide/httpServer/customArgumentBinding.adoc b/src/main/docs/guide/httpServer/customArgumentBinding.adoc index 7ce9fef4d30..8783740a636 100644 --- a/src/main/docs/guide/httpServer/customArgumentBinding.adoc +++ b/src/main/docs/guide/httpServer/customArgumentBinding.adoc @@ -1,9 +1,9 @@ -Micronaut uses an api:core.bind.ArgumentBinderRegistry[ArgumentBinderRegistry] to look up api:core.bind.ArgumentBinder[ArgumentBinder] beans +The Micronaut framework uses an api:core.bind.ArgumentBinderRegistry[ArgumentBinderRegistry] to look up api:core.bind.ArgumentBinder[ArgumentBinder] beans capable of binding to the arguments in controller methods. The default implementation looks for an annotation on the argument that is meta-annotated with ann:core.bind.annotation.Bindable[]. If one exists the argument binder registry searches for an argument binder that supports that annotation. -If no fitting annotation is found Micronaut tries to find an argument binder that supports the argument type. +If no fitting annotation is found, the Micronaut framework tries to find an argument binder that supports the argument type. -An argument binder returns a link:{api}/io/micronaut/core/bind/ArgumentBinder.BindingResult.html[ArgumentBinder.BindingResult]. The binding result gives Micronaut more information than just the value. Binding results are either satisfied or unsatisfied, and either empty or not empty. If an argument binder returns an unsatisfied result, the binder may be called again at different times in request processing. Argument binders are initially called before the body is read and before any filters are executed. If a binder relies on any of that data and it is not present, return a link:{api}/io/micronaut/core/bind/ArgumentBinder.BindingResult.html#UNSATISFIED[ArgumentBinder.BindingResult#UNSATISFIED] result. Returning an link:{api}/io/micronaut/core/bind/ArgumentBinder.BindingResult.html#EMPTY[ArgumentBinder.BindingResult#EMPTY] or satisfied result will be the final result and the binder will not be called again for that request. +An argument binder returns a link:{api}/io/micronaut/core/bind/ArgumentBinder.BindingResult.html[ArgumentBinder.BindingResult]. The binding result gives the Micronaut framework more information than just the value. Binding results are either satisfied or unsatisfied, and either empty or not empty. If an argument binder returns an unsatisfied result, the binder may be called again at different times in request processing. Argument binders are initially called before the body is read and before any filters are executed. If a binder relies on any of that data and it is not present, return a link:{api}/io/micronaut/core/bind/ArgumentBinder.BindingResult.html#UNSATISFIED[ArgumentBinder.BindingResult#UNSATISFIED] result. Returning an link:{api}/io/micronaut/core/bind/ArgumentBinder.BindingResult.html#EMPTY[ArgumentBinder.BindingResult#EMPTY] or satisfied result will be the final result and the binder will not be called again for that request. NOTE: At the end of processing if the result is still link:{api}/io/micronaut/core/bind/ArgumentBinder.BindingResult.html#UNSATISFIED[ArgumentBinder.BindingResult#UNSATISFIED], it is considered link:{api}/io/micronaut/core/bind/ArgumentBinder.BindingResult.html#EMPTY[ArgumentBinder.BindingResult#EMPTY]. diff --git a/src/main/docs/guide/httpServer/datavalidation.adoc b/src/main/docs/guide/httpServer/datavalidation.adoc index 432a1c0eab0..bbb644b9ed1 100644 --- a/src/main/docs/guide/httpServer/datavalidation.adoc +++ b/src/main/docs/guide/httpServer/datavalidation.adoc @@ -1,6 +1,6 @@ It is easy to validate incoming data with Micronaut controllers using <>. -Micronaut provides native support for the `jakarta.validation` annotations with the `micronaut-validation` dependency: +The Micronaut framework provides native support for the `jakarta.validation` annotations with the `micronaut-validation` dependency: dependency:micronaut-validation-processor[groupId="io.micronaut.validation",scope="annotationProcessor"] diff --git a/src/main/docs/guide/httpServer/errorHandling/errorFormatting.adoc b/src/main/docs/guide/httpServer/errorHandling/errorFormatting.adoc index 779180c318e..05b97cf6d19 100644 --- a/src/main/docs/guide/httpServer/errorHandling/errorFormatting.adoc +++ b/src/main/docs/guide/httpServer/errorHandling/errorFormatting.adoc @@ -1,4 +1,4 @@ -Micronaut produces error response bodies via beans of type api:http.server.exceptions.response.ErrorResponseProcessor[]. +The Micronaut framework produces error response bodies via beans of type api:http.server.exceptions.response.ErrorResponseProcessor[]. The default response body is link:https://github.com/blongden/vnd.error[vnd.error], however you can create your own implementation of type api:http.server.exceptions.response.ErrorResponseProcessor[] to control the responses. diff --git a/src/main/docs/guide/httpServer/errorHandling/exceptionHandler/builtInExceptionHandlers.adoc b/src/main/docs/guide/httpServer/errorHandling/exceptionHandler/builtInExceptionHandlers.adoc index b7631328ab6..b61fe763e78 100644 --- a/src/main/docs/guide/httpServer/errorHandling/exceptionHandler/builtInExceptionHandlers.adoc +++ b/src/main/docs/guide/httpServer/errorHandling/exceptionHandler/builtInExceptionHandlers.adoc @@ -1,4 +1,4 @@ -Micronaut ships with several built-in handlers: +The Micronaut framework ships with several built-in handlers: |=== |Exception|Handler diff --git a/src/main/docs/guide/httpServer/filters/filtermethods/filtermethodsexample.adoc b/src/main/docs/guide/httpServer/filters/filtermethods/filtermethodsexample.adoc index 0dbeb9fdd3e..cb768594786 100644 --- a/src/main/docs/guide/httpServer/filters/filtermethods/filtermethodsexample.adoc +++ b/src/main/docs/guide/httpServer/filters/filtermethods/filtermethodsexample.adoc @@ -1,4 +1,4 @@ -Suppose you wish to trace each request to the Micronaut "Hello World" example using some external system. This system could be a database or a distributed tracing service, and may require I/O operations. +Suppose you wish to trace each request to the "Hello World" example using some external system. This system could be a database or a distributed tracing service, and may require I/O operations. You should not block the underlying Netty event loop in your filter; instead the filter should proceed with execution once any I/O is complete. diff --git a/src/main/docs/guide/httpServer/filters/httpServerFilter/httpServerFilterExample.adoc b/src/main/docs/guide/httpServer/filters/httpServerFilter/httpServerFilterExample.adoc index 14c19a28e59..abe38de566d 100644 --- a/src/main/docs/guide/httpServer/filters/httpServerFilter/httpServerFilterExample.adoc +++ b/src/main/docs/guide/httpServer/filters/httpServerFilter/httpServerFilterExample.adoc @@ -1,4 +1,4 @@ -Suppose you wish to trace each request to the Micronaut "Hello World" example using some external system. This system could be a database or a distributed tracing service, and may require I/O operations. +Suppose you wish to trace each request to the "Hello World" example using some external system. This system could be a database or a distributed tracing service, and may require I/O operations. You should not block the underlying Netty event loop in your filter; instead the filter should proceed with execution once any I/O is complete. diff --git a/src/main/docs/guide/httpServer/formData.adoc b/src/main/docs/guide/httpServer/formData.adoc index a67e6616d1b..c5b099d748a 100644 --- a/src/main/docs/guide/httpServer/formData.adoc +++ b/src/main/docs/guide/httpServer/formData.adoc @@ -1,4 +1,4 @@ -To make data binding model customizations consistent between form data and JSON, Micronaut uses Jackson to implement binding data from form submissions. +To make data binding model customizations consistent between form data and JSON, the Micronaut framework uses Jackson to implement binding data from form submissions. The advantage of this approach is that the same Jackson annotations used for customizing JSON binding can be used for form submissions. diff --git a/src/main/docs/guide/httpServer/hostResolution.adoc b/src/main/docs/guide/httpServer/hostResolution.adoc index 71d7458a555..db6c0216598 100644 --- a/src/main/docs/guide/httpServer/hostResolution.adoc +++ b/src/main/docs/guide/httpServer/hostResolution.adoc @@ -1,4 +1,4 @@ -You may need to resolve the host name of the current server. Micronaut includes an implementation of the api:http.server.util.HttpHostResolver[] interface. +You may need to resolve the host name of the current server. The Micronaut framework includes an implementation of the api:http.server.util.HttpHostResolver[] interface. The default implementation looks for host information in the following places in order: diff --git a/src/main/docs/guide/httpServer/http2Server.adoc b/src/main/docs/guide/httpServer/http2Server.adoc index 1eb3a5bdbc1..b960c8d80ca 100644 --- a/src/main/docs/guide/httpServer/http2Server.adoc +++ b/src/main/docs/guide/httpServer/http2Server.adoc @@ -1,4 +1,4 @@ -Since Micronaut 2.x, Micronaut's Netty-based HTTP server can be configured to support HTTP/2. +Since Micronaut framework 2.x, Micronaut Netty-based HTTP server can be configured to support HTTP/2. ==== Configuring the Server for HTTP/2 @@ -12,7 +12,7 @@ micronaut: http-version: 2.0 ---- -With this configuration, Micronaut enables support for the `h2c` protocol (see https://httpwg.org/specs/rfc7540.html#discover-http[HTTP/2 over cleartext]) which is fine for development. +With this configuration, the Micronaut framework enables support for the `h2c` protocol (see https://httpwg.org/specs/rfc7540.html#discover-http[HTTP/2 over cleartext]) which is fine for development. Since browsers don't support `h2c` and in general https://httpwg.org/specs/rfc7540.html#discover-https[HTTP/2 over TLS] (the `h2` protocol), it is recommended for production that you enable <>. For development this can be done with: diff --git a/src/main/docs/guide/httpServer/http3Server.adoc b/src/main/docs/guide/httpServer/http3Server.adoc index 9af5699ec7c..2d709ad3aef 100644 --- a/src/main/docs/guide/httpServer/http3Server.adoc +++ b/src/main/docs/guide/httpServer/http3Server.adoc @@ -1,4 +1,4 @@ -Since Micronaut 4.x, Micronaut's Netty-based HTTP server can be configured to support HTTP/3. This support is experimental and may change without notice. +Since Micronaut framework 4.x, Micronaut's Netty-based HTTP server can be configured to support HTTP/3. This support is experimental and may change without notice. ==== Configuring the Server for HTTP/3 diff --git a/src/main/docs/guide/httpServer/jsonBinding.adoc b/src/main/docs/guide/httpServer/jsonBinding.adoc index 609b368e45b..d52e38778a1 100644 --- a/src/main/docs/guide/httpServer/jsonBinding.adoc +++ b/src/main/docs/guide/httpServer/jsonBinding.adoc @@ -4,9 +4,9 @@ The most common data interchange format nowadays is JSON. -In fact, the defaults in the api:http.annotation.Controller[] annotation specify that the controllers in Micronaut consume and produce JSON by default. +In fact, the defaults in the api:http.annotation.Controller[] annotation specify that the controllers in Micronaut framework consume and produce JSON by default. -To do so in a non-blocking manner, Micronaut builds on the https://github.com/FasterXML/jackson[Jackson] Asynchronous JSON parsing API and Netty, such that the reading of incoming JSON is done in a non-blocking manner. +To do so in a non-blocking manner, Micronaut framework builds on the https://github.com/FasterXML/jackson[Jackson] Asynchronous JSON parsing API and Netty, such that the reading of incoming JSON is done in a non-blocking manner. == Binding using Reactive Frameworks @@ -39,7 +39,7 @@ Note however you can just as easily write: snippet::io.micronaut.docs.server.json.PersonController[tags="class,regular,endclass", indent=0, title="Binding JSON POJOs"] -Micronaut only executes your method once the data has been read in a non-blocking manner. +The Micronaut framework only executes your method once the data has been read in a non-blocking manner. TIP: The output produced by Jackson can be customized in a variety of ways, from defining Jackson modules to using https://github.com/FasterXML/jackson-annotations/wiki/Jackson-Annotations[Jackson annotations] diff --git a/src/main/docs/guide/httpServer/localeResolution.adoc b/src/main/docs/guide/httpServer/localeResolution.adoc index ae8b9682d12..8e5c2a7bfa8 100644 --- a/src/main/docs/guide/httpServer/localeResolution.adoc +++ b/src/main/docs/guide/httpServer/localeResolution.adoc @@ -1,4 +1,4 @@ -Micronaut supports several strategies for resolving locales for a given request. The api:http.HttpRequest#getLocale--[] method is available on the request, however it only supports parsing the `Accept-Language` header. For other use cases where the locale can be in a cookie, the user's session, or should be set to a fixed value, api:http.server.util.locale.HttpLocaleResolver[] can be used to determine the current locale. +The Micronaut framework supports several strategies for resolving locales for a given request. The api:http.HttpRequest#getLocale--[] method is available on the request, however it only supports parsing the `Accept-Language` header. For other use cases where the locale can be in a cookie, the user's session, or should be set to a fixed value, api:http.server.util.locale.HttpLocaleResolver[] can be used to determine the current locale. The api:core.util.LocaleResolver[] API does not need to be used directly. Simply define a parameter to a controller method of type `java.util.Locale` and the locale will be resolved and injected automatically. diff --git a/src/main/docs/guide/httpServer/openapi.adoc b/src/main/docs/guide/httpServer/openapi.adoc index 45707ebf861..8a2414f1a6a 100644 --- a/src/main/docs/guide/httpServer/openapi.adoc +++ b/src/main/docs/guide/httpServer/openapi.adoc @@ -1,2 +1,2 @@ -To configure Micronaut integration with https://swagger.io/docs/specification/about/[OpenAPI/Swagger], please follow +To configure the Micronaut integration with https://swagger.io/docs/specification/about/[OpenAPI/Swagger], please follow https://micronaut-projects.github.io/micronaut-openapi/latest/guide/index.html[these instructions] diff --git a/src/main/docs/guide/httpServer/reactiveServer.adoc b/src/main/docs/guide/httpServer/reactiveServer.adoc index a25df2ad1aa..19f445721a6 100644 --- a/src/main/docs/guide/httpServer/reactiveServer.adoc +++ b/src/main/docs/guide/httpServer/reactiveServer.adoc @@ -1,4 +1,4 @@ -As mentioned previously, Micronaut is built on Netty which is designed around an Event loop model and non-blocking I/O. Micronaut executes code defined in ann:http.annotation.Controller[] beans in the same thread as the request thread (an Event Loop thread). +As mentioned previously, Micronaut framework is built on Netty which is designed around an Event loop model and non-blocking I/O. Micronaut executes code defined in ann:http.annotation.Controller[] beans in the same thread as the request thread (an Event Loop thread). This makes it critical that if you do any blocking I/O operations (for example interactions with Hibernate/JPA or JDBC) that you offload those tasks to a separate thread pool that does not block the Event loop. diff --git a/src/main/docs/guide/httpServer/reactiveServer/bodyAnnotation.adoc b/src/main/docs/guide/httpServer/reactiveServer/bodyAnnotation.adoc index 02a6fb1e6d4..43804daf18c 100644 --- a/src/main/docs/guide/httpServer/reactiveServer/bodyAnnotation.adoc +++ b/src/main/docs/guide/httpServer/reactiveServer/bodyAnnotation.adoc @@ -1,4 +1,4 @@ -To parse the request body, you first indicate to Micronaut which parameter receives the data with the api:http.annotation.Body[] annotation. +To parse the request body, you first indicate to the Micronaut framework which parameter receives the data with the api:http.annotation.Body[] annotation. The following example implements a simple echo server that echoes the body sent in the request: @@ -17,7 +17,7 @@ Regardless of the limit, for a large amount of data accumulating the data into a snippet::io.micronaut.docs.server.body.MessageController[tags="imports,importsreactive,class,echoReactive,endclass", indent=0, title="Using Reactive Streams to Read the request body"] <1> In this case the method is altered to receive and return an `Publisher` type. -<2> This example uses https://projectreactor.io[Project Reactor] and returns a single item. Because of that the response type is annotated also with api:core.async.annotation.SingleResult[]. Micronaut only emits the response once the operation completes without blocking. +<2> This example uses https://projectreactor.io[Project Reactor] and returns a single item. Because of that the response type is annotated also with api:core.async.annotation.SingleResult[]. The Micronaut framework only emits the response once the operation completes without blocking. <3> The `collect` method is used to accumulate the data in this simulated example, but it could for example write the data to a logging service, database, etc. chunk by chunk -IMPORTANT: Body arguments of types that do not require conversion cause Micronaut to skip decoding of the request! +IMPORTANT: Body arguments of types that do not require conversion cause the Micronaut framework to skip decoding of the request! diff --git a/src/main/docs/guide/httpServer/reactiveServer/reactiveResponses.adoc b/src/main/docs/guide/httpServer/reactiveServer/reactiveResponses.adoc index b6593a1f0b7..09720c809c6 100644 --- a/src/main/docs/guide/httpServer/reactiveServer/reactiveResponses.adoc +++ b/src/main/docs/guide/httpServer/reactiveServer/reactiveResponses.adoc @@ -1,6 +1,6 @@ The previous section introduced the notion of Reactive programming using https://projectreactor.io[Project Reactor] and Micronaut. -Micronaut supports returning common reactive types such as reactor:Mono[] (or rx:Single[] rx:Maybe[] rx:Observable[] types from RxJava), an instance of rs:Publisher[] or jdk:java.util.concurrent.CompletableFuture[] from any controller method. +The Micronaut framework supports returning common reactive types such as reactor:Mono[] (or rx:Single[] rx:Maybe[] rx:Observable[] types from RxJava), an instance of rs:Publisher[] or jdk:java.util.concurrent.CompletableFuture[] from any controller method. NOTE: To use https://projectreactor.io[Project Reactor]'s `Flux` or `Mono` you need to add the Micronaut Reactor dependency to your project to include the necessary converters. @@ -8,9 +8,9 @@ NOTE: To use https://github.com/ReactiveX/RxJava[RxJava]'s `Flowable`, `Single` The argument designated as the body of the request using the api:http.annotation.Body[] annotation can also be a reactive type or a jdk:java.util.concurrent.CompletableFuture[]. -When returning a reactive type, Micronaut subscribes to the returned reactive type on the same thread as the request (a Netty Event Loop thread). It is therefore important that if you perform any blocking operations, you offload those operations to an appropriately configured thread pool, for example using the https://projectreactor.io[Project Reactor] or https://github.com/ReactiveX/RxJava[RxJava] `subscribeOn(..)` facility or ann:scheduling.annotation.ExecuteOn[]. +When returning a reactive type, The Micronaut framework subscribes to the returned reactive type on the same thread as the request (a Netty Event Loop thread). It is therefore important that if you perform any blocking operations, you offload those operations to an appropriately configured thread pool, for example using the https://projectreactor.io[Project Reactor] or https://github.com/ReactiveX/RxJava[RxJava] `subscribeOn(..)` facility or ann:scheduling.annotation.ExecuteOn[]. -TIP: See the section on <> for information on the thread pools that Micronaut sets up and how to configure them. +TIP: See the section on <> for information on the thread pools that the Micronaut framework sets up and how to configure them. To summarize, the following table illustrates some common response types and their handling: @@ -39,4 +39,4 @@ To summarize, the following table illustrates some common response types and the |`Book show()` |=== -NOTE: When returning a Reactive type, its type affects the returned response. For example, when returning a reactor:Flux[], Micronaut cannot know the size of the response, so `Transfer-Encoding` type of `Chunked` is used. Whilst for types that emit a single result such as `reactor:Mono[]` the `Content-Length` header is populated. +NOTE: When returning a Reactive type, its type affects the returned response. For example, when returning a reactor:Flux[], the Micronaut framework cannot know the size of the response, so `Transfer-Encoding` type of `Chunked` is used. Whilst for types that emit a single result such as `reactor:Mono[]` the `Content-Length` header is populated. diff --git a/src/main/docs/guide/httpServer/routing.adoc b/src/main/docs/guide/httpServer/routing.adoc index 32325fe2198..8f87e1ffa61 100644 --- a/src/main/docs/guide/httpServer/routing.adoc +++ b/src/main/docs/guide/httpServer/routing.adoc @@ -28,7 +28,7 @@ snippet::io.micronaut.docs.server.routes.IssuesController[tags="imports,class", <3> The method argument can optionally be annotated with link:{api}/io/micronaut/http/annotation/PathVariable.html[PathVariable] <4> The value of the URI variable is referenced in the implementation -Micronaut maps the URI `/issues/{number}` for the above controller. We can assert this is the case by writing unit tests: +The Micronaut framework maps the URI `/issues/{number}` for the above controller. We can assert this is the case by writing unit tests: snippet::io.micronaut.docs.server.routes.IssuesControllerTest[tags="imports,class", indent=0, title="Testing URI Variables"] @@ -130,7 +130,7 @@ NOTE: Route validation is more complicated with multiple templates. If a variabl == Building Routes Programmatically -If you prefer to not use annotations and instead declare all routes in code then never fear, Micronaut has a flexible link:{api}/io/micronaut/web/router/RouteBuilder.html[RouteBuilder] API that makes it a breeze to define routes programmatically. +If you prefer to not use annotations and instead declare all routes in code then never fear, the Micronaut framework has a flexible link:{api}/io/micronaut/web/router/RouteBuilder.html[RouteBuilder] API that makes it a breeze to define routes programmatically. To start, subclass link:{api}/io/micronaut/web/router/DefaultRouteBuilder.html[DefaultRouteBuilder] and inject the controller to route to into the method, and define your routes: @@ -144,7 +144,7 @@ TIP: Unfortunately due to type erasure, a Java method lambda reference cannot be == Route Compile-Time Validation -Micronaut supports validating route arguments at compile time with the validation library. To get started, add the `micronaut-http-validation` dependency to your build: +The Micronaut framework supports validating route arguments at compile time with the validation library. To get started, add the `micronaut-http-validation` dependency to your build: dependency:io.micronaut:micronaut-http-validation[scope='annotationProcessor'] diff --git a/src/main/docs/guide/httpServer/serverConfiguration.adoc b/src/main/docs/guide/httpServer/serverConfiguration.adoc index 16971d46536..586f9d6a1b5 100644 --- a/src/main/docs/guide/httpServer/serverConfiguration.adoc +++ b/src/main/docs/guide/httpServer/serverConfiguration.adoc @@ -16,7 +16,7 @@ micronaut: childOptions: autoRead: true ---- -- By default Micronaut binds to all network interfaces. Use `localhost` to bind only to loopback network interface +- By default, Micronaut framework binds to all network interfaces. Use `localhost` to bind only to loopback network interface - `maxHeaderSize` sets the maximum size for headers - `worker.threads` specifies the number of Netty worker threads - `autoRead` enables request body auto read diff --git a/src/main/docs/guide/httpServer/serverConfiguration/cors.adoc b/src/main/docs/guide/httpServer/serverConfiguration/cors.adoc index e9f41b40e26..d3fe5746e5b 100644 --- a/src/main/docs/guide/httpServer/serverConfiguration/cors.adoc +++ b/src/main/docs/guide/httpServer/serverConfiguration/cors.adoc @@ -1 +1 @@ -Micronaut supports CORS (link:https://www.w3.org/TR/cors/[Cross Origin Resource Sharing]) out of the box. By default, CORS requests are rejected. +The Micronaut framework supports CORS (link:https://www.w3.org/TR/cors/[Cross Origin Resource Sharing]) out of the box. By default, CORS requests are rejected. diff --git a/src/main/docs/guide/httpServer/serverConfiguration/dualProtocol.adoc b/src/main/docs/guide/httpServer/serverConfiguration/dualProtocol.adoc index 58df6ab710e..fdf932f6b45 100644 --- a/src/main/docs/guide/httpServer/serverConfiguration/dualProtocol.adoc +++ b/src/main/docs/guide/httpServer/serverConfiguration/dualProtocol.adoc @@ -1,4 +1,4 @@ -Micronaut supports binding both HTTP and HTTPS. To enable dual protocol support, modify your configuration. For example: +The Micronaut framework supports binding both HTTP and HTTPS. To enable dual protocol support, modify your configuration. For example: .Dual Protocol Configuration Example [configuration] @@ -11,8 +11,7 @@ micronaut: dual-protocol : true ---- - You must configure SSL for HTTPS to work. In this example we are just using a self-signed certificate with `build-self-signed`, but see <> for other configurations -- `dual-protocol` enables both HTTP and HTTPS is an opt-in feature - setting the `dualProtocol` flag enables it. By default Micronaut only enables one - +- `dual-protocol` enables both HTTP and HTTPS is an opt-in feature - setting the `dualProtocol` flag enables it. By default, the Micronaut framework only enables one. It is also possible to redirect automatically all HTTP request to HTTPS. Besides the previous configuration, you need to enable this option. For example: diff --git a/src/main/docs/guide/httpServer/serverConfiguration/https.adoc b/src/main/docs/guide/httpServer/serverConfiguration/https.adoc index 4660633ed59..bb0016b4184 100644 --- a/src/main/docs/guide/httpServer/serverConfiguration/https.adoc +++ b/src/main/docs/guide/httpServer/serverConfiguration/https.adoc @@ -1,4 +1,4 @@ -Micronaut supports HTTPS out of the box. By default HTTPS is disabled and all requests are served using HTTP. To enable HTTPS support, modify your configuration. For example: +The Micronaut framework supports HTTPS out of the box. By default, HTTPS is disabled and all requests are served using HTTP. To enable HTTPS support, modify your configuration. For example: .HTTPS Configuration Example [configuration] @@ -9,9 +9,9 @@ micronaut: enabled: true buildSelfSigned: true ---- -- Micronaut will create a self-signed certificate. +- The Micronaut framework will create a self-signed certificate. -TIP: By default, Micronaut with HTTPS support starts on port `8443` but you can change the port with the property `micronaut.server.ssl.port`. +TIP: By default, a Micronaut application with HTTPS support starts on port `8443` but you can change the port with the property `micronaut.server.ssl.port`. For generating self-signed certificates, the Micronaut HTTP server will use netty. Netty uses one of two approaches to generate the certificate. @@ -28,7 +28,7 @@ image::https-warning.jpg[] == Using a valid x509 certificate -It is also possible to configure Micronaut to use an existing valid x509 certificate, for example one created with https://letsencrypt.org/[Let's Encrypt]. You will need the `server.crt` and `server.key` files and to convert them to a PKCS #12 file. +It is also possible to configure a Micronaut application to use an existing valid x509 certificate, for example one created with https://letsencrypt.org/[Let's Encrypt]. You will need the `server.crt` and `server.key` files and to convert them to a PKCS #12 file. [source,bash] ---- @@ -62,7 +62,7 @@ micronaut: - Specify the `p12` file path. It can also be referenced as `file:/path/to/the/file` - Also provide the `password` defined during the export -With this configuration, if we start Micronaut and connect to `https://localhost:8443` we still see the warning in the browser, but if we inspect the certificate we can check that it is the one generated by Let's Encrypt. +With this configuration, if we start a Micronaut application and connect to `https://localhost:8443` we still see the warning in the browser, but if we inspect the certificate we can check that it is the one generated by Let's Encrypt. image::https-certificate.jpg[] @@ -82,7 +82,7 @@ image::https-valid-certificate.jpg[] == Using Java Keystore (JKS) -Using this type of certificate is not recommended because the format is proprietary - PKCS12 format is preferred. Regardless, Micronaut also supports it. +Using this type of certificate is not recommended because the format is proprietary - PKCS12 format is preferred. Regardless, the Micronaut framework also supports it. Convert the `p12` certificate to a JKS one: @@ -121,7 +121,7 @@ Start Micronaut, and the application will run on `https://localhost:8443` using Keeping HTTPS certificates up-to-date after expiry can be a challenge. A great solution to this is https://en.wikipedia.org/wiki/Automated_Certificate_Management_Environment[Automated Certificate Management Environment] (ACME) and the https://micronaut-projects.github.io/micronaut-acme/latest/guide/index.html[Micronaut ACME Module] which provides support for automatically refreshing certificates from a certificate authority. -If the use of a certificate authority is not possible and you need to manually update certificates from disk then you should fire a api:runtime.context.scope.refresh.RefreshEvent[] using Micronaut's support for <> containing the keys where your HTTPS configuration is defined and Micronaut will reload the certificates from disk and apply the new configuration to the server. +If the use of a certificate authority is not possible and you need to manually update certificates from disk then you should fire a api:runtime.context.scope.refresh.RefreshEvent[] using Micronaut's support for <> containing the keys where your HTTPS configuration is defined and the Micronaut framework will reload the certificates from disk and apply the new configuration to the server. NOTE: You can also use the <>, however this will only apply if the physical location of certificate on disk has changed diff --git a/src/main/docs/guide/httpServer/serverConfiguration/nettyClientPipeline.adoc b/src/main/docs/guide/httpServer/serverConfiguration/nettyClientPipeline.adoc index 11fe326374a..34005ee9907 100644 --- a/src/main/docs/guide/httpServer/serverConfiguration/nettyClientPipeline.adoc +++ b/src/main/docs/guide/httpServer/serverConfiguration/nettyClientPipeline.adoc @@ -1,6 +1,6 @@ You can customize the Netty client pipeline by writing a <> that listens for the creation of a api:http.client.netty.NettyClientCustomizer.Registry[]. -The api:http.netty.channel.ChannelPipelineCustomizer[] interface defines constants for the names of the various handlers Micronaut registers. +The api:http.netty.channel.ChannelPipelineCustomizer[] interface defines constants for the names of the various handlers that the Micronaut framework registers. As an example the following code sample demonstrates registering the https://github.com/zalando/logbook[Logbook] library which includes additional Netty handlers to perform request and response logging: diff --git a/src/main/docs/guide/httpServer/serverConfiguration/nettyServerPipeline.adoc b/src/main/docs/guide/httpServer/serverConfiguration/nettyServerPipeline.adoc index 9a5caa957ed..8e4a43ab6ac 100644 --- a/src/main/docs/guide/httpServer/serverConfiguration/nettyServerPipeline.adoc +++ b/src/main/docs/guide/httpServer/serverConfiguration/nettyServerPipeline.adoc @@ -1,6 +1,6 @@ You can customize the Netty server pipeline by writing a <> that listens for the creation of api:io.micronaut.http.server.netty.NettyServerCustomizer.Registry[]. -The api:http.netty.channel.ChannelPipelineCustomizer[] interface defines constants for the names of the various handlers Micronaut registers. +The api:http.netty.channel.ChannelPipelineCustomizer[] interface defines constants for the names of the various handlers the Micronaut framework registers. As an example the following code sample demonstrates registering the https://github.com/zalando/logbook[Logbook] library which includes additional Netty handlers to perform request and response logging: diff --git a/src/main/docs/guide/httpServer/serverConfiguration/secondaryServers.adoc b/src/main/docs/guide/httpServer/serverConfiguration/secondaryServers.adoc index ab0fe3c4988..59e4caa1382 100644 --- a/src/main/docs/guide/httpServer/serverConfiguration/secondaryServers.adoc +++ b/src/main/docs/guide/httpServer/serverConfiguration/secondaryServers.adoc @@ -1,5 +1,4 @@ - -Micronaut supports the programmatic creation of additional Netty servers through the api:http.server.netty.NettyEmbeddedServerFactory[] interface. +The Micronaut framework supports the programmatic creation of additional Netty servers through the api:http.server.netty.NettyEmbeddedServerFactory[] interface. This is useful in cases where you, for example, need to expose distinct servers over different ports with potentially differing configurations (HTTPS, thread resources etc.). diff --git a/src/main/docs/guide/httpServer/serverConfiguration/threadPools/atBlocking.adoc b/src/main/docs/guide/httpServer/serverConfiguration/threadPools/atBlocking.adoc index 902ede50829..027f1686e35 100644 --- a/src/main/docs/guide/httpServer/serverConfiguration/threadPools/atBlocking.adoc +++ b/src/main/docs/guide/httpServer/serverConfiguration/threadPools/atBlocking.adoc @@ -1,8 +1,8 @@ You can use the ann:core.annotation.Blocking[] annotation to mark methods as blocking. -If you set `micronaut.server.thread-selection` to `AUTO`, The Micronaut Framework offloads the execution of methods annotated with `@Blocking` to the IO thread pool (See: api:io.micronaut.scheduling.TaskExecutors[]). +If you set `micronaut.server.thread-selection` to `AUTO`, the Micronaut framework offloads the execution of methods annotated with `@Blocking` to the IO thread pool (See: api:io.micronaut.scheduling.TaskExecutors[]). -NOTE: `@Blocking` only works if you are using `AUTO` thread selection. Micronaut Framework defaults to `MANUAL` thread selection since Micronaut 2.0. We recommend the usage of ann:scheduling.annotation.ExecuteOn[] annotation to execute the blocking operations on a different thread pool. `@ExecutesOn` works for both `MANUAL` and `AUTO` thread selection. +NOTE: `@Blocking` only works if you are using `AUTO` thread selection. Micronaut framework defaults to `MANUAL` thread selection since Micronaut 2.0. We recommend the usage of ann:scheduling.annotation.ExecuteOn[] annotation to execute the blocking operations on a different thread pool. `@ExecutesOn` works for both `MANUAL` and `AUTO` thread selection. There are some places where the Micronaut framework uses ann:core.annotation.Blocking[] internally: diff --git a/src/main/docs/guide/httpServer/serverConfiguration/threadPools/blockingOperations.adoc b/src/main/docs/guide/httpServer/serverConfiguration/threadPools/blockingOperations.adoc index 845e4317797..ff3cbff3c18 100644 --- a/src/main/docs/guide/httpServer/serverConfiguration/threadPools/blockingOperations.adoc +++ b/src/main/docs/guide/httpServer/serverConfiguration/threadPools/blockingOperations.adoc @@ -1,4 +1,4 @@ -When dealing with blocking operations, Micronaut shifts the blocking operations to an unbound, caching I/O thread pool by default. You can configure the I/O thread pool using the api:scheduling.executor.ExecutorConfiguration[] named `io`. For example: +When dealing with blocking operations, the Micronaut framework shifts the blocking operations to an unbound, caching I/O thread pool by default. You can configure the I/O thread pool using the api:scheduling.executor.ExecutorConfiguration[] named `io`. For example: .Configuring the Server I/O Thread Pool [configuration] diff --git a/src/main/docs/guide/httpServer/staticResources.adoc b/src/main/docs/guide/httpServer/staticResources.adoc index ef311372b4d..5158bf20c0f 100644 --- a/src/main/docs/guide/httpServer/staticResources.adoc +++ b/src/main/docs/guide/httpServer/staticResources.adoc @@ -1,4 +1,4 @@ -Static resource resolution is enabled by default. Micronaut supports resolving resources from the classpath or the file system. +Static resource resolution is enabled by default. The Micronaut framework supports resolving resources from the classpath or the file system. See the information below for available configuration options: diff --git a/src/main/docs/guide/httpServer/transfers.adoc b/src/main/docs/guide/httpServer/transfers.adoc index fbd3808bde6..44c467a5887 100644 --- a/src/main/docs/guide/httpServer/transfers.adoc +++ b/src/main/docs/guide/httpServer/transfers.adoc @@ -1,4 +1,4 @@ -Micronaut supports sending files to the client in a couple of easy ways. +The Micronaut framework supports sending files to the client in a couple of easy ways. == Sending File Objects @@ -19,7 +19,7 @@ public SystemFile download() { == Sending an InputStream -For cases where a reference to a `File` object is not possible (for example resources in JAR files), Micronaut supports transferring input streams. To return a stream of data from the controller method, construct a api:http.server.types.files.StreamedFile[]. +For cases where a reference to a `File` object is not possible (for example resources in JAR files), the Micronaut framework supports transferring input streams. To return a stream of data from the controller method, construct a api:http.server.types.files.StreamedFile[]. TIP: The constructor for `StreamedFile` also accepts a `java.net.URL` for your convenience. @@ -34,7 +34,7 @@ public StreamedFile download() { } ---- -The server supports returning `304` (Not Modified) responses if the files being transferred have not changed, and the request contains the appropriate header. In addition, if the client accepts encoded responses, Micronaut encodes the file if appropriate. Encoding happens if the file is text-based and larger than 1KB by default. The threshold at which data is encoded is configurable. See the server configuration reference for details. +The server supports returning `304` (Not Modified) responses if the files being transferred have not changed, and the request contains the appropriate header. In addition, if the client accepts encoded responses, the Micronaut framework encodes the file if appropriate. Encoding happens if the file is text-based and larger than 1KB by default. The threshold at which data is encoded is configurable. See the server configuration reference for details. TIP: To use a custom data source to send data through an input stream, construct a link:{javase}java/io/PipedInputStream.html[PipedInputStream] and link:{javase}java/io/PipedOutputStream.html[PipedOutputStream] to write data from the output stream to the input. Make sure to do the work on a separate thread so the file can be returned immediately. diff --git a/src/main/docs/guide/httpServer/views.adoc b/src/main/docs/guide/httpServer/views.adoc index 580b8dad958..680dee54869 100644 --- a/src/main/docs/guide/httpServer/views.adoc +++ b/src/main/docs/guide/httpServer/views.adoc @@ -1,3 +1,3 @@ -Micronaut supports Server Side View Rendering. +The Micronaut framework supports Server Side View Rendering. See the documentation for link:https://micronaut-projects.github.io/micronaut-views/latest/guide[Micronaut Views] for more information. diff --git a/src/main/docs/guide/httpServer/websocket.adoc b/src/main/docs/guide/httpServer/websocket.adoc index f865217fe8e..927240b0baa 100644 --- a/src/main/docs/guide/httpServer/websocket.adoc +++ b/src/main/docs/guide/httpServer/websocket.adoc @@ -1,4 +1,4 @@ -Micronaut features dedicated support for creating WebSocket clients and servers. The pkg:websocket.annotation[] package includes annotations for defining both clients and servers. +The Micronaut framework features dedicated support for creating WebSocket clients and servers. The pkg:websocket.annotation[] package includes annotations for defining both clients and servers. WARNING: Since Micronaut Framework 4.0. `io.micronaut:micronaut-http-server` no longer exposes `micronaut-websocket` transitively. To use annotations such as ann:websocket.annotation.ServerWebSocket[], add the `micronaut-websocket` dependency to your application classpath: diff --git a/src/main/docs/guide/httpServer/websocket/websocketClient.adoc b/src/main/docs/guide/httpServer/websocket/websocketClient.adoc index 1c29065fd5a..d74f48fbd47 100644 --- a/src/main/docs/guide/httpServer/websocket/websocketClient.adoc +++ b/src/main/docs/guide/httpServer/websocket/websocketClient.adoc @@ -30,7 +30,7 @@ You can also define abstract methods that start with either `send` or `broadcast public abstract void send(String message); ---- -Note by returning `void` this tells Micronaut that the method is a blocking send. You can instead define methods that return either futures or a rs:Publisher[]: +Note by returning `void` this tells the Micronaut framework that the method is a blocking send. You can instead define methods that return either futures or a rs:Publisher[]: .WebSocket Send Methods [source,java] diff --git a/src/main/docs/guide/httpServer/websocket/websocketServer.adoc b/src/main/docs/guide/httpServer/websocket/websocketServer.adoc index 9ce0b803bfa..253521ab5fb 100644 --- a/src/main/docs/guide/httpServer/websocket/websocketServer.adoc +++ b/src/main/docs/guide/httpServer/websocket/websocketServer.adoc @@ -39,7 +39,7 @@ The previous example uses the `broadcastSync` method of the api:websocket.WebSoc snippet::io.micronaut.docs.http.server.netty.websocket.ReactivePojoChatServerWebSocket[tags="onmessage", indent=0,title="WebSocket Chat Example"] -The example above uses `broadcast`, which creates an instance of rs:Publisher[] and returns the value to Micronaut. Micronaut sends the message asynchronously based on the Publisher interface. The similar `send` method sends a single message asynchronously via Micronaut return value. +The example above uses `broadcast`, which creates an instance of rs:Publisher[] and returns the value to Micronaut. The Micronaut framework sends the message asynchronously based on the Publisher interface. The similar `send` method sends a single message asynchronously via Micronaut return value. For sending messages asynchronously outside Micronaut annotated handler methods, you can use `broadcastAsync` and `sendAsync` methods in their respective api:websocket.WebSocketBroadcaster[] and api:websocket.WebSocketSession[] interfaces. For blocking sends, the `broadcastSync` and `sendSync` methods can be used. @@ -68,7 +68,7 @@ $ mn create-websocket-server MyChat === Connection Timeouts -By default, Micronaut times out idle connections with no activity after five minutes. Normally this is not a problem as browsers automatically reconnect WebSocket sessions, however you can control this behaviour by setting the `micronaut.server.idle-timeout` setting (a negative value results in no timeout): +By default, Micronaut framework times out idle connections with no activity after five minutes. Normally this is not a problem as browsers automatically reconnect WebSocket sessions, however you can control this behaviour by setting the `micronaut.server.idle-timeout` setting (a negative value results in no timeout): .Setting the Connection Timeout for the Server [configuration] diff --git a/src/main/docs/guide/introduction.adoc b/src/main/docs/guide/introduction.adoc index 6abca8a22bb..25209427559 100644 --- a/src/main/docs/guide/introduction.adoc +++ b/src/main/docs/guide/introduction.adoc @@ -1,21 +1,21 @@ -Micronaut is a modern, JVM-based, full stack Java framework designed for building modular, easily testable JVM applications with support for Java, Kotlin, and Groovy. +The Micronaut Framework is a modern, JVM-based, full stack Java framework designed for building modular, easily testable JVM applications with support for Java, Kotlin, and Groovy. -Micronaut is developed by the creators of the Grails framework and takes inspiration from lessons learnt over the years building real-world applications from monoliths to microservices using Spring, Spring Boot and Grails. +The Micronaut framework is developed by the creators of the Grails framework and takes inspiration from lessons learnt over the years building real-world applications from monoliths to microservices using Spring, Spring Boot and Grails. -Micronaut aims to provide all the tools necessary to build JVM applications including: +The Micronaut framework aims to provide all the tools necessary to build JVM applications including: * Dependency Injection and Inversion of Control (IoC) * Aspect Oriented Programming (AOP) * Sensible Defaults and Auto-Configuration -With Micronaut you can build Message-Driven Applications, Command Line Applications, HTTP Servers and more whilst for Microservices in particular Micronaut also provides: +With the Micronaut framework you can build Message-Driven Applications, Command Line Applications, HTTP Servers and more whilst for Microservices in particular Micronaut also provides: * Distributed Configuration * Service Discovery * HTTP Routing * Client-Side Load Balancing -At the same time Micronaut aims to avoid the downsides of frameworks like Spring, Spring Boot and Grails by providing: +At the same time, the Micronaut framework aims to avoid the downsides of frameworks like Spring, Spring Boot and Grails by providing: * Fast startup time * Reduced memory footprint @@ -24,8 +24,8 @@ At the same time Micronaut aims to avoid the downsides of frameworks like Spring * No runtime bytecode generation * Easy Unit Testing -Historically, frameworks such as Spring and Grails were not designed to run in scenarios such as serverless functions, Android apps, or low memory footprint microservices. In contrast, Micronaut is designed to be suitable for all of these scenarios. +Historically, frameworks such as Spring and Grails were not designed to run in scenarios such as serverless functions, Android apps, or low memory footprint microservices. In contrast, the Micronaut framework is designed to be suitable for all of these scenarios. This goal is achieved through the use of Java's link:{javaseapi17}/java.compiler/javax/annotation/processing/Processor.html[annotation processors], which are usable on any JVM language that supports them, as well as an HTTP Server (with several runtimes https://netty.io/[Netty], link:{micronautservletdocs}#jetty[Jetty], link:{micronautservletdocs}#tomcat[Tomcat], link:{micronautservletdocs}#undertow[Undertow]...) and an HTTP Client (with several runtimes <>, <>, ...). To provide a similar programming model to Spring and Grails, these annotation processors precompile the necessary metadata to perform DI, define AOP proxies and configure your application to run in a low-memory environment. -Many APIs in Micronaut are heavily inspired by Spring and Grails. This is by design, and helps bring developers up to speed quickly. +Many APIs in the Micronaut framework are heavily inspired by Spring and Grails. This is by design, and helps bring developers up to speed quickly. diff --git a/src/main/docs/guide/introduction/upgrading.adoc b/src/main/docs/guide/introduction/upgrading.adoc index 803a20fd7d2..c69c981836a 100644 --- a/src/main/docs/guide/introduction/upgrading.adoc +++ b/src/main/docs/guide/introduction/upgrading.adoc @@ -1,4 +1,4 @@ -This section covers the steps required to upgrade a Micronaut 2.x application to Micronaut 3.0.0. +This section covers the steps required to upgrade a Micronaut framework 2.x application to Micronaut framework 3.0.0. The sections below go into more detail, but at a high level the process generally involves: @@ -115,7 +115,7 @@ For Maven users the plugin version is updated automatically when you update the === Inject Annotations -The `javax.inject` annotations are no longer a transitive dependency. Micronaut now ships with the Jakarta inject annotations. Either replace all `javax.inject` imports with `jakarta.inject`, or add a dependency on `javax-inject` to continue using the older annotations: +The `javax.inject` annotations are no longer a transitive dependency. The Micronaut framework now ships with the Jakarta inject annotations. Either replace all `javax.inject` imports with `jakarta.inject`, or add a dependency on `javax-inject` to continue using the older annotations: dependency:javax.inject:javax.inject:1[] @@ -123,13 +123,13 @@ Any code that relied on the `javax.inject` annotations being present in the anno === Nullability Annotations -Micronaut now only comes with its own set of annotations to declare nullability. The findbugs, javax, and jetbrains annotations are all still supported, however you must add a dependency to use them. Either switch to the Micronaut ann:core.annotation.Nullable[] / ann:core.annotation.NonNull[] annotations or add a dependency for the annotation library you wish to use. +The Micronaut framework now only comes with its own set of annotations to declare nullability. The findbugs, javax, and jetbrains annotations are all still supported, however you must add a dependency to use them. Either switch to the Micronaut ann:core.annotation.Nullable[] / ann:core.annotation.NonNull[] annotations or add a dependency for the annotation library you wish to use. === RxJava2 -Micronaut no longer ships any reactive implementation as a default in any of our modules or core libraries. Upgrading to Micronaut 3 requires choosing which reactive streams implementation to use, and then adding the relevant dependency. +The Micronaut framework no longer ships any reactive implementation as a default in any of our modules or core libraries. Upgrading to Micronaut 3 requires choosing which reactive streams implementation to use, and then adding the relevant dependency. -For those already using RxJava3 or Project Reactor, there should be no changes required to upgrade to Micronaut 3. If you use RxJava2 and wish to continue using it, you must add a dependency: +For those already using RxJava3 or Project Reactor, there should be no changes required to upgrade to the Micronaut framework 3. If you use RxJava2 and wish to continue using it, you must add a dependency: dependency:io.micronaut.rxjava2:micronaut-rxjava2[gradleScope="implementation"] diff --git a/src/main/docs/guide/introduction/whatsNew.adoc b/src/main/docs/guide/introduction/whatsNew.adoc index bb780eb8ef4..0c214f50859 100644 --- a/src/main/docs/guide/introduction/whatsNew.adoc +++ b/src/main/docs/guide/introduction/whatsNew.adoc @@ -3,15 +3,15 @@ === Kotlin 1.8.0 -Micronaut Framework 4.0 supports https://kotlinlang.org/docs/whatsnew18.html[Kotlin 1.8.0] +Micronaut framework 4.0 supports https://kotlinlang.org/docs/whatsnew18.html[Kotlin 1.8.0] === Experimental Support for Kotlin Symbol Processing (KSP) -Micronaut Framework has offered support for Kotlin via <>. With version 4.0, Micronaut Framework supports Kotlin also via <>. +Micronaut framework has offered support for Kotlin via <>. With version 4.0, Micronaut framework supports Kotlin also via <>. === Apache Groovy 4.0 -Micronaut Framework 4.x supports https://groovy-lang.org/releasenotes/groovy-4.0.html[Apache Groovy 4.0]. +Micronaut framework 4.x supports https://groovy-lang.org/releasenotes/groovy-4.0.html[Apache Groovy 4.0]. === Core Changes @@ -38,5 +38,5 @@ The disabled beans are also now visible via the < - Kotlin 1.7.10 -<> +<> diff --git a/src/main/docs/guide/ioc.adoc b/src/main/docs/guide/ioc.adoc index 6dcc81d31a2..86525204cf3 100644 --- a/src/main/docs/guide/ioc.adoc +++ b/src/main/docs/guide/ioc.adoc @@ -1,4 +1,4 @@ -Unlike other frameworks which rely on runtime reflection and proxies, Micronaut uses compile time data to implement dependency injection. +Unlike other frameworks which rely on runtime reflection and proxies, the Micronaut framework uses compile time data to implement dependency injection. This is a similar approach taken by tools such as Google https://google.github.io/dagger/[Dagger], which is designed primarily with Android in mind. Micronaut, on the other hand, is designed for building server-side microservices and provides many of the same tools and utilities as other frameworks but without using reflection or caching excessive amounts of reflection metadata. @@ -10,7 +10,7 @@ The goals of the Micronaut IoC container are summarized as: * Reduce memory footprint * Provide clear, understandable error handling -Note that the IoC part of Micronaut can be used completely independently of Micronaut for whatever application type you wish to build. +Note that the IoC part of the Micronaut framework can be used completely independently of Micronaut for whatever application type you wish to build. To do so, configure your build to include the `micronaut-inject-java` dependency as an annotation processor. @@ -35,7 +35,7 @@ micronaut { } ---- <1> Define the https://plugins.gradle.org/plugin/io.micronaut.library[Micronaut Library plugin] -<2> Specify the Micronaut version to use +<2> Specify the Micronaut framework version to use The entry point for IoC is then the api:context.ApplicationContext[] interface, which includes a `run` method. The following example demonstrates using it: diff --git a/src/main/docs/guide/ioc/annotationMetadata.adoc b/src/main/docs/guide/ioc/annotationMetadata.adoc index 3b6ab175922..c58cd781d18 100644 --- a/src/main/docs/guide/ioc/annotationMetadata.adoc +++ b/src/main/docs/guide/ioc/annotationMetadata.adoc @@ -2,7 +2,7 @@ The methods provided by Java's link:{jdkapi}/java/lang/reflect/AnnotatedElement. To solve this problem many frameworks produce runtime metadata or perform expensive reflection to analyze the annotations of a class. -Micronaut instead produces this annotation metadata at compile time, avoiding expensive reflection and saving memory. +The Micronaut framework instead produces this annotation metadata at compile time, avoiding expensive reflection and saving memory. The link:{api}/io/micronaut/context/BeanContext.html[BeanContext] API can be used to obtain a reference to a link:{api}/io/micronaut/inject/BeanDefinition.html[BeanDefinition] which implements the link:{api}/io/micronaut/core/annotation/AnnotationMetadata.html[AnnotationMetadata] interface. @@ -37,12 +37,12 @@ This approach is not recommended however, as it requires reflection and increase === Annotation Inheritance -Micronaut will respect the rules defined in Java's jdk:java.lang.reflect.AnnotatedElement[] API with regards to annotation inheritance: +The Micronaut framework will respect the rules defined in Java's jdk:java.lang.reflect.AnnotatedElement[] API with regards to annotation inheritance: * Annotations meta-annotated with jdk:java.lang.annotation.Inherited[] will be available via the `getAnnotation*` methods of the api:core.annotation.AnnotationMetadata[] API whilst those directly declared are available via the `getDeclaredAnnotation*` methods. * Annotations not meta-annotated with jdk:java.lang.annotation.Inherited[] will not be included in the metadata -Micronaut differs from the jdk:java.lang.reflect.AnnotatedElement[] API in that it extends these rules to methods and method parameters such that: +The Micronaut framework differs from the jdk:java.lang.reflect.AnnotatedElement[] API in that it extends these rules to methods and method parameters such that: * Any annotations annotated with jdk:java.lang.annotation.Inherited[] and present on a method of interface or super class `A` that is overridden by child interface or class `B` will be inherited into the api:core.annotation.AnnotationMetadata[] retrievable via the api:inject.ExecutableMethod[] API from a api:inject.BeanDefinition[] or an <>. * Any annotations annotated with jdk:java.lang.annotation.Inherited[] and present on a method parameter of interface or super class `A` that is overridden by child interface or class `B` will be inherited into the api:core.annotation.AnnotationMetadata[] retrievable via the api:core.type.Argument[] interface from the `getArguments` method of the api:inject.ExecutableMethod[] API. diff --git a/src/main/docs/guide/ioc/beanConfigurations.adoc b/src/main/docs/guide/ioc/beanConfigurations.adoc index 5fe6e6cb382..cd0bd186f53 100644 --- a/src/main/docs/guide/ioc/beanConfigurations.adoc +++ b/src/main/docs/guide/ioc/beanConfigurations.adoc @@ -1,6 +1,6 @@ A bean link:{api}/io/micronaut/context/annotation/Configuration.html[@Configuration] is a grouping of multiple bean definitions within a package. -The `@Configuration` annotation is applied at the package level and informs Micronaut that the beans defined with the package form a logical grouping. +The `@Configuration` annotation is applied at the package level and informs the Micronaut framework that the beans defined with the package form a logical grouping. The `@Configuration` annotation is typically applied to `package-info` classes. For example: diff --git a/src/main/docs/guide/ioc/beanImport.adoc b/src/main/docs/guide/ioc/beanImport.adoc index 08958943c2a..5237a5a4d77 100644 --- a/src/main/docs/guide/ioc/beanImport.adoc +++ b/src/main/docs/guide/ioc/beanImport.adoc @@ -24,7 +24,7 @@ public class Application { ---- <1> The ann:context.annotation.Import[] is defined -<2> The `packages` to import are defined. Note that Micronaut will not recurse through sub-packages so sub-packages need to be listed explicitly -<3> By default Micronaut will only import classes that feature a scope or a qualifier. By using `*` you can make every type a bean. +<2> The `packages` to import are defined. Note that the Micronaut framework will not recurse through sub-packages so sub-packages need to be listed explicitly +<3> By default, Micronaut framework will only import classes that feature a scope or a qualifier. By using `*` you can make every type a bean. NOTE: In general `@Import` should be used in applications rather than libraries since if two libraries import the same beans the result will likely be a api:context.exceptions.NonUniqueBeanException[] diff --git a/src/main/docs/guide/ioc/beans.adoc b/src/main/docs/guide/ioc/beans.adoc index 2d097940a0d..b7baa254b72 100644 --- a/src/main/docs/guide/ioc/beans.adoc +++ b/src/main/docs/guide/ioc/beans.adoc @@ -12,9 +12,9 @@ To perform dependency injection, run the link:{api}/io/micronaut/context/BeanCon snippet::io.micronaut.docs.inject.intro.VehicleSpec[tags="start",indent="0"] -Micronaut automatically discovers dependency injection metadata on the classpath and wires the beans together according to injection points you define. +The Micronaut framework automatically discovers dependency injection metadata on the classpath and wires the beans together according to injection points you define. -Micronaut supports the following types of dependency injection: +The Micronaut framework supports the following types of dependency injection: * Constructor injection (must be one public constructor or a single constructor annotated with `@Inject`) * Field injection diff --git a/src/main/docs/guide/ioc/contextEvents.adoc b/src/main/docs/guide/ioc/contextEvents.adoc index 067389c5d81..d88a9418764 100644 --- a/src/main/docs/guide/ioc/contextEvents.adoc +++ b/src/main/docs/guide/ioc/contextEvents.adoc @@ -1,8 +1,8 @@ -Micronaut supports a general event system through the context. The api:context.event.ApplicationEventPublisher[] API publishes events and the api:context.event.ApplicationEventListener[] API is used to listen to events. The event system is not limited to events that Micronaut publishes and supports custom events created by users. +The Micronaut framework supports a general event system through the context. The api:context.event.ApplicationEventPublisher[] API publishes events and the api:context.event.ApplicationEventListener[] API is used to listen to events. The event system is not limited to events that Micronaut publishes and supports custom events created by users. === Publishing Events -The api:context.event.ApplicationEventPublisher[] API supports events of any type, although all events that Micronaut publishes extend api:context.event.ApplicationEvent[]. +The api:context.event.ApplicationEventPublisher[] API supports events of any type, although all events that the Micronaut framework publishes extend api:context.event.ApplicationEvent[]. To publish an event, use dependency injection to obtain an instance of api:context.event.ApplicationEventPublisher[] where the generic type is the type of event and invoke the `publishEvent` method with your event object. diff --git a/src/main/docs/guide/ioc/factories.adoc b/src/main/docs/guide/ioc/factories.adoc index ccd517561d6..b01b9387b41 100644 --- a/src/main/docs/guide/ioc/factories.adoc +++ b/src/main/docs/guide/ioc/factories.adoc @@ -18,7 +18,7 @@ TIP: To allow the resulting bean to participate in the application context shutd === Beans from Fields -With Micronaut 3.0 or above it is also possible to produce beans from fields by declaring the ann:context.annotation.Bean[] annotation on a field. +With Micronaut framework 3.0 or above it is also possible to produce beans from fields by declaring the ann:context.annotation.Bean[] annotation on a field. Whilst generally this approach should be discouraged in favour for factory methods, which provide more flexibility it does simplify testing code. For example with bean fields you can easily produce mocks in your test code: @@ -36,7 +36,7 @@ NOTE: Qualifiers from the factory instance aren't inherited to the beans. ==== Primitive Beans and Arrays -Since Micronaut 3.1 it is possible to define and inject primitive types and array types from factories. +Since Micronaut framework 3.1 it is possible to define and inject primitive types and array types from factories. For example: diff --git a/src/main/docs/guide/ioc/how.adoc b/src/main/docs/guide/ioc/how.adoc index bb186905daa..609305af8b2 100644 --- a/src/main/docs/guide/ioc/how.adoc +++ b/src/main/docs/guide/ioc/how.adoc @@ -1,9 +1,9 @@ -At this point, you may be wondering how Micronaut performs the above dependency injection without requiring reflection. +At this point, you may be wondering how Micronaut framework performs the above dependency injection without requiring reflection. The key is a set of AST transformations (for Groovy) and annotation processors (for Java) that generate classes that implement the link:{api}/io/micronaut/inject/BeanDefinition.html[BeanDefinition] interface. -Micronaut uses the ASM bytecode library to generate classes, and because Micronaut knows ahead of time the injection points, there is no need to scan all of the methods, fields, constructors, etc. at runtime like other frameworks such as Spring do. +Micronaut framework uses the ASM bytecode library to generate classes, and because Micronaut knows ahead of time the injection points, there is no need to scan all of the methods, fields, constructors, etc. at runtime like other frameworks such as Spring do. Also, since reflection is not used when constructing the bean, the JVM can inline and optimize the code far better, resulting in better runtime performance and reduced memory consumption. This is particularly important for non-singleton scopes where application performance depends on bean creation performance. -In addition, with Micronaut your application startup time and memory consumption are not affected by the size of your codebase in the same way as with a framework that uses reflection. Reflection-based IoC frameworks load and cache reflection data for every single field, method, and constructor in your code. Thus as your code grows in size so do your memory requirements, whilst with Micronaut this is not the case. +In addition, with Micronaut framework your application startup time and memory consumption are not affected by the size of your codebase in the same way as with a framework that uses reflection. Reflection-based IoC frameworks load and cache reflection data for every single field, method, and constructor in your code. Thus as your code grows in size so do your memory requirements, whilst with Micronaut this is not the case. diff --git a/src/main/docs/guide/ioc/introspection.adoc b/src/main/docs/guide/ioc/introspection.adoc index e0d5b4e724b..7ac833aadc4 100644 --- a/src/main/docs/guide/ioc/introspection.adoc +++ b/src/main/docs/guide/ioc/introspection.adoc @@ -1,4 +1,4 @@ -Since Micronaut 1.1, a compile-time replacement for the JDK's jdk:java.beans.Introspector[] class has been included. +Since Micronaut framework 1.1, a compile-time replacement for the JDK's jdk:java.beans.Introspector[] class has been included. The api:core.beans.BeanIntrospector[] and api:core.beans.BeanIntrospection[] interfaces allow looking up bean introspections to instantiate and read/write bean properties without using reflection or caching reflective metadata, which consume excessive memory for large beans. diff --git a/src/main/docs/guide/ioc/lifecycle.adoc b/src/main/docs/guide/ioc/lifecycle.adoc index e60c8013263..d56a87f7ba4 100644 --- a/src/main/docs/guide/ioc/lifecycle.adoc +++ b/src/main/docs/guide/ioc/lifecycle.adoc @@ -19,7 +19,7 @@ snippet::io.micronaut.docs.lifecycle.PreDestroyBean[tags="class", indent=0] <1> The `PreDestroy` annotation is imported <2> A method is annotated with `@PreDestroy` and will be invoked when the context is closed. -For factory beans, the `preDestroy` value in the api:context.annotation.Bean[] annotation tells Micronaut which method to invoke. +For factory beans, the `preDestroy` value in the api:context.annotation.Bean[] annotation tells Micronaut framework which method to invoke. snippet::io.micronaut.docs.lifecycle.ConnectionFactory[tags="class", indent=0] @@ -35,4 +35,4 @@ NOTE: Simply implementing the `Closeable` or `AutoCloseable` interface is not en == Dependent Beans Dependent beans are the beans used in the construction of your bean. -If the dependent bean's scope is `@Prototype` or unknown, it will be destroyed along with your instance. \ No newline at end of file +If the dependent bean's scope is `@Prototype` or unknown, it will be destroyed along with your instance. diff --git a/src/main/docs/guide/ioc/nullabilityAnnotations.adoc b/src/main/docs/guide/ioc/nullabilityAnnotations.adoc index 9a96cc80ed3..873289e583b 100644 --- a/src/main/docs/guide/ioc/nullabilityAnnotations.adoc +++ b/src/main/docs/guide/ioc/nullabilityAnnotations.adoc @@ -4,11 +4,11 @@ Micronaut framework comes with its own set of annotations to declare nullability **Why does the Micronaut framework add its own set of nullability annotations instead of using one of the existing nullability annotations libraries?** -Throughout the history of the framework, we used other nullability annotation libraries. However, licensing issues made us change nullability annotations several times. To avoid having to change nullability annotations in the future, we added our own set of nullability annotations in Micronaut Framework 2.4 +Throughout the history of the framework, we used other nullability annotation libraries. However, licensing issues made us change nullability annotations several times. To avoid having to change nullability annotations in the future, we added our own set of nullability annotations in Micronaut framework 2.4 **Are Micronaut Nullability annotations recognized by Kotlin?** -Yes, Micronaut nullability annotations are mapped at compilation time to `javax.annotation.Nullable` and `javax.annotation.Nonnull`. +Yes, Micronaut framework's nullability annotations are mapped at compilation time to `javax.annotation.Nullable` and `javax.annotation.Nonnull`. **Why should you use nullability annotations in your code?** diff --git a/src/main/docs/guide/ioc/qualifiers.adoc b/src/main/docs/guide/ioc/qualifiers.adoc index 07287fd83d3..bd3bd4875cb 100644 --- a/src/main/docs/guide/ioc/qualifiers.adoc +++ b/src/main/docs/guide/ioc/qualifiers.adoc @@ -1,6 +1,6 @@ If you have multiple possible implementations for a given interface to inject, you need to use a qualifier. -Once again Micronaut leverages JSR-330 and the link:{jeeapi}/javax/inject/Qualifier.html[Qualifier] and link:{jeeapi}/javax/inject/Named.html[Named] annotations to support this use case. +Once again Micronaut framework leverages JSR-330 and the link:{jeeapi}/javax/inject/Qualifier.html[Qualifier] and link:{jeeapi}/javax/inject/Named.html[Named] annotations to support this use case. == Qualifying By Name @@ -14,7 +14,7 @@ snippet::io.micronaut.docs.inject.qualifiers.named.Engine,io.micronaut.docs.inje <4> The link:{jeeapi}/javax/inject/Named.html[javax.inject.Named] annotation indicates that the `V8Engine` implementation is required <5> Calling the start method prints: `"Starting V8"` -Micronaut is capable of injecting `V8Engine` in the previous example, because: +Micronaut framework is capable of injecting `V8Engine` in the previous example, because: `@Named` qualifier value (`v8`) + type being injected simple name (`Engine`) == (case-insensitive) == The simple name of a bean of type `Engine` (`V8Engine`) @@ -32,7 +32,7 @@ snippet::io.micronaut.docs.qualifiers.annotation.Vehicle[tags="constructor",inde == Qualifying By Annotation Members -Since Micronaut 3.0, annotation qualifiers can also use annotation members to resolve the correct bean to inject. For example, consider the following annotation: +Since Micronaut framework 3.0, annotation qualifiers can also use annotation members to resolve the correct bean to inject. For example, consider the following annotation: snippet::io.micronaut.docs.qualifiers.annotationmember.Cylinders[tags="imports,class",indent=0] @@ -57,7 +57,7 @@ snippet::io.micronaut.docs.qualifiers.annotationmember.Vehicle[tags="constructor == Qualifying by Generic Type Arguments -Since Micronaut 3.0, it is possible to select which bean to inject based on the generic type arguments of the class or interface. Consider the following example: +Since Micronaut framework 3.0, it is possible to select which bean to inject based on the generic type arguments of the class or interface. Consider the following example: snippet::io.micronaut.docs.inject.generics.CylinderProvider[tags="class",indent=0] @@ -87,7 +87,7 @@ snippet::io.micronaut.docs.inject.generics.V8Engine[tags="class",indent=0] <1> The `V8Engine` implements `Engine` providing `V8` as a generic type parameter -You can then use the generic arguments when defining the injection point and Micronaut will pick the correct bean +You can then use the generic arguments when defining the injection point and Micronaut framework will pick the correct bean to inject based on the specific generic type arguments: snippet::io.micronaut.docs.inject.generics.Vehicle[tags="constructor",indent=0] diff --git a/src/main/docs/guide/ioc/scopes.adoc b/src/main/docs/guide/ioc/scopes.adoc index 6ec2ea2c85c..8dee727e1e1 100644 --- a/src/main/docs/guide/ioc/scopes.adoc +++ b/src/main/docs/guide/ioc/scopes.adoc @@ -1 +1 @@ -Micronaut features an extensible bean scoping mechanism based on JSR-330. The following default scopes are supported: +Micronaut framework features an extensible bean scoping mechanism based on JSR-330. The following default scopes are supported: diff --git a/src/main/docs/guide/ioc/scopes/builtInScopes/eagerInit.adoc b/src/main/docs/guide/ioc/scopes/builtInScopes/eagerInit.adoc index 564eb35dad2..1f426310507 100644 --- a/src/main/docs/guide/ioc/scopes/builtInScopes/eagerInit.adoc +++ b/src/main/docs/guide/ioc/scopes/builtInScopes/eagerInit.adoc @@ -18,7 +18,7 @@ public class Application { <1> Setting eager init to `true` initializes all singletons -When you use Micronaut in environments such as <>, you will not have an Application class and instead you extend a Micronaut-provided class. In those cases, Micronaut provides methods which you can override to enhance the api:context.ApplicationContextBuilder[] +When you use Micronaut framework in environments such as <>, you will not have an Application class and instead you extend a Micronaut-provided class. In those cases, Micronaut provides methods which you can override to enhance the api:context.ApplicationContextBuilder[] .Override of newApplicationContextBuilder() [source,java] diff --git a/src/main/docs/guide/ioc/springBeans.adoc b/src/main/docs/guide/ioc/springBeans.adoc index 8a21715edbc..8fd28e5d53d 100644 --- a/src/main/docs/guide/ioc/springBeans.adoc +++ b/src/main/docs/guide/ioc/springBeans.adoc @@ -1 +1 @@ -Micronaut has integrations with Spring in several forms. See the https://micronaut-projects.github.io/micronaut-spring/latest/guide[Micronaut Spring Documentation] for more information. +Micronaut framework has integrations with Spring in several forms. See the https://micronaut-projects.github.io/micronaut-spring/latest/guide[Micronaut Spring Documentation] for more information. diff --git a/src/main/docs/guide/ioc/types.adoc b/src/main/docs/guide/ioc/types.adoc index d8b9bb1ab74..7bda3020f1b 100644 --- a/src/main/docs/guide/ioc/types.adoc +++ b/src/main/docs/guide/ioc/types.adoc @@ -1,4 +1,4 @@ -In addition to being able to inject beans, Micronaut natively supports injecting the following types: +In addition to being able to inject beans, Micronaut framework natively supports injecting the following types: .Injectable Container Types |=== @@ -64,7 +64,7 @@ TIP: A prototype bean will have one instance created per place the bean is injec === Collection Ordering -When injecting a collection of beans, they are not ordered by default. Implement the api:core.order.Ordered[] interface to inject an ordered collection. If the requested bean type does not implement api:core.order.Ordered[], Micronaut searches for the ann:core.annotation.Order[] annotation on beans. +When injecting a collection of beans, they are not ordered by default. Implement the api:core.order.Ordered[] interface to inject an ordered collection. If the requested bean type does not implement api:core.order.Ordered[], Micronaut framework searches for the ann:core.annotation.Order[] annotation on beans. The ann:core.annotation.Order[] annotation is especially useful for ordering beans created by factories where the bean type is a class in a third-party library. In this example, both `LowRateLimit` and `HighRateLimit` implement the `RateLimit` interface. diff --git a/src/main/docs/guide/languageSupport.adoc b/src/main/docs/guide/languageSupport.adoc index 196faede379..a02e0e0aac2 100644 --- a/src/main/docs/guide/languageSupport.adoc +++ b/src/main/docs/guide/languageSupport.adoc @@ -1,4 +1,4 @@ -Micronaut supports any JVM language that implements the https://docs.oracle.com/javase/8/docs/api/javax/annotation/processing/package-summary.html[Java Annotation Processor] API. +Micronaut framework supports any JVM language that implements the https://docs.oracle.com/javase/8/docs/api/javax/annotation/processing/package-summary.html[Java Annotation Processor] API. Although Groovy does not support this API, special support has been built using AST transformations. The current list of supported languages is: Java, Groovy, and Kotlin (via the `kapt` tool). diff --git a/src/main/docs/guide/languageSupport/graal.adoc b/src/main/docs/guide/languageSupport/graal.adoc index 4527d7e67e2..a0634ce0410 100644 --- a/src/main/docs/guide/languageSupport/graal.adoc +++ b/src/main/docs/guide/languageSupport/graal.adoc @@ -2,7 +2,7 @@ https://www.graalvm.org[GraalVM] is a new universal virtual machine from Oracle Any Micronaut application can be run using the GraalVM JVM, however special support has been added to Micronaut to support running Micronaut applications using https://www.graalvm.org/reference-manual/native-image/[GraalVM's `native-image` tool]. -Micronaut currently supports GraalVM version {graalVersion} and the team is improving the support in every new release. Don't hesitate to https://github.com/micronaut-projects/micronaut-core/issues[report issues] however if you find any problem. +Micronaut framework currently supports GraalVM version {graalVersion} and the team is improving the support in every new release. Don't hesitate to https://github.com/micronaut-projects/micronaut-core/issues[report issues] however if you find any problem. Many of Micronaut's modules and third-party libraries have been verified to work with GraalVM: HTTP server, HTTP client, Function support, Micronaut Data JDBC and JPA, Service Discovery, RabbitMQ, Views, Security, Zipkin, etc. Support for other modules is evolving and will improve over time. diff --git a/src/main/docs/guide/languageSupport/graal/graalFAQ.adoc b/src/main/docs/guide/languageSupport/graal/graalFAQ.adoc index 59015c7fcdc..35bc459ec97 100644 --- a/src/main/docs/guide/languageSupport/graal/graalFAQ.adoc +++ b/src/main/docs/guide/languageSupport/graal/graalFAQ.adoc @@ -1,6 +1,6 @@ -==== How does Micronaut manage to run on GraalVM? +==== How does Micronaut framework manage to run on GraalVM? -Micronaut features a Dependency Injection and Aspect-Oriented Programming runtime that uses no reflection. This makes it easier for Micronaut applications to run on GraalVM since there are https://github.com/oracle/graal/blob/master/docs/reference-manual/native-image/Compatibility.md[compatibility] concerns particularly around https://github.com/oracle/graal/blob/master/docs/reference-manual/native-image/Reflection.md[reflection] in Native Images. +The Micronaut framework features a Dependency Injection and Aspect-Oriented Programming runtime that uses no reflection. This makes it easier for Micronaut applications to run on GraalVM since there are https://github.com/oracle/graal/blob/master/docs/reference-manual/native-image/Compatibility.md[compatibility] concerns particularly around https://github.com/oracle/graal/blob/master/docs/reference-manual/native-image/Reflection.md[reflection] in Native Images. ==== How can I make a Micronaut application that uses picocli run on GraalVM? @@ -8,7 +8,7 @@ Picocli provides a `picocli-codegen` module with a tool for generating a GraalVM ==== What about other Third-Party Libraries? -Micronaut cannot guarantee that third-party libraries work on GraalVM SubstrateVM, that is down to each individual library to implement support. +The Micronaut framework cannot guarantee that third-party libraries work on GraalVM SubstrateVM, that is down to each individual library to implement support. ==== I Get a "Class XXX is instantiated reflectively..." Exception. What do I do? diff --git a/src/main/docs/guide/languageSupport/graal/graalServices.adoc b/src/main/docs/guide/languageSupport/graal/graalServices.adoc index f02324c2f97..d82df0a1c53 100644 --- a/src/main/docs/guide/languageSupport/graal/graalServices.adoc +++ b/src/main/docs/guide/languageSupport/graal/graalServices.adoc @@ -1,6 +1,6 @@ -=== Getting Started with Micronaut and GraalVM +=== Getting Started with the Micronaut framework and GraalVM -Since Micronaut 2.2, any Micronaut application is ready to be built into a native image using the Micronaut Gradle or Maven plugins. To get started, create a new application: +Since Micronaut framework 2.2, any Micronaut application is ready to be built into a native image using the Micronaut Gradle or Maven plugins. To get started, create a new application: .Creating a GraalVM Native Microservice [source,bash] @@ -75,11 +75,11 @@ You can then run the native image from the directory where you built it. $ ./hello-world ---- -=== Understanding Micronaut and GraalVM +=== Understanding Micronaut framework and GraalVM -Micronaut itself does not rely on reflection or dynamic classloading, so it works automatically with GraalVM native, however certain third-party libraries used by Micronaut may require additional input about uses of reflection. +The Micronaut framework itself does not rely on reflection or dynamic classloading, so it works automatically with GraalVM native, however certain third-party libraries used by Micronaut may require additional input about uses of reflection. -Micronaut includes an annotation processor that helps to generate reflection configuration that is automatically picked up by the `native-image` tool: +The Micronaut framework includes an annotation processor that helps to generate reflection configuration that is automatically picked up by the `native-image` tool: dependency:micronaut-graal[scope="annotationProcessor"] @@ -105,7 +105,7 @@ If you have more advanced requirements and only wish to include certain fields o === Adding Additional Classes for Reflective Access -To inform Micronaut of additional classes to be included in the generated reflection configuration a number of annotations are available including: +To inform the Micronaut framework of additional classes to be included in the generated reflection configuration a number of annotations are available including: * ann:core.annotation.ReflectiveAccess[] - An annotation that can be declared on a specific type, constructor, method or field to enable reflective access just for the annotated element. * ann:core.annotation.TypeHint[] - An annotation that allows to bulk configuration of reflective access to one or many types @@ -177,4 +177,4 @@ As you can see, the native image startup completes in milliseconds, and memory c === Resource file generation -Starting in Micronaut 3.0 the automatic generation of the `resource-config.json` file is now part of the https://github.com/micronaut-projects/micronaut-gradle-plugin[Gradle] and https://github.com/micronaut-projects/micronaut-maven-plugin[Maven] plugins. +Starting in Micronaut framework 3.0 the automatic generation of the `resource-config.json` file is now part of the https://github.com/micronaut-projects/micronaut-gradle-plugin[Gradle] and https://github.com/micronaut-projects/micronaut-maven-plugin[Maven] plugins. diff --git a/src/main/docs/guide/languageSupport/groovy.adoc b/src/main/docs/guide/languageSupport/groovy.adoc index c62bbdc3f08..d0e11ec5d23 100644 --- a/src/main/docs/guide/languageSupport/groovy.adoc +++ b/src/main/docs/guide/languageSupport/groovy.adoc @@ -67,7 +67,7 @@ class HelloBean { WARNING: Groovy automatically imports `groovy.lang.Singleton` which can be confusing as it conflicts with `javax.inject.Singleton`. Make sure you use `javax.inject.Singleton` when declaring a Micronaut singleton bean to avoid surprising behavior. -We can also create a client - don't forget Micronaut can act as a client or a server! +We can also create a client - don't forget Micronaut framework can act as a client or a server! [source,bash] .Create a client @@ -139,7 +139,7 @@ As you can see from the output from the CLI, a https://spockframework.org[Spock] ... ---- -Notice how you use Micronaut both as client and as a server to test itself. +Notice how you use the Micronaut framework both as client and as a server to test itself. == Programmatic Routes with GroovyRouterBuilder @@ -209,6 +209,6 @@ You can also define the service as an interface instead of an abstract class to A microservice application is just one way to use Micronaut. You can also use it for serverless functions like on AWS Lambda. -With the `function-groovy` module, Micronaut features enhanced support for functions written in Groovy. +With the `function-groovy` module, the Micronaut framework features enhanced support for functions written in Groovy. See the section on <> for more information. diff --git a/src/main/docs/guide/languageSupport/java.adoc b/src/main/docs/guide/languageSupport/java.adoc index 7f8c0c4d25a..635177a5c51 100644 --- a/src/main/docs/guide/languageSupport/java.adoc +++ b/src/main/docs/guide/languageSupport/java.adoc @@ -1,3 +1,3 @@ -For Java, Micronaut uses a Java api:annotation.processing.BeanDefinitionInjectProcessor[] annotation processor to process classes at compile time and produce api:inject.BeanDefinition[] classes. +For Java, Micronaut framework uses a Java api:annotation.processing.BeanDefinitionInjectProcessor[] annotation processor to process classes at compile time and produce api:inject.BeanDefinition[] classes. -The major advantage here is that you pay a slight cost at compile time, but at runtime Micronaut is largely reflection-free, fast, and consumes very little memory. +The major advantage here is that you pay a slight cost at compile time, but at runtime Micronaut framework is largely reflection-free, fast, and consumes very little memory. diff --git a/src/main/docs/guide/languageSupport/java/incrementalannotationgradle.adoc b/src/main/docs/guide/languageSupport/java/incrementalannotationgradle.adoc index bec3a2ecffc..c1dccef6669 100644 --- a/src/main/docs/guide/languageSupport/java/incrementalannotationgradle.adoc +++ b/src/main/docs/guide/languageSupport/java/incrementalannotationgradle.adoc @@ -1,6 +1,6 @@ -Micronaut supports https://docs.gradle.org/current/userguide/java_plugin.html#sec:incremental_annotation_processing[Gradle incremental annotation processing] which speeds up builds by compiling only classes that have changed, avoiding a full recompilation. +The Micronaut framework supports https://docs.gradle.org/current/userguide/java_plugin.html#sec:incremental_annotation_processing[Gradle incremental annotation processing] which speeds up builds by compiling only classes that have changed, avoiding a full recompilation. -However, the support is disabled by default since Micronaut allows the definition of custom meta-annotations (to for example define <>) that need to be configured for processing. +However, the support is disabled by default since the Micronaut framework allows the definition of custom meta-annotations (to for example define <>) that need to be configured for processing. The following example demonstrates how to enable and configure incremental annotation processing for annotations you have defined under the `com.example` package: diff --git a/src/main/docs/guide/languageSupport/java/java9.adoc b/src/main/docs/guide/languageSupport/java/java9.adoc index 6ecc417212d..44e84d8f38f 100644 --- a/src/main/docs/guide/languageSupport/java/java9.adoc +++ b/src/main/docs/guide/languageSupport/java/java9.adoc @@ -1,4 +1,4 @@ -Micronaut is built with Java 8 but works fine with Java 9 and above. The classes that Micronaut generates sit alongside existing classes in the same package, hence do not violate anything regarding the Java module system. +The Micronaut framework is built with Java 8 but works fine with Java 9 and above. The classes that Micronaut generates sit alongside existing classes in the same package, hence do not violate anything regarding the Java module system. There are some considerations when using Java 9+ with Micronaut. diff --git a/src/main/docs/guide/languageSupport/java/lombok.adoc b/src/main/docs/guide/languageSupport/java/lombok.adoc index 26550f90efa..c5bec9fbeb0 100644 --- a/src/main/docs/guide/languageSupport/java/lombok.adoc +++ b/src/main/docs/guide/languageSupport/java/lombok.adoc @@ -1,6 +1,6 @@ https://projectlombok.org[Project Lombok] is a popular java library that adds a number of useful AST transformations to the Java language via annotation processors. -Since both Micronaut and Lombok use annotation processors, special care must be taken when configuring Lombok to ensure that the Lombok processor runs *before* Micronaut's processor. +Since both the Micronaut framework and Lombok use annotation processors, special care must be taken when configuring Lombok to ensure that the Lombok processor runs *before* Micronaut's processor. If you use Gradle, add the following dependencies: diff --git a/src/main/docs/guide/languageSupport/java/retainparameternames.adoc b/src/main/docs/guide/languageSupport/java/retainparameternames.adoc index 942b06f0840..e4d7e2e4369 100644 --- a/src/main/docs/guide/languageSupport/java/retainparameternames.adoc +++ b/src/main/docs/guide/languageSupport/java/retainparameternames.adoc @@ -1,4 +1,4 @@ -By default with Java, the parameter name data for method parameters is not retained at compile time. This can be a problem for Micronaut if you do not define parameter names explicitly and depend on an external JAR that is already compiled. +By default with Java, the parameter name data for method parameters is not retained at compile time. This can be a problem for the Micronaut framework if you do not define parameter names explicitly and depend on an external JAR that is already compiled. Consider this interface: diff --git a/src/main/docs/guide/languageSupport/kotlin.adoc b/src/main/docs/guide/languageSupport/kotlin.adoc index cee774c6563..a17ae9f2ece 100644 --- a/src/main/docs/guide/languageSupport/kotlin.adoc +++ b/src/main/docs/guide/languageSupport/kotlin.adoc @@ -1,4 +1,4 @@ -TIP: The <> for Micronaut includes special support for Kotlin. To create a Kotlin application use the `kotlin` lang option. For example: +TIP: The <> for Micronaut framework includes special support for Kotlin. To create a Kotlin application use the `kotlin` lang option. For example: [source,bash] .Create a Micronaut Kotlin application @@ -6,7 +6,7 @@ TIP: The <> for Micronaut includes special support $ mn create-app hello-world --lang kotlin ---- -Since the 4.0 release, Micronaut Framework offers support for Kotlin via https://kotlinlang.org/docs/reference/kapt.html[Kapt] or https://kotlinlang.org/docs/ksp-overview.html[Kotlin Symbol Processing API]. +Since the 4.0 release, Micronaut framework offers support for Kotlin via https://kotlinlang.org/docs/reference/kapt.html[Kapt] or https://kotlinlang.org/docs/ksp-overview.html[Kotlin Symbol Processing API]. diff --git a/src/main/docs/guide/languageSupport/kotlin/coroutineTracingContextPropagation.adoc b/src/main/docs/guide/languageSupport/kotlin/coroutineTracingContextPropagation.adoc index 27fa062e6fe..d881548a35d 100644 --- a/src/main/docs/guide/languageSupport/kotlin/coroutineTracingContextPropagation.adoc +++ b/src/main/docs/guide/languageSupport/kotlin/coroutineTracingContextPropagation.adoc @@ -1,4 +1,4 @@ -Micronaut supports tracing context propagation. If you use `suspend` functions all the way from your controller actions down to all your services, +The Micronaut framework supports tracing context propagation. If you use `suspend` functions all the way from your controller actions down to all your services, you don't have to do anything special. However, when you create coroutines within a regular function, tracing propagation won't happen automatically. You have to use a `HttpCoroutineContextFactory` to create a new `CoroutineTracingDispatcher` and use it as a `CoroutineContext`. diff --git a/src/main/docs/guide/languageSupport/kotlin/kaptOrKsp.adoc b/src/main/docs/guide/languageSupport/kotlin/kaptOrKsp.adoc index 20850c1cdff..8aa13a9094c 100644 --- a/src/main/docs/guide/languageSupport/kotlin/kaptOrKsp.adoc +++ b/src/main/docs/guide/languageSupport/kotlin/kaptOrKsp.adoc @@ -1,6 +1,6 @@ -Micronaut Framework has offered support for Kotlin via https://kotlinlang.org/docs/reference/kapt.html[Kapt]. +Micronaut framework has offered support for Kotlin via https://kotlinlang.org/docs/reference/kapt.html[Kapt]. -With version 4.0, Micronaut Framework supports Kotlin also via https://kotlinlang.org/docs/ksp-overview.html[Kotlin Symbol Processing (KSP) API]. +With version 4.0, Micronaut framework supports Kotlin also via https://kotlinlang.org/docs/ksp-overview.html[Kotlin Symbol Processing (KSP) API]. Please note that KAPT is in maintenance mode. Micronaut framework 4 includes experimental support for KSP which Kotlin users should consider migrating in the future. diff --git a/src/main/docs/guide/languageSupport/kotlin/kaptintellij.adoc b/src/main/docs/guide/languageSupport/kotlin/kaptintellij.adoc index 17136c1e596..98e21ac514b 100644 --- a/src/main/docs/guide/languageSupport/kotlin/kaptintellij.adoc +++ b/src/main/docs/guide/languageSupport/kotlin/kaptintellij.adoc @@ -8,7 +8,7 @@ Then add the `classes` task as task to execute for the application or for tests image::kotlin-run-2.png[Intellij Settings,1024,768] -Now when you run tests or start the application, Micronaut will generate classes at compile time. +Now when you run tests or start the application, the Micronaut framework will generate classes at compile time. Alternatively, you can https://www.jetbrains.com/help/idea/gradle.html#delegate_build_gradle[delegate IntelliJ build/run actions to Gradle] completely: diff --git a/src/main/docs/guide/languageSupport/kotlin/kotlinContextPropagation.adoc b/src/main/docs/guide/languageSupport/kotlin/kotlinContextPropagation.adoc index 68462e113ea..daa22bfca2f 100644 --- a/src/main/docs/guide/languageSupport/kotlin/kotlinContextPropagation.adoc +++ b/src/main/docs/guide/languageSupport/kotlin/kotlinContextPropagation.adoc @@ -1,4 +1,4 @@ -Micronaut supports context propagation from Reactor's context to coroutine context. To enable this propagation you need to include following dependency: +The Micronaut framework supports context propagation from Reactor's context to coroutine context. To enable this propagation you need to include following dependency: dependency:org.jetbrains.kotlinx:kotlinx-coroutines-reactor[] diff --git a/src/main/docs/guide/languageSupport/kotlin/kotlinretainparamnames.adoc b/src/main/docs/guide/languageSupport/kotlin/kotlinretainparamnames.adoc index 978d7b42438..ab5b1ad5b9b 100644 --- a/src/main/docs/guide/languageSupport/kotlin/kotlinretainparamnames.adoc +++ b/src/main/docs/guide/languageSupport/kotlin/kotlinretainparamnames.adoc @@ -1,4 +1,4 @@ -Like with Java, the parameter name data for method parameters is not retained at compile time when using Kotlin. This can be a problem for Micronaut if you do not define parameter names explicitly and depend on an external JAR that is already compiled. +Like with Java, the parameter name data for method parameters is not retained at compile time when using Kotlin. This can be a problem for the Micronaut framework if you do not define parameter names explicitly and depend on an external JAR that is already compiled. To enable retention of parameter name data with Kotlin, set the `javaParameters` option to `true` in your `build.gradle`: @@ -13,7 +13,7 @@ compileTestKotlin { } ---- -NOTE: If you use interfaces with default methods add `freeCompilerArgs = ["-Xjvm-default=all"]` for Micronaut to recognize them. +NOTE: If you use interfaces with default methods add `freeCompilerArgs = ["-Xjvm-default=all"]` for the Micronaut framework to recognize them. Or if using Maven configure the Micronaut Maven Plugin accordingly: diff --git a/src/main/docs/guide/languageSupport/kotlin/openandaop.adoc b/src/main/docs/guide/languageSupport/kotlin/openandaop.adoc index ac5bc21733d..dd298fa2754 100644 --- a/src/main/docs/guide/languageSupport/kotlin/openandaop.adoc +++ b/src/main/docs/guide/languageSupport/kotlin/openandaop.adoc @@ -1,4 +1,4 @@ -Micronaut provides a compile-time AOP API that does not use reflection. When you use any Micronaut <>, it creates a subclass at compile-time to provide the AOP behaviour. This can be a problem because Kotlin classes are final by default. If the application was created with the Micronaut CLI, the Kotlin link:https://kotlinlang.org/docs/reference/compiler-plugins.html#all-open-compiler-plugin[all-open] plugin is configured for you to automatically change your classes to `open` when an AOP annotation is used. To configure it yourself, add the api:io.micronaut.aop.Around[] class to the list of supported annotations. +The Micronaut framework provides a compile-time AOP API that does not use reflection. When you use any Micronaut <>, it creates a subclass at compile-time to provide the AOP behaviour. This can be a problem because Kotlin classes are final by default. If the application was created with the Micronaut CLI, the Kotlin link:https://kotlinlang.org/docs/reference/compiler-plugins.html#all-open-compiler-plugin[all-open] plugin is configured for you to automatically change your classes to `open` when an AOP annotation is used. To configure it yourself, add the api:io.micronaut.aop.Around[] class to the list of supported annotations. If you prefer not to or cannot use the `all-open` plugin, you must declare the classes that are annotated with an AOP annotation to be open: diff --git a/src/main/docs/guide/logging.adoc b/src/main/docs/guide/logging.adoc index cb93392de81..29fce045374 100644 --- a/src/main/docs/guide/logging.adoc +++ b/src/main/docs/guide/logging.adoc @@ -1 +1 @@ -Micronaut uses https://www.slf4j.org/[Slf4j] to log messages. The default implementation for applications created via Micronaut Launch is https://logback.qos.ch/[Logback]. Any other Slf4j implementation is supported however. +The Micronaut framework uses https://www.slf4j.org/[Slf4j] to log messages. The default implementation for applications created via Micronaut Launch is https://logback.qos.ch/[Logback]. Any other Slf4j implementation is supported however. diff --git a/src/main/docs/guide/logging/loggingConfiguration.adoc b/src/main/docs/guide/logging/loggingConfiguration.adoc index 84fa68a8a77..bf75e3172b1 100644 --- a/src/main/docs/guide/logging/loggingConfiguration.adoc +++ b/src/main/docs/guide/logging/loggingConfiguration.adoc @@ -32,4 +32,4 @@ logger: - This will disable ALL logging for the class `io.verbose.logger.who.CriedWolf` -Note that the ability to control log levels via config is controlled via the api:logging.LoggingSystem[] interface. Currently, Micronaut includes a single implementation that allows setting log levels for the Logback library. If you use another library, you should provide a bean that implements this interface. +Note that the ability to control log levels via config is controlled via the api:logging.LoggingSystem[] interface. Currently, the Micronaut framework includes a single implementation that allows setting log levels for the Logback library. If you use another library, you should provide a bean that implements this interface. diff --git a/src/main/docs/guide/logging/loggingSystem.adoc b/src/main/docs/guide/logging/loggingSystem.adoc index ff262239e62..d1141da7644 100644 --- a/src/main/docs/guide/logging/loggingSystem.adoc +++ b/src/main/docs/guide/logging/loggingSystem.adoc @@ -1 +1 @@ -The Micronaut Framework has a notion of a logging system. In short, it is a simple API to be able to set log levels in the logging implementation at runtime. Default implementations are provided for Logback and Log4j2. The behavior of the logging system can be overridden by creating your own implementation of api:logging.LoggingSystem[] and replace the implementation being used with the ann:context.annotation.Replaces[] annotation. +The Micronaut framework has a notion of a logging system. In short, it is a simple API to be able to set log levels in the logging implementation at runtime. Default implementations are provided for Logback and Log4j2. The behavior of the logging system can be overridden by creating your own implementation of api:logging.LoggingSystem[] and replace the implementation being used with the ann:context.annotation.Replaces[] annotation. diff --git a/src/main/docs/guide/management/providedEndpoints.adoc b/src/main/docs/guide/management/providedEndpoints.adoc index 8ffc1661c23..d73eec92e24 100644 --- a/src/main/docs/guide/management/providedEndpoints.adoc +++ b/src/main/docs/guide/management/providedEndpoints.adoc @@ -76,4 +76,4 @@ In the above example the management endpoints are exposed only over port 8085. === JMX -Micronaut provides functionality to register endpoints with JMX. See the section on <> to get started. +The Micronaut framework provides functionality to register endpoints with JMX. See the section on <> to get started. diff --git a/src/main/docs/guide/management/providedEndpoints/healthEndpoint.adoc b/src/main/docs/guide/management/providedEndpoints/healthEndpoint.adoc index 40b3bbe09f2..f12f0e3e5af 100644 --- a/src/main/docs/guide/management/providedEndpoints/healthEndpoint.adoc +++ b/src/main/docs/guide/management/providedEndpoints/healthEndpoint.adoc @@ -83,7 +83,7 @@ The "worst" status is returned as the overall status. A non-operational status i == Provided Indicators -All Micronaut provided health indicators are exposed on /health and /health/readiness endpoints. +All the Micronaut framework provided health indicators are exposed on /health and /health/readiness endpoints. === Disk Space diff --git a/src/main/docs/guide/management/providedEndpoints/metricsEndpoint.adoc b/src/main/docs/guide/management/providedEndpoints/metricsEndpoint.adoc index 5e041385b24..2ef310bd9e3 100644 --- a/src/main/docs/guide/management/providedEndpoints/metricsEndpoint.adoc +++ b/src/main/docs/guide/management/providedEndpoints/metricsEndpoint.adoc @@ -1,4 +1,4 @@ -Micronaut can expose application metrics via integration with https://micrometer.io[Micrometer]. +The Micronaut framework can expose application metrics via integration with https://micrometer.io[Micrometer]. [TIP] .Using the CLI diff --git a/src/main/docs/guide/messaging/kafka.adoc b/src/main/docs/guide/messaging/kafka.adoc index 1d6d3b1a82d..bdf63a8e87d 100644 --- a/src/main/docs/guide/messaging/kafka.adoc +++ b/src/main/docs/guide/messaging/kafka.adoc @@ -1,6 +1,6 @@ https://kafka.apache.org[Apache Kafka] is a distributed stream processing platform that can be used for a range of messaging requirements in addition to stream processing and real-time data handling. -Micronaut features dedicated support for defining both Kafka `Producer` and `Consumer` instances. Micronaut applications built with Kafka can be deployed with or without the presence of an HTTP server. +The Micronaut framework features dedicated support for defining both Kafka `Producer` and `Consumer` instances. Micronaut applications built with Kafka can be deployed with or without the presence of an HTTP server. With Micronaut's efficient compile-time AOP and cloud native features, writing efficient Kafka consumer applications that use very little resources is a breeze. diff --git a/src/main/docs/guide/messaging/nats.adoc b/src/main/docs/guide/messaging/nats.adoc index cf8eb2b9e7a..78f5428ef91 100644 --- a/src/main/docs/guide/messaging/nats.adoc +++ b/src/main/docs/guide/messaging/nats.adoc @@ -1,6 +1,6 @@ https://nats.io/[Nats.io] is a simple, secure, and high-performance open source messaging system for cloud native applications, IoT messaging, and microservices architectures. -Micronaut features dedicated support for defining both Nats.io publishers and consumers. Micronaut applications built with Nats.io can be deployed with or without an HTTP server. +The Micronaut framework features dedicated support for defining both Nats.io publishers and consumers. Micronaut applications built with Nats.io can be deployed with or without an HTTP server. With Micronaut's efficient compile-time AOP, using Nats.io has never been easier. Support has been added for publisher confirms and RPC through reactive streams. diff --git a/src/main/docs/guide/messaging/rabbitmq.adoc b/src/main/docs/guide/messaging/rabbitmq.adoc index c8d9b321077..36a73dbe68a 100644 --- a/src/main/docs/guide/messaging/rabbitmq.adoc +++ b/src/main/docs/guide/messaging/rabbitmq.adoc @@ -1,7 +1,7 @@ https://www.rabbitmq.com[RabbitMQ] is the most widely deployed open source message broker. -Micronaut features dedicated support for defining both RabbitMQ publishers and consumers. Micronaut applications built with RabbitMQ can be deployed with or without an HTTP server. +The Micronaut framework features dedicated support for defining both RabbitMQ publishers and consumers. Micronaut applications built with RabbitMQ can be deployed with or without an HTTP server. -With Micronaut's efficient compile-time AOP, using RabbitMQ has never been easier. Support has been added for publisher confirms and RPC through reactive streams. +With Micronaut framework's efficient compile-time AOP, using RabbitMQ has never been easier. Support has been added for publisher confirms and RPC through reactive streams. See the documentation for https://micronaut-projects.github.io/micronaut-rabbitmq/latest/guide[Micronaut RabbitMQ] for more information on how to build RabbitMQ applications with Micronaut. diff --git a/src/main/docs/guide/quickStart.adoc b/src/main/docs/guide/quickStart.adoc index 69d75e83423..b6e408530fa 100644 --- a/src/main/docs/guide/quickStart.adoc +++ b/src/main/docs/guide/quickStart.adoc @@ -1,3 +1,3 @@ -The following sections walk you through a Quick Start on how to use Micronaut to setup a basic "Hello World" application. +The following sections walk you through a Quick Start on how to use the Micronaut framework to setup a basic "Hello World" application. Before getting started ensure you have a Java 8 or higher JDK installed, and it is recommended that you use a <> such as IntelliJ IDEA. diff --git a/src/main/docs/guide/quickStart/creatingClient.adoc b/src/main/docs/guide/quickStart/creatingClient.adoc index f6ececb5c05..12a3d900c64 100644 --- a/src/main/docs/guide/quickStart/creatingClient.adoc +++ b/src/main/docs/guide/quickStart/creatingClient.adoc @@ -1,4 +1,4 @@ -As mentioned previously, Micronaut includes both an <> and an <>. A <> is provided which you can use to test the `HelloController` created in the previous section. +As mentioned previously, the Micronaut framework includes both an <> and an <>. A <> is provided which you can use to test the `HelloController` created in the previous section. .Testing Hello World @@ -9,7 +9,7 @@ snippet::io.micronaut.docs.server.intro.HelloControllerSpec[tags="imports,class" <3> The test uses the `toBlocking()` method to make a blocking call <4> The `retrieve` method returns the controller response as a `String` -In addition to a low-level client, Micronaut features a <>, powered by the api:http.client.annotation.Client[] annotation. +In addition to a low-level client, the Micronaut framework features a <>, powered by the api:http.client.annotation.Client[] annotation. To create a client, create an interface annotated with `@Client`, for example: diff --git a/src/main/docs/guide/quickStart/ideSetup/eclipseSetup.adoc b/src/main/docs/guide/quickStart/ideSetup/eclipseSetup.adoc index a1d7236b517..373bc17e971 100644 --- a/src/main/docs/guide/quickStart/ideSetup/eclipseSetup.adoc +++ b/src/main/docs/guide/quickStart/ideSetup/eclipseSetup.adoc @@ -1,6 +1,6 @@ To use Eclipse IDE, it is recommended you import your Micronaut project into Eclipse using either https://projects.eclipse.org/projects/tools.buildship[Gradle BuildShip] for Gradle or https://www.eclipse.org/m2e/[M2Eclipse] for Maven. -NOTE: Micronaut requires Eclipse IDE 4.9 or higher +NOTE: The Micronaut framework requires Eclipse IDE 4.9 or higher === Eclipse and Gradle diff --git a/src/main/docs/guide/quickStart/ideSetup/vsCodeSetup.adoc b/src/main/docs/guide/quickStart/ideSetup/vsCodeSetup.adoc index 47debf0f40b..e90d8e3f011 100644 --- a/src/main/docs/guide/quickStart/ideSetup/vsCodeSetup.adoc +++ b/src/main/docs/guide/quickStart/ideSetup/vsCodeSetup.adoc @@ -1,4 +1,4 @@ -Micronaut can be set up within Visual Studio Code in one of two ways. +The Micronaut framework can be set up within Visual Studio Code in one of two ways. ==== Option 1) GraalVM Extension Pack for Java diff --git a/src/main/docs/guide/security.adoc b/src/main/docs/guide/security.adoc index de2c3701160..a7cdc8a443a 100644 --- a/src/main/docs/guide/security.adoc +++ b/src/main/docs/guide/security.adoc @@ -1,3 +1,3 @@ -Micronaut has a full-featured security solution for all of the common security patterns. +The Micronaut framework has a full-featured security solution for all of the common security patterns. See the documentation for link:https://micronaut-projects.github.io/micronaut-security/latest/guide/[Micronaut Security] for more information on how to secure your applications. diff --git a/src/main/docs/guide/serverlessFunctions.adoc b/src/main/docs/guide/serverlessFunctions.adoc index 1d5d0e384ff..c72bd65275a 100644 --- a/src/main/docs/guide/serverlessFunctions.adoc +++ b/src/main/docs/guide/serverlessFunctions.adoc @@ -2,7 +2,7 @@ Serverless architectures, where you deploy functions that are fully managed by a Traditional frameworks like Grails and Spring are not really suitable since low memory consumption and fast startup time are critical, since the Function as a Service (FaaS) server typically spins up your function for a period using a cold start and then keeps it warm. -Micronaut's compile-time approach, fast startup time, and low memory footprint make it a great candidate for developing functions, and Micronaut includes dedicated support for developing and deploying functions to AWS Lambda, Google Cloud Function, Azure Function, and any FaaS system that supports running functions as containers (such as OpenFaaS, Rift or Fn). +Micronaut's compile-time approach, fast startup time, and low memory footprint make it a great candidate for developing functions, and the Micronaut framework includes dedicated support for developing and deploying functions to AWS Lambda, Google Cloud Function, Azure Function, and any FaaS system that supports running functions as containers (such as OpenFaaS, Rift or Fn). There are generally two approaches to writing functions with Micronaut: diff --git a/src/main/docs/guide/serverlessFunctions/awsLambda.adoc b/src/main/docs/guide/serverlessFunctions/awsLambda.adoc index 542e197018a..edc5ad49c46 100644 --- a/src/main/docs/guide/serverlessFunctions/awsLambda.adoc +++ b/src/main/docs/guide/serverlessFunctions/awsLambda.adoc @@ -2,7 +2,7 @@ Support for AWS Lambda is implemented in the https://micronaut-projects.github.i ==== Simple Functions with AWS Lambda -You can implement AWS Request Handlers with Micronaut that directly implement the AWS Lambda SDK API. See the documentation on https://micronaut-projects.github.io/micronaut-aws/latest/guide/#requestHandlers[Micronaut Request Handlers] for more information. +You can implement AWS Request Handlers with the Micronaut framework that directly implement the AWS Lambda SDK API. See the documentation on https://micronaut-projects.github.io/micronaut-aws/latest/guide/#requestHandlers[Micronaut Request Handlers] for more information. [TIP] .Using the CLI diff --git a/src/main/docs/guide/serverlessFunctions/azureFunction.adoc b/src/main/docs/guide/serverlessFunctions/azureFunction.adoc index 0ced2436545..244db53f4f1 100644 --- a/src/main/docs/guide/serverlessFunctions/azureFunction.adoc +++ b/src/main/docs/guide/serverlessFunctions/azureFunction.adoc @@ -2,7 +2,7 @@ Support for Azure Function is implemented in the https://micronaut-projects.gith ==== Simple Functions with Azure Function -You can implement Azure Functions with Micronaut that directly implement the https://docs.microsoft.com/en-us/azure/azure-functions/functions-reference-java?tabs=consumption[Azure Function Java SDK]. See the documentation on https://micronaut-projects.github.io/micronaut-azure/1.0.x/guide/index.html#azureFunction[Azure Functions] for more information. +You can implement Azure Functions with the Micronaut framework that directly implement the https://docs.microsoft.com/en-us/azure/azure-functions/functions-reference-java?tabs=consumption[Azure Function Java SDK]. See the documentation on https://micronaut-projects.github.io/micronaut-azure/1.0.x/guide/index.html#azureFunction[Azure Functions] for more information. [TIP] .Using the CLI diff --git a/src/main/docs/guide/serverlessFunctions/gcpFunction.adoc b/src/main/docs/guide/serverlessFunctions/gcpFunction.adoc index cde8a3d8f90..61f78eee3a8 100644 --- a/src/main/docs/guide/serverlessFunctions/gcpFunction.adoc +++ b/src/main/docs/guide/serverlessFunctions/gcpFunction.adoc @@ -2,7 +2,7 @@ Support for Google Cloud Function is implemented in the https://micronaut-projec ==== Simple Functions with Cloud Function -You can implement Cloud Functions with Micronaut that directly implement the https://github.com/GoogleCloudPlatform/functions-framework-java[Cloud Function Framework API]. See the documentation on https://micronaut-projects.github.io/micronaut-gcp/latest/guide/#simpleFunctions[Simple Functions] for more information. +You can implement Cloud Functions with the Micronaut framework that directly implement the https://github.com/GoogleCloudPlatform/functions-framework-java[Cloud Function Framework API]. See the documentation on https://micronaut-projects.github.io/micronaut-gcp/latest/guide/#simpleFunctions[Simple Functions] for more information. [TIP] .Using the CLI diff --git a/src/main/docs/guide/spring.adoc b/src/main/docs/guide/spring.adoc index 86e2c2f71b9..ddcdf2f8080 100644 --- a/src/main/docs/guide/spring.adoc +++ b/src/main/docs/guide/spring.adoc @@ -1,2 +1,2 @@ -Micronaut provides support for adding Micronaut beans to a +The Micronaut framework provides support for adding Micronaut beans to a Spring Application Context. From d649a04a1928bb87352a03dee13707d5fcaf7b0f Mon Sep 17 00:00:00 2001 From: Denis Stepanov Date: Wed, 22 Mar 2023 15:14:12 -0600 Subject: [PATCH 618/743] Correct KSP parameters matching (#8989) --- .../processing/annotation/KotlinAnnotationMetadataBuilder.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/annotation/KotlinAnnotationMetadataBuilder.kt b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/annotation/KotlinAnnotationMetadataBuilder.kt index 7c693b934e0..3c247593e42 100644 --- a/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/annotation/KotlinAnnotationMetadataBuilder.kt +++ b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/annotation/KotlinAnnotationMetadataBuilder.kt @@ -224,7 +224,7 @@ internal class KotlinAnnotationMetadataBuilder(private val symbolProcessorEnviro } else { val parameters = parent.parameters val parameterIndex = - parameters.indexOf(parameters.find { it.name == element.name }) + parameters.indexOf(parameters.find { it.name!!.asString() == element.name!!.asString() }) methodsHierarchy(parent) .map { if (it == parent) element else it.parameters[parameterIndex] } .toMutableList() From 9b92751a1820605aa6ce603afbe7bd008fa759e8 Mon Sep 17 00:00:00 2001 From: Denis Stepanov Date: Wed, 22 Mar 2023 15:14:58 -0600 Subject: [PATCH 619/743] Correct KotlinClassElement's `isAssignable` (#8988) --- .../test/AbstractKotlinCompilerSpec.groovy | 15 +++++++++ .../processing/visitor/KotlinClassElement.kt | 17 +++++----- .../inject/ast/ClassElementSpec.groovy | 32 +++++++++++++++++++ 3 files changed, 56 insertions(+), 8 deletions(-) diff --git a/inject-kotlin-test/src/main/groovy/io/micronaut/annotation/processing/test/AbstractKotlinCompilerSpec.groovy b/inject-kotlin-test/src/main/groovy/io/micronaut/annotation/processing/test/AbstractKotlinCompilerSpec.groovy index 3234495d6e3..e2d224fe346 100644 --- a/inject-kotlin-test/src/main/groovy/io/micronaut/annotation/processing/test/AbstractKotlinCompilerSpec.groovy +++ b/inject-kotlin-test/src/main/groovy/io/micronaut/annotation/processing/test/AbstractKotlinCompilerSpec.groovy @@ -113,6 +113,21 @@ class AbstractKotlinCompilerSpec extends Specification { return elements.first() } + /** + * Builds a class element for the given source code. + * @param cls The source + * @return The class element + */ + V buildClassElementMapped(String className, @Language("kotlin") String cls, @NonNull Function processor) { + List elements = [] + KotlinCompiler.compile(className, cls, { + if (it.name == className) { + elements.add(processor.apply(it)) + } + }) + return elements.first() + } + Object getBean(ApplicationContext context, String className, Qualifier qualifier = null) { context.getBean(context.classLoader.loadClass(className), qualifier) } diff --git a/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/KotlinClassElement.kt b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/KotlinClassElement.kt index 165bb981fb2..5311a5c1a95 100644 --- a/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/KotlinClassElement.kt +++ b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/KotlinClassElement.kt @@ -489,16 +489,17 @@ internal open class KotlinClassElement( return true } } + return isAssignable2(type) + } + + // Second attempt to check if the class is assignable, the method is public for testing + @OptIn(KspExperimental::class) + fun isAssignable2(type: String): Boolean { val kotlinName = visitorContext.resolver.mapJavaNameToKotlin( visitorContext.resolver.getKSNameFromString(type) - ) - if (kotlinName != null) { - val kotlinClassByName = visitorContext.resolver.getKotlinClassByName(kotlinName) - if (kotlinClassByName != null && kotlinType.starProjection().isAssignableFrom(kotlinClassByName.asStarProjectedType())) { - return true - } - } - return false + ) ?: return false + val kotlinClassByName = visitorContext.resolver.getKotlinClassByName(kotlinName) ?: return false + return kotlinClassByName.asStarProjectedType().isAssignableFrom(kotlinType.starProjection()) } override fun isAssignable(type: ClassElement): Boolean { diff --git a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/inject/ast/ClassElementSpec.groovy b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/inject/ast/ClassElementSpec.groovy index 7eb7e96acca..f792783d5d2 100644 --- a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/inject/ast/ClassElementSpec.groovy +++ b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/inject/ast/ClassElementSpec.groovy @@ -1853,6 +1853,38 @@ class Test { ce.findMethod("helloWorld\$main").isPresent() } + void "test type isAssignable"() { + boolean isAssignable = buildClassElementMapped('test.Test', ''' +package test +import io.micronaut.context.annotation.Executable +import io.micronaut.context.annotation.Prototype +import jakarta.inject.Singleton +import java.util.List + +@Prototype +class Test { + @Executable + fun method1() : kotlin.collections.List { + return listOf() + } + + @Executable + fun method2() : java.util.List? { + return null + } +} + +''', ce -> { + return ce.findMethod("method1").get().getReturnType().isAssignable(Iterable.class) + && ce.findMethod("method2").get().getReturnType().isAssignable(Iterable.class) + && ((KotlinClassElement) ce.findMethod("method1").get().getReturnType()).isAssignable2(Iterable.class.name) + && ((KotlinClassElement) ce.findMethod("method2").get().getReturnType()).isAssignable2(Iterable.class.name) + }) + + expect: + isAssignable + } + private void assertListGenericArgument(ClassElement type, Closure cl) { def arg1 = type.getAllTypeArguments().get(List.class.name).get("E") From fff00e9ec0a959fb7b0c7dea4f631bfc88ee8f26 Mon Sep 17 00:00:00 2001 From: Dean Wette Date: Thu, 23 Mar 2023 04:10:57 -0500 Subject: [PATCH 620/743] add missing logback.xml (#8987) closes #8985 --- test-suite-graal/src/test/resources/logback.xml | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 test-suite-graal/src/test/resources/logback.xml diff --git a/test-suite-graal/src/test/resources/logback.xml b/test-suite-graal/src/test/resources/logback.xml new file mode 100644 index 00000000000..8eb8c3a8170 --- /dev/null +++ b/test-suite-graal/src/test/resources/logback.xml @@ -0,0 +1,10 @@ + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + From 091f10ca641d563330c24406f77d3bab1d894078 Mon Sep 17 00:00:00 2001 From: Jonas Konrad Date: Fri, 24 Mar 2023 14:07:54 +0100 Subject: [PATCH 621/743] Fix race condition in FilterRunner (#9005) Before this change, processRequestFilter returned nextFilterProcessing, which is completed when the downstream filters should be called (e.g. when a legacy filter subscribes to the publisher returned by chain.proceed). The return value of processRequestFilter was "subscribed to" by filterRequest, which would trigger downstream filters when nextFilterProcessing completes. The problem is that the "subscription" (i.e. the flatMap call) and the "completion" (i.e. the subscribe) could happen on different threads, as would happen with the test `io.micronaut.http.server.netty.filters.FiltersSpec#test filters order and threads with subscribeOn for #method`. This could lead to a race condition where whichever thread came second would actually execute the downstream filters. The test asserts that the downstream filters are run on the io executor, but this would sometimes fail. This behavior is intended and unfortunately I believe there are filters that actually rely on this subscribeOn behavior. This patch changes the downstream subscriptions like flatMap to happen *before* the doFilter method is ever called. This ensures that the completion of nextFilterProcessing always happens after the "subscription". This also fixes `around filter with blocking continuation`. --- .../netty/stack/InvocationStackSpec.groovy | 2 +- .../micronaut/http/filter/FilterRunner.java | 69 ++++++++++--------- .../http/filter/FilterRunnerSpec.groovy | 2 - 3 files changed, 38 insertions(+), 35 deletions(-) diff --git a/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/stack/InvocationStackSpec.groovy b/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/stack/InvocationStackSpec.groovy index 11cc06dbc0d..2d1939b1cb5 100644 --- a/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/stack/InvocationStackSpec.groovy +++ b/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/stack/InvocationStackSpec.groovy @@ -178,7 +178,7 @@ class InvocationStackSpec extends Specification { } boolean isKnownStack(String className, boolean allowExecutor) { - if (allowExecutor && className.startsWith("java.util.concurrent")) { + if (className.startsWith("java.util.concurrent")) { return true } if (className.startsWith("io.netty")) { diff --git a/http/src/main/java/io/micronaut/http/filter/FilterRunner.java b/http/src/main/java/io/micronaut/http/filter/FilterRunner.java index 225e556d937..e7619accc1b 100644 --- a/http/src/main/java/io/micronaut/http/filter/FilterRunner.java +++ b/http/src/main/java/io/micronaut/http/filter/FilterRunner.java @@ -180,13 +180,18 @@ public final ExecutionFlow> run(HttpRequest request) { private ExecutionFlow> filterRequest(FilterContext context, ListIterator iterator, Map, FilterContinuationImpl>> suspended) { - return filterRequest0(context, iterator, suspended) - .flatMap(newContext -> { - if (newContext.response != null) { - return filterResponse(newContext, iterator, null, suspended); - } - return ExecutionFlow.error(new IllegalStateException("Request filters didn't produce any response!")); - }); + GenericHttpFilter filter = iterator.next(); + return processRequestFilter(filter, context, suspended, f -> f.flatMap(newContext -> filterRequest0(newContext, iterator, suspended)) + .onErrorResume(throwable -> { + // Un-suspend possibly awaiting filter and exception filtering scenario of the http client + return filterResponse(context, iterator, throwable, suspended).map(context::withResponse); + }) + .flatMap(newContext -> { + if (newContext.response != null) { + return filterResponse(newContext, iterator, null, suspended); + } + return ExecutionFlow.error(new IllegalStateException("Request filters didn't produce any response!")); + })); } private ExecutionFlow filterRequest0(FilterContext context, @@ -197,12 +202,11 @@ private ExecutionFlow filterRequest0(FilterContext context, } if (iterator.hasNext()) { GenericHttpFilter filter = iterator.next(); - return processRequestFilter(filter, context, suspended) - .flatMap(newContext -> filterRequest0(newContext, iterator, suspended)) - .onErrorResume(throwable -> { - // Un-suspend possibly awaiting filter and exception filtering scenario of the http client - return filterResponse(context, iterator, throwable, suspended).map(context::withResponse); - }); + return processRequestFilter(filter, context, suspended, f -> f.flatMap(newContext -> filterRequest0(newContext, iterator, suspended)) + .onErrorResume(throwable -> { + // Un-suspend possibly awaiting filter and exception filtering scenario of the http client + return filterResponse(context, iterator, throwable, suspended).map(context::withResponse); + })); } else { return ExecutionFlow.error(new IllegalStateException("Request filters didn't produce any response!")); } @@ -241,10 +245,11 @@ private ExecutionFlow> filterResponse(FilterContext context, "java:S2259", // false positive "java:S1181" // this is a framework not an application }) - private ExecutionFlow processRequestFilter(GenericHttpFilter filter, + private ExecutionFlow processRequestFilter(GenericHttpFilter filter, FilterContext context, Map, - FilterContinuationImpl>> suspended) { + FilterContinuationImpl>> suspended, + Function, ExecutionFlow> downstream) { Executor executeOn; if (filter instanceof GenericHttpFilter.Async async) { executeOn = async.executor(); @@ -256,10 +261,19 @@ private ExecutionFlow processRequestFilter(GenericHttpFilter filt if (filter instanceof FilterMethod before) { if (before.isResponseFilter) { // skip filter, only used for response - return ExecutionFlow.just(context); + return downstream.apply(ExecutionFlow.just(context)); } ExecutionFlow filterMethodFlow; - FilterContinuationImpl continuation = before.isSuspended() ? before.createContinuation(context) : null; + FilterContinuationImpl continuation; + ExecutionFlow downstreamFlow; + if (before.isSuspended()) { + continuation = before.createContinuation(context); + downstreamFlow = downstream.apply(continuation.nextFilterFlow()); + suspended.put(filter, Map.entry(continuation.filterProcessedFlow(), continuation)); + } else { + continuation = null; + downstreamFlow = null; + } FilterMethodContext filterMethodContext = new FilterMethodContext( context.request, context.response, @@ -275,32 +289,23 @@ private ExecutionFlow processRequestFilter(GenericHttpFilter filt filterMethodFlow = ExecutionFlow.async(executeOn, () -> before.filter(context, filterMethodContext)); } if (before.isSuspended()) { - if (continuation instanceof ReactiveResultAwareReactiveContinuationImpl) { - // Method consumes reactive continuation and returns reactive result - suspended.put(filter, Map.entry(continuation.filterProcessedFlow(), continuation)); - } else if (continuation instanceof ReactiveContinuationImpl) { - // Method consumes reactive continuation and doesn't return reactive result - throw new IllegalStateException("Not supported use-case with reactive continuation and non-reactive return type"); - } else { - // Method consumes blocking continuation - suspended.put(filter, Map.entry(filterMethodFlow, continuation)); - } // Continue executing other filters while this one is suspended - return continuation.nextFilterFlow(); + return downstreamFlow; } - return filterMethodFlow; + return downstream.apply(filterMethodFlow); } else if (filter instanceof GenericHttpFilter.AroundLegacy around) { FilterChainImpl chainSuspensionPoint = new FilterChainImpl(conversionService, context); // Legacy `Publisher proceed(..)` filters are always suspended suspended.put(around, Map.entry(chainSuspensionPoint.filterProcessedFlow(), chainSuspensionPoint)); chainSuspensionPoint.completeOn = executeOn; + ExecutionFlow downstreamFlow = downstream.apply(chainSuspensionPoint.nextFilterFlow()); if (executeOn == null) { try { around.bean().doFilter(context.request, chainSuspensionPoint).subscribe(chainSuspensionPoint); } catch (Throwable e) { chainSuspensionPoint.triggerFilterProcessed(context, null, e); } - return chainSuspensionPoint.nextFilterFlow(); + return downstreamFlow; } else { return ExecutionFlow.async(executeOn, () -> { try { @@ -308,7 +313,7 @@ private ExecutionFlow processRequestFilter(GenericHttpFilter filt } catch (Throwable e) { chainSuspensionPoint.triggerFilterProcessed(context, null, e); } - return chainSuspensionPoint.nextFilterFlow(); + return downstreamFlow; }); } } else if (filter instanceof GenericHttpFilter.TerminalReactive || filter instanceof GenericHttpFilter.Terminal || filter instanceof GenericHttpFilter.TerminalWithReactorContext) { @@ -335,7 +340,7 @@ private ExecutionFlow processRequestFilter(GenericHttpFilter filt terminalFlow = ReactiveExecutionFlow.fromPublisher(Mono.from(((GenericHttpFilter.TerminalReactive) filter).responsePublisher()) .contextWrite(context.reactorContext)); } - return terminalFlow.flatMap(response -> ExecutionFlow.just(context.withResponse(response))); + return downstream.apply(terminalFlow.flatMap(response -> ExecutionFlow.just(context.withResponse(response)))); } else { throw new IllegalStateException("Unknown filter type"); } diff --git a/http/src/test/groovy/io/micronaut/http/filter/FilterRunnerSpec.groovy b/http/src/test/groovy/io/micronaut/http/filter/FilterRunnerSpec.groovy index 8368b93d7a2..14ddf3d3b3b 100644 --- a/http/src/test/groovy/io/micronaut/http/filter/FilterRunnerSpec.groovy +++ b/http/src/test/groovy/io/micronaut/http/filter/FilterRunnerSpec.groovy @@ -15,7 +15,6 @@ import io.micronaut.inject.ExecutableMethod import org.reactivestreams.Publisher import reactor.core.publisher.Flux import reactor.util.context.Context -import spock.lang.Ignore import spock.lang.Specification import java.lang.reflect.Method @@ -612,7 +611,6 @@ class FilterRunnerSpec extends Specification { events == ["before1 thread-outside", "before2 thread-before", "before3 thread-before", "terminal thread-before", "after3 thread-before", "after2 thread-after", "after1 thread-after"] } - @Ignore def 'around filter with blocking continuation'() { given: def events = [] From c2bdadbedfbd7cf492f44d087ce31822724f6b3c Mon Sep 17 00:00:00 2001 From: Dean Wette Date: Fri, 24 Mar 2023 08:08:49 -0500 Subject: [PATCH 622/743] Make Kotlin and Kotlin coroutines versions managed. (#8995) closes #8991 --- aop/build.gradle | 2 +- core-processor/build.gradle | 2 +- core/build.gradle | 2 +- gradle/libs.versions.toml | 34 +++++++++++++++++------------- http-client-core/build.gradle | 2 +- http-server-netty/build.gradle | 2 +- http-server/build.gradle | 4 ++-- http/build.gradle | 4 ++-- inject-kotlin-test/build.gradle | 4 ++-- inject-kotlin/build.gradle | 8 +++---- inject/build.gradle | 2 +- runtime/build.gradle | 4 ++-- test-suite-kotlin-ksp/build.gradle | 22 +++++++++---------- test-suite-kotlin/build.gradle | 24 ++++++++++----------- 14 files changed, 60 insertions(+), 56 deletions(-) diff --git a/aop/build.gradle b/aop/build.gradle index 900459d6e42..67e9695581d 100644 --- a/aop/build.gradle +++ b/aop/build.gradle @@ -13,7 +13,7 @@ dependencies { api project(':inject') api project(':core') compileOnly project(':core-reactive') - compileOnly libs.kotlinx.coroutines.core + compileOnly libs.managed.kotlinx.coroutines.core } tasks.named("compileKotlin") { diff --git a/core-processor/build.gradle b/core-processor/build.gradle index 7667825f58a..2847cdcbcca 100644 --- a/core-processor/build.gradle +++ b/core-processor/build.gradle @@ -12,5 +12,5 @@ dependencies { exclude group:'com.google.guava', module:'guava' } - compileOnly libs.kotlin.stdlib.jdk8 + compileOnly libs.managed.kotlin.stdlib.jdk8 } diff --git a/core/build.gradle b/core/build.gradle index 84f6e6288da..d3bd67cfc42 100644 --- a/core/build.gradle +++ b/core/build.gradle @@ -14,7 +14,7 @@ micronautBuild { dependencies { compileOnly libs.managed.jakarta.annotation.api compileOnly libs.graal - compileOnly libs.kotlin.stdlib + compileOnly libs.managed.kotlin.stdlib } spotless { diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index ac7fba287ce..6de85f907ee 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -31,8 +31,6 @@ jakarta-el-impl = "5.0.0-M1" jcache = "1.1.1" junit5 = "5.9.1" junit-platform="1.9.1" -kotlin = "1.8.10" -kotlin-coroutines = "1.6.4" ktor = "1.6.8" managed-logback = "1.4.6" logbook-netty = "2.14.0" @@ -64,6 +62,8 @@ managed-groovy = "4.0.10" managed-jakarta-annotation-api = "2.1.1" managed-jackson = "2.14.0" managed-jackson-databind = "2.14.1" +managed-kotlin = "1.8.10" +managed-kotlin-coroutines = "1.6.4" managed-maven-native-plugin = "0.9.13" managed-methvin-directory-watcher = "0.16.1" managed-netty = "4.1.87.Final" @@ -85,6 +85,8 @@ test-boms-micronaut-tracing = { module = "io.micronaut.tracing:micronaut-tracing test-boms-micronaut-validation = { module = "io.micronaut.validation:micronaut-validation-bom", version.ref = "micronaut-validation" } boms-groovy = { module = "org.apache.groovy:groovy-bom", version.ref = "managed-groovy" } +boms-kotlin = { module = "org.jetbrains.kotlin:kotlin-bom", version.ref = "managed-kotlin" } +boms-kotlin-coroutines = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-bom", version.ref = "managed-kotlin-coroutines" } boms-netty = { module = "io.netty:netty-bom", version.ref = "managed-netty" } boms-jackson = { module = "com.fasterxml.jackson:jackson-bom", version.ref = "managed-jackson" } @@ -108,6 +110,21 @@ managed-jackson-datatype-jsr310 = { module = "com.fasterxml.jackson.datatype:jac managed-jackson-module-afterburner = { module = "com.fasterxml.jackson.module:jackson-module-afterburner", version.ref = "managed-jackson" } managed-jackson-module-kotlin = { module = "com.fasterxml.jackson.module:jackson-module-kotlin", version.ref = "managed-jackson" } managed-jackson-module-parameterNames = { module = "com.fasterxml.jackson.module:jackson-module-parameter-names", version.ref = "managed-jackson" } + +managed-kotlin-annotation-processing-embeddable = { module = "org.jetbrains.kotlin:kotlin-annotation-processing-embeddable", version.ref = "managed-kotlin" } +managed-kotlin-compiler-embeddable = { module = "org.jetbrains.kotlin:kotlin-compiler-embeddable", version.ref = "managed-kotlin" } +managed-kotlin-reflect = { module = "org.jetbrains.kotlin:kotlin-reflect", version.ref = "managed-kotlin" } +managed-kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "managed-kotlin" } +managed-kotlin-stdlib-jdk8 = { module = "org.jetbrains.kotlin:kotlin-stdlib-jdk8", version.ref = "managed-kotlin" } +managed-kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "managed-kotlin" } + +managed-kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "managed-kotlin-coroutines" } +managed-kotlinx-coroutines-jdk8 = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-jdk8", version.ref = "managed-kotlin-coroutines" } +managed-kotlinx-coroutines-reactive = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-reactive", version.ref = "managed-kotlin-coroutines" } +managed-kotlinx-coroutines-rx2 = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-rx2", version.ref = "managed-kotlin-coroutines" } +managed-kotlinx-coroutines-slf4j = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-slf4j", version.ref = "managed-kotlin-coroutines" } +managed-kotlinx-coroutines-reactor = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-reactor", version.ref = "managed-kotlin-coroutines" } + managed-ksp-api = { module = "com.google.devtools.ksp:symbol-processing-api", version.ref = "managed-ksp" } managed-ksp = { module = "com.google.devtools.ksp:symbol-processing", version.ref = "managed-ksp" } managed-java-parser-core = { module = "com.github.javaparser:javaparser-symbol-solver-core", version.ref = "managed-java-parser-core" } @@ -192,20 +209,7 @@ junit-vintage = { module = "org.junit.vintage:junit-vintage-engine", version.ref jetbrains-annotations = { module = "org.jetbrains:annotations", version.ref = "jetbrains-annotations" } -kotlin-annotation-processing-embeddable = { module = "org.jetbrains.kotlin:kotlin-annotation-processing-embeddable", version.ref = "kotlin" } -kotlin-compiler-embeddable = { module = "org.jetbrains.kotlin:kotlin-compiler-embeddable", version.ref = "kotlin" } kotlin-kotest-junit5 = { module = "io.kotest:kotest-runner-junit5-jvm" } -kotlin-reflect = { module = "org.jetbrains.kotlin:kotlin-reflect", version.ref = "kotlin" } -kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "kotlin" } -kotlin-stdlib-jdk8 = { module = "org.jetbrains.kotlin:kotlin-stdlib-jdk8", version.ref = "kotlin" } -kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" } - -kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlin-coroutines" } -kotlinx-coroutines-jdk8 = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-jdk8", version.ref = "kotlin-coroutines" } -kotlinx-coroutines-reactive = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-reactive", version.ref = "kotlin-coroutines" } -kotlinx-coroutines-rx2 = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-rx2", version.ref = "kotlin-coroutines" } -kotlinx-coroutines-slf4j = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-slf4j", version.ref = "kotlin-coroutines" } -kotlinx-coroutines-reactor = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-reactor", version.ref = "kotlin-coroutines" } log4j = { module = "org.apache.logging.log4j:log4j-core", version.ref = "log4j" } diff --git a/http-client-core/build.gradle b/http-client-core/build.gradle index a3d5d2c140c..3d52f72e8b0 100644 --- a/http-client-core/build.gradle +++ b/http-client-core/build.gradle @@ -11,7 +11,7 @@ dependencies { api project(":json-core") api project(":discovery-core") - compileOnly libs.kotlin.stdlib + compileOnly libs.managed.kotlin.stdlib testImplementation project(":jackson-databind") } diff --git a/http-server-netty/build.gradle b/http-server-netty/build.gradle index b2dd9cf9229..d253b43786a 100644 --- a/http-server-netty/build.gradle +++ b/http-server-netty/build.gradle @@ -28,7 +28,7 @@ dependencies { implementation libs.managed.reactor compileOnly project(":jackson-databind") compileOnly project(":websocket") - compileOnly libs.kotlin.stdlib + compileOnly libs.managed.kotlin.stdlib compileOnly libs.managed.netty.transport.native.unix.common compileOnly libs.managed.netty.incubator.codec.http3 diff --git a/http-server/build.gradle b/http-server/build.gradle index 3752465053e..abdfd70d663 100644 --- a/http-server/build.gradle +++ b/http-server/build.gradle @@ -13,8 +13,8 @@ dependencies { compileOnly project(":websocket") compileOnly project(":jackson-databind") - compileOnly libs.kotlinx.coroutines.core - compileOnly libs.kotlinx.coroutines.reactor + compileOnly libs.managed.kotlinx.coroutines.core + compileOnly libs.managed.kotlinx.coroutines.reactor compileOnly(libs.micronaut.runtime.groovy) implementation libs.managed.reactor annotationProcessor project(":inject-java") diff --git a/http/build.gradle b/http/build.gradle index 82a85694853..f4646ef2521 100644 --- a/http/build.gradle +++ b/http/build.gradle @@ -9,8 +9,8 @@ dependencies { api project(":context") api project(":core-reactive") implementation libs.managed.reactor - compileOnly libs.kotlinx.coroutines.core - compileOnly libs.kotlinx.coroutines.reactor + compileOnly libs.managed.kotlinx.coroutines.core + compileOnly libs.managed.kotlinx.coroutines.reactor compileOnly libs.managed.jackson.annotations diff --git a/inject-kotlin-test/build.gradle b/inject-kotlin-test/build.gradle index e84b043181e..62adee10da4 100644 --- a/inject-kotlin-test/build.gradle +++ b/inject-kotlin-test/build.gradle @@ -14,13 +14,13 @@ dependencies { } api(libs.managed.ksp.api) api(libs.managed.ksp) - implementation(libs.kotlin.compiler.embeddable) + implementation(libs.managed.kotlin.compiler.embeddable) implementation "com.squareup.okio:okio:3.2.0" implementation "io.github.classgraph:classgraph:4.8.149" testImplementation libs.javax.persistence testImplementation project(":runtime") api libs.blaze.persistence.core - implementation libs.kotlin.stdlib + implementation libs.managed.kotlin.stdlib } tasks.named("sourcesJar") { diff --git a/inject-kotlin/build.gradle b/inject-kotlin/build.gradle index 070c68aa5d4..7262401dd69 100644 --- a/inject-kotlin/build.gradle +++ b/inject-kotlin/build.gradle @@ -27,7 +27,7 @@ dependencies { testImplementation project(":jackson-databind") testImplementation project(":inject-kotlin-test") - testImplementation libs.kotlin.stdlib + testImplementation libs.managed.kotlin.stdlib testImplementation project(':http-client') testImplementation libs.managed.jackson.annotations testImplementation libs.managed.reactor @@ -42,9 +42,9 @@ dependencies { testImplementation libs.javax.persistence testImplementation project(":runtime") testImplementation(libs.neo4j.bolt) - testImplementation libs.kotlinx.coroutines.core - testImplementation libs.kotlinx.coroutines.jdk8 - testImplementation libs.kotlinx.coroutines.rx2 + testImplementation libs.managed.kotlinx.coroutines.core + testImplementation libs.managed.kotlinx.coroutines.jdk8 + testImplementation libs.managed.kotlinx.coroutines.rx2 testImplementation (libs.micronaut.test.junit5) { exclude group: 'io.micronaut' } diff --git a/inject/build.gradle b/inject/build.gradle index 8a108a1b950..dd0b65b1e58 100644 --- a/inject/build.gradle +++ b/inject/build.gradle @@ -18,7 +18,7 @@ dependencies { compileOnly libs.managed.snakeyaml compileOnly libs.managed.groovy - compileOnly libs.kotlin.stdlib.jdk8 + compileOnly libs.managed.kotlin.stdlib.jdk8 testImplementation project(":context") testImplementation project(":inject-groovy") diff --git a/runtime/build.gradle b/runtime/build.gradle index 445fdcc2b05..05569ed098a 100644 --- a/runtime/build.gradle +++ b/runtime/build.gradle @@ -20,8 +20,8 @@ dependencies { compileOnly libs.jakarta.el compileOnly libs.caffeine - compileOnly libs.kotlinx.coroutines.core - compileOnly libs.kotlinx.coroutines.reactive + compileOnly libs.managed.kotlinx.coroutines.core + compileOnly libs.managed.kotlinx.coroutines.reactive testImplementation libs.managed.logback.classic testImplementation libs.managed.snakeyaml testAnnotationProcessor project(":inject-java") diff --git a/test-suite-kotlin-ksp/build.gradle b/test-suite-kotlin-ksp/build.gradle index cf99dd09659..b6d64411c56 100644 --- a/test-suite-kotlin-ksp/build.gradle +++ b/test-suite-kotlin-ksp/build.gradle @@ -24,21 +24,21 @@ repositories { } dependencies { - api libs.kotlin.stdlib - api libs.kotlin.reflect - api libs.kotlinx.coroutines.core - api libs.kotlinx.coroutines.jdk8 - api libs.kotlinx.coroutines.rx2 + api libs.managed.kotlin.stdlib + api libs.managed.kotlin.reflect + api libs.managed.kotlinx.coroutines.core + api libs.managed.kotlinx.coroutines.jdk8 + api libs.managed.kotlinx.coroutines.rx2 api project(':http-server-netty') api project(':http-client') api project(':runtime') testImplementation project(":context") - testImplementation libs.kotlin.test - testImplementation libs.kotlinx.coroutines.core - testImplementation libs.kotlinx.coroutines.rx2 - testImplementation libs.kotlinx.coroutines.slf4j - testImplementation libs.kotlinx.coroutines.reactor + testImplementation libs.managed.kotlin.test + testImplementation libs.managed.kotlinx.coroutines.core + testImplementation libs.managed.kotlinx.coroutines.rx2 + testImplementation libs.managed.kotlinx.coroutines.slf4j + testImplementation libs.managed.kotlinx.coroutines.reactor // Adding these for now since micronaut-test isnt resolving correctly ... probably need to upgrade gradle there too testImplementation libs.junit.jupiter.api @@ -86,7 +86,7 @@ dependencies { configurations.testRuntimeClasspath { resolutionStrategy.eachDependency { if (it.requested.group == 'org.jetbrains.kotlin') { - it.useVersion(libs.versions.kotlin.asProvider().get()) + it.useVersion(libs.versions.managed.kotlin.asProvider().get()) } } } diff --git a/test-suite-kotlin/build.gradle b/test-suite-kotlin/build.gradle index d54d5d3ae4b..3601da2ba3a 100644 --- a/test-suite-kotlin/build.gradle +++ b/test-suite-kotlin/build.gradle @@ -24,22 +24,22 @@ repositories { } dependencies { - api libs.kotlin.stdlib - api libs.kotlin.reflect - api libs.kotlinx.coroutines.core - api libs.kotlinx.coroutines.jdk8 - api libs.kotlinx.coroutines.rx2 + api libs.managed.kotlin.stdlib + api libs.managed.kotlin.reflect + api libs.managed.kotlinx.coroutines.core + api libs.managed.kotlinx.coroutines.jdk8 + api libs.managed.kotlinx.coroutines.rx2 api project(':http-server-netty') api project(':http-client') api project(':runtime') testImplementation project(":context") - testImplementation libs.kotlin.test - testImplementation libs.kotlinx.coroutines.core - testImplementation libs.kotlinx.coroutines.rx2 - testImplementation libs.kotlinx.coroutines.slf4j - testImplementation libs.kotlinx.coroutines.reactor - testImplementation libs.kotlinx.coroutines.reactive + testImplementation libs.managed.kotlin.test + testImplementation libs.managed.kotlinx.coroutines.core + testImplementation libs.managed.kotlinx.coroutines.rx2 + testImplementation libs.managed.kotlinx.coroutines.slf4j + testImplementation libs.managed.kotlinx.coroutines.reactor + testImplementation libs.managed.kotlinx.coroutines.reactive // Adding these for now since micronaut-test isnt resolving correctly ... probably need to upgrade gradle there too testImplementation libs.junit.jupiter.api @@ -89,7 +89,7 @@ dependencies { configurations.testRuntimeClasspath { resolutionStrategy.eachDependency { if (it.requested.group == 'org.jetbrains.kotlin') { - it.useVersion(libs.versions.kotlin.asProvider().get()) + it.useVersion(libs.versions.managed.kotlin.asProvider().get()) } } } From aa98255c5e2d273e21a96a0966d3db6efe47bc61 Mon Sep 17 00:00:00 2001 From: Jonas Konrad Date: Fri, 24 Mar 2023 15:07:53 +0100 Subject: [PATCH 623/743] More performance improvements (#8993) Only big change is the introduction of a copy on write map, based on HashMap. It's slightly faster than ConcurrentHashMap for reading. I think it will be useful elsewhere too. --- .../core/CopyOnWriteMapBenchmark.java | 83 +++++ .../micronaut/core/type/DefaultArgument.java | 11 + .../micronaut/core/util/CopyOnWriteMap.java | 296 ++++++++++++++++++ .../core/util/CopyOnWriteMapSpec.groovy | 96 ++++++ .../http/netty/NettyHttpHeaders.java | 25 ++ .../http/netty/NettyMutableHttpResponse.java | 19 +- .../DefaultHttpContentProcessorResolver.java | 29 +- .../http/server/netty/NettyHttpRequest.java | 51 ++- .../netty/NettyHttpResponseFactory.java | 8 +- .../server/netty/NettyRequestLifecycle.java | 14 +- .../netty/jackson/JsonContentProcessor.java | 58 ++-- .../DefaultJsonErrorHandlingSpec.groovy | 4 +- .../netty/binding/JsonBodyBindingSpec.groovy | 2 +- .../server/netty/cors/CorsFilterSpec.groovy | 8 +- .../http/server/cors/CorsFilter.java | 9 +- .../micronaut/http/server/cors/CorsUtil.java | 4 +- .../java/io/micronaut/http/HttpHeaders.java | 26 +- .../java/io/micronaut/http/HttpRequest.java | 27 +- .../java/io/micronaut/http/MediaType.java | 72 +++-- .../core/parser/JacksonCoreParserFactory.java | 2 +- .../json/convert/JsonConverterRegistrar.java | 24 +- 21 files changed, 737 insertions(+), 131 deletions(-) create mode 100644 benchmarks/src/jmh/java/io/micronaut/core/CopyOnWriteMapBenchmark.java create mode 100644 core/src/main/java/io/micronaut/core/util/CopyOnWriteMap.java create mode 100644 core/src/test/groovy/io/micronaut/core/util/CopyOnWriteMapSpec.groovy diff --git a/benchmarks/src/jmh/java/io/micronaut/core/CopyOnWriteMapBenchmark.java b/benchmarks/src/jmh/java/io/micronaut/core/CopyOnWriteMapBenchmark.java new file mode 100644 index 00000000000..2e49f1d39e5 --- /dev/null +++ b/benchmarks/src/jmh/java/io/micronaut/core/CopyOnWriteMapBenchmark.java @@ -0,0 +1,83 @@ +package io.micronaut.core; + +import io.micronaut.core.util.CopyOnWriteMap; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.Param; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.runner.Runner; +import org.openjdk.jmh.runner.RunnerException; +import org.openjdk.jmh.runner.options.Options; +import org.openjdk.jmh.runner.options.OptionsBuilder; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.TimeUnit; +import java.util.function.Function; + +public class CopyOnWriteMapBenchmark { + public static void main(String[] args) throws RunnerException { + Options opt = new OptionsBuilder() + .include(CopyOnWriteMapBenchmark.class.getName() + ".*") + .warmupIterations(3) + .measurementIterations(5) + .mode(Mode.AverageTime) + .timeUnit(TimeUnit.NANOSECONDS) + .forks(1) + //.addProfiler(LinuxPerfAsmProfiler.class) + .build(); + + new Runner(opt).run(); + } + + @Benchmark + public String get(S s) { + return s.map.get("foo"); + } + + @Benchmark + public String computeIfAbsent(S s) { + return s.map.computeIfAbsent("fizz", s.ciaUpdate); + } + + @Benchmark + public String getWithCheck(S s) { + String v = s.map.get("fizz"); + if (v == null) { + return s.map.computeIfAbsent("fizz", s.ciaUpdate); + } else { + return v; + } + } + + @State(Scope.Thread) + public static class S { + @Param({"CHM", "COW"}) + Type type; + @Param({"1", "2", "5", "10"}) + int load; + private Map map; + private Function ciaUpdate; + + @Setup + public void setUp() { + map = switch (type) { + case CHM -> new ConcurrentHashMap<>(16, 0.75f, 1); + case COW -> new CopyOnWriteMap<>(16); + }; + // doesn't really stress the collision avoidance algorithm but oh well + map.put("foo", "bar"); + for (int i = 0; i < load; i++) { + map.put("f" + i, "b" + i); + } + ciaUpdate = m -> "buzz" + m; + } + } + + public enum Type { + CHM, + COW, + } +} diff --git a/core/src/main/java/io/micronaut/core/type/DefaultArgument.java b/core/src/main/java/io/micronaut/core/type/DefaultArgument.java index 0661921ca96..921d4fc6801 100644 --- a/core/src/main/java/io/micronaut/core/type/DefaultArgument.java +++ b/core/src/main/java/io/micronaut/core/type/DefaultArgument.java @@ -74,6 +74,7 @@ public class DefaultArgument implements Argument, ArgumentCoercible { private final AnnotationMetadata annotationMetadata; private final boolean isTypeVar; private String namePrecalculated; + private Boolean reactive; /** * @param type The type @@ -225,6 +226,16 @@ public Class getType() { return type; } + @Override + public boolean isReactive() { + Boolean reactive = this.reactive; + if (reactive == null) { + reactive = Argument.super.isReactive(); + this.reactive = reactive; + } + return reactive; + } + @Override @NonNull public String getName() { diff --git a/core/src/main/java/io/micronaut/core/util/CopyOnWriteMap.java b/core/src/main/java/io/micronaut/core/util/CopyOnWriteMap.java new file mode 100644 index 00000000000..bb1b0431431 --- /dev/null +++ b/core/src/main/java/io/micronaut/core/util/CopyOnWriteMap.java @@ -0,0 +1,296 @@ +/* + * Copyright 2017-2023 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.core.util; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.NonNull; + +import java.util.AbstractMap; +import java.util.AbstractSet; +import java.util.BitSet; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.ThreadLocalRandom; +import java.util.function.BiConsumer; +import java.util.function.BiFunction; +import java.util.function.Function; + +/** + * Thread-safe map that is optimized for reads. Uses a normal {@link HashMap} that is copied on + * update operations. + * + * @param The key type + * @param The value type + */ +@Internal +public final class CopyOnWriteMap extends AbstractMap implements ConcurrentMap { + /** + * How many items to evict at a time, to make eviction a bit more efficient. + */ + static final int EVICTION_BATCH = 16; + /** + * Empty {@link HashMap} to avoid polymorphism. + */ + @SuppressWarnings("rawtypes") + private static final Map EMPTY = new HashMap(); + + private final int maxSizeWithEvictionMargin; + @SuppressWarnings("unchecked") + private volatile Map actual = EMPTY; + + public CopyOnWriteMap(int maxSize) { + int maxSizeWithEvictionMargin = maxSize + EVICTION_BATCH; + if (maxSizeWithEvictionMargin < 0) { + maxSizeWithEvictionMargin = Integer.MAX_VALUE; + } + this.maxSizeWithEvictionMargin = maxSizeWithEvictionMargin; + } + + @NonNull + @Override + public Set> entrySet() { + return new EntrySet(); + } + + @Override + public V get(Object key) { + return actual.get(key); + } + + @SuppressWarnings("unchecked") + @Override + public V getOrDefault(Object key, V defaultValue) { + return ((Map) actual).getOrDefault(key, defaultValue); + } + + @Override + public boolean containsKey(Object key) { + return actual.containsKey(key); + } + + @Override + public boolean containsValue(Object value) { + return actual.containsValue(value); + } + + @Override + public int size() { + return actual.size(); + } + + @SuppressWarnings("unchecked") + @Override + public synchronized void clear() { + actual = EMPTY; + } + + @Override + public void putAll(Map m) { + update(map -> { + map.putAll(m); + return null; + }); + } + + @Override + public V remove(Object key) { + return update(m -> m.remove(key)); + } + + @Override + public int hashCode() { + return actual.hashCode(); + } + + @SuppressWarnings("EqualsWhichDoesntCheckParameterClass") + @Override + public boolean equals(Object o) { + return actual.equals(o); + } + + @Override + public String toString() { + return actual.toString(); + } + + @Override + public void forEach(BiConsumer action) { + actual.forEach(action); + } + + private synchronized R update(Function, R> updater) { + Map next = new HashMap<>(actual); + R ret = updater.apply(next); + int newSize = next.size(); + if (newSize >= maxSizeWithEvictionMargin) { + // select some indices in the map to remove at random + BitSet toRemove = new BitSet(newSize); + for (int i = 0; i < EVICTION_BATCH; i++) { + setUnset(toRemove, ThreadLocalRandom.current().nextInt(newSize - i)); + } + // iterate over the map and remove those indices + Iterator iterator = next.entrySet().iterator(); + for (int i = 0; i < newSize; i++) { + iterator.next(); + if (toRemove.get(i)) { + iterator.remove(); + } + } + } + actual = next; + return ret; + } + + /** + * Set the bit at {@code index}, with the index only counting unset bits. e.g. setting index 0 + * when the first bit of the {@link BitSet} is already set would set the second bit (the first + * unset bit). + * + * @param set The bit set to modify + * @param index The index of the bit to set + */ + static void setUnset(BitSet set, int index) { + int i = 0; + while (true) { + int nextI = set.nextSetBit(i); + if (nextI == -1 || nextI > index) { + break; + } + i = nextI + 1; + index++; + } + set.set(index); + } + + @Override + public V put(K key, V value) { + return update(m -> m.put(key, value)); + } + + @Override + public boolean remove(@NonNull Object key, Object value) { + return update(m -> m.remove(key, value)); + } + + @Override + public boolean replace(@NonNull K key, @NonNull V oldValue, @NonNull V newValue) { + return update(m -> m.replace(key, oldValue, newValue)); + } + + @Override + public void replaceAll(BiFunction function) { + update(m -> { + m.replaceAll(function); + return null; + }); + } + + @Override + public V computeIfAbsent(K key, @NonNull Function mappingFunction) { + V present = get(key); + if (present != null) { + // fast path without sync + return present; + } else { + return update(m -> m.computeIfAbsent(key, mappingFunction)); + } + } + + @Override + public V computeIfPresent(K key, @NonNull BiFunction remappingFunction) { + return update(m -> m.computeIfPresent(key, remappingFunction)); + } + + @Override + public V compute(K key, @NonNull BiFunction remappingFunction) { + return update(m -> m.compute(key, remappingFunction)); + } + + @Override + public V merge(K key, @NonNull V value, @NonNull BiFunction remappingFunction) { + return update(m -> m.merge(key, value, remappingFunction)); + } + + @Override + public V putIfAbsent(@NonNull K key, V value) { + return update(m -> m.putIfAbsent(key, value)); + } + + @Override + public V replace(@NonNull K key, @NonNull V value) { + return update(m -> m.replace(key, value)); + } + + private class EntrySetIterator implements Iterator> { + final Iterator> itr = actual.entrySet().iterator(); + K lastKey; + + @Override + public boolean hasNext() { + return itr.hasNext(); + } + + @Override + public Entry next() { + Entry e = itr.next(); + lastKey = e.getKey(); + return new EntryImpl(e); + } + + @Override + public void remove() { + CopyOnWriteMap.this.remove(lastKey); + } + } + + private class EntryImpl implements Entry { + private final Entry entry; + + public EntryImpl(Entry entry) { + this.entry = entry; + } + + @Override + public K getKey() { + return entry.getKey(); + } + + @Override + public V getValue() { + return entry.getValue(); + } + + @Override + public V setValue(V value) { + return put(entry.getKey(), value); + } + } + + private class EntrySet extends AbstractSet> { + @Override + public Iterator> iterator() { + return new EntrySetIterator(); + } + + @Override + public int size() { + return actual.size(); + } + } +} diff --git a/core/src/test/groovy/io/micronaut/core/util/CopyOnWriteMapSpec.groovy b/core/src/test/groovy/io/micronaut/core/util/CopyOnWriteMapSpec.groovy new file mode 100644 index 00000000000..339b93fbfce --- /dev/null +++ b/core/src/test/groovy/io/micronaut/core/util/CopyOnWriteMapSpec.groovy @@ -0,0 +1,96 @@ +package io.micronaut.core.util + +import spock.lang.Specification + +@SuppressWarnings('GrEqualsBetweenInconvertibleTypes') +class CopyOnWriteMapSpec extends Specification { + def "simple ops"() { + given: + def map = new CopyOnWriteMap(Integer.MAX_VALUE) + + when: + map.put("foo", "bar") + then: + map == ["foo": "bar"] + map.containsKey("foo") + !map.containsKey("bar") + map.containsValue("bar") + !map.containsValue("foo") + map.size() == 1 + map.getOrDefault("foo", "baz") == "bar" + map.getOrDefault("fiz", "baz") == "baz" + map.hashCode() == ["foo": "bar"].hashCode() + map.toString() == "{foo=bar}" + + when: + map.remove("foo", "bar") + then: + map == [:] + + when: + map.putAll(["foo": "bar"]) + then: + map == ["foo": "bar"] + + when: + map.remove("foo") + then: + map == [:] + + when: + map.computeIfAbsent("foo", x -> "bar" + x) + map.computeIfAbsent("foo", x -> "baz" + x) + then: + map == ["foo": "barfoo"] + + when: + map.clear() + then: + map == [:] + } + + def setUnset() { + given: + BitSet bs = new BitSet() + + when: + CopyOnWriteMap.setUnset(bs, 0) + then: + bs.stream().toArray() == [0] + + when: + CopyOnWriteMap.setUnset(bs, 0) + then: + bs.stream().toArray() == [0, 1] + + when: + CopyOnWriteMap.setUnset(bs, 5) + then: + bs.stream().toArray() == [0, 1, 7] + + when: + CopyOnWriteMap.setUnset(bs, 1) + then: + bs.stream().toArray() == [0, 1, 3, 7] + } + + def eviction(def maxSize) { + given: + def map = new CopyOnWriteMap(maxSize) + + when: + for (int i = 0; i < maxSize + CopyOnWriteMap.EVICTION_BATCH - 1; i++) { + map.put("foo" + i, "bar" + i) + } + then: + map.size() == maxSize + CopyOnWriteMap.EVICTION_BATCH - 1 + + when: + map.put("foox", "barx") + then: + map.size() == maxSize + + where: + maxSize << [0, 5] + } +} diff --git a/http-netty/src/main/java/io/micronaut/http/netty/NettyHttpHeaders.java b/http-netty/src/main/java/io/micronaut/http/netty/NettyHttpHeaders.java index e777fb3eb37..e195b089ecb 100644 --- a/http-netty/src/main/java/io/micronaut/http/netty/NettyHttpHeaders.java +++ b/http-netty/src/main/java/io/micronaut/http/netty/NettyHttpHeaders.java @@ -38,6 +38,7 @@ import java.util.Collections; import java.util.List; import java.util.Optional; +import java.util.OptionalLong; import java.util.Set; import java.util.stream.Collectors; @@ -133,6 +134,12 @@ public String get(CharSequence name) { return nettyHeaders.get(name); } + @Override + public Optional findFirst(CharSequence name) { + // optimization to avoid ConversionService + return Optional.ofNullable(get(name)); + } + @Override public MutableHttpHeaders add(CharSequence header, CharSequence value) { nettyHeaders.add(header, value); @@ -256,4 +263,22 @@ public Optional contentType() { } return Optional.empty(); } + + @Override + public OptionalLong contentLength() { + // optimization to avoid ConversionService + Optional str = findFirst(HttpHeaderNames.CONTENT_LENGTH); + if (str.isPresent()) { + try { + return OptionalLong.of(Long.parseLong(str.get())); + } catch (NumberFormatException ignored) { + } + } + return OptionalLong.empty(); + } + + @Override + public Optional getOrigin() { + return findFirst(HttpHeaderNames.ORIGIN); + } } diff --git a/http-netty/src/main/java/io/micronaut/http/netty/NettyMutableHttpResponse.java b/http-netty/src/main/java/io/micronaut/http/netty/NettyMutableHttpResponse.java index 32c732b9883..2a9dc78b7fd 100644 --- a/http-netty/src/main/java/io/micronaut/http/netty/NettyMutableHttpResponse.java +++ b/http-netty/src/main/java/io/micronaut/http/netty/NettyMutableHttpResponse.java @@ -143,12 +143,27 @@ private NettyMutableHttpResponse(HttpVersion httpVersion, ConversionService conversionService) { this.httpVersion = httpVersion; this.httpResponseStatus = httpResponseStatus; - this.nettyHeaders = nettyHeaders; this.trailingNettyHeaders = trailingNettyHeaders; this.decoderResult = decoderResult; this.conversionService = conversionService; + + boolean hasHeaders = nettyHeaders != null; + if (!hasHeaders) { + nettyHeaders = new DefaultHttpHeaders(); + } + this.nettyHeaders = nettyHeaders; this.headers = new NettyHttpHeaders(nettyHeaders, conversionService); - setBody(body); + if (body == null) { + this.body = null; + this.optionalBody = Optional.empty(); + } else { + this.body = body; + this.optionalBody = Optional.of(body); + Optional mediaType = MediaType.fromType(body.getClass()); + if (mediaType.isPresent() && (!hasHeaders || !nettyHeaders.contains(HttpHeaderNames.CONTENT_TYPE))) { + contentType(mediaType.get()); + } + } } /** diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/DefaultHttpContentProcessorResolver.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/DefaultHttpContentProcessorResolver.java index 63ec94eb9a8..7ef1864547a 100644 --- a/http-server-netty/src/main/java/io/micronaut/http/server/netty/DefaultHttpContentProcessorResolver.java +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/DefaultHttpContentProcessorResolver.java @@ -23,6 +23,7 @@ import io.micronaut.core.io.buffer.ByteBuffer; import io.micronaut.core.type.Argument; import io.micronaut.core.util.CollectionUtils; +import io.micronaut.core.util.CopyOnWriteMap; import io.micronaut.http.HttpRequest; import io.micronaut.http.MediaType; import io.micronaut.http.annotation.Body; @@ -35,7 +36,8 @@ import java.io.InputStream; import java.util.Optional; import java.util.Set; -import java.util.function.Supplier; +import java.util.concurrent.ConcurrentMap; +import java.util.function.Function; /** @@ -55,8 +57,9 @@ class DefaultHttpContentProcessorResolver implements HttpContentProcessorResolve private static final Set> RAW_BODY_TYPES = CollectionUtils.setOf(String.class, byte[].class, ByteBuffer.class, InputStream.class); - private final BeanLocator beanLocator; private final BeanProvider serverConfiguration; + private final ConcurrentMap> subscriberFactoryCache = new CopyOnWriteMap<>(128); + private final Function> findSubscriberFactory; private NettyHttpServerConfiguration nettyServerConfiguration; /** @@ -65,8 +68,8 @@ class DefaultHttpContentProcessorResolver implements HttpContentProcessorResolve */ DefaultHttpContentProcessorResolver(BeanLocator beanLocator, BeanProvider serverConfiguration) { - this.beanLocator = beanLocator; this.serverConfiguration = serverConfiguration; + this.findSubscriberFactory = mt -> beanLocator.findBean(HttpContentSubscriberFactory.class, new ConsumesMediaTypeQualifier<>(mt)); } @Override @@ -117,20 +120,16 @@ public HttpContentProcessor resolve(@NonNull NettyHttpRequest request) { } private HttpContentProcessor resolve(NettyHttpRequest request, boolean rawBodyType) { - Supplier defaultHttpContentProcessor = () -> new DefaultHttpContentProcessor(request, getServerConfiguration()); - - if (rawBodyType) { - return defaultHttpContentProcessor.get(); - } else { + if (!rawBodyType) { Optional contentType = request.getContentType(); - return contentType - .flatMap(type -> - beanLocator.findBean(HttpContentSubscriberFactory.class, - new ConsumesMediaTypeQualifier<>(type)) - ).map(factory -> - factory.build(request) - ).orElseGet(defaultHttpContentProcessor); + if (contentType.isPresent()) { + Optional factory = subscriberFactoryCache.computeIfAbsent(contentType.get(), findSubscriberFactory); + if (factory.isPresent()) { + return factory.get().build(request); + } + } } + return new DefaultHttpContentProcessor(request, getServerConfiguration()); } private NettyHttpServerConfiguration getServerConfiguration() { diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/NettyHttpRequest.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/NettyHttpRequest.java index 96bca32ad2b..471fc0c1163 100644 --- a/http-server-netty/src/main/java/io/micronaut/http/server/netty/NettyHttpRequest.java +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/NettyHttpRequest.java @@ -157,14 +157,25 @@ public class NettyHttpRequest extends AbstractNettyHttpRequest implements private final HttpServerConfiguration serverConfiguration; private MutableConvertibleValues attributes; private NettyCookies nettyCookies; - private List receivedContent = new ArrayList<>(); - private Map receivedData = new LinkedHashMap<>(); + private final List receivedContent = new ArrayList<>(); + private final Map receivedData = new LinkedHashMap<>(); private T bodyUnwrapped; private Supplier> body; private RouteMatch matchedRoute; private boolean bodyRequired; + /** + * Set to {@code true} when the {@link #headers} may have been mutated. If this is not the case, + * we can cache some values. + */ + private boolean headersMutated = false; + private final long contentLength; + @Nullable + private final MediaType contentType; + @Nullable + private final String origin; + private final BodyConvertor bodyConvertor = newBodyConvertor(); /** @@ -194,6 +205,9 @@ public NettyHttpRequest(io.netty.handler.codec.http.HttpRequest nettyRequest, this.bodyUnwrapped = built; return Optional.ofNullable(built); }); + this.contentLength = headers.contentLength().orElse(-1); + this.contentType = headers.contentType().orElse(null); + this.origin = headers.getOrigin().orElse(null); } @Override @@ -275,6 +289,15 @@ public boolean isSecure() { return channelHandlerContext.pipeline().get(SslHandler.class) != null; } + @Override + public Optional getOrigin() { + if (headersMutated) { + return getHeaders().getOrigin(); + } else { + return Optional.ofNullable(origin); + } + } + @Override public HttpHeaders getHeaders() { return headers; @@ -588,7 +611,7 @@ protected Charset initCharset(Charset characterEncoding) { */ @Internal final boolean isFormOrMultipartData() { - MediaType ct = headers.contentType().orElse(null); + MediaType ct = getContentType().orElse(null); return ct != null && (ct.equals(MediaType.APPLICATION_FORM_URLENCODED_TYPE) || ct.equals(MediaType.MULTIPART_FORM_DATA_TYPE)); } @@ -597,10 +620,20 @@ final boolean isFormOrMultipartData() { */ @Internal final boolean isFormData() { - MediaType ct = headers.contentType().orElse(null); + MediaType ct = getContentType().orElse(null); return ct != null && (ct.equals(MediaType.APPLICATION_FORM_URLENCODED_TYPE)); } + @Override + public Optional getContentType() { + // this is better than the caching we can do in AbstractNettyHttpRequest + if (headersMutated) { + return headers.contentType(); + } else { + return Optional.ofNullable(contentType); + } + } + /** * Remove the current request from the context. * @@ -631,6 +664,15 @@ public Optional convert(ArgumentConversionContext conversionContext, Object valu }; } + @Override + public long getContentLength() { + if (headersMutated) { + return super.getContentLength(); + } else { + return contentLength; + } + } + /** * Mutable version of the request. */ @@ -677,6 +719,7 @@ public MutableHttpRequest body(T1 body) { @Override public MutableHttpHeaders getHeaders() { + headersMutated = true; return headers; } diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/NettyHttpResponseFactory.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/NettyHttpResponseFactory.java index 180b36cd1d3..763db9a4f29 100644 --- a/http-server-netty/src/main/java/io/micronaut/http/server/netty/NettyHttpResponseFactory.java +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/NettyHttpResponseFactory.java @@ -39,16 +39,12 @@ public class NettyHttpResponseFactory implements HttpResponseFactory { @Override public MutableHttpResponse ok(T body) { - MutableHttpResponse ok = new NettyMutableHttpResponse<>(ConversionService.SHARED); - - return body != null ? ok.body(body) : ok; + return new NettyMutableHttpResponse<>(HttpVersion.HTTP_1_1, HttpResponseStatus.OK, null, body, ConversionService.SHARED); } @Override public MutableHttpResponse status(HttpStatus status, T body) { - MutableHttpResponse ok = new NettyMutableHttpResponse<>(ConversionService.SHARED); - ok.status(status); - return body != null ? ok.body(body) : ok; + return ok(body).status(status); } @Override diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/NettyRequestLifecycle.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/NettyRequestLifecycle.java index 3d56eda33cf..634801b8f88 100644 --- a/http-server-netty/src/main/java/io/micronaut/http/server/netty/NettyRequestLifecycle.java +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/NettyRequestLifecycle.java @@ -50,7 +50,6 @@ import java.net.URL; import java.nio.file.Paths; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collection; import java.util.List; import java.util.Optional; @@ -166,11 +165,11 @@ private boolean shouldReadBody(RouteMatch routeMatch) { return false; } if (routeMatch instanceof MethodBasedRouteMatch methodBasedRouteMatch) { - if (Arrays.stream(methodBasedRouteMatch.getArguments()).anyMatch(argument -> MultipartBody.class.equals(argument.getType()))) { + if (hasArg(methodBasedRouteMatch, MultipartBody.class)) { // MultipartBody will subscribe to the request body in MultipartBodyArgumentBinder return false; } - if (Arrays.stream(methodBasedRouteMatch.getArguments()).anyMatch(argument -> HttpRequest.class.equals(argument.getType()))) { + if (hasArg(methodBasedRouteMatch, HttpRequest.class)) { // HttpRequest argument in the method return true; } @@ -185,6 +184,15 @@ private boolean shouldReadBody(RouteMatch routeMatch) { return !routeMatch.isExecutable(); } + private static boolean hasArg(MethodBasedRouteMatch methodBasedRouteMatch, Class type) { + for (Argument argument : methodBasedRouteMatch.getArguments()) { + if (argument.getType() == type) { + return true; + } + } + return false; + } + private static class StreamingDataSubscriber implements Subscriber { final DelayedExecutionFlow> completion = DelayedExecutionFlow.create(); private boolean completed = false; diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/jackson/JsonContentProcessor.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/jackson/JsonContentProcessor.java index 00648973a61..51a0fe0c217 100644 --- a/http-server-netty/src/main/java/io/micronaut/http/server/netty/jackson/JsonContentProcessor.java +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/jackson/JsonContentProcessor.java @@ -48,7 +48,8 @@ public class JsonContentProcessor extends AbstractHttpContentProcessor { private final JsonMapper jsonMapper; private final JsonCounter counter = new JsonCounter(); - private CompositeByteBuf buffer; + private ByteBuf singleBuffer; + private CompositeByteBuf compositeBuffer; /** * @param nettyHttpRequest The Netty Http request @@ -98,16 +99,24 @@ protected void onData(ByteBufHolder message, Collection out) throws Thro try { countLoop(out, content); } catch (Exception e) { - if (this.buffer != null) { - this.buffer.release(); - this.buffer = null; - } + releaseBuffers(); throw e; } finally { content.release(); } } + private void releaseBuffers() { + if (this.singleBuffer != null) { + this.singleBuffer.release(); + this.singleBuffer = null; + } + if (this.compositeBuffer != null) { + this.compositeBuffer.release(); + this.compositeBuffer = null; + } + } + private void countLoop(Collection out, ByteBuf content) throws IOException { long initialPosition = counter.position(); long bias = initialPosition - content.readerIndex(); @@ -116,49 +125,54 @@ private void countLoop(Collection out, ByteBuf content) throws IOExcepti JsonCounter.BufferRegion bufferRegion = counter.pollFlushedRegion(); if (bufferRegion != null) { long start = Math.max(initialPosition, bufferRegion.start()); - flush(out, content.retainedSlice( + buffer(content.retainedSlice( Math.toIntExact(start - bias), Math.toIntExact(bufferRegion.end() - start) )); + flush(out); } } if (counter.isBuffering()) { int currentBufferStart = Math.toIntExact(Math.max(initialPosition, counter.bufferStart()) - bias); - bufferForNextRun(content.retainedSlice(currentBufferStart, content.writerIndex() - currentBufferStart)); + content.readerIndex(currentBufferStart); + buffer(content.retain()); } } - private void bufferForNextRun(ByteBuf buffer) { - if (this.buffer == null) { - // number of components should not be too small to avoid unnecessary consolidation - this.buffer = buffer.alloc().compositeBuffer(((NettyHttpServerConfiguration) configuration).getJsonBufferMaxComponents()); + private void buffer(ByteBuf buffer) { + if (this.singleBuffer == null && this.compositeBuffer == null) { + this.singleBuffer = buffer; + } else { + if (this.compositeBuffer == null) { + // number of components should not be too small to avoid unnecessary consolidation + this.compositeBuffer = buffer.alloc().compositeBuffer(((NettyHttpServerConfiguration) configuration).getJsonBufferMaxComponents()); + this.compositeBuffer.addComponent(true, this.singleBuffer); + this.singleBuffer = null; + } + this.compositeBuffer.addComponent(true, buffer); } - this.buffer.addComponent(true, buffer); } - private void flush(Collection out, ByteBuf completedNode) throws IOException { - if (this.buffer != null) { - completedNode = completedNode == null ? this.buffer : this.buffer.addComponent(true, completedNode); - this.buffer = null; - } + private void flush(Collection out) throws IOException { + ByteBuf completedNode = compositeBuffer == null ? singleBuffer : compositeBuffer; ByteBuffer wrapped = NettyByteBufferFactory.DEFAULT.wrap(completedNode); if (((NettyHttpServerConfiguration) configuration).isEagerParsing()) { try { out.add(jsonMapper.readValue(wrapped, Argument.of(JsonNode.class))); } finally { - if (completedNode != null) { - completedNode.release(); - } + releaseBuffers(); } } else { out.add(new LazyJsonNode(wrapped)); + compositeBuffer = null; + singleBuffer = null; } } @Override public void complete(Collection out) throws Throwable { - if (this.buffer != null) { - flush(out, null); + if (this.singleBuffer != null || this.compositeBuffer != null) { + flush(out); } } } diff --git a/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/binding/DefaultJsonErrorHandlingSpec.groovy b/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/binding/DefaultJsonErrorHandlingSpec.groovy index a83902637fa..968cfa0f877 100644 --- a/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/binding/DefaultJsonErrorHandlingSpec.groovy +++ b/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/binding/DefaultJsonErrorHandlingSpec.groovy @@ -22,8 +22,8 @@ class DefaultJsonErrorHandlingSpec extends AbstractMicronautSpec { then: HttpClientResponseException e = thrown() - e.response.getBody(Map).get()._embedded.errors[0].message == """Invalid JSON: Unexpected end-of-input: expected close marker for Object (start marker at [Source: (byte[])"{"title":"The Stand""; line: 1, column: 1]) - at [Source: (byte[])"{"title":"The Stand""; line: 1, column: 21]""" + e.response.getBody(Map).get()._embedded.errors[0].message == """Invalid JSON: Unexpected end-of-input: expected close marker for Object (start marker at [Source: (io.netty.buffer.ByteBufInputStream); line: 1, column: 1]) + at [Source: (io.netty.buffer.ByteBufInputStream); line: 1, column: 21]""" e.response.status == HttpStatus.BAD_REQUEST when: diff --git a/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/binding/JsonBodyBindingSpec.groovy b/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/binding/JsonBodyBindingSpec.groovy index 22d9a309b0e..76675372cc6 100644 --- a/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/binding/JsonBodyBindingSpec.groovy +++ b/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/binding/JsonBodyBindingSpec.groovy @@ -73,7 +73,7 @@ class JsonBodyBindingSpec extends AbstractMicronautSpec { then: HttpClientResponseException e = thrown() e.message == """Invalid JSON: Unrecognized token 'The': was expecting (JSON String, Number, Array, Object or token 'null', 'true' or 'false') - at [Source: (byte[])"{"title":The Stand}"; line: 1, column: 14]""" + at [Source: (io.netty.buffer.ByteBufInputStream); line: 1, column: 14]""" e.response.status == HttpStatus.BAD_REQUEST when: diff --git a/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/cors/CorsFilterSpec.groovy b/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/cors/CorsFilterSpec.groovy index 65ca22d9cee..40aac01b952 100644 --- a/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/cors/CorsFilterSpec.groovy +++ b/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/cors/CorsFilterSpec.groovy @@ -338,9 +338,7 @@ class CorsFilterSpec extends Specification { getOrigin() >> Optional.of(origin) contains(ACCESS_CONTROL_REQUEST_METHOD) >> true } - HttpRequest request = Stub(HttpRequest) { - getHeaders() >> headers - } + HttpRequest request = createRequest(headers) CorsOriginConfiguration originConfig = new CorsOriginConfiguration() originConfig.exposedHeaders = ['Foo-Header', 'Bar-Header'] @@ -380,6 +378,7 @@ class CorsFilterSpec extends Specification { getHeaders() >> headers getMethod() >> HttpMethod.OPTIONS getUri() >> uri + getOrigin() >> headers.getOrigin() } List> routes = embeddedServer.getApplicationContext().getBean(Router). findAny(uri.toString(), request) @@ -434,6 +433,7 @@ class CorsFilterSpec extends Specification { getHeaders() >> headers getMethod() >> HttpMethod.OPTIONS getUri() >> uri + getOrigin() >> headers.getOrigin() } List> routes = embeddedServer.getApplicationContext().getBean(Router). findAny(request.getUri().toString(), request) @@ -561,6 +561,8 @@ class CorsFilterSpec extends Specification { private HttpRequest createRequest(HttpHeaders headers) { Stub(HttpRequest) { getHeaders() >> headers + + getOrigin() >> headers.getOrigin() } } diff --git a/http-server/src/main/java/io/micronaut/http/server/cors/CorsFilter.java b/http-server/src/main/java/io/micronaut/http/server/cors/CorsFilter.java index 48408994e68..8cc3e9662dc 100644 --- a/http-server/src/main/java/io/micronaut/http/server/cors/CorsFilter.java +++ b/http-server/src/main/java/io/micronaut/http/server/cors/CorsFilter.java @@ -92,7 +92,7 @@ public CorsFilter(HttpServerConfiguration.CorsConfiguration corsConfiguration, @Nullable @Internal public final HttpResponse filterRequest(HttpRequest request) { - String origin = request.getHeaders().getOrigin().orElse(null); + String origin = request.getOrigin().orElse(null); if (origin == null) { LOG.trace("Http Header " + HttpHeaders.ORIGIN + " not present. Proceeding with the request."); return null; // proceed @@ -144,7 +144,7 @@ protected boolean shouldDenyToPreventDriveByLocalhostAttack(@NonNull CorsOriginC if (httpHostResolver == null) { return false; } - String origin = request.getHeaders().getOrigin().orElse(null); + String origin = request.getOrigin().orElse(null); if (origin == null) { return false; } @@ -307,7 +307,7 @@ protected void setMaxAge(long maxAge, MutableHttpResponse response) { @NonNull private Optional getConfiguration(@NonNull HttpRequest request) { - String requestOrigin = request.getHeaders().getOrigin().orElse(null); + String requestOrigin = request.getOrigin().orElse(null); if (requestOrigin == null) { return Optional.empty(); } @@ -390,8 +390,7 @@ private void decorateResponseWithHeadersForPreflightRequest(@NonNull HttpRequest private void decorateResponseWithHeaders(@NonNull HttpRequest request, @NonNull MutableHttpResponse response, @NonNull CorsOriginConfiguration config) { - HttpHeaders headers = request.getHeaders(); - setOrigin(headers.getOrigin().orElse(null), response); + setOrigin(request.getOrigin().orElse(null), response); setVary(response); setExposeHeaders(config.getExposedHeaders(), response); setAllowCredentials(config, response); diff --git a/http-server/src/main/java/io/micronaut/http/server/cors/CorsUtil.java b/http-server/src/main/java/io/micronaut/http/server/cors/CorsUtil.java index f79b6499660..471afcbdd0f 100644 --- a/http-server/src/main/java/io/micronaut/http/server/cors/CorsUtil.java +++ b/http-server/src/main/java/io/micronaut/http/server/cors/CorsUtil.java @@ -35,9 +35,9 @@ class CorsUtil { * @param request The {@link HttpRequest} object * @return Return whether this request is a pre-flight request */ - static boolean isPreflightRequest(HttpRequest request) { + static boolean isPreflightRequest(HttpRequest request) { HttpHeaders headers = request.getHeaders(); - Optional origin = headers.getOrigin(); + Optional origin = request.getOrigin(); return origin.isPresent() && headers.contains(ACCESS_CONTROL_REQUEST_METHOD) && HttpMethod.OPTIONS == request.getMethod(); } } diff --git a/http/src/main/java/io/micronaut/http/HttpHeaders.java b/http/src/main/java/io/micronaut/http/HttpHeaders.java index 7a614001a54..91dd870e1a1 100644 --- a/http/src/main/java/io/micronaut/http/HttpHeaders.java +++ b/http/src/main/java/io/micronaut/http/HttpHeaders.java @@ -17,14 +17,17 @@ import io.micronaut.core.convert.ConversionContext; import io.micronaut.core.type.Headers; -import io.micronaut.core.util.StringUtils; import java.time.LocalDateTime; import java.time.ZoneId; import java.time.ZonedDateTime; import java.time.format.DateTimeFormatter; import java.time.format.DateTimeParseException; -import java.util.*; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.OptionalLong; /** * Constants for common HTTP headers. See https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html. @@ -688,29 +691,14 @@ default OptionalLong contentLength() { * @return A list of zero or many {@link MediaType} instances */ default List accept() { - final List values = getAll(HttpHeaders.ACCEPT); - if (!values.isEmpty()) { - List mediaTypes = new ArrayList<>(10); - for (String value : values) { - for (String token : StringUtils.splitOmitEmptyStrings(value, ',')) { - try { - mediaTypes.add(MediaType.of(token)); - } catch (IllegalArgumentException e) { - // ignore - } - } - } - return mediaTypes; - } else { - return Collections.emptyList(); - } + return MediaType.orderedOf(getAll(HttpHeaders.ACCEPT)); } /** * @return Whether the {@link HttpHeaders#CONNECTION} header is set to Keep-Alive */ default boolean isKeepAlive() { - return getFirst(CONNECTION, ConversionContext.STRING) + return findFirst(CONNECTION) .map(val -> val.equalsIgnoreCase(HttpHeaderValues.CONNECTION_KEEP_ALIVE)).orElse(false); } diff --git a/http/src/main/java/io/micronaut/http/HttpRequest.java b/http/src/main/java/io/micronaut/http/HttpRequest.java index 2079b99c656..cee0c7c1817 100644 --- a/http/src/main/java/io/micronaut/http/HttpRequest.java +++ b/http/src/main/java/io/micronaut/http/HttpRequest.java @@ -15,15 +15,18 @@ */ package io.micronaut.http; -import io.micronaut.http.cookie.Cookies; - import io.micronaut.core.annotation.NonNull; import io.micronaut.core.annotation.Nullable; +import io.micronaut.http.cookie.Cookies; + import java.net.InetSocketAddress; import java.net.URI; import java.security.Principal; import java.security.cert.Certificate; -import java.util.*; +import java.util.Collection; +import java.util.Locale; +import java.util.Objects; +import java.util.Optional; /** *

Common interface for HTTP request implementations.

@@ -87,13 +90,7 @@ default HttpVersion getHttpVersion() { * @return A list of zero or many {@link MediaType} instances */ default Collection accept() { - final HttpHeaders headers = getHeaders(); - if (headers.contains(HttpHeaders.ACCEPT)) { - return MediaType.orderedOf( - headers.getAll(HttpHeaders.ACCEPT) - ); - } - return Collections.emptySet(); + return getHeaders().accept(); } /** @@ -198,6 +195,16 @@ default Optional getCertificate() { return this.getAttribute(HttpAttributes.X509_CERTIFICATE, Certificate.class); } + /** + * Get the origin header. + * + * @return The origin header + * @see HttpHeaders#getOrigin() + */ + default Optional getOrigin() { + return getHeaders().getOrigin(); + } + /** * Return a {@link MutableHttpRequest} for a {@link HttpMethod#GET} request for the given URI. * diff --git a/http/src/main/java/io/micronaut/http/MediaType.java b/http/src/main/java/io/micronaut/http/MediaType.java index bcdca2078fe..7d42b4f46e2 100644 --- a/http/src/main/java/io/micronaut/http/MediaType.java +++ b/http/src/main/java/io/micronaut/http/MediaType.java @@ -23,7 +23,6 @@ import io.micronaut.core.naming.NameUtils; import io.micronaut.core.type.Argument; import io.micronaut.core.util.ArrayUtils; -import io.micronaut.core.util.CollectionUtils; import io.micronaut.core.util.StringUtils; import io.micronaut.core.value.OptionalValues; import io.micronaut.http.annotation.Produces; @@ -42,7 +41,6 @@ import java.util.Collections; import java.util.Iterator; import java.util.LinkedHashMap; -import java.util.LinkedList; import java.util.List; import java.util.Locale; import java.util.Map; @@ -399,6 +397,7 @@ public class MediaType implements CharSequence { protected final String extension; protected final Map parameters; private final String strRepr; + private final String lowerName; private BigDecimal qualityNumberField = BigDecimal.ONE; @@ -490,6 +489,7 @@ public MediaType(String name, String extension, Map params) { withoutArgs = name; } this.name = withoutArgs; + this.lowerName = withoutArgs.toLowerCase(Locale.ROOT); int i = withoutArgs.indexOf('/'); if (i > -1) { this.type = withoutArgs.substring(0, i); @@ -735,12 +735,12 @@ public boolean equals(Object o) { MediaType mediaType = (MediaType) o; - return name.equalsIgnoreCase(mediaType.name); + return lowerName.equals(mediaType.lowerName); } @Override public int hashCode() { - return name.hashCode(); + return lowerName.hashCode(); } /** @@ -760,35 +760,49 @@ public static List orderedOf(CharSequence... values) { * @since 1.3.3 */ public static List orderedOf(List values) { - if (CollectionUtils.isNotEmpty(values)) { - List mediaTypes = new LinkedList<>(); - for (CharSequence value : values) { - for (String token : StringUtils.splitOmitEmptyStrings(value, ',')) { - try { - mediaTypes.add(MediaType.of(token)); - } catch (IllegalArgumentException e) { - // ignore - } + if (values == null) { + return Collections.emptyList(); + } + int headerCount = values.size(); + if (headerCount == 0) { + return Collections.emptyList(); + } + if (headerCount == 1) { + // fast path for single header with single media type + String singleHeader = values.get(0).toString(); + if (singleHeader.indexOf(',') == -1) { + try { + return List.of(MediaType.of(singleHeader)); + } catch (IllegalArgumentException ignored) { } } - mediaTypes = new ArrayList<>(mediaTypes); - mediaTypes.sort((o1, o2) -> { - //The */* type is always last - if (o1.type.equals("*")) { - return 1; - } else if (o2.type.equals("*")) { - return -1; - } - if (o2.subtype.equals("*") && !o1.subtype.equals("*")) { - return -1; - } else if (o1.subtype.equals("*") && !o2.subtype.equals("*")) { - return 1; + } + + List mediaTypes = new ArrayList<>(); + for (CharSequence value : values) { + for (String token : StringUtils.splitOmitEmptyStrings(value, ',')) { + try { + mediaTypes.add(MediaType.of(token)); + } catch (IllegalArgumentException e) { + // ignore } - return o2.getQualityAsNumber().compareTo(o1.getQualityAsNumber()); - }); - return Collections.unmodifiableList(mediaTypes); + } } - return Collections.emptyList(); + mediaTypes.sort((o1, o2) -> { + //The */* type is always last + if (o1.type.equals("*")) { + return 1; + } else if (o2.type.equals("*")) { + return -1; + } + if (o2.subtype.equals("*") && !o1.subtype.equals("*")) { + return -1; + } else if (o1.subtype.equals("*") && !o2.subtype.equals("*")) { + return 1; + } + return o2.getQualityAsNumber().compareTo(o1.getQualityAsNumber()); + }); + return Collections.unmodifiableList(mediaTypes); } /** diff --git a/jackson-core/src/main/java/io/micronaut/jackson/core/parser/JacksonCoreParserFactory.java b/jackson-core/src/main/java/io/micronaut/jackson/core/parser/JacksonCoreParserFactory.java index 1e392970df7..22b71ee2edb 100644 --- a/jackson-core/src/main/java/io/micronaut/jackson/core/parser/JacksonCoreParserFactory.java +++ b/jackson-core/src/main/java/io/micronaut/jackson/core/parser/JacksonCoreParserFactory.java @@ -44,7 +44,7 @@ private JacksonCoreParserFactory() { static { boolean hasNettyBuffer; try { - Class.forName("io.netty.buffer.ByteBuf", false, null); + Class.forName("io.netty.buffer.ByteBuf", false, JacksonCoreParserFactory.class.getClassLoader()); hasNettyBuffer = true; } catch (ClassNotFoundException e) { hasNettyBuffer = false; diff --git a/json-core/src/main/java/io/micronaut/json/convert/JsonConverterRegistrar.java b/json-core/src/main/java/io/micronaut/json/convert/JsonConverterRegistrar.java index 68796937191..e6bddc413e8 100644 --- a/json-core/src/main/java/io/micronaut/json/convert/JsonConverterRegistrar.java +++ b/json-core/src/main/java/io/micronaut/json/convert/JsonConverterRegistrar.java @@ -52,7 +52,8 @@ @Experimental @Prototype public final class JsonConverterRegistrar implements TypeConverterRegistrar { - private final BeanProvider objectCodec; + private final BeanProvider objectCodecProvider; + private JsonMapper objectCodec; private final ConversionService conversionService; private final BeanProvider beanPropertyBinder; @@ -62,11 +63,20 @@ public JsonConverterRegistrar( ConversionService conversionService, BeanProvider beanPropertyBinder ) { - this.objectCodec = objectCodec; + this.objectCodecProvider = objectCodec; this.conversionService = conversionService; this.beanPropertyBinder = beanPropertyBinder; } + private JsonMapper objectCodec() { + // JsonMapper is immutable, so we don't need safe publication here + JsonMapper objectCodec = this.objectCodec; + if (objectCodec == null) { + this.objectCodec = objectCodec = objectCodecProvider.get(); + } + return objectCodec; + } + @Override public void register(MutableConversionService conversionService) { conversionService.addConverter( @@ -143,7 +153,7 @@ private TypeConverter unparsedNodeToConvertible return Optional.empty(); } try { - return Optional.of(new JsonNodeConvertibleValues<>(node.toJsonNode(objectCodec.get()), conversionService)); + return Optional.of(new JsonNodeConvertibleValues<>(node.toJsonNode(objectCodec()), conversionService)); } catch (IOException e) { context.reject(e); return Optional.empty(); @@ -203,7 +213,7 @@ private Object correctKeys(Object o) { private TypeConverter objectToJsonNodeConverter() { return (object, targetType, context) -> { try { - return Optional.of(objectCodec.get().writeValueToTree(object)); + return Optional.of(objectCodec().writeValueToTree(object)); } catch (IllegalArgumentException | IOException e) { context.reject(e); return Optional.empty(); @@ -233,9 +243,9 @@ private TypeConverter jsonNodeToObjectConverter() { return (node, targetType, context) -> { try { if (CharSequence.class.isAssignableFrom(targetType) && node.isObject()) { - return Optional.of(new String(objectCodec.get().writeValueAsBytes(node), StandardCharsets.UTF_8)); + return Optional.of(new String(objectCodec().writeValueAsBytes(node), StandardCharsets.UTF_8)); } else { - return Optional.ofNullable(this.objectCodec.get().readValueFromTree(node, argument(targetType, context))); + return Optional.ofNullable(objectCodec().readValueFromTree(node, argument(targetType, context))); } } catch (IOException e) { context.reject(e); @@ -250,7 +260,7 @@ private TypeConverter jsonNodeToObjectConverter() { private TypeConverter unparsedJsonNodeToObjectConverter() { return (node, targetType, context) -> { try { - JsonMapper mapper = objectCodec.get(); + JsonMapper mapper = objectCodec(); if (CharSequence.class.isAssignableFrom(targetType) && node.isObject()) { // parse once to JsonNode to ensure validity & sanitize the input byte[] sanitized = mapper.writeValueAsBytes(node.toJsonNode(mapper)); From 72b1a7eea8717630b796959855944b10a4361d43 Mon Sep 17 00:00:00 2001 From: Graeme Rocher Date: Sat, 25 Mar 2023 19:15:32 +0100 Subject: [PATCH 624/743] Support for Compilation Time Expressions in Annotations (#8954) From feature request https://github.com/micronaut-projects/micronaut-core/issues/8323 this PR implements the following set of features (some parts will remain unimplemented in the first version): 1. Declaring literals. - [x] strings (delimited by single quotes) - [x] numeric types (int, long, float, double, incl. scientific notation and hex) - [x] boolean - [x] `null` 2. Mathematical operators - [x] `-`, `/`, `*`, `%`,`^` on numeric types - [x] `+` on both numeric types and strings 3. Relational operators - [x] Standart relational operators (`>`, `<`, `>=`, `<=`, `==`, `!=`) - [x] `instanceof` operator (e.g. `'abc' instanceof T(java.langString)`) - [x] `matches` for regex mathing (e.g. `'abc' matches '^[a-z]*'`) (including `/.+/` syntax for regex) - [x] `empty` for checking of an object is null or empty - [ ] Relational operators need to support comparables as well 4. Logical operators - [x] `&&`, `||`, `!`. We should probably also allow using aliases here (`and`, `or`, `not`) 5. Working with type references - [x] as part of `instanceof` operation - [x] as an argument for method invocation which is treated as io.micronaut.CutsomType.class (e.g. `getBean(T(io.micronaut.CustomType))` - [x] as a type reference followed by static method invocation (e.g. `T(java.lang.Math).random()`) 6. Working with collections - [x] Accessing by index for lists (e.g. `list[1]`) - [x] Accessing by key for maps (e.g. `map['key']`) - [ ] Collection filtering (e.g. `ages.filter(age -> age > 18)` ) - [ ] Collection mapping (e.g. `persons.map(p -> p.age)` ) For filtering/mapping probably makes sense to support lambda syntax. 7. Accessing object properties - [x] This should allow chaining property access with `.` (e.g. `object.property.nestedProperty`) - [x] Safe property access allowing to avoid NPE (`object?.property.?.nestedProperty`) 8. Methods invocation - [x] Object method invocation (e.g. `object.name().lenght()`) - [x] Static method invocation (`T(java.lang.Math).random()`) 9. Ternary operator - [x] Standard `expr ? 'trueResult' : 'falseResult'` - [x] Elvis operator (e.g. `age?:18`, note only makes sense if we implement coercion to boolean for numbers, lists, strings etc.) 10. Object construction - [ ] construction with `new` keyword (this might be useful in `@Value` annotation, e.g. `@Value(#{ new io.micronaut.CustomType() })` - [ ] array construcation (`new int[] {1, 2, 3}`) - not sure whether this is a useful one, since inlining list gives similar behavior - [ ] lists with `{1, 2, 3}` syntax - [ ] maps with `{ 'first': 1, 'second': 2 }` 11. Predefined syntax constructs, e.g. - [ ] Getting beans from context (e.g. `ctx[io.micronaut.CustomBean]`) - [ ] Accessing properties (e.g. `env['custom.property']` ) See https://github.com/micronaut-projects/micronaut-core/issues/8323 --------- Co-authored-by: Sergey Gavrilov Co-authored-by: Sergio del Amo --- .../aop/chain/DefaultInterceptorRegistry.java | 4 + .../aop/chain/MethodInterceptorChain.java | 9 + .../DefaultTaskExceptionHandler.java | 4 +- .../scheduling/TaskExceptionHandler.java | 18 + .../scheduling/annotation/Scheduled.java | 9 + .../processor/ScheduledMethodProcessor.java | 68 +- .../micronaut/aop/writer/AopProxyWriter.java | 8 +- .../EvaluatedExpressionConstants.java | 35 + .../EvaluatedExpressionWriter.java | 141 +++ .../DefaultExpressionCompilationContext.java | 124 +++ ...ltExpressionCompilationContextFactory.java | 131 +++ .../context/ExpressionCompilationContext.java | 62 ++ .../ExpressionCompilationContextFactory.java | 63 ++ .../ExpressionEvaluationContextRegistrar.java | 51 + .../context/ExpressionWithContext.java | 55 ++ ...xtensibleExpressionCompilationContext.java | 52 + ...undEvaluatedEvaluatedExpressionParser.java | 150 +++ .../parser/EvaluatedExpressionParser.java | 40 + ...gleEvaluatedEvaluatedExpressionParser.java | 549 +++++++++++ .../parser/ast/ExpressionNode.java | 102 ++ .../parser/ast/access/AbstractMethodCall.java | 207 ++++ .../parser/ast/access/CandidateMethod.java | 252 +++++ .../ast/access/ContextElementAccess.java | 138 +++ .../parser/ast/access/ContextMethodCall.java | 108 ++ .../access/ContextMethodParameterAccess.java | 93 ++ .../parser/ast/access/ElementMethodCall.java | 138 +++ .../parser/ast/access/PropertyAccess.java | 78 ++ .../parser/ast/access/SubscriptOperator.java | 118 +++ .../ast/collection/OneDimensionalArray.java | 95 ++ .../parser/ast/conditional/ElvisOperator.java | 32 + .../ast/conditional/TernaryExpression.java | 163 ++++ .../parser/ast/literal/BoolLiteral.java | 55 ++ .../parser/ast/literal/DoubleLiteral.java | 55 ++ .../parser/ast/literal/FloatLiteral.java | 55 ++ .../parser/ast/literal/IntLiteral.java | 63 ++ .../parser/ast/literal/LongLiteral.java | 55 ++ .../parser/ast/literal/NullLiteral.java | 49 + .../parser/ast/literal/StringLiteral.java | 60 ++ .../ast/operator/binary/AddOperator.java | 155 +++ .../ast/operator/binary/AndOperator.java | 67 ++ .../ast/operator/binary/BinaryOperator.java | 61 ++ .../ast/operator/binary/DivOperator.java | 58 ++ .../ast/operator/binary/EqOperator.java | 62 ++ .../ast/operator/binary/GtOperator.java | 45 + .../ast/operator/binary/GteOperator.java | 45 + .../operator/binary/InstanceofOperator.java | 75 ++ .../ast/operator/binary/LogicalOperator.java | 49 + .../ast/operator/binary/LtOperator.java | 45 + .../ast/operator/binary/LteOperator.java | 45 + .../ast/operator/binary/MatchesOperator.java | 83 ++ .../ast/operator/binary/MathOperator.java | 68 ++ .../ast/operator/binary/ModOperator.java | 58 ++ .../ast/operator/binary/MulOperator.java | 59 ++ .../ast/operator/binary/NeqOperator.java | 62 ++ .../ast/operator/binary/OrOperator.java | 55 ++ .../ast/operator/binary/PowOperator.java | 88 ++ .../operator/binary/RelationalOperator.java | 111 +++ .../ast/operator/binary/SubOperator.java | 59 ++ .../ast/operator/unary/EmptyOperator.java | 135 +++ .../ast/operator/unary/NegOperator.java | 83 ++ .../ast/operator/unary/NotOperator.java | 74 ++ .../ast/operator/unary/PosOperator.java | 53 + .../ast/operator/unary/UnaryOperator.java | 53 + .../parser/ast/types/TypeIdentifier.java | 110 +++ .../EvaluatedExpressionCompilationUtils.java | 254 +++++ .../parser/ast/util/TypeDescriptors.java | 193 ++++ .../compilation/ExpressionVisitorContext.java | 38 + .../ExpressionCompilationException.java | 32 + .../exception/ExpressionParsingException.java | 31 + .../expressions/parser/token/Token.java | 30 + .../expressions/parser/token/TokenType.java | 90 ++ .../expressions/parser/token/Tokenizer.java | 222 +++++ .../util/EvaluatedExpressionsUtils.java | 85 ++ .../AbstractAnnotationMetadataBuilder.java | 50 +- .../annotation/AnnotationMetadataWriter.java | 45 +- .../micronaut/inject/ast/MethodElement.java | 9 + .../inject/visitor/VisitorContext.java | 11 +- .../inject/writer/BeanDefinitionVisitor.java | 9 + .../inject/writer/BeanDefinitionWriter.java | 115 ++- .../ExecutableMethodsDefinitionWriter.java | 37 +- .../core/annotation/AnnotationMetadata.java | 10 + .../core/annotation/AnnotationValue.java | 30 +- .../annotation/AnnotationValueBuilder.java | 6 +- .../core/expressions/EvaluatedExpression.java | 36 + .../EvaluatedExpressionReference.java | 82 ++ .../ExpressionEvaluationContext.java | 46 + .../io/micronaut/core/util/ObjectUtils.java | 39 + .../core/util/ObjectUtilsSpec.groovy | 31 + .../micronaut/http/uri/UriMatchTemplate.java | 2 +- .../AbstractEvaluatedExpressionsSpec.groovy | 125 +++ .../ast/groovy/InjectTransform.groovy | 8 + .../ast/groovy/TypeElementVisitorEnd.groovy | 3 +- .../ast/groovy/TypeElementVisitorStart.groovy | 3 +- .../GroovyAnnotationMetadataBuilder.java | 55 +- .../groovy/visitor/GroovyVisitorContext.java | 9 + ...odehaus.groovy.transform.ASTTransformation | 2 +- ...notationLevelContextExpressionsSpec.groovy | 76 ++ .../ArrayMethodsExpressionsSpec.groovy | 220 +++++ .../CompoundExpressionsSpec.groovy | 56 ++ .../ContextMethodCallsExpressionsSpec.groovy | 149 +++ ...ontextPropertyAccessExpressionsSpec.groovy | 62 ++ .../expressions/ContextRegistrar.groovy | 27 + .../expressions/LiteralExpressionsSpec.groovy | 96 ++ ...entEvaluationContextExpressionsSpec.groovy | 30 + .../OperatorExpressionsSpec.groovy | 905 +++++++++++++++++ .../TernaryOperationExpressionsSpec.groovy | 36 + .../TestExpressionsInjectionSpec.groovy | 166 ++++ .../TestExpressionsUsageSpec.groovy | 130 +++ .../TypeIdentifierExpressionsSpec.groovy | 52 + ...icronaut.inject.visitor.TypeElementVisitor | 1 + .../AbstractEvaluatedExpressionsSpec.groovy | 156 +++ .../BeanDefinitionInjectProcessor.java | 23 +- .../JavaAnnotationMetadataBuilder.java | 23 +- .../processing/visitor/JavaMethodElement.java | 23 + .../visitor/JavaVisitorContext.java | 9 + .../processing/visitor/LoadedVisitor.java | 41 +- ...notationLevelContextExpressionsSpec.groovy | 75 ++ .../ArrayMethodsExpressionsSpec.groovy | 219 +++++ .../CollectionExpressionSpec.groovy | 86 ++ .../CompoundExpressionsSpec.groovy | 54 + .../ContextMethodCallsExpressionsSpec.groovy | 149 +++ ...ontextPropertyAccessExpressionsSpec.groovy | 175 ++++ .../expressions/LiteralExpressionSpec.groovy | 95 ++ ...entEvaluationContextExpressionsSpec.groovy | 32 + .../expressions/OperatorExpressionSpec.groovy | 923 ++++++++++++++++++ .../TernaryOperationExpressionsSpec.groovy | 54 + .../TestExpressionsInjectionSpec.groovy | 167 ++++ .../TestExpressionsUsageSpec.groovy | 130 +++ .../TypeIdentifierExpressionsSpec.groovy | 52 + .../KotlinAnnotationMetadataBuilder.kt | 39 +- .../beans/BeanDefinitionProcessor.kt | 29 +- .../micronaut/kotlin/processing/extensions.kt | 22 + .../visitor/KotlinVisitorContext.kt | 8 + .../TestExpressionsInjectionSpec.groovy | 29 + .../visitor/BeanIntrospectionSpec.groovy | 3 +- .../AbstractBeanResolutionContext.java | 5 + .../AbstractExecutableMethodsDefinition.java | 48 +- .../AbstractInitializableBeanDefinition.java | 242 ++++- ...tInitializableBeanDefinitionReference.java | 3 + .../context/BeanDefinitionAware.java | 33 + .../context/ContextConfigurable.java | 31 + .../context/ExpressionsAwareArgument.java | 89 ++ .../micronaut/context/RequiresCondition.java | 7 + .../AnnotationExpressionContext.java | 49 + .../micronaut/context/annotation/Value.java | 7 +- .../DefaultExecutableBeanContextBinder.java | 187 ++++ .../bind/ExecutableBeanContextBinder.java | 45 + .../ExpressionEvaluationException.java | 33 + .../AbstractEvaluatedExpression.java | 66 ++ ...nfigurableExpressionEvaluationContext.java | 60 ++ .../DefaultExpressionEvaluationContext.java | 108 ++ ...AbstractEnvironmentAnnotationMetadata.java | 5 + .../AnnotationMetadataHierarchy.java | 10 + .../annotation/DefaultAnnotationMetadata.java | 33 + .../EvaluatedAnnotationMetadata.java | 98 ++ .../annotation/EvaluatedAnnotationValue.java | 61 ++ .../EvaluatedConvertibleValuesMap.java | 86 ++ .../MappingAnnotationMetadataDelegate.java | 452 +++++++++ .../annotation/MutableAnnotationMetadata.java | 55 +- .../guide/config/evaluatedExpressions.adoc | 408 ++++++++ .../docs/guide/introduction/whatsNew.adoc | 53 +- src/main/docs/guide/toc.yml | 1 + test-suite-groovy/build.gradle | 1 + .../AnnotationContextExample.groovy | 30 + .../AnnotationContextExampleSpec.groovy | 21 + .../docs/expressions/ExampleJob.groovy | 38 + .../docs/expressions/ExampleJobSpec.groovy | 40 + test-suite-kotlin/build.gradle | 2 +- .../expressions/AnnotationContextExample.kt | 24 + .../AnnotationContextExampleTest.kt | 21 + .../micronaut/docs/expressions/ExampleJob.kt | 33 + .../docs/expressions/ExampleJobTest.kt | 21 + .../docs/expressions/ContextRegistrar.java | 25 + .../expressions/CustomEvaluationContext.java | 29 + ...icronaut.inject.visitor.TypeElementVisitor | 3 +- .../docs/client/versioning/HelloClient.java | 2 +- .../expressions/AnnotationContextExample.java | 30 + .../AnnotationContextExampleTest.java | 19 + .../docs/expressions/ContextConsumer.java | 13 + .../docs/expressions/ContextConsumerTest.java | 13 + .../docs/expressions/ContextRegistrar.java | 10 + .../expressions/CustomEvaluationContext.java | 11 + .../docs/expressions/ExampleJob.java | 38 + .../docs/expressions/ExampleJobTest.java | 25 + 184 files changed, 14276 insertions(+), 155 deletions(-) create mode 100644 core-processor/src/main/java/io/micronaut/expressions/EvaluatedExpressionConstants.java create mode 100644 core-processor/src/main/java/io/micronaut/expressions/EvaluatedExpressionWriter.java create mode 100644 core-processor/src/main/java/io/micronaut/expressions/context/DefaultExpressionCompilationContext.java create mode 100644 core-processor/src/main/java/io/micronaut/expressions/context/DefaultExpressionCompilationContextFactory.java create mode 100644 core-processor/src/main/java/io/micronaut/expressions/context/ExpressionCompilationContext.java create mode 100644 core-processor/src/main/java/io/micronaut/expressions/context/ExpressionCompilationContextFactory.java create mode 100644 core-processor/src/main/java/io/micronaut/expressions/context/ExpressionEvaluationContextRegistrar.java create mode 100644 core-processor/src/main/java/io/micronaut/expressions/context/ExpressionWithContext.java create mode 100644 core-processor/src/main/java/io/micronaut/expressions/context/ExtensibleExpressionCompilationContext.java create mode 100644 core-processor/src/main/java/io/micronaut/expressions/parser/CompoundEvaluatedEvaluatedExpressionParser.java create mode 100644 core-processor/src/main/java/io/micronaut/expressions/parser/EvaluatedExpressionParser.java create mode 100644 core-processor/src/main/java/io/micronaut/expressions/parser/SingleEvaluatedEvaluatedExpressionParser.java create mode 100644 core-processor/src/main/java/io/micronaut/expressions/parser/ast/ExpressionNode.java create mode 100644 core-processor/src/main/java/io/micronaut/expressions/parser/ast/access/AbstractMethodCall.java create mode 100644 core-processor/src/main/java/io/micronaut/expressions/parser/ast/access/CandidateMethod.java create mode 100644 core-processor/src/main/java/io/micronaut/expressions/parser/ast/access/ContextElementAccess.java create mode 100644 core-processor/src/main/java/io/micronaut/expressions/parser/ast/access/ContextMethodCall.java create mode 100644 core-processor/src/main/java/io/micronaut/expressions/parser/ast/access/ContextMethodParameterAccess.java create mode 100644 core-processor/src/main/java/io/micronaut/expressions/parser/ast/access/ElementMethodCall.java create mode 100644 core-processor/src/main/java/io/micronaut/expressions/parser/ast/access/PropertyAccess.java create mode 100644 core-processor/src/main/java/io/micronaut/expressions/parser/ast/access/SubscriptOperator.java create mode 100644 core-processor/src/main/java/io/micronaut/expressions/parser/ast/collection/OneDimensionalArray.java create mode 100644 core-processor/src/main/java/io/micronaut/expressions/parser/ast/conditional/ElvisOperator.java create mode 100644 core-processor/src/main/java/io/micronaut/expressions/parser/ast/conditional/TernaryExpression.java create mode 100644 core-processor/src/main/java/io/micronaut/expressions/parser/ast/literal/BoolLiteral.java create mode 100644 core-processor/src/main/java/io/micronaut/expressions/parser/ast/literal/DoubleLiteral.java create mode 100644 core-processor/src/main/java/io/micronaut/expressions/parser/ast/literal/FloatLiteral.java create mode 100644 core-processor/src/main/java/io/micronaut/expressions/parser/ast/literal/IntLiteral.java create mode 100644 core-processor/src/main/java/io/micronaut/expressions/parser/ast/literal/LongLiteral.java create mode 100644 core-processor/src/main/java/io/micronaut/expressions/parser/ast/literal/NullLiteral.java create mode 100644 core-processor/src/main/java/io/micronaut/expressions/parser/ast/literal/StringLiteral.java create mode 100644 core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/AddOperator.java create mode 100644 core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/AndOperator.java create mode 100644 core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/BinaryOperator.java create mode 100644 core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/DivOperator.java create mode 100644 core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/EqOperator.java create mode 100644 core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/GtOperator.java create mode 100644 core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/GteOperator.java create mode 100644 core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/InstanceofOperator.java create mode 100644 core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/LogicalOperator.java create mode 100644 core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/LtOperator.java create mode 100644 core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/LteOperator.java create mode 100644 core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/MatchesOperator.java create mode 100644 core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/MathOperator.java create mode 100644 core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/ModOperator.java create mode 100644 core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/MulOperator.java create mode 100644 core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/NeqOperator.java create mode 100644 core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/OrOperator.java create mode 100644 core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/PowOperator.java create mode 100644 core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/RelationalOperator.java create mode 100644 core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/SubOperator.java create mode 100644 core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/unary/EmptyOperator.java create mode 100644 core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/unary/NegOperator.java create mode 100644 core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/unary/NotOperator.java create mode 100644 core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/unary/PosOperator.java create mode 100644 core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/unary/UnaryOperator.java create mode 100644 core-processor/src/main/java/io/micronaut/expressions/parser/ast/types/TypeIdentifier.java create mode 100644 core-processor/src/main/java/io/micronaut/expressions/parser/ast/util/EvaluatedExpressionCompilationUtils.java create mode 100644 core-processor/src/main/java/io/micronaut/expressions/parser/ast/util/TypeDescriptors.java create mode 100644 core-processor/src/main/java/io/micronaut/expressions/parser/compilation/ExpressionVisitorContext.java create mode 100644 core-processor/src/main/java/io/micronaut/expressions/parser/exception/ExpressionCompilationException.java create mode 100644 core-processor/src/main/java/io/micronaut/expressions/parser/exception/ExpressionParsingException.java create mode 100644 core-processor/src/main/java/io/micronaut/expressions/parser/token/Token.java create mode 100644 core-processor/src/main/java/io/micronaut/expressions/parser/token/TokenType.java create mode 100644 core-processor/src/main/java/io/micronaut/expressions/parser/token/Tokenizer.java create mode 100644 core-processor/src/main/java/io/micronaut/expressions/util/EvaluatedExpressionsUtils.java create mode 100644 core/src/main/java/io/micronaut/core/expressions/EvaluatedExpression.java create mode 100644 core/src/main/java/io/micronaut/core/expressions/EvaluatedExpressionReference.java create mode 100644 core/src/main/java/io/micronaut/core/expressions/ExpressionEvaluationContext.java create mode 100644 inject-groovy-test/src/main/groovy/io/micronaut/ast/transform/test/AbstractEvaluatedExpressionsSpec.groovy create mode 100644 inject-groovy/src/test/groovy/io/micronaut/expressions/AnnotationLevelContextExpressionsSpec.groovy create mode 100644 inject-groovy/src/test/groovy/io/micronaut/expressions/ArrayMethodsExpressionsSpec.groovy create mode 100644 inject-groovy/src/test/groovy/io/micronaut/expressions/CompoundExpressionsSpec.groovy create mode 100644 inject-groovy/src/test/groovy/io/micronaut/expressions/ContextMethodCallsExpressionsSpec.groovy create mode 100644 inject-groovy/src/test/groovy/io/micronaut/expressions/ContextPropertyAccessExpressionsSpec.groovy create mode 100644 inject-groovy/src/test/groovy/io/micronaut/expressions/ContextRegistrar.groovy create mode 100644 inject-groovy/src/test/groovy/io/micronaut/expressions/LiteralExpressionsSpec.groovy create mode 100644 inject-groovy/src/test/groovy/io/micronaut/expressions/MethodArgumentEvaluationContextExpressionsSpec.groovy create mode 100644 inject-groovy/src/test/groovy/io/micronaut/expressions/OperatorExpressionsSpec.groovy create mode 100644 inject-groovy/src/test/groovy/io/micronaut/expressions/TernaryOperationExpressionsSpec.groovy create mode 100644 inject-groovy/src/test/groovy/io/micronaut/expressions/TestExpressionsInjectionSpec.groovy create mode 100644 inject-groovy/src/test/groovy/io/micronaut/expressions/TestExpressionsUsageSpec.groovy create mode 100644 inject-groovy/src/test/groovy/io/micronaut/expressions/TypeIdentifierExpressionsSpec.groovy create mode 100644 inject-java-test/src/main/groovy/io/micronaut/annotation/processing/test/AbstractEvaluatedExpressionsSpec.groovy create mode 100644 inject-java/src/test/groovy/io/micronaut/expressions/AnnotationLevelContextExpressionsSpec.groovy create mode 100644 inject-java/src/test/groovy/io/micronaut/expressions/ArrayMethodsExpressionsSpec.groovy create mode 100644 inject-java/src/test/groovy/io/micronaut/expressions/CollectionExpressionSpec.groovy create mode 100644 inject-java/src/test/groovy/io/micronaut/expressions/CompoundExpressionsSpec.groovy create mode 100644 inject-java/src/test/groovy/io/micronaut/expressions/ContextMethodCallsExpressionsSpec.groovy create mode 100644 inject-java/src/test/groovy/io/micronaut/expressions/ContextPropertyAccessExpressionsSpec.groovy create mode 100644 inject-java/src/test/groovy/io/micronaut/expressions/LiteralExpressionSpec.groovy create mode 100644 inject-java/src/test/groovy/io/micronaut/expressions/MethodArgumentEvaluationContextExpressionsSpec.groovy create mode 100644 inject-java/src/test/groovy/io/micronaut/expressions/OperatorExpressionSpec.groovy create mode 100644 inject-java/src/test/groovy/io/micronaut/expressions/TernaryOperationExpressionsSpec.groovy create mode 100644 inject-java/src/test/groovy/io/micronaut/expressions/TestExpressionsInjectionSpec.groovy create mode 100644 inject-java/src/test/groovy/io/micronaut/expressions/TestExpressionsUsageSpec.groovy create mode 100644 inject-java/src/test/groovy/io/micronaut/expressions/TypeIdentifierExpressionsSpec.groovy create mode 100644 inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/expressions/TestExpressionsInjectionSpec.groovy create mode 100644 inject/src/main/java/io/micronaut/context/BeanDefinitionAware.java create mode 100644 inject/src/main/java/io/micronaut/context/ContextConfigurable.java create mode 100644 inject/src/main/java/io/micronaut/context/ExpressionsAwareArgument.java create mode 100644 inject/src/main/java/io/micronaut/context/annotation/AnnotationExpressionContext.java create mode 100644 inject/src/main/java/io/micronaut/context/bind/DefaultExecutableBeanContextBinder.java create mode 100644 inject/src/main/java/io/micronaut/context/bind/ExecutableBeanContextBinder.java create mode 100644 inject/src/main/java/io/micronaut/context/exceptions/ExpressionEvaluationException.java create mode 100644 inject/src/main/java/io/micronaut/context/expressions/AbstractEvaluatedExpression.java create mode 100644 inject/src/main/java/io/micronaut/context/expressions/ConfigurableExpressionEvaluationContext.java create mode 100644 inject/src/main/java/io/micronaut/context/expressions/DefaultExpressionEvaluationContext.java create mode 100644 inject/src/main/java/io/micronaut/inject/annotation/EvaluatedAnnotationMetadata.java create mode 100644 inject/src/main/java/io/micronaut/inject/annotation/EvaluatedAnnotationValue.java create mode 100644 inject/src/main/java/io/micronaut/inject/annotation/EvaluatedConvertibleValuesMap.java create mode 100644 inject/src/main/java/io/micronaut/inject/annotation/MappingAnnotationMetadataDelegate.java create mode 100644 src/main/docs/guide/config/evaluatedExpressions.adoc create mode 100644 test-suite-groovy/src/test/groovy/io/micronaut/docs/expressions/AnnotationContextExample.groovy create mode 100644 test-suite-groovy/src/test/groovy/io/micronaut/docs/expressions/AnnotationContextExampleSpec.groovy create mode 100644 test-suite-groovy/src/test/groovy/io/micronaut/docs/expressions/ExampleJob.groovy create mode 100644 test-suite-groovy/src/test/groovy/io/micronaut/docs/expressions/ExampleJobSpec.groovy create mode 100644 test-suite-kotlin/src/test/kotlin/io/micronaut/docs/expressions/AnnotationContextExample.kt create mode 100644 test-suite-kotlin/src/test/kotlin/io/micronaut/docs/expressions/AnnotationContextExampleTest.kt create mode 100644 test-suite-kotlin/src/test/kotlin/io/micronaut/docs/expressions/ExampleJob.kt create mode 100644 test-suite-kotlin/src/test/kotlin/io/micronaut/docs/expressions/ExampleJobTest.kt create mode 100644 test-suite/src/main/java/io/micronaut/docs/expressions/ContextRegistrar.java create mode 100644 test-suite/src/main/java/io/micronaut/docs/expressions/CustomEvaluationContext.java create mode 100644 test-suite/src/test/java/io/micronaut/docs/expressions/AnnotationContextExample.java create mode 100644 test-suite/src/test/java/io/micronaut/docs/expressions/AnnotationContextExampleTest.java create mode 100644 test-suite/src/test/java/io/micronaut/docs/expressions/ContextConsumer.java create mode 100644 test-suite/src/test/java/io/micronaut/docs/expressions/ContextConsumerTest.java create mode 100644 test-suite/src/test/java/io/micronaut/docs/expressions/ContextRegistrar.java create mode 100644 test-suite/src/test/java/io/micronaut/docs/expressions/CustomEvaluationContext.java create mode 100644 test-suite/src/test/java/io/micronaut/docs/expressions/ExampleJob.java create mode 100644 test-suite/src/test/java/io/micronaut/docs/expressions/ExampleJobTest.java diff --git a/aop/src/main/java/io/micronaut/aop/chain/DefaultInterceptorRegistry.java b/aop/src/main/java/io/micronaut/aop/chain/DefaultInterceptorRegistry.java index 1f3005e0dfd..f41e2995df6 100644 --- a/aop/src/main/java/io/micronaut/aop/chain/DefaultInterceptorRegistry.java +++ b/aop/src/main/java/io/micronaut/aop/chain/DefaultInterceptorRegistry.java @@ -34,6 +34,7 @@ import io.micronaut.core.type.Argument; import io.micronaut.core.type.Executable; import io.micronaut.inject.ExecutableMethod; +import io.micronaut.context.ContextConfigurable; import io.micronaut.inject.qualifiers.InterceptorBindingQualifier; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -238,6 +239,9 @@ public Interceptor[] resolveConstructorInterceptors( } private static void instrumentAnnotationMetadata(BeanContext beanContext, Object method) { + if (method instanceof ContextConfigurable ctxConfigurable) { + ctxConfigurable.configure(beanContext); + } if (beanContext instanceof ApplicationContext applicationContext && method instanceof EnvironmentConfigurable environmentConfigurable) { // ensure metadata is environment aware if (environmentConfigurable.hasPropertyExpressions()) { diff --git a/aop/src/main/java/io/micronaut/aop/chain/MethodInterceptorChain.java b/aop/src/main/java/io/micronaut/aop/chain/MethodInterceptorChain.java index cd792b0a919..02c50aad48d 100644 --- a/aop/src/main/java/io/micronaut/aop/chain/MethodInterceptorChain.java +++ b/aop/src/main/java/io/micronaut/aop/chain/MethodInterceptorChain.java @@ -31,6 +31,7 @@ import io.micronaut.core.util.ArrayUtils; import io.micronaut.inject.BeanDefinition; import io.micronaut.inject.ExecutableMethod; +import io.micronaut.inject.annotation.EvaluatedAnnotationMetadata; import io.micronaut.inject.qualifiers.Qualifiers; import java.lang.reflect.Method; @@ -97,6 +98,14 @@ public MethodInterceptorChain(Interceptor[] interceptors, T target, Execut this.kind = null; } + @Override + public AnnotationMetadata getAnnotationMetadata() { + if (executionHandle.getAnnotationMetadata() instanceof EvaluatedAnnotationMetadata eam) { + return eam.withArguments(originalParameters); + } + return executionHandle.getAnnotationMetadata(); + } + @Override @NonNull public InterceptorKind getKind() { diff --git a/context/src/main/java/io/micronaut/scheduling/DefaultTaskExceptionHandler.java b/context/src/main/java/io/micronaut/scheduling/DefaultTaskExceptionHandler.java index 3e219baae90..3465d6dfa21 100644 --- a/context/src/main/java/io/micronaut/scheduling/DefaultTaskExceptionHandler.java +++ b/context/src/main/java/io/micronaut/scheduling/DefaultTaskExceptionHandler.java @@ -34,14 +34,14 @@ @Primary public class DefaultTaskExceptionHandler implements TaskExceptionHandler { - private static final Logger LOG = LoggerFactory.getLogger(DefaultTaskExceptionHandler.class); + static final Logger LOG = LoggerFactory.getLogger(DefaultTaskExceptionHandler.class); @Override public void handle(@Nullable Object bean, @NonNull Throwable throwable) { if (LOG.isErrorEnabled()) { StringBuilder message = new StringBuilder("Error invoking scheduled task "); if (bean != null) { - message.append("for bean [").append(bean.toString()).append("] "); + message.append("for bean [").append(bean).append("] "); } message.append(throwable.getMessage()); LOG.error(message.toString(), throwable); diff --git a/context/src/main/java/io/micronaut/scheduling/TaskExceptionHandler.java b/context/src/main/java/io/micronaut/scheduling/TaskExceptionHandler.java index 52fda32a852..c9e3da26ed1 100644 --- a/context/src/main/java/io/micronaut/scheduling/TaskExceptionHandler.java +++ b/context/src/main/java/io/micronaut/scheduling/TaskExceptionHandler.java @@ -16,6 +16,7 @@ package io.micronaut.scheduling; import io.micronaut.core.exceptions.BeanExceptionHandler; +import io.micronaut.inject.BeanDefinition; /** * An exception handler interface for task related exceptions. @@ -26,4 +27,21 @@ * @param The generic type of the exception */ public interface TaskExceptionHandler extends BeanExceptionHandler { + + /** + * Handle an error that occurs during creation of the scheduled task. + * @param beanType The bean type + * @param throwable The throwable + * @since 4.0.0 + */ + default void handleCreationFailure(BeanDefinition beanType, E throwable) { + if (DefaultTaskExceptionHandler.LOG.isErrorEnabled()) { + StringBuilder message = new StringBuilder("Error creating scheduled task "); + if (beanType != null) { + message.append("for bean [").append(beanType.asArgument()).append("] "); + } + message.append(throwable.getMessage()); + DefaultTaskExceptionHandler.LOG.error(message.toString(), throwable); + } + } } diff --git a/context/src/main/java/io/micronaut/scheduling/annotation/Scheduled.java b/context/src/main/java/io/micronaut/scheduling/annotation/Scheduled.java index 99f2788e215..879cfc8ed79 100644 --- a/context/src/main/java/io/micronaut/scheduling/annotation/Scheduled.java +++ b/context/src/main/java/io/micronaut/scheduling/annotation/Scheduled.java @@ -82,4 +82,13 @@ * {@link java.util.concurrent.ScheduledExecutorService} to use to schedule the task */ String scheduler() default TaskExecutors.SCHEDULED; + + /** + * A custom expression that can be used to indicate whether the job should run. + * Will be evaluated each time the job is scheduled to run and if the condition evaluates to false the job will not run. + * + * @return The condition + * @since 4.0.0 + */ + String condition() default ""; } diff --git a/context/src/main/java/io/micronaut/scheduling/processor/ScheduledMethodProcessor.java b/context/src/main/java/io/micronaut/scheduling/processor/ScheduledMethodProcessor.java index b488ed69cd9..372f6b8447e 100644 --- a/context/src/main/java/io/micronaut/scheduling/processor/ScheduledMethodProcessor.java +++ b/context/src/main/java/io/micronaut/scheduling/processor/ScheduledMethodProcessor.java @@ -17,14 +17,18 @@ import io.micronaut.context.ApplicationContext; import io.micronaut.context.BeanContext; +import io.micronaut.context.Qualifier; +import io.micronaut.context.bind.DefaultExecutableBeanContextBinder; +import io.micronaut.context.bind.ExecutableBeanContextBinder; import io.micronaut.context.processor.ExecutableMethodProcessor; -import io.micronaut.core.annotation.AnnotationUtil; import io.micronaut.core.annotation.AnnotationValue; +import io.micronaut.core.bind.BoundExecutable; import io.micronaut.core.convert.ConversionService; import io.micronaut.core.type.Argument; import io.micronaut.core.util.StringUtils; import io.micronaut.inject.BeanDefinition; import io.micronaut.inject.ExecutableMethod; +import io.micronaut.inject.annotation.EvaluatedAnnotationValue; import io.micronaut.inject.qualifiers.Qualifiers; import io.micronaut.scheduling.ScheduledExecutorTaskScheduler; import io.micronaut.scheduling.TaskExceptionHandler; @@ -64,6 +68,7 @@ public class ScheduledMethodProcessor implements ExecutableMethodProcessor beanDefinition, ExecutableMethod met } TaskScheduler taskScheduler = optionalTaskScheduler.orElseThrow(() -> new SchedulerConfigurationException(method, "No scheduler of type TaskScheduler configured for name: " + scheduler)); - + Argument beanType = (Argument) beanDefinition.asArgument(); + Qualifier declaredQualifier = (Qualifier) beanDefinition.getDeclaredQualifier(); Runnable task = () -> { - io.micronaut.context.Qualifier qualifer = beanDefinition - .getAnnotationTypeByStereotype(AnnotationUtil.QUALIFIER) - .map(type -> Qualifiers.byAnnotation(beanDefinition, type)) - .orElse(null); - - Class beanType = (Class) beanDefinition.getBeanType(); - Object bean = null; try { - bean = beanContext.getBean(beanType, qualifer); - if (method.getArguments().length == 0) { - ((ExecutableMethod) method).invoke(bean); + ExecutableBeanContextBinder binder = new DefaultExecutableBeanContextBinder(); + BoundExecutable boundExecutable = binder.bind(method, beanContext); + Object bean = beanContext.getBean(beanType, declaredQualifier); + AnnotationValue finalAnnotationValue = scheduledAnnotation; + if (finalAnnotationValue instanceof EvaluatedAnnotationValue evaluated) { + finalAnnotationValue = evaluated.withArguments(boundExecutable.getBoundArguments()); } - } catch (Throwable e) { - io.micronaut.context.Qualifier qualifier = Qualifiers.byTypeArguments(beanType, e.getClass()); - Collection> definitions = beanContext.getBeanDefinitions(TaskExceptionHandler.class, qualifier); - Optional> mostSpecific = definitions.stream().filter(def -> { - List> typeArguments = def.getTypeArguments(TaskExceptionHandler.class); - if (typeArguments.size() == 2) { - return typeArguments.get(0).getType() == beanType && typeArguments.get(1).getType() == e.getClass(); + boolean shouldRun = finalAnnotationValue.booleanValue(MEMBER_CONDITION).orElse(true); + if (shouldRun) { + try { + ((BoundExecutable) boundExecutable).invoke(bean); + } catch (Throwable e) { + handleException(beanType.getType(), bean, e); } - return false; - }).findFirst(); - - TaskExceptionHandler finalHandler = mostSpecific.map(bd -> beanContext.getBean(bd.getBeanType(), qualifier)).orElse(this.taskExceptionHandler); - finalHandler.handle(bean, e); + } + } catch (Exception e) { + TaskExceptionHandler finalHandler = findHandler(beanDefinition.getBeanType(), e); + finalHandler.handleCreationFailure(beanDefinition, e); } }; @@ -188,6 +188,26 @@ public void process(BeanDefinition beanDefinition, ExecutableMethod met } } + private void handleException(Class beanType, Object bean, Throwable e) { + TaskExceptionHandler finalHandler = findHandler(beanType, e); + finalHandler.handle(bean, e); + } + + private TaskExceptionHandler findHandler(Class beanType, Throwable e) { + io.micronaut.context.Qualifier qualifier = Qualifiers.byTypeArguments(beanType, e.getClass()); + Collection> definitions = beanContext.getBeanDefinitions(TaskExceptionHandler.class, qualifier); + Optional> mostSpecific = definitions.stream().filter(def -> { + List> typeArguments = def.getTypeArguments(TaskExceptionHandler.class); + if (typeArguments.size() == 2) { + return typeArguments.get(0).getType() == beanType && typeArguments.get(1).getType() == e.getClass(); + } + return false; + }).findFirst(); + + TaskExceptionHandler finalHandler = mostSpecific.map(bd -> beanContext.getBean(bd.getBeanType(), qualifier)).orElse(this.taskExceptionHandler); + return finalHandler; + } + @Override @PreDestroy public void close() { diff --git a/core-processor/src/main/java/io/micronaut/aop/writer/AopProxyWriter.java b/core-processor/src/main/java/io/micronaut/aop/writer/AopProxyWriter.java index a7602010107..9821128df9d 100644 --- a/core-processor/src/main/java/io/micronaut/aop/writer/AopProxyWriter.java +++ b/core-processor/src/main/java/io/micronaut/aop/writer/AopProxyWriter.java @@ -41,6 +41,7 @@ import io.micronaut.core.util.ArrayUtils; import io.micronaut.core.util.Toggleable; import io.micronaut.core.value.OptionalValues; +import io.micronaut.expressions.context.ExpressionWithContext; import io.micronaut.inject.BeanDefinition; import io.micronaut.inject.ExecutableMethod; import io.micronaut.inject.ProxyBeanDefinition; @@ -1301,6 +1302,11 @@ public AnnotationMetadata getAnnotationMetadata() { return proxyBeanDefinitionWriter.getAnnotationMetadata(); } + @Override + public Set getEvaluatedExpressions() { + return proxyBeanDefinitionWriter.getEvaluatedExpressions(); + } + @Override public void visitConfigBuilderField(ClassElement type, String field, AnnotationMetadata annotationMetadata, ConfigurationMetadataBuilder metadataBuilder, boolean isInterface) { proxyBeanDefinitionWriter.visitConfigBuilderField(type, field, annotationMetadata, metadataBuilder, isInterface); @@ -1613,12 +1619,12 @@ private void processAlreadyVisitedMethods(BeanDefinitionWriter parent) { * Method Reference class with names and a list of argument types. Used as the targets. */ private static final class MethodRef { + int methodIndex; private final String name; private final List argumentTypes; private final List genericArgumentTypes; private final Type returnType; private final List rawTypes; - int methodIndex; public MethodRef(String name, List parameterElements, Type returnType) { this.name = name; diff --git a/core-processor/src/main/java/io/micronaut/expressions/EvaluatedExpressionConstants.java b/core-processor/src/main/java/io/micronaut/expressions/EvaluatedExpressionConstants.java new file mode 100644 index 00000000000..71249085c3c --- /dev/null +++ b/core-processor/src/main/java/io/micronaut/expressions/EvaluatedExpressionConstants.java @@ -0,0 +1,35 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.expressions; + +/** + * Set of constants used for evaluated expressions processing. + * + * @since 4.0.0 + * @author Sergey Gavrilov + */ +public class EvaluatedExpressionConstants { + /** + * Evaluated expression prefix. + */ + public static final String EXPRESSION_PREFIX = "#{"; + + /** + * RegEx pattern used to determine whether string value in + * annotation includes evaluated expression. + */ + public static final String EXPRESSION_PATTERN = ".*#\\{.*}.*"; +} diff --git a/core-processor/src/main/java/io/micronaut/expressions/EvaluatedExpressionWriter.java b/core-processor/src/main/java/io/micronaut/expressions/EvaluatedExpressionWriter.java new file mode 100644 index 00000000000..d08c9e1136e --- /dev/null +++ b/core-processor/src/main/java/io/micronaut/expressions/EvaluatedExpressionWriter.java @@ -0,0 +1,141 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.expressions; + +import io.micronaut.context.expressions.AbstractEvaluatedExpression; +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.expressions.ExpressionEvaluationContext; +import io.micronaut.expressions.context.ExpressionWithContext; +import io.micronaut.expressions.parser.CompoundEvaluatedEvaluatedExpressionParser; +import io.micronaut.expressions.parser.ast.ExpressionNode; +import io.micronaut.expressions.parser.compilation.ExpressionVisitorContext; +import io.micronaut.expressions.parser.exception.ExpressionCompilationException; +import io.micronaut.expressions.parser.exception.ExpressionParsingException; +import io.micronaut.inject.ast.Element; +import io.micronaut.inject.visitor.VisitorContext; +import io.micronaut.inject.writer.AbstractClassFileWriter; +import io.micronaut.inject.writer.ClassWriterOutputVisitor; +import org.objectweb.asm.ClassWriter; +import org.objectweb.asm.Type; +import org.objectweb.asm.commons.GeneratorAdapter; +import org.objectweb.asm.commons.Method; + +import java.io.IOException; +import java.io.OutputStream; +import java.util.Arrays; + +import static org.objectweb.asm.ClassWriter.COMPUTE_FRAMES; +import static org.objectweb.asm.ClassWriter.COMPUTE_MAXS; + +/** + * Writer for compile-time expressions. + * + * @author Sergey Gavrilov + * @since 4.0.0 + */ +@Internal +public final class EvaluatedExpressionWriter extends AbstractClassFileWriter { + private static final Method EVALUATED_EXPRESSIONS_CONSTRUCTOR = + new Method(CONSTRUCTOR_NAME, getConstructorDescriptor(Object.class)); + + private static final Type EVALUATED_EXPRESSION_TYPE = + Type.getType(AbstractEvaluatedExpression.class); + + private final ExpressionWithContext expressionMetadata; + private final VisitorContext visitorContext; + private final Element originatingElement; + + public EvaluatedExpressionWriter(ExpressionWithContext expressionMetadata, + VisitorContext visitorContext, + Element originatingElement) { + this.visitorContext = visitorContext; + this.expressionMetadata = expressionMetadata; + this.originatingElement = originatingElement; + } + + @Override + public void accept(ClassWriterOutputVisitor outputVisitor) throws IOException { + String expressionClassName = expressionMetadata.expressionClassName(); + try (OutputStream outputStream = outputVisitor.visitClass(expressionClassName, + getOriginatingElements())) { + ClassWriter classWriter = generateClassBytes(expressionClassName); + outputStream.write(classWriter.toByteArray()); + } + } + + private ClassWriter generateClassBytes(String expressionClassName) { + ClassWriter classWriter = new ClassWriter(COMPUTE_MAXS | COMPUTE_FRAMES); + + startPublicClass( + classWriter, + getInternalName(expressionClassName), + EVALUATED_EXPRESSION_TYPE); + + GeneratorAdapter cv = startConstructor(classWriter, Object.class); + cv.loadThis(); + cv.loadArg(0); + + cv.invokeConstructor(EVALUATED_EXPRESSION_TYPE, EVALUATED_EXPRESSIONS_CONSTRUCTOR); + // RETURN + cv.visitInsn(RETURN); + // MAXSTACK = 2 + // MAXLOCALS = 1 + cv.visitMaxs(2, 1); + + GeneratorAdapter evaluateMethodVisitor = startProtectedMethod(classWriter, "doEvaluate", + Object.class.getName(), ExpressionEvaluationContext.class.getName()); + + ExpressionVisitorContext ctx = new ExpressionVisitorContext( + expressionMetadata.evaluationContext(), + visitorContext, + evaluateMethodVisitor); + + Object annotationValue = expressionMetadata.annotationValue(); + + try { + ExpressionNode ast = new CompoundEvaluatedEvaluatedExpressionParser(annotationValue).parse(); + ast.compile(ctx); + pushBoxPrimitiveIfNecessary(ast.resolveType(ctx), evaluateMethodVisitor); + } catch (ExpressionParsingException | ExpressionCompilationException ex) { + failCompilation(ex, annotationValue); + } + + evaluateMethodVisitor.visitMaxs(2, 3); + evaluateMethodVisitor.returnValue(); + return classWriter; + } + + private void failCompilation(Throwable ex, Object initialAnnotationValue) { + String strRepresentation = null; + + if (initialAnnotationValue instanceof String str) { + strRepresentation = str; + } else if (initialAnnotationValue instanceof String[] strArray) { + strRepresentation = Arrays.toString(strArray); + } + + String message = null; + if (ex instanceof ExpressionParsingException parsingException) { + message = "Failed to parse evaluated expression [" + strRepresentation + "]. " + + "Cause: " + parsingException.getMessage(); + } else if (ex instanceof ExpressionCompilationException compilationException) { + message = "Failed to compile evaluated expression [" + strRepresentation + "]. " + + "Cause: " + compilationException.getMessage(); + } + + visitorContext.fail(message, originatingElement); + } +} diff --git a/core-processor/src/main/java/io/micronaut/expressions/context/DefaultExpressionCompilationContext.java b/core-processor/src/main/java/io/micronaut/expressions/context/DefaultExpressionCompilationContext.java new file mode 100644 index 00000000000..36bc8c254e2 --- /dev/null +++ b/core-processor/src/main/java/io/micronaut/expressions/context/DefaultExpressionCompilationContext.java @@ -0,0 +1,124 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.expressions.context; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.naming.NameUtils; +import io.micronaut.core.util.ArrayUtils; +import io.micronaut.inject.ast.ClassElement; +import io.micronaut.inject.ast.MethodElement; +import io.micronaut.inject.ast.ParameterElement; +import io.micronaut.inject.ast.PropertyElement; +import io.micronaut.inject.ast.PropertyElementQuery; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.stream.Stream; + +import static io.micronaut.inject.ast.ElementQuery.ALL_METHODS; +import static java.util.function.Predicate.not; + +/** + * Default implementation of {@link ExtensibleExpressionCompilationContext}. Extending + * this context will always return new instance instead of modifying the existing one. + * + * @since 4.0.0 + * @author Sergey Gavrilov + */ +@Internal +public class DefaultExpressionCompilationContext implements ExtensibleExpressionCompilationContext { + + private final Collection classElements; + private final MethodElement methodElement; + + DefaultExpressionCompilationContext(ClassElement... classElements) { + this(null, classElements); + } + + private DefaultExpressionCompilationContext(MethodElement methodElement, + ClassElement... classElements) { + this.methodElement = methodElement; + this.classElements = Arrays.asList(classElements); + } + + @Override + public DefaultExpressionCompilationContext extendWith(MethodElement methodElement) { + return new DefaultExpressionCompilationContext( + methodElement, + classElements.toArray(ClassElement[]::new) + ); + } + + @Override + public DefaultExpressionCompilationContext extendWith(ClassElement classElement) { + return new DefaultExpressionCompilationContext( + this.methodElement, + ArrayUtils.concat(classElements.toArray(ClassElement[]::new), classElement) + ); + } + + @Override + public List findMethods(String name) { + return classElements.stream() + .flatMap(element -> findMatchingMethods(element, name).stream()) + .toList(); + } + + private List findMatchingMethods(ClassElement classElement, String name) { + String propertyName = NameUtils.getPropertyNameForGetter(name, + PropertyElementQuery.of(classElement.getAnnotationMetadata()) + .getReadPrefixes()); + + return Stream.concat( + classElement.getEnclosedElements(ALL_METHODS.onlyAccessible().named(name)).stream(), + getNamedProperties(classElement, propertyName).stream() + .map(PropertyElement::getReadMethod) + .flatMap(Optional::stream)) + .distinct() + .filter(method -> method.getSimpleName().equals(name)) + .toList(); + } + + @Override + public List findProperties(String name) { + return classElements.stream() + .flatMap(classElement -> getNamedProperties(classElement, name).stream()) + .toList(); + } + + @Override + public List findParameters(String name) { + if (this.methodElement == null) { + return Collections.emptyList(); + } + + return Arrays.stream(methodElement.getParameters()) + .filter(parameter -> parameter.getName().equals(name)) + .toList(); + } + + private List getNamedProperties(ClassElement classElement, String name) { + return classElement.getBeanProperties( + PropertyElementQuery.of(classElement.getAnnotationMetadata()) + .includes(Collections.singleton(name))) + .stream() + .filter(not(PropertyElement::isExcluded)) + .toList(); + } +} diff --git a/core-processor/src/main/java/io/micronaut/expressions/context/DefaultExpressionCompilationContextFactory.java b/core-processor/src/main/java/io/micronaut/expressions/context/DefaultExpressionCompilationContextFactory.java new file mode 100644 index 00000000000..0277f2c56a0 --- /dev/null +++ b/core-processor/src/main/java/io/micronaut/expressions/context/DefaultExpressionCompilationContextFactory.java @@ -0,0 +1,131 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.expressions.context; + +import io.micronaut.context.annotation.AnnotationExpressionContext; +import io.micronaut.core.annotation.AnnotationClassValue; +import io.micronaut.core.annotation.NonNull; +import io.micronaut.core.expressions.EvaluatedExpressionReference; +import io.micronaut.core.annotation.AnnotationMetadata; +import io.micronaut.core.annotation.Internal; +import io.micronaut.inject.ast.ClassElement; +import io.micronaut.inject.ast.ElementQuery; +import io.micronaut.inject.ast.MethodElement; +import io.micronaut.inject.visitor.VisitorContext; +import org.jetbrains.annotations.NotNull; + +import java.util.Collection; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Factory for producing expression evaluation context. + * + * @author Sergey Gavrilov + * @since 4.0.0 + */ +@Internal +public final class DefaultExpressionCompilationContextFactory implements ExpressionCompilationContextFactory { + + private static final Collection CONTEXT_TYPES = ConcurrentHashMap.newKeySet(); + private ExtensibleExpressionCompilationContext sharedContext; + private final VisitorContext visitorContext; + + public DefaultExpressionCompilationContextFactory(VisitorContext visitorContext) { + this.sharedContext = recreateContext(); + this.visitorContext = visitorContext; + } + + @NotNull + private DefaultExpressionCompilationContext recreateContext() { + return new DefaultExpressionCompilationContext(CONTEXT_TYPES.toArray(ClassElement[]::new)); + } + + @Override + @NonNull + public ExpressionCompilationContext buildContextForMethod(@NonNull EvaluatedExpressionReference expression, + @NonNull MethodElement methodElement) { + return buildForExpression(expression) + .extendWith(methodElement); + } + + @Override + @NonNull + public ExpressionCompilationContext buildContext(EvaluatedExpressionReference expression) { + return buildForExpression(expression); + } + + @Override + public ExpressionCompilationContextFactory registerContextClass(ClassElement contextClass) { + CONTEXT_TYPES.add(contextClass); + this.sharedContext = recreateContext(); + return this; + } + + private ExtensibleExpressionCompilationContext buildForExpression(EvaluatedExpressionReference expression) { + String annotationName = expression.annotationName(); + String memberName = expression.annotationMember(); + + ClassElement annotation = visitorContext.getClassElement(annotationName).orElse(null); + + ExtensibleExpressionCompilationContext evaluationContext = sharedContext; + if (annotation != null) { + evaluationContext = addAnnotationEvaluationContext(evaluationContext, annotation); + evaluationContext = addAnnotationMemberEvaluationContext(evaluationContext, annotation, memberName); + } + + return evaluationContext; + } + + private ExtensibleExpressionCompilationContext addAnnotationEvaluationContext( + ExtensibleExpressionCompilationContext currentEvaluationContext, + ClassElement annotation) { + + return annotation.findAnnotation(AnnotationExpressionContext.class) + .flatMap(av -> av.annotationClassValue(AnnotationMetadata.VALUE_MEMBER)) + .map(AnnotationClassValue::getName) + .flatMap(visitorContext::getClassElement) + .map(currentEvaluationContext::extendWith) + .orElse(currentEvaluationContext); + } + + private ExtensibleExpressionCompilationContext addAnnotationMemberEvaluationContext( + ExtensibleExpressionCompilationContext currentEvaluationContext, + ClassElement annotation, + String annotationMember) { + + ElementQuery memberQuery = + ElementQuery.ALL_METHODS + .onlyDeclared() + .annotated(am -> am.hasAnnotation(AnnotationExpressionContext.class)) + .named(annotationMember); + + return annotation.getEnclosedElements(memberQuery).stream() + .flatMap(element -> Optional.ofNullable(element.getDeclaredAnnotation(AnnotationExpressionContext.class)).stream()) + .flatMap(av -> av.annotationClassValue(AnnotationMetadata.VALUE_MEMBER).stream()) + .map(AnnotationClassValue::getName) + .flatMap(className -> visitorContext.getClassElement(className).stream()) + .reduce(currentEvaluationContext, ExtensibleExpressionCompilationContext::extendWith, (a, b) -> a); + } + + /** + * cleanup any stored contexts. + */ + @Internal + public static void reset() { + CONTEXT_TYPES.clear(); + } +} diff --git a/core-processor/src/main/java/io/micronaut/expressions/context/ExpressionCompilationContext.java b/core-processor/src/main/java/io/micronaut/expressions/context/ExpressionCompilationContext.java new file mode 100644 index 00000000000..47fd9e494b1 --- /dev/null +++ b/core-processor/src/main/java/io/micronaut/expressions/context/ExpressionCompilationContext.java @@ -0,0 +1,62 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.expressions.context; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.NonNull; +import io.micronaut.inject.ast.MethodElement; +import io.micronaut.inject.ast.ParameterElement; +import io.micronaut.inject.ast.PropertyElement; + +import java.util.List; + +/** + * Compilation context is a set of entries which can be referenced in evaluated expression + * using the '#' sign followed by entry name. + * + * @since 4.0.0 + * @author Sergey Gavrilov + */ +@Internal +public interface ExpressionCompilationContext { + + /** + * Search methods in compilation context by name. + * + * @param name searched method name + * @return list of methods with provided name + */ + @NonNull + List findMethods(@NonNull String name); + + /** + * Search bean properties in compilation context by name. + * + * @param name searched property name + * @return list of properties with provided name + */ + @NonNull + List findProperties(@NonNull String name); + + /** + * Search method parameters in compilation context by name. + * + * @param name searched parameter name + * @return list of parameters with provided name + */ + @NonNull + List findParameters(@NonNull String name); +} diff --git a/core-processor/src/main/java/io/micronaut/expressions/context/ExpressionCompilationContextFactory.java b/core-processor/src/main/java/io/micronaut/expressions/context/ExpressionCompilationContextFactory.java new file mode 100644 index 00000000000..6c81cef084c --- /dev/null +++ b/core-processor/src/main/java/io/micronaut/expressions/context/ExpressionCompilationContextFactory.java @@ -0,0 +1,63 @@ +/* + * Copyright 2017-2023 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.expressions.context; + +import io.micronaut.core.annotation.Experimental; +import io.micronaut.core.annotation.NonNull; +import io.micronaut.core.expressions.EvaluatedExpressionReference; +import io.micronaut.inject.ast.ClassElement; +import io.micronaut.inject.ast.MethodElement; +import io.micronaut.inject.visitor.VisitorContext; + +/** + * Factory interface for producing expression evaluation context. + */ +@Experimental +public interface ExpressionCompilationContextFactory { + /** + * Builds expression evaluation context for method. Expression evaluation context + * for method allows referencing method parameter names in evaluated expressions. + * + * @param expression expression reference + * @param methodElement annotated method + * @return evaluation context for method + */ + @NonNull + ExpressionCompilationContext buildContextForMethod(@NonNull EvaluatedExpressionReference expression, + @NonNull MethodElement methodElement); + + /** + * Builds expression evaluation context for expression reference. + * + * @param expression expression reference + * @return evaluation context for method + */ + @NonNull + ExpressionCompilationContext buildContext(EvaluatedExpressionReference expression); + + /** + * Adds evaluated expression context class element to context loader + * at compilation time. + * + *

This method should be invoked from the {@link io.micronaut.inject.visitor.TypeElementVisitor#start(VisitorContext)} of a {@link io.micronaut.inject.visitor.TypeElementVisitor}

+ * + * @param contextClass context class element + * @return This context factory + */ + @NonNull + ExpressionCompilationContextFactory registerContextClass(@NonNull ClassElement contextClass); + +} diff --git a/core-processor/src/main/java/io/micronaut/expressions/context/ExpressionEvaluationContextRegistrar.java b/core-processor/src/main/java/io/micronaut/expressions/context/ExpressionEvaluationContextRegistrar.java new file mode 100644 index 00000000000..66ed5febb15 --- /dev/null +++ b/core-processor/src/main/java/io/micronaut/expressions/context/ExpressionEvaluationContextRegistrar.java @@ -0,0 +1,51 @@ +/* + * Copyright 2017-2023 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.expressions.context; + +import io.micronaut.context.annotation.AnnotationExpressionContext; +import io.micronaut.core.annotation.Experimental; +import io.micronaut.inject.ast.ClassElement; +import io.micronaut.inject.visitor.TypeElementVisitor; +import io.micronaut.inject.visitor.VisitorContext; + +/** + * Custom type that simplifies registering a new context class. + * + *

A {@code META-INF/services/io.micronaut.inject.visitor.TypeElementVisitor} should be created for any new implementations.

+ * + * @since 4.0.0 + */ +@Experimental +public interface ExpressionEvaluationContextRegistrar extends TypeElementVisitor { + @Override + default void start(VisitorContext visitorContext) { + ClassElement contextClass = visitorContext.getClassElement(getContextClassName()) + .orElse(null); + if (contextClass == null) { + visitorContext.fail("Evaluation context class is not on the compilation classpath: " + getContextClassName(), null); + } else { + visitorContext.getExpressionCompilationContextFactory() + .registerContextClass(contextClass); + } + } + + String getContextClassName(); + + @Override + default VisitorKind getVisitorKind() { + return VisitorKind.ISOLATING; + } +} diff --git a/core-processor/src/main/java/io/micronaut/expressions/context/ExpressionWithContext.java b/core-processor/src/main/java/io/micronaut/expressions/context/ExpressionWithContext.java new file mode 100644 index 00000000000..aba972bd491 --- /dev/null +++ b/core-processor/src/main/java/io/micronaut/expressions/context/ExpressionWithContext.java @@ -0,0 +1,55 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.expressions.context; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.NonNull; +import io.micronaut.core.expressions.EvaluatedExpressionReference; + +/** + * Metadata for evaluated expression used at compilation time + * to generate expression class. + * + * @param expressionReference reference to evaluated expression in annotation + * @param evaluationContext the context against which expression will be evaluated + * + * @author Sergey Gavrilov + * @since 4.0.0 + */ +@Internal +public record ExpressionWithContext(@NonNull EvaluatedExpressionReference expressionReference, + @NonNull ExpressionCompilationContext evaluationContext) { + + /** + * Provides initial annotation value treated as evaluated expression. + * + * @return initial annotation value + */ + @NonNull + public Object annotationValue() { + return expressionReference.annotationValue(); + } + + /** + * Provides generated class name for this expression. + * + * @return expression class name + */ + @NonNull + public String expressionClassName() { + return expressionReference.expressionClassName(); + } +} diff --git a/core-processor/src/main/java/io/micronaut/expressions/context/ExtensibleExpressionCompilationContext.java b/core-processor/src/main/java/io/micronaut/expressions/context/ExtensibleExpressionCompilationContext.java new file mode 100644 index 00000000000..59ce4a0ab90 --- /dev/null +++ b/core-processor/src/main/java/io/micronaut/expressions/context/ExtensibleExpressionCompilationContext.java @@ -0,0 +1,52 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.expressions.context; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.NonNull; +import io.micronaut.inject.ast.ClassElement; +import io.micronaut.inject.ast.MethodElement; + +/** + * Expression compilation context that can be extended with extra elements. + * + * @since 4.0.0 + * @author Sergey Gavrilov + */ +@Internal +public interface ExtensibleExpressionCompilationContext extends ExpressionCompilationContext { + /** + * Extends compilation context with method element. Compilation context can only include + * one method at the same time, so this method will return the context which will + * replace previous context method element if it was set. + * + * @param methodElement extending method + * @return extended context + */ + @NonNull + ExtensibleExpressionCompilationContext extendWith(@NonNull MethodElement methodElement); + + /** + * Extends compilation context with class element. Compilation context can include + * multiple class elements at the same time. + * + * @param classElement extending class + * @return extended context + */ + @NonNull + ExtensibleExpressionCompilationContext extendWith(@NonNull ClassElement classElement); + +} diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/CompoundEvaluatedEvaluatedExpressionParser.java b/core-processor/src/main/java/io/micronaut/expressions/parser/CompoundEvaluatedEvaluatedExpressionParser.java new file mode 100644 index 00000000000..da5d727bd1b --- /dev/null +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/CompoundEvaluatedEvaluatedExpressionParser.java @@ -0,0 +1,150 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.expressions.parser; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.NonNull; +import io.micronaut.expressions.parser.ast.ExpressionNode; +import io.micronaut.expressions.parser.ast.collection.OneDimensionalArray; +import io.micronaut.expressions.parser.ast.literal.StringLiteral; +import io.micronaut.expressions.parser.ast.operator.binary.AddOperator; +import io.micronaut.expressions.parser.ast.types.TypeIdentifier; +import io.micronaut.expressions.parser.exception.ExpressionParsingException; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import static io.micronaut.expressions.EvaluatedExpressionConstants.EXPRESSION_PREFIX; + + +/** + * This parser is used to split complex expression into multiple + * single expressions if necessary and delegate each atomic expression + * parsing to separate instance of {@link SingleEvaluatedEvaluatedExpressionParser}, + * then combining single expressions parsing results. + * + * @author Sergey Gavrilov + * @since 4.0.0 + */ +@Internal +public final class CompoundEvaluatedEvaluatedExpressionParser implements EvaluatedExpressionParser { + + private final Object expression; + + /** + * Instantiates compound expression parser. + * + * @param expression either string or string[] + */ + public CompoundEvaluatedEvaluatedExpressionParser(@NonNull Object expression) { + if (!(expression instanceof String || expression instanceof String[])) { + throw new ExpressionParsingException("Can not parse expression: " + expression); + } + + this.expression = expression; + } + + @Override + public ExpressionNode parse() throws ExpressionParsingException { + // if expression doesn't have prefix, the whole string is treated as expression + if (expression instanceof String str && !str.contains(EXPRESSION_PREFIX)) { + return new SingleEvaluatedEvaluatedExpressionParser(str).parse(); + } + + return parseTemplateExpression(expression); + } + + private ExpressionNode parseTemplateExpression(Object expression) { + if (expression instanceof String str) { + List expressionParts = + splitExpressionParts(str).stream() + .map(this::prepareExpressionPart) + .map(SingleEvaluatedEvaluatedExpressionParser::new) + .map(SingleEvaluatedEvaluatedExpressionParser::parse) + .toList(); + + if (expressionParts.size() == 1) { + return expressionParts.get(0); + } else { + return expressionParts.stream() + .reduce(new StringLiteral(""), AddOperator::new); + } + } else { + List arrayNodes = + Arrays.stream((String[]) expression) + .map(this::parseTemplateExpression) + .toList(); + + return buildArrayOfExpressions(arrayNodes); + } + } + + private ExpressionNode buildArrayOfExpressions(List nodes) { + TypeIdentifier arrayElementType = new TypeIdentifier("Object"); + return new OneDimensionalArray(arrayElementType, nodes); + } + + private List splitExpressionParts(String expression) { + List parts = new ArrayList<>(); + String nextPart = nextPart(expression); + while (!nextPart.isEmpty()) { + parts.add(nextPart); + expression = expression.substring(nextPart.length()); + nextPart = nextPart(expression); + } + + return parts; + } + + private String nextPart(String expression) { + if (expression.startsWith(EXPRESSION_PREFIX)) { + int unbalancedParenthesis = 1; + + StringBuilder expressionPart = new StringBuilder(EXPRESSION_PREFIX); + int pointer = EXPRESSION_PREFIX.length(); + while (unbalancedParenthesis > 0 && pointer < expression.length()) { + char nextChar = expression.charAt(pointer++); + expressionPart.append(nextChar); + if (nextChar == '{') { + unbalancedParenthesis++; + } else if (nextChar == '}') { + unbalancedParenthesis--; + } + } + + if (unbalancedParenthesis > 0) { + throw new ExpressionParsingException("Unbalanced parenthesis in expression: " + expression); + } + + return expressionPart.toString(); + } else { + int substringUntil = expression.contains(EXPRESSION_PREFIX) + ? expression.indexOf(EXPRESSION_PREFIX) + : expression.length(); + return expression.substring(0, substringUntil); + } + } + + private String prepareExpressionPart(String expressionPart) { + if (expressionPart.startsWith(EXPRESSION_PREFIX)) { + return expressionPart.substring(EXPRESSION_PREFIX.length(), + expressionPart.length() - 1); + } else { + return "'" + expressionPart + "'"; + } + } +} diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/EvaluatedExpressionParser.java b/core-processor/src/main/java/io/micronaut/expressions/parser/EvaluatedExpressionParser.java new file mode 100644 index 00000000000..fe218c32691 --- /dev/null +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/EvaluatedExpressionParser.java @@ -0,0 +1,40 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.expressions.parser; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.NonNull; +import io.micronaut.expressions.parser.ast.ExpressionNode; +import io.micronaut.expressions.parser.exception.ExpressionParsingException; + +/** + * Interface for evaluated expression parser. + * + * @author Sergey Gavrilov + * @since 4.0.0 + */ +@Internal +public sealed interface EvaluatedExpressionParser permits SingleEvaluatedEvaluatedExpressionParser, + CompoundEvaluatedEvaluatedExpressionParser { + /** + * Parse expression into AST. + * + * @return expression AST + * @throws ExpressionParsingException when expression violates syntactic rules + */ + @NonNull + ExpressionNode parse() throws ExpressionParsingException; +} diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/SingleEvaluatedEvaluatedExpressionParser.java b/core-processor/src/main/java/io/micronaut/expressions/parser/SingleEvaluatedEvaluatedExpressionParser.java new file mode 100644 index 00000000000..24e437bdf6b --- /dev/null +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/SingleEvaluatedEvaluatedExpressionParser.java @@ -0,0 +1,549 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.expressions.parser; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.expressions.parser.ast.ExpressionNode; +import io.micronaut.expressions.parser.ast.access.ContextElementAccess; +import io.micronaut.expressions.parser.ast.access.ContextMethodCall; +import io.micronaut.expressions.parser.ast.access.ElementMethodCall; +import io.micronaut.expressions.parser.ast.access.SubscriptOperator; +import io.micronaut.expressions.parser.ast.access.PropertyAccess; +import io.micronaut.expressions.parser.ast.conditional.ElvisOperator; +import io.micronaut.expressions.parser.ast.conditional.TernaryExpression; +import io.micronaut.expressions.parser.ast.literal.BoolLiteral; +import io.micronaut.expressions.parser.ast.literal.DoubleLiteral; +import io.micronaut.expressions.parser.ast.literal.FloatLiteral; +import io.micronaut.expressions.parser.ast.literal.IntLiteral; +import io.micronaut.expressions.parser.ast.literal.LongLiteral; +import io.micronaut.expressions.parser.ast.literal.NullLiteral; +import io.micronaut.expressions.parser.ast.literal.StringLiteral; +import io.micronaut.expressions.parser.ast.operator.binary.InstanceofOperator; +import io.micronaut.expressions.parser.ast.operator.binary.MatchesOperator; +import io.micronaut.expressions.parser.ast.operator.binary.PowOperator; +import io.micronaut.expressions.parser.ast.operator.binary.AndOperator; +import io.micronaut.expressions.parser.ast.operator.binary.OrOperator; +import io.micronaut.expressions.parser.ast.operator.binary.AddOperator; +import io.micronaut.expressions.parser.ast.operator.binary.DivOperator; +import io.micronaut.expressions.parser.ast.operator.binary.ModOperator; +import io.micronaut.expressions.parser.ast.operator.binary.MulOperator; +import io.micronaut.expressions.parser.ast.operator.binary.SubOperator; +import io.micronaut.expressions.parser.ast.operator.binary.EqOperator; +import io.micronaut.expressions.parser.ast.operator.binary.GtOperator; +import io.micronaut.expressions.parser.ast.operator.binary.GteOperator; +import io.micronaut.expressions.parser.ast.operator.binary.LtOperator; +import io.micronaut.expressions.parser.ast.operator.binary.LteOperator; +import io.micronaut.expressions.parser.ast.operator.binary.NeqOperator; +import io.micronaut.expressions.parser.ast.operator.unary.EmptyOperator; +import io.micronaut.expressions.parser.ast.operator.unary.NegOperator; +import io.micronaut.expressions.parser.ast.operator.unary.NotOperator; +import io.micronaut.expressions.parser.ast.operator.unary.PosOperator; +import io.micronaut.expressions.parser.ast.types.TypeIdentifier; +import io.micronaut.expressions.parser.exception.ExpressionParsingException; +import io.micronaut.expressions.parser.token.Token; +import io.micronaut.expressions.parser.token.TokenType; +import io.micronaut.expressions.parser.token.Tokenizer; + +import java.util.ArrayList; +import java.util.List; + +import static io.micronaut.expressions.parser.token.TokenType.BOOL; +import static io.micronaut.expressions.parser.token.TokenType.COLON; +import static io.micronaut.expressions.parser.token.TokenType.DECREMENT; +import static io.micronaut.expressions.parser.token.TokenType.DIV; +import static io.micronaut.expressions.parser.token.TokenType.DOT; +import static io.micronaut.expressions.parser.token.TokenType.DOUBLE; +import static io.micronaut.expressions.parser.token.TokenType.ELVIS; +import static io.micronaut.expressions.parser.token.TokenType.EMPTY; +import static io.micronaut.expressions.parser.token.TokenType.EQ; +import static io.micronaut.expressions.parser.token.TokenType.EXPRESSION_CONTEXT_REF; +import static io.micronaut.expressions.parser.token.TokenType.FLOAT; +import static io.micronaut.expressions.parser.token.TokenType.GT; +import static io.micronaut.expressions.parser.token.TokenType.GTE; +import static io.micronaut.expressions.parser.token.TokenType.R_SQUARE; +import static io.micronaut.expressions.parser.token.TokenType.TYPE_IDENTIFIER; +import static io.micronaut.expressions.parser.token.TokenType.INCREMENT; +import static io.micronaut.expressions.parser.token.TokenType.INSTANCEOF; +import static io.micronaut.expressions.parser.token.TokenType.INT; +import static io.micronaut.expressions.parser.token.TokenType.LONG; +import static io.micronaut.expressions.parser.token.TokenType.LT; +import static io.micronaut.expressions.parser.token.TokenType.LTE; +import static io.micronaut.expressions.parser.token.TokenType.L_PAREN; +import static io.micronaut.expressions.parser.token.TokenType.L_SQUARE; +import static io.micronaut.expressions.parser.token.TokenType.MATCHES; +import static io.micronaut.expressions.parser.token.TokenType.MINUS; +import static io.micronaut.expressions.parser.token.TokenType.MOD; +import static io.micronaut.expressions.parser.token.TokenType.MUL; +import static io.micronaut.expressions.parser.token.TokenType.NE; +import static io.micronaut.expressions.parser.token.TokenType.NOT; +import static io.micronaut.expressions.parser.token.TokenType.NULL; +import static io.micronaut.expressions.parser.token.TokenType.OR; +import static io.micronaut.expressions.parser.token.TokenType.PLUS; +import static io.micronaut.expressions.parser.token.TokenType.POW; +import static io.micronaut.expressions.parser.token.TokenType.QMARK; +import static io.micronaut.expressions.parser.token.TokenType.R_PAREN; +import static io.micronaut.expressions.parser.token.TokenType.SAFE_NAV; +import static io.micronaut.expressions.parser.token.TokenType.STRING; + +/** + * Parser for building AST for single evaluated expression. + * A single expression is parsed as a whole, + * it cannot contain multiple expressions. + * + * @author Sergey Gavrilov + * @since 4.0.0 + */ +@Internal +public final class SingleEvaluatedEvaluatedExpressionParser implements EvaluatedExpressionParser { + private final Tokenizer tokenizer; + private Token lookahead; + + /** + * Instantiates a parser for single passed expression. + * Expression string must not contain expression template wrapper like #{...} + * + * @param expression expression to parse + */ + public SingleEvaluatedEvaluatedExpressionParser(String expression) { + this.tokenizer = new Tokenizer(expression); + this.lookahead = tokenizer.getNextToken(); + } + + @Override + public ExpressionNode parse() throws ExpressionParsingException { + try { + final ExpressionNode expressionNode = expression(); + if (lookahead != null) { + throw new ExpressionParsingException("Unexpected token: " + lookahead.value()); + } + return expressionNode; + } catch (NullPointerException ex) { + throw new ExpressionParsingException("Unexpected end of input"); + } + } + + // Expression + // : TernaryExpression + // ; + private ExpressionNode expression() { + return ternaryExpression(); + } + + // TernaryExpression + // : OrExpression + // | OrExpression '?' Expression ':' Expression + // ; + private ExpressionNode ternaryExpression() { + ExpressionNode orExpression = orExpression(); + if (lookahead != null) { + if (lookahead.type() == QMARK) { + eat(QMARK); + ExpressionNode trueExpr = expression(); + eat(COLON); + ExpressionNode falseExpr = expression(); + return new TernaryExpression(orExpression, trueExpr, falseExpr); + } else if (lookahead.type() == ELVIS) { + eat(ELVIS); + ExpressionNode falseExpr = expression(); + return new ElvisOperator(orExpression, falseExpr); + } + } + return orExpression; + } + + // OrExpression + // : AndExpression + // | OrExpression '||' AndExpression -> AndExpression '||' AndExpression '||' AndExpression + // ; + private ExpressionNode orExpression() { + ExpressionNode leftNode = andExpression(); + while (lookahead != null && lookahead.type() == OR) { + eat(OR); + leftNode = new OrOperator(leftNode, andExpression()); + } + return leftNode; + } + + // AndExpression + // : EqualityExpression + // | AndExpression '&&' EqualityExpression + // ; + private ExpressionNode andExpression() { + ExpressionNode leftNode = equalityExpression(); + while (lookahead != null && lookahead.type() == TokenType.AND) { + eat(TokenType.AND); + leftNode = new AndOperator(leftNode, equalityExpression()); + } + return leftNode; + } + + // EqualityExpression + // : RelationalExpression + // | EqualityExpression '==' RelationalExpression + // | EqualityExpression '!=' RelationalExpression + // ; + private ExpressionNode equalityExpression() { + ExpressionNode leftNode = relationalExpression(); + while (lookahead != null && lookahead.type().isOneOf(EQ, NE)) { + TokenType tokenType = lookahead.type(); + eat(tokenType); + if (tokenType == EQ) { + leftNode = new EqOperator(leftNode, relationalExpression()); + } else if (tokenType == NE) { + leftNode = new NeqOperator(leftNode, relationalExpression()); + } + } + return leftNode; + } + + // RelationalExpression + // : AdditiveExpression + // | RelationalExpression RelOperator AdditiveExpression + // | RelationalExpression 'instanceof' TypeIdentifier + // | RelationalExpression 'matches' StringLiteral + // ; + private ExpressionNode relationalExpression() { + ExpressionNode leftNode = additiveExpression(); + while (lookahead != null && (lookahead.type() + .isOneOf(GT, GTE, LT, LTE, INSTANCEOF, MATCHES))) { + TokenType tokenType = lookahead.type(); + eat(lookahead.type()); + leftNode = switch (tokenType) { + case GT -> new GtOperator(leftNode, additiveExpression()); + case LT -> new LtOperator(leftNode, additiveExpression()); + case GTE -> new GteOperator(leftNode, additiveExpression()); + case LTE -> new LteOperator(leftNode, additiveExpression()); + case INSTANCEOF -> new InstanceofOperator(leftNode, typeIdentifier()); + case MATCHES -> new MatchesOperator(leftNode, stringLiteral()); + default -> leftNode; + }; + } + return leftNode; + } + + // AdditiveExpression + // : PowExpression + // | AdditiveExpression '+' PowExpression + // | AdditiveExpression '-' PowExpression + // ; + private ExpressionNode additiveExpression() { + ExpressionNode leftNode = multiplicativeExpression(); + while (lookahead != null && lookahead.type().isOneOf(PLUS, MINUS)) { + TokenType tokenType = lookahead.type(); + eat(tokenType); + if (tokenType == PLUS) { + leftNode = new AddOperator(leftNode, multiplicativeExpression()); + } else if (tokenType == MINUS) { + leftNode = new SubOperator(leftNode, multiplicativeExpression()); + } + } + return leftNode; + } + + // MultiplicativeExpression + // : PowExpression + // | MultiplicativeExpression '*' PowExpression + // | MultiplicativeExpression '/' PowExpression + // | MultiplicativeExpression '%' PowExpression + // ; + private ExpressionNode multiplicativeExpression() { + ExpressionNode leftNode = powExpression(); + while (lookahead != null && lookahead.type().isOneOf(MUL, DIV, MOD)) { + TokenType tokenType = lookahead.type(); + eat(tokenType); + if (tokenType == MUL) { + leftNode = new MulOperator(leftNode, powExpression()); + } else if (tokenType == DIV) { + leftNode = new DivOperator(leftNode, powExpression()); + } else if (tokenType == MOD) { + leftNode = new ModOperator(leftNode, powExpression()); + } + } + return leftNode; + } + + // PowExpression + // : UnaryExpression + // | PowExpression '^' UnaryExpression + // ; + private ExpressionNode powExpression() { + ExpressionNode leftNode = unaryExpression(); + while (lookahead != null && lookahead.type() == POW) { + eat(POW); + leftNode = new PowOperator(leftNode, unaryExpression()); + } + return leftNode; + } + + // UnaryExpression + // : '+' UnaryExpression + // | '-' UnaryExpression + // | '!' UnaryExpression + // | '++' UnaryExpression + // | '--' UnaryExpression + // | PostfixExpression + // ; + private ExpressionNode unaryExpression() { + TokenType tokenType = lookahead.type(); + if (tokenType == PLUS) { + eat(PLUS); + return new PosOperator(unaryExpression()); + } else if (tokenType == MINUS) { + eat(MINUS); + return new NegOperator(unaryExpression()); + } else if (tokenType == NOT) { + eat(NOT); + return new NotOperator(unaryExpression()); + } else if (tokenType == EMPTY) { + eat(EMPTY); + return new EmptyOperator(unaryExpression()); + } else if (tokenType == INCREMENT) { + throw new ExpressionParsingException("Prefix increment operation is not supported"); + } else if (tokenType == DECREMENT) { + throw new ExpressionParsingException("Prefix decrement operation is not supported"); + } else { + return postfixExpression(); + } + } + + // PostfixExpression + // : PrimaryExpression + // | PostfixExpression '.' MethodOrPropertyAccess + // | PostfixExpression '?.' MethodOrPropertyAccess with safe navigation + // | PostfixExpression '++' + // | PostfixExpression '--' + // ; + private ExpressionNode postfixExpression() { + ExpressionNode leftNode = primaryExpression(); + while (lookahead != null && (lookahead.type() + .isOneOf(DOT, SAFE_NAV, L_SQUARE, INCREMENT, DECREMENT))) { + TokenType tokenType = lookahead.type(); + if (tokenType == INCREMENT) { + throw new ExpressionParsingException("Postfix increment operation is not " + + "supported"); + } else if (tokenType == DECREMENT) { + throw new ExpressionParsingException("Postfix decrement operation is not " + + "supported"); + } else if (tokenType == DOT) { + eat(DOT); + leftNode = methodOrPropertyAccess(leftNode, false); + } else if (tokenType == SAFE_NAV) { + eat(SAFE_NAV); + leftNode = methodOrPropertyAccess(leftNode, true); + } else if (tokenType == L_SQUARE) { + eat(L_SQUARE); + leftNode = subscriptOperator(leftNode); + } else { + throw new ExpressionParsingException("Unexpected token: " + lookahead.value()); + } + } + return leftNode; + } + + // PrimaryExpression + // : ContextAccess + // | TypeIdentifier + // | ParenthesizedExpression + // | Literal + // ; + private ExpressionNode primaryExpression() { + return switch (lookahead.type()) { + case EXPRESSION_CONTEXT_REF -> contextAccess(); + case IDENTIFIER -> identifier(); + case TYPE_IDENTIFIER -> typeIdentifier(); + case L_PAREN -> parenthesizedExpression(); + case STRING, INT, LONG, DOUBLE, FLOAT, BOOL, NULL -> literal(); + default -> throw new ExpressionParsingException("Unexpected token: " + lookahead.value()); + }; + } + + // ContextAccess + // : '#' Identifier + // | '#' Identifier MethodArguments + // ; + private ExpressionNode contextAccess() { + eat(EXPRESSION_CONTEXT_REF); + return primaryExpression(); + } + + private ExpressionNode identifier() { + String identifier = eat(TokenType.IDENTIFIER).value(); + if (lookahead != null && lookahead.type() == L_PAREN) { + List methodArguments = methodArguments(); + return new ContextMethodCall(identifier, methodArguments); + } + return new ContextElementAccess(identifier); + } + + // MethodOrFieldAccess + // : SimpleIdentifier + // | SimpleIdentifier MethodArguments + // ; + private ExpressionNode methodOrPropertyAccess(ExpressionNode callee, boolean nullSafe) { + String identifier = eat(TokenType.IDENTIFIER).value(); + if (lookahead != null && lookahead.type() == L_PAREN) { + List methodArguments = methodArguments(); + return new ElementMethodCall(callee, identifier, methodArguments, nullSafe); + } + return new PropertyAccess(callee, identifier, nullSafe); + } + + // SubscriptOperator + // : SimpleIdentifier + // | SimpleIdentifier [index] + // ; + private ExpressionNode subscriptOperator(ExpressionNode callee) { + if (lookahead != null) { + ExpressionNode indexExpression = expression(); + SubscriptOperator subscriptOperator = new SubscriptOperator( + callee, + indexExpression + ); + eat(R_SQUARE); + return subscriptOperator; + } else { + throw new ExpressionParsingException("Unclosed subscript operator"); + } + } + + // MethodArguments: + // '(' MethodArgumentsList ')' + // ; + private List methodArguments() { + eat(L_PAREN); + List arguments = new ArrayList<>(); + if (lookahead.type() != R_PAREN) { + arguments = methodArgumentsList(); + } + eat(R_PAREN); + return arguments; + } + + // MethodArgumentsList + // : Expression + // | MethodArgumentsList ',' Expression + // ; + private List methodArgumentsList() { + List arguments = new ArrayList<>(); + if (lookahead.type() != R_PAREN) { + ExpressionNode firstArgument = expression(); + arguments.add(firstArgument); + + while (lookahead.type() != R_PAREN) { + eat(TokenType.COMMA); + arguments.add(expression()); + } + } + return arguments; + } + + // TypeReference + // : 'T(' ChainedIdentifier')' + // ; + private TypeIdentifier typeIdentifier() { + eat(TYPE_IDENTIFIER); + List parts = new ArrayList<>(); + parts.add(eat(TokenType.IDENTIFIER).value()); + while (lookahead != null && lookahead.type() == DOT) { + eat(DOT); + parts.add(eat(TokenType.IDENTIFIER).value()); + } + eat(R_PAREN); + return new TypeIdentifier(String.join(".", parts)); + } + + // ParenthesizedExpression + // : '(' Expression ')' + // ; + private ExpressionNode parenthesizedExpression() { + eat(L_PAREN); + ExpressionNode parenthesizedExpression = expression(); + eat(R_PAREN); + return parenthesizedExpression; + } + + // Literal + // : StringLiteral + // | IntLiteral + // | LongLiteral + // | DecimalLiteral + // | FloatLiteral + // | BoolLiteral + // ; + private ExpressionNode literal() { + return switch (lookahead.type()) { + case DOUBLE -> doubleLiteral(); + case FLOAT -> floatLiteral(); + case INT -> intLiteral(); + case STRING -> stringLiteral(); + case LONG -> longLiteral(); + case BOOL -> boolLiteral(); + case NULL -> nullLiteral(); + default -> throw new ExpressionParsingException("Unknown literal type: " + lookahead.type()); + }; + } + + private StringLiteral stringLiteral() { + Token token = eat(STRING); + String value = token.value(); + // removing surrounding quotes + return new StringLiteral(token.value().substring(1, value.length() - 1)); + } + + private DoubleLiteral doubleLiteral() { + Token token = eat(DOUBLE); + return new DoubleLiteral(Double.parseDouble(token.value())); + } + + private FloatLiteral floatLiteral() { + Token token = eat(FLOAT); + return new FloatLiteral(Float.parseFloat(token.value())); + } + + private IntLiteral intLiteral() { + Token token = eat(INT); + return new IntLiteral(Integer.decode(token.value())); + } + + private LongLiteral longLiteral() { + Token token = eat(LONG); + return new LongLiteral(Long.decode(token.value().replaceAll("([lL])", ""))); + } + + private BoolLiteral boolLiteral() { + Token token = eat(BOOL); + return new BoolLiteral(Boolean.parseBoolean(token.value())); + } + + private NullLiteral nullLiteral() { + eat(NULL); + return new NullLiteral(); + } + + private Token eat(TokenType tokenType) { + if (lookahead == null) { + throw new ExpressionParsingException("Unexpected end of input. Expected: '" + tokenType + "'"); + } + + Token token = lookahead; + if (token.type() != tokenType) { + throw new ExpressionParsingException("Unexpected token: " + token.value() + ". Expected: '" + tokenType + "'"); + } + + lookahead = tokenizer.getNextToken(); + return token; + } +} diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/ExpressionNode.java b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/ExpressionNode.java new file mode 100644 index 00000000000..2134b16e79d --- /dev/null +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/ExpressionNode.java @@ -0,0 +1,102 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.expressions.parser.ast; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.NonNull; +import io.micronaut.expressions.parser.compilation.ExpressionVisitorContext; +import io.micronaut.inject.ast.ClassElement; +import org.objectweb.asm.Type; + +/** + * Abstract evaluated expression AST node. + * + * @author Sergey Gavrilov + * @since 4.0.0 + */ +@Internal +public abstract class ExpressionNode { + + protected Type nodeType; + protected ClassElement classElement; + + /** + * Compiles this expression AST node against passes compilation context. + * Node compilation includes type resolution and bytecode generation. + * + * @param ctx expression compilation context + */ + public final void compile(@NonNull ExpressionVisitorContext ctx) { + resolveType(ctx); + generateBytecode(ctx); + } + + /** + * Generates bytecode for this AST node. + * + * @param ctx expression compilation context + */ + protected abstract void generateBytecode(@NonNull ExpressionVisitorContext ctx); + + /** + * On resolution stage type information is collected and node validity is checked. Once type + * is resolved, type resolution result is cached. + * + * @param ctx expression compilation context + * + * @return resolved type + */ + @NonNull + public final Type resolveType(@NonNull ExpressionVisitorContext ctx) { + if (nodeType == null) { + nodeType = doResolveType(ctx); + } + return nodeType; + } + + /** + * On resolution stage type information is collected and node validity is checked. Once type + * is resolved, type resolution result is cached. + * + * @param ctx expression compilation context + * + * @return resolved type + */ + @NonNull + public final ClassElement resolveClassElement(@NonNull ExpressionVisitorContext ctx) { + if (classElement == null) { + classElement = doResolveClassElement(ctx); + } + return classElement; + } + + /** + * Resolves the class element for this node. + * @param ctx The expression compilation context + * @return The resolved type + */ + protected abstract ClassElement doResolveClassElement(ExpressionVisitorContext ctx); + + /** + * Resolves expression AST node type. + * + * @param ctx expression compilation context + * + * @return resolved type + */ + @NonNull + protected abstract Type doResolveType(@NonNull ExpressionVisitorContext ctx); +} diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/access/AbstractMethodCall.java b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/access/AbstractMethodCall.java new file mode 100644 index 00000000000..bc88b659388 --- /dev/null +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/access/AbstractMethodCall.java @@ -0,0 +1,207 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.expressions.parser.ast.access; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.NonNull; +import io.micronaut.expressions.parser.ast.ExpressionNode; +import io.micronaut.expressions.parser.ast.collection.OneDimensionalArray; +import io.micronaut.expressions.parser.ast.util.TypeDescriptors; +import io.micronaut.expressions.parser.ast.types.TypeIdentifier; +import io.micronaut.expressions.parser.compilation.ExpressionVisitorContext; +import io.micronaut.inject.ast.ClassElement; +import io.micronaut.inject.ast.MethodElement; +import io.micronaut.inject.visitor.VisitorContext; +import org.objectweb.asm.Type; +import org.objectweb.asm.commons.GeneratorAdapter; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +import static io.micronaut.expressions.parser.ast.util.TypeDescriptors.isPrimitive; +import static io.micronaut.expressions.parser.ast.util.EvaluatedExpressionCompilationUtils.getRequiredClassElement; +import static io.micronaut.expressions.parser.ast.util.EvaluatedExpressionCompilationUtils.pushBoxPrimitiveIfNecessary; +import static io.micronaut.expressions.parser.ast.util.EvaluatedExpressionCompilationUtils.pushUnboxPrimitiveIfNecessary; +import static io.micronaut.inject.processing.JavaModelUtils.getTypeReference; + +/** + * Abstract expression AST node for method calls. + * + * @author Sergey Gavrilov + * @since 4.0.0 + */ +@Internal +public abstract sealed class AbstractMethodCall extends ExpressionNode permits ContextMethodCall, + ElementMethodCall { + protected final String name; + protected final List arguments; + + protected CandidateMethod usedMethod; + + public AbstractMethodCall(String name, + List arguments) { + this.name = name; + this.arguments = arguments; + } + + @Override + protected Type doResolveType(ExpressionVisitorContext ctx) { + if (usedMethod == null) { + usedMethod = resolveUsedMethod(ctx); + } + return usedMethod.getReturnType(); + } + + @Override + protected ClassElement doResolveClassElement(ExpressionVisitorContext ctx) { + doResolveType(ctx); + return usedMethod.getMethodElement().getGenericReturnType(); + } + + /** + * Resolves single {@link CandidateMethod} used by this AST node. + * + * @param ctx Expression compilation context + * @return AST node candidate method + * @throws io.micronaut.expressions.parser.exception.ExpressionCompilationException if no + * candidate method can be found or if there is more than one candidate method. + */ + @NonNull + protected abstract CandidateMethod resolveUsedMethod(ExpressionVisitorContext ctx); + + /** + * Builds candidate method for method element. + * + * @param ctx expression compilation context + * @param methodElement method element + * @param argumentTypes types of arguments used for method invocation in expression + * + * @return candidate method + */ + protected CandidateMethod toCandidateMethod(ExpressionVisitorContext ctx, + MethodElement methodElement, + List argumentTypes) { + VisitorContext visitorContext = ctx.visitorContext(); + + List arguments = + argumentTypes.stream() + .map(type -> getRequiredClassElement(type, visitorContext)) + .toList(); + + return new CandidateMethod(methodElement, arguments); + } + + /** + * This method wraps original method arguments into + * array for methods using varargs. + * + * @return list of arguments, including varargs arguments wrapped in array + */ + protected List prepareVarargsArguments() { + List arguments = new ArrayList<>(); + int varargsIndex = usedMethod.getVarargsIndex(); + + List nodesWrappedInArray = new ArrayList<>(); + for (int i = 0; i < this.arguments.size(); i++) { + ExpressionNode argument = this.arguments.get(i); + if (varargsIndex > i) { + arguments.add(argument); + } else { + nodesWrappedInArray.add(argument); + } + } + + ClassElement lastParameter = this.usedMethod.getLastParameter(); + + OneDimensionalArray varargsArray = + new OneDimensionalArray( + new TypeIdentifier(lastParameter.getCanonicalName()), + nodesWrappedInArray); + + arguments.add(varargsArray); + return arguments; + } + + /** + * Resolve types of method invocation arguments. + * + * @param ctx expression evaluation context + * + * @return types of method arguments + */ + protected List resolveArgumentTypes(ExpressionVisitorContext ctx) { + return arguments.stream() + .map(argument -> argument instanceof TypeIdentifier + ? TypeDescriptors.CLASS + : argument.resolveType(ctx)) + .toList(); + } + + /** + * Compiles method arguments. + * + * @param ctx expression evaluation context + */ + protected void compileArguments(ExpressionVisitorContext ctx) { + List arguments = this.arguments; + if (usedMethod.isVarArgs()) { + arguments = prepareVarargsArguments(); + } + + for (int i = 0; i < arguments.size(); i++) { + compileArgument(ctx, i, arguments.get(i)); + } + } + + /** + * Compiles given method argument. + * + * @param ctx expression evaluation context + * @param argumentIndex argument index + * @param argument compiled argument + */ + private void compileArgument(ExpressionVisitorContext ctx, + int argumentIndex, + ExpressionNode argument) { + GeneratorAdapter mv = ctx.methodVisitor(); + if (usedMethod.getParameters().size() > argumentIndex) { + Type parameterType = getTypeReference(usedMethod.getParameters().get(argumentIndex)); + Type argumentType = argument.resolveType(ctx); + + argument.compile(ctx); + if (isPrimitive(parameterType)) { + pushUnboxPrimitiveIfNecessary(argumentType, mv); + } else { + pushBoxPrimitiveIfNecessary(argumentType, mv); + } + } + } + + /** + * Prepares arguments string for logging purposes. + * + * @param ctx expression compilation context + * + * @return arguments string + */ + protected String stringifyArguments(ExpressionVisitorContext ctx) { + return arguments.stream() + .map(argument -> argument.resolveType(ctx)) + .map(Type::getClassName) + .collect(Collectors.joining(", ", "(", ")")); + } +} diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/access/CandidateMethod.java b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/access/CandidateMethod.java new file mode 100644 index 00000000000..43102694993 --- /dev/null +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/access/CandidateMethod.java @@ -0,0 +1,252 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.expressions.parser.ast.access; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.NonNull; +import io.micronaut.core.util.CollectionUtils; +import io.micronaut.inject.ast.ClassElement; +import io.micronaut.inject.ast.MethodElement; +import io.micronaut.inject.ast.ParameterElement; +import io.micronaut.inject.ast.TypedElement; +import io.micronaut.inject.visitor.VisitorContext; +import org.objectweb.asm.Type; +import org.objectweb.asm.commons.Method; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import static io.micronaut.expressions.parser.ast.util.EvaluatedExpressionCompilationUtils.getRequiredClassElement; +import static io.micronaut.expressions.parser.ast.util.EvaluatedExpressionCompilationUtils.isAssignable; +import static io.micronaut.inject.processing.JavaModelUtils.getTypeReference; + +/** + * Class representing candidate method used in evaluated expression. + * Encapsulates logic determining whether invocation of method in expression + * with concrete arguments matches list of parameters of concrete method. + * + * @author Sergey Gavrilov + * @since 4.0.0 + */ +@Internal +final class CandidateMethod { + private final MethodElement methodElement; + private final List parameterTypes; + private final List argumentTypes; + + private int varargsIndex = -1; + + public CandidateMethod(MethodElement methodElement, List argumentTypes) { + this.methodElement = methodElement; + this.argumentTypes = argumentTypes; + this.parameterTypes = Arrays.stream(methodElement.getParameters()) + .map(ParameterElement::getType) + .toList(); + } + + public CandidateMethod(MethodElement methodElement) { + this(methodElement, Collections.emptyList()); + } + + /** + * @return The method element. + */ + public MethodElement getMethodElement() { + return methodElement; + } + + /** + * Whether candidate method is vargars method. + * + * @return true if it is + */ + public boolean isVarArgs() { + return getVarargsIndex() != -1; + } + + /** + * Returns index of varargs parameter. If method has no varargs + * parameter, -1 is returned. + * + * @return varargs index or -1 + */ + public int getVarargsIndex() { + return varargsIndex; + } + + /** + * @return Returns candidate method return type. + */ + @NonNull + public Type getReturnType() { + return getTypeReference(methodElement.getReturnType()); + } + + /** + * @return Type of class that owns candidate method. + */ + @NonNull + public Type getOwningType() { + return getTypeReference(methodElement.getOwningType()); + } + + /** + * @return last parameter of candidate method. + */ + @NonNull + public ClassElement getLastParameter() { + return CollectionUtils.last(parameterTypes); + } + + /** + * @return candidate method descriptor. + */ + @NonNull + public String getDescriptor() { + return toAsmMethod().getDescriptor(); + } + + /** + * @return list of candidate method parameters. + */ + @NonNull + public List getParameters() { + return parameterTypes; + } + + /** + * Checks list of arguments against list of method parameters to decide whether there is + * a match. This check also supports varargs resolution for cases when method is explicitly + * defined as varargs method or when last method parameter is a one-dimensional array. + * + * @param ctx + * @return + */ + public boolean isMatching(VisitorContext ctx) { + int totalParams = parameterTypes.size(); + int totalArguments = argumentTypes.size(); + + if (totalParams == 0) { + return totalArguments == 0; + } else if (totalArguments < totalParams - 1) { + // list of arguments may be shorter than list of parameters only by 1 element and + // only in case last parameter is varargs parameter, otherwise method doesn't match + return false; + } + + ClassElement lastArgument = CollectionUtils.last(argumentTypes); + ClassElement lastParameter = getLastParameter(); + boolean varargsCandidate = methodElement.isVarArgs() || + (lastParameter.isArray() && lastParameter.getArrayDimensions() == 1); + + if (varargsCandidate) { + // maybe just array argument + if (totalArguments == totalParams && isAssignable(lastParameter, lastArgument)) { + return true; + } + + if (isMatchingVarargs(ctx)) { + this.varargsIndex = calculateVarargsIndex(); + return true; + } + + return false; + } + + if (totalArguments != totalParams) { + return false; + } + + for (int i = 0; i < parameterTypes.size(); i++) { + ClassElement argumentType = argumentTypes.get(i); + ClassElement parameterType = parameterTypes.get(i); + + if (!isAssignable(parameterType, argumentType)) { + return false; + } + } + return true; + } + + /** + * Returns {@link Method} representation of this candidate method. + * + * @return asm method + */ + public Method toAsmMethod() { + StringBuilder builder = new StringBuilder(); + builder.append('('); + + for (TypedElement parameterType : parameterTypes) { + builder.append(getTypeReference(parameterType).getDescriptor()); + } + + builder.append(')'); + + builder.append(getTypeReference(methodElement.getReturnType()).getDescriptor()); + return new Method(methodElement.getSimpleName(), builder.toString()); + } + + private boolean isMatchingVarargs(VisitorContext ctx) { + for (int paramIndex = 0; paramIndex < parameterTypes.size(); paramIndex++) { + ClassElement parameterType = parameterTypes.get(paramIndex); + + boolean isLastParameter = paramIndex == parameterTypes.size() - 1; + if (isLastParameter) { + parameterType = getRequiredClassElement(getTypeReference(parameterType).getElementType(), ctx); + + if (argumentTypes.size() < paramIndex) { + // if we got here it means that last parameter is varargs but methods + // arguments list doesn't include an argument for varargs parameter, so + // an empty array is used as varargs argument, which is treated as a match + return true; + } + + // check whether all remaining arguments match parameter type + for (int argIndex = paramIndex; argIndex < argumentTypes.size(); argIndex++) { + ClassElement argumentType = argumentTypes.get(paramIndex); + if (!isAssignable(parameterType, argumentType)) { + return false; + } + } + + return true; + } + + // too little arguments, no match + if (argumentTypes.size() < paramIndex) { + return false; + } + + // no match if argument is not assignable to parameter + if (!isAssignable(parameterType, argumentTypes.get(paramIndex))) { + return false; + } + } + + return false; + } + + private int calculateVarargsIndex() { + return CollectionUtils.last(parameterTypes) == null ? -1 : parameterTypes.size() - 1; + } + + @Override + public String toString() { + return methodElement.getDescription(false); + } +} diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/access/ContextElementAccess.java b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/access/ContextElementAccess.java new file mode 100644 index 00000000000..abe34c0dc7b --- /dev/null +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/access/ContextElementAccess.java @@ -0,0 +1,138 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.expressions.parser.ast.access; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.expressions.context.ExpressionCompilationContext; +import io.micronaut.expressions.parser.ast.ExpressionNode; +import io.micronaut.expressions.parser.compilation.ExpressionVisitorContext; +import io.micronaut.expressions.parser.exception.ExpressionCompilationException; +import io.micronaut.inject.ast.ClassElement; +import io.micronaut.inject.ast.ParameterElement; +import io.micronaut.inject.ast.PropertyElement; +import org.objectweb.asm.Type; + +import java.util.List; + +import static java.util.Collections.emptyList; + +/** + * Expression AST node used for context element access. + * Either evaluation context method element, property element or method argument can + * be accessed. When method is accessed it is clear at AST building stage, + * but whether property or method argument is accessed is unclear until type resolution against + * evaluation context is executed. This node checks evaluation context to resolve + * concrete node type, instantiates respective node and delegates type resolution + * and bytecode generation to this node + * + * @author Sergey Gavrilov + * @since 4.0.0 + */ +@Internal +public final class ContextElementAccess extends ExpressionNode { + + private final String name; + + private ContextMethodCall contextPropertyMethodCall; + private ContextMethodParameterAccess contextMethodParameterAccess; + + public ContextElementAccess(String name) { + this.name = name; + } + + @Override + protected void generateBytecode(ExpressionVisitorContext ctx) { + if (contextMethodParameterAccess != null) { + contextMethodParameterAccess.compile(ctx); + } else if (contextPropertyMethodCall != null) { + contextPropertyMethodCall.compile(ctx); + } + } + + @Override + protected ClassElement doResolveClassElement(ExpressionVisitorContext ctx) { + ExpressionCompilationContext evaluationContext = ctx.compilationContext(); + + List propertyElements = evaluationContext.findProperties(name); + List parameterElements = evaluationContext.findParameters(name); + + int totalElements = propertyElements.size() + parameterElements.size(); + + if (totalElements == 0) { + throw new ExpressionCompilationException( + "No element with name [" + name + "] available in evaluation context"); + } else if (totalElements > 1) { + throw new ExpressionCompilationException( + "Ambiguous expression evaluation context reference. Found " + totalElements + + " elements with name [" + name + "]"); + } + + if (!propertyElements.isEmpty()) { + + PropertyElement property = propertyElements.iterator().next(); + String readMethodName = + property.getReadMethod() + .orElseThrow(() -> new ExpressionCompilationException( + "Failed to obtain read method for property [" + name + "]")) + .getName(); + + contextPropertyMethodCall = new ContextMethodCall(readMethodName, emptyList()); + return contextPropertyMethodCall.resolveClassElement(ctx); + + } + + ParameterElement parameter = parameterElements.iterator().next(); + contextMethodParameterAccess = new ContextMethodParameterAccess(parameter); + return contextMethodParameterAccess.resolveClassElement(ctx); + } + + @Override + public Type doResolveType(ExpressionVisitorContext ctx) { + ExpressionCompilationContext evaluationContext = ctx.compilationContext(); + + List propertyElements = evaluationContext.findProperties(name); + List parameterElements = evaluationContext.findParameters(name); + + int totalElements = propertyElements.size() + parameterElements.size(); + + if (totalElements == 0) { + throw new ExpressionCompilationException( + "No element with name [" + name + "] available in evaluation context"); + } else if (totalElements > 1) { + throw new ExpressionCompilationException( + "Ambiguous expression evaluation context reference. Found " + totalElements + + " elements with name [" + name + "]"); + } + + if (!propertyElements.isEmpty()) { + + PropertyElement property = propertyElements.iterator().next(); + String readMethodName = + property.getReadMethod() + .orElseThrow(() -> new ExpressionCompilationException( + "Failed to obtain read method for property [" + name + "]")) + .getName(); + + contextPropertyMethodCall = new ContextMethodCall(readMethodName, emptyList()); + return contextPropertyMethodCall.resolveType(ctx); + + } + + ParameterElement parameter = parameterElements.iterator().next(); + contextMethodParameterAccess = new ContextMethodParameterAccess(parameter); + return contextMethodParameterAccess.resolveType(ctx); + } +} diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/access/ContextMethodCall.java b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/access/ContextMethodCall.java new file mode 100644 index 00000000000..4ae80e1391a --- /dev/null +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/access/ContextMethodCall.java @@ -0,0 +1,108 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.expressions.parser.ast.access; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.expressions.context.ExpressionCompilationContext; +import io.micronaut.expressions.parser.ast.ExpressionNode; +import io.micronaut.expressions.parser.compilation.ExpressionVisitorContext; +import io.micronaut.expressions.parser.exception.ExpressionCompilationException; +import io.micronaut.inject.ast.ClassElement; +import org.objectweb.asm.Type; +import org.objectweb.asm.commons.GeneratorAdapter; +import org.objectweb.asm.commons.Method; + +import java.util.List; + +import static io.micronaut.expressions.parser.ast.util.TypeDescriptors.EVALUATION_CONTEXT_TYPE; +import static io.micronaut.expressions.parser.ast.util.EvaluatedExpressionCompilationUtils.getRequiredClassElement; +import static org.objectweb.asm.Opcodes.CHECKCAST; + +/** + * Expression node used for invocation of method from expression + * evaluation context. + * + * @author Sergey Gavrilov + * @since 4.0.0 + */ +@Internal +public final class ContextMethodCall extends AbstractMethodCall { + + private static final Method GET_BEAN_METHOD = + new Method("getBean", Type.getType(Object.class), + new Type[]{Type.getType(Class.class)}); + + public ContextMethodCall(String name, List arguments) { + super(name, arguments); + } + + @Override + protected CandidateMethod resolveUsedMethod(ExpressionVisitorContext ctx) { + List argumentTypes = resolveArgumentTypes(ctx); + + ExpressionCompilationContext evaluationContext = ctx.compilationContext(); + List candidateMethods = + evaluationContext.findMethods(name) + .stream() + .map(method -> toCandidateMethod(ctx, method, argumentTypes)) + .filter(method -> method.isMatching(ctx.visitorContext())) + .toList(); + + if (candidateMethods.isEmpty()) { + throw new ExpressionCompilationException( + "No method [ " + name + stringifyArguments(ctx) + " ] available in evaluation context"); + } else if (candidateMethods.size() > 1) { + throw new ExpressionCompilationException( + "Ambiguous expression evaluation context reference. Found " + candidateMethods.size() + + " matching methods: " + candidateMethods); + } + + return candidateMethods.iterator().next(); + } + + @Override + public void generateBytecode(ExpressionVisitorContext ctx) { + GeneratorAdapter mv = ctx.methodVisitor(); + Type calleeType = usedMethod.getOwningType(); + + ClassElement calleeClass = getRequiredClassElement(calleeType, ctx.visitorContext()); + + pushGetBeanFromContext(mv, calleeType); + compileArguments(ctx); + if (calleeClass.isInterface()) { + mv.invokeInterface(calleeType, usedMethod.toAsmMethod()); + } else { + mv.invokeVirtual(calleeType, usedMethod.toAsmMethod()); + } + } + + /** + * Pushing method obtaining bean of provided type from beanContext. + * + * @param mv methodVisitor + * @param beanType required bean typ + */ + private void pushGetBeanFromContext(GeneratorAdapter mv, Type beanType) { + mv.loadArg(0); + mv.push(beanType); + + // invoke getBean method + mv.invokeInterface(EVALUATION_CONTEXT_TYPE, GET_BEAN_METHOD); + + // cast the return value to the correct type + mv.visitTypeInsn(CHECKCAST, beanType.getInternalName()); + } +} diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/access/ContextMethodParameterAccess.java b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/access/ContextMethodParameterAccess.java new file mode 100644 index 00000000000..16e72d1afe1 --- /dev/null +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/access/ContextMethodParameterAccess.java @@ -0,0 +1,93 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.expressions.parser.ast.access; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.expressions.parser.ast.ExpressionNode; +import io.micronaut.expressions.parser.ast.util.TypeDescriptors; +import io.micronaut.expressions.parser.compilation.ExpressionVisitorContext; +import io.micronaut.expressions.parser.exception.ExpressionCompilationException; +import io.micronaut.inject.ast.ClassElement; +import io.micronaut.inject.ast.ParameterElement; +import org.objectweb.asm.Type; +import org.objectweb.asm.commons.GeneratorAdapter; +import org.objectweb.asm.commons.Method; + +import static io.micronaut.expressions.parser.ast.util.TypeDescriptors.EVALUATION_CONTEXT_TYPE; +import static io.micronaut.inject.processing.JavaModelUtils.getTypeReference; + +/** + * Expression AST node used for context method parameter access. + * + * @author Sergey Gavrilov + * @since 4.0.0 + */ +@Internal +final class ContextMethodParameterAccess extends ExpressionNode { + + private static final Method GET_ARGUMENT_METHOD = + new Method("getArgument", Type.getType(Object.class), + new Type[]{TypeDescriptors.INT}); + + private final ParameterElement parameterElement; + + private Integer parameterIndex; + + public ContextMethodParameterAccess(ParameterElement parameterElement) { + this.parameterElement = parameterElement; + } + + @Override + protected void generateBytecode(ExpressionVisitorContext ctx) { + GeneratorAdapter mv = ctx.methodVisitor(); + mv.loadArg(0); + mv.push(parameterIndex); + // invoke getArgument method + mv.invokeInterface(EVALUATION_CONTEXT_TYPE, GET_ARGUMENT_METHOD); + if (nodeType != null) { + mv.checkCast(nodeType); + } + } + + @Override + protected ClassElement doResolveClassElement(ExpressionVisitorContext ctx) { + String parameterName = parameterElement.getName(); + ParameterElement[] methodParameters = parameterElement.getMethodElement().getParameters(); + + Integer paramIndex = null; + for (int i = 0; i < methodParameters.length; i++) { + ParameterElement methodParameter = methodParameters[i]; + if (methodParameter.getName().equals(parameterName)) { + paramIndex = i; + break; + } + } + + if (paramIndex == null) { + throw new ExpressionCompilationException( + "Can not find parameter with name [" + parameterName + "] in method parameters"); + } + + this.parameterIndex = paramIndex; + return parameterElement.getGenericType(); + } + + @Override + protected Type doResolveType(ExpressionVisitorContext ctx) { + doResolveClassElement(ctx); + return getTypeReference(parameterElement.getType()); + } +} diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/access/ElementMethodCall.java b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/access/ElementMethodCall.java new file mode 100644 index 00000000000..f99dde9ef90 --- /dev/null +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/access/ElementMethodCall.java @@ -0,0 +1,138 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.expressions.parser.ast.access; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.expressions.parser.ast.ExpressionNode; +import io.micronaut.expressions.parser.ast.types.TypeIdentifier; +import io.micronaut.expressions.parser.compilation.ExpressionVisitorContext; +import io.micronaut.expressions.parser.exception.ExpressionCompilationException; +import io.micronaut.inject.ast.ClassElement; +import io.micronaut.inject.ast.ElementQuery; +import io.micronaut.inject.ast.MethodElement; +import io.micronaut.inject.visitor.VisitorContext; +import org.objectweb.asm.Label; +import org.objectweb.asm.Type; +import org.objectweb.asm.commons.GeneratorAdapter; +import org.objectweb.asm.commons.Method; + +import java.util.List; + +import static io.micronaut.expressions.parser.ast.util.EvaluatedExpressionCompilationUtils.getRequiredClassElement; +import static org.objectweb.asm.Opcodes.ACONST_NULL; +import static org.objectweb.asm.Opcodes.INVOKESTATIC; + +/** + * Expression AST node used for method invocation. + * This node represents both object method invocation and static method + * invocation + * + * @author Sergey Gavrilov + * @since 4.0.0 + */ +@Internal +public sealed class ElementMethodCall extends AbstractMethodCall permits PropertyAccess { + + protected final ExpressionNode callee; + private final boolean nullSafe; + + public ElementMethodCall(ExpressionNode callee, + String name, + List arguments, + boolean nullSafe) { + super(name, arguments); + this.callee = callee; + this.nullSafe = nullSafe; + } + + @Override + protected void generateBytecode(ExpressionVisitorContext ctx) { + GeneratorAdapter mv = ctx.methodVisitor(); + VisitorContext visitorContext = ctx.visitorContext(); + Type calleeType = callee.resolveType(ctx); + Method method = usedMethod.toAsmMethod(); + + ClassElement calleeClass = getRequiredClassElement(calleeType, visitorContext); + + if (callee instanceof TypeIdentifier) { + compileArguments(ctx); + if (calleeClass.isInterface()) { + mv.visitMethodInsn(INVOKESTATIC, calleeType.getInternalName(), name, + usedMethod.getDescriptor(), true); + } else { + mv.invokeStatic(calleeType, method); + } + } else { + callee.compile(ctx); + if (nullSafe) { + // null safe operator is used so we need to check the result is null + Type returnType = method.getReturnType(); + mv.storeLocal(2, returnType); + mv.loadLocal(2, returnType); + Label proceed = new Label(); + mv.ifNonNull(proceed); + mv.visitInsn(ACONST_NULL); + mv.returnValue(); + mv.visitLabel(proceed); + mv.loadLocal(2, returnType); + } + compileArguments(ctx); + if (calleeClass.isInterface()) { + mv.invokeInterface(calleeType, method); + } else { + mv.invokeVirtual(calleeType, method); + } + } + } + + @Override + protected CandidateMethod resolveUsedMethod(ExpressionVisitorContext ctx) { + List argumentTypes = resolveArgumentTypes(ctx); + Type calleeType = callee.resolveType(ctx); + + ElementQuery methodQuery = buildMethodQuery(); + List candidateMethods = + ctx.visitorContext() + .getClassElement(calleeType.getClassName()) + .stream() + .flatMap(element -> element.getEnclosedElements(methodQuery).stream()) + .map(method -> toCandidateMethod(ctx, method, argumentTypes)) + .filter(method -> method.isMatching(ctx.visitorContext())) + .toList(); + + if (candidateMethods.isEmpty()) { + throw new ExpressionCompilationException( + "No method [ " + name + stringifyArguments(ctx) + " ] available in class " + calleeType); + } else if (candidateMethods.size() > 1) { + throw new ExpressionCompilationException( + "Ambiguous method call. Found " + candidateMethods.size() + + " matching methods: " + candidateMethods + " in class " + calleeType); + } + + return candidateMethods.iterator().next(); + } + + private ElementQuery buildMethodQuery() { + ElementQuery query = ElementQuery.ALL_METHODS.onlyAccessible() + .named(name); + + if (callee instanceof TypeIdentifier) { + query = query.onlyStatic(); + } + + return query; + } +} diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/access/PropertyAccess.java b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/access/PropertyAccess.java new file mode 100644 index 00000000000..a480986c7f1 --- /dev/null +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/access/PropertyAccess.java @@ -0,0 +1,78 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.expressions.parser.ast.access; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.expressions.parser.ast.ExpressionNode; +import io.micronaut.expressions.parser.compilation.ExpressionVisitorContext; +import io.micronaut.expressions.parser.exception.ExpressionCompilationException; +import io.micronaut.inject.ast.ClassElement; +import io.micronaut.inject.ast.MethodElement; +import io.micronaut.inject.ast.PropertyElement; +import io.micronaut.inject.ast.PropertyElementQuery; +import org.objectweb.asm.Type; + +import java.util.Collections; +import java.util.List; + +import static io.micronaut.expressions.parser.ast.util.EvaluatedExpressionCompilationUtils.getRequiredClassElement; +import static java.util.Collections.emptyList; +import static java.util.function.Predicate.not; + +/** + * Expression AST node used for accessing object property. + * Property access is under the hood an invocation of object getter method + * of respective property. + * + * @author Sergey Gavrilov + * @since 4.0.0 + */ +@Internal +public final class PropertyAccess extends ElementMethodCall { + public PropertyAccess(ExpressionNode callee, String name, boolean nullSafe) { + super(callee, name, emptyList(), nullSafe); + } + + @Override + protected CandidateMethod resolveUsedMethod(ExpressionVisitorContext ctx) { + Type calleeType = callee.resolveType(ctx); + ClassElement classElement = getRequiredClassElement(calleeType, ctx.visitorContext()); + + List propertyElements = + classElement.getBeanProperties( + PropertyElementQuery.of(classElement.getAnnotationMetadata()) + .includes(Collections.singleton(name))).stream() + .filter(not(PropertyElement::isExcluded)) + .toList(); + + if (propertyElements.size() == 0) { + throw new ExpressionCompilationException( + "Can not find property with name [" + name + "] in class " + calleeType); + } else if (propertyElements.size() > 1) { + throw new ExpressionCompilationException( + "Ambiguous property access. Found " + propertyElements.size() + + " matching properties with name [" + name + "] in class " + calleeType); + } + + PropertyElement property = propertyElements.iterator().next(); + MethodElement methodElement = + property.getReadMethod() + .orElseThrow(() -> new ExpressionCompilationException( + "Can not resolve access method for property [" + name + "] in class " + calleeType)); + + return new CandidateMethod(methodElement); + } +} diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/access/SubscriptOperator.java b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/access/SubscriptOperator.java new file mode 100644 index 00000000000..529301a53be --- /dev/null +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/access/SubscriptOperator.java @@ -0,0 +1,118 @@ +/* + * Copyright 2017-2023 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.expressions.parser.ast.access; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.reflect.ReflectionUtils; +import io.micronaut.expressions.parser.ast.ExpressionNode; +import io.micronaut.expressions.parser.compilation.ExpressionVisitorContext; +import io.micronaut.expressions.parser.exception.ExpressionCompilationException; +import io.micronaut.inject.ast.ClassElement; +import io.micronaut.inject.ast.PrimitiveElement; +import io.micronaut.inject.processing.JavaModelUtils; +import org.objectweb.asm.Type; +import org.objectweb.asm.commons.GeneratorAdapter; +import org.objectweb.asm.commons.Method; + +import java.util.List; +import java.util.Map; + +/** + * Handles list, map and array de-referencing. + */ +@Internal +public class SubscriptOperator extends ExpressionNode { + + private static final Method LIST_GET_METHOD = Method.getMethod( + ReflectionUtils.getRequiredMethod(List.class, "get", int.class) + ); + private static final Method MAP_GET_METHOD = Method.getMethod( + ReflectionUtils.getRequiredMethod(Map.class, "get", Object.class) + ); + + private final ExpressionNode callee; + private final ExpressionNode index; + private boolean isArray = false; + private boolean isMap = false; + + public SubscriptOperator(ExpressionNode callee, ExpressionNode index) { + this.callee = callee; + this.index = index; + } + + @Override + protected void generateBytecode(ExpressionVisitorContext ctx) { + callee.compile(ctx); + GeneratorAdapter methodVisitor = ctx.methodVisitor(); + ClassElement indexType = index.resolveClassElement(ctx); + index.compile(ctx); + + if (isMap) { + if (!indexType.isAssignable(String.class)) { + throw new ExpressionCompilationException("Invalid subscript operator. Map key must be a string."); + } else { + methodVisitor.invokeInterface( + Type.getType(Map.class), + MAP_GET_METHOD + ); + } + } else { + if (!indexType.equals(PrimitiveElement.INT)) { + throw new ExpressionCompilationException("Invalid subscript operator. Index must be an integer."); + } + if (isArray) { + methodVisitor.arrayLoad(resolveType(ctx)); + } else { + methodVisitor.invokeInterface( + Type.getType(List.class), + LIST_GET_METHOD + ); + } + } + if (!isArray) { + methodVisitor.checkCast(resolveType(ctx)); + } + } + + @Override + protected ClassElement doResolveClassElement(ExpressionVisitorContext ctx) { + ClassElement classElement = callee.resolveClassElement(ctx); + this.isArray = classElement.isArray(); + this.isMap = classElement.isAssignable(Map.class); + if (!isMap && !classElement.isAssignable(List.class) && !isArray) { + throw new ExpressionCompilationException("Invalid subscript operator. Subscript operator can only be applied to maps, lists and arrays"); + } + if (isArray) { + return classElement.fromArray(); + } else if (isMap) { + Map typeArguments = classElement.getTypeArguments(); + if (typeArguments.containsKey("V")) { + return typeArguments.get("V"); + } else { + return ClassElement.of(Object.class); + } + } else { + return classElement.getFirstTypeArgument() + .orElseGet(() -> ClassElement.of(Object.class)); + } + } + + @Override + protected Type doResolveType(ExpressionVisitorContext ctx) { + ClassElement valueElement = resolveClassElement(ctx); + return JavaModelUtils.getTypeReference(valueElement); + } +} diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/collection/OneDimensionalArray.java b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/collection/OneDimensionalArray.java new file mode 100644 index 00000000000..1f1168227ee --- /dev/null +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/collection/OneDimensionalArray.java @@ -0,0 +1,95 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.expressions.parser.ast.collection; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.expressions.parser.ast.ExpressionNode; +import io.micronaut.expressions.parser.ast.types.TypeIdentifier; +import io.micronaut.expressions.parser.compilation.ExpressionVisitorContext; +import io.micronaut.inject.ast.ClassElement; +import org.objectweb.asm.Type; +import org.objectweb.asm.commons.GeneratorAdapter; + +import java.util.List; + +import static io.micronaut.expressions.parser.ast.util.EvaluatedExpressionCompilationUtils.getRequiredClassElement; +import static io.micronaut.expressions.parser.ast.util.EvaluatedExpressionCompilationUtils.pushBoxPrimitiveIfNecessary; +import static io.micronaut.inject.processing.JavaModelUtils.getTypeReference; +import static org.objectweb.asm.Opcodes.AASTORE; + +/** + * Expression AST node for array instantiation. This node is not used when + * parsing user's expressions as array instantiation is not supported in + * evaluated expressions. That's why it doesn't support multidimensional arrays, + * and the presence of initializer is assumed. It is designed for concrete use-cases, + * such as wrapping varargs method arguments and building compound evaluated expressions. + * + * @author Sergey Gavrilov + * @since 4.0.0 + */ +@Internal +public final class OneDimensionalArray extends ExpressionNode { + private final TypeIdentifier elementTypeIdentifier; + private final List initializer; + + public OneDimensionalArray(TypeIdentifier elementTypeIdentifier, + List initializer) { + this.elementTypeIdentifier = elementTypeIdentifier; + this.initializer = initializer; + } + + @Override + public void generateBytecode(ExpressionVisitorContext ctx) { + GeneratorAdapter mv = ctx.methodVisitor(); + int arraySize = initializer.size(); + + mv.push(arraySize); + mv.newArray(elementTypeIdentifier.resolveType(ctx)); + + if (arraySize > 0) { + mv.dup(); + } + + for (int i = 0; i < arraySize; i++) { + ExpressionNode element = initializer.get(i); + boolean isLastElement = i == arraySize - 1; + mv.push(i); + + Type elementType = element.resolveType(ctx); + element.compile(ctx); + if (!elementTypeIdentifier.isPrimitive()) { + pushBoxPrimitiveIfNecessary(elementType, mv); + mv.visitInsn(AASTORE); + } else { + mv.arrayStore(elementType); + } + if (!isLastElement) { + mv.dup(); + } + } + } + + @Override + protected ClassElement doResolveClassElement(ExpressionVisitorContext ctx) { + return getRequiredClassElement(elementTypeIdentifier.resolveType(ctx), ctx.visitorContext()) + .toArray(); + } + + @Override + protected Type doResolveType(ExpressionVisitorContext ctx) { + return getTypeReference(doResolveClassElement(ctx)); + } +} diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/conditional/ElvisOperator.java b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/conditional/ElvisOperator.java new file mode 100644 index 00000000000..9ac33b618e2 --- /dev/null +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/conditional/ElvisOperator.java @@ -0,0 +1,32 @@ +/* + * Copyright 2017-2023 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.expressions.parser.ast.conditional; + +import io.micronaut.expressions.parser.ast.ExpressionNode; + +/** + * Support for the elvis operator. Example: {@code foo ?: bar}. + */ +public final class ElvisOperator extends TernaryExpression { + public ElvisOperator(ExpressionNode condition, ExpressionNode falseExpr) { + super(condition, condition, falseExpr); + } + + @Override + protected boolean shouldCoerceConditionToBoolean() { + return true; + } +} diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/conditional/TernaryExpression.java b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/conditional/TernaryExpression.java new file mode 100644 index 00000000000..f43bbdc4766 --- /dev/null +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/conditional/TernaryExpression.java @@ -0,0 +1,163 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.expressions.parser.ast.conditional; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.reflect.ReflectionUtils; +import io.micronaut.core.util.ObjectUtils; +import io.micronaut.expressions.parser.ast.ExpressionNode; +import io.micronaut.expressions.parser.compilation.ExpressionVisitorContext; +import io.micronaut.expressions.parser.exception.ExpressionCompilationException; +import io.micronaut.inject.ast.ClassElement; +import org.objectweb.asm.Label; +import org.objectweb.asm.Type; +import org.objectweb.asm.commons.GeneratorAdapter; +import org.objectweb.asm.commons.Method; + +import static io.micronaut.expressions.parser.ast.util.TypeDescriptors.BOOLEAN; +import static io.micronaut.expressions.parser.ast.util.TypeDescriptors.BOOLEAN_WRAPPER; +import static io.micronaut.expressions.parser.ast.util.TypeDescriptors.OBJECT; +import static io.micronaut.expressions.parser.ast.util.TypeDescriptors.computeNumericOperationTargetType; +import static io.micronaut.expressions.parser.ast.util.TypeDescriptors.isNumeric; +import static io.micronaut.expressions.parser.ast.util.TypeDescriptors.isOneOf; +import static io.micronaut.expressions.parser.ast.util.TypeDescriptors.toUnboxedIfNecessary; +import static io.micronaut.expressions.parser.ast.util.EvaluatedExpressionCompilationUtils.getRequiredClassElement; +import static io.micronaut.expressions.parser.ast.util.EvaluatedExpressionCompilationUtils.isAssignable; +import static io.micronaut.expressions.parser.ast.util.EvaluatedExpressionCompilationUtils.pushBoxPrimitiveIfNecessary; +import static io.micronaut.expressions.parser.ast.util.EvaluatedExpressionCompilationUtils.pushPrimitiveCastIfNecessary; +import static io.micronaut.expressions.parser.ast.util.EvaluatedExpressionCompilationUtils.pushUnboxPrimitiveIfNecessary; +import static org.objectweb.asm.Opcodes.GOTO; +import static org.objectweb.asm.commons.GeneratorAdapter.NE; + +/** + * Expression AST node for ternary expressions. + * + * @author Sergey Gavrilov + * @since 4.0.0 + */ +@Internal +public class TernaryExpression extends ExpressionNode { + private static final Method COERCE_TO_BOOLEAN = Method.getMethod( + ReflectionUtils.getRequiredMethod(ObjectUtils.class, "coerceToBoolean", Object.class) + ); + private final ExpressionNode condition; + private final ExpressionNode trueExpr; + private final ExpressionNode falseExpr; + + public TernaryExpression(ExpressionNode condition, ExpressionNode trueExpr, + ExpressionNode falseExpr) { + this.condition = condition; + this.trueExpr = trueExpr; + this.falseExpr = falseExpr; + } + + @Override + public void generateBytecode(ExpressionVisitorContext ctx) { + GeneratorAdapter mv = ctx.methodVisitor(); + Label falseLabel = new Label(); + Label returnLabel = new Label(); + + Type trueType = trueExpr.resolveType(ctx); + Type falseType = falseExpr.resolveType(ctx); + Type numericType = null; + if (isNumeric(trueType) && isNumeric(falseType)) { + numericType = computeNumericOperationTargetType( + toUnboxedIfNecessary(trueType), + toUnboxedIfNecessary(falseType)); + } + + mv.push(true); + Type conditionType = condition.resolveType(ctx); + + condition.compile(ctx); + if (shouldCoerceConditionToBoolean()) { + pushBoxPrimitiveIfNecessary(conditionType, mv); + mv.invokeStatic( + Type.getType(ObjectUtils.class), + COERCE_TO_BOOLEAN + ); + } else { + pushUnboxPrimitiveIfNecessary(conditionType, mv); + } + + mv.ifCmp(BOOLEAN, NE, falseLabel); + trueExpr.compile(ctx); + if (numericType != null) { + pushPrimitiveCastIfNecessary(trueType, numericType, mv); + } else { + pushBoxPrimitiveIfNecessary(trueType, mv); + } + + mv.visitJumpInsn(GOTO, returnLabel); + + mv.visitLabel(falseLabel); + falseExpr.compile(ctx); + if (numericType != null) { + pushPrimitiveCastIfNecessary(falseType, numericType, mv); + } else { + pushBoxPrimitiveIfNecessary(falseType, mv); + } + + mv.visitLabel(returnLabel); + } + + @Override + protected ClassElement doResolveClassElement(ExpressionVisitorContext ctx) { + return ClassElement.of(doResolveType(ctx).getClassName()); + } + + /** + * @return Whether the condition should be coerced to a boolean type. + */ + protected boolean shouldCoerceConditionToBoolean() { + return false; + } + + @Override + protected Type doResolveType(ExpressionVisitorContext ctx) { + if (!shouldCoerceConditionToBoolean() && !isOneOf(condition.resolveType(ctx), BOOLEAN, BOOLEAN_WRAPPER)) { + throw new ExpressionCompilationException("Invalid ternary operator. Condition should resolve to boolean type"); + } + + Type trueType = trueExpr.resolveType(ctx); + Type falseType = falseExpr.resolveType(ctx); + + if (trueType.equals(falseType)) { + return trueType; + } + + if (isNumeric(trueType) && isNumeric(falseType)) { + return computeNumericOperationTargetType( + toUnboxedIfNecessary(trueType), + toUnboxedIfNecessary(falseType)); + } else if (isNumeric(trueType) || isNumeric(falseType)) { + return OBJECT; + } + + ClassElement trueClassElement = getRequiredClassElement(trueType, ctx.visitorContext()); + ClassElement falseClassElement = getRequiredClassElement(falseType, ctx.visitorContext()); + + if (isAssignable(trueClassElement, falseClassElement)) { + return trueType; + } + + if (isAssignable(falseClassElement, trueClassElement)) { + return falseType; + } + + return OBJECT; + } +} diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/literal/BoolLiteral.java b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/literal/BoolLiteral.java new file mode 100644 index 00000000000..aabdcc0d0de --- /dev/null +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/literal/BoolLiteral.java @@ -0,0 +1,55 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.expressions.parser.ast.literal; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.expressions.parser.ast.ExpressionNode; +import io.micronaut.expressions.parser.compilation.ExpressionVisitorContext; +import io.micronaut.inject.ast.ClassElement; +import io.micronaut.inject.ast.PrimitiveElement; +import org.objectweb.asm.Type; + +import static io.micronaut.expressions.parser.ast.util.TypeDescriptors.BOOLEAN; + +/** + * Expression AST node for boolean literal. + * + * @author Sergey Gavrilov + * @since 4.0.0 + */ +@Internal +public final class BoolLiteral extends ExpressionNode { + private final boolean value; + + public BoolLiteral(boolean value) { + this.value = value; + } + + @Override + public void generateBytecode(ExpressionVisitorContext ctx) { + ctx.methodVisitor().push(value); + } + + @Override + protected ClassElement doResolveClassElement(ExpressionVisitorContext ctx) { + return PrimitiveElement.BOOLEAN; + } + + @Override + protected Type doResolveType(ExpressionVisitorContext ctx) { + return BOOLEAN; + } +} diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/literal/DoubleLiteral.java b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/literal/DoubleLiteral.java new file mode 100644 index 00000000000..9ad9b922274 --- /dev/null +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/literal/DoubleLiteral.java @@ -0,0 +1,55 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.expressions.parser.ast.literal; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.expressions.parser.ast.ExpressionNode; +import io.micronaut.expressions.parser.compilation.ExpressionVisitorContext; +import io.micronaut.inject.ast.ClassElement; +import io.micronaut.inject.ast.PrimitiveElement; +import org.objectweb.asm.Type; + +import static io.micronaut.expressions.parser.ast.util.TypeDescriptors.DOUBLE; + +/** + * Expression AST node for double literal. + * + * @author Sergey Gavrilov + * @since 4.0.0 + */ +@Internal +public final class DoubleLiteral extends ExpressionNode { + private final double value; + + public DoubleLiteral(double value) { + this.value = value; + } + + @Override + public void generateBytecode(ExpressionVisitorContext ctx) { + ctx.methodVisitor().push(value); + } + + @Override + protected ClassElement doResolveClassElement(ExpressionVisitorContext ctx) { + return PrimitiveElement.DOUBLE; + } + + @Override + protected Type doResolveType(ExpressionVisitorContext ctx) { + return DOUBLE; + } +} diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/literal/FloatLiteral.java b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/literal/FloatLiteral.java new file mode 100644 index 00000000000..1e06c17d0a5 --- /dev/null +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/literal/FloatLiteral.java @@ -0,0 +1,55 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.expressions.parser.ast.literal; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.expressions.parser.ast.ExpressionNode; +import io.micronaut.expressions.parser.compilation.ExpressionVisitorContext; +import io.micronaut.inject.ast.ClassElement; +import io.micronaut.inject.ast.PrimitiveElement; +import org.objectweb.asm.Type; + +import static io.micronaut.expressions.parser.ast.util.TypeDescriptors.FLOAT; + +/** + * Expression AST node for float literal. + * + * @author Sergey Gavrilov + * @since 4.0.0 + */ +@Internal +public final class FloatLiteral extends ExpressionNode { + private final float value; + + public FloatLiteral(float value) { + this.value = value; + } + + @Override + public void generateBytecode(ExpressionVisitorContext ctx) { + ctx.methodVisitor().push(value); + } + + @Override + protected ClassElement doResolveClassElement(ExpressionVisitorContext ctx) { + return PrimitiveElement.FLOAT; + } + + @Override + protected Type doResolveType(ExpressionVisitorContext ctx) { + return FLOAT; + } +} diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/literal/IntLiteral.java b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/literal/IntLiteral.java new file mode 100644 index 00000000000..a09355e01b9 --- /dev/null +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/literal/IntLiteral.java @@ -0,0 +1,63 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.expressions.parser.ast.literal; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.expressions.parser.ast.ExpressionNode; +import io.micronaut.expressions.parser.compilation.ExpressionVisitorContext; +import io.micronaut.inject.ast.ClassElement; +import io.micronaut.inject.ast.PrimitiveElement; +import org.objectweb.asm.Type; + +import static io.micronaut.expressions.parser.ast.util.TypeDescriptors.INT; + +/** + * Expression AST node for integer literal. + * + * @author Sergey Gavrilov + * @since 4.0.0 + */ +@Internal +public final class IntLiteral extends ExpressionNode { + + private final int value; + + public IntLiteral(int value) { + this.value = value; + } + + @Override + public void generateBytecode(ExpressionVisitorContext ctx) { + ctx.methodVisitor().push(value); + } + + @Override + protected ClassElement doResolveClassElement(ExpressionVisitorContext ctx) { + return PrimitiveElement.INT; + } + + @Override + protected Type doResolveType(ExpressionVisitorContext ctx) { + return INT; + } + + /** + * @return The value + */ + public int getValue() { + return value; + } +} diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/literal/LongLiteral.java b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/literal/LongLiteral.java new file mode 100644 index 00000000000..729a0326c21 --- /dev/null +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/literal/LongLiteral.java @@ -0,0 +1,55 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.expressions.parser.ast.literal; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.expressions.parser.ast.ExpressionNode; +import io.micronaut.expressions.parser.compilation.ExpressionVisitorContext; +import io.micronaut.inject.ast.ClassElement; +import io.micronaut.inject.ast.PrimitiveElement; +import org.objectweb.asm.Type; + +import static io.micronaut.expressions.parser.ast.util.TypeDescriptors.LONG; + +/** + * Expression AST node for long literal. + * + * @author Sergey Gavrilov + * @since 4.0.0 + */ +@Internal +public final class LongLiteral extends ExpressionNode { + private final long value; + + public LongLiteral(long value) { + this.value = value; + } + + @Override + public void generateBytecode(ExpressionVisitorContext ctx) { + ctx.methodVisitor().push(value); + } + + @Override + protected ClassElement doResolveClassElement(ExpressionVisitorContext ctx) { + return PrimitiveElement.LONG; + } + + @Override + protected Type doResolveType(ExpressionVisitorContext ctx) { + return LONG; + } +} diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/literal/NullLiteral.java b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/literal/NullLiteral.java new file mode 100644 index 00000000000..a89f43c1033 --- /dev/null +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/literal/NullLiteral.java @@ -0,0 +1,49 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.expressions.parser.ast.literal; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.expressions.parser.ast.ExpressionNode; +import io.micronaut.expressions.parser.compilation.ExpressionVisitorContext; +import io.micronaut.inject.ast.ClassElement; +import org.objectweb.asm.Type; + +import static io.micronaut.expressions.parser.ast.util.TypeDescriptors.OBJECT; +import static org.objectweb.asm.Opcodes.ACONST_NULL; + +/** + * Expression AST node for null literal. + * + * @author Sergey Gavrilov + * @since 4.0.0 + */ +@Internal +public final class NullLiteral extends ExpressionNode { + @Override + public void generateBytecode(ExpressionVisitorContext ctx) { + ctx.methodVisitor().visitInsn(ACONST_NULL); + } + + @Override + protected ClassElement doResolveClassElement(ExpressionVisitorContext ctx) { + return ClassElement.of(Object.class); + } + + @Override + protected Type doResolveType(ExpressionVisitorContext ctx) { + return OBJECT; + } +} diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/literal/StringLiteral.java b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/literal/StringLiteral.java new file mode 100644 index 00000000000..7c6cc6ac57b --- /dev/null +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/literal/StringLiteral.java @@ -0,0 +1,60 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.expressions.parser.ast.literal; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.expressions.parser.ast.ExpressionNode; +import io.micronaut.expressions.parser.compilation.ExpressionVisitorContext; +import io.micronaut.inject.ast.ClassElement; +import org.objectweb.asm.Type; + +import static io.micronaut.expressions.parser.ast.util.TypeDescriptors.STRING; + +/** + * Expression AST node for string literal. + * + * @author Sergey Gavrilov + * @since 4.0.0 + */ +@Internal +public final class StringLiteral extends ExpressionNode { + + private static final ClassElement STRING_ELEMENT = ClassElement.of(String.class); + private final String value; + + public StringLiteral(String value) { + this.value = value; + } + + public String getValue() { + return value; + } + + @Override + public void generateBytecode(ExpressionVisitorContext ctx) { + ctx.methodVisitor().push(value); + } + + @Override + protected ClassElement doResolveClassElement(ExpressionVisitorContext ctx) { + return STRING_ELEMENT; + } + + @Override + protected Type doResolveType(ExpressionVisitorContext ctx) { + return STRING; + } +} diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/AddOperator.java b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/AddOperator.java new file mode 100644 index 00000000000..83c67753af0 --- /dev/null +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/AddOperator.java @@ -0,0 +1,155 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.expressions.parser.ast.operator.binary; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.expressions.parser.ast.ExpressionNode; +import io.micronaut.expressions.parser.ast.util.TypeDescriptors; +import io.micronaut.expressions.parser.compilation.ExpressionVisitorContext; +import io.micronaut.expressions.parser.exception.ExpressionCompilationException; +import org.objectweb.asm.Type; +import org.objectweb.asm.commons.GeneratorAdapter; +import org.objectweb.asm.commons.Method; + +import java.util.Map; +import java.util.Optional; + +import static io.micronaut.expressions.parser.ast.util.TypeDescriptors.STRING; +import static io.micronaut.expressions.parser.ast.util.TypeDescriptors.VOID; +import static io.micronaut.expressions.parser.ast.util.TypeDescriptors.computeNumericOperationTargetType; +import static io.micronaut.expressions.parser.ast.util.TypeDescriptors.isNumeric; +import static io.micronaut.expressions.parser.ast.util.EvaluatedExpressionCompilationUtils.pushPrimitiveCastIfNecessary; +import static io.micronaut.expressions.parser.ast.util.EvaluatedExpressionCompilationUtils.pushUnboxPrimitiveIfNecessary; +import static org.objectweb.asm.Opcodes.DADD; +import static org.objectweb.asm.Opcodes.DUP; +import static org.objectweb.asm.Opcodes.FADD; +import static org.objectweb.asm.Opcodes.IADD; +import static org.objectweb.asm.Opcodes.LADD; +import static org.objectweb.asm.Opcodes.NEW; + +/** + * Expression node for binary '+' operator. Works both for math operation and string + * concatenation. + * + * @author Sergey Gavrilov + * @since 4.0.0 + */ +@Internal +public final class AddOperator extends BinaryOperator { + + private static final Map ADD_OPERATION_OPCODES = Map.of( + "D", DADD, + "I", IADD, + "F", FADD, + "J", LADD); + + private static final Type STRING_BUILDER_TYPE = Type.getType(StringBuilder.class); + + private static final Method STRING_BUILD_CONSTRUCTOR = + new Method("", VOID, new Type[]{}); + + private static final Method STRING_BUILD_TO_STRING = + new Method("toString", STRING, new Type[]{}); + + public AddOperator(ExpressionNode leftOperand, ExpressionNode rightOperand) { + super(leftOperand, rightOperand); + } + + @Override + protected Type resolveOperationType(Type leftOperandType, Type rightOperandType) { + if (!(leftOperandType.equals(STRING) + || rightOperandType.equals(STRING) + || (isNumeric(leftOperandType) && isNumeric(rightOperandType)))) { + throw new ExpressionCompilationException( + "'+' operation can only be applied to numeric and string types"); + } + + if (leftOperandType.equals(STRING) + || rightOperandType.equals(STRING)) { + return STRING; + } + + return computeNumericOperationTargetType(leftOperandType, rightOperandType); + } + + @Override + public void generateBytecode(ExpressionVisitorContext ctx) { + Type leftType = leftOperand.resolveType(ctx); + Type rightType = rightOperand.resolveType(ctx); + + GeneratorAdapter mv = ctx.methodVisitor(); + if (leftType.equals(STRING) || (rightType.equals(STRING))) { + concatStrings(ctx); + } else { + Type targetType = resolveType(ctx); + + leftOperand.compile(ctx); + pushUnboxPrimitiveIfNecessary(leftType, mv); + pushPrimitiveCastIfNecessary(leftType, targetType, mv); + + rightOperand.compile(ctx); + pushUnboxPrimitiveIfNecessary(rightType, mv); + pushPrimitiveCastIfNecessary(rightType, targetType, mv); + + int opcode = + Optional.ofNullable(ADD_OPERATION_OPCODES.get(targetType.getDescriptor())) + .orElseThrow(() -> new ExpressionCompilationException( + "Can not apply '+' operation to " + targetType)); + + mv.visitInsn(opcode); + } + } + + private void concatStrings(ExpressionVisitorContext ctx) { + GeneratorAdapter mv = ctx.methodVisitor(); + initStringBuilder(mv); + pushOperand(ctx, leftOperand); + pushOperand(ctx, rightOperand); + mv.invokeVirtual(STRING_BUILDER_TYPE, STRING_BUILD_TO_STRING); + } + + private void initStringBuilder(GeneratorAdapter mv) { + mv.visitTypeInsn(NEW, STRING_BUILDER_TYPE.getInternalName()); + mv.visitInsn(DUP); + mv.invokeConstructor(STRING_BUILDER_TYPE, STRING_BUILD_CONSTRUCTOR); + } + + private void pushOperand(ExpressionVisitorContext ctx, ExpressionNode operand) { + GeneratorAdapter mv = ctx.methodVisitor(); + if (operand instanceof AddOperator addOperator) { + Type operatorType = addOperator.resolveType(ctx); + if (operatorType.equals(STRING)) { + pushOperand(ctx, addOperator.leftOperand); + pushOperand(ctx, addOperator.rightOperand); + } else { + addOperator.compile(ctx); + pushAppendMethod(operand.resolveType(ctx), mv); + } + } else if (operand != null) { + operand.compile(ctx); + pushAppendMethod(operand.resolveType(ctx), mv); + } + } + + private void pushAppendMethod(Type operandType, GeneratorAdapter mv) { + Type argumentType = TypeDescriptors.isPrimitive(operandType) + ? operandType + : Type.getType(Object.class); + + Method appendMethod = new Method("append", STRING_BUILDER_TYPE, new Type[]{argumentType}); + mv.invokeVirtual(STRING_BUILDER_TYPE, appendMethod); + } +} diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/AndOperator.java b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/AndOperator.java new file mode 100644 index 00000000000..1424815b3d6 --- /dev/null +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/AndOperator.java @@ -0,0 +1,67 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.expressions.parser.ast.operator.binary; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.expressions.parser.ast.ExpressionNode; +import io.micronaut.expressions.parser.compilation.ExpressionVisitorContext; +import org.objectweb.asm.Label; +import org.objectweb.asm.commons.GeneratorAdapter; + +import static org.objectweb.asm.Opcodes.GOTO; +import static org.objectweb.asm.Opcodes.IFEQ; + +/** + * Expression AST node for binary && operator. + * + * @author Sergey Gavrilov + * @since 4.0.0 + */ +@Internal +public final class AndOperator extends LogicalOperator { + public AndOperator(ExpressionNode leftOperand, ExpressionNode rightOperand) { + super(leftOperand, rightOperand); + } + + @Override + public void generateBytecode(ExpressionVisitorContext ctx) { + GeneratorAdapter mv = ctx.methodVisitor(); + Label falseLabel = new Label(); + Label trueLabel = new Label(); + + pushOperand(ctx, leftOperand, falseLabel); + pushOperand(ctx, rightOperand, falseLabel); + + mv.push(true); + mv.visitJumpInsn(GOTO, trueLabel); + + mv.visitLabel(falseLabel); + mv.push(false); + + mv.visitLabel(trueLabel); + } + + private void pushOperand(ExpressionVisitorContext ctx, ExpressionNode operand, Label falseLabel) { + if (operand instanceof AndOperator andOperator) { + pushOperand(ctx, andOperator.leftOperand, falseLabel); + pushOperand(ctx, andOperator.rightOperand, falseLabel); + } else if (operand != null) { + GeneratorAdapter mv = ctx.methodVisitor(); + operand.compile(ctx); + mv.visitJumpInsn(IFEQ, falseLabel); + } + } +} diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/BinaryOperator.java b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/BinaryOperator.java new file mode 100644 index 00000000000..13f7b9075cb --- /dev/null +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/BinaryOperator.java @@ -0,0 +1,61 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.expressions.parser.ast.operator.binary; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.expressions.parser.ast.ExpressionNode; +import io.micronaut.expressions.parser.ast.conditional.ElvisOperator; +import io.micronaut.expressions.parser.compilation.ExpressionVisitorContext; +import io.micronaut.inject.ast.ClassElement; +import io.micronaut.inject.ast.PrimitiveElement; +import org.objectweb.asm.Type; + +/** + * Abstract expression AST node for binary operators. + * + * @author Sergey Gavrilov + * @since 4.0.0 + */ +@Internal +public abstract sealed class BinaryOperator extends ExpressionNode permits AddOperator, EqOperator, LogicalOperator, MathOperator, PowOperator, RelationalOperator { + protected final ExpressionNode leftOperand; + protected final ExpressionNode rightOperand; + + public BinaryOperator(ExpressionNode leftOperand, ExpressionNode rightOperand) { + this.leftOperand = leftOperand; + this.rightOperand = rightOperand; + } + + @Override + protected Type doResolveType(ExpressionVisitorContext ctx) { + Type leftType = leftOperand.resolveType(ctx); + Type rightType = rightOperand.resolveType(ctx); + return resolveOperationType(leftType, rightType); + } + + @Override + protected ClassElement doResolveClassElement(ExpressionVisitorContext ctx) { + Type type = doResolveType(ctx); + try { + return PrimitiveElement.valueOf(type.getClassName()); + } catch (IllegalArgumentException e) { + return ClassElement.of(type.getClassName()); + } + } + + protected abstract Type resolveOperationType(Type leftOperandType, + Type rightOperandType); +} diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/DivOperator.java b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/DivOperator.java new file mode 100644 index 00000000000..cbcfeebce9a --- /dev/null +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/DivOperator.java @@ -0,0 +1,58 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.expressions.parser.ast.operator.binary; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.expressions.parser.ast.ExpressionNode; +import io.micronaut.expressions.parser.compilation.ExpressionVisitorContext; +import io.micronaut.expressions.parser.exception.ExpressionCompilationException; +import org.objectweb.asm.Type; + +import java.util.Map; +import java.util.Optional; + +import static org.objectweb.asm.Opcodes.DDIV; +import static org.objectweb.asm.Opcodes.FDIV; +import static org.objectweb.asm.Opcodes.IDIV; +import static org.objectweb.asm.Opcodes.LDIV; + +/** + * Expression node for binary '/' operator. + * + * @author Sergey Gavrilov + * @since 4.0.0 + */ +@Internal +public final class DivOperator extends MathOperator { + private static final Map DIV_OPERATION_OPCODES = Map.of( + "D", DDIV, + "I", IDIV, + "F", FDIV, + "J", LDIV); + + public DivOperator(ExpressionNode leftOperand, ExpressionNode rightOperand) { + super(leftOperand, rightOperand); + } + + @Override + protected int getMathOperationOpcode(ExpressionVisitorContext ctx) { + Type type = resolveType(ctx); + String typeDescriptor = type.getDescriptor(); + return Optional.ofNullable(DIV_OPERATION_OPCODES.get(typeDescriptor)) + .orElseThrow(() -> new ExpressionCompilationException( + "'/' operation can not be applied to " + type)); + } +} diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/EqOperator.java b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/EqOperator.java new file mode 100644 index 00000000000..7445cbc75b5 --- /dev/null +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/EqOperator.java @@ -0,0 +1,62 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.expressions.parser.ast.operator.binary; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.expressions.parser.ast.ExpressionNode; +import io.micronaut.expressions.parser.compilation.ExpressionVisitorContext; +import org.objectweb.asm.Type; +import org.objectweb.asm.commons.GeneratorAdapter; +import org.objectweb.asm.commons.Method; + +import java.util.Objects; + +import static io.micronaut.expressions.parser.ast.util.TypeDescriptors.BOOLEAN; +import static io.micronaut.expressions.parser.ast.util.TypeDescriptors.OBJECT; +import static io.micronaut.expressions.parser.ast.util.EvaluatedExpressionCompilationUtils.pushBoxPrimitiveIfNecessary; + +/** + * Expression AST node for binary '==' operator. + * + * @author Sergey Gavrilov + * @since 4.0.0 + */ +@Internal +public sealed class EqOperator extends BinaryOperator permits NeqOperator { + public EqOperator(ExpressionNode leftOperand, ExpressionNode rightOperand) { + super(leftOperand, rightOperand); + } + + @Override + public void generateBytecode(ExpressionVisitorContext ctx) { + GeneratorAdapter mv = ctx.methodVisitor(); + Type lefType = leftOperand.resolveType(ctx); + Type rightType = rightOperand.resolveType(ctx); + + leftOperand.compile(ctx); + pushBoxPrimitiveIfNecessary(lefType, mv); + + rightOperand.compile(ctx); + pushBoxPrimitiveIfNecessary(rightType, mv); + + mv.invokeStatic(Type.getType(Objects.class), new Method("equals", BOOLEAN, new Type[]{OBJECT, OBJECT})); + } + + @Override + protected Type resolveOperationType(Type leftOperandType, Type rightOperandType) { + return BOOLEAN; + } +} diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/GtOperator.java b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/GtOperator.java new file mode 100644 index 00000000000..5c855ea07b1 --- /dev/null +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/GtOperator.java @@ -0,0 +1,45 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.expressions.parser.ast.operator.binary; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.expressions.parser.ast.ExpressionNode; + +import static org.objectweb.asm.Opcodes.IFLE; +import static org.objectweb.asm.Opcodes.IF_ICMPLE; + +/** + * Expression AST node for binary '>' operator. + * + * @author Sergey Gavrilov + * @since 4.0.0 + */ +@Internal +public final class GtOperator extends RelationalOperator { + public GtOperator(ExpressionNode leftOperand, ExpressionNode rightOperand) { + super(leftOperand, rightOperand); + } + + @Override + protected Integer intComparisonOpcode() { + return IF_ICMPLE; + } + + @Override + protected Integer nonIntComparisonOpcode() { + return IFLE; + } +} diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/GteOperator.java b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/GteOperator.java new file mode 100644 index 00000000000..1a36cee4ace --- /dev/null +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/GteOperator.java @@ -0,0 +1,45 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.expressions.parser.ast.operator.binary; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.expressions.parser.ast.ExpressionNode; + +import static org.objectweb.asm.Opcodes.IFLT; +import static org.objectweb.asm.Opcodes.IF_ICMPLT; + +/** + * Expression AST node for binary '>=' operator. + * + * @author Sergey Gavrilov + * @since 4.0.0 + */ +@Internal +public final class GteOperator extends RelationalOperator { + public GteOperator(ExpressionNode leftOperand, ExpressionNode rightOperand) { + super(leftOperand, rightOperand); + } + + @Override + protected Integer intComparisonOpcode() { + return IF_ICMPLT; + } + + @Override + protected Integer nonIntComparisonOpcode() { + return IFLT; + } +} diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/InstanceofOperator.java b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/InstanceofOperator.java new file mode 100644 index 00000000000..2eecb7d9c3c --- /dev/null +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/InstanceofOperator.java @@ -0,0 +1,75 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.expressions.parser.ast.operator.binary; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.expressions.parser.ast.ExpressionNode; +import io.micronaut.expressions.parser.ast.types.TypeIdentifier; +import io.micronaut.expressions.parser.compilation.ExpressionVisitorContext; +import io.micronaut.expressions.parser.exception.ExpressionCompilationException; +import io.micronaut.inject.ast.ClassElement; +import io.micronaut.inject.ast.PrimitiveElement; +import org.objectweb.asm.Type; +import org.objectweb.asm.commons.GeneratorAdapter; + +import static io.micronaut.expressions.parser.ast.util.TypeDescriptors.BOOLEAN; +import static io.micronaut.expressions.parser.ast.util.TypeDescriptors.isPrimitive; +import static io.micronaut.expressions.parser.ast.util.EvaluatedExpressionCompilationUtils.pushBoxPrimitiveIfNecessary; +import static org.objectweb.asm.Opcodes.INSTANCEOF; + +/** + * Expression AST node for 'instanceof' operator. + * + * @author Sergey Gavrilov + * @since 4.0.0 + */ +@Internal +public final class InstanceofOperator extends ExpressionNode { + private final ExpressionNode operand; + private final TypeIdentifier typeIdentifier; + + public InstanceofOperator(ExpressionNode operand, TypeIdentifier typeIdentifier) { + this.operand = operand; + this.typeIdentifier = typeIdentifier; + } + + @Override + public void generateBytecode(ExpressionVisitorContext ctx) { + Type targetType = typeIdentifier.resolveType(ctx); + if (isPrimitive(targetType)) { + throw new ExpressionCompilationException( + "'instanceof' operation can not be used with primitive right-hand side type"); + } + + GeneratorAdapter mv = ctx.methodVisitor(); + Type expressionType = operand.resolveType(ctx); + + operand.compile(ctx); + pushBoxPrimitiveIfNecessary(expressionType, mv); + + mv.visitTypeInsn(INSTANCEOF, targetType.getInternalName()); + } + + @Override + protected ClassElement doResolveClassElement(ExpressionVisitorContext ctx) { + return PrimitiveElement.BOOLEAN; + } + + @Override + protected Type doResolveType(ExpressionVisitorContext ctx) { + return BOOLEAN; + } +} diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/LogicalOperator.java b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/LogicalOperator.java new file mode 100644 index 00000000000..62a296aaeb5 --- /dev/null +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/LogicalOperator.java @@ -0,0 +1,49 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.expressions.parser.ast.operator.binary; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.expressions.parser.ast.ExpressionNode; +import io.micronaut.expressions.parser.exception.ExpressionCompilationException; +import org.objectweb.asm.Type; + +import static io.micronaut.expressions.parser.ast.util.TypeDescriptors.isBoolean; +import static org.objectweb.asm.Type.BOOLEAN_TYPE; + +/** + * Abstract expression AST node for binary logical operator. + * + * @author Sergey Gavrilov + * @since 4.0.0 + */ +@Internal +public abstract sealed class LogicalOperator extends BinaryOperator permits AndOperator, OrOperator { + + public LogicalOperator(ExpressionNode leftOperand, ExpressionNode rightOperand) { + super(leftOperand, rightOperand); + } + + @Override + protected Type resolveOperationType(Type leftOperandType, + Type rightOperandType) { + if (!isBoolean(leftOperandType) && !isBoolean(rightOperandType)) { + throw new ExpressionCompilationException( + "Logical operation can only be applied to boolean types"); + } + + return BOOLEAN_TYPE; + } +} diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/LtOperator.java b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/LtOperator.java new file mode 100644 index 00000000000..78971a4b699 --- /dev/null +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/LtOperator.java @@ -0,0 +1,45 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.expressions.parser.ast.operator.binary; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.expressions.parser.ast.ExpressionNode; + +import static org.objectweb.asm.Opcodes.IFGE; +import static org.objectweb.asm.Opcodes.IF_ICMPGE; + +/** + * Expression AST node for binary '<' operator. + * + * @author Sergey Gavrilov + * @since 4.0.0 + */ +@Internal +public final class LtOperator extends RelationalOperator { + public LtOperator(ExpressionNode leftOperand, ExpressionNode rightOperand) { + super(leftOperand, rightOperand); + } + + @Override + protected Integer intComparisonOpcode() { + return IF_ICMPGE; + } + + @Override + protected Integer nonIntComparisonOpcode() { + return IFGE; + } +} diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/LteOperator.java b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/LteOperator.java new file mode 100644 index 00000000000..bc21106281c --- /dev/null +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/LteOperator.java @@ -0,0 +1,45 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.expressions.parser.ast.operator.binary; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.expressions.parser.ast.ExpressionNode; + +import static org.objectweb.asm.Opcodes.IFGT; +import static org.objectweb.asm.Opcodes.IF_ICMPGT; + +/** + * Expression AST node for binary '<=' operator. + * + * @author Sergey Gavrilov + * @since 4.0.0 + */ +@Internal +public final class LteOperator extends RelationalOperator { + public LteOperator(ExpressionNode leftOperand, ExpressionNode rightOperand) { + super(leftOperand, rightOperand); + } + + @Override + protected Integer intComparisonOpcode() { + return IF_ICMPGT; + } + + @Override + protected Integer nonIntComparisonOpcode() { + return IFGT; + } +} diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/MatchesOperator.java b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/MatchesOperator.java new file mode 100644 index 00000000000..e89b5415f77 --- /dev/null +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/MatchesOperator.java @@ -0,0 +1,83 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.expressions.parser.ast.operator.binary; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.expressions.parser.ast.ExpressionNode; +import io.micronaut.expressions.parser.ast.literal.StringLiteral; +import io.micronaut.expressions.parser.compilation.ExpressionVisitorContext; +import io.micronaut.expressions.parser.exception.ExpressionCompilationException; +import io.micronaut.inject.ast.ClassElement; +import io.micronaut.inject.ast.PrimitiveElement; +import org.objectweb.asm.Type; +import org.objectweb.asm.commons.GeneratorAdapter; +import org.objectweb.asm.commons.Method; + +import java.util.regex.Pattern; +import java.util.regex.PatternSyntaxException; + +import static io.micronaut.expressions.parser.ast.util.TypeDescriptors.BOOLEAN; +import static io.micronaut.expressions.parser.ast.util.TypeDescriptors.STRING; + +/** + * Expression AST node for regex 'matches' operator. + * + * @author Sergey Gavrilov + * @since 4.0.0 + */ +@Internal +public final class MatchesOperator extends ExpressionNode { + + private static final Method MATCHES = new Method("matches", BOOLEAN, new Type[]{STRING}); + + private final ExpressionNode operand; + private final StringLiteral pattern; + + public MatchesOperator(ExpressionNode operand, StringLiteral pattern) { + this.operand = operand; + this.pattern = pattern; + } + + @Override + public void generateBytecode(ExpressionVisitorContext ctx) { + GeneratorAdapter mv = ctx.methodVisitor(); + operand.compile(ctx); + pattern.compile(ctx); + mv.invokeVirtual(STRING, MATCHES); + } + + @Override + protected ClassElement doResolveClassElement(ExpressionVisitorContext ctx) { + return PrimitiveElement.BOOLEAN; + } + + @Override + protected Type doResolveType(ExpressionVisitorContext ctx) { + if (!operand.resolveType(ctx).equals(STRING)) { + throw new ExpressionCompilationException( + "Operator 'matches' can only be applied to String operand"); + } + + String patternValue = pattern.getValue(); + try { + Pattern.compile(patternValue); + } catch (PatternSyntaxException ex) { + throw new ExpressionCompilationException("Invalid RegEx pattern provided: " + patternValue); + } + + return BOOLEAN; + } +} diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/MathOperator.java b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/MathOperator.java new file mode 100644 index 00000000000..7687f025b92 --- /dev/null +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/MathOperator.java @@ -0,0 +1,68 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.expressions.parser.ast.operator.binary; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.expressions.parser.ast.ExpressionNode; +import io.micronaut.expressions.parser.compilation.ExpressionVisitorContext; +import org.objectweb.asm.Type; +import org.objectweb.asm.commons.GeneratorAdapter; + +import static io.micronaut.expressions.parser.ast.util.TypeDescriptors.computeNumericOperationTargetType; +import static io.micronaut.expressions.parser.ast.util.EvaluatedExpressionCompilationUtils.pushPrimitiveCastIfNecessary; +import static io.micronaut.expressions.parser.ast.util.EvaluatedExpressionCompilationUtils.pushUnboxPrimitiveIfNecessary; + +/** + * Abstract expression AST node for binary math operations + * on primitive types. + * + * @author Sergey Gavrilov + * @since 4.0.0 + */ +@Internal +public abstract sealed class MathOperator extends BinaryOperator permits DivOperator, + ModOperator, + MulOperator, + SubOperator { + public MathOperator(ExpressionNode leftOperand, ExpressionNode rightOperand) { + super(leftOperand, rightOperand); + } + + @Override + public void generateBytecode(ExpressionVisitorContext ctx) { + GeneratorAdapter mv = ctx.methodVisitor(); + Type targetType = resolveType(ctx); + + Type leftType = leftOperand.resolveType(ctx); + leftOperand.compile(ctx); + pushUnboxPrimitiveIfNecessary(leftType, mv); + pushPrimitiveCastIfNecessary(leftType, targetType, mv); + + Type rightType = rightOperand.resolveType(ctx); + rightOperand.compile(ctx); + pushUnboxPrimitiveIfNecessary(rightType, mv); + pushPrimitiveCastIfNecessary(rightType, targetType, mv); + + mv.visitInsn(getMathOperationOpcode(ctx)); + } + + @Override + protected Type resolveOperationType(Type leftOperandType, Type rightOperandType) { + return computeNumericOperationTargetType(leftOperandType, rightOperandType); + } + + protected abstract int getMathOperationOpcode(ExpressionVisitorContext ctx); +} diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/ModOperator.java b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/ModOperator.java new file mode 100644 index 00000000000..92e62d306f8 --- /dev/null +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/ModOperator.java @@ -0,0 +1,58 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.expressions.parser.ast.operator.binary; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.expressions.parser.ast.ExpressionNode; +import io.micronaut.expressions.parser.compilation.ExpressionVisitorContext; +import io.micronaut.expressions.parser.exception.ExpressionCompilationException; +import org.objectweb.asm.Type; + +import java.util.Map; +import java.util.Optional; + +import static org.objectweb.asm.Opcodes.DREM; +import static org.objectweb.asm.Opcodes.FREM; +import static org.objectweb.asm.Opcodes.IREM; +import static org.objectweb.asm.Opcodes.LREM; + +/** + * Expression AST node for binary '/' operator. + * + * @author Sergey Gavrilov + * @since 4.0.0 + */ +@Internal +public final class ModOperator extends MathOperator { + private static final Map MOD_OPERATION_OPCODES = Map.of( + "D", DREM, + "I", IREM, + "F", FREM, + "J", LREM); + + public ModOperator(ExpressionNode leftOperand, ExpressionNode rightOperand) { + super(leftOperand, rightOperand); + } + + @Override + protected int getMathOperationOpcode(ExpressionVisitorContext ctx) { + Type type = resolveType(ctx); + String typeDescriptor = type.getDescriptor(); + return Optional.ofNullable(MOD_OPERATION_OPCODES.get(typeDescriptor)) + .orElseThrow(() -> new ExpressionCompilationException( + "'%' operation can not be applied to " + type)); + } +} diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/MulOperator.java b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/MulOperator.java new file mode 100644 index 00000000000..3eea3ab5592 --- /dev/null +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/MulOperator.java @@ -0,0 +1,59 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.expressions.parser.ast.operator.binary; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.expressions.parser.ast.ExpressionNode; +import io.micronaut.expressions.parser.compilation.ExpressionVisitorContext; +import io.micronaut.expressions.parser.exception.ExpressionCompilationException; +import org.objectweb.asm.Type; + +import java.util.Map; +import java.util.Optional; + +import static org.objectweb.asm.Opcodes.DMUL; +import static org.objectweb.asm.Opcodes.FMUL; +import static org.objectweb.asm.Opcodes.IMUL; +import static org.objectweb.asm.Opcodes.LMUL; + +/** + * Expression node for binary '*' operator. + * + * @author Sergey Gavrilov + * @since 4.0.0 + */ +@Internal +public final class MulOperator extends MathOperator { + + private static final Map MUL_OPERATION_OPCODES = Map.of( + "D", DMUL, + "I", IMUL, + "F", FMUL, + "J", LMUL); + + public MulOperator(ExpressionNode leftOperand, ExpressionNode rightOperand) { + super(leftOperand, rightOperand); + } + + @Override + protected int getMathOperationOpcode(ExpressionVisitorContext ctx) { + Type type = resolveType(ctx); + String typeDescriptor = type.getDescriptor(); + return Optional.ofNullable(MUL_OPERATION_OPCODES.get(typeDescriptor)) + .orElseThrow(() -> new ExpressionCompilationException( + "'*' operation can not be applied to " + type)); + } +} diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/NeqOperator.java b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/NeqOperator.java new file mode 100644 index 00000000000..428c7ecf68e --- /dev/null +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/NeqOperator.java @@ -0,0 +1,62 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.expressions.parser.ast.operator.binary; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.expressions.parser.ast.ExpressionNode; +import io.micronaut.expressions.parser.compilation.ExpressionVisitorContext; +import org.objectweb.asm.Label; +import org.objectweb.asm.Type; +import org.objectweb.asm.commons.GeneratorAdapter; + +import static org.objectweb.asm.Opcodes.GOTO; +import static org.objectweb.asm.Opcodes.IFNE; +import static org.objectweb.asm.Type.BOOLEAN_TYPE; + +/** + * Expression AST node for binary '!=' operator. + * + * @author Sergey Gavrilov + * @since 4.0.0 + */ +@Internal +public final class NeqOperator extends EqOperator { + public NeqOperator(ExpressionNode leftOperand, ExpressionNode rightOperand) { + super(leftOperand, rightOperand); + } + + @Override + public void generateBytecode(ExpressionVisitorContext ctx) { + super.generateBytecode(ctx); + + GeneratorAdapter mv = ctx.methodVisitor(); + Label elseLabel = new Label(); + Label endOfCmpLabel = new Label(); + + mv.visitJumpInsn(IFNE, elseLabel); + + mv.push(true); + mv.visitJumpInsn(GOTO, endOfCmpLabel); + mv.visitLabel(elseLabel); + mv.push(false); + mv.visitLabel(endOfCmpLabel); + } + + @Override + protected Type resolveOperationType(Type leftOperandType, Type rightOperandType) { + return BOOLEAN_TYPE; + } +} diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/OrOperator.java b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/OrOperator.java new file mode 100644 index 00000000000..283e02b9ca3 --- /dev/null +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/OrOperator.java @@ -0,0 +1,55 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.expressions.parser.ast.operator.binary; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.expressions.parser.ast.ExpressionNode; +import io.micronaut.expressions.parser.compilation.ExpressionVisitorContext; +import org.objectweb.asm.Label; +import org.objectweb.asm.commons.GeneratorAdapter; + +import static org.objectweb.asm.Opcodes.GOTO; +import static org.objectweb.asm.Opcodes.IFEQ; + +/** + * Expression node for binary '||' operator. + * + * @author Sergey Gavrilov + * @since 4.0.0 + */ +@Internal +public final class OrOperator extends LogicalOperator { + public OrOperator(ExpressionNode leftOperand, ExpressionNode rightOperand) { + super(leftOperand, rightOperand); + } + + @Override + public void generateBytecode(ExpressionVisitorContext ctx) { + GeneratorAdapter mv = ctx.methodVisitor(); + Label falseLabel = new Label(); + Label returnLabel = new Label(); + + leftOperand.compile(ctx); + mv.visitJumpInsn(IFEQ, falseLabel); + mv.push(true); + mv.visitJumpInsn(GOTO, returnLabel); + + mv.visitLabel(falseLabel); + rightOperand.compile(ctx); + + mv.visitLabel(returnLabel); + } +} diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/PowOperator.java b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/PowOperator.java new file mode 100644 index 00000000000..c62d15703c8 --- /dev/null +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/PowOperator.java @@ -0,0 +1,88 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.expressions.parser.ast.operator.binary; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.expressions.parser.ast.ExpressionNode; +import io.micronaut.expressions.parser.compilation.ExpressionVisitorContext; +import io.micronaut.expressions.parser.exception.ExpressionCompilationException; +import org.objectweb.asm.Type; +import org.objectweb.asm.commons.GeneratorAdapter; +import org.objectweb.asm.commons.Method; + +import static io.micronaut.expressions.parser.ast.util.TypeDescriptors.DOUBLE; +import static io.micronaut.expressions.parser.ast.util.TypeDescriptors.FLOAT; +import static io.micronaut.expressions.parser.ast.util.TypeDescriptors.LONG; +import static io.micronaut.expressions.parser.ast.util.TypeDescriptors.isNumeric; +import static io.micronaut.expressions.parser.ast.util.TypeDescriptors.isOneOf; +import static io.micronaut.expressions.parser.ast.util.TypeDescriptors.toUnboxedIfNecessary; +import static io.micronaut.expressions.parser.ast.util.EvaluatedExpressionCompilationUtils.pushPrimitiveCastIfNecessary; +import static io.micronaut.expressions.parser.ast.util.EvaluatedExpressionCompilationUtils.pushUnboxPrimitiveIfNecessary; + +/** + * Expression AST node for '^' operator. '^' operator in evaluated + * expressions means power operation + * + * @author Sergey Gavrilov + * @since 4.0.0 + */ +@Internal +public final class PowOperator extends BinaryOperator { + + private static final Type MATH_TYPE = Type.getType(Math.class); + private static final Method POW_METHOD = new Method("pow", DOUBLE, new Type[]{DOUBLE, DOUBLE}); + + public PowOperator(ExpressionNode leftOperand, ExpressionNode rightOperand) { + super(leftOperand, rightOperand); + } + + @Override + public void generateBytecode(ExpressionVisitorContext ctx) { + GeneratorAdapter mv = ctx.methodVisitor(); + + Type leftType = leftOperand.resolveType(ctx); + Type rightType = rightOperand.resolveType(ctx); + + leftOperand.compile(ctx); + pushUnboxPrimitiveIfNecessary(leftType, mv); + pushPrimitiveCastIfNecessary(leftType, DOUBLE, mv); + + rightOperand.compile(ctx); + pushUnboxPrimitiveIfNecessary(leftType, mv); + pushPrimitiveCastIfNecessary(rightType, DOUBLE, mv); + + mv.invokeStatic(MATH_TYPE, POW_METHOD); + + if (resolveType(ctx) == LONG) { + pushPrimitiveCastIfNecessary(DOUBLE, LONG, mv); + } + } + + @Override + protected Type resolveOperationType(Type leftOperandType, Type rightOperandType) { + if (!isNumeric(leftOperandType) || !isNumeric(rightOperandType)) { + throw new ExpressionCompilationException("Power operation can only be applied to numeric types"); + } + + if (isOneOf(toUnboxedIfNecessary(leftOperandType), DOUBLE, FLOAT) || + isOneOf(toUnboxedIfNecessary(rightOperandType), DOUBLE, FLOAT)) { + return DOUBLE; + } + + // Int power operation result might not fit in int value + return LONG; + } +} diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/RelationalOperator.java b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/RelationalOperator.java new file mode 100644 index 00000000000..41fcb9c8309 --- /dev/null +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/RelationalOperator.java @@ -0,0 +1,111 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.expressions.parser.ast.operator.binary; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.expressions.parser.ast.ExpressionNode; +import io.micronaut.expressions.parser.compilation.ExpressionVisitorContext; +import io.micronaut.expressions.parser.exception.ExpressionCompilationException; +import org.objectweb.asm.Label; +import org.objectweb.asm.Type; +import org.objectweb.asm.commons.GeneratorAdapter; + +import static io.micronaut.expressions.parser.ast.util.TypeDescriptors.BOOLEAN; +import static io.micronaut.expressions.parser.ast.util.TypeDescriptors.DOUBLE; +import static io.micronaut.expressions.parser.ast.util.TypeDescriptors.FLOAT; +import static io.micronaut.expressions.parser.ast.util.TypeDescriptors.LONG; +import static io.micronaut.expressions.parser.ast.util.TypeDescriptors.computeNumericOperationTargetType; +import static io.micronaut.expressions.parser.ast.util.TypeDescriptors.isNumeric; +import static io.micronaut.expressions.parser.ast.util.TypeDescriptors.isOneOf; +import static io.micronaut.expressions.parser.ast.util.EvaluatedExpressionCompilationUtils.pushPrimitiveCastIfNecessary; +import static io.micronaut.expressions.parser.ast.util.EvaluatedExpressionCompilationUtils.pushUnboxPrimitiveIfNecessary; +import static org.objectweb.asm.Opcodes.DCMPL; +import static org.objectweb.asm.Opcodes.FCMPL; +import static org.objectweb.asm.Opcodes.GOTO; +import static org.objectweb.asm.Opcodes.LCMP; +import static org.objectweb.asm.Type.BOOLEAN_TYPE; + +/** + * Abstract expression AST node for relational operators. + * + * @author Sergey Gavrilov + * @since 4.0.0 + */ +@Internal +public abstract sealed class RelationalOperator extends BinaryOperator permits GtOperator, + GteOperator, + LtOperator, + LteOperator { + public RelationalOperator(ExpressionNode leftOperand, ExpressionNode rightOperand) { + super(leftOperand, rightOperand); + this.nodeType = BOOLEAN; + } + + @Override + protected Type resolveOperationType(Type leftOperandType, + Type rightOperandType) { + if (!isNumeric(leftOperandType) || !isNumeric(rightOperandType)) { + throw new ExpressionCompilationException("Relational operation can only be applied to" + + " numeric types"); + } + + return BOOLEAN_TYPE; + } + + @Override + public void generateBytecode(ExpressionVisitorContext ctx) { + GeneratorAdapter mv = ctx.methodVisitor(); + + Type leftType = leftOperand.resolveType(ctx); + Type rightType = rightOperand.resolveType(ctx); + + Type targetType = computeNumericOperationTargetType(leftType, rightType); + + leftOperand.compile(ctx); + pushUnboxPrimitiveIfNecessary(leftType, mv); + pushPrimitiveCastIfNecessary(leftType, targetType, mv); + + rightOperand.compile(ctx); + pushUnboxPrimitiveIfNecessary(rightType, mv); + pushPrimitiveCastIfNecessary(rightType, targetType, mv); + + Label elseLabel = new Label(); + Label endOfCmpLabel = new Label(); + + if (isOneOf(targetType, DOUBLE, FLOAT, LONG)) { + String targetDescriptor = targetType.getDescriptor(); + switch (targetDescriptor) { + case "D" -> mv.visitInsn(DCMPL); + case "F" -> mv.visitInsn(FCMPL); + case "J" -> mv.visitInsn(LCMP); + default -> { } + } + mv.visitJumpInsn(nonIntComparisonOpcode(), elseLabel); + } else { + mv.visitJumpInsn(intComparisonOpcode(), elseLabel); + } + + mv.push(true); + mv.visitJumpInsn(GOTO, endOfCmpLabel); + mv.visitLabel(elseLabel); + mv.push(false); + mv.visitLabel(endOfCmpLabel); + } + + protected abstract Integer intComparisonOpcode(); + + protected abstract Integer nonIntComparisonOpcode(); +} diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/SubOperator.java b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/SubOperator.java new file mode 100644 index 00000000000..932bdcf5652 --- /dev/null +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/SubOperator.java @@ -0,0 +1,59 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.expressions.parser.ast.operator.binary; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.expressions.parser.ast.ExpressionNode; +import io.micronaut.expressions.parser.compilation.ExpressionVisitorContext; +import io.micronaut.expressions.parser.exception.ExpressionCompilationException; +import org.objectweb.asm.Type; + +import java.util.Map; +import java.util.Optional; + +import static org.objectweb.asm.Opcodes.DSUB; +import static org.objectweb.asm.Opcodes.FSUB; +import static org.objectweb.asm.Opcodes.ISUB; +import static org.objectweb.asm.Opcodes.LSUB; + +/** + * Expression AST node for binary '-' operator. + * + * @author Sergey Gavrilov + * @since 4.0.0 + */ +@Internal +public final class SubOperator extends MathOperator { + + private static final Map SUB_OPERATION_OPCODES = Map.of( + "D", DSUB, + "I", ISUB, + "F", FSUB, + "J", LSUB); + + public SubOperator(ExpressionNode leftOperand, ExpressionNode rightOperand) { + super(leftOperand, rightOperand); + } + + @Override + protected int getMathOperationOpcode(ExpressionVisitorContext ctx) { + Type type = resolveType(ctx); + String typeDescriptor = type.getDescriptor(); + return Optional.ofNullable(SUB_OPERATION_OPCODES.get(typeDescriptor)) + .orElseThrow(() -> new ExpressionCompilationException( + "'*' operation can not be applied to " + type)); + } +} diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/unary/EmptyOperator.java b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/unary/EmptyOperator.java new file mode 100644 index 00000000000..b7f87a592fd --- /dev/null +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/unary/EmptyOperator.java @@ -0,0 +1,135 @@ +/* + * Copyright 2017-2023 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.expressions.parser.ast.operator.unary; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.reflect.ReflectionUtils; +import io.micronaut.core.util.ArrayUtils; +import io.micronaut.core.util.CollectionUtils; +import io.micronaut.core.util.StringUtils; +import io.micronaut.expressions.parser.ast.ExpressionNode; +import io.micronaut.expressions.parser.compilation.ExpressionVisitorContext; +import io.micronaut.inject.ast.ClassElement; +import io.micronaut.inject.ast.PrimitiveElement; +import org.objectweb.asm.Type; +import org.objectweb.asm.commons.GeneratorAdapter; +import org.objectweb.asm.commons.Method; + +import java.util.Collection; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; + +/** + * The empty operator. + */ +@Internal +public final class EmptyOperator extends UnaryOperator { + + private static final String IS_EMPTY = "isEmpty"; + + public EmptyOperator(ExpressionNode operand) { + super(operand); + } + + @Override + protected void generateBytecode(ExpressionVisitorContext ctx) { + ClassElement type = operand.resolveClassElement(ctx); + + GeneratorAdapter mv = ctx.methodVisitor(); + + operand.compile(ctx); + if (type.isAssignable(CharSequence.class)) { + mv.invokeStatic( + Type.getType(StringUtils.class), + Method.getMethod( + ReflectionUtils.getRequiredMethod( + StringUtils.class, + IS_EMPTY, + CharSequence.class + ) + ) + ); + } else if (type.isAssignable(Collection.class)) { + mv.invokeStatic( + Type.getType(CollectionUtils.class), + Method.getMethod( + ReflectionUtils.getRequiredMethod( + CollectionUtils.class, + IS_EMPTY, + Collection.class + ) + ) + ); + } else if (type.isAssignable(Map.class)) { + mv.invokeStatic( + Type.getType(CollectionUtils.class), + Method.getMethod( + ReflectionUtils.getRequiredMethod( + CollectionUtils.class, + IS_EMPTY, + Map.class + ) + ) + ); + } else if (type.isAssignable(Optional.class)) { + mv.invokeVirtual( + Type.getType(Optional.class), + Method.getMethod( + ReflectionUtils.getRequiredMethod( + Optional.class, + IS_EMPTY + ) + ) + ); + } else if (type.isArray() && !type.isPrimitive()) { + mv.invokeStatic( + Type.getType(ArrayUtils.class), + Method.getMethod( + ReflectionUtils.getRequiredMethod( + ArrayUtils.class, + IS_EMPTY, + Object[].class + ) + ) + ); + } else if (type.isPrimitive()) { + // primitives are never empty + mv.push(false); + } else { + mv.invokeStatic( + Type.getType(Objects.class), + Method.getMethod( + ReflectionUtils.getRequiredMethod( + Objects.class, + "isNull", + Object.class + ) + ) + ); + } + } + + @Override + public Type doResolveType(ExpressionVisitorContext ctx) { + return Type.BOOLEAN_TYPE; + } + + @Override + protected ClassElement doResolveClassElement(ExpressionVisitorContext ctx) { + return PrimitiveElement.BOOLEAN; + } +} diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/unary/NegOperator.java b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/unary/NegOperator.java new file mode 100644 index 00000000000..6b5bab4a3b8 --- /dev/null +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/unary/NegOperator.java @@ -0,0 +1,83 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.expressions.parser.ast.operator.unary; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.expressions.parser.ast.ExpressionNode; +import io.micronaut.expressions.parser.compilation.ExpressionVisitorContext; +import io.micronaut.expressions.parser.exception.ExpressionCompilationException; +import org.objectweb.asm.Type; +import org.objectweb.asm.commons.GeneratorAdapter; + +import static io.micronaut.expressions.parser.ast.util.TypeDescriptors.DOUBLE; +import static io.micronaut.expressions.parser.ast.util.TypeDescriptors.DOUBLE_WRAPPER; +import static io.micronaut.expressions.parser.ast.util.TypeDescriptors.FLOAT; +import static io.micronaut.expressions.parser.ast.util.TypeDescriptors.FLOAT_WRAPPER; +import static io.micronaut.expressions.parser.ast.util.TypeDescriptors.INT; +import static io.micronaut.expressions.parser.ast.util.TypeDescriptors.INT_WRAPPER; +import static io.micronaut.expressions.parser.ast.util.TypeDescriptors.LONG; +import static io.micronaut.expressions.parser.ast.util.TypeDescriptors.LONG_WRAPPER; +import static io.micronaut.expressions.parser.ast.util.TypeDescriptors.isNumeric; +import static io.micronaut.expressions.parser.ast.util.TypeDescriptors.isOneOf; +import static io.micronaut.expressions.parser.ast.util.EvaluatedExpressionCompilationUtils.pushUnboxPrimitiveIfNecessary; +import static org.objectweb.asm.Opcodes.DNEG; +import static org.objectweb.asm.Opcodes.FNEG; +import static org.objectweb.asm.Opcodes.INEG; +import static org.objectweb.asm.Opcodes.LNEG; + +/** + * Expression node for unary '-' operator. + * + * @author Sergey Gavrilov + * @since 4.0.0 + */ +@Internal +public final class NegOperator extends UnaryOperator { + public NegOperator(ExpressionNode operand) { + super(operand); + } + + @Override + public Type doResolveType(ExpressionVisitorContext ctx) { + Type nodeType = super.doResolveType(ctx); + if (!isNumeric(nodeType)) { + throw new ExpressionCompilationException( + "Invalid unary '-' operation. Unary '-' can only be applied to numeric types"); + } + return nodeType; + } + + @Override + public void generateBytecode(ExpressionVisitorContext ctx) { + GeneratorAdapter mv = ctx.methodVisitor(); + + operand.compile(ctx); + pushUnboxPrimitiveIfNecessary(operand.resolveType(ctx), mv); + + if (isOneOf(operand.resolveType(ctx), INT, INT_WRAPPER)) { + mv.visitInsn(INEG); + } else if (isOneOf(operand.resolveType(ctx), DOUBLE, DOUBLE_WRAPPER)) { + mv.visitInsn(DNEG); + } else if (isOneOf(operand.resolveType(ctx), FLOAT, FLOAT_WRAPPER)) { + mv.visitInsn(FNEG); + } else if (isOneOf(operand.resolveType(ctx), LONG, LONG_WRAPPER)) { + mv.visitInsn(LNEG); + } else { + throw new ExpressionCompilationException( + "Invalid unary '-' operation. Unary '-' can only be applied to numeric types"); + } + } +} diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/unary/NotOperator.java b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/unary/NotOperator.java new file mode 100644 index 00000000000..d1d4fb9723c --- /dev/null +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/unary/NotOperator.java @@ -0,0 +1,74 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.expressions.parser.ast.operator.unary; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.expressions.parser.ast.ExpressionNode; +import io.micronaut.expressions.parser.compilation.ExpressionVisitorContext; +import io.micronaut.expressions.parser.exception.ExpressionCompilationException; +import org.objectweb.asm.Label; +import org.objectweb.asm.Type; +import org.objectweb.asm.commons.GeneratorAdapter; + +import static io.micronaut.expressions.parser.ast.util.TypeDescriptors.isBoolean; +import static org.objectweb.asm.Opcodes.GOTO; +import static org.objectweb.asm.Opcodes.IFNE; + +/** + * Expression node for unary '!' operator. + * + * @author Sergey Gavrilov + * @since 4.0.0 + */ +@Internal +public final class NotOperator extends UnaryOperator { + public NotOperator(ExpressionNode operand) { + super(operand); + } + + @Override + public Type doResolveType(ExpressionVisitorContext ctx) { + if (nodeType != null) { + return nodeType; + } + + Type nodeType = super.doResolveType(ctx); + if (!isBoolean(nodeType)) { + throw new ExpressionCompilationException( + "Invalid unary '!' operation. Unary '!' can only be applied to boolean types"); + } + + this.nodeType = nodeType; + return nodeType; + } + + @Override + public void generateBytecode(ExpressionVisitorContext ctx) { + GeneratorAdapter mv = ctx.methodVisitor(); + Label falseLabel = new Label(); + Label returnLabel = new Label(); + + operand.compile(ctx); + mv.visitJumpInsn(IFNE, falseLabel); + mv.push(true); + mv.visitJumpInsn(GOTO, returnLabel); + + mv.visitLabel(falseLabel); + mv.push(false); + + mv.visitLabel(returnLabel); + } +} diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/unary/PosOperator.java b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/unary/PosOperator.java new file mode 100644 index 00000000000..907976253cd --- /dev/null +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/unary/PosOperator.java @@ -0,0 +1,53 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.expressions.parser.ast.operator.unary; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.expressions.parser.ast.ExpressionNode; +import io.micronaut.expressions.parser.compilation.ExpressionVisitorContext; +import io.micronaut.expressions.parser.exception.ExpressionCompilationException; +import org.objectweb.asm.Type; + +import static io.micronaut.expressions.parser.ast.util.TypeDescriptors.isNumeric; + +/** + * Expression node for unary '+' operator. + * + * @author Sergey Gavrilov + * @since 4.0.0 + */ +@Internal +public final class PosOperator extends UnaryOperator { + public PosOperator(ExpressionNode operand) { + super(operand); + } + + @Override + public Type doResolveType(ExpressionVisitorContext ctx) { + Type nodeType = super.doResolveType(ctx); + + if (!isNumeric(nodeType)) { + throw new ExpressionCompilationException("Invalid unary '+' operation"); + } + + return nodeType; + } + + @Override + public void generateBytecode(ExpressionVisitorContext ctx) { + operand.compile(ctx); + } +} diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/unary/UnaryOperator.java b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/unary/UnaryOperator.java new file mode 100644 index 00000000000..9422afaf200 --- /dev/null +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/unary/UnaryOperator.java @@ -0,0 +1,53 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.expressions.parser.ast.operator.unary; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.expressions.parser.ast.ExpressionNode; +import io.micronaut.expressions.parser.compilation.ExpressionVisitorContext; +import io.micronaut.inject.ast.ClassElement; +import io.micronaut.inject.ast.PrimitiveElement; +import org.objectweb.asm.Type; + +/** + * Abstract expression node for unary operators. + * + * @author Sergey Gavrilov + * @since 4.0.0 + */ +@Internal +public abstract sealed class UnaryOperator extends ExpressionNode permits EmptyOperator, NegOperator, NotOperator, PosOperator { + protected final ExpressionNode operand; + + public UnaryOperator(ExpressionNode operand) { + this.operand = operand; + } + + @Override + public Type doResolveType(ExpressionVisitorContext ctx) { + return operand.resolveType(ctx); + } + + @Override + protected ClassElement doResolveClassElement(ExpressionVisitorContext ctx) { + Type type = doResolveType(ctx); + try { + return PrimitiveElement.valueOf(type.getClassName()); + } catch (IllegalArgumentException e) { + return ClassElement.of(type.getClassName()); + } + } +} diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/types/TypeIdentifier.java b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/types/TypeIdentifier.java new file mode 100644 index 00000000000..bf58e550d71 --- /dev/null +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/types/TypeIdentifier.java @@ -0,0 +1,110 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.expressions.parser.ast.types; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.expressions.parser.ast.ExpressionNode; +import io.micronaut.expressions.parser.ast.util.TypeDescriptors; +import io.micronaut.expressions.parser.compilation.ExpressionVisitorContext; +import io.micronaut.expressions.parser.exception.ExpressionCompilationException; +import io.micronaut.inject.ast.ClassElement; +import io.micronaut.inject.ast.PrimitiveElement; +import io.micronaut.inject.processing.JavaModelUtils; +import org.objectweb.asm.Type; + +import java.util.Map; +import java.util.Optional; + +/** + * Expression node for type identifier. Bytecode for identifier is not generated + * directly - it is generated by nodes using the identifier. + * + * @author Sergey Gavrilov + * @since 4.0.0 + */ +@Internal +public final class TypeIdentifier extends ExpressionNode { + private static final Map PRIMITIVES = Map.of( + "int", TypeDescriptors.INT, + "long", TypeDescriptors.LONG, + "byte", TypeDescriptors.BYTE, + "short", TypeDescriptors.SHORT, + "char", TypeDescriptors.CHAR, + "boolean", TypeDescriptors.BOOLEAN, + "double", TypeDescriptors.DOUBLE, + "float", TypeDescriptors.FLOAT); + + private final String name; + + public TypeIdentifier(String name) { + this.name = name; + } + + public boolean isPrimitive() { + return PRIMITIVES.containsKey(this.toString()); + } + + @Override + public void generateBytecode(ExpressionVisitorContext ctx) { + ctx.methodVisitor().push(resolveType(ctx)); + } + + @Override + protected ClassElement doResolveClassElement(ExpressionVisitorContext ctx) { + String name = this.toString(); + if (PRIMITIVES.containsKey(name)) { + return PrimitiveElement.valueOf(name); + } + Optional resolvedType = ctx.visitorContext().getClassElement(name); + if (resolvedType.isEmpty() && !name.contains(".")) { + resolvedType = ctx.visitorContext().getClassElement("java.lang." + name); + } + return resolvedType + .orElseThrow(() -> new ExpressionCompilationException("Unknown type identifier: " + name)); + } + + @Override + public Type doResolveType(ExpressionVisitorContext ctx) { + String name = this.toString(); + if (PRIMITIVES.containsKey(name)) { + return PRIMITIVES.get(name); + } + + Type resolvedType = resolveObjectType(ctx, name); + + // may be java.lang type + if (resolvedType == null && !name.contains(".")) { + resolvedType = resolveObjectType(ctx, "java.lang." + name); + } + + if (resolvedType == null) { + throw new ExpressionCompilationException("Unknown type identifier: " + name); + } + + return resolvedType; + } + + private Type resolveObjectType(ExpressionVisitorContext ctx, String name) { + return ctx.visitorContext().getClassElement(name) + .map(JavaModelUtils::getTypeReference) + .orElse(null); + } + + @Override + public String toString() { + return name; + } +} diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/util/EvaluatedExpressionCompilationUtils.java b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/util/EvaluatedExpressionCompilationUtils.java new file mode 100644 index 00000000000..3a6df9a282d --- /dev/null +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/util/EvaluatedExpressionCompilationUtils.java @@ -0,0 +1,254 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.expressions.parser.ast.util; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.NonNull; +import io.micronaut.expressions.parser.exception.ExpressionCompilationException; +import io.micronaut.inject.ast.ClassElement; +import io.micronaut.inject.ast.PrimitiveElement; +import io.micronaut.inject.visitor.VisitorContext; +import org.objectweb.asm.Type; +import org.objectweb.asm.commons.GeneratorAdapter; +import org.objectweb.asm.commons.Method; + +import java.util.Optional; + +import static io.micronaut.expressions.parser.ast.util.TypeDescriptors.BOOLEAN; +import static io.micronaut.expressions.parser.ast.util.TypeDescriptors.BOOLEAN_WRAPPER; +import static io.micronaut.expressions.parser.ast.util.TypeDescriptors.BYTE; +import static io.micronaut.expressions.parser.ast.util.TypeDescriptors.BYTE_WRAPPER; +import static io.micronaut.expressions.parser.ast.util.TypeDescriptors.CHAR; +import static io.micronaut.expressions.parser.ast.util.TypeDescriptors.CHAR_WRAPPER; +import static io.micronaut.expressions.parser.ast.util.TypeDescriptors.DOUBLE; +import static io.micronaut.expressions.parser.ast.util.TypeDescriptors.DOUBLE_WRAPPER; +import static io.micronaut.expressions.parser.ast.util.TypeDescriptors.FLOAT; +import static io.micronaut.expressions.parser.ast.util.TypeDescriptors.FLOAT_WRAPPER; +import static io.micronaut.expressions.parser.ast.util.TypeDescriptors.INT; +import static io.micronaut.expressions.parser.ast.util.TypeDescriptors.INT_WRAPPER; +import static io.micronaut.expressions.parser.ast.util.TypeDescriptors.LONG; +import static io.micronaut.expressions.parser.ast.util.TypeDescriptors.LONG_WRAPPER; +import static io.micronaut.expressions.parser.ast.util.TypeDescriptors.SHORT; +import static io.micronaut.expressions.parser.ast.util.TypeDescriptors.SHORT_WRAPPER; +import static io.micronaut.expressions.parser.ast.util.TypeDescriptors.toBoxedIfNecessary; +import static io.micronaut.expressions.parser.ast.util.TypeDescriptors.toUnboxedIfNecessary; +import static io.micronaut.inject.processing.JavaModelUtils.getTypeReference; +import static org.objectweb.asm.Opcodes.D2F; +import static org.objectweb.asm.Opcodes.D2I; +import static org.objectweb.asm.Opcodes.D2L; +import static org.objectweb.asm.Opcodes.F2D; +import static org.objectweb.asm.Opcodes.F2I; +import static org.objectweb.asm.Opcodes.F2L; +import static org.objectweb.asm.Opcodes.I2D; +import static org.objectweb.asm.Opcodes.I2F; +import static org.objectweb.asm.Opcodes.I2L; +import static org.objectweb.asm.Opcodes.L2D; +import static org.objectweb.asm.Opcodes.L2F; +import static org.objectweb.asm.Opcodes.L2I; + +/** + * Unility methods for used when compiling evaluated expressions. + * + * @author Sergey Gavrilov + * @since 4.0.0 + */ +@Internal +public final class EvaluatedExpressionCompilationUtils { + + /** + * Checks whether the argument class element is assignable to the parameter + * class element. This method also accepts primitive and wrapper elements and + * determines whether argument can be assigned to parameter after boxing or unboxing. + * In case when parameter or argument is an array, array dimensions are also checked. + * + * @param parameter checked parameter + * @param argument checked argument + * @return whether argument is assignable to parameter + */ + public static boolean isAssignable(@NonNull ClassElement parameter, + @NonNull ClassElement argument) { + if (!argument.isAssignable(parameter)) { + Type parameterType = getTypeReference(parameter); + Type argumentType = getTypeReference(argument); + + return toUnboxedIfNecessary(parameterType).equals(toUnboxedIfNecessary(argumentType)) + || toBoxedIfNecessary(parameterType).equals(toBoxedIfNecessary(argumentType)); + } + + if (parameter.getArrayDimensions() > 0 || argument.getArrayDimensions() > 0) { + return parameter.getArrayDimensions() == argument.getArrayDimensions(); + } + + return true; + } + + /** + * Provides {@link ClassElement} for passed type or throws exception + * if class element can not be provided. + * + * @param type Type element for which {@link ClassElement} needs to be obtained. + * This type can also represent a primitive type. In this case it will be + * boxed + * @param visitorContext visitor context + * @return resolved class element + * @throws ExpressionCompilationException if class element can not be obtained + */ + @NonNull + public static ClassElement getRequiredClassElement(Type type, + VisitorContext visitorContext) { + boolean isArrayType = type.getDescriptor().startsWith("["); + if (isArrayType) { + Type elementType = type.getElementType(); + ClassElement classElement = toPrimitiveElement(elementType).orElse(null); + if (classElement == null) { + classElement = getClassElementForName(visitorContext, elementType.getClassName()); + } + + for (int i = 0; i < type.getDimensions(); i++) { + classElement = classElement.toArray(); + } + + return classElement; + } + + + String className = toBoxedIfNecessary(type).getClassName(); + return getClassElementForName(visitorContext, className); + } + + private static ClassElement getClassElementForName(VisitorContext visitorContext, String className) { + return visitorContext.getClassElement(className) + .orElseThrow(() -> new ExpressionCompilationException( + "Can not resolve type information for [" + className + "]")); + } + + /** + * Pushed unboxing instruction if passed type is a primitive wrapper. + * + * @param type type to unbox + * @param mv method visitor + */ + public static void pushUnboxPrimitiveIfNecessary(@NonNull Type type, + @NonNull GeneratorAdapter mv) { + if (type.equals(BOOLEAN_WRAPPER)) { + mv.invokeVirtual(BOOLEAN_WRAPPER, new Method("booleanValue", "()Z")); + } else if (type.equals(INT_WRAPPER)) { + mv.invokeVirtual(INT_WRAPPER, new Method("intValue", "()I")); + } else if (type.equals(DOUBLE_WRAPPER)) { + mv.invokeVirtual(DOUBLE_WRAPPER, new Method("doubleValue", "()D")); + } else if (type.equals(LONG_WRAPPER)) { + mv.invokeVirtual(LONG_WRAPPER, new Method("longValue", "()J")); + } else if (type.equals(FLOAT_WRAPPER)) { + mv.invokeVirtual(FLOAT_WRAPPER, new Method("floatValue", "()F")); + } else if (type.equals(SHORT_WRAPPER)) { + mv.invokeVirtual(SHORT_WRAPPER, new Method("shortValue", "()S")); + } else if (type.equals(CHAR_WRAPPER)) { + mv.invokeVirtual(CHAR_WRAPPER, new Method("charValue", "()C")); + } else if (type.equals(BYTE_WRAPPER)) { + mv.invokeVirtual(BYTE_WRAPPER, new Method("byteValue", "()B")); + } + } + + /** + * Pushed primitive boxing instruction if passed type is a wrapper. + * + * @param type type to box + * @param mv method visitor + */ + public static void pushBoxPrimitiveIfNecessary(@NonNull Type type, + @NonNull GeneratorAdapter mv) { + if (type.equals(BOOLEAN)) { + mv.invokeStatic(BOOLEAN_WRAPPER, new Method("valueOf", BOOLEAN_WRAPPER, new Type[]{BOOLEAN})); + } else if (type.equals(INT)) { + mv.invokeStatic(INT_WRAPPER, new Method("valueOf", INT_WRAPPER, new Type[]{INT})); + } else if (type.equals(DOUBLE)) { + mv.invokeStatic(DOUBLE_WRAPPER, new Method("valueOf", DOUBLE_WRAPPER, new Type[]{DOUBLE})); + } else if (type.equals(LONG)) { + mv.invokeStatic(LONG_WRAPPER, new Method("valueOf", LONG_WRAPPER, new Type[]{LONG})); + } else if (type.equals(FLOAT)) { + mv.invokeStatic(FLOAT_WRAPPER, new Method("valueOf", FLOAT_WRAPPER, new Type[]{FLOAT})); + } else if (type.equals(SHORT)) { + mv.invokeStatic(SHORT_WRAPPER, new Method("valueOf", SHORT_WRAPPER, new Type[]{SHORT})); + } else if (type.equals(CHAR)) { + mv.invokeStatic(CHAR_WRAPPER, new Method("valueOf", CHAR_WRAPPER, new Type[]{CHAR})); + } else if (type.equals(BYTE)) { + mv.invokeStatic(BYTE_WRAPPER, new Method("valueOf", BYTE_WRAPPER, new Type[]{BYTE})); + } + } + + /** + * @param type type to be converted to {@link PrimitiveElement} + * @return optional corresponding primitive element + */ + public static Optional toPrimitiveElement(Type type) { + try { + return Optional.of(PrimitiveElement.valueOf(type.getClassName())); + } catch (IllegalArgumentException ex) { + return Optional.empty(); + } + } + + /** + * This method checks whether passed primitive type needs to be explicitly cast + * to target type. If it is, respective cast instruction is pushed. + * + * @param type type to cast + * @param targetType target type to which the cast is required + * @param mv method visitor + */ + public static void pushPrimitiveCastIfNecessary(@NonNull Type type, + @NonNull Type targetType, + @NonNull GeneratorAdapter mv) { + String typeDescriptor = type.getDescriptor(); + String targetDescriptor = targetType.getDescriptor(); + + switch (targetDescriptor) { + case "J" -> { + switch (typeDescriptor) { + case "I" -> mv.visitInsn(I2L); + case "D" -> mv.visitInsn(D2L); + case "F" -> mv.visitInsn(F2L); + default -> { } + } + } + case "I" -> { + switch (typeDescriptor) { + case "J" -> mv.visitInsn(L2I); + case "D" -> mv.visitInsn(D2I); + case "F" -> mv.visitInsn(F2I); + default -> { } + } + } + case "D" -> { + switch (typeDescriptor) { + case "J" -> mv.visitInsn(L2D); + case "I" -> mv.visitInsn(I2D); + case "F" -> mv.visitInsn(F2D); + default -> { } + } + } + case "F" -> { + switch (typeDescriptor) { + case "J" -> mv.visitInsn(L2F); + case "I" -> mv.visitInsn(I2F); + case "D" -> mv.visitInsn(D2F); + default -> { } + } + } + default -> { } + } + } +} diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/util/TypeDescriptors.java b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/util/TypeDescriptors.java new file mode 100644 index 00000000000..80df94db8a2 --- /dev/null +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/util/TypeDescriptors.java @@ -0,0 +1,193 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.expressions.parser.ast.util; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.NonNull; +import io.micronaut.core.expressions.ExpressionEvaluationContext; +import io.micronaut.expressions.parser.exception.ExpressionCompilationException; +import org.objectweb.asm.Type; + +import java.util.Map; + +import static java.util.stream.Collectors.toMap; + +/** + * Set of constants and utility methods for working with type descriptors + * while compiling evaluated expressions. + * + * @author Sergey Gavrilov + * @since 4.0.0 + */ +@Internal +public final class TypeDescriptors { + public static final Type EVALUATION_CONTEXT_TYPE = Type.getType(ExpressionEvaluationContext.class); + + public static final Type STRING = Type.getType(String.class); + public static final Type OBJECT = Type.getType(Object.class); + public static final Type CLASS = Type.getType(Class.class); + public static final Type VOID = Type.VOID_TYPE; + + // Primitives + public static final Type DOUBLE = Type.DOUBLE_TYPE; + public static final Type FLOAT = Type.FLOAT_TYPE; + public static final Type INT = Type.INT_TYPE; + public static final Type LONG = Type.LONG_TYPE; + public static final Type BOOLEAN = Type.BOOLEAN_TYPE; + public static final Type CHAR = Type.CHAR_TYPE; + public static final Type SHORT = Type.SHORT_TYPE; + public static final Type BYTE = Type.BYTE_TYPE; + + // Wrappers + public static final Type BOOLEAN_WRAPPER = Type.getType(Boolean.class); + public static final Type INT_WRAPPER = Type.getType(Integer.class); + public static final Type LONG_WRAPPER = Type.getType(Long.class); + public static final Type DOUBLE_WRAPPER = Type.getType(Double.class); + public static final Type FLOAT_WRAPPER = Type.getType(Float.class); + public static final Type SHORT_WRAPPER = Type.getType(Short.class); + public static final Type BYTE_WRAPPER = Type.getType(Byte.class); + public static final Type CHAR_WRAPPER = Type.getType(Character.class); + + public static final Map PRIMITIVE_TO_WRAPPER = Map.of( + BOOLEAN, BOOLEAN_WRAPPER, + INT, INT_WRAPPER, + DOUBLE, DOUBLE_WRAPPER, + LONG, LONG_WRAPPER, + FLOAT, FLOAT_WRAPPER, + SHORT, SHORT_WRAPPER, + CHAR, CHAR_WRAPPER, + BYTE, BYTE_WRAPPER); + + public static final Map WRAPPER_TO_PRIMITIVE = + PRIMITIVE_TO_WRAPPER.entrySet() + .stream() + .collect(toMap(Map.Entry::getValue, Map.Entry::getKey)); + + /** + * Checks if passed type is a primitive. + * + * @param type type to check + * @return true if it is + */ + public static boolean isPrimitive(@NonNull Type type) { + return PRIMITIVE_TO_WRAPPER.containsKey(type); + } + + /** + * Checks if passed type is either boolean primitive or wrapper. + * + * @param type type to check + * @return true if it is + */ + public static boolean isBoolean(@NonNull Type type) { + return isOneOf(type, BOOLEAN, BOOLEAN_WRAPPER); + } + + /** + * Checks if passed type is one of numeric primitives or numeric wrappers. + * + * @param type type to check + * @return true if it is + */ + @NonNull + public static boolean isNumeric(@NonNull Type type) { + return isOneOf(type, + DOUBLE, DOUBLE_WRAPPER, + FLOAT, FLOAT_WRAPPER, + INT, INT_WRAPPER, + LONG, LONG_WRAPPER, + SHORT, SHORT_WRAPPER, + CHAR, CHAR_WRAPPER, + BYTE, BYTE_WRAPPER); + } + + /** + * If passed type is boxed type, returns responsive primitive, otherwise returns + * original passed type. + * + * @param type type to check + * @return unboxed type or original passed type + */ + @NonNull + public static Type toUnboxedIfNecessary(@NonNull Type type) { + if (WRAPPER_TO_PRIMITIVE.containsKey(type)) { + return WRAPPER_TO_PRIMITIVE.get(type); + } + return type; + } + + /** + * If passed type is primitive, returns responsive boxed type, otherwise returns + * original passed type. + * + * @param type type to check + * @return boxed type or original passed type + */ + @NonNull + public static Type toBoxedIfNecessary(@NonNull Type type) { + if (PRIMITIVE_TO_WRAPPER.containsKey(type)) { + return PRIMITIVE_TO_WRAPPER.get(type); + } + return type; + } + + /** + * For two passed types computes result numeric operation type. This method accepts + * both primitive and wrapper types, but returns only primitive type. + * + * @param leftOperandType left operand type + * @param rightOperandType right operand type + * @return numeric operation result type + * @throws ExpressionCompilationException if ony of the passed types is not a numeric type + */ + @NonNull + public static Type computeNumericOperationTargetType(@NonNull Type leftOperandType, + @NonNull Type rightOperandType) { + if (!isNumeric(leftOperandType) || !isNumeric(rightOperandType)) { + throw new ExpressionCompilationException("Numeric operation can only be applied to numeric types"); + } + + if (toUnboxedIfNecessary(leftOperandType).equals(DOUBLE) + || toUnboxedIfNecessary(rightOperandType).equals(DOUBLE)) { + return DOUBLE; + } else if (toUnboxedIfNecessary(leftOperandType).equals(FLOAT) + || toUnboxedIfNecessary(rightOperandType).equals(FLOAT)) { + return FLOAT; + } else if (toUnboxedIfNecessary(leftOperandType).equals(LONG) + || toUnboxedIfNecessary(rightOperandType).equals(LONG)) { + return LONG; + } else { + return INT; + } + } + + /** + * Utility method to check if passed type (first argument) is the same as any of + * compared types (second and following args). + * + * @param type type to check + * @param comparedTypes types against which checked types is compared + * @return true if checked type is amount compared types + */ + public static boolean isOneOf(Type type, Type... comparedTypes) { + for (Type comparedType: comparedTypes) { + if (type.equals(comparedType)) { + return true; + } + } + return false; + } +} diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/compilation/ExpressionVisitorContext.java b/core-processor/src/main/java/io/micronaut/expressions/parser/compilation/ExpressionVisitorContext.java new file mode 100644 index 00000000000..fefcc32d70b --- /dev/null +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/compilation/ExpressionVisitorContext.java @@ -0,0 +1,38 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.expressions.parser.compilation; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.NonNull; +import io.micronaut.expressions.context.ExpressionCompilationContext; +import io.micronaut.inject.visitor.VisitorContext; +import org.objectweb.asm.commons.GeneratorAdapter; + +/** + * Context class used for compiling expressions. + * + * @param compilationContext expression compilation context + * @param visitorContext visitor context + * @param methodVisitor method visitor for compiled expression class + * + * @author Sergey Gavrilov + * @since 4.0.0 + */ +@Internal +public record ExpressionVisitorContext(@NonNull ExpressionCompilationContext compilationContext, + @NonNull VisitorContext visitorContext, + @NonNull GeneratorAdapter methodVisitor) { +} diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/exception/ExpressionCompilationException.java b/core-processor/src/main/java/io/micronaut/expressions/parser/exception/ExpressionCompilationException.java new file mode 100644 index 00000000000..af8d08142cc --- /dev/null +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/exception/ExpressionCompilationException.java @@ -0,0 +1,32 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.expressions.parser.exception; + +import io.micronaut.core.annotation.Internal; + +/** + * Exception throws when problems with expression compilation occur. + * These usually include problems with type resolution. + * + * @author Sergey Gavrilov + * @since 4.0.0 + */ +@Internal +public class ExpressionCompilationException extends RuntimeException { + public ExpressionCompilationException(String message) { + super(message); + } +} diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/exception/ExpressionParsingException.java b/core-processor/src/main/java/io/micronaut/expressions/parser/exception/ExpressionParsingException.java new file mode 100644 index 00000000000..a5bb653dbd6 --- /dev/null +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/exception/ExpressionParsingException.java @@ -0,0 +1,31 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.expressions.parser.exception; + +import io.micronaut.core.annotation.Internal; + +/** + * Exception throws when problems with expression parsing occur. + * + * @author Sergey Gavrilov + * @since 4.0.0 + */ +@Internal +public class ExpressionParsingException extends RuntimeException { + public ExpressionParsingException(String message) { + super(message); + } +} diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/token/Token.java b/core-processor/src/main/java/io/micronaut/expressions/parser/token/Token.java new file mode 100644 index 00000000000..1e6a5b8de70 --- /dev/null +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/token/Token.java @@ -0,0 +1,30 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.expressions.parser.token; + +import io.micronaut.core.annotation.Internal; + +/** + * Parsed token with value and type. + * + * @param type token type + * @param value token string value + * + * @author Sergey Gavrilov + * @since 4.0.0 + */ +@Internal +public record Token(TokenType type, String value) { } diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/token/TokenType.java b/core-processor/src/main/java/io/micronaut/expressions/parser/token/TokenType.java new file mode 100644 index 00000000000..831aafca89f --- /dev/null +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/token/TokenType.java @@ -0,0 +1,90 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.expressions.parser.token; + +import io.micronaut.core.annotation.Internal; + +/** + * List of supported token types. + * + * @author Sergey Gavrilov + * @since 4.0.0 + */ +@Internal +public enum TokenType { + WHITESPACE, + IDENTIFIER, + TYPE_IDENTIFIER, + EXPRESSION_CONTEXT_REF, + DOT, + SAFE_NAV, + + ELVIS, + COMMA, + COLON, + L_PAREN, + R_PAREN, + L_CURLY, + R_CURLY, + L_SQUARE, + R_SQUARE, + QMARK, + NOT, + + // MATH OPERATORS + POW, + PLUS, + MINUS, + MUL, + DIV, + MOD, + INCREMENT, + DECREMENT, + + // LITERALS + DOUBLE, + FLOAT, + INT, + LONG, + STRING, + BOOL, + NULL, + + // LOGICAL OPERATORS + OR, + AND, + + // RELATIONAL OPERATORS + EQ, + NE, + GT, + GTE, + LT, + LTE, + INSTANCEOF, + MATCHES, + + EMPTY; + + public boolean isOneOf(TokenType... others) { + for (TokenType comparedType: others) { + if (comparedType == this) { + return true; + } + } + return false; + } +} diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/token/Tokenizer.java b/core-processor/src/main/java/io/micronaut/expressions/parser/token/Tokenizer.java new file mode 100644 index 00000000000..b7885e2ab18 --- /dev/null +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/token/Tokenizer.java @@ -0,0 +1,222 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.expressions.parser.token; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.Nullable; +import io.micronaut.core.util.CollectionUtils; +import io.micronaut.expressions.parser.exception.ExpressionParsingException; + +import java.util.List; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import static io.micronaut.expressions.parser.token.TokenType.AND; +import static io.micronaut.expressions.parser.token.TokenType.BOOL; +import static io.micronaut.expressions.parser.token.TokenType.COLON; +import static io.micronaut.expressions.parser.token.TokenType.COMMA; +import static io.micronaut.expressions.parser.token.TokenType.DECREMENT; +import static io.micronaut.expressions.parser.token.TokenType.DIV; +import static io.micronaut.expressions.parser.token.TokenType.DOT; +import static io.micronaut.expressions.parser.token.TokenType.DOUBLE; +import static io.micronaut.expressions.parser.token.TokenType.ELVIS; +import static io.micronaut.expressions.parser.token.TokenType.EMPTY; +import static io.micronaut.expressions.parser.token.TokenType.EQ; +import static io.micronaut.expressions.parser.token.TokenType.EXPRESSION_CONTEXT_REF; +import static io.micronaut.expressions.parser.token.TokenType.IDENTIFIER; +import static io.micronaut.expressions.parser.token.TokenType.FLOAT; +import static io.micronaut.expressions.parser.token.TokenType.GT; +import static io.micronaut.expressions.parser.token.TokenType.GTE; +import static io.micronaut.expressions.parser.token.TokenType.TYPE_IDENTIFIER; +import static io.micronaut.expressions.parser.token.TokenType.INCREMENT; +import static io.micronaut.expressions.parser.token.TokenType.INSTANCEOF; +import static io.micronaut.expressions.parser.token.TokenType.INT; +import static io.micronaut.expressions.parser.token.TokenType.LONG; +import static io.micronaut.expressions.parser.token.TokenType.LT; +import static io.micronaut.expressions.parser.token.TokenType.LTE; +import static io.micronaut.expressions.parser.token.TokenType.L_CURLY; +import static io.micronaut.expressions.parser.token.TokenType.L_PAREN; +import static io.micronaut.expressions.parser.token.TokenType.L_SQUARE; +import static io.micronaut.expressions.parser.token.TokenType.MATCHES; +import static io.micronaut.expressions.parser.token.TokenType.MINUS; +import static io.micronaut.expressions.parser.token.TokenType.MOD; +import static io.micronaut.expressions.parser.token.TokenType.MUL; +import static io.micronaut.expressions.parser.token.TokenType.NE; +import static io.micronaut.expressions.parser.token.TokenType.NOT; +import static io.micronaut.expressions.parser.token.TokenType.NULL; +import static io.micronaut.expressions.parser.token.TokenType.OR; +import static io.micronaut.expressions.parser.token.TokenType.PLUS; +import static io.micronaut.expressions.parser.token.TokenType.POW; +import static io.micronaut.expressions.parser.token.TokenType.QMARK; +import static io.micronaut.expressions.parser.token.TokenType.R_CURLY; +import static io.micronaut.expressions.parser.token.TokenType.R_PAREN; +import static io.micronaut.expressions.parser.token.TokenType.R_SQUARE; +import static io.micronaut.expressions.parser.token.TokenType.SAFE_NAV; +import static io.micronaut.expressions.parser.token.TokenType.STRING; +import static io.micronaut.expressions.parser.token.TokenType.WHITESPACE; + +/** + * Tokenizer for parsing evaluated expressions. + * + * @author Sergey Gavrilov + * @since 4.0.0 + */ +@Internal +public final class Tokenizer { + + private static final Map TOKENS = CollectionUtils.mapOf( + // WHITESPACES + "^\\s+", WHITESPACE, + + // BRACES + "^\\{", L_CURLY, + "^}", R_CURLY, + "^\\[", L_SQUARE, + "^]", R_SQUARE, + "^\\(", L_PAREN, + "^\\)", R_PAREN, + + // KEYWORDS + "^instanceof\\b", INSTANCEOF, + "^matches\\b", MATCHES, + "^empty\\b", EMPTY, + + // LITERALS + "^null\\b", NULL, // NULL + "^(true|false)\\b", BOOL, // BOOLEAN + "^'[^']*'", STRING, // STRING + // FLOAT + "^\\d+\\.\\d*((e|E)(\\+|-)?\\d+)?(f|F)", FLOAT, + "^\\.\\d+((e|E)(\\+|-)?\\d+)?(f|F)", FLOAT, + "^\\d+((e|E)(\\+|-)?\\d+)?(f|F)", FLOAT, + // DOUBLE + "^\\d+\\.\\d*((e|E)(\\+|-)?\\d+)?(d|D)?", DOUBLE, + "^\\.\\d+((e|E)(\\+|-)?\\d+)?(d|D)?", DOUBLE, + "^\\d+((e|E)(\\+|-)?\\d+)(d|D)?", DOUBLE, + "^\\d+((e|E)(\\+|-)?\\d+)?(d|D)", DOUBLE, + // LONG + "^0(x|X)[0-9a-fA-F]+(l|L)", LONG, + "^\\d+(l|L)", LONG, + // INT + "^0(x|X)[0-9a-fA-F]+", INT, + "^\\d+", INT, + + // SYMBOLS + "^#", EXPRESSION_CONTEXT_REF, + "^\\?\\.", SAFE_NAV, + "^\\?\\:", ELVIS, + "^\\?", QMARK, + "^\\.", DOT, + "^,", COMMA, + "^\\:", COLON, + + // RELATIONAL OPERATORS + "^==", EQ, + "^!=", NE, + "^>=", GTE, + "^>", GT, + "^<=", LTE, + "^<", LT, + + // LOGICAL OPERATORS + "^!", NOT, + "^not\\b", NOT, + "^&&", AND, + "^and\\b", AND, + "^\\|\\|", OR, + "^or\\b", OR, + + // MATH OPERATORS + "^\\+\\+", INCREMENT, + "^\\+", PLUS, + "^\\-\\-", DECREMENT, + "^\\-", MINUS, + "^\\*", MUL, + "^/", DIV, + "^div\\b", DIV, + "^%", MOD, + "^mod\\b", MOD, + "^\\^", POW, + + // IDENTIFIERS + "^T\\(", TYPE_IDENTIFIER, + "\\w+", IDENTIFIER); + + private static final List PATTERNS = + TOKENS.entrySet() + .stream() + .map(entry -> TokenPattern.of(entry.getKey(), entry.getValue())) + .toList(); + + private final int length; + private final String expression; + + private int cursor; + private String remaining; + + public Tokenizer(String expression) { + this.expression = expression; + this.remaining = expression; + this.cursor = 0; + this.length = expression.length(); + } + + @Nullable + public Token getNextToken() { + if (!hasMoreTokens()) { + return null; + } + + remaining = expression.substring(cursor); + for (TokenPattern pattern: PATTERNS) { + Token token = pattern.matches(remaining); + if (token == null) { + continue; + } + + cursor += token.value().length(); + + if (token.type() == WHITESPACE) { + return getNextToken(); + } + + return token; + } + + throw new ExpressionParsingException("Unexpected token: " + remaining); + } + + private boolean hasMoreTokens() { + return cursor < length; + } + + private record TokenPattern(Pattern pattern, TokenType tokenType) { + public static TokenPattern of(String pattern, TokenType tokenType) { + return new TokenPattern(Pattern.compile(pattern), tokenType); + } + + @Nullable + public Token matches(String value) { + Matcher matcher = pattern.matcher(value); + if (!matcher.find()) { + return null; + } + + return new Token(tokenType, matcher.group()); + } + } +} diff --git a/core-processor/src/main/java/io/micronaut/expressions/util/EvaluatedExpressionsUtils.java b/core-processor/src/main/java/io/micronaut/expressions/util/EvaluatedExpressionsUtils.java new file mode 100644 index 00000000000..5a7281896e6 --- /dev/null +++ b/core-processor/src/main/java/io/micronaut/expressions/util/EvaluatedExpressionsUtils.java @@ -0,0 +1,85 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.expressions.util; + +import io.micronaut.core.expressions.EvaluatedExpressionReference; +import io.micronaut.core.annotation.AnnotationMetadata; +import io.micronaut.core.annotation.AnnotationValue; +import io.micronaut.core.annotation.Internal; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.stream.Stream; + +/** + * Utility class for working with annotation metadata containing + * evaluated expressions. + * + * @author Sergey Gavrilov + * @since 4.0.0 + */ +@Internal +public final class EvaluatedExpressionsUtils { + + /** + * Finds evaluated expression references in provided annotation metadata, + * including nested annotation values. + * + * @param annotationMetadata metadata to search references in + * @return collection of expression references + */ + public static Collection findEvaluatedExpressionReferences(AnnotationMetadata annotationMetadata) { + return Stream.concat( + annotationMetadata.getAnnotationNames().stream(), + annotationMetadata.getStereotypeAnnotationNames().stream()) + .map(annotationMetadata::getAnnotation) + .flatMap(annotation -> getNestedAnnotationValues(annotation).stream()) + .flatMap(av -> av.getValues().values().stream()) + .filter(EvaluatedExpressionReference.class::isInstance) + .map(EvaluatedExpressionReference.class::cast) + .distinct() + .toList(); + } + + private static Collection> getNestedAnnotationValues(Object value) { + List> result = new ArrayList<>(); + if (value instanceof AnnotationValue annotationValue) { + for (Object nestedValue: annotationValue.getValues().values()) { + result.addAll(getNestedAnnotationValues(nestedValue)); + } + result.add(annotationValue); + } else { + Iterable nestedValues = null; + if (value instanceof Iterable iterable) { + nestedValues = iterable; + } else if (value.getClass().isArray()) { + nestedValues = Arrays.asList(value); + } + + if (nestedValues != null) { + for (Object nextValue: nestedValues) { + if (nextValue instanceof AnnotationValue) { + result.addAll(getNestedAnnotationValues(nextValue)); + } + } + } + } + + return result; + } +} diff --git a/core-processor/src/main/java/io/micronaut/inject/annotation/AbstractAnnotationMetadataBuilder.java b/core-processor/src/main/java/io/micronaut/inject/annotation/AbstractAnnotationMetadataBuilder.java index 6af26a577be..bf91cd3479d 100644 --- a/core-processor/src/main/java/io/micronaut/inject/annotation/AbstractAnnotationMetadataBuilder.java +++ b/core-processor/src/main/java/io/micronaut/inject/annotation/AbstractAnnotationMetadataBuilder.java @@ -25,6 +25,7 @@ import io.micronaut.core.annotation.AnnotationMetadataDelegate; import io.micronaut.core.annotation.AnnotationUtil; import io.micronaut.core.annotation.AnnotationValue; +import io.micronaut.core.expressions.EvaluatedExpressionReference; import io.micronaut.core.annotation.InstantiatedMember; import io.micronaut.core.annotation.Internal; import io.micronaut.core.annotation.NonNull; @@ -38,6 +39,7 @@ import java.lang.annotation.Annotation; import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; @@ -53,6 +55,9 @@ import java.util.function.Predicate; import java.util.stream.Stream; +import static io.micronaut.core.expressions.EvaluatedExpressionReference.EXPR_SUFFIX; +import static io.micronaut.expressions.EvaluatedExpressionConstants.EXPRESSION_PATTERN; + /** * An abstract implementation that builds {@link AnnotationMetadata}. * @@ -433,11 +438,12 @@ protected AnnotatedElementValidator getElementValidator() { * * @param originatingElement The originating element * @param member The member + * @param annotationName The annotation name * @param memberName The member name * @param annotationValue The value * @return The object */ - protected abstract Object readAnnotationValue(T originatingElement, T member, String memberName, Object annotationValue); + protected abstract Object readAnnotationValue(T originatingElement, T member, String annotationName, String memberName, Object annotationValue); /** * Read the raw default annotation values from the given annotation. @@ -544,6 +550,46 @@ protected AnnotationValue readNestedAnnotationValue(T annotationElement, A an */ protected abstract Optional getAnnotationMirror(String annotationName); + /** + * Detect evaluated expression in annotation value. + * + * @param value - Annotation value + * @return if value contains evaluated expression + */ + protected boolean isEvaluatedExpression(@Nullable Object value) { + return (value instanceof String str && str.matches(EXPRESSION_PATTERN)) + || (value instanceof String[] strArray && + Arrays.stream(strArray).anyMatch(this::isEvaluatedExpression)); + } + + /** + * Wraps original annotation value to make it processable at later stages. + * + * @param originatingElement originating annotated element + * @param annotationName annotation name + * @param memberName annotation member name + * @param initialAnnotationValue original annotation value + * @return expression reference + */ + @NonNull + protected Object buildEvaluatedExpressionReference(@NonNull T originatingElement, + @NonNull String annotationName, + @NonNull String memberName, + @NonNull Object initialAnnotationValue) { + String originatingClassName = getOriginatingClassName(originatingElement); + + String packageName = NameUtils.getPackageName(originatingClassName); + String simpleClassName = NameUtils.getSimpleName(originatingClassName); + String exprClassName = "%s.$%s%s".formatted(packageName, simpleClassName, EXPR_SUFFIX); + + Integer expressionIndex = EvaluatedExpressionReference.nextIndex(exprClassName); + + return new EvaluatedExpressionReference(initialAnnotationValue, annotationName, memberName, exprClassName + expressionIndex); + } + + @NonNull + protected abstract String getOriginatingClassName(@NonNull T orginatingElement); + /** * Get the annotation member. * @@ -783,7 +829,7 @@ private ProcessedAnnotation createAnnotationValue(@NonNull T originatingElement, } if (isInstantiatedMember) { final String memberName = getAnnotationMemberName(member); - final Object rawValue = readAnnotationValue(originatingElement, member, memberName, annotationValue); + final Object rawValue = readAnnotationValue(originatingElement, member, annotationName, memberName, annotationValue); if (rawValue instanceof AnnotationClassValue annotationClassValue) { annotationValues.put(memberName, new AnnotationClassValue<>(annotationClassValue.getName(), true)); } diff --git a/core-processor/src/main/java/io/micronaut/inject/annotation/AnnotationMetadataWriter.java b/core-processor/src/main/java/io/micronaut/inject/annotation/AnnotationMetadataWriter.java index e97af9966e8..de184a9aaaf 100644 --- a/core-processor/src/main/java/io/micronaut/inject/annotation/AnnotationMetadataWriter.java +++ b/core-processor/src/main/java/io/micronaut/inject/annotation/AnnotationMetadataWriter.java @@ -19,12 +19,14 @@ import io.micronaut.core.annotation.AnnotationMetadata; import io.micronaut.core.annotation.AnnotationMetadataDelegate; import io.micronaut.core.annotation.AnnotationUtil; +import io.micronaut.core.expressions.EvaluatedExpressionReference; import io.micronaut.core.annotation.Internal; import io.micronaut.core.annotation.UsedByGeneratedCode; import io.micronaut.core.reflect.ReflectionUtils; import io.micronaut.core.util.ArrayUtils; import io.micronaut.core.util.CollectionUtils; import io.micronaut.inject.ast.ClassElement; +import io.micronaut.context.expressions.AbstractEvaluatedExpression; import io.micronaut.inject.writer.AbstractAnnotationMetadataWriter; import io.micronaut.inject.writer.AbstractClassFileWriter; import io.micronaut.inject.writer.ClassGenerationException; @@ -64,7 +66,7 @@ public class AnnotationMetadataWriter extends AbstractClassFileWriter { private static final Type TYPE_DEFAULT_ANNOTATION_METADATA_HIERARCHY = Type.getType(AnnotationMetadataHierarchy.class); private static final Type TYPE_ANNOTATION_CLASS_VALUE = Type.getType(AnnotationClassValue.class); - private static final org.objectweb.asm.commons.Method METHOD_LIST_OF = org.objectweb.asm.commons.Method.getMethod( + private static final org.objectweb.asm.commons.Method METHOD_LIST_OF = Method.getMethod( ReflectionUtils.getRequiredInternalMethod( AnnotationUtil.class, "internListOf", @@ -72,7 +74,7 @@ public class AnnotationMetadataWriter extends AbstractClassFileWriter { ) ); - private static final org.objectweb.asm.commons.Method METHOD_REGISTER_ANNOTATION_DEFAULTS = org.objectweb.asm.commons.Method.getMethod( + private static final org.objectweb.asm.commons.Method METHOD_REGISTER_ANNOTATION_DEFAULTS = Method.getMethod( ReflectionUtils.getRequiredInternalMethod( DefaultAnnotationMetadata.class, "registerAnnotationDefaults", @@ -81,7 +83,7 @@ public class AnnotationMetadataWriter extends AbstractClassFileWriter { ) ); - private static final org.objectweb.asm.commons.Method METHOD_REGISTER_ANNOTATION_TYPE = org.objectweb.asm.commons.Method.getMethod( + private static final org.objectweb.asm.commons.Method METHOD_REGISTER_ANNOTATION_TYPE = Method.getMethod( ReflectionUtils.getRequiredInternalMethod( DefaultAnnotationMetadata.class, "registerAnnotationType", @@ -89,7 +91,7 @@ public class AnnotationMetadataWriter extends AbstractClassFileWriter { ) ); - private static final org.objectweb.asm.commons.Method METHOD_REGISTER_REPEATABLE_ANNOTATIONS = org.objectweb.asm.commons.Method.getMethod( + private static final org.objectweb.asm.commons.Method METHOD_REGISTER_REPEATABLE_ANNOTATIONS = Method.getMethod( ReflectionUtils.getRequiredInternalMethod( DefaultAnnotationMetadata.class, "registerRepeatableAnnotations", @@ -97,7 +99,7 @@ public class AnnotationMetadataWriter extends AbstractClassFileWriter { ) ); - private static final org.objectweb.asm.commons.Method METHOD_GET_DEFAULT_VALUES = org.objectweb.asm.commons.Method.getMethod( + private static final org.objectweb.asm.commons.Method METHOD_GET_DEFAULT_VALUES = Method.getMethod( ReflectionUtils.getRequiredInternalMethod( AnnotationMetadataSupport.class, "getDefaultValues", @@ -105,7 +107,7 @@ public class AnnotationMetadataWriter extends AbstractClassFileWriter { ) ); - private static final org.objectweb.asm.commons.Method CONSTRUCTOR_ANNOTATION_METADATA = org.objectweb.asm.commons.Method.getMethod( + private static final org.objectweb.asm.commons.Method CONSTRUCTOR_ANNOTATION_METADATA = Method.getMethod( ReflectionUtils.getRequiredInternalConstructor( DefaultAnnotationMetadata.class, Map.class, @@ -113,6 +115,7 @@ public class AnnotationMetadataWriter extends AbstractClassFileWriter { Map.class, Map.class, Map.class, + boolean.class, boolean.class ) ); @@ -154,6 +157,12 @@ public class AnnotationMetadataWriter extends AbstractClassFileWriter { ) ); + private static final org.objectweb.asm.commons.Method CONSTRUCTOR_CONTEXT_EVALUATED_EXPRESSION = org.objectweb.asm.commons.Method.getMethod( + ReflectionUtils.getRequiredInternalConstructor( + AbstractEvaluatedExpression.class, + Object.class + )); + private static final Type ANNOTATION_UTIL_TYPE = Type.getType(AnnotationUtil.class); private static final Type LIST_TYPE = Type.getType(List.class); private static final String EMPTY_LIST = "EMPTY_LIST"; @@ -481,6 +490,8 @@ private static void instantiateInternal( pushStringMapOf(generatorAdapter, annotationsByStereotype, false, Collections.emptyList(), list -> pushListOfString(generatorAdapter, list)); // 6th argument: has property expressions generatorAdapter.push(annotationMetadata.hasPropertyExpressions()); + // 7th argument: has evaluated expressions + generatorAdapter.push(annotationMetadata.hasEvaluatedExpressions()); // invoke the constructor generatorAdapter.invokeConstructor(TYPE_DEFAULT_ANNOTATION_METADATA, CONSTRUCTOR_ANNOTATION_METADATA); @@ -669,6 +680,28 @@ private static void pushValue(Type declaringType, ClassVisitor declaringClassWri methodVisitor.loadLocal(defaultIndex); } methodVisitor.invokeConstructor(annotationValueType, CONSTRUCTOR_ANNOTATION_VALUE_AND_MAP); + } else if (value instanceof EvaluatedExpressionReference expressionReference) { + Type type = Type.getType(getTypeDescriptor(expressionReference.expressionClassName())); + + methodVisitor.visitTypeInsn(NEW, type.getInternalName()); + methodVisitor.visitInsn(DUP); + + Object annotationValue = expressionReference.annotationValue(); + if (annotationValue instanceof String str) { + methodVisitor.push(str); + } else if (annotationValue instanceof String[] strings) { + int len = Array.getLength(strings); + pushNewArray(methodVisitor, String.class, len); + for (int i = 0; i < len; i++) { + final Object v = Array.get(strings, i); + pushStoreInArray(methodVisitor, Type.getType(String.class), i, len, + () -> pushValue(declaringType, declaringClassWriter, methodVisitor, v, + defaultsStorage, loadTypeMethods, false)); + } + } + + methodVisitor.invokeConstructor(type, CONSTRUCTOR_CONTEXT_EVALUATED_EXPRESSION); + } else { methodVisitor.visitInsn(ACONST_NULL); } diff --git a/core-processor/src/main/java/io/micronaut/inject/ast/MethodElement.java b/core-processor/src/main/java/io/micronaut/inject/ast/MethodElement.java index df3d078dbde..12c1befa472 100644 --- a/core-processor/src/main/java/io/micronaut/inject/ast/MethodElement.java +++ b/core-processor/src/main/java/io/micronaut/inject/ast/MethodElement.java @@ -227,6 +227,15 @@ default boolean isDefault() { return false; } + /** + * If method has varargs parameter. + * @return True if it does + * @since 4.0.0 + */ + default boolean isVarArgs() { + return false; + } + /** * The generic return type of the method. * diff --git a/core-processor/src/main/java/io/micronaut/inject/visitor/VisitorContext.java b/core-processor/src/main/java/io/micronaut/inject/visitor/VisitorContext.java index dca9b75d233..f8a3122c960 100644 --- a/core-processor/src/main/java/io/micronaut/inject/visitor/VisitorContext.java +++ b/core-processor/src/main/java/io/micronaut/inject/visitor/VisitorContext.java @@ -20,6 +20,7 @@ import io.micronaut.core.annotation.NonNull; import io.micronaut.core.annotation.Nullable; import io.micronaut.core.convert.value.MutableConvertibleValues; +import io.micronaut.expressions.context.ExpressionCompilationContextFactory; import io.micronaut.inject.annotation.AbstractAnnotationMetadataBuilder; import io.micronaut.inject.ast.ClassElement; import io.micronaut.inject.ast.Element; @@ -64,11 +65,19 @@ public interface VisitorContext extends MutableConvertibleValues, ClassW * Gets the element annotation metadata factory. * * @return The element annotation metadata factory - * @see 4.0.0 + * @since 4.0.0 */ @NonNull ElementAnnotationMetadataFactory getElementAnnotationMetadataFactory(); + /** + * @return The expression compilation context factory. + * @since 4.0.0 + */ + @Experimental + @NonNull + ExpressionCompilationContextFactory getExpressionCompilationContextFactory(); + /** * Gets the annotation metadata builder. * diff --git a/core-processor/src/main/java/io/micronaut/inject/writer/BeanDefinitionVisitor.java b/core-processor/src/main/java/io/micronaut/inject/writer/BeanDefinitionVisitor.java index e1e08cf1824..4244eaca666 100644 --- a/core-processor/src/main/java/io/micronaut/inject/writer/BeanDefinitionVisitor.java +++ b/core-processor/src/main/java/io/micronaut/inject/writer/BeanDefinitionVisitor.java @@ -17,6 +17,7 @@ import io.micronaut.core.annotation.AnnotationMetadata; import io.micronaut.core.util.Toggleable; +import io.micronaut.expressions.context.ExpressionWithContext; import io.micronaut.inject.BeanDefinition; import io.micronaut.inject.ast.ClassElement; import io.micronaut.inject.ast.Element; @@ -35,6 +36,7 @@ import java.io.IOException; import java.util.Map; import java.util.Optional; +import java.util.Set; /** * Interface for {@link BeanDefinitionVisitor} implementations such as {@link BeanDefinitionWriter}. @@ -338,6 +340,13 @@ void visitFieldValue(TypedElement declaringType, */ AnnotationMetadata getAnnotationMetadata(); + /** + * @return The evaluated expressions metadata + * @since 4.0.0 + */ + @NonNull + Set getEvaluatedExpressions(); + /** * Begin defining a configuration builder. * diff --git a/core-processor/src/main/java/io/micronaut/inject/writer/BeanDefinitionWriter.java b/core-processor/src/main/java/io/micronaut/inject/writer/BeanDefinitionWriter.java index 80a60b6a669..64d6b3185ef 100644 --- a/core-processor/src/main/java/io/micronaut/inject/writer/BeanDefinitionWriter.java +++ b/core-processor/src/main/java/io/micronaut/inject/writer/BeanDefinitionWriter.java @@ -41,6 +41,7 @@ import io.micronaut.context.annotation.Value; import io.micronaut.context.env.ConfigurationPath; import io.micronaut.core.annotation.AccessorsStyle; +import io.micronaut.core.expressions.EvaluatedExpressionReference; import io.micronaut.core.annotation.AnnotationMetadata; import io.micronaut.core.annotation.AnnotationMetadataProvider; import io.micronaut.core.annotation.AnnotationUtil; @@ -63,6 +64,10 @@ import io.micronaut.core.util.CollectionUtils; import io.micronaut.core.util.StringUtils; import io.micronaut.core.util.Toggleable; +import io.micronaut.expressions.context.ExpressionCompilationContext; +import io.micronaut.expressions.context.ExpressionWithContext; +import io.micronaut.expressions.context.DefaultExpressionCompilationContextFactory; +import io.micronaut.expressions.util.EvaluatedExpressionsUtils; import io.micronaut.inject.AdvisedBeanType; import io.micronaut.inject.BeanDefinition; import io.micronaut.inject.DisposableBeanDefinition; @@ -73,6 +78,7 @@ import io.micronaut.inject.ParametrizedInstantiatableBeanDefinition; import io.micronaut.inject.ProxyBeanDefinition; import io.micronaut.inject.ValidatedBeanDefinition; +import io.micronaut.inject.annotation.AbstractAnnotationMetadataBuilder; import io.micronaut.inject.annotation.AnnotationMetadataHierarchy; import io.micronaut.inject.annotation.AnnotationMetadataWriter; import io.micronaut.inject.annotation.MutableAnnotationMetadata; @@ -253,6 +259,12 @@ public class BeanDefinitionWriter extends AbstractClassFileWriter implements Bea int.class, String.class); + private static final Method GET_EVALUATED_EXPRESSION_VALUE_FOR_METHOD_ARGUMENT = ReflectionUtils.getRequiredInternalMethod( + AbstractInitializableBeanDefinition.class, + "getEvaluatedExpressionValueForMethodArgument", + int.class, + int.class); + private static final Method GET_BEAN_FOR_SETTER = ReflectionUtils.getRequiredInternalMethod( AbstractInitializableBeanDefinition.class, "getBeanForSetter", @@ -308,6 +320,11 @@ public class BeanDefinitionWriter extends AbstractClassFileWriter implements Bea int.class, String.class); + private static final Method GET_EVALUATED_EXPRESSION_VALUE_FOR_CONSTRUCTOR_ARGUMENT = ReflectionUtils.getRequiredInternalMethod( + AbstractInitializableBeanDefinition.class, + "getEvaluatedExpressionValueForConstructorArgument", + int.class); + private static final Method GET_PROPERTY_VALUE_FOR_FIELD = ReflectionUtils.getRequiredInternalMethod( AbstractInitializableBeanDefinition.class, "getPropertyValueForField", @@ -564,6 +581,9 @@ public class BeanDefinitionWriter extends AbstractClassFileWriter implements Bea private final Map isLifeCycleCache = new HashMap<>(2); private ExecutableMethodsDefinitionWriter executableMethodsDefinitionWriter; + private final Collection evaluatedExpressions = new ArrayList<>(2); + private final DefaultExpressionCompilationContextFactory expressionCompilationContextFactory; + private Object constructor; // MethodElement or FieldElement private boolean disabled = false; @@ -698,6 +718,8 @@ public BeanDefinitionWriter(Element beanProducingElement, this.isConfigurationProperties = isConfigurationProperties(annotationMetadata); validateExposedTypes(annotationMetadata, visitorContext); this.visitorContext = visitorContext; + this.expressionCompilationContextFactory = new DefaultExpressionCompilationContextFactory(visitorContext); + processEvaluatedExpressions(this.annotationMetadata); beanTypeInnerClasses = beanTypeElement.getEnclosedElements(ElementQuery.of(ClassElement.class)) .stream() @@ -969,6 +991,11 @@ public void visitBeanDefinitionConstructor(MethodElement constructor, // now implement the inject method visitInjectMethodDefinition(); + + processEvaluatedExpressions(constructor.getAnnotationMetadata()); + for (ParameterElement parameter: constructor.getParameters()) { + processEvaluatedExpressions(parameter.getAnnotationMetadata()); + } } } @@ -1529,6 +1556,7 @@ public void visitMethodInjectionPoint(TypedElement declaringType, boolean requiresReflection, VisitorContext visitorContext) { MethodVisitData methodVisitData = new MethodVisitData(declaringType, methodElement, requiresReflection, methodElement.getAnnotationMetadata()); + processEvaluatedExpressions(methodElement.getAnnotationMetadata()); methodInjectionPoints.add(methodVisitData); allMethodVisits.add(methodVisitData); visitMethodInjectionPointInternal(methodVisitData, injectMethodVisitor, injectInstanceLocalVarIndex); @@ -1562,7 +1590,7 @@ public int visitExecutableMethod(TypedElement declaringType, String interceptedProxyBridgeMethodName) { if (executableMethodsDefinitionWriter == null) { - executableMethodsDefinitionWriter = new ExecutableMethodsDefinitionWriter(annotationMetadata, beanDefinitionName, getBeanDefinitionReferenceClassName(), originatingElements); + executableMethodsDefinitionWriter = new ExecutableMethodsDefinitionWriter(visitorContext, annotationMetadata, beanDefinitionName, getBeanDefinitionReferenceClassName(), originatingElements); } return executableMethodsDefinitionWriter.visitExecutableMethod(declaringType, methodElement, interceptedProxyClassName, interceptedProxyBridgeMethodName); } @@ -1589,6 +1617,16 @@ public AnnotationMetadata getAnnotationMetadata() { return this.annotationMetadata; } + @Override + public Set getEvaluatedExpressions() { + return Stream.concat( + evaluatedExpressions.stream(), + executableMethodsDefinitionWriter != null + ? executableMethodsDefinitionWriter.getEvaluatedExpressions().stream() + : Stream.empty()) + .collect(Collectors.toSet()); + } + @Override public void visitConfigBuilderField( ClassElement type, @@ -2142,6 +2180,7 @@ private void visitFieldInjectionPointInternal( Method methodToInvoke, boolean isArray, boolean requiresGenericType) { + processEvaluatedExpressions(annotationMetadata); autoApplyNamedIfPresent(fieldElement, annotationMetadata); @@ -2400,6 +2439,7 @@ private void visitMethodInjectionPointInternal(MethodVisitData methodVisitData, Type declaringTypeRef = JavaModelUtils.getTypeReference(declaringType); boolean hasInjectScope = false; for (ParameterElement value : argumentTypes) { + processEvaluatedExpressions(value.getAnnotationMetadata()); if (value.hasDeclaredAnnotation(InjectScope.class)) { hasInjectScope = true; } @@ -2481,9 +2521,13 @@ private void pushMethodParameterValue(GeneratorAdapter injectMethodVisitor, int if (property.isPresent()) { pushInvokeGetPropertyValueForMethod(injectMethodVisitor, i, entry, property.get()); } else { - Optional valueValue = entry.getAnnotationMetadata().stringValue(Value.class); - if (valueValue.isPresent()) { - pushInvokeGetPropertyPlaceholderValueForMethod(injectMethodVisitor, i, entry, valueValue.get()); + if (entry.getAnnotationMetadata().getValue(Value.class, EvaluatedExpressionReference.class).isPresent()) { + pushInvokeGetEvaluatedExpressionValueForMethodArgument(injectMethodVisitor, i, entry); + } else { + Optional valueValue = entry.getAnnotationMetadata().stringValue(Value.class); + if (valueValue.isPresent()) { + pushInvokeGetPropertyPlaceholderValueForMethod(injectMethodVisitor, i, entry, valueValue.get()); + } } } return; @@ -2563,6 +2607,19 @@ private void pushInvokeGetPropertyValueForMethod(GeneratorAdapter injectMethodVi pushCastToType(injectMethodVisitor, entry); } + private void pushInvokeGetEvaluatedExpressionValueForMethodArgument(GeneratorAdapter injectMethodVisitor, int i, ParameterElement entry) { + // load 'this' + injectMethodVisitor.loadThis(); + // 1st argument the method index + injectMethodVisitor.push(currentMethodIndex); + // 2nd argument the argument index + injectMethodVisitor.push(i); + + pushInvokeMethodOnSuperClass(injectMethodVisitor, GET_EVALUATED_EXPRESSION_VALUE_FOR_METHOD_ARGUMENT); + // cast the return value to the correct type + pushCastToType(injectMethodVisitor, entry); + } + private void pushInvokeGetPropertyPlaceholderValueForMethod(GeneratorAdapter injectMethodVisitor, int i, ParameterElement entry, String value) { // load 'this' injectMethodVisitor.loadThis(); @@ -3096,6 +3153,10 @@ private void visitBuildFactoryMethodDefinition( ClassElement factoryClass, Element factoryElement, ParameterElement... parameters) { if (buildMethodVisitor == null) { + processEvaluatedExpressions(factoryElement.getAnnotationMetadata()); + for (ParameterElement parameterElement: parameters) { + processEvaluatedExpressions(parameterElement.getAnnotationMetadata()); + } List parameterList = Arrays.asList(parameters); boolean isParametrized = isParametrized(parameters); @@ -3679,8 +3740,12 @@ private void pushConstructorArgument(GeneratorAdapter buildMethodVisitor, if (property.isPresent()) { pushInvokeGetPropertyValueForConstructor(buildMethodVisitor, index, argumentType, property.get()); } else { - Optional valueValue = argumentType.stringValue(Value.class); - valueValue.ifPresent(s -> pushInvokeGetPropertyPlaceholderValueForConstructor(buildMethodVisitor, index, argumentType, s)); + if (argumentType.getValue(Value.class, EvaluatedExpressionReference.class).isPresent()) { + pushInvokeGetEvaluatedExpressionValueForConstructorArgument(buildMethodVisitor, index, argumentType); + } else { + Optional valueValue = argumentType.stringValue(Value.class); + valueValue.ifPresent(s -> pushInvokeGetPropertyPlaceholderValueForConstructor(buildMethodVisitor, index, argumentType, s)); + } } return; } else { @@ -3779,6 +3844,17 @@ private void pushInvokeGetPropertyPlaceholderValueForConstructor(GeneratorAdapte pushCastToType(injectMethodVisitor, entry); } + private void pushInvokeGetEvaluatedExpressionValueForConstructorArgument(GeneratorAdapter injectMethodVisitor, int i, ParameterElement entry) { + // load 'this' + injectMethodVisitor.loadThis(); + // 2nd argument the argument index + injectMethodVisitor.push(i); + + pushInvokeMethodOnSuperClass(injectMethodVisitor, GET_EVALUATED_EXPRESSION_VALUE_FOR_CONSTRUCTOR_ARGUMENT); + // cast the return value to the correct type + pushCastToType(injectMethodVisitor, entry); + } + private void resolveConstructorArgumentGenericType(GeneratorAdapter visitor, ClassElement type, int argumentIndex) { if (!resolveArgumentGenericType(visitor, type)) { resolveConstructorArgument(visitor, argumentIndex); @@ -4496,6 +4572,22 @@ private void populateBeanTypes(Set processedTypes, Set bea } } + private void processEvaluatedExpressions(AnnotationMetadata annotationMetadata) { + if (annotationMetadata instanceof AnnotationMetadataHierarchy) { + annotationMetadata = annotationMetadata.getDeclaredMetadata(); + } + + Collection expressionReferences = + EvaluatedExpressionsUtils.findEvaluatedExpressionReferences(annotationMetadata); + + expressionReferences.stream() + .map(expressionReference -> { + ExpressionCompilationContext evaluationContext = expressionCompilationContextFactory.buildContext(expressionReference); + return new ExpressionWithContext(expressionReference, evaluationContext); + }) + .forEach(evaluatedExpressions::add); + } + @Override public Optional getScope() { return annotationMetadata.getAnnotationNameByStereotype(AnnotationUtil.SCOPE); @@ -4542,6 +4634,17 @@ public boolean isProxiedBean() { return proxiedBean; } + + /** + * Finish any work writing beans. + */ + @Internal + public static void finish() { + AbstractAnnotationMetadataBuilder.clearMutated(); + AbstractAnnotationMetadataBuilder.clearCaches(); + DefaultExpressionCompilationContextFactory.reset(); + } + @Internal private static final class AnnotationVisitData { final TypedElement memberBeanType; diff --git a/core-processor/src/main/java/io/micronaut/inject/writer/ExecutableMethodsDefinitionWriter.java b/core-processor/src/main/java/io/micronaut/inject/writer/ExecutableMethodsDefinitionWriter.java index 1cd18628e23..52852719aba 100644 --- a/core-processor/src/main/java/io/micronaut/inject/writer/ExecutableMethodsDefinitionWriter.java +++ b/core-processor/src/main/java/io/micronaut/inject/writer/ExecutableMethodsDefinitionWriter.java @@ -18,17 +18,24 @@ import io.micronaut.context.AbstractExecutableMethodsDefinition; import io.micronaut.core.annotation.AnnotationMetadata; import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.NonNull; import io.micronaut.core.reflect.ReflectionUtils; import io.micronaut.core.type.Argument; +import io.micronaut.expressions.context.DefaultExpressionCompilationContextFactory; +import io.micronaut.expressions.context.ExpressionCompilationContext; +import io.micronaut.expressions.context.ExpressionWithContext; +import io.micronaut.expressions.util.EvaluatedExpressionsUtils; import io.micronaut.inject.annotation.AnnotationMetadataHierarchy; import io.micronaut.inject.annotation.AnnotationMetadataReference; import io.micronaut.inject.annotation.AnnotationMetadataWriter; +import io.micronaut.core.expressions.EvaluatedExpressionReference; import io.micronaut.inject.annotation.MutableAnnotationMetadata; import io.micronaut.inject.ast.ClassElement; import io.micronaut.inject.ast.MethodElement; import io.micronaut.inject.ast.ParameterElement; import io.micronaut.inject.ast.TypedElement; import io.micronaut.inject.processing.JavaModelUtils; +import io.micronaut.inject.visitor.VisitorContext; import org.objectweb.asm.ClassWriter; import org.objectweb.asm.Label; import org.objectweb.asm.Opcodes; @@ -41,6 +48,7 @@ import java.io.OutputStream; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; @@ -96,12 +104,13 @@ public class ExecutableMethodsDefinitionWriter extends AbstractClassFileWriter i private final DispatchWriter methodDispatchWriter; private final Set methodNames = new HashSet<>(); - + private final DefaultExpressionCompilationContextFactory expressionCompilationContextFactory; + private final Set evaluatedExpressions = new HashSet<>(); private final AnnotationMetadata annotationMetadataWithDefaults; - private ClassWriter classWriter; - public ExecutableMethodsDefinitionWriter(AnnotationMetadata annotationMetadataWithDefaults, + public ExecutableMethodsDefinitionWriter(VisitorContext visitorContext, + AnnotationMetadata annotationMetadataWithDefaults, String beanDefinitionClassName, String beanDefinitionReferenceClassName, OriginatingElements originatingElements) { @@ -112,6 +121,7 @@ public ExecutableMethodsDefinitionWriter(AnnotationMetadata annotationMetadataWi this.thisType = Type.getObjectType(internalName); this.beanDefinitionReferenceClassName = beanDefinitionReferenceClassName; this.methodDispatchWriter = new DispatchWriter(thisType); + this.expressionCompilationContextFactory = new DefaultExpressionCompilationContextFactory(visitorContext); } /** @@ -128,6 +138,14 @@ public Type getClassType() { return thisType; } + /** + * @return list of evaluated expressions. + */ + @NonNull + public Set getEvaluatedExpressions() { + return evaluatedExpressions; + } + private MethodElement getMethodElement(int index) { return ((DispatchWriter.MethodDispatchTarget) methodDispatchWriter.getDispatchTargets().get(index)).methodElement; } @@ -196,6 +214,7 @@ public int visitExecutableMethod(TypedElement declaringType, MethodElement methodElement, String interceptedProxyClassName, String interceptedProxyBridgeMethodName) { + processEvaluatedExpressions(methodElement); String methodKey = methodElement.getName() + "(" + @@ -507,4 +526,16 @@ private void pushAnnotationMetadata(AnnotationMetadata annotationMetadataWithDef throw new IllegalStateException("Unknown metadata: " + annotationMetadata); } } + + private void processEvaluatedExpressions(MethodElement methodElement) { + Collection expressionReferences = + EvaluatedExpressionsUtils.findEvaluatedExpressionReferences(methodElement.getDeclaredMetadata()); + + expressionReferences.stream() + .map(expression -> { + ExpressionCompilationContext evaluationContext = expressionCompilationContextFactory.buildContextForMethod(expression, methodElement); + return new ExpressionWithContext(expression, evaluationContext); + }) + .forEach(evaluatedExpressions::add); + } } diff --git a/core/src/main/java/io/micronaut/core/annotation/AnnotationMetadata.java b/core/src/main/java/io/micronaut/core/annotation/AnnotationMetadata.java index ace0c41212c..75817324561 100644 --- a/core/src/main/java/io/micronaut/core/annotation/AnnotationMetadata.java +++ b/core/src/main/java/io/micronaut/core/annotation/AnnotationMetadata.java @@ -92,6 +92,16 @@ default boolean hasPropertyExpressions() { return true; } + /** + * Does the metadata contain any evaluated expressions like {@code #{ T(java.lang.Math).random() }}. + * + * @return True if evaluated expressions are present + * @since 4.0.0 + */ + default boolean hasEvaluatedExpressions() { + return false; + } + /** * Resolve all of the annotation names that feature the given stereotype. * diff --git a/core/src/main/java/io/micronaut/core/annotation/AnnotationValue.java b/core/src/main/java/io/micronaut/core/annotation/AnnotationValue.java index 3a0c022f86b..b88b601c811 100644 --- a/core/src/main/java/io/micronaut/core/annotation/AnnotationValue.java +++ b/core/src/main/java/io/micronaut/core/annotation/AnnotationValue.java @@ -19,6 +19,7 @@ import io.micronaut.core.convert.ConversionContext; import io.micronaut.core.convert.ConversionService; import io.micronaut.core.convert.value.ConvertibleValues; +import io.micronaut.core.expressions.EvaluatedExpression; import io.micronaut.core.reflect.ClassUtils; import io.micronaut.core.reflect.ReflectionUtils; import io.micronaut.core.type.Argument; @@ -160,8 +161,6 @@ public AnnotationValue(String annotationName, ConvertibleValues converti } /** - * Internal copy constructor. - * * @param target The target * @param defaultValues The default values * @param convertibleValues The convertible values @@ -169,10 +168,10 @@ public AnnotationValue(String annotationName, ConvertibleValues converti */ @Internal @UsedByGeneratedCode - protected AnnotationValue(AnnotationValue target, - Map defaultValues, - ConvertibleValues convertibleValues, - Function valueMapper) { + public AnnotationValue(AnnotationValue target, + Map defaultValues, + ConvertibleValues convertibleValues, + Function valueMapper) { this.annotationName = target.annotationName; this.defaultValues = defaultValues; this.values = target.values; @@ -752,7 +751,7 @@ public OptionalInt intValue() { @Override public OptionalLong longValue(@NonNull String member) { - return longValue(member, null); + return longValue(member, valueMapper); } /** @@ -789,7 +788,7 @@ public OptionalLong longValue(@NonNull String member, @Nullable Function shortValue(@NonNull String member) { - return shortValue(member, null); + return shortValue(member, valueMapper); } /** @@ -972,7 +971,7 @@ public Optional stringValue() { @Override public Optional booleanValue(@NonNull String member) { - return booleanValue(member, null); + return booleanValue(member, valueMapper); } /** @@ -1301,6 +1300,17 @@ public Optional> getAnnotation(@NonNul return Optional.empty(); } + /** + * If this AnnotationValue contains Evaluated Expressions. + * + * @return true if it is + * @since 4.0.0 + */ + public boolean hasEvaluatedExpressions() { + return values.values().stream() + .anyMatch(value -> value instanceof EvaluatedExpression); + } + @Override public String toString() { if (values.isEmpty()) { @@ -1605,7 +1615,7 @@ private Object getRawSingleValue(@NonNull String member, Function members(@Nullable Map mem clazz == String.class || clazz == Enum.class || clazz == AnnotationClassValue.class || - clazz == AnnotationValue.class + clazz == AnnotationValue.class || + clazz == EvaluatedExpressionReference.class || + clazz == EvaluatedExpression.class ); if (!isValid) { throw new IllegalArgumentException("The member named [" + entry.getKey().toString() + "] with type [" + value.getClass().getName() + "] is not a valid member type"); diff --git a/core/src/main/java/io/micronaut/core/expressions/EvaluatedExpression.java b/core/src/main/java/io/micronaut/core/expressions/EvaluatedExpression.java new file mode 100644 index 00000000000..6b65133ba2b --- /dev/null +++ b/core/src/main/java/io/micronaut/core/expressions/EvaluatedExpression.java @@ -0,0 +1,36 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.core.expressions; + +import io.micronaut.core.annotation.Internal; + +/** + * Expression included in annotation metadata which can be evaluated at runtime. + * + * @author Sergey Gavrilov + * @since 4.0.0 + */ +@Internal +public interface EvaluatedExpression { + /** + * Evaluate expression to obtain evaluation result. + * + * @param evaluationContext context that expression might need for evaluation. + * + * @return evaluation result + */ + Object evaluate(ExpressionEvaluationContext evaluationContext); +} diff --git a/core/src/main/java/io/micronaut/core/expressions/EvaluatedExpressionReference.java b/core/src/main/java/io/micronaut/core/expressions/EvaluatedExpressionReference.java new file mode 100644 index 00000000000..da1030214dc --- /dev/null +++ b/core/src/main/java/io/micronaut/core/expressions/EvaluatedExpressionReference.java @@ -0,0 +1,82 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.core.expressions; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.NonNull; + +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Wrapper for annotation value, containing evaluated expressions and + * class name for generated expression class. This class is only used + * at compilation time as part of compile-time annotation metadata. + * + * @param annotationValue initial annotation value which is treated as evaluated expression + * @param annotationName name of the annotation in which evaluated expression is used. + * @param annotationMember annotation member for which evaluated expression is used + * @param expressionClassName name for the class which is generated at compilation time and contains expression evaluation logic + * + * @author Sergey Gavrilov + * @since 4.0.0 + */ +@Internal +public record EvaluatedExpressionReference(@NonNull Object annotationValue, + @NonNull String annotationName, + @NonNull String annotationMember, + @NonNull String expressionClassName) { + + public static final String EXPR_SUFFIX = "$Expr"; + + private static final Map CLASS_NAME_INDEXES = new ConcurrentHashMap<>(); + + /** + * Provides next expression index for passed class name. In general indexes are needed only + * to make names of generated expression classes unique and avoid conflicts in cases when + * multiple expressions are defined in the same class. On each invocation with the same + * argument this method will return value incremented by 1. On first invocation it will return 0 + * + * @param className name of class owning evaluated expression + * @return next index + */ + public static Integer nextIndex(String className) { + if (CLASS_NAME_INDEXES.containsKey(className)) { + return CLASS_NAME_INDEXES.merge(className, 1, Integer::sum); + } + + CLASS_NAME_INDEXES.put(className, 0); + return 0; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + EvaluatedExpressionReference that = (EvaluatedExpressionReference) o; + return expressionClassName.equals(that.expressionClassName); + } + + @Override + public int hashCode() { + return Objects.hash(expressionClassName); + } +} diff --git a/core/src/main/java/io/micronaut/core/expressions/ExpressionEvaluationContext.java b/core/src/main/java/io/micronaut/core/expressions/ExpressionEvaluationContext.java new file mode 100644 index 00000000000..3c09ac6cc72 --- /dev/null +++ b/core/src/main/java/io/micronaut/core/expressions/ExpressionEvaluationContext.java @@ -0,0 +1,46 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.core.expressions; + +import io.micronaut.core.annotation.Internal; + +/** + * Context that can be used by evaluated expression to obtain objects required + * for evaluation process. + * + * @since 4.0.0 + * @author Sergey Gavrilov + */ +@Internal +public interface ExpressionEvaluationContext extends AutoCloseable { + + /** + * Provides method argument by index. + * + * @param index argument index + * @return argument value + */ + Object getArgument(int index); + + /** + * Provides bean by type. + * + * @param type of required bean + * @param type required bean class object + * @return bean instance + */ + T getBean(Class type); +} diff --git a/core/src/main/java/io/micronaut/core/util/ObjectUtils.java b/core/src/main/java/io/micronaut/core/util/ObjectUtils.java index 3bac9a947c7..dbcfa510a4a 100644 --- a/core/src/main/java/io/micronaut/core/util/ObjectUtils.java +++ b/core/src/main/java/io/micronaut/core/util/ObjectUtils.java @@ -18,6 +18,10 @@ import io.micronaut.core.annotation.Internal; import io.micronaut.core.annotation.Nullable; +import java.util.Collection; +import java.util.Map; +import java.util.Optional; + /** *

Utility methods for working with objects

. * @@ -60,4 +64,39 @@ public static int hash(@Nullable Object o1, @Nullable Object o2, @Nullable Obje return result; } + /** + * Coerce the given object to boolean. The following cases are handled: + * + *
    + *
  1. {@code null} results in {@code false}
  2. + *
  3. empty strings result in {@code false}
  4. + *
  5. positive numbers are {@code true}
  6. + *
  7. empty collections, arrays, optionals and maps are {@code false}
  8. + *
+ * @param object The object + * @return The boolean + * @since 4.0.0 + */ + @SuppressWarnings("unused") // used by expressions + public static boolean coerceToBoolean(@Nullable Object object) { + if (object == null) { + return false; + } else if (object instanceof Boolean b) { + return b; + } else if (object instanceof CharSequence charSequence) { + return charSequence.length() > 0; + } else if (object instanceof Number n) { + return n.doubleValue() != 0; + } else if (object instanceof Collection col) { + return !col.isEmpty(); + } else if (object instanceof Map col) { + return !col.isEmpty(); + } else if (object instanceof Object[] array) { + return array.length > 0; + } else if (object instanceof Optional opt) { + return opt.isPresent(); + } + return true; + + } } diff --git a/core/src/test/groovy/io/micronaut/core/util/ObjectUtilsSpec.groovy b/core/src/test/groovy/io/micronaut/core/util/ObjectUtilsSpec.groovy index e52f1a84901..5b426c39490 100644 --- a/core/src/test/groovy/io/micronaut/core/util/ObjectUtilsSpec.groovy +++ b/core/src/test/groovy/io/micronaut/core/util/ObjectUtilsSpec.groovy @@ -1,6 +1,7 @@ package io.micronaut.core.util import spock.lang.Specification +import spock.lang.Unroll class ObjectUtilsSpec extends Specification { @@ -21,4 +22,34 @@ class ObjectUtilsSpec extends Specification { o3 << ["abc", null, "xyz", null] } + @Unroll("ObjectUtils.coerceToBoolean with argument #obj returns #expected") + def "ObjectUtils::coerceToBoolean"(boolean expected, Object obj) { + expect: + expected == ObjectUtils.coerceToBoolean(obj) + where: + expected | obj + false | null + false | Boolean.FALSE + true | Boolean.TRUE + true | "string" + false | "" + false | 0L + false | new BigDecimal("0.0") + false | 0 + false | 0.0f + true | 1L + true | new BigDecimal("0.1") + true | 1 + true | -1 + true | 0.1f + false | Collections.emptyList() + true | Collections.singletonList("1") + false | Collections.emptyMap() + true | Collections.singletonMap("foo", "bar") + false | new String[] {} + true | new String[] {"foo"} + false | Optional.empty() + true | Optional.of("foo") + } + } diff --git a/http/src/main/java/io/micronaut/http/uri/UriMatchTemplate.java b/http/src/main/java/io/micronaut/http/uri/UriMatchTemplate.java index 2db9f8254a7..0e402d8d41d 100644 --- a/http/src/main/java/io/micronaut/http/uri/UriMatchTemplate.java +++ b/http/src/main/java/io/micronaut/http/uri/UriMatchTemplate.java @@ -37,7 +37,7 @@ */ public class UriMatchTemplate extends UriTemplate implements UriMatcher { - protected static final String VARIABLE_MATCH_PATTERN = "([^\\/\\?#&;\\+]"; + protected static final String VARIABLE_MATCH_PATTERN = "([^\\/\\?#(?!\\{)&;\\+]"; protected StringBuilder pattern; protected List variables; private final Pattern matchPattern; diff --git a/inject-groovy-test/src/main/groovy/io/micronaut/ast/transform/test/AbstractEvaluatedExpressionsSpec.groovy b/inject-groovy-test/src/main/groovy/io/micronaut/ast/transform/test/AbstractEvaluatedExpressionsSpec.groovy new file mode 100644 index 00000000000..0e57a80a62d --- /dev/null +++ b/inject-groovy-test/src/main/groovy/io/micronaut/ast/transform/test/AbstractEvaluatedExpressionsSpec.groovy @@ -0,0 +1,125 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.ast.transform.test + +import io.micronaut.context.expressions.AbstractEvaluatedExpression +import io.micronaut.context.expressions.DefaultExpressionEvaluationContext +import io.micronaut.core.expressions.EvaluatedExpressionReference +import io.micronaut.core.naming.NameUtils +import org.intellij.lang.annotations.Language + +class AbstractEvaluatedExpressionsSpec extends AbstractBeanDefinitionSpec { + + List evaluateMultiple(String... expressions) { + + String classContent = "" + for (int i = 0; i < expressions.size(); i++) { + classContent += """ + + @Value("${expressions[i]}") + Object field${i} + + """ + } + + def cls = """ + package test + import io.micronaut.context.annotation.Value + + class Expr { + ${classContent} + } + """.stripIndent().stripLeading() + + def applicationContext = buildContext(cls) + def classLoader = applicationContext.classLoader + + def exprClassName = 'test.$Expr$Expr' + def startingIndex = EvaluatedExpressionReference.nextIndex(exprClassName) - expressions.length + + List result = new ArrayList<>() + for (int i = startingIndex; i < startingIndex + expressions.size(); i++) { + String exprFullName = exprClassName + i + try { + def exprClass = (AbstractEvaluatedExpression) classLoader.loadClass(exprFullName).newInstance() + result.add(exprClass.evaluate(new DefaultExpressionEvaluationContext(null, applicationContext, null))) + } catch (ClassNotFoundException e) { + return null + } + } + + return result + } + + Object evaluate(String expression) { + return evaluateAgainstContext(expression, "") + } + + Object evaluateAgainstContext(String expression, @Language("groovy") String contextClass) { + String exprClassName = 'test.$Expr$Expr'; + + def cls = """ + package test + import io.micronaut.context.annotation.Value + import jakarta.inject.Singleton + + ${contextClass} + + class Expr { + @Value("${expression}") + Object field + } + """.stripIndent().stripLeading() + + def applicationContext = buildContext(cls) + def classLoader = applicationContext.classLoader + + try { + def index = EvaluatedExpressionReference.nextIndex(exprClassName) + def exprClass = (AbstractEvaluatedExpression) classLoader.loadClass(exprClassName + (index == 0 ? index : index - 1)).newInstance() + return exprClass.evaluate(new DefaultExpressionEvaluationContext(null, applicationContext, null)); + } catch (ClassNotFoundException e) { + return null + } + } + + Object evaluateSingle(String className, + @Language("groovy") String cls) { + return evaluateSingle(className, cls, null); + } + + Object evaluateSingle(String className, + @Language("groovy") String cls, + Object[] args) { + + def classSimpleName = NameUtils.getSimpleName(className) + def packageName = NameUtils.getPackageName(className) + def exprClassName = (classSimpleName.startsWith('$') ? '' : '$') + classSimpleName + '$Expr' + + String exprFullName = "${packageName}.${exprClassName}" + + def applicationContext = buildContext(cls) + def classLoader = applicationContext.classLoader + + try { + def index = EvaluatedExpressionReference.nextIndex(exprFullName) + def exprClass = (AbstractEvaluatedExpression) classLoader.loadClass(exprFullName + (index == 0 ? index : index - 1)).newInstance() + return exprClass.evaluate(new DefaultExpressionEvaluationContext(args, applicationContext, null)); + } catch (ClassNotFoundException e) { + return null + } + } +} diff --git a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/InjectTransform.groovy b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/InjectTransform.groovy index c81b429b294..79bb85c4d40 100644 --- a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/InjectTransform.groovy +++ b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/InjectTransform.groovy @@ -25,6 +25,7 @@ import io.micronaut.ast.groovy.visitor.GroovyPackageElement import io.micronaut.ast.groovy.visitor.GroovyVisitorContext import io.micronaut.context.annotation.Configuration import io.micronaut.context.annotation.Context +import io.micronaut.expressions.context.ExpressionWithContext import io.micronaut.inject.processing.BeanDefinitionCreator import io.micronaut.inject.processing.BeanDefinitionCreatorFactory import io.micronaut.inject.processing.ProcessingException @@ -39,6 +40,7 @@ import org.codehaus.groovy.ast.ClassNode import org.codehaus.groovy.ast.InnerClassNode import org.codehaus.groovy.ast.ModuleNode import org.codehaus.groovy.ast.PackageNode +import io.micronaut.expressions.EvaluatedExpressionWriter import org.codehaus.groovy.control.CompilationUnit import org.codehaus.groovy.control.CompilePhase import org.codehaus.groovy.control.SourceUnit @@ -73,6 +75,7 @@ class InjectTransform implements ASTTransformation, CompilationUnitAware { } else { outputVisitor = new DirectoryClassWriterOutputVisitor(classesDir) } + List classes = moduleNode.getClasses() if (classes.size() == 1) { ClassNode classNode = classes[0] @@ -125,6 +128,11 @@ class InjectTransform implements ASTTransformation, CompilationUnitAware { String beanTypeName = beanDefWriter.beanTypeName AnnotatedNode beanClassNode = entry.key try { + for (ExpressionWithContext expression: beanDefWriter.evaluatedExpressions) { + new EvaluatedExpressionWriter(expression, new GroovyVisitorContext(source, unit), beanDefWriter.originatingElement) + .accept(outputVisitor); + } + BeanDefinitionReferenceWriter beanReferenceWriter = new BeanDefinitionReferenceWriter(beanDefWriter) beanReferenceWriter.setRequiresMethodProcessing(beanDefWriter.requiresMethodProcessing()) beanReferenceWriter.setContextScope(beanDefWriter.getAnnotationMetadata().hasDeclaredAnnotation(Context)) diff --git a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/TypeElementVisitorEnd.groovy b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/TypeElementVisitorEnd.groovy index 25953bdc5a9..08b42bdc9dc 100644 --- a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/TypeElementVisitorEnd.groovy +++ b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/TypeElementVisitorEnd.groovy @@ -25,6 +25,7 @@ import io.micronaut.ast.groovy.visitor.LoadedVisitor import io.micronaut.core.order.OrderUtil import io.micronaut.inject.annotation.AbstractAnnotationMetadataBuilder import io.micronaut.inject.writer.AbstractBeanDefinitionBuilder +import io.micronaut.inject.writer.BeanDefinitionWriter import io.micronaut.inject.writer.ClassWriterOutputVisitor import io.micronaut.inject.writer.DirectoryClassWriterOutputVisitor import org.codehaus.groovy.ast.ASTNode @@ -98,7 +99,7 @@ class TypeElementVisitorEnd implements ASTTransformation, CompilationUnitAware { TypeElementVisitorTransform.loadedVisitors.remove() TypeElementVisitorTransform.beanDefinitionBuilders.remove() - AbstractAnnotationMetadataBuilder.clearMutated() + BeanDefinitionWriter.finish() } @Override diff --git a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/TypeElementVisitorStart.groovy b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/TypeElementVisitorStart.groovy index 0513823831e..9b6125124ed 100644 --- a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/TypeElementVisitorStart.groovy +++ b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/TypeElementVisitorStart.groovy @@ -44,7 +44,8 @@ import org.codehaus.groovy.transform.GroovyASTTransformation * @since 1.0 */ @CompileStatic -@GroovyASTTransformation(phase = CompilePhase.INITIALIZATION) +// IMPORTANT NOTE: This transform runs in phase CONVERSION so it runs before TypeElementVisitorTransform +@GroovyASTTransformation(phase = CompilePhase.CONVERSION) class TypeElementVisitorStart implements ASTTransformation, CompilationUnitAware { public static final String ELEMENT_VISITORS_PROPERTY = "micronaut.element.visitors" diff --git a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/annotation/GroovyAnnotationMetadataBuilder.java b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/annotation/GroovyAnnotationMetadataBuilder.java index 3245cce1ae3..184b263579d 100644 --- a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/annotation/GroovyAnnotationMetadataBuilder.java +++ b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/annotation/GroovyAnnotationMetadataBuilder.java @@ -24,6 +24,7 @@ import io.micronaut.ast.groovy.visitor.GroovyVisitorContext; import io.micronaut.core.annotation.AnnotationClassValue; import io.micronaut.core.annotation.AnnotationMetadata; +import io.micronaut.core.expressions.EvaluatedExpressionReference; import io.micronaut.core.annotation.AnnotationValue; import io.micronaut.core.annotation.NonNull; import io.micronaut.core.convert.ConversionService; @@ -147,6 +148,20 @@ protected AnnotatedNode getAnnotationMember(AnnotatedNode annotationElement, Cha return null; } + @Override + protected String getOriginatingClassName(AnnotatedNode originatingElement) + { + if (originatingElement instanceof ClassNode classNode) { + return classNode.getName(); + } else if (originatingElement instanceof ExtendedParameter extendedParameter) { + return extendedParameter.getMethodNode().getDeclaringClass().getName(); + } else if (originatingElement instanceof MethodNode methodNode) { + return methodNode.getDeclaringClass().getName(); + } + + return originatingElement.getDeclaringClass().getName(); + } + @Override protected RetentionPolicy getRetentionPolicy(@NonNull AnnotatedNode annotation) { List annotations = annotation.getAnnotations(); @@ -351,7 +366,7 @@ protected void readAnnotationRawValues( Object annotationValue, Map annotationValues) { if (!annotationValues.containsKey(memberName)) { - final Object v = readAnnotationValue(originatingElement, member, memberName, annotationValue); + Object v = readAnnotationValue(originatingElement, member, annotationName, memberName, annotationValue); if (v != null) { validateAnnotationValue(originatingElement, annotationName, member, memberName, v); annotationValues.put(memberName, v); @@ -386,9 +401,9 @@ protected void readAnnotationRawValues( } @Override - protected Object readAnnotationValue(AnnotatedNode originatingElement, AnnotatedNode member, String memberName, Object annotationValue) { + protected Object readAnnotationValue(AnnotatedNode originatingElement, AnnotatedNode member, String annotationName, String memberName, Object annotationValue) { if (annotationValue instanceof ConstantExpression constantExpression) { - return readConstantExpression(originatingElement, member, constantExpression); + return readConstantExpression(originatingElement, annotationName, member, constantExpression); } else if (annotationValue instanceof PropertyExpression pe) { if (pe.getObjectExpression() instanceof ClassExpression classExpression) { ClassNode propertyType = classExpression.getType(); @@ -417,15 +432,21 @@ protected Object readAnnotationValue(AnnotatedNode originatingElement, Annotated Expression valueExpression = propertyExpression.getProperty(); Expression objectExpression = propertyExpression.getObjectExpression(); if (valueExpression instanceof ConstantExpression constantExpression && objectExpression instanceof ClassExpression) { - Object value = readConstantExpression(originatingElement, member, constantExpression); + Object value = readConstantExpression(originatingElement, annotationName, member, constantExpression); if (value != null) { converted.add(value); } } } if (exp instanceof ConstantExpression constantExpression) { - Object value = readConstantExpression(originatingElement, member, constantExpression); + Object value = readConstantExpression(originatingElement, annotationName, member, constantExpression); if (value != null) { + // if value is an expression reference, since we're iterating through a list, + // we extract initial annotation value to wrap it into a single expression reference + // after the iteration is complete + if (value instanceof EvaluatedExpressionReference expressionReference) { + value = expressionReference.annotationValue(); + } converted.add(value); } } else if (exp instanceof ClassExpression classExpression) { @@ -438,11 +459,15 @@ protected Object readAnnotationValue(AnnotatedNode originatingElement, Annotated converted.add(new AnnotationClassValue<>(typeName)); } } - return toArray(member, converted); + Object array = toArray(member, converted); + if (isEvaluatedExpression(array)) { + return buildEvaluatedExpressionReference(originatingElement, annotationName, memberName, array); + } + return array; } else if (annotationValue instanceof VariableExpression variableExpression) { Variable variable = variableExpression.getAccessedVariable(); if (variable != null && variable.hasInitialExpression()) { - return readAnnotationValue(originatingElement, member, memberName, variable.getInitialExpression()); + return readAnnotationValue(originatingElement, member, annotationName, memberName, variable.getInitialExpression()); } } else if (annotationValue != null) { if (ClassUtils.isJavaLangType(annotationValue.getClass())) { @@ -477,6 +502,8 @@ private static Object toArray(AnnotatedNode member, Collection collection) { arrayType = AnnotationValue.class; } else if (Class.class.isAssignableFrom(arrayType)) { arrayType = AnnotationClassValue.class; + } else if (EvaluatedExpressionReference.class.isAssignableFrom(arrayType)) { + arrayType = EvaluatedExpressionReference.class; } } } @@ -487,6 +514,8 @@ private static Object toArray(AnnotatedNode member, Collection collection) { arrayType = AnnotationClassValue.class; } else if (collection.stream().allMatch(val -> val instanceof AnnotationValue)) { arrayType = AnnotationValue.class; + } else if (collection.stream().anyMatch(val -> val instanceof EvaluatedExpressionReference)) { + arrayType = Object.class; } if (arrayType.isPrimitive()) { Class wrapperType = ReflectionUtils.getWrapperType(arrayType); @@ -500,7 +529,7 @@ private static Object toArray(AnnotatedNode member, Collection collection) { .orElse(null); } - private Object readConstantExpression(AnnotatedNode originatingElement, AnnotatedNode member, ConstantExpression constantExpression) { + private Object readConstantExpression(AnnotatedNode originatingElement, String annotationName, AnnotatedNode member, ConstantExpression constantExpression) { if (constantExpression instanceof AnnotationConstantExpression ann) { AnnotationNode value = (AnnotationNode) ann.getValue(); return readNestedAnnotationValue(originatingElement, value); @@ -509,9 +538,17 @@ private Object readConstantExpression(AnnotatedNode originatingElement, Annotate if (value == null) { return null; } + if (isEvaluatedExpression(value)) { + String memberName = getAnnotationMemberName(member); + return buildEvaluatedExpressionReference(originatingElement, annotationName, memberName, value); + } if (value instanceof Collection collection) { collection = collection.stream().map(this::convertConstantValue).toList(); - return toArray(member, collection); + Object array = toArray(member, collection); + if (isEvaluatedExpression(array)) { + return buildEvaluatedExpressionReference(originatingElement, annotationName, getAnnotationMemberName(member), array); + } + return array; } return convertConstantValue(value); } diff --git a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyVisitorContext.java b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyVisitorContext.java index d168cf7521d..ffe1753a94e 100644 --- a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyVisitorContext.java +++ b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyVisitorContext.java @@ -29,6 +29,8 @@ import io.micronaut.core.reflect.ClassUtils; import io.micronaut.core.util.ArgumentUtils; import io.micronaut.core.util.CollectionUtils; +import io.micronaut.expressions.context.DefaultExpressionCompilationContextFactory; +import io.micronaut.expressions.context.ExpressionCompilationContextFactory; import io.micronaut.inject.annotation.AbstractAnnotationMetadataBuilder; import io.micronaut.inject.ast.ClassElement; import io.micronaut.inject.ast.Element; @@ -75,6 +77,7 @@ public class GroovyVisitorContext implements VisitorContext { private final GroovyElementFactory groovyElementFactory; private final List beanDefinitionBuilders = new ArrayList<>(); private final GroovyElementAnnotationMetadataFactory elementAnnotationMetadataFactory; + private final ExpressionCompilationContextFactory expressionCompilationContextFactory; /** * @param sourceUnit The source unit @@ -96,6 +99,7 @@ public GroovyVisitorContext(SourceUnit sourceUnit, @Nullable CompilationUnit com this.attributes = VISITOR_ATTRIBUTES; this.groovyElementFactory = new GroovyElementFactory(this); this.elementAnnotationMetadataFactory = new GroovyElementAnnotationMetadataFactory(false, new GroovyAnnotationMetadataBuilder(sourceUnit, compilationUnit)); + this.expressionCompilationContextFactory = new DefaultExpressionCompilationContextFactory(this); } @NonNull @@ -179,6 +183,11 @@ public GroovyElementAnnotationMetadataFactory getElementAnnotationMetadataFactor return elementAnnotationMetadataFactory; } + @Override + public ExpressionCompilationContextFactory getExpressionCompilationContextFactory() { + return this.expressionCompilationContextFactory; + } + @Override public AbstractAnnotationMetadataBuilder getAnnotationMetadataBuilder() { return new GroovyAnnotationMetadataBuilder(sourceUnit, compilationUnit); diff --git a/inject-groovy/src/main/resources/META-INF/services/org.codehaus.groovy.transform.ASTTransformation b/inject-groovy/src/main/resources/META-INF/services/org.codehaus.groovy.transform.ASTTransformation index 927e76a4c0a..9c6e6b6f9eb 100644 --- a/inject-groovy/src/main/resources/META-INF/services/org.codehaus.groovy.transform.ASTTransformation +++ b/inject-groovy/src/main/resources/META-INF/services/org.codehaus.groovy.transform.ASTTransformation @@ -1,4 +1,4 @@ io.micronaut.ast.groovy.InjectTransform io.micronaut.ast.groovy.TypeElementVisitorTransform io.micronaut.ast.groovy.TypeElementVisitorStart -io.micronaut.ast.groovy.TypeElementVisitorEnd \ No newline at end of file +io.micronaut.ast.groovy.TypeElementVisitorEnd diff --git a/inject-groovy/src/test/groovy/io/micronaut/expressions/AnnotationLevelContextExpressionsSpec.groovy b/inject-groovy/src/test/groovy/io/micronaut/expressions/AnnotationLevelContextExpressionsSpec.groovy new file mode 100644 index 00000000000..a7e53946c33 --- /dev/null +++ b/inject-groovy/src/test/groovy/io/micronaut/expressions/AnnotationLevelContextExpressionsSpec.groovy @@ -0,0 +1,76 @@ +package io.micronaut.expressions + +import io.micronaut.ast.transform.test.AbstractEvaluatedExpressionsSpec + + +class AnnotationLevelContextExpressionsSpec extends AbstractEvaluatedExpressionsSpec { + + void "test annotation level context"() { + given: + Object result = evaluateSingle("test.Expr", """ + package test + + import io.micronaut.context.annotation.AnnotationExpressionContext + import io.micronaut.context.annotation.Executable; + import io.micronaut.context.annotation.Requires; + import jakarta.inject.Singleton + + @Singleton + @CustomAnnotation("#{ #getAnnotationLevelValue() }") + class Expr { + } + + @Singleton + class CustomContext { + String getAnnotationLevelValue() { + return "annotationLevelValue"; + } + } + + @AnnotationExpressionContext(CustomContext.class) + @interface CustomAnnotation { + String value(); + } + + """) + + expect: + result instanceof String && result == "annotationLevelValue" + + } + + void "test annotation member level context"() { + given: + Object result = evaluateSingle("test.Expr", """ + package test + + import io.micronaut.context.annotation.AnnotationExpressionContext + import io.micronaut.context.annotation.Executable + import io.micronaut.context.annotation.Requires + import jakarta.inject.Singleton + + @Singleton + @CustomAnnotation(customValue = "#{ #getAnnotationLevelValue() }") + class Expr { + } + + @Singleton + class CustomContext { + String getAnnotationLevelValue() { + return "annotationLevelValue"; + } + } + + @interface CustomAnnotation { + @AnnotationExpressionContext(CustomContext.class) + String customValue(); + } + + """) + + expect: + result instanceof String && result == "annotationLevelValue" + + } + +} diff --git a/inject-groovy/src/test/groovy/io/micronaut/expressions/ArrayMethodsExpressionsSpec.groovy b/inject-groovy/src/test/groovy/io/micronaut/expressions/ArrayMethodsExpressionsSpec.groovy new file mode 100644 index 00000000000..c835d893784 --- /dev/null +++ b/inject-groovy/src/test/groovy/io/micronaut/expressions/ArrayMethodsExpressionsSpec.groovy @@ -0,0 +1,220 @@ +package io.micronaut.expressions + +import io.micronaut.ast.transform.test.AbstractEvaluatedExpressionsSpec + + +class ArrayMethodsExpressionsSpec extends AbstractEvaluatedExpressionsSpec { + + void "test primitive and wrapper varargs methods"() { + given: + Object expr1 = evaluateAgainstContext("#{ #countValues(1, 2, 3) }", + """ + @jakarta.inject.Singleton + class Context { + int countValues(int... array) { + return array.length + } + } + """) + + Object expr2 = evaluateAgainstContext("#{ #countValues(1) }", + """ + @jakarta.inject.Singleton + class Context { + int countValues(int... array) { + return array.length + } + } + """) + + Object expr3 = evaluateAgainstContext("#{ #countValues() }", + """ + @jakarta.inject.Singleton + class Context { + int countValues(int... array) { + return array.length + } + } + """) + + Object expr4 = evaluateAgainstContext("#{ #countValues(1, 2, T(Integer).valueOf('3')) }", + """ + @jakarta.inject.Singleton + class Context { + int countValues(Integer... array) { + return array.length + } + } + """) + + expect: + expr1 instanceof Integer && expr1 == 3 + expr2 instanceof Integer && expr2 == 1 + expr3 instanceof Integer && expr3 == 0 + expr4 instanceof Integer && expr4 == 3 + } + + void "test string varargs methods"() { + given: + Object expr1 = evaluateAgainstContext("#{ #countValues('a', 'b', 'c') }", + """ + @jakarta.inject.Singleton + class Context { + int countValues(String... values) { + return values.length + } + } + """) + + Object expr2 = evaluateAgainstContext("#{ #countValues('a') }", + """ + @jakarta.inject.Singleton + class Context { + int countValues(String... values) { + return values.length + } + } + """) + + Object expr3 = evaluateAgainstContext("#{ #countValues() }", + """ + @jakarta.inject.Singleton + class Context { + int countValues(String... values) { + return values.length + } + } + """) + + expect: + expr1 instanceof Integer && expr1 == 3 + expr2 instanceof Integer && expr2 == 1 + expr3 instanceof Integer && expr3 == 0 + } + + void "test mixed types varargs methods"() { + given: + Object expr1 = evaluateAgainstContext("#{ #multiplyLength(3, '1', 8, null) }", + """ + @jakarta.inject.Singleton + class Context { + int multiplyLength(int time, Object... values) { + return values.length * time + } + } + """) + + expect: + expr1 instanceof Integer && expr1 == 9 + } + + void "test arrays as varargs"() { + given: + Object expr1 = evaluateAgainstContext("#{ #multiplyLength(3, '1', 8, null) }", + """ + @jakarta.inject.Singleton + class Context { + int multiplyLength(Integer time, Object[] values) { + return values.length * time + } + } + """) + + expect: + expr1 instanceof Integer && expr1 == 9 + } + + void "test non-varargs arrays"() { + given: + Object expr1 = evaluateAgainstContext("#{ #countLength(#values()) }", + """ + @jakarta.inject.Singleton + class Context { + String[] values() { + return [ "a", "b", "c" ] + } + + int countLength(Object[] array) { + return array.length + } + } + """) + + Object expr2 = evaluateAgainstContext("#{ #multiplyLength(#values(), 3) }", + """ + @jakarta.inject.Singleton + class Context { + String[] values() { + return ["a", "b"] + } + + int multiplyLength(String[] array, int times) { + return array.length * times + } + } + """) + + Object expr3 = evaluateAgainstContext("#{ #multiplyLength(#values(), 1) }", + """ + @jakarta.inject.Singleton + class Context { + int[] values() { + return new int[]{1, 2} + } + + int multiplyLength(int[] array, int times) { + return array.length * times + } + } + """) + + expect: + expr1 instanceof Integer && expr1 == 3 + expr2 instanceof Integer && expr2 == 6 + expr3 instanceof Integer && expr3 == 2 + } + + void "test multi-dimensional arrays"() { + given: + Object expr1 = evaluateAgainstContext("#{ #countLength(#values()) }", + """ + import java.util.Arrays + + @jakarta.inject.Singleton + class Context { + String[][] values() { + return [ ["a", "b", "c"], ["a", "b"] ] + } + + int countLength(Object[][] array) { + return Arrays.stream(array) + .map(a -> a.length) + .reduce(0, Integer::sum) + } + } + """) + + Object expr2 = evaluateAgainstContext("#{ #countLength(#values()) }", + """ + import java.util.Arrays + + @jakarta.inject.Singleton + class Context { + int[][] values() { + return [[1, 2, 3], [1, 2, 3, 3]] + } + + int countLength(int[][] array) { + return Arrays.stream(array) + .map(a -> a.length) + .reduce(0, Integer::sum) + } + } + """) + + + expect: + expr1 instanceof Integer && expr1 == 5 + expr2 instanceof Integer && expr2 == 7 + } +} diff --git a/inject-groovy/src/test/groovy/io/micronaut/expressions/CompoundExpressionsSpec.groovy b/inject-groovy/src/test/groovy/io/micronaut/expressions/CompoundExpressionsSpec.groovy new file mode 100644 index 00000000000..899b9fd3038 --- /dev/null +++ b/inject-groovy/src/test/groovy/io/micronaut/expressions/CompoundExpressionsSpec.groovy @@ -0,0 +1,56 @@ +package io.micronaut.expressions + +import io.micronaut.ast.transform.test.AbstractEvaluatedExpressionsSpec + +class CompoundExpressionsSpec extends AbstractEvaluatedExpressionsSpec { + + void "test compound expressions"() { + given: + List results = evaluateMultiple( + "a #{1}#{'b'} #{3}", + "#{1 + 2}#{2 + 3}", + "#{ '5' } s", + "a#{ null }b" + ) + + expect: + results[0] instanceof String && results[0] == 'a 1b 3' + results[1] instanceof String && results[1] == '35' + results[2] instanceof String && results[2] == '5 s' + results[3] instanceof String && results[3] == 'anullb' + } + + void "test string expressions in arrays"() { + Object result = evaluateSingle("test.Expr", """ + package test + import io.micronaut.context.annotation.Requires + import jakarta.inject.Singleton + + @Singleton + @Requires(env = ["#{ 'a' }", "b", "#{ 'c' + 'd' }"]) + class Expr { + } + """) + + expect: + result instanceof Object[] && Arrays.equals((Object[]) result, new Object[]{'a', 'b', 'cd'}) + } + + + void "test mixed expressions in arrays"() { + Object result = evaluateSingle("test.Expr", """ + package test; + import io.micronaut.context.annotation.Requires; + import jakarta.inject.Singleton; + + @Singleton + @Requires(env = ["#{ 1 }", "b", "#{ 15l }", "#{ 'c' }"]) + class Expr { + } + """) + + expect: + result instanceof Object[] && Arrays.equals((Object[]) result, new Object[]{1, 'b', 15l, 'c'}) + } + +} diff --git a/inject-groovy/src/test/groovy/io/micronaut/expressions/ContextMethodCallsExpressionsSpec.groovy b/inject-groovy/src/test/groovy/io/micronaut/expressions/ContextMethodCallsExpressionsSpec.groovy new file mode 100644 index 00000000000..45183ca9862 --- /dev/null +++ b/inject-groovy/src/test/groovy/io/micronaut/expressions/ContextMethodCallsExpressionsSpec.groovy @@ -0,0 +1,149 @@ +package io.micronaut.expressions + +import io.micronaut.ast.transform.test.AbstractEvaluatedExpressionsSpec + +class ContextMethodCallsExpressionsSpec extends AbstractEvaluatedExpressionsSpec{ + + void "test context method calls"() { + given: + Object expr1 = evaluateAgainstContext("#{ #getIntValue() }", + """ + @jakarta.inject.Singleton + class Context { + public int getIntValue() { + return 15 + } + } + """) + + Object expr2 = evaluateAgainstContext("#{ #getStringValue().toUpperCase() }", + """ + @jakarta.inject.Singleton + class Context { + String getStringValue() { + return "test" + } + } + """) + + Object expr3 = evaluateAgainstContext("#{ #randomizer().nextInt(10) }", + """ + import java.util.Random + + @jakarta.inject.Singleton + class Context { + Random randomizer() { + return new Random() + } + } + """) + + Object expr4 = evaluateAgainstContext("#{ #lowercase('TEST') }", + """ + import java.util.Random + + @jakarta.inject.Singleton + class Context { + String lowercase(String value) { + return value.toLowerCase() + } + } + """) + + ContextRegistrar.setClasses( + "test.FirstContext", + "test.SecondContext", + "test.ThirdContext", + "test.FourthContext" + ) + Object expr5 = evaluateAgainstContext("#{ #transform(#getName(), #getRepeat(), #toLower()) }", + """ + import java.util.Random + + @jakarta.inject.Singleton + class FirstContext { + String transform(String value, int repeat, Boolean toLower) { + return (toLower ? value.toLowerCase() : value).repeat(repeat) + } + } + + @jakarta.inject.Singleton + class SecondContext { + String getName() { + return "TEST" + } + } + + @jakarta.inject.Singleton + class ThirdContext { + Integer getRepeat() { + return 2 + } + } + + @jakarta.inject.Singleton + class FourthContext { + boolean toLower() { + return true + } + } + """) + + ContextRegistrar.reset() + Object expr6 = evaluateAgainstContext("#{ #getTestObject().name }", + """ + import java.util.Random + + @jakarta.inject.Singleton + class Context { + TestObject getTestObject() { + return new TestObject() + } + } + + class TestObject { + String getName() { + return "name" + } + } + """) + + Object expr7 = evaluateAgainstContext("#{ #values().get(#random(#values())) }", + """ + import java.util.Random + import java.util.List + import java.util.Collection + import java.util.concurrent.ThreadLocalRandom + + @jakarta.inject.Singleton + class Context { + TestObject getTestObject() { + return new TestObject(); + } + + List values() { + return List.of(1, 2, 3); + } + + int random(Collection values) { + return ThreadLocalRandom.current().nextInt(values.size()); + } + } + + class TestObject { + String getName() { + return "name"; + } + } + """) + + expect: + expr1 instanceof Integer && expr1 == 15 + expr2 instanceof String && expr2 == "TEST" + expr3 instanceof Integer && expr3 >= 0 && expr3 < 10 + expr4 instanceof String && expr4 == "test" + expr5 instanceof String && expr5 == "testtest" + expr6 instanceof String && expr6 == "name" + expr7 instanceof Integer && (expr7 == 1 || expr7 == 2 || expr7 == 3) + } +} diff --git a/inject-groovy/src/test/groovy/io/micronaut/expressions/ContextPropertyAccessExpressionsSpec.groovy b/inject-groovy/src/test/groovy/io/micronaut/expressions/ContextPropertyAccessExpressionsSpec.groovy new file mode 100644 index 00000000000..0ff25cb888c --- /dev/null +++ b/inject-groovy/src/test/groovy/io/micronaut/expressions/ContextPropertyAccessExpressionsSpec.groovy @@ -0,0 +1,62 @@ +package io.micronaut.expressions + +import io.micronaut.ast.transform.test.AbstractEvaluatedExpressionsSpec +import io.micronaut.context.exceptions.ExpressionEvaluationException; + +class ContextPropertyAccessExpressionsSpec extends AbstractEvaluatedExpressionsSpec +{ + void "test context property access"() { + given: + Object expr1 = evaluateAgainstContext("#{ #intValue }", + """ + @jakarta.inject.Singleton + class Context { + int getIntValue() { + return 15 + } + } + """) + + Object expr2 = evaluateAgainstContext("#{ #boolean }", + """ + @jakarta.inject.Singleton + class Context { + Boolean isBoolean() { + return false + } + } + """) + + Object expr3 = evaluateAgainstContext("#{ #stringValue }", + """ + @jakarta.inject.Singleton + class Context { + String getStringValue() { + return "test value" + } + } + """) + + Object expr4 = evaluateAgainstContext("#{ #customClass.customProperty }", + """ + @jakarta.inject.Singleton + class Context { + CustomClass getCustomClass() { + return new CustomClass() + } + } + + class CustomClass { + String customProperty = "custom property" + } + """) + + expect: + expr1 instanceof Integer && expr1 == 15 + expr2 instanceof Boolean && expr2 == false + expr3 instanceof String && expr3 == "test value" + expr4 instanceof String && expr4 == "custom property" + } + + +} diff --git a/inject-groovy/src/test/groovy/io/micronaut/expressions/ContextRegistrar.groovy b/inject-groovy/src/test/groovy/io/micronaut/expressions/ContextRegistrar.groovy new file mode 100644 index 00000000000..281012083fe --- /dev/null +++ b/inject-groovy/src/test/groovy/io/micronaut/expressions/ContextRegistrar.groovy @@ -0,0 +1,27 @@ +package io.micronaut.expressions + +import io.micronaut.inject.visitor.TypeElementVisitor +import io.micronaut.inject.visitor.VisitorContext + +class ContextRegistrar implements TypeElementVisitor { + static final List CLASSES = ["test.Context"] + + static void setClasses(String...classes) { + CLASSES.clear() + CLASSES.addAll(classes) + } + + static void reset() { + CLASSES.clear() + CLASSES.add("test.Context") + } + + @Override + void start(VisitorContext visitorContext) { + for (cls in CLASSES) { + visitorContext.getClassElement(cls).ifPresent { + visitorContext.expressionCompilationContextFactory.registerContextClass(it) + } + } + } +} diff --git a/inject-groovy/src/test/groovy/io/micronaut/expressions/LiteralExpressionsSpec.groovy b/inject-groovy/src/test/groovy/io/micronaut/expressions/LiteralExpressionsSpec.groovy new file mode 100644 index 00000000000..411c6483a22 --- /dev/null +++ b/inject-groovy/src/test/groovy/io/micronaut/expressions/LiteralExpressionsSpec.groovy @@ -0,0 +1,96 @@ +package io.micronaut.expressions + + +import io.micronaut.ast.transform.test.AbstractEvaluatedExpressionsSpec + +class LiteralExpressionsSpec extends AbstractEvaluatedExpressionsSpec { + + void "test literals"() { + given: + List results = evaluateMultiple( + // null + "#{ null }", // 0 + + // string literals + "#{ 'string literal' }", // 1 + "#{ 'testValue' }", // 2 + + // bool literals + "#{ false }", // 3 + "#{ true }", // 4 + + // int literals + "#{ 15 }", // 5 + "#{ 0XAB013 }", // 6 + "#{ 0xFF }", // 7 + "#{ 291231 }", // 8 + "#{ 0 }", // 9 + "#{ 0x0 }", // 10 + "#{ 00 }", // 11 + + // long literals + "#{ 0xFFL }", // 12 + "#{ 0x0123l }", // 13 + "#{ 102L }", // 14 + "#{ 99l }", // 15 + "#{ 0L }", // 16 + + // float literals + "#{ 123.e+14f }", // 17 + "#{ 123.f }", // 18 + "#{ 123.F }", // 19 + "#{ .123f }", // 20 + "#{ 19F }", // 21 + + // double literals + "#{ 123. }", // 22 + "#{ 123.321 }", // 23 + "#{ 123.d }", // 24 + "#{ 123.D }", // 25 + "#{ .123 }", // 26 + "#{ 123D }", // 27 + "#{ 1E-7 }", // 28 + "#{ 1E+1d }", // 29 + "#{ 2e-1 }", // 30 + ) + + expect: + results[0] == null + + results[1] instanceof String && results[1] == 'string literal' + results[2] instanceof String && results[2] == 'testValue' + + results[3] instanceof Boolean && results[3] == false + results[4] instanceof Boolean && results[4] == true + + results[5] instanceof Integer && results[5] == 15 + results[6] instanceof Integer && results[6] == 0XAB013 + results[7] instanceof Integer && results[7] == 0xFF + results[8] instanceof Integer && results[8] == 291231 + results[9] instanceof Integer && results[9] == 0 + results[10] instanceof Integer && results[10] == 0x0 + results[11] instanceof Integer && results[11] == 00 + + results[12] instanceof Long && results[12] == 0xFFL + results[13] instanceof Long && results[13] == 0x0123l + results[14] instanceof Long && results[14] == 102L + results[15] instanceof Long && results[15] == 99L + results[16] instanceof Long && results[16] == 0L + + results[17] instanceof Float + results[18] instanceof Float + results[19] instanceof Float + results[20] instanceof Float && results[20] == .123f + results[21] instanceof Float && results[21] == 19F + + results[22] instanceof Double && results[22] == Double.valueOf("123.") + results[23] instanceof Double && results[23] == 123.321 + results[24] instanceof Double && results[24] == Double.valueOf("123.d") + results[25] instanceof Double && results[25] == Double.valueOf("123.D") + results[26] instanceof Double && results[26] == .123 + results[27] instanceof Double && results[27] == 123D + results[28] instanceof Double && results[28] == 1E-7 + results[29] instanceof Double && results[29] == 1E+1d + results[30] instanceof Double && results[30] == 2e-1 + } +} diff --git a/inject-groovy/src/test/groovy/io/micronaut/expressions/MethodArgumentEvaluationContextExpressionsSpec.groovy b/inject-groovy/src/test/groovy/io/micronaut/expressions/MethodArgumentEvaluationContextExpressionsSpec.groovy new file mode 100644 index 00000000000..34753fdeb75 --- /dev/null +++ b/inject-groovy/src/test/groovy/io/micronaut/expressions/MethodArgumentEvaluationContextExpressionsSpec.groovy @@ -0,0 +1,30 @@ +package io.micronaut.expressions; + +import io.micronaut.ast.transform.test.AbstractEvaluatedExpressionsSpec + +class MethodArgumentEvaluationContextExpressionsSpec extends AbstractEvaluatedExpressionsSpec +{ + void "test method argument access"() { + given: + Object result = evaluateSingle("test.Expr", """ + package test + import io.micronaut.context.annotation.Executable + import io.micronaut.context.annotation.Requires + import jakarta.inject.Singleton + + @Singleton + class Expr { + + @Executable + @Requires(value = "#{ #second + \'abc\' }") + void test(String first, String second) { + } + } + + + """, ["arg0", "arg1"] as Object[]); + + expect: + result instanceof String && result == 'arg1abc' + } +} diff --git a/inject-groovy/src/test/groovy/io/micronaut/expressions/OperatorExpressionsSpec.groovy b/inject-groovy/src/test/groovy/io/micronaut/expressions/OperatorExpressionsSpec.groovy new file mode 100644 index 00000000000..f97f3248d40 --- /dev/null +++ b/inject-groovy/src/test/groovy/io/micronaut/expressions/OperatorExpressionsSpec.groovy @@ -0,0 +1,905 @@ +package io.micronaut.expressions + +import io.micronaut.ast.transform.test.AbstractEvaluatedExpressionsSpec +import org.codehaus.groovy.control.CompilationFailedException + +class OperatorExpressionsSpec extends AbstractEvaluatedExpressionsSpec { + + void "test '/' operator"() { + given: + List results = evaluateMultiple( + // int + "#{ 10 / 5 }", + "#{ 5 / 10 }", + "#{ 10 div 5 }", + "#{ 5 div 10 }", + "#{ 10 / 5 / 2 }", + + // long + "#{ 10 / 5L }", + "#{ 5l / 10 }", + "#{ 10L div 5l }", + "#{ 5 div 10L }", + "#{ 10L div 5 / 2 }", + + // float + "#{ 10f / 5 }", + "#{ 5 / 10f }", + "#{ 10f div 5F }", + "#{ 5 div 10F }", + + // double + "#{ 10d / 5 }", + "#{ 5 / 10d }", + "#{ 10d div 5D }", + "#{ 5 div 10D }", + + // mixed + "#{ 10d / 5f }", + "#{ 5L / 10d }", + "#{ 10L div 5f }" + ) + + expect: + results[0] instanceof Integer && results[0] == 2 + results[1] instanceof Integer && results[1] == 0 + results[2] instanceof Integer && results[2] == 2 + results[3] instanceof Integer && results[3] == 0 + results[4] instanceof Integer && results[4] == 1 + + results[5] instanceof Long && results[5] == 2 + results[6] instanceof Long && results[6] == 0 + results[7] instanceof Long && results[7] == 2 + results[8] instanceof Long && results[8] == 0 + results[9] instanceof Long && results[9] == 1 + + results[10] instanceof Float && results[10] == 10f / 5 + results[11] instanceof Float && results[11] == 5 / 10f + results[12] instanceof Float && results[12] == 10f / 5F + results[13] instanceof Float && results[13] == 5 / 10F + + results[14] instanceof Double && results[14] == 10d / 5 + results[15] instanceof Double && results[15] == 5 / 10d + results[16] instanceof Double && results[16] == 10d / 5D + results[17] instanceof Double && results[17] == 5 / 10D + + results[18] instanceof Double && results[18] == 10d / 5f + results[19] instanceof Double && results[19] == 5L / 10d + results[20] instanceof Float && results[20] == 10L / 5f + } + + void "test '%' operator"() { + given: + List results = evaluateMultiple( + // int + "#{ 10 % 5 }", // 0 + "#{ 5 % 10 }", // 1 + "#{ 10 mod 5 }", // 2 + "#{ 5 mod 10 }", // 3 + "#{ 10 % 5 % 3}", // 4 + + // long + "#{ 10 % 5L }", // 5 + "#{ 5l % 10 }", // 6 + "#{ 10L mod 5l }", // 7 + "#{ 5 mod 10L }", // 8 + "#{ 13L % 5 mod 2 }", // 9 + + // float + "#{ 10f % 5 }", // 10 + "#{ 5 % 10f }", // 11 + "#{ 10f mod 5F }", // 12 + "#{ 5 mod 10F }", // 13 + + // double + "#{ 10d % 5 }", // 14 + "#{ 5 % 10d }", // 15 + "#{ 10d mod 5D }", // 16 + "#{ 5 mod 10D }", // 17 + + // mixed + "#{ 10d % 5f }", // 18 + "#{ 5L % 10d }", // 19 + "#{ 10L mod 5f }" // 20 + ) + + + expect: + results[0] instanceof Integer && results[0] == 0 + results[1] instanceof Integer && results[1] == 5 + results[2] instanceof Integer && results[2] == 0 + results[3] instanceof Integer && results[3] == 5 + results[4] instanceof Integer && results[4] == 0 + + results[5] instanceof Long && results[5] == 0 + results[6] instanceof Long && results[6] == 5 + results[7] instanceof Long && results[7] == 0 + results[8] instanceof Long && results[8] == 5 + results[9] instanceof Long && results[9] == 1 + + results[10] instanceof Float && results[10] == 10f % 5 + results[11] instanceof Float && results[11] == 5 % 10f + results[12] instanceof Float && results[12] == 10f % 5F + results[13] instanceof Float && results[13] == 5 % 10F + + results[14] instanceof Double && results[14] == 10d % 5 + results[15] instanceof Double && results[15] == 5 % 10d + results[16] instanceof Double && results[16] == 10d % 5D + results[17] instanceof Double && results[17] == 5 % 10D + + results[18] instanceof Double && results[18] == 10d % 5f + results[19] instanceof Double && results[19] == 5L % 10d + results[20] instanceof Float && results[20] == 10L % 5f + } + + void "test -' operator"() { + given: + List results = evaluateMultiple( + // int + "#{ 10 - 5 }", // 0 + "#{ 5 - 10 }", // 1 + "#{ 25 - 5 - 10 }", // 2 + + // long + "#{ 10 - 5L }", // 3 + "#{ 5l - 10 }", // 4 + "#{ 10L - 5l }", // 5 + "#{ 5 - 10L }", // 6 + + + // float + "#{ 10f - 5 }", // 7 + "#{ 5 - 10f }", // 8 + "#{ 10f - 5F }", // 9 + "#{ 5 - 10F }", // 10 + + // double + "#{ 10d - 5 }", // 11 + "#{ 5 - 10d }", // 12 + "#{ 10d - 5D }", // 13 + "#{ 5 - 10D }", // 14 + + // mixed + "#{ 10d - 5f }", // 15 + "#{ 5L - 10d }", // 16 + "#{ 10L - 5f }" // 17 + ) + + expect: + results[0] instanceof Integer && results[0] == 5 + results[1] instanceof Integer && results[1] == -5 + results[2] instanceof Integer && results[2] == 10 + + results[3] instanceof Long && results[3] == 10 - 5L + results[4] instanceof Long && results[4] == 5l - 10 + results[5] instanceof Long && results[5] == 10L - 5l + results[6] instanceof Long && results[6] == 5 - 10L + + results[7] instanceof Float && results[7] == 10f - 5 + results[8] instanceof Float && results[8] == 5 - 10f + results[9] instanceof Float && results[9] == 10f - 5F + results[10] instanceof Float && results[10] == 5 - 10F + + results[11] instanceof Double && results[11] == 10d - 5 + results[12] instanceof Double && results[12] == 5 - 10d + results[13] instanceof Double && results[13] == 10d - 5D + results[14] instanceof Double && results[14] == 5 - 10D + + results[15] instanceof Double && results[15] == 10d - 5f + results[16] instanceof Double && results[16] == 5L - 10d + results[17] instanceof Float && results[17] == 10L - 5f + } + + void "test '*' operator"() { + given: + List results = evaluateMultiple( + // int + "#{ 10 * 5 }", // 0 + "#{ -8 * 10 * 5 }", // 1 + + // long + "#{ 10 * 5L }", // 2 + "#{ 5l * 10 }", // 3 + "#{ 10L * 5l }", // 4 + "#{ 5 * 10L }", // 5 + + // float + "#{ 10f * 5 }", // 6 + "#{ 5 * 10f }", // 7 + "#{ 10f * 5F }", // 8 + "#{ 5 * 10F }", // 9 + + // double + "#{ 10d * 5 }", // 10 + "#{ 5 * 10d }", // 11 + "#{ 10d * 5D }", // 12 + "#{ 5 * 10D }", // 13 + + // mixed + "#{ 10d * 5f }", // 14 + "#{ 5L * 10d }", // 15 + "#{ 10L * 5f }" // 16 + ) + + expect: + results[0] instanceof Integer && results[0] == 50 + results[1] instanceof Integer && results[1] == -8 * 10 * 5 + + results[2] instanceof Long && results[2] == 10 * 5L + results[3] instanceof Long && results[3] == 5l * 10 + results[4] instanceof Long && results[4] == 10L * 5l + results[5] instanceof Long && results[5] == 5 * 10L + + results[6] instanceof Float && results[6] == 10f * 5 + results[7] instanceof Float && results[7] == 5 * 10f + results[8] instanceof Float && results[8] == 10f * 5F + results[9] instanceof Float && results[9] == 5 * 10F + + results[10] instanceof Double && results[10] == 10d * 5 + results[11] instanceof Double && results[11] == 5 * 10d + results[12] instanceof Double && results[12] == 10d * 5D + results[13] instanceof Double && results[13] == 5 * 10D + + results[14] instanceof Double && results[14] == 10d * 5f + results[15] instanceof Double && results[15] == 5L * 10d + results[16] instanceof Float && results[16] == 10L * 5f + } + + void "test '+' operator"() { + given: + List results = evaluateMultiple( + // int + "#{ 10 + 5 }", // 0 + "#{ -8 + 10 + 5 }", // 1 + + // long + "#{ 10 + 5L }", // 2 + "#{ 5l + 10 }", // 3 + "#{ 10L + 5l }", // 4 + "#{ 5 + 10L }", // 5 + + // float + "#{ 10f + 5 }", // 6 + "#{ 5 + 10f }", // 7 + "#{ 10f + 5F }", // 8 + "#{ 5 + 10F }", // 9 + + // double + "#{ 10d + 5 }", // 10 + "#{ 5 + 10d }", // 11 + "#{ 10d + 5D }", // 12 + "#{ 5 + 10D }", // 13 + + // mixed + "#{ 10d + 5f }", // 14 + "#{ 5L + 10d }", // 15 + "#{ 10L + 5f }", // 16 + + // string + "#{ '1' + '2' }", // 17 + "#{ '1' + null }", // 18 + "#{ null + '1' }", // 19 + "#{ null + '1' + null + 2D }", // 20 + "#{ 15 + 'str' + 2L }", // 21 + "#{ 2f + 'str' + 2 }", // 22 + "#{ .014 + 'str' + 2L + 'test' }", // 23 + "#{ 1 + 2 + 'str' + 2L + 'test' }", // 24 + "#{ 1 + 2 + 3 + 'str' }", // 25 + "#{ 1 + 2 - 3 + 'str' }" // 26 + ) + + expect: + results[0] instanceof Integer && results[0] == 10 + 5 + results[1] instanceof Integer && results[1] == -8 + 10 + 5 + + results[2] instanceof Long && results[2] == 10 + 5L + results[3] instanceof Long && results[3] == 5l + 10 + results[4] instanceof Long && results[4] == 10L + 5l + results[5] instanceof Long && results[5] == 5 + 10L + + results[6] instanceof Float && results[6] == 10f + 5 + results[7] instanceof Float && results[7] == 5 + 10f + results[8] instanceof Float && results[8] == 10f + 5F + results[9] instanceof Float && results[9] == 5 + 10F + + results[10] instanceof Double && results[10] == 10d + 5 + results[11] instanceof Double && results[11] == 5 + 10d + results[12] instanceof Double && results[12] == 10d + 5D + results[13] instanceof Double && results[13] == 5 + 10D + + results[14] instanceof Double && results[14] == 10d + 5f + results[15] instanceof Double && results[15] == 5L + 10d + results[16] instanceof Float && results[16] == 10L + 5f + + results[17] instanceof String && results[17] == '12' + results[18] instanceof String && results[18] == '1null' + results[19] instanceof String && results[19] == 'null1' + results[20] instanceof String && results[20] == 'null1null2.0' + results[21] instanceof String && results[21] == '15str2' + results[22] instanceof String && results[22] == '2.0str2' + results[23] instanceof String && results[23] == '0.014str2test' + results[24] instanceof String && results[24] == '3str2test' + results[25] instanceof String && results[25] == '6str' + results[26] instanceof String && results[26] == '0str' + } + + void "test '>' operator"() { + given: + List results = evaluateMultiple( + // int + "#{ 10 > 5 }", // 0 + "#{ -8 > -3 }", // 1 + + // long + "#{ 10L > 5l }", // 2 + "#{ 5 > 10L }", // 3 + "#{ 10l > 5 }", // 4 + + // double + "#{ 10d > 5 }", // 5 + "#{ 5 > 10d }", // 6 + "#{ 10D > 5d }", // 7 + "#{ -.4 > 5d }", // 8 + "#{ .123 > .1 }", // 9 + "#{ .123 > .1229 }", // 10 + + // float + "#{ 10f > 5 }", // 11 + "#{ 5 > 10f }", // 12 + "#{ 10F > 5f }", // 13 + "#{ -.4f > 5f }", // 14 + "#{ .123f > -5f }", // 15 + + // mixed + "#{ 10f > 5d }", // 16 + "#{ 5L > 10f }", // 17 + "#{ 10L > 5D }", // 18 + "#{ -.4 > 5l }", // 19 + "#{ .123f > -5 }", // 20 + "#{ 10L > 11 }" // 21 + ) + + expect: + results[0] instanceof Boolean && results[0] == true + results[1] instanceof Boolean && results[1] == false + + results[2] instanceof Boolean && results[2] == true + results[3] instanceof Boolean && results[3] == false + results[4] instanceof Boolean && results[4] == true + + results[5] instanceof Boolean && results[5] == true + results[6] instanceof Boolean && results[6] == false + results[7] instanceof Boolean && results[7] == true + results[8] instanceof Boolean && results[8] == false + results[9] instanceof Boolean && results[9] == true + results[10] instanceof Boolean && results[10] == true + + results[11] instanceof Boolean && results[11] == true + results[12] instanceof Boolean && results[12] == false + results[13] instanceof Boolean && results[13] == true + results[14] instanceof Boolean && results[14] == false + results[15] instanceof Boolean && results[15] == true + + results[16] instanceof Boolean && results[16] == true + results[17] instanceof Boolean && results[17] == false + results[18] instanceof Boolean && results[18] == true + results[19] instanceof Boolean && results[19] == false + results[20] instanceof Boolean && results[20] == true + results[21] instanceof Boolean && results[21] == false + } + + void "test '<' operator"() { + given: + List results = evaluateMultiple( + // int + "#{ 10 < 5 }", // 0 + "#{ -8 < -3 }", // 1 + + // long + "#{ 10L < 5l }", // 2 + "#{ 5 < 10L }", // 3 + "#{ 10l < 5 }", // 4 + + // double + "#{ 10d < 5 }", // 5 + "#{ 5 < 10d }", // 6 + "#{ 10D < 5d }", // 7 + "#{ -.4 < 5d }", // 8 + "#{ .123 < .1 }", // 9 + "#{ .1229 < .123 }", // 10 + + // float + "#{ 10f < 5 }", // 11 + "#{ 5 < 10f }", // 12 + "#{ 10F < 5f }", // 13 + "#{ -.4f < 5f }", // 14 + "#{ .123f < -5f }", // 15 + + // mixed + "#{ 10f < 5d }", // 16 + "#{ 5L < 10f }", // 17 + "#{ 10L < 5D }", // 18 + "#{ -.4 < 5l }", // 19 + "#{ .123f < -5 }", // 20 + "#{ 10L < 11 }" // 21 + ) + + expect: + results[0] instanceof Boolean && results[0] == false + results[1] instanceof Boolean && results[1] == true + + results[2] instanceof Boolean && results[2] == false + results[3] instanceof Boolean && results[3] == true + results[4] instanceof Boolean && results[4] == false + + results[5] instanceof Boolean && results[5] == false + results[6] instanceof Boolean && results[6] == true + results[7] instanceof Boolean && results[7] == false + results[8] instanceof Boolean && results[8] == true + results[9] instanceof Boolean && results[9] == false + results[10] instanceof Boolean && results[10] == true + + results[11] instanceof Boolean && results[11] == false + results[12] instanceof Boolean && results[12] == true + results[13] instanceof Boolean && results[13] == false + results[14] instanceof Boolean && results[14] == true + results[15] instanceof Boolean && results[15] == false + + results[16] instanceof Boolean && results[16] == false + results[17] instanceof Boolean && results[17] == true + results[18] instanceof Boolean && results[18] == false + results[19] instanceof Boolean && results[19] == true + results[20] instanceof Boolean && results[20] == false + results[21] instanceof Boolean && results[21] == true + } + + void "test '>=' operator"() { + given: + List results = evaluateMultiple( + // int + "#{ 10 >= 5 }", // 0 + "#{ -8 >= -3 }", // 1 + "#{ 3 >= 3 }", // 2 + + // long + "#{ 10L >= 5l }", // 3 + "#{ 5 >= 10L }", // 4 + "#{ 10l >= 5 }", // 5 + "#{ 5l >= 5 }", // 6 + + // double + "#{ 10d >= 5 }", // 7 + "#{ 5 >= 10d }", // 8 + "#{ 10D >= 5d }", // 9 + "#{ -.4 >= 5d }", // 10 + "#{ .123 >= .1 }", // 11 + + // float + "#{ 10f >= 5 }", // 12 + "#{ 5 >= 10f }", // 13 + "#{ 10F >= 5f }", // 14 + "#{ -.4f >= 5f }", // 15 + "#{ .123f >= -5f }", // 16 + + // mixed + "#{ 10f >= 5d }", // 17 + "#{ 5L >= 10f }", // 18 + "#{ 10L >= 5D }", // 19 + "#{ -.4 >= 5l }", // 20 + "#{ .123f >= -5 }", // 21 + "#{ 12L >= 11 }", // 22 + "#{ 11 >= 11L }" // 23 + ) + + expect: + results[0] instanceof Boolean && results[0] == true + results[1] instanceof Boolean && results[1] == false + results[2] instanceof Boolean && results[2] == true + + results[3] instanceof Boolean && results[3] == true + results[4] instanceof Boolean && results[4] == false + results[5] instanceof Boolean && results[5] == true + results[6] instanceof Boolean && results[6] == true + + results[7] instanceof Boolean && results[7] == true + results[8] instanceof Boolean && results[8] == false + results[9] instanceof Boolean && results[9] == true + results[10] instanceof Boolean && results[10] == false + results[11] instanceof Boolean && results[11] == true + + results[12] instanceof Boolean && results[12] == true + results[13] instanceof Boolean && results[13] == false + results[14] instanceof Boolean && results[14] == true + results[15] instanceof Boolean && results[15] == false + results[16] instanceof Boolean && results[16] == true + + results[17] instanceof Boolean && results[17] == true + results[18] instanceof Boolean && results[18] == false + results[19] instanceof Boolean && results[19] == true + results[20] instanceof Boolean && results[20] == false + results[21] instanceof Boolean && results[21] == true + results[22] instanceof Boolean && results[22] == true + results[23] instanceof Boolean && results[23] == true + } + + void "test '<=' operator"() { + given: + List results = evaluateMultiple( + // int + "#{ 10 <= 5 }", // 0 + "#{ -8 <= -3 }", // 1 + "#{ 3 <= 3 }", // 2 + + // long + "#{ 10L <= 5l }", // 3 + "#{ 5 <= 10L }", // 4 + "#{ 10l <= 5 }", // 5 + "#{ 5l <= 5 }", // 6 + + // double + "#{ 10d <= 5 }", // 7 + "#{ 5 <= 10d }", // 8 + "#{ 10D <= 5d }", // 9 + "#{ -.4 <= 5d }", // 10 + "#{ .123 <= .1 }", // 11 + + // float + "#{ 10f <= 5 }", // 12 + "#{ 5 <= 10f }", // 13 + "#{ 10F <= 5f }", // 14 + "#{ -.4f <= 5f }", // 15 + "#{ .123f <= -5f }", // 16 + + // mixed + "#{ 10f <= 5d }", // 17 + "#{ 5L <= 10f }", // 18 + "#{ 10L <= 5D }", // 19 + "#{ -.4 <= 5l }", // 20 + "#{ .123f <= -5 }", // 21 + "#{ 12L <= 11 }", // 22 + "#{ 11 <= 11L }" // 23 + ) + + expect: + results[0] instanceof Boolean && results[0] == false + results[1] instanceof Boolean && results[1] == true + results[2] instanceof Boolean && results[2] == true + + results[3] instanceof Boolean && results[3] == false + results[4] instanceof Boolean && results[4] == true + results[5] instanceof Boolean && results[5] == false + results[6] instanceof Boolean && results[6] == true + + results[7] instanceof Boolean && results[7] == false + results[8] instanceof Boolean && results[8] == true + results[9] instanceof Boolean && results[9] == false + results[10] instanceof Boolean && results[10] == true + results[11] instanceof Boolean && results[11] == false + + results[12] instanceof Boolean && results[12] == false + results[13] instanceof Boolean && results[13] == true + results[14] instanceof Boolean && results[14] == false + results[15] instanceof Boolean && results[15] == true + results[16] instanceof Boolean && results[16] == false + + results[17] instanceof Boolean && results[17] == false + results[18] instanceof Boolean && results[18] == true + results[19] instanceof Boolean && results[19] == false + results[20] instanceof Boolean && results[20] == true + results[21] instanceof Boolean && results[21] == false + results[22] instanceof Boolean && results[22] == false + results[23] instanceof Boolean && results[23] == true + } + + void "test '==' operator"() { + given: + List results = evaluateMultiple( + // int + "#{ 10 == 10}", // 0 + "#{ 8 == 11 }", // 1 + "#{ -3 == 3}", // 2 + "#{ -15 == -15 }", // 3 + + // long + "#{ 10L == 10L}", // 4 + "#{ 8L == 11L }", // 5 + "#{ -3L == 3L }", // 6 + "#{ -15L == -15L }", // 7 + + // float + "#{ 1f == 1f}", // 8 + "#{ 0f == 1f }", // 9 + + // double + "#{ 1d == 1.0 }", // 10 + "#{ .0 == 1d }", // 11 + + // string + "#{ 'str' == 'str' }", // 12 + "#{ 'str1' == 'str2' }" // 13 + ) + + + expect: + results[0] instanceof Boolean && results[0] == true + results[1] instanceof Boolean && results[1] == false + results[2] instanceof Boolean && results[2] == false + results[3] instanceof Boolean && results[3] == true + + results[4] instanceof Boolean && results[4] == true + results[5] instanceof Boolean && results[5] == false + results[6] instanceof Boolean && results[6] == false + results[7] instanceof Boolean && results[7] == true + + results[8] instanceof Boolean && results[8] == true + results[9] instanceof Boolean && results[9] == false + + results[10] instanceof Boolean && results[10] == true + results[11] instanceof Boolean && results[11] == false + + results[12] instanceof Boolean && results[12] == true + results[13] instanceof Boolean && results[13] == false + } + + void "test '!=' operator"() { + given: + List results = evaluateMultiple( + // int + "#{ 10 != 10}", // 0 + "#{ 8 != 11 }", // 1 + "#{ -3 != 3}", // 2 + "#{ -15 != -15 }", // 3 + + // long + "#{ 10L != 10L}", // 4 + "#{ 8L != 11L }", // 5 + "#{ -3L != 3L }", // 6 + "#{ -15L != -15L }", // 7 + + // float + "#{ 1f != 1f}", // 8 + "#{ 0f != 1f }", // 9 + + // double + "#{ 1d != 1.0 }", // 10 + "#{ .0 != 1d }", // 11 + + // string + "#{ 'str' != 'str' }", // 12 + "#{ 'str1' != 'str2' }" // 13 + ) + + expect: + results[0] instanceof Boolean && results[0] == false + results[1] instanceof Boolean && results[1] == true + results[2] instanceof Boolean && results[2] == true + results[3] instanceof Boolean && results[3] == false + + results[4] instanceof Boolean && results[4] == false + results[5] instanceof Boolean && results[5] == true + results[6] instanceof Boolean && results[6] == true + results[7] instanceof Boolean && results[7] == false + + results[8] instanceof Boolean && results[8] == false + results[9] instanceof Boolean && results[9] == true + + results[10] instanceof Boolean && results[10] == false + results[11] instanceof Boolean && results[11] == true + + results[12] instanceof Boolean && results[12] == false + results[13] instanceof Boolean && results[13] == true + } + + // '^' operator in expressions means power operation + void "test '^' operator"() { + given: + List results = evaluateMultiple( + "#{ 2^3 }", // 0 + "#{ 3L ^ 2}", // 1 + "#{ 2.0^0}", // 2 + "#{ 2f ^ 2L}", // 3 + "#{ 2^2 ^2}", // 4 + "#{ (2 ^ 32)^2}" // 5 + ) + + expect: + results[0] instanceof Long && results[0] == 8 + results[1] instanceof Long && results[1] == 9 + results[2] instanceof Double && results[2] == 1.0 + results[3] instanceof Double && results[3] == 4.0 + results[4] instanceof Long && results[4] == 16 + results[5] instanceof Long && results[5] == 9223372036854775807L + } + + void "test '&&' operator"() { + given: + List results = evaluateMultiple( + "#{ true && true }", // 0 + "#{ true && false }", // 1 + "#{ false && true }", // 2 + "#{ false && false }", // 3 + "#{ true and true }", // 4 + "#{ true and false }", // 5 + "#{ false and true }", // 6 + "#{ false and false }", // 7 + "#{ true and true && true }", // 8 + "#{ true && true and false }" // 9 + ) + + expect: + results[0] instanceof Boolean && results[0] == true + results[1] instanceof Boolean && results[1] == false + results[2] instanceof Boolean && results[2] == false + results[3] instanceof Boolean && results[3] == false + results[4] instanceof Boolean && results[4] == true + results[5] instanceof Boolean && results[5] == false + results[6] instanceof Boolean && results[6] == false + results[7] instanceof Boolean && results[7] == false + results[8] instanceof Boolean && results[8] == true + results[9] instanceof Boolean && results[9] == false + } + + void "test '||' operator"() { + given: + List results = evaluateMultiple( + "#{ true || true }", // 0 + "#{ true || false }", // 1 + "#{ false || true }", // 2 + "#{ false || false }", // 3 + "#{ true or true }", // 4 + "#{ true or false }", // 5 + "#{ false or true }", // 6 + "#{ false or false }", // 7 + "#{ true or true || true }", // 8 + "#{ true or true or false }", // 9 + "#{ true || false or false }", // 10 + "#{ false or false || false or true }" // 11 + ) + + expect: + results[0] instanceof Boolean && results[0] == true + results[1] instanceof Boolean && results[1] == true + results[2] instanceof Boolean && results[2] == true + results[3] instanceof Boolean && results[3] == false + results[4] instanceof Boolean && results[4] == true + results[5] instanceof Boolean && results[5] == true + results[6] instanceof Boolean && results[6] == true + results[7] instanceof Boolean && results[7] == false + results[8] instanceof Boolean && results[8] == true + results[9] instanceof Boolean && results[9] == true + results[10] instanceof Boolean && results[10] == true + results[11] instanceof Boolean && results[11] == true + } + + void "test 'instanceof' operator"() { + given: + List results = evaluateMultiple( + "#{ 1 instanceof T(java.lang.Integer) }", // 0 + "#{ 1 instanceof T(Integer) }", // 1 + "#{ 1L instanceof T(java.lang.Long) }", // 2 + "#{ 1f instanceof T(java.lang.Float) }", // 3 + "#{ 1d instanceof T(java.lang.Double) }", // 4 + "#{ 'str' instanceof T(java.lang.String) }", // 5 + "#{ 'str' instanceof T(Double) }", // 6 + "#{ 1L instanceof T(Integer) }", // 7 + "#{ 1f instanceof T(Double) }" // 8 + ) + + + expect: + results[0] instanceof Boolean && results[0] == true + results[1] instanceof Boolean && results[1] == true + results[2] instanceof Boolean && results[2] == true + results[3] instanceof Boolean && results[3] == true + results[4] instanceof Boolean && results[4] == true + results[5] instanceof Boolean && results[5] == true + results[6] instanceof Boolean && results[6] == false + results[7] instanceof Boolean && results[7] == false + results[8] instanceof Boolean && results[8] == false + } + + void "test 'matches' operator"() { + given: + List results = evaluateMultiple( + "#{ '123' matches '\\\\d+' }", // 0 + "#{ '5.0' matches '[0-9]*\\\\.[0-9]+(d|D)?' }", // 1 + "#{ '5.0' matches '[0-9]*\\\\.[0-9]+(d|D)' }", // 2 + "#{ 'AbC' matches '[A-Za-z]*' }", // 3 + "#{ ' ' matches '\\\\s*' }", // 4 + "#{ '' matches '\\\\s+' }" // 5 + ) + + expect: + results[0] instanceof Boolean && results[0] == true + results[1] instanceof Boolean && results[1] == true + results[2] instanceof Boolean && results[2] == false + results[3] instanceof Boolean && results[3] == true + results[4] instanceof Boolean && results[4] == true + results[5] instanceof Boolean && results[5] == false + } + + void "test '!' operator"() { + given: + List results = evaluateMultiple( + "#{ !true }", // 0 + "#{ !false }", // 1 + "#{ !!true }", // 2 + "#{ !!false }", // 3 + "#{ !!!false }", // 4 + "#{ !!!true }" // 5 + ) + + expect: + results[0] == false + results[1] == true + results[2] == true + results[3] == false + results[4] == true + results[5] == false + } + + void "test unary '-'"() { + given: + List results = evaluateMultiple( + "#{ -5 }", + "#{ -(-3) }", + "#{ -3L }", + "#{ -1.0D }" + ) + + expect: + results[0] instanceof Integer && results[0] == -5 + results[1] instanceof Integer && results[1] == 3 + results[2] instanceof Long && results[2] == -3 + results[3] instanceof Double && results[3] == -1.0 + + when: + evaluate("#{ --5 }") + + then: + thrown(Throwable.class) + } + + void "test unary '+'"() { + given: + List results = evaluateMultiple( + "#{ +5 }", + "#{ +(+3) }", + "#{ +3L }", + "#{ +1.0D }" + ) + + expect: + results[0] instanceof Integer && results[0] == 5 + results[1] instanceof Integer && results[1] == 3 + results[2] instanceof Long && results[2] == 3 + results[3] instanceof Double && results[3] == 1.0 + + when: + evaluate("#{ ++5 }") + + then: + thrown(Throwable.class) + } + + void "test parenthesized expressions"() { + given: + List results = evaluateMultiple( + "#{ (2 + 3) * 5 }", // 0 + "#{ 2 * ( 3 + 1) }", // 1 + "#{ (-1 + 9) * (2 + 3) }", // 2 + "#{ 2 ^ ( 2 + 2) }", // 3 + "#{ 5 * ( 2d * (1 - 1)) }", // 4 + "#{ 5L * ( 2d + 2f ) }" // 5 + ) + + expect: + results[0] instanceof Integer && results[0] == 25 + results[1] instanceof Integer && results[1] == 8 + results[2] instanceof Integer && results[2] == 40 + results[3] instanceof Long && results[3] == 16 + results[4] instanceof Double && results[4] == 0 + results[5] instanceof Double && results[5] == 20.0 + } + +} diff --git a/inject-groovy/src/test/groovy/io/micronaut/expressions/TernaryOperationExpressionsSpec.groovy b/inject-groovy/src/test/groovy/io/micronaut/expressions/TernaryOperationExpressionsSpec.groovy new file mode 100644 index 00000000000..b50798d6809 --- /dev/null +++ b/inject-groovy/src/test/groovy/io/micronaut/expressions/TernaryOperationExpressionsSpec.groovy @@ -0,0 +1,36 @@ +package io.micronaut.expressions + +import io.micronaut.ast.transform.test.AbstractEvaluatedExpressionsSpec + +class TernaryOperationExpressionsSpec extends AbstractEvaluatedExpressionsSpec { + + void "test ternary operator"() { + given: + List results = evaluateMultiple( + "#{ 15 > 10 ? 'a' : 'b' }", + "#{ 15 == 10 ? 'a' : 'b' }", + "#{ 10 > 9 ? 'a' + 'b' : 'b' + 'a' }" + ) + + expect: + results[0] instanceof String && results[0] == 'a' + results[1] instanceof String && results[1] == 'b' + results[2] instanceof String && results[2] == 'ab' + } + + void "test ternary type resolution"() { + given: + List results = evaluateMultiple( + "#{ (15 > 10 ? 'a' : 'b').length() }", + "#{ 15 > 10 ? 15L : 'test' }", + "#{ (15 > 10 ? 15L : 10) + 8}", + "#{ 10 > 15 ? 15L : true}" + ) + + expect: + results[0] instanceof Integer && results[0] == 1 + results[1] instanceof Long && results[1] == 15 + results[2] instanceof Long && results[2] == 23 + results[3] instanceof Boolean && results[3] == true + } +} diff --git a/inject-groovy/src/test/groovy/io/micronaut/expressions/TestExpressionsInjectionSpec.groovy b/inject-groovy/src/test/groovy/io/micronaut/expressions/TestExpressionsInjectionSpec.groovy new file mode 100644 index 00000000000..f5faa5166de --- /dev/null +++ b/inject-groovy/src/test/groovy/io/micronaut/expressions/TestExpressionsInjectionSpec.groovy @@ -0,0 +1,166 @@ +package io.micronaut.expressions + +import io.micronaut.ast.transform.test.AbstractBeanDefinitionSpec +import io.micronaut.context.env.PropertySource +import io.micronaut.context.exceptions.NoSuchBeanException + +class TestExpressionsInjectionSpec extends AbstractBeanDefinitionSpec { + + void "test expression field injection"() { + given: + def ctx = buildContext(""" + package test + + import jakarta.inject.Singleton + import io.micronaut.context.annotation.Value + + @Singleton + class Expr { + @Value("#{ 15 ^ 2 }") + int intValue + + @Value("#{ 100 }") + Integer boxedIntValue + + @Value("#{ T(String).join(',', 'a', 'b', 'c') }") + String strValue + + @Value("#{null}") + Object nullValue + } + """) + + def type = ctx.classLoader.loadClass('test.Expr') + def bean = ctx.getBean(type) + + expect: + bean.intValue == 225 + bean.boxedIntValue == 100 + bean.strValue == 'a,b,c' + bean.nullValue == null + + cleanup: + ctx.close() + } + + void "test expression constructor injection"() { + given: + def ctx = buildContext(""" + package test + + import jakarta.inject.Singleton + import io.micronaut.context.annotation.Value + + @Singleton + class Expr { + private Integer wrapper; + private int primitive; + + Expr(@Value("#{ 25 }") Integer wrapper, + @Value("#{ 23 }") int primitive) { + this.wrapper = wrapper + this.primitive = primitive + } + } + """) + + def type = ctx.classLoader.loadClass('test.Expr') + def bean = ctx.getBean(type) + + expect: + bean.wrapper == 25 + bean.primitive == 23 + + cleanup: + ctx.close() + } + + void "test expression setter injection"() { + given: + def ctx = buildContext(""" + package test + + import jakarta.inject.Inject; + import jakarta.inject.Singleton + import io.micronaut.context.annotation.Value + + @Singleton + class Expr { + private Integer wrapper; + private int primitive; + + @Inject + public void setWrapper(@Value("#{ 25 }") Integer value) { + this.wrapper = value; + } + + @Inject + public void setPrimitive(@Value("#{ 23 }") int value) { + this.primitive = value; + } + } + """) + + def type = ctx.classLoader.loadClass('test.Expr') + def bean = ctx.getBean(type) + + expect: + bean.wrapper == 25 + bean.primitive == 23 + + cleanup: + ctx.close() + } + + + void "test expressions in @Factory injection"() { + given: + def ctx = buildContext(""" + package test + + import io.micronaut.context.annotation.Bean + import io.micronaut.context.annotation.Factory + import jakarta.inject.Inject + import jakarta.inject.Singleton + import io.micronaut.context.annotation.Value + + @jakarta.inject.Singleton + class Context { + String contextValue = "context value" + } + + class Expr { + private final Integer wrapper + private final int primitive + private final String contextValue + + Expr(Integer wrapper, int primitive, String contextValue) { + this.wrapper = wrapper + this.primitive = primitive + this.contextValue = contextValue + } + } + + @Factory + class TestFactory { + @Bean + Expr factoryBean(@Value('#{ 25 }') Integer wrapper, + @Value('#{ 23 }') int primitive, + @Value('#{ #contextValue }') String contextValue) { + return new Expr(wrapper, primitive, contextValue) + } + } + """) + + def type = ctx.classLoader.loadClass('test.Expr') + def bean = ctx.getBean(type) + + expect: + bean.wrapper == 25 + bean.primitive == 23 + bean.contextValue == "context value" + + cleanup: + ctx.close() + } +} diff --git a/inject-groovy/src/test/groovy/io/micronaut/expressions/TestExpressionsUsageSpec.groovy b/inject-groovy/src/test/groovy/io/micronaut/expressions/TestExpressionsUsageSpec.groovy new file mode 100644 index 00000000000..8391585955a --- /dev/null +++ b/inject-groovy/src/test/groovy/io/micronaut/expressions/TestExpressionsUsageSpec.groovy @@ -0,0 +1,130 @@ +package io.micronaut.expressions + +import io.micronaut.ast.transform.test.AbstractBeanDefinitionSpec +import io.micronaut.context.env.PropertySource +import io.micronaut.context.exceptions.CircularDependencyException +import io.micronaut.context.exceptions.ExpressionEvaluationException +import io.micronaut.context.exceptions.NoSuchBeanException +import io.micronaut.core.expressions.ExpressionEvaluationContext + +class TestExpressionsUsageSpec extends AbstractBeanDefinitionSpec { + + void "test expression array in requires"() { + given: + def ctx = buildContext(""" + package test + import io.micronaut.context.annotation.Requires + import jakarta.inject.Singleton + + @Singleton + @Requires(env = ["#{ 'test' }"]) + class Expr { + } + """) + + when: + getBean(ctx, "test.Expr") + + then: + noExceptionThrown() + + cleanup: + ctx.close() + } + + void "test requires expression property value"() { + given: + def ctx = buildContext(""" + package test + import io.micronaut.context.annotation.Requires + import jakarta.inject.Singleton + + @Singleton + @Requires(property = 'test-property', value = "#{ 'test-value'.toUpperCase() }") + class Expr { + } + """) + + def type = ctx.classLoader.loadClass('test.Expr') + + when: + ctx.environment.addPropertySource(PropertySource.of("test", ['test-property': 'TEST-VALUE'])) + ctx.getBean(type) + + then: + noExceptionThrown() + + cleanup: + ctx.close() + } + + void "test requires expression context value"() { + given: + def ctx = buildContext(""" + package test + + import io.micronaut.context.annotation.ConfigurationProperties + import io.micronaut.context.annotation.Requires + + import jakarta.inject.Singleton + + @Singleton + @Requires(property = 'test.enabled', value = "#{ #enabled }") + class Expr { + } + + @ConfigurationProperties('test') + @jakarta.inject.Singleton + class Context { + boolean enabled + } + """) + + def type = ctx.classLoader.loadClass('test.Expr') + + when: + ctx.environment.addPropertySource(PropertySource.of("test", ['test.enabled': false])) + ctx.getBean(type) + + then: + noExceptionThrown() + + cleanup: + ctx.close() + } + + void "test disabled by expression bean"() { + given: + def ctx = buildContext(""" + package test + + import io.micronaut.context.annotation.ConfigurationProperties + import io.micronaut.context.annotation.Requires + + import jakarta.inject.Singleton + + @Singleton + @Requires(property = 'test.property', value = "#{ 5 * 2 }") + class Expr { + } + + @ConfigurationProperties('test') + @jakarta.inject.Singleton + class Context { + boolean enabled + } + """) + + def type = ctx.classLoader.loadClass('test.Expr') + + when: + ctx.environment.addPropertySource(PropertySource.of("test", ['test.property': 15])) + ctx.getBean(type) + + then: + thrown(NoSuchBeanException) + + cleanup: + ctx.close() + } +} diff --git a/inject-groovy/src/test/groovy/io/micronaut/expressions/TypeIdentifierExpressionsSpec.groovy b/inject-groovy/src/test/groovy/io/micronaut/expressions/TypeIdentifierExpressionsSpec.groovy new file mode 100644 index 00000000000..6ee0f805f1a --- /dev/null +++ b/inject-groovy/src/test/groovy/io/micronaut/expressions/TypeIdentifierExpressionsSpec.groovy @@ -0,0 +1,52 @@ +package io.micronaut.expressions + +import io.micronaut.ast.transform.test.AbstractEvaluatedExpressionsSpec + +class TypeIdentifierExpressionsSpec extends AbstractEvaluatedExpressionsSpec { + + void "test static methods"() { + given: + List results = evaluateMultiple( + "#{ T(Math).random() }", + "#{ T(java.lang.Math).random() }", + "#{ T(Integer).valueOf('10') }", + "#{ T(String).join(',', '1', '2', '3') }", + "#{ T(String).join(',', 'a', 'b').repeat(2) }" + ) + + expect: + results[0] instanceof Double && results[0] >= 0 && results[0] < 1 + results[1] instanceof Double && results[1] >= 0 && results[1] < 1 + results[2] instanceof Integer && results[2] == 10 + results[3] instanceof String && results[3] == "1,2,3" + results[4] instanceof String && results[4] == "a,ba,b" + } + + void "test type identifier as argument"() { + given: + Object expr1 = evaluateAgainstContext("#{ #getType(T(java.lang.String)) }", + """ + @jakarta.inject.Singleton + class Context { + Class getType(Class type) { + return type + } + } + """) + + Object expr2 = evaluateAgainstContext("#{ #getType(T(String), T(Object)) }", + """ + @jakarta.inject.Singleton + class Context { + Class getType(Class... types) { + return types[1] + } + } + """) + + expect: + expr1 instanceof Class && expr1 == String.class + expr2 instanceof Class && expr2 == Object.class + } + +} diff --git a/inject-groovy/src/test/resources/META-INF/services/io.micronaut.inject.visitor.TypeElementVisitor b/inject-groovy/src/test/resources/META-INF/services/io.micronaut.inject.visitor.TypeElementVisitor index b22d1586918..86de026a342 100644 --- a/inject-groovy/src/test/resources/META-INF/services/io.micronaut.inject.visitor.TypeElementVisitor +++ b/inject-groovy/src/test/resources/META-INF/services/io.micronaut.inject.visitor.TypeElementVisitor @@ -4,3 +4,4 @@ io.micronaut.inject.visitor.AllClassesVisitor io.micronaut.inject.visitor.ControllerGetVisitor io.micronaut.inject.visitor.IntroductionVisitor io.micronaut.inject.visitor.MutatingVisitor +io.micronaut.expressions.ContextRegistrar diff --git a/inject-java-test/src/main/groovy/io/micronaut/annotation/processing/test/AbstractEvaluatedExpressionsSpec.groovy b/inject-java-test/src/main/groovy/io/micronaut/annotation/processing/test/AbstractEvaluatedExpressionsSpec.groovy new file mode 100644 index 00000000000..a58327f9ba3 --- /dev/null +++ b/inject-java-test/src/main/groovy/io/micronaut/annotation/processing/test/AbstractEvaluatedExpressionsSpec.groovy @@ -0,0 +1,156 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.annotation.processing.test + +import io.micronaut.context.expressions.AbstractEvaluatedExpression +import io.micronaut.context.expressions.DefaultExpressionEvaluationContext +import io.micronaut.core.naming.NameUtils +import io.micronaut.core.expressions.EvaluatedExpressionReference +import io.micronaut.expressions.context.ExpressionEvaluationContextRegistrar +import io.micronaut.inject.visitor.TypeElementVisitor +import io.micronaut.inject.visitor.VisitorContext +import org.intellij.lang.annotations.Language + +abstract class AbstractEvaluatedExpressionsSpec extends AbstractTypeElementSpec { + + @Override + protected Collection getLocalTypeElementVisitors() { + return [new ContextRegistrar()] + } + + List evaluateMultiple(String... expressions) { + + String classContent = "" + for (int i = 0; i < expressions.size(); i++) { + classContent += """ + + @Value("${expressions[i]}") + public Object field${i}; + + """ + } + + def cls = """ + package test; + import io.micronaut.context.annotation.Value; + + class Expr { + ${classContent} + } + """.stripIndent().stripLeading() + + def applicationContext = buildContext(cls) + def classLoader = applicationContext.classLoader + + def exprClassName = 'test.$Expr$Expr' + def startingIndex = EvaluatedExpressionReference.nextIndex(exprClassName) - expressions.length + + List result = new ArrayList<>() + for (int i = startingIndex; i < startingIndex + expressions.size(); i++) { + String exprFullName = exprClassName + i + try { + def exprClass = (AbstractEvaluatedExpression) classLoader.loadClass(exprFullName).newInstance() + result.add(exprClass.evaluate(new DefaultExpressionEvaluationContext(null, applicationContext, null))) + } catch (ClassNotFoundException e) { + return null + } + } + + return result + } + + Object evaluate(String expression) { + return evaluateAgainstContext(expression, "") + } + + Object evaluateAgainstContext(String expression, @Language("java") String contextClass) { + String exprFullName = 'test.$Expr$Expr'; + + def cls = """ + package test; + import io.micronaut.context.annotation.Value; + import jakarta.inject.Singleton; + + ${contextClass} + + @Singleton + class Expr { + @Value("${expression}") + public Object field; + } + """.stripIndent().stripLeading() + + def applicationContext = buildContext(cls) + def classLoader = applicationContext.classLoader + + try { + def index = EvaluatedExpressionReference.nextIndex(exprFullName) + def exprClass = (AbstractEvaluatedExpression) classLoader.loadClass(exprFullName + (index == 0 ? index : index - 1)).newInstance() + exprClass.evaluate(new DefaultExpressionEvaluationContext(null, applicationContext, null)) + } catch (ClassNotFoundException e) { + return null + } + } + + Object evaluateSingle(String className, + @Language("java") String cls) { + return evaluateSingle(className, cls, null) + } + + Object evaluateSingle(String className, + @Language("java") String cls, + Object[] args) { + def classSimpleName = NameUtils.getSimpleName(className) + def packageName = NameUtils.getPackageName(className) + def exprClassName = (classSimpleName.startsWith('$') ? '' : '$') + classSimpleName + '$Expr' + + String exprFullName = "${packageName}.${exprClassName}" + + def applicationContext = buildContext(cls) + def classLoader = applicationContext.classLoader + + try { + def index = EvaluatedExpressionReference.nextIndex(exprFullName) + def exprClass = (AbstractEvaluatedExpression) classLoader.loadClass(exprFullName + (index == 0 ? index : index - 1)).newInstance() + return exprClass.evaluate(new DefaultExpressionEvaluationContext(args, applicationContext, null)) + } catch (ClassNotFoundException e) { + return null + } + } + + static class ContextRegistrar implements TypeElementVisitor { + static final List CLASSES = ["test.Context"] + + static void setClasses(String...classes) { + CLASSES.clear() + CLASSES.addAll(classes) + } + + static void reset() { + CLASSES.clear() + CLASSES.add("test.Context") + } + + @Override + void start(VisitorContext visitorContext) { + for (cls in CLASSES) { + visitorContext.getClassElement(cls).ifPresent { + visitorContext.expressionCompilationContextFactory.registerContextClass(it) + } + } + } + } +} diff --git a/inject-java/src/main/java/io/micronaut/annotation/processing/BeanDefinitionInjectProcessor.java b/inject-java/src/main/java/io/micronaut/annotation/processing/BeanDefinitionInjectProcessor.java index c2f80ba9d23..a3f5880abac 100644 --- a/inject-java/src/main/java/io/micronaut/annotation/processing/BeanDefinitionInjectProcessor.java +++ b/inject-java/src/main/java/io/micronaut/annotation/processing/BeanDefinitionInjectProcessor.java @@ -27,7 +27,8 @@ import io.micronaut.core.annotation.Vetoed; import io.micronaut.core.naming.NameUtils; import io.micronaut.core.util.CollectionUtils; -import io.micronaut.inject.annotation.AbstractAnnotationMetadataBuilder; +import io.micronaut.expressions.EvaluatedExpressionWriter; +import io.micronaut.expressions.context.ExpressionWithContext; import io.micronaut.inject.ast.annotation.ElementAnnotationMetadataFactory; import io.micronaut.inject.processing.BeanDefinitionCreator; import io.micronaut.inject.processing.BeanDefinitionCreatorFactory; @@ -258,8 +259,7 @@ public final boolean process(Set annotations, RoundEnviro } } } finally { - AbstractAnnotationMetadataBuilder.clearMutated(); - JavaAnnotationMetadataBuilder.clearCaches(); + BeanDefinitionWriter.finish(); } } @@ -267,7 +267,8 @@ public final boolean process(Set annotations, RoundEnviro } /** - * Writes {@link io.micronaut.inject.BeanDefinitionReference} into /META-INF/services/io.micronaut.inject.BeanDefinitionReference. + * Writes {@link io.micronaut.inject.BeanDefinitionReference} into /META-INF/services/io + * .micronaut.inject.BeanDefinitionReference. */ private void writeBeanDefinitionsToMetaInf() { try { @@ -283,6 +284,9 @@ private void processBeanDefinitions(BeanDefinitionVisitor beanDefinitionWriter) beanDefinitionWriter.visitBeanDefinitionEnd(); if (beanDefinitionWriter.isEnabled()) { beanDefinitionWriter.accept(classWriterOutputVisitor); + + processEvaluatedExpressions(beanDefinitionWriter); + BeanDefinitionReferenceWriter beanDefinitionReferenceWriter = new BeanDefinitionReferenceWriter(beanDefinitionWriter); beanDefinitionReferenceWriter.setRequiresMethodProcessing(beanDefinitionWriter.requiresMethodProcessing()); @@ -301,4 +305,15 @@ private void processBeanDefinitions(BeanDefinitionVisitor beanDefinitionWriter) } } + private void processEvaluatedExpressions(BeanDefinitionVisitor beanDefinitionWriter) throws IOException { + for (ExpressionWithContext expressionMetadata: beanDefinitionWriter.getEvaluatedExpressions()) { + EvaluatedExpressionWriter expressionWriter = new EvaluatedExpressionWriter( + expressionMetadata, + javaVisitorContext, + beanDefinitionWriter.getOriginatingElement()); + + expressionWriter.accept(classWriterOutputVisitor); + } + } + } diff --git a/inject-java/src/main/java/io/micronaut/annotation/processing/JavaAnnotationMetadataBuilder.java b/inject-java/src/main/java/io/micronaut/annotation/processing/JavaAnnotationMetadataBuilder.java index 9b6551463b9..f0c95d2993e 100644 --- a/inject-java/src/main/java/io/micronaut/annotation/processing/JavaAnnotationMetadataBuilder.java +++ b/inject-java/src/main/java/io/micronaut/annotation/processing/JavaAnnotationMetadataBuilder.java @@ -298,6 +298,19 @@ protected Element getAnnotationMember(Element annotationElement, CharSequence me return null; } + @Override + protected String getOriginatingClassName(Element orginatingElement) { + return JavaModelUtils.getClassName(getOriginatingTypeElement(orginatingElement)); + } + + private TypeElement getOriginatingTypeElement(Element element) { + if (element instanceof TypeElement typeElement) { + return typeElement; + } + + return getOriginatingTypeElement(element.getEnclosingElement()); + } + @Override protected Optional> getAnnotationValues(Element originatingElement, Element member, Class annotationType) { List annotationMirrors = member.getAnnotationMirrors(); @@ -328,8 +341,11 @@ protected void readAnnotationRawValues( if (memberName != null && annotationValue instanceof javax.lang.model.element.AnnotationValue && !annotationValues.containsKey(memberName)) { final MetadataAnnotationValueVisitor resolver = new MetadataAnnotationValueVisitor(originatingElement, (ExecutableElement) member); ((javax.lang.model.element.AnnotationValue) annotationValue).accept(resolver, this); - final Object resolvedValue = resolver.resolvedValue; + Object resolvedValue = resolver.resolvedValue; if (resolvedValue != null) { + if (isEvaluatedExpression(resolvedValue)) { + resolvedValue = buildEvaluatedExpressionReference(originatingElement, annotationName, memberName, resolvedValue); + } validateAnnotationValue(originatingElement, annotationName, member, memberName, resolvedValue); annotationValues.put(memberName, resolvedValue); } @@ -364,13 +380,16 @@ private boolean isValidationRequired(List annotation } @Override - protected Object readAnnotationValue(Element originatingElement, Element member, String memberName, Object annotationValue) { + protected Object readAnnotationValue(Element originatingElement, Element member, String annotationName, String memberName, Object annotationValue) { if (memberName != null && annotationValue instanceof javax.lang.model.element.AnnotationValue) { final MetadataAnnotationValueVisitor visitor = new MetadataAnnotationValueVisitor(originatingElement, (ExecutableElement) member); ((javax.lang.model.element.AnnotationValue) annotationValue).accept(visitor, this); return visitor.resolvedValue; } else if (memberName != null && annotationValue != null && ClassUtils.isJavaLangType(annotationValue.getClass())) { // only allow basic types + if (isEvaluatedExpression(annotationValue)) { + annotationValue = buildEvaluatedExpressionReference(originatingElement, annotationName, memberName, annotationValue); + } return annotationValue; } return null; diff --git a/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaMethodElement.java b/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaMethodElement.java index 99c9c109a16..0c1a7d2490f 100644 --- a/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaMethodElement.java +++ b/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaMethodElement.java @@ -155,6 +155,29 @@ public boolean isDefault() { return executableElement.isDefault(); } + @Override + public boolean isVarArgs() { + return executableElement.isVarArgs(); + } + + @Override + public boolean overrides(MethodElement overridden) { + if (this.equals(overridden) || isStatic() || overridden.isStatic()) { + return false; + } + if (overridden instanceof JavaMethodElement javaMethodElement) { + boolean overrides = visitorContext.getElements().overrides( + executableElement, + javaMethodElement.executableElement, + owningType.classElement + ); + if (overrides) { + return true; + } + } + return MethodElement.super.overrides(overridden); + } + @Override public boolean hides(MemberElement hidden) { if (isStatic() && getDeclaringType().isInterface()) { diff --git a/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaVisitorContext.java b/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaVisitorContext.java index 300d4968292..3db66d93b21 100644 --- a/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaVisitorContext.java +++ b/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaVisitorContext.java @@ -30,6 +30,8 @@ import io.micronaut.core.util.ArgumentUtils; import io.micronaut.core.util.CollectionUtils; import io.micronaut.core.util.StringUtils; +import io.micronaut.expressions.context.DefaultExpressionCompilationContextFactory; +import io.micronaut.expressions.context.ExpressionCompilationContextFactory; import io.micronaut.inject.ast.ClassElement; import io.micronaut.inject.ast.annotation.AbstractAnnotationElement; import io.micronaut.inject.ast.annotation.ElementAnnotationMetadataFactory; @@ -92,6 +94,7 @@ public final class JavaVisitorContext implements VisitorContext, BeanElementVisi private final List beanDefinitionBuilders = new ArrayList<>(); private final JavaElementFactory elementFactory; private final TypeElementVisitor.VisitorKind visitorKind; + private final DefaultExpressionCompilationContextFactory expressionCompilationContextFactory; private @Nullable JavaFileManager standardFileManager; private final JavaAnnotationMetadataBuilder annotationMetadataBuilder; @@ -135,6 +138,7 @@ public JavaVisitorContext( this.visitorKind = visitorKind; this.annotationMetadataBuilder = new JavaAnnotationMetadataBuilder(elements, messager, annotationUtils, modelUtils); this.elementAnnotationMetadataFactory = new JavaElementAnnotationMetadataFactory(false, this.annotationMetadataBuilder); + this.expressionCompilationContextFactory = new DefaultExpressionCompilationContextFactory(this); } /** @@ -216,6 +220,11 @@ public JavaElementAnnotationMetadataFactory getElementAnnotationMetadataFactory( return elementAnnotationMetadataFactory; } + @Override + public ExpressionCompilationContextFactory getExpressionCompilationContextFactory() { + return expressionCompilationContextFactory; + } + @Override public JavaAnnotationMetadataBuilder getAnnotationMetadataBuilder() { return annotationMetadataBuilder; diff --git a/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/LoadedVisitor.java b/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/LoadedVisitor.java index 98771517b90..0a482b2ded3 100644 --- a/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/LoadedVisitor.java +++ b/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/LoadedVisitor.java @@ -58,17 +58,38 @@ public LoadedVisitor(TypeElementVisitor visitor, TypeElement typeElement = processingEnvironment.getElementUtils().getTypeElement(aClass.getName()); if (typeElement != null) { List generics = genericUtils.interfaceGenericTypesFor(typeElement, TypeElementVisitor.class.getName()); - String typeName = generics.get(0).toString(); - if (typeName.equals(OBJECT_CLASS)) { - classAnnotation = visitor.getClassType(); - } else { - classAnnotation = typeName; - } - String elementName = generics.get(1).toString(); - if (elementName.equals(OBJECT_CLASS)) { - elementAnnotation = visitor.getElementType(); + if (generics.size() == 2) { + String typeName = generics.get(0).toString(); + if (typeName.equals(OBJECT_CLASS)) { + classAnnotation = visitor.getClassType(); + } else { + classAnnotation = typeName; + } + String elementName = generics.get(1).toString(); + if (elementName.equals(OBJECT_CLASS)) { + elementAnnotation = visitor.getElementType(); + } else { + elementAnnotation = elementName; + } } else { - elementAnnotation = elementName; + Class[] classes = GenericTypeUtils.resolveInterfaceTypeArguments(aClass, TypeElementVisitor.class); + if (classes != null && classes.length == 2) { + Class classGeneric = classes[0]; + if (classGeneric == Object.class) { + classAnnotation = visitor.getClassType(); + } else { + classAnnotation = classGeneric.getName(); + } + Class elementGeneric = classes[1]; + if (elementGeneric == Object.class) { + elementAnnotation = visitor.getElementType(); + } else { + elementAnnotation = elementGeneric.getName(); + } + } else { + classAnnotation = Object.class.getName(); + elementAnnotation = Object.class.getName(); + } } } else { Class[] classes = GenericTypeUtils.resolveInterfaceTypeArguments(aClass, TypeElementVisitor.class); diff --git a/inject-java/src/test/groovy/io/micronaut/expressions/AnnotationLevelContextExpressionsSpec.groovy b/inject-java/src/test/groovy/io/micronaut/expressions/AnnotationLevelContextExpressionsSpec.groovy new file mode 100644 index 00000000000..f8f85658a06 --- /dev/null +++ b/inject-java/src/test/groovy/io/micronaut/expressions/AnnotationLevelContextExpressionsSpec.groovy @@ -0,0 +1,75 @@ +package io.micronaut.expressions + +import io.micronaut.annotation.processing.test.AbstractEvaluatedExpressionsSpec + +class AnnotationLevelContextExpressionsSpec extends AbstractEvaluatedExpressionsSpec { + + void "test annotation level context"() { + given: + Object result = evaluateSingle("test.Expr", """ + + package test; + import io.micronaut.context.annotation.AnnotationExpressionContext; + import io.micronaut.context.annotation.Executable; + import io.micronaut.context.annotation.Requires; + import jakarta.inject.Singleton; + + @Singleton + @CustomAnnotation("#{ #getAnnotationLevelValue() }") + class Expr { + } + + @Singleton + class CustomContext { + String getAnnotationLevelValue() { + return "annotationLevelValue"; + } + } + + @AnnotationExpressionContext(CustomContext.class) + @interface CustomAnnotation { + String value(); + } + + """) + + expect: + result instanceof String && result == "annotationLevelValue" + + } + + void "test annotation member level context"() { + given: + Object result = evaluateSingle("test.Expr", """ + + package test; + import io.micronaut.context.annotation.AnnotationExpressionContext; + import io.micronaut.context.annotation.Executable; + import io.micronaut.context.annotation.Requires; + import jakarta.inject.Singleton; + + @Singleton + @CustomAnnotation(customValue = "#{ #getAnnotationLevelValue() }") + class Expr { + } + + @Singleton + class CustomContext { + String getAnnotationLevelValue() { + return "annotationLevelValue"; + } + } + + @interface CustomAnnotation { + @AnnotationExpressionContext(CustomContext.class) + String customValue(); + } + + """) + + expect: + result instanceof String && result == "annotationLevelValue" + + } + +} diff --git a/inject-java/src/test/groovy/io/micronaut/expressions/ArrayMethodsExpressionsSpec.groovy b/inject-java/src/test/groovy/io/micronaut/expressions/ArrayMethodsExpressionsSpec.groovy new file mode 100644 index 00000000000..cefc851a59d --- /dev/null +++ b/inject-java/src/test/groovy/io/micronaut/expressions/ArrayMethodsExpressionsSpec.groovy @@ -0,0 +1,219 @@ +package io.micronaut.expressions + +import io.micronaut.annotation.processing.test.AbstractEvaluatedExpressionsSpec + +class ArrayMethodsExpressionsSpec extends AbstractEvaluatedExpressionsSpec { + + void "test primitive and wrapper varargs methods"() { + given: + Object expr1 = evaluateAgainstContext("#{ #countValues(1, 2, 3) }", + """ + @jakarta.inject.Singleton + class Context { + int countValues(int... array) { + return array.length; + } + } + """) + + Object expr2 = evaluateAgainstContext("#{ #countValues(1) }", + """ + @jakarta.inject.Singleton + class Context { + int countValues(int... array) { + return array.length; + } + } + """) + + Object expr3 = evaluateAgainstContext("#{ #countValues() }", + """ + @jakarta.inject.Singleton + class Context { + int countValues(int... array) { + return array.length; + } + } + """) + + Object expr4 = evaluateAgainstContext("#{ #countValues(1, 2, T(Integer).valueOf('3')) }", + """ + @jakarta.inject.Singleton + class Context { + int countValues(Integer... array) { + return array.length; + } + } + """) + + expect: + expr1 instanceof Integer && expr1 == 3 + expr2 instanceof Integer && expr2 == 1 + expr3 instanceof Integer && expr3 == 0 + expr4 instanceof Integer && expr4 == 3 + } + + void "test string varargs methods"() { + given: + Object expr1 = evaluateAgainstContext("#{ #countValues('a', 'b', 'c') }", + """ + @jakarta.inject.Singleton + class Context { + int countValues(String... values) { + return values.length; + } + } + """) + + Object expr2 = evaluateAgainstContext("#{ #countValues('a') }", + """ + @jakarta.inject.Singleton + class Context { + int countValues(String... values) { + return values.length; + } + } + """) + + Object expr3 = evaluateAgainstContext("#{ #countValues() }", + """ + @jakarta.inject.Singleton + class Context { + int countValues(String... values) { + return values.length; + } + } + """) + + expect: + expr1 instanceof Integer && expr1 == 3 + expr2 instanceof Integer && expr2 == 1 + expr3 instanceof Integer && expr3 == 0 + } + + void "test mixed types varargs methods"() { + given: + Object expr1 = evaluateAgainstContext("#{ #multiplyLength(3, '1', 8, null) }", + """ + @jakarta.inject.Singleton + class Context { + int multiplyLength(int time, Object... values) { + return values.length * time; + } + } + """) + + expect: + expr1 instanceof Integer && expr1 == 9 + } + + void "test arrays as varargs"() { + given: + Object expr1 = evaluateAgainstContext("#{ #multiplyLength(3, '1', 8, null) }", + """ + @jakarta.inject.Singleton + class Context { + int multiplyLength(Integer time, Object[] values) { + return values.length * time; + } + } + """) + + expect: + expr1 instanceof Integer && expr1 == 9 + } + + void "test non-varargs arrays"() { + given: + Object expr1 = evaluateAgainstContext("#{ #countLength(#values()) }", + """ + @jakarta.inject.Singleton + class Context { + String[] values() { + return new String[]{"a", "b", "c"}; + } + + int countLength(Object[] array) { + return array.length; + } + } + """) + + Object expr2 = evaluateAgainstContext("#{ #multiplyLength(#values(), 3) }", + """ + @jakarta.inject.Singleton + class Context { + String[] values() { + return new String[]{"a", "b"}; + } + + int multiplyLength(String[] array, int times) { + return array.length * times; + } + } + """) + + Object expr3 = evaluateAgainstContext("#{ #multiplyLength(#values(), 1) }", + """ + @jakarta.inject.Singleton + class Context { + int[] values() { + return new int[]{1, 2}; + } + + int multiplyLength(int[] array, int times) { + return array.length * times; + } + } + """) + + expect: + expr1 instanceof Integer && expr1 == 3 + expr2 instanceof Integer && expr2 == 6 + expr3 instanceof Integer && expr3 == 2 + } + + void "test multi-dimensional arrays"() { + given: + Object expr1 = evaluateAgainstContext("#{ #countLength(#values()) }", + """ + import java.util.Arrays; + + @jakarta.inject.Singleton + class Context { + String[][] values() { + return new String[][]{new String[]{"a", "b", "c"}, new String[]{"a", "b"}}; + } + + int countLength(Object[][] array) { + return Arrays.stream(array) + .map(a -> a.length) + .reduce(0, Integer::sum); + } + } + """) + + Object expr2 = evaluateAgainstContext("#{ #countLength(#values()) }", + """ + import java.util.Arrays; + + @jakarta.inject.Singleton + class Context { + int[][] values() { + return new int[][]{new int[]{1, 2, 3}, new int[]{1, 2, 3, 4}}; + } + + int countLength(int[][] array) { + return Arrays.stream(array) + .map(a -> a.length) + .reduce(0, Integer::sum); + } + } + """) + + + expect: + expr1 instanceof Integer && expr1 == 5 + expr2 instanceof Integer && expr2 == 7 + } +} diff --git a/inject-java/src/test/groovy/io/micronaut/expressions/CollectionExpressionSpec.groovy b/inject-java/src/test/groovy/io/micronaut/expressions/CollectionExpressionSpec.groovy new file mode 100644 index 00000000000..806bb156f2b --- /dev/null +++ b/inject-java/src/test/groovy/io/micronaut/expressions/CollectionExpressionSpec.groovy @@ -0,0 +1,86 @@ +package io.micronaut.expressions + +import io.micronaut.annotation.processing.test.AbstractEvaluatedExpressionsSpec +import org.intellij.lang.annotations.Language + +class CollectionExpressionSpec extends AbstractEvaluatedExpressionsSpec { + + void "test list dereference"() { + given: + @Language("java") def context = """ + import java.util.*; + @jakarta.inject.Singleton + class Context { + List getList() { + return List.of(1, 2, 3); + } + + int index() { + return 1; + } + + List foos() { + List list = new ArrayList<>(); + list.add(new Foo("one")); + list.add(new Foo("two")); + list.add(null); + return list; + } + } + + record Foo(String name) {} + """ + + Object result = evaluateAgainstContext("#{list[1]}", context) + Object result2 = evaluateAgainstContext("#{list[index()]}", context) + Object result3 = evaluateAgainstContext("#{not empty list}", context) + Object result4 = evaluateAgainstContext("#{foos()[1].name()}", context) + Object result5 = evaluateAgainstContext("#{foos()[2]?.name()}", context) + + expect: + result == 2 + result2 == 2 + result3 == true + result4 == 'two' + result5 == null + } + + void "test primitive array dereference"() { + given: + Object result = evaluateAgainstContext("#{array[1]}", + """ + import java.util.*; + @jakarta.inject.Singleton + class Context { + int[] getArray() { + return new int[] {1,2,3}; + } + } + """) + + expect: + result == 2 + } + + void "test map dereference"() { + given: + @Language("java") def context = """ + import java.util.*; + @jakarta.inject.Singleton + class Context { + Map getMap() { + return Map.of( + "foo", "bar", + "baz", "stuff" + ); + } + } + """ + Object result = evaluateAgainstContext("#{map['foo']}",context) + Object result2 = evaluateAgainstContext("#{not empty map}",context) + + expect: + result == "bar" + result2 == true + } +} diff --git a/inject-java/src/test/groovy/io/micronaut/expressions/CompoundExpressionsSpec.groovy b/inject-java/src/test/groovy/io/micronaut/expressions/CompoundExpressionsSpec.groovy new file mode 100644 index 00000000000..84f6cd70f8b --- /dev/null +++ b/inject-java/src/test/groovy/io/micronaut/expressions/CompoundExpressionsSpec.groovy @@ -0,0 +1,54 @@ +package io.micronaut.expressions + +import io.micronaut.annotation.processing.test.AbstractEvaluatedExpressionsSpec + +class CompoundExpressionsSpec extends AbstractEvaluatedExpressionsSpec { + + void "test compound expressions"() { + given: + List results = evaluateMultiple( + "a #{1}#{'b'} #{3}", + "#{1 + 2}#{2 + 3}", + "#{ '5' } s", + "a#{ null }b" + ) + + expect: + results[0] instanceof String && results[0] == 'a 1b 3' + results[1] instanceof String && results[1] == '35' + results[2] instanceof String && results[2] == '5 s' + results[3] instanceof String && results[3] == 'anullb' + } + + void "test string expressions in arrays"() { + Object result = evaluateSingle("test.Expr", """ + package test; + import io.micronaut.context.annotation.Requires; + import jakarta.inject.Singleton; + + @Singleton + @Requires(env = {"#{ 'a' }", "b", "#{ 'c' + 'd' }"}) + class Expr { + } + """) + + expect: + result instanceof Object[] && Arrays.equals((Object[]) result, new Object[]{'a', 'b', 'cd'}) + } + + void "test mixed expressions in arrays"() { + Object result = evaluateSingle("test.Expr", """ + package test; + import io.micronaut.context.annotation.Requires; + import jakarta.inject.Singleton; + + @Singleton + @Requires(env = {"#{ 1 }", "b", "#{ 15l }", "#{ 'c' }"}) + class Expr { + } + """) + + expect: + result instanceof Object[] && Arrays.equals((Object[]) result, new Object[]{1, 'b', 15l, 'c'}) + } +} diff --git a/inject-java/src/test/groovy/io/micronaut/expressions/ContextMethodCallsExpressionsSpec.groovy b/inject-java/src/test/groovy/io/micronaut/expressions/ContextMethodCallsExpressionsSpec.groovy new file mode 100644 index 00000000000..9de2c292e03 --- /dev/null +++ b/inject-java/src/test/groovy/io/micronaut/expressions/ContextMethodCallsExpressionsSpec.groovy @@ -0,0 +1,149 @@ +package io.micronaut.expressions; + +import io.micronaut.annotation.processing.test.AbstractEvaluatedExpressionsSpec; + +class ContextMethodCallsExpressionsSpec extends AbstractEvaluatedExpressionsSpec +{ + void "test context method calls"() { + given: + Object expr1 = evaluateAgainstContext("#{ getIntValue() }", + """ + @jakarta.inject.Singleton + class Context { + int getIntValue() { + return 15; + } + } + """) + + Object expr2 = evaluateAgainstContext("#{ getStringValue().toUpperCase() }", + """ + @jakarta.inject.Singleton + class Context { + String getStringValue() { + return "test"; + } + } + """) + + Object expr3 = evaluateAgainstContext("#{ randomizer().nextInt(10) }", + """ + import java.util.Random; + + @jakarta.inject.Singleton + class Context { + Random randomizer() { + return new Random(); + } + } + """) + + Object expr4 = evaluateAgainstContext("#{ lowercase('TEST') }", + """ + import java.util.Random; + + @jakarta.inject.Singleton + class Context { + String lowercase(String value) { + return value.toLowerCase(); + } + } + """) + + ContextRegistrar.setClasses( + "test.FirstContext", + "test.SecondContext", + "test.ThirdContext", + "test.FourthContext" + ) + Object expr5 = evaluateAgainstContext("#{ transform(getName(), getRepeat(), toLower()) }", + """ + import java.util.Random; + + @jakarta.inject.Singleton + class FirstContext { + String transform(String value, int repeat, Boolean toLower) { + return (toLower ? value.toLowerCase() : value).repeat(repeat); + } + } + + @jakarta.inject.Singleton + class SecondContext { + String getName() { + return "TEST"; + } + } + + @jakarta.inject.Singleton + class ThirdContext { + Integer getRepeat() { + return 2; + } + } + + @jakarta.inject.Singleton + class FourthContext { + boolean toLower() { + return true; + } + } + """) + ContextRegistrar.reset() + + Object expr6 = evaluateAgainstContext("#{ getTestObject().name }", + """ + import java.util.Random; + + @jakarta.inject.Singleton + class Context { + TestObject getTestObject() { + return new TestObject(); + } + } + + class TestObject { + String getName() { + return "name"; + } + } + """) + + Object expr7 = evaluateAgainstContext("#{ values().get(random(values())) }", + """ + import java.util.Random; + import java.util.List; + import java.util.Collection; + import java.util.concurrent.ThreadLocalRandom; + + @jakarta.inject.Singleton + class Context { + TestObject getTestObject() { + return new TestObject(); + } + + List values() { + return List.of(1, 2, 3); + } + + int random(Collection values) { + return ThreadLocalRandom.current().nextInt(values.size()); + } + } + + class TestObject { + String getName() { + return "name"; + } + } + """) + + expect: + expr1 instanceof Integer && expr1 == 15 + expr2 instanceof String && expr2 == "TEST" + expr3 instanceof Integer && expr3 >= 0 && expr3 < 10 + expr4 instanceof String && expr4 == "test" + expr5 instanceof String && expr5 == "testtest" + expr6 instanceof String && expr6 == "name" + expr7 instanceof Integer && (expr7 == 1 || expr7 == 2 || expr7 == 3) + } +} diff --git a/inject-java/src/test/groovy/io/micronaut/expressions/ContextPropertyAccessExpressionsSpec.groovy b/inject-java/src/test/groovy/io/micronaut/expressions/ContextPropertyAccessExpressionsSpec.groovy new file mode 100644 index 00000000000..80198df2750 --- /dev/null +++ b/inject-java/src/test/groovy/io/micronaut/expressions/ContextPropertyAccessExpressionsSpec.groovy @@ -0,0 +1,175 @@ +package io.micronaut.expressions + +import io.micronaut.annotation.processing.test.AbstractEvaluatedExpressionsSpec +import io.micronaut.context.exceptions.ExpressionEvaluationException + +class ContextPropertyAccessExpressionsSpec extends AbstractEvaluatedExpressionsSpec { + + void "test context property access"() { + given: + Object expr1 = evaluateAgainstContext("#{intValue}", + """ + @jakarta.inject.Singleton + class Context { + int getIntValue() { + return 15; + } + } + """) + + Object expr2 = evaluateAgainstContext("#{ boolean }", + """ + @jakarta.inject.Singleton + class Context { + Boolean isBoolean() { + return false; + } + } + """) + + Object expr3 = evaluateAgainstContext("#{ stringValue }", + """ + @jakarta.inject.Singleton + class Context { + String getStringValue() { + return "test value"; + } + } + """) + + Object expr4 = evaluateAgainstContext("#{ customClass.customProperty }", + """ + @jakarta.inject.Singleton + class Context { + CustomClass getCustomClass() { + return new CustomClass(); + } + } + + class CustomClass { + String getCustomProperty() { + return "custom property"; + } + } + """) + + expect: + expr1 instanceof Integer && expr1 == 15 + expr2 instanceof Boolean && expr2 == false + expr3 instanceof String && expr3 == "test value" + expr4 instanceof String && expr4 == "custom property" + } + + void "test multi-level context property access - records"() { + given: + Object expr = evaluateAgainstContext("#{ foo.bar.name }", + """ + @jakarta.inject.Singleton + class Context { + Foo getFoo() { + return new Foo(new Bar("test")); + } + } + + record Foo(Bar bar) { + } + + record Bar(String name) { + } + """) + + expect: + expr instanceof String && expr == "test" + } + + void "test multi-level context property access"() { + given: + Object expr = evaluateAgainstContext("#{ foo.bar.name }", + """ + @jakarta.inject.Singleton + class Context { + public Foo getFoo() { + return new Foo(); + } + } + + class Foo { + private Bar bar = new Bar(); + public Bar getBar() { + return bar; + } + } + + class Bar { + private String name = "test"; + public String getName() { + return name; + } + } + """) + + expect: + expr instanceof String && expr == "test" + } + + void "test multi-level context property access safe navigation"() { + given: + Object expr = evaluateAgainstContext("#{ foo?.bar?.name }", + """ + @jakarta.inject.Singleton + class Context { + public Foo getFoo() { + return new Foo(); + } + } + + class Foo { + private Bar bar; + public Bar getBar() { + return bar; + } + } + + class Bar { + private String name = "test"; + public String getName() { + return name; + } + } + """) + + expect: + expr == null + } + + void "test multi-level context property access non-safe navigation"() { + when: + Object expr = evaluateAgainstContext("#{ foo.bar.name }", + """ + @jakarta.inject.Singleton + class Context { + public Foo getFoo() { + return new Foo(); + } + } + + class Foo { + private Bar bar; + public Bar getBar() { + return bar; + } + } + + class Bar { + private String name = "test"; + public String getName() { + return name; + } + } + """) + + then: + def e = thrown(ExpressionEvaluationException) + e.message.startsWith('Can not evaluate expression [null]') + } +} diff --git a/inject-java/src/test/groovy/io/micronaut/expressions/LiteralExpressionSpec.groovy b/inject-java/src/test/groovy/io/micronaut/expressions/LiteralExpressionSpec.groovy new file mode 100644 index 00000000000..bceecd8a9ca --- /dev/null +++ b/inject-java/src/test/groovy/io/micronaut/expressions/LiteralExpressionSpec.groovy @@ -0,0 +1,95 @@ +package io.micronaut.expressions + +import io.micronaut.annotation.processing.test.AbstractEvaluatedExpressionsSpec + +class LiteralExpressionSpec extends AbstractEvaluatedExpressionsSpec { + + void "test literals"() { + given: + List results = evaluateMultiple( + // null + "#{ null }", // 0 + + // string literals + "#{ 'string literal' }", // 1 + "#{ 'testValue' }", // 2 + + // bool literals + "#{ false }", // 3 + "#{ true }", // 4 + + // int literals + "#{ 15 }", // 5 + "#{ 0XAB013 }", // 6 + "#{ 0xFF }", // 7 + "#{ 291231 }", // 8 + "#{ 0 }", // 9 + "#{ 0x0 }", // 10 + "#{ 00 }", // 11 + + // long literals + "#{ 0xFFL }", // 12 + "#{ 0x0123l }", // 13 + "#{ 102L }", // 14 + "#{ 99l }", // 15 + "#{ 0L }", // 16 + + // float literals + "#{ 123.e+14f }", // 17 + "#{ 123.f }", // 18 + "#{ 123.F }", // 19 + "#{ .123f }", // 20 + "#{ 19F }", // 21 + + // double literals + "#{ 123. }", // 22 + "#{ 123.321 }", // 23 + "#{ 123.d }", // 24 + "#{ 123.D }", // 25 + "#{ .123 }", // 26 + "#{ 123D }", // 27 + "#{ 1E-7 }", // 28 + "#{ 1E+1d }", // 29 + "#{ 2e-1 }", // 30 + ) + + expect: + results[0] == null + + results[1] instanceof String && results[1] == 'string literal' + results[2] instanceof String && results[2] == 'testValue' + + results[3] instanceof Boolean && results[3] == false + results[4] instanceof Boolean && results[4] == true + + results[5] instanceof Integer && results[5] == 15 + results[6] instanceof Integer && results[6] == 0XAB013 + results[7] instanceof Integer && results[7] == 0xFF + results[8] instanceof Integer && results[8] == 291231 + results[9] instanceof Integer && results[9] == 0 + results[10] instanceof Integer && results[10] == 0x0 + results[11] instanceof Integer && results[11] == 00 + + results[12] instanceof Long && results[12] == 0xFFL + results[13] instanceof Long && results[13] == 0x0123l + results[14] instanceof Long && results[14] == 102L + results[15] instanceof Long && results[15] == 99L + results[16] instanceof Long && results[16] == 0L + + results[17] instanceof Float + results[18] instanceof Float + results[19] instanceof Float + results[20] instanceof Float && results[20] == .123f + results[21] instanceof Float && results[21] == 19F + + results[22] instanceof Double && results[22] == Double.valueOf("123.") + results[23] instanceof Double && results[23] == 123.321 + results[24] instanceof Double && results[24] == Double.valueOf("123.d") + results[25] instanceof Double && results[25] == Double.valueOf("123.D") + results[26] instanceof Double && results[26] == .123 + results[27] instanceof Double && results[27] == 123D + results[28] instanceof Double && results[28] == 1E-7 + results[29] instanceof Double && results[29] == 1E+1d + results[30] instanceof Double && results[30] == 2e-1 + } +} diff --git a/inject-java/src/test/groovy/io/micronaut/expressions/MethodArgumentEvaluationContextExpressionsSpec.groovy b/inject-java/src/test/groovy/io/micronaut/expressions/MethodArgumentEvaluationContextExpressionsSpec.groovy new file mode 100644 index 00000000000..2c1db7f8bb5 --- /dev/null +++ b/inject-java/src/test/groovy/io/micronaut/expressions/MethodArgumentEvaluationContextExpressionsSpec.groovy @@ -0,0 +1,32 @@ +package io.micronaut.expressions + +import io.micronaut.annotation.processing.test.AbstractEvaluatedExpressionsSpec; + + +class MethodArgumentEvaluationContextExpressionsSpec extends AbstractEvaluatedExpressionsSpec { + + void "test method argument access"() { + given: + Object result = evaluateSingle("test.Expr", """ + package test; + import io.micronaut.context.annotation.Executable; + import io.micronaut.context.annotation.Requires; + import jakarta.inject.Singleton; + + @Singleton + class Expr { + + @Executable + @Requires(value = "#{ #second + 'abc' }") + void test(String first, String second) { + } + } + + + """, ["arg0", "arg1"] as Object[]) + + expect: + result instanceof String && result == 'arg1abc' + } + +} diff --git a/inject-java/src/test/groovy/io/micronaut/expressions/OperatorExpressionSpec.groovy b/inject-java/src/test/groovy/io/micronaut/expressions/OperatorExpressionSpec.groovy new file mode 100644 index 00000000000..037fb7f81eb --- /dev/null +++ b/inject-java/src/test/groovy/io/micronaut/expressions/OperatorExpressionSpec.groovy @@ -0,0 +1,923 @@ +package io.micronaut.expressions + +import io.micronaut.annotation.processing.test.AbstractEvaluatedExpressionsSpec + +class OperatorExpressionSpec extends AbstractEvaluatedExpressionsSpec { + + void "test '/' operator"() { + given: + List results = evaluateMultiple( + // int + "#{ 10 / 5 }", + "#{ 5 / 10 }", + "#{ 10 div 5 }", + "#{ 5 div 10 }", + "#{ 10 / 5 / 2 }", + + // long + "#{ 10 / 5L }", + "#{ 5l / 10 }", + "#{ 10L div 5l }", + "#{ 5 div 10L }", + "#{ 10L div 5 / 2 }", + + // float + "#{ 10f / 5 }", + "#{ 5 / 10f }", + "#{ 10f div 5F }", + "#{ 5 div 10F }", + + // double + "#{ 10d / 5 }", + "#{ 5 / 10d }", + "#{ 10d div 5D }", + "#{ 5 div 10D }", + + // mixed + "#{ 10d / 5f }", + "#{ 5L / 10d }", + "#{ 10L div 5f }" + ) + + expect: + results[0] instanceof Integer && results[0] == 2 + results[1] instanceof Integer && results[1] == 0 + results[2] instanceof Integer && results[2] == 2 + results[3] instanceof Integer && results[3] == 0 + results[4] instanceof Integer && results[4] == 1 + + results[5] instanceof Long && results[5] == 2 + results[6] instanceof Long && results[6] == 0 + results[7] instanceof Long && results[7] == 2 + results[8] instanceof Long && results[8] == 0 + results[9] instanceof Long && results[9] == 1 + + results[10] instanceof Float && results[10] == 10f / 5 + results[11] instanceof Float && results[11] == 5 / 10f + results[12] instanceof Float && results[12] == 10f / 5F + results[13] instanceof Float && results[13] == 5 / 10F + + results[14] instanceof Double && results[14] == 10d / 5 + results[15] instanceof Double && results[15] == 5 / 10d + results[16] instanceof Double && results[16] == 10d / 5D + results[17] instanceof Double && results[17] == 5 / 10D + + results[18] instanceof Double && results[18] == 10d / 5f + results[19] instanceof Double && results[19] == 5L / 10d + results[20] instanceof Float && results[20] == 10L / 5f + } + + void "test '%' operator"() { + given: + List results = evaluateMultiple( + // int + "#{ 10 % 5 }", // 0 + "#{ 5 % 10 }", // 1 + "#{ 10 mod 5 }", // 2 + "#{ 5 mod 10 }", // 3 + "#{ 10 % 5 % 3}", // 4 + + // long + "#{ 10 % 5L }", // 5 + "#{ 5l % 10 }", // 6 + "#{ 10L mod 5l }", // 7 + "#{ 5 mod 10L }", // 8 + "#{ 13L % 5 mod 2 }", // 9 + + // float + "#{ 10f % 5 }", // 10 + "#{ 5 % 10f }", // 11 + "#{ 10f mod 5F }", // 12 + "#{ 5 mod 10F }", // 13 + + // double + "#{ 10d % 5 }", // 14 + "#{ 5 % 10d }", // 15 + "#{ 10d mod 5D }", // 16 + "#{ 5 mod 10D }", // 17 + + // mixed + "#{ 10d % 5f }", // 18 + "#{ 5L % 10d }", // 19 + "#{ 10L mod 5f }" // 20 + ) + + + expect: + results[0] instanceof Integer && results[0] == 0 + results[1] instanceof Integer && results[1] == 5 + results[2] instanceof Integer && results[2] == 0 + results[3] instanceof Integer && results[3] == 5 + results[4] instanceof Integer && results[4] == 0 + + results[5] instanceof Long && results[5] == 0 + results[6] instanceof Long && results[6] == 5 + results[7] instanceof Long && results[7] == 0 + results[8] instanceof Long && results[8] == 5 + results[9] instanceof Long && results[9] == 1 + + results[10] instanceof Float && results[10] == 10f % 5 + results[11] instanceof Float && results[11] == 5 % 10f + results[12] instanceof Float && results[12] == 10f % 5F + results[13] instanceof Float && results[13] == 5 % 10F + + results[14] instanceof Double && results[14] == 10d % 5 + results[15] instanceof Double && results[15] == 5 % 10d + results[16] instanceof Double && results[16] == 10d % 5D + results[17] instanceof Double && results[17] == 5 % 10D + + results[18] instanceof Double && results[18] == 10d % 5f + results[19] instanceof Double && results[19] == 5L % 10d + results[20] instanceof Float && results[20] == 10L % 5f + } + + void "test -' operator"() { + given: + List results = evaluateMultiple( + // int + "#{ 10 - 5 }", // 0 + "#{ 5 - 10 }", // 1 + "#{ 25 - 5 - 10 }", // 2 + + // long + "#{ 10 - 5L }", // 3 + "#{ 5l - 10 }", // 4 + "#{ 10L - 5l }", // 5 + "#{ 5 - 10L }", // 6 + + + // float + "#{ 10f - 5 }", // 7 + "#{ 5 - 10f }", // 8 + "#{ 10f - 5F }", // 9 + "#{ 5 - 10F }", // 10 + + // double + "#{ 10d - 5 }", // 11 + "#{ 5 - 10d }", // 12 + "#{ 10d - 5D }", // 13 + "#{ 5 - 10D }", // 14 + + // mixed + "#{ 10d - 5f }", // 15 + "#{ 5L - 10d }", // 16 + "#{ 10L - 5f }" // 17 + ) + + expect: + results[0] instanceof Integer && results[0] == 5 + results[1] instanceof Integer && results[1] == -5 + results[2] instanceof Integer && results[2] == 10 + + results[3] instanceof Long && results[3] == 10 - 5L + results[4] instanceof Long && results[4] == 5l - 10 + results[5] instanceof Long && results[5] == 10L - 5l + results[6] instanceof Long && results[6] == 5 - 10L + + results[7] instanceof Float && results[7] == 10f - 5 + results[8] instanceof Float && results[8] == 5 - 10f + results[9] instanceof Float && results[9] == 10f - 5F + results[10] instanceof Float && results[10] == 5 - 10F + + results[11] instanceof Double && results[11] == 10d - 5 + results[12] instanceof Double && results[12] == 5 - 10d + results[13] instanceof Double && results[13] == 10d - 5D + results[14] instanceof Double && results[14] == 5 - 10D + + results[15] instanceof Double && results[15] == 10d - 5f + results[16] instanceof Double && results[16] == 5L - 10d + results[17] instanceof Float && results[17] == 10L - 5f + } + + void "test '*' operator"() { + given: + List results = evaluateMultiple( + // int + "#{ 10 * 5 }", // 0 + "#{ -8 * 10 * 5 }", // 1 + + // long + "#{ 10 * 5L }", // 2 + "#{ 5l * 10 }", // 3 + "#{ 10L * 5l }", // 4 + "#{ 5 * 10L }", // 5 + + // float + "#{ 10f * 5 }", // 6 + "#{ 5 * 10f }", // 7 + "#{ 10f * 5F }", // 8 + "#{ 5 * 10F }", // 9 + + // double + "#{ 10d * 5 }", // 10 + "#{ 5 * 10d }", // 11 + "#{ 10d * 5D }", // 12 + "#{ 5 * 10D }", // 13 + + // mixed + "#{ 10d * 5f }", // 14 + "#{ 5L * 10d }", // 15 + "#{ 10L * 5f }" // 16 + ) + + expect: + results[0] instanceof Integer && results[0] == 50 + results[1] instanceof Integer && results[1] == -8 * 10 * 5 + + results[2] instanceof Long && results[2] == 10 * 5L + results[3] instanceof Long && results[3] == 5l * 10 + results[4] instanceof Long && results[4] == 10L * 5l + results[5] instanceof Long && results[5] == 5 * 10L + + results[6] instanceof Float && results[6] == 10f * 5 + results[7] instanceof Float && results[7] == 5 * 10f + results[8] instanceof Float && results[8] == 10f * 5F + results[9] instanceof Float && results[9] == 5 * 10F + + results[10] instanceof Double && results[10] == 10d * 5 + results[11] instanceof Double && results[11] == 5 * 10d + results[12] instanceof Double && results[12] == 10d * 5D + results[13] instanceof Double && results[13] == 5 * 10D + + results[14] instanceof Double && results[14] == 10d * 5f + results[15] instanceof Double && results[15] == 5L * 10d + results[16] instanceof Float && results[16] == 10L * 5f + } + + void "test '+' operator"() { + given: + List results = evaluateMultiple( + // int + "#{ 10 + 5 }", // 0 + "#{ -8 + 10 + 5 }", // 1 + + // long + "#{ 10 + 5L }", // 2 + "#{ 5l + 10 }", // 3 + "#{ 10L + 5l }", // 4 + "#{ 5 + 10L }", // 5 + + // float + "#{ 10f + 5 }", // 6 + "#{ 5 + 10f }", // 7 + "#{ 10f + 5F }", // 8 + "#{ 5 + 10F }", // 9 + + // double + "#{ 10d + 5 }", // 10 + "#{ 5 + 10d }", // 11 + "#{ 10d + 5D }", // 12 + "#{ 5 + 10D }", // 13 + + // mixed + "#{ 10d + 5f }", // 14 + "#{ 5L + 10d }", // 15 + "#{ 10L + 5f }", // 16 + + // string + "#{ '1' + '2' }", // 17 + "#{ '1' + null }", // 18 + "#{ null + '1' }", // 19 + "#{ null + '1' + null + 2D }", // 20 + "#{ 15 + 'str' + 2L }", // 21 + "#{ 2f + 'str' + 2 }", // 22 + "#{ .014 + 'str' + 2L + 'test' }", // 23 + "#{ 1 + 2 + 'str' + 2L + 'test' }", // 24 + "#{ 1 + 2 + 3 + 'str' }", // 25 + "#{ 1 + 2 - 3 + 'str' }" // 26 + ) + + expect: + results[0] instanceof Integer && results[0] == 10 + 5 + results[1] instanceof Integer && results[1] == -8 + 10 + 5 + + results[2] instanceof Long && results[2] == 10 + 5L + results[3] instanceof Long && results[3] == 5l + 10 + results[4] instanceof Long && results[4] == 10L + 5l + results[5] instanceof Long && results[5] == 5 + 10L + + results[6] instanceof Float && results[6] == 10f + 5 + results[7] instanceof Float && results[7] == 5 + 10f + results[8] instanceof Float && results[8] == 10f + 5F + results[9] instanceof Float && results[9] == 5 + 10F + + results[10] instanceof Double && results[10] == 10d + 5 + results[11] instanceof Double && results[11] == 5 + 10d + results[12] instanceof Double && results[12] == 10d + 5D + results[13] instanceof Double && results[13] == 5 + 10D + + results[14] instanceof Double && results[14] == 10d + 5f + results[15] instanceof Double && results[15] == 5L + 10d + results[16] instanceof Float && results[16] == 10L + 5f + + results[17] instanceof String && results[17] == '12' + results[18] instanceof String && results[18] == '1null' + results[19] instanceof String && results[19] == 'null1' + results[20] instanceof String && results[20] == 'null1null2.0' + results[21] instanceof String && results[21] == '15str2' + results[22] instanceof String && results[22] == '2.0str2' + results[23] instanceof String && results[23] == '0.014str2test' + results[24] instanceof String && results[24] == '3str2test' + results[25] instanceof String && results[25] == '6str' + results[26] instanceof String && results[26] == '0str' + } + + void "test '>' operator"() { + given: + List results = evaluateMultiple( + // int + "#{ 10 > 5 }", // 0 + "#{ -8 > -3 }", // 1 + + // long + "#{ 10L > 5l }", // 2 + "#{ 5 > 10L }", // 3 + "#{ 10l > 5 }", // 4 + + // double + "#{ 10d > 5 }", // 5 + "#{ 5 > 10d }", // 6 + "#{ 10D > 5d }", // 7 + "#{ -.4 > 5d }", // 8 + "#{ .123 > .1 }", // 9 + "#{ .123 > .1229 }", // 10 + + // float + "#{ 10f > 5 }", // 11 + "#{ 5 > 10f }", // 12 + "#{ 10F > 5f }", // 13 + "#{ -.4f > 5f }", // 14 + "#{ .123f > -5f }", // 15 + + // mixed + "#{ 10f > 5d }", // 16 + "#{ 5L > 10f }", // 17 + "#{ 10L > 5D }", // 18 + "#{ -.4 > 5l }", // 19 + "#{ .123f > -5 }", // 20 + "#{ 10L > 11 }" // 21 + ) + + expect: + results[0] instanceof Boolean && results[0] == true + results[1] instanceof Boolean && results[1] == false + + results[2] instanceof Boolean && results[2] == true + results[3] instanceof Boolean && results[3] == false + results[4] instanceof Boolean && results[4] == true + + results[5] instanceof Boolean && results[5] == true + results[6] instanceof Boolean && results[6] == false + results[7] instanceof Boolean && results[7] == true + results[8] instanceof Boolean && results[8] == false + results[9] instanceof Boolean && results[9] == true + results[10] instanceof Boolean && results[10] == true + + results[11] instanceof Boolean && results[11] == true + results[12] instanceof Boolean && results[12] == false + results[13] instanceof Boolean && results[13] == true + results[14] instanceof Boolean && results[14] == false + results[15] instanceof Boolean && results[15] == true + + results[16] instanceof Boolean && results[16] == true + results[17] instanceof Boolean && results[17] == false + results[18] instanceof Boolean && results[18] == true + results[19] instanceof Boolean && results[19] == false + results[20] instanceof Boolean && results[20] == true + results[21] instanceof Boolean && results[21] == false + } + + void "test '<' operator"() { + given: + List results = evaluateMultiple( + // int + "#{ 10 < 5 }", // 0 + "#{ -8 < -3 }", // 1 + + // long + "#{ 10L < 5l }", // 2 + "#{ 5 < 10L }", // 3 + "#{ 10l < 5 }", // 4 + + // double + "#{ 10d < 5 }", // 5 + "#{ 5 < 10d }", // 6 + "#{ 10D < 5d }", // 7 + "#{ -.4 < 5d }", // 8 + "#{ .123 < .1 }", // 9 + "#{ .1229 < .123 }", // 10 + + // float + "#{ 10f < 5 }", // 11 + "#{ 5 < 10f }", // 12 + "#{ 10F < 5f }", // 13 + "#{ -.4f < 5f }", // 14 + "#{ .123f < -5f }", // 15 + + // mixed + "#{ 10f < 5d }", // 16 + "#{ 5L < 10f }", // 17 + "#{ 10L < 5D }", // 18 + "#{ -.4 < 5l }", // 19 + "#{ .123f < -5 }", // 20 + "#{ 10L < 11 }" // 21 + ) + + expect: + results[0] instanceof Boolean && results[0] == false + results[1] instanceof Boolean && results[1] == true + + results[2] instanceof Boolean && results[2] == false + results[3] instanceof Boolean && results[3] == true + results[4] instanceof Boolean && results[4] == false + + results[5] instanceof Boolean && results[5] == false + results[6] instanceof Boolean && results[6] == true + results[7] instanceof Boolean && results[7] == false + results[8] instanceof Boolean && results[8] == true + results[9] instanceof Boolean && results[9] == false + results[10] instanceof Boolean && results[10] == true + + results[11] instanceof Boolean && results[11] == false + results[12] instanceof Boolean && results[12] == true + results[13] instanceof Boolean && results[13] == false + results[14] instanceof Boolean && results[14] == true + results[15] instanceof Boolean && results[15] == false + + results[16] instanceof Boolean && results[16] == false + results[17] instanceof Boolean && results[17] == true + results[18] instanceof Boolean && results[18] == false + results[19] instanceof Boolean && results[19] == true + results[20] instanceof Boolean && results[20] == false + results[21] instanceof Boolean && results[21] == true + } + + void "test '>=' operator"() { + given: + List results = evaluateMultiple( + // int + "#{ 10 >= 5 }", // 0 + "#{ -8 >= -3 }", // 1 + "#{ 3 >= 3 }", // 2 + + // long + "#{ 10L >= 5l }", // 3 + "#{ 5 >= 10L }", // 4 + "#{ 10l >= 5 }", // 5 + "#{ 5l >= 5 }", // 6 + + // double + "#{ 10d >= 5 }", // 7 + "#{ 5 >= 10d }", // 8 + "#{ 10D >= 5d }", // 9 + "#{ -.4 >= 5d }", // 10 + "#{ .123 >= .1 }", // 11 + + // float + "#{ 10f >= 5 }", // 12 + "#{ 5 >= 10f }", // 13 + "#{ 10F >= 5f }", // 14 + "#{ -.4f >= 5f }", // 15 + "#{ .123f >= -5f }", // 16 + + // mixed + "#{ 10f >= 5d }", // 17 + "#{ 5L >= 10f }", // 18 + "#{ 10L >= 5D }", // 19 + "#{ -.4 >= 5l }", // 20 + "#{ .123f >= -5 }", // 21 + "#{ 12L >= 11 }", // 22 + "#{ 11 >= 11L }" // 23 + ) + + expect: + results[0] instanceof Boolean && results[0] == true + results[1] instanceof Boolean && results[1] == false + results[2] instanceof Boolean && results[2] == true + + results[3] instanceof Boolean && results[3] == true + results[4] instanceof Boolean && results[4] == false + results[5] instanceof Boolean && results[5] == true + results[6] instanceof Boolean && results[6] == true + + results[7] instanceof Boolean && results[7] == true + results[8] instanceof Boolean && results[8] == false + results[9] instanceof Boolean && results[9] == true + results[10] instanceof Boolean && results[10] == false + results[11] instanceof Boolean && results[11] == true + + results[12] instanceof Boolean && results[12] == true + results[13] instanceof Boolean && results[13] == false + results[14] instanceof Boolean && results[14] == true + results[15] instanceof Boolean && results[15] == false + results[16] instanceof Boolean && results[16] == true + + results[17] instanceof Boolean && results[17] == true + results[18] instanceof Boolean && results[18] == false + results[19] instanceof Boolean && results[19] == true + results[20] instanceof Boolean && results[20] == false + results[21] instanceof Boolean && results[21] == true + results[22] instanceof Boolean && results[22] == true + results[23] instanceof Boolean && results[23] == true + } + + void "test '<=' operator"() { + given: + List results = evaluateMultiple( + // int + "#{ 10 <= 5 }", // 0 + "#{ -8 <= -3 }", // 1 + "#{ 3 <= 3 }", // 2 + + // long + "#{ 10L <= 5l }", // 3 + "#{ 5 <= 10L }", // 4 + "#{ 10l <= 5 }", // 5 + "#{ 5l <= 5 }", // 6 + + // double + "#{ 10d <= 5 }", // 7 + "#{ 5 <= 10d }", // 8 + "#{ 10D <= 5d }", // 9 + "#{ -.4 <= 5d }", // 10 + "#{ .123 <= .1 }", // 11 + + // float + "#{ 10f <= 5 }", // 12 + "#{ 5 <= 10f }", // 13 + "#{ 10F <= 5f }", // 14 + "#{ -.4f <= 5f }", // 15 + "#{ .123f <= -5f }", // 16 + + // mixed + "#{ 10f <= 5d }", // 17 + "#{ 5L <= 10f }", // 18 + "#{ 10L <= 5D }", // 19 + "#{ -.4 <= 5l }", // 20 + "#{ .123f <= -5 }", // 21 + "#{ 12L <= 11 }", // 22 + "#{ 11 <= 11L }" // 23 + ) + + expect: + results[0] instanceof Boolean && results[0] == false + results[1] instanceof Boolean && results[1] == true + results[2] instanceof Boolean && results[2] == true + + results[3] instanceof Boolean && results[3] == false + results[4] instanceof Boolean && results[4] == true + results[5] instanceof Boolean && results[5] == false + results[6] instanceof Boolean && results[6] == true + + results[7] instanceof Boolean && results[7] == false + results[8] instanceof Boolean && results[8] == true + results[9] instanceof Boolean && results[9] == false + results[10] instanceof Boolean && results[10] == true + results[11] instanceof Boolean && results[11] == false + + results[12] instanceof Boolean && results[12] == false + results[13] instanceof Boolean && results[13] == true + results[14] instanceof Boolean && results[14] == false + results[15] instanceof Boolean && results[15] == true + results[16] instanceof Boolean && results[16] == false + + results[17] instanceof Boolean && results[17] == false + results[18] instanceof Boolean && results[18] == true + results[19] instanceof Boolean && results[19] == false + results[20] instanceof Boolean && results[20] == true + results[21] instanceof Boolean && results[21] == false + results[22] instanceof Boolean && results[22] == false + results[23] instanceof Boolean && results[23] == true + } + + void "test '==' operator"() { + given: + List results = evaluateMultiple( + // int + "#{ 10 == 10}", // 0 + "#{ 8 == 11 }", // 1 + "#{ -3 == 3}", // 2 + "#{ -15 == -15 }", // 3 + + // long + "#{ 10L == 10L}", // 4 + "#{ 8L == 11L }", // 5 + "#{ -3L == 3L }", // 6 + "#{ -15L == -15L }", // 7 + + // float + "#{ 1f == 1f}", // 8 + "#{ 0f == 1f }", // 9 + + // double + "#{ 1d == 1.0 }", // 10 + "#{ .0 == 1d }", // 11 + + // string + "#{ 'str' == 'str' }", // 12 + "#{ 'str1' == 'str2' }" // 13 + ) + + + expect: + results[0] instanceof Boolean && results[0] == true + results[1] instanceof Boolean && results[1] == false + results[2] instanceof Boolean && results[2] == false + results[3] instanceof Boolean && results[3] == true + + results[4] instanceof Boolean && results[4] == true + results[5] instanceof Boolean && results[5] == false + results[6] instanceof Boolean && results[6] == false + results[7] instanceof Boolean && results[7] == true + + results[8] instanceof Boolean && results[8] == true + results[9] instanceof Boolean && results[9] == false + + results[10] instanceof Boolean && results[10] == true + results[11] instanceof Boolean && results[11] == false + + results[12] instanceof Boolean && results[12] == true + results[13] instanceof Boolean && results[13] == false + } + + void "test '!=' operator"() { + given: + List results = evaluateMultiple( + // int + "#{ 10 != 10}", // 0 + "#{ 8 != 11 }", // 1 + "#{ -3 != 3}", // 2 + "#{ -15 != -15 }", // 3 + + // long + "#{ 10L != 10L}", // 4 + "#{ 8L != 11L }", // 5 + "#{ -3L != 3L }", // 6 + "#{ -15L != -15L }", // 7 + + // float + "#{ 1f != 1f}", // 8 + "#{ 0f != 1f }", // 9 + + // double + "#{ 1d != 1.0 }", // 10 + "#{ .0 != 1d }", // 11 + + // string + "#{ 'str' != 'str' }", // 12 + "#{ 'str1' != 'str2' }" // 13 + ) + + expect: + results[0] instanceof Boolean && results[0] == false + results[1] instanceof Boolean && results[1] == true + results[2] instanceof Boolean && results[2] == true + results[3] instanceof Boolean && results[3] == false + + results[4] instanceof Boolean && results[4] == false + results[5] instanceof Boolean && results[5] == true + results[6] instanceof Boolean && results[6] == true + results[7] instanceof Boolean && results[7] == false + + results[8] instanceof Boolean && results[8] == false + results[9] instanceof Boolean && results[9] == true + + results[10] instanceof Boolean && results[10] == false + results[11] instanceof Boolean && results[11] == true + + results[12] instanceof Boolean && results[12] == false + results[13] instanceof Boolean && results[13] == true + } + + // '^' operator in expressions means power operation + void "test '^' operator"() { + given: + List results = evaluateMultiple( + "#{ 2^3 }", // 0 + "#{ 3L ^ 2}", // 1 + "#{ 2.0^0}", // 2 + "#{ 2f ^ 2L}", // 3 + "#{ 2^2 ^2}", // 4 + "#{ (2 ^ 32)^2}" // 5 + ) + + expect: + results[0] instanceof Long && results[0] == 8 + results[1] instanceof Long && results[1] == 9 + results[2] instanceof Double && results[2] == 1.0 + results[3] instanceof Double && results[3] == 4.0 + results[4] instanceof Long && results[4] == 16 + results[5] instanceof Long && results[5] == 9223372036854775807L + } + + void "test '&&' operator"() { + given: + List results = evaluateMultiple( + "#{ true && true }", // 0 + "#{ true && false }", // 1 + "#{ false && true }", // 2 + "#{ false && false }", // 3 + "#{ true and true }", // 4 + "#{ true and false }", // 5 + "#{ false and true }", // 6 + "#{ false and false }", // 7 + "#{ true and true && true }", // 8 + "#{ true && true and false }" // 9 + ) + + expect: + results[0] instanceof Boolean && results[0] == true + results[1] instanceof Boolean && results[1] == false + results[2] instanceof Boolean && results[2] == false + results[3] instanceof Boolean && results[3] == false + results[4] instanceof Boolean && results[4] == true + results[5] instanceof Boolean && results[5] == false + results[6] instanceof Boolean && results[6] == false + results[7] instanceof Boolean && results[7] == false + results[8] instanceof Boolean && results[8] == true + results[9] instanceof Boolean && results[9] == false + } + + void "test '||' operator"() { + given: + List results = evaluateMultiple( + "#{ true || true }", // 0 + "#{ true || false }", // 1 + "#{ false || true }", // 2 + "#{ false || false }", // 3 + "#{ true or true }", // 4 + "#{ true or false }", // 5 + "#{ false or true }", // 6 + "#{ false or false }", // 7 + "#{ true or true || true }", // 8 + "#{ true or true or false }", // 9 + "#{ true || false or false }", // 10 + "#{ false or false || false or true }" // 11 + ) + + expect: + results[0] instanceof Boolean && results[0] == true + results[1] instanceof Boolean && results[1] == true + results[2] instanceof Boolean && results[2] == true + results[3] instanceof Boolean && results[3] == false + results[4] instanceof Boolean && results[4] == true + results[5] instanceof Boolean && results[5] == true + results[6] instanceof Boolean && results[6] == true + results[7] instanceof Boolean && results[7] == false + results[8] instanceof Boolean && results[8] == true + results[9] instanceof Boolean && results[9] == true + results[10] instanceof Boolean && results[10] == true + results[11] instanceof Boolean && results[11] == true + } + + void "test 'instanceof' operator"() { + given: + List results = evaluateMultiple( + "#{ 1 instanceof T(java.lang.Integer) }", // 0 + "#{ 1 instanceof T(Integer) }", // 1 + "#{ 1L instanceof T(java.lang.Long) }", // 2 + "#{ 1f instanceof T(java.lang.Float) }", // 3 + "#{ 1d instanceof T(java.lang.Double) }", // 4 + "#{ 'str' instanceof T(java.lang.String) }", // 5 + "#{ 'str' instanceof T(Double) }", // 6 + "#{ 1L instanceof T(Integer) }", // 7 + "#{ 1f instanceof T(Double) }" // 8 + ) + + + expect: + results[0] instanceof Boolean && results[0] == true + results[1] instanceof Boolean && results[1] == true + results[2] instanceof Boolean && results[2] == true + results[3] instanceof Boolean && results[3] == true + results[4] instanceof Boolean && results[4] == true + results[5] instanceof Boolean && results[5] == true + results[6] instanceof Boolean && results[6] == false + results[7] instanceof Boolean && results[7] == false + results[8] instanceof Boolean && results[8] == false + } + + void "test 'matches' operator"() { + given: + List results = evaluateMultiple( + "#{ '123' matches '\\\\d+' }", // 0 + "#{ '5.0' matches '[0-9]*\\\\.[0-9]+(d|D)?' }", // 1 + "#{ '5.0' matches '[0-9]*\\\\.[0-9]+(d|D)' }", // 2 + "#{ 'AbC' matches '[A-Za-z]*' }", // 3 + "#{ ' ' matches '\\\\s*' }", // 4 + "#{ '' matches '\\\\s+' }" // 5 + ) + + expect: + results[0] instanceof Boolean && results[0] == true + results[1] instanceof Boolean && results[1] == true + results[2] instanceof Boolean && results[2] == false + results[3] instanceof Boolean && results[3] == true + results[4] instanceof Boolean && results[4] == true + results[5] instanceof Boolean && results[5] == false + } + + void "test 'empty' operator"() { + given: + List results = evaluateMultiple( + "#{ empty '' }", // 0 + "#{ not empty '' }", // 1 + "#{ empty null }", // 2 + "#{ not empty null }", // 3 + "#{ empty 'foo' }", // 4 + "#{ not empty 'foo' }" // 5 + ) + + expect: + results[0] == true + results[1] == false + results[2] == true + results[3] == false + results[4] == false + results[5] == true + } + + void "test '!' operator"() { + given: + List results = evaluateMultiple( + "#{ !true }", // 0 + "#{ !false }", // 1 + "#{ !!true }", // 2 + "#{ !!false }", // 3 + "#{ !!!false }", // 4 + "#{ !!!true }" // 5 + ) + + expect: + results[0] == false + results[1] == true + results[2] == true + results[3] == false + results[4] == true + results[5] == false + } + + void "test unary '-'"() { + given: + List results = evaluateMultiple( + "#{ -5 }", + "#{ -(-3) }", + "#{ -3L }", + "#{ -1.0D }" + ) + + expect: + results[0] instanceof Integer && results[0] == -5 + results[1] instanceof Integer && results[1] == 3 + results[2] instanceof Long && results[2] == -3 + results[3] instanceof Double && results[3] == -1.0 + + when: + evaluate("#{ --5 }") + + then: + thrown(Throwable.class) + } + + void "test unary '+'"() { + given: + List results = evaluateMultiple( + "#{ +5 }", + "#{ +(+3) }", + "#{ +3L }", + "#{ +1.0D }" + ) + + expect: + results[0] instanceof Integer && results[0] == 5 + results[1] instanceof Integer && results[1] == 3 + results[2] instanceof Long && results[2] == 3 + results[3] instanceof Double && results[3] == 1.0 + + when: + evaluate("#{ ++5 }") + + then: + thrown(Throwable.class) + } + + void "test parenthesized expressions"() { + given: + List results = evaluateMultiple( + "#{ (2 + 3) * 5 }", // 0 + "#{ 2 * ( 3 + 1) }", // 1 + "#{ (-1 + 9) * (2 + 3) }", // 2 + "#{ 2 ^ ( 2 + 2) }", // 3 + "#{ 5 * ( 2d * (1 - 1)) }", // 4 + "#{ 5L * ( 2d + 2f ) }" // 5 + ) + + expect: + results[0] instanceof Integer && results[0] == 25 + results[1] instanceof Integer && results[1] == 8 + results[2] instanceof Integer && results[2] == 40 + results[3] instanceof Long && results[3] == 16 + results[4] instanceof Double && results[4] == 0 + results[5] instanceof Double && results[5] == 20.0 + } +} diff --git a/inject-java/src/test/groovy/io/micronaut/expressions/TernaryOperationExpressionsSpec.groovy b/inject-java/src/test/groovy/io/micronaut/expressions/TernaryOperationExpressionsSpec.groovy new file mode 100644 index 00000000000..f3d75af5f84 --- /dev/null +++ b/inject-java/src/test/groovy/io/micronaut/expressions/TernaryOperationExpressionsSpec.groovy @@ -0,0 +1,54 @@ +package io.micronaut.expressions + +import io.micronaut.annotation.processing.test.AbstractEvaluatedExpressionsSpec + +class TernaryOperationExpressionsSpec extends AbstractEvaluatedExpressionsSpec { + + void "test elvis operator"() { + given: + List results = evaluateMultiple( + "#{ 10 ?: 5 }", + "#{ -10 ?: 5 }", + "#{ '' ?: 'test' }", + "#{ 'foo' ?: 'test' }", + "#{ 0 ?: 5 }" + ) + + expect: + results[0] instanceof Integer && results[0] == 10 + results[1] instanceof Integer && results[1] == -10 + results[2] instanceof String && results[2] == 'test' + results[3] instanceof String && results[3] == 'foo' + results[4] instanceof Integer && results[4] == 5 + } + + void "test ternary operator"() { + given: + List results = evaluateMultiple( + "#{ 15 > 10 ? 'a' : 'b' }", + "#{ 15 == 10 ? 'a' : 'b' }", + "#{ 10 > 9 ? 'a' + 'b' : 'b' + 'a' }" + ) + + expect: + results[0] instanceof String && results[0] == 'a' + results[1] instanceof String && results[1] == 'b' + results[2] instanceof String && results[2] == 'ab' + } + + void "test ternary type resolution"() { + given: + List results = evaluateMultiple( + "#{ (15 > 10 ? 'a' : 'b').length() }", + "#{ 15 > 10 ? 15L : 'test' }", + "#{ (15 > 10 ? 15L : 10) + 8}", + "#{ 10 > 15 ? 15L : true}" + ) + + expect: + results[0] instanceof Integer && results[0] == 1 + results[1] instanceof Long && results[1] == 15 + results[2] instanceof Long && results[2] == 23 + results[3] instanceof Boolean && results[3] == true + } +} diff --git a/inject-java/src/test/groovy/io/micronaut/expressions/TestExpressionsInjectionSpec.groovy b/inject-java/src/test/groovy/io/micronaut/expressions/TestExpressionsInjectionSpec.groovy new file mode 100644 index 00000000000..cd3919fb9d4 --- /dev/null +++ b/inject-java/src/test/groovy/io/micronaut/expressions/TestExpressionsInjectionSpec.groovy @@ -0,0 +1,167 @@ +package io.micronaut.expressions + +import io.micronaut.annotation.processing.test.AbstractEvaluatedExpressionsSpec +import io.micronaut.context.env.PropertySource +import io.micronaut.context.exceptions.NoSuchBeanException + +class TestExpressionsInjectionSpec extends AbstractEvaluatedExpressionsSpec { + + void "test expression field injection"() { + given: + def ctx = buildContext(""" + package test; + + import jakarta.inject.Singleton; + import io.micronaut.context.annotation.Value; + + @Singleton + class Expr { + @Value("#{ 15 ^ 2 }") + private int intValue; + + @Value("#{ 100 }") + protected Integer boxedIntValue; + + @Value("#{ T(String).join(',', 'a', 'b', 'c') }") + public String strValue; + + @Value("#{null}") + private Object nullValue; + } + """) + + def type = ctx.classLoader.loadClass('test.Expr') + def bean = ctx.getBean(type) + + expect: + bean.intValue == 225 + bean.boxedIntValue == 100 + bean.strValue == 'a,b,c' + bean.nullValue == null + + cleanup: + ctx.close() + } + + void "test expression constructor injection"() { + given: + def ctx = buildContext(""" + package test; + + import jakarta.inject.Singleton; + import io.micronaut.context.annotation.Value; + + @Singleton + class Expr { + private final Integer wrapper; + private final Integer primitive; + + public Expr(@Value("#{ 25 }") Integer wrapper, + @Value("#{ 23 }") int primitive) { + this.wrapper = wrapper; + this.primitive = primitive; + } + } + """) + + def type = ctx.classLoader.loadClass('test.Expr') + def bean = ctx.getBean(type) + + expect: + bean.wrapper == 25 + bean.primitive == 23 + + cleanup: + ctx.close() + } + + void "test expression setter injection"() { + given: + def ctx = buildContext(""" + package test; + + import jakarta.inject.Inject; + import jakarta.inject.Singleton; + import io.micronaut.context.annotation.Value; + + @Singleton + class Expr { + private Integer wrapper; + private int primitive; + + @Inject + public void setWrapper(@Value("#{ 25 }") Integer value) { + this.wrapper = value; + } + + @Inject + public void setPrimitive(@Value("#{ 23 }") int value) { + this.primitive = value; + } + } + """) + + def type = ctx.classLoader.loadClass('test.Expr') + def bean = ctx.getBean(type) + + expect: + bean.wrapper == 25 + bean.primitive == 23 + + cleanup: + ctx.close() + } + + void "test expressions in @Factory injection"() { + given: + def ctx = buildContext(""" + package test; + + import io.micronaut.context.annotation.Bean; + import io.micronaut.context.annotation.Factory; + import jakarta.inject.Inject; + import jakarta.inject.Singleton; + import io.micronaut.context.annotation.Value; + + @jakarta.inject.Singleton + class Context { + public String getContextValue() { + return "context value"; + } + } + + class Expr { + private final Integer wrapper; + private final int primitive; + private final String contextValue; + + Expr(Integer wrapper, int primitive, String contextValue) { + this.wrapper = wrapper; + this.primitive = primitive; + this.contextValue = contextValue; + } + } + + @Factory + class TestFactory { + @Bean + public Expr factoryBean(@Value("#{ 25 }") Integer wrapper, + @Value("#{ 23 }") int primitive, + @Value("#{ #contextValue }") String contextValue) { + return new Expr(wrapper, primitive, contextValue); + } + } + """) + + def type = ctx.classLoader.loadClass('test.Expr') + def bean = ctx.getBean(type) + + expect: + bean.wrapper == 25 + bean.primitive == 23 + bean.contextValue == "context value" + + cleanup: + ctx.close() + } +} diff --git a/inject-java/src/test/groovy/io/micronaut/expressions/TestExpressionsUsageSpec.groovy b/inject-java/src/test/groovy/io/micronaut/expressions/TestExpressionsUsageSpec.groovy new file mode 100644 index 00000000000..12ec1b2ed6f --- /dev/null +++ b/inject-java/src/test/groovy/io/micronaut/expressions/TestExpressionsUsageSpec.groovy @@ -0,0 +1,130 @@ +package io.micronaut.expressions + +import io.micronaut.annotation.processing.test.AbstractEvaluatedExpressionsSpec +import io.micronaut.context.env.PropertySource +import io.micronaut.context.exceptions.CircularDependencyException +import io.micronaut.context.exceptions.ExpressionEvaluationException +import io.micronaut.context.exceptions.NoSuchBeanException + +class TestExpressionsUsageSpec extends AbstractEvaluatedExpressionsSpec { + void "test expression array in requires"() { + given: + def ctx = buildContext(""" + package test; + import io.micronaut.context.annotation.Requires; + import jakarta.inject.Singleton; + + @Singleton + @Requires(env = {"#{ 'test' }"}) + class Expr { + } + """) + + when: + getBean(ctx, "test.Expr") + + then: + noExceptionThrown() + + cleanup: + ctx.close() + } + + void "test requires expression property value"() { + given: + def ctx = buildContext(""" + package test; + + import io.micronaut.context.annotation.Requires; + import jakarta.inject.Singleton; + + @Singleton + @Requires(property = "test-property", value = "#{ 'test-value'.toUpperCase() }") + class Expr { + } + """) + + def type = ctx.classLoader.loadClass('test.Expr') + + when: + ctx.environment.addPropertySource(PropertySource.of("test", ['test-property': 'TEST-VALUE'])) + ctx.getBean(type) + + then: + noExceptionThrown() + + cleanup: + ctx.close() + } + + void "test requires expression context value"() { + given: + def ctx = buildContext(""" + package test; + + import io.micronaut.context.annotation.ConfigurationProperties; + import io.micronaut.context.annotation.Requires; + + import jakarta.inject.Singleton; + + @Singleton + @Requires(property = "test.enabled", value = "#{ #enabled }") + class Expr { + } + + @ConfigurationProperties("test") + @jakarta.inject.Singleton + class Context { + private boolean enabled; + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + public boolean isEnabled() { + return enabled; + } + } + """) + + def type = ctx.classLoader.loadClass('test.Expr') + + when: + ctx.environment.addPropertySource(PropertySource.of("test", ['test.enabled': false])) + ctx.getBean(type) + + then: + noExceptionThrown() + + cleanup: + ctx.close() + } + + void "test disabled by expression bean"() { + given: + def ctx = buildContext(""" + package test; + + import io.micronaut.context.annotation.Requires; + + import jakarta.inject.Singleton; + + @Singleton + @Requires(property = "test.property", value = "#{ 5 * 2 }") + class Expr { + } + """) + + def type = ctx.classLoader.loadClass('test.Expr') + + when: + ctx.environment.addPropertySource(PropertySource.of("test", ['test.property': 15])) + ctx.getBean(type) + + then: + thrown(NoSuchBeanException) + + cleanup: + ctx.close() + } +} diff --git a/inject-java/src/test/groovy/io/micronaut/expressions/TypeIdentifierExpressionsSpec.groovy b/inject-java/src/test/groovy/io/micronaut/expressions/TypeIdentifierExpressionsSpec.groovy new file mode 100644 index 00000000000..dbd40aa4d78 --- /dev/null +++ b/inject-java/src/test/groovy/io/micronaut/expressions/TypeIdentifierExpressionsSpec.groovy @@ -0,0 +1,52 @@ +package io.micronaut.expressions + +import io.micronaut.annotation.processing.test.AbstractEvaluatedExpressionsSpec + +class TypeIdentifierExpressionsSpec extends AbstractEvaluatedExpressionsSpec { + + void "test static methods"() { + given: + List results = evaluateMultiple( + "#{ T(Math).random() }", + "#{ T(java.lang.Math).random() }", + "#{ T(Integer).valueOf('10') }", + "#{ T(String).join(',', '1', '2', '3') }", + "#{ T(String).join(',', 'a', 'b').repeat(2) }" + ) + + expect: + results[0] instanceof Double && results[0] >= 0 && results[0] < 1 + results[1] instanceof Double && results[1] >= 0 && results[1] < 1 + results[2] instanceof Integer && results[2] == 10 + results[3] instanceof String && results[3] == "1,2,3" + results[4] instanceof String && results[4] == "a,ba,b" + } + + void "test type identifier as argument"() { + given: + Object expr1 = evaluateAgainstContext("#{ #getType(T(java.lang.String)) }", + """ + @jakarta.inject.Singleton + class Context { + Class getType(Class type) { + return type; + } + } + """) + + Object expr2 = evaluateAgainstContext("#{ #getType(T(String), T(Object)) }", + """ + @jakarta.inject.Singleton + class Context { + Class getType(Class... types) { + return types[1]; + } + } + """) + + expect: + expr1 instanceof Class && expr1 == String.class + expr2 instanceof Class && expr2 == Object.class + } + +} diff --git a/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/annotation/KotlinAnnotationMetadataBuilder.kt b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/annotation/KotlinAnnotationMetadataBuilder.kt index 3c247593e42..b2b02b04f6e 100644 --- a/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/annotation/KotlinAnnotationMetadataBuilder.kt +++ b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/annotation/KotlinAnnotationMetadataBuilder.kt @@ -281,7 +281,7 @@ internal class KotlinAnnotationMetadataBuilder(private val symbolProcessorEnviro annotationValues: MutableMap ) { if (!annotationValues.containsKey(memberName)) { - val value = readAnnotationValue(originatingElement, member, memberName, annotationValue) + val value = readAnnotationValue(originatingElement, member, annotationName, memberName, annotationValue) if (value != null) { validateAnnotationValue(originatingElement, annotationName, member, memberName, value) annotationValues[memberName] = value @@ -314,6 +314,7 @@ internal class KotlinAnnotationMetadataBuilder(private val symbolProcessorEnviro override fun readAnnotationValue( originatingElement: KSAnnotated, member: KSAnnotated, + annotationName: String, memberName: String, annotationValue: Any ): Any? { @@ -324,7 +325,18 @@ internal class KotlinAnnotationMetadataBuilder(private val symbolProcessorEnviro is Array<*> -> { toArray(annotationValue.toList(), originatingElement) } - else -> readAnnotationValue(originatingElement, annotationValue) + else -> { + if (isEvaluatedExpression(annotationValue)) { + return buildEvaluatedExpressionReference( + originatingElement, + annotationName, + memberName, + annotationValue + ) + } else { + return readAnnotationValue(originatingElement, annotationValue) + } + } } } @@ -366,6 +378,15 @@ internal class KotlinAnnotationMetadataBuilder(private val symbolProcessorEnviro } } + override fun getOriginatingClassName(orginatingElement: KSAnnotated): String { + return if (orginatingElement is KSClassDeclaration) { + orginatingElement.getBinaryName(resolver, visitorContext) + } else { + val classDeclaration = orginatingElement.getClassDeclaration(visitorContext) + classDeclaration.getBinaryName(resolver, visitorContext) + } + } + private fun readDefaultValuesReflectively(classDeclaration : KSClassDeclaration, annotationType: KSAnnotated, vararg path : String): MutableMap { var o: Any? = findValueReflectively(annotationType, *path) val declaredProperties = classDeclaration.getDeclaredProperties() @@ -441,12 +462,21 @@ internal class KotlinAnnotationMetadataBuilder(private val symbolProcessorEnviro val values: Map = readAnnotationRawValues(annotationMirror) val converted: MutableMap = mutableMapOf() for ((key, value1) in values) { - val value = value1!! + var value = value1!! + val memberName = key.simpleName.asString() + if (isEvaluatedExpression(value)) { + value = buildEvaluatedExpressionReference( + originatingElement, + annotationName, + memberName, + value + ) + } readAnnotationRawValues( originatingElement, annotationName, key, - key.simpleName.asString(), + memberName, value, converted ) @@ -574,6 +604,7 @@ internal class KotlinAnnotationMetadataBuilder(private val symbolProcessorEnviro if (value is KSAnnotation) { return readNestedAnnotationValue(originatingElement, value) } + return value } diff --git a/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/beans/BeanDefinitionProcessor.kt b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/beans/BeanDefinitionProcessor.kt index 0f83136adf3..b371110638d 100644 --- a/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/beans/BeanDefinitionProcessor.kt +++ b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/beans/BeanDefinitionProcessor.kt @@ -21,12 +21,13 @@ import com.google.devtools.ksp.processing.SymbolProcessorEnvironment import com.google.devtools.ksp.symbol.* import io.micronaut.context.annotation.Context import io.micronaut.core.annotation.Generated -import io.micronaut.inject.annotation.AbstractAnnotationMetadataBuilder +import io.micronaut.expressions.EvaluatedExpressionWriter import io.micronaut.inject.processing.BeanDefinitionCreator import io.micronaut.inject.processing.BeanDefinitionCreatorFactory import io.micronaut.inject.processing.ProcessingException import io.micronaut.inject.writer.BeanDefinitionReferenceWriter import io.micronaut.inject.writer.BeanDefinitionVisitor +import io.micronaut.inject.writer.BeanDefinitionWriter import io.micronaut.kotlin.processing.KotlinOutputVisitor import io.micronaut.kotlin.processing.visitor.KotlinClassElement import io.micronaut.kotlin.processing.visitor.KotlinNativeElement @@ -36,9 +37,10 @@ import java.io.IOException internal class BeanDefinitionProcessor(private val environment: SymbolProcessorEnvironment): SymbolProcessor { private val beanDefinitionMap = mutableMapOf() + private var visitorContext : KotlinVisitorContext? = null override fun process(resolver: Resolver): List { - val visitorContext = KotlinVisitorContext(environment, resolver) + visitorContext = KotlinVisitorContext(environment, resolver) val elements = resolver.getAllFiles() .flatMap { file: KSFile -> @@ -53,7 +55,7 @@ internal class BeanDefinitionProcessor(private val environment: SymbolProcessorE .toList() try { - processClassDeclarations(elements, visitorContext) + processClassDeclarations(elements, visitorContext!!) } catch (e: ProcessingException) { handleProcessingException(environment, e) } @@ -92,7 +94,7 @@ internal class BeanDefinitionProcessor(private val environment: SymbolProcessorE for (beanDefinitionCreator in beanDefinitionMap.values) { for (writer in beanDefinitionCreator.build()) { if (processed.add(writer.beanDefinitionName)) { - processBeanDefinitions(writer, outputVisitor, processed) + processBeanDefinitions(writer, outputVisitor, visitorContext!!, processed) count++ } } @@ -103,7 +105,7 @@ internal class BeanDefinitionProcessor(private val environment: SymbolProcessorE } catch (e: ProcessingException) { handleProcessingException(environment, e) } finally { - AbstractAnnotationMetadataBuilder.clearMutated() + BeanDefinitionWriter.finish() beanDefinitionMap.clear() } } @@ -134,12 +136,14 @@ internal class BeanDefinitionProcessor(private val environment: SymbolProcessorE private fun processBeanDefinitions( beanDefinitionWriter: BeanDefinitionVisitor, outputVisitor: KotlinOutputVisitor, + visitorContext: KotlinVisitorContext, processed: HashSet ) { try { beanDefinitionWriter.visitBeanDefinitionEnd() if (beanDefinitionWriter.isEnabled) { beanDefinitionWriter.accept(outputVisitor) + processEvaluatedExpressions(beanDefinitionWriter, visitorContext, outputVisitor) val beanDefinitionReferenceWriter = BeanDefinitionReferenceWriter(beanDefinitionWriter) beanDefinitionReferenceWriter.setRequiresMethodProcessing(beanDefinitionWriter.requiresMethodProcessing()) val className = beanDefinitionReferenceWriter.beanDefinitionQualifiedClassName @@ -156,4 +160,19 @@ internal class BeanDefinitionProcessor(private val environment: SymbolProcessorE } } + private fun processEvaluatedExpressions( + beanDefinitionWriter: BeanDefinitionVisitor, + visitorContext: KotlinVisitorContext, + outputVisitor: KotlinOutputVisitor + ) { + for (expressionMetadata in beanDefinitionWriter.evaluatedExpressions) { + val expressionWriter = EvaluatedExpressionWriter( + expressionMetadata, + visitorContext, + beanDefinitionWriter.originatingElement + ) + expressionWriter.accept(outputVisitor) + } + } + } diff --git a/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/extensions.kt b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/extensions.kt index e863d7a6d47..cedbb75c802 100644 --- a/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/extensions.kt +++ b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/extensions.kt @@ -99,6 +99,28 @@ internal fun KSAnnotated.getClassDeclaration(visitorContext: KotlinVisitorContex val declaration = this.type.resolve().declaration return declaration.getClassDeclaration(visitorContext) } + is KSValueParameter -> { + val p = this.parent + if (p is KSDeclaration) { + return p.getClassDeclaration(visitorContext) + } else { + return visitorContext.resolver.getJavaClassByName(Object::class.java.name)!! + } + } + is KSFunctionDeclaration -> { + val parentDeclaration = this.parentDeclaration + if (parentDeclaration != null) { + return parentDeclaration.getClassDeclaration(visitorContext) + } + return visitorContext.resolver.getJavaClassByName(Object::class.java.name)!! + } + is KSPropertyDeclaration -> { + val parentDeclaration = this.parentDeclaration + if (parentDeclaration != null) { + return parentDeclaration.getClassDeclaration(visitorContext) + } + return visitorContext.resolver.getJavaClassByName(Object::class.java.name)!! + } else -> { return visitorContext.resolver.getJavaClassByName(Object::class.java.name)!! } diff --git a/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/KotlinVisitorContext.kt b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/KotlinVisitorContext.kt index b42cb7217f4..b17a1d95137 100644 --- a/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/KotlinVisitorContext.kt +++ b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/KotlinVisitorContext.kt @@ -25,6 +25,8 @@ import io.micronaut.core.convert.ArgumentConversionContext import io.micronaut.core.convert.value.MutableConvertibleValues import io.micronaut.core.convert.value.MutableConvertibleValuesMap import io.micronaut.core.util.StringUtils +import io.micronaut.expressions.context.DefaultExpressionCompilationContextFactory +import io.micronaut.expressions.context.ExpressionCompilationContextFactory import io.micronaut.inject.annotation.AbstractAnnotationMetadataBuilder import io.micronaut.inject.ast.ClassElement import io.micronaut.inject.ast.Element @@ -51,6 +53,7 @@ internal open class KotlinVisitorContext( private val outputVisitor = KotlinOutputVisitor(environment) val annotationMetadataBuilder: KotlinAnnotationMetadataBuilder private val elementAnnotationMetadataFactory: KotlinElementAnnotationMetadataFactory + private val expressionCompilationContextFactory : ExpressionCompilationContextFactory init { visitorAttributes = MutableConvertibleValuesMap() @@ -58,6 +61,7 @@ internal open class KotlinVisitorContext( elementFactory = KotlinElementFactory(this) elementAnnotationMetadataFactory = KotlinElementAnnotationMetadataFactory(false, annotationMetadataBuilder) + expressionCompilationContextFactory = DefaultExpressionCompilationContextFactory(this) } override fun get( @@ -175,6 +179,10 @@ internal open class KotlinVisitorContext( return elementAnnotationMetadataFactory } + override fun getExpressionCompilationContextFactory(): ExpressionCompilationContextFactory { + return expressionCompilationContextFactory + } + override fun getAnnotationMetadataBuilder(): AbstractAnnotationMetadataBuilder<*, *> { return annotationMetadataBuilder } diff --git a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/expressions/TestExpressionsInjectionSpec.groovy b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/expressions/TestExpressionsInjectionSpec.groovy new file mode 100644 index 00000000000..f2fbe8f08b9 --- /dev/null +++ b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/expressions/TestExpressionsInjectionSpec.groovy @@ -0,0 +1,29 @@ +package io.micronaut.kotlin.processing.expressions + +import spock.lang.Specification + +import static io.micronaut.annotation.processing.test.KotlinCompiler.buildContext + +class TestExpressionsInjectionSpec extends Specification { + void "test expression constructor injection"() { + given: + def ctx = buildContext(""" + package test; + + import jakarta.inject.Singleton; + import io.micronaut.context.annotation.Value; + + @Singleton + class Expr(@Value("#{ 25 }") val num : Int) + """) + + def type = ctx.classLoader.loadClass('test.Expr') + def bean = ctx.getBean(type) + + expect: + bean.num == 25 + + cleanup: + ctx.close() + } +} diff --git a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/visitor/BeanIntrospectionSpec.groovy b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/visitor/BeanIntrospectionSpec.groovy index ce6c16908c4..c8138a20684 100644 --- a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/visitor/BeanIntrospectionSpec.groovy +++ b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/visitor/BeanIntrospectionSpec.groovy @@ -17,7 +17,6 @@ import io.micronaut.core.reflect.exception.InstantiationException import io.micronaut.core.type.Argument import io.micronaut.core.type.GenericPlaceholder import io.micronaut.inject.ExecutableMethod -import io.micronaut.inject.beans.visitor.MappedSuperClassIntrospectionMapper import io.micronaut.kotlin.processing.elementapi.SomeEnum import io.micronaut.kotlin.processing.elementapi.TestClass @@ -1332,7 +1331,7 @@ class Test then:"The reference is valid" reference != null - reference.getBeanType() == MappedSuperClassIntrospectionMapper + reference.getBeanType() == io.micronaut.inject.beans.visitor.MappedSuperClassIntrospectionMapper } void "test write bean introspection data"() { diff --git a/inject/src/main/java/io/micronaut/context/AbstractBeanResolutionContext.java b/inject/src/main/java/io/micronaut/context/AbstractBeanResolutionContext.java index 569c4ab3e45..d22a436bcd5 100644 --- a/inject/src/main/java/io/micronaut/context/AbstractBeanResolutionContext.java +++ b/inject/src/main/java/io/micronaut/context/AbstractBeanResolutionContext.java @@ -728,6 +728,11 @@ public InjectionPoint getInjectionPoint() { public BeanDefinition getDeclaringBean() { return getDeclaringType(); } + + @Override + public AnnotationMetadata getAnnotationMetadata() { + return getArgument().getAnnotationMetadata(); + } } /** diff --git a/inject/src/main/java/io/micronaut/context/AbstractExecutableMethodsDefinition.java b/inject/src/main/java/io/micronaut/context/AbstractExecutableMethodsDefinition.java index 5984ac1e81f..ecb96501661 100644 --- a/inject/src/main/java/io/micronaut/context/AbstractExecutableMethodsDefinition.java +++ b/inject/src/main/java/io/micronaut/context/AbstractExecutableMethodsDefinition.java @@ -16,7 +16,11 @@ package io.micronaut.context; import io.micronaut.context.env.Environment; -import io.micronaut.core.annotation.*; +import io.micronaut.core.annotation.AnnotationMetadata; +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.NonNull; +import io.micronaut.core.annotation.Nullable; +import io.micronaut.core.annotation.UsedByGeneratedCode; import io.micronaut.core.reflect.ClassUtils; import io.micronaut.core.type.Argument; import io.micronaut.core.type.ReturnType; @@ -26,9 +30,15 @@ import io.micronaut.inject.ExecutableMethodsDefinition; import io.micronaut.inject.annotation.AbstractEnvironmentAnnotationMetadata; import io.micronaut.inject.annotation.AnnotationMetadataHierarchy; +import io.micronaut.inject.annotation.EvaluatedAnnotationMetadata; import java.lang.reflect.Method; -import java.util.*; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; import java.util.stream.IntStream; import java.util.stream.Stream; @@ -40,11 +50,12 @@ * @since 3.0 */ @Internal -public abstract class AbstractExecutableMethodsDefinition implements ExecutableMethodsDefinition, EnvironmentConfigurable { +public abstract class AbstractExecutableMethodsDefinition implements ExecutableMethodsDefinition, EnvironmentConfigurable, ContextConfigurable { private final MethodReference[] methodsReferences; private final DispatchedExecutableMethod[] executableMethods; private Environment environment; + private BeanContext beanContext; private List> executableMethodsList; protected AbstractExecutableMethodsDefinition(MethodReference[] methodsReferences) { @@ -62,6 +73,16 @@ public void configure(Environment environment) { } } + @Override + public void configure(BeanContext beanContext) { + this.beanContext = beanContext; + for (DispatchedExecutableMethod executableMethod : executableMethods) { + if (executableMethod != null) { + executableMethod.configure(beanContext); + } + } + } + @Override public Collection> getExecutableMethods() { if (executableMethodsList == null) { @@ -102,6 +123,9 @@ public ExecutableMethod getExecutableMethodByIndex(int index) { if (environment != null) { executableMethod.configure(environment); } + if (beanContext != null) { + executableMethod.configure(beanContext); + } executableMethods[index] = executableMethod; } return executableMethod; @@ -302,7 +326,9 @@ public String toString() { * @param The type * @param The result type */ - private static final class DispatchedExecutableMethod implements ExecutableMethod, EnvironmentConfigurable { + private static final class DispatchedExecutableMethod implements ExecutableMethod, + EnvironmentConfigurable, + ContextConfigurable { private final AbstractExecutableMethodsDefinition dispatcher; private final int index; @@ -327,11 +353,24 @@ public void configure(Environment environment) { } } + @Override + public void configure(BeanContext beanContext) { + annotationMetadata = EvaluatedAnnotationMetadata.wrapIfNecessary(annotationMetadata); + if (annotationMetadata instanceof EvaluatedAnnotationMetadata eam) { + eam.configure(beanContext); + } + } + @Override public boolean hasPropertyExpressions() { return annotationMetadata.hasPropertyExpressions(); } + @Override + public boolean hasEvaluatedExpressions() { + return annotationMetadata.hasEvaluatedExpressions(); + } + @Override public boolean isAbstract() { return methodReference.isAbstract; @@ -492,5 +531,4 @@ protected Environment getEnvironment() { return environment; } } - } diff --git a/inject/src/main/java/io/micronaut/context/AbstractInitializableBeanDefinition.java b/inject/src/main/java/io/micronaut/context/AbstractInitializableBeanDefinition.java index 1453f786d4f..7d152d88365 100644 --- a/inject/src/main/java/io/micronaut/context/AbstractInitializableBeanDefinition.java +++ b/inject/src/main/java/io/micronaut/context/AbstractInitializableBeanDefinition.java @@ -32,6 +32,7 @@ import io.micronaut.context.exceptions.NoSuchBeanException; import io.micronaut.core.annotation.AnnotationMetadata; import io.micronaut.core.annotation.AnnotationUtil; +import io.micronaut.core.annotation.AnnotationValue; import io.micronaut.core.annotation.Internal; import io.micronaut.core.annotation.NextMajorVersion; import io.micronaut.core.annotation.NonNull; @@ -58,6 +59,7 @@ import io.micronaut.inject.MethodInjectionPoint; import io.micronaut.inject.ValidatedBeanDefinition; import io.micronaut.inject.annotation.AbstractEnvironmentAnnotationMetadata; +import io.micronaut.inject.annotation.EvaluatedAnnotationMetadata; import io.micronaut.inject.qualifiers.InterceptorBindingQualifier; import io.micronaut.inject.qualifiers.Qualifiers; import io.micronaut.inject.qualifiers.TypeAnnotationQualifier; @@ -103,7 +105,7 @@ */ @Internal public abstract class AbstractInitializableBeanDefinition extends AbstractBeanContextConditional - implements InstantiatableBeanDefinition, InjectableBeanDefinition, EnvironmentConfigurable { + implements InstantiatableBeanDefinition, InjectableBeanDefinition, EnvironmentConfigurable, ContextConfigurable { private static final Logger LOG = LoggerFactory.getLogger(AbstractInitializableBeanDefinition.class); private final Class type; @@ -158,13 +160,13 @@ protected AbstractInitializableBeanDefinition( if (annotationMetadata == null || annotationMetadata == AnnotationMetadata.EMPTY_METADATA) { this.annotationMetadata = AnnotationMetadata.EMPTY_METADATA; } else { + AnnotationMetadata beanAnnotationMetadata = annotationMetadata; if (annotationMetadata.hasPropertyExpressions()) { // we make a copy of the result of annotation metadata which is normally a reference // to the class metadata - this.annotationMetadata = new BeanAnnotationMetadata(annotationMetadata); - } else { - this.annotationMetadata = annotationMetadata; + beanAnnotationMetadata = new BeanAnnotationMetadata(annotationMetadata); } + this.annotationMetadata = EvaluatedAnnotationMetadata.wrapIfNecessary(beanAnnotationMetadata); } this.constructor = constructor; this.methodInjection = methodInjection; @@ -271,6 +273,11 @@ public final boolean hasPropertyExpressions() { return getAnnotationMetadata().hasPropertyExpressions(); } + @Override + public boolean hasEvaluatedExpressions() { + return getAnnotationMetadata().hasEvaluatedExpressions(); + } + @Override public final @NonNull List> getTypeArguments(String type) { @@ -641,6 +648,97 @@ public final void configure(Environment environment) { } } + @Override + public void configure(BeanContext beanContext) { + if (beanContext == null) { + return; + } + + if (annotationMetadata instanceof EvaluatedAnnotationMetadata eam) { + eam.configure(beanContext); + eam.setBeanDefinition(this); + } + + if (constructor != null) { + if (constructor instanceof MethodReference mr) { + if (mr.annotationMetadata instanceof EvaluatedAnnotationMetadata eam) { + eam.configure(beanContext); + eam.setBeanDefinition(this); + } + + if (mr.arguments != null) { + for (Argument argument: mr.arguments) { + if (argument instanceof ExpressionsAwareArgument exprArg) { + exprArg.configure(beanContext); + exprArg.setBeanDefinition(this); + } + } + } + } + if (constructor instanceof FieldReference fr + && fr.argument instanceof ExpressionsAwareArgument exprArg) { + exprArg.configure(beanContext); + exprArg.setBeanDefinition(this); + } + } + + if (constructorInjectionPoint != null) { + if (constructorInjectionPoint.getAnnotationMetadata() instanceof EvaluatedAnnotationMetadata eam) { + eam.configure(beanContext); + eam.setBeanDefinition(this); + } + } + + if (methodInjection != null) { + for (MethodReference methodReference: methodInjection) { + if (methodReference.annotationMetadata instanceof EvaluatedAnnotationMetadata eam) { + eam.configure(beanContext); + eam.setBeanDefinition(this); + } + + if (methodReference.arguments != null) { + for (Argument argument: methodReference.arguments) { + if (argument instanceof ExpressionsAwareArgument exprArg) { + exprArg.configure(beanContext); + exprArg.setBeanDefinition(this); + } + } + } + } + } + + if (methodInjectionPoints != null) { + for (MethodInjectionPoint methodInjectionPoint : methodInjectionPoints) { + if (methodInjectionPoint.getAnnotationMetadata() instanceof EvaluatedAnnotationMetadata eam) { + eam.configure(beanContext); + eam.setBeanDefinition(this); + } + } + } + + if (fieldInjection != null) { + for (FieldReference fieldReference: fieldInjection) { + if (fieldReference.argument instanceof ExpressionsAwareArgument exprArg) { + exprArg.configure(beanContext); + exprArg.setBeanDefinition(this); + } + } + } + + if (fieldInjectionPoints != null) { + for (FieldInjectionPoint fieldInjectionPoint : fieldInjectionPoints) { + if (fieldInjectionPoint.getAnnotationMetadata() instanceof EvaluatedAnnotationMetadata eam) { + eam.configure(beanContext); + eam.setBeanDefinition(this); + } + } + } + + if (executableMethodsDefinition instanceof ContextConfigurable ctxConfigurable) { + ctxConfigurable.configure(beanContext); + } + } + /** * Allows printing warning messages produced by the compiler. * @@ -1082,6 +1180,15 @@ protected final Object getPropertyPlaceholderValueForMethodArgument(BeanResoluti } } + @Internal + @UsedByGeneratedCode + protected final Object getEvaluatedExpressionValueForMethodArgument(int methodIndex, + int argIndex) { + MethodReference methodRef = methodInjection[methodIndex]; + Argument argument = methodRef.arguments[argIndex]; + return getExpressionValueForArgument(argument); + } + /** * Obtains a property value for the given method argument. * @@ -1205,7 +1312,7 @@ protected final > R getBeansOfTypeForMethodArgument(B Argument argument = resolveArgument(context, argumentIndex, methodRef.arguments); try (BeanResolutionContext.Path ignored = resolutionContext.getPath().pushMethodArgumentResolve(this, methodRef.methodName, argument, methodRef.arguments)) { - return resolveBeansOfType(resolutionContext, context, argument, resolveEnvironmentArgument(context, genericType), qualifier); + return resolveBeansOfType(resolutionContext, context, argument, resolveArgument(context, genericType), qualifier); } } @@ -1247,7 +1354,7 @@ protected final Object getBeanForSetter(BeanResolutionContext resolutionContext, protected final Collection getBeansOfTypeForSetter(BeanResolutionContext resolutionContext, BeanContext context, String setterName, Argument argument, Argument genericType, Qualifier qualifier) { try (BeanResolutionContext.Path ignored = resolutionContext.getPath().pushMethodArgumentResolve(this, setterName, argument, new Argument[]{argument})) { - return resolveBeansOfType(resolutionContext, context, argument, resolveEnvironmentArgument(context, genericType), qualifier); + return resolveBeansOfType(resolutionContext, context, argument, resolveArgument(context, genericType), qualifier); } } @@ -1272,7 +1379,7 @@ protected final Optional findBeanForMethodArgument(BeanResolutionContext Argument argument = resolveArgument(context, argIndex, methodRef.arguments); try (BeanResolutionContext.Path ignored = resolutionContext.getPath().pushMethodArgumentResolve(this, methodRef.methodName, argument, methodRef.arguments)) { - return resolveOptionalBean(resolutionContext, argument, resolveEnvironmentArgument(context, genericType), qualifier); + return resolveOptionalBean(resolutionContext, argument, resolveArgument(context, genericType), qualifier); } } @@ -1296,7 +1403,7 @@ protected final Stream getStreamOfTypeForMethodArgument(BeanResolutionContext Argument argument = resolveArgument(context, argIndex, methodRef.arguments); try (BeanResolutionContext.Path ignored = resolutionContext.getPath().pushMethodArgumentResolve(this, methodRef.methodName, argument, methodRef.arguments)) { - return resolveStreamOfType(resolutionContext, argument, resolveEnvironmentArgument(context, genericType), qualifier); + return resolveStreamOfType(resolutionContext, argument, resolveArgument(context, genericType), qualifier); } } @@ -1327,7 +1434,7 @@ protected final Map getMapOfTypeForMethodArgument( Argument> argument = resolveArgument(context, argIndex, methodRef.arguments); try (BeanResolutionContext.Path ignored = resolutionContext.getPath().pushMethodArgumentResolve(this, methodRef.methodName, argument, methodRef.arguments)) { - return resolveMapOfType(resolutionContext, argument, resolveEnvironmentArgument(context, genericType), qualifier); + return resolveMapOfType(resolutionContext, argument, resolveArgument(context, genericType), qualifier); } } @@ -1439,6 +1546,14 @@ protected final Object getPropertyValueForConstructorArgument(BeanResolutionCont } } + @Internal + @UsedByGeneratedCode + protected final Object getEvaluatedExpressionValueForConstructorArgument(int argIndex) { + MethodReference constructorRef = (MethodReference) constructor; + Argument argument = constructorRef.arguments[argIndex]; + return getExpressionValueForArgument(argument); + } + /** * Obtains a property value for a bean definition for a constructor at the given index *

@@ -1495,7 +1610,7 @@ protected final Collection getBeansOfTypeForConstructorArgument(BeanReso MethodReference constructorMethodRef = (MethodReference) constructor; Argument argument = resolveArgument(context, argumentIndex, constructorMethodRef.arguments); try (BeanResolutionContext.Path ignored = resolutionContext.getPath().pushConstructorResolve(this, argument)) { - return resolveBeansOfType(resolutionContext, context, argument, resolveEnvironmentArgument(context, genericType), qualifier); + return resolveBeansOfType(resolutionContext, context, argument, resolveArgument(context, genericType), qualifier); } } @@ -1519,7 +1634,7 @@ protected final >> R getBeanRegistra MethodReference constructorMethodRef = (MethodReference) constructor; Argument argument = resolveArgument(context, argumentIndex, constructorMethodRef.arguments); try (BeanResolutionContext.Path ignored = resolutionContext.getPath().pushConstructorResolve(this, argument)) { - return resolveBeanRegistrations(resolutionContext, argument, resolveEnvironmentArgument(context, genericType), qualifier); + return resolveBeanRegistrations(resolutionContext, argument, resolveArgument(context, genericType), qualifier); } } @@ -1542,7 +1657,7 @@ protected final BeanRegistration getBeanRegistrationForConstructorArgumen MethodReference constructorMethodRef = (MethodReference) constructor; Argument argument = resolveArgument(context, argumentIndex, constructorMethodRef.arguments); try (BeanResolutionContext.Path ignored = resolutionContext.getPath().pushConstructorResolve(this, argument)) { - return resolveBeanRegistration(resolutionContext, context, argument, resolveEnvironmentArgument(context, genericType), qualifier); + return resolveBeanRegistration(resolutionContext, context, argument, resolveArgument(context, genericType), qualifier); } } @@ -1568,7 +1683,7 @@ protected final >> R getBeanRegistra Argument argument = resolveArgument(context, argIndex, methodReference.arguments); try (BeanResolutionContext.Path ignored = resolutionContext.getPath() .pushMethodArgumentResolve(this, methodReference.methodName, argument, methodReference.arguments)) { - return resolveBeanRegistrations(resolutionContext, argument, resolveEnvironmentArgument(context, genericType), qualifier); + return resolveBeanRegistrations(resolutionContext, argument, resolveArgument(context, genericType), qualifier); } } @@ -1593,7 +1708,7 @@ protected final BeanRegistration getBeanRegistrationForMethodArgument(Bea Argument argument = resolveArgument(context, argIndex, methodRef.arguments); try (BeanResolutionContext.Path ignored = resolutionContext.getPath() .pushMethodArgumentResolve(this, methodRef.methodName, argument, methodRef.arguments)) { - return resolveBeanRegistration(resolutionContext, context, argument, resolveEnvironmentArgument(context, genericType), qualifier); + return resolveBeanRegistration(resolutionContext, context, argument, resolveArgument(context, genericType), qualifier); } } @@ -1616,7 +1731,7 @@ protected final Stream getStreamOfTypeForConstructorArgument(BeanResoluti MethodReference constructorMethodRef = (MethodReference) constructor; Argument argument = resolveArgument(context, argIndex, constructorMethodRef.arguments); try (BeanResolutionContext.Path ignored = resolutionContext.getPath().pushConstructorResolve(this, argument)) { - return resolveStreamOfType(resolutionContext, argument, resolveEnvironmentArgument(context, genericType), qualifier); + return resolveStreamOfType(resolutionContext, argument, resolveArgument(context, genericType), qualifier); } } @@ -1647,7 +1762,7 @@ protected final Map getMapOfTypeForConstructorArgument( } Argument> argument = resolveArgument(context, argIndex, constructorMethodRef.arguments); try (BeanResolutionContext.Path ignored = resolutionContext.getPath().pushConstructorResolve(this, argument)) { - return resolveMapOfType(resolutionContext, argument, resolveEnvironmentArgument(context, genericType), qualifier); + return resolveMapOfType(resolutionContext, argument, resolveArgument(context, genericType), qualifier); } } @@ -1670,7 +1785,7 @@ protected final Optional findBeanForConstructorArgument(BeanResolutionCon MethodReference constructorMethodRef = (MethodReference) constructor; Argument argument = resolveArgument(context, argIndex, constructorMethodRef.arguments); try (BeanResolutionContext.Path ignored = resolutionContext.getPath().pushConstructorResolve(this, argument)) { - return resolveOptionalBean(resolutionContext, argument, resolveEnvironmentArgument(context, genericType), qualifier); + return resolveOptionalBean(resolutionContext, argument, resolveArgument(context, genericType), qualifier); } } @@ -1689,7 +1804,7 @@ protected final Optional findBeanForConstructorArgument(BeanResolutionCon @Internal @UsedByGeneratedCode protected final K getBeanForField(BeanResolutionContext resolutionContext, BeanContext context, int fieldIndex, Qualifier qualifier) { - final Argument argument = resolveEnvironmentArgument(context, fieldInjection[fieldIndex].argument); + final Argument argument = resolveArgument(context, fieldInjection[fieldIndex].argument); try (BeanResolutionContext.Path ignored = resolutionContext.getPath().pushFieldResolve(this, argument)) { return resolveBean(resolutionContext, argument, qualifier); } @@ -1698,7 +1813,7 @@ protected final K getBeanForField(BeanResolutionContext resolutionContext, B @Internal @UsedByGeneratedCode protected final K getBeanForAnnotation(BeanResolutionContext resolutionContext, BeanContext context, int annotationBeanIndex, Qualifier qualifier) { - final Argument argument = resolveEnvironmentArgument(context, annotationInjection[annotationBeanIndex].argument); + final Argument argument = resolveArgument(context, annotationInjection[annotationBeanIndex].argument); try (BeanResolutionContext.Path ignored = resolutionContext.getPath() .pushAnnotationResolve(this, argument)) { return resolveBean(resolutionContext, argument, qualifier); @@ -1856,9 +1971,9 @@ protected final boolean containsProperties(@SuppressWarnings("unused") BeanResol protected final > Object getBeansOfTypeForField(BeanResolutionContext resolutionContext, BeanContext context, int fieldIndex, Argument genericType, Qualifier qualifier) { // Keep Object type for backwards compatibility final FieldReference fieldRef = fieldInjection[fieldIndex]; - final Argument argument = resolveEnvironmentArgument(context, fieldRef.argument); + final Argument argument = resolveArgument(context, fieldRef.argument); try (BeanResolutionContext.Path ignored = resolutionContext.getPath().pushFieldResolve(this, argument)) { - return resolveBeansOfType(resolutionContext, context, argument, resolveEnvironmentArgument(context, genericType), qualifier); + return resolveBeansOfType(resolutionContext, context, argument, resolveArgument(context, genericType), qualifier); } } @@ -1880,9 +1995,9 @@ protected final > Object getBeansOfTypeForField(BeanR @UsedByGeneratedCode protected final >> R getBeanRegistrationsForField(BeanResolutionContext resolutionContext, BeanContext context, int fieldIndex, Argument genericType, Qualifier qualifier) { FieldReference fieldRef = fieldInjection[fieldIndex]; - Argument argument = resolveEnvironmentArgument(context, fieldRef.argument); + Argument argument = resolveArgument(context, fieldRef.argument); try (BeanResolutionContext.Path ignored = resolutionContext.getPath().pushFieldResolve(this, argument)) { - return resolveBeanRegistrations(resolutionContext, argument, resolveEnvironmentArgument(context, genericType), qualifier); + return resolveBeanRegistrations(resolutionContext, argument, resolveArgument(context, genericType), qualifier); } } @@ -1903,9 +2018,9 @@ protected final >> R getBeanRegistra @UsedByGeneratedCode protected final BeanRegistration getBeanRegistrationForField(BeanResolutionContext resolutionContext, BeanContext context, int fieldIndex, Argument genericType, Qualifier qualifier) { FieldReference fieldRef = fieldInjection[fieldIndex]; - Argument argument = resolveEnvironmentArgument(context, fieldRef.argument); + Argument argument = resolveArgument(context, fieldRef.argument); try (BeanResolutionContext.Path ignored = resolutionContext.getPath().pushFieldResolve(this, argument)) { - return resolveBeanRegistration(resolutionContext, context, argument, resolveEnvironmentArgument(context, genericType), qualifier); + return resolveBeanRegistration(resolutionContext, context, argument, resolveArgument(context, genericType), qualifier); } } @@ -1926,9 +2041,9 @@ protected final BeanRegistration getBeanRegistrationForField(BeanResoluti @UsedByGeneratedCode protected final Optional findBeanForField(BeanResolutionContext resolutionContext, BeanContext context, int fieldIndex, Argument genericType, Qualifier qualifier) { FieldReference fieldRef = fieldInjection[fieldIndex]; - Argument argument = resolveEnvironmentArgument(context, fieldRef.argument); + Argument argument = resolveArgument(context, fieldRef.argument); try (BeanResolutionContext.Path ignored = resolutionContext.getPath().pushFieldResolve(this, argument)) { - return resolveOptionalBean(resolutionContext, argument, resolveEnvironmentArgument(context, genericType), qualifier); + return resolveOptionalBean(resolutionContext, argument, resolveArgument(context, genericType), qualifier); } } @@ -1949,9 +2064,9 @@ protected final Optional findBeanForField(BeanResolutionContext resolutio @UsedByGeneratedCode protected final Stream getStreamOfTypeForField(BeanResolutionContext resolutionContext, BeanContext context, int fieldIndex, Argument genericType, Qualifier qualifier) { FieldReference fieldRef = fieldInjection[fieldIndex]; - Argument argument = resolveEnvironmentArgument(context, fieldRef.argument); + Argument argument = resolveArgument(context, fieldRef.argument); try (BeanResolutionContext.Path ignored = resolutionContext.getPath().pushFieldResolve(this, argument)) { - return resolveStreamOfType(resolutionContext, argument, resolveEnvironmentArgument(context, genericType), qualifier); + return resolveStreamOfType(resolutionContext, argument, resolveArgument(context, genericType), qualifier); } } @@ -1977,9 +2092,9 @@ protected final Map getMapOfTypeForField( Qualifier qualifier) { FieldReference fieldRef = fieldInjection[fieldIndex]; @SuppressWarnings("unchecked") - Argument> argument = resolveEnvironmentArgument(context, fieldRef.argument); + Argument> argument = resolveArgument(context, fieldRef.argument); try (BeanResolutionContext.Path ignored = resolutionContext.getPath().pushFieldResolve(this, argument)) { - return resolveMapOfType(resolutionContext, argument, resolveEnvironmentArgument(context, genericType), qualifier); + return resolveMapOfType(resolutionContext, argument, resolveArgument(context, genericType), qualifier); } } @@ -2022,6 +2137,17 @@ private boolean resolveContainsValue(BeanResolutionContext resolutionContext, Be } private Object resolveValue(BeanResolutionContext resolutionContext, BeanContext context, AnnotationMetadata parentAnnotationMetadata, Argument argument, Qualifier qualifier) { + AnnotationMetadata argumentAnnotationMetadata = argument.getAnnotationMetadata(); + if (argumentAnnotationMetadata instanceof EvaluatedAnnotationMetadata eam) { + eam.configure(context); + eam.setBeanDefinition(this); + AnnotationValue annotation = eam.getAnnotation(Value.class); + if (annotation.hasEvaluatedExpressions()) { + Optional value = annotation.getValue(argument); + return value.orElse(null); + } + } + if (!(context instanceof PropertyResolver)) { throw new DependencyInjectionException(resolutionContext, "@Value requires a BeanContext that implements PropertyResolver"); } @@ -2261,7 +2387,7 @@ private > R resolveBeansOfType(BeanResolutionContext throw noGenericsError(resolutionContext, returnType); } qualifier = qualifier == null ? resolveQualifier(resolutionContext, beanType, returnType) : qualifier; - Collection beansOfType = resolutionContext.getBeansOfType(resolveEnvironmentArgument(context, beanType), qualifier); + Collection beansOfType = resolutionContext.getBeansOfType(resolveArgument(context, beanType), qualifier); return coerceCollectionToCorrectType(returnType.getType(), beansOfType, resolutionContext, returnType); } @@ -2343,17 +2469,17 @@ private Argument resolveArgument(BeanContext context, int argIndex, Argum if (arguments == null) { return null; } - return resolveEnvironmentArgument(context, (Argument) arguments[argIndex]); + return resolveArgument(context, (Argument) arguments[argIndex]); } - private Argument resolveEnvironmentArgument(BeanContext context, Argument argument) { + private Argument resolveArgument(BeanContext context, Argument argument) { if (argument instanceof DefaultArgument) { if (argument.getAnnotationMetadata().hasPropertyExpressions()) { argument = new EnvironmentAwareArgument<>((DefaultArgument) argument); instrumentAnnotationMetadata(context, argument); } } - return argument; + return ExpressionsAwareArgument.wrapIfNecessary(argument, context, this); } private BeanRegistration resolveBeanRegistration(BeanResolutionContext resolutionContext, BeanContext context, @@ -2412,13 +2538,34 @@ private > K coerceCollectionToCorrectType(Class co } private void instrumentAnnotationMetadata(BeanContext context, Object object) { - if (object instanceof final EnvironmentConfigurable ec && context instanceof ApplicationContext) { + if (object instanceof final EnvironmentConfigurable ec && context instanceof ApplicationContext ac) { if (ec.hasPropertyExpressions()) { - ec.configure(((ApplicationContext) context).getEnvironment()); + ec.configure(ac.getEnvironment()); } } } + private Object getExpressionValueForArgument(Argument argument) { + Optional expressionValue = + argument.getAnnotationMetadata() + .getValue(Value.class, argument.getType()); + + if (argument.isOptional()) { + if (expressionValue.isEmpty()) { + return expressionValue; + } else { + Object convertedOptional = expressionValue.get(); + if (convertedOptional instanceof Optional) { + return convertedOptional; + } else { + return expressionValue; + } + } + } else { + return expressionValue.orElse(null); + } + } + @Internal @UsedByGeneratedCode public record PrecalculatedInfo( @@ -2498,10 +2645,22 @@ public MethodReference(Class declaringType, boolean isPreDestroyMethod) { super(declaringType); this.methodName = methodName; - this.arguments = arguments; - this.annotationMetadata = annotationMetadata == null ? AnnotationMetadata.EMPTY_METADATA : annotationMetadata; this.isPostConstructMethod = isPostConstructMethod; this.isPreDestroyMethod = isPreDestroyMethod; + if (arguments != null) { + for (int i = 0; i < arguments.length; i++) { + Argument argument = arguments[i]; + if (argument.getAnnotationMetadata().hasEvaluatedExpressions()) { + arguments[i] = ExpressionsAwareArgument.wrapIfNecessary(argument); + } + } + } + this.arguments = arguments; + + this.annotationMetadata = + annotationMetadata == null + ? AnnotationMetadata.EMPTY_METADATA + : EvaluatedAnnotationMetadata.wrapIfNecessary(annotationMetadata); } } @@ -2521,7 +2680,7 @@ public FieldReference(Class declaringType, Argument argument, boolean requiresRe public FieldReference(Class declaringType, Argument argument) { super(declaringType); - this.argument = argument; + this.argument = ExpressionsAwareArgument.wrapIfNecessary(argument); } } @@ -2554,8 +2713,7 @@ public static final class AnnotationReference { public final Argument argument; public AnnotationReference(Argument argument) { - this.argument = argument; + this.argument = ExpressionsAwareArgument.wrapIfNecessary(argument); } - } } diff --git a/inject/src/main/java/io/micronaut/context/AbstractInitializableBeanDefinitionReference.java b/inject/src/main/java/io/micronaut/context/AbstractInitializableBeanDefinitionReference.java index 48afebbb258..f4ec216943e 100644 --- a/inject/src/main/java/io/micronaut/context/AbstractInitializableBeanDefinitionReference.java +++ b/inject/src/main/java/io/micronaut/context/AbstractInitializableBeanDefinitionReference.java @@ -183,6 +183,9 @@ public BeanDefinition load(BeanContext context) { } else if (context instanceof ApplicationContext applicationContext && definition instanceof EnvironmentConfigurable environmentConfigurable) { environmentConfigurable.configure(applicationContext.getEnvironment()); } + if (definition instanceof ContextConfigurable ctxConfigurable) { + ctxConfigurable.configure(context); + } return definition; } diff --git a/inject/src/main/java/io/micronaut/context/BeanDefinitionAware.java b/inject/src/main/java/io/micronaut/context/BeanDefinitionAware.java new file mode 100644 index 00000000000..e9f23db8c82 --- /dev/null +++ b/inject/src/main/java/io/micronaut/context/BeanDefinitionAware.java @@ -0,0 +1,33 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.context; + +import io.micronaut.inject.BeanDefinition; + +/** + * Interface for components aware of bean definition associated with them. + * + * @author Sergey Gavrilov + * @since 4.0 + */ +public interface BeanDefinitionAware { + /** + * Configure the component for the given bean definition. + * + * @param beanDefinition The bean context + */ + void setBeanDefinition(BeanDefinition beanDefinition); +} diff --git a/inject/src/main/java/io/micronaut/context/ContextConfigurable.java b/inject/src/main/java/io/micronaut/context/ContextConfigurable.java new file mode 100644 index 00000000000..dcc23f5dd5e --- /dev/null +++ b/inject/src/main/java/io/micronaut/context/ContextConfigurable.java @@ -0,0 +1,31 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.context; + +/** + * Interface for components configurable by the bean context. + * + * @author Sergey Gavirlov + * @since 4.0 + */ +public interface ContextConfigurable { + /** + * Configure the component for the given bean context. + * + * @param context The bean context + */ + void configure(BeanContext context); +} diff --git a/inject/src/main/java/io/micronaut/context/ExpressionsAwareArgument.java b/inject/src/main/java/io/micronaut/context/ExpressionsAwareArgument.java new file mode 100644 index 00000000000..5c31639ab22 --- /dev/null +++ b/inject/src/main/java/io/micronaut/context/ExpressionsAwareArgument.java @@ -0,0 +1,89 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.context; + +import io.micronaut.core.annotation.AnnotationMetadata; +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.Nullable; +import io.micronaut.core.type.Argument; +import io.micronaut.core.type.DefaultArgument; +import io.micronaut.inject.BeanDefinition; +import io.micronaut.inject.annotation.EvaluatedAnnotationMetadata; + +/** + * An argument that is aware of evaluated expressions which can be used to resolve + * expression placeholders in the annotation metadata. + * + * @param The argument type + * + * @author Sergey Gavrilov + * @since 4.0 + */ +@Internal +final class ExpressionsAwareArgument extends DefaultArgument implements ContextConfigurable, + BeanDefinitionAware { + + private final EvaluatedAnnotationMetadata annotationMetadata; + + private ExpressionsAwareArgument(Argument argument, + EvaluatedAnnotationMetadata annotationMetadata) { + super(argument.getType(), argument.getName(), argument.getAnnotationMetadata(), + argument.getTypeVariables(), argument.getTypeParameters(), argument.isTypeVariable()); + this.annotationMetadata = annotationMetadata; + } + + public static Argument wrapIfNecessary(Argument argument) { + return wrapIfNecessary(argument, null, null); + } + + public static Argument wrapIfNecessary(Argument argument, + @Nullable BeanContext beanContext, + @Nullable BeanDefinition owningBean) { + if (argument == null) { + return null; + } + + AnnotationMetadata annotationMetadata = + EvaluatedAnnotationMetadata.wrapIfNecessary(argument.getAnnotationMetadata()); + if (annotationMetadata instanceof EvaluatedAnnotationMetadata evaluatedAnnotationMetadata) { + if (beanContext != null) { + evaluatedAnnotationMetadata.configure(beanContext); + } + + if (owningBean != null) { + evaluatedAnnotationMetadata.setBeanDefinition(owningBean); + } + + return new ExpressionsAwareArgument<>(argument, evaluatedAnnotationMetadata); + } + return argument; + } + + @Override + public void setBeanDefinition(BeanDefinition beanDefinition) { + annotationMetadata.setBeanDefinition(beanDefinition); + } + + @Override + public void configure(BeanContext context) { + annotationMetadata.configure(context); + } + + @Override + public AnnotationMetadata getAnnotationMetadata() { + return annotationMetadata; + } +} diff --git a/inject/src/main/java/io/micronaut/context/RequiresCondition.java b/inject/src/main/java/io/micronaut/context/RequiresCondition.java index e8b62dbc8b1..fdfa4520ef1 100644 --- a/inject/src/main/java/io/micronaut/context/RequiresCondition.java +++ b/inject/src/main/java/io/micronaut/context/RequiresCondition.java @@ -98,9 +98,16 @@ public boolean matches(ConditionContext context) { } AnnotationMetadataProvider component = context.getComponent(); boolean isBeanReference = component instanceof BeanDefinitionReference; + // here we use AnnotationMetadata to avoid loading the classes referenced in the annotations directly if (isBeanReference) { for (AnnotationValue requirement : requirements) { + // if annotation value has evaluated expressions, postpone + // decision until the bean is loaded + if (requirement.hasEvaluatedExpressions()) { + continue; + } + processPreStartRequirements(context, requirement); if (context.isFailing()) { return false; diff --git a/inject/src/main/java/io/micronaut/context/annotation/AnnotationExpressionContext.java b/inject/src/main/java/io/micronaut/context/annotation/AnnotationExpressionContext.java new file mode 100644 index 00000000000..4c6c866273d --- /dev/null +++ b/inject/src/main/java/io/micronaut/context/annotation/AnnotationExpressionContext.java @@ -0,0 +1,49 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.context.annotation; + +import io.micronaut.core.annotation.AnnotationMetadata; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * A meta annotation used to extend {@link io.micronaut.core.expressions.EvaluatedExpression} + * context with specified type. Being an expression context means that expressions can reference + * methods and properties of this object directly with # prefix. This + * annotation allows to specify context + * that will only be scoped to this concrete annotation or annotation member. + * + * @author Sergey Gavrilov + * @author gkrocher + * @since 4.0.0 + */ +@Target({ElementType.ANNOTATION_TYPE, ElementType.METHOD}) +@Retention(RetentionPolicy.CLASS) +public @interface AnnotationExpressionContext { + /** + * @return The class that should be included in the context where expressions are evaluated. + */ + Class value(); + + /** + * @return The name of the class that should be included in the context where expressions are evaluated. + */ + @AliasFor(member = AnnotationMetadata.VALUE_MEMBER) + String className() default ""; +} diff --git a/inject/src/main/java/io/micronaut/context/annotation/Value.java b/inject/src/main/java/io/micronaut/context/annotation/Value.java index 6a168b2aaca..acc4765a7c5 100644 --- a/inject/src/main/java/io/micronaut/context/annotation/Value.java +++ b/inject/src/main/java/io/micronaut/context/annotation/Value.java @@ -24,8 +24,8 @@ import java.lang.annotation.Target; /** - *

Allows configuration injection from the environment on a per property, field, method/constructor parameter - * basis.

+ *

Allows configuration injection from the environment or bean context on a per property, field, + * method/constructor parameter basis.

* * @author Graeme Rocher * @see ConfigurationProperties @@ -38,7 +38,8 @@ public @interface Value { /** - * A string containing a value, which my optionally contain property placeholder expressions. + * A string containing a value, which my optionally contain property placeholder expressions or + * evaluated expressions. * * @return The value to inject. */ diff --git a/inject/src/main/java/io/micronaut/context/bind/DefaultExecutableBeanContextBinder.java b/inject/src/main/java/io/micronaut/context/bind/DefaultExecutableBeanContextBinder.java new file mode 100644 index 00000000000..13b19aac081 --- /dev/null +++ b/inject/src/main/java/io/micronaut/context/bind/DefaultExecutableBeanContextBinder.java @@ -0,0 +1,187 @@ +/* + * Copyright 2017-2023 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.context.bind; + +import io.micronaut.context.ApplicationContext; +import io.micronaut.context.BeanContext; +import io.micronaut.context.Qualifier; +import io.micronaut.context.annotation.Property; +import io.micronaut.context.annotation.Value; +import io.micronaut.core.annotation.AnnotationMetadata; +import io.micronaut.core.annotation.AnnotationUtil; +import io.micronaut.core.bind.ArgumentBinderRegistry; +import io.micronaut.core.bind.BoundExecutable; +import io.micronaut.core.bind.exceptions.UnsatisfiedArgumentException; +import io.micronaut.core.type.Argument; +import io.micronaut.core.type.Executable; +import io.micronaut.core.util.ArrayUtils; +import io.micronaut.core.util.CollectionUtils; +import io.micronaut.inject.qualifiers.Qualifiers; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.Optional; + +/** + * Implementation of {@link ExecutableBeanContextBinder}. + * + * @since 4.0.0 + */ +public final class DefaultExecutableBeanContextBinder implements ExecutableBeanContextBinder { + + @Override + public BoundExecutable bind(Executable target, ArgumentBinderRegistry registry, BeanContext source) throws UnsatisfiedArgumentException { + return bind(target, source); + } + + @Override + public BoundExecutable tryBind(Executable target, ArgumentBinderRegistry registry, BeanContext source) { + Argument[] arguments = target.getArguments(); + if (arguments.length == 0) { + return new ContextBoundExecutable<>(target, ArrayUtils.EMPTY_OBJECT_ARRAY); + } + Object[] bound = new Object[arguments.length]; + for (int i = 0; i < arguments.length; i++) { + Argument argument = arguments[i]; + Optional v = argument.getAnnotationMetadata().stringValue(Value.class); + if (v.isPresent() && source instanceof ApplicationContext applicationContext) { + bound[i] = applicationContext.getEnvironment().getProperty(v.get(), argument) + .orElse(null); + } else { + v = argument.getAnnotationMetadata().stringValue(Property.class, "name"); + if (v.isPresent() && source instanceof ApplicationContext applicationContext) { + bound[i] = applicationContext.getEnvironment() + .getProperty(v.get(), argument).orElse(null); + } else { + bound[i] = source.findBean(argument, resolveQualifier(argument)) + .orElse(null); + } + } + } + return new ContextBoundExecutable<>(target, bound); + } + + @Override + public BoundExecutable bind(Executable target, BeanContext source) throws UnsatisfiedArgumentException { + Argument[] arguments = target.getArguments(); + if (arguments.length == 0) { + return new ContextBoundExecutable<>(target, ArrayUtils.EMPTY_OBJECT_ARRAY); + } + Object[] bound = new Object[arguments.length]; + for (int i = 0; i < arguments.length; i++) { + Argument argument = arguments[i]; + Optional v = argument.getAnnotationMetadata().stringValue(Value.class); + if (v.isPresent() && source instanceof ApplicationContext applicationContext) { + Optional finalV = v; + bound[i] = applicationContext.getEnvironment().getProperty(v.get(), argument) + .orElseThrow(() -> + new UnsatisfiedArgumentException(argument, "Unresolvable property specified to @Value: " + finalV.get()) + ); + } else { + v = argument.getAnnotationMetadata().stringValue(Property.class, "name"); + if (v.isPresent() && source instanceof ApplicationContext applicationContext) { + Optional finalV1 = v; + bound[i] = applicationContext.getEnvironment() + .getProperty(v.get(), argument).orElseThrow(() -> + new UnsatisfiedArgumentException(argument, "Unresolvable property specified to @Value: " + finalV1.get()) + ); + } else { + bound[i] = source.findBean(argument, resolveQualifier(argument)) + .orElseThrow(() -> + new UnsatisfiedArgumentException(argument, "Unresolvable bean argument: " + argument) + ); + } + } + } + return new ContextBoundExecutable<>(target, bound); + } + + /** + * Build a qualifier for the given argument. + * @param argument The argument + * @param The type + * @return The resolved qualifier + */ + @SuppressWarnings("unchecked") + private static Qualifier resolveQualifier(Argument argument) { + AnnotationMetadata annotationMetadata = Objects.requireNonNull(argument, "Argument cannot be null").getAnnotationMetadata(); + boolean hasMetadata = annotationMetadata != AnnotationMetadata.EMPTY_METADATA; + + List qualifierTypes = hasMetadata ? annotationMetadata.getAnnotationNamesByStereotype(AnnotationUtil.QUALIFIER) : Collections.emptyList(); + if (CollectionUtils.isNotEmpty(qualifierTypes)) { + if (qualifierTypes.size() == 1) { + return Qualifiers.byAnnotation( + annotationMetadata, + qualifierTypes.iterator().next() + ); + } else { + final Qualifier[] qualifiers = qualifierTypes + .stream().map((type) -> Qualifiers.byAnnotation(annotationMetadata, type)) + .toArray(Qualifier[]::new); + return Qualifiers.byQualifiers( + qualifiers + ); + } + } + return null; + } + + private record ContextBoundExecutable (Executable target, Object[] bound) implements BoundExecutable { + @Override + public Executable getTarget() { + return target; + } + + @Override + public R invoke(T instance) { + return target.invoke(instance, bound); + } + + @Override + public Object[] getBoundArguments() { + return bound; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + ContextBoundExecutable that = (ContextBoundExecutable) o; + return target.equals(that.target) && Arrays.equals(bound, that.bound); + } + + @Override + public int hashCode() { + int result = Objects.hash(target); + result = 31 * result + Arrays.hashCode(bound); + return result; + } + + @Override + public String toString() { + return "ContextBoundExecutable{" + + "target=" + target + + ", bound=" + Arrays.toString(bound) + + '}'; + } + } +} diff --git a/inject/src/main/java/io/micronaut/context/bind/ExecutableBeanContextBinder.java b/inject/src/main/java/io/micronaut/context/bind/ExecutableBeanContextBinder.java new file mode 100644 index 00000000000..de946970e15 --- /dev/null +++ b/inject/src/main/java/io/micronaut/context/bind/ExecutableBeanContextBinder.java @@ -0,0 +1,45 @@ +/* + * Copyright 2017-2023 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.context.bind; + +import io.micronaut.context.BeanContext; +import io.micronaut.core.bind.BoundExecutable; +import io.micronaut.core.bind.ExecutableBinder; +import io.micronaut.core.bind.exceptions.UnsatisfiedArgumentException; +import io.micronaut.core.type.Executable; + +/** + * Sub-interface of {@link ExecutableBinder} that binds arguments from a {@link BeanContext}. + * + * @since 4.0.0 + */ +public interface ExecutableBeanContextBinder extends ExecutableBinder { + + /** + * Binds a given {@link Executable} using the given registry and source object. + * + * @param target The target executable + * @param source The bean context + * @param The executable target type + * @param The executable return type + * @return The bound executable + * @throws UnsatisfiedArgumentException When the executable could not be satisfied + */ + BoundExecutable bind( + Executable target, + BeanContext source + ) throws UnsatisfiedArgumentException; +} diff --git a/inject/src/main/java/io/micronaut/context/exceptions/ExpressionEvaluationException.java b/inject/src/main/java/io/micronaut/context/exceptions/ExpressionEvaluationException.java new file mode 100644 index 00000000000..3673e7d4b06 --- /dev/null +++ b/inject/src/main/java/io/micronaut/context/exceptions/ExpressionEvaluationException.java @@ -0,0 +1,33 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.context.exceptions; + +/** + * Exception thrown on expression evaluation failure. + * + * @since 4.0 + * @author Sergey Gavrilov + */ +public class ExpressionEvaluationException extends RuntimeException { + + public ExpressionEvaluationException(String message, Throwable ex) { + super(message, ex); + } + + public ExpressionEvaluationException(String message) { + super(message); + } +} diff --git a/inject/src/main/java/io/micronaut/context/expressions/AbstractEvaluatedExpression.java b/inject/src/main/java/io/micronaut/context/expressions/AbstractEvaluatedExpression.java new file mode 100644 index 00000000000..5b381dca21c --- /dev/null +++ b/inject/src/main/java/io/micronaut/context/expressions/AbstractEvaluatedExpression.java @@ -0,0 +1,66 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.context.expressions; + +import io.micronaut.context.exceptions.ExpressionEvaluationException; +import io.micronaut.core.expressions.EvaluatedExpression; +import io.micronaut.core.expressions.ExpressionEvaluationContext; +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.UsedByGeneratedCode; + +/** + * Default implementation for evaluated expressions. This class is subclassed + * by evaluated expressions classes at compilation time. + * + * @author Sergey Gavrilov + * @since 4.0.0 + */ +@Internal +@UsedByGeneratedCode +public abstract class AbstractEvaluatedExpression implements EvaluatedExpression { + + private final Object initialAnnotationValue; + + public AbstractEvaluatedExpression(Object initialAnnotationValue) { + this.initialAnnotationValue = initialAnnotationValue; + } + + @Override + public final Object evaluate(ExpressionEvaluationContext evaluationContext) { + try (evaluationContext) { + return doEvaluate(evaluationContext); + } catch (Throwable ex) { + throw new ExpressionEvaluationException( + "Can not evaluate expression [" + initialAnnotationValue + "]. " + ex.getMessage(), ex); + } + } + + /** + * This method is overridden by expression classes generated at compilation time and + * contains concrete expression evaluation logic. + * + * @param evaluationContext context used for expression evaluation + * @return evaluation result + */ + protected Object doEvaluate(ExpressionEvaluationContext evaluationContext) { + return initialAnnotationValue; + } + + @Override + public String toString() { + return initialAnnotationValue.toString(); + } +} diff --git a/inject/src/main/java/io/micronaut/context/expressions/ConfigurableExpressionEvaluationContext.java b/inject/src/main/java/io/micronaut/context/expressions/ConfigurableExpressionEvaluationContext.java new file mode 100644 index 00000000000..8c3823f338f --- /dev/null +++ b/inject/src/main/java/io/micronaut/context/expressions/ConfigurableExpressionEvaluationContext.java @@ -0,0 +1,60 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.context.expressions; + +import io.micronaut.context.BeanContext; +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.NonNull; +import io.micronaut.core.annotation.Nullable; +import io.micronaut.core.expressions.ExpressionEvaluationContext; +import io.micronaut.inject.BeanDefinition; + +/** + * Expression evaluation context that can be configured before evaluation. + * + * @since 4.0.0 + * @author Sergey Gavrilov + */ +@Internal +public interface ConfigurableExpressionEvaluationContext extends ExpressionEvaluationContext { + + /** + * Set arguments passed to invoked method. + * + * @param args method arguments + * @return evaluation context which arguments can be used in evaluation. + */ + @NonNull + ConfigurableExpressionEvaluationContext setArguments(@Nullable Object[] args); + + /** + * Set bean owning evaluated expression. + * + * @param beanDefinition owning bean definition + * @return evaluation context aware of owning bean. + */ + @NonNull + ConfigurableExpressionEvaluationContext setOwningBean(@Nullable BeanDefinition beanDefinition); + + /** + * Set context in which expression is evaluated. + * + * @param beanContext bean context + * @return evaluation context aware of bean context. + */ + @NonNull + ConfigurableExpressionEvaluationContext setBeanContext(@Nullable BeanContext beanContext); +} diff --git a/inject/src/main/java/io/micronaut/context/expressions/DefaultExpressionEvaluationContext.java b/inject/src/main/java/io/micronaut/context/expressions/DefaultExpressionEvaluationContext.java new file mode 100644 index 00000000000..e480e84eb24 --- /dev/null +++ b/inject/src/main/java/io/micronaut/context/expressions/DefaultExpressionEvaluationContext.java @@ -0,0 +1,108 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.context.expressions; + +import io.micronaut.context.BeanContext; +import io.micronaut.context.BeanResolutionContext; +import io.micronaut.context.DefaultBeanContext; +import io.micronaut.context.DefaultBeanResolutionContext; +import io.micronaut.context.exceptions.ExpressionEvaluationException; +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.Nullable; +import io.micronaut.core.type.Argument; +import io.micronaut.inject.BeanDefinition; + +/** + * Default implementation of {@link ConfigurableExpressionEvaluationContext}. + * For this implementation, the methods mutating evaluation context return new instance of + * expression evaluation context. + * + * @since 4.0.0 + * @author Sergey Gavrilov + */ +@Internal +public class DefaultExpressionEvaluationContext implements ConfigurableExpressionEvaluationContext { + + private final Object[] args; + private final BeanContext beanContext; + private final BeanDefinition owningBean; + + private BeanResolutionContext resolutionContext; + + public DefaultExpressionEvaluationContext() { + this(null, null, null); + } + + public DefaultExpressionEvaluationContext(@Nullable Object[] args, + @Nullable BeanContext beanContext, + @Nullable BeanDefinition owningBean) { + this.args = args; + this.beanContext = beanContext; + this.owningBean = owningBean; + } + + @Override + public ConfigurableExpressionEvaluationContext setArguments(Object[] args) { + return new DefaultExpressionEvaluationContext(args, this.beanContext, this.owningBean); + } + + @Override + public ConfigurableExpressionEvaluationContext setOwningBean(BeanDefinition beanDefinition) { + return new DefaultExpressionEvaluationContext(this.args, this.beanContext, beanDefinition); + } + + @Override + public ConfigurableExpressionEvaluationContext setBeanContext(BeanContext beanContext) { + return new DefaultExpressionEvaluationContext(this.args, beanContext, this.owningBean); + } + + @Override + public Object getArgument(int index) { + if (args == null || args.length == 0) { + throw new ExpressionEvaluationException( + "Can not obtain argument at index [" + index + "] since arguments are not provided"); + } + + return args[index]; + } + + @Override + public T getBean(Class type) { + if (beanContext == null) { + throw new ExpressionEvaluationException("Can not obtain bean of type [" + type + "] since bean context is not set"); + } + + if (beanContext instanceof DefaultBeanContext defaultBeanContext) { + if (resolutionContext == null && owningBean != null) { + resolutionContext = new DefaultBeanResolutionContext(defaultBeanContext, owningBean); + } + + if (resolutionContext != null) { + try (BeanResolutionContext.Path ignored = + resolutionContext.getPath().pushAnnotationResolve(owningBean, Argument.of(type))) { + return defaultBeanContext.getBean(resolutionContext, type); + } + } + } + + return beanContext.getBean(type); + } + + @Override + public void close() throws Exception { + resolutionContext = null; + } +} diff --git a/inject/src/main/java/io/micronaut/inject/annotation/AbstractEnvironmentAnnotationMetadata.java b/inject/src/main/java/io/micronaut/inject/annotation/AbstractEnvironmentAnnotationMetadata.java index 112fee14961..f9cc725319f 100644 --- a/inject/src/main/java/io/micronaut/inject/annotation/AbstractEnvironmentAnnotationMetadata.java +++ b/inject/src/main/java/io/micronaut/inject/annotation/AbstractEnvironmentAnnotationMetadata.java @@ -77,6 +77,11 @@ public T synthesizeDeclared(@NonNull Class annotationC return environmentAnnotationMetadata.synthesizeDeclared(annotationClass); } + @Override + public boolean hasEvaluatedExpressions() { + return environmentAnnotationMetadata.hasEvaluatedExpressions(); + } + @Override public Optional getValue(@NonNull String annotation, @NonNull String member, @NonNull Argument requiredType) { Environment environment = getEnvironment(); diff --git a/inject/src/main/java/io/micronaut/inject/annotation/AnnotationMetadataHierarchy.java b/inject/src/main/java/io/micronaut/inject/annotation/AnnotationMetadataHierarchy.java index c6298f573fa..e9b46f6a28c 100644 --- a/inject/src/main/java/io/micronaut/inject/annotation/AnnotationMetadataHierarchy.java +++ b/inject/src/main/java/io/micronaut/inject/annotation/AnnotationMetadataHierarchy.java @@ -114,6 +114,16 @@ public boolean hasPropertyExpressions() { return false; } + @Override + public boolean hasEvaluatedExpressions() { + for (AnnotationMetadata annotationMetadata: hierarchy) { + if (annotationMetadata.hasEvaluatedExpressions()) { + return true; + } + } + return false; + } + @Override public Optional> getAnnotationType(@NonNull String name) { for (AnnotationMetadata metadata : hierarchy) { diff --git a/inject/src/main/java/io/micronaut/inject/annotation/DefaultAnnotationMetadata.java b/inject/src/main/java/io/micronaut/inject/annotation/DefaultAnnotationMetadata.java index 31feb48c1c3..160086db905 100644 --- a/inject/src/main/java/io/micronaut/inject/annotation/DefaultAnnotationMetadata.java +++ b/inject/src/main/java/io/micronaut/inject/annotation/DefaultAnnotationMetadata.java @@ -82,6 +82,7 @@ public class DefaultAnnotationMetadata extends AbstractAnnotationMetadata implem private final Map annotationValuesByType = new ConcurrentHashMap<>(2); private final boolean hasPropertyExpressions; + private final boolean hasEvaluatedExpressions; /** * Constructs empty annotation metadata. @@ -89,6 +90,7 @@ public class DefaultAnnotationMetadata extends AbstractAnnotationMetadata implem @Internal protected DefaultAnnotationMetadata() { hasPropertyExpressions = false; + hasEvaluatedExpressions = false; } /** @@ -133,6 +135,30 @@ public DefaultAnnotationMetadata( this(declaredAnnotations, declaredStereotypes, allStereotypes, allAnnotations, annotationsByStereotype, hasPropertyExpressions, false); } + /** + * This constructor is designed to be used by compile time produced subclasses. + * + * @param declaredAnnotations The directly declared annotations + * @param declaredStereotypes The directly declared stereotypes + * @param allStereotypes All of the stereotypes + * @param allAnnotations All of the annotations + * @param annotationsByStereotype The annotations by stereotype + * @param hasPropertyExpressions Whether property expressions exist in the metadata + * @param hasEvaluatedExpressions Whether evaluated expressions exist in the metadata + */ + @Internal + @UsedByGeneratedCode + public DefaultAnnotationMetadata( + @Nullable Map> declaredAnnotations, + @Nullable Map> declaredStereotypes, + @Nullable Map> allStereotypes, + @Nullable Map> allAnnotations, + @Nullable Map> annotationsByStereotype, + boolean hasPropertyExpressions, + boolean hasEvaluatedExpressions) { + this(declaredAnnotations, declaredStereotypes, allStereotypes, allAnnotations, annotationsByStereotype, hasPropertyExpressions, hasEvaluatedExpressions, false); + } + /** * This constructor is designed to be used by compile time produced subclasses. * @@ -156,6 +182,7 @@ public DefaultAnnotationMetadata( @Nullable Map> allAnnotations, @Nullable Map> annotationsByStereotype, boolean hasPropertyExpressions, + boolean hasEvaluatedExpressions, boolean useRepeatableDefaults) { super(declaredAnnotations, allAnnotations); this.declaredAnnotations = declaredAnnotations; @@ -164,6 +191,7 @@ public DefaultAnnotationMetadata( this.allAnnotations = allAnnotations; this.annotationsByStereotype = annotationsByStereotype; this.hasPropertyExpressions = hasPropertyExpressions; + this.hasEvaluatedExpressions = hasEvaluatedExpressions; } @NonNull @@ -184,6 +212,11 @@ public boolean hasPropertyExpressions() { return hasPropertyExpressions; } + @Override + public boolean hasEvaluatedExpressions() { + return hasEvaluatedExpressions; + } + @NonNull @Override public Map getDefaultValues(@NonNull String annotation) { diff --git a/inject/src/main/java/io/micronaut/inject/annotation/EvaluatedAnnotationMetadata.java b/inject/src/main/java/io/micronaut/inject/annotation/EvaluatedAnnotationMetadata.java new file mode 100644 index 00000000000..efecbfec77f --- /dev/null +++ b/inject/src/main/java/io/micronaut/inject/annotation/EvaluatedAnnotationMetadata.java @@ -0,0 +1,98 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.inject.annotation; + +import io.micronaut.context.BeanContext; +import io.micronaut.context.BeanDefinitionAware; +import io.micronaut.context.ContextConfigurable; +import io.micronaut.context.expressions.ConfigurableExpressionEvaluationContext; +import io.micronaut.context.expressions.DefaultExpressionEvaluationContext; +import io.micronaut.core.annotation.AnnotationMetadata; +import io.micronaut.core.annotation.AnnotationValue; +import io.micronaut.core.annotation.Internal; +import io.micronaut.inject.BeanDefinition; + +import java.lang.annotation.Annotation; + +/** + * Variation of {@link AnnotationMetadata} that is used when evaluated expression + * in annotation values need to be resolved at runtime. + * + * @author Sergey Gavrilov + * @since 4.0 + */ +@Internal +public final class EvaluatedAnnotationMetadata extends MappingAnnotationMetadataDelegate implements ContextConfigurable, BeanDefinitionAware { + + private final AnnotationMetadata delegateAnnotationMetadata; + + private ConfigurableExpressionEvaluationContext evaluationContext; + + private EvaluatedAnnotationMetadata(AnnotationMetadata targetMetadata, + ConfigurableExpressionEvaluationContext evaluationContext) { + this.delegateAnnotationMetadata = targetMetadata; + this.evaluationContext = evaluationContext; + } + + /** + * Provide a copy of this annotation metadata with passed method arguments. + * + * @param args arguments passed to method + * @return copy of annotation metadata + */ + public EvaluatedAnnotationMetadata withArguments(Object[] args) { + return new EvaluatedAnnotationMetadata( + delegateAnnotationMetadata, + evaluationContext.setArguments(args)); + } + + @Override + public void configure(BeanContext context) { + evaluationContext = evaluationContext.setBeanContext(context); + } + + @Override + public void setBeanDefinition(BeanDefinition beanDefinition) { + evaluationContext = evaluationContext.setOwningBean(beanDefinition); + } + + public static AnnotationMetadata wrapIfNecessary(AnnotationMetadata targetMetadata) { + if (targetMetadata == null) { + return null; + } else if (targetMetadata instanceof EvaluatedAnnotationMetadata) { + return targetMetadata; + } else if (targetMetadata.hasEvaluatedExpressions()) { + return new EvaluatedAnnotationMetadata(targetMetadata, new DefaultExpressionEvaluationContext()); + } + return targetMetadata; + } + + @Override + public boolean hasEvaluatedExpressions() { + // this type of metadata always has evaluated expressions + return true; + } + + @Override + public AnnotationMetadata getAnnotationMetadata() { + return delegateAnnotationMetadata; + } + + @Override + public AnnotationValue mapAnnotationValue(AnnotationValue av) { + return new EvaluatedAnnotationValue<>(av, evaluationContext); + } +} diff --git a/inject/src/main/java/io/micronaut/inject/annotation/EvaluatedAnnotationValue.java b/inject/src/main/java/io/micronaut/inject/annotation/EvaluatedAnnotationValue.java new file mode 100644 index 00000000000..8e44eaf225b --- /dev/null +++ b/inject/src/main/java/io/micronaut/inject/annotation/EvaluatedAnnotationValue.java @@ -0,0 +1,61 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.inject.annotation; + +import io.micronaut.context.expressions.ConfigurableExpressionEvaluationContext; +import io.micronaut.core.annotation.AnnotationValue; +import io.micronaut.core.expressions.EvaluatedExpression; + +import java.lang.annotation.Annotation; + +/** + * An EvaluatedAnnotationValue is a {@link AnnotationValue} that contains one or more expressions. + * + * @param The annotation + * @since 4.0.0 + */ +public class EvaluatedAnnotationValue extends AnnotationValue { + private final ConfigurableExpressionEvaluationContext evaluationContext; + private final AnnotationValue annotationValue; + + public EvaluatedAnnotationValue(AnnotationValue annotationValue, ConfigurableExpressionEvaluationContext evaluationContext) { + super( + annotationValue, + annotationValue.getDefaultValues(), + new EvaluatedConvertibleValuesMap<>(evaluationContext, annotationValue.getConvertibleValues()), + value -> { + if (value instanceof EvaluatedExpression expression) { + return expression.evaluate(evaluationContext); + } + return value; + } + ); + this.evaluationContext = evaluationContext; + this.annotationValue = annotationValue; + } + + /** + * Provide a copy of this annotation metadata with passed method arguments. + * + * @param args arguments passed to method + * @return copy of annotation metadata + */ + public EvaluatedAnnotationValue withArguments(Object[] args) { + return new EvaluatedAnnotationValue<>( + annotationValue, + evaluationContext.setArguments(args)); + } +} diff --git a/inject/src/main/java/io/micronaut/inject/annotation/EvaluatedConvertibleValuesMap.java b/inject/src/main/java/io/micronaut/inject/annotation/EvaluatedConvertibleValuesMap.java new file mode 100644 index 00000000000..7e0ad7f1147 --- /dev/null +++ b/inject/src/main/java/io/micronaut/inject/annotation/EvaluatedConvertibleValuesMap.java @@ -0,0 +1,86 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.inject.annotation; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.convert.ArgumentConversionContext; +import io.micronaut.core.convert.ConversionService; +import io.micronaut.core.convert.value.ConvertibleValues; +import io.micronaut.core.convert.value.ConvertibleValuesMap; +import io.micronaut.core.expressions.EvaluatedExpression; +import io.micronaut.core.expressions.ExpressionEvaluationContext; + +import java.util.Collection; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * Version of {@link ConvertibleValuesMap} that is aware of evaluated expressions. + * + * @param The generic value + * + * @since 4.0.0 + * @author Sergey Gavrilov + */ +@Internal +public class EvaluatedConvertibleValuesMap implements ConvertibleValues { + + private final ExpressionEvaluationContext evaluationContext; + private final ConvertibleValues delegateValues; + + EvaluatedConvertibleValuesMap(ExpressionEvaluationContext evaluationContext, + ConvertibleValues delegateValues) { + this.evaluationContext = evaluationContext; + this.delegateValues = delegateValues; + } + + @Override + public Set names() { + return delegateValues.names(); + } + + @Override + public Optional get(CharSequence name, + ArgumentConversionContext conversionContext) { + V value = delegateValues.getValue(name); + if (value instanceof EvaluatedExpression expression) { + if (EvaluatedExpression.class.isAssignableFrom(conversionContext.getArgument().getClass())) { + return Optional.of((T) value); + } + + Object evaluationResult = expression.evaluate(evaluationContext); + if (evaluationResult == null || conversionContext.getArgument().isAssignableFrom(evaluationResult.getClass())) { + return Optional.ofNullable((T) evaluationResult); + } + return ConversionService.SHARED.convert(evaluationResult, conversionContext); + } else { + return delegateValues.get(name, conversionContext); + } + } + + @SuppressWarnings("unchecked") + @Override + public Collection values() { + return delegateValues.values().stream().map(v -> { + if (v instanceof EvaluatedExpression expression) { + Object evaluationResult = expression.evaluate(evaluationContext); + return (V) evaluationResult; + } + return v; + }).collect(Collectors.toList()); + } +} diff --git a/inject/src/main/java/io/micronaut/inject/annotation/MappingAnnotationMetadataDelegate.java b/inject/src/main/java/io/micronaut/inject/annotation/MappingAnnotationMetadataDelegate.java new file mode 100644 index 00000000000..73839203225 --- /dev/null +++ b/inject/src/main/java/io/micronaut/inject/annotation/MappingAnnotationMetadataDelegate.java @@ -0,0 +1,452 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.inject.annotation; + +import io.micronaut.core.annotation.AnnotationMetadataDelegate; +import io.micronaut.core.annotation.AnnotationValue; +import io.micronaut.core.reflect.ReflectionUtils; +import io.micronaut.core.type.Argument; +import io.micronaut.core.util.StringUtils; +import io.micronaut.core.value.OptionalValues; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Array; +import java.util.Collections; +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.function.Supplier; + +/** + * Abstract annotation metadata delegate for cases when annotation + * values need to be mapped before being returned. + * + * @since 4.0.0 + * @author Sergey Gavrilov + */ +public abstract class MappingAnnotationMetadataDelegate implements AnnotationMetadataDelegate { + public abstract AnnotationValue mapAnnotationValue(AnnotationValue av); + + + @Override + public Optional stringValue(String annotation, String member) { + return findAnnotation(annotation) + .flatMap(av -> av.stringValue(member)); + } + + @Override + public Optional stringValue(Class annotation, String member) { + return stringValue(annotation.getName(), member); + } + + @Override + public Optional stringValue(Class annotation) { + return stringValue(annotation, VALUE_MEMBER); + } + + @Override + public Optional stringValue(String annotation) { + return stringValue(annotation, VALUE_MEMBER); + } + + @Override + public String[] stringValues(String annotation, String member) { + return findAnnotation(annotation) + .map(av -> av.stringValues(member)) + .orElse(StringUtils.EMPTY_STRING_ARRAY); + } + + @Override + public String[] stringValues(Class annotation, String member) { + return stringValues(annotation.getName(), member); + } + + @Override + public String[] stringValues(Class annotation) { + return stringValues(annotation, VALUE_MEMBER); + } + + @Override + public String[] stringValues(String annotation) { + return stringValues(annotation, VALUE_MEMBER); + } + + @Override + public > Optional enumValue(String annotation, Class enumType) { + return enumValue(annotation, VALUE_MEMBER, enumType); + } + + @Override + public > Optional enumValue(String annotation, String member, + Class enumType) { + return findAnnotation(annotation) + .flatMap(av -> av.enumValue(member, enumType)); + } + + @Override + public > Optional enumValue(Class annotation, + Class enumType) { + return enumValue(annotation.getName(), VALUE_MEMBER, enumType); + } + + @Override + public > Optional enumValue(Class annotation, + String member, Class enumType) { + return enumValue(annotation.getName(), member, enumType); + } + + @Override + public > E[] enumValues(String annotation, String member, Class enumType) { + return findAnnotation(annotation) + .map(av -> av.enumValues(member, enumType)) + .orElse((E[]) Array.newInstance(enumType, 0)); + } + + @Override + public > E[] enumValues(String annotation, Class enumType) { + return enumValues(annotation, VALUE_MEMBER, enumType); + } + + @Override + public > E[] enumValues(Class annotation, + Class enumType) { + return enumValues(annotation.getName(), VALUE_MEMBER, enumType); + } + + @Override + public > E[] enumValues(Class annotation, + String member, Class enumType) { + return enumValues(annotation.getName(), member, enumType); + } + + @Override + public Class[] classValues(String annotation, String member) { + return (Class[]) findAnnotation(annotation) + .map(av -> av.classValues(member)) + .orElse(ReflectionUtils.EMPTY_CLASS_ARRAY); + } + + @Override + public Class[] classValues(String annotation) { + return classValues(annotation, VALUE_MEMBER); + } + + @Override + public Class[] classValues(Class annotation) { + return classValues(annotation.getName(), VALUE_MEMBER); + } + + @Override + public Class[] classValues(Class annotation, String member) { + return classValues(annotation.getName(), member); + } + + @Override + public Optional booleanValue(String annotation, String member) { + return findAnnotation(annotation) + .flatMap(av -> av.booleanValue(member)); + } + + @Override + public Optional booleanValue(Class annotation, String member) { + return booleanValue(annotation.getName(), member); + } + + @Override + public Optional booleanValue(Class annotation) { + return booleanValue(annotation.getName(), VALUE_MEMBER); + } + + @Override + public Optional booleanValue(String annotation) { + return booleanValue(annotation, VALUE_MEMBER); + } + + @Override + public boolean isTrue(String annotation, String member) { + return getValue(annotation, member, Boolean.class).orElse(false); + } + + @Override + public boolean isTrue(Class annotation, String member) { + return isTrue(annotation.getName(), member); + } + + @Override + public boolean isFalse(String annotation, String member) { + return !isTrue(annotation, member); + } + + @Override + public boolean isFalse(Class annotation, String member) { + return isFalse(annotation.getName(), member); + } + + @Override + public Optional classValue(String annotation, String member) { + return findAnnotation(annotation) + .flatMap(av -> av.classValue(member)); + } + + @Override + public Optional classValue(String annotation) { + return classValue(annotation, VALUE_MEMBER); + } + + @Override + public Optional classValue(Class annotation) { + return classValue(annotation.getName(), VALUE_MEMBER); + } + + @Override + public Optional classValue(Class annotation, String member) { + return classValue(annotation.getName(), member); + } + + @Override + public OptionalInt intValue(String annotation, String member) { + return findAnnotation(annotation) + .map(AnnotationValue::intValue) + .orElse(OptionalInt.empty()); + } + + @Override + public OptionalInt intValue(Class annotation, String member) { + return intValue(annotation.getName(), member); + } + + @Override + public OptionalInt intValue(Class annotation) { + return intValue(annotation.getName(), VALUE_MEMBER); + } + + @Override + public OptionalLong longValue(String annotation, String member) { + return findAnnotation(annotation) + .map(AnnotationValue::longValue) + .orElse(OptionalLong.empty()); + } + + @Override + public OptionalLong longValue(Class annotation, String member) { + return longValue(annotation.getName(), member); + } + + @Override + public OptionalDouble doubleValue(String annotation, String member) { + return findAnnotation(annotation) + .map(av -> av.doubleValue(member)) + .orElse(OptionalDouble.empty()); + } + + @Override + public OptionalDouble doubleValue(Class annotation, String member) { + return findAnnotation(annotation) + .map(av -> av.doubleValue(member)) + .orElse(OptionalDouble.empty()); + } + + @Override + public OptionalDouble doubleValue(Class annotation) { + return doubleValue(annotation, VALUE_MEMBER); + } + + @Override + public Optional getValue(String annotation, String member, Argument requiredType) { + return findAnnotation(annotation) + .flatMap(av -> av.get(member, requiredType)); + } + + @Override + public Optional getValue(Class annotation, String member, + Argument requiredType) { + return findAnnotation(annotation) + .flatMap(av -> av.get(member, requiredType)); + } + + @Override + public Optional getValue(String annotation, Argument requiredType) { + return getValue(annotation, VALUE_MEMBER, requiredType); + } + + @Override + public Optional getValue(Class annotation, + Argument requiredType) { + return getValue(annotation, VALUE_MEMBER, requiredType); + } + + @Override + public Optional getValue(Class annotation, String member, + Class requiredType) { + return getValue(annotation, member, Argument.of(requiredType)); + } + + @Override + public Optional getValue(Class annotation, Class requiredType) { + return getValue(annotation, VALUE_MEMBER, requiredType); + } + + @Override + public Optional getValue(String annotation, String member, Class requiredType) { + return getValue(annotation, member, Argument.of(requiredType)); + } + + @Override + public Optional getValue(String annotation, Class requiredType) { + return getValue(annotation, VALUE_MEMBER, Argument.of(requiredType)); + } + + @Override + public Optional getValue(String annotation, String member) { + return getValue(annotation, member, Object.class); + } + + @Override + public Optional getValue(Class annotation, String member) { + return getValue(annotation, member, Object.class); + } + + @Override + public Optional getValue(String annotation) { + return getValue(annotation, VALUE_MEMBER, Object.class); + } + + @Override + public Optional getValue(Class annotation) { + return getValue(annotation, VALUE_MEMBER, Object.class); + } + + @Override + public OptionalValues getValues(Class annotation, + Class valueType) { + return getValues(annotation.getName(), valueType); + } + + @Override + public OptionalValues getValues(String annotation, Class valueType) { + return OptionalValues.of(valueType, getValues(annotation)); + } + + @Override + public Map getValues(String annotation) { + return findAnnotation(annotation) + .map(AnnotationValue::getValues) + .orElse(Collections.emptyMap()); + } + + @Override + public AnnotationValue getDeclaredAnnotation(Class annotationClass) { + return AnnotationMetadataDelegate.super.getDeclaredAnnotation(annotationClass); + } + + @Override + public AnnotationValue getAnnotation(Class annotationClass) { + return getAnnotation(annotationClass.getName()); + } + + @Override + public AnnotationValue getAnnotation(String annotation) { + return this.findAnnotation(annotation).orElse(null); + } + + @Override + public Optional> findAnnotation(String annotation) { + Optional> av = getAnnotationMetadata().findAnnotation(annotation); + return av.map(this::mapAnnotationValue); + } + + @Override + public Optional> findAnnotation(Class annotationClass) { + return getAnnotationMetadata().findAnnotation(annotationClass) + .map(this::mapAnnotationValue); + } + + @Override + public Optional> findDeclaredAnnotation(Class annotationClass) { + return findDeclaredAnnotation(annotationClass.getName()); + } + + @Override + public Optional> findDeclaredAnnotation(String annotation) { + Optional> av = + getAnnotationMetadata().findDeclaredAnnotation(annotation); + return av.map(this::mapAnnotationValue); + } + + @Override + public T[] synthesizeDeclaredAnnotationsByType(Class annotationClass) { + return getDeclaredAnnotationValuesByType(annotationClass).stream() + .map(annotation -> AnnotationMetadataSupport.buildAnnotation(annotationClass, + annotation)) + .toArray(value -> (T[]) Array.newInstance(annotationClass, value)); + } + + @Override + public T[] synthesizeAnnotationsByType(Class annotationClass) { + return getAnnotationValuesByType(annotationClass).stream() + .map(annotation -> AnnotationMetadataSupport.buildAnnotation(annotationClass, + annotation)) + .toArray(value -> (T[]) Array.newInstance(annotationClass, value)); + } + + @Override + public T synthesizeDeclared(Class annotationClass) { + return findDeclaredAnnotation(annotationClass) + .map(av -> AnnotationMetadataSupport.buildAnnotation(annotationClass, av)) + .orElse(null); + } + + @Override + public T synthesize(Class annotationClass) { + return findAnnotation(annotationClass) + .map(av -> AnnotationMetadataSupport.buildAnnotation(annotationClass, av)) + .orElse(null); + } + + @Override + public List> getAnnotationValuesByType(Class annotationType) { + return getAnnotationValues(() -> getAnnotationMetadata().getAnnotationValuesByType(annotationType)); + } + + @Override + public List> getDeclaredAnnotationValuesByType(Class annotationType) { + return getAnnotationValues(() -> getAnnotationMetadata().getDeclaredAnnotationValuesByType(annotationType)); + } + + @Override + public List> getAnnotationValuesByStereotype(String stereotype) { + return getAnnotationValues(() -> getAnnotationMetadata().getAnnotationValuesByStereotype(stereotype)); + } + + @Override + public List> getDeclaredAnnotationValuesByName(String annotationType) { + return getAnnotationValues(() -> getAnnotationMetadata().getDeclaredAnnotationValuesByName(annotationType)); + } + + @Override + public List> getAnnotationValuesByName(String annotationType) { + return getAnnotationValues(() -> getAnnotationMetadata().getAnnotationValuesByName(annotationType)); + } + + private List> getAnnotationValues(Supplier>> supplier) { + return supplier.get().stream() + .map(this::mapAnnotationValue) + .toList(); + } +} diff --git a/inject/src/main/java/io/micronaut/inject/annotation/MutableAnnotationMetadata.java b/inject/src/main/java/io/micronaut/inject/annotation/MutableAnnotationMetadata.java index fd4a696037c..b094717c6b2 100644 --- a/inject/src/main/java/io/micronaut/inject/annotation/MutableAnnotationMetadata.java +++ b/inject/src/main/java/io/micronaut/inject/annotation/MutableAnnotationMetadata.java @@ -18,6 +18,7 @@ import io.micronaut.context.env.DefaultPropertyPlaceholderResolver; import io.micronaut.core.annotation.AnnotationMetadata; import io.micronaut.core.annotation.AnnotationValue; +import io.micronaut.core.expressions.EvaluatedExpressionReference; import io.micronaut.core.annotation.Internal; import io.micronaut.core.annotation.NonNull; import io.micronaut.core.annotation.Nullable; @@ -50,6 +51,8 @@ public class MutableAnnotationMetadata extends DefaultAnnotationMetadata { private boolean hasPropertyExpressions = false; + private boolean hasEvaluatedExpressions = false; + @Nullable Map> annotationDefaultValues; @Nullable @@ -65,12 +68,22 @@ public class MutableAnnotationMetadata extends DefaultAnnotationMetadata { public MutableAnnotationMetadata() { } + private MutableAnnotationMetadata(@Nullable Map> declaredAnnotations, + @Nullable Map> declaredStereotypes, + @Nullable Map> allStereotypes, + @Nullable Map> allAnnotations, + @Nullable Map> annotationsByStereotype, + boolean hasPropertyExpressions) { + this(declaredAnnotations, declaredStereotypes, allStereotypes, allAnnotations, annotationsByStereotype, hasPropertyExpressions, false); + } + private MutableAnnotationMetadata(@Nullable Map> declaredAnnotations, @Nullable Map> declaredStereotypes, @Nullable Map> allStereotypes, @Nullable Map> allAnnotations, @Nullable Map> annotationsByStereotype, - boolean hasPropertyExpressions) { + boolean hasPropertyExpressions, + boolean hasEvaluatedExpressions) { super(declaredAnnotations, declaredStereotypes, allStereotypes, @@ -78,6 +91,7 @@ private MutableAnnotationMetadata(@Nullable Map(annotationDefaultValues); @@ -129,6 +149,7 @@ public MutableAnnotationMetadata clone() { cloned.sourceAnnotationDefaultValues = cloneMapOfMapValue(sourceAnnotationDefaultValues); } cloned.hasPropertyExpressions = hasPropertyExpressions; + cloned.hasEvaluatedExpressions = hasEvaluatedExpressions; return cloned; } @@ -151,6 +172,10 @@ private boolean computeHasPropertyExpressions(Map values, return hasPropertyExpressions || values != null && retentionPolicy == RetentionPolicy.RUNTIME && hasPropertyExpressions(values); } + private boolean computeHasEvaluatedExpressions(Map values, RetentionPolicy retentionPolicy) { + return hasEvaluatedExpressions || values != null && retentionPolicy == RetentionPolicy.RUNTIME && hasEvaluatedExpressions(values); + } + private boolean hasPropertyExpressions(Map values) { if (CollectionUtils.isEmpty(values)) { return false; @@ -544,6 +569,8 @@ private void addAnnotation(String annotation, boolean isDeclared, RetentionPolicy retentionPolicy) { hasPropertyExpressions = computeHasPropertyExpressions(values, retentionPolicy); + hasEvaluatedExpressions = computeHasEvaluatedExpressions(values, retentionPolicy); + if (isDeclared && declaredAnnotations != null) { putValues(annotation, values, declaredAnnotations); } @@ -646,6 +673,8 @@ private void addRepeatableInternal(String repeatableAnnotationContainer, Map> allAnnotations, RetentionPolicy retentionPolicy) { hasPropertyExpressions = computeHasPropertyExpressions(annotationValue.getValues(), retentionPolicy); + hasEvaluatedExpressions = computeHasEvaluatedExpressions(annotationValue.getValues(), retentionPolicy); + if (annotationRepeatableContainer == null) { annotationRepeatableContainer = new HashMap<>(2); } @@ -718,6 +747,7 @@ protected AnnotationValue newAnnotationValue(String an @Internal public void addAnnotationMetadata(DefaultAnnotationMetadata annotationMetadata) { hasPropertyExpressions |= annotationMetadata.hasPropertyExpressions(); + hasEvaluatedExpressions |= annotationMetadata.hasEvaluatedExpressions(); if (annotationMetadata.declaredAnnotations != null && !annotationMetadata.declaredAnnotations.isEmpty()) { if (declaredAnnotations == null) { declaredAnnotations = new LinkedHashMap<>(); @@ -779,6 +809,7 @@ public void addAnnotationMetadata(DefaultAnnotationMetadata annotationMetadata) public void addAnnotationMetadata(MutableAnnotationMetadata annotationMetadata) { addAnnotationMetadata((DefaultAnnotationMetadata) annotationMetadata); hasPropertyExpressions |= annotationMetadata.hasPropertyExpressions; + hasEvaluatedExpressions |= annotationMetadata.hasEvaluatedExpressions; if (annotationMetadata.sourceRetentionAnnotations != null) { if (sourceRetentionAnnotations == null) { sourceRetentionAnnotations = new HashSet<>(annotationMetadata.sourceRetentionAnnotations); @@ -1047,4 +1078,24 @@ protected String findRepeatableAnnotationContainerInternal(String annotation) { } return AnnotationMetadataSupport.getRepeatableAnnotation(annotation); } + + private boolean hasEvaluatedExpressions(Map annotationValues) { + if (CollectionUtils.isEmpty(annotationValues)) { + return false; + } + + return annotationValues.values().stream().anyMatch(value -> { + if (value instanceof EvaluatedExpressionReference) { + return true; + } else if (value instanceof AnnotationValue av) { + return hasEvaluatedExpressions(av.getValues()); + } else if (value instanceof AnnotationValue[] avArray) { + return Arrays.stream(avArray) + .map(AnnotationValue::getValues) + .anyMatch(this::hasEvaluatedExpressions); + } else { + return false; + } + }); + } } diff --git a/src/main/docs/guide/config/evaluatedExpressions.adoc b/src/main/docs/guide/config/evaluatedExpressions.adoc new file mode 100644 index 00000000000..d36b8f6ad92 --- /dev/null +++ b/src/main/docs/guide/config/evaluatedExpressions.adoc @@ -0,0 +1,408 @@ +Since 4.0, Micronaut framework supports embedding evaluated expressions in annotation values using `#{...}` syntax which +allows to achieve even more flexibility while configuring your application. + +.Evaluated Expression example +[source,groovy] +---- +@Value("#{ T(Math).random() }") +double injectedValue; +---- + +Expressions can be defined whenever an annotation member accepts a string or an array of strings. + +.Evaluated Expression in array +[source,java] +---- +@Singleton +@Requires(env = {"dev", "#{ 'test' }"}) +public class EvaluatedExpressionInArray {} +---- + +You can also embed one or more expressions in a string template in a similar manner to embedding properties with the `${...}` syntax. + +.Evaluated Expression template +[source,groovy] +---- +@Value("http://#{'hostname'}/#{'path'}") +String url; +---- + +Evaluated Expressions are validated and compiled at build time which guarantees type safety at runtime. + +Once an application is running expressions are evaluated on demand as part of annotation metadata resolution. The +usage of expressions does not impact performance as evaluation process is completely reflection free. + +Note that, for security reasons expressions cannot be dynamically compiled at runtime from potentially untrusted +input. All expressions are compiled and checked statically during the compilation process of the application with +errors reported as compilation failures. + +In general, expressions can be treated as statement written using a programming language with reduced +set of available features. Even though the complexity of expression is only limited by the list of supported syntax +constructs, it is in general not recommended to place complex logic inside an expression as there are usually better +ways to achieve the same result. + +== Example Use Case + +Expressions can be used anywhere throughout the Micronaut framework and associated modules, but as an example, you can use them to implement simple scheduled job control, for example: + +snippet::io.micronaut.docs.expressions.ExampleJob[title="Job Control with Expressions"] + +<1> Here the `condition` member of the ann:scheduling.annotation.Scheduled[] annotation is used to only execute the job if a pre-condition is met. +<2> The `condition` invokes a method of a parameter which is another bean called `ExampleJobControl` which can be used to pause and resume execution of the job as desired. + +== Evaluated Expression Language Reference + +The Evaluated Expressions syntax supports the following functionality: + +* Literal Values +* Math Operators +* Comparison Operators +* Logical Operators +* Ternary Operator +* Type References +* Method Invocation +* Property Access + +=== Literal Values + +The following types of literal values are supported: + +* `null` +* boolean values (`true`, `false`) +* strings, which need to be surrounded with single quotation mark (`'`) +* numeric values (`int`, `long`, `float`, `double`) + +Integer and Long values can also be specified in hexadecimal or octal notation. Float and Double values can also be +specified in exponential notation. All numeric values can be negative as well. + +.Literal values examples +[source] +---- +#{ null } +#{ true } +#{ 'string value' } +#{ 10 } +#{ 0xFFL } +#{ 10L } +#{ .123f } +#{ 1E+1d } +#{ 123D } +---- + +=== Math Operators + +The supported mathematical operators are `+`, `-`, `*`, `/`, `%`, `^`. Math operators can only be applied to numeric +values (except `+` which can be used for string concatenation as well). Mathematical operations are performed in order +enforced by standard operator precedence. You can also change evaluation order by using brackets `()`. + +`/` and `%` operators can be aliased by `div` and `mod` keywords respectively. + +.Math operators examples +[source] +---- +#{ 1 + 2 } // 3 +#{ 'a' + 'b' + 'c' } // 'abc' +#{ 7 - 3 } // 4 +#{ 7 * 3 } // 21 +#{ 7 * ( 3 + 1) } // 28 + +#{ 15 / 3 } // 5 +#{ 15 div 3 } // 5 + +#{ 15 % 3 } // 0 +#{ 15 mod 3 } // 0 + +// Unlike in Java, ^ operator means exponentiation +#{ 3 ^ 2 } // 9 +---- + +=== Comparison Operators + +The following comparison operators are supported: `==`, `!=`, `>`, `<`, `>=`, `\<=`, `matches` +Comparison operations are performed in order enforced by standard operator precedence. +You can also change evaluation order by using brackets `()`. + +Equality check is supported for both primitive types and objects. It is performed using `Object.equals()` method. + +`>`, `<`, `>=`, `\<=` operations can only be applied to numeric types. + +`matches` keyword can be used to determine whether a string matches provided regular expression which has to +be specified as string literal. The regular expression itself will be checked for validity at compilation time. + +.Comparison operators examples +[source] +---- +#{ 1 + 2 == 3 } // true +#{ 'abc' != 'abc' } // false +#{ 7 > 3 } // true +#{ 7 < 3 } // false +#{ 7 >= 7 } // true +#{ 7 <= 8 } // false + +#{ 'AbC' matches '[A-Za-z*' } // Compilation failure +#{ 'AbC' matches '[A-Za-z]*' } // true +#{ 'AbC' matches '[a-z]*' } // false +---- + +=== Logical Operators + +The following logical operators are supported: + +* `&&` (can be aliased with `and`) +* `||` (can be aliased with `or`), +* `!` (can be aliaded with `not`) +* `empty` / `not empty` (works with strings, collections, arrays, and maps) + +Logical operations are performed in order enforced by standard operator precedence. +You can also change evaluation order by using brackets `()`. + +.Logical operators examples +[source] +---- +#{ true && false } // false +#{ true and true } // true + +#{ true || false } // true +#{ false or false } // false + +#{ !false } // true +#{ !!true } // true + +#{ empty '' } // true +#{ not empty '' } // false +---- + +=== Ternary Operator + +A standard ternary operator is supported to allow specifying if-then-else conditional logic in expression + +[source] +---- +condition ? thenBranch : elseBranch +---- + +where `condition` evaluation should provide boolean value, and the complexity of `then` and `else` branches is not +limited. + +.Ternary operator examples +[source] +---- +#{ 15 > 10 ? 'a' : 'b' } // 'a' +#{ 15 >= 16 ? 'a' : 'b' } // 'b' +---- + +=== Dot and Safe Navigation Operator + +The dot operator can be use to access methods and properties of a value within an expression. For example: + +.Dot operator usage +[source] +---- +#{ collection.size() > 0 } +#{ foo.bar.name == "Fred" } +---- + +You can also use the safe dereference operator `?.` to navigate paths in a null safe way: + +.Safe dereference operator +[source] +---- +#{ foo?.bar?.name == "Fred" } +---- + +=== Type References + +A predefined syntax construct `T(...)` can be used to reference a class. The value inside brackets should be fully +qualified class name (including the package name). The only exception is `java.lang.*` classes which can be referenced +directly by only specifying the simple class name. Primitive types can not be referenced. + +Type References are evaluated in different ways depending on the context. + +==== Simple type reference + +A simple type reference is resolved as a `Class` object. + +.Type reference example +[source] +---- +#{ T(java.lang.String) } // String.class +---- + +Same rule applies if type reference is specified as a method argument. + +==== Type check with `instanceof` + +A Type Reference can be used as the right-hand side part of the `instanceof` operator + +.Type check example +[source] +---- +#{ 'abc' instanceof T(String) } // true +---- + +which is equivalent to the following Java code and will be evaluated as a boolean value: + +[source] +---- +"abc" instanceof String +---- + +==== Static method invocation + +Type Reference can be used to invoke a static method of a class + +.Static method invocation +[source] +---- +#{ T(Math).random() } +---- + +=== Expression Evaluation Context + +By default, the only methods you can invoke inside Evaluated Expressions are static methods using type references. + +The available methods can be extended by extended the evaluation context. There are two ways to extend the evaluation context. The first involves registering new context class via a custom api:TypeElementVisitor[]. + +NOTE: The api:TypeElementVisitor[] has to be on the annotation processor classpath, therefore needs to be defined in a separate module that can be included on this classpath. + +Once a class is registered within evaluation context the methods and properties of the class are available for referencing in evaluated expressions. Any context reference +needs to be prefixed with `#` sign. + +Consider the following example: + +snippet::io.micronaut.docs.expressions.CustomEvaluationContext[title="User-defined evaluated expression context"] + +NOTE: The class should be resolvable as a bean can use `jakarta.inject` annotations to inject other types if necessary. + +Registering this class can be achieved with a custom implementation of api:expressions.context.ExpressionEvaluationContextRegistrar[] that is registered via service loader as a api:inject.visitor.TypeElementVisitor[] (create a new `META-INF/services/io.micronaut.inject.visitor.TypeElementVisitor` file referencing the new class) and placed on the annotation processor classpath: + +snippet::io.micronaut.docs.expressions.ContextRegistrar[title="Defining a ExpressionEvaluationContextRegistrar"] + +Method `generateRandom(int, int)` can now be used within Evaluated Expression in the following way: + +snippet::io.micronaut.docs.expressions.ContextConsumer[title="Usage of user-defined evaluated expression context"] + +At runtime, the bean will be retrieved from +application context and respective method will be invoked. + +If a matching method is not found within evaluation context at compilation time, the compilation will fail. A +compilation error will also occur if multiple suitable methods are found in the evaluation context, keep that in mind +if you provide multiple api:expressions.context.ExpressionEvaluationContextRegistrar[] that a conflict can occur as these types are effectively global. + +The methods will be considered ambiguous (leading to compilation failure) when their names are the same and list of +provided arguments matches multiple methods parameters. + +Using a api:expressions.context.ExpressionEvaluationContextRegistrar[] makes its methods and properties available for evaluated +expressions within any annotation in a global manner. + +However, you can also specify evaluation context scoped to concrete annotation or +annotation member using ann:context.annotation.AnnotationExpressionContext[]. + +snippet::io.micronaut.docs.expressions.AnnotationContextExample[title="Usage of annotation level evaluated expression context"] + +<1> Here two new methods are introduced to the context called `firstValue()` and `secondValue()` only for the scope of the `@CustomAnnotation` +<2> The `firstValue()` method is defined in a bean called `AnnotationContext` +<3> The `secondValue()` method is defined in a bean called `AnnotationMemberContext` +<4> On the `@CustomAnnotation` annotation the methods of the `AnnotationContext` type are exposed to all members of the annotation (type level context). +<5> On the `value()` member of the `@CustomAnnotation` annotation the methods of the `AnnotationContextExample` are made available but scoped only to the `value()` member. + +Again context classes need to be explicitly defined as beans to make them available for retrieval from +application context at runtime. + +=== Method Invocation + +You can invoke both static methods using type references, methods from evaluation context and methods on objects, +which means method chaining is supported. + +.Chaining methods in expression +[source,java] +---- +import io.micronaut.context.annotation.Value; +import jakarta.inject.Singleton; + +@Singleton +class CustomEvaluationContext { + + public String stringValue() { + return "stringValue"; + } + +} + +@Singleton +class ContextConsumer { + + @Value("#{ #stringValue().length() }") + public int stringLength; + +} +---- + +Varargs methods invocation is supported as well. Note that if last parameter of a method is an array, you can still +invoke it providing list of arguments separated by comma without explicitly wrapping it into array. So in this case +it will be treated in same way as if last method argument was explicitly specified as varargs parameter. + +.Invoking varargs methods in expressions +[source,java] +---- +import io.micronaut.context.annotation.Value; +import jakarta.inject.Singleton; + +@Singleton +class CustomEvaluationContext { + + public int countIntegers(int... values) { + return values.length; + } + + public int countStrings(String[] values) { + return values.length; + } + +} + +@Singleton +class ContextConsumer { + + @Value("#{ #countIntegers(1, 2, 3) }") + public int totalIntegers; + + @Value("#{ #countStrings('a', 'b', 'c') }") + public int totalStrings; + +} +---- + +=== Property Access + +JavaBean properties can be accessed simply be referencing their names from evaluation context prefixed with `#`. Bean +properties can also be chained with dot in the same way as methods. + +.Accessing bean properties in expressions +[source,java] +---- + +import io.micronaut.context.annotation.Value; +import jakarta.inject.Singleton; + +@Singleton +class CustomEvaluationContext { + + public String getName() { + return "Bob"; + } + + public int getAge() { + return 25; + } + +} + +@Singleton +class ContextConsumer { + + @Value("#{ 'Name is ' + #name + ', age is ' + #age }") + public String value; + +} +---- diff --git a/src/main/docs/guide/introduction/whatsNew.adoc b/src/main/docs/guide/introduction/whatsNew.adoc index 0c214f50859..27e901b81aa 100644 --- a/src/main/docs/guide/introduction/whatsNew.adoc +++ b/src/main/docs/guide/introduction/whatsNew.adoc @@ -15,15 +15,42 @@ Micronaut framework 4.x supports https://groovy-lang.org/releasenotes/groovy-4.0 === Core Changes -* <> -* <> +==== Java 17 Baseline -* <> +Micronaut 4 now requires a minimum of Java 17 for building and running applications. + +==== Improved Modularity + +The core of Micronaut has been further refactored to improve modularity and reduce the footprint of a Micronaut application, including: + +* Third-party dependencies on SnakeYAML and Jackson Databind are now optional and can be removed if other implementations are present. +* The runtime and compiler code has been split, allowing the removal of the re-packaging of ASM and Caffeine and reduction of the runtime footprint. +* The built in <>, <>, <>, <> and <> features have been split into separate modules allowing removal of this functionality if not needed. + +==== GraalVM Metadata Repository and Runtime Initialization + +The https://graalvm.github.io/native-build-tools/latest/gradle-plugin.html#metadata-support[GraalVM Metadata Repository] in Micronaut's Gradle and Maven plugins is now enabled by default and Micronaut has been altered to by default primarily initialize at runtime to ensure consistency in behaviour between JIT and Native applications. + +==== Completed `javax` to `jakarta` Migration + +The remaining functionality depending on the `javax` specification has been migrated to `jakarta` including the validation module (for `jakarta.validation`) and support for Hibernate 6 (for `jakarta.persistence`). + +==== Expression Language + +A new fully compilation time, type-safe and reflection-free <> has been added to the framework which unlocks a number of new possibilities (like conditional job scheduling). It is expected that sub-modules will adopt the new EL over time to add features and capabilities. ==== Injection of Maps It is now possible to inject a `java.util.Map` of beans where the key is the bean name. The name of the bean is derived from the <> or (if not present) the simple name of the class. +==== Arbitrary Nesting of Configuration Properties + +With Micronaut 4 it is now possible to arbitrarily nest ann:context.annotation.ConfigurationProperties[] and ann:context.annotation.EachProperty[] annotations allowing for more dynamic configuration possibilities. + +==== Improved Error Messages for Missing Configuration + +When a bean is not present due to missing configuration (such as a bean that uses ann:context.annotation.EachProperty[]) error messages have been improved to display the configuration that is required to activate the bean. + ==== Improved Error Messages for Missing Beans When a bean annotated with ann:context.annotation.EachProperty[] or ann:context.annotation.Bean[] is not found due to missing configuration an error is thrown showing the configuration prefix necessary to resolve the issue. @@ -34,9 +61,27 @@ Beans that are disabled via <> are now trac The disabled beans are also now visible via the <> in the <> aiding in understanding the state of your application configuration. +=== HTTP Changes + +==== Initial Support for Virtual Threads (Loom) + +Preview <> has been added. When using JDK 19 or above with preview features enabled you can off load processing to a virtual thread pool. + +==== Rewritten HTTP layer + +The HTTP layer has been rewritten to improve performance and reduce the presence of reactive stack frames if reactive is not used (such as with Virtual threads). + +==== Annotation-Based HTTP Filters + +See <> + +==== JDK HTTP Client + +<> + === Other Dependency Upgrades -- Kotlin 1.7.10 +- Kotlin 1.8.10 <> diff --git a/src/main/docs/guide/toc.yml b/src/main/docs/guide/toc.yml index f90cca28647..ed1af90a3e8 100644 --- a/src/main/docs/guide/toc.yml +++ b/src/main/docs/guide/toc.yml @@ -47,6 +47,7 @@ config: environments: The Environment propertySource: Externalized Configuration with PropertySources valueAnnotation: Configuration Injection + evaluatedExpressions: Expression Language configurationProperties: Configuration Properties customTypeConverter: Custom Type Converters eachProperty: Using @EachProperty to Drive Configuration diff --git a/test-suite-groovy/build.gradle b/test-suite-groovy/build.gradle index 9961729f7f4..697d3f0d357 100644 --- a/test-suite-groovy/build.gradle +++ b/test-suite-groovy/build.gradle @@ -51,6 +51,7 @@ dependencies { testRuntimeOnly libs.bcpkix testImplementation libs.managed.reactor + testImplementation(libs.awaitility) } //compileTestGroovy.groovyOptions.forkOptions.jvmArgs = ['-Xdebug', '-Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=5005'] diff --git a/test-suite-groovy/src/test/groovy/io/micronaut/docs/expressions/AnnotationContextExample.groovy b/test-suite-groovy/src/test/groovy/io/micronaut/docs/expressions/AnnotationContextExample.groovy new file mode 100644 index 00000000000..929e7ed9299 --- /dev/null +++ b/test-suite-groovy/src/test/groovy/io/micronaut/docs/expressions/AnnotationContextExample.groovy @@ -0,0 +1,30 @@ +package io.micronaut.docs.expressions; + +import jakarta.inject.Singleton; +import io.micronaut.context.annotation.AnnotationExpressionContext; + +@Singleton +@CustomAnnotation(value = "#{firstValue() + secondValue()}") // <1> +class Example { +} + +@Singleton +class AnnotationContext { // <2> + String firstValue() { + return "first value" + } +} + +@Singleton +class AnnotationMemberContext { // <3> + String secondValue() { + return "second value" + } +} + +@AnnotationExpressionContext(AnnotationContext.class) // <4> +@interface CustomAnnotation { + + @AnnotationExpressionContext(AnnotationMemberContext.class) // <5> + String value(); +} diff --git a/test-suite-groovy/src/test/groovy/io/micronaut/docs/expressions/AnnotationContextExampleSpec.groovy b/test-suite-groovy/src/test/groovy/io/micronaut/docs/expressions/AnnotationContextExampleSpec.groovy new file mode 100644 index 00000000000..6bc733755d1 --- /dev/null +++ b/test-suite-groovy/src/test/groovy/io/micronaut/docs/expressions/AnnotationContextExampleSpec.groovy @@ -0,0 +1,21 @@ +package io.micronaut.docs.expressions + +import io.micronaut.context.BeanContext +import io.micronaut.inject.BeanDefinition +import spock.lang.AutoCleanup +import spock.lang.Shared +import spock.lang.Specification + +class AnnotationContextExampleSpec extends Specification { + @Shared + @AutoCleanup + BeanContext beanContext = BeanContext.run() + void "testAnnotationContextEvaluation"() { + given: + BeanDefinition beanDefinition = beanContext.getBeanDefinition(Example) + String val = beanDefinition.stringValue(CustomAnnotation).orElse(null) + + expect: + "first valuesecond value" == val + } +} diff --git a/test-suite-groovy/src/test/groovy/io/micronaut/docs/expressions/ExampleJob.groovy b/test-suite-groovy/src/test/groovy/io/micronaut/docs/expressions/ExampleJob.groovy new file mode 100644 index 00000000000..0b239f5e2d0 --- /dev/null +++ b/test-suite-groovy/src/test/groovy/io/micronaut/docs/expressions/ExampleJob.groovy @@ -0,0 +1,38 @@ +package io.micronaut.docs.expressions + +import io.micronaut.scheduling.annotation.Scheduled +import jakarta.inject.Singleton + +@Singleton +class ExampleJob { + private boolean jobRan = false + + @Scheduled( + fixedRate = "1s", + condition = '#{!jobControl.paused}') // <1> + void run(ExampleJobControl jobControl) { + System.out.println("Job Running") + this.jobRan = true + } + + boolean hasJobRun() { + return jobRan + } +} + +@Singleton +class ExampleJobControl { // <2> + private boolean paused = true + + boolean isPaused() { + return paused + } + + void unpause() { + paused = false + } + + void pause() { + paused = true + } +} diff --git a/test-suite-groovy/src/test/groovy/io/micronaut/docs/expressions/ExampleJobSpec.groovy b/test-suite-groovy/src/test/groovy/io/micronaut/docs/expressions/ExampleJobSpec.groovy new file mode 100644 index 00000000000..7646d09fb78 --- /dev/null +++ b/test-suite-groovy/src/test/groovy/io/micronaut/docs/expressions/ExampleJobSpec.groovy @@ -0,0 +1,40 @@ +package io.micronaut.docs.expressions + +import io.micronaut.context.ApplicationContext +import spock.lang.AutoCleanup +import spock.lang.Shared; +import spock.lang.Specification + +import static java.util.concurrent.TimeUnit.SECONDS +import static org.awaitility.Awaitility.await + +class ExampleJobSpec extends Specification { + @Shared + @AutoCleanup + ApplicationContext ctx = ApplicationContext.run() + + void testJobCondition(){ + given: + ExampleJob exampleJob = ctx.getBean(ExampleJob) + ExampleJobControl jobControl = ctx.getBean(ExampleJobControl) + + expect: + jobControl.isPaused() + !exampleJob.hasJobRun() + + when: + Thread.sleep(5000) + + then: + !exampleJob.hasJobRun() + + when: + jobControl.unpause() + + then: + await().atMost(3, SECONDS).until(exampleJob::hasJobRun) + + and: + exampleJob.hasJobRun() + } +} diff --git a/test-suite-kotlin/build.gradle b/test-suite-kotlin/build.gradle index 3601da2ba3a..5bb63ffe9bf 100644 --- a/test-suite-kotlin/build.gradle +++ b/test-suite-kotlin/build.gradle @@ -43,7 +43,7 @@ dependencies { // Adding these for now since micronaut-test isnt resolving correctly ... probably need to upgrade gradle there too testImplementation libs.junit.jupiter.api - + testImplementation libs.awaitility testImplementation platform(libs.test.boms.micronaut.validation) testImplementation (libs.micronaut.validation) { exclude group: 'io.micronaut' diff --git a/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/expressions/AnnotationContextExample.kt b/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/expressions/AnnotationContextExample.kt new file mode 100644 index 00000000000..f7cc3c90976 --- /dev/null +++ b/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/expressions/AnnotationContextExample.kt @@ -0,0 +1,24 @@ +package io.micronaut.docs.expressions + +import io.micronaut.context.annotation.AnnotationExpressionContext +import jakarta.inject.Singleton + +@Singleton +@CustomAnnotation(value = "#{firstValue() + secondValue()}") // <1> +class Example + +@Singleton +class AnnotationContext { // <2> + fun firstValue() = "first value" +} + +@Singleton +class AnnotationMemberContext { // <3> + fun secondValue() = "second value" +} + +@AnnotationExpressionContext(AnnotationContext::class) // <4> +annotation class CustomAnnotation( + @get:AnnotationExpressionContext(AnnotationMemberContext::class) // <5> + val value: String +) diff --git a/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/expressions/AnnotationContextExampleTest.kt b/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/expressions/AnnotationContextExampleTest.kt new file mode 100644 index 00000000000..bbc41db6d17 --- /dev/null +++ b/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/expressions/AnnotationContextExampleTest.kt @@ -0,0 +1,21 @@ +package io.micronaut.docs.expressions + +import io.micronaut.context.BeanContext +import io.micronaut.test.extensions.junit5.annotation.MicronautTest +import jakarta.inject.Inject +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Test + +@MicronautTest(startApplication = false) +class AnnotationContextExampleTest { + + @Inject + lateinit var beanContext: BeanContext + + @Test + fun testAnnotationContextEvaluation() { + val beanDefinition = beanContext.getBeanDefinition(Example::class.java) + val value = beanDefinition.stringValue(CustomAnnotation::class.java).orElse(null) + Assertions.assertEquals(value, "first valuesecond value") + } +} diff --git a/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/expressions/ExampleJob.kt b/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/expressions/ExampleJob.kt new file mode 100644 index 00000000000..923fe8bcea3 --- /dev/null +++ b/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/expressions/ExampleJob.kt @@ -0,0 +1,33 @@ +package io.micronaut.docs.expressions + +import io.micronaut.scheduling.annotation.Scheduled +import jakarta.inject.Singleton + +@Singleton +class ExampleJob { + private var jobRan = false + @Scheduled( + fixedRate = "1s", + condition = "#{!jobControl.paused}") // <1> + fun run(jobControl: ExampleJobControl) { + println("Job Running") + jobRan = true + } + + fun hasJobRun(): Boolean { + return jobRan + } +} + +@Singleton +class ExampleJobControl { // <2> + var paused = true + + fun unpause() { + paused = false + } + + fun pause() { + paused = true + } +} diff --git a/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/expressions/ExampleJobTest.kt b/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/expressions/ExampleJobTest.kt new file mode 100644 index 00000000000..21620a078fa --- /dev/null +++ b/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/expressions/ExampleJobTest.kt @@ -0,0 +1,21 @@ +package io.micronaut.docs.expressions + +import io.micronaut.test.extensions.junit5.annotation.MicronautTest +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Test +import java.util.concurrent.Callable +import java.util.concurrent.TimeUnit + +@MicronautTest +class ExampleJobTest { + @Test + fun testJobCondition(exampleJob: ExampleJob, exampleJobControl: ExampleJobControl) { + Assertions.assertTrue(exampleJobControl.paused) + Assertions.assertFalse(exampleJob.hasJobRun()) + Thread.sleep(5000) + Assertions.assertFalse(exampleJob.hasJobRun()) + exampleJobControl.unpause() + org.awaitility.Awaitility.await().atMost(3, TimeUnit.SECONDS).until(Callable { exampleJob.hasJobRun() }) + Assertions.assertTrue(exampleJob.hasJobRun()) + } +} diff --git a/test-suite/src/main/java/io/micronaut/docs/expressions/ContextRegistrar.java b/test-suite/src/main/java/io/micronaut/docs/expressions/ContextRegistrar.java new file mode 100644 index 00000000000..87a6774a0a7 --- /dev/null +++ b/test-suite/src/main/java/io/micronaut/docs/expressions/ContextRegistrar.java @@ -0,0 +1,25 @@ +/* + * Copyright 2017-2023 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.expressions; + +import io.micronaut.expressions.context.ExpressionEvaluationContextRegistrar; + +public class ContextRegistrar implements ExpressionEvaluationContextRegistrar { + @Override + public String getContextClassName() { + return "io.micronaut.docs.expressions.CustomEvaluationContext"; + } +} diff --git a/test-suite/src/main/java/io/micronaut/docs/expressions/CustomEvaluationContext.java b/test-suite/src/main/java/io/micronaut/docs/expressions/CustomEvaluationContext.java new file mode 100644 index 00000000000..e2c249127d8 --- /dev/null +++ b/test-suite/src/main/java/io/micronaut/docs/expressions/CustomEvaluationContext.java @@ -0,0 +1,29 @@ +/* + * Copyright 2017-2023 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.expressions; + +import jakarta.inject.Singleton; + +import java.util.Random; + +@Singleton +public class CustomEvaluationContext { + private Random random = random = new Random(); + + public int generateRandom(int min, int max) { + return random.nextInt(max - min) + min; + } +} diff --git a/test-suite/src/main/resources/META-INF/services/io.micronaut.inject.visitor.TypeElementVisitor b/test-suite/src/main/resources/META-INF/services/io.micronaut.inject.visitor.TypeElementVisitor index 41fbeedc4ca..cef9589929f 100644 --- a/test-suite/src/main/resources/META-INF/services/io.micronaut.inject.visitor.TypeElementVisitor +++ b/test-suite/src/main/resources/META-INF/services/io.micronaut.inject.visitor.TypeElementVisitor @@ -1 +1,2 @@ -example.micronaut.inject.visitor.AnnotatingVisitor \ No newline at end of file +example.micronaut.inject.visitor.AnnotatingVisitor +io.micronaut.docs.expressions.ContextRegistrar diff --git a/test-suite/src/test/java/io/micronaut/docs/client/versioning/HelloClient.java b/test-suite/src/test/java/io/micronaut/docs/client/versioning/HelloClient.java index e20c9966830..09fc05378bc 100644 --- a/test-suite/src/test/java/io/micronaut/docs/client/versioning/HelloClient.java +++ b/test-suite/src/test/java/io/micronaut/docs/client/versioning/HelloClient.java @@ -26,7 +26,7 @@ // tag::clazz[] @Client("/hello") @Version("1") // <1> -public interface HelloClient { +public interface HelloClient { @Get("/greeting/{name}") String sayHello(String name); diff --git a/test-suite/src/test/java/io/micronaut/docs/expressions/AnnotationContextExample.java b/test-suite/src/test/java/io/micronaut/docs/expressions/AnnotationContextExample.java new file mode 100644 index 00000000000..7bc0f1621ef --- /dev/null +++ b/test-suite/src/test/java/io/micronaut/docs/expressions/AnnotationContextExample.java @@ -0,0 +1,30 @@ +package io.micronaut.docs.expressions; + +import jakarta.inject.Singleton; +import io.micronaut.context.annotation.AnnotationExpressionContext; + +@Singleton +@CustomAnnotation(value = "#{firstValue() + secondValue()}") // <1> +class Example { +} + +@Singleton +class AnnotationContext { // <2> + String firstValue() { + return "first value"; + } +} + +@Singleton +class AnnotationMemberContext { // <3> + String secondValue() { + return "second value"; + } +} + +@AnnotationExpressionContext(AnnotationContext.class) // <4> +@interface CustomAnnotation { + + @AnnotationExpressionContext(AnnotationMemberContext.class) // <5> + String value(); +} diff --git a/test-suite/src/test/java/io/micronaut/docs/expressions/AnnotationContextExampleTest.java b/test-suite/src/test/java/io/micronaut/docs/expressions/AnnotationContextExampleTest.java new file mode 100644 index 00000000000..5ca9209bd59 --- /dev/null +++ b/test-suite/src/test/java/io/micronaut/docs/expressions/AnnotationContextExampleTest.java @@ -0,0 +1,19 @@ +package io.micronaut.docs.expressions; + +import io.micronaut.context.BeanContext; +import io.micronaut.inject.BeanDefinition; +import io.micronaut.test.extensions.junit5.annotation.MicronautTest; +import jakarta.inject.Inject; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +@MicronautTest +public class AnnotationContextExampleTest { + @Inject BeanContext beanContext; + @Test + void testAnnotationContextEvaluation() { + BeanDefinition beanDefinition = beanContext.getBeanDefinition(Example.class); + String val = beanDefinition.stringValue(CustomAnnotation.class).orElse(null); + Assertions.assertEquals(val, "first valuesecond value"); + } +} diff --git a/test-suite/src/test/java/io/micronaut/docs/expressions/ContextConsumer.java b/test-suite/src/test/java/io/micronaut/docs/expressions/ContextConsumer.java new file mode 100644 index 00000000000..120f36339a5 --- /dev/null +++ b/test-suite/src/test/java/io/micronaut/docs/expressions/ContextConsumer.java @@ -0,0 +1,13 @@ +package io.micronaut.docs.expressions; + +import io.micronaut.context.annotation.Value; +import jakarta.inject.Singleton; + +@Singleton +public class ContextConsumer { + + @Value("#{ generateRandom(1, 10) }") + public int randomField; + +} + diff --git a/test-suite/src/test/java/io/micronaut/docs/expressions/ContextConsumerTest.java b/test-suite/src/test/java/io/micronaut/docs/expressions/ContextConsumerTest.java new file mode 100644 index 00000000000..4a9bdec283b --- /dev/null +++ b/test-suite/src/test/java/io/micronaut/docs/expressions/ContextConsumerTest.java @@ -0,0 +1,13 @@ +package io.micronaut.docs.expressions; + +import io.micronaut.test.extensions.junit5.annotation.MicronautTest; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +@MicronautTest +public class ContextConsumerTest { + @Test + void testContextConsumer(ContextConsumer consumer) { + Assertions.assertTrue(consumer.randomField > 0 && consumer.randomField < 20); + } +} diff --git a/test-suite/src/test/java/io/micronaut/docs/expressions/ContextRegistrar.java b/test-suite/src/test/java/io/micronaut/docs/expressions/ContextRegistrar.java new file mode 100644 index 00000000000..71ac0a8dab1 --- /dev/null +++ b/test-suite/src/test/java/io/micronaut/docs/expressions/ContextRegistrar.java @@ -0,0 +1,10 @@ +package io.micronaut.docs.expressions; + +import io.micronaut.expressions.context.ExpressionEvaluationContextRegistrar; + +public class ContextRegistrar implements ExpressionEvaluationContextRegistrar { + @Override + public String getContextClassName() { + return "io.micronaut.docs.expressions.CustomEvaluationContext"; + } +} diff --git a/test-suite/src/test/java/io/micronaut/docs/expressions/CustomEvaluationContext.java b/test-suite/src/test/java/io/micronaut/docs/expressions/CustomEvaluationContext.java new file mode 100644 index 00000000000..eeb76e339e1 --- /dev/null +++ b/test-suite/src/test/java/io/micronaut/docs/expressions/CustomEvaluationContext.java @@ -0,0 +1,11 @@ +package io.micronaut.docs.expressions; + +import jakarta.inject.Singleton; +import java.util.Random; + +@Singleton +public class CustomEvaluationContext { + public int generateRandom(int min, int max) { + return new Random().nextInt(max - min) + min; + } +} diff --git a/test-suite/src/test/java/io/micronaut/docs/expressions/ExampleJob.java b/test-suite/src/test/java/io/micronaut/docs/expressions/ExampleJob.java new file mode 100644 index 00000000000..fb2e180d9b1 --- /dev/null +++ b/test-suite/src/test/java/io/micronaut/docs/expressions/ExampleJob.java @@ -0,0 +1,38 @@ +package io.micronaut.docs.expressions; + +import io.micronaut.scheduling.annotation.Scheduled; +import jakarta.inject.Singleton; + +@Singleton +public class ExampleJob { + private boolean jobRan = false; + + @Scheduled( + fixedRate = "1s", + condition = "#{!jobControl.paused}") // <1> + void run(ExampleJobControl jobControl) { + System.out.println("Job Running"); + this.jobRan = true; + } + + public boolean hasJobRun() { + return jobRan; + } +} + +@Singleton +class ExampleJobControl { // <2> + private boolean paused = true; + + public boolean isPaused() { + return paused; + } + + public void unpause() { + paused = false; + } + + public void pause() { + paused = true; + } +} diff --git a/test-suite/src/test/java/io/micronaut/docs/expressions/ExampleJobTest.java b/test-suite/src/test/java/io/micronaut/docs/expressions/ExampleJobTest.java new file mode 100644 index 00000000000..e061d3db904 --- /dev/null +++ b/test-suite/src/test/java/io/micronaut/docs/expressions/ExampleJobTest.java @@ -0,0 +1,25 @@ +package io.micronaut.docs.expressions; + +import io.micronaut.test.extensions.junit5.annotation.MicronautTest; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import static java.util.concurrent.TimeUnit.SECONDS; +import static org.awaitility.Awaitility.await; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@MicronautTest +public class ExampleJobTest { + + @Test + void testJobCondition(ExampleJob exampleJob, ExampleJobControl jobControl) throws InterruptedException { + assertTrue(jobControl.isPaused()); + assertFalse(exampleJob.hasJobRun()); + Thread.sleep(5000); + assertFalse(exampleJob.hasJobRun()); + jobControl.unpause(); + await().atMost(3, SECONDS).until(exampleJob::hasJobRun); + assertTrue(exampleJob.hasJobRun()); + } +} From 9ca308f515f57a9b54fdb7d73e246016271c6837 Mon Sep 17 00:00:00 2001 From: micronaut-build <65172877+micronaut-build@users.noreply.github.com> Date: Mon, 27 Mar 2023 23:08:07 +0200 Subject: [PATCH 625/743] Bump micronaut-maven-plugin to 3.5.3 (#9012) --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 6df7eb03e0b..e29867a61b7 100644 --- a/gradle.properties +++ b/gradle.properties @@ -43,7 +43,7 @@ developers=Graeme Rocher kapt.use.worker.api=true # Dependency Versions -micronautMavenPluginVersion=3.5.2 +micronautMavenPluginVersion=3.5.3 chromedriverVersion=79.0.3945.36 geckodriverVersion=0.26.0 webdriverBinariesVersion=1.4 From 0c1d4867f4b333ab672f204cd6307160856a7e54 Mon Sep 17 00:00:00 2001 From: Tim Yates Date: Tue, 28 Mar 2023 11:40:02 +0100 Subject: [PATCH 626/743] Logback configuration defined via JAVA_TOOL_OPTIONS is ignored (#9009) Previously, we only checked the classpath for logback configuration files, and when refreshing we ignored the `logback.configurationFile` setting. This PR checks the filesystem if the config cannot be found on the classpath It also adds `logback.configurationFile` as an optional property that points to the location of the config for when we refresh the configuration. `logback.configurationFile` has precedence over the existing `logger.config` property --- .../logging/impl/LogbackLoggingSystem.java | 32 ++++++++++- .../micronaut/logging/impl/LogbackUtils.java | 27 +++++++-- settings.gradle | 1 + .../build.gradle.kts | 18 ++++++ .../src/external/external-logback.xml | 16 ++++++ .../logback/ExternalConfigurationSpec.groovy | 56 +++++++++++++++++++ .../src/test/resources/logback.xml | 16 ++++++ 7 files changed, 160 insertions(+), 6 deletions(-) create mode 100644 test-suite-logback-external-configuration/build.gradle.kts create mode 100644 test-suite-logback-external-configuration/src/external/external-logback.xml create mode 100644 test-suite-logback-external-configuration/src/test/groovy/io/micronaut/logback/ExternalConfigurationSpec.groovy create mode 100644 test-suite-logback-external-configuration/src/test/resources/logback.xml diff --git a/runtime/src/main/java/io/micronaut/logging/impl/LogbackLoggingSystem.java b/runtime/src/main/java/io/micronaut/logging/impl/LogbackLoggingSystem.java index bb7c0cc3ad4..f1f98b53106 100644 --- a/runtime/src/main/java/io/micronaut/logging/impl/LogbackLoggingSystem.java +++ b/runtime/src/main/java/io/micronaut/logging/impl/LogbackLoggingSystem.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2020 original authors + * Copyright 2017-2023 original authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -23,6 +23,7 @@ import io.micronaut.core.annotation.Nullable; import io.micronaut.logging.LogLevel; import io.micronaut.logging.LoggingSystem; +import jakarta.inject.Inject; import jakarta.inject.Singleton; import org.slf4j.LoggerFactory; @@ -41,8 +42,35 @@ public final class LogbackLoggingSystem implements LoggingSystem { private final String logbackXmlLocation; + /** + * @deprecated Use {@link LogbackLoggingSystem#LogbackLoggingSystem(String, String)} instead + * @param logbackXmlLocation + */ + @Deprecated public LogbackLoggingSystem(@Nullable @Property(name = "logger.config") String logbackXmlLocation) { - this.logbackXmlLocation = logbackXmlLocation != null ? logbackXmlLocation : DEFAULT_LOGBACK_LOCATION; + this( + System.getProperty("logback.configurationFile"), + logbackXmlLocation + ); + } + + /** + * @param logbackExternalConfigLocation The location of the logback configuration file set via logback properties + * @param logbackXmlLocation The location of the logback configuration file set via micronaut properties + * @since 3.8.8 + */ + @Inject + public LogbackLoggingSystem( + @Nullable @Property(name = "logback.configurationFile") String logbackExternalConfigLocation, + @Nullable @Property(name = "logger.config") String logbackXmlLocation + ) { + if (logbackExternalConfigLocation != null) { + this.logbackXmlLocation = logbackExternalConfigLocation; + } else if (logbackXmlLocation != null) { + this.logbackXmlLocation = logbackXmlLocation; + } else { + this.logbackXmlLocation = DEFAULT_LOGBACK_LOCATION; + } } @Override diff --git a/runtime/src/main/java/io/micronaut/logging/impl/LogbackUtils.java b/runtime/src/main/java/io/micronaut/logging/impl/LogbackUtils.java index 29bb45f652f..25c914851b2 100644 --- a/runtime/src/main/java/io/micronaut/logging/impl/LogbackUtils.java +++ b/runtime/src/main/java/io/micronaut/logging/impl/LogbackUtils.java @@ -24,6 +24,8 @@ import io.micronaut.core.annotation.Nullable; import io.micronaut.logging.LoggingSystemException; +import java.io.File; +import java.net.MalformedURLException; import java.net.URL; import java.util.Iterator; import java.util.ServiceLoader; @@ -50,18 +52,35 @@ private LogbackUtils() { public static void configure(@NonNull ClassLoader classLoader, @NonNull LoggerContext context, @NonNull String logbackXmlLocation) { - configure(context, logbackXmlLocation, () -> classLoader.getResource(logbackXmlLocation)); + configure(context, logbackXmlLocation, () -> { + // Check classpath first + URL resource = classLoader.getResource(logbackXmlLocation); + if (resource != null) { + return resource; + } + // Check file system + File file = new File(logbackXmlLocation); + if (file.exists()) { + try { + resource = file.toURI().toURL(); + } catch (MalformedURLException e) { + + throw new LoggingSystemException("Error creating URL for off-classpath resource", e); + } + } + return resource; + }); } /** * Configures a Logger Context. - * + *

* Searches fpr a custom {@link Configurator} via a service loader. * If not present it configures the context with the resource. * - * @param context Logger Context + * @param context Logger Context * @param logbackXmlLocation the location of the xml logback config file - * @param resourceSupplier A resource for example logback.xml + * @param resourceSupplier A resource for example logback.xml */ private static void configure( @NonNull LoggerContext context, diff --git a/settings.gradle b/settings.gradle index 4bdc2a553f5..290ed698e71 100644 --- a/settings.gradle +++ b/settings.gradle @@ -70,6 +70,7 @@ include "test-suite-graal" include "test-suite-groovy" include "test-suite-groovy" include "test-suite-logback" +include "test-suite-logback-external-configuration" include "test-utils" // benchmarks diff --git a/test-suite-logback-external-configuration/build.gradle.kts b/test-suite-logback-external-configuration/build.gradle.kts new file mode 100644 index 00000000000..a35d4e5419d --- /dev/null +++ b/test-suite-logback-external-configuration/build.gradle.kts @@ -0,0 +1,18 @@ +plugins { + id("io.micronaut.build.internal.convention-test-library") +} + +dependencies { + testAnnotationProcessor(projects.injectJava) + + testImplementation(libs.managed.micronaut.test.spock) { + exclude(group="io.micronaut", module="micronaut-aop") + } + testImplementation(projects.context) + testImplementation(projects.injectGroovy) + testImplementation(libs.managed.logback) + testImplementation(projects.management) + testImplementation(projects.httpClient) + + testRuntimeOnly(projects.httpServerNetty) +} diff --git a/test-suite-logback-external-configuration/src/external/external-logback.xml b/test-suite-logback-external-configuration/src/external/external-logback.xml new file mode 100644 index 00000000000..a21b01a7a8d --- /dev/null +++ b/test-suite-logback-external-configuration/src/external/external-logback.xml @@ -0,0 +1,16 @@ + + + + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + + diff --git a/test-suite-logback-external-configuration/src/test/groovy/io/micronaut/logback/ExternalConfigurationSpec.groovy b/test-suite-logback-external-configuration/src/test/groovy/io/micronaut/logback/ExternalConfigurationSpec.groovy new file mode 100644 index 00000000000..3a0f4fed2dc --- /dev/null +++ b/test-suite-logback-external-configuration/src/test/groovy/io/micronaut/logback/ExternalConfigurationSpec.groovy @@ -0,0 +1,56 @@ +package io.micronaut.logback + +import ch.qos.logback.classic.Level +import ch.qos.logback.classic.Logger +import io.micronaut.context.ApplicationContext +import io.micronaut.runtime.server.EmbeddedServer +import org.slf4j.LoggerFactory +import spock.lang.See +import spock.lang.Specification +import spock.util.environment.RestoreSystemProperties + +@See("https://logback.qos.ch/manual/configuration.html#auto_configuration") +class ExternalConfigurationSpec extends Specification { + + @RestoreSystemProperties + def "should use the external configuration"() { + given: + System.setProperty("logback.configurationFile", "src/external/external-logback.xml") + + when: + Logger fromXml = (Logger) LoggerFactory.getLogger("i.should.not.exist") + Logger external = (Logger) LoggerFactory.getLogger("external.logging") + + then: 'logback.xml is ignored as we have set a configurationFile' + fromXml.level == null + + and: 'external configuration is used' + external.level == Level.TRACE + } + + @RestoreSystemProperties + def "should still use the external config if custom levels are defines"() { + given: + System.setProperty("logback.configurationFile", "src/external/external-logback.xml") + + when: + def server = ApplicationContext.run(EmbeddedServer, [ + "logger.levels.app.customisation": "DEBUG" + ]) + Logger fromXml = (Logger) LoggerFactory.getLogger("i.should.not.exist") + Logger custom = (Logger) LoggerFactory.getLogger("app.customisation") + Logger external = (Logger) LoggerFactory.getLogger("external.logging") + + then: 'logback.xml is ignored as we have set a configurationFile' + fromXml.level == null + + and: 'custom levels are still respected' + custom.level == Level.DEBUG + + and: 'external configuration is used' + external.level == Level.TRACE + + cleanup: + server.stop() + } +} diff --git a/test-suite-logback-external-configuration/src/test/resources/logback.xml b/test-suite-logback-external-configuration/src/test/resources/logback.xml new file mode 100644 index 00000000000..7b053ed9a39 --- /dev/null +++ b/test-suite-logback-external-configuration/src/test/resources/logback.xml @@ -0,0 +1,16 @@ + + + + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + + From 9bd35a1b0c0d90c60ce6afcc4ca98fd674175e4e Mon Sep 17 00:00:00 2001 From: Graeme Rocher Date: Tue, 28 Mar 2023 15:12:22 +0200 Subject: [PATCH 627/743] fix java.lang.IllegalArgumentException: duplicate element: ALL_PUBLIC_METHODS (#9015) * fix java.lang.IllegalArgumentException: duplicate element: ALL_PUBLIC_METHODS * fix test --- .../io/micronaut/core/graal/GraalReflectionConfigurer.java | 7 +++++-- .../micronaut/graal/reflect/GraalTypeElementVisitor.java | 4 +++- .../graal/reflect/GraalTypeElementVisitorSpec.groovy | 6 +++--- 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/core/src/main/java/io/micronaut/core/graal/GraalReflectionConfigurer.java b/core/src/main/java/io/micronaut/core/graal/GraalReflectionConfigurer.java index e1f02dde4c9..12d84c22406 100644 --- a/core/src/main/java/io/micronaut/core/graal/GraalReflectionConfigurer.java +++ b/core/src/main/java/io/micronaut/core/graal/GraalReflectionConfigurer.java @@ -23,6 +23,7 @@ import io.micronaut.core.annotation.Nullable; import io.micronaut.core.annotation.ReflectionConfig; import io.micronaut.core.annotation.TypeHint; +import io.micronaut.core.util.CollectionUtils; import java.lang.reflect.Constructor; import java.lang.reflect.Field; @@ -62,8 +63,10 @@ default void configure(ReflectionConfigurationContext context) { return; } context.register(t); - final Set accessType = Set.of( - reflectConfig.enumValues("accessType", TypeHint.AccessType.class) + TypeHint.AccessType[] accessTypes = reflectConfig.enumValues("accessType", TypeHint.AccessType.class); + // DO NOT change to Set.of(..) which disallows duplicates + final Set accessType = CollectionUtils.setOf( + accessTypes ); if (accessType.contains(TypeHint.AccessType.ALL_PUBLIC_METHODS)) { final Method[] methods = t.getMethods(); diff --git a/graal/src/main/java/io/micronaut/graal/reflect/GraalTypeElementVisitor.java b/graal/src/main/java/io/micronaut/graal/reflect/GraalTypeElementVisitor.java index 01cc7b8bf9b..e27b8232572 100644 --- a/graal/src/main/java/io/micronaut/graal/reflect/GraalTypeElementVisitor.java +++ b/graal/src/main/java/io/micronaut/graal/reflect/GraalTypeElementVisitor.java @@ -50,6 +50,8 @@ import java.util.List; import java.util.Map; import java.util.Set; +import java.util.SortedSet; +import java.util.TreeSet; import java.util.function.Predicate; import java.util.stream.Collectors; @@ -365,7 +367,7 @@ private ReflectionConfigData resolveClassData(String introspectedClass, Map type; - private final List accessTypes = new ArrayList<>(5); + private final SortedSet accessTypes = new TreeSet<>(); private final List> methods = new ArrayList<>(30); private final List> fields = new ArrayList<>(30); diff --git a/graal/src/test/groovy/io/micronaut/graal/reflect/GraalTypeElementVisitorSpec.groovy b/graal/src/test/groovy/io/micronaut/graal/reflect/GraalTypeElementVisitorSpec.groovy index 659b9fdf7b2..6de46e72193 100644 --- a/graal/src/test/groovy/io/micronaut/graal/reflect/GraalTypeElementVisitorSpec.groovy +++ b/graal/src/test/groovy/io/micronaut/graal/reflect/GraalTypeElementVisitorSpec.groovy @@ -332,7 +332,7 @@ class Test { then: config config.stringValue("type").get() == 'test.Test' - config.enumValues("accessType", TypeHint.AccessType) == [TypeHint.AccessType.ALL_PUBLIC_METHODS, TypeHint.AccessType.ALL_DECLARED_CONSTRUCTORS, TypeHint.AccessType.ALL_DECLARED_FIELDS] as TypeHint.AccessType[] + config.enumValues("accessType", TypeHint.AccessType) as Set == [TypeHint.AccessType.ALL_PUBLIC_METHODS, TypeHint.AccessType.ALL_DECLARED_CONSTRUCTORS, TypeHint.AccessType.ALL_DECLARED_FIELDS] as Set config.getAnnotations("methods").first().stringValue("name").get() == '' } @@ -359,7 +359,7 @@ class Test { then: config config.stringValue("type").get() == 'test.Test' - config.enumValues("accessType", TypeHint.AccessType) == [TypeHint.AccessType.ALL_PUBLIC_METHODS, TypeHint.AccessType.ALL_DECLARED_CONSTRUCTORS, TypeHint.AccessType.ALL_DECLARED_FIELDS] as TypeHint.AccessType[] + config.enumValues("accessType", TypeHint.AccessType) as Set == [TypeHint.AccessType.ALL_PUBLIC_METHODS, TypeHint.AccessType.ALL_DECLARED_CONSTRUCTORS, TypeHint.AccessType.ALL_DECLARED_FIELDS] as Set config.getAnnotations("methods").size() == 1 config.getAnnotations("methods").first().stringValue("name").get() == '' @@ -405,7 +405,7 @@ enum Test { then: config config.stringValue("type").get() == 'test.Test' - config.enumValues("accessType", TypeHint.AccessType) == [TypeHint.AccessType.ALL_PUBLIC_METHODS, TypeHint.AccessType.ALL_DECLARED_CONSTRUCTORS, TypeHint.AccessType.ALL_DECLARED_FIELDS] as TypeHint.AccessType[] + config.enumValues("accessType", TypeHint.AccessType) as Set == [TypeHint.AccessType.ALL_PUBLIC_METHODS, TypeHint.AccessType.ALL_DECLARED_CONSTRUCTORS, TypeHint.AccessType.ALL_DECLARED_FIELDS] as TypeHint.AccessType[] as Set config.getAnnotations("methods").size() == 2 // Two methods from Enum } From 42413d11fa3078b74722fafa7591eb8798fe07eb Mon Sep 17 00:00:00 2001 From: micronaut-build <65172877+micronaut-build@users.noreply.github.com> Date: Wed, 29 Mar 2023 10:18:01 +0200 Subject: [PATCH 628/743] Update common files (#8950) --- .github/workflows/graalvm.yml | 76 +++++++++++++++++++++++++++++++++++ .github/workflows/gradle.yml | 76 +++++++++++++++++------------------ 2 files changed, 112 insertions(+), 40 deletions(-) create mode 100644 .github/workflows/graalvm.yml diff --git a/.github/workflows/graalvm.yml b/.github/workflows/graalvm.yml new file mode 100644 index 00000000000..25f4ec7d7f2 --- /dev/null +++ b/.github/workflows/graalvm.yml @@ -0,0 +1,76 @@ +# WARNING: Do not edit this file directly. Instead, go to: +# +# https://github.com/micronaut-projects/micronaut-project-template/tree/master/.github/workflows +# +# and edit them there. Note that it will be sync'ed to all the Micronaut repos +name: GraalVM Dev CI +on: + schedule: + - cron: "0 1 * * 1-5" # Mon-Fri at 1am UTC + workflow_dispatch: +jobs: + build: + if: github.repository != 'micronaut-projects/micronaut-project-template' + runs-on: ubuntu-latest + strategy: + matrix: + graalvm: [ 'dev'] + java: ['17'] + env: + GRADLE_ENTERPRISE_ACCESS_KEY: ${{ secrets.GRADLE_ENTERPRISE_ACCESS_KEY }} + GRADLE_ENTERPRISE_CACHE_USERNAME: ${{ secrets.GRADLE_ENTERPRISE_CACHE_USERNAME }} + GRADLE_ENTERPRISE_CACHE_PASSWORD: ${{ secrets.GRADLE_ENTERPRISE_CACHE_PASSWORD }} + GH_TOKEN_PUBLIC_REPOS_READONLY: ${{ secrets.GH_TOKEN_PUBLIC_REPOS_READONLY }} + GH_USERNAME: ${{ secrets.GH_USERNAME }} + TESTCONTAINERS_RYUK_DISABLED: true + PREDICTIVE_TEST_SELECTION: "${{ github.event_name == 'pull_request' && 'true' || 'false' }}" + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + steps: + # https://github.com/actions/virtual-environments/issues/709 + - name: "🗑 Free disk space" + run: | + sudo rm -rf "/usr/local/share/boost" + sudo rm -rf "$AGENT_TOOLSDIRECTORY" + sudo apt-get clean + df -h + + - name: "📥 Checkout repository" + uses: actions/checkout@v3 + with: + fetch-depth: 0 + + - name: "🔧 Setup GraalVM CE" + uses: graalvm/setup-graalvm@v1 + with: + version: ${{ matrix.graalvm }} + java-version: ${{ matrix.java }} + components: 'native-image' + github-token: ${{ secrets.GITHUB_TOKEN }} + + - name: "🔧 Setup Gradle" + uses: gradle/gradle-build-action@v2 + + - name: "❓ Optional setup step" + run: | + [ -f ./setup.sh ] && ./setup.sh || [ ! -f ./setup.sh ] + + - name: "🛠 Build with Gradle" + id: gradle + run: | + ./gradlew check --no-daemon --continue + + - name: "📊 Publish Test Report" + if: always() + uses: mikepenz/action-junit-report@v3 + with: + check_name: Java CI / Test Report (${{ matrix.java }}) + report_paths: '**/build/test-results/test/TEST-*.xml' + check_retries: 'true' + + - name: "📜 Upload binary compatibility check results" + if: always() + uses: actions/upload-artifact@v3 + with: + name: binary-compatibility-reports + path: "**/build/reports/binary-compatibility-*.html" + diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index a8eea2a8faf..d0c741c8a23 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -21,90 +21,86 @@ jobs: matrix: graalvm: [ 'latest'] java: ['17'] + env: + GRADLE_ENTERPRISE_ACCESS_KEY: ${{ secrets.GRADLE_ENTERPRISE_ACCESS_KEY }} + GRADLE_ENTERPRISE_CACHE_USERNAME: ${{ secrets.GRADLE_ENTERPRISE_CACHE_USERNAME }} + GRADLE_ENTERPRISE_CACHE_PASSWORD: ${{ secrets.GRADLE_ENTERPRISE_CACHE_PASSWORD }} + GH_TOKEN_PUBLIC_REPOS_READONLY: ${{ secrets.GH_TOKEN_PUBLIC_REPOS_READONLY }} + GH_USERNAME: ${{ secrets.GH_USERNAME }} + TESTCONTAINERS_RYUK_DISABLED: true + PREDICTIVE_TEST_SELECTION: "${{ github.event_name == 'pull_request' && 'true' || 'false' }}" + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: # https://github.com/actions/virtual-environments/issues/709 - - name: Free disk space + - name: "🗑 Free disk space" run: | sudo rm -rf "/usr/local/share/boost" sudo rm -rf "$AGENT_TOOLSDIRECTORY" sudo apt-get clean df -h - - uses: actions/checkout@v3 + + - name: "📥 Checkout repository" + uses: actions/checkout@v3 with: fetch-depth: 0 - - name: Setup GraalVM CE + + - name: "🔧 Setup GraalVM CE" uses: graalvm/setup-graalvm@v1 with: version: ${{ matrix.graalvm }} java-version: ${{ matrix.java }} components: 'native-image' github-token: ${{ secrets.GITHUB_TOKEN }} - - name: Setup Gradle + + - name: "🔧 Setup Gradle" uses: gradle/gradle-build-action@v2 - - name: Optional setup step - env: - GRADLE_ENTERPRISE_ACCESS_KEY: ${{ secrets.GRADLE_ENTERPRISE_ACCESS_KEY }} - GRADLE_ENTERPRISE_CACHE_USERNAME: ${{ secrets.GRADLE_ENTERPRISE_CACHE_USERNAME }} - GRADLE_ENTERPRISE_CACHE_PASSWORD: ${{ secrets.GRADLE_ENTERPRISE_CACHE_PASSWORD }} + + - name: "❓ Optional setup step" run: | - [ -f ./setup.sh ] && ./setup.sh || true - - name: Build with Gradle + [ -f ./setup.sh ] && ./setup.sh || [ ! -f ./setup.sh ] + + - name: "🛠 Build with Gradle" id: gradle run: | - ./gradlew preReleaseCheck check --no-daemon --parallel --continue - env: - GH_TOKEN_PUBLIC_REPOS_READONLY: ${{ secrets.GH_TOKEN_PUBLIC_REPOS_READONLY }} - GH_USERNAME: ${{ secrets.GH_USERNAME }} - TESTCONTAINERS_RYUK_DISABLED: true - GRADLE_ENTERPRISE_ACCESS_KEY: ${{ secrets.GRADLE_ENTERPRISE_ACCESS_KEY }} - GRADLE_ENTERPRISE_CACHE_USERNAME: ${{ secrets.GRADLE_ENTERPRISE_CACHE_USERNAME }} - GRADLE_ENTERPRISE_CACHE_PASSWORD: ${{ secrets.GRADLE_ENTERPRISE_CACHE_PASSWORD }} - PREDICTIVE_TEST_SELECTION: "${{ github.event_name == 'pull_request' && 'true' || 'false' }}" - - name: Run static analysis - if: github.repository_owner == 'micronaut-projects' + ./gradlew check --no-daemon --continue + + - name: "🔎 Run static analysis" + if: env.SONAR_TOKEN != '' run: | ./gradlew sonar - env: - TESTCONTAINERS_RYUK_DISABLED: true - GRADLE_ENTERPRISE_ACCESS_KEY: ${{ secrets.GRADLE_ENTERPRISE_ACCESS_KEY }} - GRADLE_ENTERPRISE_CACHE_USERNAME: ${{ secrets.GRADLE_ENTERPRISE_CACHE_USERNAME }} - GRADLE_ENTERPRISE_CACHE_PASSWORD: ${{ secrets.GRADLE_ENTERPRISE_CACHE_PASSWORD }} - SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - GH_TOKEN_PUBLIC_REPOS_READONLY: ${{ secrets.GH_TOKEN_PUBLIC_REPOS_READONLY }} - GH_USERNAME: ${{ secrets.GH_USERNAME }} - - name: Publish Test Report + + - name: "📊 Publish Test Report" if: always() uses: mikepenz/action-junit-report@v3 with: check_name: Java CI / Test Report (${{ matrix.java }}) report_paths: '**/build/test-results/test/TEST-*.xml' check_retries: 'true' + - name: "📜 Upload binary compatibility check results" if: always() uses: actions/upload-artifact@v3 with: name: binary-compatibility-reports path: "**/build/reports/binary-compatibility-*.html" - - name: Publish to Sonatype Snapshots + + - name: "📦 Publish to Sonatype Snapshots" if: success() && github.event_name == 'push' && matrix.java == '17' env: - GH_TOKEN_PUBLIC_REPOS_READONLY: ${{ secrets.GH_TOKEN_PUBLIC_REPOS_READONLY }} - GH_USERNAME: ${{ secrets.GH_USERNAME }} SONATYPE_USERNAME: ${{ secrets.SONATYPE_USERNAME }} SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }} - GRADLE_ENTERPRISE_ACCESS_KEY: ${{ secrets.GRADLE_ENTERPRISE_ACCESS_KEY }} - GRADLE_ENTERPRISE_CACHE_USERNAME: ${{ secrets.GRADLE_ENTERPRISE_CACHE_USERNAME }} - GRADLE_ENTERPRISE_CACHE_PASSWORD: ${{ secrets.GRADLE_ENTERPRISE_CACHE_PASSWORD }} run: ./gradlew publishToSonatype docs --no-daemon - - name: Determine docs target repository + + - name: "❓ Determine docs target repository" uses: haya14busa/action-cond@v1 id: docs_target with: cond: ${{ github.repository == 'micronaut-projects/micronaut-core' }} if_true: "micronaut-projects/micronaut-docs" if_false: ${{ github.repository }} - - name: Publish to Github Pages + + - name: "📑 Publish to Github Pages" if: success() && github.event_name == 'push' && matrix.java == '17' uses: micronaut-projects/github-pages-deploy-action@master env: From 21effc4383f1e444f6596fb5f02ec97bbb9b10f1 Mon Sep 17 00:00:00 2001 From: Dean Wette Date: Wed, 29 Mar 2023 03:52:29 -0500 Subject: [PATCH 629/743] Correct typo in `OutgointRequestProcessorMatcher` classname. (#9016) fixes #8550 --- .../micronaut/http/util/OutgoingHttpRequestProcessor.java | 2 +- .../http/util/OutgoingHttpRequestProcessorImpl.java | 8 ++++---- ...rMatcher.java => OutgoingRequestProcessorMatcher.java} | 4 ++-- .../http/util/OutgoingHttpRequestProcessorImplSpec.groovy | 2 +- 4 files changed, 8 insertions(+), 8 deletions(-) rename http/src/main/java/io/micronaut/http/util/{OutgointRequestProcessorMatcher.java => OutgoingRequestProcessorMatcher.java} (88%) diff --git a/http/src/main/java/io/micronaut/http/util/OutgoingHttpRequestProcessor.java b/http/src/main/java/io/micronaut/http/util/OutgoingHttpRequestProcessor.java index 852739be83e..fb23a5eb01f 100644 --- a/http/src/main/java/io/micronaut/http/util/OutgoingHttpRequestProcessor.java +++ b/http/src/main/java/io/micronaut/http/util/OutgoingHttpRequestProcessor.java @@ -30,5 +30,5 @@ public interface OutgoingHttpRequestProcessor { * @param request The request * @return true if the request should be processed */ - boolean shouldProcessRequest(OutgointRequestProcessorMatcher matcher, HttpRequest request); + boolean shouldProcessRequest(OutgoingRequestProcessorMatcher matcher, HttpRequest request); } diff --git a/http/src/main/java/io/micronaut/http/util/OutgoingHttpRequestProcessorImpl.java b/http/src/main/java/io/micronaut/http/util/OutgoingHttpRequestProcessorImpl.java index 5f0fe2081c8..12decb3b952 100644 --- a/http/src/main/java/io/micronaut/http/util/OutgoingHttpRequestProcessorImpl.java +++ b/http/src/main/java/io/micronaut/http/util/OutgoingHttpRequestProcessorImpl.java @@ -32,12 +32,12 @@ public class OutgoingHttpRequestProcessorImpl implements OutgoingHttpRequestProcessor { /** - * @param matcher A {@link OutgointRequestProcessorMatcher} implementation. Entity defining matching rules. + * @param matcher A {@link OutgoingRequestProcessorMatcher} implementation. Entity defining matching rules. * @param request The request * @return true if the request should be processed */ @Override - public boolean shouldProcessRequest(OutgointRequestProcessorMatcher matcher, HttpRequest request) { + public boolean shouldProcessRequest(OutgoingRequestProcessorMatcher matcher, HttpRequest request) { Optional serviceId = request.getAttribute(HttpAttributes.SERVICE_ID.toString(), String.class); String uri = request.getUri().toString(); return shouldProcessRequest(matcher, serviceId.orElse(null), uri); @@ -45,12 +45,12 @@ public boolean shouldProcessRequest(OutgointRequestProcessorMatcher matcher, Htt /** * - * @param matcher A {@link OutgointRequestProcessorMatcher} implementation. Entity defining matching rules. + * @param matcher A {@link OutgoingRequestProcessorMatcher} implementation. Entity defining matching rules. * @param serviceId The service id * @param uri The URI of the request being processed * @return true if the request should be processed */ - public boolean shouldProcessRequest(OutgointRequestProcessorMatcher matcher, String serviceId, String uri) { + public boolean shouldProcessRequest(OutgoingRequestProcessorMatcher matcher, String serviceId, String uri) { if (matcher.getServiceIdPattern() != null && serviceId != null && matcher.getServiceIdPattern().matcher(serviceId).matches()) { return true; } diff --git a/http/src/main/java/io/micronaut/http/util/OutgointRequestProcessorMatcher.java b/http/src/main/java/io/micronaut/http/util/OutgoingRequestProcessorMatcher.java similarity index 88% rename from http/src/main/java/io/micronaut/http/util/OutgointRequestProcessorMatcher.java rename to http/src/main/java/io/micronaut/http/util/OutgoingRequestProcessorMatcher.java index ffa5f43c4ca..30c8bd5aff5 100644 --- a/http/src/main/java/io/micronaut/http/util/OutgointRequestProcessorMatcher.java +++ b/http/src/main/java/io/micronaut/http/util/OutgoingRequestProcessorMatcher.java @@ -23,10 +23,10 @@ * @author Sergio del Amo * @since 1.0 */ -public interface OutgointRequestProcessorMatcher { +public interface OutgoingRequestProcessorMatcher { /** - * @return a regular expresion to validate the service id against. + * @return a regular expression to validate the service id against. */ Pattern getServiceIdPattern(); diff --git a/http/src/test/groovy/io/micronaut/http/util/OutgoingHttpRequestProcessorImplSpec.groovy b/http/src/test/groovy/io/micronaut/http/util/OutgoingHttpRequestProcessorImplSpec.groovy index 01d346abb73..f588559ac26 100644 --- a/http/src/test/groovy/io/micronaut/http/util/OutgoingHttpRequestProcessorImplSpec.groovy +++ b/http/src/test/groovy/io/micronaut/http/util/OutgoingHttpRequestProcessorImplSpec.groovy @@ -47,7 +47,7 @@ class OutgoingHttpRequestProcessorImplSpec extends Specification { } } -class MockOutgoingRequestProcessorMatcher implements OutgointRequestProcessorMatcher { +class MockOutgoingRequestProcessorMatcher implements OutgoingRequestProcessorMatcher { Pattern serviceIdPattern Pattern uriPattern From 257c25c45089aeda4950d1625d8894d9b17be24f Mon Sep 17 00:00:00 2001 From: micronaut-build <65172877+micronaut-build@users.noreply.github.com> Date: Wed, 29 Mar 2023 21:21:08 +0200 Subject: [PATCH 630/743] Bump micronaut-security to 3.9.4 (#9021) --- gradle.properties | 2 +- gradle/libs.versions.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle.properties b/gradle.properties index e29867a61b7..eefa1985436 100644 --- a/gradle.properties +++ b/gradle.properties @@ -52,7 +52,7 @@ kotlin.stdlib.default.dependency=false # For the docs graalVersion=22.0.0.2 -micronautSecurityVersion=3.9.3 +micronautSecurityVersion=3.9.4 org.gradle.caching=true org.gradle.parallel=true diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index aada27d0e49..67bd872381a 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -112,7 +112,7 @@ managed-micronaut-rss = "3.2.0" managed-micronaut-rxjava1 = "1.0.0" managed-micronaut-rxjava2 = "1.3.0" managed-micronaut-rxjava3 = "2.4.0" -managed-micronaut-security = "3.9.3" +managed-micronaut-security = "3.9.4" managed-micronaut-serialization = "1.5.2" managed-micronaut-servlet = "3.3.5" managed-micronaut-spring = "4.4.0" From f8e5514003be47125b49955255952ec9bc743675 Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Wed, 29 Mar 2023 21:21:20 +0200 Subject: [PATCH 631/743] build: netty 4.1.90-Final (#9019) * build: netty 4.1.90-Final see https://netty.io/news/2023/03/14/4-1-90-Final.html Close #8773 * refactor test --- gradle/libs.versions.toml | 2 +- .../http/server/netty/binding/HttpResponseSpec.groovy | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 67bd872381a..2957f4e3d7a 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -126,7 +126,7 @@ managed-micronaut-views = "3.8.1" managed-micronaut-xml = "3.2.0" managed-neo4j = "3.5.35" managed-neo4j-java-driver = "4.4.9" -managed-netty = "4.1.87.Final" +managed-netty = "4.1.90.Final" managed-reactive-pg-client = "0.11.4" managed-reactive-streams = "1.0.4" # This should be kept aligned with https://github.com/micronaut-projects/micronaut-reactor/blob/master/gradle.properties from the BOM diff --git a/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/binding/HttpResponseSpec.groovy b/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/binding/HttpResponseSpec.groovy index df3fe92a1cc..02005fb800c 100644 --- a/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/binding/HttpResponseSpec.groovy +++ b/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/binding/HttpResponseSpec.groovy @@ -153,11 +153,10 @@ class HttpResponseSpec extends AbstractMicronautSpec { HttpHeaders headers = response.headers then: // The content length header was replaced, not appended - !headers.names().contains("content-type") - !headers.names().contains("Content-Length") - headers.contains("content-length") response.header("Content-Type") == "text/plain" response.header("Content-Length") == "3" + response.header("content-type") == "text/plain" + response.header("content-length") == "3" } void "test server header"() { From b16c05b7445acbc9a713c9f121d62678a60ded06 Mon Sep 17 00:00:00 2001 From: micronaut-build <65172877+micronaut-build@users.noreply.github.com> Date: Wed, 29 Mar 2023 21:23:31 +0200 Subject: [PATCH 632/743] Bump micronaut-openapi to 4.8.6 (#9020) * Bump micronaut-openapi to 4.8.6 * Update libs.versions.toml --------- Co-authored-by: Sergio del Amo --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 2957f4e3d7a..7973477ae68 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -100,7 +100,7 @@ managed-micronaut-neo4j = "5.2.0" managed-micronaut-nats = "3.1.0" managed-micronaut-netflix = "2.1.0" managed-micronaut-object-storage = "1.1.0" -managed-micronaut-openapi = "4.8.5" +managed-micronaut-openapi = "4.8.6" managed-micronaut-oraclecloud = "2.3.4" managed-micronaut-picocli = "4.3.0" managed-micronaut-problem = "2.6.0" From d6a69281c786e913f61b5f5c858ecd72af0dcb1b Mon Sep 17 00:00:00 2001 From: Denis Stepanov Date: Thu, 30 Mar 2023 01:27:42 -0600 Subject: [PATCH 633/743] Support JDK 20 in annotation processors (#9022) --- .../processing/AbstractInjectAnnotationProcessor.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/inject-java/src/main/java/io/micronaut/annotation/processing/AbstractInjectAnnotationProcessor.java b/inject-java/src/main/java/io/micronaut/annotation/processing/AbstractInjectAnnotationProcessor.java index 5e088701145..1f68fba099e 100644 --- a/inject-java/src/main/java/io/micronaut/annotation/processing/AbstractInjectAnnotationProcessor.java +++ b/inject-java/src/main/java/io/micronaut/annotation/processing/AbstractInjectAnnotationProcessor.java @@ -79,14 +79,14 @@ abstract class AbstractInjectAnnotationProcessor extends AbstractProcessor { @Override public SourceVersion getSupportedSourceVersion() { SourceVersion sourceVersion = SourceVersion.latest(); - if (sourceVersion.ordinal() <= 18) { + if (sourceVersion.ordinal() <= 20) { if (sourceVersion.ordinal() >= 8) { return sourceVersion; } else { return SourceVersion.RELEASE_8; } } else { - return (SourceVersion.values())[18]; + return (SourceVersion.values())[20]; } } From e39a1beb7d07eb894ea94a0d35cfee621d676113 Mon Sep 17 00:00:00 2001 From: micronaut-build <65172877+micronaut-build@users.noreply.github.com> Date: Thu, 30 Mar 2023 09:35:11 +0200 Subject: [PATCH 634/743] Bump micronaut-views to 3.8.2 (#9023) --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 7973477ae68..e0f1a21b26a 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -122,7 +122,7 @@ managed-micronaut-test-resources = "1.2.3" managed-micronaut-toml = "1.1.3" managed-micronaut-tracing = "4.4.1" managed-micronaut-tracing-legacy = "3.2.7" -managed-micronaut-views = "3.8.1" +managed-micronaut-views = "3.8.2" managed-micronaut-xml = "3.2.0" managed-neo4j = "3.5.35" managed-neo4j-java-driver = "4.4.9" From 6abd9c1cdd0a9aae251b5837ebb7ad3607d7f580 Mon Sep 17 00:00:00 2001 From: Tim Yates Date: Thu, 30 Mar 2023 08:35:24 +0100 Subject: [PATCH 635/743] Improve support for logback 1.3+ (#9018) * Improve support for logback 1.3+ Logback 1.3.x+ changed the binary format for Configurators, and introduced a default one. The issue is that when we call .configure on one, the JVM crashes as there is now an unexpected return value. I cannot find a way of detecting and logging this issue (without some sort of reflection) This change checks to see if the configurator we've detected is the default one added to logback in 1.3.x here https://github.com/qos-ch/logback/blob/5ac98f440dabec45d8ab9b3519b2aa308f05793b/logback-classic/src/main/java/ch/qos/logback/classic/util/DefaultJoranConfigurator.java And if it is, we ignore it. This will not fix it for people using a Custom Configurator that they compile under Logback 1.3+, they will still see the crash with no warning. But people upgrading to 1.3+ should see their application just run, and if they change their logback.xml emit debug with ``` ``` They will see ``` 12:55:21,222 |-INFO in ch.qos.logback.classic.LoggerContext[default] - Skipping ch.qos.logback.classic.util.DefaultJoranConfigurator as it's assumed to be from an unsupported version of Logback ``` * Fix for Java 8 --- .../micronaut/logging/impl/LogbackUtils.java | 15 +++- settings.gradle | 1 + test-suite-logback-14/build.gradle | 26 +++++++ .../logback/LoggerConfigurationSpec.groovy | 31 ++++++++ .../logback/LoggerEndpointSpec.groovy | 71 +++++++++++++++++++ .../micronaut/logback/LoggerLevelSpec.groovy | 46 ++++++++++++ .../micronaut/logback/MemoryAppender.groovy | 17 +++++ .../io/micronaut/logback/Application.java | 15 ++++ .../controllers/HelloWorldController.java | 21 ++++++ .../src/test/resources/logback.xml | 16 +++++ 10 files changed, 258 insertions(+), 1 deletion(-) create mode 100644 test-suite-logback-14/build.gradle create mode 100644 test-suite-logback-14/src/test/groovy/io/micronaut/logback/LoggerConfigurationSpec.groovy create mode 100644 test-suite-logback-14/src/test/groovy/io/micronaut/logback/LoggerEndpointSpec.groovy create mode 100644 test-suite-logback-14/src/test/groovy/io/micronaut/logback/LoggerLevelSpec.groovy create mode 100644 test-suite-logback-14/src/test/groovy/io/micronaut/logback/MemoryAppender.groovy create mode 100644 test-suite-logback-14/src/test/java/io/micronaut/logback/Application.java create mode 100644 test-suite-logback-14/src/test/java/io/micronaut/logback/controllers/HelloWorldController.java create mode 100644 test-suite-logback-14/src/test/resources/logback.xml diff --git a/runtime/src/main/java/io/micronaut/logging/impl/LogbackUtils.java b/runtime/src/main/java/io/micronaut/logging/impl/LogbackUtils.java index 25c914851b2..a395313c558 100644 --- a/runtime/src/main/java/io/micronaut/logging/impl/LogbackUtils.java +++ b/runtime/src/main/java/io/micronaut/logging/impl/LogbackUtils.java @@ -39,6 +39,8 @@ */ public final class LogbackUtils { + private static final String DEFAULT_LOGBACK_13_PROGRAMMATIC_CONFIGURATOR = "ch.qos.logback.classic.util.DefaultJoranConfigurator"; + private LogbackUtils() { } @@ -88,7 +90,7 @@ private static void configure( Supplier resourceSupplier ) { Configurator configurator = loadFromServiceLoader(); - if (configurator != null) { + if (isSupportedConfigurator(context, configurator)) { context.getStatusManager().add(new InfoStatus("Using " + configurator.getClass().getName(), context)); programmaticConfiguration(context, configurator); } else { @@ -105,6 +107,17 @@ private static void configure( } } + private static boolean isSupportedConfigurator(LoggerContext context, Configurator configurator) { + if (configurator == null) { + return false; + } + if (DEFAULT_LOGBACK_13_PROGRAMMATIC_CONFIGURATOR.equals(configurator.getClass().getName())) { + context.getStatusManager().add(new InfoStatus("Skipping " + configurator.getClass().getName() + " as it's assumed to be from an unsupported version of Logback", context)); + return false; + } + return true; + } + /** * Taken from {@link ch.qos.logback.classic.util.ContextInitializer#autoConfig}. */ diff --git a/settings.gradle b/settings.gradle index 290ed698e71..3fae51bce05 100644 --- a/settings.gradle +++ b/settings.gradle @@ -70,6 +70,7 @@ include "test-suite-graal" include "test-suite-groovy" include "test-suite-groovy" include "test-suite-logback" +include "test-suite-logback-14" include "test-suite-logback-external-configuration" include "test-utils" diff --git a/test-suite-logback-14/build.gradle b/test-suite-logback-14/build.gradle new file mode 100644 index 00000000000..e0a46cc5dee --- /dev/null +++ b/test-suite-logback-14/build.gradle @@ -0,0 +1,26 @@ +plugins { + id("io.micronaut.build.internal.convention-test-library") +} + +description = "logback tests with a new version of logback than we support, just to check it runs. Can be removed when we upgrade as part of 4.0.0" + +// Logback 1.4.x is Java 11+ compatible +def logbackVersion = JavaVersion.current().isJava11Compatible() ? "1.4.6" : "1.3.6" + +dependencies { + testAnnotationProcessor(projects.injectJava) + + testImplementation(libs.managed.micronaut.test.spock) { + exclude(group: "io.micronaut", module: "micronaut-aop") + } + testImplementation(projects.context) + testImplementation(projects.injectGroovy) + + // Use a newer version of logback than we support + testImplementation("ch.qos.logback:logback-classic:${logbackVersion}") + + testImplementation(projects.management) + testImplementation(projects.httpClient) + + testRuntimeOnly(projects.httpServerNetty) +} diff --git a/test-suite-logback-14/src/test/groovy/io/micronaut/logback/LoggerConfigurationSpec.groovy b/test-suite-logback-14/src/test/groovy/io/micronaut/logback/LoggerConfigurationSpec.groovy new file mode 100644 index 00000000000..7e79856d9ea --- /dev/null +++ b/test-suite-logback-14/src/test/groovy/io/micronaut/logback/LoggerConfigurationSpec.groovy @@ -0,0 +1,31 @@ +package io.micronaut.logback + +import ch.qos.logback.classic.Level +import ch.qos.logback.classic.Logger +import io.micronaut.context.annotation.Property +import io.micronaut.http.client.HttpClient +import io.micronaut.http.client.annotation.Client +import io.micronaut.test.extensions.spock.annotation.MicronautTest +import jakarta.inject.Inject +import org.slf4j.LoggerFactory +import spock.lang.Specification + +@MicronautTest +// Setting a level in a property forces a refresh, so the XML configuration is ignored. Without this in 3.8.x, the test fails. +@Property(name = "logger.levels.set.by.property", value = "DEBUG") +class LoggerConfigurationSpec extends Specification { + + @Inject + @Client("/") + HttpClient client + + void "if configuration is supplied, xml should be ignored"() { + given: + Logger fromXml = (Logger) LoggerFactory.getLogger("xml.config") + Logger fromProperties = (Logger) LoggerFactory.getLogger("set.by.property") + + expect: + fromXml.level == Level.TRACE + fromProperties.level == Level.DEBUG + } +} diff --git a/test-suite-logback-14/src/test/groovy/io/micronaut/logback/LoggerEndpointSpec.groovy b/test-suite-logback-14/src/test/groovy/io/micronaut/logback/LoggerEndpointSpec.groovy new file mode 100644 index 00000000000..d7aef0db31d --- /dev/null +++ b/test-suite-logback-14/src/test/groovy/io/micronaut/logback/LoggerEndpointSpec.groovy @@ -0,0 +1,71 @@ +package io.micronaut.logback + +import ch.qos.logback.classic.Logger +import ch.qos.logback.classic.spi.ILoggingEvent +import ch.qos.logback.core.AppenderBase +import io.micronaut.context.annotation.Property +import io.micronaut.http.HttpRequest +import io.micronaut.http.MediaType +import io.micronaut.http.client.HttpClient +import io.micronaut.http.client.annotation.Client +import io.micronaut.logback.controllers.HelloWorldController +import io.micronaut.test.extensions.spock.annotation.MicronautTest +import jakarta.inject.Inject +import org.slf4j.LoggerFactory +import spock.lang.Issue +import spock.lang.Specification + +@MicronautTest +@Property(name = "logger.levels.io.micronaut.logback", value = "INFO") +@Property(name = "endpoints.loggers.enabled", value = "true") +@Property(name = "endpoints.loggers.sensitive", value = "false") +@Property(name = "endpoints.loggers.write-sensitive", value = "false") +@Issue("https://github.com/micronaut-projects/micronaut-core/issues/8679") +class LoggerEndpointSpec extends Specification { + + @Inject + @Client("/") + HttpClient client + + void "logback configuration from properties is as expected"() { + when: + def response = client.toBlocking().retrieve("/loggers/io.micronaut.logback") + + then: + response.contains("INFO") + } + + void "logback can be configured"() { + given: + MemoryAppender appender = new MemoryAppender() + Logger l = (Logger) LoggerFactory.getLogger("io.micronaut.logback.controllers") + l.addAppender(appender) + appender.start() + + when: + def response = client.toBlocking().retrieve("/", String) + + then: 'response is as expected' + response == HelloWorldController.RESPONSE + + and: 'no log message is emitted' + appender.events.empty + + when: 'log level is changed to TRACE' + def body = '{ "configuredLevel": "TRACE" }' + def post = HttpRequest.POST("/loggers/io.micronaut.logback.controllers", body).contentType(MediaType.APPLICATION_JSON_TYPE) + client.toBlocking().exchange(post) + + and: + response = client.toBlocking().retrieve("/", String) + + then: 'response is as expected' + response == HelloWorldController.RESPONSE + + and: 'log message is emitted' + appender.events == [HelloWorldController.LOG_MESSAGE] + + cleanup: + appender.stop() + } +} diff --git a/test-suite-logback-14/src/test/groovy/io/micronaut/logback/LoggerLevelSpec.groovy b/test-suite-logback-14/src/test/groovy/io/micronaut/logback/LoggerLevelSpec.groovy new file mode 100644 index 00000000000..b953c4869e0 --- /dev/null +++ b/test-suite-logback-14/src/test/groovy/io/micronaut/logback/LoggerLevelSpec.groovy @@ -0,0 +1,46 @@ +package io.micronaut.logback + +import ch.qos.logback.classic.Logger +import ch.qos.logback.classic.spi.ILoggingEvent +import ch.qos.logback.core.AppenderBase +import io.micronaut.context.annotation.Property +import io.micronaut.http.HttpRequest +import io.micronaut.http.MediaType +import io.micronaut.http.client.HttpClient +import io.micronaut.http.client.annotation.Client +import io.micronaut.logback.controllers.HelloWorldController +import io.micronaut.test.extensions.spock.annotation.MicronautTest +import jakarta.inject.Inject +import org.slf4j.LoggerFactory +import spock.lang.Issue +import spock.lang.Specification + +@MicronautTest +@Property(name = "logger.levels.io.micronaut.logback.controllers", value = "TRACE") +@Issue("https://github.com/micronaut-projects/micronaut-core/issues/8678") +class LoggerLevelSpec extends Specification { + + @Inject + @Client("/") + HttpClient client + + void "logback can be configured via properties"() { + given: + MemoryAppender appender = new MemoryAppender() + Logger l = (Logger) LoggerFactory.getLogger("io.micronaut.logback.controllers") + l.addAppender(appender) + appender.start() + + when: + def response = client.toBlocking().retrieve("/", String) + + then: 'response is as expected' + response == HelloWorldController.RESPONSE + + and: 'log message is emitted' + appender.events == [HelloWorldController.LOG_MESSAGE] + + cleanup: + appender.stop() + } +} diff --git a/test-suite-logback-14/src/test/groovy/io/micronaut/logback/MemoryAppender.groovy b/test-suite-logback-14/src/test/groovy/io/micronaut/logback/MemoryAppender.groovy new file mode 100644 index 00000000000..3e978a6888e --- /dev/null +++ b/test-suite-logback-14/src/test/groovy/io/micronaut/logback/MemoryAppender.groovy @@ -0,0 +1,17 @@ +package io.micronaut.logback + +import ch.qos.logback.classic.spi.ILoggingEvent +import ch.qos.logback.core.AppenderBase +import groovy.transform.CompileStatic +import groovy.transform.PackageScope + +@PackageScope +@CompileStatic +class MemoryAppender extends AppenderBase { + List events = [] + + @Override + protected void append(ILoggingEvent e) { + events << e.formattedMessage + } +} diff --git a/test-suite-logback-14/src/test/java/io/micronaut/logback/Application.java b/test-suite-logback-14/src/test/java/io/micronaut/logback/Application.java new file mode 100644 index 00000000000..8f06535b06c --- /dev/null +++ b/test-suite-logback-14/src/test/java/io/micronaut/logback/Application.java @@ -0,0 +1,15 @@ +package io.micronaut.logback; + +import io.micronaut.runtime.Micronaut; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class Application { + + private static final Logger LOG = LoggerFactory.getLogger(Application.class); + + public static void main(String[] args) { + LOG.trace("starting the app"); + Micronaut.run(Application.class, args); + } +} diff --git a/test-suite-logback-14/src/test/java/io/micronaut/logback/controllers/HelloWorldController.java b/test-suite-logback-14/src/test/java/io/micronaut/logback/controllers/HelloWorldController.java new file mode 100644 index 00000000000..d431f7bb72b --- /dev/null +++ b/test-suite-logback-14/src/test/java/io/micronaut/logback/controllers/HelloWorldController.java @@ -0,0 +1,21 @@ +package io.micronaut.logback.controllers; + +import io.micronaut.http.annotation.Controller; +import io.micronaut.http.annotation.Get; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@Controller +public class HelloWorldController { + + public static final String RESPONSE = "Hello world!"; + public static final String LOG_MESSAGE = "inside hello world"; + + private static final Logger LOG = LoggerFactory.getLogger(HelloWorldController.class); + + @Get + String index() { + LOG.trace(LOG_MESSAGE); + return RESPONSE; + } +} diff --git a/test-suite-logback-14/src/test/resources/logback.xml b/test-suite-logback-14/src/test/resources/logback.xml new file mode 100644 index 00000000000..be7932b2810 --- /dev/null +++ b/test-suite-logback-14/src/test/resources/logback.xml @@ -0,0 +1,16 @@ + + + + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + + From 78f1ed6be376024debecb13a874a5faae0239e9e Mon Sep 17 00:00:00 2001 From: Tim Yates Date: Thu, 30 Mar 2023 13:49:13 +0100 Subject: [PATCH 636/743] Enhance TCK server headers test (#9025) Previously, this test could pass if the module simply capitalized the headers. This change ensures that headers are case-insensitive --- .../io/micronaut/http/server/tck/tests/HeadersTest.java | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/HeadersTest.java b/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/HeadersTest.java index eb087256105..4ec836899b3 100644 --- a/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/HeadersTest.java +++ b/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/HeadersTest.java @@ -30,7 +30,6 @@ import static io.micronaut.http.tck.TestScenario.asserts; - @SuppressWarnings({ "java:S5960", // We're allowed assertions, as these are used in tests only "checkstyle:MissingJavadocType", @@ -58,7 +57,7 @@ void headersAreCaseInsensitiveAsPerMessageHeadersSpecification() throws IOExcept HttpRequest.GET("/foo/bar").header("fOO", "ok"), (server, request) -> AssertionUtils.assertDoesNotThrow(server, request, HttpResponseAssertion.builder() .status(HttpStatus.OK) - .body("{\"status\":\"ok\"}") + .body("{\"status\":\"okok\"}") .build())); } @@ -71,8 +70,8 @@ String getOkAsJson() { } @Get(value = "/bar", produces = MediaType.APPLICATION_JSON) - String getFooAsJson(@Header("Foo") String foo) { - return "{\"status\":\"" + foo + "\"}"; + String getFooAsJson(@Header("Foo") String foo, @Header("fOo") String foo2) { + return "{\"status\":\"" + foo + foo2 + "\"}"; } } } From 7fd36d06bfa5bd239a79974557c57cfd5ab3299a Mon Sep 17 00:00:00 2001 From: Graeme Rocher Date: Thu, 30 Mar 2023 14:50:34 +0200 Subject: [PATCH 637/743] Unwrap optionals with safe navigation in expressions (#9024) --- .../parser/ast/access/ElementMethodCall.java | 37 ++++++-- .../parser/ast/access/PropertyAccess.java | 21 ++-- ...ontextPropertyAccessExpressionsSpec.groovy | 95 +++++++++++++++++++ .../guide/config/evaluatedExpressions.adoc | 2 + 4 files changed, 137 insertions(+), 18 deletions(-) diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/access/ElementMethodCall.java b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/access/ElementMethodCall.java index f99dde9ef90..ba5dc064f22 100644 --- a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/access/ElementMethodCall.java +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/access/ElementMethodCall.java @@ -16,6 +16,7 @@ package io.micronaut.expressions.parser.ast.access; import io.micronaut.core.annotation.Internal; +import io.micronaut.core.reflect.ReflectionUtils; import io.micronaut.expressions.parser.ast.ExpressionNode; import io.micronaut.expressions.parser.ast.types.TypeIdentifier; import io.micronaut.expressions.parser.compilation.ExpressionVisitorContext; @@ -23,15 +24,15 @@ import io.micronaut.inject.ast.ClassElement; import io.micronaut.inject.ast.ElementQuery; import io.micronaut.inject.ast.MethodElement; -import io.micronaut.inject.visitor.VisitorContext; +import io.micronaut.inject.processing.JavaModelUtils; import org.objectweb.asm.Label; import org.objectweb.asm.Type; import org.objectweb.asm.commons.GeneratorAdapter; import org.objectweb.asm.commons.Method; import java.util.List; +import java.util.Optional; -import static io.micronaut.expressions.parser.ast.util.EvaluatedExpressionCompilationUtils.getRequiredClassElement; import static org.objectweb.asm.Opcodes.ACONST_NULL; import static org.objectweb.asm.Opcodes.INVOKESTATIC; @@ -46,6 +47,8 @@ @Internal public sealed class ElementMethodCall extends AbstractMethodCall permits PropertyAccess { + private static final Type TYPE_OPTIONAL = Type.getType(Optional.class); + private static final Method METHOD_OR_ELSE = Method.getMethod(ReflectionUtils.getRequiredInternalMethod(Optional.class, "orElse", Object.class)); protected final ExpressionNode callee; private final boolean nullSafe; @@ -58,14 +61,19 @@ public ElementMethodCall(ExpressionNode callee, this.nullSafe = nullSafe; } + /** + * @return Is the method call null safe + */ + protected boolean isNullSafe() { + return nullSafe; + } + @Override protected void generateBytecode(ExpressionVisitorContext ctx) { GeneratorAdapter mv = ctx.methodVisitor(); - VisitorContext visitorContext = ctx.visitorContext(); - Type calleeType = callee.resolveType(ctx); + ClassElement calleeClass = callee.resolveClassElement(ctx); Method method = usedMethod.toAsmMethod(); - - ClassElement calleeClass = getRequiredClassElement(calleeType, visitorContext); + Type calleeType = JavaModelUtils.getTypeReference(calleeClass); if (callee instanceof TypeIdentifier) { compileArguments(ctx); @@ -78,16 +86,25 @@ protected void generateBytecode(ExpressionVisitorContext ctx) { } else { callee.compile(ctx); if (nullSafe) { + if (calleeClass.isAssignable(Optional.class)) { + mv.checkCast(TYPE_OPTIONAL); + // safe navigate optional + mv.visitInsn(ACONST_NULL); + mv.invokeVirtual(TYPE_OPTIONAL, METHOD_OR_ELSE); + // recompute new return type + calleeClass = calleeClass.getFirstTypeArgument().orElse(ClassElement.of(Object.class)); + calleeType = JavaModelUtils.getTypeReference(calleeClass); + mv.checkCast(calleeType); + } // null safe operator is used so we need to check the result is null - Type returnType = method.getReturnType(); - mv.storeLocal(2, returnType); - mv.loadLocal(2, returnType); + mv.storeLocal(2, calleeType); + mv.loadLocal(2, calleeType); Label proceed = new Label(); mv.ifNonNull(proceed); mv.visitInsn(ACONST_NULL); mv.returnValue(); mv.visitLabel(proceed); - mv.loadLocal(2, returnType); + mv.loadLocal(2, calleeType); } compileArguments(ctx); if (calleeClass.isInterface()) { diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/access/PropertyAccess.java b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/access/PropertyAccess.java index a480986c7f1..04146c9d35d 100644 --- a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/access/PropertyAccess.java +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/access/PropertyAccess.java @@ -23,12 +23,11 @@ import io.micronaut.inject.ast.MethodElement; import io.micronaut.inject.ast.PropertyElement; import io.micronaut.inject.ast.PropertyElementQuery; -import org.objectweb.asm.Type; import java.util.Collections; import java.util.List; +import java.util.Optional; -import static io.micronaut.expressions.parser.ast.util.EvaluatedExpressionCompilationUtils.getRequiredClassElement; import static java.util.Collections.emptyList; import static java.util.function.Predicate.not; @@ -48,30 +47,36 @@ public PropertyAccess(ExpressionNode callee, String name, boolean nullSafe) { @Override protected CandidateMethod resolveUsedMethod(ExpressionVisitorContext ctx) { - Type calleeType = callee.resolveType(ctx); - ClassElement classElement = getRequiredClassElement(calleeType, ctx.visitorContext()); + ClassElement classElement = callee.resolveClassElement(ctx); + + if (isNullSafe() && classElement.isAssignable(Optional.class)) { + // safe navigate optional + classElement = classElement.getFirstTypeArgument().orElse(classElement); + } List propertyElements = classElement.getBeanProperties( PropertyElementQuery.of(classElement.getAnnotationMetadata()) + .allowStaticProperties(false) .includes(Collections.singleton(name))).stream() .filter(not(PropertyElement::isExcluded)) .toList(); - if (propertyElements.size() == 0) { + if (propertyElements.isEmpty()) { throw new ExpressionCompilationException( - "Can not find property with name [" + name + "] in class " + calleeType); + "Can not find property with name [" + name + "] in class " + classElement.getName()); } else if (propertyElements.size() > 1) { throw new ExpressionCompilationException( "Ambiguous property access. Found " + propertyElements.size() + - " matching properties with name [" + name + "] in class " + calleeType); + " matching properties with name [" + name + "] in class " + classElement.getName()); } PropertyElement property = propertyElements.iterator().next(); + ClassElement finalClassElement = classElement; MethodElement methodElement = property.getReadMethod() .orElseThrow(() -> new ExpressionCompilationException( - "Can not resolve access method for property [" + name + "] in class " + calleeType)); + "Can not resolve access method for property [" + name + "] in class " + finalClassElement.getName())); return new CandidateMethod(methodElement); } diff --git a/inject-java/src/test/groovy/io/micronaut/expressions/ContextPropertyAccessExpressionsSpec.groovy b/inject-java/src/test/groovy/io/micronaut/expressions/ContextPropertyAccessExpressionsSpec.groovy index 80198df2750..bfde85a583c 100644 --- a/inject-java/src/test/groovy/io/micronaut/expressions/ContextPropertyAccessExpressionsSpec.groovy +++ b/inject-java/src/test/groovy/io/micronaut/expressions/ContextPropertyAccessExpressionsSpec.groovy @@ -142,6 +142,101 @@ class ContextPropertyAccessExpressionsSpec extends AbstractEvaluatedExpressionsS expr == null } + void "test multi-level context property access safe navigation - success"() { + given: + Object expr = evaluateAgainstContext("#{ foo?.bar?.name }", + """ + @jakarta.inject.Singleton + class Context { + public Foo getFoo() { + return new Foo(); + } + } + + class Foo { + private Bar bar = new Bar(); + public Bar getBar() { + return bar; + } + } + + class Bar { + private String name = "test"; + public String getName() { + return name; + } + } + """) + + expect: + expr == "test" + } + + void "test multi-level context property access safe navigation with optionals"() { + given: + Object expr = evaluateAgainstContext("#{ foo?.bar?.name }", + """ + import java.util.Optional; + + @jakarta.inject.Singleton + class Context { + public Optional getFoo() { + return Optional.of(new Foo()); + } + } + + class Foo { + private Bar bar; + public Optional getBar() { + return Optional.ofNullable(bar); + } + } + + class Bar { + private String name = "test"; + public String getName() { + return name; + } + } + """) + + expect: + expr == null + } + + void "test multi-level context property access safe navigation with optionals - success"() { + given: + Object expr = evaluateAgainstContext("#{ foo?.bar?.name }", + """ + import java.util.Optional; + + @jakarta.inject.Singleton + class Context { + public Optional getFoo() { + return Optional.of(new Foo()); + } + } + + class Foo { + private Bar bar = new Bar(); + public Optional getBar() { + return Optional.ofNullable(bar); + } + } + + class Bar { + private String name = "test"; + public String getName() { + return name; + } + } + """) + + expect: + expr == "test" + } + + void "test multi-level context property access non-safe navigation"() { when: Object expr = evaluateAgainstContext("#{ foo.bar.name }", diff --git a/src/main/docs/guide/config/evaluatedExpressions.adoc b/src/main/docs/guide/config/evaluatedExpressions.adoc index d36b8f6ad92..2966c2ede7b 100644 --- a/src/main/docs/guide/config/evaluatedExpressions.adoc +++ b/src/main/docs/guide/config/evaluatedExpressions.adoc @@ -210,6 +210,8 @@ You can also use the safe dereference operator `?.` to navigate paths in a null #{ foo?.bar?.name == "Fred" } ---- +TIP: When used, the safe dereference operator will also automatically unwrap Java's `Optional` type. + === Type References A predefined syntax construct `T(...)` can be used to reference a class. The value inside brackets should be fully From c396648a6b9bc459c4aa7913eebb8fabaa276c80 Mon Sep 17 00:00:00 2001 From: micronaut-build <65172877+micronaut-build@users.noreply.github.com> Date: Thu, 30 Mar 2023 16:10:03 +0200 Subject: [PATCH 638/743] Bump micronaut-data to 3.9.7 (#9026) --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index e0f1a21b26a..355d46a6225 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -73,7 +73,7 @@ managed-micronaut-cache = "3.5.0" managed-micronaut-cassandra = "5.1.1" managed-micronaut-coherence = "3.7.2" managed-micronaut-crac = "1.1.2" -managed-micronaut-data = "3.9.6" +managed-micronaut-data = "3.9.7" managed-micronaut-discovery = "3.2.0" managed-micronaut-elasticsearch = "4.3.0" managed-micronaut-email = "1.5.0" From d5f0fbcf1ae3f564983c692d8261a95c491b3d9f Mon Sep 17 00:00:00 2001 From: micronaut-build Date: Thu, 30 Mar 2023 17:45:40 +0000 Subject: [PATCH 639/743] [skip ci] Release v3.8.8 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index eefa1985436..01f2cca0f4d 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -projectVersion=3.8.8-SNAPSHOT +projectVersion=3.8.8 projectGroupId=io.micronaut title=Micronaut projectDesc=Natively Cloud Native From 4dd90d433521934dc7df00f1050ca2e6be45113d Mon Sep 17 00:00:00 2001 From: micronaut-build Date: Thu, 30 Mar 2023 17:58:51 +0000 Subject: [PATCH 640/743] Back to 3.8.9-SNAPSHOT --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 01f2cca0f4d..7ddadd8c695 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -projectVersion=3.8.8 +projectVersion=3.8.9-SNAPSHOT projectGroupId=io.micronaut title=Micronaut projectDesc=Natively Cloud Native From 2eb7b23b71dea09b162bd85573082fd9cf855771 Mon Sep 17 00:00:00 2001 From: Graeme Rocher Date: Fri, 31 Mar 2023 10:58:33 +0200 Subject: [PATCH 641/743] Cleanup expressions implementation and improve perf (#9027) --- .../aop/chain/DefaultInterceptorRegistry.java | 4 +- .../micronaut/aop/chain/InterceptorChain.java | 10 ++- .../aop/chain/MethodInterceptorChain.java | 9 --- .../processor/ScheduledMethodProcessor.java | 2 +- .../inject/writer/BeanDefinitionWriter.java | 19 ++++- .../core/annotation/AnnotationValue.java | 2 +- .../ast/groovy/InjectTransform.groovy | 7 -- .../BeanDefinitionInjectProcessor.java | 16 ---- .../beans/BeanDefinitionProcessor.kt | 17 ----- .../AbstractExecutableMethodsDefinition.java | 4 +- .../AbstractInitializableBeanDefinition.java | 75 +++++++------------ ...tInitializableBeanDefinitionReference.java | 2 +- ...able.java => BeanContextConfigurable.java} | 2 +- .../context/EnvironmentAwareArgument.java | 2 +- .../context/ExpressionsAwareArgument.java | 33 +++++--- .../EvaluatedAnnotationMetadata.java | 11 +-- .../annotation/EvaluatedAnnotationValue.java | 6 +- .../EvaluatedConvertibleValuesMap.java | 2 +- 18 files changed, 95 insertions(+), 128 deletions(-) rename inject/src/main/java/io/micronaut/context/{ContextConfigurable.java => BeanContextConfigurable.java} (95%) diff --git a/aop/src/main/java/io/micronaut/aop/chain/DefaultInterceptorRegistry.java b/aop/src/main/java/io/micronaut/aop/chain/DefaultInterceptorRegistry.java index f41e2995df6..04db3e7989d 100644 --- a/aop/src/main/java/io/micronaut/aop/chain/DefaultInterceptorRegistry.java +++ b/aop/src/main/java/io/micronaut/aop/chain/DefaultInterceptorRegistry.java @@ -34,7 +34,7 @@ import io.micronaut.core.type.Argument; import io.micronaut.core.type.Executable; import io.micronaut.inject.ExecutableMethod; -import io.micronaut.context.ContextConfigurable; +import io.micronaut.context.BeanContextConfigurable; import io.micronaut.inject.qualifiers.InterceptorBindingQualifier; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -239,7 +239,7 @@ public Interceptor[] resolveConstructorInterceptors( } private static void instrumentAnnotationMetadata(BeanContext beanContext, Object method) { - if (method instanceof ContextConfigurable ctxConfigurable) { + if (method instanceof BeanContextConfigurable ctxConfigurable) { ctxConfigurable.configure(beanContext); } if (beanContext instanceof ApplicationContext applicationContext && method instanceof EnvironmentConfigurable environmentConfigurable) { diff --git a/aop/src/main/java/io/micronaut/aop/chain/InterceptorChain.java b/aop/src/main/java/io/micronaut/aop/chain/InterceptorChain.java index 9baddba5bee..204bc7fc4dd 100644 --- a/aop/src/main/java/io/micronaut/aop/chain/InterceptorChain.java +++ b/aop/src/main/java/io/micronaut/aop/chain/InterceptorChain.java @@ -27,6 +27,7 @@ import io.micronaut.core.type.Argument; import io.micronaut.core.util.ArrayUtils; import io.micronaut.inject.ExecutableMethod; +import io.micronaut.inject.annotation.EvaluatedAnnotationMetadata; import java.lang.annotation.Annotation; import java.util.*; @@ -47,6 +48,7 @@ public class InterceptorChain extends AbstractInterceptorChain imple protected final B target; protected final ExecutableMethod executionHandle; + private final AnnotationMetadata annotationMetadata; /** * Constructor. @@ -66,11 +68,17 @@ public InterceptorChain(Interceptor[] interceptors, } this.target = target; this.executionHandle = method; + AnnotationMetadata metadata = executionHandle.getAnnotationMetadata(); + if (originalParameters.length > 0 && metadata instanceof EvaluatedAnnotationMetadata eam) { + this.annotationMetadata = eam.withArguments(originalParameters); + } else { + this.annotationMetadata = metadata; + } } @Override public AnnotationMetadata getAnnotationMetadata() { - return executionHandle.getAnnotationMetadata(); + return annotationMetadata; } @Override diff --git a/aop/src/main/java/io/micronaut/aop/chain/MethodInterceptorChain.java b/aop/src/main/java/io/micronaut/aop/chain/MethodInterceptorChain.java index 02c50aad48d..cd792b0a919 100644 --- a/aop/src/main/java/io/micronaut/aop/chain/MethodInterceptorChain.java +++ b/aop/src/main/java/io/micronaut/aop/chain/MethodInterceptorChain.java @@ -31,7 +31,6 @@ import io.micronaut.core.util.ArrayUtils; import io.micronaut.inject.BeanDefinition; import io.micronaut.inject.ExecutableMethod; -import io.micronaut.inject.annotation.EvaluatedAnnotationMetadata; import io.micronaut.inject.qualifiers.Qualifiers; import java.lang.reflect.Method; @@ -98,14 +97,6 @@ public MethodInterceptorChain(Interceptor[] interceptors, T target, Execut this.kind = null; } - @Override - public AnnotationMetadata getAnnotationMetadata() { - if (executionHandle.getAnnotationMetadata() instanceof EvaluatedAnnotationMetadata eam) { - return eam.withArguments(originalParameters); - } - return executionHandle.getAnnotationMetadata(); - } - @Override @NonNull public InterceptorKind getKind() { diff --git a/context/src/main/java/io/micronaut/scheduling/processor/ScheduledMethodProcessor.java b/context/src/main/java/io/micronaut/scheduling/processor/ScheduledMethodProcessor.java index 372f6b8447e..94d664543be 100644 --- a/context/src/main/java/io/micronaut/scheduling/processor/ScheduledMethodProcessor.java +++ b/context/src/main/java/io/micronaut/scheduling/processor/ScheduledMethodProcessor.java @@ -124,7 +124,7 @@ public void process(BeanDefinition beanDefinition, ExecutableMethod met ExecutableBeanContextBinder binder = new DefaultExecutableBeanContextBinder(); BoundExecutable boundExecutable = binder.bind(method, beanContext); Object bean = beanContext.getBean(beanType, declaredQualifier); - AnnotationValue finalAnnotationValue = scheduledAnnotation; + AnnotationValue finalAnnotationValue = scheduledAnnotation; if (finalAnnotationValue instanceof EvaluatedAnnotationValue evaluated) { finalAnnotationValue = evaluated.withArguments(boundExecutable.getBoundArguments()); } diff --git a/core-processor/src/main/java/io/micronaut/inject/writer/BeanDefinitionWriter.java b/core-processor/src/main/java/io/micronaut/inject/writer/BeanDefinitionWriter.java index 64d6b3185ef..91fc91a1073 100644 --- a/core-processor/src/main/java/io/micronaut/inject/writer/BeanDefinitionWriter.java +++ b/core-processor/src/main/java/io/micronaut/inject/writer/BeanDefinitionWriter.java @@ -64,6 +64,7 @@ import io.micronaut.core.util.CollectionUtils; import io.micronaut.core.util.StringUtils; import io.micronaut.core.util.Toggleable; +import io.micronaut.expressions.EvaluatedExpressionWriter; import io.micronaut.expressions.context.ExpressionCompilationContext; import io.micronaut.expressions.context.ExpressionWithContext; import io.micronaut.expressions.context.DefaultExpressionCompilationContextFactory; @@ -439,7 +440,8 @@ public class BeanDefinitionWriter extends AbstractClassFileWriter implements Bea boolean.class, // isPrimary boolean.class, // isConfigurationProperties boolean.class, // isContainerType - boolean.class // requiresMethodProcessing + boolean.class, // requiresMethodProcessing, + boolean.class // hasEvaluatedExpressions )); private static final String FIELD_CONSTRUCTOR = "$CONSTRUCTOR"; @@ -1418,10 +1420,22 @@ public void accept(ClassWriterOutputVisitor visitor) throws IOException { throw e; } } + writeEvaluatedExpressions(visitor); out.write(toByteArray()); } } + private void writeEvaluatedExpressions(ClassWriterOutputVisitor visitor) throws IOException { + for (ExpressionWithContext expressionMetadata: getEvaluatedExpressions()) { + EvaluatedExpressionWriter expressionWriter = new EvaluatedExpressionWriter( + expressionMetadata, + visitorContext, + getOriginatingElement()); + + expressionWriter.accept(visitor); + } + } + @Override public void visitSetterValue( TypedElement declaringType, @@ -4193,6 +4207,9 @@ private void pushPrecalculatedInfo(GeneratorAdapter protectedConstructor, Annota // 8: requiresMethodProcessing protectedConstructor.push(preprocessMethods); + // 9: hasEvaluatedExpressions + protectedConstructor.push(!getEvaluatedExpressions().isEmpty()); + protectedConstructor.invokeConstructor(PRECALCULATED_INFO, PRECALCULATED_INFO_CONSTRUCTOR); } diff --git a/core/src/main/java/io/micronaut/core/annotation/AnnotationValue.java b/core/src/main/java/io/micronaut/core/annotation/AnnotationValue.java index b88b601c811..e1968d947c4 100644 --- a/core/src/main/java/io/micronaut/core/annotation/AnnotationValue.java +++ b/core/src/main/java/io/micronaut/core/annotation/AnnotationValue.java @@ -178,7 +178,7 @@ public AnnotationValue(AnnotationValue target, this.convertibleValues = convertibleValues; this.valueMapper = valueMapper; this.retentionPolicy = RetentionPolicy.RUNTIME; - this.stereotypes = null; + this.stereotypes = target.stereotypes; } /** diff --git a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/InjectTransform.groovy b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/InjectTransform.groovy index 79bb85c4d40..f08d2bf4f63 100644 --- a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/InjectTransform.groovy +++ b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/InjectTransform.groovy @@ -25,7 +25,6 @@ import io.micronaut.ast.groovy.visitor.GroovyPackageElement import io.micronaut.ast.groovy.visitor.GroovyVisitorContext import io.micronaut.context.annotation.Configuration import io.micronaut.context.annotation.Context -import io.micronaut.expressions.context.ExpressionWithContext import io.micronaut.inject.processing.BeanDefinitionCreator import io.micronaut.inject.processing.BeanDefinitionCreatorFactory import io.micronaut.inject.processing.ProcessingException @@ -40,7 +39,6 @@ import org.codehaus.groovy.ast.ClassNode import org.codehaus.groovy.ast.InnerClassNode import org.codehaus.groovy.ast.ModuleNode import org.codehaus.groovy.ast.PackageNode -import io.micronaut.expressions.EvaluatedExpressionWriter import org.codehaus.groovy.control.CompilationUnit import org.codehaus.groovy.control.CompilePhase import org.codehaus.groovy.control.SourceUnit @@ -128,11 +126,6 @@ class InjectTransform implements ASTTransformation, CompilationUnitAware { String beanTypeName = beanDefWriter.beanTypeName AnnotatedNode beanClassNode = entry.key try { - for (ExpressionWithContext expression: beanDefWriter.evaluatedExpressions) { - new EvaluatedExpressionWriter(expression, new GroovyVisitorContext(source, unit), beanDefWriter.originatingElement) - .accept(outputVisitor); - } - BeanDefinitionReferenceWriter beanReferenceWriter = new BeanDefinitionReferenceWriter(beanDefWriter) beanReferenceWriter.setRequiresMethodProcessing(beanDefWriter.requiresMethodProcessing()) beanReferenceWriter.setContextScope(beanDefWriter.getAnnotationMetadata().hasDeclaredAnnotation(Context)) diff --git a/inject-java/src/main/java/io/micronaut/annotation/processing/BeanDefinitionInjectProcessor.java b/inject-java/src/main/java/io/micronaut/annotation/processing/BeanDefinitionInjectProcessor.java index a3f5880abac..e88aba3522a 100644 --- a/inject-java/src/main/java/io/micronaut/annotation/processing/BeanDefinitionInjectProcessor.java +++ b/inject-java/src/main/java/io/micronaut/annotation/processing/BeanDefinitionInjectProcessor.java @@ -27,8 +27,6 @@ import io.micronaut.core.annotation.Vetoed; import io.micronaut.core.naming.NameUtils; import io.micronaut.core.util.CollectionUtils; -import io.micronaut.expressions.EvaluatedExpressionWriter; -import io.micronaut.expressions.context.ExpressionWithContext; import io.micronaut.inject.ast.annotation.ElementAnnotationMetadataFactory; import io.micronaut.inject.processing.BeanDefinitionCreator; import io.micronaut.inject.processing.BeanDefinitionCreatorFactory; @@ -284,9 +282,6 @@ private void processBeanDefinitions(BeanDefinitionVisitor beanDefinitionWriter) beanDefinitionWriter.visitBeanDefinitionEnd(); if (beanDefinitionWriter.isEnabled()) { beanDefinitionWriter.accept(classWriterOutputVisitor); - - processEvaluatedExpressions(beanDefinitionWriter); - BeanDefinitionReferenceWriter beanDefinitionReferenceWriter = new BeanDefinitionReferenceWriter(beanDefinitionWriter); beanDefinitionReferenceWriter.setRequiresMethodProcessing(beanDefinitionWriter.requiresMethodProcessing()); @@ -305,15 +300,4 @@ private void processBeanDefinitions(BeanDefinitionVisitor beanDefinitionWriter) } } - private void processEvaluatedExpressions(BeanDefinitionVisitor beanDefinitionWriter) throws IOException { - for (ExpressionWithContext expressionMetadata: beanDefinitionWriter.getEvaluatedExpressions()) { - EvaluatedExpressionWriter expressionWriter = new EvaluatedExpressionWriter( - expressionMetadata, - javaVisitorContext, - beanDefinitionWriter.getOriginatingElement()); - - expressionWriter.accept(classWriterOutputVisitor); - } - } - } diff --git a/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/beans/BeanDefinitionProcessor.kt b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/beans/BeanDefinitionProcessor.kt index b371110638d..72aa8554478 100644 --- a/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/beans/BeanDefinitionProcessor.kt +++ b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/beans/BeanDefinitionProcessor.kt @@ -21,7 +21,6 @@ import com.google.devtools.ksp.processing.SymbolProcessorEnvironment import com.google.devtools.ksp.symbol.* import io.micronaut.context.annotation.Context import io.micronaut.core.annotation.Generated -import io.micronaut.expressions.EvaluatedExpressionWriter import io.micronaut.inject.processing.BeanDefinitionCreator import io.micronaut.inject.processing.BeanDefinitionCreatorFactory import io.micronaut.inject.processing.ProcessingException @@ -143,7 +142,6 @@ internal class BeanDefinitionProcessor(private val environment: SymbolProcessorE beanDefinitionWriter.visitBeanDefinitionEnd() if (beanDefinitionWriter.isEnabled) { beanDefinitionWriter.accept(outputVisitor) - processEvaluatedExpressions(beanDefinitionWriter, visitorContext, outputVisitor) val beanDefinitionReferenceWriter = BeanDefinitionReferenceWriter(beanDefinitionWriter) beanDefinitionReferenceWriter.setRequiresMethodProcessing(beanDefinitionWriter.requiresMethodProcessing()) val className = beanDefinitionReferenceWriter.beanDefinitionQualifiedClassName @@ -160,19 +158,4 @@ internal class BeanDefinitionProcessor(private val environment: SymbolProcessorE } } - private fun processEvaluatedExpressions( - beanDefinitionWriter: BeanDefinitionVisitor, - visitorContext: KotlinVisitorContext, - outputVisitor: KotlinOutputVisitor - ) { - for (expressionMetadata in beanDefinitionWriter.evaluatedExpressions) { - val expressionWriter = EvaluatedExpressionWriter( - expressionMetadata, - visitorContext, - beanDefinitionWriter.originatingElement - ) - expressionWriter.accept(outputVisitor) - } - } - } diff --git a/inject/src/main/java/io/micronaut/context/AbstractExecutableMethodsDefinition.java b/inject/src/main/java/io/micronaut/context/AbstractExecutableMethodsDefinition.java index ecb96501661..ffa4f0b091a 100644 --- a/inject/src/main/java/io/micronaut/context/AbstractExecutableMethodsDefinition.java +++ b/inject/src/main/java/io/micronaut/context/AbstractExecutableMethodsDefinition.java @@ -50,7 +50,7 @@ * @since 3.0 */ @Internal -public abstract class AbstractExecutableMethodsDefinition implements ExecutableMethodsDefinition, EnvironmentConfigurable, ContextConfigurable { +public abstract class AbstractExecutableMethodsDefinition implements ExecutableMethodsDefinition, EnvironmentConfigurable, BeanContextConfigurable { private final MethodReference[] methodsReferences; private final DispatchedExecutableMethod[] executableMethods; @@ -328,7 +328,7 @@ public String toString() { */ private static final class DispatchedExecutableMethod implements ExecutableMethod, EnvironmentConfigurable, - ContextConfigurable { + BeanContextConfigurable { private final AbstractExecutableMethodsDefinition dispatcher; private final int index; diff --git a/inject/src/main/java/io/micronaut/context/AbstractInitializableBeanDefinition.java b/inject/src/main/java/io/micronaut/context/AbstractInitializableBeanDefinition.java index 7d152d88365..b7b1894a82a 100644 --- a/inject/src/main/java/io/micronaut/context/AbstractInitializableBeanDefinition.java +++ b/inject/src/main/java/io/micronaut/context/AbstractInitializableBeanDefinition.java @@ -105,7 +105,7 @@ */ @Internal public abstract class AbstractInitializableBeanDefinition extends AbstractBeanContextConditional - implements InstantiatableBeanDefinition, InjectableBeanDefinition, EnvironmentConfigurable, ContextConfigurable { + implements InstantiatableBeanDefinition, InjectableBeanDefinition, EnvironmentConfigurable, BeanContextConfigurable { private static final Logger LOG = LoggerFactory.getLogger(AbstractInitializableBeanDefinition.class); private final Class type; @@ -275,7 +275,7 @@ public final boolean hasPropertyExpressions() { @Override public boolean hasEvaluatedExpressions() { - return getAnnotationMetadata().hasEvaluatedExpressions(); + return precalculatedInfo.hasEvaluatedExpressions(); } @Override @@ -650,7 +650,7 @@ public final void configure(Environment environment) { @Override public void configure(BeanContext beanContext) { - if (beanContext == null) { + if (beanContext == null || !hasEvaluatedExpressions()) { return; } @@ -734,7 +734,7 @@ public void configure(BeanContext beanContext) { } } - if (executableMethodsDefinition instanceof ContextConfigurable ctxConfigurable) { + if (executableMethodsDefinition instanceof BeanContextConfigurable ctxConfigurable) { ctxConfigurable.configure(beanContext); } } @@ -2137,21 +2137,22 @@ private boolean resolveContainsValue(BeanResolutionContext resolutionContext, Be } private Object resolveValue(BeanResolutionContext resolutionContext, BeanContext context, AnnotationMetadata parentAnnotationMetadata, Argument argument, Qualifier qualifier) { + if (!(context instanceof PropertyResolver)) { + throw new DependencyInjectionException(resolutionContext, "@Value requires a BeanContext that implements PropertyResolver"); + } AnnotationMetadata argumentAnnotationMetadata = argument.getAnnotationMetadata(); - if (argumentAnnotationMetadata instanceof EvaluatedAnnotationMetadata eam) { - eam.configure(context); - eam.setBeanDefinition(this); - AnnotationValue annotation = eam.getAnnotation(Value.class); - if (annotation.hasEvaluatedExpressions()) { - Optional value = annotation.getValue(argument); - return value.orElse(null); + if (argumentAnnotationMetadata.hasEvaluatedExpressions()) { + boolean isOptional = argument.isOptional(); + if (isOptional) { + Argument t = isOptional ? argument.getFirstTypeVariable().orElse(Argument.OBJECT_ARGUMENT) : argument; + Object v = argumentAnnotationMetadata.getValue(Value.class, t).orElse(null); + return Optional.ofNullable(v); + } else { + return argumentAnnotationMetadata.getValue(Value.class, argument).orElse(null); } } - if (!(context instanceof PropertyResolver)) { - throw new DependencyInjectionException(resolutionContext, "@Value requires a BeanContext that implements PropertyResolver"); - } - String valueAnnVal = argument.getAnnotationMetadata().stringValue(Value.class).orElse(null); + String valueAnnVal = argumentAnnotationMetadata.stringValue(Value.class).orElse(null); Argument argumentType; boolean isCollection = false; final boolean wrapperType = argument.isWrapperType(); @@ -2173,7 +2174,7 @@ private Object resolveValue(BeanResolutionContext resolutionContext, BeanContext return resolutionContext.getBean(argumentType, qualifier); } } else { - String valString = resolvePropertyValueName(resolutionContext, parentAnnotationMetadata, argument.getAnnotationMetadata(), valueAnnVal); + String valString = resolvePropertyValueName(resolutionContext, parentAnnotationMetadata, argumentAnnotationMetadata, valueAnnVal); ArgumentConversionContext conversionContext = wrapperType ? ConversionContext.of(argumentType) : ConversionContext.of(argument); Optional value = resolveValue((ApplicationContext) context, conversionContext, valueAnnVal != null, valString); if (argument.isOptional()) { @@ -2204,7 +2205,7 @@ private Object resolveValue(BeanResolutionContext resolutionContext, BeanContext if (argument.isDeclaredNullable()) { return null; } - return argument.getAnnotationMetadata().getValue(Bindable.class, "defaultValue", argument) + return argumentAnnotationMetadata.getValue(Bindable.class, "defaultValue", argument) .orElseThrow(() -> DependencyInjectionException.missingProperty(resolutionContext, conversionContext, valString)); } } @@ -2473,12 +2474,6 @@ private Argument resolveArgument(BeanContext context, int argIndex, Argum } private Argument resolveArgument(BeanContext context, Argument argument) { - if (argument instanceof DefaultArgument) { - if (argument.getAnnotationMetadata().hasPropertyExpressions()) { - argument = new EnvironmentAwareArgument<>((DefaultArgument) argument); - instrumentAnnotationMetadata(context, argument); - } - } return ExpressionsAwareArgument.wrapIfNecessary(argument, context, this); } @@ -2537,33 +2532,12 @@ private > K coerceCollectionToCorrectType(Class co } } - private void instrumentAnnotationMetadata(BeanContext context, Object object) { - if (object instanceof final EnvironmentConfigurable ec && context instanceof ApplicationContext ac) { - if (ec.hasPropertyExpressions()) { - ec.configure(ac.getEnvironment()); - } - } - } - private Object getExpressionValueForArgument(Argument argument) { + Argument t = argument.isOptional() ? argument.getFirstTypeVariable().orElse(Argument.OBJECT_ARGUMENT) : argument; Optional expressionValue = argument.getAnnotationMetadata() - .getValue(Value.class, argument.getType()); - - if (argument.isOptional()) { - if (expressionValue.isEmpty()) { - return expressionValue; - } else { - Object convertedOptional = expressionValue.get(); - if (convertedOptional instanceof Optional) { - return convertedOptional; - } else { - return expressionValue; - } - } - } else { - return expressionValue.orElse(null); - } + .getValue(Value.class, t); + return expressionValue.orElse(null); } @Internal @@ -2576,9 +2550,12 @@ public record PrecalculatedInfo( boolean isPrimary, boolean isConfigurationProperties, boolean isContainerType, - boolean requiresMethodProcessing + boolean requiresMethodProcessing, + boolean hasEvaluatedExpressions ) { - + public PrecalculatedInfo(Optional scope, boolean isAbstract, boolean isIterable, boolean isSingleton, boolean isPrimary, boolean isConfigurationProperties, boolean isContainerType, boolean requiresMethodProcessing) { + this(scope, isAbstract, isIterable, isSingleton, isPrimary, isConfigurationProperties, isContainerType, requiresMethodProcessing, false); + } } /** diff --git a/inject/src/main/java/io/micronaut/context/AbstractInitializableBeanDefinitionReference.java b/inject/src/main/java/io/micronaut/context/AbstractInitializableBeanDefinitionReference.java index f4ec216943e..800a9ec703d 100644 --- a/inject/src/main/java/io/micronaut/context/AbstractInitializableBeanDefinitionReference.java +++ b/inject/src/main/java/io/micronaut/context/AbstractInitializableBeanDefinitionReference.java @@ -183,7 +183,7 @@ public BeanDefinition load(BeanContext context) { } else if (context instanceof ApplicationContext applicationContext && definition instanceof EnvironmentConfigurable environmentConfigurable) { environmentConfigurable.configure(applicationContext.getEnvironment()); } - if (definition instanceof ContextConfigurable ctxConfigurable) { + if (definition instanceof BeanContextConfigurable ctxConfigurable) { ctxConfigurable.configure(context); } return definition; diff --git a/inject/src/main/java/io/micronaut/context/ContextConfigurable.java b/inject/src/main/java/io/micronaut/context/BeanContextConfigurable.java similarity index 95% rename from inject/src/main/java/io/micronaut/context/ContextConfigurable.java rename to inject/src/main/java/io/micronaut/context/BeanContextConfigurable.java index dcc23f5dd5e..bc1e681b6c4 100644 --- a/inject/src/main/java/io/micronaut/context/ContextConfigurable.java +++ b/inject/src/main/java/io/micronaut/context/BeanContextConfigurable.java @@ -21,7 +21,7 @@ * @author Sergey Gavirlov * @since 4.0 */ -public interface ContextConfigurable { +public interface BeanContextConfigurable { /** * Configure the component for the given bean context. * diff --git a/inject/src/main/java/io/micronaut/context/EnvironmentAwareArgument.java b/inject/src/main/java/io/micronaut/context/EnvironmentAwareArgument.java index a5382970d27..ba940703b4e 100644 --- a/inject/src/main/java/io/micronaut/context/EnvironmentAwareArgument.java +++ b/inject/src/main/java/io/micronaut/context/EnvironmentAwareArgument.java @@ -33,7 +33,7 @@ * @param The argument type */ @Internal -class EnvironmentAwareArgument extends DefaultArgument implements EnvironmentConfigurable { +final class EnvironmentAwareArgument extends DefaultArgument implements EnvironmentConfigurable { private final AnnotationMetadata annotationMetadata; private Environment environment; diff --git a/inject/src/main/java/io/micronaut/context/ExpressionsAwareArgument.java b/inject/src/main/java/io/micronaut/context/ExpressionsAwareArgument.java index 5c31639ab22..0cdd31aeed9 100644 --- a/inject/src/main/java/io/micronaut/context/ExpressionsAwareArgument.java +++ b/inject/src/main/java/io/micronaut/context/ExpressionsAwareArgument.java @@ -33,7 +33,7 @@ * @since 4.0 */ @Internal -final class ExpressionsAwareArgument extends DefaultArgument implements ContextConfigurable, +final class ExpressionsAwareArgument extends DefaultArgument implements BeanContextConfigurable, BeanDefinitionAware { private final EvaluatedAnnotationMetadata annotationMetadata; @@ -52,22 +52,33 @@ public static Argument wrapIfNecessary(Argument argument) { public static Argument wrapIfNecessary(Argument argument, @Nullable BeanContext beanContext, @Nullable BeanDefinition owningBean) { - if (argument == null) { + if (!(argument instanceof DefaultArgument)) { return null; } - AnnotationMetadata annotationMetadata = - EvaluatedAnnotationMetadata.wrapIfNecessary(argument.getAnnotationMetadata()); - if (annotationMetadata instanceof EvaluatedAnnotationMetadata evaluatedAnnotationMetadata) { - if (beanContext != null) { - evaluatedAnnotationMetadata.configure(beanContext); + AnnotationMetadata argumentAnnotationMetadata = argument.getAnnotationMetadata(); + if (argumentAnnotationMetadata.hasPropertyExpressions()) { + if (!(argument instanceof EnvironmentAwareArgument) && beanContext instanceof ApplicationContext ac) { + EnvironmentAwareArgument environmentAwareArgument = new EnvironmentAwareArgument<>((DefaultArgument) argument); + environmentAwareArgument.configure(ac.getEnvironment()); + return environmentAwareArgument; + } else { + return argument; } + } else if (argumentAnnotationMetadata.hasEvaluatedExpressions()) { + AnnotationMetadata annotationMetadata = + EvaluatedAnnotationMetadata.wrapIfNecessary(argumentAnnotationMetadata); + if (annotationMetadata instanceof EvaluatedAnnotationMetadata evaluatedAnnotationMetadata) { + if (beanContext != null) { + evaluatedAnnotationMetadata.configure(beanContext); + } - if (owningBean != null) { - evaluatedAnnotationMetadata.setBeanDefinition(owningBean); - } + if (owningBean != null) { + evaluatedAnnotationMetadata.setBeanDefinition(owningBean); + } - return new ExpressionsAwareArgument<>(argument, evaluatedAnnotationMetadata); + return new ExpressionsAwareArgument<>(argument, evaluatedAnnotationMetadata); + } } return argument; } diff --git a/inject/src/main/java/io/micronaut/inject/annotation/EvaluatedAnnotationMetadata.java b/inject/src/main/java/io/micronaut/inject/annotation/EvaluatedAnnotationMetadata.java index efecbfec77f..9edcfe0f913 100644 --- a/inject/src/main/java/io/micronaut/inject/annotation/EvaluatedAnnotationMetadata.java +++ b/inject/src/main/java/io/micronaut/inject/annotation/EvaluatedAnnotationMetadata.java @@ -17,12 +17,12 @@ import io.micronaut.context.BeanContext; import io.micronaut.context.BeanDefinitionAware; -import io.micronaut.context.ContextConfigurable; +import io.micronaut.context.BeanContextConfigurable; import io.micronaut.context.expressions.ConfigurableExpressionEvaluationContext; import io.micronaut.context.expressions.DefaultExpressionEvaluationContext; import io.micronaut.core.annotation.AnnotationMetadata; import io.micronaut.core.annotation.AnnotationValue; -import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.Experimental; import io.micronaut.inject.BeanDefinition; import java.lang.annotation.Annotation; @@ -34,8 +34,8 @@ * @author Sergey Gavrilov * @since 4.0 */ -@Internal -public final class EvaluatedAnnotationMetadata extends MappingAnnotationMetadataDelegate implements ContextConfigurable, BeanDefinitionAware { +@Experimental +public final class EvaluatedAnnotationMetadata extends MappingAnnotationMetadataDelegate implements BeanContextConfigurable, BeanDefinitionAware { private final AnnotationMetadata delegateAnnotationMetadata; @@ -56,7 +56,8 @@ private EvaluatedAnnotationMetadata(AnnotationMetadata targetMetadata, public EvaluatedAnnotationMetadata withArguments(Object[] args) { return new EvaluatedAnnotationMetadata( delegateAnnotationMetadata, - evaluationContext.setArguments(args)); + evaluationContext.setArguments(args) + ); } @Override diff --git a/inject/src/main/java/io/micronaut/inject/annotation/EvaluatedAnnotationValue.java b/inject/src/main/java/io/micronaut/inject/annotation/EvaluatedAnnotationValue.java index 8e44eaf225b..89387ea52e9 100644 --- a/inject/src/main/java/io/micronaut/inject/annotation/EvaluatedAnnotationValue.java +++ b/inject/src/main/java/io/micronaut/inject/annotation/EvaluatedAnnotationValue.java @@ -17,6 +17,7 @@ import io.micronaut.context.expressions.ConfigurableExpressionEvaluationContext; import io.micronaut.core.annotation.AnnotationValue; +import io.micronaut.core.annotation.Experimental; import io.micronaut.core.expressions.EvaluatedExpression; import java.lang.annotation.Annotation; @@ -27,11 +28,12 @@ * @param The annotation * @since 4.0.0 */ -public class EvaluatedAnnotationValue extends AnnotationValue { +@Experimental +public final class EvaluatedAnnotationValue extends AnnotationValue { private final ConfigurableExpressionEvaluationContext evaluationContext; private final AnnotationValue annotationValue; - public EvaluatedAnnotationValue(AnnotationValue annotationValue, ConfigurableExpressionEvaluationContext evaluationContext) { + EvaluatedAnnotationValue(AnnotationValue annotationValue, ConfigurableExpressionEvaluationContext evaluationContext) { super( annotationValue, annotationValue.getDefaultValues(), diff --git a/inject/src/main/java/io/micronaut/inject/annotation/EvaluatedConvertibleValuesMap.java b/inject/src/main/java/io/micronaut/inject/annotation/EvaluatedConvertibleValuesMap.java index 7e0ad7f1147..420b82e74d3 100644 --- a/inject/src/main/java/io/micronaut/inject/annotation/EvaluatedConvertibleValuesMap.java +++ b/inject/src/main/java/io/micronaut/inject/annotation/EvaluatedConvertibleValuesMap.java @@ -37,7 +37,7 @@ * @author Sergey Gavrilov */ @Internal -public class EvaluatedConvertibleValuesMap implements ConvertibleValues { +final class EvaluatedConvertibleValuesMap implements ConvertibleValues { private final ExpressionEvaluationContext evaluationContext; private final ConvertibleValues delegateValues; From d65a60845ac311528c68e96f074fd2bfcb057a00 Mon Sep 17 00:00:00 2001 From: Denis Stepanov Date: Fri, 31 Mar 2023 05:29:57 -0600 Subject: [PATCH 642/743] Use `Map.of` instead of custom implementation (#9032) --- .../server/stack/FullHttpStackBenchmark.java | 3 +- .../annotation/AnnotationMetadataWriter.java | 100 +++++++++-------- .../writer/AbstractClassFileWriter.java | 106 ++++++++++++------ .../AbstractAnnotationMetadata.java | 34 ------ .../annotation/DefaultAnnotationMetadata.java | 92 +++++++++------ 5 files changed, 183 insertions(+), 152 deletions(-) diff --git a/benchmarks/src/jmh/java/io/micronaut/http/server/stack/FullHttpStackBenchmark.java b/benchmarks/src/jmh/java/io/micronaut/http/server/stack/FullHttpStackBenchmark.java index ae33a657cd8..ee684837918 100644 --- a/benchmarks/src/jmh/java/io/micronaut/http/server/stack/FullHttpStackBenchmark.java +++ b/benchmarks/src/jmh/java/io/micronaut/http/server/stack/FullHttpStackBenchmark.java @@ -28,7 +28,6 @@ import org.openjdk.jmh.annotations.Setup; import org.openjdk.jmh.annotations.State; import org.openjdk.jmh.annotations.TearDown; -import org.openjdk.jmh.profile.AsyncProfiler; import org.openjdk.jmh.runner.Runner; import org.openjdk.jmh.runner.options.Options; import org.openjdk.jmh.runner.options.OptionsBuilder; @@ -67,7 +66,7 @@ public static void main(String[] args) throws Exception { .measurementIterations(30) .mode(Mode.AverageTime) .timeUnit(TimeUnit.NANOSECONDS) - .addProfiler(AsyncProfiler.class, "libPath=/home/yawkat/bin/async-profiler-2.9-linux-x64/build/libasyncProfiler.so;output=flamegraph") +// .addProfiler(AsyncProfiler.class, "libPath=/home/yawkat/bin/async-profiler-2.9-linux-x64/build/libasyncProfiler.so;output=flamegraph") .forks(1) .jvmArgsAppend("-Djmh.executor=CUSTOM", "-Djmh.executor.class=" + JmhFastThreadLocalExecutor.class.getName()) .build(); diff --git a/core-processor/src/main/java/io/micronaut/inject/annotation/AnnotationMetadataWriter.java b/core-processor/src/main/java/io/micronaut/inject/annotation/AnnotationMetadataWriter.java index de184a9aaaf..80ec3721bf9 100644 --- a/core-processor/src/main/java/io/micronaut/inject/annotation/AnnotationMetadataWriter.java +++ b/core-processor/src/main/java/io/micronaut/inject/annotation/AnnotationMetadataWriter.java @@ -15,18 +15,17 @@ */ package io.micronaut.inject.annotation; +import io.micronaut.context.expressions.AbstractEvaluatedExpression; import io.micronaut.core.annotation.AnnotationClassValue; import io.micronaut.core.annotation.AnnotationMetadata; import io.micronaut.core.annotation.AnnotationMetadataDelegate; -import io.micronaut.core.annotation.AnnotationUtil; -import io.micronaut.core.expressions.EvaluatedExpressionReference; import io.micronaut.core.annotation.Internal; import io.micronaut.core.annotation.UsedByGeneratedCode; +import io.micronaut.core.expressions.EvaluatedExpressionReference; import io.micronaut.core.reflect.ReflectionUtils; import io.micronaut.core.util.ArrayUtils; import io.micronaut.core.util.CollectionUtils; import io.micronaut.inject.ast.ClassElement; -import io.micronaut.context.expressions.AbstractEvaluatedExpression; import io.micronaut.inject.writer.AbstractAnnotationMetadataWriter; import io.micronaut.inject.writer.AbstractClassFileWriter; import io.micronaut.inject.writer.ClassGenerationException; @@ -42,16 +41,13 @@ import java.io.IOException; import java.io.OutputStream; import java.lang.reflect.Array; -import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; -import java.util.Objects; import java.util.Set; -import java.util.stream.Collectors; /** * Responsible for writing class files that are instances of {@link AnnotationMetadata}. @@ -66,14 +62,6 @@ public class AnnotationMetadataWriter extends AbstractClassFileWriter { private static final Type TYPE_DEFAULT_ANNOTATION_METADATA_HIERARCHY = Type.getType(AnnotationMetadataHierarchy.class); private static final Type TYPE_ANNOTATION_CLASS_VALUE = Type.getType(AnnotationClassValue.class); - private static final org.objectweb.asm.commons.Method METHOD_LIST_OF = Method.getMethod( - ReflectionUtils.getRequiredInternalMethod( - AnnotationUtil.class, - "internListOf", - Object[].class - ) - ); - private static final org.objectweb.asm.commons.Method METHOD_REGISTER_ANNOTATION_DEFAULTS = Method.getMethod( ReflectionUtils.getRequiredInternalMethod( DefaultAnnotationMetadata.class, @@ -163,10 +151,6 @@ public class AnnotationMetadataWriter extends AbstractClassFileWriter { Object.class )); - private static final Type ANNOTATION_UTIL_TYPE = Type.getType(AnnotationUtil.class); - private static final Type LIST_TYPE = Type.getType(List.class); - private static final String EMPTY_LIST = "EMPTY_LIST"; - private static final String LOAD_CLASS_PREFIX = "$micronaut_load_class_value_"; private final String className; @@ -436,27 +420,6 @@ public static void writeAnnotationDefaults( } } - private static void pushListOfString(GeneratorAdapter methodVisitor, List names) { - if (names != null) { - names = names.stream().filter(Objects::nonNull).collect(Collectors.toList()); - } - if (names == null || names.isEmpty()) { - methodVisitor.getStatic(Type.getType(Collections.class), EMPTY_LIST, LIST_TYPE); - return; - } - int totalSize = names.size(); - // start a new array - pushNewArray(methodVisitor, Object.class, totalSize); - int i = 0; - for (String name : names) { - // use the property name as the key - pushStoreStringInArray(methodVisitor, i++, totalSize, name); - // use the property type as the value - } - // invoke the AbstractBeanDefinition.createMap method - methodVisitor.invokeStatic(ANNOTATION_UTIL_TYPE, METHOD_LIST_OF); - } - private static void instantiateInternal( Type owningType, ClassWriter declaringClassWriter, @@ -569,8 +532,7 @@ private static void pushValue(Type declaringType, ClassVisitor declaringClassWri } } else if (value instanceof String) { methodVisitor.push(value.toString()); - } else if (value instanceof AnnotationClassValue) { - AnnotationClassValue acv = (AnnotationClassValue) value; + } else if (value instanceof AnnotationClassValue acv) { if (acv.isInstantiated()) { methodVisitor.visitTypeInsn(NEW, TYPE_ANNOTATION_CLASS_VALUE.getInternalName()); methodVisitor.visitInsn(DUP); @@ -600,11 +562,11 @@ private static void pushValue(Type declaringType, ClassVisitor declaringClassWri ); } } - } else if (value instanceof Collection) { - if (((Collection) value).isEmpty()) { + } else if (value instanceof Collection collection) { + if (collection.isEmpty()) { pushEmptyObjectsArray(methodVisitor); } else { - List array = Arrays.asList(((Collection) value).toArray()); + List array = CollectionUtils.iterableToList(collection); int len = array.size(); boolean first = true; Class arrayType = Object.class; @@ -657,8 +619,7 @@ private static void pushValue(Type declaringType, ClassVisitor declaringClassWri if (boxValue) { pushBoxPrimitiveIfNecessary(ReflectionUtils.getPrimitiveType(value.getClass()), methodVisitor); } - } else if (value instanceof io.micronaut.core.annotation.AnnotationValue) { - io.micronaut.core.annotation.AnnotationValue data = (io.micronaut.core.annotation.AnnotationValue) value; + } else if (value instanceof io.micronaut.core.annotation.AnnotationValue data) { String annotationName = data.getAnnotationName(); Map values = data.getValues(); Type annotationValueType = Type.getType(io.micronaut.core.annotation.AnnotationValue.class); @@ -698,13 +659,56 @@ private static void pushValue(Type declaringType, ClassVisitor declaringClassWri () -> pushValue(declaringType, declaringClassWriter, methodVisitor, v, defaultsStorage, loadTypeMethods, false)); } + } else { + throw new IllegalStateException(); } methodVisitor.invokeConstructor(type, CONSTRUCTOR_CONTEXT_EVALUATED_EXPRESSION); - } else { - methodVisitor.visitInsn(ACONST_NULL); + throw new IllegalStateException("Unsupported Map value: " + value + " " + value.getClass().getName()); + } + } + + public static boolean isSupportedMapValue(Object value) { + if (value == null) { + return false; + } else if (value instanceof Boolean) { + return true; + } else if (value instanceof String) { + return true; + } else if (value instanceof AnnotationClassValue) { + return true; + } else if (value instanceof Enum) { + return true; + } else if (value.getClass().isArray()) { + return true; + } else if (value instanceof Collection) { + return true; + } else if (value instanceof Map) { + return true; + } else if (value instanceof Long) { + return true; + } else if (value instanceof Double) { + return true; + } else if (value instanceof Float) { + return true; + } else if (value instanceof Byte) { + return true; + } else if (value instanceof Short) { + return true; + } else if (value instanceof Character) { + return true; + } else if (value instanceof Number) { + return true; + } else if (value instanceof io.micronaut.core.annotation.AnnotationValue) { + return true; + } else if (value instanceof EvaluatedExpressionReference) { + return true; + } else if (value instanceof Class) { + // The class should be added as AnnotationClassValue + return false; } + return false; } private static void pushEmptyObjectsArray(GeneratorAdapter methodVisitor) { diff --git a/core-processor/src/main/java/io/micronaut/inject/writer/AbstractClassFileWriter.java b/core-processor/src/main/java/io/micronaut/inject/writer/AbstractClassFileWriter.java index da4fd3e0274..2c71e9cbe55 100644 --- a/core-processor/src/main/java/io/micronaut/inject/writer/AbstractClassFileWriter.java +++ b/core-processor/src/main/java/io/micronaut/inject/writer/AbstractClassFileWriter.java @@ -16,7 +16,6 @@ package io.micronaut.inject.writer; import io.micronaut.core.annotation.AnnotationMetadata; -import io.micronaut.core.annotation.AnnotationUtil; import io.micronaut.core.annotation.Generated; import io.micronaut.core.annotation.Internal; import io.micronaut.core.annotation.NonNull; @@ -55,9 +54,9 @@ import java.util.AbstractMap; import java.util.Arrays; import java.util.Collection; -import java.util.Collections; import java.util.HashSet; import java.util.Iterator; +import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Objects; @@ -70,6 +69,8 @@ import java.util.regex.Pattern; import java.util.stream.Collectors; +import static io.micronaut.inject.annotation.AnnotationMetadataWriter.isSupportedMapValue; + /** * Abstract class that writes generated classes to disk and provides convenience methods for building classes. * @@ -157,34 +158,40 @@ public abstract class AbstractClassFileWriter implements Opcodes, OriginatingEle ) ); - private static final Type ANNOTATION_UTIL_TYPE = Type.getType(AnnotationUtil.class); private static final Type MAP_TYPE = Type.getType(Map.class); - private static final String EMPTY_MAP = "EMPTY_MAP"; + private static final Type LIST_TYPE = Type.getType(List.class); private static final org.objectweb.asm.commons.Method[] MAP_OF; + private static final org.objectweb.asm.commons.Method[] LIST_OF; private static final org.objectweb.asm.commons.Method MAP_BY_ARRAY; + private static final org.objectweb.asm.commons.Method MAP_ENTRY; + private static final org.objectweb.asm.commons.Method LIST_BY_ARRAY; static { MAP_OF = new Method[11]; - for (int i = 1; i < MAP_OF.length; i++) { + for (int i = 0; i < MAP_OF.length; i++) { Class[] mapArgs = new Class[i * 2]; for (int k = 0; k < i * 2; k += 2) { - mapArgs[k] = String.class; + mapArgs[k] = Object.class; mapArgs[k + 1] = Object.class; } - MAP_OF[i] = org.objectweb.asm.commons.Method.getMethod(ReflectionUtils.getRequiredMethod(AnnotationUtil.class, "mapOf", mapArgs)); + MAP_OF[i] = org.objectweb.asm.commons.Method.getMethod(ReflectionUtils.getRequiredMethod(Map.class, "of", mapArgs)); } - MAP_BY_ARRAY = org.objectweb.asm.commons.Method.getMethod(ReflectionUtils.getRequiredMethod(AnnotationUtil.class, "mapOf", Object[].class)); + MAP_ENTRY = org.objectweb.asm.commons.Method.getMethod(ReflectionUtils.getRequiredMethod(Map.class, "entry", Object.class, Object.class)); + MAP_BY_ARRAY = org.objectweb.asm.commons.Method.getMethod(ReflectionUtils.getRequiredMethod(Map.class, "ofEntries", Map.Entry[].class)); } - private static final org.objectweb.asm.commons.Method INTERN_MAP_OF_METHOD = org.objectweb.asm.commons.Method.getMethod( - ReflectionUtils.getRequiredInternalMethod( - AnnotationUtil.class, - "internMapOf", - String.class, - Object.class - ) - ); + static { + LIST_OF = new Method[11]; + for (int i = 0; i < LIST_OF.length; i++) { + Class[] listArgs = new Class[i]; + for (int k = 0; k < i; k += 1) { + listArgs[k] = Object.class; + } + LIST_OF[i] = org.objectweb.asm.commons.Method.getMethod(ReflectionUtils.getRequiredMethod(List.class, "of", listArgs)); + } + LIST_BY_ARRAY = org.objectweb.asm.commons.Method.getMethod(ReflectionUtils.getRequiredMethod(List.class, "of", Object[].class)); + } protected final OriginatingElements originatingElements; @@ -235,7 +242,7 @@ protected static void pushTypeArgumentElements( Map defaults, Map loadTypeMethods) { if (types == null || types.isEmpty()) { - generatorAdapter.visitInsn(ACONST_NULL); + pushNewArray(generatorAdapter, Argument.class, 0); return; } pushTypeArgumentElements(annotationMetadataWithDefaults, owningType, owningTypeWriter, generatorAdapter, declaringElementName, null, types, new HashSet<>(5), defaults, loadTypeMethods); @@ -1798,44 +1805,71 @@ ClassElement invokeMethod(@NonNull GeneratorAdapter generatorAdapter, @NonNull M return returnType; } - public static void pushStringMapOf(GeneratorAdapter generatorAdapter, Map annotationData, + public static void pushStringMapOf(GeneratorAdapter generatorAdapter, + Map annotationData, boolean skipEmpty, T empty, Consumer pushValue) { Set> entrySet = annotationData != null ? annotationData.entrySet() .stream() - .filter(e -> !skipEmpty || (e.getKey() != null && e.getValue() != null)) + .filter(e -> !skipEmpty || (e.getKey() != null && isSupportedMapValue(e.getValue()))) .map(e -> e.getValue() == null && empty != null ? new AbstractMap.SimpleEntry<>(e.getKey().toString(), empty) : new AbstractMap.SimpleEntry<>(e.getKey().toString(), e.getValue())) .collect(Collectors.toCollection(() -> new TreeSet<>(Map.Entry.comparingByKey()))) : null; if (entrySet == null || entrySet.isEmpty()) { - generatorAdapter.getStatic(Type.getType(Collections.class), EMPTY_MAP, MAP_TYPE); + invokeInterfaceStatic(generatorAdapter, MAP_TYPE, MAP_OF[0]); return; } - if (entrySet.size() == 1 && entrySet.iterator().next().getValue() == Collections.EMPTY_MAP) { + if (entrySet.size() < MAP_OF.length) { for (Map.Entry entry : entrySet) { generatorAdapter.push(entry.getKey()); pushValue.accept(entry.getValue()); } - generatorAdapter.invokeStatic(ANNOTATION_UTIL_TYPE, INTERN_MAP_OF_METHOD); - } else if (entrySet.size() < MAP_OF.length) { - for (Map.Entry entry : entrySet) { - generatorAdapter.push(entry.getKey()); - pushValue.accept(entry.getValue()); - } - generatorAdapter.invokeStatic(ANNOTATION_UTIL_TYPE, MAP_OF[entrySet.size()]); + invokeInterfaceStatic(generatorAdapter, MAP_TYPE, MAP_OF[entrySet.size()]); } else { - int totalSize = entrySet.size() * 2; + int totalSize = entrySet.size(); // start a new array - pushNewArray(generatorAdapter, Object.class, totalSize); + pushNewArray(generatorAdapter, Map.Entry.class, totalSize); int i = 0; for (Map.Entry entry : entrySet) { - // use the property name as the key - String memberName = entry.getKey().toString(); - pushStoreStringInArray(generatorAdapter, i++, totalSize, memberName); - // use the property type as the value - pushStoreInArray(generatorAdapter, i++, totalSize, () -> pushValue.accept(entry.getValue())); + + pushStoreInArray(generatorAdapter, i++, totalSize, () -> { + generatorAdapter.push(entry.getKey().toString()); + pushValue.accept(entry.getValue()); + invokeInterfaceStatic(generatorAdapter, MAP_TYPE, MAP_ENTRY); + }); + } - generatorAdapter.invokeStatic(ANNOTATION_UTIL_TYPE, MAP_BY_ARRAY); + invokeInterfaceStatic(generatorAdapter, MAP_TYPE, MAP_BY_ARRAY); } } + + public static void pushListOfString(GeneratorAdapter methodVisitor, List names) { + if (names != null) { + names = names.stream().filter(Objects::nonNull).toList(); + } + if (names == null || names.isEmpty()) { + invokeInterfaceStatic(methodVisitor, LIST_TYPE, LIST_OF[0]); + return; + } + if (names.size() < LIST_OF.length) { + for (String name : names) { + methodVisitor.push(name); + } + invokeInterfaceStatic(methodVisitor, LIST_TYPE, LIST_OF[names.size()]); + } else { + int totalSize = names.size(); + // start a new array + pushNewArray(methodVisitor, String.class, totalSize); + int i = 0; + for (String name : names) { + pushStoreStringInArray(methodVisitor, i++, totalSize, name); + } + invokeInterfaceStatic(methodVisitor, LIST_TYPE, LIST_BY_ARRAY); + } + } + + private static void invokeInterfaceStatic(GeneratorAdapter methodVisitor, Type type, org.objectweb.asm.commons.Method method) { + methodVisitor.visitMethodInsn(INVOKESTATIC, type.getInternalName(), method.getName(), method.getDescriptor(), true); + } + } diff --git a/inject/src/main/java/io/micronaut/inject/annotation/AbstractAnnotationMetadata.java b/inject/src/main/java/io/micronaut/inject/annotation/AbstractAnnotationMetadata.java index ae23a8fd4d7..e0be781fd50 100644 --- a/inject/src/main/java/io/micronaut/inject/annotation/AbstractAnnotationMetadata.java +++ b/inject/src/main/java/io/micronaut/inject/annotation/AbstractAnnotationMetadata.java @@ -175,40 +175,6 @@ public T synthesizeDeclared(@NonNull Class annotationC return annotations; } - /** - * Adds any annotation values found in the values map to the results. - * - * @param results The results - * @param values The values - */ - protected final void addAnnotationValuesFromData(List results, Map values) { - if (values != null) { - Object v = values.get(AnnotationMetadata.VALUE_MEMBER); - if (v instanceof io.micronaut.core.annotation.AnnotationValue[]) { - io.micronaut.core.annotation.AnnotationValue[] avs = (io.micronaut.core.annotation.AnnotationValue[]) v; - for (io.micronaut.core.annotation.AnnotationValue av : avs) { - addValuesToResults(results, av); - } - } else if (v instanceof Collection) { - Collection c = (Collection) v; - for (Object o : c) { - if (o instanceof io.micronaut.core.annotation.AnnotationValue) { - addValuesToResults(results, ((io.micronaut.core.annotation.AnnotationValue) o)); - } - } - } - } - } - - /** - * Adds a values instance to the results. - * - * @param results The results - * @param values The values - */ - protected void addValuesToResults(List results, io.micronaut.core.annotation.AnnotationValue values) { - results.add(values); - } private Annotation[] initializeAnnotations(Set names) { if (CollectionUtils.isNotEmpty(names)) { diff --git a/inject/src/main/java/io/micronaut/inject/annotation/DefaultAnnotationMetadata.java b/inject/src/main/java/io/micronaut/inject/annotation/DefaultAnnotationMetadata.java index 160086db905..7ea336d5f79 100644 --- a/inject/src/main/java/io/micronaut/inject/annotation/DefaultAnnotationMetadata.java +++ b/inject/src/main/java/io/micronaut/inject/annotation/DefaultAnnotationMetadata.java @@ -37,6 +37,7 @@ import java.lang.reflect.Array; import java.util.AbstractMap; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashMap; @@ -994,7 +995,7 @@ public Optional getValue(@NonNull String annotation, @NonNull String memb @Override public @NonNull List> getAnnotationValuesByType(@Nullable Class annotationType) { if (annotationType == null) { - return Collections.emptyList(); + return List.of(); } final String annotationTypeName = annotationType.getName(); List> results = annotationValuesByType.get(annotationTypeName); @@ -1005,11 +1006,11 @@ public Optional getValue(@NonNull String annotation, @NonNull String memb } else if (allAnnotations != null) { final Map values = allAnnotations.get(annotationTypeName); if (values != null) { - results = Collections.singletonList(newAnnotationValue(annotationTypeName, values)); + results = List.of(newAnnotationValue(annotationTypeName, values)); } } if (results == null) { - results = Collections.emptyList(); + results = List.of(); } annotationValuesByType.put(annotationTypeName, results); } @@ -1019,11 +1020,11 @@ public Optional getValue(@NonNull String annotation, @NonNull String memb @Override public List> getAnnotationValuesByName(String annotationType) { if (annotationType == null) { - return Collections.emptyList(); + return List.of(); } String repeatableTypeName = findRepeatableAnnotationContainerInternal(annotationType); if (repeatableTypeName == null) { - return Collections.emptyList(); + return List.of(); } List> results = resolveRepeatableAnnotations(repeatableTypeName, allAnnotations, allStereotypes); if (results != null) { @@ -1032,14 +1033,14 @@ public List> getAnnotationValuesByName if (allAnnotations != null) { final Map values = allAnnotations.get(annotationType); if (values != null) { - results = Collections.singletonList(newAnnotationValue(annotationType, values)); + results = List.of(newAnnotationValue(annotationType, values)); } } if (results == null) { - results = Collections.emptyList(); + results = List.of(); } annotationValuesByType.put(annotationType, results); - return Collections.emptyList(); + return List.of(); } @NonNull @@ -1051,26 +1052,26 @@ protected AnnotationValue newAnnotationValue(String a @Override public List> getDeclaredAnnotationValuesByType(@NonNull Class annotationType) { if (annotationType == null) { - return Collections.emptyList(); + return List.of(); } List> results = resolveAnnotationValuesByType(annotationType, declaredAnnotations, declaredStereotypes); if (results != null) { return results; } - return Collections.emptyList(); + return List.of(); } @Override public List> getDeclaredAnnotationValuesByName(String annotationType) { if (annotationType == null) { - return Collections.emptyList(); + return List.of(); } String repeatableTypeName = findRepeatableAnnotationContainerInternal(annotationType); List> results = resolveRepeatableAnnotations(repeatableTypeName, declaredAnnotations, declaredStereotypes); if (results != null) { return results; } - return Collections.emptyList(); + return List.of(); } @SuppressWarnings("unchecked") @@ -1165,7 +1166,7 @@ public Optional getAnnotationNameByStereotype(@Nullable String stereotyp @Override public List getAnnotationNamesByStereotype(@Nullable String stereotype) { if (stereotype == null) { - return Collections.emptyList(); + return List.of(); } if (annotationsByStereotype != null) { List annotations = annotationsByStereotype.get(stereotype); @@ -1174,18 +1175,18 @@ public List getAnnotationNamesByStereotype(@Nullable String stereotype) } } if (allAnnotations != null && allAnnotations.containsKey(stereotype)) { - return StringUtils.internListOf(stereotype); + return List.of(stereotype); } if (declaredAnnotations != null && declaredAnnotations.containsKey(stereotype)) { - return StringUtils.internListOf(stereotype); + return List.of(stereotype); } - return Collections.emptyList(); + return List.of(); } @Override public List> getAnnotationValuesByStereotype(String stereotype) { if (stereotype == null) { - return Collections.emptyList(); + return List.of(); } if (annotationsByStereotype != null) { List annotations = annotationsByStereotype.get(stereotype); @@ -1215,16 +1216,16 @@ public List> getAnnotationValuesBySter if (declaredAnnotations != null) { return getDeclaredAnnotationValuesByName(stereotype); } - return Collections.emptyList(); + return List.of(); } @NonNull @Override public Set getAnnotationNames() { if (allAnnotations != null) { - return allAnnotations.keySet(); + return Collections.unmodifiableSet(allAnnotations.keySet()); } - return Collections.emptySet(); + return Set.of(); } @NonNull @@ -1233,7 +1234,7 @@ public Set getStereotypeAnnotationNames() { if (allStereotypes != null) { return Collections.unmodifiableSet(allStereotypes.keySet()); } - return Collections.emptySet(); + return Set.of(); } @NonNull @@ -1242,23 +1243,23 @@ public Set getDeclaredStereotypeAnnotationNames() { if (declaredStereotypes != null) { return Collections.unmodifiableSet(declaredStereotypes.keySet()); } - return Collections.emptySet(); + return Set.of(); } @NonNull @Override public Set getDeclaredAnnotationNames() { if (declaredAnnotations != null) { - return declaredAnnotations.keySet(); + return Collections.unmodifiableSet(declaredAnnotations.keySet()); } - return Collections.emptySet(); + return Set.of(); } @NonNull @Override public List getDeclaredAnnotationNamesByStereotype(@Nullable String stereotype) { if (stereotype == null) { - return Collections.emptyList(); + return List.of(); } if (annotationsByStereotype != null) { List annotations = annotationsByStereotype.get(stereotype); @@ -1269,14 +1270,14 @@ public List getDeclaredAnnotationNamesByStereotype(@Nullable String ster return Collections.unmodifiableList(annotations); } else { // no declared - return Collections.emptyList(); + return List.of(); } } } if (declaredAnnotations != null && declaredAnnotations.containsKey(stereotype)) { - return StringUtils.internListOf(stereotype); + return List.of(stereotype); } - return Collections.emptyList(); + return List.of(); } @NonNull @@ -1535,16 +1536,16 @@ private List> resolveRepeatableAnnotat if (!hasStereotype(repeatableTypeName)) { return null; } - List> results = new ArrayList<>(); + List> results = null; if (sourceAnnotations != null) { Map values = sourceAnnotations.get(repeatableTypeName); - addAnnotationValuesFromData(results, values); + results = collectResult(results, values); } if (sourceStereotypes != null) { Map values = sourceStereotypes.get(repeatableTypeName); - addAnnotationValuesFromData(results, values); + results = collectResult(results, values); } - return results; + return results == null ? List.of() : results; } @Nullable @@ -1597,4 +1598,31 @@ private Object getRawValue(@NonNull String annotation, @NonNull String member) { protected String findRepeatableAnnotationContainerInternal(@NonNull String annotation) { return AnnotationMetadataSupport.getRepeatableAnnotation(annotation); } + + private List> collectResult(List> results, Map values) { + if (values != null) { + Object v = values.get(AnnotationMetadata.VALUE_MEMBER); + if (v instanceof AnnotationValue[] avs) { + List> result = (List) Arrays.asList(avs); + if (results == null) { + return result; + } else { + return CollectionUtils.concat(results, result); + } + } else if (v instanceof Collection c) { + List> result = new ArrayList<>(c.size()); + for (Object o : c) { + if (o instanceof io.micronaut.core.annotation.AnnotationValue av) { + result.add((AnnotationValue) av); + } + } + if (results == null) { + return result; + } else { + return CollectionUtils.concat(results, result); + } + } + } + return results; + } } From fc5103c3b5a788571787e8b069c647b7d3441bf5 Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Fri, 31 Mar 2023 15:05:38 +0200 Subject: [PATCH 643/743] ci: fold release-notes & sonar in gradle workflow (#9033) --- .github/workflows/corretto.yml | 38 ------------ .github/workflows/graalvm.yml | 14 ----- .github/workflows/gradle.yml | 95 +++++++++++++---------------- .github/workflows/release-notes.yml | 50 --------------- .github/workflows/sonarqube.yml | 58 ------------------ 5 files changed, 44 insertions(+), 211 deletions(-) delete mode 100644 .github/workflows/corretto.yml delete mode 100644 .github/workflows/release-notes.yml delete mode 100644 .github/workflows/sonarqube.yml diff --git a/.github/workflows/corretto.yml b/.github/workflows/corretto.yml deleted file mode 100644 index aacb77c2c95..00000000000 --- a/.github/workflows/corretto.yml +++ /dev/null @@ -1,38 +0,0 @@ -name: Corretto CI -on: - push: - branches: - - master -jobs: - build: - runs-on: ubuntu-latest - strategy: - matrix: - java: ['8', '11', '17'] - container: amazoncorretto:${{ matrix.java }} - steps: - - name: Display Java and Linux version - run: java -version && cat /etc/system-release - - name: Install tar && gzip - run: yum install -y tar gzip - - uses: actions/checkout@v3 - - uses: actions/cache@v3.0.2 - with: - path: ~/.gradle/caches - key: ${{ runner.os }}-micronaut-core-gradle-${{ hashFiles('**/*.gradle') }} - restore-keys: | - ${{ runner.os }}-micronaut-core-gradle- - - uses: actions/cache@v3.0.2 - with: - path: ~/.gradle/wrapper - key: ${{ runner.os }}-micronaut-core-wrapper-${{ hashFiles('**/*.gradle') }} - restore-keys: | - ${{ runner.os }}-micronaut-core-wrapper- - - name: Build with Gradle - env: - GH_TOKEN_PUBLIC_REPOS_READONLY: ${{ secrets.GH_TOKEN_PUBLIC_REPOS_READONLY }} - GH_USERNAME: ${{ secrets.GH_USERNAME }} - GRADLE_ENTERPRISE_ACCESS_KEY: ${{ secrets.GRADLE_ENTERPRISE_ACCESS_KEY }} - GRADLE_ENTERPRISE_CACHE_USERNAME: ${{ secrets.GRADLE_ENTERPRISE_CACHE_USERNAME }} - GRADLE_ENTERPRISE_CACHE_PASSWORD: ${{ secrets.GRADLE_ENTERPRISE_CACHE_PASSWORD }} - run: unset HOSTNAME ; LANG=en_US.utf-8 LC_ALL=en_US.utf-8 ./gradlew check --no-daemon --parallel --continue diff --git a/.github/workflows/graalvm.yml b/.github/workflows/graalvm.yml index c06db472f01..490d9b0952b 100644 --- a/.github/workflows/graalvm.yml +++ b/.github/workflows/graalvm.yml @@ -7,11 +7,9 @@ name: GraalVM CE CI on: push: branches: - - master - '[1-9]+.[0-9]+.x' pull_request: branches: - - master - '[1-9]+.[0-9]+.x' jobs: build: @@ -62,18 +60,6 @@ jobs: GRADLE_ENTERPRISE_CACHE_USERNAME: ${{ secrets.GRADLE_ENTERPRISE_CACHE_USERNAME }} GRADLE_ENTERPRISE_CACHE_PASSWORD: ${{ secrets.GRADLE_ENTERPRISE_CACHE_PASSWORD }} PREDICTIVE_TEST_SELECTION: "${{ github.event_name == 'pull_request' && 'true' || 'false' }}" - - name: Add build scan URL as PR comment - uses: actions/github-script@v5 - if: github.event_name == 'pull_request' && failure() - with: - github-token: ${{secrets.GITHUB_TOKEN}} - script: | - github.rest.issues.createComment({ - issue_number: context.issue.number, - owner: context.repo.owner, - repo: context.repo.repo, - body: '❌ ${{ github.workflow }} ${{ matrix.java }} ${{ matrix.graalvm }} failed: ${{ steps.gradle.outputs.build-scan-url }}' - }) - name: Publish Test Report if: always() uses: mikepenz/action-junit-report@v3.7.1 diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index 69a7471dd8d..24680e4eaa4 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -7,11 +7,9 @@ name: Java CI on: push: branches: - - master - '[1-9]+.[0-9]+.x' pull_request: branches: - - master - '[1-9]+.[0-9]+.x' jobs: build: @@ -20,85 +18,80 @@ jobs: strategy: matrix: java: ['8', '11', '17'] + env: + GRADLE_ENTERPRISE_ACCESS_KEY: ${{ secrets.GRADLE_ENTERPRISE_ACCESS_KEY }} + GRADLE_ENTERPRISE_CACHE_USERNAME: ${{ secrets.GRADLE_ENTERPRISE_CACHE_USERNAME }} + GRADLE_ENTERPRISE_CACHE_PASSWORD: ${{ secrets.GRADLE_ENTERPRISE_CACHE_PASSWORD }} + GH_TOKEN_PUBLIC_REPOS_READONLY: ${{ secrets.GH_TOKEN_PUBLIC_REPOS_READONLY }} + GH_USERNAME: ${{ secrets.GH_USERNAME }} + TESTCONTAINERS_RYUK_DISABLED: true + PREDICTIVE_TEST_SELECTION: "${{ github.event_name == 'pull_request' && 'true' || 'false' }}" + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: - # https://github.com/actions/virtual-environments/issues/709 - - name: Free disk space + # https://github.com/actions/virtual-environments/issues/709 + - name: "🗑 Free disk space" run: | - sudo rm -rf "/usr/local/share/boost" - sudo rm -rf "$AGENT_TOOLSDIRECTORY" - sudo apt-get clean - df -h - - uses: actions/checkout@v3 - - name: Set up JDK - uses: actions/setup-java@v3 + sudo rm -rf "/usr/local/share/boost" + sudo rm -rf "$AGENT_TOOLSDIRECTORY" + sudo apt-get clean + df -h + + - name: "📥 Checkout repository" + uses: actions/checkout@v3 with: + fetch-depth: 0 distribution: 'temurin' java-version: ${{ matrix.java }} - - name: Setup Gradle - uses: gradle/gradle-build-action@v2.3.3 - - name: Optional setup step - env: - GRADLE_ENTERPRISE_ACCESS_KEY: ${{ secrets.GRADLE_ENTERPRISE_ACCESS_KEY }} - GRADLE_ENTERPRISE_CACHE_USERNAME: ${{ secrets.GRADLE_ENTERPRISE_CACHE_USERNAME }} - GRADLE_ENTERPRISE_CACHE_PASSWORD: ${{ secrets.GRADLE_ENTERPRISE_CACHE_PASSWORD }} + + - name: "🔧 Setup Gradle" + uses: gradle/gradle-build-action@v2 + + - name: "❓ Optional setup step" run: | - [ -f ./setup.sh ] && ./setup.sh || true - - name: Build with Gradle + [ -f ./setup.sh ] && ./setup.sh || [ ! -f ./setup.sh ] + + - name: "🛠 Build with Gradle" id: gradle run: | - ./gradlew check --no-daemon --parallel --continue - env: - GH_TOKEN_PUBLIC_REPOS_READONLY: ${{ secrets.GH_TOKEN_PUBLIC_REPOS_READONLY }} - GH_USERNAME: ${{ secrets.GH_USERNAME }} - TESTCONTAINERS_RYUK_DISABLED: true - GRADLE_ENTERPRISE_ACCESS_KEY: ${{ secrets.GRADLE_ENTERPRISE_ACCESS_KEY }} - GRADLE_ENTERPRISE_CACHE_USERNAME: ${{ secrets.GRADLE_ENTERPRISE_CACHE_USERNAME }} - GRADLE_ENTERPRISE_CACHE_PASSWORD: ${{ secrets.GRADLE_ENTERPRISE_CACHE_PASSWORD }} - PREDICTIVE_TEST_SELECTION: "${{ github.event_name == 'pull_request' && 'true' || 'false' }}" - - name: Add build scan URL as PR comment - uses: actions/github-script@v5 - if: github.event_name == 'pull_request' && failure() - with: - github-token: ${{secrets.GITHUB_TOKEN}} - script: | - github.rest.issues.createComment({ - issue_number: context.issue.number, - owner: context.repo.owner, - repo: context.repo.repo, - body: '❌ ${{ github.workflow }} failed: ${{ steps.gradle.outputs.build-scan-url }}' - }) - - name: Publish Test Report + ./gradlew check --no-daemon --continue + + - name: "🔎 Run static analysis" + if: env.SONAR_TOKEN != '' + run: | + ./gradlew sonar + + - name: "📊 Publish Test Report" if: always() - uses: mikepenz/action-junit-report@v3.7.1 + uses: mikepenz/action-junit-report@v3 with: check_name: Java CI / Test Report (${{ matrix.java }}) report_paths: '**/build/test-results/test/TEST-*.xml' check_retries: 'true' + - name: "📜 Upload binary compatibility check results" if: always() uses: actions/upload-artifact@v3 with: name: binary-compatibility-reports path: "**/build/reports/binary-compatibility-*.html" - - name: Publish to Sonatype Snapshots + + - name: "📦 Publish to Sonatype Snapshots" if: success() && github.event_name == 'push' && matrix.java == '11' env: - GH_TOKEN_PUBLIC_REPOS_READONLY: ${{ secrets.GH_TOKEN_PUBLIC_REPOS_READONLY }} - GH_USERNAME: ${{ secrets.GH_USERNAME }} SONATYPE_USERNAME: ${{ secrets.SONATYPE_USERNAME }} SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }} - GRADLE_ENTERPRISE_ACCESS_KEY: ${{ secrets.GRADLE_ENTERPRISE_ACCESS_KEY }} - GRADLE_ENTERPRISE_CACHE_USERNAME: ${{ secrets.GRADLE_ENTERPRISE_CACHE_USERNAME }} - GRADLE_ENTERPRISE_CACHE_PASSWORD: ${{ secrets.GRADLE_ENTERPRISE_CACHE_PASSWORD }} run: ./gradlew publishToSonatype docs --no-daemon - - name: Determine docs target repository + + - name: "❓ Determine docs target repository" uses: haya14busa/action-cond@v1 id: docs_target with: cond: ${{ github.repository == 'micronaut-projects/micronaut-core' }} if_true: "micronaut-projects/micronaut-docs" if_false: ${{ github.repository }} - - name: Publish to Github Pages + + - name: "📑 Publish to Github Pages" if: success() && github.event_name == 'push' && matrix.java == '11' uses: micronaut-projects/github-pages-deploy-action@master env: diff --git a/.github/workflows/release-notes.yml b/.github/workflows/release-notes.yml deleted file mode 100644 index ecb1f667c65..00000000000 --- a/.github/workflows/release-notes.yml +++ /dev/null @@ -1,50 +0,0 @@ -# WARNING: Do not edit this file directly. Instead, go to: -# -# https://github.com/micronaut-projects/micronaut-project-template/tree/master/.github/workflows -# -# and edit them there. Note that it will be sync'ed to all the Micronaut repos -name: Changelog -on: - issues: - types: [closed,reopened] - push: - branches: - - master - - '[1-9]+.[0-9]+.x' -jobs: - release_notes: - if: github.repository != 'micronaut-projects/micronaut-project-template' - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - name: Check if it has release drafter config file - id: check_release_drafter - run: | - has_release_drafter=$([ -f .github/release-drafter.yml ] && echo "true" || echo "false") - echo "has_release_drafter=${has_release_drafter}" >> $GITHUB_OUTPUT - - # If it has release drafter: - - uses: release-drafter/release-drafter@v5 - if: steps.check_release_drafter.outputs.has_release_drafter == 'true' - env: - GITHUB_TOKEN: ${{ secrets.GH_TOKEN }} - - # Otherwise: - - name: Export Gradle Properties - if: steps.check_release_drafter.outputs.has_release_drafter == 'false' - uses: micronaut-projects/github-actions/export-gradle-properties@master - - uses: micronaut-projects/github-actions/release-notes@master - if: steps.check_release_drafter.outputs.has_release_drafter == 'false' - id: release_notes - with: - token: ${{ secrets.GH_TOKEN }} - - uses: ncipollo/release-action@v1 - if: steps.check_release_drafter.outputs.has_release_drafter == 'false' && steps.release_notes.outputs.generated_changelog == 'true' - with: - allowUpdates: true - commit: ${{ steps.release_notes.outputs.current_branch }} - draft: true - name: ${{ env.title }} ${{ steps.release_notes.outputs.next_version }} - tag: v${{ steps.release_notes.outputs.next_version }} - bodyFile: CHANGELOG.md - token: ${{ secrets.GH_TOKEN }} diff --git a/.github/workflows/sonarqube.yml b/.github/workflows/sonarqube.yml deleted file mode 100644 index 5faafe7cbe8..00000000000 --- a/.github/workflows/sonarqube.yml +++ /dev/null @@ -1,58 +0,0 @@ -# WARNING: Do not edit this file directly. Instead, go to: -# -# https://github.com/micronaut-projects/micronaut-project-template/tree/master/.github/workflows -# -# and edit them there. Note that it will be sync'ed to all the Micronaut repos -name: Static Analysis -on: - push: - branches: - - master - - '[1-9]+.[0-9]+.x' - pull_request: - branches: - - master - - '[1-9]+.[0-9]+.x' -jobs: - build: - if: github.repository != 'micronaut-projects/micronaut-project-template' - runs-on: ubuntu-latest - steps: - # https://github.com/actions/virtual-environments/issues/709 - - name: Free disk space - run: | - sudo rm -rf "/usr/local/share/boost" - sudo rm -rf "$AGENT_TOOLSDIRECTORY" - sudo apt-get clean - df -h - - uses: actions/checkout@v3 - with: - fetch-depth: 0 - - uses: actions/cache@v3 - with: - path: ~/.gradle/caches - key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle') }} - restore-keys: | - ${{ runner.os }}-gradle- - - name: Set up JDK - uses: actions/setup-java@v3 - with: - java-version: 11 - distribution: 'temurin' - - name: Optional setup step - env: - GRADLE_ENTERPRISE_ACCESS_KEY: ${{ secrets.GRADLE_ENTERPRISE_ACCESS_KEY }} - GRADLE_ENTERPRISE_CACHE_USERNAME: ${{ secrets.GRADLE_ENTERPRISE_CACHE_USERNAME }} - GRADLE_ENTERPRISE_CACHE_PASSWORD: ${{ secrets.GRADLE_ENTERPRISE_CACHE_PASSWORD }} - run: | - [ -f ./setup.sh ] && ./setup.sh || true - - name: Analyse with Gradle - run: | - ./gradlew check sonarqube --no-daemon --parallel --continue - env: - TESTCONTAINERS_RYUK_DISABLED: true - GRADLE_ENTERPRISE_ACCESS_KEY: ${{ secrets.GRADLE_ENTERPRISE_ACCESS_KEY }} - GRADLE_ENTERPRISE_CACHE_USERNAME: ${{ secrets.GRADLE_ENTERPRISE_CACHE_USERNAME }} - GRADLE_ENTERPRISE_CACHE_PASSWORD: ${{ secrets.GRADLE_ENTERPRISE_CACHE_PASSWORD }} - SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} From 381232b63bc1b916dee592570a7688f1a6fe200c Mon Sep 17 00:00:00 2001 From: Graeme Rocher Date: Fri, 31 Mar 2023 15:07:49 +0200 Subject: [PATCH 644/743] Make evaluation context objects effectively singleton (#9034) --- .../TestExpressionsInjectionSpec.groovy | 16 +++++--- ...nfigurableExpressionEvaluationContext.java | 2 +- .../DefaultExpressionEvaluationContext.java | 39 ++++++++++++++----- .../inject/DefaultBeanIdentifier.java | 2 +- .../guide/config/evaluatedExpressions.adoc | 5 +-- 5 files changed, 43 insertions(+), 21 deletions(-) diff --git a/inject-groovy/src/test/groovy/io/micronaut/expressions/TestExpressionsInjectionSpec.groovy b/inject-groovy/src/test/groovy/io/micronaut/expressions/TestExpressionsInjectionSpec.groovy index f5faa5166de..d8462ff2ad1 100644 --- a/inject-groovy/src/test/groovy/io/micronaut/expressions/TestExpressionsInjectionSpec.groovy +++ b/inject-groovy/src/test/groovy/io/micronaut/expressions/TestExpressionsInjectionSpec.groovy @@ -130,14 +130,16 @@ class TestExpressionsInjectionSpec extends AbstractBeanDefinitionSpec { } class Expr { - private final Integer wrapper - private final int primitive - private final String contextValue + public final Integer wrapper + public final int primitive + public final String contextValue + public final String contextValue2 - Expr(Integer wrapper, int primitive, String contextValue) { + Expr(Integer wrapper, int primitive, String contextValue, String contextValue2) { this.wrapper = wrapper this.primitive = primitive this.contextValue = contextValue + this.contextValue2 = contextValue2; } } @@ -146,8 +148,9 @@ class TestExpressionsInjectionSpec extends AbstractBeanDefinitionSpec { @Bean Expr factoryBean(@Value('#{ 25 }') Integer wrapper, @Value('#{ 23 }') int primitive, - @Value('#{ #contextValue }') String contextValue) { - return new Expr(wrapper, primitive, contextValue) + @Value('#{ #contextValue }') String contextValue, + @Value("#{ contextValue + ' ' + contextValue }") String contextValue2) { + return new Expr(wrapper, primitive, contextValue, contextValue2) } } """) @@ -159,6 +162,7 @@ class TestExpressionsInjectionSpec extends AbstractBeanDefinitionSpec { bean.wrapper == 25 bean.primitive == 23 bean.contextValue == "context value" + bean.contextValue2 == "context value context value" cleanup: ctx.close() diff --git a/inject/src/main/java/io/micronaut/context/expressions/ConfigurableExpressionEvaluationContext.java b/inject/src/main/java/io/micronaut/context/expressions/ConfigurableExpressionEvaluationContext.java index 8c3823f338f..dec168cacb9 100644 --- a/inject/src/main/java/io/micronaut/context/expressions/ConfigurableExpressionEvaluationContext.java +++ b/inject/src/main/java/io/micronaut/context/expressions/ConfigurableExpressionEvaluationContext.java @@ -29,7 +29,7 @@ * @author Sergey Gavrilov */ @Internal -public interface ConfigurableExpressionEvaluationContext extends ExpressionEvaluationContext { +public sealed interface ConfigurableExpressionEvaluationContext extends ExpressionEvaluationContext permits DefaultExpressionEvaluationContext { /** * Set arguments passed to invoked method. diff --git a/inject/src/main/java/io/micronaut/context/expressions/DefaultExpressionEvaluationContext.java b/inject/src/main/java/io/micronaut/context/expressions/DefaultExpressionEvaluationContext.java index e480e84eb24..ac164a63ed1 100644 --- a/inject/src/main/java/io/micronaut/context/expressions/DefaultExpressionEvaluationContext.java +++ b/inject/src/main/java/io/micronaut/context/expressions/DefaultExpressionEvaluationContext.java @@ -16,6 +16,7 @@ package io.micronaut.context.expressions; import io.micronaut.context.BeanContext; +import io.micronaut.context.BeanRegistration; import io.micronaut.context.BeanResolutionContext; import io.micronaut.context.DefaultBeanContext; import io.micronaut.context.DefaultBeanResolutionContext; @@ -24,6 +25,7 @@ import io.micronaut.core.annotation.Nullable; import io.micronaut.core.type.Argument; import io.micronaut.inject.BeanDefinition; +import io.micronaut.inject.BeanIdentifier; /** * Default implementation of {@link ConfigurableExpressionEvaluationContext}. @@ -34,7 +36,7 @@ * @author Sergey Gavrilov */ @Internal -public class DefaultExpressionEvaluationContext implements ConfigurableExpressionEvaluationContext { +public final class DefaultExpressionEvaluationContext implements ConfigurableExpressionEvaluationContext { private final Object[] args; private final BeanContext beanContext; @@ -56,22 +58,28 @@ public DefaultExpressionEvaluationContext(@Nullable Object[] args, @Override public ConfigurableExpressionEvaluationContext setArguments(Object[] args) { - return new DefaultExpressionEvaluationContext(args, this.beanContext, this.owningBean); + DefaultExpressionEvaluationContext evaluationContext = new DefaultExpressionEvaluationContext(args, this.beanContext, this.owningBean); + evaluationContext.resolutionContext = resolutionContext; + return evaluationContext; } @Override public ConfigurableExpressionEvaluationContext setOwningBean(BeanDefinition beanDefinition) { - return new DefaultExpressionEvaluationContext(this.args, this.beanContext, beanDefinition); + DefaultExpressionEvaluationContext evaluationContext = new DefaultExpressionEvaluationContext(this.args, this.beanContext, beanDefinition); + evaluationContext.resolutionContext = resolutionContext; + return evaluationContext; } @Override public ConfigurableExpressionEvaluationContext setBeanContext(BeanContext beanContext) { - return new DefaultExpressionEvaluationContext(this.args, beanContext, this.owningBean); + DefaultExpressionEvaluationContext evaluationContext = new DefaultExpressionEvaluationContext(this.args, beanContext, this.owningBean); + evaluationContext.resolutionContext = resolutionContext; + return evaluationContext; } @Override public Object getArgument(int index) { - if (args == null || args.length == 0) { + if (args == null || args.length == 0 || args.length < index) { throw new ExpressionEvaluationException( "Can not obtain argument at index [" + index + "] since arguments are not provided"); } @@ -89,11 +97,19 @@ public T getBean(Class type) { if (resolutionContext == null && owningBean != null) { resolutionContext = new DefaultBeanResolutionContext(defaultBeanContext, owningBean); } - if (resolutionContext != null) { - try (BeanResolutionContext.Path ignored = - resolutionContext.getPath().pushAnnotationResolve(owningBean, Argument.of(type))) { - return defaultBeanContext.getBean(resolutionContext, type); + BeanIdentifier identifier = BeanIdentifier.of(type.getName()); + BeanRegistration existing = resolutionContext.getInFlightBean(identifier); + if (existing != null) { + return (T) existing.getBean(); + } else { + Argument t = Argument.of(type); + try (BeanResolutionContext.Path ignored = + resolutionContext.getPath().pushAnnotationResolve(owningBean, t)) { + BeanRegistration beanRegistration = defaultBeanContext.getBeanRegistration(resolutionContext, t, null); + resolutionContext.addInFlightBean(identifier, beanRegistration); + return beanRegistration.getBean(); + } } } } @@ -103,6 +119,9 @@ public T getBean(Class type) { @Override public void close() throws Exception { - resolutionContext = null; + if (resolutionContext != null) { + resolutionContext.close(); + resolutionContext = null; + } } } diff --git a/inject/src/main/java/io/micronaut/inject/DefaultBeanIdentifier.java b/inject/src/main/java/io/micronaut/inject/DefaultBeanIdentifier.java index 55074850e3b..835fa85a9ea 100644 --- a/inject/src/main/java/io/micronaut/inject/DefaultBeanIdentifier.java +++ b/inject/src/main/java/io/micronaut/inject/DefaultBeanIdentifier.java @@ -26,7 +26,7 @@ * @since 1.0 */ @Internal -class DefaultBeanIdentifier implements BeanIdentifier { +final class DefaultBeanIdentifier implements BeanIdentifier { private final String id; diff --git a/src/main/docs/guide/config/evaluatedExpressions.adoc b/src/main/docs/guide/config/evaluatedExpressions.adoc index 2966c2ede7b..1e4babd3321 100644 --- a/src/main/docs/guide/config/evaluatedExpressions.adoc +++ b/src/main/docs/guide/config/evaluatedExpressions.adoc @@ -267,14 +267,13 @@ The available methods can be extended by extended the evaluation context. There NOTE: The api:TypeElementVisitor[] has to be on the annotation processor classpath, therefore needs to be defined in a separate module that can be included on this classpath. -Once a class is registered within evaluation context the methods and properties of the class are available for referencing in evaluated expressions. Any context reference -needs to be prefixed with `#` sign. +Once a class is registered within evaluation context the methods and properties of the class are available for referencing in evaluated expressions. Consider the following example: snippet::io.micronaut.docs.expressions.CustomEvaluationContext[title="User-defined evaluated expression context"] -NOTE: The class should be resolvable as a bean can use `jakarta.inject` annotations to inject other types if necessary. +NOTE: The class should be resolvable as a bean can use `jakarta.inject` annotations to inject other types if necessary. In addition, for performance reasons all evaluation context classes are effectively singleton regardless of the defined scope. Registering this class can be achieved with a custom implementation of api:expressions.context.ExpressionEvaluationContextRegistrar[] that is registered via service loader as a api:inject.visitor.TypeElementVisitor[] (create a new `META-INF/services/io.micronaut.inject.visitor.TypeElementVisitor` file referencing the new class) and placed on the annotation processor classpath: From 609461769d24097b43b93ef549289ea1dd78aa13 Mon Sep 17 00:00:00 2001 From: Tim Yates Date: Fri, 31 Mar 2023 14:15:11 +0100 Subject: [PATCH 645/743] feat: Add a class for handling headers in a case-insensitive way (#9031) --- .../CaseInsensitiveMutableHttpHeaders.java | 294 ++++++++++++++++++ ...seInsensitiveMutableHttpHeadersSpec.groovy | 183 +++++++++++ 2 files changed, 477 insertions(+) create mode 100644 http/src/main/java/io/micronaut/http/CaseInsensitiveMutableHttpHeaders.java create mode 100644 http/src/test/groovy/io/micronaut/http/CaseInsensitiveMutableHttpHeadersSpec.groovy diff --git a/http/src/main/java/io/micronaut/http/CaseInsensitiveMutableHttpHeaders.java b/http/src/main/java/io/micronaut/http/CaseInsensitiveMutableHttpHeaders.java new file mode 100644 index 00000000000..2084e6e2059 --- /dev/null +++ b/http/src/main/java/io/micronaut/http/CaseInsensitiveMutableHttpHeaders.java @@ -0,0 +1,294 @@ +/* + * Copyright 2017-2023 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.http; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.Nullable; +import io.micronaut.core.convert.ArgumentConversionContext; +import io.micronaut.core.convert.ConversionService; +import io.micronaut.core.util.CollectionUtils; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.TreeMap; + +/** + * A {@link MutableHttpHeaders} implementation that is case-insensitive. + * + * @author Tim Yates + * @since 4.0.0 + */ +@Internal +public final class CaseInsensitiveMutableHttpHeaders implements MutableHttpHeaders { + + private final boolean validate; + private final TreeMap> backing; + private final ConversionService conversionService; + + /** + * Create an empty CaseInsensitiveMutableHttpHeaders. + * + * @param conversionService The conversion service + */ + public CaseInsensitiveMutableHttpHeaders(ConversionService conversionService) { + this(true, Collections.emptyMap(), conversionService); + } + + /** + * Create an empty CaseInsensitiveMutableHttpHeaders. + * + * @param validate Whether to validate the headers + * @param conversionService The conversion service + */ + public CaseInsensitiveMutableHttpHeaders(boolean validate, ConversionService conversionService) { + this(validate, Collections.emptyMap(), conversionService); + } + + /** + * Create a CaseInsensitiveMutableHttpHeaders populated by the entries in the provided {@literal Map}. + * + * @param defaults The defaults + * @param conversionService The conversion service + */ + public CaseInsensitiveMutableHttpHeaders(Map> defaults, ConversionService conversionService) { + this(true, defaults, conversionService); + } + + /** + * Create a CaseInsensitiveMutableHttpHeaders populated by the entries in the provided {@literal Map}. + *

+ * Warning! Setting {@code validate} to {@code false} will not validate header names and values, and can leave your server implementation vulnerable to + * CWE-113: Improper Neutralization of CRLF Sequences in HTTP Headers ('HTTP Response Splitting'). + * + * @param validate Whether to validate the headers + * @param defaults The defaults + * @param conversionService The conversion service + */ + public CaseInsensitiveMutableHttpHeaders(boolean validate, Map> defaults, ConversionService conversionService) { + this.validate = validate; + this.conversionService = conversionService; + this.backing = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); + defaults.forEach((key, value) -> value.forEach(v -> this.add(key, v))); + } + + @Override + public List getAll(CharSequence name) { + if (name == null) { + return Collections.emptyList(); + } + List values = backing.get(name.toString()); + if (CollectionUtils.isEmpty(values)) { + return Collections.emptyList(); + } + return Collections.unmodifiableList(values); + } + + @Nullable + @Override + public String get(CharSequence name) { + if (name == null) { + return null; + } + List strings = backing.get(name.toString()); + if (CollectionUtils.isEmpty(strings)) { + return null; + } + return strings.get(0); + } + + @Override + public Set names() { + return backing.keySet(); + } + + @Override + public Collection> values() { + return backing.values(); + } + + @Override + @SuppressWarnings("unchecked") + public Optional get(CharSequence name, ArgumentConversionContext conversionContext) { + String value = get(name); + if (value != null) { + if (conversionContext.getArgument().getType().isInstance(value)) { + return Optional.of((T) value); + } else { + return conversionService.convert(value, conversionContext); + } + } + return Optional.empty(); + } + + @Override + public MutableHttpHeaders add(CharSequence header, CharSequence value) { + validate(header, value); + backing.computeIfAbsent(header.toString(), s -> new ArrayList<>(2)).add(value.toString()); + return this; + } + + @Override + public MutableHttpHeaders remove(CharSequence header) { + if (header != null) { + backing.remove(header.toString()); + } + return this; + } + + /******************************************************************************************************************* + * Header validation code taken from io.netty.handler.codec.http.HttpHeaderValidationUtils. + ******************************************************************************************************************/ + + private void validate(CharSequence header, CharSequence value) { + if (header == null) { + throw new IllegalArgumentException("Header name cannot be null"); + } + if (validate) { + int index = validateCharSequenceToken(header); + if (index != -1) { + throw new IllegalArgumentException("A header name can only contain \"token\" characters, but found invalid character 0x" + Integer.toHexString(header.charAt(index)) + " at index " + index + " of header '" + header + "'."); + } + index = verifyValidHeaderValueCharSequence(value); + if (index != -1) { + throw new IllegalArgumentException("The header value for '" + header + "' contains prohibited character 0x" + Integer.toHexString(value.charAt(index)) + " at index " + index + '.'); + } + } + } + + private static int validateCharSequenceToken(CharSequence token) { + for (int i = 0, len = token.length(); i < len; i++) { + byte value = (byte) token.charAt(i); + if (!BitSet128.contains(value, TOKEN_CHARS_HIGH, TOKEN_CHARS_LOW)) { + return i; + } + } + return -1; + } + + private static int verifyValidHeaderValueCharSequence(CharSequence value) { + // Validate value to field-content rule. + // field-content = field-vchar [ 1*( SP / HTAB ) field-vchar ] + // field-vchar = VCHAR / obs-text + // VCHAR = %x21-7E ; visible (printing) characters + // obs-text = %x80-FF + // SP = %x20 + // HTAB = %x09 ; horizontal tab + // See: https://datatracker.ietf.org/doc/html/rfc7230#section-3.2 + // And: https://datatracker.ietf.org/doc/html/rfc5234#appendix-B.1 + int b = value.charAt(0); + if (b < 0x21 || b == 0x7F) { + return 0; + } + int length = value.length(); + for (int i = 1; i < length; i++) { + b = value.charAt(i); + if (b < 0x20 && b != 0x09 || b == 0x7F) { + return i; + } + } + return -1; + } + + @SuppressWarnings("DeclarationOrder") + private static final long TOKEN_CHARS_HIGH; + @SuppressWarnings("DeclarationOrder") + private static final long TOKEN_CHARS_LOW; + + static { + // HEADER + // header-field = field-name ":" OWS field-value OWS + // + // field-name = token + // token = 1*tchar + // + // tchar = "!" / "#" / "$" / "%" / "&" / "'" / "*" + // / "+" / "-" / "." / "^" / "_" / "`" / "|" / "~" + // / DIGIT / ALPHA + // ; any VCHAR, except delimiters. + // Delimiters are chosen + // from the set of US-ASCII visual characters not allowed in a token + // (DQUOTE and "(),/:;<=>?@[\]{}") + // + // COOKIE + // cookie-pair = cookie-name "=" cookie-value + // cookie-name = token + // token = 1* + // CTL = + // separators = "(" | ")" | "<" | ">" | "@" + // | "," | ";" | ":" | "\" | <"> + // | "/" | "[" | "]" | "?" | "=" + // | "{" | "}" | SP | HT + // + // field-name's token is equivalent to cookie-name's token, we can reuse the tchar mask for both: + BitSet128 tokenChars = new BitSet128() + .range('0', '9').range('a', 'z').range('A', 'Z') // Alphanumeric. + .bits('-', '.', '_', '~') // Unreserved characters. + .bits('!', '#', '$', '%', '&', '\'', '*', '+', '^', '`', '|'); // Token special characters. + TOKEN_CHARS_HIGH = tokenChars.high(); + TOKEN_CHARS_LOW = tokenChars.low(); + } + + private static final class BitSet128 { + private long high; + private long low; + + BitSet128 range(char fromInc, char toInc) { + for (int bit = fromInc; bit <= toInc; bit++) { + if (bit < 64) { + low |= 1L << bit; + } else { + high |= 1L << bit - 64; + } + } + return this; + } + + BitSet128 bits(char... bits) { + for (char bit : bits) { + if (bit < 64) { + low |= 1L << bit; + } else { + high |= 1L << bit - 64; + } + } + return this; + } + + long high() { + return high; + } + + long low() { + return low; + } + + static boolean contains(byte bit, long high, long low) { + if (bit < 0) { + return false; + } + if (bit < 64) { + return 0 != (low & 1L << bit); + } + return 0 != (high & 1L << bit - 64); + } + } +} diff --git a/http/src/test/groovy/io/micronaut/http/CaseInsensitiveMutableHttpHeadersSpec.groovy b/http/src/test/groovy/io/micronaut/http/CaseInsensitiveMutableHttpHeadersSpec.groovy new file mode 100644 index 00000000000..c7fa07deac9 --- /dev/null +++ b/http/src/test/groovy/io/micronaut/http/CaseInsensitiveMutableHttpHeadersSpec.groovy @@ -0,0 +1,183 @@ +package io.micronaut.http + +import io.micronaut.core.convert.ConversionService +import io.micronaut.core.type.Argument +import spock.lang.Specification + +class CaseInsensitiveMutableHttpHeadersSpec extends Specification { + + void "starts empty"() { + given: + CaseInsensitiveMutableHttpHeaders headers = new CaseInsensitiveMutableHttpHeaders(ConversionService.SHARED) + + expect: + headers.isEmpty() + } + + void "can be set up with a map"() { + given: + CaseInsensitiveMutableHttpHeaders headers = new CaseInsensitiveMutableHttpHeaders(ConversionService.SHARED, "Content-Type": ["application/json"], "Content-Length": ["123"]) + + expect: + headers.size() == 2 + headers.get("content-type") == "application/json" + headers.get("content-length") == "123" + } + + void "values can be converted"() { + given: + CaseInsensitiveMutableHttpHeaders headers = new CaseInsensitiveMutableHttpHeaders(ConversionService.SHARED, "Content-Type": ["application/json"], "Content-Length": ["123"]) + + expect: + headers.size() == 2 + headers.get("content-type", Argument.of(MediaType)).get() == MediaType.APPLICATION_JSON_TYPE + headers.get("content-length", Argument.of(Integer)).get() == 123 + headers.get("content-type", Argument.of(String)).get() == MediaType.APPLICATION_JSON + } + + void "values can be removed"() { + when: + CaseInsensitiveMutableHttpHeaders headers = new CaseInsensitiveMutableHttpHeaders(ConversionService.SHARED, "Content-Type": ["application/json"]) + + then: + headers.size() == 1 + headers.get("content-type") == "application/json" + + when: + headers.remove("content-TYPE") + + then: + headers.empty + } + + void "case insensitivity"() { + given: + CaseInsensitiveMutableHttpHeaders headers = new CaseInsensitiveMutableHttpHeaders(ConversionService.SHARED, "foo": ["123"]) + + expect: + ["foo", "FOO", "Foo", "fOo"].each { + assert headers.get(it) == "123" + } + } + + void "getAll returns an unmodifiable collection"() { + given: + CaseInsensitiveMutableHttpHeaders headers = new CaseInsensitiveMutableHttpHeaders(ConversionService.SHARED, "foo": ["123"]) + + when: + headers.getAll("foo").add("456") + + then: + thrown(UnsupportedOperationException) + + when: + headers.getAll("missing").add("456") + + then: + thrown(UnsupportedOperationException) + + when: + headers.getAll(null).add("456") + + then: + thrown(UnsupportedOperationException) + } + + void "calling get with a null name returns null"() { + expect: + new CaseInsensitiveMutableHttpHeaders(ConversionService.SHARED).get(null) == null + } + + void "calling getAll with a null name returns an empty list"() { + expect: + new CaseInsensitiveMutableHttpHeaders(ConversionService.SHARED).getAll(null) == [] + } + + void "calling remove with a null name doesn't throw an exception"() { + when: + new CaseInsensitiveMutableHttpHeaders(ConversionService.SHARED).remove(null) + + then: + noExceptionThrown() + } + + void "getAll on a missing key results in an empty collection"() { + given: + CaseInsensitiveMutableHttpHeaders headers = new CaseInsensitiveMutableHttpHeaders(ConversionService.SHARED) + + expect: + headers.getAll("foo").empty + } + + void "get on a missing key results in null"() { + given: + CaseInsensitiveMutableHttpHeaders headers = new CaseInsensitiveMutableHttpHeaders(ConversionService.SHARED) + + expect: + headers.get("foo") == null + } + + void "cannot add invalid or insecure header names"() { + given: + CaseInsensitiveMutableHttpHeaders headers = new CaseInsensitiveMutableHttpHeaders(ConversionService.SHARED) + + when: + headers.add("foo ", "bar") + + then: + IllegalArgumentException ex = thrown() + ex.message == '''A header name can only contain "token" characters, but found invalid character 0x20 at index 3 of header 'foo '.''' + + when: + new CaseInsensitiveMutableHttpHeaders(ConversionService.SHARED, "foo\nha": ["bar"]) + + then: + IllegalArgumentException cex = thrown() + cex.message == '''A header name can only contain "token" characters, but found invalid character 0xa at index 3 of header 'foo\nha'.''' + + when: + headers.add(null, "null isn't allowed") + + then: + IllegalArgumentException nex = thrown() + nex.message == "Header name cannot be null" + } + + void "cannot add invalid or insecure header values"() { + given: + CaseInsensitiveMutableHttpHeaders headers = new CaseInsensitiveMutableHttpHeaders(ConversionService.SHARED) + + when: + headers.add("foo", "bar\nOrigin: localhost") + + then: + IllegalArgumentException ex = thrown() + ex.message == "The header value for 'foo' contains prohibited character 0xa at index 3." + + when: + new CaseInsensitiveMutableHttpHeaders(ConversionService.SHARED, "foo": ["bar\nOrigin: localhost"]) + + then: + IllegalArgumentException cex = thrown() + cex.message == "The header value for 'foo' contains prohibited character 0xa at index 3." + } + + void "can switch off validation"() { + given: + CaseInsensitiveMutableHttpHeaders headers = new CaseInsensitiveMutableHttpHeaders(false, ConversionService.SHARED) + + when: + headers.add("foo ", "bar") + headers.add("foo", "bar\nOrigin: localhost") + + then: + noExceptionThrown() + + when: + headers.add(null, "null isn't allowed") + + then: + IllegalArgumentException ex = thrown() + ex.message == "Header name cannot be null" + } +} From 48ca00e1fcaf0bfdba46c80831c3d038a9be04ad Mon Sep 17 00:00:00 2001 From: Tim Yates Date: Fri, 31 Mar 2023 14:16:32 +0100 Subject: [PATCH 646/743] Update CRaC to 1.2.1 for Micronaut 3.9.0 (#8992) Co-authored-by: Sergio del Amo --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index ae873c82885..b2bf267ec68 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -72,7 +72,7 @@ managed-micronaut-azure = "3.9.0" managed-micronaut-cache = "3.5.0" managed-micronaut-cassandra = "5.1.1" managed-micronaut-coherence = "3.7.2" -managed-micronaut-crac = "1.2.0" +managed-micronaut-crac = "1.2.1" managed-micronaut-data = "3.9.7" managed-micronaut-discovery = "3.3.0" managed-micronaut-elasticsearch = "4.3.0" From 6a8571d16aaf34493ce86efcffb05792975b4489 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 31 Mar 2023 15:19:47 +0200 Subject: [PATCH 647/743] fix(deps): update managed-jackson to v2.14.2 (#8169) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 6de85f907ee..7adc5655621 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -60,7 +60,7 @@ wiremock = "2.33.2" # managed-groovy = "4.0.10" managed-jakarta-annotation-api = "2.1.1" -managed-jackson = "2.14.0" +managed-jackson = "2.14.2" managed-jackson-databind = "2.14.1" managed-kotlin = "1.8.10" managed-kotlin-coroutines = "1.6.4" From 52499b3249eab99b773a0b4074eac39922f12f10 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 31 Mar 2023 15:20:05 +0200 Subject: [PATCH 648/743] fix(deps): update managed-slf4j to v2.0.7 (#8537) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 7adc5655621..05abe7781c8 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -71,7 +71,7 @@ managed-netty-http3 = "0.0.16.Final" managed-reactive-streams = "1.0.4" # This should be kept aligned with https://github.com/micronaut-projects/micronaut-reactor/blob/master/gradle.properties from the BOM managed-reactor = "3.4.24" -managed-slf4j = "2.0.4" +managed-slf4j = "2.0.7" managed-snakeyaml = "2.0" managed-java-parser-core = "3.24.9" managed-ksp = "1.8.0-1.0.9" From 860df1de7bb838f2edf152c7057ff67e3215a601 Mon Sep 17 00:00:00 2001 From: micronaut-build <65172877+micronaut-build@users.noreply.github.com> Date: Fri, 31 Mar 2023 18:18:36 +0200 Subject: [PATCH 649/743] Bump micronaut-liquibase to 5.6.1 (#9040) --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 355d46a6225..ec466cd980c 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -92,7 +92,7 @@ managed-micronaut-kotlin = "3.2.2" managed-micronaut-kubernetes = "3.4.0" managed-micronaut-micrometer = "4.7.2" managed-micronaut-microstream = "1.3.0" -managed-micronaut-liquibase = "5.6.0" +managed-micronaut-liquibase = "5.6.1" managed-micronaut-mongo = "4.6.0" managed-micronaut-mqtt = "2.3.0" managed-micronaut-multitenancy = "4.2.0" From b7a5ec4c8d1b64a563754ef63fa1d8562c254482 Mon Sep 17 00:00:00 2001 From: Graeme Rocher Date: Sat, 1 Apr 2023 21:35:34 +0200 Subject: [PATCH 650/743] Fix broken incremental compilation with Gradle (#9038) --- .../processing/AnnotationProcessingOutputVisitor.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/inject-java/src/main/java/io/micronaut/annotation/processing/AnnotationProcessingOutputVisitor.java b/inject-java/src/main/java/io/micronaut/annotation/processing/AnnotationProcessingOutputVisitor.java index 44f0525bc86..b2a0a762bea 100644 --- a/inject-java/src/main/java/io/micronaut/annotation/processing/AnnotationProcessingOutputVisitor.java +++ b/inject-java/src/main/java/io/micronaut/annotation/processing/AnnotationProcessingOutputVisitor.java @@ -87,8 +87,8 @@ public OutputStream visitClass(String classname, io.micronaut.inject.ast.Element // gradle filer only support single originating element for isolating processors final io.micronaut.inject.ast.Element e = originatingElements[0]; final Object nativeType = e.getNativeType(); - if (nativeType instanceof Element) { - nativeOriginatingElements = new Element[] { (Element) nativeType }; + if (nativeType instanceof JavaNativeElement javaNativeElement) { + nativeOriginatingElements = new Element[] { javaNativeElement.element() }; } else { nativeOriginatingElements = new Element[0]; } From d439f3519d204ee2d5b6f0b7ffe5b276d31c866c Mon Sep 17 00:00:00 2001 From: Sergey Gavrilov Date: Sun, 2 Apr 2023 14:02:38 +0300 Subject: [PATCH 651/743] retrieving beans from bean context in expressions (#9041) Allows to retrieve beans from context in expressions with the following syntax ``` ctx[T(io.micronaut.example.TestBean)] ``` or simply ``` ctx[io.micronaut.example.TestBean] ``` --- ...gleEvaluatedEvaluatedExpressionParser.java | 100 +++++++++++------- .../parser/ast/access/BeanContextAccess.java | 73 +++++++++++++ .../expressions/parser/token/TokenType.java | 1 + .../expressions/parser/token/Tokenizer.java | 2 + .../BeanContextAccessExpressionsSpec.groovy | 50 +++++++++ .../BeanContextAccessExpressionsSpec.groovy | 50 +++++++++ .../guide/config/evaluatedExpressions.adoc | 16 ++- 7 files changed, 255 insertions(+), 37 deletions(-) create mode 100644 core-processor/src/main/java/io/micronaut/expressions/parser/ast/access/BeanContextAccess.java create mode 100644 inject-groovy/src/test/groovy/io/micronaut/expressions/BeanContextAccessExpressionsSpec.groovy create mode 100644 inject-java/src/test/groovy/io/micronaut/expressions/BeanContextAccessExpressionsSpec.groovy diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/SingleEvaluatedEvaluatedExpressionParser.java b/core-processor/src/main/java/io/micronaut/expressions/parser/SingleEvaluatedEvaluatedExpressionParser.java index 24e437bdf6b..cefd54b7855 100644 --- a/core-processor/src/main/java/io/micronaut/expressions/parser/SingleEvaluatedEvaluatedExpressionParser.java +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/SingleEvaluatedEvaluatedExpressionParser.java @@ -17,6 +17,7 @@ import io.micronaut.core.annotation.Internal; import io.micronaut.expressions.parser.ast.ExpressionNode; +import io.micronaut.expressions.parser.ast.access.BeanContextAccess; import io.micronaut.expressions.parser.ast.access.ContextElementAccess; import io.micronaut.expressions.parser.ast.access.ContextMethodCall; import io.micronaut.expressions.parser.ast.access.ElementMethodCall; @@ -60,8 +61,11 @@ import java.util.ArrayList; import java.util.List; +import static io.micronaut.expressions.parser.token.TokenType.AND; +import static io.micronaut.expressions.parser.token.TokenType.BEAN_CONTEXT; import static io.micronaut.expressions.parser.token.TokenType.BOOL; import static io.micronaut.expressions.parser.token.TokenType.COLON; +import static io.micronaut.expressions.parser.token.TokenType.COMMA; import static io.micronaut.expressions.parser.token.TokenType.DECREMENT; import static io.micronaut.expressions.parser.token.TokenType.DIV; import static io.micronaut.expressions.parser.token.TokenType.DOT; @@ -73,6 +77,7 @@ import static io.micronaut.expressions.parser.token.TokenType.FLOAT; import static io.micronaut.expressions.parser.token.TokenType.GT; import static io.micronaut.expressions.parser.token.TokenType.GTE; +import static io.micronaut.expressions.parser.token.TokenType.IDENTIFIER; import static io.micronaut.expressions.parser.token.TokenType.R_SQUARE; import static io.micronaut.expressions.parser.token.TokenType.TYPE_IDENTIFIER; import static io.micronaut.expressions.parser.token.TokenType.INCREMENT; @@ -183,8 +188,8 @@ private ExpressionNode orExpression() { // ; private ExpressionNode andExpression() { ExpressionNode leftNode = equalityExpression(); - while (lookahead != null && lookahead.type() == TokenType.AND) { - eat(TokenType.AND); + while (lookahead != null && lookahead.type() == AND) { + eat(AND); leftNode = new AndOperator(leftNode, equalityExpression()); } return leftNode; @@ -226,7 +231,7 @@ private ExpressionNode relationalExpression() { case LT -> new LtOperator(leftNode, additiveExpression()); case GTE -> new GteOperator(leftNode, additiveExpression()); case LTE -> new LteOperator(leftNode, additiveExpression()); - case INSTANCEOF -> new InstanceofOperator(leftNode, typeIdentifier()); + case INSTANCEOF -> new InstanceofOperator(leftNode, typeIdentifier(true)); case MATCHES -> new MatchesOperator(leftNode, stringLiteral()); default -> leftNode; }; @@ -323,6 +328,7 @@ private ExpressionNode unaryExpression() { // : PrimaryExpression // | PostfixExpression '.' MethodOrPropertyAccess // | PostfixExpression '?.' MethodOrPropertyAccess with safe navigation + // | PostfixExpression SubscriptOperator // | PostfixExpression '++' // | PostfixExpression '--' // ; @@ -344,7 +350,6 @@ private ExpressionNode postfixExpression() { eat(SAFE_NAV); leftNode = methodOrPropertyAccess(leftNode, true); } else if (tokenType == L_SQUARE) { - eat(L_SQUARE); leftNode = subscriptOperator(leftNode); } else { throw new ExpressionParsingException("Unexpected token: " + lookahead.value()); @@ -354,33 +359,36 @@ private ExpressionNode postfixExpression() { } // PrimaryExpression - // : ContextAccess + // : EvaluationContextAccess + // | BeanContextAccess // | TypeIdentifier // | ParenthesizedExpression // | Literal // ; private ExpressionNode primaryExpression() { return switch (lookahead.type()) { - case EXPRESSION_CONTEXT_REF -> contextAccess(); - case IDENTIFIER -> identifier(); - case TYPE_IDENTIFIER -> typeIdentifier(); + case EXPRESSION_CONTEXT_REF -> evaluationContextAccess(true); + case IDENTIFIER -> evaluationContextAccess(false); + case BEAN_CONTEXT -> beanContextAccess(); + case TYPE_IDENTIFIER -> typeIdentifier(true); case L_PAREN -> parenthesizedExpression(); case STRING, INT, LONG, DOUBLE, FLOAT, BOOL, NULL -> literal(); default -> throw new ExpressionParsingException("Unexpected token: " + lookahead.value()); }; } - // ContextAccess + // EvaluationContextAccess // : '#' Identifier // | '#' Identifier MethodArguments + // | Identifier + // | Identifier MethodArguments // ; - private ExpressionNode contextAccess() { - eat(EXPRESSION_CONTEXT_REF); - return primaryExpression(); - } + private ExpressionNode evaluationContextAccess(boolean prefixed) { + if (prefixed) { + eat(EXPRESSION_CONTEXT_REF); + } - private ExpressionNode identifier() { - String identifier = eat(TokenType.IDENTIFIER).value(); + String identifier = eat(IDENTIFIER).value(); if (lookahead != null && lookahead.type() == L_PAREN) { List methodArguments = methodArguments(); return new ContextMethodCall(identifier, methodArguments); @@ -388,12 +396,31 @@ private ExpressionNode identifier() { return new ContextElementAccess(identifier); } + // BeanContextAccess + // : 'ctx' '[' TypeIdentifier ']' + // ; + private ExpressionNode beanContextAccess() { + eat(BEAN_CONTEXT); + eat(L_SQUARE); + TypeIdentifier typeIdentifier; + if (lookahead != null) { + typeIdentifier = lookahead.type() == TYPE_IDENTIFIER + ? typeIdentifier(true) + : typeIdentifier(false); + } else { + throw new ExpressionParsingException("Bean context access must be followed by type reference"); + } + + eat(R_SQUARE); + return new BeanContextAccess(typeIdentifier); + } + // MethodOrFieldAccess // : SimpleIdentifier // | SimpleIdentifier MethodArguments // ; private ExpressionNode methodOrPropertyAccess(ExpressionNode callee, boolean nullSafe) { - String identifier = eat(TokenType.IDENTIFIER).value(); + String identifier = eat(IDENTIFIER).value(); if (lookahead != null && lookahead.type() == L_PAREN) { List methodArguments = methodArguments(); return new ElementMethodCall(callee, identifier, methodArguments, nullSafe); @@ -402,21 +429,16 @@ private ExpressionNode methodOrPropertyAccess(ExpressionNode callee, boolean nul } // SubscriptOperator - // : SimpleIdentifier - // | SimpleIdentifier [index] - // ; + // '[' Expression ']' private ExpressionNode subscriptOperator(ExpressionNode callee) { - if (lookahead != null) { - ExpressionNode indexExpression = expression(); - SubscriptOperator subscriptOperator = new SubscriptOperator( - callee, - indexExpression - ); - eat(R_SQUARE); - return subscriptOperator; - } else { - throw new ExpressionParsingException("Unclosed subscript operator"); - } + eat(L_SQUARE); + ExpressionNode indexExpression = expression(); + SubscriptOperator subscriptOperator = new SubscriptOperator( + callee, + indexExpression + ); + eat(R_SQUARE); + return subscriptOperator; } // MethodArguments: @@ -443,7 +465,7 @@ private List methodArgumentsList() { arguments.add(firstArgument); while (lookahead.type() != R_PAREN) { - eat(TokenType.COMMA); + eat(COMMA); arguments.add(expression()); } } @@ -453,15 +475,21 @@ private List methodArgumentsList() { // TypeReference // : 'T(' ChainedIdentifier')' // ; - private TypeIdentifier typeIdentifier() { - eat(TYPE_IDENTIFIER); + private TypeIdentifier typeIdentifier(boolean wrapped) { + if (wrapped) { + eat(TYPE_IDENTIFIER); + } + List parts = new ArrayList<>(); - parts.add(eat(TokenType.IDENTIFIER).value()); + parts.add(eat(IDENTIFIER).value()); while (lookahead != null && lookahead.type() == DOT) { eat(DOT); - parts.add(eat(TokenType.IDENTIFIER).value()); + parts.add(eat(IDENTIFIER).value()); + } + + if (wrapped) { + eat(R_PAREN); } - eat(R_PAREN); return new TypeIdentifier(String.join(".", parts)); } diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/access/BeanContextAccess.java b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/access/BeanContextAccess.java new file mode 100644 index 00000000000..f64a85170ba --- /dev/null +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/access/BeanContextAccess.java @@ -0,0 +1,73 @@ +/* + * Copyright 2017-2023 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.expressions.parser.ast.access; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.expressions.parser.ast.ExpressionNode; +import io.micronaut.expressions.parser.ast.types.TypeIdentifier; +import io.micronaut.expressions.parser.compilation.ExpressionVisitorContext; +import io.micronaut.inject.ast.ClassElement; +import org.objectweb.asm.Type; +import org.objectweb.asm.commons.GeneratorAdapter; +import org.objectweb.asm.commons.Method; + +import static io.micronaut.expressions.parser.ast.util.TypeDescriptors.EVALUATION_CONTEXT_TYPE; +import static org.objectweb.asm.Opcodes.CHECKCAST; + +/** + * Expression AST node used for to retrieve beans from bean context. + * + * @author Sergey Gavrilov + * @since 4.0.0 + */ +@Internal +public class BeanContextAccess extends ExpressionNode { + + private static final Method GET_BEAN_METHOD = + new Method("getBean", Type.getType(Object.class), + new Type[]{Type.getType(Class.class)}); + + private final TypeIdentifier typeIdentifier; + + public BeanContextAccess(TypeIdentifier typeIdentifier) { + this.typeIdentifier = typeIdentifier; + } + + @Override + protected void generateBytecode(ExpressionVisitorContext ctx) { + GeneratorAdapter mv = ctx.methodVisitor(); + mv.loadArg(0); + + Type beanType = typeIdentifier.resolveType(ctx); + mv.push(beanType); + + // invoke getBean method + mv.invokeInterface(EVALUATION_CONTEXT_TYPE, GET_BEAN_METHOD); + + // cast the return value to the correct type + mv.visitTypeInsn(CHECKCAST, beanType.getInternalName()); + } + + @Override + protected ClassElement doResolveClassElement(ExpressionVisitorContext ctx) { + return typeIdentifier.resolveClassElement(ctx); + } + + @Override + protected Type doResolveType(ExpressionVisitorContext ctx) { + return typeIdentifier.doResolveType(ctx); + } +} diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/token/TokenType.java b/core-processor/src/main/java/io/micronaut/expressions/parser/token/TokenType.java index 831aafca89f..ccfb2a89085 100644 --- a/core-processor/src/main/java/io/micronaut/expressions/parser/token/TokenType.java +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/token/TokenType.java @@ -27,6 +27,7 @@ public enum TokenType { WHITESPACE, IDENTIFIER, + BEAN_CONTEXT, TYPE_IDENTIFIER, EXPRESSION_CONTEXT_REF, DOT, diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/token/Tokenizer.java b/core-processor/src/main/java/io/micronaut/expressions/parser/token/Tokenizer.java index b7885e2ab18..7646b6f49f6 100644 --- a/core-processor/src/main/java/io/micronaut/expressions/parser/token/Tokenizer.java +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/token/Tokenizer.java @@ -26,6 +26,7 @@ import java.util.regex.Pattern; import static io.micronaut.expressions.parser.token.TokenType.AND; +import static io.micronaut.expressions.parser.token.TokenType.BEAN_CONTEXT; import static io.micronaut.expressions.parser.token.TokenType.BOOL; import static io.micronaut.expressions.parser.token.TokenType.COLON; import static io.micronaut.expressions.parser.token.TokenType.COMMA; @@ -94,6 +95,7 @@ public final class Tokenizer { "^instanceof\\b", INSTANCEOF, "^matches\\b", MATCHES, "^empty\\b", EMPTY, + "^ctx\\b", BEAN_CONTEXT, // LITERALS "^null\\b", NULL, // NULL diff --git a/inject-groovy/src/test/groovy/io/micronaut/expressions/BeanContextAccessExpressionsSpec.groovy b/inject-groovy/src/test/groovy/io/micronaut/expressions/BeanContextAccessExpressionsSpec.groovy new file mode 100644 index 00000000000..79ef7f12b50 --- /dev/null +++ b/inject-groovy/src/test/groovy/io/micronaut/expressions/BeanContextAccessExpressionsSpec.groovy @@ -0,0 +1,50 @@ +package io.micronaut.expressions + +import io.micronaut.ast.transform.test.AbstractEvaluatedExpressionsSpec + +class BeanContextAccessExpressionsSpec extends AbstractEvaluatedExpressionsSpec { + + void "test bean context access"() { + given: + def ctx = buildContext(""" + package test + + import io.micronaut.context.annotation.Value + + @jakarta.inject.Singleton + class AccessedBean { + + String firstValue() { + return "firstValue" + } + + String secondValue() { + return "secondValue" + } + + } + + @jakarta.inject.Singleton + class Expr { + + @Value("#{ ctx[T(test.AccessedBean)].firstValue() }") + String firstValue + + @Value("#{ ctx[test.AccessedBean].secondValue() }") + String secondValue + + } + """) + + def type = ctx.classLoader.loadClass('test.Expr') + def bean = ctx.getBean(type) + + expect: + bean.firstValue == 'firstValue' + bean.secondValue == 'secondValue' + + cleanup: + ctx.close() + } + +} diff --git a/inject-java/src/test/groovy/io/micronaut/expressions/BeanContextAccessExpressionsSpec.groovy b/inject-java/src/test/groovy/io/micronaut/expressions/BeanContextAccessExpressionsSpec.groovy new file mode 100644 index 00000000000..e168d6d6fdc --- /dev/null +++ b/inject-java/src/test/groovy/io/micronaut/expressions/BeanContextAccessExpressionsSpec.groovy @@ -0,0 +1,50 @@ +package io.micronaut.expressions + +import io.micronaut.annotation.processing.test.AbstractEvaluatedExpressionsSpec + +class BeanContextAccessExpressionsSpec extends AbstractEvaluatedExpressionsSpec { + + void "test bean context access"() { + given: + def ctx = buildContext(""" + package test; + + import io.micronaut.context.annotation.Value; + + @jakarta.inject.Singleton + class AccessedBean { + + String firstValue() { + return "firstValue"; + } + + String secondValue() { + return "secondValue"; + } + + } + + @jakarta.inject.Singleton + class Expr { + + @Value("#{ ctx[T(test.AccessedBean)].firstValue() }") + public String firstValue; + + @Value("#{ ctx[test.AccessedBean].secondValue() }") + public String secondValue; + + } + """) + + def type = ctx.classLoader.loadClass('test.Expr') + def bean = ctx.getBean(type) + + expect: + bean.firstValue == 'firstValue' + bean.secondValue == 'secondValue' + + cleanup: + ctx.close() + } + +} diff --git a/src/main/docs/guide/config/evaluatedExpressions.adoc b/src/main/docs/guide/config/evaluatedExpressions.adoc index 1e4babd3321..59e0b780b40 100644 --- a/src/main/docs/guide/config/evaluatedExpressions.adoc +++ b/src/main/docs/guide/config/evaluatedExpressions.adoc @@ -62,6 +62,7 @@ The Evaluated Expressions syntax supports the following functionality: * Type References * Method Invocation * Property Access +* Retrieving Beans from Bean Context === Literal Values @@ -193,7 +194,7 @@ limited. === Dot and Safe Navigation Operator -The dot operator can be use to access methods and properties of a value within an expression. For example: +The dot operator can be used to access methods and properties of a value within an expression. For example: .Dot operator usage [source] @@ -407,3 +408,16 @@ class ContextConsumer { } ---- + +==== Retrieving Beans from Bean Context + +A predefined syntax construct `ctx[...]` can be used to retrieve beans from bean +context. The argument inside square brackets has to be a fully qualified class name (note that `T(...)` wrapper is +optional and can be omitted for simplicity). + +.Retrieving beans from bean context +[source] +---- +#{ ctx[T(io.micronaut.example.ContextBean)] } +#{ ctx[io.micronaut.example.ContextBean] } +---- From 3614cbb895667863d783ac701675df8f523d0ef9 Mon Sep 17 00:00:00 2001 From: micronaut-build <65172877+micronaut-build@users.noreply.github.com> Date: Mon, 3 Apr 2023 11:36:03 +0200 Subject: [PATCH 652/743] Bump micronaut-kafka to 4.5.3 (#9036) --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index ec466cd980c..4472d92055a 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -87,7 +87,7 @@ managed-micronaut-ignite = "1.0.0.RC1" managed-micronaut-jaxrs = "3.4.0" managed-micronaut-jms = "2.1.0" managed-micronaut-jmx = "3.2.0" -managed-micronaut-kafka = "4.5.2" +managed-micronaut-kafka = "4.5.3" managed-micronaut-kotlin = "3.2.2" managed-micronaut-kubernetes = "3.4.0" managed-micronaut-micrometer = "4.7.2" From b386c030996c40f93fc074efc187cae8446b8476 Mon Sep 17 00:00:00 2001 From: Graeme Rocher Date: Mon, 3 Apr 2023 15:34:06 +0200 Subject: [PATCH 653/743] Support JDK 20 in annotation processors (#9022) (#9048) Co-authored-by: Denis Stepanov --- .../processing/AbstractInjectAnnotationProcessor.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/inject-java/src/main/java/io/micronaut/annotation/processing/AbstractInjectAnnotationProcessor.java b/inject-java/src/main/java/io/micronaut/annotation/processing/AbstractInjectAnnotationProcessor.java index 5e088701145..1f68fba099e 100644 --- a/inject-java/src/main/java/io/micronaut/annotation/processing/AbstractInjectAnnotationProcessor.java +++ b/inject-java/src/main/java/io/micronaut/annotation/processing/AbstractInjectAnnotationProcessor.java @@ -79,14 +79,14 @@ abstract class AbstractInjectAnnotationProcessor extends AbstractProcessor { @Override public SourceVersion getSupportedSourceVersion() { SourceVersion sourceVersion = SourceVersion.latest(); - if (sourceVersion.ordinal() <= 18) { + if (sourceVersion.ordinal() <= 20) { if (sourceVersion.ordinal() >= 8) { return sourceVersion; } else { return SourceVersion.RELEASE_8; } } else { - return (SourceVersion.values())[18]; + return (SourceVersion.values())[20]; } } From d4c6feb29d95c83b70fdf315879c8f8c648d1708 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 3 Apr 2023 16:12:03 +0200 Subject: [PATCH 654/743] chore(deps): update plugin com.google.devtools.ksp to v1.8.20-1.0.10 (#8676) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- inject-kotlin/build.gradle | 2 +- test-suite-kotlin-ksp/build.gradle | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/inject-kotlin/build.gradle b/inject-kotlin/build.gradle index 7262401dd69..e4777d27399 100644 --- a/inject-kotlin/build.gradle +++ b/inject-kotlin/build.gradle @@ -1,7 +1,7 @@ plugins { id "io.micronaut.build.internal.convention-library" id "org.jetbrains.kotlin.jvm" - id "com.google.devtools.ksp" version "1.8.0-Beta-1.0.8" + id "com.google.devtools.ksp" version "1.8.20-1.0.10" } diff --git a/test-suite-kotlin-ksp/build.gradle b/test-suite-kotlin-ksp/build.gradle index b6d64411c56..a615728ed98 100644 --- a/test-suite-kotlin-ksp/build.gradle +++ b/test-suite-kotlin-ksp/build.gradle @@ -1,7 +1,7 @@ plugins { id "io.micronaut.build.internal.convention-test-library" id "org.jetbrains.kotlin.jvm" - id("com.google.devtools.ksp") version "1.8.0-1.0.8" + id("com.google.devtools.ksp") version "1.8.20-1.0.10" } micronautBuild { From 9110bb2ff0d1b27b4cd62bd6b25c711476e03a2c Mon Sep 17 00:00:00 2001 From: micronaut-build <65172877+micronaut-build@users.noreply.github.com> Date: Mon, 3 Apr 2023 16:47:59 +0200 Subject: [PATCH 655/743] Bump micronaut-azure to 3.10.0 (#9050) --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index b2bf267ec68..cc45c105317 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -68,7 +68,7 @@ managed-micrometer = "1.10.3" managed-micronaut-acme = "3.2.0" managed-micronaut-aot = "1.1.2" managed-micronaut-aws = "3.15.0" -managed-micronaut-azure = "3.9.0" +managed-micronaut-azure = "3.10.0" managed-micronaut-cache = "3.5.0" managed-micronaut-cassandra = "5.1.1" managed-micronaut-coherence = "3.7.2" From e82e1fa2e485f34cd3902be6d2d0ebc35ddbe0e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcin=20Zaj=C4=85czkowski?= <148013+szpak@users.noreply.github.com> Date: Tue, 4 Apr 2023 11:39:10 +0200 Subject: [PATCH 656/743] Bump Spock to 2.3 and Groovy to 4.0.11 (#9054) --- gradle/libs.versions.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 2ae84742c42..57853e71ae6 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -47,7 +47,7 @@ micrometer = "1.10.2" neo4j-java-driver = "1.4.5" selenium = "4.7.2" smallrye = "5.5.0" -spock = "2.2-groovy-4.0" +spock = "2.3-groovy-4.0" spotbugs = "4.7.1" systemlambda = "1.2.1" testcontainers = "1.17.5" @@ -58,7 +58,7 @@ wiremock = "2.33.2" # Versions which start with managed- are managed by Micronaut in the sense # that they will appear in the Micronaut BOM as # -managed-groovy = "4.0.10" +managed-groovy = "4.0.11" managed-jakarta-annotation-api = "2.1.1" managed-jackson = "2.14.2" managed-jackson-databind = "2.14.1" From df65f6cebdc7e6ce40eb0cc4d17251603461994a Mon Sep 17 00:00:00 2001 From: micronaut-build <65172877+micronaut-build@users.noreply.github.com> Date: Tue, 4 Apr 2023 11:40:35 +0200 Subject: [PATCH 657/743] Bump micronaut-kubernetes to 4.0.0 (#9046) --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index cc45c105317..e0ad34a1bdf 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -89,7 +89,7 @@ managed-micronaut-jms = "2.1.0" managed-micronaut-jmx = "3.2.0" managed-micronaut-kafka = "4.5.2" managed-micronaut-kotlin = "3.2.2" -managed-micronaut-kubernetes = "3.4.0" +managed-micronaut-kubernetes = "4.0.0" managed-micronaut-micrometer = "4.8.2" managed-micronaut-microstream = "1.3.0" managed-micronaut-liquibase = "5.7.0" From 85bf53490582359a5324ef313796c4ee4fb75174 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 4 Apr 2023 11:40:56 +0200 Subject: [PATCH 658/743] fix(deps): update dependency org.testcontainers:spock to v1.17.6 (#9044) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 57853e71ae6..6fa93fe5516 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -50,7 +50,7 @@ smallrye = "5.5.0" spock = "2.3-groovy-4.0" spotbugs = "4.7.1" systemlambda = "1.2.1" -testcontainers = "1.17.5" +testcontainers = "1.17.6" vertx = "3.9.13" wiremock = "2.33.2" From 329010358c1a68118d80a9faf4896ff6f5ec5350 Mon Sep 17 00:00:00 2001 From: micronaut-build <65172877+micronaut-build@users.noreply.github.com> Date: Tue, 4 Apr 2023 11:42:40 +0200 Subject: [PATCH 659/743] Bump micronaut-liquibase to 5.7.1 (#9043) --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index e0ad34a1bdf..c4e8790fc43 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -92,7 +92,7 @@ managed-micronaut-kotlin = "3.2.2" managed-micronaut-kubernetes = "4.0.0" managed-micronaut-micrometer = "4.8.2" managed-micronaut-microstream = "1.3.0" -managed-micronaut-liquibase = "5.7.0" +managed-micronaut-liquibase = "5.7.1" managed-micronaut-mongo = "4.6.0" managed-micronaut-mqtt = "2.3.0" managed-micronaut-multitenancy = "4.2.0" From 7f23eb2fe9e01316b3a3655b0b31f9a48badd908 Mon Sep 17 00:00:00 2001 From: Sergey Gavrilov Date: Tue, 4 Apr 2023 13:11:16 +0300 Subject: [PATCH 660/743] retrieving environment properties in expressions (#9053) Allows retrieving properties in expressions using syntax ``` #{ env['test.property' ]} ``` Any expression resolving to string can be used inside square brackets (not only string literals). The expression itself returns string property value or null if value is not set --- ...gleEvaluatedEvaluatedExpressionParser.java | 15 ++++ .../parser/ast/access/EnvironmentAccess.java | 78 +++++++++++++++++++ .../expressions/parser/token/TokenType.java | 1 + .../expressions/parser/token/Tokenizer.java | 2 + .../ExpressionEvaluationContext.java | 9 +++ .../EnvironmentAccessExpressionsSpec.groovy | 51 ++++++++++++ .../EnvironmentAccessExpressionsSpec.groovy | 51 ++++++++++++ .../DefaultExpressionEvaluationContext.java | 12 +++ .../guide/config/evaluatedExpressions.adoc | 13 ++++ 9 files changed, 232 insertions(+) create mode 100644 core-processor/src/main/java/io/micronaut/expressions/parser/ast/access/EnvironmentAccess.java create mode 100644 inject-groovy/src/test/groovy/io/micronaut/expressions/EnvironmentAccessExpressionsSpec.groovy create mode 100644 inject-java/src/test/groovy/io/micronaut/expressions/EnvironmentAccessExpressionsSpec.groovy diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/SingleEvaluatedEvaluatedExpressionParser.java b/core-processor/src/main/java/io/micronaut/expressions/parser/SingleEvaluatedEvaluatedExpressionParser.java index cefd54b7855..8761a9d8781 100644 --- a/core-processor/src/main/java/io/micronaut/expressions/parser/SingleEvaluatedEvaluatedExpressionParser.java +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/SingleEvaluatedEvaluatedExpressionParser.java @@ -21,6 +21,7 @@ import io.micronaut.expressions.parser.ast.access.ContextElementAccess; import io.micronaut.expressions.parser.ast.access.ContextMethodCall; import io.micronaut.expressions.parser.ast.access.ElementMethodCall; +import io.micronaut.expressions.parser.ast.access.EnvironmentAccess; import io.micronaut.expressions.parser.ast.access.SubscriptOperator; import io.micronaut.expressions.parser.ast.access.PropertyAccess; import io.micronaut.expressions.parser.ast.conditional.ElvisOperator; @@ -72,6 +73,7 @@ import static io.micronaut.expressions.parser.token.TokenType.DOUBLE; import static io.micronaut.expressions.parser.token.TokenType.ELVIS; import static io.micronaut.expressions.parser.token.TokenType.EMPTY; +import static io.micronaut.expressions.parser.token.TokenType.ENVIRONMENT; import static io.micronaut.expressions.parser.token.TokenType.EQ; import static io.micronaut.expressions.parser.token.TokenType.EXPRESSION_CONTEXT_REF; import static io.micronaut.expressions.parser.token.TokenType.FLOAT; @@ -361,6 +363,7 @@ private ExpressionNode postfixExpression() { // PrimaryExpression // : EvaluationContextAccess // | BeanContextAccess + // | EnvironmentAccess // | TypeIdentifier // | ParenthesizedExpression // | Literal @@ -370,6 +373,7 @@ private ExpressionNode primaryExpression() { case EXPRESSION_CONTEXT_REF -> evaluationContextAccess(true); case IDENTIFIER -> evaluationContextAccess(false); case BEAN_CONTEXT -> beanContextAccess(); + case ENVIRONMENT -> environmentAccess(); case TYPE_IDENTIFIER -> typeIdentifier(true); case L_PAREN -> parenthesizedExpression(); case STRING, INT, LONG, DOUBLE, FLOAT, BOOL, NULL -> literal(); @@ -415,6 +419,17 @@ private ExpressionNode beanContextAccess() { return new BeanContextAccess(typeIdentifier); } + // EnvironmentAccess + // : 'env' '[' Expression ']' + // ; + private ExpressionNode environmentAccess() { + eat(ENVIRONMENT); + eat(L_SQUARE); + ExpressionNode propertyName = expression(); + eat(R_SQUARE); + return new EnvironmentAccess(propertyName); + } + // MethodOrFieldAccess // : SimpleIdentifier // | SimpleIdentifier MethodArguments diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/access/EnvironmentAccess.java b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/access/EnvironmentAccess.java new file mode 100644 index 00000000000..ce9cbd48607 --- /dev/null +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/access/EnvironmentAccess.java @@ -0,0 +1,78 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.expressions.parser.ast.access; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.expressions.parser.ast.ExpressionNode; +import io.micronaut.expressions.parser.compilation.ExpressionVisitorContext; +import io.micronaut.expressions.parser.exception.ExpressionCompilationException; +import io.micronaut.inject.ast.ClassElement; +import org.objectweb.asm.Type; +import org.objectweb.asm.commons.GeneratorAdapter; +import org.objectweb.asm.commons.Method; + +import static io.micronaut.expressions.parser.ast.util.TypeDescriptors.EVALUATION_CONTEXT_TYPE; +import static io.micronaut.expressions.parser.ast.util.TypeDescriptors.STRING; + +/** + * Expression AST node used for retrieving properties from environment at runtime. + * + * @author Sergey Gavrilov + * @since 4.0.0 + */ +@Internal +public final class EnvironmentAccess extends ExpressionNode { + + private static final ClassElement STRING_ELEMENT = ClassElement.of(String.class); + + private static final Method GET_PROPERTY_METHOD = + new Method("getProperty", Type.getType(String.class), + new Type[]{Type.getType(String.class)}); + + private final ExpressionNode propertyName; + + public EnvironmentAccess(ExpressionNode propertyName) { + this.propertyName = propertyName; + } + + @Override + protected void generateBytecode(ExpressionVisitorContext ctx) { + GeneratorAdapter mv = ctx.methodVisitor(); + mv.loadArg(0); + propertyName.compile(ctx); + // invoke getProperty method + mv.invokeInterface(EVALUATION_CONTEXT_TYPE, GET_PROPERTY_METHOD); + } + + @Override + protected ClassElement doResolveClassElement(ExpressionVisitorContext ctx) { + resolveType(ctx); + return STRING_ELEMENT; + } + + @Override + protected Type doResolveType(ExpressionVisitorContext ctx) { + Type propertyNameType = propertyName.resolveType(ctx); + if (!propertyNameType.equals(STRING)) { + throw new ExpressionCompilationException("Invalid environment access operation. The expression inside environment " + + "access must resolve to String value of property name"); + } + + // Property value is always returned as string + return STRING; + } + +} diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/token/TokenType.java b/core-processor/src/main/java/io/micronaut/expressions/parser/token/TokenType.java index ccfb2a89085..f7afe29208b 100644 --- a/core-processor/src/main/java/io/micronaut/expressions/parser/token/TokenType.java +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/token/TokenType.java @@ -28,6 +28,7 @@ public enum TokenType { WHITESPACE, IDENTIFIER, BEAN_CONTEXT, + ENVIRONMENT, TYPE_IDENTIFIER, EXPRESSION_CONTEXT_REF, DOT, diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/token/Tokenizer.java b/core-processor/src/main/java/io/micronaut/expressions/parser/token/Tokenizer.java index 7646b6f49f6..f78d4f1ffa8 100644 --- a/core-processor/src/main/java/io/micronaut/expressions/parser/token/Tokenizer.java +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/token/Tokenizer.java @@ -36,6 +36,7 @@ import static io.micronaut.expressions.parser.token.TokenType.DOUBLE; import static io.micronaut.expressions.parser.token.TokenType.ELVIS; import static io.micronaut.expressions.parser.token.TokenType.EMPTY; +import static io.micronaut.expressions.parser.token.TokenType.ENVIRONMENT; import static io.micronaut.expressions.parser.token.TokenType.EQ; import static io.micronaut.expressions.parser.token.TokenType.EXPRESSION_CONTEXT_REF; import static io.micronaut.expressions.parser.token.TokenType.IDENTIFIER; @@ -96,6 +97,7 @@ public final class Tokenizer { "^matches\\b", MATCHES, "^empty\\b", EMPTY, "^ctx\\b", BEAN_CONTEXT, + "^env\\b", ENVIRONMENT, // LITERALS "^null\\b", NULL, // NULL diff --git a/core/src/main/java/io/micronaut/core/expressions/ExpressionEvaluationContext.java b/core/src/main/java/io/micronaut/core/expressions/ExpressionEvaluationContext.java index 3c09ac6cc72..95741985980 100644 --- a/core/src/main/java/io/micronaut/core/expressions/ExpressionEvaluationContext.java +++ b/core/src/main/java/io/micronaut/core/expressions/ExpressionEvaluationContext.java @@ -16,6 +16,7 @@ package io.micronaut.core.expressions; import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.Nullable; /** * Context that can be used by evaluated expression to obtain objects required @@ -43,4 +44,12 @@ public interface ExpressionEvaluationContext extends AutoCloseable { * @return bean instance */ T getBean(Class type); + + /** + * Provides property by name. + * @param name property name + * @return property value or null + */ + @Nullable + String getProperty(String name); } diff --git a/inject-groovy/src/test/groovy/io/micronaut/expressions/EnvironmentAccessExpressionsSpec.groovy b/inject-groovy/src/test/groovy/io/micronaut/expressions/EnvironmentAccessExpressionsSpec.groovy new file mode 100644 index 00000000000..0d2bfc9508f --- /dev/null +++ b/inject-groovy/src/test/groovy/io/micronaut/expressions/EnvironmentAccessExpressionsSpec.groovy @@ -0,0 +1,51 @@ +package io.micronaut.expressions + +import io.micronaut.ast.transform.test.AbstractEvaluatedExpressionsSpec +import io.micronaut.context.env.PropertySource + +class EnvironmentAccessExpressionsSpec extends AbstractEvaluatedExpressionsSpec { + + void "test environment access"() { + given: + def ctx = buildContext(""" + package test + + import io.micronaut.context.annotation.Value + + @jakarta.inject.Singleton + class Expr { + + @Value("#{ env['first.property'] }") + String firstProperty + + @Value("#{ env['second' + '.' + 'property'] }") + String secondProperty + + @Value("#{ env [ 'third.property' ].toUpperCase() }") + String thirdProperty + + @Value("#{ env['nullable.property']?.toUpperCase() }") + String nullableProperty + + } + """) + + def type = ctx.classLoader.loadClass('test.Expr') + + ctx.environment.addPropertySource(PropertySource.of("test", + ['first.property': 'firstValue', + 'second.property': 'secondValue', + 'third.property': 'thirdValue'])) + + def bean = ctx.getBean(type) + + expect: + bean.firstProperty == 'firstValue' + bean.secondProperty == 'secondValue' + bean.thirdProperty == 'THIRDVALUE' + bean.nullableProperty == null + + cleanup: + ctx.close() + } +} diff --git a/inject-java/src/test/groovy/io/micronaut/expressions/EnvironmentAccessExpressionsSpec.groovy b/inject-java/src/test/groovy/io/micronaut/expressions/EnvironmentAccessExpressionsSpec.groovy new file mode 100644 index 00000000000..88034465269 --- /dev/null +++ b/inject-java/src/test/groovy/io/micronaut/expressions/EnvironmentAccessExpressionsSpec.groovy @@ -0,0 +1,51 @@ +package io.micronaut.expressions + +import io.micronaut.annotation.processing.test.AbstractEvaluatedExpressionsSpec +import io.micronaut.context.env.PropertySource + +class EnvironmentAccessExpressionsSpec extends AbstractEvaluatedExpressionsSpec { + + void "test environment access"() { + given: + def ctx = buildContext(""" + package test; + + import io.micronaut.context.annotation.Value; + + @jakarta.inject.Singleton + class Expr { + + @Value("#{ env['first.property'] }") + public String firstProperty; + + @Value("#{ env['second' + '.' + 'property'] }") + public String secondProperty; + + @Value("#{ env [ 'third.property' ].toUpperCase() }") + public String thirdProperty; + + @Value("#{ env['nullable.property']?.toUpperCase() }") + public String nullableProperty; + + } + """) + + def type = ctx.classLoader.loadClass('test.Expr') + + ctx.environment.addPropertySource(PropertySource.of("test", + ['first.property': 'firstValue', + 'second.property': 'secondValue', + 'third.property': 'thirdValue'])) + + def bean = ctx.getBean(type) + + expect: + bean.firstProperty == 'firstValue' + bean.secondProperty == 'secondValue' + bean.thirdProperty == 'THIRDVALUE' + bean.nullableProperty == null + + cleanup: + ctx.close() + } +} diff --git a/inject/src/main/java/io/micronaut/context/expressions/DefaultExpressionEvaluationContext.java b/inject/src/main/java/io/micronaut/context/expressions/DefaultExpressionEvaluationContext.java index ac164a63ed1..09e0e11c8bd 100644 --- a/inject/src/main/java/io/micronaut/context/expressions/DefaultExpressionEvaluationContext.java +++ b/inject/src/main/java/io/micronaut/context/expressions/DefaultExpressionEvaluationContext.java @@ -15,6 +15,7 @@ */ package io.micronaut.context.expressions; +import io.micronaut.context.ApplicationContext; import io.micronaut.context.BeanContext; import io.micronaut.context.BeanRegistration; import io.micronaut.context.BeanResolutionContext; @@ -87,6 +88,17 @@ public Object getArgument(int index) { return args[index]; } + @Override + public String getProperty(String name) { + if (beanContext == null || !(beanContext instanceof ApplicationContext applicationContext)) { + throw new ExpressionEvaluationException("Can not obtain environment property [" + name + "] " + + "since application context is not set"); + } + + return applicationContext.getProperty(name, String.class) + .orElse(null); + } + @Override public T getBean(Class type) { if (beanContext == null) { diff --git a/src/main/docs/guide/config/evaluatedExpressions.adoc b/src/main/docs/guide/config/evaluatedExpressions.adoc index 59e0b780b40..4bcd1fe46ba 100644 --- a/src/main/docs/guide/config/evaluatedExpressions.adoc +++ b/src/main/docs/guide/config/evaluatedExpressions.adoc @@ -63,6 +63,7 @@ The Evaluated Expressions syntax supports the following functionality: * Method Invocation * Property Access * Retrieving Beans from Bean Context +* Retrieving Environment Properties === Literal Values @@ -421,3 +422,15 @@ optional and can be omitted for simplicity). #{ ctx[T(io.micronaut.example.ContextBean)] } #{ ctx[io.micronaut.example.ContextBean] } ---- + +==== Retrieving Environment Properties + +A syntax construct `env[...]` can be used to retrieve environment properties by name. +The expression inside square brackets has to resolve to string value, otherwise compilation will fail. If property +value will be absent at runtime, the expression will return `null` + +.Retrieving Environment Properties +[source] +---- +#{ env['test.property'] } +---- From 46858e7dd03ce7c730b190a7d49800878df2a81b Mon Sep 17 00:00:00 2001 From: micronaut-build <65172877+micronaut-build@users.noreply.github.com> Date: Tue, 4 Apr 2023 17:17:48 +0200 Subject: [PATCH 661/743] Bump micronaut-gcp to 4.10.0 (#9058) --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 19598c49ebb..849dd39166c 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -78,7 +78,7 @@ managed-micronaut-discovery = "3.3.0" managed-micronaut-elasticsearch = "4.3.0" managed-micronaut-email = "1.5.0" managed-micronaut-flyway = "5.5.0" -managed-micronaut-gcp = "4.9.0" +managed-micronaut-gcp = "4.10.0" managed-micronaut-graphql = "3.2.0" managed-micronaut-groovy = "3.4.0" managed-micronaut-grpc = "3.5.0" From 96eef38e6c69d3d4ba8abb903c64762daa264bde Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Tue, 4 Apr 2023 18:33:19 +0200 Subject: [PATCH 662/743] build: Kotlin 1.8.20 (#9055) * build: Kotlin 1.8.2 * build: ksp 1.8.20-1.0.10 --- gradle/libs.versions.toml | 4 ++-- src/main/docs/guide/introduction/whatsNew.adoc | 2 +- src/main/docs/guide/languageSupport/kotlin/ksp.adoc | 12 ++++++------ 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 6fa93fe5516..433002ec3cd 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -62,7 +62,7 @@ managed-groovy = "4.0.11" managed-jakarta-annotation-api = "2.1.1" managed-jackson = "2.14.2" managed-jackson-databind = "2.14.1" -managed-kotlin = "1.8.10" +managed-kotlin = "1.8.20" managed-kotlin-coroutines = "1.6.4" managed-maven-native-plugin = "0.9.13" managed-methvin-directory-watcher = "0.16.1" @@ -74,7 +74,7 @@ managed-reactor = "3.4.24" managed-slf4j = "2.0.7" managed-snakeyaml = "2.0" managed-java-parser-core = "3.24.9" -managed-ksp = "1.8.0-1.0.9" +managed-ksp = "1.8.20-1.0.10" micronaut-docs = "2.0.0" [libraries] diff --git a/src/main/docs/guide/introduction/whatsNew.adoc b/src/main/docs/guide/introduction/whatsNew.adoc index 27e901b81aa..fcc536ee315 100644 --- a/src/main/docs/guide/introduction/whatsNew.adoc +++ b/src/main/docs/guide/introduction/whatsNew.adoc @@ -81,7 +81,7 @@ See <> === Other Dependency Upgrades -- Kotlin 1.8.10 +- Kotlin 1.8.20 <> diff --git a/src/main/docs/guide/languageSupport/kotlin/ksp.adoc b/src/main/docs/guide/languageSupport/kotlin/ksp.adoc index 7fa203a2117..2d69b588f72 100644 --- a/src/main/docs/guide/languageSupport/kotlin/ksp.adoc +++ b/src/main/docs/guide/languageSupport/kotlin/ksp.adoc @@ -10,9 +10,9 @@ If you use the https://micronaut-projects.github.io/micronaut-gradle-plugin/late .build.gradle.kts ---- plugins { - id("org.jetbrains.kotlin.jvm") version "1.8.10" - id("com.google.devtools.ksp") version "1.8.10-1.0.9" - id("org.jetbrains.kotlin.plugin.allopen") version "1.8.10" + id("org.jetbrains.kotlin.jvm") version "1.8.20" + id("com.google.devtools.ksp") version "1.8.20-1.0.10" + id("org.jetbrains.kotlin.plugin.allopen") version "1.8.20" id("io.micronaut.application") version "4.0.0" } version = "0.1" @@ -61,9 +61,9 @@ If you don't use the https://micronaut-projects.github.io/micronaut-gradle-plugi [source, kotlin] ---- plugins { - id("org.jetbrains.kotlin.jvm") version "1.8.10" - id("com.google.devtools.ksp") version "1.8.10-1.0.9" - id("org.jetbrains.kotlin.plugin.allopen") version "1.8.10" + id("org.jetbrains.kotlin.jvm") version "1.8.20" + id("com.google.devtools.ksp") version "1.8.20-1.0.10" + id("org.jetbrains.kotlin.plugin.allopen") version "1.8.20" application } version = "0.1" From 9338ca6f6a7bbf83b885b37e526c044be38b1c19 Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Wed, 5 Apr 2023 09:35:01 +0200 Subject: [PATCH 663/743] build: add Micronaut Chatbots BOM (#9056) --- gradle/libs.versions.toml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 849dd39166c..da8d03625ac 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -70,6 +70,7 @@ managed-micronaut-aot = "1.1.2" managed-micronaut-aws = "3.15.0" managed-micronaut-azure = "3.10.0" managed-micronaut-cache = "3.5.0" +managed-micronaut-chatbots = "1.0.0-M1" managed-micronaut-cassandra = "5.1.1" managed-micronaut-coherence = "3.7.2" managed-micronaut-crac = "1.2.1" @@ -150,6 +151,7 @@ micronaut-docs = "2.0.0" boms-micronaut-aws = { module = "io.micronaut.aws:micronaut-aws-bom", version.ref = "managed-micronaut-aws" } boms-micronaut-azure = { module = "io.micronaut.azure:micronaut-azure-bom", version.ref = "managed-micronaut-azure" } boms-micronaut-cache = { module = "io.micronaut.cache:micronaut-cache-bom", version.ref = "managed-micronaut-cache" } +boms-micronaut-chatbots = { module = "io.micronaut.chatbots:micronaut-chatbots-bom", version.ref = "managed-micronaut-chatbots" } boms-micronaut-coherence = { module = "io.micronaut.coherence:micronaut-coherence-bom", version.ref = "managed-micronaut-coherence" } boms-micronaut-crac = { module = "io.micronaut.crac:micronaut-crac-bom", version.ref = "managed-micronaut-crac" } boms-micronaut-email = { module = "io.micronaut.email:micronaut-email-bom", version.ref = "managed-micronaut-email" } From 7cb5068b4beb3dd0303be08bd0e31da16d0e9f0f Mon Sep 17 00:00:00 2001 From: Graeme Rocher Date: Wed, 5 Apr 2023 10:27:53 +0200 Subject: [PATCH 664/743] add ability to disable streaming http request processing (#9006) Adds a new `micronaut.server.netty.server-type` setting that can be set to `FULL_CONTENT` to disable streaming requests. This will make it easier to implement certain use cases like retrieving the body from a filter and also improves performance. This PR also optimises route execution further to improvement the overall performance of the framework. --------- Co-authored-by: yawkat Co-authored-by: Denis Stepanov --- .../RequestArgumentSatisfierBenchmark.java | 6 +- .../server/stack/FullHttpStackBenchmark.java | 2 + .../executor/DefaultExecutorSelector.java | 2 +- .../scheduling/executor/ExecutorSelector.java | 2 +- .../core/async/publisher/Publishers.java | 2 +- .../core/annotation/AnnotationMetadata.java | 12 + .../micronaut/core/bind/ArgumentBinder.java | 21 +- .../core/bind/ArgumentBinderRegistry.java | 28 +- .../core/bind/DefaultExecutableBinder.java | 7 +- .../AbstractAnnotatedArgumentBinder.java | 127 +--- .../annotation/AbstractArgumentBinder.java | 186 ++++++ .../DefaultMutableConversionService.java | 2 +- .../core/bind/ExecutableBinderSpec.groovy | 6 +- .../jdk/OptionsRequestAttributesSpec.groovy | 1 - .../http/client/netty/DefaultHttpClient.java | 1 + .../client/netty/NettyClientHttpRequest.java | 5 +- .../micronaut/http/client/HttpPostSpec.groovy | 10 +- .../http/netty/NettyHttpHeaders.java | 39 +- .../http/netty/NettyMutableHttpResponse.java | 30 +- .../http/netty/reactive/HandlerPublisher.java | 10 +- .../StreamingInboundHttp2ToHttpAdapter.java | 407 ------------- .../http/netty/NettyHttpHeadersSpec.groovy | 28 + .../http/server/netty/BaseRouteCompleter.java | 107 ---- .../netty/DefaultHttpContentProcessor.java | 8 - .../DefaultHttpContentProcessorResolver.java | 22 +- .../netty/FormDataHttpContentProcessor.java | 10 +- .../http/server/netty/FormRouteCompleter.java | 417 ++++++++------ .../server/netty/HttpContentProcessor.java | 14 + ...tpContentProcessorAsReactiveProcessor.java | 9 +- .../server/netty/HttpPipelineBuilder.java | 16 +- .../http/server/netty/MicronautHttpData.java | 7 +- ...ronautHttpPostMultipartRequestDecoder.java | 49 -- .../http/server/netty/NettyHttpRequest.java | 335 +++++------ .../netty/NettyRequestArgumentSatisfier.java | 16 +- .../server/netty/NettyRequestLifecycle.java | 185 ++---- .../server/netty/RoutingInBoundHandler.java | 57 +- .../binders/CompletableFutureBodyBinder.java | 113 ++-- .../binders/CompletedFileUploadBinder.java | 86 +++ .../netty/binders/InputStreamBodyBinder.java | 120 +--- .../netty/binders/NettyBinderRegistrar.java | 36 +- .../binders/NettyRequestArgumentBinder.java | 52 ++ .../binders/PartUploadAnnotationBinder.java | 99 ++++ .../netty/binders/PublisherBodyBinder.java | 172 +++--- .../binders/PublisherPartUploadBinder.java | 110 ++++ .../StreamedNettyRequestArgumentBinder.java | 59 ++ .../binders/StreamingFileUploadBinder.java | 81 +++ .../http/server/netty/body/ByteBody.java | 66 +++ .../http/server/netty/body/HttpBody.java | 44 ++ .../server/netty/body/ImmediateByteBody.java | 95 +++ .../netty/body/ImmediateMultiObjectBody.java | 146 +++++ .../netty/body/ImmediateSingleObjectBody.java | 92 +++ .../http/server/netty/body/ManagedBody.java | 118 ++++ .../server/netty/body/MultiObjectBody.java | 78 +++ .../server/netty/body/StreamingByteBody.java | 171 ++++++ .../netty/body/StreamingMultiObjectBody.java | 278 +++++++++ .../NettyHttpServerConfiguration.java | 62 ++ .../netty/converters/NettyConvertersSpi.java | 2 +- .../netty/decoders/HttpRequestDecoder.java | 17 +- .../netty/jackson/JsonContentProcessor.java | 34 +- .../MultipartBodyArgumentBinder.java | 124 ++-- .../multipart/NettyCompletedFileUpload.java | 1 - .../multipart/NettyStreamingFileUpload.java | 2 +- .../NettyServerWebSocketHandler.java | 1 - .../server/netty/MaxRequestSizeSpec.groovy | 6 +- .../netty/OptionsRequestAttributesSpec.groovy | 3 - .../netty/binding/ByteBufferSpec.groovy | 15 +- .../binding/FullJsonBodyBindingSpec.groovy | 11 + .../server/netty/cors/CorsFilterSpec.groovy | 49 +- .../netty/stream/FluxFullBodySpec.groovy | 40 ++ .../stream/InputStreamFullBodySpec.groovy | 97 ++++ .../JsonContentProcessorBenchmark.java | 1 - .../http/server/ExecutableRouteInfo.java | 20 +- .../http/server/RequestLifecycle.java | 61 +- .../micronaut/http/server/RouteExecutor.java | 103 ++-- .../binding/RequestArgumentSatisfier.java | 118 +--- .../io/micronaut/http/HttpAttributes.java | 7 +- .../java/io/micronaut/http/MediaType.java | 17 + .../bind/DefaultRequestBinderRegistry.java | 63 +- .../http/bind/RequestBinderRegistry.java | 9 + .../AnnotatedRequestArgumentBinder.java | 2 +- .../bind/binders/CookieAnnotationBinder.java | 6 +- .../binders/DefaultBodyAnnotationBinder.java | 72 +-- ...DefaultUnmatchedRequestArgumentBinder.java | 120 ++++ .../bind/binders/HeaderAnnotationBinder.java | 6 +- .../binders/ParameterAnnotationBinder.java | 110 ---- .../bind/binders/PartAnnotationBinder.java | 11 +- .../binders/PathVariableAnnotationBinder.java | 55 +- .../binders/PendingRequestBindingResult.java | 51 ++ .../PostponedRequestArgumentBinder.java | 43 ++ .../binders/QueryValueArgumentBinder.java | 84 ++- .../RequestAttributeAnnotationBinder.java | 10 +- .../binders/RequestBeanAnnotationBinder.java | 25 +- .../binders/TypedRequestArgumentBinder.java | 17 + .../UnmatchedRequestArgumentBinder.java | 19 +- .../execution/ReactorExecutionFlowImpl.java | 7 +- .../micronaut/context/DefaultBeanContext.java | 6 +- .../io/micronaut/inject/ExecutionHandle.java | 4 +- .../inject/MethodExecutionHandle.java | 2 +- .../management/endpoint/routes/RouteData.java | 6 +- .../endpoint/routes/RouteDataCollector.java | 7 +- .../endpoint/routes/RoutesEndpoint.java | 8 +- .../routes/impl/DefaultRouteData.java | 17 +- .../impl/DefaultRouteDataCollector.java | 6 +- .../web/router/AbstractRouteMatch.java | 541 ++++++++++-------- .../web/router/BasicObjectRouteMatch.java | 108 ---- .../web/router/DefaultErrorRouteInfo.java | 128 +++++ .../router/DefaultMethodBasedRouteInfo.java | 163 ++++++ .../web/router/DefaultRequestMatcher.java | 64 +++ .../web/router/DefaultRouteBuilder.java | 330 ++++------- .../web/router/DefaultRouteInfo.java | 291 ++++++++++ .../micronaut/web/router/DefaultRouter.java | 308 ++++++---- .../web/router/DefaultStatusRouteInfo.java | 126 ++++ .../web/router/DefaultUriRouteMatch.java | 69 +-- .../web/router/DefaultUrlRouteInfo.java | 109 ++++ .../io/micronaut/web/router/ErrorRoute.java | 27 +- .../micronaut/web/router/ErrorRouteInfo.java | 56 ++ .../micronaut/web/router/ErrorRouteMatch.java | 54 +- ...edRoute.java => MethodBasedRouteInfo.java} | 17 +- ...{NullArgument.java => RequestMatcher.java} | 20 +- .../java/io/micronaut/web/router/Route.java | 2 + .../io/micronaut/web/router/RouteInfo.java | 191 ++++--- .../io/micronaut/web/router/RouteMatch.java | 141 ++--- .../java/io/micronaut/web/router/Router.java | 32 +- .../io/micronaut/web/router/StatusRoute.java | 28 +- .../micronaut/web/router/StatusRouteInfo.java | 60 ++ .../web/router/StatusRouteMatch.java | 65 +-- .../io/micronaut/web/router/UriRoute.java | 29 +- .../io/micronaut/web/router/UriRouteInfo.java | 79 +++ .../micronaut/web/router/UriRouteMatch.java | 15 +- .../web/router/filter/FilteredRouter.java | 9 +- .../router/GroovyRouteBuilderSpec.groovy | 15 +- .../context/router/RouteBuilderTests.java | 22 +- .../docs/server/upload/UploadSpec.groovy | 4 +- .../server/upload/UploadControllerSpec.kt | 6 +- .../server/upload/UploadControllerSpec.kt | 6 +- .../server/upload/UploadControllerSpec.java | 6 +- .../bind/WebSocketStateBinderRegistry.java | 33 +- 137 files changed, 5409 insertions(+), 3504 deletions(-) create mode 100644 core/src/main/java/io/micronaut/core/bind/annotation/AbstractArgumentBinder.java delete mode 100644 http-netty/src/main/java/io/micronaut/http/netty/stream/StreamingInboundHttp2ToHttpAdapter.java create mode 100644 http-netty/src/test/groovy/io/micronaut/http/netty/NettyHttpHeadersSpec.groovy delete mode 100644 http-server-netty/src/main/java/io/micronaut/http/server/netty/BaseRouteCompleter.java delete mode 100644 http-server-netty/src/main/java/io/micronaut/http/server/netty/MicronautHttpPostMultipartRequestDecoder.java create mode 100644 http-server-netty/src/main/java/io/micronaut/http/server/netty/binders/CompletedFileUploadBinder.java create mode 100644 http-server-netty/src/main/java/io/micronaut/http/server/netty/binders/NettyRequestArgumentBinder.java create mode 100644 http-server-netty/src/main/java/io/micronaut/http/server/netty/binders/PartUploadAnnotationBinder.java create mode 100644 http-server-netty/src/main/java/io/micronaut/http/server/netty/binders/PublisherPartUploadBinder.java create mode 100644 http-server-netty/src/main/java/io/micronaut/http/server/netty/binders/StreamedNettyRequestArgumentBinder.java create mode 100644 http-server-netty/src/main/java/io/micronaut/http/server/netty/binders/StreamingFileUploadBinder.java create mode 100644 http-server-netty/src/main/java/io/micronaut/http/server/netty/body/ByteBody.java create mode 100644 http-server-netty/src/main/java/io/micronaut/http/server/netty/body/HttpBody.java create mode 100644 http-server-netty/src/main/java/io/micronaut/http/server/netty/body/ImmediateByteBody.java create mode 100644 http-server-netty/src/main/java/io/micronaut/http/server/netty/body/ImmediateMultiObjectBody.java create mode 100644 http-server-netty/src/main/java/io/micronaut/http/server/netty/body/ImmediateSingleObjectBody.java create mode 100644 http-server-netty/src/main/java/io/micronaut/http/server/netty/body/ManagedBody.java create mode 100644 http-server-netty/src/main/java/io/micronaut/http/server/netty/body/MultiObjectBody.java create mode 100644 http-server-netty/src/main/java/io/micronaut/http/server/netty/body/StreamingByteBody.java create mode 100644 http-server-netty/src/main/java/io/micronaut/http/server/netty/body/StreamingMultiObjectBody.java create mode 100644 http-server-netty/src/test/groovy/io/micronaut/http/server/netty/binding/FullJsonBodyBindingSpec.groovy create mode 100644 http-server-netty/src/test/groovy/io/micronaut/http/server/netty/stream/FluxFullBodySpec.groovy create mode 100644 http-server-netty/src/test/groovy/io/micronaut/http/server/netty/stream/InputStreamFullBodySpec.groovy create mode 100644 http/src/main/java/io/micronaut/http/bind/binders/DefaultUnmatchedRequestArgumentBinder.java delete mode 100644 http/src/main/java/io/micronaut/http/bind/binders/ParameterAnnotationBinder.java create mode 100644 http/src/main/java/io/micronaut/http/bind/binders/PendingRequestBindingResult.java create mode 100644 http/src/main/java/io/micronaut/http/bind/binders/PostponedRequestArgumentBinder.java rename router/src/main/java/io/micronaut/web/router/UnresolvedArgument.java => http/src/main/java/io/micronaut/http/bind/binders/UnmatchedRequestArgumentBinder.java (57%) delete mode 100644 router/src/main/java/io/micronaut/web/router/BasicObjectRouteMatch.java create mode 100644 router/src/main/java/io/micronaut/web/router/DefaultErrorRouteInfo.java create mode 100644 router/src/main/java/io/micronaut/web/router/DefaultMethodBasedRouteInfo.java create mode 100644 router/src/main/java/io/micronaut/web/router/DefaultRequestMatcher.java create mode 100644 router/src/main/java/io/micronaut/web/router/DefaultRouteInfo.java create mode 100644 router/src/main/java/io/micronaut/web/router/DefaultStatusRouteInfo.java create mode 100644 router/src/main/java/io/micronaut/web/router/DefaultUrlRouteInfo.java create mode 100644 router/src/main/java/io/micronaut/web/router/ErrorRouteInfo.java rename router/src/main/java/io/micronaut/web/router/{MethodBasedRoute.java => MethodBasedRouteInfo.java} (60%) rename router/src/main/java/io/micronaut/web/router/{NullArgument.java => RequestMatcher.java} (67%) create mode 100644 router/src/main/java/io/micronaut/web/router/StatusRouteInfo.java create mode 100644 router/src/main/java/io/micronaut/web/router/UriRouteInfo.java diff --git a/benchmarks/src/jmh/java/io/micronaut/http/server/binding/RequestArgumentSatisfierBenchmark.java b/benchmarks/src/jmh/java/io/micronaut/http/server/binding/RequestArgumentSatisfierBenchmark.java index 2acdbe8ac85..7bcc2e06519 100644 --- a/benchmarks/src/jmh/java/io/micronaut/http/server/binding/RequestArgumentSatisfierBenchmark.java +++ b/benchmarks/src/jmh/java/io/micronaut/http/server/binding/RequestArgumentSatisfierBenchmark.java @@ -19,7 +19,6 @@ import io.micronaut.context.ApplicationContext; import io.micronaut.http.HttpRequest; import io.micronaut.http.MutableHttpRequest; -import io.micronaut.web.router.RouteMatch; import io.micronaut.web.router.Router; import io.micronaut.web.router.UriRouteMatch; import org.openjdk.jmh.annotations.Benchmark; @@ -49,10 +48,9 @@ public void setup() { public void benchmarkFulfillArgumentRequirements() { final MutableHttpRequest request = HttpRequest.GET("/arguments/foo/bar/10"); final UriRouteMatch routeMatch = router.find(request.getMethod(), request.getUri().toString(), request).findFirst().orElse(null); - final RouteMatch transformed = requestArgumentSatisfier.fulfillArgumentRequirements( + requestArgumentSatisfier.fulfillArgumentRequirementsBeforeFilters( routeMatch, - request, - true + request ); } diff --git a/benchmarks/src/jmh/java/io/micronaut/http/server/stack/FullHttpStackBenchmark.java b/benchmarks/src/jmh/java/io/micronaut/http/server/stack/FullHttpStackBenchmark.java index ee684837918..4dbea6d71fc 100644 --- a/benchmarks/src/jmh/java/io/micronaut/http/server/stack/FullHttpStackBenchmark.java +++ b/benchmarks/src/jmh/java/io/micronaut/http/server/stack/FullHttpStackBenchmark.java @@ -2,6 +2,7 @@ import io.micronaut.context.ApplicationContext; import io.micronaut.http.server.netty.NettyHttpServer; +import io.micronaut.http.server.netty.configuration.NettyHttpServerConfiguration; import io.micronaut.runtime.server.EmbeddedServer; import io.netty.buffer.ByteBuf; import io.netty.buffer.CompositeByteBuf; @@ -164,6 +165,7 @@ public enum StackFactory { Stack openChannel() { ApplicationContext ctx = ApplicationContext.run(Map.of( "spec.name", "FullHttpStackBenchmark", + "micronaut.server.netty.server-type", NettyHttpServerConfiguration.HttpServerType.FULL_CONTENT, "micronaut.server.date-header", false // disabling this makes the response identical each time )); EmbeddedServer server = ctx.getBean(EmbeddedServer.class); diff --git a/context/src/main/java/io/micronaut/scheduling/executor/DefaultExecutorSelector.java b/context/src/main/java/io/micronaut/scheduling/executor/DefaultExecutorSelector.java index 0dba088526c..92fb0247e0e 100644 --- a/context/src/main/java/io/micronaut/scheduling/executor/DefaultExecutorSelector.java +++ b/context/src/main/java/io/micronaut/scheduling/executor/DefaultExecutorSelector.java @@ -66,7 +66,7 @@ protected DefaultExecutorSelector( } @Override - public Optional select(MethodReference method, ThreadSelection threadSelection) { + public Optional select(MethodReference method, ThreadSelection threadSelection) { final String name = method.stringValue(EXECUTE_ON).orElse(null); if (name != null) { final ExecutorService executorService; diff --git a/context/src/main/java/io/micronaut/scheduling/executor/ExecutorSelector.java b/context/src/main/java/io/micronaut/scheduling/executor/ExecutorSelector.java index 7c5c3c91b28..1129635e685 100644 --- a/context/src/main/java/io/micronaut/scheduling/executor/ExecutorSelector.java +++ b/context/src/main/java/io/micronaut/scheduling/executor/ExecutorSelector.java @@ -36,7 +36,7 @@ public interface ExecutorSelector { * @return An optional {@link ExecutorService}. If an {@link ExecutorService} cannot be established * {@link Optional#empty()} is returned */ - Optional select(MethodReference method, ThreadSelection threadSelection); + Optional select(MethodReference method, ThreadSelection threadSelection); /** * Obtain executor for the given name. diff --git a/core-reactive/src/main/java/io/micronaut/core/async/publisher/Publishers.java b/core-reactive/src/main/java/io/micronaut/core/async/publisher/Publishers.java index 8be1f64e3c1..ed547442775 100644 --- a/core-reactive/src/main/java/io/micronaut/core/async/publisher/Publishers.java +++ b/core-reactive/src/main/java/io/micronaut/core/async/publisher/Publishers.java @@ -426,7 +426,7 @@ public static boolean isConvertibleToPublisher(Class type) { if (Publisher.class.isAssignableFrom(type)) { return true; } else { - if (type.isPrimitive() || packageOf(type).startsWith("java.")) { + if (type.isPrimitive() || type.getName().startsWith("java.")) { return false; } for (Class reactiveType : REACTIVE_TYPES) { diff --git a/core/src/main/java/io/micronaut/core/annotation/AnnotationMetadata.java b/core/src/main/java/io/micronaut/core/annotation/AnnotationMetadata.java index 75817324561..9efc6a237d1 100644 --- a/core/src/main/java/io/micronaut/core/annotation/AnnotationMetadata.java +++ b/core/src/main/java/io/micronaut/core/annotation/AnnotationMetadata.java @@ -1504,6 +1504,18 @@ default boolean hasStereotype(@Nullable Class annotation) return false; } + /** + * Faster version of {@link #hasStereotype(Class)} that does not support repeatable + * annotations. + * + * @param annotation The annotation type + * @return Whether this metadata has the given stereotype + */ + @Internal + default boolean hasStereotypeNonRepeating(@NonNull Class annotation) { + return hasStereotype(annotation.getName()); + } + /** * Check whether any of the given stereotypes is present. * diff --git a/core/src/main/java/io/micronaut/core/bind/ArgumentBinder.java b/core/src/main/java/io/micronaut/core/bind/ArgumentBinder.java index 078143a8950..f353738ec9f 100644 --- a/core/src/main/java/io/micronaut/core/bind/ArgumentBinder.java +++ b/core/src/main/java/io/micronaut/core/bind/ArgumentBinder.java @@ -59,6 +59,7 @@ public interface ArgumentBinder { * @param */ interface BindingResult { + /** * An empty but satisfied result. */ @@ -95,7 +96,7 @@ default List getConversionErrors() { * @return Was the binding requirement satisfied */ default boolean isSatisfied() { - return getConversionErrors() == Collections.EMPTY_LIST; + return getConversionErrors().isEmpty(); } /** @@ -114,5 +115,23 @@ default boolean isPresentAndSatisfied() { default T get() { return getValue().get(); } + + /** + * @param The result type + * @return An empty but satisfied result. + * @since 4.0.0 + */ + static BindingResult empty() { + return BindingResult.EMPTY; + } + + /** + * @param The result type + * @return An empty but unsatisfied result. + * @since 4.0.0 + */ + static BindingResult unsatisfied() { + return UNSATISFIED; + } } } diff --git a/core/src/main/java/io/micronaut/core/bind/ArgumentBinderRegistry.java b/core/src/main/java/io/micronaut/core/bind/ArgumentBinderRegistry.java index d7fee5cc2a1..4a5cc282fc6 100644 --- a/core/src/main/java/io/micronaut/core/bind/ArgumentBinderRegistry.java +++ b/core/src/main/java/io/micronaut/core/bind/ArgumentBinderRegistry.java @@ -33,8 +33,20 @@ public interface ArgumentBinderRegistry { * @param The argument type * @param The source type * @since 2.0 + * @deprecated replaced with {@link #addArgumentBinder(ArgumentBinder)} */ + @Deprecated(since = "4", forRemoval = true) default void addRequestArgumentBinder(ArgumentBinder binder) { + addArgumentBinder((ArgumentBinder) binder); + } + + /** + * Adds a request argument binder to the registry. + * @param binder The binder + * @param The argument type + * @since 4.0.0 + */ + default void addArgumentBinder(ArgumentBinder binder) { throw new UnsupportedOperationException("Binder registry is not mutable"); } @@ -45,6 +57,20 @@ default void addRequestArgumentBinder(ArgumentBinder binder) { * @param source The source * @param The argument type * @return An {@link Optional} of {@link ArgumentBinder} + * @deprecated replaced with {@link #findArgumentBinder(Argument)} + */ + @Deprecated(since = "4", forRemoval = true) + default Optional> findArgumentBinder(Argument argument, S source) { + return findArgumentBinder(argument); + } + + /** + * Locate an {@link ArgumentBinder} for the given argument. + * + * @param argument The argument + * @param The argument type + * @return An {@link Optional} of {@link ArgumentBinder} + * @since 4.0.0 */ - Optional> findArgumentBinder(Argument argument, S source); + Optional> findArgumentBinder(Argument argument); } diff --git a/core/src/main/java/io/micronaut/core/bind/DefaultExecutableBinder.java b/core/src/main/java/io/micronaut/core/bind/DefaultExecutableBinder.java index 288913e534f..55a03499da2 100644 --- a/core/src/main/java/io/micronaut/core/bind/DefaultExecutableBinder.java +++ b/core/src/main/java/io/micronaut/core/bind/DefaultExecutableBinder.java @@ -66,8 +66,7 @@ public BoundExecutable bind( if (preBound.containsKey(argument)) { boundArguments[i] = preBound.get(argument); } else { - Optional> argumentBinder = - registry.findArgumentBinder(argument, source); + Optional> argumentBinder = registry.findArgumentBinder(argument); if (argumentBinder.isPresent()) { ArgumentBinder binder = argumentBinder.get(); @@ -131,9 +130,7 @@ public BoundExecutable tryBind(Executable target, ArgumentBin boundArguments[i] = preBound.get(argument); } else { - Optional> argumentBinder = - registry.findArgumentBinder(argument, source); - + Optional> argumentBinder = registry.findArgumentBinder(argument); if (argumentBinder.isPresent()) { ArgumentBinder binder = argumentBinder.get(); ArgumentConversionContext conversionContext = ConversionContext.of(argument); diff --git a/core/src/main/java/io/micronaut/core/bind/annotation/AbstractAnnotatedArgumentBinder.java b/core/src/main/java/io/micronaut/core/bind/annotation/AbstractAnnotatedArgumentBinder.java index f8e2eefe96f..0b9ace57324 100644 --- a/core/src/main/java/io/micronaut/core/bind/annotation/AbstractAnnotatedArgumentBinder.java +++ b/core/src/main/java/io/micronaut/core/bind/annotation/AbstractAnnotatedArgumentBinder.java @@ -15,17 +15,9 @@ */ package io.micronaut.core.bind.annotation; -import io.micronaut.core.bind.ArgumentBinder; -import io.micronaut.core.convert.ArgumentConversionContext; import io.micronaut.core.convert.ConversionService; -import io.micronaut.core.convert.value.ConvertibleValues; -import io.micronaut.core.naming.NameUtils; -import io.micronaut.core.type.Argument; -import io.micronaut.core.util.StringUtils; -import io.micronaut.core.annotation.Nullable; import java.lang.annotation.Annotation; -import java.util.Optional; /** * An abstract {@link AnnotatedArgumentBinder} implementation. @@ -35,11 +27,10 @@ * @param The binding source type * @author Graeme Rocher * @since 1.0 + * @deprecated Replaced by {@link AbstractArgumentBinder} */ -public abstract class AbstractAnnotatedArgumentBinder implements AnnotatedArgumentBinder { - - private static final String DEFAULT_VALUE_MEMBER = "defaultValue"; - protected final ConversionService conversionService; +@Deprecated(forRemoval = true, since = "4.0") +public abstract class AbstractAnnotatedArgumentBinder extends AbstractArgumentBinder implements AnnotatedArgumentBinder { /** * Constructor. @@ -47,116 +38,6 @@ public abstract class AbstractAnnotatedArgumentBinder doBind( - ArgumentConversionContext context, - ConvertibleValues values, - String annotationValue) { - return doBind(context, values, annotationValue, BindingResult.EMPTY); - } - - /** - * Do binding. - * - * @param context context - * @param values values - * @param annotationValue annotationValue - * @param defaultResult The default binding result if the value is null - * @return result - */ - @SuppressWarnings("unchecked") - protected BindingResult doBind( - ArgumentConversionContext context, - ConvertibleValues values, - String annotationValue, - ArgumentBinder.BindingResult defaultResult) { - - return doConvert(doResolve(context, values, annotationValue), context, defaultResult); - } - - /** - * Do resolve. - * - * @param context context - * @param values values - * @param annotationValue annotationValue - * @return result - */ - @SuppressWarnings("unchecked") - protected @Nullable Object doResolve( - ArgumentConversionContext context, - ConvertibleValues values, - String annotationValue) { - - Object value = resolveValue(context, values, annotationValue); - if (value == null) { - String fallbackName = getFallbackFormat(context.getArgument()); - if (!annotationValue.equals(fallbackName)) { - annotationValue = fallbackName; - value = resolveValue(context, values, annotationValue); - } - } - - return value; - } - - /** - * @param argument The argument - * @return The fallback format - */ - protected String getFallbackFormat(Argument argument) { - return NameUtils.hyphenate(argument.getName()); - } - - private Object resolveValue(ArgumentConversionContext context, ConvertibleValues values, String annotationValue) { - Argument argument = context.getArgument(); - if (StringUtils.isEmpty(annotationValue)) { - annotationValue = argument.getName(); - } - return values.get(annotationValue, context).orElseGet(() -> - conversionService.convert(argument.getAnnotationMetadata().stringValue(Bindable.class, DEFAULT_VALUE_MEMBER).orElse(null), context).orElse(null) - ); - } - - /** - * Convert the value and return a binding result. - * - * @param value The value to convert - * @param context The conversion context - * @return The binding result - */ - protected BindingResult doConvert(Object value, ArgumentConversionContext context) { - return doConvert(value, context, BindingResult.EMPTY); - } - - /** - * Convert the value and return a binding result. - * - * @param value The value to convert - * @param context The conversion context - * @param defaultResult The binding result if the value is null - * @return The binding result - */ - protected BindingResult doConvert(Object value, ArgumentConversionContext context, ArgumentBinder.BindingResult defaultResult) { - if (value == null) { - return defaultResult; - } else { - Optional result = conversionService.convert(value, context); - if (result.isPresent() && context.getArgument().getType() == Optional.class) { - return () -> (Optional) result.get(); - } - return () -> result; - } + super(conversionService); } } diff --git a/core/src/main/java/io/micronaut/core/bind/annotation/AbstractArgumentBinder.java b/core/src/main/java/io/micronaut/core/bind/annotation/AbstractArgumentBinder.java new file mode 100644 index 00000000000..07b8d1c3eb5 --- /dev/null +++ b/core/src/main/java/io/micronaut/core/bind/annotation/AbstractArgumentBinder.java @@ -0,0 +1,186 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.core.bind.annotation; + +import io.micronaut.core.annotation.Nullable; +import io.micronaut.core.bind.ArgumentBinder.BindingResult; +import io.micronaut.core.convert.ArgumentConversionContext; +import io.micronaut.core.convert.ConversionError; +import io.micronaut.core.convert.ConversionService; +import io.micronaut.core.convert.value.ConvertibleValues; +import io.micronaut.core.naming.NameUtils; +import io.micronaut.core.type.Argument; +import io.micronaut.core.util.StringUtils; + +import java.util.List; +import java.util.Optional; + +/** + * An abstract {@link AnnotatedArgumentBinder} implementation. + * + * @param The argument type + * @author Graeme Rocher + * @since 1.0 + */ +public abstract class AbstractArgumentBinder { + + private static final String DEFAULT_VALUE_MEMBER = "defaultValue"; + protected final ConversionService conversionService; + + /** + * Constructor. + * + * @param conversionService conversionService + */ + protected AbstractArgumentBinder(ConversionService conversionService) { + this.conversionService = conversionService; + } + + /** + * Do binding. + * + * @param context context + * @param values values + * @param annotationValue annotationValue + * @return result + */ + protected BindingResult doBind( + ArgumentConversionContext context, + ConvertibleValues values, + String annotationValue) { + return doBind(context, values, annotationValue, BindingResult.empty()); + } + + /** + * Do binding. + * + * @param context context + * @param values values + * @param name annotationValue + * @param defaultResult The default binding result if the value is null + * @return result + */ + protected BindingResult doBind(ArgumentConversionContext context, + ConvertibleValues values, + String name, + BindingResult defaultResult) { + + return doConvert(doResolve(context, values, name), context, defaultResult); + } + + /** + * Do resolve. + * + * @param context context + * @param values values + * @param name annotationValue + * @return result + */ + @Nullable + protected Object doResolve(ArgumentConversionContext context, + ConvertibleValues values, + String name) { + + Object value = resolveValue(context, values, name); + if (value == null) { + String fallbackName = getFallbackFormat(context.getArgument()); + if (!name.equals(fallbackName)) { + name = fallbackName; + value = resolveValue(context, values, name); + } + } + + return value; + } + + /** + * @param argument The argument + * @return The fallback format + */ + protected String getFallbackFormat(Argument argument) { + return NameUtils.hyphenate(argument.getName()); + } + + private Object resolveValue(ArgumentConversionContext context, ConvertibleValues values, String annotationValue) { + Argument argument = context.getArgument(); + if (StringUtils.isEmpty(annotationValue)) { + annotationValue = argument.getName(); + } + return values.get(annotationValue, context).orElseGet(() -> + conversionService.convert( + argument.getAnnotationMetadata().stringValue(Bindable.class, DEFAULT_VALUE_MEMBER).orElse(null), + context + ).orElse(null) + ); + } + + /** + * Convert the value and return a binding result. + * + * @param value The value to convert + * @param context The conversion context + * @return The binding result + */ + protected BindingResult doConvert(Object value, ArgumentConversionContext context) { + return doConvert(value, context, BindingResult.empty()); + } + + /** + * Convert the value and return a binding result. + * + * @param value The value to convert + * @param context The conversion context + * @param defaultResult The binding result if the value is null + * @return The binding result + */ + protected BindingResult doConvert(Object value, ArgumentConversionContext context, BindingResult defaultResult) { + if (value == null) { + Optional lastError = context.getLastError(); + if (lastError.isPresent()) { + return new BindingResult<>() { + @Override + public Optional getValue() { + return Optional.empty(); + } + + @Override + public List getConversionErrors() { + return lastError.map(List::of).orElseGet(List::of); + } + }; + } + return defaultResult; + } else { + Optional result = conversionService.convert(value, context); + Optional lastError = context.getLastError(); + if (result.isPresent() && context.getArgument().getType() == Optional.class) { + result = (Optional) result.get(); + } + Optional finalResult = result; + return new BindingResult<>() { + @Override + public Optional getValue() { + return finalResult; + } + + @Override + public List getConversionErrors() { + return lastError.map(List::of).orElseGet(List::of); + } + }; + } + } +} diff --git a/core/src/main/java/io/micronaut/core/convert/DefaultMutableConversionService.java b/core/src/main/java/io/micronaut/core/convert/DefaultMutableConversionService.java index 61a3bb23e95..385bea75f7a 100644 --- a/core/src/main/java/io/micronaut/core/convert/DefaultMutableConversionService.java +++ b/core/src/main/java/io/micronaut/core/convert/DefaultMutableConversionService.java @@ -120,7 +120,7 @@ public Optional convert(Object object, Class targetType, ConversionCon Class sourceType = object.getClass(); final AnnotationMetadata annotationMetadata = context.getAnnotationMetadata(); - if (annotationMetadata.hasStereotype(Format.class)) { + if (annotationMetadata.hasStereotypeNonRepeating(Format.class)) { Optional formattingAnn = annotationMetadata.getAnnotationNameByStereotype(Format.class); String formattingAnnotation = formattingAnn.orElse(null); ConvertiblePair pair = new ConvertiblePair(sourceType, targetType, formattingAnnotation); diff --git a/core/src/test/groovy/io/micronaut/core/bind/ExecutableBinderSpec.groovy b/core/src/test/groovy/io/micronaut/core/bind/ExecutableBinderSpec.groovy index 912b78a9950..4a13d35e121 100644 --- a/core/src/test/groovy/io/micronaut/core/bind/ExecutableBinderSpec.groovy +++ b/core/src/test/groovy/io/micronaut/core/bind/ExecutableBinderSpec.groovy @@ -60,7 +60,7 @@ class ExecutableBinderSpec extends Specification { return { Optional.of(args[1].get(args[0].argument.name)) } as ArgumentBinder.BindingResult } ) - registry.findArgumentBinder(_,_) >> Optional.of( argumentBinder) + registry.findArgumentBinder(_) >> Optional.of( argumentBinder) when: def bound = binder.bind(executable, registry, [foo:"bar"]) @@ -103,7 +103,7 @@ class ExecutableBinderSpec extends Specification { return ArgumentBinder.BindingResult.UNSATISFIED } ) - registry.findArgumentBinder(_,_) >> Optional.of( argumentBinder) + registry.findArgumentBinder(_) >> Optional.of( argumentBinder) when: def bound = binder.bind(executable, registry, [foo:"bar"]) @@ -141,7 +141,7 @@ class ExecutableBinderSpec extends Specification { return ArgumentBinder.BindingResult.UNSATISFIED } ) - registry.findArgumentBinder(_,_) >> Optional.of( argumentBinder) + registry.findArgumentBinder(_) >> Optional.of( argumentBinder) when: def bound = binder.bind(executable, registry, [not:"there"]) diff --git a/http-client-jdk/src/test/groovy/io/micronaut/http/client/jdk/OptionsRequestAttributesSpec.groovy b/http-client-jdk/src/test/groovy/io/micronaut/http/client/jdk/OptionsRequestAttributesSpec.groovy index 5baef541258..e68b4d5748b 100644 --- a/http-client-jdk/src/test/groovy/io/micronaut/http/client/jdk/OptionsRequestAttributesSpec.groovy +++ b/http-client-jdk/src/test/groovy/io/micronaut/http/client/jdk/OptionsRequestAttributesSpec.groovy @@ -51,7 +51,6 @@ class OptionsRequestAttributesSpec extends Specification { @Override Publisher> doFilter(HttpRequest request, ServerFilterChain chain) { - Assert.that(request.getAttributes().contains(HttpAttributes.ROUTE.toString())) Assert.that(request.getAttributes().contains(HttpAttributes.ROUTE_MATCH.toString())) Assert.that(request.getAttributes().contains(HttpAttributes.ROUTE_INFO.toString())) Assert.that(request.getAttributes().contains(HttpAttributes.URI_TEMPLATE.toString())) diff --git a/http-client/src/main/java/io/micronaut/http/client/netty/DefaultHttpClient.java b/http-client/src/main/java/io/micronaut/http/client/netty/DefaultHttpClient.java index 50036f2c2a7..22e2bcfbee0 100644 --- a/http-client/src/main/java/io/micronaut/http/client/netty/DefaultHttpClient.java +++ b/http-client/src/main/java/io/micronaut/http/client/netty/DefaultHttpClient.java @@ -879,6 +879,7 @@ private Publisher connectWebSocket(URI uri, MutableHttpRequest request customHeaders = ((NettyHttpHeaders) headers).getNettyHeaders(); } if (StringUtils.isNotEmpty(subprotocol)) { + NettyHttpHeaders.validateHeader("Sec-WebSocket-Protocol", subprotocol); customHeaders.add("Sec-WebSocket-Protocol", subprotocol); } diff --git a/http-client/src/main/java/io/micronaut/http/client/netty/NettyClientHttpRequest.java b/http-client/src/main/java/io/micronaut/http/client/netty/NettyClientHttpRequest.java index 7db2b822bc3..4016eee4307 100644 --- a/http-client/src/main/java/io/micronaut/http/client/netty/NettyClientHttpRequest.java +++ b/http-client/src/main/java/io/micronaut/http/client/netty/NettyClientHttpRequest.java @@ -282,7 +282,8 @@ public FullHttpRequest toFullHttpRequest() { req = new DefaultFullHttpRequest( HttpVersion.HTTP_1_1, method, - uriStr + uriStr, + false ); req.headers().setAll(headers.getNettyHeaders()); } @@ -306,7 +307,7 @@ public StreamedHttpRequest toStreamHttpRequest() { String uriStr = resolveUriPath(); io.netty.handler.codec.http.HttpMethod method = getMethod(httpMethodName); DefaultStreamedHttpRequest req = new DefaultStreamedHttpRequest( - HttpVersion.HTTP_1_1, method, uriStr, (Publisher) body); + HttpVersion.HTTP_1_1, method, uriStr, false, (Publisher) body); req.headers().setAll(headers.getNettyHeaders()); return req; } else { diff --git a/http-client/src/test/groovy/io/micronaut/http/client/HttpPostSpec.groovy b/http-client/src/test/groovy/io/micronaut/http/client/HttpPostSpec.groovy index 3b46e480043..1710ccb32eb 100644 --- a/http-client/src/test/groovy/io/micronaut/http/client/HttpPostSpec.groovy +++ b/http-client/src/test/groovy/io/micronaut/http/client/HttpPostSpec.groovy @@ -25,7 +25,12 @@ import io.micronaut.http.HttpRequest import io.micronaut.http.HttpResponse import io.micronaut.http.HttpStatus import io.micronaut.http.MediaType -import io.micronaut.http.annotation.* +import io.micronaut.http.annotation.Body +import io.micronaut.http.annotation.Controller +import io.micronaut.http.annotation.Get +import io.micronaut.http.annotation.Header +import io.micronaut.http.annotation.Post +import io.micronaut.http.annotation.QueryValue import io.micronaut.http.client.annotation.Client import io.micronaut.http.client.exceptions.HttpClientResponseException import io.micronaut.http.client.multipart.MultipartBody @@ -35,6 +40,7 @@ import io.micronaut.test.extensions.spock.annotation.MicronautTest import jakarta.inject.Inject import reactor.core.publisher.Flux import spock.lang.Specification + import java.nio.charset.StandardCharsets /** @@ -481,7 +487,7 @@ class HttpPostSpec extends Specification { @Post(uri = "/multipartCharset", consumes = MediaType.MULTIPART_FORM_DATA, produces = MediaType.TEXT_PLAIN) - String multipartCharset(@Body CompletedFileUpload file) { + String multipartCharset(CompletedFileUpload file) { return file.fileUpload.getCharset() } diff --git a/http-netty/src/main/java/io/micronaut/http/netty/NettyHttpHeaders.java b/http-netty/src/main/java/io/micronaut/http/netty/NettyHttpHeaders.java index e195b089ecb..0ea1940fbd7 100644 --- a/http-netty/src/main/java/io/micronaut/http/netty/NettyHttpHeaders.java +++ b/http-netty/src/main/java/io/micronaut/http/netty/NettyHttpHeaders.java @@ -25,6 +25,7 @@ import io.micronaut.http.MutableHttpHeaders; import io.netty.handler.codec.http.DefaultHttpHeaders; import io.netty.handler.codec.http.HttpHeaderNames; +import io.netty.handler.codec.http.HttpHeaderValidationUtil; import java.net.URI; import java.nio.charset.StandardCharsets; @@ -51,8 +52,8 @@ @Internal public class NettyHttpHeaders implements MutableHttpHeaders { - io.netty.handler.codec.http.HttpHeaders nettyHeaders; - ConversionService conversionService; + private final io.netty.handler.codec.http.HttpHeaders nettyHeaders; + private ConversionService conversionService; /** * @param nettyHeaders The Netty Http headers @@ -67,11 +68,13 @@ public NettyHttpHeaders(io.netty.handler.codec.http.HttpHeaders nettyHeaders, Co * Default constructor. */ public NettyHttpHeaders() { - this.nettyHeaders = new DefaultHttpHeaders(); + this.nettyHeaders = new DefaultHttpHeaders(false); this.conversionService = ConversionService.SHARED; } /** + * Note: Caller must take care to validate headers inserted into this object! + * * @return The underlying Netty headers. */ public io.netty.handler.codec.http.HttpHeaders getNettyHeaders() { @@ -83,15 +86,6 @@ public final boolean contains(String name) { return nettyHeaders.contains(name); } - /** - * Sets the underlying netty headers. - * - * @param headers The Netty http headers - */ - void setNettyHeaders(io.netty.handler.codec.http.HttpHeaders headers) { - this.nettyHeaders = headers; - } - @Override public Optional get(CharSequence name, ArgumentConversionContext conversionContext) { List values = nettyHeaders.getAll(name); @@ -142,16 +136,37 @@ public Optional findFirst(CharSequence name) { @Override public MutableHttpHeaders add(CharSequence header, CharSequence value) { + validateHeader(header, value); nettyHeaders.add(header, value); return this; } @Override public MutableHeaders set(CharSequence header, CharSequence value) { + validateHeader(header, value); nettyHeaders.set(header, value); return this; } + /** + * Like {@link #set(CharSequence, CharSequence)} but without header validation. + * + * @param header The header name + * @param value The header value + */ + public void setUnsafe(CharSequence header, CharSequence value) { + nettyHeaders.set(header, value); + } + + public static void validateHeader(CharSequence name, CharSequence value) { + if (name == null || name.isEmpty() || HttpHeaderValidationUtil.validateToken(name) != -1) { + throw new IllegalArgumentException("Invalid header name"); + } + if (HttpHeaderValidationUtil.validateValidHeaderValue(value) != -1) { + throw new IllegalArgumentException("Invalid header value"); + } + } + @Override public MutableHttpHeaders remove(CharSequence header) { nettyHeaders.remove(header); diff --git a/http-netty/src/main/java/io/micronaut/http/netty/NettyMutableHttpResponse.java b/http-netty/src/main/java/io/micronaut/http/netty/NettyMutableHttpResponse.java index 2a9dc78b7fd..04f98529c3a 100644 --- a/http-netty/src/main/java/io/micronaut/http/netty/NettyMutableHttpResponse.java +++ b/http-netty/src/main/java/io/micronaut/http/netty/NettyMutableHttpResponse.java @@ -26,6 +26,7 @@ import io.micronaut.core.convert.value.MutableConvertibleValuesMap; import io.micronaut.core.io.buffer.ByteBuffer; import io.micronaut.core.type.Argument; +import io.micronaut.core.util.StringUtils; import io.micronaut.http.HttpStatus; import io.micronaut.http.MediaType; import io.micronaut.http.MutableHttpHeaders; @@ -114,7 +115,7 @@ public NettyMutableHttpResponse(HttpVersion httpVersion, HttpResponseStatus http * @param conversionService The conversion service */ public NettyMutableHttpResponse(HttpVersion httpVersion, HttpResponseStatus httpResponseStatus, Object body, ConversionService conversionService) { - this(httpVersion, httpResponseStatus, new DefaultHttpHeaders(), body, conversionService); + this(httpVersion, httpResponseStatus, null, body, conversionService); } /** @@ -149,7 +150,7 @@ private NettyMutableHttpResponse(HttpVersion httpVersion, boolean hasHeaders = nettyHeaders != null; if (!hasHeaders) { - nettyHeaders = new DefaultHttpHeaders(); + nettyHeaders = new DefaultHttpHeaders(false); } this.nettyHeaders = nettyHeaders; this.headers = new NettyHttpHeaders(nettyHeaders, conversionService); @@ -218,6 +219,19 @@ public MutableConvertibleValues getAttributes() { return attributes; } + @Override + public io.micronaut.http.HttpResponse setAttribute(CharSequence name, Object value) { + // This is the copy from the super method to avoid the type pollution + if (StringUtils.isNotEmpty(name)) { + if (value == null) { + getAttributes().remove(name.toString()); + } else { + getAttributes().put(name.toString(), value); + } + } + return this; + } + @Override public int code() { return httpResponseStatus.code(); @@ -287,6 +301,18 @@ public MutableHttpResponse body(@Nullable T body) { return (MutableHttpResponse) this; } + @Override + public MutableHttpResponse contentType(MediaType mediaType) { + if (mediaType == null) { + headers.remove(HttpHeaderNames.CONTENT_TYPE); + } else { + // optimization for content type validation + mediaType.validate(() -> NettyHttpHeaders.validateHeader(HttpHeaderNames.CONTENT_TYPE, mediaType)); + headers.setUnsafe(HttpHeaderNames.CONTENT_TYPE, mediaType); + } + return this; + } + /** * @return Server cookie encoder */ diff --git a/http-netty/src/main/java/io/micronaut/http/netty/reactive/HandlerPublisher.java b/http-netty/src/main/java/io/micronaut/http/netty/reactive/HandlerPublisher.java index 1c826cfb1c6..af72cf0514b 100644 --- a/http-netty/src/main/java/io/micronaut/http/netty/reactive/HandlerPublisher.java +++ b/http-netty/src/main/java/io/micronaut/http/netty/reactive/HandlerPublisher.java @@ -19,6 +19,7 @@ import io.netty.channel.ChannelDuplexHandler; import io.netty.channel.ChannelHandlerContext; import io.netty.handler.codec.http.HttpContent; +import io.netty.handler.codec.http.LastHttpContent; import io.netty.util.ReferenceCountUtil; import io.netty.util.concurrent.EventExecutor; import org.reactivestreams.Subscriber; @@ -98,6 +99,7 @@ public String toString() { private ChannelHandlerContext ctx; private volatile long outstandingDemand = 0; private Throwable noSubscriberError; + private boolean seenLast; /** * Create a handler publisher. @@ -164,7 +166,10 @@ protected void requestDemand() { LOG.trace("Demand received for next message (state = " + state + "). Calling context.read()"); } - ctx.read(); + // prevent requesting demand beyond LastHttpContent + if (!seenLast) { + ctx.read(); + } } /** @@ -321,6 +326,9 @@ private void receivedCancel() { @Override public void channelRead(ChannelHandlerContext ctx, Object message) { + if (message instanceof LastHttpContent) { + seenLast = true; + } if (acceptInboundMessage(message)) { publishMessageLater(message); } else { diff --git a/http-netty/src/main/java/io/micronaut/http/netty/stream/StreamingInboundHttp2ToHttpAdapter.java b/http-netty/src/main/java/io/micronaut/http/netty/stream/StreamingInboundHttp2ToHttpAdapter.java deleted file mode 100644 index 135ef2c6094..00000000000 --- a/http-netty/src/main/java/io/micronaut/http/netty/stream/StreamingInboundHttp2ToHttpAdapter.java +++ /dev/null @@ -1,407 +0,0 @@ -/* - * Copyright 2017-2020 original authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.micronaut.http.netty.stream; - -import io.micronaut.core.annotation.Internal; -import io.netty.buffer.ByteBuf; -import io.netty.buffer.Unpooled; -import io.netty.channel.ChannelHandlerContext; -import io.netty.handler.codec.http.*; -import io.netty.handler.codec.http2.Http2CodecUtil; -import io.netty.handler.codec.http2.Http2Connection; -import io.netty.handler.codec.http2.Http2EventAdapter; -import io.netty.handler.codec.http2.Http2Exception; -import io.netty.handler.codec.http2.Http2Headers; -import io.netty.handler.codec.http2.Http2Settings; -import io.netty.handler.codec.http2.Http2Stream; -import io.netty.handler.codec.http2.HttpConversionUtil; - -import java.util.concurrent.atomic.AtomicInteger; - -import static io.netty.handler.codec.http.HttpResponseStatus.OK; -import static io.netty.handler.codec.http2.Http2Error.INTERNAL_ERROR; -import static io.netty.handler.codec.http2.Http2Error.PROTOCOL_ERROR; -import static io.netty.handler.codec.http2.Http2Exception.connectionError; -import static io.netty.util.internal.ObjectUtil.checkNotNull; - -/** - * Implementation of {@link Http2EventAdapter} that allows streaming requests for servers and responses for clients by - * establishing a processor that emits chunks as {@link HttpContent}. - * - * This implementation does not buffer the data. If you need data buffering a {@link io.netty.handler.flow.FlowControlHandler} - * can be placed after this implementation so that downstream handlers can control flow. - * - * Based on code in {@link io.netty.handler.codec.http2.InboundHttp2ToHttpAdapter}. - * - * @author graemerocher - * @since 2.0 - */ -@Internal -public class StreamingInboundHttp2ToHttpAdapter extends Http2EventAdapter { - protected final Http2Connection connection; - protected final boolean validateHttpHeaders; - private final int maxContentLength; - private final Http2Connection.PropertyKey messageKey; - private final boolean propagateSettings; - private final Http2Connection.PropertyKey dataReadKey; - - /** - * Default constructor. - * @param connection The connection - * @param maxContentLength The max content length - * @param validateHttpHeaders Whether to validate headers - * @param propagateSettings Whether to propagate settings - */ - public StreamingInboundHttp2ToHttpAdapter(Http2Connection connection, int maxContentLength, - boolean validateHttpHeaders, boolean propagateSettings) { - - if (maxContentLength <= 0) { - throw new IllegalArgumentException("maxContentLength: " + maxContentLength + " (expected: > 0)"); - } - this.connection = checkNotNull(connection, "connection"); - this.maxContentLength = maxContentLength; - this.validateHttpHeaders = validateHttpHeaders; - this.propagateSettings = propagateSettings; - messageKey = connection.newKey(); - dataReadKey = connection.newKey(); - } - - /** - * Default constructor. - * @param connection The connection - * @param maxContentLength The max content length - */ - public StreamingInboundHttp2ToHttpAdapter(Http2Connection connection, int maxContentLength) { - - this(connection, maxContentLength, true, true); - } - - /** - * The stream is out of scope for the HTTP message flow and will no longer be tracked. - * - * @param stream The stream to remove associated state with - */ - protected final void removeMessage(Http2Stream stream) { - stream.removeProperty(messageKey); - } - - /** - * Get the {@link FullHttpMessage} associated with {@code stream}. - * @param stream The stream to get the associated state from - * @return The {@link FullHttpMessage} associated with {@code stream}. - */ - protected final HttpMessage getMessage(Http2Stream stream) { - return (HttpMessage) stream.getProperty(messageKey); - } - - /** - * Make {@code message} be the state associated with {@code stream}. - * @param stream The stream which {@code message} is associated with. - * @param message The message which contains the HTTP semantics. - */ - protected final void putMessage(Http2Stream stream, HttpMessage message) { - // reset the data read key - stream.setProperty(dataReadKey, new AtomicInteger(0)); - stream.setProperty(messageKey, message); - } - - @Override - public void onStreamRemoved(Http2Stream stream) { - removeMessage(stream); - } - - /** - * fire a channel read event. - * - * @param ctx The context to fire the event on - * @param msg The message to send - * @param stream the stream of the message which is being fired - */ - protected void fireChannelRead(ChannelHandlerContext ctx, HttpContent msg, - Http2Stream stream) { - ctx.fireChannelRead(msg); - } - - /** - * fire a channel read event. - * - * @param ctx The context to fire the event on - * @param msg The message to send - * @param stream the stream of the message which is being fired - */ - protected void fireChannelRead(ChannelHandlerContext ctx, HttpMessage msg, - Http2Stream stream) { - if (connection.isServer()) { - // the event has to come after the flow control handler to avoid buffering - // there may be a better way to do this. - final ChannelHandlerContext context = ctx.pipeline().context("flow-control-handler"); - if (context != null) { - context.fireChannelRead(msg); - } else { - ctx.fireChannelRead(msg); - } - } else { - ctx.fireChannelRead(msg); - } - } - - /** - * Create a new {@link FullHttpMessage} based upon the current connection parameters. - * - * - * @param ctx The channel context - * @param stream The stream to create a message for - * @param headers The headers associated with {@code stream} - * @param validateHttpHeaders - *
    - *
  • {@code true} to validate HTTP headers in the http-codec
  • - *
  • {@code false} not to validate HTTP headers in the http-codec
  • - *
- * @throws Http2Exception thrown if an error occurs creating the request - * @return A new {@link StreamedHttpMessage} - */ - protected HttpMessage newMessage( - ChannelHandlerContext ctx, - Http2Stream stream, - Http2Headers headers, - boolean validateHttpHeaders) - throws Http2Exception { - return connection.isServer() ? HttpConversionUtil.toHttpRequest(stream.id(), headers, validateHttpHeaders) : - HttpConversionUtil.toHttpResponse(stream.id(), headers, validateHttpHeaders); - } - - /** - * Provides translation between HTTP/2 and HTTP header objects while ensuring the stream - * is in a valid state for additional headers. - * - * @param ctx The context for which this message has been received. - * Used to send informational header if detected. - * @param stream The stream the {@code headers} apply to - * @param headers The headers to process - * @param allowAppend - *
    - *
  • {@code true} if headers will be appended if the stream already exists.
  • - *
  • if {@code false} and the stream already exists this method returns {@code null}.
  • - *
- * @param appendToTrailer - *
    - *
  • {@code true} if a message {@code stream} already exists then the headers - * should be added to the trailing headers.
  • - *
  • {@code false} then appends will be done to the initial headers.
  • - *
- * @return The object used to track the stream corresponding to {@code stream}. {@code null} if - * {@code allowAppend} is {@code false} and the stream already exists. - * @throws Http2Exception If the stream id is not in the correct state to process the headers request - */ - protected HttpMessage processHeadersBegin(ChannelHandlerContext ctx, Http2Stream stream, Http2Headers headers, - boolean allowAppend, boolean appendToTrailer) throws Http2Exception { - HttpMessage msg = getMessage(stream); - if (msg == null) { - msg = newMessage(ctx, stream, headers, validateHttpHeaders); - putMessage(stream, msg); - } else if (allowAppend) { - HttpConversionUtil.addHttp2ToHttpHeaders( - stream.id(), - headers, - msg.headers(), - HttpVersion.HTTP_1_1, - appendToTrailer, - msg instanceof HttpRequest - ); - } else { - msg = null; - } - return msg; - } - - /** - * After HTTP/2 headers have been processed by {@link #processHeadersBegin} this method either - * sends the result up the pipeline or retains the message for future processing. - * - * @param ctx The context for which this message has been received - * @param stream The stream the {@code objAccumulator} corresponds to - * @param msg The object which represents all headers/data for corresponding to {@code stream} - * @param endOfStream {@code true} if this is the last event for the stream - */ - private void processHeadersEnd( - ChannelHandlerContext ctx, - Http2Stream stream, - HttpMessage msg, - boolean endOfStream) { - if (endOfStream) { - if (connection.isServer()) { - HttpRequest existing = (HttpRequest) msg; - msg = new DefaultFullHttpRequest( - HttpVersion.HTTP_1_1, - existing.method(), - existing.uri(), - Unpooled.EMPTY_BUFFER, - existing.headers(), - EmptyHttpHeaders.INSTANCE); - } else { - HttpResponse existing = (HttpResponse) msg; - msg = new DefaultFullHttpResponse( - existing.protocolVersion(), - existing.status(), - Unpooled.EMPTY_BUFFER, - existing.headers(), - EmptyHttpHeaders.INSTANCE - ); - } - // no more data after headers to just fire as a regular http request - HttpUtil.setContentLength(msg, 0); - fireChannelRead(ctx, msg, stream); - } else { - if (!msg.headers().contains(HttpHeaderNames.CONTENT_LENGTH)) { - HttpUtil.setTransferEncodingChunked(msg, true); - } - fireChannelRead(ctx, msg, stream); - } - } - - @Override - public int onDataRead(ChannelHandlerContext ctx, int streamId, ByteBuf data, int padding, boolean endOfStream) - throws Http2Exception { - Http2Stream stream = connection.stream(streamId); - HttpMessage msg = getMessage(stream); - if (msg == null) { - throw connectionError(PROTOCOL_ERROR, "Data Frame received for unknown stream id %d", streamId); - } - - AtomicInteger dataRead = getDataRead(stream); - final int dataReadableBytes = data.readableBytes(); - final int readSoFar = dataRead.getAndAdd(dataReadableBytes); - if (readSoFar > maxContentLength - dataReadableBytes) { - throw connectionError(INTERNAL_ERROR, - "Content length exceeded max of %d for stream id %d", maxContentLength, streamId); - - } - - if (endOfStream) { - // end of stream, emits a LastHttpContent - // will be released by HttpStreamsHandler - if (dataReadableBytes > 0) { - final DefaultLastHttpContent content = new DefaultLastHttp2Content(data.retain(), stream); - fireChannelRead(ctx, content, stream); - } else { - fireChannelRead(ctx, new DefaultLastHttp2Content(Unpooled.EMPTY_BUFFER, stream), stream); - } - } else { - // will be released by HttpStreamsHandler - final DefaultHttp2Content content = new DefaultHttp2Content(data.retain(), stream); - fireChannelRead(ctx, content, stream); - } - - // All bytes have been processed. - return dataReadableBytes + padding; - } - - private AtomicInteger getDataRead(Http2Stream stream) { - final Object demand = stream.getProperty(dataReadKey); - if (demand instanceof AtomicInteger) { - return ((AtomicInteger) demand); - } else { - final AtomicInteger newValue = new AtomicInteger(0); - stream.setProperty(dataReadKey, newValue); - return newValue; - } - } - - @Override - public void onHeadersRead(ChannelHandlerContext ctx, int streamId, Http2Headers headers, int padding, - boolean endOfStream) throws Http2Exception { - Http2Stream stream = connection.stream(streamId); - HttpMessage msg = - processHeadersBegin(ctx, stream, headers, true, true); - if (msg != null) { - processHeadersEnd(ctx, stream, msg, endOfStream); - } - } - - @Override - public void onHeadersRead(ChannelHandlerContext ctx, int streamId, Http2Headers headers, int streamDependency, - short weight, boolean exclusive, int padding, boolean endOfStream) throws Http2Exception { - Http2Stream stream = connection.stream(streamId); - HttpMessage msg = processHeadersBegin(ctx, stream, headers, true, true); - if (msg != null) { - // Add headers for dependency and weight. - // See https://github.com/netty/netty/issues/5866 - if (streamDependency != Http2CodecUtil.CONNECTION_STREAM_ID) { - msg.headers().setInt(HttpConversionUtil.ExtensionHeaderNames.STREAM_DEPENDENCY_ID.text(), - streamDependency); - } - msg.headers().setShort(HttpConversionUtil.ExtensionHeaderNames.STREAM_WEIGHT.text(), weight); - - processHeadersEnd(ctx, stream, msg, endOfStream); - } - } - - @Override - public void onRstStreamRead(ChannelHandlerContext ctx, int streamId, long errorCode) { - Http2Stream stream = connection.stream(streamId); - HttpMessage msg = getMessage(stream); - if (msg != null) { - onRstStreamRead(stream, msg); - } - - // discard stream since it has been reset - stream.close(); - } - - @Override - public void onPushPromiseRead(ChannelHandlerContext ctx, int streamId, int promisedStreamId, - Http2Headers headers, int padding) throws Http2Exception { - // A push promise should not be allowed to add headers to an existing stream - Http2Stream promisedStream = connection.stream(promisedStreamId); - if (headers.status() == null) { - // A PUSH_PROMISE frame has no Http response status. - // https://tools.ietf.org/html/rfc7540#section-8.2.1 - // Server push is semantically equivalent to a server responding to a - // request; however, in this case, that request is also sent by the - // server, as a PUSH_PROMISE frame. - headers.status(OK.codeAsText()); - } - HttpMessage msg = processHeadersBegin(ctx, promisedStream, headers, false, false); - if (msg == null) { - throw connectionError(PROTOCOL_ERROR, "Push Promise Frame received for pre-existing stream id %d", - promisedStreamId); - } - - msg.headers().setInt(HttpConversionUtil.ExtensionHeaderNames.STREAM_PROMISE_ID.text(), streamId); - msg.headers().setShort(HttpConversionUtil.ExtensionHeaderNames.STREAM_WEIGHT.text(), - Http2CodecUtil.DEFAULT_PRIORITY_WEIGHT); - - processHeadersEnd(ctx, promisedStream, msg, false); - } - - @Override - public void onSettingsRead(ChannelHandlerContext ctx, Http2Settings settings) throws Http2Exception { - if (propagateSettings) { - // Provide an interface for non-listeners to capture settings - ctx.fireChannelRead(settings); - } - } - - /** - * Called if a {@code RST_STREAM} is received but we have some data for that stream. - * - * @param stream The stream - * @param msg The message - */ - protected void onRstStreamRead(Http2Stream stream, HttpMessage msg) { - removeMessage(stream); - } -} diff --git a/http-netty/src/test/groovy/io/micronaut/http/netty/NettyHttpHeadersSpec.groovy b/http-netty/src/test/groovy/io/micronaut/http/netty/NettyHttpHeadersSpec.groovy new file mode 100644 index 00000000000..821caeb198c --- /dev/null +++ b/http-netty/src/test/groovy/io/micronaut/http/netty/NettyHttpHeadersSpec.groovy @@ -0,0 +1,28 @@ +package io.micronaut.http.netty + +import spock.lang.Specification + +class NettyHttpHeadersSpec extends Specification { + def validation(String key, String value) { + given: + def headers = new NettyHttpHeaders() + + when: + headers.add(key, value) + then: + thrown IllegalArgumentException + + when: + headers.set(key, value) + then: + thrown IllegalArgumentException + + where: + key | value + "foo bar" | "baz" + "fooä" | "baz" + null | "baz" + "" | "baz" + "foo" | "bar\nbaz" + } +} diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/BaseRouteCompleter.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/BaseRouteCompleter.java deleted file mode 100644 index a9702c7e0fa..00000000000 --- a/http-server-netty/src/main/java/io/micronaut/http/server/netty/BaseRouteCompleter.java +++ /dev/null @@ -1,107 +0,0 @@ -/* - * Copyright 2017-2022 original authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.micronaut.http.server.netty; - -import io.micronaut.core.annotation.Internal; -import io.micronaut.core.annotation.Nullable; -import io.micronaut.core.io.buffer.ReferenceCounted; -import io.micronaut.http.server.netty.multipart.NettyCompletedFileUpload; -import io.micronaut.web.router.RouteMatch; -import io.netty.buffer.ByteBufHolder; -import io.netty.util.ReferenceCountUtil; - -/** - * This class consumes objects produced by a {@link HttpContentProcessor}. Normally it just adds - * the data to the {@link NettyHttpRequest}. For multipart data, there is additional logic in - * {@link FormRouteCompleter} that also dynamically binds parameters, though usually this is done - * by the {@link io.micronaut.http.server.binding.RequestArgumentSatisfier}. - * - * @since 4.0.0 - * @author Jonas Konrad - */ -@Internal -class BaseRouteCompleter { - final NettyHttpRequest request; - volatile boolean needsInput = true; - /** - * Optional runnable that may be called from other threads (i.e. downstream subscribers) to - * notify that {@link #needsInput} may have changed. - */ - @Nullable - volatile Runnable checkDemand; - RouteMatch routeMatch; - boolean execute = false; - - public BaseRouteCompleter(NettyHttpRequest request, RouteMatch routeMatch) { - this.request = request; - this.routeMatch = routeMatch; - } - - final void add(Object message) throws Throwable { - try { - if (request.destroyed) { - // we don't want this message anymore - ReferenceCountUtil.release(message); - return; - } - - if (message instanceof ByteBufHolder bbh) { - addHolder(bbh); - } else { - ((NettyHttpRequest) request).setBody(message); - needsInput = true; - } - - // now, a pseudo try-finally with addSuppressed. - } catch (Throwable t) { - try { - ReferenceCountUtil.release(message); - } catch (Throwable u) { - t.addSuppressed(u); - } - throw t; - } - - // the upstream processor gives us ownership of the message, so we need to release it. - ReferenceCountUtil.release(message); - } - - protected void addHolder(ByteBufHolder holder) { - request.addContent(holder); - needsInput = true; - } - - void completeSuccess() { - execute = true; - } - - void completeFailure(Throwable failure) { - if (!execute) { - // discard parameters that have already been bound - for (Object toDiscard : routeMatch.getVariableValues().values()) { - if (toDiscard instanceof ReferenceCounted rc) { - rc.release(); - } - if (toDiscard instanceof io.netty.util.ReferenceCounted rc) { - rc.release(); - } - if (toDiscard instanceof NettyCompletedFileUpload fu) { - fu.discard(); - } - } - } - } -} diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/DefaultHttpContentProcessor.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/DefaultHttpContentProcessor.java index e5b47b63827..522b5b047a0 100644 --- a/http-server-netty/src/main/java/io/micronaut/http/server/netty/DefaultHttpContentProcessor.java +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/DefaultHttpContentProcessor.java @@ -17,11 +17,9 @@ import io.micronaut.core.annotation.Internal; import io.micronaut.http.exceptions.ContentLengthExceededException; -import io.micronaut.http.netty.stream.StreamedHttpMessage; import io.micronaut.http.server.HttpServerConfiguration; import io.netty.buffer.ByteBufHolder; import io.netty.channel.ChannelHandlerContext; -import io.netty.handler.codec.http.HttpRequest; import io.netty.handler.codec.http.multipart.HttpData; import io.netty.util.ReferenceCountUtil; @@ -42,7 +40,6 @@ public class DefaultHttpContentProcessor implements HttpContentProcessor { protected final HttpServerConfiguration configuration; protected final long advertisedLength; protected final long requestMaxSize; - protected final StreamedHttpMessage streamedHttpMessage; protected final AtomicLong receivedLength = new AtomicLong(); /** @@ -51,11 +48,6 @@ public class DefaultHttpContentProcessor implements HttpContentProcessor { */ public DefaultHttpContentProcessor(NettyHttpRequest nettyHttpRequest, HttpServerConfiguration configuration) { this.nettyHttpRequest = nettyHttpRequest; - HttpRequest nativeRequest = nettyHttpRequest.getNativeRequest(); - if (!(nativeRequest instanceof StreamedHttpMessage)) { - throw new IllegalStateException("Streamed HTTP message expected"); - } - this.streamedHttpMessage = (StreamedHttpMessage) nativeRequest; this.configuration = configuration; this.requestMaxSize = configuration.getMaxRequestSize(); this.ctx = nettyHttpRequest.getChannelHandlerContext(); diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/DefaultHttpContentProcessorResolver.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/DefaultHttpContentProcessorResolver.java index 7ef1864547a..95a8a588c6e 100644 --- a/http-server-netty/src/main/java/io/micronaut/http/server/netty/DefaultHttpContentProcessorResolver.java +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/DefaultHttpContentProcessorResolver.java @@ -17,7 +17,6 @@ import io.micronaut.context.BeanLocator; import io.micronaut.context.BeanProvider; -import io.micronaut.core.annotation.AnnotationMetadata; import io.micronaut.core.annotation.Internal; import io.micronaut.core.annotation.NonNull; import io.micronaut.core.io.buffer.ByteBuffer; @@ -26,7 +25,6 @@ import io.micronaut.core.util.CopyOnWriteMap; import io.micronaut.http.HttpRequest; import io.micronaut.http.MediaType; -import io.micronaut.http.annotation.Body; import io.micronaut.http.server.netty.configuration.NettyHttpServerConfiguration; import io.micronaut.inject.ExecutionHandle; import io.micronaut.web.router.RouteMatch; @@ -75,24 +73,10 @@ class DefaultHttpContentProcessorResolver implements HttpContentProcessorResolve @Override @NonNull public HttpContentProcessor resolve(@NonNull NettyHttpRequest request, @NonNull RouteMatch route) { - Argument bodyType = route.getBodyArgument() - /* - The getBodyArgument() method returns arguments for functions where it is - not possible to dictate whether the argument is supposed to bind the entire - body or just a part of the body. We check to ensure the argument has the body - annotation to exclude that use case - */ - .filter(argument -> { - AnnotationMetadata annotationMetadata = argument.getAnnotationMetadata(); - if (annotationMetadata.hasAnnotation(Body.class)) { - return !annotationMetadata.stringValue(Body.class).isPresent(); - } else { - return false; - } - }) + Argument bodyType = route.getRouteInfo().getFullBodyArgument() .orElseGet(() -> { - if (route instanceof ExecutionHandle) { - for (Argument argument: ((ExecutionHandle) route).getArguments()) { + if (route instanceof ExecutionHandle executionHandle) { + for (Argument argument: executionHandle.getArguments()) { if (argument.getType() == HttpRequest.class) { return argument; } diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/FormDataHttpContentProcessor.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/FormDataHttpContentProcessor.java index 305a001bd08..7d7380c7dbc 100644 --- a/http-server-netty/src/main/java/io/micronaut/http/server/netty/FormDataHttpContentProcessor.java +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/FormDataHttpContentProcessor.java @@ -25,8 +25,8 @@ import io.netty.handler.codec.http.HttpRequest; import io.netty.handler.codec.http.multipart.Attribute; import io.netty.handler.codec.http.multipart.FileUpload; -import io.netty.handler.codec.http.multipart.HttpData; import io.netty.handler.codec.http.multipart.HttpDataFactory; +import io.netty.handler.codec.http.multipart.HttpPostMultipartRequestDecoder; import io.netty.handler.codec.http.multipart.HttpPostRequestDecoder; import io.netty.handler.codec.http.multipart.HttpPostStandardRequestDecoder; import io.netty.handler.codec.http.multipart.InterfaceHttpData; @@ -75,7 +75,7 @@ public class FormDataHttpContentProcessor extends AbstractHttpContentProcessor { HttpDataFactory factory = new MicronautHttpData.Factory(multipart, characterEncoding); final HttpRequest nativeRequest = nettyHttpRequest.getNativeRequest(); if (HttpPostRequestDecoder.isMultipart(nativeRequest)) { - this.decoder = new MicronautHttpPostMultipartRequestDecoder(factory, nativeRequest, characterEncoding); + this.decoder = new HttpPostMultipartRequestDecoder(factory, nativeRequest, characterEncoding); } else { this.decoder = new HttpPostStandardRequestDecoder(factory, nativeRequest, characterEncoding); } @@ -135,9 +135,9 @@ protected void onData(ByteBufHolder message, Collection out) { } InterfaceHttpData currentPartialHttpData = postRequestDecoder.currentPartialHttpData(); - if (currentPartialHttpData instanceof HttpData) { - // can't give away ownership of this data yet, so retain it - out.add(currentPartialHttpData.retain()); + if (currentPartialHttpData != null) { + out.add(currentPartialHttpData); + postRequestDecoder.removeHttpDataFromClean(currentPartialHttpData); } } catch (HttpPostRequestDecoder.EndOfDataDecoderException e) { diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/FormRouteCompleter.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/FormRouteCompleter.java index eb69dd21d41..e0bad169f38 100644 --- a/http-server-netty/src/main/java/io/micronaut/http/server/netty/FormRouteCompleter.java +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/FormRouteCompleter.java @@ -16,104 +16,106 @@ package io.micronaut.http.server.netty; import io.micronaut.core.annotation.Internal; -import io.micronaut.core.async.publisher.Publishers; -import io.micronaut.core.convert.ConversionService; -import io.micronaut.core.reflect.ClassUtils; -import io.micronaut.core.type.Argument; +import io.micronaut.core.annotation.Nullable; +import io.micronaut.core.execution.DelayedExecutionFlow; +import io.micronaut.core.io.buffer.ReferenceCounted; import io.micronaut.http.MediaType; import io.micronaut.http.multipart.PartData; -import io.micronaut.http.multipart.StreamingFileUpload; +import io.micronaut.http.server.netty.body.HttpBody; +import io.micronaut.http.server.netty.body.ImmediateMultiObjectBody; +import io.micronaut.http.server.netty.multipart.NettyCompletedFileUpload; import io.micronaut.http.server.netty.multipart.NettyPartData; -import io.micronaut.http.server.netty.multipart.NettyStreamingFileUpload; import io.micronaut.web.router.RouteMatch; -import io.netty.buffer.ByteBufHolder; -import io.netty.handler.codec.http.multipart.Attribute; +import io.netty.channel.EventLoop; import io.netty.handler.codec.http.multipart.FileUpload; -import io.netty.handler.codec.http.multipart.HttpData; +import org.reactivestreams.Subscriber; +import org.reactivestreams.Subscription; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import reactor.core.publisher.Flux; import reactor.core.publisher.Sinks; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; +import java.nio.charset.Charset; import java.util.HashMap; +import java.util.LinkedHashSet; import java.util.Map; import java.util.Optional; -import java.util.concurrent.atomic.AtomicLong; -import java.util.function.Supplier; +import java.util.Set; +import java.util.function.BiFunction; /** - * Extension of {@link BaseRouteCompleter} that handles incoming multipart data and binds - * parameters (e.g. {@link io.micronaut.http.annotation.Part}). + * Special {@link HttpBody} that "demultiplexes" form data. Basically, this class receives a stream + * of {@link MicronautHttpData} and splits it into individual streams for each form field, and they + * can all be subscribed to and bound independently. * * @since 4.0.0 * @author Jonas Konrad */ @Internal -final class FormRouteCompleter extends BaseRouteCompleter { - static final Argument ARGUMENT_PART_DATA = Argument.of(PartData.class); +public final class FormRouteCompleter implements Subscriber, HttpBody { private static final Logger LOG = LoggerFactory.getLogger(FormRouteCompleter.class); - private final NettyStreamingFileUpload.Factory fileUploadFactory; - private final ConversionService conversionService; - private final boolean alwaysAddContent = request.isFormData(); - private final AtomicLong pressureRequested = new AtomicLong(); - private final Map> subjectsByDataName = new HashMap<>(); - private final Collection> downstreamSubscribers = new ArrayList<>(); - - FormRouteCompleter(NettyStreamingFileUpload.Factory fileUploadFactory, ConversionService conversionService, NettyHttpRequest request, RouteMatch routeMatch) { - super(request, routeMatch); - this.fileUploadFactory = fileUploadFactory; - this.conversionService = conversionService; - } - - private void request(long n) { - pressureRequested.getAndUpdate(old -> { - if ((old + n) < old) { - return Long.MAX_VALUE; - } else { - return old + n; - } - }); - needsInput = true; - Runnable checkDemand = this.checkDemand; - if (checkDemand != null) { - checkDemand.run(); - } + final DelayedExecutionFlow> execute = DelayedExecutionFlow.create(); + private final NettyHttpRequest request; + private boolean executed; + private final RouteMatch routeMatch; + private Subscription upstreamSubscription; + private final Set> allData = new LinkedHashSet<>(); + private final Map claimants = new HashMap<>(); + private boolean upstreamDemanded = false; + + FormRouteCompleter(NettyHttpRequest request, RouteMatch routeMatch) { + this.request = request; + this.routeMatch = routeMatch; } - private Flux withFlowControl(Flux flux, MicronautHttpData data) { - return flux - .doOnComplete(data::release) - .doOnRequest(this::request); + @Override + public void onSubscribe(Subscription s) { + upstreamSubscription = s; + s.request(1); } @Override - protected void addHolder(ByteBufHolder holder) { - if (holder instanceof HttpData data) { - needsInput = pressureRequested.decrementAndGet() > 0; - addData((MicronautHttpData) data); - } else { - super.addHolder(holder); + public void onNext(Object o) { + try { + addData((MicronautHttpData) o); + } catch (Exception e) { + upstreamSubscription.cancel(); + onError(e); } } @Override - void completeSuccess() { - for (Sinks.Many subject : downstreamSubscribers) { - // subjects will ignore the onComplete if they're already done - subject.tryEmitComplete(); + public void onComplete() { + for (Claimant claimant : claimants.values()) { + claimant.sink.tryEmitComplete(); + } + if (!executed) { + executed = true; + execute.complete(routeMatch); } - super.completeSuccess(); } @Override - void completeFailure(Throwable failure) { - super.completeFailure(failure); - for (Sinks.Many subject : downstreamSubscribers) { - subject.tryEmitError(failure); + public void onError(Throwable failure) { + for (Claimant claimant : claimants.values()) { + claimant.sink.tryEmitError(failure); + } + for (Object toDiscard : routeMatch.getVariableValues().values()) { + if (toDiscard instanceof ReferenceCounted rc) { + rc.release(); + } + if (toDiscard instanceof io.netty.util.ReferenceCounted rc) { + rc.release(); + } + if (toDiscard instanceof NettyCompletedFileUpload fu) { + fu.discard(); + } + } + executed = true; + try { + execute.completeExceptionally(failure); + } catch (IllegalStateException ignored) { } } @@ -121,165 +123,204 @@ private void addData(MicronautHttpData data) { if (LOG.isTraceEnabled()) { LOG.trace("Received HTTP Data for request [{}]: {}", request, data); } + allData.add(data); + upstreamDemanded = false; String name = data.getName(); - Optional> requiredInput = routeMatch.getRequiredInput(name); - - if (requiredInput.isEmpty()) { - request.addContent(data); - request(1); + Claimant claimant = claimants.get(name); + if (claimant == null) { + upstreamSubscription.request(1); return; } - - Argument argument = requiredInput.get(); - Supplier value; - boolean isPublisher = Publishers.isConvertibleToPublisher(argument.getType()); - boolean chunkedProcessing = false; - - if (isPublisher) { - if (data.attachment == null) { - data.attachment = new HttpDataAttachment(); - // retain exactly once - data.retain(); + claimant.send(data); + if (!executed && routeMatch.isFulfilled()) { + executed = true; + execute.complete(routeMatch); + } + if (executed) { + if (!upstreamDemanded) { + for (Claimant other : claimants.values()) { + if (other.demand > 0) { + upstreamDemanded = true; + upstreamSubscription.request(1); + break; + } + } } + } else { + // while we still have unfulfilled parameters, request as much data as possible + upstreamSubscription.request(1); + } + } - Argument typeVariable; + /** + * Claim all fields of the given name. In the returned publisher, each + * {@link MicronautHttpData} may appear multiple times if there is new data. + * + * @param name The field name + * @return The publisher of data with this field name + */ + public Flux> claimFieldsRaw(String name) { + Claimant claimant = new Claimant(); + if (claimants.putIfAbsent(name, claimant) != null) { + throw new IllegalStateException("Field already claimed"); + } + return claimant.flux(); + } - if (StreamingFileUpload.class.isAssignableFrom(argument.getType())) { - typeVariable = ARGUMENT_PART_DATA; - } else { - typeVariable = argument.getFirstTypeVariable().orElse(Argument.OBJECT_ARGUMENT); - } - Class typeVariableType = typeVariable.getType(); + /** + * Claim all fields of the given name. When a new field of the name is seen, + * {@code fieldFactory} is called with that field and a publisher that gets the + * {@link PartData} every time there is new data for the field. + * + * @param name The field name + * @param fieldFactory The factory to call when a new field is seen + * @return A publisher of the objects returned by the factory + * @param The return type of the factory + */ + public Flux claimFields(String name, BiFunction, ? super Flux, R> fieldFactory) { + FieldSplitter proc = new FieldSplitter<>(fieldFactory); + claimFieldsRaw(name).subscribe(proc); + return proc.outer.asFlux(); + } - Sinks.Many namedSubject = subjectsByDataName.computeIfAbsent(name, key -> makeDownstreamUnicastProcessor()); + /** + * Claim all fields of the given name. The returned publisher will only contain fields that are + * {@link MicronautHttpData#isCompleted() completed}. + * + * @param name The field name + * @return The publisher of the complete fields + */ + public Flux> claimFieldsComplete(String name) { + return claimFieldsRaw(name).filter(MicronautHttpData::isCompleted); + } - chunkedProcessing = PartData.class.equals(typeVariableType) || - Publishers.isConvertibleToPublisher(typeVariableType) || - ClassUtils.isJavaLangType(typeVariableType); + @Override + public void release() { + for (MicronautHttpData data : allData) { + data.release(); + } + } - if (Publishers.isConvertibleToPublisher(typeVariableType)) { - boolean streamingFileUpload = StreamingFileUpload.class.isAssignableFrom(typeVariableType); - if (streamingFileUpload) { - typeVariable = ARGUMENT_PART_DATA; - } else { - typeVariable = typeVariable.getFirstTypeVariable().orElse(Argument.OBJECT_ARGUMENT); - } - if (data.attachment.subject == null) { - Sinks.Many childSubject = makeDownstreamUnicastProcessor(); - Flux flowable = withFlowControl(childSubject.asFlux(), data); - if (streamingFileUpload && data instanceof FileUpload fu) { - namedSubject.tryEmitNext(fileUploadFactory.create(fu, flowable)); - } else { - namedSubject.tryEmitNext(flowable); - } + @Nullable + @Override + public HttpBody next() { + return null; + } - data.attachment.subject = childSubject; - } - } + public Map asMap(Charset defaultCharset) { + return ImmediateMultiObjectBody.toMap(defaultCharset, allData); + } - Sinks.Many subject; + private class Claimant { + private final Sinks.Many> sink = Sinks.many().unicast().onBackpressureBuffer(); + private long demand; - if (data.attachment.subject != null) { - subject = data.attachment.subject; - } else { - subject = namedSubject; + public Flux> flux() { + return sink.asFlux().doOnRequest(this::request); + } + + private void request(long n) { + EventLoop eventLoop = request.getChannelHandlerContext().channel().eventLoop(); + if (!eventLoop.inEventLoop()) { + eventLoop.execute(() -> request(n)); + return; } - Object part = data; + long newDemand = demand + n; + if (newDemand < demand) { + newDemand = Long.MAX_VALUE; + } + demand = newDemand; + if (newDemand > 0) { + if (!upstreamDemanded) { + upstreamDemanded = true; + upstreamSubscription.request(1); + } + } + } - if (chunkedProcessing) { - MicronautHttpData.Chunk chunk = data.pollChunk(); - part = new NettyPartData(() -> { - if (data instanceof FileUpload fu) { - return Optional.of(MediaType.of(fu.getContentType())); - } else { - return Optional.empty(); - } - }, chunk::claim); + public void send(MicronautHttpData data) { + demand--; + if (sink.tryEmitNext(data) != Sinks.EmitResult.OK) { + if (LOG.isDebugEnabled()) { + LOG.debug("Failed to emit data for field {}", data.getName()); + } } + } + } - if (data instanceof FileUpload fu && - StreamingFileUpload.class.isAssignableFrom(argument.getType()) && - data.attachment.upload == null) { + private static class FieldSplitter implements Subscriber> { + final BiFunction, ? super Flux, R> fieldFactory; - data.attachment.upload = fileUploadFactory.create(fu, withFlowControl(subject.asFlux(), data)); - } + Subscription upstream; + final Sinks.Many outer = Sinks.many().unicast().onBackpressureBuffer(); + MicronautHttpData currentData = null; - Optional converted = conversionService.convert(part, typeVariable); + Sinks.Many innerSink; + boolean firstInner = true; - converted.ifPresent(subject::tryEmitNext); + FieldSplitter(BiFunction, ? super Flux, R> fieldFactory) { + this.fieldFactory = fieldFactory; + } - if (data.isCompleted() && chunkedProcessing) { - subject.tryEmitComplete(); - } + @Override + public void onSubscribe(Subscription s) { + upstream = s; + s.request(1); + } - value = () -> { - if (data.attachment.upload != null) { - return data.attachment.upload; - } else { - if (data.attachment.subject == null) { - return withFlowControl(namedSubject.asFlux(), data); - } else { - return namedSubject.asFlux(); - } + @Override + public void onNext(MicronautHttpData data) { + if (data != currentData) { + if (innerSink != null) { + innerSink.tryEmitComplete(); } - }; - } else { - if (data instanceof Attribute && !data.isCompleted()) { - request.addContent(data); - request(1); - return; + currentData = data; + innerSink = Sinks.many().unicast().onBackpressureBuffer(); + firstInner = true; + outer.tryEmitNext(fieldFactory.apply(data, innerSink.asFlux().doOnRequest(n -> { + if (firstInner) { + firstInner = false; + if (n != Long.MAX_VALUE) { + n--; + } + } + if (n != 0) { + upstream.request(n); + } + }))); + } + MicronautHttpData.Chunk chunk = data.pollChunk(); + if (chunk == null) { + upstream.request(1); } else { - value = () -> { - if (data.refCnt() > 0) { - return data; + NettyPartData part = new NettyPartData(() -> { + if (data instanceof FileUpload fileUpload) { + return Optional.of(MediaType.of(fileUpload.getContentType())); } else { - return null; + return Optional.empty(); } - }; + }, chunk::claim); + innerSink.tryEmitNext(part); } } - if (!execute) { - String argumentName = argument.getName(); - if (!routeMatch.isSatisfied(argumentName)) { - Object fulfillParamter = value.get(); - routeMatch = routeMatch.fulfill(Collections.singletonMap(argumentName, fulfillParamter)); - // we need to release the data here. However, if the route argument is a - // ByteBuffer, we need to retain the data until the route is executed. Adding - // the data to the request ensures it is cleaned up after the route completes. - if (!alwaysAddContent && fulfillParamter instanceof ByteBufHolder holder) { - request.addContent(holder); - } - } - if (isPublisher && chunkedProcessing) { - //accounting for the previous request - request(1); - } - if (routeMatch.isExecutable()) { - execute = true; + @Override + public void onError(Throwable t) { + outer.tryEmitError(t); + if (innerSink != null) { + innerSink.tryEmitError(t); } } - if (alwaysAddContent && !request.destroyed) { - request.addContent(data); - } - - if (!execute || !chunkedProcessing) { - request(1); + @Override + public void onComplete() { + outer.tryEmitComplete(); + if (innerSink != null) { + innerSink.tryEmitComplete(); + } } } - - private Sinks.Many makeDownstreamUnicastProcessor() { - Sinks.Many processor = Sinks.many().unicast().onBackpressureBuffer(); - downstreamSubscribers.add(processor); - return processor; - } - - static class HttpDataAttachment { - private Sinks.Many subject; - private StreamingFileUpload upload; - } } diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/HttpContentProcessor.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/HttpContentProcessor.java index 603c798dc0d..b68e709b6f0 100644 --- a/http-server-netty/src/main/java/io/micronaut/http/server/netty/HttpContentProcessor.java +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/HttpContentProcessor.java @@ -15,8 +15,10 @@ */ package io.micronaut.http.server.netty; +import io.micronaut.core.annotation.Nullable; import io.micronaut.core.type.Argument; import io.micronaut.core.util.Toggleable; +import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufHolder; import java.util.Collection; @@ -68,4 +70,16 @@ default void cancel() throws Throwable { default HttpContentProcessor resultType(Argument type) { return this; } + + /** + * Process a single {@link ByteBuf} into a single item, if possible. + * + * @param data The input data + * @return The output value, or {@code null} if this is unsupported. + * @throws Throwable Any failure + */ + @Nullable + default Object processSingle(ByteBuf data) throws Throwable { + return null; + } } diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/HttpContentProcessorAsReactiveProcessor.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/HttpContentProcessorAsReactiveProcessor.java index 504fcf74ae1..e63c0623e67 100644 --- a/http-server-netty/src/main/java/io/micronaut/http/server/netty/HttpContentProcessorAsReactiveProcessor.java +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/HttpContentProcessorAsReactiveProcessor.java @@ -16,7 +16,9 @@ package io.micronaut.http.server.netty; import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.NonNull; import io.micronaut.http.netty.stream.StreamedHttpMessage; +import io.netty.handler.codec.http.HttpContent; import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; @@ -44,13 +46,12 @@ private HttpContentProcessorAsReactiveProcessor() { * {@link org.reactivestreams.Processor}. * * @param processor The content processor to use - * @param request The request to subscribe to + * @param streamed The request to subscribe to * @return The publisher producing output data * @param The output element type */ - @SuppressWarnings("unchecked") - public static Publisher asPublisher(HttpContentProcessor processor, NettyHttpRequest request) { - StreamedHttpMessage streamed = (StreamedHttpMessage) request.getNativeRequest(); + @NonNull + public static Flux asPublisher(HttpContentProcessor processor, Publisher streamed) { return Flux.concat(Flux.from(streamed) .doOnError(e -> { try { diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/HttpPipelineBuilder.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/HttpPipelineBuilder.java index 1a4b638562b..9784bbf7d95 100644 --- a/http-server-netty/src/main/java/io/micronaut/http/server/netty/HttpPipelineBuilder.java +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/HttpPipelineBuilder.java @@ -41,6 +41,7 @@ import io.netty.handler.codec.http.FullHttpRequest; import io.netty.handler.codec.http.HttpContentDecompressor; import io.netty.handler.codec.http.HttpMessage; +import io.netty.handler.codec.http.HttpObjectAggregator; import io.netty.handler.codec.http.HttpServerCodec; import io.netty.handler.codec.http.HttpServerKeepAliveHandler; import io.netty.handler.codec.http.HttpServerUpgradeHandler; @@ -554,7 +555,16 @@ private void insertMicronautHandlers(boolean zeroCopySupported) { if (webSocketUpgradeHandler.isPresent()) { pipeline.addLast(NettyServerWebSocketUpgradeHandler.COMPRESSION_HANDLER, new WebSocketServerCompressionHandler()); } - pipeline.addLast(ChannelPipelineCustomizer.HANDLER_HTTP_STREAM, new HttpStreamsServerHandler()); + if (server.getServerConfiguration().getServerType() == NettyHttpServerConfiguration.HttpServerType.STREAMED) { + pipeline.addLast(ChannelPipelineCustomizer.HANDLER_HTTP_STREAM, new HttpStreamsServerHandler()); + } else { + pipeline.addLast(ChannelPipelineCustomizer.HANDLER_HTTP_AGGREGATOR, + new HttpObjectAggregator( + (int) server.getServerConfiguration().getMaxRequestSize(), + server.getServerConfiguration().isCloseOnExpectationFailed() + ) + ); + } pipeline.addLast(ChannelPipelineCustomizer.HANDLER_HTTP_CHUNK, new ChunkedWriteHandler()); pipeline.addLast(HttpRequestDecoder.ID, requestDecoder); if (server.getServerConfiguration().isDualProtocol() && server.getServerConfiguration().isHttpToHttpsRedirect() && !https) { @@ -580,7 +590,9 @@ private void insertHttp1DownstreamHandlers() { pipeline.addLast(ChannelPipelineCustomizer.HANDLER_ACCESS_LOGGER, accessLogHandler); } registerMicronautChannelHandlers(); - pipeline.addLast(ChannelPipelineCustomizer.HANDLER_FLOW_CONTROL, new FlowControlHandler()); + if (server.getServerConfiguration().getServerType() == NettyHttpServerConfiguration.HttpServerType.STREAMED) { + pipeline.addLast(ChannelPipelineCustomizer.HANDLER_FLOW_CONTROL, new FlowControlHandler()); + } pipeline.addLast(ChannelPipelineCustomizer.HANDLER_HTTP_KEEP_ALIVE, new HttpServerKeepAliveHandler()); insertMicronautHandlers(sslHandler == null && !https); diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/MicronautHttpData.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/MicronautHttpData.java index 7544b039b40..cc9bcae74d2 100644 --- a/http-server-netty/src/main/java/io/micronaut/http/server/netty/MicronautHttpData.java +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/MicronautHttpData.java @@ -82,11 +82,6 @@ public abstract sealed class MicronautHttpData extends Abstr long definedSize = 0; Charset charset; - /** - * Additional data for {@link FormRouteCompleter}. - */ - FormRouteCompleter.HttpDataAttachment attachment; - @Nullable @SuppressWarnings("rawtypes") private final ResourceLeakTracker tracker = LEAK_DETECTOR.get().track(this); @@ -619,7 +614,7 @@ private void loadFromDisk(int length) throws IOException { * * @return The contents of this chunk */ - ByteBuf claim() { + public ByteBuf claim() { lock.lock(); if (buf == null) { return Unpooled.EMPTY_BUFFER; diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/MicronautHttpPostMultipartRequestDecoder.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/MicronautHttpPostMultipartRequestDecoder.java deleted file mode 100644 index 908fff6f95f..00000000000 --- a/http-server-netty/src/main/java/io/micronaut/http/server/netty/MicronautHttpPostMultipartRequestDecoder.java +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright 2017-2020 original authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.micronaut.http.server.netty; - -import io.micronaut.core.annotation.Internal; -import io.netty.handler.codec.http.HttpRequest; -import io.netty.handler.codec.http.multipart.HttpDataFactory; -import io.netty.handler.codec.http.multipart.HttpPostMultipartRequestDecoder; -import io.netty.handler.codec.http.multipart.InterfaceHttpData; -import io.netty.util.ReferenceCountUtil; - -import java.nio.charset.Charset; - -/** - * Customized multipart decoder for custom destroy behavior. - * - * @author James Kleeh - * @since 2.5.5 - */ -@Internal -class MicronautHttpPostMultipartRequestDecoder extends HttpPostMultipartRequestDecoder { - - MicronautHttpPostMultipartRequestDecoder(HttpDataFactory factory, HttpRequest request, Charset charset) { - super(factory, request, charset); - } - - @Override - public void destroy() { - super.destroy(); - // release any data partially uploaded but not completed - final InterfaceHttpData data = currentPartialHttpData(); - if (data != null && data.refCnt() != 0) { - ReferenceCountUtil.safeRelease(data); - } - } -} diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/NettyHttpRequest.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/NettyHttpRequest.java index 471fc0c1163..78d300a56f4 100644 --- a/http-server-netty/src/main/java/io/micronaut/http/server/netty/NettyHttpRequest.java +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/NettyHttpRequest.java @@ -24,7 +24,8 @@ import io.micronaut.core.convert.value.MutableConvertibleValues; import io.micronaut.core.convert.value.MutableConvertibleValuesMap; import io.micronaut.core.type.Argument; -import io.micronaut.core.util.SupplierUtil; +import io.micronaut.core.util.StringUtils; +import io.micronaut.http.HttpAttributes; import io.micronaut.http.HttpHeaders; import io.micronaut.http.HttpMethod; import io.micronaut.http.HttpRequest; @@ -46,11 +47,12 @@ import io.micronaut.http.netty.stream.DefaultStreamedHttpRequest; import io.micronaut.http.netty.stream.StreamedHttpRequest; import io.micronaut.http.server.HttpServerConfiguration; -import io.micronaut.http.server.exceptions.InternalServerException; +import io.micronaut.http.server.netty.body.ByteBody; +import io.micronaut.http.server.netty.body.HttpBody; +import io.micronaut.http.server.netty.body.ImmediateMultiObjectBody; +import io.micronaut.http.server.netty.body.ImmediateSingleObjectBody; +import io.micronaut.http.server.netty.multipart.NettyCompletedFileUpload; import io.micronaut.web.router.RouteMatch; -import io.netty.buffer.ByteBuf; -import io.netty.buffer.ByteBufHolder; -import io.netty.buffer.CompositeByteBuf; import io.netty.buffer.Unpooled; import io.netty.channel.Channel; import io.netty.channel.ChannelHandlerContext; @@ -63,7 +65,6 @@ import io.netty.handler.codec.http.HttpHeaderNames; import io.netty.handler.codec.http.QueryStringDecoder; import io.netty.handler.codec.http.cookie.ClientCookieEncoder; -import io.netty.handler.codec.http.multipart.HttpData; import io.netty.handler.codec.http2.DefaultHttp2PushPromiseFrame; import io.netty.handler.codec.http2.Http2ConnectionHandler; import io.netty.handler.codec.http2.Http2FrameCodec; @@ -71,29 +72,21 @@ import io.netty.handler.codec.http2.Http2StreamChannelBootstrap; import io.netty.handler.codec.http2.HttpConversionUtil; import io.netty.handler.ssl.SslHandler; -import io.netty.util.ReferenceCountUtil; import io.netty.util.ReferenceCounted; import io.netty.util.concurrent.Future; import io.netty.util.concurrent.GenericFutureListener; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.io.IOException; import java.net.InetSocketAddress; import java.net.URI; import java.net.URISyntaxException; import java.nio.charset.Charset; -import java.util.ArrayList; -import java.util.Collection; import java.util.HashMap; import java.util.Iterator; -import java.util.LinkedHashMap; -import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Optional; -import java.util.function.Consumer; -import java.util.function.Supplier; /** * Delegates to the Netty {@link io.netty.handler.codec.http.HttpRequest} instance. @@ -150,20 +143,14 @@ public class NettyHttpRequest extends AbstractNettyHttpRequest implements // we do copy the weight and dependency id } - boolean destroyed = false; - private final NettyHttpHeaders headers; private final ChannelHandlerContext channelHandlerContext; private final HttpServerConfiguration serverConfiguration; private MutableConvertibleValues attributes; private NettyCookies nettyCookies; - private final List receivedContent = new ArrayList<>(); - private final Map receivedData = new LinkedHashMap<>(); - - private T bodyUnwrapped; - private Supplier> body; - private RouteMatch matchedRoute; - private boolean bodyRequired; + private final ByteBody body; + @Nullable + private FormRouteCompleter formRouteCompleter; /** * Set to {@code true} when the {@link #headers} may have been mutated. If this is not the case, @@ -179,10 +166,10 @@ public class NettyHttpRequest extends AbstractNettyHttpRequest implements private final BodyConvertor bodyConvertor = newBodyConvertor(); /** - * @param nettyRequest The {@link io.netty.handler.codec.http.HttpRequest} - * @param ctx The {@link ChannelHandlerContext} - * @param environment The Environment - * @param serverConfiguration The {@link HttpServerConfiguration} + * @param nettyRequest The {@link io.netty.handler.codec.http.HttpRequest} + * @param ctx The {@link ChannelHandlerContext} + * @param environment The Environment + * @param serverConfiguration The {@link HttpServerConfiguration} */ @SuppressWarnings("MagicNumber") public NettyHttpRequest(io.netty.handler.codec.http.HttpRequest nettyRequest, @@ -200,16 +187,35 @@ public NettyHttpRequest(io.netty.handler.codec.http.HttpRequest nettyRequest, this.serverConfiguration = serverConfiguration; this.channelHandlerContext = ctx; this.headers = new NettyHttpHeaders(nettyRequest.headers(), conversionService); - this.body = SupplierUtil.memoizedNonEmpty(() -> { - T built = (T) buildBody(); - this.bodyUnwrapped = built; - return Optional.ofNullable(built); - }); + this.body = ByteBody.of(nettyRequest); this.contentLength = headers.contentLength().orElse(-1); this.contentType = headers.contentType().orElse(null); this.origin = headers.getOrigin().orElse(null); } + public final ByteBody rootBody() { + return body; + } + + private HttpBody lastBody() { + HttpBody body = rootBody(); + while (true) { + HttpBody next = body.next(); + if (next == null) { + break; + } + body = next; + } + return body; + } + + public final FormRouteCompleter formRouteCompleter() { + if (formRouteCompleter == null) { + formRouteCompleter = new FormRouteCompleter(this, (RouteMatch) getAttribute(HttpAttributes.ROUTE_MATCH).get()); + } + return formRouteCompleter; + } + @Override public MutableHttpRequest mutate() { return new NettyMutableHttpRequest(); @@ -319,61 +325,33 @@ public MutableConvertibleValues getAttributes() { } @Override - public Optional getBody() { - return this.body.get(); - } - - /** - * @return A {@link CompositeByteBuf} - */ - protected Object buildBody() { - if (!receivedData.isEmpty()) { - Map body = new LinkedHashMap(receivedData.size()); - - for (HttpData data: receivedData.values()) { - String newValue = getContent(data); - //noinspection unchecked - body.compute(data.getName(), (key, oldValue) -> { - if (oldValue == null) { - return newValue; - } else if (oldValue instanceof Collection) { - //noinspection unchecked - ((Collection) oldValue).add(newValue); - return oldValue; - } else { - ArrayList values = new ArrayList<>(2); - values.add(oldValue); - values.add(newValue); - return values; - } - }); - } - return body; - } else if (!receivedContent.isEmpty()) { - int size = receivedContent.size(); - CompositeByteBuf byteBufs = channelHandlerContext.alloc().compositeBuffer(size); - for (ByteBufHolder holder : receivedContent) { - ByteBuf content = holder.content(); - if (content != null) { - content.touch(); - // need to retain content, because for addComponent "ownership of buffer is transferred to this CompositeByteBuf." - byteBufs.addComponent(true, content.retain()); - } + public HttpRequest setAttribute(CharSequence name, Object value) { + // This is the copy from the super method to avoid the type pollution + if (StringUtils.isNotEmpty(name)) { + if (value == null) { + getAttributes().remove(name.toString()); + } else { + getAttributes().put(name.toString(), value); } - return byteBufs; - } else { - return null; } + return this; } - private String getContent(HttpData data) { - String newValue; - try { - newValue = data.getString(serverConfiguration.getDefaultCharset()); - } catch (IOException e) { - throw new InternalServerException("Error retrieving or decoding the value for: " + data.getName()); + @Override + public Optional getBody() { + HttpBody lastBody = lastBody(); + if (lastBody instanceof ImmediateMultiObjectBody multi) { + lastBody = multi.single(serverConfiguration.getDefaultCharset(), channelHandlerContext.alloc()); + } + if (lastBody instanceof ImmediateSingleObjectBody single) { + //noinspection unchecked + return (Optional) Optional.ofNullable(single.valueUnclaimed()); + } else if (lastBody instanceof FormRouteCompleter frc) { + //noinspection unchecked + return (Optional) Optional.of(frc.asMap(serverConfiguration.getDefaultCharset())); + } else { + return Optional.empty(); } - return newValue; } @Override @@ -392,16 +370,27 @@ public Optional getBody(ArgumentConversionContext conversionContext */ @Internal public void release() { - destroyed = true; - Consumer releaseIfNecessary = this::releaseIfNecessary; - receivedContent.forEach(releaseIfNecessary); - receivedData.values().forEach(releaseIfNecessary); - releaseIfNecessary(bodyUnwrapped); + RouteMatch routeMatch = (RouteMatch) getAttribute(HttpAttributes.ROUTE_MATCH).orElse(null); + if (routeMatch != null) { + // discard parameters that have already been bound + for (Object toDiscard : routeMatch.getVariableValues().values()) { + if (toDiscard instanceof io.micronaut.core.io.buffer.ReferenceCounted rc) { + rc.release(); + } + if (toDiscard instanceof io.netty.util.ReferenceCounted rc) { + rc.release(); + } + if (toDiscard instanceof NettyCompletedFileUpload fu) { + fu.discard(); + } + } + } + body.release(); if (attributes != null) { - attributes.values().forEach(releaseIfNecessary); + attributes.values().forEach(this::releaseIfNecessary); } - if (nettyRequest instanceof StreamedHttpRequest) { - ((StreamedHttpRequest) nettyRequest).closeIfNoSubscriber(); + if (nettyRequest instanceof StreamedHttpRequest streamedHttpRequest) { + streamedHttpRequest.closeIfNoSubscriber(); } } @@ -409,8 +398,7 @@ public void release() { * @param value An object with a value */ protected void releaseIfNecessary(Object value) { - if (value instanceof ReferenceCounted) { - ReferenceCounted referenceCounted = (ReferenceCounted) value; + if (value instanceof ReferenceCounted referenceCounted) { int i = referenceCounted.refCnt(); if (i != 0) { referenceCounted.release(); @@ -418,69 +406,6 @@ protected void releaseIfNecessary(Object value) { } } - /** - * Sets the body. - * - * @param body The body to set - */ - @Internal - public void setBody(T body) { - ReferenceCountUtil.retain(body); - this.bodyUnwrapped = body; - this.body = () -> Optional.ofNullable(body); - bodyConvertor.cleanup(); - } - - /** - * @return Obtains the matched route - */ - @Internal - public RouteMatch getMatchedRoute() { - return matchedRoute; - } - - /** - * @param httpContent The HttpContent as {@link ByteBufHolder} - */ - @Internal - public void addContent(ByteBufHolder httpContent) { - httpContent.touch(); - if (httpContent instanceof MicronautHttpData) { - receivedData.computeIfAbsent(new IdentityWrapper(httpContent), key -> { - // released in release() - httpContent.retain(); - return (HttpData) httpContent; - }); - } else { - // released in release() - receivedContent.add(httpContent.retain()); - } - } - - /** - * @param matchedRoute The matched route - */ - @Internal - void setMatchedRoute(RouteMatch matchedRoute) { - this.matchedRoute = matchedRoute; - } - - /** - * @param bodyRequired Sets the body as required - */ - @Internal - void setBodyRequired(boolean bodyRequired) { - this.bodyRequired = bodyRequired; - } - - /** - * @return Whether the body is required - */ - @Internal - boolean isBodyRequired() { - return bodyRequired || HttpMethod.requiresRequestBody(getMethod()); - } - @Nullable private ChannelHandlerContext findConnectionHandler() { ChannelHandlerContext current = channelHandlerContext.pipeline().context(Http2ConnectionHandler.class); @@ -550,51 +475,51 @@ public PushCapableHttpRequest serverPush(@NonNull HttpRequest request) { // request used to compute the headers for the PUSH_PROMISE frame io.netty.handler.codec.http.HttpRequest outboundRequest = new DefaultHttpRequest( - inboundRequest.protocolVersion(), - inboundRequest.method(), - fixedUri.toString(), - inboundRequest.headers() + inboundRequest.protocolVersion(), + inboundRequest.method(), + fixedUri.toString(), + inboundRequest.headers() ); int ourStream = ((Http2StreamChannel) channelHandlerContext.channel()).stream().id(); HttpPipelineBuilder.StreamPipeline originalStreamPipeline = channelHandlerContext.channel().attr(HttpPipelineBuilder.STREAM_PIPELINE_ATTRIBUTE.get()).get(); new Http2StreamChannelBootstrap(channelHandlerContext.channel().parent()) - .handler(new ChannelInitializer() { - @Override - protected void initChannel(@NonNull Http2StreamChannel ch) throws Exception { - int newStream = ch.stream().id(); - - channelHandlerContext.write(new DefaultHttp2PushPromiseFrame(HttpConversionUtil.toHttp2Headers(outboundRequest, false)) - .stream(((Http2StreamChannel) channelHandlerContext.channel()).stream()) - .pushStream(ch.stream())); - - originalStreamPipeline.initializeChildPipelineForPushPromise(ch); - - inboundRequest.headers().setInt(HttpConversionUtil.ExtensionHeaderNames.STREAM_ID.text(), newStream); - inboundRequest.headers().setInt(HttpConversionUtil.ExtensionHeaderNames.STREAM_PROMISE_ID.text(), ourStream); - - // delay until our handling is complete - connectionHandlerContext.executor().execute(() -> { - try { - ch.pipeline().context(ChannelPipelineCustomizer.HANDLER_HTTP_DECODER).fireChannelRead(inboundRequest); - } catch (Exception e) { - LOG.warn("Failed to complete push promise", e); - } - }); - } - }) - .open() - .addListener((GenericFutureListener>) future -> { - try { - future.sync(); - } catch (Exception e) { - if (e instanceof InterruptedException) { - Thread.currentThread().interrupt(); + .handler(new ChannelInitializer() { + @Override + protected void initChannel(@NonNull Http2StreamChannel ch) throws Exception { + int newStream = ch.stream().id(); + + channelHandlerContext.write(new DefaultHttp2PushPromiseFrame(HttpConversionUtil.toHttp2Headers(outboundRequest, false)) + .stream(((Http2StreamChannel) channelHandlerContext.channel()).stream()) + .pushStream(ch.stream())); + + originalStreamPipeline.initializeChildPipelineForPushPromise(ch); + + inboundRequest.headers().setInt(HttpConversionUtil.ExtensionHeaderNames.STREAM_ID.text(), newStream); + inboundRequest.headers().setInt(HttpConversionUtil.ExtensionHeaderNames.STREAM_PROMISE_ID.text(), ourStream); + + // delay until our handling is complete + connectionHandlerContext.executor().execute(() -> { + try { + ch.pipeline().context(ChannelPipelineCustomizer.HANDLER_HTTP_DECODER).fireChannelRead(inboundRequest); + } catch (Exception e) { + LOG.warn("Failed to complete push promise", e); } - LOG.warn("Failed to complete push promise", e); + }); + } + }) + .open() + .addListener((GenericFutureListener>) future -> { + try { + future.sync(); + } catch (Exception e) { + if (e instanceof InterruptedException) { + Thread.currentThread().interrupt(); } - }); + LOG.warn("Failed to complete push promise", e); + } + }); return this; } else { throw new UnsupportedOperationException("Server push not supported by this client: Not a HTTP2 client"); @@ -610,7 +535,7 @@ protected Charset initCharset(Charset characterEncoding) { * @return Return true if the request is form data. */ @Internal - final boolean isFormOrMultipartData() { + public final boolean isFormOrMultipartData() { MediaType ct = getContentType().orElse(null); return ct != null && (ct.equals(MediaType.APPLICATION_FORM_URLENCODED_TYPE) || ct.equals(MediaType.MULTIPART_FORM_DATA_TYPE)); } @@ -619,7 +544,7 @@ final boolean isFormOrMultipartData() { * @return Return true if the request is form data. */ @Internal - final boolean isFormData() { + public final boolean isFormData() { MediaType ct = getContentType().orElse(null); return ct != null && (ct.equals(MediaType.APPLICATION_FORM_URLENCODED_TYPE)); } @@ -783,12 +708,12 @@ public io.netty.handler.codec.http.FullHttpRequest toFullHttpRequest() { return (io.netty.handler.codec.http.FullHttpRequest) NettyHttpRequest.this.nettyRequest; } else { return new DefaultFullHttpRequest( - nr.protocolVersion(), - nr.method(), - nr.uri(), - Unpooled.EMPTY_BUFFER, - nr.headers(), - EmptyHttpHeaders.INSTANCE + nr.protocolVersion(), + nr.method(), + nr.uri(), + Unpooled.EMPTY_BUFFER, + nr.headers(), + EmptyHttpHeaders.INSTANCE ); } } @@ -801,11 +726,11 @@ public StreamedHttpRequest toStreamHttpRequest() { } else { io.netty.handler.codec.http.FullHttpRequest fullHttpRequest = toFullHttpRequest(); DefaultStreamedHttpRequest request = new DefaultStreamedHttpRequest( - fullHttpRequest.protocolVersion(), - fullHttpRequest.method(), - fullHttpRequest.uri(), - true, - Publishers.just(new DefaultLastHttpContent(fullHttpRequest.content())) + fullHttpRequest.protocolVersion(), + fullHttpRequest.method(), + fullHttpRequest.uri(), + true, + Publishers.just(new DefaultLastHttpContent(fullHttpRequest.content())) ); request.headers().setAll(fullHttpRequest.headers()); return request; diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/NettyRequestArgumentSatisfier.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/NettyRequestArgumentSatisfier.java index b251f22fa75..fc0fcacab57 100644 --- a/http-server-netty/src/main/java/io/micronaut/http/server/netty/NettyRequestArgumentSatisfier.java +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/NettyRequestArgumentSatisfier.java @@ -17,14 +17,12 @@ import io.micronaut.context.annotation.Primary; import io.micronaut.core.annotation.Internal; -import io.micronaut.core.type.Argument; import io.micronaut.http.HttpRequest; import io.micronaut.http.bind.RequestBinderRegistry; import io.micronaut.http.server.binding.RequestArgumentSatisfier; +import io.micronaut.web.router.RouteMatch; import jakarta.inject.Singleton; -import java.util.Optional; - /** * A class containing methods to aid in satisfying arguments of a {@link io.micronaut.web.router.Route}. * @@ -39,6 +37,8 @@ @Internal public class NettyRequestArgumentSatisfier extends RequestArgumentSatisfier { + private final RequestBinderRegistry requestBinderRegistry; + /** * Constructor. * @@ -46,14 +46,12 @@ public class NettyRequestArgumentSatisfier extends RequestArgumentSatisfier { */ public NettyRequestArgumentSatisfier(RequestBinderRegistry requestBinderRegistry) { super(requestBinderRegistry); + this.requestBinderRegistry = requestBinderRegistry; } @Override - protected Optional getValueForArgument(Argument argument, HttpRequest request, boolean satisfyOptionals) { - if (request instanceof NettyHttpRequest) { - NettyHttpRequest nettyHttpRequest = (NettyHttpRequest) request; - nettyHttpRequest.setBodyRequired(true); - } - return super.getValueForArgument(argument, request, satisfyOptionals); + public void fulfillArgumentRequirementsBeforeFilters(RouteMatch route, HttpRequest request) { + super.fulfillArgumentRequirementsBeforeFilters(route, request); } + } diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/NettyRequestLifecycle.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/NettyRequestLifecycle.java index 634801b8f88..3c4ad673047 100644 --- a/http-server-netty/src/main/java/io/micronaut/http/server/netty/NettyRequestLifecycle.java +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/NettyRequestLifecycle.java @@ -17,7 +17,6 @@ import io.micronaut.core.annotation.Internal; import io.micronaut.core.annotation.Nullable; -import io.micronaut.core.execution.DelayedExecutionFlow; import io.micronaut.core.execution.ExecutionFlow; import io.micronaut.core.type.Argument; import io.micronaut.http.HttpMethod; @@ -27,21 +26,16 @@ import io.micronaut.http.MutableHttpResponse; import io.micronaut.http.annotation.Body; import io.micronaut.http.context.ServerRequestContext; -import io.micronaut.http.netty.stream.StreamedHttpRequest; import io.micronaut.http.server.RequestLifecycle; -import io.micronaut.http.server.multipart.MultipartBody; -import io.micronaut.http.server.netty.multipart.NettyStreamingFileUpload; +import io.micronaut.http.server.netty.body.ByteBody; import io.micronaut.http.server.netty.types.files.NettyStreamedFileCustomizableResponseType; import io.micronaut.http.server.netty.types.files.NettySystemFileCustomizableResponseType; import io.micronaut.http.server.types.files.FileCustomizableResponseType; import io.micronaut.web.router.MethodBasedRouteMatch; import io.micronaut.web.router.RouteMatch; -import io.netty.buffer.ByteBufHolder; import io.netty.channel.ChannelHandlerContext; import io.netty.handler.codec.DecoderResult; import io.netty.handler.codec.TooLongFrameException; -import org.reactivestreams.Subscriber; -import org.reactivestreams.Subscription; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -49,9 +43,6 @@ import java.net.URISyntaxException; import java.net.URL; import java.nio.file.Paths; -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; import java.util.Optional; @Internal @@ -139,49 +130,59 @@ protected ExecutionFlow> fulfillArguments(RouteMatch routeMatch * This method also sometimes fulfills more controller parameters with form data. */ private ExecutionFlow> waitForBody(RouteMatch routeMatch) { - if (!shouldReadBody(routeMatch)) { + // note: shouldReadBody only works when fulfill has been called at least once + if (nettyRequest.rootBody().next() != null) { ctx.read(); return ExecutionFlow.just(routeMatch); } - BaseRouteCompleter completer = nettyRequest.isFormOrMultipartData() ? - new FormRouteCompleter(new NettyStreamingFileUpload.Factory(rib.serverConfiguration.getMultipart(), rib.getIoExecutor()), rib.conversionService, nettyRequest, routeMatch) : - new BaseRouteCompleter(nettyRequest, routeMatch); HttpContentProcessor processor = rib.httpContentProcessorResolver.resolve(nettyRequest, routeMatch); - StreamingDataSubscriber pr = new StreamingDataSubscriber(completer, processor); - ((StreamedHttpRequest) nettyRequest.getNativeRequest()).subscribe(pr); - return pr.completion; + ByteBody rootBody = nettyRequest.rootBody(); + if (processor instanceof FormDataHttpContentProcessor && nettyRequest.isFormOrMultipartData()) { + FormRouteCompleter frc = nettyRequest.formRouteCompleter(); + try { + rootBody.processMulti(processor).handleForm(frc); + } catch (Throwable e) { + return ExecutionFlow.error(e); + } + return frc.execute; + } else if (needsBody(routeMatch)) { + return rootBody.buffer(nettyRequest.getChannelHandlerContext().alloc()) + .map(ibb -> { + try { + ibb.processMulti(processor); + } catch (RuntimeException e) { + throw e; + } catch (Throwable e) { + throw new RuntimeException(e); + } + return routeMatch; + }); + } else { + ctx.read(); + return ExecutionFlow.just(routeMatch); + } } void handleException(Throwable cause) { onError(cause).onComplete((response, throwable) -> rib.writeResponse(ctx, nettyRequest, response, throwable)); } - private boolean shouldReadBody(RouteMatch routeMatch) { - if (!HttpMethod.permitsRequestBody(request().getMethod())) { - return false; - } - if (!(nettyRequest.getNativeRequest() instanceof StreamedHttpRequest)) { - // Illegal state: The request body is required, so at this point we must have a StreamedHttpRequest + private boolean needsBody(RouteMatch routeMatch) { + if (!routeMatch.getRouteInfo().isPermitsRequestBody()) { return false; } if (routeMatch instanceof MethodBasedRouteMatch methodBasedRouteMatch) { - if (hasArg(methodBasedRouteMatch, MultipartBody.class)) { - // MultipartBody will subscribe to the request body in MultipartBodyArgumentBinder - return false; - } if (hasArg(methodBasedRouteMatch, HttpRequest.class)) { // HttpRequest argument in the method return true; } } - Optional> bodyArgument = routeMatch.getBodyArgument() - .filter(argument -> argument.getAnnotationMetadata().hasAnnotation(Body.class)); - if (bodyArgument.isPresent() && !routeMatch.isSatisfied(bodyArgument.get().getName())) { + if (routeMatch.getRouteInfo().getBodyArgument().isPresent()) { // Body argument in the method return true; } - // Might be some body parts - return !routeMatch.isExecutable(); + // Not annotated body argument + return !routeMatch.isFulfilled(); } private static boolean hasArg(MethodBasedRouteMatch methodBasedRouteMatch, Class type) { @@ -192,124 +193,4 @@ private static boolean hasArg(MethodBasedRouteMatch methodBasedRouteMatch, } return false; } - - private static class StreamingDataSubscriber implements Subscriber { - final DelayedExecutionFlow> completion = DelayedExecutionFlow.create(); - private boolean completed = false; - - private final List bufferList = new ArrayList<>(1); - private final HttpContentProcessor contentProcessor; - private final BaseRouteCompleter completer; - private Subscription upstream; - - private volatile boolean upstreamRequested = false; - private boolean downstreamDone = false; - - StreamingDataSubscriber(BaseRouteCompleter completer, HttpContentProcessor contentProcessor) { - this.completer = completer; - this.contentProcessor = contentProcessor; - } - - private void checkDemand() { - if (completer.needsInput && !upstreamRequested) { - upstreamRequested = true; - upstream.request(1); - } - } - - @Override - public void onSubscribe(Subscription s) { - if (upstream != null) { - throw new IllegalStateException("Only one upstream subscription allowed"); - } - upstream = s; - completer.checkDemand = this::checkDemand; - checkDemand(); - } - - private void sendToCompleter(Collection out) throws Throwable { - for (Object processed : out) { - boolean wasExecuted = completer.execute; - completer.add(processed); - if (!wasExecuted && completer.execute) { - executeRoute(); - } - } - } - - @Override - public void onNext(ByteBufHolder holder) { - upstreamRequested = false; - if (downstreamDone) { - // previous error - holder.release(); - return; - } - try { - bufferList.clear(); - contentProcessor.add(holder, bufferList); - sendToCompleter(bufferList); - checkDemand(); - } catch (Throwable t) { - handleError(t); - } - } - - @Override - public void onError(Throwable t) { - if (downstreamDone) { - // previous error - LOG.warn("Downstream already complete, dropping error", t); - return; - } - handleError(t); - } - - private void handleError(Throwable t) { - try { - upstream.cancel(); - } catch (Throwable o) { - t.addSuppressed(o); - } - try { - contentProcessor.cancel(); - } catch (Throwable o) { - t.addSuppressed(o); - } - completer.completeFailure(t); - // this may drop the exception if the route has already been executed. However, that is - // only the case if there are publisher parameters, and those will still receive the - // failure. Hopefully. - if (!completed) { - completion.completeExceptionally(t); - completed = true; - } - downstreamDone = true; - } - - @Override - public void onComplete() { - if (downstreamDone) { - // previous error - return; - } - try { - bufferList.clear(); - contentProcessor.complete(bufferList); - sendToCompleter(bufferList); - boolean wasExecuted = completer.execute; - completer.completeSuccess(); - if (!wasExecuted && completer.execute) { - executeRoute(); - } - } catch (Throwable t) { - handleError(t); - } - } - - private void executeRoute() { - completion.complete(completer.routeMatch); - completed = true; - } - } } diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/RoutingInBoundHandler.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/RoutingInBoundHandler.java index 5fd8a56a56f..a2a0ec95bbd 100644 --- a/http-server-netty/src/main/java/io/micronaut/http/server/netty/RoutingInBoundHandler.java +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/RoutingInBoundHandler.java @@ -38,6 +38,7 @@ import io.micronaut.http.codec.MediaTypeCodecRegistry; import io.micronaut.http.context.ServerRequestContext; import io.micronaut.http.context.event.HttpRequestTerminatedEvent; +import io.micronaut.http.exceptions.HttpStatusException; import io.micronaut.http.netty.NettyHttpResponseBuilder; import io.micronaut.http.netty.NettyMutableHttpResponse; import io.micronaut.http.netty.stream.JsonSubscriber; @@ -53,14 +54,17 @@ import io.micronaut.web.router.resource.StaticResourceResolver; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufOutputStream; +import io.netty.buffer.CompositeByteBuf; import io.netty.buffer.Unpooled; import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelFutureListener; import io.netty.channel.ChannelHandler.Sharable; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.SimpleChannelInboundHandler; import io.netty.handler.codec.http.DefaultFullHttpResponse; import io.netty.handler.codec.http.DefaultHttpContent; import io.netty.handler.codec.http.DefaultHttpHeaders; +import io.netty.handler.codec.http.DefaultHttpResponse; import io.netty.handler.codec.http.HttpContent; import io.netty.handler.codec.http.HttpHeaderNames; import io.netty.handler.codec.http.HttpHeaderValues; @@ -317,12 +321,50 @@ private void encodeHttpResponse( }); } else if (body instanceof Publisher) { response.body(null); - DelegateStreamedHttpResponse streamedResponse = new DelegateStreamedHttpResponse( - toNettyResponse(response), - mapToHttpContent(nettyRequest, response, body, context) - ); - context.writeAndFlush(streamedResponse); - context.read(); + if (serverConfiguration.getServerType() == NettyHttpServerConfiguration.HttpServerType.FULL_CONTENT) { + // HttpStreamsHandler is not present, so we can't write a StreamedHttpResponse. + Flux.from(mapToHttpContent(nettyRequest, response, body, context)).collectList().subscribe(contents -> { + if (contents.size() == 0) { + setResponseBody(response, Unpooled.EMPTY_BUFFER); + } else if (contents.size() == 1) { + setResponseBody(response, contents.get(0).content().retain()); + } else { + CompositeByteBuf composite = context.alloc().compositeBuffer(); + for (HttpContent c : contents) { + composite.addComponent(true, c.content().retain()); + } + setResponseBody(response, composite); + } + for (HttpContent content : contents) { + content.release(); + } + + writeFinalNettyResponse( + response, + nettyRequest, + context + ); + }, error -> { + if (LOG.isErrorEnabled()) { + LOG.error("Error occurred writing publisher response: " + error.getMessage(), error); + } + HttpResponseStatus responseStatus; + if (error instanceof HttpStatusException) { + responseStatus = HttpResponseStatus.valueOf(((HttpStatusException) error).getStatus().getCode(), error.getMessage()); + } else { + responseStatus = HttpResponseStatus.INTERNAL_SERVER_ERROR; + } + context.writeAndFlush(new DefaultHttpResponse(HttpVersion.HTTP_1_1, responseStatus)) + .addListener(ChannelFutureListener.CLOSE); + }); + } else { + DelegateStreamedHttpResponse streamedResponse = new DelegateStreamedHttpResponse( + toNettyResponse(response), + mapToHttpContent(nettyRequest, response, body, context) + ); + context.writeAndFlush(streamedResponse); + context.read(); + } } else { encodeResponseBody( context, @@ -562,8 +604,7 @@ private void writeFinalNettyResponse(MutableHttpResponse message, HttpRequest io.netty.handler.codec.http.HttpRequest nativeRequest = nettyHttpRequest.getNativeRequest(); - if (nativeRequest instanceof StreamedHttpRequest && !((StreamedHttpRequest) nativeRequest).isConsumed()) { - StreamedHttpRequest streamedHttpRequest = (StreamedHttpRequest) nativeRequest; + if (nativeRequest instanceof StreamedHttpRequest streamedHttpRequest && !streamedHttpRequest.isConsumed()) { // We have to clear the buffer of FlowControlHandler before writing the response // If this is a streamed request and there is still content to consume then subscribe // and write the buffer is empty. diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/binders/CompletableFutureBodyBinder.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/binders/CompletableFutureBodyBinder.java index c32a5c553db..0a5efb4b70c 100644 --- a/http-server-netty/src/main/java/io/micronaut/http/server/netty/binders/CompletableFutureBodyBinder.java +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/binders/CompletableFutureBodyBinder.java @@ -15,23 +15,21 @@ */ package io.micronaut.http.server.netty.binders; +import io.micronaut.context.BeanProvider; import io.micronaut.core.annotation.Internal; import io.micronaut.core.annotation.NonNull; -import io.micronaut.core.async.subscriber.CompletionAwareSubscriber; import io.micronaut.core.convert.ArgumentConversionContext; import io.micronaut.core.convert.ConversionService; +import io.micronaut.core.execution.ExecutionFlow; import io.micronaut.core.type.Argument; import io.micronaut.http.HttpRequest; -import io.micronaut.http.bind.binders.DefaultBodyAnnotationBinder; import io.micronaut.http.bind.binders.NonBlockingBodyArgumentBinder; -import io.micronaut.http.netty.stream.StreamedHttpRequest; -import io.micronaut.http.server.netty.HttpContentProcessor; -import io.micronaut.http.server.netty.HttpContentProcessorAsReactiveProcessor; +import io.micronaut.http.server.HttpServerConfiguration; import io.micronaut.http.server.netty.HttpContentProcessorResolver; import io.micronaut.http.server.netty.NettyHttpRequest; -import io.netty.buffer.ByteBufHolder; -import io.netty.util.ReferenceCountUtil; -import org.reactivestreams.Subscription; +import io.micronaut.http.server.netty.body.ByteBody; +import io.micronaut.http.server.netty.body.ImmediateByteBody; +import io.micronaut.http.server.netty.body.ImmediateSingleObjectBody; import java.util.Arrays; import java.util.List; @@ -47,20 +45,24 @@ * @since 1.0 */ @Internal -public class CompletableFutureBodyBinder extends DefaultBodyAnnotationBinder - implements NonBlockingBodyArgumentBinder { +public class CompletableFutureBodyBinder + implements NonBlockingBodyArgumentBinder> { - private static final Argument TYPE = Argument.of(CompletableFuture.class); + private static final Argument> TYPE = (Argument) Argument.of(CompletableFuture.class); private final HttpContentProcessorResolver httpContentProcessorResolver; + private final ConversionService conversionService; + private final BeanProvider httpServerConfiguration; /** * @param httpContentProcessorResolver The http content processor resolver * @param conversionService The conversion service + * @param httpServerConfiguration The server configuration */ - public CompletableFutureBodyBinder(HttpContentProcessorResolver httpContentProcessorResolver, ConversionService conversionService) { - super(conversionService); + public CompletableFutureBodyBinder(HttpContentProcessorResolver httpContentProcessorResolver, ConversionService conversionService, BeanProvider httpServerConfiguration) { this.httpContentProcessorResolver = httpContentProcessorResolver; + this.conversionService = conversionService; + this.httpServerConfiguration = httpServerConfiguration; } @NonNull @@ -70,66 +72,47 @@ public List> superTypes() { } @Override - public Argument argumentType() { + public Argument> argumentType() { return TYPE; } @Override - public BindingResult bind(ArgumentConversionContext context, HttpRequest source) { - if (source instanceof NettyHttpRequest) { - NettyHttpRequest nettyHttpRequest = (NettyHttpRequest) source; - io.netty.handler.codec.http.HttpRequest nativeRequest = ((NettyHttpRequest) source).getNativeRequest(); - if (nativeRequest instanceof StreamedHttpRequest) { - - CompletableFuture future = new CompletableFuture(); - Argument targetType = context.getFirstTypeVariable().orElse(Argument.OBJECT_ARGUMENT); - - HttpContentProcessor processor = httpContentProcessorResolver.resolve(nettyHttpRequest, targetType); - - HttpContentProcessorAsReactiveProcessor.asPublisher(processor, nettyHttpRequest).subscribe(new CompletionAwareSubscriber() { - @Override - protected void doOnSubscribe(Subscription subscription) { - subscription.request(1); - } - - @Override - protected void doOnNext(Object message) { - if (message instanceof ByteBufHolder) { - nettyHttpRequest.addContent((ByteBufHolder) message); - } else { - nettyHttpRequest.setBody(message); - } - // upstream producer gave us control of the message. release it now, if we still need it, - // nettyHttpRequest will have retained it - ReferenceCountUtil.release(message); - subscription.request(1); - } - - @Override - protected void doOnError(Throwable t) { - future.completeExceptionally(t); - } + public BindingResult> bind(ArgumentConversionContext> context, HttpRequest source) { + if (source instanceof NettyHttpRequest nhr) { + ByteBody rootBody = nhr.rootBody(); + if (rootBody instanceof ImmediateByteBody immediate && immediate.empty()) { + return BindingResult.EMPTY; + } - @Override - protected void doOnComplete() { - Optional> firstTypeParameter = context.getFirstTypeVariable(); - if (firstTypeParameter.isPresent()) { - Argument arg = firstTypeParameter.get(); - Optional converted = nettyHttpRequest.getBody(arg); - if (converted.isPresent()) { - future.complete(converted.get()); - } else { - future.completeExceptionally(new IllegalArgumentException("Cannot bind body to argument type: " + arg.getType().getName())); - } - } else { - future.complete(nettyHttpRequest.getBody().orElse(null)); + Optional> firstTypeParameter = context.getFirstTypeVariable(); + Argument targetType = firstTypeParameter.orElse(Argument.OBJECT_ARGUMENT); + try { + ExecutionFlow retFlow = rootBody + .buffer(nhr.getChannelHandlerContext().alloc()) + .map(bytes -> { + try { + return bytes.processSingle( + httpContentProcessorResolver.resolve(nhr, targetType), + httpServerConfiguration.get().getDefaultCharset(), + nhr.getChannelHandlerContext().alloc() + ); + } catch (RuntimeException e) { + throw e; + } catch (Throwable e) { + throw new RuntimeException(e); } + }); + CompletableFuture future = retFlow.map(immediateSingleObjectBody -> { + Object claimed = immediateSingleObjectBody.claimForExternal(); + if (firstTypeParameter.isPresent()) { + return PublisherBodyBinder.convertAndRelease(conversionService, context.with(targetType), claimed); + } else { + return claimed; } - }); - + }).toCompletableFuture(); return () -> Optional.of(future); - } else { - return BindingResult.EMPTY; + } catch (Throwable e) { + return () -> Optional.of(CompletableFuture.failedFuture(e)); } } else { return BindingResult.EMPTY; diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/binders/CompletedFileUploadBinder.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/binders/CompletedFileUploadBinder.java new file mode 100644 index 00000000000..6c8f31820fc --- /dev/null +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/binders/CompletedFileUploadBinder.java @@ -0,0 +1,86 @@ +/* + * Copyright 2017-2023 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.http.server.netty.binders; + +import io.micronaut.core.bind.annotation.Bindable; +import io.micronaut.core.convert.ArgumentConversionContext; +import io.micronaut.core.convert.ConversionService; +import io.micronaut.core.type.Argument; +import io.micronaut.http.bind.binders.PendingRequestBindingResult; +import io.micronaut.http.bind.binders.TypedRequestArgumentBinder; +import io.micronaut.http.multipart.CompletedFileUpload; +import io.micronaut.http.multipart.FileUpload; +import io.micronaut.http.server.netty.NettyHttpRequest; +import reactor.core.publisher.Mono; + +import java.util.List; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; + +/** + * Binds {@link CompletedFileUpload}. + * + * @author Denis Stepanov + * @since 4.0.0 + */ +public non-sealed class CompletedFileUploadBinder implements TypedRequestArgumentBinder, NettyRequestArgumentBinder { + + private static final Argument STREAMING_FILE_UPLOAD_ARGUMENT = Argument.of(CompletedFileUpload.class); + + private final ConversionService conversionService; + + public CompletedFileUploadBinder(ConversionService conversionService) { + this.conversionService = conversionService; + } + + @Override + public List> superTypes() { + return List.of(FileUpload.class); + } + + @Override + public BindingResult bindForNettyRequest(ArgumentConversionContext context, + NettyHttpRequest request) { + if (request.getContentType().isEmpty() || !request.isFormOrMultipartData()) { + return BindingResult.unsatisfied(); + } + + Argument argument = context.getArgument(); + String inputName = argument.getAnnotationMetadata().stringValue(Bindable.NAME).orElse(argument.getName()); + + CompletableFuture completableFuture = Mono.from(request.formRouteCompleter().claimFieldsComplete(inputName)) + .map(d -> conversionService.convertRequired(d, CompletedFileUpload.class)) + .toFuture(); + + return new PendingRequestBindingResult<>() { + + @Override + public boolean isPending() { + return !completableFuture.isDone(); + } + + @Override + public Optional getValue() { + return Optional.ofNullable(completableFuture.getNow(null)); + } + }; + } + + @Override + public Argument argumentType() { + return STREAMING_FILE_UPLOAD_ARGUMENT; + } +} diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/binders/InputStreamBodyBinder.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/binders/InputStreamBodyBinder.java index 9b77fb16970..c9b1a8a7a8b 100644 --- a/http-server-netty/src/main/java/io/micronaut/http/server/netty/binders/InputStreamBodyBinder.java +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/binders/InputStreamBodyBinder.java @@ -16,33 +16,19 @@ package io.micronaut.http.server.netty.binders; import io.micronaut.core.annotation.Internal; -import io.micronaut.core.async.subscriber.CompletionAwareSubscriber; import io.micronaut.core.convert.ArgumentConversionContext; import io.micronaut.core.type.Argument; import io.micronaut.http.HttpRequest; import io.micronaut.http.bind.binders.NonBlockingBodyArgumentBinder; -import io.micronaut.http.netty.stream.StreamedHttpRequest; -import io.micronaut.http.server.netty.HttpContentProcessor; -import io.micronaut.http.server.netty.HttpContentProcessorAsReactiveProcessor; import io.micronaut.http.server.netty.HttpContentProcessorResolver; import io.micronaut.http.server.netty.NettyHttpRequest; import io.micronaut.http.server.netty.NettyHttpServer; -import io.netty.buffer.ByteBuf; -import io.netty.buffer.ByteBufHolder; -import io.netty.buffer.ByteBufUtil; -import io.netty.buffer.EmptyByteBuf; -import org.reactivestreams.Subscription; +import io.micronaut.http.server.netty.body.ImmediateByteBody; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import reactor.core.publisher.Flux; -import reactor.core.scheduler.Schedulers; -import java.io.IOException; import java.io.InputStream; -import java.io.PipedInputStream; -import java.io.PipedOutputStream; import java.util.Optional; -import java.util.concurrent.ExecutorService; /** * Responsible for binding to a {@link InputStream} argument from the body of the request. @@ -57,16 +43,12 @@ public class InputStreamBodyBinder implements NonBlockingBodyArgumentBinder argumentType() { return TYPE; } - @SuppressWarnings("unchecked") @Override public BindingResult bind(ArgumentConversionContext context, HttpRequest source) { - if (source instanceof NettyHttpRequest) { - NettyHttpRequest nettyHttpRequest = (NettyHttpRequest) source; - io.netty.handler.codec.http.HttpRequest nativeRequest = nettyHttpRequest.getNativeRequest(); - if (nativeRequest instanceof StreamedHttpRequest) { - PipedOutputStream outputStream = new PipedOutputStream(); - try { - PipedInputStream inputStream = new PipedInputStream(outputStream) { - private volatile HttpContentProcessor processor; - - private synchronized void init() { - if (processor == null) { - processor = processorResolver.resolve(nettyHttpRequest, context.getArgument()); - Flux.from(HttpContentProcessorAsReactiveProcessor.asPublisher(processor, nettyHttpRequest)) - .publishOn(Schedulers.fromExecutor(executorService)) - .subscribe(new CompletionAwareSubscriber() { - - @Override - protected void doOnSubscribe(Subscription subscription) { - subscription.request(1); - } - - @Override - protected synchronized void doOnNext(ByteBufHolder message) { - if (LOG.isTraceEnabled()) { - LOG.trace("Server received streaming message for argument [{}]: {}", context.getArgument(), message); - } - ByteBuf content = message.content(); - if (!(content instanceof EmptyByteBuf)) { - try { - byte[] bytes = ByteBufUtil.getBytes(content); - outputStream.write(bytes, 0, bytes.length); - } catch (IOException e) { - subscription.cancel(); - return; - } finally { - content.release(); - } - } - subscription.request(1); - } - - @Override - protected synchronized void doOnError(Throwable t) { - if (LOG.isTraceEnabled()) { - LOG.trace("Server received error for argument [" + context.getArgument() + "]: " + t.getMessage(), t); - } - try { - outputStream.close(); - } catch (IOException ignored) { - } finally { - subscription.cancel(); - } - } - - @Override - protected synchronized void doOnComplete() { - if (LOG.isTraceEnabled()) { - LOG.trace("Done receiving messages for argument: {}", context.getArgument()); - } - try { - outputStream.close(); - } catch (IOException ignored) { - } - } - }); - } - } - - @Override - public synchronized int read(byte[] b, int off, int len) throws IOException { - init(); - return super.read(b, off, len); - } - - @Override - public synchronized int read() throws IOException { - init(); - return super.read(); - } - }; - - return () -> Optional.of(inputStream); - } catch (IOException e) { - context.reject(e); + if (source instanceof NettyHttpRequest nhr) { + if (nhr.rootBody() instanceof ImmediateByteBody imm && imm.empty()) { + return BindingResult.empty(); + } + try { + InputStream s = nhr.rootBody().processMulti(processorResolver.resolve(nhr, context.getArgument())).coerceToInputStream(nhr.getChannelHandlerContext().alloc()); + return () -> Optional.of(s); + } catch (Throwable t) { + if (LOG.isTraceEnabled()) { + LOG.trace("Server received error for argument [" + context.getArgument() + "]: " + t.getMessage(), t); } + return BindingResult.empty(); } } - return BindingResult.EMPTY; + return BindingResult.empty(); } } diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/binders/NettyBinderRegistrar.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/binders/NettyBinderRegistrar.java index 35810048258..5f35146b958 100644 --- a/http-server-netty/src/main/java/io/micronaut/http/server/netty/binders/NettyBinderRegistrar.java +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/binders/NettyBinderRegistrar.java @@ -26,6 +26,7 @@ import io.micronaut.http.server.HttpServerConfiguration; import io.micronaut.http.server.netty.HttpContentProcessorResolver; import io.micronaut.http.server.netty.multipart.MultipartBodyArgumentBinder; +import io.micronaut.http.server.netty.multipart.NettyStreamingFileUpload; import io.micronaut.scheduling.TaskExecutors; import jakarta.inject.Named; @@ -50,17 +51,17 @@ class NettyBinderRegistrar implements BeanCreatedEventListener httpServerConfiguration, - @Named(TaskExecutors.BLOCKING) BeanProvider executorService) { + @Named(TaskExecutors.BLOCKING) + BeanProvider executorService) { this.conversionService = conversionService; this.httpContentProcessorResolver = httpContentProcessorResolver; this.beanLocator = beanLocator; @@ -71,18 +72,35 @@ class NettyBinderRegistrar implements BeanCreatedEventListener event) { RequestBinderRegistry registry = event.getBean(); - registry.addRequestArgumentBinder(new CompletableFutureBodyBinder( + registry.addArgumentBinder(new CompletableFutureBodyBinder( httpContentProcessorResolver, - conversionService + conversionService, + httpServerConfiguration )); - registry.addRequestArgumentBinder(new MultipartBodyArgumentBinder( + registry.addArgumentBinder(new MultipartBodyArgumentBinder( beanLocator, httpServerConfiguration )); - registry.addRequestArgumentBinder(new InputStreamBodyBinder( - httpContentProcessorResolver, - executorService.get() + registry.addArgumentBinder(new InputStreamBodyBinder( + httpContentProcessorResolver )); + NettyStreamingFileUpload.Factory fileUploadFactory = new NettyStreamingFileUpload.Factory(httpServerConfiguration.get().getMultipart(), executorService.get()); + registry.addArgumentBinder(new StreamingFileUploadBinder( + conversionService, + fileUploadFactory) + ); + CompletedFileUploadBinder completedFileUploadBinder = new CompletedFileUploadBinder(conversionService); + registry.addArgumentBinder(completedFileUploadBinder); + PublisherPartUploadBinder publisherPartUploadBinder = new PublisherPartUploadBinder(conversionService, fileUploadFactory); + registry.addArgumentBinder(publisherPartUploadBinder); + PartUploadAnnotationBinder partUploadAnnotationBinder = new PartUploadAnnotationBinder<>( + conversionService, + completedFileUploadBinder, + publisherPartUploadBinder + ); + registry.addArgumentBinder(partUploadAnnotationBinder); + + registry.addUnmatchedRequestArgumentBinder(partUploadAnnotationBinder); return registry; } } diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/binders/NettyRequestArgumentBinder.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/binders/NettyRequestArgumentBinder.java new file mode 100644 index 00000000000..ff9e04a08a9 --- /dev/null +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/binders/NettyRequestArgumentBinder.java @@ -0,0 +1,52 @@ +/* + * Copyright 2017-2023 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.http.server.netty.binders; + +import io.micronaut.core.annotation.Experimental; +import io.micronaut.core.convert.ArgumentConversionContext; +import io.micronaut.http.HttpRequest; +import io.micronaut.http.bind.binders.RequestArgumentBinder; +import io.micronaut.http.server.netty.NettyHttpRequest; + +import java.util.Optional; + +/** + * A version of {@link RequestArgumentBinder} that requires {@link NettyHttpRequest}. + * @param A type + * @author Denis Stepanov + * @since 4.0.0 + */ +@Experimental +public sealed interface NettyRequestArgumentBinder extends RequestArgumentBinder + permits CompletedFileUploadBinder, PublisherPartUploadBinder, StreamingFileUploadBinder { + + @Override + default BindingResult bind(ArgumentConversionContext context, HttpRequest source) { + if (source instanceof NettyHttpRequest nettyHttpRequest) { + return bindForNettyRequest(context, nettyHttpRequest); + } + return BindingResult.EMPTY; + } + + /** + * Bind the given argument from the given source. + * + * @param context The {@link ArgumentConversionContext} + * @param nettyHttpRequest The netty http request + * @return An {@link Optional} of the value. If no binding was possible {@link Optional#empty()} + */ + BindingResult bindForNettyRequest(ArgumentConversionContext context, NettyHttpRequest nettyHttpRequest); +} diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/binders/PartUploadAnnotationBinder.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/binders/PartUploadAnnotationBinder.java new file mode 100644 index 00000000000..9727114e103 --- /dev/null +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/binders/PartUploadAnnotationBinder.java @@ -0,0 +1,99 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.http.server.netty.binders; + +import io.micronaut.core.bind.annotation.Bindable; +import io.micronaut.core.convert.ArgumentConversionContext; +import io.micronaut.core.convert.ConversionError; +import io.micronaut.core.convert.ConversionService; +import io.micronaut.core.type.Argument; +import io.micronaut.http.annotation.Part; +import io.micronaut.http.bind.binders.AnnotatedRequestArgumentBinder; +import io.micronaut.http.bind.binders.PendingRequestBindingResult; +import io.micronaut.http.netty.stream.StreamedHttpRequest; +import io.micronaut.http.server.netty.NettyHttpRequest; +import reactor.core.publisher.Mono; + +import java.util.List; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; + +/** + * Bind values annotated with {@link Part}. + * + * @param The part type + * @author Denis Stepanov + * @since 4.0.0 + */ +public class PartUploadAnnotationBinder implements AnnotatedRequestArgumentBinder, StreamedNettyRequestArgumentBinder { + + private final ConversionService conversionService; + private final CompletedFileUploadBinder completedFileUploadBinder; + private final PublisherPartUploadBinder publisherPartUploadBinder; + + public PartUploadAnnotationBinder(ConversionService conversionService, + CompletedFileUploadBinder completedFileUploadBinder, + PublisherPartUploadBinder publisherPartUploadBinder) { + this.conversionService = conversionService; + this.completedFileUploadBinder = completedFileUploadBinder; + this.publisherPartUploadBinder = publisherPartUploadBinder; + } + + @Override + public BindingResult bindForStreamedNettyRequest(ArgumentConversionContext context, + StreamedHttpRequest streamedHttpRequest, + NettyHttpRequest request) { + if (request.getContentType().isEmpty() || !request.isFormOrMultipartData()) { + return BindingResult.unsatisfied(); + } + if (completedFileUploadBinder.matches(context.getArgument().getType())) { + return completedFileUploadBinder.bind((ArgumentConversionContext) context, request); + } + if (publisherPartUploadBinder.matches(context.getArgument().getType())) { + return publisherPartUploadBinder.bind((ArgumentConversionContext) context, request); + } + + Argument argument = context.getArgument(); + String inputName = argument.getAnnotationMetadata().stringValue(Bindable.NAME).orElse(argument.getName()); + + CompletableFuture completableFuture = Mono.from(request.formRouteCompleter().claimFieldsComplete(inputName)) + .map(d -> conversionService.convert(d, argument.getType(), context).orElse(null)) + .toFuture(); + + return new PendingRequestBindingResult<>() { + + @Override + public boolean isPending() { + return !completableFuture.isDone(); + } + + @Override + public List getConversionErrors() { + return context.getLastError().map(List::of).orElseGet(List::of); + } + + @Override + public Optional getValue() { + return Optional.ofNullable(completableFuture.getNow(null)); + } + }; + } + + @Override + public Class getAnnotationType() { + return Part.class; + } +} diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/binders/PublisherBodyBinder.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/binders/PublisherBodyBinder.java index 0bcd9eeec4c..bab66049422 100644 --- a/http-server-netty/src/main/java/io/micronaut/http/server/netty/binders/PublisherBodyBinder.java +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/binders/PublisherBodyBinder.java @@ -15,30 +15,27 @@ */ package io.micronaut.http.server.netty.binders; -import io.micronaut.core.async.subscriber.CompletionAwareSubscriber; import io.micronaut.core.convert.ArgumentConversionContext; import io.micronaut.core.convert.ConversionError; import io.micronaut.core.convert.ConversionService; import io.micronaut.core.convert.exceptions.ConversionErrorException; +import io.micronaut.core.io.buffer.ReferenceCounted; import io.micronaut.core.type.Argument; import io.micronaut.http.HttpRequest; -import io.micronaut.http.bind.binders.DefaultBodyAnnotationBinder; import io.micronaut.http.bind.binders.NonBlockingBodyArgumentBinder; -import io.micronaut.http.netty.stream.StreamedHttpRequest; -import io.micronaut.http.server.netty.HttpContentProcessor; -import io.micronaut.http.server.netty.HttpContentProcessorAsReactiveProcessor; import io.micronaut.http.server.netty.HttpContentProcessorResolver; import io.micronaut.http.server.netty.NettyHttpRequest; import io.micronaut.http.server.netty.NettyHttpServer; +import io.micronaut.http.server.netty.body.ImmediateByteBody; import io.micronaut.web.router.exceptions.UnsatisfiedRouteException; +import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufHolder; -import io.netty.buffer.EmptyByteBuf; -import io.netty.util.ReferenceCounted; +import io.netty.util.ReferenceCountUtil; import jakarta.inject.Singleton; import org.reactivestreams.Publisher; -import org.reactivestreams.Subscription; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import reactor.core.publisher.Mono; import java.util.Optional; @@ -49,12 +46,14 @@ * @since 1.0 */ @Singleton -public class PublisherBodyBinder extends DefaultBodyAnnotationBinder implements NonBlockingBodyArgumentBinder { +public class PublisherBodyBinder implements NonBlockingBodyArgumentBinder> { + public static final String MSG_CONVERT_DEBUG = "Cannot convert message for argument [{}] and value: {}"; private static final Logger LOG = LoggerFactory.getLogger(NettyHttpServer.class); - private static final Argument TYPE = Argument.of(Publisher.class); + private static final Argument> TYPE = (Argument) Argument.of(Publisher.class); private final HttpContentProcessorResolver httpContentProcessorResolver; + private final ConversionService conversionService; /** * @param conversionService The conversion service @@ -62,102 +61,89 @@ public class PublisherBodyBinder extends DefaultBodyAnnotationBinder */ public PublisherBodyBinder(ConversionService conversionService, HttpContentProcessorResolver httpContentProcessorResolver) { - super(conversionService); this.httpContentProcessorResolver = httpContentProcessorResolver; + this.conversionService = conversionService; } @Override - public Argument argumentType() { + public Argument> argumentType() { return TYPE; } @Override - public BindingResult bind(ArgumentConversionContext context, HttpRequest source) { - if (source instanceof NettyHttpRequest) { - NettyHttpRequest nettyHttpRequest = (NettyHttpRequest) source; - io.netty.handler.codec.http.HttpRequest nativeRequest = nettyHttpRequest.getNativeRequest(); - if (nativeRequest instanceof StreamedHttpRequest) { - Argument targetType = context.getFirstTypeVariable().orElse(Argument.OBJECT_ARGUMENT); - - HttpContentProcessor processor = httpContentProcessorResolver.resolve(nettyHttpRequest, targetType); - - //noinspection unchecked - return () -> Optional.of(subscriber -> HttpContentProcessorAsReactiveProcessor.asPublisher(processor.resultType(context.getArgument()), nettyHttpRequest).subscribe(new CompletionAwareSubscriber<>() { - - Subscription s; - - @Override - protected void doOnSubscribe(Subscription subscription) { - this.s = subscription; - subscriber.onSubscribe(subscription); - } - - @Override - protected void doOnNext(Object message) { - if (LOG.isTraceEnabled()) { - LOG.trace("Server received streaming message for argument [{}]: {}", context.getArgument(), message); - } - if (message instanceof ByteBufHolder) { - message = ((ByteBufHolder) message).content(); - if (message instanceof EmptyByteBuf) { - s.request(1); - return; - } - } - + public BindingResult> bind(ArgumentConversionContext> context, HttpRequest source) { + if (source instanceof NettyHttpRequest nhr) { + if (nhr.rootBody() instanceof ImmediateByteBody imm && imm.empty()) { + return BindingResult.empty(); + } + Argument targetType = context.getFirstTypeVariable().orElse(Argument.OBJECT_ARGUMENT); + try { + Publisher publisher = nhr.rootBody() + .processMulti(httpContentProcessorResolver.resolve(nhr, targetType).resultType(context.getArgument())) + .mapNotNull(o -> { ArgumentConversionContext conversionContext = context.with(targetType); - Optional converted = conversionService.convert(message, conversionContext); - - if (converted.isPresent()) { - subscriber.onNext(converted.get()); - } else { - - try { - Optional lastError = conversionContext.getLastError(); - if (lastError.isPresent()) { - if (LOG.isDebugEnabled()) { - LOG.debug("Cannot convert message for argument [" + context.getArgument() + "] and value: " + message, lastError.get()); - } - subscriber.onError(new ConversionErrorException(context.getArgument(), lastError.get())); - } else { - if (LOG.isDebugEnabled()) { - LOG.debug("Cannot convert message for argument [{}] and value: {}", context.getArgument(), message); - } - subscriber.onError(UnsatisfiedRouteException.create(context.getArgument())); - } - } finally { - s.cancel(); - } - } - - if (message instanceof ReferenceCounted) { - ((ReferenceCounted) message).release(); - } - } + return convertAndRelease(conversionService, conversionContext, o); + }) + .asPublisher(); + return () -> Optional.of(publisher); + } catch (Throwable t) { + if (LOG.isTraceEnabled()) { + LOG.trace("Server received error for argument [" + context.getArgument() + "]: " + t.getMessage(), t); + } + return () -> Optional.of(Mono.error(t)); + } + } + return BindingResult.empty(); + } - @Override - protected void doOnError(Throwable t) { - if (LOG.isTraceEnabled()) { - LOG.trace("Server received error for argument [" + context.getArgument() + "]: " + t.getMessage(), t); - } - try { - subscriber.onError(t); - } finally { - s.cancel(); - } - } + private static RuntimeException extractError(Object message, ArgumentConversionContext conversionContext) { + Optional lastError = conversionContext.getLastError(); + if (lastError.isPresent()) { + if (LOG.isDebugEnabled()) { + LOG.debug(MSG_CONVERT_DEBUG, conversionContext.getArgument(), lastError.get()); + } + return new ConversionErrorException(conversionContext.getArgument(), lastError.get()); + } else { + if (LOG.isDebugEnabled()) { + LOG.debug(MSG_CONVERT_DEBUG, conversionContext.getArgument(), message); + } + return UnsatisfiedRouteException.create(conversionContext.getArgument()); + } + } - @Override - protected void doOnComplete() { - if (LOG.isTraceEnabled()) { - LOG.trace("Done receiving messages for argument: {}", context.getArgument()); - } - subscriber.onComplete(); - } + /** + * This method converts a potentially + * {@link io.netty.util.ReferenceCounted netty reference counted} and transfers release + * ownership to the new object. + * + * @param conversionService The conversion service + * @param conversionContext The context to convert to + * @param o The object to convert + * @return The converted object + */ + static Object convertAndRelease(ConversionService conversionService, ArgumentConversionContext conversionContext, Object o) { + try { + if (o instanceof ByteBufHolder holder) { + o = holder.content(); + if (!((ByteBuf) o).isReadable()) { + return null; + } + } - })); + Optional converted = conversionService.convert(o, conversionContext); + if (converted.isPresent()) { + Object conv = converted.get(); + if (conv instanceof ReferenceCounted rc) { + rc.retain(); + } else if (conv instanceof io.netty.util.ReferenceCounted rc) { + rc.retain(); + } + return conv; + } else { + throw extractError(o, conversionContext); } + } finally { + ReferenceCountUtil.release(o); } - return BindingResult.EMPTY; } } diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/binders/PublisherPartUploadBinder.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/binders/PublisherPartUploadBinder.java new file mode 100644 index 00000000000..94335740443 --- /dev/null +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/binders/PublisherPartUploadBinder.java @@ -0,0 +1,110 @@ +/* + * Copyright 2017-2023 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.http.server.netty.binders; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.bind.annotation.Bindable; +import io.micronaut.core.convert.ArgumentConversionContext; +import io.micronaut.core.convert.ConversionService; +import io.micronaut.core.reflect.ClassUtils; +import io.micronaut.core.type.Argument; +import io.micronaut.http.MediaType; +import io.micronaut.http.bind.binders.TypedRequestArgumentBinder; +import io.micronaut.http.multipart.PartData; +import io.micronaut.http.multipart.StreamingFileUpload; +import io.micronaut.http.server.netty.MicronautHttpData; +import io.micronaut.http.server.netty.NettyHttpRequest; +import io.micronaut.http.server.netty.multipart.NettyPartData; +import io.micronaut.http.server.netty.multipart.NettyStreamingFileUpload; +import io.netty.handler.codec.http.multipart.FileUpload; +import org.reactivestreams.Publisher; +import reactor.core.publisher.Flux; + +import java.util.Optional; + +/** + * Bind publisher annotated {@link io.micronaut.http.annotation.Part}. + * + * @author Denis Stepanov + * @since 4.0.0 + */ +@Internal +final class PublisherPartUploadBinder implements TypedRequestArgumentBinder>, NettyRequestArgumentBinder> { + + private static final Argument> PUBLISHER_ARGUMENT = (Argument) Argument.of(Publisher.class); + private static final Argument PART_DATA_ARGUMENT = Argument.of(PartData.class); + + private final ConversionService conversionService; + private final NettyStreamingFileUpload.Factory fileUploadFactory; + + public PublisherPartUploadBinder(ConversionService conversionService, NettyStreamingFileUpload.Factory fileUploadFactory) { + this.conversionService = conversionService; + this.fileUploadFactory = fileUploadFactory; + } + + @Override + public BindingResult> bindForNettyRequest(ArgumentConversionContext> context, + NettyHttpRequest request) { + if (request.getContentType().isEmpty() || !request.isFormOrMultipartData()) { + return BindingResult.unsatisfied(); + } + + Argument> argument = context.getArgument(); + String inputName = argument.getAnnotationMetadata().stringValue(Bindable.NAME).orElse(argument.getName()); + + Argument contentArgument = argument.getFirstTypeVariable().orElse(Argument.OBJECT_ARGUMENT); + Class contentTypeClass = contentArgument.getType(); + + Flux publisher; + if (contentTypeClass == StreamingFileUpload.class) { + publisher = request.formRouteCompleter().claimFields(inputName, (data, flux) -> fileUploadFactory.create((FileUpload) data, flux)); + } else if (contentTypeClass == Publisher.class) { + Argument nestedType = contentArgument.getFirstTypeVariable().orElse(Argument.OBJECT_ARGUMENT); + publisher = request.formRouteCompleter() + .claimFields(inputName, (data, flux) -> flux.mapNotNull(partData -> conversionService.convert(partData, nestedType).orElse(null))); + } else { + Flux> raw = request.formRouteCompleter().claimFieldsRaw(inputName); + Flux mnTypeIfNecessary; + if (contentTypeClass == PartData.class || ClassUtils.isJavaLangType(contentTypeClass)) { + mnTypeIfNecessary = raw + .mapNotNull(data -> { + MicronautHttpData.Chunk chunk = data.pollChunk(); + if (chunk != null) { + return new NettyPartData(() -> { + if (data instanceof FileUpload fileUpload) { + return Optional.of(MediaType.of(fileUpload.getContentType())); + } else { + return Optional.empty(); + } + }, chunk::claim); + } else { + return null; + } + }); + } else { + mnTypeIfNecessary = raw; + } + publisher = mnTypeIfNecessary.mapNotNull(it -> conversionService.convert(it, contentArgument).orElse(null)); + } + + return () -> Optional.of(publisher); + } + + @Override + public Argument> argumentType() { + return PUBLISHER_ARGUMENT; + } +} diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/binders/StreamedNettyRequestArgumentBinder.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/binders/StreamedNettyRequestArgumentBinder.java new file mode 100644 index 00000000000..aa4d32137b3 --- /dev/null +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/binders/StreamedNettyRequestArgumentBinder.java @@ -0,0 +1,59 @@ +/* + * Copyright 2017-2023 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.http.server.netty.binders; + +import io.micronaut.core.annotation.Experimental; +import io.micronaut.core.convert.ArgumentConversionContext; +import io.micronaut.http.HttpRequest; +import io.micronaut.http.bind.binders.RequestArgumentBinder; +import io.micronaut.http.netty.stream.StreamedHttpRequest; +import io.micronaut.http.server.netty.NettyHttpRequest; + +import java.util.Optional; + +/** + * A version of {@link RequestArgumentBinder} that requires {@link NettyHttpRequest} and {@link StreamedHttpRequest}. + * + * @param A type + * @author Denis Stepanov + * @since 4.0.0 + */ +@Experimental +public interface StreamedNettyRequestArgumentBinder extends RequestArgumentBinder { + + @Override + default BindingResult bind(ArgumentConversionContext context, HttpRequest source) { + if (source instanceof NettyHttpRequest nettyHttpRequest) { + io.netty.handler.codec.http.HttpRequest nativeRequest = nettyHttpRequest.getNativeRequest(); + if (nativeRequest instanceof StreamedHttpRequest streamedHttpRequest) { + return bindForStreamedNettyRequest(context, streamedHttpRequest, nettyHttpRequest); + } + } + return BindingResult.EMPTY; + } + + /** + * Bind the given argument from the given source. + * + * @param context The {@link ArgumentConversionContext} + * @param streamedHttpRequest The streamed HTTP request + * @param nettyHttpRequest The netty http request + * @return An {@link Optional} of the value. If no binding was possible {@link Optional#empty()} + */ + BindingResult bindForStreamedNettyRequest(ArgumentConversionContext context, + StreamedHttpRequest streamedHttpRequest, + NettyHttpRequest nettyHttpRequest); +} diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/binders/StreamingFileUploadBinder.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/binders/StreamingFileUploadBinder.java new file mode 100644 index 00000000000..4355d5f7eaa --- /dev/null +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/binders/StreamingFileUploadBinder.java @@ -0,0 +1,81 @@ +/* + * Copyright 2017-2023 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.http.server.netty.binders; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.bind.annotation.Bindable; +import io.micronaut.core.convert.ArgumentConversionContext; +import io.micronaut.core.convert.ConversionService; +import io.micronaut.core.type.Argument; +import io.micronaut.http.bind.binders.PendingRequestBindingResult; +import io.micronaut.http.bind.binders.TypedRequestArgumentBinder; +import io.micronaut.http.multipart.StreamingFileUpload; +import io.micronaut.http.server.netty.NettyHttpRequest; +import io.micronaut.http.server.netty.multipart.NettyStreamingFileUpload; +import io.netty.handler.codec.http.multipart.FileUpload; +import reactor.core.publisher.Mono; + +import java.util.Optional; +import java.util.concurrent.CompletableFuture; + +/** + * Binds {@link StreamingFileUpload}. + * + * @author Denis Stepanov + * @since 4.0.0 + */ +@Internal +final class StreamingFileUploadBinder implements TypedRequestArgumentBinder, NettyRequestArgumentBinder { + + private static final Argument STREAMING_FILE_UPLOAD_ARGUMENT = Argument.of(StreamingFileUpload.class); + + private final ConversionService conversionService; + private final NettyStreamingFileUpload.Factory fileUploadFactory; + + public StreamingFileUploadBinder(ConversionService conversionService, NettyStreamingFileUpload.Factory fileUploadFactory) { + this.conversionService = conversionService; + this.fileUploadFactory = fileUploadFactory; + } + + @Override + public BindingResult bindForNettyRequest(ArgumentConversionContext context, + NettyHttpRequest request) { + + Argument argument = context.getArgument(); + String inputName = argument.getAnnotationMetadata().stringValue(Bindable.NAME).orElse(argument.getName()); + + CompletableFuture completableFuture = Mono.from( + request.formRouteCompleter().claimFields(inputName, (data, publisher) -> fileUploadFactory.create((FileUpload) data, publisher))).toFuture(); + + return new PendingRequestBindingResult<>() { + + @Override + public boolean isPending() { + return !completableFuture.isDone(); + } + + @Override + public Optional getValue() { + return Optional.ofNullable(completableFuture.getNow(null)); + } + }; + } + + @Override + public Argument argumentType() { + return STREAMING_FILE_UPLOAD_ARGUMENT; + } +} diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/body/ByteBody.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/body/ByteBody.java new file mode 100644 index 00000000000..29e6cfc7cbb --- /dev/null +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/body/ByteBody.java @@ -0,0 +1,66 @@ +/* + * Copyright 2017-2023 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.http.server.netty.body; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.execution.ExecutionFlow; +import io.micronaut.http.netty.stream.StreamedHttpRequest; +import io.micronaut.http.server.netty.HttpContentProcessor; +import io.netty.buffer.ByteBufAllocator; +import io.netty.handler.codec.http.FullHttpRequest; +import io.netty.handler.codec.http.HttpRequest; + +/** + * Base class for a raw {@link HttpBody} with just bytes. + * + * @since 4.0.0 + * @author Jonas Konrad + */ +@Internal +public sealed interface ByteBody extends HttpBody permits ImmediateByteBody, StreamingByteBody { + /** + * Process this body using the given processor. + * + * @param processor The processor to apply + * @return The new processed body + * @throws Throwable Any exception thrown by the processor. Not all processing failures may + * throw immediately, however + */ + MultiObjectBody processMulti(HttpContentProcessor processor) throws Throwable; + + /** + * Fully buffer this body. + * + * @param alloc The allocator for storage + * @return A flow that completes when all data has been read + */ + ExecutionFlow buffer(ByteBufAllocator alloc); + + /** + * Create a byte body for the given request. The request must be either a + * {@link FullHttpRequest} or a {@link StreamedHttpRequest}. + * + * @param request The request + * @return The {@link ByteBody} for the body data + */ + static ByteBody of(HttpRequest request) { + if (request instanceof FullHttpRequest full) { + return new ImmediateByteBody(full.content()); + } else { + return new StreamingByteBody((StreamedHttpRequest) request); + } + } +} diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/body/HttpBody.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/body/HttpBody.java new file mode 100644 index 00000000000..5df772701ec --- /dev/null +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/body/HttpBody.java @@ -0,0 +1,44 @@ +/* + * Copyright 2017-2023 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.http.server.netty.body; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.Nullable; + +/** + *

Base type for a representation of an HTTP request body.

+ *

Exactly one HttpBody holds control over a request body at a time. When a transformation of + * the body is performed, e.g. multipart processing, the new HttpBody takes control and the old one + * becomes invalid. The new body will be available via {@link #next()}.

+ * + * @since 4.0.0 + * @author Jonas Konrad + */ +@Internal +public interface HttpBody { + /** + * Release this body and any downstream representations. + */ + void release(); + + /** + * Get the next representation this body was transformed into, if any. + * + * @return The next representation, or {@code null} if this body has not been transformed + */ + @Nullable + HttpBody next(); +} diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/body/ImmediateByteBody.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/body/ImmediateByteBody.java new file mode 100644 index 00000000000..3417038ac65 --- /dev/null +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/body/ImmediateByteBody.java @@ -0,0 +1,95 @@ +/* + * Copyright 2017-2023 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.http.server.netty.body; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.execution.ExecutionFlow; +import io.micronaut.http.server.netty.HttpContentProcessor; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.handler.codec.http.DefaultHttpContent; +import org.jetbrains.annotations.NotNull; + +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.List; + +/** + * Fully buffered {@link ByteBody}, all operations are eager. + * + * @since 4.0.0 + * @author Jonas Konrad + */ +@Internal +public final class ImmediateByteBody extends ManagedBody implements ByteBody { + private final boolean empty; + + ImmediateByteBody(ByteBuf buf) { + super(buf); + this.empty = !buf.isReadable(); + } + + @Override + void release(ByteBuf value) { + value.release(); + } + + @Override + public MultiObjectBody processMulti(HttpContentProcessor processor) throws Throwable { + ByteBuf data = prepareClaim(); + Object item = processor.processSingle(data); + if (item != null) { + return next(new ImmediateSingleObjectBody(item)); + } + + return next(processMultiImpl(processor, data)); + } + + @NotNull + private ImmediateMultiObjectBody processMultiImpl(HttpContentProcessor processor, ByteBuf data) throws Throwable { + List out = new ArrayList<>(1); + if (data.isReadable()) { + processor.add(new DefaultHttpContent(data), out); + } else { + data.release(); + } + processor.complete(out); + return new ImmediateMultiObjectBody(out); + } + + /** + * Process this body and then transform it into a single object using + * {@link ImmediateMultiObjectBody#single}. + * + * @param processor The processor + * @param defaultCharset The default charset (see {@link ImmediateMultiObjectBody#single}) + * @param alloc The buffer allocator (see {@link ImmediateMultiObjectBody#single}) + * @return The processed object + * @throws Throwable Any failure + */ + public ImmediateSingleObjectBody processSingle(HttpContentProcessor processor, Charset defaultCharset, ByteBufAllocator alloc) throws Throwable { + return next(processMultiImpl(processor, prepareClaim()).single(defaultCharset, alloc)); + } + + @Override + public ExecutionFlow buffer(ByteBufAllocator alloc) { + return ExecutionFlow.just(this); + } + + public boolean empty() { + return empty; + } +} diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/body/ImmediateMultiObjectBody.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/body/ImmediateMultiObjectBody.java new file mode 100644 index 00000000000..267aa7bcaa5 --- /dev/null +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/body/ImmediateMultiObjectBody.java @@ -0,0 +1,146 @@ +/* + * Copyright 2017-2023 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.http.server.netty.body; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.util.CollectionUtils; +import io.micronaut.http.server.exceptions.InternalServerException; +import io.micronaut.http.server.netty.FormRouteCompleter; +import io.micronaut.http.server.netty.MicronautHttpData; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.buffer.ByteBufHolder; +import io.netty.buffer.ByteBufInputStream; +import io.netty.buffer.CompositeByteBuf; +import io.netty.buffer.Unpooled; +import io.netty.util.ReferenceCountUtil; +import io.netty.util.ReferenceCounted; +import org.reactivestreams.Publisher; +import reactor.core.publisher.Flux; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.Collection; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Function; + +/** + * Immediate {@link MultiObjectBody}, all operations are eager. + * + * @since 4.0.0 + * @author Jonas Konrad + */ +@Internal +public final class ImmediateMultiObjectBody extends ManagedBody> implements MultiObjectBody { + ImmediateMultiObjectBody(List objects) { + super(objects); + } + + @Override + void release(List value) { + value.forEach(ReferenceCountUtil::release); + } + + public ImmediateSingleObjectBody single(Charset defaultCharset, ByteBufAllocator alloc) { + List objects = prepareClaim(); + if (objects.isEmpty()) { + return next(new ImmediateSingleObjectBody(null)); + } + boolean allFormData = true; + for (Object object : objects) { + if (!(object instanceof MicronautHttpData)) { + allFormData = false; + break; + } + } + if (allFormData) { + //noinspection unchecked + return next(new ImmediateSingleObjectBody(toMap(defaultCharset, (List>) objects))); + } + if (objects.size() == 1) { + Object o = objects.get(0); + return next(new ImmediateSingleObjectBody(o instanceof ByteBufHolder bbh ? bbh.content() : o)); + } + return next(new ImmediateSingleObjectBody(coerceToComposite(objects, alloc))); + } + + private static CompositeByteBuf coerceToComposite(List objects, ByteBufAllocator alloc) { + CompositeByteBuf composite = alloc.compositeBuffer(); + for (Object object : objects) { + composite.addComponent(true, ((ByteBufHolder) object).content()); + } + return composite; + } + + public static Map toMap(Charset charset, Collection> dataList) { + Map singleMap = CollectionUtils.newLinkedHashMap(dataList.size()); + Map> multiMap = new LinkedHashMap<>(); + for (MicronautHttpData data : dataList) { + String key = data.getName(); + String newValue; + try { + newValue = data.getString(charset); + } catch (IOException e) { + throw new InternalServerException("Error retrieving or decoding the value for: " + data.getName()); + } + List multi = multiMap.get(key); + if (multi != null) { + multi.add(newValue); + } else { + Object existing = singleMap.put(key, newValue); + if (existing != null) { + List combined = new ArrayList<>(2); + combined.add(existing); + combined.add(newValue); + singleMap.put(key, combined); + multiMap.put(key, combined); + } + } + } + return singleMap; + } + + @Override + public InputStream coerceToInputStream(ByteBufAllocator alloc) { + List objects = claim(); + ByteBuf buf = switch (objects.size()) { + case 0 -> Unpooled.EMPTY_BUFFER; + case 1 -> ((ByteBufHolder) objects.get(0)).content(); + default -> coerceToComposite(objects, alloc); + }; + return new ByteBufInputStream(buf, true); + } + + @Override + public Publisher asPublisher() { + return Flux.fromIterable(claim()).doOnDiscard(ReferenceCounted.class, ReferenceCounted::release); + } + + @Override + public MultiObjectBody mapNotNull(Function transform) { + return next(new ImmediateMultiObjectBody(prepareClaim().stream().map(transform).toList())); + } + + @Override + public void handleForm(FormRouteCompleter formRouteCompleter) { + Flux.fromIterable(prepareClaim()).subscribe(formRouteCompleter); + next(formRouteCompleter); + } +} diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/body/ImmediateSingleObjectBody.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/body/ImmediateSingleObjectBody.java new file mode 100644 index 00000000000..bc14d64f665 --- /dev/null +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/body/ImmediateSingleObjectBody.java @@ -0,0 +1,92 @@ +/* + * Copyright 2017-2023 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.http.server.netty.body; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.http.server.netty.FormRouteCompleter; +import io.micronaut.http.server.netty.NettyHttpRequest; +import io.netty.buffer.ByteBufAllocator; +import io.netty.buffer.ByteBufHolder; +import io.netty.buffer.ByteBufInputStream; +import io.netty.util.ReferenceCountUtil; +import io.netty.util.ReferenceCounted; +import org.reactivestreams.Publisher; +import reactor.core.publisher.Flux; + +import java.io.InputStream; +import java.util.List; +import java.util.function.Function; + +/** + * {@link HttpBody} that contains a single object. This is used to implement + * {@link NettyHttpRequest#getBody()} and {@link java.util.concurrent.CompletableFuture} binding. + * + * @since 4.0.0 + * @author Jonas Konrad + */ +@Internal +public final class ImmediateSingleObjectBody extends ManagedBody implements HttpBody, MultiObjectBody { + ImmediateSingleObjectBody(Object value) { + super(value); + } + + @Override + void release(Object value) { + ReferenceCountUtil.release(value); + } + + /** + * Get the value and transfer ownership to the caller. The caller must release the value after + * it's done. Can only be called once. + * + * @return The claimed value + */ + public Object claimForExternal() { + return claim(); + } + + /** + * Get the value without transferring ownership. The returned value may become invalid when + * other code calls {@link #claimForExternal()} or when the netty request is destroyed. + * + * @return The unclaimed value + */ + public Object valueUnclaimed() { + return value(); + } + + @Override + public InputStream coerceToInputStream(ByteBufAllocator alloc) { + return new ByteBufInputStream(((ByteBufHolder) claim()).content(), true); + } + + @Override + public Publisher asPublisher() { + return Flux.just(claim()).doOnDiscard(ReferenceCounted.class, ReferenceCounted::release); + } + + @Override + public MultiObjectBody mapNotNull(Function transform) { + Object result = transform.apply(prepareClaim()); + return next(result == null ? new ImmediateMultiObjectBody(List.of()) : new ImmediateSingleObjectBody(result)); + } + + @Override + public void handleForm(FormRouteCompleter formRouteCompleter) { + Flux.just(prepareClaim()).subscribe(formRouteCompleter); + next(formRouteCompleter); + } +} diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/body/ManagedBody.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/body/ManagedBody.java new file mode 100644 index 00000000000..072d7360f10 --- /dev/null +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/body/ManagedBody.java @@ -0,0 +1,118 @@ +/* + * Copyright 2017-2023 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.http.server.netty.body; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.Nullable; + +/** + * Standard implementation of {@link HttpBody} that contains a single value that can be claimed + * once. + * + * @param The value type + * @since 4.0.0 + * @author Jonas Konrad + */ +@Internal +abstract class ManagedBody implements HttpBody { + private final T value; + private boolean claimed; + private HttpBody next; + + ManagedBody(T value) { + this.value = value; + } + + /** + * Get the value without claiming it. + * + * @return The value + * @throws IllegalStateException if the value has already been claimed + */ + final T value() { + if (claimed) { + throw new IllegalStateException("Already claimed"); + } + return value; + } + + /** + * Get and claim the value. + * + * @return The value + * @throws IllegalStateException if the value has already been claimed + */ + final T claim() { + if (claimed) { + throw new IllegalStateException("Already claimed"); + } + claimed = true; + return value; + } + + /** + * Prepare to claim this value. The actual claim is done by {@link #next(HttpBody)}. This makes + * sure that if there is an exception between prepareClaim and {@link #next(HttpBody)}, this + * {@link HttpBody} actually retains ownership and releases the value on {@link #release()}. + * + * @return The value that will be claimed + */ + final T prepareClaim() { + if (claimed) { + throw new IllegalStateException("Already claimed"); + } + return value; + } + + /** + * Claim this value and set the next {@link HttpBody}. This operation must be preceded by a + * call to {@link #prepareClaim()}. + * + * @param next The next body that takes responsibility of our data + * @return The {@code next} parameter, for easy chaining + * @param The body type + */ + final B next(B next) { + if (claimed) { + throw new AssertionError("Should have called prepareClaim"); + } + this.next = next; + claim(); + return next; + } + + @Override + public final void release() { + if (!claimed) { + release(value); + } else if (next != null) { + next.release(); + } + } + + /** + * Release the given value. Only called by {@link #release()} if the value is still unclaimed. + * + * @param value The value to release + */ + abstract void release(T value); + + @Nullable + @Override + public HttpBody next() { + return next; + } +} diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/body/MultiObjectBody.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/body/MultiObjectBody.java new file mode 100644 index 00000000000..c878bf8693d --- /dev/null +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/body/MultiObjectBody.java @@ -0,0 +1,78 @@ +/* + * Copyright 2017-2023 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.http.server.netty.body; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.http.server.netty.FormRouteCompleter; +import io.netty.buffer.ByteBufAllocator; +import org.reactivestreams.Publisher; + +import java.io.InputStream; +import java.util.function.Function; + +/** + * A body consisting of multiple objects of arbitrary type. Basically a + * {@link Publisher}{@code }. This class is so generic for compatibility reasons, it's the + * result of processing a {@link ByteBody} using a + * {@link io.micronaut.http.server.netty.HttpContentProcessor}. + * + * @since 4.0.0 + * @author Jonas Konrad + */ +@Internal +public sealed interface MultiObjectBody extends HttpBody permits ImmediateMultiObjectBody, ImmediateSingleObjectBody, StreamingMultiObjectBody { + /** + * Coerce this value to an {@link InputStream}. This implements + * {@link io.micronaut.http.server.netty.binders.InputStreamBodyBinder}. Requires the objects + * of this body to be {@link io.netty.buffer.ByteBuf}s or + * {@link io.netty.buffer.ByteBufHolder}s.
+ * Ownership is transferred to the stream, it must be closed to release all buffers. + * + * @param alloc The buffer allocator to use + * @return The stream that reads the data in this body + */ + InputStream coerceToInputStream(ByteBufAllocator alloc); + + /** + * Get this value as a publisher. The publisher must be subscribed to exactly once. All objects + * forwarded to the subscriber become its responsibility and must be released by the + * subscriber. + * + * @return The publisher + */ + Publisher asPublisher(); + + /** + * Apply a mapping function to all objects in this body. {@code null} values in the output are + * skipped. + * + * @param transform The mapping function + * @return A new body with the mapped values + */ + MultiObjectBody mapNotNull(Function transform); + + /** + * Special handling for form data. This method basically acts like + * {@code asPublisher().subscribe(formRouteCompleter)}. However, {@link FormRouteCompleter} + * needs to release the form data fields when the request is destroyed. To do this, it + * implements {@link HttpBody#release()}. By calling this method, the + * {@link FormRouteCompleter} is registered as the {@link #next() next body} and will be + * released. + * + * @param formRouteCompleter The form route completer that should take over processing + */ + void handleForm(FormRouteCompleter formRouteCompleter); +} diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/body/StreamingByteBody.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/body/StreamingByteBody.java new file mode 100644 index 00000000000..235a0a805d9 --- /dev/null +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/body/StreamingByteBody.java @@ -0,0 +1,171 @@ +/* + * Copyright 2017-2023 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.http.server.netty.body; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.Nullable; +import io.micronaut.core.execution.DelayedExecutionFlow; +import io.micronaut.core.execution.ExecutionFlow; +import io.micronaut.http.server.netty.HttpContentProcessor; +import io.micronaut.http.server.netty.HttpContentProcessorAsReactiveProcessor; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.buffer.CompositeByteBuf; +import io.netty.handler.codec.http.HttpContent; +import org.reactivestreams.Publisher; +import org.reactivestreams.Subscriber; +import org.reactivestreams.Subscription; + +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; + +/** + * {@link ByteBody} implementation that wraps a + * {@link io.micronaut.http.netty.stream.StreamedHttpRequest}. + * + * @since 4.0.0 + * @author Jonas Konrad + */ +@Internal +public final class StreamingByteBody extends ManagedBody> implements ByteBody { + StreamingByteBody(Publisher publisher) { + super(publisher); + } + + @Override + public MultiObjectBody processMulti(HttpContentProcessor processor) { + return next(new StreamingMultiObjectBody(HttpContentProcessorAsReactiveProcessor.asPublisher(processor, prepareClaim()))); + } + + @Override + public ExecutionFlow buffer(ByteBufAllocator alloc) { + IntermediateBuffering intermediateBuffering = new IntermediateBuffering(alloc); + prepareClaim().subscribe(intermediateBuffering); + next(intermediateBuffering); + return intermediateBuffering.completion; + } + + @Override + void release(Publisher value) { + // not subscribed, don't need to do anything + } + + /** + * Intermediate {@link HttpBody} after {@link #buffer(ByteBufAllocator)} has been called but + * before all data is in. + */ + private static final class IntermediateBuffering implements Subscriber, HttpBody { + private final DelayedExecutionFlow completion = DelayedExecutionFlow.create(); + private final Lock lock = new ReentrantLock(); + private final ByteBufAllocator alloc; + private Subscription subscription; + private boolean discarded = false; + private CompositeByteBuf composite; + private ByteBuf single; + private ImmediateByteBody next; + + private IntermediateBuffering(ByteBufAllocator alloc) { + this.alloc = alloc; + } + + @Override + public void onSubscribe(Subscription s) { + s.request(Long.MAX_VALUE); + this.subscription = s; + } + + @Override + public void onNext(HttpContent httpContent) { + lock.lock(); + try { + if (discarded) { + httpContent.release(); + return; + } + if (composite != null) { + composite.addComponent(true, httpContent.content()); + } else if (single == null) { + single = httpContent.content(); + } else { + composite = alloc.compositeBuffer(); + composite.addComponent(true, single); + composite.addComponent(true, httpContent.content()); + single = null; + } + } finally { + lock.unlock(); + } + } + + @Override + public void onError(Throwable t) { + discard(); + try { + completion.completeExceptionally(t); + } catch (IllegalStateException ignored) { + // already completed + } + } + + @Override + public void onComplete() { + lock.lock(); + try { + discarded = true; + next = new ImmediateByteBody(composite == null ? single : composite); + single = null; + composite = null; + } finally { + lock.unlock(); + } + completion.complete(next); + } + + private void discard() { + lock.lock(); + try { + discarded = true; + if (composite != null) { + composite.release(); + composite = null; + } + if (single != null) { + single.release(); + single = null; + } + } finally { + lock.unlock(); + } + if (next != null) { + next.release(); + } + if (subscription != null) { + subscription.cancel(); + } + } + + @Override + public void release() { + discard(); + } + + @Nullable + @Override + public HttpBody next() { + return next; + } + } +} diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/body/StreamingMultiObjectBody.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/body/StreamingMultiObjectBody.java new file mode 100644 index 00000000000..c3820754fa9 --- /dev/null +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/body/StreamingMultiObjectBody.java @@ -0,0 +1,278 @@ +/* + * Copyright 2017-2023 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.http.server.netty.body; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.NonNull; +import io.micronaut.core.annotation.Nullable; +import io.micronaut.http.server.netty.FormRouteCompleter; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.buffer.ByteBufHolder; +import io.netty.util.ReferenceCountUtil; +import org.reactivestreams.Publisher; +import org.reactivestreams.Subscriber; +import org.reactivestreams.Subscription; +import reactor.core.publisher.Flux; + +import java.io.Closeable; +import java.io.IOException; +import java.io.InputStream; +import java.io.InterruptedIOException; +import java.util.concurrent.locks.Condition; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; +import java.util.function.Function; + +/** + * {@link MultiObjectBody} derived from a {@link StreamingByteBody}. Operations are lazy. + * + * @since 4.0.0 + * @author Jonas Konrad + */ +@Internal +public final class StreamingMultiObjectBody extends ManagedBody> implements MultiObjectBody { + StreamingMultiObjectBody(Publisher publisher) { + super(publisher); + } + + @Override + void release(Publisher value) { + // not subscribed, don't need to do anything + } + + @Override + public InputStream coerceToInputStream(ByteBufAllocator alloc) { + PublisherAsBlocking publisherAsBlocking = new PublisherAsBlocking(); + claim().subscribe(publisherAsBlocking); + return new PublisherAsStream(publisherAsBlocking); + } + + @Override + public Publisher asPublisher() { + return claim(); + } + + @Override + public MultiObjectBody mapNotNull(Function transform) { + return next(new StreamingMultiObjectBody(Flux.from(prepareClaim()).mapNotNull(transform))); + } + + @Override + public void handleForm(FormRouteCompleter formRouteCompleter) { + prepareClaim().subscribe(formRouteCompleter); + next(formRouteCompleter); + } + + /** + * A subscriber that allows blocking reads from a publisher. Handles resource cleanup properly. + */ + private static final class PublisherAsBlocking implements Subscriber, Closeable { + private final Lock lock = new ReentrantLock(); + private final Condition newDataCondition = lock.newCondition(); + /** + * Set when {@link #take()} is called before {@link #onSubscribe}. {@link #onSubscribe} will + * immediately request some input. + */ + private boolean pendingDemand; + /** + * Pending object, this field is used to transfer from {@link #onNext} to {@link #take}. + */ + private Object swap; + /** + * The upstream subscription. + */ + private Subscription subscription; + /** + * Set by {@link #onComplete} and {@link #onError}. + */ + private boolean done; + /** + * Set by {@link #close}. Further objects will be discarded. + */ + private boolean closed; + /** + * Failure from {@link #onError}. + */ + private Throwable failure; + + @Override + public void onSubscribe(Subscription s) { + boolean pendingDemand; + lock.lock(); + try { + this.subscription = s; + pendingDemand = this.pendingDemand; + } finally { + lock.unlock(); + } + if (pendingDemand) { + s.request(1); + } + } + + @Override + public void onNext(Object o) { + lock.lock(); + try { + if (closed) { + ReferenceCountUtil.release(o); + return; + } + swap = o; + newDataCondition.signalAll(); + } finally { + lock.unlock(); + } + } + + @Override + public void onError(Throwable t) { + lock.lock(); + try { + if (swap != null) { + ReferenceCountUtil.release(swap); + swap = null; + } + failure = t; + done = true; + newDataCondition.signalAll(); + } finally { + lock.unlock(); + } + } + + @Override + public void onComplete() { + lock.lock(); + try { + done = true; + newDataCondition.signalAll(); + } finally { + lock.unlock(); + } + } + + /** + * Get the next object. + * + * @return The next object, or {@code null} if the stream is done + */ + @Nullable + public Object take() throws InterruptedException { + boolean demanded = false; + while (true) { + Subscription subscription; + lock.lock(); + try { + Object swap = this.swap; + if (swap != null) { + this.swap = null; + return swap; + } + if (done) { + return null; + } + if (demanded) { + newDataCondition.await(); + } + subscription = this.subscription; + if (subscription == null) { + pendingDemand = true; + } + } finally { + lock.unlock(); + } + if (!demanded) { + demanded = true; + if (subscription != null) { + subscription.request(1); + } + } + } + } + + @Override + public void close() { + lock.lock(); + try { + closed = true; + if (swap != null) { + ReferenceCountUtil.release(swap); + swap = null; + } + } finally { + lock.unlock(); + } + } + } + + private static final class PublisherAsStream extends InputStream { + private final PublisherAsBlocking publisherAsBlocking; + private ByteBuf buffer; + + private PublisherAsStream(PublisherAsBlocking publisherAsBlocking) { + this.publisherAsBlocking = publisherAsBlocking; + } + + @Override + public int read() throws IOException { + byte[] arr = new byte[1]; + int n = read(arr); + return n == -1 ? -1 : arr[0] & 0xff; + } + + @Override + public int read(@NonNull byte[] b, int off, int len) throws IOException { + while (buffer == null) { + try { + Object o = publisherAsBlocking.take(); + if (o == null) { + if (publisherAsBlocking.failure == null) { + return -1; + } else { + throw new IOException(publisherAsBlocking.failure); + } + } + ByteBuf buf = o instanceof ByteBufHolder holder ? holder.content() : (ByteBuf) o; + if (!buf.isReadable()) { + continue; + } + buffer = buf; + } catch (InterruptedException e) { + throw new InterruptedIOException(); + } + } + + int toRead = Math.min(len, buffer.readableBytes()); + buffer.readBytes(b, off, toRead); + if (!buffer.isReadable()) { + buffer.release(); + buffer = null; + } + return toRead; + } + + @Override + public void close() throws IOException { + if (buffer != null) { + buffer.release(); + buffer = null; + } + publisherAsBlocking.close(); + } + } +} diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/configuration/NettyHttpServerConfiguration.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/configuration/NettyHttpServerConfiguration.java index 152f8549ba7..add319d4941 100644 --- a/http-server-netty/src/main/java/io/micronaut/http/server/netty/configuration/NettyHttpServerConfiguration.java +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/configuration/NettyHttpServerConfiguration.java @@ -167,6 +167,8 @@ public class NettyHttpServerConfiguration extends HttpServerConfiguration { private final List pipelineCustomizers; + private HttpServerType serverType = HttpServerType.STREAMED; + private Map childOptions = Collections.emptyMap(); private Map options = Collections.emptyMap(); private Worker worker; @@ -176,6 +178,8 @@ public class NettyHttpServerConfiguration extends HttpServerConfiguration { private int maxHeaderSize = DEFAULT_MAXHEADERSIZE; private int maxChunkSize = DEFAULT_MAXCHUNKSIZE; private int maxH2cUpgradeRequestSize = DEFAULT_MAXCHUNKSIZE; // same default as maxChunkSize, we don't want to buffer super long bodies + + private boolean closeOnExpectationFailed = false; private boolean chunkedSupported = DEFAULT_CHUNKSUPPORTED; private boolean validateHeaders = DEFAULT_VALIDATEHEADERS; private int initialBufferSize = DEFAULT_INITIALBUFFERSIZE; @@ -219,6 +223,50 @@ public NettyHttpServerConfiguration( this.pipelineCustomizers = pipelineCustomizers; } + /** + * @return Sets the server type. + * @see HttpServerType + */ + @NonNull + public HttpServerType getServerType() { + return serverType; + } + + /** + * If a 100-continue response is detected but the content length is too large then true means close the connection. otherwise the connection will remain open and data will be consumed and discarded until the next request is received. + * + *

only relevant when {@link HttpServerType#FULL_CONTENT} is set

+ * @return True if the connection should be closed + * @see #setServerType(HttpServerType) + * @see io.netty.handler.codec.http.HttpObjectAggregator + */ + public boolean isCloseOnExpectationFailed() { + return closeOnExpectationFailed; + } + + /** + * If a 100-continue response is detected but the content length is too large then true means close the connection. otherwise the connection will remain open and data will be consumed and discarded until the next request is received. + * + *

only relevant when {@link HttpServerType#FULL_CONTENT} is set

+ * @param closeOnExpectationFailed True if the connection should be closed + * @see #setServerType(HttpServerType) + * @see io.netty.handler.codec.http.HttpObjectAggregator + */ + public void setCloseOnExpectationFailed(boolean closeOnExpectationFailed) { + this.closeOnExpectationFailed = closeOnExpectationFailed; + } + + /** + * Set the server type. + * + * @param serverType The server type + */ + public void setServerType(@Nullable HttpServerType serverType) { + if (serverType != null) { + this.serverType = serverType; + } + } + /** * Returns the AccessLogger configuration. * @return The AccessLogger configuration. @@ -1416,4 +1464,18 @@ public enum Family { QUIC, } } + + /** + * Sets the manner in which the HTTP server is configured to receive requests. + */ + public enum HttpServerType { + /** + * Requests are streamed on demand with {@link io.netty.handler.flow.FlowControlHandler} used to control back pressure. + */ + STREAMED, + /** + * Execute controllers only once the full content of the request has been received. + */ + FULL_CONTENT + } } diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/converters/NettyConvertersSpi.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/converters/NettyConvertersSpi.java index d4e843e04cd..2184d37fcf7 100644 --- a/http-server-netty/src/main/java/io/micronaut/http/server/netty/converters/NettyConvertersSpi.java +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/converters/NettyConvertersSpi.java @@ -160,7 +160,7 @@ private TypeConverter fileUploadToCompletedFile private TypeConverter attributeToCompletedPartConverter() { return (object, targetType, context) -> { try { - if (!object.isCompleted()) { + if (!object.isCompleted() || !targetType.isAssignableFrom(NettyCompletedAttribute.class)) { return Optional.empty(); } diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/decoders/HttpRequestDecoder.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/decoders/HttpRequestDecoder.java index c86816d4ce5..63c95fbeea1 100644 --- a/http-server-netty/src/main/java/io/micronaut/http/server/netty/decoders/HttpRequestDecoder.java +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/decoders/HttpRequestDecoder.java @@ -25,11 +25,13 @@ import io.micronaut.http.server.netty.NettyHttpRequest; import io.micronaut.http.server.netty.NettyHttpServer; import io.micronaut.runtime.server.EmbeddedServer; +import io.netty.buffer.Unpooled; import io.netty.channel.ChannelHandler; import io.netty.channel.ChannelHandlerContext; import io.netty.handler.codec.MessageToMessageDecoder; -import io.netty.handler.codec.http.DefaultHttpRequest; +import io.netty.handler.codec.http.DefaultFullHttpRequest; import io.netty.handler.codec.http.HttpRequest; +import io.netty.util.ReferenceCountUtil; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -64,7 +66,11 @@ public class HttpRequestDecoder extends MessageToMessageDecoder imp * @param configuration The Http server configuration * @param httpRequestReceivedEventPublisher The publisher of {@link HttpRequestReceivedEvent} */ - public HttpRequestDecoder(EmbeddedServer embeddedServer, ConversionService conversionService, HttpServerConfiguration configuration, ApplicationEventPublisher httpRequestReceivedEventPublisher) { + public HttpRequestDecoder( + EmbeddedServer embeddedServer, + ConversionService conversionService, + HttpServerConfiguration configuration, + ApplicationEventPublisher httpRequestReceivedEventPublisher) { this.embeddedServer = embeddedServer; this.conversionService = conversionService; this.configuration = configuration; @@ -96,19 +102,20 @@ protected void decode(ChannelHandlerContext ctx, HttpRequest msg, List o } } out.add(request); + ReferenceCountUtil.retain(msg); // retain the body if it's a FullHttpRequest } catch (IllegalArgumentException e) { // this configured the request in the channel as an attribute new NettyHttpRequest<>( - new DefaultHttpRequest(msg.protocolVersion(), msg.method(), "/"), + new DefaultFullHttpRequest(msg.protocolVersion(), msg.method(), "/", Unpooled.EMPTY_BUFFER), ctx, conversionService, configuration ); final Throwable cause = e.getCause(); ctx.fireExceptionCaught(cause != null ? cause : e); - if (msg instanceof StreamedHttpRequest) { + if (msg instanceof StreamedHttpRequest streamedHttpRequest) { // discard any data that may come in - ((StreamedHttpRequest) msg).closeIfNoSubscriber(); + streamedHttpRequest.closeIfNoSubscriber(); } } } diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/jackson/JsonContentProcessor.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/jackson/JsonContentProcessor.java index 51a0fe0c217..d594c257463 100644 --- a/http-server-netty/src/main/java/io/micronaut/http/server/netty/jackson/JsonContentProcessor.java +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/jackson/JsonContentProcessor.java @@ -31,6 +31,7 @@ import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufHolder; import io.netty.buffer.CompositeByteBuf; +import io.netty.handler.codec.http.DefaultHttpContent; import java.io.IOException; import java.util.Collection; @@ -44,10 +45,11 @@ * @since 1.0 */ @Internal -public class JsonContentProcessor extends AbstractHttpContentProcessor { +public final class JsonContentProcessor extends AbstractHttpContentProcessor { private final JsonMapper jsonMapper; private final JsonCounter counter = new JsonCounter(); + private final boolean tokenize; private ByteBuf singleBuffer; private CompositeByteBuf compositeBuffer; @@ -62,9 +64,8 @@ public JsonContentProcessor( JsonMapper jsonMapper) { super(nettyHttpRequest, configuration); this.jsonMapper = jsonMapper; - - if (hasContentType(MediaType.APPLICATION_JSON_TYPE)) { - + this.tokenize = !hasContentType(MediaType.APPLICATION_JSON_TYPE); + if (!tokenize) { // if the content type is application/json, we can only have one root-level value counter.noTokenization(); } @@ -88,6 +89,31 @@ public HttpContentProcessor resultType(Argument type) { return this; } + @Override + public Object processSingle(ByteBuf data) throws Throwable { + // if data is empty, we return no json nodes, so can't use this method + if (tokenize || !data.isReadable()) { + return null; + } + + if (data.readableBytes() > requestMaxSize) { + fireExceedsLength(data.readableBytes(), requestMaxSize, new DefaultHttpContent(data)); + } + int start = data.readerIndex(); + counter.feed(data); + data.readerIndex(start); + ByteBuffer wrapped = NettyByteBufferFactory.DEFAULT.wrap(data); + if (((NettyHttpServerConfiguration) configuration).isEagerParsing()) { + try { + return jsonMapper.readValue(wrapped, Argument.of(JsonNode.class)); + } finally { + data.release(); + } + } else { + return new LazyJsonNode(wrapped); + } + } + private boolean hasContentType(MediaType expected) { Optional actual = nettyHttpRequest.getContentType(); return actual.isPresent() && actual.get().equals(expected); diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/multipart/MultipartBodyArgumentBinder.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/multipart/MultipartBodyArgumentBinder.java index bbf2446af27..de4e1ec7ad0 100644 --- a/http-server-netty/src/main/java/io/micronaut/http/server/netty/multipart/MultipartBodyArgumentBinder.java +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/multipart/MultipartBodyArgumentBinder.java @@ -18,31 +18,33 @@ import io.micronaut.context.BeanLocator; import io.micronaut.context.BeanProvider; import io.micronaut.core.annotation.Internal; -import io.micronaut.core.async.subscriber.CompletionAwareSubscriber; import io.micronaut.core.convert.ArgumentConversionContext; import io.micronaut.core.type.Argument; import io.micronaut.http.HttpRequest; import io.micronaut.http.MediaType; import io.micronaut.http.bind.binders.NonBlockingBodyArgumentBinder; -import io.micronaut.http.netty.stream.StreamedHttpRequest; +import io.micronaut.http.multipart.CompletedPart; import io.micronaut.http.server.HttpServerConfiguration; import io.micronaut.http.server.multipart.MultipartBody; import io.micronaut.http.server.netty.DefaultHttpContentProcessor; import io.micronaut.http.server.netty.HttpContentProcessor; -import io.micronaut.http.server.netty.HttpContentProcessorAsReactiveProcessor; import io.micronaut.http.server.netty.HttpContentSubscriberFactory; import io.micronaut.http.server.netty.NettyHttpRequest; import io.micronaut.http.server.netty.NettyHttpServer; +import io.micronaut.http.server.netty.body.MultiObjectBody; import io.micronaut.web.router.qualifier.ConsumesMediaTypeQualifier; import io.netty.handler.codec.http.multipart.Attribute; import io.netty.handler.codec.http.multipart.FileUpload; import io.netty.handler.codec.http.multipart.HttpData; -import org.reactivestreams.Subscription; +import io.netty.util.ReferenceCounted; +import org.reactivestreams.Publisher; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import reactor.core.publisher.Flux; +import java.util.HashSet; import java.util.Optional; -import java.util.concurrent.atomic.AtomicLong; +import java.util.Set; /** * A {@link io.micronaut.http.annotation.Body} argument binder for a {@link MultipartBody} argument. @@ -61,7 +63,7 @@ public class MultipartBodyArgumentBinder implements NonBlockingBodyArgumentBinde /** * Default constructor. * - * @param beanLocator The bean locator + * @param beanLocator The bean locator * @param httpServerConfiguration The server configuration */ public MultipartBodyArgumentBinder(BeanLocator beanLocator, BeanProvider httpServerConfiguration) { @@ -76,89 +78,35 @@ public Argument argumentType() { @Override public BindingResult bind(ArgumentConversionContext context, HttpRequest source) { - if (source instanceof NettyHttpRequest) { - NettyHttpRequest nettyHttpRequest = (NettyHttpRequest) source; - io.netty.handler.codec.http.HttpRequest nativeRequest = nettyHttpRequest.getNativeRequest(); - if (nativeRequest instanceof StreamedHttpRequest) { - HttpContentProcessor processor = beanLocator.findBean(HttpContentSubscriberFactory.class, - new ConsumesMediaTypeQualifier<>(MediaType.MULTIPART_FORM_DATA_TYPE)) - .map(factory -> factory.build(nettyHttpRequest)) - .orElse(new DefaultHttpContentProcessor(nettyHttpRequest, httpServerConfiguration.get())); - - //noinspection unchecked - return () -> Optional.of(subscriber -> HttpContentProcessorAsReactiveProcessor.asPublisher(processor.resultType(context.getArgument()), nettyHttpRequest).subscribe(new CompletionAwareSubscriber<>() { - - Subscription s; - AtomicLong partsRequested = new AtomicLong(0); - - @Override - protected void doOnSubscribe(Subscription subscription) { - this.s = subscription; - subscriber.onSubscribe(new Subscription() { - - @Override - public void request(long n) { - if (partsRequested.getAndUpdate(prev -> prev + n) == 0) { - s.request(n); - } - } - - @Override - public void cancel() { - subscription.cancel(); - } - }); - } - - @Override - protected void doOnNext(HttpData message) { - if (LOG.isTraceEnabled()) { - LOG.trace("Server received streaming message for argument [{}]: {}", context.getArgument(), message); - } - // MicronautHttpData does not support .content() - if (message.length() == 0) { - return; - } - - if (message.isCompleted()) { - partsRequested.decrementAndGet(); - if (message instanceof FileUpload fu) { - subscriber.onNext(new NettyCompletedFileUpload(fu, false)); - } else if (message instanceof Attribute attr) { - subscriber.onNext(new NettyCompletedAttribute(attr, false)); - } - } - - message.release(); - - if (partsRequested.get() > 0) { - s.request(1); - } - } - - @Override - protected void doOnError(Throwable t) { - if (LOG.isTraceEnabled()) { - LOG.trace("Server received error for argument [" + context.getArgument() + "]: " + t.getMessage(), t); - } - try { - subscriber.onError(t); - } finally { - s.cancel(); - } - } - - @Override - protected void doOnComplete() { - if (LOG.isTraceEnabled()) { - LOG.trace("Done receiving messages for argument: {}", context.getArgument()); - } - subscriber.onComplete(); - } - - })); + if (source instanceof NettyHttpRequest nhr) { + HttpContentProcessor processor = beanLocator.findBean(HttpContentSubscriberFactory.class, + new ConsumesMediaTypeQualifier<>(MediaType.MULTIPART_FORM_DATA_TYPE)) + .map(factory -> factory.build(nhr)) + .orElse(new DefaultHttpContentProcessor(nhr, httpServerConfiguration.get())); + MultiObjectBody multiObjectBody; + try { + multiObjectBody = nhr.rootBody() + .processMulti(processor); + } catch (Throwable e) { + throw new RuntimeException(e); } + Set partial = new HashSet<>(); + //noinspection unchecked + Flux completed = Flux.from(((Publisher) multiObjectBody.asPublisher())).mapNotNull(message -> { + if (message.isCompleted() && message.length() != 0) { + partial.remove(message); + if (message instanceof FileUpload fu) { + return new NettyCompletedFileUpload(fu, true); + } else { + return new NettyCompletedAttribute((Attribute) message, true); + } + } else { + partial.add(message); + return null; + } + }).doOnTerminate(() -> partial.forEach(ReferenceCounted::release)); + return () -> Optional.of(completed::subscribe); } - return BindingResult.EMPTY; + return BindingResult.empty(); } } diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/multipart/NettyCompletedFileUpload.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/multipart/NettyCompletedFileUpload.java index 9ec64e9d756..9d2ebf20238 100644 --- a/http-server-netty/src/main/java/io/micronaut/http/server/netty/multipart/NettyCompletedFileUpload.java +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/multipart/NettyCompletedFileUpload.java @@ -66,7 +66,6 @@ public NettyCompletedFileUpload(FileUpload fileUpload, boolean controlRelease) { this.fileUpload = fileUpload; this.controlRelease = controlRelease; if (controlRelease) { - fileUpload.retain(); tracker = RESOURCE_LEAK_DETECTOR.get().track(this); } else { tracker = null; diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/multipart/NettyStreamingFileUpload.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/multipart/NettyStreamingFileUpload.java index 17cc04bd2cb..8bb9d38e005 100644 --- a/http-server-netty/src/main/java/io/micronaut/http/server/netty/multipart/NettyStreamingFileUpload.java +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/multipart/NettyStreamingFileUpload.java @@ -148,7 +148,7 @@ public void discard() { private Publisher transferTo(ThrowingSupplier outputStreamSupplier) { return Mono.create(emitter -> - subject.subscribeOn(Schedulers.fromExecutorService(ioExecutor)) + subject.publishOn(Schedulers.fromExecutorService(ioExecutor)) .subscribe(new Subscriber() { Subscription subscription; OutputStream outputStream; diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/websocket/NettyServerWebSocketHandler.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/websocket/NettyServerWebSocketHandler.java index 7a473380d44..1c8ea0a3fd8 100644 --- a/http-server-netty/src/main/java/io/micronaut/http/server/netty/websocket/NettyServerWebSocketHandler.java +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/websocket/NettyServerWebSocketHandler.java @@ -170,7 +170,6 @@ public class NettyServerWebSocketHandler extends AbstractNettyWebSocketHandler { this.nettyEmbeddedServices = nettyEmbeddedServices; this.coroutineHelper = coroutineHelper; request.setAttribute(HttpAttributes.ROUTE_MATCH, routeMatch); - request.setAttribute(HttpAttributes.ROUTE, routeMatch.getRoute()); Flux.from(callOpenMethod(ctx)).subscribe(v -> { }, t -> { forwardErrorToUser(ctx, e -> { diff --git a/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/MaxRequestSizeSpec.groovy b/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/MaxRequestSizeSpec.groovy index 79307ec02b7..3ffd672a006 100644 --- a/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/MaxRequestSizeSpec.groovy +++ b/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/MaxRequestSizeSpec.groovy @@ -12,6 +12,7 @@ import io.micronaut.http.client.HttpClient import io.micronaut.http.client.exceptions.HttpClientResponseException import io.micronaut.http.client.multipart.MultipartBody import io.micronaut.http.multipart.CompletedFileUpload +import io.micronaut.http.multipart.FileUpload import io.micronaut.runtime.server.EmbeddedServer import io.netty.bootstrap.Bootstrap import io.netty.buffer.Unpooled @@ -461,7 +462,10 @@ class MaxRequestSizeSpec extends Specification { @Post(uri = "/multipart-body", consumes = MediaType.MULTIPART_FORM_DATA) @SingleResult Publisher multipart(@Body io.micronaut.http.server.multipart.MultipartBody body) { - return Flux.from(body).collectList().map({ list -> "OK" }) + return Flux.from(body).map { + if (it instanceof FileUpload) it.discard() + return it + }.collectList().map({ list -> "OK" }) } } } diff --git a/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/OptionsRequestAttributesSpec.groovy b/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/OptionsRequestAttributesSpec.groovy index 09861d16c55..c7bf6a3b916 100644 --- a/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/OptionsRequestAttributesSpec.groovy +++ b/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/OptionsRequestAttributesSpec.groovy @@ -3,7 +3,6 @@ package io.micronaut.http.server.netty import io.micronaut.context.ApplicationContext import io.micronaut.context.annotation.Requires import io.micronaut.http.HttpAttributes -import io.micronaut.http.HttpMethod import io.micronaut.http.HttpRequest import io.micronaut.http.HttpStatus import io.micronaut.http.MutableHttpResponse @@ -20,7 +19,6 @@ import org.reactivestreams.Publisher import org.spockframework.util.Assert import spock.lang.Specification - class OptionsRequestAttributesSpec extends Specification { def 'test OPTIONS requests attributes'() { @@ -53,7 +51,6 @@ class OptionsRequestAttributesSpec extends Specification { @Override Publisher> doFilter(HttpRequest request, ServerFilterChain chain) { - Assert.that(request.getAttributes().contains(HttpAttributes.ROUTE.toString())) Assert.that(request.getAttributes().contains(HttpAttributes.ROUTE_MATCH.toString())) Assert.that(request.getAttributes().contains(HttpAttributes.ROUTE_INFO.toString())) Assert.that(request.getAttributes().contains(HttpAttributes.URI_TEMPLATE.toString())) diff --git a/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/binding/ByteBufferSpec.groovy b/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/binding/ByteBufferSpec.groovy index 5063ad109fc..e567aba85f1 100644 --- a/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/binding/ByteBufferSpec.groovy +++ b/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/binding/ByteBufferSpec.groovy @@ -2,8 +2,10 @@ package io.micronaut.http.server.netty.binding import io.micronaut.context.annotation.Property import io.micronaut.context.annotation.Requires +import io.micronaut.core.async.annotation.SingleResult import io.micronaut.core.io.buffer.ByteBuffer import io.micronaut.core.io.buffer.ByteBufferFactory +import io.micronaut.core.io.buffer.ReferenceCounted import io.micronaut.http.HttpRequest import io.micronaut.http.HttpResponse import io.micronaut.http.MediaType @@ -22,7 +24,6 @@ import reactor.core.publisher.Flux import reactor.core.publisher.Mono import spock.lang.AutoCleanup import spock.lang.Specification -import io.micronaut.core.async.annotation.SingleResult import java.nio.charset.StandardCharsets import java.util.concurrent.CompletableFuture @@ -98,12 +99,20 @@ class ByteBufferSpec extends Specification { @Post(uri = "/buffer-test", processes = MediaType.TEXT_PLAIN) Publisher buffer(@Body Publisher body) { - return Flux.from(body).map({ buffer -> buffer.toString(StandardCharsets.UTF_8) }) + return Flux.from(body).map({ buffer -> + String s = buffer.toString(StandardCharsets.UTF_8) + if (buffer instanceof ReferenceCounted) buffer.release() + return s + }) } @Post(uri = "/buffer-completable", processes = MediaType.TEXT_PLAIN) CompletableFuture buffer(@Body CompletableFuture body) { - return body.thenApply({ buffer -> buffer.toString(StandardCharsets.UTF_8) }) + return body.thenApply({ buffer -> + String s = buffer.toString(StandardCharsets.UTF_8) + if (buffer instanceof ReferenceCounted) buffer.release() + return s + }) } @Get(uri = "/bytes", produces = MediaType.IMAGE_JPEG) diff --git a/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/binding/FullJsonBodyBindingSpec.groovy b/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/binding/FullJsonBodyBindingSpec.groovy new file mode 100644 index 00000000000..9f86b70556d --- /dev/null +++ b/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/binding/FullJsonBodyBindingSpec.groovy @@ -0,0 +1,11 @@ +package io.micronaut.http.server.netty.binding + +import io.micronaut.http.server.netty.configuration.NettyHttpServerConfiguration + +class FullJsonBodyBindingSpec extends JsonBodyBindingSpec { + + @Override + Map getConfiguration() { + return super.getConfiguration() + ["micronaut.server.netty.server-type": NettyHttpServerConfiguration.HttpServerType.FULL_CONTENT] + } +} diff --git a/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/cors/CorsFilterSpec.groovy b/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/cors/CorsFilterSpec.groovy index 40aac01b952..d610c401c1a 100644 --- a/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/cors/CorsFilterSpec.groovy +++ b/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/cors/CorsFilterSpec.groovy @@ -49,8 +49,6 @@ import static io.micronaut.http.HttpHeaders.ACCESS_CONTROL_ALLOW_METHODS import static io.micronaut.http.HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN import static io.micronaut.http.HttpHeaders.ACCESS_CONTROL_EXPOSE_HEADERS import static io.micronaut.http.HttpHeaders.ACCESS_CONTROL_MAX_AGE -import static io.micronaut.http.HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD -import static io.micronaut.http.HttpHeaders.ACCESS_CONTROL_REQUEST_HEADERS import static io.micronaut.http.HttpHeaders.VARY class CorsFilterSpec extends Specification { @@ -206,16 +204,14 @@ class CorsFilterSpec extends Specification { String origin = 'http://www.foo.com' HttpHeaders headers = Stub(HttpHeaders) { getOrigin() >> Optional.of(origin) - getFirst(ACCESS_CONTROL_REQUEST_METHOD, _) >> Optional.of(HttpMethod.GET) - get(ACCESS_CONTROL_REQUEST_HEADERS, _) >> Optional.of(['foo', 'bar']) - contains(ACCESS_CONTROL_REQUEST_METHOD) >> true + getFirst(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, _) >> Optional.of(HttpMethod.GET) + get(HttpHeaders.ACCESS_CONTROL_REQUEST_HEADERS, _) >> Optional.of(['foo', 'bar']) + contains(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD) >> true } HttpRequest request = createRequest(headers) request.getMethod() >> HttpMethod.OPTIONS request.getUri() >> new URIBuilder( '/example' ).build() - List> routes = embeddedServer.getApplicationContext().getBean(Router). - findAny(request.getUri().toString(), request) - .collect(Collectors.toList()) + List> routes = embeddedServer.getApplicationContext().getBean(Router).findAny(request.getUri().toString(), request).collect(Collectors.toList()) request.getAttribute(HttpAttributes.AVAILABLE_HTTP_METHODS, _) >> Optional.of(routes.stream().map(route->route.getHttpMethod()).collect(Collectors.toList())) @@ -247,16 +243,15 @@ class CorsFilterSpec extends Specification { HttpHeaders headers = Stub(HttpHeaders) { getOrigin() >> Optional.of(origin) - getFirst(ACCESS_CONTROL_REQUEST_METHOD, _) >> Optional.of(HttpMethod.GET) - get(ACCESS_CONTROL_REQUEST_HEADERS, _) >> Optional.of(['foo']) - contains(ACCESS_CONTROL_REQUEST_METHOD) >> true + getFirst(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, _) >> Optional.of(HttpMethod.GET) + get(HttpHeaders.ACCESS_CONTROL_REQUEST_HEADERS, _) >> Optional.of(['foo']) + contains(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD) >> true } HttpRequest request = createRequest(headers) request.getMethod() >> HttpMethod.OPTIONS request.getUri() >> new URIBuilder( '/example' ).build() List> routes = embeddedServer.getApplicationContext().getBean(Router). - findAny(request.getUri().toString(), request) - .collect(Collectors.toList()) + findAny(request) request.getAttribute(HttpAttributes.AVAILABLE_HTTP_METHODS, _) >> Optional.of(routes.stream().map(route->route.getHttpMethod()).collect(Collectors.toList())) CorsOriginConfiguration originConfig = new CorsOriginConfiguration() @@ -336,7 +331,7 @@ class CorsFilterSpec extends Specification { String origin = 'http://www.foo.com' HttpHeaders headers = Stub(HttpHeaders) { getOrigin() >> Optional.of(origin) - contains(ACCESS_CONTROL_REQUEST_METHOD) >> true + contains(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD) >> true } HttpRequest request = createRequest(headers) @@ -368,9 +363,9 @@ class CorsFilterSpec extends Specification { void "test handleResponse for preflight request"() { given: HttpHeaders headers = Stub(HttpHeaders) { - contains(ACCESS_CONTROL_REQUEST_METHOD) >> true - get(ACCESS_CONTROL_REQUEST_HEADERS, _) >> Optional.of(['X-Header', 'Y-Header']) - getFirst(ACCESS_CONTROL_REQUEST_METHOD, _) >> Optional.of(HttpMethod.GET) + contains(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD) >> true + get(HttpHeaders.ACCESS_CONTROL_REQUEST_HEADERS, _) >> Optional.of(['X-Header', 'Y-Header']) + getFirst(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, _) >> Optional.of(HttpMethod.GET) getOrigin() >> Optional.of('http://www.foo.com') } URI uri = new URIBuilder('/example').build() @@ -424,9 +419,9 @@ class CorsFilterSpec extends Specification { HttpHeaders headers = Stub(HttpHeaders) { getOrigin() >> Optional.of('http://www.foo.com') - contains(ACCESS_CONTROL_REQUEST_METHOD) >> true - get(ACCESS_CONTROL_REQUEST_HEADERS, _) >> Optional.of(['X-Header', 'Y-Header']) - getFirst(ACCESS_CONTROL_REQUEST_METHOD, _) >> Optional.of(HttpMethod.GET) + contains(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD) >> true + get(HttpHeaders.ACCESS_CONTROL_REQUEST_HEADERS, _) >> Optional.of(['X-Header', 'Y-Header']) + getFirst(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, _) >> Optional.of(HttpMethod.GET) } URI uri = new URIBuilder( '/example' ).build() HttpRequest request = Stub(HttpRequest) { @@ -436,8 +431,7 @@ class CorsFilterSpec extends Specification { getOrigin() >> headers.getOrigin() } List> routes = embeddedServer.getApplicationContext().getBean(Router). - findAny(request.getUri().toString(), request) - .collect(Collectors.toList()) + findAny(request) request.getAttribute(HttpAttributes.AVAILABLE_HTTP_METHODS, _) >> Optional.of(routes.stream().map(route->route.getHttpMethod()).collect(Collectors.toList())) when: @@ -466,9 +460,9 @@ class CorsFilterSpec extends Specification { given: String origin = 'http://www.foo.com' HttpHeaders headers = Stub(HttpHeaders) { - getFirst(ACCESS_CONTROL_REQUEST_METHOD, _) >> Optional.of(HttpMethod.GET) + getFirst(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, _) >> Optional.of(HttpMethod.GET) getOrigin() >> Optional.of(origin) - contains(ACCESS_CONTROL_REQUEST_METHOD) >> true + contains(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD) >> true } URI uri = new URIBuilder( '/doesnt-exists-route' ).build() HttpRequest request = Stub(HttpRequest) { @@ -516,8 +510,8 @@ class CorsFilterSpec extends Specification { String origin = 'http://www.foo.com' HttpHeaders headers = Stub(HttpHeaders) { getOrigin() >> Optional.of(origin) - getFirst(ACCESS_CONTROL_REQUEST_METHOD, _) >> Optional.of(HttpMethod.POST) - contains(ACCESS_CONTROL_REQUEST_METHOD) >> true + getFirst(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, _) >> Optional.of(HttpMethod.POST) + contains(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD) >> true } URI uri = new URIBuilder( '/example' ).build() HttpRequest request = Stub(HttpRequest) { @@ -527,8 +521,7 @@ class CorsFilterSpec extends Specification { } List> routes = embeddedServer.getApplicationContext().getBean(Router). - findAny(request.getUri().toString(), request) - .collect(Collectors.toList()) + findAny(request) request.getAttribute(HttpAttributes.AVAILABLE_HTTP_METHODS, _) >> Optional.of(routes.stream().map(route->route.getHttpMethod()).collect(Collectors.toList())) when: diff --git a/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/stream/FluxFullBodySpec.groovy b/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/stream/FluxFullBodySpec.groovy new file mode 100644 index 00000000000..0519b6a3a63 --- /dev/null +++ b/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/stream/FluxFullBodySpec.groovy @@ -0,0 +1,40 @@ +package io.micronaut.http.server.netty.stream + +import io.micronaut.context.ApplicationContext +import io.micronaut.http.HttpRequest +import io.micronaut.http.HttpStatus +import io.micronaut.http.client.HttpClient +import io.micronaut.http.client.exceptions.HttpClientResponseException +import io.micronaut.http.server.netty.configuration.NettyHttpServerConfiguration +import io.micronaut.runtime.server.EmbeddedServer +import spock.lang.AutoCleanup +import spock.lang.Shared +import spock.lang.Specification + +class FluxFullBodySpec extends Specification { + + @Shared @AutoCleanup EmbeddedServer embeddedServer = ApplicationContext.run( + EmbeddedServer, + ["micronaut.server.netty.server-type": NettyHttpServerConfiguration.HttpServerType.FULL_CONTENT] + ) + @Shared @AutoCleanup HttpClient client = embeddedServer.applicationContext.createBean(HttpClient, embeddedServer.getURI()) + + void "test empty and non-empty flux"() { + when: + def response = client.toBlocking() + .exchange(HttpRequest.POST("/body/flux/test", "Some content"), String) + + then: + response.status() == HttpStatus.OK + response.body() == '["Some content"]' + + when: + response = client.toBlocking() + .exchange(HttpRequest.POST("/body/flux/test", null), String) + + then: + def e = thrown(HttpClientResponseException) + e.response.status() == HttpStatus.BAD_REQUEST + } + +} diff --git a/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/stream/InputStreamFullBodySpec.groovy b/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/stream/InputStreamFullBodySpec.groovy new file mode 100644 index 00000000000..9c02c75eca2 --- /dev/null +++ b/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/stream/InputStreamFullBodySpec.groovy @@ -0,0 +1,97 @@ +package io.micronaut.http.server.netty.stream + +import io.micronaut.context.ApplicationContext +import io.micronaut.context.annotation.Requires +import io.micronaut.http.HttpRequest +import io.micronaut.http.HttpResponse +import io.micronaut.http.HttpStatus +import io.micronaut.http.MediaType +import io.micronaut.http.MutableHttpResponse +import io.micronaut.http.annotation.Body +import io.micronaut.http.annotation.Consumes +import io.micronaut.http.annotation.Controller +import io.micronaut.http.annotation.Get +import io.micronaut.http.annotation.Post +import io.micronaut.http.annotation.Produces +import io.micronaut.http.client.HttpClient +import io.micronaut.http.client.annotation.Client +import io.micronaut.http.client.exceptions.HttpClientResponseException +import io.micronaut.http.client.multipart.MultipartBody +import io.micronaut.http.server.netty.configuration.NettyHttpServerConfiguration +import io.micronaut.runtime.server.EmbeddedServer +import io.micronaut.scheduling.TaskExecutors +import io.micronaut.scheduling.annotation.ExecuteOn +import jakarta.inject.Inject +import spock.lang.AutoCleanup +import spock.lang.Issue +import spock.lang.Shared +import spock.lang.Specification + +import javax.annotation.Nullable +import java.util.concurrent.ConcurrentLinkedQueue +import java.util.concurrent.CountDownLatch +import java.util.concurrent.ExecutorService +import java.util.concurrent.Executors + +class InputStreamFullBodySpec extends Specification { + + @Shared + @AutoCleanup + EmbeddedServer embeddedServer = ApplicationContext.run(EmbeddedServer, [ + 'spec.name': InputStreamBodySpec.class.name, + "micronaut.server.netty.server-type": NettyHttpServerConfiguration.HttpServerType.FULL_CONTENT + ]) + @Shared + @AutoCleanup + HttpClient client = embeddedServer.applicationContext.createBean(HttpClient, embeddedServer.getURI()) + + void "test nullable body"() { + when: + def response = client.toBlocking() + .exchange(HttpRequest.POST("/input-stream-test/hello", null)) + + then: + response.status() == HttpStatus.NO_CONTENT + } + + @Issue('https://github.com/micronaut-projects/micronaut-core/issues/6100') + void "test apply load to InputStream read"() { + when: + int max = 30 + CountDownLatch latch = new CountDownLatch(max) + + ExecutorService pool = Executors.newCachedThreadPool() + ConcurrentLinkedQueue responses = new ConcurrentLinkedQueue() + for (int i = 0; i < max; i++) { + pool.submit(() -> { + try { + MultipartBody multipartBody = MultipartBody.builder() + .addPart("myfile", + "largefile" * 1024) + .build() + HttpRequest request = HttpRequest.POST("/input-stream-test/hello", multipartBody) + .contentType(MediaType.MULTIPART_FORM_DATA_TYPE) + HttpResponse response = client.toBlocking() + .exchange(request) + responses.add(response.status()) + System.out.println(response.getStatus()) + System.out.println(response.getHeaders().asMap()) + + } catch (HttpClientResponseException e) { + System.out.println(e.getStatus()) + } catch (URISyntaxException e) { + e.printStackTrace() + } finally { + latch.countDown() + } + + }) + } + latch.await() + + then: + responses.size() == 30 + responses.every({ it == HttpStatus.OK }) + } + +} diff --git a/http-server-netty/src/test/java/io/micronaut/http/server/netty/jackson/JsonContentProcessorBenchmark.java b/http-server-netty/src/test/java/io/micronaut/http/server/netty/jackson/JsonContentProcessorBenchmark.java index e866fa8579b..0e3b66eee25 100644 --- a/http-server-netty/src/test/java/io/micronaut/http/server/netty/jackson/JsonContentProcessorBenchmark.java +++ b/http-server-netty/src/test/java/io/micronaut/http/server/netty/jackson/JsonContentProcessorBenchmark.java @@ -9,7 +9,6 @@ import io.micronaut.json.JsonMapper; import io.micronaut.json.JsonSyntaxException; import io.micronaut.json.convert.LazyJsonNode; -import io.micronaut.json.tree.JsonNode; import io.netty.buffer.ByteBuf; import io.netty.buffer.DefaultByteBufHolder; import io.netty.buffer.PooledByteBufAllocator; diff --git a/http-server/src/main/java/io/micronaut/http/server/ExecutableRouteInfo.java b/http-server/src/main/java/io/micronaut/http/server/ExecutableRouteInfo.java index e62b2885de2..dbe7e5d40da 100644 --- a/http-server/src/main/java/io/micronaut/http/server/ExecutableRouteInfo.java +++ b/http-server/src/main/java/io/micronaut/http/server/ExecutableRouteInfo.java @@ -21,19 +21,19 @@ import io.micronaut.core.type.ReturnType; import io.micronaut.inject.ExecutableMethod; import io.micronaut.inject.MethodReference; -import io.micronaut.web.router.RouteInfo; +import io.micronaut.web.router.DefaultRouteInfo; import java.lang.reflect.Method; +import java.util.List; -class ExecutableRouteInfo implements RouteInfo, MethodReference { +class ExecutableRouteInfo extends DefaultRouteInfo implements MethodReference { - private final ExecutableMethod method; - private final boolean errorRoute; + private final ExecutableMethod method; - ExecutableRouteInfo(ExecutableMethod method, + ExecutableRouteInfo(ExecutableMethod method, boolean errorRoute) { + super(method, method.getReturnType(), List.of(), List.of(), method.getDeclaringType(), errorRoute, false); this.method = method; - this.errorRoute = errorRoute; } @Override @@ -47,7 +47,7 @@ public Method getTargetMethod() { } @Override - public ReturnType getReturnType() { + public ReturnType getReturnType() { return method.getReturnType(); } @@ -60,12 +60,6 @@ public Class getDeclaringType() { public String getMethodName() { return method.getMethodName(); } - - @Override - public boolean isErrorRoute() { - return errorRoute; - } - @Override @NonNull public AnnotationMetadata getAnnotationMetadata() { diff --git a/http-server/src/main/java/io/micronaut/http/server/RequestLifecycle.java b/http-server/src/main/java/io/micronaut/http/server/RequestLifecycle.java index bc1d98cf4d2..070a827af20 100644 --- a/http-server/src/main/java/io/micronaut/http/server/RequestLifecycle.java +++ b/http-server/src/main/java/io/micronaut/http/server/RequestLifecycle.java @@ -15,6 +15,7 @@ */ package io.micronaut.http.server; +import io.micronaut.core.annotation.AnnotationMetadata; import io.micronaut.core.annotation.Nullable; import io.micronaut.core.convert.exceptions.ConversionErrorException; import io.micronaut.core.execution.ExecutionFlow; @@ -30,6 +31,7 @@ import io.micronaut.http.context.ServerRequestContext; import io.micronaut.http.filter.FilterRunner; import io.micronaut.http.filter.GenericHttpFilter; +import io.micronaut.http.server.binding.RequestArgumentSatisfier; import io.micronaut.http.server.exceptions.ExceptionHandler; import io.micronaut.http.server.exceptions.response.ErrorContext; import io.micronaut.http.server.types.files.FileCustomizableResponseType; @@ -37,6 +39,7 @@ import io.micronaut.inject.ExecutableMethod; import io.micronaut.inject.qualifiers.Qualifiers; import io.micronaut.json.JsonSyntaxException; +import io.micronaut.web.router.DefaultRouteInfo; import io.micronaut.web.router.RouteInfo; import io.micronaut.web.router.RouteMatch; import io.micronaut.web.router.UriRouteMatch; @@ -67,6 +70,7 @@ public class RequestLifecycle { private static final Logger LOG = LoggerFactory.getLogger(RequestLifecycle.class); private final RouteExecutor routeExecutor; + private final RequestArgumentSatisfier requestArgumentSatisfier; private HttpRequest request; private Context context = Context.empty(); private boolean multipartEnabled = true; @@ -78,6 +82,7 @@ public class RequestLifecycle { protected RequestLifecycle(RouteExecutor routeExecutor, HttpRequest request) { this.routeExecutor = Objects.requireNonNull(routeExecutor, "routeExecutor"); this.request = Objects.requireNonNull(request, "request"); + this.requestArgumentSatisfier = routeExecutor.getRequestArgumentSatisfier(); } /** @@ -131,7 +136,7 @@ protected final ExecutionFlow> normalFlow() { LOG.trace("Matched route {} - {} to controller {}", request.getMethodName(), request.getUri().getPath(), routeMatch.getDeclaringType()); } // all ok proceed to try and execute the route - if (routeMatch.isWebSocketRoute()) { + if (routeMatch.getRouteInfo().isWebSocketRoute()) { return onStatusError( HttpResponse.status(HttpStatus.BAD_REQUEST), "Not a WebSocket request"); @@ -194,31 +199,17 @@ final ExecutionFlow> onErrorNoFilter(Throwable t) { final Optional> optionalMethod = handlerDefinition.findPossibleMethods("handle").findFirst(); RouteInfo routeInfo; if (optionalMethod.isPresent()) { - routeInfo = new ExecutableRouteInfo(optionalMethod.get(), true); + routeInfo = new ExecutableRouteInfo<>(optionalMethod.get(), true); } else { - routeInfo = new RouteInfo<>() { - @Override - public ReturnType getReturnType() { - return ReturnType.of(Object.class); - } - - @Override - public Class getDeclaringType() { - return handlerDefinition.getBeanType(); - } - - @Override - public boolean isErrorRoute() { - return true; - } - - @Override - public List getProduces() { - return MediaType.fromType(getDeclaringType()) - .map(Collections::singletonList) - .orElse(Collections.emptyList()); - } - }; + routeInfo = new DefaultRouteInfo<>( + AnnotationMetadata.EMPTY_METADATA, + ReturnType.of(Object.class), + List.of(), + MediaType.fromType(handlerDefinition.getBeanType()).map(Collections::singletonList).orElse(Collections.emptyList()), + handlerDefinition.getBeanType(), + true, + false + ); } Supplier>> responseSupplier = () -> { ExceptionHandler handler = routeExecutor.beanContext.getBean(handlerDefinition); @@ -227,7 +218,7 @@ public List getProduces() { routeExecutor.logException(cause); } Object result = handler.handle(request, cause); - return routeExecutor.createResponseForBody(request, result, routeInfo); + return routeExecutor.createResponseForBody(request, result, routeInfo, null); } catch (Throwable e) { return createDefaultErrorResponseFlow(request, e); } @@ -293,7 +284,7 @@ private ExecutionFlow> handleStatusException(MutableHttpR if (response.code() >= 400 && routeInfo != null && !routeInfo.isErrorRoute()) { RouteMatch statusRoute = routeExecutor.findStatusRoute(request, response.status(), routeInfo); if (statusRoute != null) { - return ExecutionFlow.just(statusRoute) + return fulfillArguments(statusRoute) .flatMap(routeMatch -> routeExecutor.callRoute(Context.empty(), routeMatch, request)) .flatMap(this::handleStatusException) .onErrorResume(this::onErrorNoFilter); @@ -316,8 +307,7 @@ final ExecutionFlow> onRouteMiss(HttpRequest httpReque } // if there is no route present try to locate a route that matches a different HTTP method - final List> anyMatchingRoutes = routeExecutor.router - .findAny(httpRequest.getPath(), httpRequest).toList(); + final List> anyMatchingRoutes = routeExecutor.router.findAny(httpRequest); final Collection acceptedTypes = httpRequest.accept(); final boolean hasAcceptHeader = CollectionUtils.isNotEmpty(acceptedTypes); @@ -325,15 +315,15 @@ final ExecutionFlow> onRouteMiss(HttpRequest httpReque Set allowedMethods = new HashSet<>(5); Set produceableContentTypes = hasAcceptHeader ? new HashSet<>(5) : null; for (UriRouteMatch anyRoute : anyMatchingRoutes) { - final String routeMethod = anyRoute.getRoute().getHttpMethodName(); + final String routeMethod = anyRoute.getRouteInfo().getHttpMethodName(); if (!requestMethodName.equals(routeMethod)) { allowedMethods.add(routeMethod); } - if (contentType != null && !anyRoute.doesConsume(contentType)) { - acceptableContentTypes.addAll(anyRoute.getRoute().getConsumes()); + if (contentType != null && !anyRoute.getRouteInfo().doesConsume(contentType)) { + acceptableContentTypes.addAll(anyRoute.getRouteInfo().getConsumes()); } - if (hasAcceptHeader && !anyRoute.doesProduce(acceptedTypes)) { - produceableContentTypes.addAll(anyRoute.getRoute().getProduces()); + if (hasAcceptHeader && !anyRoute.getRouteInfo().doesProduce(acceptedTypes)) { + produceableContentTypes.addAll(anyRoute.getRouteInfo().getProduces()); } } @@ -417,6 +407,7 @@ protected FileCustomizableResponseType findFile() { */ protected ExecutionFlow> fulfillArguments(RouteMatch routeMatch) { // try to fulfill the argument requirements of the route - return ExecutionFlow.just(routeExecutor.requestArgumentSatisfier.fulfillArgumentRequirements(routeMatch, request(), false)); + routeExecutor.requestArgumentSatisfier.fulfillArgumentRequirementsBeforeFilters(routeMatch, request()); + return ExecutionFlow.just(routeMatch); } } diff --git a/http-server/src/main/java/io/micronaut/http/server/RouteExecutor.java b/http-server/src/main/java/io/micronaut/http/server/RouteExecutor.java index 18877024f7a..7033a63b267 100644 --- a/http-server/src/main/java/io/micronaut/http/server/RouteExecutor.java +++ b/http-server/src/main/java/io/micronaut/http/server/RouteExecutor.java @@ -46,6 +46,8 @@ import io.micronaut.inject.BeanType; import io.micronaut.inject.MethodReference; import io.micronaut.scheduling.executor.ExecutorSelector; +import io.micronaut.web.router.DefaultRouteInfo; +import io.micronaut.web.router.MethodBasedRouteInfo; import io.micronaut.web.router.MethodBasedRouteMatch; import io.micronaut.web.router.RouteInfo; import io.micronaut.web.router.RouteMatch; @@ -177,7 +179,7 @@ UriRouteMatch findRouteMatch(HttpRequest httpRequest) { } if (routeMatch == null && httpRequest.getMethod().equals(HttpMethod.OPTIONS)) { - List> anyUriRoutes = router.findAny(httpRequest.getPath(), httpRequest).toList(); + List> anyUriRoutes = router.findAny(httpRequest); if (!anyUriRoutes.isEmpty()) { setRouteAttributes(httpRequest, anyUriRoutes.get(0)); httpRequest.setAttribute(AVAILABLE_HTTP_METHODS, anyUriRoutes.stream().map(UriRouteMatch::getHttpMethod).toList()); @@ -187,10 +189,9 @@ UriRouteMatch findRouteMatch(HttpRequest httpRequest) { } static void setRouteAttributes(HttpRequest request, UriRouteMatch route) { - request.setAttribute(HttpAttributes.ROUTE, route.getRoute()); request.setAttribute(HttpAttributes.ROUTE_MATCH, route); - request.setAttribute(HttpAttributes.ROUTE_INFO, route); - request.setAttribute(HttpAttributes.URI_TEMPLATE, route.getRoute().getUriMatchTemplate().toString()); + request.setAttribute(HttpAttributes.ROUTE_INFO, route.getRouteInfo()); + request.setAttribute(HttpAttributes.URI_TEMPLATE, route.getRouteInfo().getUriMatchTemplate().toString()); } /** @@ -206,29 +207,18 @@ public MutableHttpResponse createDefaultErrorResponse(HttpRequest httpRequ logException(cause); final MutableHttpResponse response = HttpResponse.serverError(); response.setAttribute(HttpAttributes.EXCEPTION, cause); - response.setAttribute(HttpAttributes.ROUTE_INFO, new RouteInfo() { - @Override - public ReturnType getReturnType() { - return ReturnType.of(MutableHttpResponse.class, Argument.OBJECT_ARGUMENT); - } - - @Override - public Class getDeclaringType() { - return Object.class; - } - - @Override - public boolean isErrorRoute() { - return true; - } - }); + response.setAttribute(HttpAttributes.ROUTE_INFO, new DefaultRouteInfo<>( + ReturnType.of(MutableHttpResponse.class, Argument.OBJECT_ARGUMENT), + Object.class, + true, + false)); MutableHttpResponse mutableHttpResponse = errorResponseProcessor.processResponse( ErrorContext.builder(httpRequest) .cause(cause) .errorMessage("Internal Server Error: " + cause.getMessage()) .build(), response); applyConfiguredHeaders(mutableHttpResponse.getHeaders()); - if (!mutableHttpResponse.getContentType().isPresent() && httpRequest.getMethod() != HttpMethod.HEAD) { + if (mutableHttpResponse.getContentType().isEmpty() && httpRequest.getMethod() != HttpMethod.HEAD) { return mutableHttpResponse.contentType(MediaType.APPLICATION_JSON_TYPE); } return mutableHttpResponse; @@ -350,7 +340,7 @@ RouteMatch findErrorRoute(Throwable cause, if (LOG.isDebugEnabled()) { LOG.debug("Found matching exception handler for exception [{}]: {}", cause.getMessage(), errorRoute); } - errorRoute = requestArgumentSatisfier.fulfillArgumentRequirements(errorRoute, httpRequest, false); + requestArgumentSatisfier.fulfillArgumentRequirementsBeforeFilters(errorRoute, httpRequest); } return errorRoute; @@ -368,11 +358,13 @@ RouteMatch findStatusRoute(HttpRequest incomingRequest, HttpStatus st return statusRoute; } - ExecutorService findExecutor(RouteInfo routeMatch) { + ExecutorService findExecutor(RouteInfo routeInfo) { // Select the most appropriate Executor ExecutorService executor; - if (routeMatch instanceof MethodReference) { - executor = executorSelector.select((MethodReference) routeMatch, serverConfiguration.getThreadSelection()).orElse(null); + if (routeInfo instanceof MethodReference methodReference) { + executor = executorSelector.select(methodReference, serverConfiguration.getThreadSelection()).orElse(null); + } else if (routeInfo instanceof MethodBasedRouteInfo methodBasedRouteInfo) { + executor = executorSelector.select(methodBasedRouteInfo.getTargetMethod().getExecutableMethod(), serverConfiguration.getThreadSelection()).orElse(null); } else { executor = null; } @@ -412,7 +404,7 @@ private MutableHttpResponse toMutableResponse(HttpResponse message) { return mutableHttpResponse; } - private ExecutionFlow> fromImperativeExecute(HttpRequest request, RouteInfo routeInfo, HttpStatus defaultHttpStatus, Object body) { + private ExecutionFlow> fromImperativeExecute(HttpRequest request, RouteInfo routeInfo, Object body) { if (body instanceof HttpResponse) { MutableHttpResponse outgoingResponse = toMutableResponse((HttpResponse) body); final Argument bodyArgument = routeInfo.getReturnType().getFirstTypeVariable().orElse(Argument.OBJECT_ARGUMENT); @@ -423,15 +415,16 @@ private ExecutionFlow> fromImperativeExecute(HttpRequest< } return ExecutionFlow.just(outgoingResponse); } - return ExecutionFlow.just(forStatus(routeInfo, defaultHttpStatus).body(body)); + return ExecutionFlow.just(forStatus(routeInfo, null).body(body)); } ExecutionFlow> callRoute(ContextView contextFromFilter, RouteMatch routeMatch, HttpRequest request) { - ExecutorService executorService = findExecutor(routeMatch); + RouteInfo routeInfo = routeMatch.getRouteInfo(); + ExecutorService executorService = findExecutor(routeInfo); Supplier>> flowSupplier = () -> executeRouteAndConvertBody(routeMatch, request); ExecutionFlow> executeMethodResponseFlow; if (executorService != null) { - if (routeMatch.isSuspended()) { + if (routeInfo.isSuspended()) { executeMethodResponseFlow = ReactiveExecutionFlow.fromPublisher(Mono.deferContextual(contextView -> { coroutineHelper.ifPresent(helper -> helper.setupCoroutineContext(request, contextView)); return Mono.from( @@ -439,14 +432,14 @@ ExecutionFlow> callRoute(ContextView contextFromFilter, R ); }).contextWrite(contextFromFilter)) .putInContext(ServerRequestContext.KEY, request); - } else if (routeMatch.isReactive()) { + } else if (routeInfo.isReactive()) { executeMethodResponseFlow = ReactiveExecutionFlow.async(executorService, flowSupplier) .putInContext(ServerRequestContext.KEY, request); } else { executeMethodResponseFlow = ExecutionFlow.async(executorService, flowSupplier); } } else { - if (routeMatch.isSuspended()) { + if (routeInfo.isSuspended()) { executeMethodResponseFlow = ReactiveExecutionFlow.fromPublisher(Mono.deferContextual(contextView -> { coroutineHelper.ifPresent(helper -> helper.setupCoroutineContext(request, contextView)); return Mono.from( @@ -454,7 +447,7 @@ ExecutionFlow> callRoute(ContextView contextFromFilter, R ); }).contextWrite(contextFromFilter)) .putInContext(ServerRequestContext.KEY, request); - } else if (routeMatch.isReactive()) { + } else if (routeInfo.isReactive()) { executeMethodResponseFlow = ReactiveExecutionFlow.fromFlow(flowSupplier.get()) .putInContext(ServerRequestContext.KEY, request); } else { @@ -466,27 +459,22 @@ ExecutionFlow> callRoute(ContextView contextFromFilter, R private ExecutionFlow> executeRouteAndConvertBody(RouteMatch routeMatch, HttpRequest httpRequest) { try { - final RouteMatch finalRoute; - // ensure the route requirements are completely satisfied - if (!routeMatch.isExecutable()) { - finalRoute = requestArgumentSatisfier - .fulfillArgumentRequirements(routeMatch, httpRequest, true); - } else { - finalRoute = routeMatch; - } - Object body = ServerRequestContext.with(httpRequest, (Supplier) finalRoute::execute); + requestArgumentSatisfier.fulfillArgumentRequirementsAfterFilters(routeMatch, httpRequest); + Object body = ServerRequestContext.with(httpRequest, (Supplier) routeMatch::execute); if (body instanceof Optional) { body = ((Optional) body).orElse(null); } - return createResponseForBody(httpRequest, body, finalRoute); + return createResponseForBody(httpRequest, body, routeMatch.getRouteInfo(), routeMatch); } catch (Throwable e) { return ExecutionFlow.error(e); } } ExecutionFlow> createResponseForBody(HttpRequest request, - Object body, - RouteInfo routeInfo) { + Object body, + RouteInfo routeInfo, + @Nullable + RouteMatch routeMatch) { ExecutionFlow> outgoingResponse; if (body == null) { if (routeInfo.isVoid()) { @@ -499,20 +487,19 @@ ExecutionFlow> createResponseForBody(HttpRequest reque outgoingResponse = ExecutionFlow.just(newNotFoundError(request)); } } else { - HttpStatus defaultHttpStatus = routeInfo.isErrorRoute() ? HttpStatus.INTERNAL_SERVER_ERROR : HttpStatus.OK; // special case HttpResponse because FullNettyClientHttpResponse implements Completable... boolean isReactive = routeInfo.isAsyncOrReactive() || (Publishers.isConvertibleToPublisher(body) && !(body instanceof HttpResponse)); if (isReactive) { outgoingResponse = ReactiveExecutionFlow.fromPublisher( - fromReactiveExecute(request, body, routeInfo, defaultHttpStatus) + fromReactiveExecute(request, body, routeInfo) ); } else if (body instanceof HttpStatus httpStatus) { // now we have the raw result, transform it as necessary outgoingResponse = ExecutionFlow.just(HttpResponse.status(httpStatus)); } else { if (routeInfo.isSuspended()) { - outgoingResponse = fromKotlinCoroutineExecute(request, body, routeInfo, defaultHttpStatus); + outgoingResponse = fromKotlinCoroutineExecute(request, body, routeInfo); } else { - outgoingResponse = fromImperativeExecute(request, routeInfo, defaultHttpStatus, body); + outgoingResponse = fromImperativeExecute(request, routeInfo, body); } } } @@ -526,8 +513,8 @@ ExecutionFlow> createResponseForBody(HttpRequest reque response.body(null); } applyConfiguredHeaders(response.getHeaders()); - if (routeInfo instanceof RouteMatch) { - response.setAttribute(HttpAttributes.ROUTE_MATCH, routeInfo); + if (routeMatch != null) { + response.setAttribute(HttpAttributes.ROUTE_MATCH, routeMatch); } response.setAttribute(HttpAttributes.ROUTE_INFO, routeInfo); return response; @@ -535,11 +522,11 @@ ExecutionFlow> createResponseForBody(HttpRequest reque return outgoingResponse; } - private ExecutionFlow> fromKotlinCoroutineExecute(HttpRequest request, Object body, RouteInfo routeInfo, HttpStatus defaultHttpStatus) { + private ExecutionFlow> fromKotlinCoroutineExecute(HttpRequest request, Object body, RouteInfo routeInfo) { ExecutionFlow> outgoingResponse; boolean isKotlinFunctionReturnTypeUnit = - routeInfo instanceof MethodBasedRouteMatch && - isKotlinFunctionReturnTypeUnit(((MethodBasedRouteMatch) routeInfo).getExecutableMethod()); + routeInfo instanceof MethodBasedRouteMatch methodBasedRouteMatch && + isKotlinFunctionReturnTypeUnit(methodBasedRouteMatch.getExecutableMethod()); final Supplier> supplier = ContinuationArgumentBinder.extractContinuationCompletableFutureSupplier(request); if (isKotlinCoroutineSuspended(body)) { return ReactiveExecutionFlow.fromPublisher( @@ -553,7 +540,7 @@ private ExecutionFlow> fromKotlinCoroutineExecute(HttpReq return processPublisherBody(request, response, routeInfo); } } else { - response = forStatus(routeInfo, defaultHttpStatus); + response = forStatus(routeInfo, null); if (!isKotlinFunctionReturnTypeUnit) { response = response.body(obj); } @@ -569,11 +556,11 @@ private ExecutionFlow> fromKotlinCoroutineExecute(HttpReq } else { suspendedBody = body; } - outgoingResponse = fromImperativeExecute(request, routeInfo, defaultHttpStatus, suspendedBody); + outgoingResponse = fromImperativeExecute(request, routeInfo, suspendedBody); return outgoingResponse; } - private CorePublisher> fromReactiveExecute(HttpRequest request, Object body, RouteInfo routeInfo, HttpStatus defaultHttpStatus) { + private CorePublisher> fromReactiveExecute(HttpRequest request, Object body, RouteInfo routeInfo) { Class bodyClass = body.getClass(); boolean isSingle = isSingle(routeInfo, bodyClass); boolean isCompletable = !isSingle && routeInfo.isVoid() && Publishers.isCompletable(bodyClass); @@ -611,7 +598,7 @@ private CorePublisher> fromReactiveExecute(HttpRequest } else if (o instanceof HttpStatus) { singleResponse = forStatus(routeInfo, (HttpStatus) o); } else { - singleResponse = forStatus(routeInfo, defaultHttpStatus) + singleResponse = forStatus(routeInfo, null) .body(o); } return Flux.just(singleResponse); @@ -632,7 +619,7 @@ private CorePublisher> fromReactiveExecute(HttpRequest } return response; } - MutableHttpResponse response = forStatus(routeInfo, defaultHttpStatus).body(body); + MutableHttpResponse response = forStatus(routeInfo, null).body(body); return processPublisherBody(request, response, routeInfo); } diff --git a/http-server/src/main/java/io/micronaut/http/server/binding/RequestArgumentSatisfier.java b/http-server/src/main/java/io/micronaut/http/server/binding/RequestArgumentSatisfier.java index 5fcf9e94c0d..533edb26f87 100644 --- a/http-server/src/main/java/io/micronaut/http/server/binding/RequestArgumentSatisfier.java +++ b/http-server/src/main/java/io/micronaut/http/server/binding/RequestArgumentSatisfier.java @@ -16,28 +16,11 @@ package io.micronaut.http.server.binding; import io.micronaut.core.annotation.Internal; -import io.micronaut.core.bind.ArgumentBinder; -import io.micronaut.core.convert.ArgumentConversionContext; -import io.micronaut.core.convert.ConversionContext; -import io.micronaut.core.convert.ConversionError; -import io.micronaut.core.type.Argument; -import io.micronaut.http.HttpMethod; import io.micronaut.http.HttpRequest; import io.micronaut.http.bind.RequestBinderRegistry; -import io.micronaut.http.bind.binders.BodyArgumentBinder; -import io.micronaut.http.bind.binders.NonBlockingBodyArgumentBinder; -import io.micronaut.http.bind.binders.RequestBeanAnnotationBinder; -import io.micronaut.web.router.NullArgument; import io.micronaut.web.router.RouteMatch; -import io.micronaut.web.router.UnresolvedArgument; import jakarta.inject.Singleton; -import java.util.Collection; -import java.util.Collections; -import java.util.LinkedHashMap; -import java.util.Map; -import java.util.Optional; - /** * A class containing methods to aid in satisfying arguments of a {@link io.micronaut.web.router.Route}. * @@ -67,104 +50,21 @@ public RequestBinderRegistry getBinderRegistry() { /** * Attempt to satisfy the arguments of the given route with the data from the given request. * - * @param route The route - * @param request The request - * @param satisfyOptionals Whether to satisfy optionals - * @return The route - */ - public RouteMatch fulfillArgumentRequirements(RouteMatch route, HttpRequest request, boolean satisfyOptionals) { - Collection> requiredArguments = route.getRequiredArguments(); - Map argumentValues; - - if (requiredArguments.isEmpty()) { - // no required arguments so just execute - argumentValues = Collections.emptyMap(); - } else { - argumentValues = new LinkedHashMap<>(requiredArguments.size()); - // Begin try fulfilling the argument requirements - for (Argument argument : requiredArguments) { - getValueForArgument(argument, request, satisfyOptionals).ifPresent(value -> - argumentValues.put(argument.getName(), value)); - } - } - - route = route.fulfill(argumentValues); - return route; - } - - /** - * @param argument The argument - * @param request The HTTP request - * @param satisfyOptionals Whether to satisfy optionals - * @return An {@link Optional} for the value + * @param route The route + * @param request The request */ - protected Optional getValueForArgument(Argument argument, HttpRequest request, boolean satisfyOptionals) { - Object value = null; - Optional registeredBinder = - binderRegistry.findArgumentBinder(argument, request); - if (registeredBinder.isPresent()) { - ArgumentBinder argumentBinder = registeredBinder.get(); - ArgumentConversionContext conversionContext = ConversionContext.of( - argument, - request.getLocale().orElse(null), - request.getCharacterEncoding() - ); - - if (argumentBinder instanceof BodyArgumentBinder) { - if (argumentBinder instanceof NonBlockingBodyArgumentBinder) { - ArgumentBinder.BindingResult bindingResult = argumentBinder - .bind(conversionContext, request); - - if (bindingResult.isPresentAndSatisfied()) { - value = bindingResult.get(); - } else if (bindingResult.isSatisfied() && argument.isNullable()) { - value = NullArgument.INSTANCE; - } - } else { - value = getValueForBlockingBodyArgumentBinder(request, argumentBinder, conversionContext); - } - } else if (argumentBinder instanceof RequestBeanAnnotationBinder) { - // Resolve RequestBean after filters since some field types may depend on filters, i.e. Authentication - value = (UnresolvedArgument) () -> argumentBinder.bind(conversionContext, request); - } else { - ArgumentBinder.BindingResult bindingResult = argumentBinder.bind(conversionContext, request); - - if (argument.getType() == Optional.class) { - if (bindingResult.isSatisfied() || satisfyOptionals) { - Optional optionalValue = bindingResult.getValue(); - if (optionalValue.isPresent()) { - value = optionalValue.get(); - } else { - value = optionalValue; - } - } - } else if (bindingResult.isPresentAndSatisfied()) { - value = bindingResult.get(); - } else if (bindingResult.isSatisfied() && argument.isNullable()) { - value = NullArgument.INSTANCE; - } else if (HttpMethod.requiresRequestBody(request.getMethod()) || argument.isNullable() || conversionContext.hasErrors()) { - value = (UnresolvedArgument) () -> { - ArgumentBinder.BindingResult result = argumentBinder.bind(conversionContext, request); - Optional lastError = conversionContext.getLastError(); - if (lastError.isPresent()) { - return (ArgumentBinder.BindingResult) () -> lastError; - } - return result; - }; - } - } - } - return Optional.ofNullable(value); + public void fulfillArgumentRequirementsBeforeFilters(RouteMatch route, HttpRequest request) { + route.fulfillBeforeFilters(binderRegistry, request); } /** + * Attempt to satisfy the arguments of the given route with the data from the given request. * + * @param route The route * @param request The request - * @param argumentBinder The argument binder - * @param conversionContext The conversion context - * @return The body argument */ - private UnresolvedArgument getValueForBlockingBodyArgumentBinder(HttpRequest request, ArgumentBinder> argumentBinder, ArgumentConversionContext conversionContext) { - return () -> argumentBinder.bind(conversionContext, request); + public void fulfillArgumentRequirementsAfterFilters(RouteMatch route, HttpRequest request) { + route.fulfillAfterFilters(binderRegistry, request); } + } diff --git a/http/src/main/java/io/micronaut/http/HttpAttributes.java b/http/src/main/java/io/micronaut/http/HttpAttributes.java index 13816dc2c63..59a7883d140 100644 --- a/http/src/main/java/io/micronaut/http/HttpAttributes.java +++ b/http/src/main/java/io/micronaut/http/HttpAttributes.java @@ -33,18 +33,13 @@ public enum HttpAttributes implements CharSequence { */ ERROR(Constants.PREFIX + ".error"), - /** - * Attribute used to store the object that represents the Route. - */ - ROUTE(Constants.PREFIX + ".route"), - /** * Attribute used to store the object that represents the Route match. */ ROUTE_MATCH(Constants.PREFIX + ".route.match"), /** - * Attribute used to store the object that represents the Route metadata. + * Attribute used to store the object that represents the Route. */ ROUTE_INFO(Constants.PREFIX + ".route.info"), diff --git a/http/src/main/java/io/micronaut/http/MediaType.java b/http/src/main/java/io/micronaut/http/MediaType.java index 7d42b4f46e2..47d31710495 100644 --- a/http/src/main/java/io/micronaut/http/MediaType.java +++ b/http/src/main/java/io/micronaut/http/MediaType.java @@ -401,6 +401,8 @@ public class MediaType implements CharSequence { private BigDecimal qualityNumberField = BigDecimal.ONE; + private boolean valid; + static { textTypePatterns.add(Pattern.compile("^text/.*$")); textTypePatterns.add(Pattern.compile("^.*\\+json$")); @@ -699,6 +701,21 @@ public static boolean isTextBased(String contentType) { } } + /** + * Validate this media type for sending as an HTTP header. This is an optimization to only run + * the validation once if possible. If the validation function does not throw, future calls to + * this method will not call the validation function again. + * + * @param r Validation function + */ + @Internal + public void validate(Runnable r) { + if (!valid) { + r.run(); + valid = true; + } + } + @Override public String toString() { return strRepr; diff --git a/http/src/main/java/io/micronaut/http/bind/DefaultRequestBinderRegistry.java b/http/src/main/java/io/micronaut/http/bind/DefaultRequestBinderRegistry.java index b45559ec6e2..53caac6581d 100644 --- a/http/src/main/java/io/micronaut/http/bind/DefaultRequestBinderRegistry.java +++ b/http/src/main/java/io/micronaut/http/bind/DefaultRequestBinderRegistry.java @@ -35,19 +35,21 @@ import io.micronaut.http.bind.binders.CookieAnnotationBinder; import io.micronaut.http.bind.binders.DefaultBodyAnnotationBinder; import io.micronaut.http.bind.binders.HeaderAnnotationBinder; -import io.micronaut.http.bind.binders.ParameterAnnotationBinder; import io.micronaut.http.bind.binders.PartAnnotationBinder; import io.micronaut.http.bind.binders.PathVariableAnnotationBinder; +import io.micronaut.http.bind.binders.QueryValueArgumentBinder; import io.micronaut.http.bind.binders.RequestArgumentBinder; import io.micronaut.http.bind.binders.RequestAttributeAnnotationBinder; import io.micronaut.http.bind.binders.RequestBeanAnnotationBinder; import io.micronaut.http.bind.binders.TypedRequestArgumentBinder; +import io.micronaut.http.bind.binders.DefaultUnmatchedRequestArgumentBinder; import io.micronaut.http.cookie.Cookie; import io.micronaut.http.cookie.Cookies; import jakarta.inject.Inject; import jakarta.inject.Singleton; import java.lang.annotation.Annotation; +import java.util.ArrayList; import java.util.Arrays; import java.util.LinkedHashMap; import java.util.List; @@ -73,6 +75,8 @@ public class DefaultRequestBinderRegistry implements RequestBinderRegistry { private final ConversionService conversionService; private final Map> argumentBinderCache = new ConcurrentLinkedHashMap.Builder>().maximumWeightedCapacity(CACHE_MAX_SIZE).build(); + private final List> unmatchedBinders = new ArrayList<>(); + private final DefaultUnmatchedRequestArgumentBinder defaultUnmatchedRequestArgumentBinder; /** * @param conversionService The conversion service @@ -83,16 +87,15 @@ public DefaultRequestBinderRegistry(ConversionService conversionService, Request } /** - * @param conversionService The conversion service - * @param binders The request argument binders + * @param conversionService The conversion service + * @param binders The request argument binders */ @Inject public DefaultRequestBinderRegistry(ConversionService conversionService, List binders) { this.conversionService = conversionService; - if (CollectionUtils.isNotEmpty(binders)) { for (RequestArgumentBinder binder : binders) { - addRequestArgumentBinder(binder); + addArgumentBinder(binder); } } @@ -113,19 +116,19 @@ public DefaultRequestBinderRegistry(ConversionService conversionService, List) (argument, source) -> { if (source instanceof PushCapableHttpRequest) { Optional> typeVariable = argument.getFirstTypeVariable() - .filter(arg -> arg.getType() != Object.class) - .filter(arg -> arg.getType() != Void.class); + .filter(arg -> arg.getType() != Object.class) + .filter(arg -> arg.getType() != Void.class); if (typeVariable.isPresent() && HttpMethod.permitsRequestBody(source.getMethod())) { if (source.getBody().isPresent()) { return () -> Optional.of(new PushCapableFullHttpRequest((PushCapableHttpRequest) source, typeVariable.get())); } else { - return ArgumentBinder.BindingResult.EMPTY; + return ArgumentBinder.BindingResult.empty(); } } else { return () -> Optional.of((PushCapableHttpRequest) source); } } else { - return ArgumentBinder.BindingResult.UNSATISFIED; + return ArgumentBinder.BindingResult.unsatisfied(); } }); byType.put(Argument.of(HttpParameters.class).typeHashCode(), (RequestArgumentBinder) (argument, source) -> () -> Optional.of(source.getParameters())); @@ -140,16 +143,25 @@ public DefaultRequestBinderRegistry(ConversionService conversionService, List finalCookie != null ? Optional.of(finalCookie) : Optional.empty(); }); + + defaultUnmatchedRequestArgumentBinder = new DefaultUnmatchedRequestArgumentBinder<>( + List.of( + new QueryValueArgumentBinder<>(conversionService), + new RequestAttributeAnnotationBinder<>(conversionService) + ), + unmatchedBinders, + List.of( + new DefaultBodyAnnotationBinder<>(conversionService) + ) + ); } @SuppressWarnings("rawtypes") @Override - public void addRequestArgumentBinder(ArgumentBinder binder) { - if (binder instanceof AnnotatedRequestArgumentBinder) { - AnnotatedRequestArgumentBinder annotatedRequestArgumentBinder = (AnnotatedRequestArgumentBinder) binder; + public void addArgumentBinder(ArgumentBinder> binder) { + if (binder instanceof AnnotatedRequestArgumentBinder annotatedRequestArgumentBinder) { Class annotationType = annotatedRequestArgumentBinder.getAnnotationType(); - if (binder instanceof TypedRequestArgumentBinder) { - TypedRequestArgumentBinder typedRequestArgumentBinder = (TypedRequestArgumentBinder) binder; + if (binder instanceof TypedRequestArgumentBinder typedRequestArgumentBinder) { Argument argumentType = typedRequestArgumentBinder.argumentType(); byTypeAndAnnotation.put(new TypeAndAnnotation(argumentType, annotationType), (RequestArgumentBinder) binder); List> superTypes = typedRequestArgumentBinder.superTypes(); @@ -162,14 +174,18 @@ public void addRequestArgumentBinder(ArgumentBinder binder) { byAnnotation.put(annotationType, annotatedRequestArgumentBinder); } - } else if (binder instanceof TypedRequestArgumentBinder) { - TypedRequestArgumentBinder typedRequestArgumentBinder = (TypedRequestArgumentBinder) binder; + } else if (binder instanceof TypedRequestArgumentBinder typedRequestArgumentBinder) { byType.put(typedRequestArgumentBinder.argumentType().typeHashCode(), typedRequestArgumentBinder); } } @Override - public Optional>> findArgumentBinder(Argument argument, HttpRequest source) { + public void addUnmatchedRequestArgumentBinder(RequestArgumentBinder binder) { + unmatchedBinders.add(binder); + } + + @Override + public Optional>> findArgumentBinder(Argument argument) { Optional> opt = argument.getAnnotationMetadata().getAnnotationTypeByStereotype(Bindable.class); if (opt.isPresent()) { Class annotationType = opt.get(); @@ -191,7 +207,7 @@ public Optional>> findArgumentBinder(Argume } } } - return Optional.of(new ParameterAnnotationBinder<>(conversionService)); + return Optional.of(defaultUnmatchedRequestArgumentBinder); } /** @@ -234,8 +250,7 @@ protected RequestArgumentBinder findBinder(Argument argument, Class, RequestArgumentBinder> byAnnotation) { - DefaultBodyAnnotationBinder bodyBinder = new DefaultBodyAnnotationBinder(conversionService); - byAnnotation.put(Body.class, bodyBinder); + byAnnotation.put(Body.class, new DefaultBodyAnnotationBinder<>(conversionService)); CookieAnnotationBinder cookieAnnotationBinder = new CookieAnnotationBinder<>(conversionService); byAnnotation.put(cookieAnnotationBinder.getAnnotationType(), cookieAnnotationBinder); @@ -243,8 +258,8 @@ protected void registerDefaultAnnotationBinders(Map, HeaderAnnotationBinder headerAnnotationBinder = new HeaderAnnotationBinder<>(conversionService); byAnnotation.put(headerAnnotationBinder.getAnnotationType(), headerAnnotationBinder); - ParameterAnnotationBinder parameterAnnotationBinder = new ParameterAnnotationBinder<>(conversionService); - byAnnotation.put(parameterAnnotationBinder.getAnnotationType(), parameterAnnotationBinder); + QueryValueArgumentBinder queryValueAnnotationBinder = new QueryValueArgumentBinder<>(conversionService); + byAnnotation.put(queryValueAnnotationBinder.getAnnotationType(), queryValueAnnotationBinder); RequestAttributeAnnotationBinder requestAttributeAnnotationBinder = new RequestAttributeAnnotationBinder<>(conversionService); byAnnotation.put(requestAttributeAnnotationBinder.getAnnotationType(), requestAttributeAnnotationBinder); @@ -252,10 +267,10 @@ protected void registerDefaultAnnotationBinders(Map, PathVariableAnnotationBinder pathVariableAnnotationBinder = new PathVariableAnnotationBinder<>(conversionService); byAnnotation.put(pathVariableAnnotationBinder.getAnnotationType(), pathVariableAnnotationBinder); - RequestBeanAnnotationBinder requestBeanAnnotationBinder = new RequestBeanAnnotationBinder<>(this, conversionService); + RequestBeanAnnotationBinder requestBeanAnnotationBinder = new RequestBeanAnnotationBinder<>(this); byAnnotation.put(requestBeanAnnotationBinder.getAnnotationType(), requestBeanAnnotationBinder); - PartAnnotationBinder partAnnotationBinder = new PartAnnotationBinder<>(conversionService); + PartAnnotationBinder partAnnotationBinder = new PartAnnotationBinder<>(); byAnnotation.put(partAnnotationBinder.getAnnotationType(), partAnnotationBinder); if (KOTLIN_COROUTINES_SUPPORTED) { diff --git a/http/src/main/java/io/micronaut/http/bind/RequestBinderRegistry.java b/http/src/main/java/io/micronaut/http/bind/RequestBinderRegistry.java index 1c6a00e763a..91e4865e959 100644 --- a/http/src/main/java/io/micronaut/http/bind/RequestBinderRegistry.java +++ b/http/src/main/java/io/micronaut/http/bind/RequestBinderRegistry.java @@ -17,6 +17,7 @@ import io.micronaut.core.bind.ArgumentBinderRegistry; import io.micronaut.http.HttpRequest; +import io.micronaut.http.bind.binders.RequestArgumentBinder; /** * A {@link ArgumentBinderRegistry} where the source of binding is a {@link HttpRequest}. @@ -26,5 +27,13 @@ */ public interface RequestBinderRegistry extends ArgumentBinderRegistry> { + /** + * Adds a request argument binder that will be used to match the argument that wasn't matched by a type or an annotation. + * @param binder The binder + * @since 4.0.0 + */ + default void addUnmatchedRequestArgumentBinder(RequestArgumentBinder binder) { + throw new UnsupportedOperationException("Binder registry is not mutable"); + } } diff --git a/http/src/main/java/io/micronaut/http/bind/binders/AnnotatedRequestArgumentBinder.java b/http/src/main/java/io/micronaut/http/bind/binders/AnnotatedRequestArgumentBinder.java index cf8dd2f8006..95a3168a733 100644 --- a/http/src/main/java/io/micronaut/http/bind/binders/AnnotatedRequestArgumentBinder.java +++ b/http/src/main/java/io/micronaut/http/bind/binders/AnnotatedRequestArgumentBinder.java @@ -30,7 +30,7 @@ * @param A type * @author Graeme Rocher * @see CookieAnnotationBinder - * @see ParameterAnnotationBinder + * @see DefaultUnmatchedRequestArgumentBinder * @see HeaderAnnotationBinder * @see RequestAttributeAnnotationBinder * @since 1.0 diff --git a/http/src/main/java/io/micronaut/http/bind/binders/CookieAnnotationBinder.java b/http/src/main/java/io/micronaut/http/bind/binders/CookieAnnotationBinder.java index 0f02c97d895..ce8e3cd27a9 100644 --- a/http/src/main/java/io/micronaut/http/bind/binders/CookieAnnotationBinder.java +++ b/http/src/main/java/io/micronaut/http/bind/binders/CookieAnnotationBinder.java @@ -16,7 +16,7 @@ package io.micronaut.http.bind.binders; import io.micronaut.core.annotation.AnnotationMetadata; -import io.micronaut.core.bind.annotation.AbstractAnnotatedArgumentBinder; +import io.micronaut.core.bind.annotation.AbstractArgumentBinder; import io.micronaut.core.convert.ArgumentConversionContext; import io.micronaut.core.convert.ConversionService; import io.micronaut.core.convert.value.ConvertibleValues; @@ -33,7 +33,7 @@ * @author Graeme Rocher * @since 1.0 */ -public class CookieAnnotationBinder extends AbstractAnnotatedArgumentBinder> implements AnnotatedRequestArgumentBinder { +public class CookieAnnotationBinder extends AbstractArgumentBinder implements AnnotatedRequestArgumentBinder { /** * @param conversionService The conversion service @@ -57,7 +57,7 @@ public BindingResult bind(ArgumentConversionContext argument, HttpRequest< } @Override - protected String getFallbackFormat(Argument argument) { + protected String getFallbackFormat(Argument argument) { return NameUtils.hyphenate(argument.getName()); } } diff --git a/http/src/main/java/io/micronaut/http/bind/binders/DefaultBodyAnnotationBinder.java b/http/src/main/java/io/micronaut/http/bind/binders/DefaultBodyAnnotationBinder.java index 4857e3a33a5..36acedf5805 100644 --- a/http/src/main/java/io/micronaut/http/bind/binders/DefaultBodyAnnotationBinder.java +++ b/http/src/main/java/io/micronaut/http/bind/binders/DefaultBodyAnnotationBinder.java @@ -15,16 +15,14 @@ */ package io.micronaut.http.bind.binders; +import io.micronaut.core.bind.annotation.AbstractArgumentBinder; import io.micronaut.core.convert.ArgumentConversionContext; -import io.micronaut.core.convert.ConversionError; import io.micronaut.core.convert.ConversionService; import io.micronaut.core.convert.value.ConvertibleValues; -import io.micronaut.core.naming.NameUtils; +import io.micronaut.http.HttpMethod; import io.micronaut.http.HttpRequest; import io.micronaut.http.annotation.Body; -import java.util.Collections; -import java.util.List; import java.util.Optional; /** @@ -34,7 +32,7 @@ * @author Graeme Rocher * @since 1.0 */ -public class DefaultBodyAnnotationBinder implements BodyArgumentBinder { +public class DefaultBodyAnnotationBinder extends AbstractArgumentBinder implements BodyArgumentBinder, PostponedRequestArgumentBinder { protected final ConversionService conversionService; @@ -42,6 +40,7 @@ public class DefaultBodyAnnotationBinder implements BodyArgumentBinder { * @param conversionService The conversion service */ public DefaultBodyAnnotationBinder(ConversionService conversionService) { + super(conversionService); this.conversionService = conversionService; } @@ -52,49 +51,36 @@ public Class getAnnotationType() { @Override public BindingResult bind(ArgumentConversionContext context, HttpRequest source) { - Optional bodyComponent = context.getAnnotationMetadata().stringValue(Body.class); - if (bodyComponent.isPresent()) { - Optional body = source.getBody(ConvertibleValues.class); - if (body.isPresent()) { - ConvertibleValues values = body.get(); - String component = bodyComponent.get(); - if (!values.contains(component)) { - component = NameUtils.hyphenate(component); - } - - Optional value = values.get(component, context); - return newResult(value.orElse(null), context); - } - //noinspection unchecked - return BindingResult.EMPTY; + if (!HttpMethod.permitsRequestBody(source.getMethod())) { + return BindingResult.unsatisfied(); } + Optional body = source.getBody(); if (body.isEmpty()) { - //noinspection unchecked - return BindingResult.EMPTY; + return BindingResult.empty(); } - Object o = body.get(); - Optional converted = conversionService.convert(o, context); - return newResult(converted.orElse(null), context); - } - - @SuppressWarnings("java:S3655") // false positive - private BindingResult newResult(T converted, ArgumentConversionContext context) { - final Optional lastError = context.getLastError(); - if (lastError.isPresent()) { - return new BindingResult() { - @Override - public Optional getValue() { - return Optional.empty(); - } - - @Override - public List getConversionErrors() { - return Collections.singletonList(lastError.get()); + boolean annotatedAsBody = context.getAnnotationMetadata().hasAnnotation(Body.class); + Optional optionalBodyComponent = context.getAnnotationMetadata().stringValue(Body.class); + String bodyComponent = optionalBodyComponent.orElseGet(() -> { + if (annotatedAsBody) { + return null; + } + return context.getArgument().getName(); + }); + if (bodyComponent != null) { + Optional convertibleValuesBody = source.getBody(ConvertibleValues.class); + if (convertibleValuesBody.isPresent()) { + BindingResult convertibleValuesBindingResult = doBind(context, convertibleValuesBody.get(), bodyComponent); + if (convertibleValuesBindingResult.getValue().isPresent() || !convertibleValuesBindingResult.getConversionErrors().isEmpty()) { + return convertibleValuesBindingResult; } - }; - } else { - return () -> Optional.ofNullable(converted); + } + } + BindingResult bindingResult = doConvert(body.get(), context); + if (!annotatedAsBody && bindingResult.getValue().isEmpty()) { + return BindingResult.empty(); } + return bindingResult; } + } diff --git a/http/src/main/java/io/micronaut/http/bind/binders/DefaultUnmatchedRequestArgumentBinder.java b/http/src/main/java/io/micronaut/http/bind/binders/DefaultUnmatchedRequestArgumentBinder.java new file mode 100644 index 00000000000..fa7d411d97f --- /dev/null +++ b/http/src/main/java/io/micronaut/http/bind/binders/DefaultUnmatchedRequestArgumentBinder.java @@ -0,0 +1,120 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.http.bind.binders; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.convert.ArgumentConversionContext; +import io.micronaut.core.convert.ConversionError; +import io.micronaut.http.HttpRequest; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.stream.Stream; + +/** + * The binder will try to bind the argument value which wasn't matched by an annotation or a type. + * + * @param A type + * @author Denis Stepanov + * @since 4.0.0 + */ +@Internal +public final class DefaultUnmatchedRequestArgumentBinder implements PostponedRequestArgumentBinder, UnmatchedRequestArgumentBinder { + + private final List> internalPreUnmatchedArgumentBinders; + private final List> unmatchedArgumentBinders; + private final List> internalPostUnmatchedArgumentBinders; + + /** + * @param internalPreUnmatchedArgumentBinders The internal pre unmatched binders + * @param unmatchedArgumentBinders The unmatched binders + * @param internalPostUnmatchedArgumentBinders The internal post unmatched binders + */ + public DefaultUnmatchedRequestArgumentBinder(List> internalPreUnmatchedArgumentBinders, + List> unmatchedArgumentBinders, + List> internalPostUnmatchedArgumentBinders) { + this.internalPreUnmatchedArgumentBinders = internalPreUnmatchedArgumentBinders; + this.unmatchedArgumentBinders = unmatchedArgumentBinders; + this.internalPostUnmatchedArgumentBinders = internalPostUnmatchedArgumentBinders; + } + + private Stream> stream() { + return Stream.concat( + internalPreUnmatchedArgumentBinders.stream(), + Stream.concat( + unmatchedArgumentBinders.stream(), + internalPostUnmatchedArgumentBinders.stream() + ) + ); + } + + @Override + public BindingResult bind(ArgumentConversionContext context, HttpRequest request) { + List> pending = new ArrayList<>(); + List errors = new ArrayList<>(); + boolean allUnsatisfied = true; + for (RequestArgumentBinder binder : stream().filter(binder -> !(binder instanceof PostponedRequestArgumentBinder)).toList()) { + BindingResult result = binder.bind((ArgumentConversionContext) context, request); + if (result.isPresentAndSatisfied()) { + return (BindingResult) result; + } else if (result instanceof PendingRequestBindingResult pendingRequestBindingResult) { + pending.add(pendingRequestBindingResult); + allUnsatisfied = false; + } else { + if (result != BindingResult.UNSATISFIED) { + errors.addAll(result.getConversionErrors()); + allUnsatisfied = false; + } + } + } + if (allUnsatisfied) { + return BindingResult.unsatisfied(); + } + return new PendingRequestBindingResult<>() { + + @Override + public boolean isPending() { + return pending.stream().allMatch(PendingRequestBindingResult::isPending); + } + + @Override + public Optional getValue() { + return pending.stream().filter(r -> !r.isPending()).findFirst().flatMap(r -> (Optional) r.getValue()); + } + + @Override + public List getConversionErrors() { + return Stream.concat(errors.stream(), pending.stream().flatMap(r -> r.getConversionErrors().stream())).toList(); + } + }; + } + + @Override + public BindingResult bindPostponed(ArgumentConversionContext context, HttpRequest request) { + BindingResult lastWithError = null; + for (RequestArgumentBinder binder : stream().filter(binder -> (binder instanceof PostponedRequestArgumentBinder)).toList()) { + BindingResult result = binder.bind((ArgumentConversionContext) context, request); + if (result.getValue().isPresent()) { + return (BindingResult) result; + } + if (!result.getConversionErrors().isEmpty()) { + lastWithError = (BindingResult) result; + } + } + return lastWithError == null ? BindingResult.unsatisfied() : lastWithError; + } +} diff --git a/http/src/main/java/io/micronaut/http/bind/binders/HeaderAnnotationBinder.java b/http/src/main/java/io/micronaut/http/bind/binders/HeaderAnnotationBinder.java index 2572c350d97..8f9e02b5a52 100644 --- a/http/src/main/java/io/micronaut/http/bind/binders/HeaderAnnotationBinder.java +++ b/http/src/main/java/io/micronaut/http/bind/binders/HeaderAnnotationBinder.java @@ -16,7 +16,7 @@ package io.micronaut.http.bind.binders; import io.micronaut.core.annotation.AnnotationMetadata; -import io.micronaut.core.bind.annotation.AbstractAnnotatedArgumentBinder; +import io.micronaut.core.bind.annotation.AbstractArgumentBinder; import io.micronaut.core.convert.ArgumentConversionContext; import io.micronaut.core.convert.ConversionService; import io.micronaut.core.convert.value.ConvertibleMultiValues; @@ -34,7 +34,7 @@ * @see io.micronaut.http.HttpHeaders * @since 1.0 */ -public class HeaderAnnotationBinder extends AbstractAnnotatedArgumentBinder> implements AnnotatedRequestArgumentBinder { +public class HeaderAnnotationBinder extends AbstractArgumentBinder implements AnnotatedRequestArgumentBinder { /** * @param conversionService The conversion service @@ -57,7 +57,7 @@ public Class
getAnnotationType() { } @Override - protected String getFallbackFormat(Argument argument) { + protected String getFallbackFormat(Argument argument) { return NameUtils.hyphenate(NameUtils.capitalize(argument.getName()), false); } } diff --git a/http/src/main/java/io/micronaut/http/bind/binders/ParameterAnnotationBinder.java b/http/src/main/java/io/micronaut/http/bind/binders/ParameterAnnotationBinder.java deleted file mode 100644 index 77f9d9e1def..00000000000 --- a/http/src/main/java/io/micronaut/http/bind/binders/ParameterAnnotationBinder.java +++ /dev/null @@ -1,110 +0,0 @@ -/* - * Copyright 2017-2020 original authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.micronaut.http.bind.binders; - -import io.micronaut.core.annotation.AnnotationMetadata; -import io.micronaut.core.bind.annotation.AbstractAnnotatedArgumentBinder; -import io.micronaut.core.convert.ArgumentConversionContext; -import io.micronaut.core.convert.ConversionService; -import io.micronaut.core.convert.value.ConvertibleValues; -import io.micronaut.core.reflect.ClassUtils; -import io.micronaut.core.type.Argument; -import io.micronaut.http.HttpMethod; -import io.micronaut.http.HttpRequest; -import io.micronaut.http.annotation.QueryValue; - -import java.util.Optional; - -/** - * An {@link io.micronaut.core.bind.annotation.AnnotatedArgumentBinder} implementation that uses the {@link QueryValue} - * to trigger binding from an HTTP request parameter. - * - * @param A type - * @author Graeme Rocher - * @since 1.0 - */ -public class ParameterAnnotationBinder extends AbstractAnnotatedArgumentBinder> implements AnnotatedRequestArgumentBinder { - - QueryValueArgumentBinder queryValueArgumentBinder; - - /** - * @param conversionService The conversion service - */ - public ParameterAnnotationBinder(ConversionService conversionService) { - super(conversionService); - this.queryValueArgumentBinder = new QueryValueArgumentBinder<>(conversionService); - } - - @Override - public Class getAnnotationType() { - return QueryValue.class; - } - - @Override - public BindingResult bind(ArgumentConversionContext context, HttpRequest source) { - Argument argument = context.getArgument(); - HttpMethod httpMethod = source.getMethod(); - boolean permitsRequestBody = HttpMethod.permitsRequestBody(httpMethod); - - AnnotationMetadata annotationMetadata = argument.getAnnotationMetadata(); - boolean hasAnnotation = annotationMetadata.hasAnnotation(QueryValue.class); - String parameterName = argument.getName(); - - BindingResult result = queryValueArgumentBinder.bind(context, source); - - Optional val = result.getValue(); - if (!val.isPresent() && !hasAnnotation) { - // attributes are sometimes added by filters, so this should return unsatisfied if not found - // so it can be picked up after the filters are executed - result = doBind(context, source.getAttributes(), parameterName, BindingResult.UNSATISFIED); - } - - Argument argumentType; - if (argument.getType() == Optional.class) { - argumentType = argument.getFirstTypeVariable().orElse(argument); - } else { - argumentType = argument; - } - - // If there is still no value at this point and no annotation is specified and - // the HTTP method allows a request body try and bind from the body - if (!result.getValue().isPresent() && !hasAnnotation && permitsRequestBody) { - Optional body = source.getBody(ConvertibleValues.class); - if (body.isPresent()) { - result = doBind(context, body.get(), parameterName); - if (!result.getValue().isPresent()) { - if (ClassUtils.isJavaLangType(argumentType.getType())) { - return Optional::empty; - } else { - //noinspection unchecked - return () -> (Optional) source.getBody(argumentType); - } - } - } else { - if (source.getBody().isPresent()) { - Optional text = source.getBody(String.class); - if (text.isPresent()) { - return doConvert(text.get(), context); - } - } - //noinspection unchecked - return BindingResult.UNSATISFIED; - - } - } - return result; - } -} diff --git a/http/src/main/java/io/micronaut/http/bind/binders/PartAnnotationBinder.java b/http/src/main/java/io/micronaut/http/bind/binders/PartAnnotationBinder.java index a1528871bd8..7833ad9eda9 100644 --- a/http/src/main/java/io/micronaut/http/bind/binders/PartAnnotationBinder.java +++ b/http/src/main/java/io/micronaut/http/bind/binders/PartAnnotationBinder.java @@ -15,9 +15,7 @@ */ package io.micronaut.http.bind.binders; -import io.micronaut.core.bind.annotation.AbstractAnnotatedArgumentBinder; import io.micronaut.core.convert.ArgumentConversionContext; -import io.micronaut.core.convert.ConversionService; import io.micronaut.http.HttpRequest; import io.micronaut.http.annotation.Part; @@ -28,16 +26,11 @@ * @author James Kleeh * @since 3.6.4 */ -public class PartAnnotationBinder extends AbstractAnnotatedArgumentBinder> implements AnnotatedRequestArgumentBinder { - - public PartAnnotationBinder(ConversionService conversionService) { - super(conversionService); - } +public class PartAnnotationBinder implements AnnotatedRequestArgumentBinder { @Override public BindingResult bind(ArgumentConversionContext context, HttpRequest source) { - //noinspection unchecked - return BindingResult.UNSATISFIED; + return BindingResult.unsatisfied(); } @Override diff --git a/http/src/main/java/io/micronaut/http/bind/binders/PathVariableAnnotationBinder.java b/http/src/main/java/io/micronaut/http/bind/binders/PathVariableAnnotationBinder.java index 0e552930c60..777d952e6c7 100644 --- a/http/src/main/java/io/micronaut/http/bind/binders/PathVariableAnnotationBinder.java +++ b/http/src/main/java/io/micronaut/http/bind/binders/PathVariableAnnotationBinder.java @@ -16,7 +16,7 @@ package io.micronaut.http.bind.binders; import io.micronaut.core.annotation.AnnotationMetadata; -import io.micronaut.core.bind.annotation.AbstractAnnotatedArgumentBinder; +import io.micronaut.core.bind.annotation.AbstractArgumentBinder; import io.micronaut.core.convert.ArgumentConversionContext; import io.micronaut.core.convert.ConversionService; import io.micronaut.core.convert.value.ConvertibleMultiValues; @@ -34,12 +34,12 @@ /** * Used for binding a parameter exclusively from a path variable. * + * @param * @author graemerocher - * @since 1.0.3 * @see PathVariable - * @param + * @since 1.0.3 */ -public class PathVariableAnnotationBinder extends AbstractAnnotatedArgumentBinder> implements AnnotatedRequestArgumentBinder { +public class PathVariableAnnotationBinder extends AbstractArgumentBinder implements AnnotatedRequestArgumentBinder { /** * @param conversionService The conversion service @@ -59,46 +59,33 @@ public BindingResult bind(ArgumentConversionContext context, HttpRequest argument = context.getArgument(); AnnotationMetadata annotationMetadata = argument.getAnnotationMetadata(); - boolean hasAnnotation = annotationMetadata.hasAnnotation(PathVariable.class); String parameterName = annotationMetadata.stringValue(PathVariable.class).orElse(argument.getName()); // If we need to bind all request params to command object // checks if the variable is defined with modifier char * // eg. ?pojo* final Optional matchInfo = source.getAttribute(HttpAttributes.ROUTE_MATCH, UriMatchInfo.class); boolean bindAll = matchInfo - .flatMap(umi -> umi.getVariables() - .stream() - .filter(v -> v.getName().equals(parameterName)) - .findFirst() - .map(UriMatchVariable::isExploded)).orElse(false); + .flatMap(umi -> umi.getVariables() + .stream() + .filter(v -> v.getName().equals(parameterName)) + .findFirst() + .map(UriMatchVariable::isExploded)).orElse(false); - - BindingResult result; - // if the annotation is present or the HTTP method doesn't allow a request body - // attempt to bind from request parameters. This avoids allowing the request URI to - // be manipulated to override POST or JSON variables - if (hasAnnotation && matchInfo.isPresent()) { - final ConvertibleValues variableValues = ConvertibleValues.of(matchInfo.get().getVariableValues(), conversionService); - if (bindAll) { - Object value; - // Only maps and POJOs will "bindAll", lists work like normal - if (Iterable.class.isAssignableFrom(argument.getType())) { - value = doResolve(context, variableValues, parameterName); - if (value == null) { - value = Collections.emptyList(); - } - } else { - value = parameters.asMap(); + final ConvertibleValues variableValues = ConvertibleValues.of(matchInfo.get().getVariableValues(), conversionService); + if (bindAll) { + Object value; + // Only maps and POJOs will "bindAll", lists work like normal + if (Iterable.class.isAssignableFrom(argument.getType())) { + value = doResolve(context, variableValues, parameterName); + if (value == null) { + value = Collections.emptyList(); } - result = doConvert(value, context); } else { - result = doBind(context, variableValues, parameterName); + value = parameters.asMap(); } - } else { - //noinspection unchecked - result = BindingResult.EMPTY; + return doConvert(value, context); } - - return result; + return doBind(context, variableValues, parameterName, BindingResult.unsatisfied()); } + } diff --git a/http/src/main/java/io/micronaut/http/bind/binders/PendingRequestBindingResult.java b/http/src/main/java/io/micronaut/http/bind/binders/PendingRequestBindingResult.java new file mode 100644 index 00000000000..e4d6a4c3ba3 --- /dev/null +++ b/http/src/main/java/io/micronaut/http/bind/binders/PendingRequestBindingResult.java @@ -0,0 +1,51 @@ +/* + * Copyright 2017-2023 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.http.bind.binders; + +import io.micronaut.core.annotation.Experimental; +import io.micronaut.core.bind.ArgumentBinder; + +/** + * A variation of {@link io.micronaut.core.bind.ArgumentBinder.BindingResult} that indicates + * that the binding result is pending and the value should be checked later. + * + * @param The result type + * @author Denis Stepanov + * @since 4.0.0 + */ +@Experimental +public interface PendingRequestBindingResult extends ArgumentBinder.BindingResult { + + /** + * @return True if the result is pending - not ready to be resolved + */ + boolean isPending(); + + /** + * @return Was the binding requirement satisfied + */ + default boolean isSatisfied() { + return !isPending() && ArgumentBinder.BindingResult.super.isSatisfied(); + } + + /** + * @return Is the value present and satisfied + */ + default boolean isPresentAndSatisfied() { + return !isPending() && ArgumentBinder.BindingResult.super.isPresentAndSatisfied(); + } + +} diff --git a/http/src/main/java/io/micronaut/http/bind/binders/PostponedRequestArgumentBinder.java b/http/src/main/java/io/micronaut/http/bind/binders/PostponedRequestArgumentBinder.java new file mode 100644 index 00000000000..89aed07b2b6 --- /dev/null +++ b/http/src/main/java/io/micronaut/http/bind/binders/PostponedRequestArgumentBinder.java @@ -0,0 +1,43 @@ +/* + * Copyright 2017-2023 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.http.bind.binders; + +import io.micronaut.core.bind.ArgumentBinder; +import io.micronaut.core.convert.ArgumentConversionContext; +import io.micronaut.http.HttpRequest; + +import java.util.Optional; + +/** + * Marker interface for {@link RequestArgumentBinder} to indicate that it should bind after filters are applied. + * @param A type + * @author Denis Stepanov + * @since 4.0.0 + */ +public interface PostponedRequestArgumentBinder extends RequestArgumentBinder { + + /** + * Bind postponed the given argument from the given source. + * + * @param context The {@link ArgumentConversionContext} + * @param request The request + * @return An {@link Optional} of the value. If no binding was possible {@link Optional#empty()} + */ + default ArgumentBinder.BindingResult bindPostponed(ArgumentConversionContext context, HttpRequest request) { + return bind(context, request); + } + +} diff --git a/http/src/main/java/io/micronaut/http/bind/binders/QueryValueArgumentBinder.java b/http/src/main/java/io/micronaut/http/bind/binders/QueryValueArgumentBinder.java index 7ee7d6a0add..a263af46649 100644 --- a/http/src/main/java/io/micronaut/http/bind/binders/QueryValueArgumentBinder.java +++ b/http/src/main/java/io/micronaut/http/bind/binders/QueryValueArgumentBinder.java @@ -16,7 +16,7 @@ package io.micronaut.http.bind.binders; import io.micronaut.core.annotation.AnnotationMetadata; -import io.micronaut.core.bind.annotation.AbstractAnnotatedArgumentBinder; +import io.micronaut.core.bind.annotation.AbstractArgumentBinder; import io.micronaut.core.convert.ArgumentConversionContext; import io.micronaut.core.convert.ConversionService; import io.micronaut.core.convert.format.Format; @@ -40,9 +40,7 @@ * @author Andriy Dmytruk * @since 2.0.2 */ -public class QueryValueArgumentBinder - extends AbstractAnnotatedArgumentBinder> - implements AnnotatedRequestArgumentBinder { +public class QueryValueArgumentBinder extends AbstractArgumentBinder implements AnnotatedRequestArgumentBinder { /** * Constructor. @@ -61,7 +59,7 @@ public Class getAnnotationType() { /** * Binds the argument with {@link QueryValue} annotation to the request * (Also binds without annotation if request body is not permitted). - * + *

* It will first try to convert to ConvertibleMultiValues type and if conversion is successful, add the * corresponding parameters to the request. (By default the conversion will be successful if the {@link Format} * annotation is present and has one of the supported values - see @@ -74,54 +72,48 @@ public BindingResult bind(ArgumentConversionContext context, HttpRequest parameters = source.getParameters(); Argument argument = context.getArgument(); AnnotationMetadata annotationMetadata = argument.getAnnotationMetadata(); - boolean hasAnnotation = annotationMetadata.hasAnnotation(QueryValue.class); - HttpMethod httpMethod = source.getMethod(); - boolean permitsRequestBody = HttpMethod.permitsRequestBody(httpMethod); + if (HttpMethod.permitsRequestBody(source.getMethod()) && !annotationMetadata.hasAnnotation(QueryValue.class)) { + // During the unmatched check avoid requests that don't allow bodies + return BindingResult.unsatisfied(); + } - BindingResult result; - if (hasAnnotation || !permitsRequestBody) { - // First try converting from the ConvertibleMultiValues type and if conversion is successful, return it. - // Otherwise use the given uri template to deduce what to do with the variable - Optional multiValueConversion; - if (annotationMetadata.hasAnnotation(Format.class)) { - multiValueConversion = conversionService.convert(parameters, context); - } else { - multiValueConversion = Optional.empty(); - } + // First try converting from the ConvertibleMultiValues type and if conversion is successful, return it. + // Otherwise use the given uri template to deduce what to do with the variable + Optional multiValueConversion; + if (annotationMetadata.hasAnnotation(Format.class)) { + multiValueConversion = conversionService.convert(parameters, context); + } else { + multiValueConversion = Optional.empty(); + } - if (multiValueConversion.isPresent()) { - result = () -> multiValueConversion; - } else { - String parameterName = annotationMetadata.stringValue(QueryValue.class).orElse(argument.getName()); + if (multiValueConversion.isPresent()) { + return () -> multiValueConversion; + } + + String parameterName = annotationMetadata.stringValue(QueryValue.class).orElse(argument.getName()); - // If we need to bind all request params to command object - // checks if the variable is defined with modifier char *, eg. ?pojo* - boolean bindAll = source.getAttribute(HttpAttributes.ROUTE_MATCH, UriMatchInfo.class) - .map(umi -> { - UriMatchVariable uriMatchVariable = umi.getVariableMap().get(parameterName); - return uriMatchVariable != null && uriMatchVariable.isExploded(); - }).orElse(false); + // If we need to bind all request params to command object + // checks if the variable is defined with modifier char *, eg. ?pojo* + boolean bindAll = source.getAttribute(HttpAttributes.ROUTE_MATCH, UriMatchInfo.class) + .map(umi -> { + UriMatchVariable uriMatchVariable = umi.getVariableMap().get(parameterName); + return uriMatchVariable != null && uriMatchVariable.isExploded(); + }).orElse(false); - if (bindAll) { - Object value; - // Only maps and POJOs will "bindAll", lists work like normal - if (Iterable.class.isAssignableFrom(argument.getType())) { - value = doResolve(context, parameters, parameterName); - if (value == null) { - value = Collections.emptyList(); - } - } else { - value = parameters.asMap(); - } - result = doConvert(value, context); - } else { - result = doBind(context, parameters, parameterName); + if (bindAll) { + Object value; + // Only maps and POJOs will "bindAll", lists work like normal + if (Iterable.class.isAssignableFrom(argument.getType())) { + value = doResolve(context, parameters, parameterName); + if (value == null) { + value = Collections.emptyList(); } + } else { + value = parameters.asMap(); } - } else { - result = BindingResult.EMPTY; + return doConvert(value, context); } - return result; + return doBind(context, parameters, parameterName, BindingResult.unsatisfied()); } } diff --git a/http/src/main/java/io/micronaut/http/bind/binders/RequestAttributeAnnotationBinder.java b/http/src/main/java/io/micronaut/http/bind/binders/RequestAttributeAnnotationBinder.java index cc63bac32f4..870110419dc 100644 --- a/http/src/main/java/io/micronaut/http/bind/binders/RequestAttributeAnnotationBinder.java +++ b/http/src/main/java/io/micronaut/http/bind/binders/RequestAttributeAnnotationBinder.java @@ -16,7 +16,7 @@ package io.micronaut.http.bind.binders; import io.micronaut.core.annotation.AnnotationMetadata; -import io.micronaut.core.bind.annotation.AbstractAnnotatedArgumentBinder; +import io.micronaut.core.bind.annotation.AbstractArgumentBinder; import io.micronaut.core.convert.ArgumentConversionContext; import io.micronaut.core.convert.ConversionService; import io.micronaut.core.convert.value.MutableConvertibleValues; @@ -28,12 +28,14 @@ /** * An {@link io.micronaut.core.bind.annotation.AnnotatedArgumentBinder} implementation that uses the {@link RequestAttribute} * annotation to trigger binding from an HTTP request attribute. + * NOTE: The binder is annotates as postponed to allow injecting attributes added by filters. * * @param A type * @author Ahmed Lafta * @see io.micronaut.http.HttpAttributes */ -public class RequestAttributeAnnotationBinder extends AbstractAnnotatedArgumentBinder> implements AnnotatedRequestArgumentBinder { +public class RequestAttributeAnnotationBinder extends AbstractArgumentBinder + implements AnnotatedRequestArgumentBinder, PostponedRequestArgumentBinder { /** * @param conversionService conversionService @@ -52,11 +54,11 @@ public BindingResult bind(ArgumentConversionContext argument, HttpRequest< MutableConvertibleValues parameters = source.getAttributes(); AnnotationMetadata annotationMetadata = argument.getAnnotationMetadata(); String parameterName = annotationMetadata.stringValue(RequestAttribute.class).orElse(argument.getArgument().getName()); - return doBind(argument, parameters, parameterName, BindingResult.UNSATISFIED); + return doBind(argument, parameters, parameterName, BindingResult.unsatisfied()); } @Override - protected String getFallbackFormat(Argument argument) { + protected String getFallbackFormat(Argument argument) { return NameUtils.hyphenate(NameUtils.capitalize(argument.getName()), false); } } diff --git a/http/src/main/java/io/micronaut/http/bind/binders/RequestBeanAnnotationBinder.java b/http/src/main/java/io/micronaut/http/bind/binders/RequestBeanAnnotationBinder.java index d7342c069ca..6a31f660962 100644 --- a/http/src/main/java/io/micronaut/http/bind/binders/RequestBeanAnnotationBinder.java +++ b/http/src/main/java/io/micronaut/http/bind/binders/RequestBeanAnnotationBinder.java @@ -15,19 +15,14 @@ */ package io.micronaut.http.bind.binders; -import java.util.*; -import java.util.stream.Collectors; - import io.micronaut.core.annotation.AnnotationMetadata; import io.micronaut.core.beans.BeanIntrospection; import io.micronaut.core.beans.BeanProperty; import io.micronaut.core.bind.ArgumentBinder; -import io.micronaut.core.bind.annotation.AbstractAnnotatedArgumentBinder; import io.micronaut.core.bind.exceptions.UnsatisfiedArgumentException; import io.micronaut.core.convert.ArgumentConversionContext; import io.micronaut.core.convert.ConversionContext; import io.micronaut.core.convert.ConversionError; -import io.micronaut.core.convert.ConversionService; import io.micronaut.core.convert.exceptions.ConversionErrorException; import io.micronaut.core.naming.Named; import io.micronaut.core.type.Argument; @@ -35,26 +30,30 @@ import io.micronaut.http.annotation.RequestBean; import io.micronaut.http.bind.RequestBinderRegistry; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; + /** * Used to bind Bindable parameters to a Bean object. - * + * NOTE: The binder is annotates as postponed to allow injecting values added by filters. + * @author Anze Sodja * @author graemerocher * @since 2.0 * @see RequestBean * @param */ -public class RequestBeanAnnotationBinder extends AbstractAnnotatedArgumentBinder> - implements AnnotatedRequestArgumentBinder { +public class RequestBeanAnnotationBinder implements AnnotatedRequestArgumentBinder, PostponedRequestArgumentBinder { private final RequestBinderRegistry requestBinderRegistry; /** * @param requestBinderRegistry Original request binder registry - * @param conversionService The conversion service */ - public RequestBeanAnnotationBinder(RequestBinderRegistry requestBinderRegistry, ConversionService conversionService) { - super(conversionService); + public RequestBeanAnnotationBinder(RequestBinderRegistry requestBinderRegistry) { this.requestBinderRegistry = requestBinderRegistry; } @@ -121,8 +120,8 @@ private Optional getBindableResult(HttpRequest source, Argument getBindableResult(ArgumentConversionContext conversionContext, HttpRequest source) { Argument argument = conversionContext.getArgument(); - Optional>> binder = requestBinderRegistry.findArgumentBinder(argument, source); - if (!binder.isPresent()) { + Optional>> binder = requestBinderRegistry.findArgumentBinder(argument); + if (binder.isEmpty()) { throw new UnsatisfiedArgumentException(argument); } BindingResult result = binder.get().bind(conversionContext, source); diff --git a/http/src/main/java/io/micronaut/http/bind/binders/TypedRequestArgumentBinder.java b/http/src/main/java/io/micronaut/http/bind/binders/TypedRequestArgumentBinder.java index b4e5241bd82..7c927b47182 100644 --- a/http/src/main/java/io/micronaut/http/bind/binders/TypedRequestArgumentBinder.java +++ b/http/src/main/java/io/micronaut/http/bind/binders/TypedRequestArgumentBinder.java @@ -39,4 +39,21 @@ public interface TypedRequestArgumentBinder extends RequestArgumentBinder, default @NonNull List> superTypes() { return Collections.emptyList(); } + + /** + * Check if this typed argument binder matches the provided class. + * @param aClass The class to match + * @return true if matches + */ + default boolean matches(Class aClass) { + if (aClass.equals(argumentType().getType())) { + return true; + } + for (Class superType : superTypes()) { + if (aClass.equals(superType)) { + return true; + } + } + return false; + } } diff --git a/router/src/main/java/io/micronaut/web/router/UnresolvedArgument.java b/http/src/main/java/io/micronaut/http/bind/binders/UnmatchedRequestArgumentBinder.java similarity index 57% rename from router/src/main/java/io/micronaut/web/router/UnresolvedArgument.java rename to http/src/main/java/io/micronaut/http/bind/binders/UnmatchedRequestArgumentBinder.java index 33c65db92c3..6655daf02e1 100644 --- a/router/src/main/java/io/micronaut/web/router/UnresolvedArgument.java +++ b/http/src/main/java/io/micronaut/http/bind/binders/UnmatchedRequestArgumentBinder.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2020 original authors + * Copyright 2017-2023 original authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,19 +13,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.micronaut.web.router; - -import io.micronaut.core.bind.ArgumentBinder; - -import java.util.function.Supplier; +package io.micronaut.http.bind.binders; /** - * Represents an unresolved argument to a {@link io.micronaut.web.router.Route}. - * - * @param The Type - * @author Graeme Rocher - * @since 1.0 + * Marker interface for unmatched request argument binder. + * @author Denis Stepanov + * @since 4.0.0 */ -@FunctionalInterface -public interface UnresolvedArgument extends Supplier> { +public interface UnmatchedRequestArgumentBinder { } diff --git a/http/src/main/java/io/micronaut/http/reactive/execution/ReactorExecutionFlowImpl.java b/http/src/main/java/io/micronaut/http/reactive/execution/ReactorExecutionFlowImpl.java index 7ece13d7075..6b21a8283bb 100644 --- a/http/src/main/java/io/micronaut/http/reactive/execution/ReactorExecutionFlowImpl.java +++ b/http/src/main/java/io/micronaut/http/reactive/execution/ReactorExecutionFlowImpl.java @@ -27,7 +27,6 @@ import java.util.Map; import java.util.concurrent.CompletableFuture; -import java.util.concurrent.atomic.AtomicReference; import java.util.function.BiConsumer; import java.util.function.Function; import java.util.function.Supplier; @@ -86,7 +85,7 @@ public void onComplete(BiConsumer fn) { value.subscribe(new Subscriber<>() { Subscription subscription; - final AtomicReference value = new AtomicReference<>(); + Object value; @Override public void onSubscribe(Subscription s) { @@ -96,8 +95,8 @@ public void onSubscribe(Subscription s) { @Override public void onNext(Object v) { + value = v; subscription.request(1); // ??? - value.set(v); } @Override @@ -107,7 +106,7 @@ public void onError(Throwable t) { @Override public void onComplete() { - fn.accept(value.get(), null); + fn.accept(value, null); } }); } diff --git a/inject/src/main/java/io/micronaut/context/DefaultBeanContext.java b/inject/src/main/java/io/micronaut/context/DefaultBeanContext.java index 10088129c8c..e7ddc3e2159 100644 --- a/inject/src/main/java/io/micronaut/context/DefaultBeanContext.java +++ b/inject/src/main/java/io/micronaut/context/DefaultBeanContext.java @@ -651,8 +651,8 @@ public Object invoke(Object... arguments) { @NonNull @Override - public ExecutableMethod getExecutableMethod() { - return (ExecutableMethod) method; + public ExecutableMethod getExecutableMethod() { + return (ExecutableMethod) method; } }; } @@ -3785,7 +3785,7 @@ private abstract static class AbstractExecutionHandle implements MethodExe @NonNull @Override - public ExecutableMethod getExecutableMethod() { + public ExecutableMethod getExecutableMethod() { return method; } diff --git a/inject/src/main/java/io/micronaut/inject/ExecutionHandle.java b/inject/src/main/java/io/micronaut/inject/ExecutionHandle.java index bf1776287df..245ea970f2c 100644 --- a/inject/src/main/java/io/micronaut/inject/ExecutionHandle.java +++ b/inject/src/main/java/io/micronaut/inject/ExecutionHandle.java @@ -72,10 +72,10 @@ public interface ExecutionHandle extends AnnotationMetadataDelegate { * @return The execution handle */ static MethodExecutionHandle of(T2 bean, ExecutableMethod method) { - return new MethodExecutionHandle() { + return new MethodExecutionHandle<>() { @NonNull @Override - public ExecutableMethod getExecutableMethod() { + public ExecutableMethod getExecutableMethod() { return method; } diff --git a/inject/src/main/java/io/micronaut/inject/MethodExecutionHandle.java b/inject/src/main/java/io/micronaut/inject/MethodExecutionHandle.java index 24682f2f380..643c415a697 100644 --- a/inject/src/main/java/io/micronaut/inject/MethodExecutionHandle.java +++ b/inject/src/main/java/io/micronaut/inject/MethodExecutionHandle.java @@ -32,5 +32,5 @@ public interface MethodExecutionHandle extends ExecutionHandle, Meth * @return The underlying method reference. */ @NonNull - ExecutableMethod getExecutableMethod(); + ExecutableMethod getExecutableMethod(); } diff --git a/management/src/main/java/io/micronaut/management/endpoint/routes/RouteData.java b/management/src/main/java/io/micronaut/management/endpoint/routes/RouteData.java index 4fdae7b20cc..bdd294d8d8f 100644 --- a/management/src/main/java/io/micronaut/management/endpoint/routes/RouteData.java +++ b/management/src/main/java/io/micronaut/management/endpoint/routes/RouteData.java @@ -15,7 +15,7 @@ */ package io.micronaut.management.endpoint.routes; -import io.micronaut.web.router.UriRoute; +import io.micronaut.web.router.UriRouteInfo; /** *

Returns data for a given route to be used for the {@link RoutesEndpoint}.

@@ -27,8 +27,8 @@ public interface RouteData { /** - * @param route The route + * @param routeInfo The route info * @return Route data */ - T getData(UriRoute route); + T getData(UriRouteInfo routeInfo); } diff --git a/management/src/main/java/io/micronaut/management/endpoint/routes/RouteDataCollector.java b/management/src/main/java/io/micronaut/management/endpoint/routes/RouteDataCollector.java index 62a7d83668f..f13c21f87a2 100644 --- a/management/src/main/java/io/micronaut/management/endpoint/routes/RouteDataCollector.java +++ b/management/src/main/java/io/micronaut/management/endpoint/routes/RouteDataCollector.java @@ -15,7 +15,8 @@ */ package io.micronaut.management.endpoint.routes; -import io.micronaut.web.router.UriRoute; +import io.micronaut.web.router.UriRouteInfo; + import java.util.stream.Stream; /** @@ -28,9 +29,9 @@ public interface RouteDataCollector { /** - * @param routes A java stream of uri routes + * @param routes A java stream of uri route infos * @return A publisher that returns data representing all of * the given routes. */ - T getData(Stream routes); + T getData(Stream> routes); } diff --git a/management/src/main/java/io/micronaut/management/endpoint/routes/RoutesEndpoint.java b/management/src/main/java/io/micronaut/management/endpoint/routes/RoutesEndpoint.java index 2a7da46e282..899de8f66c2 100644 --- a/management/src/main/java/io/micronaut/management/endpoint/routes/RoutesEndpoint.java +++ b/management/src/main/java/io/micronaut/management/endpoint/routes/RoutesEndpoint.java @@ -19,7 +19,7 @@ import io.micronaut.management.endpoint.annotation.Endpoint; import io.micronaut.management.endpoint.annotation.Read; import io.micronaut.web.router.Router; -import io.micronaut.web.router.UriRoute; +import io.micronaut.web.router.UriRouteInfo; import java.util.Comparator; import java.util.stream.Stream; @@ -52,10 +52,8 @@ public RoutesEndpoint(Router router, RouteDataCollector routeDataCollect @Read @SingleResult public Object getRoutes() { - Stream uriRoutes = router.uriRoutes() - .sorted(Comparator - .comparing((UriRoute r) -> r.getUriMatchTemplate().toPathString()) - .thenComparing(UriRoute::getHttpMethodName)); + Stream> uriRoutes = router.uriRoutes() + .sorted(Comparator.comparing((UriRouteInfo r) -> r.getUriMatchTemplate().toPathString()).thenComparing(UriRouteInfo::getHttpMethodName)); return routeDataCollector.getData(uriRoutes); } } diff --git a/management/src/main/java/io/micronaut/management/endpoint/routes/impl/DefaultRouteData.java b/management/src/main/java/io/micronaut/management/endpoint/routes/impl/DefaultRouteData.java index ca9b4268cdf..6e541f7a5ba 100644 --- a/management/src/main/java/io/micronaut/management/endpoint/routes/impl/DefaultRouteData.java +++ b/management/src/main/java/io/micronaut/management/endpoint/routes/impl/DefaultRouteData.java @@ -19,12 +19,11 @@ import io.micronaut.inject.MethodExecutionHandle; import io.micronaut.management.endpoint.routes.RouteData; import io.micronaut.management.endpoint.routes.RoutesEndpoint; -import io.micronaut.web.router.MethodBasedRoute; -import io.micronaut.web.router.UriRoute; +import io.micronaut.web.router.UriRouteInfo; import jakarta.inject.Singleton; import java.util.Arrays; -import java.util.LinkedHashMap; +import java.util.Collections; import java.util.Map; import java.util.stream.Collectors; @@ -39,21 +38,15 @@ public class DefaultRouteData implements RouteData> { @Override - public Map getData(UriRoute route) { - Map values = new LinkedHashMap<>(1); - - if (route instanceof MethodBasedRoute) { - values.put("method", getMethodString(((MethodBasedRoute) route).getTargetMethod())); - } - - return values; + public Map getData(UriRouteInfo routeInfo) { + return Collections.singletonMap("method", getMethodString(routeInfo.getTargetMethod())); } /** * @param targetMethod The {@link MethodExecutionHandle} * @return A String with the target method */ - protected String getMethodString(MethodExecutionHandle targetMethod) { + protected String getMethodString(MethodExecutionHandle targetMethod) { return new StringBuilder() .append(targetMethod.getReturnType().asArgument().getTypeString(false)) .append(" ") diff --git a/management/src/main/java/io/micronaut/management/endpoint/routes/impl/DefaultRouteDataCollector.java b/management/src/main/java/io/micronaut/management/endpoint/routes/impl/DefaultRouteDataCollector.java index 1f438c994d7..532abf418e0 100644 --- a/management/src/main/java/io/micronaut/management/endpoint/routes/impl/DefaultRouteDataCollector.java +++ b/management/src/main/java/io/micronaut/management/endpoint/routes/impl/DefaultRouteDataCollector.java @@ -20,7 +20,7 @@ import io.micronaut.management.endpoint.routes.RouteData; import io.micronaut.management.endpoint.routes.RouteDataCollector; import io.micronaut.management.endpoint.routes.RoutesEndpoint; -import io.micronaut.web.router.UriRoute; +import io.micronaut.web.router.UriRouteInfo; import jakarta.inject.Singleton; import java.util.LinkedHashMap; @@ -48,7 +48,7 @@ public DefaultRouteDataCollector(RouteData routeData) { } @Override - public Map getData(Stream routes) { + public Map getData(Stream> routes) { return routes .collect(Collectors.toMap( this::getRouteKey, @@ -62,7 +62,7 @@ public Map getData(Stream routes) { * @param route The URI route * @return The route key */ - protected String getRouteKey(UriRoute route) { + protected String getRouteKey(UriRouteInfo route) { String produces = route .getProduces() .stream() diff --git a/router/src/main/java/io/micronaut/web/router/AbstractRouteMatch.java b/router/src/main/java/io/micronaut/web/router/AbstractRouteMatch.java index 1138f3e8370..8fac0fa976c 100644 --- a/router/src/main/java/io/micronaut/web/router/AbstractRouteMatch.java +++ b/router/src/main/java/io/micronaut/web/router/AbstractRouteMatch.java @@ -15,8 +15,8 @@ */ package io.micronaut.web.router; -import io.micronaut.core.annotation.Nullable; import io.micronaut.core.annotation.AnnotationMetadata; +import io.micronaut.core.annotation.NonNull; import io.micronaut.core.bind.ArgumentBinder; import io.micronaut.core.convert.ArgumentConversionContext; import io.micronaut.core.convert.ConversionContext; @@ -25,19 +25,22 @@ import io.micronaut.core.convert.exceptions.ConversionErrorException; import io.micronaut.core.type.Argument; import io.micronaut.core.type.ReturnType; -import io.micronaut.core.util.CollectionUtils; import io.micronaut.http.HttpRequest; -import io.micronaut.http.HttpStatus; -import io.micronaut.http.MediaType; +import io.micronaut.http.bind.RequestBinderRegistry; +import io.micronaut.http.bind.binders.PendingRequestBindingResult; +import io.micronaut.http.bind.binders.PostponedRequestArgumentBinder; +import io.micronaut.http.bind.binders.RequestArgumentBinder; +import io.micronaut.http.bind.binders.UnmatchedRequestArgumentBinder; import io.micronaut.inject.ExecutableMethod; import io.micronaut.inject.MethodExecutionHandle; import io.micronaut.web.router.exceptions.UnsatisfiedRouteException; -import io.micronaut.core.annotation.NonNull; - import java.lang.reflect.Method; -import java.util.*; -import java.util.function.Predicate; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Optional; /** * Abstract implementation of the {@link RouteMatch} interface. @@ -45,80 +48,68 @@ * @param The target type * @param Route Match * @author Graeme Rocher + * @author Denis Stepanov * @since 1.0 */ abstract class AbstractRouteMatch implements MethodBasedRouteMatch { - protected final MethodExecutionHandle executableMethod; protected final ConversionService conversionService; - protected final DefaultRouteBuilder.AbstractRoute abstractRoute; - protected final List consumedMediaTypes; - protected final List producedMediaTypes; + protected final MethodBasedRouteInfo routeInfo; + protected final MethodExecutionHandle methodExecutionHandle; + protected final ExecutableMethod executableMethod; + + private final Argument[] arguments; + private final String[] argumentNames; + private final Object[] argumentValues; + private final PostponedRequestArgumentBinder[] postponedArgumentBinders; + private final PendingRequestBindingResult[] pendingRequestBindingResults; + private final boolean[] fulfilledArguments; + private boolean fulfilled; + private boolean beforeBindersApplied; + private boolean afterBindersApplied; /** * Constructor. * - * @param abstractRoute The abstract route builder + * @param routeInfo The route info * @param conversionService The conversion service */ - protected AbstractRouteMatch(DefaultRouteBuilder.AbstractRoute abstractRoute, ConversionService conversionService) { - this.abstractRoute = abstractRoute; - //noinspection unchecked - this.executableMethod = (MethodExecutionHandle) abstractRoute.targetMethod; + protected AbstractRouteMatch(MethodBasedRouteInfo routeInfo, ConversionService conversionService) { + this.routeInfo = routeInfo; this.conversionService = conversionService; - this.consumedMediaTypes = abstractRoute.getConsumes(); - this.producedMediaTypes = abstractRoute.getProduces(); - } - - @Override - public final boolean isSuspended() { - return this.abstractRoute.isSuspended(); - } - - @Override - public final boolean isReactive() { - return this.abstractRoute.isReactive(); - } - - @Override - public final boolean isSingleResult() { - return this.abstractRoute.isSingleResult(); - } - - @Override - public final boolean isSpecifiedSingle() { - return this.abstractRoute.isSpecifiedSingle(); - } - - @Override - public final boolean isAsync() { - return this.abstractRoute.isAsync(); - } - - @Override - public final boolean isVoid() { - return this.abstractRoute.isVoid(); + this.methodExecutionHandle = routeInfo.getTargetMethod(); + this.executableMethod = methodExecutionHandle.getExecutableMethod(); + this.arguments = executableMethod.getArguments(); + this.argumentNames = routeInfo.getArgumentNames(); + int length = arguments.length; + if (length == 0) { + fulfilled = true; + this.argumentValues = null; + this.fulfilledArguments = null; + this.postponedArgumentBinders = null; + this.pendingRequestBindingResults = null; + } else { + this.argumentValues = new Object[length]; + this.fulfilledArguments = new boolean[length]; + this.postponedArgumentBinders = new PostponedRequestArgumentBinder[length]; + this.pendingRequestBindingResults = new PendingRequestBindingResult[length]; + } } @Override - public boolean isAsyncOrReactive() { - return abstractRoute.isAsyncOrReactive(); + public RouteInfo getRouteInfo() { + return routeInfo; } @Override public T getTarget() { - return executableMethod.getTarget(); + return routeInfo.getTargetMethod().getTarget(); } @NonNull @Override - public ExecutableMethod getExecutableMethod() { - return executableMethod.getExecutableMethod(); - } - - @Override - public List getProduces() { - return abstractRoute.getProduces(); + public ExecutableMethod getExecutableMethod() { + return executableMethod; } @Override @@ -127,53 +118,56 @@ public AnnotationMetadata getAnnotationMetadata() { } @Override - public Optional> getBodyArgument() { - Argument arg = abstractRoute.bodyArgument; - if (arg != null) { - return Optional.of(arg); - } - String bodyArgument = abstractRoute.bodyArgumentName; - if (bodyArgument != null) { - return Optional.ofNullable(abstractRoute.requiredInputs.get(bodyArgument)); + public Optional> getRequiredInput(String name) { + for (int i = 0; i < argumentNames.length; i++) { + String argumentName = argumentNames[i]; + if (name.equals(argumentName)) { + return Optional.of(arguments[i]); + } } return Optional.empty(); } @Override - public boolean isRequiredInput(String name) { - return abstractRoute.requiredInputs.containsKey(name); - } - - @Override - public Optional> getRequiredInput(String name) { - return Optional.ofNullable(abstractRoute.requiredInputs.get(name)); + public boolean isFulfilled() { + if (fulfilled) { + return true; + } + for (int i = 0; i < arguments.length; i++) { + boolean isFulfilled = fulfilledArguments[i]; + if (isFulfilled) { + continue; + } + PendingRequestBindingResult pendingRequestBindingResult = pendingRequestBindingResults[i]; + if (pendingRequestBindingResult != null && !pendingRequestBindingResult.isPending()) { + Argument argument = arguments[i]; + setBindingResult(i, argument, pendingRequestBindingResult); + failOnConversionErrors(argument, pendingRequestBindingResult); + } + } + checkIfFulfilled(); + return fulfilled; } @Override - public boolean isExecutable() { - Map variables = getVariableValues(); - for (Map.Entry> entry : abstractRoute.requiredInputs.entrySet()) { - Object value = variables.get(entry.getKey()); - if (value == null || value instanceof UnresolvedArgument) { - return false; + public boolean isSatisfied(String name) { + for (int i = 0; i < argumentNames.length; i++) { + String argumentName = argumentNames[i]; + if (name.equals(argumentName)) { + return fulfilledArguments[i]; } } - Optional> bodyArgument = getBodyArgument(); - if (bodyArgument.isPresent()) { - Object value = variables.get(bodyArgument.get().getName()); - return value != null && !(value instanceof UnresolvedArgument); - } - return true; + return false; } @Override public Method getTargetMethod() { - return executableMethod.getTargetMethod(); + return routeInfo.getTargetMethod().getTargetMethod(); } @Override public String getMethodName() { - return this.executableMethod.getMethodName(); + return executableMethod.getMethodName(); } @Override @@ -186,16 +180,6 @@ public Argument[] getArguments() { return executableMethod.getArguments(); } - @Override - public boolean test(HttpRequest request) { - for (Predicate> condition : abstractRoute.conditions) { - if (!condition.test(request)) { - return false; - } - } - return true; - } - @Override public ReturnType getReturnType() { return executableMethod.getReturnType(); @@ -205,7 +189,7 @@ public ReturnType getReturnType() { public R invoke(Object... arguments) { Argument[] targetArguments = getArguments(); if (targetArguments.length == 0) { - return executableMethod.invoke(); + return methodExecutionHandle.invoke(); } else { List argumentList = new ArrayList<>(arguments.length); Map variables = getVariableValues(); @@ -227,184 +211,260 @@ public R invoke(Object... arguments) { throw new IllegalArgumentException("Wrong number of arguments to method: " + executableMethod); } } - return executableMethod.invoke(argumentList.toArray()); + return methodExecutionHandle.invoke(argumentList.toArray()); } } @Override - public R execute(Map argumentValues) { + public R execute() { Argument[] targetArguments = getArguments(); if (targetArguments.length == 0) { - return executableMethod.invoke(); - } - Map uriVariables = getVariableValues(); - List argumentList = new ArrayList<>(argumentValues.size()); - - for (Map.Entry> entry : abstractRoute.requiredInputs.entrySet()) { - Argument argument = entry.getValue(); - String name = entry.getKey(); - Object value = DefaultRouteBuilder.NO_VALUE; - if (uriVariables.containsKey(name)) { - value = uriVariables.get(name); - } else if (argumentValues.containsKey(name)) { - value = argumentValues.get(name); + return methodExecutionHandle.invoke(); + } + if (fulfilled) { + return methodExecutionHandle.invoke(argumentValues); + } + if (!beforeBindersApplied) { + throw new IllegalStateException("Argument binders before filters not processed!"); + } + if (!afterBindersApplied) { + throw new IllegalStateException("Argument binders after filters not processed!"); + } + for (int i = 0; i < arguments.length; i++) { + if (fulfilledArguments[i]) { + continue; } - - Class argumentType = argument.getType(); - if (value instanceof UnresolvedArgument unresolved) { - ArgumentBinder.BindingResult bindingResult = unresolved.get(); - if (bindingResult.isPresentAndSatisfied()) { - Object resolved = bindingResult.get(); - if (resolved instanceof ConversionError conversionError) { - throw new ConversionErrorException(argument, conversionError); - } else { - convertValueAndAddToList(conversionService, argumentList, argument, resolved, argumentType); - } - } else { - if (argument.isNullable()) { - argumentList.add(null); - } else { - List conversionErrors = bindingResult.getConversionErrors(); - if (!conversionErrors.isEmpty()) { - // should support multiple errors - ConversionError conversionError = conversionErrors.iterator().next(); - throw new ConversionErrorException(argument, conversionError); - } - throw UnsatisfiedRouteException.create(argument); - } - } - } else if (value instanceof NullArgument) { - argumentList.add(null); - } else if (value instanceof ConversionError conversionError) { - throw new ConversionErrorException(argument, conversionError); - } else if (value == DefaultRouteBuilder.NO_VALUE) { + PendingRequestBindingResult pendingRequestBindingResult = pendingRequestBindingResults[i]; + Argument argument = arguments[i]; + if (pendingRequestBindingResult != null) { + setBindingResultOfFail(i, argument, pendingRequestBindingResult); + continue; + } + Object value = getVariableValues().get(argumentNames[i]); + if (value != null) { + setValue(i, argument, value); + continue; + } + if (argument.isOptional()) { + setValue(i, argument, Optional.empty()); + continue; + } + if (!argument.isNullable()) { throw UnsatisfiedRouteException.create(argument); - } else { - convertValueAndAddToList(conversionService, argumentList, argument, value, argumentType); } } + return methodExecutionHandle.invoke(argumentValues); + } - return executableMethod.invoke(argumentList.toArray()); + @Override + public void fulfill(Map newValues) { + if (fulfilled) { + return; + } + for (int i = 0; i < argumentNames.length; i++) { + if (fulfilledArguments[i]) { + continue; + } + String argumentName = argumentNames[i]; + Object value = newValues.get(argumentName); + if (value != null) { + setValue(i, arguments[i], value); + } + } + checkIfFulfilled(); } - private void convertValueAndAddToList(ConversionService conversionService, List argumentList, Argument argument, Object value, Class argumentType) { - if (argumentType.isInstance(value)) { - if (argument.isContainerType()) { - if (argument.hasTypeVariables()) { - ConversionContext conversionContext = ConversionContext.of(argument); - Optional result = conversionService.convert(value, argumentType, conversionContext); - argumentList.add(resolveValueOrError(argument, conversionContext, result)); - } else { - argumentList.add(value); + @Override + public void fulfillBeforeFilters(RequestBinderRegistry requestBinderRegistry, HttpRequest request) { + if (fulfilled) { + return; + } + if (beforeBindersApplied) { + throw new IllegalStateException("Argument before filters already processed!"); + } + RequestArgumentBinder[] argumentBinders = routeInfo.resolveArgumentBinders(requestBinderRegistry); + for (int i = 0; i < arguments.length; i++) { + if (fulfilledArguments[i]) { + continue; + } + Argument argument = (Argument) arguments[i]; + Object value = getVariableValues().get(argumentNames[i]); + if (value != null) { + setValue(i, argument, value); + continue; + } + RequestArgumentBinder argumentBinder = argumentBinders[i]; + if (argumentBinder instanceof PostponedRequestArgumentBinder postponedRequestArgumentBinder) { + postponedArgumentBinders[i] = postponedRequestArgumentBinder; + if (!(argumentBinder instanceof UnmatchedRequestArgumentBinder)) { + // Allow for the unmatched request argument binder to run even so it's postponed + continue; } - } else { - argumentList.add(value); } - } else { - ConversionContext conversionContext = ConversionContext.of(argument); - Optional result = conversionService.convert(value, argumentType, conversionContext); - argumentList.add(resolveValueOrError(argument, conversionContext, result)); + if (argumentBinder != null) { + fulfillValue( + i, + argumentBinder, + argument, + request + ); + } } + checkIfFulfilled(); + beforeBindersApplied = true; } @Override - public boolean doesConsume(MediaType contentType) { - return contentType == null || abstractRoute.consumesMediaTypesContainsAll || explicitlyConsumes(contentType); + public void fulfillAfterFilters(RequestBinderRegistry requestBinderRegistry, HttpRequest request) { + if (fulfilled) { + return; + } + if (afterBindersApplied) { + throw new IllegalStateException("Argument binders after filters already processed!"); + } + for (int i = 0; i < arguments.length; i++) { + if (fulfilledArguments[i]) { + continue; + } + Argument argument = (Argument) arguments[i]; + PostponedRequestArgumentBinder argumentBinder = postponedArgumentBinders[i]; + if (argumentBinder != null) { + fulfillValuePostponed( + i, + argumentBinder, + argument, + request + ); + } + } + checkIfFulfilled(); + afterBindersApplied = true; } - @Override - public boolean doesProduce(@Nullable Collection acceptableTypes) { - return abstractRoute.producesMediaTypesContainsAll || anyMediaTypesMatch(producedMediaTypes, acceptableTypes); + private void fulfillValue(int index, + RequestArgumentBinder argumentBinder, + Argument argument, + HttpRequest request) { + ArgumentConversionContext conversionContext = newContext(argument, request); + ArgumentBinder.BindingResult bindingResult = argumentBinder.bind(conversionContext, request); + fulfillValue(index, argument, bindingResult); } - @Override - public boolean doesProduce(@Nullable MediaType acceptableType) { - return abstractRoute.producesMediaTypesContainsAll || acceptableType == null || acceptableType.equals(MediaType.ALL_TYPE) || producedMediaTypes.contains(acceptableType); + private void fulfillValuePostponed(int index, + PostponedRequestArgumentBinder argumentBinder, + Argument argument, + HttpRequest request) { + ArgumentConversionContext conversionContext = newContext(argument, request); + ArgumentBinder.BindingResult bindingResult = argumentBinder.bindPostponed(conversionContext, request); + fulfillValue(index, argument, bindingResult); } - private boolean anyMediaTypesMatch(List producedMediaTypes, Collection acceptableTypes) { - if (CollectionUtils.isEmpty(acceptableTypes)) { - return true; - } - for (MediaType acceptableType : acceptableTypes) { - if (acceptableType.equals(MediaType.ALL_TYPE) || producedMediaTypes.contains(acceptableType)) { - return true; - } + private ArgumentConversionContext newContext(Argument argument, HttpRequest request) { + ArgumentConversionContext conversionContext = ConversionContext.of( + argument, + request.getLocale().orElse(null), + request.getCharacterEncoding() + ); + return conversionContext; + } + + private void fulfillValue(int index, Argument argument, ArgumentBinder.BindingResult bindingResult) { + if (bindingResult instanceof PendingRequestBindingResult pendingRequestBindingResult) { + pendingRequestBindingResults[index] = pendingRequestBindingResult; + return; } - return false; + failOnConversionErrors(argument, bindingResult); + setBindingResult(index, argument, bindingResult); } - @Override - public boolean explicitlyConsumes(MediaType contentType) { - return consumedMediaTypes.contains(contentType); + private void setBindingResultOfFail(int index, Argument argument, ArgumentBinder.BindingResult bindingResult) { + boolean isSet = setBindingResult(index, argument, bindingResult); + failOnConversionErrors(argument, bindingResult); + if (isSet) { + return; + } + if (argument.isNullable()) { + setValue(index, argument, null); + return; + } + if (argument.isOptional()) { + setValue(index, argument, Optional.empty()); + return; + } + throw UnsatisfiedRouteException.create(argument); } - @Override - public boolean explicitlyProduces(MediaType contentType) { - return producedMediaTypes == null || producedMediaTypes.isEmpty() || producedMediaTypes.contains(contentType); + private void failOnConversionErrors(Argument argument, ArgumentBinder.BindingResult bindingResult) { + List conversionErrors = bindingResult.getConversionErrors(); + if (!conversionErrors.isEmpty()) { + // should support multiple errors + ConversionError conversionError = conversionErrors.iterator().next(); + throw new ConversionErrorException(argument, conversionError); + } } - @Override - public RouteMatch fulfill(Map argumentValues) { - if (CollectionUtils.isEmpty(argumentValues)) { - return this; - } - Map oldVariables = getVariableValues(); - Map newVariables = new LinkedHashMap<>(oldVariables); - final Argument bodyArgument = getBodyArgument().orElse(null); - Collection> requiredArguments = getRequiredArguments(); - boolean hasRequiredArguments = CollectionUtils.isNotEmpty(requiredArguments); - requiredArguments = hasRequiredArguments ? new ArrayList<>(requiredArguments) : requiredArguments; - for (Argument requiredArgument : getArguments()) { - String argumentName = requiredArgument.getName(); - if (argumentValues.containsKey(argumentName)) { - Object value = argumentValues.get(argumentName); - if (bodyArgument != null && bodyArgument.getName().equals(argumentName)) { - requiredArgument = bodyArgument; - } - if (hasRequiredArguments) { - requiredArguments.remove(requiredArgument); - } - if (value != null) { - String name = abstractRoute.resolveInputName(requiredArgument); - if (value instanceof UnresolvedArgument || value instanceof NullArgument) { - newVariables.put(name, value); - } else { - Class type = requiredArgument.getType(); - if (type.isInstance(value)) { - newVariables.put(name, value); - } else { - ArgumentConversionContext conversionContext = ConversionContext.of(requiredArgument); - Optional converted = conversionService.convert(value, conversionContext); - Object result = converted.isPresent() ? converted.get() : conversionContext.getLastError().orElse(null); - if (result != null) { - newVariables.put(name, result); - } - } - } - } + private boolean setBindingResult(int index, Argument argument, ArgumentBinder.BindingResult bindingResult) { + if (!bindingResult.isSatisfied()) { + return false; + } + Object value; + if (argument.getType() == Optional.class) { + Optional optionalValue = bindingResult.getValue(); + if (optionalValue.isPresent()) { + value = optionalValue.get(); + } else { + return false; } + } else if (bindingResult.isPresentAndSatisfied()) { + value = bindingResult.get(); + } else { + return false; } - return newFulfilled(newVariables, (List>) requiredArguments); + setValue(index, argument, value); + return true; } - @Override - public HttpStatus findStatus(HttpStatus defaultStatus) { - return abstractRoute.definedStatus == null ? defaultStatus : abstractRoute.definedStatus; + private void setValue(int index, Argument argument, Object value) { + if (value != null) { + argumentValues[index] = convertValue(conversionService, argument, value); + } + fulfilledArguments[index] = true; } - @Override - public boolean isWebSocketRoute() { - return abstractRoute.isWebSocketRoute; + private void checkIfFulfilled() { + if (fulfilled) { + return; + } + for (boolean isFulfilled : fulfilledArguments) { + if (!isFulfilled) { + return; + } + } + fulfilled = true; } - /** - * @param argument The argument - * @param conversionContext The conversion context - * @param result An optional result - * @return The resolved value or an error - */ - protected Object resolveValueOrError(Argument argument, ConversionContext conversionContext, Optional result) { + private Object convertValue(ConversionService conversionService, Argument argument, Object value) { + if (value instanceof ConversionError conversionError) { + throw new ConversionErrorException(argument, conversionError); + } + Class argumentType = argument.getType(); + if (argumentType.isInstance(value)) { + if (argument.isContainerType()) { + if (argument.hasTypeVariables()) { + ConversionContext conversionContext = ConversionContext.of(argument); + Optional result = conversionService.convert(value, argumentType, conversionContext); + return resolveValueOrError(argument, conversionContext, result); + } + } + return value; + } else { + ConversionContext conversionContext = ConversionContext.of(argument); + Optional result = conversionService.convert(value, argumentType, conversionContext); + return resolveValueOrError(argument, conversionContext, result); + } + } + + private Object resolveValueOrError(Argument argument, ConversionContext conversionContext, Optional result) { if (result.isEmpty()) { Optional lastError = conversionContext.getLastError(); if (lastError.isEmpty() && argument.isDeclaredNullable()) { @@ -417,11 +477,4 @@ protected Object resolveValueOrError(Argument argument, ConversionContext con return result.get(); } - /** - * @param newVariables The new variables - * @param requiredArguments The required arguments - * @return A RouteMatch - */ - protected abstract RouteMatch newFulfilled(Map newVariables, List> requiredArguments); - } diff --git a/router/src/main/java/io/micronaut/web/router/BasicObjectRouteMatch.java b/router/src/main/java/io/micronaut/web/router/BasicObjectRouteMatch.java deleted file mode 100644 index 3e14dc25594..00000000000 --- a/router/src/main/java/io/micronaut/web/router/BasicObjectRouteMatch.java +++ /dev/null @@ -1,108 +0,0 @@ -/* - * Copyright 2017-2020 original authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.micronaut.web.router; - -import io.micronaut.core.type.Argument; -import io.micronaut.core.type.ReturnType; -import io.micronaut.http.HttpRequest; -import io.micronaut.http.MediaType; - -import io.micronaut.core.annotation.Nullable; -import java.util.*; -import java.util.function.Function; - -/** - * A route match designed to return an existing object. - * - * @author James Kleeh - * @since 1.0 - */ -public class BasicObjectRouteMatch implements RouteMatch { - - private final Object object; - - /** - * @param object An object - */ - public BasicObjectRouteMatch(Object object) { - this.object = object; - } - - @Override - public Class getDeclaringType() { - return object.getClass(); - } - - @Override - public Map getVariableValues() { - return Collections.emptyMap(); - } - - @Override - public Object execute(Map argumentValues) { - return object; - } - - @Override - public RouteMatch fulfill(Map argumentValues) { - return this; - } - - @Override - public RouteMatch decorate(Function, Object> executor) { - return new BasicObjectRouteMatch(executor.apply(this)); - } - - @Override - public Optional> getRequiredInput(String name) { - return Optional.empty(); - } - - @Override - public Optional> getBodyArgument() { - return Optional.empty(); - } - - @Override - public List getProduces() { - return Collections.emptyList(); - } - - @Override - public ReturnType getReturnType() { - return ReturnType.of(object.getClass()); - } - - @Override - public boolean doesConsume(@Nullable MediaType contentType) { - return true; - } - - @Override - public boolean doesProduce(@Nullable Collection acceptableTypes) { - return true; - } - - @Override - public boolean doesProduce(@Nullable MediaType acceptableType) { - return true; - } - - @Override - public boolean test(HttpRequest httpRequest) { - return true; - } -} diff --git a/router/src/main/java/io/micronaut/web/router/DefaultErrorRouteInfo.java b/router/src/main/java/io/micronaut/web/router/DefaultErrorRouteInfo.java new file mode 100644 index 00000000000..47a56bf0b64 --- /dev/null +++ b/router/src/main/java/io/micronaut/web/router/DefaultErrorRouteInfo.java @@ -0,0 +1,128 @@ +/* + * Copyright 2017-2023 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.web.router; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.Nullable; +import io.micronaut.core.convert.ConversionService; +import io.micronaut.core.type.Argument; +import io.micronaut.core.util.ObjectUtils; +import io.micronaut.http.HttpRequest; +import io.micronaut.http.HttpStatus; +import io.micronaut.http.MediaType; +import io.micronaut.inject.MethodExecutionHandle; + +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.function.Predicate; + +/** + * The default error route info implementation. + * + * @param The target + * @param The result + * @author Denis Stepanov + * @since 4.0.0 + */ +@Internal +public final class DefaultErrorRouteInfo extends DefaultRequestMatcher implements ErrorRouteInfo { + + @Nullable + private final Class originatingType; + private final Class exceptionType; + private final ConversionService conversionService; + + public DefaultErrorRouteInfo(@Nullable Class originatingType, + Class exceptionType, + MethodExecutionHandle targetMethod, + @Nullable + String bodyArgumentName, + @Nullable + Argument bodyArgument, + List consumesMediaTypes, + List producesMediaTypes, + List>> predicates, + ConversionService conversionService) { + super(targetMethod, bodyArgument, bodyArgumentName, consumesMediaTypes, producesMediaTypes, true, true, predicates); + this.originatingType = originatingType; + this.exceptionType = exceptionType; + this.conversionService = conversionService; + } + + @Override + public Class originatingType() { + return originatingType; + } + + @Override + public Class exceptionType() { + return exceptionType; + } + + @Override + public Optional> match(Class originatingClass, Throwable exception) { + if (originatingClass == originatingType && exceptionType.isInstance(exception)) { + return Optional.of(new ErrorRouteMatch<>(exception, this, conversionService)); + } + return Optional.empty(); + } + + @Override + public Optional> match(Throwable exception) { + if (originatingType == null && exceptionType.isInstance(exception)) { + return Optional.of(new ErrorRouteMatch<>(exception, this, conversionService)); + } + return Optional.empty(); + } + + @Override + public HttpStatus findStatus(HttpStatus defaultStatus) { + return super.findStatus(defaultStatus == null ? HttpStatus.INTERNAL_SERVER_ERROR : defaultStatus); + } + + @Override + public int hashCode() { + return ObjectUtils.hash(super.hashCode(), exceptionType, originatingType); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + if (!super.equals(o)) { + return false; + } + DefaultErrorRouteInfo that = (DefaultErrorRouteInfo) o; + return exceptionType.equals(that.exceptionType) && + Objects.equals(originatingType, that.originatingType); + } + + @Override + public String toString() { + return new StringBuilder().append(' ') + .append(exceptionType.getSimpleName()) + .append(" -> ") + .append(getTargetMethod().getDeclaringType().getSimpleName()) + .append('#') + .append(getTargetMethod()) + .toString(); + } +} diff --git a/router/src/main/java/io/micronaut/web/router/DefaultMethodBasedRouteInfo.java b/router/src/main/java/io/micronaut/web/router/DefaultMethodBasedRouteInfo.java new file mode 100644 index 00000000000..4bcebff8da1 --- /dev/null +++ b/router/src/main/java/io/micronaut/web/router/DefaultMethodBasedRouteInfo.java @@ -0,0 +1,163 @@ +/* + * Copyright 2017-2023 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.web.router; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.NonNull; +import io.micronaut.core.annotation.Nullable; +import io.micronaut.core.bind.ArgumentBinder; +import io.micronaut.core.bind.annotation.Bindable; +import io.micronaut.core.type.Argument; +import io.micronaut.core.util.CollectionUtils; +import io.micronaut.core.util.StringUtils; +import io.micronaut.http.HttpRequest; +import io.micronaut.http.MediaType; +import io.micronaut.http.bind.RequestBinderRegistry; +import io.micronaut.http.bind.binders.RequestArgumentBinder; +import io.micronaut.inject.MethodExecutionHandle; +import io.micronaut.inject.beans.KotlinExecutableMethodUtils; + +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +/** + * The default {@link MethodBasedRouteInfo} implementation. + * + * @param The target + * @param The result + * @author Denis Stepanov + * @since 4.0.0 + */ +@Internal +public class DefaultMethodBasedRouteInfo extends DefaultRouteInfo implements MethodBasedRouteInfo { + + private final MethodExecutionHandle targetMethod; + private final String[] argumentNames; + private final Map> requiredInputs; + private final boolean isVoid; + private final Optional> optionalBodyArgument; + private final Optional> optionalFullBodyArgument; + + private RequestArgumentBinder[] argumentBinders; + + public DefaultMethodBasedRouteInfo(MethodExecutionHandle targetMethod, + @Nullable + Argument bodyArgument, + @Nullable + String bodyArgumentName, + List consumesMediaTypes, + List producesMediaTypes, + boolean isPermitsBody, + boolean isErrorRoute) { + super(targetMethod, targetMethod.getReturnType(), consumesMediaTypes, producesMediaTypes, targetMethod.getDeclaringType(), isErrorRoute, isPermitsBody); + this.targetMethod = targetMethod; + + Argument[] arguments = targetMethod.getArguments(); + argumentNames = new String[arguments.length]; + if (arguments.length > 0) { + Map> requiredInputs = CollectionUtils.newLinkedHashMap(arguments.length); + for (int i = 0; i < arguments.length; i++) { + Argument requiredArgument = arguments[i]; + String inputName = resolveInputName(requiredArgument); + requiredInputs.put(inputName, requiredArgument); + argumentNames[i] = inputName; + } + this.requiredInputs = Collections.unmodifiableMap(requiredInputs); + } else { + this.requiredInputs = Collections.emptyMap(); + } + if (returnType.isVoid()) { + isVoid = true; + } else if (isSuspended()) { + isVoid = KotlinExecutableMethodUtils.isKotlinFunctionReturnTypeUnit(targetMethod.getExecutableMethod()); + } else { + isVoid = false; + } + if (bodyArgument != null) { + optionalBodyArgument = Optional.of(bodyArgument); + } else if (bodyArgumentName != null) { + optionalBodyArgument = Optional.ofNullable(requiredInputs.get(bodyArgumentName)); + } else { + optionalBodyArgument = Optional.empty(); + } + optionalFullBodyArgument = super.getFullBodyArgument(); + } + + @Override + public RequestArgumentBinder[] resolveArgumentBinders(RequestBinderRegistry requestBinderRegistry) { + // Allow concurrent access + if (argumentBinders == null) { + argumentBinders = resolveArgumentBindersInternal(requestBinderRegistry); + } + return argumentBinders; + } + + private RequestArgumentBinder[] resolveArgumentBindersInternal(RequestBinderRegistry requestBinderRegistry) { + Argument[] arguments = targetMethod.getArguments(); + if (arguments.length == 0) { + return new RequestArgumentBinder[0]; + } + + RequestArgumentBinder[] binders = new RequestArgumentBinder[arguments.length]; + for (int i = 0; i < arguments.length; i++) { + Argument argument = arguments[i]; + Optional>> argumentBinder = requestBinderRegistry.findArgumentBinder(argument); + binders[i] = (RequestArgumentBinder) argumentBinder.orElse(null); + } + return binders; + } + + @Override + public boolean isVoid() { + return isVoid; + } + + /** + * Resolves the name for an argument. + * + * @param argument the argument + * @return the name + */ + private static @NonNull String resolveInputName(@NonNull Argument argument) { + String inputName = argument.getAnnotationMetadata().stringValue(Bindable.NAME).orElse(null); + if (StringUtils.isEmpty(inputName)) { + inputName = argument.getName(); + } + return inputName; + } + + @Override + public MethodExecutionHandle getTargetMethod() { + return targetMethod; + } + + @Override + public Optional> getBodyArgument() { + return optionalBodyArgument; + } + + @Override + public Optional> getFullBodyArgument() { + return optionalFullBodyArgument; + } + + @Override + public String[] getArgumentNames() { + return argumentNames; + } +} diff --git a/router/src/main/java/io/micronaut/web/router/DefaultRequestMatcher.java b/router/src/main/java/io/micronaut/web/router/DefaultRequestMatcher.java new file mode 100644 index 00000000000..b116b52fac1 --- /dev/null +++ b/router/src/main/java/io/micronaut/web/router/DefaultRequestMatcher.java @@ -0,0 +1,64 @@ +/* + * Copyright 2017-2023 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.web.router; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.type.Argument; +import io.micronaut.http.HttpRequest; +import io.micronaut.http.MediaType; +import io.micronaut.inject.MethodExecutionHandle; + +import java.util.List; +import java.util.function.Predicate; + +/** + * The default {@link RequestMatcher} implementation. + * + * @param The target + * @param The result + * @author Denis Stepanov + * @since 4.0.0 + */ +@Internal +public class DefaultRequestMatcher extends DefaultMethodBasedRouteInfo implements RequestMatcher { + + private final List>> predicates; + + public DefaultRequestMatcher(MethodExecutionHandle targetMethod, + Argument bodyArgument, + String bodyArgumentName, + List producesMediaTypes, + List consumesMediaTypes, + boolean isPermitsBody, + boolean isErrorRoute, + List>> predicates) { + super(targetMethod, bodyArgument, bodyArgumentName, producesMediaTypes, consumesMediaTypes, isPermitsBody, isErrorRoute); + this.predicates = predicates; + } + + @Override + public boolean matching(HttpRequest httpRequest) { + if (predicates.isEmpty()) { + return true; + } + for (Predicate> predicate : predicates) { + if (!predicate.test(httpRequest)) { + return false; + } + } + return true; + } +} diff --git a/router/src/main/java/io/micronaut/web/router/DefaultRouteBuilder.java b/router/src/main/java/io/micronaut/web/router/DefaultRouteBuilder.java index 8e397aad297..ad4583c5c03 100644 --- a/router/src/main/java/io/micronaut/web/router/DefaultRouteBuilder.java +++ b/router/src/main/java/io/micronaut/web/router/DefaultRouteBuilder.java @@ -21,23 +21,17 @@ import io.micronaut.context.env.Environment; import io.micronaut.core.annotation.AnnotationMetadata; import io.micronaut.core.annotation.AnnotationMetadataResolver; -import io.micronaut.core.annotation.NonNull; import io.micronaut.core.annotation.Nullable; -import io.micronaut.core.bind.annotation.Bindable; import io.micronaut.core.convert.ConversionService; import io.micronaut.core.type.Argument; -import io.micronaut.core.type.ReturnType; import io.micronaut.core.util.ObjectUtils; -import io.micronaut.core.util.StringUtils; import io.micronaut.http.HttpMethod; import io.micronaut.http.HttpRequest; import io.micronaut.http.HttpStatus; import io.micronaut.http.MediaType; import io.micronaut.http.annotation.Body; -import io.micronaut.http.annotation.Status; import io.micronaut.http.filter.GenericHttpFilter; import io.micronaut.http.filter.HttpFilter; -import io.micronaut.http.uri.UriMatchInfo; import io.micronaut.http.uri.UriMatchTemplate; import io.micronaut.inject.BeanDefinition; import io.micronaut.inject.ExecutableMethod; @@ -77,18 +71,17 @@ public abstract class DefaultRouteBuilder implements RouteBuilder { protected static final Logger LOG = LoggerFactory.getLogger(DefaultRouteBuilder.class); - static final Object NO_VALUE = new Object(); protected final ExecutionHandleLocator executionHandleLocator; protected final UriNamingStrategy uriNamingStrategy; protected final ConversionService conversionService; protected final Charset defaultCharset; private DefaultUriRoute currentParentRoute; - private List uriRoutes = new ArrayList<>(); - private List statusRoutes = new ArrayList<>(); - private List errorRoutes = new ArrayList<>(); - private List filterRoutes = new ArrayList<>(); - private Set exposedPorts = new HashSet<>(5); + private final List uriRoutes = new ArrayList<>(); + private final List statusRoutes = new ArrayList<>(); + private final List errorRoutes = new ArrayList<>(); + private final List filterRoutes = new ArrayList<>(); + private final Set exposedPorts = new HashSet<>(5); /** * @param executionHandleLocator The execution handler locator @@ -114,8 +107,7 @@ public DefaultRouteBuilder(ExecutionHandleLocator executionHandleLocator, UriNam this.executionHandleLocator = executionHandleLocator; this.uriNamingStrategy = uriNamingStrategy; this.conversionService = conversionService; - if (executionHandleLocator instanceof ApplicationContext) { - ApplicationContext applicationContext = (ApplicationContext) executionHandleLocator; + if (executionHandleLocator instanceof ApplicationContext applicationContext) { Environment environment = applicationContext.getEnvironment(); defaultCharset = environment.get("micronaut.application.default-charset", Charset.class, StandardCharsets.UTF_8); } else { @@ -189,7 +181,7 @@ public ResourceRoute single(Class cls) { public StatusRoute status(Class originatingClass, HttpStatus status, Class type, String method, Class[] parameterTypes) { Optional> executionHandle = executionHandleLocator.findExecutionHandle((Class) type, method, parameterTypes); - MethodExecutionHandle executableHandle = executionHandle.orElseThrow(() -> + MethodExecutionHandle executableHandle = executionHandle.orElseThrow(() -> new RoutingException("No such route: " + type.getName() + "." + method) ); @@ -202,7 +194,7 @@ public StatusRoute status(Class originatingClass, HttpStatus status, Class public StatusRoute status(HttpStatus status, Class type, String method, Class[] parameterTypes) { Optional> executionHandle = executionHandleLocator.findExecutionHandle((Class) type, method, parameterTypes); - MethodExecutionHandle executableHandle = executionHandle.orElseThrow(() -> + MethodExecutionHandle executableHandle = executionHandle.orElseThrow(() -> new RoutingException("No such route: " + type.getName() + "." + method) ); @@ -215,7 +207,7 @@ public StatusRoute status(HttpStatus status, Class type, String method, Class public ErrorRoute error(Class originatingClass, Class error, Class type, String method, Class[] parameterTypes) { Optional> executionHandle = executionHandleLocator.findExecutionHandle((Class) type, method, parameterTypes); - MethodExecutionHandle executableHandle = executionHandle.orElseThrow(() -> + MethodExecutionHandle executableHandle = executionHandle.orElseThrow(() -> new RoutingException("No such route: " + type.getName() + "." + method) ); @@ -228,7 +220,7 @@ public ErrorRoute error(Class originatingClass, Class er public ErrorRoute error(Class error, Class type, String method, Class[] parameterTypes) { Optional> executionHandle = executionHandleLocator.findExecutionHandle((Class) type, method, parameterTypes); - MethodExecutionHandle executableHandle = executionHandle.orElseThrow(() -> + MethodExecutionHandle executableHandle = executionHandle.orElseThrow(() -> new RoutingException("No such route: " + type.getName() + "." + method) ); @@ -369,9 +361,10 @@ public UriRoute TRACE(String uri, BeanDefinition beanDefinition, ExecutableMe * @return an {@link UriRoute} */ protected UriRoute buildRoute(HttpMethod httpMethod, String uri, Class type, String method, Class... parameterTypes) { - Optional> executionHandle = executionHandleLocator.findExecutionHandle(type, method, parameterTypes); + Optional> executionHandle = + executionHandleLocator.findExecutionHandle((Class) type, method, parameterTypes); - MethodExecutionHandle executableHandle = executionHandle.orElseThrow(() -> + MethodExecutionHandle executableHandle = executionHandle.orElseThrow(() -> new RoutingException("No such route: " + type.getName() + "." + method) ); @@ -387,11 +380,11 @@ protected UriRoute buildRoute(HttpMethod httpMethod, String uri, Class type, * * @return an {@link UriRoute} */ - protected UriRoute buildRoute(HttpMethod httpMethod, String uri, MethodExecutionHandle executableHandle) { + protected UriRoute buildRoute(HttpMethod httpMethod, String uri, MethodExecutionHandle executableHandle) { return buildRoute(httpMethod.name(), httpMethod, uri, executableHandle); } - private UriRoute buildRoute(String httpMethodName, HttpMethod httpMethod, String uri, MethodExecutionHandle executableHandle) { + private UriRoute buildRoute(String httpMethodName, HttpMethod httpMethod, String uri, MethodExecutionHandle executableHandle) { UriRoute route; if (currentParentRoute != null) { route = new DefaultUriRoute(httpMethod, currentParentRoute.uriMatchTemplate.nest(uri), executableHandle, httpMethodName, conversionService); @@ -418,155 +411,43 @@ private UriRoute buildBeanRoute(HttpMethod httpMethod, String uri, BeanDefinitio * @return The uri route corresponding to the method. */ protected UriRoute buildBeanRoute(String httpMethodName, HttpMethod httpMethod, String uri, BeanDefinition beanDefinition, ExecutableMethod method) { - MethodExecutionHandle executionHandle = executionHandleLocator - .createExecutionHandle(beanDefinition, (ExecutableMethod) method); + MethodExecutionHandle executionHandle = (MethodExecutionHandle) executionHandleLocator + .createExecutionHandle(beanDefinition, (ExecutableMethod) method); return buildRoute(httpMethodName, httpMethod, uri, executionHandle); } /** - * Abstract class for base {@link MethodBasedRoute}. + * Abstract class for base {@link MethodBasedRouteInfo}. */ - abstract class AbstractRoute implements MethodBasedRoute, RouteInfo { + abstract static class AbstractRoute implements Route { protected final List>> conditions = new ArrayList<>(); - protected final MethodExecutionHandle targetMethod; + protected final MethodExecutionHandle targetMethod; protected final ConversionService conversionService; - protected List consumesMediaTypes; - protected List producesMediaTypes; + protected List consumesMediaTypes = List.of(); + protected List producesMediaTypes = List.of(); protected String bodyArgumentName; protected Argument bodyArgument; - protected final Map> requiredInputs; - protected final Class declaringType; - protected boolean consumesMediaTypesContainsAll; - protected boolean producesMediaTypesContainsAll; - protected final HttpStatus definedStatus; - protected final boolean isWebSocketRoute; - private final boolean isVoid; - private final boolean suspended; - private final boolean reactive; - private final boolean single; - private final boolean async; - private final boolean specifiedSingle; - private final boolean isAsyncOrReactive; /** * @param targetMethod The target method execution handle * @param conversionService The conversion service * @param mediaTypes The media types */ - AbstractRoute(MethodExecutionHandle targetMethod, ConversionService conversionService, List mediaTypes) { + AbstractRoute(MethodExecutionHandle targetMethod, ConversionService conversionService, List mediaTypes) { this.targetMethod = targetMethod; this.conversionService = conversionService; this.consumesMediaTypes = mediaTypes; - this.declaringType = targetMethod.getDeclaringType(); - this.producesMediaTypes = RouteInfo.super.getProduces(); - this.consumesMediaTypes = RouteInfo.super.getConsumes(); - suspended = targetMethod.getExecutableMethod().isSuspend(); - reactive = RouteInfo.super.isReactive(); - async = RouteInfo.super.isAsync(); - single = RouteInfo.super.isSingleResult(); - isVoid = RouteInfo.super.isVoid(); - specifiedSingle = RouteInfo.super.isSpecifiedSingle(); - isAsyncOrReactive = RouteInfo.super.isAsyncOrReactive(); for (Argument argument : targetMethod.getArguments()) { if (argument.getAnnotationMetadata().hasAnnotation(Body.class)) { this.bodyArgument = argument; } } - Argument[] requiredArguments = targetMethod.getArguments(); - if (requiredArguments.length > 0) { - Map> requiredInputs = new LinkedHashMap<>(requiredArguments.length); - for (Argument requiredArgument : requiredArguments) { - String inputName = resolveInputName(requiredArgument); - requiredInputs.put(inputName, requiredArgument); - } - this.requiredInputs = Collections.unmodifiableMap(requiredInputs); - } else { - this.requiredInputs = Collections.emptyMap(); - } - setConsumesMediaTypesContainsAll(); - setProducesMediaTypesContainsAll(); - this.definedStatus = targetMethod.enumValue(Status.class, HttpStatus.class).orElse(null); - this.isWebSocketRoute = targetMethod.hasAnnotation("io.micronaut.websocket.annotation.OnMessage"); - } - - @Override - public Class getDeclaringType() { - return declaringType; - } - - private void setConsumesMediaTypesContainsAll() { - this.consumesMediaTypesContainsAll = consumesMediaTypes == null || consumesMediaTypes.isEmpty() || consumesMediaTypes.contains(MediaType.ALL_TYPE); - } - - private void setProducesMediaTypesContainsAll() { - this.producesMediaTypesContainsAll = producesMediaTypes == null || producesMediaTypes.isEmpty() || producesMediaTypes.contains(MediaType.ALL_TYPE); - } - - /** - * Resolves the name for an argument. - * - * @param argument the argument - * @return the name - */ - protected @NonNull String resolveInputName(@NonNull Argument argument) { - String inputName = argument.getAnnotationMetadata().stringValue(Bindable.NAME).orElse(null); - if (StringUtils.isEmpty(inputName)) { - inputName = argument.getName(); - } - return inputName; - } - - @NonNull - @Override - public AnnotationMetadata getAnnotationMetadata() { - return targetMethod.getAnnotationMetadata(); - } - - @Override - public ReturnType getReturnType() { - return targetMethod.getReturnType(); - } - - @Override - public boolean isSuspended() { - return suspended; - } - - @Override - public boolean isReactive() { - return reactive; - } - - @Override - public boolean isSingleResult() { - return single; - } - - @Override - public boolean isSpecifiedSingle() { - return specifiedSingle; - } - - @Override - public boolean isAsync() { - return async; - } - - @Override - public boolean isAsyncOrReactive() { - return isAsyncOrReactive; - } - - @Override - public boolean isVoid() { - return isVoid; } @Override public Route consumes(MediaType... mediaTypes) { if (mediaTypes != null) { - this.consumesMediaTypes = Collections.unmodifiableList(Arrays.asList(mediaTypes)); - setConsumesMediaTypesContainsAll(); + this.consumesMediaTypes = List.of(mediaTypes); } return this; } @@ -579,7 +460,6 @@ public List getConsumes() { @Override public Route consumesAll() { this.consumesMediaTypes = Collections.emptyList(); - setConsumesMediaTypesContainsAll(); return this; } @@ -606,8 +486,7 @@ public Route body(Argument argument) { @Override public Route produces(MediaType... mediaType) { if (mediaType != null) { - this.producesMediaTypes = Collections.unmodifiableList(Arrays.asList(mediaType)); - setProducesMediaTypesContainsAll(); + this.producesMediaTypes = List.of(mediaType); } return this; } @@ -617,28 +496,14 @@ public List getProduces() { return producesMediaTypes; } - @Override - public MethodExecutionHandle getTargetMethod() { - return this.targetMethod; - } - - /** - * Whether the route permits a request body. - * @return True if the route permits a request body - */ - protected boolean permitsRequestBody() { - return true; - } - @Override public boolean equals(Object o) { if (this == o) { return true; } - if (!(o instanceof AbstractRoute)) { + if (!(o instanceof AbstractRoute that)) { return false; } - AbstractRoute that = (AbstractRoute) o; return Objects.equals(consumesMediaTypes, that.consumesMediaTypes) && Objects.equals(producesMediaTypes, that.producesMediaTypes); } @@ -652,7 +517,7 @@ public int hashCode() { /** * Default Error Route. */ - class DefaultErrorRoute extends AbstractRoute implements ErrorRoute { + static class DefaultErrorRoute extends AbstractRoute implements ErrorRoute { private final Class error; private final Class originatingClass; @@ -662,7 +527,7 @@ class DefaultErrorRoute extends AbstractRoute implements ErrorRoute { * @param targetMethod The target method execution handle * @param conversionService The conversion service */ - public DefaultErrorRoute(Class error, MethodExecutionHandle targetMethod, ConversionService conversionService) { + public DefaultErrorRoute(Class error, MethodExecutionHandle targetMethod, ConversionService conversionService) { this(null, error, targetMethod, conversionService); } @@ -672,15 +537,29 @@ public DefaultErrorRoute(Class error, MethodExecutionHandle * @param targetMethod The target method execution handle * @param conversionService The conversion service */ - public DefaultErrorRoute( - Class originatingClass, Class error, - MethodExecutionHandle targetMethod, - ConversionService conversionService) { + public DefaultErrorRoute(Class originatingClass, + Class error, + MethodExecutionHandle targetMethod, + ConversionService conversionService) { super(targetMethod, conversionService, Collections.emptyList()); this.originatingClass = originatingClass; this.error = error; } + @Override + public ErrorRouteInfo toRouteInfo() { + return new DefaultErrorRouteInfo<>( + originatingClass, + error, + targetMethod, + bodyArgumentName, + bodyArgument, + consumesMediaTypes, + producesMediaTypes, + conditions, + conversionService); + } + @Override @Nullable public Class originatingType() { @@ -692,24 +571,6 @@ public Class exceptionType() { return error; } - @SuppressWarnings("unchecked") - @Override - public Optional> match(Class originatingClass, Throwable exception) { - if (originatingClass == this.originatingClass && error.isInstance(exception)) { - return Optional.of(new ErrorRouteMatch(exception, this, conversionService)); - } - return Optional.empty(); - } - - @SuppressWarnings("unchecked") - @Override - public Optional> match(Throwable exception) { - if (originatingClass == null && error.isInstance(exception)) { - return Optional.of(new ErrorRouteMatch(exception, this, conversionService)); - } - return Optional.empty(); - } - @Override public ErrorRoute consumes(MediaType... mediaType) { return (ErrorRoute) super.consumes(mediaType); @@ -759,8 +620,7 @@ public int hashCode() { @Override public String toString() { - StringBuilder builder = new StringBuilder(); - return builder.append(' ') + return new StringBuilder().append(' ') .append(error.getSimpleName()) .append(" -> ") .append(targetMethod.getDeclaringType().getSimpleName()) @@ -773,7 +633,7 @@ public String toString() { /** * Represents a route for an {@link io.micronaut.http.HttpStatus} code. */ - class DefaultStatusRoute extends AbstractRoute implements StatusRoute { + static class DefaultStatusRoute extends AbstractRoute implements StatusRoute { private final HttpStatus status; private final Class originatingClass; @@ -783,7 +643,7 @@ class DefaultStatusRoute extends AbstractRoute implements StatusRoute { * @param targetMethod The target method execution handle * @param conversionService The conversion service */ - public DefaultStatusRoute(HttpStatus status, MethodExecutionHandle targetMethod, ConversionService conversionService) { + public DefaultStatusRoute(HttpStatus status, MethodExecutionHandle targetMethod, ConversionService conversionService) { this(null, status, targetMethod, conversionService); } @@ -793,12 +653,26 @@ public DefaultStatusRoute(HttpStatus status, MethodExecutionHandle targetMethod, * @param targetMethod The target method execution handle * @param conversionService The conversion service */ - public DefaultStatusRoute(Class originatingClass, HttpStatus status, MethodExecutionHandle targetMethod, ConversionService conversionService) { + public DefaultStatusRoute(Class originatingClass, HttpStatus status, MethodExecutionHandle targetMethod, ConversionService conversionService) { super(targetMethod, conversionService, Collections.emptyList()); this.originatingClass = originatingClass; this.status = status; } + @Override + public StatusRouteInfo toRouteInfo() { + return new DefaultStatusRouteInfo<>( + originatingClass, + status, + targetMethod, + bodyArgumentName, + bodyArgument, + consumesMediaTypes, + producesMediaTypes, + conditions, + conversionService); + } + @Override @Nullable public Class originatingType() { @@ -810,24 +684,6 @@ public HttpStatus status() { return status; } - @SuppressWarnings("unchecked") - @Override - public Optional> match(Class originatingClass, HttpStatus status) { - if (originatingClass == this.originatingClass && this.status == status) { - return Optional.of(new StatusRouteMatch(status, this, conversionService)); - } - return Optional.empty(); - } - - @SuppressWarnings("unchecked") - @Override - public Optional> match(HttpStatus status) { - if (this.originatingClass == null && this.status == status) { - return Optional.of(new StatusRouteMatch(status, this, conversionService)); - } - return Optional.empty(); - } - @Override public StatusRoute consumes(MediaType... mediaType) { return this; @@ -860,13 +716,12 @@ public boolean equals(Object o) { if (this == o) { return true; } - if (!(o instanceof DefaultStatusRoute)) { + if (!(o instanceof DefaultStatusRoute that)) { return false; } if (!super.equals(o)) { return false; } - DefaultStatusRoute that = (DefaultStatusRoute) o; return status == that.status && Objects.equals(originatingClass, that.originatingClass); } @@ -895,7 +750,7 @@ class DefaultUriRoute extends AbstractRoute implements UriRoute { */ DefaultUriRoute(HttpMethod httpMethod, CharSequence uriTemplate, - MethodExecutionHandle targetMethod, + MethodExecutionHandle targetMethod, ConversionService conversionService) { this(httpMethod, uriTemplate, targetMethod, httpMethod.name(), conversionService); } @@ -909,7 +764,7 @@ class DefaultUriRoute extends AbstractRoute implements UriRoute { */ DefaultUriRoute(HttpMethod httpMethod, CharSequence uriTemplate, - MethodExecutionHandle targetMethod, + MethodExecutionHandle targetMethod, String httpMethodName, ConversionService conversionService) { this(httpMethod, uriTemplate, MediaType.APPLICATION_JSON_TYPE, targetMethod, httpMethodName, conversionService); @@ -925,7 +780,7 @@ class DefaultUriRoute extends AbstractRoute implements UriRoute { DefaultUriRoute(HttpMethod httpMethod, CharSequence uriTemplate, MediaType mediaType, - MethodExecutionHandle targetMethod, + MethodExecutionHandle targetMethod, ConversionService conversionService) { this(httpMethod, uriTemplate, mediaType, targetMethod, httpMethod.name(), conversionService); } @@ -941,7 +796,7 @@ class DefaultUriRoute extends AbstractRoute implements UriRoute { DefaultUriRoute(HttpMethod httpMethod, CharSequence uriTemplate, MediaType mediaType, - MethodExecutionHandle targetMethod, + MethodExecutionHandle targetMethod, String httpMethodName, ConversionService conversionService) { this(httpMethod, new UriMatchTemplate(uriTemplate), Collections.singletonList(mediaType), targetMethod, httpMethodName, conversionService); @@ -955,7 +810,7 @@ class DefaultUriRoute extends AbstractRoute implements UriRoute { */ DefaultUriRoute(HttpMethod httpMethod, UriMatchTemplate uriTemplate, - MethodExecutionHandle targetMethod, + MethodExecutionHandle targetMethod, ConversionService conversionService) { this(httpMethod, uriTemplate, targetMethod, httpMethod.name(), conversionService); } @@ -969,7 +824,7 @@ class DefaultUriRoute extends AbstractRoute implements UriRoute { */ DefaultUriRoute(HttpMethod httpMethod, UriMatchTemplate uriTemplate, - MethodExecutionHandle targetMethod, + MethodExecutionHandle targetMethod, String httpMethodName, ConversionService conversionService) { this(httpMethod, uriTemplate, Collections.singletonList(MediaType.APPLICATION_JSON_TYPE), targetMethod, httpMethodName, conversionService); @@ -985,7 +840,7 @@ class DefaultUriRoute extends AbstractRoute implements UriRoute { DefaultUriRoute(HttpMethod httpMethod, UriMatchTemplate uriTemplate, List mediaTypes, - MethodExecutionHandle targetMethod, + MethodExecutionHandle targetMethod, ConversionService conversionService) { this(httpMethod, uriTemplate, mediaTypes, targetMethod, httpMethod.name(), conversionService); } @@ -998,9 +853,10 @@ class DefaultUriRoute extends AbstractRoute implements UriRoute { * @param httpMethodName The actual name of the method - may differ from {@link HttpMethod#name()} for non-standard http methods * @param conversionService The conversion service */ - DefaultUriRoute(HttpMethod httpMethod, UriMatchTemplate uriTemplate, + DefaultUriRoute(HttpMethod httpMethod, + UriMatchTemplate uriTemplate, List mediaTypes, - MethodExecutionHandle targetMethod, + MethodExecutionHandle targetMethod, String httpMethodName, ConversionService conversionService) { super(targetMethod, conversionService, mediaTypes); @@ -1009,6 +865,22 @@ class DefaultUriRoute extends AbstractRoute implements UriRoute { this.httpMethodName = httpMethodName; } + @Override + public UriRouteInfo toRouteInfo() { + return new DefaultUrlRouteInfo<>( + httpMethod, + uriMatchTemplate, + defaultCharset, + targetMethod, + bodyArgumentName, + bodyArgument, + consumesMediaTypes, + producesMediaTypes, + conditions, + port, + conversionService); + } + @Override public String getHttpMethodName() { return httpMethodName; @@ -1016,8 +888,7 @@ public String getHttpMethodName() { @Override public String toString() { - StringBuilder builder = new StringBuilder(getHttpMethodName()); - return builder.append(' ') + return new StringBuilder(getHttpMethodName()).append(' ') .append(uriMatchTemplate) .append(" -> ") .append(targetMethod.getDeclaringType().getSimpleName()) @@ -1084,13 +955,6 @@ public UriRoute where(Predicate> condition) { return (UriRoute) super.where(condition); } - @SuppressWarnings("unchecked") - @Override - public Optional match(String uri) { - Optional matchInfo = uriMatchTemplate.match(uri); - return matchInfo.map(info -> new DefaultUriRouteMatch(info, this, defaultCharset, conversionService)); - } - @Override public UriMatchTemplate getUriMatchTemplate() { return this.uriMatchTemplate; @@ -1100,11 +964,6 @@ public UriMatchTemplate getUriMatchTemplate() { public int compareTo(UriRoute o) { return uriMatchTemplate.compareTo(o.getUriMatchTemplate()); } - - @Override - protected boolean permitsRequestBody() { - return HttpMethod.permitsRequestBody(httpMethod); - } } /** @@ -1190,6 +1049,11 @@ class DefaultResourceRoute implements ResourceRoute { buildRemainingRoutes(type, routeMap); } + @Override + public RouteInfo toRouteInfo() { + throw new IllegalStateException("Not implemented!"); + } + @Override public ResourceRoute consumes(MediaType... mediaTypes) { if (mediaTypes != null) { diff --git a/router/src/main/java/io/micronaut/web/router/DefaultRouteInfo.java b/router/src/main/java/io/micronaut/web/router/DefaultRouteInfo.java new file mode 100644 index 00000000000..6a7778aa887 --- /dev/null +++ b/router/src/main/java/io/micronaut/web/router/DefaultRouteInfo.java @@ -0,0 +1,291 @@ +/* + * Copyright 2017-2023 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.web.router; + +import io.micronaut.core.annotation.AnnotationMetadata; +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.Nullable; +import io.micronaut.core.type.Argument; +import io.micronaut.core.type.ReturnType; +import io.micronaut.core.util.ArrayUtils; +import io.micronaut.core.util.CollectionUtils; +import io.micronaut.http.HttpResponse; +import io.micronaut.http.HttpStatus; +import io.micronaut.http.MediaType; +import io.micronaut.http.annotation.Consumes; +import io.micronaut.http.annotation.Produces; +import io.micronaut.http.annotation.Status; +import io.micronaut.http.sse.Event; + +import java.util.Collection; +import java.util.List; +import java.util.Optional; + +/** + * The default route info implementation. + * + * @param The result type + * @author Denis Stepanov + * @since 4.0.0 + */ +@Internal +public class DefaultRouteInfo implements RouteInfo { + + protected final ReturnType returnType; + protected final List consumesMediaTypes; + protected final List producesMediaTypes; + protected final AnnotationMetadata annotationMetadata; + protected final Class declaringType; + protected final boolean consumesMediaTypesContainsAll; + protected final boolean producesMediaTypesContainsAll; + @Nullable + protected final HttpStatus definedStatus; + protected final boolean isWebSocketRoute; + private final boolean isVoid; + private final boolean suspended; + private final boolean reactive; + private final boolean single; + private final boolean async; + private final boolean completable; + private final boolean specifiedSingle; + private final boolean asyncOrReactive; + private final Argument bodyType; + private final boolean isErrorRoute; + private final boolean isPermitsBody; + + public DefaultRouteInfo(ReturnType returnType, + Class declaringType, + boolean isErrorRoute, + boolean isPermitsBody) { + this(AnnotationMetadata.EMPTY_METADATA, returnType, List.of(), List.of(), declaringType, isErrorRoute, isPermitsBody); + } + + public DefaultRouteInfo(AnnotationMetadata annotationMetadata, + ReturnType returnType, + List consumesMediaTypes, + List producesMediaTypes, + Class declaringType, + boolean isErrorRoute, + boolean isPermitsBody) { + this.annotationMetadata = annotationMetadata; + this.returnType = returnType; + bodyType = resolveBodyType(returnType); + single = returnType.isSingleResult() || + (isReactive() && returnType.getFirstTypeVariable() + .filter(t -> HttpResponse.class.isAssignableFrom(t.getType())).isPresent()) || + returnType.isAsync() || + returnType.isSuspended(); + specifiedSingle = returnType.isSpecifiedSingle(); + completable = returnType.isCompletable(); + async = returnType.isAsync(); + asyncOrReactive = returnType.isAsyncOrReactive(); + reactive = returnType.isReactive(); + suspended = returnType.isSuspended(); + this.declaringType = declaringType; + this.isErrorRoute = isErrorRoute; + this.isPermitsBody = isPermitsBody; + this.isVoid = returnType.isVoid(); + isWebSocketRoute = annotationMetadata.hasAnnotation("io.micronaut.websocket.annotation.OnMessage"); + definedStatus = annotationMetadata.enumValue(Status.class, HttpStatus.class).orElse(null); + + if (producesMediaTypes.isEmpty()) { + MediaType[] producesTypes = MediaType.of(annotationMetadata.stringValues(Produces.class)); + Optional> firstTypeVariable = returnType.getFirstTypeVariable(); + if (firstTypeVariable.isPresent() && Event.class.isAssignableFrom(firstTypeVariable.get().getType())) { + this.producesMediaTypes = List.of(MediaType.TEXT_EVENT_STREAM_TYPE); + producesMediaTypesContainsAll = true; + } else if (ArrayUtils.isNotEmpty(producesTypes)) { + this.producesMediaTypes = List.of(producesTypes); + producesMediaTypesContainsAll = this.producesMediaTypes.contains(MediaType.ALL_TYPE); + } else { + producesMediaTypesContainsAll = true; + this.producesMediaTypes = RouteInfo.DEFAULT_PRODUCES; + } + } else { + this.producesMediaTypes = producesMediaTypes; + producesMediaTypesContainsAll = this.producesMediaTypes.contains(MediaType.ALL_TYPE); + } + + if (consumesMediaTypes.isEmpty()) { + MediaType[] consumesTypes = MediaType.of(annotationMetadata.stringValues(Consumes.class)); + if (ArrayUtils.isNotEmpty(consumesTypes)) { + this.consumesMediaTypes = List.of(consumesTypes); + consumesMediaTypesContainsAll = this.consumesMediaTypes.contains(MediaType.ALL_TYPE); + } else { + this.consumesMediaTypes = List.of(); + consumesMediaTypesContainsAll = true; + } + } else { + this.consumesMediaTypes = consumesMediaTypes; + consumesMediaTypesContainsAll = this.consumesMediaTypes.contains(MediaType.ALL_TYPE); + } + } + + private static Argument resolveBodyType(ReturnType returnType) { + if (returnType.isAsyncOrReactive()) { + Argument reactiveType = returnType.getFirstTypeVariable().orElse(Argument.OBJECT_ARGUMENT); + if (HttpResponse.class.isAssignableFrom(reactiveType.getType())) { + reactiveType = reactiveType.getFirstTypeVariable().orElse(Argument.OBJECT_ARGUMENT); + } + return reactiveType; + } else if (HttpResponse.class.isAssignableFrom(returnType.getType())) { + Argument responseType = returnType.getFirstTypeVariable().orElse(Argument.OBJECT_ARGUMENT); + if (responseType.isAsyncOrReactive()) { + return responseType.getFirstTypeVariable().orElse(Argument.OBJECT_ARGUMENT); + } + return responseType; + } + return returnType.asArgument(); + } + + @Override + public Optional> getBodyArgument() { + return Optional.empty(); + } + + @Override + public ReturnType getReturnType() { + return returnType; + } + + @Override + public Argument getBodyType() { + return bodyType; + } + + @Override + public Class getDeclaringType() { + return declaringType; + } + + @Override + public List getProduces() { + return producesMediaTypes; + } + + @Override + public List getConsumes() { + return consumesMediaTypes; + } + + @Override + public boolean doesConsume(MediaType contentType) { + return contentType == null || consumesMediaTypesContainsAll || explicitlyConsumes(contentType); + } + + @Override + public boolean doesProduce(@Nullable Collection acceptableTypes) { + return producesMediaTypesContainsAll || anyMediaTypesMatch(producesMediaTypes, acceptableTypes); + } + + @Override + public boolean doesProduce(@Nullable MediaType acceptableType) { + return producesMediaTypesContainsAll || acceptableType == null || acceptableType.equals(MediaType.ALL_TYPE) || producesMediaTypes.contains(acceptableType); + } + + private boolean anyMediaTypesMatch(List producedMediaTypes, Collection acceptableTypes) { + if (CollectionUtils.isEmpty(acceptableTypes)) { + return true; + } + for (MediaType acceptableType : acceptableTypes) { + if (acceptableType.equals(MediaType.ALL_TYPE) || producedMediaTypes.contains(acceptableType)) { + return true; + } + } + return false; + } + + @Override + public boolean explicitlyConsumes(MediaType contentType) { + return consumesMediaTypes.contains(contentType); + } + + @Override + public boolean explicitlyProduces(MediaType contentType) { + return producesMediaTypes == null || producesMediaTypes.isEmpty() || producesMediaTypes.contains(contentType); + } + + @Override + public boolean isSuspended() { + return suspended; + } + + @Override + public boolean isReactive() { + return reactive; + } + + @Override + public boolean isSingleResult() { + return single; + } + + @Override + public boolean isSpecifiedSingle() { + return specifiedSingle; + } + + @Override + public boolean isCompletable() { + return completable; + } + + @Override + public boolean isAsync() { + return async; + } + + @Override + public boolean isAsyncOrReactive() { + return asyncOrReactive; + } + + @Override + public boolean isVoid() { + return isVoid; + } + + @Override + public HttpStatus findStatus(HttpStatus defaultStatus) { + if (definedStatus != null) { + return definedStatus; + } + if (defaultStatus != null) { + return defaultStatus; + } + return HttpStatus.OK; + } + + @Override + public boolean isErrorRoute() { + return isErrorRoute; + } + + @Override + public boolean isWebSocketRoute() { + return isWebSocketRoute; + } + + @Override + public boolean isPermitsRequestBody() { + return isPermitsBody; + } + + @Override + public AnnotationMetadata getAnnotationMetadata() { + return annotationMetadata; + } +} diff --git a/router/src/main/java/io/micronaut/web/router/DefaultRouter.java b/router/src/main/java/io/micronaut/web/router/DefaultRouter.java index a06b41f0d85..30f1efdf016 100644 --- a/router/src/main/java/io/micronaut/web/router/DefaultRouter.java +++ b/router/src/main/java/io/micronaut/web/router/DefaultRouter.java @@ -45,13 +45,10 @@ import java.util.Collections; import java.util.HashMap; import java.util.HashSet; -import java.util.LinkedHashMap; -import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; -import java.util.function.Predicate; import java.util.function.Supplier; import java.util.stream.Stream; @@ -65,10 +62,12 @@ @Singleton public class DefaultRouter implements Router, HttpServerFilterResolver> { - private final Map> routesByMethod = new HashMap<>(); - private final List statusRoutes = new ArrayList<>(); - private final List errorRoutes = new ArrayList<>(); + private final Map>> routesByMethod = new HashMap<>(); + private final List> statusRoutes = new ArrayList<>(); + private final List> errorRoutes = new ArrayList<>(); private final Set exposedPorts; + @Nullable + private Set ports; private final List alwaysMatchesFilterRoutes = new ArrayList<>(); private final List preconditionFilterRoutes = new ArrayList<>(); private final Supplier> alwaysMatchesHttpFilters = SupplierUtil.memoized(() -> { @@ -105,22 +104,25 @@ public DefaultRouter(Collection builders) { List constructedRoutes = builder.getUriRoutes(); for (UriRoute route : constructedRoutes) { String key = route.getHttpMethodName(); - routesByMethod.computeIfAbsent(key, x -> new ArrayList<>()).add(route); + UriRouteInfo uriRouteInfo = route.toRouteInfo(); + routesByMethod.computeIfAbsent(key, x -> new ArrayList<>()).add(uriRouteInfo); } for (StatusRoute statusRoute : builder.getStatusRoutes()) { - if (statusRoutes.contains(statusRoute)) { - final StatusRoute existing = statusRoutes.stream().filter(r -> r.equals(statusRoute)).findFirst().orElse(null); + StatusRouteInfo routeInfo = statusRoute.toRouteInfo(); + if (statusRoutes.contains(routeInfo)) { + final StatusRouteInfo existing = statusRoutes.stream().filter(r -> r.equals(routeInfo)).findFirst().orElse(null); throw new RoutingException("Attempted to register multiple local routes for http status [" + statusRoute.status() + "]. New route: " + statusRoute + ". Existing: " + existing); } - this.statusRoutes.add(statusRoute); + this.statusRoutes.add(routeInfo); } for (ErrorRoute errorRoute : builder.getErrorRoutes()) { - if (errorRoutes.contains(errorRoute)) { - final ErrorRoute existing = errorRoutes.stream().filter(r -> r.equals(errorRoute)).findFirst().orElse(null); + ErrorRouteInfo routeInfo = errorRoute.toRouteInfo(); + if (errorRoutes.contains(routeInfo)) { + final ErrorRouteInfo existing = errorRoutes.stream().filter(r -> r.equals(routeInfo)).findFirst().orElse(null); throw new RoutingException("Attempted to register multiple local routes for error [" + errorRoute.exceptionType().getSimpleName() + "]. New route: " + errorRoute + ". Existing: " + existing); } - this.errorRoutes.add(errorRoute); + this.errorRoutes.add(routeInfo); } filterRoutes.addAll(builder.getFilterRoutes()); exposedPorts.addAll(builder.getExposedPorts()); @@ -166,67 +168,57 @@ public Set getExposedPorts() { @Override public void applyDefaultPorts(List ports) { - Predicate> portMatches = (httpRequest -> ports.contains(httpRequest.getServerAddress().getPort())); - for (List routes : routesByMethod.values()) { - for (int i = 0; i < routes.size(); i++) { - UriRoute route = routes.get(i); - if (route.getPort() == null) { - routes.set(i, route.where(portMatches)); - } - } - } + this.ports = new HashSet<>(ports); } @NonNull @Override public Stream> find(@NonNull HttpRequest request, @NonNull CharSequence uri) { - return this.find(request.getMethodName(), uri, null).stream(); + return this.toMatches(uri.toString(), findInternal(request)).stream(); } @NonNull @Override public Stream> find(@NonNull HttpRequest request) { - boolean permitsBody = HttpMethod.permitsRequestBody(request.getMethod()); - return this.find(request, request.getPath()) - .filter(match -> match.test(request) && (!permitsBody || match.doesConsume(request.getContentType().orElse(null)))); + return this.toMatches(request.getPath(), findInternal(request)).stream(); } @NonNull @Override public Stream> find(@NonNull HttpMethod httpMethod, @NonNull CharSequence uri, @Nullable HttpRequest context) { - return this.find(httpMethod.name(), uri, null).stream(); + return this.toMatches( + uri.toString(), + routesByMethod.getOrDefault(httpMethod.name(), Collections.emptyList()) + ).stream(); } @NonNull @Override - public Stream uriRoutes() { + public Stream> uriRoutes() { return routesByMethod.values().stream().flatMap(List::stream); } @NonNull @Override public List> findAllClosest(@NonNull HttpRequest request) { - final HttpMethod httpMethod = request.getMethod(); - final MediaType contentType = request.getContentType().orElse(null); - boolean permitsBody = HttpMethod.permitsRequestBody(httpMethod); - final Collection acceptedProducedTypes = request.accept(); - List> uriRoutes = this.find( - request.getMethodName(), - request.getPath(), - routeMatch -> routeMatch.test(request) && (!permitsBody || routeMatch.doesConsume(contentType)) && routeMatch.doesProduce(acceptedProducedTypes) - ); - int routeCount = uriRoutes.size(); - if (routeCount <= 1) { + List> routes = findInternal(request); + if (routes.isEmpty()) { + return Collections.emptyList(); + } + List> uriRoutes = toMatches(request.getPath(), routes); + if (routes.size() == 1) { return uriRoutes; } + // if there are multiple routes, try to resolve the ambiguity + final Collection acceptedProducedTypes = request.accept(); if (CollectionUtils.isNotEmpty(acceptedProducedTypes)) { // take the highest priority accepted type final MediaType mediaType = acceptedProducedTypes.iterator().next(); - List> mostSpecific = new ArrayList<>(uriRoutes.size()); + List> mostSpecific = new ArrayList<>(routes.size()); for (UriRouteMatch routeMatch : uriRoutes) { - if (routeMatch.explicitlyProduces(mediaType)) { + if (routeMatch.getRouteInfo().explicitlyProduces(mediaType)) { mostSpecific.add(routeMatch); } } @@ -234,17 +226,18 @@ public List> findAllClosest(@NonNull HttpRequest r uriRoutes = mostSpecific; } } - routeCount = uriRoutes.size(); + boolean permitsBody = HttpMethod.permitsRequestBody(request.getMethod()); + int routeCount = uriRoutes.size(); if (routeCount > 1 && permitsBody) { - + final MediaType contentType = request.getContentType().orElse(MediaType.ALL_TYPE); List> explicitlyConsumedRoutes = new ArrayList<>(routeCount); List> consumesRoutes = new ArrayList<>(routeCount); - for (UriRouteMatch match: uriRoutes) { - if (match.explicitlyConsumes(contentType != null ? contentType : MediaType.ALL_TYPE)) { + for (UriRouteMatch match : uriRoutes) { + if (match.getRouteInfo().explicitlyConsumes(contentType)) { explicitlyConsumedRoutes.add(match); } - if (explicitlyConsumedRoutes.isEmpty() && match.doesConsume(contentType)) { + if (explicitlyConsumedRoutes.isEmpty()) { consumesRoutes.add(match); } } @@ -264,7 +257,7 @@ public List> findAllClosest(@NonNull HttpRequest r for (int i = 0; i < routeCount; i++) { UriRouteMatch match = uriRoutes.get(i); - UriMatchTemplate template = match.getRoute().getUriMatchTemplate(); + UriMatchTemplate template = match.getRouteInfo().getUriMatchTemplate(); long variable = template.getPathVariableSegmentCount(); long raw = template.getRawSegmentLength(); if (i == 0) { @@ -282,12 +275,23 @@ public List> findAllClosest(@NonNull HttpRequest r return uriRoutes; } + private List> toMatches(String path, List> routes) { + List> uriRoutes = new ArrayList<>(routes.size()); + for (UriRouteInfo route : routes) { + Optional> match = route.match(path); + if (match.isPresent()) { + uriRoutes.add((UriRouteMatch) match.get()); + } + } + return uriRoutes; + } + @NonNull @Override public Optional> route(@NonNull HttpMethod httpMethod, @NonNull CharSequence uri) { - List routes = routesByMethod.getOrDefault(httpMethod.name(), Collections.emptyList()); - for (UriRoute uriRoute : routes) { - Optional match = uriRoute.match(uri.toString()); + List> routes = routesByMethod.getOrDefault(httpMethod.name(), Collections.emptyList()); + for (UriRouteInfo uriRouteInfo : routes) { + Optional> match = uriRouteInfo.match(uri.toString()); if (match.isPresent()) { return (Optional) match; } @@ -297,11 +301,11 @@ public Optional> route(@NonNull HttpMethod httpMethod @Override public Optional> route(@NonNull HttpStatus status) { - for (StatusRoute statusRoute : statusRoutes) { - if (statusRoute.originatingType() == null) { - Optional> match = statusRoute.match(status); + for (StatusRouteInfo statusRouteInfo : statusRoutes) { + if (statusRouteInfo.originatingType() == null) { + Optional> match = statusRouteInfo.match(status); if (match.isPresent()) { - return match; + return (Optional) match; } } } @@ -310,10 +314,10 @@ public Optional> route(@NonNull HttpStatus status) { @Override public Optional> route(@NonNull Class originatingClass, @NonNull HttpStatus status) { - for (StatusRoute statusRoute : statusRoutes) { - Optional> match = statusRoute.match(originatingClass, status); + for (StatusRouteInfo statusRouteInfo : statusRoutes) { + Optional> match = statusRouteInfo.match(originatingClass, status); if (match.isPresent()) { - return match; + return (Optional) match; } } return Optional.empty(); @@ -321,11 +325,11 @@ public Optional> route(@NonNull Class originatingClass, @No @Override public Optional> route(@NonNull Class originatingClass, @NonNull Throwable error) { - Map> matchedRoutes = new LinkedHashMap<>(); - for (ErrorRoute errorRoute : errorRoutes) { - Optional> match = errorRoute.match(originatingClass, error); + List> matchedRoutes = new ArrayList<>(); + for (ErrorRouteInfo errorRouteInfo : errorRoutes) { + Optional match = errorRouteInfo.match(originatingClass, error); match.ifPresent(m -> - matchedRoutes.put(errorRoute, m) + matchedRoutes.add((RouteMatch) m) ); } return findRouteMatch(matchedRoutes, error); @@ -345,29 +349,36 @@ private Optional> findErrorRouteInternal( Collection accept = request.accept(); final boolean hasAcceptHeader = CollectionUtils.isNotEmpty(accept); if (hasAcceptHeader) { - Map> matchedRoutes = new LinkedHashMap<>(); - for (ErrorRoute errorRoute : errorRoutes) { + List> matchedRoutes = new ArrayList<>(); + for (ErrorRouteInfo errorRoute : errorRoutes) { + if (!errorRoute.doesProduce(accept)) { + continue; + } + if (!errorRoute.matching(request)) { + continue; + } @SuppressWarnings("unchecked") - final RouteMatch match = (RouteMatch) errorRoute - .match(originatingClass, error).orElse(null); - if (match != null && match.doesProduce(accept)) { - matchedRoutes.put(errorRoute, match); + final RouteMatch match = (RouteMatch) errorRoute.match(originatingClass, error).orElse(null); + if (match != null) { + matchedRoutes.add(match); } } return findRouteMatch(matchedRoutes, error); } else { - Map> producesAllMatchedRoutes = new LinkedHashMap<>(); - Map> producesSpecificMatchedRoutes = new LinkedHashMap<>(); - for (ErrorRoute errorRoute : errorRoutes) { - @SuppressWarnings("unchecked") - final RouteMatch match = (RouteMatch) errorRoute + List> producesAllMatchedRoutes = new ArrayList<>(errorRoutes.size()); + List> producesSpecificMatchedRoutes = new ArrayList<>(errorRoutes.size()); + for (ErrorRouteInfo errorRouteInfo : errorRoutes) { + if (!errorRouteInfo.matching(request)) { + continue; + } + @SuppressWarnings("unchecked") final RouteMatch match = (RouteMatch) errorRouteInfo .match(originatingClass, error).orElse(null); if (match != null) { - final List produces = match.getProduces(); + final List produces = match.getRouteInfo().getProduces(); if (CollectionUtils.isEmpty(produces) || produces.contains(MediaType.ALL_TYPE)) { - producesAllMatchedRoutes.put(errorRoute, match); + producesAllMatchedRoutes.add(match); } else { - producesSpecificMatchedRoutes.put(errorRoute, match); + producesSpecificMatchedRoutes.add(match); } } } @@ -401,23 +412,29 @@ private Optional> findStatusInternal(@Nullable Class origin request.accept(); final boolean hasAcceptHeader = CollectionUtils.isNotEmpty(accept); if (hasAcceptHeader) { - - for (StatusRoute statusRoute : statusRoutes) { - @SuppressWarnings("unchecked") - final RouteMatch match = (RouteMatch) statusRoute + for (StatusRouteInfo statusRouteInfo : statusRoutes) { + if (!statusRouteInfo.doesProduce(accept)) { + continue; + } + if (!statusRouteInfo.matching(request)) { + continue; + } + @SuppressWarnings("unchecked") final RouteMatch match = (RouteMatch) statusRouteInfo .match(originatingClass, status).orElse(null); - if (match != null && match.doesProduce(accept)) { + if (match != null) { return Optional.of(match); } } } else { RouteMatch firstMatch = null; - for (StatusRoute errorRoute : statusRoutes) { - @SuppressWarnings("unchecked") - final RouteMatch match = (RouteMatch) errorRoute + for (StatusRouteInfo statusRouteInfo : statusRoutes) { + if (!statusRouteInfo.matching(request)) { + continue; + } + @SuppressWarnings("unchecked") final RouteMatch match = (RouteMatch) statusRouteInfo .match(originatingClass, status).orElse(null); if (match != null) { - final List produces = match.getProduces(); + final List produces = match.getRouteInfo().getProduces(); if (CollectionUtils.isEmpty(produces) || produces.contains(MediaType.ALL_TYPE)) { return Optional.of(match); } else if (firstMatch == null) { @@ -434,14 +451,13 @@ private Optional> findStatusInternal(@Nullable Class origin @Override public Optional> route(@NonNull Throwable error) { - Map> matchedRoutes = new LinkedHashMap<>(); - for (ErrorRoute errorRoute : errorRoutes) { - if (errorRoute.originatingType() == null) { - Optional> match = errorRoute.match(error); - match.ifPresent(m -> matchedRoutes.put(errorRoute, m)); + List> matchedRoutes = new ArrayList<>(); + for (ErrorRouteInfo errorRouteInfo : errorRoutes) { + if (errorRouteInfo.originatingType() == null) { + Optional match = errorRouteInfo.match(error); + match.ifPresent(m -> matchedRoutes.add((RouteMatch) m)); } } - return findRouteMatch(matchedRoutes, error); } @@ -471,13 +487,22 @@ public List findFilters(@NonNull HttpRequest request) { @SuppressWarnings("unchecked") @NonNull @Override - public Stream> findAny(@NonNull CharSequence uri, @Nullable HttpRequest context) { + public Stream> findAny(@NonNull CharSequence uri, @Nullable HttpRequest request) { List matchedRoutes = new ArrayList<>(5); final String uriStr = uri.toString(); - for (List routes : routesByMethod.values()) { - for (UriRoute route : routes) { - final UriRouteMatch match = route.match(uriStr).orElse(null); - if (match != null && match.test(context)) { + for (List> routes : routesByMethod.values()) { + for (UriRouteInfo route : routes) { + if (request != null) { + if (shouldSkipForPort(request, route)) { + continue; + } + if (!route.matching(request)) { + continue; + } + } + + final UriRouteMatch match = route.match(uriStr).orElse(null); + if (match != null) { matchedRoutes.add(match); } } @@ -485,33 +510,77 @@ public Stream> findAny(@NonNull CharSequence uri, @Nu return matchedRoutes.stream(); } - private List> find(String httpMethodName, CharSequence uri, @Nullable Predicate predicate) { - List routes = routesByMethod.getOrDefault(httpMethodName, Collections.emptyList()); - if (CollectionUtils.isNotEmpty(routes)) { - final String uriStr = uri.toString(); - List> routeMatches = new LinkedList<>(); - for (UriRoute route : routes) { - Optional match = route.match(uriStr); - if (predicate != null) { - match = match.filter(predicate); + @Override + public List> findAny(HttpRequest request) { + String path = request.getPath(); + List matchedRoutes = new ArrayList<>(5); + for (List> routes : routesByMethod.values()) { + for (UriRouteInfo route : routes) { + if (shouldSkipForPort(request, route)) { + continue; + } + if (!route.matching(request)) { + continue; + } + final UriRouteMatch match = route.match(path).orElse(null); + if (match != null) { + matchedRoutes.add(match); } - match.ifPresent(routeMatches::add); } - return routeMatches; - } else { - //noinspection unchecked + } + return matchedRoutes; + } + + private List> findInternal(HttpRequest request) { + String httpMethodName = request.getMethodName(); + boolean permitsBody = HttpMethod.permitsRequestBody(request.getMethod()); + final Collection acceptedProducedTypes = request.accept(); + List> routes = routesByMethod.getOrDefault(httpMethodName, Collections.emptyList()); + if (CollectionUtils.isEmpty(routes)) { return Collections.emptyList(); } + List> result = new ArrayList<>(routes.size()); + for (UriRouteInfo route : routes) { + if (shouldSkipForPort(request, route)) { + continue; + } + if (permitsBody) { + if (!route.isPermitsRequestBody()) { + continue; + } + if (!route.doesConsume(request.getContentType().orElse(null))) { + continue; + } + } + if (!route.doesProduce(acceptedProducedTypes)) { + continue; + } + if (!route.matching(request)) { + continue; + } + result.add(route); + } + return result; + } + + private boolean shouldSkipForPort(HttpRequest request, UriRouteInfo route) { + if (ports == null || route.getPort() != null) { + return false; + } + if (!ports.contains(request.getServerAddress().getPort())) { + return true; + } + return false; } - private UriRoute[] finalizeRoutes(List routes) { + private UriRouteInfo[] finalizeRoutes(List> routes) { Collections.sort(routes); - return routes.toArray(new UriRoute[0]); + return routes.toArray(new UriRouteInfo[0]); } - private Optional> findRouteMatch(Map> matchedRoutes, Throwable error) { + private Optional> findRouteMatch(List> matchedRoutes, Throwable error) { if (matchedRoutes.size() == 1) { - return matchedRoutes.values().stream().findFirst(); + return matchedRoutes.stream().findFirst(); } else if (matchedRoutes.size() > 1) { int minCount = Integer.MAX_VALUE; @@ -519,10 +588,11 @@ private Optional> findRouteMatch(Map Optional> match = Optional.empty(); Class errorClass = error.getClass(); - for (Map.Entry> entry: matchedRoutes.entrySet()) { - Class exceptionType = entry.getKey().exceptionType(); + for (RouteMatch errorMatch : matchedRoutes) { + ErrorRouteInfo routeInfo = (ErrorRouteInfo) errorMatch.getRouteInfo(); + Class exceptionType = routeInfo.exceptionType(); if (exceptionType.equals(errorClass)) { - match = Optional.of(entry.getValue()); + match = Optional.of(errorMatch); break; } else { List> hierarchy = hierarchySupplier.get(); @@ -531,7 +601,7 @@ private Optional> findRouteMatch(Map //the class closest in the hierarchy should be chosen if (index > -1 && index < minCount) { minCount = index; - match = Optional.of(entry.getValue()); + match = Optional.of(errorMatch); } } } @@ -568,8 +638,8 @@ public List resolveFilters(HttpRequest request, List cont if (!matches) { String filterAnnotation = annotationMetadata.getAnnotationNameByStereotype(FilterMatcher.NAME).orElse(null); if (filterAnnotation != null) { - matches = context.getAnnotationMetadata().hasStereotype(filterAnnotation); + matches = context.getRouteInfo().getAnnotationMetadata().hasStereotype(filterAnnotation); } } return matches; diff --git a/router/src/main/java/io/micronaut/web/router/DefaultStatusRouteInfo.java b/router/src/main/java/io/micronaut/web/router/DefaultStatusRouteInfo.java new file mode 100644 index 00000000000..0943906a0e3 --- /dev/null +++ b/router/src/main/java/io/micronaut/web/router/DefaultStatusRouteInfo.java @@ -0,0 +1,126 @@ +/* + * Copyright 2017-2023 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.web.router; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.Nullable; +import io.micronaut.core.convert.ConversionService; +import io.micronaut.core.type.Argument; +import io.micronaut.core.util.ObjectUtils; +import io.micronaut.http.HttpRequest; +import io.micronaut.http.HttpStatus; +import io.micronaut.http.MediaType; +import io.micronaut.inject.MethodExecutionHandle; + +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.function.Predicate; + +/** + * The default {@link StatusRouteInfo} implementation. + * + * @param The target + * @param The result + * @author Denis Stepanov + * @since 4.0.0 + */ +@Internal +public class DefaultStatusRouteInfo extends DefaultRequestMatcher implements StatusRouteInfo { + + private final Class originatingType; + private final HttpStatus status; + private final ConversionService conversionService; + + public DefaultStatusRouteInfo(Class originatingType, + HttpStatus status, + MethodExecutionHandle targetMethod, + @Nullable + String bodyArgumentName, + @Nullable + Argument bodyArgument, + List consumesMediaTypes, + List producesMediaTypes, + List>> predicates, + ConversionService conversionService) { + super(targetMethod, bodyArgument, bodyArgumentName, consumesMediaTypes, producesMediaTypes, true, true, predicates); + this.originatingType = originatingType; + this.status = status; + this.conversionService = conversionService; + } + + @Override + public Class originatingType() { + return originatingType; + } + + @Override + public HttpStatus status() { + return status; + } + + @Override + public HttpStatus findStatus(HttpStatus defaultStatus) { + return super.findStatus(status); + } + + @Override + public Optional> match(Class originatingClass, HttpStatus status) { + if (originatingClass == this.originatingType && this.status == status) { + return Optional.of(new StatusRouteMatch<>(this, conversionService)); + } + return Optional.empty(); + } + + @Override + public Optional> match(HttpStatus status) { + if (this.originatingType == null && this.status == status) { + return Optional.of(new StatusRouteMatch<>(this, conversionService)); + } + return Optional.empty(); + } + + @Override + public int hashCode() { + return ObjectUtils.hash(super.hashCode(), status, originatingType); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof DefaultStatusRouteInfo that)) { + return false; + } + if (!super.equals(o)) { + return false; + } + return status == that.status && + Objects.equals(originatingType, that.originatingType); + } + + @Override + public String toString() { + return new StringBuilder().append(' ') + .append(status) + .append(" -> ") + .append(getTargetMethod().getDeclaringType().getSimpleName()) + .append('#') + .append(getTargetMethod()) + .toString(); + } +} diff --git a/router/src/main/java/io/micronaut/web/router/DefaultUriRouteMatch.java b/router/src/main/java/io/micronaut/web/router/DefaultUriRouteMatch.java index a0c0bb0324f..5a389594263 100644 --- a/router/src/main/java/io/micronaut/web/router/DefaultUriRouteMatch.java +++ b/router/src/main/java/io/micronaut/web/router/DefaultUriRouteMatch.java @@ -17,7 +17,6 @@ import io.micronaut.core.annotation.Internal; import io.micronaut.core.convert.ConversionService; -import io.micronaut.core.type.Argument; import io.micronaut.core.util.CollectionUtils; import io.micronaut.http.HttpMethod; import io.micronaut.http.uri.UriMatchInfo; @@ -26,10 +25,8 @@ import java.io.UnsupportedEncodingException; import java.net.URLDecoder; import java.nio.charset.Charset; -import java.util.LinkedHashMap; import java.util.List; import java.util.Map; -import java.util.function.Function; /** * Default implementation of the {@link RouteMatch} interface for matches to URIs. @@ -42,72 +39,26 @@ @Internal class DefaultUriRouteMatch extends AbstractRouteMatch implements UriRouteMatch { - private final HttpMethod httpMethod; private final UriMatchInfo matchInfo; - private final DefaultRouteBuilder.DefaultUriRoute uriRoute; + private final UriRouteInfo uriRouteInfo; private final Charset defaultCharset; /** * @param matchInfo The URI match info - * @param uriRoute The URI route + * @param routeInfo The URI route * @param defaultCharset The default charset * @param conversionService The conversion service */ DefaultUriRouteMatch(UriMatchInfo matchInfo, - DefaultRouteBuilder.DefaultUriRoute uriRoute, + UriRouteInfo routeInfo, Charset defaultCharset, ConversionService conversionService ) { - super(uriRoute, conversionService); - this.uriRoute = uriRoute; + super(routeInfo, conversionService); this.matchInfo = matchInfo; - this.httpMethod = uriRoute.httpMethod; + this.uriRouteInfo = routeInfo; this.defaultCharset = defaultCharset; } - @Override - public UriRouteMatch decorate(Function, R> executor) { - Map variables = getVariableValues(); - List> arguments = getRequiredArguments(); - RouteMatch thisRoute = this; - return new DefaultUriRouteMatch<>(matchInfo, uriRoute, defaultCharset, conversionService) { - @Override - public List> getRequiredArguments() { - return arguments; - } - - @Override - public R execute(Map argumentValues) { - return executor.apply(thisRoute); - } - - @Override - public Map getVariableValues() { - return variables; - } - }; - } - - @Override - protected RouteMatch newFulfilled(Map newVariables, List> requiredArguments) { - return new DefaultUriRouteMatch(matchInfo, uriRoute, defaultCharset, conversionService) { - - @Override - public List> getRequiredArguments() { - return requiredArguments; - } - - @Override - public Map getVariableValues() { - return newVariables; - } - }; - } - - @Override - public UriRouteMatch fulfill(Map argumentValues) { - return (UriRouteMatch) super.fulfill(argumentValues); - } - @Override public String getUri() { return matchInfo.getUri(); @@ -118,7 +69,7 @@ public Map getVariableValues() { Map variables = matchInfo.getVariableValues(); if (CollectionUtils.isNotEmpty(variables)) { final String charset = defaultCharset.toString(); - Map decoded = new LinkedHashMap<>(variables.size()); + Map decoded = CollectionUtils.newLinkedHashMap(variables.size()); variables.forEach((k, v) -> { if (v instanceof CharSequence) { try { @@ -145,17 +96,17 @@ public Map getVariableMap() { } @Override - public UriRoute getRoute() { - return (UriRoute) abstractRoute; + public UriRouteInfo getRouteInfo() { + return uriRouteInfo; } @Override public HttpMethod getHttpMethod() { - return httpMethod; + return uriRouteInfo.getHttpMethod(); } @Override public String toString() { - return httpMethod + " - " + matchInfo.getUri(); + return uriRouteInfo.getHttpMethod() + " - " + matchInfo.getUri(); } } diff --git a/router/src/main/java/io/micronaut/web/router/DefaultUrlRouteInfo.java b/router/src/main/java/io/micronaut/web/router/DefaultUrlRouteInfo.java new file mode 100644 index 00000000000..deb38a2d272 --- /dev/null +++ b/router/src/main/java/io/micronaut/web/router/DefaultUrlRouteInfo.java @@ -0,0 +1,109 @@ +/* + * Copyright 2017-2023 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.web.router; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.Nullable; +import io.micronaut.core.convert.ConversionService; +import io.micronaut.core.type.Argument; +import io.micronaut.http.HttpMethod; +import io.micronaut.http.HttpRequest; +import io.micronaut.http.MediaType; +import io.micronaut.http.uri.UriMatchInfo; +import io.micronaut.http.uri.UriMatchTemplate; +import io.micronaut.inject.MethodExecutionHandle; + +import java.nio.charset.Charset; +import java.util.List; +import java.util.Optional; +import java.util.function.Predicate; + +/** + * The default {@link UriRouteInfo} implementation. + * + * @param The target + * @param The result + * @author Denis Stepanov + * @since 4.0.0 + */ +@Internal +public final class DefaultUrlRouteInfo extends DefaultRequestMatcher implements UriRouteInfo { + + private final HttpMethod httpMethod; + private final UriMatchTemplate uriMatchTemplate; + private final Charset defaultCharset; + private final Integer port; + private final ConversionService conversionService; + + public DefaultUrlRouteInfo(HttpMethod httpMethod, + UriMatchTemplate uriMatchTemplate, + Charset defaultCharset, + MethodExecutionHandle targetMethod, + @Nullable String bodyArgumentName, + @Nullable Argument bodyArgument, + List consumesMediaTypes, + List producesMediaTypes, + List>> predicates, + Integer port, + ConversionService conversionService) { + super(targetMethod, bodyArgument, bodyArgumentName, consumesMediaTypes, producesMediaTypes, HttpMethod.permitsRequestBody(httpMethod), false, predicates); + this.httpMethod = httpMethod; + this.uriMatchTemplate = uriMatchTemplate; + this.defaultCharset = defaultCharset; + this.port = port; + this.conversionService = conversionService; + } + + @Override + public HttpMethod getHttpMethod() { + return httpMethod; + } + + @Override + public UriMatchTemplate getUriMatchTemplate() { + return uriMatchTemplate; + } + + @Override + public Optional> match(String uri) { + Optional matchInfo = uriMatchTemplate.match(uri); + return matchInfo.map(info -> new DefaultUriRouteMatch<>(info, this, defaultCharset, conversionService)); + } + + @Override + public Integer getPort() { + return port; + } + + @Override + public int compareTo(UriRouteInfo o) { + return uriMatchTemplate.compareTo(o.getUriMatchTemplate()); + } + + @Override + public String toString() { + return new StringBuilder(getHttpMethodName()).append(' ') + .append(uriMatchTemplate) + .append(" -> ") + .append(getTargetMethod().getDeclaringType().getSimpleName()) + .append('#') + .append(getTargetMethod().getName()) + .append(" (") + .append(String.join(",", consumesMediaTypes)) + .append(")") + .toString(); + } +} diff --git a/router/src/main/java/io/micronaut/web/router/ErrorRoute.java b/router/src/main/java/io/micronaut/web/router/ErrorRoute.java index 2f3edb7743d..86c1ad1fc94 100644 --- a/router/src/main/java/io/micronaut/web/router/ErrorRoute.java +++ b/router/src/main/java/io/micronaut/web/router/ErrorRoute.java @@ -15,10 +15,10 @@ */ package io.micronaut.web.router; +import io.micronaut.core.annotation.Nullable; import io.micronaut.http.HttpRequest; import io.micronaut.http.MediaType; -import java.util.Optional; import java.util.function.Predicate; /** @@ -27,11 +27,15 @@ * @author Graeme Rocher * @since 1.0 */ -public interface ErrorRoute extends MethodBasedRoute { +public interface ErrorRoute extends Route { + + @Override + ErrorRouteInfo toRouteInfo(); /** * @return The type the exception originates from. Null if the error route is global. */ + @Nullable Class originatingType(); /** @@ -39,25 +43,6 @@ public interface ErrorRoute extends MethodBasedRoute { */ Class exceptionType(); - /** - * Match the given exception. - * - * @param exception The exception to match - * @param The type - * @return The route match - */ - Optional> match(Throwable exception); - - /** - * Match the given exception. - * - * @param originatingClass The class where the error originates from - * @param exception The exception to match - * @param The type - * @return The route match - */ - Optional> match(Class originatingClass, Throwable exception); - @Override ErrorRoute consumes(MediaType... mediaType); diff --git a/router/src/main/java/io/micronaut/web/router/ErrorRouteInfo.java b/router/src/main/java/io/micronaut/web/router/ErrorRouteInfo.java new file mode 100644 index 00000000000..2fb59bde6f2 --- /dev/null +++ b/router/src/main/java/io/micronaut/web/router/ErrorRouteInfo.java @@ -0,0 +1,56 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.web.router; + +import java.util.Optional; + +/** + * Represents a {@link Route} that matches an exception. + * @param The target + * @param The result + * @author Graeme Rocher + * @since 1.0 + */ +public interface ErrorRouteInfo extends MethodBasedRouteInfo, RequestMatcher { + + /** + * @return The type the exception originates from. Null if the error route is global. + */ + Class originatingType(); + + /** + * @return The type of exception + */ + Class exceptionType(); + + /** + * Match the given exception. + * + * @param exception The exception to match + * @return The route match + */ + Optional> match(Throwable exception); + + /** + * Match the given exception. + * + * @param originatingClass The class where the error originates from + * @param exception The exception to match + * @return The route match + */ + Optional> match(Class originatingClass, Throwable exception); + +} diff --git a/router/src/main/java/io/micronaut/web/router/ErrorRouteMatch.java b/router/src/main/java/io/micronaut/web/router/ErrorRouteMatch.java index dcc0e20a2e8..5cb6792fffb 100644 --- a/router/src/main/java/io/micronaut/web/router/ErrorRouteMatch.java +++ b/router/src/main/java/io/micronaut/web/router/ErrorRouteMatch.java @@ -24,7 +24,6 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; -import java.util.function.Function; /** * Represents a match for an error. @@ -41,12 +40,12 @@ class ErrorRouteMatch extends AbstractRouteMatch { private final Map variables; /** - * @param error The throwable - * @param abstractRoute The abstract route + * @param error The throwable + * @param routeInfo The route info * @param conversionService The conversion service */ - ErrorRouteMatch(Throwable error, DefaultRouteBuilder.AbstractRoute abstractRoute, ConversionService conversionService) { - super(abstractRoute, conversionService); + ErrorRouteMatch(Throwable error, ErrorRouteInfo routeInfo, ConversionService conversionService) { + super(routeInfo, conversionService); this.error = error; this.variables = new LinkedHashMap<>(); for (Argument argument : getArguments()) { @@ -73,51 +72,8 @@ public Map getVariableValues() { return variables; } - @Override - public boolean isErrorRoute() { - return true; - } - - @Override - protected RouteMatch newFulfilled(Map newVariables, List> requiredArguments) { - return new ErrorRouteMatch(error, abstractRoute, conversionService) { - @Override - public Collection> getRequiredArguments() { - return requiredArguments; - } - - @Override - public Map getVariableValues() { - return newVariables; - } - }; - } - - @Override - public RouteMatch decorate(Function, R> executor) { - Map variables = getVariableValues(); - Collection> arguments = getRequiredArguments(); - RouteMatch thisRoute = this; - return new ErrorRouteMatch<>(error, abstractRoute, conversionService) { - @Override - public Collection> getRequiredArguments() { - return arguments; - } - - @Override - public R execute(Map argumentValues) { - return executor.apply(thisRoute); - } - - @Override - public Map getVariableValues() { - return variables; - } - }; - } - @Override public String toString() { - return abstractRoute.toString(); + return routeInfo.toString(); } } diff --git a/router/src/main/java/io/micronaut/web/router/MethodBasedRoute.java b/router/src/main/java/io/micronaut/web/router/MethodBasedRouteInfo.java similarity index 60% rename from router/src/main/java/io/micronaut/web/router/MethodBasedRoute.java rename to router/src/main/java/io/micronaut/web/router/MethodBasedRouteInfo.java index c776ef75bb4..35920f634e8 100644 --- a/router/src/main/java/io/micronaut/web/router/MethodBasedRoute.java +++ b/router/src/main/java/io/micronaut/web/router/MethodBasedRouteInfo.java @@ -15,18 +15,29 @@ */ package io.micronaut.web.router; +import io.micronaut.core.annotation.NonNull; +import io.micronaut.http.bind.RequestBinderRegistry; +import io.micronaut.http.bind.binders.RequestArgumentBinder; import io.micronaut.inject.MethodExecutionHandle; /** - * Represents a {@link Route} that is backed by a method. + * Represents a route that is backed by a method. * + * @param The target + * @param The result * @author James Kleeh * @since 1.0 */ -public interface MethodBasedRoute extends Route { +public interface MethodBasedRouteInfo extends RouteInfo { /** * @return The {@link MethodExecutionHandle} */ - MethodExecutionHandle getTargetMethod(); + MethodExecutionHandle getTargetMethod(); + + @NonNull + String[] getArgumentNames(); + + RequestArgumentBinder[] resolveArgumentBinders(RequestBinderRegistry requestBinderRegistry); + } diff --git a/router/src/main/java/io/micronaut/web/router/NullArgument.java b/router/src/main/java/io/micronaut/web/router/RequestMatcher.java similarity index 67% rename from router/src/main/java/io/micronaut/web/router/NullArgument.java rename to router/src/main/java/io/micronaut/web/router/RequestMatcher.java index 6f3031477dc..de3866f5061 100644 --- a/router/src/main/java/io/micronaut/web/router/NullArgument.java +++ b/router/src/main/java/io/micronaut/web/router/RequestMatcher.java @@ -15,20 +15,22 @@ */ package io.micronaut.web.router; +import io.micronaut.http.HttpRequest; + /** - * Represents an argument with a null value. + * Route with a request predicate. * - * @author James Kleeh - * @since 1.3.1 + * @author Denis Stepanov + * @since 4.0.0 */ -public final class NullArgument { +public interface RequestMatcher { /** - * The NullArgument instance. + * Match the given request. + * + * @param httpRequest The request + * @return true if route matches this request */ - public static final NullArgument INSTANCE = new NullArgument(); - - private NullArgument() { + boolean matching(HttpRequest httpRequest); - } } diff --git a/router/src/main/java/io/micronaut/web/router/Route.java b/router/src/main/java/io/micronaut/web/router/Route.java index 7d6724284d0..3c03e4545ea 100644 --- a/router/src/main/java/io/micronaut/web/router/Route.java +++ b/router/src/main/java/io/micronaut/web/router/Route.java @@ -38,6 +38,8 @@ public interface Route { */ List DEFAULT_PRODUCES = Collections.singletonList(MediaType.APPLICATION_JSON_TYPE); + RouteInfo toRouteInfo(); + /** * Applies the given accepted media type the route. * diff --git a/router/src/main/java/io/micronaut/web/router/RouteInfo.java b/router/src/main/java/io/micronaut/web/router/RouteInfo.java index e7ba7c2dad0..8292a95e6b6 100644 --- a/router/src/main/java/io/micronaut/web/router/RouteInfo.java +++ b/router/src/main/java/io/micronaut/web/router/RouteInfo.java @@ -15,21 +15,18 @@ */ package io.micronaut.web.router; +import io.micronaut.core.annotation.AnnotationMetadata; import io.micronaut.core.annotation.AnnotationMetadataProvider; +import io.micronaut.core.annotation.Internal; import io.micronaut.core.annotation.NonNull; +import io.micronaut.core.annotation.Nullable; import io.micronaut.core.type.Argument; import io.micronaut.core.type.ReturnType; -import io.micronaut.core.util.ArrayUtils; -import io.micronaut.http.HttpResponse; import io.micronaut.http.HttpStatus; import io.micronaut.http.MediaType; -import io.micronaut.http.annotation.Consumes; -import io.micronaut.http.annotation.Produces; -import io.micronaut.http.annotation.Status; -import io.micronaut.http.sse.Event; -import io.micronaut.inject.beans.KotlinExecutableMethodUtils; +import io.micronaut.http.annotation.Body; -import java.util.Arrays; +import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Optional; @@ -37,12 +34,17 @@ /** * Common information shared between route and route match. * - * @param The route + * @param The result * @author Graeme Rocher * @since 1.0 */ public interface RouteInfo extends AnnotationMetadataProvider { + /** + * The default media type produced by routes. + */ + List DEFAULT_PRODUCES = Collections.singletonList(MediaType.APPLICATION_JSON_TYPE); + /** * @return The return type */ @@ -51,22 +53,37 @@ public interface RouteInfo extends AnnotationMetadataProvider { /** * @return The argument representing the data type being produced. */ - default Argument getBodyType() { - final ReturnType returnType = getReturnType(); - if (returnType.isAsyncOrReactive()) { - Argument reactiveType = returnType.getFirstTypeVariable().orElse(Argument.OBJECT_ARGUMENT); - if (HttpResponse.class.isAssignableFrom(reactiveType.getType())) { - reactiveType = reactiveType.getFirstTypeVariable().orElse(Argument.OBJECT_ARGUMENT); - } - return reactiveType; - } else if (HttpResponse.class.isAssignableFrom(returnType.getType())) { - Argument responseType = returnType.getFirstTypeVariable().orElse(Argument.OBJECT_ARGUMENT); - if (responseType.isAsyncOrReactive()) { - return responseType.getFirstTypeVariable().orElse(Argument.OBJECT_ARGUMENT); - } - return responseType; - } - return returnType.asArgument(); + Argument getBodyType(); + + /** + * @return The argument that represents the body + */ + Optional> getBodyArgument(); + + /** + * Like {@link #getBodyArgument()}, but excludes body arguments that may match only a part of + * the body (i.e. that have no {@code @Body} annotation, or where the {@code @Body} has a value + * set). + * + * @return The argument that represents the body + */ + @Internal + default Optional> getFullBodyArgument() { + return getBodyArgument() + /* + The getBodyArgument() method returns arguments for functions where it is + not possible to dictate whether the argument is supposed to bind the entire + body or just a part of the body. We check to ensure the argument has the body + annotation to exclude that use case + */ + .filter(argument -> { + AnnotationMetadata annotationMetadata = argument.getAnnotationMetadata(); + if (annotationMetadata.hasAnnotation(Body.class)) { + return annotationMetadata.stringValue(Body.class).isEmpty(); + } else { + return false; + } + }); } /** @@ -79,113 +96,109 @@ default Argument getBodyType() { * * @return A list of {@link MediaType} that this route can produce */ - default List getProduces() { - MediaType[] types = MediaType.of(getAnnotationMetadata().stringValues(Produces.class)); - Optional> firstTypeVariable = getReturnType().getFirstTypeVariable(); - if (firstTypeVariable.isPresent() && Event.class.isAssignableFrom(firstTypeVariable.get().getType())) { - return Collections.singletonList(MediaType.TEXT_EVENT_STREAM_TYPE); - } else if (ArrayUtils.isNotEmpty(types)) { - return Collections.unmodifiableList(Arrays.asList(types)); - } else { - return Route.DEFAULT_PRODUCES; - } - } + List getProduces(); /** * The media types able to produced by this route. * * @return A list of {@link MediaType} that this route can produce */ - default List getConsumes() { - MediaType[] types = MediaType.of(getAnnotationMetadata().stringValues(Consumes.class)); - if (ArrayUtils.isNotEmpty(types)) { - return Collections.unmodifiableList(Arrays.asList(types)); - } else { - return Collections.emptyList(); - } - } + List getConsumes(); + + /** + * Whether the specified content type is an accepted type. + * + * @param contentType The content type + * @return True if it is + */ + boolean doesConsume(@Nullable MediaType contentType); + + /** + * Whether the route does produce any of the given types. + * + * @param acceptableTypes The acceptable types + * @return True if it is + */ + boolean doesProduce(@Nullable Collection acceptableTypes); + + /** + * Whether the route does produce any of the given types. + * + * @param acceptableType The acceptable type + * @return True if it is + */ + boolean doesProduce(@Nullable MediaType acceptableType); + + /** + * Whether the specified content type is explicitly an accepted type. + * + * @param contentType The content type + * @return True if it is + */ + boolean explicitlyConsumes(@Nullable MediaType contentType); + + /** + * Whether the specified content type is explicitly a producing type. + * + * @param contentType The content type + * @return True if it is + * @since 2.5.0 + */ + boolean explicitlyProduces(@Nullable MediaType contentType); /** * @return Is this route match a suspended function (Kotlin). * @since 2.0.0 */ - default boolean isSuspended() { - return getReturnType().isSuspended(); - } + boolean isSuspended(); /** * @return Is the route a reactive route. * @since 2.0.0 */ - default boolean isReactive() { - return getReturnType().isReactive(); - } + boolean isReactive(); /** * @return Does the route emit a single result or multiple results * @since 2.0 */ - default boolean isSingleResult() { - ReturnType returnType = getReturnType(); - return returnType.isSingleResult() || - (isReactive() && returnType.getFirstTypeVariable() - .filter(t -> HttpResponse.class.isAssignableFrom(t.getType())).isPresent()) || - returnType.isAsync() || - returnType.isSuspended(); - } + boolean isSingleResult(); /** * @return Does the route emit a single result or multiple results * @since 2.0 */ - default boolean isSpecifiedSingle() { - return getReturnType().isSpecifiedSingle(); - } + boolean isSpecifiedSingle(); /** * @return is the return type completable * @since 2.0 */ - default boolean isCompletable() { - return getReturnType().isCompletable(); - } + boolean isCompletable(); /** * @return Is the route an async route. * @since 2.0.0 */ - default boolean isAsync() { - return getReturnType().isAsync(); - } + boolean isAsync(); /** * @return Is the route an async or reactive route. * @since 2.0.0 */ - default boolean isAsyncOrReactive() { - return getReturnType().isAsyncOrReactive(); - } + boolean isAsyncOrReactive(); /** * @return Does the route return void * @since 2.0.0 */ - default boolean isVoid() { - if (getReturnType().isVoid()) { - return true; - } else if (this instanceof MethodBasedRouteMatch && isSuspended()) { - return KotlinExecutableMethodUtils.isKotlinFunctionReturnTypeUnit(((MethodBasedRouteMatch) this).getExecutableMethod()); - } - return false; - } + boolean isVoid(); /** * @return True if the route was called due to an error * @since 3.0.0 */ - default boolean isErrorRoute() { - return false; - } + boolean isErrorRoute(); /** * Finds predefined route http status or uses default. @@ -195,9 +208,7 @@ default boolean isErrorRoute() { * @since 2.5.2 */ @NonNull - default HttpStatus findStatus(HttpStatus defaultStatus) { - return getAnnotationMetadata().enumValue(Status.class, HttpStatus.class).orElse(defaultStatus); - } + HttpStatus findStatus(HttpStatus defaultStatus); /** * Checks if route is for web socket. @@ -205,7 +216,13 @@ default HttpStatus findStatus(HttpStatus defaultStatus) { * @return true if it's web socket route * @since 2.5.2 */ - default boolean isWebSocketRoute() { - return getAnnotationMetadata().hasAnnotation("io.micronaut.websocket.annotation.OnMessage"); - } + boolean isWebSocketRoute(); + + /** + * Whether the route permits a request body. + * @return True if the route permits a request body + * @since 4.0.0 + */ + boolean isPermitsRequestBody(); + } diff --git a/router/src/main/java/io/micronaut/web/router/RouteMatch.java b/router/src/main/java/io/micronaut/web/router/RouteMatch.java index 0416f2d6bee..1c87c89add9 100644 --- a/router/src/main/java/io/micronaut/web/router/RouteMatch.java +++ b/router/src/main/java/io/micronaut/web/router/RouteMatch.java @@ -15,16 +15,16 @@ */ package io.micronaut.web.router; -import io.micronaut.core.annotation.Nullable; +import io.micronaut.core.annotation.AnnotationMetadataProvider; import io.micronaut.core.type.Argument; -import io.micronaut.core.type.ReturnType; import io.micronaut.http.HttpRequest; -import io.micronaut.http.MediaType; +import io.micronaut.http.bind.RequestBinderRegistry; -import java.util.*; +import java.util.Collection; +import java.util.Collections; +import java.util.Map; +import java.util.Optional; import java.util.concurrent.Callable; -import java.util.function.Function; -import java.util.function.Predicate; /** * A {@link Route} that is executable. @@ -33,58 +33,57 @@ * @author Graeme Rocher * @since 1.0 */ -public interface RouteMatch extends Callable, Predicate, RouteInfo { +public interface RouteMatch extends Callable, AnnotationMetadataProvider { /** - * @return The variable values following a successful match. + * @return The route info */ - Map getVariableValues(); + RouteInfo getRouteInfo(); /** - * Execute the route with the given values. The passed map should contain values for every argument returned by - * {@link #getRequiredArguments()}. - * - * @param argumentValues The argument values - * @return The result + * @return The variable values following a successful match. */ - R execute(Map argumentValues); + Map getVariableValues(); /** - * Returns a new {@link RouteMatch} fulfilling arguments required by this route to execute. The new route will not - * return the given arguments from the {@link #getRequiredArguments()} method. + * Fulfill argument values. * * @param argumentValues The argument values - * @return The fulfilled route */ - RouteMatch fulfill(Map argumentValues); + void fulfill(Map argumentValues); /** - * Decorates the execution of the route with the given executor. + * Attempt to satisfy the arguments of the given route with the data from the given request. * - * @param executor The executor - * @return A new route match + * @param requestBinderRegistry The request binder registry + * @param request The request + * @since 4.0.0 */ - RouteMatch decorate(Function, R> executor); + void fulfillBeforeFilters(RequestBinderRegistry requestBinderRegistry, HttpRequest request); /** - * Return whether the given named input is required by this route. + * Attempt to satisfy the arguments of the given route with the data from the given request. * - * @param name The name of the input - * @return True if it is + * @param requestBinderRegistry The request binder registry + * @param request The request + * @since 4.0.0 */ - Optional> getRequiredInput(String name); + void fulfillAfterFilters(RequestBinderRegistry requestBinderRegistry, HttpRequest request); /** - * @return The argument that represents the body + * @return Whether the route match can be executed without passing any additional arguments ie. via + * {@link #execute()} + * @since 4.0.0 */ - Optional> getBodyArgument(); + boolean isFulfilled(); /** - * The media types able to produced by this route. + * Return whether the given named input is required by this route. * - * @return A list of {@link MediaType} that this route can produce + * @param name The name of the input + * @return True if it is */ - List getProduces(); + Optional> getRequiredInput(String name); /** *

Returns the required arguments for this RouteMatch.

@@ -95,21 +94,13 @@ default Collection> getRequiredArguments() { return Collections.emptyList(); } - /** - * @return The return type - */ - @Override - ReturnType getReturnType(); - /** * Execute the route with the given values. Note if there are required arguments returned from * {@link #getRequiredArguments()} this method will throw an {@link IllegalArgumentException}. * * @return The result */ - default R execute() { - return execute(Collections.emptyMap()); - } + R execute(); /** * Same as {@link #execute()}. @@ -122,78 +113,12 @@ default R call() throws Exception { return execute(); } - /** - * @return Whether the route match can be executed without passing any additional arguments ie. via - * {@link #execute()} - */ - default boolean isExecutable() { - return getRequiredArguments().isEmpty(); - } - - /** - * Return whether the given named input is required by this route. - * - * @param name The name of the input - * @return True if it is - */ - default boolean isRequiredInput(String name) { - return getRequiredInput(name).isPresent(); - } - - /** - * Whether the specified content type is an accepted type. - * - * @param contentType The content type - * @return True if it is - */ - boolean doesConsume(@Nullable MediaType contentType); - - /** - * Whether the route does produce any of the given types. - * - * @param acceptableTypes The acceptable types - * @return True if it is - */ - boolean doesProduce(@Nullable Collection acceptableTypes); - - /** - * Whether the route does produce any of the given types. - * - * @param acceptableType The acceptable type - * @return True if it is - */ - boolean doesProduce(@Nullable MediaType acceptableType); - - /** - * Whether the specified content type is explicitly an accepted type. - * - * @param contentType The content type - * @return True if it is - */ - default boolean explicitlyConsumes(@Nullable MediaType contentType) { - return false; - } - - /** - * Whether the specified content type is explicitly a producing type. - * - * @param contentType The content type - * @return True if it is - * @since 2.5.0 - */ - default boolean explicitlyProduces(@Nullable MediaType contentType) { - return false; - } - /** * Is the given input satisfied. * * @param name The name of the input * @return True if it is */ - default boolean isSatisfied(String name) { - Object val = getVariableValues().get(name); - return val != null && !(val instanceof UnresolvedArgument); - } + boolean isSatisfied(String name); } diff --git a/router/src/main/java/io/micronaut/web/router/Router.java b/router/src/main/java/io/micronaut/web/router/Router.java index 1a5b7eb444d..87a166eadc8 100644 --- a/router/src/main/java/io/micronaut/web/router/Router.java +++ b/router/src/main/java/io/micronaut/web/router/Router.java @@ -46,7 +46,20 @@ public interface Router { * @param The return type * @return A stream of route matches */ - @NonNull Stream> findAny(@NonNull CharSequence uri, @Nullable HttpRequest context); + @NonNull + Stream> findAny(@NonNull CharSequence uri, @Nullable HttpRequest context); + + /** + * Find any {@link RouteMatch} regardless of HTTP method. + * + * @param request The request + * @param The target type + * @param The return type + * @return A stream of route matches + * @since 4.0.0 + */ + @NonNull + List> findAny(@NonNull HttpRequest request); /** * @return The exposed ports. @@ -83,8 +96,8 @@ public interface Router { * @param The URI route match * @return A {@link Stream} of possible {@link Route} instances. */ - default @NonNull - Stream> find(@NonNull HttpMethod httpMethod, @NonNull URI uri, @Nullable HttpRequest context) { + @NonNull + default Stream> find(@NonNull HttpMethod httpMethod, @NonNull URI uri, @Nullable HttpRequest context) { return find(httpMethod, uri.toString(), context); } @@ -96,8 +109,8 @@ Stream> find(@NonNull HttpMethod httpMethod, @NonNull * @param The URI route match * @return A {@link Stream} of possible {@link Route} instances. */ - default @NonNull - Stream> find(@NonNull HttpRequest request) { + @NonNull + default Stream> find(@NonNull HttpRequest request) { return find(request, request.getPath()); } @@ -110,7 +123,8 @@ Stream> find(@NonNull HttpRequest request) { * @param The type of what * @return A {@link Stream} of possible {@link Route} instances. */ - default @NonNull Stream> find(@NonNull HttpRequest request, @NonNull CharSequence uri) { + @NonNull + default Stream> find(@NonNull HttpRequest request, @NonNull CharSequence uri) { return find(HttpMethod.valueOf(request.getMethodName()), uri, request); } @@ -123,14 +137,16 @@ Stream> find(@NonNull HttpRequest request) { * @return A {@link List} of possible {@link Route} instances. * @since 1.2.1 */ - @NonNull List> findAllClosest(@NonNull HttpRequest request); + @NonNull + List> findAllClosest(@NonNull HttpRequest request); /** * Returns all UriRoutes. * * @return A {@link Stream} of all registered {@link UriRoute} instances. */ - @NonNull Stream uriRoutes(); + @NonNull + Stream> uriRoutes(); /** * Finds the first possible route for the given HTTP method and URI. diff --git a/router/src/main/java/io/micronaut/web/router/StatusRoute.java b/router/src/main/java/io/micronaut/web/router/StatusRoute.java index 19a67ddca1b..24e9278dd47 100644 --- a/router/src/main/java/io/micronaut/web/router/StatusRoute.java +++ b/router/src/main/java/io/micronaut/web/router/StatusRoute.java @@ -15,12 +15,11 @@ */ package io.micronaut.web.router; +import io.micronaut.core.annotation.Nullable; import io.micronaut.http.HttpRequest; import io.micronaut.http.HttpStatus; import io.micronaut.http.MediaType; -import io.micronaut.core.annotation.Nullable; -import java.util.Optional; import java.util.function.Predicate; /** @@ -29,7 +28,11 @@ * @author Graeme Rocher * @since 1.0 */ -public interface StatusRoute extends MethodBasedRoute { +public interface StatusRoute extends Route { + + @Override + StatusRouteInfo toRouteInfo(); + /** * @return The type the exception originates from. Null if the error route is global. */ @@ -41,25 +44,6 @@ public interface StatusRoute extends MethodBasedRoute { */ HttpStatus status(); - /** - * Match the given HTTP status. - * - * @param status The status to match - * @param The matched route - * @return The route match - */ - Optional> match(HttpStatus status); - - /** - * Match the given HTTP status. - * - * @param originatingClass The class where the error originates from - * @param status The status to match - * @param The matched route - * @return The route match - */ - Optional> match(Class originatingClass, HttpStatus status); - @Override StatusRoute consumes(MediaType... mediaType); diff --git a/router/src/main/java/io/micronaut/web/router/StatusRouteInfo.java b/router/src/main/java/io/micronaut/web/router/StatusRouteInfo.java new file mode 100644 index 00000000000..ff954cb8f63 --- /dev/null +++ b/router/src/main/java/io/micronaut/web/router/StatusRouteInfo.java @@ -0,0 +1,60 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.web.router; + +import io.micronaut.core.annotation.Nullable; +import io.micronaut.http.HttpStatus; + +import java.util.Optional; + +/** + * Represents a {@link RouteInfo} that matches a status. + * + * @param The target + * @param The result + * @author Denis Stepanov + * @since 4.0.0 + */ +public interface StatusRouteInfo extends MethodBasedRouteInfo, RequestMatcher { + /** + * @return The type the exception originates from. Null if the error route is global. + */ + @Nullable + Class originatingType(); + + /** + * @return The status + */ + HttpStatus status(); + + /** + * Match the given HTTP status. + * + * @param status The status to match + * @return The route match + */ + Optional> match(HttpStatus status); + + /** + * Match the given HTTP status. + * + * @param originatingClass The class where the error originates from + * @param status The status to match + * @return The route match + */ + Optional> match(Class originatingClass, HttpStatus status); + +} diff --git a/router/src/main/java/io/micronaut/web/router/StatusRouteMatch.java b/router/src/main/java/io/micronaut/web/router/StatusRouteMatch.java index d912820ffaa..8848986bf6c 100644 --- a/router/src/main/java/io/micronaut/web/router/StatusRouteMatch.java +++ b/router/src/main/java/io/micronaut/web/router/StatusRouteMatch.java @@ -17,10 +17,12 @@ import io.micronaut.core.convert.ConversionService; import io.micronaut.core.type.Argument; -import io.micronaut.http.HttpStatus; -import java.util.*; -import java.util.function.Function; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Map; /** * A {@link RouteMatch} for a status code. @@ -32,17 +34,14 @@ */ class StatusRouteMatch extends AbstractRouteMatch { - final HttpStatus httpStatus; private final ArrayList> requiredArguments; /** - * @param httpStatus The HTTP status - * @param abstractRoute The abstract route + * @param routeInfo The route info * @param conversionService The conversion service */ - StatusRouteMatch(HttpStatus httpStatus, DefaultRouteBuilder.AbstractRoute abstractRoute, ConversionService conversionService) { - super(abstractRoute, conversionService); - this.httpStatus = httpStatus; + StatusRouteMatch(StatusRouteInfo routeInfo, ConversionService conversionService) { + super(routeInfo, conversionService); this.requiredArguments = new ArrayList<>(Arrays.asList(getArguments())); } @@ -56,52 +55,4 @@ public Collection> getRequiredArguments() { return requiredArguments; } - @Override - public boolean isErrorRoute() { - return true; - } - - @Override - public HttpStatus findStatus(HttpStatus defaultStatus) { - return super.findStatus(httpStatus); - } - - @Override - protected RouteMatch newFulfilled(Map newVariables, List> requiredArguments) { - return new StatusRouteMatch(httpStatus, abstractRoute, conversionService) { - @Override - public Collection> getRequiredArguments() { - return requiredArguments; - } - - @Override - public Map getVariableValues() { - return newVariables; - } - }; - } - - @Override - public RouteMatch decorate(Function, R> executor) { - Map variables = getVariableValues(); - Collection> arguments = getRequiredArguments(); - RouteMatch thisRoute = this; - return new StatusRouteMatch(httpStatus, abstractRoute, conversionService) { - @Override - public Collection> getRequiredArguments() { - return arguments; - } - - @Override - public T execute(Map argumentValues) { - return (T) executor.apply(thisRoute); - } - - @Override - public Map getVariableValues() { - return variables; - } - }; - } - } diff --git a/router/src/main/java/io/micronaut/web/router/UriRoute.java b/router/src/main/java/io/micronaut/web/router/UriRoute.java index 5926bec810d..a1ec0b0548f 100644 --- a/router/src/main/java/io/micronaut/web/router/UriRoute.java +++ b/router/src/main/java/io/micronaut/web/router/UriRoute.java @@ -15,15 +15,13 @@ */ package io.micronaut.web.router; +import io.micronaut.core.annotation.Nullable; import io.micronaut.http.HttpMethod; import io.micronaut.http.HttpRequest; import io.micronaut.http.MediaType; import io.micronaut.http.uri.UriMatchTemplate; -import io.micronaut.http.uri.UriMatcher; -import io.micronaut.core.annotation.Nullable; import java.net.URI; -import java.util.Optional; import java.util.function.Predicate; /** @@ -32,7 +30,10 @@ * @author Graeme Rocher * @since 1.0 */ -public interface UriRoute extends Route, UriMatcher, Comparable { +public interface UriRoute extends Route, Comparable { + + @Override + UriRouteInfo toRouteInfo(); /** * Defines routes nested within this route. @@ -53,26 +54,6 @@ public interface UriRoute extends Route, UriMatcher, Comparable { */ UriMatchTemplate getUriMatchTemplate(); - /** - * Match this route within the given URI and produce a {@link RouteMatch} if a match is found. - * - * @param uri The URI The URI - * @return An {@link Optional} of {@link RouteMatch} - */ - @Override - default Optional match(URI uri) { - return match(uri.toString()); - } - - /** - * Match this route within the given URI and produce a {@link RouteMatch} if a match is found. - * - * @param uri The URI The URI - * @return An {@link Optional} of {@link RouteMatch} - */ - @Override - Optional match(String uri); - @Override UriRoute consumes(MediaType... mediaType); diff --git a/router/src/main/java/io/micronaut/web/router/UriRouteInfo.java b/router/src/main/java/io/micronaut/web/router/UriRouteInfo.java new file mode 100644 index 00000000000..3b45fbdb25e --- /dev/null +++ b/router/src/main/java/io/micronaut/web/router/UriRouteInfo.java @@ -0,0 +1,79 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.web.router; + +import io.micronaut.core.annotation.Nullable; +import io.micronaut.http.HttpMethod; +import io.micronaut.http.uri.UriMatchTemplate; +import io.micronaut.http.uri.UriMatcher; + +import java.net.URI; +import java.util.Optional; + +/** + * Represents a {@link Route} that matches a {@link URI}. + * + * @param The target + * @param The result + * @author Denis Stepanov + * @since 4.0.0 + */ +public interface UriRouteInfo extends MethodBasedRouteInfo, RequestMatcher, UriMatcher, Comparable> { + + /** + * @return The HTTP method for this route + */ + HttpMethod getHttpMethod(); + + /** + * @return The {@link UriMatchTemplate} used to match URIs + */ + UriMatchTemplate getUriMatchTemplate(); + + /** + * Match this route within the given URI and produce a {@link RouteMatch} if a match is found. + * + * @param uri The URI The URI + * @return An {@link Optional} of {@link RouteMatch} + */ + @Override + default Optional> match(URI uri) { + return match(uri.toString()); + } + + /** + * Match this route within the given URI and produce a {@link RouteMatch} if a match is found. + * + * @param uri The URI + * @return An {@link Optional} of {@link RouteMatch} + */ + @Override + Optional> match(String uri); + + /** + * @return The port the route listens to, or null if the default port + */ + @Nullable + Integer getPort(); + + /** + * + * @return The http method. Is equal to {@link #getHttpMethod()} value for standard http methods. + */ + default String getHttpMethodName() { + return getHttpMethod().name(); + } +} diff --git a/router/src/main/java/io/micronaut/web/router/UriRouteMatch.java b/router/src/main/java/io/micronaut/web/router/UriRouteMatch.java index e32a8b21761..8d1ffe2f5b1 100644 --- a/router/src/main/java/io/micronaut/web/router/UriRouteMatch.java +++ b/router/src/main/java/io/micronaut/web/router/UriRouteMatch.java @@ -20,8 +20,10 @@ import io.micronaut.http.HttpMethod; import io.micronaut.http.uri.UriMatchInfo; -import java.util.*; -import java.util.function.Function; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; /** * A {@link RouteMatch} that matches a URI and {@link HttpMethod}. @@ -34,9 +36,9 @@ public interface UriRouteMatch extends UriMatchInfo, MethodBasedRouteMatch { /** - * @return The backing {@link UriRoute} + * @return The backing {@link UriRouteInfo} */ - UriRoute getRoute(); + UriRouteInfo getRouteInfo(); /** *

Returns the required arguments for this RouteMatch.

@@ -68,9 +70,4 @@ default List> getRequiredArguments() { */ HttpMethod getHttpMethod(); - @Override - UriRouteMatch fulfill(Map argumentValues); - - @Override - UriRouteMatch decorate(Function, R> executor); } diff --git a/router/src/main/java/io/micronaut/web/router/filter/FilteredRouter.java b/router/src/main/java/io/micronaut/web/router/filter/FilteredRouter.java index c44d9bb6dff..345ea7462c0 100644 --- a/router/src/main/java/io/micronaut/web/router/filter/FilteredRouter.java +++ b/router/src/main/java/io/micronaut/web/router/filter/FilteredRouter.java @@ -23,7 +23,7 @@ import io.micronaut.http.filter.GenericHttpFilter; import io.micronaut.web.router.RouteMatch; import io.micronaut.web.router.Router; -import io.micronaut.web.router.UriRoute; +import io.micronaut.web.router.UriRouteInfo; import io.micronaut.web.router.UriRouteMatch; import java.util.List; @@ -72,6 +72,11 @@ public Stream> findAny(@NonNull CharSequence uri, @Nu return matchStream; } + @Override + public List> findAny(HttpRequest request) { + return router.findAny(request).stream().filter(routeFilter.filter(request)).toList(); + } + @Override public Set getExposedPorts() { return router.getExposedPorts(); @@ -108,7 +113,7 @@ public Stream> find(@NonNull HttpRequest request, @NonNull @Override - public Stream uriRoutes() { + public Stream> uriRoutes() { return router.uriRoutes(); } diff --git a/router/src/test/groovy/io/micronaut/context/router/GroovyRouteBuilderSpec.groovy b/router/src/test/groovy/io/micronaut/context/router/GroovyRouteBuilderSpec.groovy index 709a8f6bb1f..0cd2dfc9d55 100644 --- a/router/src/test/groovy/io/micronaut/context/router/GroovyRouteBuilderSpec.groovy +++ b/router/src/test/groovy/io/micronaut/context/router/GroovyRouteBuilderSpec.groovy @@ -19,9 +19,11 @@ import io.micronaut.context.ApplicationContext import io.micronaut.context.DefaultApplicationContext import io.micronaut.context.annotation.Executable import io.micronaut.http.HttpMethod +import io.micronaut.http.HttpRequest import io.micronaut.http.annotation.Controller import io.micronaut.http.annotation.Get import io.micronaut.http.annotation.Error +import io.micronaut.http.bind.RequestBinderRegistry import io.micronaut.web.router.GroovyRouteBuilder import io.micronaut.web.router.RouteMatch import io.micronaut.web.router.Router @@ -74,16 +76,19 @@ class GroovyRouteBuilderSpec extends Specification { given: def context = new DefaultApplicationContext("test").start() Router router = context.getBean(Router) + RequestBinderRegistry requestBinderRegistry = context.getBean(RequestBinderRegistry) + def route = router.route(ErrorHandlingController, bean).get() expect: - router.route(ErrorHandlingController, new A()).get().execute() == "c" - router.route(ErrorHandlingController, new B()).get().execute() == "c" - router.route(ErrorHandlingController, new C()).get().execute() == "c" - router.route(ErrorHandlingController, new D()).get().execute() == "e" - router.route(ErrorHandlingController, new E()).get().execute() == "e" + route.fulfillBeforeFilters(requestBinderRegistry, Mock(HttpRequest)) + route.execute() == result cleanup: context.stop() + + where: + bean << [new A(), new B(), new C(), new D(), new E()] + result << ["c", "c", "c", "e", "e"] } // tag::routes[] diff --git a/router/src/test/groovy/io/micronaut/context/router/RouteBuilderTests.java b/router/src/test/groovy/io/micronaut/context/router/RouteBuilderTests.java index daa9681c036..d45b3ab150e 100644 --- a/router/src/test/groovy/io/micronaut/context/router/RouteBuilderTests.java +++ b/router/src/test/groovy/io/micronaut/context/router/RouteBuilderTests.java @@ -21,15 +21,21 @@ import io.micronaut.http.HttpMethod; import io.micronaut.http.HttpStatus; import io.micronaut.http.MediaType; -import io.micronaut.web.router.*; - +import io.micronaut.web.router.DefaultRouteBuilder; +import io.micronaut.web.router.DefaultRouter; +import io.micronaut.web.router.MethodBasedRouteMatch; +import io.micronaut.web.router.Router; +import io.micronaut.web.router.UriRoute; import jakarta.inject.Inject; import jakarta.inject.Singleton; -import static org.junit.jupiter.api.Assertions.*; import org.junit.jupiter.api.Test; import java.util.List; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + /** * @author Graeme Rocher * @since 1.0 @@ -58,27 +64,27 @@ public void testRouterBuilder() { assertTrue(builtRoutes .stream() .anyMatch(route -> - route.match("/books/1/authors").isPresent() && route.getHttpMethod() == HttpMethod.GET) + route.toRouteInfo().match("/books/1/authors").isPresent() && route.getHttpMethod() == HttpMethod.GET) ); assertTrue(builtRoutes .stream() .anyMatch(route -> - route.match("/books").isPresent() && route.getHttpMethod() == HttpMethod.POST) + route.toRouteInfo().match("/books").isPresent() && route.getHttpMethod() == HttpMethod.POST) ); assertTrue(builtRoutes .stream() .anyMatch(route -> - route.match("/book").isPresent() && route.getHttpMethod() == HttpMethod.POST) + route.toRouteInfo().match("/book").isPresent() && route.getHttpMethod() == HttpMethod.POST) ); assertFalse(builtRoutes .stream() .anyMatch(route -> - route.match("/boo").isPresent() && route.getHttpMethod() == HttpMethod.POST) + route.toRouteInfo().match("/boo").isPresent() && route.getHttpMethod() == HttpMethod.POST) ); assertTrue(builtRoutes .stream() .anyMatch(route -> - route.match("/book/1").isPresent() && route.getHttpMethod() == HttpMethod.GET) + route.toRouteInfo().match("/book/1").isPresent() && route.getHttpMethod() == HttpMethod.GET) ); } diff --git a/test-suite-groovy/src/test/groovy/io/micronaut/docs/server/upload/UploadSpec.groovy b/test-suite-groovy/src/test/groovy/io/micronaut/docs/server/upload/UploadSpec.groovy index cf418af65f6..0b1f9843fe1 100644 --- a/test-suite-groovy/src/test/groovy/io/micronaut/docs/server/upload/UploadSpec.groovy +++ b/test-suite-groovy/src/test/groovy/io/micronaut/docs/server/upload/UploadSpec.groovy @@ -113,7 +113,7 @@ class UploadSpec extends AbstractMicronautSpec { then: def ex = thrown(HttpClientResponseException) - ex.response.getBody(Map).get()._embedded.errors[0].message == "Required argument [CompletedFileUpload file] not specified" + ex.response.getBody(Map).get()._embedded.errors[0].message == "Cannot convert type [class io.micronaut.http.server.netty.MicronautHttpData\$AttributeImpl] to target type: interface io.micronaut.http.multipart.CompletedFileUpload. Considering defining a TypeConverter bean to handle this case." } void "test completed file upload with no file name and no bytes"() { @@ -133,7 +133,7 @@ class UploadSpec extends AbstractMicronautSpec { then: def ex = thrown(HttpClientResponseException) - ex.response.getBody(Map).get()._embedded.errors[0].message == "Required argument [CompletedFileUpload file] not specified" + ex.response.getBody(Map).get()._embedded.errors[0].message == "Cannot convert type [class io.micronaut.http.server.netty.MicronautHttpData\$AttributeImpl] to target type: interface io.micronaut.http.multipart.CompletedFileUpload. Considering defining a TypeConverter bean to handle this case." } void "test completed file upload with no part"() { diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/upload/UploadControllerSpec.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/upload/UploadControllerSpec.kt index 049f6a3b3e1..4ffa45325dc 100644 --- a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/upload/UploadControllerSpec.kt +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/upload/UploadControllerSpec.kt @@ -1,8 +1,8 @@ package io.micronaut.docs.server.upload -import io.kotest.matchers.shouldBe import io.kotest.assertions.throwables.shouldThrow import io.kotest.core.spec.style.StringSpec +import io.kotest.matchers.shouldBe import io.micronaut.context.ApplicationContext import io.micronaut.http.HttpRequest import io.micronaut.http.HttpStatus @@ -109,7 +109,7 @@ class UploadControllerSpec: StringSpec() { val embedded: Map<*, *> = response.getBody(Map::class.java).get().get("_embedded") as Map<*, *> val message = ((embedded.get("errors") as java.util.List<*>).get(0) as Map<*, *>).get("message") - message shouldBe "Required argument [CompletedFileUpload file] not specified" + message shouldBe "Cannot convert type [class io.micronaut.http.server.netty.MicronautHttpData\$AttributeImpl] to target type: interface io.micronaut.http.multipart.CompletedFileUpload. Considering defining a TypeConverter bean to handle this case." } "test completed file upload with no filename and no bytes"() { @@ -129,7 +129,7 @@ class UploadControllerSpec: StringSpec() { val embedded: Map<*, *> = response.getBody(Map::class.java).get().get("_embedded") as Map<*, *> val message = ((embedded.get("errors") as java.util.List<*>).get(0) as Map<*, *>).get("message") - message shouldBe "Required argument [CompletedFileUpload file] not specified" + message shouldBe "Cannot convert type [class io.micronaut.http.server.netty.MicronautHttpData\$AttributeImpl] to target type: interface io.micronaut.http.multipart.CompletedFileUpload. Considering defining a TypeConverter bean to handle this case." } "test completed file upload with no part"() { diff --git a/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/server/upload/UploadControllerSpec.kt b/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/server/upload/UploadControllerSpec.kt index 049f6a3b3e1..4ffa45325dc 100644 --- a/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/server/upload/UploadControllerSpec.kt +++ b/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/server/upload/UploadControllerSpec.kt @@ -1,8 +1,8 @@ package io.micronaut.docs.server.upload -import io.kotest.matchers.shouldBe import io.kotest.assertions.throwables.shouldThrow import io.kotest.core.spec.style.StringSpec +import io.kotest.matchers.shouldBe import io.micronaut.context.ApplicationContext import io.micronaut.http.HttpRequest import io.micronaut.http.HttpStatus @@ -109,7 +109,7 @@ class UploadControllerSpec: StringSpec() { val embedded: Map<*, *> = response.getBody(Map::class.java).get().get("_embedded") as Map<*, *> val message = ((embedded.get("errors") as java.util.List<*>).get(0) as Map<*, *>).get("message") - message shouldBe "Required argument [CompletedFileUpload file] not specified" + message shouldBe "Cannot convert type [class io.micronaut.http.server.netty.MicronautHttpData\$AttributeImpl] to target type: interface io.micronaut.http.multipart.CompletedFileUpload. Considering defining a TypeConverter bean to handle this case." } "test completed file upload with no filename and no bytes"() { @@ -129,7 +129,7 @@ class UploadControllerSpec: StringSpec() { val embedded: Map<*, *> = response.getBody(Map::class.java).get().get("_embedded") as Map<*, *> val message = ((embedded.get("errors") as java.util.List<*>).get(0) as Map<*, *>).get("message") - message shouldBe "Required argument [CompletedFileUpload file] not specified" + message shouldBe "Cannot convert type [class io.micronaut.http.server.netty.MicronautHttpData\$AttributeImpl] to target type: interface io.micronaut.http.multipart.CompletedFileUpload. Considering defining a TypeConverter bean to handle this case." } "test completed file upload with no part"() { diff --git a/test-suite/src/test/java/io/micronaut/docs/server/upload/UploadControllerSpec.java b/test-suite/src/test/java/io/micronaut/docs/server/upload/UploadControllerSpec.java index 7c771a90bb6..a17fa54a96b 100644 --- a/test-suite/src/test/java/io/micronaut/docs/server/upload/UploadControllerSpec.java +++ b/test-suite/src/test/java/io/micronaut/docs/server/upload/UploadControllerSpec.java @@ -153,7 +153,8 @@ public void testCompletedFileUploadNoNameWithBytes() { Map embedded = (Map) ex.getResponse().getBody(Map.class).get().get("_embedded"); Object message = ((Map) ((List) embedded.get("errors")).get(0)).get("message"); - assertEquals("Required argument [CompletedFileUpload file] not specified", message); + // a part without a filename is an attribute, which cannot be bound to CompletedFileUpload + assertEquals("Cannot convert type [class io.micronaut.http.server.netty.MicronautHttpData$AttributeImpl] to target type: interface io.micronaut.http.multipart.CompletedFileUpload. Considering defining a TypeConverter bean to handle this case.", message); } @Test @@ -173,7 +174,8 @@ public void testCompletedFileUploadWithNoFileNameAndNoBytes() { Map embedded = (Map) ex.getResponse().getBody(Map.class).get().get("_embedded"); Object message = ((Map) ((List) embedded.get("errors")).get(0)).get("message"); - assertEquals("Required argument [CompletedFileUpload file] not specified", message); + // a part without a filename is an attribute, which cannot be bound to CompletedFileUpload + assertEquals("Cannot convert type [class io.micronaut.http.server.netty.MicronautHttpData$AttributeImpl] to target type: interface io.micronaut.http.multipart.CompletedFileUpload. Considering defining a TypeConverter bean to handle this case.", message); } @Test diff --git a/websocket/src/main/java/io/micronaut/websocket/bind/WebSocketStateBinderRegistry.java b/websocket/src/main/java/io/micronaut/websocket/bind/WebSocketStateBinderRegistry.java index 8a13e78ee64..883e934b1d2 100644 --- a/websocket/src/main/java/io/micronaut/websocket/bind/WebSocketStateBinderRegistry.java +++ b/websocket/src/main/java/io/micronaut/websocket/bind/WebSocketStateBinderRegistry.java @@ -18,15 +18,14 @@ import io.micronaut.core.annotation.Internal; import io.micronaut.core.bind.ArgumentBinder; import io.micronaut.core.bind.ArgumentBinderRegistry; -import io.micronaut.core.bind.annotation.AnnotatedArgumentBinder; import io.micronaut.core.convert.ArgumentConversionContext; import io.micronaut.core.convert.ConversionService; import io.micronaut.core.convert.value.ConvertibleValues; import io.micronaut.core.type.Argument; import io.micronaut.http.HttpRequest; -import io.micronaut.http.annotation.QueryValue; import io.micronaut.http.bind.RequestBinderRegistry; import io.micronaut.http.bind.binders.QueryValueArgumentBinder; +import io.micronaut.http.bind.binders.UnmatchedRequestArgumentBinder; import io.micronaut.websocket.WebSocketSession; import java.util.HashMap; @@ -61,33 +60,29 @@ public WebSocketStateBinderRegistry(RequestBinderRegistry requestBinderRegistry, } @Override - public void addRequestArgumentBinder(ArgumentBinder binder) { - requestBinderRegistry.addRequestArgumentBinder(binder); - } - - @Override - public Optional> findArgumentBinder(Argument argument, WebSocketState source) { - Optional>> argumentBinder = requestBinderRegistry.findArgumentBinder(argument, source.getOriginatingRequest()); + public Optional> findArgumentBinder(Argument argument) { + Optional>> argumentBinder = requestBinderRegistry.findArgumentBinder(argument); if (argumentBinder.isPresent()) { ArgumentBinder> adapted = argumentBinder.get(); - boolean isParameterBinder = adapted instanceof AnnotatedArgumentBinder && ((AnnotatedArgumentBinder) adapted).getAnnotationType() == QueryValue.class; - if (!isParameterBinder) { - return Optional.of((context, source1) -> adapted.bind(context, source.getOriginatingRequest())); + boolean isUnmatchedRequestArgumentBinder = adapted instanceof UnmatchedRequestArgumentBinder; + if (!isUnmatchedRequestArgumentBinder) { + return Optional.of((context, source1) -> adapted.bind(context, source1.getOriginatingRequest())); } } - ArgumentBinder binder = byType.get(argument.getType()); + ArgumentBinder binder = (ArgumentBinder) byType.get(argument.getType()); if (binder != null) { - //noinspection unchecked return Optional.of(binder); - } else { + } + return Optional.of((context, source) -> { ConvertibleValues uriVariables = source.getSession().getUriVariables(); if (uriVariables.contains(argument.getName())) { - return Optional.of((context, s) -> () -> uriVariables.get(argument.getName(), argument)); - } else { - return Optional.of((context, s) -> (ArgumentBinder.BindingResult) queryValueArgumentBinder.bind((ArgumentConversionContext) context, s.getOriginatingRequest())); + Optional val = uriVariables.get(argument.getName(), argument); + return val.isEmpty() ? ArgumentBinder.BindingResult.UNSATISFIED : () -> val; } - } + return (ArgumentBinder.BindingResult) queryValueArgumentBinder.bind((ArgumentConversionContext) context, source.getOriginatingRequest()); + }); + } } From 50c6e014127cca2b3104b706f325832c02886e89 Mon Sep 17 00:00:00 2001 From: micronaut-build <65172877+micronaut-build@users.noreply.github.com> Date: Wed, 5 Apr 2023 13:29:19 +0200 Subject: [PATCH 665/743] Bump micronaut-aws to 3.16.0 (#9065) --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index da8d03625ac..b985c43bfe6 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -67,7 +67,7 @@ managed-methvin-directory-watcher = "0.16.1" managed-micrometer = "1.10.3" managed-micronaut-acme = "3.2.0" managed-micronaut-aot = "1.1.2" -managed-micronaut-aws = "3.15.0" +managed-micronaut-aws = "3.16.0" managed-micronaut-azure = "3.10.0" managed-micronaut-cache = "3.5.0" managed-micronaut-chatbots = "1.0.0-M1" From 120e35a8511b8acda29f2ea61450495b48979ea3 Mon Sep 17 00:00:00 2001 From: micronaut-build <65172877+micronaut-build@users.noreply.github.com> Date: Wed, 5 Apr 2023 14:43:26 +0200 Subject: [PATCH 666/743] Bump micronaut-micrometer to 4.8.3 (#9066) * Bump micronaut-micrometer to 4.8.3 * micrometer to 1.10.5 --------- Co-authored-by: Sergio del Amo --- gradle/libs.versions.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index b985c43bfe6..9a0aff409ae 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -64,7 +64,7 @@ managed-logback = "1.2.11" managed-lombok = "1.18.24" managed-maven-native-plugin = "0.9.19" managed-methvin-directory-watcher = "0.16.1" -managed-micrometer = "1.10.3" +managed-micrometer = "1.10.5" managed-micronaut-acme = "3.2.0" managed-micronaut-aot = "1.1.2" managed-micronaut-aws = "3.16.0" @@ -91,7 +91,7 @@ managed-micronaut-jmx = "3.2.0" managed-micronaut-kafka = "4.5.3" managed-micronaut-kotlin = "3.2.2" managed-micronaut-kubernetes = "4.0.0" -managed-micronaut-micrometer = "4.8.2" +managed-micronaut-micrometer = "4.8.3" managed-micronaut-microstream = "1.3.0" managed-micronaut-liquibase = "5.7.1" managed-micronaut-mongo = "4.6.0" From 35fca05e14981b907bcb0bc240f4fc2239eceea1 Mon Sep 17 00:00:00 2001 From: Januson Date: Wed, 5 Apr 2023 14:47:08 +0200 Subject: [PATCH 667/743] Fix broken link to checkstyle issue img (#9047) - Replaced broken link to checkstyle-issue.png with a link to docassets --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 897e96032c0..b4fbde32271 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -74,7 +74,7 @@ Before contributing new code it is recommended you install IntelliJ [CheckStyle- IntelliJ will mark in red the issues Checkstyle finds. For example: -![](https://github.com/micronaut-projects/micronaut-core/raw/master/src/main/docs/resources/img/checkstyle-issue.png) +![checkstyle-issue](https://docs.micronaut.io/docsassets/img/checkstyle-issue.png) In this case, to fix the issues, we need to: From 2310670976d39b7948cd9d7fec95101c5d230c14 Mon Sep 17 00:00:00 2001 From: altro3 Date: Wed, 5 Apr 2023 19:54:28 +0700 Subject: [PATCH 668/743] Add ability to disable log in environments (#8946) --- .../core/io/scan/ClassPathResourceLoader.java | 2 +- .../element/AccessLogFormatParser.java | 16 ++++----- .../env/AbstractPropertySourceLoader.java | 35 ++++++++++++++++--- .../context/env/DefaultEnvironment.java | 26 +++++++------- .../env/PropertySourcePropertyResolver.java | 33 +++++++++++++++-- .../env/yaml/YamlPropertySourceLoader.java | 4 +-- 6 files changed, 85 insertions(+), 31 deletions(-) diff --git a/core/src/main/java/io/micronaut/core/io/scan/ClassPathResourceLoader.java b/core/src/main/java/io/micronaut/core/io/scan/ClassPathResourceLoader.java index 6a42e99e0e6..14ebda53e56 100644 --- a/core/src/main/java/io/micronaut/core/io/scan/ClassPathResourceLoader.java +++ b/core/src/main/java/io/micronaut/core/io/scan/ClassPathResourceLoader.java @@ -20,7 +20,7 @@ import io.micronaut.core.annotation.Nullable; /** - * Abstraction to load resources from the the classpath. + * Abstraction to load resources from the classpath. * * @author James Kleeh * @author Graeme Rocher diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/handler/accesslog/element/AccessLogFormatParser.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/handler/accesslog/element/AccessLogFormatParser.java index 4f338697dd1..312d4e60055 100644 --- a/http-server-netty/src/main/java/io/micronaut/http/server/netty/handler/accesslog/element/AccessLogFormatParser.java +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/handler/accesslog/element/AccessLogFormatParser.java @@ -33,7 +33,7 @@ /** * The access log format parser. - * + *

* The syntax is based on Apache httpd log format. * Here are the supported directives: *

    @@ -43,17 +43,17 @@ *
  • %B - Bytes sent, excluding HTTP headers
  • *
  • %h - Remote host name
  • *
  • %H - Request protocol
  • - *
  • %{
    }i - Request header. If the argument is omitted (%i) all headers will be printed
  • - *
  • %{
    }o - Response header. If the argument is omitted (%o) all headers will be printed
  • - *
  • %{}C - Request cookie (COOKIE). If the argument is omitted (%C) all cookies will be printed
  • - *
  • %{}c - Response cookie (SET_COOKIE). If the argument is omitted (%c) all cookies will be printed
  • + *
  • %{<header>}i - Request header. If the argument is omitted (%i) all headers will be printed
  • + *
  • %{<header>}o - Response header. If the argument is omitted (%o) all headers will be printed
  • + *
  • %{<cookie>}C - Request cookie (COOKIE). If the argument is omitted (%C) all cookies will be printed
  • + *
  • %{<cookie>}c - Response cookie (SET_COOKIE). If the argument is omitted (%c) all cookies will be printed
  • *
  • %l - Remote logical username from identd (always returns '-')
  • *
  • %m - Request method
  • *
  • %p - Local port
  • *
  • %q - Query string (excluding the '?' character)
  • *
  • %r - First line of the request
  • *
  • %s - HTTP status code of the response
  • - *
  • %{}t - Date and time. If the argument is omitted the Common Log Format format is used ("'['dd/MMM/yyyy:HH:mm:ss Z']'"). + *
  • %{<format>}t - Date and time. If the argument is omitted the Common Log Format format is used ("'['dd/MMM/yyyy:HH:mm:ss Z']'"). * If the format starts with begin: (default) the time is taken at the beginning of the request processing. If it starts with end: it is the time when the log entry gets written, close to the end of the request processing. * The format should follow the DateTimeFormatter syntax.
  • *
  • %u - Remote user that was authenticated. Not implemented. Prints '-'.
  • @@ -64,9 +64,9 @@ *
*

In addition, the following aliases for commonly utilized patterns:

*
    - *
  • common - %h %l %u %t "%r" %s %b Common Log Format (CLF)
  • + *
  • common - {@code %h %l %u %t "%r" %s %b} Common Log Format (CLF)
  • *
  • combined - - * %h %l %u %t "%r" %s %b "%{Referer}i" "%{User-Agent}i" Combined Log Format
  • + * {@code %h %l %u %t "%r" %s %b "%{Referer}i" "%{User-Agent}i"} Combined Log Format *
* * @author croudet diff --git a/inject/src/main/java/io/micronaut/context/env/AbstractPropertySourceLoader.java b/inject/src/main/java/io/micronaut/context/env/AbstractPropertySourceLoader.java index 142b2257925..9e5c08e9eb6 100644 --- a/inject/src/main/java/io/micronaut/context/env/AbstractPropertySourceLoader.java +++ b/inject/src/main/java/io/micronaut/context/env/AbstractPropertySourceLoader.java @@ -45,6 +45,11 @@ public abstract class AbstractPropertySourceLoader implements PropertySourceLoad private static final Logger LOG = LoggerFactory.getLogger(AbstractPropertySourceLoader.class); + /** + * If you don't need to initialize SLF4J, set 'false'. + */ + protected boolean logEnabled = true; + @Override public int getOrder() { return DEFAULT_POSITION; @@ -95,8 +100,8 @@ public int getOrder() { private Map loadProperties(ResourceLoader resourceLoader, String qualifiedName, String fileName) { Optional config = readInput(resourceLoader, fileName); if (config.isPresent()) { - if (LOG.isDebugEnabled()) { - LOG.debug("Found PropertySource for file name: " + fileName); + if (logEnabled) { + LOG.debug("Found PropertySource for file name: {}", fileName); } try (InputStream input = config.get()) { return read(qualifiedName, input); @@ -104,8 +109,8 @@ private Map loadProperties(ResourceLoader resourceLoader, String throw new ConfigurationException("I/O exception occurred reading [" + fileName + "]: " + e.getMessage(), e); } } else { - if (LOG.isDebugEnabled()) { - LOG.debug("No PropertySource found for file name: " + fileName); + if (logEnabled) { + LOG.debug("No PropertySource found for file name: {}", fileName); } } return Collections.emptyMap(); @@ -152,4 +157,26 @@ protected void processMap(Map finalMap, Map map, String prefix) } } } + + /** + * Return logEnabled value. + * + * @return is log enabled + * + * @since 3.9.0 + */ + public boolean isLogEnabled() { + return logEnabled; + } + + /** + * Setter for logEnabled. + * + * @param logEnabled is log enabled + * + * @since 3.9.0 + */ + public void setLogEnabled(boolean logEnabled) { + this.logEnabled = logEnabled; + } } diff --git a/inject/src/main/java/io/micronaut/context/env/DefaultEnvironment.java b/inject/src/main/java/io/micronaut/context/env/DefaultEnvironment.java index b83fd78d29f..02c7d1f2272 100644 --- a/inject/src/main/java/io/micronaut/context/env/DefaultEnvironment.java +++ b/inject/src/main/java/io/micronaut/context/env/DefaultEnvironment.java @@ -155,7 +155,7 @@ public DefaultEnvironment(@NonNull ApplicationContextConfiguration configuration this.classLoader = configuration.getClassLoader(); this.annotationScanner = createAnnotationScanner(classLoader); this.names = environments; - if (LOG.isInfoEnabled() && !environments.isEmpty()) { + if (logEnabled && !environments.isEmpty()) { LOG.info("Established active environments: {}", environments); } List configLocations = configuration.getOverrideConfigLocations() == null ? @@ -262,7 +262,7 @@ public Collection getPropertySources() { @Override public Environment start() { if (running.compareAndSet(false, true)) { - if (LOG.isDebugEnabled()) { + if (logEnabled) { LOG.debug("Starting environment {} for active names {}", this, getActiveNames()); } if (reading.compareAndSet(false, true)) { @@ -352,8 +352,8 @@ public ResourceLoader forBase(String basePath) { */ protected boolean shouldDeduceEnvironments() { if (deduceEnvironments != null) { - if (LOG.isDebugEnabled()) { - LOG.debug("Environment deduction was set explicitly via builder to: " + deduceEnvironments); + if (logEnabled) { + LOG.debug("Environment deduction was set explicitly via builder to: {}", deduceEnvironments); } return deduceEnvironments; @@ -363,20 +363,20 @@ protected boolean shouldDeduceEnvironments() { if (StringUtils.isNotEmpty(deduceEnv)) { boolean deduce = Boolean.parseBoolean(deduceEnv); - if (LOG.isDebugEnabled()) { - LOG.debug("Environment deduction was set via environment variable to: " + deduce); + if (logEnabled) { + LOG.debug("Environment deduction was set via environment variable to: {}", deduce); } return deduce; } else if (StringUtils.isNotEmpty(deduceProperty)) { boolean deduce = Boolean.parseBoolean(deduceProperty); - if (LOG.isDebugEnabled()) { - LOG.debug("Environment deduction was set via system property to: " + deduce); + if (logEnabled) { + LOG.debug("Environment deduction was set via system property to: {}", deduce); } return deduce; } else { boolean deduceDefault = DEDUCE_ENVIRONMENT_DEFAULT; - if (LOG.isDebugEnabled()) { - LOG.debug("Environment deduction is using the default of: " + deduceDefault); + if (logEnabled) { + LOG.debug("Environment deduction is using the default of: {}", deduceDefault); } return deduceDefault; } @@ -428,7 +428,7 @@ protected void readPropertySources(String name) { propertySources.addAll(this.propertySources.values()); OrderUtil.sort(propertySources); for (PropertySource propertySource : propertySources) { - if (LOG.isDebugEnabled()) { + if (logEnabled) { LOG.debug("Processing property source: {}", propertySource.getName()); } processPropertySource(propertySource, propertySource.getConvention()); @@ -483,7 +483,7 @@ protected List readPropertySourceListFromFiles(String files) { String fileName = NameUtils.filename(filePath); Optional propertySourceLoader = Optional.ofNullable(loaderByFormatMap.get(extension)); if (propertySourceLoader.isPresent()) { - if (LOG.isDebugEnabled()) { + if (logEnabled) { LOG.debug("Reading property sources from loader: {}", propertySourceLoader); } Optional> properties = readPropertiesFromLoader(fileName, filePath, propertySourceLoader.get()); @@ -535,7 +535,7 @@ private void readPropertySourceList(String name, ResourceLoader resourceLoader, loadPropertySourceFromLoader(name, new PropertiesPropertySourceLoader(), propertySources, resourceLoader); } else { for (PropertySourceLoader propertySourceLoader : propertySourceLoaders) { - if (LOG.isDebugEnabled()) { + if (logEnabled) { LOG.debug("Reading property sources from loader: {}", propertySourceLoader); } loadPropertySourceFromLoader(name, propertySourceLoader, propertySources, resourceLoader); diff --git a/inject/src/main/java/io/micronaut/context/env/PropertySourcePropertyResolver.java b/inject/src/main/java/io/micronaut/context/env/PropertySourcePropertyResolver.java index b36fe35424e..e2e302ed098 100644 --- a/inject/src/main/java/io/micronaut/context/env/PropertySourcePropertyResolver.java +++ b/inject/src/main/java/io/micronaut/context/env/PropertySourcePropertyResolver.java @@ -91,6 +91,11 @@ public class PropertySourcePropertyResolver implements PropertyResolver, AutoClo private final Map resolvedValueCache = new ConcurrentHashMap<>(20); private final EnvironmentProperties environmentProperties = EnvironmentProperties.fork(CURRENT_ENV); + /** + * If you don't need to initialize SLF4J, set 'false'. + */ + protected boolean logEnabled = true; + /** * Creates a new, initially empty, {@link PropertySourcePropertyResolver} for the given {@link ConversionService}. * @@ -324,7 +329,7 @@ public Optional getProperty(@NonNull String name, @NonNull ArgumentConver converted = conversionService.convert(value, conversionContext); } - if (LOG.isTraceEnabled()) { + if (logEnabled && LOG.isTraceEnabled()) { if (converted.isPresent()) { LOG.trace("Resolved value [{}] for property: {}", converted.get(), name); } else { @@ -357,7 +362,7 @@ public Optional getProperty(@NonNull String name, @NonNull ArgumentConver } } - if (LOG.isTraceEnabled()) { + if (logEnabled) { LOG.trace("No value found for property: {}", name); } @@ -527,7 +532,7 @@ protected void processPropertySource(PropertySource properties, PropertySource.P synchronized (catalog) { for (String property : properties) { - if (LOG.isTraceEnabled()) { + if (logEnabled) { LOG.trace("Processing property key {}", property); } @@ -903,6 +908,28 @@ public void close() throws Exception { } } + /** + * Return logEnabled value. + * + * @return is log enabled + * + * @since 3.9.0 + */ + public boolean isLogEnabled() { + return logEnabled; + } + + /** + * Setter for logEnabled. + * + * @param logEnabled is log enabled + * + * @since 3.9.0 + */ + public void setLogEnabled(boolean logEnabled) { + this.logEnabled = logEnabled; + } + /** * The property catalog to use. */ diff --git a/inject/src/main/java/io/micronaut/context/env/yaml/YamlPropertySourceLoader.java b/inject/src/main/java/io/micronaut/context/env/yaml/YamlPropertySourceLoader.java index d5f4cca0d3e..45946dc7adf 100644 --- a/inject/src/main/java/io/micronaut/context/env/yaml/YamlPropertySourceLoader.java +++ b/inject/src/main/java/io/micronaut/context/env/yaml/YamlPropertySourceLoader.java @@ -61,7 +61,7 @@ protected void processInput(String name, InputStream input, Map Object object = i.next(); if (object instanceof Map) { Map map = (Map) object; - if (LOG.isTraceEnabled()) { + if (logEnabled) { LOG.trace("Processing YAML: {}", map); } String prefix = ""; @@ -69,7 +69,7 @@ protected void processInput(String name, InputStream input, Map } } } else { - if (LOG.isTraceEnabled()) { + if (logEnabled) { LOG.trace("PropertySource [{}] produced no YAML content", name); } } From edfd75ad4e485bec204aa7660819f9220b53cb5f Mon Sep 17 00:00:00 2001 From: micronaut-build <65172877+micronaut-build@users.noreply.github.com> Date: Wed, 5 Apr 2023 15:25:08 +0200 Subject: [PATCH 669/743] Bump micronaut-jms to 2.2.0 (#8729) --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 9a0aff409ae..cd2cc73939f 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -86,7 +86,7 @@ managed-micronaut-grpc = "3.5.0" managed-micronaut-hibernate-validator = "3.3.0" managed-micronaut-ignite = "1.0.0.RC1" managed-micronaut-jaxrs = "3.4.0" -managed-micronaut-jms = "2.1.0" +managed-micronaut-jms = "2.2.0" managed-micronaut-jmx = "3.2.0" managed-micronaut-kafka = "4.5.3" managed-micronaut-kotlin = "3.2.2" From 5d8af1967d6b7c36ac6854a6e63cb6651bf4cd40 Mon Sep 17 00:00:00 2001 From: micronaut-build <65172877+micronaut-build@users.noreply.github.com> Date: Wed, 5 Apr 2023 17:35:43 +0200 Subject: [PATCH 670/743] Bump micronaut-grpc to 3.6.0 (#9068) --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index cd2cc73939f..9c4a50783df 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -82,7 +82,7 @@ managed-micronaut-flyway = "5.5.0" managed-micronaut-gcp = "4.10.0" managed-micronaut-graphql = "3.2.0" managed-micronaut-groovy = "3.4.0" -managed-micronaut-grpc = "3.5.0" +managed-micronaut-grpc = "3.6.0" managed-micronaut-hibernate-validator = "3.3.0" managed-micronaut-ignite = "1.0.0.RC1" managed-micronaut-jaxrs = "3.4.0" From d7819be4ddb7032a075fbf27b2d4decc80dbd5a5 Mon Sep 17 00:00:00 2001 From: Denis Stepanov Date: Wed, 5 Apr 2023 11:08:08 -0600 Subject: [PATCH 671/743] More performance optimizations (#9070) Optimises route location and execution --------- Co-authored-by: Jonas Konrad --- .../NettyWebSocketClientHandler.java | 2 +- .../http/server/ExecutableRouteInfo.java | 1 + .../http/server/RequestLifecycle.java | 23 ++--- .../micronaut/http/uri/UriMatchTemplate.java | 37 +++++---- .../io/micronaut/http/uri/UriTemplate.java | 7 +- .../micronaut/web/router/DefaultRouter.java | 83 +++++++++++++------ .../web/router/DefaultUrlRouteInfo.java | 12 ++- .../io/micronaut/web/router/UriRouteInfo.java | 21 +++++ 8 files changed, 133 insertions(+), 53 deletions(-) diff --git a/http-client/src/main/java/io/micronaut/http/client/netty/websocket/NettyWebSocketClientHandler.java b/http-client/src/main/java/io/micronaut/http/client/netty/websocket/NettyWebSocketClientHandler.java index e3ee2fcc948..fd42db3a890 100644 --- a/http-client/src/main/java/io/micronaut/http/client/netty/websocket/NettyWebSocketClientHandler.java +++ b/http-client/src/main/java/io/micronaut/http/client/netty/websocket/NettyWebSocketClientHandler.java @@ -99,7 +99,7 @@ public NettyWebSocketClientHandler( this.genericWebSocketBean = webSocketBean; String clientPath = webSocketBean.getBeanDefinition().stringValue(ClientWebSocket.class).orElse(""); UriMatchTemplate matchTemplate = UriMatchTemplate.of(clientPath); - this.matchInfo = matchTemplate.match(request.getPath()).orElse(null); + this.matchInfo = matchTemplate.tryMatch(request.getPath()); } @Override diff --git a/http-server/src/main/java/io/micronaut/http/server/ExecutableRouteInfo.java b/http-server/src/main/java/io/micronaut/http/server/ExecutableRouteInfo.java index dbe7e5d40da..268d864af3a 100644 --- a/http-server/src/main/java/io/micronaut/http/server/ExecutableRouteInfo.java +++ b/http-server/src/main/java/io/micronaut/http/server/ExecutableRouteInfo.java @@ -60,6 +60,7 @@ public Class getDeclaringType() { public String getMethodName() { return method.getMethodName(); } + @Override @NonNull public AnnotationMetadata getAnnotationMetadata() { diff --git a/http-server/src/main/java/io/micronaut/http/server/RequestLifecycle.java b/http-server/src/main/java/io/micronaut/http/server/RequestLifecycle.java index 070a827af20..66dac7f7429 100644 --- a/http-server/src/main/java/io/micronaut/http/server/RequestLifecycle.java +++ b/http-server/src/main/java/io/micronaut/http/server/RequestLifecycle.java @@ -144,8 +144,7 @@ protected final ExecutionFlow> normalFlow() { return runWithFilters(() -> fulfillArguments(routeMatch) - .flatMap(rm -> routeExecutor.callRoute(context, rm, request)) - .flatMap(this::handleStatusException) + .flatMap(rm -> routeExecutor.callRoute(context, rm, request).flatMap(res -> handleStatusException(res, rm))) .onErrorResume(this::onErrorNoFilter)); } @@ -181,8 +180,7 @@ final ExecutionFlow> onErrorNoFilter(Throwable t) { } try { return ExecutionFlow.just(errorRoute) - .flatMap(routeMatch -> routeExecutor.callRoute(context, routeMatch, request)) - .flatMap(this::handleStatusException) + .flatMap(routeMatch -> routeExecutor.callRoute(context, routeMatch, request).flatMap(res -> handleStatusException(res, routeMatch))) .onErrorResume(u -> createDefaultErrorResponseFlow(request, u)) .>map(response -> { response.setAttribute(HttpAttributes.EXCEPTION, cause); @@ -266,7 +264,8 @@ protected final ExecutionFlow> runWithFilters(Supplier> processResponse(HttpRequest request, HttpResponse response) { RequestLifecycle.this.request = request; - return handleStatusException((MutableHttpResponse) response) + RouteInfo routeInfo = response.getAttribute(HttpAttributes.ROUTE_INFO, RouteInfo.class).orElse(null); + return handleStatusException((MutableHttpResponse) response, routeInfo) .onErrorResume(throwable -> onErrorNoFilter(throwable)); } @@ -279,14 +278,17 @@ protected ExecutionFlow> processFailure(HttpRequest return filterRunner.run(request); } - private ExecutionFlow> handleStatusException(MutableHttpResponse response) { - RouteInfo routeInfo = response.getAttribute(HttpAttributes.ROUTE_INFO, RouteInfo.class).orElse(null); + private ExecutionFlow> handleStatusException(MutableHttpResponse response, RouteMatch routeMatch) { + RouteInfo routeInfo = routeMatch == null ? null : routeMatch.getRouteInfo(); + return handleStatusException(response, routeInfo); + } + + private ExecutionFlow> handleStatusException(MutableHttpResponse response, RouteInfo routeInfo) { if (response.code() >= 400 && routeInfo != null && !routeInfo.isErrorRoute()) { RouteMatch statusRoute = routeExecutor.findStatusRoute(request, response.status(), routeInfo); if (statusRoute != null) { return fulfillArguments(statusRoute) - .flatMap(routeMatch -> routeExecutor.callRoute(Context.empty(), routeMatch, request)) - .flatMap(this::handleStatusException) + .flatMap(rm -> routeExecutor.callRoute(Context.empty(), rm, request).flatMap(res -> handleStatusException(res, rm))) .onErrorResume(this::onErrorNoFilter); } } @@ -369,8 +371,7 @@ protected final ExecutionFlow> onStatusError(MutableHttpR Optional> statusRoute = routeExecutor.router.findStatusRoute(defaultResponse.status(), request); if (statusRoute.isPresent()) { return runWithFilters(() -> fulfillArguments(statusRoute.get()) - .flatMap(routeMatch -> routeExecutor.callRoute(context, routeMatch, request)) - .flatMap(this::handleStatusException) + .flatMap(routeMatch -> routeExecutor.callRoute(context, routeMatch, request).flatMap(res -> handleStatusException(res, routeMatch))) .onErrorResume(this::onErrorNoFilter)); } if (request.getMethod() != HttpMethod.HEAD) { diff --git a/http/src/main/java/io/micronaut/http/uri/UriMatchTemplate.java b/http/src/main/java/io/micronaut/http/uri/UriMatchTemplate.java index 0e402d8d41d..ab4491db89c 100644 --- a/http/src/main/java/io/micronaut/http/uri/UriMatchTemplate.java +++ b/http/src/main/java/io/micronaut/http/uri/UriMatchTemplate.java @@ -15,6 +15,8 @@ */ package io.micronaut.http.uri; +import io.micronaut.core.annotation.NonNull; +import io.micronaut.core.annotation.Nullable; import io.micronaut.core.util.ObjectUtils; import java.util.ArrayList; @@ -45,8 +47,8 @@ public class UriMatchTemplate extends UriTemplate implements UriMatcher { private final boolean exactMatch; // Matches cache - private Optional rootMatchInfo; - private Optional exactMatchInfo; + private UriMatchInfo rootMatchInfo; + private UriMatchInfo exactMatchInfo; /** * Construct a new URI template for the given template. @@ -135,10 +137,7 @@ public String toPathString() { final Optional umv = variables.stream() .filter(v -> v.getName().equals(var.get())).findFirst(); if (umv.isPresent()) { - final UriMatchVariable uriMatchVariable = umv.get(); - if (uriMatchVariable.isQuery()) { - return false; - } + return !umv.get().isQuery(); } } return true; @@ -149,11 +148,21 @@ public String toPathString() { * Match the given URI string. * * @param uri The uRI - * @return True if it matches + * @return an optional match */ @Override - @SuppressWarnings("java:S2789") // performance optimization public Optional match(String uri) { + return Optional.ofNullable(tryMatch(uri)); + } + + /** + * Match the given URI string. + * + * @param uri The uRI + * @return a match or null + */ + @Nullable + public UriMatchInfo tryMatch(@NonNull String uri) { if (uri == null) { throw new IllegalArgumentException("Argument 'uri' cannot be null"); } @@ -164,7 +173,7 @@ public Optional match(String uri) { if (isRoot && (length == 0 || (length == 1 && uri.charAt(0) == '/'))) { if (rootMatchInfo == null) { - rootMatchInfo = Optional.of(new DefaultUriMatchInfo(uri, Collections.emptyMap(), variables)); + rootMatchInfo = new DefaultUriMatchInfo(uri, Collections.emptyMap(), variables); } return rootMatchInfo; } @@ -179,16 +188,16 @@ public Optional match(String uri) { if (exactMatch) { if (uri.equals(templateString)) { if (exactMatchInfo == null) { - exactMatchInfo = Optional.of(new DefaultUriMatchInfo(uri, Collections.emptyMap(), variables)); + exactMatchInfo = new DefaultUriMatchInfo(uri, Collections.emptyMap(), variables); } return exactMatchInfo; } - return Optional.empty(); + return null; } Matcher matcher = matchPattern.matcher(uri); if (matcher.matches()) { if (variables.isEmpty()) { - return Optional.of(new DefaultUriMatchInfo(uri, Collections.emptyMap(), variables)); + return new DefaultUriMatchInfo(uri, Collections.emptyMap(), variables); } else { int count = matcher.groupCount(); Map variableMap = new LinkedHashMap<>(count); @@ -201,10 +210,10 @@ public Optional match(String uri) { String value = matcher.group(index); variableMap.put(variable.getName(), value); } - return Optional.of(new DefaultUriMatchInfo(uri, variableMap, variables)); + return new DefaultUriMatchInfo(uri, variableMap, variables); } } - return Optional.empty(); + return null; } @Override diff --git a/http/src/main/java/io/micronaut/http/uri/UriTemplate.java b/http/src/main/java/io/micronaut/http/uri/UriTemplate.java index 2fa49784f67..fc1d194eda3 100644 --- a/http/src/main/java/io/micronaut/http/uri/UriTemplate.java +++ b/http/src/main/java/io/micronaut/http/uri/UriTemplate.java @@ -75,6 +75,8 @@ public class UriTemplate implements Comparable { protected final String templateString; final List segments = new ArrayList<>(); + private String asString; + /** * Construct a new URI template for the given template. * @@ -247,7 +249,10 @@ public String expand(Object bean) { @Override public String toString() { - return toString(pathSegment -> true); + if (asString == null) { + asString = toString(pathSegment -> true); + } + return asString; } @Override diff --git a/router/src/main/java/io/micronaut/web/router/DefaultRouter.java b/router/src/main/java/io/micronaut/web/router/DefaultRouter.java index 30f1efdf016..3c848880521 100644 --- a/router/src/main/java/io/micronaut/web/router/DefaultRouter.java +++ b/router/src/main/java/io/micronaut/web/router/DefaultRouter.java @@ -43,8 +43,8 @@ import java.util.Arrays; import java.util.Collection; import java.util.Collections; -import java.util.HashMap; import java.util.HashSet; +import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Optional; @@ -62,9 +62,11 @@ @Singleton public class DefaultRouter implements Router, HttpServerFilterResolver> { - private final Map>> routesByMethod = new HashMap<>(); - private final List> statusRoutes = new ArrayList<>(); - private final List> errorRoutes = new ArrayList<>(); + private static final UriRouteInfo[] EMPTY = new UriRouteInfo[0]; + + private final Map[]> routesByMethod; + private final StatusRouteInfo[] statusRoutes; + private final ErrorRouteInfo[] errorRoutes; private final Set exposedPorts; @Nullable private Set ports; @@ -100,6 +102,9 @@ public DefaultRouter(RouteBuilder... builders) { public DefaultRouter(Collection builders) { Set exposedPorts = new HashSet<>(5); List filterRoutes = new ArrayList<>(); + Map>> routesByMethod = CollectionUtils.newHashMap(HttpMethod.values().length); + Set> statusRoutes = new LinkedHashSet<>(); + Set> errorRoutes = new LinkedHashSet<>(); for (RouteBuilder builder : builders) { List constructedRoutes = builder.getUriRoutes(); for (UriRoute route : constructedRoutes) { @@ -114,7 +119,7 @@ public DefaultRouter(Collection builders) { final StatusRouteInfo existing = statusRoutes.stream().filter(r -> r.equals(routeInfo)).findFirst().orElse(null); throw new RoutingException("Attempted to register multiple local routes for http status [" + statusRoute.status() + "]. New route: " + statusRoute + ". Existing: " + existing); } - this.statusRoutes.add(routeInfo); + statusRoutes.add(routeInfo); } for (ErrorRoute errorRoute : builder.getErrorRoutes()) { ErrorRouteInfo routeInfo = errorRoute.toRouteInfo(); @@ -122,7 +127,7 @@ public DefaultRouter(Collection builders) { final ErrorRouteInfo existing = errorRoutes.stream().filter(r -> r.equals(routeInfo)).findFirst().orElse(null); throw new RoutingException("Attempted to register multiple local routes for error [" + errorRoute.exceptionType().getSimpleName() + "]. New route: " + errorRoute + ". Existing: " + existing); } - this.errorRoutes.add(routeInfo); + errorRoutes.add(routeInfo); } filterRoutes.addAll(builder.getFilterRoutes()); exposedPorts.addAll(builder.getExposedPorts()); @@ -142,6 +147,13 @@ public DefaultRouter(Collection builders) { preconditionFilterRoutes.add(filterRoute); } } + Map[]> map = CollectionUtils.newHashMap(routesByMethod.size()); + for (Map.Entry>> e : routesByMethod.entrySet()) { + map.put(e.getKey(), finalizeRoutes(e.getValue())); + } + this.routesByMethod = map; + this.statusRoutes = statusRoutes.toArray(StatusRouteInfo[]::new); + this.errorRoutes = errorRoutes.toArray(ErrorRouteInfo[]::new); } private boolean isMatchesAll(FilterRoute filterRoute) { @@ -188,14 +200,14 @@ public Stream> find(@NonNull HttpRequest request) public Stream> find(@NonNull HttpMethod httpMethod, @NonNull CharSequence uri, @Nullable HttpRequest context) { return this.toMatches( uri.toString(), - routesByMethod.getOrDefault(httpMethod.name(), Collections.emptyList()) + routesByMethod.getOrDefault(httpMethod.name(), EMPTY) ).stream(); } @NonNull @Override public Stream> uriRoutes() { - return routesByMethod.values().stream().flatMap(List::stream); + return routesByMethod.values().stream().flatMap(Arrays::stream); } @NonNull @@ -276,11 +288,36 @@ public List> findAllClosest(@NonNull HttpRequest r } private List> toMatches(String path, List> routes) { + if (routes.size() == 1) { + UriRouteMatch match = routes.iterator().next().tryMatch(path); + if (match != null) { + return List.of(match); + } + return List.of(); + } List> uriRoutes = new ArrayList<>(routes.size()); for (UriRouteInfo route : routes) { - Optional> match = route.match(path); - if (match.isPresent()) { - uriRoutes.add((UriRouteMatch) match.get()); + UriRouteMatch match = route.tryMatch(path); + if (match != null) { + uriRoutes.add(match); + } + } + return uriRoutes; + } + + private List> toMatches(String path, UriRouteInfo[] routes) { + if (routes.length == 1) { + UriRouteMatch match = routes[0].tryMatch(path); + if (match != null) { + return List.of(match); + } + return List.of(); + } + List> uriRoutes = new ArrayList<>(routes.length); + for (UriRouteInfo route : routes) { + UriRouteMatch match = route.tryMatch(path); + if (match != null) { + uriRoutes.add(match); } } return uriRoutes; @@ -289,8 +326,7 @@ private List> toMatches(String path, List Optional> route(@NonNull HttpMethod httpMethod, @NonNull CharSequence uri) { - List> routes = routesByMethod.getOrDefault(httpMethod.name(), Collections.emptyList()); - for (UriRouteInfo uriRouteInfo : routes) { + for (UriRouteInfo uriRouteInfo : routesByMethod.getOrDefault(httpMethod.name(), EMPTY)) { Optional> match = uriRouteInfo.match(uri.toString()); if (match.isPresent()) { return (Optional) match; @@ -365,8 +401,8 @@ private Optional> findErrorRouteInternal( } return findRouteMatch(matchedRoutes, error); } else { - List> producesAllMatchedRoutes = new ArrayList<>(errorRoutes.size()); - List> producesSpecificMatchedRoutes = new ArrayList<>(errorRoutes.size()); + List> producesAllMatchedRoutes = new ArrayList<>(errorRoutes.length); + List> producesSpecificMatchedRoutes = new ArrayList<>(errorRoutes.length); for (ErrorRouteInfo errorRouteInfo : errorRoutes) { if (!errorRouteInfo.matching(request)) { continue; @@ -490,7 +526,7 @@ public List findFilters(@NonNull HttpRequest request) { public Stream> findAny(@NonNull CharSequence uri, @Nullable HttpRequest request) { List matchedRoutes = new ArrayList<>(5); final String uriStr = uri.toString(); - for (List> routes : routesByMethod.values()) { + for (UriRouteInfo[] routes : routesByMethod.values()) { for (UriRouteInfo route : routes) { if (request != null) { if (shouldSkipForPort(request, route)) { @@ -500,8 +536,7 @@ public Stream> findAny(@NonNull CharSequence uri, @Nu continue; } } - - final UriRouteMatch match = route.match(uriStr).orElse(null); + UriRouteMatch match = route.tryMatch(uriStr); if (match != null) { matchedRoutes.add(match); } @@ -514,7 +549,7 @@ public Stream> findAny(@NonNull CharSequence uri, @Nu public List> findAny(HttpRequest request) { String path = request.getPath(); List matchedRoutes = new ArrayList<>(5); - for (List> routes : routesByMethod.values()) { + for (UriRouteInfo[] routes : routesByMethod.values()) { for (UriRouteInfo route : routes) { if (shouldSkipForPort(request, route)) { continue; @@ -522,7 +557,7 @@ public List> findAny(HttpRequest request) { if (!route.matching(request)) { continue; } - final UriRouteMatch match = route.match(path).orElse(null); + UriRouteMatch match = route.tryMatch(path); if (match != null) { matchedRoutes.add(match); } @@ -535,11 +570,11 @@ private List> findInternal(HttpRequest request) String httpMethodName = request.getMethodName(); boolean permitsBody = HttpMethod.permitsRequestBody(request.getMethod()); final Collection acceptedProducedTypes = request.accept(); - List> routes = routesByMethod.getOrDefault(httpMethodName, Collections.emptyList()); - if (CollectionUtils.isEmpty(routes)) { + UriRouteInfo[] routes = routesByMethod.getOrDefault(httpMethodName, EMPTY); + if (routes.length == 0) { return Collections.emptyList(); } - List> result = new ArrayList<>(routes.size()); + List> result = new ArrayList<>(routes.length); for (UriRouteInfo route : routes) { if (shouldSkipForPort(request, route)) { continue; @@ -573,7 +608,7 @@ private boolean shouldSkipForPort(HttpRequest request, UriRouteInfo> routes) { + private UriRouteInfo[] finalizeRoutes(List> routes) { Collections.sort(routes); return routes.toArray(new UriRouteInfo[0]); } diff --git a/router/src/main/java/io/micronaut/web/router/DefaultUrlRouteInfo.java b/router/src/main/java/io/micronaut/web/router/DefaultUrlRouteInfo.java index deb38a2d272..689018285cd 100644 --- a/router/src/main/java/io/micronaut/web/router/DefaultUrlRouteInfo.java +++ b/router/src/main/java/io/micronaut/web/router/DefaultUrlRouteInfo.java @@ -79,8 +79,16 @@ public UriMatchTemplate getUriMatchTemplate() { @Override public Optional> match(String uri) { - Optional matchInfo = uriMatchTemplate.match(uri); - return matchInfo.map(info -> new DefaultUriRouteMatch<>(info, this, defaultCharset, conversionService)); + return uriMatchTemplate.match(uri).map(info -> new DefaultUriRouteMatch<>(info, this, defaultCharset, conversionService)); + } + + @Override + public UriRouteMatch tryMatch(String uri) { + UriMatchInfo matchInfo = uriMatchTemplate.tryMatch(uri); + if (matchInfo != null) { + return new DefaultUriRouteMatch<>(matchInfo, this, defaultCharset, conversionService); + } + return null; } @Override diff --git a/router/src/main/java/io/micronaut/web/router/UriRouteInfo.java b/router/src/main/java/io/micronaut/web/router/UriRouteInfo.java index 3b45fbdb25e..19e29b0d6d0 100644 --- a/router/src/main/java/io/micronaut/web/router/UriRouteInfo.java +++ b/router/src/main/java/io/micronaut/web/router/UriRouteInfo.java @@ -15,6 +15,7 @@ */ package io.micronaut.web.router; +import io.micronaut.core.annotation.NonNull; import io.micronaut.core.annotation.Nullable; import io.micronaut.http.HttpMethod; import io.micronaut.http.uri.UriMatchTemplate; @@ -54,6 +55,17 @@ default Optional> match(URI uri) { return match(uri.toString()); } + /** + * Match this route within the given URI and produce a {@link RouteMatch} if a match is found. + * + * @param uri The URI The URI + * @return A null or a {@link RouteMatch} + */ + @Nullable + default UriRouteMatch tryMatch(@NonNull URI uri) { + return tryMatch(uri.toString()); + } + /** * Match this route within the given URI and produce a {@link RouteMatch} if a match is found. * @@ -63,6 +75,15 @@ default Optional> match(URI uri) { @Override Optional> match(String uri); + /** + * Match this route within the given URI and produce a {@link RouteMatch} if a match is found. + * + * @param uri The URI + * @return A null or a {@link RouteMatch} + */ + @Nullable + UriRouteMatch tryMatch(@NonNull String uri); + /** * @return The port the route listens to, or null if the default port */ From 1cc2d753df958e1d7ea9ca1f8c55690a8fcf182b Mon Sep 17 00:00:00 2001 From: Graeme Rocher Date: Wed, 5 Apr 2023 21:13:22 +0200 Subject: [PATCH 672/743] Performance optimizations: cache executor, optimize HttpMethod enum (#9063) --- .../http/server/RequestLifecycle.java | 21 +++---- .../micronaut/http/server/RouteExecutor.java | 17 +++--- .../java/io/micronaut/http/HttpMethod.java | 48 ++++++++++++---- .../bind/DefaultRequestBinderRegistry.java | 5 +- .../binders/DefaultBodyAnnotationBinder.java | 3 +- .../binders/QueryValueArgumentBinder.java | 3 +- .../web/router/DefaultRouteBuilder.java | 57 ++++++++++++++----- .../web/router/DefaultRouteInfo.java | 7 +++ .../micronaut/web/router/DefaultRouter.java | 4 +- .../web/router/DefaultUrlRouteInfo.java | 27 ++++++++- .../io/micronaut/web/router/RouteInfo.java | 9 +++ 11 files changed, 148 insertions(+), 53 deletions(-) diff --git a/http-server/src/main/java/io/micronaut/http/server/RequestLifecycle.java b/http-server/src/main/java/io/micronaut/http/server/RequestLifecycle.java index 66dac7f7429..7d9130745e1 100644 --- a/http-server/src/main/java/io/micronaut/http/server/RequestLifecycle.java +++ b/http-server/src/main/java/io/micronaut/http/server/RequestLifecycle.java @@ -106,18 +106,19 @@ protected final void multipartEnabled(boolean multipartEnabled) { protected final ExecutionFlow> normalFlow() { ServerRequestContext.set(request); - MediaType contentType = request.getContentType().orElse(null); - if (!multipartEnabled && - contentType != null && + if (!multipartEnabled) { + MediaType contentType = request.getContentType().orElse(null); + if (contentType != null && contentType.equals(MediaType.MULTIPART_FORM_DATA_TYPE)) { - if (LOG.isDebugEnabled()) { - LOG.debug("Multipart uploads have been disabled via configuration. Rejected request for URI {}, method {}, and content type {}", request.getUri(), - request.getMethodName(), contentType); + if (LOG.isDebugEnabled()) { + LOG.debug("Multipart uploads have been disabled via configuration. Rejected request for URI {}, method {}, and content type {}", request.getUri(), + request.getMethodName(), contentType); + } + return onStatusError( + HttpResponse.status(HttpStatus.UNSUPPORTED_MEDIA_TYPE), + "Content Type [" + contentType + "] not allowed" + ); } - return onStatusError( - HttpResponse.status(HttpStatus.UNSUPPORTED_MEDIA_TYPE), - "Content Type [" + contentType + "] not allowed" - ); } UriRouteMatch routeMatch = routeExecutor.findRouteMatch(request); diff --git a/http-server/src/main/java/io/micronaut/http/server/RouteExecutor.java b/http-server/src/main/java/io/micronaut/http/server/RouteExecutor.java index 7033a63b267..95203724d3f 100644 --- a/http-server/src/main/java/io/micronaut/http/server/RouteExecutor.java +++ b/http-server/src/main/java/io/micronaut/http/server/RouteExecutor.java @@ -420,38 +420,37 @@ private ExecutionFlow> fromImperativeExecute(HttpRequest< ExecutionFlow> callRoute(ContextView contextFromFilter, RouteMatch routeMatch, HttpRequest request) { RouteInfo routeInfo = routeMatch.getRouteInfo(); - ExecutorService executorService = findExecutor(routeInfo); - Supplier>> flowSupplier = () -> executeRouteAndConvertBody(routeMatch, request); + ExecutorService executorService = routeInfo.getExecutor(serverConfiguration.getThreadSelection()); ExecutionFlow> executeMethodResponseFlow; if (executorService != null) { if (routeInfo.isSuspended()) { executeMethodResponseFlow = ReactiveExecutionFlow.fromPublisher(Mono.deferContextual(contextView -> { coroutineHelper.ifPresent(helper -> helper.setupCoroutineContext(request, contextView)); return Mono.from( - ReactiveExecutionFlow.fromFlow(flowSupplier.get()).toPublisher() + ReactiveExecutionFlow.fromFlow(executeRouteAndConvertBody(routeMatch, request)).toPublisher() ); }).contextWrite(contextFromFilter)) .putInContext(ServerRequestContext.KEY, request); } else if (routeInfo.isReactive()) { - executeMethodResponseFlow = ReactiveExecutionFlow.async(executorService, flowSupplier) + executeMethodResponseFlow = ReactiveExecutionFlow.async(executorService, () -> executeRouteAndConvertBody(routeMatch, request)) .putInContext(ServerRequestContext.KEY, request); } else { - executeMethodResponseFlow = ExecutionFlow.async(executorService, flowSupplier); + executeMethodResponseFlow = ExecutionFlow.async(executorService, () -> executeRouteAndConvertBody(routeMatch, request)); } } else { if (routeInfo.isSuspended()) { executeMethodResponseFlow = ReactiveExecutionFlow.fromPublisher(Mono.deferContextual(contextView -> { coroutineHelper.ifPresent(helper -> helper.setupCoroutineContext(request, contextView)); return Mono.from( - ReactiveExecutionFlow.fromFlow(flowSupplier.get()).toPublisher() + ReactiveExecutionFlow.fromFlow(executeRouteAndConvertBody(routeMatch, request)).toPublisher() ); }).contextWrite(contextFromFilter)) .putInContext(ServerRequestContext.KEY, request); } else if (routeInfo.isReactive()) { - executeMethodResponseFlow = ReactiveExecutionFlow.fromFlow(flowSupplier.get()) + executeMethodResponseFlow = ReactiveExecutionFlow.fromFlow(executeRouteAndConvertBody(routeMatch, request)) .putInContext(ServerRequestContext.KEY, request); } else { - executeMethodResponseFlow = flowSupplier.get(); + executeMethodResponseFlow = executeRouteAndConvertBody(routeMatch, request); } } return executeMethodResponseFlow; @@ -479,7 +478,7 @@ ExecutionFlow> createResponseForBody(HttpRequest reque if (body == null) { if (routeInfo.isVoid()) { MutableHttpResponse data = forStatus(routeInfo); - if (HttpMethod.permitsRequestBody(request.getMethod())) { + if (request.getMethod().permitsRequestBody()) { data.header(HttpHeaders.CONTENT_LENGTH, "0"); } outgoingResponse = ExecutionFlow.just(data); diff --git a/http/src/main/java/io/micronaut/http/HttpMethod.java b/http/src/main/java/io/micronaut/http/HttpMethod.java index ed78490ac1f..6d952803bc8 100644 --- a/http/src/main/java/io/micronaut/http/HttpMethod.java +++ b/http/src/main/java/io/micronaut/http/HttpMethod.java @@ -26,52 +26,60 @@ public enum HttpMethod implements CharSequence { /** * See https://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.2. */ - OPTIONS, + OPTIONS(false, true), /** * See https://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.3. */ - GET, + GET(false, false), /** * See https://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.4. */ - HEAD, + HEAD(false, false), /** * See https://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.5. */ - POST, + POST(true, true), /** * See https://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.6. */ - PUT, + PUT(true, true), /** * See https://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.7. */ - DELETE, + DELETE(false, true), /** * See https://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.8. */ - TRACE, + TRACE(false, false), /** * See https://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.9. */ - CONNECT, + CONNECT(false, false), /** * See https://tools.ietf.org/html/rfc5789. */ - PATCH, + PATCH(true, true), /** * A custom non-standard HTTP method. */ - CUSTOM; + CUSTOM(false, true); + + private final boolean requiresRequestBody; + private final boolean permitsRequestBody; + + HttpMethod(boolean requiresRequestBody, boolean permisRequestBody) { + this.requiresRequestBody = requiresRequestBody; + this.permitsRequestBody = permisRequestBody; + } @Override public int length() { @@ -88,6 +96,26 @@ public CharSequence subSequence(int start, int end) { return name().subSequence(start, end); } + /** + * Whether the given method requires a request body. + * + * @return Does the method require a request body. + * @since 4.0.0 + */ + public boolean requiresRequestBody() { + return requiresRequestBody; + } + + /** + * Whether the given method allows a request body. + * + * @return Does the method allows a request body. + * @since 4.0.0 + */ + public boolean permitsRequestBody() { + return permitsRequestBody; + } + /** * Whether the given method requires a request body. * diff --git a/http/src/main/java/io/micronaut/http/bind/DefaultRequestBinderRegistry.java b/http/src/main/java/io/micronaut/http/bind/DefaultRequestBinderRegistry.java index 53caac6581d..e996e946058 100644 --- a/http/src/main/java/io/micronaut/http/bind/DefaultRequestBinderRegistry.java +++ b/http/src/main/java/io/micronaut/http/bind/DefaultRequestBinderRegistry.java @@ -25,7 +25,6 @@ import io.micronaut.core.util.clhm.ConcurrentLinkedHashMap; import io.micronaut.http.FullHttpRequest; import io.micronaut.http.HttpHeaders; -import io.micronaut.http.HttpMethod; import io.micronaut.http.HttpParameters; import io.micronaut.http.HttpRequest; import io.micronaut.http.PushCapableHttpRequest; @@ -103,7 +102,7 @@ public DefaultRequestBinderRegistry(ConversionService conversionService, List) (argument, source) -> () -> Optional.of(source.getHeaders())); byType.put(Argument.of(HttpRequest.class).typeHashCode(), (RequestArgumentBinder>) (argument, source) -> { - if (HttpMethod.permitsRequestBody(source.getMethod())) { + if (source.getMethod().permitsRequestBody()) { Optional> typeVariable = argument.getFirstTypeVariable() .filter(arg -> arg.getType() != Object.class) .filter(arg -> arg.getType() != Void.class); @@ -118,7 +117,7 @@ public DefaultRequestBinderRegistry(ConversionService conversionService, List> typeVariable = argument.getFirstTypeVariable() .filter(arg -> arg.getType() != Object.class) .filter(arg -> arg.getType() != Void.class); - if (typeVariable.isPresent() && HttpMethod.permitsRequestBody(source.getMethod())) { + if (typeVariable.isPresent() && source.getMethod().permitsRequestBody()) { if (source.getBody().isPresent()) { return () -> Optional.of(new PushCapableFullHttpRequest((PushCapableHttpRequest) source, typeVariable.get())); } else { diff --git a/http/src/main/java/io/micronaut/http/bind/binders/DefaultBodyAnnotationBinder.java b/http/src/main/java/io/micronaut/http/bind/binders/DefaultBodyAnnotationBinder.java index 36acedf5805..19714da1bb8 100644 --- a/http/src/main/java/io/micronaut/http/bind/binders/DefaultBodyAnnotationBinder.java +++ b/http/src/main/java/io/micronaut/http/bind/binders/DefaultBodyAnnotationBinder.java @@ -19,7 +19,6 @@ import io.micronaut.core.convert.ArgumentConversionContext; import io.micronaut.core.convert.ConversionService; import io.micronaut.core.convert.value.ConvertibleValues; -import io.micronaut.http.HttpMethod; import io.micronaut.http.HttpRequest; import io.micronaut.http.annotation.Body; @@ -51,7 +50,7 @@ public Class getAnnotationType() { @Override public BindingResult bind(ArgumentConversionContext context, HttpRequest source) { - if (!HttpMethod.permitsRequestBody(source.getMethod())) { + if (!source.getMethod().permitsRequestBody()) { return BindingResult.unsatisfied(); } diff --git a/http/src/main/java/io/micronaut/http/bind/binders/QueryValueArgumentBinder.java b/http/src/main/java/io/micronaut/http/bind/binders/QueryValueArgumentBinder.java index a263af46649..f1e20470c8f 100644 --- a/http/src/main/java/io/micronaut/http/bind/binders/QueryValueArgumentBinder.java +++ b/http/src/main/java/io/micronaut/http/bind/binders/QueryValueArgumentBinder.java @@ -23,7 +23,6 @@ import io.micronaut.core.convert.value.ConvertibleMultiValues; import io.micronaut.core.type.Argument; import io.micronaut.http.HttpAttributes; -import io.micronaut.http.HttpMethod; import io.micronaut.http.HttpRequest; import io.micronaut.http.annotation.QueryValue; import io.micronaut.http.uri.UriMatchInfo; @@ -73,7 +72,7 @@ public BindingResult bind(ArgumentConversionContext context, HttpRequest argument = context.getArgument(); AnnotationMetadata annotationMetadata = argument.getAnnotationMetadata(); - if (HttpMethod.permitsRequestBody(source.getMethod()) && !annotationMetadata.hasAnnotation(QueryValue.class)) { + if (source.getMethod().permitsRequestBody() && !annotationMetadata.hasAnnotation(QueryValue.class)) { // During the unmatched check avoid requests that don't allow bodies return BindingResult.unsatisfied(); } diff --git a/router/src/main/java/io/micronaut/web/router/DefaultRouteBuilder.java b/router/src/main/java/io/micronaut/web/router/DefaultRouteBuilder.java index ad4583c5c03..c375de9d422 100644 --- a/router/src/main/java/io/micronaut/web/router/DefaultRouteBuilder.java +++ b/router/src/main/java/io/micronaut/web/router/DefaultRouteBuilder.java @@ -36,6 +36,9 @@ import io.micronaut.inject.BeanDefinition; import io.micronaut.inject.ExecutableMethod; import io.micronaut.inject.MethodExecutionHandle; +import io.micronaut.inject.MethodReference; +import io.micronaut.scheduling.executor.ExecutorSelector; +import io.micronaut.scheduling.executor.ThreadSelection; import io.micronaut.web.router.exceptions.RoutingException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -52,6 +55,7 @@ import java.util.Objects; import java.util.Optional; import java.util.Set; +import java.util.concurrent.ExecutorService; import java.util.function.Predicate; import java.util.function.Supplier; @@ -75,6 +79,7 @@ public abstract class DefaultRouteBuilder implements RouteBuilder { protected final UriNamingStrategy uriNamingStrategy; protected final ConversionService conversionService; protected final Charset defaultCharset; + private final ExecutorSelector executorSelector; private DefaultUriRoute currentParentRoute; private final List uriRoutes = new ArrayList<>(); @@ -110,8 +115,10 @@ public DefaultRouteBuilder(ExecutionHandleLocator executionHandleLocator, UriNam if (executionHandleLocator instanceof ApplicationContext applicationContext) { Environment environment = applicationContext.getEnvironment(); defaultCharset = environment.get("micronaut.application.default-charset", Charset.class, StandardCharsets.UTF_8); + this.executorSelector = applicationContext.findBean(ExecutorSelector.class).orElse(null); } else { defaultCharset = StandardCharsets.UTF_8; + this.executorSelector = null; } } @@ -735,12 +742,13 @@ public int hashCode() { /** * The default route impl. */ - class DefaultUriRoute extends AbstractRoute implements UriRoute { + final class DefaultUriRoute extends AbstractRoute implements UriRoute { final String httpMethodName; final HttpMethod httpMethod; final UriMatchTemplate uriMatchTemplate; final List nestedRoutes = new ArrayList<>(2); private Integer port; + private final RouteExecutorSelector executorSelector; /** * @param httpMethod The HTTP method @@ -863,22 +871,25 @@ class DefaultUriRoute extends AbstractRoute implements UriRoute { this.httpMethod = httpMethod; this.uriMatchTemplate = uriTemplate; this.httpMethodName = httpMethodName; + this.executorSelector = new RouteExecutorSelector(); } @Override public UriRouteInfo toRouteInfo() { return new DefaultUrlRouteInfo<>( - httpMethod, - uriMatchTemplate, - defaultCharset, - targetMethod, - bodyArgumentName, - bodyArgument, - consumesMediaTypes, - producesMediaTypes, - conditions, - port, - conversionService); + httpMethod, + uriMatchTemplate, + defaultCharset, + targetMethod, + bodyArgumentName, + bodyArgument, + consumesMediaTypes, + producesMediaTypes, + conditions, + port, + conversionService, + executorSelector + ); } @Override @@ -964,12 +975,32 @@ public UriMatchTemplate getUriMatchTemplate() { public int compareTo(UriRoute o) { return uriMatchTemplate.compareTo(o.getUriMatchTemplate()); } + + private final class RouteExecutorSelector implements ExecutorSelector { + @Override + public Optional select(MethodReference method, ThreadSelection threadSelection) { + if (DefaultRouteBuilder.this.executorSelector != null) { + return DefaultRouteBuilder.this.executorSelector.select(targetMethod.getExecutableMethod(), threadSelection); + } else { + return Optional.empty(); + } + } + + @Override + public Optional select(String name) { + if (DefaultRouteBuilder.this.executorSelector != null) { + return DefaultRouteBuilder.this.executorSelector.select(name); + } else { + return Optional.empty(); + } + } + } } /** * Define a single route. */ - class DefaultSingleRoute extends DefaultResourceRoute { + final class DefaultSingleRoute extends DefaultResourceRoute { /** * @param resourceRoutes The resource routes diff --git a/router/src/main/java/io/micronaut/web/router/DefaultRouteInfo.java b/router/src/main/java/io/micronaut/web/router/DefaultRouteInfo.java index 6a7778aa887..ffdb03a4a0e 100644 --- a/router/src/main/java/io/micronaut/web/router/DefaultRouteInfo.java +++ b/router/src/main/java/io/micronaut/web/router/DefaultRouteInfo.java @@ -29,10 +29,12 @@ import io.micronaut.http.annotation.Produces; import io.micronaut.http.annotation.Status; import io.micronaut.http.sse.Event; +import io.micronaut.scheduling.executor.ThreadSelection; import java.util.Collection; import java.util.List; import java.util.Optional; +import java.util.concurrent.ExecutorService; /** * The default route info implementation. @@ -284,6 +286,11 @@ public boolean isPermitsRequestBody() { return isPermitsBody; } + @Override + public ExecutorService getExecutor(ThreadSelection threadSelection) { + return null; + } + @Override public AnnotationMetadata getAnnotationMetadata() { return annotationMetadata; diff --git a/router/src/main/java/io/micronaut/web/router/DefaultRouter.java b/router/src/main/java/io/micronaut/web/router/DefaultRouter.java index 3c848880521..15f02d96a45 100644 --- a/router/src/main/java/io/micronaut/web/router/DefaultRouter.java +++ b/router/src/main/java/io/micronaut/web/router/DefaultRouter.java @@ -238,7 +238,7 @@ public List> findAllClosest(@NonNull HttpRequest r uriRoutes = mostSpecific; } } - boolean permitsBody = HttpMethod.permitsRequestBody(request.getMethod()); + boolean permitsBody = request.getMethod().permitsRequestBody(); int routeCount = uriRoutes.size(); if (routeCount > 1 && permitsBody) { final MediaType contentType = request.getContentType().orElse(MediaType.ALL_TYPE); @@ -568,7 +568,7 @@ public List> findAny(HttpRequest request) { private List> findInternal(HttpRequest request) { String httpMethodName = request.getMethodName(); - boolean permitsBody = HttpMethod.permitsRequestBody(request.getMethod()); + boolean permitsBody = request.getMethod().permitsRequestBody(); final Collection acceptedProducedTypes = request.accept(); UriRouteInfo[] routes = routesByMethod.getOrDefault(httpMethodName, EMPTY); if (routes.length == 0) { diff --git a/router/src/main/java/io/micronaut/web/router/DefaultUrlRouteInfo.java b/router/src/main/java/io/micronaut/web/router/DefaultUrlRouteInfo.java index 689018285cd..801e99a5a81 100644 --- a/router/src/main/java/io/micronaut/web/router/DefaultUrlRouteInfo.java +++ b/router/src/main/java/io/micronaut/web/router/DefaultUrlRouteInfo.java @@ -25,10 +25,13 @@ import io.micronaut.http.uri.UriMatchInfo; import io.micronaut.http.uri.UriMatchTemplate; import io.micronaut.inject.MethodExecutionHandle; +import io.micronaut.scheduling.executor.ExecutorSelector; +import io.micronaut.scheduling.executor.ThreadSelection; import java.nio.charset.Charset; import java.util.List; import java.util.Optional; +import java.util.concurrent.ExecutorService; import java.util.function.Predicate; /** @@ -47,6 +50,9 @@ public final class DefaultUrlRouteInfo extends DefaultRequestMatcher private final Charset defaultCharset; private final Integer port; private final ConversionService conversionService; + private final ExecutorSelector executorSelector; + private boolean noExecutor; + private ExecutorService executor; public DefaultUrlRouteInfo(HttpMethod httpMethod, UriMatchTemplate uriMatchTemplate, @@ -58,13 +64,15 @@ public DefaultUrlRouteInfo(HttpMethod httpMethod, List producesMediaTypes, List>> predicates, Integer port, - ConversionService conversionService) { - super(targetMethod, bodyArgument, bodyArgumentName, consumesMediaTypes, producesMediaTypes, HttpMethod.permitsRequestBody(httpMethod), false, predicates); + ConversionService conversionService, + ExecutorSelector executorSelector) { + super(targetMethod, bodyArgument, bodyArgumentName, consumesMediaTypes, producesMediaTypes, httpMethod.permitsRequestBody(), false, predicates); this.httpMethod = httpMethod; this.uriMatchTemplate = uriMatchTemplate; this.defaultCharset = defaultCharset; this.port = port; this.conversionService = conversionService; + this.executorSelector = executorSelector; } @Override @@ -114,4 +122,19 @@ public String toString() { .append(")") .toString(); } + + @Override + public ExecutorService getExecutor(ThreadSelection threadSelection) { + if (executorSelector == null || noExecutor) { + return null; + } else { + this.executor = + executorSelector.select(getTargetMethod(), threadSelection) + .orElse(null); + if (executor == null) { + noExecutor = true; + } + return executor; + } + } } diff --git a/router/src/main/java/io/micronaut/web/router/RouteInfo.java b/router/src/main/java/io/micronaut/web/router/RouteInfo.java index 8292a95e6b6..ca466d7694c 100644 --- a/router/src/main/java/io/micronaut/web/router/RouteInfo.java +++ b/router/src/main/java/io/micronaut/web/router/RouteInfo.java @@ -25,11 +25,13 @@ import io.micronaut.http.HttpStatus; import io.micronaut.http.MediaType; import io.micronaut.http.annotation.Body; +import io.micronaut.scheduling.executor.ThreadSelection; import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Optional; +import java.util.concurrent.ExecutorService; /** * Common information shared between route and route match. @@ -225,4 +227,11 @@ The getBodyArgument() method returns arguments for functions where it is */ boolean isPermitsRequestBody(); + /** + * @param threadSelection The thread selection + * @return The route executor + * @since 4.0.0 + */ + @Nullable + ExecutorService getExecutor(@Nullable ThreadSelection threadSelection); } From 6bb27ceb3cfe8074ee1e773b65753243a2f94e28 Mon Sep 17 00:00:00 2001 From: Graeme Rocher Date: Thu, 6 Apr 2023 12:52:46 +0200 Subject: [PATCH 673/743] Fix Groovy/Spock bug in Kubernetes module (#9067) * Reproduce bug * Allow getters/setters/fields with $ for Groovy --- .../ast/utils/AstBeanPropertiesUtils.java | 2 +- .../io/micronaut/core/naming/NameUtils.java | 9 +++++-- .../core/naming/NameUtilsSpec.groovy | 4 +++ .../groovy/visitor/GroovyClassElement.java | 3 +-- .../inject/visitor/ClassElementSpec.groovy | 10 ++++--- test-suite-groovy/build.gradle | 1 + ...mParentInAnotherPackageCompiledSpec.groovy | 25 +++++++++++++++++ ...njectFromParentInAnotherPackageSpec.groovy | 27 +++++++++++++++++++ .../other/AbstractMicronautTestSpec.groovy | 13 +++++++++ 9 files changed, 86 insertions(+), 8 deletions(-) create mode 100644 test-suite-groovy/src/test/groovy/io/micronaut/inject/spock/another/InjectFromParentInAnotherPackageCompiledSpec.groovy create mode 100644 test-suite-groovy/src/test/groovy/io/micronaut/inject/spock/another/InjectFromParentInAnotherPackageSpec.groovy create mode 100644 test-suite-groovy/src/test/groovy/io/micronaut/inject/spock/other/AbstractMicronautTestSpec.groovy diff --git a/core-processor/src/main/java/io/micronaut/inject/ast/utils/AstBeanPropertiesUtils.java b/core-processor/src/main/java/io/micronaut/inject/ast/utils/AstBeanPropertiesUtils.java index cf2c0b09d95..54a49e05f4f 100644 --- a/core-processor/src/main/java/io/micronaut/inject/ast/utils/AstBeanPropertiesUtils.java +++ b/core-processor/src/main/java/io/micronaut/inject/ast/utils/AstBeanPropertiesUtils.java @@ -89,7 +89,7 @@ public static List resolveBeanProperties(PropertyElementQuery c continue; } String methodName = methodElement.getName(); - if (methodName.contains("$") || methodName.equals("getMetaClass")) { + if (methodName.equals("getMetaClass")) { continue; } boolean isAccessor = canMethodBeUsedForAccess(methodElement, accessKinds, visibility); diff --git a/core/src/main/java/io/micronaut/core/naming/NameUtils.java b/core/src/main/java/io/micronaut/core/naming/NameUtils.java index 0b918523fbc..41869276b9f 100644 --- a/core/src/main/java/io/micronaut/core/naming/NameUtils.java +++ b/core/src/main/java/io/micronaut/core/naming/NameUtils.java @@ -235,7 +235,8 @@ public static boolean isWriterName(@NonNull String methodName, @NonNull String[] int len = methodName.length(); int prefixLength = writePrefix.length(); if (len > prefixLength && methodName.startsWith(writePrefix)) { - isValid = Character.isUpperCase(methodName.charAt(prefixLength)); + char nextChar = methodName.charAt(prefixLength); + isValid = isValidCharacterAfterReaderWriterPrefix(nextChar); } if (isValid) { @@ -370,7 +371,7 @@ public static boolean isReaderName(@NonNull String methodName, @NonNull String[] int len = methodName.length(); if (len > prefixLength) { char firstVarNameChar = methodName.charAt(prefixLength); - isValid = firstVarNameChar == '_' || firstVarNameChar == '$' || Character.isUpperCase(firstVarNameChar); + isValid = isValidCharacterAfterReaderWriterPrefix(firstVarNameChar); } if (isValid) { @@ -381,6 +382,10 @@ public static boolean isReaderName(@NonNull String methodName, @NonNull String[] return isValid; } + private static boolean isValidCharacterAfterReaderWriterPrefix(char c) { + return c == '_' || c == '$' || Character.isUpperCase(c); + } + /** * Get the equivalent property name for the given getter. * diff --git a/core/src/test/groovy/io/micronaut/core/naming/NameUtilsSpec.groovy b/core/src/test/groovy/io/micronaut/core/naming/NameUtilsSpec.groovy index 26e03fa0a0e..f8cf791d234 100644 --- a/core/src/test/groovy/io/micronaut/core/naming/NameUtilsSpec.groovy +++ b/core/src/test/groovy/io/micronaut/core/naming/NameUtilsSpec.groovy @@ -297,6 +297,8 @@ class NameUtilsSpec extends Specification { "isFoo" | ["get"] | true "isfoo" | ["get"] | false "getFoo" | ["get"] | true + 'get$foo' | ["get"] | true + 'get_foo' | ["get"] | true "getfoo" | ["get"] | false "a" | ["get"] | false "foo" | ["with"] | false @@ -322,6 +324,8 @@ class NameUtilsSpec extends Specification { name | prefixes | isValid "foo" | ["set"] | false "setFoo" | ["set"] | true + 'set$foo' | ["set"] | true + 'set_foo' | ["set"] | true "setfoo" | ["set"] | false "a" | ["set"] | false "foo" | ["with"] | false diff --git a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyClassElement.java b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyClassElement.java index 9113f17e28d..e0052a821be 100644 --- a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyClassElement.java +++ b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyClassElement.java @@ -112,8 +112,7 @@ public class GroovyClassElement extends AbstractGroovyElement implements Arrayab private static final Predicate JUNK_FIELD_FILTER = m -> { String fieldName = m.getName(); - return fieldName.startsWith("$") || - fieldName.startsWith("__$") || + return fieldName.startsWith("__$") || fieldName.contains("trait$") || fieldName.equals("metaClass") || m.getDeclaringClass().equals(ClassHelper.GROOVY_OBJECT_TYPE) || diff --git a/inject-groovy/src/test/groovy/io/micronaut/inject/visitor/ClassElementSpec.groovy b/inject-groovy/src/test/groovy/io/micronaut/inject/visitor/ClassElementSpec.groovy index 371466f00ac..a6a6093e656 100644 --- a/inject-groovy/src/test/groovy/io/micronaut/inject/visitor/ClassElementSpec.groovy +++ b/inject-groovy/src/test/groovy/io/micronaut/inject/visitor/ClassElementSpec.groovy @@ -932,9 +932,13 @@ class SuccessfulTest extends AbstractExample { def allFields = classElement.getEnclosedElements(ElementQuery.ALL_FIELDS) then: props.size() == 3 - props[0].name == "ctx" - props[1].name.contains "dummy" - props[2].name.contains "sharedCtx" + props[0].name == '$spock_sharedField_sharedCtx' + props[1].name == "ctx" + props[2].name.contains "dummy" + allFields.size() == 3 + allFields[0].name == '$spock_sharedField_sharedCtx' + allFields[1].name == "ctx" + allFields[2].name.contains "dummy" } void "test fields selection"() { diff --git a/test-suite-groovy/build.gradle b/test-suite-groovy/build.gradle index 697d3f0d357..19dd0a2380e 100644 --- a/test-suite-groovy/build.gradle +++ b/test-suite-groovy/build.gradle @@ -24,6 +24,7 @@ dependencies { testImplementation project(":http-client") testImplementation project(":http-client-jdk") testImplementation project(":inject-groovy") + testImplementation project(":inject-groovy-test") testImplementation project(":http-server-netty") testImplementation project(":jackson-databind") testImplementation project(":runtime") diff --git a/test-suite-groovy/src/test/groovy/io/micronaut/inject/spock/another/InjectFromParentInAnotherPackageCompiledSpec.groovy b/test-suite-groovy/src/test/groovy/io/micronaut/inject/spock/another/InjectFromParentInAnotherPackageCompiledSpec.groovy new file mode 100644 index 00000000000..49927bd2ef1 --- /dev/null +++ b/test-suite-groovy/src/test/groovy/io/micronaut/inject/spock/another/InjectFromParentInAnotherPackageCompiledSpec.groovy @@ -0,0 +1,25 @@ +package io.micronaut.inject.spock.another + +import io.micronaut.ast.transform.test.AbstractBeanDefinitionSpec +import io.micronaut.inject.BeanDefinition + +class InjectFromParentInAnotherPackageCompiledSpec extends AbstractBeanDefinitionSpec { + + void "test compile spock specification that inherits from already compiled class"() { + given: + def definition = buildBeanDefinition('test.MySpockSpec', ''' +package test + +import io.micronaut.inject.spock.other.AbstractMicronautTestSpec +import io.micronaut.test.extensions.spock.annotation.MicronautTest + +@MicronautTest +class MySpockSpec extends AbstractMicronautTestSpec { + +} +''') + expect: + definition != null + definition.injectedFields.size() == 1 + } +} diff --git a/test-suite-groovy/src/test/groovy/io/micronaut/inject/spock/another/InjectFromParentInAnotherPackageSpec.groovy b/test-suite-groovy/src/test/groovy/io/micronaut/inject/spock/another/InjectFromParentInAnotherPackageSpec.groovy new file mode 100644 index 00000000000..dfb94507243 --- /dev/null +++ b/test-suite-groovy/src/test/groovy/io/micronaut/inject/spock/another/InjectFromParentInAnotherPackageSpec.groovy @@ -0,0 +1,27 @@ +package io.micronaut.inject.spock.another + + +import io.micronaut.core.convert.ConversionService +import io.micronaut.inject.spock.other.AbstractMicronautTestSpec +import io.micronaut.runtime.server.EmbeddedServer +import io.micronaut.test.extensions.spock.annotation.MicronautTest +import jakarta.inject.Inject +import spock.lang.Shared + +@MicronautTest +class InjectFromParentInAnotherPackageSpec extends AbstractMicronautTestSpec { + + @Inject + EmbeddedServer embeddedServer + + @Inject + @Shared + ConversionService sharedTest + + void "test parent injected"() { + expect:"parent and child beans are injected" + embeddedServer != null + sharedTest != null + sharedFromParent != null + } +} diff --git a/test-suite-groovy/src/test/groovy/io/micronaut/inject/spock/other/AbstractMicronautTestSpec.groovy b/test-suite-groovy/src/test/groovy/io/micronaut/inject/spock/other/AbstractMicronautTestSpec.groovy new file mode 100644 index 00000000000..42431db3cfe --- /dev/null +++ b/test-suite-groovy/src/test/groovy/io/micronaut/inject/spock/other/AbstractMicronautTestSpec.groovy @@ -0,0 +1,13 @@ +package io.micronaut.inject.spock.other + +import io.micronaut.context.env.Environment +import jakarta.inject.Inject +import spock.lang.Shared +import spock.lang.Specification + +abstract class AbstractMicronautTestSpec extends Specification { + + @Inject + @Shared + Environment sharedFromParent +} From 8b11219eccea719fa6d4527651fc938a1e093aa5 Mon Sep 17 00:00:00 2001 From: Jonas Konrad Date: Thu, 6 Apr 2023 16:18:19 +0200 Subject: [PATCH 674/743] Microoptimizations (#9069) * optimize executeOn * optimize application events when there are no subscribers * optimize some header lookups to use AsciiString * replace ConcurrentLinkedHashMap with ConcurrentHashMap in conversion service * Revert "optimize executeOn" This reverts commit 1c4390c602ed1c0d70834edecec73ec74efbf072. * address review * remove containServer --- .../server/stack/FullHttpStackBenchmark.java | 3 +- .../DefaultMutableConversionService.java | 30 ++++++++----- .../micronaut/core/util/CopyOnWriteMap.java | 38 ++++++++++------ .../http/netty/NettyHttpHeaders.java | 42 +++++++++++++++++- .../server/netty/RoutingInBoundHandler.java | 2 +- .../netty/decoders/HttpRequestDecoder.java | 2 +- .../micronaut/http/server/RouteExecutor.java | 2 +- .../java/io/micronaut/http/HttpHeaders.java | 44 +++++++++++++++++++ .../java/io/micronaut/http/HttpRequest.java | 16 +------ .../micronaut/http/util/HttpHeadersUtil.java | 25 +++++++++++ .../java/io/micronaut/http/util/HttpUtil.java | 23 +--------- .../http/util/HttpHeadersUtilSpec.groovy | 11 +++++ .../event/ApplicationEventPublisher.java | 11 +++++ .../ApplicationEventPublisherFactory.java | 5 +++ .../event/NoOpApplicationEventPublisher.java | 5 +++ 15 files changed, 191 insertions(+), 68 deletions(-) diff --git a/benchmarks/src/jmh/java/io/micronaut/http/server/stack/FullHttpStackBenchmark.java b/benchmarks/src/jmh/java/io/micronaut/http/server/stack/FullHttpStackBenchmark.java index 4dbea6d71fc..4914ea83f4e 100644 --- a/benchmarks/src/jmh/java/io/micronaut/http/server/stack/FullHttpStackBenchmark.java +++ b/benchmarks/src/jmh/java/io/micronaut/http/server/stack/FullHttpStackBenchmark.java @@ -29,6 +29,7 @@ import org.openjdk.jmh.annotations.Setup; import org.openjdk.jmh.annotations.State; import org.openjdk.jmh.annotations.TearDown; +import org.openjdk.jmh.profile.AsyncProfiler; import org.openjdk.jmh.runner.Runner; import org.openjdk.jmh.runner.options.Options; import org.openjdk.jmh.runner.options.OptionsBuilder; @@ -67,7 +68,7 @@ public static void main(String[] args) throws Exception { .measurementIterations(30) .mode(Mode.AverageTime) .timeUnit(TimeUnit.NANOSECONDS) -// .addProfiler(AsyncProfiler.class, "libPath=/home/yawkat/bin/async-profiler-2.9-linux-x64/build/libasyncProfiler.so;output=flamegraph") + .addProfiler(AsyncProfiler.class, "libPath=/home/yawkat/bin/async-profiler-2.9-linux-x64/build/libasyncProfiler.so;output=flamegraph") .forks(1) .jvmArgsAppend("-Djmh.executor=CUSTOM", "-Djmh.executor.class=" + JmhFastThreadLocalExecutor.class.getName()) .build(); diff --git a/core/src/main/java/io/micronaut/core/convert/DefaultMutableConversionService.java b/core/src/main/java/io/micronaut/core/convert/DefaultMutableConversionService.java index 385bea75f7a..c72e6243a55 100644 --- a/core/src/main/java/io/micronaut/core/convert/DefaultMutableConversionService.java +++ b/core/src/main/java/io/micronaut/core/convert/DefaultMutableConversionService.java @@ -33,9 +33,9 @@ import io.micronaut.core.type.Argument; import io.micronaut.core.util.ArrayUtils; import io.micronaut.core.util.CollectionUtils; +import io.micronaut.core.util.CopyOnWriteMap; import io.micronaut.core.util.ObjectUtils; import io.micronaut.core.util.StringUtils; -import io.micronaut.core.util.clhm.ConcurrentLinkedHashMap; import java.io.BufferedReader; import java.io.File; @@ -88,13 +88,12 @@ */ public class DefaultMutableConversionService implements MutableConversionService { - private static final int CACHE_MAX = 150; + private static final int CACHE_MAX = 256; + private static final int CACHE_EVICTION_BATCH = 64; private static final TypeConverter UNCONVERTIBLE = (object, targetType, context) -> Optional.empty(); private final Map typeConverters = new ConcurrentHashMap<>(); - private final Map converterCache = new ConcurrentLinkedHashMap.Builder() - .maximumWeightedCapacity(CACHE_MAX) - .build(); + private final Map converterCache = new ConcurrentHashMap<>(); /** * Constructor. @@ -130,7 +129,7 @@ public Optional convert(Object object, Class targetType, ConversionCon if (typeConverter == null) { return Optional.empty(); } else { - converterCache.put(pair, typeConverter); + addToConverterCache(pair, typeConverter); if (typeConverter == UNCONVERTIBLE) { return Optional.empty(); } else { @@ -146,10 +145,10 @@ public Optional convert(Object object, Class targetType, ConversionCon if (typeConverter == null) { typeConverter = findTypeConverter(sourceType, targetType, null); if (typeConverter == null) { - converterCache.put(pair, UNCONVERTIBLE); + addToConverterCache(pair, UNCONVERTIBLE); return Optional.empty(); } else { - converterCache.put(pair, typeConverter); + addToConverterCache(pair, typeConverter); if (typeConverter == UNCONVERTIBLE) { return Optional.empty(); } else { @@ -171,7 +170,7 @@ public boolean canConvert(Class sourceType, Class targetType) { if (typeConverter == null) { typeConverter = findTypeConverter(sourceType, targetType, null); if (typeConverter != null) { - converterCache.put(pair, typeConverter); + addToConverterCache(pair, typeConverter); return typeConverter != UNCONVERTIBLE; } return false; @@ -183,7 +182,7 @@ public boolean canConvert(Class sourceType, Class targetType) { public void addConverter(Class sourceType, Class targetType, TypeConverter typeConverter) { ConvertiblePair pair = newPair(sourceType, targetType, typeConverter); typeConverters.put(pair, typeConverter); - converterCache.put(pair, typeConverter); + addToConverterCache(pair, typeConverter); } @Override @@ -191,7 +190,14 @@ public void addConverter(Class sourceType, Class targetType, Functi ConvertiblePair pair = new ConvertiblePair(sourceType, targetType); TypeConverter typeConverter = TypeConverter.of(sourceType, targetType, function); typeConverters.put(pair, typeConverter); + addToConverterCache(pair, typeConverter); + } + + private void addToConverterCache(ConvertiblePair pair, TypeConverter typeConverter) { converterCache.put(pair, typeConverter); + if (converterCache.size() > CACHE_MAX) { + CopyOnWriteMap.evict(converterCache, CACHE_EVICTION_BATCH); + } } /** @@ -976,7 +982,7 @@ protected TypeConverter findTypeConverter(Class sourceType, Cl ConvertiblePair pair = new ConvertiblePair(sourceSuperType, targetSuperType, formattingAnnotation); typeConverter = typeConverters.get(pair); if (typeConverter != null) { - converterCache.put(pair, typeConverter); + addToConverterCache(pair, typeConverter); return typeConverter; } } @@ -988,7 +994,7 @@ protected TypeConverter findTypeConverter(Class sourceType, Cl ConvertiblePair pair = new ConvertiblePair(sourceSuperType, targetSuperType); typeConverter = typeConverters.get(pair); if (typeConverter != null) { - converterCache.put(pair, typeConverter); + addToConverterCache(pair, typeConverter); return typeConverter; } } diff --git a/core/src/main/java/io/micronaut/core/util/CopyOnWriteMap.java b/core/src/main/java/io/micronaut/core/util/CopyOnWriteMap.java index bb1b0431431..af025174f35 100644 --- a/core/src/main/java/io/micronaut/core/util/CopyOnWriteMap.java +++ b/core/src/main/java/io/micronaut/core/util/CopyOnWriteMap.java @@ -139,24 +139,36 @@ private synchronized R update(Function, R> updater) { R ret = updater.apply(next); int newSize = next.size(); if (newSize >= maxSizeWithEvictionMargin) { - // select some indices in the map to remove at random - BitSet toRemove = new BitSet(newSize); - for (int i = 0; i < EVICTION_BATCH; i++) { - setUnset(toRemove, ThreadLocalRandom.current().nextInt(newSize - i)); - } - // iterate over the map and remove those indices - Iterator iterator = next.entrySet().iterator(); - for (int i = 0; i < newSize; i++) { - iterator.next(); - if (toRemove.get(i)) { - iterator.remove(); - } - } + evict(next, EVICTION_BATCH); } actual = next; return ret; } + /** + * Evict {@code numToEvict} items from the given {@code map} at random. This is not an atomic + * operation. + * + * @param map The map to modify + * @param numToEvict The number of items to remove + */ + public static void evict(Map map, int numToEvict) { + int size = map.size(); + // select some indices in the map to remove at random + BitSet toRemove = new BitSet(size); + for (int i = 0; i < numToEvict; i++) { + setUnset(toRemove, ThreadLocalRandom.current().nextInt(size - i)); + } + // iterate over the map and remove those indices + Iterator iterator = map.entrySet().iterator(); + for (int i = 0; i < size; i++) { + iterator.next(); + if (toRemove.get(i)) { + iterator.remove(); + } + } + } + /** * Set the bit at {@code index}, with the index only counting unset bits. e.g. setting index 0 * when the first bit of the {@link BitSet} is already set would set the second bit (the first diff --git a/http-netty/src/main/java/io/micronaut/http/netty/NettyHttpHeaders.java b/http-netty/src/main/java/io/micronaut/http/netty/NettyHttpHeaders.java index 0ea1940fbd7..22101cdb2fd 100644 --- a/http-netty/src/main/java/io/micronaut/http/netty/NettyHttpHeaders.java +++ b/http-netty/src/main/java/io/micronaut/http/netty/NettyHttpHeaders.java @@ -20,14 +20,16 @@ import io.micronaut.core.convert.ConversionService; import io.micronaut.core.type.MutableHeaders; import io.micronaut.http.HttpHeaderValues; -import io.micronaut.http.HttpHeaders; import io.micronaut.http.MediaType; import io.micronaut.http.MutableHttpHeaders; +import io.micronaut.http.util.HttpHeadersUtil; import io.netty.handler.codec.http.DefaultHttpHeaders; import io.netty.handler.codec.http.HttpHeaderNames; import io.netty.handler.codec.http.HttpHeaderValidationUtil; +import jakarta.annotation.Nullable; import java.net.URI; +import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.time.Instant; import java.time.LocalDateTime; @@ -38,6 +40,7 @@ import java.util.Collection; import java.util.Collections; import java.util.List; +import java.util.Locale; import java.util.Optional; import java.util.OptionalLong; import java.util.Set; @@ -269,7 +272,7 @@ public void setConversionService(ConversionService conversionService) { @Override public Optional contentType() { // optimization to avoid ConversionService - String str = get(HttpHeaders.CONTENT_TYPE); + String str = get(HttpHeaderNames.CONTENT_TYPE); if (str != null) { try { return Optional.of(MediaType.of(str)); @@ -292,6 +295,41 @@ public OptionalLong contentLength() { return OptionalLong.empty(); } + @Override + public List accept() { + // use HttpHeaderNames instead of HttpHeaders + return MediaType.orderedOf(getAll(HttpHeaderNames.ACCEPT)); + } + + @Nullable + @Override + public Charset acceptCharset() { + String text = get(HttpHeaderNames.ACCEPT_CHARSET); + if (text == null) { + return null; + } + text = HttpHeadersUtil.splitAcceptHeader(text); + if (text != null) { + try { + return Charset.forName(text); + } catch (Exception ignored) { + } + } + // default to UTF-8 + return StandardCharsets.UTF_8; + } + + @Nullable + @Override + public Locale acceptLanguage() { + String text = get(HttpHeaderNames.ACCEPT_LANGUAGE); + if (text == null) { + return null; + } + String part = HttpHeadersUtil.splitAcceptHeader(text); + return part == null ? Locale.getDefault() : Locale.forLanguageTag(part); + } + @Override public Optional getOrigin() { return findFirst(HttpHeaderNames.ORIGIN); diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/RoutingInBoundHandler.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/RoutingInBoundHandler.java index a2a0ec95bbd..f9aceb4bbca 100644 --- a/http-server-netty/src/main/java/io/micronaut/http/server/netty/RoutingInBoundHandler.java +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/RoutingInBoundHandler.java @@ -177,7 +177,7 @@ private void cleanupRequest(ChannelHandlerContext ctx, NettyHttpRequest reque try { request.release(); } finally { - if (terminateEventPublisher != ApplicationEventPublisher.NO_OP) { + if (!terminateEventPublisher.isEmpty()) { ctx.executor().execute(() -> { try { terminateEventPublisher.publishEvent(new HttpRequestTerminatedEvent(request)); diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/decoders/HttpRequestDecoder.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/decoders/HttpRequestDecoder.java index 63c95fbeea1..877901bd162 100644 --- a/http-server-netty/src/main/java/io/micronaut/http/server/netty/decoders/HttpRequestDecoder.java +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/decoders/HttpRequestDecoder.java @@ -84,7 +84,7 @@ protected void decode(ChannelHandlerContext ctx, HttpRequest msg, List o } try { NettyHttpRequest request = new NettyHttpRequest<>(msg, ctx, conversionService, configuration); - if (httpRequestReceivedEventPublisher != ApplicationEventPublisher.NO_OP) { + if (!httpRequestReceivedEventPublisher.isEmpty()) { try { ctx.executor().execute(() -> { try { diff --git a/http-server/src/main/java/io/micronaut/http/server/RouteExecutor.java b/http-server/src/main/java/io/micronaut/http/server/RouteExecutor.java index 95203724d3f..0b6d56f2d6e 100644 --- a/http-server/src/main/java/io/micronaut/http/server/RouteExecutor.java +++ b/http-server/src/main/java/io/micronaut/http/server/RouteExecutor.java @@ -649,7 +649,7 @@ private void applyConfiguredHeaders(MutableHttpHeaders headers) { if (serverConfiguration.isDateHeader() && !headers.contains(HttpHeaders.DATE)) { headers.date(LocalDateTime.now()); } - if (!headers.contains(HttpHeaders.SERVER)) { + if (headers.get(HttpHeaders.SERVER) == null) { serverConfiguration.getServerHeader() .ifPresent(header -> headers.add(HttpHeaders.SERVER, header)); } diff --git a/http/src/main/java/io/micronaut/http/HttpHeaders.java b/http/src/main/java/io/micronaut/http/HttpHeaders.java index 91dd870e1a1..47c2d5f2f2b 100644 --- a/http/src/main/java/io/micronaut/http/HttpHeaders.java +++ b/http/src/main/java/io/micronaut/http/HttpHeaders.java @@ -17,7 +17,11 @@ import io.micronaut.core.convert.ConversionContext; import io.micronaut.core.type.Headers; +import io.micronaut.http.util.HttpHeadersUtil; +import jakarta.annotation.Nullable; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; import java.time.LocalDateTime; import java.time.ZoneId; import java.time.ZonedDateTime; @@ -26,6 +30,7 @@ import java.util.Arrays; import java.util.Collections; import java.util.List; +import java.util.Locale; import java.util.Optional; import java.util.OptionalLong; @@ -694,6 +699,45 @@ default List accept() { return MediaType.orderedOf(getAll(HttpHeaders.ACCEPT)); } + /** + * The {@code Accept-Charset} header, or {@code null} if unset. + * + * @return The {@code Accept-Charset} header + * @since 4.0.0 + */ + @Nullable + default Charset acceptCharset() { + return findFirst(HttpHeaders.ACCEPT_CHARSET) + .map(text -> { + text = HttpHeadersUtil.splitAcceptHeader(text); + if (text != null) { + try { + return Charset.forName(text); + } catch (Exception ignored) { + } + } + // default to UTF-8 + return StandardCharsets.UTF_8; + }) + .orElse(null); + } + + /** + * The {@code Accept-Language} header, or {@code null} if unset. + * + * @return The {@code Accept-Language} header + * @since 4.0.0 + */ + @Nullable + default Locale acceptLanguage() { + return findFirst(HttpHeaders.ACCEPT_LANGUAGE) + .map(text -> { + String part = HttpHeadersUtil.splitAcceptHeader(text); + return part == null ? Locale.getDefault() : Locale.forLanguageTag(part); + }) + .orElse(null); + } + /** * @return Whether the {@link HttpHeaders#CONNECTION} header is set to Keep-Alive */ diff --git a/http/src/main/java/io/micronaut/http/HttpRequest.java b/http/src/main/java/io/micronaut/http/HttpRequest.java index cee0c7c1817..b3695b4a055 100644 --- a/http/src/main/java/io/micronaut/http/HttpRequest.java +++ b/http/src/main/java/io/micronaut/http/HttpRequest.java @@ -169,21 +169,7 @@ default HttpRequest setAttribute(CharSequence name, Object value) { @Override default Optional getLocale() { - return getHeaders().findFirst(HttpHeaders.ACCEPT_LANGUAGE) - .map(text -> { - int len = text.length(); - if (len == 0 || (len == 1 && text.charAt(0) == '*')) { - return Locale.getDefault().toLanguageTag(); - } - if (text.indexOf(';') > -1) { - text = text.split(";")[0]; - } - if (text.indexOf(',') > -1) { - text = text.split(",")[0]; - } - return text; - }) - .map(Locale::forLanguageTag); + return Optional.ofNullable(getHeaders().acceptLanguage()); } /** diff --git a/http/src/main/java/io/micronaut/http/util/HttpHeadersUtil.java b/http/src/main/java/io/micronaut/http/util/HttpHeadersUtil.java index efcbc4213b2..746ef244b07 100644 --- a/http/src/main/java/io/micronaut/http/util/HttpHeadersUtil.java +++ b/http/src/main/java/io/micronaut/http/util/HttpHeadersUtil.java @@ -15,6 +15,7 @@ */ package io.micronaut.http.util; +import io.micronaut.core.annotation.Internal; import io.micronaut.core.annotation.NonNull; import io.micronaut.core.annotation.Nullable; import io.micronaut.core.util.SupplierUtil; @@ -92,4 +93,28 @@ private static String mask(@Nullable String value) { } return "*MASKED*"; } + + /** + * Split an accept-x header and get the first component. If the header is {@code *}, return + * null. + * + * @param text The input header + * @return The first part of the header, or {@code null} if the header is {@code *} + * @since 4.0.0 + */ + @Internal + @Nullable + public static String splitAcceptHeader(@NonNull String text) { + int len = text.length(); + if (len == 0 || (len == 1 && text.charAt(0) == '*')) { + return null; + } + if (text.indexOf(';') > -1) { + text = text.split(";")[0]; + } + if (text.indexOf(',') > -1) { + text = text.split(",")[0]; + } + return text; + } } diff --git a/http/src/main/java/io/micronaut/http/util/HttpUtil.java b/http/src/main/java/io/micronaut/http/util/HttpUtil.java index ca433ca6e9d..a989c04632c 100644 --- a/http/src/main/java/io/micronaut/http/util/HttpUtil.java +++ b/http/src/main/java/io/micronaut/http/util/HttpUtil.java @@ -15,7 +15,6 @@ */ package io.micronaut.http.util; -import io.micronaut.http.HttpHeaders; import io.micronaut.http.HttpMessage; import io.micronaut.http.HttpRequest; import io.micronaut.http.MediaType; @@ -77,27 +76,7 @@ public static Optional resolveCharset(HttpMessage request) { if (contentTypeCharset.isPresent()) { return contentTypeCharset; } else { - return request - .getHeaders() - .findFirst(HttpHeaders.ACCEPT_CHARSET) - .map(text -> { - int len = text.length(); - if (len == 0 || (len == 1 && text.charAt(0) == '*')) { - return StandardCharsets.UTF_8; - } - if (text.indexOf(';') > -1) { - text = text.split(";")[0]; - } - if (text.indexOf(',') > -1) { - text = text.split(",")[0]; - } - try { - return Charset.forName(text); - } catch (Exception e) { - // unsupported charset, default to UTF-8 - return StandardCharsets.UTF_8; - } - }); + return Optional.ofNullable(request.getHeaders().acceptCharset()); } } catch (UnsupportedCharsetException e) { return Optional.empty(); diff --git a/http/src/test/groovy/io/micronaut/http/util/HttpHeadersUtilSpec.groovy b/http/src/test/groovy/io/micronaut/http/util/HttpHeadersUtilSpec.groovy index 54098639c29..a3e581e30a1 100644 --- a/http/src/test/groovy/io/micronaut/http/util/HttpHeadersUtilSpec.groovy +++ b/http/src/test/groovy/io/micronaut/http/util/HttpHeadersUtilSpec.groovy @@ -67,6 +67,17 @@ class HttpHeadersUtilSpec extends Specification { appender.stop() } + def "splitAcceptHeader"(String header, String result) { + expect: + HttpHeadersUtil.splitAcceptHeader(header) == result + + where: + header | result + "fr-CH, fr;q=0.9, en;q=0.8, de;q=0.7, *;q=0.5" | "fr-CH" + "fr-CH;q=0.9, en;q=0.8, de;q=0.7, *;q=0.5" | "fr-CH" + "*" | null + } + static class MemoryAppender extends AppenderBase { final BlockingQueue events = new LinkedBlockingQueue<>() diff --git a/inject/src/main/java/io/micronaut/context/event/ApplicationEventPublisher.java b/inject/src/main/java/io/micronaut/context/event/ApplicationEventPublisher.java index 35f2ea975e6..2667a63c7cb 100644 --- a/inject/src/main/java/io/micronaut/context/event/ApplicationEventPublisher.java +++ b/inject/src/main/java/io/micronaut/context/event/ApplicationEventPublisher.java @@ -16,6 +16,7 @@ package io.micronaut.context.event; import io.micronaut.core.annotation.NonNull; + import java.util.concurrent.CompletableFuture; import java.util.concurrent.Future; @@ -63,4 +64,14 @@ static ApplicationEventPublisher noOp() { return future; } + /** + * Check whether this publisher is empty (i.e. has no listeners). If this method returns + * {@code true}, {@link #publishEvent(Object)} does not need to be called. + * + * @return {@code true} iff there are no subscribers + * @since 4.0.0 + */ + default boolean isEmpty() { + return false; + } } diff --git a/inject/src/main/java/io/micronaut/context/event/ApplicationEventPublisherFactory.java b/inject/src/main/java/io/micronaut/context/event/ApplicationEventPublisherFactory.java index dfbcbc18707..2a7e680a8cc 100644 --- a/inject/src/main/java/io/micronaut/context/event/ApplicationEventPublisherFactory.java +++ b/inject/src/main/java/io/micronaut/context/event/ApplicationEventPublisherFactory.java @@ -247,6 +247,11 @@ public Future publishEventAsync(Object event) { }); return future; } + + @Override + public boolean isEmpty() { + return lazyListeners.get().isEmpty(); + } }; } diff --git a/inject/src/main/java/io/micronaut/context/event/NoOpApplicationEventPublisher.java b/inject/src/main/java/io/micronaut/context/event/NoOpApplicationEventPublisher.java index c735e5e1c5b..5c1b0d8bff2 100644 --- a/inject/src/main/java/io/micronaut/context/event/NoOpApplicationEventPublisher.java +++ b/inject/src/main/java/io/micronaut/context/event/NoOpApplicationEventPublisher.java @@ -34,4 +34,9 @@ public void publishEvent(Object event) { public Future publishEventAsync(Object event) { return CompletableFuture.completedFuture(null); } + + @Override + public boolean isEmpty() { + return true; + } } From cc4964d0fd273856a49417549db35d8c669ab840 Mon Sep 17 00:00:00 2001 From: Denis Stepanov Date: Thu, 6 Apr 2023 09:40:56 -0600 Subject: [PATCH 675/743] Make legacy filters work like in Micronaut 3 (#9071) --- .../execution/DelayedExecutionFlowImpl.java | 250 ++---- .../http/client/netty/DefaultHttpClient.java | 8 +- .../server/netty/filters/FiltersSpec.groovy | 8 +- .../http/server/RequestLifecycle.java | 15 +- .../micronaut/http/server/RouteExecutor.java | 7 +- .../micronaut/http/filter/FilterRunner.java | 711 ++++++------------ .../http/filter/GenericHttpFilter.java | 30 +- .../execution/ReactorExecutionFlowImpl.java | 44 +- .../http/filter/FilterRunnerSpec.groovy | 67 +- 9 files changed, 409 insertions(+), 731 deletions(-) diff --git a/core/src/main/java/io/micronaut/core/execution/DelayedExecutionFlowImpl.java b/core/src/main/java/io/micronaut/core/execution/DelayedExecutionFlowImpl.java index 10ccc9fe7b1..3f5d04019f5 100644 --- a/core/src/main/java/io/micronaut/core/execution/DelayedExecutionFlowImpl.java +++ b/core/src/main/java/io/micronaut/core/execution/DelayedExecutionFlowImpl.java @@ -28,12 +28,6 @@ final class DelayedExecutionFlowImpl implements DelayedExecutionFlow { private static final Logger LOG = LoggerFactory.getLogger(DelayedExecutionFlowImpl.class); - /** - * Object used as a stand-in for a {@code null} completion to distinguish it from the - * uncompleted state. - */ - private static final Object NULL = new Object(); - /** * The head of the linked list of steps in this flow. */ @@ -50,43 +44,35 @@ final class DelayedExecutionFlowImpl implements DelayedExecutionFlow { * flow. * * @param step The step to execute first - * @param item The input item for the step + * @param executionFlow The previous execution flow */ - private static void work(Step step, Object item) { - while (true) { - item = step.apply(item); - if (item == null) { - // step suspended - break; - } - step = step.atomicSetOutput(item); - if (step == null) { - break; - } - } + private static void work(Step step, ExecutionFlow executionFlow) { + do { + executionFlow = step.apply(executionFlow); + step = step.atomicSetOutput(executionFlow); + } while (step != null); } /** - * Complete this flow with the given result. + * Complete with initial execution flow. * - * @param result The result object. May be a {@link Failure}, {@link #NULL}, or any other - * successful value. + * @param executionFlow The execution flow */ - private void complete0(@NonNull Object result) { - Step immediateStep = head.atomicSetOutput(result); + private void complete0(@NonNull ExecutionFlow executionFlow) { + Step immediateStep = head.atomicSetOutput(executionFlow); if (immediateStep != null) { - work(immediateStep, result); + work(immediateStep, executionFlow); } } @Override public void complete(T result) { - complete0(result == null ? NULL : result); + complete0(result == null ? ExecutionFlow.empty() : ExecutionFlow.just(result)); } @Override public void completeExceptionally(Throwable exc) { - complete0(new Failure(exc)); + complete0(ExecutionFlow.error(exc)); } /** @@ -100,7 +86,7 @@ public void completeExceptionally(Throwable exc) { private ExecutionFlow next(Step next) { Step oldTail = tail; tail = next; - Object output = oldTail.atomicSetNext(next); + ExecutionFlow output = oldTail.atomicSetNext(next); if (output != null) { work(next, output); } @@ -109,13 +95,12 @@ private ExecutionFlow next(Step next) { @Override public ExecutionFlow map(Function transformer) { - return next(new Map(transformer)); + return next(new Map<>(transformer)); } - @SuppressWarnings("unchecked") @Override public ExecutionFlow flatMap(Function> transformer) { - return next(new FlatMap((Function) transformer)); + return next(new FlatMap<>(transformer)); } @Override @@ -125,7 +110,7 @@ public ExecutionFlow then(Supplier> @Override public ExecutionFlow onErrorResume(Function> fallback) { - return next(new OnErrorResume(fallback)); + return next(new OnErrorResume<>(fallback)); } @Override @@ -142,29 +127,15 @@ public void onComplete(BiConsumer fn) { @Nullable @Override public ImperativeExecutionFlow tryComplete() { - Object tailOutput = tail.output; + ExecutionFlow tailOutput = tail.output; if (tailOutput != null) { - if (tailOutput instanceof Failure failure) { - return (ImperativeExecutionFlow) new ImperativeExecutionFlowImpl(null, failure.t); - } else if (tailOutput == NULL) { - return (ImperativeExecutionFlow) new ImperativeExecutionFlowImpl(null, null); - } else { - return (ImperativeExecutionFlow) new ImperativeExecutionFlowImpl(tailOutput, null); - } + return tailOutput.tryComplete(); } else { return null; } } - /** - * Special wrapper for exception results. - * - * @param t The exception of the failure - */ - private record Failure(Throwable t) { - } - - private abstract static class Step { + private abstract static class Step { /** * The next step to take, or {@code null} if there is no next step yet. */ @@ -172,27 +143,26 @@ private abstract static class Step { /** * The output of this step, or {@code null} if this step has not completed yet. */ - private volatile Object output; + private volatile ExecutionFlow output; /** - * Apply this step. Must call one of {@link #returnImmediate}, {@link #returnFlow}, - * {@link #returnError} or {@link #returnUnchanged}. + * Apply this step. * * @param input The input for the step * @return The return value of the {@code return*} method called */ - abstract Object apply(Object input); + abstract ExecutionFlow apply(ExecutionFlow input); /** * Atomically set the output of this step. If this returns non-null, the caller must call - * {@link #work(Step, Object)} with the returned step. + * {@link #work(Step, ExecutionFlow)} with the returned step. * * @param output The output of this step - * @return The next step to execute using {@link #work(Step, Object)}, or {@code null} if + * @return The next step to execute using {@link #work(Step, ExecutionFlow)}, or {@code null} if * the next step will be executed later */ @Nullable - final Step atomicSetOutput(Object output) { + final Step atomicSetOutput(ExecutionFlow output) { if (this.output != null) { // this is a best-effort check, the output field isn't always set throw new IllegalStateException("Already completed"); @@ -222,19 +192,18 @@ final Step atomicSetOutput(Object output) { /** * Atomically set the next step. If this returns non-null, the caller must call - * {@link #work(Step, Object)} with the returned output value. + * {@link #work(Step, ExecutionFlow)} with the returned output value. * * @param next The next step to execute - * @return The output value of this step, to be passed to {@link #work(Step, Object)}, or - * {@code null} if the output is not yet known and the given step will be executed later + * @return The output flow value of this step, to be passed to {@link #work(Step, ExecutionFlow)} */ @Nullable - final Object atomicSetNext(Step next) { + final ExecutionFlow atomicSetNext(Step next) { if (this.next != null) { // this is a best-effort check, the next field isn't always set throw new IllegalStateException("Already added a next step"); } - Object output = this.output; + ExecutionFlow output = this.output; if (output != null) { return output; } @@ -257,193 +226,112 @@ final Object atomicSetNext(Step next) { return null; } - /** - * Return a flow from this step (e.g. from flatMap). - * - * @param outputFlow The flow to return - * @return The value to return from {@link #work} - */ - final Object returnFlow(ExecutionFlow outputFlow) { - ImperativeExecutionFlow complete = outputFlow.tryComplete(); - if (complete != null) { - Throwable error = complete.getError(); - if (error == null) { - return returnImmediate(complete.getValue()); - } else { - return returnError(error); - } - } - - outputFlow.onComplete((v, t) -> { - Object result; - if (t == null) { - result = v == null ? NULL : v; - } else { - result = new Failure(t); - } - Step step = atomicSetOutput(result); - if (step != null) { - work(step, result); - } - }); - return null; - } - - /** - * Return an immediate successful value from this step (e.g. from map). - * - * @param o The value to return - * @return The value to return from {@link #work} - */ - final Object returnImmediate(@Nullable Object o) { - return o == null ? NULL : o; - } - - /** - * Signal that this step made no change to the input (e.g. a {@code map} when the flow has - * an error). - * - * @param input The input passed to {@link #apply} - * @return The value to return from {@link #work} - */ - final Object returnUnchanged(Object input) { - return input; - } - /** * Return an immediate failed value from this step (e.g. from map). * * @param e The exception to return * @return The value to return from {@link #work} */ - final Object returnError(Throwable e) { - return new Failure(e); + final ExecutionFlow returnError(Throwable e) { + return ExecutionFlow.error(e); } } /** * Mock step used as the head of the linked list of steps. */ - private static final class Head extends Step { + private static final class Head extends Step { + @Override - Object apply(Object input) { + ExecutionFlow apply(ExecutionFlow input) { throw new UnsupportedOperationException(); } + } - private static final class Map extends Step { - private final Function transformer; + private static final class Map extends Step { + private final Function transformer; - private Map(Function transformer) { + private Map(Function transformer) { this.transformer = transformer; } - @SuppressWarnings("unchecked") @Override - Object apply(Object input) { + ExecutionFlow apply(ExecutionFlow executionFlow) { try { - if (input instanceof Failure) { - return returnUnchanged(input); - } else if (input == NULL) { - return returnImmediate(transformer.apply(null)); - } else { - return returnImmediate(transformer.apply(input)); - } + return executionFlow.map(transformer); } catch (Exception e) { return returnError(e); } } } - private static final class FlatMap extends Step { - private final Function transformer; + private static final class FlatMap extends Step { + private final Function> transformer; - private FlatMap(Function transformer) { + private FlatMap(Function> transformer) { this.transformer = transformer; } @Override - Object apply(Object input) { - if (input instanceof Failure) { - return returnUnchanged(input); - } else { - try { - if (input == NULL) { - return returnFlow(transformer.apply(null)); - } else { - return returnFlow(transformer.apply(input)); - } - } catch (Exception e) { - return returnError(e); - } + ExecutionFlow apply(ExecutionFlow executionFlow) { + try { + return executionFlow.flatMap(transformer); + } catch (Exception e) { + return returnError(e); } } } - private static final class Then extends Step { - private final Supplier> transformer; + private static final class Then extends Step { + private final Supplier> transformer; - private Then(Supplier> transformer) { + private Then(Supplier> transformer) { this.transformer = transformer; } @Override - Object apply(Object input) { - if (input instanceof Failure) { - return returnUnchanged(input); - } else { - try { - return returnFlow(transformer.get()); - } catch (Exception e) { - return returnError(e); - } + ExecutionFlow apply(ExecutionFlow executionFlow) { + try { + return executionFlow.then(transformer); + } catch (Exception e) { + return returnError(e); } } } - private static final class OnErrorResume extends Step { - private final Function> fallback; + private static final class OnErrorResume extends Step { + private final Function> fallback; - private OnErrorResume(Function> fallback) { + private OnErrorResume(Function> fallback) { this.fallback = fallback; } @Override - Object apply(Object input) { - if (input instanceof Failure failure) { - try { - return returnFlow(fallback.apply(failure.t)); - } catch (Exception e) { - return returnError(e); - } - } else { - return returnUnchanged(input); + ExecutionFlow apply(ExecutionFlow executionFlow) { + try { + return executionFlow.onErrorResume(fallback); + } catch (Exception e) { + return returnError(e); } } } - private static final class OnComplete extends Step { + private static final class OnComplete extends Step { private final BiConsumer consumer; public OnComplete(BiConsumer consumer) { this.consumer = consumer; } - @SuppressWarnings("unchecked") @Override - Object apply(Object input) { + ExecutionFlow apply(ExecutionFlow executionFlow) { try { - if (input instanceof Failure failure) { - consumer.accept(null, failure.t); - } else if (input == NULL) { - consumer.accept(null, null); - } else { - consumer.accept((E) input, null); - } + executionFlow.onComplete(consumer); } catch (Exception e) { LOG.error("Failed to execute onComplete", e); } - return null; + return executionFlow; } } } diff --git a/http-client/src/main/java/io/micronaut/http/client/netty/DefaultHttpClient.java b/http-client/src/main/java/io/micronaut/http/client/netty/DefaultHttpClient.java index 22e2bcfbee0..ac30bdb5928 100644 --- a/http-client/src/main/java/io/micronaut/http/client/netty/DefaultHttpClient.java +++ b/http-client/src/main/java/io/micronaut/http/client/netty/DefaultHttpClient.java @@ -170,7 +170,6 @@ import reactor.core.publisher.Flux; import reactor.core.publisher.FluxSink; import reactor.core.publisher.Mono; -import reactor.util.context.Context; import java.io.Closeable; import java.io.File; @@ -1261,11 +1260,8 @@ private > Publisher applyFilte FilterRunner.sortReverse(filters); filters.add(new GenericHttpFilter.TerminalReactive(responsePublisher)); - FilterRunner runner = new FilterRunner(conversionService, filters); - Mono responseMono = Mono.deferContextual(ctx -> { - runner.reactorContext(Context.of(ctx)); - return Mono.from(ReactiveExecutionFlow.fromFlow((ExecutionFlow) runner.run(request)).toPublisher()); - }); + FilterRunner runner = new FilterRunner(filters); + Mono responseMono = Mono.from(ReactiveExecutionFlow.fromFlow((ExecutionFlow) runner.run(request)).toPublisher()); if (parentRequest != null) { responseMono = responseMono.contextWrite(c -> { // existing entry takes precedence. The parentRequest is derived from a thread diff --git a/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/filters/FiltersSpec.groovy b/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/filters/FiltersSpec.groovy index 67ba1f7d53e..81eb6367d44 100644 --- a/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/filters/FiltersSpec.groovy +++ b/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/filters/FiltersSpec.groovy @@ -113,19 +113,19 @@ class FiltersSpec extends Specification { filter3.mapExecutedOn.startsWith "io-executor" filter3.filterOrder == 3 - filter4.doFilterExecutedOn.startsWith "io-executor" + filter4.doFilterExecutedOn.startsWith "default-nioEventLoopGroup" filter4.mapExecutedOn.startsWith "io-executor" filter4.filterOrder == 4 - filter5.doFilterExecutedOn.startsWith "io-executor" + filter5.doFilterExecutedOn.startsWith "default-nioEventLoopGroup" filter5.mapExecutedOn.startsWith "io-executor" filter5.filterOrder == 5 - filter6.doFilterExecutedOn.startsWith "io-executor" + filter6.doFilterExecutedOn.startsWith "default-nioEventLoopGroup" filter6.mapExecutedOn.startsWith "io-executor" filter6.filterOrder == 6 - filter7.doFilterExecutedOn.startsWith "io-executor" + filter7.doFilterExecutedOn.startsWith "default-nioEventLoopGroup" filter7.mapExecutedOn.startsWith "io-executor" filter7.filterOrder == 7 diff --git a/http-server/src/main/java/io/micronaut/http/server/RequestLifecycle.java b/http-server/src/main/java/io/micronaut/http/server/RequestLifecycle.java index 7d9130745e1..5f16d17a45c 100644 --- a/http-server/src/main/java/io/micronaut/http/server/RequestLifecycle.java +++ b/http-server/src/main/java/io/micronaut/http/server/RequestLifecycle.java @@ -45,7 +45,6 @@ import io.micronaut.web.router.UriRouteMatch; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import reactor.util.context.Context; import java.util.ArrayList; import java.util.Collection; @@ -72,7 +71,6 @@ public class RequestLifecycle { private final RouteExecutor routeExecutor; private final RequestArgumentSatisfier requestArgumentSatisfier; private HttpRequest request; - private Context context = Context.empty(); private boolean multipartEnabled = true; /** @@ -145,7 +143,7 @@ protected final ExecutionFlow> normalFlow() { return runWithFilters(() -> fulfillArguments(routeMatch) - .flatMap(rm -> routeExecutor.callRoute(context, rm, request).flatMap(res -> handleStatusException(res, rm))) + .flatMap(rm -> routeExecutor.callRoute(rm, request).flatMap(res -> handleStatusException(res, rm))) .onErrorResume(this::onErrorNoFilter)); } @@ -181,7 +179,7 @@ final ExecutionFlow> onErrorNoFilter(Throwable t) { } try { return ExecutionFlow.just(errorRoute) - .flatMap(routeMatch -> routeExecutor.callRoute(context, routeMatch, request).flatMap(res -> handleStatusException(res, routeMatch))) + .flatMap(routeMatch -> routeExecutor.callRoute(routeMatch, request).flatMap(res -> handleStatusException(res, routeMatch))) .onErrorResume(u -> createDefaultErrorResponseFlow(request, u)) .>map(response -> { response.setAttribute(HttpAttributes.EXCEPTION, cause); @@ -256,12 +254,11 @@ protected final ExecutionFlow> runWithFilters(Supplier filters = new ArrayList<>(httpFilters.size() + 1); filters.addAll(httpFilters); - filters.add((GenericHttpFilter.TerminalWithReactorContext) (request, context) -> { + filters.add((GenericHttpFilter.Terminal) (request) -> { this.request = request; - this.context = context; return downstream.get(); }); - FilterRunner filterRunner = new FilterRunner(routeExecutor.beanContext.getConversionService(), filters) { + FilterRunner filterRunner = new FilterRunner(filters) { @Override protected ExecutionFlow> processResponse(HttpRequest request, HttpResponse response) { RequestLifecycle.this.request = request; @@ -289,7 +286,7 @@ private ExecutionFlow> handleStatusException(MutableHttpR RouteMatch statusRoute = routeExecutor.findStatusRoute(request, response.status(), routeInfo); if (statusRoute != null) { return fulfillArguments(statusRoute) - .flatMap(rm -> routeExecutor.callRoute(Context.empty(), rm, request).flatMap(res -> handleStatusException(res, rm))) + .flatMap(rm -> routeExecutor.callRoute(rm, request).flatMap(res -> handleStatusException(res, rm))) .onErrorResume(this::onErrorNoFilter); } } @@ -372,7 +369,7 @@ protected final ExecutionFlow> onStatusError(MutableHttpR Optional> statusRoute = routeExecutor.router.findStatusRoute(defaultResponse.status(), request); if (statusRoute.isPresent()) { return runWithFilters(() -> fulfillArguments(statusRoute.get()) - .flatMap(routeMatch -> routeExecutor.callRoute(context, routeMatch, request).flatMap(res -> handleStatusException(res, routeMatch))) + .flatMap(routeMatch -> routeExecutor.callRoute(routeMatch, request).flatMap(res -> handleStatusException(res, routeMatch))) .onErrorResume(this::onErrorNoFilter)); } if (request.getMethod() != HttpMethod.HEAD) { diff --git a/http-server/src/main/java/io/micronaut/http/server/RouteExecutor.java b/http-server/src/main/java/io/micronaut/http/server/RouteExecutor.java index 0b6d56f2d6e..5d2bddcf00e 100644 --- a/http-server/src/main/java/io/micronaut/http/server/RouteExecutor.java +++ b/http-server/src/main/java/io/micronaut/http/server/RouteExecutor.java @@ -64,7 +64,6 @@ import reactor.core.publisher.Mono; import reactor.core.scheduler.Scheduler; import reactor.core.scheduler.Schedulers; -import reactor.util.context.ContextView; import java.io.IOException; import java.time.LocalDateTime; @@ -418,7 +417,7 @@ private ExecutionFlow> fromImperativeExecute(HttpRequest< return ExecutionFlow.just(forStatus(routeInfo, null).body(body)); } - ExecutionFlow> callRoute(ContextView contextFromFilter, RouteMatch routeMatch, HttpRequest request) { + ExecutionFlow> callRoute(RouteMatch routeMatch, HttpRequest request) { RouteInfo routeInfo = routeMatch.getRouteInfo(); ExecutorService executorService = routeInfo.getExecutor(serverConfiguration.getThreadSelection()); ExecutionFlow> executeMethodResponseFlow; @@ -429,7 +428,7 @@ ExecutionFlow> callRoute(ContextView contextFromFilter, R return Mono.from( ReactiveExecutionFlow.fromFlow(executeRouteAndConvertBody(routeMatch, request)).toPublisher() ); - }).contextWrite(contextFromFilter)) + })) .putInContext(ServerRequestContext.KEY, request); } else if (routeInfo.isReactive()) { executeMethodResponseFlow = ReactiveExecutionFlow.async(executorService, () -> executeRouteAndConvertBody(routeMatch, request)) @@ -444,7 +443,7 @@ ExecutionFlow> callRoute(ContextView contextFromFilter, R return Mono.from( ReactiveExecutionFlow.fromFlow(executeRouteAndConvertBody(routeMatch, request)).toPublisher() ); - }).contextWrite(contextFromFilter)) + })) .putInContext(ServerRequestContext.KEY, request); } else if (routeInfo.isReactive()) { executeMethodResponseFlow = ReactiveExecutionFlow.fromFlow(executeRouteAndConvertBody(routeMatch, request)) diff --git a/http/src/main/java/io/micronaut/http/filter/FilterRunner.java b/http/src/main/java/io/micronaut/http/filter/FilterRunner.java index e7619accc1b..e5be9ff2ebe 100644 --- a/http/src/main/java/io/micronaut/http/filter/FilterRunner.java +++ b/http/src/main/java/io/micronaut/http/filter/FilterRunner.java @@ -34,26 +34,15 @@ import io.micronaut.http.reactive.execution.ReactiveExecutionFlow; import io.micronaut.inject.ExecutableMethod; import org.reactivestreams.Publisher; -import org.reactivestreams.Subscriber; -import org.reactivestreams.Subscription; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import reactor.core.CorePublisher; -import reactor.core.CoreSubscriber; import reactor.core.publisher.Mono; -import reactor.util.context.Context; -import java.util.HashMap; import java.util.List; import java.util.ListIterator; -import java.util.Map; import java.util.Objects; import java.util.Optional; -import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionStage; import java.util.concurrent.ExecutionException; import java.util.concurrent.Executor; -import java.util.function.BiConsumer; import java.util.function.Function; import java.util.function.Predicate; @@ -75,26 +64,20 @@ */ @Internal public class FilterRunner { - private static final Logger LOG = LoggerFactory.getLogger(FilterRunner.class); private static final Predicate FILTER_CONDITION_ALWAYS_TRUE = runner -> true; - private final ConversionService conversionService; /** * All filters to run. Request filters are executed in order from first to last, response * filters in the reverse order. */ private final List filters; - private Context initialReactorContext = Context.empty(); - /** * Create a new filter runner, to be used only once. * - * @param conversionService The conversion service * @param filters The filters to run */ - public FilterRunner(ConversionService conversionService, List filters) { - this.conversionService = conversionService; + public FilterRunner(List filters) { this.filters = Objects.requireNonNull(filters, "filters"); } @@ -152,19 +135,6 @@ protected ExecutionFlow> processFailure(HttpRequest return ExecutionFlow.error(failure); } - /** - * Set the initial reactor context. This is passed on to every filter that requests a reactive - * type, and, if applicable, to the - * {@link io.micronaut.http.filter.GenericHttpFilter.TerminalWithReactorContext terminal}. - * - * @param reactorContext The reactor context, may be updated by filters - * @return This filter runner, for chaining - */ - public final FilterRunner reactorContext(Context reactorContext) { - this.initialReactorContext = reactorContext; - return this; - } - /** * Execute the filters for the given request. May only be called once * @@ -174,61 +144,56 @@ public final FilterRunner reactorContext(Context reactorContext) { */ @SuppressWarnings("java:S1452") public final ExecutionFlow> run(HttpRequest request) { - return (ExecutionFlow) filterRequest(new FilterContext(request, initialReactorContext), filters.listIterator(), new HashMap<>()); + return (ExecutionFlow) filterRequest(new FilterContext(request), filters.listIterator()); } private ExecutionFlow> filterRequest(FilterContext context, - ListIterator iterator, - Map, FilterContinuationImpl>> suspended) { - GenericHttpFilter filter = iterator.next(); - return processRequestFilter(filter, context, suspended, f -> f.flatMap(newContext -> filterRequest0(newContext, iterator, suspended)) - .onErrorResume(throwable -> { - // Un-suspend possibly awaiting filter and exception filtering scenario of the http client - return filterResponse(context, iterator, throwable, suspended).map(context::withResponse); - }) + ListIterator iterator) { + return filterRequest0(context, iterator) .flatMap(newContext -> { if (newContext.response != null) { - return filterResponse(newContext, iterator, null, suspended); + return filterResponse(newContext, iterator, null); } return ExecutionFlow.error(new IllegalStateException("Request filters didn't produce any response!")); - })); + }); } private ExecutionFlow filterRequest0(FilterContext context, - ListIterator iterator, - Map, FilterContinuationImpl>> suspended) { + ListIterator iterator) { if (context.response != null) { return ExecutionFlow.just(context); } if (iterator.hasNext()) { GenericHttpFilter filter = iterator.next(); - return processRequestFilter(filter, context, suspended, f -> f.flatMap(newContext -> filterRequest0(newContext, iterator, suspended)) + return processRequestFilter(filter, context, newContext -> filterRequest0(newContext, iterator)) .onErrorResume(throwable -> { - // Un-suspend possibly awaiting filter and exception filtering scenario of the http client - return filterResponse(context, iterator, throwable, suspended).map(context::withResponse); - })); + return processFailure(context.request, throwable).map(context::withResponse) + .onErrorResume(throwable2 -> { + // Exception filtering scenario of the http client + return filterResponse(context, iterator, throwable2).map(context::withResponse); + }); + }); } else { - return ExecutionFlow.error(new IllegalStateException("Request filters didn't produce any response!")); + return ExecutionFlow.just(context); } } private ExecutionFlow> filterResponse(FilterContext context, ListIterator iterator, @Nullable - Throwable exception, - Map, FilterContinuationImpl>> suspended) { + Throwable exception) { if (iterator.hasPrevious()) { - // Walk backwards and execute response filters or un-suspend request filters waiting for the response + // Walk backwards and execute response filters GenericHttpFilter filter = iterator.previous(); - return processResponseFilter(filter, context, exception, suspended) - .flatMap(newContext -> { - if (context != newContext) { - return processResponse(newContext.request, newContext.response).map(context::withResponse); - } - return ExecutionFlow.just(newContext); - }) - .onErrorResume(throwable -> processFailure(context.request, throwable).map(context::withResponse)) - .flatMap(newContext -> filterResponse(newContext, iterator, newContext.response == null ? exception : null, suspended)); + return processResponseFilter(filter, context, exception) + .flatMap(newContext -> { + if (context != newContext) { + return processResponse(newContext.request, newContext.response).map(context::withResponse); + } + return ExecutionFlow.just(newContext); + }) + .onErrorResume(throwable -> processFailure(context.request, throwable).map(context::withResponse)) + .flatMap(newContext -> filterResponse(newContext, iterator, newContext.response == null ? exception : null)); } else if (context.response != null) { return ExecutionFlow.just(context.response); } else if (exception != null) { @@ -245,11 +210,9 @@ private ExecutionFlow> filterResponse(FilterContext context, "java:S2259", // false positive "java:S1181" // this is a framework not an application }) - private ExecutionFlow processRequestFilter(GenericHttpFilter filter, + private ExecutionFlow processRequestFilter(GenericHttpFilter filter, FilterContext context, - Map, - FilterContinuationImpl>> suspended, - Function, ExecutionFlow> downstream) { + Function> downstream) { Executor executeOn; if (filter instanceof GenericHttpFilter.Async async) { executeOn = async.executor(); @@ -261,95 +224,69 @@ private ExecutionFlow processRequestFilter(GenericHttpFilter filter, if (filter instanceof FilterMethod before) { if (before.isResponseFilter) { // skip filter, only used for response - return downstream.apply(ExecutionFlow.just(context)); + return downstream.apply(context); } ExecutionFlow filterMethodFlow; - FilterContinuationImpl continuation; - ExecutionFlow downstreamFlow; + InternalFilterContinuation continuation; if (before.isSuspended()) { - continuation = before.createContinuation(context); - downstreamFlow = downstream.apply(continuation.nextFilterFlow()); - suspended.put(filter, Map.entry(continuation.filterProcessedFlow(), continuation)); + continuation = before.createContinuation(downstream, context); } else { continuation = null; - downstreamFlow = null; } FilterMethodContext filterMethodContext = new FilterMethodContext( - context.request, - context.response, - null, - continuation); + context.request, + context.response, + null, + continuation); if (executeOn == null) { - // possibly continue with next filter filterMethodFlow = before.filter(context, filterMethodContext); } else { - if (continuation != null) { - continuation.completeOn = executeOn; - } filterMethodFlow = ExecutionFlow.async(executeOn, () -> before.filter(context, filterMethodContext)); } if (before.isSuspended()) { - // Continue executing other filters while this one is suspended - return downstreamFlow; + return filterMethodFlow; } - return downstream.apply(filterMethodFlow); + return filterMethodFlow.flatMap(downstream); } else if (filter instanceof GenericHttpFilter.AroundLegacy around) { - FilterChainImpl chainSuspensionPoint = new FilterChainImpl(conversionService, context); + FilterChainImpl chainSuspensionPoint = new FilterChainImpl(downstream, context); // Legacy `Publisher proceed(..)` filters are always suspended - suspended.put(around, Map.entry(chainSuspensionPoint.filterProcessedFlow(), chainSuspensionPoint)); - chainSuspensionPoint.completeOn = executeOn; - ExecutionFlow downstreamFlow = downstream.apply(chainSuspensionPoint.nextFilterFlow()); if (executeOn == null) { try { - around.bean().doFilter(context.request, chainSuspensionPoint).subscribe(chainSuspensionPoint); - } catch (Throwable e) { - chainSuspensionPoint.triggerFilterProcessed(context, null, e); + return chainSuspensionPoint.processResult( + around.bean().doFilter(context.request, chainSuspensionPoint) + ); + } catch (Exception e) { + return ExecutionFlow.error(e); } - return downstreamFlow; } else { return ExecutionFlow.async(executeOn, () -> { try { - around.bean().doFilter(context.request, chainSuspensionPoint).subscribe(chainSuspensionPoint); - } catch (Throwable e) { - chainSuspensionPoint.triggerFilterProcessed(context, null, e); + return chainSuspensionPoint.processResult(around.bean().doFilter(context.request, chainSuspensionPoint)); + } catch (Exception e) { + return ExecutionFlow.error(e); } - return downstreamFlow; }); } - } else if (filter instanceof GenericHttpFilter.TerminalReactive || filter instanceof GenericHttpFilter.Terminal || filter instanceof GenericHttpFilter.TerminalWithReactorContext) { + } else if (filter instanceof GenericHttpFilter.Terminal terminalFilter) { if (executeOn != null) { throw new IllegalStateException("Async terminal filters not supported"); } if (filter.isSuspended()) { throw new IllegalStateException("Terminal filters cannot be suspended"); } - ExecutionFlow> terminalFlow; - if (filter instanceof GenericHttpFilter.TerminalWithReactorContext t) { - try { - terminalFlow = t.execute(context.request, context.reactorContext); - } catch (Throwable e) { - terminalFlow = ExecutionFlow.error(e); - } - } else if (filter instanceof GenericHttpFilter.Terminal t) { - try { - terminalFlow = t.execute(context.request); - } catch (Throwable e) { - terminalFlow = ExecutionFlow.error(e); - } - } else { - terminalFlow = ReactiveExecutionFlow.fromPublisher(Mono.from(((GenericHttpFilter.TerminalReactive) filter).responsePublisher()) - .contextWrite(context.reactorContext)); + try { + return terminalFilter.execute(context.request).map(context::withResponse).flatMap(downstream); + } catch (Throwable e) { + return ExecutionFlow.error(e); } - return downstream.apply(terminalFlow.flatMap(response -> ExecutionFlow.just(context.withResponse(response)))); } else { - throw new IllegalStateException("Unknown filter type"); + throw new IllegalStateException("Unknown filter: " + filter); } } private ExecutionFlow processResponseFilter(GenericHttpFilter filter, FilterContext filterContext, - Throwable exceptionToFilter, - Map, FilterContinuationImpl>> suspended) { + Throwable exceptionToFilter) { Executor executeOn; if (filter instanceof GenericHttpFilter.Async async) { executeOn = async.executor(); @@ -358,17 +295,6 @@ private ExecutionFlow processResponseFilter(GenericHttpFilter fil executeOn = null; } - Map.Entry, FilterContinuationImpl> suspendedFilterData = suspended.get(filter); - if (suspendedFilterData != null) { - // This filter is suspended and awaiting to receive the response - ExecutionFlow filterProcessedFlow = suspendedFilterData.getKey(); - FilterContinuationImpl continuation = suspendedFilterData.getValue(); - // Resume suspended filter - continuation.resume(filterContext, exceptionToFilter); - // Filter flow might modify the context provided - return filterProcessedFlow; - } - if (exceptionToFilter != null && !filter.isFiltersException()) { return ExecutionFlow.just(filterContext); } @@ -378,10 +304,10 @@ private ExecutionFlow processResponseFilter(GenericHttpFilter fil return ExecutionFlow.error(new IllegalStateException("Response filter cannot have a continuation!")); } FilterMethodContext filterMethodContext = new FilterMethodContext( - filterContext.request, - filterContext.response, - exceptionToFilter, - null); + filterContext.request, + filterContext.response, + exceptionToFilter, + null); if (executeOn == null) { return after.filter(filterContext, filterMethodContext); } else { @@ -402,8 +328,8 @@ public static FilterMethod prepareFilterMethod(ConversionService conversi @Internal public static void validateFilterMethod(Argument[] arguments, - Argument returnType, - boolean isResponseFilter) throws IllegalArgumentException { + Argument returnType, + boolean isResponseFilter) throws IllegalArgumentException { prepareFilterMethod(ConversionService.SHARED, null, null, arguments, returnType, isResponseFilter, null); } @@ -420,7 +346,7 @@ public static FilterMethod prepareFilterMethod(ConversionService conversi Predicate filterCondition = FILTER_CONDITION_ALWAYS_TRUE; boolean skipOnError = isResponseFilter; boolean filtersException = false; - Function> continuationCreator = null; + ContinuationCreator continuationCreator = null; for (int i = 0; i < arguments.length; i++) { Argument argument = arguments[i]; if (argument.getType().isAssignableFrom(HttpRequest.class)) { @@ -465,9 +391,9 @@ public static FilterMethod prepareFilterMethod(ConversionService conversi Argument continuationReturnType = argument.getFirstTypeVariable().orElseThrow(() -> new IllegalArgumentException("Continuations must specify generic type")); if (isReactive(continuationReturnType) && continuationReturnType.getWrappedType().isAssignableFrom(MutableHttpResponse.class)) { if (isReactive(returnType)) { - continuationCreator = ctx -> new ReactiveResultAwareReactiveContinuationImpl<>(conversionService, ctx); + continuationCreator = ResultAwareReactiveContinuationImpl::new; } else { - continuationCreator = ctx -> new ReactiveContinuationImpl<>(conversionService, ctx, continuationReturnType.getType()); + continuationCreator = ReactiveContinuationImpl::new; } fulfilled[i] = ctx -> ctx.continuation; } else if (continuationReturnType.getType().isAssignableFrom(MutableHttpResponse.class)) { @@ -487,15 +413,15 @@ public static FilterMethod prepareFilterMethod(ConversionService conversi } FilterReturnHandler returnHandler = prepareReturnHandler(conversionService, returnType, isResponseFilter, continuationCreator != null, false); return new FilterMethod<>( - order, - bean, - method, - isResponseFilter, - fulfilled, - filterCondition, - continuationCreator, - filtersException, - returnHandler + order, + bean, + method, + isResponseFilter, + fulfilled, + filterCondition, + continuationCreator, + filtersException, + returnHandler ); } @@ -533,14 +459,10 @@ private static FilterReturnHandler prepareReturnHandler(ConversionService conver return FilterReturnHandler.REQUEST; } } else if (type.getType() == HttpResponse.class || type.getType() == MutableHttpResponse.class) { - if (hasContinuation) { - return FilterReturnHandler.FROM_REQUEST_RESPONSE_WITH_CONTINUATION; + if (nullable) { + return FilterReturnHandler.FROM_REQUEST_RESPONSE_NULLABLE; } else { - if (nullable) { - return FilterReturnHandler.FROM_REQUEST_RESPONSE_NULLABLE; - } else { - return FilterReturnHandler.FROM_REQUEST_RESPONSE; - } + return FilterReturnHandler.FROM_REQUEST_RESPONSE; } } } else { @@ -562,12 +484,10 @@ private static FilterReturnHandler prepareReturnHandler(ConversionService conver return next.handle(context, null, continuation); } - Mono publisher = Mono.from(Publishers.convertPublisher(conversionService, returnValue, Publisher.class)) - .contextWrite(context.reactorContext()); + Mono publisher = Mono.from(Publishers.convertPublisher(conversionService, returnValue, Publisher.class)); - if (continuation instanceof ReactiveResultAwareReactiveContinuationImpl reactiveContinuation) { - publisher.subscribe(reactiveContinuation); - return reactiveContinuation.nextFilterFlow(); + if (continuation instanceof ResultAwareContinuation resultAwareContinuation) { + return resultAwareContinuation.processResult(publisher); } return ReactiveExecutionFlow.fromPublisher(publisher).flatMap(v -> { try { @@ -581,7 +501,7 @@ private static FilterReturnHandler prepareReturnHandler(ConversionService conver var next = prepareReturnHandler(conversionService, type.getWrappedType(), isResponseFilter, hasContinuation, false); return new DelayedFilterReturnHandler(isResponseFilter, next, nullable) { @Override - protected ExecutionFlow toFlow(FilterContext context, Object returnValue, FilterContinuationImpl continuation) { + protected ExecutionFlow toFlow(FilterContext context, Object returnValue, InternalFilterContinuation continuation) { //noinspection unchecked return CompletableFutureExecutionFlow.just(((CompletionStage) returnValue).toCompletableFuture()); } @@ -599,7 +519,7 @@ record FilterMethod(FilterOrder order, FilterArgBinder[] argBinders, @Nullable Predicate filterCondition, - Function> continuationCreator, + ContinuationCreator continuationCreator, boolean filtersException, FilterReturnHandler returnHandler ) implements GenericHttpFilter, Ordered { @@ -620,8 +540,8 @@ public int getOrder() { } @SuppressWarnings("java:S1452") - public FilterContinuationImpl createContinuation(FilterContext filterContext) { - return continuationCreator.apply(filterContext); + public InternalFilterContinuation createContinuation(Function> downstream, FilterContext filterContext) { + return continuationCreator.create(downstream, filterContext); } private ExecutionFlow filter(FilterContext filterContext, @@ -634,9 +554,6 @@ private ExecutionFlow filter(FilterContext filterContext, Object returnValue = method.invoke(bean, args); return returnHandler.handle(filterContext, returnValue, methodContext.continuation); } catch (Throwable e) { - if (methodContext.continuation != null) { - return methodContext.continuation.afterMethodExecuted(e); - } return ExecutionFlow.error(e); } } @@ -652,10 +569,10 @@ private Object[] bindArgs(FilterMethodContext context) { } private record FilterMethodContext( - HttpRequest request, - @Nullable HttpResponse response, - @Nullable Throwable failure, - @Nullable FilterContinuationImpl continuation) { + HttpRequest request, + @Nullable HttpResponse response, + @Nullable Throwable failure, + @Nullable InternalFilterContinuation continuation) { } private interface FilterArgBinder { @@ -666,28 +583,18 @@ private interface FilterReturnHandler { /** * Void method that accepts a continuation. */ - FilterReturnHandler VOID_WITH_CONTINUATION = (filterContext, returnValue, continuation) -> continuation.afterMethodExecuted(); + FilterReturnHandler VOID_WITH_CONTINUATION = (filterContext, returnValue, continuation) -> ExecutionFlow.just(continuation.afterMethodContext()); /** * Void method. */ FilterReturnHandler VOID = (filterContext, returnValue, continuation) -> ExecutionFlow.just(filterContext); - /** - * Request handler that returns a response but also accepts a continuation. - */ - FilterReturnHandler FROM_REQUEST_RESPONSE_WITH_CONTINUATION = (filterContext, returnValue, continuation) -> { - if (returnValue == null) { - return continuation.afterMethodExecuted(); - } else { - return continuation.afterMethodExecuted((HttpResponse) returnValue); - } - }; /** * Request handler that returns a new request. */ FilterReturnHandler REQUEST = (filterContext, returnValue, continuation) -> ExecutionFlow.just( - filterContext.withRequest( - (HttpRequest) Objects.requireNonNull(returnValue, "Returned request must not be null, or mark the method as @Nullable") - ) + filterContext.withRequest( + (HttpRequest) Objects.requireNonNull(returnValue, "Returned request must not be null, or mark the method as @Nullable") + ) ); /** * Request handler that returns a new request (nullable). @@ -697,7 +604,7 @@ private interface FilterReturnHandler { return ExecutionFlow.just(filterContext); } return ExecutionFlow.just( - filterContext.withRequest((HttpRequest) returnValue) + filterContext.withRequest((HttpRequest) returnValue) ); }; /** @@ -706,10 +613,10 @@ private interface FilterReturnHandler { FilterReturnHandler FROM_REQUEST_RESPONSE = (filterContext, returnValue, continuation) -> { // cancel request pipeline, move immediately to response handling return ExecutionFlow.just( - filterContext - .withResponse( - (HttpResponse) Objects.requireNonNull(returnValue, "Returned response must not be null, or mark the method as @Nullable") - ) + filterContext + .withResponse( + (HttpResponse) Objects.requireNonNull(returnValue, "Returned response must not be null, or mark the method as @Nullable") + ) ); }; /** @@ -721,7 +628,7 @@ private interface FilterReturnHandler { } // cancel request pipeline, move immediately to response handling return ExecutionFlow.just( - filterContext.withResponse((HttpResponse) returnValue) + filterContext.withResponse((HttpResponse) returnValue) ); }; /** @@ -730,10 +637,10 @@ private interface FilterReturnHandler { FilterReturnHandler FROM_RESPONSE_RESPONSE = (filterContext, returnValue, continuation) -> { // cancel request pipeline, move immediately to response handling return ExecutionFlow.just( - filterContext - .withResponse( - (HttpResponse) Objects.requireNonNull(returnValue, "Returned response must not be null, or mark the method as @Nullable") - ) + filterContext + .withResponse( + (HttpResponse) Objects.requireNonNull(returnValue, "Returned response must not be null, or mark the method as @Nullable") + ) ); }; /** @@ -745,14 +652,24 @@ private interface FilterReturnHandler { } // cancel request pipeline, move immediately to response handling return ExecutionFlow.just( - filterContext.withResponse((HttpResponse) returnValue) + filterContext.withResponse((HttpResponse) returnValue) ); }; - @SuppressWarnings("java:S112") // internal interface + @SuppressWarnings("java:S112") + // internal interface ExecutionFlow handle(FilterContext context, @Nullable Object returnValue, - @Nullable FilterContinuationImpl passedOnContinuation) throws Throwable; + @Nullable InternalFilterContinuation passedOnContinuation) throws Throwable; + } + + /** + * The continuation creator. + */ + private interface ContinuationCreator { + + InternalFilterContinuation create(Function> downstream, FilterContext filterContext); + } private abstract static class DelayedFilterReturnHandler implements FilterReturnHandler { @@ -769,19 +686,19 @@ private DelayedFilterReturnHandler(boolean isResponseFilter, FilterReturnHandler @SuppressWarnings("java:S1452") protected abstract ExecutionFlow toFlow(FilterContext context, Object returnValue, - @Nullable FilterContinuationImpl continuation); + @Nullable InternalFilterContinuation continuation); @Override public ExecutionFlow handle(FilterContext context, @Nullable Object returnValue, - FilterContinuationImpl continuation) throws Throwable { + InternalFilterContinuation continuation) throws Throwable { if (returnValue == null && nullable) { return next.handle(context, null, continuation); } ExecutionFlow delayedFlow = toFlow(context, - Objects.requireNonNull(returnValue, "Returned value must not be null, or mark the method as @Nullable"), - continuation + Objects.requireNonNull(returnValue, "Returned value must not be null, or mark the method as @Nullable"), + continuation ); ImperativeExecutionFlow doneFlow = delayedFlow.tryComplete(); if (doneFlow != null) { @@ -802,183 +719,19 @@ public ExecutionFlow handle(FilterContext context, } /** - * This class implements the "continuation" request filter pattern. It is used by filters that - * accept a {@link FilterContinuation}, but also by legacy {@link HttpFilter}s.
- * Continuations give the user the choice when to proceed with filter execution. - * After the proceed is triggered the filter is essentially suspended and the next filter in the chain should be executed. - * - * @param Return value of the continuation + * The internal filter continuation implementation. + * @param The response type */ - private abstract static class FilterContinuationImpl implements FilterContinuation { - - /** - * Executor to run any downstream reactive code on. Only used by some implementations, e.g. - * it doesn't make sense for a blocking continuation. - */ - @Nullable - Executor completeOn = null; - - FilterContext filterContext; - - /** - * The future indicating that the next filter should be executed. - */ - final CompletableFuture nextFilterProcessing = new CompletableFuture<>(); - /** - * The future representing the suspension point, completing it will resume this filter processing. - */ - final CompletableFuture suspensionPoint = new CompletableFuture<>(); - /** - * The future representing the filter return value and will be completed when the filter method is finally processed. - */ - final CompletableFuture filterProcessed = new CompletableFuture<>(); - - FilterContinuationImpl(FilterContext filterContext) { - this.filterContext = filterContext; - } - - @Override - public FilterContinuation request(HttpRequest request) { - filterContext = filterContext.withRequest(Objects.requireNonNull(request, "request")); - return this; - } - - protected final void proceedRequested() { - if (!nextFilterProcessing.isDone()) { - nextFilterProcessing.complete(filterContext); - } else { - throw new IllegalStateException("Already subscribed to proceed() publisher, or filter method threw an exception and was cancelled"); - } - } - - /** - * The filter is suspended. After this filter is ready returned flow will process a next filter. - */ - public ExecutionFlow nextFilterFlow() { - return CompletableFutureExecutionFlow.just(nextFilterProcessing); - } - - /** - * The flow to continue after the suspended filter is finished. - */ - public ExecutionFlow filterProcessedFlow() { - return CompletableFutureExecutionFlow.just(filterProcessed); - } - - /** - * Resume suspended method with a new context. - * - * @param filterContext The context to resume the suspend method. - * @param throwable The exception - */ - public void resume(FilterContext filterContext, Throwable throwable) { - if (!suspensionPoint.isDone()) { - if (throwable == null) { - suspensionPoint.complete(filterContext); - } else { - suspensionPoint.completeExceptionally(throwable); - } - } else { - if (throwable == null) { - LOG.warn("Two outcomes for one continuation, this one is swallowed: {}", filterContext.response); - } else { - LOG.warn("Two outcomes for one continuation, this one is swallowed:", throwable); - } - } - } - - /** - * The filter method completed without modifying response / failed status. - */ - private ExecutionFlow afterMethodExecuted() { - return afterMethodExecuted(null, null); - } - - /** - * The filter method completed with modified response. - */ - private ExecutionFlow afterMethodExecuted(@NonNull HttpResponse response) { - return afterMethodExecuted(response, null); - } - - /** - * The filter method completed with a failure. - */ - ExecutionFlow afterMethodExecuted(@NonNull Throwable throwable) { - return afterMethodExecuted(null, throwable); - } - - /** - * Forward a given response from this suspension point. If {@link #proceed} was already - * called, this waits for the downstream filters to finish. - */ - private ExecutionFlow afterMethodExecuted(@Nullable HttpResponse newResponse, - @Nullable Throwable newFailure) { - FilterContext newFilterContext; - if (suspensionPoint.isDone()) { - // If the method modifies the response / failure, extend its filter context for downstream - // This is blocking scenario - try { - newFilterContext = suspensionPoint.get(); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - return ExecutionFlow.error(new IllegalStateException("Failed to extract suspension point result", e)); - } catch (Exception e) { - return ExecutionFlow.error(new IllegalStateException("Failed to extract suspension point result", e)); - } - } else { - newFilterContext = filterContext; - } - return asFilterProcessed(newFilterContext, newResponse, newFailure); - } - - @SuppressWarnings("java:S3776") // performance - protected void triggerFilterProcessed(FilterContext filterContext, - @Nullable - HttpResponse newResponse, - @Nullable - Throwable newFailure) { - if (!nextFilterProcessing.isDone()) { - // Publish the error to the nextFilterProcessing as well - if (newFailure == null) { - nextFilterProcessing.complete(newResponse == null ? filterContext : filterContext.withResponse(newResponse)); - } else { - nextFilterProcessing.completeExceptionally(newFailure); - } - } - if (!filterProcessed.isDone()) { - if (newFailure == null) { - filterProcessed.complete(newResponse == null ? filterContext : filterContext.withResponse(newResponse)); - } else { - filterProcessed.completeExceptionally(newFailure); - } - } else { - if (newFailure == null) { - LOG.warn("Two outcomes for one continuation, this one is swallowed: {}", newResponse); - } else { - LOG.warn("Two outcomes for one continuation, this one is swallowed:", newFailure); - } - } - } - - @NonNull - private ExecutionFlow asFilterProcessed(FilterContext filterContext, - @Nullable - HttpResponse newResponse, - @Nullable - Throwable newFailure) { - triggerFilterProcessed(filterContext, newResponse, newFailure); - return CompletableFutureExecutionFlow.just(filterProcessed); - } + private sealed interface InternalFilterContinuation extends FilterContinuation { + FilterContext afterMethodContext(); } private record FilterContext(HttpRequest request, - @Nullable HttpResponse response, - Context reactorContext) { + @Nullable HttpResponse response) { - FilterContext(HttpRequest request, Context reactorContext) { - this(request, null, reactorContext); + FilterContext(HttpRequest request) { + this(request, null); } public FilterContext withRequest(@NonNull HttpRequest request) { @@ -989,7 +742,7 @@ public FilterContext withRequest(@NonNull HttpRequest request) { throw new IllegalStateException("Cannot modify the request after response is set!"); } Objects.requireNonNull(request); - return new FilterContext(request, response, reactorContext); + return new FilterContext(request, response); } public FilterContext withResponse(@NonNull HttpResponse response) { @@ -998,172 +751,114 @@ public FilterContext withResponse(@NonNull HttpResponse response) { } Objects.requireNonNull(response); // New response should remove the failure - return new FilterContext(request, response, reactorContext); - } - - public FilterContext withReactorContext(@NonNull Context reactorContext) { - if (this.reactorContext == reactorContext) { - return this; - } - Objects.requireNonNull(reactorContext); - return new FilterContext(request, response, reactorContext); + return new FilterContext(request, response); } } /** - * Continuation implementation that yields a reactive type.
- * This class implements a bunch of interfaces that it would otherwise have to create lambdas - * for. - * - * @param The reactive type to return (e.g. Publisher, Mono, Flux...) + * The reactive continuation that processes the method return value. */ - private static class ReactiveContinuationImpl extends FilterContinuationImpl - implements CorePublisher>, Subscription, BiConsumer { - private final ConversionService conversionService; - private final Class reactiveType; - private Subscriber> subscriber = null; - private boolean addedListener = false; - - ReactiveContinuationImpl(ConversionService conversionService, FilterContext filterContext, Class reactiveType) { - super(filterContext); - this.conversionService = conversionService; - this.reactiveType = reactiveType; - } + private static final class ResultAwareReactiveContinuationImpl extends ReactiveContinuationImpl + implements ResultAwareContinuation>> { - @Override - public R proceed() { - return Publishers.convertPublisher(conversionService, this, reactiveType); + private ResultAwareReactiveContinuationImpl(Function> next, + FilterContext filterContext) { + super(next, filterContext); } - @SuppressWarnings("NullableProblems") @Override - public void subscribe(@NonNull CoreSubscriber> subscriber) { - subscribe((Subscriber>) subscriber); + public ExecutionFlow processResult(Publisher> publisher) { + return ReactiveExecutionFlow.fromPublisher(publisher).map(httpResponse -> filterContext.withResponse(httpResponse)); } + } - @Override - public void subscribe(Subscriber> s) { - if (this.subscriber != null) { - throw new IllegalStateException("Only one subscriber allowed"); - } - this.subscriber = s; + /** + * Continuation implementation that yields a reactive type.
+ * This class implements a bunch of interfaces that it would otherwise have to create lambdas + * for. + */ + private static sealed class ReactiveContinuationImpl implements FilterContinuation>>, + InternalFilterContinuation>> { - if (s instanceof CoreSubscriber cs) { - filterContext = filterContext.withReactorContext(cs.currentContext()); - } + private final Function> downstream; + protected FilterContext filterContext; - proceedRequested(); - s.onSubscribe(this); + private ReactiveContinuationImpl(Function> downstream, + FilterContext filterContext) { + this.downstream = downstream; + this.filterContext = filterContext; } @Override - public void request(long n) { - if (n > 0 && !addedListener) { - addedListener = true; - if (completeOn == null) { - suspensionPoint.whenComplete(this); - } else { - suspensionPoint.whenCompleteAsync(this, completeOn); - } - } + public FilterContinuation>> request(HttpRequest request) { + return new ReactiveContinuationImpl(downstream, filterContext.withRequest(request)); } @Override - public void cancel() { - // ignored + public Publisher> proceed() { + return ReactiveExecutionFlow.fromFlow( + downstream.apply(filterContext).>map(newFilterContext -> { + filterContext = newFilterContext; + return newFilterContext.response; + }) + ).toPublisher(); } @Override - public void accept(FilterContext filterContext, Throwable throwable) { - // Suspension point resumed - try { - if (throwable == null) { - this.filterContext = filterContext; - subscriber.onNext(filterContext.response); - subscriber.onComplete(); - } else { - subscriber.onError(throwable); - } - } catch (Throwable t) { - LOG.warn("Subscriber threw exception", t); - } + public FilterContext afterMethodContext() { + return filterContext; } } /** - * {@link FilterContinuationImpl} that is adapted for filters returning a reactive response . - * Implements the {@link Subscriber} that will subscribe to the method's return value. - * - * @param The published item type + * The internal continuation that processes the method result. + * @param The continuation result. */ - private static class ReactiveResultAwareReactiveContinuationImpl extends ReactiveContinuationImpl> - implements CoreSubscriber> { - - ReactiveResultAwareReactiveContinuationImpl(ConversionService conversionService, FilterContext filterContext) { - //noinspection unchecked,rawtypes - super(conversionService, filterContext, (Class) Publisher.class); - } - - @Override - public Publisher proceed() { - // HACK: kotlin coroutine context propagation only supports reactor types (see - // ReactorContextInjector). If we want to support our own type, we would need our own - // ContextInjector, but that interface is marked as internal. - // Another solution could be to PR kotlin to support all CorePublishers in - // ReactorContextInjector. - return Mono.from(super.proceed()); - } - - @SuppressWarnings("NullableProblems") - @Override - public void onSubscribe(@NonNull Subscription s) { - s.request(Long.MAX_VALUE); - } - - @Override - public void onNext(HttpResponse response) { - triggerFilterProcessed(filterContext, response, null); - } - - @Override - public void onError(Throwable t) { - triggerFilterProcessed(filterContext, null, t); - } + private sealed interface ResultAwareContinuation extends InternalFilterContinuation { - @Override - public void onComplete() { - if (!suspensionPoint.isDone()) { - triggerFilterProcessed(filterContext, null, new IllegalStateException("Publisher did not return response")); - } - } + ExecutionFlow processResult(T result); - @SuppressWarnings("NullableProblems") - @NonNull - @Override - public Context currentContext() { - return filterContext.reactorContext; - } } /** - * {@link ReactiveResultAwareReactiveContinuationImpl} that is adapted for legacy filters: Implements {@link FilterChain}. + * A filter chain implementation that triggers the downstream on the proceed invocation. */ - private static final class FilterChainImpl extends ReactiveResultAwareReactiveContinuationImpl> - implements ClientFilterChain, ServerFilterChain { - FilterChainImpl(ConversionService conversionService, FilterContext filterContext) { - super(conversionService, filterContext); + private static final class FilterChainImpl implements ClientFilterChain, ServerFilterChain { + + private final Function> downstream; + private FilterContext filterContext; + + private FilterChainImpl(Function> downstream, + FilterContext filterContext) { + this.downstream = downstream; + this.filterContext = filterContext; } @Override public Publisher> proceed(MutableHttpRequest request) { - return proceed((HttpRequest) request); + filterContext = filterContext.withRequest(request); + return ReactiveExecutionFlow.fromFlow( + downstream.apply(filterContext).>map(newFilterContext -> { + filterContext = newFilterContext; + return newFilterContext.response; + }) + ).toPublisher(); } @Override public Publisher> proceed(HttpRequest request) { - request(request); - return proceed(); + filterContext = filterContext.withRequest(request); + return ReactiveExecutionFlow.fromFlow( + downstream.apply(filterContext).>map(newFilterContext -> { + filterContext = newFilterContext; + return (MutableHttpResponse) newFilterContext.response; + }) + ).toPublisher(); + } + + public ExecutionFlow processResult(Publisher> publisher) { + return ReactiveExecutionFlow.fromPublisher(publisher).map(httpResponse -> filterContext.withResponse(httpResponse)); } } @@ -1172,20 +867,29 @@ public Publisher> proceed(HttpRequest request) { * Implementation of {@link FilterContinuation} for blocking calls. */ @SuppressWarnings("java:S112") // framework code - private static final class BlockingContinuationImpl extends FilterContinuationImpl> { - BlockingContinuationImpl(FilterContext filterContext) { - super(filterContext); + private static final class BlockingContinuationImpl implements FilterContinuation>, InternalFilterContinuation> { + + private final Function> downstream; + private FilterContext filterContext; + + private BlockingContinuationImpl(Function> downstream, FilterContext filterContext) { + this.downstream = downstream; + this.filterContext = filterContext; } @Override - public HttpResponse proceed() { - proceedRequested(); + public FilterContinuation> request(HttpRequest request) { + filterContext = filterContext.withRequest(request); + return new BlockingContinuationImpl(downstream, filterContext); + } + @Override + public HttpResponse proceed() { boolean interrupted = false; while (true) { try { // todo: detect event loop thread - filterContext = suspensionPoint.get(); + filterContext = downstream.apply(filterContext).toCompletableFuture().get(); if (interrupted) { Thread.currentThread().interrupt(); } @@ -1203,6 +907,11 @@ public HttpResponse proceed() { } } } + + @Override + public FilterContext afterMethodContext() { + return filterContext; + } } } diff --git a/http/src/main/java/io/micronaut/http/filter/GenericHttpFilter.java b/http/src/main/java/io/micronaut/http/filter/GenericHttpFilter.java index 8259ad252c4..6dfa7d3ac53 100644 --- a/http/src/main/java/io/micronaut/http/filter/GenericHttpFilter.java +++ b/http/src/main/java/io/micronaut/http/filter/GenericHttpFilter.java @@ -21,8 +21,8 @@ import io.micronaut.core.util.Toggleable; import io.micronaut.http.HttpRequest; import io.micronaut.http.HttpResponse; +import io.micronaut.http.reactive.execution.ReactiveExecutionFlow; import org.reactivestreams.Publisher; -import reactor.util.context.Context; import java.util.concurrent.Executor; @@ -40,9 +40,7 @@ public sealed interface GenericHttpFilter FilterRunner.FilterMethod, GenericHttpFilter.AroundLegacy, GenericHttpFilter.Async, - GenericHttpFilter.Terminal, - GenericHttpFilter.TerminalReactive, - GenericHttpFilter.TerminalWithReactorContext { + GenericHttpFilter.Terminal { /** * When the filter is using the continuation it needs to be suspended and wait for the response. @@ -126,19 +124,23 @@ public int getOrder() { * Terminal filter that accepts a reactive type. Used as a temporary solution for the http * client, until that is un-reactified. * - * @param responsePublisher The response publisher */ @Internal - record TerminalReactive(Publisher> responsePublisher) implements GenericHttpFilter { - } + final class TerminalReactive implements Terminal { - /** - * Like {@link Terminal}, with an additional parameter for the reactive context. - */ - @Internal - @FunctionalInterface - non-sealed interface TerminalWithReactorContext extends GenericHttpFilter { - ExecutionFlow> execute(HttpRequest request, Context context) throws Exception; + private final Publisher> responsePublisher; + + /** + * @param responsePublisher The response publisher + */ + public TerminalReactive(Publisher> responsePublisher) { + this.responsePublisher = responsePublisher; + } + + @Override + public ExecutionFlow> execute(HttpRequest request) throws Exception { + return ReactiveExecutionFlow.fromPublisher(responsePublisher); + } } /** diff --git a/http/src/main/java/io/micronaut/http/reactive/execution/ReactorExecutionFlowImpl.java b/http/src/main/java/io/micronaut/http/reactive/execution/ReactorExecutionFlowImpl.java index 6b21a8283bb..1c18bd23b92 100644 --- a/http/src/main/java/io/micronaut/http/reactive/execution/ReactorExecutionFlowImpl.java +++ b/http/src/main/java/io/micronaut/http/reactive/execution/ReactorExecutionFlowImpl.java @@ -20,10 +20,14 @@ import io.micronaut.core.execution.ExecutionFlow; import io.micronaut.core.execution.ImperativeExecutionFlow; import org.reactivestreams.Publisher; -import org.reactivestreams.Subscriber; import org.reactivestreams.Subscription; +import reactor.core.CoreSubscriber; import reactor.core.Fuseable; +import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; +import reactor.core.publisher.Sinks; +import reactor.util.context.Context; +import reactor.util.context.ContextView; import java.util.Map; import java.util.concurrent.CompletableFuture; @@ -43,7 +47,7 @@ final class ReactorExecutionFlowImpl implements ReactiveExecutionFlow { private Mono value; ReactorExecutionFlowImpl(Publisher value) { - this(Mono.from(value)); + this(value instanceof Flux flux ? flux.next() : Mono.from(value)); } ReactorExecutionFlowImpl(Mono value) { @@ -82,11 +86,19 @@ public ExecutionFlow putInContext(String key, Object value) { @Override public void onComplete(BiConsumer fn) { - value.subscribe(new Subscriber<>() { + value.subscribe(new CoreSubscriber<>() { Subscription subscription; Object value; + @Override + public Context currentContext() { + if (fn instanceof ReactiveConsumer reactiveConsumer) { + return Context.of(reactiveConsumer.contextView); + } + return CoreSubscriber.super.currentContext(); + } + @Override public void onSubscribe(Subscription s) { this.subscription = s; @@ -147,7 +159,22 @@ static Mono toMono(ExecutionFlow next) { } return m; } else { - return Mono.fromCompletionStage(next.toCompletableFuture()); + return Mono.deferContextual(contextView -> { + Sinks.One sink = Sinks.one(); + ReactiveConsumer reactiveConsumer = new ReactiveConsumer(contextView) { + + @Override + public void accept(Object o, Throwable throwable) { + if (throwable != null) { + sink.tryEmitError(throwable); + } else { + sink.tryEmitValue(o); + } + } + }; + next.onComplete(reactiveConsumer); + return sink.asMono(); + }); } } @@ -164,4 +191,13 @@ public Publisher toPublisher() { public CompletableFuture toCompletableFuture() { return value.toFuture(); } + + private abstract static class ReactiveConsumer implements BiConsumer { + + private final ContextView contextView; + + private ReactiveConsumer(ContextView contextView) { + this.contextView = contextView; + } + } } diff --git a/http/src/test/groovy/io/micronaut/http/filter/FilterRunnerSpec.groovy b/http/src/test/groovy/io/micronaut/http/filter/FilterRunnerSpec.groovy index 14ddf3d3b3b..4928c55d58b 100644 --- a/http/src/test/groovy/io/micronaut/http/filter/FilterRunnerSpec.groovy +++ b/http/src/test/groovy/io/micronaut/http/filter/FilterRunnerSpec.groovy @@ -11,10 +11,11 @@ import io.micronaut.http.HttpRequest import io.micronaut.http.HttpResponse import io.micronaut.http.HttpStatus import io.micronaut.http.MutableHttpResponse +import io.micronaut.http.reactive.execution.ReactiveExecutionFlow import io.micronaut.inject.ExecutableMethod import org.reactivestreams.Publisher import reactor.core.publisher.Flux -import reactor.util.context.Context +import reactor.core.publisher.Mono import spock.lang.Specification import java.lang.reflect.Method @@ -25,7 +26,7 @@ import java.util.concurrent.ThreadFactory class FilterRunnerSpec extends Specification { private FilterRunner filterRunner(List filters) { - return new FilterRunner(ConversionService.SHARED, filters); + return new FilterRunner(filters) } def 'simple tasks should not suspend'() { @@ -105,16 +106,21 @@ class FilterRunnerSpec extends Specification { .contextWrite { it.put('value', 'around 2') } } }, - (GenericHttpFilter.TerminalWithReactorContext) ((req, ctx) -> { - events.add('terminal: ' + ctx.get('value')) - ExecutionFlow.just(HttpResponse.ok("resp1")) - }) + (GenericHttpFilter.Terminal) (req) -> { + return ReactiveExecutionFlow.fromPublisher(Mono.deferContextual(ctx -> { + events.add('terminal: ' + ctx.get('value')) + Mono.just(HttpResponse.ok("resp1")) + })) + } ] when: def runner = filterRunner(filters) - runner.reactorContext(Context.of('value', 'outer')) - def result = await(runner.run(HttpRequest.GET("/req1"))) + def result = await( + ReactiveExecutionFlow.fromFlow( + runner.run(HttpRequest.GET("/req1")) + ).putInContext('value', 'outer') + ) then: result != null events == ["context 1: outer", "context 2: around 1", "terminal: around 2"] @@ -123,6 +129,51 @@ class FilterRunnerSpec extends Specification { legacy << [false, true] } + def 'around filters invocation order'(boolean legacy) { + given: + def events = [] + List filters = [ + around(legacy) { request, chain -> + events.add('before 1') + return Flux.deferContextual { ctx -> + events.add('context 1: ' + ctx.get('value')) + Flux.from(chain.proceed(request)) + .doOnNext { events.add('next 1') } + .contextWrite { it.put('value', 'around 1') } + } + }, + around(legacy) { request, chain -> + events.add('before 2') + return Flux.deferContextual { ctx -> + events.add('context 2: ' + ctx.get('value')) + Flux.from(chain.proceed(request)) + .doOnNext { events.add('next 2') } + .contextWrite { it.put('value', 'around 2') } + } + }, + (GenericHttpFilter.Terminal) (req) -> { + return ReactiveExecutionFlow.fromPublisher(Mono.deferContextual(ctx -> { + events.add('terminal: ' + ctx.get('value')) + Mono.just(HttpResponse.ok("resp1")) + })) + } + ] + + when: + def runner = filterRunner(filters) + def result = await( + ReactiveExecutionFlow.fromFlow( + runner.run(HttpRequest.GET("/req1")) + ).putInContext('value', 'outer') + ) + then: + result != null + events == ["before 1", "context 1: outer", "before 2", "context 2: around 1", "terminal: around 2", "next 2", "next 1"] + + where: + legacy << [false, true] + } + def 'exception in before'() { given: def events = [] From 62f68a4d1fb61202d5ab221226ef08a541880805 Mon Sep 17 00:00:00 2001 From: Denis Stepanov Date: Thu, 6 Apr 2023 12:38:09 -0600 Subject: [PATCH 676/743] KSP: Ignore class element nullability for isAssignable (#9075) * KSP: Ignore class element nullability for isAssignable * Correct other case * Correct other case --- .../processing/visitor/KotlinClassElement.kt | 12 +++---- .../inject/ast/ClassElementSpec.groovy | 35 +++++++++++++++++++ .../visitor/BeanIntrospectionSpec.groovy | 32 +++++++++++++++++ 3 files changed, 73 insertions(+), 6 deletions(-) diff --git a/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/KotlinClassElement.kt b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/KotlinClassElement.kt index 5311a5c1a95..e444f60b880 100644 --- a/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/KotlinClassElement.kt +++ b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/KotlinClassElement.kt @@ -463,7 +463,6 @@ internal open class KotlinClassElement( override fun isTypeVariable() = typeVariable - @OptIn(KspExperimental::class) override fun isAssignable(type: String): Boolean { val otherDeclaration = visitorContext.resolver.getClassDeclarationByName(type) if (otherDeclaration != null) { @@ -481,11 +480,12 @@ internal open class KotlinClassElement( if (thisFullName == otherFullName) { return true } - val otherKotlinType = otherDeclaration.asStarProjectedType() - if (otherKotlinType == kotlinType) { + val otherKotlinType = otherDeclaration.asStarProjectedType().makeNullable() + val kotlinTypeNullable = kotlinType.makeNullable() + if (otherKotlinType == kotlinTypeNullable) { return true } - if (otherKotlinType.isAssignableFrom(kotlinType)) { + if (otherKotlinType.isAssignableFrom(kotlinTypeNullable)) { return true } } @@ -499,12 +499,12 @@ internal open class KotlinClassElement( visitorContext.resolver.getKSNameFromString(type) ) ?: return false val kotlinClassByName = visitorContext.resolver.getKotlinClassByName(kotlinName) ?: return false - return kotlinClassByName.asStarProjectedType().isAssignableFrom(kotlinType.starProjection()) + return kotlinClassByName.asStarProjectedType().makeNullable().isAssignableFrom(kotlinType.starProjection().makeNullable()) } override fun isAssignable(type: ClassElement): Boolean { if (type is KotlinClassElement) { - return type.kotlinType.isAssignableFrom(kotlinType) + return type.kotlinType.starProjection().makeNullable().isAssignableFrom(kotlinType.starProjection().makeNullable()) } return super.isAssignable(type) } diff --git a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/inject/ast/ClassElementSpec.groovy b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/inject/ast/ClassElementSpec.groovy index f792783d5d2..cab2aa738df 100644 --- a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/inject/ast/ClassElementSpec.groovy +++ b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/inject/ast/ClassElementSpec.groovy @@ -1872,19 +1872,54 @@ class Test { fun method2() : java.util.List? { return null } + + @Executable + fun method3() : kotlin.collections.List? { + return listOf() + } + } ''', ce -> { return ce.findMethod("method1").get().getReturnType().isAssignable(Iterable.class) && ce.findMethod("method2").get().getReturnType().isAssignable(Iterable.class) + && ce.findMethod("method3").get().getReturnType().isAssignable(Iterable.class) && ((KotlinClassElement) ce.findMethod("method1").get().getReturnType()).isAssignable2(Iterable.class.name) && ((KotlinClassElement) ce.findMethod("method2").get().getReturnType()).isAssignable2(Iterable.class.name) + && ((KotlinClassElement) ce.findMethod("method3").get().getReturnType()).isAssignable2(Iterable.class.name) }) expect: isAssignable } + void "test type isAssignable between nullable and not nullable"() { + when: + boolean isAssignable = buildClassElementMapped('test.Cart', ''' +package test + +data class CartItem( + val id: Long?, + val name: String, + val cart: Cart? +) { + constructor(name: String) : this(null, name, null) +} + +data class Cart( + val id: Long?, + val items: List? +) { + + constructor(items: List) : this(null, items) + + fun cartItemsNotNullable() : List = listOf() +} + +''', cl -> cl.getPrimaryConstructor().get().parameters[1].getType().isAssignable(cl.findMethod("cartItemsNotNullable").get().getReturnType())) + then: + isAssignable + } private void assertListGenericArgument(ClassElement type, Closure cl) { def arg1 = type.getAllTypeArguments().get(List.class.name).get("E") diff --git a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/visitor/BeanIntrospectionSpec.groovy b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/visitor/BeanIntrospectionSpec.groovy index c8138a20684..78d56e3ee53 100644 --- a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/visitor/BeanIntrospectionSpec.groovy +++ b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/visitor/BeanIntrospectionSpec.groovy @@ -2166,4 +2166,36 @@ class Holder( animal instanceof GenericPlaceholder animal.isTypeVariable() } + + void "test list property"() { + given: + BeanIntrospection introspection = buildBeanIntrospection('test.Cart', ''' +package test +import io.micronaut.core.annotation.Introspected + +@io.micronaut.core.annotation.Introspected +data class CartItem( + val id: Long?, + val name: String, + val cart: Cart? +) { + constructor(name: String) : this(null, name, null) +} + +@io.micronaut.core.annotation.Introspected +data class Cart( + val id: Long?, + val items: List? +) { + + constructor(items: List) : this(null, items) + + fun cartItemsNotNullable() : List = listOf() +} + ''') + def bean = introspection.instantiate(1L, new ArrayList()) + bean = introspection.getProperty("items").get().withValue(bean, new ArrayList()) + expect: + bean + } } From e761475aa027be2458889388205ac0b49f15eead Mon Sep 17 00:00:00 2001 From: micronaut-build <65172877+micronaut-build@users.noreply.github.com> Date: Mon, 10 Apr 2023 11:11:31 +0200 Subject: [PATCH 677/743] Bump micronaut-security to 3.10.1 (#9082) --- gradle.properties | 2 +- gradle/libs.versions.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle.properties b/gradle.properties index d205b0a73f6..e273bdde4be 100644 --- a/gradle.properties +++ b/gradle.properties @@ -52,7 +52,7 @@ kotlin.stdlib.default.dependency=false # For the docs graalVersion=22.0.0.2 -micronautSecurityVersion=3.9.4 +micronautSecurityVersion=3.10.1 org.gradle.caching=true org.gradle.parallel=true diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 9c4a50783df..a0573461013 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -113,7 +113,7 @@ managed-micronaut-rss = "3.2.0" managed-micronaut-rxjava1 = "1.0.0" managed-micronaut-rxjava2 = "1.3.0" managed-micronaut-rxjava3 = "2.4.0" -managed-micronaut-security = "3.9.4" +managed-micronaut-security = "3.10.1" managed-micronaut-serialization = "1.5.2" managed-micronaut-servlet = "3.3.5" managed-micronaut-spring = "4.5.1" From 6ae110aed0a0c3717b1cd869b8bdd013427aa7ff Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 10 Apr 2023 11:11:51 +0200 Subject: [PATCH 678/743] chore(deps): update gradle/gradle-build-action action to v2.4.0 (#9080) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/corretto.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/corretto.yml b/.github/workflows/corretto.yml index e9127e5a38a..f26ceb3f1bf 100644 --- a/.github/workflows/corretto.yml +++ b/.github/workflows/corretto.yml @@ -24,7 +24,7 @@ jobs: distribution: 'corretto' java-version: ${{ matrix.java }} - name: Setup Gradle - uses: gradle/gradle-build-action@v2.3.3 + uses: gradle/gradle-build-action@v2.4.0 - name: Optional setup step env: GRADLE_ENTERPRISE_ACCESS_KEY: ${{ secrets.GRADLE_ENTERPRISE_ACCESS_KEY }} From f929fa47faf07d25084527d34af412d449cdb6ce Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 10 Apr 2023 11:12:04 +0200 Subject: [PATCH 679/743] fix(deps): update netty monorepo to v4.1.91.final (#9079) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 433002ec3cd..3b1f772782c 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -66,7 +66,7 @@ managed-kotlin = "1.8.20" managed-kotlin-coroutines = "1.6.4" managed-maven-native-plugin = "0.9.13" managed-methvin-directory-watcher = "0.16.1" -managed-netty = "4.1.90.Final" +managed-netty = "4.1.91.Final" managed-netty-http3 = "0.0.16.Final" managed-reactive-streams = "1.0.4" # This should be kept aligned with https://github.com/micronaut-projects/micronaut-reactor/blob/master/gradle.properties from the BOM From a783b1d5eb6bba44678c80324a90885ea5eb84ca Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 10 Apr 2023 11:12:04 +0200 Subject: [PATCH 680/743] fix(deps): update netty monorepo to v4.1.91.final (#9079) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> see: https://netty.io/news/2023/04/03/4-1-91-Final.html --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 4472d92055a..e7fa1220157 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -126,7 +126,7 @@ managed-micronaut-views = "3.8.2" managed-micronaut-xml = "3.2.0" managed-neo4j = "3.5.35" managed-neo4j-java-driver = "4.4.9" -managed-netty = "4.1.90.Final" +managed-netty = "4.1.91.Final" managed-reactive-pg-client = "0.11.4" managed-reactive-streams = "1.0.4" # This should be kept aligned with https://github.com/micronaut-projects/micronaut-reactor/blob/master/gradle.properties from the BOM From beddff78d27eea56c7b21b98de21f02c7ba54044 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 10 Apr 2023 11:28:31 +0200 Subject: [PATCH 681/743] fix(deps): update junit5 monorepo (#9078) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 3b1f772782c..a7fce8ede43 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -29,8 +29,8 @@ jsr305 = "3.0.2" jakarta-el = "5.0.1" jakarta-el-impl = "5.0.0-M1" jcache = "1.1.1" -junit5 = "5.9.1" -junit-platform="1.9.1" +junit5 = "5.9.2" +junit-platform="1.9.2" ktor = "1.6.8" managed-logback = "1.4.6" logbook-netty = "2.14.0" From 4b590ffb7ba484ba7a307c83e55b8989f1f9d7ec Mon Sep 17 00:00:00 2001 From: Jonas Konrad Date: Mon, 10 Apr 2023 12:06:36 +0200 Subject: [PATCH 682/743] expose ConnectionManager (#9073) --- .../http/client/netty/ConnectionManager.java | 69 ++++++++++++------- .../http/client/netty/DefaultHttpClient.java | 11 ++- 2 files changed, 54 insertions(+), 26 deletions(-) diff --git a/http-client/src/main/java/io/micronaut/http/client/netty/ConnectionManager.java b/http-client/src/main/java/io/micronaut/http/client/netty/ConnectionManager.java index 7a0d1814349..c110351b93b 100644 --- a/http-client/src/main/java/io/micronaut/http/client/netty/ConnectionManager.java +++ b/http-client/src/main/java/io/micronaut/http/client/netty/ConnectionManager.java @@ -32,6 +32,7 @@ import io.micronaut.scheduling.instrument.InvocationInstrumenter; import io.micronaut.websocket.exceptions.WebSocketSessionException; import io.netty.bootstrap.Bootstrap; +import io.netty.buffer.ByteBufAllocator; import io.netty.buffer.Unpooled; import io.netty.channel.Channel; import io.netty.channel.ChannelFactory; @@ -123,10 +124,11 @@ /** * Connection manager for {@link DefaultHttpClient}. This class manages the lifecycle of netty - * channels (wrapped in {@link PoolHandle}s), including pooling and timeouts. + * channels (wrapped in {@link PoolHandle}s), including pooling and timeouts.
+ * Note: This class is public for use in micronaut-oracle-cloud. */ @Internal -class ConnectionManager { +public class ConnectionManager { final InvocationInstrumenter instrumenter; private final HttpVersionSelection httpVersion; @@ -259,6 +261,15 @@ private static NioEventLoopGroup createEventLoopGroup(HttpClientConfiguration co return group; } + /** + * Allocator for this connection manager. Used by micronaut-oracle-cloud. + * + * @return The configured allocator + */ + public final ByteBufAllocator alloc() { + return (ByteBufAllocator) bootstrap.config().options().getOrDefault(ChannelOption.ALLOCATOR, ByteBufAllocator.DEFAULT); + } + /** * For testing. * @@ -267,7 +278,7 @@ private static NioEventLoopGroup createEventLoopGroup(HttpClientConfiguration co */ @NonNull @SuppressWarnings("unused") - List getChannels() { + final List getChannels() { List channels = new ArrayList<>(); for (Pool pool : pools.values()) { pool.forEachConnection(c -> channels.add(((Pool.ConnectionHolder) c).channel)); @@ -282,7 +293,7 @@ List getChannels() { * @since 4.0.0 */ @SuppressWarnings("unused") - int liveRequestCount() { + final int liveRequestCount() { AtomicInteger count = new AtomicInteger(); for (Pool pool : pools.values()) { pool.forEachConnection(c -> { @@ -301,7 +312,7 @@ int liveRequestCount() { /** * @see DefaultHttpClient#start() */ - public void start() { + public final void start() { // only need to start new group if it's managed by us if (shutdownGroup) { group = createEventLoopGroup(configuration, threadFactory); @@ -324,7 +335,7 @@ private void initBootstrap() { /** * @see DefaultHttpClient#stop() */ - public void shutdown() { + public final void shutdown() { for (Pool pool : pools.values()) { pool.shutdown(); } @@ -353,19 +364,19 @@ public void shutdown() { * * @return Whether this connection manager is still running and can serve requests */ - public boolean isRunning() { + public final boolean isRunning() { return !group.isShutdown(); } /** * Use the bootstrap to connect to the given host. Also does some proxy setup. This method is - * protected: The test suite overrides it to return embedded channels instead. + * not final: The test suite overrides it to return embedded channels instead. * * @param requestKey The host to connect to * @param channelInitializer The initializer to use * @return Future that terminates when the TCP connection is established. */ - protected ChannelFuture doConnect(DefaultHttpClient.RequestKey requestKey, ChannelInitializer channelInitializer) { + ChannelFuture doConnect(DefaultHttpClient.RequestKey requestKey, ChannelInitializer channelInitializer) { String host = requestKey.getHost(); int port = requestKey.getPort(); Bootstrap localBootstrap = bootstrap.clone(); @@ -404,7 +415,7 @@ private SslContext buildSslContext(DefaultHttpClient.RequestKey requestKey) { * @param blockHint Optional information about what threads are blocked for this connection request * @return A mono that will complete once the channel is ready for transmission */ - Mono connect(DefaultHttpClient.RequestKey requestKey, @Nullable BlockHint blockHint) { + public final Mono connect(DefaultHttpClient.RequestKey requestKey, @Nullable BlockHint blockHint) { return pools.computeIfAbsent(requestKey, Pool::new).acquire(blockHint); } @@ -416,7 +427,7 @@ Mono connect(DefaultHttpClient.RequestKey requestKey, @Nullable Bloc * @param handler The websocket message handler * @return A mono that will complete when the handshakes complete */ - Mono connectForWebsocket(DefaultHttpClient.RequestKey requestKey, ChannelHandler handler) { + final Mono connectForWebsocket(DefaultHttpClient.RequestKey requestKey, ChannelHandler handler) { Sinks.Empty initial = new CancellableMonoSink<>(null); ChannelFuture connectFuture = doConnect(requestKey, new ChannelInitializer() { @@ -507,7 +518,7 @@ private void configureProxy(ChannelPipeline pipeline, boolean secure, String hos } } - > void addInstrumentedListener( + final > void addInstrumentedListener( Future channelFuture, GenericFutureListener listener) { channelFuture.addListener(f -> { try (Instrumentation ignored = instrumenter.newInstrumentation()) { @@ -854,7 +865,7 @@ public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws E * once the request and response are done, the handle is {@link #release() released} and a new * request can claim the same connection. */ - abstract static class PoolHandle { + public abstract static class PoolHandle { private static final Supplier> LEAK_DETECTOR = SupplierUtil.memoized(() -> ResourceLeakDetectorFactory.instance().newResourceLeakDetector(PoolHandle.class)); @@ -870,16 +881,24 @@ private PoolHandle(boolean http2, Channel channel) { this.channel = channel; } + public final Channel channel() { + return channel; + } + + public final boolean http2() { + return http2; + } + /** * Prevent this connection from being reused, e.g. because garbage was written because of * an error. */ - abstract void taint(); + public abstract void taint(); /** * Close this connection or release it back to the pool. */ - void release() { + public void release() { if (released) { throw new IllegalStateException("Already released"); } @@ -895,12 +914,12 @@ void release() { * * @return Whether this connection may be reused */ - abstract boolean canReturn(); + public abstract boolean canReturn(); /** * Notify any {@link NettyClientCustomizer} that the request pipeline has been built. */ - abstract void notifyRequestPipelineBuilt(); + public abstract void notifyRequestPipelineBuilt(); } /** @@ -1226,12 +1245,12 @@ void dispatch0(PoolSink sink) { final ChannelHandlerContext lastContext = channel.pipeline().lastContext(); @Override - void taint() { + public void taint() { windDownConnection = true; } @Override - void release() { + public void release() { super.release(); if (!windDownConnection) { ChannelHandlerContext newLast = channel.pipeline().lastContext(); @@ -1249,12 +1268,12 @@ void release() { } @Override - boolean canReturn() { + public boolean canReturn() { return !windDownConnection; } @Override - void notifyRequestPipelineBuilt() { + public void notifyRequestPipelineBuilt() { connectionCustomizer.onRequestPipelineBuilt(); } }; @@ -1345,12 +1364,12 @@ public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) NettyClientCustomizer streamCustomizer = connectionCustomizer.specializeForChannel(streamChannel, NettyClientCustomizer.ChannelRole.HTTP2_STREAM); PoolHandle ph = new PoolHandle(true, streamChannel) { @Override - void taint() { + public void taint() { // do nothing, we don't reuse stream channels } @Override - void release() { + public void release() { super.release(); liveStreamChannels.remove(streamChannel); streamChannel.close(); @@ -1363,12 +1382,12 @@ void release() { } @Override - boolean canReturn() { + public boolean canReturn() { return true; } @Override - void notifyRequestPipelineBuilt() { + public void notifyRequestPipelineBuilt() { streamCustomizer.onRequestPipelineBuilt(); } }; diff --git a/http-client/src/main/java/io/micronaut/http/client/netty/DefaultHttpClient.java b/http-client/src/main/java/io/micronaut/http/client/netty/DefaultHttpClient.java index ac30bdb5928..17a6d69206d 100644 --- a/http-client/src/main/java/io/micronaut/http/client/netty/DefaultHttpClient.java +++ b/http-client/src/main/java/io/micronaut/http/client/netty/DefaultHttpClient.java @@ -455,6 +455,15 @@ public Logger getLog() { return log; } + /** + * Access to the connection manager, for micronaut-oracle-cloud. + * + * @return The connection manager of this client + */ + public ConnectionManager connectionManager() { + return connectionManager; + } + @Override public HttpClient start() { if (!isRunning()) { @@ -1855,7 +1864,7 @@ private E decorate(E exc) { /** * Key used for connection pooling and determining host/port. */ - static final class RequestKey { + public static final class RequestKey { private final String host; private final int port; private final boolean secure; From 497704c6f70ef69d9e2a8e2003bd5257795afeef Mon Sep 17 00:00:00 2001 From: Dean Wette Date: Mon, 10 Apr 2023 06:58:30 -0500 Subject: [PATCH 683/743] doc: Update Custom converter docs to use ConversionService bean (#8638) Close #8592 --- .../guide/config/customTypeConverter.adoc | 7 ++++--- .../converters/MapToLocalDateConverter.groovy | 19 +++++++++++++------ .../converters/MapToLocalDateConverter.kt | 19 +++++++++++-------- .../converters/MapToLocalDateConverter.java | 19 +++++++++++++------ 4 files changed, 41 insertions(+), 23 deletions(-) diff --git a/src/main/docs/guide/config/customTypeConverter.adoc b/src/main/docs/guide/config/customTypeConverter.adoc index 64ce5d5798b..0373596bf75 100644 --- a/src/main/docs/guide/config/customTypeConverter.adoc +++ b/src/main/docs/guide/config/customTypeConverter.adoc @@ -19,7 +19,8 @@ This won't work by default, since there is no built-in conversion from `Map` to snippet::io.micronaut.docs.config.converters.MapToLocalDateConverter[tags="imports,class", indent=0] <1> The class implements api:core.convert.TypeConverter[] which has two generic arguments, the type you are converting from, and the type you are converting to -<2> The implementation delegates to the default shared conversion service to convert the values from the Map used to create a `LocalDate` -<3> If an exception occurs during binding, call `reject(..)` which propagates additional information to the container +<2> The constructor injects a bean of type `ConversionService`, introduced in Micronaut 4, instead of making static calls to `ConversionService.SHARED` used in previous versions +<3> The implementation delegates to the injected conversion service to convert the values from the Map used to create a `LocalDate` +<4> If an exception occurs during binding, call `reject(..)` which propagates additional information to the container -NOTE: It's possible to add a custom type converter into `ConversionService.SHARED` by registering it via the service loader. +NOTE: Register new type converters by declaring a bean of type api:core.convert.TypeConverter[]. `ConversionService.SHARED` is deprecated, and it will be removed in a future version of the Micronaut Framework. diff --git a/test-suite-groovy/src/test/groovy/io/micronaut/docs/config/converters/MapToLocalDateConverter.groovy b/test-suite-groovy/src/test/groovy/io/micronaut/docs/config/converters/MapToLocalDateConverter.groovy index 5fb7d0a3f92..3943a9b9f8c 100644 --- a/test-suite-groovy/src/test/groovy/io/micronaut/docs/config/converters/MapToLocalDateConverter.groovy +++ b/test-suite-groovy/src/test/groovy/io/micronaut/docs/config/converters/MapToLocalDateConverter.groovy @@ -1,5 +1,5 @@ /* - * Copyright 2017-2020 original authors + * Copyright 2017-2023 original authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -30,16 +30,23 @@ import java.time.LocalDate // tag::class[] @Prototype class MapToLocalDateConverter implements TypeConverter { // <1> + + final ConversionService conversionService + + MapToLocalDateConverter(ConversionService conversionService) { // <2> + this.conversionService = conversionService; + } + @Override Optional convert(Map propertyMap, Class targetType, ConversionContext context) { - Optional day = ConversionService.SHARED.convert(propertyMap.day, Integer) - Optional month = ConversionService.SHARED.convert(propertyMap.month, Integer) - Optional year = ConversionService.SHARED.convert(propertyMap.year, Integer) + Optional day = conversionService.convert(propertyMap.day, Integer) + Optional month = conversionService.convert(propertyMap.month, Integer) + Optional year = conversionService.convert(propertyMap.year, Integer) if (day.present && month.present && year.present) { try { - return Optional.of(LocalDate.of(year.get(), month.get(), day.get())) // <2> + return Optional.of(LocalDate.of(year.get(), month.get(), day.get())) // <3> } catch (DateTimeException e) { - context.reject(propertyMap, e) // <3> + context.reject(propertyMap, e) // <4> return Optional.empty() } } diff --git a/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/config/converters/MapToLocalDateConverter.kt b/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/config/converters/MapToLocalDateConverter.kt index c4b3c050665..86ddf4035a6 100644 --- a/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/config/converters/MapToLocalDateConverter.kt +++ b/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/config/converters/MapToLocalDateConverter.kt @@ -1,5 +1,5 @@ /* - * Copyright 2017-2020 original authors + * Copyright 2017-2023 original authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -23,21 +23,24 @@ import io.micronaut.core.convert.TypeConverter import java.time.DateTimeException import java.time.LocalDate import java.util.Optional -import jakarta.inject.Singleton // end::imports[] // tag::class[] @Prototype -class MapToLocalDateConverter : TypeConverter, LocalDate> { // <1> +class MapToLocalDateConverter( + private val conversionService: ConversionService // <2> +) + : TypeConverter, LocalDate> { // <1> + override fun convert(propertyMap: Map<*, *>, targetType: Class, context: ConversionContext): Optional { - val day = ConversionService.SHARED.convert(propertyMap["day"], Int::class.java) - val month = ConversionService.SHARED.convert(propertyMap["month"], Int::class.java) - val year = ConversionService.SHARED.convert(propertyMap["year"], Int::class.java) + val day = conversionService.convert(propertyMap["day"], Int::class.java) + val month = conversionService.convert(propertyMap["month"], Int::class.java) + val year = conversionService.convert(propertyMap["year"], Int::class.java) if (day.isPresent && month.isPresent && year.isPresent) { try { - return Optional.of(LocalDate.of(year.get(), month.get(), day.get())) // <2> + return Optional.of(LocalDate.of(year.get(), month.get(), day.get())) // <3> } catch (e: DateTimeException) { - context.reject(propertyMap, e) // <3> + context.reject(propertyMap, e) // <4> return Optional.empty() } } diff --git a/test-suite/src/test/java/io/micronaut/docs/config/converters/MapToLocalDateConverter.java b/test-suite/src/test/java/io/micronaut/docs/config/converters/MapToLocalDateConverter.java index 9de76daf8ee..a5cdc7c07ca 100644 --- a/test-suite/src/test/java/io/micronaut/docs/config/converters/MapToLocalDateConverter.java +++ b/test-suite/src/test/java/io/micronaut/docs/config/converters/MapToLocalDateConverter.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2020 original authors + * Copyright 2017-2023 original authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -31,16 +31,23 @@ // tag::class[] @Prototype public class MapToLocalDateConverter implements TypeConverter { // <1> + + private final ConversionService conversionService; + + public MapToLocalDateConverter(ConversionService conversionService) { // <2> + this.conversionService = conversionService; + } + @Override public Optional convert(Map propertyMap, Class targetType, ConversionContext context) { - Optional day = ConversionService.SHARED.convert(propertyMap.get("day"), Integer.class); - Optional month = ConversionService.SHARED.convert(propertyMap.get("month"), Integer.class); - Optional year = ConversionService.SHARED.convert(propertyMap.get("year"), Integer.class); + Optional day = conversionService.convert(propertyMap.get("day"), Integer.class); + Optional month = conversionService.convert(propertyMap.get("month"), Integer.class); + Optional year = conversionService.convert(propertyMap.get("year"), Integer.class); if (day.isPresent() && month.isPresent() && year.isPresent()) { try { - return Optional.of(LocalDate.of(year.get(), month.get(), day.get())); // <2> + return Optional.of(LocalDate.of(year.get(), month.get(), day.get())); // <3> } catch (DateTimeException e) { - context.reject(propertyMap, e); // <3> + context.reject(propertyMap, e); // <4> return Optional.empty(); } } From 65ff55f021cd95ec1e4e10e124c471cbd3920509 Mon Sep 17 00:00:00 2001 From: Jonas Konrad Date: Tue, 11 Apr 2023 10:23:20 +0200 Subject: [PATCH 684/743] Disable leak detection by default (#9074) * Disable leak detection by default * update docs --- .../http/netty/channel/DefaultEventLoopGroupFactory.java | 6 ++++++ .../micronaut/http/netty/channel/EventLoopGroupSpec.groovy | 2 +- src/main/docs/guide/httpServer/serverConfiguration.adoc | 2 +- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/http-netty/src/main/java/io/micronaut/http/netty/channel/DefaultEventLoopGroupFactory.java b/http-netty/src/main/java/io/micronaut/http/netty/channel/DefaultEventLoopGroupFactory.java index 727882e416a..507e2c2c270 100644 --- a/http-netty/src/main/java/io/micronaut/http/netty/channel/DefaultEventLoopGroupFactory.java +++ b/http-netty/src/main/java/io/micronaut/http/netty/channel/DefaultEventLoopGroupFactory.java @@ -75,6 +75,12 @@ public DefaultEventLoopGroupFactory( this.nativeFactory = nativeFactory != null ? nativeFactory : defaultFactory; if (nettyGlobalConfiguration != null && nettyGlobalConfiguration.getResourceLeakDetectorLevel() != null) { ResourceLeakDetector.setLevel(nettyGlobalConfiguration.getResourceLeakDetectorLevel()); + } else if (ResourceLeakDetector.getLevel() == ResourceLeakDetector.Level.SIMPLE && + System.getProperty("io.netty.leakDetectionLevel") == null && + System.getProperty("io.netty.leakDetection.level") == null) { + // disable leak detection for performance if it's not explicitly enabled in a system + // property, via config, or via setLevel + ResourceLeakDetector.setLevel(ResourceLeakDetector.Level.DISABLED); } } diff --git a/http-netty/src/test/groovy/io/micronaut/http/netty/channel/EventLoopGroupSpec.groovy b/http-netty/src/test/groovy/io/micronaut/http/netty/channel/EventLoopGroupSpec.groovy index 1ae1f2706d1..038a2b23c27 100644 --- a/http-netty/src/test/groovy/io/micronaut/http/netty/channel/EventLoopGroupSpec.groovy +++ b/http-netty/src/test/groovy/io/micronaut/http/netty/channel/EventLoopGroupSpec.groovy @@ -23,7 +23,7 @@ class EventLoopGroupSpec extends Specification { then: !eventLoopGroup.isTerminated() eventLoopGroup.executorCount() == NettyRuntime.availableProcessors() * 2 - ResourceLeakDetector.level == ResourceLeakDetector.Level.SIMPLE + ResourceLeakDetector.level == ResourceLeakDetector.Level.DISABLED when: context.close() diff --git a/src/main/docs/guide/httpServer/serverConfiguration.adoc b/src/main/docs/guide/httpServer/serverConfiguration.adoc index 586f9d6a1b5..a2118ddbf1b 100644 --- a/src/main/docs/guide/httpServer/serverConfiguration.adoc +++ b/src/main/docs/guide/httpServer/serverConfiguration.adoc @@ -57,4 +57,4 @@ micronaut: prefer-native-transport: true ---- -NOTE: Netty enables simplistic sampling resource leak detection which reports there is a leak or not, at the cost of small overhead. You can disable it or enable more advanced detection by setting property `netty.resource-leak-detector-level` to one of: `SIMPLE` (default), `DISABLED`, `PARANOID` or `ADVANCED`. +NOTE: Netty enables simplistic sampling resource leak detection which reports there is a leak or not, at the cost of small overhead. You can enable it by setting property `netty.resource-leak-detector-level` to one of: `DISABLED` (default), `SIMPLE`, `PARANOID` or `ADVANCED`. From 22d2e48ed0f85fc37b0371c95a418ccedcd718b5 Mon Sep 17 00:00:00 2001 From: Denis Stepanov Date: Tue, 11 Apr 2023 02:49:04 -0600 Subject: [PATCH 685/743] More performance optimizations (#9076) * Introduces `UnsafeExecutable` which allows executing methods without argument checking hence improving performance * Optimises event listeners. Co-authored-by: Jonas Konrad --------- Co-authored-by: Graeme Rocher Co-authored-by: Jonas Konrad --- .../micronaut/core/type/UnsafeExecutable.java | 38 ++++ .../server/netty/NettyRequestLifecycle.java | 26 +-- .../server/netty/RoutingInBoundHandler.java | 2 +- .../micronaut/http/server/RouteExecutor.java | 41 +---- .../java/io/micronaut/http/HttpResponse.java | 21 +++ .../micronaut/http/MutableHttpResponse.java | 5 + .../micronaut/http/filter/FilterRunner.java | 8 +- .../context/AbstractExecutableMethod.java | 8 +- .../AbstractExecutableMethodsDefinition.java | 9 +- .../micronaut/context/DefaultBeanContext.java | 170 +++++++++++------- .../ApplicationEventPublisherFactory.java | 60 +++---- .../inject/UnsafeExecutionHandle.java | 39 ++++ .../web/router/AbstractRouteMatch.java | 9 + .../router/DefaultMethodBasedRouteInfo.java | 16 ++ .../web/router/DefaultRouteInfo.java | 5 + .../io/micronaut/web/router/RouteInfo.java | 6 + .../java/io/micronaut/web/router/Router.java | 21 +++ 17 files changed, 326 insertions(+), 158 deletions(-) create mode 100644 core/src/main/java/io/micronaut/core/type/UnsafeExecutable.java create mode 100644 inject/src/main/java/io/micronaut/inject/UnsafeExecutionHandle.java diff --git a/core/src/main/java/io/micronaut/core/type/UnsafeExecutable.java b/core/src/main/java/io/micronaut/core/type/UnsafeExecutable.java new file mode 100644 index 00000000000..32604f8aad0 --- /dev/null +++ b/core/src/main/java/io/micronaut/core/type/UnsafeExecutable.java @@ -0,0 +1,38 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.core.type; + +import io.micronaut.core.annotation.Internal; + +/** + * A variation of {@link Executable} that exposes invoke method without arguments validation. + * @param The declaring type + * @param The result of the method call + * @author Denis Stepanov + * @since 4.0.0 + */ +@Internal +public interface UnsafeExecutable extends Executable { + + /** + * Invokes the method without the arguments validation. + * + * @param instance The instance. Nullable only if it's a static method call. + * @param arguments The arguments + * @return The result + */ + R invokeUnsafe(T instance, Object... arguments); +} diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/NettyRequestLifecycle.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/NettyRequestLifecycle.java index 3c4ad673047..64c0434535b 100644 --- a/http-server-netty/src/main/java/io/micronaut/http/server/netty/NettyRequestLifecycle.java +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/NettyRequestLifecycle.java @@ -18,9 +18,7 @@ import io.micronaut.core.annotation.Internal; import io.micronaut.core.annotation.Nullable; import io.micronaut.core.execution.ExecutionFlow; -import io.micronaut.core.type.Argument; import io.micronaut.http.HttpMethod; -import io.micronaut.http.HttpRequest; import io.micronaut.http.HttpResponse; import io.micronaut.http.HttpStatus; import io.micronaut.http.MutableHttpResponse; @@ -31,7 +29,6 @@ import io.micronaut.http.server.netty.types.files.NettyStreamedFileCustomizableResponseType; import io.micronaut.http.server.netty.types.files.NettySystemFileCustomizableResponseType; import io.micronaut.http.server.types.files.FileCustomizableResponseType; -import io.micronaut.web.router.MethodBasedRouteMatch; import io.micronaut.web.router.RouteMatch; import io.netty.channel.ChannelHandlerContext; import io.netty.handler.codec.DecoderResult; @@ -168,29 +165,8 @@ void handleException(Throwable cause) { } private boolean needsBody(RouteMatch routeMatch) { - if (!routeMatch.getRouteInfo().isPermitsRequestBody()) { - return false; - } - if (routeMatch instanceof MethodBasedRouteMatch methodBasedRouteMatch) { - if (hasArg(methodBasedRouteMatch, HttpRequest.class)) { - // HttpRequest argument in the method - return true; - } - } - if (routeMatch.getRouteInfo().getBodyArgument().isPresent()) { - // Body argument in the method - return true; - } // Not annotated body argument - return !routeMatch.isFulfilled(); + return routeMatch.getRouteInfo().needsRequestBody() || !routeMatch.isFulfilled(); } - private static boolean hasArg(MethodBasedRouteMatch methodBasedRouteMatch, Class type) { - for (Argument argument : methodBasedRouteMatch.getArguments()) { - if (argument.getType() == type) { - return true; - } - } - return false; - } } diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/RoutingInBoundHandler.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/RoutingInBoundHandler.java index f9aceb4bbca..0115b4af9f0 100644 --- a/http-server-netty/src/main/java/io/micronaut/http/server/netty/RoutingInBoundHandler.java +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/RoutingInBoundHandler.java @@ -301,7 +301,7 @@ private void encodeHttpResponse( Writable writable = (Writable) body; writable.writeTo(outputStream, nettyRequest.getCharacterEncoding()); response.body(byteBuf); - if (!response.getContentType().isPresent()) { + if (response.getContentType().isEmpty()) { response.getAttribute(HttpAttributes.ROUTE_INFO, RouteInfo.class).ifPresent((routeInfo) -> response.contentType(routeExecutor.resolveDefaultResponseContentType(nettyRequest, routeInfo))); } diff --git a/http-server/src/main/java/io/micronaut/http/server/RouteExecutor.java b/http-server/src/main/java/io/micronaut/http/server/RouteExecutor.java index 5d2bddcf00e..94471ed657c 100644 --- a/http-server/src/main/java/io/micronaut/http/server/RouteExecutor.java +++ b/http-server/src/main/java/io/micronaut/http/server/RouteExecutor.java @@ -53,7 +53,6 @@ import io.micronaut.web.router.RouteMatch; import io.micronaut.web.router.Router; import io.micronaut.web.router.UriRouteMatch; -import io.micronaut.web.router.exceptions.DuplicateRouteException; import io.micronaut.web.router.exceptions.UnsatisfiedRouteException; import jakarta.inject.Singleton; import org.reactivestreams.Publisher; @@ -168,14 +167,7 @@ public Optional getCoroutineHelper() { @Nullable UriRouteMatch findRouteMatch(HttpRequest httpRequest) { - UriRouteMatch routeMatch = null; - - List> uriRoutes = router.findAllClosest(httpRequest); - if (uriRoutes.size() > 1) { - throw new DuplicateRouteException(httpRequest.getPath(), uriRoutes); - } else if (uriRoutes.size() == 1) { - routeMatch = uriRoutes.get(0); - } + UriRouteMatch routeMatch = router.findClosest(httpRequest); if (routeMatch == null && httpRequest.getMethod().equals(HttpMethod.OPTIONS)) { List> anyUriRoutes = router.findAny(httpRequest); @@ -386,26 +378,9 @@ private boolean isSingle(RouteInfo finalRoute, Class bodyClass) { (finalRoute.isAsync() || finalRoute.isSuspended() || Publishers.isSingle(bodyClass))); } - private MutableHttpResponse toMutableResponse(HttpResponse message) { - MutableHttpResponse mutableHttpResponse; - if (message instanceof MutableHttpResponse) { - mutableHttpResponse = (MutableHttpResponse) message; - } else { - mutableHttpResponse = HttpResponse.status(message.code(), message.reason()); - mutableHttpResponse.body(message.body()); - message.getHeaders().forEach((name, value) -> { - for (String val : value) { - mutableHttpResponse.header(name, val); - } - }); - mutableHttpResponse.getAttributes().putAll(message.getAttributes()); - } - return mutableHttpResponse; - } - private ExecutionFlow> fromImperativeExecute(HttpRequest request, RouteInfo routeInfo, Object body) { - if (body instanceof HttpResponse) { - MutableHttpResponse outgoingResponse = toMutableResponse((HttpResponse) body); + if (body instanceof HttpResponse httpResponse) { + MutableHttpResponse outgoingResponse = httpResponse.toMutableResponse(); final Argument bodyArgument = routeInfo.getReturnType().getFirstTypeVariable().orElse(Argument.OBJECT_ARGUMENT); if (bodyArgument.isAsyncOrReactive()) { return fromPublisher( @@ -531,8 +506,8 @@ private ExecutionFlow> fromKotlinCoroutineExecute(HttpReq Mono.fromCompletionStage(supplier) .flatMap(obj -> { MutableHttpResponse response; - if (obj instanceof HttpResponse) { - response = toMutableResponse((HttpResponse) obj); + if (obj instanceof HttpResponse httpResponse) { + response = httpResponse.toMutableResponse(); final Argument bodyArgument = routeInfo.getReturnType().getFirstTypeVariable().orElse(Argument.OBJECT_ARGUMENT); if (bodyArgument.isAsyncOrReactive()) { return processPublisherBody(request, response, routeInfo); @@ -585,8 +560,8 @@ private CorePublisher> fromReactiveExecute(HttpRequest return Flux.just(emptyResponse.get()); } } - if (o instanceof HttpResponse) { - singleResponse = toMutableResponse((HttpResponse) o); + if (o instanceof HttpResponse httpResponse) { + singleResponse = httpResponse.toMutableResponse(); final Argument bodyArgument = routeInfo.getReturnType() //Mono .getFirstTypeVariable().orElse(Argument.OBJECT_ARGUMENT) //HttpResponse .getFirstTypeVariable().orElse(Argument.OBJECT_ARGUMENT); //Mono @@ -609,7 +584,7 @@ private CorePublisher> fromReactiveExecute(HttpRequest // a response stream Publisher> bodyPublisher = Publishers.convertPublisher(conversionService, body, Publisher.class); Flux> response = Flux.from(bodyPublisher) - .map(this::toMutableResponse); + .map(HttpResponse::toMutableResponse); Argument bodyArgument = typeArgument.getFirstTypeVariable().orElse(Argument.OBJECT_ARGUMENT); if (bodyArgument.isAsyncOrReactive()) { return response.flatMap((resp) -> diff --git a/http/src/main/java/io/micronaut/http/HttpResponse.java b/http/src/main/java/io/micronaut/http/HttpResponse.java index a4df598304c..9baa60216c6 100644 --- a/http/src/main/java/io/micronaut/http/HttpResponse.java +++ b/http/src/main/java/io/micronaut/http/HttpResponse.java @@ -428,4 +428,25 @@ default Cookies getCookies() { default Optional getCookie(String name) { throw new UnsupportedOperationException("Operation not supported on a " + this.getClass() + " response."); } + + /** + * Returns a mutable response based on this response. + * @return the mutable response + * @since 4.0.0 + */ + default MutableHttpResponse toMutableResponse() { + if (this instanceof MutableHttpResponse mutableHttpResponse) { + return mutableHttpResponse; + } + MutableHttpResponse mutableHttpResponse = HttpResponse.status(code(), reason()); + mutableHttpResponse.body(body()); + getHeaders().forEach((name, value) -> { + for (String val : value) { + mutableHttpResponse.header(name, val); + } + }); + mutableHttpResponse.getAttributes().putAll(getAttributes()); + return mutableHttpResponse; + } + } diff --git a/http/src/main/java/io/micronaut/http/MutableHttpResponse.java b/http/src/main/java/io/micronaut/http/MutableHttpResponse.java index 3ef2d8b5742..8dc96353d2a 100644 --- a/http/src/main/java/io/micronaut/http/MutableHttpResponse.java +++ b/http/src/main/java/io/micronaut/http/MutableHttpResponse.java @@ -188,4 +188,9 @@ default MutableHttpResponse status(HttpStatus status) { default MutableHttpResponse attribute(CharSequence name, Object value) { return (MutableHttpResponse) setAttribute(name, value); } + + @Override + default MutableHttpResponse toMutableResponse() { + return this; + } } diff --git a/http/src/main/java/io/micronaut/http/filter/FilterRunner.java b/http/src/main/java/io/micronaut/http/filter/FilterRunner.java index e5be9ff2ebe..c5a8e2b0435 100644 --- a/http/src/main/java/io/micronaut/http/filter/FilterRunner.java +++ b/http/src/main/java/io/micronaut/http/filter/FilterRunner.java @@ -27,6 +27,7 @@ import io.micronaut.core.order.Ordered; import io.micronaut.core.type.Argument; import io.micronaut.core.type.Executable; +import io.micronaut.core.type.UnsafeExecutable; import io.micronaut.http.HttpRequest; import io.micronaut.http.HttpResponse; import io.micronaut.http.MutableHttpRequest; @@ -551,7 +552,12 @@ private ExecutionFlow filter(FilterContext filterContext, return ExecutionFlow.just(filterContext); } Object[] args = bindArgs(methodContext); - Object returnValue = method.invoke(bean, args); + Object returnValue; + if (method instanceof UnsafeExecutable unsafeExecutable) { + returnValue = unsafeExecutable.invokeUnsafe(bean, args); + } else { + returnValue = method.invoke(bean, args); + } return returnHandler.handle(filterContext, returnValue, methodContext.continuation); } catch (Throwable e) { return ExecutionFlow.error(e); diff --git a/inject/src/main/java/io/micronaut/context/AbstractExecutableMethod.java b/inject/src/main/java/io/micronaut/context/AbstractExecutableMethod.java index 6f9f3464046..e8190c52e82 100644 --- a/inject/src/main/java/io/micronaut/context/AbstractExecutableMethod.java +++ b/inject/src/main/java/io/micronaut/context/AbstractExecutableMethod.java @@ -22,6 +22,7 @@ import io.micronaut.core.annotation.UsedByGeneratedCode; import io.micronaut.core.type.Argument; import io.micronaut.core.type.ReturnType; +import io.micronaut.core.type.UnsafeExecutable; import io.micronaut.core.util.ArgumentUtils; import io.micronaut.core.util.ObjectUtils; import io.micronaut.inject.ExecutableMethod; @@ -43,7 +44,7 @@ * @since 1.0 */ @Internal -public abstract class AbstractExecutableMethod extends AbstractExecutable implements ExecutableMethod, EnvironmentConfigurable { +public abstract class AbstractExecutableMethod extends AbstractExecutable implements UnsafeExecutable, ExecutableMethod, EnvironmentConfigurable { private final ReturnType returnType; private final Argument genericReturnType; @@ -163,6 +164,11 @@ public final Object invoke(Object instance, Object... arguments) { return invokeInternal(instance, arguments); } + @Override + public Object invokeUnsafe(Object instance, Object... arguments) { + return invokeInternal(instance, arguments); + } + /** * @param instance The instance * @param arguments The arguments diff --git a/inject/src/main/java/io/micronaut/context/AbstractExecutableMethodsDefinition.java b/inject/src/main/java/io/micronaut/context/AbstractExecutableMethodsDefinition.java index ffa4f0b091a..e85cc7a2b40 100644 --- a/inject/src/main/java/io/micronaut/context/AbstractExecutableMethodsDefinition.java +++ b/inject/src/main/java/io/micronaut/context/AbstractExecutableMethodsDefinition.java @@ -24,6 +24,7 @@ import io.micronaut.core.reflect.ClassUtils; import io.micronaut.core.type.Argument; import io.micronaut.core.type.ReturnType; +import io.micronaut.core.type.UnsafeExecutable; import io.micronaut.core.util.ArgumentUtils; import io.micronaut.core.util.ObjectUtils; import io.micronaut.inject.ExecutableMethod; @@ -327,8 +328,7 @@ public String toString() { * @param The result type */ private static final class DispatchedExecutableMethod implements ExecutableMethod, - EnvironmentConfigurable, - BeanContextConfigurable { + EnvironmentConfigurable, BeanContextConfigurable, UnsafeExecutable { private final AbstractExecutableMethodsDefinition dispatcher; private final int index; @@ -442,6 +442,11 @@ public R invoke(T instance, Object... arguments) { return (R) dispatcher.dispatch(index, instance, arguments); } + @Override + public R invokeUnsafe(T instance, Object... arguments) { + return (R) dispatcher.dispatch(index, instance, arguments); + } + @Override public boolean equals(Object o) { if (this == o) { diff --git a/inject/src/main/java/io/micronaut/context/DefaultBeanContext.java b/inject/src/main/java/io/micronaut/context/DefaultBeanContext.java index e7ddc3e2159..0126f950d8c 100644 --- a/inject/src/main/java/io/micronaut/context/DefaultBeanContext.java +++ b/inject/src/main/java/io/micronaut/context/DefaultBeanContext.java @@ -82,6 +82,7 @@ import io.micronaut.core.reflect.ClassUtils; import io.micronaut.core.type.Argument; import io.micronaut.core.type.ReturnType; +import io.micronaut.core.type.UnsafeExecutable; import io.micronaut.core.util.ArgumentUtils; import io.micronaut.core.util.ArrayUtils; import io.micronaut.core.util.CollectionUtils; @@ -108,6 +109,7 @@ import io.micronaut.inject.ParametrizedInstantiatableBeanDefinition; import io.micronaut.inject.ProxyBeanDefinition; import io.micronaut.inject.QualifiedBeanType; +import io.micronaut.inject.UnsafeExecutionHandle; import io.micronaut.inject.ValidatedBeanDefinition; import io.micronaut.inject.provider.AbstractProviderDefinition; import io.micronaut.inject.proxy.InterceptedBeanProxy; @@ -593,68 +595,11 @@ public Optional> findExecutionHandle(Class } @Override - public MethodExecutionHandle createExecutionHandle(BeanDefinition beanDefinition, ExecutableMethod method) { - return new MethodExecutionHandle<>() { - - private Object target; - - @NonNull - @Override - public AnnotationMetadata getAnnotationMetadata() { - return method.getAnnotationMetadata(); - } - - @Override - public Object getTarget() { - Object target = this.target; - if (target == null) { - synchronized (this) { // double check - target = this.target; - if (target == null) { - target = getBean(beanDefinition); - this.target = target; - } - } - } - return target; - } - - @Override - public Class getDeclaringType() { - return beanDefinition.getBeanType(); - } - - @Override - public String getMethodName() { - return method.getMethodName(); - } - - @Override - public Argument[] getArguments() { - return method.getArguments(); - } - - @Override - public Method getTargetMethod() { - return method.getTargetMethod(); - } - - @Override - public ReturnType getReturnType() { - return method.getReturnType(); - } - - @Override - public Object invoke(Object... arguments) { - return method.invoke(getTarget(), arguments); - } - - @NonNull - @Override - public ExecutableMethod getExecutableMethod() { - return (ExecutableMethod) method; - } - }; + public MethodExecutionHandle createExecutionHandle(BeanDefinition beanDefinition, ExecutableMethod method) { + if (method instanceof UnsafeExecutable) { + return new BeanContextUnsafeExecutionHandle(method, beanDefinition, (UnsafeExecutable) method); + } + return new BeanContextExecutionHandle(method, beanDefinition); } @SuppressWarnings("unchecked") @@ -3819,8 +3764,10 @@ public AnnotationMetadata getAnnotationMetadata() { * @param The targe type * @param The return type */ - private static final class ObjectExecutionHandle extends AbstractExecutionHandle { + private static final class ObjectExecutionHandle extends AbstractExecutionHandle implements UnsafeExecutionHandle { + @Nullable + private final UnsafeExecutable unsafeExecutable; private final T target; /** @@ -3830,6 +3777,11 @@ private static final class ObjectExecutionHandle extends AbstractExecution ObjectExecutionHandle(T target, ExecutableMethod method) { super(method); this.target = target; + if (method instanceof UnsafeExecutable unsafeExecutable) { + this.unsafeExecutable = unsafeExecutable; + } else { + this.unsafeExecutable = null; + } } @Override @@ -3842,6 +3794,14 @@ public R invoke(Object... arguments) { return method.invoke(target, arguments); } + @Override + public R invokeUnsafe(Object... arguments) { + if (unsafeExecutable == null) { + return invoke(arguments); + } + return unsafeExecutable.invokeUnsafe(target, arguments); + } + @Override public Method getTargetMethod() { return method.getTargetMethod(); @@ -4228,4 +4188,88 @@ public void disable(BeanDefinitionReference reference) { } } } + + private class BeanContextUnsafeExecutionHandle extends BeanContextExecutionHandle implements UnsafeExecutionHandle { + + private final UnsafeExecutable unsafeExecutionHandle; + + public BeanContextUnsafeExecutionHandle(ExecutableMethod method, BeanDefinition beanDefinition, UnsafeExecutable unsafeExecutionHandle) { + super(method, beanDefinition); + this.unsafeExecutionHandle = unsafeExecutionHandle; + } + + @Override + public Object invokeUnsafe(Object... arguments) { + return unsafeExecutionHandle.invokeUnsafe(getTarget(), arguments); + } + } + + private class BeanContextExecutionHandle implements MethodExecutionHandle { + + private final ExecutableMethod method; + private final BeanDefinition beanDefinition; + private Object target; + + public BeanContextExecutionHandle(ExecutableMethod method, BeanDefinition beanDefinition) { + this.method = method; + this.beanDefinition = beanDefinition; + } + + @NonNull + @Override + public AnnotationMetadata getAnnotationMetadata() { + return method.getAnnotationMetadata(); + } + + @Override + public Object getTarget() { + Object target = this.target; + if (target == null) { + synchronized (this) { // double check + target = this.target; + if (target == null) { + target = getBean(beanDefinition); + this.target = target; + } + } + } + return target; + } + + @Override + public Class getDeclaringType() { + return beanDefinition.getBeanType(); + } + + @Override + public String getMethodName() { + return method.getMethodName(); + } + + @Override + public Argument[] getArguments() { + return method.getArguments(); + } + + @Override + public Method getTargetMethod() { + return method.getTargetMethod(); + } + + @Override + public ReturnType getReturnType() { + return method.getReturnType(); + } + + @Override + public Object invoke(Object... arguments) { + return method.invoke(getTarget(), arguments); + } + + @NonNull + @Override + public ExecutableMethod getExecutableMethod() { + return (ExecutableMethod) method; + } + } } diff --git a/inject/src/main/java/io/micronaut/context/event/ApplicationEventPublisherFactory.java b/inject/src/main/java/io/micronaut/context/event/ApplicationEventPublisherFactory.java index 2a7e680a8cc..bc130840204 100644 --- a/inject/src/main/java/io/micronaut/context/event/ApplicationEventPublisherFactory.java +++ b/inject/src/main/java/io/micronaut/context/event/ApplicationEventPublisherFactory.java @@ -36,8 +36,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.util.ArrayList; -import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Map; @@ -212,15 +210,12 @@ private ApplicationEventPublisher getTypedEventPublisher(Argument eventType, Bea } private ApplicationEventPublisher createEventPublisher(Argument eventType, BeanContext beanContext) { - return new ApplicationEventPublisher() { + return new ApplicationEventPublisher<>() { - private final Supplier> lazyListeners = SupplierUtil.memoized(() -> { - List listeners = new ArrayList<>( - beanContext.getBeansOfType(ApplicationEventListener.class, Qualifiers.byTypeArguments(eventType.getType())) - ); - listeners.sort(OrderUtil.COMPARATOR); - return listeners; - }); + private final Supplier lazyListeners = SupplierUtil.memoized(() -> beanContext.getBeansOfType(ApplicationEventListener.class, Qualifiers.byTypeArguments(eventType.getType())) + .stream() + .sorted(OrderUtil.COMPARATOR) + .toArray(ApplicationEventListener[]::new)); @Override public void publishEvent(Object event) { @@ -236,7 +231,7 @@ public void publishEvent(Object event) { public Future publishEventAsync(Object event) { Objects.requireNonNull(event, "Event cannot be null"); CompletableFuture future = new CompletableFuture<>(); - List eventListeners = lazyListeners.get(); + ApplicationEventListener[] eventListeners = lazyListeners.get(); executorSupplier.get().execute(() -> { try { notifyEventListeners(event, eventListeners); @@ -250,32 +245,33 @@ public Future publishEventAsync(Object event) { @Override public boolean isEmpty() { - return lazyListeners.get().isEmpty(); + return lazyListeners.get().length == 0; } }; } - private void notifyEventListeners(@NonNull Object event, Collection eventListeners) { - if (!eventListeners.isEmpty()) { - if (EventLogger.LOG.isTraceEnabled()) { - EventLogger.LOG.trace("Established event listeners {} for event: {}", eventListeners, event); - } - for (ApplicationEventListener listener : eventListeners) { - if (listener.supports(event)) { - try { - if (EventLogger.LOG.isTraceEnabled()) { - EventLogger.LOG.trace("Invoking event listener [{}] for event: {}", listener, event); - } - listener.onApplicationEvent(event); - } catch (ClassCastException ex) { - String msg = ex.getMessage(); - if (msg == null || msg.startsWith(event.getClass().getName())) { - if (EventLogger.LOG.isDebugEnabled()) { - EventLogger.LOG.debug("Incompatible listener for event: " + listener, ex); - } - } else { - throw ex; + private void notifyEventListeners(@NonNull Object event, ApplicationEventListener[] eventListeners) { + if (eventListeners.length == 0) { + return; + } + if (EventLogger.LOG.isTraceEnabled()) { + EventLogger.LOG.trace("Established event listeners {} for event: {}", eventListeners, event); + } + for (ApplicationEventListener listener : eventListeners) { + if (listener.supports(event)) { + try { + if (EventLogger.LOG.isTraceEnabled()) { + EventLogger.LOG.trace("Invoking event listener [{}] for event: {}", listener, event); + } + listener.onApplicationEvent(event); + } catch (ClassCastException ex) { + String msg = ex.getMessage(); + if (msg == null || msg.startsWith(event.getClass().getName())) { + if (EventLogger.LOG.isDebugEnabled()) { + EventLogger.LOG.debug("Incompatible listener for event: " + listener, ex); } + } else { + throw ex; } } } diff --git a/inject/src/main/java/io/micronaut/inject/UnsafeExecutionHandle.java b/inject/src/main/java/io/micronaut/inject/UnsafeExecutionHandle.java new file mode 100644 index 00000000000..9f2c8c36111 --- /dev/null +++ b/inject/src/main/java/io/micronaut/inject/UnsafeExecutionHandle.java @@ -0,0 +1,39 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.inject; + +import io.micronaut.core.annotation.Internal; + +/** + * A variation of {@link ExecutionHandle} that invokes without arguments validation. + * + * @param The target type + * @param The result type + * @author Denis Stepanov + * @since 4.0.0 + */ +@Internal +public interface UnsafeExecutionHandle extends ExecutionHandle { + + /** + * Invokes the method without validation. + * + * @param arguments The arguments + * @return The result + */ + R invokeUnsafe(Object... arguments); + +} diff --git a/router/src/main/java/io/micronaut/web/router/AbstractRouteMatch.java b/router/src/main/java/io/micronaut/web/router/AbstractRouteMatch.java index 8fac0fa976c..5011a68f3a4 100644 --- a/router/src/main/java/io/micronaut/web/router/AbstractRouteMatch.java +++ b/router/src/main/java/io/micronaut/web/router/AbstractRouteMatch.java @@ -33,6 +33,7 @@ import io.micronaut.http.bind.binders.UnmatchedRequestArgumentBinder; import io.micronaut.inject.ExecutableMethod; import io.micronaut.inject.MethodExecutionHandle; +import io.micronaut.inject.UnsafeExecutionHandle; import io.micronaut.web.router.exceptions.UnsatisfiedRouteException; import java.lang.reflect.Method; @@ -222,6 +223,10 @@ public R execute() { return methodExecutionHandle.invoke(); } if (fulfilled) { + if (methodExecutionHandle instanceof UnsafeExecutionHandle) { + UnsafeExecutionHandle unsafeExecutionHandle = (UnsafeExecutionHandle) methodExecutionHandle; + return unsafeExecutionHandle.invokeUnsafe(argumentValues); + } return methodExecutionHandle.invoke(argumentValues); } if (!beforeBindersApplied) { @@ -253,6 +258,10 @@ public R execute() { throw UnsatisfiedRouteException.create(argument); } } + if (methodExecutionHandle instanceof UnsafeExecutionHandle) { + UnsafeExecutionHandle unsafeExecutionHandle = (UnsafeExecutionHandle) methodExecutionHandle; + return unsafeExecutionHandle.invokeUnsafe(argumentValues); + } return methodExecutionHandle.invoke(argumentValues); } diff --git a/router/src/main/java/io/micronaut/web/router/DefaultMethodBasedRouteInfo.java b/router/src/main/java/io/micronaut/web/router/DefaultMethodBasedRouteInfo.java index 4bcebff8da1..87f379a495b 100644 --- a/router/src/main/java/io/micronaut/web/router/DefaultMethodBasedRouteInfo.java +++ b/router/src/main/java/io/micronaut/web/router/DefaultMethodBasedRouteInfo.java @@ -54,6 +54,7 @@ public class DefaultMethodBasedRouteInfo extends DefaultRouteInfo imple private final Optional> optionalFullBodyArgument; private RequestArgumentBinder[] argumentBinders; + private final boolean needsBody; public DefaultMethodBasedRouteInfo(MethodExecutionHandle targetMethod, @Nullable @@ -96,6 +97,16 @@ public DefaultMethodBasedRouteInfo(MethodExecutionHandle targetMethod, optionalBodyArgument = Optional.empty(); } optionalFullBodyArgument = super.getFullBodyArgument(); + needsBody = optionalBodyArgument.isPresent() || hasArg(arguments, HttpRequest.class); + } + + private static boolean hasArg(Argument[] arguments, Class type) { + for (Argument argument : arguments) { + if (argument.getType() == type) { + return true; + } + } + return false; } @Override @@ -160,4 +171,9 @@ public Optional> getFullBodyArgument() { public String[] getArgumentNames() { return argumentNames; } + + @Override + public boolean needsRequestBody() { + return needsBody || super.needsRequestBody(); + } } diff --git a/router/src/main/java/io/micronaut/web/router/DefaultRouteInfo.java b/router/src/main/java/io/micronaut/web/router/DefaultRouteInfo.java index ffdb03a4a0e..bfbcabc69eb 100644 --- a/router/src/main/java/io/micronaut/web/router/DefaultRouteInfo.java +++ b/router/src/main/java/io/micronaut/web/router/DefaultRouteInfo.java @@ -295,4 +295,9 @@ public ExecutorService getExecutor(ThreadSelection threadSelection) { public AnnotationMetadata getAnnotationMetadata() { return annotationMetadata; } + + @Override + public boolean needsRequestBody() { + return isPermitsBody; + } } diff --git a/router/src/main/java/io/micronaut/web/router/RouteInfo.java b/router/src/main/java/io/micronaut/web/router/RouteInfo.java index ca466d7694c..cef830da96f 100644 --- a/router/src/main/java/io/micronaut/web/router/RouteInfo.java +++ b/router/src/main/java/io/micronaut/web/router/RouteInfo.java @@ -234,4 +234,10 @@ The getBodyArgument() method returns arguments for functions where it is */ @Nullable ExecutorService getExecutor(@Nullable ThreadSelection threadSelection); + + /** + * @return true if the route needs request body to be read + * @since 4.0.0 + */ + boolean needsRequestBody(); } diff --git a/router/src/main/java/io/micronaut/web/router/Router.java b/router/src/main/java/io/micronaut/web/router/Router.java index 87a166eadc8..c14ffa34a9d 100644 --- a/router/src/main/java/io/micronaut/web/router/Router.java +++ b/router/src/main/java/io/micronaut/web/router/Router.java @@ -21,6 +21,7 @@ import io.micronaut.http.HttpRequest; import io.micronaut.http.HttpStatus; import io.micronaut.http.filter.GenericHttpFilter; +import io.micronaut.web.router.exceptions.DuplicateRouteException; import java.net.URI; import java.util.List; @@ -140,6 +141,26 @@ default Stream> find(@NonNull HttpRequest request @NonNull List> findAllClosest(@NonNull HttpRequest request); + /** + * Finds the closest match for the given request or null if none is found. + * + * @param request The request + * @param The target type + * @param The type + * @return A match or null, throws {@link DuplicateRouteException} on multiple routes. + * @since 4.0.0 + */ + @NonNull + default UriRouteMatch findClosest(@NonNull HttpRequest request) throws DuplicateRouteException { + List> uriRoutes = findAllClosest(request); + if (uriRoutes.size() > 1) { + throw new DuplicateRouteException(request.getPath(), (List) uriRoutes); + } else if (uriRoutes.size() == 1) { + return uriRoutes.get(0); + } + return null; + } + /** * Returns all UriRoutes. * From ce9b81271e26603c448e2f51be9323ae1aa65e1f Mon Sep 17 00:00:00 2001 From: micronaut-build <65172877+micronaut-build@users.noreply.github.com> Date: Tue, 11 Apr 2023 11:44:43 +0200 Subject: [PATCH 686/743] Bump micronaut-aws to 3.10.10 (#9084) --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index e7fa1220157..1ea9e146cef 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -67,7 +67,7 @@ managed-methvin-directory-watcher = "0.16.1" managed-micrometer = "1.10.3" managed-micronaut-acme = "3.2.0" managed-micronaut-aot = "1.1.2" -managed-micronaut-aws = "3.10.9" +managed-micronaut-aws = "3.10.10" managed-micronaut-azure = "3.7.1" managed-micronaut-cache = "3.5.0" managed-micronaut-cassandra = "5.1.1" From 755f8360f1ec22624b1909e3d1d0093edb15b76c Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Tue, 11 Apr 2023 12:06:19 +0200 Subject: [PATCH 687/743] Update to Jackson 2.14.2 (#9064) --- gradle/libs.versions.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 03c7cf30ef4..b97dce962a1 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -52,8 +52,8 @@ managed-groovy = "3.0.13" managed-h2 = "1.4.200" managed-hystrix = "1.5.18" managed-jakarta-annotation-api = "2.1.1" -managed-jackson = "2.14.0" -managed-jackson-databind = "2.14.1" +managed-jackson = "2.14.2" +managed-jackson-databind = "2.14.2" managed-javax-annotation-api = "1.3.2" managed-jcache = "1.1.1" managed-jna = "5.12.1" From b297e07ac0173292c0e482ec9d90b536ab20acc7 Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Tue, 11 Apr 2023 12:14:42 +0200 Subject: [PATCH 688/743] build: Micronaut Security 3.11.0 (#9087) --- gradle.properties | 2 +- gradle/libs.versions.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle.properties b/gradle.properties index e273bdde4be..23c1dfcae93 100644 --- a/gradle.properties +++ b/gradle.properties @@ -52,7 +52,7 @@ kotlin.stdlib.default.dependency=false # For the docs graalVersion=22.0.0.2 -micronautSecurityVersion=3.10.1 +micronautSecurityVersion=3.11.0 org.gradle.caching=true org.gradle.parallel=true diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index b97dce962a1..4d20a7976aa 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -113,7 +113,7 @@ managed-micronaut-rss = "3.2.0" managed-micronaut-rxjava1 = "1.0.0" managed-micronaut-rxjava2 = "1.3.0" managed-micronaut-rxjava3 = "2.4.0" -managed-micronaut-security = "3.10.1" +managed-micronaut-security = "3.11.0" managed-micronaut-serialization = "1.5.2" managed-micronaut-servlet = "3.3.5" managed-micronaut-spring = "4.5.1" From b8ae15acc5a52d74c10f85d348b86fd70cc191da Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Tue, 11 Apr 2023 12:58:04 +0200 Subject: [PATCH 689/743] CRaC 1.2.2 (#9088) --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 4d20a7976aa..9746ebccaab 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -73,7 +73,7 @@ managed-micronaut-cache = "3.5.0" managed-micronaut-chatbots = "1.0.0-M1" managed-micronaut-cassandra = "5.1.1" managed-micronaut-coherence = "3.7.2" -managed-micronaut-crac = "1.2.1" +managed-micronaut-crac = "1.2.2" managed-micronaut-data = "3.9.7" managed-micronaut-discovery = "3.3.0" managed-micronaut-elasticsearch = "4.3.0" From 0dcd54193ac06bf803c9c2bb38e3846964fa5ab1 Mon Sep 17 00:00:00 2001 From: Jonas Konrad Date: Tue, 11 Apr 2023 15:07:14 +0200 Subject: [PATCH 690/743] Fix buffer leak (#9092) Fixes #9091 --- .../netty/body/ImmediateMultiObjectBody.java | 3 +-- .../netty/body/ImmediateSingleObjectBody.java | 14 ++++++++++++-- .../io/micronaut/json/convert/LazyJsonNode.java | 2 +- 3 files changed, 14 insertions(+), 5 deletions(-) diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/body/ImmediateMultiObjectBody.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/body/ImmediateMultiObjectBody.java index 267aa7bcaa5..624a4332cb8 100644 --- a/http-server-netty/src/main/java/io/micronaut/http/server/netty/body/ImmediateMultiObjectBody.java +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/body/ImmediateMultiObjectBody.java @@ -26,7 +26,6 @@ import io.netty.buffer.ByteBufInputStream; import io.netty.buffer.CompositeByteBuf; import io.netty.buffer.Unpooled; -import io.netty.util.ReferenceCountUtil; import io.netty.util.ReferenceCounted; import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; @@ -55,7 +54,7 @@ public final class ImmediateMultiObjectBody extends ManagedBody> impleme @Override void release(List value) { - value.forEach(ReferenceCountUtil::release); + value.forEach(ImmediateSingleObjectBody::release0); } public ImmediateSingleObjectBody single(Charset defaultCharset, ByteBufAllocator alloc) { diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/body/ImmediateSingleObjectBody.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/body/ImmediateSingleObjectBody.java index bc14d64f665..7c9ebec62ac 100644 --- a/http-server-netty/src/main/java/io/micronaut/http/server/netty/body/ImmediateSingleObjectBody.java +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/body/ImmediateSingleObjectBody.java @@ -18,10 +18,10 @@ import io.micronaut.core.annotation.Internal; import io.micronaut.http.server.netty.FormRouteCompleter; import io.micronaut.http.server.netty.NettyHttpRequest; +import io.micronaut.json.convert.LazyJsonNode; import io.netty.buffer.ByteBufAllocator; import io.netty.buffer.ByteBufHolder; import io.netty.buffer.ByteBufInputStream; -import io.netty.util.ReferenceCountUtil; import io.netty.util.ReferenceCounted; import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; @@ -45,7 +45,17 @@ public final class ImmediateSingleObjectBody extends ManagedBody impleme @Override void release(Object value) { - ReferenceCountUtil.release(value); + release0(value); + } + + static void release0(Object value) { + if (value instanceof LazyJsonNode rc) { + // need to release LazyJsonNode in case it hasn't been converted yet. But conversion + // can also happen multiple times, so tryRelease is the best we can do, unfortunately. + rc.tryRelease(); + } else if (value instanceof ReferenceCounted rc) { + rc.release(); + } } /** diff --git a/json-core/src/main/java/io/micronaut/json/convert/LazyJsonNode.java b/json-core/src/main/java/io/micronaut/json/convert/LazyJsonNode.java index d074ee4e1ba..f1fd18a150e 100644 --- a/json-core/src/main/java/io/micronaut/json/convert/LazyJsonNode.java +++ b/json-core/src/main/java/io/micronaut/json/convert/LazyJsonNode.java @@ -186,7 +186,7 @@ public boolean release() { * Try to release this node if it hasn't been released already. */ @Internal - void tryRelease() { + public void tryRelease() { // this is a bit yikes but it's necessary so we can attempt conversion twice. // it seems to work fine because the first conversion is to JsonNode, which we store // locally. From 5113c335b070e2db1c3f892d025c95955ee85e2f Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Tue, 11 Apr 2023 16:33:50 +0200 Subject: [PATCH 691/743] use Milestone versions (#9085) * use Milestone versions * Fix management test H2 version was updated in the micronaut-sql bom This commit aligns those versions, and fixes the test * Use validation M3 --------- Co-authored-by: Tim Yates --- gradle/libs.versions.toml | 14 +++++++------- .../endpoint/health/HealthEndpointSpec.groovy | 4 ++-- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index a7fce8ede43..a1bddd3c574 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -10,7 +10,7 @@ geb = "7.0" gorm = "7.3.2" # be sure to update graal version in gradle.properties as well graal-svm = "22.3.1" -h2 = "2.1.210" +h2 = "2.1.214" hibernate = "5.5.9.Final" hibernate-validator = "6.1.6.Final" htmlSanityCheck = "1.1.6" @@ -36,13 +36,13 @@ managed-logback = "1.4.6" logbook-netty = "2.14.0" log4j = "2.19.0" micronaut-aws = "3.9.2" -micronaut-groovy = "4.0.0-SNAPSHOT" -micronaut-session = "1.0.0-SNAPSHOT" -micronaut-sql = "4.7.2" -micronaut-test = "4.0.0-SNAPSHOT" -micronaut-serde = "2.0.0-SNAPSHOT" +micronaut-groovy = "4.0.0-M1" +micronaut-session = "4.0.0-M1" +micronaut-sql = "5.0.0-M3" +micronaut-test = "4.0.0-M1" +micronaut-serde = "2.0.0-M1" micronaut-tracing = "5.0.0-SNAPSHOT" -micronaut-validation = "4.0.0-SNAPSHOT" +micronaut-validation = "4.0.0-M3" micrometer = "1.10.2" neo4j-java-driver = "1.4.5" selenium = "4.7.2" diff --git a/management/src/test/groovy/io/micronaut/management/endpoint/health/HealthEndpointSpec.groovy b/management/src/test/groovy/io/micronaut/management/endpoint/health/HealthEndpointSpec.groovy index 321e65b1cdf..e96f478e299 100644 --- a/management/src/test/groovy/io/micronaut/management/endpoint/health/HealthEndpointSpec.groovy +++ b/management/src/test/groovy/io/micronaut/management/endpoint/health/HealthEndpointSpec.groovy @@ -159,10 +159,10 @@ class HealthEndpointSpec extends Specification { result.details.jdbc.status == "UP" result.details.jdbc.details."jdbc:h2:mem:oneDb".status == "UP" result.details.jdbc.details."jdbc:h2:mem:oneDb".details.database == "H2" - result.details.jdbc.details."jdbc:h2:mem:oneDb".details.version == "2.1.210 (2022-01-17)" + result.details.jdbc.details."jdbc:h2:mem:oneDb".details.version == "2.1.214 (2022-06-13)" result.details.jdbc.details."jdbc:h2:mem:twoDb".status == "UP" result.details.jdbc.details."jdbc:h2:mem:twoDb".details.database == "H2" - result.details.jdbc.details."jdbc:h2:mem:twoDb".details.version == "2.1.210 (2022-01-17)" + result.details.jdbc.details."jdbc:h2:mem:twoDb".details.version == "2.1.214 (2022-06-13)" result.details.service.status == "UP" cleanup: From 8d1db4efd561d9f0ada4301ab8ba993c387f86ce Mon Sep 17 00:00:00 2001 From: Alexander Simpson Date: Wed, 12 Apr 2023 06:20:35 -0500 Subject: [PATCH 692/743] Idea(8855)/service health indicator (#9061) * added management to gradle * added health indicator config * added indicator factory * added unit tests * updated test and added to service discovery manual * removed dependency and updated version to 3.9 * moved indicator to management module, removed health-indicator config in favor of config under endpoints * revert change to lib * changed to compileonly and added additional config requirement in indicator * removed factory and references --- management/build.gradle | 1 + .../ServiceHttpClientHealthIndicator.java | 85 +++++++++++++++++++ ...erviceHttpClientHealthIndicatorSpec.groovy | 66 ++++++++++++++ 3 files changed, 152 insertions(+) create mode 100644 management/src/main/java/io/micronaut/management/health/indicator/client/ServiceHttpClientHealthIndicator.java create mode 100644 management/src/test/groovy/io/micronaut/management/health/indicator/client/ServiceHttpClientHealthIndicatorSpec.groovy diff --git a/management/build.gradle b/management/build.gradle index 568e3422750..8ac3ec53265 100644 --- a/management/build.gradle +++ b/management/build.gradle @@ -12,6 +12,7 @@ dependencies { exclude module:'micronaut-inject' exclude module:'micronaut-bom' } + compileOnly project(":http-client-core") implementation libs.managed.reactor diff --git a/management/src/main/java/io/micronaut/management/health/indicator/client/ServiceHttpClientHealthIndicator.java b/management/src/main/java/io/micronaut/management/health/indicator/client/ServiceHttpClientHealthIndicator.java new file mode 100644 index 00000000000..678f536f6bc --- /dev/null +++ b/management/src/main/java/io/micronaut/management/health/indicator/client/ServiceHttpClientHealthIndicator.java @@ -0,0 +1,85 @@ +/* + * Copyright 2017-2023 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.management.health.indicator.client; + +import io.micronaut.context.annotation.EachBean; +import io.micronaut.context.annotation.Parameter; +import io.micronaut.context.annotation.Requires; +import io.micronaut.core.async.publisher.Publishers; +import io.micronaut.core.util.StringUtils; +import io.micronaut.discovery.StaticServiceInstanceList; +import io.micronaut.health.HealthStatus; +import io.micronaut.http.client.ServiceHttpClientConfiguration; +import io.micronaut.management.endpoint.health.HealthEndpoint; +import io.micronaut.management.health.indicator.HealthIndicator; +import io.micronaut.management.health.indicator.HealthResult; +import org.reactivestreams.Publisher; + +import java.net.URI; +import java.util.Collection; +import java.util.LinkedHashMap; +import java.util.Map; + +/** + *

A {@link io.micronaut.management.health.indicator.HealthIndicator} used to display available load balancer URLs. + * Returns {@link HealthStatus#DOWN} if there are no available URLs in the load balancer.

+ * + * @author Alexander Simpson + * @since 3.9 + */ +@EachBean(ServiceHttpClientConfiguration.class) +@Requires(beans = HealthEndpoint.class) +@Requires(classes = ServiceHttpClientConfiguration.class) +@Requires(property = HealthEndpoint.PREFIX + ".service-http-client.enabled", defaultValue = StringUtils.FALSE, notEquals = StringUtils.FALSE) +public class ServiceHttpClientHealthIndicator implements HealthIndicator { + + private final ServiceHttpClientConfiguration configuration; + private final Collection loadBalancerUrls; + private final Collection originalUrls; + private final HealthResult.Builder serviceHealthBuilder; + + /** + * @param configuration Configuration for the individual service http client. + * @param instanceList Instance List for the individual service http client. Used to obtain available load balancer URLs. + */ + public ServiceHttpClientHealthIndicator(@Parameter ServiceHttpClientConfiguration configuration, @Parameter StaticServiceInstanceList instanceList) { + this.configuration = configuration; + this.loadBalancerUrls = instanceList.getLoadBalancedURIs(); + this.originalUrls = configuration.getUrls(); + this.serviceHealthBuilder = HealthResult.builder(configuration.getServiceId()); + } + + @Override + public Publisher getResult() { + if (!configuration.isHealthCheck()) { + return Publishers.empty(); + } + + return Publishers.just(determineServiceHealth()); + } + + private HealthResult determineServiceHealth() { + Map details = new LinkedHashMap<>(2); + details.put("all_urls", originalUrls); + details.put("available_urls", loadBalancerUrls); + + if (loadBalancerUrls.isEmpty()) { + return serviceHealthBuilder.status(HealthStatus.DOWN).details(details).build(); + } + + return serviceHealthBuilder.status(HealthStatus.UP).details(details).build(); + } +} diff --git a/management/src/test/groovy/io/micronaut/management/health/indicator/client/ServiceHttpClientHealthIndicatorSpec.groovy b/management/src/test/groovy/io/micronaut/management/health/indicator/client/ServiceHttpClientHealthIndicatorSpec.groovy new file mode 100644 index 00000000000..24b88ea1073 --- /dev/null +++ b/management/src/test/groovy/io/micronaut/management/health/indicator/client/ServiceHttpClientHealthIndicatorSpec.groovy @@ -0,0 +1,66 @@ +package io.micronaut.management.health.indicator.client + +import io.micronaut.discovery.StaticServiceInstanceList +import io.micronaut.health.HealthStatus +import io.micronaut.http.client.ServiceHttpClientConfiguration +import io.micronaut.runtime.ApplicationConfiguration +import reactor.core.publisher.Mono +import spock.lang.Specification + +class ServiceHttpClientHealthIndicatorSpec extends Specification { + + def static uri1 = new URI("http://localhost:8080") + def static uri2 = new URI("http://localhost:8081") + def instanceList = new StaticServiceInstanceList("some-http-service", [uri1, uri2]) + + def serviceHttpConfiguration = new ServiceHttpClientConfiguration("some-http-service", null, null, GroovyMock(ApplicationConfiguration)) + + def "Health Indicator is set to true and is healthy"() { + given: + serviceHttpConfiguration.setHealthCheck(true) + def healthIndicator = new ServiceHttpClientHealthIndicator(serviceHttpConfiguration, instanceList) + + when: + def result = Mono.from(healthIndicator.getResult()).block() + + then: + HealthStatus.UP == result.status + + 0 * _ + } + + def "Health Indicator and check are true, instance list is updated - #scenario"() { + given: + serviceHttpConfiguration.setHealthCheck(true) + def healthIndicator = new ServiceHttpClientHealthIndicator(serviceHttpConfiguration, instanceList) + + when: "uri is removed from list" + instanceList.getLoadBalancedURIs().removeAll(urisToRemove) + + def result = Mono.from(healthIndicator.getResult()).block() + + then: + expectedStatus == result.status + + 0 * _ + + where: + scenario | urisToRemove || expectedStatus + "one uri is removed" | [uri1] || HealthStatus.UP + "both uris are removed" | [uri1, uri2] || HealthStatus.DOWN + } + + def "Calling getResult but health-check is false, so result is null"() { + given: + serviceHttpConfiguration.setHealthCheck(false) + def healthIndicator = new ServiceHttpClientHealthIndicator(serviceHttpConfiguration, instanceList) + + when: + def result = Mono.from(healthIndicator.getResult()).block() + + then: + null == result + + 0 * _ + } +} From 1ef53a853dc99f2a9f3c2b174204ffbd4aafe774 Mon Sep 17 00:00:00 2001 From: micronaut-build <65172877+micronaut-build@users.noreply.github.com> Date: Wed, 12 Apr 2023 17:12:50 +0200 Subject: [PATCH 693/743] Bump micronaut-data to 3.10.0 (#9095) --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 9746ebccaab..e079c282630 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -74,7 +74,7 @@ managed-micronaut-chatbots = "1.0.0-M1" managed-micronaut-cassandra = "5.1.1" managed-micronaut-coherence = "3.7.2" managed-micronaut-crac = "1.2.2" -managed-micronaut-data = "3.9.7" +managed-micronaut-data = "3.10.0" managed-micronaut-discovery = "3.3.0" managed-micronaut-elasticsearch = "4.3.0" managed-micronaut-email = "1.5.0" From 2bdf5d4a0f21e8c13701dbe656d877cd7efe9e95 Mon Sep 17 00:00:00 2001 From: micronaut-build <65172877+micronaut-build@users.noreply.github.com> Date: Wed, 12 Apr 2023 18:14:41 +0200 Subject: [PATCH 694/743] Bump micronaut-aws to 3.17.0 (#9100) --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index e079c282630..db37b273d55 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -67,7 +67,7 @@ managed-methvin-directory-watcher = "0.16.1" managed-micrometer = "1.10.5" managed-micronaut-acme = "3.2.0" managed-micronaut-aot = "1.1.2" -managed-micronaut-aws = "3.16.0" +managed-micronaut-aws = "3.17.0" managed-micronaut-azure = "3.10.0" managed-micronaut-cache = "3.5.0" managed-micronaut-chatbots = "1.0.0-M1" From 940f37e5d861ac540b96fc6c01ef34cea26744c2 Mon Sep 17 00:00:00 2001 From: Dean Wette Date: Wed, 12 Apr 2023 11:15:39 -0500 Subject: [PATCH 695/743] Deprecate `ConversionService.SHARED` and update custom converter docs/examples to use ConversionService bean. (#8695) * Deprecate `ConversionService.SHARED` and update custom converter docs/examples to use ConversionService bean. closes #8620 * build: Micronaut Build Plugins to 5.4.3 * build plugin to 5.4.6 --------- Co-authored-by: Sergio del Amo --- .../core/convert/ConversionService.java | 3 +++ settings.gradle | 2 +- .../guide/config/customTypeConverter.adoc | 7 +++++-- .../docs/guide/introduction/whatsNew.adoc | 7 +++++++ .../converters/MapToLocalDateConverter.groovy | 19 ++++++++++++------ .../converters/MapToLocalDateConverter.kt | 20 +++++++++++-------- .../converters/MapToLocalDateConverter.java | 19 ++++++++++++------ 7 files changed, 54 insertions(+), 23 deletions(-) diff --git a/core/src/main/java/io/micronaut/core/convert/ConversionService.java b/core/src/main/java/io/micronaut/core/convert/ConversionService.java index e99afba7288..1b39073e96a 100644 --- a/core/src/main/java/io/micronaut/core/convert/ConversionService.java +++ b/core/src/main/java/io/micronaut/core/convert/ConversionService.java @@ -32,7 +32,10 @@ public interface ConversionService { /** * The default shared conversion service. + * + * @deprecated This will be removed in the next major version. Use an injected {@link ConversionService} instead. */ + @Deprecated ConversionService SHARED = new DefaultConversionService(); /** diff --git a/settings.gradle b/settings.gradle index fd8adac26dd..8654fe66d1d 100644 --- a/settings.gradle +++ b/settings.gradle @@ -14,7 +14,7 @@ pluginManagement { } plugins { - id 'io.micronaut.build.shared.settings' version '5.4.6' + id 'io.micronaut.build.shared.settings' version '5.4.8' } enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") diff --git a/src/main/docs/guide/config/customTypeConverter.adoc b/src/main/docs/guide/config/customTypeConverter.adoc index 94f32456cb3..6a27a9bea3c 100644 --- a/src/main/docs/guide/config/customTypeConverter.adoc +++ b/src/main/docs/guide/config/customTypeConverter.adoc @@ -19,5 +19,8 @@ This won't work by default, since there is no built-in conversion from `Map` to snippet::io.micronaut.docs.config.converters.MapToLocalDateConverter[tags="imports,class", indent=0] <1> The class implements api:core.convert.TypeConverter[] which has two generic arguments, the type you are converting from, and the type you are converting to -<2> The implementation delegates to the default shared conversion service to convert the values from the Map used to create a `LocalDate` -<3> If an exception occurs during binding, call `reject(..)` which propagates additional information to the container +<2> The constructor injects a bean of type `ConversionService` +<3> The implementation delegates to the injected conversion service to convert the values from the Map used to create a `LocalDate` +<4> If an exception occurs during binding, call `reject(..)` which propagates additional information to the container + +NOTE: Register new type converters by declaring a bean of type api:core.convert.TypeConverter[]. `ConversionService.SHARED` is deprecated, and it will be removed in a future version of the Micronaut Framework. diff --git a/src/main/docs/guide/introduction/whatsNew.adoc b/src/main/docs/guide/introduction/whatsNew.adoc index 55a613d0ac4..2e087767913 100644 --- a/src/main/docs/guide/introduction/whatsNew.adoc +++ b/src/main/docs/guide/introduction/whatsNew.adoc @@ -1,4 +1,11 @@ //Micronaut {version} includes the following changes: +== + +== 3.9.0 + +=== Deprecations +`ConversionService.SHARED` is deprecated, and it will be removed in a future version of the Micronaut Framework. Use an injected api:core.convert.ConversionService[] bean instead, and register new type converters by declaring beans of type api:core.convert.TypeConverter[]. + == 3.8.0 Key features: diff --git a/test-suite-groovy/src/test/groovy/io/micronaut/docs/config/converters/MapToLocalDateConverter.groovy b/test-suite-groovy/src/test/groovy/io/micronaut/docs/config/converters/MapToLocalDateConverter.groovy index 22a0a54363d..f02fa66dfce 100644 --- a/test-suite-groovy/src/test/groovy/io/micronaut/docs/config/converters/MapToLocalDateConverter.groovy +++ b/test-suite-groovy/src/test/groovy/io/micronaut/docs/config/converters/MapToLocalDateConverter.groovy @@ -1,5 +1,5 @@ /* - * Copyright 2017-2020 original authors + * Copyright 2017-2023 original authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -28,16 +28,23 @@ import java.time.LocalDate // tag::class[] @Singleton class MapToLocalDateConverter implements TypeConverter { // <1> + + final ConversionService conversionService + + MapToLocalDateConverter(ConversionService conversionService) { // <2> + this.conversionService = conversionService; + } + @Override Optional convert(Map propertyMap, Class targetType, ConversionContext context) { - Optional day = ConversionService.SHARED.convert(propertyMap.day, Integer) - Optional month = ConversionService.SHARED.convert(propertyMap.month, Integer) - Optional year = ConversionService.SHARED.convert(propertyMap.year, Integer) + Optional day = conversionService.convert(propertyMap.day, Integer) + Optional month = conversionService.convert(propertyMap.month, Integer) + Optional year = conversionService.convert(propertyMap.year, Integer) if (day.present && month.present && year.present) { try { - return Optional.of(LocalDate.of(year.get(), month.get(), day.get())) // <2> + return Optional.of(LocalDate.of(year.get(), month.get(), day.get())) // <3> } catch (DateTimeException e) { - context.reject(propertyMap, e) // <3> + context.reject(propertyMap, e) // <4> return Optional.empty() } } diff --git a/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/config/converters/MapToLocalDateConverter.kt b/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/config/converters/MapToLocalDateConverter.kt index 1b65c027717..eab216597b5 100644 --- a/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/config/converters/MapToLocalDateConverter.kt +++ b/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/config/converters/MapToLocalDateConverter.kt @@ -1,5 +1,5 @@ /* - * Copyright 2017-2020 original authors + * Copyright 2017-2023 original authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,24 +19,28 @@ package io.micronaut.docs.config.converters import io.micronaut.core.convert.ConversionContext import io.micronaut.core.convert.ConversionService import io.micronaut.core.convert.TypeConverter +import jakarta.inject.Singleton import java.time.DateTimeException import java.time.LocalDate import java.util.Optional -import jakarta.inject.Singleton // end::imports[] // tag::class[] @Singleton -class MapToLocalDateConverter : TypeConverter, LocalDate> { // <1> +class MapToLocalDateConverter( + private val conversionService: ConversionService<*> // <2> +) + : TypeConverter, LocalDate> { // <1> + override fun convert(propertyMap: Map<*, *>, targetType: Class, context: ConversionContext): Optional { - val day = ConversionService.SHARED.convert(propertyMap["day"], Int::class.java) - val month = ConversionService.SHARED.convert(propertyMap["month"], Int::class.java) - val year = ConversionService.SHARED.convert(propertyMap["year"], Int::class.java) + val day = conversionService.convert(propertyMap["day"], Int::class.java) + val month = conversionService.convert(propertyMap["month"], Int::class.java) + val year = conversionService.convert(propertyMap["year"], Int::class.java) if (day.isPresent && month.isPresent && year.isPresent) { try { - return Optional.of(LocalDate.of(year.get(), month.get(), day.get())) // <2> + return Optional.of(LocalDate.of(year.get(), month.get(), day.get())) // <3> } catch (e: DateTimeException) { - context.reject(propertyMap, e) // <3> + context.reject(propertyMap, e) // <4> return Optional.empty() } } diff --git a/test-suite/src/test/java/io/micronaut/docs/config/converters/MapToLocalDateConverter.java b/test-suite/src/test/java/io/micronaut/docs/config/converters/MapToLocalDateConverter.java index 04696212fa8..1d60ffc3092 100644 --- a/test-suite/src/test/java/io/micronaut/docs/config/converters/MapToLocalDateConverter.java +++ b/test-suite/src/test/java/io/micronaut/docs/config/converters/MapToLocalDateConverter.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2020 original authors + * Copyright 2017-2023 original authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -30,16 +30,23 @@ // tag::class[] @Singleton public class MapToLocalDateConverter implements TypeConverter { // <1> + + private final ConversionService conversionService; + + public MapToLocalDateConverter(ConversionService conversionService) { // <2> + this.conversionService = conversionService; + } + @Override public Optional convert(Map propertyMap, Class targetType, ConversionContext context) { - Optional day = ConversionService.SHARED.convert(propertyMap.get("day"), Integer.class); - Optional month = ConversionService.SHARED.convert(propertyMap.get("month"), Integer.class); - Optional year = ConversionService.SHARED.convert(propertyMap.get("year"), Integer.class); + Optional day = conversionService.convert(propertyMap.get("day"), Integer.class); + Optional month = conversionService.convert(propertyMap.get("month"), Integer.class); + Optional year = conversionService.convert(propertyMap.get("year"), Integer.class); if (day.isPresent() && month.isPresent() && year.isPresent()) { try { - return Optional.of(LocalDate.of(year.get(), month.get(), day.get())); // <2> + return Optional.of(LocalDate.of(year.get(), month.get(), day.get())); // <3> } catch (DateTimeException e) { - context.reject(propertyMap, e); // <3> + context.reject(propertyMap, e); // <4> return Optional.empty(); } } From 1a7b7b1c57c2b2ab568ea2ac7b00663ec3745791 Mon Sep 17 00:00:00 2001 From: Nick Hensel <47005420+Goldmensch@users.noreply.github.com> Date: Thu, 13 Apr 2023 08:11:48 +0200 Subject: [PATCH 696/743] Load ApplicationContextConfigurer with passed classloader instead of the class one (#8608) * fix: Load ApplicationContextConfigurer with passed classloader instead of the class one * test: Add test for loading ApplicationContextConfigurer with passed class loader --- .../micronaut/context/ApplicationContext.java | 16 +++++++++---- .../DefaultApplicationContextBuilder.java | 7 +++++- .../ApplicationContextBuilderSpec.groovy | 23 +++++++++++++++++++ ...io.micronaut.context.TestContextConfigurer | 0 4 files changed, 41 insertions(+), 5 deletions(-) create mode 100644 inject/src/test/resources/test-meta-inf/META-INF/micronaut/io.micronaut.context.ApplicationContextConfigurer/io.micronaut.context.TestContextConfigurer diff --git a/inject/src/main/java/io/micronaut/context/ApplicationContext.java b/inject/src/main/java/io/micronaut/context/ApplicationContext.java index cdb9f011b29..1c49105581a 100644 --- a/inject/src/main/java/io/micronaut/context/ApplicationContext.java +++ b/inject/src/main/java/io/micronaut/context/ApplicationContext.java @@ -256,7 +256,7 @@ public interface ApplicationContext extends BeanContext, PropertyResolver, Prope */ static @NonNull ApplicationContextBuilder builder(@NonNull String... environments) { ArgumentUtils.requireNonNull("environments", environments); - return new DefaultApplicationContextBuilder() + return builder() .environments(environments); } @@ -270,7 +270,7 @@ public interface ApplicationContext extends BeanContext, PropertyResolver, Prope static @NonNull ApplicationContextBuilder builder(@NonNull Map properties, @NonNull String... environments) { ArgumentUtils.requireNonNull("environments", environments); ArgumentUtils.requireNonNull("properties", properties); - return new DefaultApplicationContextBuilder() + return builder() .properties(properties) .environments(environments); } @@ -284,6 +284,14 @@ public interface ApplicationContext extends BeanContext, PropertyResolver, Prope return new DefaultApplicationContextBuilder(); } + /** + * @param classLoader The class loader to use + * @return The application context builder + */ + static @NonNull ApplicationContextBuilder builder(ClassLoader classLoader) { + return new DefaultApplicationContextBuilder(classLoader); + } + /** * Run the {@link BeanContext}. This method will instantiate a new {@link BeanContext} and call {@link #start()} * @@ -308,8 +316,8 @@ public interface ApplicationContext extends BeanContext, PropertyResolver, Prope ArgumentUtils.requireNonNull("environments", environments); ArgumentUtils.requireNonNull("classLoader", classLoader); - return builder(environments) - .classLoader(classLoader); + return builder(classLoader) + .environments(environments); } /** diff --git a/inject/src/main/java/io/micronaut/context/DefaultApplicationContextBuilder.java b/inject/src/main/java/io/micronaut/context/DefaultApplicationContextBuilder.java index ae86c07eba2..8f68feb2bbf 100644 --- a/inject/src/main/java/io/micronaut/context/DefaultApplicationContextBuilder.java +++ b/inject/src/main/java/io/micronaut/context/DefaultApplicationContextBuilder.java @@ -75,6 +75,11 @@ protected DefaultApplicationContextBuilder() { loadApplicationContextCustomizer(resolveClassLoader()).configure(this); } + DefaultApplicationContextBuilder(ClassLoader classLoader) { + loadApplicationContextCustomizer(classLoader).configure(this); + this.classLoader = classLoader; + } + private ClassLoader resolveClassLoader() { final ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader(); if (contextClassLoader != null) { @@ -89,7 +94,7 @@ public boolean isAllowEmptyProviders() { } @Override - @NonNull + @NonNull public ApplicationContextBuilder enableDefaultPropertySources(boolean areEnabled) { this.enableDefaultPropertySources = areEnabled; return this; diff --git a/inject/src/test/groovy/io/micronaut/context/ApplicationContextBuilderSpec.groovy b/inject/src/test/groovy/io/micronaut/context/ApplicationContextBuilderSpec.groovy index fe777cf7195..047ef74db2b 100644 --- a/inject/src/test/groovy/io/micronaut/context/ApplicationContextBuilderSpec.groovy +++ b/inject/src/test/groovy/io/micronaut/context/ApplicationContextBuilderSpec.groovy @@ -37,6 +37,29 @@ class ApplicationContextBuilderSpec extends Specification { config.resourceLoader.classLoader.is(loader) config.environments.contains('foo') config.deduceEnvironments.get() == false + } + + void "test context configurer with own class loader"() { + given: + def loader = new GroovyClassLoader() + loader.parseClass(""" + package io.micronaut.context + import io.micronaut.context.* + + class TestContextConfigurer implements ApplicationContextConfigurer { + @Override + void configure(ApplicationContextBuilder builder) { + builder.environments("success") + } + } +""") + loader.addURL(getClass().getResource("/test-meta-inf/")) + + ApplicationContext context = ApplicationContext.builder(loader) + .start() + + expect: + context.getEnvironment().getActiveNames().contains("success") } } diff --git a/inject/src/test/resources/test-meta-inf/META-INF/micronaut/io.micronaut.context.ApplicationContextConfigurer/io.micronaut.context.TestContextConfigurer b/inject/src/test/resources/test-meta-inf/META-INF/micronaut/io.micronaut.context.ApplicationContextConfigurer/io.micronaut.context.TestContextConfigurer new file mode 100644 index 00000000000..e69de29bb2d From 8a63ceddbccd9e099d3927765d4f9960c5d51977 Mon Sep 17 00:00:00 2001 From: micronaut-build <65172877+micronaut-build@users.noreply.github.com> Date: Thu, 13 Apr 2023 08:20:09 +0200 Subject: [PATCH 697/743] Bump micronaut-elasticsearch to 4.4.0 (#9101) --- gradle/libs.versions.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index db37b273d55..3270fb3e985 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -33,7 +33,7 @@ wiremock = "2.33.2" # that they will appear in the Micronaut BOM as # managed-dekorate = "1.0.3" -managed-elasticsearch = "7.16.3" +managed-elasticsearch = "" managed-ignite = "2.13.0" managed-junit5 = "5.9.1" managed-junit-platform="1.9.1" @@ -76,7 +76,7 @@ managed-micronaut-coherence = "3.7.2" managed-micronaut-crac = "1.2.2" managed-micronaut-data = "3.10.0" managed-micronaut-discovery = "3.3.0" -managed-micronaut-elasticsearch = "4.3.0" +managed-micronaut-elasticsearch = "4.4.0" managed-micronaut-email = "1.5.0" managed-micronaut-flyway = "5.5.0" managed-micronaut-gcp = "4.10.0" From ba2890e90104c32666f60f79bd70e5c7824a7253 Mon Sep 17 00:00:00 2001 From: micronaut-build Date: Thu, 13 Apr 2023 07:20:19 +0000 Subject: [PATCH 698/743] [skip ci] Release v3.8.9 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 7ddadd8c695..7f54a7e5bab 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -projectVersion=3.8.9-SNAPSHOT +projectVersion=3.8.9 projectGroupId=io.micronaut title=Micronaut projectDesc=Natively Cloud Native From efb066ac6a5b0e2a9cbceb45fbe3ca63b813ea1d Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Thu, 13 Apr 2023 09:24:08 +0200 Subject: [PATCH 699/743] build: managed-elasticsearch 7.17.9 (#9102) --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 3270fb3e985..5695466c695 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -33,7 +33,7 @@ wiremock = "2.33.2" # that they will appear in the Micronaut BOM as # managed-dekorate = "1.0.3" -managed-elasticsearch = "" +managed-elasticsearch = "7.17.9" managed-ignite = "2.13.0" managed-junit5 = "5.9.1" managed-junit-platform="1.9.1" From 77d00c70f41764171458a886785df29c2356b83e Mon Sep 17 00:00:00 2001 From: micronaut-build Date: Thu, 13 Apr 2023 07:39:02 +0000 Subject: [PATCH 700/743] Back to 3.8.10-SNAPSHOT --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 7f54a7e5bab..97753d1044c 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -projectVersion=3.8.9 +projectVersion=3.8.10-SNAPSHOT projectGroupId=io.micronaut title=Micronaut projectDesc=Natively Cloud Native From 8c37c57b6cb02e85536736d27e6acabcf4b159f1 Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Thu, 13 Apr 2023 10:09:27 +0200 Subject: [PATCH 701/743] build: crac 1.2.3 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 5695466c695..7655b309290 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -73,7 +73,7 @@ managed-micronaut-cache = "3.5.0" managed-micronaut-chatbots = "1.0.0-M1" managed-micronaut-cassandra = "5.1.1" managed-micronaut-coherence = "3.7.2" -managed-micronaut-crac = "1.2.2" +managed-micronaut-crac = "1.2.3" managed-micronaut-data = "3.10.0" managed-micronaut-discovery = "3.3.0" managed-micronaut-elasticsearch = "4.4.0" From a436cb83c8c6bec8f9b62eebb9b30d7252bbbc39 Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Thu, 13 Apr 2023 10:09:48 +0200 Subject: [PATCH 702/743] discoery client 3.3.1 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 7655b309290..e4c38f3ea94 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -75,7 +75,7 @@ managed-micronaut-cassandra = "5.1.1" managed-micronaut-coherence = "3.7.2" managed-micronaut-crac = "1.2.3" managed-micronaut-data = "3.10.0" -managed-micronaut-discovery = "3.3.0" +managed-micronaut-discovery = "3.3.1" managed-micronaut-elasticsearch = "4.4.0" managed-micronaut-email = "1.5.0" managed-micronaut-flyway = "5.5.0" From 04c93d8201bb4139cde8cf157f886fb2cb97779a Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Thu, 13 Apr 2023 10:10:07 +0200 Subject: [PATCH 703/743] build: rxjava 3 2.4.1 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index e4c38f3ea94..477c351affc 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -112,7 +112,7 @@ managed-micronaut-redis = "5.3.2" managed-micronaut-rss = "3.2.0" managed-micronaut-rxjava1 = "1.0.0" managed-micronaut-rxjava2 = "1.3.0" -managed-micronaut-rxjava3 = "2.4.0" +managed-micronaut-rxjava3 = "2.4.1" managed-micronaut-security = "3.11.0" managed-micronaut-serialization = "1.5.2" managed-micronaut-servlet = "3.3.5" From 9e397f91aad6c02bb239bb048c63a7b8f20a050f Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Thu, 13 Apr 2023 10:10:32 +0200 Subject: [PATCH 704/743] build: test-resources 1.2.5 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 477c351affc..f818771acdf 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -119,7 +119,7 @@ managed-micronaut-servlet = "3.3.5" managed-micronaut-spring = "4.5.1" managed-micronaut-sql = "4.8.0" managed-micronaut-test = "3.9.2" -managed-micronaut-test-resources = "1.2.3" +managed-micronaut-test-resources = "1.2.5" managed-micronaut-toml = "1.1.3" managed-micronaut-tracing = "4.5.0" managed-micronaut-tracing-legacy = "3.2.7" From 200474982ce1d974e8a32e916eeada2e64c104c6 Mon Sep 17 00:00:00 2001 From: Tim Yates Date: Fri, 14 Apr 2023 08:51:03 +0100 Subject: [PATCH 705/743] Remove unused logback.xml file (#9104) In https://github.com/micronaut-projects/micronaut-core/pull/8865 for 4.0.x we moved the native tests to the TCK At the same time we added logback to the test-suite-graal module to 3.9.x https://github.com/micronaut-projects/micronaut-core/pull/8987 When we merged 3.9.x up, we removed the source from test-suite-graal, but then added in logback.xml This PR removes the logback.xml which is no longer used --- test-suite-graal/src/test/resources/logback.xml | 10 ---------- 1 file changed, 10 deletions(-) delete mode 100644 test-suite-graal/src/test/resources/logback.xml diff --git a/test-suite-graal/src/test/resources/logback.xml b/test-suite-graal/src/test/resources/logback.xml deleted file mode 100644 index 8eb8c3a8170..00000000000 --- a/test-suite-graal/src/test/resources/logback.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n - - - - - - From 0c8c44f008db187fb55d470833d6dcf0d0f812b3 Mon Sep 17 00:00:00 2001 From: Graeme Rocher Date: Fri, 14 Apr 2023 13:08:05 +0200 Subject: [PATCH 706/743] Support for conditional routing using expressions (#9094) Adds a new `@RequestCondition` annotation which can make the route execution logic conditional. --- .../server/stack/FullHttpStackBenchmark.java | 1 - .../parser/ast/access/ElementMethodCall.java | 18 ++--- .../parser/ast/access/EnvironmentAccess.java | 2 +- .../ast/conditional/TernaryExpression.java | 4 +- .../parser/ast/literal/StringLiteral.java | 2 +- .../http/server/tck/tests/ExpressionTest.java | 70 +++++++++++++++++++ .../http/annotation/CustomHttpMethod.java | 8 +-- .../io/micronaut/http/annotation/Delete.java | 4 +- .../io/micronaut/http/annotation/Get.java | 4 +- .../io/micronaut/http/annotation/Head.java | 4 +- .../io/micronaut/http/annotation/Options.java | 5 +- .../io/micronaut/http/annotation/Patch.java | 4 +- .../io/micronaut/http/annotation/Post.java | 4 +- .../io/micronaut/http/annotation/Put.java | 4 +- .../http/annotation/RouteCondition.java | 51 ++++++++++++++ .../io/micronaut/http/annotation/Trace.java | 4 +- .../expression/RequestConditionContext.java | 57 +++++++++++++++ .../micronaut/http/filter/FilterRunner.java | 2 +- .../ContextMethodCallsExpressionsSpec.groovy | 2 + ...ontextPropertyAccessExpressionsSpec.groovy | 32 +++++++++ .../web/router/DefaultRouteBuilder.java | 9 +++ .../guide/config/evaluatedExpressions.adoc | 4 +- 22 files changed, 261 insertions(+), 34 deletions(-) create mode 100644 http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/ExpressionTest.java create mode 100644 http/src/main/java/io/micronaut/http/annotation/RouteCondition.java create mode 100644 http/src/main/java/io/micronaut/http/expression/RequestConditionContext.java diff --git a/benchmarks/src/jmh/java/io/micronaut/http/server/stack/FullHttpStackBenchmark.java b/benchmarks/src/jmh/java/io/micronaut/http/server/stack/FullHttpStackBenchmark.java index 4914ea83f4e..48c723611db 100644 --- a/benchmarks/src/jmh/java/io/micronaut/http/server/stack/FullHttpStackBenchmark.java +++ b/benchmarks/src/jmh/java/io/micronaut/http/server/stack/FullHttpStackBenchmark.java @@ -68,7 +68,6 @@ public static void main(String[] args) throws Exception { .measurementIterations(30) .mode(Mode.AverageTime) .timeUnit(TimeUnit.NANOSECONDS) - .addProfiler(AsyncProfiler.class, "libPath=/home/yawkat/bin/async-profiler-2.9-linux-x64/build/libasyncProfiler.so;output=flamegraph") .forks(1) .jvmArgsAppend("-Djmh.executor=CUSTOM", "-Djmh.executor.class=" + JmhFastThreadLocalExecutor.class.getName()) .build(); diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/access/ElementMethodCall.java b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/access/ElementMethodCall.java index ba5dc064f22..93f0a3fecd0 100644 --- a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/access/ElementMethodCall.java +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/access/ElementMethodCall.java @@ -118,25 +118,27 @@ protected void generateBytecode(ExpressionVisitorContext ctx) { @Override protected CandidateMethod resolveUsedMethod(ExpressionVisitorContext ctx) { List argumentTypes = resolveArgumentTypes(ctx); - Type calleeType = callee.resolveType(ctx); + ClassElement classElement = callee.resolveClassElement(ctx); + + if (isNullSafe() && classElement.isAssignable(Optional.class)) { + // safe navigate optional + classElement = classElement.getFirstTypeArgument().orElse(classElement); + } + ElementQuery methodQuery = buildMethodQuery(); - List candidateMethods = - ctx.visitorContext() - .getClassElement(calleeType.getClassName()) - .stream() - .flatMap(element -> element.getEnclosedElements(methodQuery).stream()) + List candidateMethods = classElement.getEnclosedElements(methodQuery).stream() .map(method -> toCandidateMethod(ctx, method, argumentTypes)) .filter(method -> method.isMatching(ctx.visitorContext())) .toList(); if (candidateMethods.isEmpty()) { throw new ExpressionCompilationException( - "No method [ " + name + stringifyArguments(ctx) + " ] available in class " + calleeType); + "No method [ " + name + stringifyArguments(ctx) + " ] available in class " + classElement.getName()); } else if (candidateMethods.size() > 1) { throw new ExpressionCompilationException( "Ambiguous method call. Found " + candidateMethods.size() + - " matching methods: " + candidateMethods + " in class " + calleeType); + " matching methods: " + candidateMethods + " in class " + classElement.getName()); } return candidateMethods.iterator().next(); diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/access/EnvironmentAccess.java b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/access/EnvironmentAccess.java index ce9cbd48607..8d5ba39e3ee 100644 --- a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/access/EnvironmentAccess.java +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/access/EnvironmentAccess.java @@ -60,7 +60,7 @@ protected void generateBytecode(ExpressionVisitorContext ctx) { @Override protected ClassElement doResolveClassElement(ExpressionVisitorContext ctx) { resolveType(ctx); - return STRING_ELEMENT; + return ctx.visitorContext().getClassElement(String.class).orElse(STRING_ELEMENT); } @Override diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/conditional/TernaryExpression.java b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/conditional/TernaryExpression.java index f43bbdc4766..630f99a5857 100644 --- a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/conditional/TernaryExpression.java +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/conditional/TernaryExpression.java @@ -116,7 +116,9 @@ public void generateBytecode(ExpressionVisitorContext ctx) { @Override protected ClassElement doResolveClassElement(ExpressionVisitorContext ctx) { - return ClassElement.of(doResolveType(ctx).getClassName()); + String className = doResolveType(ctx).getClassName(); + return ctx.visitorContext().getClassElement(className) + .orElse(ClassElement.of(className)); } /** diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/literal/StringLiteral.java b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/literal/StringLiteral.java index 7c6cc6ac57b..19318eaa308 100644 --- a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/literal/StringLiteral.java +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/literal/StringLiteral.java @@ -50,7 +50,7 @@ public void generateBytecode(ExpressionVisitorContext ctx) { @Override protected ClassElement doResolveClassElement(ExpressionVisitorContext ctx) { - return STRING_ELEMENT; + return ctx.visitorContext().getClassElement(String.class).orElse(STRING_ELEMENT); } @Override diff --git a/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/ExpressionTest.java b/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/ExpressionTest.java new file mode 100644 index 00000000000..55f45928fe6 --- /dev/null +++ b/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/ExpressionTest.java @@ -0,0 +1,70 @@ +/* + * Copyright 2017-2023 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.http.server.tck.tests; + +import io.micronaut.context.annotation.Requires; +import io.micronaut.http.HttpHeaders; +import io.micronaut.http.HttpRequest; +import io.micronaut.http.HttpStatus; +import io.micronaut.http.MediaType; +import io.micronaut.http.annotation.Controller; +import io.micronaut.http.annotation.Get; +import io.micronaut.http.annotation.RouteCondition; +import io.micronaut.http.tck.AssertionUtils; +import io.micronaut.http.tck.HttpResponseAssertion; +import org.junit.jupiter.api.Test; + +import java.io.IOException; + +import static io.micronaut.http.tck.TestScenario.asserts; + +@SuppressWarnings({ + "java:S5960", // We're allowed assertions, as these are used in tests only + "checkstyle:MissingJavadocType", + "checkstyle:DesignForExtension" +}) +public class ExpressionTest { + public static final String SPEC_NAME = "ExpressionTest"; + + @Test + void testConditionalGetRequest() throws IOException { + asserts(SPEC_NAME, + HttpRequest.GET("/expr/test"), + (server, request) -> AssertionUtils.assertThrows(server, request, + HttpResponseAssertion.builder() + .status(HttpStatus.NOT_FOUND) + .build())); + + asserts(SPEC_NAME, + HttpRequest.GET("/expr/test") + .header(HttpHeaders.AUTHORIZATION, "foo"), + (server, request) -> AssertionUtils.assertDoesNotThrow(server, request, + HttpResponseAssertion.builder() + .status(HttpStatus.OK) + .body("ok") + .build())); + } + + @Controller("/expr") + @Requires(property = "spec.name", value = SPEC_NAME) + static class ExpressionController { + @Get(value = "/test") + @RouteCondition("#{request.headers.getFirst('Authorization')?.contains('foo')}") + String testGet() { + return "ok"; + } + } +} diff --git a/http/src/main/java/io/micronaut/http/annotation/CustomHttpMethod.java b/http/src/main/java/io/micronaut/http/annotation/CustomHttpMethod.java index 8e1971aecf2..4b69110ff07 100644 --- a/http/src/main/java/io/micronaut/http/annotation/CustomHttpMethod.java +++ b/http/src/main/java/io/micronaut/http/annotation/CustomHttpMethod.java @@ -15,21 +15,21 @@ */ package io.micronaut.http.annotation; +import io.micronaut.context.annotation.AliasFor; +import io.micronaut.core.async.annotation.SingleResult; + import java.lang.annotation.Documented; import java.lang.annotation.ElementType; import java.lang.annotation.Inherited; import java.lang.annotation.Retention; import java.lang.annotation.Target; -import io.micronaut.context.annotation.AliasFor; -import io.micronaut.core.async.annotation.SingleResult; - import static java.lang.annotation.RetentionPolicy.RUNTIME; /** * This annotation is designed for non-standard http methods, that you * can provide by specifying the required {@link #method()} property. - * + * * @author spirit-1984 * @since 1.3.0 */ diff --git a/http/src/main/java/io/micronaut/http/annotation/Delete.java b/http/src/main/java/io/micronaut/http/annotation/Delete.java index 655407cd1e0..80d334b8ea8 100644 --- a/http/src/main/java/io/micronaut/http/annotation/Delete.java +++ b/http/src/main/java/io/micronaut/http/annotation/Delete.java @@ -15,8 +15,6 @@ */ package io.micronaut.http.annotation; -import static java.lang.annotation.RetentionPolicy.RUNTIME; - import io.micronaut.context.annotation.AliasFor; import io.micronaut.core.async.annotation.SingleResult; @@ -26,6 +24,8 @@ import java.lang.annotation.Retention; import java.lang.annotation.Target; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + /** * Annotation that can be applied to method to signify the method receives a {@link io.micronaut.http.HttpMethod#DELETE}. * diff --git a/http/src/main/java/io/micronaut/http/annotation/Get.java b/http/src/main/java/io/micronaut/http/annotation/Get.java index 69ffda905d5..3e0f2e1b792 100644 --- a/http/src/main/java/io/micronaut/http/annotation/Get.java +++ b/http/src/main/java/io/micronaut/http/annotation/Get.java @@ -15,8 +15,6 @@ */ package io.micronaut.http.annotation; -import static java.lang.annotation.RetentionPolicy.RUNTIME; - import io.micronaut.context.annotation.AliasFor; import io.micronaut.core.async.annotation.SingleResult; @@ -26,6 +24,8 @@ import java.lang.annotation.Retention; import java.lang.annotation.Target; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + /** * Annotation that can be applied to method to signify the method receives a {@link io.micronaut.http.HttpMethod#GET}. * diff --git a/http/src/main/java/io/micronaut/http/annotation/Head.java b/http/src/main/java/io/micronaut/http/annotation/Head.java index 972ea6718c6..31a4d1bacc5 100644 --- a/http/src/main/java/io/micronaut/http/annotation/Head.java +++ b/http/src/main/java/io/micronaut/http/annotation/Head.java @@ -15,8 +15,6 @@ */ package io.micronaut.http.annotation; -import static java.lang.annotation.RetentionPolicy.RUNTIME; - import io.micronaut.context.annotation.AliasFor; import java.lang.annotation.Documented; @@ -25,6 +23,8 @@ import java.lang.annotation.Retention; import java.lang.annotation.Target; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + /** * Annotation that can be applied to method to signify the method receives a {@link io.micronaut.http.HttpMethod#HEAD}. * diff --git a/http/src/main/java/io/micronaut/http/annotation/Options.java b/http/src/main/java/io/micronaut/http/annotation/Options.java index 9db08f6a5fa..7a4408b8960 100644 --- a/http/src/main/java/io/micronaut/http/annotation/Options.java +++ b/http/src/main/java/io/micronaut/http/annotation/Options.java @@ -15,8 +15,6 @@ */ package io.micronaut.http.annotation; -import static java.lang.annotation.RetentionPolicy.RUNTIME; - import io.micronaut.context.annotation.AliasFor; import java.lang.annotation.Documented; @@ -25,6 +23,8 @@ import java.lang.annotation.Retention; import java.lang.annotation.Target; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + /** * Annotation that can be applied to method to signify the method receives a * {@link io.micronaut.http.HttpMethod#OPTIONS}. @@ -67,4 +67,5 @@ */ @AliasFor(annotation = Consumes.class, member = "value") String[] consumes() default {}; + } diff --git a/http/src/main/java/io/micronaut/http/annotation/Patch.java b/http/src/main/java/io/micronaut/http/annotation/Patch.java index d79d98dd857..9d193096da9 100644 --- a/http/src/main/java/io/micronaut/http/annotation/Patch.java +++ b/http/src/main/java/io/micronaut/http/annotation/Patch.java @@ -15,8 +15,6 @@ */ package io.micronaut.http.annotation; -import static java.lang.annotation.RetentionPolicy.RUNTIME; - import io.micronaut.context.annotation.AliasFor; import io.micronaut.core.async.annotation.SingleResult; @@ -26,6 +24,8 @@ import java.lang.annotation.Retention; import java.lang.annotation.Target; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + /** * Annotation that can be applied to method to signify the method receives a {@link io.micronaut.http.HttpMethod#PATCH}. * diff --git a/http/src/main/java/io/micronaut/http/annotation/Post.java b/http/src/main/java/io/micronaut/http/annotation/Post.java index 26ff5390a93..ce55880c631 100644 --- a/http/src/main/java/io/micronaut/http/annotation/Post.java +++ b/http/src/main/java/io/micronaut/http/annotation/Post.java @@ -15,8 +15,6 @@ */ package io.micronaut.http.annotation; -import static java.lang.annotation.RetentionPolicy.RUNTIME; - import io.micronaut.context.annotation.AliasFor; import io.micronaut.core.async.annotation.SingleResult; @@ -26,6 +24,8 @@ import java.lang.annotation.Retention; import java.lang.annotation.Target; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + /** * Annotation that can be applied to method to signify the method receives a {@link io.micronaut.http.HttpMethod#POST}. * diff --git a/http/src/main/java/io/micronaut/http/annotation/Put.java b/http/src/main/java/io/micronaut/http/annotation/Put.java index 86d856a15b0..b3338ec0f11 100644 --- a/http/src/main/java/io/micronaut/http/annotation/Put.java +++ b/http/src/main/java/io/micronaut/http/annotation/Put.java @@ -15,8 +15,6 @@ */ package io.micronaut.http.annotation; -import static java.lang.annotation.RetentionPolicy.RUNTIME; - import io.micronaut.context.annotation.AliasFor; import io.micronaut.core.async.annotation.SingleResult; @@ -26,6 +24,8 @@ import java.lang.annotation.Retention; import java.lang.annotation.Target; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + /** * Annotation that can be applied to method to signify the method receives a {@link io.micronaut.http.HttpMethod#PUT}. * diff --git a/http/src/main/java/io/micronaut/http/annotation/RouteCondition.java b/http/src/main/java/io/micronaut/http/annotation/RouteCondition.java new file mode 100644 index 00000000000..205afd0a89c --- /dev/null +++ b/http/src/main/java/io/micronaut/http/annotation/RouteCondition.java @@ -0,0 +1,51 @@ +/* + * Copyright 2017-2023 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.http.annotation; + +import io.micronaut.context.annotation.AnnotationExpressionContext; +import io.micronaut.core.annotation.Experimental; +import io.micronaut.http.expression.RequestConditionContext; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Allows defining a condition for this route to match using an expression. + * + *

Within the scope of the expression a {@code request} variable is available that references the {@link io.micronaut.http.HttpRequest}.

+ * + *

When added to a method the condition will be evaluated during route matching and if the condition + * does not evaluate to {@code true} the route will not be matched resulting in a {@link io.micronaut.http.HttpStatus#NOT_FOUND} response.

+ * + *

Note that this annotation only applies to the server and is ignored when placed on declarative HTTP client routes.

+ * + * @see io.micronaut.http.expression.RequestConditionContext + * @since 4.0.0 + */ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@AnnotationExpressionContext(RequestConditionContext.class) +@Experimental +public @interface RouteCondition { + /** + * An expression that evalutes to {@code true} or {@code false}. + * @return The expression + * @since 4.0.0 + */ + String value(); +} diff --git a/http/src/main/java/io/micronaut/http/annotation/Trace.java b/http/src/main/java/io/micronaut/http/annotation/Trace.java index a2296c2ea63..f173be76226 100644 --- a/http/src/main/java/io/micronaut/http/annotation/Trace.java +++ b/http/src/main/java/io/micronaut/http/annotation/Trace.java @@ -15,8 +15,6 @@ */ package io.micronaut.http.annotation; -import static java.lang.annotation.RetentionPolicy.RUNTIME; - import io.micronaut.context.annotation.AliasFor; import java.lang.annotation.Documented; @@ -25,6 +23,8 @@ import java.lang.annotation.Retention; import java.lang.annotation.Target; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + /** * Annotation that can be applied to method to signify the method receives a {@link io.micronaut.http.HttpMethod#TRACE}. * diff --git a/http/src/main/java/io/micronaut/http/expression/RequestConditionContext.java b/http/src/main/java/io/micronaut/http/expression/RequestConditionContext.java new file mode 100644 index 00000000000..a725d471343 --- /dev/null +++ b/http/src/main/java/io/micronaut/http/expression/RequestConditionContext.java @@ -0,0 +1,57 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.http.expression; + +import io.micronaut.core.annotation.Experimental; +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.NonNull; +import io.micronaut.http.HttpRequest; +import io.micronaut.http.context.ServerRequestContext; +import jakarta.inject.Singleton; + +/** + * An expression evaluation context for use with HTTP annotations and the {@code condition} member. + * + *

This context allows access to the current request via a {@code request} object which is an instance of {@link HttpRequest}.

+ * + * @see HttpRequest + * @author graemerocher + * @since 4.0.0 + */ +@Singleton +@Experimental +public final class RequestConditionContext { + + /** + * Default constructor. + */ + @Internal + RequestConditionContext() { + } + + /** + * @return The request object. + */ + @SuppressWarnings("java:S1452") + public @NonNull HttpRequest getRequest() { + return currentRequest(); + } + + private static HttpRequest currentRequest() { + return ServerRequestContext.currentRequest() + .orElseThrow(() -> new IllegalStateException("No request present in evaluation context")); + } +} diff --git a/http/src/main/java/io/micronaut/http/filter/FilterRunner.java b/http/src/main/java/io/micronaut/http/filter/FilterRunner.java index c5a8e2b0435..3f627a3ff93 100644 --- a/http/src/main/java/io/micronaut/http/filter/FilterRunner.java +++ b/http/src/main/java/io/micronaut/http/filter/FilterRunner.java @@ -787,8 +787,8 @@ public ExecutionFlow processResult(Publisher> pub private static sealed class ReactiveContinuationImpl implements FilterContinuation>>, InternalFilterContinuation>> { - private final Function> downstream; protected FilterContext filterContext; + private final Function> downstream; private ReactiveContinuationImpl(Function> downstream, FilterContext filterContext) { diff --git a/inject-groovy/src/test/groovy/io/micronaut/expressions/ContextMethodCallsExpressionsSpec.groovy b/inject-groovy/src/test/groovy/io/micronaut/expressions/ContextMethodCallsExpressionsSpec.groovy index 45183ca9862..de8f07bd533 100644 --- a/inject-groovy/src/test/groovy/io/micronaut/expressions/ContextMethodCallsExpressionsSpec.groovy +++ b/inject-groovy/src/test/groovy/io/micronaut/expressions/ContextMethodCallsExpressionsSpec.groovy @@ -1,9 +1,11 @@ package io.micronaut.expressions import io.micronaut.ast.transform.test.AbstractEvaluatedExpressionsSpec +import spock.lang.Ignore class ContextMethodCallsExpressionsSpec extends AbstractEvaluatedExpressionsSpec{ + @Ignore("Already tested in Java and fails intermittently due to a Groovy classloading bug") void "test context method calls"() { given: Object expr1 = evaluateAgainstContext("#{ #getIntValue() }", diff --git a/inject-java/src/test/groovy/io/micronaut/expressions/ContextPropertyAccessExpressionsSpec.groovy b/inject-java/src/test/groovy/io/micronaut/expressions/ContextPropertyAccessExpressionsSpec.groovy index bfde85a583c..fe621a66901 100644 --- a/inject-java/src/test/groovy/io/micronaut/expressions/ContextPropertyAccessExpressionsSpec.groovy +++ b/inject-java/src/test/groovy/io/micronaut/expressions/ContextPropertyAccessExpressionsSpec.groovy @@ -236,6 +236,38 @@ class ContextPropertyAccessExpressionsSpec extends AbstractEvaluatedExpressionsS expr == "test" } + void "test multi-level context property access safe navigation with optionals - method call"() { + given: + Object expr = evaluateAgainstContext("#{ foo?.bar?.getName() }", + """ + import java.util.Optional; + + @jakarta.inject.Singleton + class Context { + public Optional getFoo() { + return Optional.of(new Foo()); + } + } + + class Foo { + private Bar bar = new Bar(); + public Optional getBar() { + return Optional.ofNullable(bar); + } + } + + class Bar { + private String name = "test"; + public String getName() { + return name; + } + } + """) + + expect: + expr == "test" + } + void "test multi-level context property access non-safe navigation"() { when: diff --git a/router/src/main/java/io/micronaut/web/router/DefaultRouteBuilder.java b/router/src/main/java/io/micronaut/web/router/DefaultRouteBuilder.java index c375de9d422..801db858f65 100644 --- a/router/src/main/java/io/micronaut/web/router/DefaultRouteBuilder.java +++ b/router/src/main/java/io/micronaut/web/router/DefaultRouteBuilder.java @@ -21,6 +21,7 @@ import io.micronaut.context.env.Environment; import io.micronaut.core.annotation.AnnotationMetadata; import io.micronaut.core.annotation.AnnotationMetadataResolver; +import io.micronaut.core.annotation.AnnotationValue; import io.micronaut.core.annotation.Nullable; import io.micronaut.core.convert.ConversionService; import io.micronaut.core.type.Argument; @@ -30,6 +31,7 @@ import io.micronaut.http.HttpStatus; import io.micronaut.http.MediaType; import io.micronaut.http.annotation.Body; +import io.micronaut.http.annotation.RouteCondition; import io.micronaut.http.filter.GenericHttpFilter; import io.micronaut.http.filter.HttpFilter; import io.micronaut.http.uri.UriMatchTemplate; @@ -37,6 +39,7 @@ import io.micronaut.inject.ExecutableMethod; import io.micronaut.inject.MethodExecutionHandle; import io.micronaut.inject.MethodReference; +import io.micronaut.inject.annotation.EvaluatedAnnotationValue; import io.micronaut.scheduling.executor.ExecutorSelector; import io.micronaut.scheduling.executor.ThreadSelection; import io.micronaut.web.router.exceptions.RoutingException; @@ -872,6 +875,12 @@ final class DefaultUriRoute extends AbstractRoute implements UriRoute { this.uriMatchTemplate = uriTemplate; this.httpMethodName = httpMethodName; this.executorSelector = new RouteExecutorSelector(); + if (targetMethod.isPresent(RouteCondition.class, AnnotationMetadata.VALUE_MEMBER)) { + AnnotationValue annotation = targetMethod.getAnnotation(RouteCondition.class); + if (annotation instanceof EvaluatedAnnotationValue) { + where(request -> annotation.booleanValue().orElse(false)); + } + } } @Override diff --git a/src/main/docs/guide/config/evaluatedExpressions.adoc b/src/main/docs/guide/config/evaluatedExpressions.adoc index 4bcd1fe46ba..e96234da39f 100644 --- a/src/main/docs/guide/config/evaluatedExpressions.adoc +++ b/src/main/docs/guide/config/evaluatedExpressions.adoc @@ -41,7 +41,7 @@ set of available features. Even though the complexity of expression is only limi constructs, it is in general not recommended to place complex logic inside an expression as there are usually better ways to achieve the same result. -== Example Use Case +== Using Expressions in Micronaut framework Expressions can be used anywhere throughout the Micronaut framework and associated modules, but as an example, you can use them to implement simple scheduled job control, for example: @@ -50,6 +50,8 @@ snippet::io.micronaut.docs.expressions.ExampleJob[title="Job Control with Expres <1> Here the `condition` member of the ann:scheduling.annotation.Scheduled[] annotation is used to only execute the job if a pre-condition is met. <2> The `condition` invokes a method of a parameter which is another bean called `ExampleJobControl` which can be used to pause and resume execution of the job as desired. +TIP: You can also use expressions to perform conditional routing using the ann:http.annotation.RouteCondition[] annotation. + == Evaluated Expression Language Reference The Evaluated Expressions syntax supports the following functionality: From ca3eb9a33b2ad72c9c0783d9eb29ea5c27c4fec9 Mon Sep 17 00:00:00 2001 From: micronaut-build Date: Fri, 14 Apr 2023 12:45:25 +0000 Subject: [PATCH 707/743] [skip ci] Release v4.0.0-M2 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index dbc16a1f447..0fe44d25cc3 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -projectVersion=4.0.0-SNAPSHOT +projectVersion=4.0.0-M2 projectGroupId=io.micronaut projectDesc=Natively Cloud Native title=Micronaut Framework From 9820df30ec9d0ff3940fbee054584d2bd7bd89c6 Mon Sep 17 00:00:00 2001 From: micronaut-build Date: Fri, 14 Apr 2023 13:00:58 +0000 Subject: [PATCH 708/743] Back to 4.0.0-SNAPSHOT --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 0fe44d25cc3..dbc16a1f447 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -projectVersion=4.0.0-M2 +projectVersion=4.0.0-SNAPSHOT projectGroupId=io.micronaut projectDesc=Natively Cloud Native title=Micronaut Framework From cbcc42f79a2695233298a1d770041996640249f6 Mon Sep 17 00:00:00 2001 From: micronaut-build <65172877+micronaut-build@users.noreply.github.com> Date: Fri, 14 Apr 2023 18:21:18 +0200 Subject: [PATCH 709/743] Bump micronaut-liquibase to 5.7.2 (#9109) --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index f818771acdf..92248113194 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -93,7 +93,7 @@ managed-micronaut-kotlin = "3.2.2" managed-micronaut-kubernetes = "4.0.0" managed-micronaut-micrometer = "4.8.3" managed-micronaut-microstream = "1.3.0" -managed-micronaut-liquibase = "5.7.1" +managed-micronaut-liquibase = "5.7.2" managed-micronaut-mongo = "4.6.0" managed-micronaut-mqtt = "2.3.0" managed-micronaut-multitenancy = "4.2.0" From 1cdac4a84a80c452cd8c9db2407adbca4c3c6485 Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Mon, 17 Apr 2023 10:19:09 +0200 Subject: [PATCH 710/743] bug: SslConfiguration::setProtocol (#9118) * bug: SslConfiguration::setProtocol * test: Deflake the BinaryWebSocketSpec (#8725) --------- Co-authored-by: Tim Yates --- .../websocket/BinaryChatClientWebSocket.java | 4 ++-- .../websocket/BinaryChatServerWebSocket.java | 6 ++--- .../websocket/BinaryWebSocketSpec.groovy | 7 ------ .../micronaut/http/ssl/SslConfiguration.java | 2 +- .../http/ssl/SslConfigurationSpec.groovy | 23 +++++++++++++++++++ 5 files changed, 29 insertions(+), 13 deletions(-) create mode 100644 http/src/test/groovy/io/micronaut/http/ssl/SslConfigurationSpec.groovy diff --git a/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/websocket/BinaryChatClientWebSocket.java b/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/websocket/BinaryChatClientWebSocket.java index 533483b0142..4d2ecf914dc 100644 --- a/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/websocket/BinaryChatClientWebSocket.java +++ b/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/websocket/BinaryChatClientWebSocket.java @@ -46,7 +46,7 @@ public void onOpen(String topic, String username, WebSocketSession session) { this.topic = topic; this.username = username; this.session = session; - System.out.println("Client session opened for username = " + username); + System.out.println("Client session " + session.getId() + " opened for username = " + username); } public String getTopic() { @@ -72,7 +72,7 @@ public WebSocketSession getSession() { @OnMessage public void onMessage( byte[] message) { - System.out.println("Client received message = " + new String(message)); + System.out.println("Client " + username + " received message = " + new String(message)); replies.add(new String(message)); } diff --git a/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/websocket/BinaryChatServerWebSocket.java b/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/websocket/BinaryChatServerWebSocket.java index 49b581b7d64..141e417db33 100644 --- a/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/websocket/BinaryChatServerWebSocket.java +++ b/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/websocket/BinaryChatServerWebSocket.java @@ -36,7 +36,7 @@ public void onOpen(String topic, String username, WebSocketSession session) { if(isValid(topic, session, openSession)) { String msg = "[" + username + "] Joined!"; System.out.println("Server sending msg = " + msg); - openSession.sendSync(msg.getBytes()); + openSession.sendAsync(msg.getBytes()); } } } @@ -55,7 +55,7 @@ public void onMessage( if(isValid(topic, session, openSession)) { String msg = "[" + username + "] " + new String(message); System.out.println("Server sending msg = " + msg); - openSession.sendSync(msg.getBytes()); + openSession.sendAsync(msg.getBytes()); } } } @@ -72,7 +72,7 @@ public void onClose( if(isValid(topic, session, openSession)) { String msg = "[" + username + "] Disconnected!"; System.out.println("Server sending msg = " + msg); - openSession.sendSync(msg.getBytes()); + openSession.sendAsync(msg.getBytes()); } } } diff --git a/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/websocket/BinaryWebSocketSpec.groovy b/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/websocket/BinaryWebSocketSpec.groovy index a4bd3a39ca2..8c935de3c18 100644 --- a/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/websocket/BinaryWebSocketSpec.groovy +++ b/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/websocket/BinaryWebSocketSpec.groovy @@ -32,17 +32,14 @@ import jakarta.inject.Singleton import reactor.core.publisher.Flux import reactor.core.publisher.Mono import spock.lang.Issue -import spock.lang.Retry import spock.lang.Specification import spock.util.concurrent.PollingConditions import java.nio.ByteBuffer import java.nio.charset.StandardCharsets -@Retry class BinaryWebSocketSpec extends Specification { - @Retry void "test binary websocket exchange"() { given: EmbeddedServer embeddedServer = ApplicationContext.builder('micronaut.server.netty.log-level':'TRACE').run(EmbeddedServer) @@ -69,7 +66,6 @@ class BinaryWebSocketSpec extends Specification { fred.replies.size() == 1 } - when:"A message is sent" fred.send("Hello bob!".bytes) @@ -86,7 +82,6 @@ class BinaryWebSocketSpec extends Specification { then: conditions.eventually { - fred.replies.contains("[bob] Hi fred. How are things?") fred.replies.size() == 2 bob.replies.contains("[fred] Hello bob!") @@ -99,8 +94,6 @@ class BinaryWebSocketSpec extends Specification { when: bob.close() - sleep(1000) - then: conditions.eventually { diff --git a/http/src/main/java/io/micronaut/http/ssl/SslConfiguration.java b/http/src/main/java/io/micronaut/http/ssl/SslConfiguration.java index 08a40c89bf8..bf9327f6774 100644 --- a/http/src/main/java/io/micronaut/http/ssl/SslConfiguration.java +++ b/http/src/main/java/io/micronaut/http/ssl/SslConfiguration.java @@ -252,7 +252,7 @@ public void setProtocols(String[] protocols) { * @param protocol The protocol */ public void setProtocol(String protocol) { - if (!StringUtils.isNotEmpty(protocol)) { + if (StringUtils.isNotEmpty(protocol)) { this.protocol = protocol; } } diff --git a/http/src/test/groovy/io/micronaut/http/ssl/SslConfigurationSpec.groovy b/http/src/test/groovy/io/micronaut/http/ssl/SslConfigurationSpec.groovy new file mode 100644 index 00000000000..8312ff55eaf --- /dev/null +++ b/http/src/test/groovy/io/micronaut/http/ssl/SslConfigurationSpec.groovy @@ -0,0 +1,23 @@ +package io.micronaut.http.ssl + +import spock.lang.Specification + +class SslConfigurationSpec extends Specification { + + void "setProtocol should override protocol"() { + given: + SslConfiguration configuration = new SslConfiguration() + + expect: + configuration.protocol.isPresent() + "TLS" == configuration.protocol.get() + + when: + configuration.protocol = "foo" + + then: + configuration.protocol.isPresent() + "foo" == configuration.protocol.get() + } + +} From 8fae07ce259c72216fde227e3c0ee1a1dad5876e Mon Sep 17 00:00:00 2001 From: Andriy Dmytruk <80816836+andriy-dmytruk@users.noreply.github.com> Date: Mon, 17 Apr 2023 05:57:03 -0400 Subject: [PATCH 711/743] Add targetPackage property to the @Introspected annotation (#9105) --------- Co-authored-by: Sergio del Amo --- .../core/annotation/Introspected.java | 7 ++ .../beans/BeanIntrospectionSpec.groovy | 66 +++++++++++++++++++ .../visitor/BeanIntrospectionWriter.java | 22 +++---- .../IntrospectedTypeElementVisitor.java | 33 +++++++--- 4 files changed, 109 insertions(+), 19 deletions(-) diff --git a/core/src/main/java/io/micronaut/core/annotation/Introspected.java b/core/src/main/java/io/micronaut/core/annotation/Introspected.java index c3b59a74da7..492ea7c3ef7 100644 --- a/core/src/main/java/io/micronaut/core/annotation/Introspected.java +++ b/core/src/main/java/io/micronaut/core/annotation/Introspected.java @@ -156,6 +156,13 @@ */ String withPrefix() default "with"; + /** + * @return The package to write introspections to. By default, uses the class package. + * @since 3.9.0 + */ + @Experimental + String targetPackage() default ""; + /** * Allow pre-computed indexes for property lookups based on an annotation and a member. * diff --git a/inject-java-test/src/test/groovy/io/micronaut/inject/visitor/beans/BeanIntrospectionSpec.groovy b/inject-java-test/src/test/groovy/io/micronaut/inject/visitor/beans/BeanIntrospectionSpec.groovy index d6b90ee6214..b18e6be47ff 100644 --- a/inject-java-test/src/test/groovy/io/micronaut/inject/visitor/beans/BeanIntrospectionSpec.groovy +++ b/inject-java-test/src/test/groovy/io/micronaut/inject/visitor/beans/BeanIntrospectionSpec.groovy @@ -82,6 +82,41 @@ class Test extends Auditable { bean.updatedAt != null } + void "test introspection can written to different package"() { + when: + def classLoader = buildClassLoader("test.Test", ''' +package test; + +import io.micronaut.core.annotation.Introspected; + +@Introspected(targetPackage = "test.introspections") +public class Test { + private String name; + public Test(String name) { + this.name = name; + } + public String getName() { + return name; + } +} + +''') + def introspectionName = 'test.introspections.$Test$Introspection' + def introspection = classLoader.loadClass(introspectionName).newInstance() as BeanIntrospection + + then: + introspection != null + introspection.getProperty("name").isPresent() + + when: + def introspectionRefName = 'test.introspections.$Test$IntrospectionRef' + def introspectionRef = classLoader.loadClass(introspectionRefName).newInstance() as BeanIntrospectionReference + + then: + introspectionRef != null + introspectionRef.load() != null + } + void "test generics in arrays don't stack overflow"() { given: def introspection = buildBeanIntrospection('arraygenerics.Test', ''' @@ -904,6 +939,37 @@ class Test {} applicationContext.close() } + void "test create bean introspection for external class with custom package"() { + given: + def classLoader = buildClassLoader('test.Test', ''' +package test; + +import io.micronaut.core.annotation.Introspected; +import io.micronaut.inject.visitor.beans.OuterBean; + +@Introspected(classes=OuterBean.InnerBean.class, targetPackage="test.micronaut.intro") +class Test {} +''') + + when:"the reference is loaded" + def reference = classLoader.loadClass('test.micronaut.intro.$Test$IntrospectionRef0').newInstance() as BeanIntrospectionReference + + then:"the reference is valid" + notThrown(ClassNotFoundException) + reference.getBeanType() == OuterBean.InnerBean.class + reference.load() != null + + print(classLoader) + + when:"the introspection is loaded" + def introspectionName = 'test.micronaut.intro.$io_micronaut_inject_visitor_beans_OuterBean$InnerBean$Introspection' + def introspection = classLoader.loadClass(introspectionName).newInstance() as BeanIntrospection + + then:"the introspection is valid" + notThrown(ClassNotFoundException) + introspection.getProperty("name").isPresent() + } + void "test create bean introspection for interface"() { given: def context = buildContext('itfcetest.MyInterface',''' diff --git a/inject/src/main/java/io/micronaut/inject/beans/visitor/BeanIntrospectionWriter.java b/inject/src/main/java/io/micronaut/inject/beans/visitor/BeanIntrospectionWriter.java index 7e86c9baa36..f597f1d6640 100644 --- a/inject/src/main/java/io/micronaut/inject/beans/visitor/BeanIntrospectionWriter.java +++ b/inject/src/main/java/io/micronaut/inject/beans/visitor/BeanIntrospectionWriter.java @@ -125,15 +125,16 @@ final class BeanIntrospectionWriter extends AbstractAnnotationMetadataWriter { /** * Default constructor. * + * @param targetPackage The package to write introspection to * @param classElement The class element * @param beanAnnotationMetadata The bean annotation metadata */ - BeanIntrospectionWriter(ClassElement classElement, AnnotationMetadata beanAnnotationMetadata) { - super(computeReferenceName(classElement.getName()), classElement, beanAnnotationMetadata, true); + BeanIntrospectionWriter(String targetPackage, ClassElement classElement, AnnotationMetadata beanAnnotationMetadata) { + super(computeReferenceName(targetPackage, classElement.getName()), classElement, beanAnnotationMetadata, true); final String name = classElement.getName(); this.classElement = classElement; this.referenceWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS); - this.introspectionName = computeIntrospectionName(name); + this.introspectionName = computeShortIntrospectionName(targetPackage, name); this.introspectionType = getTypeReferenceForName(introspectionName); this.beanType = getTypeReferenceForName(name); this.dispatchWriter = new DispatchWriter(introspectionType, Type.getType(AbstractInitializableBeanIntrospection.class)); @@ -142,6 +143,7 @@ final class BeanIntrospectionWriter extends AbstractAnnotationMetadataWriter { /** * Constructor used to generate a reference for already compiled classes. * + * @param targetPackage The package to write introspection to * @param generatingType The originating type * @param index A unique index * @param originatingElement The originating element @@ -149,16 +151,17 @@ final class BeanIntrospectionWriter extends AbstractAnnotationMetadataWriter { * @param beanAnnotationMetadata The bean annotation metadata */ BeanIntrospectionWriter( + String targetPackage, String generatingType, int index, ClassElement originatingElement, ClassElement classElement, AnnotationMetadata beanAnnotationMetadata) { - super(computeReferenceName(generatingType) + index, originatingElement, beanAnnotationMetadata, true); + super(computeReferenceName(targetPackage, generatingType) + index, originatingElement, beanAnnotationMetadata, true); final String className = classElement.getName(); this.classElement = classElement; this.referenceWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS); - this.introspectionName = computeIntrospectionName(generatingType, className); + this.introspectionName = computeIntrospectionName(targetPackage, className); this.introspectionType = getTypeReferenceForName(introspectionName); this.beanType = getTypeReferenceForName(className); this.dispatchWriter = new DispatchWriter(introspectionType); @@ -996,22 +999,19 @@ private void pushAnnotationMetadata(ClassWriter classWriter, GeneratorAdapter st } @NonNull - private static String computeReferenceName(String className) { - String packageName = NameUtils.getPackageName(className); + private static String computeReferenceName(String packageName, String className) { final String shortName = NameUtils.getSimpleName(className); return packageName + ".$" + shortName + REFERENCE_SUFFIX; } @NonNull - private static String computeIntrospectionName(String className) { - String packageName = NameUtils.getPackageName(className); + private static String computeShortIntrospectionName(String packageName, String className) { final String shortName = NameUtils.getSimpleName(className); return packageName + ".$" + shortName + INTROSPECTION_SUFFIX; } @NonNull - private static String computeIntrospectionName(String generatingName, String className) { - final String packageName = NameUtils.getPackageName(generatingName); + private static String computeIntrospectionName(String packageName, String className) { return packageName + ".$" + className.replace('.', '_') + INTROSPECTION_SUFFIX; } diff --git a/inject/src/main/java/io/micronaut/inject/beans/visitor/IntrospectedTypeElementVisitor.java b/inject/src/main/java/io/micronaut/inject/beans/visitor/IntrospectedTypeElementVisitor.java index e474a9e52b0..d200fb8a991 100644 --- a/inject/src/main/java/io/micronaut/inject/beans/visitor/IntrospectedTypeElementVisitor.java +++ b/inject/src/main/java/io/micronaut/inject/beans/visitor/IntrospectedTypeElementVisitor.java @@ -67,6 +67,8 @@ public class IntrospectedTypeElementVisitor implements TypeElementVisitor writers = new LinkedHashMap<>(10); private List abstractIntrospections = new ArrayList<>(); private AbstractIntrospection currentAbstractIntrospection; @@ -165,6 +167,17 @@ private void introspectIfValidated(VisitorContext context, ClassElement declarin } } + private static Introspected.Visibility[] getVisibilities(Introspected.Visibility[] current, String targetPackage, ClassElement classElement) { + if (ArrayUtils.isEmpty(current)) { + if (classElement.getPackageName().equals(targetPackage)) { + return DEFAULT_VISIBILITY; + } else { + return PUBLIC_VISIBILITY; + } + } + return current; + } + private void processIntrospected(ClassElement element, VisitorContext context, AnnotationValue introspected) { final String[] packages = introspected.stringValues("packages"); final AnnotationClassValue[] classes = introspected.get("classes", AnnotationClassValue[].class, new AnnotationClassValue[0]); @@ -180,14 +193,16 @@ private void processIntrospected(ClassElement element, VisitorContext context, A Introspected.AccessKind[] accessKinds = introspected.enumValues("accessKind", Introspected.AccessKind.class); Introspected.Visibility[] visibilities = introspected.enumValues("visibility", Introspected.Visibility.class); + final String targetPackage = introspected.stringValue("targetPackage").orElse(element.getPackageName()); + if (ArrayUtils.isEmpty(accessKinds)) { accessKinds = DEFAULT_ACCESS_KIND; } - if (ArrayUtils.isEmpty(visibilities)) { - visibilities = DEFAULT_VISIBILITY; - } +// if (ArrayUtils.isEmpty(visibilities)) { +// visibilities = DEFAULT_VISIBILITY; +// } Introspected.AccessKind[] finalAccessKinds = accessKinds; - Introspected.Visibility[] finalVisibilities = visibilities; +// Introspected.Visibility[] finalVisibilities = visibilities; if (CollectionUtils.isEmpty(toIndex)) { indexedAnnotations = CollectionUtils.setOf( @@ -209,7 +224,6 @@ private void processIntrospected(ClassElement element, VisitorContext context, A for (AnnotationClassValue aClass : classes) { final Optional classElement = context.getClassElement(aClass.getName()); - classElement.ifPresent(ce -> { if (ce.isPublic() && !isIntrospected(context, ce)) { final AnnotationMetadata typeMetadata = ce.getAnnotationMetadata(); @@ -217,6 +231,7 @@ private void processIntrospected(ClassElement element, VisitorContext context, A ? element.getAnnotationMetadata() : new AnnotationMetadataHierarchy(element.getAnnotationMetadata(), typeMetadata); final BeanIntrospectionWriter writer = new BeanIntrospectionWriter( + targetPackage, element.getName(), index.getAndIncrement(), element, @@ -232,7 +247,7 @@ private void processIntrospected(ClassElement element, VisitorContext context, A indexedAnnotations, ce, writer, - finalVisibilities, + getVisibilities(visibilities, targetPackage, ce), finalAccessKinds ); } @@ -251,6 +266,7 @@ private void processIntrospected(ClassElement element, VisitorContext context, A continue; } final BeanIntrospectionWriter writer = new BeanIntrospectionWriter( + targetPackage, element.getName(), j++, element, @@ -266,7 +282,7 @@ private void processIntrospected(ClassElement element, VisitorContext context, A indexedAnnotations, classElement, writer, - finalVisibilities, + getVisibilities(visibilities, targetPackage, classElement), finalAccessKinds ); } @@ -275,6 +291,7 @@ private void processIntrospected(ClassElement element, VisitorContext context, A } else { final BeanIntrospectionWriter writer = new BeanIntrospectionWriter( + targetPackage, element, metadata ? element.getAnnotationMetadata() : null ); @@ -287,7 +304,7 @@ private void processIntrospected(ClassElement element, VisitorContext context, A indexedAnnotations, element, writer, - finalVisibilities, + getVisibilities(visibilities, targetPackage, element), finalAccessKinds ); } From f8bd12d3f01bddd6fb8911a85f76f8f022b7c99c Mon Sep 17 00:00:00 2001 From: Graeme Rocher Date: Mon, 17 Apr 2023 17:32:35 +0200 Subject: [PATCH 712/743] Allow access to 'this' in expressions / support introspections (#9123) * Allows access to `this` in expressions in certain contexts (currently only executable methods), fails compilation if used in an invalid context * Include expression processing in introspections for properties and methods. --- .../micronaut/aop/chain/InterceptorChain.java | 2 +- .../processor/ScheduledMethodProcessor.java | 2 +- .../micronaut/aop/writer/AopProxyWriter.java | 6 - .../DefaultExpressionCompilationContext.java | 26 +++- ...ltExpressionCompilationContextFactory.java | 11 +- .../context/ExpressionCompilationContext.java | 8 ++ .../ExpressionCompilationContextFactory.java | 6 +- ...xtensibleExpressionCompilationContext.java | 7 ++ ...gleEvaluatedEvaluatedExpressionParser.java | 50 ++------ .../parser/ast/access/ThisAccess.java | 58 +++++++++ .../expressions/parser/token/TokenType.java | 1 + .../expressions/parser/token/Tokenizer.java | 47 +------ .../visitor/BeanIntrospectionWriter.java | 29 ++++- .../IntrospectedTypeElementVisitor.java | 9 +- .../AbstractAnnotationMetadataWriter.java | 20 ++- .../writer/AbstractBeanDefinitionBuilder.java | 2 +- .../writer/BeanConfigurationWriter.java | 11 +- .../writer/BeanDefinitionReferenceWriter.java | 8 +- .../inject/writer/BeanDefinitionVisitor.java | 14 +-- .../inject/writer/BeanDefinitionWriter.java | 84 ++++--------- .../writer/EvaluatedExpressionProcessor.java | 119 ++++++++++++++++++ .../ExecutableMethodsDefinitionWriter.java | 35 +----- .../ExpressionEvaluationContext.java | 9 ++ .../GraalReflectionMetadataWriter.java | 6 +- .../reflect/GraalTypeElementVisitor.java | 3 +- .../AbstractEvaluatedExpressionsSpec.groovy | 6 +- .../ast/groovy/InjectTransform.groovy | 5 +- ...ontextPropertyAccessExpressionsSpec.groovy | 4 +- .../AbstractEvaluatedExpressionsSpec.groovy | 7 +- .../beans/BeanIntrospectionSpec.groovy | 43 +++++++ .../BeanDefinitionInjectProcessor.java | 2 +- .../PackageConfigurationInjectProcessor.java | 3 +- .../expressions/ThisExpressionSpec.groovy | 40 ++++++ .../beans/BeanDefinitionProcessor.kt | 2 +- ...nfigurableExpressionEvaluationContext.java | 18 ++- .../DefaultExpressionEvaluationContext.java | 38 ++++-- .../EvaluatedAnnotationMetadata.java | 10 +- .../annotation/EvaluatedAnnotationValue.java | 6 +- .../MappingAnnotationMetadataDelegate.java | 42 ++++++- ...bstractInitializableBeanIntrospection.java | 15 ++- .../guide/config/evaluatedExpressions.adoc | 2 +- .../docs/expressions/ExampleJob.groovy | 17 +-- .../docs/expressions/ExampleJobSpec.groovy | 5 +- .../micronaut/docs/expressions/ExampleJob.kt | 10 +- .../docs/expressions/ExampleJobTest.kt | 10 +- .../docs/expressions/ExampleJob.java | 21 ++-- .../docs/expressions/ExampleJobTest.java | 6 +- 47 files changed, 572 insertions(+), 313 deletions(-) create mode 100644 core-processor/src/main/java/io/micronaut/expressions/parser/ast/access/ThisAccess.java create mode 100644 core-processor/src/main/java/io/micronaut/inject/writer/EvaluatedExpressionProcessor.java create mode 100644 inject-java/src/test/groovy/io/micronaut/expressions/ThisExpressionSpec.groovy diff --git a/aop/src/main/java/io/micronaut/aop/chain/InterceptorChain.java b/aop/src/main/java/io/micronaut/aop/chain/InterceptorChain.java index 204bc7fc4dd..6a29ebfd8f8 100644 --- a/aop/src/main/java/io/micronaut/aop/chain/InterceptorChain.java +++ b/aop/src/main/java/io/micronaut/aop/chain/InterceptorChain.java @@ -70,7 +70,7 @@ public InterceptorChain(Interceptor[] interceptors, this.executionHandle = method; AnnotationMetadata metadata = executionHandle.getAnnotationMetadata(); if (originalParameters.length > 0 && metadata instanceof EvaluatedAnnotationMetadata eam) { - this.annotationMetadata = eam.withArguments(originalParameters); + this.annotationMetadata = eam.withArguments(target, originalParameters); } else { this.annotationMetadata = metadata; } diff --git a/context/src/main/java/io/micronaut/scheduling/processor/ScheduledMethodProcessor.java b/context/src/main/java/io/micronaut/scheduling/processor/ScheduledMethodProcessor.java index 94d664543be..33b39887bc9 100644 --- a/context/src/main/java/io/micronaut/scheduling/processor/ScheduledMethodProcessor.java +++ b/context/src/main/java/io/micronaut/scheduling/processor/ScheduledMethodProcessor.java @@ -126,7 +126,7 @@ public void process(BeanDefinition beanDefinition, ExecutableMethod met Object bean = beanContext.getBean(beanType, declaredQualifier); AnnotationValue finalAnnotationValue = scheduledAnnotation; if (finalAnnotationValue instanceof EvaluatedAnnotationValue evaluated) { - finalAnnotationValue = evaluated.withArguments(boundExecutable.getBoundArguments()); + finalAnnotationValue = evaluated.withArguments(bean, boundExecutable.getBoundArguments()); } boolean shouldRun = finalAnnotationValue.booleanValue(MEMBER_CONDITION).orElse(true); if (shouldRun) { diff --git a/core-processor/src/main/java/io/micronaut/aop/writer/AopProxyWriter.java b/core-processor/src/main/java/io/micronaut/aop/writer/AopProxyWriter.java index 9821128df9d..3a05c769281 100644 --- a/core-processor/src/main/java/io/micronaut/aop/writer/AopProxyWriter.java +++ b/core-processor/src/main/java/io/micronaut/aop/writer/AopProxyWriter.java @@ -41,7 +41,6 @@ import io.micronaut.core.util.ArrayUtils; import io.micronaut.core.util.Toggleable; import io.micronaut.core.value.OptionalValues; -import io.micronaut.expressions.context.ExpressionWithContext; import io.micronaut.inject.BeanDefinition; import io.micronaut.inject.ExecutableMethod; import io.micronaut.inject.ProxyBeanDefinition; @@ -1302,11 +1301,6 @@ public AnnotationMetadata getAnnotationMetadata() { return proxyBeanDefinitionWriter.getAnnotationMetadata(); } - @Override - public Set getEvaluatedExpressions() { - return proxyBeanDefinitionWriter.getEvaluatedExpressions(); - } - @Override public void visitConfigBuilderField(ClassElement type, String field, AnnotationMetadata annotationMetadata, ConfigurationMetadataBuilder metadataBuilder, boolean isInterface) { proxyBeanDefinitionWriter.visitConfigBuilderField(type, field, annotationMetadata, metadataBuilder, isInterface); diff --git a/core-processor/src/main/java/io/micronaut/expressions/context/DefaultExpressionCompilationContext.java b/core-processor/src/main/java/io/micronaut/expressions/context/DefaultExpressionCompilationContext.java index 36bc8c254e2..2db2efa3e34 100644 --- a/core-processor/src/main/java/io/micronaut/expressions/context/DefaultExpressionCompilationContext.java +++ b/core-processor/src/main/java/io/micronaut/expressions/context/DefaultExpressionCompilationContext.java @@ -19,6 +19,7 @@ import io.micronaut.core.naming.NameUtils; import io.micronaut.core.util.ArrayUtils; import io.micronaut.inject.ast.ClassElement; +import io.micronaut.inject.ast.ConstructorElement; import io.micronaut.inject.ast.MethodElement; import io.micronaut.inject.ast.ParameterElement; import io.micronaut.inject.ast.PropertyElement; @@ -47,19 +48,34 @@ public class DefaultExpressionCompilationContext implements ExtensibleExpression private final Collection classElements; private final MethodElement methodElement; + private final ClassElement thisType; + DefaultExpressionCompilationContext(ClassElement... classElements) { - this(null, classElements); + this(null, null, classElements); } - private DefaultExpressionCompilationContext(MethodElement methodElement, + private DefaultExpressionCompilationContext(ClassElement thisType, + MethodElement methodElement, ClassElement... classElements) { + this.thisType = thisType; this.methodElement = methodElement; this.classElements = Arrays.asList(classElements); } + @Override + public ExtensibleExpressionCompilationContext withThis(ClassElement classElement) { + return new DefaultExpressionCompilationContext( + classElement, + methodElement, + classElements.toArray(ClassElement[]::new) + ); + } + @Override public DefaultExpressionCompilationContext extendWith(MethodElement methodElement) { + ClassElement resolvedThis = methodElement.isStatic() || methodElement instanceof ConstructorElement ? null : methodElement.getOwningType(); return new DefaultExpressionCompilationContext( + resolvedThis, methodElement, classElements.toArray(ClassElement[]::new) ); @@ -68,11 +84,17 @@ public DefaultExpressionCompilationContext extendWith(MethodElement methodElemen @Override public DefaultExpressionCompilationContext extendWith(ClassElement classElement) { return new DefaultExpressionCompilationContext( + this.thisType, this.methodElement, ArrayUtils.concat(classElements.toArray(ClassElement[]::new), classElement) ); } + @Override + public ClassElement findThis() { + return thisType; + } + @Override public List findMethods(String name) { return classElements.stream() diff --git a/core-processor/src/main/java/io/micronaut/expressions/context/DefaultExpressionCompilationContextFactory.java b/core-processor/src/main/java/io/micronaut/expressions/context/DefaultExpressionCompilationContextFactory.java index 0277f2c56a0..821753eca1e 100644 --- a/core-processor/src/main/java/io/micronaut/expressions/context/DefaultExpressionCompilationContextFactory.java +++ b/core-processor/src/main/java/io/micronaut/expressions/context/DefaultExpressionCompilationContextFactory.java @@ -58,14 +58,14 @@ private DefaultExpressionCompilationContext recreateContext() { @NonNull public ExpressionCompilationContext buildContextForMethod(@NonNull EvaluatedExpressionReference expression, @NonNull MethodElement methodElement) { - return buildForExpression(expression) + return buildForExpression(expression, null) .extendWith(methodElement); } @Override @NonNull - public ExpressionCompilationContext buildContext(EvaluatedExpressionReference expression) { - return buildForExpression(expression); + public ExpressionCompilationContext buildContext(EvaluatedExpressionReference expression, ClassElement thisElement) { + return buildForExpression(expression, thisElement); } @Override @@ -75,7 +75,7 @@ public ExpressionCompilationContextFactory registerContextClass(ClassElement con return this; } - private ExtensibleExpressionCompilationContext buildForExpression(EvaluatedExpressionReference expression) { + private ExtensibleExpressionCompilationContext buildForExpression(EvaluatedExpressionReference expression, ClassElement thisElement) { String annotationName = expression.annotationName(); String memberName = expression.annotationMember(); @@ -87,6 +87,9 @@ private ExtensibleExpressionCompilationContext buildForExpression(EvaluatedExpre evaluationContext = addAnnotationMemberEvaluationContext(evaluationContext, annotation, memberName); } + if (thisElement != null) { + return evaluationContext.withThis(thisElement); + } return evaluationContext; } diff --git a/core-processor/src/main/java/io/micronaut/expressions/context/ExpressionCompilationContext.java b/core-processor/src/main/java/io/micronaut/expressions/context/ExpressionCompilationContext.java index 47fd9e494b1..f552b674613 100644 --- a/core-processor/src/main/java/io/micronaut/expressions/context/ExpressionCompilationContext.java +++ b/core-processor/src/main/java/io/micronaut/expressions/context/ExpressionCompilationContext.java @@ -17,6 +17,8 @@ import io.micronaut.core.annotation.Internal; import io.micronaut.core.annotation.NonNull; +import io.micronaut.core.annotation.Nullable; +import io.micronaut.inject.ast.ClassElement; import io.micronaut.inject.ast.MethodElement; import io.micronaut.inject.ast.ParameterElement; import io.micronaut.inject.ast.PropertyElement; @@ -33,6 +35,12 @@ @Internal public interface ExpressionCompilationContext { + /** + * @return Find the type that represents this. + */ + @Nullable + ClassElement findThis(); + /** * Search methods in compilation context by name. * diff --git a/core-processor/src/main/java/io/micronaut/expressions/context/ExpressionCompilationContextFactory.java b/core-processor/src/main/java/io/micronaut/expressions/context/ExpressionCompilationContextFactory.java index 6c81cef084c..bf96010ebbe 100644 --- a/core-processor/src/main/java/io/micronaut/expressions/context/ExpressionCompilationContextFactory.java +++ b/core-processor/src/main/java/io/micronaut/expressions/context/ExpressionCompilationContextFactory.java @@ -17,6 +17,7 @@ import io.micronaut.core.annotation.Experimental; import io.micronaut.core.annotation.NonNull; +import io.micronaut.core.annotation.Nullable; import io.micronaut.core.expressions.EvaluatedExpressionReference; import io.micronaut.inject.ast.ClassElement; import io.micronaut.inject.ast.MethodElement; @@ -42,11 +43,12 @@ ExpressionCompilationContext buildContextForMethod(@NonNull EvaluatedExpressionR /** * Builds expression evaluation context for expression reference. * - * @param expression expression reference + * @param expression expression reference + * @param thisElement * @return evaluation context for method */ @NonNull - ExpressionCompilationContext buildContext(EvaluatedExpressionReference expression); + ExpressionCompilationContext buildContext(EvaluatedExpressionReference expression, @Nullable ClassElement thisElement); /** * Adds evaluated expression context class element to context loader diff --git a/core-processor/src/main/java/io/micronaut/expressions/context/ExtensibleExpressionCompilationContext.java b/core-processor/src/main/java/io/micronaut/expressions/context/ExtensibleExpressionCompilationContext.java index 59ce4a0ab90..0a3513d86a3 100644 --- a/core-processor/src/main/java/io/micronaut/expressions/context/ExtensibleExpressionCompilationContext.java +++ b/core-processor/src/main/java/io/micronaut/expressions/context/ExtensibleExpressionCompilationContext.java @@ -28,6 +28,13 @@ */ @Internal public interface ExtensibleExpressionCompilationContext extends ExpressionCompilationContext { + + /** + * @param classElement The type that represents this. + * @return extended context + */ + ExtensibleExpressionCompilationContext withThis(@NonNull ClassElement classElement); + /** * Extends compilation context with method element. Compilation context can only include * one method at the same time, so this method will return the context which will diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/SingleEvaluatedEvaluatedExpressionParser.java b/core-processor/src/main/java/io/micronaut/expressions/parser/SingleEvaluatedEvaluatedExpressionParser.java index 8761a9d8781..6aa012f0f8a 100644 --- a/core-processor/src/main/java/io/micronaut/expressions/parser/SingleEvaluatedEvaluatedExpressionParser.java +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/SingleEvaluatedEvaluatedExpressionParser.java @@ -24,6 +24,7 @@ import io.micronaut.expressions.parser.ast.access.EnvironmentAccess; import io.micronaut.expressions.parser.ast.access.SubscriptOperator; import io.micronaut.expressions.parser.ast.access.PropertyAccess; +import io.micronaut.expressions.parser.ast.access.ThisAccess; import io.micronaut.expressions.parser.ast.conditional.ElvisOperator; import io.micronaut.expressions.parser.ast.conditional.TernaryExpression; import io.micronaut.expressions.parser.ast.literal.BoolLiteral; @@ -62,48 +63,7 @@ import java.util.ArrayList; import java.util.List; -import static io.micronaut.expressions.parser.token.TokenType.AND; -import static io.micronaut.expressions.parser.token.TokenType.BEAN_CONTEXT; -import static io.micronaut.expressions.parser.token.TokenType.BOOL; -import static io.micronaut.expressions.parser.token.TokenType.COLON; -import static io.micronaut.expressions.parser.token.TokenType.COMMA; -import static io.micronaut.expressions.parser.token.TokenType.DECREMENT; -import static io.micronaut.expressions.parser.token.TokenType.DIV; -import static io.micronaut.expressions.parser.token.TokenType.DOT; -import static io.micronaut.expressions.parser.token.TokenType.DOUBLE; -import static io.micronaut.expressions.parser.token.TokenType.ELVIS; -import static io.micronaut.expressions.parser.token.TokenType.EMPTY; -import static io.micronaut.expressions.parser.token.TokenType.ENVIRONMENT; -import static io.micronaut.expressions.parser.token.TokenType.EQ; -import static io.micronaut.expressions.parser.token.TokenType.EXPRESSION_CONTEXT_REF; -import static io.micronaut.expressions.parser.token.TokenType.FLOAT; -import static io.micronaut.expressions.parser.token.TokenType.GT; -import static io.micronaut.expressions.parser.token.TokenType.GTE; -import static io.micronaut.expressions.parser.token.TokenType.IDENTIFIER; -import static io.micronaut.expressions.parser.token.TokenType.R_SQUARE; -import static io.micronaut.expressions.parser.token.TokenType.TYPE_IDENTIFIER; -import static io.micronaut.expressions.parser.token.TokenType.INCREMENT; -import static io.micronaut.expressions.parser.token.TokenType.INSTANCEOF; -import static io.micronaut.expressions.parser.token.TokenType.INT; -import static io.micronaut.expressions.parser.token.TokenType.LONG; -import static io.micronaut.expressions.parser.token.TokenType.LT; -import static io.micronaut.expressions.parser.token.TokenType.LTE; -import static io.micronaut.expressions.parser.token.TokenType.L_PAREN; -import static io.micronaut.expressions.parser.token.TokenType.L_SQUARE; -import static io.micronaut.expressions.parser.token.TokenType.MATCHES; -import static io.micronaut.expressions.parser.token.TokenType.MINUS; -import static io.micronaut.expressions.parser.token.TokenType.MOD; -import static io.micronaut.expressions.parser.token.TokenType.MUL; -import static io.micronaut.expressions.parser.token.TokenType.NE; -import static io.micronaut.expressions.parser.token.TokenType.NOT; -import static io.micronaut.expressions.parser.token.TokenType.NULL; -import static io.micronaut.expressions.parser.token.TokenType.OR; -import static io.micronaut.expressions.parser.token.TokenType.PLUS; -import static io.micronaut.expressions.parser.token.TokenType.POW; -import static io.micronaut.expressions.parser.token.TokenType.QMARK; -import static io.micronaut.expressions.parser.token.TokenType.R_PAREN; -import static io.micronaut.expressions.parser.token.TokenType.SAFE_NAV; -import static io.micronaut.expressions.parser.token.TokenType.STRING; +import static io.micronaut.expressions.parser.token.TokenType.*; /** * Parser for building AST for single evaluated expression. @@ -374,6 +334,7 @@ private ExpressionNode primaryExpression() { case IDENTIFIER -> evaluationContextAccess(false); case BEAN_CONTEXT -> beanContextAccess(); case ENVIRONMENT -> environmentAccess(); + case THIS -> thisAccess(); case TYPE_IDENTIFIER -> typeIdentifier(true); case L_PAREN -> parenthesizedExpression(); case STRING, INT, LONG, DOUBLE, FLOAT, BOOL, NULL -> literal(); @@ -381,6 +342,11 @@ private ExpressionNode primaryExpression() { }; } + private ExpressionNode thisAccess() { + eat(THIS); + return new ThisAccess(); + } + // EvaluationContextAccess // : '#' Identifier // | '#' Identifier MethodArguments diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/access/ThisAccess.java b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/access/ThisAccess.java new file mode 100644 index 00000000000..705c2287213 --- /dev/null +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/access/ThisAccess.java @@ -0,0 +1,58 @@ +/* + * Copyright 2017-2023 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.expressions.parser.ast.access; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.expressions.parser.ast.ExpressionNode; +import io.micronaut.expressions.parser.compilation.ExpressionVisitorContext; +import io.micronaut.expressions.parser.exception.ExpressionCompilationException; +import io.micronaut.inject.ast.ClassElement; +import io.micronaut.inject.processing.JavaModelUtils; +import org.objectweb.asm.Type; +import org.objectweb.asm.commons.GeneratorAdapter; +import org.objectweb.asm.commons.Method; + +import static io.micronaut.expressions.parser.ast.util.TypeDescriptors.EVALUATION_CONTEXT_TYPE; + +/** + * Enables access to 'this' in non-static contexts. + */ +@Internal +public final class ThisAccess extends ExpressionNode { + @Override + protected void generateBytecode(ExpressionVisitorContext ctx) { + GeneratorAdapter mv = ctx.methodVisitor(); + mv.loadArg(0); + mv.invokeInterface(EVALUATION_CONTEXT_TYPE, new Method("getThis", Type.getType(Object.class), new Type[0])); + mv.checkCast(resolveType(ctx)); + } + + @Override + protected ClassElement doResolveClassElement(ExpressionVisitorContext ctx) { + ClassElement thisType = ctx.compilationContext().findThis(); + if (thisType == null) { + throw new ExpressionCompilationException( + "Cannot reference 'this' from the current context."); + + } + return thisType; + } + + @Override + protected Type doResolveType(ExpressionVisitorContext ctx) { + return JavaModelUtils.getTypeReference(doResolveClassElement(ctx)); + } +} diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/token/TokenType.java b/core-processor/src/main/java/io/micronaut/expressions/parser/token/TokenType.java index f7afe29208b..e42966f188b 100644 --- a/core-processor/src/main/java/io/micronaut/expressions/parser/token/TokenType.java +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/token/TokenType.java @@ -29,6 +29,7 @@ public enum TokenType { IDENTIFIER, BEAN_CONTEXT, ENVIRONMENT, + THIS, TYPE_IDENTIFIER, EXPRESSION_CONTEXT_REF, DOT, diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/token/Tokenizer.java b/core-processor/src/main/java/io/micronaut/expressions/parser/token/Tokenizer.java index f78d4f1ffa8..2f845ca9c8e 100644 --- a/core-processor/src/main/java/io/micronaut/expressions/parser/token/Tokenizer.java +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/token/Tokenizer.java @@ -25,51 +25,7 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; -import static io.micronaut.expressions.parser.token.TokenType.AND; -import static io.micronaut.expressions.parser.token.TokenType.BEAN_CONTEXT; -import static io.micronaut.expressions.parser.token.TokenType.BOOL; -import static io.micronaut.expressions.parser.token.TokenType.COLON; -import static io.micronaut.expressions.parser.token.TokenType.COMMA; -import static io.micronaut.expressions.parser.token.TokenType.DECREMENT; -import static io.micronaut.expressions.parser.token.TokenType.DIV; -import static io.micronaut.expressions.parser.token.TokenType.DOT; -import static io.micronaut.expressions.parser.token.TokenType.DOUBLE; -import static io.micronaut.expressions.parser.token.TokenType.ELVIS; -import static io.micronaut.expressions.parser.token.TokenType.EMPTY; -import static io.micronaut.expressions.parser.token.TokenType.ENVIRONMENT; -import static io.micronaut.expressions.parser.token.TokenType.EQ; -import static io.micronaut.expressions.parser.token.TokenType.EXPRESSION_CONTEXT_REF; -import static io.micronaut.expressions.parser.token.TokenType.IDENTIFIER; -import static io.micronaut.expressions.parser.token.TokenType.FLOAT; -import static io.micronaut.expressions.parser.token.TokenType.GT; -import static io.micronaut.expressions.parser.token.TokenType.GTE; -import static io.micronaut.expressions.parser.token.TokenType.TYPE_IDENTIFIER; -import static io.micronaut.expressions.parser.token.TokenType.INCREMENT; -import static io.micronaut.expressions.parser.token.TokenType.INSTANCEOF; -import static io.micronaut.expressions.parser.token.TokenType.INT; -import static io.micronaut.expressions.parser.token.TokenType.LONG; -import static io.micronaut.expressions.parser.token.TokenType.LT; -import static io.micronaut.expressions.parser.token.TokenType.LTE; -import static io.micronaut.expressions.parser.token.TokenType.L_CURLY; -import static io.micronaut.expressions.parser.token.TokenType.L_PAREN; -import static io.micronaut.expressions.parser.token.TokenType.L_SQUARE; -import static io.micronaut.expressions.parser.token.TokenType.MATCHES; -import static io.micronaut.expressions.parser.token.TokenType.MINUS; -import static io.micronaut.expressions.parser.token.TokenType.MOD; -import static io.micronaut.expressions.parser.token.TokenType.MUL; -import static io.micronaut.expressions.parser.token.TokenType.NE; -import static io.micronaut.expressions.parser.token.TokenType.NOT; -import static io.micronaut.expressions.parser.token.TokenType.NULL; -import static io.micronaut.expressions.parser.token.TokenType.OR; -import static io.micronaut.expressions.parser.token.TokenType.PLUS; -import static io.micronaut.expressions.parser.token.TokenType.POW; -import static io.micronaut.expressions.parser.token.TokenType.QMARK; -import static io.micronaut.expressions.parser.token.TokenType.R_CURLY; -import static io.micronaut.expressions.parser.token.TokenType.R_PAREN; -import static io.micronaut.expressions.parser.token.TokenType.R_SQUARE; -import static io.micronaut.expressions.parser.token.TokenType.SAFE_NAV; -import static io.micronaut.expressions.parser.token.TokenType.STRING; -import static io.micronaut.expressions.parser.token.TokenType.WHITESPACE; +import static io.micronaut.expressions.parser.token.TokenType.*; /** * Tokenizer for parsing evaluated expressions. @@ -98,6 +54,7 @@ public final class Tokenizer { "^empty\\b", EMPTY, "^ctx\\b", BEAN_CONTEXT, "^env\\b", ENVIRONMENT, + "^this\\b", THIS, // LITERALS "^null\\b", NULL, // NULL diff --git a/core-processor/src/main/java/io/micronaut/inject/beans/visitor/BeanIntrospectionWriter.java b/core-processor/src/main/java/io/micronaut/inject/beans/visitor/BeanIntrospectionWriter.java index b858224804d..3103ca2e73d 100644 --- a/core-processor/src/main/java/io/micronaut/inject/beans/visitor/BeanIntrospectionWriter.java +++ b/core-processor/src/main/java/io/micronaut/inject/beans/visitor/BeanIntrospectionWriter.java @@ -41,6 +41,7 @@ import io.micronaut.inject.ast.TypedElement; import io.micronaut.inject.beans.AbstractInitializableBeanIntrospection; import io.micronaut.inject.processing.JavaModelUtils; +import io.micronaut.inject.visitor.VisitorContext; import io.micronaut.inject.writer.AbstractAnnotationMetadataWriter; import io.micronaut.inject.writer.ClassWriterOutputVisitor; import io.micronaut.inject.writer.DispatchWriter; @@ -125,9 +126,11 @@ final class BeanIntrospectionWriter extends AbstractAnnotationMetadataWriter { * * @param classElement The class element * @param beanAnnotationMetadata The bean annotation metadata + * @param visitorContext The visitor context */ - BeanIntrospectionWriter(ClassElement classElement, AnnotationMetadata beanAnnotationMetadata) { - super(computeReferenceName(classElement.getName()), classElement, beanAnnotationMetadata, true); + BeanIntrospectionWriter(ClassElement classElement, AnnotationMetadata beanAnnotationMetadata, + VisitorContext visitorContext) { + super(computeReferenceName(classElement.getName()), classElement, beanAnnotationMetadata, true, visitorContext); final String name = classElement.getName(); this.classElement = classElement; this.referenceWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS); @@ -145,14 +148,16 @@ final class BeanIntrospectionWriter extends AbstractAnnotationMetadataWriter { * @param originatingElement The originating element * @param classElement The class element * @param beanAnnotationMetadata The bean annotation metadata + * @param visitorContext The visitor context */ BeanIntrospectionWriter( String generatingType, int index, ClassElement originatingElement, ClassElement classElement, - AnnotationMetadata beanAnnotationMetadata) { - super(computeReferenceName(generatingType) + index, originatingElement, beanAnnotationMetadata, true); + AnnotationMetadata beanAnnotationMetadata, + VisitorContext visitorContext) { + super(computeReferenceName(generatingType) + index, originatingElement, beanAnnotationMetadata, true, visitorContext); final String className = classElement.getName(); this.classElement = classElement; this.referenceWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS); @@ -200,7 +205,7 @@ void visitProperty( boolean isReadOnly, @Nullable AnnotationMetadata annotationMetadata, @Nullable Map typeArguments) { - + this.evaluatedExpressionProcessor.processEvaluatedExpressions(annotationMetadata, classElement); int readDispatchIndex = -1; if (readMember != null) { if (readMember instanceof MethodElement) { @@ -276,6 +281,10 @@ public void visitBeanMethod(MethodElement element) { if (element != null && !element.isPrivate()) { int dispatchIndex = dispatchWriter.addMethod(classElement, element); beanMethods.add(new BeanMethodData(element, dispatchIndex)); + this.evaluatedExpressionProcessor.processEvaluatedExpressions(element.getAnnotationMetadata(), classElement); + for (ParameterElement parameter : element.getParameters()) { + this.evaluatedExpressionProcessor.processEvaluatedExpressions(parameter.getAnnotationMetadata(), classElement); + } } } @@ -300,6 +309,7 @@ public void accept(ClassWriterOutputVisitor classWriterOutputVisitor) throws IOE // First write the introspection for the annotation metadata can be populated with defaults that reference will contain writeIntrospectionClass(classWriterOutputVisitor); + this.evaluatedExpressionProcessor.writeEvaluatedExpressions(classWriterOutputVisitor); loadTypeMethods.clear(); // Second write the reference @@ -965,6 +975,7 @@ private static String computeIntrospectionName(String generatingName, String cla */ void visitConstructor(MethodElement constructor) { this.constructor = constructor; + processConstructorEvaluatedMetadata(constructor); } /** @@ -974,6 +985,14 @@ void visitConstructor(MethodElement constructor) { */ void visitDefaultConstructor(MethodElement constructor) { this.defaultConstructor = constructor; + processConstructorEvaluatedMetadata(constructor); + } + + private void processConstructorEvaluatedMetadata(MethodElement constructor) { + this.evaluatedExpressionProcessor.processEvaluatedExpressions(constructor.getAnnotationMetadata(), null); + for (ParameterElement parameter : constructor.getParameters()) { + this.evaluatedExpressionProcessor.processEvaluatedExpressions(parameter.getAnnotationMetadata(), null); + } } private record ExceptionDispatchTarget(Class exceptionType, String message) implements DispatchWriter.DispatchTarget { diff --git a/core-processor/src/main/java/io/micronaut/inject/beans/visitor/IntrospectedTypeElementVisitor.java b/core-processor/src/main/java/io/micronaut/inject/beans/visitor/IntrospectedTypeElementVisitor.java index d7217278c61..1d8478a793c 100644 --- a/core-processor/src/main/java/io/micronaut/inject/beans/visitor/IntrospectedTypeElementVisitor.java +++ b/core-processor/src/main/java/io/micronaut/inject/beans/visitor/IntrospectedTypeElementVisitor.java @@ -109,7 +109,8 @@ private void processIntrospected(ClassElement element, VisitorContext context, A index.getAndIncrement(), element, ce, - metadata ? resolvedMetadata : null + metadata ? resolvedMetadata : null, + context ); processElement( @@ -136,7 +137,8 @@ private void processIntrospected(ClassElement element, VisitorContext context, A j++, element, classElement, - metadata ? element.getAnnotationMetadata() : null + metadata ? element.getAnnotationMetadata() : null, + context ); processElement(metadata, @@ -150,7 +152,8 @@ private void processIntrospected(ClassElement element, VisitorContext context, A } else { final BeanIntrospectionWriter writer = new BeanIntrospectionWriter( element, - metadata ? element.getAnnotationMetadata() : null + metadata ? element.getAnnotationMetadata() : null, + context ); processElement(metadata, indexedAnnotations, element, writer); } diff --git a/core-processor/src/main/java/io/micronaut/inject/writer/AbstractAnnotationMetadataWriter.java b/core-processor/src/main/java/io/micronaut/inject/writer/AbstractAnnotationMetadataWriter.java index 909d912e9f8..156125d805d 100644 --- a/core-processor/src/main/java/io/micronaut/inject/writer/AbstractAnnotationMetadataWriter.java +++ b/core-processor/src/main/java/io/micronaut/inject/writer/AbstractAnnotationMetadataWriter.java @@ -23,6 +23,7 @@ import io.micronaut.inject.annotation.AnnotationMetadataWriter; import io.micronaut.inject.annotation.MutableAnnotationMetadata; import io.micronaut.inject.ast.Element; +import io.micronaut.inject.visitor.VisitorContext; import org.objectweb.asm.ClassWriter; import org.objectweb.asm.Label; import org.objectweb.asm.Type; @@ -54,6 +55,7 @@ public abstract class AbstractAnnotationMetadataWriter extends AbstractClassFile protected final AnnotationMetadata annotationMetadata; protected final Map loadTypeMethods = new HashMap<>(); protected final Map defaults = new HashMap<>(); + protected final EvaluatedExpressionProcessor evaluatedExpressionProcessor; private final boolean writeAnnotationDefault; /** @@ -61,16 +63,20 @@ public abstract class AbstractAnnotationMetadataWriter extends AbstractClassFile * @param originatingElements The originating elements * @param annotationMetadata The annotation metadata * @param writeAnnotationDefaults Whether to write annotation defaults + * @param visitorContext The visitor context */ protected AbstractAnnotationMetadataWriter( - String className, - OriginatingElements originatingElements, - AnnotationMetadata annotationMetadata, - boolean writeAnnotationDefaults) { + String className, + OriginatingElements originatingElements, + AnnotationMetadata annotationMetadata, + boolean writeAnnotationDefaults, + VisitorContext visitorContext) { super(originatingElements); this.targetClassType = getTypeReferenceForName(className); this.annotationMetadata = annotationMetadata.getTargetAnnotationMetadata(); this.writeAnnotationDefault = writeAnnotationDefaults; + this.evaluatedExpressionProcessor = new EvaluatedExpressionProcessor(visitorContext, getOriginatingElement()); + this.evaluatedExpressionProcessor.processEvaluatedExpressions(this.annotationMetadata, null); } /** @@ -78,16 +84,20 @@ protected AbstractAnnotationMetadataWriter( * @param originatingElement The originating element * @param annotationMetadata The annotation metadata * @param writeAnnotationDefaults Whether to write annotation defaults + * @param visitorContext The visitor context */ protected AbstractAnnotationMetadataWriter( String className, Element originatingElement, AnnotationMetadata annotationMetadata, - boolean writeAnnotationDefaults) { + boolean writeAnnotationDefaults, + VisitorContext visitorContext) { super(originatingElement); this.targetClassType = getTypeReferenceForName(className); this.annotationMetadata = annotationMetadata.getTargetAnnotationMetadata(); this.writeAnnotationDefault = writeAnnotationDefaults; + this.evaluatedExpressionProcessor = new EvaluatedExpressionProcessor(visitorContext, originatingElement); + this.evaluatedExpressionProcessor.processEvaluatedExpressions(this.annotationMetadata, null); } /** diff --git a/core-processor/src/main/java/io/micronaut/inject/writer/AbstractBeanDefinitionBuilder.java b/core-processor/src/main/java/io/micronaut/inject/writer/AbstractBeanDefinitionBuilder.java index 016a1c335c3..6831f24d689 100644 --- a/core-processor/src/main/java/io/micronaut/inject/writer/AbstractBeanDefinitionBuilder.java +++ b/core-processor/src/main/java/io/micronaut/inject/writer/AbstractBeanDefinitionBuilder.java @@ -756,7 +756,7 @@ protected void finalizeAndWriteBean( BeanDefinitionVisitor beanDefinitionWriter) throws IOException { beanDefinitionWriter.visitBeanDefinitionEnd(); BeanDefinitionReferenceWriter beanDefinitionReferenceWriter = - new BeanDefinitionReferenceWriter(beanDefinitionWriter); + new BeanDefinitionReferenceWriter(beanDefinitionWriter, visitorContext); beanDefinitionReferenceWriter .setRequiresMethodProcessing(beanDefinitionWriter.requiresMethodProcessing()); beanDefinitionReferenceWriter.accept(classWriterOutputVisitor); diff --git a/core-processor/src/main/java/io/micronaut/inject/writer/BeanConfigurationWriter.java b/core-processor/src/main/java/io/micronaut/inject/writer/BeanConfigurationWriter.java index 42ef1a43fec..1e032b78ca5 100644 --- a/core-processor/src/main/java/io/micronaut/inject/writer/BeanConfigurationWriter.java +++ b/core-processor/src/main/java/io/micronaut/inject/writer/BeanConfigurationWriter.java @@ -20,6 +20,7 @@ import io.micronaut.core.annotation.Internal; import io.micronaut.inject.BeanConfiguration; import io.micronaut.inject.ast.Element; +import io.micronaut.inject.visitor.VisitorContext; import org.objectweb.asm.ClassWriter; import org.objectweb.asm.Type; import org.objectweb.asm.commons.GeneratorAdapter; @@ -50,12 +51,14 @@ public class BeanConfigurationWriter extends AbstractAnnotationMetadataWriter { * @param packageName The package name * @param originatingElement The originating element * @param annotationMetadata The annotation metadata + * @param visitorContext The visitor context */ public BeanConfigurationWriter( - String packageName, - Element originatingElement, - AnnotationMetadata annotationMetadata) { - super(packageName + '.' + CLASS_SUFFIX, originatingElement, annotationMetadata, true); + String packageName, + Element originatingElement, + AnnotationMetadata annotationMetadata, + VisitorContext visitorContext) { + super(packageName + '.' + CLASS_SUFFIX, originatingElement, annotationMetadata, true, visitorContext); this.packageName = packageName; this.configurationClassName = targetClassType.getClassName(); this.configurationClassInternalName = targetClassType.getInternalName(); diff --git a/core-processor/src/main/java/io/micronaut/inject/writer/BeanDefinitionReferenceWriter.java b/core-processor/src/main/java/io/micronaut/inject/writer/BeanDefinitionReferenceWriter.java index a7b0d2600aa..17f16e3ca87 100644 --- a/core-processor/src/main/java/io/micronaut/inject/writer/BeanDefinitionReferenceWriter.java +++ b/core-processor/src/main/java/io/micronaut/inject/writer/BeanDefinitionReferenceWriter.java @@ -33,6 +33,7 @@ import io.micronaut.inject.BeanDefinitionReference; import io.micronaut.inject.annotation.AnnotationMetadataReference; import io.micronaut.inject.ast.ClassElement; +import io.micronaut.inject.visitor.VisitorContext; import jakarta.inject.Singleton; import org.objectweb.asm.ClassWriter; import org.objectweb.asm.Type; @@ -91,13 +92,16 @@ public class BeanDefinitionReferenceWriter extends AbstractAnnotationMetadataWri * Default constructor. * * @param visitor The visitor + * @param visitorContext The visitor context */ - public BeanDefinitionReferenceWriter(BeanDefinitionVisitor visitor) { + public BeanDefinitionReferenceWriter(BeanDefinitionVisitor visitor, + VisitorContext visitorContext) { super( visitor.getBeanDefinitionName() + REF_SUFFIX, visitor, visitor.getAnnotationMetadata(), - true); + true, + visitorContext); this.providedType = visitor.getProvidedType(); this.beanTypeName = visitor.getBeanTypeName(); this.typeParameters = visitor.getTypeArgumentMap(); diff --git a/core-processor/src/main/java/io/micronaut/inject/writer/BeanDefinitionVisitor.java b/core-processor/src/main/java/io/micronaut/inject/writer/BeanDefinitionVisitor.java index 4244eaca666..1dc4238b630 100644 --- a/core-processor/src/main/java/io/micronaut/inject/writer/BeanDefinitionVisitor.java +++ b/core-processor/src/main/java/io/micronaut/inject/writer/BeanDefinitionVisitor.java @@ -16,8 +16,9 @@ package io.micronaut.inject.writer; import io.micronaut.core.annotation.AnnotationMetadata; +import io.micronaut.core.annotation.NonNull; +import io.micronaut.core.annotation.Nullable; import io.micronaut.core.util.Toggleable; -import io.micronaut.expressions.context.ExpressionWithContext; import io.micronaut.inject.BeanDefinition; import io.micronaut.inject.ast.ClassElement; import io.micronaut.inject.ast.Element; @@ -29,14 +30,10 @@ import io.micronaut.inject.visitor.VisitorContext; import org.objectweb.asm.Type; -import io.micronaut.core.annotation.NonNull; -import io.micronaut.core.annotation.Nullable; - import java.io.File; import java.io.IOException; import java.util.Map; import java.util.Optional; -import java.util.Set; /** * Interface for {@link BeanDefinitionVisitor} implementations such as {@link BeanDefinitionWriter}. @@ -340,13 +337,6 @@ void visitFieldValue(TypedElement declaringType, */ AnnotationMetadata getAnnotationMetadata(); - /** - * @return The evaluated expressions metadata - * @since 4.0.0 - */ - @NonNull - Set getEvaluatedExpressions(); - /** * Begin defining a configuration builder. * diff --git a/core-processor/src/main/java/io/micronaut/inject/writer/BeanDefinitionWriter.java b/core-processor/src/main/java/io/micronaut/inject/writer/BeanDefinitionWriter.java index 91fc91a1073..e6d13c6f200 100644 --- a/core-processor/src/main/java/io/micronaut/inject/writer/BeanDefinitionWriter.java +++ b/core-processor/src/main/java/io/micronaut/inject/writer/BeanDefinitionWriter.java @@ -41,7 +41,6 @@ import io.micronaut.context.annotation.Value; import io.micronaut.context.env.ConfigurationPath; import io.micronaut.core.annotation.AccessorsStyle; -import io.micronaut.core.expressions.EvaluatedExpressionReference; import io.micronaut.core.annotation.AnnotationMetadata; import io.micronaut.core.annotation.AnnotationMetadataProvider; import io.micronaut.core.annotation.AnnotationUtil; @@ -53,6 +52,7 @@ import io.micronaut.core.beans.BeanConstructor; import io.micronaut.core.convert.ConversionService; import io.micronaut.core.convert.ConversionServiceProvider; +import io.micronaut.core.expressions.EvaluatedExpressionReference; import io.micronaut.core.naming.NameUtils; import io.micronaut.core.reflect.ClassUtils; import io.micronaut.core.reflect.InstantiationUtils; @@ -64,11 +64,6 @@ import io.micronaut.core.util.CollectionUtils; import io.micronaut.core.util.StringUtils; import io.micronaut.core.util.Toggleable; -import io.micronaut.expressions.EvaluatedExpressionWriter; -import io.micronaut.expressions.context.ExpressionCompilationContext; -import io.micronaut.expressions.context.ExpressionWithContext; -import io.micronaut.expressions.context.DefaultExpressionCompilationContextFactory; -import io.micronaut.expressions.util.EvaluatedExpressionsUtils; import io.micronaut.inject.AdvisedBeanType; import io.micronaut.inject.BeanDefinition; import io.micronaut.inject.DisposableBeanDefinition; @@ -136,7 +131,6 @@ import java.util.concurrent.TimeUnit; import java.util.function.Consumer; import java.util.function.Predicate; -import java.util.stream.Collectors; import java.util.stream.Stream; import static io.micronaut.inject.visitor.BeanElementVisitor.VISITORS; @@ -546,6 +540,7 @@ public class BeanDefinitionWriter extends AbstractClassFileWriter implements Bea private final VisitorContext visitorContext; private final boolean isPrimitiveBean; private final List beanTypeInnerClasses; + private final EvaluatedExpressionProcessor evaluatedExpressionProcessor; private GeneratorAdapter buildMethodVisitor; private GeneratorAdapter injectMethodVisitor; private GeneratorAdapter checkIfShouldLoadMethodVisitor; @@ -583,9 +578,6 @@ public class BeanDefinitionWriter extends AbstractClassFileWriter implements Bea private final Map isLifeCycleCache = new HashMap<>(2); private ExecutableMethodsDefinitionWriter executableMethodsDefinitionWriter; - private final Collection evaluatedExpressions = new ArrayList<>(2); - private final DefaultExpressionCompilationContextFactory expressionCompilationContextFactory; - private Object constructor; // MethodElement or FieldElement private boolean disabled = false; @@ -720,14 +712,14 @@ public BeanDefinitionWriter(Element beanProducingElement, this.isConfigurationProperties = isConfigurationProperties(annotationMetadata); validateExposedTypes(annotationMetadata, visitorContext); this.visitorContext = visitorContext; - this.expressionCompilationContextFactory = new DefaultExpressionCompilationContextFactory(visitorContext); - processEvaluatedExpressions(this.annotationMetadata); + this.evaluatedExpressionProcessor = new EvaluatedExpressionProcessor(visitorContext, getOriginatingElement()); + evaluatedExpressionProcessor.processEvaluatedExpressions(this.annotationMetadata, null); beanTypeInnerClasses = beanTypeElement.getEnclosedElements(ElementQuery.of(ClassElement.class)) .stream() .filter(this::isConfigurationProperties) .map(Element::getName) - .collect(Collectors.toList()); + .toList(); String prop = visitorContext.getOptions().get(OMIT_CONFPROP_INJECTION_POINTS); keepConfPropInjectPoints = prop == null || !prop.equals("true"); } @@ -994,9 +986,9 @@ public void visitBeanDefinitionConstructor(MethodElement constructor, // now implement the inject method visitInjectMethodDefinition(); - processEvaluatedExpressions(constructor.getAnnotationMetadata()); + evaluatedExpressionProcessor.processEvaluatedExpressions(constructor.getAnnotationMetadata(), null); for (ParameterElement parameter: constructor.getParameters()) { - processEvaluatedExpressions(parameter.getAnnotationMetadata()); + evaluatedExpressionProcessor.processEvaluatedExpressions(parameter.getAnnotationMetadata(), null); } } } @@ -1420,22 +1412,11 @@ public void accept(ClassWriterOutputVisitor visitor) throws IOException { throw e; } } - writeEvaluatedExpressions(visitor); + evaluatedExpressionProcessor.writeEvaluatedExpressions(visitor); out.write(toByteArray()); } } - private void writeEvaluatedExpressions(ClassWriterOutputVisitor visitor) throws IOException { - for (ExpressionWithContext expressionMetadata: getEvaluatedExpressions()) { - EvaluatedExpressionWriter expressionWriter = new EvaluatedExpressionWriter( - expressionMetadata, - visitorContext, - getOriginatingElement()); - - expressionWriter.accept(visitor); - } - } - @Override public void visitSetterValue( TypedElement declaringType, @@ -1570,7 +1551,7 @@ public void visitMethodInjectionPoint(TypedElement declaringType, boolean requiresReflection, VisitorContext visitorContext) { MethodVisitData methodVisitData = new MethodVisitData(declaringType, methodElement, requiresReflection, methodElement.getAnnotationMetadata()); - processEvaluatedExpressions(methodElement.getAnnotationMetadata()); + evaluatedExpressionProcessor.processEvaluatedExpressions(methodElement.getAnnotationMetadata(), this.beanTypeElement); methodInjectionPoints.add(methodVisitData); allMethodVisits.add(methodVisitData); visitMethodInjectionPointInternal(methodVisitData, injectMethodVisitor, injectInstanceLocalVarIndex); @@ -1604,7 +1585,14 @@ public int visitExecutableMethod(TypedElement declaringType, String interceptedProxyBridgeMethodName) { if (executableMethodsDefinitionWriter == null) { - executableMethodsDefinitionWriter = new ExecutableMethodsDefinitionWriter(visitorContext, annotationMetadata, beanDefinitionName, getBeanDefinitionReferenceClassName(), originatingElements); + executableMethodsDefinitionWriter = new ExecutableMethodsDefinitionWriter( + visitorContext, + evaluatedExpressionProcessor, + annotationMetadata, + beanDefinitionName, + getBeanDefinitionReferenceClassName(), + originatingElements + ); } return executableMethodsDefinitionWriter.visitExecutableMethod(declaringType, methodElement, interceptedProxyClassName, interceptedProxyBridgeMethodName); } @@ -1631,16 +1619,6 @@ public AnnotationMetadata getAnnotationMetadata() { return this.annotationMetadata; } - @Override - public Set getEvaluatedExpressions() { - return Stream.concat( - evaluatedExpressions.stream(), - executableMethodsDefinitionWriter != null - ? executableMethodsDefinitionWriter.getEvaluatedExpressions().stream() - : Stream.empty()) - .collect(Collectors.toSet()); - } - @Override public void visitConfigBuilderField( ClassElement type, @@ -2194,7 +2172,7 @@ private void visitFieldInjectionPointInternal( Method methodToInvoke, boolean isArray, boolean requiresGenericType) { - processEvaluatedExpressions(annotationMetadata); + evaluatedExpressionProcessor.processEvaluatedExpressions(annotationMetadata, null); autoApplyNamedIfPresent(fieldElement, annotationMetadata); @@ -2453,7 +2431,7 @@ private void visitMethodInjectionPointInternal(MethodVisitData methodVisitData, Type declaringTypeRef = JavaModelUtils.getTypeReference(declaringType); boolean hasInjectScope = false; for (ParameterElement value : argumentTypes) { - processEvaluatedExpressions(value.getAnnotationMetadata()); + evaluatedExpressionProcessor.processEvaluatedExpressions(value.getAnnotationMetadata(), null); if (value.hasDeclaredAnnotation(InjectScope.class)) { hasInjectScope = true; } @@ -3167,9 +3145,9 @@ private void visitBuildFactoryMethodDefinition( ClassElement factoryClass, Element factoryElement, ParameterElement... parameters) { if (buildMethodVisitor == null) { - processEvaluatedExpressions(factoryElement.getAnnotationMetadata()); + evaluatedExpressionProcessor.processEvaluatedExpressions(factoryElement.getAnnotationMetadata(), null); for (ParameterElement parameterElement: parameters) { - processEvaluatedExpressions(parameterElement.getAnnotationMetadata()); + evaluatedExpressionProcessor.processEvaluatedExpressions(parameterElement.getAnnotationMetadata(), null); } List parameterList = Arrays.asList(parameters); @@ -4208,7 +4186,7 @@ private void pushPrecalculatedInfo(GeneratorAdapter protectedConstructor, Annota protectedConstructor.push(preprocessMethods); // 9: hasEvaluatedExpressions - protectedConstructor.push(!getEvaluatedExpressions().isEmpty()); + protectedConstructor.push(evaluatedExpressionProcessor.hasEvaluatedExpressions()); protectedConstructor.invokeConstructor(PRECALCULATED_INFO, PRECALCULATED_INFO_CONSTRUCTOR); } @@ -4589,22 +4567,6 @@ private void populateBeanTypes(Set processedTypes, Set bea } } - private void processEvaluatedExpressions(AnnotationMetadata annotationMetadata) { - if (annotationMetadata instanceof AnnotationMetadataHierarchy) { - annotationMetadata = annotationMetadata.getDeclaredMetadata(); - } - - Collection expressionReferences = - EvaluatedExpressionsUtils.findEvaluatedExpressionReferences(annotationMetadata); - - expressionReferences.stream() - .map(expressionReference -> { - ExpressionCompilationContext evaluationContext = expressionCompilationContextFactory.buildContext(expressionReference); - return new ExpressionWithContext(expressionReference, evaluationContext); - }) - .forEach(evaluatedExpressions::add); - } - @Override public Optional getScope() { return annotationMetadata.getAnnotationNameByStereotype(AnnotationUtil.SCOPE); @@ -4659,7 +4621,7 @@ public boolean isProxiedBean() { public static void finish() { AbstractAnnotationMetadataBuilder.clearMutated(); AbstractAnnotationMetadataBuilder.clearCaches(); - DefaultExpressionCompilationContextFactory.reset(); + EvaluatedExpressionProcessor.reset(); } @Internal diff --git a/core-processor/src/main/java/io/micronaut/inject/writer/EvaluatedExpressionProcessor.java b/core-processor/src/main/java/io/micronaut/inject/writer/EvaluatedExpressionProcessor.java new file mode 100644 index 00000000000..5fac1587185 --- /dev/null +++ b/core-processor/src/main/java/io/micronaut/inject/writer/EvaluatedExpressionProcessor.java @@ -0,0 +1,119 @@ +/* + * Copyright 2017-2023 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.inject.writer; + +import io.micronaut.core.annotation.AnnotationMetadata; +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.Nullable; +import io.micronaut.core.expressions.EvaluatedExpressionReference; +import io.micronaut.expressions.EvaluatedExpressionWriter; +import io.micronaut.expressions.context.DefaultExpressionCompilationContextFactory; +import io.micronaut.expressions.context.ExpressionCompilationContext; +import io.micronaut.expressions.context.ExpressionWithContext; +import io.micronaut.expressions.util.EvaluatedExpressionsUtils; +import io.micronaut.inject.annotation.AnnotationMetadataHierarchy; +import io.micronaut.inject.ast.ClassElement; +import io.micronaut.inject.ast.Element; +import io.micronaut.inject.ast.MethodElement; +import io.micronaut.inject.visitor.VisitorContext; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; + +/** + * Internal utility class for writing annotation metadata with evaluated expressions. + */ +@Internal +public final class EvaluatedExpressionProcessor { + private final Collection evaluatedExpressions = new ArrayList<>(2); + private final DefaultExpressionCompilationContextFactory expressionCompilationContextFactory; + private final VisitorContext visitorContext; + private final Element originatingElement; + + /** + * Default constructor. + * @param visitorContext The visitor context + * @param originatingElement The originating element + */ + public EvaluatedExpressionProcessor( + VisitorContext visitorContext, + Element originatingElement) { + this.visitorContext = visitorContext; + this.expressionCompilationContextFactory = new DefaultExpressionCompilationContextFactory(visitorContext); + this.originatingElement = originatingElement; + } + + /** + * Reset after processing. + */ + public static void reset() { + DefaultExpressionCompilationContextFactory.reset(); + } + + /** + * Process evaluated expression contained within annotation metadata. + * @param annotationMetadata The annotation metadata + * @param thisElement If the expressino is evaluated in a non-static context, this type represents {@code this} + */ + public void processEvaluatedExpressions(AnnotationMetadata annotationMetadata, @Nullable ClassElement thisElement) { + if (annotationMetadata instanceof AnnotationMetadataHierarchy) { + annotationMetadata = annotationMetadata.getDeclaredMetadata(); + } + + Collection expressionReferences = + EvaluatedExpressionsUtils.findEvaluatedExpressionReferences(annotationMetadata); + + expressionReferences.stream() + .map(expressionReference -> { + ExpressionCompilationContext evaluationContext = expressionCompilationContextFactory.buildContext(expressionReference, thisElement); + return new ExpressionWithContext(expressionReference, evaluationContext); + }) + .forEach(evaluatedExpressions::add); + } + + public void processEvaluatedExpressions(MethodElement methodElement) { + Collection expressionReferences = + EvaluatedExpressionsUtils.findEvaluatedExpressionReferences(methodElement.getDeclaredMetadata()); + + expressionReferences.stream() + .map(expression -> { + ExpressionCompilationContext evaluationContext = expressionCompilationContextFactory.buildContextForMethod(expression, methodElement); + return new ExpressionWithContext(expression, evaluationContext); + }) + .forEach(evaluatedExpressions::add); + } + + public Collection getEvaluatedExpressions() { + return evaluatedExpressions; + } + + public void writeEvaluatedExpressions(ClassWriterOutputVisitor visitor) throws IOException { + for (ExpressionWithContext expressionMetadata: getEvaluatedExpressions()) { + EvaluatedExpressionWriter expressionWriter = new EvaluatedExpressionWriter( + expressionMetadata, + visitorContext, + originatingElement + ); + + expressionWriter.accept(visitor); + } + } + + public boolean hasEvaluatedExpressions() { + return !this.evaluatedExpressions.isEmpty(); + } +} diff --git a/core-processor/src/main/java/io/micronaut/inject/writer/ExecutableMethodsDefinitionWriter.java b/core-processor/src/main/java/io/micronaut/inject/writer/ExecutableMethodsDefinitionWriter.java index 52852719aba..eada06f0704 100644 --- a/core-processor/src/main/java/io/micronaut/inject/writer/ExecutableMethodsDefinitionWriter.java +++ b/core-processor/src/main/java/io/micronaut/inject/writer/ExecutableMethodsDefinitionWriter.java @@ -18,17 +18,11 @@ import io.micronaut.context.AbstractExecutableMethodsDefinition; import io.micronaut.core.annotation.AnnotationMetadata; import io.micronaut.core.annotation.Internal; -import io.micronaut.core.annotation.NonNull; import io.micronaut.core.reflect.ReflectionUtils; import io.micronaut.core.type.Argument; -import io.micronaut.expressions.context.DefaultExpressionCompilationContextFactory; -import io.micronaut.expressions.context.ExpressionCompilationContext; -import io.micronaut.expressions.context.ExpressionWithContext; -import io.micronaut.expressions.util.EvaluatedExpressionsUtils; import io.micronaut.inject.annotation.AnnotationMetadataHierarchy; import io.micronaut.inject.annotation.AnnotationMetadataReference; import io.micronaut.inject.annotation.AnnotationMetadataWriter; -import io.micronaut.core.expressions.EvaluatedExpressionReference; import io.micronaut.inject.annotation.MutableAnnotationMetadata; import io.micronaut.inject.ast.ClassElement; import io.micronaut.inject.ast.MethodElement; @@ -48,7 +42,6 @@ import java.io.OutputStream; import java.util.ArrayList; import java.util.Arrays; -import java.util.Collection; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; @@ -104,24 +97,24 @@ public class ExecutableMethodsDefinitionWriter extends AbstractClassFileWriter i private final DispatchWriter methodDispatchWriter; private final Set methodNames = new HashSet<>(); - private final DefaultExpressionCompilationContextFactory expressionCompilationContextFactory; - private final Set evaluatedExpressions = new HashSet<>(); private final AnnotationMetadata annotationMetadataWithDefaults; + private final EvaluatedExpressionProcessor evaluatedExpressionProcessor; private ClassWriter classWriter; public ExecutableMethodsDefinitionWriter(VisitorContext visitorContext, + EvaluatedExpressionProcessor evaluatedExpressionProcessor, AnnotationMetadata annotationMetadataWithDefaults, String beanDefinitionClassName, String beanDefinitionReferenceClassName, OriginatingElements originatingElements) { super(originatingElements); this.annotationMetadataWithDefaults = annotationMetadataWithDefaults; + this.evaluatedExpressionProcessor = evaluatedExpressionProcessor; this.className = beanDefinitionClassName + CLASS_SUFFIX; this.internalName = getInternalName(className); this.thisType = Type.getObjectType(internalName); this.beanDefinitionReferenceClassName = beanDefinitionReferenceClassName; this.methodDispatchWriter = new DispatchWriter(thisType); - this.expressionCompilationContextFactory = new DefaultExpressionCompilationContextFactory(visitorContext); } /** @@ -138,14 +131,6 @@ public Type getClassType() { return thisType; } - /** - * @return list of evaluated expressions. - */ - @NonNull - public Set getEvaluatedExpressions() { - return evaluatedExpressions; - } - private MethodElement getMethodElement(int index) { return ((DispatchWriter.MethodDispatchTarget) methodDispatchWriter.getDispatchTargets().get(index)).methodElement; } @@ -214,7 +199,7 @@ public int visitExecutableMethod(TypedElement declaringType, MethodElement methodElement, String interceptedProxyClassName, String interceptedProxyBridgeMethodName) { - processEvaluatedExpressions(methodElement); + evaluatedExpressionProcessor.processEvaluatedExpressions(methodElement); String methodKey = methodElement.getName() + "(" + @@ -526,16 +511,4 @@ private void pushAnnotationMetadata(AnnotationMetadata annotationMetadataWithDef throw new IllegalStateException("Unknown metadata: " + annotationMetadata); } } - - private void processEvaluatedExpressions(MethodElement methodElement) { - Collection expressionReferences = - EvaluatedExpressionsUtils.findEvaluatedExpressionReferences(methodElement.getDeclaredMetadata()); - - expressionReferences.stream() - .map(expression -> { - ExpressionCompilationContext evaluationContext = expressionCompilationContextFactory.buildContextForMethod(expression, methodElement); - return new ExpressionWithContext(expression, evaluationContext); - }) - .forEach(evaluatedExpressions::add); - } } diff --git a/core/src/main/java/io/micronaut/core/expressions/ExpressionEvaluationContext.java b/core/src/main/java/io/micronaut/core/expressions/ExpressionEvaluationContext.java index 95741985980..22d8130f780 100644 --- a/core/src/main/java/io/micronaut/core/expressions/ExpressionEvaluationContext.java +++ b/core/src/main/java/io/micronaut/core/expressions/ExpressionEvaluationContext.java @@ -28,6 +28,15 @@ @Internal public interface ExpressionEvaluationContext extends AutoCloseable { + /** + * Expressions that are evaluated in non-static contexts can reference "this". + * + *

The object returned here is a reference to this.

+ * + * @return The object that represents this. + */ + @Nullable Object getThis(); + /** * Provides method argument by index. * diff --git a/graal/src/main/java/io/micronaut/graal/reflect/GraalReflectionMetadataWriter.java b/graal/src/main/java/io/micronaut/graal/reflect/GraalReflectionMetadataWriter.java index dd25faef3ec..a483328d20f 100644 --- a/graal/src/main/java/io/micronaut/graal/reflect/GraalReflectionMetadataWriter.java +++ b/graal/src/main/java/io/micronaut/graal/reflect/GraalReflectionMetadataWriter.java @@ -18,6 +18,7 @@ import io.micronaut.core.annotation.AnnotationMetadata; import io.micronaut.core.graal.GraalReflectionConfigurer; import io.micronaut.inject.ast.ClassElement; +import io.micronaut.inject.visitor.VisitorContext; import io.micronaut.inject.writer.AbstractAnnotationMetadataWriter; import io.micronaut.inject.writer.ClassWriterOutputVisitor; import org.objectweb.asm.ClassWriter; @@ -39,8 +40,9 @@ final class GraalReflectionMetadataWriter extends AbstractAnnotationMetadataWrit private final String classInternalName; public GraalReflectionMetadataWriter(ClassElement originatingElement, - AnnotationMetadata annotationMetadata) { - super(resolveName(originatingElement), originatingElement, annotationMetadata, true); + AnnotationMetadata annotationMetadata, + VisitorContext visitorContext) { + super(resolveName(originatingElement), originatingElement, annotationMetadata, true, visitorContext); this.className = targetClassType.getClassName(); this.classInternalName = targetClassType.getInternalName(); } diff --git a/graal/src/main/java/io/micronaut/graal/reflect/GraalTypeElementVisitor.java b/graal/src/main/java/io/micronaut/graal/reflect/GraalTypeElementVisitor.java index e27b8232572..2eb9c41481a 100644 --- a/graal/src/main/java/io/micronaut/graal/reflect/GraalTypeElementVisitor.java +++ b/graal/src/main/java/io/micronaut/graal/reflect/GraalTypeElementVisitor.java @@ -212,7 +212,8 @@ public void visitClass(ClassElement element, VisitorContext context) { ); GraalReflectionMetadataWriter writer = new GraalReflectionMetadataWriter( element, - annotationMetadata + annotationMetadata, + context ); try { writer.accept(context); diff --git a/inject-groovy-test/src/main/groovy/io/micronaut/ast/transform/test/AbstractEvaluatedExpressionsSpec.groovy b/inject-groovy-test/src/main/groovy/io/micronaut/ast/transform/test/AbstractEvaluatedExpressionsSpec.groovy index 0e57a80a62d..85bc2caa457 100644 --- a/inject-groovy-test/src/main/groovy/io/micronaut/ast/transform/test/AbstractEvaluatedExpressionsSpec.groovy +++ b/inject-groovy-test/src/main/groovy/io/micronaut/ast/transform/test/AbstractEvaluatedExpressionsSpec.groovy @@ -55,7 +55,7 @@ class AbstractEvaluatedExpressionsSpec extends AbstractBeanDefinitionSpec { String exprFullName = exprClassName + i try { def exprClass = (AbstractEvaluatedExpression) classLoader.loadClass(exprFullName).newInstance() - result.add(exprClass.evaluate(new DefaultExpressionEvaluationContext(null, applicationContext, null))) + result.add(exprClass.evaluate(new DefaultExpressionEvaluationContext(null, null, applicationContext, null))) } catch (ClassNotFoundException e) { return null } @@ -90,7 +90,7 @@ class AbstractEvaluatedExpressionsSpec extends AbstractBeanDefinitionSpec { try { def index = EvaluatedExpressionReference.nextIndex(exprClassName) def exprClass = (AbstractEvaluatedExpression) classLoader.loadClass(exprClassName + (index == 0 ? index : index - 1)).newInstance() - return exprClass.evaluate(new DefaultExpressionEvaluationContext(null, applicationContext, null)); + return exprClass.evaluate(new DefaultExpressionEvaluationContext(null, null, applicationContext, null)); } catch (ClassNotFoundException e) { return null } @@ -117,7 +117,7 @@ class AbstractEvaluatedExpressionsSpec extends AbstractBeanDefinitionSpec { try { def index = EvaluatedExpressionReference.nextIndex(exprFullName) def exprClass = (AbstractEvaluatedExpression) classLoader.loadClass(exprFullName + (index == 0 ? index : index - 1)).newInstance() - return exprClass.evaluate(new DefaultExpressionEvaluationContext(args, applicationContext, null)); + return exprClass.evaluate(new DefaultExpressionEvaluationContext(null, args, applicationContext, null)); } catch (ClassNotFoundException e) { return null } diff --git a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/InjectTransform.groovy b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/InjectTransform.groovy index f08d2bf4f63..6a52cf2e79b 100644 --- a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/InjectTransform.groovy +++ b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/InjectTransform.groovy @@ -85,7 +85,8 @@ class InjectTransform implements ASTTransformation, CompilationUnitAware { BeanConfigurationWriter writer = new BeanConfigurationWriter( classNode.packageName, groovyPackageElement, - groovyPackageElement.getAnnotationMetadata() + groovyPackageElement.getAnnotationMetadata(), + visitorContext ) try { writer.accept(outputVisitor) @@ -126,7 +127,7 @@ class InjectTransform implements ASTTransformation, CompilationUnitAware { String beanTypeName = beanDefWriter.beanTypeName AnnotatedNode beanClassNode = entry.key try { - BeanDefinitionReferenceWriter beanReferenceWriter = new BeanDefinitionReferenceWriter(beanDefWriter) + BeanDefinitionReferenceWriter beanReferenceWriter = new BeanDefinitionReferenceWriter(beanDefWriter, groovyVisitorContext) beanReferenceWriter.setRequiresMethodProcessing(beanDefWriter.requiresMethodProcessing()) beanReferenceWriter.setContextScope(beanDefWriter.getAnnotationMetadata().hasDeclaredAnnotation(Context)) beanDefWriter.visitBeanDefinitionEnd() diff --git a/inject-groovy/src/test/groovy/io/micronaut/expressions/ContextPropertyAccessExpressionsSpec.groovy b/inject-groovy/src/test/groovy/io/micronaut/expressions/ContextPropertyAccessExpressionsSpec.groovy index 0ff25cb888c..95308e9a16f 100644 --- a/inject-groovy/src/test/groovy/io/micronaut/expressions/ContextPropertyAccessExpressionsSpec.groovy +++ b/inject-groovy/src/test/groovy/io/micronaut/expressions/ContextPropertyAccessExpressionsSpec.groovy @@ -1,10 +1,12 @@ package io.micronaut.expressions import io.micronaut.ast.transform.test.AbstractEvaluatedExpressionsSpec -import io.micronaut.context.exceptions.ExpressionEvaluationException; +import io.micronaut.context.exceptions.ExpressionEvaluationException +import spock.lang.Ignore; class ContextPropertyAccessExpressionsSpec extends AbstractEvaluatedExpressionsSpec { + @Ignore("already tested in java and flakey in Groovy") void "test context property access"() { given: Object expr1 = evaluateAgainstContext("#{ #intValue }", diff --git a/inject-java-test/src/main/groovy/io/micronaut/annotation/processing/test/AbstractEvaluatedExpressionsSpec.groovy b/inject-java-test/src/main/groovy/io/micronaut/annotation/processing/test/AbstractEvaluatedExpressionsSpec.groovy index a58327f9ba3..e1b60f2ecc2 100644 --- a/inject-java-test/src/main/groovy/io/micronaut/annotation/processing/test/AbstractEvaluatedExpressionsSpec.groovy +++ b/inject-java-test/src/main/groovy/io/micronaut/annotation/processing/test/AbstractEvaluatedExpressionsSpec.groovy @@ -19,7 +19,6 @@ import io.micronaut.context.expressions.AbstractEvaluatedExpression import io.micronaut.context.expressions.DefaultExpressionEvaluationContext import io.micronaut.core.naming.NameUtils import io.micronaut.core.expressions.EvaluatedExpressionReference -import io.micronaut.expressions.context.ExpressionEvaluationContextRegistrar import io.micronaut.inject.visitor.TypeElementVisitor import io.micronaut.inject.visitor.VisitorContext import org.intellij.lang.annotations.Language @@ -63,7 +62,7 @@ abstract class AbstractEvaluatedExpressionsSpec extends AbstractTypeElementSpec String exprFullName = exprClassName + i try { def exprClass = (AbstractEvaluatedExpression) classLoader.loadClass(exprFullName).newInstance() - result.add(exprClass.evaluate(new DefaultExpressionEvaluationContext(null, applicationContext, null))) + result.add(exprClass.evaluate(new DefaultExpressionEvaluationContext(null, null, applicationContext, null))) } catch (ClassNotFoundException e) { return null } @@ -99,7 +98,7 @@ abstract class AbstractEvaluatedExpressionsSpec extends AbstractTypeElementSpec try { def index = EvaluatedExpressionReference.nextIndex(exprFullName) def exprClass = (AbstractEvaluatedExpression) classLoader.loadClass(exprFullName + (index == 0 ? index : index - 1)).newInstance() - exprClass.evaluate(new DefaultExpressionEvaluationContext(null, applicationContext, null)) + exprClass.evaluate(new DefaultExpressionEvaluationContext(null, null, applicationContext, null)) } catch (ClassNotFoundException e) { return null } @@ -125,7 +124,7 @@ abstract class AbstractEvaluatedExpressionsSpec extends AbstractTypeElementSpec try { def index = EvaluatedExpressionReference.nextIndex(exprFullName) def exprClass = (AbstractEvaluatedExpression) classLoader.loadClass(exprFullName + (index == 0 ? index : index - 1)).newInstance() - return exprClass.evaluate(new DefaultExpressionEvaluationContext(args, applicationContext, null)) + return exprClass.evaluate(new DefaultExpressionEvaluationContext(null, args, applicationContext, null)) } catch (ClassNotFoundException e) { return null } diff --git a/inject-java-test/src/test/groovy/io/micronaut/inject/visitor/beans/BeanIntrospectionSpec.groovy b/inject-java-test/src/test/groovy/io/micronaut/inject/visitor/beans/BeanIntrospectionSpec.groovy index 621af43bc99..0518851fefe 100644 --- a/inject-java-test/src/test/groovy/io/micronaut/inject/visitor/beans/BeanIntrospectionSpec.groovy +++ b/inject-java-test/src/test/groovy/io/micronaut/inject/visitor/beans/BeanIntrospectionSpec.groovy @@ -12,6 +12,7 @@ import io.micronaut.annotation.processing.test.JavaParser import io.micronaut.context.ApplicationContext import io.micronaut.context.annotation.Executable import io.micronaut.context.annotation.Replaces +import io.micronaut.context.annotation.Value import io.micronaut.context.visitor.ConfigurationReaderVisitor import io.micronaut.core.annotation.Introspected import io.micronaut.core.beans.BeanIntrospection @@ -26,6 +27,7 @@ import io.micronaut.core.reflect.exception.InstantiationException import io.micronaut.core.type.Argument import io.micronaut.core.type.GenericPlaceholder import io.micronaut.inject.ExecutableMethod +import io.micronaut.inject.annotation.EvaluatedAnnotationMetadata import io.micronaut.inject.beans.visitor.IntrospectedTypeElementVisitor import io.micronaut.inject.visitor.TypeElementVisitor import io.micronaut.jackson.modules.BeanIntrospectionModule @@ -50,6 +52,47 @@ import java.time.Instant class BeanIntrospectionSpec extends AbstractTypeElementSpec { + void "test expressions in introspection properties"() { + given: + def introspection = buildBeanIntrospection('mixed.Test', ''' +package mixed; + +import io.micronaut.core.annotation.Introspected; +import io.micronaut.context.annotation.Value; +import io.micronaut.core.annotation.Nullable; +import java.util.Optional; +import java.lang.annotation.*; + +@Introspected +class Test { + @Nullable + @Ann("#{'test'}") + private String foo; + public String getFoo() { + return foo; + } + public void setFoo(@Nullable String foo) { + this.foo = foo; + } +} + +@Retention(RetentionPolicy.RUNTIME) +@Documented +@interface Ann { + String value(); +} +''') + when: + def test = introspection.instantiate() + def prop = introspection.getRequiredProperty("foo", String) + test.foo = 'value' + + then: 'expressions can be retrieved' + prop.get(test) == 'value' + prop.getAnnotationMetadata() instanceof EvaluatedAnnotationMetadata + prop.stringValue("mixed.Ann").get() == 'test' + } + void "test mix getter and setter with interface type"() { def introspection = buildBeanIntrospection('mixed.Pet', ''' package mixed; diff --git a/inject-java/src/main/java/io/micronaut/annotation/processing/BeanDefinitionInjectProcessor.java b/inject-java/src/main/java/io/micronaut/annotation/processing/BeanDefinitionInjectProcessor.java index e88aba3522a..5022ec5b5b1 100644 --- a/inject-java/src/main/java/io/micronaut/annotation/processing/BeanDefinitionInjectProcessor.java +++ b/inject-java/src/main/java/io/micronaut/annotation/processing/BeanDefinitionInjectProcessor.java @@ -283,7 +283,7 @@ private void processBeanDefinitions(BeanDefinitionVisitor beanDefinitionWriter) if (beanDefinitionWriter.isEnabled()) { beanDefinitionWriter.accept(classWriterOutputVisitor); BeanDefinitionReferenceWriter beanDefinitionReferenceWriter = - new BeanDefinitionReferenceWriter(beanDefinitionWriter); + new BeanDefinitionReferenceWriter(beanDefinitionWriter, this.javaVisitorContext); beanDefinitionReferenceWriter.setRequiresMethodProcessing(beanDefinitionWriter.requiresMethodProcessing()); String className = beanDefinitionReferenceWriter.getBeanDefinitionQualifiedClassName(); diff --git a/inject-java/src/main/java/io/micronaut/annotation/processing/PackageConfigurationInjectProcessor.java b/inject-java/src/main/java/io/micronaut/annotation/processing/PackageConfigurationInjectProcessor.java index e8f0a283f03..f75f25c26b9 100644 --- a/inject-java/src/main/java/io/micronaut/annotation/processing/PackageConfigurationInjectProcessor.java +++ b/inject-java/src/main/java/io/micronaut/annotation/processing/PackageConfigurationInjectProcessor.java @@ -91,7 +91,8 @@ public Object visitPackage(PackageElement packageElement, Object p) { BeanConfigurationWriter writer = new BeanConfigurationWriter( packageName, javaPackageElement, - javaPackageElement.getAnnotationMetadata() + javaPackageElement.getAnnotationMetadata(), + javaVisitorContext ); try { writer.accept(classWriterOutputVisitor); diff --git a/inject-java/src/test/groovy/io/micronaut/expressions/ThisExpressionSpec.groovy b/inject-java/src/test/groovy/io/micronaut/expressions/ThisExpressionSpec.groovy new file mode 100644 index 00000000000..1e8c210a970 --- /dev/null +++ b/inject-java/src/test/groovy/io/micronaut/expressions/ThisExpressionSpec.groovy @@ -0,0 +1,40 @@ +package io.micronaut.expressions + +import io.micronaut.annotation.processing.test.AbstractEvaluatedExpressionsSpec +import spock.lang.PendingFeature + +class ThisExpressionSpec extends AbstractEvaluatedExpressionsSpec { + @PendingFeature(reason = "At some point it would be nice to support resolving this in injection points but requires signficant changes") + void "test this access for field"() { + given: + def ctx = buildContext(""" + package test; + + import io.micronaut.context.annotation.Value; + + @jakarta.inject.Singleton + class Expr { + + @Value("#{ 'test1' }") + private String firstValue; + + @Value("#{ this.firstValue + 'ok' }") + public String secondValue; + + public String getFirstValue() { + return firstValue; + } + } + """) + + def type = ctx.classLoader.loadClass('test.Expr') + def bean = ctx.getBean(type) + + expect: + bean.firstValue == 'test1' + bean.secondValue == 'test1ok' + + cleanup: + ctx.close() + } +} diff --git a/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/beans/BeanDefinitionProcessor.kt b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/beans/BeanDefinitionProcessor.kt index 72aa8554478..fac8bb69406 100644 --- a/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/beans/BeanDefinitionProcessor.kt +++ b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/beans/BeanDefinitionProcessor.kt @@ -142,7 +142,7 @@ internal class BeanDefinitionProcessor(private val environment: SymbolProcessorE beanDefinitionWriter.visitBeanDefinitionEnd() if (beanDefinitionWriter.isEnabled) { beanDefinitionWriter.accept(outputVisitor) - val beanDefinitionReferenceWriter = BeanDefinitionReferenceWriter(beanDefinitionWriter) + val beanDefinitionReferenceWriter = BeanDefinitionReferenceWriter(beanDefinitionWriter, visitorContext) beanDefinitionReferenceWriter.setRequiresMethodProcessing(beanDefinitionWriter.requiresMethodProcessing()) val className = beanDefinitionReferenceWriter.beanDefinitionQualifiedClassName processed.add(className) diff --git a/inject/src/main/java/io/micronaut/context/expressions/ConfigurableExpressionEvaluationContext.java b/inject/src/main/java/io/micronaut/context/expressions/ConfigurableExpressionEvaluationContext.java index dec168cacb9..d0bba12cb78 100644 --- a/inject/src/main/java/io/micronaut/context/expressions/ConfigurableExpressionEvaluationContext.java +++ b/inject/src/main/java/io/micronaut/context/expressions/ConfigurableExpressionEvaluationContext.java @@ -38,7 +38,19 @@ public sealed interface ConfigurableExpressionEvaluationContext extends Expressi * @return evaluation context which arguments can be used in evaluation. */ @NonNull - ConfigurableExpressionEvaluationContext setArguments(@Nullable Object[] args); + default ConfigurableExpressionEvaluationContext withArguments(@Nullable Object[] args) { + return withArguments(null, args); + } + + /** + * Set arguments passed to invoked method. + * + * @param thisObject In the case of non-static methods the object that represents this object + * @param args method arguments + * @return evaluation context which arguments can be used in evaluation. + */ + @NonNull + ConfigurableExpressionEvaluationContext withArguments(@Nullable Object thisObject, @Nullable Object[] args); /** * Set bean owning evaluated expression. @@ -47,7 +59,7 @@ public sealed interface ConfigurableExpressionEvaluationContext extends Expressi * @return evaluation context aware of owning bean. */ @NonNull - ConfigurableExpressionEvaluationContext setOwningBean(@Nullable BeanDefinition beanDefinition); + ConfigurableExpressionEvaluationContext withOwningBean(@Nullable BeanDefinition beanDefinition); /** * Set context in which expression is evaluated. @@ -56,5 +68,5 @@ public sealed interface ConfigurableExpressionEvaluationContext extends Expressi * @return evaluation context aware of bean context. */ @NonNull - ConfigurableExpressionEvaluationContext setBeanContext(@Nullable BeanContext beanContext); + ConfigurableExpressionEvaluationContext withBeanContext(@Nullable BeanContext beanContext); } diff --git a/inject/src/main/java/io/micronaut/context/expressions/DefaultExpressionEvaluationContext.java b/inject/src/main/java/io/micronaut/context/expressions/DefaultExpressionEvaluationContext.java index 09e0e11c8bd..780cd7d0f54 100644 --- a/inject/src/main/java/io/micronaut/context/expressions/DefaultExpressionEvaluationContext.java +++ b/inject/src/main/java/io/micronaut/context/expressions/DefaultExpressionEvaluationContext.java @@ -39,6 +39,7 @@ @Internal public final class DefaultExpressionEvaluationContext implements ConfigurableExpressionEvaluationContext { + private final Object thisObject; private final Object[] args; private final BeanContext beanContext; private final BeanDefinition owningBean; @@ -46,38 +47,59 @@ public final class DefaultExpressionEvaluationContext implements ConfigurableExp private BeanResolutionContext resolutionContext; public DefaultExpressionEvaluationContext() { - this(null, null, null); + this(null, null, null, null); } - public DefaultExpressionEvaluationContext(@Nullable Object[] args, + public DefaultExpressionEvaluationContext(@Nullable Object thisObject, @Nullable Object[] args, @Nullable BeanContext beanContext, @Nullable BeanDefinition owningBean) { + this.thisObject = thisObject; this.args = args; this.beanContext = beanContext; this.owningBean = owningBean; } @Override - public ConfigurableExpressionEvaluationContext setArguments(Object[] args) { - DefaultExpressionEvaluationContext evaluationContext = new DefaultExpressionEvaluationContext(args, this.beanContext, this.owningBean); + public ConfigurableExpressionEvaluationContext withArguments(Object thisObject, Object[] args) { + DefaultExpressionEvaluationContext evaluationContext = new DefaultExpressionEvaluationContext( + thisObject, args, + this.beanContext, + this.owningBean + ); evaluationContext.resolutionContext = resolutionContext; return evaluationContext; } @Override - public ConfigurableExpressionEvaluationContext setOwningBean(BeanDefinition beanDefinition) { - DefaultExpressionEvaluationContext evaluationContext = new DefaultExpressionEvaluationContext(this.args, this.beanContext, beanDefinition); + public ConfigurableExpressionEvaluationContext withOwningBean(BeanDefinition beanDefinition) { + DefaultExpressionEvaluationContext evaluationContext = new DefaultExpressionEvaluationContext( + thisObject, this.args, + this.beanContext, + beanDefinition + ); evaluationContext.resolutionContext = resolutionContext; return evaluationContext; } @Override - public ConfigurableExpressionEvaluationContext setBeanContext(BeanContext beanContext) { - DefaultExpressionEvaluationContext evaluationContext = new DefaultExpressionEvaluationContext(this.args, beanContext, this.owningBean); + public ConfigurableExpressionEvaluationContext withBeanContext(BeanContext beanContext) { + DefaultExpressionEvaluationContext evaluationContext = new DefaultExpressionEvaluationContext( + thisObject, this.args, + beanContext, + this.owningBean + ); evaluationContext.resolutionContext = resolutionContext; return evaluationContext; } + @Override + public Object getThis() { + if (thisObject == null) { + throw new ExpressionEvaluationException("Current resolve 'this' within expression context. Expressions that resolve 'this' should be executed in a non-static context."); + } + return thisObject; + } + @Override public Object getArgument(int index) { if (args == null || args.length == 0 || args.length < index) { diff --git a/inject/src/main/java/io/micronaut/inject/annotation/EvaluatedAnnotationMetadata.java b/inject/src/main/java/io/micronaut/inject/annotation/EvaluatedAnnotationMetadata.java index 9edcfe0f913..e40e4004f28 100644 --- a/inject/src/main/java/io/micronaut/inject/annotation/EvaluatedAnnotationMetadata.java +++ b/inject/src/main/java/io/micronaut/inject/annotation/EvaluatedAnnotationMetadata.java @@ -23,6 +23,7 @@ import io.micronaut.core.annotation.AnnotationMetadata; import io.micronaut.core.annotation.AnnotationValue; import io.micronaut.core.annotation.Experimental; +import io.micronaut.core.annotation.Nullable; import io.micronaut.inject.BeanDefinition; import java.lang.annotation.Annotation; @@ -50,24 +51,25 @@ private EvaluatedAnnotationMetadata(AnnotationMetadata targetMetadata, /** * Provide a copy of this annotation metadata with passed method arguments. * + * @param thisObject The object that represent this object * @param args arguments passed to method * @return copy of annotation metadata */ - public EvaluatedAnnotationMetadata withArguments(Object[] args) { + public EvaluatedAnnotationMetadata withArguments(@Nullable Object thisObject, Object[] args) { return new EvaluatedAnnotationMetadata( delegateAnnotationMetadata, - evaluationContext.setArguments(args) + evaluationContext.withArguments(thisObject, args) ); } @Override public void configure(BeanContext context) { - evaluationContext = evaluationContext.setBeanContext(context); + evaluationContext = evaluationContext.withBeanContext(context); } @Override public void setBeanDefinition(BeanDefinition beanDefinition) { - evaluationContext = evaluationContext.setOwningBean(beanDefinition); + evaluationContext = evaluationContext.withOwningBean(beanDefinition); } public static AnnotationMetadata wrapIfNecessary(AnnotationMetadata targetMetadata) { diff --git a/inject/src/main/java/io/micronaut/inject/annotation/EvaluatedAnnotationValue.java b/inject/src/main/java/io/micronaut/inject/annotation/EvaluatedAnnotationValue.java index 89387ea52e9..938207f52e2 100644 --- a/inject/src/main/java/io/micronaut/inject/annotation/EvaluatedAnnotationValue.java +++ b/inject/src/main/java/io/micronaut/inject/annotation/EvaluatedAnnotationValue.java @@ -18,6 +18,7 @@ import io.micronaut.context.expressions.ConfigurableExpressionEvaluationContext; import io.micronaut.core.annotation.AnnotationValue; import io.micronaut.core.annotation.Experimental; +import io.micronaut.core.annotation.Nullable; import io.micronaut.core.expressions.EvaluatedExpression; import java.lang.annotation.Annotation; @@ -52,12 +53,13 @@ public final class EvaluatedAnnotationValue extends Annota /** * Provide a copy of this annotation metadata with passed method arguments. * + * @param thisObject The object that represents this in a non-static context. * @param args arguments passed to method * @return copy of annotation metadata */ - public EvaluatedAnnotationValue withArguments(Object[] args) { + public EvaluatedAnnotationValue withArguments(@Nullable Object thisObject, Object[] args) { return new EvaluatedAnnotationValue<>( annotationValue, - evaluationContext.setArguments(args)); + evaluationContext.withArguments(thisObject, args)); } } diff --git a/inject/src/main/java/io/micronaut/inject/annotation/MappingAnnotationMetadataDelegate.java b/inject/src/main/java/io/micronaut/inject/annotation/MappingAnnotationMetadataDelegate.java index 73839203225..4b9ed37469f 100644 --- a/inject/src/main/java/io/micronaut/inject/annotation/MappingAnnotationMetadataDelegate.java +++ b/inject/src/main/java/io/micronaut/inject/annotation/MappingAnnotationMetadataDelegate.java @@ -17,6 +17,7 @@ import io.micronaut.core.annotation.AnnotationMetadataDelegate; import io.micronaut.core.annotation.AnnotationValue; +import io.micronaut.core.annotation.Experimental; import io.micronaut.core.reflect.ReflectionUtils; import io.micronaut.core.type.Argument; import io.micronaut.core.util.StringUtils; @@ -40,7 +41,8 @@ * @since 4.0.0 * @author Sergey Gavrilov */ -public abstract class MappingAnnotationMetadataDelegate implements AnnotationMetadataDelegate { +@Experimental +public abstract sealed class MappingAnnotationMetadataDelegate implements AnnotationMetadataDelegate permits EvaluatedAnnotationMetadata { public abstract AnnotationValue mapAnnotationValue(AnnotationValue av); @@ -352,7 +354,11 @@ public Map getValues(String annotation) { @Override public AnnotationValue getDeclaredAnnotation(Class annotationClass) { - return AnnotationMetadataDelegate.super.getDeclaredAnnotation(annotationClass); + AnnotationValue av = getAnnotationMetadata().getDeclaredAnnotation(annotationClass); + if (av != null) { + return mapAnnotationValue(av); + } + return null; } @Override @@ -362,13 +368,21 @@ public AnnotationValue getAnnotation(Class annotati @Override public AnnotationValue getAnnotation(String annotation) { - return this.findAnnotation(annotation).orElse(null); + AnnotationValue av = getAnnotationMetadata().getAnnotation(annotation); + if (av != null) { + return mapAnnotationValue(av); + } + return null; } @Override public Optional> findAnnotation(String annotation) { - Optional> av = getAnnotationMetadata().findAnnotation(annotation); - return av.map(this::mapAnnotationValue); + AnnotationValue av = getAnnotationMetadata().getAnnotation(annotation); + if (av != null) { + //noinspection unchecked + return Optional.of((AnnotationValue) mapAnnotationValue(av)); + } + return Optional.empty(); } @Override @@ -419,6 +433,24 @@ public T synthesize(Class annotationClass) { .orElse(null); } + @Override + public T synthesize(Class annotationClass, String sourceAnnotation) { + AnnotationValue av = getAnnotation(sourceAnnotation); + if (av != null) { + return AnnotationMetadataSupport.buildAnnotation(annotationClass, av); + } + return null; + } + + @Override + public T synthesizeDeclared(Class annotationClass, String sourceAnnotation) { + AnnotationValue av = getDeclaredAnnotation(sourceAnnotation); + if (av != null) { + return AnnotationMetadataSupport.buildAnnotation(annotationClass, av); + } + return null; + } + @Override public List> getAnnotationValuesByType(Class annotationType) { return getAnnotationValues(() -> getAnnotationMetadata().getAnnotationValuesByType(annotationType)); diff --git a/inject/src/main/java/io/micronaut/inject/beans/AbstractInitializableBeanIntrospection.java b/inject/src/main/java/io/micronaut/inject/beans/AbstractInitializableBeanIntrospection.java index fff93d3135e..fee1b2e0148 100644 --- a/inject/src/main/java/io/micronaut/inject/beans/AbstractInitializableBeanIntrospection.java +++ b/inject/src/main/java/io/micronaut/inject/beans/AbstractInitializableBeanIntrospection.java @@ -32,6 +32,7 @@ import io.micronaut.core.type.ReturnType; import io.micronaut.core.util.ArgumentUtils; import io.micronaut.inject.ExecutableMethod; +import io.micronaut.inject.annotation.EvaluatedAnnotationMetadata; import java.lang.annotation.Annotation; import java.lang.reflect.Method; @@ -66,15 +67,15 @@ public abstract class AbstractInitializableBeanIntrospection implements BeanI private BeanConstructor beanConstructor; - public AbstractInitializableBeanIntrospection(Class beanType, + protected AbstractInitializableBeanIntrospection(Class beanType, AnnotationMetadata annotationMetadata, AnnotationMetadata constructorAnnotationMetadata, Argument[] constructorArguments, BeanPropertyRef[] propertiesRefs, BeanMethodRef[] methodsRefs) { this.beanType = beanType; - this.annotationMetadata = annotationMetadata == null ? AnnotationMetadata.EMPTY_METADATA : annotationMetadata; - this.constructorAnnotationMetadata = constructorAnnotationMetadata == null ? AnnotationMetadata.EMPTY_METADATA : constructorAnnotationMetadata; + this.annotationMetadata = annotationMetadata == null ? AnnotationMetadata.EMPTY_METADATA : EvaluatedAnnotationMetadata.wrapIfNecessary(annotationMetadata); + this.constructorAnnotationMetadata = constructorAnnotationMetadata == null ? AnnotationMetadata.EMPTY_METADATA : EvaluatedAnnotationMetadata.wrapIfNecessary(constructorAnnotationMetadata); this.constructorArguments = constructorArguments == null ? Argument.ZERO_ARGUMENTS : constructorArguments; if (propertiesRefs != null) { List> beanProperties = new ArrayList<>(propertiesRefs.length); @@ -408,10 +409,12 @@ private final class BeanPropertyImpl

implements UnsafeBeanProperty { private final BeanPropertyRef

ref; private final Class typeOrWrapperType; + private final AnnotationMetadata annotationMetadata; private BeanPropertyImpl(BeanPropertyRef

ref) { this.ref = ref; this.typeOrWrapperType = ReflectionUtils.getWrapperType(getType()); + this.annotationMetadata = EvaluatedAnnotationMetadata.wrapIfNecessary(ref.argument.getAnnotationMetadata()); } @NonNull @@ -440,7 +443,7 @@ public BeanIntrospection getDeclaringBean() { @Override public AnnotationMetadata getAnnotationMetadata() { - return ref.argument.getAnnotationMetadata(); + return annotationMetadata; } @Nullable @@ -574,7 +577,7 @@ public Map> getTypeVariables() { @NonNull @Override public AnnotationMetadata getAnnotationMetadata() { - return ref.returnType.getAnnotationMetadata(); + return EvaluatedAnnotationMetadata.wrapIfNecessary(ref.returnType.getAnnotationMetadata()); } }; } @@ -678,7 +681,7 @@ public BeanMethodRef(@NonNull Argument

returnType, int methodIndex) { this.returnType = returnType; this.name = name; - this.annotationMetadata = annotationMetadata; + this.annotationMetadata = EvaluatedAnnotationMetadata.wrapIfNecessary(annotationMetadata); this.arguments = arguments; this.methodIndex = methodIndex; } diff --git a/src/main/docs/guide/config/evaluatedExpressions.adoc b/src/main/docs/guide/config/evaluatedExpressions.adoc index e96234da39f..b2c9b9b1811 100644 --- a/src/main/docs/guide/config/evaluatedExpressions.adoc +++ b/src/main/docs/guide/config/evaluatedExpressions.adoc @@ -48,7 +48,7 @@ Expressions can be used anywhere throughout the Micronaut framework and associat snippet::io.micronaut.docs.expressions.ExampleJob[title="Job Control with Expressions"] <1> Here the `condition` member of the ann:scheduling.annotation.Scheduled[] annotation is used to only execute the job if a pre-condition is met. -<2> The `condition` invokes a method of a parameter which is another bean called `ExampleJobControl` which can be used to pause and resume execution of the job as desired. +<2> The `condition` invokes a method of the type that checks if the job is paused. Other methods can be used to pause and resume execution of the job as desired. TIP: You can also use expressions to perform conditional routing using the ann:http.annotation.RouteCondition[] annotation. diff --git a/test-suite-groovy/src/test/groovy/io/micronaut/docs/expressions/ExampleJob.groovy b/test-suite-groovy/src/test/groovy/io/micronaut/docs/expressions/ExampleJob.groovy index 0b239f5e2d0..2eeb97eab2e 100644 --- a/test-suite-groovy/src/test/groovy/io/micronaut/docs/expressions/ExampleJob.groovy +++ b/test-suite-groovy/src/test/groovy/io/micronaut/docs/expressions/ExampleJob.groovy @@ -5,28 +5,20 @@ import jakarta.inject.Singleton @Singleton class ExampleJob { + boolean paused = true // <2> private boolean jobRan = false @Scheduled( fixedRate = "1s", - condition = '#{!jobControl.paused}') // <1> - void run(ExampleJobControl jobControl) { - System.out.println("Job Running") + condition = '#{!this.paused}') // <1> + void run() { + println("Job Running") this.jobRan = true } boolean hasJobRun() { return jobRan } -} - -@Singleton -class ExampleJobControl { // <2> - private boolean paused = true - - boolean isPaused() { - return paused - } void unpause() { paused = false @@ -36,3 +28,4 @@ class ExampleJobControl { // <2> paused = true } } + diff --git a/test-suite-groovy/src/test/groovy/io/micronaut/docs/expressions/ExampleJobSpec.groovy b/test-suite-groovy/src/test/groovy/io/micronaut/docs/expressions/ExampleJobSpec.groovy index 7646d09fb78..ce4b06fe3c0 100644 --- a/test-suite-groovy/src/test/groovy/io/micronaut/docs/expressions/ExampleJobSpec.groovy +++ b/test-suite-groovy/src/test/groovy/io/micronaut/docs/expressions/ExampleJobSpec.groovy @@ -16,10 +16,9 @@ class ExampleJobSpec extends Specification { void testJobCondition(){ given: ExampleJob exampleJob = ctx.getBean(ExampleJob) - ExampleJobControl jobControl = ctx.getBean(ExampleJobControl) expect: - jobControl.isPaused() + exampleJob.isPaused() !exampleJob.hasJobRun() when: @@ -29,7 +28,7 @@ class ExampleJobSpec extends Specification { !exampleJob.hasJobRun() when: - jobControl.unpause() + exampleJob.unpause() then: await().atMost(3, SECONDS).until(exampleJob::hasJobRun) diff --git a/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/expressions/ExampleJob.kt b/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/expressions/ExampleJob.kt index 923fe8bcea3..9bb74b0d1b3 100644 --- a/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/expressions/ExampleJob.kt +++ b/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/expressions/ExampleJob.kt @@ -5,11 +5,12 @@ import jakarta.inject.Singleton @Singleton class ExampleJob { + var paused = true private var jobRan = false @Scheduled( fixedRate = "1s", - condition = "#{!jobControl.paused}") // <1> - fun run(jobControl: ExampleJobControl) { + condition = "#{!this.paused}") // <1> + fun run() { println("Job Running") jobRan = true } @@ -17,11 +18,6 @@ class ExampleJob { fun hasJobRun(): Boolean { return jobRan } -} - -@Singleton -class ExampleJobControl { // <2> - var paused = true fun unpause() { paused = false diff --git a/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/expressions/ExampleJobTest.kt b/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/expressions/ExampleJobTest.kt index 21620a078fa..2e29b86345c 100644 --- a/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/expressions/ExampleJobTest.kt +++ b/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/expressions/ExampleJobTest.kt @@ -1,21 +1,21 @@ package io.micronaut.docs.expressions import io.micronaut.test.extensions.junit5.annotation.MicronautTest +import org.awaitility.Awaitility import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.Test -import java.util.concurrent.Callable import java.util.concurrent.TimeUnit @MicronautTest class ExampleJobTest { @Test - fun testJobCondition(exampleJob: ExampleJob, exampleJobControl: ExampleJobControl) { - Assertions.assertTrue(exampleJobControl.paused) + fun testJobCondition(exampleJob: ExampleJob) { + Assertions.assertTrue(exampleJob.paused) Assertions.assertFalse(exampleJob.hasJobRun()) Thread.sleep(5000) Assertions.assertFalse(exampleJob.hasJobRun()) - exampleJobControl.unpause() - org.awaitility.Awaitility.await().atMost(3, TimeUnit.SECONDS).until(Callable { exampleJob.hasJobRun() }) + exampleJob.unpause() + Awaitility.await().atMost(3, TimeUnit.SECONDS).until { exampleJob.hasJobRun() } Assertions.assertTrue(exampleJob.hasJobRun()) } } diff --git a/test-suite/src/test/java/io/micronaut/docs/expressions/ExampleJob.java b/test-suite/src/test/java/io/micronaut/docs/expressions/ExampleJob.java index fb2e180d9b1..4d2c94dd075 100644 --- a/test-suite/src/test/java/io/micronaut/docs/expressions/ExampleJob.java +++ b/test-suite/src/test/java/io/micronaut/docs/expressions/ExampleJob.java @@ -6,26 +6,23 @@ @Singleton public class ExampleJob { private boolean jobRan = false; + private boolean paused = true; + @Scheduled( fixedRate = "1s", - condition = "#{!jobControl.paused}") // <1> - void run(ExampleJobControl jobControl) { + condition = "#{!this.paused}") // <1> + void run() { System.out.println("Job Running"); this.jobRan = true; } - public boolean hasJobRun() { - return jobRan; - } -} - -@Singleton -class ExampleJobControl { // <2> - private boolean paused = true; - public boolean isPaused() { return paused; + } // <2> + + public boolean hasJobRun() { + return jobRan; } public void unpause() { @@ -35,4 +32,6 @@ public void unpause() { public void pause() { paused = true; } + } + diff --git a/test-suite/src/test/java/io/micronaut/docs/expressions/ExampleJobTest.java b/test-suite/src/test/java/io/micronaut/docs/expressions/ExampleJobTest.java index e061d3db904..d1f296464a7 100644 --- a/test-suite/src/test/java/io/micronaut/docs/expressions/ExampleJobTest.java +++ b/test-suite/src/test/java/io/micronaut/docs/expressions/ExampleJobTest.java @@ -13,12 +13,12 @@ public class ExampleJobTest { @Test - void testJobCondition(ExampleJob exampleJob, ExampleJobControl jobControl) throws InterruptedException { - assertTrue(jobControl.isPaused()); + void testJobCondition(ExampleJob exampleJob) throws InterruptedException { + assertTrue(exampleJob.isPaused()); assertFalse(exampleJob.hasJobRun()); Thread.sleep(5000); assertFalse(exampleJob.hasJobRun()); - jobControl.unpause(); + exampleJob.unpause(); await().atMost(3, SECONDS).until(exampleJob::hasJobRun); assertTrue(exampleJob.hasJobRun()); } From dedfdd5c6a205a3d02e56acaa4752df8c8302e4a Mon Sep 17 00:00:00 2001 From: micronaut-build Date: Mon, 17 Apr 2023 16:19:51 +0000 Subject: [PATCH 713/743] [skip ci] Release v3.9.0 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 23c1dfcae93..e23c60e13dd 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -projectVersion=3.9.0-SNAPSHOT +projectVersion=3.9.0 projectGroupId=io.micronaut title=Micronaut projectDesc=Natively Cloud Native From 277e6db45ef2088a1905bc02b578c6217081acf0 Mon Sep 17 00:00:00 2001 From: micronaut-build Date: Mon, 17 Apr 2023 16:35:17 +0000 Subject: [PATCH 714/743] Back to 3.9.1-SNAPSHOT --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index e23c60e13dd..0f88b3e7296 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -projectVersion=3.9.0 +projectVersion=3.9.1-SNAPSHOT projectGroupId=io.micronaut title=Micronaut projectDesc=Natively Cloud Native From 70d7256cb99ed055fd46eaa8a260974c7dc46bd6 Mon Sep 17 00:00:00 2001 From: micronaut-build <65172877+micronaut-build@users.noreply.github.com> Date: Tue, 18 Apr 2023 10:54:15 +0200 Subject: [PATCH 715/743] Update common files (#9116) --- gradle/wrapper/gradle-wrapper.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index bdc9a83b1e6..0c85a1f7519 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.0.2-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.1-bin.zip networkTimeout=10000 zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists From c524d44977cc64dc06ca5d1abd5c2b746dd6e110 Mon Sep 17 00:00:00 2001 From: Andriy Dmytruk <80816836+andriy-dmytruk@users.noreply.github.com> Date: Tue, 18 Apr 2023 05:05:48 -0400 Subject: [PATCH 716/743] Stop adding all the annotations from class that uses @Introspected(classes=) to the classes that it imports (#9039) --- .../beans/visitor/IntrospectedTypeElementVisitor.java | 6 +----- .../java/io/micronaut/core/annotation/Introspected.java | 2 +- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/core-processor/src/main/java/io/micronaut/inject/beans/visitor/IntrospectedTypeElementVisitor.java b/core-processor/src/main/java/io/micronaut/inject/beans/visitor/IntrospectedTypeElementVisitor.java index 1d8478a793c..9377fe615e0 100644 --- a/core-processor/src/main/java/io/micronaut/inject/beans/visitor/IntrospectedTypeElementVisitor.java +++ b/core-processor/src/main/java/io/micronaut/inject/beans/visitor/IntrospectedTypeElementVisitor.java @@ -100,16 +100,12 @@ private void processIntrospected(ClassElement element, VisitorContext context, A if (isIntrospected(context, ce)) { return; } - final AnnotationMetadata typeMetadata = ce.getAnnotationMetadata(); - final AnnotationMetadata resolvedMetadata = typeMetadata == AnnotationMetadata.EMPTY_METADATA - ? element.getAnnotationMetadata() - : new AnnotationMetadataHierarchy(element.getAnnotationMetadata(), typeMetadata); final BeanIntrospectionWriter writer = new BeanIntrospectionWriter( element.getName(), index.getAndIncrement(), element, ce, - metadata ? resolvedMetadata : null, + metadata ? ce.getAnnotationMetadata() : null, context ); diff --git a/core/src/main/java/io/micronaut/core/annotation/Introspected.java b/core/src/main/java/io/micronaut/core/annotation/Introspected.java index 6cd213e4fc6..3be38b5b721 100644 --- a/core/src/main/java/io/micronaut/core/annotation/Introspected.java +++ b/core/src/main/java/io/micronaut/core/annotation/Introspected.java @@ -67,7 +67,7 @@ Introspected.Visibility[] DEFAULT_VISIBILITY = {Introspected.Visibility.DEFAULT}; /** - * By default {@link Introspected} applies to the class it is applied on. However if classes are specified + * By default {@link Introspected} applies to the class it is applied on. However, if classes are specified * introspections will instead be generated for each class specified. This is useful in cases where you cannot * alter the source code and wish to generate introspections for already compiled classes. * From 4d38833431a2a370d178d6827947e1130f7a871d Mon Sep 17 00:00:00 2001 From: micronaut-build <65172877+micronaut-build@users.noreply.github.com> Date: Tue, 18 Apr 2023 11:51:19 +0200 Subject: [PATCH 717/743] Bump micronaut-maven-plugin to 4.0.0-M1 (#9108) --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index dbc16a1f447..1e799f7a088 100644 --- a/gradle.properties +++ b/gradle.properties @@ -47,7 +47,7 @@ projectUrl=https://micronaut.io developers=Graeme Rocher # Dependency Versions -micronautMavenPluginVersion=3.5.3 +micronautMavenPluginVersion=4.0.0-M1 chromedriverVersion=79.0.3945.36 geckodriverVersion=0.26.0 webdriverBinariesVersion=1.4 From 1bdef20af32a18614565990aff16e36a364d8b23 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 18 Apr 2023 19:02:32 +0200 Subject: [PATCH 718/743] fix(deps): update managed-reactor to v3.5.5 (#9127) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index a1bddd3c574..74c47faebeb 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -70,7 +70,7 @@ managed-netty = "4.1.91.Final" managed-netty-http3 = "0.0.16.Final" managed-reactive-streams = "1.0.4" # This should be kept aligned with https://github.com/micronaut-projects/micronaut-reactor/blob/master/gradle.properties from the BOM -managed-reactor = "3.4.24" +managed-reactor = "3.5.5" managed-slf4j = "2.0.7" managed-snakeyaml = "2.0" managed-java-parser-core = "3.24.9" From a002ccb5ebe3f64c1cc966ad33b1a08d9abebca2 Mon Sep 17 00:00:00 2001 From: Graeme Rocher Date: Thu, 20 Apr 2023 06:43:48 +0200 Subject: [PATCH 719/743] Fix SingleResult was unintentionally initialized at build time (#9126) --- .../io.micronaut/micronaut-core-reactive/native-image.properties | 1 + 1 file changed, 1 insertion(+) create mode 100644 core-reactive/src/main/resources/META-INF/native-image/io.micronaut/micronaut-core-reactive/native-image.properties diff --git a/core-reactive/src/main/resources/META-INF/native-image/io.micronaut/micronaut-core-reactive/native-image.properties b/core-reactive/src/main/resources/META-INF/native-image/io.micronaut/micronaut-core-reactive/native-image.properties new file mode 100644 index 00000000000..e236b4f407a --- /dev/null +++ b/core-reactive/src/main/resources/META-INF/native-image/io.micronaut/micronaut-core-reactive/native-image.properties @@ -0,0 +1 @@ +Args = --initialize-at-build-time=io.micronaut.core.async.annotation From 062704715197a3c56ca91162fdbeef7a6d82ace0 Mon Sep 17 00:00:00 2001 From: Tim Yates Date: Thu, 20 Apr 2023 13:23:47 +0100 Subject: [PATCH 720/743] tck: Write length and first 10 bytes if bytearray assertion fails (#9111) * tck: Write length and first 10 bytes if bytearray assertion fails * Ignore flaky test * Ignore flaky test * Kick the CLA check which was asleep --- .../io/micronaut/http/tck/BodyAssertion.java | 23 +++++++++++++++++-- .../ArrayMethodsExpressionsSpec.groovy | 6 ++++- ...ontextPropertyAccessExpressionsSpec.groovy | 9 ++++---- 3 files changed, 31 insertions(+), 7 deletions(-) diff --git a/http-tck/src/main/java/io/micronaut/http/tck/BodyAssertion.java b/http-tck/src/main/java/io/micronaut/http/tck/BodyAssertion.java index fa4b0fea200..c4674e924ae 100644 --- a/http-tck/src/main/java/io/micronaut/http/tck/BodyAssertion.java +++ b/http-tck/src/main/java/io/micronaut/http/tck/BodyAssertion.java @@ -22,6 +22,8 @@ import java.util.Arrays; import java.util.function.BiPredicate; +import java.util.stream.Collectors; +import java.util.stream.IntStream; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -80,7 +82,7 @@ public Class getErrorType() { return errorType; } - private static enum EvaluatorType { + private enum EvaluatorType { EQUAL, CONTAIN, } @@ -108,8 +110,12 @@ private interface BodyEvaluator extends BiPredicate { EvaluatorType type(); + default String render(T value) { + return String.valueOf(value); + } + default String message(T expected, T actual) { - return "Expected received body of '" + actual + "' to " + type().name().toLowerCase() + " '" + expected + "'"; + return "Expected received body of '" + render(actual) + "' to " + type().name().toLowerCase() + " '" + render(expected) + "'"; } } @@ -200,6 +206,19 @@ public boolean test(String expected, String received) { private record ByteArrayEvaluator(EvaluatorType type) implements BodyEvaluator { + @Override + public String render(byte[] value) { + if (value == null) { + return "null"; + } + String firstTen = IntStream.range(0, value.length) + .map(i -> value[i] & 0xff) + .mapToObj(i -> String.format("%02x", i)) + .limit(10) + .collect(Collectors.joining(", ", "", "...")); + return "ByteArray(length=" + value.length + ", [" + firstTen + "])"; + } + @Override public boolean test(byte[] expected, byte[] received) { return switch (type) { diff --git a/inject-groovy/src/test/groovy/io/micronaut/expressions/ArrayMethodsExpressionsSpec.groovy b/inject-groovy/src/test/groovy/io/micronaut/expressions/ArrayMethodsExpressionsSpec.groovy index c835d893784..3cc775607f3 100644 --- a/inject-groovy/src/test/groovy/io/micronaut/expressions/ArrayMethodsExpressionsSpec.groovy +++ b/inject-groovy/src/test/groovy/io/micronaut/expressions/ArrayMethodsExpressionsSpec.groovy @@ -1,8 +1,12 @@ package io.micronaut.expressions import io.micronaut.ast.transform.test.AbstractEvaluatedExpressionsSpec +import spock.lang.Ignore - +@Ignore(''' + FLAKY: We already test the Java code-path, and this should be re-instated at some point, but until the flakiness is resolved we are disabling it. + It's our opinion that this is due to a Groovy bug where the classloader sometimes sees a different class depending on the order of the compilation. +''') class ArrayMethodsExpressionsSpec extends AbstractEvaluatedExpressionsSpec { void "test primitive and wrapper varargs methods"() { diff --git a/inject-groovy/src/test/groovy/io/micronaut/expressions/ContextPropertyAccessExpressionsSpec.groovy b/inject-groovy/src/test/groovy/io/micronaut/expressions/ContextPropertyAccessExpressionsSpec.groovy index 95308e9a16f..6acc5ac8f24 100644 --- a/inject-groovy/src/test/groovy/io/micronaut/expressions/ContextPropertyAccessExpressionsSpec.groovy +++ b/inject-groovy/src/test/groovy/io/micronaut/expressions/ContextPropertyAccessExpressionsSpec.groovy @@ -1,9 +1,12 @@ package io.micronaut.expressions import io.micronaut.ast.transform.test.AbstractEvaluatedExpressionsSpec -import io.micronaut.context.exceptions.ExpressionEvaluationException -import spock.lang.Ignore; +import spock.lang.Ignore +@Ignore(''' + FLAKY: We already test the Java code-path, and this should be re-instated at some point, but until the flakiness is resolved we are disabling it. + It's our opinion that this is due to a Groovy bug where the classloader sometimes sees a different class depending on the order of the compilation. +''') class ContextPropertyAccessExpressionsSpec extends AbstractEvaluatedExpressionsSpec { @Ignore("already tested in java and flakey in Groovy") @@ -59,6 +62,4 @@ class ContextPropertyAccessExpressionsSpec extends AbstractEvaluatedExpressionsS expr3 instanceof String && expr3 == "test value" expr4 instanceof String && expr4 == "custom property" } - - } From 046a374e17cf9fab5643f0a0ab74801ae94b858c Mon Sep 17 00:00:00 2001 From: Jonas Konrad Date: Thu, 20 Apr 2023 15:02:33 +0200 Subject: [PATCH 721/743] Replace RoutingInboundHandler and streams handlers by a single handler (#9128) This PR replaces the HttpStreamsServerHandler, HandlerPublisher, HttpRequestDecoder and HttpResponseDecoder by a new PipeliningServerHandler. PipeliningServerHandler directly receives netty HttpMessages, combines them into a FullHttpRequest or StreamedHttpRequest, and passes them on to a handler. It also takes care of pipelining, so that the responses for different requests are sent in the same order those requests came in, which fixes some old bugs in RoutingInboundHandler. PipeliningServerHandler optimizes the read and write operations. Concurrent reads are handled safely, removing the need for the FlowControlHandler even in the streaming case. Flushes are delayed until readComplete, so that multiple requests in the same TCP packet can be responded to in a single TCP packet as well. The biggest performance win comes from message aggregation, however. While PipeliningServerHandler can handle a FullHttpRequest from FULL_CONTENT, it can also do some basic aggregation on its own. Small requests commonly have the HttpRequest and HttpContent come in in the same read call. PipeliningServerHandler will "hold back" the HttpRequest until readComplete to merge it with the HttpContent if available, into a FullHttpRequest. This gives a performance benefit downstream, it's actually ~600ns faster in FullHttpStackBenchmark by default than 4.0.x with FULL_CONTENT. The big advantage over FULL_CONTENT is that this aggregation is always enabled and is not a compatibility issue. Large requests will still safely fall back to StreamedHttpRequest. FULL_CONTENT is still useful for customizers that want to observe the full HTTP message, however, and may give some perf benefit for medium-sized request bodies. The reason this PR is so big is that now, many small requests are processed using the FullHttpRequest APIs. There are many subtle differences in this code path. For example, form items are parsed immediately and then passed to FormRouteCompleter, where previously parsing and processing would be interleaved (the reactive code path). This exposed some bugs because it moves some `if (refCnt > 0) release` calls to before the processing. --- .../server/stack/FullHttpStackBenchmark.java | 3 +- .../http/client/netty/DefaultHttpClient.java | 96 +- .../client/netty/NettyClientHttpRequest.java | 15 + .../http/client/StreamRequestSpec.groovy | 8 +- .../http/netty/NettyHttpRequestBuilder.java | 112 ++- .../stream/DelegateStreamedHttpRequest.java | 4 +- .../http/netty/stream/EmptyHttpRequest.java | 4 +- .../stream/HttpStreamsServerHandler.java | 302 ------ .../netty/stream/StreamedHttpRequest.java | 4 +- http-server-netty/build.gradle | 1 + .../netty/AbstractHttpContentProcessor.java | 2 +- .../netty/DefaultHttpContentProcessor.java | 3 +- .../DefaultNettyEmbeddedServerFactory.java | 4 +- .../netty/DelegateNettyEmbeddedServices.java | 12 +- .../netty/DelegateStreamedHttpResponse.java | 4 +- .../netty/FormDataHttpContentProcessor.java | 3 +- .../http/server/netty/FormRouteCompleter.java | 55 +- ...tpContentProcessorAsReactiveProcessor.java | 1 + .../server/netty/HttpPipelineBuilder.java | 59 +- .../netty/HttpToHttpsRedirectHandler.java | 76 +- .../server/netty/NettyEmbeddedServices.java | 4 +- .../http/server/netty/NettyHttpRequest.java | 99 +- .../server/netty/NettyRequestLifecycle.java | 43 +- .../server/netty/RoutingInBoundHandler.java | 262 ++---- .../http/server/netty/StreamTypeHandler.java | 7 +- .../binders/PartUploadAnnotationBinder.java | 19 +- .../netty/binders/PublisherBodyBinder.java | 38 +- .../http/server/netty/body/ByteBody.java | 10 + .../server/netty/body/HttpBodyReused.java | 34 + .../server/netty/body/ImmediateByteBody.java | 30 +- .../server/netty/body/StreamingByteBody.java | 14 +- .../netty/body/StreamingMultiObjectBody.java | 5 +- .../netty/converters/NettyConverters.java | 53 ++ .../netty/decoders/HttpRequestDecoder.java | 122 --- .../netty/encoders/HttpResponseEncoder.java | 206 ----- .../handler/PipeliningServerHandler.java | 874 ++++++++++++++++++ .../server/netty/handler/RequestHandler.java | 64 ++ .../netty/jackson/JsonContentProcessor.java | 3 +- .../MultipartBodyArgumentBinder.java | 2 +- .../multipart/NettyStreamingFileUpload.java | 2 + .../ssl/HttpRequestCertificateHandler.java | 69 -- .../types/NettyCustomizableResponseType.java | 22 +- .../NettyCustomizableResponseTypeHandler.java | 7 +- .../netty/types/files/FileTypeHandler.java | 9 +- ...ttySystemFileCustomizableResponseType.java | 98 +- ...NettyStreamedCustomizableResponseType.java | 19 +- .../NettyServerWebSocketUpgradeHandler.java | 95 +- .../WebSocketUpgradeHandlerFactory.java | 19 +- .../binding/ConcurrentFormTransferSpec.groovy | 10 +- .../netty/fuzzing/FuzzyInputSpec.groovy | 2 +- .../PipeliningServerHandlerSpec.groovy | 217 +++++ .../handler/accesslog/AccessLogSpec.groovy | 16 +- .../server/netty/websocket/UpgradeSpec.groovy | 26 +- .../io/micronaut/http/HttpAttributes.java | 3 + .../java/io/micronaut/http/HttpRequest.java | 1 + .../netty/LogbookNettyServerCustomizer.kt | 2 +- .../netty/LogbookNettyServerCustomizer.kt | 2 +- .../netty/LogbookNettyServerCustomizer.java | 6 +- 58 files changed, 1983 insertions(+), 1299 deletions(-) delete mode 100644 http-netty/src/main/java/io/micronaut/http/netty/stream/HttpStreamsServerHandler.java create mode 100644 http-server-netty/src/main/java/io/micronaut/http/server/netty/body/HttpBodyReused.java delete mode 100644 http-server-netty/src/main/java/io/micronaut/http/server/netty/decoders/HttpRequestDecoder.java delete mode 100644 http-server-netty/src/main/java/io/micronaut/http/server/netty/encoders/HttpResponseEncoder.java create mode 100644 http-server-netty/src/main/java/io/micronaut/http/server/netty/handler/PipeliningServerHandler.java create mode 100644 http-server-netty/src/main/java/io/micronaut/http/server/netty/handler/RequestHandler.java delete mode 100644 http-server-netty/src/main/java/io/micronaut/http/server/netty/ssl/HttpRequestCertificateHandler.java create mode 100644 http-server-netty/src/test/groovy/io/micronaut/http/server/netty/handler/PipeliningServerHandlerSpec.groovy diff --git a/benchmarks/src/jmh/java/io/micronaut/http/server/stack/FullHttpStackBenchmark.java b/benchmarks/src/jmh/java/io/micronaut/http/server/stack/FullHttpStackBenchmark.java index 48c723611db..057651bbb7c 100644 --- a/benchmarks/src/jmh/java/io/micronaut/http/server/stack/FullHttpStackBenchmark.java +++ b/benchmarks/src/jmh/java/io/micronaut/http/server/stack/FullHttpStackBenchmark.java @@ -2,7 +2,6 @@ import io.micronaut.context.ApplicationContext; import io.micronaut.http.server.netty.NettyHttpServer; -import io.micronaut.http.server.netty.configuration.NettyHttpServerConfiguration; import io.micronaut.runtime.server.EmbeddedServer; import io.netty.buffer.ByteBuf; import io.netty.buffer.CompositeByteBuf; @@ -165,7 +164,7 @@ public enum StackFactory { Stack openChannel() { ApplicationContext ctx = ApplicationContext.run(Map.of( "spec.name", "FullHttpStackBenchmark", - "micronaut.server.netty.server-type", NettyHttpServerConfiguration.HttpServerType.FULL_CONTENT, + //"micronaut.server.netty.server-type", NettyHttpServerConfiguration.HttpServerType.FULL_CONTENT, "micronaut.server.date-header", false // disabling this makes the response identical each time )); EmbeddedServer server = ctx.getBean(EmbeddedServer.class); diff --git a/http-client/src/main/java/io/micronaut/http/client/netty/DefaultHttpClient.java b/http-client/src/main/java/io/micronaut/http/client/netty/DefaultHttpClient.java index 17a6d69206d..39d16efa3f5 100644 --- a/http-client/src/main/java/io/micronaut/http/client/netty/DefaultHttpClient.java +++ b/http-client/src/main/java/io/micronaut/http/client/netty/DefaultHttpClient.java @@ -88,9 +88,9 @@ import io.micronaut.http.netty.NettyHttpResponseBuilder; import io.micronaut.http.netty.channel.ChannelPipelineCustomizer; import io.micronaut.http.netty.stream.DefaultStreamedHttpResponse; +import io.micronaut.http.netty.stream.DelegateStreamedHttpRequest; import io.micronaut.http.netty.stream.HttpStreamsClientHandler; import io.micronaut.http.netty.stream.JsonSubscriber; -import io.micronaut.http.netty.stream.StreamedHttpRequest; import io.micronaut.http.netty.stream.StreamedHttpResponse; import io.micronaut.http.reactive.execution.ReactiveExecutionFlow; import io.micronaut.http.sse.Event; @@ -129,6 +129,7 @@ import io.netty.channel.socket.nio.NioDatagramChannel; import io.netty.channel.socket.nio.NioSocketChannel; import io.netty.handler.codec.TooLongFrameException; +import io.netty.handler.codec.http.DefaultFullHttpRequest; import io.netty.handler.codec.http.DefaultFullHttpResponse; import io.netty.handler.codec.http.DefaultHttpContent; import io.netty.handler.codec.http.DefaultHttpHeaders; @@ -176,9 +177,9 @@ import java.io.IOException; import java.io.InputStream; import java.net.InetSocketAddress; -import java.net.MalformedURLException; import java.net.URI; import java.net.URISyntaxException; +import java.nio.channels.ClosedChannelException; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.time.Duration; @@ -1303,6 +1304,16 @@ protected NettyRequestWriter buildNettyRequest( @Nullable Argument bodyType, Consumer onError) throws HttpPostRequestEncoder.ErrorDataEncoderException { + NettyHttpRequestBuilder nettyRequestBuilder = NettyHttpRequestBuilder.asBuilder(request); + Optional direct = nettyRequestBuilder.toHttpRequestDirect(); + String newUri = requestURI.getRawPath(); + if (requestURI.getRawQuery() != null) { + newUri += "?" + requestURI.getRawQuery(); + } + if (direct.isPresent()) { + return new NettyRequestWriter(direct.get().setUri(newUri), null); + } + io.netty.handler.codec.http.HttpRequest nettyRequest; HttpPostRequestEncoder postRequestEncoder = null; if (permitsBody) { @@ -1312,8 +1323,7 @@ protected NettyRequestWriter buildNettyRequest( Object bodyValue = body.get(); if (bodyValue instanceof CharSequence) { ByteBuf byteBuf = charSequenceToByteBuf((CharSequence) bodyValue, requestContentType); - request.body(byteBuf); - nettyRequest = NettyHttpRequestBuilder.toHttpRequest(request); + nettyRequest = withBytes(nettyRequestBuilder.toHttpRequestWithoutBody(), byteBuf); } else { postRequestEncoder = buildFormDataRequest(request, bodyValue); nettyRequest = postRequestEncoder.finalizeRequest(); @@ -1389,13 +1399,8 @@ protected NettyRequestWriter buildNettyRequest( requestBodyPublisher = requestBodyPublisher.doOnError(onError); - request.body(requestBodyPublisher); - nettyRequest = NettyHttpRequestBuilder.toHttpRequest(request); - try { - nettyRequest.setUri(requestURI.toURL().getFile()); - } catch (MalformedURLException e) { - //should never happen - } + nettyRequest = new DelegateStreamedHttpRequest(nettyRequestBuilder.toHttpRequestWithoutBody(), requestBodyPublisher); + nettyRequest.setUri(newUri); return new NettyRequestWriter(nettyRequest, null); } else if (bodyValue instanceof CharSequence) { bodyContent = charSequenceToByteBuf((CharSequence) bodyValue, requestContentType); @@ -1415,26 +1420,32 @@ protected NettyRequestWriter buildNettyRequest( decorate(new HttpClientException("Body [" + bodyValue + "] cannot be encoded to content type [" + requestContentType + "]. No possible codecs or converters found.")) ); } + } else { + bodyContent = Unpooled.EMPTY_BUFFER; } - request.body(bodyContent); - try { - nettyRequest = NettyHttpRequestBuilder.toHttpRequest(request); - } finally { - // reset body after encoding request in case of retry - request.body(body.orElse(null)); - } + nettyRequest = withBytes(nettyRequestBuilder.toHttpRequestWithoutBody(), bodyContent); } } else { - nettyRequest = NettyHttpRequestBuilder.toHttpRequest(request); - } - try { - nettyRequest.setUri(requestURI.toURL().getFile()); - } catch (MalformedURLException e) { - //should never happen + nettyRequest = withBytes(nettyRequestBuilder.toHttpRequestWithoutBody(), Unpooled.EMPTY_BUFFER); } + nettyRequest.setUri(newUri); return new NettyRequestWriter(nettyRequest, postRequestEncoder); } + private static FullHttpRequest withBytes(HttpRequest request, ByteBuf bytes) { + HttpHeaders headers = request.headers(); + headers.remove(HttpHeaderNames.TRANSFER_ENCODING); + headers.set(HttpHeaderNames.CONTENT_LENGTH, bytes.readableBytes()); + return new DefaultFullHttpRequest( + request.protocolVersion(), + request.method(), + request.uri(), + bytes, + headers, + LastHttpContent.EMPTY_LAST_CONTENT.trailingHeaders() + ); + } + private Flux> readBodyOnError(@Nullable Argument errorType, @NonNull Flux> publisher) { if (errorType != null && errorType != HttpClient.DEFAULT_ERROR_TYPE) { return publisher.onErrorResume(clientException -> { @@ -1703,14 +1714,14 @@ private void prepareHttpHeaders( headers.set(HttpHeaderNames.TRANSFER_ENCODING, HttpHeaderValues.CHUNKED); } } - } else if (!(nettyRequest instanceof StreamedHttpRequest)) { - headers.set(HttpHeaderNames.CONTENT_LENGTH, 0); + } else if (nettyRequest instanceof FullHttpRequest full) { + headers.set(HttpHeaderNames.CONTENT_LENGTH, full.content().readableBytes()); } } } private HttpPostRequestEncoder buildFormDataRequest(MutableHttpRequest clientHttpRequest, Object bodyValue) throws HttpPostRequestEncoder.ErrorDataEncoderException { - HttpPostRequestEncoder postRequestEncoder = new HttpPostRequestEncoder(NettyHttpRequestBuilder.toHttpRequest(clientHttpRequest), false); + HttpPostRequestEncoder postRequestEncoder = new HttpPostRequestEncoder(NettyHttpRequestBuilder.asBuilder(clientHttpRequest).toHttpRequestWithoutBody(), false); Map formData; if (bodyValue instanceof Map) { @@ -1743,7 +1754,7 @@ private void addBodyAttribute(HttpPostRequestEncoder postRequestEncoder, String private HttpPostRequestEncoder buildMultipartRequest(MutableHttpRequest clientHttpRequest, Object bodyValue) throws HttpPostRequestEncoder.ErrorDataEncoderException { HttpDataFactory factory = new DefaultHttpDataFactory(DefaultHttpDataFactory.MINSIZE); - io.netty.handler.codec.http.HttpRequest request = NettyHttpRequestBuilder.toHttpRequest(clientHttpRequest); + io.netty.handler.codec.http.HttpRequest request = NettyHttpRequestBuilder.asBuilder(clientHttpRequest).toHttpRequestWithoutBody(); HttpPostRequestEncoder postRequestEncoder = new HttpPostRequestEncoder(factory, request, true, CharsetUtil.UTF_8, HttpPostRequestEncoder.EncoderMode.HTML5); if (bodyValue instanceof MultipartBody.Builder) { bodyValue = ((MultipartBody.Builder) bodyValue).build(); @@ -1852,15 +1863,15 @@ static boolean isSecureScheme(String scheme) { return io.micronaut.http.HttpRequest.SCHEME_HTTPS.equalsIgnoreCase(scheme) || SCHEME_WSS.equalsIgnoreCase(scheme); } + private E decorate(E exc) { + return HttpClientExceptionUtils.populateServiceId(exc, informationalServiceId, configuration); + } + @FunctionalInterface interface ThrowingBiConsumer { void accept(T1 t1, T2 t2) throws Exception; } - private E decorate(E exc) { - return HttpClientExceptionUtils.populateServiceId(exc, informationalServiceId, configuration); - } - /** * Key used for connection pooling and determining host/port. */ @@ -2053,6 +2064,13 @@ public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { responsePromise.tryFailure(result); } + @Override + public void channelInactive(ChannelHandlerContext ctx) throws Exception { + // connection became inactive before this handler was removed (i.e. before channelRead) + responsePromise.tryFailure(new ClosedChannelException()); + ctx.fireChannelInactive(); + } + @Override protected void channelReadInstrumented(ChannelHandlerContext ctx, R msg) throws Exception { if (responsePromise.isDone()) { @@ -2080,15 +2098,15 @@ protected void channelReadInstrumented(ChannelHandlerContext ctx, R msg) throws Flux.from(resolveRedirectURI(parentRequest, redirectRequest)) .flatMap(makeRedirectHandler(parentRequest, redirectRequest)) .subscribe(new NettyPromiseSubscriber<>(responsePromise)); - return; + } else { + HttpHeaders headers = msg.headers(); + if (log.isTraceEnabled()) { + log.trace("HTTP Client Response Received ({}) for Request: {} {}", msg.status(), finalRequest.getMethodName(), finalRequest.getUri()); + HttpHeadersUtil.trace(log, headers.names(), headers::getAll); + } + buildResponse(responsePromise, msg); } - HttpHeaders headers = msg.headers(); - if (log.isTraceEnabled()) { - log.trace("HTTP Client Response Received ({}) for Request: {} {}", msg.status(), finalRequest.getMethodName(), finalRequest.getUri()); - HttpHeadersUtil.trace(log, headers.names(), headers::getAll); - } - buildResponse(responsePromise, msg); removeHandler(ctx); } diff --git a/http-client/src/main/java/io/micronaut/http/client/netty/NettyClientHttpRequest.java b/http-client/src/main/java/io/micronaut/http/client/netty/NettyClientHttpRequest.java index 4016eee4307..078b0bb0108 100644 --- a/http-client/src/main/java/io/micronaut/http/client/netty/NettyClientHttpRequest.java +++ b/http-client/src/main/java/io/micronaut/http/client/netty/NettyClientHttpRequest.java @@ -39,6 +39,7 @@ import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; import io.netty.handler.codec.http.DefaultFullHttpRequest; +import io.netty.handler.codec.http.DefaultHttpRequest; import io.netty.handler.codec.http.EmptyHttpHeaders; import io.netty.handler.codec.http.FullHttpRequest; import io.netty.handler.codec.http.HttpContent; @@ -264,6 +265,7 @@ public String getMethodName() { @NonNull @Override + @Deprecated public FullHttpRequest toFullHttpRequest() { String uriStr = resolveUriPath(); io.netty.handler.codec.http.HttpMethod method = getMethod(httpMethodName); @@ -302,6 +304,7 @@ public FullHttpRequest toFullHttpRequest() { @NonNull @Override + @Deprecated public StreamedHttpRequest toStreamHttpRequest() { if (body instanceof Publisher) { String uriStr = resolveUriPath(); @@ -317,6 +320,7 @@ public StreamedHttpRequest toStreamHttpRequest() { @NonNull @Override + @Deprecated public HttpRequest toHttpRequest() { if (isStream()) { return toStreamHttpRequest(); @@ -326,6 +330,17 @@ public HttpRequest toHttpRequest() { } @Override + public HttpRequest toHttpRequestWithoutBody() { + return new DefaultHttpRequest( + HttpVersion.HTTP_1_1, + getMethod(httpMethodName), + resolveUriPath(), + headers.getNettyHeaders() + ); + } + + @Override + @Deprecated public boolean isStream() { return body instanceof Publisher; } diff --git a/http-client/src/test/groovy/io/micronaut/http/client/StreamRequestSpec.groovy b/http-client/src/test/groovy/io/micronaut/http/client/StreamRequestSpec.groovy index 303207d3f22..cfbdae986fb 100644 --- a/http-client/src/test/groovy/io/micronaut/http/client/StreamRequestSpec.groovy +++ b/http-client/src/test/groovy/io/micronaut/http/client/StreamRequestSpec.groovy @@ -88,8 +88,8 @@ class StreamRequestSpec extends Specification { )).contentType(MediaType.TEXT_PLAIN_TYPE), List)).blockFirst() then: - result.body().size() == 5 - result.body() == ["Number 0", "Number 1", "Number 2", "Number 3", "Number 4"] + // no guarantee that the strings aren't merged, so we have to allow for it + result.body().join("") == "Number 0Number 1Number 2Number 3Number 4" } @@ -121,8 +121,8 @@ class StreamRequestSpec extends Specification { )).contentType(MediaType.TEXT_PLAIN_TYPE), List)).blockFirst() then: - result.body().size() == 5 - result.body() == ["Number 0", "Number 1", "Number 2", "Number 3", "Number 4"] + // no guarantee that the strings aren't merged, so we have to allow for it + result.body().join("") == "Number 0Number 1Number 2Number 3Number 4" } void "test stream post request with POJOs"() { diff --git a/http-netty/src/main/java/io/micronaut/http/netty/NettyHttpRequestBuilder.java b/http-netty/src/main/java/io/micronaut/http/netty/NettyHttpRequestBuilder.java index b8a3444f0bd..9d6835bfcb0 100644 --- a/http-netty/src/main/java/io/micronaut/http/netty/NettyHttpRequestBuilder.java +++ b/http-netty/src/main/java/io/micronaut/http/netty/NettyHttpRequestBuilder.java @@ -15,17 +15,17 @@ */ package io.micronaut.http.netty; +import io.micronaut.core.annotation.Internal; import io.micronaut.core.annotation.NonNull; import io.micronaut.http.HttpRequestWrapper; import io.micronaut.http.netty.stream.StreamedHttpRequest; -import io.netty.buffer.ByteBuf; -import io.netty.handler.codec.http.DefaultFullHttpRequest; +import io.netty.handler.codec.http.DefaultHttpRequest; import io.netty.handler.codec.http.FullHttpRequest; import io.netty.handler.codec.http.HttpMethod; import io.netty.handler.codec.http.HttpRequest; import io.netty.handler.codec.http.HttpVersion; -import java.util.Objects; +import java.util.Optional; /** * Common interface for client and server to implement to construct the Netty versions of the request objects. @@ -33,67 +33,119 @@ * @author graemerocher * @since 2.0.0 */ +@Internal public interface NettyHttpRequestBuilder { /** * Converts this object to a full http request. * * @return a full http request */ + @Deprecated @NonNull - FullHttpRequest toFullHttpRequest(); + default FullHttpRequest toFullHttpRequest() { + throw new UnsupportedOperationException(); + } /** * Converts this object to a streamed http request. + * * @return The streamed request + * @deprecated Go through {@link #toHttpRequestDirect()} and {@link #toHttpRequestWithoutBody()} instead */ + @Deprecated(since = "4.0.0", forRemoval = true) @NonNull - StreamedHttpRequest toStreamHttpRequest(); + default StreamedHttpRequest toStreamHttpRequest() { + throw new UnsupportedOperationException(); + } /** * Converts this object to the most appropriate http request type. * @return The http request + * @deprecated Go through {@link #toHttpRequestDirect()} and {@link #toHttpRequestWithoutBody()} instead + */ + @NonNull + @Deprecated(since = "4.0.0", forRemoval = true) + default HttpRequest toHttpRequest() { + throw new UnsupportedOperationException(); + } + + /** + * Directly convert this request to netty, including the body, if possible. If the body of this + * request has been changed, this will return an empty value. + * + * @return The request including the body + */ + @NonNull + default Optional toHttpRequestDirect() { + return Optional.empty(); + } + + /** + * Convert this request to a netty request without the body. The caller will handle adding the + * body. + * + * @return The request excluding the body */ @NonNull - HttpRequest toHttpRequest(); + HttpRequest toHttpRequestWithoutBody(); /** * @return Is the request a stream. + * @deprecated Go through {@link #toHttpRequestDirect()} and {@link #toHttpRequestWithoutBody()} instead */ - boolean isStream(); + @Deprecated(since = "4.0.0", forRemoval = true) + default boolean isStream() { + throw new UnsupportedOperationException(); + } /** * Convert the given request to a full http request. * @param request The request * @return The full request. + * @deprecated Go through {@link #toHttpRequestDirect()} and {@link #toHttpRequestWithoutBody()} instead */ + @Deprecated(since = "4.0.0", forRemoval = true) static @NonNull HttpRequest toHttpRequest(@NonNull io.micronaut.http.HttpRequest request) { - Objects.requireNonNull(request, "The request cannot be null"); - while (request instanceof HttpRequestWrapper) { - request = ((HttpRequestWrapper) request).getDelegate(); - } - if (request instanceof NettyHttpRequestBuilder) { - return ((NettyHttpRequestBuilder) request).toHttpRequest(); + return asBuilder(request).toHttpRequestWithoutBody(); + } + + /** + * Transform the given request to an equivalent {@link NettyHttpRequestBuilder}, so that it can + * be transformed to a netty request. + * + * @param request The micronaut http request + * @return The builder for further operations + */ + static NettyHttpRequestBuilder asBuilder(@NonNull io.micronaut.http.HttpRequest request) { + boolean supportDirect = true; + while (request instanceof HttpRequestWrapper wrapper) { + supportDirect &= wrapper.getBody() == wrapper.getDelegate().getBody(); + request = wrapper.getDelegate(); } - // manual conversion - HttpRequest nettyRequest; - ByteBuf byteBuf = request.getBody(ByteBuf.class).orElse(null); - if (byteBuf != null) { - nettyRequest = new DefaultFullHttpRequest( - HttpVersion.HTTP_1_1, - HttpMethod.valueOf(request.getMethodName()), - request.getUri().toString(), - byteBuf - ); - } else { - nettyRequest = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, - HttpMethod.valueOf(request.getMethodName()), - request.getUri().toString() - ); + if (request instanceof NettyHttpRequestBuilder builder) { + if (supportDirect) { + return builder; + } else { + // delegate to builder, excluding toHttpRequestDirect + //noinspection Convert2Lambda + return new NettyHttpRequestBuilder() { + @Override + public HttpRequest toHttpRequestWithoutBody() { + return builder.toHttpRequestWithoutBody(); + } + }; + } } + // manual conversion + HttpRequest nettyRequest = new DefaultHttpRequest( + HttpVersion.HTTP_1_1, + HttpMethod.valueOf(request.getMethodName()), + request.getUri().toString() + ); request.getHeaders() - .forEach((s, strings) -> nettyRequest.headers().add(s, strings)); - return nettyRequest; + .forEach((s, strings) -> nettyRequest.headers().add(s, strings)); + return () -> nettyRequest; } } diff --git a/http-netty/src/main/java/io/micronaut/http/netty/stream/DelegateStreamedHttpRequest.java b/http-netty/src/main/java/io/micronaut/http/netty/stream/DelegateStreamedHttpRequest.java index 3f377776745..afebfe62eba 100644 --- a/http-netty/src/main/java/io/micronaut/http/netty/stream/DelegateStreamedHttpRequest.java +++ b/http-netty/src/main/java/io/micronaut/http/netty/stream/DelegateStreamedHttpRequest.java @@ -30,7 +30,7 @@ * @since 1.0 */ @Internal -final class DelegateStreamedHttpRequest extends DelegateHttpRequest implements StreamedHttpRequest { +public class DelegateStreamedHttpRequest extends DelegateHttpRequest implements StreamedHttpRequest { private final Publisher stream; private boolean consumed; @@ -39,7 +39,7 @@ final class DelegateStreamedHttpRequest extends DelegateHttpRequest implements S * @param request The Http request * @param stream The publisher */ - DelegateStreamedHttpRequest(HttpRequest request, Publisher stream) { + public DelegateStreamedHttpRequest(HttpRequest request, Publisher stream) { super(request); this.stream = stream; } diff --git a/http-netty/src/main/java/io/micronaut/http/netty/stream/EmptyHttpRequest.java b/http-netty/src/main/java/io/micronaut/http/netty/stream/EmptyHttpRequest.java index f007c4c48b1..6b589e04d87 100644 --- a/http-netty/src/main/java/io/micronaut/http/netty/stream/EmptyHttpRequest.java +++ b/http-netty/src/main/java/io/micronaut/http/netty/stream/EmptyHttpRequest.java @@ -36,12 +36,12 @@ * @since 1.0 */ @Internal -class EmptyHttpRequest extends DelegateHttpRequest implements FullHttpRequest { +public class EmptyHttpRequest extends DelegateHttpRequest implements FullHttpRequest { /** * @param request The Http request */ - EmptyHttpRequest(HttpRequest request) { + public EmptyHttpRequest(HttpRequest request) { super(request); } diff --git a/http-netty/src/main/java/io/micronaut/http/netty/stream/HttpStreamsServerHandler.java b/http-netty/src/main/java/io/micronaut/http/netty/stream/HttpStreamsServerHandler.java deleted file mode 100644 index d0baea2d300..00000000000 --- a/http-netty/src/main/java/io/micronaut/http/netty/stream/HttpStreamsServerHandler.java +++ /dev/null @@ -1,302 +0,0 @@ -/* - * Copyright 2017-2020 original authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.micronaut.http.netty.stream; - -import io.micronaut.core.annotation.Internal; -import io.micronaut.http.netty.reactive.CancelledSubscriber; -import io.micronaut.http.netty.reactive.HandlerPublisher; -import io.micronaut.http.netty.reactive.HandlerSubscriber; -import io.netty.channel.ChannelHandler; -import io.netty.channel.ChannelHandlerContext; -import io.netty.channel.ChannelPipeline; -import io.netty.channel.ChannelPromise; -import io.netty.handler.codec.http.DefaultFullHttpResponse; -import io.netty.handler.codec.http.FullHttpRequest; -import io.netty.handler.codec.http.FullHttpResponse; -import io.netty.handler.codec.http.HttpContent; -import io.netty.handler.codec.http.HttpHeaderNames; -import io.netty.handler.codec.http.HttpRequest; -import io.netty.handler.codec.http.HttpResponse; -import io.netty.handler.codec.http.HttpResponseStatus; -import io.netty.handler.codec.http.HttpUtil; -import io.netty.handler.codec.http.HttpVersion; -import io.netty.handler.codec.http.websocketx.WebSocketFrame; -import io.netty.handler.codec.http.websocketx.WebSocketServerHandshaker; -import io.netty.handler.codec.http.websocketx.WebSocketVersion; -import org.reactivestreams.Publisher; - -import java.util.Collections; -import java.util.List; -import java.util.NoSuchElementException; - -/** - * Handler that reads {@link HttpRequest} messages followed by {@link HttpContent} messages and produces - * {@link StreamedHttpRequest} messages, and converts written {@link StreamedHttpResponse} messages into - * {@link HttpResponse} messages followed by {@link HttpContent} messages. - *

- * This allows request and response bodies to be handled using reactive streams. - *

- * There are two types of messages that this handler will send down the chain, {@link StreamedHttpRequest}, - * and {@link FullHttpRequest}. If {@link io.netty.channel.ChannelOption#AUTO_READ} is false for the channel, - * then any {@link StreamedHttpRequest} messages must be subscribed to consume the body, otherwise - * it's possible that no read will be done of the messages. - *

- * There are three types of messages that this handler accepts for writing, {@link StreamedHttpResponse}, - * {@link WebSocketHttpResponse} and {@link io.netty.handler.codec.http.FullHttpResponse}. Writing any other messages - * may potentially lead to HTTP message mangling. - *

- * As long as messages are returned in the order that they arrive, this handler implicitly supports HTTP - * pipelining. - * - * @author jroper - * @author Graeme Rocher - */ -@Internal -public class HttpStreamsServerHandler extends HttpStreamsHandler { - - private HttpRequest lastRequest = null; - private HttpResponse webSocketResponse = null; - private ChannelPromise webSocketResponseChannelPromise = null; - private int inFlight = 0; - private boolean continueExpected = true; - private boolean sendContinue = false; - private boolean close = false; - - private final List dependentHandlers; - - /** - * Default constructor. - */ - public HttpStreamsServerHandler() { - this(Collections.emptyList()); - } - - /** - * Create a new handler that is depended on by the given handlers. - *

- * The list of dependent handlers will be removed from the chain when this handler is removed from the chain, - * for example, when the connection is upgraded to use websockets. This is useful, for example, for removing - * the reactive streams publisher/subscriber from the chain in that event. - * - * @param dependentHandlers The handlers that depend on this handler. - */ - public HttpStreamsServerHandler(List dependentHandlers) { - super(HttpRequest.class, HttpResponse.class); - this.dependentHandlers = dependentHandlers; - } - - @Override - protected boolean hasBody(HttpRequest request) { - // if there's a decoder failure (e.g. invalid header), don't expect the body to come in - if (request.decoderResult().isFailure()) { - return false; - } - // Http requests don't have a body if they define 0 content length, or no content length and no transfer - // encoding - int contentLength; - try { - contentLength = HttpUtil.getContentLength(request, 0); - } catch (NumberFormatException e) { - // handle invalid content length, https://github.com/netty/netty/issues/12113 - contentLength = 0; - } - return contentLength != 0 || HttpUtil.isTransferEncodingChunked(request); - } - - @Override - protected HttpRequest createEmptyMessage(HttpRequest request) { - return new EmptyHttpRequest(request); - } - - @Override - protected HttpRequest createStreamedMessage(HttpRequest httpRequest, Publisher stream) { - return new DelegateStreamedHttpRequest(httpRequest, stream); - } - - @Override - public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { - // Set to false, since if it was true, and the client is sending data, then the - // client must no longer be expecting it (due to a timeout, for example). - continueExpected = false; - sendContinue = false; - - if (msg instanceof HttpRequest) { - HttpRequest request = (HttpRequest) msg; - lastRequest = request; - if (HttpUtil.is100ContinueExpected(request)) { - continueExpected = true; - } - } - super.channelRead(ctx, msg); - } - - @Override - protected void receivedInMessage(ChannelHandlerContext ctx) { - inFlight++; - } - - @Override - protected void sentOutMessage(ChannelHandlerContext ctx) { - inFlight--; - if (inFlight == 1 && continueExpected && sendContinue) { - ctx.writeAndFlush(new DefaultFullHttpResponse(lastRequest.protocolVersion(), HttpResponseStatus.CONTINUE)); - sendContinue = false; - continueExpected = false; - } - - if (close) { - ctx.close(); - } - } - - @Override - protected void unbufferedWrite(ChannelHandlerContext ctx, HttpResponse message, ChannelPromise promise) { - - if (message instanceof WebSocketHttpResponse) { - if ((lastRequest instanceof FullHttpRequest) || !hasBody(lastRequest)) { - handleWebSocketResponse(ctx, message, promise); - } else { - // If the response has a streamed body, then we can't send the WebSocket response until we've received - // the body. - webSocketResponse = message; - webSocketResponseChannelPromise = promise; - } - } else { - if (lastRequest.protocolVersion().isKeepAliveDefault()) { - if (message.headers().contains(HttpHeaderNames.CONNECTION, "close", true)) { - close = true; - } - } else { - if (!message.headers().contains(HttpHeaderNames.CONNECTION, "keep-alive", true)) { - close = true; - } - } - if (inFlight == 1 && continueExpected) { - HttpUtil.setKeepAlive(message, false); - close = true; - continueExpected = false; - } - // According to RFC 7230 a server MUST NOT send a Content-Length or a Transfer-Encoding when the status - // code is 1xx or 204, also a status code 304 may not have a Content-Length or Transfer-Encoding set. - if (!HttpUtil.isContentLengthSet(message) && !HttpUtil.isTransferEncodingChunked(message) && canHaveBody(message)) { - HttpUtil.setKeepAlive(message, false); - close = true; - } - super.unbufferedWrite(ctx, message, promise); - } - } - - @Override - protected boolean isValidOutMessage(Object msg) { - return msg instanceof FullHttpResponse || msg instanceof StreamedHttpResponse || msg instanceof WebSocketHttpResponse; - } - - private boolean canHaveBody(HttpResponse message) { - HttpResponseStatus status = message.status(); - // All 1xx (Informational), 204 (No Content), and 304 (Not Modified) - // responses do not include a message body - return !(status == HttpResponseStatus.CONTINUE || status == HttpResponseStatus.SWITCHING_PROTOCOLS || - status == HttpResponseStatus.PROCESSING || status == HttpResponseStatus.NO_CONTENT || - status == HttpResponseStatus.NOT_MODIFIED); - } - - @Override - protected void consumedInMessage(ChannelHandlerContext ctx) { - if (webSocketResponse != null) { - handleWebSocketResponse(ctx, webSocketResponse, webSocketResponseChannelPromise); - webSocketResponse = null; - webSocketResponseChannelPromise = null; - } - if (inFlight == 0) { - // normally, after writing the response, the routing handler triggers a read() for the - // next request. However, if at this point the request is not fully read yet (e.g. - // still missing a LastHttpContent), then that read() call will simply read the - // remaining content, and the HandlerPublisher also won't trigger more read()s since - // it's complete. To prevent the connection from being stuck in that case, we trigger a - // read here. - ctx.read(); - } - } - - private void handleWebSocketResponse(ChannelHandlerContext ctx, HttpResponse message, ChannelPromise promise) { - WebSocketHttpResponse response = (WebSocketHttpResponse) message; - WebSocketServerHandshaker handshaker = response.handshakerFactory().newHandshaker(lastRequest); - - if (handshaker == null) { - HttpResponse res = new DefaultFullHttpResponse( - HttpVersion.HTTP_1_1, - HttpResponseStatus.UPGRADE_REQUIRED); - res.headers().set(HttpHeaderNames.SEC_WEBSOCKET_VERSION, WebSocketVersion.V13.toHttpHeaderValue()); - HttpUtil.setContentLength(res, 0); - super.unbufferedWrite(ctx, message, promise); - response.subscribe(new CancelledSubscriber<>()); - } else { - // First, insert new handlers in the chain after us for handling the websocket - ChannelPipeline pipeline = ctx.pipeline(); - HandlerPublisher publisher = new HandlerPublisher<>(ctx.executor()) { - @Override - protected boolean acceptInboundMessage(Object msg) { - return msg instanceof WebSocketFrame; - } - }; - HandlerSubscriber subscriber = new HandlerSubscriber<>(ctx.executor()); - pipeline.addAfter(ctx.executor(), ctx.name(), "websocket-subscriber", subscriber); - pipeline.addAfter(ctx.executor(), ctx.name(), "websocket-publisher", publisher); - - // Now remove ourselves from the chain - ctx.pipeline().remove(ctx.name()); - - // Now do the handshake - // Wrap the request in an empty request because we don't need the WebSocket handshaker ignoring the body, - // we already have handled the body. - handshaker.handshake(ctx.channel(), new EmptyHttpRequest(lastRequest)); - - // And hook up the subscriber/publishers - response.subscribe(subscriber); - publisher.subscribe(response); - } - - } - - @Override - protected void bodyRequested(ChannelHandlerContext ctx) { - if (continueExpected) { - if (inFlight == 1) { - ctx.writeAndFlush(new DefaultFullHttpResponse(lastRequest.protocolVersion(), HttpResponseStatus.CONTINUE)); - continueExpected = false; - } else { - sendContinue = true; - } - } - } - - @Override - protected final boolean isClient() { - return false; - } - - @Override - public void handlerRemoved(ChannelHandlerContext ctx) throws Exception { - super.handlerRemoved(ctx); - for (ChannelHandler dependent : dependentHandlers) { - try { - ctx.pipeline().remove(dependent); - } catch (NoSuchElementException e) { - // Ignore, maybe something else removed it - } - } - } -} diff --git a/http-netty/src/main/java/io/micronaut/http/netty/stream/StreamedHttpRequest.java b/http-netty/src/main/java/io/micronaut/http/netty/stream/StreamedHttpRequest.java index efd55601806..97cc3fb6a3b 100644 --- a/http-netty/src/main/java/io/micronaut/http/netty/stream/StreamedHttpRequest.java +++ b/http-netty/src/main/java/io/micronaut/http/netty/stream/StreamedHttpRequest.java @@ -15,6 +15,8 @@ */ package io.micronaut.http.netty.stream; +import io.micronaut.http.netty.reactive.HotObservable; +import io.netty.handler.codec.http.HttpContent; import io.netty.handler.codec.http.HttpRequest; /** @@ -25,7 +27,7 @@ * @author jroper * @author Graeme Rocher */ -public interface StreamedHttpRequest extends HttpRequest, StreamedHttpMessage { +public interface StreamedHttpRequest extends HttpRequest, StreamedHttpMessage, HotObservable { /** * Releases the stream if there is no subscriber. diff --git a/http-server-netty/build.gradle b/http-server-netty/build.gradle index d253b43786a..8d4762da5fa 100644 --- a/http-server-netty/build.gradle +++ b/http-server-netty/build.gradle @@ -14,6 +14,7 @@ micronautBuild { tasks.named("test") { systemProperty("io.netty.leakDetection.level", "paranoid") systemProperty("io.netty.customResourceLeakDetector", "io.micronaut.http.server.netty.fuzzing.BufferLeakDetection") + systemProperty("io.netty.leakDetection.targetRecords", "100") maxHeapSize("1G") } diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/AbstractHttpContentProcessor.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/AbstractHttpContentProcessor.java index 3ea2840e82e..07f0bde9afb 100644 --- a/http-server-netty/src/main/java/io/micronaut/http/server/netty/AbstractHttpContentProcessor.java +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/AbstractHttpContentProcessor.java @@ -78,7 +78,7 @@ public void add(ByteBufHolder message, Collection out) throws Throwable * @param message The message to release */ protected void fireExceedsLength(long receivedLength, long expected, ByteBufHolder message) { - ReferenceCountUtil.safeRelease(message); + message.release(); throw new ContentLengthExceededException(expected, receivedLength); } } diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/DefaultHttpContentProcessor.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/DefaultHttpContentProcessor.java index 522b5b047a0..79860426dba 100644 --- a/http-server-netty/src/main/java/io/micronaut/http/server/netty/DefaultHttpContentProcessor.java +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/DefaultHttpContentProcessor.java @@ -21,7 +21,6 @@ import io.netty.buffer.ByteBufHolder; import io.netty.channel.ChannelHandlerContext; import io.netty.handler.codec.http.multipart.HttpData; -import io.netty.util.ReferenceCountUtil; import java.util.Collection; import java.util.concurrent.atomic.AtomicLong; @@ -76,7 +75,7 @@ private long resolveLength(ByteBufHolder message) { } private void fireExceedsLength(long receivedLength, long expected, ByteBufHolder message) { - ReferenceCountUtil.safeRelease(message); + message.release(); throw new ContentLengthExceededException(expected, receivedLength); } } diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/DefaultNettyEmbeddedServerFactory.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/DefaultNettyEmbeddedServerFactory.java index 4cc4fe98e27..95aa73d2580 100644 --- a/http-server-netty/src/main/java/io/micronaut/http/server/netty/DefaultNettyEmbeddedServerFactory.java +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/DefaultNettyEmbeddedServerFactory.java @@ -43,6 +43,7 @@ import io.micronaut.http.server.netty.types.DefaultCustomizableResponseTypeHandlerRegistry; import io.micronaut.http.server.netty.types.NettyCustomizableResponseTypeHandler; import io.micronaut.http.server.netty.types.files.FileTypeHandler; +import io.micronaut.http.server.netty.websocket.NettyServerWebSocketUpgradeHandler; import io.micronaut.http.server.netty.websocket.WebSocketUpgradeHandlerFactory; import io.micronaut.http.ssl.ServerSslConfiguration; import io.micronaut.scheduling.executor.ExecutorSelector; @@ -51,7 +52,6 @@ import io.netty.channel.ChannelOutboundHandler; import io.netty.channel.EventLoopGroup; import io.netty.channel.ServerChannel; -import io.netty.channel.SimpleChannelInboundHandler; import io.netty.channel.socket.ServerSocketChannel; import jakarta.inject.Inject; import jakarta.inject.Named; @@ -269,7 +269,7 @@ public HttpCompressionStrategy getHttpCompressionStrategy() { } @Override - public Optional>> getWebSocketUpgradeHandler(NettyEmbeddedServer server) { + public Optional getWebSocketUpgradeHandler(NettyEmbeddedServer server) { return Optional.ofNullable(webSocketUpgradeHandlerFactory) .map(factory -> factory.create(server, this)); } diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/DelegateNettyEmbeddedServices.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/DelegateNettyEmbeddedServices.java index b1f932f0202..17b50f26bbc 100644 --- a/http-server-netty/src/main/java/io/micronaut/http/server/netty/DelegateNettyEmbeddedServices.java +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/DelegateNettyEmbeddedServices.java @@ -15,10 +15,6 @@ */ package io.micronaut.http.server.netty; -import java.util.List; -import java.util.Optional; -import java.util.concurrent.ExecutorService; - import io.micronaut.context.ApplicationContext; import io.micronaut.context.event.ApplicationEventPublisher; import io.micronaut.core.annotation.Internal; @@ -29,12 +25,16 @@ import io.micronaut.http.netty.channel.converters.ChannelOptionFactory; import io.micronaut.http.server.RouteExecutor; import io.micronaut.http.server.netty.ssl.ServerSslBuilder; +import io.micronaut.http.server.netty.websocket.NettyServerWebSocketUpgradeHandler; import io.micronaut.web.router.resource.StaticResourceResolver; import io.netty.channel.ChannelOutboundHandler; import io.netty.channel.EventLoopGroup; -import io.netty.channel.SimpleChannelInboundHandler; import io.netty.channel.socket.ServerSocketChannel; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.ExecutorService; + /** * A delegating Netty embedded services instance. * @@ -90,7 +90,7 @@ default HttpCompressionStrategy getHttpCompressionStrategy() { } @Override - default Optional>> getWebSocketUpgradeHandler(NettyEmbeddedServer server) { + default Optional getWebSocketUpgradeHandler(NettyEmbeddedServer server) { return getDelegate().getWebSocketUpgradeHandler(server); } diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/DelegateStreamedHttpResponse.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/DelegateStreamedHttpResponse.java index c658c6c442c..8e388130d9b 100644 --- a/http-server-netty/src/main/java/io/micronaut/http/server/netty/DelegateStreamedHttpResponse.java +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/DelegateStreamedHttpResponse.java @@ -29,7 +29,7 @@ * @since 1.0 */ @Internal -final class DelegateStreamedHttpResponse extends DelegateHttpResponse implements StreamedHttpResponse { +public final class DelegateStreamedHttpResponse extends DelegateHttpResponse implements StreamedHttpResponse { private final Publisher stream; @@ -37,7 +37,7 @@ final class DelegateStreamedHttpResponse extends DelegateHttpResponse implements * @param response The {@link HttpResponse} * @param stream The {@link Publisher} for {@link HttpContent} */ - DelegateStreamedHttpResponse(HttpResponse response, + public DelegateStreamedHttpResponse(HttpResponse response, Publisher stream) { super(response); this.stream = stream; diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/FormDataHttpContentProcessor.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/FormDataHttpContentProcessor.java index 7d7380c7dbc..64e7c4371e3 100644 --- a/http-server-netty/src/main/java/io/micronaut/http/server/netty/FormDataHttpContentProcessor.java +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/FormDataHttpContentProcessor.java @@ -73,7 +73,8 @@ public class FormDataHttpContentProcessor extends AbstractHttpContentProcessor { Charset characterEncoding = nettyHttpRequest.getCharacterEncoding(); HttpServerConfiguration.MultipartConfiguration multipart = configuration.getMultipart(); HttpDataFactory factory = new MicronautHttpData.Factory(multipart, characterEncoding); - final HttpRequest nativeRequest = nettyHttpRequest.getNativeRequest(); + // prevent the decoders from immediately parsing the content + HttpRequest nativeRequest = nettyHttpRequest.toHttpRequestWithoutBody(); if (HttpPostRequestDecoder.isMultipart(nativeRequest)) { this.decoder = new HttpPostMultipartRequestDecoder(factory, nativeRequest, characterEncoding); } else { diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/FormRouteCompleter.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/FormRouteCompleter.java index e0bad169f38..71fb9332a1c 100644 --- a/http-server-netty/src/main/java/io/micronaut/http/server/netty/FormRouteCompleter.java +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/FormRouteCompleter.java @@ -56,7 +56,7 @@ public final class FormRouteCompleter implements Subscriber, HttpBody { private static final Logger LOG = LoggerFactory.getLogger(FormRouteCompleter.class); final DelayedExecutionFlow> execute = DelayedExecutionFlow.create(); - private final NettyHttpRequest request; + private final EventLoop eventLoop; private boolean executed; private final RouteMatch routeMatch; private Subscription upstreamSubscription; @@ -64,8 +64,8 @@ public final class FormRouteCompleter implements Subscriber, HttpBody { private final Map claimants = new HashMap<>(); private boolean upstreamDemanded = false; - FormRouteCompleter(NettyHttpRequest request, RouteMatch routeMatch) { - this.request = request; + FormRouteCompleter(RouteMatch routeMatch, EventLoop eventLoop) { + this.eventLoop = eventLoop; this.routeMatch = routeMatch; } @@ -120,14 +120,12 @@ public void onError(Throwable failure) { } private void addData(MicronautHttpData data) { - if (LOG.isTraceEnabled()) { - LOG.trace("Received HTTP Data for request [{}]: {}", request, data); - } allData.add(data); upstreamDemanded = false; String name = data.getName(); Claimant claimant = claimants.get(name); + data.touch(claimant != null); if (claimant == null) { upstreamSubscription.request(1); return; @@ -153,6 +151,14 @@ private void addData(MicronautHttpData data) { } } + private Claimant createClaimant(String name) { + Claimant claimant = new Claimant(); + if (claimants.putIfAbsent(name, claimant) != null) { + throw new IllegalStateException("Field already claimed"); + } + return claimant; + } + /** * Claim all fields of the given name. In the returned publisher, each * {@link MicronautHttpData} may appear multiple times if there is new data. @@ -161,11 +167,7 @@ private void addData(MicronautHttpData data) { * @return The publisher of data with this field name */ public Flux> claimFieldsRaw(String name) { - Claimant claimant = new Claimant(); - if (claimants.putIfAbsent(name, claimant) != null) { - throw new IllegalStateException("Field already claimed"); - } - return claimant.flux(); + return createClaimant(name).flux(); } /** @@ -192,7 +194,9 @@ public Flux claimFields(String name, BiFunction> claimFieldsComplete(String name) { - return claimFieldsRaw(name).filter(MicronautHttpData::isCompleted); + Claimant claimant = createClaimant(name); + claimant.skipUnfinished = true; + return claimant.flux(); } @Override @@ -215,13 +219,18 @@ public Map asMap(Charset defaultCharset) { private class Claimant { private final Sinks.Many> sink = Sinks.many().unicast().onBackpressureBuffer(); private long demand; + private MicronautHttpData last; + private MicronautHttpData unsentIncomplete; + private boolean skipUnfinished = false; public Flux> flux() { - return sink.asFlux().doOnRequest(this::request); + return sink.asFlux() + .doOnRequest(this::request) + .doOnTerminate(this::releaseNotForwarded) + .doOnCancel(this::releaseNotForwarded); } private void request(long n) { - EventLoop eventLoop = request.getChannelHandlerContext().channel().eventLoop(); if (!eventLoop.inEventLoop()) { eventLoop.execute(() -> request(n)); return; @@ -241,6 +250,17 @@ private void request(long n) { } public void send(MicronautHttpData data) { + if (last != data) { + // take ownership for this claimant. FormRouteCompleter also keeps ownership for asMap + data.retain(); + last = data; + } + + if (skipUnfinished && !data.isCompleted()) { + unsentIncomplete = data; + return; + } + demand--; if (sink.tryEmitNext(data) != Sinks.EmitResult.OK) { if (LOG.isDebugEnabled()) { @@ -248,6 +268,13 @@ public void send(MicronautHttpData data) { } } } + + void releaseNotForwarded() { + if (unsentIncomplete != null) { + unsentIncomplete.release(); + unsentIncomplete = null; + } + } } private static class FieldSplitter implements Subscriber> { diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/HttpContentProcessorAsReactiveProcessor.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/HttpContentProcessorAsReactiveProcessor.java index e63c0623e67..2fdc3fc7e11 100644 --- a/http-server-netty/src/main/java/io/micronaut/http/server/netty/HttpContentProcessorAsReactiveProcessor.java +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/HttpContentProcessorAsReactiveProcessor.java @@ -66,6 +66,7 @@ public static Flux asPublisher(HttpContentProcessor processor, Publisher< processor.add(c, (List) out); return Flux.fromIterable(out); } catch (Throwable e) { + c.touch(); return Flux.error(e); } }), Flux.defer(() -> { diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/HttpPipelineBuilder.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/HttpPipelineBuilder.java index 9784bbf7d95..50382e5c411 100644 --- a/http-server-netty/src/main/java/io/micronaut/http/server/netty/HttpPipelineBuilder.java +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/HttpPipelineBuilder.java @@ -20,14 +20,11 @@ import io.micronaut.core.naming.Named; import io.micronaut.core.util.SupplierUtil; import io.micronaut.http.HttpVersion; -import io.micronaut.http.context.event.HttpRequestReceivedEvent; import io.micronaut.http.netty.channel.ChannelPipelineCustomizer; -import io.micronaut.http.netty.stream.HttpStreamsServerHandler; import io.micronaut.http.server.netty.configuration.NettyHttpServerConfiguration; -import io.micronaut.http.server.netty.decoders.HttpRequestDecoder; -import io.micronaut.http.server.netty.encoders.HttpResponseEncoder; +import io.micronaut.http.server.netty.handler.PipeliningServerHandler; +import io.micronaut.http.server.netty.handler.RequestHandler; import io.micronaut.http.server.netty.handler.accesslog.HttpAccessLogHandler; -import io.micronaut.http.server.netty.ssl.HttpRequestCertificateHandler; import io.micronaut.http.server.netty.types.files.NettySystemFileCustomizableResponseType; import io.micronaut.http.server.netty.websocket.NettyServerWebSocketUpgradeHandler; import io.micronaut.http.server.util.HttpHostResolver; @@ -77,11 +74,13 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import javax.net.ssl.SSLPeerUnverifiedException; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.net.InetSocketAddress; import java.net.SocketAddress; import java.nio.channels.ClosedChannelException; +import java.security.cert.Certificate; import java.time.Duration; import java.time.Instant; import java.util.Optional; @@ -100,6 +99,8 @@ final class HttpPipelineBuilder { static final Supplier> STREAM_PIPELINE_ATTRIBUTE = SupplierUtil.memoized(() -> AttributeKey.newInstance("stream-pipeline")); + static final Supplier>> CERTIFICATE_SUPPLIER_ATTRIBUTE = + SupplierUtil.memoized(() -> AttributeKey.newInstance("certificate-supplier")); private static final Logger LOG = LoggerFactory.getLogger(HttpPipelineBuilder.class); @@ -113,8 +114,6 @@ final class HttpPipelineBuilder { private final SslContext sslContext; private final QuicSslContext quicSslContext; private final HttpAccessLogHandler accessLogHandler; - private final HttpRequestDecoder requestDecoder; - private final HttpResponseEncoder responseEncoder; private final NettyServerCustomizer serverCustomizer; @@ -140,15 +139,6 @@ final class HttpPipelineBuilder { } else { accessLogHandler = null; } - - requestDecoder = new HttpRequestDecoder(server, - server.getEnvironment(), - server.getServerConfiguration(), - embeddedServices.getEventPublisher(HttpRequestReceivedEvent.class)); - responseEncoder = new HttpResponseEncoder( - embeddedServices.getMediaTypeCodecRegistry(), - server.getServerConfiguration(), - embeddedServices.getApplicationContext().getConversionService()); } boolean supportsSsl() { @@ -543,6 +533,15 @@ private void insertHttp2DownstreamHandlers() { */ private void insertMicronautHandlers(boolean zeroCopySupported) { channel.attr(STREAM_PIPELINE_ATTRIBUTE.get()).set(this); + if (sslHandler != null) { + channel.attr(CERTIFICATE_SUPPLIER_ATTRIBUTE.get()).set(SupplierUtil.memoized(() -> { + try { + return sslHandler.engine().getSession().getPeerCertificates()[0]; + } catch (SSLPeerUnverifiedException ex) { + return null; + } + })); + } SmartHttpContentCompressor contentCompressor = new SmartHttpContentCompressor(embeddedServices.getHttpCompressionStrategy()); if (zeroCopySupported) { @@ -551,13 +550,11 @@ private void insertMicronautHandlers(boolean zeroCopySupported) { pipeline.addLast(ChannelPipelineCustomizer.HANDLER_HTTP_COMPRESSOR, contentCompressor); pipeline.addLast(ChannelPipelineCustomizer.HANDLER_HTTP_DECOMPRESSOR, new HttpContentDecompressor()); - Optional>> webSocketUpgradeHandler = embeddedServices.getWebSocketUpgradeHandler(server); + Optional webSocketUpgradeHandler = embeddedServices.getWebSocketUpgradeHandler(server); if (webSocketUpgradeHandler.isPresent()) { pipeline.addLast(NettyServerWebSocketUpgradeHandler.COMPRESSION_HANDLER, new WebSocketServerCompressionHandler()); } - if (server.getServerConfiguration().getServerType() == NettyHttpServerConfiguration.HttpServerType.STREAMED) { - pipeline.addLast(ChannelPipelineCustomizer.HANDLER_HTTP_STREAM, new HttpStreamsServerHandler()); - } else { + if (server.getServerConfiguration().getServerType() != NettyHttpServerConfiguration.HttpServerType.STREAMED) { pipeline.addLast(ChannelPipelineCustomizer.HANDLER_HTTP_AGGREGATOR, new HttpObjectAggregator( (int) server.getServerConfiguration().getMaxRequestSize(), @@ -565,18 +562,17 @@ private void insertMicronautHandlers(boolean zeroCopySupported) { ) ); } - pipeline.addLast(ChannelPipelineCustomizer.HANDLER_HTTP_CHUNK, new ChunkedWriteHandler()); - pipeline.addLast(HttpRequestDecoder.ID, requestDecoder); - if (server.getServerConfiguration().isDualProtocol() && server.getServerConfiguration().isHttpToHttpsRedirect() && !https) { - pipeline.addLast(ChannelPipelineCustomizer.HANDLER_HTTP_TO_HTTPS_REDIRECT, new HttpToHttpsRedirectHandler(sslConfiguration, hostResolver)); + pipeline.addLast(ChannelPipelineCustomizer.HANDLER_HTTP_CHUNK, new ChunkedWriteHandler()); // todo: move to PipeliningServerHandler + + RequestHandler requestHandler = routingInBoundHandler; + if (webSocketUpgradeHandler.isPresent()) { + webSocketUpgradeHandler.get().setNext(routingInBoundHandler); + requestHandler = webSocketUpgradeHandler.get(); } - if (sslHandler != null) { - pipeline.addLast("request-certificate-handler", new HttpRequestCertificateHandler(sslHandler)); + if (server.getServerConfiguration().isDualProtocol() && server.getServerConfiguration().isHttpToHttpsRedirect() && !https) { + requestHandler = new HttpToHttpsRedirectHandler(routingInBoundHandler.conversionService, server.getServerConfiguration(), sslConfiguration, hostResolver); } - pipeline.addLast(HttpResponseEncoder.ID, responseEncoder); - webSocketUpgradeHandler.ifPresent(h -> pipeline.addLast(ChannelPipelineCustomizer.HANDLER_WEBSOCKET_UPGRADE, h)); - - pipeline.addLast(ChannelPipelineCustomizer.HANDLER_MICRONAUT_INBOUND, routingInBoundHandler); + pipeline.addLast(ChannelPipelineCustomizer.HANDLER_MICRONAUT_INBOUND, new PipeliningServerHandler(requestHandler)); } /** @@ -590,9 +586,6 @@ private void insertHttp1DownstreamHandlers() { pipeline.addLast(ChannelPipelineCustomizer.HANDLER_ACCESS_LOGGER, accessLogHandler); } registerMicronautChannelHandlers(); - if (server.getServerConfiguration().getServerType() == NettyHttpServerConfiguration.HttpServerType.STREAMED) { - pipeline.addLast(ChannelPipelineCustomizer.HANDLER_FLOW_CONTROL, new FlowControlHandler()); - } pipeline.addLast(ChannelPipelineCustomizer.HANDLER_HTTP_KEEP_ALIVE, new HttpServerKeepAliveHandler()); insertMicronautHandlers(sslHandler == null && !https); diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/HttpToHttpsRedirectHandler.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/HttpToHttpsRedirectHandler.java index cadae629f5b..4b0783bdd55 100644 --- a/http-server-netty/src/main/java/io/micronaut/http/server/netty/HttpToHttpsRedirectHandler.java +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/HttpToHttpsRedirectHandler.java @@ -16,66 +16,64 @@ package io.micronaut.http.server.netty; import io.micronaut.core.annotation.Internal; -import io.micronaut.http.HttpRequest; +import io.micronaut.core.convert.ConversionService; import io.micronaut.http.HttpResponse; import io.micronaut.http.MutableHttpResponse; import io.micronaut.http.netty.NettyHttpResponseBuilder; +import io.micronaut.http.server.netty.configuration.NettyHttpServerConfiguration; +import io.micronaut.http.server.netty.handler.PipeliningServerHandler; +import io.micronaut.http.server.netty.handler.RequestHandler; import io.micronaut.http.server.util.HttpHostResolver; import io.micronaut.http.ssl.ServerSslConfiguration; import io.micronaut.http.uri.UriBuilder; -import io.netty.channel.ChannelDuplexHandler; -import io.netty.channel.ChannelHandler; import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.http.FullHttpResponse; import io.netty.handler.codec.http.HttpHeaderNames; import io.netty.handler.codec.http.HttpHeaderValues; -import io.netty.handler.ssl.SslHandler; /** * Handler to automatically redirect HTTP to HTTPS request when using dual protocol. * + * @param conversionService The conversion service + * @param serverConfiguration The server configuration + * @param sslConfiguration The SSL configuration + * @param hostResolver The host resolver * @author Iván López * @since 2.5.0 */ -@ChannelHandler.Sharable @Internal -final class HttpToHttpsRedirectHandler extends ChannelDuplexHandler { - - private final ServerSslConfiguration sslConfiguration; - private final HttpHostResolver hostResolver; - - /** - * Construct HttpToHttpsRedirectHandler for the given arguments. - * - * @param sslConfiguration The {@link ServerSslConfiguration} - * @param hostResolver The {@link HttpHostResolver} - */ - public HttpToHttpsRedirectHandler(ServerSslConfiguration sslConfiguration, - HttpHostResolver hostResolver) { - this.hostResolver = hostResolver; - this.sslConfiguration = sslConfiguration; - } +record HttpToHttpsRedirectHandler( + ConversionService conversionService, + NettyHttpServerConfiguration serverConfiguration, + ServerSslConfiguration sslConfiguration, + HttpHostResolver hostResolver +) implements RequestHandler { @Override - public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { - if (msg instanceof HttpRequest && ctx.pipeline().get(SslHandler.class) == null) { - HttpRequest request = (HttpRequest) msg; - UriBuilder uriBuilder = UriBuilder.of(hostResolver.resolve(request)); - uriBuilder.scheme("https"); - int port = sslConfiguration.getPort(); - if (port == 443) { - uriBuilder.port(-1); - } else { - uriBuilder.port(port); - } - uriBuilder.path(request.getPath()); + public void accept(ChannelHandlerContext ctx, io.netty.handler.codec.http.HttpRequest request, PipeliningServerHandler.OutboundAccess outboundAccess) { + NettyHttpRequest strippedRequest = NettyHttpRequest.createSafe(request, ctx, conversionService, serverConfiguration); - MutableHttpResponse response = HttpResponse - .permanentRedirect(uriBuilder.build()) - .header(HttpHeaderNames.CONNECTION, HttpHeaderValues.CLOSE); - io.netty.handler.codec.http.HttpResponse nettyResponse = NettyHttpResponseBuilder.toHttpResponse(response); - ctx.writeAndFlush(nettyResponse); + UriBuilder uriBuilder = UriBuilder.of(hostResolver.resolve(strippedRequest)); + strippedRequest.release(); + uriBuilder.scheme("https"); + int port = sslConfiguration.getPort(); + if (port == 443) { + uriBuilder.port(-1); } else { - ctx.fireChannelRead(msg); + uriBuilder.port(port); } + uriBuilder.path(strippedRequest.getPath()); + + MutableHttpResponse response = HttpResponse + .permanentRedirect(uriBuilder.build()) + .header(HttpHeaderNames.CONNECTION, HttpHeaderValues.CLOSE); + io.netty.handler.codec.http.HttpResponse nettyResponse = NettyHttpResponseBuilder.toHttpResponse(response); + outboundAccess.closeAfterWrite(); + outboundAccess.writeFull((FullHttpResponse) nettyResponse); + } + + @Override + public void handleUnboundError(Throwable cause) { + // this connection doesn't process requests, so just ignore errors } } diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/NettyEmbeddedServices.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/NettyEmbeddedServices.java index 9d1d73036aa..2c98533fbc5 100644 --- a/http-server-netty/src/main/java/io/micronaut/http/server/netty/NettyEmbeddedServices.java +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/NettyEmbeddedServices.java @@ -28,6 +28,7 @@ import io.micronaut.http.server.RouteExecutor; import io.micronaut.http.server.binding.RequestArgumentSatisfier; import io.micronaut.http.server.netty.ssl.ServerSslBuilder; +import io.micronaut.http.server.netty.websocket.NettyServerWebSocketUpgradeHandler; import io.micronaut.scheduling.executor.ExecutorSelector; import io.micronaut.web.router.Router; import io.micronaut.web.router.resource.StaticResourceResolver; @@ -35,7 +36,6 @@ import io.netty.channel.ChannelOutboundHandler; import io.netty.channel.EventLoopGroup; import io.netty.channel.ServerChannel; -import io.netty.channel.SimpleChannelInboundHandler; import io.netty.channel.socket.ServerSocketChannel; import java.util.List; @@ -124,7 +124,7 @@ default ExecutorSelector getExecutorSelector() { * @return The websocket upgrade handler if present */ @SuppressWarnings("java:S1452") - Optional>> getWebSocketUpgradeHandler(NettyEmbeddedServer embeddedServer); + Optional getWebSocketUpgradeHandler(NettyEmbeddedServer embeddedServer); /** * @return The event loop group registry. diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/NettyHttpRequest.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/NettyHttpRequest.java index 78d300a56f4..c9def9af0e8 100644 --- a/http-server-netty/src/main/java/io/micronaut/http/server/netty/NettyHttpRequest.java +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/NettyHttpRequest.java @@ -51,6 +51,7 @@ import io.micronaut.http.server.netty.body.HttpBody; import io.micronaut.http.server.netty.body.ImmediateMultiObjectBody; import io.micronaut.http.server.netty.body.ImmediateSingleObjectBody; +import io.micronaut.http.server.netty.configuration.NettyHttpServerConfiguration; import io.micronaut.http.server.netty.multipart.NettyCompletedFileUpload; import io.micronaut.web.router.RouteMatch; import io.netty.buffer.Unpooled; @@ -62,6 +63,7 @@ import io.netty.handler.codec.http.DefaultHttpRequest; import io.netty.handler.codec.http.DefaultLastHttpContent; import io.netty.handler.codec.http.EmptyHttpHeaders; +import io.netty.handler.codec.http.FullHttpRequest; import io.netty.handler.codec.http.HttpHeaderNames; import io.netty.handler.codec.http.QueryStringDecoder; import io.netty.handler.codec.http.cookie.ClientCookieEncoder; @@ -82,11 +84,13 @@ import java.net.URI; import java.net.URISyntaxException; import java.nio.charset.Charset; +import java.security.cert.Certificate; import java.util.HashMap; import java.util.Iterator; import java.util.Map; import java.util.Objects; import java.util.Optional; +import java.util.function.Supplier; /** * Delegates to the Netty {@link io.netty.handler.codec.http.HttpRequest} instance. @@ -166,16 +170,17 @@ public class NettyHttpRequest extends AbstractNettyHttpRequest implements private final BodyConvertor bodyConvertor = newBodyConvertor(); /** - * @param nettyRequest The {@link io.netty.handler.codec.http.HttpRequest} - * @param ctx The {@link ChannelHandlerContext} - * @param environment The Environment - * @param serverConfiguration The {@link HttpServerConfiguration} + * @param nettyRequest The {@link io.netty.handler.codec.http.HttpRequest} + * @param ctx The {@link ChannelHandlerContext} + * @param environment The Environment + * @param serverConfiguration The {@link HttpServerConfiguration} + * @throws IllegalArgumentException When the request URI is invalid */ @SuppressWarnings("MagicNumber") public NettyHttpRequest(io.netty.handler.codec.http.HttpRequest nettyRequest, ChannelHandlerContext ctx, ConversionService environment, - HttpServerConfiguration serverConfiguration) { + HttpServerConfiguration serverConfiguration) throws IllegalArgumentException { super(nettyRequest, environment); Objects.requireNonNull(nettyRequest, "Netty request cannot be null"); Objects.requireNonNull(ctx, "ChannelHandlerContext cannot be null"); @@ -193,6 +198,31 @@ public NettyHttpRequest(io.netty.handler.codec.http.HttpRequest nettyRequest, this.origin = headers.getOrigin().orElse(null); } + public static NettyHttpRequest createSafe(io.netty.handler.codec.http.HttpRequest request, ChannelHandlerContext ctx, ConversionService conversionService, NettyHttpServerConfiguration serverConfiguration) { + try { + return new NettyHttpRequest<>( + request, + ctx, + conversionService, + serverConfiguration + ); + } catch (IllegalArgumentException iae) { + // invalid URI + if (request instanceof StreamedHttpRequest streamed) { + streamed.closeIfNoSubscriber(); + } else { + ((FullHttpRequest) request).release(); + } + + return new NettyHttpRequest<>( + new DefaultFullHttpRequest(request.protocolVersion(), request.method(), "/", Unpooled.EMPTY_BUFFER), + ctx, + conversionService, + serverConfiguration + ); + } + } + public final ByteBody rootBody() { return body; } @@ -211,7 +241,7 @@ private HttpBody lastBody() { public final FormRouteCompleter formRouteCompleter() { if (formRouteCompleter == null) { - formRouteCompleter = new FormRouteCompleter(this, (RouteMatch) getAttribute(HttpAttributes.ROUTE_MATCH).get()); + formRouteCompleter = new FormRouteCompleter((RouteMatch) getAttribute(HttpAttributes.ROUTE_MATCH).get(), getChannelHandlerContext().channel().eventLoop()); } return formRouteCompleter; } @@ -337,6 +367,12 @@ public HttpRequest setAttribute(CharSequence name, Object value) { return this; } + @Override + public Optional getCertificate() { + Supplier sup = channelHandlerContext.channel().attr(HttpPipelineBuilder.CERTIFICATE_SUPPLIER_ATTRIBUTE.get()).get(); + return sup == null ? Optional.empty() : Optional.ofNullable(sup.get()); + } + @Override public Optional getBody() { HttpBody lastBody = lastBody(); @@ -389,9 +425,6 @@ public void release() { if (attributes != null) { attributes.values().forEach(this::releaseIfNecessary); } - if (nettyRequest instanceof StreamedHttpRequest streamedHttpRequest) { - streamedHttpRequest.closeIfNoSubscriber(); - } } /** @@ -460,7 +493,7 @@ public PushCapableHttpRequest serverPush(@NonNull HttpRequest request) { } // request used to trigger our handlers - io.netty.handler.codec.http.HttpRequest inboundRequest = NettyHttpRequestBuilder.toHttpRequest(request); + io.netty.handler.codec.http.HttpRequest inboundRequest = NettyHttpRequestBuilder.asBuilder(request).toHttpRequestWithoutBody(); // copy headers from our request for (Iterator> itr = headers.getNettyHeaders().iteratorCharSequence(); itr.hasNext(); ) { @@ -540,13 +573,31 @@ public final boolean isFormOrMultipartData() { return ct != null && (ct.equals(MediaType.APPLICATION_FORM_URLENCODED_TYPE) || ct.equals(MediaType.MULTIPART_FORM_DATA_TYPE)); } - /** - * @return Return true if the request is form data. - */ - @Internal - public final boolean isFormData() { - MediaType ct = getContentType().orElse(null); - return ct != null && (ct.equals(MediaType.APPLICATION_FORM_URLENCODED_TYPE)); + @Override + @Deprecated + public io.netty.handler.codec.http.HttpRequest toHttpRequest() { + return toHttpRequestWithoutBody(); + } + + @Override + public Optional toHttpRequestDirect() { + return Optional.of(rootBody().claimForReuse(nettyRequest)); + } + + @Override + public io.netty.handler.codec.http.HttpRequest toHttpRequestWithoutBody() { + if (nettyRequest instanceof FullHttpRequest) { + // do not include body, the body is owned by us + DefaultHttpRequest copy = new DefaultHttpRequest( + nettyRequest.protocolVersion(), + nettyRequest.method(), + nettyRequest.uri(), + nettyRequest.headers() + ); + copy.setDecoderResult(nettyRequest.decoderResult()); + return copy; + } + return nettyRequest; } @Override @@ -702,6 +753,7 @@ public URI getUri() { @NonNull @Override + @Deprecated public io.netty.handler.codec.http.FullHttpRequest toFullHttpRequest() { io.netty.handler.codec.http.HttpRequest nr = NettyHttpRequest.this.nettyRequest; if (nr instanceof io.netty.handler.codec.http.FullHttpRequest) { @@ -720,6 +772,7 @@ public io.netty.handler.codec.http.FullHttpRequest toFullHttpRequest() { @NonNull @Override + @Deprecated public StreamedHttpRequest toStreamHttpRequest() { if (isStream()) { return (StreamedHttpRequest) NettyHttpRequest.this.nettyRequest; @@ -739,6 +792,7 @@ public StreamedHttpRequest toStreamHttpRequest() { @NonNull @Override + @Deprecated public io.netty.handler.codec.http.HttpRequest toHttpRequest() { if (isStream()) { return toStreamHttpRequest(); @@ -747,6 +801,7 @@ public io.netty.handler.codec.http.HttpRequest toHttpRequest() { } @Override + @Deprecated public boolean isStream() { return NettyHttpRequest.this.nettyRequest instanceof StreamedHttpRequest; } @@ -755,6 +810,16 @@ public boolean isStream() { public MutableHttpRequest mutate() { return new NettyMutableHttpRequest(); } + + @Override + public io.netty.handler.codec.http.HttpRequest toHttpRequestWithoutBody() { + return NettyHttpRequest.this.toHttpRequestWithoutBody(); + } + + @Override + public Optional toHttpRequestDirect() { + return body != null ? Optional.empty() : NettyHttpRequest.this.toHttpRequestDirect(); + } } private abstract static class BodyConvertor { diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/NettyRequestLifecycle.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/NettyRequestLifecycle.java index 64c0434535b..cc7ae28071a 100644 --- a/http-server-netty/src/main/java/io/micronaut/http/server/netty/NettyRequestLifecycle.java +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/NettyRequestLifecycle.java @@ -26,11 +26,11 @@ import io.micronaut.http.context.ServerRequestContext; import io.micronaut.http.server.RequestLifecycle; import io.micronaut.http.server.netty.body.ByteBody; +import io.micronaut.http.server.netty.handler.PipeliningServerHandler; import io.micronaut.http.server.netty.types.files.NettyStreamedFileCustomizableResponseType; import io.micronaut.http.server.netty.types.files.NettySystemFileCustomizableResponseType; import io.micronaut.http.server.types.files.FileCustomizableResponseType; import io.micronaut.web.router.RouteMatch; -import io.netty.channel.ChannelHandlerContext; import io.netty.handler.codec.DecoderResult; import io.netty.handler.codec.TooLongFrameException; import org.slf4j.Logger; @@ -47,7 +47,7 @@ final class NettyRequestLifecycle extends RequestLifecycle { private static final Logger LOG = LoggerFactory.getLogger(NettyRequestLifecycle.class); private final RoutingInBoundHandler rib; - private final ChannelHandlerContext ctx; + private final PipeliningServerHandler.OutboundAccess outboundAccess; /** * Should only be used where netty-specific stuff is needed, such as reading the body or @@ -55,18 +55,16 @@ final class NettyRequestLifecycle extends RequestLifecycle { */ private final NettyHttpRequest nettyRequest; - NettyRequestLifecycle(RoutingInBoundHandler rib, ChannelHandlerContext ctx, NettyHttpRequest request) { + NettyRequestLifecycle(RoutingInBoundHandler rib, PipeliningServerHandler.OutboundAccess outboundAccess, NettyHttpRequest request) { super(rib.routeExecutor, request); this.rib = rib; - this.ctx = ctx; + this.outboundAccess = outboundAccess; this.nettyRequest = request; multipartEnabled(rib.multipartEnabled); } void handleNormal() { - ctx.channel().config().setAutoRead(false); - if (LOG.isDebugEnabled()) { HttpMethod httpMethod = request().getMethod(); ServerRequestContext.set(request()); @@ -75,20 +73,23 @@ void handleNormal() { ExecutionFlow> result; - // handle decoding failure - DecoderResult decoderResult = nettyRequest.getNativeRequest().decoderResult(); - if (decoderResult.isFailure()) { - Throwable cause = decoderResult.cause(); - HttpStatus status = cause instanceof TooLongFrameException ? HttpStatus.REQUEST_ENTITY_TOO_LARGE : HttpStatus.BAD_REQUEST; - result = onStatusError( - HttpResponse.status(status), - status.getReason() - ); - } else { - result = normalFlow(); + try { + // handle decoding failure + DecoderResult decoderResult = nettyRequest.getNativeRequest().decoderResult(); + if (decoderResult.isFailure()) { + Throwable cause = decoderResult.cause(); + HttpStatus status = cause instanceof TooLongFrameException ? HttpStatus.REQUEST_ENTITY_TOO_LARGE : HttpStatus.BAD_REQUEST; + result = onStatusError( + HttpResponse.status(status), + status.getReason() + ); + } else { + result = normalFlow(); + } + result.onComplete((response, throwable) -> rib.writeResponse(outboundAccess, nettyRequest, response, throwable)); + } catch (Exception e) { + handleException(e); } - - result.onComplete((response, throwable) -> rib.writeResponse(ctx, nettyRequest, response, throwable)); } @Nullable @@ -129,7 +130,6 @@ protected ExecutionFlow> fulfillArguments(RouteMatch routeMatch private ExecutionFlow> waitForBody(RouteMatch routeMatch) { // note: shouldReadBody only works when fulfill has been called at least once if (nettyRequest.rootBody().next() != null) { - ctx.read(); return ExecutionFlow.just(routeMatch); } HttpContentProcessor processor = rib.httpContentProcessorResolver.resolve(nettyRequest, routeMatch); @@ -155,13 +155,12 @@ private ExecutionFlow> waitForBody(RouteMatch routeMatch) { return routeMatch; }); } else { - ctx.read(); return ExecutionFlow.just(routeMatch); } } void handleException(Throwable cause) { - onError(cause).onComplete((response, throwable) -> rib.writeResponse(ctx, nettyRequest, response, throwable)); + onError(cause).onComplete((response, throwable) -> rib.writeResponse(outboundAccess, nettyRequest, response, throwable)); } private boolean needsBody(RouteMatch routeMatch) { diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/RoutingInBoundHandler.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/RoutingInBoundHandler.java index 0115b4af9f0..bc4d0ee5d58 100644 --- a/http-server-netty/src/main/java/io/micronaut/http/server/netty/RoutingInBoundHandler.java +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/RoutingInBoundHandler.java @@ -43,10 +43,14 @@ import io.micronaut.http.netty.NettyMutableHttpResponse; import io.micronaut.http.netty.stream.JsonSubscriber; import io.micronaut.http.netty.stream.StreamedHttpRequest; +import io.micronaut.http.netty.stream.StreamedHttpResponse; import io.micronaut.http.server.RouteExecutor; import io.micronaut.http.server.binding.RequestArgumentSatisfier; import io.micronaut.http.server.exceptions.InternalServerException; import io.micronaut.http.server.netty.configuration.NettyHttpServerConfiguration; +import io.micronaut.http.server.netty.handler.PipeliningServerHandler; +import io.micronaut.http.server.netty.handler.RequestHandler; +import io.micronaut.http.server.netty.types.NettyCustomizableResponseType; import io.micronaut.http.server.netty.types.NettyCustomizableResponseTypeHandler; import io.micronaut.http.server.netty.types.NettyCustomizableResponseTypeHandlerRegistry; import io.micronaut.runtime.http.codec.TextPlainCodec; @@ -56,29 +60,20 @@ import io.netty.buffer.ByteBufOutputStream; import io.netty.buffer.CompositeByteBuf; import io.netty.buffer.Unpooled; -import io.netty.channel.ChannelFuture; -import io.netty.channel.ChannelFutureListener; import io.netty.channel.ChannelHandler.Sharable; import io.netty.channel.ChannelHandlerContext; -import io.netty.channel.SimpleChannelInboundHandler; +import io.netty.handler.codec.http.DefaultFullHttpRequest; import io.netty.handler.codec.http.DefaultFullHttpResponse; import io.netty.handler.codec.http.DefaultHttpContent; import io.netty.handler.codec.http.DefaultHttpHeaders; -import io.netty.handler.codec.http.DefaultHttpResponse; +import io.netty.handler.codec.http.FullHttpRequest; +import io.netty.handler.codec.http.FullHttpResponse; import io.netty.handler.codec.http.HttpContent; import io.netty.handler.codec.http.HttpHeaderNames; import io.netty.handler.codec.http.HttpHeaderValues; import io.netty.handler.codec.http.HttpResponseStatus; import io.netty.handler.codec.http.HttpVersion; -import io.netty.handler.codec.http2.Http2Error; -import io.netty.handler.codec.http2.Http2Exception; -import io.netty.handler.timeout.IdleState; -import io.netty.handler.timeout.IdleStateEvent; -import io.netty.util.concurrent.Future; -import io.netty.util.concurrent.GenericFutureListener; import org.reactivestreams.Publisher; -import org.reactivestreams.Subscriber; -import org.reactivestreams.Subscription; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import reactor.core.publisher.Flux; @@ -102,7 +97,7 @@ @Internal @Sharable @SuppressWarnings("FileLength") -final class RoutingInBoundHandler extends SimpleChannelInboundHandler> { +public final class RoutingInBoundHandler implements RequestHandler { private static final Logger LOG = LoggerFactory.getLogger(RoutingInBoundHandler.class); /* @@ -154,59 +149,31 @@ final class RoutingInBoundHandler extends SimpleChannelInboundHandler request) { + private void cleanupRequest(NettyHttpRequest request) { try { request.release(); } finally { if (!terminateEventPublisher.isEmpty()) { - ctx.executor().execute(() -> { - try { - terminateEventPublisher.publishEvent(new HttpRequestTerminatedEvent(request)); - } catch (Exception e) { - if (LOG.isErrorEnabled()) { - LOG.error("Error publishing request terminated event: " + e.getMessage(), e); - } + try { + terminateEventPublisher.publishEvent(new HttpRequestTerminatedEvent(request)); + } catch (Exception e) { + if (LOG.isErrorEnabled()) { + LOG.error("Error publishing request terminated event: " + e.getMessage(), e); } - }); + } } } } @Override - public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception { - try { - if (evt instanceof IdleStateEvent idleStateEvent) { - IdleState state = idleStateEvent.state(); - if (state == IdleState.ALL_IDLE) { - ctx.close(); - } - } - } finally { - super.userEventTriggered(ctx, evt); + public void responseWritten(Object attachment) { + if (attachment != null) { + cleanupRequest((NettyHttpRequest) attachment); } } @Override - public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { + public void handleUnboundError(Throwable cause) { // short-circuit ignorable exceptions: This is also handled by RouteExecutor, but handling this early avoids // running any filters if (isIgnorable(cause)) { @@ -216,42 +183,52 @@ public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { return; } - NettyHttpRequest nettyHttpRequest = NettyHttpRequest.remove(ctx); - if (nettyHttpRequest == null) { - if (cause instanceof SSLException || cause.getCause() instanceof SSLException) { - if (LOG.isDebugEnabled()) { - LOG.debug("Micronaut Server Error - No request state present. Cause: " + cause.getMessage(), cause); - } - } else { - if (LOG.isErrorEnabled()) { - LOG.error("Micronaut Server Error - No request state present. Cause: " + cause.getMessage(), cause); - } + if (cause instanceof SSLException || cause.getCause() instanceof SSLException) { + if (LOG.isDebugEnabled()) { + LOG.debug("Micronaut Server Error - No request state present. Cause: " + cause.getMessage(), cause); + } + } else { + if (LOG.isErrorEnabled()) { + LOG.error("Micronaut Server Error - No request state present. Cause: " + cause.getMessage(), cause); } - - ctx.writeAndFlush(new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.INTERNAL_SERVER_ERROR)); - return; } - new NettyRequestLifecycle(this, ctx, nettyHttpRequest).handleException(cause); } @Override - protected void channelRead0(ChannelHandlerContext ctx, io.micronaut.http.HttpRequest httpRequest) { - new NettyRequestLifecycle(this, ctx, (NettyHttpRequest) httpRequest).handleNormal(); + public void accept(ChannelHandlerContext ctx, io.netty.handler.codec.http.HttpRequest request, PipeliningServerHandler.OutboundAccess outboundAccess) { + NettyHttpRequest mnRequest; + try { + mnRequest = new NettyHttpRequest<>(request, ctx, conversionService, serverConfiguration); + } catch (IllegalArgumentException e) { + // invalid URI + NettyHttpRequest errorRequest = new NettyHttpRequest<>( + new DefaultFullHttpRequest(request.protocolVersion(), request.method(), "/", Unpooled.EMPTY_BUFFER), + ctx, + conversionService, + serverConfiguration + ); + new NettyRequestLifecycle(this, outboundAccess, errorRequest).handleException(e.getCause() == null ? e : e.getCause()); + if (request instanceof StreamedHttpRequest streamed) { + streamed.closeIfNoSubscriber(); + } else { + ((FullHttpRequest) request).release(); + } + return; + } + new NettyRequestLifecycle(this, outboundAccess, mnRequest).handleNormal(); } - void writeResponse(ChannelHandlerContext ctx, - NettyHttpRequest nettyHttpRequest, - MutableHttpResponse response, - Throwable throwable) { + public void writeResponse(PipeliningServerHandler.OutboundAccess outboundAccess, + NettyHttpRequest nettyHttpRequest, + MutableHttpResponse response, + Throwable throwable) { if (throwable != null) { response = routeExecutor.createDefaultErrorResponse(nettyHttpRequest, throwable); } - if (response == null) { - ctx.read(); - } else { + if (response != null) { try { encodeHttpResponse( - ctx, + outboundAccess, nettyHttpRequest, response, null, @@ -260,7 +237,7 @@ void writeResponse(ChannelHandlerContext ctx, } catch (Throwable e) { response = routeExecutor.createDefaultErrorResponse(nettyHttpRequest, e); encodeHttpResponse( - ctx, + outboundAccess, nettyHttpRequest, response, null, @@ -285,7 +262,7 @@ ExecutorService getIoExecutor() { } private void encodeHttpResponse( - ChannelHandlerContext context, + PipeliningServerHandler.OutboundAccess outboundAccess, NettyHttpRequest nettyRequest, MutableHttpResponse response, @Nullable Argument bodyType, @@ -295,7 +272,7 @@ private void encodeHttpResponse( if (isNotHead) { if (body instanceof Writable) { getIoExecutor().execute(() -> { - ByteBuf byteBuf = context.alloc().ioBuffer(128); + ByteBuf byteBuf = nettyRequest.getChannelHandlerContext().alloc().ioBuffer(128); ByteBufOutputStream outputStream = new ByteBufOutputStream(byteBuf); try { Writable writable = (Writable) body; @@ -308,14 +285,14 @@ private void encodeHttpResponse( writeFinalNettyResponse( response, nettyRequest, - context + outboundAccess ); } catch (IOException e) { final MutableHttpResponse errorResponse = routeExecutor.createDefaultErrorResponse(nettyRequest, e); writeFinalNettyResponse( errorResponse, nettyRequest, - context + outboundAccess ); } }); @@ -323,13 +300,13 @@ private void encodeHttpResponse( response.body(null); if (serverConfiguration.getServerType() == NettyHttpServerConfiguration.HttpServerType.FULL_CONTENT) { // HttpStreamsHandler is not present, so we can't write a StreamedHttpResponse. - Flux.from(mapToHttpContent(nettyRequest, response, body, context)).collectList().subscribe(contents -> { + Flux.from(mapToHttpContent(nettyRequest, response, body)).collectList().subscribe(contents -> { if (contents.size() == 0) { setResponseBody(response, Unpooled.EMPTY_BUFFER); } else if (contents.size() == 1) { setResponseBody(response, contents.get(0).content().retain()); } else { - CompositeByteBuf composite = context.alloc().compositeBuffer(); + CompositeByteBuf composite = nettyRequest.getChannelHandlerContext().alloc().compositeBuffer(); for (HttpContent c : contents) { composite.addComponent(true, c.content().retain()); } @@ -342,7 +319,7 @@ private void encodeHttpResponse( writeFinalNettyResponse( response, nettyRequest, - context + outboundAccess ); }, error -> { if (LOG.isErrorEnabled()) { @@ -354,20 +331,20 @@ private void encodeHttpResponse( } else { responseStatus = HttpResponseStatus.INTERNAL_SERVER_ERROR; } - context.writeAndFlush(new DefaultHttpResponse(HttpVersion.HTTP_1_1, responseStatus)) - .addListener(ChannelFutureListener.CLOSE); + outboundAccess.closeAfterWrite(); + outboundAccess.writeFull(new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, responseStatus)); }); } else { DelegateStreamedHttpResponse streamedResponse = new DelegateStreamedHttpResponse( toNettyResponse(response), - mapToHttpContent(nettyRequest, response, body, context) + mapToHttpContent(nettyRequest, response, body) ); - context.writeAndFlush(streamedResponse); - context.read(); + outboundAccess.attachment(nettyRequest); + outboundAccess.writeStreamed(streamedResponse); } } else { encodeResponseBody( - context, + nettyRequest.getChannelHandlerContext(), nettyRequest, response, bodyType, @@ -377,7 +354,7 @@ private void encodeHttpResponse( writeFinalNettyResponse( response, nettyRequest, - context + outboundAccess ); } } else { @@ -385,15 +362,14 @@ private void encodeHttpResponse( writeFinalNettyResponse( response, nettyRequest, - context + outboundAccess ); } } private Flux mapToHttpContent(NettyHttpRequest request, MutableHttpResponse response, - Object body, - ChannelHandlerContext context) { + Object body) { final RouteInfo routeInfo = response.getAttribute(HttpAttributes.ROUTE_INFO, RouteInfo.class).orElse(null); final boolean hasRouteInfo = routeInfo != null; MediaType mediaType = response.getContentType().orElse(null); @@ -402,7 +378,7 @@ private Flux mapToHttpContent(NettyHttpRequest request, } boolean isJson = mediaType != null && mediaType.getExtension().equals(MediaType.EXTENSION_JSON) && isJsonFormattable(hasRouteInfo ? routeInfo.getBodyType() : null); - NettyByteBufferFactory byteBufferFactory = new NettyByteBufferFactory(context.alloc()); + NettyByteBufferFactory byteBufferFactory = new NettyByteBufferFactory(request.getChannelHandlerContext().alloc()); Flux bodyPublisher = Flux.from(Publishers.convertPublisher(conversionService, body, Publisher.class)); @@ -455,11 +431,7 @@ private Flux mapToHttpContent(NettyHttpRequest request, } httpContentPublisher = httpContentPublisher - .contextWrite(reactorContext -> reactorContext.put(ServerRequestContext.KEY, request)) - .doOnNext(httpContent -> - // once an http content is written, read the next item if it is available - context.read()) - .doAfterTerminate(() -> cleanupRequest(context, request)); + .contextWrite(reactorContext -> reactorContext.put(ServerRequestContext.KEY, request)); return httpContentPublisher; } @@ -531,7 +503,7 @@ private void encodeResponseBody( } - private void writeFinalNettyResponse(MutableHttpResponse message, HttpRequest request, ChannelHandlerContext context) { + private void writeFinalNettyResponse(MutableHttpResponse message, HttpRequest request, PipeliningServerHandler.OutboundAccess outboundAccess) { int httpStatus = message.code(); final io.micronaut.http.HttpVersion httpVersion = request.getHttpVersion(); @@ -540,30 +512,6 @@ private void writeFinalNettyResponse(MutableHttpResponse message, HttpRequest boolean decodeError = request instanceof NettyHttpRequest && ((NettyHttpRequest) request).getNativeRequest().decoderResult().isFailure(); - GenericFutureListener> requestCompletor = future -> { - try { - if (!future.isSuccess()) { - final Throwable throwable = future.cause(); - if (!isIgnorable(throwable)) { - if (throwable instanceof Http2Exception.StreamException se) { - if (se.error() == Http2Error.STREAM_CLOSED) { - // ignore - return; - } - } - if (LOG.isErrorEnabled()) { - LOG.error("Error writing final response: " + throwable.getMessage(), throwable); - } - } - } - } finally { - if (request instanceof NettyHttpRequest) { - cleanupRequest(context, (NettyHttpRequest) request); - } - context.read(); - } - }; - final Object body = message.body(); if (body instanceof NettyCustomizableResponseTypeHandlerInvoker) { // default Connection header if not set explicitly @@ -578,7 +526,8 @@ private void writeFinalNettyResponse(MutableHttpResponse message, HttpRequest } NettyCustomizableResponseTypeHandlerInvoker handler = (NettyCustomizableResponseTypeHandlerInvoker) body; message.body(null); - handler.invoke(request, message, context).addListener(requestCompletor); + outboundAccess.attachment(request instanceof NettyHttpRequest ? request : null); + outboundAccess.writeStreamed(handler.invoke(request, message)); } else { io.netty.handler.codec.http.HttpResponse nettyResponse = NettyHttpResponseBuilder.toHttpResponse(message); io.netty.handler.codec.http.HttpHeaders nettyHeaders = nettyResponse.headers(); @@ -601,53 +550,30 @@ private void writeFinalNettyResponse(MutableHttpResponse message, HttpRequest } // close handled by HttpServerKeepAliveHandler final NettyHttpRequest nettyHttpRequest = (NettyHttpRequest) request; - - io.netty.handler.codec.http.HttpRequest nativeRequest = nettyHttpRequest.getNativeRequest(); - - if (nativeRequest instanceof StreamedHttpRequest streamedHttpRequest && !streamedHttpRequest.isConsumed()) { - // We have to clear the buffer of FlowControlHandler before writing the response - // If this is a streamed request and there is still content to consume then subscribe - // and write the buffer is empty. - - //noinspection ReactiveStreamsSubscriberImplementation - streamedHttpRequest.subscribe(new Subscriber() { - private Subscription streamSub; - - @Override - public void onSubscribe(Subscription s) { - streamSub = s; - s.request(1); - } - - @Override - public void onNext(HttpContent httpContent) { - httpContent.release(); - streamSub.request(1); - } - - @Override - public void onError(Throwable t) { - syncWriteAndFlushNettyResponse(context, request, nettyResponse, requestCompletor); - } - - @Override - public void onComplete() { - syncWriteAndFlushNettyResponse(context, request, nettyResponse, requestCompletor); - } - }); - } else { - syncWriteAndFlushNettyResponse(context, request, nettyResponse, requestCompletor); + if (nettyHttpRequest.getNativeRequest() instanceof StreamedHttpRequest streamed && !streamed.isConsumed()) { + // consume incoming data + Flux.from(streamed).subscribe(HttpContent::release); } + syncWriteAndFlushNettyResponse(outboundAccess, request, nettyResponse); } } private void syncWriteAndFlushNettyResponse( - ChannelHandlerContext context, + PipeliningServerHandler.OutboundAccess outboundAccess, HttpRequest request, - io.netty.handler.codec.http.HttpResponse nettyResponse, - GenericFutureListener> requestCompletor + io.netty.handler.codec.http.HttpResponse nettyResponse ) { - context.writeAndFlush(nettyResponse).addListener(requestCompletor); + if (nettyResponse instanceof StreamedHttpResponse streamed) { + outboundAccess.attachment(request instanceof NettyHttpRequest ? request : null); + outboundAccess.writeStreamed(streamed); + } else { + if (request instanceof NettyHttpRequest nettyRequest) { + // no need to wait, can release immediately + cleanupRequest(nettyRequest); + outboundAccess.attachment(null); + } + outboundAccess.writeFull((FullHttpResponse) nettyResponse); + } if (LOG.isDebugEnabled()) { LOG.debug("Response {} - {} {}", @@ -788,8 +714,8 @@ private static class NettyCustomizableResponseTypeHandlerInvoker { } @SuppressWarnings("unchecked") - ChannelFuture invoke(HttpRequest request, MutableHttpResponse response, ChannelHandlerContext channelHandlerContext) { - return this.handler.handle(body, request, response, channelHandlerContext); + NettyCustomizableResponseType.CustomResponse invoke(HttpRequest request, MutableHttpResponse response) { + return this.handler.handle(body, request, response); } } } diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/StreamTypeHandler.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/StreamTypeHandler.java index ca8738d1132..a10610c2d85 100644 --- a/http-server-netty/src/main/java/io/micronaut/http/server/netty/StreamTypeHandler.java +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/StreamTypeHandler.java @@ -18,11 +18,10 @@ import io.micronaut.core.annotation.Internal; import io.micronaut.http.HttpRequest; import io.micronaut.http.MutableHttpResponse; +import io.micronaut.http.server.netty.types.NettyCustomizableResponseType; import io.micronaut.http.server.netty.types.NettyCustomizableResponseTypeHandler; import io.micronaut.http.server.netty.types.stream.NettyStreamedCustomizableResponseType; import io.micronaut.http.server.types.CustomizableResponseTypeException; -import io.netty.channel.ChannelFuture; -import io.netty.channel.ChannelHandlerContext; import java.io.InputStream; import java.util.Arrays; @@ -39,7 +38,7 @@ class StreamTypeHandler implements NettyCustomizableResponseTypeHandler private static final Class[] SUPPORTED_TYPES = new Class[]{NettyStreamedCustomizableResponseType.class, InputStream.class}; @Override - public ChannelFuture handle(Object object, HttpRequest request, MutableHttpResponse response, ChannelHandlerContext context) { + public NettyCustomizableResponseType.CustomResponse handle(Object object, HttpRequest request, MutableHttpResponse response) { NettyStreamedCustomizableResponseType type; if (object instanceof InputStream) { @@ -51,7 +50,7 @@ public ChannelFuture handle(Object object, HttpRequest request, MutableHttpRe } type.process(response); - return type.write(request, response, context); + return type.write(request, response); } @Override diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/binders/PartUploadAnnotationBinder.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/binders/PartUploadAnnotationBinder.java index 9727114e103..3e3224329b9 100644 --- a/http-server-netty/src/main/java/io/micronaut/http/server/netty/binders/PartUploadAnnotationBinder.java +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/binders/PartUploadAnnotationBinder.java @@ -20,11 +20,13 @@ import io.micronaut.core.convert.ConversionError; import io.micronaut.core.convert.ConversionService; import io.micronaut.core.type.Argument; +import io.micronaut.http.HttpRequest; import io.micronaut.http.annotation.Part; import io.micronaut.http.bind.binders.AnnotatedRequestArgumentBinder; import io.micronaut.http.bind.binders.PendingRequestBindingResult; -import io.micronaut.http.netty.stream.StreamedHttpRequest; +import io.micronaut.http.bind.binders.RequestArgumentBinder; import io.micronaut.http.server.netty.NettyHttpRequest; +import io.micronaut.http.server.netty.converters.NettyConverters; import reactor.core.publisher.Mono; import java.util.List; @@ -38,7 +40,7 @@ * @author Denis Stepanov * @since 4.0.0 */ -public class PartUploadAnnotationBinder implements AnnotatedRequestArgumentBinder, StreamedNettyRequestArgumentBinder { +public class PartUploadAnnotationBinder implements AnnotatedRequestArgumentBinder, RequestArgumentBinder { private final ConversionService conversionService; private final CompletedFileUploadBinder completedFileUploadBinder; @@ -53,10 +55,11 @@ public PartUploadAnnotationBinder(ConversionService conversionService, } @Override - public BindingResult bindForStreamedNettyRequest(ArgumentConversionContext context, - StreamedHttpRequest streamedHttpRequest, - NettyHttpRequest request) { - if (request.getContentType().isEmpty() || !request.isFormOrMultipartData()) { + public BindingResult bind(ArgumentConversionContext context, HttpRequest request) { + if (!(request instanceof NettyHttpRequest nettyRequest)) { + return BindingResult.unsatisfied(); + } + if (request.getContentType().isEmpty() || !nettyRequest.isFormOrMultipartData()) { return BindingResult.unsatisfied(); } if (completedFileUploadBinder.matches(context.getArgument().getType())) { @@ -69,8 +72,8 @@ public BindingResult bindForStreamedNettyRequest(ArgumentConversionContext Argument argument = context.getArgument(); String inputName = argument.getAnnotationMetadata().stringValue(Bindable.NAME).orElse(argument.getName()); - CompletableFuture completableFuture = Mono.from(request.formRouteCompleter().claimFieldsComplete(inputName)) - .map(d -> conversionService.convert(d, argument.getType(), context).orElse(null)) + CompletableFuture completableFuture = Mono.from(nettyRequest.formRouteCompleter().claimFieldsComplete(inputName)) + .map(d -> NettyConverters.refCountAwareConvert(conversionService, d, argument.getType(), context).orElse(null)) .toFuture(); return new PendingRequestBindingResult<>() { diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/binders/PublisherBodyBinder.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/binders/PublisherBodyBinder.java index bab66049422..e8969b4b630 100644 --- a/http-server-netty/src/main/java/io/micronaut/http/server/netty/binders/PublisherBodyBinder.java +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/binders/PublisherBodyBinder.java @@ -19,7 +19,6 @@ import io.micronaut.core.convert.ConversionError; import io.micronaut.core.convert.ConversionService; import io.micronaut.core.convert.exceptions.ConversionErrorException; -import io.micronaut.core.io.buffer.ReferenceCounted; import io.micronaut.core.type.Argument; import io.micronaut.http.HttpRequest; import io.micronaut.http.bind.binders.NonBlockingBodyArgumentBinder; @@ -27,10 +26,10 @@ import io.micronaut.http.server.netty.NettyHttpRequest; import io.micronaut.http.server.netty.NettyHttpServer; import io.micronaut.http.server.netty.body.ImmediateByteBody; +import io.micronaut.http.server.netty.converters.NettyConverters; import io.micronaut.web.router.exceptions.UnsatisfiedRouteException; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufHolder; -import io.netty.util.ReferenceCountUtil; import jakarta.inject.Singleton; import org.reactivestreams.Publisher; import org.slf4j.Logger; @@ -122,28 +121,23 @@ private static RuntimeException extractError(Object message, ArgumentConversionC * @return The converted object */ static Object convertAndRelease(ConversionService conversionService, ArgumentConversionContext conversionContext, Object o) { - try { - if (o instanceof ByteBufHolder holder) { - o = holder.content(); - if (!((ByteBuf) o).isReadable()) { - return null; - } + if (o instanceof ByteBufHolder holder) { + o = holder.content(); + if (!((ByteBuf) o).isReadable()) { + return null; } + } - Optional converted = conversionService.convert(o, conversionContext); - if (converted.isPresent()) { - Object conv = converted.get(); - if (conv instanceof ReferenceCounted rc) { - rc.retain(); - } else if (conv instanceof io.netty.util.ReferenceCounted rc) { - rc.retain(); - } - return conv; - } else { - throw extractError(o, conversionContext); - } - } finally { - ReferenceCountUtil.release(o); + Optional converted; + if (o instanceof io.netty.util.ReferenceCounted rc) { + converted = NettyConverters.refCountAwareConvert(conversionService, rc, conversionContext); + } else { + converted = conversionService.convert(o, conversionContext); + } + if (converted.isPresent()) { + return converted.get(); + } else { + throw extractError(o, conversionContext); } } } diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/body/ByteBody.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/body/ByteBody.java index 29e6cfc7cbb..3cfff3fc751 100644 --- a/http-server-netty/src/main/java/io/micronaut/http/server/netty/body/ByteBody.java +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/body/ByteBody.java @@ -49,6 +49,16 @@ public sealed interface ByteBody extends HttpBody permits ImmediateByteBody, Str */ ExecutionFlow buffer(ByteBufAllocator alloc); + /** + * Claim this body and convert it back to a {@link HttpRequest}. This is used for proxying, + * where the request received by the server is reused by the client. + * + * @param request The input request (headers and such) + * @return The request including the body, either a {@link FullHttpRequest} or a + * {@link StreamedHttpRequest} + */ + HttpRequest claimForReuse(HttpRequest request); + /** * Create a byte body for the given request. The request must be either a * {@link FullHttpRequest} or a {@link StreamedHttpRequest}. diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/body/HttpBodyReused.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/body/HttpBodyReused.java new file mode 100644 index 00000000000..0aed45a66fe --- /dev/null +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/body/HttpBodyReused.java @@ -0,0 +1,34 @@ +/* + * Copyright 2017-2023 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.http.server.netty.body; + +import io.micronaut.http.server.netty.NettyHttpRequest; + +/** + * Special {@link HttpBody} that is inserted on calling {@link ByteBody#claimForReuse}. This allows + * {@link NettyHttpRequest#getBody()} to still be called (though it will return nothing). + */ +final class HttpBodyReused implements HttpBody { + @Override + public void release() { + // nothing to do + } + + @Override + public HttpBody next() { + return null; + } +} diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/body/ImmediateByteBody.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/body/ImmediateByteBody.java index 3417038ac65..9eaabdc0f5e 100644 --- a/http-server-netty/src/main/java/io/micronaut/http/server/netty/body/ImmediateByteBody.java +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/body/ImmediateByteBody.java @@ -16,12 +16,15 @@ package io.micronaut.http.server.netty.body; import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.NonNull; import io.micronaut.core.execution.ExecutionFlow; import io.micronaut.http.server.netty.HttpContentProcessor; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; -import io.netty.handler.codec.http.DefaultHttpContent; -import org.jetbrains.annotations.NotNull; +import io.netty.handler.codec.http.DefaultFullHttpRequest; +import io.netty.handler.codec.http.DefaultLastHttpContent; +import io.netty.handler.codec.http.HttpRequest; +import io.netty.handler.codec.http.LastHttpContent; import java.nio.charset.Charset; import java.util.ArrayList; @@ -58,14 +61,14 @@ public MultiObjectBody processMulti(HttpContentProcessor processor) throws Throw return next(processMultiImpl(processor, data)); } - @NotNull + @NonNull private ImmediateMultiObjectBody processMultiImpl(HttpContentProcessor processor, ByteBuf data) throws Throwable { List out = new ArrayList<>(1); if (data.isReadable()) { - processor.add(new DefaultHttpContent(data), out); - } else { - data.release(); + data.retain(); + processor.add(new DefaultLastHttpContent(data), out); } + data.release(); processor.complete(out); return new ImmediateMultiObjectBody(out); } @@ -89,6 +92,21 @@ public ExecutionFlow buffer(ByteBufAllocator alloc) { return ExecutionFlow.just(this); } + @Override + public HttpRequest claimForReuse(HttpRequest request) { + DefaultFullHttpRequest copy = new DefaultFullHttpRequest( + request.protocolVersion(), + request.method(), + request.uri(), + prepareClaim(), + request.headers(), + LastHttpContent.EMPTY_LAST_CONTENT.trailingHeaders() + ); + copy.setDecoderResult(request.decoderResult()); + next(new HttpBodyReused()); + return copy; + } + public boolean empty() { return empty; } diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/body/StreamingByteBody.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/body/StreamingByteBody.java index 235a0a805d9..2d8be74c903 100644 --- a/http-server-netty/src/main/java/io/micronaut/http/server/netty/body/StreamingByteBody.java +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/body/StreamingByteBody.java @@ -19,12 +19,15 @@ import io.micronaut.core.annotation.Nullable; import io.micronaut.core.execution.DelayedExecutionFlow; import io.micronaut.core.execution.ExecutionFlow; +import io.micronaut.http.netty.reactive.HotObservable; +import io.micronaut.http.netty.stream.DelegateStreamedHttpRequest; import io.micronaut.http.server.netty.HttpContentProcessor; import io.micronaut.http.server.netty.HttpContentProcessorAsReactiveProcessor; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; import io.netty.buffer.CompositeByteBuf; import io.netty.handler.codec.http.HttpContent; +import io.netty.handler.codec.http.HttpRequest; import org.reactivestreams.Publisher; import org.reactivestreams.Subscriber; import org.reactivestreams.Subscription; @@ -58,9 +61,18 @@ public ExecutionFlow buffer(ByteBufAllocator alloc) { return intermediateBuffering.completion; } + @Override + public HttpRequest claimForReuse(HttpRequest request) { + Publisher publisher = prepareClaim(); + next(new HttpBodyReused()); + return new DelegateStreamedHttpRequest(request, publisher); + } + @Override void release(Publisher value) { - // not subscribed, don't need to do anything + if (value instanceof HotObservable hot) { + hot.closeIfNoSubscriber(); + } } /** diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/body/StreamingMultiObjectBody.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/body/StreamingMultiObjectBody.java index c3820754fa9..4c05a59fae1 100644 --- a/http-server-netty/src/main/java/io/micronaut/http/server/netty/body/StreamingMultiObjectBody.java +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/body/StreamingMultiObjectBody.java @@ -18,6 +18,7 @@ import io.micronaut.core.annotation.Internal; import io.micronaut.core.annotation.NonNull; import io.micronaut.core.annotation.Nullable; +import io.micronaut.http.netty.reactive.HotObservable; import io.micronaut.http.server.netty.FormRouteCompleter; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; @@ -51,7 +52,9 @@ public final class StreamingMultiObjectBody extends ManagedBody> im @Override void release(Publisher value) { - // not subscribed, don't need to do anything + if (value instanceof HotObservable hot) { + hot.closeIfNoSubscriber(); + } } @Override diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/converters/NettyConverters.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/converters/NettyConverters.java index 22ed62a320f..c7fc6c7fb16 100644 --- a/http-server-netty/src/main/java/io/micronaut/http/server/netty/converters/NettyConverters.java +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/converters/NettyConverters.java @@ -18,6 +18,8 @@ import io.micronaut.context.BeanProvider; import io.micronaut.context.annotation.Prototype; import io.micronaut.core.annotation.Internal; +import io.micronaut.core.convert.ArgumentConversionContext; +import io.micronaut.core.convert.ConversionContext; import io.micronaut.core.convert.ConversionService; import io.micronaut.core.convert.MutableConversionService; import io.micronaut.core.convert.TypeConverter; @@ -34,6 +36,7 @@ import io.netty.channel.ChannelOption; import io.netty.handler.codec.http.multipart.Attribute; import io.netty.handler.codec.http.multipart.FileUpload; +import io.netty.util.ReferenceCounted; import java.io.IOException; import java.io.InputStream; @@ -188,4 +191,54 @@ protected TypeConverter fileUploadToObjectConverter() { protected TypeConverter byteBufToObjectConverter() { return (object, targetType, context) -> conversionService.convert(object.toString(context.getCharset()), targetType, context); } + + /** + * This method converts a + * {@link io.netty.util.ReferenceCounted netty reference counted object} and transfers release + * ownership to the new object. + * + * @param service The conversion service + * @param context The context to convert to + * @param input The object to convert + * @param Target type + * @return The converted object + */ + public static Optional refCountAwareConvert(ConversionService service, ReferenceCounted input, ArgumentConversionContext context) { + Optional converted = service.convert(input, context); + postProcess(input, converted); + return converted; + } + + /** + * This method converts a + * {@link io.netty.util.ReferenceCounted netty reference counted object} and transfers release + * ownership to the new object. + * + * @param service The conversion service + * @param input The object to convert + * @param targetType The type to convert to + * @param context The context to convert with + * @param Target type + * @return The converted object + */ + public static Optional refCountAwareConvert(ConversionService service, ReferenceCounted input, Class targetType, ConversionContext context) { + Optional converted = service.convert(input, targetType, context); + postProcess(input, converted); + return converted; + } + + @SuppressWarnings("OptionalUsedAsFieldOrParameterType") + private static void postProcess(ReferenceCounted input, Optional converted) { + if (converted.isPresent()) { + input.touch(); + T item = converted.get(); + // this is not great, but what can we do? + boolean targetRefCounted = item instanceof ReferenceCounted || item instanceof io.micronaut.core.io.buffer.ReferenceCounted; + if (!targetRefCounted) { + input.release(); + } + } else { + input.release(); + } + } } diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/decoders/HttpRequestDecoder.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/decoders/HttpRequestDecoder.java deleted file mode 100644 index 877901bd162..00000000000 --- a/http-server-netty/src/main/java/io/micronaut/http/server/netty/decoders/HttpRequestDecoder.java +++ /dev/null @@ -1,122 +0,0 @@ -/* - * Copyright 2017-2020 original authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.micronaut.http.server.netty.decoders; - -import io.micronaut.context.event.ApplicationEventPublisher; -import io.micronaut.core.annotation.Internal; -import io.micronaut.core.convert.ConversionService; -import io.micronaut.core.order.Ordered; -import io.micronaut.http.context.event.HttpRequestReceivedEvent; -import io.micronaut.http.netty.stream.StreamedHttpRequest; -import io.micronaut.http.server.HttpServerConfiguration; -import io.micronaut.http.server.netty.NettyHttpRequest; -import io.micronaut.http.server.netty.NettyHttpServer; -import io.micronaut.runtime.server.EmbeddedServer; -import io.netty.buffer.Unpooled; -import io.netty.channel.ChannelHandler; -import io.netty.channel.ChannelHandlerContext; -import io.netty.handler.codec.MessageToMessageDecoder; -import io.netty.handler.codec.http.DefaultFullHttpRequest; -import io.netty.handler.codec.http.HttpRequest; -import io.netty.util.ReferenceCountUtil; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.util.List; - -/** - * A {@link MessageToMessageDecoder} that decodes a Netty {@link HttpRequest} into a Micronaut - * {@link io.micronaut.http.HttpRequest}. - * - * @author Graeme Rocher - * @since 1.0 - */ -@ChannelHandler.Sharable -@Internal -public class HttpRequestDecoder extends MessageToMessageDecoder implements Ordered { - - /** - * Constant for Micronaut http decoder. - */ - public static final String ID = "micronaut-http-decoder"; - - private static final Logger LOG = LoggerFactory.getLogger(NettyHttpServer.class); - - private final EmbeddedServer embeddedServer; - private final ConversionService conversionService; - private final HttpServerConfiguration configuration; - private final ApplicationEventPublisher httpRequestReceivedEventPublisher; - - /** - * @param embeddedServer The embedded service - * @param conversionService The conversion service - * @param configuration The Http server configuration - * @param httpRequestReceivedEventPublisher The publisher of {@link HttpRequestReceivedEvent} - */ - public HttpRequestDecoder( - EmbeddedServer embeddedServer, - ConversionService conversionService, - HttpServerConfiguration configuration, - ApplicationEventPublisher httpRequestReceivedEventPublisher) { - this.embeddedServer = embeddedServer; - this.conversionService = conversionService; - this.configuration = configuration; - this.httpRequestReceivedEventPublisher = httpRequestReceivedEventPublisher; - } - - @Override - protected void decode(ChannelHandlerContext ctx, HttpRequest msg, List out) { - if (LOG.isTraceEnabled()) { - LOG.trace("Server {}:{} Received Request: {} {}", embeddedServer.getHost(), embeddedServer.getPort(), msg.method(), msg.uri()); - } - try { - NettyHttpRequest request = new NettyHttpRequest<>(msg, ctx, conversionService, configuration); - if (!httpRequestReceivedEventPublisher.isEmpty()) { - try { - ctx.executor().execute(() -> { - try { - httpRequestReceivedEventPublisher.publishEvent(new HttpRequestReceivedEvent(request)); - } catch (Exception e) { - if (LOG.isErrorEnabled()) { - LOG.error("Error publishing Http request received event: " + e.getMessage(), e); - } - } - }); - } catch (Exception e) { - if (LOG.isErrorEnabled()) { - LOG.error("Error publishing Http request received event: " + e.getMessage(), e); - } - } - } - out.add(request); - ReferenceCountUtil.retain(msg); // retain the body if it's a FullHttpRequest - } catch (IllegalArgumentException e) { - // this configured the request in the channel as an attribute - new NettyHttpRequest<>( - new DefaultFullHttpRequest(msg.protocolVersion(), msg.method(), "/", Unpooled.EMPTY_BUFFER), - ctx, - conversionService, - configuration - ); - final Throwable cause = e.getCause(); - ctx.fireExceptionCaught(cause != null ? cause : e); - if (msg instanceof StreamedHttpRequest streamedHttpRequest) { - // discard any data that may come in - streamedHttpRequest.closeIfNoSubscriber(); - } - } - } -} diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/encoders/HttpResponseEncoder.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/encoders/HttpResponseEncoder.java deleted file mode 100644 index 16fea285227..00000000000 --- a/http-server-netty/src/main/java/io/micronaut/http/server/netty/encoders/HttpResponseEncoder.java +++ /dev/null @@ -1,206 +0,0 @@ -/* - * Copyright 2017-2020 original authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.micronaut.http.server.netty.encoders; - -import io.micronaut.buffer.netty.NettyByteBufferFactory; -import io.micronaut.core.annotation.Internal; -import io.micronaut.core.convert.ConversionService; -import io.micronaut.core.io.Writable; -import io.micronaut.core.io.buffer.ByteBuffer; -import io.micronaut.http.HttpHeaders; -import io.micronaut.http.MediaType; -import io.micronaut.http.MutableHttpHeaders; -import io.micronaut.http.MutableHttpResponse; -import io.micronaut.http.codec.MediaTypeCodec; -import io.micronaut.http.codec.MediaTypeCodecRegistry; -import io.micronaut.http.netty.NettyMutableHttpResponse; -import io.micronaut.http.server.HttpServerConfiguration; -import io.micronaut.runtime.http.codec.TextPlainCodec; -import io.netty.buffer.ByteBuf; -import io.netty.buffer.ByteBufOutputStream; -import io.netty.buffer.Unpooled; -import io.netty.channel.ChannelHandler.Sharable; -import io.netty.channel.ChannelHandlerContext; -import io.netty.handler.codec.MessageToMessageEncoder; -import io.netty.handler.codec.http.DefaultFullHttpResponse; -import io.netty.handler.codec.http.DefaultHttpHeaders; -import io.netty.handler.codec.http.EmptyHttpHeaders; -import io.netty.handler.codec.http.FullHttpResponse; -import io.netty.handler.codec.http.HttpHeaderNames; -import io.netty.handler.codec.http.HttpResponseStatus; -import io.netty.handler.codec.http.HttpVersion; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.IOException; -import java.time.LocalDateTime; -import java.util.List; -import java.util.Map; -import java.util.Optional; - -/** - * Encodes Micronaut's representation of an {@link MutableHttpResponse}. - * - * @author graemerocher - * @since 1.0 - */ -@Internal -@Sharable -public class HttpResponseEncoder extends MessageToMessageEncoder> { - public static final String ID = "micronaut-http-encoder"; - private static final Logger LOG = LoggerFactory.getLogger(HttpResponseEncoder.class); - - private final MediaTypeCodecRegistry mediaTypeCodecRegistry; - private final HttpServerConfiguration serverConfiguration; - private final ConversionService conversionService; - - /** - * Default constructor. - * - * @param mediaTypeCodecRegistry The media type registry - * @param serverConfiguration The server config - * @param conversionService The conversion service - */ - public HttpResponseEncoder(MediaTypeCodecRegistry mediaTypeCodecRegistry, HttpServerConfiguration serverConfiguration, ConversionService conversionService) { - this.mediaTypeCodecRegistry = mediaTypeCodecRegistry; - this.serverConfiguration = serverConfiguration; - this.conversionService = conversionService; - } - - @Override - protected void encode(ChannelHandlerContext context, MutableHttpResponse response, List out) { - - - Optional specifiedMediaType = response.getContentType(); - MediaType responseMediaType = specifiedMediaType.orElse(MediaType.APPLICATION_JSON_TYPE); - - applyConfiguredHeaders(response.getHeaders()); - - Optional responseBody = response.getBody(); - if (responseBody.isPresent()) { - - Object body = responseBody.get(); - - if (specifiedMediaType.isPresent()) { - - Optional registeredCodec = mediaTypeCodecRegistry.findCodec(responseMediaType, body.getClass()); - if (registeredCodec.isPresent()) { - MediaTypeCodec codec = registeredCodec.get(); - response = encodeBodyWithCodec(response, body, codec, responseMediaType, context); - } - } - - Optional registeredCodec = mediaTypeCodecRegistry.findCodec(MediaType.APPLICATION_JSON_TYPE, body.getClass()); - if (registeredCodec.isPresent()) { - MediaTypeCodec codec = registeredCodec.get(); - response = encodeBodyWithCodec(response, body, codec, responseMediaType, context); - } - - MediaTypeCodec defaultCodec = new TextPlainCodec(serverConfiguration.getDefaultCharset(), conversionService); - - response = encodeBodyWithCodec(response, body, defaultCodec, responseMediaType, context); - } - - if (response instanceof NettyMutableHttpResponse) { - out.add(((NettyMutableHttpResponse) response).toHttpResponse()); - } else { - io.netty.handler.codec.http.HttpHeaders nettyHeaders = new DefaultHttpHeaders(); - for (Map.Entry> header : response.getHeaders()) { - nettyHeaders.add(header.getKey(), header.getValue()); - } - Object b = response.getBody().orElse(null); - ByteBuf body = b instanceof ByteBuf ? (ByteBuf) b : Unpooled.buffer(0); - FullHttpResponse nettyResponse = new DefaultFullHttpResponse( - HttpVersion.HTTP_1_1, - HttpResponseStatus.valueOf(response.code(), response.reason()), - body, - nettyHeaders, - EmptyHttpHeaders.INSTANCE - ); - out.add(nettyResponse); - } - } - - private void applyConfiguredHeaders(MutableHttpHeaders headers) { - if (serverConfiguration.isDateHeader() && !headers.contains(HttpHeaders.DATE)) { - headers.date(LocalDateTime.now()); - } - serverConfiguration.getServerHeader().ifPresent(server -> { - if (!headers.contains(HttpHeaders.SERVER)) { - headers.add(HttpHeaderNames.SERVER, server); - } - }); - } - - private MutableHttpResponse encodeBodyWithCodec(MutableHttpResponse response, - Object body, - MediaTypeCodec codec, - MediaType mediaType, - ChannelHandlerContext context) { - ByteBuf byteBuf = encodeBodyAsByteBuf(body, codec, context, response); - int len = byteBuf.readableBytes(); - MutableHttpHeaders headers = response.getHeaders(); - if (!headers.contains(HttpHeaders.CONTENT_TYPE)) { - headers.add(HttpHeaderNames.CONTENT_TYPE, mediaType); - } - headers.remove(HttpHeaders.CONTENT_LENGTH); - headers.add(HttpHeaderNames.CONTENT_LENGTH, String.valueOf(len)); - - setBodyContent(response, byteBuf); - return response; - } - - private MutableHttpResponse setBodyContent(MutableHttpResponse response, Object bodyContent) { - @SuppressWarnings("unchecked") - MutableHttpResponse res = response.body(bodyContent); - return res; - } - - private ByteBuf encodeBodyAsByteBuf(Object body, MediaTypeCodec codec, ChannelHandlerContext context, MutableHttpResponse response) { - ByteBuf byteBuf; - if (body instanceof ByteBuf) { - byteBuf = (ByteBuf) body; - } else if (body instanceof ByteBuffer) { - ByteBuffer byteBuffer = (ByteBuffer) body; - Object nativeBuffer = byteBuffer.asNativeBuffer(); - if (nativeBuffer instanceof ByteBuf) { - byteBuf = (ByteBuf) nativeBuffer; - } else { - byteBuf = Unpooled.wrappedBuffer(byteBuffer.asNioBuffer()); - } - } else if (body instanceof byte[]) { - byteBuf = Unpooled.wrappedBuffer((byte[]) body); - - } else if (body instanceof Writable) { - byteBuf = context.alloc().ioBuffer(128); - ByteBufOutputStream outputStream = new ByteBufOutputStream(byteBuf); - Writable writable = (Writable) body; - try { - writable.writeTo(outputStream, response.getCharacterEncoding()); - } catch (IOException e) { - if (LOG.isErrorEnabled()) { - LOG.error(e.getMessage()); - } - } - } else { - if (LOG.isDebugEnabled()) { - LOG.debug("Encoding emitted response object [{}] using codec: {}", body, codec); - } - byteBuf = codec.encode(body, new NettyByteBufferFactory(context.alloc())).asNativeBuffer(); - } - return byteBuf; - } -} diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/handler/PipeliningServerHandler.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/handler/PipeliningServerHandler.java new file mode 100644 index 00000000000..983e2a54797 --- /dev/null +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/handler/PipeliningServerHandler.java @@ -0,0 +1,874 @@ +/* + * Copyright 2017-2023 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.http.server.netty.handler; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.NonNull; +import io.micronaut.core.annotation.Nullable; +import io.micronaut.http.exceptions.HttpStatusException; +import io.micronaut.http.netty.stream.DelegateStreamedHttpRequest; +import io.micronaut.http.netty.stream.EmptyHttpRequest; +import io.micronaut.http.netty.stream.StreamedHttpResponse; +import io.micronaut.http.server.netty.types.NettyCustomizableResponseType; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.CompositeByteBuf; +import io.netty.buffer.Unpooled; +import io.netty.channel.ChannelFutureListener; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; +import io.netty.channel.EventLoop; +import io.netty.channel.FileRegion; +import io.netty.handler.codec.http.DefaultFullHttpRequest; +import io.netty.handler.codec.http.DefaultFullHttpResponse; +import io.netty.handler.codec.http.DefaultHttpResponse; +import io.netty.handler.codec.http.FullHttpRequest; +import io.netty.handler.codec.http.FullHttpResponse; +import io.netty.handler.codec.http.HttpContent; +import io.netty.handler.codec.http.HttpHeaderNames; +import io.netty.handler.codec.http.HttpRequest; +import io.netty.handler.codec.http.HttpResponse; +import io.netty.handler.codec.http.HttpResponseStatus; +import io.netty.handler.codec.http.HttpUtil; +import io.netty.handler.codec.http.HttpVersion; +import io.netty.handler.codec.http.LastHttpContent; +import io.netty.handler.stream.ChunkedInput; +import io.netty.handler.timeout.IdleState; +import io.netty.handler.timeout.IdleStateEvent; +import io.netty.util.ReferenceCountUtil; +import org.reactivestreams.Subscriber; +import org.reactivestreams.Subscription; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Sinks; +import reactor.util.concurrent.Queues; + +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.Queue; + +/** + * Netty handler that handles incoming {@link HttpRequest}s and forwards them to a + * {@link RequestHandler}. + * + * @author Jonas Konrad + * @since 4.0.0 + */ +@Internal +public final class PipeliningServerHandler extends ChannelInboundHandlerAdapter { + private static final Logger LOG = LoggerFactory.getLogger(PipeliningServerHandler.class); + + private final RequestHandler requestHandler; + + // these three handlers can be reused and are cached here + private final DroppingInboundHandler droppingInboundHandler = new DroppingInboundHandler(); + private final InboundHandler baseInboundHandler = new MessageInboundHandler(); + private final OptimisticBufferingInboundHandler optimisticBufferingInboundHandler = new OptimisticBufferingInboundHandler(); + + /** + * Current handler for inbound messages. + */ + private InboundHandler inboundHandler = baseInboundHandler; + + /** + * Queue of outbound messages that can't be written yet. + */ + private final Queue outboundQueue = new ArrayDeque<>(1); + /** + * Current outbound message, or {@code null} if no outbound message is waiting. + */ + @Nullable + private OutboundHandler outboundHandler = null; + + private ChannelHandlerContext ctx; + /** + * {@code true} iff we are in a read operation, before {@link #channelReadComplete}. + */ + private boolean reading = false; + /** + * {@code true} iff we want to read more data. + */ + private boolean moreRequested = false; + /** + * {@code true} iff this handler has been removed. + */ + private boolean removed = false; + /** + * {@code true} iff we should flush on {@link #channelReadComplete}. + */ + private boolean flushPending = false; + + public PipeliningServerHandler(RequestHandler requestHandler) { + this.requestHandler = requestHandler; + } + + private static boolean canHaveBody(HttpResponse message) { + HttpResponseStatus status = message.status(); + // All 1xx (Informational), 204 (No Content), and 304 (Not Modified) + // responses do not include a message body + return !(status == HttpResponseStatus.CONTINUE || status == HttpResponseStatus.SWITCHING_PROTOCOLS || + status == HttpResponseStatus.PROCESSING || status == HttpResponseStatus.NO_CONTENT || + status == HttpResponseStatus.NOT_MODIFIED); + } + + private static boolean hasBody(HttpRequest request) { + // if there's a decoder failure (e.g. invalid header), don't expect the body to come in + if (request.decoderResult().isFailure()) { + return false; + } + // Http requests don't have a body if they define 0 content length, or no content length and no transfer + // encoding + int contentLength; + try { + contentLength = HttpUtil.getContentLength(request, 0); + } catch (NumberFormatException e) { + // handle invalid content length, https://github.com/netty/netty/issues/12113 + contentLength = 0; + } + return contentLength != 0 || HttpUtil.isTransferEncodingChunked(request); + } + + /** + * Set whether we need more input, i.e. another call to {@link #channelRead}. This is usally a + * {@link ChannelHandlerContext#read()} call, but it's coalesced until + * {@link #channelReadComplete}. + * + * @param needMore {@code true} iff we need more input + */ + private void setNeedMore(boolean needMore) { + boolean oldMoreRequested = moreRequested; + moreRequested = needMore; + if (!oldMoreRequested && !reading && needMore) { + ctx.read(); + } + } + + @Override + public void handlerAdded(ChannelHandlerContext ctx) throws Exception { + this.ctx = ctx; + } + + @Override + public void handlerRemoved(ChannelHandlerContext ctx) throws Exception { + removed = true; + if (outboundHandler != null) { + outboundHandler.discard(); + } + for (OutboundAccess queued : outboundQueue) { + if (queued.handler != null) { + queued.handler.discard(); + } + } + outboundQueue.clear(); + requestHandler.removed(); + } + + @Override + public void channelRead(@NonNull ChannelHandlerContext ctx, @NonNull Object msg) throws Exception { + reading = true; + inboundHandler.read(msg); + } + + @Override + public void channelReadComplete(ChannelHandlerContext ctx) throws Exception { + inboundHandler.readComplete(); + reading = false; + if (flushPending) { + ctx.flush(); + flushPending = false; + } + if (moreRequested) { + ctx.read(); + } + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { + inboundHandler.handleUpstreamError(cause); + } + + @Override + public void channelWritabilityChanged(ChannelHandlerContext ctx) throws Exception { + writeSome(); + } + + @Override + public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception { + if (evt instanceof IdleStateEvent idleStateEvent) { + IdleState state = idleStateEvent.state(); + if (state == IdleState.ALL_IDLE) { + ctx.close(); + } + } + super.userEventTriggered(ctx, evt); + } + + /** + * Write a message. + * + * @param message The message to write + * @param flush {@code true} iff we should flush after this message + * @param close {@code true} iff the channel should be closed after this message + */ + private void write(Object message, boolean flush, boolean close) { + if (close) { + ctx.writeAndFlush(message).addListener(ChannelFutureListener.CLOSE); + } else { + if (flush) { + // delay flush until readComplete if possible + if (reading) { + ctx.write(message, ctx.voidPromise()); + flushPending = true; + } else { + ctx.writeAndFlush(message, ctx.voidPromise()); + } + } else { + ctx.write(message, ctx.voidPromise()); + } + } + } + + /** + * Write some data if possible. + */ + private void writeSome() { + while (ctx.channel().isWritable()) { + // if we have no outboundHandler, check whether the first queued response is ready + if (outboundHandler == null) { + OutboundAccess next = outboundQueue.peek(); + if (next != null && next.handler != null) { + outboundQueue.poll(); + outboundHandler = next.handler; + } else { + return; + } + } + OutboundHandler oldHandler = outboundHandler; + oldHandler.writeSome(); + if (outboundHandler == oldHandler) { + // handler is not done yet + break; + } + } + } + + /** + * An inbound handler is responsible for all incoming messages. + */ + private abstract static class InboundHandler { + /** + * @see #channelRead + */ + abstract void read(Object message); + + /** + * @see #exceptionCaught + */ + abstract void handleUpstreamError(Throwable cause); + + /** + * @see #channelReadComplete + */ + void readComplete() { + } + } + + /** + * Base {@link InboundHandler} that handles {@link HttpRequest}s and then determines how to + * deal with the body. + */ + private final class MessageInboundHandler extends InboundHandler { + @Override + void read(Object message) { + HttpRequest request = (HttpRequest) message; + OutboundAccess outboundAccess = new OutboundAccess(); + outboundQueue.add(outboundAccess); + if (request instanceof FullHttpRequest full) { + requestHandler.accept(ctx, full, outboundAccess); + } else if (!hasBody(request)) { + inboundHandler = droppingInboundHandler; + if (message instanceof HttpContent) { + inboundHandler.read(message); + } + requestHandler.accept(ctx, new EmptyHttpRequest(request), outboundAccess); + } else { + optimisticBufferingInboundHandler.init(request, outboundAccess); + inboundHandler = optimisticBufferingInboundHandler; + } + } + + @Override + void handleUpstreamError(Throwable cause) { + requestHandler.handleUnboundError(cause); + } + } + + /** + * Handler that buffers input data until the request is complete, in which case it forwards it + * as {@link FullHttpRequest}, or devolves to {@link StreamingInboundHandler} if not all data + * has arrived yet by the time {@link #channelReadComplete} is called. + */ + private final class OptimisticBufferingInboundHandler extends InboundHandler { + private HttpRequest request; + private OutboundAccess outboundAccess; + private final List buffer = new ArrayList<>(); + + void init(HttpRequest request, OutboundAccess outboundAccess) { + assert buffer.isEmpty(); + assert !(request instanceof HttpContent); + this.request = request; + this.outboundAccess = outboundAccess; + } + + @Override + void read(Object message) { + HttpContent content = (HttpContent) message; + if (content.content().isReadable()) { + buffer.add(content); + } else { + content.release(); + } + if (message instanceof LastHttpContent last) { + // we got the full message before readComplete + ByteBuf fullBody; + if (buffer.size() == 0) { + fullBody = Unpooled.EMPTY_BUFFER; + } else if (buffer.size() == 1) { + fullBody = buffer.get(0).content(); + } else { + CompositeByteBuf composite = ctx.alloc().compositeBuffer(); + for (HttpContent c : buffer) { + composite.addComponent(true, c.content()); + } + fullBody = composite; + } + buffer.clear(); + FullHttpRequest fullRequest = new DefaultFullHttpRequest( + request.protocolVersion(), + request.method(), + request.uri(), + fullBody, + request.headers(), + last.trailingHeaders() + ); + fullRequest.setDecoderResult(request.decoderResult()); + request = null; + OutboundAccess outboundAccess = this.outboundAccess; + this.outboundAccess = null; + requestHandler.accept(ctx, fullRequest, outboundAccess); + + inboundHandler = baseInboundHandler; + } + } + + @Override + void readComplete() { + devolveToStreaming(); + inboundHandler.readComplete(); + } + + @Override + void handleUpstreamError(Throwable cause) { + devolveToStreaming(); + inboundHandler.handleUpstreamError(cause); + } + + private void devolveToStreaming() { + StreamingInboundHandler streamingInboundHandler = new StreamingInboundHandler(); + for (HttpContent content : buffer) { + streamingInboundHandler.read(content); + } + buffer.clear(); + HttpRequest request = this.request; + OutboundAccess outboundAccess = this.outboundAccess; + this.request = null; + this.outboundAccess = null; + + inboundHandler = streamingInboundHandler; + Flux flux = streamingInboundHandler.flux(); + if (HttpUtil.is100ContinueExpected(request)) { + flux = flux.doOnSubscribe(s -> outboundAccess.writeContinue()); + } + requestHandler.accept(ctx, new DelegateStreamedHttpRequest(request, flux) { + @Override + public void closeIfNoSubscriber() { + streamingInboundHandler.closeIfNoSubscriber(); + } + }, outboundAccess); + } + } + + /** + * Handler that exposes incoming content as a {@link Flux}. + */ + private final class StreamingInboundHandler extends InboundHandler { + private final Queue queue = Queues.unbounded().get(); + private final Sinks.Many sink = Sinks.many().unicast().onBackpressureBuffer(queue); + private long requested = 0; + + @Override + void read(Object message) { + requested--; + HttpContent content = (HttpContent) message; + if (sink.tryEmitNext(content.touch()) != Sinks.EmitResult.OK) { + content.release(); + } + if (message instanceof LastHttpContent) { + sink.tryEmitComplete(); + inboundHandler = baseInboundHandler; + } + setNeedMore(requested > 0); + } + + @Override + void handleUpstreamError(Throwable cause) { + releaseQueue(); + if (sink.tryEmitError(cause) != Sinks.EmitResult.OK) { + requestHandler.handleUnboundError(cause); + } + } + + private void request(long n) { + EventLoop eventLoop = ctx.channel().eventLoop(); + if (!eventLoop.inEventLoop()) { + eventLoop.execute(() -> request(n)); + return; + } + + long newRequested = requested + n; + if (newRequested < requested) { + // overflow + newRequested = Long.MAX_VALUE; + } + requested = newRequested; + setNeedMore(newRequested > 0); + } + + Flux flux() { + return sink.asFlux() + .doOnRequest(this::request) + .doOnCancel(this::releaseQueue); + } + + void closeIfNoSubscriber() { + if (sink.currentSubscriberCount() == 0) { + releaseQueue(); + if (inboundHandler == this) { + inboundHandler = droppingInboundHandler; + } + } + } + + private void releaseQueue() { + while (true) { + HttpContent c = queue.poll(); + if (c == null) { + break; + } + c.release(); + } + } + } + + /** + * Handler that drops all incoming content. + */ + private final class DroppingInboundHandler extends InboundHandler { + @Override + void read(Object message) { + ((HttpContent) message).release(); + if (message instanceof LastHttpContent) { + inboundHandler = baseInboundHandler; + } + } + + @Override + void handleUpstreamError(Throwable cause) { + requestHandler.handleUnboundError(cause); + } + } + + /** + * Class that allows writing the response for the request this object is associated with. + */ + public final class OutboundAccess { + /** + * The handler that will perform the actual write operation. + */ + private OutboundHandler handler; + private Object attachment = null; + private boolean closeAfterWrite = false; + + private OutboundAccess() { + } + + /** + * Set an attachment that is passed to {@link RequestHandler#responseWritten}. Defaults to + * {@code null}. + * + * @param attachment The attachment to forward + */ + public void attachment(Object attachment) { + this.attachment = attachment; + } + + /** + * Mark this channel to be closed after this response has been written. + */ + public void closeAfterWrite() { + closeAfterWrite = true; + } + + private void preprocess(HttpResponse message) { + if (message.protocolVersion().isKeepAliveDefault()) { + if (message.headers().contains(HttpHeaderNames.CONNECTION, "close", true)) { + closeAfterWrite(); + } + } else { + if (!message.headers().contains(HttpHeaderNames.CONNECTION, "keep-alive", true)) { + closeAfterWrite(); + } + } + // According to RFC 7230 a server MUST NOT send a Content-Length or a Transfer-Encoding when the status + // code is 1xx or 204, also a status code 304 may not have a Content-Length or Transfer-Encoding set. + if (!HttpUtil.isContentLengthSet(message) && !HttpUtil.isTransferEncodingChunked(message) && canHaveBody(message)) { + HttpUtil.setKeepAlive(message, false); + closeAfterWrite(); + } + } + + /** + * Write a 100 CONTINUE response. + */ + private void writeContinue() { + if (handler == null) { + write(new ContinueOutboundHandler()); + } + } + + /** + * Write a response using the given outbound handler, when ready. + */ + private void write(OutboundHandler handler) { + // technically handler should be volatile for this check, but this is only for sanity anyway + if (this.handler != null && !(this.handler instanceof ContinueOutboundHandler)) { + throw new IllegalStateException("Only one response per request"); + } + + EventLoop eventLoop = ctx.channel().eventLoop(); + if (!eventLoop.inEventLoop()) { + eventLoop.execute(() -> write(handler)); + return; + } + + if (this.handler instanceof ContinueOutboundHandler cont) { + cont.next = handler; + writeSome(); + } else { + this.handler = handler; + if (outboundQueue.peek() == this) { + writeSome(); + } + } + } + + /** + * Write a full response. + * + * @param response The response to write + */ + public void writeFull(FullHttpResponse response) { + preprocess(response); + write(new FullOutboundHandler(this, response)); + } + + /** + * Write a streamed response. The actual response will only be written when the first item + * of the {@link org.reactivestreams.Publisher} is received, in order to handle errors. + * + * @param response The response to write + */ + public void writeStreamed(StreamedHttpResponse response) { + preprocess(response); + response.subscribe(new StreamingOutboundHandler(this, response)); + } + + /** + * Write a response with a special body + * ({@link io.netty.handler.codec.http.HttpChunkedInput}, + * {@link io.micronaut.http.server.types.files.SystemFile}). + * + * @param response The response to write + */ + public void writeStreamed(NettyCustomizableResponseType.CustomResponse response) { + preprocess(response.response()); + write(new ChunkedOutboundHandler(this, response)); + } + } + + private abstract static class OutboundHandler { + /** + * {@link OutboundAccess} that created this handler, for metadata access. + */ + final OutboundAccess outboundAccess; + + private OutboundHandler(OutboundAccess outboundAccess) { + this.outboundAccess = outboundAccess; + } + + /** + * Write some data to the channel. + */ + abstract void writeSome(); + + /** + * Discard the remaining data. + */ + abstract void discard(); + } + + /** + * Handler that writes a 100 CONTINUE response and then proceeds with the {@link #next} handler. + */ + private final class ContinueOutboundHandler extends OutboundHandler { + private static final FullHttpResponse CONTINUE = + new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.CONTINUE, Unpooled.EMPTY_BUFFER); + + boolean written = false; + OutboundHandler next; + + private ContinueOutboundHandler() { + super(null); + } + + @Override + void writeSome() { + if (!written) { + write(CONTINUE, true, false); + written = true; + } + if (next != null) { + outboundHandler = next; + } + } + + @Override + void discard() { + if (next != null) { + next.discard(); + next = null; + } + } + } + + /** + * Handler that writes a {@link FullHttpResponse}. + */ + private final class FullOutboundHandler extends OutboundHandler { + private final FullHttpResponse message; + + FullOutboundHandler(OutboundAccess outboundAccess, FullHttpResponse message) { + super(outboundAccess); + this.message = message; + } + + @Override + void writeSome() { + write(message, true, outboundAccess.closeAfterWrite); + outboundHandler = null; + requestHandler.responseWritten(outboundAccess.attachment); + PipeliningServerHandler.this.writeSome(); + } + + @Override + void discard() { + message.release(); + outboundHandler = null; + } + } + + /** + * Handler that writes a {@link StreamedHttpResponse}. + */ + private final class StreamingOutboundHandler extends OutboundHandler implements Subscriber { + private final OutboundAccess outboundAccess; + private StreamedHttpResponse initialMessage; + private Subscription subscription; + private boolean writtenLast = false; + + StreamingOutboundHandler(OutboundAccess outboundAccess, StreamedHttpResponse initialMessage) { + super(outboundAccess); + this.outboundAccess = outboundAccess; + this.initialMessage = Objects.requireNonNull(initialMessage, "initialMessage"); + } + + @Override + void writeSome() { + subscription.request(1); + } + + @Override + public void onSubscribe(Subscription s) { + subscription = s; + // delay access.write call until the subscription is available, so that writeSome is + // only called then + outboundAccess.write(this); + } + + @Override + public void onNext(HttpContent httpContent) { + EventLoop eventLoop = ctx.channel().eventLoop(); + if (!eventLoop.inEventLoop()) { + eventLoop.execute(() -> onNext(httpContent)); + return; + } + + if (outboundHandler != this) { + throw new IllegalStateException("onNext before request?"); + } + + if (writtenLast) { + throw new IllegalStateException("Already written a LastHttpContent"); + } + + if (initialMessage != null) { + write(initialMessage, false, false); + initialMessage = null; + } + + if (!removed) { + if (httpContent instanceof LastHttpContent) { + writtenLast = true; + } + write(httpContent, true, false); + if (ctx.channel().isWritable()) { + subscription.request(1); + } + } else { + httpContent.release(); + } + } + + @Override + public void onError(Throwable t) { + EventLoop eventLoop = ctx.channel().eventLoop(); + if (!eventLoop.inEventLoop()) { + eventLoop.execute(() -> onError(t)); + return; + } + + if (!removed) { + if (initialMessage != null) { + initialMessage = null; + HttpResponseStatus responseStatus; + if (t instanceof HttpStatusException se) { + responseStatus = HttpResponseStatus.valueOf(se.getStatus().getCode(), se.getMessage()); + } else { + responseStatus = HttpResponseStatus.INTERNAL_SERVER_ERROR; + } + write(new DefaultHttpResponse(HttpVersion.HTTP_1_1, responseStatus), true, true); + } else { + if (LOG.isWarnEnabled()) { + LOG.warn("Reactive response received an error after some data has already been written. This error cannot be forwarded to the client.", t); + } + ctx.close(); + } + + requestHandler.responseWritten(outboundAccess.attachment); + } + } + + @Override + public void onComplete() { + EventLoop eventLoop = ctx.channel().eventLoop(); + if (!eventLoop.inEventLoop()) { + eventLoop.execute(this::onComplete); + return; + } + + if (outboundHandler != this) { + throw new IllegalStateException("onComplete before request?"); + } + + outboundHandler = null; + if (!removed) { + if (initialMessage != null) { + write(initialMessage, false, false); + initialMessage = null; + } + + if (!writtenLast) { + write(LastHttpContent.EMPTY_LAST_CONTENT, true, outboundAccess.closeAfterWrite); + } + requestHandler.responseWritten(outboundAccess.attachment); + PipeliningServerHandler.this.writeSome(); + } + } + + @Override + void discard() { + subscription.cancel(); + outboundHandler = null; + } + } + + /** + * Handler that writes a {@link NettyCustomizableResponseType.CustomResponse}. + */ + private final class ChunkedOutboundHandler extends OutboundHandler { + private final NettyCustomizableResponseType.CustomResponse message; + + ChunkedOutboundHandler(OutboundAccess outboundAccess, NettyCustomizableResponseType.CustomResponse message) { + super(outboundAccess); + this.message = message; + } + + @Override + void writeSome() { + boolean responseIsLast = message.body() == null && !message.needLast(); + write(message.response(), responseIsLast, responseIsLast && outboundAccess.closeAfterWrite); + if (message.body() != null) { + boolean bodyIsLast = !message.needLast(); + write(message.body(), bodyIsLast, bodyIsLast && outboundAccess.closeAfterWrite); + } + if (message.needLast()) { + write(LastHttpContent.EMPTY_LAST_CONTENT, true, outboundAccess.closeAfterWrite); + } + outboundHandler = null; + requestHandler.responseWritten(outboundAccess.attachment); + PipeliningServerHandler.this.writeSome(); + } + + @Override + void discard() { + ReferenceCountUtil.release(message.response()); + if (message.body() instanceof ChunkedInput ci) { + try { + ci.close(); + } catch (Exception e) { + if (LOG.isWarnEnabled()) { + LOG.warn("Failed to close ChunkedInput", e); + } + } + } else if (message.body() instanceof FileRegion fr) { + fr.release(); + } + outboundHandler = null; + } + } +} diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/handler/RequestHandler.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/handler/RequestHandler.java new file mode 100644 index 00000000000..e84e21066a6 --- /dev/null +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/handler/RequestHandler.java @@ -0,0 +1,64 @@ +/* + * Copyright 2017-2023 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.http.server.netty.handler; + +import io.micronaut.core.annotation.Internal; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.http.HttpRequest; + +/** + * Handler for incoming requests. + * + * @author Jonas Konrad + * @since 4.0.0 + */ +@Internal +public interface RequestHandler { + /** + * Handle a request. + * + * @param ctx The context this request came in on + * @param request The request, either a {@link io.netty.handler.codec.http.FullHttpRequest} or a {@link io.micronaut.http.netty.stream.StreamedHttpRequest} + * @param outboundAccess The {@link io.micronaut.http.server.netty.handler.PipeliningServerHandler.OutboundAccess} to use for writing the response + */ + void accept(ChannelHandlerContext ctx, HttpRequest request, PipeliningServerHandler.OutboundAccess outboundAccess); + + /** + * Handle an error that is not bound to a request, i.e. happens outside of a + * {@link io.micronaut.http.netty.stream.StreamedHttpRequest}. + * + * @param cause The error + */ + void handleUnboundError(Throwable cause); + + /** + * Called roughly when a response has been written. In particular, it's called when the user + * is "done" with the response and has no way of adding further data. The bytes may not have + * been fully flushed yet, but e.g. the response {@link org.reactivestreams.Publisher} has been + * fully consumed.
+ * This is used for cleaning up the request. + * + * @param attachment Object passed to {@link io.micronaut.http.server.netty.handler.PipeliningServerHandler.OutboundAccess#attachment(Object)} + */ + default void responseWritten(Object attachment) { + } + + /** + * Called when the handler is removed. + */ + default void removed() { + } +} diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/jackson/JsonContentProcessor.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/jackson/JsonContentProcessor.java index d594c257463..7fa61892238 100644 --- a/http-server-netty/src/main/java/io/micronaut/http/server/netty/jackson/JsonContentProcessor.java +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/jackson/JsonContentProcessor.java @@ -49,7 +49,7 @@ public final class JsonContentProcessor extends AbstractHttpContentProcessor { private final JsonMapper jsonMapper; private final JsonCounter counter = new JsonCounter(); - private final boolean tokenize; + private boolean tokenize; private ByteBuf singleBuffer; private CompositeByteBuf compositeBuffer; @@ -83,6 +83,7 @@ public HttpContentProcessor resultType(Argument type) { if (genericArgument.isPresent() && !Iterable.class.isAssignableFrom(genericArgument.get().getType()) && !isJsonStream) { // if the generic argument is not a iterable type them stream the array into the publisher counter.unwrapTopLevelArray(); + tokenize = true; } } } diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/multipart/MultipartBodyArgumentBinder.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/multipart/MultipartBodyArgumentBinder.java index de4e1ec7ad0..b520c13aed3 100644 --- a/http-server-netty/src/main/java/io/micronaut/http/server/netty/multipart/MultipartBodyArgumentBinder.java +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/multipart/MultipartBodyArgumentBinder.java @@ -88,7 +88,7 @@ public BindingResult bind(ArgumentConversionContext Optional.of(Flux.error(e)::subscribe); } Set partial = new HashSet<>(); //noinspection unchecked diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/multipart/NettyStreamingFileUpload.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/multipart/NettyStreamingFileUpload.java index 8bb9d38e005..7fb642356b0 100644 --- a/http-server-netty/src/main/java/io/micronaut/http/server/netty/multipart/NettyStreamingFileUpload.java +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/multipart/NettyStreamingFileUpload.java @@ -175,6 +175,7 @@ public void onNext(PartData o) { @Override public void onError(Throwable t) { + discard(); emitter.error(t); try { if (outputStream != null) { @@ -189,6 +190,7 @@ public void onError(Throwable t) { @Override public void onComplete() { + discard(); try { outputStream.close(); emitter.success(true); diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/ssl/HttpRequestCertificateHandler.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/ssl/HttpRequestCertificateHandler.java deleted file mode 100644 index d8824ea55b7..00000000000 --- a/http-server-netty/src/main/java/io/micronaut/http/server/netty/ssl/HttpRequestCertificateHandler.java +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Copyright 2017-2020 original authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.micronaut.http.server.netty.ssl; - -import io.micronaut.core.annotation.Internal; -import io.micronaut.core.annotation.Nullable; -import io.micronaut.http.HttpAttributes; -import io.micronaut.http.HttpMessage; -import io.netty.channel.ChannelHandlerContext; -import io.netty.channel.ChannelInboundHandlerAdapter; -import io.netty.handler.ssl.SslHandler; - -import javax.net.ssl.SSLPeerUnverifiedException; -import java.security.cert.Certificate; - -/** - * Adds the certificate to the decoded request. - * - * @author James Kleeh - * @author Björn Heinrichs - * @since 1.3.0 - */ -@Internal -public class HttpRequestCertificateHandler extends ChannelInboundHandlerAdapter { - private final SslHandler sslHandler; - private Certificate certificate; - - public HttpRequestCertificateHandler(SslHandler sslHandler) { - this.sslHandler = sslHandler; - } - - @Override - public void channelRead(final ChannelHandlerContext ctx, final Object msg) throws Exception { - if (msg instanceof HttpMessage http) { - if (certificate == null) { - certificate = getCertificate(sslHandler); - if (certificate == null) { - ctx.pipeline().remove(this); - super.channelRead(ctx, msg); - return; - } - } - http.setAttribute(HttpAttributes.X509_CERTIFICATE, certificate); - } - super.channelRead(ctx, msg); - } - - @Nullable - private static Certificate getCertificate(final SslHandler handler) { - try { - return handler.engine().getSession().getPeerCertificates()[0]; - } catch (SSLPeerUnverifiedException ex) { - return null; - } - } -} diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/types/NettyCustomizableResponseType.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/types/NettyCustomizableResponseType.java index 9b7b86e9a04..aaa4f074277 100644 --- a/http-server-netty/src/main/java/io/micronaut/http/server/netty/types/NettyCustomizableResponseType.java +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/types/NettyCustomizableResponseType.java @@ -16,11 +16,11 @@ package io.micronaut.http.server.netty.types; import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.Nullable; import io.micronaut.http.HttpRequest; import io.micronaut.http.MutableHttpResponse; import io.micronaut.http.server.types.CustomizableResponseType; -import io.netty.channel.ChannelFuture; -import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.http.HttpResponse; /** * A special type that allows writing data in Netty. @@ -36,8 +36,20 @@ public interface NettyCustomizableResponseType extends CustomizableResponseType * * @param request The request * @param response The response - * @param context The Netty {@link ChannelHandlerContext} - * @return The netty future that completes when the response is fully written. + * @return The netty response */ - ChannelFuture write(HttpRequest request, MutableHttpResponse response, ChannelHandlerContext context); + CustomResponse write(HttpRequest request, MutableHttpResponse response); + + /** + * Wrapper class for a netty response with a special body type, like + * {@link io.netty.handler.codec.http.HttpChunkedInput} or + * {@link io.netty.channel.FileRegion}. + * + * @param response The response + * @param body The body, or {@code null} if there is no body + * @param needLast Whether to finish the response with a + * {@link io.netty.handler.codec.http.LastHttpContent} + */ + record CustomResponse(HttpResponse response, @Nullable Object body, boolean needLast) { + } } diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/types/NettyCustomizableResponseTypeHandler.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/types/NettyCustomizableResponseTypeHandler.java index 1a781eabee6..1e8c908b2b5 100644 --- a/http-server-netty/src/main/java/io/micronaut/http/server/netty/types/NettyCustomizableResponseTypeHandler.java +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/types/NettyCustomizableResponseTypeHandler.java @@ -20,8 +20,6 @@ import io.micronaut.core.order.Ordered; import io.micronaut.http.HttpRequest; import io.micronaut.http.MutableHttpResponse; -import io.netty.channel.ChannelFuture; -import io.netty.channel.ChannelHandlerContext; /** * Represents a class that is designed to handle specific types that are returned from routes in a netty specific way. @@ -40,10 +38,9 @@ public interface NettyCustomizableResponseTypeHandler extends Ordered { * @param object The object to be handled * @param request The native Netty request * @param response The mutable Micronaut response - * @param context The channel context - * @return The channel future that completes when the response is fully written. + * @return The netty response */ - ChannelFuture handle(T object, HttpRequest request, MutableHttpResponse response, ChannelHandlerContext context); + NettyCustomizableResponseType.CustomResponse handle(T object, HttpRequest request, MutableHttpResponse response); /** * @param type The type to check diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/types/files/FileTypeHandler.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/types/files/FileTypeHandler.java index 340f284c20b..56b2d92989b 100644 --- a/http-server-netty/src/main/java/io/micronaut/http/server/netty/types/files/FileTypeHandler.java +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/types/files/FileTypeHandler.java @@ -23,13 +23,12 @@ import io.micronaut.http.MutableHttpResponse; import io.micronaut.http.netty.NettyMutableHttpResponse; import io.micronaut.http.server.netty.configuration.NettyHttpServerConfiguration; +import io.micronaut.http.server.netty.types.NettyCustomizableResponseType; import io.micronaut.http.server.netty.types.NettyCustomizableResponseTypeHandler; import io.micronaut.http.server.netty.types.NettyFileCustomizableResponseType; import io.micronaut.http.server.types.CustomizableResponseTypeException; import io.micronaut.http.server.types.files.StreamedFile; import io.micronaut.http.server.types.files.SystemFile; -import io.netty.channel.ChannelFuture; -import io.netty.channel.ChannelHandlerContext; import io.netty.handler.codec.http.FullHttpResponse; import java.io.File; @@ -62,7 +61,7 @@ public FileTypeHandler(NettyHttpServerConfiguration.FileTypeHandlerConfiguration @SuppressWarnings("MagicNumber") @Override - public ChannelFuture handle(Object obj, HttpRequest request, MutableHttpResponse response, ChannelHandlerContext context) { + public NettyCustomizableResponseType.CustomResponse handle(Object obj, HttpRequest request, MutableHttpResponse response) { NettyFileCustomizableResponseType type; if (obj instanceof File) { type = new NettySystemFileCustomizableResponseType((File) obj); @@ -88,7 +87,7 @@ public ChannelFuture handle(Object obj, HttpRequest request, MutableHttpRespo long fileLastModifiedSeconds = lastModified / 1000; if (ifModifiedSinceDateSeconds == fileLastModifiedSeconds) { FullHttpResponse nettyResponse = notModified(response); - return context.writeAndFlush(nettyResponse); + return new NettyCustomizableResponseType.CustomResponse(nettyResponse, null, false); } } @@ -98,7 +97,7 @@ public ChannelFuture handle(Object obj, HttpRequest request, MutableHttpRespo setDateAndCacheHeaders(response, lastModified); type.process(response); - return type.write(request, response, context); + return type.write(request, response); } @Override diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/types/files/NettySystemFileCustomizableResponseType.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/types/files/NettySystemFileCustomizableResponseType.java index 3f95327b5c2..9a3711fffce 100644 --- a/http-server-netty/src/main/java/io/micronaut/http/server/netty/types/files/NettySystemFileCustomizableResponseType.java +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/types/files/NettySystemFileCustomizableResponseType.java @@ -16,7 +16,6 @@ package io.micronaut.http.server.netty.types.files; import io.micronaut.core.annotation.Internal; -import io.micronaut.core.annotation.NonNull; import io.micronaut.core.annotation.Nullable; import io.micronaut.core.util.SupplierUtil; import io.micronaut.http.HttpHeaders; @@ -26,30 +25,26 @@ import io.micronaut.http.MediaType; import io.micronaut.http.MutableHttpResponse; import io.micronaut.http.netty.NettyMutableHttpResponse; +import io.micronaut.http.server.netty.NettyHttpRequest; import io.micronaut.http.server.netty.SmartHttpContentCompressor; import io.micronaut.http.server.netty.types.NettyFileCustomizableResponseType; import io.micronaut.http.server.types.CustomizableResponseTypeException; import io.micronaut.http.server.types.files.FileCustomizableResponseType; import io.micronaut.http.server.types.files.SystemFile; -import io.netty.channel.ChannelFuture; -import io.netty.channel.ChannelFutureListener; -import io.netty.channel.ChannelHandlerContext; import io.netty.channel.DefaultFileRegion; import io.netty.handler.codec.http.DefaultHttpResponse; import io.netty.handler.codec.http.HttpChunkedInput; -import io.netty.handler.codec.http.LastHttpContent; import io.netty.handler.stream.ChunkedFile; import io.netty.util.AttributeKey; import io.netty.util.ResourceLeakDetector; import io.netty.util.ResourceLeakDetectorFactory; import io.netty.util.ResourceLeakTracker; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; import java.io.RandomAccessFile; +import java.nio.channels.FileChannel; import java.util.Optional; import java.util.function.Supplier; @@ -69,7 +64,6 @@ public class NettySystemFileCustomizableResponseType extends SystemFile implemen private static final int LENGTH_8K = 8192; private static final String UNIT_BYTES = "bytes"; - private static final Logger LOG = LoggerFactory.getLogger(NettySystemFileCustomizableResponseType.class); protected Optional delegate = Optional.empty(); @@ -110,7 +104,7 @@ public void process(MutableHttpResponse response) { } @Override - public ChannelFuture write(HttpRequest request, MutableHttpResponse response, ChannelHandlerContext context) { + public CustomResponse write(HttpRequest request, MutableHttpResponse response) { if (response instanceof NettyMutableHttpResponse) { @@ -146,24 +140,19 @@ public ChannelFuture write(HttpRequest request, MutableHttpResponse respon // Write the request data final DefaultHttpResponse finalResponse = new DefaultHttpResponse(nettyResponse.getNettyHttpVersion(), nettyResponse.getNettyHttpStatus(), nettyResponse.getNettyHeaders()); - context.write(finalResponse, context.voidPromise()); - - FileHolder file = new FileHolder(getFile()); // Write the content. - SmartHttpContentCompressor predicate = context.channel().attr(ZERO_COPY_PREDICATE.get()).get(); + SmartHttpContentCompressor predicate = request instanceof NettyHttpRequest nettyRequest ? + nettyRequest.getChannelHandlerContext().channel().attr(ZERO_COPY_PREDICATE.get()).get() : null; if (predicate != null && predicate.shouldSkip(finalResponse)) { // SSL not enabled - can use zero-copy file transfer. - context.write(new DefaultFileRegion(file.raf.getChannel(), position, contentLength), context.newProgressivePromise()) - .addListener(file); - return context.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT); + return new CustomResponse(finalResponse, new TrackedDefaultFileRegion(open(getFile()).getChannel(), position, contentLength), true); } else { // SSL enabled - cannot use zero-copy file transfer. try { // HttpChunkedInput will write the end marker (LastHttpContent) for us. - final HttpChunkedInput chunkedInput = new HttpChunkedInput(new ChunkedFile(file.raf, position, contentLength, LENGTH_8K)); - return context.writeAndFlush(chunkedInput, context.newProgressivePromise()) - .addListener(file); + final HttpChunkedInput chunkedInput = new HttpChunkedInput(new TrackedChunkedFile(open(getFile()), position, contentLength, LENGTH_8K)); + return new CustomResponse(finalResponse, chunkedInput, false); } catch (IOException e) { throw new CustomizableResponseTypeException("Could not read file", e); } @@ -196,6 +185,14 @@ private static IntRange parseRangeHeader(String value, long contentLength) { } } + private static RandomAccessFile open(File file) { + try { + return new RandomAccessFile(file, "r"); + } catch (FileNotFoundException e) { + throw new CustomizableResponseTypeException("Could not find file", e); + } + } + // See https://httpwg.org/specs/rfc9110.html#rule.int-range private static class IntRange { private final long firstPos; @@ -207,51 +204,44 @@ private static class IntRange { } } - /** - * Wrapper class around {@link RandomAccessFile} with two purposes: Leak detection, and implementation of - * {@link ChannelFutureListener} that closes the file when called. - */ - private static final class FileHolder implements ChannelFutureListener { + private static class TrackedDefaultFileRegion extends DefaultFileRegion { //to avoid initializing Netty at build time - private static final Supplier> LEAK_DETECTOR = SupplierUtil.memoized(() -> - ResourceLeakDetectorFactory.instance().newResourceLeakDetector(RandomAccessFile.class)); - - final RandomAccessFile raf; - final long length; + private static final Supplier> LEAK_DETECTOR = SupplierUtil.memoized(() -> + ResourceLeakDetectorFactory.instance().newResourceLeakDetector(TrackedDefaultFileRegion.class)); - private final ResourceLeakTracker tracker; + private final ResourceLeakTracker tracker; - private final File file; + public TrackedDefaultFileRegion(FileChannel fileChannel, long position, long count) { + super(fileChannel, position, count); + this.tracker = LEAK_DETECTOR.get().track(this); + } - FileHolder(File file) { - this.file = file; - try { - this.raf = new RandomAccessFile(file, "r"); - } catch (FileNotFoundException e) { - throw new CustomizableResponseTypeException("Could not find file", e); - } - this.tracker = LEAK_DETECTOR.get().track(raf); - try { - this.length = raf.length(); - } catch (IOException e) { - close(); - throw new CustomizableResponseTypeException("Could not determine file length", e); + @Override + protected void deallocate() { + super.deallocate(); + if (tracker != null) { + tracker.close(this); } } + } - @Override - public void operationComplete(@NonNull ChannelFuture future) throws Exception { - close(); + private static class TrackedChunkedFile extends ChunkedFile { + //to avoid initializing Netty at build time + private static final Supplier> LEAK_DETECTOR = SupplierUtil.memoized(() -> + ResourceLeakDetectorFactory.instance().newResourceLeakDetector(TrackedChunkedFile.class)); + + private final ResourceLeakTracker tracker; + + public TrackedChunkedFile(RandomAccessFile file, long offset, long length, int chunkSize) throws IOException { + super(file, offset, length, chunkSize); + this.tracker = LEAK_DETECTOR.get().track(this); } - void close() { - try { - raf.close(); - } catch (IOException e) { - LOG.warn("An error occurred closing the file reference: " + file.getAbsolutePath(), e); - } + @Override + public void close() throws Exception { + super.close(); if (tracker != null) { - tracker.close(raf); + tracker.close(this); } } } diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/types/stream/NettyStreamedCustomizableResponseType.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/types/stream/NettyStreamedCustomizableResponseType.java index 2177d826eea..458bb779ac7 100644 --- a/http-server-netty/src/main/java/io/micronaut/http/server/netty/types/stream/NettyStreamedCustomizableResponseType.java +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/types/stream/NettyStreamedCustomizableResponseType.java @@ -20,19 +20,14 @@ import io.micronaut.http.MutableHttpResponse; import io.micronaut.http.netty.NettyMutableHttpResponse; import io.micronaut.http.server.netty.types.NettyCustomizableResponseType; -import io.netty.channel.ChannelFuture; -import io.netty.channel.ChannelFutureListener; -import io.netty.channel.ChannelHandlerContext; import io.netty.handler.codec.http.DefaultHttpResponse; import io.netty.handler.codec.http.HttpChunkedInput; import io.netty.handler.codec.http.HttpHeaderNames; import io.netty.handler.codec.http.HttpHeaderValues; -import io.netty.handler.codec.http.LastHttpContent; import io.netty.handler.stream.ChunkedStream; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.io.IOException; import java.io.InputStream; /** @@ -49,7 +44,7 @@ public interface NettyStreamedCustomizableResponseType extends NettyCustomizable InputStream getInputStream(); @Override - default ChannelFuture write(HttpRequest request, MutableHttpResponse response, ChannelHandlerContext context) { + default CustomResponse write(HttpRequest request, MutableHttpResponse response) { if (response instanceof NettyMutableHttpResponse) { NettyMutableHttpResponse nettyResponse = ((NettyMutableHttpResponse) response); @@ -57,20 +52,12 @@ default ChannelFuture write(HttpRequest request, MutableHttpResponse respo final DefaultHttpResponse finalResponse = new DefaultHttpResponse(nettyResponse.getNettyHttpVersion(), nettyResponse.getNettyHttpStatus(), nettyResponse.getNettyHeaders()); InputStream inputStream = getInputStream(); // can be null if the stream was closed - context.write(finalResponse, context.voidPromise()); if (inputStream != null) { - ChannelFutureListener closeListener = (future) -> { - try { - inputStream.close(); - } catch (IOException e) { - LOG.warn("An error occurred closing an input stream", e); - } - }; final HttpChunkedInput chunkedInput = new HttpChunkedInput(new ChunkedStream(inputStream)); - return context.writeAndFlush(chunkedInput).addListener(closeListener); + return new CustomResponse(finalResponse, chunkedInput, false); } else { - return context.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT); + return new CustomResponse(finalResponse, null, true); } } else { diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/websocket/NettyServerWebSocketUpgradeHandler.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/websocket/NettyServerWebSocketUpgradeHandler.java index bc90a491a53..5b13ff214e5 100644 --- a/http-server-netty/src/main/java/io/micronaut/http/server/netty/websocket/NettyServerWebSocketUpgradeHandler.java +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/websocket/NettyServerWebSocketUpgradeHandler.java @@ -18,6 +18,7 @@ import io.micronaut.core.annotation.Internal; import io.micronaut.core.annotation.NonNull; import io.micronaut.core.annotation.Nullable; +import io.micronaut.core.convert.ConversionService; import io.micronaut.core.execution.ExecutionFlow; import io.micronaut.core.util.StringUtils; import io.micronaut.http.HttpAttributes; @@ -36,6 +37,10 @@ import io.micronaut.http.server.RouteExecutor; import io.micronaut.http.server.netty.NettyEmbeddedServices; import io.micronaut.http.server.netty.NettyHttpRequest; +import io.micronaut.http.server.netty.RoutingInBoundHandler; +import io.micronaut.http.server.netty.configuration.NettyHttpServerConfiguration; +import io.micronaut.http.server.netty.handler.PipeliningServerHandler; +import io.micronaut.http.server.netty.handler.RequestHandler; import io.micronaut.web.router.RouteMatch; import io.micronaut.web.router.Router; import io.micronaut.web.router.UriRouteMatch; @@ -47,10 +52,8 @@ import io.micronaut.websocket.context.WebSocketBeanRegistry; import io.netty.channel.Channel; import io.netty.channel.ChannelFuture; -import io.netty.channel.ChannelHandler; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelPipeline; -import io.netty.channel.SimpleChannelInboundHandler; import io.netty.handler.codec.http.DefaultHttpHeaders; import io.netty.handler.codec.http.HttpHeaderNames; import io.netty.handler.codec.http.HttpHeaderValues; @@ -65,6 +68,7 @@ import java.util.List; import java.util.Map; +import java.util.NoSuchElementException; import java.util.Optional; /** @@ -74,7 +78,7 @@ * @since 1.0 */ @Internal -public class NettyServerWebSocketUpgradeHandler extends SimpleChannelInboundHandler> { +public final class NettyServerWebSocketUpgradeHandler implements RequestHandler { public static final String ID = ChannelPipelineCustomizer.HANDLER_WEBSOCKET_UPGRADE; public static final String SCHEME_WEBSOCKET = "ws://"; @@ -90,31 +94,36 @@ public class NettyServerWebSocketUpgradeHandler extends SimpleChannelInboundHand private final WebSocketSessionRepository webSocketSessionRepository; private final RouteExecutor routeExecutor; private final NettyEmbeddedServices nettyEmbeddedServices; + private final ConversionService conversionService; + private final NettyHttpServerConfiguration serverConfiguration; private WebSocketServerHandshaker handshaker; private boolean cancelUpgrade = false; + private RoutingInBoundHandler next; + /** * Default constructor. * * @param embeddedServices The embedded server services * @param webSocketSessionRepository The websocket session repository + * @param conversionService The conversion service + * @param serverConfiguration The server configuration */ public NettyServerWebSocketUpgradeHandler(NettyEmbeddedServices embeddedServices, - WebSocketSessionRepository webSocketSessionRepository) { + WebSocketSessionRepository webSocketSessionRepository, + ConversionService conversionService, + NettyHttpServerConfiguration serverConfiguration) { this.router = embeddedServices.getRouter(); this.webSocketBeanRegistry = WebSocketBeanRegistry.forServer(embeddedServices.getApplicationContext()); this.webSocketSessionRepository = webSocketSessionRepository; this.routeExecutor = embeddedServices.getRouteExecutor(); this.nettyEmbeddedServices = embeddedServices; + this.conversionService = conversionService; + this.serverConfiguration = serverConfiguration; } - @Override - public boolean acceptInboundMessage(Object msg) { - return msg instanceof NettyHttpRequest && isWebSocketUpgrade((NettyHttpRequest) msg); - } - - private boolean isWebSocketUpgrade(@NonNull NettyHttpRequest request) { - HttpHeaders headers = request.getNativeRequest().headers(); + static boolean isWebSocketUpgrade(@NonNull io.netty.handler.codec.http.HttpRequest request) { + HttpHeaders headers = request.headers(); if (headers.containsValue(HttpHeaderNames.CONNECTION, HttpHeaderValues.UPGRADE, true)) { return headers.containsValue(HttpHeaderNames.UPGRADE, WEB_SOCKET_HEADER_VALUE, true); } @@ -122,23 +131,37 @@ private boolean isWebSocketUpgrade(@NonNull NettyHttpRequest request) { } @Override - protected final void channelRead0(ChannelHandlerContext ctx, NettyHttpRequest msg) { - ServerRequestContext.set(msg); - - Optional> optionalRoute = router.find(HttpMethod.GET, msg.getPath(), msg) - .filter(rm -> rm.isAnnotationPresent(OnMessage.class) || rm.isAnnotationPresent(OnOpen.class)) - .findFirst(); - - WebsocketRequestLifecycle requestLifecycle = new WebsocketRequestLifecycle(routeExecutor, msg, optionalRoute.orElse(null)); - ExecutionFlow> responseFlow = ExecutionFlow.async(ctx.channel().eventLoop(), requestLifecycle::handle); - responseFlow.onComplete((response, throwable) -> { - if (response != null) { - writeResponse(ctx, msg, requestLifecycle.shouldProceedNormally, response); - } - }); + public void accept(ChannelHandlerContext ctx, io.netty.handler.codec.http.HttpRequest request, PipeliningServerHandler.OutboundAccess outboundAccess) { + if (isWebSocketUpgrade(request)) { + NettyHttpRequest msg = new NettyHttpRequest<>(request, ctx, conversionService, serverConfiguration); + + Optional> optionalRoute = router.find(HttpMethod.GET, msg.getPath(), msg) + .filter(rm -> rm.isAnnotationPresent(OnMessage.class) || rm.isAnnotationPresent(OnOpen.class)) + .findFirst(); + + WebsocketRequestLifecycle requestLifecycle = new WebsocketRequestLifecycle(routeExecutor, msg, optionalRoute.orElse(null)); + ExecutionFlow> responseFlow = ExecutionFlow.async(ctx.channel().eventLoop(), requestLifecycle::handle); + responseFlow.onComplete((response, throwable) -> { + if (response != null) { + writeResponse(ctx, msg, requestLifecycle.shouldProceedNormally, response, outboundAccess); + } + }); + } else { + next.accept(ctx, request, outboundAccess); + } } - private void writeResponse(ChannelHandlerContext ctx, NettyHttpRequest msg, boolean shouldProceedNormally, MutableHttpResponse actualResponse) { + @Override + public void handleUnboundError(Throwable cause) { + next.handleUnboundError(cause); + } + + @Override + public void responseWritten(Object attachment) { + next.responseWritten(attachment); + } + + private void writeResponse(ChannelHandlerContext ctx, NettyHttpRequest msg, boolean shouldProceedNormally, MutableHttpResponse actualResponse, PipeliningServerHandler.OutboundAccess outboundAccess) { if (cancelUpgrade) { if (LOG.isDebugEnabled()) { LOG.debug("Cancelling websocket upgrade, handler was removed while request was processing"); @@ -169,11 +192,10 @@ private void writeResponse(ChannelHandlerContext ctx, NettyHttpRequest msg, b routeExecutor.getCoroutineHelper().orElse(null)); pipeline.addBefore(ctx.name(), NettyServerWebSocketHandler.ID, webSocketHandler); - pipeline.remove(ChannelPipelineCustomizer.HANDLER_HTTP_STREAM); - pipeline.remove(NettyServerWebSocketUpgradeHandler.this); - ChannelHandler accessLoggerHandler = pipeline.get(ChannelPipelineCustomizer.HANDLER_ACCESS_LOGGER); - if (accessLoggerHandler != null) { - pipeline.remove(accessLoggerHandler); + pipeline.remove(ctx.name()); + try { + pipeline.remove(ChannelPipelineCustomizer.HANDLER_ACCESS_LOGGER); + } catch (NoSuchElementException ignored) { } } catch (Throwable e) { @@ -183,7 +205,7 @@ private void writeResponse(ChannelHandlerContext ctx, NettyHttpRequest msg, b ctx.writeAndFlush(new CloseWebSocketFrame(CloseReason.INTERNAL_ERROR.getCode(), CloseReason.INTERNAL_ERROR.getReason())); } } else { - ctx.writeAndFlush(actualResponse); + next.writeResponse(outboundAccess, msg, actualResponse, null); } } @@ -247,15 +269,12 @@ protected String getWebSocketURL(ChannelHandlerContext ctx, HttpRequest req) { } @Override - public void handlerRemoved(ChannelHandlerContext ctx) throws Exception { - super.handlerRemoved(ctx); + public void removed() { cancelUpgrade = true; } - @Override - public void channelInactive(@NonNull ChannelHandlerContext ctx) throws Exception { - super.channelInactive(ctx); - cancelUpgrade = true; + public void setNext(RoutingInBoundHandler next) { + this.next = next; } private static final class WebsocketRequestLifecycle extends RequestLifecycle { diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/websocket/WebSocketUpgradeHandlerFactory.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/websocket/WebSocketUpgradeHandlerFactory.java index 89dc3d992f6..73042b83396 100644 --- a/http-server-netty/src/main/java/io/micronaut/http/server/netty/websocket/WebSocketUpgradeHandlerFactory.java +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/websocket/WebSocketUpgradeHandlerFactory.java @@ -17,11 +17,11 @@ import io.micronaut.context.annotation.Requires; import io.micronaut.core.annotation.Internal; +import io.micronaut.core.convert.ConversionService; import io.micronaut.http.server.netty.NettyEmbeddedServer; import io.micronaut.http.server.netty.NettyEmbeddedServices; -import io.micronaut.http.server.netty.NettyHttpRequest; +import io.micronaut.http.server.netty.configuration.NettyHttpServerConfiguration; import io.micronaut.websocket.context.WebSocketBeanRegistry; -import io.netty.channel.SimpleChannelInboundHandler; import jakarta.inject.Singleton; /** @@ -34,13 +34,22 @@ @Singleton @Internal public final class WebSocketUpgradeHandlerFactory { + private final ConversionService conversionService; + private final NettyHttpServerConfiguration serverConfiguration; + + public WebSocketUpgradeHandlerFactory(ConversionService conversionService, NettyHttpServerConfiguration serverConfiguration) { + this.conversionService = conversionService; + this.serverConfiguration = serverConfiguration; + } + /** * Creates the websocket upgrade inbound handler. - * @param embeddedServer The server + * + * @param embeddedServer The server * @param nettyEmbeddedServices The services * @return The handler */ - public SimpleChannelInboundHandler> create(NettyEmbeddedServer embeddedServer, NettyEmbeddedServices nettyEmbeddedServices) { - return new NettyServerWebSocketUpgradeHandler(nettyEmbeddedServices, embeddedServer); + public NettyServerWebSocketUpgradeHandler create(NettyEmbeddedServer embeddedServer, NettyEmbeddedServices nettyEmbeddedServices) { + return new NettyServerWebSocketUpgradeHandler(nettyEmbeddedServices, embeddedServer, conversionService, serverConfiguration); } } diff --git a/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/binding/ConcurrentFormTransferSpec.groovy b/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/binding/ConcurrentFormTransferSpec.groovy index 07766986019..8dd89e7026c 100644 --- a/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/binding/ConcurrentFormTransferSpec.groovy +++ b/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/binding/ConcurrentFormTransferSpec.groovy @@ -83,11 +83,9 @@ Content-Type: ${contentType}\r def request = uploadRequest(embeddedServer.URI) def response = client.send(request, loadClass('java.net.http.HttpResponse$BodyHandlers').ofString()) - println 'status code: ' + response.statusCode() - println response.body() - then: - 1 == 1 + response.statusCode() == 200 + response.body() == "uploaded" cleanup: ctx.stop() @@ -99,13 +97,13 @@ Content-Type: ${contentType}\r @SuppressWarnings(['GrMethodMayBeStatic', 'unused']) @Post('/testupload2') @Consumes(MediaType.MULTIPART_FORM_DATA) - Publisher> uploadTest2(Flux dataFile) { + Publisher> uploadTest2(Publisher dataFile) { def os = new OutputStream() { @Override void write(int b) throws IOException { } } - return dataFile + return Flux.from(dataFile) .flatMap { it.transferTo(os) } .map { success -> success ? io.micronaut.http.HttpResponse. ok('uploaded') : io.micronaut.http.HttpResponse. status(HttpStatus.INTERNAL_SERVER_ERROR, 'error 1') } } diff --git a/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/fuzzing/FuzzyInputSpec.groovy b/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/fuzzing/FuzzyInputSpec.groovy index ac43f9b3005..082027167a9 100644 --- a/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/fuzzing/FuzzyInputSpec.groovy +++ b/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/fuzzing/FuzzyInputSpec.groovy @@ -85,7 +85,7 @@ class FuzzyInputSpec extends Specification { when: def embeddedChannel = embeddedServer.buildEmbeddedChannel(false) - embeddedChannel.writeOneInbound(Unpooled.wrappedBuffer(input)); + embeddedChannel.writeInbound(Unpooled.wrappedBuffer(input)); embeddedChannel.runPendingTasks(); embeddedChannel.releaseOutbound() diff --git a/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/handler/PipeliningServerHandlerSpec.groovy b/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/handler/PipeliningServerHandlerSpec.groovy new file mode 100644 index 00000000000..0949f987bc6 --- /dev/null +++ b/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/handler/PipeliningServerHandlerSpec.groovy @@ -0,0 +1,217 @@ +package io.micronaut.http.server.netty.handler + + +import io.micronaut.http.netty.stream.StreamedHttpRequest +import io.micronaut.http.server.netty.DelegateStreamedHttpResponse +import io.netty.buffer.Unpooled +import io.netty.channel.ChannelHandlerContext +import io.netty.channel.ChannelOutboundHandlerAdapter +import io.netty.channel.embedded.EmbeddedChannel +import io.netty.handler.codec.http.DefaultFullHttpRequest +import io.netty.handler.codec.http.DefaultFullHttpResponse +import io.netty.handler.codec.http.DefaultHttpContent +import io.netty.handler.codec.http.DefaultHttpRequest +import io.netty.handler.codec.http.DefaultHttpResponse +import io.netty.handler.codec.http.DefaultLastHttpContent +import io.netty.handler.codec.http.FullHttpRequest +import io.netty.handler.codec.http.HttpContent +import io.netty.handler.codec.http.HttpHeaderNames +import io.netty.handler.codec.http.HttpHeaderValues +import io.netty.handler.codec.http.HttpMethod +import io.netty.handler.codec.http.HttpRequest +import io.netty.handler.codec.http.HttpResponse +import io.netty.handler.codec.http.HttpResponseStatus +import io.netty.handler.codec.http.HttpVersion +import reactor.core.publisher.Flux +import reactor.core.publisher.Sinks +import spock.lang.Specification + +import java.nio.charset.StandardCharsets + +class PipeliningServerHandlerSpec extends Specification { + def 'pipelined requests have their responses batched'() { + given: + def mon = new MonitorHandler() + def resp = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.NO_CONTENT) + def ch = new EmbeddedChannel(mon, new PipeliningServerHandler(new RequestHandler() { + @Override + void accept(ChannelHandlerContext ctx, HttpRequest request, PipeliningServerHandler.OutboundAccess outboundAccess) { + outboundAccess.writeFull(resp) + } + + @Override + void handleUnboundError(Throwable cause) { + cause.printStackTrace() + } + })) + + expect: + mon.read == 1 + mon.flush == 0 + + when: + ch.writeOneInbound(new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, "/")) + then: + mon.read == 1 + mon.flush == 0 + + when: + ch.writeOneInbound(new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, "/")) + then: + mon.read == 1 + mon.flush == 0 + + when: + ch.flushInbound() + then: + mon.read == 2 + mon.flush == 1 + ch.readOutbound() == resp + ch.readOutbound() == resp + ch.readOutbound() == null + ch.checkException() + } + + def 'streaming responses flush after every item'() { + given: + def mon = new MonitorHandler() + def resp = new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK) + resp.headers().add(HttpHeaderNames.TRANSFER_ENCODING, HttpHeaderValues.CHUNKED) + def sink = Sinks.many().unicast().onBackpressureBuffer() + def ch = new EmbeddedChannel(mon, new PipeliningServerHandler(new RequestHandler() { + @Override + void accept(ChannelHandlerContext ctx, HttpRequest request, PipeliningServerHandler.OutboundAccess outboundAccess) { + outboundAccess.writeStreamed(new DelegateStreamedHttpResponse(resp, sink.asFlux())) + } + + @Override + void handleUnboundError(Throwable cause) { + cause.printStackTrace() + } + })) + + expect: + mon.read == 1 + mon.flush == 0 + + when: + ch.writeInbound(new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, "/")) + then: + mon.read == 2 + // response is delayed until first content + mon.flush == 0 + + when: + def c1 = new DefaultHttpContent(Unpooled.wrappedBuffer("foo".getBytes(StandardCharsets.UTF_8))) + sink.tryEmitNext(c1) + then: + mon.read == 2 + mon.flush == 1 + ch.readOutbound() instanceof HttpResponse + ch.readOutbound() == c1 + ch.readOutbound() == null + + when: + def c2 = new DefaultHttpContent(Unpooled.wrappedBuffer("foo".getBytes(StandardCharsets.UTF_8))) + sink.tryEmitNext(c2) + then: + mon.read == 2 + mon.flush == 2 + ch.readOutbound() == c2 + ch.readOutbound() == null + ch.checkException() + } + + def 'requests that come in a single packet are accumulated'() { + given: + def mon = new MonitorHandler() + def resp = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.NO_CONTENT) + def ch = new EmbeddedChannel(mon, new PipeliningServerHandler(new RequestHandler() { + @Override + void accept(ChannelHandlerContext ctx, HttpRequest request, PipeliningServerHandler.OutboundAccess outboundAccess) { + assert request instanceof FullHttpRequest + assert request.content().toString(StandardCharsets.UTF_8) == "foobar" + request.release() + outboundAccess.writeFull(resp) + } + + @Override + void handleUnboundError(Throwable cause) { + cause.printStackTrace() + } + })) + + expect: + mon.read == 1 + mon.flush == 0 + + when: + def req = new DefaultHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.POST, "/") + req.headers().add(HttpHeaderNames.CONTENT_LENGTH, 6) + ch.writeInbound( + req, + new DefaultHttpContent(Unpooled.wrappedBuffer("foo".getBytes(StandardCharsets.UTF_8))), + new DefaultLastHttpContent(Unpooled.wrappedBuffer("bar".getBytes(StandardCharsets.UTF_8))) + ) + then: + ch.checkException() + mon.read == 2 + mon.flush == 1 + ch.readOutbound() == resp + ch.readOutbound() == null + } + + def 'continue support'() { + given: + def mon = new MonitorHandler() + def resp = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.NO_CONTENT) + def ch = new EmbeddedChannel(mon, new PipeliningServerHandler(new RequestHandler() { + @Override + void accept(ChannelHandlerContext ctx, HttpRequest request, PipeliningServerHandler.OutboundAccess outboundAccess) { + Flux.from((StreamedHttpRequest) request).collectList().subscribe { outboundAccess.writeFull(resp) } + } + + @Override + void handleUnboundError(Throwable cause) { + cause.printStackTrace() + } + })) + + expect: + mon.read == 1 + mon.flush == 0 + + when: + def req = new DefaultHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, "/") + req.headers().add(HttpHeaderNames.EXPECT, HttpHeaderValues.CONTINUE) + req.headers().add(HttpHeaderNames.CONTENT_LENGTH, 3) + ch.writeInbound(req) + then: + HttpResponse cont = ch.readOutbound() + cont.status() == HttpResponseStatus.CONTINUE + ch.readOutbound() == null + + when: + ch.writeInbound(new DefaultLastHttpContent(Unpooled.wrappedBuffer("foo".getBytes(StandardCharsets.UTF_8)))) + then: + ch.readOutbound() == resp + ch.readOutbound() == null + } + + static class MonitorHandler extends ChannelOutboundHandlerAdapter { + int flush = 0 + int read = 0 + + @Override + void flush(ChannelHandlerContext ctx) throws Exception { + super.flush(ctx) + flush++ + } + + @Override + void read(ChannelHandlerContext ctx) throws Exception { + super.read(ctx) + read++ + } + } +} diff --git a/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/handler/accesslog/AccessLogSpec.groovy b/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/handler/accesslog/AccessLogSpec.groovy index 34266f9c18c..05bf072881d 100644 --- a/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/handler/accesslog/AccessLogSpec.groovy +++ b/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/handler/accesslog/AccessLogSpec.groovy @@ -24,12 +24,13 @@ import io.netty.channel.ChannelOption import io.netty.channel.nio.NioEventLoopGroup import io.netty.channel.socket.nio.NioSocketChannel import io.netty.handler.codec.http.DefaultFullHttpRequest +import io.netty.handler.codec.http.DefaultHttpRequest +import io.netty.handler.codec.http.DefaultLastHttpContent import io.netty.handler.codec.http.FullHttpResponse import io.netty.handler.codec.http.HttpClientCodec import io.netty.handler.codec.http.HttpClientUpgradeHandler import io.netty.handler.codec.http.HttpHeaderNames import io.netty.handler.codec.http.HttpHeaderValues -import io.netty.handler.codec.http.HttpMessage import io.netty.handler.codec.http.HttpMethod import io.netty.handler.codec.http.HttpObjectAggregator import io.netty.handler.codec.http.HttpResponseStatus @@ -48,7 +49,6 @@ import io.netty.handler.ssl.ApplicationProtocolNegotiationHandler import io.netty.handler.ssl.SslContextBuilder import io.netty.handler.ssl.SupportedCipherSuiteFilter import io.netty.handler.ssl.util.InsecureTrustManagerFactory -import io.netty.util.ReferenceCountUtil import jakarta.inject.Singleton import org.slf4j.LoggerFactory import reactor.core.publisher.Mono @@ -225,11 +225,12 @@ class AccessLogSpec extends Specification { }) .remoteAddress(server.host, server.port) - def request1 = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.POST, '/interleave/post', Unpooled.wrappedBuffer('foo'.getBytes(StandardCharsets.UTF_8))) + def request1 = new DefaultHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.POST, '/interleave/post') request1.headers().add(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE) request1.headers().add(HttpHeaderNames.EXPECT, HttpHeaderValues.CONTINUE) request1.headers().add(HttpHeaderNames.CONTENT_TYPE, HttpHeaderValues.TEXT_PLAIN) request1.headers().add(HttpHeaderNames.CONTENT_LENGTH, 3) + def body1 = new DefaultLastHttpContent(Unpooled.wrappedBuffer('foo'.getBytes(StandardCharsets.UTF_8))) def request2 = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, '/interleave/simple') request2.headers().add(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE) @@ -239,7 +240,14 @@ class AccessLogSpec extends Specification { when: def channel = bootstrap.connect().sync().channel() - channel.write(request1) + channel.writeAndFlush(request1) + then: + new PollingConditions(timeout: 5).eventually { + responses.size() == 1 + } + + when: + channel.write(body1) channel.writeAndFlush(request2) then: diff --git a/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/websocket/UpgradeSpec.groovy b/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/websocket/UpgradeSpec.groovy index 0ba56da9b04..a241dae170a 100644 --- a/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/websocket/UpgradeSpec.groovy +++ b/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/websocket/UpgradeSpec.groovy @@ -1,20 +1,12 @@ package io.micronaut.http.server.netty.websocket -import io.micronaut.core.convert.ConversionService + import io.micronaut.http.HttpHeaders -import io.micronaut.http.HttpRequest -import io.micronaut.http.netty.websocket.WebSocketSessionRepository -import io.micronaut.http.server.HttpServerConfiguration -import io.micronaut.http.server.binding.RequestArgumentSatisfier -import io.micronaut.http.server.netty.NettyEmbeddedServices -import io.micronaut.http.server.netty.NettyHttpRequest -import io.netty.channel.ChannelHandlerContext import io.netty.handler.codec.http.DefaultFullHttpRequest import io.netty.handler.codec.http.HttpMethod import io.netty.handler.codec.http.HttpVersion import spock.lang.Specification import spock.lang.Unroll -import spock.mock.DetachedMockFactory class UpgradeSpec extends Specification { @@ -26,20 +18,8 @@ class UpgradeSpec extends Specification { nettyRequest.headers().set(header.name, header.value) } - def mock = Mock(NettyEmbeddedServices) - mock.getRequestArgumentSatisfier() >> new RequestArgumentSatisfier(null) - NettyServerWebSocketUpgradeHandler handler = new NettyServerWebSocketUpgradeHandler(mock, Mock(WebSocketSessionRepository)) - - when: - HttpRequest request = new NettyHttpRequest( - nettyRequest, - new DetachedMockFactory().Mock(ChannelHandlerContext.class), - ConversionService.SHARED, - new HttpServerConfiguration() - ) - - then: - handler.acceptInboundMessage(request) + expect: + NettyServerWebSocketUpgradeHandler.isWebSocketUpgrade(nettyRequest) where: headers << [ diff --git a/http/src/main/java/io/micronaut/http/HttpAttributes.java b/http/src/main/java/io/micronaut/http/HttpAttributes.java index 59a7883d140..63e14860156 100644 --- a/http/src/main/java/io/micronaut/http/HttpAttributes.java +++ b/http/src/main/java/io/micronaut/http/HttpAttributes.java @@ -75,7 +75,10 @@ public enum HttpAttributes implements CharSequence { /** * Attribute used to store a client Certificate (mutual authentication). + * + * @deprecated Use {@link HttpRequest#getCertificate()} instead */ + @Deprecated X509_CERTIFICATE("javax.servlet.request.X509Certificate"), /** diff --git a/http/src/main/java/io/micronaut/http/HttpRequest.java b/http/src/main/java/io/micronaut/http/HttpRequest.java index b3695b4a055..3ca5a95cade 100644 --- a/http/src/main/java/io/micronaut/http/HttpRequest.java +++ b/http/src/main/java/io/micronaut/http/HttpRequest.java @@ -177,6 +177,7 @@ default Optional getLocale() { * * @return A certificate used for authentication, if applicable. */ + @SuppressWarnings("deprecation") default Optional getCertificate() { return this.getAttribute(HttpAttributes.X509_CERTIFICATE, Certificate.class); } diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/netty/LogbookNettyServerCustomizer.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/netty/LogbookNettyServerCustomizer.kt index 5ac765c1f24..dc54e12258f 100644 --- a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/netty/LogbookNettyServerCustomizer.kt +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/netty/LogbookNettyServerCustomizer.kt @@ -32,7 +32,7 @@ class LogbookNettyServerCustomizer(private val logbook: Logbook) : override fun onStreamPipelineBuilt() { channel!!.pipeline().addBefore( // <5> - ChannelPipelineCustomizer.HANDLER_HTTP_STREAM, + ChannelPipelineCustomizer.HANDLER_MICRONAUT_INBOUND, "logbook", LogbookServerHandler(logbook) ) diff --git a/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/netty/LogbookNettyServerCustomizer.kt b/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/netty/LogbookNettyServerCustomizer.kt index 5ac765c1f24..dc54e12258f 100644 --- a/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/netty/LogbookNettyServerCustomizer.kt +++ b/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/netty/LogbookNettyServerCustomizer.kt @@ -32,7 +32,7 @@ class LogbookNettyServerCustomizer(private val logbook: Logbook) : override fun onStreamPipelineBuilt() { channel!!.pipeline().addBefore( // <5> - ChannelPipelineCustomizer.HANDLER_HTTP_STREAM, + ChannelPipelineCustomizer.HANDLER_MICRONAUT_INBOUND, "logbook", LogbookServerHandler(logbook) ) diff --git a/test-suite/src/test/java/io/micronaut/docs/netty/LogbookNettyServerCustomizer.java b/test-suite/src/test/java/io/micronaut/docs/netty/LogbookNettyServerCustomizer.java index 6e8b860058d..6b0f53be0eb 100644 --- a/test-suite/src/test/java/io/micronaut/docs/netty/LogbookNettyServerCustomizer.java +++ b/test-suite/src/test/java/io/micronaut/docs/netty/LogbookNettyServerCustomizer.java @@ -1,16 +1,16 @@ package io.micronaut.docs.netty; // tag::imports[] + import io.micronaut.context.annotation.Requires; import io.micronaut.context.event.BeanCreatedEvent; import io.micronaut.context.event.BeanCreatedEventListener; import io.micronaut.http.netty.channel.ChannelPipelineCustomizer; import io.micronaut.http.server.netty.NettyServerCustomizer; import io.netty.channel.Channel; +import jakarta.inject.Singleton; import org.zalando.logbook.Logbook; import org.zalando.logbook.netty.LogbookServerHandler; - -import jakarta.inject.Singleton; // end::imports[] // tag::class[] @@ -48,7 +48,7 @@ public NettyServerCustomizer specializeForChannel(Channel channel, ChannelRole r @Override public void onStreamPipelineBuilt() { channel.pipeline().addBefore( // <5> - ChannelPipelineCustomizer.HANDLER_HTTP_STREAM, + ChannelPipelineCustomizer.HANDLER_MICRONAUT_INBOUND, "logbook", new LogbookServerHandler(logbook) ); From 0536d110e9e4076dd5ff0657df049df4fea13d00 Mon Sep 17 00:00:00 2001 From: Tim Yates Date: Thu, 20 Apr 2023 16:21:08 +0100 Subject: [PATCH 722/743] Add a TCK test for String based Exception handler (#9132) This fails in 4.0.0 as the response is OK --- .../tck/tests/ErrorHandlerStringTest.java | 81 +++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/ErrorHandlerStringTest.java diff --git a/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/ErrorHandlerStringTest.java b/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/ErrorHandlerStringTest.java new file mode 100644 index 00000000000..10d43af3e11 --- /dev/null +++ b/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/ErrorHandlerStringTest.java @@ -0,0 +1,81 @@ +/* + * Copyright 2017-2023 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.http.server.tck.tests; + +import io.micronaut.context.annotation.Requires; +import io.micronaut.http.HttpHeaders; +import io.micronaut.http.HttpRequest; +import io.micronaut.http.HttpStatus; +import io.micronaut.http.MediaType; +import io.micronaut.http.annotation.Controller; +import io.micronaut.http.annotation.Get; +import io.micronaut.http.server.exceptions.ExceptionHandler; +import io.micronaut.http.server.tck.AssertionUtils; +import jakarta.inject.Singleton; +import org.junit.jupiter.api.Test; + +import java.io.IOException; + +import static io.micronaut.http.server.tck.TestScenario.asserts; + +@SuppressWarnings({ + "java:S5960", // We're allowed assertions, as these are used in tests only + "checkstyle:MissingJavadocType", + "checkstyle:DesignForExtension" +}) +public class ErrorHandlerStringTest { + public static final String SPEC_NAME = "ErrorHandlerStringTest"; + + @Test + void testErrorHandlerWithStringReturn() throws IOException { + asserts(SPEC_NAME, + HttpRequest.GET("/exception/my").header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON), + (server, request) -> AssertionUtils.assertThrows( + server, + request, + HttpStatus.INTERNAL_SERVER_ERROR, + "hello", + null + ) + ); + } + + @Requires(property = "spec.name", value = SPEC_NAME) + @Controller("/exception") + static class ExceptionController { + + @Get("/my") + void throwsMy() { + throw new MyException("bad"); + } + } + + static class MyException extends RuntimeException { + public MyException(String badThings) { + super(badThings); + } + } + + @Requires(property = "spec.name", value = SPEC_NAME) + @Singleton + static class MyExceptionHandler implements ExceptionHandler { + + @Override + public String handle(HttpRequest request, MyException exception) { + return "hello"; + } + } +} From b176fa6b23d99cb98260b66cfda3f8db73eeb75e Mon Sep 17 00:00:00 2001 From: Tim Yates Date: Thu, 20 Apr 2023 16:39:12 +0100 Subject: [PATCH 723/743] Fix ExceptionHandler TCK test --- .../http/server/tck/tests/ErrorHandlerStringTest.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/ErrorHandlerStringTest.java b/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/ErrorHandlerStringTest.java index 6447fe6e2c4..19925fc2168 100644 --- a/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/ErrorHandlerStringTest.java +++ b/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/ErrorHandlerStringTest.java @@ -28,6 +28,7 @@ import org.junit.jupiter.api.Test; import java.io.IOException; +import java.util.Map; import static io.micronaut.http.tck.TestScenario.asserts; @@ -43,12 +44,12 @@ public class ErrorHandlerStringTest { void testErrorHandlerWithStringReturn() throws IOException { asserts(SPEC_NAME, HttpRequest.GET("/exception/my").header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON), - (server, request) -> AssertionUtils.assertThrows( + (server, request) -> AssertionUtils.assertDoesNotThrow( server, request, - HttpStatus.INTERNAL_SERVER_ERROR, + HttpStatus.OK, "hello", - null + Map.of(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON) ) ); } From 97e30ba37f612dc159f31fe513dad2e582aa0be8 Mon Sep 17 00:00:00 2001 From: Tim Yates Date: Thu, 20 Apr 2023 16:43:56 +0100 Subject: [PATCH 724/743] Add breaking change --- src/main/docs/guide/appendix/breaks.adoc | 32 ++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/src/main/docs/guide/appendix/breaks.adoc b/src/main/docs/guide/appendix/breaks.adoc index 92cf24c8ec2..57baf4529ff 100644 --- a/src/main/docs/guide/appendix/breaks.adoc +++ b/src/main/docs/guide/appendix/breaks.adoc @@ -128,3 +128,35 @@ Interceptors with multiple interceptor bindings annotations now require the same New type converters can be added to api:core.convert.MutableConversionService[] retrieved from the bean context or by declaring a bean of type api:core.convert.TypeConverter[]. To register a type converter into `ConversionService.SHARED`, the registration needs to be done via the service loader. + +==== `ExceptionHandler` with POJO response type no longer results in an exception + +Previously if you had an ExceptionHandler such as: + +[source,java] +---- +@Singleton +public class MyExceptionHandler implements ExceptionHandler { + + @Override + public String handle(HttpRequest request, MyException exception) { + return "caught!"; + } +} +---- + +This would result in an internal server error response with `caught!` as the body. +This now returns an OK response. +If you want to return a POJO response as an error, you should use the `HttpResponse` type: + +[source,java] +---- +@Singleton +public class MyExceptionHandler implements ExceptionHandler> { + + @Override + public HttpResponse handle(HttpRequest request, MyException exception) { + return HttpResponse.badRequest("caught!"); + } +} +---- From 4fb1e45c67d44e16baddbd8d4b5a3c92410c1c4d Mon Sep 17 00:00:00 2001 From: Tim Yates Date: Thu, 20 Apr 2023 16:54:35 +0100 Subject: [PATCH 725/743] Better wording --- src/main/docs/guide/appendix/breaks.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/docs/guide/appendix/breaks.adoc b/src/main/docs/guide/appendix/breaks.adoc index 57baf4529ff..c79a88ae2f4 100644 --- a/src/main/docs/guide/appendix/breaks.adoc +++ b/src/main/docs/guide/appendix/breaks.adoc @@ -129,7 +129,7 @@ Interceptors with multiple interceptor bindings annotations now require the same New type converters can be added to api:core.convert.MutableConversionService[] retrieved from the bean context or by declaring a bean of type api:core.convert.TypeConverter[]. To register a type converter into `ConversionService.SHARED`, the registration needs to be done via the service loader. -==== `ExceptionHandler` with POJO response type no longer results in an exception +==== `ExceptionHandler` with POJO response type no longer results in an error response Previously if you had an ExceptionHandler such as: From ad72f7e5e0004c4f3537812fb56384819488441c Mon Sep 17 00:00:00 2001 From: Tim Yates Date: Thu, 20 Apr 2023 18:12:27 +0100 Subject: [PATCH 726/743] Add in PR #9105 This got dropped in the merge by mistake... --- .../visitor/BeanIntrospectionWriter.java | 20 +++++++++---------- .../IntrospectedTypeElementVisitor.java | 4 ++++ 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/core-processor/src/main/java/io/micronaut/inject/beans/visitor/BeanIntrospectionWriter.java b/core-processor/src/main/java/io/micronaut/inject/beans/visitor/BeanIntrospectionWriter.java index 3103ca2e73d..5308275e680 100644 --- a/core-processor/src/main/java/io/micronaut/inject/beans/visitor/BeanIntrospectionWriter.java +++ b/core-processor/src/main/java/io/micronaut/inject/beans/visitor/BeanIntrospectionWriter.java @@ -128,13 +128,13 @@ final class BeanIntrospectionWriter extends AbstractAnnotationMetadataWriter { * @param beanAnnotationMetadata The bean annotation metadata * @param visitorContext The visitor context */ - BeanIntrospectionWriter(ClassElement classElement, AnnotationMetadata beanAnnotationMetadata, + BeanIntrospectionWriter(String targetPackage, ClassElement classElement, AnnotationMetadata beanAnnotationMetadata, VisitorContext visitorContext) { - super(computeReferenceName(classElement.getName()), classElement, beanAnnotationMetadata, true, visitorContext); + super(computeReferenceName(targetPackage, classElement.getName()), classElement, beanAnnotationMetadata, true, visitorContext); final String name = classElement.getName(); this.classElement = classElement; this.referenceWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS); - this.introspectionName = computeIntrospectionName(name); + this.introspectionName = computeShortIntrospectionName(targetPackage, name); this.introspectionType = getTypeReferenceForName(introspectionName); this.beanType = getTypeReferenceForName(name); this.dispatchWriter = new DispatchWriter(introspectionType, Type.getType(AbstractInitializableBeanIntrospection.class)); @@ -151,17 +151,18 @@ final class BeanIntrospectionWriter extends AbstractAnnotationMetadataWriter { * @param visitorContext The visitor context */ BeanIntrospectionWriter( + String targetPackage, String generatingType, int index, ClassElement originatingElement, ClassElement classElement, AnnotationMetadata beanAnnotationMetadata, VisitorContext visitorContext) { - super(computeReferenceName(generatingType) + index, originatingElement, beanAnnotationMetadata, true, visitorContext); + super(computeReferenceName(targetPackage, generatingType) + index, originatingElement, beanAnnotationMetadata, true, visitorContext); final String className = classElement.getName(); this.classElement = classElement; this.referenceWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS); - this.introspectionName = computeIntrospectionName(generatingType, className); + this.introspectionName = computeIntrospectionName(targetPackage, className); this.introspectionType = getTypeReferenceForName(introspectionName); this.beanType = getTypeReferenceForName(className); this.dispatchWriter = new DispatchWriter(introspectionType); @@ -949,22 +950,19 @@ private void pushAnnotationMetadata(ClassWriter classWriter, GeneratorAdapter st } @NonNull - private static String computeReferenceName(String className) { - String packageName = NameUtils.getPackageName(className); + private static String computeReferenceName(String packageName, String className) { final String shortName = NameUtils.getSimpleName(className); return packageName + ".$" + shortName + REFERENCE_SUFFIX; } @NonNull - private static String computeIntrospectionName(String className) { - String packageName = NameUtils.getPackageName(className); + private static String computeShortIntrospectionName(String packageName, String className) { final String shortName = NameUtils.getSimpleName(className); return packageName + ".$" + shortName + INTROSPECTION_SUFFIX; } @NonNull - private static String computeIntrospectionName(String generatingName, String className) { - final String packageName = NameUtils.getPackageName(generatingName); + private static String computeIntrospectionName(String packageName, String className) { return packageName + ".$" + className.replace('.', '_') + INTROSPECTION_SUFFIX; } diff --git a/core-processor/src/main/java/io/micronaut/inject/beans/visitor/IntrospectedTypeElementVisitor.java b/core-processor/src/main/java/io/micronaut/inject/beans/visitor/IntrospectedTypeElementVisitor.java index 9377fe615e0..9fa395f6a86 100644 --- a/core-processor/src/main/java/io/micronaut/inject/beans/visitor/IntrospectedTypeElementVisitor.java +++ b/core-processor/src/main/java/io/micronaut/inject/beans/visitor/IntrospectedTypeElementVisitor.java @@ -93,6 +93,7 @@ private void processIntrospected(ClassElement element, VisitorContext context, A final boolean metadata = introspected.booleanValue("annotationMetadata").orElse(true); final Set includedAnnotations = CollectionUtils.setOf(introspected.stringValues("includedAnnotations")); final Set> indexedAnnotations = CollectionUtils.setOf(introspected.get("indexed", AnnotationValue[].class, new AnnotationValue[0])); + final String targetPackage = introspected.stringValue("targetPackage").orElse(element.getPackageName()); if (!classes.isEmpty()) { AtomicInteger index = new AtomicInteger(0); @@ -101,6 +102,7 @@ private void processIntrospected(ClassElement element, VisitorContext context, A return; } final BeanIntrospectionWriter writer = new BeanIntrospectionWriter( + targetPackage, element.getName(), index.getAndIncrement(), element, @@ -129,6 +131,7 @@ private void processIntrospected(ClassElement element, VisitorContext context, A continue; } final BeanIntrospectionWriter writer = new BeanIntrospectionWriter( + targetPackage, element.getName(), j++, element, @@ -147,6 +150,7 @@ private void processIntrospected(ClassElement element, VisitorContext context, A } } else { final BeanIntrospectionWriter writer = new BeanIntrospectionWriter( + targetPackage, element, metadata ? element.getAnnotationMetadata() : null, context From 930b4bd0fdb9066b3919b9b9d42f346a0c6f1604 Mon Sep 17 00:00:00 2001 From: Denis Stepanov Date: Thu, 20 Apr 2023 15:51:19 -0600 Subject: [PATCH 727/743] Add interface type arguments test (#9134) --- .../inject/visitor/ClassElementSpec.groovy | 41 +++++++++++++++++++ .../visitors/ClassElementSpec.groovy | 41 +++++++++++++++++++ .../inject/ast/ClassElementSpec.groovy | 31 ++++++++++++++ 3 files changed, 113 insertions(+) diff --git a/inject-groovy/src/test/groovy/io/micronaut/inject/visitor/ClassElementSpec.groovy b/inject-groovy/src/test/groovy/io/micronaut/inject/visitor/ClassElementSpec.groovy index a6a6093e656..5837400a3cb 100644 --- a/inject-groovy/src/test/groovy/io/micronaut/inject/visitor/ClassElementSpec.groovy +++ b/inject-groovy/src/test/groovy/io/micronaut/inject/visitor/ClassElementSpec.groovy @@ -33,6 +33,7 @@ import io.micronaut.inject.ast.PrimitiveElement import io.micronaut.inject.ast.PropertyElement import io.micronaut.inject.ast.TypedElement import io.micronaut.inject.ast.WildcardElement +import jakarta.validation.Valid import spock.lang.Issue import spock.lang.PendingFeature import spock.lang.Unroll @@ -2286,6 +2287,46 @@ class MyRepo implements Repo { interfaces[0].simpleName == "Repo" } + void "test interface type annotations"() { + ClassElement ce = buildClassElement('test.MyRepo', ''' +package test; +import jakarta.validation.Valid; +import java.util.List; + +interface MyRepo extends Repo<@Valid MyBean, Long> { +} + +interface Repo extends GenericRepository { + + void save(E entity); + +} + +interface GenericRepository { +} + + +class MyBean { + private String name; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } +} + +''') + + when: + def method = ce.findMethod("save").get() + def type = method.parameters[0].getGenericType() + then: + type.hasAnnotation(Valid) + } + void validateBookArgument(ClassElement classElement) { // The class element should have all the annotations present assert classElement.hasAnnotation(TypeUseRuntimeAnn.class) diff --git a/inject-java/src/test/groovy/io/micronaut/visitors/ClassElementSpec.groovy b/inject-java/src/test/groovy/io/micronaut/visitors/ClassElementSpec.groovy index 79dfdcddb29..6b61d1b0ce0 100644 --- a/inject-java/src/test/groovy/io/micronaut/visitors/ClassElementSpec.groovy +++ b/inject-java/src/test/groovy/io/micronaut/visitors/ClassElementSpec.groovy @@ -36,6 +36,7 @@ import io.micronaut.inject.ast.PrimitiveElement import io.micronaut.inject.ast.PropertyElement import io.micronaut.inject.ast.WildcardElement import jakarta.inject.Singleton +import jakarta.validation.Valid import spock.lang.IgnoreIf import spock.lang.Issue import spock.lang.Requires @@ -2896,6 +2897,46 @@ class MyBean { genRepo.get("E").getFields().size() == 1 } + void "test interface type annotations"() { + ClassElement ce = buildClassElement(''' +package test; +import jakarta.validation.Valid; +import java.util.List; + +interface MyRepo extends Repo<@Valid MyBean, Long> { +} + +interface Repo extends GenericRepository { + + void save(E entity); + +} + +interface GenericRepository { +} + + +class MyBean { + private String name; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } +} + +''') + + when: + def method = ce.findMethod("save").get() + def type = method.parameters[0].getGenericType() + then: + type.hasAnnotation(Valid) + } + private void assertListGenericArgument(ClassElement type, Closure cl) { def arg1 = type.getAllTypeArguments().get(List.class.name).get("E") def arg2 = type.getAllTypeArguments().get(Collection.class.name).get("E") diff --git a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/inject/ast/ClassElementSpec.groovy b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/inject/ast/ClassElementSpec.groovy index cab2aa738df..3926c30d2b4 100644 --- a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/inject/ast/ClassElementSpec.groovy +++ b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/inject/ast/ClassElementSpec.groovy @@ -16,6 +16,7 @@ import io.micronaut.inject.ast.MethodElement import io.micronaut.inject.ast.PropertyElement import io.micronaut.inject.ast.WildcardElement import io.micronaut.kotlin.processing.visitor.KotlinClassElement +import jakarta.validation.Valid import spock.lang.PendingFeature class ClassElementSpec extends AbstractKotlinCompilerSpec { @@ -1921,6 +1922,36 @@ data class Cart( isAssignable } + void "test interface type annotations"() { + ClassElement ce = buildClassElement('test.MyRepo', ''' +package test +import jakarta.validation.Valid +import java.util.List + +interface MyRepo : Repo<@Valid MyBean, Long> { +} + +interface Repo : GenericRepository { + + fun save(entity: E) + +} + +interface GenericRepository + + +class MyBean { +} + +''') + + when: + def method = ce.findMethod("save").get() + def type = method.parameters[0].getGenericType() + then: + type.hasAnnotation(Valid) + } + private void assertListGenericArgument(ClassElement type, Closure cl) { def arg1 = type.getAllTypeArguments().get(List.class.name).get("E") def arg2 = type.getAllTypeArguments().get(Collection.class.name).get("E") From 16c5c03732dd562f5564042d508e95ecf0a0180d Mon Sep 17 00:00:00 2001 From: micronaut-build <65172877+micronaut-build@users.noreply.github.com> Date: Fri, 21 Apr 2023 14:21:28 +0200 Subject: [PATCH 728/743] Bump micronaut-test to 4.0.0-M2 (#9136) --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 74c47faebeb..72ed6e22c22 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -39,7 +39,7 @@ micronaut-aws = "3.9.2" micronaut-groovy = "4.0.0-M1" micronaut-session = "4.0.0-M1" micronaut-sql = "5.0.0-M3" -micronaut-test = "4.0.0-M1" +micronaut-test = "4.0.0-M2" micronaut-serde = "2.0.0-M1" micronaut-tracing = "5.0.0-SNAPSHOT" micronaut-validation = "4.0.0-M3" From 1b044611fd746ddbf63dd6905c515eb6062561db Mon Sep 17 00:00:00 2001 From: Denis Stepanov Date: Fri, 21 Apr 2023 08:58:01 -0500 Subject: [PATCH 729/743] Correct persisting parameter's type annotations (#9139) --- .../writer/AbstractClassFileWriter.java | 2 +- .../aop/introduction/DataCrudRepo.java | 9 +++++++ .../InterfaceIntroductionAdviceSpec.groovy | 26 ++++++++++++++++--- 3 files changed, 33 insertions(+), 4 deletions(-) create mode 100644 inject-java/src/test/groovy/io/micronaut/aop/introduction/DataCrudRepo.java diff --git a/core-processor/src/main/java/io/micronaut/inject/writer/AbstractClassFileWriter.java b/core-processor/src/main/java/io/micronaut/inject/writer/AbstractClassFileWriter.java index 2c71e9cbe55..56b72c684ea 100644 --- a/core-processor/src/main/java/io/micronaut/inject/writer/AbstractClassFileWriter.java +++ b/core-processor/src/main/java/io/micronaut/inject/writer/AbstractClassFileWriter.java @@ -560,7 +560,7 @@ protected static void pushBuildArgumentsForMethod( String argumentName = entry.getName(); AnnotationMetadata annotationMetadata = new AnnotationMetadataHierarchy( entry.getAnnotationMetadata(), - entry.getType().getTypeAnnotationMetadata() + entry.getGenericType().getTypeAnnotationMetadata() ).merge(); Map typeArguments = classElement.getTypeArguments(); pushCreateArgument( diff --git a/inject-java/src/test/groovy/io/micronaut/aop/introduction/DataCrudRepo.java b/inject-java/src/test/groovy/io/micronaut/aop/introduction/DataCrudRepo.java new file mode 100644 index 00000000000..86f8107863d --- /dev/null +++ b/inject-java/src/test/groovy/io/micronaut/aop/introduction/DataCrudRepo.java @@ -0,0 +1,9 @@ +package io.micronaut.aop.introduction; + +public interface DataCrudRepo { + + E findById(I id); + + void save(E entity); + +} diff --git a/inject-java/src/test/groovy/io/micronaut/aop/introduction/InterfaceIntroductionAdviceSpec.groovy b/inject-java/src/test/groovy/io/micronaut/aop/introduction/InterfaceIntroductionAdviceSpec.groovy index 7e3af129bbd..8fccac6635e 100644 --- a/inject-java/src/test/groovy/io/micronaut/aop/introduction/InterfaceIntroductionAdviceSpec.groovy +++ b/inject-java/src/test/groovy/io/micronaut/aop/introduction/InterfaceIntroductionAdviceSpec.groovy @@ -19,10 +19,9 @@ import io.micronaut.annotation.processing.test.AbstractTypeElementSpec import io.micronaut.aop.Intercepted import io.micronaut.context.BeanContext import io.micronaut.context.DefaultBeanContext -import io.micronaut.inject.BeanDefinition -import spock.lang.Specification +import jakarta.validation.Valid +import jakarta.validation.constraints.Min import spock.lang.Unroll - /** * @author Graeme Rocher * @since 1.0 @@ -75,4 +74,25 @@ interface Test extends ParentInterface> { expect: !definition.getTypeArguments(ParentInterface).isEmpty() } + + void "test type argument annotation propagation"() { + def definition = buildBeanDefinition("test.Test\$Intercepted", """ +package test; + +import java.util.List; +import io.micronaut.aop.introduction.DataCrudRepo; +import io.micronaut.aop.introduction.Stub; +import jakarta.validation.Valid; +import jakarta.validation.constraints.Min; + +@Stub +interface Test extends DataCrudRepo<@Valid String, @Min(5) Integer> { +} +""") + + expect: + definition.getRequiredMethod("save", String).getArguments()[0].getAnnotationMetadata().hasAnnotation(Valid) + definition.getRequiredMethod("findById", Integer).getArguments()[0].getAnnotationMetadata().hasAnnotation(Min) + definition.getRequiredMethod("findById", Integer).getReturnType().getAnnotationMetadata().hasAnnotation(Valid) + } } From 450f256753ea5c13e43eefdf0d26c3e37629d356 Mon Sep 17 00:00:00 2001 From: Jonas Konrad Date: Fri, 21 Apr 2023 16:41:24 +0200 Subject: [PATCH 730/743] io_uring support (#9090) --- gradle/libs.versions.toml | 2 + http-netty/build.gradle | 1 + .../channel/IoUringAvailabilityCondition.java | 42 ++++++ .../channel/IoUringEventLoopGroupFactory.java | 133 ++++++++++++++++++ .../guide/httpServer/serverConfiguration.adoc | 8 ++ 5 files changed, 186 insertions(+) create mode 100644 http-netty/src/main/java/io/micronaut/http/netty/channel/IoUringAvailabilityCondition.java create mode 100644 http-netty/src/main/java/io/micronaut/http/netty/channel/IoUringEventLoopGroupFactory.java diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 72ed6e22c22..1246f4e203e 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -67,6 +67,7 @@ managed-kotlin-coroutines = "1.6.4" managed-maven-native-plugin = "0.9.13" managed-methvin-directory-watcher = "0.16.1" managed-netty = "4.1.91.Final" +managed-netty-iouring = "0.0.20.Final" managed-netty-http3 = "0.0.16.Final" managed-reactive-streams = "1.0.4" # This should be kept aligned with https://github.com/micronaut-projects/micronaut-reactor/blob/master/gradle.properties from the BOM @@ -138,6 +139,7 @@ managed-netty-handler = { module = "io.netty:netty-handler", version.ref = "mana managed-netty-handler-proxy = { module = "io.netty:netty-handler-proxy", version.ref = "managed-netty" } managed-netty-transport-native-epoll = { module = "io.netty:netty-transport-native-epoll", version.ref = "managed-netty" } managed-netty-transport-native-kqueue = { module = "io.netty:netty-transport-native-kqueue", version.ref = "managed-netty" } +managed-netty-transport-native-iouring = { module = "io.netty.incubator:netty-incubator-transport-native-io_uring", version.ref = "managed-netty-iouring" } managed-netty-transport-native-unix-common = { module = "io.netty:netty-transport-native-unix-common", version.ref = "managed-netty" } managed-reactive-streams = { module = "org.reactivestreams:reactive-streams", version.ref = "managed-reactive-streams" } diff --git a/http-netty/build.gradle b/http-netty/build.gradle index 3732901ad0d..9a6cd4dfb20 100644 --- a/http-netty/build.gradle +++ b/http-netty/build.gradle @@ -8,6 +8,7 @@ dependencies { compileOnly libs.graal compileOnly libs.managed.netty.transport.native.epoll compileOnly libs.managed.netty.transport.native.kqueue + compileOnly libs.managed.netty.transport.native.iouring compileOnly project(":websocket") api project(":http") diff --git a/http-netty/src/main/java/io/micronaut/http/netty/channel/IoUringAvailabilityCondition.java b/http-netty/src/main/java/io/micronaut/http/netty/channel/IoUringAvailabilityCondition.java new file mode 100644 index 00000000000..8a56f42b8b7 --- /dev/null +++ b/http-netty/src/main/java/io/micronaut/http/netty/channel/IoUringAvailabilityCondition.java @@ -0,0 +1,42 @@ +/* + * Copyright 2017-2023 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.http.netty.channel; + +import io.micronaut.context.condition.Condition; +import io.micronaut.context.condition.ConditionContext; +import io.micronaut.core.annotation.Internal; +import io.netty.incubator.channel.uring.IOUring; + +/** + * Checks if io-uring is available. + * + * @author Jonas Konrad + * @since 4.0.0 + */ +@Internal +public class IoUringAvailabilityCondition implements Condition { + + /** + * Checks if netty's io-uring native transport is available. + * + * @param context The ConditionContext. + * @return true if the io-uring native transport is available. + */ + @Override + public boolean matches(ConditionContext context) { + return IOUring.isAvailable(); + } +} diff --git a/http-netty/src/main/java/io/micronaut/http/netty/channel/IoUringEventLoopGroupFactory.java b/http-netty/src/main/java/io/micronaut/http/netty/channel/IoUringEventLoopGroupFactory.java new file mode 100644 index 00000000000..e82e8555744 --- /dev/null +++ b/http-netty/src/main/java/io/micronaut/http/netty/channel/IoUringEventLoopGroupFactory.java @@ -0,0 +1,133 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.http.netty.channel; + +import io.micronaut.context.annotation.BootstrapContextCompatible; +import io.micronaut.context.annotation.Requires; +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.NonNull; +import io.micronaut.core.annotation.Nullable; +import io.netty.channel.Channel; +import io.netty.channel.EventLoopGroup; +import io.netty.channel.socket.ServerSocketChannel; +import io.netty.channel.socket.SocketChannel; +import io.netty.incubator.channel.uring.IOUring; +import io.netty.incubator.channel.uring.IOUringDatagramChannel; +import io.netty.incubator.channel.uring.IOUringEventLoopGroup; +import io.netty.incubator.channel.uring.IOUringServerSocketChannel; +import io.netty.incubator.channel.uring.IOUringSocketChannel; +import jakarta.inject.Named; +import jakarta.inject.Singleton; + +import java.util.concurrent.Executor; +import java.util.concurrent.ThreadFactory; + +/** + * Factory for IOUringEventLoopGroup. + * + * @author Jonas Konrad + * @since 4.0.0 + */ +@Singleton +@Requires(classes = IOUring.class, condition = IoUringAvailabilityCondition.class) +@Internal +@Named(EventLoopGroupFactory.NATIVE) +@BootstrapContextCompatible +public class IoUringEventLoopGroupFactory implements EventLoopGroupFactory { + + /** + * Creates an IOUringEventLoopGroup. + * + * @param threads The number of threads to use. + * @param threadFactory The thread factory. + * @param ioRatio The io ratio. + * @return An IOUringEventLoopGroup. + */ + @Override + public EventLoopGroup createEventLoopGroup(int threads, ThreadFactory threadFactory, @Nullable Integer ioRatio) { + return new IOUringEventLoopGroup(threads, threadFactory); + } + + /** + * Creates an IOUringEventLoopGroup. + * + * @param threads The number of threads to use. + * @param executor An Executor. + * @param ioRatio The io ratio. + * @return An IOUringEventLoopGroup. + */ + @Override + public EventLoopGroup createEventLoopGroup(int threads, Executor executor, @Nullable Integer ioRatio) { + return new IOUringEventLoopGroup(threads, executor); + } + + /** + * Returns the server channel class. + * + * @return IOUringServerSocketChannel. + */ + @Override + public Class serverSocketChannelClass() { + return IOUringServerSocketChannel.class; + } + + @NonNull + @Override + public IOUringServerSocketChannel serverSocketChannelInstance(@Nullable EventLoopGroupConfiguration configuration) { + return new IOUringServerSocketChannel(); + } + + @NonNull + @Override + public Class clientSocketChannelClass(@Nullable EventLoopGroupConfiguration configuration) { + return IOUringSocketChannel.class; + } + + @Override + public SocketChannel clientSocketChannelInstance(EventLoopGroupConfiguration configuration) { + return new IOUringSocketChannel(); + } + + @Override + public boolean isNative() { + return true; + } + + @Override + public Class channelClass(NettyChannelType type) throws UnsupportedOperationException { + return switch (type) { + case SERVER_SOCKET -> IOUringServerSocketChannel.class; + case CLIENT_SOCKET -> IOUringSocketChannel.class; + case DATAGRAM_SOCKET -> IOUringDatagramChannel.class; + default -> throw new UnsupportedOperationException("Channel type not supported"); + }; + } + + @Override + public Class channelClass(NettyChannelType type, @Nullable EventLoopGroupConfiguration configuration) { + return channelClass(type); + } + + @Override + public Channel channelInstance(NettyChannelType type, @Nullable EventLoopGroupConfiguration configuration) { + return switch (type) { + case SERVER_SOCKET -> new IOUringServerSocketChannel(); + case CLIENT_SOCKET -> new IOUringSocketChannel(); + case DATAGRAM_SOCKET -> new IOUringDatagramChannel(); + default -> throw new UnsupportedOperationException("Channel type not supported"); + }; + } +} diff --git a/src/main/docs/guide/httpServer/serverConfiguration.adoc b/src/main/docs/guide/httpServer/serverConfiguration.adoc index a2118ddbf1b..3e7c176f3c9 100644 --- a/src/main/docs/guide/httpServer/serverConfiguration.adoc +++ b/src/main/docs/guide/httpServer/serverConfiguration.adoc @@ -57,4 +57,12 @@ micronaut: prefer-native-transport: true ---- +On Linux, Netty also offers an io_uring-based transport in incubator status. On x86_64: + +dependency:netty-incubator-transport-native-io_uring[groupId="io.netty.incubator",scope="runtimeOnly",classifier="linux-x86_64"] + +On ARM64: + +dependency:netty-incubator-transport-native-io_uring[groupId="io.netty.incubator",scope="runtimeOnly",classifier="linux-aarch_64"] + NOTE: Netty enables simplistic sampling resource leak detection which reports there is a leak or not, at the cost of small overhead. You can enable it by setting property `netty.resource-leak-detector-level` to one of: `DISABLED` (default), `SIMPLE`, `PARANOID` or `ADVANCED`. From 466b1a35b8a881209112a407e90e39be685ec95a Mon Sep 17 00:00:00 2001 From: Jonas Konrad Date: Fri, 21 Apr 2023 18:06:08 +0200 Subject: [PATCH 731/743] Use StringIntMap for propertyIndexOf (#9137) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Use StringIntMap for propertyIndexOf This patch replaces the propertyIndexOf method generated for each introspection with an optimized String->int map. For cases where propertyIndexOf may be called for many different types, this avoids a megamorphic call site (benchmark about 5x as fast). On the other hand, for cases where propertyIndexOf is only called with one or two different introspections at the same call site, this is slightly slower (benchmark about 0.65x as fast). imo this tradeoff is acceptable, because the speedup for megamorphic case far exceeds the slowdown for the monomorphic case. Monomorphic call sites should also be less "hot" in practical code, so performance of propertyIndexOf matters less. Finally, the absolute performance difference is quite small for the monomorphic case, in the benchmark it's 70ns so roughly 1.5ns per item, while for the megamorphic case it's roughly 30ns per item. Benchmark results, before this change: Benchmark (itemCount) (typeCount) Mode Cnt Score Error Units PropertyIndexBenchmark.test 50 1 avgt 5 115.205 ± 1.016 ns/op PropertyIndexBenchmark.test 50 2 avgt 5 118.745 ± 0.106 ns/op PropertyIndexBenchmark.test 50 3 avgt 5 832.091 ± 8.271 ns/op After this change: Benchmark (itemCount) (typeCount) Mode Cnt Score Error Units PropertyIndexBenchmark.test 50 1 avgt 5 183.315 ± 0.435 ns/op PropertyIndexBenchmark.test 50 2 avgt 5 162.453 ± 0.881 ns/op PropertyIndexBenchmark.test 50 3 avgt 5 162.690 ± 1.004 ns/op * Use array instead of list --------- Co-authored-by: Denis Stepanov --- .../core/beans/PropertyIndexBenchmark.java | 91 +++++++++++++++++++ .../visitor/BeanIntrospectionWriter.java | 45 --------- .../io/micronaut/core/util/StringIntMap.java | 79 ++++++++++++++++ .../core/util/StringIntMapSpec.groovy | 39 ++++++++ ...bstractInitializableBeanIntrospection.java | 46 ++++++---- 5 files changed, 239 insertions(+), 61 deletions(-) create mode 100644 benchmarks/src/jmh/java/io/micronaut/core/beans/PropertyIndexBenchmark.java create mode 100644 core/src/main/java/io/micronaut/core/util/StringIntMap.java create mode 100644 core/src/test/groovy/io/micronaut/core/util/StringIntMapSpec.groovy diff --git a/benchmarks/src/jmh/java/io/micronaut/core/beans/PropertyIndexBenchmark.java b/benchmarks/src/jmh/java/io/micronaut/core/beans/PropertyIndexBenchmark.java new file mode 100644 index 00000000000..c3fee0a5be9 --- /dev/null +++ b/benchmarks/src/jmh/java/io/micronaut/core/beans/PropertyIndexBenchmark.java @@ -0,0 +1,91 @@ +package io.micronaut.core.beans; + +import io.micronaut.core.annotation.Introspected; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.Param; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.infra.Blackhole; +import org.openjdk.jmh.runner.Runner; +import org.openjdk.jmh.runner.RunnerException; +import org.openjdk.jmh.runner.options.Options; +import org.openjdk.jmh.runner.options.OptionsBuilder; + +import java.util.concurrent.ThreadLocalRandom; +import java.util.concurrent.TimeUnit; +import java.util.stream.IntStream; + +@State(Scope.Benchmark) +public class PropertyIndexBenchmark { + @Param({"1", "2", "3"}) + int typeCount = 3; + @Param({"50"}) + int itemCount = 100; + + String needle; + BeanIntrospection[] introspections; + + public static void main(String[] args) throws RunnerException { + PropertyIndexBenchmark propertyIndexBenchmark = new PropertyIndexBenchmark(); + propertyIndexBenchmark.setUp(); + // calm down shipilev, I'm only verifying the benchmark works. + propertyIndexBenchmark.test(new Blackhole("Today's password is swordfish. I understand instantiating Blackholes directly is dangerous.")); + + Options opt = new OptionsBuilder() + .include(PropertyIndexBenchmark.class.getName() + ".*") + .warmupIterations(5) + .measurementIterations(5) + .mode(Mode.AverageTime) + .timeUnit(TimeUnit.NANOSECONDS) + .forks(1) + .build(); + + new Runner(opt).run(); + } + + @Setup + public void setUp() { + introspections = IntStream.range(0, itemCount) + .mapToObj(i -> switch (ThreadLocalRandom.current().nextInt(typeCount)) { + case 0 -> BeanA.class; + case 1 -> BeanB.class; + case 2 -> BeanC.class; + default -> throw new AssertionError(); + }) + .map(BeanIntrospector.SHARED::getIntrospection) + .toArray(BeanIntrospection[]::new); + needle = "foo"; + } + + @Benchmark + public void test(Blackhole blackhole) { + String needle = this.needle; + for (BeanIntrospection introspection : introspections) { + blackhole.consume(introspection.propertyIndexOf(needle)); + } + } + + @Introspected + public record BeanA( + String foo, + String bar + ) { + } + + @Introspected + public record BeanB( + String baz, + int foo + ) { + } + + @Introspected + public record BeanC( + String fizz, + double buzz, + int foo + ) { + } +} diff --git a/core-processor/src/main/java/io/micronaut/inject/beans/visitor/BeanIntrospectionWriter.java b/core-processor/src/main/java/io/micronaut/inject/beans/visitor/BeanIntrospectionWriter.java index 5308275e680..709b4d51ea3 100644 --- a/core-processor/src/main/java/io/micronaut/inject/beans/visitor/BeanIntrospectionWriter.java +++ b/core-processor/src/main/java/io/micronaut/inject/beans/visitor/BeanIntrospectionWriter.java @@ -85,9 +85,6 @@ final class BeanIntrospectionWriter extends AbstractAnnotationMetadataWriter { private static final String FIELD_CONSTRUCTOR_ARGUMENTS = "$CONSTRUCTOR_ARGUMENTS"; private static final String FIELD_BEAN_PROPERTIES_REFERENCES = "$PROPERTIES_REFERENCES"; private static final String FIELD_BEAN_METHODS_REFERENCES = "$METHODS_REFERENCES"; - private static final Method PROPERTY_INDEX_OF = Method.getMethod( - ReflectionUtils.getRequiredInternalMethod(BeanIntrospection.class, "propertyIndexOf", String.class) - ); private static final Method FIND_PROPERTY_BY_INDEX_METHOD = Method.getMethod( ReflectionUtils.getRequiredInternalMethod(AbstractInitializableBeanIntrospection.class, "getPropertyByIndex", int.class) ); @@ -575,7 +572,6 @@ private void writeIntrospectionClass(ClassWriterOutputVisitor classWriterOutputV dispatchWriter.buildDispatchOneMethod(classWriter); dispatchWriter.buildDispatchMethod(classWriter); dispatchWriter.buildGetTargetMethodByIndex(classWriter); - buildPropertyIndexOfMethod(classWriter); buildFindIndexedProperty(classWriter); buildGetIndexedProperties(classWriter); @@ -598,47 +594,6 @@ private void writeIntrospectionClass(ClassWriterOutputVisitor classWriterOutputV } } - private void buildPropertyIndexOfMethod(ClassWriter classWriter) { - GeneratorAdapter findMethod = new GeneratorAdapter(classWriter.visitMethod( - ACC_PUBLIC | ACC_FINAL, - PROPERTY_INDEX_OF.getName(), - PROPERTY_INDEX_OF.getDescriptor(), - null, - null), - ACC_PUBLIC | ACC_FINAL, - PROPERTY_INDEX_OF.getName(), - PROPERTY_INDEX_OF.getDescriptor() - ); - new StringSwitchWriter() { - - @Override - protected Set getKeys() { - Set keys = new HashSet<>(); - for (BeanPropertyData prop : beanProperties) { - keys.add(prop.name); - } - return keys; - } - - @Override - protected void pushStringValue() { - findMethod.loadArg(0); - } - - @Override - protected void onMatch(String value, Label end) { - findMethod.loadThis(); - findMethod.push(getPropertyIndex(value)); - findMethod.returnValue(); - } - - }.write(findMethod); - findMethod.push(-1); - findMethod.returnValue(); - findMethod.visitMaxs(DEFAULT_MAX_STACK, 1); - findMethod.visitEnd(); - } - private void buildFindIndexedProperty(ClassWriter classWriter) { if (indexByAnnotationAndValue.isEmpty()) { return; diff --git a/core/src/main/java/io/micronaut/core/util/StringIntMap.java b/core/src/main/java/io/micronaut/core/util/StringIntMap.java new file mode 100644 index 00000000000..b23ed24d070 --- /dev/null +++ b/core/src/main/java/io/micronaut/core/util/StringIntMap.java @@ -0,0 +1,79 @@ +/* + * Copyright 2017-2023 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.core.util; + +import io.micronaut.core.annotation.Internal; + +/** + * Fixed-size String->int map optimized for very fast read operations. + * + * @author Jonas Konrad + * @since 4.0.0 + */ +@Internal +public final class StringIntMap { + private final int mask; + private final String[] keys; + private final int[] values; + + /** + * Create a new map. The given size must not be exceeded by {@link #put} operations, or + * there may be infinite loops. There is no sanity check for this for performance reasons! + * + * @param size The maximum size of the map + */ + public StringIntMap(int size) { + // min size: at least one slot, aim for 50% load factor + int tableSize = (size * 2) + 1; + // round to next power of two for efficient hash code masking + tableSize = Integer.highestOneBit(tableSize) * 2; + this.mask = tableSize - 1; + this.keys = new String[tableSize]; + this.values = new int[keys.length]; + } + + private int probe(String key) { + int n = keys.length; + int i = key.hashCode() & mask; + while (true) { + String candidate = keys[i]; + if (candidate == null) { + return ~i; + } else if (candidate.equals(key)) { + return i; + } else { + i++; + if (i == n) { + i = 0; + } + } + } + } + + public int get(String key, int def) { + int i = probe(key); + return i < 0 ? def : values[i]; + } + + public void put(String key, int value) { + int tableIndex = ~probe(key); + if (tableIndex < 0) { + throw new IllegalArgumentException("Duplicate key"); + } + keys[tableIndex] = key; + values[tableIndex] = value; + } +} diff --git a/core/src/test/groovy/io/micronaut/core/util/StringIntMapSpec.groovy b/core/src/test/groovy/io/micronaut/core/util/StringIntMapSpec.groovy new file mode 100644 index 00000000000..b59daa647ca --- /dev/null +++ b/core/src/test/groovy/io/micronaut/core/util/StringIntMapSpec.groovy @@ -0,0 +1,39 @@ +package io.micronaut.core.util + +import spock.lang.Specification + +class StringIntMapSpec extends Specification { + def simple() { + given: + def map = new StringIntMap(4) + + when: + map.put("foo", 1) + then: + map.get("foo", -1) == 1 + map.get("bar", -1) == -1 + + when: + map.put("bar", 2) + then: + map.get("foo", -1) == 1 + map.get("bar", -1) == 2 + map.get("fizz", -1) == -1 + + when: + map.put("fizz", 3) + then: + map.get("foo", -1) == 1 + map.get("bar", -1) == 2 + map.get("fizz", -1) == 3 + map.get("buzz", -1) == -1 + + when: + map.put("buzz", 4) + then: + map.get("foo", -1) == 1 + map.get("bar", -1) == 2 + map.get("fizz", -1) == 3 + map.get("buzz", -1) == 4 + } +} diff --git a/inject/src/main/java/io/micronaut/inject/beans/AbstractInitializableBeanIntrospection.java b/inject/src/main/java/io/micronaut/inject/beans/AbstractInitializableBeanIntrospection.java index fee1b2e0148..bd57b106959 100644 --- a/inject/src/main/java/io/micronaut/inject/beans/AbstractInitializableBeanIntrospection.java +++ b/inject/src/main/java/io/micronaut/inject/beans/AbstractInitializableBeanIntrospection.java @@ -31,6 +31,7 @@ import io.micronaut.core.type.Argument; import io.micronaut.core.type.ReturnType; import io.micronaut.core.util.ArgumentUtils; +import io.micronaut.core.util.StringIntMap; import io.micronaut.inject.ExecutableMethod; import io.micronaut.inject.annotation.EvaluatedAnnotationMetadata; @@ -62,8 +63,10 @@ public abstract class AbstractInitializableBeanIntrospection implements BeanI private final AnnotationMetadata annotationMetadata; private final AnnotationMetadata constructorAnnotationMetadata; private final Argument[] constructorArguments; - private final List> beanProperties; - private final List> beanMethods; + private final BeanProperty[] beanProperties; + private final List> beanPropertiesList; + private final List> beanMethodsList; + private final StringIntMap beanPropertyIndex; private BeanConstructor beanConstructor; @@ -82,18 +85,24 @@ protected AbstractInitializableBeanIntrospection(Class beanType, for (BeanPropertyRef beanPropertyRef : propertiesRefs) { beanProperties.add(new BeanPropertyImpl<>(beanPropertyRef)); } - this.beanProperties = Collections.unmodifiableList(beanProperties); + this.beanProperties = beanProperties.toArray(BeanProperty[]::new); + this.beanPropertiesList = Collections.unmodifiableList(beanProperties); } else { - this.beanProperties = Collections.emptyList(); + this.beanProperties = new BeanProperty[0]; + this.beanPropertiesList = Collections.emptyList(); + } + this.beanPropertyIndex = new StringIntMap(beanProperties.length); + for (int i = 0; i < beanProperties.length; i++) { + beanPropertyIndex.put(beanProperties[i].getName(), i); } if (methodsRefs != null) { List> beanMethods = new ArrayList<>(methodsRefs.length); for (BeanMethodRef beanMethodRef : methodsRefs) { beanMethods.add(new BeanMethodImpl<>(beanMethodRef)); } - this.beanMethods = Collections.unmodifiableList(beanMethods); + this.beanMethodsList = Collections.unmodifiableList(beanMethods); } else { - this.beanMethods = Collections.emptyList(); + this.beanMethodsList = Collections.emptyList(); } } @@ -117,7 +126,12 @@ protected AbstractInitializableBeanIntrospection(Class beanType, @Internal @UsedByGeneratedCode protected BeanProperty getPropertyByIndex(int index) { - return beanProperties.get(index); + return beanProperties[index]; + } + + @Override + public int propertyIndexOf(String name) { + return beanPropertyIndex.get(name, -1); } /** @@ -267,7 +281,7 @@ public B instantiate(boolean strictNullable, Object... arguments) throws Instant @Override public BeanConstructor getConstructor() { if (beanConstructor == null) { - beanConstructor = new BeanConstructor() { + beanConstructor = new BeanConstructor<>() { @Override public Class getDeclaringBeanType() { return beanType; @@ -308,7 +322,7 @@ public Optional> getIndexedProperty(@NonNull Class> getProperty(@NonNull String name) { ArgumentUtils.requireNonNull("name", name); int index = propertyIndexOf(name); - return index == -1 ? Optional.empty() : Optional.of(beanProperties.get(index)); + return index == -1 ? Optional.empty() : Optional.of(beanProperties[index]); } @Override @@ -319,7 +333,7 @@ public AnnotationMetadata getAnnotationMetadata() { @NonNull @Override public Collection> getBeanProperties() { - return beanProperties; + return beanPropertiesList; } @NonNull @@ -331,7 +345,7 @@ public Class getBeanType() { @NonNull @Override public Collection> getBeanMethods() { - return beanMethods; + return beanMethodsList; } @Override @@ -364,16 +378,16 @@ public String toString() { private static final class IndexedCollections extends AbstractCollection { private final int[] indexed; - private final List list; + private final T[] array; - private IndexedCollections(int[] indexed, List list) { + private IndexedCollections(int[] indexed, T[] array) { this.indexed = indexed; - this.list = list; + this.array = array; } @Override public Iterator iterator() { - return new Iterator() { + return new Iterator<>() { int i = -1; @@ -388,7 +402,7 @@ public T next() { throw new NoSuchElementException(); } int index = indexed[++i]; - return list.get(index); + return array[index]; } }; } From bdab80df63176799a2ea220b74ffffc6e02c3a80 Mon Sep 17 00:00:00 2001 From: Denis Stepanov Date: Sat, 22 Apr 2023 17:53:16 -0500 Subject: [PATCH 732/743] Correct persisting parameter's type annotations 2 (#9142) --- .../inject/writer/AbstractClassFileWriter.java | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/core-processor/src/main/java/io/micronaut/inject/writer/AbstractClassFileWriter.java b/core-processor/src/main/java/io/micronaut/inject/writer/AbstractClassFileWriter.java index 56b72c684ea..c10e3877d10 100644 --- a/core-processor/src/main/java/io/micronaut/inject/writer/AbstractClassFileWriter.java +++ b/core-processor/src/main/java/io/micronaut/inject/writer/AbstractClassFileWriter.java @@ -547,22 +547,23 @@ protected static void pushBuildArgumentsForMethod( // the array index position generatorAdapter.push(i); + ClassElement genericType = entry.getGenericType(); + MutableAnnotationMetadata.contributeDefaults( annotationMetadataWithDefaults, entry.getAnnotationMetadata() ); MutableAnnotationMetadata.contributeDefaults( annotationMetadataWithDefaults, - entry.getType().getTypeAnnotationMetadata() + genericType.getTypeAnnotationMetadata() ); - ClassElement classElement = entry.getGenericType(); String argumentName = entry.getName(); AnnotationMetadata annotationMetadata = new AnnotationMetadataHierarchy( entry.getAnnotationMetadata(), - entry.getGenericType().getTypeAnnotationMetadata() + genericType.getTypeAnnotationMetadata() ).merge(); - Map typeArguments = classElement.getTypeArguments(); + Map typeArguments = genericType.getTypeArguments(); pushCreateArgument( annotationMetadataWithDefaults, declaringElementName, @@ -570,7 +571,7 @@ protected static void pushBuildArgumentsForMethod( declaringClassWriter, generatorAdapter, argumentName, - classElement, + genericType, annotationMetadata, typeArguments, defaults, loadTypeMethods ); From 26e4c2e2014ba694abbec76db3893e64a735da95 Mon Sep 17 00:00:00 2001 From: Tim Yates Date: Mon, 24 Apr 2023 15:10:13 +0100 Subject: [PATCH 733/743] Re-enable PTS and Test Distribution for Kotlin (#9138) We had to disable Predictive Test Selection and Test Distribution for Kotest tests as Kotest was unsupported. As of micronaut-test-4.0.0-M2, we use a supported version of Kotest 5, so this pr reverses https://github.com/micronaut-projects/micronaut-core/pull/8612 To re-enable these Gradle features --- test-suite-kotlin/build.gradle | 9 --------- 1 file changed, 9 deletions(-) diff --git a/test-suite-kotlin/build.gradle b/test-suite-kotlin/build.gradle index 5bb63ffe9bf..e8803551c28 100644 --- a/test-suite-kotlin/build.gradle +++ b/test-suite-kotlin/build.gradle @@ -101,12 +101,3 @@ tasks.named("compileTestKotlin") { tasks.named("test") { useJUnitPlatform() } - -tasks.withType(Test).configureEach { - distribution { - enabled = false - } - predictiveSelection { - enabled = false - } -} From 1432dc527b75508889e83d7b1b82d7dba8b527bd Mon Sep 17 00:00:00 2001 From: Denis Stepanov Date: Tue, 25 Apr 2023 10:03:19 -0500 Subject: [PATCH 734/743] Define the property by the first setter in the same class (#9155) --- .../ast/utils/AstBeanPropertiesUtils.java | 11 ++-- .../ConfigurationBuilderSpec2.groovy | 53 +++++++++++++++++++ .../inject/configuration/HierarchyConfig.java | 51 ++++++++++++++++++ 3 files changed, 111 insertions(+), 4 deletions(-) create mode 100644 inject-java/src/test/groovy/io/micronaut/inject/configuration/ConfigurationBuilderSpec2.groovy create mode 100644 inject-java/src/test/groovy/io/micronaut/inject/configuration/HierarchyConfig.java diff --git a/core-processor/src/main/java/io/micronaut/inject/ast/utils/AstBeanPropertiesUtils.java b/core-processor/src/main/java/io/micronaut/inject/ast/utils/AstBeanPropertiesUtils.java index 54a49e05f4f..44511eb5345 100644 --- a/core-processor/src/main/java/io/micronaut/inject/ast/utils/AstBeanPropertiesUtils.java +++ b/core-processor/src/main/java/io/micronaut/inject/ast/utils/AstBeanPropertiesUtils.java @@ -262,15 +262,18 @@ private static void processSetter(ClassElement classElement, Map testProps = ctx.classLoader.loadClass("test.TestProps") + def testPropBean = ctx.getBean(testProps) + + then: + noExceptionThrown() + ctx.getProperty("test.props.name", String).get() == "Tim Yates" + testPropBean.builder.build().name == "Tim Yates" + + cleanup: + ctx.close() + } +} diff --git a/inject-java/src/test/groovy/io/micronaut/inject/configuration/HierarchyConfig.java b/inject-java/src/test/groovy/io/micronaut/inject/configuration/HierarchyConfig.java new file mode 100644 index 00000000000..e5a85b86dd2 --- /dev/null +++ b/inject-java/src/test/groovy/io/micronaut/inject/configuration/HierarchyConfig.java @@ -0,0 +1,51 @@ +package io.micronaut.inject.configuration; + +import io.micronaut.context.annotation.ConfigurationBuilder; +import io.micronaut.context.annotation.ConfigurationProperties; + +@ConfigurationProperties("test.props") +public class HierarchyConfig { + + @ConfigurationBuilder(prefixes = "with") + RealizedBuilder builder = new RealizedBuilder(); + + public static class RealizedBuilder extends Builder { + } + + public abstract static class Builder { + + private String name; + + public String getName() { + return name; + } + + public final T withName(String name) { + this.name = name; + return getSubclass(); + } + + public final T withName(NameHolder name) { + this.name = name.name; + return getSubclass(); + } + + public NameHolder build() { + return new NameHolder(name); + } + + @SuppressWarnings("unchecked") + protected final T getSubclass() { + return (T) this; + } + } + + public static class NameHolder { + + private final String name; + + public NameHolder(String name) { + this.name = name; + } + } +} From 8b54c0efe9483e6408b00390db011afe83b800b2 Mon Sep 17 00:00:00 2001 From: micronaut-build <65172877+micronaut-build@users.noreply.github.com> Date: Wed, 26 Apr 2023 08:52:20 +0200 Subject: [PATCH 735/743] Update common files (#9145) --- gradle/wrapper/gradle-wrapper.jar | Bin 61608 -> 62076 bytes gradle/wrapper/gradle-wrapper.properties | 2 +- gradlew | 7 ++++--- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index ccebba7710deaf9f98673a68957ea02138b60d0a..c1962a79e29d3e0ab67b14947c167a862655af9b 100644 GIT binary patch delta 8979 zcmY*fV{{$d(moANW81db*tXT!Nn`UgX2ZtD$%&n`v2C-lt;YD?@2-14?EPcUv!0n* z`^Ws4HP4i8L%;4p*JkD-J9ja2aKi!sX@~#-MY5?EPBK~fXAl)Ti}^QGH@6h+V+|}F zv=1RqQxhWW9!hTvYE!)+*m%jEL^9caK;am9X8QP~a9X0N6(=WSX8KF#WpU-6TjyR3 zpKhscivP97d$DGc{KI(f#g07u{Jr0wn#+qNr}yW}2N3{Kx0lCq%p4LBKil*QDTEyR zg{{&=GAy_O0VJ(8ZbtS4tPeeeILKK(M?HtQY!6K^wt zxsPH>E%g%V@=!B;kWF54$xjC&4hO!ZEG0QFMHLqe!tgH;%vO62BQj||nokbX&2kxF zzg#N!2M|NxFL#YdwOL8}>iDLr%2=!LZvk_&`AMrm7Zm%#_{Ot_qw=HkdVg{f9hYHF zlRF*9kxo~FPfyBD!^d6MbD?BRZj(4u9j!5}HFUt+$#Jd48Fd~ahe@)R9Z2M1t%LHa z_IP|tDb0CDl(fsEbvIYawJLJ7hXfpVw)D-)R-mHdyn5uZYefN0rZ-#KDzb`gsow;v zGX>k|g5?D%Vn_}IJIgf%nAz{@j0FCIEVWffc1Z+lliA}L+WJY=MAf$GeI7xw5YD1) z;BJn$T;JI5vTbZ&4aYfmd-XPQd)YQ~d({>(^5u>Y^5rfxEUDci9I5?dXp6{zHG=Tc z6$rLd^C~60=K4ptlZ%Fl-%QLc-x{y=zU$%&4ZU}4&Yu?jF4eqB#kTHhty`Aq=kJE% zzq(5OS9o1t-)}S}`chh1Uu-Sl?ljxMDVIy5j`97Eqg7L~Ak9NSZ?!5M>5TRMXfD#} zFlMmFnr%?ra>vkvJQjmWa8oB{63qPo1L#LAht%FG|6CEe9KP2&VNe_HNb7M}pd*!t zpGL0vzCU02%iK@AKWxP^64fz-U#%u~D+FV?*KdPY9C_9{Ggn;Y;;iKE0b|}KmC&f(WIDcFtvRPDju z?Dc&_dP4*hh!%!6(nYB*TEJs<4zn*V0Nw1O4VzYaNZul>anE2Feb@T$XkI?)u6VK$bg* z22AY7|Ju!_jwc2@JX(;SUE>VDWRD|d56WYUGLAAwPYXU9K&NgY{t{dyMskUBgV%@p zMVcFn>W|hJA?3S?$k!M|1S2e1A&_~W2p$;O2Wpn`$|8W(@~w>RR4kxHdEr`+q|>m@ zTYp%Ut+g`T#HkyE5zw<5uhFvt2=k5fM3!8OxvGgMRS|t7RaJn7!2$r_-~a%C7@*Dq zGUp2g0N^HzLU=%bROVFi2J;#`7#WGTUI$r!(wmbJlbS`E#ZpNp7vOR#TwPQWNf$IW zoX>v@6S8n6+HhUZB7V^A`Y9t4ngdfUFZrDOayMVvg&=RY4@0Z~L|vW)DZTIvqA)%D zi!pa)8L7BipsVh5-LMH4bmwt2?t88YUfIRf!@8^gX$xpKTE^WpM!-=3?UVw^Cs`Y7 z2b<*~Q=1uqs79{h&H_8+X%><4qSbz_cSEa;Hkdmtq5uwGTY+|APD{i_zYhLXqT7HO zT^Am_tW?Cmn%N~MC0!9mYt-~WK;hj-SnayMwqAAHo#^ALwkg0>72&W}5^4%|Z|@T; zwwBQTg*&eXC}j8 zra77(XC^p&&o;KrZ$`_)C$@SDWT+p$3!;ZB#yhnK{CxQc&?R}ZQMcp`!!eXLLhiP8W zM=McHAMnUMlar8XLXk&jx#HBH3U0jbhJuqa~#l`aB)N6;WI(Im322o#{K&92l6(K z)(;=;-m!%9@j#WSA1uniU(^x(UTi+%idMd)x*!*Hub0Rg7DblI!cqo9QUZf29Y#?XN!K!|ovJ7~!^H}!zsaMl(57lpztQ7V zyo#`qJ4jv1zGAW2uIkU3o&7_=lYWz3=SR!sgfuYp{Um<*H%uW8MdUT2&o*QKjD3PEH zHz;H}qCN~`GFsJ_xz$9xga*@VzJTH7-3lggkBM&7xlz5#qWfkgi=#j%{&f-NMsaSv zeIZ60Jpw}QV+t`ovOJxVhYCXe8E7r*eLCJ{lP6sqc}BYrhjXlt(6e9nw=2Le1gOT0 zZX!q9r#DZ&8_cAhWPeq~CJkGvpRU&q8>rR@RBW4~@3j1X>RBum#U z1wjcEdB`|@sXAWxk2*TOj> zr(j{nr1;Mk3x^gvAtZsahY=ou{eAJi-d(XISF-?+Q6{Um4+lu?aA=S33@k=6^OT?F z8TE`ha;q@=ZQ-dlt!q49;Wjjl<&Yee^!h5MFkd)Oj=fsvxytK%!B z-P#YJ)8^dMi=wpKmt43|apX6v2dNXzZ-WHlLEh`JoKFNjCK7LhO^P5XW?Y~rjGcIpv$2v41rE}~0{aj9NVpDXGdD6W8{fyzioQdu&xkn8 zhT*^NY0zv>Om?h3XAku3p-4SHkK@fXrpi{T=@#bwY76TsD4$tAHAhXAStdb$odc z02~lZyb!fG_7qrU_F5 zoOG|pEwdyDhLXDwlU>T|;LF@ACJk(qZ*2h6GB@33mKk};HO^CQM(N7@Ml5|8IeHzt zdG4f$q}SNYA4P=?jV!mJ%3hRKwi&!wFptWZRq4bpV9^b7&L>nW%~Y|junw!jHj%85 z3Ck6%`Y=Abvrujnm{`OtE0uQkeX@3JPzj#iO#eNoAX6cDhM+cc2mLk8;^bG62mtjQ zj|kxI2W|4n{VqMqB?@YnA0y}@Mju)&j3UQ4tSdH=Eu?>i7A50b%i$pc{YJki7ubq7 zVTDqdkGjeAuZdF)KBwR6LZob}7`2935iKIU2-I;88&?t16c-~TNWIcQ8C_cE_F1tv z*>4<_kimwX^CQtFrlk)i!3-+2zD|=!D43Qqk-LtpPnX#QQt%eullxHat97k=00qR|b2|M}`q??yf+h~};_PJ2bLeEeteO3rh+H{9otNQDki^lu)(`a~_x(8NWLE*rb%T=Z~s?JC|G zXNnO~2SzW)H}p6Zn%WqAyadG=?$BXuS(x-2(T!E&sBcIz6`w=MdtxR<7M`s6-#!s+ znhpkcNMw{c#!F%#O!K*?(Hl(;Tgl9~WYBB(P@9KHb8ZkLN>|}+pQ)K#>ANpV1IM{Q z8qL^PiNEOrY*%!7Hj!CwRT2CN4r(ipJA%kCc&s;wOfrweu)H!YlFM z247pwv!nFWbTKq&zm4UVH^d?H2M276ny~@v5jR2>@ihAmcdZI-ah(&)7uLQM5COqg?hjX2<75QU4o5Q7 zZ5gG;6RMhxLa5NFTXgegSXb0a%aPdmLL4=`ox2smE)lDn^!;^PNftzTf~n{NH7uh_ zc9sKmx@q1InUh_BgI3C!f>`HnO~X`9#XTI^Yzaj1928gz8ClI!WIB&2!&;M18pf0T zsZ81LY3$-_O`@4$vrO`Cb&{apkvUwrA0Z49YfZYD)V4;c2&`JPJuwN_o~2vnyW_b! z%yUSS5K{a*t>;WJr&$A_&}bLTTXK23<;*EiNHHF-F<#hy8v2eegrqnE=^gt+|8R5o z_80IY4&-!2`uISX6lb0kCVmkQ{D}HMGUAkCe`I~t2~99(<#}{E;{+Y0!FU>leSP(M zuMoSOEfw3OC5kQ~Y2)EMlJceJlh}p?uw}!cq?h44=b2k@T1;6KviZGc_zbeTtTE$@EDwUcjxd#fpK=W*U@S#U|YKz{#qbb*|BpcaU!>6&Ir zhsA+ywgvk54%Nj>!!oH>MQ+L~36v1pV%^pOmvo7sT|N}$U!T6l^<3W2 z6}mT7Cl=IQo%Y~d%l=+;vdK)yW!C>Es-~b^E?IjUU4h6<86tun6rO#?!37B)M8>ph zJ@`~09W^@5=}sWg8`~ew=0>0*V^b9eG=rBIGbe3Ko$pj!0CBUTmF^Q}l7|kCeB(pX zi6UvbUJWfKcA&PDq?2HrMnJBTW#nm$(vPZE;%FRM#ge$S)i4!y$ShDwduz@EPp3H? z`+%=~-g6`Ibtrb=QsH3w-bKCX1_aGKo4Q7n-zYp->k~KE!(K@VZder&^^hIF6AhiG z;_ig2NDd_hpo!W1Un{GcB@e{O@P3zHnj;@SzYCxsImCHJS5I&^s-J6?cw92qeK8}W zk<_SvajS&d_tDP~>nhkJSoN>UZUHs?)bDY`{`;D^@wMW0@!H1I_BYphly0iqq^Jp; z_aD>eHbu@e6&PUQ4*q*ik0i*$Ru^_@`Mbyrscb&`8|c=RWZ>Ybs16Q?Cj1r6RQA5! zOeuxfzWm(fX!geO(anpBCOV|a&mu|$4cZ<*{pb1F{`-cm1)yB6AGm7b=GV@r*DataJ^I!>^lCvS_@AftZiwtpszHmq{UVl zKL9164tmF5g>uOZ({Jg~fH~QyHd#h#E;WzSYO~zt)_ZMhefdm5*H1K-#=_kw#o%ch zgX|C$K4l4IY8=PV6Q{T8dd`*6MG-TlsTEaA&W{EuwaoN+-BDdSL2>|lwiZ++4eR8h zNS1yJdbhAWjW4k`i1KL)l#G*Y=a0ouTbg8R1aUU`8X7p*AnO+uaNF9mwa+ooA)hlj zR26XBpQ-{6E9;PQAvq2<%!M1;@Q%r@xZ16YRyL&v}9F`Nnx#RLUc<78w$S zZElh==Rnr2u<*qKY|aUR9(A|{cURqP81O-1a@X)khheokEhC}BS-g~|zRbn-igmID z$Ww!O0-j!t(lx>-JH+0KW3*Bgafpm>%n=`(ZLa^TWd*-je!Xi7H*bZ8pz`HPFYeC? zk>`W)4Cj6*A3A8g$MEhp*<@qO&&>3<4YI%0YAMmQvD3 z${78Fa2mqiI>P7|gE)xs$cg3~^?UBb4y6B4Z#0Fzy zN8Gf!c+$uPS`VRB=wRV1f)>+PEHBYco<1?ceXET}Q-tKI=E`21<15xTe@%Bhk$v09 zVpoL_wNuw)@^O+C@VCeuWM}(%C(%lTJ}7n)JVV!^0H!3@)ydq#vEt;_*+xos$9i?{ zCw5^ZcNS&GzaeBmPg6IKrbT`OSuKg$wai+5K}$mTO-Z$s3Y+vb3G}x%WqlnQS1;|Z zlZ$L{onq1Ag#5JrM)%6~ToQ}NmM2A(7X5gy$nVI=tQFOm;7|Oeij{xb_KU{d@%)2z zsVqzTl@XPf(a95;P;oBm9Hlpo`9)D9>G>!Bj=ZmX{ces=aC~E^$rTO5hO$#X65jEA zMj1(p+HXdOh7FAV;(_)_RR#P>&NW?&4C7K1Y$C$i**g;KOdu|JI_Ep zV-N$wuDRkn6=k|tCDXU%d=YvT!M1nU?JY;Pl`dxQX5+660TX7~q@ukEKc!Iqy2y)KuG^Q-Y%$;SR&Mv{%=CjphG1_^dkUM=qI*3Ih^Bk621n`6;q(D;nB_y|~ zW*1ps&h|wcET!#~+Ptsiex~YVhDiIREiw1=uwlNpPyqDZ`qqv9GtKwvxnFE}ME93fD9(Iq zz=f&4ZpD~+qROW6Y2AjPj9pH*r_pS_f@tLl88dbkO9LG0+|4*Xq(Eo7fr5MVg{n<+p>H{LGr}UzToqfk_x6(2YB~-^7>%X z+331Ob|NyMST64u|1dK*#J>qEW@dKNj-u}3MG)ZQi~#GzJ_S4n5lb7vu&>;I-M49a z0Uc#GD-KjO`tQ5ftuSz<+`rT)cLio$OJDLtC`t)bE+Nu@Rok2;`#zv1=n z7_CZr&EhVy{jq(eJPS)XA>!7t<&ormWI~w0@Y#VKjK)`KAO~3|%+{ z$HKIF?86~jH*1p=`j#}8ON0{mvoiN7fS^N+TzF~;9G0_lQ?(OT8!b1F8a~epAH#uA zSN+goE<-psRqPXdG7}w=ddH=QAL|g}x5%l-`Kh69D4{M?jv!l))<@jxLL$Eg2vt@E zc6w`$?_z%awCE~ca)9nMvj($VH%2!?w3c(5Y4&ZC2q#yQ=r{H2O839eoBJ{rfMTs8 zn2aL6e6?;LY#&(BvX_gC6uFK`0yt zJbUATdyz5d3lRyV!rwbj0hVg#KHdK0^A7_3KA%gKi#F#-^K%1XQbeF49arI2LA|Bj z?=;VxKbZo(iQmHB5eAg=8IPRqyskQNR!&KEPrGv&kMr(8`4oe?vd?sIZJK+JY04kc zXWk)4N|~*|0$4sUV3U6W6g+Z3;nN<~n4H17QT*%MCLt_huVl@QkV`A`jyq<|q=&F_ zPEOotTu9?zGKaPJ#9P&ljgW!|Vxhe+l85%G5zpD5kAtn*ZC})qEy!v`_R}EcOn)&# z-+B52@Zle@$!^-N@<_=LKF}fqQkwf1rE(OQP&8!En}jqr-l0A0K>77K8{zT%wVpT~ zMgDx}RUG$jgaeqv*E~<#RT?Q)(RGi8bUm(1X?2OAG2!LbBR+u1r7$}s=lKqu&VjXP zUw3L9DH({yj)M%OqP%GC+$}o0iG|*hN-Ecv3bxS|Mxpmz*%x`w7~=o9BKfEVzr~K- zo&Fh`wZ{#1Jd5QFM4&!PabL!tf%TfJ4wi;45AqWe$x}8*c2cgqua`(6@ErE&P{K5M zQfwGQ4Qg&M3r4^^$B?_AdLzqtxn5nb#kItDY?BTW z#hShspeIDJ1FDmfq@dz1TT`OV;SS0ImUp`P6GzOqB3dPfzf?+w^40!Wn*4s!E;iHW zNzpDG+Vmtnh%CyfAX>X z{Y=vt;yb z;TBRZpw##Kh$l<8qq5|3LkrwX%MoxqWwclBS6|7LDM(I31>$_w=;{=HcyWlak3xM1 z_oaOa)a;AtV{*xSj6v|x%a42{h@X-cr%#HO5hWbuKRGTZS)o=^Id^>H5}0p_(BEXX zx3VnRUj6&1JjDI);c=#EYcsg;D5TFlhe)=nAycR1N)YSHQvO+P5hKe9T0ggZT{oF@ z#i3V4TpQlO1A8*TWn|e}UWZ(OU;Isd^ zb<#Vj`~W_-S_=lDR#223!xq8sRjAAVSY2MhRyUyHa-{ql=zyMz?~i_c&dS>eb>s>#q#$UI+!&6MftpQvxHA@f|k2(G9z zAQCx-lJ-AT;PnX%dY5}N$m6tFt5h6;Mf78TmFUN9#4*qBNg4it3-s22P+|Rw zG@X%R0sm*X07ZZEOJRbDkcjr}tvaVWlrwJ#7KYEw&X`2lDa@qb!0*SHa%+-FU!83q zY{R15$vfL56^Nj42#vGQlQ%coT4bLr2s5Y0zBFp8u&F(+*%k4xE1{s75Q?P(SL7kf zhG?3rfM9V*b?>dOpwr%uGH7Xfk1HZ!*k`@CNM77g_mGN=ucMG&QX19B!%y77w?g#b z%k3x6q_w_%ghL;9Zk_J#V{hxK%6j`?-`UN?^e%(L6R#t#97kZaOr1{&<8VGVs1O>} z6~!myW`ja01v%qy%WI=8WI!cf#YA8KNRoU>`_muCqpt_;F@rkVeDY}F7puI_wBPH9 zgRGre(X_z4PUO5!VDSyg)bea1x_a7M z4AJ?dd9rf{*P`AY+w?g_TyJlB5Nks~1$@PxdtpUGGG##7j<$g&BhKq0mXTva{;h5E ztcN!O17bquKEDC#;Yw2yE>*=|WdZT9+ycgUR^f?~+TY-E552AZlzYn{-2CLRV9mn8 z+zNoWLae^P{co`F?)r;f!C=nnl*1+DI)mZY!frp~f%6tX2g=?zQL^d-j^t1~+xYgK zv;np&js@X=_e7F&&ZUX|N6Q2P0L=fWoBuh*L7$3~$-A)sdy6EQ@Pd-)|7lDA@%ra2 z4jL@^w92&KC>H(=v2j!tVE_3w0KogtrNjgPBsTvW F{TFmrHLU;u delta 8469 zcmY*q~ZGqoW{=01$bgB@1Nex`%9%S2I04)5Jw9+UyLS&r+9O2bq{gY;dCa zHW3WY0%Dem?S7n5JZO%*yiT9fb!XGk9^Q`o-EO{a^j%&)ZsxsSN@2k2eFx1*psqn0e*crIbAO}Rd~_BifMu*q7SUn{>WD$=7n_$uiQ0wGc$?u1hM%gf??nL?m22h!8{ zYmFMLvx6fjz*nwF^tAqx1uv0yEW9-tcIV5Q{HNh`9PMsuqD8VE%oAs5FsWa0mLV$L zPAF5e^$tJ8_Kwp!$N1M<#Z154n!X6hFpk8)eMLu; zaXS71&`24 zV`x~}yAxBw##Oj@qo_@DcBqc+2TB&=bJyZWTeR55zG<{Z@T^hSbMdm~Ikkr?4{7WT zcjPyu>0sDjl7&?TL@ z)cW?lW@Pfwu#nm7E1%6*nBIzQrKhHl`t54$-m>j8f%0vVr?N0PTz`}VrYAl+8h^O~ zuWQj@aZSZmGPtcVjGq-EQ1V`)%x{HZ6pT-tZttJOQm?q-#KzchbH>>5-jEX*K~KDa z#oO&Qf4$@}ZGQ7gxn<;D$ziphThbi6zL^YC;J#t0GCbjY)NHdqF=M4e(@|DUPY_=F zLcX1HAJ+O-3VkU#LW`4;=6szwwo%^R4#UK}HdAXK` z{m!VZj5q9tVYL=^TqPH*6?>*yr>VxyYF4tY{~?qJ*eIoIU0}-TLepzga4g}}D7#Qu zn;6I;l!`xaL^8r*Tz*h`^(xJCnuVR_O@Gl*Q}y$lp%!kxD`%zN19WTIf`VX*M=cDp z*s4<9wP|ev;PARRV`g$R*QV@rr%Ku~z(2-s>nt{JI$357vnFAz9!ZsiiH#4wOt+!1 zM;h;EN__zBn)*-A^l!`b?b*VI-?)Sj6&Ov3!j9k$5+#w)M>`AExCm0!#XL+E{Bp)s;Hochs+-@@)7_XDMPby#p<9mLu+S{8e2Jn`1`1nrffBfy4u)p7FFQWzgYt zXC}GypRdkTUS+mP!jSH$K71PYI%QI-{m;DvlRb*|4GMPmvURv0uD2bvS%FOSe_$4zc--*>gfRMKN|D ztP^WFfGEkcm?sqXoyRmuCgb?bSG17#QSv4~XsbPH>BE%;bZQ_HQb?q%CjykL7CWDf z!rtrPk~46_!{V`V<;AjAza;w-F%t1^+b|r_um$#1cHZ1|WpVUS&1aq?Mnss|HVDRY z*sVYNB+4#TJAh4#rGbr}oSnxjD6_LIkanNvZ9_#bm?$HKKdDdg4%vxbm-t@ZcKr#x z6<$$VPNBpWM2S+bf5IBjY3-IY2-BwRfW_DonEaXa=h{xOH%oa~gPW6LTF26Y*M)$N z=9i`Y8};Qgr#zvU)_^yU5yB;9@yJjrMvc4T%}a|jCze826soW-d`V~eo%RTh)&#XR zRe<8$42S2oz|NVcB%rG(FP2U&X>3 z4M^}|K{v64>~rob;$GO55t;Nb&T+A3u(>P6;wtp6DBGWbX|3EZBDAM2DCo&4w|WGpi;~qUY?Ofg$pX&`zR~)lr)8}z^U3U38Nrtnmf~e7$i=l>+*R%hQgDrj%P7F zIjyBCj2$Td=Fp=0Dk{=8d6cIcW6zhK!$>k*uC^f}c6-NR$ zd<)oa+_fQDyY-}9DsPBvh@6EvLZ}c)C&O-+wY|}RYHbc2cdGuNcJ7#yE}9=!Vt-Q~ z4tOePK!0IJ0cW*jOkCO? zS-T!bE{5LD&u!I4tqy;dI*)#e^i)uIDxU?8wK1COP3Qk{$vM3Sm8(F2VwM?1A+dle z6`M6bbZye|kew%w9l`GS74yhLluJU5R=#!&zGwB7lmTt}&eCt0g(-a;Mom-{lL6u~ zFgjyUs1$K*0R51qQTW_165~#WRrMxiUx{0F#+tvgtcjV$U|Z}G*JWo6)8f!+(4o>O zuaAxLfUl;GHI}A}Kc>A8h^v6C-9bb}lw@rtA*4Q8)z>0oa6V1>N4GFyi&v69#x&CwK*^!w&$`dv zQKRMKcN$^=$?4to7X4I`?PKGi(=R}d8cv{74o|9FwS zvvTg0D~O%bQpbp@{r49;r~5`mcE^P<9;Zi$?4LP-^P^kuY#uBz$F!u1d{Ens6~$Od zf)dV+8-4!eURXZZ;lM4rJw{R3f1Ng<9nn2_RQUZDrOw5+DtdAIv*v@3ZBU9G)sC&y!vM28daSH7(SKNGcV z&5x#e#W2eY?XN@jyOQiSj$BlXkTG3uAL{D|PwoMp$}f3h5o7b4Y+X#P)0jlolgLn9xC%zr3jr$gl$8?II`DO6gIGm;O`R`bN{;DlXaY4b`>x6xH=Kl@ z!>mh~TLOo)#dTb~F;O z8hpjW9Ga?AX&&J+T#RM6u*9x{&%I8m?vk4eDWz^l2N_k(TbeBpIwcV4FhL(S$4l5p z@{n7|sax){t!3t4O!`o(dYCNh90+hl|p%V_q&cwBzT*?Nu*D0wZ)fPXv z@*;`TO7T0WKtFh8~mQx;49VG_`l`g|&VK}LysK%eU4})Cvvg3YN)%;zI?;_Nr z)5zuU1^r3h;Y+mJov*->dOOj>RV^u2*|RraaQWsY5N?Uu)fKJOCSL2^G=RB%(4K{* zx!^cB@I|kJR`b+5IK}(6)m=O{49P5E^)!XvD5zVuzJH{01^#$@Cn514w41BB;FAoS2SYl3SRrOBDLfl5MvgA3 zU6{T?BW}l~8vU;q@p9IOM(=;WdioeQmt?X|=L9kyM&ZsNc*-Knv8@U*O96T@4ZiJ$ zeFL2}pw_~Tm3d4#q!zZS0km@vYgym33C0h(6D)6|Y)*UXI^T`(QPQh$WF?&h(3QYh zqGw@?BTk@VA_VxK@z?a@UrMhY zUD16oqx4$$6J_k0HnXgARm}N#(^yA1MLdbwmEqHnX*JdHN>$5k2E|^_bL< zGf5Z+D!9dXR>^(5F&5gIew1%kJtFUwI5P1~I$4LL_6)3RPzw|@2vV;Q^MeQUKzc=KxSTTX`}u%z?h~;qI#%dE@OZwehZyDBsWTc&tOC1c%HS#AyTJ= zQixj=BNVaRS*G!;B$}cJljeiVQabC25O+xr4A+32HVb;@+%r}$^u4-R?^3yij)0xb z86i@aoVxa%?bfOE;Bgvm&8_8K(M-ZEj*u9ms_Hk#2eL`PSnD#At!0l{f!v`&Kg}M$n(&R)?AigC5Z?T7Jv^lrDL!yYS{4 zq_H}oezX-Svu>dp)wE@khE@aR5vY=;{C-8Hws++5LDpArYd)U47jc-;f~07_TPa^1 zO`0+uIq)@?^!%JXCDid+nt|c@NG1+ce@ijUX&@rV9UiT|m+t-nqVB7?&UX*|{yDBFw9x52&dTh@;CL)Q?6s1gL=CUQTX7#TJPs9cpw<4>GFMUKo|f{! z&(%2hP6ghr%UFVO-N^v9l|tKy>&e%8us}wT0N*l(tezoctVtLmNdGPOF6oaAGJI5R zZ*|k@z3H!~Mm9fXw{bbP6?lV-j#Rfgnjf++O7*|5vz2#XK;kk ztJbi%r0{U5@QwHYfwdjtqJ6?;X{Ul3?W0O0bZ$k*y z4jWsNedRoCb7_|>nazmq{T3Y_{<5IO&zQ?9&uS@iL+|K|eXy^F>-60HDoVvovHelY zy6p(}H^7b+$gu@7xLn_^oQryjVu#pRE5&-w5ZLCK&)WJ5jJF{B>y;-=)C;xbF#wig zNxN^>TwzZbV+{+M?}UfbFSe#(x$c)|d_9fRLLHH?Xbn!PoM{(+S5IEFRe4$aHg~hP zJYt`h&?WuNs4mVAmk$yeM;8?R6;YBMp8VilyM!RXWj<95=yp=4@y?`Ua8 znR^R?u&g%`$Wa~usp|pO$aMF-en!DrolPjD_g#{8X1f=#_7hH8i|WF+wMqmxUm*!G z*4p980g{sgR9?{}B+a0yiOdR()tWE8u)vMPxAdK)?$M+O_S+;nB34@o<%lGJbXbP` z5)<({mNpHp&45UvN`b&K5SD#W){}6Y_d4v~amZPGg|3GdlWDB;;?a=Z{dd zELTfXnjCqq{Dgbh9c%LjK!Epi1TGI{A7AP|eg2@TFQiUd4Bo!JsCqsS-8ml`j{gM& zEd7yU`djX!EX2I{WZq=qasFzdDWD`Z?ULFVIP!(KQP=fJh5QC9D|$JGV95jv)!sYWY?irpvh06rw&O?iIvMMj=X zr%`aa(|{Ad=Vr9%Q(61{PB-V_(3A%p&V#0zGKI1O(^;tkS{>Y<`Ql@_-b7IOT&@?l zavh?#FW?5otMIjq+Bp?Lq)w7S(0Vp0o!J*~O1>av;)Cdok@h&JKaoHDV6IVtJ?N#XY=lknPN+SN8@3Gb+D-X*y5pQ)wnIpQlRR!Rd)@0LdA85}1 zu7W6tJ*p26ovz+`YCPePT>-+p@T_QsW$uE`McLlXb;k}!wwWuh$YC4qHRd=RS!s>2 zo39VCB-#Ew?PAYOx`x!@0qa5lZKrE?PJEwVfkww#aB_$CLKlkzHSIi4p3#IeyA@u@ z`x^!`0HJxe>#V7+Grku^in>Ppz|TD*`Ca4X%R3Yo|J=!)l$vYks|KhG{1CEfyuzK( zLjCz{5l}9>$J=FC?59^85awK0$;^9t9UxwOU8kP7ReVCc*rPOr(9uMY*aCZi2=JBu z(D0svsJRB&a9nY;6|4kMr1Er5kUVOh1TuBwa3B2C<+rS|xJo&Lnx3K-*P83eXQCJ= z(htQSA3hgOMcs`#NdYB17#zP_1N_P0peHrNo1%NsYn=;PgLXTic6b#{Y0Z~x9Ffav z^3eO+diquPfo1AXW*>G(JcGn{yN?segqKL$Wc9po(Kex z#tw_};zd++we+MPhOOgaXSmguul67JOvBysmg?wRf=OUeh(XyRcyY@8RTV@xck_c~ zLFMWAWb4^7xwR)3iO1PIs1<}L3CMJ1L-}s=>_y!`!FvYf^pJO|&nII{!Dz+b?=bUd zPJUUn))z)-TcpqKF(1tr-x1;lS?SB@mT#O7skl0sER{a|d?&>EKKaw* zQ>D^m*pNgV`54BKv?knU-T5bcvBKnI@KZo^UYjKp{2hpCo?_6v(Sg77@nQa{tSKbn zUgMtF>A3hndGocRY+Snm#)Q4%`|Qq3YTOU^uG}BGlz!B=zb?vB16sN&6J`L(k1r+$ z5G6E9tJ~Iwd!d!NH7Q%Z@BR@0e{p6#XF2))?FLAVG`npIjih*I+0!f6;+DM zLOP-qDsm9=ZrI!lfSDn%XuF17$j~gZE@I}S(Ctw&Te75P5?Fj%FLT;p-tm33FaUQc z5cR;$SwV|N0xmjox3V~XL3sV?YN}U0kkfmygW@a5JOCGgce6JyzGmgN$?NM%4;wEhUMg0uTTB~L==1Fvc(6)KMLmU z(12l^#g&9OpF7+Ll30F6(q=~>NIY=-YUJJ}@&;!RYnq*xA9h!iMi`t;B2SUqbyNGn zye@*0#Uu`OQy%utS%IA%$M1f4B|bOH={!3K1=Tc7Ra|%qZgZ{mjAGKXb)}jUu1mQ_ zRW7<;tkHv(m7E0m>**8D;+2ddTL>EcH_1YqCaTTu_#6Djm z*64!w#=Hz<>Fi1n+P}l#-)0e0P4o+D8^^Mk& zhHeJoh2paKlO+8r?$tx`qEcm|PSt6|1$1q?r@VvvMd1!*zAy3<`X9j?ZI|;jE-F(H zIn1+sm(zAnoJArtytHC|0&F0`i*dy-PiwbD-+j`ezvd4C`%F1y^7t}2aww}ZlPk)t z=Y`tm#jNM$d`pG%F42Xmg_pZnEnvC%avz=xNs!=6b%%JSuc(WObezkCeZ#C|3PpXj zkR8hDPyTIUv~?<%*)6=8`WfPPyB9goi+p$1N2N<%!tS2wopT2x`2IZi?|_P{GA|I5 z?7DP*?Gi#2SJZ!x#W9Npm)T;=;~Swyeb*!P{I^s@o5m_3GS2Lg?VUeBdOeae7&s5$ zSL_VuTJih_fq7g8O8b0g+GbmE+xG}^Wx`g~{mWTyr@=h zKlAymoHeZa`DgR?Pj8Yc+I|MrSB>X*ts#wNFOJxs!3aGE)xeTHlF`fC5^g(DTacl$ zx!ezQJdwIyc$8RyNS~Wh{0pp>8NcW)*J=7AQYdT?(QhJuq4u`QniZ!%6l{KWp-0Xp z4ZC6(E(_&c$$U_cmGFslsyX6(62~m*z8Yx2p+F5xmD%6A7eOnx`1lJA-Mrc#&xZWJ zzXV{{OIgzYaq|D4k^j%z|8JB8GnRu3hw#8Z@({sSmsF(x>!w0Meg5y(zg!Z0S^0k# z5x^g1@L;toCK$NB|Fn Date: Wed, 26 Apr 2023 16:36:41 +0200 Subject: [PATCH 736/743] Bump micronaut-sql to 5.0.0-M4 (#9156) --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 1246f4e203e..49aa966308b 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -38,7 +38,7 @@ log4j = "2.19.0" micronaut-aws = "3.9.2" micronaut-groovy = "4.0.0-M1" micronaut-session = "4.0.0-M1" -micronaut-sql = "5.0.0-M3" +micronaut-sql = "5.0.0-M4" micronaut-test = "4.0.0-M2" micronaut-serde = "2.0.0-M1" micronaut-tracing = "5.0.0-SNAPSHOT" From b7c447b17407e49c701c787e17dd92b6bdd69fa2 Mon Sep 17 00:00:00 2001 From: Sergey Gavrilov Date: Thu, 27 Apr 2023 17:29:01 +0300 Subject: [PATCH 737/743] support for Comparable types in expressions (#9163) --- ...gleEvaluatedEvaluatedExpressionParser.java | 4 + .../parser/ast/ExpressionNode.java | 10 +- .../ast/access/ContextElementAccess.java | 63 ++--- .../ast/operator/binary/BinaryOperator.java | 19 +- .../ComparablesComparisonOperation.java | 195 ++++++++++++++ .../binary/NumericComparisonOperation.java | 117 +++++++++ .../operator/binary/RelationalOperator.java | 91 +++---- .../AbstractEvaluatedExpressionsSpec.groovy | 47 ++++ .../OperatorExpressionsSpec.groovy | 237 ++++++++++++++++++ .../AbstractEvaluatedExpressionsSpec.groovy | 46 ++++ .../expressions/OperatorExpressionSpec.groovy | 237 ++++++++++++++++++ .../guide/config/evaluatedExpressions.adoc | 7 +- 12 files changed, 945 insertions(+), 128 deletions(-) create mode 100644 core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/ComparablesComparisonOperation.java create mode 100644 core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/NumericComparisonOperation.java diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/SingleEvaluatedEvaluatedExpressionParser.java b/core-processor/src/main/java/io/micronaut/expressions/parser/SingleEvaluatedEvaluatedExpressionParser.java index 6aa012f0f8a..fdaeb2f4f17 100644 --- a/core-processor/src/main/java/io/micronaut/expressions/parser/SingleEvaluatedEvaluatedExpressionParser.java +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/SingleEvaluatedEvaluatedExpressionParser.java @@ -324,6 +324,7 @@ private ExpressionNode postfixExpression() { // : EvaluationContextAccess // | BeanContextAccess // | EnvironmentAccess + // | ThisAccess // | TypeIdentifier // | ParenthesizedExpression // | Literal @@ -342,6 +343,9 @@ private ExpressionNode primaryExpression() { }; } + // ThisAccess + // : 'this' + // ; private ExpressionNode thisAccess() { eat(THIS); return new ThisAccess(); diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/ExpressionNode.java b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/ExpressionNode.java index 2134b16e79d..2d66cafb9cf 100644 --- a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/ExpressionNode.java +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/ExpressionNode.java @@ -19,6 +19,7 @@ import io.micronaut.core.annotation.NonNull; import io.micronaut.expressions.parser.compilation.ExpressionVisitorContext; import io.micronaut.inject.ast.ClassElement; +import io.micronaut.inject.ast.PrimitiveElement; import org.objectweb.asm.Type; /** @@ -88,7 +89,14 @@ public final ClassElement resolveClassElement(@NonNull ExpressionVisitorContext * @param ctx The expression compilation context * @return The resolved type */ - protected abstract ClassElement doResolveClassElement(ExpressionVisitorContext ctx); + protected ClassElement doResolveClassElement(ExpressionVisitorContext ctx) { + Type type = doResolveType(ctx); + try { + return PrimitiveElement.valueOf(type.getClassName()); + } catch (IllegalArgumentException e) { + return ClassElement.of(type.getClassName()); + } + } /** * Resolves expression AST node type. diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/access/ContextElementAccess.java b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/access/ContextElementAccess.java index abe34c0dc7b..9a54f8efaa4 100644 --- a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/access/ContextElementAccess.java +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/access/ContextElementAccess.java @@ -46,8 +46,7 @@ public final class ContextElementAccess extends ExpressionNode { private final String name; - private ContextMethodCall contextPropertyMethodCall; - private ContextMethodParameterAccess contextMethodParameterAccess; + private ExpressionNode contextOperation; public ContextElementAccess(String name) { this.name = name; @@ -55,52 +54,24 @@ public ContextElementAccess(String name) { @Override protected void generateBytecode(ExpressionVisitorContext ctx) { - if (contextMethodParameterAccess != null) { - contextMethodParameterAccess.compile(ctx); - } else if (contextPropertyMethodCall != null) { - contextPropertyMethodCall.compile(ctx); - } + contextOperation.compile(ctx); } @Override protected ClassElement doResolveClassElement(ExpressionVisitorContext ctx) { - ExpressionCompilationContext evaluationContext = ctx.compilationContext(); - - List propertyElements = evaluationContext.findProperties(name); - List parameterElements = evaluationContext.findParameters(name); - - int totalElements = propertyElements.size() + parameterElements.size(); - - if (totalElements == 0) { - throw new ExpressionCompilationException( - "No element with name [" + name + "] available in evaluation context"); - } else if (totalElements > 1) { - throw new ExpressionCompilationException( - "Ambiguous expression evaluation context reference. Found " + totalElements + - " elements with name [" + name + "]"); - } - - if (!propertyElements.isEmpty()) { - - PropertyElement property = propertyElements.iterator().next(); - String readMethodName = - property.getReadMethod() - .orElseThrow(() -> new ExpressionCompilationException( - "Failed to obtain read method for property [" + name + "]")) - .getName(); - - contextPropertyMethodCall = new ContextMethodCall(readMethodName, emptyList()); - return contextPropertyMethodCall.resolveClassElement(ctx); - - } - - ParameterElement parameter = parameterElements.iterator().next(); - contextMethodParameterAccess = new ContextMethodParameterAccess(parameter); - return contextMethodParameterAccess.resolveClassElement(ctx); + return resolveContextOperation(ctx).resolveClassElement(ctx); } @Override public Type doResolveType(ExpressionVisitorContext ctx) { + return resolveContextOperation(ctx).resolveType(ctx); + } + + private ExpressionNode resolveContextOperation(ExpressionVisitorContext ctx) { + if (contextOperation != null) { + return contextOperation; + } + ExpressionCompilationContext evaluationContext = ctx.compilationContext(); List propertyElements = evaluationContext.findProperties(name); @@ -118,7 +89,6 @@ public Type doResolveType(ExpressionVisitorContext ctx) { } if (!propertyElements.isEmpty()) { - PropertyElement property = propertyElements.iterator().next(); String readMethodName = property.getReadMethod() @@ -126,13 +96,12 @@ public Type doResolveType(ExpressionVisitorContext ctx) { "Failed to obtain read method for property [" + name + "]")) .getName(); - contextPropertyMethodCall = new ContextMethodCall(readMethodName, emptyList()); - return contextPropertyMethodCall.resolveType(ctx); - + contextOperation = new ContextMethodCall(readMethodName, emptyList()); + } else { + ParameterElement parameter = parameterElements.iterator().next(); + contextOperation = new ContextMethodParameterAccess(parameter); } - ParameterElement parameter = parameterElements.iterator().next(); - contextMethodParameterAccess = new ContextMethodParameterAccess(parameter); - return contextMethodParameterAccess.resolveType(ctx); + return contextOperation; } } diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/BinaryOperator.java b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/BinaryOperator.java index 13f7b9075cb..e37cc2c64f0 100644 --- a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/BinaryOperator.java +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/BinaryOperator.java @@ -17,10 +17,7 @@ import io.micronaut.core.annotation.Internal; import io.micronaut.expressions.parser.ast.ExpressionNode; -import io.micronaut.expressions.parser.ast.conditional.ElvisOperator; import io.micronaut.expressions.parser.compilation.ExpressionVisitorContext; -import io.micronaut.inject.ast.ClassElement; -import io.micronaut.inject.ast.PrimitiveElement; import org.objectweb.asm.Type; /** @@ -30,7 +27,11 @@ * @since 4.0.0 */ @Internal -public abstract sealed class BinaryOperator extends ExpressionNode permits AddOperator, EqOperator, LogicalOperator, MathOperator, PowOperator, RelationalOperator { +public abstract sealed class BinaryOperator extends ExpressionNode permits AddOperator, + EqOperator, + LogicalOperator, + MathOperator, + PowOperator { protected final ExpressionNode leftOperand; protected final ExpressionNode rightOperand; @@ -46,16 +47,6 @@ protected Type doResolveType(ExpressionVisitorContext ctx) { return resolveOperationType(leftType, rightType); } - @Override - protected ClassElement doResolveClassElement(ExpressionVisitorContext ctx) { - Type type = doResolveType(ctx); - try { - return PrimitiveElement.valueOf(type.getClassName()); - } catch (IllegalArgumentException e) { - return ClassElement.of(type.getClassName()); - } - } - protected abstract Type resolveOperationType(Type leftOperandType, Type rightOperandType); } diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/ComparablesComparisonOperation.java b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/ComparablesComparisonOperation.java new file mode 100644 index 00000000000..7ad9d32ba78 --- /dev/null +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/ComparablesComparisonOperation.java @@ -0,0 +1,195 @@ +/* + * Copyright 2017-2023 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.expressions.parser.ast.operator.binary; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.Nullable; +import io.micronaut.expressions.parser.ast.ExpressionNode; +import io.micronaut.expressions.parser.ast.util.TypeDescriptors; +import io.micronaut.expressions.parser.compilation.ExpressionVisitorContext; +import io.micronaut.expressions.parser.exception.ExpressionCompilationException; +import io.micronaut.inject.ast.ClassElement; +import io.micronaut.inject.ast.PrimitiveElement; +import io.micronaut.inject.processing.JavaModelUtils; +import org.objectweb.asm.Label; +import org.objectweb.asm.Type; +import org.objectweb.asm.commons.GeneratorAdapter; +import org.objectweb.asm.commons.Method; + +import java.util.Optional; + +import static io.micronaut.expressions.parser.ast.util.EvaluatedExpressionCompilationUtils.pushBoxPrimitiveIfNecessary; +import static io.micronaut.expressions.parser.ast.util.TypeDescriptors.toBoxedIfNecessary; +import static org.objectweb.asm.Opcodes.GOTO; +import static org.objectweb.asm.Opcodes.IFGE; +import static org.objectweb.asm.Opcodes.IFGT; +import static org.objectweb.asm.Opcodes.IFLE; +import static org.objectweb.asm.Opcodes.IFLT; +import static org.objectweb.asm.Type.BOOLEAN_TYPE; + +/** + * Expression AST node for relational operations (>, <, >=, <=) on + * types that implement {@link Comparable} interface. + * + * @since 4.0.0 + * @author Sergey Gavrilov + */ +@Internal +public final class ComparablesComparisonOperation extends ExpressionNode { + + private static final String COMPARABLE_CLASS_NAME = Comparable.class.getName(); + + private final ExpressionNode leftOperand; + private final ExpressionNode rightOperand; + private final int comparisonOpcode; + + private ClassElement comparableTypeArgument; + private ComparisonType comparisonType; + + public ComparablesComparisonOperation(ExpressionNode leftOperand, + ExpressionNode rightOperand, + int comparisonOpcode) { + this.leftOperand = leftOperand; + this.rightOperand = rightOperand; + this.comparisonOpcode = comparisonOpcode; + } + + @Override + protected Type doResolveType(ExpressionVisitorContext ctx) { + // resolving non-primitive class elements is necessary to handle cases + // when one of expression nodes is of primitive type, but other expression node + // is comparable to respective boxed type + ClassElement leftClassElement = resolveNonPrimitiveClassElement(leftOperand, ctx); + ClassElement rightClassElement = resolveNonPrimitiveClassElement(rightOperand, ctx); + + ClassElement leftComparableTypeArgument = resolveComparableTypeArgument(leftClassElement); + ClassElement rightComparableTypeArgument = resolveComparableTypeArgument(rightClassElement); + + if (leftComparableTypeArgument != null && rightClassElement.isAssignable(leftComparableTypeArgument)) { + comparisonType = ComparisonType.LEFT; + comparableTypeArgument = leftComparableTypeArgument; + } else if (rightComparableTypeArgument != null && leftClassElement.isAssignable(rightComparableTypeArgument)) { + comparisonType = ComparisonType.RIGHT; + comparableTypeArgument = rightComparableTypeArgument; + } else { + throw new ExpressionCompilationException( + "Comparison operation can only be applied to numeric types or types that are " + + "Comparable to each other"); + } + + return BOOLEAN_TYPE; + } + + /** + * Resolves {@link ClassElement} of passed {@link ExpressionNode}, returning original + * {@link ClassElement} of node when it is of object type or boxed type in case + * {@link ExpressionNode} resolves to primitive type. + */ + private ClassElement resolveNonPrimitiveClassElement(ExpressionNode expressionNode, + ExpressionVisitorContext ctx) { + ClassElement classElement = expressionNode.resolveClassElement(ctx); + if (classElement instanceof PrimitiveElement) { + return ctx.visitorContext() + .getClassElement(toBoxedIfNecessary(expressionNode.resolveType(ctx)).getClassName()) + .orElseThrow(); + } + return classElement; + } + + @Nullable + private ClassElement resolveComparableTypeArgument(ClassElement classElement) { + return Optional.ofNullable(classElement + .getAllTypeArguments() + .get(COMPARABLE_CLASS_NAME)) + .map(types -> types.get("T")) + .orElse(null); + } + + @Override + public void generateBytecode(ExpressionVisitorContext ctx) { + GeneratorAdapter mv = ctx.methodVisitor(); + + Label elseLabel = new Label(); + Label endOfCmpLabel = new Label(); + + if (comparisonType == ComparisonType.LEFT) { + pushCompareToMethodCall(leftOperand, rightOperand, ctx); + mv.visitJumpInsn(comparisonOpcode, elseLabel); + } else { + pushCompareToMethodCall(rightOperand, leftOperand, ctx); + mv.visitJumpInsn(invertInstruction(comparisonOpcode), elseLabel); + } + + mv.push(true); + mv.visitJumpInsn(GOTO, endOfCmpLabel); + mv.visitLabel(elseLabel); + mv.push(false); + mv.visitLabel(endOfCmpLabel); + } + + private void pushCompareToMethodCall(ExpressionNode comparableNode, + ExpressionNode comparedNode, + ExpressionVisitorContext ctx) { + GeneratorAdapter mv = ctx.methodVisitor(); + ClassElement comparableClass = comparableNode.resolveClassElement(ctx); + + Type comparableType = comparableNode.resolveType(ctx); + Type comparedType = comparedNode.resolveType(ctx); + + comparableNode.compile(ctx); + pushBoxPrimitiveIfNecessary(comparableType, mv); + + comparedNode.compile(ctx); + pushBoxPrimitiveIfNecessary(comparedType, mv); + + if (comparableClass.isInterface()) { + mv.invokeInterface(comparableType, + new Method("compareTo", TypeDescriptors.INT, + new org.objectweb.asm.Type[]{TypeDescriptors.OBJECT})); + } else { + mv.invokeVirtual(comparableType, + new Method("compareTo", TypeDescriptors.INT, + new org.objectweb.asm.Type[]{JavaModelUtils.getTypeReference(comparableTypeArgument)})); + } + } + + private Integer invertInstruction(Integer instruction) { + return switch (instruction) { + case IFLE -> IFGE; + case IFLT -> IFGT; + case IFGE -> IFLE; + case IFGT -> IFLT; + default -> instruction; + }; + } + + private enum ComparisonType { + + /** + * Comparison type for cases when left compared value implements {@link Comparable} + * interface and right element of comparison expression is assignable to generic + * type parameter of left value. + */ + LEFT, + + /** + * Comparison type for cases when right compared value implements {@link Comparable} + * interface and left element of comparison expression is assignable to generic + * type parameter of right value. + */ + RIGHT + } +} diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/NumericComparisonOperation.java b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/NumericComparisonOperation.java new file mode 100644 index 00000000000..096c56e02ea --- /dev/null +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/NumericComparisonOperation.java @@ -0,0 +1,117 @@ +/* + * Copyright 2017-2023 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.expressions.parser.ast.operator.binary; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.expressions.parser.ast.ExpressionNode; +import io.micronaut.expressions.parser.compilation.ExpressionVisitorContext; +import io.micronaut.expressions.parser.exception.ExpressionCompilationException; +import org.objectweb.asm.Label; +import org.objectweb.asm.Type; +import org.objectweb.asm.commons.GeneratorAdapter; + +import static io.micronaut.expressions.parser.ast.util.EvaluatedExpressionCompilationUtils.pushPrimitiveCastIfNecessary; +import static io.micronaut.expressions.parser.ast.util.EvaluatedExpressionCompilationUtils.pushUnboxPrimitiveIfNecessary; +import static io.micronaut.expressions.parser.ast.util.TypeDescriptors.DOUBLE; +import static io.micronaut.expressions.parser.ast.util.TypeDescriptors.FLOAT; +import static io.micronaut.expressions.parser.ast.util.TypeDescriptors.LONG; +import static io.micronaut.expressions.parser.ast.util.TypeDescriptors.computeNumericOperationTargetType; +import static io.micronaut.expressions.parser.ast.util.TypeDescriptors.isNumeric; +import static io.micronaut.expressions.parser.ast.util.TypeDescriptors.isOneOf; +import static org.objectweb.asm.Opcodes.DCMPL; +import static org.objectweb.asm.Opcodes.FCMPL; +import static org.objectweb.asm.Opcodes.GOTO; +import static org.objectweb.asm.Opcodes.LCMP; +import static org.objectweb.asm.Type.BOOLEAN_TYPE; + +/** + * Expression AST node for relational operations (>, <, >=, <=) on + * numeric types. + * + * @since 4.0.0 + * @author Sergey Gavrilov + */ +@Internal +public final class NumericComparisonOperation extends ExpressionNode { + + private final ExpressionNode leftOperand; + private final ExpressionNode rightOperand; + private final int intComparisonOpcode; + private final int nonIntComparisonOpcode; + + public NumericComparisonOperation(ExpressionNode leftOperand, + ExpressionNode rightOperand, + int intComparisonOpcode, + int nonIntComparisonOpcode) { + this.leftOperand = leftOperand; + this.rightOperand = rightOperand; + this.intComparisonOpcode = intComparisonOpcode; + this.nonIntComparisonOpcode = nonIntComparisonOpcode; + } + + @Override + protected Type doResolveType(ExpressionVisitorContext ctx) { + Type leftType = leftOperand.resolveType(ctx); + Type rightType = rightOperand.resolveType(ctx); + + if (!isNumeric(leftType) || !isNumeric(rightType)) { + throw new ExpressionCompilationException( + "Numeric comparison operation can only be applied to numeric types"); + } + + return BOOLEAN_TYPE; + } + + @Override + public void generateBytecode(ExpressionVisitorContext ctx) { + GeneratorAdapter mv = ctx.methodVisitor(); + + Label elseLabel = new Label(); + Label endOfCmpLabel = new Label(); + + Type leftType = leftOperand.resolveType(ctx); + Type rightType = rightOperand.resolveType(ctx); + + Type targetType = computeNumericOperationTargetType(leftType, rightType); + + leftOperand.compile(ctx); + pushUnboxPrimitiveIfNecessary(leftType, mv); + pushPrimitiveCastIfNecessary(leftType, targetType, mv); + + rightOperand.compile(ctx); + pushUnboxPrimitiveIfNecessary(rightType, mv); + pushPrimitiveCastIfNecessary(rightType, targetType, mv); + + if (isOneOf(targetType, DOUBLE, FLOAT, LONG)) { + String targetDescriptor = targetType.getDescriptor(); + switch (targetDescriptor) { + case "D" -> mv.visitInsn(DCMPL); + case "F" -> mv.visitInsn(FCMPL); + case "J" -> mv.visitInsn(LCMP); + default -> { } + } + mv.visitJumpInsn(nonIntComparisonOpcode, elseLabel); + } else { + mv.visitJumpInsn(intComparisonOpcode, elseLabel); + } + + mv.push(true); + mv.visitJumpInsn(GOTO, endOfCmpLabel); + mv.visitLabel(elseLabel); + mv.push(false); + mv.visitLabel(endOfCmpLabel); + } +} diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/RelationalOperator.java b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/RelationalOperator.java index 41fcb9c8309..d5603389f5c 100644 --- a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/RelationalOperator.java +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/RelationalOperator.java @@ -18,94 +18,59 @@ import io.micronaut.core.annotation.Internal; import io.micronaut.expressions.parser.ast.ExpressionNode; import io.micronaut.expressions.parser.compilation.ExpressionVisitorContext; -import io.micronaut.expressions.parser.exception.ExpressionCompilationException; -import org.objectweb.asm.Label; import org.objectweb.asm.Type; -import org.objectweb.asm.commons.GeneratorAdapter; -import static io.micronaut.expressions.parser.ast.util.TypeDescriptors.BOOLEAN; -import static io.micronaut.expressions.parser.ast.util.TypeDescriptors.DOUBLE; -import static io.micronaut.expressions.parser.ast.util.TypeDescriptors.FLOAT; -import static io.micronaut.expressions.parser.ast.util.TypeDescriptors.LONG; -import static io.micronaut.expressions.parser.ast.util.TypeDescriptors.computeNumericOperationTargetType; import static io.micronaut.expressions.parser.ast.util.TypeDescriptors.isNumeric; -import static io.micronaut.expressions.parser.ast.util.TypeDescriptors.isOneOf; -import static io.micronaut.expressions.parser.ast.util.EvaluatedExpressionCompilationUtils.pushPrimitiveCastIfNecessary; -import static io.micronaut.expressions.parser.ast.util.EvaluatedExpressionCompilationUtils.pushUnboxPrimitiveIfNecessary; -import static org.objectweb.asm.Opcodes.DCMPL; -import static org.objectweb.asm.Opcodes.FCMPL; -import static org.objectweb.asm.Opcodes.GOTO; -import static org.objectweb.asm.Opcodes.LCMP; -import static org.objectweb.asm.Type.BOOLEAN_TYPE; /** - * Abstract expression AST node for relational operators. + * Abstract expression AST node for relational operations. Relational operations can + * be applied to numeric types or types that are {@link Comparable} to each other. It + * is unclear at AST building stage what kind of relational operation will be performed, so + * this node does not directly include bytecode generation logic. At type resolution stage it + * instantiates either {@link NumericComparisonOperation} or {@link ComparablesComparisonOperation} + * and delegates bytecode generation to respective node instance. * * @author Sergey Gavrilov * @since 4.0.0 */ @Internal -public abstract sealed class RelationalOperator extends BinaryOperator permits GtOperator, +public abstract sealed class RelationalOperator extends ExpressionNode permits GtOperator, GteOperator, LtOperator, LteOperator { + protected final ExpressionNode leftOperand; + protected final ExpressionNode rightOperand; + + private ExpressionNode comparisonOperation; + public RelationalOperator(ExpressionNode leftOperand, ExpressionNode rightOperand) { - super(leftOperand, rightOperand); - this.nodeType = BOOLEAN; + this.leftOperand = leftOperand; + this.rightOperand = rightOperand; } - @Override - protected Type resolveOperationType(Type leftOperandType, - Type rightOperandType) { - if (!isNumeric(leftOperandType) || !isNumeric(rightOperandType)) { - throw new ExpressionCompilationException("Relational operation can only be applied to" + - " numeric types"); - } + protected abstract Integer intComparisonOpcode(); - return BOOLEAN_TYPE; - } + protected abstract Integer nonIntComparisonOpcode(); @Override - public void generateBytecode(ExpressionVisitorContext ctx) { - GeneratorAdapter mv = ctx.methodVisitor(); - + protected Type doResolveType(ExpressionVisitorContext ctx) { Type leftType = leftOperand.resolveType(ctx); Type rightType = rightOperand.resolveType(ctx); - Type targetType = computeNumericOperationTargetType(leftType, rightType); - - leftOperand.compile(ctx); - pushUnboxPrimitiveIfNecessary(leftType, mv); - pushPrimitiveCastIfNecessary(leftType, targetType, mv); - - rightOperand.compile(ctx); - pushUnboxPrimitiveIfNecessary(rightType, mv); - pushPrimitiveCastIfNecessary(rightType, targetType, mv); - - Label elseLabel = new Label(); - Label endOfCmpLabel = new Label(); - - if (isOneOf(targetType, DOUBLE, FLOAT, LONG)) { - String targetDescriptor = targetType.getDescriptor(); - switch (targetDescriptor) { - case "D" -> mv.visitInsn(DCMPL); - case "F" -> mv.visitInsn(FCMPL); - case "J" -> mv.visitInsn(LCMP); - default -> { } - } - mv.visitJumpInsn(nonIntComparisonOpcode(), elseLabel); + if (isNumeric(leftType) && isNumeric(rightType)) { + comparisonOperation = new NumericComparisonOperation( + leftOperand, rightOperand, + intComparisonOpcode(), nonIntComparisonOpcode()); } else { - mv.visitJumpInsn(intComparisonOpcode(), elseLabel); + comparisonOperation = new ComparablesComparisonOperation( + leftOperand, rightOperand, nonIntComparisonOpcode()); } - mv.push(true); - mv.visitJumpInsn(GOTO, endOfCmpLabel); - mv.visitLabel(elseLabel); - mv.push(false); - mv.visitLabel(endOfCmpLabel); + return comparisonOperation.resolveType(ctx); } - protected abstract Integer intComparisonOpcode(); - - protected abstract Integer nonIntComparisonOpcode(); + @Override + public void generateBytecode(ExpressionVisitorContext ctx) { + comparisonOperation.compile(ctx); + } } diff --git a/inject-groovy-test/src/main/groovy/io/micronaut/ast/transform/test/AbstractEvaluatedExpressionsSpec.groovy b/inject-groovy-test/src/main/groovy/io/micronaut/ast/transform/test/AbstractEvaluatedExpressionsSpec.groovy index 85bc2caa457..3ba8fc661e8 100644 --- a/inject-groovy-test/src/main/groovy/io/micronaut/ast/transform/test/AbstractEvaluatedExpressionsSpec.groovy +++ b/inject-groovy-test/src/main/groovy/io/micronaut/ast/transform/test/AbstractEvaluatedExpressionsSpec.groovy @@ -64,6 +64,53 @@ class AbstractEvaluatedExpressionsSpec extends AbstractBeanDefinitionSpec { return result } + Object evaluateMultipleAgainstContext(@Language("groovy") String contextClass, String... expressions) { + + String expr = "" + for (int i = 0; i < expressions.length; i++) { + expr += """ + + @Value("${expressions[i]}") + public Object field${i} + + """ + } + + def cls = """ + package test; + import io.micronaut.context.annotation.Value; + import jakarta.inject.Singleton; + + ${contextClass} + + @Singleton + class Expr { + ${expr} + } + """.stripIndent().stripLeading() + + def applicationContext = buildContext(cls) + def classLoader = applicationContext.classLoader + + def exprClassName = 'test.$Expr$Expr' + def startingIndex = EvaluatedExpressionReference.nextIndex(exprClassName) - expressions.length + + List result = new ArrayList<>() + for (int i = startingIndex; i < startingIndex + expressions.size(); i++) { + String exprFullName = exprClassName + i + try { + def exprClass = (AbstractEvaluatedExpression) classLoader.loadClass(exprFullName).newInstance() + result.add(exprClass.evaluate(new DefaultExpressionEvaluationContext(null, null, applicationContext, + null))) + } catch (ClassNotFoundException e) { + return null + } + } + + return result + } + + Object evaluate(String expression) { return evaluateAgainstContext(expression, "") } diff --git a/inject-groovy/src/test/groovy/io/micronaut/expressions/OperatorExpressionsSpec.groovy b/inject-groovy/src/test/groovy/io/micronaut/expressions/OperatorExpressionsSpec.groovy index f97f3248d40..104a916335b 100644 --- a/inject-groovy/src/test/groovy/io/micronaut/expressions/OperatorExpressionsSpec.groovy +++ b/inject-groovy/src/test/groovy/io/micronaut/expressions/OperatorExpressionsSpec.groovy @@ -591,6 +591,243 @@ class OperatorExpressionsSpec extends AbstractEvaluatedExpressionsSpec { results[23] instanceof Boolean && results[23] == true } + void "test comparables"() { + given: + Object[] results = evaluateMultipleAgainstContext(""" + + record NonComparable(int value) {} + + class Comp1 implements Comparable { + + private final int value + + Comp1(int value) { + this.value = value + } + + @Override + int compareTo(Comp1 o) { + return value - o.value + } + } + + class InheritedComp extends Comp1 { + InheritedComp(int value) { + super(value) + } + } + + class Comp2 implements Comparable { + private final int value + + Comp2(int value) { + this.value = value + } + + @Override + int compareTo(NonComparable o) { + return value - o.value() + } + + } + + class Comp3 implements Comparable { + + private final int value + + Comp3(int value) { + this.value = value + } + + @Override + int compareTo(Integer i) { + return value - i + } + + } + + class Comp4 implements Comparable { + + private final int value + + Comp4(int value) { + this.value = value + } + + @Override + int compareTo(Object i) { + return value - (Integer) i + } + } + + @jakarta.inject.Singleton + class Context { + + NonComparable nonComp(int value) { + return new NonComparable(value) + } + + InheritedComp inheritedComp(int value) { + return new InheritedComp(value) + } + + Comparable compInterface(int value) { + return new Comp1(value) + } + + Comp1 comp1(int value) { + return new Comp1(value) + } + + Comp2 comp2(int value) { + return new Comp2(value) + } + + Comp3 comp3(int value) { + return new Comp3(value) + } + + Comp4 comp4(int value) { + return new Comp4(value) + } + } + + """, + // comparable to itself + "#{ comp1(10) > comp1(7) }", // 0 + "#{ comp1(10) < comp1(7) }", // 1 + "#{ comp1(10) <= comp1(7) }", // 2 + "#{ comp1(7) >= comp1(7) }", // 3 + "#{ comp1(7) < comp1(8) }", // 4 + "#{ comp1(7) <= comp1(8) }", // 5 + "#{ comp1(7) > comp1(8) }", // 6 + "#{ comp1(7) >= comp1(8) }", // 7 + + // left to right comparable + "#{ comp2(10) > nonComp(7) }", // 8 + "#{ comp2(10) < nonComp(7) }", // 9 + "#{ comp2(10) <= nonComp(7) }", // 10 + "#{ comp2(7) >= nonComp(7) }", // 11 + "#{ comp2(7) < nonComp(8) }", // 12 + "#{ comp2(7) <= nonComp(8) }", // 13 + "#{ comp2(7) > nonComp(8) }", // 14 + "#{ comp2(7) >= nonComp(8) }", // 15 + + // right to left comparable + "#{ nonComp(10) > comp2(7) }", // 16 + "#{ nonComp(10) < comp2(7) }", // 17 + "#{ nonComp(10) <= comp2(7) }", // 18 + "#{ nonComp(7) >= comp2(7) }", // 19 + "#{ nonComp(7) < comp2(8) }", // 20 + "#{ nonComp(7) <= comp2(8) }", // 21 + "#{ nonComp(7) > comp2(8) }", // 22 + "#{ nonComp(7) >= comp2(8) }", // 23 + + // comparable to primitive + "#{ comp3(10) > 7 }", // 24 + "#{ comp3(10) < 7 }", // 25 + "#{ comp3(10) <= 7 }", // 26 + "#{ comp3(7) >= 7 }", // 27 + "#{ comp3(7) < 8 }", // 28 + "#{ comp3(7) <= 8 }", // 29 + "#{ comp3(7) > 8 }", // 30 + "#{ comp3(7) >= 8 }", // 31 + + // primitive to comparable + "#{ 10 > comp3(7) }", // 32 + "#{ 10 < comp3(7) }", // 33 + "#{ 10 <= comp3(7) }", // 34 + "#{ 7 >= comp3(7) }", // 35 + "#{ 7 < comp3(8) }", // 36 + "#{ 7 <= comp3(8) }", // 37 + "#{ 7 > comp3(8) }", // 38 + "#{ 7 >= comp3(8) }", // 39 + + // inherited comparable + "#{ comp1(10) > inheritedComp(7) }", // 40 + "#{ comp1(10) < inheritedComp(7) }", // 41 + "#{ inheritedComp(10) > comp1(7) }", // 42 + "#{ inheritedComp(10) < comp1(7) }", // 43 + + // interface comparable + "#{ compInterface(10) < comp1(7) }", // 44 + "#{ compInterface(10) > comp1(7) }", // 45 + "#{ comp1(10) < compInterface(7) }", // 46 + "#{ comp1(10) > compInterface(7) }", // 47 + + // raw comparable + "#{ comp4(10) > 7 }", // 48 + "#{ 7 > comp4(10) }" // 49 + ) + + expect: + // comparable to itself + results[0] instanceof Boolean && results[0] == true + results[1] instanceof Boolean && results[1] == false + results[2] instanceof Boolean && results[2] == false + results[3] instanceof Boolean && results[3] == true + results[4] instanceof Boolean && results[4] == true + results[5] instanceof Boolean && results[5] == true + results[6] instanceof Boolean && results[6] == false + results[7] instanceof Boolean && results[7] == false + + // left to right comparable + results[8] instanceof Boolean && results[8] == true + results[9] instanceof Boolean && results[9] == false + results[10] instanceof Boolean && results[10] == false + results[11] instanceof Boolean && results[11] == true + results[12] instanceof Boolean && results[12] == true + results[13] instanceof Boolean && results[13] == true + results[14] instanceof Boolean && results[14] == false + results[15] instanceof Boolean && results[15] == false + + // right to left comparable + results[16] instanceof Boolean && results[16] == true + results[17] instanceof Boolean && results[17] == false + results[18] instanceof Boolean && results[18] == false + results[19] instanceof Boolean && results[19] == true + results[20] instanceof Boolean && results[20] == true + results[21] instanceof Boolean && results[21] == true + results[22] instanceof Boolean && results[22] == false + results[23] instanceof Boolean && results[23] == false + + // comparable to primitive + results[24] instanceof Boolean && results[24] == true + results[25] instanceof Boolean && results[25] == false + results[26] instanceof Boolean && results[26] == false + results[27] instanceof Boolean && results[27] == true + results[28] instanceof Boolean && results[28] == true + results[29] instanceof Boolean && results[29] == true + results[30] instanceof Boolean && results[30] == false + results[31] instanceof Boolean && results[31] == false + + // primitive to comparable + results[32] instanceof Boolean && results[32] == true + results[33] instanceof Boolean && results[33] == false + results[34] instanceof Boolean && results[34] == false + results[35] instanceof Boolean && results[35] == true + results[36] instanceof Boolean && results[36] == true + results[37] instanceof Boolean && results[37] == true + results[38] instanceof Boolean && results[38] == false + results[39] instanceof Boolean && results[39] == false + + // inherited comparable + results[40] instanceof Boolean && results[40] == true + results[41] instanceof Boolean && results[41] == false + results[42] instanceof Boolean && results[42] == true + results[43] instanceof Boolean && results[43] == false + + // comparable interface + results[44] instanceof Boolean && results[44] == false + results[45] instanceof Boolean && results[45] == true + results[46] instanceof Boolean && results[46] == false + results[47] instanceof Boolean && results[47] == true + + // raw comparable + results[48] instanceof Boolean && results[48] == true + results[49] instanceof Boolean && results[49] == false + } + void "test '==' operator"() { given: List results = evaluateMultiple( diff --git a/inject-java-test/src/main/groovy/io/micronaut/annotation/processing/test/AbstractEvaluatedExpressionsSpec.groovy b/inject-java-test/src/main/groovy/io/micronaut/annotation/processing/test/AbstractEvaluatedExpressionsSpec.groovy index e1b60f2ecc2..1f42b15be3b 100644 --- a/inject-java-test/src/main/groovy/io/micronaut/annotation/processing/test/AbstractEvaluatedExpressionsSpec.groovy +++ b/inject-java-test/src/main/groovy/io/micronaut/annotation/processing/test/AbstractEvaluatedExpressionsSpec.groovy @@ -104,6 +104,52 @@ abstract class AbstractEvaluatedExpressionsSpec extends AbstractTypeElementSpec } } + Object evaluateMultipleAgainstContext(@Language("java") String contextClass, String... expressions) { + + String expr = "" + for (int i = 0; i < expressions.length; i++) { + expr += """ + + @Value("${expressions[i]}") + public Object field${i}; + + """ + } + + def cls = """ + package test; + import io.micronaut.context.annotation.Value; + import jakarta.inject.Singleton; + + ${contextClass} + + @Singleton + class Expr { + ${expr} + } + """.stripIndent().stripLeading() + + def applicationContext = buildContext(cls) + def classLoader = applicationContext.classLoader + + def exprClassName = 'test.$Expr$Expr' + def startingIndex = EvaluatedExpressionReference.nextIndex(exprClassName) - expressions.length + + List result = new ArrayList<>() + for (int i = startingIndex; i < startingIndex + expressions.size(); i++) { + String exprFullName = exprClassName + i + try { + def exprClass = (AbstractEvaluatedExpression) classLoader.loadClass(exprFullName).newInstance() + result.add(exprClass.evaluate(new DefaultExpressionEvaluationContext(null, null, applicationContext, + null))) + } catch (ClassNotFoundException e) { + return null + } + } + + return result + } + Object evaluateSingle(String className, @Language("java") String cls) { return evaluateSingle(className, cls, null) diff --git a/inject-java/src/test/groovy/io/micronaut/expressions/OperatorExpressionSpec.groovy b/inject-java/src/test/groovy/io/micronaut/expressions/OperatorExpressionSpec.groovy index 037fb7f81eb..130a8fc86b1 100644 --- a/inject-java/src/test/groovy/io/micronaut/expressions/OperatorExpressionSpec.groovy +++ b/inject-java/src/test/groovy/io/micronaut/expressions/OperatorExpressionSpec.groovy @@ -590,6 +590,243 @@ class OperatorExpressionSpec extends AbstractEvaluatedExpressionsSpec { results[23] instanceof Boolean && results[23] == true } + void "test comparables"() { + given: + Object[] results = evaluateMultipleAgainstContext(""" + + record NonComparable(int value) {} + + class Comp1 implements Comparable { + + private final int value; + + public Comp1(int value) { + this.value = value; + } + + @Override + public int compareTo(Comp1 o) { + return value - o.value; + } + } + + class InheritedComp extends Comp1 { + public InheritedComp(int value) { + super(value); + } + } + + class Comp2 implements Comparable { + private final int value; + + public Comp2(int value) { + this.value = value; + } + + @Override + public int compareTo(NonComparable o) { + return value - o.value(); + } + + } + + class Comp3 implements Comparable { + + private final int value; + + public Comp3(int value) { + this.value = value; + } + + @Override + public int compareTo(Integer i) { + return value - i; + } + + } + + class Comp4 implements Comparable { + + private final int value; + + public Comp4(int value) { + this.value = value; + } + + @Override + public int compareTo(Object i) { + return value - (Integer) i; + } + } + + @jakarta.inject.Singleton + class Context { + + public NonComparable nonComp(int value) { + return new NonComparable(value); + } + + public InheritedComp inheritedComp(int value) { + return new InheritedComp(value); + } + + public Comparable compInterface(int value) { + return new Comp1(value); + } + + public Comp1 comp1(int value) { + return new Comp1(value); + } + + public Comp2 comp2(int value) { + return new Comp2(value); + } + + public Comp3 comp3(int value) { + return new Comp3(value); + } + + public Comp4 comp4(int value) { + return new Comp4(value); + } + } + + """, + // comparable to itself + "#{ comp1(10) > comp1(7) }", // 0 + "#{ comp1(10) < comp1(7) }", // 1 + "#{ comp1(10) <= comp1(7) }", // 2 + "#{ comp1(7) >= comp1(7) }", // 3 + "#{ comp1(7) < comp1(8) }", // 4 + "#{ comp1(7) <= comp1(8) }", // 5 + "#{ comp1(7) > comp1(8) }", // 6 + "#{ comp1(7) >= comp1(8) }", // 7 + + // left to right comparable + "#{ comp2(10) > nonComp(7) }", // 8 + "#{ comp2(10) < nonComp(7) }", // 9 + "#{ comp2(10) <= nonComp(7) }", // 10 + "#{ comp2(7) >= nonComp(7) }", // 11 + "#{ comp2(7) < nonComp(8) }", // 12 + "#{ comp2(7) <= nonComp(8) }", // 13 + "#{ comp2(7) > nonComp(8) }", // 14 + "#{ comp2(7) >= nonComp(8) }", // 15 + + // right to left comparable + "#{ nonComp(10) > comp2(7) }", // 16 + "#{ nonComp(10) < comp2(7) }", // 17 + "#{ nonComp(10) <= comp2(7) }", // 18 + "#{ nonComp(7) >= comp2(7) }", // 19 + "#{ nonComp(7) < comp2(8) }", // 20 + "#{ nonComp(7) <= comp2(8) }", // 21 + "#{ nonComp(7) > comp2(8) }", // 22 + "#{ nonComp(7) >= comp2(8) }", // 23 + + // comparable to primitive + "#{ comp3(10) > 7 }", // 24 + "#{ comp3(10) < 7 }", // 25 + "#{ comp3(10) <= 7 }", // 26 + "#{ comp3(7) >= 7 }", // 27 + "#{ comp3(7) < 8 }", // 28 + "#{ comp3(7) <= 8 }", // 29 + "#{ comp3(7) > 8 }", // 30 + "#{ comp3(7) >= 8 }", // 31 + + // primitive to comparable + "#{ 10 > comp3(7) }", // 32 + "#{ 10 < comp3(7) }", // 33 + "#{ 10 <= comp3(7) }", // 34 + "#{ 7 >= comp3(7) }", // 35 + "#{ 7 < comp3(8) }", // 36 + "#{ 7 <= comp3(8) }", // 37 + "#{ 7 > comp3(8) }", // 38 + "#{ 7 >= comp3(8) }", // 39 + + // inherited comparable + "#{ comp1(10) > inheritedComp(7) }", // 40 + "#{ comp1(10) < inheritedComp(7) }", // 41 + "#{ inheritedComp(10) > comp1(7) }", // 42 + "#{ inheritedComp(10) < comp1(7) }", // 43 + + // interface comparable + "#{ compInterface(10) < comp1(7) }", // 44 + "#{ compInterface(10) > comp1(7) }", // 45 + "#{ comp1(10) < compInterface(7) }", // 46 + "#{ comp1(10) > compInterface(7) }", // 47 + + // raw comparable + "#{ comp4(10) > 7 }", // 48 + "#{ 7 > comp4(10) }" // 49 + ) + + expect: + // comparable to itself + results[0] instanceof Boolean && results[0] == true + results[1] instanceof Boolean && results[1] == false + results[2] instanceof Boolean && results[2] == false + results[3] instanceof Boolean && results[3] == true + results[4] instanceof Boolean && results[4] == true + results[5] instanceof Boolean && results[5] == true + results[6] instanceof Boolean && results[6] == false + results[7] instanceof Boolean && results[7] == false + + // left to right comparable + results[8] instanceof Boolean && results[8] == true + results[9] instanceof Boolean && results[9] == false + results[10] instanceof Boolean && results[10] == false + results[11] instanceof Boolean && results[11] == true + results[12] instanceof Boolean && results[12] == true + results[13] instanceof Boolean && results[13] == true + results[14] instanceof Boolean && results[14] == false + results[15] instanceof Boolean && results[15] == false + + // right to left comparable + results[16] instanceof Boolean && results[16] == true + results[17] instanceof Boolean && results[17] == false + results[18] instanceof Boolean && results[18] == false + results[19] instanceof Boolean && results[19] == true + results[20] instanceof Boolean && results[20] == true + results[21] instanceof Boolean && results[21] == true + results[22] instanceof Boolean && results[22] == false + results[23] instanceof Boolean && results[23] == false + + // comparable to primitive + results[24] instanceof Boolean && results[24] == true + results[25] instanceof Boolean && results[25] == false + results[26] instanceof Boolean && results[26] == false + results[27] instanceof Boolean && results[27] == true + results[28] instanceof Boolean && results[28] == true + results[29] instanceof Boolean && results[29] == true + results[30] instanceof Boolean && results[30] == false + results[31] instanceof Boolean && results[31] == false + + // primitive to comparable + results[32] instanceof Boolean && results[32] == true + results[33] instanceof Boolean && results[33] == false + results[34] instanceof Boolean && results[34] == false + results[35] instanceof Boolean && results[35] == true + results[36] instanceof Boolean && results[36] == true + results[37] instanceof Boolean && results[37] == true + results[38] instanceof Boolean && results[38] == false + results[39] instanceof Boolean && results[39] == false + + // inherited comparable + results[40] instanceof Boolean && results[40] == true + results[41] instanceof Boolean && results[41] == false + results[42] instanceof Boolean && results[42] == true + results[43] instanceof Boolean && results[43] == false + + // comparable interface + results[44] instanceof Boolean && results[44] == false + results[45] instanceof Boolean && results[45] == true + results[46] instanceof Boolean && results[46] == false + results[47] instanceof Boolean && results[47] == true + + // raw comparable + results[48] instanceof Boolean && results[48] == true + results[49] instanceof Boolean && results[49] == false + } + void "test '==' operator"() { given: List results = evaluateMultiple( diff --git a/src/main/docs/guide/config/evaluatedExpressions.adoc b/src/main/docs/guide/config/evaluatedExpressions.adoc index b2c9b9b1811..a961257a16d 100644 --- a/src/main/docs/guide/config/evaluatedExpressions.adoc +++ b/src/main/docs/guide/config/evaluatedExpressions.adoc @@ -128,7 +128,8 @@ You can also change evaluation order by using brackets `()`. Equality check is supported for both primitive types and objects. It is performed using `Object.equals()` method. -`>`, `<`, `>=`, `\<=` operations can only be applied to numeric types. +`>`, `<`, `>=`, `\<=` operations can be applied to numeric types or types that implement `java.lang.Comparable` +interface. `matches` keyword can be used to determine whether a string matches provided regular expression which has to be specified as string literal. The regular expression itself will be checked for validity at compilation time. @@ -412,7 +413,7 @@ class ContextConsumer { } ---- -==== Retrieving Beans from Bean Context +=== Retrieving Beans from Bean Context A predefined syntax construct `ctx[...]` can be used to retrieve beans from bean context. The argument inside square brackets has to be a fully qualified class name (note that `T(...)` wrapper is @@ -425,7 +426,7 @@ optional and can be omitted for simplicity). #{ ctx[io.micronaut.example.ContextBean] } ---- -==== Retrieving Environment Properties +=== Retrieving Environment Properties A syntax construct `env[...]` can be used to retrieve environment properties by name. The expression inside square brackets has to resolve to string value, otherwise compilation will fail. If property From a94d133b26d25842156512ae97a4d786ec06a930 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Champeau?= Date: Fri, 28 Apr 2023 12:04:06 +0200 Subject: [PATCH 738/743] Upgrade to latest build plugins (#9171) --- settings.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/settings.gradle b/settings.gradle index 9e7f909202f..fac42dfadf5 100644 --- a/settings.gradle +++ b/settings.gradle @@ -14,7 +14,7 @@ pluginManagement { } plugins { - id 'io.micronaut.build.shared.settings' version '6.3.5' + id 'io.micronaut.build.shared.settings' version '6.4.2' } enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") From 633fc76516c0caa53f35d039ced9bf32b5c702c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Champeau?= Date: Fri, 28 Apr 2023 13:39:39 +0200 Subject: [PATCH 739/743] Align build plugins versions used in `buildSrc` (#9173) Apparently this causes some issues in some environements, probably an ordering problem, but they should always be aligned in any case. --- buildSrc/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/buildSrc/build.gradle b/buildSrc/build.gradle index d75793b323a..1fa66dec90d 100644 --- a/buildSrc/build.gradle +++ b/buildSrc/build.gradle @@ -9,7 +9,7 @@ repositories { dependencies { implementation "org.aim42:htmlSanityCheck:1.1.6" - implementation "io.micronaut.build.internal:micronaut-gradle-plugins:5.3.15" + implementation "io.micronaut.build.internal:micronaut-gradle-plugins:6.4.2" implementation "org.tomlj:tomlj:1.1.0" implementation "me.champeau.gradle:japicmp-gradle-plugin:0.4.1" From 0db6f7d19bda1ee9c7ebd27f16da0daed8b26754 Mon Sep 17 00:00:00 2001 From: Graeme Rocher Date: Fri, 28 Apr 2023 19:30:04 +0200 Subject: [PATCH 740/743] Sure JsonNode is serializable/deserializable by default mapper (#9175) --- .../micronaut/jackson/ObjectMapperFactory.java | 18 ++++++++++++++++++ .../databind/JacksonDatabindMapper.java | 11 ++++++++++- .../serialize/JsonNodeDeserializer.java | 2 +- .../jackson/serialize/JsonNodeSerializer.java | 2 +- .../databind/JacksonDatabindMapperSpec.groovy | 8 ++++++++ 5 files changed, 38 insertions(+), 3 deletions(-) diff --git a/jackson-databind/src/main/java/io/micronaut/jackson/ObjectMapperFactory.java b/jackson-databind/src/main/java/io/micronaut/jackson/ObjectMapperFactory.java index 3aa1b639ba5..d0a44545798 100644 --- a/jackson-databind/src/main/java/io/micronaut/jackson/ObjectMapperFactory.java +++ b/jackson-databind/src/main/java/io/micronaut/jackson/ObjectMapperFactory.java @@ -99,6 +99,24 @@ public JsonFactory jsonFactory(JacksonConfiguration jacksonConfiguration) { return jsonFactoryBuilder.build(); } + /** + * Set additional serializers. + * @param serializers The serializers + * @since 4.0 + */ + public void setSerializers(JsonSerializer... serializers) { + this.serializers = serializers; + } + + /** + * Set additional deserializers. + * @param deserializers The deserializers + * @since 4.0 + */ + public void setDeserializers(JsonDeserializer... deserializers) { + this.deserializers = deserializers; + } + /** * Builds the core Jackson {@link ObjectMapper} from the optional configuration and {@link JsonFactory}. * diff --git a/jackson-databind/src/main/java/io/micronaut/jackson/databind/JacksonDatabindMapper.java b/jackson-databind/src/main/java/io/micronaut/jackson/databind/JacksonDatabindMapper.java index 5e84d5b621a..ae2b596d4b9 100644 --- a/jackson-databind/src/main/java/io/micronaut/jackson/databind/JacksonDatabindMapper.java +++ b/jackson-databind/src/main/java/io/micronaut/jackson/databind/JacksonDatabindMapper.java @@ -37,6 +37,8 @@ import io.micronaut.jackson.core.parser.JacksonCoreProcessor; import io.micronaut.jackson.core.tree.JsonNodeTreeCodec; import io.micronaut.jackson.core.tree.TreeGenerator; +import io.micronaut.jackson.serialize.JsonNodeDeserializer; +import io.micronaut.jackson.serialize.JsonNodeSerializer; import io.micronaut.json.JsonFeatures; import io.micronaut.json.JsonMapper; import io.micronaut.json.JsonStreamConfig; @@ -79,7 +81,14 @@ public JacksonDatabindMapper(ObjectMapper objectMapper) { @Internal public JacksonDatabindMapper() { - this(new ObjectMapperFactory().objectMapper(null, null)); + this(createDefaultMapper()); + } + + private static ObjectMapper createDefaultMapper() { + ObjectMapperFactory objectMapperFactory = new ObjectMapperFactory(); + objectMapperFactory.setDeserializers(new JsonNodeDeserializer()); + objectMapperFactory.setSerializers(new JsonNodeSerializer()); + return objectMapperFactory.objectMapper(null, null); } @Internal diff --git a/jackson-databind/src/main/java/io/micronaut/jackson/serialize/JsonNodeDeserializer.java b/jackson-databind/src/main/java/io/micronaut/jackson/serialize/JsonNodeDeserializer.java index 69ad58537af..41d2a305543 100644 --- a/jackson-databind/src/main/java/io/micronaut/jackson/serialize/JsonNodeDeserializer.java +++ b/jackson-databind/src/main/java/io/micronaut/jackson/serialize/JsonNodeDeserializer.java @@ -31,7 +31,7 @@ * @since 3.1 */ @Singleton -final class JsonNodeDeserializer extends JsonDeserializer { +public final class JsonNodeDeserializer extends JsonDeserializer { @Override public JsonNode deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { return JsonNodeTreeCodec.getInstance().readTree(p); diff --git a/jackson-databind/src/main/java/io/micronaut/jackson/serialize/JsonNodeSerializer.java b/jackson-databind/src/main/java/io/micronaut/jackson/serialize/JsonNodeSerializer.java index fa413a82e65..9a391aae546 100644 --- a/jackson-databind/src/main/java/io/micronaut/jackson/serialize/JsonNodeSerializer.java +++ b/jackson-databind/src/main/java/io/micronaut/jackson/serialize/JsonNodeSerializer.java @@ -31,7 +31,7 @@ * @since 3.1 */ @Singleton -final class JsonNodeSerializer extends JsonSerializer { +public final class JsonNodeSerializer extends JsonSerializer { @Override public void serialize(JsonNode value, JsonGenerator gen, SerializerProvider serializers) throws IOException { if (value == null) { diff --git a/jackson-databind/src/test/groovy/io/micronaut/jackson/databind/JacksonDatabindMapperSpec.groovy b/jackson-databind/src/test/groovy/io/micronaut/jackson/databind/JacksonDatabindMapperSpec.groovy index a15a432a38e..ae6cb258d2b 100644 --- a/jackson-databind/src/test/groovy/io/micronaut/jackson/databind/JacksonDatabindMapperSpec.groovy +++ b/jackson-databind/src/test/groovy/io/micronaut/jackson/databind/JacksonDatabindMapperSpec.groovy @@ -13,6 +13,14 @@ import io.micronaut.json.tree.JsonNode import spock.lang.Specification class JacksonDatabindMapperSpec extends Specification { + def "test default parsing to JsonNode"() { + given: + def mapper = JsonMapper.createDefault() + + expect: + mapper.readValue('{}', Argument.of(JsonNode)) == JsonNode.createObjectNode([:]) + } + def 'parsing to JsonNode'() { given: def ctx = ApplicationContext.run() From 9e59716c57ecbaa3e5fefd8e9dce0a3b78a89092 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 29 Apr 2023 07:01:30 +0200 Subject: [PATCH 741/743] Update dependency ch.qos.logback:logback-classic to v1.4.7 (#9178) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 49aa966308b..adc0d5daf57 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -32,7 +32,7 @@ jcache = "1.1.1" junit5 = "5.9.2" junit-platform="1.9.2" ktor = "1.6.8" -managed-logback = "1.4.6" +managed-logback = "1.4.7" logbook-netty = "2.14.0" log4j = "2.19.0" micronaut-aws = "3.9.2" From 681d394a08c9a0d6314d6649e2f4451949842eb7 Mon Sep 17 00:00:00 2001 From: micronaut-build <65172877+micronaut-build@users.noreply.github.com> Date: Sat, 29 Apr 2023 08:35:32 +0200 Subject: [PATCH 742/743] Bump micronaut-maven-plugin to 4.0.0-M2 (#9174) --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index e50541116ba..538bd760872 100644 --- a/gradle.properties +++ b/gradle.properties @@ -47,7 +47,7 @@ projectUrl=https://micronaut.io developers=Graeme Rocher # Dependency Versions -micronautMavenPluginVersion=4.0.0-M1 +micronautMavenPluginVersion=4.0.0-M2 chromedriverVersion=79.0.3945.36 geckodriverVersion=0.26.0 webdriverBinariesVersion=1.4 From 4705566ca8504d7d9854905c6d1b7f26293d98f5 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 30 Apr 2023 08:04:19 +0000 Subject: [PATCH 743/743] Update dependency com.github.ben-manes.caffeine:caffeine to v3 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index adc0d5daf57..1f06dbced99 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -3,7 +3,7 @@ asm = "9.4" awaitility = "4.2.0" bcpkix = "1.70" blaze = "1.6.8" -caffeine = "2.9.3" +caffeine = "3.1.6" compile-testing = "0.19" geb = "7.0"